diff --git a/MANIFEST b/MANIFEST index e77ce8f..967a04d 100644 --- a/MANIFEST +++ b/MANIFEST @@ -52,6 +52,7 @@ MANIFEST.SKIP README.md t/lib/Test/ACME2_Server.pm t/lib/Test/Crypt.pm +t/Net-ACME2-AccountKey.t t/Net-ACME2-Authorization.t t/Net-ACME2-Challenge-dns_01.t t/Net-ACME2-Challenge-dns_account_01.t diff --git a/t/Net-ACME2-AccountKey.t b/t/Net-ACME2-AccountKey.t new file mode 100644 index 0000000..f4d7fe9 --- /dev/null +++ b/t/Net-ACME2-AccountKey.t @@ -0,0 +1,278 @@ +#!/usr/bin/env perl + +use strict; +use warnings; + +use Test::More; +use Test::Exception; +use Test::FailWarnings; + +use JSON (); +use MIME::Base64 (); +use Crypt::Format (); + +use Net::ACME2::AccountKey (); + +#---------------------------------------------------------------------- +# Test keys (same PEM strings used across the test suite) +#---------------------------------------------------------------------- + +my $_RSA_KEY = <new($_RSA_KEY); + isa_ok($rsa, 'Net::ACME2::AccountKey', 'RSA PEM constructs AccountKey'); + is($rsa->get_type(), 'rsa', 'RSA PEM key type is rsa'); +} + +{ + my $p256 = Net::ACME2::AccountKey->new($_P256_KEY); + isa_ok($p256, 'Net::ACME2::AccountKey', 'P-256 PEM constructs AccountKey'); + is($p256->get_type(), 'ecdsa', 'P-256 PEM key type is ecdsa'); +} + +{ + my $p384 = Net::ACME2::AccountKey->new($_P384_KEY); + isa_ok($p384, 'Net::ACME2::AccountKey', 'P-384 PEM constructs AccountKey'); + is($p384->get_type(), 'ecdsa', 'P-384 PEM key type is ecdsa'); +} + +#---------------------------------------------------------------------- +# Tests: constructor with DER inputs (no PEM header to guess from) +#---------------------------------------------------------------------- + +{ + my $rsa_der = Crypt::Format::pem2der($_RSA_KEY); + my $rsa = Net::ACME2::AccountKey->new($rsa_der); + is($rsa->get_type(), 'rsa', 'RSA DER key type is rsa'); +} + +{ + my $ecc_der = Crypt::Format::pem2der($_P256_KEY); + my $ecc = Net::ACME2::AccountKey->new($ecc_der); + is($ecc->get_type(), 'ecdsa', 'P-256 DER key type is ecdsa'); +} + +#---------------------------------------------------------------------- +# Tests: get_struct_for_public_jwk (RSA) +#---------------------------------------------------------------------- + +{ + my $rsa = Net::ACME2::AccountKey->new($_RSA_KEY); + my $jwk = $rsa->get_struct_for_public_jwk(); + + is(ref($jwk), 'HASH', 'RSA JWK is a hashref'); + is($jwk->{'kty'}, 'RSA', 'RSA JWK kty is RSA'); + ok($jwk->{'n'}, 'RSA JWK has modulus (n)'); + ok($jwk->{'e'}, 'RSA JWK has exponent (e)'); + + # Public JWK must not contain private components + ok(!$jwk->{'d'}, 'RSA public JWK has no private exponent (d)'); + ok(!$jwk->{'p'}, 'RSA public JWK has no prime factor (p)'); +} + +#---------------------------------------------------------------------- +# Tests: get_struct_for_public_jwk (ECDSA) +#---------------------------------------------------------------------- + +{ + my $p256 = Net::ACME2::AccountKey->new($_P256_KEY); + my $jwk = $p256->get_struct_for_public_jwk(); + + is(ref($jwk), 'HASH', 'P-256 JWK is a hashref'); + is($jwk->{'kty'}, 'EC', 'P-256 JWK kty is EC'); + is($jwk->{'crv'}, 'P-256', 'P-256 JWK curve is P-256'); + ok($jwk->{'x'}, 'P-256 JWK has x coordinate'); + ok($jwk->{'y'}, 'P-256 JWK has y coordinate'); + ok(!$jwk->{'d'}, 'P-256 public JWK has no private key (d)'); +} + +{ + my $p384 = Net::ACME2::AccountKey->new($_P384_KEY); + my $jwk = $p384->get_struct_for_public_jwk(); + + is($jwk->{'crv'}, 'P-384', 'P-384 JWK curve is P-384'); +} + +#---------------------------------------------------------------------- +# Tests: get_jwk_thumbprint +#---------------------------------------------------------------------- + +{ + my $rsa = Net::ACME2::AccountKey->new($_RSA_KEY); + my $tp = $rsa->get_jwk_thumbprint(); + ok(defined($tp) && length($tp) > 0, 'RSA thumbprint is non-empty'); + + # Thumbprint must be deterministic + my $tp2 = $rsa->get_jwk_thumbprint(); + is($tp2, $tp, 'RSA thumbprint is deterministic'); +} + +{ + my $p256 = Net::ACME2::AccountKey->new($_P256_KEY); + my $tp = $p256->get_jwk_thumbprint(); + ok(defined($tp) && length($tp) > 0, 'P-256 thumbprint is non-empty'); + + my $rsa = Net::ACME2::AccountKey->new($_RSA_KEY); + isnt($tp, $rsa->get_jwk_thumbprint(), 'different keys yield different thumbprints'); +} + +#---------------------------------------------------------------------- +# Tests: get_jwa_alg (ECDSA only) +#---------------------------------------------------------------------- + +{ + my $p256 = Net::ACME2::AccountKey->new($_P256_KEY); + is($p256->get_jwa_alg(), 'ES256', 'P-256 JWA alg is ES256'); +} + +{ + my $p384 = Net::ACME2::AccountKey->new($_P384_KEY); + is($p384->get_jwa_alg(), 'ES384', 'P-384 JWA alg is ES384'); +} + +#---------------------------------------------------------------------- +# Tests: sign_RS256 (RSA) +#---------------------------------------------------------------------- + +{ + my $rsa = Net::ACME2::AccountKey->new($_RSA_KEY); + my $sig = $rsa->sign_RS256('hello world'); + + ok(defined($sig) && length($sig) > 0, 'RSA sign_RS256 produces a signature'); + + # RSA signatures are deterministic (PKCS#1 v1.5) + my $sig2 = $rsa->sign_RS256('hello world'); + is($sig2, $sig, 'RSA sign_RS256 is deterministic for same input'); + + # Different messages yield different signatures + my $sig3 = $rsa->sign_RS256('different message'); + isnt($sig3, $sig, 'RSA sign_RS256 differs for different input'); +} + +#---------------------------------------------------------------------- +# Tests: sign_jwa (ECDSA) +#---------------------------------------------------------------------- + +{ + my $p256 = Net::ACME2::AccountKey->new($_P256_KEY); + my $sig = $p256->sign_jwa('hello world'); + + ok(defined($sig) && length($sig) > 0, 'P-256 sign_jwa produces a signature'); + + # ECDSA signatures are non-deterministic, so we can't compare + # two signatures for equality. Just verify both are non-empty. + my $sig2 = $p256->sign_jwa('hello world'); + ok(defined($sig2) && length($sig2) > 0, 'P-256 sign_jwa produces a second signature'); +} + +{ + my $p384 = Net::ACME2::AccountKey->new($_P384_KEY); + my $sig = $p384->sign_jwa('test message'); + ok(defined($sig) && length($sig) > 0, 'P-384 sign_jwa produces a signature'); +} + +#---------------------------------------------------------------------- +# Tests: signature verification round-trip +#---------------------------------------------------------------------- +# Verify that what AccountKey signs can be verified by Crypt::Perl. + +{ + require Crypt::Perl::PK; + + # RSA round-trip + my $rsa = Net::ACME2::AccountKey->new($_RSA_KEY); + my $msg = 'verify this message'; + my $sig = $rsa->sign_RS256($msg); + + my $rsa_pub = Crypt::Perl::PK::parse_key($_RSA_KEY); + ok($rsa_pub->verify_RS256($msg, $sig), 'RSA signature verifies via Crypt::Perl'); + + # ECDSA round-trip + my $p256 = Net::ACME2::AccountKey->new($_P256_KEY); + my $ecc_sig = $p256->sign_jwa('ecc verify'); + + my $ecc_pub = Crypt::Perl::PK::parse_key($_P256_KEY); + ok($ecc_pub->verify_jwa('ecc verify', $ecc_sig), 'P-256 signature verifies via Crypt::Perl'); +} + +#---------------------------------------------------------------------- +# Tests: constructor preserves caller's $@ +#---------------------------------------------------------------------- + +{ + $@ = 'sentinel error value'; + my $key = Net::ACME2::AccountKey->new($_RSA_KEY); + is($@, 'sentinel error value', 'constructor preserves caller $@'); +} + +#---------------------------------------------------------------------- +# Tests: PEM vs DER produce equivalent keys +#---------------------------------------------------------------------- + +{ + my $pem_key = Net::ACME2::AccountKey->new($_RSA_KEY); + my $der_key = Net::ACME2::AccountKey->new(Crypt::Format::pem2der($_RSA_KEY)); + + is_deeply( + $pem_key->get_struct_for_public_jwk(), + $der_key->get_struct_for_public_jwk(), + 'RSA PEM and DER produce identical public JWK', + ); + + is( + $pem_key->get_jwk_thumbprint(), + $der_key->get_jwk_thumbprint(), + 'RSA PEM and DER produce identical thumbprint', + ); +} + +{ + my $pem_key = Net::ACME2::AccountKey->new($_P256_KEY); + my $der_key = Net::ACME2::AccountKey->new(Crypt::Format::pem2der($_P256_KEY)); + + is_deeply( + $pem_key->get_struct_for_public_jwk(), + $der_key->get_struct_for_public_jwk(), + 'P-256 PEM and DER produce identical public JWK', + ); +} + +done_testing();