Skip to content

Commit

Permalink
Add secp256r1 host function for signature verification (#1376)
Browse files Browse the repository at this point in the history
### What

Resolves #807 by adding
a new host function `verify_sig_ecdsa_secp256r1` for ECDSA signature
verification using secp256r1 curve. The function accepts following
inputs:
- `public_key: BytesObject` containing the 65-byte SEC-1 uncompressed
ECDSA public key
- `msg_digest: BytesObject` a 32-byte hash of the message
- `signature`: the 64-byte signature `(r, s)` serialized as fixed-width
big endian scalars

The function is gated behind protocol 21 (`min_supported_protocol =
21`).

PR with the associated XDR changes:
stellar/stellar-xdr#178,
stellar/rs-stellar-xdr#355

#### Metering and Calibration
Two new cost types have been newly added:
- `Sec1DecodePointUncompressed`: constant cost type representing the
cost to decode the `public_key`
- `VerifyEcdsaSecp256r1Sig` : constant cost type represent the cost of
ECDSA sig verification

A prevous cost type `ComputeEcdsaSecp256k1Sig` has been renamed to
`DecodeEcdsaCurve256Sig`, which represents the cost of deserializing
both the `secp256k1` and `secp256r1` signatures.

Calibration: 
- each new cost type mentioned above have been benchmarked and
calibrated.
- plus a few experimental types have been added to answer key questions
regarding the host interface (will provide a supplemental doc soon).

#### Testing

Unit tests have been added to test against various forms of invalid
inputs.

In addition, two set of test vectors has been added in integration test:
- [NIST test
vectors](https://csrc.nist.gov/groups/STM/cavp/documents/dss/186-3ecdsatestvectors.zip)
- Google's [wycheproof](https://github.com/C2SP/wycheproof) test vectors
  • Loading branch information
jayz22 authored Apr 3, 2024
1 parent a1cb3fd commit 97168ab
Show file tree
Hide file tree
Showing 38 changed files with 1,670 additions and 248 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/target
.vscode
/vendor
lcov.info
lcov.info
.DS_Store
43 changes: 40 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ wasmparser = "=0.116.1"
[workspace.dependencies.stellar-xdr]
version = "=20.1.0"
git = "https://github.com/stellar/rs-stellar-xdr"
rev = "44b7e2d4cdf27a3611663e82828de56c5274cba0"
rev = "3a001b1fbb20e4cfa2cef2c0cc450564e8528057"
default-features = false

[workspace.dependencies.wasmi]
Expand Down
23 changes: 22 additions & 1 deletion soroban-env-common/env.json
Original file line number Diff line number Diff line change
Expand Up @@ -2000,7 +2000,28 @@
}
],
"return": "BytesObject",
"docs": "Recovers the SEC-1-encoded ECDSA secp256k1 public key that produced a given 64-byte signature over a given 32-byte message digest, for a given recovery_id byte."
"docs": "Recovers the SEC-1-encoded ECDSA secp256k1 public key that produced a given 64-byte `signature` over a given 32-byte `msg_digest` for a given `recovery_id` byte. Warning: The `msg_digest` must be produced by a secure cryptographic hash function on the message, otherwise the attacker can potentially forge signatures. The `signature` is the ECDSA signature `(r, s)` serialized as fixed-size big endian scalar values, both `r`, `s` must be non-zero and `s` must be in the lower range. Returns a `BytesObject` containing 65-bytes representing SEC-1 encoded point in uncompressed format. The `recovery_id` is an integer value `0`, `1`, `2`, or `3`, the low bit (0/1) indicates the parity of the y-coordinate of the `public_key` (even/odd) and the high bit (3/4) indicate if the `r` (x-coordinate of `k x G`) has overflown during its computation."
},
{
"export": "3",
"name": "verify_sig_ecdsa_secp256r1",
"args": [
{
"name": "public_key",
"type": "BytesObject"
},
{
"name": "msg_digest",
"type": "BytesObject"
},
{
"name": "signature",
"type": "BytesObject"
}
],
"return": "Void",
"docs": "Verifies the `signature` using an ECDSA secp256r1 `public_key` on a 32-byte `msg_digest`. Warning: The `msg_digest` must be produced by a secure cryptographic hash function on the message, otherwise the attacker can potentially forge signatures. The `public_key` is expected to be 65 bytes in length, representing a SEC-1 encoded point in uncompressed format. The `signature` is the ECDSA signature `(r, s)` serialized as fixed-size big endian scalar values, both `r`, `s` must be non-zero and `s` must be in the lower range. ",
"min_supported_protocol": 21
}
]
},
Expand Down
12 changes: 10 additions & 2 deletions soroban-env-host/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,12 @@ num-traits = "=0.2.17"
num-integer = "=0.1.45"
num-derive = "=0.4.1"
backtrace = { version = "=0.3.69", optional = true }
k256 = {version = "=0.13.1", features=["ecdsa", "arithmetic"]}
k256 = {version = "=0.13.1", default-features = false, features = ["ecdsa", "arithmetic"]}
p256 = {version = "=0.13.2", default-features = false, features = ["ecdsa", "arithmetic"]}
ecdsa = {version = "=0.16.8", default-features = false}
sec1 = {version = "=0.7.3"}
elliptic-curve ={ version = "0.13.6", default-features = false}
generic-array ={ version = "0.14.7"}
# NB: getrandom is a transitive dependency of k256 which we're not using directly
# but we have to specify it here in order to enable its 'js' feature which
# is needed to build the host for wasm (a rare but supported config).
Expand Down Expand Up @@ -83,11 +88,14 @@ lstsq = "=0.5.0"
nalgebra = { version = "=0.32.3", default-features = false, features = ["std"]}
wasm-encoder = "=0.36.2"
rustversion = "1.0"
wycheproof = "=0.5.1"
k256 = {version = "=0.13.1", default-features = false, features = ["alloc"]}
p256 = {version = "=0.13.2", default-features = false, features = ["alloc"]}

