#![forbid(unsafe_code)]
use ring::signature::{UnparsedPublicKey, ECDSA_P256_SHA256_FIXED};
use crate::attestation::error::AttestationError;
#[derive(Debug, Clone)]
pub struct AttestationResponse {
pub attestation_elements: Vec<u8>,
pub signature: [u8; 64],
}
pub fn verify_dac_signed_elements(
elements: &[u8],
attestation_challenge: &[u8; 16],
dac_public_key: &[u8],
signature: &[u8; 64],
) -> Result<(), AttestationError> {
let mut tbs = Vec::with_capacity(elements.len() + attestation_challenge.len());
tbs.extend_from_slice(elements);
tbs.extend_from_slice(attestation_challenge);
let key = UnparsedPublicKey::new(&ECDSA_P256_SHA256_FIXED, dac_public_key);
key.verify(&tbs, signature)
.map_err(|_| AttestationError::BadResponseSignature)
}
pub fn verify_attestation_response(
response: &AttestationResponse,
attestation_challenge: &[u8; 16],
dac_public_key: &[u8],
) -> Result<(), AttestationError> {
verify_dac_signed_elements(
&response.attestation_elements,
attestation_challenge,
dac_public_key,
&response.signature,
)
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct AttestationElementsFields {
pub certification_declaration: Vec<u8>,
pub attestation_nonce: [u8; 32],
pub timestamp_epoch_seconds: u64,
}
pub fn extract_attestation_elements_fields(
tlv: &[u8],
) -> Result<AttestationElementsFields, AttestationError> {
use matter_codec::{ContainerKind, Element, Tag, TlvReader, Value};
let mut reader = TlvReader::new(tlv);
match reader
.next()
.map_err(|_| AttestationError::ResponseElementsMalformed)?
{
Some(Element::ContainerStart {
tag: Tag::Anonymous,
kind: ContainerKind::Structure,
}) => {}
_ => return Err(AttestationError::ResponseElementsMalformed),
}
let mut cd: Option<Vec<u8>> = None;
let mut nonce: Option<[u8; 32]> = None;
let mut ts: Option<u64> = None;
loop {
match reader
.next()
.map_err(|_| AttestationError::ResponseElementsMalformed)?
{
None => return Err(AttestationError::ResponseElementsMalformed),
Some(Element::ContainerEnd) => break,
Some(Element::Scalar {
tag: Tag::Context(1),
value: Value::Bytes(b),
}) => {
if cd.is_some() {
return Err(AttestationError::ResponseElementsMalformed);
}
cd = Some(b);
}
Some(Element::Scalar {
tag: Tag::Context(2),
value: Value::Bytes(b),
}) => {
if nonce.is_some() {
return Err(AttestationError::ResponseElementsMalformed);
}
let arr: [u8; 32] = b
.as_slice()
.try_into()
.map_err(|_| AttestationError::ResponseElementsMalformed)?;
nonce = Some(arr);
}
Some(Element::Scalar {
tag: Tag::Context(3),
value: Value::Uint(v),
}) => {
if ts.is_some() {
return Err(AttestationError::ResponseElementsMalformed);
}
ts = Some(v);
}
Some(_) => {}
}
}
Ok(AttestationElementsFields {
certification_declaration: cd.ok_or(AttestationError::ResponseElementsMalformed)?,
attestation_nonce: nonce.ok_or(AttestationError::ResponseElementsMalformed)?,
timestamp_epoch_seconds: ts.ok_or(AttestationError::ResponseElementsMalformed)?,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn response_struct_is_constructible() {
let r = AttestationResponse {
attestation_elements: vec![0u8; 0],
signature: [0u8; 64],
};
assert_eq!(r.attestation_elements.len(), 0);
assert_eq!(r.signature.len(), 64);
}
use p256::ecdsa::{signature::Signer, Signature, SigningKey};
#[allow(clippy::expect_used)] fn mint_signed(elements: &[u8], challenge: &[u8; 16], seed: u8) -> ([u8; 65], [u8; 64]) {
let mut scalar = [0u8; 32];
scalar[0] = seed;
let signing_key = SigningKey::from_slice(&scalar)
.expect("non-zero 32-byte scalar is a valid P-256 scalar");
let verifying_key = signing_key.verifying_key();
let pubkey_point = verifying_key.to_encoded_point(false);
let pubkey_bytes = pubkey_point.as_bytes();
let mut pubkey_arr = [0u8; 65];
pubkey_arr.copy_from_slice(pubkey_bytes);
let mut tbs = Vec::with_capacity(elements.len() + 16);
tbs.extend_from_slice(elements);
tbs.extend_from_slice(challenge);
let sig: Signature = signing_key.sign(&tbs);
let sig_bytes = sig.to_bytes();
let mut sig_arr = [0u8; 64];
sig_arr.copy_from_slice(&sig_bytes);
(pubkey_arr, sig_arr)
}
#[test]
#[allow(clippy::expect_used)] fn verify_attestation_response_accepts_valid_signature() {
let elements = b"opaque attestation_elements TLV blob".to_vec();
let challenge = [0x42u8; 16];
let (pubkey, sig) = mint_signed(&elements, &challenge, 0x01);
let response = AttestationResponse {
attestation_elements: elements,
signature: sig,
};
verify_attestation_response(&response, &challenge, &pubkey)
.expect("valid signature must verify");
}
#[test]
#[allow(clippy::expect_used)] fn verify_rejects_flipped_signature_byte() {
let elements = b"some elements".to_vec();
let challenge = [0x42u8; 16];
let (pubkey, mut sig) = mint_signed(&elements, &challenge, 0x02);
sig[0] ^= 0x80;
let response = AttestationResponse {
attestation_elements: elements,
signature: sig,
};
let err = verify_attestation_response(&response, &challenge, &pubkey)
.expect_err("flipped signature byte must fail");
assert!(matches!(err, AttestationError::BadResponseSignature));
}
#[test]
#[allow(clippy::expect_used)] fn verify_rejects_wrong_challenge() {
let elements = b"some elements".to_vec();
let challenge_correct = [0x42u8; 16];
let (pubkey, sig) = mint_signed(&elements, &challenge_correct, 0x03);
let response = AttestationResponse {
attestation_elements: elements,
signature: sig,
};
let challenge_wrong = [0x43u8; 16];
let err = verify_attestation_response(&response, &challenge_wrong, &pubkey)
.expect_err("wrong challenge must fail");
assert!(matches!(err, AttestationError::BadResponseSignature));
}
#[test]
#[allow(clippy::expect_used)] fn verify_rejects_tampered_elements() {
let elements = b"some elements".to_vec();
let challenge = [0x42u8; 16];
let (pubkey, sig) = mint_signed(&elements, &challenge, 0x04);
let mut tampered = elements.clone();
tampered[0] ^= 0x80;
let response = AttestationResponse {
attestation_elements: tampered,
signature: sig,
};
let err = verify_attestation_response(&response, &challenge, &pubkey)
.expect_err("tampered elements must fail");
assert!(matches!(err, AttestationError::BadResponseSignature));
}
#[test]
#[allow(clippy::expect_used)] fn verify_rejects_wrong_public_key() {
let elements = b"some elements".to_vec();
let challenge = [0x42u8; 16];
let (_pubkey_correct, sig) = mint_signed(&elements, &challenge, 0x05);
let (pubkey_wrong, _other_sig) = mint_signed(&elements, &challenge, 0x06);
let response = AttestationResponse {
attestation_elements: elements,
signature: sig,
};
let err = verify_attestation_response(&response, &challenge, &pubkey_wrong)
.expect_err("wrong public key must fail");
assert!(matches!(err, AttestationError::BadResponseSignature));
}
#[test]
#[allow(clippy::expect_used)] fn verify_rejects_malformed_public_key() {
let elements = b"some elements".to_vec();
let challenge = [0x42u8; 16];
let (_pubkey, sig) = mint_signed(&elements, &challenge, 0x07);
let response = AttestationResponse {
attestation_elements: elements,
signature: sig,
};
let too_short = vec![0u8; 32];
let err = verify_attestation_response(&response, &challenge, &too_short)
.expect_err("malformed pubkey must fail");
assert!(matches!(err, AttestationError::BadResponseSignature));
}
#[test]
#[allow(clippy::expect_used)] fn extract_attestation_elements_fields_happy_path() {
let nonce = [0xAB_u8; 32];
let mut tlv = vec![0x15];
tlv.extend_from_slice(&[0x30, 0x01, 0x03, 0xCD, 0xCD, 0xCD]);
tlv.extend_from_slice(&[0x30, 0x02, 0x20]);
tlv.extend_from_slice(&nonce);
tlv.extend_from_slice(&[0x27, 0x03]);
tlv.extend_from_slice(&0x42_u64.to_le_bytes());
tlv.push(0x18);
let fields = extract_attestation_elements_fields(&tlv).expect("happy path decodes");
assert_eq!(fields.certification_declaration, vec![0xCD, 0xCD, 0xCD]);
assert_eq!(fields.attestation_nonce, nonce);
assert_eq!(fields.timestamp_epoch_seconds, 0x42);
}
#[test]
#[allow(clippy::expect_used)] fn extract_attestation_elements_fields_rejects_malformed() {
let err = extract_attestation_elements_fields(&[0xFF]).expect_err("malformed");
assert!(matches!(err, AttestationError::ResponseElementsMalformed));
}
#[test]
#[allow(clippy::expect_used)] fn extract_attestation_elements_fields_rejects_short_nonce() {
let mut tlv = vec![0x15];
tlv.extend_from_slice(&[0x30, 0x01, 0x03, 0xCD, 0xCD, 0xCD]);
tlv.extend_from_slice(&[0x30, 0x02, 0x10]); tlv.extend_from_slice(&[0u8; 16]);
tlv.extend_from_slice(&[0x27, 0x03]);
tlv.extend_from_slice(&0_u64.to_le_bytes());
tlv.push(0x18);
let err = extract_attestation_elements_fields(&tlv).expect_err("short nonce");
assert!(matches!(err, AttestationError::ResponseElementsMalformed));
}
}