Skip to main content

auths_verifier/
lib.rs

1#![deny(
2    clippy::print_stdout,
3    clippy::print_stderr,
4    clippy::exit,
5    clippy::dbg_macro
6)]
7#![deny(clippy::disallowed_methods)]
8#![deny(rustdoc::broken_intra_doc_links)]
9#![warn(clippy::too_many_lines, clippy::cognitive_complexity)]
10#![warn(missing_docs)]
11//! # auths-verifier
12//!
13//! Attestation verification library for Auths.
14//!
15//! This crate provides signature and chain verification without requiring
16//! access to private keys or platform keychains. It's designed to be:
17//! - **Lightweight** — minimal dependencies
18//! - **Cross-platform** — works on any target including WASM
19//! - **FFI-friendly** — C-compatible interface available
20//!
21//! ## Quick Start
22//!
23//! ```rust,ignore
24//! use auths_verifier::{verify_chain, VerificationStatus};
25//!
26//! let report = verify_chain(&attestations)?;
27//!
28//! match report.status {
29//!     VerificationStatus::Valid => println!("Chain verified!"),
30//!     VerificationStatus::Expired { at } => println!("Expired at {}", at),
31//!     VerificationStatus::InvalidSignature { step } => {
32//!         println!("Bad signature at step {}", step);
33//!     }
34//!     _ => println!("Verification failed"),
35//! }
36//! ```
37//!
38//! ## With Capability Checking
39//!
40//! ```rust,ignore
41//! use auths_verifier::{verify_with_capability, Capability};
42//!
43//! // Verify device has sign-commit permission
44//! let report = verify_with_capability(&chain, Capability::SignCommit)?;
45//! ```
46//!
47//! ## Feature Flags
48//!
49//! - `wasm` — Enable WASM bindings via wasm-bindgen
50
51pub mod action;
52pub mod clock;
53pub mod commit;
54pub mod commit_error;
55pub mod core;
56pub mod error;
57/// C-compatible FFI bindings for attestation and chain verification.
58#[cfg(feature = "ffi")]
59pub mod ffi;
60pub mod keri;
61pub mod ssh_sig;
62pub mod types;
63pub mod verifier;
64pub mod verify;
65/// WASM bindings for browser and edge-runtime verification.
66#[cfg(feature = "wasm")]
67pub mod wasm;
68pub mod witness;
69
70// Re-export verification types for convenience
71pub use types::{
72    AssuranceLevel, AssuranceLevelParseError, CanonicalDid, ChainLink, DeviceDID,
73    DidConversionError, DidParseError, IdentityDID, VerificationReport, VerificationStatus,
74    signer_hex_to_did, validate_did,
75};
76
77// Re-export action envelope
78pub use action::ActionEnvelope;
79
80// Re-export core types
81pub use core::{
82    Attestation, Capability, CapabilityError, CommitOid, CommitOidError, Ed25519KeyError,
83    Ed25519PublicKey, Ed25519Signature, IdentityBundle, MAX_ATTESTATION_JSON_SIZE,
84    MAX_JSON_BATCH_SIZE, OidcBinding, PolicyId, PublicKeyHex, PublicKeyHexError, ResourceId, Role,
85    RoleParseError, SignatureLengthError, ThresholdPolicy, VerifiedAttestation,
86};
87
88// Re-export test utilities
89#[cfg(any(test, feature = "test-utils"))]
90pub use testing::AttestationBuilder;
91
92#[cfg(any(test, feature = "test-utils"))]
93pub use testing::MockClock;
94
95// Re-export error types
96pub use commit_error::CommitVerificationError;
97pub use error::{AttestationError, AuthsErrorInfo};
98
99// Re-export Verifier struct
100pub use verifier::Verifier;
101
102// Re-export verification functions (native-only, async)
103#[cfg(feature = "native")]
104pub use verify::{
105    verify_at_time, verify_chain, verify_chain_with_capability, verify_chain_with_witnesses,
106    verify_device_authorization, verify_with_capability, verify_with_keys,
107};
108
109// Re-export sync utility functions (always available)
110pub use verify::{
111    DeviceLinkVerification, compute_attestation_seal_digest, did_to_ed25519, is_device_listed,
112    verify_device_link,
113};
114
115// Re-export witness types
116pub use witness::{WitnessQuorum, WitnessReceipt, WitnessReceiptResult, WitnessVerifyConfig};
117
118// Re-export KERI verification types (key parsing lives in auths-crypto)
119pub use keri::{
120    IcpEvent as KeriIcpEvent, IxnEvent as KeriIxnEvent, KeriEvent, KeriKeyState, KeriTypeError,
121    KeriVerifyError, Prefix, RotEvent as KeriRotEvent, Said, Seal as KeriSeal, compute_said,
122    find_seal_in_kel, parse_kel_json, verify_kel,
123};
124
125// Re-export commit verification types
126pub use commit::VerifiedCommit;
127pub use ssh_sig::SshSigEnvelope;
128
129// Re-export crypto provider trait for downstream consumers
130pub use auths_crypto::CryptoProvider;
131
132// Re-export clock types for downstream consumers (auths-core re-exports from here)
133pub use clock::{ClockProvider, SystemClock};
134
135/// Test utilities for auths-verifier consumers (behind `test-utils` feature).
136#[cfg(any(test, feature = "test-utils"))]
137pub mod testing;
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142    use chrono::{TimeZone, Utc};
143
144    fn fixed_ts() -> chrono::DateTime<Utc> {
145        Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap()
146    }
147
148    #[test]
149    fn verification_report_is_valid_returns_true_for_valid_status() {
150        let report = VerificationReport::valid(vec![]);
151        assert!(report.is_valid());
152    }
153
154    #[test]
155    fn verification_report_is_valid_returns_false_for_expired_status() {
156        let report =
157            VerificationReport::with_status(VerificationStatus::Expired { at: fixed_ts() }, vec![]);
158        assert!(!report.is_valid());
159    }
160
161    #[test]
162    fn verification_report_is_valid_returns_false_for_revoked_status() {
163        let report = VerificationReport::with_status(
164            VerificationStatus::Revoked {
165                at: Some(fixed_ts()),
166            },
167            vec![],
168        );
169        assert!(!report.is_valid());
170    }
171
172    #[test]
173    fn verification_report_is_valid_returns_false_for_invalid_signature() {
174        let report = VerificationReport::with_status(
175            VerificationStatus::InvalidSignature { step: 0 },
176            vec![],
177        );
178        assert!(!report.is_valid());
179    }
180
181    #[test]
182    fn verification_report_is_valid_returns_false_for_broken_chain() {
183        let report = VerificationReport::with_status(
184            VerificationStatus::BrokenChain {
185                missing_link: "test".to_string(),
186            },
187            vec![],
188        );
189        assert!(!report.is_valid());
190    }
191
192    #[test]
193    fn verification_report_serializes_to_expected_json() {
194        let chain = vec![
195            ChainLink::valid(
196                "did:key:issuer1".to_string(),
197                "did:key:subject1".to_string(),
198            ),
199            ChainLink::invalid(
200                "did:key:issuer2".to_string(),
201                "did:key:subject2".to_string(),
202                "signature mismatch".to_string(),
203            ),
204        ];
205
206        let report = VerificationReport {
207            status: VerificationStatus::InvalidSignature { step: 1 },
208            chain,
209            warnings: vec!["Key expires soon".to_string()],
210            witness_quorum: None,
211        };
212
213        let json = serde_json::to_string(&report).expect("serialization failed");
214
215        // Verify structure
216        let parsed: serde_json::Value = serde_json::from_str(&json).expect("parse failed");
217
218        // Check status has "type" tag
219        assert_eq!(parsed["status"]["type"], "InvalidSignature");
220        assert_eq!(parsed["status"]["step"], 1);
221
222        // Check chain structure
223        assert_eq!(parsed["chain"].as_array().unwrap().len(), 2);
224        assert_eq!(parsed["chain"][0]["issuer"], "did:key:issuer1");
225        assert_eq!(parsed["chain"][0]["valid"], true);
226        assert_eq!(parsed["chain"][1]["valid"], false);
227        assert_eq!(parsed["chain"][1]["error"], "signature mismatch");
228
229        // Check warnings
230        assert_eq!(parsed["warnings"][0], "Key expires soon");
231    }
232
233    #[test]
234    fn verification_status_valid_serializes_correctly() {
235        let status = VerificationStatus::Valid;
236        let json = serde_json::to_string(&status).unwrap();
237        assert_eq!(json, r#"{"type":"Valid"}"#);
238    }
239
240    #[test]
241    fn verification_status_expired_serializes_with_timestamp() {
242        let status = VerificationStatus::Expired {
243            at: chrono::DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
244                .unwrap()
245                .with_timezone(&Utc),
246        };
247        let json = serde_json::to_string(&status).unwrap();
248        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
249        assert_eq!(parsed["type"], "Expired");
250        assert_eq!(parsed["at"], "2024-01-01T00:00:00Z");
251    }
252
253    #[test]
254    fn verification_status_revoked_serializes_with_optional_timestamp() {
255        // With timestamp
256        let status = VerificationStatus::Revoked {
257            at: Some(
258                chrono::DateTime::parse_from_rfc3339("2024-06-15T12:00:00Z")
259                    .unwrap()
260                    .with_timezone(&Utc),
261            ),
262        };
263        let json = serde_json::to_string(&status).unwrap();
264        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
265        assert_eq!(parsed["type"], "Revoked");
266        assert_eq!(parsed["at"], "2024-06-15T12:00:00Z");
267
268        // Without timestamp
269        let status = VerificationStatus::Revoked { at: None };
270        let json = serde_json::to_string(&status).unwrap();
271        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
272        assert_eq!(parsed["type"], "Revoked");
273        assert!(parsed["at"].is_null());
274    }
275
276    #[test]
277    fn chain_link_helpers_work() {
278        let valid = ChainLink::valid("issuer".to_string(), "subject".to_string());
279        assert!(valid.valid);
280        assert!(valid.error.is_none());
281
282        let invalid = ChainLink::invalid(
283            "issuer".to_string(),
284            "subject".to_string(),
285            "error".to_string(),
286        );
287        assert!(!invalid.valid);
288        assert_eq!(invalid.error, Some("error".to_string()));
289    }
290}