Skip to content

Commit 0694fe9

Browse files
committed
rebase: apply review feedback on cpan-authors#126
- **Added DER-encoded private key support in `new_private_key()`** per @timlegge's request to support importing private keys from DER format, matching the existing DER support added for public keys. - **RSA.xs**: Renamed XS `new_private_key` to `_new_private_key_pem` (internal), added `_new_private_key_der` XS function using `OSSL_DECODER` on OpenSSL 3.x and `d2i_PrivateKey_bio` on pre-3.x. - **RSA.pm**: Added Perl-level `new_private_key` wrapper that detects PEM (starts with `-----`) vs DER (ASN.1 SEQUENCE tag `\x30`) and routes to the appropriate XS function. Passphrase parameter is passed through for PEM keys. - **RSA.pm POD**: Updated `new_private_key` documentation to describe DER support and clarify passphrase is PEM-only. - **t/der.t**: Added 8 new tests (14 → 22 total): DER private key loading, round-trip verification, signing with DER-loaded private key, error cases for invalid DER private key data, and regression test that PEM private keys still work through the new wrapper.
1 parent 7cb4377 commit 0694fe9

3 files changed

Lines changed: 105 additions & 9 deletions

File tree

RSA.pm

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,21 @@ sub new_public_key {
4343
}
4444
}
4545

