From 2283c750c267dc05ae857af974588de9e576c02f Mon Sep 17 00:00:00 2001 From: Toddr Bot Date: Thu, 23 Apr 2026 13:41:29 +0000 Subject: [PATCH] Add passphrase support for DER-encoded private keys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _new_private_key_der() now accepts an optional passphrase to decrypt encrypted PKCS#8 DER (EncryptedPrivateKeyInfo) private keys. On OpenSSL 3.x, sets OSSL_DECODER_CTX_set_passphrase() on the existing decoder context. On pre-3.x, uses d2i_PKCS8PrivateKey_bio() via a helper placed before the EVP_PKEY->RSA compatibility macros. Addresses review feedback on PR #176 — rather than croaking when a passphrase is provided for DER keys, we now support the use case. Co-Authored-By: Claude Opus 4.6 --- RSA.pm | 11 +++++++---- RSA.xs | 39 +++++++++++++++++++++++++++++++++++++-- t/der.t | 29 ++++++++++++++++++++++++++++- 3 files changed, 72 insertions(+), 7 deletions(-) diff --git a/RSA.pm b/RSA.pm index 108f1f8..2e03653 100644 --- a/RSA.pm +++ b/RSA.pm @@ -63,7 +63,7 @@ sub new_private_key { } elsif ( substr($p_key_string, 0, 1) eq "\x30" ) { # ASN.1 SEQUENCE tag detected — likely DER-encoded private key. - return $proto->_new_private_key_der($p_key_string); + return $proto->_new_private_key_der($p_key_string, @rest); } else { croak "unrecognized key format: expected PEM-encoded key (starting with '-----BEGIN') " @@ -193,15 +193,18 @@ be changed with C. DER-encoded keys (raw binary ASN.1) are also accepted. -An optional parameter can be passed for passphrase-protected PEM private +An optional parameter can be passed for passphrase-protected private keys: =over =item passphrase -The passphrase which protects the private key. Note: passphrase -protection is only supported for PEM-encoded keys. +The passphrase which protects the private key. For PEM keys, this +decrypts traditional encrypted PEM (C header) and encrypted +PKCS#8 PEM (C). For DER keys, this +decrypts encrypted PKCS#8 DER (C ASN.1 +structure). =back diff --git a/RSA.xs b/RSA.xs index 22ea5dc..238bebb 100644 --- a/RSA.xs +++ b/RSA.xs @@ -49,6 +49,26 @@ static int _write_pkcs8_pem(BIO* bio, RSA* rsa, const EVP_CIPHER* enc, } #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 + real OpenSSL symbols. */ +#if OPENSSL_VERSION_NUMBER < 0x30000000L +static RSA* _load_pkcs8_der_key(BIO* bio, const char* passphrase) +{ + EVP_PKEY* pkey; + RSA* rsa; + + pkey = d2i_PKCS8PrivateKey_bio(bio, NULL, NULL, (void*)passphrase); + if (!pkey) + return NULL; + + rsa = EVP_PKEY_get1_RSA(pkey); + EVP_PKEY_free(pkey); + return rsa; +} +#endif + #if OPENSSL_VERSION_NUMBER >= 0x30000000L #define UNSIGNED_CHAR unsigned char #define SIZE_T_INT size_t @@ -630,9 +650,10 @@ _new_public_key_pkcs1_der(proto, key_string_SV) RETVAL SV* -_new_private_key_der(proto, key_string_SV) +_new_private_key_der(proto, key_string_SV, passphrase_SV=&PL_sv_undef) SV* proto; SV* key_string_SV; + SV* passphrase_SV; PREINIT: STRLEN keyStringLength; char* keyString; @@ -653,6 +674,15 @@ _new_private_key_der(proto, key_string_SV) BIO_free(bio); croakSsl(__FILE__, __LINE__); } + if (SvPOK(passphrase_SV)) { + STRLEN passlen; + unsigned char* pass = (unsigned char*)SvPV(passphrase_SV, passlen); + if (!OSSL_DECODER_CTX_set_passphrase(dctx, pass, passlen)) { + OSSL_DECODER_CTX_free(dctx); + BIO_free(bio); + croakSsl(__FILE__, __LINE__); + } + } if (!OSSL_DECODER_from_bio(dctx, bio)) { OSSL_DECODER_CTX_free(dctx); BIO_free(bio); @@ -660,7 +690,12 @@ _new_private_key_der(proto, key_string_SV) } OSSL_DECODER_CTX_free(dctx); #else - pkey = d2i_RSAPrivateKey_bio(bio, NULL); + if (SvPOK(passphrase_SV)) { + char* passphrase = SvPV_nolen(passphrase_SV); + pkey = _load_pkcs8_der_key(bio, passphrase); + } else { + pkey = d2i_RSAPrivateKey_bio(bio, NULL); + } #endif BIO_free(bio); CHECK_OPEN_SSL(pkey); diff --git a/t/der.t b/t/der.t index cc64c88..914293f 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 => 24 } +BEGIN { plan tests => 30 } # --- Generate a key pair for testing --- @@ -125,6 +125,33 @@ eval { Crypt::OpenSSL::RSA->new_public_key("") }; like( $@, qr/unrecognized key format/, "new_public_key gives helpful error on empty string" ); +# --- Encrypted PKCS#8 DER private key with passphrase --- + +my $passphrase = 'test_der_pass'; +my $enc_pkcs8_pem = $rsa->get_private_key_pkcs8_string($passphrase, 'aes-128-cbc'); +my $enc_pkcs8_der = pem_to_der($enc_pkcs8_pem); + +is( ord(substr($enc_pkcs8_der, 0, 1)), 0x30, + "Encrypted PKCS#8 DER starts with SEQUENCE tag" ); + +my $priv_from_enc_der; +ok( $priv_from_enc_der = Crypt::OpenSSL::RSA->new_private_key($enc_pkcs8_der, $passphrase), + "new_private_key loads encrypted PKCS#8 DER with passphrase" ); + +ok( $priv_from_enc_der->is_private(), + "Encrypted PKCS#8 DER-loaded key is private" ); + +is( $priv_from_enc_der->get_public_key_x509_string(), $x509_pem, + "Encrypted PKCS#8 DER key exports same public key as original" ); + +$priv_from_enc_der->use_sha256_hash(); +my $sig3 = $priv_from_enc_der->sign($plaintext); +ok( $pub_from_x509_der->verify($plaintext, $sig3), + "Signature from encrypted PKCS#8 DER-loaded key verifies" ); + +eval { Crypt::OpenSSL::RSA->new_private_key($enc_pkcs8_der, 'wrong_pass') }; +ok( $@, "new_private_key croaks on wrong passphrase for encrypted PKCS#8 DER" ); + # PEM header for wrong type eval { Crypt::OpenSSL::RSA->new_public_key("-----BEGIN CERTIFICATE-----\nfoo\n-----END CERTIFICATE-----\n") }; like( $@, qr/unrecognized key format/,