File Coverage

blib/lib/Crypt/RSA/Blind.pm
Criterion Covered Total %
statement 92 100 92.0
branch 2 4 50.0
condition 2 6 33.3
subroutine 25 27 92.5
pod 0 6 0.0
total 121 143 84.6


line stmt bran cond sub pod time code
1             # -*-cperl-*-
2             #
3             # Crypt::RSA::Blind - Blind RSA signatures
4             # Copyright (c) Ashish Gulhati
5             #
6             # $Id: lib/Crypt/RSA/Blind.pm v1.035 Wed Jun 11 13:34:13 EST 2025 $
7              
8 3     3   401024 use warnings;
  3         7  
  3         178  
9 3     3   28 use strict;
  3         6  
  3         70  
10              
11 3     3   37 use v5.26;
  3         12  
12 3     3   1815 use Feature::Compat::Class;
  3         1390  
  3         17  
13 3     3   691 use feature qw(signatures);
  3         5  
  3         98  
14 3     3   13 no warnings qw(experimental::signatures);
  3         20  
  3         191  
15              
16             class Crypt::RSA::Blind;
17              
18 3     3   17 use vars qw( $VERSION );
  3         5  
  3         389  
19             our ( $VERSION ) = '$Revision: 1.035 $' =~ /\s+([\d\.]+)/;
20              
21 3     3   47 use Carp;
  3         9  
  3         214  
22 3     3   1996 use Carp::Assert;
  3         4071  
  3         20  
23 3     3   2209 use Crypt::FDH;
  3         15664  
  3         172  
24 3     3   2089 use Crypt::RSA;
  3         431131  
  3         240  
25 3     3   29 use Crypt::RSA::DataFormat qw(bitsize i2osp os2ip octet_xor);
  3         7  
  3         217  
26 3     3   1774 use Crypt::RSA::Primitives;
  3         6091  
  3         150  
27 3     3   27 use Digest::SHA qw(sha384 sha384_hex);
  3         18  
  3         353  
28 3     3   26 use Math::Pari qw (Mod ceil component gcd lift);
  3         7  
  3         17  
29 3     3   792 use Crypt::Random qw(makerandom_itv makerandom);
  3         4  
  3         15046  
