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