From 5e2c1bc95cb1dfeebdbef678bcd601e908fbb574 Mon Sep 17 00:00:00 2001 From: 0xZensh Date: Sat, 27 Jul 2024 13:58:34 +0800 Subject: [PATCH] feat: implement threshold Schnorr --- Cargo.lock | 49 ++++++++ Cargo.toml | 2 +- src/ic_cose_canister/Cargo.toml | 2 + src/ic_cose_canister/ic_cose_canister.did | 9 +- src/ic_cose_canister/src/api_cose.rs | 58 +++++++--- src/ic_cose_canister/src/ecdsa.rs | 29 ++--- src/ic_cose_canister/src/lib.rs | 1 + src/ic_cose_canister/src/schnorr.rs | 131 ++++++++++++++++++++++ src/ic_cose_canister/src/store.rs | 127 ++++++++++++++++----- src/ic_cose_types/src/cose/ecdsa.rs | 22 ---- src/ic_cose_types/src/cose/ed25519.rs | 14 +++ src/ic_cose_types/src/cose/k256.rs | 49 ++++++++ src/ic_cose_types/src/cose/mod.rs | 2 +- src/ic_cose_types/src/cose/sign1.rs | 16 +-- src/ic_cose_types/src/lib.rs | 1 + src/ic_cose_types/src/types/mod.rs | 11 +- 16 files changed, 427 insertions(+), 96 deletions(-) create mode 100644 src/ic_cose_canister/src/schnorr.rs delete mode 100644 src/ic_cose_types/src/cose/ecdsa.rs create mode 100644 src/ic_cose_types/src/cose/k256.rs diff --git a/Cargo.lock b/Cargo.lock index f84f522..d1cbf28 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -538,8 +538,11 @@ checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" dependencies = [ "curve25519-dalek", "ed25519", + "merlin", + "rand_core", "serde", "sha2", + "signature", "subtle", "zeroize", ] @@ -804,6 +807,15 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + [[package]] name = "hmac" version = "0.12.1" @@ -972,6 +984,20 @@ name = "ic-constants" version = "0.9.0" source = "git+https://github.com/dfinity/ic/?rev=d19fa446ab35780b2c6d8b82ea32d808cca558d5#d19fa446ab35780b2c6d8b82ea32d808cca558d5" +[[package]] +name = "ic-crypto-ed25519" +version = "0.9.0" +source = "git+https://github.com/dfinity/ic/?rev=d19fa446ab35780b2c6d8b82ea32d808cca558d5#d19fa446ab35780b2c6d8b82ea32d808cca558d5" +dependencies = [ + "curve25519-dalek", + "ed25519-dalek", + "hkdf", + "pem", + "rand", + "sha2", + "zeroize", +] + [[package]] name = "ic-crypto-extended-bip32" version = "0.9.0" @@ -1228,11 +1254,13 @@ dependencies = [ "bytes", "candid", "ciborium", + "ed25519-dalek", "getrandom", "hex", "hmac", "ic-cdk 0.15.0", "ic-cdk-timers", + "ic-crypto-ed25519", "ic-crypto-extended-bip32", "ic-http-certification", "ic-stable-structures", @@ -1448,6 +1476,18 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "merlin" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d" +dependencies = [ + "byteorder", + "keccak", + "rand_core", + "zeroize", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -1536,6 +1576,15 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pem" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8835c273a76a90455d7344889b0964598e3316e2a79ede8e36f16bdcf2228b8" +dependencies = [ + "base64 0.13.1", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" diff --git a/Cargo.toml b/Cargo.toml index 11adda8..58dee83 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ serde_bytes = "0.11" serde_json = { version = "1", features = ["preserve_order"] } structured-logger = "1" tokio = { version = "1", features = ["full"] } -k256 = { version = "0.13", features = ["ecdsa"] } +k256 = { version = "0.13", features = ["ecdsa", "schnorr"] } ed25519-dalek = "2" x25519-dalek = { version = "2", features = ["static_secrets"] } hex = "0.4" diff --git a/src/ic_cose_canister/Cargo.toml b/src/ic_cose_canister/Cargo.toml index 100dd33..12468f1 100644 --- a/src/ic_cose_canister/Cargo.toml +++ b/src/ic_cose_canister/Cargo.toml @@ -32,9 +32,11 @@ once_cell = { workspace = true } ic-cdk-timers = { workspace = true } ic-stable-structures = { workspace = true } ic-http-certification = { workspace = true } +ed25519-dalek = { workspace = true } x25519-dalek = { workspace = true } lazy_static = "1.4" getrandom = { version = "0.2", features = ["custom"] } ic_cose_types = { path = "../ic_cose_types", version = "0.1" } ic-crypto-extended-bip32 = { git = "https://github.com/dfinity/ic/", rev = "d19fa446ab35780b2c6d8b82ea32d808cca558d5" } +ic-crypto-ed25519 = { git = "https://github.com/dfinity/ic/", rev = "d19fa446ab35780b2c6d8b82ea32d808cca558d5" } icrc-ledger-types = { git = "https://github.com/dfinity/ic/", rev = "d19fa446ab35780b2c6d8b82ea32d808cca558d5" } diff --git a/src/ic_cose_canister/ic_cose_canister.did b/src/ic_cose_canister/ic_cose_canister.did index 4adf2a8..2ffcf90 100644 --- a/src/ic_cose_canister/ic_cose_canister.did +++ b/src/ic_cose_canister/ic_cose_canister.did @@ -68,6 +68,7 @@ type Result_6 = variant { Ok : vec NamespaceInfo; Err : text }; type Result_7 = variant { Ok : CreateSettingOutput; Err : text }; type Result_8 = variant { Ok : SettingInfo; Err : text }; type Result_9 = variant { Ok : SettingArchivedPayload; Err : text }; +type SchnorrAlgorithm = variant { ed25519; bip340secp256k1 }; type SettingArchivedPayload = record { dek : opt blob; version : nat32; @@ -150,8 +151,10 @@ service : (opt ChainArgs) -> { namespace_remove_managers : (text, vec principal) -> (Result); namespace_remove_users : (text, vec principal) -> (Result); namespace_update_info : (UpdateNamespaceInput) -> (Result); - schnorr_public_key : (text, opt PublicKeyInput) -> (Result_4) query; - schnorr_sign : (text, SignInput) -> (Result_5); + schnorr_public_key : (SchnorrAlgorithm, opt PublicKeyInput) -> ( + Result_4, + ) query; + schnorr_sign : (SchnorrAlgorithm, SignInput) -> (Result_5); setting_add_readers : (SettingPath, vec principal) -> (Result); setting_create : (SettingPath, CreateSettingInput) -> (Result_7); setting_get : (SettingPath) -> (Result_8) query; @@ -159,7 +162,7 @@ service : (opt ChainArgs) -> { setting_get_info : (SettingPath) -> (Result_8) query; setting_remove_readers : (SettingPath, vec principal) -> (Result); setting_update_info : (SettingPath, UpdateSettingInfoInput) -> (Result_7); - sign_identity : (text, text, opt text) -> (Result_5); + sign_identity : (text, text, opt SchnorrAlgorithm) -> (Result_5); state_get_info : () -> (Result_10) query; update_setting_payload : (SettingPath, UpdateSettingPayloadInput) -> ( Result_7, diff --git a/src/ic_cose_canister/src/api_cose.rs b/src/ic_cose_canister/src/api_cose.rs index 8018656..1d421ca 100644 --- a/src/ic_cose_canister/src/api_cose.rs +++ b/src/ic_cose_canister/src/api_cose.rs @@ -3,7 +3,10 @@ use ic_cose_types::{ cose_aes256_key, ecdh::ecdh_x25519, encrypt0::cose_encrypt0, format_error, mac3_256, CborSerializable, }, - types::{CosePath, ECDHInput, ECDHOutput, PublicKeyInput, PublicKeyOutput, SignInput}, + types::{ + CosePath, ECDHInput, ECDHOutput, PublicKeyInput, PublicKeyOutput, SchnorrAlgorithm, + SignInput, + }, validate_key, MILLISECONDS, }; use serde_bytes::ByteBuf; @@ -18,10 +21,7 @@ fn ecdsa_public_key(input: Option) -> Result store::state::with(|s| { s.ecdsa_public_key .as_ref() - .map(|k| PublicKeyOutput { - public_key: ByteBuf::from(k.public_key.clone()), - chain_code: ByteBuf::from(k.chain_code.clone()), - }) + .cloned() .ok_or_else(|| "failed to retrieve ECDSA public key".to_string()) }), } @@ -30,32 +30,64 @@ fn ecdsa_public_key(input: Option) -> Result Result { let caller = ic_cdk::caller(); - store::ns::ecdsa_sign(&caller, input.ns, input.derivation_path, input.message).await + store::ns::ecdsa_sign_with(&caller, input.ns, input.derivation_path, input.message).await } #[ic_cdk::query] fn schnorr_public_key( - _algorithm: String, - _input: Option, + algorithm: SchnorrAlgorithm, + input: Option, ) -> Result { - Err("not implemented".to_string()) + let caller = ic_cdk::caller(); + match input { + Some(input) => { + store::ns::schnorr_public_key(&caller, algorithm, input.ns, input.derivation_path) + } + None => store::state::with(|s| match algorithm { + SchnorrAlgorithm::Bip340Secp256k1 => s + .schnorr_secp256k1_public_key + .as_ref() + .cloned() + .ok_or_else(|| "failed to retrieve schnorr secp256k1 public key".to_string()), + SchnorrAlgorithm::Ed25519 => s + .schnorr_ed25519_public_key + .as_ref() + .cloned() + .ok_or_else(|| "failed to retrieve schnorr ed25519 public key".to_string()), + }), + } } #[ic_cdk::update(guard = "is_authenticated")] -async fn schnorr_sign(_algorithm: String, _input: SignInput) -> Result { - Err("not implemented".to_string()) +async fn schnorr_sign(algorithm: SchnorrAlgorithm, input: SignInput) -> Result { + let caller = ic_cdk::caller(); + store::ns::schnorr_sign_with( + &caller, + algorithm, + input.ns, + input.derivation_path, + input.message, + ) + .await } #[ic_cdk::update(guard = "is_authenticated")] async fn sign_identity( namespace: String, audience: String, - _algorithm: Option, + algorithm: Option, ) -> Result { validate_key(&namespace)?; let caller = ic_cdk::caller(); let now_ms = ic_cdk::api::time() / MILLISECONDS; - store::ns::ecdsa_sign_identity(&caller, namespace, audience, now_ms).await + store::ns::sign_identity( + &caller, + algorithm.unwrap_or(SchnorrAlgorithm::Ed25519), + namespace, + audience, + now_ms, + ) + .await } #[ic_cdk::update(guard = "is_authenticated")] diff --git a/src/ic_cose_canister/src/ecdsa.rs b/src/ic_cose_canister/src/ecdsa.rs index 1a4a3a6..c03f214 100644 --- a/src/ic_cose_canister/src/ecdsa.rs +++ b/src/ic_cose_canister/src/ecdsa.rs @@ -1,26 +1,26 @@ use ic_cdk::api::management_canister::ecdsa; +use ic_cose_types::{format_error, types::PublicKeyOutput}; use ic_crypto_extended_bip32::{DerivationIndex, DerivationPath, ExtendedBip32DerivationOutput}; - -pub type ECDSAPublicKey = ecdsa::EcdsaPublicKeyResponse; +use serde_bytes::ByteBuf; /// Returns a valid extended BIP-32 derivation path from an Account (Principal + subaccount) pub fn derive_public_key( - ecdsa_public_key: &ECDSAPublicKey, + ecdsa_public_key: &PublicKeyOutput, derivation_path: Vec>, -) -> ECDSAPublicKey { +) -> Result { let ExtendedBip32DerivationOutput { derived_public_key, derived_chain_code, } = DerivationPath::new(derivation_path.into_iter().map(DerivationIndex).collect()) .public_key_derivation(&ecdsa_public_key.public_key, &ecdsa_public_key.chain_code) - .expect("bug: failed to derive an ECDSA public key from valid inputs"); - ECDSAPublicKey { - public_key: derived_public_key, - chain_code: derived_chain_code, - } + .map_err(format_error)?; + Ok(PublicKeyOutput { + public_key: ByteBuf::from(derived_public_key), + chain_code: ByteBuf::from(derived_chain_code), + }) } -pub async fn sign_with( +pub async fn sign_with_ecdsa( key_name: String, derivation_path: Vec>, message_hash: Vec, @@ -45,10 +45,10 @@ pub async fn sign_with( Ok(response.signature) } -pub async fn public_key_with( +pub async fn ecdsa_public_key( key_name: String, derivation_path: Vec>, -) -> Result { +) -> Result { let args = ecdsa::EcdsaPublicKeyArgument { canister_id: None, derivation_path, @@ -62,5 +62,8 @@ pub async fn public_key_with( .await .map_err(|err| format!("ecdsa_public_key failed {:?}", err))?; - Ok(response) + Ok(PublicKeyOutput { + public_key: ByteBuf::from(response.public_key), + chain_code: ByteBuf::from(response.chain_code), + }) } diff --git a/src/ic_cose_canister/src/lib.rs b/src/ic_cose_canister/src/lib.rs index 8bcc14e..a2ea9a7 100644 --- a/src/ic_cose_canister/src/lib.rs +++ b/src/ic_cose_canister/src/lib.rs @@ -11,6 +11,7 @@ mod api_init; mod api_namespace; mod api_setting; mod ecdsa; +mod schnorr; mod store; use api_init::ChainArgs; diff --git a/src/ic_cose_canister/src/schnorr.rs b/src/ic_cose_canister/src/schnorr.rs new file mode 100644 index 0000000..009f4ee --- /dev/null +++ b/src/ic_cose_canister/src/schnorr.rs @@ -0,0 +1,131 @@ +use candid::{CandidType, Principal}; +use ic_cose_types::{ + format_error, + types::{PublicKeyOutput, SchnorrAlgorithm}, +}; +use ic_crypto_extended_bip32::{DerivationIndex, DerivationPath, ExtendedBip32DerivationOutput}; +use serde::{Deserialize, Serialize}; +use serde_bytes::ByteBuf; + +const MAX_SIGN_WITH_SCHNORR_FEE: u128 = 26_153_846_153; + +pub fn derive_schnorr_public_key( + alg: SchnorrAlgorithm, + public_key: &PublicKeyOutput, + derivation_path: Vec>, +) -> Result { + match alg { + SchnorrAlgorithm::Bip340Secp256k1 => { + let ExtendedBip32DerivationOutput { + derived_public_key, + derived_chain_code, + } = DerivationPath::new(derivation_path.into_iter().map(DerivationIndex).collect()) + .public_key_derivation(&public_key.public_key, &public_key.chain_code) + .map_err(format_error)?; + Ok(PublicKeyOutput { + public_key: ByteBuf::from(derived_public_key), + chain_code: ByteBuf::from(derived_chain_code), + }) + } + + SchnorrAlgorithm::Ed25519 => { + let path = ic_crypto_ed25519::DerivationPath::new( + derivation_path + .into_iter() + .map(ic_crypto_ed25519::DerivationIndex) + .collect(), + ); + + let chain_code: [u8; 32] = public_key + .chain_code + .to_vec() + .try_into() + .map_err(format_error)?; + let pk = ic_crypto_ed25519::PublicKey::deserialize_raw(&public_key.public_key) + .map_err(format_error)?; + let (derived_public_key, derived_chain_code) = + pk.derive_subkey_with_chain_code(&path, &chain_code); + + Ok(PublicKeyOutput { + public_key: ByteBuf::from(derived_public_key.serialize_raw()), + chain_code: ByteBuf::from(derived_chain_code), + }) + } + } +} + +#[derive(CandidType, Deserialize, Serialize, Debug)] +pub struct SignWithSchnorrArgs { + pub message: Vec, + pub derivation_path: Vec>, + pub key_id: SchnorrKeyId, +} + +#[derive(CandidType, Deserialize, Serialize, Debug)] +pub struct SignWithSchnorrResult { + pub signature: Vec, +} + +pub async fn sign_with_schnorr( + key_name: String, + alg: SchnorrAlgorithm, + derivation_path: Vec>, + message: Vec, +) -> Result, String> { + let args = SignWithSchnorrArgs { + message, + derivation_path, + key_id: SchnorrKeyId { + algorithm: alg, + name: key_name, + }, + }; + + let (res,): (SignWithSchnorrResult,) = ic_cdk::api::call::call_with_payment128( + Principal::management_canister(), + "sign_with_schnorr", + (args,), + MAX_SIGN_WITH_SCHNORR_FEE, + ) + .await + .map_err(|err| format!("sign_with_ecdsa failed {:?}", err))?; + + Ok(res.signature) +} + +#[derive(CandidType, Deserialize, Serialize, Debug)] +pub struct SchnorrPublicKeyArgs { + pub canister_id: Option, + pub derivation_path: Vec>, + pub key_id: SchnorrKeyId, +} + +#[derive(CandidType, Deserialize, Serialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct SchnorrKeyId { + algorithm: SchnorrAlgorithm, + name: String, +} + +pub async fn schnorr_public_key( + key_name: String, + alg: SchnorrAlgorithm, + derivation_path: Vec>, +) -> Result { + let args = SchnorrPublicKeyArgs { + canister_id: None, + derivation_path, + key_id: SchnorrKeyId { + algorithm: alg, + name: key_name, + }, + }; + + let (res,): (PublicKeyOutput,) = ic_cdk::call( + Principal::management_canister(), + "schnorr_public_key", + (args,), + ) + .await + .map_err(|err| format!("schnorr_public_key failed {:?}", err))?; + Ok(res) +} diff --git a/src/ic_cose_canister/src/store.rs b/src/ic_cose_canister/src/store.rs index 0461fe7..474c07b 100644 --- a/src/ic_cose_canister/src/store.rs +++ b/src/ic_cose_canister/src/store.rs @@ -4,11 +4,11 @@ use ic_cose_types::{ cose::{ cwt::{ClaimsSet, Timestamp, SCOPE_NAME}, encrypt0::try_decode_encrypt0, - format_error, sha256, sha3_256_n, + format_error, sha3_256_n, sign1::{cose_sign1, ES256K}, CborSerializable, }, - types::{namespace::*, setting::*, state::StateInfo, PublicKeyOutput}, + types::{namespace::*, setting::*, state::StateInfo, PublicKeyOutput, SchnorrAlgorithm}, ByteN, }; use ic_stable_structures::{ @@ -26,8 +26,9 @@ use std::{ }; use crate::{ - ecdsa::{derive_public_key, public_key_with, sign_with, ECDSAPublicKey}, + ecdsa::{derive_public_key, ecdsa_public_key, sign_with_ecdsa}, rand_bytes, + schnorr::{derive_schnorr_public_key, schnorr_public_key, sign_with_schnorr}, }; type Memory = VirtualMemory; @@ -36,8 +37,10 @@ type Memory = VirtualMemory; pub struct State { pub name: String, pub ecdsa_key_name: String, - pub ecdsa_public_key: Option, + pub ecdsa_public_key: Option, pub schnorr_key_name: String, + pub schnorr_ed25519_public_key: Option, + pub schnorr_secp256k1_public_key: Option, pub vetkd_key_name: String, pub managers: BTreeSet, // managers can read and write namespaces, not settings // auditors can read and list namespaces and settings info even if it is private @@ -350,24 +353,32 @@ pub mod state { } pub async fn init_ecdsa_public_key() { - let ecdsa_key_name = with(|r| { - if r.ecdsa_public_key.is_none() { - Some(r.ecdsa_key_name.clone()) - } else { - None - } - }); + let (ecdsa_key_name, schnorr_key_name) = + with(|r| (r.ecdsa_key_name.clone(), r.schnorr_key_name.clone())); - if let Some(ecdsa_key_name) = ecdsa_key_name { - let ecdsa_public_key = public_key_with(ecdsa_key_name, vec![]) + let ecdsa_public_key = ecdsa_public_key(ecdsa_key_name, vec![]) + .await + .unwrap_or_else(|err| { + ic_cdk::trap(&format!("failed to retrieve ECDSA public key: {err}")) + }); + let schnorr_ed25519_public_key = + schnorr_public_key(schnorr_key_name.clone(), SchnorrAlgorithm::Ed25519, vec![]) .await .unwrap_or_else(|err| { ic_cdk::trap(&format!("failed to retrieve ECDSA public key: {err}")) }); - with_mut(|r| { - r.ecdsa_public_key = Some(ecdsa_public_key); - }); - } + let schnorr_secp256k1_public_key = + schnorr_public_key(schnorr_key_name, SchnorrAlgorithm::Bip340Secp256k1, vec![]) + .await + .unwrap_or_else(|err| { + ic_cdk::trap(&format!("failed to retrieve ECDSA public key: {err}")) + }); + + with_mut(|r| { + r.ecdsa_public_key = Some(ecdsa_public_key); + r.schnorr_ed25519_public_key = Some(schnorr_ed25519_public_key); + r.schnorr_secp256k1_public_key = Some(schnorr_secp256k1_public_key); + }); } pub fn load() { @@ -412,6 +423,8 @@ pub mod state { } pub mod ns { + use ic_cose_types::cose::iana::Algorithm::EdDSA; + use super::*; pub fn namespace_count() -> u64 { @@ -459,16 +472,12 @@ pub mod ns { path.push(namespace.to_bytes().to_vec()); path.push(ns.iv.to_vec()); path.extend(derivation_path.into_iter().map(|b| b.into_vec())); - let derived_pk = derive_public_key(pk, path); - Ok(PublicKeyOutput { - public_key: ByteBuf::from(derived_pk.public_key), - chain_code: ByteBuf::from(derived_pk.chain_code), - }) + derive_public_key(pk, path) }) }) } - pub async fn ecdsa_sign( + pub async fn ecdsa_sign_with( caller: &Principal, namespace: String, derivation_path: Vec, @@ -487,13 +496,70 @@ pub mod ns { path.push(namespace.to_bytes().to_vec()); path.push(iv); path.extend(derivation_path.into_iter().map(|b| b.into_vec())); - let sig = sign_with(key_name, path, message.into_vec()).await?; + let sig = sign_with_ecdsa(key_name, path, message.into_vec()).await?; + Ok(ByteBuf::from(sig)) + } + + pub fn schnorr_public_key( + caller: &Principal, + alg: SchnorrAlgorithm, + namespace: String, + derivation_path: Vec, + ) -> Result { + with(&namespace, |ns| { + if !ns.can_read_namespace(caller) { + Err("no permission".to_string())?; + } + + state::with(|s| { + let pk = match alg { + SchnorrAlgorithm::Bip340Secp256k1 => s + .schnorr_secp256k1_public_key + .as_ref() + .ok_or("no schnorr secp256k1 public key")?, + SchnorrAlgorithm::Ed25519 => s + .schnorr_ed25519_public_key + .as_ref() + .ok_or("no schnorr ed25519 public key")?, + }; + let mut path: Vec> = Vec::with_capacity(derivation_path.len() + 3); + path.push(b"Schnorr_Signing".to_vec()); + path.push(namespace.to_bytes().to_vec()); + path.push(ns.iv.to_vec()); + path.extend(derivation_path.into_iter().map(|b| b.into_vec())); + derive_schnorr_public_key(alg, pk, path) + }) + }) + } + + pub async fn schnorr_sign_with( + caller: &Principal, + alg: SchnorrAlgorithm, + namespace: String, + derivation_path: Vec, + message: ByteBuf, + ) -> Result { + let iv = with(&namespace, |ns| { + if !ns.has_ns_signing_permission(caller) { + Err("no permission".to_string())?; + } + Ok(ns.iv.to_vec()) + })?; + + let key_name = state::with(|s| s.ecdsa_key_name.clone()); + let mut path: Vec> = Vec::with_capacity(derivation_path.len() + 3); + path.push(b"Schnorr_Signing".to_vec()); + path.push(namespace.to_bytes().to_vec()); + path.push(iv); + path.extend(derivation_path.into_iter().map(|b| b.into_vec())); + let sig = sign_with_schnorr(key_name, alg, path, message.into_vec()).await?; Ok(ByteBuf::from(sig)) } const CWT_EXPIRATION_SECONDS: i64 = 3600; - pub async fn ecdsa_sign_identity( + pub async fn sign_identity( caller: &Principal, + algorithm: SchnorrAlgorithm, namespace: String, audience: String, now_ms: u64, @@ -535,10 +601,13 @@ pub mod ns { rest: vec![(SCOPE_NAME.clone(), permission.into())], }; let payload = claims.to_vec().map_err(format_error)?; - let mut sign1 = cose_sign1(payload, ES256K, None)?; + let alg = match algorithm { + SchnorrAlgorithm::Ed25519 => EdDSA, + SchnorrAlgorithm::Bip340Secp256k1 => ES256K, + }; + let mut sign1 = cose_sign1(payload, alg, None)?; let tbs_data = sign1.tbs_data(caller.as_slice()); - let message_hash = sha256(&tbs_data); - let sig = sign_with(key_name, vec![], message_hash.to_vec()).await?; + let sig = sign_with_schnorr(key_name, algorithm, vec![], tbs_data).await?; sign1.signature = sig; let token = sign1.to_vec().map_err(format_error)?; Ok(ByteBuf::from(token)) @@ -557,7 +626,7 @@ pub mod ns { spk.0.to_bytes().to_vec(), ]; let message_hash = sha3_256_n([iv, partial_key]); - let sig = sign_with(key_name, derivation_path, message_hash.to_vec()).await?; + let sig = sign_with_ecdsa(key_name, derivation_path, message_hash.to_vec()).await?; Ok(sha3_256_n([&sig, partial_key])) } diff --git a/src/ic_cose_types/src/cose/ecdsa.rs b/src/ic_cose_types/src/cose/ecdsa.rs deleted file mode 100644 index c49f537..0000000 --- a/src/ic_cose_types/src/cose/ecdsa.rs +++ /dev/null @@ -1,22 +0,0 @@ -use super::{format_error, sha256}; - -pub use k256::ecdsa::{ - signature::hazmat::{PrehashSigner, PrehashVerifier}, - Signature, SigningKey, VerifyingKey, -}; - -pub fn secp256k1_verify_any( - public_keys: &[VerifyingKey], - message: &[u8], - signature: &[u8], -) -> Result<(), String> { - let sig = Signature::try_from(signature).map_err(format_error)?; - let digest = sha256(message); - match public_keys - .iter() - .any(|key| key.verify_prehash(&digest, &sig).is_ok()) - { - true => Ok(()), - false => Err("secp256k1 signature verification failed".to_string()), - } -} diff --git a/src/ic_cose_types/src/cose/ed25519.rs b/src/ic_cose_types/src/cose/ed25519.rs index 22f23f7..fdf75b8 100644 --- a/src/ic_cose_types/src/cose/ed25519.rs +++ b/src/ic_cose_types/src/cose/ed25519.rs @@ -2,6 +2,20 @@ use super::format_error; pub use ed25519_dalek::{Signature, SigningKey, VerifyingKey}; +pub fn ed25519_verify( + public_key: &[u8; 32], + message: &[u8], + signature: &[u8], +) -> Result<(), String> { + let key = VerifyingKey::from_bytes(public_key).map_err(format_error)?; + let sig = Signature::from_slice(signature).map_err(format_error)?; + + match key.verify_strict(message, &sig).is_ok() { + true => Ok(()), + false => Err("ed25519 signature verification failed".to_string()), + } +} + pub fn ed25519_verify_any( public_keys: &[VerifyingKey], message: &[u8], diff --git a/src/ic_cose_types/src/cose/k256.rs b/src/ic_cose_types/src/cose/k256.rs new file mode 100644 index 0000000..90eab60 --- /dev/null +++ b/src/ic_cose_types/src/cose/k256.rs @@ -0,0 +1,49 @@ +use super::{format_error, sha256}; + +use k256::ecdsa::signature::hazmat::PrehashVerifier; +// use k256::schnorr::signature::Verifier; + +pub use k256::{ecdsa, schnorr}; + +pub fn secp256k1_verify(public_key: &[u8], message: &[u8], signature: &[u8]) -> Result<(), String> { + let key = ecdsa::VerifyingKey::from_sec1_bytes(public_key).map_err(format_error)?; + let sig = ecdsa::Signature::try_from(signature).map_err(format_error)?; + let digest = sha256(message); + match key.verify_prehash(&digest, &sig).is_ok() { + true => Ok(()), + false => Err("secp256k1 signature verification failed".to_string()), + } +} + +pub fn secp256k1_verify_any( + public_keys: &[ecdsa::VerifyingKey], + message: &[u8], + signature: &[u8], +) -> Result<(), String> { + let sig = ecdsa::Signature::try_from(signature).map_err(format_error)?; + let digest = sha256(message); + match public_keys + .iter() + .any(|key| key.verify_prehash(&digest, &sig).is_ok()) + { + true => Ok(()), + false => Err("secp256k1 signature verification failed".to_string()), + } +} + +// wait for k256@0.14.0 +// pub fn schnorr_secp256k1_verify_any( +// public_keys: &[schnorr::VerifyingKey], +// message: &[u8], +// signature: &[u8], +// ) -> Result<(), String> { +// let sig = schnorr::Signature::try_from(signature).map_err(format_error)?; +// let digest = sha256(message); +// match public_keys +// .iter() +// .any(|key| key.verify_raw(&digest, &sig).is_ok()) +// { +// true => Ok(()), +// false => Err("schnorr secp256k1 signature verification failed".to_string()), +// } +// } diff --git a/src/ic_cose_types/src/cose/mod.rs b/src/ic_cose_types/src/cose/mod.rs index d7391bb..1795f7a 100644 --- a/src/ic_cose_types/src/cose/mod.rs +++ b/src/ic_cose_types/src/cose/mod.rs @@ -5,9 +5,9 @@ use sha3::{Digest, Sha3_256}; pub mod aes; pub mod cwt; pub mod ecdh; -pub mod ecdsa; pub mod ed25519; pub mod encrypt0; +pub mod k256; pub mod sign1; pub use coset::{iana, CborSerializable, CoseKey}; diff --git a/src/ic_cose_types/src/cose/sign1.rs b/src/ic_cose_types/src/cose/sign1.rs index 26f31ac..349f325 100644 --- a/src/ic_cose_types/src/cose/sign1.rs +++ b/src/ic_cose_types/src/cose/sign1.rs @@ -1,6 +1,6 @@ use coset::{iana, Algorithm, CborSerializable, CoseSign1, CoseSign1Builder, HeaderBuilder}; -use super::{ecdsa, ed25519}; +use super::{ed25519, k256}; pub use iana::Algorithm::{EdDSA, ES256K}; const ALG_ED25519: Algorithm = Algorithm::Assigned(EdDSA); @@ -26,7 +26,7 @@ pub fn cose_sign1( pub fn cose_sign1_from( sign1_bytes: &[u8], aad: &[u8], - secp256k1_pub_keys: &[ecdsa::VerifyingKey], + secp256k1_pub_keys: &[k256::ecdsa::VerifyingKey], ed25519_pub_keys: &[ed25519::VerifyingKey], ) -> Result { let cs1 = CoseSign1::from_slice(sign1_bytes) @@ -34,19 +34,9 @@ pub fn cose_sign1_from( match &cs1.protected.header.alg { Some(ALG_SECP256K1) if !secp256k1_pub_keys.is_empty() => { - // let secp256k1_pub_keys: Vec = secp256k1_pub_keys - // .iter() - // .map(|key| ecdsa::VerifyingKey::from_sec1_bytes(key).map_err(format_error)) - // .collect::>()?; - - ecdsa::secp256k1_verify_any(secp256k1_pub_keys, &cs1.tbs_data(aad), &cs1.signature)?; + k256::secp256k1_verify_any(secp256k1_pub_keys, &cs1.tbs_data(aad), &cs1.signature)?; } Some(ALG_ED25519) if !ed25519_pub_keys.is_empty() => { - // let ed25519_pub_keys: Vec = ed25519_pub_keys - // .iter() - // .map(|key| ed25519::VerifyingKey::from_bytes(key).map_err(format_error)) - // .collect::>()?; - ed25519::ed25519_verify_any(ed25519_pub_keys, &cs1.tbs_data(aad), &cs1.signature)?; } alg => { diff --git a/src/ic_cose_types/src/lib.rs b/src/ic_cose_types/src/lib.rs index b099947..9143b40 100644 --- a/src/ic_cose_types/src/lib.rs +++ b/src/ic_cose_types/src/lib.rs @@ -11,6 +11,7 @@ pub mod cose; pub mod types; pub use bytes::*; +pub use cose::format_error; pub static ANONYMOUS: Principal = Principal::anonymous(); pub const MILLISECONDS: u64 = 1_000_000u64; diff --git a/src/ic_cose_types/src/types/mod.rs b/src/ic_cose_types/src/types/mod.rs index 742d0e2..29fcc36 100644 --- a/src/ic_cose_types/src/types/mod.rs +++ b/src/ic_cose_types/src/types/mod.rs @@ -10,7 +10,16 @@ pub mod state; use crate::{bytes::ByteN, validate_key}; // should update to ICRC3Map -pub type MapValue = BTreeMap; +pub type MapValue = + BTreeMap; + +#[derive(CandidType, Deserialize, Serialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub enum SchnorrAlgorithm { + #[serde(rename = "bip340secp256k1")] + Bip340Secp256k1, + #[serde(rename = "ed25519")] + Ed25519, +} #[derive(CandidType, Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] pub struct PublicKeyInput {