Skip to main content

auths_verifier/
lib.rs

1// crate-level allow during curve-agnostic refactor.
2#![allow(clippy::disallowed_methods)]
3#![deny(
4    clippy::print_stdout,
5    clippy::print_stderr,
6    clippy::exit,
7    clippy::dbg_macro
8)]
9#![deny(rustdoc::broken_intra_doc_links)]
10#![warn(clippy::too_many_lines, clippy::cognitive_complexity)]
11#![warn(missing_docs)]
12//! # auths-verifier
13//!
14//! Attestation verification library for Auths.
15//!
16//! This crate provides signature and chain verification without requiring
17//! access to private keys or platform keychains. It's designed to be:
18//! - **Lightweight** — minimal dependencies
19//! - **Cross-platform** — works on any target including WASM
20//! - **FFI-friendly** — C-compatible interface available
21//!
22//! ## Quick Start
23//!
24//! ```rust,ignore
25//! use auths_verifier::{verify_chain, VerificationStatus};
26//!
27//! let report = verify_chain(&attestations)?;
28//!
29//! match report.status {
30//!     VerificationStatus::Valid => println!("Chain verified!"),
31//!     VerificationStatus::Expired { at } => println!("Expired at {}", at),
32//!     VerificationStatus::InvalidSignature { step } => {
33//!         println!("Bad signature at step {}", step);
34//!     }
35//!     _ => println!("Verification failed"),
36//! }
37//! ```
38//!
39//! ## Authority via Credentials
40//!
41//! The verifier checks **authenticity** only — signatures, chain linkage, expiry,
42//! and witness quorum. Capability/role **authority** is no longer read from the
43//! attestation; it flows exclusively through a holder-verified ACDC credential
44//! presentation (see `auths_id::policy::context_from_credential`).
45//!
46//! ## Feature Flags
47//!
48//! - `wasm` — Enable WASM bindings via wasm-bindgen
49
50pub mod action;
51/// Legible "what you are authorizing" summary derived from the bytes being signed.
52pub mod authorization_summary;
53pub mod clock;
54pub mod commit;
55/// Stateless commit verification against an identity bundle (CLI + WASM).
56pub mod commit_bundle;
57pub mod commit_error;
58pub mod commit_kel;
59/// The single cross-boundary verify contract (JSON request → tagged verdict).
60pub mod contract;
61pub mod core;
62pub mod credential;
63pub mod duplicity;
64pub mod error;
65/// Offline verification of compliance evidence packs.
66pub mod evidence_pack;
67/// C-compatible FFI bindings for attestation and chain verification.
68#[cfg(feature = "ffi")]
69pub mod ffi;
70/// Freshness verdict model (ADR 009): bounded freshness, verifier-set policy.
71pub mod freshness;
72/// OIDC-subject policy and the verify-time join for keyless CI signing.
73pub mod oidc_policy;
74/// Offline verification of air-gapped org provenance bundles.
75pub mod org_bundle;
76pub mod presentation;
77mod software_verify;
78pub mod ssh_sig;
79/// Transparency-log verification primitives (Merkle proofs, checkpoints).
80pub mod tlog;
81pub mod types;
82pub mod verifier;
83pub mod verify;
84/// WASM bindings for browser and edge-runtime verification.
85#[cfg(feature = "wasm")]
86pub mod wasm;
87pub mod witness;
88
89// Re-export verification types for convenience
90pub use types::{
91    AssuranceLevel, AssuranceLevelParseError, CanonicalDid, ChainLink, DidConversionError,
92    DidParseError, IdentityDID, VerificationReport, VerificationStatus, signer_hex_to_did,
93    validate_did,
94};
95
96// Re-export action envelope
97pub use action::ActionEnvelope;
98
99// Re-export the legible authorization summary (signing-consent surface)
100pub use authorization_summary::AuthorizationSummary;
101
102// Re-export core types
103pub use core::{
104    ATTESTATION_VERSION, Attestation, BundleDeviceKel, Capability, CapabilityError, CommitOid,
105    CommitOidError, DevicePublicKey, EcdsaP256Error, EcdsaP256PublicKey, EcdsaP256Signature,
106    Ed25519KeyError, Ed25519PublicKey, Ed25519Signature, IdentityBundle, InvalidKeyError,
107    MAX_ATTESTATION_JSON_SIZE, MAX_JSON_BATCH_SIZE, OidcBinding, PolicyId, PublicKeyDecodeError,
108    PublicKeyHex, PublicKeyHexError, ResourceId, Role, RoleParseError, SignatureAlgorithm,
109    SignatureLengthError, SignatureVerifyError, ThresholdPolicy, TypedSignature,
110    VerifiedAttestation, decode_public_key_bytes, decode_public_key_hex,
111};
112
113// Re-export the OIDC policy join (keyless CI verify-time exchange)
114pub use oidc_policy::{OidcPolicyError, OidcPolicyJoin, OidcSubjectPolicy};
115
116// Re-export test utilities
117#[cfg(any(test, feature = "test-utils"))]
118pub use testing::AttestationBuilder;
119
120#[cfg(any(test, feature = "test-utils"))]
121pub use testing::MockClock;
122
123// Re-export error types
124pub use commit_error::CommitVerificationError;
125pub use error::{AttestationError, AuthsErrorInfo};
126
127// Re-export Verifier struct
128pub use verifier::Verifier;
129
130// Re-export verification functions (native-only, async)
131#[cfg(feature = "native")]
132pub use verify::{
133    verify_at_time, verify_chain, verify_chain_with_witnesses, verify_device_authorization,
134    verify_with_keys,
135};
136
137// Re-export sync utility functions (always available)
138pub use verify::{
139    DeviceLinkVerification, compute_attestation_seal_digest, is_device_listed, verify_device_link,
140};
141
142// Re-export witness types
143#[cfg(feature = "native")]
144pub use witness::verify_build_attestation_offline;
145pub use witness::{
146    OfflineBuildVerdict, OfflineReceiptVerdict, SignedReceipt, WitnessQuorum, WitnessReceiptResult,
147    WitnessVerifyConfig, verify_receipt_offline,
148};
149
150// Re-export KERI types directly from auths-keri
151pub use auths_keri::{
152    Event as KeriEvent, IcpEvent as KeriIcpEvent, IxnEvent as KeriIxnEvent, KeriTypeError, Prefix,
153    RotEvent as KeriRotEvent, Said, Seal as KeriSeal, ValidationError, compute_said,
154    find_seal_in_kel, parse_kel_json,
155};
156
157// Re-export commit verification types
158pub use commit::{VerifiedCommit, commit_object_is_signed};
159pub use commit_bundle::{
160    AuthenticatedDeviceKel, BundleTrust, BundleTrustError, verify_commit_with_bundle_json,
161};
162pub use commit_kel::{
163    ANCHOR_SEQ_TRAILER, CommitVerdict, DEVICE_TRAILER, ID_TRAILER, SCOPE_TRAILER,
164    VerifierWitnessPolicy, WitnessGateStatus, WitnessedVerdict, anchor_seq_trailer,
165    commit_signer_trailers, scope_trailer, verify_commit_against_kel,
166    verify_commit_against_kel_scoped, verify_commit_against_kel_witnessed,
167    verify_commit_against_kel_witnessed_scoped,
168};
169pub use ssh_sig::{SshKeyType, SshSigEnvelope};
170
171// Re-export ACDC credential verification (Epic F.5). The `_sync` entrypoint is the
172// executor-free core every non-Rust binding target (C-ABI, WASM, Node, Python, Go)
173// calls directly; the `async fn` is a thin wrapper kept for native Rust callers.
174pub use credential::{
175    CredentialVerdict, LifecycleEvent, SignedAcdc, verify_credential, verify_credential_sync,
176};
177
178// Re-export holder-binding presentation verification (Epic F.8). `verify_presentation_sync`
179// is the executor-free core; `verify_presentation` is the thin async wrapper over it.
180pub use presentation::{
181    PresentationBinding, PresentationEnvelope, PresentationVerdict, verify_presentation,
182    verify_presentation_sync,
183};
184
185// Re-export the freshness model (ADR 009): it now appears in public verdict surfaces
186// (`PresentationVerdict::Valid`, `CredentialVerdict::Valid`), so consumers reach it directly.
187pub use freshness::{Freshness, FreshnessEvidence, FreshnessPolicy};
188
189// Re-export the cross-boundary JSON verify contract (Epic D2). One bundled request in, one
190// tagged discriminated-union verdict out — the single surface FFI/WASM/Node/Python/Go share.
191pub use contract::{SCHEMA_VERSION, verify_credential_json, verify_presentation_json};
192
193// Re-export crypto provider trait for downstream consumers
194pub use auths_crypto::CryptoProvider;
195pub use auths_crypto::Hash256;
196
197// Re-export clock types for downstream consumers (auths-core re-exports from here)
198pub use clock::{ClockProvider, SystemClock};
199
200/// Test utilities for auths-verifier consumers (behind `test-utils` feature).
201#[cfg(any(test, feature = "test-utils"))]
202pub mod testing;
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207    use chrono::{TimeZone, Utc};
208
209    fn fixed_ts() -> chrono::DateTime<Utc> {
210        Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap()
211    }
212
213    #[test]
214    fn verification_report_is_valid_returns_true_for_valid_status() {
215        let report = VerificationReport::valid(vec![]);
216        assert!(report.is_valid());
217    }
218
219    #[test]
220    fn verification_report_is_valid_returns_false_for_expired_status() {
221        let report =
222            VerificationReport::with_status(VerificationStatus::Expired { at: fixed_ts() }, vec![]);
223        assert!(!report.is_valid());
224    }
225
226    #[test]
227    fn verification_report_is_valid_returns_false_for_revoked_status() {
228        let report = VerificationReport::with_status(
229            VerificationStatus::Revoked {
230                at: Some(fixed_ts()),
231            },
232            vec![],
233        );
234        assert!(!report.is_valid());
235    }
236
237    #[test]
238    fn verification_report_is_valid_returns_false_for_invalid_signature() {
239        let report = VerificationReport::with_status(
240            VerificationStatus::InvalidSignature { step: 0 },
241            vec![],
242        );
243        assert!(!report.is_valid());
244    }
245
246    #[test]
247    fn verification_report_is_valid_returns_false_for_broken_chain() {
248        let report = VerificationReport::with_status(
249            VerificationStatus::BrokenChain {
250                missing_link: "test".to_string(),
251            },
252            vec![],
253        );
254        assert!(!report.is_valid());
255    }
256
257    #[test]
258    fn is_valid_is_false_when_a_duplicity_warning_is_present() {
259        // Fail-closed: a valid signature on a KEL that has forked is not trustworthy.
260        // The signature `status` stays Valid, but the trust decision (`is_valid`) refuses.
261        let report = VerificationReport::with_status(VerificationStatus::Valid, vec![])
262            .with_duplicity_warning(crate::duplicity::DuplicityReport::Diverging {
263                #[allow(clippy::disallowed_methods)]
264                shared_kel_prefix: crate::types::IdentityDID::new_unchecked(
265                    "did:keri:EShared".to_string(),
266                ),
267                seq: 2,
268                event_saids: vec!["ErotA".into(), "ErotB".into()],
269            });
270        assert!(matches!(report.status, VerificationStatus::Valid));
271        assert!(
272            !report.is_valid(),
273            "a diverging KEL must fail closed, not warn-and-pass"
274        );
275    }
276
277    #[test]
278    fn verification_report_serializes_to_expected_json() {
279        let chain = vec![
280            ChainLink::valid(
281                "did:key:issuer1".to_string(),
282                "did:key:subject1".to_string(),
283            ),
284            ChainLink::invalid(
285                "did:key:issuer2".to_string(),
286                "did:key:subject2".to_string(),
287                "signature mismatch".to_string(),
288            ),
289        ];
290
291        let report = VerificationReport {
292            status: VerificationStatus::InvalidSignature { step: 1 },
293            chain,
294            warnings: vec!["Key expires soon".to_string()],
295            witness_quorum: None,
296            anchored: None,
297            duplicity_warning: None,
298            freshness: crate::freshness::Freshness::Unknown,
299            as_of: None,
300        };
301
302        let json = serde_json::to_string(&report).expect("serialization failed");
303
304        // Verify structure
305        let parsed: serde_json::Value = serde_json::from_str(&json).expect("parse failed");
306
307        // Check status has "type" tag
308        assert_eq!(parsed["status"]["type"], "InvalidSignature");
309        assert_eq!(parsed["status"]["step"], 1);
310
311        // Check chain structure
312        assert_eq!(parsed["chain"].as_array().unwrap().len(), 2);
313        assert_eq!(parsed["chain"][0]["issuer"], "did:key:issuer1");
314        assert_eq!(parsed["chain"][0]["valid"], true);
315        assert_eq!(parsed["chain"][1]["valid"], false);
316        assert_eq!(parsed["chain"][1]["error"], "signature mismatch");
317
318        // Check warnings
319        assert_eq!(parsed["warnings"][0], "Key expires soon");
320    }
321
322    #[test]
323    fn verification_status_valid_serializes_correctly() {
324        let status = VerificationStatus::Valid;
325        let json = serde_json::to_string(&status).unwrap();
326        assert_eq!(json, r#"{"type":"Valid"}"#);
327    }
328
329    #[test]
330    fn verification_status_expired_serializes_with_timestamp() {
331        let status = VerificationStatus::Expired {
332            at: chrono::DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
333                .unwrap()
334                .with_timezone(&Utc),
335        };
336        let json = serde_json::to_string(&status).unwrap();
337        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
338        assert_eq!(parsed["type"], "Expired");
339        assert_eq!(parsed["at"], "2024-01-01T00:00:00Z");
340    }
341
342    #[test]
343    fn verification_status_revoked_serializes_with_optional_timestamp() {
344        // With timestamp
345        let status = VerificationStatus::Revoked {
346            at: Some(
347                chrono::DateTime::parse_from_rfc3339("2024-06-15T12:00:00Z")
348                    .unwrap()
349                    .with_timezone(&Utc),
350            ),
351        };
352        let json = serde_json::to_string(&status).unwrap();
353        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
354        assert_eq!(parsed["type"], "Revoked");
355        assert_eq!(parsed["at"], "2024-06-15T12:00:00Z");
356
357        // Without timestamp
358        let status = VerificationStatus::Revoked { at: None };
359        let json = serde_json::to_string(&status).unwrap();
360        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
361        assert_eq!(parsed["type"], "Revoked");
362        assert!(parsed["at"].is_null());
363    }
364
365    #[test]
366    fn chain_link_helpers_work() {
367        let valid = ChainLink::valid("issuer".to_string(), "subject".to_string());
368        assert!(valid.valid);
369        assert!(valid.error.is_none());
370
371        let invalid = ChainLink::invalid(
372            "issuer".to_string(),
373            "subject".to_string(),
374            "error".to_string(),
375        );
376        assert!(!invalid.valid);
377        assert_eq!(invalid.error, Some("error".to_string()));
378    }
379}