File Coverage

blib/lib/Net/SSH/Perl/Key/Ed25519.pm
Criterion Covered Total %
statement 185 255 72.5
branch 24 64 37.5
condition 7 28 25.0
subroutine 28 32 87.5
pod 7 12 58.3
total 251 391 64.1


line stmt bran cond sub pod time code
1             package Net::SSH::Perl::Key::Ed25519;
2 1     1   7 use strict;
  1         1  
  1         32  
3              
4 1     1   5 use Net::SSH::Perl::Buffer;
  1         5  
  1         27  
5 1     1   486 use Crypt::Digest::SHA512 qw( sha512 );
  1         674  
  1         62  
6              
7 1     1   9 use base qw( Net::SSH::Perl::Key );
  1         2  
  1         89  
8              
9 1     1   7 use Crypt::PRNG qw( random_bytes );
  1         2  
  1         41  
10 1     1   13 use Crypt::Misc qw( decode_b64 encode_b64 );
  1         2  
  1         39  
11 1     1   5 use Carp qw( croak );
  1         2  
  1         45  
12              
13 1     1   6 use constant MARK_BEGIN => "-----BEGIN OPENSSH PRIVATE KEY-----\n";
  1         2  
  1         57  
14 1     1   6 use constant MARK_END => "-----END OPENSSH PRIVATE KEY-----\n";
  1         14  
  1         82  
15 1     1   7 use constant AUTH_MAGIC => "openssh-key-v1\0";
  1         2  
  1         51  
16 1     1   6 use constant ED25519_SK_SZ => 64;
  1         12  
  1         75  
17 1     1   7 use constant ED25519_PK_SZ => 32;
  1         2  
  1         67  
18 1     1   22 use constant SALT_LEN => 16;
  1         3  
  1         67  
19 1     1   7 use constant DEFAULT_ROUNDS => 16;
  1         7  
  1         61  
20 1     1   6 use constant DEFAULT_CIPHERNAME => 'aes256-cbc';
  1         2  
  1         52  
21 1     1   5 use constant KDFNAME => 'bcrypt';
  1         2  
  1         103  
