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    let key_state = match auths_keri::validate_kel(events) {
216        Ok(ks) => ks,
217        Err(e) => return DeviceLinkVerification::failure(format!("KEL verification failed: {e}")),
218    };
219
220    if attestation.subject.as_str() != device_did {
221        return DeviceLinkVerification::failure(format!(
222            "Device DID mismatch: attestation subject is '{}', expected '{device_did}'",
223            attestation.subject
224        ));
225    }
226
227    let current_pk = match key_state.current_keys.first() {
228        Some(encoded) => match auths_keri::KeriPublicKey::parse(encoded.as_str()) {
229            Ok(keri_pk) => {
230                // Curve travels with the key (CESR prefix) — never hardcode it; P-256
231                // device keys must verify too (the workspace default curve).
232                let curve = keri_pk.curve();
233                let bytes = keri_pk.into_bytes().to_vec();
234                match DevicePublicKey::try_new(curve, &bytes) {
235                    Ok(dpk) => dpk,
236                    Err(e) => {
237                        return DeviceLinkVerification::failure(format!(
238                            "Invalid current key: {e}"
239                        ));
240                    }
241                }
242            }
243            Err(e) => {
244                return DeviceLinkVerification::failure(format!("Invalid current key: {e}"));
245            }
246        },
247        None => return DeviceLinkVerification::failure("KEL has no current keys"),
248    };
249
250    if let Err(e) = verify_with_keys_at(attestation, &current_pk, now, true, provider).await {
251        return DeviceLinkVerification::failure(format!("Attestation verification failed: {e}"));
252    }
253
254    let seal_sequence = compute_attestation_seal_digest(attestation)
255        .ok()
256        .and_then(|digest| find_seal_in_kel(events, digest.as_str()));
257
258    DeviceLinkVerification::success(key_state, seal_sequence)
259}
260
261/// Compute the KERI SAID (Blake3 digest) of an attestation's canonical form.
262///
263/// This is the digest that should appear in a KEL IXN seal when the attestation
264/// is anchored. Returns the SAID string (E-prefixed base64url Blake3).
265pub fn compute_attestation_seal_digest(
266    attestation: &Attestation,
267) -> Result<String, AttestationError> {
268    let canonical = canonicalize_attestation_data(&attestation.canonical_data())?;
269    let value: serde_json::Value = serde_json::from_slice(&canonical)
270        .map_err(|e| AttestationError::SerializationError(e.to_string()))?;
271    Ok(compute_said(&value)
272        .map_err(|e| AttestationError::SerializationError(e.to_string()))?
273        .into_inner())
274}
275
276// ---------------------------------------------------------------------------
277// Internal async functions — used by Verifier and free function wrappers
278// ---------------------------------------------------------------------------
279
280pub(crate) async fn verify_with_keys_at(
281    att: &Attestation,
282    issuer_pk: &DevicePublicKey,
283    at: DateTime<Utc>,
284    check_skew: bool,
285    provider: &dyn CryptoProvider,
286) -> Result<(), AttestationError> {
287    let reference_time = at;
288
289    // --- 1. Check revocation (time-aware) ---
290    if let Some(revoked_at) = att.revoked_at
291        && revoked_at <= reference_time
292    {
293        return Err(AttestationError::AttestationRevoked);
294    }
295
296    // --- 2. Check expiration against reference time ---
297    if let Some(exp) = att.expires_at
298        && reference_time > exp
299    {
300        return Err(AttestationError::AttestationExpired {
301            at: exp.to_rfc3339(),
302        });
303    }
304
305    // --- 3. Check timestamp skew against reference time ---
306    if check_skew
307        && let Some(ts) = att.timestamp
308        && ts > reference_time + Duration::seconds(MAX_SKEW_SECS)
309    {
310        return Err(AttestationError::TimestampInFuture {
311            at: ts.to_rfc3339(),
312        });
313    }
314
315    // --- 4. Reconstruct and canonicalize data ---
316    let canonical_json_bytes = canonicalize_attestation_data(&att.canonical_data())?;
317    let data_to_verify = canonical_json_bytes.as_slice();
318    debug!(
319        "(Verify) Canonical data: {}",
320        String::from_utf8_lossy(&canonical_json_bytes)
321    );
322
323    // --- 5. Verify issuer signature (dispatched on curve) ---
324    if !att.identity_signature.is_empty() {
325        verify_signature_by_curve(
326            issuer_pk,
327            data_to_verify,
328            att.identity_signature.as_bytes(),
329            provider,
330            SignatureRole::Issuer,
331        )
332        .await?;
333        debug!("(Verify) Issuer signature verified successfully.");
334    } else {
335        debug!(
336            "(Verify) No identity signature present (device-only attestation), skipping issuer check."
337        );
338    }
339
340    // --- 6. Verify device signature (dispatched on curve) ---
341    verify_signature_by_curve(
342        &att.device_public_key,
343        data_to_verify,
344        att.device_signature.as_bytes(),
345        provider,
346        SignatureRole::Device,
347    )
348    .await?;
349    debug!("(Verify) Device signature verified successfully.");
350
351    Ok(())
352}
353
354/// Which signature slot a curve-dispatch error should be attributed to.
355#[derive(Clone, Copy)]
356enum SignatureRole {
357    Issuer,
358    Device,
359}
360
361/// Verify a signature via the canonical `DevicePublicKey::verify`, attributing
362/// failures to the appropriate role (issuer vs device) at the boundary.
363///
364/// Thin wrapper preserved so that existing chain-verification code can keep
365/// its role-attributed error enum; the dispatch itself now lives in
366/// `DevicePublicKey::verify` (fn-114.14).
367async fn verify_signature_by_curve(
368    pk: &DevicePublicKey,
369    message: &[u8],
370    signature: &[u8],
371    provider: &dyn CryptoProvider,
372    role: SignatureRole,
373) -> Result<(), AttestationError> {
374    let map_err = |e: String| match role {
375        SignatureRole::Issuer => AttestationError::IssuerSignatureFailed(e),
376        SignatureRole::Device => AttestationError::DeviceSignatureFailed(e),
377    };
378
379    pk.verify(message, signature, provider)
380        .await
381        .map_err(|e| map_err(e.to_string()))
382}
383
384pub(crate) async fn verify_chain_inner(
385    attestations: &[Attestation],
386    root_pk: &DevicePublicKey,
387    provider: &dyn CryptoProvider,
388    now: DateTime<Utc>,
389) -> Result<VerificationReport, AttestationError> {
390    if attestations.is_empty() {
391        return Ok(VerificationReport::with_status(
392            VerificationStatus::BrokenChain {
393                missing_link: "empty chain".to_string(),
394            },
395            vec![],
396        ));
397    }
398
399    let mut chain_links: Vec<ChainLink> = Vec::with_capacity(attestations.len());
400
401    let first_att = &attestations[0];
402    match verify_single_attestation(first_att, root_pk, 0, provider, now).await {
403        Ok(link) => chain_links.push(link),
404        Err((status, link)) => {
405            chain_links.push(link);
406            return Ok(VerificationReport::with_status(status, chain_links));
407        }
408    }
409
410    for (idx, att) in attestations.iter().enumerate().skip(1) {
411        let prev_att = &attestations[idx - 1];
412
413        if att.issuer.as_str() != prev_att.subject.as_str() {
414            let link = ChainLink::invalid(
415                att.issuer.to_string(),
416                att.subject.to_string(),
417                format!(
418                    "Chain broken: expected issuer '{}', got '{}'",
419                    prev_att.subject, att.issuer
420                ),
421            );
422            chain_links.push(link);
423            return Ok(VerificationReport::with_status(
424                VerificationStatus::BrokenChain {
425                    missing_link: format!(
426                        "Issuer mismatch at step {}: expected '{}', got '{}'",
427                        idx, prev_att.subject, att.issuer
428                    ),
429                },
430                chain_links,
431            ));
432        }
433
434        let issuer_pk = &prev_att.device_public_key;
435
436        match verify_single_attestation(att, issuer_pk, idx, provider, now).await {
437            Ok(link) => chain_links.push(link),
438            Err((status, link)) => {
439                chain_links.push(link);
440                return Ok(VerificationReport::with_status(status, chain_links));
441            }
442        }
443    }
444
445    Ok(VerificationReport::valid(chain_links))
446}
447
448pub(crate) async fn verify_device_authorization_inner(
449    identity_did: &str,
450    device_did: &CanonicalDid,
451    attestations: &[Attestation],
452    identity_pk: &DevicePublicKey,
453    provider: &dyn CryptoProvider,
454    now: DateTime<Utc>,
455) -> Result<VerificationReport, AttestationError> {
456    let device_did_str = device_did.to_string();
457
458    let matching: Vec<&Attestation> = attestations
459        .iter()
460        .filter(|a| a.issuer == identity_did && a.subject.to_string() == device_did_str)
461        .collect();
462
463    if matching.is_empty() {
464        return Ok(VerificationReport::with_status(
465            VerificationStatus::BrokenChain {
466                missing_link: format!(
467                    "No attestation found for device {} under {}",
468                    device_did_str, identity_did
469                ),
470            },
471            vec![],
472        ));
473    }
474
475    match verify_single_attestation(matching[0], identity_pk, 0, provider, now).await {
476        Ok(link) => Ok(VerificationReport::valid(vec![link])),
477        Err((status, link)) => Ok(VerificationReport::with_status(status, vec![link])),
478    }
479}
480
481async fn verify_single_attestation(
482    att: &Attestation,
483    issuer_pk: &DevicePublicKey,
484    step: usize,
485    provider: &dyn CryptoProvider,
486    now: DateTime<Utc>,
487) -> Result<ChainLink, (VerificationStatus, ChainLink)> {
488    let issuer = att.issuer.to_string();
489    let subject = att.subject.to_string();
490
491    if att.is_revoked() {
492        return Err((
493            VerificationStatus::Revoked { at: att.revoked_at },
494            ChainLink::invalid(issuer, subject, "Attestation revoked".to_string()),
495        ));
496    }
497
498    if let Some(exp) = att.expires_at
499        && now > exp
500    {
501        return Err((
502            VerificationStatus::Expired { at: exp },
503            ChainLink::invalid(
504                issuer,
505                subject,
506                format!("Attestation expired on {}", exp.to_rfc3339()),
507            ),
508        ));
509    }
510
511    match verify_with_keys_at(att, issuer_pk, now, true, provider).await {
512        Ok(()) => Ok(ChainLink::valid(issuer, subject)),
513        Err(e) => Err((
514            VerificationStatus::InvalidSignature { step },
515            ChainLink::invalid(issuer, subject, e.to_string()),
516        )),
517    }
518}
519
520#[cfg(all(test, not(target_arch = "wasm32")))]
521#[allow(clippy::unwrap_used, clippy::expect_used, clippy::disallowed_methods)]
522mod tests {
523    use super::*;
524    use crate::clock::ClockProvider;
525    use crate::core::{Ed25519PublicKey, Ed25519Signature, ResourceId};
526    use crate::types::CanonicalDid;
527    use crate::verifier::Verifier;
528    use auths_crypto::RingCryptoProvider;
529    use auths_crypto::testing::create_test_keypair;
530    use auths_keri::Said;
531    use chrono::{DateTime, Duration, TimeZone, Utc};
532    use ring::signature::{Ed25519KeyPair, KeyPair};
533    use std::sync::Arc;
534
535    /// Wrap a raw 32-byte Ed25519 key into a `DevicePublicKey` for tests.
536    fn ed(pk: &[u8]) -> DevicePublicKey {
537        DevicePublicKey::try_new(auths_crypto::CurveType::Ed25519, pk).unwrap()
538    }
539
540    /// Build a `did:key:z...` string from a 32-byte Ed25519 public key (test helper).
541    fn ed25519_did(pk: &[u8; 32]) -> String {
542        CanonicalDid::from_public_key_did_key(pk, auths_crypto::CurveType::Ed25519).to_string()
543    }
544
545    struct TestClock(DateTime<Utc>);
546    impl ClockProvider for TestClock {
547        fn now(&self) -> DateTime<Utc> {
548            self.0
549        }
550    }
551
552    fn fixed_now() -> DateTime<Utc> {
553        Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap()
554    }
555
556    fn test_verifier() -> Verifier {
557        Verifier::new(
558            Arc::new(RingCryptoProvider),
559            Arc::new(TestClock(fixed_now())),
560        )
561    }
562
563    /// Helper to create a signed attestation
564    fn create_signed_attestation(
565        issuer_kp: &Ed25519KeyPair,
566        device_kp: &Ed25519KeyPair,
567        issuer_did: &str,
568        subject_did: &str,
569        revoked_at: Option<DateTime<Utc>>,
570        expires_at: Option<DateTime<Utc>>,
571    ) -> Attestation {
572        let device_pk: [u8; 32] = device_kp.public_key().as_ref().try_into().unwrap();
573
574        let mut att = Attestation {
575            version: 1,
576            rid: ResourceId::new("test-rid"),
577            issuer: CanonicalDid::new_unchecked(issuer_did),
578            subject: CanonicalDid::new_unchecked(subject_did),
579            device_public_key: Ed25519PublicKey::from_bytes(device_pk).into(),
580            identity_signature: Ed25519Signature::empty(),
581            device_signature: Ed25519Signature::empty(),
582            revoked_at,
583            expires_at,
584            timestamp: Some(fixed_now()),
585            note: None,
586            payload: None,
587            delegated_by: None,
588            signer_type: None,
589            environment_claim: None,
590            commit_sha: None,
591            commit_message: None,
592            author: None,
593            oidc_binding: None,
594        };
595
596        let canonical_bytes = canonicalize_attestation_data(&att.canonical_data()).unwrap();
597
598        att.identity_signature =
599            Ed25519Signature::try_from_slice(issuer_kp.sign(&canonical_bytes).as_ref()).unwrap();
600        att.device_signature =
601            Ed25519Signature::try_from_slice(device_kp.sign(&canonical_bytes).as_ref()).unwrap();
602
603        att
604    }
605
606    #[tokio::test]
607    async fn verify_chain_empty_returns_broken_chain() {
608        let result = test_verifier()
609            .verify_chain(&[], &ed(&[0u8; 32]))
610            .await
611            .unwrap();
612        assert!(!result.is_valid());
613        match result.status {
614            VerificationStatus::BrokenChain { missing_link } => {
615                assert_eq!(missing_link, "empty chain");
616            }
617            _ => panic!("Expected BrokenChain status"),
618        }
619        assert!(result.chain.is_empty());
620    }
621
622    #[tokio::test]
623    async fn verify_chain_single_valid_attestation() {
624        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
625        let root_did = ed25519_did(&root_pk);
626        let (device_kp, device_pk) = create_test_keypair(&[2u8; 32]);
627        let device_did = ed25519_did(&device_pk);
628
629        let att = create_signed_attestation(
630            &root_kp,
631            &device_kp,
632            &root_did,
633            &device_did,
634            None,
635            Some(fixed_now() + Duration::days(365)),
636        );
637
638        let result = test_verifier()
639            .verify_chain(&[att], &ed(&root_pk))
640            .await
641            .unwrap();
642        assert!(result.is_valid());
643        assert_eq!(result.chain.len(), 1);
644        assert!(result.chain[0].valid);
645    }
646
647    #[tokio::test]
648    async fn verify_chain_revoked_attestation_returns_revoked() {
649        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
650        let root_did = ed25519_did(&root_pk);
651        let (device_kp, device_pk) = create_test_keypair(&[2u8; 32]);
652        let device_did = ed25519_did(&device_pk);
653
654        let att = create_signed_attestation(
655            &root_kp,
656            &device_kp,
657            &root_did,
658            &device_did,
659            Some(fixed_now()),
660            Some(fixed_now() + Duration::days(365)),
661        );
662
663        let result = test_verifier()
664            .verify_chain(&[att], &ed(&root_pk))
665            .await
666            .unwrap();
667        assert!(!result.is_valid());
668        match result.status {
669            VerificationStatus::Revoked { .. } => {}
670            _ => panic!("Expected Revoked status, got {:?}", result.status),
671        }
672    }
673
674    #[tokio::test]
675    async fn verify_chain_expired_attestation_returns_expired() {
676        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
677        let root_did = ed25519_did(&root_pk);
678        let (device_kp, device_pk) = create_test_keypair(&[2u8; 32]);
679        let device_did = ed25519_did(&device_pk);
680
681        let att = create_signed_attestation(
682            &root_kp,
683            &device_kp,
684            &root_did,
685            &device_did,
686            None,
687            Some(fixed_now() - Duration::days(1)),
688        );
689
690        let result = test_verifier()
691            .verify_chain(&[att], &ed(&root_pk))
692            .await
693            .unwrap();
694        assert!(!result.is_valid());
695        match result.status {
696            VerificationStatus::Expired { .. } => {}
697            _ => panic!("Expected Expired status, got {:?}", result.status),
698        }
699    }
700
701    #[tokio::test]
702    async fn verify_chain_invalid_signature_returns_invalid_signature() {
703        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
704        let root_did = ed25519_did(&root_pk);
705        let (device_kp, device_pk) = create_test_keypair(&[2u8; 32]);
706        let device_did = ed25519_did(&device_pk);
707
708        let mut att = create_signed_attestation(
709            &root_kp,
710            &device_kp,
711            &root_did,
712            &device_did,
713            None,
714            Some(fixed_now() + Duration::days(365)),
715        );
716        let mut tampered = *att.identity_signature.as_bytes();
717        tampered[0] ^= 0xFF;
718        att.identity_signature = Ed25519Signature::from_bytes(tampered);
719
720        let result = test_verifier()
721            .verify_chain(&[att], &ed(&root_pk))
722            .await
723            .unwrap();
724        assert!(!result.is_valid());
725        match result.status {
726            VerificationStatus::InvalidSignature { step } => {
727                assert_eq!(step, 0);
728            }
729            _ => panic!("Expected InvalidSignature status, got {:?}", result.status),
730        }
731    }
732
733    #[tokio::test]
734    async fn verify_chain_broken_link_returns_broken_chain() {
735        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
736        let root_did = ed25519_did(&root_pk);
737        let (device1_kp, device1_pk) = create_test_keypair(&[2u8; 32]);
738        let device1_did = ed25519_did(&device1_pk);
739        let (device2_kp, device2_pk) = create_test_keypair(&[3u8; 32]);
740        let device2_did = ed25519_did(&device2_pk);
741
742        let att1 = create_signed_attestation(
743            &root_kp,
744            &device1_kp,
745            &root_did,
746            &device1_did,
747            None,
748            Some(fixed_now() + Duration::days(365)),
749        );
750        let att2 = create_signed_attestation(
751            &device1_kp,
752            &device2_kp,
753            &root_did, // WRONG: should be device1_did
754            &device2_did,
755            None,
756            Some(fixed_now() + Duration::days(365)),
757        );
758
759        let result = test_verifier()
760            .verify_chain(&[att1, att2], &ed(&root_pk))
761            .await
762            .unwrap();
763        assert!(!result.is_valid());
764        match result.status {
765            VerificationStatus::BrokenChain { missing_link } => {
766                assert!(missing_link.contains("Issuer mismatch"));
767            }
768            _ => panic!("Expected BrokenChain status, got {:?}", result.status),
769        }
770    }
771
772    #[tokio::test]
773    async fn verify_chain_valid_three_level_chain() {
774        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
775        let root_did = ed25519_did(&root_pk);
776        let (identity_kp, identity_pk) = create_test_keypair(&[2u8; 32]);
777        let identity_did = ed25519_did(&identity_pk);
778        let (device_kp, device_pk) = create_test_keypair(&[3u8; 32]);
779        let device_did = ed25519_did(&device_pk);
780
781        let att1 = create_signed_attestation(
782            &root_kp,
783            &identity_kp,
784            &root_did,
785            &identity_did,
786            None,
787            Some(fixed_now() + Duration::days(365)),
788        );
789        let att2 = create_signed_attestation(
790            &identity_kp,
791            &device_kp,
792            &identity_did,
793            &device_did,
794            None,
795            Some(fixed_now() + Duration::days(365)),
796        );
797
798        let result = test_verifier()
799            .verify_chain(&[att1, att2], &ed(&root_pk))
800            .await
801            .unwrap();
802        assert!(result.is_valid());
803        assert_eq!(result.chain.len(), 2);
804        assert!(result.chain[0].valid);
805        assert!(result.chain[1].valid);
806    }
807
808    #[tokio::test]
809    async fn verify_chain_revoked_intermediate_returns_revoked() {
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            Some(fixed_now()),
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        match result.status {
840            VerificationStatus::Revoked { .. } => {}
841            _ => panic!("Expected Revoked status, got {:?}", result.status),
842        }
843        assert_eq!(result.chain.len(), 2);
844        assert!(result.chain[0].valid);
845        assert!(!result.chain[1].valid);
846    }
847
848    #[tokio::test]
849    async fn verify_at_time_valid_before_expiration() {
850        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
851        let root_did = ed25519_did(&root_pk);
852        let (device_kp, _) = create_test_keypair(&[2u8; 32]);
853        let device_did = ed25519_did(&root_pk);
854
855        let expires = fixed_now() + Duration::days(30);
856        let att = create_signed_attestation(
857            &root_kp,
858            &device_kp,
859            &root_did,
860            &device_did,
861            None,
862            Some(expires),
863        );
864
865        let verification_time = fixed_now() + Duration::days(10);
866        let result = test_verifier()
867            .verify_at_time(&att, &ed(&root_pk), verification_time)
868            .await;
869        assert!(result.is_ok());
870    }
871
872    #[tokio::test]
873    async fn verify_at_time_expired_after_expiration() {
874        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
875        let root_did = ed25519_did(&root_pk);
876        let (device_kp, _) = create_test_keypair(&[2u8; 32]);
877        let device_did = ed25519_did(&root_pk);
878
879        let expires = fixed_now() + Duration::days(30);
880        let att = create_signed_attestation(
881            &root_kp,
882            &device_kp,
883            &root_did,
884            &device_did,
885            None,
886            Some(expires),
887        );
888
889        let verification_time = fixed_now() + Duration::days(60);
890        let result = test_verifier()
891            .verify_at_time(&att, &ed(&root_pk), verification_time)
892            .await;
893        assert!(result.is_err());
894        assert!(result.unwrap_err().to_string().contains("expired"));
895    }
896
897    #[tokio::test]
898    async fn verify_at_time_signature_always_checked() {
899        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
900        let root_did = ed25519_did(&root_pk);
901        let (device_kp, _) = create_test_keypair(&[2u8; 32]);
902        let device_did = ed25519_did(&root_pk);
903
904        let mut att = create_signed_attestation(
905            &root_kp,
906            &device_kp,
907            &root_did,
908            &device_did,
909            None,
910            Some(fixed_now() + Duration::days(365)),
911        );
912        let mut tampered = *att.identity_signature.as_bytes();
913        tampered[0] ^= 0xFF;
914        att.identity_signature = Ed25519Signature::from_bytes(tampered);
915
916        let verification_time = fixed_now() - Duration::days(10);
917        let result = test_verifier()
918            .verify_at_time(&att, &ed(&root_pk), verification_time)
919            .await;
920        assert!(result.is_err());
921        assert!(
922            result
923                .unwrap_err()
924                .to_string()
925                .contains("signature verification failed")
926        );
927    }
928
929    #[tokio::test]
930    async fn verify_at_time_with_past_time_skips_skew_check() {
931        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
932        let root_did = ed25519_did(&root_pk);
933        let (device_kp, _) = create_test_keypair(&[2u8; 32]);
934        let device_did = ed25519_did(&root_pk);
935
936        let att = create_signed_attestation(
937            &root_kp,
938            &device_kp,
939            &root_did,
940            &device_did,
941            None,
942            Some(fixed_now() + Duration::days(365)),
943        );
944
945        let verification_time = fixed_now() - Duration::days(30);
946        let result = test_verifier()
947            .verify_at_time(&att, &ed(&root_pk), verification_time)
948            .await;
949        assert!(result.is_ok());
950    }
951
952    #[tokio::test]
953    async fn verify_with_keys_still_works() {
954        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
955        let root_did = ed25519_did(&root_pk);
956        let (device_kp, _) = create_test_keypair(&[2u8; 32]);
957        let device_did = ed25519_did(&root_pk);
958
959        let att = create_signed_attestation(
960            &root_kp,
961            &device_kp,
962            &root_did,
963            &device_did,
964            None,
965            Some(fixed_now() + Duration::days(365)),
966        );
967
968        let result = test_verifier().verify_with_keys(&att, &ed(&root_pk)).await;
969        assert!(result.is_ok());
970    }
971
972    /// Helper to wrap an attestation as verified (for tests where we created it ourselves).
973    fn verified(att: Attestation) -> VerifiedAttestation {
974        VerifiedAttestation::dangerous_from_unchecked(att)
975    }
976
977    #[test]
978    fn is_device_listed_returns_true_for_valid_attestation() {
979        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
980        let root_did = ed25519_did(&root_pk);
981        let (device_kp, device_pk) = create_test_keypair(&[2u8; 32]);
982        let device_did_str = ed25519_did(&device_pk);
983        let device_did = CanonicalDid::new_unchecked(&device_did_str);
984
985        let att = create_signed_attestation(
986            &root_kp,
987            &device_kp,
988            &root_did,
989            &device_did_str,
990            None,
991            Some(fixed_now() + Duration::days(365)),
992        );
993
994        assert!(is_device_listed(
995            &root_did,
996            &device_did,
997            &[verified(att)],
998            fixed_now()
999        ));
1000    }
1001
1002    #[test]
1003    fn is_device_listed_returns_false_for_no_attestations() {
1004        let (_, root_pk) = create_test_keypair(&[1u8; 32]);
1005        let root_did = ed25519_did(&root_pk);
1006        let (_, device_pk) = create_test_keypair(&[2u8; 32]);
1007        let device_did_str = ed25519_did(&device_pk);
1008        let device_did = CanonicalDid::new_unchecked(&device_did_str);
1009
1010        assert!(!is_device_listed(&root_did, &device_did, &[], fixed_now()));
1011    }
1012
1013    #[test]
1014    fn is_device_listed_returns_false_for_expired_attestation() {
1015        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
1016        let root_did = ed25519_did(&root_pk);
1017        let (device_kp, device_pk) = create_test_keypair(&[2u8; 32]);
1018        let device_did_str = ed25519_did(&device_pk);
1019        let device_did = CanonicalDid::new_unchecked(&device_did_str);
1020
1021        let att = create_signed_attestation(
1022            &root_kp,
1023            &device_kp,
1024            &root_did,
1025            &device_did_str,
1026            None,
1027            Some(fixed_now() - Duration::days(1)),
1028        );
1029
1030        assert!(!is_device_listed(
1031            &root_did,
1032            &device_did,
1033            &[verified(att)],
1034            fixed_now()
1035        ));
1036    }
1037
1038    #[test]
1039    fn is_device_listed_returns_false_for_revoked_attestation() {
1040        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
1041        let root_did = ed25519_did(&root_pk);
1042        let (device_kp, device_pk) = create_test_keypair(&[2u8; 32]);
1043        let device_did_str = ed25519_did(&device_pk);
1044        let device_did = CanonicalDid::new_unchecked(&device_did_str);
1045
1046        let att = create_signed_attestation(
1047            &root_kp,
1048            &device_kp,
1049            &root_did,
1050            &device_did_str,
1051            Some(fixed_now()),
1052            Some(fixed_now() + Duration::days(365)),
1053        );
1054
1055        assert!(!is_device_listed(
1056            &root_did,
1057            &device_did,
1058            &[verified(att)],
1059            fixed_now()
1060        ));
1061    }
1062
1063    #[test]
1064    fn is_device_listed_returns_true_if_one_valid_among_many() {
1065        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
1066        let root_did = ed25519_did(&root_pk);
1067        let (device_kp, device_pk) = create_test_keypair(&[2u8; 32]);
1068        let device_did_str = ed25519_did(&device_pk);
1069        let device_did = CanonicalDid::new_unchecked(&device_did_str);
1070
1071        let att_expired = verified(create_signed_attestation(
1072            &root_kp,
1073            &device_kp,
1074            &root_did,
1075            &device_did_str,
1076            None,
1077            Some(fixed_now() - Duration::days(1)),
1078        ));
1079        let att_revoked = verified(create_signed_attestation(
1080            &root_kp,
1081            &device_kp,
1082            &root_did,
1083            &device_did_str,
1084            Some(fixed_now()),
1085            Some(fixed_now() + Duration::days(365)),
1086        ));
1087        let att_valid = verified(create_signed_attestation(
1088            &root_kp,
1089            &device_kp,
1090            &root_did,
1091            &device_did_str,
1092            None,
1093            Some(fixed_now() + Duration::days(365)),
1094        ));
1095
1096        assert!(is_device_listed(
1097            &root_did,
1098            &device_did,
1099            &[att_expired, att_revoked, att_valid],
1100            fixed_now()
1101        ));
1102    }
1103
1104    #[test]
1105    fn is_device_listed_returns_false_for_wrong_identity() {
1106        let (_, root_pk) = create_test_keypair(&[1u8; 32]);
1107        let root_did = ed25519_did(&root_pk);
1108        let (other_kp, other_pk) = create_test_keypair(&[3u8; 32]);
1109        let other_did = ed25519_did(&other_pk);
1110        let (device_kp, device_pk) = create_test_keypair(&[2u8; 32]);
1111        let device_did_str = ed25519_did(&device_pk);
1112        let device_did = CanonicalDid::new_unchecked(&device_did_str);
1113
1114        let att = create_signed_attestation(
1115            &other_kp,
1116            &device_kp,
1117            &other_did,
1118            &device_did_str,
1119            None,
1120            Some(fixed_now() + Duration::days(365)),
1121        );
1122        assert!(!is_device_listed(
1123            &root_did,
1124            &device_did,
1125            &[verified(att)],
1126            fixed_now()
1127        ));
1128    }
1129
1130    #[test]
1131    fn is_device_listed_returns_false_for_wrong_device() {
1132        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
1133        let root_did = ed25519_did(&root_pk);
1134        let (device_kp, device_pk) = create_test_keypair(&[2u8; 32]);
1135        let device_did_str = ed25519_did(&device_pk);
1136        let (_, other_device_pk) = create_test_keypair(&[4u8; 32]);
1137        let other_device_did_str = ed25519_did(&other_device_pk);
1138        let other_device_did = CanonicalDid::new_unchecked(&other_device_did_str);
1139
1140        let att = create_signed_attestation(
1141            &root_kp,
1142            &device_kp,
1143            &root_did,
1144            &device_did_str,
1145            None,
1146            Some(fixed_now() + Duration::days(365)),
1147        );
1148        assert!(!is_device_listed(
1149            &root_did,
1150            &other_device_did,
1151            &[verified(att)],
1152            fixed_now()
1153        ));
1154    }
1155
1156    /// Helper to create a signed attestation carrying a `delegated_by` link.
1157    fn create_signed_attestation_with_delegation(
1158        issuer_kp: &Ed25519KeyPair,
1159        device_kp: &Ed25519KeyPair,
1160        issuer_did: &str,
1161        subject_did: &str,
1162        delegated_by: Option<CanonicalDid>,
1163    ) -> Attestation {
1164        let device_pk: [u8; 32] = device_kp.public_key().as_ref().try_into().unwrap();
1165
1166        let mut att = Attestation {
1167            version: 1,
1168            rid: ResourceId::new("test-rid"),
1169            issuer: CanonicalDid::new_unchecked(issuer_did),
1170            subject: CanonicalDid::new_unchecked(subject_did),
1171            device_public_key: Ed25519PublicKey::from_bytes(device_pk).into(),
1172            identity_signature: Ed25519Signature::empty(),
1173            device_signature: Ed25519Signature::empty(),
1174            revoked_at: None,
1175            expires_at: Some(fixed_now() + Duration::days(365)),
1176            timestamp: Some(fixed_now()),
1177            note: None,
1178            payload: None,
1179            delegated_by,
1180            signer_type: None,
1181            environment_claim: None,
1182            commit_sha: None,
1183            commit_message: None,
1184            author: None,
1185            oidc_binding: None,
1186        };
1187
1188        let canonical_bytes = canonicalize_attestation_data(&att.canonical_data()).unwrap();
1189
1190        att.identity_signature =
1191            Ed25519Signature::try_from_slice(issuer_kp.sign(&canonical_bytes).as_ref()).unwrap();
1192        att.device_signature =
1193            Ed25519Signature::try_from_slice(device_kp.sign(&canonical_bytes).as_ref()).unwrap();
1194
1195        att
1196    }
1197
1198    #[tokio::test]
1199    async fn verify_attestation_rejects_tampered_delegated_by() {
1200        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
1201        let root_did = ed25519_did(&root_pk);
1202        let (device_kp, device_pk) = create_test_keypair(&[2u8; 32]);
1203        let device_did = ed25519_did(&device_pk);
1204
1205        let mut att = create_signed_attestation_with_delegation(
1206            &root_kp,
1207            &device_kp,
1208            &root_did,
1209            &device_did,
1210            Some(CanonicalDid::new_unchecked("did:keri:Eadmin")),
1211        );
1212        assert!(
1213            test_verifier()
1214                .verify_with_keys(&att, &ed(&root_pk))
1215                .await
1216                .is_ok()
1217        );
1218
1219        att.delegated_by = Some(CanonicalDid::new_unchecked("did:keri:Eattacker"));
1220        let result = test_verifier().verify_with_keys(&att, &ed(&root_pk)).await;
1221        assert!(
1222            result.is_err(),
1223            "Attestation should reject tampered delegated_by"
1224        );
1225    }
1226
1227    #[tokio::test]
1228    async fn verify_attestation_valid_with_delegation() {
1229        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
1230        let root_did = ed25519_did(&root_pk);
1231        let (device_kp, device_pk) = create_test_keypair(&[2u8; 32]);
1232        let device_did = ed25519_did(&device_pk);
1233
1234        let att = create_signed_attestation_with_delegation(
1235            &root_kp,
1236            &device_kp,
1237            &root_did,
1238            &device_did,
1239            Some(CanonicalDid::new_unchecked("did:keri:Eadmin")),
1240        );
1241
1242        let result = test_verifier().verify_with_keys(&att, &ed(&root_pk)).await;
1243        assert!(
1244            result.is_ok(),
1245            "Attestation with delegation should verify: {:?}",
1246            result.err()
1247        );
1248    }
1249
1250    fn create_signed_attestation_with_timestamp(
1251        issuer_kp: &Ed25519KeyPair,
1252        device_kp: &Ed25519KeyPair,
1253        issuer_did: &str,
1254        subject_did: &str,
1255        timestamp: Option<DateTime<Utc>>,
1256    ) -> Attestation {
1257        let device_pk: [u8; 32] = device_kp.public_key().as_ref().try_into().unwrap();
1258
1259        let mut att = Attestation {
1260            version: 1,
1261            rid: ResourceId::new("test-rid"),
1262            issuer: CanonicalDid::new_unchecked(issuer_did),
1263            subject: CanonicalDid::new_unchecked(subject_did),
1264            device_public_key: Ed25519PublicKey::from_bytes(device_pk).into(),
1265            identity_signature: Ed25519Signature::empty(),
1266            device_signature: Ed25519Signature::empty(),
1267            revoked_at: None,
1268            expires_at: Some(fixed_now() + Duration::days(365)),
1269            timestamp,
1270            note: None,
1271            payload: None,
1272            delegated_by: None,
1273            signer_type: None,
1274            environment_claim: None,
1275            commit_sha: None,
1276            commit_message: None,
1277            author: None,
1278            oidc_binding: None,
1279        };
1280
1281        let canonical_bytes = canonicalize_attestation_data(&att.canonical_data()).unwrap();
1282
1283        att.identity_signature =
1284            Ed25519Signature::try_from_slice(issuer_kp.sign(&canonical_bytes).as_ref()).unwrap();
1285        att.device_signature =
1286            Ed25519Signature::try_from_slice(device_kp.sign(&canonical_bytes).as_ref()).unwrap();
1287
1288        att
1289    }
1290
1291    #[tokio::test]
1292    async fn verify_attestation_created_1_hour_ago_succeeds() {
1293        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
1294        let root_did = ed25519_did(&root_pk);
1295        let (device_kp, device_pk) = create_test_keypair(&[2u8; 32]);
1296        let device_did = ed25519_did(&device_pk);
1297
1298        let one_hour_ago = fixed_now() - Duration::hours(1);
1299        let att = create_signed_attestation_with_timestamp(
1300            &root_kp,
1301            &device_kp,
1302            &root_did,
1303            &device_did,
1304            Some(one_hour_ago),
1305        );
1306
1307        let result = test_verifier().verify_with_keys(&att, &ed(&root_pk)).await;
1308        assert!(
1309            result.is_ok(),
1310            "Attestation created 1 hour ago should verify: {:?}",
1311            result.err()
1312        );
1313    }
1314
1315    #[tokio::test]
1316    async fn verify_attestation_created_30_days_ago_succeeds() {
1317        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
1318        let root_did = ed25519_did(&root_pk);
1319        let (device_kp, device_pk) = create_test_keypair(&[2u8; 32]);
1320        let device_did = ed25519_did(&device_pk);
1321
1322        let thirty_days_ago = fixed_now() - Duration::days(30);
1323        let att = create_signed_attestation_with_timestamp(
1324            &root_kp,
1325            &device_kp,
1326            &root_did,
1327            &device_did,
1328            Some(thirty_days_ago),
1329        );
1330
1331        let result = test_verifier().verify_with_keys(&att, &ed(&root_pk)).await;
1332        assert!(
1333            result.is_ok(),
1334            "Attestation created 30 days ago should verify: {:?}",
1335            result.err()
1336        );
1337    }
1338
1339    #[tokio::test]
1340    async fn verify_attestation_with_future_timestamp_fails() {
1341        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
1342        let root_did = ed25519_did(&root_pk);
1343        let (device_kp, device_pk) = create_test_keypair(&[2u8; 32]);
1344        let device_did = ed25519_did(&device_pk);
1345
1346        let ten_minutes_future = fixed_now() + Duration::minutes(10);
1347        let att = create_signed_attestation_with_timestamp(
1348            &root_kp,
1349            &device_kp,
1350            &root_did,
1351            &device_did,
1352            Some(ten_minutes_future),
1353        );
1354
1355        let result = test_verifier().verify_with_keys(&att, &ed(&root_pk)).await;
1356        assert!(
1357            result.is_err(),
1358            "Attestation with future timestamp should fail"
1359        );
1360        let err_msg = result.unwrap_err().to_string();
1361        assert!(
1362            err_msg.contains("in the future"),
1363            "Error should mention 'in the future': {}",
1364            err_msg
1365        );
1366    }
1367
1368    #[tokio::test]
1369    async fn verify_device_authorization_returns_valid_for_signed_attestation() {
1370        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
1371        let root_did = ed25519_did(&root_pk);
1372        let (device_kp, device_pk) = create_test_keypair(&[2u8; 32]);
1373        let device_did_str = ed25519_did(&device_pk);
1374        let device_did = CanonicalDid::new_unchecked(&device_did_str);
1375
1376        let att = create_signed_attestation(
1377            &root_kp,
1378            &device_kp,
1379            &root_did,
1380            &device_did_str,
1381            None,
1382            Some(fixed_now() + Duration::days(365)),
1383        );
1384
1385        let result = test_verifier()
1386            .verify_device_authorization(&root_did, &device_did, &[att], &ed(&root_pk))
1387            .await;
1388        assert!(result.is_ok());
1389        let report = result.unwrap();
1390        assert!(report.is_valid());
1391        assert_eq!(report.chain.len(), 1);
1392        assert!(report.chain[0].valid);
1393    }
1394
1395    #[tokio::test]
1396    async fn verify_device_authorization_returns_broken_chain_for_no_attestations() {
1397        let (_, root_pk) = create_test_keypair(&[1u8; 32]);
1398        let root_did = ed25519_did(&root_pk);
1399        let (_, device_pk) = create_test_keypair(&[2u8; 32]);
1400        let device_did_str = ed25519_did(&device_pk);
1401        let device_did = CanonicalDid::new_unchecked(&device_did_str);
1402
1403        let result = test_verifier()
1404            .verify_device_authorization(&root_did, &device_did, &[], &ed(&root_pk))
1405            .await;
1406        assert!(result.is_ok());
1407        let report = result.unwrap();
1408        assert!(!report.is_valid());
1409        match report.status {
1410            VerificationStatus::BrokenChain { missing_link } => {
1411                assert!(missing_link.contains("No attestation found"));
1412            }
1413            _ => panic!("Expected BrokenChain status, got {:?}", report.status),
1414        }
1415    }
1416
1417    #[tokio::test]
1418    async fn verify_device_authorization_fails_for_forged_signature() {
1419        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
1420        let root_did = ed25519_did(&root_pk);
1421        let (device_kp, device_pk) = create_test_keypair(&[2u8; 32]);
1422        let device_did_str = ed25519_did(&device_pk);
1423        let device_did = CanonicalDid::new_unchecked(&device_did_str);
1424
1425        let mut att = create_signed_attestation(
1426            &root_kp,
1427            &device_kp,
1428            &root_did,
1429            &device_did_str,
1430            None,
1431            Some(fixed_now() + Duration::days(365)),
1432        );
1433        let mut tampered = *att.identity_signature.as_bytes();
1434        tampered[0] ^= 0xFF;
1435        att.identity_signature = Ed25519Signature::from_bytes(tampered);
1436
1437        let result = test_verifier()
1438            .verify_device_authorization(&root_did, &device_did, &[att], &ed(&root_pk))
1439            .await;
1440        assert!(result.is_ok());
1441        let report = result.unwrap();
1442        assert!(!report.is_valid());
1443        match report.status {
1444            VerificationStatus::InvalidSignature { step } => assert_eq!(step, 0),
1445            _ => panic!("Expected InvalidSignature status, got {:?}", report.status),
1446        }
1447    }
1448
1449    #[tokio::test]
1450    async fn verify_device_authorization_fails_for_wrong_issuer_key() {
1451        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
1452        let root_did = ed25519_did(&root_pk);
1453        let (device_kp, device_pk) = create_test_keypair(&[2u8; 32]);
1454        let device_did_str = ed25519_did(&device_pk);
1455        let device_did = CanonicalDid::new_unchecked(&device_did_str);
1456        let (_, wrong_pk) = create_test_keypair(&[99u8; 32]);
1457
1458        let att = create_signed_attestation(
1459            &root_kp,
1460            &device_kp,
1461            &root_did,
1462            &device_did_str,
1463            None,
1464            Some(fixed_now() + Duration::days(365)),
1465        );
1466
1467        let result = test_verifier()
1468            .verify_device_authorization(&root_did, &device_did, &[att], &ed(&wrong_pk))
1469            .await;
1470        assert!(result.is_ok());
1471        let report = result.unwrap();
1472        assert!(!report.is_valid());
1473        match report.status {
1474            VerificationStatus::InvalidSignature { .. } => {}
1475            _ => panic!("Expected InvalidSignature status, got {:?}", report.status),
1476        }
1477    }
1478
1479    #[tokio::test]
1480    async fn verify_device_authorization_checks_expiry_and_revocation() {
1481        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
1482        let root_did = ed25519_did(&root_pk);
1483        let (device_kp, device_pk) = create_test_keypair(&[2u8; 32]);
1484        let device_did_str = ed25519_did(&device_pk);
1485        let device_did = CanonicalDid::new_unchecked(&device_did_str);
1486
1487        let att_expired = create_signed_attestation(
1488            &root_kp,
1489            &device_kp,
1490            &root_did,
1491            &device_did_str,
1492            None,
1493            Some(fixed_now() - Duration::days(1)),
1494        );
1495        let result = test_verifier()
1496            .verify_device_authorization(&root_did, &device_did, &[att_expired], &ed(&root_pk))
1497            .await;
1498        assert!(result.is_ok());
1499        let report = result.unwrap();
1500        assert!(!report.is_valid());
1501        match report.status {
1502            VerificationStatus::Expired { .. } => {}
1503            _ => panic!("Expected Expired status, got {:?}", report.status),
1504        }
1505
1506        let att_revoked = create_signed_attestation(
1507            &root_kp,
1508            &device_kp,
1509            &root_did,
1510            &device_did_str,
1511            Some(fixed_now()),
1512            Some(fixed_now() + Duration::days(365)),
1513        );
1514        let result = test_verifier()
1515            .verify_device_authorization(&root_did, &device_did, &[att_revoked], &ed(&root_pk))
1516            .await;
1517        assert!(result.is_ok());
1518        let report = result.unwrap();
1519        assert!(!report.is_valid());
1520        match report.status {
1521            VerificationStatus::Revoked { .. } => {}
1522            _ => panic!("Expected Revoked status, got {:?}", report.status),
1523        }
1524    }
1525
1526    fn create_witness_receipt(
1527        witness_kp: &Ed25519KeyPair,
1528        witness_did: &str,
1529        event_said: &str,
1530        seq: u128,
1531    ) -> auths_keri::witness::SignedReceipt {
1532        let receipt = auths_keri::witness::Receipt {
1533            v: auths_keri::VersionString::placeholder(),
1534            t: auths_keri::witness::ReceiptTag,
1535            d: Said::new_unchecked(event_said.to_string()),
1536            i: auths_keri::Prefix::new_unchecked(witness_did.to_string()),
1537            s: auths_keri::KeriSequence::new(seq),
1538        };
1539        let payload = serde_json::to_vec(&receipt).unwrap();
1540        let sig = witness_kp.sign(&payload).as_ref().to_vec();
1541        auths_keri::witness::SignedReceipt {
1542            receipt,
1543            signature: sig,
1544        }
1545    }
1546
1547    #[tokio::test]
1548    async fn verify_chain_with_witnesses_valid() {
1549        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
1550        let root_did = ed25519_did(&root_pk);
1551        let (device_kp, device_pk) = create_test_keypair(&[2u8; 32]);
1552        let device_did = ed25519_did(&device_pk);
1553
1554        let att = create_signed_attestation(
1555            &root_kp,
1556            &device_kp,
1557            &root_did,
1558            &device_did,
1559            None,
1560            Some(fixed_now() + Duration::days(365)),
1561        );
1562
1563        let (w1_kp, w1_pk) = create_test_keypair(&[10u8; 32]);
1564        let (w2_kp, w2_pk) = create_test_keypair(&[20u8; 32]);
1565
1566        let r1 = create_witness_receipt(&w1_kp, "did:key:w1", "EEvent1", 1);
1567        let r2 = create_witness_receipt(&w2_kp, "did:key:w2", "EEvent1", 1);
1568
1569        let witness_keys = vec![
1570            ("did:key:w1".into(), w1_pk.to_vec()),
1571            ("did:key:w2".into(), w2_pk.to_vec()),
1572        ];
1573
1574        let config = crate::witness::WitnessVerifyConfig {
1575            receipts: &[r1, r2],
1576            witness_keys: &witness_keys,
1577            threshold: 2,
1578        };
1579
1580        let report = test_verifier()
1581            .verify_chain_with_witnesses(&[att], &ed(&root_pk), &config)
1582            .await
1583            .unwrap();
1584        assert!(report.is_valid());
1585        assert!(report.witness_quorum.is_some());
1586        let quorum = report.witness_quorum.unwrap();
1587        assert_eq!(quorum.required, 2);
1588        assert_eq!(quorum.verified, 2);
1589    }
1590
1591    #[tokio::test]
1592    async fn verify_chain_with_witnesses_chain_fails() {
1593        let (w1_kp, w1_pk) = create_test_keypair(&[10u8; 32]);
1594        let r1 = create_witness_receipt(&w1_kp, "did:key:w1", "EEvent1", 1);
1595        let witness_keys = vec![("did:key:w1".into(), w1_pk.to_vec())];
1596        let config = crate::witness::WitnessVerifyConfig {
1597            receipts: &[r1],
1598            witness_keys: &witness_keys,
1599            threshold: 1,
1600        };
1601
1602        let report = test_verifier()
1603            .verify_chain_with_witnesses(&[], &ed(&[0u8; 32]), &config)
1604            .await
1605            .unwrap();
1606        assert!(!report.is_valid());
1607        match report.status {
1608            VerificationStatus::BrokenChain { .. } => {}
1609            _ => panic!("Expected BrokenChain, got {:?}", report.status),
1610        }
1611        assert!(report.witness_quorum.is_none());
1612    }
1613
1614    #[tokio::test]
1615    async fn verify_chain_with_witnesses_quorum_fails() {
1616        let (root_kp, root_pk) = create_test_keypair(&[1u8; 32]);
1617        let root_did = ed25519_did(&root_pk);
1618        let (device_kp, device_pk) = create_test_keypair(&[2u8; 32]);
1619        let device_did = ed25519_did(&device_pk);
1620
1621        let att = create_signed_attestation(
1622            &root_kp,
1623            &device_kp,
1624            &root_did,
1625            &device_did,
1626            None,
1627            Some(fixed_now() + Duration::days(365)),
1628        );
1629
1630        let (w1_kp, w1_pk) = create_test_keypair(&[10u8; 32]);
1631        let r1 = create_witness_receipt(&w1_kp, "did:key:w1", "EEvent1", 1);
1632        let witness_keys = vec![("did:key:w1".into(), w1_pk.to_vec())];
1633        let config = crate::witness::WitnessVerifyConfig {
1634            receipts: &[r1],
1635            witness_keys: &witness_keys,
1636            threshold: 2,
1637        };
1638
1639        let report = test_verifier()
1640            .verify_chain_with_witnesses(&[att], &ed(&root_pk), &config)
1641            .await
1642            .unwrap();
1643        assert!(!report.is_valid());
1644        match report.status {
1645            VerificationStatus::InsufficientWitnesses { required, verified } => {
1646                assert_eq!(required, 2);
1647                assert_eq!(verified, 1);
1648            }
1649            _ => panic!("Expected InsufficientWitnesses, got {:?}", report.status),
1650        }
1651        assert!(report.witness_quorum.is_some());
1652        assert!(!report.warnings.is_empty());
1653    }
1654
1655    /// Verify an attestation signed by a P-256 identity using a P-256 `DevicePublicKey`.
1656    ///
1657    /// Reproduces the production CI flow where a P-256 identity signs an Ed25519-device
1658    /// attestation, and ensures the curve-dispatched verifier accepts a 33-byte compressed
1659    /// P-256 public key.
1660    #[tokio::test]
1661    async fn verify_p256_identity_signed_attestation() {
1662        use auths_crypto::RingCryptoProvider;
1663
1664        // Generate a P-256 identity keypair (compressed 33-byte pubkey).
1665        let (p256_seed, p256_pk_bytes) = RingCryptoProvider::p256_generate().unwrap();
1666        assert_eq!(
1667            p256_pk_bytes.len(),
1668            33,
1669            "P-256 compressed pubkey is 33 bytes"
1670        );
1671        let seed_arr: [u8; 32] = *p256_seed.as_bytes();
1672
1673        // Use a KERI-style DID for the issuer; the DID value is opaque to the verifier
1674        // (it does not re-derive the issuer key from the DID).
1675        let issuer_did = "did:keri:Etest-p256-identity";
1676
1677        // Device remains Ed25519 for this test — matches real-world usage.
1678        let (device_kp, device_pk) = create_test_keypair(&[42u8; 32]);
1679        let device_did = ed25519_did(&device_pk);
1680
1681        let mut att = Attestation {
1682            version: 1,
1683            rid: ResourceId::new("test-rid"),
1684            issuer: CanonicalDid::new_unchecked(issuer_did),
1685            subject: CanonicalDid::new_unchecked(&device_did),
1686            device_public_key: Ed25519PublicKey::from_bytes(device_pk).into(),
1687            identity_signature: Ed25519Signature::empty(),
1688            device_signature: Ed25519Signature::empty(),
1689            revoked_at: None,
1690            expires_at: Some(fixed_now() + Duration::days(365)),
1691            timestamp: Some(fixed_now()),
1692            note: None,
1693            payload: None,
1694            delegated_by: None,
1695            signer_type: None,
1696            environment_claim: None,
1697            commit_sha: None,
1698            commit_message: None,
1699            author: None,
1700            oidc_binding: None,
1701        };
1702
1703        let canonical_bytes = canonicalize_attestation_data(&att.canonical_data()).unwrap();
1704
1705        // P-256 identity signature over canonical bytes (64 bytes: r||s).
1706        let p256_sig = RingCryptoProvider::p256_sign(&seed_arr, &canonical_bytes).unwrap();
1707        // Ed25519Signature holds exactly 64 bytes, matching P-256 r||s.
1708        assert_eq!(p256_sig.len(), 64);
1709        att.identity_signature = Ed25519Signature::try_from_slice(&p256_sig).unwrap();
1710
1711        // Device signature is Ed25519.
1712        att.device_signature =
1713            Ed25519Signature::try_from_slice(device_kp.sign(&canonical_bytes).as_ref()).unwrap();
1714
1715        let issuer_dpk =
1716            DevicePublicKey::try_new(auths_crypto::CurveType::P256, &p256_pk_bytes).unwrap();
1717
1718        let result = test_verifier().verify_with_keys(&att, &issuer_dpk).await;
1719        assert!(
1720            result.is_ok(),
1721            "P-256-signed attestation should verify with a P-256 DevicePublicKey: {:?}",
1722            result.err()
1723        );
1724    }
1725}