Skip to content

Commit 67199f4

Browse files
authored
Merge pull request #126 from atoomic/koan.atoomic/new-public-key-der-support
feat: support DER-encoded public keys in new_public_key()
2 parents ae488e3 + ad7ab01 commit 67199f4

4 files changed

Lines changed: 453 additions & 12 deletions

File tree

RSA.pm

Lines changed: 62 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,54 @@ BEGIN {
1919

2020
sub new_public_key {
2121
my ( $proto, $p_key_string ) = @_;
22+
croak "unrecognized key format: expected PEM-encoded key (starting with '-----BEGIN') "
23+
. "or DER-encoded key (binary ASN.1 data)"
24+
unless defined $p_key_string && length($p_key_string) > 0;
2225
if ( $p_key_string =~ /^-----BEGIN RSA PUBLIC KEY-----/ ) {
2326
return $proto->_new_public_key_pkcs1($p_key_string);
2427
}
2528
elsif ( $p_key_string =~ /^-----BEGIN PUBLIC KEY-----/ ) {
2629
return $proto->_new_public_key_x509($p_key_string);
2730
}
31+
elsif ( $p_key_string =~ /^-----/ ) {
32+
croak "unrecognized key format: PEM header not recognized as RSA public key. "
33+
. "Expected '-----BEGIN RSA PUBLIC KEY-----' (PKCS#1) or "
34+
. "'-----BEGIN PUBLIC KEY-----' (X.509)";
35+
}
36+
elsif ( substr($p_key_string, 0, 1) eq "\x30" ) {
37+
# ASN.1 SEQUENCE tag detected — likely DER-encoded key.
38+
# Search for the RSA OID (1.2.840.113549.1.1.1) in raw binary to distinguish
39+
# X.509 SubjectPublicKeyInfo from PKCS#1 RSAPublicKey.
40+
if (index($p_key_string, "\x06\x09\x2a\x86\x48\x86\xf7\x0d\x01\x01\x01") >= 0) {
41+
# RSA encryption OID found — X.509 SubjectPublicKeyInfo
42+
return $proto->_new_public_key_x509_der($p_key_string);
43+
}
44+
else {
45+
# No OID — assume PKCS#1 RSAPublicKey, let OpenSSL reject invalid data
46+
return $proto->_new_public_key_pkcs1_der($p_key_string);
47+
}
48+
}
49+
else {
50+
croak "unrecognized key format: expected PEM-encoded key (starting with '-----BEGIN') "
51+
. "or DER-encoded key (binary ASN.1 data)";
52+
}
53+
}
54+
55+
sub new_private_key {
56+
my ( $proto, $p_key_string, @rest ) = @_;
57+
croak "unrecognized key format: expected PEM-encoded key (starting with '-----BEGIN') "
58+
. "or DER-encoded key (binary ASN.1 data)"
59+
unless defined $p_key_string && length($p_key_string) > 0;
60+
if ( $p_key_string =~ /^-----/ ) {
61+
return $proto->_new_private_key_pem($p_key_string, @rest);
62+
}
63+
elsif ( substr($p_key_string, 0, 1) eq "\x30" ) {
64+
# ASN.1 SEQUENCE tag detected — likely DER-encoded private key.
65+
return $proto->_new_private_key_der($p_key_string);
66+
}
2867
else {
29-
croak "unrecognized key format";
68+
croak "unrecognized key format: expected PEM-encoded key (starting with '-----BEGIN') "
69+
. "or DER-encoded key (binary ASN.1 data)";
3070
}
3171
}
3272

@@ -125,9 +165,15 @@ this (never documented) behavior is no longer the case.
125165
=item new_public_key
126166
127167
Create a new C<Crypt::OpenSSL::RSA> object by loading a public key in
128-
from a string containing Base64/DER-encoding of either the PKCS1 or
129-
X.509 representation of the key. The string should include the
130-
C<-----BEGIN...-----> and C<-----END...-----> lines.
168+
from a string containing either PEM or DER encoding of the PKCS#1 or
169+
X.509 representation of the key.
170+
171+
For PEM keys, the string should include the C<-----BEGIN...-----> and
172+
C<-----END...-----> lines. Both C<BEGIN RSA PUBLIC KEY> (PKCS#1) and
173+
C<BEGIN PUBLIC KEY> (X.509/SubjectPublicKeyInfo) formats are supported.
174+
175+
DER-encoded keys (raw binary ASN.1) are also accepted and the format
176+
(PKCS#1 vs X.509) is auto-detected.
131177
132178
The padding is set to PKCS1_OAEP, but can be changed with the
133179
C<use_xxx_padding> methods.
@@ -138,18 +184,23 @@ C<use_pkcs1_pss_padding> or C<use_pkcs1_padding> prior to signing operations.
138184
=item new_private_key
139185
140186
Create a new C<Crypt::OpenSSL::RSA> object by loading a private key in
141-
from an string containing the Base64/DER encoding of the PKCS1
142-
representation of the key. The string should include the
143-
C<-----BEGIN...-----> and C<-----END...-----> lines. The padding is set to
144-
PKCS1_OAEP, but can be changed with C<use_xxx_padding>.
187+
from a string containing either PEM or DER encoding of the key.
188+
189+
For PEM keys, the string should include the C<-----BEGIN...-----> and
190+
C<-----END...-----> lines. The padding is set to PKCS1_OAEP, but can
191+
be changed with C<use_xxx_padding>.
145192
146-
An optional parameter can be passed for passphase protected private key:
193+
DER-encoded keys (raw binary ASN.1) are also accepted.
194+
195+
An optional parameter can be passed for passphrase-protected PEM private
196+
keys:
147197
148198
=over
149199
150-
=item passphase
200+
=item passphrase
151201
152-
The passphase which protects the private key.
202+
The passphrase which protects the private key. Note: passphrase
203+
protection is only supported for PEM-encoded keys.
153204
154205
=back
155206

RSA.xs

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
#include <openssl/core_names.h>
2626
#include <openssl/param_build.h>
2727
#include <openssl/encoder.h>
28+
#include <openssl/decoder.h>
2829
#endif
2930

3031
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
@@ -482,7 +483,7 @@ BOOT:
482483
#endif
483484

484485
SV*
485-
new_private_key(proto, key_string_SV, passphase_SV=&PL_sv_undef)
486+
_new_private_key_pem(proto, key_string_SV, passphase_SV=&PL_sv_undef)
486487
SV* proto;
487488
SV* key_string_SV;
488489
SV* passphase_SV;
@@ -512,6 +513,107 @@ _new_public_key_x509(proto, key_string_SV)
512513
OUTPUT:
513514
RETVAL
514515

516+
SV*
517+
_new_public_key_x509_der(proto, key_string_SV)
518+
SV* proto;
519+
SV* key_string_SV;
520+
PREINIT:
521+
STRLEN keyStringLength;
522+
char* keyString;
523+
EVP_PKEY* pkey;
524+
BIO* bio;
525+
CODE:
526+
keyString = SvPV(key_string_SV, keyStringLength);
527+
CHECK_OPEN_SSL(bio = BIO_new_mem_buf(keyString, keyStringLength));
528+
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
529+
pkey = d2i_PUBKEY_bio(bio, NULL);
530+
#else
531+
pkey = d2i_RSA_PUBKEY_bio(bio, NULL);
532+
#endif
533+
BIO_free(bio);
534+
CHECK_OPEN_SSL(pkey);
535+
RETVAL = make_rsa_obj(proto, pkey);
536+
OUTPUT:
537+
RETVAL
538+
539+
SV*
540+
_new_public_key_pkcs1_der(proto, key_string_SV)
541+
SV* proto;
542+
SV* key_string_SV;
543+
PREINIT:
544+
STRLEN keyStringLength;
545+
char* keyString;
546+
EVP_PKEY* pkey;
547+
BIO* bio;
548+
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
549+
OSSL_DECODER_CTX* dctx;
550+
#endif
551+
CODE:
552+
keyString = SvPV(key_string_SV, keyStringLength);
553+
CHECK_OPEN_SSL(bio = BIO_new_mem_buf(keyString, keyStringLength));
554+
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
555+
pkey = NULL;
556+
dctx = OSSL_DECODER_CTX_new_for_pkey(&pkey, "DER", "type-specific",
557+
"RSA", OSSL_KEYMGMT_SELECT_PUBLIC_KEY,
558+
NULL, NULL);
559+
if (!dctx) {
560+
BIO_free(bio);
561+
croakSsl(__FILE__, __LINE__);
562+
}
563+
if (!OSSL_DECODER_from_bio(dctx, bio)) {
564+
OSSL_DECODER_CTX_free(dctx);
565+
BIO_free(bio);
566+
croakSsl(__FILE__, __LINE__);
567+
}
568+
OSSL_DECODER_CTX_free(dctx);
569+
#else
570+
pkey = d2i_RSAPublicKey_bio(bio, NULL);
571+
#endif
572+
BIO_free(bio);
573+
CHECK_OPEN_SSL(pkey);
574+
RETVAL = make_rsa_obj(proto, pkey);
575+
OUTPUT:
576+
RETVAL
577+
578+
SV*
579+
_new_private_key_der(proto, key_string_SV)
580+
SV* proto;
581+
SV* key_string_SV;
582+
PREINIT:
583+
STRLEN keyStringLength;
584+
char* keyString;
585+
EVP_PKEY* pkey;
586+
BIO* bio;
587+
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
588+
OSSL_DECODER_CTX* dctx;
589+
#endif
590+
CODE:
591+
keyString = SvPV(key_string_SV, keyStringLength);
592+
CHECK_OPEN_SSL(bio = BIO_new_mem_buf(keyString, keyStringLength));
593+
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
594+
pkey = NULL;
595+
dctx = OSSL_DECODER_CTX_new_for_pkey(&pkey, "DER", NULL,
596+
"RSA", OSSL_KEYMGMT_SELECT_ALL,
597+
NULL, NULL);
598+
if (!dctx) {
599+
BIO_free(bio);
600+
croakSsl(__FILE__, __LINE__);
601+
}
602+
if (!OSSL_DECODER_from_bio(dctx, bio)) {
603+
OSSL_DECODER_CTX_free(dctx);
604+
BIO_free(bio);
605+
croakSsl(__FILE__, __LINE__);
606+
}
607+
OSSL_DECODER_CTX_free(dctx);
608+
#else
609+
pkey = d2i_RSAPrivateKey_bio(bio, NULL);
610+
#endif
611+
BIO_free(bio);
612+
CHECK_OPEN_SSL(pkey);
613+
RETVAL = make_rsa_obj(proto, pkey);
614+
OUTPUT:
615+
RETVAL
616+
515617
void
516618
DESTROY(p_rsa)
517619
rsaData* p_rsa;

t/der.t

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
use strict;
2+
use warnings;
3+
use Test::More;
4+
use MIME::Base64;
5+
use Crypt::OpenSSL::RSA;
6+
7+
BEGIN { plan tests => 22 }
8+
9+
# --- Generate a key pair for testing ---
10+
11+
my $rsa = Crypt::OpenSSL::RSA->generate_key(2048);
12+
13+
# --- Extract PEM public keys ---
14+
15+
my $pkcs1_pem = $rsa->get_public_key_string(); # PKCS#1 (BEGIN RSA PUBLIC KEY)
16+
my $x509_pem = $rsa->get_public_key_x509_string(); # X.509 (BEGIN PUBLIC KEY)
17+
18+
# --- Convert PEM to DER by stripping headers and base64-decoding ---
19+
20+
sub pem_to_der {
21+
my ($pem) = @_;
22+
$pem =~ s/-----BEGIN [^-]+-----//;
23+
$pem =~ s/-----END [^-]+-----//;
24+
$pem =~ s/\s+//g;
25+
return decode_base64($pem);
26+
}
27+
28+
my $pkcs1_der = pem_to_der($pkcs1_pem);
29+
my $x509_der = pem_to_der($x509_pem);
30+
31+
# Sanity check: DER data starts with ASN.1 SEQUENCE tag
32+
is( ord(substr($pkcs1_der, 0, 1)), 0x30, "PKCS#1 DER starts with SEQUENCE tag" );
33+
is( ord(substr($x509_der, 0, 1)), 0x30, "X.509 DER starts with SEQUENCE tag" );
34+
35+
# --- Load DER keys via new_public_key ---
36+
37+
my ($pub_from_x509_der, $pub_from_pkcs1_der);
38+
39+
ok( $pub_from_x509_der = Crypt::OpenSSL::RSA->new_public_key($x509_der),
40+
"new_public_key loads X.509 DER key" );
41+
42+
ok( $pub_from_pkcs1_der = Crypt::OpenSSL::RSA->new_public_key($pkcs1_der),
43+
"new_public_key loads PKCS#1 DER key" );
44+
45+
# --- Verify round-trip: DER-loaded keys produce the same PEM output ---
46+
47+
is( $pub_from_x509_der->get_public_key_x509_string(), $x509_pem,
48+
"X.509 DER key exports to same X.509 PEM" );
49+
50+
is( $pub_from_x509_der->get_public_key_string(), $pkcs1_pem,
51+
"X.509 DER key exports to same PKCS#1 PEM" );
52+
53+
is( $pub_from_pkcs1_der->get_public_key_x509_string(), $x509_pem,
54+
"PKCS#1 DER key exports to same X.509 PEM" );
55+
56+
is( $pub_from_pkcs1_der->get_public_key_string(), $pkcs1_pem,
57+
"PKCS#1 DER key exports to same PKCS#1 PEM" );
58+
59+
# --- Verify DER-loaded keys can actually verify signatures ---
60+
61+
$rsa->use_sha256_hash();
62+
my $plaintext = "Hello, DER world!";
63+
my $sig = $rsa->sign($plaintext);
64+
65+
$pub_from_x509_der->use_sha256_hash();
66+
ok( $pub_from_x509_der->verify($plaintext, $sig),
67+
"X.509 DER-loaded key verifies signature" );
68+
69+
$pub_from_pkcs1_der->use_sha256_hash();
70+
ok( $pub_from_pkcs1_der->verify($plaintext, $sig),
71+
"PKCS#1 DER-loaded key verifies signature" );
72+
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+
110+
# --- Error cases ---
111+
112+
# DER-like data that isn't a valid key (no RSA OID, so falls through to PKCS#1 path)
113+
eval { Crypt::OpenSSL::RSA->new_public_key("\x30\x00") };
114+
ok( $@, "new_public_key croaks on truncated DER data" );
115+
116+
# Completely bogus binary data (not starting with 0x30)
117+
eval { Crypt::OpenSSL::RSA->new_public_key("\x01\x02\x03\x04") };
118+
like( $@, qr/unrecognized key format/,
119+
"new_public_key gives helpful error on random binary data" );
120+
121+
# Empty string
122+
eval { Crypt::OpenSSL::RSA->new_public_key("") };
123+
like( $@, qr/unrecognized key format/,
124+
"new_public_key gives helpful error on empty string" );
125+
126+
# PEM header for wrong type
127+
eval { Crypt::OpenSSL::RSA->new_public_key("-----BEGIN CERTIFICATE-----\nfoo\n-----END CERTIFICATE-----\n") };
128+
like( $@, qr/unrecognized key format/,
129+
"new_public_key gives helpful error on certificate PEM" );

0 commit comments

Comments
 (0)