Skip to main content

auths_verifier/
core.rs

1//! Core attestation types and canonical serialization.
2
3use crate::error::AttestationError;
4use crate::types::{CanonicalDid, IdentityDID};
5use chrono::{DateTime, Utc};
6use hex;
7use json_canon;
8use log::debug;
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11use std::fmt;
12use std::ops::Deref;
13use std::str::FromStr;
14
15/// Maximum allowed size for a single attestation JSON input (64 KiB).
16pub const MAX_ATTESTATION_JSON_SIZE: usize = 64 * 1024;
17
18/// Maximum allowed size for JSON array inputs — chains, receipts, witness keys (1 MiB).
19pub const MAX_JSON_BATCH_SIZE: usize = 1024 * 1024;
20
21/// Maximum hex string length for Ed25519 public key (32 bytes × 2).
22pub const MAX_PUBLIC_KEY_HEX_LEN: usize = 64;
23/// Maximum hex string length for Ed25519 signature (64 bytes × 2).
24pub const MAX_SIGNATURE_HEX_LEN: usize = 128;
25/// Maximum hex string length for SHA-256 file hash (32 bytes × 2).
26pub const MAX_FILE_HASH_HEX_LEN: usize = 64;
27
28// Well-known capability strings (without auths: prefix for backward compat)
29const SIGN_COMMIT: &str = "sign_commit";
30const SIGN_RELEASE: &str = "sign_release";
31const MANAGE_MEMBERS: &str = "manage_members";
32const ROTATE_KEYS: &str = "rotate_keys";
33
34// =============================================================================
35// ResourceId newtype
36// =============================================================================
37
38/// A validated resource identifier linking an attestation to its storage ref.
39///
40/// Wraps a `String` with `#[serde(transparent)]` so JSON output is identical to bare `String`.
41/// Prevents accidental substitution of a DID, Git ref, or other string where a
42/// resource ID is expected.
43#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
44#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
45#[serde(transparent)]
46pub struct ResourceId(String);
47
48impl ResourceId {
49    /// Creates a new ResourceId.
50    pub fn new(s: impl Into<String>) -> Self {
51        Self(s.into())
52    }
53
54    /// Returns the inner string slice.
55    pub fn as_str(&self) -> &str {
56        &self.0
57    }
58}
59
60impl Deref for ResourceId {
61    type Target = str;
62    fn deref(&self) -> &str {
63        &self.0
64    }
65}
66
67impl fmt::Display for ResourceId {
68    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69        f.write_str(&self.0)
70    }
71}
72
73impl From<String> for ResourceId {
74    fn from(s: String) -> Self {
75        Self(s)
76    }
77}
78
79impl From<&str> for ResourceId {
80    fn from(s: &str) -> Self {
81        Self(s.to_string())
82    }
83}
84
85impl PartialEq<str> for ResourceId {
86    fn eq(&self, other: &str) -> bool {
87        self.0 == other
88    }
89}
90
91impl PartialEq<&str> for ResourceId {
92    fn eq(&self, other: &&str) -> bool {
93        self.0 == *other
94    }
95}
96
97impl PartialEq<String> for ResourceId {
98    fn eq(&self, other: &String) -> bool {
99        self.0 == *other
100    }
101}
102
103// =============================================================================
104// Role enum
105// =============================================================================
106
107/// Role classification for organization members.
108///
109/// Governs the default capability set assigned at member authorization time.
110/// Serializes as lowercase strings: `"admin"`, `"member"`, `"readonly"`.
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
112#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
113#[serde(rename_all = "lowercase")]
114pub enum Role {
115    /// Full admin access with all capabilities.
116    Admin,
117    /// Standard member with signing capabilities.
118    Member,
119    /// Read-only access; no signing capabilities.
120    Readonly,
121}
122
123impl Role {
124    /// Returns the canonical string representation.
125    pub fn as_str(&self) -> &str {
126        match self {
127            Role::Admin => "admin",
128            Role::Member => "member",
129            Role::Readonly => "readonly",
130        }
131    }
132
133    /// Return the default capability set for this role.
134    pub fn default_capabilities(&self) -> Vec<Capability> {
135        match self {
136            Role::Admin => vec![
137                Capability::sign_commit(),
138                Capability::sign_release(),
139                Capability::manage_members(),
140                Capability::rotate_keys(),
141            ],
142            Role::Member => vec![Capability::sign_commit(), Capability::sign_release()],
143            Role::Readonly => vec![],
144        }
145    }
146}
147
148impl fmt::Display for Role {
149    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150        f.write_str(self.as_str())
151    }
152}
153
154impl FromStr for Role {
155    type Err = RoleParseError;
156
157    fn from_str(s: &str) -> Result<Self, Self::Err> {
158        match s.trim().to_lowercase().as_str() {
159            "admin" => Ok(Role::Admin),
160            "member" => Ok(Role::Member),
161            "readonly" => Ok(Role::Readonly),
162            other => Err(RoleParseError(other.to_string())),
163        }
164    }
165}
166
167/// Error returned when parsing an invalid role string.
168#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
169#[error("unknown role: '{0}' (expected admin, member, or readonly)")]
170pub struct RoleParseError(String);
171
172// =============================================================================
173// Ed25519PublicKey newtype
174// =============================================================================
175
176/// A 32-byte Ed25519 public key.
177///
178/// Serializes as a hex string for JSON compatibility. Enforces exactly 32 bytes
179/// at construction time.
180#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
181pub struct Ed25519PublicKey([u8; 32]);
182
183impl Ed25519PublicKey {
184    /// Creates a new Ed25519PublicKey from a 32-byte array.
185    pub fn from_bytes(bytes: [u8; 32]) -> Self {
186        Self(bytes)
187    }
188
189    /// Creates a new Ed25519PublicKey from a byte slice.
190    ///
191    /// Args:
192    /// * `slice`: Byte slice that must be exactly 32 bytes.
193    ///
194    /// Usage:
195    /// ```ignore
196    /// let pk = Ed25519PublicKey::try_from_slice(&bytes)?;
197    /// ```
198    pub fn try_from_slice(slice: &[u8]) -> Result<Self, Ed25519KeyError> {
199        let arr: [u8; 32] = slice
200            .try_into()
201            .map_err(|_| Ed25519KeyError::InvalidLength(slice.len()))?;
202        Ok(Self(arr))
203    }
204
205    /// Returns the inner 32-byte array.
206    pub fn as_bytes(&self) -> &[u8; 32] {
207        &self.0
208    }
209
210    /// Returns `true` if all 32 bytes are zero (used for unsigned org-member attestations).
211    pub fn is_zero(&self) -> bool {
212        self.0 == [0u8; 32]
213    }
214}
215
216impl Serialize for Ed25519PublicKey {
217    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
218        s.serialize_str(&hex::encode(self.0))
219    }
220}
221
222impl<'de> Deserialize<'de> for Ed25519PublicKey {
223    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
224        let s = String::deserialize(d)?;
225        let bytes =
226            hex::decode(&s).map_err(|e| serde::de::Error::custom(format!("invalid hex: {e}")))?;
227        let arr: [u8; 32] = bytes.try_into().map_err(|v: Vec<u8>| {
228            serde::de::Error::custom(format!("expected 32 bytes, got {}", v.len()))
229        })?;
230        Ok(Self(arr))
231    }
232}
233
234impl fmt::Display for Ed25519PublicKey {
235    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
236        f.write_str(&hex::encode(self.0))
237    }
238}
239
240impl AsRef<[u8]> for Ed25519PublicKey {
241    fn as_ref(&self) -> &[u8] {
242        &self.0
243    }
244}
245
246#[cfg(feature = "schema")]
247impl schemars::JsonSchema for Ed25519PublicKey {
248    fn schema_name() -> String {
249        "Ed25519PublicKey".to_owned()
250    }
251
252    fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
253        schemars::schema::SchemaObject {
254            instance_type: Some(schemars::schema::InstanceType::String.into()),
255            format: Some("hex".to_owned()),
256            metadata: Some(Box::new(schemars::schema::Metadata {
257                description: Some("Ed25519 public key (32 bytes, hex-encoded)".to_owned()),
258                ..Default::default()
259            })),
260            ..Default::default()
261        }
262        .into()
263    }
264}
265
266/// Error type for Ed25519 public key construction.
267#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
268pub enum Ed25519KeyError {
269    /// The byte slice is not exactly 32 bytes.
270    #[error("expected 32 bytes, got {0}")]
271    InvalidLength(usize),
272    /// The hex string is not valid.
273    #[error("invalid hex: {0}")]
274    InvalidHex(String),
275}
276
277// =============================================================================
278// TypedSignature newtype (fn-114: was Ed25519Signature)
279// =============================================================================
280
281/// Attestation schema version.
282///
283/// Bumped to 2 in fn-114.15 as part of the curve-agnostic refactor (hard break,
284/// pre-launch posture). Attestations serialized under older versions are not
285/// readable — fn-114 has no v1 tolerant reader by design.
286pub const ATTESTATION_VERSION: u32 = 2;
287
288/// A validated 64-byte signature. Curve is determined by the companion
289/// [`DevicePublicKey`] — both Ed25519 and ECDSA P-256 r||s are 64 bytes.
290///
291/// Previously named `Ed25519Signature`. The new name reflects that the byte
292/// container is curve-agnostic; the receiver dispatches verify by looking at
293/// the associated key's curve. If/when a curve with a different signature
294/// length joins the workspace (e.g. ML-DSA-44 at 2420 bytes), this type
295/// graduates to an enum variant.
296#[derive(Debug, Clone, PartialEq, Eq)]
297pub struct TypedSignature([u8; 64]);
298
299/// Transitional alias for pre-fn-114 callers. Removed in fn-114.40.
300pub type Ed25519Signature = TypedSignature;
301
302impl TypedSignature {
303    /// Creates a signature from a 64-byte array.
304    pub fn from_bytes(bytes: [u8; 64]) -> Self {
305        Self(bytes)
306    }
307
308    /// Attempts to create a signature from a byte slice, returning an error if the length is not 64.
309    pub fn try_from_slice(slice: &[u8]) -> Result<Self, SignatureLengthError> {
310        let arr: [u8; 64] = slice
311            .try_into()
312            .map_err(|_| SignatureLengthError(slice.len()))?;
313        Ok(Self(arr))
314    }
315
316    /// Creates an all-zero signature, used as a placeholder.
317    pub fn empty() -> Self {
318        Self([0u8; 64])
319    }
320
321    /// Returns `true` if the signature is all zeros (placeholder).
322    pub fn is_empty(&self) -> bool {
323        self.0 == [0u8; 64]
324    }
325
326    /// Returns a reference to the underlying 64-byte array.
327    pub fn as_bytes(&self) -> &[u8; 64] {
328        &self.0
329    }
330}
331
332impl Default for TypedSignature {
333    fn default() -> Self {
334        Self::empty()
335    }
336}
337
338impl std::fmt::Display for TypedSignature {
339    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
340        write!(f, "{}", hex::encode(self.0))
341    }
342}
343
344impl AsRef<[u8]> for TypedSignature {
345    fn as_ref(&self) -> &[u8] {
346        &self.0
347    }
348}
349
350#[cfg(feature = "schema")]
351impl schemars::JsonSchema for TypedSignature {
352    fn schema_name() -> String {
353        "TypedSignature".to_owned()
354    }
355
356    fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
357        schemars::schema::SchemaObject {
358            instance_type: Some(schemars::schema::InstanceType::String.into()),
359            format: Some("hex".to_owned()),
360            metadata: Some(Box::new(schemars::schema::Metadata {
361                description: Some(
362                    "Curve-agnostic 64-byte signature (Ed25519 or P-256 r||s, hex-encoded). \
363                     Curve is determined by the companion DevicePublicKey."
364                        .to_owned(),
365                ),
366                ..Default::default()
367            })),
368            ..Default::default()
369        }
370        .into()
371    }
372}
373
374impl serde::Serialize for TypedSignature {
375    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
376        serializer.serialize_str(&hex::encode(self.0))
377    }
378}
379
380impl<'de> serde::Deserialize<'de> for TypedSignature {
381    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
382        let s = String::deserialize(deserializer)?;
383        if s.is_empty() {
384            return Ok(Self::empty());
385        }
386        let bytes = hex::decode(&s).map_err(serde::de::Error::custom)?;
387        Self::try_from_slice(&bytes).map_err(serde::de::Error::custom)
388    }
389}
390
391/// Error when constructing an Ed25519Signature from a byte slice of wrong length.
392#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
393#[error("expected 64 bytes, got {0}")]
394pub struct SignatureLengthError(pub usize);
395
396/// A device public key carrying its curve type explicitly.
397///
398/// Curve is stored alongside the raw key bytes so dispatch never relies on
399/// key length — adding a new curve that shares a byte length (e.g. secp256k1,
400/// also 33 bytes compressed) won't break existing match arms.
401///
402/// Accepted byte lengths per curve:
403/// - Ed25519: 32
404/// - P-256: 33 (compressed SEC1) or 65 (uncompressed SEC1)
405#[derive(Debug, Clone)]
406#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
407pub struct DevicePublicKey {
408    #[cfg_attr(feature = "schema", schemars(skip))]
409    curve: auths_crypto::CurveType,
410    #[cfg_attr(feature = "schema", schemars(with = "String"))]
411    bytes: Vec<u8>,
412}
413
414impl DevicePublicKey {
415    /// Canonical SEC1 bytes for equality + hashing. P-256 is normalized to its 33-byte
416    /// compressed form, so two encodings of the *same point* compare equal — the SSH
417    /// wire format is 65-byte uncompressed while the KEL/CESR form is 33-byte compressed.
418    /// Pure byte math: a compressed point is `[0x02|0x03 by Y-parity] || X`, and the
419    /// Y-parity is the uncompressed point's final-byte LSB. Ed25519 is unchanged.
420    fn canonical_sec1(&self) -> std::borrow::Cow<'_, [u8]> {
421        if self.curve == auths_crypto::CurveType::P256
422            && self.bytes.len() == 65
423            && self.bytes[0] == 0x04
424        {
425            let mut compressed = vec![0u8; 33];
426            compressed[0] = if self.bytes[64] & 1 == 0 { 0x02 } else { 0x03 };
427            compressed[1..].copy_from_slice(&self.bytes[1..33]);
428            std::borrow::Cow::Owned(compressed)
429        } else {
430            std::borrow::Cow::Borrowed(&self.bytes)
431        }
432    }
433}
434
435impl PartialEq for DevicePublicKey {
436    fn eq(&self, other: &Self) -> bool {
437        self.curve == other.curve && self.canonical_sec1() == other.canonical_sec1()
438    }
439}
440
441impl Eq for DevicePublicKey {}
442
443impl std::hash::Hash for DevicePublicKey {
444    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
445        self.curve.hash(state);
446        self.canonical_sec1().hash(state);
447    }
448}
449
450/// Error returned when constructing a `DevicePublicKey` with invalid key material.
451#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
452pub enum InvalidKeyError {
453    /// The byte length is wrong for the specified curve.
454    #[error("invalid key length for {curve}: expected {expected}, got {actual}")]
455    InvalidLength {
456        /// The curve that was specified.
457        curve: auths_crypto::CurveType,
458        /// The expected length(s) as a human-readable string.
459        expected: &'static str,
460        /// The actual byte count.
461        actual: usize,
462    },
463}
464
465impl DevicePublicKey {
466    /// Create from a curve type and raw bytes, validating length per curve.
467    ///
468    /// Args:
469    /// * `curve`: Which elliptic curve this key belongs to.
470    /// * `bytes`: Raw public key bytes.
471    ///
472    /// Usage:
473    /// ```ignore
474    /// let pk = DevicePublicKey::try_new(CurveType::Ed25519, &key_bytes)?;
475    /// ```
476    pub fn try_new(curve: auths_crypto::CurveType, bytes: &[u8]) -> Result<Self, InvalidKeyError> {
477        let valid = match curve {
478            auths_crypto::CurveType::Ed25519 => bytes.len() == 32,
479            auths_crypto::CurveType::P256 => bytes.len() == 33 || bytes.len() == 65,
480        };
481        if !valid {
482            return Err(InvalidKeyError::InvalidLength {
483                curve,
484                expected: match curve {
485                    auths_crypto::CurveType::Ed25519 => "32",
486                    auths_crypto::CurveType::P256 => "33 or 65",
487                },
488                actual: bytes.len(),
489            });
490        }
491        Ok(Self {
492            curve,
493            bytes: bytes.to_vec(),
494        })
495    }
496
497    /// Create an Ed25519 device key from raw 32-byte key.
498    pub fn ed25519(bytes: &[u8; 32]) -> Self {
499        Self {
500            curve: auths_crypto::CurveType::Ed25519,
501            bytes: bytes.to_vec(),
502        }
503    }
504
505    /// Create a P-256 device key from raw SEC1 bytes (33 compressed or 65 uncompressed).
506    ///
507    /// Returns `Err` if `bytes` is not 33 or 65 bytes.
508    pub fn p256(bytes: &[u8]) -> Result<Self, InvalidKeyError> {
509        Self::try_new(auths_crypto::CurveType::P256, bytes)
510    }
511
512    /// Returns the curve type.
513    pub fn curve(&self) -> auths_crypto::CurveType {
514        self.curve
515    }
516
517    /// Returns the raw key bytes.
518    pub fn as_bytes(&self) -> &[u8] {
519        &self.bytes
520    }
521
522    /// Returns true if all bytes are zero.
523    pub fn is_zero(&self) -> bool {
524        self.bytes.iter().all(|&b| b == 0)
525    }
526
527    /// Returns the byte length.
528    pub fn len(&self) -> usize {
529        self.bytes.len()
530    }
531
532    /// Returns true if empty.
533    pub fn is_empty(&self) -> bool {
534        self.bytes.is_empty()
535    }
536
537    /// Verify a signature against this key, dispatching on curve.
538    ///
539    /// This is the single canonical verify method — every caller that holds
540    /// a `DevicePublicKey` should call this rather than branching on curve
541    /// themselves. Adding a new curve means updating this one impl; the
542    /// compiler then flags every call site still assuming only Ed25519.
543    ///
544    /// Args:
545    /// * `message`: Payload bytes that were signed.
546    /// * `signature`: Raw signature bytes (64 for Ed25519 / P-256).
547    /// * `provider`: Pluggable crypto provider (`RingCryptoProvider` native,
548    ///   `WebCryptoProvider` wasm) — used for Ed25519. P-256 routes through
549    ///   `RingCryptoProvider::p256_verify` on native.
550    ///
551    /// Usage:
552    /// ```ignore
553    /// issuer_pk.verify(&payload, &signature, provider).await?;
554    /// ```
555    pub async fn verify(
556        &self,
557        message: &[u8],
558        signature: &[u8],
559        provider: &dyn auths_crypto::CryptoProvider,
560    ) -> Result<(), SignatureVerifyError> {
561        let result = match self.curve {
562            auths_crypto::CurveType::Ed25519 => {
563                provider
564                    .verify_ed25519(&self.bytes, message, signature)
565                    .await
566            }
567            auths_crypto::CurveType::P256 => {
568                provider.verify_p256(&self.bytes, message, signature).await
569            }
570        };
571        result.map_err(|e| SignatureVerifyError::VerificationFailed(e.to_string()))
572    }
573}
574
575/// Error returned by the typed ingestion helpers
576/// [`decode_public_key_hex`] / [`decode_public_key_bytes`].
577#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
578pub enum PublicKeyDecodeError {
579    /// The input hex string failed to decode.
580    #[error("invalid hex: {0}")]
581    InvalidHex(String),
582
583    /// The decoded byte length does not match any supported curve (32 Ed25519,
584    /// 33 or 65 P-256).
585    #[error("invalid public key length {len} — expected 32 (Ed25519) or 33/65 (P-256)")]
586    InvalidLength {
587        /// Number of bytes supplied.
588        len: usize,
589    },
590
591    /// `DevicePublicKey::try_new` rejected the bytes even after curve
592    /// inference succeeded.
593    #[error("DevicePublicKey validation failed: {0}")]
594    Validation(String),
595}
596
597/// Decode a hex-encoded public key string into a typed, curve-tagged
598/// `DevicePublicKey`.
599///
600/// Intended for external ingestion boundaries ONLY (FFI hex strings, WASM
601/// entry points, `--signer-key` CLI flags). Internal code threads
602/// `DevicePublicKey` end-to-end.
603///
604/// Args:
605/// * `hex_str`: Hex-encoded public key (32, 33, or 65 bytes after decode).
606/// * `curve`: The curve type for the key.
607///
608/// Usage:
609/// ```ignore
610/// let pk = decode_public_key_hex(user_supplied_hex, CurveType::P256)?;
611/// issuer_pk.verify(msg, sig, provider).await?;
612/// ```
613pub fn decode_public_key_hex(
614    hex_str: &str,
615    curve: auths_crypto::CurveType,
616) -> Result<DevicePublicKey, PublicKeyDecodeError> {
617    let bytes =
618        hex::decode(hex_str.trim()).map_err(|e| PublicKeyDecodeError::InvalidHex(e.to_string()))?;
619    decode_public_key_bytes(&bytes, curve)
620}
621
622/// Decode raw public key bytes into a typed, curve-tagged `DevicePublicKey`
623/// using an explicit curve tag.
624///
625/// Args:
626/// * `bytes`: Raw public key bytes.
627/// * `curve`: The curve type for the key.
628///
629/// Usage:
630/// ```ignore
631/// let pk = decode_public_key_bytes(&ffi_bytes[..len], CurveType::P256)?;
632/// ```
633pub fn decode_public_key_bytes(
634    bytes: &[u8],
635    curve: auths_crypto::CurveType,
636) -> Result<DevicePublicKey, PublicKeyDecodeError> {
637    DevicePublicKey::try_new(curve, bytes)
638        .map_err(|e| PublicKeyDecodeError::Validation(e.to_string()))
639}
640
641/// Error returned by [`DevicePublicKey::verify`].
642#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
643pub enum SignatureVerifyError {
644    /// Underlying signature check failed. Argument contains provider-specific detail.
645    #[error("signature verification failed: {0}")]
646    VerificationFailed(String),
647
648    /// The target platform does not support the requested curve (e.g. WASM
649    /// without the `native` feature bundled).
650    #[error("{0}")]
651    UnsupportedOnTarget(String),
652}
653
654impl From<Ed25519PublicKey> for DevicePublicKey {
655    fn from(pk: Ed25519PublicKey) -> Self {
656        Self {
657            curve: auths_crypto::CurveType::Ed25519,
658            bytes: pk.as_bytes().to_vec(),
659        }
660    }
661}
662
663impl Serialize for DevicePublicKey {
664    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
665        use serde::ser::SerializeStruct;
666        let mut st = s.serialize_struct("DevicePublicKey", 2)?;
667        st.serialize_field("curve", &self.curve.to_string())?;
668        st.serialize_field("key", &hex::encode(&self.bytes))?;
669        st.end()
670    }
671}
672
673impl<'de> Deserialize<'de> for DevicePublicKey {
674    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
675        // Accept both formats:
676        // - New: {"curve": "p256", "key": "hex..."}
677        // - Legacy: "hex..." (bare string, infer curve from length)
678        let value = serde_json::Value::deserialize(d)?;
679
680        if let Some(s) = value.as_str() {
681            // Legacy format: bare hex string
682            if s.is_empty() {
683                return Ok(Self::default());
684            }
685            let bytes = hex::decode(s)
686                .map_err(|e| serde::de::Error::custom(format!("invalid hex: {e}")))?;
687            let curve = match bytes.len() {
688                32 => auths_crypto::CurveType::Ed25519,
689                33 | 65 => auths_crypto::CurveType::P256,
690                n => {
691                    return Err(serde::de::Error::custom(format!(
692                        "invalid device public key length: {n}"
693                    )));
694                }
695            };
696            return Self::try_new(curve, &bytes)
697                .map_err(|e| serde::de::Error::custom(e.to_string()));
698        }
699
700        // New format: {"curve": "...", "key": "..."}
701        let curve_str = value["curve"]
702            .as_str()
703            .ok_or_else(|| serde::de::Error::custom("missing 'curve' field"))?;
704        let key_hex = value["key"]
705            .as_str()
706            .ok_or_else(|| serde::de::Error::custom("missing 'key' field"))?;
707
708        let curve = match curve_str {
709            "ed25519" => auths_crypto::CurveType::Ed25519,
710            "p256" => auths_crypto::CurveType::P256,
711            other => {
712                return Err(serde::de::Error::custom(format!("unknown curve: {other}")));
713            }
714        };
715        if key_hex.is_empty() {
716            return Err(serde::de::Error::custom("empty key"));
717        }
718        let bytes = hex::decode(key_hex)
719            .map_err(|e| serde::de::Error::custom(format!("invalid hex: {e}")))?;
720        Self::try_new(curve, &bytes).map_err(|e| serde::de::Error::custom(e.to_string()))
721    }
722}
723
724impl Default for DevicePublicKey {
725    fn default() -> Self {
726        Self {
727            curve: auths_crypto::CurveType::Ed25519,
728            bytes: vec![0u8; 32],
729        }
730    }
731}
732
733impl fmt::Display for DevicePublicKey {
734    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
735        write!(f, "{}:{}", self.curve, hex::encode(&self.bytes))
736    }
737}
738
739// =============================================================================
740// Signature algorithm enum (for configurable checkpoint verification)
741// =============================================================================
742
743/// Signature algorithm used by a transparency log for checkpoint signing.
744///
745/// Each log in a `TrustConfig` specifies which algorithm its checkpoints use.
746/// The verifier dispatches on this when checking checkpoint signatures.
747///
748/// Usage:
749/// ```ignore
750/// match trust_root.signature_algorithm {
751///     SignatureAlgorithm::Ed25519 => verify_ed25519(..),
752///     SignatureAlgorithm::EcdsaP256 => verify_ecdsa_p256(..),
753/// }
754/// ```
755#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
756#[serde(rename_all = "snake_case")]
757pub enum SignatureAlgorithm {
758    /// Ed25519 (RFC 8032). Default for auths-native logs.
759    #[default]
760    Ed25519,
761    /// ECDSA with NIST P-256 and SHA-256. Used by Rekor production shard.
762    EcdsaP256,
763}
764
765impl fmt::Display for SignatureAlgorithm {
766    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
767        match self {
768            Self::Ed25519 => f.write_str("ed25519"),
769            Self::EcdsaP256 => f.write_str("ecdsa_p256"),
770        }
771    }
772}
773
774// =============================================================================
775// ECDSA P-256 types (for Rekor checkpoint verification)
776// =============================================================================
777
778/// A DER-encoded ECDSA P-256 public key (PKIX SubjectPublicKeyInfo).
779///
780/// Stores the full DER encoding so `ring::signature::UnparsedPublicKey`
781/// can consume it directly.
782///
783/// Usage:
784/// ```ignore
785/// let pk = EcdsaP256PublicKey::from_der(&der_bytes)?;
786/// ```
787#[derive(Debug, Clone, PartialEq, Eq)]
788pub struct EcdsaP256PublicKey(Vec<u8>);
789
790impl EcdsaP256PublicKey {
791    /// Creates from DER-encoded PKIX SubjectPublicKeyInfo bytes.
792    ///
793    /// Performs minimal validation: checks the ASN.1 OID prefix for P-256
794    /// (`06 08 2a 86 48 ce 3d 03 01 07`).
795    pub fn from_der(der: &[u8]) -> Result<Self, EcdsaP256Error> {
796        // PKIX P-256 key is typically 91 bytes (SEQUENCE { AlgorithmIdentifier, BIT STRING })
797        // The AlgorithmIdentifier contains OID 1.2.840.10045.3.1.7 (P-256)
798        const P256_OID: &[u8] = &[0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07];
799        if der.len() < 26 {
800            return Err(EcdsaP256Error::InvalidKey(
801                "DER too short for P-256 PKIX key".into(),
802            ));
803        }
804        if !der.windows(P256_OID.len()).any(|w| w == P256_OID) {
805            return Err(EcdsaP256Error::InvalidKey(
806                "missing P-256 OID in PKIX key".into(),
807            ));
808        }
809        Ok(Self(der.to_vec()))
810    }
811
812    /// Returns the raw DER bytes.
813    pub fn as_der(&self) -> &[u8] {
814        &self.0
815    }
816
817    /// Returns the public key as a raw uncompressed SEC1 point
818    /// (`0x04 || X || Y`, 65 bytes).
819    ///
820    /// This is the form `ring`'s `ECDSA_P256_SHA256_ASN1` verifier consumes — it
821    /// does NOT accept the DER SPKI returned by [`Self::as_der`]. For a
822    /// well-formed P-256 SPKI the point is the trailing 65 bytes (the BIT STRING
823    /// value, sans its `0x00` unused-bits prefix). Returns `None` for a
824    /// compressed or malformed key.
825    ///
826    /// Usage:
827    /// ```ignore
828    /// let point = key.as_sec1_uncompressed().ok_or(/* malformed */)?;
829    /// let verifier = UnparsedPublicKey::new(&ECDSA_P256_SHA256_ASN1, point);
830    /// ```
831    pub fn as_sec1_uncompressed(&self) -> Option<&[u8]> {
832        let start = self.0.len().checked_sub(65)?;
833        let point = &self.0[start..];
834        (point[0] == 0x04).then_some(point)
835    }
836}
837
838impl Serialize for EcdsaP256PublicKey {
839    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
840        use base64::Engine;
841        s.serialize_str(&base64::engine::general_purpose::STANDARD.encode(&self.0))
842    }
843}
844
845impl<'de> Deserialize<'de> for EcdsaP256PublicKey {
846    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
847        use base64::Engine;
848        let s = String::deserialize(d)?;
849        let bytes = base64::engine::general_purpose::STANDARD
850            .decode(&s)
851            .map_err(|e| serde::de::Error::custom(format!("invalid base64: {e}")))?;
852        Self::from_der(&bytes).map_err(serde::de::Error::custom)
853    }
854}
855
856/// A DER-encoded ECDSA P-256 signature.
857///
858/// ECDSA signatures are variable-length ASN.1 DER (typically 70-72 bytes).
859///
860/// Usage:
861/// ```ignore
862/// let sig = EcdsaP256Signature::from_der(&der_bytes)?;
863/// ```
864#[derive(Debug, Clone, PartialEq, Eq)]
865pub struct EcdsaP256Signature(Vec<u8>);
866
867impl EcdsaP256Signature {
868    /// Creates from DER-encoded signature bytes.
869    pub fn from_der(der: &[u8]) -> Result<Self, EcdsaP256Error> {
870        // Minimal check: ASN.1 SEQUENCE tag (0x30)
871        if der.is_empty() || der[0] != 0x30 {
872            return Err(EcdsaP256Error::InvalidSignature(
873                "not an ASN.1 SEQUENCE".into(),
874            ));
875        }
876        Ok(Self(der.to_vec()))
877    }
878
879    /// Returns the raw DER bytes.
880    pub fn as_der(&self) -> &[u8] {
881        &self.0
882    }
883}
884
885impl Serialize for EcdsaP256Signature {
886    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
887        use base64::Engine;
888        s.serialize_str(&base64::engine::general_purpose::STANDARD.encode(&self.0))
889    }
890}
891
892impl<'de> Deserialize<'de> for EcdsaP256Signature {
893    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
894        use base64::Engine;
895        let s = String::deserialize(d)?;
896        let bytes = base64::engine::general_purpose::STANDARD
897            .decode(&s)
898            .map_err(|e| serde::de::Error::custom(format!("invalid base64: {e}")))?;
899        Self::from_der(&bytes).map_err(serde::de::Error::custom)
900    }
901}
902
903/// Errors from ECDSA P-256 operations.
904#[derive(Debug, Clone, thiserror::Error)]
905pub enum EcdsaP256Error {
906    /// Invalid DER-encoded public key.
907    #[error("invalid ECDSA P-256 key: {0}")]
908    InvalidKey(String),
909    /// Invalid DER-encoded signature.
910    #[error("invalid ECDSA P-256 signature: {0}")]
911    InvalidSignature(String),
912}
913
914// =============================================================================
915// Capability types
916// =============================================================================
917
918/// Error type for capability parsing and validation.
919#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
920pub enum CapabilityError {
921    /// The capability string is empty.
922    #[error("capability is empty")]
923    Empty,
924    /// The capability string exceeds the maximum length.
925    #[error("capability exceeds 64 chars: {0}")]
926    TooLong(usize),
927    /// The capability string contains invalid characters.
928    #[error("invalid characters in capability '{0}': only alphanumeric, ':', '-', '_' allowed")]
929    InvalidChars(String),
930    /// The capability uses the reserved 'auths:' namespace.
931    #[error(
932        "reserved namespace 'auths:' — use well-known constructors or choose a different prefix"
933    )]
934    ReservedNamespace,
935    /// The capability uses a reserved infrastructure namespace prefix.
936    #[error("the '{0}' prefix is reserved for infrastructure capabilities")]
937    ReservedInfraNamespace(String),
938}
939
940/// A validated capability identifier.
941///
942/// Capabilities are the atomic unit of authorization in Auths.
943/// They follow a namespace convention:
944///
945/// - Well-known capabilities: `sign_commit`, `sign_release`, `manage_members`, `rotate_keys`
946/// - Custom capabilities: any valid string (alphanumeric + `:` + `-` + `_`, max 64 chars)
947///
948/// The `auths:` prefix is reserved for future well-known capabilities and cannot be
949/// used in custom capabilities created via `parse()`.
950///
951/// # Examples
952///
953/// ```
954/// use auths_verifier::Capability;
955///
956/// // Well-known capabilities
957/// let cap = Capability::sign_commit();
958/// assert_eq!(cap.as_str(), "sign_commit");
959///
960/// // Custom capabilities
961/// let custom = Capability::parse("acme:deploy").unwrap();
962/// assert_eq!(custom.as_str(), "acme:deploy");
963///
964/// // Reserved namespace is rejected
965/// assert!(Capability::parse("auths:custom").is_err());
966/// ```
967#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
968#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
969#[serde(try_from = "String", into = "String")]
970pub struct Capability(String);
971
972impl Capability {
973    /// Maximum length for capability strings.
974    pub const MAX_LEN: usize = 64;
975
976    /// Reserved namespace prefix for Auths well-known capabilities.
977    const RESERVED_PREFIX: &'static str = "auths:";
978
979    /// Reserved infrastructure capability namespace prefixes.
980    const RESERVED_INFRA_PREFIXES: &'static [&'static str] =
981        &["compute:", "network:", "storage:", "runtime:", "env:"];
982
983    // ========================================================================
984    // Well-known capability constructors
985    // ========================================================================
986
987    /// Creates the `sign_commit` capability.
988    ///
989    /// Grants permission to sign commits.
990    #[inline]
991    pub fn sign_commit() -> Self {
992        Self(SIGN_COMMIT.to_string())
993    }
994
995    /// Creates the `sign_release` capability.
996    ///
997    /// Grants permission to sign releases.
998    #[inline]
999    pub fn sign_release() -> Self {
1000        Self(SIGN_RELEASE.to_string())
1001    }
1002
1003    /// Creates the `manage_members` capability.
1004    ///
1005    /// Grants permission to add/remove members in an organization.
1006    #[inline]
1007    pub fn manage_members() -> Self {
1008        Self(MANAGE_MEMBERS.to_string())
1009    }
1010
1011    /// Creates the `rotate_keys` capability.
1012    ///
1013    /// Grants permission to rotate keys for an identity.
1014    #[inline]
1015    pub fn rotate_keys() -> Self {
1016        Self(ROTATE_KEYS.to_string())
1017    }
1018
1019    // ========================================================================
1020    // Parsing and validation
1021    // ========================================================================
1022
1023    /// Parses and validates a capability string.
1024    ///
1025    /// This is the primary way to create custom capabilities. The input is
1026    /// trimmed and lowercased to produce a canonical form.
1027    ///
1028    /// # Validation Rules
1029    ///
1030    /// - Non-empty
1031    /// - Maximum 64 characters
1032    /// - Only alphanumeric characters, colons (`:`), hyphens (`-`), and underscores (`_`)
1033    /// - Cannot start with `auths:` (reserved namespace)
1034    ///
1035    /// # Examples
1036    ///
1037    /// ```
1038    /// use auths_verifier::Capability;
1039    ///
1040    /// // Valid custom capabilities
1041    /// assert!(Capability::parse("deploy").is_ok());
1042    /// assert!(Capability::parse("acme:deploy").is_ok());
1043    /// assert!(Capability::parse("org:team:action").is_ok());
1044    ///
1045    /// // Invalid capabilities
1046    /// assert!(Capability::parse("").is_err());           // empty
1047    /// assert!(Capability::parse("has space").is_err());  // invalid char
1048    /// assert!(Capability::parse("auths:custom").is_err()); // reserved namespace
1049    /// ```
1050    pub fn parse(raw: &str) -> Result<Self, CapabilityError> {
1051        let canonical = raw.trim().to_lowercase();
1052
1053        if canonical.is_empty() {
1054            return Err(CapabilityError::Empty);
1055        }
1056        if canonical.len() > Self::MAX_LEN {
1057            return Err(CapabilityError::TooLong(canonical.len()));
1058        }
1059        if !canonical
1060            .chars()
1061            .all(|c| c.is_alphanumeric() || c == ':' || c == '-' || c == '_')
1062        {
1063            return Err(CapabilityError::InvalidChars(canonical));
1064        }
1065        if canonical.starts_with(Self::RESERVED_PREFIX) {
1066            return Err(CapabilityError::ReservedNamespace);
1067        }
1068        for prefix in Self::RESERVED_INFRA_PREFIXES {
1069            if canonical.starts_with(prefix) {
1070                return Err(CapabilityError::ReservedInfraNamespace(prefix.to_string()));
1071            }
1072        }
1073
1074        Ok(Self(canonical))
1075    }
1076
1077    /// Creates a custom capability after validation.
1078    ///
1079    /// This is a convenience method that returns `Option<Self>` instead of `Result`.
1080    ///
1081    /// # Deprecated
1082    ///
1083    /// Prefer using `parse()` for better error handling.
1084    #[deprecated(since = "0.2.0", note = "Use parse() for better error handling")]
1085    pub fn custom(s: impl Into<String>) -> Option<Self> {
1086        Self::parse(&s.into()).ok()
1087    }
1088
1089    /// Validates a custom capability string.
1090    ///
1091    /// # Deprecated
1092    ///
1093    /// This method is retained for backward compatibility. Use `parse()` instead.
1094    #[deprecated(since = "0.2.0", note = "Use parse() for validation")]
1095    pub fn validate_custom(s: &str) -> bool {
1096        Self::parse(s).is_ok()
1097    }
1098
1099    // ========================================================================
1100    // Accessors
1101    // ========================================================================
1102
1103    /// Returns the canonical string representation of this capability.
1104    ///
1105    /// This is the authoritative string form used for comparison, display,
1106    /// and serialization.
1107    #[inline]
1108    pub fn as_str(&self) -> &str {
1109        &self.0
1110    }
1111
1112    /// Returns `true` if this is a well-known Auths capability.
1113    pub fn is_well_known(&self) -> bool {
1114        matches!(
1115            self.0.as_str(),
1116            SIGN_COMMIT | SIGN_RELEASE | MANAGE_MEMBERS | ROTATE_KEYS
1117        )
1118    }
1119
1120    /// Returns the namespace portion of the capability (before first colon), if any.
1121    pub fn namespace(&self) -> Option<&str> {
1122        self.0.split(':').next().filter(|_| self.0.contains(':'))
1123    }
1124}
1125
1126impl fmt::Display for Capability {
1127    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1128        f.write_str(&self.0)
1129    }
1130}
1131
1132impl TryFrom<String> for Capability {
1133    type Error = CapabilityError;
1134
1135    fn try_from(s: String) -> Result<Self, Self::Error> {
1136        let canonical = s.trim().to_lowercase();
1137
1138        if canonical.is_empty() {
1139            return Err(CapabilityError::Empty);
1140        }
1141        if canonical.len() > Self::MAX_LEN {
1142            return Err(CapabilityError::TooLong(canonical.len()));
1143        }
1144        if !canonical
1145            .chars()
1146            .all(|c| c.is_alphanumeric() || c == ':' || c == '-' || c == '_')
1147        {
1148            return Err(CapabilityError::InvalidChars(canonical));
1149        }
1150
1151        // During deserialization, allow well-known capabilities and auths: prefix
1152        // This ensures backward compatibility with existing attestations
1153        Ok(Self(canonical))
1154    }
1155}
1156
1157impl std::str::FromStr for Capability {
1158    type Err = CapabilityError;
1159
1160    /// Parses a capability string with CLI-friendly alias resolution.
1161    ///
1162    /// Normalizes the input (trim, lowercase, replace hyphens with underscores)
1163    /// and matches well-known capabilities before falling through to
1164    /// `Capability::parse()` for custom capability validation.
1165    ///
1166    /// Unlike the deprecated `parse_capability_cli`, this returns an error
1167    /// for unrecognized well-known names instead of silently defaulting.
1168    ///
1169    /// Args:
1170    /// * `s`: The capability string (e.g., "sign_commit", "Sign-Commit").
1171    ///
1172    /// Usage:
1173    /// ```
1174    /// use auths_verifier::Capability;
1175    /// let cap: Capability = "sign_commit".parse().unwrap();
1176    /// assert_eq!(cap.as_str(), "sign_commit");
1177    /// ```
1178    fn from_str(s: &str) -> Result<Self, Self::Err> {
1179        let normalized = s.trim().to_lowercase().replace('-', "_");
1180        match normalized.as_str() {
1181            "sign_commit" | "signcommit" => Ok(Capability::sign_commit()),
1182            "sign_release" | "signrelease" => Ok(Capability::sign_release()),
1183            "manage_members" | "managemembers" => Ok(Capability::manage_members()),
1184            "rotate_keys" | "rotatekeys" => Ok(Capability::rotate_keys()),
1185            _ => Capability::parse(&normalized),
1186        }
1187    }
1188}
1189
1190impl From<Capability> for String {
1191    fn from(cap: Capability) -> Self {
1192        cap.0
1193    }
1194}
1195
1196/// An identity bundle for stateless verification in CI/CD environments.
1197///
1198/// Contains all the information needed to verify commit signatures without
1199/// requiring access to the identity repository or daemon.
1200#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1201#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1202pub struct IdentityBundle {
1203    /// The DID of the identity (e.g., `"did:keri:..."`)
1204    pub identity_did: IdentityDID,
1205    /// The public key in hex format for signature verification.
1206    pub public_key_hex: PublicKeyHex,
1207    /// Curve of `public_key_hex`. Carried in-band so verifiers never infer
1208    /// curve from byte length. Defaults to P-256 when absent (older bundles
1209    /// shipped before this field existed).
1210    #[serde(default)]
1211    #[cfg_attr(feature = "schema", schemars(with = "String"))]
1212    pub curve: auths_crypto::CurveType,
1213    /// Chain of attestations linking the signing key to the identity
1214    pub attestation_chain: Vec<Attestation>,
1215    /// UTC timestamp when this bundle was created
1216    pub bundle_timestamp: DateTime<Utc>,
1217    /// Maximum age in seconds before this bundle is considered stale
1218    pub max_valid_for_secs: u64,
1219}
1220
1221impl IdentityBundle {
1222    /// Check that this bundle is still within its TTL.
1223    ///
1224    /// Args:
1225    /// * `now`: The current time, injected for deterministic verification.
1226    ///
1227    /// Usage:
1228    /// ```ignore
1229    /// bundle.check_freshness(Utc::now())?;
1230    /// ```
1231    pub fn check_freshness(&self, now: DateTime<Utc>) -> Result<(), AttestationError> {
1232        let age = (now - self.bundle_timestamp).num_seconds().max(0) as u64;
1233        if age > self.max_valid_for_secs {
1234            return Err(AttestationError::BundleExpired {
1235                age_secs: age,
1236                max_secs: self.max_valid_for_secs,
1237            });
1238        }
1239        Ok(())
1240    }
1241}
1242
1243/// Represents a 2-way key attestation between a primary identity and a device key.
1244#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1245#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1246pub struct Attestation {
1247    /// Schema version.
1248    pub version: u32,
1249    /// Record identifier linking this attestation to its storage ref.
1250    pub rid: ResourceId,
1251    /// DID of the issuing identity (can be `did:keri:` or `did:key:`).
1252    pub issuer: CanonicalDid,
1253    /// DID of the attested subject (device `did:key:` or identity `did:keri:`).
1254    pub subject: CanonicalDid,
1255    /// Device public key (32 bytes Ed25519 or 33 bytes P-256 compressed, hex-encoded in JSON).
1256    pub device_public_key: DevicePublicKey,
1257    /// Issuer's Ed25519 signature over the canonical attestation data (hex-encoded in JSON).
1258    #[serde(default, skip_serializing_if = "Ed25519Signature::is_empty")]
1259    pub identity_signature: Ed25519Signature,
1260    /// Device's Ed25519 signature over the canonical attestation data (hex-encoded in JSON).
1261    pub device_signature: Ed25519Signature,
1262    /// Timestamp when the attestation was revoked, if applicable.
1263    #[serde(default, skip_serializing_if = "Option::is_none")]
1264    pub revoked_at: Option<DateTime<Utc>>,
1265    /// Expiration timestamp, if set.
1266    #[serde(skip_serializing_if = "Option::is_none")]
1267    pub expires_at: Option<DateTime<Utc>>,
1268    /// Creation timestamp.
1269    pub timestamp: Option<DateTime<Utc>>,
1270    /// Optional human-readable note.
1271    #[serde(skip_serializing_if = "Option::is_none")]
1272    pub note: Option<String>,
1273    /// Optional arbitrary JSON payload.
1274    #[serde(skip_serializing_if = "Option::is_none")]
1275    pub payload: Option<Value>,
1276
1277    /// Git commit SHA (for commit signing attestations).
1278    #[serde(skip_serializing_if = "Option::is_none")]
1279    pub commit_sha: Option<String>,
1280
1281    /// Git commit message (for commit signing attestations).
1282    #[serde(skip_serializing_if = "Option::is_none")]
1283    pub commit_message: Option<String>,
1284
1285    /// Git commit author (for commit signing attestations).
1286    #[serde(skip_serializing_if = "Option::is_none")]
1287    pub author: Option<String>,
1288
1289    /// OIDC binding information (issuer, subject, audience, expiration).
1290    #[serde(skip_serializing_if = "Option::is_none")]
1291    pub oidc_binding: Option<OidcBinding>,
1292
1293    /// DID of the attestation that delegated authority (for chain tracking).
1294    #[serde(default, skip_serializing_if = "Option::is_none")]
1295    pub delegated_by: Option<CanonicalDid>,
1296
1297    /// The type of entity that produced this signature (human, agent, workload).
1298    /// Included in the canonical JSON before signing — the signature covers this field.
1299    #[serde(default, skip_serializing_if = "Option::is_none")]
1300    pub signer_type: Option<SignerType>,
1301
1302    /// Unsigned environment claim for gateway-level verification via `auths-env`.
1303    /// Excluded from `CanonicalAttestationData` — does not affect signatures.
1304    #[serde(default, skip_serializing_if = "Option::is_none")]
1305    pub environment_claim: Option<Value>,
1306}
1307
1308/// OIDC token binding information for machine identity attestations.
1309///
1310/// Proves that the attestation was created by a CI/CD workload with a specific
1311/// OIDC token. Contains the issuer, subject, audience, and expiration so verifiers
1312/// can reconstruct the identity without needing the ephemeral private key.
1313#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1314#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1315pub struct OidcBinding {
1316    /// OIDC token issuer (e.g., "https://token.actions.githubusercontent.com").
1317    pub issuer: String,
1318    /// Token subject (unique workload identifier).
1319    pub subject: String,
1320    /// Expected audience.
1321    pub audience: String,
1322    /// Token expiration timestamp (Unix timestamp).
1323    pub token_exp: i64,
1324    /// CI/CD platform (e.g., "github", "gitlab", "circleci").
1325    #[serde(default, skip_serializing_if = "Option::is_none")]
1326    pub platform: Option<String>,
1327    /// JTI for replay detection (if available).
1328    #[serde(default, skip_serializing_if = "Option::is_none")]
1329    pub jti: Option<String>,
1330    /// Platform-normalized claims (e.g., repo, actor, run_id for GitHub).
1331    #[serde(default, skip_serializing_if = "Option::is_none")]
1332    pub normalized_claims: Option<serde_json::Map<String, serde_json::Value>>,
1333}
1334
1335/// The type of entity that produced a signature.
1336///
1337/// Duplicated here (also in `auths-policy`) because `auths-verifier` is a
1338/// standalone minimal-dependency crate that cannot depend on `auths-policy`.
1339#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1340#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1341#[non_exhaustive]
1342pub enum SignerType {
1343    /// A human user.
1344    Human,
1345    /// An autonomous AI agent.
1346    Agent,
1347    /// A CI/CD workload or service identity.
1348    Workload,
1349}
1350
1351/// An attestation that has passed signature verification.
1352///
1353/// This type enforces at compile time that an attestation's signatures were verified
1354/// before it can be stored. It can only be constructed by:
1355/// - Verification functions (`verify_with_keys`, `verify_chain`)
1356/// - The `dangerous_from_unchecked` escape hatch (for self-signed attestations)
1357///
1358/// Does NOT implement `Deserialize` to prevent bypassing verification by
1359/// deserializing directly.
1360#[derive(Debug, Clone, Serialize)]
1361pub struct VerifiedAttestation(Attestation);
1362
1363impl VerifiedAttestation {
1364    /// Access the inner attestation.
1365    pub fn inner(&self) -> &Attestation {
1366        &self.0
1367    }
1368
1369    /// Consume and return the inner attestation.
1370    pub fn into_inner(self) -> Attestation {
1371        self.0
1372    }
1373
1374    /// Construct a `VerifiedAttestation` without running verification.
1375    ///
1376    /// # Safety (logical)
1377    /// Only use this when you are the signer (e.g., you just created and signed
1378    /// the attestation) or in test code. Misuse defeats the purpose of this type.
1379    #[doc(hidden)]
1380    pub fn dangerous_from_unchecked(attestation: Attestation) -> Self {
1381        Self(attestation)
1382    }
1383
1384    pub(crate) fn from_verified(attestation: Attestation) -> Self {
1385        Self(attestation)
1386    }
1387}
1388
1389impl std::ops::Deref for VerifiedAttestation {
1390    type Target = Attestation;
1391
1392    fn deref(&self) -> &Attestation {
1393        &self.0
1394    }
1395}
1396
1397/// Data structure for canonicalizing standard attestations (link, extend).
1398#[derive(Serialize, Debug)]
1399pub struct CanonicalAttestationData<'a> {
1400    /// Schema version.
1401    pub version: u32,
1402    /// Record identifier.
1403    pub rid: &'a str,
1404    /// DID of the issuing identity.
1405    pub issuer: &'a CanonicalDid,
1406    /// DID of the attested subject.
1407    pub subject: &'a CanonicalDid,
1408    /// Raw Ed25519 public key of the device.
1409    #[serde(with = "hex::serde")]
1410    pub device_public_key: &'a [u8],
1411    /// Optional arbitrary JSON payload.
1412    pub payload: &'a Option<Value>,
1413    /// Creation timestamp.
1414    pub timestamp: &'a Option<DateTime<Utc>>,
1415    /// Expiration timestamp.
1416    pub expires_at: &'a Option<DateTime<Utc>>,
1417    /// Revocation timestamp.
1418    pub revoked_at: &'a Option<DateTime<Utc>>,
1419    /// Optional human-readable note.
1420    pub note: &'a Option<String>,
1421
1422    /// DID of the delegating attestation (included in signed envelope).
1423    #[serde(skip_serializing_if = "Option::is_none")]
1424    pub delegated_by: Option<&'a CanonicalDid>,
1425    /// Type of signer (included in signed envelope).
1426    #[serde(skip_serializing_if = "Option::is_none")]
1427    pub signer_type: Option<&'a SignerType>,
1428    /// Git commit SHA for provenance binding (included in signed envelope).
1429    #[serde(skip_serializing_if = "Option::is_none")]
1430    pub commit_sha: Option<&'a str>,
1431}
1432
1433/// Produce the canonical JSON bytes over which signatures are computed.
1434///
1435/// Args:
1436/// * `data`: The attestation data to canonicalize.
1437pub fn canonicalize_attestation_data(
1438    data: &CanonicalAttestationData,
1439) -> Result<Vec<u8>, AttestationError> {
1440    let canonical_json_string = json_canon::to_string(data).map_err(|e| {
1441        AttestationError::SerializationError(format!("Failed to create canonical JSON: {}", e))
1442    })?;
1443    debug!(
1444        "Generated canonical data (standard): {}",
1445        canonical_json_string
1446    );
1447    Ok(canonical_json_string.into_bytes())
1448}
1449
1450impl Attestation {
1451    /// Returns `true` if this attestation has been revoked.
1452    pub fn is_revoked(&self) -> bool {
1453        self.revoked_at.is_some()
1454    }
1455
1456    /// Deserializes an Attestation from JSON bytes.
1457    ///
1458    /// Returns an error if the input exceeds [`MAX_ATTESTATION_JSON_SIZE`] (64 KiB).
1459    pub fn from_json(json_bytes: &[u8]) -> Result<Self, AttestationError> {
1460        if json_bytes.len() > MAX_ATTESTATION_JSON_SIZE {
1461            return Err(AttestationError::InputTooLarge(format!(
1462                "attestation JSON is {} bytes, max {}",
1463                json_bytes.len(),
1464                MAX_ATTESTATION_JSON_SIZE
1465            )));
1466        }
1467        serde_json::from_slice(json_bytes)
1468            .map_err(|e| AttestationError::SerializationError(e.to_string()))
1469    }
1470
1471    /// Returns the canonical subset of fields that signatures are computed over.
1472    ///
1473    /// Args:
1474    /// * `&self`: The attestation to extract canonical data from.
1475    ///
1476    /// Usage:
1477    /// ```ignore
1478    /// let canonical = attestation.canonical_data();
1479    /// let bytes = canonicalize_attestation_data(&canonical)?;
1480    /// ```
1481    pub fn canonical_data(&self) -> CanonicalAttestationData<'_> {
1482        CanonicalAttestationData {
1483            version: self.version,
1484            rid: &self.rid,
1485            issuer: &self.issuer,
1486            subject: &self.subject,
1487            device_public_key: self.device_public_key.as_bytes(),
1488            payload: &self.payload,
1489            timestamp: &self.timestamp,
1490            expires_at: &self.expires_at,
1491            revoked_at: &self.revoked_at,
1492            note: &self.note,
1493            delegated_by: self.delegated_by.as_ref(),
1494            signer_type: self.signer_type.as_ref(),
1495            commit_sha: self.commit_sha.as_deref(),
1496        }
1497    }
1498
1499    /// Formats the attestation contents for debug or inspection purposes.
1500    pub fn to_debug_string(&self) -> String {
1501        format!(
1502            "RID: {}\nIssuer DID: {}\nSubject DID: {}\nDevice PK: {}\nIdentity Sig: {}\nDevice Sig: {}\nRevoked At: {:?}\nExpires: {:?}\nNote: {:?}",
1503            self.rid,
1504            self.issuer,
1505            self.subject, // CanonicalDid implements Display
1506            hex::encode(self.device_public_key.as_bytes()),
1507            hex::encode(self.identity_signature.as_bytes()),
1508            hex::encode(self.device_signature.as_bytes()),
1509            self.revoked_at,
1510            self.expires_at,
1511            self.note
1512        )
1513    }
1514}
1515
1516// =============================================================================
1517// Threshold Signatures (FROST) - Future Implementation
1518// =============================================================================
1519
1520/// Policy for threshold signature operations (M-of-N).
1521///
1522/// This struct defines the parameters for FROST (Flexible Round-Optimized
1523/// Schnorr Threshold) signature operations. FROST enables M-of-N threshold
1524/// signing where at least M participants must cooperate to produce a valid
1525/// signature, but no single participant can sign alone.
1526///
1527/// # Protocol Choice: FROST
1528///
1529/// FROST was chosen over alternatives for several reasons:
1530/// - **Ed25519 native**: Works with existing Ed25519 key infrastructure
1531/// - **Round-optimized**: Only 2 rounds for signing (vs 3+ for alternatives)
1532/// - **Rust ecosystem**: `frost-ed25519` crate from ZcashFoundation is mature
1533/// - **Security**: Proven secure under discrete log assumption
1534///
1535/// # Key Generation Approaches
1536///
1537/// Two approaches exist for generating threshold key shares:
1538///
1539/// 1. **Trusted Dealer**: One party generates the key and distributes shares
1540///    - Simpler to implement
1541///    - Single point of failure during key generation
1542///    - Appropriate for org-controlled scenarios
1543///
1544/// 2. **Distributed Key Generation (DKG)**: Participants jointly generate key
1545///    - No single party ever sees the full key
1546///    - More complex, requires additional round-trips
1547///    - Better for trustless scenarios
1548///
1549/// # Integration with Auths
1550///
1551/// Threshold policies can be attached to high-value operations like:
1552/// - `sign-release`: Release signing requires M-of-N approvers
1553/// - `rotate-keys`: Key rotation requires multi-party approval
1554/// - `manage-members`: Adding admins requires quorum
1555///
1556/// # Example
1557///
1558/// ```ignore
1559/// let policy = ThresholdPolicy {
1560///     threshold: 2,
1561///     signers: vec![
1562///         "did:key:alice".to_string(),
1563///         "did:key:bob".to_string(),
1564///         "did:key:carol".to_string(),
1565///     ],
1566///     policy_id: "release-signing-v1".to_string(),
1567///     scope: Some(Capability::sign_release()),
1568///     ceremony_endpoint: Some("wss://auths.example/ceremony".to_string()),
1569/// };
1570/// // 2-of-3: Any 2 of Alice, Bob, Carol can sign releases
1571/// ```
1572///
1573/// # Storage
1574///
1575/// Key shares are NOT stored in Git refs (they are secrets). Options:
1576/// - Platform keychain (macOS Keychain, Windows Credential Manager)
1577/// - Hardware security modules (HSMs)
1578/// - Secret managers (Vault, AWS Secrets Manager)
1579///
1580/// The policy itself (public info) is stored in Git at:
1581/// `refs/auths/policies/threshold/<policy_id>`
1582#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1583#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1584pub struct ThresholdPolicy {
1585    /// Minimum signers required (M in M-of-N)
1586    pub threshold: u8,
1587
1588    /// Total authorized signers (N in M-of-N) - DIDs of participants
1589    pub signers: Vec<String>,
1590
1591    /// Unique identifier for this policy
1592    pub policy_id: PolicyId,
1593
1594    /// Scope of operations this policy covers (optional)
1595    #[serde(default, skip_serializing_if = "Option::is_none")]
1596    pub scope: Option<Capability>,
1597
1598    /// Ceremony coordination endpoint (e.g., WebSocket URL for signing rounds)
1599    #[serde(default, skip_serializing_if = "Option::is_none")]
1600    pub ceremony_endpoint: Option<String>,
1601}
1602
1603impl ThresholdPolicy {
1604    /// Create a new threshold policy
1605    pub fn new(threshold: u8, signers: Vec<String>, policy_id: impl Into<PolicyId>) -> Self {
1606        Self {
1607            threshold,
1608            signers,
1609            policy_id: policy_id.into(),
1610            scope: None,
1611            ceremony_endpoint: None,
1612        }
1613    }
1614
1615    /// Check if the policy parameters are valid
1616    pub fn is_valid(&self) -> bool {
1617        // Threshold must be at least 1
1618        if self.threshold < 1 {
1619            return false;
1620        }
1621        // Threshold cannot exceed number of signers
1622        if self.threshold as usize > self.signers.len() {
1623            return false;
1624        }
1625        // Must have at least one signer
1626        if self.signers.is_empty() {
1627            return false;
1628        }
1629        // Policy ID must not be empty
1630        if self.policy_id.is_empty() {
1631            return false;
1632        }
1633        true
1634    }
1635
1636    /// Returns M (threshold) and N (total signers)
1637    pub fn m_of_n(&self) -> (u8, usize) {
1638        (self.threshold, self.signers.len())
1639    }
1640}
1641
1642// =============================================================================
1643// CommitOid newtype (validated)
1644// =============================================================================
1645
1646/// Error type for `CommitOid` construction.
1647#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1648pub enum CommitOidError {
1649    /// The string is empty.
1650    #[error("commit OID is empty")]
1651    Empty,
1652    /// The string length is not 40 (SHA-1) or 64 (SHA-256).
1653    #[error("expected 40 or 64 hex chars, got {0}")]
1654    InvalidLength(usize),
1655    /// The string contains non-hex characters.
1656    #[error("invalid hex character in commit OID")]
1657    InvalidHex,
1658}
1659
1660/// A validated Git commit object identifier (SHA-1 or SHA-256 hex string).
1661///
1662/// Accepts exactly 40 lowercase hex characters (SHA-1) or 64 (SHA-256).
1663#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
1664#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1665#[repr(transparent)]
1666#[serde(try_from = "String")]
1667pub struct CommitOid(String);
1668
1669impl CommitOid {
1670    /// Parses and validates a commit OID string.
1671    ///
1672    /// Args:
1673    /// * `raw`: A hex string that must be exactly 40 or 64 lowercase hex characters.
1674    ///
1675    /// Usage:
1676    /// ```ignore
1677    /// let oid = CommitOid::parse("a".repeat(40))?;
1678    /// ```
1679    pub fn parse(raw: &str) -> Result<Self, CommitOidError> {
1680        let s = raw.trim().to_lowercase();
1681        if s.is_empty() {
1682            return Err(CommitOidError::Empty);
1683        }
1684        if s.len() != 40 && s.len() != 64 {
1685            return Err(CommitOidError::InvalidLength(s.len()));
1686        }
1687        if !s.chars().all(|c| c.is_ascii_hexdigit()) {
1688            return Err(CommitOidError::InvalidHex);
1689        }
1690        Ok(Self(s))
1691    }
1692
1693    /// Creates a `CommitOid` without validation.
1694    ///
1695    /// Only use at deserialization boundaries where the value was previously validated.
1696    pub fn new_unchecked(s: impl Into<String>) -> Self {
1697        Self(s.into())
1698    }
1699
1700    /// Returns the inner string slice.
1701    pub fn as_str(&self) -> &str {
1702        &self.0
1703    }
1704
1705    /// Consumes self and returns the inner `String`.
1706    pub fn into_inner(self) -> String {
1707        self.0
1708    }
1709}
1710
1711impl fmt::Display for CommitOid {
1712    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1713        f.write_str(&self.0)
1714    }
1715}
1716
1717impl AsRef<str> for CommitOid {
1718    fn as_ref(&self) -> &str {
1719        &self.0
1720    }
1721}
1722
1723impl TryFrom<String> for CommitOid {
1724    type Error = CommitOidError;
1725    fn try_from(s: String) -> Result<Self, Self::Error> {
1726        Self::parse(&s)
1727    }
1728}
1729
1730impl TryFrom<&str> for CommitOid {
1731    type Error = CommitOidError;
1732    fn try_from(s: &str) -> Result<Self, Self::Error> {
1733        Self::parse(s)
1734    }
1735}
1736
1737impl FromStr for CommitOid {
1738    type Err = CommitOidError;
1739    fn from_str(s: &str) -> Result<Self, Self::Err> {
1740        Self::parse(s)
1741    }
1742}
1743
1744impl From<CommitOid> for String {
1745    fn from(oid: CommitOid) -> Self {
1746        oid.0
1747    }
1748}
1749
1750impl<'de> Deserialize<'de> for CommitOid {
1751    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
1752        let s = String::deserialize(d)?;
1753        Self::parse(&s).map_err(serde::de::Error::custom)
1754    }
1755}
1756
1757// =============================================================================
1758// PublicKeyHex newtype (validated)
1759// =============================================================================
1760
1761/// Error type for `PublicKeyHex` construction.
1762#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1763pub enum PublicKeyHexError {
1764    /// The hex string has the wrong length (not 64 or 66 chars).
1765    #[error("expected 64 (Ed25519) or 66 (P-256) hex chars, got {0} chars")]
1766    InvalidLength(usize),
1767    /// The string contains non-hex characters.
1768    #[error("invalid hex: {0}")]
1769    InvalidHex(String),
1770}
1771
1772/// A validated hex-encoded public key (64 hex chars for Ed25519, 66 for P-256 compressed).
1773#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
1774#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1775#[repr(transparent)]
1776#[serde(try_from = "String")]
1777pub struct PublicKeyHex(String);
1778
1779impl PublicKeyHex {
1780    /// Parses and validates a hex-encoded public key string.
1781    ///
1782    /// Args:
1783    /// * `raw`: A 64-character hex string encoding 32 bytes.
1784    ///
1785    /// Usage:
1786    /// ```ignore
1787    /// let pk = PublicKeyHex::parse("ab".repeat(32))?;
1788    /// ```
1789    pub fn parse(raw: &str) -> Result<Self, PublicKeyHexError> {
1790        let s = raw.trim().to_lowercase();
1791        let bytes = hex::decode(&s).map_err(|e| PublicKeyHexError::InvalidHex(e.to_string()))?;
1792        if bytes.len() != 32 && bytes.len() != 33 {
1793            return Err(PublicKeyHexError::InvalidLength(s.len()));
1794        }
1795        Ok(Self(s))
1796    }
1797
1798    /// Creates a `PublicKeyHex` without validation.
1799    ///
1800    /// Only use at deserialization boundaries where the value was previously validated.
1801    pub fn new_unchecked(s: impl Into<String>) -> Self {
1802        Self(s.into())
1803    }
1804
1805    /// Returns the inner string slice.
1806    pub fn as_str(&self) -> &str {
1807        &self.0
1808    }
1809
1810    /// Consumes self and returns the inner `String`.
1811    pub fn into_inner(self) -> String {
1812        self.0
1813    }
1814}
1815
1816impl fmt::Display for PublicKeyHex {
1817    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1818        f.write_str(&self.0)
1819    }
1820}
1821
1822impl AsRef<str> for PublicKeyHex {
1823    fn as_ref(&self) -> &str {
1824        &self.0
1825    }
1826}
1827
1828impl TryFrom<String> for PublicKeyHex {
1829    type Error = PublicKeyHexError;
1830    fn try_from(s: String) -> Result<Self, Self::Error> {
1831        Self::parse(&s)
1832    }
1833}
1834
1835impl TryFrom<&str> for PublicKeyHex {
1836    type Error = PublicKeyHexError;
1837    fn try_from(s: &str) -> Result<Self, Self::Error> {
1838        Self::parse(s)
1839    }
1840}
1841
1842impl FromStr for PublicKeyHex {
1843    type Err = PublicKeyHexError;
1844    fn from_str(s: &str) -> Result<Self, Self::Err> {
1845        Self::parse(s)
1846    }
1847}
1848
1849impl From<PublicKeyHex> for String {
1850    fn from(pk: PublicKeyHex) -> Self {
1851        pk.0
1852    }
1853}
1854
1855impl<'de> Deserialize<'de> for PublicKeyHex {
1856    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
1857        let s = String::deserialize(d)?;
1858        Self::parse(&s).map_err(serde::de::Error::custom)
1859    }
1860}
1861
1862// =============================================================================
1863// PolicyId newtype (unvalidated)
1864// =============================================================================
1865
1866/// An opaque policy identifier.
1867///
1868/// No validation — wraps any `String`. Use where policy IDs are passed around
1869/// without needing to inspect their content.
1870#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1871#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1872#[serde(transparent)]
1873pub struct PolicyId(String);
1874
1875impl PolicyId {
1876    /// Creates a new PolicyId.
1877    pub fn new(s: impl Into<String>) -> Self {
1878        Self(s.into())
1879    }
1880
1881    /// Returns the inner string slice.
1882    pub fn as_str(&self) -> &str {
1883        &self.0
1884    }
1885}
1886
1887impl Deref for PolicyId {
1888    type Target = str;
1889    fn deref(&self) -> &str {
1890        &self.0
1891    }
1892}
1893
1894impl fmt::Display for PolicyId {
1895    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1896        f.write_str(&self.0)
1897    }
1898}
1899
1900impl From<String> for PolicyId {
1901    fn from(s: String) -> Self {
1902        Self(s)
1903    }
1904}
1905
1906impl From<&str> for PolicyId {
1907    fn from(s: &str) -> Self {
1908        Self(s.to_string())
1909    }
1910}
1911
1912impl PartialEq<str> for PolicyId {
1913    fn eq(&self, other: &str) -> bool {
1914        self.0 == other
1915    }
1916}
1917
1918impl PartialEq<&str> for PolicyId {
1919    fn eq(&self, other: &&str) -> bool {
1920        self.0 == *other
1921    }
1922}
1923
1924#[cfg(test)]
1925#[allow(clippy::disallowed_methods)]
1926mod tests {
1927    use super::*;
1928    use crate::AttestationBuilder;
1929
1930    // ========================================================================
1931    // Capability serialization tests
1932    // ========================================================================
1933
1934    #[test]
1935    fn capability_serializes_to_snake_case() {
1936        assert_eq!(
1937            serde_json::to_string(&Capability::sign_commit()).unwrap(),
1938            r#""sign_commit""#
1939        );
1940        assert_eq!(
1941            serde_json::to_string(&Capability::sign_release()).unwrap(),
1942            r#""sign_release""#
1943        );
1944        assert_eq!(
1945            serde_json::to_string(&Capability::manage_members()).unwrap(),
1946            r#""manage_members""#
1947        );
1948        assert_eq!(
1949            serde_json::to_string(&Capability::rotate_keys()).unwrap(),
1950            r#""rotate_keys""#
1951        );
1952    }
1953
1954    #[test]
1955    fn capability_deserializes_from_snake_case() {
1956        assert_eq!(
1957            serde_json::from_str::<Capability>(r#""sign_commit""#).unwrap(),
1958            Capability::sign_commit()
1959        );
1960        assert_eq!(
1961            serde_json::from_str::<Capability>(r#""sign_release""#).unwrap(),
1962            Capability::sign_release()
1963        );
1964        assert_eq!(
1965            serde_json::from_str::<Capability>(r#""manage_members""#).unwrap(),
1966            Capability::manage_members()
1967        );
1968        assert_eq!(
1969            serde_json::from_str::<Capability>(r#""rotate_keys""#).unwrap(),
1970            Capability::rotate_keys()
1971        );
1972    }
1973
1974    #[test]
1975    fn capability_custom_serializes_as_string() {
1976        let cap = Capability::parse("acme:deploy").unwrap();
1977        assert_eq!(serde_json::to_string(&cap).unwrap(), r#""acme:deploy""#);
1978    }
1979
1980    #[test]
1981    fn capability_custom_deserializes_unknown_strings() {
1982        // Unknown strings become custom capabilities
1983        let cap: Capability = serde_json::from_str(r#""custom-capability""#).unwrap();
1984        assert_eq!(cap, Capability::parse("custom-capability").unwrap());
1985    }
1986
1987    // ========================================================================
1988    // Capability parse() validation tests
1989    // ========================================================================
1990
1991    #[test]
1992    fn capability_parse_accepts_valid_strings() {
1993        assert!(Capability::parse("deploy").is_ok());
1994        assert!(Capability::parse("acme:deploy").is_ok());
1995        assert!(Capability::parse("my-custom-cap").is_ok());
1996        assert!(Capability::parse("org:team:action").is_ok());
1997        assert!(Capability::parse("with_underscore").is_ok()); // underscore allowed
1998    }
1999
2000    #[test]
2001    fn capability_parse_rejects_invalid_strings() {
2002        // Empty
2003        assert!(matches!(Capability::parse(""), Err(CapabilityError::Empty)));
2004
2005        // Too long
2006        assert!(matches!(
2007            Capability::parse(&"a".repeat(65)),
2008            Err(CapabilityError::TooLong(65))
2009        ));
2010
2011        // Invalid characters
2012        assert!(matches!(
2013            Capability::parse("has spaces"),
2014            Err(CapabilityError::InvalidChars(_))
2015        ));
2016        assert!(matches!(
2017            Capability::parse("has.dot"),
2018            Err(CapabilityError::InvalidChars(_))
2019        ));
2020    }
2021
2022    #[test]
2023    fn capability_parse_rejects_reserved_namespace() {
2024        assert!(matches!(
2025            Capability::parse("auths:custom"),
2026            Err(CapabilityError::ReservedNamespace)
2027        ));
2028        assert!(matches!(
2029            Capability::parse("auths:sign_commit"),
2030            Err(CapabilityError::ReservedNamespace)
2031        ));
2032    }
2033
2034    #[test]
2035    fn capability_parse_normalizes_to_lowercase() {
2036        let cap = Capability::parse("DEPLOY").unwrap();
2037        assert_eq!(cap.as_str(), "deploy");
2038
2039        let cap = Capability::parse("ACME:Deploy").unwrap();
2040        assert_eq!(cap.as_str(), "acme:deploy");
2041    }
2042
2043    #[test]
2044    fn capability_parse_trims_whitespace() {
2045        let cap = Capability::parse("  deploy  ").unwrap();
2046        assert_eq!(cap.as_str(), "deploy");
2047    }
2048
2049    // ========================================================================
2050    // Capability equality and hashing tests
2051    // ========================================================================
2052
2053    #[test]
2054    fn capability_is_hashable() {
2055        use std::collections::HashSet;
2056        let mut set = HashSet::new();
2057        set.insert(Capability::sign_commit());
2058        set.insert(Capability::sign_release());
2059        set.insert(Capability::parse("test").unwrap());
2060        assert_eq!(set.len(), 3);
2061        assert!(set.contains(&Capability::sign_commit()));
2062    }
2063
2064    #[test]
2065    fn capability_equality_with_different_construction_paths() {
2066        // Well-known constructor equals deserialized
2067        let from_constructor = Capability::sign_commit();
2068        let from_deser: Capability = serde_json::from_str(r#""sign_commit""#).unwrap();
2069        assert_eq!(from_constructor, from_deser);
2070
2071        // Parse equals deserialized for custom capabilities
2072        let from_parse = Capability::parse("acme:deploy").unwrap();
2073        let from_deser: Capability = serde_json::from_str(r#""acme:deploy""#).unwrap();
2074        assert_eq!(from_parse, from_deser);
2075    }
2076
2077    // ========================================================================
2078    // Capability display and accessor tests
2079    // ========================================================================
2080
2081    #[test]
2082    fn capability_display_matches_canonical_form() {
2083        assert_eq!(Capability::sign_commit().to_string(), "sign_commit");
2084        assert_eq!(Capability::sign_release().to_string(), "sign_release");
2085        assert_eq!(Capability::manage_members().to_string(), "manage_members");
2086        assert_eq!(Capability::rotate_keys().to_string(), "rotate_keys");
2087        assert_eq!(
2088            Capability::parse("acme:deploy").unwrap().to_string(),
2089            "acme:deploy"
2090        );
2091    }
2092
2093    #[test]
2094    fn capability_as_str_returns_canonical_form() {
2095        assert_eq!(Capability::sign_commit().as_str(), "sign_commit");
2096        assert_eq!(Capability::sign_release().as_str(), "sign_release");
2097        assert_eq!(Capability::manage_members().as_str(), "manage_members");
2098        assert_eq!(Capability::rotate_keys().as_str(), "rotate_keys");
2099        assert_eq!(
2100            Capability::parse("acme:deploy").unwrap().as_str(),
2101            "acme:deploy"
2102        );
2103    }
2104
2105    #[test]
2106    fn capability_is_well_known() {
2107        assert!(Capability::sign_commit().is_well_known());
2108        assert!(Capability::sign_release().is_well_known());
2109        assert!(Capability::manage_members().is_well_known());
2110        assert!(Capability::rotate_keys().is_well_known());
2111        assert!(!Capability::parse("custom").unwrap().is_well_known());
2112    }
2113
2114    #[test]
2115    fn capability_namespace() {
2116        assert_eq!(
2117            Capability::parse("acme:deploy").unwrap().namespace(),
2118            Some("acme")
2119        );
2120        assert_eq!(
2121            Capability::parse("org:team:action").unwrap().namespace(),
2122            Some("org")
2123        );
2124        assert_eq!(Capability::parse("deploy").unwrap().namespace(), None);
2125    }
2126
2127    // ========================================================================
2128    // Capability vec serialization tests
2129    // ========================================================================
2130
2131    #[test]
2132    fn capability_vec_serializes_as_array() {
2133        let caps = vec![Capability::sign_commit(), Capability::sign_release()];
2134        let json = serde_json::to_string(&caps).unwrap();
2135        assert_eq!(json, r#"["sign_commit","sign_release"]"#);
2136    }
2137
2138    #[test]
2139    fn capability_vec_deserializes_from_array() {
2140        let json = r#"["sign_commit","manage_members","custom-cap"]"#;
2141        let caps: Vec<Capability> = serde_json::from_str(json).unwrap();
2142        assert_eq!(caps.len(), 3);
2143        assert_eq!(caps[0], Capability::sign_commit());
2144        assert_eq!(caps[1], Capability::manage_members());
2145        assert_eq!(caps[2], Capability::parse("custom-cap").unwrap());
2146    }
2147
2148    // ========================================================================
2149    // Serde roundtrip tests (critical for backward compat)
2150    // ========================================================================
2151
2152    #[test]
2153    fn capability_serde_roundtrip_well_known() {
2154        let caps = vec![
2155            Capability::sign_commit(),
2156            Capability::sign_release(),
2157            Capability::manage_members(),
2158            Capability::rotate_keys(),
2159        ];
2160        for cap in caps {
2161            let json = serde_json::to_string(&cap).unwrap();
2162            let roundtrip: Capability = serde_json::from_str(&json).unwrap();
2163            assert_eq!(cap, roundtrip);
2164        }
2165    }
2166
2167    #[test]
2168    fn capability_serde_roundtrip_custom() {
2169        let caps = vec![
2170            Capability::parse("deploy").unwrap(),
2171            Capability::parse("acme:deploy").unwrap(),
2172            Capability::parse("org:team:action").unwrap(),
2173        ];
2174        for cap in caps {
2175            let json = serde_json::to_string(&cap).unwrap();
2176            let roundtrip: Capability = serde_json::from_str(&json).unwrap();
2177            assert_eq!(cap, roundtrip);
2178        }
2179    }
2180
2181    // Tests for Attestation delegation field
2182
2183    #[test]
2184    fn attestation_old_json_without_delegated_by_deserializes() {
2185        // Simulates an attestation JSON without the delegated_by field.
2186        let old_json = r#"{
2187            "version": 1,
2188            "rid": "test-rid",
2189            "issuer": "did:keri:Eissuer",
2190            "subject": "did:key:zSubject",
2191            "device_public_key": "0102030405060708091011121314151617181920212223242526272829303132",
2192            "identity_signature": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
2193            "device_signature": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
2194            "revoked_at": null,
2195            "timestamp": null
2196        }"#;
2197
2198        let att: Attestation = serde_json::from_str(old_json).unwrap();
2199
2200        assert_eq!(att.delegated_by, None);
2201    }
2202
2203    #[test]
2204    fn attestation_delegated_by_serializes_correctly() {
2205        let att = AttestationBuilder::default()
2206            .rid("test-rid")
2207            .issuer("did:keri:Eissuer")
2208            .subject("did:key:zSubject")
2209            .delegated_by(Some(CanonicalDid::new_unchecked("did:keri:Edelegator")))
2210            .build();
2211
2212        let json = serde_json::to_string(&att).unwrap();
2213        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
2214
2215        assert_eq!(parsed["delegated_by"], "did:keri:Edelegator");
2216    }
2217
2218    #[test]
2219    fn attestation_omits_delegated_by_when_absent() {
2220        let att = AttestationBuilder::default()
2221            .rid("test-rid")
2222            .issuer("did:keri:Eissuer")
2223            .subject("did:key:zSubject")
2224            .build();
2225
2226        let json = serde_json::to_string(&att).unwrap();
2227        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
2228
2229        assert!(parsed.get("delegated_by").is_none());
2230    }
2231
2232    #[test]
2233    fn attestation_delegated_by_roundtrips() {
2234        let original = AttestationBuilder::default()
2235            .rid("test-rid")
2236            .issuer("did:keri:Eissuer")
2237            .subject("did:key:zSubject")
2238            .delegated_by(Some(CanonicalDid::new_unchecked("did:keri:Eadmin")))
2239            .build();
2240
2241        let json = serde_json::to_string(&original).unwrap();
2242        let deserialized: Attestation = serde_json::from_str(&json).unwrap();
2243
2244        assert_eq!(original.delegated_by, deserialized.delegated_by);
2245    }
2246
2247    // Tests for ThresholdPolicy (fn-6.11)
2248
2249    #[test]
2250    fn threshold_policy_new_creates_valid_policy() {
2251        let policy = ThresholdPolicy::new(
2252            2,
2253            vec![
2254                "did:key:alice".to_string(),
2255                "did:key:bob".to_string(),
2256                "did:key:carol".to_string(),
2257            ],
2258            "test-policy".to_string(),
2259        );
2260
2261        assert_eq!(policy.threshold, 2);
2262        assert_eq!(policy.signers.len(), 3);
2263        assert_eq!(policy.policy_id, "test-policy");
2264        assert!(policy.scope.is_none());
2265        assert!(policy.ceremony_endpoint.is_none());
2266    }
2267
2268    #[test]
2269    fn threshold_policy_is_valid_checks_constraints() {
2270        // Valid 2-of-3
2271        let valid = ThresholdPolicy::new(
2272            2,
2273            vec!["a".to_string(), "b".to_string(), "c".to_string()],
2274            "policy".to_string(),
2275        );
2276        assert!(valid.is_valid());
2277
2278        // Invalid: threshold 0
2279        let zero_threshold = ThresholdPolicy::new(0, vec!["a".to_string()], "policy".to_string());
2280        assert!(!zero_threshold.is_valid());
2281
2282        // Invalid: threshold > signers
2283        let too_high = ThresholdPolicy::new(
2284            3,
2285            vec!["a".to_string(), "b".to_string()],
2286            "policy".to_string(),
2287        );
2288        assert!(!too_high.is_valid());
2289
2290        // Invalid: empty signers
2291        let no_signers = ThresholdPolicy::new(1, vec![], "policy".to_string());
2292        assert!(!no_signers.is_valid());
2293
2294        // Invalid: empty policy_id
2295        let no_id = ThresholdPolicy::new(1, vec!["a".to_string()], "".to_string());
2296        assert!(!no_id.is_valid());
2297    }
2298
2299    #[test]
2300    fn threshold_policy_m_of_n_returns_correct_values() {
2301        let policy = ThresholdPolicy::new(
2302            2,
2303            vec!["a".to_string(), "b".to_string(), "c".to_string()],
2304            "policy".to_string(),
2305        );
2306        let (m, n) = policy.m_of_n();
2307        assert_eq!(m, 2);
2308        assert_eq!(n, 3);
2309    }
2310
2311    #[test]
2312    fn threshold_policy_serializes_correctly() {
2313        let mut policy = ThresholdPolicy::new(
2314            2,
2315            vec!["did:key:alice".to_string(), "did:key:bob".to_string()],
2316            "release-policy".to_string(),
2317        );
2318        policy.scope = Some(Capability::sign_release());
2319        policy.ceremony_endpoint = Some("wss://example.com/ceremony".to_string());
2320
2321        let json = serde_json::to_string(&policy).unwrap();
2322        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
2323
2324        assert_eq!(parsed["threshold"], 2);
2325        assert_eq!(parsed["signers"][0], "did:key:alice");
2326        assert_eq!(parsed["policy_id"], "release-policy");
2327        assert_eq!(parsed["scope"], "sign_release");
2328        assert_eq!(parsed["ceremony_endpoint"], "wss://example.com/ceremony");
2329    }
2330
2331    #[test]
2332    fn threshold_policy_without_optional_fields_omits_them() {
2333        let policy =
2334            ThresholdPolicy::new(1, vec!["did:key:alice".to_string()], "policy".to_string());
2335
2336        let json = serde_json::to_string(&policy).unwrap();
2337        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
2338
2339        assert!(parsed.get("scope").is_none());
2340        assert!(parsed.get("ceremony_endpoint").is_none());
2341    }
2342
2343    #[test]
2344    fn threshold_policy_roundtrips() {
2345        let mut original = ThresholdPolicy::new(
2346            3,
2347            vec![
2348                "a".to_string(),
2349                "b".to_string(),
2350                "c".to_string(),
2351                "d".to_string(),
2352            ],
2353            "important-policy".to_string(),
2354        );
2355        original.scope = Some(Capability::rotate_keys());
2356
2357        let json = serde_json::to_string(&original).unwrap();
2358        let deserialized: ThresholdPolicy = serde_json::from_str(&json).unwrap();
2359
2360        assert_eq!(original, deserialized);
2361    }
2362
2363    // Tests for IdentityBundle (CI/CD stateless verification)
2364
2365    #[test]
2366    fn identity_bundle_serializes_correctly() {
2367        let bundle = IdentityBundle {
2368            identity_did: IdentityDID::new_unchecked("did:keri:test123"),
2369            public_key_hex: PublicKeyHex::new_unchecked("aabbccdd"),
2370            curve: Default::default(),
2371            attestation_chain: vec![],
2372            bundle_timestamp: DateTime::parse_from_rfc3339("2099-01-01T00:00:00Z")
2373                .unwrap()
2374                .with_timezone(&Utc),
2375            max_valid_for_secs: 86400,
2376        };
2377
2378        let json = serde_json::to_string(&bundle).unwrap();
2379        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
2380
2381        assert_eq!(parsed["identity_did"], "did:keri:test123");
2382        assert_eq!(parsed["public_key_hex"], "aabbccdd");
2383        assert!(parsed["attestation_chain"].as_array().unwrap().is_empty());
2384    }
2385
2386    #[test]
2387    fn identity_bundle_deserializes_correctly() {
2388        let json = r#"{
2389            "identity_did": "did:keri:abc123",
2390            "public_key_hex": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20",
2391            "attestation_chain": [],
2392            "bundle_timestamp": "2099-01-01T00:00:00Z",
2393            "max_valid_for_secs": 86400
2394        }"#;
2395
2396        let bundle: IdentityBundle = serde_json::from_str(json).unwrap();
2397
2398        assert_eq!(bundle.identity_did.as_str(), "did:keri:abc123");
2399        assert_eq!(
2400            bundle.public_key_hex.as_str(),
2401            "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"
2402        );
2403        assert!(bundle.attestation_chain.is_empty());
2404    }
2405
2406    #[test]
2407    fn identity_bundle_roundtrips() {
2408        let attestation = AttestationBuilder::default()
2409            .rid("test-rid")
2410            .issuer("did:keri:Eissuer")
2411            .subject("did:key:zSubject")
2412            .build();
2413
2414        let original = IdentityBundle {
2415            identity_did: IdentityDID::new_unchecked("did:keri:Eexample"),
2416            public_key_hex: PublicKeyHex::new_unchecked(
2417                "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
2418            ),
2419            curve: Default::default(),
2420            attestation_chain: vec![attestation],
2421            bundle_timestamp: DateTime::parse_from_rfc3339("2099-01-01T00:00:00Z")
2422                .unwrap()
2423                .with_timezone(&Utc),
2424            max_valid_for_secs: 86400,
2425        };
2426
2427        let json = serde_json::to_string(&original).unwrap();
2428        let deserialized: IdentityBundle = serde_json::from_str(&json).unwrap();
2429
2430        assert_eq!(original.identity_did, deserialized.identity_did);
2431        assert_eq!(original.public_key_hex, deserialized.public_key_hex);
2432        assert_eq!(
2433            original.attestation_chain.len(),
2434            deserialized.attestation_chain.len()
2435        );
2436    }
2437}
2438
2439#[cfg(test)]
2440mod decode_public_key_tests {
2441    use super::*;
2442
2443    #[test]
2444    fn hex_ed25519_32_bytes() {
2445        let hex = "00".repeat(32);
2446        let pk = decode_public_key_hex(&hex, auths_crypto::CurveType::Ed25519).unwrap();
2447        assert_eq!(pk.curve(), auths_crypto::CurveType::Ed25519);
2448        assert_eq!(pk.len(), 32);
2449    }
2450
2451    #[test]
2452    fn hex_p256_33_bytes_compressed() {
2453        // Need a valid-ish compressed SEC1 for try_new to accept: starts with 0x02 or 0x03
2454        let mut bytes = [0u8; 33];
2455        bytes[0] = 0x02;
2456        let hex = hex::encode(bytes);
2457        let pk = decode_public_key_hex(&hex, auths_crypto::CurveType::P256).unwrap();
2458        assert_eq!(pk.curve(), auths_crypto::CurveType::P256);
2459        assert_eq!(pk.len(), 33);
2460    }
2461
2462    #[test]
2463    fn bytes_p256_65_uncompressed() {
2464        let mut bytes = [0u8; 65];
2465        bytes[0] = 0x04;
2466        let pk = decode_public_key_bytes(&bytes, auths_crypto::CurveType::P256).unwrap();
2467        assert_eq!(pk.curve(), auths_crypto::CurveType::P256);
2468        assert_eq!(pk.len(), 65);
2469    }
2470
2471    #[test]
2472    fn rejects_validation_error() {
2473        let err = decode_public_key_bytes(&[0u8; 50], auths_crypto::CurveType::P256).unwrap_err();
2474        assert!(matches!(err, PublicKeyDecodeError::Validation(_)));
2475    }
2476
2477    #[test]
2478    fn rejects_malformed_hex() {
2479        let err = decode_public_key_hex("zz", auths_crypto::CurveType::P256).unwrap_err();
2480        assert!(matches!(err, PublicKeyDecodeError::InvalidHex(_)));
2481    }
2482}