SP_BN::from_hex( self::P ), 'n' => SP_BN::from_hex( self::N ), 'gx' => SP_BN::from_hex( self::GX ), 'gy' => SP_BN::from_hex( self::GY ), 'zero' => SP_BN::from_int( 0 ), 'one' => SP_BN::from_int( 1 ), 'two' => SP_BN::from_int( 2 ), 'three' => SP_BN::from_int( 3 ), 'four' => SP_BN::from_int( 4 ), 'seven' => SP_BN::from_int( 7 ), 'eight' => SP_BN::from_int( 8 ), ); // n/2, the low-S boundary. self::$c['half_n'] = SP_BN::from_hex( '7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0' ); } return self::$c; } // ------------------------------------------------------------- field ops private static function fadd( $a, $b ) { $c = self::consts(); return SP_BN::mod( SP_BN::add( $a, $b ), $c['p'] ); } private static function fsub( $a, $b ) { $c = self::consts(); return SP_BN::mod( SP_BN::sub( $a, $b ), $c['p'] ); } private static function fmul( $a, $b ) { $c = self::consts(); return SP_BN::mod( SP_BN::mul( $a, $b ), $c['p'] ); } // -------------------------------------------------- Jacobian point maths /** The point at infinity, in Jacobian coordinates (Z = 0). */ private static function jinf() { $c = self::consts(); return array( $c['one'], $c['one'], $c['zero'] ); } private static function is_inf( $pt ) { return SP_BN::is_zero( $pt[2] ); } /** Affine (x, y) into Jacobian with Z = 1. */ private static function to_jacobian( $x, $y ) { $c = self::consts(); return array( $x, $y, $c['one'] ); } /** Jacobian back to affine: (X/Z^2, Y/Z^3). The one inversion. */ private static function to_affine( $pt ) { $c = self::consts(); if ( self::is_inf( $pt ) ) { return null; } $zi = SP_BN::inv_mod( $pt[2], $c['p'] ); $zi2 = self::fmul( $zi, $zi ); $zi3 = self::fmul( $zi2, $zi ); return array( self::fmul( $pt[0], $zi2 ), self::fmul( $pt[1], $zi3 ) ); } /** Point doubling, a = 0 case (dbl-2009-l). */ private static function jdouble( $pt ) { $c = self::consts(); list( $x, $y, $z ) = $pt; if ( SP_BN::is_zero( $z ) || SP_BN::is_zero( $y ) ) { return self::jinf(); } $a = self::fmul( $x, $x ); // X^2 $b = self::fmul( $y, $y ); // Y^2 $cc = self::fmul( $b, $b ); // Y^4 $d = self::fmul( $c['two'], self::fsub( self::fmul( self::fadd( $x, $b ), self::fadd( $x, $b ) ), self::fadd( $a, $cc ) ) ); $e = self::fmul( $c['three'], $a ); $f = self::fmul( $e, $e ); $x3 = self::fsub( $f, self::fmul( $c['two'], $d ) ); $y3 = self::fsub( self::fmul( $e, self::fsub( $d, $x3 ) ), self::fmul( $c['eight'], $cc ) ); $z3 = self::fmul( self::fmul( $c['two'], $y ), $z ); return array( $x3, $y3, $z3 ); } /** Point addition, Jacobian + Jacobian (add-2007-bl). */ private static function jadd( $p1, $p2 ) { $c = self::consts(); if ( self::is_inf( $p1 ) ) { return $p2; } if ( self::is_inf( $p2 ) ) { return $p1; } list( $x1, $y1, $z1 ) = $p1; list( $x2, $y2, $z2 ) = $p2; $z1z1 = self::fmul( $z1, $z1 ); $z2z2 = self::fmul( $z2, $z2 ); $u1 = self::fmul( $x1, $z2z2 ); $u2 = self::fmul( $x2, $z1z1 ); $s1 = self::fmul( $y1, self::fmul( $z2, $z2z2 ) ); $s2 = self::fmul( $y2, self::fmul( $z1, $z1z1 ) ); if ( 0 === SP_BN::cmp( $u1, $u2 ) ) { return 0 === SP_BN::cmp( $s1, $s2 ) ? self::jdouble( $p1 ) : self::jinf(); } $h = self::fsub( $u2, $u1 ); $i = self::fmul( self::fmul( $c['two'], $h ), self::fmul( $c['two'], $h ) ); $j = self::fmul( $h, $i ); $r = self::fmul( $c['two'], self::fsub( $s2, $s1 ) ); $v = self::fmul( $u1, $i ); $x3 = self::fsub( self::fsub( self::fmul( $r, $r ), $j ), self::fmul( $c['two'], $v ) ); $y3 = self::fsub( self::fmul( $r, self::fsub( $v, $x3 ) ), self::fmul( $c['two'], self::fmul( $s1, $j ) ) ); $z3 = self::fmul( self::fsub( self::fmul( self::fadd( $z1, $z2 ), self::fadd( $z1, $z2 ) ), self::fadd( $z1z1, $z2z2 ) ), $h ); return array( $x3, $y3, $z3 ); } /** * Scalar multiply, left-to-right double-and-add. * * Not constant time. It does not need to be: every scalar multiplied here * is either a public value (recovery) or a per-signature nonce on a host * where the attacker able to time it can already read the private key off * disk. * * @return array{0:mixed,1:mixed}|null Affine point, or null for infinity. */ private static function mul_point( $k, $px, $py ) { $c = self::consts(); $k = SP_BN::mod( $k, $c['n'] ); if ( SP_BN::is_zero( $k ) ) { return null; } $base = self::to_jacobian( $px, $py ); $acc = self::jinf(); $bits = SP_BN::bits( $k ); $len = strlen( $bits ); for ( $i = 0; $i < $len; $i++ ) { $acc = self::jdouble( $acc ); if ( '1' === $bits[ $i ] ) { $acc = self::jadd( $acc, $base ); } } return self::to_affine( $acc ); } // ------------------------------------------------------------ public API /** * Compressed public key for a private key. * * @param string $priv_bin 32 raw bytes. * @return string 33 raw bytes, or '' if the key is out of range. */ public static function public_key( $priv_bin ) { $c = self::consts(); $d = SP_BN::from_bin( $priv_bin ); if ( SP_BN::is_zero( $d ) || SP_BN::cmp( $d, $c['n'] ) >= 0 ) { return ''; } $pt = self::mul_point( $d, $c['gx'], $c['gy'] ); return self::compress( $pt ); } /** @param array|null $pt Affine point. */ private static function compress( $pt ) { if ( null === $pt ) { return ''; } return chr( SP_BN::is_odd( $pt[1] ) ? 0x03 : 0x02 ) . SP_BN::to_bin( $pt[0], 32 ); } /** * Sign a 32-byte digest, producing the 65-byte recoverable blob. * * Low-S normalised, so the same message and key always yield the same * bytes and nothing downstream has to worry about malleability. * * @param string $digest 32 raw bytes (already hashed). * @param string $priv_bin 32 raw bytes. * @return string 65 raw bytes. * @throws Exception When the key is out of range or the host has no bignum extension. */ public static function sign_recoverable( $digest, $priv_bin ) { if ( ! SP_BN::available() ) { throw new Exception( 'Sirius Press needs either the GMP or the BCMath PHP extension to sign.' ); } if ( 32 !== strlen( $digest ) || 32 !== strlen( $priv_bin ) ) { throw new Exception( 'sign_recoverable expects a 32-byte digest and a 32-byte private key.' ); } $c = self::consts(); $d = SP_BN::from_bin( $priv_bin ); if ( SP_BN::is_zero( $d ) || SP_BN::cmp( $d, $c['n'] ) >= 0 ) { throw new Exception( 'private key out of range' ); } $z = SP_BN::mod( SP_BN::from_bin( $digest ), $c['n'] ); foreach ( self::rfc6979_nonces( $digest, $priv_bin ) as $k ) { $pt = self::mul_point( $k, $c['gx'], $c['gy'] ); if ( null === $pt ) { continue; } $r = SP_BN::mod( $pt[0], $c['n'] ); if ( SP_BN::is_zero( $r ) ) { continue; } $s = SP_BN::mod( SP_BN::mul( SP_BN::inv_mod( $k, $c['n'] ), SP_BN::add( $z, SP_BN::mul( $r, $d ) ) ), $c['n'] ); if ( SP_BN::is_zero( $s ) ) { continue; } $recid = ( SP_BN::is_odd( $pt[1] ) ? 1 : 0 ) | ( SP_BN::cmp( $pt[0], $c['n'] ) >= 0 ? 2 : 0 ); if ( SP_BN::cmp( $s, $c['half_n'] ) > 0 ) { $s = SP_BN::sub( $c['n'], $s ); $recid ^= 1; } return chr( 27 + 4 + $recid ) . SP_BN::to_bin( $r, 32 ) . SP_BN::to_bin( $s, 32 ); } throw new Exception( 'could not find a valid signature nonce' ); } /** * Recover the compressed public key that produced a 65-byte signature. * * @param string $sig 65 raw bytes, [flag|r|s]. * @param string $digest 32 raw bytes. * @return string 33 raw bytes, or '' when the signature is malformed or * does not correspond to any point on the curve. */ public static function recover( $sig, $digest ) { if ( ! SP_BN::available() || 65 !== strlen( $sig ) || 32 !== strlen( $digest ) ) { return ''; } $c = self::consts(); $flag = ord( $sig[0] ); $recid = ( $flag - 27 ) & 3; $r = SP_BN::from_bin( substr( $sig, 1, 32 ) ); $s = SP_BN::from_bin( substr( $sig, 33, 32 ) ); if ( SP_BN::is_zero( $r ) || SP_BN::is_zero( $s ) || SP_BN::cmp( $r, $c['n'] ) >= 0 || SP_BN::cmp( $s, $c['n'] ) >= 0 ) { return ''; } // x = r + (recid >> 1) * n, which is only on the curve for the // overflow cases the signer flagged. $x = $r; if ( $recid >= 2 ) { $x = SP_BN::add( $r, $c['n'] ); if ( SP_BN::cmp( $x, $c['p'] ) >= 0 ) { return ''; } } // y^2 = x^3 + 7; p ≡ 3 (mod 4) so the square root is a single powmod. $alpha = self::fadd( self::fmul( self::fmul( $x, $x ), $x ), $c['seven'] ); $exp = self::sqrt_exponent(); $beta = SP_BN::pow_mod( $alpha, $exp, $c['p'] ); // Reject non-residues: if beta^2 != alpha there is no such point. if ( 0 !== SP_BN::cmp( self::fmul( $beta, $beta ), $alpha ) ) { return ''; } $want_odd = ( $recid & 1 ) === 1; $y = ( SP_BN::is_odd( $beta ) === $want_odd ) ? $beta : SP_BN::sub( $c['p'], $beta ); // Q = r^-1 (sR - zG) $z = SP_BN::mod( SP_BN::from_bin( $digest ), $c['n'] ); $rinv = SP_BN::inv_mod( $r, $c['n'] ); $sr = self::mul_point( $s, $x, $y ); $zg = self::mul_point( SP_BN::mod( SP_BN::sub( $c['n'], $z ), $c['n'] ), $c['gx'], $c['gy'] ); if ( null === $sr ) { return ''; } $sum = null === $zg ? self::to_jacobian( $sr[0], $sr[1] ) : self::jadd( self::to_jacobian( $sr[0], $sr[1] ), self::to_jacobian( $zg[0], $zg[1] ) ); $sum = self::to_affine( $sum ); if ( null === $sum ) { return ''; } $q = self::mul_point( $rinv, $sum[0], $sum[1] ); return self::compress( $q ); } /** (p + 1) / 4, precomputed as hex — the square-root exponent. */ private static function sqrt_exponent() { return SP_BN::from_hex( '3fffffffffffffffffffffffffffffffffffffffffffffffffffffffbfffff0c' ); } // ---------------------------------------------------------------- nonces /** * RFC 6979 deterministic nonces, yielded one at a time. * * Determinism matters here beyond tidiness: a PHP process on a shared host * is exactly the place where a weak random source silently leaks the key * across two signatures. Nothing in this path touches the system RNG. * * @return Generator */ private static function rfc6979_nonces( $digest, $priv_bin ) { $c = self::consts(); $h1 = $digest; // bits2octets(h1): reduce the digest mod n, re-pad to 32 bytes. $h1_oct = SP_BN::to_bin( SP_BN::mod( SP_BN::from_bin( $h1 ), $c['n'] ), 32 ); $v = str_repeat( "\x01", 32 ); $k = str_repeat( "\x00", 32 ); $k = hash_hmac( 'sha256', $v . "\x00" . $priv_bin . $h1_oct, $k, true ); $v = hash_hmac( 'sha256', $v, $k, true ); $k = hash_hmac( 'sha256', $v . "\x01" . $priv_bin . $h1_oct, $k, true ); $v = hash_hmac( 'sha256', $v, $k, true ); for ( $attempt = 0; $attempt < 64; $attempt++ ) { $v = hash_hmac( 'sha256', $v, $k, true ); $cand = SP_BN::from_bin( $v ); if ( ! SP_BN::is_zero( $cand ) && SP_BN::cmp( $cand, $c['n'] ) < 0 ) { yield $cand; } $k = hash_hmac( 'sha256', $v . "\x00", $k, true ); $v = hash_hmac( 'sha256', $v, $k, true ); } } }