Skip to content
Draft
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
36 changes: 36 additions & 0 deletions RSA.pm
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -322,6 +323,41 @@ the private-key counterpart of C<get_public_key_x509_string>.
Accepts the same optional passphrase and cipher-name parameters as
C<get_private_key_string>.

=item get_public_key_der_string

Return the DER-encoded PKCS#1 C<RSAPublicKey> representation of the
public key as a binary string. This is the DER equivalent of
C<get_public_key_string>.

=item get_public_key_pkcs1_der_string

Alias for C<get_public_key_der_string>.

=item get_public_key_x509_der_string

Return the DER-encoded X.509 C<SubjectPublicKeyInfo> representation
of the public key as a binary string. This is the DER equivalent of
C<get_public_key_x509_string>, and the format produced by
C<openssl rsa -pubout -outform DER>.

=item get_private_key_der_string

Return the DER-encoded PKCS#1 C<RSAPrivateKey> representation of the
private key as a binary string. This is the DER equivalent of
C<get_private_key_string> (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<PrivateKeyInfo> representation of the
private key as a binary string. This is the DER equivalent of
C<get_private_key_pkcs8_string>, and the format produced by
C<openssl pkey -outform DER>.

Accepts the same optional passphrase and cipher-name parameters as
C<get_private_key_string>. When a passphrase is provided, the output
is an C<EncryptedPrivateKeyInfo> structure.

=item encrypt

Encrypt a binary "string" using the public (portion of the) key.
Expand Down
137 changes: 137 additions & 0 deletions RSA.xs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down
82 changes: 81 additions & 1 deletion t/der.t
Original file line number Diff line number Diff line change
Expand Up @@ -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 ---

Expand Down Expand Up @@ -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" );
Loading