use crate::identity::{CertError, sign_did_cert, verify_payload_sig};
use crate::signing::{b64decode, b64encode};
use sha2::{Digest, Sha256};
pub const FINGERPRINT_DOMAIN: &str = "wire-same-machine-v1";
#[derive(Debug, PartialEq, Eq)]
pub enum VerifyError {
BadFingerprint,
FingerprintMismatch,
Signature(CertError),
}
impl std::fmt::Display for VerifyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
VerifyError::BadFingerprint => write!(f, "malformed machine_fingerprint"),
VerifyError::FingerprintMismatch => {
write!(f, "attestation fingerprint is not this machine")
}
VerifyError::Signature(e) => write!(f, "attestation signature: {e}"),
}
}
}
pub fn machine_fingerprint(machine_id: &[u8], os_user_id: &[u8]) -> [u8; 32] {
let mut h = Sha256::new();
h.update(machine_id);
h.update(os_user_id);
h.update(FINGERPRINT_DOMAIN.as_bytes());
let digest = h.finalize();
let mut fp = [0u8; 32];
fp.copy_from_slice(&digest);
fp
}
pub fn attestation_payload(fingerprint: &[u8; 32], session_did: &str) -> String {
format!(
"{FINGERPRINT_DOMAIN}|{}|{session_did}",
hex::encode(fingerprint)
)
}
pub fn build_attestation(
op_sk: &[u8],
fingerprint: &[u8; 32],
session_did: &str,
) -> Result<(String, String), CertError> {
let payload = attestation_payload(fingerprint, session_did);
let sig = sign_did_cert(op_sk, &payload)?;
Ok((b64encode(fingerprint), sig))
}
pub fn verify_attestation(
op_pubkey: &[u8],
attest_fingerprint_b64: &str,
attest_sig_b64: &str,
sender_session_did: &str,
local_fingerprint: &[u8; 32],
) -> Result<(), VerifyError> {
let fp_bytes = b64decode(attest_fingerprint_b64).map_err(|_| VerifyError::BadFingerprint)?;
if fp_bytes.len() != 32 {
return Err(VerifyError::BadFingerprint);
}
if fp_bytes.as_slice() != local_fingerprint.as_slice() {
return Err(VerifyError::FingerprintMismatch);
}
let mut fp = [0u8; 32];
fp.copy_from_slice(&fp_bytes);
let payload = attestation_payload(&fp, sender_session_did);
verify_payload_sig(op_pubkey, attest_sig_b64, &payload).map_err(VerifyError::Signature)
}
pub fn local_fingerprint() -> Option<[u8; 32]> {
let machine_id = crate::platform::machine_id_raw()?;
let os_user_id = crate::platform::os_user_id_bytes()?;
Some(machine_fingerprint(&machine_id, &os_user_id))
}
fn my_op_did() -> Option<String> {
let card = crate::config::read_agent_card().ok()?;
crate::agent_card::card_op_did(&card).map(str::to_string)
}
pub fn auto_pin_decision(peer_card: &serde_json::Value) -> Option<String> {
let anchor = crate::org_membership::verify_op_anchor(peer_card)
.ok()
.flatten()?;
if anchor.op_did != my_op_did()? {
return None;
}
let att = peer_card.get("same_machine_attestation")?;
let fp_b64 = att.get("machine_fingerprint").and_then(|v| v.as_str())?;
let sig_b64 = att.get("signature").and_then(|v| v.as_str())?;
let sender_did = peer_card.get("did").and_then(|v| v.as_str())?;
let local_fp = local_fingerprint()?;
verify_attestation(&anchor.op_pubkey, fp_b64, sig_b64, sender_did, &local_fp).ok()?;
Some(anchor.op_did)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::signing::generate_keypair;
#[test]
fn fingerprint_is_deterministic_and_user_salted() {
let m = b"machine-uuid-aaaa";
let fp_u1000 = machine_fingerprint(m, b"1000");
assert_eq!(fp_u1000, machine_fingerprint(m, b"1000"));
assert_ne!(fp_u1000, machine_fingerprint(m, b"1001"));
assert_ne!(fp_u1000, machine_fingerprint(b"machine-uuid-bbbb", b"1000"));
}
#[test]
fn payload_is_domain_separated() {
let fp = machine_fingerprint(b"m", b"1000");
let p = attestation_payload(&fp, "did:wire:slate-lotus-88232017");
assert!(p.starts_with("wire-same-machine-v1|"));
assert!(p.ends_with("|did:wire:slate-lotus-88232017"));
assert!(!p.starts_with("wire-succession-v1"));
}
#[test]
fn roundtrip_same_machine_verifies() {
let (op_sk, op_pk) = generate_keypair();
let fp = machine_fingerprint(b"machine-X", b"1000");
let did = "did:wire:slate-lotus-88232017";
let (fp_b64, sig) = build_attestation(&op_sk, &fp, did).unwrap();
assert_eq!(verify_attestation(&op_pk, &fp_b64, &sig, did, &fp), Ok(()));
}
#[test]
fn different_uid_rejected() {
let (op_sk, op_pk) = generate_keypair();
let sender_fp = machine_fingerprint(b"machine-X", b"1000");
let did = "did:wire:slate-lotus-88232017";
let (fp_b64, sig) = build_attestation(&op_sk, &sender_fp, did).unwrap();
let receiver_fp = machine_fingerprint(b"machine-X", b"1001");
assert_eq!(
verify_attestation(&op_pk, &fp_b64, &sig, did, &receiver_fp),
Err(VerifyError::FingerprintMismatch)
);
}
#[test]
fn hostile_forge_signed_over_other_fingerprint_rejected() {
let (op_sk, op_pk) = generate_keypair();
let receiver_fp = machine_fingerprint(b"machine-victim", b"1000");
let attacker_fp = machine_fingerprint(b"machine-attacker", b"1000");
let did = "did:wire:evil-1234";
let sig = sign_did_cert(&op_sk, &attestation_payload(&attacker_fp, did)).unwrap();
let published_fp_b64 = b64encode(&receiver_fp);
assert_eq!(
verify_attestation(&op_pk, &published_fp_b64, &sig, did, &receiver_fp),
Err(VerifyError::Signature(CertError::Rejected))
);
}
#[test]
fn published_matches_but_signature_mismatched_rejected() {
let (op_sk, op_pk) = generate_keypair();
let receiver_fp = machine_fingerprint(b"machine-victim", b"1000");
let did = "did:wire:evil-1234";
let sig = sign_did_cert(
&op_sk,
&attestation_payload(&receiver_fp, "did:wire:some-other-9999"),
)
.unwrap();
let published_fp_b64 = b64encode(&receiver_fp);
assert_eq!(
verify_attestation(&op_pk, &published_fp_b64, &sig, did, &receiver_fp),
Err(VerifyError::Signature(CertError::Rejected))
);
}
#[test]
fn wrong_op_key_rejected() {
let (op_sk, _op_pk) = generate_keypair();
let (_other_sk, other_pk) = generate_keypair();
let fp = machine_fingerprint(b"machine-X", b"1000");
let did = "did:wire:slate-lotus-1";
let (fp_b64, sig) = build_attestation(&op_sk, &fp, did).unwrap();
assert_eq!(
verify_attestation(&other_pk, &fp_b64, &sig, did, &fp),
Err(VerifyError::Signature(CertError::Rejected))
);
}
#[test]
fn malformed_fingerprint_rejected() {
let (_sk, pk) = generate_keypair();
let fp = machine_fingerprint(b"m", b"1000");
assert_eq!(
verify_attestation(&pk, "!!!not-base64", "sig", "did:wire:x", &fp),
Err(VerifyError::BadFingerprint)
);
assert_eq!(
verify_attestation(&pk, &b64encode(b"too-short"), "sig", "did:wire:x", &fp),
Err(VerifyError::BadFingerprint)
);
}
#[test]
fn build_is_deterministic() {
let (op_sk, _pk) = generate_keypair();
let fp = machine_fingerprint(b"machine-X", b"1000");
let did = "did:wire:slate-lotus-1";
assert_eq!(
build_attestation(&op_sk, &fp, did).unwrap(),
build_attestation(&op_sk, &fp, did).unwrap()
);
}
#[cfg(test)]
fn peer_card_for(
op_sk: &[u8; 32],
op_pk: &[u8; 32],
op_handle: &str,
attest_fp: &[u8; 32],
) -> serde_json::Value {
let (_peer_sk, peer_pk) = generate_keypair();
let base = crate::agent_card::build_agent_card("sister", &peer_pk, None, None, None);
let session_did = base
.get("did")
.and_then(|v| v.as_str())
.unwrap()
.to_string();
let op_did = crate::agent_card::did_for_op(op_handle, op_pk);
let op_cert = crate::identity::sign_did_cert(op_sk, &session_did).unwrap();
let (fp_b64, sig) = build_attestation(op_sk, attest_fp, &session_did).unwrap();
let claims = crate::agent_card::IdentityClaims {
op_did: Some(op_did),
op_cert: Some(op_cert),
op_pubkey: Some(b64encode(op_pk)),
org_memberships: vec![],
project: None,
same_machine_attestation: Some((fp_b64, sig)),
};
crate::agent_card::with_identity_claims(&base, &claims).unwrap()
}
#[cfg(test)]
fn enroll_self(op_sk: &[u8; 32], op_handle: &str) {
crate::config::write_op_key(op_sk).unwrap();
crate::config::write_op_handle(op_handle).unwrap();
let (my_sk, my_pk) = generate_keypair();
let base = crate::agent_card::build_agent_card("me", &my_pk, None, None, None);
let card = crate::enroll::with_op_claims_if_enrolled(base).unwrap();
crate::config::write_agent_card(&crate::agent_card::sign_agent_card(&card, &my_sk))
.unwrap();
}
#[test]
fn auto_pin_decision_accepts_same_op_same_machine() {
crate::config::test_support::with_temp_home(|| {
let Some(local_fp) = local_fingerprint() else {
return; };
let (op_sk, op_pk) = generate_keypair();
enroll_self(&op_sk, "darby");
let op_did = crate::agent_card::did_for_op("darby", &op_pk);
let peer = peer_card_for(&op_sk, &op_pk, "darby", &local_fp);
assert_eq!(auto_pin_decision(&peer), Some(op_did));
});
}
#[test]
fn auto_pin_decision_rejects_different_operator() {
crate::config::test_support::with_temp_home(|| {
let Some(local_fp) = local_fingerprint() else {
return;
};
let (my_op_sk, _my_op_pk) = generate_keypair();
enroll_self(&my_op_sk, "darby");
let (other_sk, other_pk) = generate_keypair();
let peer = peer_card_for(&other_sk, &other_pk, "mallory", &local_fp);
assert_eq!(auto_pin_decision(&peer), None);
});
}
#[test]
fn auto_pin_decision_rejects_other_machine_fingerprint() {
crate::config::test_support::with_temp_home(|| {
if local_fingerprint().is_none() {
return;
}
let (op_sk, op_pk) = generate_keypair();
enroll_self(&op_sk, "darby");
let other_machine_fp = machine_fingerprint(b"some-other-box", b"31337");
let peer = peer_card_for(&op_sk, &op_pk, "darby", &other_machine_fp);
assert_eq!(auto_pin_decision(&peer), None);
});
}
}