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
73 changes: 62 additions & 11 deletions RSA.pm
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,54 @@ BEGIN {

sub new_public_key {
my ( $proto, $p_key_string ) = @_;
croak "unrecognized key format: expected PEM-encoded key (starting with '-----BEGIN') "
. "or DER-encoded key (binary ASN.1 data)"
unless defined $p_key_string && length($p_key_string) > 0;
if ( $p_key_string =~ /^-----BEGIN RSA PUBLIC KEY-----/ ) {
return $proto->_new_public_key_pkcs1($p_key_string);
}
elsif ( $p_key_string =~ /^-----BEGIN PUBLIC KEY-----/ ) {
return $proto->_new_public_key_x509($p_key_string);
}
elsif ( $p_key_string =~ /^-----/ ) {
croak "unrecognized key format: PEM header not recognized as RSA public key. "
. "Expected '-----BEGIN RSA PUBLIC KEY-----' (PKCS#1) or "
. "'-----BEGIN PUBLIC KEY-----' (X.509)";
}
elsif ( substr($p_key_string, 0, 1) eq "\x30" ) {
# ASN.1 SEQUENCE tag detected — likely DER-encoded key.
# Search for the RSA OID (1.2.840.113549.1.1.1) in raw binary to distinguish
# X.509 SubjectPublicKeyInfo from PKCS#1 RSAPublicKey.
if (index($p_key_string, "\x06\x09\x2a\x86\x48\x86\xf7\x0d\x01\x01\x01") >= 0) {
# RSA encryption OID found — X.509 SubjectPublicKeyInfo
return $proto->_new_public_key_x509_der($p_key_string);
}
else {
# No OID — assume PKCS#1 RSAPublicKey, let OpenSSL reject invalid data
return $proto->_new_public_key_pkcs1_der($p_key_string);
}
}
else {
croak "unrecognized key format: expected PEM-encoded key (starting with '-----BEGIN') "
. "or DER-encoded key (binary ASN.1 data)";
}
}

sub new_private_key {
my ( $proto, $p_key_string, @rest ) = @_;
croak "unrecognized key format: expected PEM-encoded key (starting with '-----BEGIN') "
. "or DER-encoded key (binary ASN.1 data)"
unless defined $p_key_string && length($p_key_string) > 0;
if ( $p_key_string =~ /^-----/ ) {
return $proto->_new_private_key_pem($p_key_string, @rest);
}
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);
}
else {
croak "unrecognized key format";
croak "unrecognized key format: expected PEM-encoded key (starting with '-----BEGIN') "
. "or DER-encoded key (binary ASN.1 data)";
}
}

Expand Down Expand Up @@ -125,9 +165,15 @@ this (never documented) behavior is no longer the case.
=item new_public_key

Create a new C<Crypt::OpenSSL::RSA> object by loading a public key in
from a string containing Base64/DER-encoding of either the PKCS1 or
X.509 representation of the key. The string should include the
C<-----BEGIN...-----> and C<-----END...-----> lines.
from a string containing either PEM or DER encoding of the PKCS#1 or
X.509 representation of the key.

