Skip to content

Commit

Permalink
Verbatim import of Schnorr code from public-square/phpecc:
Browse files Browse the repository at this point in the history
phpecc/phpecc@master...public-square:phpecc:main

The changes in Point.php seem almost all related to linting. The new $windowSize
attribute with a setter and getter is not used anywhere, so I haven't imported
the changes in that class.
  • Loading branch information
1ma committed Jan 20, 2025
1 parent 6bc975d commit ebe0449
Show file tree
Hide file tree
Showing 4 changed files with 1,128 additions and 0 deletions.
179 changes: 179 additions & 0 deletions src/Crypto/Signature/SchnorrSignature.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
<?php

declare(strict_types=1);

namespace Mdanter\Ecc\Crypto\Signature;

use Mdanter\Ecc\Curves\CurveFactory;
use Mdanter\Ecc\Curves\SecgCurve;
use Mdanter\Ecc\Primitives\JacobianPoint;
use Mdanter\Ecc\Primitives\PointInterface;

class SchnorrSignature
{
public const CHALLENGE = 'BIP0340/challenge';
public const AUX = 'BIP0340/aux';
public const NONCE = 'BIP0340/nonce';

public function sign(string $privateKey, string $message, string $randomK = null): array
{
// private key must be a hex string
if (ctype_xdigit($privateKey) === false) {
throw new \InvalidArgumentException('Private key must be a hex string');
}

// hash the message
$hash = ctype_xdigit($message) === true ? $message : hash('sha256', $message);

// create a secp256k1 curve
$generator = CurveFactory::getGeneratorByName(SecgCurve::NAME_SECP_256K1);

// initialize order (Curve.N)
$n = gmp_init(JacobianPoint::CurveN, 16);

// initialize private key
$d = gmp_init($privateKey, 16);

if ($randomK === null) {
// initialize randomness
$randomK = bin2hex(random_bytes(32));
}

if (ctype_xdigit($randomK) === false) {
throw new \InvalidArgumentException('Randomness must be a hex string');
}

// calculate multiplied point
$point = $generator->mul($d);

// is the Y coordinate even?
$isEvenY = gmp_cmp(gmp_mod($point->getY(), 2), 0) === 0;

// scalar is private key if Y is even, otherwise it's order - scalar
$scalar = $d;

if ($isEvenY === false) {
$scalar = gmp_sub($n, $scalar);
}

$auxSingle = hash('sha256', self::AUX);
$tagAux = $auxSingle . $auxSingle;

// concatenate the tag and the random k and hash it
$auxHash = hash('sha256', hex2bin($tagAux . $randomK));
$auxRandNumber = gmp_init($auxHash, 16);

// calculate the nonce
$nonce = gmp_xor($scalar, $auxRandNumber);

$tagNonceSingle = hash('sha256', self::NONCE);
$tagNonce = $tagNonceSingle . $tagNonceSingle;

// concatenate the tag and the nonce and hash it
$nonceHash = hash('sha256', hex2bin($tagNonce . $this->gmp_hexval($nonce) . $this->gmp_hexval($point->getX()) . $hash));
$nonceNumber = gmp_init($nonceHash, 16);

$k0 = gmp_mod($nonceNumber, $n);
$k0Point = $generator->mul($k0);

// is the Y coordinate even?
$isEvenYKPoint = gmp_cmp(gmp_mod($k0Point->getY(), 2), 0) === 0;

// k0Scalar is k0 if Y is even, otherwise it's order - k0Scalar
$k0Scalar = $k0;

if ($isEvenYKPoint === false) {
$k0Scalar = gmp_sub($n, $k0Scalar);
}

// Schnorr Challenge
$tagChallengeSingle = hash('sha256', self::CHALLENGE);
$tagChallenge = $tagChallengeSingle . $tagChallengeSingle;

// convert the hex to binary so it is NOT hashed as a simple string
$finalChallenge = hash('sha256', hex2bin($tagChallenge . $this->gmp_hexval($k0Point->getX()) . $this->gmp_hexval($point->getX()) . $hash));
$finalChallengeNumber = gmp_init($finalChallenge, 16);

$k0PointX = $this->gmp_hexval($k0Point->getX());
$finalVal = gmp_mod(gmp_add($k0Scalar, gmp_mul($finalChallengeNumber, $scalar)), $n);

$signature = $k0PointX . $this->gmp_hexval($finalVal);

return [
'signature' => $signature,
'message' => $message,
'publicKey' => $this->gmp_hexval($point->getX()),
];
}

public function verify(string $publicKey, string $signature, string $message): bool
{
// public key must be a hex string
if (ctype_xdigit($publicKey) === false) {
throw new \InvalidArgumentException('Public key must be a hex string');
}

['r' => $r, 's' => $s, 'm' => $m, 'P' => $P] = $this->initSchnorrVerify($signature, $message, $publicKey);

$tagChallengeSingle = hash('sha256', self::CHALLENGE);
$tagChallenge = $tagChallengeSingle . $tagChallengeSingle;
$paddedR = str_pad(gmp_strval($r, 16), 64, '0', STR_PAD_LEFT);

// convert the hex to binary so it is NOT hashed as a simple string
$concatToHash = hex2bin($tagChallenge . $paddedR . gmp_strval($P->getX(), 16) . $m);
$schnorrVal = hash('sha256', $concatToHash);

$e = gmp_mod(gmp_init($schnorrVal, 16), gmp_init(JacobianPoint::CurveN, 16));

return $this->finalizeSchnorrVerify($r, $P, $s, $e);
}

private function gmp_hexval(\GMP $gmp): string
{
// gmp_strval does not properly pad hexadecimal values
$hex = gmp_strval($gmp, 16);

if (mb_strlen($hex) % 2 === 0) {
return $hex;
}

return '0' . $hex;
}

private function finalizeSchnorrVerify(\GMP $r, PointInterface $P, \GMP $s, \GMP $e): bool
{
$pointBase = (new JacobianPoint())->getBase();

$R = $pointBase->multiplyAndAddUnsafe(
$P,
$s,
$pointBase->mod(gmp_neg($e), gmp_init(JacobianPoint::CurveN, 16))
);

if (!$R || !$R->hasEvenY() || gmp_cmp($R->getX(), $r) !== 0) {
return false;
}

return true;
}

private function initSchnorrVerify(string $signature, string $message, string $publicKey): array
{
$r = gmp_init(mb_substr($signature, 0, 64), 16);
$s = gmp_init(mb_substr($signature, 64, 64), 16);
$m = ctype_xdigit($message) === true ? $message : hash('sha256', $message);

$secp256k1Curve = CurveFactory::getCurveByName(SecgCurve::NAME_SECP_256K1);

$yCoordinate = $secp256k1Curve->recoverYfromX(false, gmp_init($publicKey, 16));

$P = $secp256k1Curve->getPoint(gmp_init($publicKey, 16), $yCoordinate);

return [
'r' => $r,
's' => $s,
'm' => $m,
'P' => $P,
];
}
}
Loading

0 comments on commit ebe0449

Please sign in to comment.