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
1 change: 1 addition & 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/crypto/internal/crypto_lib/basic_sig/cose/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ rust_library(
version = "0.9.0",
deps = [
# Keep sorted.
"//packages/ic-ed25519",
"//packages/ic-secp256r1",
"//rs/crypto/internal/crypto_lib/basic_sig/rsa_pkcs1",
"//rs/types/types",
Expand All @@ -22,6 +23,7 @@ rust_test(
crate = ":cose",
deps = [
# Keep sorted.
"//packages/ic-ed25519",
"//packages/ic-secp256r1",
"//rs/crypto/internal/crypto_lib/basic_sig/rsa_pkcs1",
"//rs/crypto/internal/test_vectors",
Expand All @@ -38,6 +40,7 @@ rust_test_suite(
deps = [
# Keep sorted.
":cose",
"//packages/ic-ed25519",
"//packages/ic-secp256r1",
"//rs/crypto/internal/crypto_lib/basic_sig/rsa_pkcs1",
"//rs/crypto/internal/test_vectors",
Expand Down
1 change: 1 addition & 0 deletions rs/crypto/internal/crypto_lib/basic_sig/cose/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ description.workspace = true
documentation.workspace = true

[dependencies]
ic-ed25519 = { path = "../../../../../../packages/ic-ed25519" }
ic-secp256r1 = { path = "../../../../../../packages/ic-secp256r1" }
ic-crypto-internal-basic-sig-rsa-pkcs1 = { path = "../rsa_pkcs1" }
ic-types = { path = "../../../../../types/types" }
Expand Down
52 changes: 52 additions & 0 deletions rs/crypto/internal/crypto_lib/basic_sig/cose/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type CborMap = std::collections::BTreeMap<serde_cbor::Value, serde_cbor::Value>;
enum CosePublicKey {
EcdsaP256Sha256(Vec<u8>),
RsaPkcs1v15Sha256(Vec<u8>),
Ed25519(Vec<u8>),
}

// see https://tools.ietf.org/html/rfc8152 section 8.1
Expand All @@ -37,6 +38,15 @@ const COSE_KTY_RSA: serde_cbor::Value = serde_cbor::Value::Integer(3);
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);

// see https://datatracker.ietf.org/doc/html/rfc8152#section-13.2
const COSE_KTY_OKP: serde_cbor::Value = serde_cbor::Value::Integer(1);
const COSE_PARAM_OKP_CRV: serde_cbor::Value = serde_cbor::Value::Integer(-1);
const COSE_PARAM_OKP_X: serde_cbor::Value = serde_cbor::Value::Integer(-2);

// see https://datatracker.ietf.org/doc/html/rfc8152#section-8.2
const COSE_ALG_EDDSA: serde_cbor::Value = serde_cbor::Value::Integer(-8);
const COSE_OKP_CRV_ED25519: serde_cbor::Value = serde_cbor::Value::Integer(6);

#[derive(Copy, Clone, Eq, PartialEq, Debug)]
/// An error that occurred while parsing the COSE key
enum CosePublicKeyParseError {
Expand Down Expand Up @@ -78,6 +88,8 @@ impl CosePublicKey {
Self::parse_ecdsa_p256(&fields)
} else if *kty == COSE_KTY_RSA && *alg == COSE_ALG_RS256 {
Self::parse_rsa_pkcs1_sha256(&fields)
} else if *kty == COSE_KTY_OKP && *alg == COSE_ALG_EDDSA {
Self::parse_eddsa_ed25519(&fields)
} else {
// Some other algorithm
Err(CosePublicKeyParseError::AlgorithmNotSupported)
Expand Down Expand Up @@ -150,6 +162,44 @@ impl CosePublicKey {
}
}

/// Parse a COSE EdDSA / Ed25519 key
fn parse_eddsa_ed25519(fields: &CborMap) -> Result<Self, CosePublicKeyParseError> {
Self::verify_key_ops(fields)?;

let crv =
fields
.get(&COSE_PARAM_OKP_CRV)
.ok_or(CosePublicKeyParseError::MalformedPublicKey(
AlgorithmId::Ed25519,
))?;

if *crv != COSE_OKP_CRV_ED25519 {
// EdDSA over a curve we don't support (e.g. Ed448)
return Err(CosePublicKeyParseError::AlgorithmNotSupported);
}

let x =
fields
.get(&COSE_PARAM_OKP_X)
.ok_or(CosePublicKeyParseError::MalformedPublicKey(
AlgorithmId::Ed25519,
))?;

match x {
serde_cbor::Value::Bytes(x) => {
let pk = ic_ed25519::PublicKey::deserialize_raw(x).map_err(|_| {
CosePublicKeyParseError::MalformedPublicKey(AlgorithmId::Ed25519)
})?;

let der = pk.serialize_rfc8410_der();
Ok(Self::Ed25519(der))
}
_ => Err(CosePublicKeyParseError::MalformedPublicKey(
AlgorithmId::Ed25519,
)),
}
}

fn parse_rsa_pkcs1_sha256(fields: &CborMap) -> Result<Self, CosePublicKeyParseError> {
Self::verify_key_ops(fields)?;

Expand Down Expand Up @@ -185,6 +235,7 @@ impl CosePublicKey {
match self {
Self::EcdsaP256Sha256(_) => AlgorithmId::EcdsaP256,
Self::RsaPkcs1v15Sha256(_) => AlgorithmId::RsaSha256,
Self::Ed25519(_) => AlgorithmId::Ed25519,
}
}

Expand All @@ -193,6 +244,7 @@ impl CosePublicKey {
match self {
Self::EcdsaP256Sha256(der) => der.to_vec(),
Self::RsaPkcs1v15Sha256(der) => der.to_vec(),
Self::Ed25519(der) => der.to_vec(),
}
}
}
Expand Down
81 changes: 81 additions & 0 deletions rs/crypto/internal/crypto_lib/basic_sig/cose/tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,84 @@ fn get_der_cose_verification_result(sig_der_hex: &str, pk_cose_hex: &str, msg_he
false
}
}

// Constructed COSE-encoded EdDSA / Ed25519 public key for the RFC 8032 TEST 1
// keypair (sk = 9d61...7f60, pk = d75a...511a). The CBOR map is:
// { 1: 1 (kty=OKP), 3: -8 (alg=EdDSA), -1: 6 (crv=Ed25519), -2: <pk> }
const ED25519_PK_COSE_HEX: &str =
"a4010103272006215820d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a";

// The RFC 8410 SPKI DER encoding of the same Ed25519 public key.
const ED25519_PK_RFC8410_DER_HEX: &str =
"302a300506032b6570032100d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a";

// Signature, with the RFC 8032 TEST 1 secret key, over a synthetic WebAuthn
// signed-bytes payload (authenticator_data || sha256(client_data_json)).
const ED25519_SIG_HEX: &str = "c75d28322702e0b762a5a6e2b2ebbe35575effa6df89132826ffa9e7ff7db3ab98017f1f162acfcffaca2e10fbec029b9578088607912200627742e82bd78b0d";
const ED25519_MSG_HEX: &str = "88e87051ccf36e3be572ef2b097e80abcb1cbc8332a6af5f9512661f3fece38e05000000005f335d5a02031b45335306e396d3ceb1a9a1802805367bc77216b80d4082ae28";

#[test]
fn should_correctly_parse_cose_encoded_ed25519_pk() {
let pk_cose = hex::decode(ED25519_PK_COSE_HEX).unwrap();
let (alg_id, pk_der) = parse_cose_public_key(&pk_cose).unwrap();
assert_eq!(alg_id, AlgorithmId::Ed25519);
assert_eq!(hex::encode(pk_der), ED25519_PK_RFC8410_DER_HEX);
}

#[test]
fn should_correctly_verify_signature_for_ed25519_cose_pk() {
let pk_cose = hex::decode(ED25519_PK_COSE_HEX).unwrap();
let (_alg_id, pk_der) = parse_cose_public_key(&pk_cose).unwrap();

let pk = ic_ed25519::PublicKey::deserialize_rfc8410_der(&pk_der)
.expect("Ed25519 public key parses from RFC 8410 DER");

let sig = hex::decode(ED25519_SIG_HEX).unwrap();
let msg = hex::decode(ED25519_MSG_HEX).unwrap();
pk.verify_signature(&msg, &sig)
.expect("signature verifies against parsed COSE public key");
}

#[test]
fn should_parse_nitrokey_3a_cose_encoded_ed25519_pk() {
// COSE key captured from a NitroKey 3A WebAuthn registration (II self-service test).
// Verifies that authenticators producing kty=OKP/alg=EdDSA/crv=Ed25519 are accepted.
let pk_cose = hex::decode(
"a40101032720062158205ae0e5d3bc439630f2a07660dc6d29c0129e545bc24380970722fcbf5c8f8b30",
)
.unwrap();
let (alg_id, _pk_der) = parse_cose_public_key(&pk_cose).unwrap();
assert_eq!(alg_id, AlgorithmId::Ed25519);
}

#[test]
fn should_reject_cose_encoded_eddsa_with_unsupported_curve() {
// Same as a valid Ed25519 key but crv = 7 (Ed448), which is not supported.
let pk_cose = hex::decode(
"a40101032720072158205ae0e5d3bc439630f2a07660dc6d29c0129e545bc24380970722fcbf5c8f8b30",
)
.unwrap();
let result = parse_cose_public_key(&pk_cose);
assert_eq!(
result,
Err(CryptoError::AlgorithmNotSupported {
algorithm: AlgorithmId::Unspecified,
reason: "Algorithm not supported in COSE parser".to_string(),
})
);
}

#[test]
fn should_reject_cose_encoded_ed25519_with_short_x() {
// x is only 16 bytes instead of the required 32.
// CBOR: a4 01 01 03 27 20 06 21 50 <16 bytes>
let pk_cose = hex::decode("a4010103272006215000112233445566778899aabbccddeeff").unwrap();
let result = parse_cose_public_key(&pk_cose);
assert!(matches!(
result,
Err(CryptoError::MalformedPublicKey {
algorithm: AlgorithmId::Ed25519,
..
})
));
}
2 changes: 2 additions & 0 deletions rs/crypto/standalone-sig-verifier/src/sign_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ pub enum KeyBytesContentType {
EcdsaSecp256k1PublicKeyDer,
RsaSha256PublicKeyDer,
EcdsaP256PublicKeyDerWrappedCose,
Ed25519PublicKeyDerWrappedCose,
RsaSha256PublicKeyDerWrappedCose,
IcCanisterSignatureAlgPublicKeyDer,
}

fn cose_key_bytes_content_type(alg_id: AlgorithmId) -> Option<KeyBytesContentType> {
match alg_id {
AlgorithmId::EcdsaP256 => Some(KeyBytesContentType::EcdsaP256PublicKeyDerWrappedCose),
AlgorithmId::Ed25519 => Some(KeyBytesContentType::Ed25519PublicKeyDerWrappedCose),
AlgorithmId::RsaSha256 => Some(KeyBytesContentType::RsaSha256PublicKeyDerWrappedCose),
_ => None,
}
Expand Down
18 changes: 18 additions & 0 deletions rs/crypto/tests/request_id_signatures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,24 @@ fn should_correctly_parse_der_encoded_openssl_ecdsa_p256_pk() {
assert_eq!(bytes_type, KeyBytesContentType::EcdsaP256PublicKeyDer);
}

#[test]
fn should_correctly_parse_cose_encoded_der_wrapped_ed25519_pk() {
// Ed25519 public key (RFC 8032 TEST 1) encoded as a COSE_Key
// {kty=OKP, alg=EdDSA, crv=Ed25519, x=pk} and DER-wrapped with the IC's
// SubjectPublicKeyInfo OID 1.3.6.1.4.1.56387.1.1. The COSE parser must
// recognize the OKP/EdDSA/Ed25519 combination and surface it via
// `Ed25519PublicKeyDerWrappedCose`.
const ED25519_PK_COSE_DER_WRAPPED_HEX: &str = "303b300c060a2b0601040183b8430101032b00a4010103272006215820d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a";

let pk_cose_der = hex::decode(ED25519_PK_COSE_DER_WRAPPED_HEX).unwrap();
let (pk, bytes_type) = user_public_key_from_bytes(&pk_cose_der).unwrap();
assert_eq!(pk.algorithm_id, AlgorithmId::Ed25519);
assert_eq!(
bytes_type,
KeyBytesContentType::Ed25519PublicKeyDerWrappedCose
);
}

#[test]
fn should_correctly_parse_cose_encoded_der_wrapped_ecdsa_p256_pk() {
for pk_cose_der_hex in &[
Expand Down
53 changes: 51 additions & 2 deletions rs/tests/crypto/ingress_verification_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,29 +76,32 @@ enum GenericIdentityType<'a> {
Canister(&'a UniversalCanister<'a>),
WebAuthnEcdsaSecp256r1,
WebAuthnRsaPkcs1,
WebAuthnEd25519,
}

impl<'a> GenericIdentityType<'a> {
fn random_incl_canister<R: Rng + CryptoRng>(
canister: &'a UniversalCanister<'a>,
rng: &mut R,
) -> Self {
match rng.r#gen::<usize>() % 6 {
match rng.r#gen::<usize>() % 7 {
0 => Self::EcdsaSecp256k1,
1 => Self::EcdsaSecp256r1,
2 => Self::Canister(canister),
3 => Self::WebAuthnEcdsaSecp256r1,
4 => Self::WebAuthnRsaPkcs1,
5 => Self::WebAuthnEd25519,
_ => Self::Ed25519,
}
}

fn random<R: Rng + CryptoRng>(rng: &mut R) -> Self {
match rng.r#gen::<usize>() % 5 {
match rng.r#gen::<usize>() % 6 {
0 => Self::EcdsaSecp256k1,
1 => Self::EcdsaSecp256r1,
2 => Self::WebAuthnEcdsaSecp256r1,
3 => Self::WebAuthnRsaPkcs1,
4 => Self::WebAuthnEd25519,
_ => Self::Ed25519,
}
}
Expand All @@ -112,6 +115,7 @@ enum GenericIdentityInner<'a> {
Canister(CanisterSigner<'a>),
WebAuthnEcdsaSecp256r1(ic_secp256r1::PrivateKey),
WebAuthnRsaPkcs1(rsa::RsaPrivateKey),
WebAuthnEd25519(ic_ed25519::PrivateKey),
}

#[derive(Clone)]
Expand Down Expand Up @@ -155,6 +159,11 @@ impl<'a> GenericIdentity<'a> {
let pk = webauthn_cose_wrap_rsa_pkcs1_key(&rsa::RsaPublicKey::from(&sk));
(GenericIdentityInner::WebAuthnRsaPkcs1(sk), pk)
}
GenericIdentityType::WebAuthnEd25519 => {
let sk = ic_ed25519::PrivateKey::generate_using_rng(rng);
let pk = webauthn_cose_wrap_ed25519_key(&sk.public_key());
(GenericIdentityInner::WebAuthnEd25519(sk), pk)
}
};

let principal = Principal::self_authenticating(&public_key_der);
Expand Down Expand Up @@ -214,6 +223,7 @@ impl<'a> GenericIdentity<'a> {
webauthn_sign_ecdsa_secp256r1(sk, bytes)
}
GenericIdentityInner::WebAuthnRsaPkcs1(sk) => webauthn_sign_rsa_pkcs1(sk, bytes),
GenericIdentityInner::WebAuthnEd25519(sk) => webauthn_sign_ed25519(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 @@ -2469,6 +2479,38 @@ fn webauthn_cose_wrap_ecdsa_secp256r1_key(pk: &ic_secp256r1::PublicKey) -> Vec<u
wrap_cose_key_in_der_spki(&Value::Map(map))
}

fn webauthn_cose_wrap_ed25519_key(pk: &ic_ed25519::PublicKey) -> Vec<u8> {
let mut map = std::collections::BTreeMap::new();

use serde_cbor::Value;

/*
See:
- RFC 8152 ("CBOR Object Signing and Encryption (COSE)"), section 13.2
for OKP key-type parameters.
- RFC 8812 ("CBOR Object Signing and Encryption (COSE) and JSON Object
Signing and Encryption (JOSE) Registrations for Web Authentication
(WebAuthn) Algorithms"), section 3.1 for the EdDSA algorithm code.
*/
const COSE_PARAM_KTY: serde_cbor::Value = serde_cbor::Value::Integer(1);
const COSE_PARAM_KTY_OKP: serde_cbor::Value = serde_cbor::Value::Integer(1);

const COSE_PARAM_ALG: serde_cbor::Value = serde_cbor::Value::Integer(3);
const COSE_PARAM_ALG_EDDSA: serde_cbor::Value = serde_cbor::Value::Integer(-8);

const COSE_PARAM_OKP_CRV: serde_cbor::Value = serde_cbor::Value::Integer(-1);
const COSE_PARAM_OKP_CRV_ED25519: serde_cbor::Value = serde_cbor::Value::Integer(6);

const COSE_PARAM_OKP_X: serde_cbor::Value = serde_cbor::Value::Integer(-2);

map.insert(COSE_PARAM_KTY, COSE_PARAM_KTY_OKP);
map.insert(COSE_PARAM_ALG, COSE_PARAM_ALG_EDDSA);
map.insert(COSE_PARAM_OKP_CRV, COSE_PARAM_OKP_CRV_ED25519);
map.insert(COSE_PARAM_OKP_X, Value::Bytes(pk.serialize_raw().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;

Expand Down Expand Up @@ -2521,3 +2563,10 @@ fn webauthn_sign_rsa_pkcs1(sk: &rsa::RsaPrivateKey, msg: &[u8]) -> Vec<u8> {
};
webauthn_sign_message(msg, sign_fn)
}

fn webauthn_sign_ed25519(sk: &ic_ed25519::PrivateKey, msg: &[u8]) -> Vec<u8> {
// WebAuthn EdDSA signatures are the raw 64-byte R || s encoding from
// RFC 8032 §5.1.6 — no DER wrapping (cf. WebAuthn §6.5.6).
let sign_fn = |to_sign: &[u8]| -> Vec<u8> { sk.sign_message(to_sign).to_vec() };
webauthn_sign_message(msg, sign_fn)
}
2 changes: 2 additions & 0 deletions rs/validator/src/ingress_validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -658,6 +658,7 @@ where

match pk_type {
KeyBytesContentType::EcdsaP256PublicKeyDerWrappedCose
| KeyBytesContentType::Ed25519PublicKeyDerWrappedCose
| KeyBytesContentType::RsaSha256PublicKeyDerWrappedCose => {
let webauthn_sig = WebAuthnSignature::try_from(signature.signature.as_slice())
.map_err(WebAuthnError)
Expand Down Expand Up @@ -800,6 +801,7 @@ where

match pk_type {
KeyBytesContentType::EcdsaP256PublicKeyDerWrappedCose
| KeyBytesContentType::Ed25519PublicKeyDerWrappedCose
| KeyBytesContentType::RsaSha256PublicKeyDerWrappedCose => {
let webauthn_sig = WebAuthnSignature::try_from(signature).map_err(WebAuthnError)?;
validate_webauthn_sig(validator, &webauthn_sig, delegation, &pk)
Expand Down
Loading
Loading