Skip to main content

auths_verifier/
core.rs

1//! Core attestation types and canonical serialization.
2
3use crate::error::AttestationError;
4use crate::types::{DeviceDID, 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}
397
398/// A validated capability identifier.
399///
400/// Capabilities are the atomic unit of authorization in Auths.
401/// They follow a namespace convention:
402///
403/// - Well-known capabilities: `sign_commit`, `sign_release`, `manage_members`, `rotate_keys`
404/// - Custom capabilities: any valid string (alphanumeric + `:` + `-` + `_`, max 64 chars)
405///
406/// The `auths:` prefix is reserved for future well-known capabilities and cannot be
407/// used in custom capabilities created via `parse()`.
408///
409/// # Examples
410///
411/// ```
412/// use auths_verifier::Capability;
413///
414/// // Well-known capabilities
415/// let cap = Capability::sign_commit();
416/// assert_eq!(cap.as_str(), "sign_commit");
417///
418/// // Custom capabilities
419/// let custom = Capability::parse("acme:deploy").unwrap();
420/// assert_eq!(custom.as_str(), "acme:deploy");
421///
422/// // Reserved namespace is rejected
423/// assert!(Capability::parse("auths:custom").is_err());
424/// ```
425#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
426#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
427#[serde(try_from = "String", into = "String")]
428pub struct Capability(String);
429
430impl Capability {
431    /// Maximum length for capability strings.
432    pub const MAX_LEN: usize = 64;
433
434    /// Reserved namespace prefix for Auths well-known capabilities.
435    const RESERVED_PREFIX: &'static str = "auths:";
436
437    // ========================================================================
438    // Well-known capability constructors
439    // ========================================================================
440
441    /// Creates the `sign_commit` capability.
442    ///
443    /// Grants permission to sign commits.
444    #[inline]
445    pub fn sign_commit() -> Self {
446        Self(SIGN_COMMIT.to_string())
447    }
448
449    /// Creates the `sign_release` capability.
450    ///
451    /// Grants permission to sign releases.
452    #[inline]
453    pub fn sign_release() -> Self {
454        Self(SIGN_RELEASE.to_string())
455    }
456
457    /// Creates the `manage_members` capability.
458    ///
459    /// Grants permission to add/remove members in an organization.
460    #[inline]
461    pub fn manage_members() -> Self {
462        Self(MANAGE_MEMBERS.to_string())
463    }
464
465    /// Creates the `rotate_keys` capability.
466    ///
467    /// Grants permission to rotate keys for an identity.
468    #[inline]
469    pub fn rotate_keys() -> Self {
470        Self(ROTATE_KEYS.to_string())
471    }
472
473    // ========================================================================
474    // Parsing and validation
475    // ========================================================================
476
477    /// Parses and validates a capability string.
478    ///
479    /// This is the primary way to create custom capabilities. The input is
480    /// trimmed and lowercased to produce a canonical form.
481    ///
482    /// # Validation Rules
483    ///
484    /// - Non-empty
485    /// - Maximum 64 characters
486    /// - Only alphanumeric characters, colons (`:`), hyphens (`-`), and underscores (`_`)
487    /// - Cannot start with `auths:` (reserved namespace)
488    ///
489    /// # Examples
490    ///
491    /// ```
492    /// use auths_verifier::Capability;
493    ///
494    /// // Valid custom capabilities
495    /// assert!(Capability::parse("deploy").is_ok());
496    /// assert!(Capability::parse("acme:deploy").is_ok());
497    /// assert!(Capability::parse("org:team:action").is_ok());
498    ///
499    /// // Invalid capabilities
500    /// assert!(Capability::parse("").is_err());           // empty
501    /// assert!(Capability::parse("has space").is_err());  // invalid char
502    /// assert!(Capability::parse("auths:custom").is_err()); // reserved namespace
503    /// ```
504    pub fn parse(raw: &str) -> Result<Self, CapabilityError> {
505        let canonical = raw.trim().to_lowercase();
506
507        if canonical.is_empty() {
508            return Err(CapabilityError::Empty);
509        }
510        if canonical.len() > Self::MAX_LEN {
511            return Err(CapabilityError::TooLong(canonical.len()));
512        }
513        if !canonical
514            .chars()
515            .all(|c| c.is_alphanumeric() || c == ':' || c == '-' || c == '_')
516        {
517            return Err(CapabilityError::InvalidChars(canonical));
518        }
519        if canonical.starts_with(Self::RESERVED_PREFIX) {
520            return Err(CapabilityError::ReservedNamespace);
521        }
522
523        Ok(Self(canonical))
524    }
525
526    /// Creates a custom capability after validation.
527    ///
528    /// This is a convenience method that returns `Option<Self>` instead of `Result`.
529    ///
530    /// # Deprecated
531    ///
532    /// Prefer using `parse()` for better error handling.
533    #[deprecated(since = "0.2.0", note = "Use parse() for better error handling")]
534    pub fn custom(s: impl Into<String>) -> Option<Self> {
535        Self::parse(&s.into()).ok()
536    }
537
538    /// Validates a custom capability string.
539    ///
540    /// # Deprecated
541    ///
542    /// This method is retained for backward compatibility. Use `parse()` instead.
543    #[deprecated(since = "0.2.0", note = "Use parse() for validation")]
544    pub fn validate_custom(s: &str) -> bool {
545        Self::parse(s).is_ok()
546    }
547
548    // ========================================================================
549    // Accessors
550    // ========================================================================
551
552    /// Returns the canonical string representation of this capability.
553    ///
554    /// This is the authoritative string form used for comparison, display,
555    /// and serialization.
556    #[inline]
557    pub fn as_str(&self) -> &str {
558        &self.0
559    }
560
561    /// Returns `true` if this is a well-known Auths capability.
562    pub fn is_well_known(&self) -> bool {
563        matches!(
564            self.0.as_str(),
565            SIGN_COMMIT | SIGN_RELEASE | MANAGE_MEMBERS | ROTATE_KEYS
566        )
567    }
568
569    /// Returns the namespace portion of the capability (before first colon), if any.
570    pub fn namespace(&self) -> Option<&str> {
571        self.0.split(':').next().filter(|_| self.0.contains(':'))
572    }
573}
574
575impl fmt::Display for Capability {
576    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
577        f.write_str(&self.0)
578    }
579}
580
581impl TryFrom<String> for Capability {
582    type Error = CapabilityError;
583
584    fn try_from(s: String) -> Result<Self, Self::Error> {
585        let canonical = s.trim().to_lowercase();
586
587        if canonical.is_empty() {
588            return Err(CapabilityError::Empty);
589        }
590        if canonical.len() > Self::MAX_LEN {
591            return Err(CapabilityError::TooLong(canonical.len()));
592        }
593        if !canonical
594            .chars()
595            .all(|c| c.is_alphanumeric() || c == ':' || c == '-' || c == '_')
596        {
597            return Err(CapabilityError::InvalidChars(canonical));
598        }
599
600        // During deserialization, allow well-known capabilities and auths: prefix
601        // This ensures backward compatibility with existing attestations
602        Ok(Self(canonical))
603    }
604}
605
606impl std::str::FromStr for Capability {
607    type Err = CapabilityError;
608
609    /// Parses a capability string with CLI-friendly alias resolution.
610    ///
611    /// Normalizes the input (trim, lowercase, replace hyphens with underscores)
612    /// and matches well-known capabilities before falling through to
613    /// `Capability::parse()` for custom capability validation.
614    ///
615    /// Unlike the deprecated `parse_capability_cli`, this returns an error
616    /// for unrecognized well-known names instead of silently defaulting.
617    ///
618    /// Args:
619    /// * `s`: The capability string (e.g., "sign_commit", "Sign-Commit").
620    ///
621    /// Usage:
622    /// ```
623    /// use auths_verifier::Capability;
624    /// let cap: Capability = "sign_commit".parse().unwrap();
625    /// assert_eq!(cap.as_str(), "sign_commit");
626    /// ```
627    fn from_str(s: &str) -> Result<Self, Self::Err> {
628        let normalized = s.trim().to_lowercase().replace('-', "_");
629        match normalized.as_str() {
630            "sign_commit" | "signcommit" => Ok(Capability::sign_commit()),
631            "sign_release" | "signrelease" => Ok(Capability::sign_release()),
632            "manage_members" | "managemembers" => Ok(Capability::manage_members()),
633            "rotate_keys" | "rotatekeys" => Ok(Capability::rotate_keys()),
634            _ => Capability::parse(&normalized),
635        }
636    }
637}
638
639impl From<Capability> for String {
640    fn from(cap: Capability) -> Self {
641        cap.0
642    }
643}
644
645/// An identity bundle for stateless verification in CI/CD environments.
646///
647/// Contains all the information needed to verify commit signatures without
648/// requiring access to the identity repository or daemon.
649#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
650#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
651pub struct IdentityBundle {
652    /// The DID of the identity (e.g., "did:keri:...")
653    pub identity_did: String,
654    /// The public key in hex format for signature verification
655    pub public_key_hex: String,
656    /// Chain of attestations linking the signing key to the identity
657    pub attestation_chain: Vec<Attestation>,
658    /// UTC timestamp when this bundle was created
659    pub bundle_timestamp: DateTime<Utc>,
660    /// Maximum age in seconds before this bundle is considered stale
661    pub max_valid_for_secs: u64,
662}
663
664impl IdentityBundle {
665    /// Check that this bundle is still within its TTL.
666    ///
667    /// Args:
668    /// * `now`: The current time, injected for deterministic verification.
669    ///
670    /// Usage:
671    /// ```ignore
672    /// bundle.check_freshness(Utc::now())?;
673    /// ```
674    pub fn check_freshness(&self, now: DateTime<Utc>) -> Result<(), AttestationError> {
675        let age = (now - self.bundle_timestamp).num_seconds().max(0) as u64;
676        if age > self.max_valid_for_secs {
677            return Err(AttestationError::BundleExpired {
678                age_secs: age,
679                max_secs: self.max_valid_for_secs,
680            });
681        }
682        Ok(())
683    }
684}
685
686/// Represents a 2-way key attestation between a primary identity and a device key.
687#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
688#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
689pub struct Attestation {
690    /// Schema version.
691    pub version: u32,
692    /// Record identifier linking this attestation to its storage ref.
693    pub rid: ResourceId,
694    /// DID of the issuing identity.
695    pub issuer: IdentityDID,
696    /// DID of the device being attested.
697    pub subject: DeviceDID,
698    /// Ed25519 public key of the device (32 bytes, hex-encoded in JSON).
699    pub device_public_key: Ed25519PublicKey,
700    /// Issuer's Ed25519 signature over the canonical attestation data (hex-encoded in JSON).
701    #[serde(default, skip_serializing_if = "Ed25519Signature::is_empty")]
702    pub identity_signature: Ed25519Signature,
703    /// Device's Ed25519 signature over the canonical attestation data (hex-encoded in JSON).
704    pub device_signature: Ed25519Signature,
705    /// Timestamp when the attestation was revoked, if applicable.
706    #[serde(default, skip_serializing_if = "Option::is_none")]
707    pub revoked_at: Option<DateTime<Utc>>,
708    /// Expiration timestamp, if set.
709    #[serde(skip_serializing_if = "Option::is_none")]
710    pub expires_at: Option<DateTime<Utc>>,
711    /// Creation timestamp.
712    pub timestamp: Option<DateTime<Utc>>,
713    /// Optional human-readable note.
714    #[serde(skip_serializing_if = "Option::is_none")]
715    pub note: Option<String>,
716    /// Optional arbitrary JSON payload.
717    #[serde(skip_serializing_if = "Option::is_none")]
718    pub payload: Option<Value>,
719
720    /// Role for org membership attestations.
721    #[serde(default, skip_serializing_if = "Option::is_none")]
722    pub role: Option<Role>,
723
724    /// Capabilities this attestation grants.
725    #[serde(default, skip_serializing_if = "Vec::is_empty")]
726    pub capabilities: Vec<Capability>,
727
728    /// DID of the attestation that delegated authority (for chain tracking).
729    #[serde(default, skip_serializing_if = "Option::is_none")]
730    pub delegated_by: Option<IdentityDID>,
731
732    /// The type of entity that produced this signature (human, agent, workload).
733    /// Included in the canonical JSON before signing — the signature covers this field.
734    #[serde(default, skip_serializing_if = "Option::is_none")]
735    pub signer_type: Option<SignerType>,
736}
737
738/// The type of entity that produced a signature.
739///
740/// Duplicated here (also in `auths-policy`) because `auths-verifier` is a
741/// standalone minimal-dependency crate that cannot depend on `auths-policy`.
742#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
743#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
744pub enum SignerType {
745    /// A human user.
746    Human,
747    /// An autonomous AI agent.
748    Agent,
749    /// A CI/CD workload or service identity.
750    Workload,
751}
752
753/// An attestation that has passed signature verification.
754///
755/// This type enforces at compile time that an attestation's signatures were verified
756/// before it can be stored. It can only be constructed by:
757/// - Verification functions (`verify_with_keys`, `verify_with_capability`)
758/// - The `dangerous_from_unchecked` escape hatch (for self-signed attestations)
759///
760/// Does NOT implement `Deserialize` to prevent bypassing verification by
761/// deserializing directly.
762#[derive(Debug, Clone, Serialize)]
763pub struct VerifiedAttestation(Attestation);
764
765impl VerifiedAttestation {
766    /// Access the inner attestation.
767    pub fn inner(&self) -> &Attestation {
768        &self.0
769    }
770
771    /// Consume and return the inner attestation.
772    pub fn into_inner(self) -> Attestation {
773        self.0
774    }
775
776    /// Construct a `VerifiedAttestation` without running verification.
777    ///
778    /// # Safety (logical)
779    /// Only use this when you are the signer (e.g., you just created and signed
780    /// the attestation) or in test code. Misuse defeats the purpose of this type.
781    #[doc(hidden)]
782    pub fn dangerous_from_unchecked(attestation: Attestation) -> Self {
783        Self(attestation)
784    }
785
786    pub(crate) fn from_verified(attestation: Attestation) -> Self {
787        Self(attestation)
788    }
789}
790
791impl std::ops::Deref for VerifiedAttestation {
792    type Target = Attestation;
793
794    fn deref(&self) -> &Attestation {
795        &self.0
796    }
797}
798
799/// Data structure for canonicalizing standard attestations (link, extend).
800#[derive(Serialize, Debug)]
801pub struct CanonicalAttestationData<'a> {
802    /// Schema version.
803    pub version: u32,
804    /// Record identifier.
805    pub rid: &'a str,
806    /// DID of the issuing identity.
807    pub issuer: &'a IdentityDID,
808    /// DID of the device being attested.
809    pub subject: &'a DeviceDID,
810    /// Raw Ed25519 public key of the device.
811    #[serde(with = "hex::serde")]
812    pub device_public_key: &'a [u8],
813    /// Optional arbitrary JSON payload.
814    pub payload: &'a Option<Value>,
815    /// Creation timestamp.
816    pub timestamp: &'a Option<DateTime<Utc>>,
817    /// Expiration timestamp.
818    pub expires_at: &'a Option<DateTime<Utc>>,
819    /// Revocation timestamp.
820    pub revoked_at: &'a Option<DateTime<Utc>>,
821    /// Optional human-readable note.
822    pub note: &'a Option<String>,
823
824    /// Org membership role (included in signed envelope).
825    #[serde(skip_serializing_if = "Option::is_none")]
826    pub role: Option<&'a str>,
827    /// Capabilities granted by this attestation (included in signed envelope).
828    #[serde(skip_serializing_if = "Option::is_none")]
829    pub capabilities: Option<&'a Vec<Capability>>,
830    /// DID of the delegating attestation (included in signed envelope).
831    #[serde(skip_serializing_if = "Option::is_none")]
832    pub delegated_by: Option<&'a IdentityDID>,
833    /// Type of signer (included in signed envelope).
834    #[serde(skip_serializing_if = "Option::is_none")]
835    pub signer_type: Option<&'a SignerType>,
836}
837
838/// Produce the canonical JSON bytes over which signatures are computed.
839///
840/// Args:
841/// * `data`: The attestation data to canonicalize.
842pub fn canonicalize_attestation_data(
843    data: &CanonicalAttestationData,
844) -> Result<Vec<u8>, AttestationError> {
845    let canonical_json_string = json_canon::to_string(data).map_err(|e| {
846        AttestationError::SerializationError(format!("Failed to create canonical JSON: {}", e))
847    })?;
848    debug!(
849        "Generated canonical data (standard): {}",
850        canonical_json_string
851    );
852    Ok(canonical_json_string.into_bytes())
853}
854
855impl Attestation {
856    /// Returns `true` if this attestation has been revoked.
857    pub fn is_revoked(&self) -> bool {
858        self.revoked_at.is_some()
859    }
860
861    /// Deserializes an Attestation from JSON bytes.
862    ///
863    /// Returns an error if the input exceeds [`MAX_ATTESTATION_JSON_SIZE`] (64 KiB).
864    pub fn from_json(json_bytes: &[u8]) -> Result<Self, AttestationError> {
865        if json_bytes.len() > MAX_ATTESTATION_JSON_SIZE {
866            return Err(AttestationError::InputTooLarge(format!(
867                "attestation JSON is {} bytes, max {}",
868                json_bytes.len(),
869                MAX_ATTESTATION_JSON_SIZE
870            )));
871        }
872        serde_json::from_slice(json_bytes)
873            .map_err(|e| AttestationError::SerializationError(e.to_string()))
874    }
875
876    /// Formats the attestation contents for debug or inspection purposes.
877    pub fn to_debug_string(&self) -> String {
878        format!(
879            "RID: {}\nIssuer DID: {}\nSubject DID: {}\nDevice PK: {}\nIdentity Sig: {}\nDevice Sig: {}\nRevoked At: {:?}\nExpires: {:?}\nNote: {:?}",
880            self.rid,
881            self.issuer,
882            self.subject, // DeviceDID implements Display
883            hex::encode(self.device_public_key.as_bytes()),
884            hex::encode(self.identity_signature.as_bytes()),
885            hex::encode(self.device_signature.as_bytes()),
886            self.revoked_at,
887            self.expires_at,
888            self.note
889        )
890    }
891}
892
893// =============================================================================
894// Threshold Signatures (FROST) - Future Implementation
895// =============================================================================
896
897/// Policy for threshold signature operations (M-of-N).
898///
899/// This struct defines the parameters for FROST (Flexible Round-Optimized
900/// Schnorr Threshold) signature operations. FROST enables M-of-N threshold
901/// signing where at least M participants must cooperate to produce a valid
902/// signature, but no single participant can sign alone.
903///
904/// # Protocol Choice: FROST
905///
906/// FROST was chosen over alternatives for several reasons:
907/// - **Ed25519 native**: Works with existing Ed25519 key infrastructure
908/// - **Round-optimized**: Only 2 rounds for signing (vs 3+ for alternatives)
909/// - **Rust ecosystem**: `frost-ed25519` crate from ZcashFoundation is mature
910/// - **Security**: Proven secure under discrete log assumption
911///
912/// # Key Generation Approaches
913///
914/// Two approaches exist for generating threshold key shares:
915///
916/// 1. **Trusted Dealer**: One party generates the key and distributes shares
917///    - Simpler to implement
918///    - Single point of failure during key generation
919///    - Appropriate for org-controlled scenarios
920///
921/// 2. **Distributed Key Generation (DKG)**: Participants jointly generate key
922///    - No single party ever sees the full key
923///    - More complex, requires additional round-trips
924///    - Better for trustless scenarios
925///
926/// # Integration with Auths
927///
928/// Threshold policies can be attached to high-value operations like:
929/// - `sign-release`: Release signing requires M-of-N approvers
930/// - `rotate-keys`: Key rotation requires multi-party approval
931/// - `manage-members`: Adding admins requires quorum
932///
933/// # Example
934///
935/// ```ignore
936/// let policy = ThresholdPolicy {
937///     threshold: 2,
938///     signers: vec![
939///         "did:key:alice".to_string(),
940///         "did:key:bob".to_string(),
941///         "did:key:carol".to_string(),
942///     ],
943///     policy_id: "release-signing-v1".to_string(),
944///     scope: Some(Capability::sign_release()),
945///     ceremony_endpoint: Some("wss://auths.example/ceremony".to_string()),
946/// };
947/// // 2-of-3: Any 2 of Alice, Bob, Carol can sign releases
948/// ```
949///
950/// # Storage
951///
952/// Key shares are NOT stored in Git refs (they are secrets). Options:
953/// - Platform keychain (macOS Keychain, Windows Credential Manager)
954/// - Hardware security modules (HSMs)
955/// - Secret managers (Vault, AWS Secrets Manager)
956///
957/// The policy itself (public info) is stored in Git at:
958/// `refs/auths/policies/threshold/<policy_id>`
959#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
960#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
961pub struct ThresholdPolicy {
962    /// Minimum signers required (M in M-of-N)
963    pub threshold: u8,
964
965    /// Total authorized signers (N in M-of-N) - DIDs of participants
966    pub signers: Vec<String>,
967
968    /// Unique identifier for this policy
969    pub policy_id: String,
970
971    /// Scope of operations this policy covers (optional)
972    #[serde(default, skip_serializing_if = "Option::is_none")]
973    pub scope: Option<Capability>,
974
975    /// Ceremony coordination endpoint (e.g., WebSocket URL for signing rounds)
976    #[serde(default, skip_serializing_if = "Option::is_none")]
977    pub ceremony_endpoint: Option<String>,
978}
979
980impl ThresholdPolicy {
981    /// Create a new threshold policy
982    pub fn new(threshold: u8, signers: Vec<String>, policy_id: String) -> Self {
983        Self {
984            threshold,
985            signers,
986            policy_id,
987            scope: None,
988            ceremony_endpoint: None,
989        }
990    }
991
992    /// Check if the policy parameters are valid
993    pub fn is_valid(&self) -> bool {
994        // Threshold must be at least 1
995        if self.threshold < 1 {
996            return false;
997        }
998        // Threshold cannot exceed number of signers
999        if self.threshold as usize > self.signers.len() {
1000            return false;
1001        }
1002        // Must have at least one signer
1003        if self.signers.is_empty() {
1004            return false;
1005        }
1006        // Policy ID must not be empty
1007        if self.policy_id.is_empty() {
1008            return false;
1009        }
1010        true
1011    }
1012
1013    /// Returns M (threshold) and N (total signers)
1014    pub fn m_of_n(&self) -> (u8, usize) {
1015        (self.threshold, self.signers.len())
1016    }
1017}
1018
1019#[cfg(test)]
1020mod tests {
1021    use super::*;
1022
1023    // ========================================================================
1024    // Capability serialization tests
1025    // ========================================================================
1026
1027    #[test]
1028    fn capability_serializes_to_snake_case() {
1029        assert_eq!(
1030            serde_json::to_string(&Capability::sign_commit()).unwrap(),
1031            r#""sign_commit""#
1032        );
1033        assert_eq!(
1034            serde_json::to_string(&Capability::sign_release()).unwrap(),
1035            r#""sign_release""#
1036        );
1037        assert_eq!(
1038            serde_json::to_string(&Capability::manage_members()).unwrap(),
1039            r#""manage_members""#
1040        );
1041        assert_eq!(
1042            serde_json::to_string(&Capability::rotate_keys()).unwrap(),
1043            r#""rotate_keys""#
1044        );
1045    }
1046
1047    #[test]
1048    fn capability_deserializes_from_snake_case() {
1049        assert_eq!(
1050            serde_json::from_str::<Capability>(r#""sign_commit""#).unwrap(),
1051            Capability::sign_commit()
1052        );
1053        assert_eq!(
1054            serde_json::from_str::<Capability>(r#""sign_release""#).unwrap(),
1055            Capability::sign_release()
1056        );
1057        assert_eq!(
1058            serde_json::from_str::<Capability>(r#""manage_members""#).unwrap(),
1059            Capability::manage_members()
1060        );
1061        assert_eq!(
1062            serde_json::from_str::<Capability>(r#""rotate_keys""#).unwrap(),
1063            Capability::rotate_keys()
1064        );
1065    }
1066
1067    #[test]
1068    fn capability_custom_serializes_as_string() {
1069        let cap = Capability::parse("acme:deploy").unwrap();
1070        assert_eq!(serde_json::to_string(&cap).unwrap(), r#""acme:deploy""#);
1071    }
1072
1073    #[test]
1074    fn capability_custom_deserializes_unknown_strings() {
1075        // Unknown strings become custom capabilities
1076        let cap: Capability = serde_json::from_str(r#""custom-capability""#).unwrap();
1077        assert_eq!(cap, Capability::parse("custom-capability").unwrap());
1078    }
1079
1080    // ========================================================================
1081    // Capability parse() validation tests
1082    // ========================================================================
1083
1084    #[test]
1085    fn capability_parse_accepts_valid_strings() {
1086        assert!(Capability::parse("deploy").is_ok());
1087        assert!(Capability::parse("acme:deploy").is_ok());
1088        assert!(Capability::parse("my-custom-cap").is_ok());
1089        assert!(Capability::parse("org:team:action").is_ok());
1090        assert!(Capability::parse("with_underscore").is_ok()); // underscore allowed
1091    }
1092
1093    #[test]
1094    fn capability_parse_rejects_invalid_strings() {
1095        // Empty
1096        assert!(matches!(Capability::parse(""), Err(CapabilityError::Empty)));
1097
1098        // Too long
1099        assert!(matches!(
1100            Capability::parse(&"a".repeat(65)),
1101            Err(CapabilityError::TooLong(65))
1102        ));
1103
1104        // Invalid characters
1105        assert!(matches!(
1106            Capability::parse("has spaces"),
1107            Err(CapabilityError::InvalidChars(_))
1108        ));
1109        assert!(matches!(
1110            Capability::parse("has.dot"),
1111            Err(CapabilityError::InvalidChars(_))
1112        ));
1113    }
1114
1115    #[test]
1116    fn capability_parse_rejects_reserved_namespace() {
1117        assert!(matches!(
1118            Capability::parse("auths:custom"),
1119            Err(CapabilityError::ReservedNamespace)
1120        ));
1121        assert!(matches!(
1122            Capability::parse("auths:sign_commit"),
1123            Err(CapabilityError::ReservedNamespace)
1124        ));
1125    }
1126
1127    #[test]
1128    fn capability_parse_normalizes_to_lowercase() {
1129        let cap = Capability::parse("DEPLOY").unwrap();
1130        assert_eq!(cap.as_str(), "deploy");
1131
1132        let cap = Capability::parse("ACME:Deploy").unwrap();
1133        assert_eq!(cap.as_str(), "acme:deploy");
1134    }
1135
1136    #[test]
1137    fn capability_parse_trims_whitespace() {
1138        let cap = Capability::parse("  deploy  ").unwrap();
1139        assert_eq!(cap.as_str(), "deploy");
1140    }
1141
1142    // ========================================================================
1143    // Capability equality and hashing tests
1144    // ========================================================================
1145
1146    #[test]
1147    fn capability_is_hashable() {
1148        use std::collections::HashSet;
1149        let mut set = HashSet::new();
1150        set.insert(Capability::sign_commit());
1151        set.insert(Capability::sign_release());
1152        set.insert(Capability::parse("test").unwrap());
1153        assert_eq!(set.len(), 3);
1154        assert!(set.contains(&Capability::sign_commit()));
1155    }
1156
1157    #[test]
1158    fn capability_equality_with_different_construction_paths() {
1159        // Well-known constructor equals deserialized
1160        let from_constructor = Capability::sign_commit();
1161        let from_deser: Capability = serde_json::from_str(r#""sign_commit""#).unwrap();
1162        assert_eq!(from_constructor, from_deser);
1163
1164        // Parse equals deserialized for custom capabilities
1165        let from_parse = Capability::parse("acme:deploy").unwrap();
1166        let from_deser: Capability = serde_json::from_str(r#""acme:deploy""#).unwrap();
1167        assert_eq!(from_parse, from_deser);
1168    }
1169
1170    // ========================================================================
1171    // Capability display and accessor tests
1172    // ========================================================================
1173
1174    #[test]
1175    fn capability_display_matches_canonical_form() {
1176        assert_eq!(Capability::sign_commit().to_string(), "sign_commit");
1177        assert_eq!(Capability::sign_release().to_string(), "sign_release");
1178        assert_eq!(Capability::manage_members().to_string(), "manage_members");
1179        assert_eq!(Capability::rotate_keys().to_string(), "rotate_keys");
1180        assert_eq!(
1181            Capability::parse("acme:deploy").unwrap().to_string(),
1182            "acme:deploy"
1183        );
1184    }
1185
1186    #[test]
1187    fn capability_as_str_returns_canonical_form() {
1188        assert_eq!(Capability::sign_commit().as_str(), "sign_commit");
1189        assert_eq!(Capability::sign_release().as_str(), "sign_release");
1190        assert_eq!(Capability::manage_members().as_str(), "manage_members");
1191        assert_eq!(Capability::rotate_keys().as_str(), "rotate_keys");
1192        assert_eq!(
1193            Capability::parse("acme:deploy").unwrap().as_str(),
1194            "acme:deploy"
1195        );
1196    }
1197
1198    #[test]
1199    fn capability_is_well_known() {
1200        assert!(Capability::sign_commit().is_well_known());
1201        assert!(Capability::sign_release().is_well_known());
1202        assert!(Capability::manage_members().is_well_known());
1203        assert!(Capability::rotate_keys().is_well_known());
1204        assert!(!Capability::parse("custom").unwrap().is_well_known());
1205    }
1206
1207    #[test]
1208    fn capability_namespace() {
1209        assert_eq!(
1210            Capability::parse("acme:deploy").unwrap().namespace(),
1211            Some("acme")
1212        );
1213        assert_eq!(
1214            Capability::parse("org:team:action").unwrap().namespace(),
1215            Some("org")
1216        );
1217        assert_eq!(Capability::parse("deploy").unwrap().namespace(), None);
1218    }
1219
1220    // ========================================================================
1221    // Capability vec serialization tests
1222    // ========================================================================
1223
1224    #[test]
1225    fn capability_vec_serializes_as_array() {
1226        let caps = vec![Capability::sign_commit(), Capability::sign_release()];
1227        let json = serde_json::to_string(&caps).unwrap();
1228        assert_eq!(json, r#"["sign_commit","sign_release"]"#);
1229    }
1230
1231    #[test]
1232    fn capability_vec_deserializes_from_array() {
1233        let json = r#"["sign_commit","manage_members","custom-cap"]"#;
1234        let caps: Vec<Capability> = serde_json::from_str(json).unwrap();
1235        assert_eq!(caps.len(), 3);
1236        assert_eq!(caps[0], Capability::sign_commit());
1237        assert_eq!(caps[1], Capability::manage_members());
1238        assert_eq!(caps[2], Capability::parse("custom-cap").unwrap());
1239    }
1240
1241    // ========================================================================
1242    // Serde roundtrip tests (critical for backward compat)
1243    // ========================================================================
1244
1245    #[test]
1246    fn capability_serde_roundtrip_well_known() {
1247        let caps = vec![
1248            Capability::sign_commit(),
1249            Capability::sign_release(),
1250            Capability::manage_members(),
1251            Capability::rotate_keys(),
1252        ];
1253        for cap in caps {
1254            let json = serde_json::to_string(&cap).unwrap();
1255            let roundtrip: Capability = serde_json::from_str(&json).unwrap();
1256            assert_eq!(cap, roundtrip);
1257        }
1258    }
1259
1260    #[test]
1261    fn capability_serde_roundtrip_custom() {
1262        let caps = vec![
1263            Capability::parse("deploy").unwrap(),
1264            Capability::parse("acme:deploy").unwrap(),
1265            Capability::parse("org:team:action").unwrap(),
1266        ];
1267        for cap in caps {
1268            let json = serde_json::to_string(&cap).unwrap();
1269            let roundtrip: Capability = serde_json::from_str(&json).unwrap();
1270            assert_eq!(cap, roundtrip);
1271        }
1272    }
1273
1274    // Tests for Attestation org fields (fn-6.2)
1275
1276    #[test]
1277    fn attestation_old_json_without_org_fields_deserializes() {
1278        // Simulates an old attestation JSON without role, capabilities, delegated_by
1279        let old_json = r#"{
1280            "version": 1,
1281            "rid": "test-rid",
1282            "issuer": "did:key:issuer",
1283            "subject": "did:key:subject",
1284            "device_public_key": "0102030405060708091011121314151617181920212223242526272829303132",
1285            "identity_signature": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
1286            "device_signature": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
1287            "revoked_at": null,
1288            "timestamp": null
1289        }"#;
1290
1291        let att: Attestation = serde_json::from_str(old_json).unwrap();
1292
1293        // New fields should have defaults
1294        assert_eq!(att.role, None);
1295        assert!(att.capabilities.is_empty());
1296        assert_eq!(att.delegated_by, None);
1297    }
1298
1299    #[test]
1300    fn attestation_with_org_fields_serializes_correctly() {
1301        use crate::types::DeviceDID;
1302
1303        let att = Attestation {
1304            version: 1,
1305            rid: ResourceId::new("test-rid"),
1306            issuer: IdentityDID::new("did:key:issuer"),
1307            subject: DeviceDID::new("did:key:subject".to_string()),
1308            device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]),
1309            identity_signature: Ed25519Signature::empty(),
1310            device_signature: Ed25519Signature::empty(),
1311            revoked_at: None,
1312            expires_at: None,
1313            timestamp: None,
1314            note: None,
1315            payload: None,
1316            role: Some(Role::Admin),
1317            capabilities: vec![Capability::sign_commit(), Capability::manage_members()],
1318            delegated_by: Some(IdentityDID::new("did:key:delegator")),
1319            signer_type: None,
1320        };
1321
1322        let json = serde_json::to_string(&att).unwrap();
1323        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1324
1325        assert_eq!(parsed["role"], "admin");
1326        assert_eq!(parsed["capabilities"][0], "sign_commit");
1327        assert_eq!(parsed["capabilities"][1], "manage_members");
1328        assert_eq!(parsed["delegated_by"], "did:key:delegator");
1329    }
1330
1331    #[test]
1332    fn attestation_without_org_fields_omits_them_in_json() {
1333        use crate::types::DeviceDID;
1334
1335        let att = Attestation {
1336            version: 1,
1337            rid: ResourceId::new("test-rid"),
1338            issuer: IdentityDID::new("did:key:issuer"),
1339            subject: DeviceDID::new("did:key:subject".to_string()),
1340            device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]),
1341            identity_signature: Ed25519Signature::empty(),
1342            device_signature: Ed25519Signature::empty(),
1343            revoked_at: None,
1344            expires_at: None,
1345            timestamp: None,
1346            note: None,
1347            payload: None,
1348            role: None,
1349            capabilities: vec![],
1350            delegated_by: None,
1351            signer_type: None,
1352        };
1353
1354        let json = serde_json::to_string(&att).unwrap();
1355        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1356
1357        // These fields should not be present in JSON
1358        assert!(parsed.get("role").is_none());
1359        assert!(parsed.get("capabilities").is_none());
1360        assert!(parsed.get("delegated_by").is_none());
1361    }
1362
1363    #[test]
1364    fn attestation_with_org_fields_roundtrips() {
1365        use crate::types::DeviceDID;
1366
1367        let original = Attestation {
1368            version: 1,
1369            rid: ResourceId::new("test-rid"),
1370            issuer: IdentityDID::new("did:key:issuer"),
1371            subject: DeviceDID::new("did:key:subject".to_string()),
1372            device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]),
1373            identity_signature: Ed25519Signature::empty(),
1374            device_signature: Ed25519Signature::empty(),
1375            revoked_at: None,
1376            expires_at: None,
1377            timestamp: None,
1378            note: None,
1379            payload: None,
1380            role: Some(Role::Member),
1381            capabilities: vec![Capability::sign_commit(), Capability::sign_release()],
1382            delegated_by: Some(IdentityDID::new("did:key:admin")),
1383            signer_type: None,
1384        };
1385
1386        let json = serde_json::to_string(&original).unwrap();
1387        let deserialized: Attestation = serde_json::from_str(&json).unwrap();
1388
1389        assert_eq!(original.role, deserialized.role);
1390        assert_eq!(original.capabilities, deserialized.capabilities);
1391        assert_eq!(original.delegated_by, deserialized.delegated_by);
1392    }
1393
1394    // Tests for ThresholdPolicy (fn-6.11)
1395
1396    #[test]
1397    fn threshold_policy_new_creates_valid_policy() {
1398        let policy = ThresholdPolicy::new(
1399            2,
1400            vec![
1401                "did:key:alice".to_string(),
1402                "did:key:bob".to_string(),
1403                "did:key:carol".to_string(),
1404            ],
1405            "test-policy".to_string(),
1406        );
1407
1408        assert_eq!(policy.threshold, 2);
1409        assert_eq!(policy.signers.len(), 3);
1410        assert_eq!(policy.policy_id, "test-policy");
1411        assert!(policy.scope.is_none());
1412        assert!(policy.ceremony_endpoint.is_none());
1413    }
1414
1415    #[test]
1416    fn threshold_policy_is_valid_checks_constraints() {
1417        // Valid 2-of-3
1418        let valid = ThresholdPolicy::new(
1419            2,
1420            vec!["a".to_string(), "b".to_string(), "c".to_string()],
1421            "policy".to_string(),
1422        );
1423        assert!(valid.is_valid());
1424
1425        // Invalid: threshold 0
1426        let zero_threshold = ThresholdPolicy::new(0, vec!["a".to_string()], "policy".to_string());
1427        assert!(!zero_threshold.is_valid());
1428
1429        // Invalid: threshold > signers
1430        let too_high = ThresholdPolicy::new(
1431            3,
1432            vec!["a".to_string(), "b".to_string()],
1433            "policy".to_string(),
1434        );
1435        assert!(!too_high.is_valid());
1436
1437        // Invalid: empty signers
1438        let no_signers = ThresholdPolicy::new(1, vec![], "policy".to_string());
1439        assert!(!no_signers.is_valid());
1440
1441        // Invalid: empty policy_id
1442        let no_id = ThresholdPolicy::new(1, vec!["a".to_string()], "".to_string());
1443        assert!(!no_id.is_valid());
1444    }
1445
1446    #[test]
1447    fn threshold_policy_m_of_n_returns_correct_values() {
1448        let policy = ThresholdPolicy::new(
1449            2,
1450            vec!["a".to_string(), "b".to_string(), "c".to_string()],
1451            "policy".to_string(),
1452        );
1453        let (m, n) = policy.m_of_n();
1454        assert_eq!(m, 2);
1455        assert_eq!(n, 3);
1456    }
1457
1458    #[test]
1459    fn threshold_policy_serializes_correctly() {
1460        let mut policy = ThresholdPolicy::new(
1461            2,
1462            vec!["did:key:alice".to_string(), "did:key:bob".to_string()],
1463            "release-policy".to_string(),
1464        );
1465        policy.scope = Some(Capability::sign_release());
1466        policy.ceremony_endpoint = Some("wss://example.com/ceremony".to_string());
1467
1468        let json = serde_json::to_string(&policy).unwrap();
1469        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1470
1471        assert_eq!(parsed["threshold"], 2);
1472        assert_eq!(parsed["signers"][0], "did:key:alice");
1473        assert_eq!(parsed["policy_id"], "release-policy");
1474        assert_eq!(parsed["scope"], "sign_release");
1475        assert_eq!(parsed["ceremony_endpoint"], "wss://example.com/ceremony");
1476    }
1477
1478    #[test]
1479    fn threshold_policy_without_optional_fields_omits_them() {
1480        let policy =
1481            ThresholdPolicy::new(1, vec!["did:key:alice".to_string()], "policy".to_string());
1482
1483        let json = serde_json::to_string(&policy).unwrap();
1484        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1485
1486        assert!(parsed.get("scope").is_none());
1487        assert!(parsed.get("ceremony_endpoint").is_none());
1488    }
1489
1490    #[test]
1491    fn threshold_policy_roundtrips() {
1492        let mut original = ThresholdPolicy::new(
1493            3,
1494            vec![
1495                "a".to_string(),
1496                "b".to_string(),
1497                "c".to_string(),
1498                "d".to_string(),
1499            ],
1500            "important-policy".to_string(),
1501        );
1502        original.scope = Some(Capability::rotate_keys());
1503
1504        let json = serde_json::to_string(&original).unwrap();
1505        let deserialized: ThresholdPolicy = serde_json::from_str(&json).unwrap();
1506
1507        assert_eq!(original, deserialized);
1508    }
1509
1510    // Tests for IdentityBundle (CI/CD stateless verification)
1511
1512    #[test]
1513    fn identity_bundle_serializes_correctly() {
1514        let bundle = IdentityBundle {
1515            identity_did: "did:keri:test123".to_string(),
1516            public_key_hex: "aabbccdd".to_string(),
1517            attestation_chain: vec![],
1518            bundle_timestamp: DateTime::parse_from_rfc3339("2099-01-01T00:00:00Z")
1519                .unwrap()
1520                .with_timezone(&Utc),
1521            max_valid_for_secs: 86400,
1522        };
1523
1524        let json = serde_json::to_string(&bundle).unwrap();
1525        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1526
1527        assert_eq!(parsed["identity_did"], "did:keri:test123");
1528        assert_eq!(parsed["public_key_hex"], "aabbccdd");
1529        assert!(parsed["attestation_chain"].as_array().unwrap().is_empty());
1530    }
1531
1532    #[test]
1533    fn identity_bundle_deserializes_correctly() {
1534        let json = r#"{
1535            "identity_did": "did:keri:abc123",
1536            "public_key_hex": "112233",
1537            "attestation_chain": [],
1538            "bundle_timestamp": "2099-01-01T00:00:00Z",
1539            "max_valid_for_secs": 86400
1540        }"#;
1541
1542        let bundle: IdentityBundle = serde_json::from_str(json).unwrap();
1543
1544        assert_eq!(bundle.identity_did, "did:keri:abc123");
1545        assert_eq!(bundle.public_key_hex, "112233");
1546        assert!(bundle.attestation_chain.is_empty());
1547    }
1548
1549    #[test]
1550    fn identity_bundle_roundtrips() {
1551        use crate::types::DeviceDID;
1552
1553        let attestation = Attestation {
1554            version: 1,
1555            rid: ResourceId::new("test-rid"),
1556            issuer: IdentityDID::new("did:key:issuer"),
1557            subject: DeviceDID::new("did:key:subject".to_string()),
1558            device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]),
1559            identity_signature: Ed25519Signature::empty(),
1560            device_signature: Ed25519Signature::empty(),
1561            revoked_at: None,
1562            expires_at: None,
1563            timestamp: None,
1564            note: None,
1565            payload: None,
1566            role: None,
1567            capabilities: vec![],
1568            delegated_by: None,
1569            signer_type: None,
1570        };
1571
1572        let original = IdentityBundle {
1573            identity_did: "did:keri:example".to_string(),
1574            public_key_hex: "deadbeef".to_string(),
1575            attestation_chain: vec![attestation],
1576            bundle_timestamp: DateTime::parse_from_rfc3339("2099-01-01T00:00:00Z")
1577                .unwrap()
1578                .with_timezone(&Utc),
1579            max_valid_for_secs: 86400,
1580        };
1581
1582        let json = serde_json::to_string(&original).unwrap();
1583        let deserialized: IdentityBundle = serde_json::from_str(&json).unwrap();
1584
1585        assert_eq!(original.identity_did, deserialized.identity_did);
1586        assert_eq!(original.public_key_hex, deserialized.public_key_hex);
1587        assert_eq!(
1588            original.attestation_chain.len(),
1589            deserialized.attestation_chain.len()
1590        );
1591    }
1592}