Skip to main content

auths_verifier/
verify.rs

1//! Free-function verification API wrapping [`crate::verifier::Verifier`].
2//!
3//! **Fail-open duplicity policy**: a diverging shared KEL is reported via
4//! `VerificationReport::duplicity_warning` but does not invalidate an
5//! attestation whose own signature checks out. Rationale: fail-closed would
6//! turn one bad rotation on the identity's shared KEL into a workspace-wide
7//! outage for every attestation signed by a current controller — strictly
8//! worse than surfacing the fork and letting the user resolve via
9//! `auths device remove` on the side they trust. The warning channel is
10//! structured (`DuplicityReport`) so downstream policy — CLI status,
11//! iOS banner, future CI gates — can decide what to do with it.
12
13use crate::core::{
14    Attestation, DevicePublicKey, VerifiedAttestation, canonicalize_attestation_data,
15};
16use crate::error::AttestationError;
17use crate::types::{ChainLink, VerificationReport, VerificationStatus};
18#[cfg(feature = "native")]
19use crate::witness::WitnessVerifyConfig;
20use auths_crypto::CryptoProvider;
21use auths_keri::{Event, compute_said, find_seal_in_kel};
22use chrono::{DateTime, Duration, Utc};
23use log::debug;
24use serde::Serialize;
25
26/// Maximum allowed clock skew in seconds for timestamp validation.
27const MAX_SKEW_SECS: i64 = 5 * 60;
28
29// ---------------------------------------------------------------------------
30// Public free functions — backward-compatible, #[cfg(feature = "native")]
31// ---------------------------------------------------------------------------
32
33/// Verify an attestation's signatures against the issuer's public key.
34///
35/// Args:
36/// * `att`: The attestation to verify.
37/// * `issuer_pk`: Typed issuer public key (Ed25519 or P-256).
38#[cfg(feature = "native")]
39pub async fn verify_with_keys(
40    att: &Attestation,
41    issuer_pk: &DevicePublicKey,
42) -> Result<VerifiedAttestation, AttestationError> {
43    crate::verifier::Verifier::native()
44        .verify_with_keys(att, issuer_pk)
45        .await
46}
47
48/// Verify an attestation against a specific point in time.
49///
50/// Args:
51/// * `att`: The attestation to verify.
52/// * `issuer_pk`: Typed issuer public key (Ed25519 or P-256).
53/// * `at`: The reference timestamp for expiry evaluation.
54#[cfg(feature = "native")]
55pub async fn verify_at_time(
56    att: &Attestation,
57    issuer_pk: &DevicePublicKey,
58    at: DateTime<Utc>,
59) -> Result<VerifiedAttestation, AttestationError> {
60    crate::verifier::Verifier::native()
61        .verify_at_time(att, issuer_pk, at)
62        .await
63}
64
65/// Verify a chain and validate witness receipts against a quorum threshold.
66///
67/// Args:
68/// * `attestations`: Ordered attestation chain (root first).
69/// * `root_pk`: Typed root identity public key (Ed25519 or P-256).
70/// * `witness_config`: Witness receipts and quorum threshold to validate.
71#[cfg(feature = "native")]
72pub async fn verify_chain_with_witnesses(
73    attestations: &[Attestation],
74    root_pk: &DevicePublicKey,
75    witness_config: &WitnessVerifyConfig<'_>,
76) -> Result<VerificationReport, AttestationError> {
77    crate::verifier::Verifier::native()
78        .verify_chain_with_witnesses(attestations, root_pk, witness_config)
79        .await
80}
81
82/// Verify an ordered attestation chain starting from a known root public key.
83///
84/// Args:
85/// * `attestations`: Ordered attestation chain (root first).
86/// * `root_pk`: Typed root identity public key (Ed25519 or P-256).
87#[cfg(feature = "native")]
88pub async fn verify_chain(
89    attestations: &[Attestation],
90    root_pk: &DevicePublicKey,
91) -> Result<VerificationReport, AttestationError> {
92    crate::verifier::Verifier::native()
93        .verify_chain(attestations, root_pk)
94        .await
95}
96
97/// Verify that a device is authorized under a given identity.
98///
99/// Args:
100/// * `identity_did`: The DID of the authorizing identity.
101/// * `device_did`: The device DID to check authorization for.
102/// * `attestations`: Pool of attestations to search.
103/// * `identity_pk`: Typed identity public key (Ed25519 or P-256).
104#[cfg(feature = "native")]
105pub async fn verify_device_authorization(
106    identity_did: &str,
107    device_did: &crate::types::CanonicalDid,
108    attestations: &[Attestation],
109    identity_pk: &DevicePublicKey,
110) -> Result<VerificationReport, AttestationError> {
111    crate::verifier::Verifier::native()
112        .verify_device_authorization(identity_did, device_did, attestations, identity_pk)
113        .await
114}
115
116use crate::types::CanonicalDid;
117
118/// Checks if a device appears in a list of **already-verified** attestations.
119pub fn is_device_listed(
120    identity_did: &str,
121    device_did: &CanonicalDid,
122    attestations: &[VerifiedAttestation],
123    now: DateTime<Utc>,
124) -> bool {
125    let device_did_str = device_did.to_string();
126
127    attestations.iter().any(|verified| {
128        let att = verified.inner();
129        if att.issuer != identity_did {
130            return false;
131        }
132        if att.subject.to_string() != device_did_str {
133            return false;
134        }
135        if att.is_revoked() {
136            return false;
137        }
138        if let Some(exp) = att.expires_at
139            && now > exp
140        {
141            return false;
142        }
143        true
144    })
145}
146
147// ---------------------------------------------------------------------------
148// Device-link verification — stateless, provider-agnostic
149// ---------------------------------------------------------------------------
150
151/// Result of verifying a device's link to a KERI identity.
152///
153/// Verification failures are expressed as `valid: false` with an error message,
154/// not as Rust `Err` values. Only infrastructure errors (bad input, serialization)
155/// produce `Err`.
156#[derive(Debug, Clone, Serialize)]
157pub struct DeviceLinkVerification {
158    /// Whether the device link verified successfully.
159    pub valid: bool,
160    /// Human-readable reason if verification failed.
161    #[serde(skip_serializing_if = "Option::is_none")]
162    pub error: Option<String>,
163    /// The KERI key state after KEL replay (present on success).
164    #[serde(skip_serializing_if = "Option::is_none")]
165    pub key_state: Option<auths_keri::KeyState>,
166    /// Sequence number of the IXN event anchoring the attestation seal (if found).
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub seal_sequence: Option<u128>,
169}
170
171impl DeviceLinkVerification {
172    fn success(key_state: auths_keri::KeyState, seal_sequence: Option<u128>) -> Self {
173        Self {
174            valid: true,
175            error: None,
176            key_state: Some(key_state),
177            seal_sequence,
178        }
179    }
180
181    fn failure(reason: impl Into<String>) -> Self {
182        Self {
183            valid: false,
184            error: Some(reason.into()),
185            key_state: None,
186            seal_sequence: None,
187        }
188    }
189}
190
191/// Verify that a device (`did:key`) is cryptographically linked to a KERI identity.
192///
193/// Composes: KEL verification, attestation signature verification, device DID matching,
194/// and optional seal anchoring. Provider-agnostic — works with any `CryptoProvider`.
195///
196/// Args:
197/// * `events`: Parsed KEL events (inception first).
198/// * `attestation`: The attestation linking identity to device.
199/// * `device_did`: The expected device DID (checked against attestation subject).
200/// * `now`: Reference time for expiration/revocation checks.
201/// * `provider`: Cryptographic provider for Ed25519 verification.
202///
203/// Usage:
204/// ```ignore
205/// let result = verify_device_link(&events, &att, "did:key:z6Mk...", now, &provider).await;
206/// if result.valid { /* ... */ }
207/// ```
208pub async fn verify_device_link(
209    events: &[Event],
210    attestation: &Attestation,
211    device_did: &str,
212    now: DateTime<Utc>,
213    provider: &dyn CryptoProvider,
214) -> DeviceLinkVerification {
215    // rt-002-allow: `events` is the device-link KEL authenticated before it reaches here — local trusted registry, authenticated bundle, or the WASM verifyDeviceLink boundary (which pairs wire attachments and runs validate_signed_kel first). The attestation signature is additionally bound to the resulting key-state.
216    let key_state = match auths_keri::TrustedKel::from_trusted_source(events).replay() {
217        Ok(ks) => ks,
218        Err(e) => return DeviceLinkVerification::failure(format!("KEL verification failed: {e}")),
219    };
220
221    if attestation.subject.as_str() != device_did {
222        return DeviceLinkVerification::failure(format!(
223            "Device DID mismatch: attestation subject is '{}', expected '{device_did}'",
224            attestation.subject
225        ));
226    }
227
228    let current_pk = match key_state.current_keys.first() {
229        Some(encoded) => match auths_keri::KeriPublicKey::parse(encoded.as_str()) {
230            Ok(keri_pk) => {
231                // Curve travels with the key (CESR prefix) — never hardcode it; P-256
232                // device keys must verify too (the workspace default curve).
233                let curve = keri_pk.curve();
234                let bytes = keri_pk.into_bytes().to_vec();
235                match DevicePublicKey::try_new(curve, &bytes) {
236                    Ok(dpk) => dpk,
237                    Err(e) => {
238                        return DeviceLinkVerification::failure(format!(
239                            "Invalid current key: {e}"
240                        ));
241                    }
242                }
243            }
244            Err(e) => {
245                return DeviceLinkVerification::failure(format!("Invalid current key: {e}"));
246            }
247        },
248        None => return DeviceLinkVerification::failure("KEL has no current keys"),
249    };
250
251    if let Err(e) = verify_with_keys_at(attestation, &current_pk, now, true, provider).await {
252        return DeviceLinkVerification::failure(format!("Attestation verification failed: {e}"));
253    }
254
255    let seal_sequence = compute_attestation_seal_digest(attestation)
256        .ok()
257        .and_then(|digest| find_seal_in_kel(events, digest.as_str()));
258
259    DeviceLinkVerification::success(key_state, seal_sequence)
260}
261
262/// Compute the KERI SAID (Blake3 digest) of an attestation's canonical form.
263///
264/// This is the digest that should appear in a KEL IXN seal when the attestation
265/// is anchored. Returns the SAID string (E-prefixed base64url Blake3).
266pub fn compute_attestation_seal_digest(
267    attestation: &Attestation,
268) -> Result<String, AttestationError> {
269    let canonical = canonicalize_attestation_data(&attestation.canonical_data())?;
270    let value: serde_json::Value = serde_json::from_slice(&canonical)
271        .map_err(|e| AttestationError::SerializationError(e.to_string()))?;
272    Ok(compute_said(&value)
273        .map_err(|e| AttestationError::SerializationError(e.to_string()))?
274        .into_inner())
275}
276
277// ---------------------------------------------------------------------------
278// Internal async functions — used by Verifier and free function wrappers
279// ---------------------------------------------------------------------------
280
281pub(crate) async fn verify_with_keys_at(
282    att: &Attestation,
283    issuer_pk: &DevicePublicKey,
284    at: DateTime<Utc>,
285    check_skew: bool,
286    provider: &dyn CryptoProvider,
287) -> Result<(), AttestationError> {
288    let reference_time = at;
289
290    // --- 1. Check revocation (time-aware) ---
291    if let Some(revoked_at) = att.revoked_at
292        && revoked_at <= reference_time
293    {
294        return Err(AttestationError::AttestationRevoked);
295    }
296
297    // --- 2. Check expiration against reference time ---
298    if let Some(exp) = att.expires_at
299        && reference_time > exp
300    {
301        return Err(AttestationError::AttestationExpired {
302            at: exp.to_rfc3339(),
303        });
304    }
305
306    // --- 3. Check timestamp skew against reference time ---
307    if check_skew
308        && let Some(ts) = att.timestamp
309        && ts > reference_time + Duration::seconds(MAX_SKEW_SECS)
310    {
311        return Err(AttestationError::TimestampInFuture {
312            at: ts.to_rfc3339(),
313        });
314    }
315
316    // --- 4. Reconstruct and canonicalize data ---
317    let canonical_json_bytes = canonicalize_attestation_data(&att.canonical_data())?;
318    let data_to_verify = canonical_json_bytes.as_slice();
319    debug!(
320        "(Verify) Canonical data: {}",
321        String::from_utf8_lossy(&canonical_json_bytes)
322    );
323
324    // --- 5. Verify issuer signature (dispatched on curve) ---
325    if !att.identity_signature.is_empty() {
326        verify_signature_by_curve(
327            issuer_pk,
328            data_to_verify,
329            att.identity_signature.as_bytes(),
330            provider,
331            SignatureRole::Issuer,
332        )
333        .await?;
334        debug!("(Verify) Issuer signature verified successfully.");
335    } else {
336        // An attestation with no issuer signature cannot prove that any identity
337        // authorized it, so the authority path fails closed. A device asserting
338        // only about its own key is checked through a dedicated self-assertion
339        // entry point, never here.
340        return Err(AttestationError::IssuerSignatureFailed(
341            "missing issuer signature".to_string(),
342        ));
343    }
344
345    // --- 6. Verify device signature (dispatched on curve) ---
346    verify_signature_by_curve(
347        &att.device_public_key,
348        data_to_verify,
349        att.device_signature.as_bytes(),
350        provider,
351        SignatureRole::Device,
352    )
353    .await?;
354    debug!("(Verify) Device signature verified successfully.");
355
356    Ok(())
357}
358
359/// Which signature slot a curve-dispatch error should be attributed to.
360#[derive(Clone, Copy)]
361enum SignatureRole {
362    Issuer,
363    Device,
364}
365
366/// Verify a signature via the canonical `DevicePublicKey::verify`, attributing
367/// failures to the appropriate role (issuer vs device) at the boundary.
368///
369/// Thin wrapper preserved so that existing chain-verification code can keep
370/// its role-attributed error enum; the dispatch itself now lives in
371/// `DevicePublicKey::verify` (fn-114.14).
372async fn verify_signature_by_curve(
373    pk: &DevicePublicKey,
374    message: &[u8],
375    signature: &[u8],
376    provider: &dyn CryptoProvider,
377    role: SignatureRole,
378) -> Result<(), AttestationError> {
379    let map_err = |e: String| match role {
380        SignatureRole::Issuer => AttestationError::IssuerSignatureFailed(e),
381        SignatureRole::Device => AttestationError::DeviceSignatureFailed(e),
382    };
383
384    pk.verify(message, signature, provider)
385        .await
386        .map_err(|e| map_err(e.to_string()))
387}
388
389pub(crate) async fn verify_chain_inner(
390    attestations: &[Attestation],
391    root_pk: &DevicePublicKey,
392    provider: &dyn CryptoProvider,
393    now: DateTime<Utc>,
394) -> Result<VerificationReport, AttestationError> {
395    if attestations.is_empty() {
396        return Ok(VerificationReport::with_status(
397            VerificationStatus::BrokenChain {
398                missing_link: "empty chain".to_string(),
399            },
400            vec![],
401        ));
402    }
403
404    let mut chain_links: Vec<ChainLink> = Vec::with_capacity(attestations.len());
405
406    let first_att = &attestations[0];
407    match verify_single_attestation(first_att, root_pk, 0, provider, now).await {
408        Ok(link) => chain_links.push(link),
409        Err((status, link)) => {
410            chain_links.push(link);
411            return Ok(VerificationReport::with_status(status, chain_links));
412        }
413    }
414
415    for (idx, att) in attestations.iter().enumerate().skip(1) {
416        let prev_att = &attestations[idx - 1];
417
418        if att.issuer.as_str() != prev_att.subject.as_str() {
419            let link = ChainLink::invalid(
420                att.issuer.to_string(),
421                att.subject.to_string(),
422                format!(
423                    "Chain broken: expected issuer '{}', got '{}'",
424                    prev_att.subject, att.issuer
425                ),
426            );
427            chain_links.push(link);
428            return Ok(VerificationReport::with_status(
429                VerificationStatus::BrokenChain {
430                    missing_link: format!(
431                        "Issuer mismatch at step {}: expected '{}', got '{}'",
432                        idx, prev_att.subject, att.issuer
433                    ),
434                },
435                chain_links,
436            ));
437        }
438
439        let issuer_pk = &prev_att.device_public_key;
440
441        match verify_single_attestation(att, issuer_pk, idx, provider, now).await {
442            Ok(link) => chain_links.push(link),
443            Err((status, link)) => {
444                chain_links.push(link);
445                return Ok(VerificationReport::with_status(status, chain_links));
446            }
447        }
448    }
449
450    Ok(VerificationReport::valid(chain_links))
451}
452
453pub(crate) async fn verify_device_authorization_inner(
454    identity_did: &str,
455    device_did: &CanonicalDid,
456    attestations: &[Attestation],
457    identity_pk: &DevicePublicKey,
458    provider: &dyn CryptoProvider,
459    now: DateTime<Utc>,
460) -> Result<VerificationReport, AttestationError> {
461    let device_did_str = device_did.to_string();
462
463    let matching: Vec<&Attestation> = attestations
464        .iter()
465        .filter(|a| a.issuer == identity_did && a.subject.to_string() == device_did_str)
466        .collect();
467
468    if matching.is_empty() {
469        return Ok(VerificationReport::with_status(
470            VerificationStatus::BrokenChain {
471                missing_link: format!(
472                    "No attestation found for device {} under {}",
473                    device_did_str, identity_did
474                ),
475            },
476            vec![],
477        ));
478    }
479
480    match verify_single_attestation(matching[0], identity_pk, 0, provider, now).await {
481        Ok(link) => Ok(VerificationReport::valid(vec![link])),
482        Err((status, link)) => Ok(VerificationReport::with_status(status, vec![link])),
483    }
484}
485
486async fn verify_single_attestation(
487    att: &Attestation,
488    issuer_pk: &DevicePublicKey,
489    step: usize,
490    provider: &dyn CryptoProvider,
491    now: DateTime<Utc>,
492) -> Result<ChainLink, (VerificationStatus, ChainLink)> {
493    let issuer = att.issuer.to_string();
494    let subject = att.subject.to_string();
495
496    if att.is_revoked() {
497        return Err((
498            VerificationStatus::Revoked { at: att.revoked_at },
499            ChainLink::invalid(issuer, subject, "Attestation revoked".to_string()),
500        ));
501    }
502
503    if let Some(exp) = att.expires_at
504        && now > exp
505    {
506        return Err((
507            VerificationStatus::Expired { at: exp },
508            ChainLink::invalid(
509                issuer,
510                subject,
511                format!("Attestation expired on {}", exp.to_rfc3339()),
512            ),
513        ));
514    }
515
516    match verify_with_keys_at(att, issuer_pk, now, true, provider).await {
517        Ok(()) => Ok(ChainLink::valid(issuer, subject)),
518        Err(e) => Err((
519            VerificationStatus::InvalidSignature { step },
520            ChainLink::invalid(issuer, subject, e.to_string()),
521        )),
522    }
523}
524
525#[cfg(all(test, not(target_arch = "wasm32")))]
526#[allow(clippy::unwrap_used, clippy::expect_used, clippy::disallowed_methods)]
527mod tests {
528    use super::*;
529    use crate::clock::ClockProvider;
530    use crate::core::{Ed25519PublicKey, Ed25519Signature, ResourceId};
531    use crate::types::CanonicalDid;
532    use crate::verifier::Verifier;
533    use auths_crypto::RingCryptoProvider;
534    use auths_crypto::testing::create_test_keypair;
535    use auths_keri::Said;
536    use chrono::{DateTime, Duration, TimeZone, Utc};
537    use ring::signature::{Ed25519KeyPair, KeyPair};
538    use std::sync::Arc;
539
540    /// Wrap a raw 32-byte Ed25519 key into a `DevicePublicKey` for tests.
541    fn ed(pk: &[u8]) -> DevicePublicKey {
542        DevicePublicKey::try_new(auths_crypto::CurveType::Ed25519, pk).unwrap()
543    }
544
545    /// Build a `did:key:z...` string from a 32-byte Ed25519 public key (test helper).
546    fn ed25519_did(pk: &[u8; 32]) -> String {
547        CanonicalDid::from_public_key_did_key(pk, auths_crypto::CurveType::Ed25519).to_string()
548    }
549
550    struct TestClock(DateTime<Utc>);
551    impl ClockProvider for TestClock {
552        fn now(&self) -> DateTime<Utc> {
553            self.0
554        }
555    }
556
557    fn fixed_now() -> DateTime<Utc> {
558        Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap()
559    }
560
561    fn test_verifier() -> Verifier {
562        Verifier::new(
563            Arc::new(RingCryptoProvider),
564            Arc::new(TestClock(fixed_now())),
565        )
566    }
567
568    /// Helper to create a signed attestation
569    fn create_signed_attestation(
570        issuer_kp: &Ed25519KeyPair,
571        device_kp: &Ed25519KeyPair,
572        issuer_did: &str,
573        subject_did: &str,
574        revoked_at: Option<DateTime<Utc>>,
575        expires_at: Option<DateTime<Utc>>,
576    ) -> Attestation {
577        let device_pk: [u8; 32] = device_kp.public_key().as_ref().try_into().unwrap();
578
579        let mut att = Attestation {
580            version: 1,
581            rid: ResourceId::new("test-rid"),
582            issuer: CanonicalDid::new_unchecked(issuer_did),
583            subject: CanonicalDid::new_unchecked(subject_did),
584            device_public_key: Ed25519PublicKey::from_bytes(device_pk).into(),
585            identity_signature: Ed25519Signature::empty(),
586            device_signature: Ed25519Signature::empty(),
587            revoked_at,
588            expires_at,
589            timestamp: Some(fixed_now()),
590            note: None,
591            payload: None,
592            delegated_by: None,
593            signer_type: None,
594            environment_claim: None,
595            commit_sha: None,
596            commit_message: None,
597            author: None,
598            oidc_binding: None,
599        };
600
601        let canonical_bytes = canonicalize_attestation_data(&att.canonical_data()).unwrap();
602
603        att.identity_signature =
604            Ed25519Signature::try_from_slice(issuer_kp.sign(&canonical_bytes).as_ref()).unwrap();
605        att.device_signature =
606            Ed25519Signature::try_from_slice(device_kp.sign(&canonical_bytes).as_ref()).unwrap();
607
608        att
609    }
610
611    #[tokio::test]
612    async fn verify_chain_empty_returns_broken_chain() {
613        let result = test_verifier()
614            .verify_chain(&[], &ed(&[0u8; 32]))
615            .await
616            .unwrap();
617        assert!(!result.is_valid());
618        match result.status {
619            VerificationStatus::BrokenChain { missing_link } => {
620                assert_eq!(missing_link, "empty chain");
621            }
622            _ => panic!("Expected BrokenChain status"),
623        }
624        assert!(result.chain.is_empty());
625    }
626
627    #[tokio::test]
628    async fn verify_chain_single_valid_attestation() {
629        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
630        let root_did = ed25519_did(&root_pk);
631        let (device_kp, device_pk) = create_test_keypair(&[2u8; 32]);
632        let device_did = ed25519_did(&device_pk);
633
634        let att = create_signed_attestation(
635            &root_kp,
636            &device_kp,
637            &root_did,
638            &device_did,
639            None,
640            Some(fixed_now() + Duration::days(365)),
641        );
642
643        let result = test_verifier()
644            .verify_chain(&[att], &ed(&root_pk))
645            .await
646            .unwrap();
647        assert!(result.is_valid());
648        assert_eq!(result.chain.len(), 1);
649        assert!(result.chain[0].valid);
650    }
651
652    #[tokio::test]
653    async fn verify_chain_valid_report_is_offline_unknown_not_bare() {
654        // A chain verified offline (no fresher source supplied) is honest about it: the report
655        // names its freshness as `Unknown`, never a bare `Valid` that reads as real-time fresh.
656        // This is what the CLI verify callers consume to render "valid (freshness unknown)".
657        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
658        let root_did = ed25519_did(&root_pk);
659        let (device_kp, device_pk) = create_test_keypair(&[2u8; 32]);
660        let device_did = ed25519_did(&device_pk);
661
662        let att = create_signed_attestation(
663            &root_kp,
664            &device_kp,
665            &root_did,
666            &device_did,
667            None,
668            Some(fixed_now() + Duration::days(365)),
669        );
670
671        let report = test_verifier()
672            .verify_chain(&[att], &ed(&root_pk))
673            .await
674            .unwrap();
675        assert!(report.is_valid());
676        assert_eq!(
677            report.freshness(),
678            crate::freshness::Freshness::Unknown,
679            "an offline chain verify must surface Unknown freshness, never a bare Valid"
680        );
681    }
682
683    #[tokio::test]
684    async fn verify_chain_revoked_attestation_returns_revoked() {
685        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
686        let root_did = ed25519_did(&root_pk);
687        let (device_kp, device_pk) = create_test_keypair(&[2u8; 32]);
688        let device_did = ed25519_did(&device_pk);
689
690        let att = create_signed_attestation(
691            &root_kp,
692            &device_kp,
693            &root_did,
694            &device_did,
695            Some(fixed_now()),
696            Some(fixed_now() + Duration::days(365)),
697        );
698
699        let result = test_verifier()
700            .verify_chain(&[att], &ed(&root_pk))
701            .await
702            .unwrap();
703        assert!(!result.is_valid());
704        match result.status {
705            VerificationStatus::Revoked { .. } => {}
706            _ => panic!("Expected Revoked status, got {:?}", result.status),
707        }
708    }
709
710    #[tokio::test]
711    async fn verify_chain_expired_attestation_returns_expired() {
712        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
713        let root_did = ed25519_did(&root_pk);
714        let (device_kp, device_pk) = create_test_keypair(&[2u8; 32]);
715        let device_did = ed25519_did(&device_pk);
716
717        let att = create_signed_attestation(
718            &root_kp,
719            &device_kp,
720            &root_did,
721            &device_did,
722            None,
723            Some(fixed_now() - Duration::days(1)),
724        );
725
726        let result = test_verifier()
727            .verify_chain(&[att], &ed(&root_pk))
728            .await
729            .unwrap();
730        assert!(!result.is_valid());
731        match result.status {
732            VerificationStatus::Expired { .. } => {}
733            _ => panic!("Expected Expired status, got {:?}", result.status),
734        }
735    }
736
737    #[tokio::test]
738    async fn verify_chain_invalid_signature_returns_invalid_signature() {
739        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
740        let root_did = ed25519_did(&root_pk);
741        let (device_kp, device_pk) = create_test_keypair(&[2u8; 32]);
742        let device_did = ed25519_did(&device_pk);
743
744        let mut att = create_signed_attestation(
745            &root_kp,
746            &device_kp,
747            &root_did,
748            &device_did,
749            None,
750            Some(fixed_now() + Duration::days(365)),
751        );
752        let mut tampered = *att.identity_signature.as_bytes();
753        tampered[0] ^= 0xFF;
754        att.identity_signature = Ed25519Signature::from_bytes(tampered);
755
756        let result = test_verifier()
757            .verify_chain(&[att], &ed(&root_pk))
758            .await
759            .unwrap();
760        assert!(!result.is_valid());
761        match result.status {
762            VerificationStatus::InvalidSignature { step } => {
763                assert_eq!(step, 0);
764            }
765            _ => panic!("Expected InvalidSignature status, got {:?}", result.status),
766        }
767    }
768
769    #[tokio::test]
770    async fn verify_chain_broken_link_returns_broken_chain() {
771        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
772        let root_did = ed25519_did(&root_pk);
773        let (device1_kp, device1_pk) = create_test_keypair(&[2u8; 32]);
774        let device1_did = ed25519_did(&device1_pk);
775        let (device2_kp, device2_pk) = create_test_keypair(&[3u8; 32]);
776        let device2_did = ed25519_did(&device2_pk);
777
778        let att1 = create_signed_attestation(
779            &root_kp,
780            &device1_kp,
781            &root_did,
782            &device1_did,
783            None,
784            Some(fixed_now() + Duration::days(365)),
785        );
786        let att2 = create_signed_attestation(
787            &device1_kp,
788            &device2_kp,
789            &root_did, // WRONG: should be device1_did
790            &device2_did,
791            None,
792            Some(fixed_now() + Duration::days(365)),
793        );
794
795        let result = test_verifier()
796            .verify_chain(&[att1, att2], &ed(&root_pk))
797            .await
798            .unwrap();
799        assert!(!result.is_valid());
800        match result.status {
801            VerificationStatus::BrokenChain { missing_link } => {
802                assert!(missing_link.contains("Issuer mismatch"));
803            }
804            _ => panic!("Expected BrokenChain status, got {:?}", result.status),
805        }
806    }
807
808    #[tokio::test]
809    async fn verify_chain_valid_three_level_chain() {
810        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
811        let root_did = ed25519_did(&root_pk);
812        let (identity_kp, identity_pk) = create_test_keypair(&[2u8; 32]);
813        let identity_did = ed25519_did(&identity_pk);
814        let (device_kp, device_pk) = create_test_keypair(&[3u8; 32]);
815        let device_did = ed25519_did(&device_pk);
816
817        let att1 = create_signed_attestation(
818            &root_kp,
819            &identity_kp,
820            &root_did,
821            &identity_did,
822            None,
823            Some(fixed_now() + Duration::days(365)),
824        );
825        let att2 = create_signed_attestation(
826            &identity_kp,
827            &device_kp,
828            &identity_did,
829            &device_did,
830            None,
831            Some(fixed_now() + Duration::days(365)),
832        );
833
834        let result = test_verifier()
835            .verify_chain(&[att1, att2], &ed(&root_pk))
836            .await
837            .unwrap();
838        assert!(result.is_valid());
839        assert_eq!(result.chain.len(), 2);
840        assert!(result.chain[0].valid);
841        assert!(result.chain[1].valid);
842    }
843
844    #[tokio::test]
845    async fn verify_chain_revoked_intermediate_returns_revoked() {
846        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
847        let root_did = ed25519_did(&root_pk);
848        let (identity_kp, identity_pk) = create_test_keypair(&[2u8; 32]);
849        let identity_did = ed25519_did(&identity_pk);
850        let (device_kp, device_pk) = create_test_keypair(&[3u8; 32]);
851        let device_did = ed25519_did(&device_pk);
852
853        let att1 = create_signed_attestation(
854            &root_kp,
855            &identity_kp,
856            &root_did,
857            &identity_did,
858            None,
859            Some(fixed_now() + Duration::days(365)),
860        );
861        let att2 = create_signed_attestation(
862            &identity_kp,
863            &device_kp,
864            &identity_did,
865            &device_did,
866            Some(fixed_now()),
867            Some(fixed_now() + Duration::days(365)),
868        );
869
870        let result = test_verifier()
871            .verify_chain(&[att1, att2], &ed(&root_pk))
872            .await
873            .unwrap();
874        assert!(!result.is_valid());
875        match result.status {
876            VerificationStatus::Revoked { .. } => {}
877            _ => panic!("Expected Revoked status, got {:?}", result.status),
878        }
879        assert_eq!(result.chain.len(), 2);
880        assert!(result.chain[0].valid);
881        assert!(!result.chain[1].valid);
882    }
883
884    #[tokio::test]
885    async fn verify_at_time_valid_before_expiration() {
886        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
887        let root_did = ed25519_did(&root_pk);
888        let (device_kp, _) = create_test_keypair(&[2u8; 32]);
889        let device_did = ed25519_did(&root_pk);
890
891        let expires = fixed_now() + Duration::days(30);
892        let att = create_signed_attestation(
893            &root_kp,
894            &device_kp,
895            &root_did,
896            &device_did,
897            None,
898            Some(expires),
899        );
900
901        let verification_time = fixed_now() + Duration::days(10);
902        let result = test_verifier()
903            .verify_at_time(&att, &ed(&root_pk), verification_time)
904            .await;
905        assert!(result.is_ok());
906    }
907
908    #[tokio::test]
909    async fn verify_at_time_expired_after_expiration() {
910        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
911        let root_did = ed25519_did(&root_pk);
912        let (device_kp, _) = create_test_keypair(&[2u8; 32]);
913        let device_did = ed25519_did(&root_pk);
914
915        let expires = fixed_now() + Duration::days(30);
916        let att = create_signed_attestation(
917            &root_kp,
918            &device_kp,
919            &root_did,
920            &device_did,
921            None,
922            Some(expires),
923        );
924
925        let verification_time = fixed_now() + Duration::days(60);
926        let result = test_verifier()
927            .verify_at_time(&att, &ed(&root_pk), verification_time)
928            .await;
929        assert!(result.is_err());
930        assert!(result.unwrap_err().to_string().contains("expired"));
931    }
932
933    #[tokio::test]
934    async fn verify_at_time_signature_always_checked() {
935        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
936        let root_did = ed25519_did(&root_pk);
937        let (device_kp, _) = create_test_keypair(&[2u8; 32]);
938        let device_did = ed25519_did(&root_pk);
939
940        let mut att = create_signed_attestation(
941            &root_kp,
942            &device_kp,
943            &root_did,
944            &device_did,
945            None,
946            Some(fixed_now() + Duration::days(365)),
947        );
948        let mut tampered = *att.identity_signature.as_bytes();
949        tampered[0] ^= 0xFF;
950        att.identity_signature = Ed25519Signature::from_bytes(tampered);
951
952        let verification_time = fixed_now() - Duration::days(10);
953        let result = test_verifier()
954            .verify_at_time(&att, &ed(&root_pk), verification_time)
955            .await;
956        assert!(result.is_err());
957        assert!(
958            result
959                .unwrap_err()
960                .to_string()
961                .contains("signature verification failed")
962        );
963    }
964
965    #[tokio::test]
966    async fn verify_at_time_with_past_time_skips_skew_check() {
967        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
968        let root_did = ed25519_did(&root_pk);
969        let (device_kp, _) = create_test_keypair(&[2u8; 32]);
970        let device_did = ed25519_did(&root_pk);
971
972        let att = create_signed_attestation(
973            &root_kp,
974            &device_kp,
975            &root_did,
976            &device_did,
977            None,
978            Some(fixed_now() + Duration::days(365)),
979        );
980
981        let verification_time = fixed_now() - Duration::days(30);
982        let result = test_verifier()
983            .verify_at_time(&att, &ed(&root_pk), verification_time)
984            .await;
985        assert!(result.is_ok());
986    }
987
988    #[tokio::test]
989    async fn verify_with_keys_still_works() {
990        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
991        let root_did = ed25519_did(&root_pk);
992        let (device_kp, _) = create_test_keypair(&[2u8; 32]);
993        let device_did = ed25519_did(&root_pk);
994
995        let att = create_signed_attestation(
996            &root_kp,
997            &device_kp,
998            &root_did,
999            &device_did,
1000            None,
1001            Some(fixed_now() + Duration::days(365)),
1002        );
1003
1004        let result = test_verifier().verify_with_keys(&att, &ed(&root_pk)).await;
1005        assert!(result.is_ok());
1006    }
1007
1008    #[tokio::test]
1009    async fn verify_chain_revoked_and_expired_attestation_is_rejected() {
1010        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
1011        let root_did = ed25519_did(&root_pk);
1012        let (device_kp, device_pk) = create_test_keypair(&[2u8; 32]);
1013        let device_did = ed25519_did(&device_pk);
1014
1015        // Both revoked AND expired: the two negative signals must not cancel — the result
1016        // is a terminal rejection, never Valid.
1017        let att = create_signed_attestation(
1018            &root_kp,
1019            &device_kp,
1020            &root_did,
1021            &device_did,
1022            Some(fixed_now()),
1023            Some(fixed_now() - Duration::days(1)),
1024        );
1025
1026        let result = test_verifier()
1027            .verify_chain(&[att], &ed(&root_pk))
1028            .await
1029            .unwrap();
1030        assert!(!result.is_valid(), "revoked+expired must not be valid");
1031        assert!(
1032            matches!(
1033                result.status,
1034                VerificationStatus::Revoked { .. } | VerificationStatus::Expired { .. }
1035            ),
1036            "expected a terminal revoked/expired status, got {:?}",
1037            result.status
1038        );
1039    }
1040
1041    #[tokio::test]
1042    async fn verify_with_keys_rejects_a_correct_signature_under_the_wrong_key() {
1043        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
1044        let root_did = ed25519_did(&root_pk);
1045        let (device_kp, device_pk) = create_test_keypair(&[2u8; 32]);
1046        let device_did = ed25519_did(&device_pk);
1047
1048        // Validly signed by root_kp, but verified against a DIFFERENT key. A perfectly
1049        // valid signature under the wrong key is still a forgery from the verifier's view —
1050        // distinct from a tampered signature (this one verifies, just not under this key).
1051        let att = create_signed_attestation(
1052            &root_kp,
1053            &device_kp,
1054            &root_did,
1055            &device_did,
1056            None,
1057            Some(fixed_now() + Duration::days(365)),
1058        );
1059        let (_other_kp, other_pk) = create_test_keypair(&[9u8; 32]);
1060
1061        let result = test_verifier().verify_with_keys(&att, &ed(&other_pk)).await;
1062        assert!(
1063            result.is_err(),
1064            "a signature that does not verify under the supplied issuer key must be rejected"
1065        );
1066    }
1067
1068    /// Helper to wrap an attestation as verified (for tests where we created it ourselves).
1069    fn verified(att: Attestation) -> VerifiedAttestation {
1070        VerifiedAttestation::dangerous_from_unchecked(att)
1071    }
1072
1073    #[test]
1074    fn is_device_listed_returns_true_for_valid_attestation() {
1075        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
1076        let root_did = ed25519_did(&root_pk);
1077        let (device_kp, device_pk) = create_test_keypair(&[2u8; 32]);
1078        let device_did_str = ed25519_did(&device_pk);
1079        let device_did = CanonicalDid::new_unchecked(&device_did_str);
1080
1081        let att = create_signed_attestation(
1082            &root_kp,
1083            &device_kp,
1084            &root_did,
1085            &device_did_str,
1086            None,
1087            Some(fixed_now() + Duration::days(365)),
1088        );
1089
1090        assert!(is_device_listed(
1091            &root_did,
1092            &device_did,
1093            &[verified(att)],
1094            fixed_now()
1095        ));
1096    }
1097
1098    #[test]
1099    fn is_device_listed_returns_false_for_no_attestations() {
1100        let (_, root_pk) = create_test_keypair(&[1u8; 32]);
1101        let root_did = ed25519_did(&root_pk);
1102        let (_, device_pk) = create_test_keypair(&[2u8; 32]);
1103        let device_did_str = ed25519_did(&device_pk);
1104        let device_did = CanonicalDid::new_unchecked(&device_did_str);
1105
1106        assert!(!is_device_listed(&root_did, &device_did, &[], fixed_now()));
1107    }
1108
1109    #[test]
1110    fn is_device_listed_returns_false_for_expired_attestation() {
1111        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
1112        let root_did = ed25519_did(&root_pk);
1113        let (device_kp, device_pk) = create_test_keypair(&[2u8; 32]);
1114        let device_did_str = ed25519_did(&device_pk);
1115        let device_did = CanonicalDid::new_unchecked(&device_did_str);
1116
1117        let att = create_signed_attestation(
1118            &root_kp,
1119            &device_kp,
1120            &root_did,
1121            &device_did_str,
1122            None,
1123            Some(fixed_now() - Duration::days(1)),
1124        );
1125
1126        assert!(!is_device_listed(
1127            &root_did,
1128            &device_did,
1129            &[verified(att)],
1130            fixed_now()
1131        ));
1132    }
1133
1134    #[test]
1135    fn is_device_listed_returns_false_for_revoked_attestation() {
1136        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
1137        let root_did = ed25519_did(&root_pk);
1138        let (device_kp, device_pk) = create_test_keypair(&[2u8; 32]);
1139        let device_did_str = ed25519_did(&device_pk);
1140        let device_did = CanonicalDid::new_unchecked(&device_did_str);
1141
1142        let att = create_signed_attestation(
1143            &root_kp,
1144            &device_kp,
1145            &root_did,
1146            &device_did_str,
1147            Some(fixed_now()),
1148            Some(fixed_now() + Duration::days(365)),
1149        );
1150
1151        assert!(!is_device_listed(
1152            &root_did,
1153            &device_did,
1154            &[verified(att)],
1155            fixed_now()
1156        ));
1157    }
1158
1159    #[test]
1160    fn is_device_listed_returns_true_if_one_valid_among_many() {
1161        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
1162        let root_did = ed25519_did(&root_pk);
1163        let (device_kp, device_pk) = create_test_keypair(&[2u8; 32]);
1164        let device_did_str = ed25519_did(&device_pk);
1165        let device_did = CanonicalDid::new_unchecked(&device_did_str);
1166
1167        let att_expired = verified(create_signed_attestation(
1168            &root_kp,
1169            &device_kp,
1170            &root_did,
1171            &device_did_str,
1172            None,
1173            Some(fixed_now() - Duration::days(1)),
1174        ));
1175        let att_revoked = verified(create_signed_attestation(
1176            &root_kp,
1177            &device_kp,
1178            &root_did,
1179            &device_did_str,
1180            Some(fixed_now()),
1181            Some(fixed_now() + Duration::days(365)),
1182        ));
1183        let att_valid = verified(create_signed_attestation(
1184            &root_kp,
1185            &device_kp,
1186            &root_did,
1187            &device_did_str,
1188            None,
1189            Some(fixed_now() + Duration::days(365)),
1190        ));
1191
1192        assert!(is_device_listed(
1193            &root_did,
1194            &device_did,
1195            &[att_expired, att_revoked, att_valid],
1196            fixed_now()
1197        ));
1198    }
1199
1200    #[test]
1201    fn is_device_listed_returns_false_for_wrong_identity() {
1202        let (_, root_pk) = create_test_keypair(&[1u8; 32]);
1203        let root_did = ed25519_did(&root_pk);
1204        let (other_kp, other_pk) = create_test_keypair(&[3u8; 32]);
1205        let other_did = ed25519_did(&other_pk);
1206        let (device_kp, device_pk) = create_test_keypair(&[2u8; 32]);
1207        let device_did_str = ed25519_did(&device_pk);
1208        let device_did = CanonicalDid::new_unchecked(&device_did_str);
1209
1210        let att = create_signed_attestation(
1211            &other_kp,
1212            &device_kp,
1213            &other_did,
1214            &device_did_str,
1215            None,
1216            Some(fixed_now() + Duration::days(365)),
1217        );
1218        assert!(!is_device_listed(
1219            &root_did,
1220            &device_did,
1221            &[verified(att)],
1222            fixed_now()
1223        ));
1224    }
1225
1226    #[test]
1227    fn is_device_listed_returns_false_for_wrong_device() {
1228        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
1229        let root_did = ed25519_did(&root_pk);
1230        let (device_kp, device_pk) = create_test_keypair(&[2u8; 32]);
1231        let device_did_str = ed25519_did(&device_pk);
1232        let (_, other_device_pk) = create_test_keypair(&[4u8; 32]);
1233        let other_device_did_str = ed25519_did(&other_device_pk);
1234        let other_device_did = CanonicalDid::new_unchecked(&other_device_did_str);
1235
1236        let att = create_signed_attestation(
1237            &root_kp,
1238            &device_kp,
1239            &root_did,
1240            &device_did_str,
1241            None,
1242            Some(fixed_now() + Duration::days(365)),
1243        );
1244        assert!(!is_device_listed(
1245            &root_did,
1246            &other_device_did,
1247            &[verified(att)],
1248            fixed_now()
1249        ));
1250    }
1251
1252    /// Helper to create a signed attestation carrying a `delegated_by` link.
1253    fn create_signed_attestation_with_delegation(
1254        issuer_kp: &Ed25519KeyPair,
1255        device_kp: &Ed25519KeyPair,
1256        issuer_did: &str,
1257        subject_did: &str,
1258        delegated_by: Option<CanonicalDid>,
1259    ) -> Attestation {
1260        let device_pk: [u8; 32] = device_kp.public_key().as_ref().try_into().unwrap();
1261
1262        let mut att = Attestation {
1263            version: 1,
1264            rid: ResourceId::new("test-rid"),
1265            issuer: CanonicalDid::new_unchecked(issuer_did),
1266            subject: CanonicalDid::new_unchecked(subject_did),
1267            device_public_key: Ed25519PublicKey::from_bytes(device_pk).into(),
1268            identity_signature: Ed25519Signature::empty(),
1269            device_signature: Ed25519Signature::empty(),
1270            revoked_at: None,
1271            expires_at: Some(fixed_now() + Duration::days(365)),
1272            timestamp: Some(fixed_now()),
1273            note: None,
1274            payload: None,
1275            delegated_by,
1276            signer_type: None,
1277            environment_claim: None,
1278            commit_sha: None,
1279            commit_message: None,
1280            author: None,
1281            oidc_binding: None,
1282        };
1283
1284        let canonical_bytes = canonicalize_attestation_data(&att.canonical_data()).unwrap();
1285
1286        att.identity_signature =
1287            Ed25519Signature::try_from_slice(issuer_kp.sign(&canonical_bytes).as_ref()).unwrap();
1288        att.device_signature =
1289            Ed25519Signature::try_from_slice(device_kp.sign(&canonical_bytes).as_ref()).unwrap();
1290
1291        att
1292    }
1293
1294    #[tokio::test]
1295    async fn verify_attestation_rejects_tampered_delegated_by() {
1296        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
1297        let root_did = ed25519_did(&root_pk);
1298        let (device_kp, device_pk) = create_test_keypair(&[2u8; 32]);
1299        let device_did = ed25519_did(&device_pk);
1300
1301        let mut att = create_signed_attestation_with_delegation(
1302            &root_kp,
1303            &device_kp,
1304            &root_did,
1305            &device_did,
1306            Some(CanonicalDid::new_unchecked("did:keri:Eadmin")),
1307        );
1308        assert!(
1309            test_verifier()
1310                .verify_with_keys(&att, &ed(&root_pk))
1311                .await
1312                .is_ok()
1313        );
1314
1315        att.delegated_by = Some(CanonicalDid::new_unchecked("did:keri:Eattacker"));
1316        let result = test_verifier().verify_with_keys(&att, &ed(&root_pk)).await;
1317        assert!(
1318            result.is_err(),
1319            "Attestation should reject tampered delegated_by"
1320        );
1321    }
1322
1323    #[tokio::test]
1324    async fn verify_attestation_valid_with_delegation() {
1325        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
1326        let root_did = ed25519_did(&root_pk);
1327        let (device_kp, device_pk) = create_test_keypair(&[2u8; 32]);
1328        let device_did = ed25519_did(&device_pk);
1329
1330        let att = create_signed_attestation_with_delegation(
1331            &root_kp,
1332            &device_kp,
1333            &root_did,
1334            &device_did,
1335            Some(CanonicalDid::new_unchecked("did:keri:Eadmin")),
1336        );
1337
1338        let result = test_verifier().verify_with_keys(&att, &ed(&root_pk)).await;
1339        assert!(
1340            result.is_ok(),
1341            "Attestation with delegation should verify: {:?}",
1342            result.err()
1343        );
1344    }
1345
1346    fn create_signed_attestation_with_timestamp(
1347        issuer_kp: &Ed25519KeyPair,
1348        device_kp: &Ed25519KeyPair,
1349        issuer_did: &str,
1350        subject_did: &str,
1351        timestamp: Option<DateTime<Utc>>,
1352    ) -> Attestation {
1353        let device_pk: [u8; 32] = device_kp.public_key().as_ref().try_into().unwrap();
1354
1355        let mut att = Attestation {
1356            version: 1,
1357            rid: ResourceId::new("test-rid"),
1358            issuer: CanonicalDid::new_unchecked(issuer_did),
1359            subject: CanonicalDid::new_unchecked(subject_did),
1360            device_public_key: Ed25519PublicKey::from_bytes(device_pk).into(),
1361            identity_signature: Ed25519Signature::empty(),
1362            device_signature: Ed25519Signature::empty(),
1363            revoked_at: None,
1364            expires_at: Some(fixed_now() + Duration::days(365)),
1365            timestamp,
1366            note: None,
1367            payload: None,
1368            delegated_by: None,
1369            signer_type: None,
1370            environment_claim: None,
1371            commit_sha: None,
1372            commit_message: None,
1373            author: None,
1374            oidc_binding: None,
1375        };
1376
1377        let canonical_bytes = canonicalize_attestation_data(&att.canonical_data()).unwrap();
1378
1379        att.identity_signature =
1380            Ed25519Signature::try_from_slice(issuer_kp.sign(&canonical_bytes).as_ref()).unwrap();
1381        att.device_signature =
1382            Ed25519Signature::try_from_slice(device_kp.sign(&canonical_bytes).as_ref()).unwrap();
1383
1384        att
1385    }
1386
1387    #[tokio::test]
1388    async fn verify_attestation_created_1_hour_ago_succeeds() {
1389        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
1390        let root_did = ed25519_did(&root_pk);
1391        let (device_kp, device_pk) = create_test_keypair(&[2u8; 32]);
1392        let device_did = ed25519_did(&device_pk);
1393
1394        let one_hour_ago = fixed_now() - Duration::hours(1);
1395        let att = create_signed_attestation_with_timestamp(
1396            &root_kp,
1397            &device_kp,
1398            &root_did,
1399            &device_did,
1400            Some(one_hour_ago),
1401        );
1402
1403        let result = test_verifier().verify_with_keys(&att, &ed(&root_pk)).await;
1404        assert!(
1405            result.is_ok(),
1406            "Attestation created 1 hour ago should verify: {:?}",
1407            result.err()
1408        );
1409    }
1410
1411    #[tokio::test]
1412    async fn verify_attestation_created_30_days_ago_succeeds() {
1413        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
1414        let root_did = ed25519_did(&root_pk);
1415        let (device_kp, device_pk) = create_test_keypair(&[2u8; 32]);
1416        let device_did = ed25519_did(&device_pk);
1417
1418        let thirty_days_ago = fixed_now() - Duration::days(30);
1419        let att = create_signed_attestation_with_timestamp(
1420            &root_kp,
1421            &device_kp,
1422            &root_did,
1423            &device_did,
1424            Some(thirty_days_ago),
1425        );
1426
1427        let result = test_verifier().verify_with_keys(&att, &ed(&root_pk)).await;
1428        assert!(
1429            result.is_ok(),
1430            "Attestation created 30 days ago should verify: {:?}",
1431            result.err()
1432        );
1433    }
1434
1435    #[tokio::test]
1436    async fn verify_attestation_with_future_timestamp_fails() {
1437        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
1438        let root_did = ed25519_did(&root_pk);
1439        let (device_kp, device_pk) = create_test_keypair(&[2u8; 32]);
1440        let device_did = ed25519_did(&device_pk);
1441
1442        let ten_minutes_future = fixed_now() + Duration::minutes(10);
1443        let att = create_signed_attestation_with_timestamp(
1444            &root_kp,
1445            &device_kp,
1446            &root_did,
1447            &device_did,
1448            Some(ten_minutes_future),
1449        );
1450
1451        let result = test_verifier().verify_with_keys(&att, &ed(&root_pk)).await;
1452        assert!(
1453            result.is_err(),
1454            "Attestation with future timestamp should fail"
1455        );
1456        let err_msg = result.unwrap_err().to_string();
1457        assert!(
1458            err_msg.contains("in the future"),
1459            "Error should mention 'in the future': {}",
1460            err_msg
1461        );
1462    }
1463
1464    #[tokio::test]
1465    async fn verify_device_authorization_returns_valid_for_signed_attestation() {
1466        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
1467        let root_did = ed25519_did(&root_pk);
1468        let (device_kp, device_pk) = create_test_keypair(&[2u8; 32]);
1469        let device_did_str = ed25519_did(&device_pk);
1470        let device_did = CanonicalDid::new_unchecked(&device_did_str);
1471
1472        let att = create_signed_attestation(
1473            &root_kp,
1474            &device_kp,
1475            &root_did,
1476            &device_did_str,
1477            None,
1478            Some(fixed_now() + Duration::days(365)),
1479        );
1480
1481        let result = test_verifier()
1482            .verify_device_authorization(&root_did, &device_did, &[att], &ed(&root_pk))
1483            .await;
1484        assert!(result.is_ok());
1485        let report = result.unwrap();
1486        assert!(report.is_valid());
1487        assert_eq!(report.chain.len(), 1);
1488        assert!(report.chain[0].valid);
1489    }
1490
1491    #[tokio::test]
1492    async fn verify_device_authorization_returns_broken_chain_for_no_attestations() {
1493        let (_, root_pk) = create_test_keypair(&[1u8; 32]);
1494        let root_did = ed25519_did(&root_pk);
1495        let (_, device_pk) = create_test_keypair(&[2u8; 32]);
1496        let device_did_str = ed25519_did(&device_pk);
1497        let device_did = CanonicalDid::new_unchecked(&device_did_str);
1498
1499        let result = test_verifier()
1500            .verify_device_authorization(&root_did, &device_did, &[], &ed(&root_pk))
1501            .await;
1502        assert!(result.is_ok());
1503        let report = result.unwrap();
1504        assert!(!report.is_valid());
1505        match report.status {
1506            VerificationStatus::BrokenChain { missing_link } => {
1507                assert!(missing_link.contains("No attestation found"));
1508            }
1509            _ => panic!("Expected BrokenChain status, got {:?}", report.status),
1510        }
1511    }
1512
1513    #[tokio::test]
1514    async fn verify_device_authorization_fails_for_forged_signature() {
1515        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
1516        let root_did = ed25519_did(&root_pk);
1517        let (device_kp, device_pk) = create_test_keypair(&[2u8; 32]);
1518        let device_did_str = ed25519_did(&device_pk);
1519        let device_did = CanonicalDid::new_unchecked(&device_did_str);
1520
1521        let mut att = create_signed_attestation(
1522            &root_kp,
1523            &device_kp,
1524            &root_did,
1525            &device_did_str,
1526            None,
1527            Some(fixed_now() + Duration::days(365)),
1528        );
1529        let mut tampered = *att.identity_signature.as_bytes();
1530        tampered[0] ^= 0xFF;
1531        att.identity_signature = Ed25519Signature::from_bytes(tampered);
1532
1533        let result = test_verifier()
1534            .verify_device_authorization(&root_did, &device_did, &[att], &ed(&root_pk))
1535            .await;
1536        assert!(result.is_ok());
1537        let report = result.unwrap();
1538        assert!(!report.is_valid());
1539        match report.status {
1540            VerificationStatus::InvalidSignature { step } => assert_eq!(step, 0),
1541            _ => panic!("Expected InvalidSignature status, got {:?}", report.status),
1542        }
1543    }
1544
1545    #[tokio::test]
1546    async fn verify_device_authorization_fails_for_wrong_issuer_key() {
1547        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
1548        let root_did = ed25519_did(&root_pk);
1549        let (device_kp, device_pk) = create_test_keypair(&[2u8; 32]);
1550        let device_did_str = ed25519_did(&device_pk);
1551        let device_did = CanonicalDid::new_unchecked(&device_did_str);
1552        let (_, wrong_pk) = create_test_keypair(&[99u8; 32]);
1553
1554        let att = create_signed_attestation(
1555            &root_kp,
1556            &device_kp,
1557            &root_did,
1558            &device_did_str,
1559            None,
1560            Some(fixed_now() + Duration::days(365)),
1561        );
1562
1563        let result = test_verifier()
1564            .verify_device_authorization(&root_did, &device_did, &[att], &ed(&wrong_pk))
1565            .await;
1566        assert!(result.is_ok());
1567        let report = result.unwrap();
1568        assert!(!report.is_valid());
1569        match report.status {
1570            VerificationStatus::InvalidSignature { .. } => {}
1571            _ => panic!("Expected InvalidSignature status, got {:?}", report.status),
1572        }
1573    }
1574
1575    #[tokio::test]
1576    async fn verify_device_authorization_checks_expiry_and_revocation() {
1577        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
1578        let root_did = ed25519_did(&root_pk);
1579        let (device_kp, device_pk) = create_test_keypair(&[2u8; 32]);
1580        let device_did_str = ed25519_did(&device_pk);
1581        let device_did = CanonicalDid::new_unchecked(&device_did_str);
1582
1583        let att_expired = create_signed_attestation(
1584            &root_kp,
1585            &device_kp,
1586            &root_did,
1587            &device_did_str,
1588            None,
1589            Some(fixed_now() - Duration::days(1)),
1590        );
1591        let result = test_verifier()
1592            .verify_device_authorization(&root_did, &device_did, &[att_expired], &ed(&root_pk))
1593            .await;
1594        assert!(result.is_ok());
1595        let report = result.unwrap();
1596        assert!(!report.is_valid());
1597        match report.status {
1598            VerificationStatus::Expired { .. } => {}
1599            _ => panic!("Expected Expired status, got {:?}", report.status),
1600        }
1601
1602        let att_revoked = create_signed_attestation(
1603            &root_kp,
1604            &device_kp,
1605            &root_did,
1606            &device_did_str,
1607            Some(fixed_now()),
1608            Some(fixed_now() + Duration::days(365)),
1609        );
1610        let result = test_verifier()
1611            .verify_device_authorization(&root_did, &device_did, &[att_revoked], &ed(&root_pk))
1612            .await;
1613        assert!(result.is_ok());
1614        let report = result.unwrap();
1615        assert!(!report.is_valid());
1616        match report.status {
1617            VerificationStatus::Revoked { .. } => {}
1618            _ => panic!("Expected Revoked status, got {:?}", report.status),
1619        }
1620    }
1621
1622    fn create_witness_receipt(
1623        witness_kp: &Ed25519KeyPair,
1624        witness_did: &str,
1625        event_said: &str,
1626        seq: u128,
1627    ) -> auths_keri::witness::SignedReceipt {
1628        let receipt = auths_keri::witness::Receipt {
1629            v: auths_keri::VersionString::placeholder(),
1630            t: auths_keri::witness::ReceiptTag,
1631            d: Said::new_unchecked(event_said.to_string()),
1632            i: auths_keri::Prefix::new_unchecked(witness_did.to_string()),
1633            s: auths_keri::KeriSequence::new(seq),
1634        };
1635        let payload = serde_json::to_vec(&receipt).unwrap();
1636        let sig = witness_kp.sign(&payload).as_ref().to_vec();
1637        auths_keri::witness::SignedReceipt {
1638            receipt,
1639            signature: sig,
1640        }
1641    }
1642
1643    #[tokio::test]
1644    async fn verify_chain_with_witnesses_valid() {
1645        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
1646        let root_did = ed25519_did(&root_pk);
1647        let (device_kp, device_pk) = create_test_keypair(&[2u8; 32]);
1648        let device_did = ed25519_did(&device_pk);
1649
1650        let att = create_signed_attestation(
1651            &root_kp,
1652            &device_kp,
1653            &root_did,
1654            &device_did,
1655            None,
1656            Some(fixed_now() + Duration::days(365)),
1657        );
1658
1659        let (w1_kp, w1_pk) = create_test_keypair(&[10u8; 32]);
1660        let (w2_kp, w2_pk) = create_test_keypair(&[20u8; 32]);
1661
1662        let r1 = create_witness_receipt(&w1_kp, "did:key:w1", "EEvent1", 1);
1663        let r2 = create_witness_receipt(&w2_kp, "did:key:w2", "EEvent1", 1);
1664
1665        let witness_keys = vec![
1666            ("did:key:w1".into(), w1_pk.to_vec()),
1667            ("did:key:w2".into(), w2_pk.to_vec()),
1668        ];
1669
1670        let config = crate::witness::WitnessVerifyConfig {
1671            receipts: &[r1, r2],
1672            witness_keys: &witness_keys,
1673            threshold: 2,
1674        };
1675
1676        let report = test_verifier()
1677            .verify_chain_with_witnesses(&[att], &ed(&root_pk), &config)
1678            .await
1679            .unwrap();
1680        assert!(report.is_valid());
1681        assert!(report.witness_quorum.is_some());
1682        let quorum = report.witness_quorum.unwrap();
1683        assert_eq!(quorum.required, 2);
1684        assert_eq!(quorum.verified, 2);
1685    }
1686
1687    #[tokio::test]
1688    async fn verify_chain_with_witnesses_chain_fails() {
1689        let (w1_kp, w1_pk) = create_test_keypair(&[10u8; 32]);
1690        let r1 = create_witness_receipt(&w1_kp, "did:key:w1", "EEvent1", 1);
1691        let witness_keys = vec![("did:key:w1".into(), w1_pk.to_vec())];
1692        let config = crate::witness::WitnessVerifyConfig {
1693            receipts: &[r1],
1694            witness_keys: &witness_keys,
1695            threshold: 1,
1696        };
1697
1698        let report = test_verifier()
1699            .verify_chain_with_witnesses(&[], &ed(&[0u8; 32]), &config)
1700            .await
1701            .unwrap();
1702        assert!(!report.is_valid());
1703        match report.status {
1704            VerificationStatus::BrokenChain { .. } => {}
1705            _ => panic!("Expected BrokenChain, got {:?}", report.status),
1706        }
1707        assert!(report.witness_quorum.is_none());
1708    }
1709
1710    #[tokio::test]
1711    async fn verify_chain_with_witnesses_quorum_fails() {
1712        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
1713        let root_did = ed25519_did(&root_pk);
1714        let (device_kp, device_pk) = create_test_keypair(&[2u8; 32]);
1715        let device_did = ed25519_did(&device_pk);
1716
1717        let att = create_signed_attestation(
1718            &root_kp,
1719            &device_kp,
1720            &root_did,
1721            &device_did,
1722            None,
1723            Some(fixed_now() + Duration::days(365)),
1724        );
1725
1726        let (w1_kp, w1_pk) = create_test_keypair(&[10u8; 32]);
1727        let r1 = create_witness_receipt(&w1_kp, "did:key:w1", "EEvent1", 1);
1728        let witness_keys = vec![("did:key:w1".into(), w1_pk.to_vec())];
1729        let config = crate::witness::WitnessVerifyConfig {
1730            receipts: &[r1],
1731            witness_keys: &witness_keys,
1732            threshold: 2,
1733        };
1734
1735        let report = test_verifier()
1736            .verify_chain_with_witnesses(&[att], &ed(&root_pk), &config)
1737            .await
1738            .unwrap();
1739        assert!(!report.is_valid());
1740        match report.status {
1741            VerificationStatus::InsufficientWitnesses { required, verified } => {
1742                assert_eq!(required, 2);
1743                assert_eq!(verified, 1);
1744            }
1745            _ => panic!("Expected InsufficientWitnesses, got {:?}", report.status),
1746        }
1747        assert!(report.witness_quorum.is_some());
1748        assert!(!report.warnings.is_empty());
1749    }
1750
1751    /// Verify an attestation signed by a P-256 identity using a P-256 `DevicePublicKey`.
1752    ///
1753    /// Reproduces the production CI flow where a P-256 identity signs an Ed25519-device
1754    /// attestation, and ensures the curve-dispatched verifier accepts a 33-byte compressed
1755    /// P-256 public key.
1756    #[tokio::test]
1757    async fn verify_p256_identity_signed_attestation() {
1758        use auths_crypto::RingCryptoProvider;
1759
1760        // Generate a P-256 identity keypair (compressed 33-byte pubkey).
1761        let (p256_seed, p256_pk_bytes) = RingCryptoProvider::p256_generate().unwrap();
1762        assert_eq!(
1763            p256_pk_bytes.len(),
1764            33,
1765            "P-256 compressed pubkey is 33 bytes"
1766        );
1767        let seed_arr: [u8; 32] = *p256_seed.as_bytes();
1768
1769        // Use a KERI-style DID for the issuer; the DID value is opaque to the verifier
1770        // (it does not re-derive the issuer key from the DID).
1771        let issuer_did = "did:keri:Etest-p256-identity";
1772
1773        // Device remains Ed25519 for this test — matches real-world usage.
1774        let (device_kp, device_pk) = create_test_keypair(&[42u8; 32]);
1775        let device_did = ed25519_did(&device_pk);
1776
1777        let mut att = Attestation {
1778            version: 1,
1779            rid: ResourceId::new("test-rid"),
1780            issuer: CanonicalDid::new_unchecked(issuer_did),
1781            subject: CanonicalDid::new_unchecked(&device_did),
1782            device_public_key: Ed25519PublicKey::from_bytes(device_pk).into(),
1783            identity_signature: Ed25519Signature::empty(),
1784            device_signature: Ed25519Signature::empty(),
1785            revoked_at: None,
1786            expires_at: Some(fixed_now() + Duration::days(365)),
1787            timestamp: Some(fixed_now()),
1788            note: None,
1789            payload: None,
1790            delegated_by: None,
1791            signer_type: None,
1792            environment_claim: None,
1793            commit_sha: None,
1794            commit_message: None,
1795            author: None,
1796            oidc_binding: None,
1797        };
1798
1799        let canonical_bytes = canonicalize_attestation_data(&att.canonical_data()).unwrap();
1800
1801        // P-256 identity signature over canonical bytes (64 bytes: r||s).
1802        let p256_sig = RingCryptoProvider::p256_sign(&seed_arr, &canonical_bytes).unwrap();
1803        // Ed25519Signature holds exactly 64 bytes, matching P-256 r||s.
1804        assert_eq!(p256_sig.len(), 64);
1805        att.identity_signature = Ed25519Signature::try_from_slice(&p256_sig).unwrap();
1806
1807        // Device signature is Ed25519.
1808        att.device_signature =
1809            Ed25519Signature::try_from_slice(device_kp.sign(&canonical_bytes).as_ref()).unwrap();
1810
1811        let issuer_dpk =
1812            DevicePublicKey::try_new(auths_crypto::CurveType::P256, &p256_pk_bytes).unwrap();
1813
1814        let result = test_verifier().verify_with_keys(&att, &issuer_dpk).await;
1815        assert!(
1816            result.is_ok(),
1817            "P-256-signed attestation should verify with a P-256 DevicePublicKey: {:?}",
1818            result.err()
1819        );
1820    }
1821}