Skip to content

Commit ed770cf

Browse files
authored
Merge pull request #124 from atoomic/koan.atoomic/add-pkcs8-private-key-export
feat: add get_private_key_pkcs8_string()
2 parents 67199f4 + 11d2db5 commit ed770cf

3 files changed

Lines changed: 125 additions & 1 deletion

File tree

RSA.pm

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,20 @@ The cipher algorithm used to protect the private key. Default to
304304
305305
=back
306306
307+
=item get_private_key_pkcs8_string
308+
309+
Return the Base64/DER-encoded PKCS#8 representation of the private
310+
key. This string has header and footer lines:
311+
312+
-----BEGIN PRIVATE KEY-----
313+
-----END PRIVATE KEY-----
314+
315+
This is the format produced by C<openssl pkey -outform PEM>, and is
316+
the private-key counterpart of C<get_public_key_x509_string>.
317+
318+
Accepts the same optional passphrase and cipher-name parameters as
319+
C<get_private_key_string>.
320+
307321
=item encrypt
308322
309323
Encrypt a binary "string" using the public (portion of the) key.

RSA.xs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,27 @@
2828
#include <openssl/decoder.h>
2929
#endif
3030

31+
/* Pre-3.x helper for PKCS#8 export: wraps RSA* in a real EVP_PKEY and
32+
writes PKCS#8 PEM. Defined BEFORE the EVP_PKEY->RSA compatibility
33+
macros so that EVP_PKEY, EVP_PKEY_new, EVP_PKEY_free, and
34+
PEM_write_bio_PrivateKey resolve to their real OpenSSL symbols. */
35+
#if OPENSSL_VERSION_NUMBER < 0x30000000L
36+
static int _write_pkcs8_pem(BIO* bio, RSA* rsa, const EVP_CIPHER* enc,
37+
unsigned char* pass, int passlen)
38+
{
39+
EVP_PKEY* pkey = EVP_PKEY_new();
40+
int ok;
41+
if (!pkey) return 0;
42+
if (!EVP_PKEY_set1_RSA(pkey, rsa)) {
43+
EVP_PKEY_free(pkey);
44+
return 0;
45+
}
46+
ok = PEM_write_bio_PrivateKey(bio, pkey, enc, pass, passlen, NULL, NULL);
47+
EVP_PKEY_free(pkey);
48+
return ok;
49+
}
50+
#endif
51+
3152
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
3253
#define UNSIGNED_CHAR unsigned char
3354
#define SIZE_T_INT size_t
@@ -662,6 +683,48 @@ get_private_key_string(p_rsa, passphase_SV=&PL_sv_undef, cipher_name_SV=&PL_sv_u
662683
OUTPUT:
663684
RETVAL
664685

686+
SV*
687+
get_private_key_pkcs8_string(p_rsa, passphase_SV=&PL_sv_undef, cipher_name_SV=&PL_sv_undef)
688+
rsaData* p_rsa;
689+
SV* passphase_SV;
690+
SV* cipher_name_SV;
691+
PREINIT:
692+
BIO* stringBIO;
693+
char* passphase = NULL;
694+
STRLEN passphaseLength = 0;
695+
char* cipher_name;
696+
const EVP_CIPHER* enc = NULL;
697+
CODE:
698+
if (SvPOK(cipher_name_SV) && !SvPOK(passphase_SV)) {
699+
croak("Passphrase is required for cipher");
700+
}
701+
if (SvPOK(passphase_SV)) {
702+
passphase = SvPV(passphase_SV, passphaseLength);
703+
if (SvPOK(cipher_name_SV)) {
704+
cipher_name = SvPV_nolen(cipher_name_SV);
705+
}
706+
else {
707+
cipher_name = "des3";
708+
}
709+
enc = EVP_get_cipherbyname(cipher_name);
710+
if (enc == NULL) {
711+
croak("Unsupported cipher: %s", cipher_name);
712+
}
713+
}
714+
715+
CHECK_OPEN_SSL(stringBIO = BIO_new(BIO_s_mem()));
716+
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
717+
CHECK_OPEN_SSL_BIO(PEM_write_bio_PrivateKey(
718+
stringBIO, p_rsa->rsa, enc, (unsigned char*) passphase, passphaseLength, NULL, NULL), stringBIO);
719+
#else
720+
CHECK_OPEN_SSL_BIO(_write_pkcs8_pem(
721+
stringBIO, p_rsa->rsa, enc, (unsigned char*) passphase, passphaseLength), stringBIO);
722+
#endif
723+
RETVAL = extractBioString(stringBIO);
724+
725+
OUTPUT:
726+
RETVAL
727+
665728
SV*
666729
get_public_key_string(p_rsa)
667730
rsaData* p_rsa;

t/format.t

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ use strict;
22
use Test::More;
33

44
use Crypt::OpenSSL::RSA;
5+
use Crypt::OpenSSL::Guess qw(openssl_version);
56

6-
BEGIN { plan tests => 39 }
7+
my ($major, $minor, $patch) = openssl_version();
8+
9+
BEGIN { plan tests => 48 }
710

811
my $PRIVATE_KEY_STRING = <<EOF;
912
-----BEGIN RSA PRIVATE KEY-----
@@ -148,6 +151,50 @@ like($@, qr/unrecognized key format/, "new_public_key croaks on certificate PEM
148151
eval { Crypt::OpenSSL::RSA->new_public_key("not a PEM key at all") };
149152
like($@, qr/unrecognized key format/, "new_public_key croaks on non-PEM input");
150153

154+
# --- PKCS#8 private key export ---
155+
156+
{
157+
my $rsa = Crypt::OpenSSL::RSA->new_private_key($DECRYPT_PRIVATE_KEY_STRING);
158+
my $pkcs8_pem = $rsa->get_private_key_pkcs8_string();
159+
like($pkcs8_pem, qr/^-----BEGIN PRIVATE KEY-----/m, "PKCS#8 output has correct header");
160+
like($pkcs8_pem, qr/-----END PRIVATE KEY-----\s*$/m, "PKCS#8 output has correct footer");
161+
unlike($pkcs8_pem, qr/BEGIN RSA PRIVATE KEY/, "PKCS#8 output is not PKCS#1 format");
162+
163+
# encrypted PKCS#8 export
164+
my $pass = 'test_pkcs8_pass';
165+
my $enc_pem = $rsa->get_private_key_pkcs8_string($pass, 'aes-128-cbc');
166+
like($enc_pem, qr/^-----BEGIN ENCRYPTED PRIVATE KEY-----/m,
167+
"encrypted PKCS#8 has correct header");
168+
169+
# Round-trip tests require new_private_key to read PKCS#8. On pre-3.x
170+
# PEM_read_bio_PrivateKey is macro'd to PEM_read_bio_RSAPrivateKey which
171+
# only reads PKCS#1, so these must be skipped.
172+
SKIP: {
173+
skip "new_private_key cannot read PKCS#8 on OpenSSL < 3.x", 3
174+
if $major < 3;
175+
176+
my $reimported = Crypt::OpenSSL::RSA->new_private_key($pkcs8_pem);
177+
is($reimported->get_private_key_string(), $DECRYPT_PRIVATE_KEY_STRING,
178+
"PKCS#8 round-trip: re-import then export as PKCS#1 matches original");
179+
is($reimported->get_private_key_pkcs8_string(), $pkcs8_pem,
180+
"PKCS#8 round-trip: re-export as PKCS#8 matches");
181+
182+
my $dec_rsa = Crypt::OpenSSL::RSA->new_private_key($enc_pem, $pass);
183+
is($dec_rsa->get_private_key_string(), $DECRYPT_PRIVATE_KEY_STRING,
184+
"encrypted PKCS#8 round-trip decrypts to original key");
185+
}
186+
187+
# error: cipher without passphrase
188+
eval { $rsa->get_private_key_pkcs8_string(undef, 'des3') };
189+
like($@, qr/Passphrase is required for cipher/,
190+
"get_private_key_pkcs8_string croaks when cipher given without passphrase");
191+
192+
# error: unsupported cipher
193+
eval { $rsa->get_private_key_pkcs8_string($pass, 'bogus-cipher-xyz') };
194+
like($@, qr/Unsupported cipher/,
195+
"get_private_key_pkcs8_string croaks on unsupported cipher");
196+
}
197+
151198
# --- X509 public key from private key matches PKCS1 ---
152199

153200
my $priv_for_x509 = Crypt::OpenSSL::RSA->new_private_key($PRIVATE_KEY_STRING);

0 commit comments

Comments
 (0)