30              
31             field $hashsize :param :reader(get_hashsize) = 768;
32             field $initsize :param :reader(get_initsize) = 128;
33             field $blindsize :param :reader(get_blindsize) = 512;
34             field $hash_algorithm :reader(get_hash_algorithm) :param = 'SHA384';
35             field $mgf_hash_algorithm :reader(get_mgf) :param = 'SHA384';
36             field $slen :param :reader(get_slen) = 0;
37             field $oldapi :param :reader(get_oldapi) = 1;
38             field $rsa :reader(get_rsa) = Crypt::RSA->new;
39             field $rsap :reader(get_rsap) = Crypt::RSA::Primitives->new;
40             field $requests = {};
41             field $messages = {};
42              
43             method set_hashsize ($value) { $hashsize = $value; }
44             method set_hash_algorithm ($value) { $hash_algorithm = $value; }
45             method set_mgf_hash_algorithm ($value) { $mgf_hash_algorithm = $value; }
46             method set_blindsize ($value) { $blindsize = $value; }
47             method set_initsize ($value) { $initsize = $value; }
48             method set_oldapi ($value) { $oldapi = $value; }
49             method set_slen ($value) { $slen = $value; }
50              
51             method keygen (@args) {
52             $self->get_rsa->keygen(@args);
53             }
54              
55             method init () {
56             makerandom( Size => $self->get_initsize, Strength => 1, Uniform => 1 );
57             }
58              
59             # RSABSSA methods
60              
61             method blind ($arg_ref) {
62             my $n = $arg_ref->{PublicKey}->n;
63             my $kbits = bitsize($n);
64             my $klen = ceil($kbits/8);
65             my $encoded_msg = $self->EMSA_PSS_ENCODE($kbits, @$arg_ref{qw(Message sLen Salt)});
66             my $m = os2ip($encoded_msg);
67              
68             croak("Invalid input") unless is_coprime($m, $n);
69              
70             my $r; my $r_inv;
71             if ($arg_ref->{R_inv}) { # for test vector verification
72             $r_inv = $arg_ref->{R_inv};
73             $r = mod_inverse($r_inv, $n);
74             }
75             else {
76             while (!$r_inv) {
77             $r = makerandom_itv( Size => 4096, Lower => 1, Upper => $n, Strength => 1, Uniform => 1 );
78             # Check that blinding factor is invertible mod n
79             $r_inv = mod_inverse($r, $n);
80             }
81             }
82             $self->_request($arg_ref->{Init} => $r_inv, $arg_ref->{Message}) if $arg_ref->{Init};
83             my $x = RSAVP1($arg_ref->{PublicKey}, $r);
84             my $z = ($m * $x) % $n;
85             my $blinded_msg = i2osp($z, $klen);
86             my $inv = i2osp($r_inv, $klen);
87             my $msglen = length($blinded_msg);
88             croak("Unexpected message size (msglen: $msglen, klen: $klen") if $msglen != $klen;
89             return ($blinded_msg, $inv);
90             }
91              
92             method blind_sign ($arg_ref) {
93             my $n = $arg_ref->{SecretKey}->n;
94             my $kbits = bitsize($n);
95             my $klen = ceil($kbits/8);
96             my $inputlen = length($arg_ref->{BlindedMessage});
97             croak("Unexpected input size (inputlen: $inputlen, klen: $klen)") if $inputlen != $klen;
98             my $m = os2ip($arg_ref->{BlindedMessage});
99             croak("Invalid message length") if $m >= $n;
100             my $s = RSASP1($arg_ref->{SecretKey}, $m);
101             if (defined $arg_ref->{PublicKey}) {
102             my $mdash = RSAVP1($arg_ref->{PublicKey}, $s);
103             croak "Signing failure" unless $m == $mdash;
104             }
105             my $blind_sig = i2osp($s, $klen);
106             }
107              
108             method finalize ($arg_ref) {
109             my $n = $arg_ref->{PublicKey}->n;
110             my $kbits = bitsize($n);
111             my $klen = ceil($kbits/8);
112             my $z = os2ip($arg_ref->{BlindSig});
113             my ($blinding, $message);
114             unless ($arg_ref->{Blinding}) {
115             my $saved = $self->_request($arg_ref->{Init});
116             $blinding = $self->get_oldapi ? $saved->[0] : $saved;
117             $message = $self->get_oldapi ? $saved->[1] : $arg_ref->{Message};
118             }
119             croak("Neither Blinding nor valid Init vector provided") unless my $r_inv = $arg_ref->{Blinding} ? os2ip($arg_ref->{Blinding}) : $blinding;
120             my $s = ($z * $r_inv) % $n;
121             my $sig = i2osp($s, $klen);
122             $self->pss_verify( { PublicKey => $arg_ref->{PublicKey}, Signature => $sig, Message => $arg_ref->{Blinding} ? $arg_ref->{Message} : $message, sLen => $arg_ref->{sLen} } );
123             return $sig;
124             }
125              
126             method randomize ($msg) {
127             my $random = makerandom(Size => 32 * 8, Strength => 1, Uniform => 1);
128             $msg = i2osp($random, 32) . $msg;
129             }
130              
131             method pss_verify ($arg_ref) {
132             my $n = $arg_ref->{PublicKey}->n;
133             my $kbits = bitsize($n);
134             my $klen = ceil($kbits/8);
135             # Step 1
136             my $siglen = length($arg_ref->{Signature});
137             croak("Incorrect signature length (siglen: $siglen, klen: $klen") if $siglen != $klen;
138             # Step 2a (OS2IP)
139             my $signature_int = os2ip($arg_ref->{Signature});
140             # Step 2b (RSAVP1)
141             my $em_int = RSAVP1($arg_ref->{PublicKey}, $signature_int);
142             # Step 2c (I2OSP)
143             my $emlen = ceil(($kbits - 1)/8);
144             my $em = i2osp($em_int, $emlen);
145             my $hash = Digest::SHA->new($self->get_hash_algorithm);
146             $hash->add($arg_ref->{Message});
147 30     30   132 $self->EMSA_PSS_VERIFY($hash, $em, $kbits-1, sub { MGF1(@_) }, $arg_ref->{sLen});
148             return 1
149             }
150              
151             method EMSA_PSS_ENCODE ($kbits, $msg, $slen, $salt) {
152             my $hash = Digest::SHA->new($self->get_hash_algorithm);
153             $hash->add($msg);
154             my $m_hash = $hash->hexdigest;
155             my $hlen = ceil($hash->hashsize/8);
156              
157             my $embits = $kbits - 1;
158             my $emlen = ceil($embits/8);
159             assert($emlen >= $hlen + $slen + 2);
160              
161             my $lmask = 0;
162             for (0..(8 * $emlen - $embits - 1)) {
163             $lmask = $lmask >> 1 | 0x80;
164             }
165              
166             unless ($salt) {
167             $salt = '';
168             $salt = uc(unpack ('H*',i2osp(makerandom(Size => $slen * 8, Strength => 1, Uniform => 1), $slen))) if $slen;
169             1 }
170              
171             my $m_prime = chr(0) x 8 . i2osp(Math::Pari::_hex_cvt('0x' . $m_hash . $salt), $hlen + $slen);
172             $hash = Digest::SHA->new($self->get_hash_algorithm);
173             $hash->add($m_prime);
174             my $h = $hash->digest;
175             my $ps = chr(0) x ($emlen - $slen - $hlen - 2);
176             my $db = $ps . chr(0x01); $db .= i2osp(Math::Pari::_hex_cvt('0x' . $salt), $slen) if $slen;
177             my $dbMask = MGF1($h, $emlen - $hlen - 1);
178             my $masked_db = octet_xor($db, $dbMask);
179             $masked_db = chr(os2ip(substr($masked_db, 0, 1)) & (~$lmask)) . substr($masked_db, 1);
180             my $encoded_msg = $masked_db . $h . chr(0xBC);
181             }
182              
183             method EMSA_PSS_VERIFY ($mhash, $em, $embits, $mgf, $slen) {
184             my $hashlen = ceil($mhash->hashsize / 8);
185             my $emlen = ceil($embits/8);
186             my $lmask = 0;
187             for (0..(8*$emlen-$embits-1)) {
188             $lmask = $lmask >> 1 | 0x80
189             }
190             # Step 1 and 2 already done
191             # Step 3
192             croak("Incorrect signature at step 3") if ($emlen < $hashlen + $slen + 2);
193             # Step 4
194             croak("Incorrect signature at step 4") if ord(substr($em, -1)) != 0xBC;
195             # Step 5
196             my $masked_db = substr($em,0,$emlen-$hashlen-1);
197             my $h = substr($em,$emlen-$hashlen-1,-1);
198             # Step 6
199             croak("Incorrect signature at step 6") if $lmask & ord(substr($em,0,1));
200             # Step 7
201             my $dbmask = &$mgf($h, $emlen-$hashlen-1);
202             # Step 8
203             my $db = octet_xor($masked_db, $dbmask);
204             # Step 9
205             $db = chr(ord(substr($db,0,1)) & ~$lmask) . substr($db,1);
206             # Step 10
207             croak("Incorrect signature at step 10") unless (substr($db, 0, $emlen-$hashlen-$slen-1) eq (chr(0) x ($emlen-$hashlen-$slen-2) . chr(1)));
208             # Step 11
209             my $salt = $slen > 0 ? substr($db,-$slen) : '';
210             # Step 12
211             my $m_prime = chr(0) x 8 . $mhash->digest . $salt;
212             # Step 13
213             my $hash = Digest::SHA->new($self->get_hash_algorithm);
214             $hash->add($m_prime);
215             my $hp = $hash->digest;
216             # Step 14
217             croak("Incorrect signature at step 14") if $h ne $hp;
218             }
219              
220             # Old-style API methods
221              
222             method request (%arg) {
223             if ($self->get_oldapi) {
224             my ($req, $blinding) = $self->blind( { PublicKey => $arg{Key}, sLen => $self->get_slen, %arg } );
225             return os2ip($req);
226             }
227             $self->_req(%arg);
228             }
229              
230             method sign (%arg) {
231             my $klen = ceil(bitsize($arg{Key}->n)/8);
232             $self->get_oldapi ? os2ip($self->blind_sign( { SecretKey => $arg{Key}, PublicKey => $arg{PublicKey}, BlindedMessage => i2osp($arg{Message}, $klen) } )) : $self->_sign(%arg);
233             }
234              
235             method unblind (%arg) {
236             my $klen = ceil(bitsize($arg{Key}->n)/8);
237             $self->get_oldapi ? os2ip($self->finalize( { PublicKey => $arg{Key}, BlindSig => i2osp($arg{Signature}, $klen), sLen => $self->get_slen, %arg } )) : $self->_unblind(%arg);
238             }
239              
240             method verify (%arg) {
241             my $klen = ceil(bitsize($arg{Key}->n)/8);
242             $self->get_oldapi ? $self->pss_verify( { PublicKey => $arg{Key}, %arg, Signature => i2osp($arg{Signature}, $klen), sLen => $self->get_slen } ) : $self->_verify(%arg);
243             }
244              
245             # Deprecated methods
246              
247             method _req (%arg) {
248             carp('Call to deprecated method: request');
249             my ($invertible, $blinding);
250             while (!$invertible) {
251             $blinding = makerandom_itv( Size => $self->get_blindsize, Upper => $arg{Key}->n-1, Strength => 1, Uniform => 0 );
252             # Check that blinding is invertible mod n
253             $invertible = gcd( $blinding, $arg{Key}->n );
254             $invertible = 0 unless $invertible == 1;
255             }
256             $self->_request($arg{Init} => $blinding);
257              
258             my $be = $self->get_rsap->core_encrypt(Key => $arg{Key}, Plaintext => $blinding);
259             my $fdh = Math::Pari::_hex_cvt ('0x'.Crypt::FDH::hash(Size => $self->get_hashsize, Message => $arg{Message}));
260             component((Mod($fdh,$arg{Key}->n)) * (Mod($be,$arg{Key}->n)), 2);
261             }
262              
263             method _sign (@args) {
264             carp('Call to deprecated method: sign');
265             $self->get_rsap->core_sign(@args);
266             }
267              
268             method _unblind (%arg) {
269             carp('Call to deprecated method: unblind');
270             my $blinding = $self->_request($arg{Init});
271             component((Mod($arg{Signature},$arg{Key}->n)) / (Mod($blinding,$arg{Key}->n)), 2);
272             }
273              
274             method _verify (%arg) {
275             carp('Call to deprecated method: verify');
276             my $pt = $self->get_rsap->core_verify(Key => $arg{Key}, Signature => $arg{Signature});
277             $pt == Math::Pari::_hex_cvt ('0x'.Crypt::FDH::hash(Size => $self->get_hashsize, Message => $arg{Message}));
278             }
279              
280             # Helper methods and functions
281              
282             method errstr (@args) {
283             $self->rsa->errstr(@args);
284             }
285              
286             method _request ($init, $blinding=undef, $message=undef) { # Save / retrieve blinding by init vector
287             my $ret;
288             if ($blinding) { # Associate blinding with init vector
289             $requests->{$init} = $blinding;
290             $messages->{$init} = $message if $self->get_oldapi;
291             }
292             else { # Retrieve blinding associated with init vector
293             $ret = $self->get_oldapi ?
294             [ $requests->{$init}, $messages->{$init} ] :
295             $requests->{$init};
296             delete $requests->{$init};
297             delete $messages->{$init} if $self->get_oldapi;
298             }
299             return $ret;
300             }
301              
302 48     48 0 120 sub RSAVP1 ($pubkey, $s) {
  48         102  
  48         122  
  48         101  
303 48         645 my $e = $pubkey->e;
304 48         2165 my $n = $pubkey->n;
305 48         1981 my $scopy = $s; my $ncopy = $n;
  48         120  
306 48 50 33     695 croak "Signature representative out of range" unless $scopy < $ncopy and $scopy > 0;
307 48         187 my $m = mod_exp($s, $e, $n);
308             }
309              
310 9     9 0 21 sub RSASP1 ($seckey, $m) {
  9         20  
  9         33  
  9         17  
311 9         101 my $d = $seckey->d;
312 9         323 my $n = $seckey->n;
313 9         156 my $mcopy = $m; my $ncopy = $n;
  9         22  
314 9 50 33     117 croak "Message representative out of range" unless $mcopy < $ncopy and $mcopy > 0;
315 9         39 my $s = mod_exp($m, $d, $n);
316             }
317              
318 39     39 0 82 sub MGF1 ($seed, $masklen) {
  39         87  
  39         72  
  39         68  
319 39         79 my $hlen = 48;
320 39         109 my $T = '';
321 39         558 for (0..ceil($masklen/$hlen)-1) {
322 206         573 my $c = i2osp($_, 4);
323 206         9826 $T = $T . sha384($seed . $c);
324             }
325 39         347 assert(length($T) >= $masklen);
326 39         665 unpack "a$masklen", $T;
327             }
328              
329             sub is_coprime {
330 9     9 0 4001 gcd(@_) == 1;
331             }
332              
333             sub mod_inverse {
334 9     9 0 35 my($a, $n) = @_;
335 9         56 my $m = Mod(1, $n);
336 9         1724 lift($m / $a);
337             }
338              
339             sub mod_exp {
340 57     57 0 4222 my($a, $exp, $n) = @_;
341 57         379 my $m = Mod($a, $n);
342 57         748619 lift($m ** $exp);
343             }
344              
345             package Crypt::RSA::Blind::PubKey {
346 3     3   37 use Compress::Zlib;
  3         5  
  3         1396  
347 0     0     sub from_hex ($pubkey) {
  0            
  0            
348 0           Crypt::RSA::Key::Public->new->deserialize(String => [ uncompress(pack('H*',$pubkey)) ]);
349             }
350             }
351              
352             package Crypt::RSA::Blind::SecKey {
353 3     3   22 use Compress::Zlib;
  3         4  
  3         1278  
354 0     0     sub from_hex ($seckey) {
  0            
  0            
355 0           Crypt::RSA::Key::Private->new->deserialize(String => [ uncompress(pack('H*',$seckey)) ]);
356             }
357             }
358              
359             1;
360              
361             __END__