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