46+
sub new_private_key {
47+
my ( $proto, $p_key_string, @rest ) = @_;
48+
if ( $p_key_string =~ /^-----/ ) {
49+
return $proto->_new_private_key_pem($p_key_string, @rest);
50+
}
51+
elsif ( length($p_key_string) > 0 && substr($p_key_string, 0, 1) eq "\x30" ) {
52+
# ASN.1 SEQUENCE tag detected — likely DER-encoded private key.
53+
return $proto->_new_private_key_der($p_key_string);
54+
}
55+
else {
56+
croak "unrecognized key format: expected PEM-encoded key (starting with '-----BEGIN') "
57+
. "or DER-encoded key (binary ASN.1 data)";
58+
}
59+
}
60+
4661
sub new_key_from_parameters {
4762
my ( $proto, $n, $e, $d, $p, $q, %opts ) = @_;
4863
my $rsa = $proto->_new_key_from_parameters( map { $_ ? $_->pointer_copy() : 0 } $n, $e, $d, $p, $q );
@@ -146,18 +161,23 @@ C<use_pkcs1_pss_padding> or C<use_pkcs1_padding> prior to signing operations.
146161
=item new_private_key
147162
148163
Create a new C<Crypt::OpenSSL::RSA> object by loading a private key in
149-
from an string containing the Base64/DER encoding of the PKCS1
150-
representation of the key. The string should include the
151-
C<-----BEGIN...-----> and C<-----END...-----> lines. The padding is set to
152-
PKCS1_OAEP, but can be changed with C<use_xxx_padding>.
164+
from a string containing either PEM or DER encoding of the key.
165+
166+
For PEM keys, the string should include the C<-----BEGIN...-----> and
167+
C<-----END...-----> lines. The padding is set to PKCS1_OAEP, but can
168+
be changed with C<use_xxx_padding>.
153169
154-
An optional parameter can be passed for passphase protected private key:
170+
DER-encoded keys (raw binary ASN.1) are also accepted.
171+
172+
An optional parameter can be passed for passphrase-protected PEM private
173+
keys:
155174
156175
=over
157176
158-
=item passphase
177+
=item passphrase
159178
160-
The passphase which protects the private key.
179+
The passphrase which protects the private key. Note: passphrase
180+
protection is only supported for PEM-encoded keys.
161181
162182
=back
163183

RSA.xs

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -427,7 +427,7 @@ BOOT:
427427
#endif
428428

429429
SV*
430-
new_private_key(proto, key_string_SV, passphase_SV=&PL_sv_undef)
430+
_new_private_key_pem(proto, key_string_SV, passphase_SV=&PL_sv_undef)
431431
SV* proto;
432432
SV* key_string_SV;
433433
SV* passphase_SV;
@@ -519,6 +519,45 @@ _new_public_key_pkcs1_der(proto, key_string_SV)
519519
OUTPUT:
520520
RETVAL
521521

522+
SV*
523+
_new_private_key_der(proto, key_string_SV)
524+
SV* proto;
525+
SV* key_string_SV;
526+
PREINIT:
527+
STRLEN keyStringLength;
528+
char* keyString;
529+
EVP_PKEY* pkey;
530+
BIO* bio;
531+
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
532+
OSSL_DECODER_CTX* dctx;
533+
#endif
534+
CODE:
535+
keyString = SvPV(key_string_SV, keyStringLength);
536+
CHECK_OPEN_SSL(bio = BIO_new_mem_buf(keyString, keyStringLength));
537+
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
538+
pkey = NULL;
539+
dctx = OSSL_DECODER_CTX_new_for_pkey(&pkey, "DER", NULL,
540+
"RSA", OSSL_KEYMGMT_SELECT_ALL,
541+
NULL, NULL);
542+
if (!dctx) {
543+
BIO_free(bio);
544+
croakSsl(__FILE__, __LINE__);
545+
}
546+
if (!OSSL_DECODER_from_bio(dctx, bio)) {
547+
OSSL_DECODER_CTX_free(dctx);
548+
BIO_free(bio);
549+
croakSsl(__FILE__, __LINE__);
550+
}
551+
OSSL_DECODER_CTX_free(dctx);
552+
#else
553+
pkey = d2i_PrivateKey_bio(bio, NULL);
554+
#endif
555+
BIO_free(bio);
556+
CHECK_OPEN_SSL(pkey);
557+
RETVAL = make_rsa_obj(proto, pkey);
558+
OUTPUT:
559+
RETVAL
560+
522561
void
523562
DESTROY(p_rsa)
524563
rsaData* p_rsa;

t/der.t

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use Test::More;
44
use MIME::Base64;
55
use Crypt::OpenSSL::RSA;
66

7-
BEGIN { plan tests => 14 }
7+
BEGIN { plan tests => 22 }
88

99
# --- Generate a key pair for testing ---
1010

@@ -70,6 +70,43 @@ $pub_from_pkcs1_der->use_sha256_hash();
7070
ok( $pub_from_pkcs1_der->verify($plaintext, $sig),
7171
"PKCS#1 DER-loaded key verifies signature" );
7272

73+
# --- Private key DER support ---
74+
75+
my $priv_pem = $rsa->get_private_key_string();
76+
my $priv_der = pem_to_der($priv_pem);
77+
78+
is( ord(substr($priv_der, 0, 1)), 0x30, "Private key DER starts with SEQUENCE tag" );
79+
80+
my $priv_from_der;
81+
ok( $priv_from_der = Crypt::OpenSSL::RSA->new_private_key($priv_der),
82+
"new_private_key loads DER-encoded private key" );
83+
84+
ok( $priv_from_der->is_private(),
85+
"DER-loaded private key is recognized as private" );
86+
87+
is( $priv_from_der->get_public_key_x509_string(), $x509_pem,
88+
"DER-loaded private key exports same public key" );
89+
90+
# Verify DER-loaded private key can sign and original public key can verify
91+
$priv_from_der->use_sha256_hash();
92+
my $sig2 = $priv_from_der->sign($plaintext);
93+
ok( $pub_from_x509_der->verify($plaintext, $sig2),
94+
"signature from DER-loaded private key verifies" );
95+
96+
# Error: DER-like data for private key
97+
eval { Crypt::OpenSSL::RSA->new_private_key("\x30\x00") };
98+
ok( $@, "new_private_key croaks on truncated DER data" );
99+
100+
# Error: bogus binary data for private key
101+
eval { Crypt::OpenSSL::RSA->new_private_key("\x01\x02\x03\x04") };
102+
like( $@, qr/unrecognized key format/,
103+
"new_private_key gives helpful error on random binary data" );
104+
105+
# PEM private keys still work through the wrapper
106+
my $priv_from_pem;
107+
ok( $priv_from_pem = Crypt::OpenSSL::RSA->new_private_key($priv_pem),
108+
"new_private_key still loads PEM-encoded private key" );
109+
73110
# --- Error cases ---
74111

75112
# DER-like data that isn't a valid key

0 commit comments

Comments
 (0)