Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions RSA.pm
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,20 @@ The cipher algorithm used to protect the private key. Default to

=back

=item get_private_key_pkcs8_string

Return the Base64/DER-encoded PKCS#8 representation of the private
key. This string has header and footer lines:

-----BEGIN PRIVATE KEY-----
-----END PRIVATE KEY-----

This is the format produced by C<openssl pkey -outform PEM>, and is
the private-key counterpart of C<get_public_key_x509_string>.

Accepts the same optional passphrase and cipher-name parameters as
C<get_private_key_string>.

=item encrypt

Encrypt a binary "string" using the public (portion of the) key.
Expand Down
63 changes: 63 additions & 0 deletions RSA.xs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,27 @@
#include <openssl/encoder.h>
#endif

/* Pre-3.x helper for PKCS#8 export: wraps RSA* in a real EVP_PKEY and
writes PKCS#8 PEM. Defined BEFORE the EVP_PKEY->RSA compatibility
macros so that EVP_PKEY, EVP_PKEY_new, EVP_PKEY_free, and
PEM_write_bio_PrivateKey resolve to their real OpenSSL symbols. */
#if OPENSSL_VERSION_NUMBER < 0x30000000L
static int _write_pkcs8_pem(BIO* bio, RSA* rsa, const EVP_CIPHER* enc,
unsigned char* pass, int passlen)
{
EVP_PKEY* pkey = EVP_PKEY_new();
int ok;
if (!pkey) return 0;
if (!EVP_PKEY_set1_RSA(pkey, rsa)) {
EVP_PKEY_free(pkey);
return 0;
}
ok = PEM_write_bio_PrivateKey(bio, pkey, enc, pass, passlen, NULL, NULL);
EVP_PKEY_free(pkey);
return ok;
}
#endif

#if OPENSSL_VERSION_NUMBER >= 0x30000000L
#define UNSIGNED_CHAR unsigned char
#define SIZE_T_INT size_t
Expand Down Expand Up @@ -500,6 +521,48 @@ get_private_key_string(p_rsa, passphase_SV=&PL_sv_undef, cipher_name_SV=&PL_sv_u
OUTPUT:
RETVAL

SV*
get_private_key_pkcs8_string(p_rsa, passphase_SV=&PL_sv_undef, cipher_name_SV=&PL_sv_undef)
rsaData* p_rsa;
SV* passphase_SV;
SV* cipher_name_SV;
PREINIT:
BIO* stringBIO;
char* passphase = NULL;
STRLEN passphaseLength = 0;
char* cipher_name;
const EVP_CIPHER* enc = NULL;
CODE:
if (SvPOK(cipher_name_SV) && !SvPOK(passphase_SV)) {
croak("Passphrase is required for cipher");
}
if (SvPOK(passphase_SV)) {
passphase = SvPV(passphase_SV, passphaseLength);
if (SvPOK(cipher_name_SV)) {
cipher_name = SvPV_nolen(cipher_name_SV);
}
else {
cipher_name = "des3";
}
enc = EVP_get_cipherbyname(cipher_name);
if (enc == NULL) {
croak("Unsupported cipher: %s", cipher_name);
}
}

CHECK_OPEN_SSL(stringBIO = BIO_new(BIO_s_mem()));
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
CHECK_OPEN_SSL_BIO(PEM_write_bio_PrivateKey(
stringBIO, p_rsa->rsa, enc, (unsigned char*) passphase, passphaseLength, NULL, NULL), stringBIO);
#else
CHECK_OPEN_SSL_BIO(_write_pkcs8_pem(
stringBIO, p_rsa->rsa, enc, (unsigned char*) passphase, passphaseLength), stringBIO);
#endif
RETVAL = extractBioString(stringBIO);

OUTPUT:
RETVAL

SV*
get_public_key_string(p_rsa)
rsaData* p_rsa;
Expand Down
49 changes: 48 additions & 1 deletion t/format.t
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ use strict;
use Test::More;

use Crypt::OpenSSL::RSA;
use Crypt::OpenSSL::Guess qw(openssl_version);

BEGIN { plan tests => 39 }
my ($major, $minor, $patch) = openssl_version();

BEGIN { plan tests => 48 }

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

# --- PKCS#8 private key export ---

{
my $rsa = Crypt::OpenSSL::RSA->new_private_key($DECRYPT_PRIVATE_KEY_STRING);
my $pkcs8_pem = $rsa->get_private_key_pkcs8_string();
like($pkcs8_pem, qr/^-----BEGIN PRIVATE KEY-----/m, "PKCS#8 output has correct header");
like($pkcs8_pem, qr/-----END PRIVATE KEY-----\s*$/m, "PKCS#8 output has correct footer");
unlike($pkcs8_pem, qr/BEGIN RSA PRIVATE KEY/, "PKCS#8 output is not PKCS#1 format");

# encrypted PKCS#8 export
my $pass = 'test_pkcs8_pass';
my $enc_pem = $rsa->get_private_key_pkcs8_string($pass, 'aes-128-cbc');
like($enc_pem, qr/^-----BEGIN ENCRYPTED PRIVATE KEY-----/m,
"encrypted PKCS#8 has correct header");

# Round-trip tests require new_private_key to read PKCS#8. On pre-3.x
# PEM_read_bio_PrivateKey is macro'd to PEM_read_bio_RSAPrivateKey which
# only reads PKCS#1, so these must be skipped.
SKIP: {
skip "new_private_key cannot read PKCS#8 on OpenSSL < 3.x", 3
if $major < 3;

my $reimported = Crypt::OpenSSL::RSA->new_private_key($pkcs8_pem);
is($reimported->get_private_key_string(), $DECRYPT_PRIVATE_KEY_STRING,
"PKCS#8 round-trip: re-import then export as PKCS#1 matches original");
is($reimported->get_private_key_pkcs8_string(), $pkcs8_pem,
"PKCS#8 round-trip: re-export as PKCS#8 matches");

my $dec_rsa = Crypt::OpenSSL::RSA->new_private_key($enc_pem, $pass);
is($dec_rsa->get_private_key_string(), $DECRYPT_PRIVATE_KEY_STRING,
"encrypted PKCS#8 round-trip decrypts to original key");
}

# error: cipher without passphrase
eval { $rsa->get_private_key_pkcs8_string(undef, 'des3') };
like($@, qr/Passphrase is required for cipher/,
"get_private_key_pkcs8_string croaks when cipher given without passphrase");

# error: unsupported cipher
eval { $rsa->get_private_key_pkcs8_string($pass, 'bogus-cipher-xyz') };
like($@, qr/Unsupported cipher/,
"get_private_key_pkcs8_string croaks on unsupported cipher");
}

# --- X509 public key from private key matches PKCS1 ---

my $priv_for_x509 = Crypt::OpenSSL::RSA->new_private_key($PRIVATE_KEY_STRING);
Expand Down
Loading