matter-commissioning 0.4.0

Matter commissioning state machine: setup payload, attestation, NOC issuance, network commissioning.
Documentation
//! M6.3.2: table-driven negative-path matrix. Each fixture exercises
//! one row of the spec's "every signature gate must reject the obvious
//! ways to attack it" table.

#![forbid(unsafe_code)]
#![allow(clippy::unwrap_used)] // Test-code carve-out: see CLAUDE.md.

use std::fs;
use std::path::PathBuf;
use std::sync::Arc;

use base64::{engine::general_purpose::STANDARD as B64, Engine as _};
use matter_cert::{MatterTime, PublicKey};
use matter_commissioning::{
    issue_noc, verify_csr_response, FabricRecord, NocError, SystemNocRng, VerifiedCsr,
};
use matter_crypto::{RingSigner, Signer};
use serde::Deserialize;

#[derive(Deserialize)]
struct Fixture {
    #[allow(dead_code)]
    kind: String,
    nocsr_elements_b64: String,
    attestation_signature_b64: String,
    expected_csr_nonce_hex: String,
    attestation_challenge_hex: String,
    dac_public_key_b64: String,
}

fn load(name: &str) -> Fixture {
    let mut p: PathBuf = env!("CARGO_MANIFEST_DIR").into();
    p.push("..");
    p.push("..");
    p.push("test-vectors");
    p.push("commissioning");
    p.push("noc");
    p.push("negative");
    p.push(format!("{name}.json"));
    let bytes = fs::read(&p).unwrap_or_else(|e| panic!("missing fixture {}: {e}", p.display()));
    serde_json::from_slice(&bytes).unwrap()
}

fn hex_to_array<const N: usize>(s: &str) -> [u8; N] {
    let bytes = hex::decode(s).unwrap();
    let mut arr = [0u8; N];
    arr.copy_from_slice(&bytes);
    arr
}

fn run(name: &str) -> Result<VerifiedCsr, NocError> {
    let f = load(name);
    let elements = B64.decode(&f.nocsr_elements_b64).unwrap();
    let sig_bytes = B64.decode(&f.attestation_signature_b64).unwrap();
    let mut sig = [0u8; 64];
    sig.copy_from_slice(&sig_bytes);
    let nonce = hex_to_array::<32>(&f.expected_csr_nonce_hex);
    let challenge = hex_to_array::<16>(&f.attestation_challenge_hex);
    let dac_pub = B64.decode(&f.dac_public_key_b64).unwrap();
    verify_csr_response(&elements, &sig, &nonce, &challenge, &dac_pub)
}

#[test]
fn bad_csr_self_sig_rejects() {
    let err = run("bad-csr-self-sig").unwrap_err();
    assert!(matches!(err, NocError::BadCsrSelfSignature), "got: {err:?}");
}

#[test]
fn wrong_nonce_echo_rejects() {
    let err = run("wrong-nonce-echo").unwrap_err();
    assert!(matches!(err, NocError::NonceMismatch), "got: {err:?}");
}

#[test]
fn bad_att_sig_rejects() {
    let err = run("bad-att-sig").unwrap_err();
    assert!(
        matches!(err, NocError::BadCsrAttestationSignature),
        "got: {err:?}"
    );
}

#[test]
fn non_p256_csr_key_rejects() {
    let err = run("non-p256-csr-key").unwrap_err();
    assert!(
        matches!(err, NocError::CsrParse(_) | NocError::InvalidCsrPublicKey),
        "got: {err:?}"
    );
}

#[test]
fn malformed_nocsr_tlv_rejects() {
    let err = run("malformed-nocsr-tlv").unwrap_err();
    assert!(matches!(err, NocError::NocsrParse(_)), "got: {err:?}");
}

#[test]
fn malformed_pkcs10_rejects() {
    let err = run("malformed-pkcs10").unwrap_err();
    assert!(matches!(err, NocError::CsrParse(_)), "got: {err:?}");
}

#[test]
fn wrong_challenge_rejects() {
    let err = run("wrong-challenge").unwrap_err();
    assert!(
        matches!(err, NocError::BadCsrAttestationSignature),
        "got: {err:?}"
    );
}

// oversized-dn-attribute exercises the issuer's overflow path via a
// happy-path NOCSR; we use the fixture as a sanity check that issue_noc
// succeeds when CATs are in valid range.
#[test]
fn oversized_cat_value_path() {
    let (root_signer, _) = RingSigner::generate().unwrap();
    let root_signer: Arc<dyn Signer> = Arc::new(root_signer);
    let fabric = FabricRecord::new_root_only(
        1,
        root_signer,
        MatterTime::from_unix_secs(1_700_000_000),
        MatterTime::NO_EXPIRY,
        7,
        &SystemNocRng,
    )
    .unwrap();

    let (device_signer, _) = RingSigner::generate().unwrap();
    let verified = VerifiedCsr {
        public_key: PublicKey::from_slice(device_signer.public_key().as_bytes()).unwrap(),
    };
    issue_noc(
        &fabric,
        &verified,
        0xABCD,
        &[0x1234_5678],
        (
            MatterTime::from_unix_secs(1_700_000_000),
            MatterTime::NO_EXPIRY,
        ),
        &SystemNocRng,
    )
    .unwrap();
}