[dev-dependencies.stellar-xdr]
version = "=20.1.0"
git = "https://github.com/stellar/rs-stellar-xdr"
rev = "44b7e2d4cdf27a3611663e82828de56c5274cba0"
rev = "3a001b1fbb20e4cfa2cef2c0cc450564e8528057"
default-features = false
features = ["arbitrary"]

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
use crate::common::HostCostMeasurement;
use ecdsa::signature::hazmat::PrehashSigner;
use elliptic_curve::scalar::IsHigh;
use k256::{
ecdsa::{Signature, SigningKey},
Secp256k1,
};
use rand::{rngs::StdRng, RngCore};
use soroban_env_host::{
cost_runner::{DecodeEcdsaCurve256SigRun, DecodeEcdsaCurve256SigSample},
Host,
};

pub(crate) struct DecodeEcdsaCurve256SigMeasure;

impl HostCostMeasurement for DecodeEcdsaCurve256SigMeasure {
type Runner = DecodeEcdsaCurve256SigRun<Secp256k1>;

fn new_random_case(
_host: &Host,
rng: &mut StdRng,
_input: u64,
) -> DecodeEcdsaCurve256SigSample {
let mut key_bytes = [0u8; 32];
rng.fill_bytes(&mut key_bytes);
let signer = SigningKey::from_bytes(&key_bytes.into()).unwrap();
let mut msg_hash = [0u8; 32];
rng.fill_bytes(&mut msg_hash);
let mut sig: Signature = signer.sign_prehash(&msg_hash).unwrap();
// in our host implementation, we are rejecting high `S`. so here we
// normalize it to the low S before sending the result
if bool::from(sig.s().is_high()) {
sig = sig.normalize_s().unwrap();
}
DecodeEcdsaCurve256SigSample {
bytes: sig.to_vec(),
}
}
}
14 changes: 14 additions & 0 deletions soroban-env-host/benches/common/cost_types/mod.rs
Original file line number Diff line number Diff line change
@@ -1,34 +1,48 @@
#[cfg(not(feature = "next"))]
mod compute_ecdsa_secp256k1_sig;
mod compute_ed25519_pubkey;
mod compute_keccak256_hash;
mod compute_sha256_hash;
#[cfg(feature = "next")]
mod decode_ecdsa_curve256_sig;
mod host_mem_alloc;
mod host_mem_cmp;
mod host_mem_cpy;
mod invoke;
mod num_ops;
mod prng;
mod recover_ecdsa_secp256k1_key;
#[cfg(feature = "next")]
mod sec1_decode_point_uncompressed;
mod val_deser;
mod val_ser;
#[cfg(feature = "next")]
mod verify_ecdsa_secp256r1_sig;
mod verify_ed25519_sig;
mod visit_object;
mod vm_ops;
mod wasm_insn_exec;

