diff --git a/RSA.pm b/RSA.pm index c2d3a3f..8a04ef9 100644 --- a/RSA.pm +++ b/RSA.pm @@ -92,6 +92,7 @@ sub get_key_parameters { } *get_public_key_pkcs1_string = \&get_public_key_string; +*get_public_key_pkcs1_der_string = \&get_public_key_der_string; unless ( defined &use_sslv23_padding ) { *use_sslv23_padding = sub { @@ -322,6 +323,41 @@ the private-key counterpart of C. Accepts the same optional passphrase and cipher-name parameters as C. +=item get_public_key_der_string + +Return the DER-encoded PKCS#1 C representation of the +public key as a binary string. This is the DER equivalent of +C. + +=item get_public_key_pkcs1_der_string + +Alias for C. + +=item get_public_key_x509_der_string + +Return the DER-encoded X.509 C representation +of the public key as a binary string. This is the DER equivalent of +C, and the format produced by +C. + +=item get_private_key_der_string + +Return the DER-encoded PKCS#1 C representation of the +private key as a binary string. This is the DER equivalent of +C (without encryption support, since the +PKCS#1 DER format has no standard encryption wrapper). + +=item get_private_key_pkcs8_der_string + +Return the DER-encoded PKCS#8 C representation of the +private key as a binary string. This is the DER equivalent of +C, and the format produced by +C. + +Accepts the same optional passphrase and cipher-name parameters as +C. When a passphrase is provided, the output +is an C structure. + =item encrypt Encrypt a binary "string" using the public (portion of the) key. diff --git a/RSA.xs b/RSA.xs index 238bebb..b2b68be 100644 --- a/RSA.xs +++ b/RSA.xs @@ -49,6 +49,27 @@ static int _write_pkcs8_pem(BIO* bio, RSA* rsa, const EVP_CIPHER* enc, } #endif +/* Pre-3.x helper for PKCS#8 DER export: wraps RSA* in a real EVP_PKEY + and writes PKCS#8 DER. Placed BEFORE the EVP_PKEY->RSA compatibility + macros so that EVP_PKEY, EVP_PKEY_new, EVP_PKEY_free, and + i2d_PKCS8PrivateKey_bio resolve to their real OpenSSL symbols. */ +#if OPENSSL_VERSION_NUMBER < 0x30000000L +static int _write_pkcs8_der(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 = i2d_PKCS8PrivateKey_bio(bio, pkey, enc, (char*)pass, passlen, NULL, NULL); + EVP_PKEY_free(pkey); + return ok; +} +#endif + /* Pre-3.x helper for loading encrypted PKCS#8 DER private keys. Placed BEFORE the EVP_PKEY->RSA compatibility macros so that EVP_PKEY, EVP_PKEY_free, and EVP_PKEY_get1_RSA resolve to their @@ -751,6 +772,27 @@ get_private_key_string(p_rsa, passphrase_SV=&PL_sv_undef, cipher_name_SV=&PL_sv_ OUTPUT: RETVAL +SV* +get_private_key_der_string(p_rsa) + rsaData* p_rsa; + PREINIT: + BIO* stringBIO; + CODE: + if (!_is_private(p_rsa)) + { + croak("Public keys cannot export private key strings"); + } + CHECK_OPEN_SSL(stringBIO = BIO_new(BIO_s_mem())); +#if OPENSSL_VERSION_NUMBER >= 0x30000000L + CHECK_OPEN_SSL_BIO(i2d_PrivateKey_bio(stringBIO, p_rsa->rsa), stringBIO); +#else + CHECK_OPEN_SSL_BIO(i2d_RSAPrivateKey_bio(stringBIO, p_rsa->rsa), stringBIO); +#endif + RETVAL = extractBioString(stringBIO); + + OUTPUT: + RETVAL + SV* get_private_key_pkcs8_string(p_rsa, passphrase_SV=&PL_sv_undef, cipher_name_SV=&PL_sv_undef) rsaData* p_rsa; @@ -793,6 +835,52 @@ get_private_key_pkcs8_string(p_rsa, passphrase_SV=&PL_sv_undef, cipher_name_SV=& OUTPUT: RETVAL +SV* +get_private_key_pkcs8_der_string(p_rsa, passphrase_SV=&PL_sv_undef, cipher_name_SV=&PL_sv_undef) + rsaData* p_rsa; + SV* passphrase_SV; + SV* cipher_name_SV; + PREINIT: + BIO* stringBIO; + char* passphrase = NULL; + STRLEN passphraseLength = 0; + char* cipher_name; + const EVP_CIPHER* enc = NULL; + CODE: + if (!_is_private(p_rsa)) + { + croak("Public keys cannot export private key strings"); + } + if (SvPOK(cipher_name_SV) && !SvPOK(passphrase_SV)) { + croak("Passphrase is required for cipher"); + } + if (SvPOK(passphrase_SV)) { + passphrase = SvPV(passphrase_SV, passphraseLength); + 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(i2d_PKCS8PrivateKey_bio( + stringBIO, p_rsa->rsa, enc, passphrase, passphraseLength, NULL, NULL), stringBIO); +#else + CHECK_OPEN_SSL_BIO(_write_pkcs8_der( + stringBIO, p_rsa->rsa, enc, (unsigned char*) passphrase, passphraseLength), stringBIO); +#endif + RETVAL = extractBioString(stringBIO); + + OUTPUT: + RETVAL + SV* get_public_key_string(p_rsa) rsaData* p_rsa; @@ -828,6 +916,38 @@ get_public_key_string(p_rsa) OUTPUT: RETVAL +SV* +get_public_key_der_string(p_rsa) + rsaData* p_rsa; + PREINIT: + BIO* stringBIO; +#if OPENSSL_VERSION_NUMBER >= 0x30000000L + OSSL_ENCODER_CTX *ctx = NULL; + int error = 0; +#endif + CODE: + CHECK_OPEN_SSL(stringBIO = BIO_new(BIO_s_mem())); +#if OPENSSL_VERSION_NUMBER >= 0x30000000L + ctx = OSSL_ENCODER_CTX_new_for_pkey(p_rsa->rsa, OSSL_KEYMGMT_SELECT_PUBLIC_KEY, + "DER", "PKCS1", NULL); + THROW(ctx != NULL && OSSL_ENCODER_CTX_get_num_encoders(ctx)); + THROW(OSSL_ENCODER_to_bio(ctx, stringBIO) == 1); + OSSL_ENCODER_CTX_free(ctx); + ctx = NULL; + goto pubkey_pkcs1_der_done; + err: + if (ctx) { OSSL_ENCODER_CTX_free(ctx); ctx = NULL; } + BIO_free(stringBIO); + CHECK_OPEN_SSL(0); + pubkey_pkcs1_der_done: +#else + CHECK_OPEN_SSL_BIO(i2d_RSAPublicKey_bio(stringBIO, p_rsa->rsa), stringBIO); +#endif + RETVAL = extractBioString(stringBIO); + + OUTPUT: + RETVAL + SV* get_public_key_x509_string(p_rsa) rsaData* p_rsa; @@ -841,6 +961,23 @@ get_public_key_x509_string(p_rsa) OUTPUT: RETVAL +SV* +get_public_key_x509_der_string(p_rsa) + rsaData* p_rsa; + PREINIT: + BIO* stringBIO; + CODE: + CHECK_OPEN_SSL(stringBIO = BIO_new(BIO_s_mem())); +#if OPENSSL_VERSION_NUMBER >= 0x30000000L + CHECK_OPEN_SSL_BIO(i2d_PUBKEY_bio(stringBIO, p_rsa->rsa), stringBIO); +#else + CHECK_OPEN_SSL_BIO(i2d_RSA_PUBKEY_bio(stringBIO, p_rsa->rsa), stringBIO); +#endif + RETVAL = extractBioString(stringBIO); + + OUTPUT: + RETVAL + SV* generate_key(proto, bitsSV, exponent = 65537) SV* proto; diff --git a/t/der.t b/t/der.t index 914293f..cf0fe33 100644 --- a/t/der.t +++ b/t/der.t @@ -6,7 +6,7 @@ use Crypt::OpenSSL::RSA; use File::Temp qw(tempfile); -BEGIN { plan tests => 30 } +BEGIN { plan tests => 49 } # --- Generate a key pair for testing --- @@ -179,3 +179,83 @@ SKIP: { like($@, qr/not an RSA key|ASN1|expecting an rsa key/i, "_new_public_key_x509_der gives appropriate error for non-RSA DER key"); } + +# --- DER export tests --- + +# PKCS#1 public key DER export +my $pub_pkcs1_der_export = $rsa->get_public_key_der_string(); +is( ord(substr($pub_pkcs1_der_export, 0, 1)), 0x30, + "get_public_key_der_string starts with SEQUENCE tag" ); +is( $pub_pkcs1_der_export, $pkcs1_der, + "get_public_key_der_string matches pem_to_der of PEM export" ); + +my $pub_from_pkcs1_der_export = Crypt::OpenSSL::RSA->new_public_key($pub_pkcs1_der_export); +$pub_from_pkcs1_der_export->use_sha256_hash(); +ok( $pub_from_pkcs1_der_export->verify($plaintext, $sig), + "PKCS#1 DER export round-trips and verifies" ); + +# Alias test +is( $rsa->get_public_key_pkcs1_der_string(), $pub_pkcs1_der_export, + "get_public_key_pkcs1_der_string is alias for get_public_key_der_string" ); + +# X.509 public key DER export +my $pub_x509_der_export = $rsa->get_public_key_x509_der_string(); +is( ord(substr($pub_x509_der_export, 0, 1)), 0x30, + "get_public_key_x509_der_string starts with SEQUENCE tag" ); +is( $pub_x509_der_export, $x509_der, + "get_public_key_x509_der_string matches pem_to_der of PEM export" ); + +my $pub_from_x509_der_export = Crypt::OpenSSL::RSA->new_public_key($pub_x509_der_export); +$pub_from_x509_der_export->use_sha256_hash(); +ok( $pub_from_x509_der_export->verify($plaintext, $sig), + "X.509 DER export round-trips and verifies" ); + +# PKCS#1 private key DER export +my $priv_pkcs1_der_export = $rsa->get_private_key_der_string(); +is( ord(substr($priv_pkcs1_der_export, 0, 1)), 0x30, + "get_private_key_der_string starts with SEQUENCE tag" ); +is( $priv_pkcs1_der_export, $priv_der, + "get_private_key_der_string matches pem_to_der of PEM export" ); + +my $priv_from_der_export = Crypt::OpenSSL::RSA->new_private_key($priv_pkcs1_der_export); +ok( $priv_from_der_export->is_private(), + "PKCS#1 DER export round-trips as private key" ); +$priv_from_der_export->use_sha256_hash(); +my $sig_from_der_export = $priv_from_der_export->sign($plaintext); +ok( $pub_from_x509_der->verify($plaintext, $sig_from_der_export), + "signature from PKCS#1 DER-exported private key verifies" ); + +# Unencrypted PKCS#8 private key DER export +my $pkcs8_pem = $rsa->get_private_key_pkcs8_string(); +my $pkcs8_der_expected = pem_to_der($pkcs8_pem); +my $pkcs8_der_export = $rsa->get_private_key_pkcs8_der_string(); +is( ord(substr($pkcs8_der_export, 0, 1)), 0x30, + "get_private_key_pkcs8_der_string starts with SEQUENCE tag" ); +is( $pkcs8_der_export, $pkcs8_der_expected, + "get_private_key_pkcs8_der_string matches pem_to_der of PEM export" ); + +my $priv_from_pkcs8_der_export = Crypt::OpenSSL::RSA->new_private_key($pkcs8_der_export); +ok( $priv_from_pkcs8_der_export->is_private(), + "PKCS#8 DER export round-trips as private key" ); + +# Encrypted PKCS#8 DER export +my $der_pass = 'test_export_pass'; +my $enc_pkcs8_der_export = $rsa->get_private_key_pkcs8_der_string($der_pass, 'aes-128-cbc'); +is( ord(substr($enc_pkcs8_der_export, 0, 1)), 0x30, + "encrypted PKCS#8 DER export starts with SEQUENCE tag" ); +my $priv_from_enc_pkcs8_der = Crypt::OpenSSL::RSA->new_private_key($enc_pkcs8_der_export, $der_pass); +ok( $priv_from_enc_pkcs8_der->is_private(), + "encrypted PKCS#8 DER export round-trips with passphrase" ); + +eval { Crypt::OpenSSL::RSA->new_private_key($enc_pkcs8_der_export, 'wrong') }; +ok( $@, "encrypted PKCS#8 DER export rejects wrong passphrase" ); + +# Error: public key cannot export private DER +my $pub_only = Crypt::OpenSSL::RSA->new_public_key($x509_pem); +eval { $pub_only->get_private_key_der_string() }; +like( $@, qr/Public keys cannot/, + "get_private_key_der_string croaks on public-only key" ); + +eval { $pub_only->get_private_key_pkcs8_der_string() }; +like( $@, qr/Public keys cannot/, + "get_private_key_pkcs8_der_string croaks on public-only key" );