diff --git a/inc/CryptX_PK_Ed25519.xs.inc b/inc/CryptX_PK_Ed25519.xs.inc index 6b56bff..5c467a6 100644 --- a/inc/CryptX_PK_Ed25519.xs.inc +++ b/inc/CryptX_PK_Ed25519.xs.inc @@ -125,7 +125,7 @@ key2hash(Crypt::PK::Ed25519 self) PREINIT: HV *rv_hash; - char buf[256]; + char buf[32 * 2 + 1]; unsigned long blen; SV **not_used; int rv; @@ -164,6 +164,32 @@ RETVAL = newSVpvn(NULL, 0); /* undef */ if (strnEQ(type, "private", 7)) { + rv = ed25519_export(out, &out_len, PK_PRIVATE|PK_STD, &self->key); + if (rv != CRYPT_OK) croak("FATAL: ed25519_export(PK_PRIVATE|PK_STD) failed: %s", error_to_string(rv)); + RETVAL = newSVpvn((char*)out, out_len); + } + else if (strnEQ(type, "public", 6)) { + rv = ed25519_export(out, &out_len, PK_PUBLIC|PK_STD, &self->key); + if (rv != CRYPT_OK) croak("FATAL: ed25519_export(PK_PUBLIC|PK_STD) failed: %s", error_to_string(rv)); + RETVAL = newSVpvn((char*)out, out_len); + } + else { + croak("FATAL: export_key_der invalid type '%s'", type); + } + } + OUTPUT: + RETVAL + +SV* +export_key_raw(Crypt::PK::Ed25519 self, char * type) + CODE: + { + int rv; + unsigned char out[4096]; + unsigned long int out_len = sizeof(out); + + RETVAL = newSVpvn(NULL, 0); /* undef */ + if (strnEQ(type, "private", 7)) { rv = ed25519_export(out, &out_len, PK_PRIVATE, &self->key); if (rv != CRYPT_OK) croak("FATAL: ed25519_export(PK_PRIVATE) failed: %s", error_to_string(rv)); RETVAL = newSVpvn((char*)out, out_len); @@ -174,7 +200,7 @@ RETVAL = newSVpvn((char*)out, out_len); } else { - croak("FATAL: export_key_der invalid type '%s'", type); + croak("FATAL: export_key_raw invalid type '%s'", type); } } OUTPUT: diff --git a/inc/CryptX_PK_X25519.xs.inc b/inc/CryptX_PK_X25519.xs.inc index 69c7919..e2c50a3 100644 --- a/inc/CryptX_PK_X25519.xs.inc +++ b/inc/CryptX_PK_X25519.xs.inc @@ -125,7 +125,7 @@ key2hash(Crypt::PK::X25519 self) PREINIT: HV *rv_hash; - char buf[256]; + char buf[32 * 2 + 1]; unsigned long blen; SV **not_used; int rv; @@ -169,8 +169,8 @@ RETVAL = newSVpvn((char*)out, out_len); } else if (strnEQ(type, "public", 6)) { - rv = x25519_export(out, &out_len, PK_PUBLIC, &self->key); - if (rv != CRYPT_OK) croak("FATAL: x25519_export(PK_PUBLIC) failed: %s", error_to_string(rv)); + rv = x25519_export(out, &out_len, PK_PUBLIC|PK_STD, &self->key); + if (rv != CRYPT_OK) croak("FATAL: x25519_export(PK_PUBLIC|PK_STD) failed: %s", error_to_string(rv)); RETVAL = newSVpvn((char*)out, out_len); } else { diff --git a/lib/Crypt/PK/Ed25519.pm b/lib/Crypt/PK/Ed25519.pm index ffd2e87..23e4312 100644 --- a/lib/Crypt/PK/Ed25519.pm +++ b/lib/Crypt/PK/Ed25519.pm @@ -23,6 +23,7 @@ sub import_key_raw { my ($self, $key, $type) = @_; croak "FATAL: undefined key" unless $key; + croak "FATAL: invalid key" unless length($key) == 32; croak "FATAL: undefined type" unless $type; return $self->_import_raw($key, 1) if $type eq 'private'; return $self->_import_raw($key, 0) if $type eq 'public'; @@ -95,6 +96,11 @@ # https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.key?annotate=HEAD croak "FATAL: OPENSSH PRIVATE KEY not supported"; } + elsif ($data =~ /---- BEGIN SSH2 PUBLIC KEY ----(.*?)---- END SSH2 PUBLIC KEY ----/sg) { + $data = pem_to_der($data); + my ($typ, $pubkey) = Crypt::PK::_ssh_parse($data); + return $self->_import_raw($pubkey, 0) if $typ eq 'ssh-ed25519' && length($pubkey) == 32; + } elsif ($data =~ /(ssh-ed25519)\s+(\S+)/) { $data = decode_b64("$2"); my ($typ, $pubkey) = Crypt::PK::_ssh_parse($data); @@ -150,26 +156,94 @@ =head2 generate_key +Uses Yarrow-based cryptographically strong random number generator seeded with +random data taken from C (UNIX) or C (Win32). + + $pk->generate_key; + =head2 import_key +TODO + =head2 import_key_raw +Import raw public/private key - can load raw key data exported by L. + + $pk->import_key_raw($key, 'public'); + $pk->import_key_raw($key, 'private'); + =head2 export_key_der + my $private_der = $pk->export_key_der('private'); + #or + my $public_der = $pk->export_key_der('public'); + =head2 export_key_pem + my $private_pem = $pk->export_key_pem('private'); + #or + my $public_pem = $pk->export_key_pem('public'); + =head2 export_key_jwk +Exports public/private keys as a JSON Web Key (JWK). + + my $private_json_text = $pk->export_key_jwk('private'); + #or + my $public_json_text = $pk->export_key_jwk('public'); + +Also exports public/private keys as a perl HASH with JWK structure. + + my $jwk_hash = $pk->export_key_jwk('private', 1); + #or + my $jwk_hash = $pk->export_key_jwk('public', 1); + +B For JWK support you need to have L, L or L module. + =head2 export_key_raw +Export raw public/private key + + my $private_pem = $pk->export_key_raw('private'); + #or + my $public_pem = $pk->export_key_raw('public'); + =head2 sign_message + my $signature = $priv->sign_message($message); + #or + my $signature = $priv->sign_message($message, $hash_name); + + #NOTE: $hash_name can be 'SHA1' (DEFAULT), 'SHA256' or any other hash supported by Crypt::Digest + =head2 verify_message + my $valid = $pub->verify_message($signature, $message) + #or + my $valid = $pub->verify_message($signature, $message, $hash_name); + + #NOTE: $hash_name can be 'SHA1' (DEFAULT), 'SHA256' or any other hash supported by Crypt::Digest + =head2 is_private + my $rv = $pk->is_private; + # 1 .. private key loaded + # 0 .. public key loaded + # undef .. no key loaded + =head2 key2hash + my $hash = $pk->key2hash; + + # returns hash like this (or undef if no key loaded): + { + curve => "ed25519", + # raw public key as a hexadecimal string + pub => "A05D1AEA5830AC9A65CDFB384660D497E3697C46B419CF2CEC85DE8BD245459D", + # raw private key as a hexadecimal string. undef if key is public only + priv => "45C109BA6FD24E8B67D23EFB6B92D99CD457E2137172C0D749FE2B5A0C142DAD", + } + =head1 SEE ALSO =over diff --git a/t/data/ssh/ssh_ed25519.pub.rfc4716 b/t/data/ssh/ssh_ed25519.pub.rfc4716 new file mode 100644 index 0000000..000906e --- /dev/null +++ b/t/data/ssh/ssh_ed25519.pub.rfc4716 @@ -0,0 +1,4 @@ +---- BEGIN SSH2 PUBLIC KEY ---- +Comment: "256-bit ED25519, converted by foo@bar from OpenSSH" +AAAAC3NzaC1lZDI1NTE5AAAAIL0XsiFcRDp6Hpsoak8OdiiBMJhM2UKszNTxoGS7dJ++ +---- END SSH2 PUBLIC KEY ---- diff --git a/t/pk_ed25519.t b/t/pk_ed25519.t index d79ab1e..531add3 100644 --- a/t/pk_ed25519.t +++ b/t/pk_ed25519.t @@ -1,8 +1,9 @@ use strict; use warnings; -use Test::More tests => 55; +use Test::More tests => 74; use Crypt::PK::Ed25519; +use Crypt::Misc qw(read_rawfile); { my $k; @@ -12,26 +13,34 @@ # priv = 45C109BA6FD24E8B67D23EFB6B92D99CD457E2137172C0D749FE2B5A0C142DAD == RcEJum_STotn0j77a5LZnNRX4hNxcsDXSf4rWgwULa0 # pub = A05D1AEA5830AC9A65CDFB384660D497E3697C46B419CF2CEC85DE8BD245459D == oF0a6lgwrJplzfs4RmDUl-NpfEa0Gc8s7IXei9JFRZ0 - $k = Crypt::PK::Ed25519->new->import_key_raw(pack("H*", "45C109BA6FD24E8B67D23EFB6B92D99CD457E2137172C0D749FE2B5A0C142DAD"), 'private'); + my $sk_data = pack("H*", "45C109BA6FD24E8B67D23EFB6B92D99CD457E2137172C0D749FE2B5A0C142DAD"); + $k = Crypt::PK::Ed25519->new->import_key_raw($sk_data, 'private'); ok($k, 'new+import_key_raw raw-priv'); ok($k->is_private, 'is_private raw-priv'); is(uc($k->key2hash->{priv}), '45C109BA6FD24E8B67D23EFB6B92D99CD457E2137172C0D749FE2B5A0C142DAD', 'key2hash->{priv} raw-priv'); is(uc($k->key2hash->{pub}), 'A05D1AEA5830AC9A65CDFB384660D497E3697C46B419CF2CEC85DE8BD245459D', 'key2hash->{pub} raw-priv'); + is($k->export_key_raw('private'), $sk_data, 'export_key_raw private'); - $k = Crypt::PK::Ed25519->new->import_key_raw(pack("H*", "A05D1AEA5830AC9A65CDFB384660D497E3697C46B419CF2CEC85DE8BD245459D"), 'public'); + my $pk_data = pack("H*", "A05D1AEA5830AC9A65CDFB384660D497E3697C46B419CF2CEC85DE8BD245459D"); + $k = Crypt::PK::Ed25519->new->import_key_raw($pk_data, 'public'); ok($k, 'new+import_key_raw raw-pub'); ok(!$k->is_private, '!is_private raw-pub'); is(uc($k->key2hash->{pub}), 'A05D1AEA5830AC9A65CDFB384660D497E3697C46B419CF2CEC85DE8BD245459D', 'key2hash->{pub} raw-pub'); + is($k->export_key_raw('public'), $pk_data, 'export_key_raw public'); - $k = Crypt::PK::Ed25519->new({ kty=>"OKP",crv=>"Ed25519",d=>"RcEJum_STotn0j77a5LZnNRX4hNxcsDXSf4rWgwULa0",x=>"oF0a6lgwrJplzfs4RmDUl-NpfEa0Gc8s7IXei9JFRZ0"}); + my $sk_jwk = { kty=>"OKP",crv=>"Ed25519",d=>"RcEJum_STotn0j77a5LZnNRX4hNxcsDXSf4rWgwULa0",x=>"oF0a6lgwrJplzfs4RmDUl-NpfEa0Gc8s7IXei9JFRZ0" }; + $k = Crypt::PK::Ed25519->new($sk_jwk); ok($k, 'new JWKHASH/priv'); ok($k->is_private, 'is_private JWKHASH/priv'); is(uc($k->key2hash->{priv}), '45C109BA6FD24E8B67D23EFB6B92D99CD457E2137172C0D749FE2B5A0C142DAD', 'key2hash->{priv} JWKHASH/priv'); + ok(eq_hash($sk_jwk, $k->export_key_jwk('private', 1)), 'JWKHASH export private'); - $k = Crypt::PK::Ed25519->new({ kty=>"OKP",crv=>"Ed25519",x=>"oF0a6lgwrJplzfs4RmDUl-NpfEa0Gc8s7IXei9JFRZ0"}); + my $pk_jwk = { kty=>"OKP",crv=>"Ed25519",x=>"oF0a6lgwrJplzfs4RmDUl-NpfEa0Gc8s7IXei9JFRZ0" }; + $k = Crypt::PK::Ed25519->new($pk_jwk); ok($k, 'new JWKHASH/pub'); ok(!$k->is_private, '!is_private JWKHASH/pub'); is(uc($k->key2hash->{pub}), 'A05D1AEA5830AC9A65CDFB384660D497E3697C46B419CF2CEC85DE8BD245459D', 'key2hash->{pub} JWKHASH/pub'); + ok(eq_hash($pk_jwk, $k->export_key_jwk('public', 1)), 'JWKHASH export public'); $k = Crypt::PK::Ed25519->new('t/data/jwk_ed25519-priv1.json'); ok($k, 'new JWK/priv'); @@ -103,11 +112,56 @@ ok(!$k->is_private, '!is_private ssh_ed25519.pub'); is(uc($k->key2hash->{pub}), 'BD17B2215C443A7A1E9B286A4F0E76288130984CD942ACCCD4F1A064BB749FBE', 'key2hash->{pub} ssh_ed25519.pub'); + $k = Crypt::PK::Ed25519->new('t/data/ssh/ssh_ed25519.pub.rfc4716'); + ok($k, 'new ssh_ed25519.pub.rfc4716'); + ok(!$k->is_private, '!is_private ssh_ed25519.pub.rfc4716'); + is(uc($k->key2hash->{pub}), 'BD17B2215C443A7A1E9B286A4F0E76288130984CD942ACCCD4F1A064BB749FBE', 'key2hash->{pub} ssh_ed25519.pub.rfc4716'); + ### $k = Crypt::PK::Ed25519->new('t/data/ssh/ssh_ed25519.priv'); ### ok($k, 'new ssh_ed25519.priv'); - ### ok($k->is_private, 'is_private ssh_ed25519.priv'); - ### + ## ok($k->is_private, 'is_private ssh_ed25519.priv'); + ### $k = Crypt::PK::Ed25519->new('t/data/ssh/ssh_ed25519_pw.priv', 'secret'); ### ok($k, 'new ssh_ed25519_pw.priv'); ### ok($k->is_private, 'is_private ssh_ed25519_pw.priv'); } + +{ + my $k = Crypt::PK::Ed25519->new; + $k->generate_key; + ok($k, 'generate_key'); + ok($k->is_private, 'is_private'); + ok($k->export_key_der('private'), 'export_key_der pri'); + ok($k->export_key_der('public'), 'export_key_der pub'); +} + +{ + for (qw( openssl_ed25519_pk.der openssl_ed25519_pk.pem )) { + my $k = Crypt::PK::Ed25519->new("t/data/$_"); + is($k->export_key_der('public'), read_rawfile("t/data/$_"), 'export_key_der public') if (substr($_, -3) eq "der"); + is($k->export_key_pem('public'), read_rawfile("t/data/$_"), 'export_key_pem public') if (substr($_, -3) eq "pem"); + } + + for (qw( openssl_ed25519_sk.der openssl_ed25519_sk_t.pem )) { + my $k = Crypt::PK::Ed25519->new("t/data/$_"); + is($k->export_key_der('private'), read_rawfile("t/data/$_"), 'export_key_der private') if (substr($_, -3) eq "der"); + is($k->export_key_pem('private'), read_rawfile("t/data/$_"), 'export_key_pem private') if (substr($_, -3) eq "pem"); + } +} + +{ + my $sk = Crypt::PK::Ed25519->new; + $sk->import_key('t/data/openssl_ed25519_sk.der'); + my $pk = Crypt::PK::Ed25519->new; + $pk->import_key('t/data/openssl_ed25519_pk.der'); + + my $sig = $sk->sign_message("message"); + ok(length $sig > 60, 'sign_message ' . length($sig)); + ok($pk->verify_message($sig, "message"), 'verify_message'); + + my $hash = pack("H*","04624fae618e9ad0c5e479f62e1420c71fff34dd"); + $sig = $sk->sign_hash($hash, 'SHA1'); + ok(length $sig > 60, 'sign_hash ' . length($sig)); + ok($pk->verify_hash($sig, $hash, 'SHA1'), 'verify_hash'); +} +