Skip to content

Commit 2f855a5

Browse files
mllwchrryArvolear
andauthored
Schnorr adaptor signatures (#171)
* refactor comments * add extract function * remove rt calculation logic from _verify * modify internal _verify function * fix changelog * add adaptorVerify and modify extract function * add scalar validation in extract * readme --------- Co-authored-by: Artem Chystiakov <artem.ch31@gmail.com>
1 parent 5997388 commit 2f855a5

File tree

5 files changed

+214
-7
lines changed

5 files changed

+214
-7
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## [patch]
4+
5+
- Added `extract` function to the `Schnorr256` library to extract a secret from a standard/adaptor Schnorr signature pair.
6+
37
## [3.2.6]
48

59
- Enhanced the `ABridge` contract architecture.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ contracts
6464
│ │ ├── ECDSA256 — "ECDSA verification over any 256-bit curve"
6565
│ │ ├── ECDSA384 — "ECDSA verification over any 384-bit curve"
6666
│ │ ├── ECDSA512 — "ECDSA verification over any 512-bit curve"
67-
│ │ ├── Schnorr256 — "Schnorr signature verification over any 256-bit curve"
67+
│ │ ├── Schnorr256 — "Schnorr + adaptor signature verification over any 256-bit curve"
6868
│ │ └── RSASSAPSS — "RSASSA-PSS signature verification with MGF1"
6969
│ ├── data—structures
7070
│ │ ├── AvlTree — "AVL tree implementation with an iterator traversal"

contracts/libs/crypto/Schnorr256.sol

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,24 @@ import {MemoryUtils} from "../utils/MemoryUtils.sol";
77
/**
88
* @notice Cryptography module
99
*
10-
* This library provides functionality for Schnorr signature verification over any 256-bit curve.
10+
* This library provides functionality for Schnorr signature verification over any 256-bit curve,
11+
* together with secret extraction from a standard/adaptor Schnorr signature pair.
1112
*/
1213
library Schnorr256 {
1314
using MemoryUtils for *;
1415
using EC256 for *;
1516

1617
error LengthIsNot64();
1718
error LengthIsNot96();
19+
error InvalidSignatureScalar();
1820

1921
/**
20-
* @notice The function to verify the Schnorr signature
22+
* @notice The function to verify the Schnorr signature.
2123
* @param ec the 256-bit curve parameters.
2224
* @param hashedMessage_ the already hashed message to be verified.
2325
* @param signature_ the Schnorr signature. Equals to `bytes(R) + bytes(e)`.
2426
* @param pubKey_ the full public key of a signer. Equals to `bytes(x) + bytes(y)`.
27+
* @return True if the signature is valid, false otherwise.
2528
*/
2629
function verify(
2730
EC256.Curve memory ec,
@@ -39,7 +42,7 @@ library Schnorr256 {
3942
EC256.JPoint memory lhs_ = ec.jMultShamir(ec.jbasepoint(), e_);
4043

4144
uint256 c_ = ec.toScalar(
42-
uint256(keccak256(abi.encodePacked(ec.gx, ec.gy, r_.x, r_.y, hashedMessage_)))
45+
uint256(keccak256(abi.encodePacked(p_.x, p_.y, r_.x, r_.y, hashedMessage_)))
4346
);
4447

4548
EC256.JPoint memory rhs_ = ec.jMultShamir(p_.toJacobian(), c_);
@@ -48,6 +51,81 @@ library Schnorr256 {
4851
return ec.jEqual(lhs_, rhs_);
4952
}
5053

54+
/**
55+
* @notice The function to verify the adaptor Schnorr signature.
56+
* @dev The adaptor Schnorr signature is expected to be computed as:
57+
*
58+
* c = H(P || (R + T) || m)
59+
* e' = (r + c * privKey) mod n
60+
* signature = (R, e')
61+
*
62+
* @param ec the 256-bit curve parameters.
63+
* @param hashedMessage_ the already hashed message to be verified.
64+
* @param signature_ The adaptor Schnorr signature. Equals to `bytes(R) + bytes(e′)`.
65+
* @param pubKey_ the full public key of a signer. Equals to `bytes(x) + bytes(y)`.
66+
* @param t_ the adaptor secret point added to the nonce in the challenge computation.
67+
* @return True if the adaptor signature is valid, false otherwise.
68+
*/
69+
function adaptorVerify(
70+
EC256.Curve memory ec,
71+
bytes32 hashedMessage_,
72+
bytes memory signature_,
73+
bytes memory pubKey_,
74+
EC256.APoint memory t_
75+
) internal view returns (bool) {
76+
(EC256.APoint memory r_, uint256 e_) = _parseSignature(signature_);
77+
EC256.APoint memory p_ = _parsePubKey(pubKey_);
78+
79+
if (!ec.isOnCurve(r_) || !ec.isOnCurve(p_) || !ec.isOnCurve(t_) || !ec.isValidScalar(e_)) {
80+
return false;
81+
}
82+
83+
EC256.JPoint memory lhs_ = ec.jMultShamir(ec.jbasepoint(), e_);
84+
EC256.APoint memory rt_ = ec.toAffine(ec.jAddPoint(r_.toJacobian(), t_.toJacobian()));
85+
86+
uint256 c_ = ec.toScalar(
87+
uint256(keccak256(abi.encodePacked(p_.x, p_.y, rt_.x, rt_.y, hashedMessage_)))
88+
);
89+
90+
EC256.JPoint memory rhs_ = ec.jMultShamir(p_.toJacobian(), c_);
91+
rhs_ = ec.jAddPoint(rhs_, r_.toJacobian());
92+
93+
return ec.jEqual(lhs_, rhs_);
94+
}
95+
96+
/**
97+
* @notice The function to extract the adaptor secret from a pair of Schnorr signatures.
98+
* @dev This function does not verify the validity of either signature.
99+
* Callers are responsible for verifying both the standard and adaptor signatures
100+
* separately via `verify` and `adaptorVerify` before extraction.
101+
*
102+
* The standard Schnorr signature is expected to be computed from the adaptor one as:
103+
* e = e' + t = (r + t + c * privKey) mod n
104+
* signature = (R + T, e)
105+
*
106+
* Secret extraction is performed as follows:
107+
* t = (e - e') mod n
108+
*
109+
* @param ec the 256-bit curve parameters.
110+
* @param signature_ the Schnorr signature. Equals to `bytes(R + T) + bytes(e)`.
111+
* @param adaptorSignature_ the adaptor Schnorr signature. Equals to `bytes(R) + bytes(e')`.
112+
* @return The secret scalar used in the signature.
113+
*/
114+
function extract(
115+
EC256.Curve memory ec,
116+
bytes memory signature_,
117+
bytes memory adaptorSignature_
118+
) internal pure returns (uint256) {
119+
(, uint256 sigScalar_) = _parseSignature(signature_);
120+
(, uint256 adaptorScalar_) = _parseSignature(adaptorSignature_);
121+
122+
if (!ec.isValidScalar(sigScalar_) || !ec.isValidScalar(adaptorScalar_)) {
123+
revert InvalidSignatureScalar();
124+
}
125+
126+
return addmod(sigScalar_, ec.n - adaptorScalar_, ec.n);
127+
}
128+
51129
/**
52130
* @dev Helper function for splitting 96-byte signature into R (affine point) and e (scalar) components.
53131
*/

contracts/mock/libs/crypto/Schnorr256Mock.sol

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,27 @@ contract Schnorr256Mock {
2222
) external view returns (bool isVerified_) {
2323
return Schnorr256.verify(_secp256k1CurveParams, hashedMessage_, signature_, pubKey_);
2424
}
25+
26+
function adaptorVerifySECP256k1(
27+
bytes32 hashedMessage_,
28+
bytes memory signature_,
29+
bytes memory pubKey_,
30+
EC256.APoint memory rt_
31+
) external view returns (bool isVerified_) {
32+
return
33+
Schnorr256.adaptorVerify(
34+
_secp256k1CurveParams,
35+
hashedMessage_,
36+
signature_,
37+
pubKey_,
38+
rt_
39+
);
40+
}
41+
42+
function extractSECP256k1(
43+
bytes memory signature_,
44+
bytes memory adaptorSignature_
45+
) external view returns (uint256 isVerified_) {
46+
return Schnorr256.extract(_secp256k1CurveParams, signature_, adaptorSignature_);
47+
}
2548
}

test/libs/crypto/Schnorr256.test.ts

Lines changed: 105 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,28 +25,54 @@ describe("Schnorr256", () => {
2525
return ethers.solidityPacked(["uint256", "uint256"], [p.x, p.y]);
2626
};
2727

28-
const schnorrSign = function (hashedMessage: string, privKey: bigint) {
28+
const schnorrSign = function (hashedMessage: string, privKey: bigint, pubKey: AffinePoint<bigint>) {
2929
const randomness = schnorrKeyPair();
3030
const k = randomness.privKey;
3131
const R = randomness.pubKey;
3232

3333
const c = BigInt(
3434
ethers.solidityPackedKeccak256(
3535
["uint256", "uint256", "uint256", "uint256", "bytes32"],
36-
[secp256k1.CURVE.Gx, secp256k1.CURVE.Gy, R.x, R.y, hashedMessage],
36+
[pubKey.x, pubKey.y, R.x, R.y, hashedMessage],
3737
),
3838
);
3939
const e = (k + c * privKey) % secp256k1.CURVE.n;
4040

4141
return ethers.solidityPacked(["uint256", "uint256", "uint256"], [R.x, R.y, e]);
4242
};
4343

44+
const schnorrAdaptorSign = function (
45+
hashedMessage: string,
46+
privKey: bigint,
47+
pubKey: AffinePoint<bigint>,
48+
T: AffinePoint<bigint>,
49+
) {
50+
const randomness = schnorrKeyPair();
51+
const k = randomness.privKey;
52+
const R = randomness.pubKey;
53+
54+
const RT = secp256k1.ProjectivePoint.fromAffine(R).add(secp256k1.ProjectivePoint.fromAffine(T)).toAffine();
55+
56+
const c = BigInt(
57+
ethers.solidityPackedKeccak256(
58+
["uint256", "uint256", "uint256", "uint256", "bytes32"],
59+
[pubKey.x, pubKey.y, RT.x, RT.y, hashedMessage],
60+
),
61+
);
62+
63+
const e = (k + c * privKey) % secp256k1.CURVE.n;
64+
65+
const signature = ethers.solidityPacked(["uint256", "uint256", "uint256"], [R.x, R.y, e]);
66+
67+
return { signature, RT, e };
68+
};
69+
4470
const prepareParameters = function (message: string) {
4571
const { privKey, pubKey } = schnorrKeyPair();
4672

4773
const hashedMessage = ethers.keccak256(message);
4874

49-
const signature = schnorrSign(hashedMessage, privKey);
75+
const signature = schnorrSign(hashedMessage, privKey, pubKey);
5076

5177
return {
5278
hashedMessage,
@@ -108,4 +134,80 @@ describe("Schnorr256", () => {
108134
expect(await schnorr.verifySECP256k1(hashedMessage, signature, wrongPubKey)).to.be.false;
109135
});
110136
});
137+
138+
describe("adaptorVerify", () => {
139+
it("should verify the adaptor signature", async () => {
140+
const { privKey, pubKey } = schnorrKeyPair();
141+
142+
const hashedMessage = ethers.keccak256("0x1337");
143+
144+
const t = bytesToNumberBE(secp256k1.utils.randomPrivateKey());
145+
const T = secp256k1.ProjectivePoint.BASE.multiply(t).toAffine();
146+
147+
const { signature } = schnorrAdaptorSign(hashedMessage, privKey, pubKey, T);
148+
149+
expect(await schnorr.adaptorVerifySECP256k1(hashedMessage, signature, serializePoint(pubKey), T)).to.be.true;
150+
});
151+
152+
it("should not verify if adaptor signature or public key or T is invalid", async () => {
153+
const { privKey, pubKey } = schnorrKeyPair();
154+
155+
const hashedMessage = ethers.keccak256("0x1337");
156+
157+
const t = bytesToNumberBE(secp256k1.utils.randomPrivateKey());
158+
const t2 = bytesToNumberBE(secp256k1.utils.randomPrivateKey());
159+
160+
const T = secp256k1.ProjectivePoint.BASE.multiply(t).toAffine();
161+
const T2 = secp256k1.ProjectivePoint.BASE.multiply(t2).toAffine();
162+
163+
const { signature } = schnorrAdaptorSign(hashedMessage, privKey, pubKey, T);
164+
165+
expect(await schnorr.adaptorVerifySECP256k1(ethers.keccak256("0x1227"), signature, serializePoint(pubKey), T)).to
166+
.be.false;
167+
168+
expect(await schnorr.adaptorVerifySECP256k1(hashedMessage, signature, serializePoint(pubKey), T2)).to.be.false;
169+
170+
const wrongPubKey = "0x" + ethers.toBeHex(0, 0x40).slice(2);
171+
expect(await schnorr.adaptorVerifySECP256k1(hashedMessage, signature, wrongPubKey, T)).to.be.false;
172+
});
173+
});
174+
175+
describe("extract", () => {
176+
it("should extract secret from two signatures correctly", async () => {
177+
const { privKey, pubKey } = schnorrKeyPair();
178+
179+
const hashedMessage = ethers.keccak256("0x1337");
180+
181+
const t = bytesToNumberBE(secp256k1.utils.randomPrivateKey());
182+
const T = secp256k1.ProjectivePoint.BASE.multiply(t).toAffine();
183+
184+
const { signature: adaptorSignature, RT, e } = schnorrAdaptorSign(hashedMessage, privKey, pubKey, T);
185+
186+
const signatureScalar = (e + t) % secp256k1.CURVE.n;
187+
188+
const signature = ethers.solidityPacked(["uint256", "uint256", "uint256"], [RT.x, RT.y, signatureScalar]);
189+
190+
expect(await schnorr.extractSECP256k1(signature, adaptorSignature)).to.be.eq(t);
191+
});
192+
193+
it("should revert if invalid signature or adaptor signature scalar is provided", async () => {
194+
const signature = ethers.solidityPacked(["uint256", "uint256", "uint256"], [1n, 1n, 10n]);
195+
const adaptorSignature = ethers.solidityPacked(["uint256", "uint256", "uint256"], [1n, 1n, 2n]);
196+
197+
const invalidSignature = ethers.solidityPacked(["uint256", "uint256", "uint256"], [1n, 1n, secp256k1.CURVE.n]);
198+
const invalidAdaptorSignature = ethers.solidityPacked(
199+
["uint256", "uint256", "uint256"],
200+
[1n, 1n, secp256k1.CURVE.n],
201+
);
202+
203+
await expect(schnorr.extractSECP256k1(invalidSignature, adaptorSignature)).to.be.revertedWithCustomError(
204+
schnorr,
205+
"InvalidSignatureScalar",
206+
);
207+
await expect(schnorr.extractSECP256k1(signature, invalidAdaptorSignature)).to.be.revertedWithCustomError(
208+
schnorr,
209+
"InvalidSignatureScalar",
210+
);
211+
});
212+
});
111213
});

0 commit comments

Comments
 (0)