22              
23             unless (grep /^Net::SSH::Perl$/, @DynaLoader::dl_modules) {
24 1     1   7 use XSLoader;
  1         2  
  1         577  
25             XSLoader::load('Net::SSH::Perl');
26             }
27              
28 3     3 0 15 sub ssh_name { 'ssh-ed25519' }
29              
30             sub init {
31 3     3 0 6 my $key = shift;
32 3         8 my($blob) = @_;
33              
34 3 50       11 if ($blob) {
35 0         0 my $b = Net::SSH::Perl::Buffer->new( MP => 'SSH2' );
36 0         0 $b->append($blob);
37 0         0 my $ktype = $b->get_str;
38 0 0       0 croak __PACKAGE__, "->init: cannot handle type '$ktype'"
39             unless $ktype eq $key->ssh_name;
40 0         0 $key->{pub} = $b->get_str;
41             }
42             }
43              
44             sub keygen {
45 1     1 1 7 my $class = shift;
46 1         5 my $key = __PACKAGE__->new(undef);
47 1         4 my $secret = random_bytes(ED25519_PK_SZ);
48 1         459 ($key->{pub},$key->{priv}) = ed25519_generate_keypair($secret);
49 1         10 $key;
50             }
51              
52             sub read_private {
53 1     1 1 3 my $class = shift;
54 1         3 my($key_file, $passphrase) = @_;
55              
56 1         2 local *FH;
57 1 50       43 open FH, $key_file or return;
58 1         3 my $content = do { local $/; };
  1         8  
  1         50  
59 1         37 close FH;
60 1         5 $content = substr($content,length(MARK_BEGIN),
61             length($content)-length(MARK_END)-length(MARK_BEGIN));
62 1         11 my $blob = decode_b64($content);
63 1         2 my $str = AUTH_MAGIC;
64 1 50       19 croak "Invalid key format" unless $blob =~ /^${str}/;
65              
66 1         8 my $b = Net::SSH::Perl::Buffer->new( MP => 'SSH2' );
67 1         4 $b->append($blob);
68 1         4 $b->consume(length(AUTH_MAGIC));
69              
70 1         7 my $ciphername = $b->get_str;
71 1         2 my $kdfname = $b->get_str;
72 1         17 my $kdfoptions = $b->get_str;
73 1         3 my $nkeys = $b->get_int32;
74 1         4 my $pub_key = $b->get_str;
75 1         7 my $encrypted = $b->get_str;
76              
77 1 50 33     10 croak 'Wrong passphrase'
78             if !$passphrase && $ciphername ne 'none';
79              
80 1 50 33     8 croak 'Unknown cipher'
81             if $kdfname ne 'none' && $kdfname ne KDFNAME;
82              
83 1 50 33     20 croak 'Invalid format'
84             if $kdfname ne 'none' && $ciphername eq 'none';
85              
86 1 50       4 croak 'Invalid format: nkeys > 1'
87             if $nkeys != 1;
88              
89 1         2 my $decrypted;
90 1 50       2 if ($ciphername eq 'none') {
91 0         0 $decrypted = $encrypted;
92             } else {
93 1 50       3 if ($kdfname eq KDFNAME) {
94 1     1   506 use Net::SSH::Perl::Cipher;
  1         2  
  1         465  
95 1         3 my $cipher = eval { Net::SSH::Perl::Cipher->new($ciphername) };
  1         9  
96 1 50       7 croak "Cannot load cipher $ciphername" unless $cipher;
97 1 50 33     5 croak 'Invalid format'
98             if length($encrypted) < $cipher->blocksize ||
99             length($encrypted) % $cipher->blocksize;
100              
101 1         3 my $keylen = $cipher->keysize;
102 1         4 my $ivlen = $cipher->ivlen;
103 1         7 my $authlen = $cipher->authlen;
104 1         3 my $tag = $b->bytes($b->offset,$authlen);
105 1 50       3 croak 'Invalid format'
106             if length($tag) != $authlen;
107              
108 1         6 $b->empty;
109 1         3 $b->append($kdfoptions);
110 1         2 my $salt = $b->get_str;
111 1 50       4 croak "Invalid format"
112             if length($salt) != SALT_LEN;
113 1         3 my $rounds = $b->get_int32;
114              
115 1         9 my $km = bcrypt_pbkdf($passphrase, $salt, $keylen+$ivlen, $rounds);
116 1         4 my $key = substr($km,0,$keylen);
117 1         4 my $iv = substr($km,$keylen,$ivlen);
118 1         37 $cipher->init($key,$iv);
119 1         13 $decrypted = $cipher->decrypt($encrypted . $tag);
120             }
121             }
122              
123 1         10 $b->empty;
124 1         5 $b->append($decrypted);
125 1         6 my $check1 = $b->get_int32;
126 1         4 my $check2 = $b->get_int32;
127 1 50 33     10 croak 'Wrong passphrase (check mismatch)'
128             if $check1 != $check2 || ! defined $check1;
129              
130 1         5 my $type = $b->get_str;
131 1 50       12 croak 'Wrong key type'
132             unless $type eq $class->ssh_name;
133 1         3 $pub_key = $b->get_str;
134 1         5 my $priv_key = $b->get_str;
135 1 50 33     16 croak 'Invalid format'
136             if length($pub_key) != ED25519_PK_SZ ||
137             length($priv_key) != ED25519_SK_SZ;
138 1         4 my $comment = $b->get_str;
139              
140             # check padding
141 1         10 my $padnum = 0;
142 1         7 while ($b->offset < $b->length) {
143 15 50       30 croak "Invalid format"
144             if ord($b->get_char) != ++$padnum;
145             }
146              
147 1         12 my $key = __PACKAGE__->new(undef);
148 1         11 $key->comment($comment);
149 1         3 $key->{pub} = $pub_key;
150 1         3 $key->{priv} = $priv_key;
151 1         23 $key;
152             }
153              
154             sub write_private {
155 0     0 1 0 my $key = shift;
156 0         0 my($key_file, $passphrase, $ciphername, $rounds) = @_;
157 0         0 my ($kdfoptions, $kdfname, $blocksize, $cipher, $authlen, $tag);
158              
159 0 0       0 if ($passphrase) {
160 0   0     0 $ciphername ||= DEFAULT_CIPHERNAME;
161 1     1   8 use Net::SSH::Perl::Cipher;
  1         3  
  1         1088  
162 0         0 $cipher = eval { Net::SSH::Perl::Cipher->new($ciphername) };
  0         0  
163 0 0       0 croak "Cannot load cipher $ciphername"
164             unless $cipher;
165              
166             # cipher init params
167 0         0 $kdfname = KDFNAME;
168 0         0 $blocksize = $cipher->blocksize;
169 0         0 my $keylen = $cipher->keysize;
170 0         0 my $ivlen = $cipher->ivlen;
171 0   0     0 $rounds ||= DEFAULT_ROUNDS;
172 0         0 my $salt = random_bytes(SALT_LEN);
173              
174 0         0 my $kdf = Net::SSH::Perl::Buffer->new( MP => 'SSH2' );
175 0         0 $kdf->put_str($salt);
176 0         0 $kdf->put_int32($rounds);
177 0         0 $kdfoptions = $kdf->bytes;
178              
179             # get key material
180 0         0 my $km = bcrypt_pbkdf($passphrase, $salt, $keylen+$ivlen, $rounds);
181 0         0 my $key = substr($km,0,$keylen);
182 0         0 my $iv = substr($km,$keylen,$ivlen);
183 0         0 $cipher->init($key,$iv);
184 0         0 $authlen = $cipher->authlen;
185             } else {
186 0         0 $ciphername = 'none';
187 0         0 $kdfname = 'none';
188 0         0 $blocksize = 8;
189             }
190 0         0 my $b = Net::SSH::Perl::Buffer->new( MP => 'SSH2' );
191 0         0 $b->put_char(AUTH_MAGIC);
192 0         0 $b->put_str($ciphername);
193 0         0 $b->put_str($kdfname);
194 0         0 $b->put_str($kdfoptions);
195 0         0 $b->put_int32(1); # one key
196              
197             # public key
198 0         0 my $pub = Net::SSH::Perl::Buffer->new( MP => 'SSH2' );
199 0         0 $pub->put_str($key->ssh_name);
200 0         0 $pub->put_str($key->{pub});
201 0         0 $b->put_str($pub->bytes);
202              
203             # create private key blob
204 0         0 my $kb = Net::SSH::Perl::Buffer->new( MP => 'SSH2' );
205 0         0 my $checkint = int(rand(0xffffffff));
206 0         0 $kb->put_int32($checkint);
207 0         0 $kb->put_int32($checkint);
208 0         0 $kb->put_str($key->ssh_name);
209 0         0 $kb->put_str($key->{pub});
210 0         0 $kb->put_str($key->{priv});
211 0         0 $kb->put_str($key->comment);
212 0 0       0 if (my $r = length($kb->bytes) % $blocksize) {
213 0         0 $kb->put_char(chr($_)) foreach (1..$blocksize-$r);
214             }
215 0 0       0 my $bytes = $cipher ? $cipher->encrypt($kb->bytes) : $kb->bytes;
216 0 0       0 $tag = substr($bytes,-$authlen,$authlen,'') if $authlen;
217 0         0 $b->put_str($bytes);
218 0 0       0 $b->put_chars($tag) if $tag;
219              
220 0         0 local *FH;
221 0 0       0 open FH, ">$key_file" or die "Cannot write key file";
222 0         0 print FH MARK_BEGIN;
223 0         0 print FH encode_b64($b->bytes),"\n";
224 0         0 print FH MARK_END;
225 0         0 close FH;
226             }
227              
228             sub sign {
229 1     1 1 406 my $key = shift;
230 1         2 my $data = shift;
231 1         329 my $sig = ed25519_sign_message($data, $key->{priv});
232              
233 1         10 my $b = Net::SSH::Perl::Buffer->new( MP => 'SSH2' );
234 1         3 $b->put_str($key->ssh_name);
235 1         3 $b->put_str($sig);
236 1         3 $b->bytes;
237             }
238              
239             sub verify {
240 1     1 1 387 my $key = shift;
241 1         3 my($signature, $data) = @_;
242 1         2 my $sigblob;
243              
244 1         4 my $b = Net::SSH::Perl::Buffer->new( MP => 'SSH2' );
245 1         5 $b->append($signature);
246 1         3 my $ktype = $b->get_str;
247 1 50       2 croak "Can't verify type ", $ktype unless $ktype eq $key->ssh_name;
248 1         5 $sigblob = $b->get_str;
249 1 50       3 croak "Invalid format" unless length($sigblob) == 64;
250              
251 1         693 ed25519_verify_message($data,$key->{pub},$sigblob);
252             }
253              
254             sub equal {
255 0     0 1 0 my($keyA, $keyB) = @_;
256             $keyA->{pub} && $keyB->{pub} &&
257 0 0 0     0 $keyA->{pub} eq $keyB->{pub};
258             }
259              
260             sub as_blob {
261 0     0 1 0 my $key = shift;
262 0         0 my $b = Net::SSH::Perl::Buffer->new( MP => 'SSH2' );
263 0         0 $b->put_str($key->ssh_name);
264 0         0 $b->put_str($key->{pub});
265 0         0 $b->bytes;
266             }
267              
268 0     0 0 0 sub fingerprint_raw { $_[0]->as_blob }
269              
270             sub bcrypt_hash {
271 32     32 0 74 my ($sha2pass, $sha2salt) = @_;
272 32         49 my $ciphertext = 'OxychromaticBlowfishSwatDynamite';
273              
274 32         537 my $ctx = bf_init();
275 32         4407 bf_expandstate($ctx,$sha2salt,$sha2pass);
276 32         111 for (my $i=0; $i<64; $i++) {
277 2048         227781 bf_expand0state($ctx,$sha2salt);
278 2048         227599 bf_expand0state($ctx,$sha2pass);
279             }
280             # iterate 64 times
281 32         2037 bf_encrypt_iterate($ctx,$ciphertext,64);
282             }
283              
284             sub bcrypt_pbkdf {
285 1     1 0 3 my ($pass, $salt, $keylen, $rounds) = @_;
286 1         2 my $out;
287 1     1   8 use constant BCRYPT_HASHSIZE => 32;
  1         2  
  1         350  
288 1         3 my $key = "\0" x $keylen;
289 1         2 my $origkeylen = $keylen;
290              
291 1 50       2 return if $rounds < 1;
292 1 50 33     5 return unless $pass && $salt;
293              
294 1         8 my $stride = int(($keylen + BCRYPT_HASHSIZE - 1) / BCRYPT_HASHSIZE);
295 1         3 my $amt = int(($keylen + $stride - 1) / $stride);
296              
297 1         3 my $sha2pass = sha512($pass);
298              
299 1         15 for (my $count = 1; $keylen > 1; $count++) {
300 2         11 my $countsalt = pack('N',$count & 0xffffffff);
301             # first round, salt is salt
302 2         12 my $sha2salt = sha512($salt . $countsalt);
303              
304 2         17 my $tmpout = $out = bcrypt_hash($sha2pass, $sha2salt);
305              
306 2         8 for (my $i=1; $i < $rounds; $i++) {
307             # subsequent rounds, salt is previous output
308 30         107 $sha2salt = sha512($tmpout);
309 30         259 $tmpout = bcrypt_hash($sha2pass,$sha2salt);
310 30         107 $out ^= $tmpout;
311             }
312              
313             # pbkdf2 deviation: output the key material non-linearly.
314 2 100       13 $amt = $amt<$keylen ? $amt : $keylen;
315 2         5 my $i;
316 2         8 for ($i=0; $i<$amt; $i++) {
317 48         79 my $dest = $i * $stride + ($count - 1);
318 48 50       84 last if $dest >= $origkeylen;
319 48         95 substr($key,$dest,1,substr($out,$i,1));
320             }
321 2         13 $keylen -= $i;
322             }
323 1         7 return $key;
324             }
325              
326             1;
327             __END__