diff --git a/CryptX.xs b/CryptX.xs index 9440296..3648693 100644 --- a/CryptX.xs +++ b/CryptX.xs @@ -148,13 +148,19 @@ ecc_key key; } *Crypt__PK__ECC; -struct curve25519_struct { +typedef struct ed25519_struct { /* used by Crypt::PK::Ed25519 */ prng_state pstate; int pindex; curve25519_key key; -} ; -typedef struct curve25519_struct *Crypt__PK__Ed25519; /* used by Crypt::PK::Ed25519 */ -typedef struct curve25519_struct *Crypt__PK__X25519; /* used by Crypt::PK::X25519 */ + int initialized; +} *Crypt__PK__Ed25519; + +typedef struct x25519_struct { /* used by Crypt::PK::X25519 */ + prng_state pstate; + int pindex; + curve25519_key key; + int initialized; +} *Crypt__PK__X25519; int mp_tohex_with_leading_zero(mp_int * a, char *str, int maxlen, int minlen) { int len, rv; diff --git a/inc/CryptX_PK_Ed25519.xs.inc b/inc/CryptX_PK_Ed25519.xs.inc index 3442c42..6b56bff 100644 --- a/inc/CryptX_PK_Ed25519.xs.inc +++ b/inc/CryptX_PK_Ed25519.xs.inc @@ -7,8 +7,9 @@ CODE: { int rv; - Newz(0, RETVAL, 1, struct curve25519_struct); + Newz(0, RETVAL, 1, struct ed25519_struct); if (!RETVAL) croak("FATAL: Newz failed"); + RETVAL->initialized = 0; RETVAL->pindex = find_prng("chacha20"); RETVAL->key.type = -1; if (RETVAL->pindex == -1) { @@ -32,6 +33,7 @@ /* gen the key */ rv = ed25519_make_key(&self->pstate, self->pindex, &self->key); if (rv != CRYPT_OK) croak("FATAL: ed25519_make_key failed: %s", error_to_string(rv)); + self->initialized = 1; XPUSHs(ST(0)); /* return self */ } @@ -47,6 +49,7 @@ self->key.type = -1; rv = ed25519_import(data, (unsigned long)data_len, &self->key); if (rv != CRYPT_OK) croak("FATAL: ed25519_import failed: %s", error_to_string(rv)); + self->initialized = 1; XPUSHs(ST(0)); /* return self */ } @@ -65,6 +68,7 @@ self->key.type = -1; rv = ed25519_import_pkcs8(data, (unsigned long)data_len, pwd, (unsigned long)pwd_len, &self->key); if (rv != CRYPT_OK) croak("FATAL: ed25519_import_pkcs8 failed: %s", error_to_string(rv)); + self->initialized = 1; XPUSHs(ST(0)); /* return self */ } @@ -80,33 +84,39 @@ self->key.type = -1; rv = ed25519_import_x509(data, (unsigned long)data_len, &self->key); if (rv != CRYPT_OK) croak("FATAL: ed25519_import_x509 failed: %s", error_to_string(rv)); - XPUSHs(ST(0)); /* return self */ - } - -void -_import_key_data(Crypt::PK::Ed25519 self, SV * priv, SV * pub) - PPCODE: - { - int rv, type; - unsigned char *priv_data=NULL, *pub_data=NULL; - STRLEN priv_len=0, pub_len=0; - - if (SvOK(priv)) { - priv_data = (unsigned char *)SvPVbyte(priv, priv_len); - } - if (SvOK(pub)) { - pub_data = (unsigned char *)SvPVbyte(pub, pub_len); - } - self->key.type = -1; - rv = ed25519_set_key(priv_data, (unsigned long)priv_len, pub_data, (unsigned long)pub_len, &self->key); - if (rv != CRYPT_OK) croak("FATAL: ed25519_set_key failed: %s", error_to_string(rv)); + self->initialized = 1; + XPUSHs(ST(0)); /* return self */ + } + +void +_import_raw(Crypt::PK::Ed25519 self, SV * key, int which) + PPCODE: + { + int rv; + unsigned char *key_data=NULL; + STRLEN key_len=0; + + if (SvOK(key)) { + key_data = (unsigned char *)SvPVbyte(key, key_len); + } + if (which == 0) { + rv = ed25519_import_raw(key_data, (unsigned long)key_len, PK_PUBLIC, &self->key); + } + else if (which == 1) { + rv = ed25519_import_raw(key_data, (unsigned long)key_len, PK_PRIVATE, &self->key); + } + else { + croak("FATAL: import_raw invalid type '%d'", which); + } + if (rv != CRYPT_OK) croak("FATAL: ed25519_import_raw failed: %s", error_to_string(rv)); + self->initialized = 1; XPUSHs(ST(0)); /* return self */ } int is_private(Crypt::PK::Ed25519 self) CODE: - if (self->key.type == -1) XSRETURN_UNDEF; + if (self->initialized == 0 || self->key.type == -1) XSRETURN_UNDEF; RETVAL = (self->key.type == PK_PRIVATE) ? 1 : 0; OUTPUT: RETVAL @@ -115,22 +125,30 @@ key2hash(Crypt::PK::Ed25519 self) PREINIT: HV *rv_hash; - char buf[20001]; + char buf[256]; + unsigned long blen; SV **not_used; - CODE: - if (self->key.type == -1) XSRETURN_UNDEF; + int rv; + CODE: + if (self->initialized == 0) XSRETURN_UNDEF; rv_hash = newHV(); /* priv */ if (self->key.type == PK_PRIVATE) { - not_used = hv_store(rv_hash, "priv", 4, newSVpv(self->key.priv, sizeof(self->key.priv)), 0); + blen = sizeof(buf); + rv = base16_encode(self->key.priv, sizeof(self->key.priv), buf, &blen, 0); + if (rv != CRYPT_OK) croak("FATAL: base16_encode failed: %s", error_to_string(rv)); + not_used = hv_store(rv_hash, "priv", 4, newSVpv(buf, blen), 0); } else { not_used = hv_store(rv_hash, "priv", 4, &PL_sv_undef, 0); } /* pub */ - not_used = hv_store(rv_hash, "pub", 3, newSVpv(self->key.pub, sizeof(self->key.pub)), 0); - /* algo */ - not_used = hv_store(rv_hash, "algo", 4, newSVpv("ed25519", 0), 0); + blen = sizeof(buf); + rv = base16_encode(self->key.pub, sizeof(self->key.pub), buf, &blen, 0); + if (rv != CRYPT_OK) croak("FATAL: base16_encode failed: %s", error_to_string(rv)); + not_used = hv_store(rv_hash, "pub", 3, newSVpv(buf, blen), 0); + /* curve */ + not_used = hv_store(rv_hash, "curve", 5, newSVpv("ed25519", 0), 0); LTC_UNUSED_PARAM(not_used); RETVAL = newRV_noinc((SV*)rv_hash); OUTPUT: @@ -151,8 +169,8 @@ 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)); + rv = ed25519_export(out, &out_len, PK_PUBLIC, &self->key); + if (rv != CRYPT_OK) croak("FATAL: ed25519_export(PK_PUBLIC) failed: %s", error_to_string(rv)); RETVAL = newSVpvn((char*)out, out_len); } else { @@ -222,4 +240,3 @@ DESTROY(Crypt::PK::Ed25519 self) CODE: Safefree(self); - diff --git a/inc/CryptX_PK_X25519.xs.inc b/inc/CryptX_PK_X25519.xs.inc index c6be9ee..69c7919 100644 --- a/inc/CryptX_PK_X25519.xs.inc +++ b/inc/CryptX_PK_X25519.xs.inc @@ -7,8 +7,9 @@ CODE: { int rv; - Newz(0, RETVAL, 1, struct curve25519_struct); + Newz(0, RETVAL, 1, struct x25519_struct); if (!RETVAL) croak("FATAL: Newz failed"); + RETVAL->initialized = 0; RETVAL->pindex = find_prng("chacha20"); RETVAL->key.type = -1; if (RETVAL->pindex == -1) { @@ -32,6 +33,7 @@ /* gen the key */ rv = x25519_make_key(&self->pstate, self->pindex, &self->key); if (rv != CRYPT_OK) croak("FATAL: x25519_make_key failed: %s", error_to_string(rv)); + self->initialized = 1; XPUSHs(ST(0)); /* return self */ } @@ -47,6 +49,7 @@ self->key.type = -1; rv = x25519_import(data, (unsigned long)data_len, &self->key); if (rv != CRYPT_OK) croak("FATAL: x25519_import failed: %s", error_to_string(rv)); + self->initialized = 1; XPUSHs(ST(0)); /* return self */ } @@ -65,6 +68,7 @@ self->key.type = -1; rv = x25519_import_pkcs8(data, (unsigned long)data_len, pwd, (unsigned long)pwd_len, &self->key); if (rv != CRYPT_OK) croak("FATAL: x25519_import_pkcs8 failed: %s", error_to_string(rv)); + self->initialized = 1; XPUSHs(ST(0)); /* return self */ } @@ -80,26 +84,32 @@ self->key.type = -1; rv = x25519_import_x509(data, (unsigned long)data_len, &self->key); if (rv != CRYPT_OK) croak("FATAL: x25519_import_x509 failed: %s", error_to_string(rv)); - XPUSHs(ST(0)); /* return self */ - } - -void -_import_key_data(Crypt::PK::X25519 self, SV * priv, SV * pub) - PPCODE: - { - int rv, type; - unsigned char *priv_data=NULL, *pub_data=NULL; - STRLEN priv_len=0, pub_len=0; - - if (SvOK(priv)) { - priv_data = (unsigned char *)SvPVbyte(priv, priv_len); - } - if (SvOK(pub)) { - pub_data = (unsigned char *)SvPVbyte(pub, pub_len); - } - self->key.type = -1; - rv = x25519_set_key(priv_data, (unsigned long)priv_len, pub_data, (unsigned long)pub_len, &self->key); - if (rv != CRYPT_OK) croak("FATAL: x25519_set_key failed: %s", error_to_string(rv)); + self->initialized = 1; + XPUSHs(ST(0)); /* return self */ + } + +void +_import_raw(Crypt::PK::X25519 self, SV * key, int which) + PPCODE: + { + int rv; + unsigned char *key_data=NULL; + STRLEN key_len=0; + + if (SvOK(key)) { + key_data = (unsigned char *)SvPVbyte(key, key_len); + } + if (which == 0) { + rv = x25519_import_raw(key_data, (unsigned long)key_len, PK_PUBLIC, &self->key); + } + else if (which == 1) { + rv = x25519_import_raw(key_data, (unsigned long)key_len, PK_PRIVATE, &self->key); + } + else { + croak("FATAL: import_raw invalid type '%d'", which); + } + if (rv != CRYPT_OK) croak("FATAL: x25519_import_raw failed: %s", error_to_string(rv)); + self->initialized = 1; XPUSHs(ST(0)); /* return self */ } @@ -115,22 +125,30 @@ key2hash(Crypt::PK::X25519 self) PREINIT: HV *rv_hash; - char buf[20001]; + char buf[256]; + unsigned long blen; SV **not_used; - CODE: - if (self->key.type == -1) XSRETURN_UNDEF; + int rv; + CODE: + if (self->initialized == 0) XSRETURN_UNDEF; rv_hash = newHV(); /* priv */ if (self->key.type == PK_PRIVATE) { - not_used = hv_store(rv_hash, "priv", 4, newSVpv(self->key.priv, sizeof(self->key.priv)), 0); + blen = sizeof(buf); + rv = base16_encode(self->key.priv, sizeof(self->key.priv), buf, &blen, 0); + if (rv != CRYPT_OK) croak("FATAL: base16_encode failed: %s", error_to_string(rv)); + not_used = hv_store(rv_hash, "priv", 4, newSVpv(buf, blen), 0); } else { not_used = hv_store(rv_hash, "priv", 4, &PL_sv_undef, 0); } /* pub */ - not_used = hv_store(rv_hash, "pub", 3, newSVpv(self->key.pub, sizeof(self->key.pub)), 0); - /* algo */ - not_used = hv_store(rv_hash, "algo", 4, newSVpv("x25519", 0), 0); + blen = sizeof(buf); + rv = base16_encode(self->key.pub, sizeof(self->key.pub), buf, &blen, 0); + if (rv != CRYPT_OK) croak("FATAL: base16_encode failed: %s", error_to_string(rv)); + not_used = hv_store(rv_hash, "pub", 3, newSVpv(buf, blen), 0); + /* curve */ + not_used = hv_store(rv_hash, "curve", 5, newSVpv("x25519", 0), 0); LTC_UNUSED_PARAM(not_used); RETVAL = newRV_noinc((SV*)rv_hash); OUTPUT: @@ -151,8 +169,8 @@ RETVAL = newSVpvn((char*)out, out_len); } else if (strnEQ(type, "public", 6)) { - 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)); + 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)); RETVAL = newSVpvn((char*)out, out_len); } else { diff --git a/lib/Crypt/PK/Ed25519.pm b/lib/Crypt/PK/Ed25519.pm new file mode 100644 index 0000000..ffd2e87 --- /dev/null +++ b/lib/Crypt/PK/Ed25519.pm @@ -0,0 +1,185 @@ +package Crypt::PK::Ed25519; + +use strict; +use warnings; +our $VERSION = '0.066_001'; + +require Exporter; our @ISA = qw(Exporter); ### use Exporter 'import'; +our %EXPORT_TAGS = ( all => [qw( )] ); +our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } ); +our @EXPORT = qw(); + +use Carp; +$Carp::Internal{(__PACKAGE__)}++; +use CryptX; +use Crypt::PK; +use Crypt::Misc qw(read_rawfile encode_b64u decode_b64u encode_b64 decode_b64 pem_to_der der_to_pem); + +sub new { + my $self = shift->_new(); + return @_ > 0 ? $self->import_key(@_) : $self; +} + +sub import_key_raw { + my ($self, $key, $type) = @_; + croak "FATAL: undefined key" unless $key; + 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'; + croak "FATAL: invalid key type '$type'"; +} + +sub import_key { + my ($self, $key, $password) = @_; + local $SIG{__DIE__} = \&CryptX::_croak; + croak "FATAL: undefined key" unless $key; + + # special case + if (ref($key) eq 'HASH') { + if ($key->{kty} && $key->{kty} eq "OKP" && $key->{crv} && $key->{crv} eq 'Ed25519') { + # JWK-like structure e.g. + # {"kty":"OKP","crv":"Ed25519","d":"...","x":"..."} + return $self->_import_raw(decode_b64u($key->{d}), 1) if $key->{d}; # private + return $self->_import_raw(decode_b64u($key->{x}), 0) if $key->{x}; # public + } + if ($key->{curve} && $key->{curve} eq "ed25519" && ($key->{priv} || $key->{pub})) { + # hash exported via key2hash + return $self->_import_raw(pack("H*", $key->{priv}), 1) if $key->{priv}; + return $self->_import_raw(pack("H*", $key->{pub}), 0) if $key->{pub}; + } + croak "FATAL: unexpected Ed25519 key hash"; + } + + my $data; + if (ref($key) eq 'SCALAR') { + $data = $$key; + } + elsif (-f $key) { + $data = read_rawfile($key); + } + else { + croak "FATAL: non-existing file '$key'"; + } + croak "FATAL: invalid key data" unless $data; + + if ($data =~ /-----BEGIN PUBLIC KEY-----(.*?)-----END/sg) { + $data = pem_to_der($data, $password); + return $self->_import($data); + } + elsif ($data =~ /-----BEGIN PRIVATE KEY-----(.*?)-----END/sg) { + $data = pem_to_der($data, $password); + return $self->_import_pkcs8($data, $password); + } + elsif ($data =~ /-----BEGIN ENCRYPTED PRIVATE KEY-----(.*?)-----END/sg) { + $data = pem_to_der($data, $password); + return $self->_import_pkcs8($data, $password); + } + elsif ($data =~ /-----BEGIN ED25519 PRIVATE KEY-----(.*?)-----END/sg) { + $data = pem_to_der($data, $password); + return $self->_import_pkcs8($data, $password); + } + elsif ($data =~ /^\s*(\{.*?\})\s*$/s) { # JSON + my $h = CryptX::_decode_json("$1"); + if ($h->{kty} && $h->{kty} eq "OKP" && $h->{crv} && $h->{crv} eq 'Ed25519') { + return $self->_import_raw(decode_b64u($h->{d}), 1) if $h->{d}; # private + return $self->_import_raw(decode_b64u($h->{x}), 0) if $h->{x}; # public + } + } + elsif ($data =~ /-----BEGIN CERTIFICATE-----(.*?)-----END CERTIFICATE-----/sg) { + $data = pem_to_der($data); + return $self->_import_x509($data); + } + elsif ($data =~ /-----BEGIN OPENSSH PRIVATE KEY-----(.*?)-----END/sg) { + #XXX-FIXME-TODO + # https://crypto.stackexchange.com/questions/71789/openssh-ed2215-private-key-format + # https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.key?annotate=HEAD + croak "FATAL: OPENSSH PRIVATE KEY not supported"; + } + elsif ($data =~ /(ssh-ed25519)\s+(\S+)/) { + $data = decode_b64("$2"); + my ($typ, $pubkey) = Crypt::PK::_ssh_parse($data); + return $self->_import_raw($pubkey, 0) if $typ eq 'ssh-ed25519' && length($pubkey) == 32; + } + elsif (length($data) == 32) { + croak "FATAL: use import_key_raw() to load raw (32 bytes) Ed25519 key"; + } + else { + my $rv = eval { $self->_import($data) } || + eval { $self->_import_pkcs8($data, $password) } || + eval { $self->_import_x509($data) }; + return $rv if $rv; + } + croak "FATAL: invalid or unsupported Ed25519 key format"; +} + +sub export_key_pem { + my ($self, $type, $password, $cipher) = @_; + local $SIG{__DIE__} = \&CryptX::_croak; + my $key = $self->export_key_der($type||''); + return unless $key; + return der_to_pem($key, "ED25519 PRIVATE KEY", $password, $cipher) if substr($type, 0, 7) eq 'private'; + return der_to_pem($key, "PUBLIC KEY") if substr($type,0, 6) eq 'public'; +} + +sub export_key_jwk { + my ($self, $type, $wanthash) = @_; + local $SIG{__DIE__} = \&CryptX::_croak; + my $kh = $self->key2hash; + return unless $kh; + my $hash = { kty => "OKP", crv => "Ed25519" }; + $hash->{x} = encode_b64u(pack("H*", $kh->{pub})); + $hash->{d} = encode_b64u(pack("H*", $kh->{priv})) if $type && $type eq 'private' && $kh->{priv}; + return $wanthash ? $hash : CryptX::_encode_json($hash); +} + +sub CLONE_SKIP { 1 } # prevent cloning + +1; + +=pod + +=head1 NAME + +Crypt::PK::Ed25519 - Digital signature based on Ed25519 + +=head1 SYNOPSIS + +=head1 METHODS + +=head2 new + +=head2 generate_key + +=head2 import_key + +=head2 import_key_raw + +=head2 export_key_der + +=head2 export_key_pem + +=head2 export_key_jwk + +=head2 export_key_raw + +=head2 sign_message + +=head2 verify_message + +=head2 is_private + +=head2 key2hash + +=head1 SEE ALSO + +=over + +=item * L + +=item * L + +=item * L + +=back + +=cut diff --git a/lib/Crypt/PK/X25519.pm b/lib/Crypt/PK/X25519.pm new file mode 100644 index 0000000..4725a9c --- /dev/null +++ b/lib/Crypt/PK/X25519.pm @@ -0,0 +1,156 @@ +package Crypt::PK::X25519; + +use strict; +use warnings; +our $VERSION = '0.066_001'; + +require Exporter; our @ISA = qw(Exporter); ### use Exporter 'import'; +our %EXPORT_TAGS = ( all => [qw( )] ); +our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } ); +our @EXPORT = qw(); + +use Carp; +$Carp::Internal{(__PACKAGE__)}++; +use CryptX; +use Crypt::PK; +use Crypt::Misc qw(read_rawfile encode_b64u decode_b64u encode_b64 decode_b64 pem_to_der der_to_pem); + +sub new { + my $self = shift->_new(); + return @_ > 0 ? $self->import_key(@_) : $self; +} + +sub import_key_raw { + my ($self, $key, $type) = @_; + croak "FATAL: undefined key" unless $key; + 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'; + croak "FATAL: invalid key type '$type'"; +} + +sub import_key { + my ($self, $key, $password) = @_; + local $SIG{__DIE__} = \&CryptX::_croak; + croak "FATAL: undefined key" unless $key; + + # special case + if (ref($key) eq 'HASH') { + if ($key->{kty} && $key->{kty} eq "OKP" && $key->{crv} && $key->{crv} eq 'X25519') { + # JWK-like structure e.g. + # {"kty":"OKP","crv":"X25519","d":"...","x":"..."} + return $self->_import_raw(decode_b64u($key->{d}), 1) if $key->{d}; # private + return $self->_import_raw(decode_b64u($key->{x}), 0) if $key->{x}; # public + } + if ($key->{curve} && $key->{curve} eq "x25519" && ($key->{priv} || $key->{pub})) { + # hash exported via key2hash + return $self->_import_raw(pack("H*", $key->{priv}), 1) if $key->{priv}; + return $self->_import_raw(pack("H*", $key->{pub}), 0) if $key->{pub}; + } + croak "FATAL: unexpected X25519 key hash"; + } + + my $data; + if (ref($key) eq 'SCALAR') { + $data = $$key; + } + elsif (-f $key) { + $data = read_rawfile($key); + } + else { + croak "FATAL: non-existing file '$key'"; + } + croak "FATAL: invalid key data" unless $data; + + if ($data =~ /-----BEGIN PUBLIC KEY-----(.*?)-----END/sg) { + $data = pem_to_der($data, $password); + return $self->_import($data); + } + elsif ($data =~ /-----BEGIN PRIVATE KEY-----(.*?)-----END/sg) { + $data = pem_to_der($data, $password); + return $self->_import_pkcs8($data, $password); + } + elsif ($data =~ /-----BEGIN ENCRYPTED PRIVATE KEY-----(.*?)-----END/sg) { + $data = pem_to_der($data, $password); + return $self->_import_pkcs8($data, $password); + } + elsif ($data =~ /-----BEGIN X25519 PRIVATE KEY-----(.*?)-----END/sg) { + $data = pem_to_der($data, $password); + return $self->_import_pkcs8($data, $password); + } + elsif ($data =~ /^\s*(\{.*?\})\s*$/s) { # JSON + my $h = CryptX::_decode_json("$1"); + if ($h->{kty} && $h->{kty} eq "OKP" && $h->{crv} && $h->{crv} eq 'X25519') { + return $self->_import_raw(decode_b64u($h->{d}), 1) if $h->{d}; # private + return $self->_import_raw(decode_b64u($h->{x}), 0) if $h->{x}; # public + } + } + elsif (length($data) == 32) { + croak "FATAL: use import_key_raw() to load raw (32 bytes) X25519 key"; + } + else { + my $rv = eval { $self->_import($data) } || + eval { $self->_import_pkcs8($data, $password) } || + eval { $self->_import_x509($data) }; + return $rv if $rv; + } + croak "FATAL: invalid or unsupported X25519 key format"; +} + +sub export_key_pem { + my ($self, $type, $password, $cipher) = @_; + local $SIG{__DIE__} = \&CryptX::_croak; + my $key = $self->export_key_der($type||''); + return unless $key; + return der_to_pem($key, "X25519 PRIVATE KEY", $password, $cipher) if substr($type, 0, 7) eq 'private'; + return der_to_pem($key, "PUBLIC KEY") if substr($type,0, 6) eq 'public'; +} + +sub export_key_jwk { + my ($self, $type, $wanthash) = @_; + local $SIG{__DIE__} = \&CryptX::_croak; + my $kh = $self->key2hash; + return unless $kh; + my $hash = { kty => "OKP", crv => "X25519" }; + $hash->{x} = encode_b64u(pack("H*", $kh->{pub})); + $hash->{d} = encode_b64u(pack("H*", $kh->{priv})) if $type && $type eq 'private' && $kh->{priv}; + return $wanthash ? $hash : CryptX::_encode_json($hash); +} + +sub CLONE_SKIP { 1 } # prevent cloning + +1; + +=pod + +=head1 NAME + +Crypt::PK::X25519 - Asymmetric cryptography based on X25519 + +=head1 SYNOPSIS + +=head1 METHODS + +=head2 new + +=head2 generate_key + +=head2 import_key + +=head2 import_key_raw + +=head2 export_key_der + +=head2 export_key_pem + +=head2 export_key_jwk + +=head2 export_key_raw + +=head2 shared_secret + +=head2 is_private + +=head2 key2hash + +=cut diff --git a/lib/CryptX.pm b/lib/CryptX.pm index 2046cca..484dedf 100644 --- a/lib/CryptX.pm +++ b/lib/CryptX.pm @@ -110,7 +110,7 @@ =item * Public key cryptography -L, L, L, L +L, L, L, L, L, L =item * Cryptographically secure random number generators - see L and related modules diff --git a/t/001_compile.t b/t/001_compile.t index 1abc45f..6fd8695 100644 --- a/t/001_compile.t +++ b/t/001_compile.t @@ -101,6 +101,8 @@ use Crypt::PK::DSA; use Crypt::PK::ECC; use Crypt::PK::RSA; +use Crypt::PK::X25519; +use Crypt::PK::Ed25519; use Crypt::PK; use Crypt::PRNG::ChaCha20; use Crypt::PRNG::Fortuna; diff --git a/t/003_all_pm_pod.t b/t/003_all_pm_pod.t index 85771b5..2da04e9 100644 --- a/t/003_all_pm_pod.t +++ b/t/003_all_pm_pod.t @@ -6,7 +6,7 @@ plan skip_all => "set AUTHOR_MODE to enable this test (developer only!)" unless $ENV{AUTHOR_MODE}; plan skip_all => "File::Find not installed" unless eval { require File::Find }; plan skip_all => "Test::Pod not installed" unless eval { require Test::Pod }; -plan tests => 107; +plan tests => 109; my @files; File::Find::find({ wanted=>sub { push @files, $_ if /\.pm$/ }, no_chdir=>1 }, 'lib'); diff --git a/t/004_all_pm_pod_spelling.t b/t/004_all_pm_pod_spelling.t index b692782..12bb5a5 100644 --- a/t/004_all_pm_pod_spelling.t +++ b/t/004_all_pm_pod_spelling.t @@ -22,7 +22,7 @@ }, ); -plan tests => 107; +plan tests => 109; my @files; File::Find::find({ wanted=>sub { push @files, $_ if /\.pm$/ }, no_chdir=>1 }, 'lib'); diff --git a/t/005_all_pm_pod_coverage.t b/t/005_all_pm_pod_coverage.t index f922b23..c35fbda 100644 --- a/t/005_all_pm_pod_coverage.t +++ b/t/005_all_pm_pod_coverage.t @@ -6,7 +6,7 @@ plan skip_all => "set AUTHOR_MODE to enable this test (developer only!)" unless $ENV{AUTHOR_MODE}; plan skip_all => "Pod::Coverage not installed" unless eval { require Pod::Coverage }; plan skip_all => "File::Find not installed" unless eval { require File::Find }; -plan tests => 107; +plan tests => 109; my @files; File::Find::find({ wanted=>sub { push @files, $_ if /\.pm$/ }, no_chdir=>1 }, 'lib'); diff --git a/t/pk_ed25519.t b/t/pk_ed25519.t new file mode 100644 index 0000000..d79ab1e --- /dev/null +++ b/t/pk_ed25519.t @@ -0,0 +1,113 @@ +use strict; +use warnings; +use Test::More tests => 55; + +use Crypt::PK::Ed25519; + +{ + my $k; + + # t/data/openssl_ed25519_sk.pem + # ED25519 Private-Key: + # priv = 45C109BA6FD24E8B67D23EFB6B92D99CD457E2137172C0D749FE2B5A0C142DAD == RcEJum_STotn0j77a5LZnNRX4hNxcsDXSf4rWgwULa0 + # pub = A05D1AEA5830AC9A65CDFB384660D497E3697C46B419CF2CEC85DE8BD245459D == oF0a6lgwrJplzfs4RmDUl-NpfEa0Gc8s7IXei9JFRZ0 + + $k = Crypt::PK::Ed25519->new->import_key_raw(pack("H*", "45C109BA6FD24E8B67D23EFB6B92D99CD457E2137172C0D749FE2B5A0C142DAD"), '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'); + + $k = Crypt::PK::Ed25519->new->import_key_raw(pack("H*", "A05D1AEA5830AC9A65CDFB384660D497E3697C46B419CF2CEC85DE8BD245459D"), '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'); + + $k = Crypt::PK::Ed25519->new({ kty=>"OKP",crv=>"Ed25519",d=>"RcEJum_STotn0j77a5LZnNRX4hNxcsDXSf4rWgwULa0",x=>"oF0a6lgwrJplzfs4RmDUl-NpfEa0Gc8s7IXei9JFRZ0"}); + ok($k, 'new JWKHASH/priv'); + ok($k->is_private, 'is_private JWKHASH/priv'); + is(uc($k->key2hash->{priv}), '45C109BA6FD24E8B67D23EFB6B92D99CD457E2137172C0D749FE2B5A0C142DAD', 'key2hash->{priv} JWKHASH/priv'); + + $k = Crypt::PK::Ed25519->new({ kty=>"OKP",crv=>"Ed25519",x=>"oF0a6lgwrJplzfs4RmDUl-NpfEa0Gc8s7IXei9JFRZ0"}); + ok($k, 'new JWKHASH/pub'); + ok(!$k->is_private, '!is_private JWKHASH/pub'); + is(uc($k->key2hash->{pub}), 'A05D1AEA5830AC9A65CDFB384660D497E3697C46B419CF2CEC85DE8BD245459D', 'key2hash->{pub} JWKHASH/pub'); + + $k = Crypt::PK::Ed25519->new('t/data/jwk_ed25519-priv1.json'); + ok($k, 'new JWK/priv'); + ok($k->is_private, 'is_private JWK/priv'); + is(uc($k->key2hash->{priv}), '45C109BA6FD24E8B67D23EFB6B92D99CD457E2137172C0D749FE2B5A0C142DAD', 'key2hash->{priv} JWK/priv'); + + $k = Crypt::PK::Ed25519->new('t/data/jwk_ed25519-pub1.json'); + ok($k, 'new JWK/pub'); + ok(!$k->is_private, '!is_private JWK/pub'); + is(uc($k->key2hash->{pub}), 'A05D1AEA5830AC9A65CDFB384660D497E3697C46B419CF2CEC85DE8BD245459D', 'key2hash->{pub} JWK/pub'); + + $k = Crypt::PK::Ed25519->new('t/data/openssl_ed25519_sk.der'); + ok($k, 'new openssl_ed25519_sk.der'); + ok($k->is_private, 'is_private openssl_ed25519_sk.der'); + is(uc($k->key2hash->{priv}), '45C109BA6FD24E8B67D23EFB6B92D99CD457E2137172C0D749FE2B5A0C142DAD', 'key2hash->{priv} openssl_ed25519_sk.der'); + + $k = Crypt::PK::Ed25519->new('t/data/openssl_ed25519_sk.pem'); + ok($k, 'new openssl_ed25519_sk.pem'); + ok($k->is_private, 'is_private openssl_ed25519_sk.pem'); + is(uc($k->key2hash->{priv}), '45C109BA6FD24E8B67D23EFB6B92D99CD457E2137172C0D749FE2B5A0C142DAD', 'key2hash->{priv} openssl_ed25519_sk.pem'); + + $k = Crypt::PK::Ed25519->new('t/data/openssl_ed25519_sk_t.pem'); + ok($k, 'new openssl_ed25519_sk_t.pem'); + ok($k->is_private, 'is_private openssl_ed25519_sk_t.pem'); + is(uc($k->key2hash->{priv}), '45C109BA6FD24E8B67D23EFB6B92D99CD457E2137172C0D749FE2B5A0C142DAD', 'key2hash->{priv} openssl_ed25519_sk_t.pem'); + + $k = Crypt::PK::Ed25519->new('t/data/openssl_ed25519_sk.pkcs8'); + ok($k, 'new openssl_ed25519_sk.pkcs8'); + ok($k->is_private, 'is_private openssl_ed25519_sk.pkcs8'); + is(uc($k->key2hash->{priv}), '45C109BA6FD24E8B67D23EFB6B92D99CD457E2137172C0D749FE2B5A0C142DAD', 'key2hash->{priv} openssl_ed25519_sk.pkcs8'); + + $k = Crypt::PK::Ed25519->new('t/data/openssl_ed25519_sk_pbes1.pkcs8', 'secret'); + ok($k, 'new openssl_ed25519_sk_pbes1.pkcs8'); + ok($k->is_private, 'is_private openssl_ed25519_sk_pbes1.pkcs8'); + is(uc($k->key2hash->{priv}), '45C109BA6FD24E8B67D23EFB6B92D99CD457E2137172C0D749FE2B5A0C142DAD', 'key2hash->{priv} openssl_ed25519_sk_pbes1.pkcs8'); + + $k = Crypt::PK::Ed25519->new('t/data/openssl_ed25519_sk_pbes2.pkcs8', 'secret'); + ok($k, 'new openssl_ed25519_sk_pbes2.pkcs8'); + ok($k->is_private, 'is_private openssl_ed25519_sk_pbes2.pkcs8'); + is(uc($k->key2hash->{priv}), '45C109BA6FD24E8B67D23EFB6B92D99CD457E2137172C0D749FE2B5A0C142DAD', 'key2hash->{priv} openssl_ed25519_sk_pbes2.pkcs8'); + + $k = Crypt::PK::Ed25519->new('t/data/openssl_ed25519_sk_pw.pem', 'secret'); + ok($k, 'new openssl_ed25519_sk_pw.pem'); + ok($k->is_private, 'is_private openssl_ed25519_sk_pw.pem'); + is(uc($k->key2hash->{priv}), '45C109BA6FD24E8B67D23EFB6B92D99CD457E2137172C0D749FE2B5A0C142DAD', 'key2hash->{priv} openssl_ed25519_sk_pw.pem'); + + $k = Crypt::PK::Ed25519->new('t/data/openssl_ed25519_sk_pw_t.pem', 'secret'); + ok($k, 'new openssl_ed25519_sk_pw_t.pem'); + ok($k->is_private, 'is_private openssl_ed25519_sk_pw_t.pem'); + is(uc($k->key2hash->{priv}), '45C109BA6FD24E8B67D23EFB6B92D99CD457E2137172C0D749FE2B5A0C142DAD', 'key2hash->{priv} openssl_ed25519_sk_pw_t.pem'); + + $k = Crypt::PK::Ed25519->new('t/data/openssl_ed25519_pk.pem'); + ok($k, 'new openssl_ed25519_pk.pem'); + ok(!$k->is_private, '!is_private openssl_ed25519_pk.pem'); + is(uc($k->key2hash->{pub}), 'A05D1AEA5830AC9A65CDFB384660D497E3697C46B419CF2CEC85DE8BD245459D', 'key2hash->{pub} openssl_ed25519_pk.pem'); + + $k = Crypt::PK::Ed25519->new('t/data/openssl_ed25519_x509.pem'); + ok($k, 'new openssl_ed25519_x509.pem'); + ok(!$k->is_private, '!is_private openssl_ed25519_x509.pem'); + is(uc($k->key2hash->{pub}), 'A05D1AEA5830AC9A65CDFB384660D497E3697C46B419CF2CEC85DE8BD245459D', 'key2hash->{pub} openssl_ed25519_x509.pem'); + + $k = Crypt::PK::Ed25519->new('t/data/openssl_ed25519_x509.der'); + ok($k, 'new openssl_ed25519_x509.der'); + ok(!$k->is_private, '!is_private openssl_ed25519_x509.der'); + is(uc($k->key2hash->{pub}), 'A05D1AEA5830AC9A65CDFB384660D497E3697C46B419CF2CEC85DE8BD245459D', 'key2hash->{pub} openssl_ed25519_x509.der'); + + $k = Crypt::PK::Ed25519->new('t/data/ssh/ssh_ed25519.pub'); + ok($k, 'new ssh_ed25519.pub'); + 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.priv'); + ### ok($k, 'new 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'); +} diff --git a/t/pk_x25519.t b/t/pk_x25519.t new file mode 100644 index 0000000..8494b1e --- /dev/null +++ b/t/pk_x25519.t @@ -0,0 +1,90 @@ +use strict; +use warnings; +use Test::More tests => 46; + +use Crypt::PK::X25519; + +{ + my $k; + + # t/data/openssl_x25519_sk.pem + # X25519 Private-Key: + # priv = 002F93D10BA5728D8DD8E9527721DABA3261C0BB1BEFDE7B4BBDAC631D454651 == AC-T0Qulco2N2OlSdyHaujJhwLsb7957S72sYx1FRlE + # pub = EA7806F721A8570512C8F6EFB4E8D620C49A529E4DF5EAA77DEC646FB1E87E41 == 6ngG9yGoVwUSyPbvtOjWIMSaUp5N9eqnfexkb7HofkE + + $k = Crypt::PK::X25519->new->import_key_raw(pack("H*", "002F93D10BA5728D8DD8E9527721DABA3261C0BB1BEFDE7B4BBDAC631D454651"), 'private'); + ok($k, 'new+import_key_raw raw-priv'); + ok($k->is_private, 'is_private raw-priv'); + is(uc($k->key2hash->{priv}), '002F93D10BA5728D8DD8E9527721DABA3261C0BB1BEFDE7B4BBDAC631D454651', 'key2hash->{priv} raw-priv'); + is(uc($k->key2hash->{pub}), 'EA7806F721A8570512C8F6EFB4E8D620C49A529E4DF5EAA77DEC646FB1E87E41', 'key2hash->{pub} raw-priv'); + + $k = Crypt::PK::X25519->new->import_key_raw(pack("H*", "EA7806F721A8570512C8F6EFB4E8D620C49A529E4DF5EAA77DEC646FB1E87E41"), 'public'); + ok($k, 'new+import_key_raw raw-pub'); + ok(!$k->is_private, '!is_private raw-pub'); + is(uc($k->key2hash->{pub}), 'EA7806F721A8570512C8F6EFB4E8D620C49A529E4DF5EAA77DEC646FB1E87E41', 'key2hash->{pub} raw-pub'); + + $k = Crypt::PK::X25519->new({ kty=>"OKP",crv=>"X25519",d=>"AC-T0Qulco2N2OlSdyHaujJhwLsb7957S72sYx1FRlE",x=>"6ngG9yGoVwUSyPbvtOjWIMSaUp5N9eqnfexkb7HofkE"}); + ok($k, 'new JWKHASH/priv'); + ok($k->is_private, 'is_private JWKHASH/priv'); + is(uc($k->key2hash->{priv}), '002F93D10BA5728D8DD8E9527721DABA3261C0BB1BEFDE7B4BBDAC631D454651', 'key2hash->{priv} JWKHASH/priv'); + + $k = Crypt::PK::X25519->new({ kty=>"OKP",crv=>"X25519",x=>"6ngG9yGoVwUSyPbvtOjWIMSaUp5N9eqnfexkb7HofkE"}); + ok($k, 'new JWKHASH/pub'); + ok(!$k->is_private, '!is_private JWKHASH/pub'); + is(uc($k->key2hash->{pub}), 'EA7806F721A8570512C8F6EFB4E8D620C49A529E4DF5EAA77DEC646FB1E87E41', 'key2hash->{pub} JWKHASH/pub'); + + $k = Crypt::PK::X25519->new('t/data/jwk_x25519-priv1.json'); + ok($k, 'new JWK/priv'); + ok($k->is_private, 'is_private JWK/priv'); + is(uc($k->key2hash->{priv}), '002F93D10BA5728D8DD8E9527721DABA3261C0BB1BEFDE7B4BBDAC631D454651', 'key2hash->{priv} JWK/priv'); + + $k = Crypt::PK::X25519->new('t/data/jwk_x25519-pub1.json'); + ok($k, 'new JWK/pub'); + ok(!$k->is_private, '!is_private JWK/pub'); + is(uc($k->key2hash->{pub}), 'EA7806F721A8570512C8F6EFB4E8D620C49A529E4DF5EAA77DEC646FB1E87E41', 'key2hash->{pub} JWK/pub'); + + $k = Crypt::PK::X25519->new('t/data/openssl_x25519_sk.der'); + ok($k, 'new openssl_x25519_sk.der'); + ok($k->is_private, 'is_private openssl_x25519_sk.der'); + is(uc($k->key2hash->{priv}), '002F93D10BA5728D8DD8E9527721DABA3261C0BB1BEFDE7B4BBDAC631D454651', 'key2hash->{priv} openssl_x25519_sk.der'); + + $k = Crypt::PK::X25519->new('t/data/openssl_x25519_sk.pem'); + ok($k, 'new openssl_x25519_sk.pem'); + ok($k->is_private, 'is_private openssl_x25519_sk.pem'); + is(uc($k->key2hash->{priv}), '002F93D10BA5728D8DD8E9527721DABA3261C0BB1BEFDE7B4BBDAC631D454651', 'key2hash->{priv} openssl_x25519_sk.pem'); + + $k = Crypt::PK::X25519->new('t/data/openssl_x25519_sk_t.pem'); + ok($k, 'new openssl_x25519_sk_t.pem'); + ok($k->is_private, 'is_private openssl_x25519_sk_t.pem'); + is(uc($k->key2hash->{priv}), '002F93D10BA5728D8DD8E9527721DABA3261C0BB1BEFDE7B4BBDAC631D454651', 'key2hash->{priv} openssl_x25519_sk_t.pem'); + + $k = Crypt::PK::X25519->new('t/data/openssl_x25519_sk.pkcs8'); + ok($k, 'new openssl_x25519_sk.pkcs8'); + ok($k->is_private, 'is_private openssl_x25519_sk.pkcs8'); + is(uc($k->key2hash->{priv}), '002F93D10BA5728D8DD8E9527721DABA3261C0BB1BEFDE7B4BBDAC631D454651', 'key2hash->{priv} openssl_x25519_sk.pkcs8'); + + $k = Crypt::PK::X25519->new('t/data/openssl_x25519_sk_pbes1.pkcs8', 'secret'); + ok($k, 'new openssl_x25519_sk_pbes1.pkcs8'); + ok($k->is_private, 'is_private openssl_x25519_sk_pbes1.pkcs8'); + is(uc($k->key2hash->{priv}), '002F93D10BA5728D8DD8E9527721DABA3261C0BB1BEFDE7B4BBDAC631D454651', 'key2hash->{priv} openssl_x25519_sk_pbes1.pkcs8'); + + $k = Crypt::PK::X25519->new('t/data/openssl_x25519_sk_pbes2.pkcs8', 'secret'); + ok($k, 'new openssl_x25519_sk_pbes2.pkcs8'); + ok($k->is_private, 'is_private openssl_x25519_sk_pbes2.pkcs8'); + is(uc($k->key2hash->{priv}), '002F93D10BA5728D8DD8E9527721DABA3261C0BB1BEFDE7B4BBDAC631D454651', 'key2hash->{priv} openssl_x25519_sk_pbes2.pkcs8'); + + $k = Crypt::PK::X25519->new('t/data/openssl_x25519_sk_pw.pem', 'secret'); + ok($k, 'new openssl_x25519_sk_pw.pem'); + ok($k->is_private, 'is_private openssl_x25519_sk_pw.pem'); + is(uc($k->key2hash->{priv}), '002F93D10BA5728D8DD8E9527721DABA3261C0BB1BEFDE7B4BBDAC631D454651', 'key2hash->{priv} openssl_x25519_sk_pw.pem'); + + $k = Crypt::PK::X25519->new('t/data/openssl_x25519_sk_pw_t.pem', 'secret'); + ok($k, 'new openssl_x25519_sk_pw_t.pem'); + ok($k->is_private, 'is_private openssl_x25519_sk_pw_t.pem'); + is(uc($k->key2hash->{priv}), '002F93D10BA5728D8DD8E9527721DABA3261C0BB1BEFDE7B4BBDAC631D454651', 'key2hash->{priv} openssl_x25519_sk_pw_t.pem'); + + $k = Crypt::PK::X25519->new('t/data/openssl_x25519_pk.pem'); + ok($k, 'new openssl_x25519_pk.pem'); + ok(!$k->is_private, '!is_private openssl_x25519_pk.pem'); + is(uc($k->key2hash->{pub}), 'EA7806F721A8570512C8F6EFB4E8D620C49A529E4DF5EAA77DEC646FB1E87E41', 'key2hash->{pub} openssl_x25519_pk.pem'); +}