Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.lock

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

3 changes: 3 additions & 0 deletions rs/tests/crypto/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,20 @@ 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",
"@crate_index//:k256",
"@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",
]
Expand Down
3 changes: 3 additions & 0 deletions rs/tests/crypto/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand All @@ -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 }

Expand Down
160 changes: 157 additions & 3 deletions rs/tests/crypto/ingress_verification_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,19 +65,21 @@ enum GenericIdentityType<'a> {
EcdsaSecp256k1,
EcdsaSecp256r1,
Canister(&'a UniversalCanister<'a>),
// TODO webauthn RSA
// TODO webauthn EC
WebAuthnEcdsaSecp256r1,
WebAuthnRsaPkcs1,
}

impl<'a> GenericIdentityType<'a> {
fn random_incl_canister<R: Rng + CryptoRng>(
canister: &'a UniversalCanister<'a>,
rng: &mut R,
) -> Self {
match rng.r#gen::<usize>() % 4 {
match rng.r#gen::<usize>() % 6 {
0 => Self::EcdsaSecp256k1,
1 => Self::EcdsaSecp256r1,
2 => Self::Canister(canister),
3 => Self::WebAuthnEcdsaSecp256r1,
4 => Self::WebAuthnRsaPkcs1,
_ => Self::Ed25519,
}
}
Expand All @@ -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)]
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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`,
Expand Down Expand Up @@ -1140,3 +1158,139 @@ fn resign_certificate_with_random_signature<R: Rng + CryptoRng>(
certificate.serialize(&mut serializer).unwrap();
serializer.into_inner()
}

fn wrap_cose_key_in_der_spki(cose: &serde_cbor::Value) -> Vec<u8> {
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<u8> {
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<u8> {
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<F: FnOnce(&[u8]) -> Vec<u8>>(msg: &[u8], sign_fn: F) -> Vec<u8> {
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<u8> {
let sign_fn = |to_sign: &[u8]| -> Vec<u8> { 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<u8> {
let sign_fn = |to_sign: &[u8]| -> Vec<u8> {
let signing_key = rsa::pkcs1v15::SigningKey::<rsa::sha2::Sha256>::new(sk.clone());
use rsa::signature::{SignatureEncoding, Signer};
signing_key.sign(to_sign).to_vec()
};
webauthn_sign_message(msg, sign_fn)
}
Loading