File Coverage

blib/lib/Web3/Tiny/Secp256k1.pm
Criterion Covered Total %
statement 71 71 100.0
branch 12 18 66.6
condition n/a
subroutine 14 14 100.0
pod 0 6 0.0
total 97 109 88.9


line stmt bran cond sub pod time code
1             package Web3::Tiny::Secp256k1;
2              
3 3     3   79900 use strict;
  3         5  
  3         80  
4 3     3   37 use warnings;
  3         5  
  3         109  
5 3     3   12 use Exporter 'import';
  3         3  
  3         87  
6 3     3   1573 use Crypt::PK::ECC;
  3         48443  
  3         179  
7 3     3   4141 use Math::BigInt try => 'GMP,Pari';
  3         106348  
  3         15  
8              
9 3     3   76978 use Web3::Tiny::Keccak256 qw(keccak256);
  3         10  
  3         2269  
10              
11             our $VERSION = '0.01';
12             our @EXPORT_OK = qw(
13             privkey_to_pubkey pubkey_to_address privkey_to_address
14             sign_hash recover_pubkey verify_hash
15             );
16              
17             # secp256k1 group order (as used by Bitcoin and Ethereum), needed only for
18             # the low-S normalization required by Ethereum since EIP-2 / Homestead.
19             my $N = Math::BigInt->new('0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141');
20             my $HALF_N = $N->copy->bsub(1)->bdiv(2);
21              
22             sub _bytes_to_bi {
23 10     10   33 my ($bytes) = @_;
24 10         141 return Math::BigInt->from_hex('0x' . unpack('H*', $bytes));
25             }
26              
27             sub _bi_to_bytes {
28 5     5   809 my ($bi, $len) = @_;
29 5         22 my $hex = $bi->as_hex;
30 5         3468 $hex =~ s/^0x//;
31 5 100       18 $hex = '0' . $hex if length($hex) % 2;
32 5         26 my $bytes = pack('H*', $hex);
33 5 50       15 $bytes = ("\x00" x ($len - length($bytes))) . $bytes if length($bytes) < $len;
34 5         10 return $bytes;
35             }
36              
37             # privkey_to_pubkey($privkey_bytes) -> ($x_bytes32, $y_bytes32)
38             sub privkey_to_pubkey {
39 5     5 0 10 my ($priv_bytes) = @_;
40 5         31 my $pk = Crypt::PK::ECC->new;
41 5         450 eval { $pk->import_key_raw($priv_bytes, 'secp256k1') };
  5         25047  
42 5 50       35 die "Web3::Tiny::Secp256k1: private key out of range\n" if $@;
43              
44 5         90 my $pub = $pk->export_key_raw('public'); # 0x04 || X(32) || Y(32)
45 5         158 return (substr($pub, 1, 32), substr($pub, 33, 32));
46             }
47              
48             # pubkey_to_address($x_bytes32, $y_bytes32) -> 20 raw bytes
49             sub pubkey_to_address {
50 5     5 0 15 my ($x, $y) = @_;
51 5         34 my $hash = keccak256($x . $y);
52 5         96 return substr($hash, 12, 20);
53             }
54              
55             sub privkey_to_address {
56 3     3 0 142699 my ($priv_bytes) = @_;
57 3         9 my ($x, $y) = privkey_to_pubkey($priv_bytes);
58 3         10 return pubkey_to_address($x, $y);
59             }
60              
61             # sign_hash($hash32_bytes, $priv_bytes) -> ($r_bytes32, $s_bytes32, $recid)
62             #
63             # RFC 6979 deterministic (HMAC-SHA256 nonce), normalized to low-s form
64             # (s <= N/2) as required by Ethereum since EIP-2 / Homestead. CryptX signs
65             # but does not itself enforce low-s, so that normalization happens here.
66             sub sign_hash {
67 10     10 0 1414 my ($hash, $priv_bytes) = @_;
68 10 50       35 die "Web3::Tiny::Secp256k1: hash must be 32 bytes\n" unless length($hash) == 32;
69              
70 10         63 my $pk = Crypt::PK::ECC->new;
71 10         748 eval { $pk->import_key_raw($priv_bytes, 'secp256k1') };
  10         63591  
72 10 50       60 die "Web3::Tiny::Secp256k1: private key out of range\n" if $@;
73              
74 10         64370 my $sig = $pk->sign_hash_eth($hash, 'SHA256');
75 10         91 my $r = substr($sig, 0, 32);
76 10         30 my $s = substr($sig, 32, 32);
77 10         60 my $recid = unpack('C', substr($sig, 64, 1)) - 27;
78              
79 10         96 my $s_bi = _bytes_to_bi($s);
80 10 100       11815 if ($s_bi->bcmp($HALF_N) > 0) {
81 5         198 $s = _bi_to_bytes($N->copy->bsub($s_bi), 32);
82 5         23 $recid ^= 1;
83             }
84              
85 10         714 return ($r, $s, $recid);
86             }
87              
88             # recover_pubkey($hash32, $r_bytes32, $s_bytes32, $recid) -> ($x_bytes32, $y_bytes32)
89             sub recover_pubkey {
90 8     8 0 68 my ($hash, $r_bytes, $s_bytes, $recid) = @_;
91              
92 8         34 my $sig = $r_bytes . $s_bytes . pack('C', 27 + $recid);
93              
94             # recovery_pub_eth needs a curve context to recover into; the private
95             # key generated here is discarded immediately, only its curve sticks.
96 8         43 my $pk = Crypt::PK::ECC->new;
97 8         26478 $pk->generate_key('secp256k1');
98 8         27 eval { $pk->recovery_pub_eth($sig, $hash) };
  8         59354  
99 8 50       51 die "Web3::Tiny::Secp256k1: invalid recovery id\n" if $@;
100              
101 8         107 my $pub = $pk->export_key_raw('public');
102 8         296 return (substr($pub, 1, 32), substr($pub, 33, 32));
103             }
104              
105             sub verify_hash {
106 2     2 0 43 my ($hash, $r_bytes, $s_bytes, $pub_x, $pub_y) = @_;
107              
108 2         10 my $pk = Crypt::PK::ECC->new;
109 2         104 eval { $pk->import_key_raw("\x04" . $pub_x . $pub_y, 'secp256k1') };
  2         5671  
110 2 50       20 return 0 if $@;
111              
112             # the recovery-id byte is irrelevant to verification, only to recovery
113 2         5 my $sig = $r_bytes . $s_bytes . "\x1b";
114 2 100       3661 return $pk->verify_hash_eth($sig, $hash) ? 1 : 0;
115             }
116              
117             1;
118              
119             __END__