diff --git a/Cargo.lock b/Cargo.lock index 60426a0827ae..fffde6e9e457 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16058,6 +16058,7 @@ name = "ic_crypto_system_tests" version = "0.9.0" dependencies = [ "anyhow", + "base64 0.13.1", "candid", "ic-agent 0.40.1", "ic-certification 3.0.3", @@ -16079,9 +16080,11 @@ dependencies = [ "p256", "rand 0.8.5", "reqwest 0.12.24", + "rsa", "serde", "serde_bytes", "serde_cbor", + "serde_json", "simple_asn1", "slog", "tokio", diff --git a/rs/tests/crypto/BUILD.bazel b/rs/tests/crypto/BUILD.bazel index d2840149fae1..dbefb52ec490 100644 --- a/rs/tests/crypto/BUILD.bazel +++ b/rs/tests/crypto/BUILD.bazel @@ -24,6 +24,7 @@ DEPENDENCIES = [ "//rs/types/types", "//rs/universal_canister/lib", "@crate_index//:anyhow", + "@crate_index//:base64", "@crate_index//:candid", "@crate_index//:ic-agent", "@crate_index//:ic-certification", @@ -31,10 +32,12 @@ DEPENDENCIES = [ "@crate_index//:p256", "@crate_index//:rand", "@crate_index//:reqwest", + "@crate_index//:rsa", "@crate_index//:simple_asn1", "@crate_index//:serde", "@crate_index//:serde_bytes", "@crate_index//:serde_cbor", + "@crate_index//:serde_json", "@crate_index//:slog", "@crate_index//:tokio", ] diff --git a/rs/tests/crypto/Cargo.toml b/rs/tests/crypto/Cargo.toml index f2a8c5d42306..0b9cbe56fd2b 100644 --- a/rs/tests/crypto/Cargo.toml +++ b/rs/tests/crypto/Cargo.toml @@ -8,6 +8,7 @@ documentation.workspace = true [dependencies] anyhow = { workspace = true } +base64 = { workspace = true } candid = { workspace = true } ic-agent = { workspace = true } ic_consensus_system_test_utils = { path = "../consensus/utils" } @@ -29,10 +30,12 @@ k256 = { workspace = true } p256 = { workspace = true } rand = { workspace = true } reqwest = { workspace = true } +rsa = { workspace = true } simple_asn1 = { workspace = true } serde = { workspace = true } serde_bytes = { workspace = true } serde_cbor = { workspace = true } +serde_json = { workspace = true } slog = { workspace = true } tokio = { workspace = true } diff --git a/rs/tests/crypto/ingress_verification_test.rs b/rs/tests/crypto/ingress_verification_test.rs index 9d638c782eb7..2586992a1abc 100644 --- a/rs/tests/crypto/ingress_verification_test.rs +++ b/rs/tests/crypto/ingress_verification_test.rs @@ -65,8 +65,8 @@ enum GenericIdentityType<'a> { EcdsaSecp256k1, EcdsaSecp256r1, Canister(&'a UniversalCanister<'a>), - // TODO webauthn RSA - // TODO webauthn EC + WebAuthnEcdsaSecp256r1, + WebAuthnRsaPkcs1, } impl<'a> GenericIdentityType<'a> { @@ -74,10 +74,12 @@ impl<'a> GenericIdentityType<'a> { canister: &'a UniversalCanister<'a>, rng: &mut R, ) -> Self { - match rng.r#gen::() % 4 { + match rng.r#gen::() % 6 { 0 => Self::EcdsaSecp256k1, 1 => Self::EcdsaSecp256r1, 2 => Self::Canister(canister), + 3 => Self::WebAuthnEcdsaSecp256r1, + 4 => Self::WebAuthnRsaPkcs1, _ => Self::Ed25519, } } @@ -89,6 +91,8 @@ enum GenericIdentityInner<'a> { P256(ic_secp256r1::PrivateKey), Ed25519(ic_ed25519::PrivateKey), Canister(CanisterSigner<'a>), + WebAuthnEcdsaSecp256r1(ic_secp256r1::PrivateKey), + WebAuthnRsaPkcs1(rsa::RsaPrivateKey), } #[derive(Clone)] @@ -122,6 +126,16 @@ impl<'a> GenericIdentity<'a> { let pk = signer.public_key_der(); (GenericIdentityInner::Canister(signer), pk) } + GenericIdentityType::WebAuthnEcdsaSecp256r1 => { + let sk = ic_secp256r1::PrivateKey::generate_using_rng(rng); + let pk = webauthn_cose_wrap_ecdsa_secp256r1_key(&sk.public_key()); + (GenericIdentityInner::WebAuthnEcdsaSecp256r1(sk), pk) + } + GenericIdentityType::WebAuthnRsaPkcs1 => { + let sk = rsa::RsaPrivateKey::new(rng, 2048).expect("RSA keygen failed"); + let pk = webauthn_cose_wrap_rsa_pkcs1_key(&rsa::RsaPublicKey::from(&sk)); + (GenericIdentityInner::WebAuthnRsaPkcs1(sk), pk) + } }; let principal = Principal::self_authenticating(&public_key_der); @@ -177,6 +191,10 @@ impl<'a> GenericIdentity<'a> { GenericIdentityInner::Ed25519(sk) => sk.sign_message(bytes).to_vec(), GenericIdentityInner::K256(sk) => sk.sign_message_with_ecdsa(bytes).to_vec(), GenericIdentityInner::P256(sk) => sk.sign_message(bytes).to_vec(), + GenericIdentityInner::WebAuthnEcdsaSecp256r1(sk) => { + webauthn_sign_ecdsa_secp256r1(sk, bytes) + } + GenericIdentityInner::WebAuthnRsaPkcs1(sk) => webauthn_sign_rsa_pkcs1(sk, bytes), GenericIdentityInner::Canister(canister_signer) => { let sign_future = canister_signer.sign(bytes); // We are in a sync method and need to call the async `CanisterSigner::sign`, @@ -1140,3 +1158,139 @@ fn resign_certificate_with_random_signature( certificate.serialize(&mut serializer).unwrap(); serializer.into_inner() } + +fn wrap_cose_key_in_der_spki(cose: &serde_cbor::Value) -> Vec { + 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); + let pk_cose = serde_cbor::to_vec(cose).unwrap(); + subject_public_key_info_der(webauthn_key_oid, &pk_cose).unwrap() +} + +fn webauthn_cose_wrap_rsa_pkcs1_key(pk: &rsa::RsaPublicKey) -> Vec { + use rsa::traits::PublicKeyParts; + + let n = pk.n(); + let e = pk.e(); + + let mut map = std::collections::BTreeMap::new(); + + use serde_cbor::Value; + + /* + Reference + + - RFC 8152 "CBOR Object Signing and Encryption (COSE)" + + - RFC 8230 "Using RSA Algorithms with CBOR Object Signing and Encryption (COSE) Messages" + + - RFC 8812 "CBOR Object Signing and Encryption (COSE) and JSON + Object Signing and Encryption (JOSE) Registrations for Web + Authentication (WebAuthn) Algorithms" + */ + const COSE_PARAM_KTY: serde_cbor::Value = serde_cbor::Value::Integer(1); + const COSE_PARAM_KTY_RSA: serde_cbor::Value = serde_cbor::Value::Integer(3); + + const COSE_PARAM_ALG: serde_cbor::Value = serde_cbor::Value::Integer(3); + const COSE_PARAM_ALG_RS256: serde_cbor::Value = serde_cbor::Value::Integer(-257); + + const COSE_PARAM_RSA_N: serde_cbor::Value = serde_cbor::Value::Integer(-1); + const COSE_PARAM_RSA_E: serde_cbor::Value = serde_cbor::Value::Integer(-2); + + map.insert(COSE_PARAM_KTY, COSE_PARAM_KTY_RSA); + map.insert(COSE_PARAM_ALG, COSE_PARAM_ALG_RS256); + map.insert(COSE_PARAM_RSA_E, Value::Bytes(e.to_bytes_be())); + map.insert(COSE_PARAM_RSA_N, Value::Bytes(n.to_bytes_be())); + + wrap_cose_key_in_der_spki(&Value::Map(map)) +} + +fn webauthn_cose_wrap_ecdsa_secp256r1_key(pk: &ic_secp256r1::PublicKey) -> Vec { + let sec1 = pk.serialize_sec1(false); + + 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 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())); + + wrap_cose_key_in_der_spki(&Value::Map(map)) +} + +fn webauthn_sign_message Vec>(msg: &[u8], sign_fn: F) -> Vec { + use serde::Serialize; + + #[derive(Debug, Serialize)] + struct ClientData { + r#type: String, + challenge: String, + origin: String, + } + + let client_data = ClientData { + r#type: "webauthn.get".to_string(), + challenge: base64::encode_config(msg, base64::URL_SAFE_NO_PAD), + origin: "ic-ingress-verification-test".to_string(), + }; + + let authenticator_data = Blob(b"arbitrary".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(sign_fn(&signed_message)); + let sig = ic_types::messages::WebAuthnSignature::new( + authenticator_data, + Blob(client_data_json), + signature, + ); + + // serialize to self-describing CBOR + let mut serializer = serde_cbor::Serializer::new(Vec::new()); + serializer.self_describe().unwrap(); + sig.serialize(&mut serializer).unwrap(); + serializer.into_inner() +} + +fn webauthn_sign_ecdsa_secp256r1(sk: &ic_secp256r1::PrivateKey, msg: &[u8]) -> Vec { + let sign_fn = |to_sign: &[u8]| -> Vec { sk.sign_message_with_der_encoded_sig(to_sign) }; + webauthn_sign_message(msg, sign_fn) +} + +fn webauthn_sign_rsa_pkcs1(sk: &rsa::RsaPrivateKey, msg: &[u8]) -> Vec { + let sign_fn = |to_sign: &[u8]| -> Vec { + let signing_key = rsa::pkcs1v15::SigningKey::::new(sk.clone()); + use rsa::signature::{SignatureEncoding, Signer}; + signing_key.sign(to_sign).to_vec() + }; + webauthn_sign_message(msg, sign_fn) +}