For PEM keys, the string should include the C<-----BEGIN...-----> and
C<-----END...-----> lines. Both C<BEGIN RSA PUBLIC KEY> (PKCS#1) and
C<BEGIN PUBLIC KEY> (X.509/SubjectPublicKeyInfo) formats are supported.

DER-encoded keys (raw binary ASN.1) are also accepted and the format
(PKCS#1 vs X.509) is auto-detected.

The padding is set to PKCS1_OAEP, but can be changed with the
C<use_xxx_padding> methods.
Expand All @@ -138,18 +184,23 @@ C<use_pkcs1_pss_padding> or C<use_pkcs1_padding> prior to signing operations.
=item new_private_key

Create a new C<Crypt::OpenSSL::RSA> object by loading a private key in
from an string containing the Base64/DER encoding of the PKCS1
representation of the key. The string should include the
C<-----BEGIN...-----> and C<-----END...-----> lines. The padding is set to
PKCS1_OAEP, but can be changed with C<use_xxx_padding>.
from a string containing either PEM or DER encoding of the key.

For PEM keys, the string should include the C<-----BEGIN...-----> and
C<-----END...-----> lines. The padding is set to PKCS1_OAEP, but can
be changed with C<use_xxx_padding>.

An optional parameter can be passed for passphase protected private key:
DER-encoded keys (raw binary ASN.1) are also accepted.

An optional parameter can be passed for passphrase-protected PEM private
keys:

=over

=item passphase
=item passphrase

The passphase which protects the private key.
The passphrase which protects the private key. Note: passphrase
protection is only supported for PEM-encoded keys.

=back

Expand Down
104 changes: 103 additions & 1 deletion RSA.xs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
#include <openssl/core_names.h>
#include <openssl/param_build.h>
#include <openssl/encoder.h>
#include <openssl/decoder.h>
#endif

#if OPENSSL_VERSION_NUMBER >= 0x30000000L
Expand Down Expand Up @@ -476,7 +477,7 @@ BOOT:
#endif

SV*
new_private_key(proto, key_string_SV, passphase_SV=&PL_sv_undef)
_new_private_key_pem(proto, key_string_SV, passphase_SV=&PL_sv_undef)
SV* proto;
SV* key_string_SV;
SV* passphase_SV;
Expand Down Expand Up @@ -506,6 +507,107 @@ _new_public_key_x509(proto, key_string_SV)
OUTPUT:
RETVAL

SV*
_new_public_key_x509_der(proto, key_string_SV)
SV* proto;
SV* key_string_SV;
PREINIT:
STRLEN keyStringLength;
char* keyString;
EVP_PKEY* pkey;
BIO* bio;
CODE:
keyString = SvPV(key_string_SV, keyStringLength);
CHECK_OPEN_SSL(bio = BIO_new_mem_buf(keyString, keyStringLength));
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
pkey = d2i_PUBKEY_bio(bio, NULL);
#else
pkey = d2i_RSA_PUBKEY_bio(bio, NULL);
#endif
BIO_free(bio);
CHECK_OPEN_SSL(pkey);
RETVAL = make_rsa_obj(proto, pkey);
OUTPUT:
RETVAL

SV*
_new_public_key_pkcs1_der(proto, key_string_SV)
SV* proto;
SV* key_string_SV;
PREINIT:
STRLEN keyStringLength;
char* keyString;
EVP_PKEY* pkey;
BIO* bio;
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
OSSL_DECODER_CTX* dctx;
#endif
CODE:
keyString = SvPV(key_string_SV, keyStringLength);
CHECK_OPEN_SSL(bio = BIO_new_mem_buf(keyString, keyStringLength));
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
pkey = NULL;
dctx = OSSL_DECODER_CTX_new_for_pkey(&pkey, "DER", "type-specific",
"RSA", OSSL_KEYMGMT_SELECT_PUBLIC_KEY,
NULL, NULL);
if (!dctx) {
BIO_free(bio);
croakSsl(__FILE__, __LINE__);
}
if (!OSSL_DECODER_from_bio(dctx, bio)) {
OSSL_DECODER_CTX_free(dctx);
BIO_free(bio);
croakSsl(__FILE__, __LINE__);
}
OSSL_DECODER_CTX_free(dctx);
#else
pkey = d2i_RSAPublicKey_bio(bio, NULL);
#endif
BIO_free(bio);
CHECK_OPEN_SSL(pkey);
RETVAL = make_rsa_obj(proto, pkey);
OUTPUT:
RETVAL

SV*
_new_private_key_der(proto, key_string_SV)
SV* proto;
SV* key_string_SV;
PREINIT:
STRLEN keyStringLength;
char* keyString;
EVP_PKEY* pkey;
BIO* bio;
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
OSSL_DECODER_CTX* dctx;
#endif
CODE:
keyString = SvPV(key_string_SV, keyStringLength);
CHECK_OPEN_SSL(bio = BIO_new_mem_buf(keyString, keyStringLength));
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
pkey = NULL;
dctx = OSSL_DECODER_CTX_new_for_pkey(&pkey, "DER", NULL,
"RSA", OSSL_KEYMGMT_SELECT_ALL,
NULL, NULL);
if (!dctx) {
BIO_free(bio);
croakSsl(__FILE__, __LINE__);
}
if (!OSSL_DECODER_from_bio(dctx, bio)) {
OSSL_DECODER_CTX_free(dctx);
BIO_free(bio);
croakSsl(__FILE__, __LINE__);
}
OSSL_DECODER_CTX_free(dctx);
#else
pkey = d2i_RSAPrivateKey_bio(bio, NULL);
#endif
BIO_free(bio);
CHECK_OPEN_SSL(pkey);
RETVAL = make_rsa_obj(proto, pkey);
OUTPUT:
RETVAL

void
DESTROY(p_rsa)
rsaData* p_rsa;
Expand Down
129 changes: 129 additions & 0 deletions t/der.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
use strict;
use warnings;
use Test::More;
use MIME::Base64;
use Crypt::OpenSSL::RSA;

BEGIN { plan tests => 22 }

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

my $rsa = Crypt::OpenSSL::RSA->generate_key(2048);

# --- Extract PEM public keys ---

my $pkcs1_pem = $rsa->get_public_key_string(); # PKCS#1 (BEGIN RSA PUBLIC KEY)
my $x509_pem = $rsa->get_public_key_x509_string(); # X.509 (BEGIN PUBLIC KEY)

# --- Convert PEM to DER by stripping headers and base64-decoding ---

sub pem_to_der {
my ($pem) = @_;
$pem =~ s/-----BEGIN [^-]+-----//;
$pem =~ s/-----END [^-]+-----//;
$pem =~ s/\s+//g;
return decode_base64($pem);
}

my $pkcs1_der = pem_to_der($pkcs1_pem);
my $x509_der = pem_to_der($x509_pem);

# Sanity check: DER data starts with ASN.1 SEQUENCE tag
is( ord(substr($pkcs1_der, 0, 1)), 0x30, "PKCS#1 DER starts with SEQUENCE tag" );
is( ord(substr($x509_der, 0, 1)), 0x30, "X.509 DER starts with SEQUENCE tag" );

# --- Load DER keys via new_public_key ---

my ($pub_from_x509_der, $pub_from_pkcs1_der);

ok( $pub_from_x509_der = Crypt::OpenSSL::RSA->new_public_key($x509_der),
"new_public_key loads X.509 DER key" );

ok( $pub_from_pkcs1_der = Crypt::OpenSSL::RSA->new_public_key($pkcs1_der),
"new_public_key loads PKCS#1 DER key" );

# --- Verify round-trip: DER-loaded keys produce the same PEM output ---

is( $pub_from_x509_der->get_public_key_x509_string(), $x509_pem,
"X.509 DER key exports to same X.509 PEM" );

is( $pub_from_x509_der->get_public_key_string(), $pkcs1_pem,
"X.509 DER key exports to same PKCS#1 PEM" );

is( $pub_from_pkcs1_der->get_public_key_x509_string(), $x509_pem,
"PKCS#1 DER key exports to same X.509 PEM" );

is( $pub_from_pkcs1_der->get_public_key_string(), $pkcs1_pem,
"PKCS#1 DER key exports to same PKCS#1 PEM" );

# --- Verify DER-loaded keys can actually verify signatures ---

$rsa->use_sha256_hash();
my $plaintext = "Hello, DER world!";
my $sig = $rsa->sign($plaintext);

$pub_from_x509_der->use_sha256_hash();
ok( $pub_from_x509_der->verify($plaintext, $sig),
"X.509 DER-loaded key verifies signature" );

$pub_from_pkcs1_der->use_sha256_hash();
ok( $pub_from_pkcs1_der->verify($plaintext, $sig),
"PKCS#1 DER-loaded key verifies signature" );

# --- Private key DER support ---

my $priv_pem = $rsa->get_private_key_string();
my $priv_der = pem_to_der($priv_pem);

is( ord(substr($priv_der, 0, 1)), 0x30, "Private key DER starts with SEQUENCE tag" );

my $priv_from_der;
ok( $priv_from_der = Crypt::OpenSSL::RSA->new_private_key($priv_der),
"new_private_key loads DER-encoded private key" );

ok( $priv_from_der->is_private(),
"DER-loaded private key is recognized as private" );

is( $priv_from_der->get_public_key_x509_string(), $x509_pem,
"DER-loaded private key exports same public key" );

# Verify DER-loaded private key can sign and original public key can verify
$priv_from_der->use_sha256_hash();
my $sig2 = $priv_from_der->sign($plaintext);
ok( $pub_from_x509_der->verify($plaintext, $sig2),
"signature from DER-loaded private key verifies" );

# Error: DER-like data for private key
eval { Crypt::OpenSSL::RSA->new_private_key("\x30\x00") };
ok( $@, "new_private_key croaks on truncated DER data" );

# Error: bogus binary data for private key
eval { Crypt::OpenSSL::RSA->new_private_key("\x01\x02\x03\x04") };
like( $@, qr/unrecognized key format/,
"new_private_key gives helpful error on random binary data" );

# PEM private keys still work through the wrapper
my $priv_from_pem;
ok( $priv_from_pem = Crypt::OpenSSL::RSA->new_private_key($priv_pem),
"new_private_key still loads PEM-encoded private key" );

# --- Error cases ---

# DER-like data that isn't a valid key (no RSA OID, so falls through to PKCS#1 path)
eval { Crypt::OpenSSL::RSA->new_public_key("\x30\x00") };
ok( $@, "new_public_key croaks on truncated DER data" );

# Completely bogus binary data (not starting with 0x30)
eval { Crypt::OpenSSL::RSA->new_public_key("\x01\x02\x03\x04") };
like( $@, qr/unrecognized key format/,
"new_public_key gives helpful error on random binary data" );

# Empty string
eval { Crypt::OpenSSL::RSA->new_public_key("") };
like( $@, qr/unrecognized key format/,
"new_public_key gives helpful error on empty string" );

# PEM header for wrong type
eval { Crypt::OpenSSL::RSA->new_public_key("-----BEGIN CERTIFICATE-----\nfoo\n-----END CERTIFICATE-----\n") };
like( $@, qr/unrecognized key format/,
"new_public_key gives helpful error on certificate PEM" );
Loading
Loading