1package Crypt::PK::Ed25519; 2 3use strict; 4use warnings; 5our $VERSION = '0.075'; 6 7require Exporter; our @ISA = qw(Exporter); ### use Exporter 5.57 'import'; 8our %EXPORT_TAGS = ( all => [qw( )] ); 9our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } ); 10our @EXPORT = qw(); 11 12use Carp; 13$Carp::Internal{(__PACKAGE__)}++; 14use CryptX; 15use Crypt::PK; 16use Crypt::Misc qw(read_rawfile encode_b64u decode_b64u encode_b64 decode_b64 pem_to_der der_to_pem); 17 18sub new { 19 my $self = shift->_new(); 20 return @_ > 0 ? $self->import_key(@_) : $self; 21} 22 23sub import_key_raw { 24 my ($self, $key, $type) = @_; 25 croak "FATAL: undefined key" unless $key; 26 croak "FATAL: invalid key" unless length($key) == 32; 27 croak "FATAL: undefined type" unless $type; 28 return $self->_import_raw($key, 1) if $type eq 'private'; 29 return $self->_import_raw($key, 0) if $type eq 'public'; 30 croak "FATAL: invalid key type '$type'"; 31} 32 33sub import_key { 34 my ($self, $key, $password) = @_; 35 local $SIG{__DIE__} = \&CryptX::_croak; 36 croak "FATAL: undefined key" unless $key; 37 38 # special case 39 if (ref($key) eq 'HASH') { 40 if ($key->{kty} && $key->{kty} eq "OKP" && $key->{crv} && $key->{crv} eq 'Ed25519') { 41 # JWK-like structure e.g. 42 # {"kty":"OKP","crv":"Ed25519","d":"...","x":"..."} 43 return $self->_import_raw(decode_b64u($key->{d}), 1) if $key->{d}; # private 44 return $self->_import_raw(decode_b64u($key->{x}), 0) if $key->{x}; # public 45 } 46 if ($key->{curve} && $key->{curve} eq "ed25519" && ($key->{priv} || $key->{pub})) { 47 # hash exported via key2hash 48 return $self->_import_raw(pack("H*", $key->{priv}), 1) if $key->{priv}; 49 return $self->_import_raw(pack("H*", $key->{pub}), 0) if $key->{pub}; 50 } 51 croak "FATAL: unexpected Ed25519 key hash"; 52 } 53 54 my $data; 55 if (ref($key) eq 'SCALAR') { 56 $data = $$key; 57 } 58 elsif (-f $key) { 59 $data = read_rawfile($key); 60 } 61 else { 62 croak "FATAL: non-existing file '$key'"; 63 } 64 croak "FATAL: invalid key data" unless $data; 65 66 if ($data =~ /-----BEGIN PUBLIC KEY-----(.*?)-----END/sg) { 67 $data = pem_to_der($data, $password) or croak "FATAL: PEM/key decode failed"; 68 return $self->_import($data); 69 } 70 elsif ($data =~ /-----BEGIN PRIVATE KEY-----(.*?)-----END/sg) { 71 $data = pem_to_der($data, $password) or croak "FATAL: PEM/key decode failed"; 72 return $self->_import_pkcs8($data, $password); 73 } 74 elsif ($data =~ /-----BEGIN ENCRYPTED PRIVATE KEY-----(.*?)-----END/sg) { 75 $data = pem_to_der($data, $password) or croak "FATAL: PEM/key decode failed"; 76 return $self->_import_pkcs8($data, $password); 77 } 78 elsif ($data =~ /-----BEGIN ED25519 PRIVATE KEY-----(.*?)-----END/sg) { 79 $data = pem_to_der($data, $password) or croak "FATAL: PEM/key decode failed"; 80 return $self->_import_pkcs8($data, $password); 81 } 82 elsif ($data =~ /^\s*(\{.*?\})\s*$/s) { # JSON 83 my $h = CryptX::_decode_json("$1"); 84 if ($h->{kty} && $h->{kty} eq "OKP" && $h->{crv} && $h->{crv} eq 'Ed25519') { 85 return $self->_import_raw(decode_b64u($h->{d}), 1) if $h->{d}; # private 86 return $self->_import_raw(decode_b64u($h->{x}), 0) if $h->{x}; # public 87 } 88 } 89 elsif ($data =~ /-----BEGIN CERTIFICATE-----(.*?)-----END CERTIFICATE-----/sg) { 90 $data = pem_to_der($data) or croak "FATAL: PEM/cert decode failed"; 91 return $self->_import_x509($data); 92 } 93 elsif ($data =~ /-----BEGIN OPENSSH PRIVATE KEY-----(.*?)-----END/sg) { 94 #XXX-FIXME-TODO 95 # https://crypto.stackexchange.com/questions/71789/openssh-ed2215-private-key-format 96 # https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.key?annotate=HEAD 97 croak "FATAL: OPENSSH PRIVATE KEY not supported"; 98 } 99 elsif ($data =~ /---- BEGIN SSH2 PUBLIC KEY ----(.*?)---- END SSH2 PUBLIC KEY ----/sg) { 100 $data = pem_to_der($data) or croak "FATAL: PEM/key decode failed"; 101 my ($typ, $pubkey) = Crypt::PK::_ssh_parse($data); 102 return $self->_import_raw($pubkey, 0) if $typ eq 'ssh-ed25519' && length($pubkey) == 32; 103 } 104 elsif ($data =~ /(ssh-ed25519)\s+(\S+)/) { 105 $data = decode_b64("$2"); 106 my ($typ, $pubkey) = Crypt::PK::_ssh_parse($data); 107 return $self->_import_raw($pubkey, 0) if $typ eq 'ssh-ed25519' && length($pubkey) == 32; 108 } 109 elsif (length($data) == 32) { 110 croak "FATAL: use import_key_raw() to load raw (32 bytes) Ed25519 key"; 111 } 112 else { 113 my $rv = eval { $self->_import($data) } || 114 eval { $self->_import_pkcs8($data, $password) } || 115 eval { $self->_import_x509($data) }; 116 return $rv if $rv; 117 } 118 croak "FATAL: invalid or unsupported Ed25519 key format"; 119} 120 121sub export_key_pem { 122 my ($self, $type, $password, $cipher) = @_; 123 local $SIG{__DIE__} = \&CryptX::_croak; 124 my $key = $self->export_key_der($type||''); 125 return unless $key; 126 return der_to_pem($key, "ED25519 PRIVATE KEY", $password, $cipher) if substr($type, 0, 7) eq 'private'; 127 return der_to_pem($key, "PUBLIC KEY") if substr($type,0, 6) eq 'public'; 128} 129 130sub export_key_jwk { 131 my ($self, $type, $wanthash) = @_; 132 local $SIG{__DIE__} = \&CryptX::_croak; 133 my $kh = $self->key2hash; 134 return unless $kh; 135 my $hash = { kty => "OKP", crv => "Ed25519" }; 136 $hash->{x} = encode_b64u(pack("H*", $kh->{pub})); 137 $hash->{d} = encode_b64u(pack("H*", $kh->{priv})) if $type && $type eq 'private' && $kh->{priv}; 138 return $wanthash ? $hash : CryptX::_encode_json($hash); 139} 140 141sub CLONE_SKIP { 1 } # prevent cloning 142 1431; 144 145=pod 146 147=head1 NAME 148 149Crypt::PK::Ed25519 - Digital signature based on Ed25519 150 151=head1 SYNOPSIS 152 153 use Crypt::PK::Ed25519; 154 155 #Signature: Alice 156 my $priv = Crypt::PK::Ed25519->new('Alice_priv_ed25519.der'); 157 my $sig = $priv->sign_message($message); 158 159 #Signature: Bob (received $message + $sig) 160 my $pub = Crypt::PK::Ed25519->new('Alice_pub_ed25519.der'); 161 $pub->verify_message($sig, $message) or die "ERROR"; 162 163 #Load key 164 my $pk = Crypt::PK::Ed25519->new; 165 my $pk_hex = "A05D1AEA5830AC9A65CDFB384660D497E3697C46B419CF2CEC85DE8BD245459D"; 166 $pk->import_key_raw(pack("H*", $pk_hex), "public"); 167 my $sk = Crypt::PK::Ed25519->new; 168 my $sk_hex = "45C109BA6FD24E8B67D23EFB6B92D99CD457E2137172C0D749FE2B5A0C142DAD"; 169 $sk->import_key_raw(pack("H*", $sk_hex), "private"); 170 171 #Key generation 172 my $pk = Crypt::PK::Ed25519->new->generate_key; 173 my $private_der = $pk->export_key_der('private'); 174 my $public_der = $pk->export_key_der('public'); 175 my $private_pem = $pk->export_key_pem('private'); 176 my $public_pem = $pk->export_key_pem('public'); 177 my $private_raw = $pk->export_key_raw('private'); 178 my $public_raw = $pk->export_key_raw('public'); 179 my $private_jwk = $pk->export_key_jwk('private'); 180 my $public_jwk = $pk->export_key_jwk('public'); 181 182=head1 DESCRIPTION 183 184I<Since: CryptX-0.067> 185 186=head1 METHODS 187 188=head2 new 189 190 my $pk = Crypt::PK::Ed25519->new(); 191 #or 192 my $pk = Crypt::PK::Ed25519->new($priv_or_pub_key_filename); 193 #or 194 my $pk = Crypt::PK::Ed25519->new(\$buffer_containing_priv_or_pub_key); 195 196Support for password protected PEM keys 197 198 my $pk = Crypt::PK::Ed25519->new($priv_pem_key_filename, $password); 199 #or 200 my $pk = Crypt::PK::Ed25519->new(\$buffer_containing_priv_pem_key, $password); 201 202=head2 generate_key 203 204Uses Yarrow-based cryptographically strong random number generator seeded with 205random data taken from C</dev/random> (UNIX) or C<CryptGenRandom> (Win32). 206 207 $pk->generate_key; 208 209=head2 import_key 210 211Loads private or public key in DER or PEM format. 212 213 $pk->import_key($filename); 214 #or 215 $pk->import_key(\$buffer_containing_key); 216 217Support for password protected PEM keys: 218 219 $pk->import_key($filename, $password); 220 #or 221 $pk->import_key(\$buffer_containing_key, $password); 222 223Loading private or public keys form perl hash: 224 225 $pk->import_key($hashref); 226 227 # the $hashref is either a key exported via key2hash 228 $pk->import_key({ 229 curve => "ed25519", 230 pub => "A05D1AEA5830AC9A65CDFB384660D497E3697C46B419CF2CEC85DE8BD245459D", 231 priv => "45C109BA6FD24E8B67D23EFB6B92D99CD457E2137172C0D749FE2B5A0C142DAD", 232 }); 233 234 # or a hash with items corresponding to JWK (JSON Web Key) 235 $pk->import_key({ 236 kty => "OKP", 237 crv => "Ed25519", 238 d => "RcEJum_STotn0j77a5LZnNRX4hNxcsDXSf4rWgwULa0", 239 x => "oF0a6lgwrJplzfs4RmDUl-NpfEa0Gc8s7IXei9JFRZ0", 240 }); 241 242Supported key formats: 243 244 # all formats can be loaded from a file 245 my $pk = Crypt::PK::Ed25519->new($filename); 246 247 # or from a buffer containing the key 248 my $pk = Crypt::PK::Ed25519->new(\$buffer_with_key); 249 250=over 251 252=item * Ed25519 private keys in PEM format 253 254 -----BEGIN ED25519 PRIVATE KEY----- 255 MC4CAQAwBQYDK2VwBCIEIEXBCbpv0k6LZ9I++2uS2ZzUV+ITcXLA10n+K1oMFC2t 256 -----END ED25519 PRIVATE KEY----- 257 258=item * Ed25519 private keys in password protected PEM format 259 260 -----BEGIN ED25519 PRIVATE KEY----- 261 Proc-Type: 4,ENCRYPTED 262 DEK-Info: DES-CBC,6A64D756D49C1EFF 263 264 8xQ7OyfQ10IITNEKcJGZA53Z1yk+NJQU7hrKqXwChZtgWNInhMBJRl9pozLKDSkH 265 v7u6EOve8NY= 266 -----END ED25519 PRIVATE KEY----- 267 268=item * PKCS#8 private keys 269 270 -----BEGIN PRIVATE KEY----- 271 MC4CAQAwBQYDK2VwBCIEIEXBCbpv0k6LZ9I++2uS2ZzUV+ITcXLA10n+K1oMFC2t 272 -----END PRIVATE KEY----- 273 274=item * PKCS#8 encrypted private keys 275 276 -----BEGIN ENCRYPTED PRIVATE KEY----- 277 MIGHMEsGCSqGSIb3DQEFDTA+MCkGCSqGSIb3DQEFDDAcBAjPx9JkdpRH2QICCAAw 278 DAYIKoZIhvcNAgkFADARBgUrDgMCBwQIWWieQojaWTcEOGj43SxqHUys4Eb2M27N 279 AkhqpmhosOxKrpGi0L3h8m8ipHE8EwI94NeOMsjfVw60aJuCrssY5vKN 280 -----END ENCRYPTED PRIVATE KEY----- 281 282=item * Ed25519 public keys in PEM format 283 284 -----BEGIN PUBLIC KEY----- 285 MCowBQYDK2VwAyEAoF0a6lgwrJplzfs4RmDUl+NpfEa0Gc8s7IXei9JFRZ0= 286 -----END PUBLIC KEY----- 287 288=item * Ed25519 public key from X509 certificate 289 290 -----BEGIN CERTIFICATE----- 291 MIIBODCB66ADAgECAhRWDU9FZBBUZ7KTdX8f7Bco8jsoaTAFBgMrZXAwETEPMA0G 292 A1UEAwwGQ3J5cHRYMCAXDTIwMDExOTEzMDIwMloYDzIyOTMxMTAyMTMwMjAyWjAR 293 MQ8wDQYDVQQDDAZDcnlwdFgwKjAFBgMrZXADIQCgXRrqWDCsmmXN+zhGYNSX42l8 294 RrQZzyzshd6L0kVFnaNTMFEwHQYDVR0OBBYEFHCGFtVibAxxWYyRt5wazMpqSZDV 295 MB8GA1UdIwQYMBaAFHCGFtVibAxxWYyRt5wazMpqSZDVMA8GA1UdEwEB/wQFMAMB 296 Af8wBQYDK2VwA0EAqG/+98smzqF/wmFX3zHXSaA67as202HnBJod1Tiurw1f+lr3 297 BX6OMtsDpgRq9O77IF1Qyx/MdJEwwErczOIbAA== 298 -----END CERTIFICATE----- 299 300=item * SSH public Ed25519 keys 301 302 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL0XsiFcRDp6Hpsoak8OdiiBMJhM2UKszNTxoGS7dJ++ 303 304=item * SSH public Ed25519 keys (RFC-4716 format) 305 306 ---- BEGIN SSH2 PUBLIC KEY ---- 307 Comment: "256-bit ED25519, converted from OpenSSH" 308 AAAAC3NzaC1lZDI1NTE5AAAAIL0XsiFcRDp6Hpsoak8OdiiBMJhM2UKszNTxoGS7dJ++ 309 ---- END SSH2 PUBLIC KEY ---- 310 311=item * Ed25519 private keys in JSON Web Key (JWK) format 312 313See L<https://tools.ietf.org/html/rfc8037> 314 315 { 316 "kty":"OKP", 317 "crv":"Ed25519", 318 "x":"oF0a6lgwrJplzfs4RmDUl-NpfEa0Gc8s7IXei9JFRZ0", 319 "d":"RcEJum_STotn0j77a5LZnNRX4hNxcsDXSf4rWgwULa0", 320 } 321 322B<BEWARE:> For JWK support you need to have L<JSON::PP>, L<JSON::XS> or L<Cpanel::JSON::XS> module. 323 324=item * Ed25519 public keys in JSON Web Key (JWK) format 325 326 { 327 "kty":"OKP", 328 "crv":"Ed25519", 329 "x":"oF0a6lgwrJplzfs4RmDUl-NpfEa0Gc8s7IXei9JFRZ0", 330 } 331 332B<BEWARE:> For JWK support you need to have L<JSON::PP>, L<JSON::XS> or L<Cpanel::JSON::XS> module. 333 334=back 335 336=head2 import_key_raw 337 338Import raw public/private key - can load raw key data exported by L</export_key_raw>. 339 340 $pk->import_key_raw($key, 'public'); 341 $pk->import_key_raw($key, 'private'); 342 343=head2 export_key_der 344 345 my $private_der = $pk->export_key_der('private'); 346 #or 347 my $public_der = $pk->export_key_der('public'); 348 349=head2 export_key_pem 350 351 my $private_pem = $pk->export_key_pem('private'); 352 #or 353 my $public_pem = $pk->export_key_pem('public'); 354 355Support for password protected PEM keys 356 357 my $private_pem = $pk->export_key_pem('private', $password); 358 #or 359 my $private_pem = $pk->export_key_pem('private', $password, $cipher); 360 361 # supported ciphers: 'DES-CBC' 362 # 'DES-EDE3-CBC' 363 # 'SEED-CBC' 364 # 'CAMELLIA-128-CBC' 365 # 'CAMELLIA-192-CBC' 366 # 'CAMELLIA-256-CBC' 367 # 'AES-128-CBC' 368 # 'AES-192-CBC' 369 # 'AES-256-CBC' (DEFAULT) 370 371=head2 export_key_jwk 372 373Exports public/private keys as a JSON Web Key (JWK). 374 375 my $private_json_text = $pk->export_key_jwk('private'); 376 #or 377 my $public_json_text = $pk->export_key_jwk('public'); 378 379Also exports public/private keys as a perl HASH with JWK structure. 380 381 my $jwk_hash = $pk->export_key_jwk('private', 1); 382 #or 383 my $jwk_hash = $pk->export_key_jwk('public', 1); 384 385B<BEWARE:> For JWK support you need to have L<JSON::PP>, L<JSON::XS> or L<Cpanel::JSON::XS> module. 386 387=head2 export_key_raw 388 389Export raw public/private key 390 391 my $private_bytes = $pk->export_key_raw('private'); 392 #or 393 my $public_bytes = $pk->export_key_raw('public'); 394 395=head2 sign_message 396 397 my $signature = $priv->sign_message($message); 398 399=head2 verify_message 400 401 my $valid = $pub->verify_message($signature, $message) 402 403=head2 is_private 404 405 my $rv = $pk->is_private; 406 # 1 .. private key loaded 407 # 0 .. public key loaded 408 # undef .. no key loaded 409 410=head2 key2hash 411 412 my $hash = $pk->key2hash; 413 414 # returns hash like this (or undef if no key loaded): 415 { 416 curve => "ed25519", 417 # raw public key as a hexadecimal string 418 pub => "A05D1AEA5830AC9A65CDFB384660D497E3697C46B419CF2CEC85DE8BD245459D", 419 # raw private key as a hexadecimal string. undef if key is public only 420 priv => "45C109BA6FD24E8B67D23EFB6B92D99CD457E2137172C0D749FE2B5A0C142DAD", 421 } 422 423=head1 SEE ALSO 424 425=over 426 427=item * L<https://en.wikipedia.org/wiki/EdDSA#Ed25519> 428 429=item * L<https://en.wikipedia.org/wiki/Curve25519> 430 431=item * L<https://tools.ietf.org/html/rfc8032> 432 433=back 434 435=cut 436