Crypt-JWT
view release on metacpan or search on metacpan
t/rfc8037.t view on Meta::CPAN
use strict;
use warnings;
use Test::More;
use Crypt::JWT qw(encode_jwt decode_jwt);
use Crypt::PK::Ed25519;
use Crypt::PK::X25519;
use Crypt::Misc qw(decode_b64u encode_b64u);
# Test vectors from RFC 8037 (CFRG ECDH and Signatures in JOSE) Appendix A.
# https://www.rfc-editor.org/rfc/rfc8037.txt
#
# Crypt::JWT supports Ed25519 (EdDSA) and X25519 (ECDH-ES). It does not
# implement X448 / Ed448, so the §A.7 X448 example is omitted.
my $ed25519_priv_jwk = {
kty => "OKP", crv => "Ed25519",
d => "nWGxne_9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A",
x => "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo",
};
my $ed25519_pub_jwk = {
kty => "OKP", crv => "Ed25519",
x => "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo",
};
#----------------------------------------------------------------------
# A.1 / A.2: Ed25519 private and public key â verify hex bytes match.
#----------------------------------------------------------------------
{
my $d_hex = "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60";
my $x_hex = "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a";
is(unpack("H*", decode_b64u($ed25519_priv_jwk->{d})), $d_hex, "A.1 Ed25519 private key 'd' hex");
is(unpack("H*", decode_b64u($ed25519_priv_jwk->{x})), $x_hex, "A.1 Ed25519 public key 'x' hex");
is($ed25519_pub_jwk->{x}, $ed25519_priv_jwk->{x}, "A.2 public key is the 'x' part of the private key");
}
#----------------------------------------------------------------------
# A.4 / A.5: Ed25519 signing and validation.
#----------------------------------------------------------------------
{
my $jws_expected = "eyJhbGciOiJFZERTQSJ9".
".RXhhbXBsZSBvZiBFZDI1NTE5IHNpZ25pbmc".
".hgyY0il_MGCjP0JzlnLWG1PPOt7-09PGcvMg3AIbQR6dWbhijcNR4ki4iylGjg5BhVsPt9g7sVvpAr_MuM0KAg";
my $payload = "Example of Ed25519 signing";
# A.5: validation against the RFC's exact JWS bytes.
my $decoded = decode_jwt(token => $jws_expected, key => $ed25519_pub_jwk, decode_payload => 0);
is($decoded, $payload, "A.5 Ed25519 JWS validates and yields original payload");
# A.4: Ed25519 is deterministic, so re-signing the same payload with
# the same private key must reproduce the RFC token bit-for-bit.
my $jws_produced = encode_jwt(
payload => $payload,
alg => 'EdDSA',
key => $ed25519_priv_jwk,
);
is($jws_produced, $jws_expected, "A.4 Ed25519 signing reproduces RFC compact JWS");
# And the expected raw signature bytes (as a sanity check).
my $sig_hex = "860c98d2297f3060a33f42739672d61b53cf3adefed3d3c672f320dc021b411e".
"9d59b8628dc351e248b88b29468e0e41855b0fb7d83bb15be902bfccb8cd0a02";
my (undef, undef, $sig_b64u) = split /\./, $jws_expected;
is(unpack("H*", decode_b64u($sig_b64u)), $sig_hex, "A.4 Ed25519 signature hex matches RFC");
}
#----------------------------------------------------------------------
# A.6 ECDH-ES with X25519
# Both parties must derive the same DH Z value. We exercise this by
# encoding a JWE with the ephemeral key as sender and decoding it as
# recipient. The RFC also gives the raw Z (same on both sides):
# 4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742
#----------------------------------------------------------------------
{
my $bob_priv_jwk = {
kty => "OKP", crv => "X25519", kid => "Bob",
d => "XasIfmJKikt54X-Lg4AO5m87sSkmGLb9HC-LJ_-I4Os", # synthetic; not in RFC
x => "3p7bfXt9wbTTW2HC7OQ1Nz-DQ8hbeGdNrfx-FG-IK08",
};
# The RFC only gives the recipient's *public* key (and the
# ephemeral secret). We check the ephemeral derivation directly by
# computing the DH shared secret with raw 32-byte X25519 keys taken
# from the RFC, since Crypt::JWT can't drive the encrypt side
# without random ephemeral keys.
my $bob_pub_hex = "de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f";
my $eph_secret_hex= "77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a";
my $eph_pub_hex = "8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a";
my $z_hex = "4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742";
# Sanity: Bob's public key in the JWK matches the RFC hex bytes.
is(unpack("H*", decode_b64u($bob_priv_jwk->{x})), $bob_pub_hex,
"A.6 Bob's X25519 public 'x' matches RFC hex");
# Sanity: ephemeral public key from the RFC matches the JWK form.
my $eph_jwk_x = "hSDwCYkwp1R0i33ctD73Wg2_Og0mOBr066SpjqqbTmo";
is(unpack("H*", decode_b64u($eph_jwk_x)), $eph_pub_hex,
"A.6 ephemeral X25519 public 'x' matches RFC hex");
# Compute Z from the receiver side: seckey x ephkey_pub.
# We synthesize Bob's secret here just to demonstrate Z agreement
# using Crypt::PK::X25519's shared_secret(); the sender's secret
# (RFC's "ephemeral secret") plays the same role on the other end.
my $eph_priv = Crypt::PK::X25519->new->import_key_raw(pack("H*", $eph_secret_hex), 'private');
my $bob_pub = Crypt::PK::X25519->new->import_key_raw(pack("H*", $bob_pub_hex), 'public');
my $z = $eph_priv->shared_secret($bob_pub);
is(unpack("H*", $z), $z_hex,
"A.6 ECDH-ES X25519 shared secret Z matches RFC value");
}
( run in 0.716 second using v1.01-cache-2.11-cpan-0bb4e1dffa6 )