#[cfg(not(feature = "next"))]
pub(crate) use compute_ecdsa_secp256k1_sig::*;
pub(crate) use compute_ed25519_pubkey::*;
pub(crate) use compute_keccak256_hash::*;
pub(crate) use compute_sha256_hash::*;
#[cfg(feature = "next")]
pub(crate) use decode_ecdsa_curve256_sig::*;
pub(crate) use host_mem_alloc::*;
pub(crate) use host_mem_cmp::*;
pub(crate) use host_mem_cpy::*;
pub(crate) use invoke::*;
pub(crate) use num_ops::*;
pub(crate) use prng::*;
pub(crate) use recover_ecdsa_secp256k1_key::*;
#[cfg(feature = "next")]
pub(crate) use sec1_decode_point_uncompressed::*;
pub(crate) use val_deser::*;
pub(crate) use val_ser::*;
#[cfg(feature = "next")]
pub(crate) use verify_ecdsa_secp256r1_sig::*;
pub(crate) use verify_ed25519_sig::*;
pub(crate) use visit_object::*;
pub(crate) use vm_ops::*;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use crate::common::HostCostMeasurement;
use p256::ecdsa::SigningKey;
use rand::{rngs::StdRng, RngCore};
use soroban_env_host::{
cost_runner::{Sec1DecodePointSample, Sec1DecodePointUncompressedRun},
Host,
};

pub(crate) struct Sec1DecodePointUncompressedMeasure {}

impl HostCostMeasurement for Sec1DecodePointUncompressedMeasure {
type Runner = Sec1DecodePointUncompressedRun;

fn new_random_case(_host: &Host, rng: &mut StdRng, _input: u64) -> Sec1DecodePointSample {
let mut key_bytes = [0u8; 32];
rng.fill_bytes(&mut key_bytes);
let signer = SigningKey::from_bytes(&key_bytes.into()).unwrap();
let verifying_key = signer.verifying_key();
let bytes = verifying_key
.to_encoded_point(false /* compress */)
.to_bytes();
Sec1DecodePointSample { bytes }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use crate::common::HostCostMeasurement;
use ecdsa::signature::hazmat::PrehashSigner;
use elliptic_curve::scalar::IsHigh;
use p256::ecdsa::{Signature, SigningKey};
use rand::{rngs::StdRng, RngCore};
use soroban_env_host::{
cost_runner::{VerifyEcdsaSecp256r1SigRun, VerifyEcdsaSecp256r1SigSample},
xdr::Hash,
Host,
};

pub(crate) struct VerifyEcdsaSecp256r1SigMeasure {}

impl HostCostMeasurement for VerifyEcdsaSecp256r1SigMeasure {
type Runner = VerifyEcdsaSecp256r1SigRun;

fn new_random_case(
_host: &Host,
rng: &mut StdRng,
_input: u64,
) -> VerifyEcdsaSecp256r1SigSample {
let mut key_bytes = [0u8; 32];
rng.fill_bytes(&mut key_bytes);
let signer = SigningKey::from_bytes(&key_bytes.into()).unwrap();
let mut msg_hash = [0u8; 32];
rng.fill_bytes(&mut msg_hash);
let mut sig: Signature = signer.sign_prehash(&msg_hash).unwrap();
// in our host implementation, we are rejecting high `s`, we are doing it here too.
if bool::from(sig.s().is_high()) {
sig = sig.normalize_s().unwrap()
}
VerifyEcdsaSecp256r1SigSample {
pub_key: signer.verifying_key().clone(),
msg_hash: Hash::from(msg_hash),
sig,
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use crate::common::HostCostMeasurement;
use ecdsa::signature::hazmat::PrehashSigner;
use elliptic_curve::scalar::IsHigh;
use rand::{rngs::StdRng, RngCore};
use soroban_env_host::{
cost_runner::{DecodeEcdsaCurve256SigSample, DecodeSecp256r1SigRun},
Host,
};

pub(crate) struct DecodeSecp256r1SigMeasure {}

impl HostCostMeasurement for DecodeSecp256r1SigMeasure {
type Runner = DecodeSecp256r1SigRun;

fn new_random_case(
_host: &Host,
rng: &mut StdRng,
_input: u64,
) -> DecodeEcdsaCurve256SigSample {
use p256::ecdsa::{Signature, SigningKey};

let mut key_bytes = [0u8; 32];
rng.fill_bytes(&mut key_bytes);
let signer = SigningKey::from_bytes(&key_bytes.into()).unwrap();
let mut msg_hash = [0u8; 32];
rng.fill_bytes(&mut msg_hash);
let mut sig: Signature = signer.sign_prehash(&msg_hash).unwrap();
// in our host implementation, we are rejecting high `s`, we are doing it here too.
if bool::from(sig.s().is_high()) {
sig = sig.normalize_s().unwrap();
}
DecodeEcdsaCurve256SigSample {
bytes: sig.to_vec(),
}
}
}
Loading

0 comments on commit 97168ab

Please sign in to comment.