diff --git a/Cargo.lock b/Cargo.lock index f555c611c9de..82577a3c1388 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15511,6 +15511,7 @@ dependencies = [ "base64 0.13.1", "hex", "ic-crypto-interfaces-sig-verification", + "ic-crypto-internal-basic-sig-der-utils", "ic-crypto-sha2", "ic-crypto-standalone-sig-verifier", "ic-crypto-temp-crypto", @@ -15518,10 +15519,15 @@ dependencies = [ "ic-crypto-test-utils-root-of-trust", "ic-crypto-tree-hash", "ic-limits", + "ic-secp256r1", "ic-test-utilities-types", "ic-types", "mockall", "rand 0.8.5", + "serde", + "serde_cbor", + "serde_json", + "simple_asn1", "thiserror 2.0.17", ] diff --git a/packages/ic-secp256r1/src/lib.rs b/packages/ic-secp256r1/src/lib.rs index 457104b48a16..41d637e74962 100644 --- a/packages/ic-secp256r1/src/lib.rs +++ b/packages/ic-secp256r1/src/lib.rs @@ -412,6 +412,15 @@ impl PrivateKey { sig.to_bytes().into() } + /// Sign a message, using a DER encoded signature + /// + /// The message is hashed with SHA-256 + pub fn sign_message_with_der_encoded_sig(&self, message: &[u8]) -> Vec { + use p256::ecdsa::{Signature, signature::Signer}; + let sig: Signature = self.key.sign(message); + sig.to_der().to_bytes().to_vec() + } + /// Sign a message digest pub fn sign_digest(&self, digest: &[u8]) -> Option<[u8; 64]> { if digest.len() < 16 { diff --git a/rs/crypto/internal/crypto_lib/basic_sig/der_utils/BUILD.bazel b/rs/crypto/internal/crypto_lib/basic_sig/der_utils/BUILD.bazel index 948398648934..70330de2d688 100644 --- a/rs/crypto/internal/crypto_lib/basic_sig/der_utils/BUILD.bazel +++ b/rs/crypto/internal/crypto_lib/basic_sig/der_utils/BUILD.bazel @@ -9,6 +9,7 @@ rust_library( "//rs/canister_client/sender:__pkg__", "//rs/crypto:__subpackages__", "//rs/tests/crypto:__subpackages__", + "//rs/validator:__subpackages__", "//rs/validator/http_request_test_utils:__subpackages__", ], deps = [ diff --git a/rs/validator/BUILD.bazel b/rs/validator/BUILD.bazel index 370afa2432e3..3a805aec70e9 100644 --- a/rs/validator/BUILD.bazel +++ b/rs/validator/BUILD.bazel @@ -16,6 +16,8 @@ DEPENDENCIES = [ DEV_DEPENDENCIES = [ # Keep sorted. + "//packages/ic-secp256r1", + "//rs/crypto/internal/crypto_lib/basic_sig/der_utils", "//rs/crypto/temp_crypto", "//rs/crypto/test_utils/reproducible_rng", "//rs/crypto/test_utils/root_of_trust", @@ -24,6 +26,10 @@ DEV_DEPENDENCIES = [ "@crate_index//:base64", "@crate_index//:mockall", "@crate_index//:rand", + "@crate_index//:serde", + "@crate_index//:serde_cbor", + "@crate_index//:serde_json", + "@crate_index//:simple_asn1", ] rust_library( diff --git a/rs/validator/Cargo.toml b/rs/validator/Cargo.toml index c97aff350fd8..f041959233eb 100644 --- a/rs/validator/Cargo.toml +++ b/rs/validator/Cargo.toml @@ -22,9 +22,15 @@ thiserror = { workspace = true } assert_matches = { workspace = true } base64 = { workspace = true } hex = { workspace = true } +ic-crypto-internal-basic-sig-der-utils = { path = "../crypto/internal/crypto_lib/basic_sig/der_utils" } ic-crypto-test-utils-reproducible-rng = { path = "../crypto/test_utils/reproducible_rng" } ic-crypto-test-utils-root-of-trust = { path = "../crypto/test_utils/root_of_trust" } ic-crypto-temp-crypto = { path = "../crypto/temp_crypto" } +ic-secp256r1 = { path = "../../packages/ic-secp256r1" } +serde = { workspace = true } +serde_cbor = { workspace = true } +serde_json = { workspace = true } +simple_asn1 = { workspace = true } ic-test-utilities-types = { path = "../test_utilities/types" } mockall = { workspace = true } rand = { workspace = true } diff --git a/rs/validator/src/webauthn.rs b/rs/validator/src/webauthn.rs index 21647244ab23..37ae35da8d38 100644 --- a/rs/validator/src/webauthn.rs +++ b/rs/validator/src/webauthn.rs @@ -116,6 +116,111 @@ mod tests { ); } + #[test] + fn should_verify_newly_generated_webauthn_ecdsa_signature() { + let verifier = temp_crypto_component_with_fake_registry(node_test_id(0)); + + let (sig_bytes, pk_bytes, msg) = { + let sk = ic_secp256r1::PrivateKey::generate(); + + let msg = "webauthn signature generation test"; + + let sig_bytes = { + use serde::Serialize; + + #[derive(Debug, Serialize)] + struct ClientData { + r#type: String, + challenge: String, + origin: String, + } + + let client_data = ClientData { + r#type: "brunettes".to_string(), + challenge: base64::encode(msg), + origin: "https://localhost/".to_string(), + }; + + let authenticator_data = Blob(b"auth_data".to_vec()); + let client_data_json = serde_json::to_vec(&client_data).unwrap(); + + let signed_message = { + let mut sm = vec![]; + sm.extend_from_slice(&authenticator_data.0); + sm.extend_from_slice(&ic_crypto_sha2::Sha256::hash(&client_data_json)); + sm + }; + let signature = Blob(sk.sign_message_with_der_encoded_sig(&signed_message)); + let sig = WebAuthnSignature::new( + authenticator_data, + Blob(client_data_json), + signature, + ); + serde_cbor::to_vec(&sig).unwrap() + }; + + let pk_cose = { + let mut map = std::collections::BTreeMap::new(); + + use serde_cbor::Value; + + /* + See RFC 8152 ("CBOR Object Signing and Encryption (COSE)"), sections 8.1 + and 13.1 for these constants + */ + const COSE_PARAM_KTY: serde_cbor::Value = serde_cbor::Value::Integer(1); + const COSE_PARAM_KTY_EC2: serde_cbor::Value = serde_cbor::Value::Integer(2); + + const COSE_PARAM_ALG: serde_cbor::Value = serde_cbor::Value::Integer(3); + const COSE_PARAM_ALG_ES256: serde_cbor::Value = serde_cbor::Value::Integer(-7); + + const COSE_PARAM_EC2_CRV: serde_cbor::Value = serde_cbor::Value::Integer(-1); + const COSE_PARAM_EC2_CRV_P256: serde_cbor::Value = + serde_cbor::Value::Integer(1); + + const COSE_PARAM_EC2_X: serde_cbor::Value = serde_cbor::Value::Integer(-2); + const COSE_PARAM_EC2_Y: serde_cbor::Value = serde_cbor::Value::Integer(-3); + + let sec1 = sk.public_key().serialize_sec1(false); + let x = &sec1[1..33]; + let y = &sec1[33..]; + + map.insert(COSE_PARAM_KTY, COSE_PARAM_KTY_EC2); + map.insert(COSE_PARAM_EC2_CRV, COSE_PARAM_EC2_CRV_P256); + map.insert(COSE_PARAM_ALG, COSE_PARAM_ALG_ES256); + map.insert(COSE_PARAM_EC2_X, Value::Bytes(x.to_vec())); + map.insert(COSE_PARAM_EC2_Y, Value::Bytes(y.to_vec())); + + serde_cbor::to_vec(&Value::Map(map)).expect("cbor encoding failed") + }; + + let pk_der = { + use ic_crypto_internal_basic_sig_der_utils::subject_public_key_info_der; + use simple_asn1::oid; + // OID 1.3.6.1.4.1.56387.1.1 + // See https://internetcomputer.org/docs/current/references/ic-interface-spec#signatures + let webauthn_key_oid = oid!(1, 3, 6, 1, 4, 1, 56387, 1, 1); + subject_public_key_info_der(webauthn_key_oid, &pk_cose).unwrap() + }; + + (sig_bytes, pk_der, msg.as_bytes().to_vec()) + }; + + let sig = WebAuthnSignature::try_from(sig_bytes.as_slice()).unwrap(); + let pk = user_public_key_from_bytes(&pk_bytes).unwrap().0; + assert_eq!(pk.algorithm_id, AlgorithmId::EcdsaP256); + + let message = SignableMock { + domain: vec![], + signed_bytes_without_domain: msg.to_vec(), + }; + + assert_eq!( + validate_webauthn_sig(&verifier, &sig, &message, &pk), + Ok(()) + ); + } + #[test] fn should_return_error_on_valid_signature_but_wrong_message() { let verifier = temp_crypto_component_with_fake_registry(node_test_id(0));