Skip to main content

auths_verifier/
core.rs

1//! Core attestation types and canonical serialization.
2
3use crate::error::AttestationError;
4use crate::types::{CanonicalDid, IdentityDID};
5use chrono::{DateTime, Utc};
6use hex;
7use json_canon;
8use log::debug;
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11use std::fmt;
12use std::ops::Deref;
13use std::str::FromStr;
14
15/// Maximum allowed size for a single attestation JSON input (64 KiB).
16pub const MAX_ATTESTATION_JSON_SIZE: usize = 64 * 1024;
17
18/// Maximum allowed size for JSON array inputs — chains, receipts, witness keys (1 MiB).
19pub const MAX_JSON_BATCH_SIZE: usize = 1024 * 1024;
20
21/// Maximum hex string length for Ed25519 public key (32 bytes × 2).
22pub const MAX_PUBLIC_KEY_HEX_LEN: usize = 64;
23/// Maximum hex string length for Ed25519 signature (64 bytes × 2).
24pub const MAX_SIGNATURE_HEX_LEN: usize = 128;
25/// Maximum hex string length for SHA-256 file hash (32 bytes × 2).
26pub const MAX_FILE_HASH_HEX_LEN: usize = 64;
27
28// Well-known capability strings (without auths: prefix for backward compat)
29const SIGN_COMMIT: &str = "sign_commit";
30const SIGN_RELEASE: &str = "sign_release";
31const MANAGE_MEMBERS: &str = "manage_members";
32const ROTATE_KEYS: &str = "rotate_keys";
33
34// =============================================================================
35// ResourceId newtype
36// =============================================================================
37
38/// A validated resource identifier linking an attestation to its storage ref.
39///
40/// Wraps a `String` with `#[serde(transparent)]` so JSON output is identical to bare `String`.
41/// Prevents accidental substitution of a DID, Git ref, or other string where a
42/// resource ID is expected.
43#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
44#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
45#[serde(transparent)]
46pub struct ResourceId(String);
47
48impl ResourceId {
49    /// Creates a new ResourceId.
50    pub fn new(s: impl Into<String>) -> Self {
51        Self(s.into())
52    }
53
54    /// Returns the inner string slice.
55    pub fn as_str(&self) -> &str {
56        &self.0
57    }
58}
59
60impl Deref for ResourceId {
61    type Target = str;
62    fn deref(&self) -> &str {
63        &self.0
64    }
65}
66
67impl fmt::Display for ResourceId {
68    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69        f.write_str(&self.0)
70    }
71}
72
73impl From<String> for ResourceId {
74    fn from(s: String) -> Self {
75        Self(s)
76    }
77}
78
79impl From<&str> for ResourceId {
80    fn from(s: &str) -> Self {
81        Self(s.to_string())
82    }
83}
84
85impl PartialEq<str> for ResourceId {
86    fn eq(&self, other: &str) -> bool {
87        self.0 == other
88    }
89}
90
91impl PartialEq<&str> for ResourceId {
92    fn eq(&self, other: &&str) -> bool {
93        self.0 == *other
94    }
95}
96
97impl PartialEq<String> for ResourceId {
98    fn eq(&self, other: &String) -> bool {
99        self.0 == *other
100    }
101}
102
103// =============================================================================
104// Role enum
105// =============================================================================
106
107/// Role classification for organization members.
108///
109/// Governs the default capability set assigned at member authorization time.
110/// Serializes as lowercase strings: `"admin"`, `"member"`, `"readonly"`.
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
112#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
113#[serde(rename_all = "lowercase")]
114pub enum Role {
115    /// Full admin access with all capabilities.
116    Admin,
117    /// Standard member with signing capabilities.
118    Member,
119    /// Read-only access; no signing capabilities.
120    Readonly,
121}
122
123impl Role {
124    /// Returns the canonical string representation.
125    pub fn as_str(&self) -> &str {
126        match self {
127            Role::Admin => "admin",
128            Role::Member => "member",
129            Role::Readonly => "readonly",
130        }
131    }
132
133    /// Return the default capability set for this role.
134    pub fn default_capabilities(&self) -> Vec<Capability> {
135        match self {
136            Role::Admin => vec![
137                Capability::sign_commit(),
138                Capability::sign_release(),
139                Capability::manage_members(),
140                Capability::rotate_keys(),
141            ],
142            Role::Member => vec![Capability::sign_commit(), Capability::sign_release()],
143            Role::Readonly => vec![],
144        }
145    }
146}
147
148impl fmt::Display for Role {
149    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150        f.write_str(self.as_str())
151    }
152}
153
154impl FromStr for Role {
155    type Err = RoleParseError;
156
157    fn from_str(s: &str) -> Result<Self, Self::Err> {
158        match s.trim().to_lowercase().as_str() {
159            "admin" => Ok(Role::Admin),
160            "member" => Ok(Role::Member),
161            "readonly" => Ok(Role::Readonly),
162            other => Err(RoleParseError(other.to_string())),
163        }
164    }
165}
166
167/// Error returned when parsing an invalid role string.
168#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
169#[error("unknown role: '{0}' (expected admin, member, or readonly)")]
170pub struct RoleParseError(String);
171
172// =============================================================================
173// Ed25519PublicKey newtype
174// =============================================================================
175
176/// A 32-byte Ed25519 public key.
177///
178/// Serializes as a hex string for JSON compatibility. Enforces exactly 32 bytes
179/// at construction time.
180#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
181pub struct Ed25519PublicKey([u8; 32]);
182
183impl Ed25519PublicKey {
184    /// Creates a new Ed25519PublicKey from a 32-byte array.
185    pub fn from_bytes(bytes: [u8; 32]) -> Self {
186        Self(bytes)
187    }
188
189    /// Creates a new Ed25519PublicKey from a byte slice.
190    ///
191    /// Args:
192    /// * `slice`: Byte slice that must be exactly 32 bytes.
193    ///
194    /// Usage:
195    /// ```ignore
196    /// let pk = Ed25519PublicKey::try_from_slice(&bytes)?;
197    /// ```
198    pub fn try_from_slice(slice: &[u8]) -> Result<Self, Ed25519KeyError> {
199        let arr: [u8; 32] = slice
200            .try_into()
201            .map_err(|_| Ed25519KeyError::InvalidLength(slice.len()))?;
202        Ok(Self(arr))
203    }
204
205    /// Returns the inner 32-byte array.
206    pub fn as_bytes(&self) -> &[u8; 32] {
207        &self.0
208    }
209
210    /// Returns `true` if all 32 bytes are zero (used for unsigned org-member attestations).
211    pub fn is_zero(&self) -> bool {
212        self.0 == [0u8; 32]
213    }
214}
215
216impl Serialize for Ed25519PublicKey {
217    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
218        s.serialize_str(&hex::encode(self.0))
219    }
220}
221
222impl<'de> Deserialize<'de> for Ed25519PublicKey {
223    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
224        let s = String::deserialize(d)?;
225        let bytes =
226            hex::decode(&s).map_err(|e| serde::de::Error::custom(format!("invalid hex: {e}")))?;
227        let arr: [u8; 32] = bytes.try_into().map_err(|v: Vec<u8>| {
228            serde::de::Error::custom(format!("expected 32 bytes, got {}", v.len()))
229        })?;
230        Ok(Self(arr))
231    }
232}
233
234impl fmt::Display for Ed25519PublicKey {
235    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
236        f.write_str(&hex::encode(self.0))
237    }
238}
239
240impl AsRef<[u8]> for Ed25519PublicKey {
241    fn as_ref(&self) -> &[u8] {
242        &self.0
243    }
244}
245
246#[cfg(feature = "schema")]
247impl schemars::JsonSchema for Ed25519PublicKey {
248    fn schema_name() -> String {
249        "Ed25519PublicKey".to_owned()
250    }
251
252    fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
253        schemars::schema::SchemaObject {
254            instance_type: Some(schemars::schema::InstanceType::String.into()),
255            format: Some("hex".to_owned()),
256            metadata: Some(Box::new(schemars::schema::Metadata {
257                description: Some("Ed25519 public key (32 bytes, hex-encoded)".to_owned()),
258                ..Default::default()
259            })),
260            ..Default::default()
261        }
262        .into()
263    }
264}
265
266/// Error type for Ed25519 public key construction.
267#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
268pub enum Ed25519KeyError {
269    /// The byte slice is not exactly 32 bytes.
270    #[error("expected 32 bytes, got {0}")]
271    InvalidLength(usize),
272    /// The hex string is not valid.
273    #[error("invalid hex: {0}")]
274    InvalidHex(String),
275}
276
277// =============================================================================
278// Ed25519Signature newtype
279// =============================================================================
280
281/// A validated Ed25519 signature (64 bytes).
282#[derive(Debug, Clone, PartialEq, Eq)]
283pub struct Ed25519Signature([u8; 64]);
284
285impl Ed25519Signature {
286    /// Creates a signature from a 64-byte array.
287    pub fn from_bytes(bytes: [u8; 64]) -> Self {
288        Self(bytes)
289    }
290
291    /// Attempts to create a signature from a byte slice, returning an error if the length is not 64.
292    pub fn try_from_slice(slice: &[u8]) -> Result<Self, SignatureLengthError> {
293        let arr: [u8; 64] = slice
294            .try_into()
295            .map_err(|_| SignatureLengthError(slice.len()))?;
296        Ok(Self(arr))
297    }
298
299    /// Creates an all-zero signature, used as a placeholder.
300    pub fn empty() -> Self {
301        Self([0u8; 64])
302    }
303
304    /// Returns `true` if the signature is all zeros (placeholder).
305    pub fn is_empty(&self) -> bool {
306        self.0 == [0u8; 64]
307    }
308
309    /// Returns a reference to the underlying 64-byte array.
310    pub fn as_bytes(&self) -> &[u8; 64] {
311        &self.0
312    }
313}
314
315impl Default for Ed25519Signature {
316    fn default() -> Self {
317        Self::empty()
318    }
319}
320
321impl std::fmt::Display for Ed25519Signature {
322    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
323        write!(f, "{}", hex::encode(self.0))
324    }
325}
326
327impl AsRef<[u8]> for Ed25519Signature {
328    fn as_ref(&self) -> &[u8] {
329        &self.0
330    }
331}
332
333#[cfg(feature = "schema")]
334impl schemars::JsonSchema for Ed25519Signature {
335    fn schema_name() -> String {
336        "Ed25519Signature".to_owned()
337    }
338
339    fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
340        schemars::schema::SchemaObject {
341            instance_type: Some(schemars::schema::InstanceType::String.into()),
342            format: Some("hex".to_owned()),
343            metadata: Some(Box::new(schemars::schema::Metadata {
344                description: Some("Ed25519 signature (64 bytes, hex-encoded)".to_owned()),
345                ..Default::default()
346            })),
347            ..Default::default()
348        }
349        .into()
350    }
351}
352
353impl serde::Serialize for Ed25519Signature {
354    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
355        serializer.serialize_str(&hex::encode(self.0))
356    }
357}
358
359impl<'de> serde::Deserialize<'de> for Ed25519Signature {
360    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
361        let s = String::deserialize(deserializer)?;
362        if s.is_empty() {
363            return Ok(Self::empty());
364        }
365        let bytes = hex::decode(&s).map_err(serde::de::Error::custom)?;
366        Self::try_from_slice(&bytes).map_err(serde::de::Error::custom)
367    }
368}
369
370/// Error when constructing an Ed25519Signature from a byte slice of wrong length.
371#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
372#[error("expected 64 bytes, got {0}")]
373pub struct SignatureLengthError(pub usize);
374
375// =============================================================================
376// Capability types
377// =============================================================================
378
379/// Error type for capability parsing and validation.
380#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
381pub enum CapabilityError {
382    /// The capability string is empty.
383    #[error("capability is empty")]
384    Empty,
385    /// The capability string exceeds the maximum length.
386    #[error("capability exceeds 64 chars: {0}")]
387    TooLong(usize),
388    /// The capability string contains invalid characters.
389    #[error("invalid characters in capability '{0}': only alphanumeric, ':', '-', '_' allowed")]
390    InvalidChars(String),
391    /// The capability uses the reserved 'auths:' namespace.
392    #[error(
393        "reserved namespace 'auths:' — use well-known constructors or choose a different prefix"
394    )]
395    ReservedNamespace,
396    /// The capability uses a reserved infrastructure namespace prefix.
397    #[error("the '{0}' prefix is reserved for infrastructure capabilities")]
398    ReservedInfraNamespace(String),
399}
400
401/// A validated capability identifier.
402///
403/// Capabilities are the atomic unit of authorization in Auths.
404/// They follow a namespace convention:
405///
406/// - Well-known capabilities: `sign_commit`, `sign_release`, `manage_members`, `rotate_keys`
407/// - Custom capabilities: any valid string (alphanumeric + `:` + `-` + `_`, max 64 chars)
408///
409/// The `auths:` prefix is reserved for future well-known capabilities and cannot be
410/// used in custom capabilities created via `parse()`.
411///
412/// # Examples
413///
414/// ```
415/// use auths_verifier::Capability;
416///
417/// // Well-known capabilities
418/// let cap = Capability::sign_commit();
419/// assert_eq!(cap.as_str(), "sign_commit");
420///
421/// // Custom capabilities
422/// let custom = Capability::parse("acme:deploy").unwrap();
423/// assert_eq!(custom.as_str(), "acme:deploy");
424///
425/// // Reserved namespace is rejected
426/// assert!(Capability::parse("auths:custom").is_err());
427/// ```
428#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
429#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
430#[serde(try_from = "String", into = "String")]
431pub struct Capability(String);
432
433impl Capability {
434    /// Maximum length for capability strings.
435    pub const MAX_LEN: usize = 64;
436
437    /// Reserved namespace prefix for Auths well-known capabilities.
438    const RESERVED_PREFIX: &'static str = "auths:";
439
440    /// Reserved infrastructure capability namespace prefixes.
441    const RESERVED_INFRA_PREFIXES: &'static [&'static str] =
442        &["compute:", "network:", "storage:", "runtime:", "env:"];
443
444    // ========================================================================
445    // Well-known capability constructors
446    // ========================================================================
447
448    /// Creates the `sign_commit` capability.
449    ///
450    /// Grants permission to sign commits.
451    #[inline]
452    pub fn sign_commit() -> Self {
453        Self(SIGN_COMMIT.to_string())
454    }
455
456    /// Creates the `sign_release` capability.
457    ///
458    /// Grants permission to sign releases.
459    #[inline]
460    pub fn sign_release() -> Self {
461        Self(SIGN_RELEASE.to_string())
462    }
463
464    /// Creates the `manage_members` capability.
465    ///
466    /// Grants permission to add/remove members in an organization.
467    #[inline]
468    pub fn manage_members() -> Self {
469        Self(MANAGE_MEMBERS.to_string())
470    }
471
472    /// Creates the `rotate_keys` capability.
473    ///
474    /// Grants permission to rotate keys for an identity.
475    #[inline]
476    pub fn rotate_keys() -> Self {
477        Self(ROTATE_KEYS.to_string())
478    }
479
480    // ========================================================================
481    // Parsing and validation
482    // ========================================================================
483
484    /// Parses and validates a capability string.
485    ///
486    /// This is the primary way to create custom capabilities. The input is
487    /// trimmed and lowercased to produce a canonical form.
488    ///
489    /// # Validation Rules
490    ///
491    /// - Non-empty
492    /// - Maximum 64 characters
493    /// - Only alphanumeric characters, colons (`:`), hyphens (`-`), and underscores (`_`)
494    /// - Cannot start with `auths:` (reserved namespace)
495    ///
496    /// # Examples
497    ///
498    /// ```
499    /// use auths_verifier::Capability;
500    ///
501    /// // Valid custom capabilities
502    /// assert!(Capability::parse("deploy").is_ok());
503    /// assert!(Capability::parse("acme:deploy").is_ok());
504    /// assert!(Capability::parse("org:team:action").is_ok());
505    ///
506    /// // Invalid capabilities
507    /// assert!(Capability::parse("").is_err());           // empty
508    /// assert!(Capability::parse("has space").is_err());  // invalid char
509    /// assert!(Capability::parse("auths:custom").is_err()); // reserved namespace
510    /// ```
511    pub fn parse(raw: &str) -> Result<Self, CapabilityError> {
512        let canonical = raw.trim().to_lowercase();
513
514        if canonical.is_empty() {
515            return Err(CapabilityError::Empty);
516        }
517        if canonical.len() > Self::MAX_LEN {
518            return Err(CapabilityError::TooLong(canonical.len()));
519        }
520        if !canonical
521            .chars()
522            .all(|c| c.is_alphanumeric() || c == ':' || c == '-' || c == '_')
523        {
524            return Err(CapabilityError::InvalidChars(canonical));
525        }
526        if canonical.starts_with(Self::RESERVED_PREFIX) {
527            return Err(CapabilityError::ReservedNamespace);
528        }
529        for prefix in Self::RESERVED_INFRA_PREFIXES {
530            if canonical.starts_with(prefix) {
531                return Err(CapabilityError::ReservedInfraNamespace(prefix.to_string()));
532            }
533        }
534
535        Ok(Self(canonical))
536    }
537
538    /// Creates a custom capability after validation.
539    ///
540    /// This is a convenience method that returns `Option<Self>` instead of `Result`.
541    ///
542    /// # Deprecated
543    ///
544    /// Prefer using `parse()` for better error handling.
545    #[deprecated(since = "0.2.0", note = "Use parse() for better error handling")]
546    pub fn custom(s: impl Into<String>) -> Option<Self> {
547        Self::parse(&s.into()).ok()
548    }
549
550    /// Validates a custom capability string.
551    ///
552    /// # Deprecated
553    ///
554    /// This method is retained for backward compatibility. Use `parse()` instead.
555    #[deprecated(since = "0.2.0", note = "Use parse() for validation")]
556    pub fn validate_custom(s: &str) -> bool {
557        Self::parse(s).is_ok()
558    }
559
560    // ========================================================================
561    // Accessors
562    // ========================================================================
563
564    /// Returns the canonical string representation of this capability.
565    ///
566    /// This is the authoritative string form used for comparison, display,
567    /// and serialization.
568    #[inline]
569    pub fn as_str(&self) -> &str {
570        &self.0
571    }
572
573    /// Returns `true` if this is a well-known Auths capability.
574    pub fn is_well_known(&self) -> bool {
575        matches!(
576            self.0.as_str(),
577            SIGN_COMMIT | SIGN_RELEASE | MANAGE_MEMBERS | ROTATE_KEYS
578        )
579    }
580
581    /// Returns the namespace portion of the capability (before first colon), if any.
582    pub fn namespace(&self) -> Option<&str> {
583        self.0.split(':').next().filter(|_| self.0.contains(':'))
584    }
585}
586
587impl fmt::Display for Capability {
588    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
589        f.write_str(&self.0)
590    }
591}
592
593impl TryFrom<String> for Capability {
594    type Error = CapabilityError;
595
596    fn try_from(s: String) -> Result<Self, Self::Error> {
597        let canonical = s.trim().to_lowercase();
598
599        if canonical.is_empty() {
600            return Err(CapabilityError::Empty);
601        }
602        if canonical.len() > Self::MAX_LEN {
603            return Err(CapabilityError::TooLong(canonical.len()));
604        }
605        if !canonical
606            .chars()
607            .all(|c| c.is_alphanumeric() || c == ':' || c == '-' || c == '_')
608        {
609            return Err(CapabilityError::InvalidChars(canonical));
610        }
611
612        // During deserialization, allow well-known capabilities and auths: prefix
613        // This ensures backward compatibility with existing attestations
614        Ok(Self(canonical))
615    }
616}
617
618impl std::str::FromStr for Capability {
619    type Err = CapabilityError;
620
621    /// Parses a capability string with CLI-friendly alias resolution.
622    ///
623    /// Normalizes the input (trim, lowercase, replace hyphens with underscores)
624    /// and matches well-known capabilities before falling through to
625    /// `Capability::parse()` for custom capability validation.
626    ///
627    /// Unlike the deprecated `parse_capability_cli`, this returns an error
628    /// for unrecognized well-known names instead of silently defaulting.
629    ///
630    /// Args:
631    /// * `s`: The capability string (e.g., "sign_commit", "Sign-Commit").
632    ///
633    /// Usage:
634    /// ```
635    /// use auths_verifier::Capability;
636    /// let cap: Capability = "sign_commit".parse().unwrap();
637    /// assert_eq!(cap.as_str(), "sign_commit");
638    /// ```
639    fn from_str(s: &str) -> Result<Self, Self::Err> {
640        let normalized = s.trim().to_lowercase().replace('-', "_");
641        match normalized.as_str() {
642            "sign_commit" | "signcommit" => Ok(Capability::sign_commit()),
643            "sign_release" | "signrelease" => Ok(Capability::sign_release()),
644            "manage_members" | "managemembers" => Ok(Capability::manage_members()),
645            "rotate_keys" | "rotatekeys" => Ok(Capability::rotate_keys()),
646            _ => Capability::parse(&normalized),
647        }
648    }
649}
650
651impl From<Capability> for String {
652    fn from(cap: Capability) -> Self {
653        cap.0
654    }
655}
656
657/// An identity bundle for stateless verification in CI/CD environments.
658///
659/// Contains all the information needed to verify commit signatures without
660/// requiring access to the identity repository or daemon.
661#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
662#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
663pub struct IdentityBundle {
664    /// The DID of the identity (e.g., `"did:keri:..."`)
665    pub identity_did: IdentityDID,
666    /// The public key in hex format for signature verification
667    pub public_key_hex: PublicKeyHex,
668    /// Chain of attestations linking the signing key to the identity
669    pub attestation_chain: Vec<Attestation>,
670    /// UTC timestamp when this bundle was created
671    pub bundle_timestamp: DateTime<Utc>,
672    /// Maximum age in seconds before this bundle is considered stale
673    pub max_valid_for_secs: u64,
674}
675
676impl IdentityBundle {
677    /// Check that this bundle is still within its TTL.
678    ///
679    /// Args:
680    /// * `now`: The current time, injected for deterministic verification.
681    ///
682    /// Usage:
683    /// ```ignore
684    /// bundle.check_freshness(Utc::now())?;
685    /// ```
686    pub fn check_freshness(&self, now: DateTime<Utc>) -> Result<(), AttestationError> {
687        let age = (now - self.bundle_timestamp).num_seconds().max(0) as u64;
688        if age > self.max_valid_for_secs {
689            return Err(AttestationError::BundleExpired {
690                age_secs: age,
691                max_secs: self.max_valid_for_secs,
692            });
693        }
694        Ok(())
695    }
696}
697
698/// Represents a 2-way key attestation between a primary identity and a device key.
699#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
700#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
701pub struct Attestation {
702    /// Schema version.
703    pub version: u32,
704    /// Record identifier linking this attestation to its storage ref.
705    pub rid: ResourceId,
706    /// DID of the issuing identity (can be `did:keri:` or `did:key:`).
707    pub issuer: CanonicalDid,
708    /// DID of the attested subject (device `did:key:` or identity `did:keri:`).
709    pub subject: CanonicalDid,
710    /// Ed25519 public key of the device (32 bytes, hex-encoded in JSON).
711    pub device_public_key: Ed25519PublicKey,
712    /// Issuer's Ed25519 signature over the canonical attestation data (hex-encoded in JSON).
713    #[serde(default, skip_serializing_if = "Ed25519Signature::is_empty")]
714    pub identity_signature: Ed25519Signature,
715    /// Device's Ed25519 signature over the canonical attestation data (hex-encoded in JSON).
716    pub device_signature: Ed25519Signature,
717    /// Timestamp when the attestation was revoked, if applicable.
718    #[serde(default, skip_serializing_if = "Option::is_none")]
719    pub revoked_at: Option<DateTime<Utc>>,
720    /// Expiration timestamp, if set.
721    #[serde(skip_serializing_if = "Option::is_none")]
722    pub expires_at: Option<DateTime<Utc>>,
723    /// Creation timestamp.
724    pub timestamp: Option<DateTime<Utc>>,
725    /// Optional human-readable note.
726    #[serde(skip_serializing_if = "Option::is_none")]
727    pub note: Option<String>,
728    /// Optional arbitrary JSON payload.
729    #[serde(skip_serializing_if = "Option::is_none")]
730    pub payload: Option<Value>,
731
732    /// Git commit SHA (for commit signing attestations).
733    #[serde(skip_serializing_if = "Option::is_none")]
734    pub commit_sha: Option<String>,
735
736    /// Git commit message (for commit signing attestations).
737    #[serde(skip_serializing_if = "Option::is_none")]
738    pub commit_message: Option<String>,
739
740    /// Git commit author (for commit signing attestations).
741    #[serde(skip_serializing_if = "Option::is_none")]
742    pub author: Option<String>,
743
744    /// OIDC binding information (issuer, subject, audience, expiration).
745    #[serde(skip_serializing_if = "Option::is_none")]
746    pub oidc_binding: Option<OidcBinding>,
747
748    /// Role for org membership attestations.
749    #[serde(default, skip_serializing_if = "Option::is_none")]
750    pub role: Option<Role>,
751
752    /// Capabilities this attestation grants.
753    #[serde(default, skip_serializing_if = "Vec::is_empty")]
754    pub capabilities: Vec<Capability>,
755
756    /// DID of the attestation that delegated authority (for chain tracking).
757    #[serde(default, skip_serializing_if = "Option::is_none")]
758    pub delegated_by: Option<CanonicalDid>,
759
760    /// The type of entity that produced this signature (human, agent, workload).
761    /// Included in the canonical JSON before signing — the signature covers this field.
762    #[serde(default, skip_serializing_if = "Option::is_none")]
763    pub signer_type: Option<SignerType>,
764
765    /// Unsigned environment claim for gateway-level verification via `auths-env`.
766    /// Excluded from `CanonicalAttestationData` — does not affect signatures.
767    #[serde(default, skip_serializing_if = "Option::is_none")]
768    pub environment_claim: Option<Value>,
769}
770
771/// OIDC token binding information for machine identity attestations.
772///
773/// Proves that the attestation was created by a CI/CD workload with a specific
774/// OIDC token. Contains the issuer, subject, audience, and expiration so verifiers
775/// can reconstruct the identity without needing the ephemeral private key.
776#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
777#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
778pub struct OidcBinding {
779    /// OIDC token issuer (e.g., "https://token.actions.githubusercontent.com").
780    pub issuer: String,
781    /// Token subject (unique workload identifier).
782    pub subject: String,
783    /// Expected audience.
784    pub audience: String,
785    /// Token expiration timestamp (Unix timestamp).
786    pub token_exp: i64,
787    /// CI/CD platform (e.g., "github", "gitlab", "circleci").
788    #[serde(default, skip_serializing_if = "Option::is_none")]
789    pub platform: Option<String>,
790    /// JTI for replay detection (if available).
791    #[serde(default, skip_serializing_if = "Option::is_none")]
792    pub jti: Option<String>,
793    /// Platform-normalized claims (e.g., repo, actor, run_id for GitHub).
794    #[serde(default, skip_serializing_if = "Option::is_none")]
795    pub normalized_claims: Option<serde_json::Map<String, serde_json::Value>>,
796}
797
798/// The type of entity that produced a signature.
799///
800/// Duplicated here (also in `auths-policy`) because `auths-verifier` is a
801/// standalone minimal-dependency crate that cannot depend on `auths-policy`.
802#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
803#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
804#[non_exhaustive]
805pub enum SignerType {
806    /// A human user.
807    Human,
808    /// An autonomous AI agent.
809    Agent,
810    /// A CI/CD workload or service identity.
811    Workload,
812}
813
814/// An attestation that has passed signature verification.
815///
816/// This type enforces at compile time that an attestation's signatures were verified
817/// before it can be stored. It can only be constructed by:
818/// - Verification functions (`verify_with_keys`, `verify_with_capability`)
819/// - The `dangerous_from_unchecked` escape hatch (for self-signed attestations)
820///
821/// Does NOT implement `Deserialize` to prevent bypassing verification by
822/// deserializing directly.
823#[derive(Debug, Clone, Serialize)]
824pub struct VerifiedAttestation(Attestation);
825
826impl VerifiedAttestation {
827    /// Access the inner attestation.
828    pub fn inner(&self) -> &Attestation {
829        &self.0
830    }
831
832    /// Consume and return the inner attestation.
833    pub fn into_inner(self) -> Attestation {
834        self.0
835    }
836
837    /// Construct a `VerifiedAttestation` without running verification.
838    ///
839    /// # Safety (logical)
840    /// Only use this when you are the signer (e.g., you just created and signed
841    /// the attestation) or in test code. Misuse defeats the purpose of this type.
842    #[doc(hidden)]
843    pub fn dangerous_from_unchecked(attestation: Attestation) -> Self {
844        Self(attestation)
845    }
846
847    pub(crate) fn from_verified(attestation: Attestation) -> Self {
848        Self(attestation)
849    }
850}
851
852impl std::ops::Deref for VerifiedAttestation {
853    type Target = Attestation;
854
855    fn deref(&self) -> &Attestation {
856        &self.0
857    }
858}
859
860/// Data structure for canonicalizing standard attestations (link, extend).
861#[derive(Serialize, Debug)]
862pub struct CanonicalAttestationData<'a> {
863    /// Schema version.
864    pub version: u32,
865    /// Record identifier.
866    pub rid: &'a str,
867    /// DID of the issuing identity.
868    pub issuer: &'a CanonicalDid,
869    /// DID of the attested subject.
870    pub subject: &'a CanonicalDid,
871    /// Raw Ed25519 public key of the device.
872    #[serde(with = "hex::serde")]
873    pub device_public_key: &'a [u8],
874    /// Optional arbitrary JSON payload.
875    pub payload: &'a Option<Value>,
876    /// Creation timestamp.
877    pub timestamp: &'a Option<DateTime<Utc>>,
878    /// Expiration timestamp.
879    pub expires_at: &'a Option<DateTime<Utc>>,
880    /// Revocation timestamp.
881    pub revoked_at: &'a Option<DateTime<Utc>>,
882    /// Optional human-readable note.
883    pub note: &'a Option<String>,
884
885    /// Org membership role (included in signed envelope).
886    #[serde(skip_serializing_if = "Option::is_none")]
887    pub role: Option<&'a str>,
888    /// Capabilities granted by this attestation (included in signed envelope).
889    #[serde(skip_serializing_if = "Option::is_none")]
890    pub capabilities: Option<&'a Vec<Capability>>,
891    /// DID of the delegating attestation (included in signed envelope).
892    #[serde(skip_serializing_if = "Option::is_none")]
893    pub delegated_by: Option<&'a CanonicalDid>,
894    /// Type of signer (included in signed envelope).
895    #[serde(skip_serializing_if = "Option::is_none")]
896    pub signer_type: Option<&'a SignerType>,
897}
898
899/// Produce the canonical JSON bytes over which signatures are computed.
900///
901/// Args:
902/// * `data`: The attestation data to canonicalize.
903pub fn canonicalize_attestation_data(
904    data: &CanonicalAttestationData,
905) -> Result<Vec<u8>, AttestationError> {
906    let canonical_json_string = json_canon::to_string(data).map_err(|e| {
907        AttestationError::SerializationError(format!("Failed to create canonical JSON: {}", e))
908    })?;
909    debug!(
910        "Generated canonical data (standard): {}",
911        canonical_json_string
912    );
913    Ok(canonical_json_string.into_bytes())
914}
915
916impl Attestation {
917    /// Returns `true` if this attestation has been revoked.
918    pub fn is_revoked(&self) -> bool {
919        self.revoked_at.is_some()
920    }
921
922    /// Deserializes an Attestation from JSON bytes.
923    ///
924    /// Returns an error if the input exceeds [`MAX_ATTESTATION_JSON_SIZE`] (64 KiB).
925    pub fn from_json(json_bytes: &[u8]) -> Result<Self, AttestationError> {
926        if json_bytes.len() > MAX_ATTESTATION_JSON_SIZE {
927            return Err(AttestationError::InputTooLarge(format!(
928                "attestation JSON is {} bytes, max {}",
929                json_bytes.len(),
930                MAX_ATTESTATION_JSON_SIZE
931            )));
932        }
933        serde_json::from_slice(json_bytes)
934            .map_err(|e| AttestationError::SerializationError(e.to_string()))
935    }
936
937    /// Formats the attestation contents for debug or inspection purposes.
938    pub fn to_debug_string(&self) -> String {
939        format!(
940            "RID: {}\nIssuer DID: {}\nSubject DID: {}\nDevice PK: {}\nIdentity Sig: {}\nDevice Sig: {}\nRevoked At: {:?}\nExpires: {:?}\nNote: {:?}",
941            self.rid,
942            self.issuer,
943            self.subject, // DeviceDID implements Display
944            hex::encode(self.device_public_key.as_bytes()),
945            hex::encode(self.identity_signature.as_bytes()),
946            hex::encode(self.device_signature.as_bytes()),
947            self.revoked_at,
948            self.expires_at,
949            self.note
950        )
951    }
952}
953
954// =============================================================================
955// Threshold Signatures (FROST) - Future Implementation
956// =============================================================================
957
958/// Policy for threshold signature operations (M-of-N).
959///
960/// This struct defines the parameters for FROST (Flexible Round-Optimized
961/// Schnorr Threshold) signature operations. FROST enables M-of-N threshold
962/// signing where at least M participants must cooperate to produce a valid
963/// signature, but no single participant can sign alone.
964///
965/// # Protocol Choice: FROST
966///
967/// FROST was chosen over alternatives for several reasons:
968/// - **Ed25519 native**: Works with existing Ed25519 key infrastructure
969/// - **Round-optimized**: Only 2 rounds for signing (vs 3+ for alternatives)
970/// - **Rust ecosystem**: `frost-ed25519` crate from ZcashFoundation is mature
971/// - **Security**: Proven secure under discrete log assumption
972///
973/// # Key Generation Approaches
974///
975/// Two approaches exist for generating threshold key shares:
976///
977/// 1. **Trusted Dealer**: One party generates the key and distributes shares
978///    - Simpler to implement
979///    - Single point of failure during key generation
980///    - Appropriate for org-controlled scenarios
981///
982/// 2. **Distributed Key Generation (DKG)**: Participants jointly generate key
983///    - No single party ever sees the full key
984///    - More complex, requires additional round-trips
985///    - Better for trustless scenarios
986///
987/// # Integration with Auths
988///
989/// Threshold policies can be attached to high-value operations like:
990/// - `sign-release`: Release signing requires M-of-N approvers
991/// - `rotate-keys`: Key rotation requires multi-party approval
992/// - `manage-members`: Adding admins requires quorum
993///
994/// # Example
995///
996/// ```ignore
997/// let policy = ThresholdPolicy {
998///     threshold: 2,
999///     signers: vec![
1000///         "did:key:alice".to_string(),
1001///         "did:key:bob".to_string(),
1002///         "did:key:carol".to_string(),
1003///     ],
1004///     policy_id: "release-signing-v1".to_string(),
1005///     scope: Some(Capability::sign_release()),
1006///     ceremony_endpoint: Some("wss://auths.example/ceremony".to_string()),
1007/// };
1008/// // 2-of-3: Any 2 of Alice, Bob, Carol can sign releases
1009/// ```
1010///
1011/// # Storage
1012///
1013/// Key shares are NOT stored in Git refs (they are secrets). Options:
1014/// - Platform keychain (macOS Keychain, Windows Credential Manager)
1015/// - Hardware security modules (HSMs)
1016/// - Secret managers (Vault, AWS Secrets Manager)
1017///
1018/// The policy itself (public info) is stored in Git at:
1019/// `refs/auths/policies/threshold/<policy_id>`
1020#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1021#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1022pub struct ThresholdPolicy {
1023    /// Minimum signers required (M in M-of-N)
1024    pub threshold: u8,
1025
1026    /// Total authorized signers (N in M-of-N) - DIDs of participants
1027    pub signers: Vec<String>,
1028
1029    /// Unique identifier for this policy
1030    pub policy_id: PolicyId,
1031
1032    /// Scope of operations this policy covers (optional)
1033    #[serde(default, skip_serializing_if = "Option::is_none")]
1034    pub scope: Option<Capability>,
1035
1036    /// Ceremony coordination endpoint (e.g., WebSocket URL for signing rounds)
1037    #[serde(default, skip_serializing_if = "Option::is_none")]
1038    pub ceremony_endpoint: Option<String>,
1039}
1040
1041impl ThresholdPolicy {
1042    /// Create a new threshold policy
1043    pub fn new(threshold: u8, signers: Vec<String>, policy_id: impl Into<PolicyId>) -> Self {
1044        Self {
1045            threshold,
1046            signers,
1047            policy_id: policy_id.into(),
1048            scope: None,
1049            ceremony_endpoint: None,
1050        }
1051    }
1052
1053    /// Check if the policy parameters are valid
1054    pub fn is_valid(&self) -> bool {
1055        // Threshold must be at least 1
1056        if self.threshold < 1 {
1057            return false;
1058        }
1059        // Threshold cannot exceed number of signers
1060        if self.threshold as usize > self.signers.len() {
1061            return false;
1062        }
1063        // Must have at least one signer
1064        if self.signers.is_empty() {
1065            return false;
1066        }
1067        // Policy ID must not be empty
1068        if self.policy_id.is_empty() {
1069            return false;
1070        }
1071        true
1072    }
1073
1074    /// Returns M (threshold) and N (total signers)
1075    pub fn m_of_n(&self) -> (u8, usize) {
1076        (self.threshold, self.signers.len())
1077    }
1078}
1079
1080// =============================================================================
1081// CommitOid newtype (validated)
1082// =============================================================================
1083
1084/// Error type for `CommitOid` construction.
1085#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1086pub enum CommitOidError {
1087    /// The string is empty.
1088    #[error("commit OID is empty")]
1089    Empty,
1090    /// The string length is not 40 (SHA-1) or 64 (SHA-256).
1091    #[error("expected 40 or 64 hex chars, got {0}")]
1092    InvalidLength(usize),
1093    /// The string contains non-hex characters.
1094    #[error("invalid hex character in commit OID")]
1095    InvalidHex,
1096}
1097
1098/// A validated Git commit object identifier (SHA-1 or SHA-256 hex string).
1099///
1100/// Accepts exactly 40 lowercase hex characters (SHA-1) or 64 (SHA-256).
1101#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
1102#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1103#[repr(transparent)]
1104#[serde(try_from = "String")]
1105pub struct CommitOid(String);
1106
1107impl CommitOid {
1108    /// Parses and validates a commit OID string.
1109    ///
1110    /// Args:
1111    /// * `raw`: A hex string that must be exactly 40 or 64 lowercase hex characters.
1112    ///
1113    /// Usage:
1114    /// ```ignore
1115    /// let oid = CommitOid::parse("a".repeat(40))?;
1116    /// ```
1117    pub fn parse(raw: &str) -> Result<Self, CommitOidError> {
1118        let s = raw.trim().to_lowercase();
1119        if s.is_empty() {
1120            return Err(CommitOidError::Empty);
1121        }
1122        if s.len() != 40 && s.len() != 64 {
1123            return Err(CommitOidError::InvalidLength(s.len()));
1124        }
1125        if !s.chars().all(|c| c.is_ascii_hexdigit()) {
1126            return Err(CommitOidError::InvalidHex);
1127        }
1128        Ok(Self(s))
1129    }
1130
1131    /// Creates a `CommitOid` without validation.
1132    ///
1133    /// Only use at deserialization boundaries where the value was previously validated.
1134    pub fn new_unchecked(s: impl Into<String>) -> Self {
1135        Self(s.into())
1136    }
1137
1138    /// Returns the inner string slice.
1139    pub fn as_str(&self) -> &str {
1140        &self.0
1141    }
1142
1143    /// Consumes self and returns the inner `String`.
1144    pub fn into_inner(self) -> String {
1145        self.0
1146    }
1147}
1148
1149impl fmt::Display for CommitOid {
1150    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1151        f.write_str(&self.0)
1152    }
1153}
1154
1155impl AsRef<str> for CommitOid {
1156    fn as_ref(&self) -> &str {
1157        &self.0
1158    }
1159}
1160
1161impl TryFrom<String> for CommitOid {
1162    type Error = CommitOidError;
1163    fn try_from(s: String) -> Result<Self, Self::Error> {
1164        Self::parse(&s)
1165    }
1166}
1167
1168impl TryFrom<&str> for CommitOid {
1169    type Error = CommitOidError;
1170    fn try_from(s: &str) -> Result<Self, Self::Error> {
1171        Self::parse(s)
1172    }
1173}
1174
1175impl FromStr for CommitOid {
1176    type Err = CommitOidError;
1177    fn from_str(s: &str) -> Result<Self, Self::Err> {
1178        Self::parse(s)
1179    }
1180}
1181
1182impl From<CommitOid> for String {
1183    fn from(oid: CommitOid) -> Self {
1184        oid.0
1185    }
1186}
1187
1188impl<'de> Deserialize<'de> for CommitOid {
1189    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
1190        let s = String::deserialize(d)?;
1191        Self::parse(&s).map_err(serde::de::Error::custom)
1192    }
1193}
1194
1195// =============================================================================
1196// PublicKeyHex newtype (validated)
1197// =============================================================================
1198
1199/// Error type for `PublicKeyHex` construction.
1200#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1201pub enum PublicKeyHexError {
1202    /// The hex string has the wrong length (not 64 chars / 32 bytes).
1203    #[error("expected 64 hex chars (32 bytes), got {0} chars")]
1204    InvalidLength(usize),
1205    /// The string contains non-hex characters.
1206    #[error("invalid hex: {0}")]
1207    InvalidHex(String),
1208}
1209
1210/// A validated hex-encoded Ed25519 public key (64 hex chars = 32 bytes).
1211///
1212/// Use `to_ed25519()` to convert to the byte-array `Ed25519PublicKey` type.
1213#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
1214#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1215#[repr(transparent)]
1216#[serde(try_from = "String")]
1217pub struct PublicKeyHex(String);
1218
1219impl PublicKeyHex {
1220    /// Parses and validates a hex-encoded public key string.
1221    ///
1222    /// Args:
1223    /// * `raw`: A 64-character hex string encoding 32 bytes.
1224    ///
1225    /// Usage:
1226    /// ```ignore
1227    /// let pk = PublicKeyHex::parse("ab".repeat(32))?;
1228    /// ```
1229    pub fn parse(raw: &str) -> Result<Self, PublicKeyHexError> {
1230        let s = raw.trim().to_lowercase();
1231        let bytes = hex::decode(&s).map_err(|e| PublicKeyHexError::InvalidHex(e.to_string()))?;
1232        if bytes.len() != 32 {
1233            return Err(PublicKeyHexError::InvalidLength(s.len()));
1234        }
1235        Ok(Self(s))
1236    }
1237
1238    /// Creates a `PublicKeyHex` without validation.
1239    ///
1240    /// Only use at deserialization boundaries where the value was previously validated.
1241    pub fn new_unchecked(s: impl Into<String>) -> Self {
1242        Self(s.into())
1243    }
1244
1245    /// Returns the inner string slice.
1246    pub fn as_str(&self) -> &str {
1247        &self.0
1248    }
1249
1250    /// Consumes self and returns the inner `String`.
1251    pub fn into_inner(self) -> String {
1252        self.0
1253    }
1254
1255    /// Decodes the hex and returns the byte-array `Ed25519PublicKey`.
1256    ///
1257    /// Usage:
1258    /// ```ignore
1259    /// let pk_hex = PublicKeyHex::parse("ab".repeat(32))?;
1260    /// let pk = pk_hex.to_ed25519()?;
1261    /// ```
1262    pub fn to_ed25519(&self) -> Result<Ed25519PublicKey, Ed25519KeyError> {
1263        let bytes = hex::decode(&self.0).map_err(|e| Ed25519KeyError::InvalidHex(e.to_string()))?;
1264        Ed25519PublicKey::try_from_slice(&bytes)
1265    }
1266}
1267
1268impl fmt::Display for PublicKeyHex {
1269    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1270        f.write_str(&self.0)
1271    }
1272}
1273
1274impl AsRef<str> for PublicKeyHex {
1275    fn as_ref(&self) -> &str {
1276        &self.0
1277    }
1278}
1279
1280impl TryFrom<String> for PublicKeyHex {
1281    type Error = PublicKeyHexError;
1282    fn try_from(s: String) -> Result<Self, Self::Error> {
1283        Self::parse(&s)
1284    }
1285}
1286
1287impl TryFrom<&str> for PublicKeyHex {
1288    type Error = PublicKeyHexError;
1289    fn try_from(s: &str) -> Result<Self, Self::Error> {
1290        Self::parse(s)
1291    }
1292}
1293
1294impl FromStr for PublicKeyHex {
1295    type Err = PublicKeyHexError;
1296    fn from_str(s: &str) -> Result<Self, Self::Err> {
1297        Self::parse(s)
1298    }
1299}
1300
1301impl From<PublicKeyHex> for String {
1302    fn from(pk: PublicKeyHex) -> Self {
1303        pk.0
1304    }
1305}
1306
1307impl<'de> Deserialize<'de> for PublicKeyHex {
1308    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
1309        let s = String::deserialize(d)?;
1310        Self::parse(&s).map_err(serde::de::Error::custom)
1311    }
1312}
1313
1314// =============================================================================
1315// PolicyId newtype (unvalidated)
1316// =============================================================================
1317
1318/// An opaque policy identifier.
1319///
1320/// No validation — wraps any `String`. Use where policy IDs are passed around
1321/// without needing to inspect their content.
1322#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1323#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1324#[serde(transparent)]
1325pub struct PolicyId(String);
1326
1327impl PolicyId {
1328    /// Creates a new PolicyId.
1329    pub fn new(s: impl Into<String>) -> Self {
1330        Self(s.into())
1331    }
1332
1333    /// Returns the inner string slice.
1334    pub fn as_str(&self) -> &str {
1335        &self.0
1336    }
1337}
1338
1339impl Deref for PolicyId {
1340    type Target = str;
1341    fn deref(&self) -> &str {
1342        &self.0
1343    }
1344}
1345
1346impl fmt::Display for PolicyId {
1347    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1348        f.write_str(&self.0)
1349    }
1350}
1351
1352impl From<String> for PolicyId {
1353    fn from(s: String) -> Self {
1354        Self(s)
1355    }
1356}
1357
1358impl From<&str> for PolicyId {
1359    fn from(s: &str) -> Self {
1360        Self(s.to_string())
1361    }
1362}
1363
1364impl PartialEq<str> for PolicyId {
1365    fn eq(&self, other: &str) -> bool {
1366        self.0 == other
1367    }
1368}
1369
1370impl PartialEq<&str> for PolicyId {
1371    fn eq(&self, other: &&str) -> bool {
1372        self.0 == *other
1373    }
1374}
1375
1376#[cfg(test)]
1377#[allow(clippy::disallowed_methods)]
1378mod tests {
1379    use super::*;
1380    use crate::AttestationBuilder;
1381
1382    // ========================================================================
1383    // Capability serialization tests
1384    // ========================================================================
1385
1386    #[test]
1387    fn capability_serializes_to_snake_case() {
1388        assert_eq!(
1389            serde_json::to_string(&Capability::sign_commit()).unwrap(),
1390            r#""sign_commit""#
1391        );
1392        assert_eq!(
1393            serde_json::to_string(&Capability::sign_release()).unwrap(),
1394            r#""sign_release""#
1395        );
1396        assert_eq!(
1397            serde_json::to_string(&Capability::manage_members()).unwrap(),
1398            r#""manage_members""#
1399        );
1400        assert_eq!(
1401            serde_json::to_string(&Capability::rotate_keys()).unwrap(),
1402            r#""rotate_keys""#
1403        );
1404    }
1405
1406    #[test]
1407    fn capability_deserializes_from_snake_case() {
1408        assert_eq!(
1409            serde_json::from_str::<Capability>(r#""sign_commit""#).unwrap(),
1410            Capability::sign_commit()
1411        );
1412        assert_eq!(
1413            serde_json::from_str::<Capability>(r#""sign_release""#).unwrap(),
1414            Capability::sign_release()
1415        );
1416        assert_eq!(
1417            serde_json::from_str::<Capability>(r#""manage_members""#).unwrap(),
1418            Capability::manage_members()
1419        );
1420        assert_eq!(
1421            serde_json::from_str::<Capability>(r#""rotate_keys""#).unwrap(),
1422            Capability::rotate_keys()
1423        );
1424    }
1425
1426    #[test]
1427    fn capability_custom_serializes_as_string() {
1428        let cap = Capability::parse("acme:deploy").unwrap();
1429        assert_eq!(serde_json::to_string(&cap).unwrap(), r#""acme:deploy""#);
1430    }
1431
1432    #[test]
1433    fn capability_custom_deserializes_unknown_strings() {
1434        // Unknown strings become custom capabilities
1435        let cap: Capability = serde_json::from_str(r#""custom-capability""#).unwrap();
1436        assert_eq!(cap, Capability::parse("custom-capability").unwrap());
1437    }
1438
1439    // ========================================================================
1440    // Capability parse() validation tests
1441    // ========================================================================
1442
1443    #[test]
1444    fn capability_parse_accepts_valid_strings() {
1445        assert!(Capability::parse("deploy").is_ok());
1446        assert!(Capability::parse("acme:deploy").is_ok());
1447        assert!(Capability::parse("my-custom-cap").is_ok());
1448        assert!(Capability::parse("org:team:action").is_ok());
1449        assert!(Capability::parse("with_underscore").is_ok()); // underscore allowed
1450    }
1451
1452    #[test]
1453    fn capability_parse_rejects_invalid_strings() {
1454        // Empty
1455        assert!(matches!(Capability::parse(""), Err(CapabilityError::Empty)));
1456
1457        // Too long
1458        assert!(matches!(
1459            Capability::parse(&"a".repeat(65)),
1460            Err(CapabilityError::TooLong(65))
1461        ));
1462
1463        // Invalid characters
1464        assert!(matches!(
1465            Capability::parse("has spaces"),
1466            Err(CapabilityError::InvalidChars(_))
1467        ));
1468        assert!(matches!(
1469            Capability::parse("has.dot"),
1470            Err(CapabilityError::InvalidChars(_))
1471        ));
1472    }
1473
1474    #[test]
1475    fn capability_parse_rejects_reserved_namespace() {
1476        assert!(matches!(
1477            Capability::parse("auths:custom"),
1478            Err(CapabilityError::ReservedNamespace)
1479        ));
1480        assert!(matches!(
1481            Capability::parse("auths:sign_commit"),
1482            Err(CapabilityError::ReservedNamespace)
1483        ));
1484    }
1485
1486    #[test]
1487    fn capability_parse_normalizes_to_lowercase() {
1488        let cap = Capability::parse("DEPLOY").unwrap();
1489        assert_eq!(cap.as_str(), "deploy");
1490
1491        let cap = Capability::parse("ACME:Deploy").unwrap();
1492        assert_eq!(cap.as_str(), "acme:deploy");
1493    }
1494
1495    #[test]
1496    fn capability_parse_trims_whitespace() {
1497        let cap = Capability::parse("  deploy  ").unwrap();
1498        assert_eq!(cap.as_str(), "deploy");
1499    }
1500
1501    // ========================================================================
1502    // Capability equality and hashing tests
1503    // ========================================================================
1504
1505    #[test]
1506    fn capability_is_hashable() {
1507        use std::collections::HashSet;
1508        let mut set = HashSet::new();
1509        set.insert(Capability::sign_commit());
1510        set.insert(Capability::sign_release());
1511        set.insert(Capability::parse("test").unwrap());
1512        assert_eq!(set.len(), 3);
1513        assert!(set.contains(&Capability::sign_commit()));
1514    }
1515
1516    #[test]
1517    fn capability_equality_with_different_construction_paths() {
1518        // Well-known constructor equals deserialized
1519        let from_constructor = Capability::sign_commit();
1520        let from_deser: Capability = serde_json::from_str(r#""sign_commit""#).unwrap();
1521        assert_eq!(from_constructor, from_deser);
1522
1523        // Parse equals deserialized for custom capabilities
1524        let from_parse = Capability::parse("acme:deploy").unwrap();
1525        let from_deser: Capability = serde_json::from_str(r#""acme:deploy""#).unwrap();
1526        assert_eq!(from_parse, from_deser);
1527    }
1528
1529    // ========================================================================
1530    // Capability display and accessor tests
1531    // ========================================================================
1532
1533    #[test]
1534    fn capability_display_matches_canonical_form() {
1535        assert_eq!(Capability::sign_commit().to_string(), "sign_commit");
1536        assert_eq!(Capability::sign_release().to_string(), "sign_release");
1537        assert_eq!(Capability::manage_members().to_string(), "manage_members");
1538        assert_eq!(Capability::rotate_keys().to_string(), "rotate_keys");
1539        assert_eq!(
1540            Capability::parse("acme:deploy").unwrap().to_string(),
1541            "acme:deploy"
1542        );
1543    }
1544
1545    #[test]
1546    fn capability_as_str_returns_canonical_form() {
1547        assert_eq!(Capability::sign_commit().as_str(), "sign_commit");
1548        assert_eq!(Capability::sign_release().as_str(), "sign_release");
1549        assert_eq!(Capability::manage_members().as_str(), "manage_members");
1550        assert_eq!(Capability::rotate_keys().as_str(), "rotate_keys");
1551        assert_eq!(
1552            Capability::parse("acme:deploy").unwrap().as_str(),
1553            "acme:deploy"
1554        );
1555    }
1556
1557    #[test]
1558    fn capability_is_well_known() {
1559        assert!(Capability::sign_commit().is_well_known());
1560        assert!(Capability::sign_release().is_well_known());
1561        assert!(Capability::manage_members().is_well_known());
1562        assert!(Capability::rotate_keys().is_well_known());
1563        assert!(!Capability::parse("custom").unwrap().is_well_known());
1564    }
1565
1566    #[test]
1567    fn capability_namespace() {
1568        assert_eq!(
1569            Capability::parse("acme:deploy").unwrap().namespace(),
1570            Some("acme")
1571        );
1572        assert_eq!(
1573            Capability::parse("org:team:action").unwrap().namespace(),
1574            Some("org")
1575        );
1576        assert_eq!(Capability::parse("deploy").unwrap().namespace(), None);
1577    }
1578
1579    // ========================================================================
1580    // Capability vec serialization tests
1581    // ========================================================================
1582
1583    #[test]
1584    fn capability_vec_serializes_as_array() {
1585        let caps = vec![Capability::sign_commit(), Capability::sign_release()];
1586        let json = serde_json::to_string(&caps).unwrap();
1587        assert_eq!(json, r#"["sign_commit","sign_release"]"#);
1588    }
1589
1590    #[test]
1591    fn capability_vec_deserializes_from_array() {
1592        let json = r#"["sign_commit","manage_members","custom-cap"]"#;
1593        let caps: Vec<Capability> = serde_json::from_str(json).unwrap();
1594        assert_eq!(caps.len(), 3);
1595        assert_eq!(caps[0], Capability::sign_commit());
1596        assert_eq!(caps[1], Capability::manage_members());
1597        assert_eq!(caps[2], Capability::parse("custom-cap").unwrap());
1598    }
1599
1600    // ========================================================================
1601    // Serde roundtrip tests (critical for backward compat)
1602    // ========================================================================
1603
1604    #[test]
1605    fn capability_serde_roundtrip_well_known() {
1606        let caps = vec![
1607            Capability::sign_commit(),
1608            Capability::sign_release(),
1609            Capability::manage_members(),
1610            Capability::rotate_keys(),
1611        ];
1612        for cap in caps {
1613            let json = serde_json::to_string(&cap).unwrap();
1614            let roundtrip: Capability = serde_json::from_str(&json).unwrap();
1615            assert_eq!(cap, roundtrip);
1616        }
1617    }
1618
1619    #[test]
1620    fn capability_serde_roundtrip_custom() {
1621        let caps = vec![
1622            Capability::parse("deploy").unwrap(),
1623            Capability::parse("acme:deploy").unwrap(),
1624            Capability::parse("org:team:action").unwrap(),
1625        ];
1626        for cap in caps {
1627            let json = serde_json::to_string(&cap).unwrap();
1628            let roundtrip: Capability = serde_json::from_str(&json).unwrap();
1629            assert_eq!(cap, roundtrip);
1630        }
1631    }
1632
1633    // Tests for Attestation org fields (fn-6.2)
1634
1635    #[test]
1636    fn attestation_old_json_without_org_fields_deserializes() {
1637        // Simulates an old attestation JSON without role, capabilities, delegated_by
1638        let old_json = r#"{
1639            "version": 1,
1640            "rid": "test-rid",
1641            "issuer": "did:keri:Eissuer",
1642            "subject": "did:key:zSubject",
1643            "device_public_key": "0102030405060708091011121314151617181920212223242526272829303132",
1644            "identity_signature": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
1645            "device_signature": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
1646            "revoked_at": null,
1647            "timestamp": null
1648        }"#;
1649
1650        let att: Attestation = serde_json::from_str(old_json).unwrap();
1651
1652        // New fields should have defaults
1653        assert_eq!(att.role, None);
1654        assert!(att.capabilities.is_empty());
1655        assert_eq!(att.delegated_by, None);
1656    }
1657
1658    #[test]
1659    fn attestation_with_org_fields_serializes_correctly() {
1660        let att = AttestationBuilder::default()
1661            .rid("test-rid")
1662            .issuer("did:keri:Eissuer")
1663            .subject("did:key:zSubject")
1664            .role(Some(Role::Admin))
1665            .capabilities(vec![
1666                Capability::sign_commit(),
1667                Capability::manage_members(),
1668            ])
1669            .delegated_by(Some(CanonicalDid::new_unchecked("did:keri:Edelegator")))
1670            .build();
1671
1672        let json = serde_json::to_string(&att).unwrap();
1673        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1674
1675        assert_eq!(parsed["role"], "admin");
1676        assert_eq!(parsed["capabilities"][0], "sign_commit");
1677        assert_eq!(parsed["capabilities"][1], "manage_members");
1678        assert_eq!(parsed["delegated_by"], "did:keri:Edelegator");
1679    }
1680
1681    #[test]
1682    fn attestation_without_org_fields_omits_them_in_json() {
1683        let att = AttestationBuilder::default()
1684            .rid("test-rid")
1685            .issuer("did:keri:Eissuer")
1686            .subject("did:key:zSubject")
1687            .build();
1688
1689        let json = serde_json::to_string(&att).unwrap();
1690        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1691
1692        // These fields should not be present in JSON
1693        assert!(parsed.get("role").is_none());
1694        assert!(parsed.get("capabilities").is_none());
1695        assert!(parsed.get("delegated_by").is_none());
1696    }
1697
1698    #[test]
1699    fn attestation_with_org_fields_roundtrips() {
1700        let original = AttestationBuilder::default()
1701            .rid("test-rid")
1702            .issuer("did:keri:Eissuer")
1703            .subject("did:key:zSubject")
1704            .role(Some(Role::Member))
1705            .capabilities(vec![Capability::sign_commit(), Capability::sign_release()])
1706            .delegated_by(Some(CanonicalDid::new_unchecked("did:keri:Eadmin")))
1707            .build();
1708
1709        let json = serde_json::to_string(&original).unwrap();
1710        let deserialized: Attestation = serde_json::from_str(&json).unwrap();
1711
1712        assert_eq!(original.role, deserialized.role);
1713        assert_eq!(original.capabilities, deserialized.capabilities);
1714        assert_eq!(original.delegated_by, deserialized.delegated_by);
1715    }
1716
1717    // Tests for ThresholdPolicy (fn-6.11)
1718
1719    #[test]
1720    fn threshold_policy_new_creates_valid_policy() {
1721        let policy = ThresholdPolicy::new(
1722            2,
1723            vec![
1724                "did:key:alice".to_string(),
1725                "did:key:bob".to_string(),
1726                "did:key:carol".to_string(),
1727            ],
1728            "test-policy".to_string(),
1729        );
1730
1731        assert_eq!(policy.threshold, 2);
1732        assert_eq!(policy.signers.len(), 3);
1733        assert_eq!(policy.policy_id, "test-policy");
1734        assert!(policy.scope.is_none());
1735        assert!(policy.ceremony_endpoint.is_none());
1736    }
1737
1738    #[test]
1739    fn threshold_policy_is_valid_checks_constraints() {
1740        // Valid 2-of-3
1741        let valid = ThresholdPolicy::new(
1742            2,
1743            vec!["a".to_string(), "b".to_string(), "c".to_string()],
1744            "policy".to_string(),
1745        );
1746        assert!(valid.is_valid());
1747
1748        // Invalid: threshold 0
1749        let zero_threshold = ThresholdPolicy::new(0, vec!["a".to_string()], "policy".to_string());
1750        assert!(!zero_threshold.is_valid());
1751
1752        // Invalid: threshold > signers
1753        let too_high = ThresholdPolicy::new(
1754            3,
1755            vec!["a".to_string(), "b".to_string()],
1756            "policy".to_string(),
1757        );
1758        assert!(!too_high.is_valid());
1759
1760        // Invalid: empty signers
1761        let no_signers = ThresholdPolicy::new(1, vec![], "policy".to_string());
1762        assert!(!no_signers.is_valid());
1763
1764        // Invalid: empty policy_id
1765        let no_id = ThresholdPolicy::new(1, vec!["a".to_string()], "".to_string());
1766        assert!(!no_id.is_valid());
1767    }
1768
1769    #[test]
1770    fn threshold_policy_m_of_n_returns_correct_values() {
1771        let policy = ThresholdPolicy::new(
1772            2,
1773            vec!["a".to_string(), "b".to_string(), "c".to_string()],
1774            "policy".to_string(),
1775        );
1776        let (m, n) = policy.m_of_n();
1777        assert_eq!(m, 2);
1778        assert_eq!(n, 3);
1779    }
1780
1781    #[test]
1782    fn threshold_policy_serializes_correctly() {
1783        let mut policy = ThresholdPolicy::new(
1784            2,
1785            vec!["did:key:alice".to_string(), "did:key:bob".to_string()],
1786            "release-policy".to_string(),
1787        );
1788        policy.scope = Some(Capability::sign_release());
1789        policy.ceremony_endpoint = Some("wss://example.com/ceremony".to_string());
1790
1791        let json = serde_json::to_string(&policy).unwrap();
1792        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1793
1794        assert_eq!(parsed["threshold"], 2);
1795        assert_eq!(parsed["signers"][0], "did:key:alice");
1796        assert_eq!(parsed["policy_id"], "release-policy");
1797        assert_eq!(parsed["scope"], "sign_release");
1798        assert_eq!(parsed["ceremony_endpoint"], "wss://example.com/ceremony");
1799    }
1800
1801    #[test]
1802    fn threshold_policy_without_optional_fields_omits_them() {
1803        let policy =
1804            ThresholdPolicy::new(1, vec!["did:key:alice".to_string()], "policy".to_string());
1805
1806        let json = serde_json::to_string(&policy).unwrap();
1807        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1808
1809        assert!(parsed.get("scope").is_none());
1810        assert!(parsed.get("ceremony_endpoint").is_none());
1811    }
1812
1813    #[test]
1814    fn threshold_policy_roundtrips() {
1815        let mut original = ThresholdPolicy::new(
1816            3,
1817            vec![
1818                "a".to_string(),
1819                "b".to_string(),
1820                "c".to_string(),
1821                "d".to_string(),
1822            ],
1823            "important-policy".to_string(),
1824        );
1825        original.scope = Some(Capability::rotate_keys());
1826
1827        let json = serde_json::to_string(&original).unwrap();
1828        let deserialized: ThresholdPolicy = serde_json::from_str(&json).unwrap();
1829
1830        assert_eq!(original, deserialized);
1831    }
1832
1833    // Tests for IdentityBundle (CI/CD stateless verification)
1834
1835    #[test]
1836    fn identity_bundle_serializes_correctly() {
1837        let bundle = IdentityBundle {
1838            identity_did: IdentityDID::new_unchecked("did:keri:test123"),
1839            public_key_hex: PublicKeyHex::new_unchecked("aabbccdd"),
1840            attestation_chain: vec![],
1841            bundle_timestamp: DateTime::parse_from_rfc3339("2099-01-01T00:00:00Z")
1842                .unwrap()
1843                .with_timezone(&Utc),
1844            max_valid_for_secs: 86400,
1845        };
1846
1847        let json = serde_json::to_string(&bundle).unwrap();
1848        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1849
1850        assert_eq!(parsed["identity_did"], "did:keri:test123");
1851        assert_eq!(parsed["public_key_hex"], "aabbccdd");
1852        assert!(parsed["attestation_chain"].as_array().unwrap().is_empty());
1853    }
1854
1855    #[test]
1856    fn identity_bundle_deserializes_correctly() {
1857        let json = r#"{
1858            "identity_did": "did:keri:abc123",
1859            "public_key_hex": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20",
1860            "attestation_chain": [],
1861            "bundle_timestamp": "2099-01-01T00:00:00Z",
1862            "max_valid_for_secs": 86400
1863        }"#;
1864
1865        let bundle: IdentityBundle = serde_json::from_str(json).unwrap();
1866
1867        assert_eq!(bundle.identity_did.as_str(), "did:keri:abc123");
1868        assert_eq!(
1869            bundle.public_key_hex.as_str(),
1870            "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"
1871        );
1872        assert!(bundle.attestation_chain.is_empty());
1873    }
1874
1875    #[test]
1876    fn identity_bundle_roundtrips() {
1877        let attestation = AttestationBuilder::default()
1878            .rid("test-rid")
1879            .issuer("did:keri:Eissuer")
1880            .subject("did:key:zSubject")
1881            .build();
1882
1883        let original = IdentityBundle {
1884            identity_did: IdentityDID::new_unchecked("did:keri:Eexample"),
1885            public_key_hex: PublicKeyHex::new_unchecked(
1886                "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
1887            ),
1888            attestation_chain: vec![attestation],
1889            bundle_timestamp: DateTime::parse_from_rfc3339("2099-01-01T00:00:00Z")
1890                .unwrap()
1891                .with_timezone(&Utc),
1892            max_valid_for_secs: 86400,
1893        };
1894
1895        let json = serde_json::to_string(&original).unwrap();
1896        let deserialized: IdentityBundle = serde_json::from_str(&json).unwrap();
1897
1898        assert_eq!(original.identity_did, deserialized.identity_did);
1899        assert_eq!(original.public_key_hex, deserialized.public_key_hex);
1900        assert_eq!(
1901            original.attestation_chain.len(),
1902            deserialized.attestation_chain.len()
1903        );
1904    }
1905}