Page MenuHomePhabricator

No OneTemporary

diff --git a/modules/ecash-lib-wasm/src/ecc.rs b/modules/ecash-lib-wasm/src/ecc.rs
index db661b831..3853adce9 100644
--- a/modules/ecash-lib-wasm/src/ecc.rs
+++ b/modules/ecash-lib-wasm/src/ecc.rs
@@ -1,215 +1,271 @@
// Copyright (c) 2024 The Bitcoin developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
//! Module for [`Ecc`] for signing secp256k1 signatures.
use ecash_secp256k1::{
- ecdsa::{RecoverableSignature, RecoveryId},
+ ecdsa::{RecoverableSignature, RecoveryId, Signature as EcdsaSignature},
+ schnorr::Signature as SchnorrSignature,
All, Message, PublicKey, Scalar, Secp256k1, SecretKey,
};
use thiserror::Error;
use wasm_bindgen::prelude::*;
use crate::ecc::EccError::*;
/// ECC signatures with libsecp256k1.
#[derive(Debug)]
#[wasm_bindgen]
pub struct Ecc {
curve: Secp256k1<All>,
}
/// Errors indicating something went wrong with [`Ecc`].
#[derive(Error, Debug, PartialEq, Eq)]
pub enum EccError {
/// Invalid secret key size
#[error("Invalid secret key size, expected 32 bytes but got {0}")]
InvalidSeckeySize(usize),
/// Invalid secret key for signing
#[error("Secret key not valid for secp256k1")]
InvalidSeckey,
/// Invalid public key size
#[error("Invalid public key size, expected 33 bytes but got {0}")]
InvalidPubkeySize(usize),
/// Invalid pubkey key for signing
#[error("Pubkey key not valid for secp256k1")]
InvalidPubkey,
/// Invalid scalar size
#[error("Invalid scalar size, expected 32 bytes but got {0}")]
InvalidScalarSize(usize),
/// Invalid scalar in range
#[error("Scalar not valid for secp256k1")]
InvalidScalar,
/// Invalid message size
#[error("Invalid message size, expected 32 bytes but got {0}")]
InvalidMessageSize(usize),
+
+ /// Invalid DER signature format
+ #[error("Invalid DER signature format")]
+ InvalidDerSignatureFormat,
+
+ /// Invalid Schnorr signature size
+ #[error("Invalid Schnorr signature size, expected 64 bytes but got {0}")]
+ InvalidSignatureSize(usize),
+
+ /// Incorrect signature
+ #[error("Incorrect signature")]
+ IncorrectSignature,
}
impl From<EccError> for String {
fn from(ecc: EccError) -> Self {
ecc.to_string()
}
}
fn parse_secret_key(seckey: &[u8]) -> Result<SecretKey, String> {
Ok(SecretKey::from_byte_array(
&seckey
.try_into()
.map_err(|_| InvalidSeckeySize(seckey.len()))?,
)
.map_err(|_| InvalidSeckey)?)
}
fn parse_public_key(pubkey: &[u8]) -> Result<PublicKey, String> {
Ok(PublicKey::from_byte_array_compressed(
pubkey
.try_into()
.map_err(|_| InvalidPubkeySize(pubkey.len()))?,
)
.map_err(|_| InvalidPubkey)?)
}
fn parse_scalar(scalar: &[u8]) -> Result<Scalar, String> {
Ok(Scalar::from_be_bytes(
scalar
.try_into()
.map_err(|_| InvalidScalarSize(scalar.len()))?,
)
.map_err(|_| InvalidScalar)?)
}
fn parse_msg(msg: &[u8]) -> Result<Message, String> {
Ok(Message::from_digest(
msg.try_into().map_err(|_| InvalidMessageSize(msg.len()))?,
))
}
+fn parse_ecdsa_sig(sig: &[u8]) -> Result<EcdsaSignature, String> {
+ Ok(EcdsaSignature::from_der(sig).map_err(|_| InvalidDerSignatureFormat)?)
+}
+
+fn parse_schnorr_sig(sig: &[u8]) -> Result<SchnorrSignature, String> {
+ Ok(SchnorrSignature::from_slice(sig)
+ .map_err(|_| InvalidSignatureSize(sig.len()))?)
+}
+
#[wasm_bindgen]
impl Ecc {
/// Create a new Ecc instance.
#[allow(clippy::new_without_default)]
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
Ecc {
curve: Secp256k1::default(),
}
}
/// Derive a public key from secret key.
#[wasm_bindgen(js_name = derivePubkey)]
pub fn derive_pubkey(&self, seckey: &[u8]) -> Result<Vec<u8>, String> {
let seckey = parse_secret_key(seckey)?;
let pubkey = PublicKey::from_secret_key(&self.curve, &seckey);
Ok(pubkey.serialize().to_vec())
}
/// Sign an ECDSA signature.
#[wasm_bindgen(js_name = ecdsaSign)]
pub fn ecdsa_sign(
&self,
seckey: &[u8],
msg: &[u8],
) -> Result<Vec<u8>, String> {
let seckey = parse_secret_key(seckey)?;
let msg = parse_msg(msg)?;
let sig = self.curve.sign_ecdsa(&msg, &seckey);
Ok(sig.serialize_der().to_vec())
}
+ /// Verify an ECDSA signature.
+ #[wasm_bindgen(js_name = ecdsaVerify)]
+ pub fn ecdsa_verify(
+ &self,
+ sig: &[u8],
+ msg: &[u8],
+ pk: &[u8],
+ ) -> Result<(), String> {
+ let msg = parse_msg(msg)?;
+ let sig = parse_ecdsa_sig(sig)?;
+ let pk = parse_public_key(pk)?;
+ self.curve
+ .verify_ecdsa(&msg, &sig, &pk)
+ .map_err(|_| IncorrectSignature)?;
+ Ok(())
+ }
+
/// Sign a Schnorr signature.
#[wasm_bindgen(js_name = schnorrSign)]
pub fn schnorr_sign(
&self,
seckey: &[u8],
msg: &[u8],
) -> Result<Vec<u8>, String> {
let seckey = parse_secret_key(seckey)?;
let msg = parse_msg(msg)?;
let sig = self.curve.sign_schnorrabc_no_aux_rand(&msg, &seckey);
Ok(sig.as_ref().to_vec())
}
+ /// Verify a Schnorr signature.
+ #[wasm_bindgen(js_name = schnorrVerify)]
+ pub fn schnorr_verify(
+ &self,
+ sig: &[u8],
+ msg: &[u8],
+ pk: &[u8],
+ ) -> Result<(), String> {
+ let msg = parse_msg(msg)?;
+ let sig = parse_schnorr_sig(sig)?;
+ let pk = parse_public_key(pk)?;
+ self.curve
+ .verify_schnorrabc(&sig, &msg, &pk)
+ .map_err(|_| IncorrectSignature)?;
+ Ok(())
+ }
+
/// Return whether the given secret key is valid, i.e. whether is of correct
/// length (32 bytes) and is on the curve.
#[wasm_bindgen(js_name = isValidSeckey)]
pub fn is_valid_seckey(&self, seckey: &[u8]) -> bool {
parse_secret_key(seckey).is_ok()
}
/// Add a scalar to a secret key.
#[wasm_bindgen(js_name = seckeyAdd)]
pub fn seckey_add(&self, a: &[u8], b: &[u8]) -> Result<Vec<u8>, String> {
let a = parse_secret_key(a)?;
let b = parse_scalar(b)?;
Ok(a.add_tweak(&b)
.map_err(|_| InvalidSeckey)?
.secret_bytes()
.to_vec())
}
/// Add a scalar to a public key (adding G*b).
#[wasm_bindgen(js_name = pubkeyAdd)]
pub fn pubkey_add(&self, a: &[u8], b: &[u8]) -> Result<Vec<u8>, String> {
let a = parse_public_key(a)?;
let b = parse_scalar(b)?;
Ok(a.add_exp_tweak(&self.curve, &b)
.map_err(|_| InvalidPubkey)?
.serialize()
.to_vec())
}
/// Create a compact ECDSA signature (65 bytes), which allows reconstructing
/// the used public key.
/// The format is one header byte, followed by two times 32 bytes for the
/// serialized r and s values.
/// The header byte: 0x1B = first key with even y,
/// 0x1C = first key with odd y,
/// 0x1D = second key with even y,
/// 0x1E = second key with odd y,
/// add 0x04 for compressed keys.
#[wasm_bindgen(js_name = signRecoverable)]
pub fn sign_recoverable(
&self,
seckey: &[u8],
msg: &[u8],
) -> Result<Vec<u8>, String> {
let seckey = parse_secret_key(seckey)?;
let msg = parse_msg(msg)?;
let sig = self.curve.sign_ecdsa_recoverable(&msg, &seckey);
let (recover_id, sig_rs) = sig.serialize_compact();
let mut ser_sig = [0u8; 65];
ser_sig[0] = i32::from(recover_id) as u8 + 0x1b + 0x04;
ser_sig[1..].copy_from_slice(&sig_rs);
Ok(ser_sig.to_vec())
}
/// Recover the public key of a signature signed by signRecoverable.
#[wasm_bindgen(js_name = recoverSig)]
pub fn recover_sig(
&self,
sig: &[u8],
msg: &[u8],
) -> Result<Vec<u8>, String> {
let recovery_id = sig[0].saturating_sub(0x1b + 0x04);
let recovery_id = RecoveryId::try_from(recovery_id as i32)
.map_err(|_| format!("Invalid recovery ID: {recovery_id}"))?;
let sig = RecoverableSignature::from_compact(&sig[1..], recovery_id)
.map_err(|_| "Invalid signature format")?;
let msg = Message::from_digest(
msg.try_into()
.map_err(|_| format!("Invalid msg length {}", msg.len()))?,
);
let pubkey = self
.curve
.recover_ecdsa(&msg, &sig)
.map_err(|_| "Signature recovery failed")?;
Ok(pubkey.serialize().to_vec())
}
}
diff --git a/modules/ecash-lib/src/ecc.test.ts b/modules/ecash-lib/src/ecc.test.ts
index 6b03298a3..230f950ed 100644
--- a/modules/ecash-lib/src/ecc.test.ts
+++ b/modules/ecash-lib/src/ecc.test.ts
@@ -1,48 +1,68 @@
// Copyright (c) 2024 The Bitcoin developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
import { expect } from 'chai';
import { fromHex, toHex } from './io/hex.js';
import { Ecc, EccDummy } from './ecc.js';
import './initNodeJs.js';
describe('Ecc', async () => {
it('EccWasm', () => {
const ecc = new Ecc();
const sk = fromHex(
'0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef',
);
const msg = fromHex(
'fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210',
);
const pk = ecc.derivePubkey(sk);
expect(toHex(pk)).to.equal(
'034646ae5047316b4230d0086c8acec687f00b1cd9d1dc634f6cb358ac0a9a8fff',
);
- expect(ecc.schnorrSign(sk, msg)).to.have.lengthOf(64);
- expect(ecc.ecdsaSign(sk, msg)).length.to.be.within(65, 73);
+ const schnorrSig = ecc.schnorrSign(sk, msg);
+ expect(schnorrSig).to.have.lengthOf(64);
+ ecc.schnorrVerify(schnorrSig, msg, pk);
+ expect(() => ecc.schnorrVerify(new Uint8Array(10), msg, pk)).to.throw(
+ 'Invalid Schnorr signature size, expected 64 bytes but got 10',
+ );
+ expect(() => ecc.schnorrVerify(new Uint8Array(64), msg, pk)).to.throw(
+ 'Incorrect signature',
+ );
+
+ const ecdsaSig = ecc.ecdsaSign(sk, msg);
+ expect(ecdsaSig).length.to.be.within(65, 73);
+ ecc.ecdsaVerify(ecdsaSig, msg, pk);
+ expect(() => ecc.ecdsaVerify(fromHex('30'), msg, pk)).to.throw(
+ 'Invalid DER signature format',
+ );
+ expect(() =>
+ ecc.ecdsaVerify(ecc.ecdsaSign(sk, new Uint8Array(32)), msg, pk),
+ ).to.throw('Incorrect signature');
+
// Round-trip recoverable signature
expect(
toHex(ecc.recoverSig(ecc.signRecoverable(sk, msg), msg)),
).to.equal(toHex(pk));
});
it('EccDummy', () => {
const dummy = new EccDummy();
expect(dummy.derivePubkey({} as any)).to.deep.equal(new Uint8Array(33));
expect(dummy.schnorrSign({} as any, {} as any)).to.deep.equal(
new Uint8Array(64),
);
+ dummy.schnorrVerify({} as any, {} as any, {} as any);
expect(dummy.ecdsaSign({} as any, {} as any)).to.deep.equal(
new Uint8Array(73),
);
+ dummy.ecdsaVerify({} as any, {} as any, {} as any);
expect(dummy.signRecoverable({} as any, {} as any)).to.deep.equal(
new Uint8Array(65),
);
expect(dummy.recoverSig({} as any, {} as any)).to.deep.equal(
new Uint8Array(33),
);
});
});
diff --git a/modules/ecash-lib/src/ecc.ts b/modules/ecash-lib/src/ecc.ts
index 42d549609..c82bf65a4 100644
--- a/modules/ecash-lib/src/ecc.ts
+++ b/modules/ecash-lib/src/ecc.ts
@@ -1,117 +1,149 @@
// Copyright (c) 2024 The Bitcoin developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
/** Interface to abstract over Elliptic Curve Cryptography */
export interface Ecc {
/** Derive a public key from secret key. */
derivePubkey(seckey: Uint8Array): Uint8Array;
/** Sign an ECDSA signature. msg needs to be a 32-byte hash */
ecdsaSign(seckey: Uint8Array, msg: Uint8Array): Uint8Array;
+ /**
+ * Verify an ECDSA signature. msg needs to be a 32-byte hash.
+ * Throws an exception if the signature is invalid.
+ **/
+ ecdsaVerify(sig: Uint8Array, msg: Uint8Array, pk: Uint8Array): void;
+
/** Sign a Schnorr signature. msg needs to be a 32-byte hash */
schnorrSign(seckey: Uint8Array, msg: Uint8Array): Uint8Array;
+ /**
+ * Verify a Schnorr signature. msg needs to be a 32-byte hash.
+ * Throws an exception if the signature is invalid.
+ **/
+ schnorrVerify(sig: Uint8Array, msg: Uint8Array, pk: Uint8Array): void;
+
/**
* Return whether the given secret key is valid, i.e. whether is of correct
* length (32 bytes) and is on the curve.
*/
isValidSeckey(seckey: Uint8Array): boolean;
/** Add a scalar to a secret key */
seckeyAdd(a: Uint8Array, b: Uint8Array): Uint8Array;
/** Add a scalar to a public key (adding G*b) */
pubkeyAdd(a: Uint8Array, b: Uint8Array): Uint8Array;
/** Sign a ECDSA recoverable signature, includes the recovery ID */
signRecoverable(seckey: Uint8Array, msg: Uint8Array): Uint8Array;
/** Recover the public key of an ECDSA signed signature (with recovery ID) */
recoverSig(sig: Uint8Array, msg: Uint8Array): Uint8Array;
}
/** Dummy Ecc impl that always returns 0, useful for measuring tx size */
export class EccDummy implements Ecc {
derivePubkey(_seckey: Uint8Array): Uint8Array {
return new Uint8Array(33);
}
ecdsaSign(_seckey: Uint8Array, _msg: Uint8Array): Uint8Array {
return new Uint8Array(73);
}
+ ecdsaVerify(_sig: Uint8Array, _msg: Uint8Array, _pk: Uint8Array): void {}
+
schnorrSign(_seckey: Uint8Array, _msg: Uint8Array): Uint8Array {
return new Uint8Array(64);
}
+ schnorrVerify(_sig: Uint8Array, _msg: Uint8Array, _pk: Uint8Array): void {}
+
isValidSeckey(_seckey: Uint8Array): boolean {
return false;
}
seckeyAdd(_a: Uint8Array, _b: Uint8Array): Uint8Array {
return new Uint8Array(32);
}
pubkeyAdd(_a: Uint8Array, _b: Uint8Array): Uint8Array {
return new Uint8Array(32);
}
signRecoverable(_seckey: Uint8Array, _msg: Uint8Array): Uint8Array {
return new Uint8Array(65);
}
recoverSig(_sig: Uint8Array, _msg: Uint8Array): Uint8Array {
return new Uint8Array(33);
}
}
const ECC: { ecc?: Ecc } = {};
export function __setEcc(ecc: Ecc) {
ECC.ecc = ecc;
}
export class Ecc implements Ecc {
/** Derive a public key from secret key. */
derivePubkey(seckey: Uint8Array): Uint8Array {
return ECC.ecc!.derivePubkey(seckey);
}
/** Sign an ECDSA signature. msg needs to be a 32-byte hash */
ecdsaSign(seckey: Uint8Array, msg: Uint8Array): Uint8Array {
return ECC.ecc!.ecdsaSign(seckey, msg);
}
+ /**
+ * Verify an ECDSA signature. msg needs to be a 32-byte hash.
+ * Throws an exception if the signature is invalid.
+ **/
+ ecdsaVerify(sig: Uint8Array, msg: Uint8Array, pk: Uint8Array): void {
+ ECC.ecc?.ecdsaVerify(sig, msg, pk);
+ }
+
/** Sign a Schnorr signature. msg needs to be a 32-byte hash */
schnorrSign(seckey: Uint8Array, msg: Uint8Array): Uint8Array {
return ECC.ecc!.schnorrSign(seckey, msg);
}
+ /**
+ * Verify a Schnorr signature. msg needs to be a 32-byte hash.
+ * Throws an exception if the signature is invalid.
+ **/
+ schnorrVerify(sig: Uint8Array, msg: Uint8Array, pk: Uint8Array): void {
+ ECC.ecc?.schnorrVerify(sig, msg, pk);
+ }
+
/**
* Return whether the given secret key is valid, i.e. whether is of correct
* length (32 bytes) and is on the curve.
*/
isValidSeckey(seckey: Uint8Array): boolean {
return ECC.ecc!.isValidSeckey(seckey);
}
/** Add a scalar to a secret key */
seckeyAdd(a: Uint8Array, b: Uint8Array): Uint8Array {
return ECC.ecc!.seckeyAdd(a, b);
}
/** Add a scalar to a public key (adding G*b) */
pubkeyAdd(a: Uint8Array, b: Uint8Array): Uint8Array {
return ECC.ecc!.pubkeyAdd(a, b);
}
signRecoverable(seckey: Uint8Array, msg: Uint8Array): Uint8Array {
return ECC.ecc!.signRecoverable(seckey, msg);
}
recoverSig(sig: Uint8Array, msg: Uint8Array): Uint8Array {
return ECC.ecc!.recoverSig(sig, msg);
}
}

File Metadata

Mime Type
text/x-diff
Expires
Wed, May 21, 17:59 (1 h, 38 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5865641
Default Alt Text
(16 KB)

Event Timeline