Crypt-OpenSSL-RSA
view release on metacpan or search on metacpan
only checked on 3.x inside rsa_crypt()).
- PR #178: Validate key size in generate_key() before calling OpenSSL.
Reject negative, zero, and sub-512-bit key sizes with a clear croak
instead of letting OpenSSL produce cryptic errors or hang.
- PR #179 GH #174: Restore the lost configure_requires prereq on
Crypt::OpenSSL::Guess in Makefile.PL.
- PR #179 GH #175: Fix failing test 'Padding method pkcs1_pss is valid
for signing with ripemd160'.
[Improvements]
- PR #180: Add optional passphrase argument to new_private_key_der(),
enabling decryption of encrypted PKCS#8 DER (EncryptedPrivateKeyInfo)
private keys. On OpenSSL 3.x the passphrase is passed to the existing
OSSL_DECODER_CTX; on pre-3.x a d2i_PKCS8PrivateKey_bio() helper is
used. Previously only PEM-encoded keys supported a passphrase.
[Maintenance]
- PR #172: Fix 'passphase' -> 'passphrase' typo throughout the codebase
(RSA.xs internal names, RSA.pm POD for get_private_key_string, and the
test variable in t/format.t). The typo dates to the original 0.33
passphrase support. No functional change -- all renames are internal.
0.38 Apr 23 2026
[Bug Fixes]
- PR #103 GH #61: Re-enable PKCS#1 v1.5 padding for sign()/verify(). It
was incorrectly disabled in 0.35; the Marvin attack only affects
decryption, not signatures.
- PR #168: Fix croak message to reference use_pkcs1_oaep_padding() (not
- PR #141: Reject non-RSA keys (EC, DSA, RSA-PSS) loaded via
_load_rsa_key() on OpenSSL 3.x with a clear error instead of a
confusing failure later.
- PR #118: Fix private_encrypt() and public_decrypt() broken on OpenSSL
3.x with any padding except NO_PADDING; rsa_crypt() now distinguishes
encrypt vs. sign paths.
- PR #142: Free signature buffer on RSA_sign() failure on pre-3.x.
- PR #164 GH #152: Drain OpenSSL error queue after _get_key_parameters()
on OpenSSL 3.x so a failed optional-param lookup does not pollute the
error queue for subsequent operations.
- PR #161 GH #152: Cache is_private_key flag in rsaData struct to avoid a
per-call BIGNUM heap allocation on OpenSSL 3.x.
- PR #159 GH #155: Check return values of EVP_PKEY_get_bn_param() in
_get_key_parameters(); a failed mandatory param (n or e) now croaks
instead of silently returning undef.
- PR #160 GH #156: Use THROW macro for make_rsa_obj() result in
_new_key_from_parameters() to prevent resource leak on a NULL return.
- PR #158 GH #154: Extract setup_pss_sign_ctx() helper to deduplicate
PSS context setup in sign() and verify(); the two paths could previously
diverge silently.
- PR #157 GH #153: Eliminate duplicate NID-to-name table in
get_message_digest(); fixes whirlpool on OpenSSL 3.x where the old
low-level WHIRLPOOL() API path was being used instead of EVP_MD_fetch().
- PR #145: Fix BIO resource leak in extractBioString() error paths.
- PR #143: Validate that a private key is present before attempting export
in get_private_key_string().
- PR #140: NULL out BIGNUMs after freeing them in _new_key_from_parameters()
to prevent a double-free when make_rsa_obj() fails after they are freed.
- PR #137: Use BN_clear_free() (instead of BN_free()) for private key
BIGNUMs in _get_key_parameters() to scrub sensitive material.
- PR #136: Remove static buffer in get_message_digest() that caused
thread-safety problems under Perl ithreads.
- PR #134: Add Perl-level stub for use_sslv23_padding() on OpenSSL 3.x
where the underlying RSA_SSLV23_PADDING constant was removed.
- PR #133: Fix PSS MGF1 setup to inspect the correct padding fields
(sign_pad/verify_pad) instead of p_rsa->padding, preventing wrong
generate_key(), sign(), verify(), rsa_crypt(), check_key(),
get_public_key_string(), _new_key_from_parameters(), and
_get_key_parameters() across success and error paths.
[Improvements]
- PR #169: Make Crypt::OpenSSL::Bignum a hard runtime requirement (moved
from recommended to required in Makefile.PL and added hard import in
RSA.pm); it was already required in practice for get_key_parameters().
- PR #126: new_public_key() now accepts DER-encoded public keys in addition
to PEM; format is detected automatically via ASN.1 OID inspection.
- PR #124: Add get_private_key_pkcs8_string() to export private keys in
PKCS#8 PEM format.
- PR #110: Add get_public_key_pkcs1_string() as an alias for
get_public_key_string() for API symmetry with the X.509/PKCS#1 naming.
- PR #111: Add optional check=>1 parameter to new_key_from_parameters()
to validate the constructed key via check_key() before returning it.
- PR #135: Add plaintext length pre-validation in rsa_crypt() with a
descriptive croak before attempting the OpenSSL operation.
- PR #151: Reject invalid (even-numbered) RSA exponents before passing
them to OpenSSL, preventing a potential hang during key generation.
release of OpenSSL.
- Apply patch from Jim Radford <radford@blackbean.org> to add support
for SHA{224,256,384,512}
0.22 Mon Nov 15 2005 21:13:20
- Add public_decrypt, private_encrypt methods, contributed
by Paul G. Weiss <paul@weiss.name>
- Some changes to help builds on Redhat9
- Remove deprecated methods:
* the no-arg new constructor - use new_from_public_key,
new_from_private_key or Crypt::OpenSSL::RSA->generate_key instead
* load_public_key - use new_from_public_key
* load_private_key - use new_from_private_key
* generate_key as an instance method - use it as a class constructor
method instead.
* set_padding_mode - use use_no_padding, use_pkcs1_padding,
use_pkcs1_oaep_padding, or use_sslv23_padding instead.
* get_padding_mode
- Eliminate all(most all) memory leaks.
- fix email address
- Stop returning true from methods just to indicate success.
- Change default public exponent from 65535 to 65537
get_key_parameters, which, working with
Crypt::OpenSSL::Bignum, allow working directly with the
paramaters of an rsa key.
0.17 Mon Jan 06 2003 22:43:31
- Workaround for gcc 3.2 compile problems:
"/usr/include/openssl/des.h:193: parse error before '&' token"
(Patch by Rob Brown <bbb@cpan.org>)
- Deprecate no-arg constructor, load_*_key methods and the
instance method generate_key; switch to three constructors:
new_public_key, new_private_key and generate_key (as a class
method)
- Deprecate set_padding_mode method; replace with
use_xxx_padding.
- move tests into t directory, use Test as a framework
0.16 Tue Jun 11 22:01:45
- Fix bug reported by Rob McMillin <rlm@pricegrabber.com> which
prevented subclassing.
0.15 Fri Jun 07 09:13:12
use Crypt::OpenSSL::Random;
use Crypt::OpenSSL::RSA;
# not necessary if we have /dev/random:
Crypt::OpenSSL::Random::random_seed($good_entropy);
Crypt::OpenSSL::RSA->import_random_seed();
$rsa_pub = Crypt::OpenSSL::RSA->new_public_key($key_string);
$ciphertext = $rsa->encrypt($plaintext);
$rsa_priv = Crypt::OpenSSL::RSA->new_private_key($key_string);
$plaintext = $rsa->decrypt($ciphertext);
$rsa = Crypt::OpenSSL::RSA->generate_key(1024); # or
$rsa = Crypt::OpenSSL::RSA->generate_key(1024, $prime);
print "private key is:\n", $rsa->get_private_key_string();
print "public key (in PKCS1 format) is:\n",
$rsa->get_public_key_string();
print "public key (in X509 format) is:\n",
$rsa->get_public_key_x509_string();
$rsa_priv->use_md5_hash(); # insecure. use_sha256_hash or use_sha1_hash are the default
$signature = $rsa_priv->sign($plaintext);
print "Signed correctly\n" if ($rsa->verify($plaintext, $signature));
# SECURITY
Return the Base64/DER-encoded PKCS#8 representation of the private
key. This string has header and footer lines:
-----BEGIN PRIVATE KEY-----
-----END PRIVATE KEY-----
This is the format produced by `openssl pkey -outform PEM`, and is
the private-key counterpart of `get_public_key_x509_string`.
Accepts the same optional passphrase and cipher-name parameters as
`get_private_key_string`.
- encrypt
Encrypt a binary "string" using the public (portion of the) key.
- decrypt
Decrypt a binary "string". Croaks if the key is public only.
- private\_encrypt
# 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, @rest);
}
else {
croak "unrecognized key format: expected PEM-encoded key (starting with '-----BEGIN') "
. "or DER-encoded key (binary ASN.1 data)";
}
}
sub new_key_from_parameters {
my ( $proto, $n, $e, $d, $p, $q, %opts ) = @_;
my $rsa = $proto->_new_key_from_parameters( map { $_ ? $_->pointer_copy() : 0 } $n, $e, $d, $p, $q );
use Crypt::OpenSSL::Random;
use Crypt::OpenSSL::RSA;
# not necessary if we have /dev/random:
Crypt::OpenSSL::Random::random_seed($good_entropy);
Crypt::OpenSSL::RSA->import_random_seed();
$rsa_pub = Crypt::OpenSSL::RSA->new_public_key($key_string);
$ciphertext = $rsa->encrypt($plaintext);
$rsa_priv = Crypt::OpenSSL::RSA->new_private_key($key_string);
$plaintext = $rsa->decrypt($ciphertext);
$rsa = Crypt::OpenSSL::RSA->generate_key(1024); # or
$rsa = Crypt::OpenSSL::RSA->generate_key(1024, $prime);
print "private key is:\n", $rsa->get_private_key_string();
print "public key (in PKCS1 format) is:\n",
$rsa->get_public_key_string();
print "public key (in X509 format) is:\n",
$rsa->get_public_key_x509_string();
$rsa_priv->use_md5_hash(); # insecure. use_sha256_hash or use_sha1_hash are the default
$signature = $rsa_priv->sign($plaintext);
print "Signed correctly\n" if ($rsa->verify($plaintext, $signature));
=head1 SECURITY
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.
Note, PKCS1_OAEP can only be used for encryption. Call
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 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>.
DER-encoded keys (raw binary ASN.1) are also accepted.
Return the Base64/DER-encoded representation of the "subject
public key", suitable for use in X509 certificates. This string has
header and footer lines:
-----BEGIN PUBLIC KEY------
-----END PUBLIC KEY------
and is the format that is produced by running C<openssl rsa -pubout>.
=item get_private_key_string
Return the Base64/DER-encoded PKCS1 representation of the private
key. This string has
header and footer lines:
-----BEGIN RSA PRIVATE KEY------
-----END RSA PRIVATE KEY------
2 optional parameters can be passed for passphrase protected private key
string:
The passphrase which protects the private key.
=item cipher name
The cipher algorithm used to protect the private key. Default to
'des3'.
=back
=item get_private_key_pkcs8_string
Return the Base64/DER-encoded PKCS#8 representation of the private
key. This string has header and footer lines:
-----BEGIN PRIVATE KEY-----
-----END PRIVATE KEY-----
This is the format produced by C<openssl pkey -outform PEM>, and is
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 encrypt
Encrypt a binary "string" using the public (portion of the) key.
=item decrypt
Decrypt a binary "string". Croaks if the key is public only.
=item private_encrypt
#define PEM_read_bio_RSA_PUBKEY PEM_read_bio_RSA_PUBKEY
#define PEM_write_bio_PUBKEY(o,p) PEM_write_bio_RSA_PUBKEY(o,p)
#define PEM_write_bio_PrivateKey_traditional(m, n, o, p, q, r, s) PEM_write_bio_RSAPrivateKey(m, n , o, p, q, r, s)
#endif
typedef struct
{
EVP_PKEY* rsa;
int padding;
int hashMode;
int is_private_key; /* cached once at construction; avoids per-call BIGNUM alloc on 3.x */
} rsaData;
/* Key names for the rsa hash structure */
#define KEY_KEY "_Key"
#define PADDING_KEY "_Padding"
#define HASH_KEY "_Hash_Mode"
#define PACKAGE_NAME "Crypt::OpenSSL::RSA"
#define PACKAGE_CROAK(p_message) croak("%s", (p_message))
#define CHECK_NEW(p_var, p_size, p_type) \
if (New(0, p_var, p_size, p_type) == NULL) \
{ PACKAGE_CROAK("unable to alloc buffer"); }
#define THROW(p_result) if (!(p_result)) { error = 1; goto err; }
char _is_private(rsaData* p_rsa)
{
return p_rsa->is_private_key;
}
static int _detect_private_key(EVP_PKEY* p_rsa)
{
#if OLD_CRUFTY_SSL_VERSION
return (p_rsa->d != NULL);
#else
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
BIGNUM* d = NULL;
EVP_PKEY_get_bn_param(p_rsa, OSSL_PKEY_PARAM_RSA_D, &d);
if (d) {
BN_clear_free(d);
return 1;
rsaData* rsa;
CHECK_NEW(rsa, 1, rsaData);
rsa->rsa = p_rsa;
#ifdef SHA512_DIGEST_LENGTH
rsa->hashMode = NID_sha256;
#else
rsa->hashMode = NID_sha1;
#endif
rsa->padding = RSA_PKCS1_OAEP_PADDING;
rsa->is_private_key = _detect_private_key(p_rsa);
return sv_bless(
newRV_noinc(newSViv((IV) rsa)),
(SvROK(p_proto) ? SvSTASH(SvRV(p_proto)) : gv_stashsv(p_proto, 1)));
}
int get_digest_length(int hash_method)
{
switch(hash_method)
{
case NID_md5:
#if OPENSSL_VERSION_NUMBER < 0x10100000L
# might introduce memory leak without calling EVP_cleanup() on exit
# see https://wiki.openssl.org/index.php/Library_Initialization
ERR_load_crypto_strings();
OpenSSL_add_all_algorithms();
#else
# NOOP
#endif
SV*
_new_private_key_pem(proto, key_string_SV, passphrase_SV=&PL_sv_undef)
SV* proto;
SV* key_string_SV;
SV* passphrase_SV;
CODE:
RETVAL = make_rsa_obj(
proto, _load_rsa_key(key_string_SV, PEM_read_bio_PrivateKey, passphrase_SV));
OUTPUT:
RETVAL
SV*
#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, passphrase_SV=&PL_sv_undef)
SV* proto;
SV* key_string_SV;
SV* passphrase_SV;
PREINIT:
STRLEN keyStringLength;
char* keyString;
EVP_PKEY* pkey;
BIO* bio;
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
OSSL_DECODER_CTX* dctx;
RETVAL
void
DESTROY(p_rsa)
rsaData* p_rsa;
CODE:
EVP_PKEY_free(p_rsa->rsa);
Safefree(p_rsa);
SV*
get_private_key_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:
CHECK_OPEN_SSL(stringBIO = BIO_new(BIO_s_mem()));
CHECK_OPEN_SSL_BIO(PEM_write_bio_PrivateKey_traditional(
stringBIO, p_rsa->rsa, enc, (unsigned char* ) passphrase, passphraseLength, NULL, NULL), stringBIO);
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;
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:
$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" );
# --- 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/,
"new_public_key gives helpful error on certificate PEM" );
# --- Non-RSA DER key rejection ---
# On OpenSSL 3.x, d2i_PUBKEY_bio() accepts any key type.
# _new_public_key_x509_der must reject non-RSA keys.
use Crypt::OpenSSL::RSA;
Crypt::OpenSSL::Random::random_seed("OpenSSL needs at least 32 bytes.");
Crypt::OpenSSL::RSA->import_random_seed();
my $rsa = Crypt::OpenSSL::RSA->generate_key(2048);
my $rsa_pub = Crypt::OpenSSL::RSA->new_public_key($rsa->get_public_key_string());
# --- Malformed key loading ---
eval { Crypt::OpenSSL::RSA->new_private_key("not a key at all") };
ok($@, "new_private_key croaks on garbage input");
eval { Crypt::OpenSSL::RSA->new_private_key("") };
ok($@, "new_private_key croaks on empty string");
eval { Crypt::OpenSSL::RSA->new_private_key(undef) };
ok($@, "new_private_key croaks on undef");
eval {
Crypt::OpenSSL::RSA->new_private_key(
"-----BEGIN RSA PRIVATE KEY-----\ngarbage\n-----END RSA PRIVATE KEY-----\n"
);
};
ok($@, "new_private_key croaks on corrupted PEM body");
eval { Crypt::OpenSSL::RSA->_new_public_key_pkcs1("not a key") };
ok($@, "_new_public_key_pkcs1 croaks on garbage input");
eval { Crypt::OpenSSL::RSA->_new_public_key_x509("not a key") };
ok($@, "_new_public_key_x509 croaks on garbage input");
# --- Unrecognized public key format (Perl-level croak) ---
eval { Crypt::OpenSSL::RSA->new_public_key("-----BEGIN CERTIFICATE-----\nfoo\n-----END CERTIFICATE-----\n") };
like($@, qr/unrecognized key format/, "new_public_key croaks on unrecognized PEM header");
eval { Crypt::OpenSSL::RSA->new_public_key("just plain text") };
like($@, qr/unrecognized key format/, "new_public_key croaks on plain text");
# --- Wrong passphrase on encrypted key ---
my $encrypted_pem = $rsa->get_private_key_string("correct_passphrase", "aes-128-cbc");
eval { Crypt::OpenSSL::RSA->new_private_key($encrypted_pem, "wrong_passphrase") };
ok($@, "new_private_key croaks on wrong passphrase");
# Note: testing with no passphrase or empty passphrase is intentionally
# omitted â OpenSSL may prompt on the terminal, hanging non-interactive runs.
# --- Public key cannot perform private operations ---
eval { $rsa_pub->sign("hello") };
like($@, qr/Public keys cannot sign/i, "public key cannot sign");
eval { $rsa_pub->decrypt("hello") };
pV/9iVvswnnSsxEanoLchzA1bAaDNa9vkIU/BrFwQO9ctw+RQbHrvc/5KPbZoGsq
bfQ/wOXUnQJBAMs/ZGlziX19lOEGfziugMR33ybLxkBS7qcrpBebAED/8etijASp
LgMEOKeRz11WAVJJ5A4wi1yxD4fnBxp44xkCQG4RejNbPVByYQdlJPeD5Aijxta6
nBWGVuKNPuC80XjHpz6Yj9lDt5wH+EkJhA1ZaJKztWNbRoZ5e4x4PcubYXECQHA0
KubcVcblkU85Gvrbu1K7KoJsdKIGJqI7QXeWpmk74v4jhVD9ZN1dczlvEZ9hX5Fi
IXiD7Cvbw8svC4jdu+ECQQCw1ZlQPz2rGE+pFQiKOFPprH+pT+zkINh1d83jeMYd
GG7hKgfQB5J/B0u8/XzEtGnCq8m0xTADx2eplIoKhAFi
-----END RSA PRIVATE KEY-----
EOF
my ( $private_key, $public_key, $private_key2 );
ok( $private_key = Crypt::OpenSSL::RSA->new_private_key($PRIVATE_KEY_STRING), "load private key from string" );
is( $private_key->get_private_key_string(), $PRIVATE_KEY_STRING, "private key round-trips" );
is( $private_key->get_public_key_string(), $PUBLIC_KEY_PKCS1_STRING, "PKCS1 public key matches expected" );
is( $private_key->get_public_key_x509_string(), $PUBLIC_KEY_X509_STRING, "X509 public key matches expected" );
ok( $public_key = Crypt::OpenSSL::RSA->new_public_key($PUBLIC_KEY_PKCS1_STRING), "load PKCS1 public key" );
is( $public_key->get_public_key_string(), $PUBLIC_KEY_PKCS1_STRING, "PKCS1 public key round-trips" );
is( $public_key->get_public_key_x509_string(), $PUBLIC_KEY_X509_STRING, "PKCS1 key exports to X509 correctly" );
ok( $public_key = Crypt::OpenSSL::RSA->new_public_key($PUBLIC_KEY_X509_STRING), "load X509 public key" );
is( $public_key->get_public_key_string(), $PUBLIC_KEY_PKCS1_STRING, "X509 key exports to PKCS1 correctly" );
is( $public_key->get_public_key_x509_string(), $PUBLIC_KEY_X509_STRING, "X509 public key round-trips" );
# get_public_key_pkcs1_string is an alias for get_public_key_string
is( $private_key->get_public_key_pkcs1_string(), $PUBLIC_KEY_PKCS1_STRING, "get_public_key_pkcs1_string returns PKCS1 from private key" );
is( $public_key->get_public_key_pkcs1_string(), $PUBLIC_KEY_PKCS1_STRING, "get_public_key_pkcs1_string returns PKCS1 from public key" );
is( $private_key->get_public_key_pkcs1_string(), $private_key->get_public_key_string(), "pkcs1 alias matches get_public_key_string on private key" );
ok( $public_key = Crypt::OpenSSL::RSA->new_public_key($private_key->get_public_key_pkcs1_string()), "new_public_key accepts output of get_public_key_pkcs1_string" );
my $passphrase = '123456';
ok( $private_key = Crypt::OpenSSL::RSA->new_private_key( $ENCRYPT_PRIVATE_KEY_STRING, $passphrase ), "load encrypted private key" );
is( $private_key->get_private_key_string(), $DECRYPT_PRIVATE_KEY_STRING, "encrypted key decrypts to expected private key" );
ok( $private_key = Crypt::OpenSSL::RSA->new_private_key($DECRYPT_PRIVATE_KEY_STRING), "load decrypted private key" );
ok( $private_key2 = Crypt::OpenSSL::RSA->new_private_key( $private_key->get_private_key_string($passphrase), $passphrase ), "re-encrypt and reload with passphrase" );
is( $private_key2->get_private_key_string(), $DECRYPT_PRIVATE_KEY_STRING, "re-encrypted key round-trips" );
ok( $private_key2 = Crypt::OpenSSL::RSA->new_private_key( $private_key->get_private_key_string( $passphrase, 'des3' ), $passphrase ), "encrypt with des3 and reload" );
is( $private_key2->get_private_key_string(), $DECRYPT_PRIVATE_KEY_STRING, "des3-encrypted key round-trips" );
ok( $private_key2 = Crypt::OpenSSL::RSA->new_private_key( $private_key->get_private_key_string( $passphrase, 'aes-128-cbc' ), $passphrase ), "encrypt with aes-128-cbc and reload" );
is( $private_key2->get_private_key_string(), $DECRYPT_PRIVATE_KEY_STRING, "aes-128-cbc-encrypted key round-trips" );
# --- Additional cipher algorithms ---
ok( $private_key2 = Crypt::OpenSSL::RSA->new_private_key( $private_key->get_private_key_string( $passphrase, 'aes-256-cbc' ), $passphrase ), "encrypt with aes-256-cbc and reload" );
is( $private_key2->get_private_key_string(), $DECRYPT_PRIVATE_KEY_STRING, "aes-256-cbc-encrypted key round-trips" );
ok( $private_key2 = Crypt::OpenSSL::RSA->new_private_key( $private_key->get_private_key_string( $passphrase, 'aes-192-cbc' ), $passphrase ), "encrypt with aes-192-cbc and reload" );
is( $private_key2->get_private_key_string(), $DECRYPT_PRIVATE_KEY_STRING, "aes-192-cbc-encrypted key round-trips" );
# --- Passphrase with special characters ---
my $special_pass = q{p@ss!w0rd#$%^&*()};
ok( $private_key2 = Crypt::OpenSSL::RSA->new_private_key( $private_key->get_private_key_string($special_pass), $special_pass ), "passphrase with special characters round-trips" );
is( $private_key2->get_private_key_string(), $DECRYPT_PRIVATE_KEY_STRING, "special-char passphrase key decrypts correctly" );
# --- Error: cipher specified without passphrase ---
eval { $private_key->get_private_key_string(undef, 'des3') };
like($@, qr/Passphrase is required for cipher/, "get_private_key_string croaks when cipher given without passphrase");
# --- Error: unsupported cipher name ---
eval { $private_key->get_private_key_string($passphrase, 'bogus-cipher-xyz') };
like($@, qr/Unsupported cipher/, "get_private_key_string croaks on unsupported cipher");
# --- Error: export private key from public-only key ---
my $pub_only = Crypt::OpenSSL::RSA->new_public_key($PUBLIC_KEY_PKCS1_STRING);
eval { $pub_only->get_private_key_string() };
like($@, qr/Public keys cannot export private key strings/,
"get_private_key_string croaks on public-only key");
# --- Error: wrong passphrase on re-import ---
my $encrypted_pem = $private_key->get_private_key_string($passphrase, 'aes-128-cbc');
eval { Crypt::OpenSSL::RSA->new_private_key($encrypted_pem, 'wrong_passphrase') };
ok($@, "new_private_key croaks on wrong passphrase");
# --- Error: garbage / truncated private key input ---
eval { Crypt::OpenSSL::RSA->new_private_key("not a valid PEM key\n") };
ok( $@, "new_private_key croaks on garbage input" );
eval { Crypt::OpenSSL::RSA->new_private_key("-----BEGIN RSA PRIVATE KEY-----\ngarbage\n-----END RSA PRIVATE KEY-----\n") };
ok( $@, "new_private_key croaks on truncated PEM" );
# --- Public key format detection ---
eval { Crypt::OpenSSL::RSA->new_public_key("-----BEGIN CERTIFICATE-----\nfoo\n-----END CERTIFICATE-----\n") };
like($@, qr/unrecognized key format/, "new_public_key croaks on certificate PEM header");
eval { Crypt::OpenSSL::RSA->new_public_key("not a PEM key at all") };
like($@, qr/unrecognized key format/, "new_public_key croaks on non-PEM input");
# --- PKCS#8 private key export ---
{
my $rsa = Crypt::OpenSSL::RSA->new_private_key($DECRYPT_PRIVATE_KEY_STRING);
my $pkcs8_pem = $rsa->get_private_key_pkcs8_string();
like($pkcs8_pem, qr/^-----BEGIN PRIVATE KEY-----/m, "PKCS#8 output has correct header");
like($pkcs8_pem, qr/-----END PRIVATE KEY-----\s*$/m, "PKCS#8 output has correct footer");
unlike($pkcs8_pem, qr/BEGIN RSA PRIVATE KEY/, "PKCS#8 output is not PKCS#1 format");
# encrypted PKCS#8 export
my $pass = 'test_pkcs8_pass';
my $enc_pem = $rsa->get_private_key_pkcs8_string($pass, 'aes-128-cbc');
like($enc_pem, qr/^-----BEGIN ENCRYPTED PRIVATE KEY-----/m,
"encrypted PKCS#8 has correct header");
# Round-trip tests require new_private_key to read PKCS#8. On pre-3.x
# PEM_read_bio_PrivateKey is macro'd to PEM_read_bio_RSAPrivateKey which
# only reads PKCS#1, so these must be skipped.
SKIP: {
skip "new_private_key cannot read PKCS#8 on OpenSSL < 3.x", 3
if $major < 3;
my $reimported = Crypt::OpenSSL::RSA->new_private_key($pkcs8_pem);
is($reimported->get_private_key_string(), $DECRYPT_PRIVATE_KEY_STRING,
"PKCS#8 round-trip: re-import then export as PKCS#1 matches original");
is($reimported->get_private_key_pkcs8_string(), $pkcs8_pem,
"PKCS#8 round-trip: re-export as PKCS#8 matches");
my $dec_rsa = Crypt::OpenSSL::RSA->new_private_key($enc_pem, $pass);
is($dec_rsa->get_private_key_string(), $DECRYPT_PRIVATE_KEY_STRING,
"encrypted PKCS#8 round-trip decrypts to original key");
}
# error: cipher without passphrase
eval { $rsa->get_private_key_pkcs8_string(undef, 'des3') };
like($@, qr/Passphrase is required for cipher/,
"get_private_key_pkcs8_string croaks when cipher given without passphrase");
# error: unsupported cipher
eval { $rsa->get_private_key_pkcs8_string($pass, 'bogus-cipher-xyz') };
like($@, qr/Unsupported cipher/,
"get_private_key_pkcs8_string croaks on unsupported cipher");
}
# --- X509 public key from private key matches PKCS1 ---
my $priv_for_x509 = Crypt::OpenSSL::RSA->new_private_key($PRIVATE_KEY_STRING);
ok( $public_key = Crypt::OpenSSL::RSA->new_public_key($priv_for_x509->get_public_key_x509_string()), "load X509 public key from private key" );
is( $public_key->get_public_key_string(), $PUBLIC_KEY_PKCS1_STRING, "X509 from private key matches PKCS1" );
# --- Non-RSA key rejection ---
# On OpenSSL 3.x, the generic PEM loaders accept any key type.
# Verify we reject non-RSA keys with a clear error.
SKIP: {
my $ec_pem = `openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:prime256v1 2>/dev/null`;
skip "EC key generation not available", 4
unless ($? >> 8) == 0 && $ec_pem =~ /-----BEGIN PRIVATE KEY-----/;
eval { Crypt::OpenSSL::RSA->new_private_key($ec_pem) };
ok($@, "new_private_key rejects EC private key");
like($@, qr/not an RSA key|expecting an rsa key|ASN1/i, "EC private key error message mentions RSA");
my ($tmpfh, $tmpfile) = tempfile(UNLINK => 1);
print $tmpfh $ec_pem;
close $tmpfh;
my $ec_pub = `openssl pkey -in $tmpfile -pubout 2>/dev/null`;
skip "EC public key export failed", 2
unless ($? >> 8) == 0 && $ec_pub =~ /-----BEGIN PUBLIC KEY-----/;
eval { Crypt::OpenSSL::RSA->new_public_key($ec_pub) };
ok($@, "new_public_key rejects EC public key");
# traditional RSA, so RSA-PSS keys should also be rejected.
SKIP: {
my $rsa_pss_pem = `openssl genpkey -algorithm RSA-PSS -pkeyopt rsa_keygen_bits:2048 2>/dev/null`;
skip "RSA-PSS key generation not available", 4
unless ($? >> 8) == 0 && $rsa_pss_pem =~ /-----BEGIN PRIVATE KEY-----/;
# On pre-3.x OpenSSL, RSA-PSS keys are loaded via RSA-specific PEM
# readers which accept them (they are structurally RSA). The
# EVP_PKEY_get_base_id() rejection only exists on OpenSSL 3.x+.
eval { Crypt::OpenSSL::RSA->new_private_key($rsa_pss_pem) };
skip "RSA-PSS rejection not supported on this OpenSSL version (pre-3.x)", 4
unless $@;
ok(1, "new_private_key rejects RSA-PSS private key");
like($@, qr/not an RSA key|expecting an rsa key|ASN1/i, "RSA-PSS private key error message mentions RSA");
my ($tmpfh, $tmpfile) = tempfile(UNLINK => 1);
print $tmpfh $rsa_pss_pem;
close $tmpfh;
my $rsa_pss_pub = `openssl pkey -in $tmpfile -pubout 2>/dev/null`;
skip "RSA-PSS public key export failed", 2
unless ($? >> 8) == 0 && $rsa_pss_pub =~ /-----BEGIN PUBLIC KEY-----/;
eval { Crypt::OpenSSL::RSA->new_public_key($rsa_pss_pub) };
ok($@, "new_public_key rejects RSA-PSS public key");
t/key_lifecycle.t view on Meta::CPAN
$cross_verify = eval { $rsa1->verify($plaintext, $sig2) };
ok( !$@, "reverse cross-key verify does not croak" );
ok( !$cross_verify, "reverse cross-key verify returns false" );
# Same key should verify its own signature
ok( $rsa1->verify($plaintext, $sig1), "key1 verifies its own signature" );
ok( $rsa2->verify($plaintext, $sig2), "key2 verifies its own signature" );
# --- Key export â import â sign/verify round-trip ---
my $priv_pem = $rsa1->get_private_key_string();
my $pub_pem = $rsa1->get_public_key_string();
my $rsa1_copy = Crypt::OpenSSL::RSA->new_private_key($priv_pem);
my $rsa1_pub = Crypt::OpenSSL::RSA->new_public_key($pub_pem);
$rsa1_copy->use_pkcs1_pss_padding();
$rsa1_pub->use_pkcs1_pss_padding();
my $sig_copy = $rsa1_copy->sign($plaintext);
ok( $rsa1_pub->verify($plaintext, $sig_copy),
"exported private key signs, exported public key verifies" );
ok( $rsa1->verify($plaintext, $sig_copy),
"original key verifies signature from exported key" );
t/openssl_der.t view on Meta::CPAN
my $priv_d = get_parameter($priv_output, 'privateExponent', 'prime1');
my $priv_p = get_parameter($priv_output, 'prime1', 'prime2');
my $priv_q = get_parameter($priv_output, 'prime2', 'exponent1');
my $priv_dmp1 = get_parameter($priv_output, 'exponent1', 'exponent2');
my $priv_dmq1 = get_parameter($priv_output, 'exponent2', 'coefficient');
my $priv_iqmp = get_parameter($priv_output, 'coefficient', '-----BEGIN .*PRIVATE KEY-----');
my $priv_pem = extract_pem_body($priv_output,
'-----BEGIN .*PRIVATE KEY-----', '-----END .*PRIVATE KEY-----');
# Load the private key from the DER (base64 decoded PEM)
my $rsa = Crypt::OpenSSL::RSA->new_private_key(decode_base64($priv_pem));
# Get the private key parameters
my ($n, $e, $d, $p, $q, $dmp1, $dmq1, $iqmp) = $rsa->get_key_parameters();
# Check each private key parameter to the expected values
ok(compare_bignum_to_hex($n, $priv_n) == 0, "Imported DER n parameter matches expected");
ok(compare_bignum_to_hex($e, $priv_e) == 0, "Imported DER e parameter matches expected");
ok(compare_bignum_to_hex($d, $priv_d) == 0, "Imported DER d parameter matches expected");
ok(compare_bignum_to_hex($p, $priv_p) == 0, "Imported DER p parameter matches expected");
ok(compare_bignum_to_hex($q, $priv_q) == 0, "Imported DER q parameter matches expected");
t/padding.t view on Meta::CPAN
}
}
Crypt::OpenSSL::Random::random_seed("OpenSSL needs at least 32 bytes.");
Crypt::OpenSSL::RSA->import_random_seed();
my $rsa = Crypt::OpenSSL::RSA->generate_key(2048);
is( $rsa->size() * 8, 2048, "2048-bit key has correct size" );
ok( $rsa->check_key(), "2048-bit key passes check_key()" );
my $private_key_string = $rsa->get_private_key_string();
my $public_key_string = $rsa->get_public_key_string();
ok( $private_key_string and $public_key_string, "key strings are non-empty" );
my $plaintext = "The quick brown fox jumped over the lazy dog";
my $rsa_priv = Crypt::OpenSSL::RSA->new_private_key($private_key_string);
is( $rsa_priv->decrypt( $rsa_priv->encrypt($plaintext) ), $plaintext, "private key round-trips encrypt/decrypt" );
my $rsa_pub = Crypt::OpenSSL::RSA->new_public_key($public_key_string);
$plaintext .= $plaintext x 5;
# sslv23 is unsupported on OpenSSL 3.x but LibreSSL still supports it
SKIP: {
skip "sslv23 is available on OpenSSL < 3.0 and LibreSSL", 2
if $major lt '3.0' || $is_libressl;
eval {
t/pkcs1_sign.t view on Meta::CPAN
like($@, qr/Marvin|vulnerable/i,
"PKCS#1 v1.5 encryption still blocked (Marvin)");
}
# --- Reload key from PEM and verify signature ---
{
$rsa->use_pkcs1_padding();
$rsa->use_sha256_hash();
my $sig = $rsa->sign("persistence test");
my $priv_pem = $rsa->get_private_key_string();
my $rsa2 = Crypt::OpenSSL::RSA->new_private_key($priv_pem);
$rsa2->use_pkcs1_padding();
$rsa2->use_sha256_hash();
ok($rsa2->verify("persistence test", $sig),
"signature verifies after key round-trip through PEM");
my $rsa_pub2 = Crypt::OpenSSL::RSA->new_public_key($pub_pem);
$rsa_pub2->use_pkcs1_padding();
$rsa_pub2->use_sha256_hash();
ok($rsa_pub2->verify("persistence test", $sig),
"signature verifies with fresh public key object");
$rsa->use_no_padding();
_Test_Encrypt_And_Decrypt( $rsa->size(), $rsa, 1 );
$rsa->use_pkcs1_oaep_padding();
# private_encrypt does not work with pkcs1_oaep_padding
_Test_Encrypt_And_Decrypt( $rsa->size() - 42, $rsa, 0 );
#FIXME - use_sslv23_padding seems to fail on decryption. openssl bug?
my $private_key_string = $rsa->get_private_key_string();
my $public_key_string = $rsa->get_public_key_string();
ok( $private_key_string and $public_key_string, "get_private_key_string and get_public_key_string return data" );
my $plaintext = "The quick brown fox jumped over the lazy dog";
my $rsa_priv = Crypt::OpenSSL::RSA->new_private_key($private_key_string);
is( $rsa_priv->decrypt( $rsa_priv->encrypt($plaintext) ), $plaintext, "private key round-trips encrypt/decrypt" );
my $rsa_pub = Crypt::OpenSSL::RSA->new_public_key($public_key_string);
$rsa->use_pkcs1_oaep_padding();
is( $rsa->decrypt( $rsa_pub->encrypt($plaintext) ), $plaintext, "pub encrypt + priv decrypt round-trips" );
ok( $rsa_priv->is_private(), "private key reports is_private() true" );
ok( !$rsa_pub->is_private(), "public key reports is_private() false" );
_check_for_croak(
( run in 2.049 seconds using v1.01-cache-2.11-cpan-39bf76dae61 )