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    /// The identity's raw KEL events (JSON, oldest first). Carried so
1216    /// KEL-native commit verification stays stateless on CI runners with no
1217    /// identity store. Empty in bundles exported before this field existed.
1218    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1219    pub kel: Vec<serde_json::Value>,
1220    /// UTC timestamp when this bundle was created
1221    pub bundle_timestamp: DateTime<Utc>,
1222    /// Maximum age in seconds before this bundle is considered stale
1223    pub max_valid_for_secs: u64,
1224}
1225
1226impl IdentityBundle {
1227    /// Check that this bundle is still within its TTL.
1228    ///
1229    /// Args:
1230    /// * `now`: The current time, injected for deterministic verification.
1231    ///
1232    /// Usage:
1233    /// ```ignore
1234    /// bundle.check_freshness(Utc::now())?;
1235    /// ```
1236    pub fn check_freshness(&self, now: DateTime<Utc>) -> Result<(), AttestationError> {
1237        let age = (now - self.bundle_timestamp).num_seconds().max(0) as u64;
1238        if age > self.max_valid_for_secs {
1239            return Err(AttestationError::BundleExpired {
1240                age_secs: age,
1241                max_secs: self.max_valid_for_secs,
1242            });
1243        }
1244        Ok(())
1245    }
1246}
1247
1248/// Represents a 2-way key attestation between a primary identity and a device key.
1249#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1250#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1251pub struct Attestation {
1252    /// Schema version.
1253    pub version: u32,
1254    /// Record identifier linking this attestation to its storage ref.
1255    pub rid: ResourceId,
1256    /// DID of the issuing identity (can be `did:keri:` or `did:key:`).
1257    pub issuer: CanonicalDid,
1258    /// DID of the attested subject (device `did:key:` or identity `did:keri:`).
1259    pub subject: CanonicalDid,
1260    /// Device public key (32 bytes Ed25519 or 33 bytes P-256 compressed, hex-encoded in JSON).
1261    pub device_public_key: DevicePublicKey,
1262    /// Issuer's Ed25519 signature over the canonical attestation data (hex-encoded in JSON).
1263    #[serde(default, skip_serializing_if = "Ed25519Signature::is_empty")]
1264    pub identity_signature: Ed25519Signature,
1265    /// Device's Ed25519 signature over the canonical attestation data (hex-encoded in JSON).
1266    pub device_signature: Ed25519Signature,
1267    /// Timestamp when the attestation was revoked, if applicable.
1268    #[serde(default, skip_serializing_if = "Option::is_none")]
1269    pub revoked_at: Option<DateTime<Utc>>,
1270    /// Expiration timestamp, if set.
1271    #[serde(skip_serializing_if = "Option::is_none")]
1272    pub expires_at: Option<DateTime<Utc>>,
1273    /// Creation timestamp.
1274    pub timestamp: Option<DateTime<Utc>>,
1275    /// Optional human-readable note.
1276    #[serde(skip_serializing_if = "Option::is_none")]
1277    pub note: Option<String>,
1278    /// Optional arbitrary JSON payload.
1279    #[serde(skip_serializing_if = "Option::is_none")]
1280    pub payload: Option<Value>,
1281
1282    /// Git commit SHA (for commit signing attestations).
1283    #[serde(skip_serializing_if = "Option::is_none")]
1284    pub commit_sha: Option<String>,
1285
1286    /// Git commit message (for commit signing attestations).
1287    #[serde(skip_serializing_if = "Option::is_none")]
1288    pub commit_message: Option<String>,
1289
1290    /// Git commit author (for commit signing attestations).
1291    #[serde(skip_serializing_if = "Option::is_none")]
1292    pub author: Option<String>,
1293
1294    /// OIDC binding information (issuer, subject, audience, expiration).
1295    #[serde(skip_serializing_if = "Option::is_none")]
1296    pub oidc_binding: Option<OidcBinding>,
1297
1298    /// DID of the attestation that delegated authority (for chain tracking).
1299    #[serde(default, skip_serializing_if = "Option::is_none")]
1300    pub delegated_by: Option<CanonicalDid>,
1301
1302    /// The type of entity that produced this signature (human, agent, workload).
1303    /// Included in the canonical JSON before signing — the signature covers this field.
1304    #[serde(default, skip_serializing_if = "Option::is_none")]
1305    pub signer_type: Option<SignerType>,
1306
1307    /// Unsigned environment claim for gateway-level verification via `auths-env`.
1308    /// Excluded from `CanonicalAttestationData` — does not affect signatures.
1309    #[serde(default, skip_serializing_if = "Option::is_none")]
1310    pub environment_claim: Option<Value>,
1311}
1312
1313/// OIDC token binding information for machine identity attestations.
1314///
1315/// Proves that the attestation was created by a CI/CD workload with a specific
1316/// OIDC token. Contains the issuer, subject, audience, and expiration so verifiers
1317/// can reconstruct the identity without needing the ephemeral private key.
1318#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1319#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1320pub struct OidcBinding {
1321    /// OIDC token issuer (e.g., "https://token.actions.githubusercontent.com").
1322    pub issuer: String,
1323    /// Token subject (unique workload identifier).
1324    pub subject: String,
1325    /// Expected audience.
1326    pub audience: String,
1327    /// Token expiration timestamp (Unix timestamp).
1328    pub token_exp: i64,
1329    /// CI/CD platform (e.g., "github", "gitlab", "circleci").
1330    #[serde(default, skip_serializing_if = "Option::is_none")]
1331    pub platform: Option<String>,
1332    /// JTI for replay detection (if available).
1333    #[serde(default, skip_serializing_if = "Option::is_none")]
1334    pub jti: Option<String>,
1335    /// Platform-normalized claims (e.g., repo, actor, run_id for GitHub).
1336    #[serde(default, skip_serializing_if = "Option::is_none")]
1337    pub normalized_claims: Option<serde_json::Map<String, serde_json::Value>>,
1338}
1339
1340/// The type of entity that produced a signature.
1341///
1342/// Duplicated here (also in `auths-policy`) because `auths-verifier` is a
1343/// standalone minimal-dependency crate that cannot depend on `auths-policy`.
1344#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1345#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1346#[non_exhaustive]
1347pub enum SignerType {
1348    /// A human user.
1349    Human,
1350    /// An autonomous AI agent.
1351    Agent,
1352    /// A CI/CD workload or service identity.
1353    Workload,
1354}
1355
1356/// An attestation that has passed signature verification.
1357///
1358/// This type enforces at compile time that an attestation's signatures were verified
1359/// before it can be stored. It can only be constructed by:
1360/// - Verification functions (`verify_with_keys`, `verify_chain`)
1361/// - The `dangerous_from_unchecked` escape hatch (for self-signed attestations)
1362///
1363/// Does NOT implement `Deserialize` to prevent bypassing verification by
1364/// deserializing directly.
1365#[derive(Debug, Clone, Serialize)]
1366pub struct VerifiedAttestation(Attestation);
1367
1368impl VerifiedAttestation {
1369    /// Access the inner attestation.
1370    pub fn inner(&self) -> &Attestation {
1371        &self.0
1372    }
1373
1374    /// Consume and return the inner attestation.
1375    pub fn into_inner(self) -> Attestation {
1376        self.0
1377    }
1378
1379    /// Construct a `VerifiedAttestation` without running verification.
1380    ///
1381    /// # Safety (logical)
1382    /// Only use this when you are the signer (e.g., you just created and signed
1383    /// the attestation) or in test code. Misuse defeats the purpose of this type.
1384    #[doc(hidden)]
1385    pub fn dangerous_from_unchecked(attestation: Attestation) -> Self {
1386        Self(attestation)
1387    }
1388
1389    pub(crate) fn from_verified(attestation: Attestation) -> Self {
1390        Self(attestation)
1391    }
1392}
1393
1394impl std::ops::Deref for VerifiedAttestation {
1395    type Target = Attestation;
1396
1397    fn deref(&self) -> &Attestation {
1398        &self.0
1399    }
1400}
1401
1402/// Data structure for canonicalizing standard attestations (link, extend).
1403#[derive(Serialize, Debug)]
1404pub struct CanonicalAttestationData<'a> {
1405    /// Schema version.
1406    pub version: u32,
1407    /// Record identifier.
1408    pub rid: &'a str,
1409    /// DID of the issuing identity.
1410    pub issuer: &'a CanonicalDid,
1411    /// DID of the attested subject.
1412    pub subject: &'a CanonicalDid,
1413    /// Raw Ed25519 public key of the device.
1414    #[serde(with = "hex::serde")]
1415    pub device_public_key: &'a [u8],
1416    /// Optional arbitrary JSON payload.
1417    pub payload: &'a Option<Value>,
1418    /// Creation timestamp.
1419    pub timestamp: &'a Option<DateTime<Utc>>,
1420    /// Expiration timestamp.
1421    pub expires_at: &'a Option<DateTime<Utc>>,
1422    /// Revocation timestamp.
1423    pub revoked_at: &'a Option<DateTime<Utc>>,
1424    /// Optional human-readable note.
1425    pub note: &'a Option<String>,
1426
1427    /// DID of the delegating attestation (included in signed envelope).
1428    #[serde(skip_serializing_if = "Option::is_none")]
1429    pub delegated_by: Option<&'a CanonicalDid>,
1430    /// Type of signer (included in signed envelope).
1431    #[serde(skip_serializing_if = "Option::is_none")]
1432    pub signer_type: Option<&'a SignerType>,
1433    /// Git commit SHA for provenance binding (included in signed envelope).
1434    #[serde(skip_serializing_if = "Option::is_none")]
1435    pub commit_sha: Option<&'a str>,
1436}
1437
1438/// Produce the canonical JSON bytes over which signatures are computed.
1439///
1440/// Args:
1441/// * `data`: The attestation data to canonicalize.
1442pub fn canonicalize_attestation_data(
1443    data: &CanonicalAttestationData,
1444) -> Result<Vec<u8>, AttestationError> {
1445    let canonical_json_string = json_canon::to_string(data).map_err(|e| {
1446        AttestationError::SerializationError(format!("Failed to create canonical JSON: {}", e))
1447    })?;
1448    debug!(
1449        "Generated canonical data (standard): {}",
1450        canonical_json_string
1451    );
1452    Ok(canonical_json_string.into_bytes())
1453}
1454
1455impl Attestation {
1456    /// Returns `true` if this attestation has been revoked.
1457    pub fn is_revoked(&self) -> bool {
1458        self.revoked_at.is_some()
1459    }
1460
1461    /// Deserializes an Attestation from JSON bytes.
1462    ///
1463    /// Returns an error if the input exceeds [`MAX_ATTESTATION_JSON_SIZE`] (64 KiB).
1464    pub fn from_json(json_bytes: &[u8]) -> Result<Self, AttestationError> {
1465        if json_bytes.len() > MAX_ATTESTATION_JSON_SIZE {
1466            return Err(AttestationError::InputTooLarge(format!(
1467                "attestation JSON is {} bytes, max {}",
1468                json_bytes.len(),
1469                MAX_ATTESTATION_JSON_SIZE
1470            )));
1471        }
1472        serde_json::from_slice(json_bytes)
1473            .map_err(|e| AttestationError::SerializationError(e.to_string()))
1474    }
1475
1476    /// Returns the canonical subset of fields that signatures are computed over.
1477    ///
1478    /// Args:
1479    /// * `&self`: The attestation to extract canonical data from.
1480    ///
1481    /// Usage:
1482    /// ```ignore
1483    /// let canonical = attestation.canonical_data();
1484    /// let bytes = canonicalize_attestation_data(&canonical)?;
1485    /// ```
1486    pub fn canonical_data(&self) -> CanonicalAttestationData<'_> {
1487        CanonicalAttestationData {
1488            version: self.version,
1489            rid: &self.rid,
1490            issuer: &self.issuer,
1491            subject: &self.subject,
1492            device_public_key: self.device_public_key.as_bytes(),
1493            payload: &self.payload,
1494            timestamp: &self.timestamp,
1495            expires_at: &self.expires_at,
1496            revoked_at: &self.revoked_at,
1497            note: &self.note,
1498            delegated_by: self.delegated_by.as_ref(),
1499            signer_type: self.signer_type.as_ref(),
1500            commit_sha: self.commit_sha.as_deref(),
1501        }
1502    }
1503
1504    /// Formats the attestation contents for debug or inspection purposes.
1505    pub fn to_debug_string(&self) -> String {
1506        format!(
1507            "RID: {}\nIssuer DID: {}\nSubject DID: {}\nDevice PK: {}\nIdentity Sig: {}\nDevice Sig: {}\nRevoked At: {:?}\nExpires: {:?}\nNote: {:?}",
1508            self.rid,
1509            self.issuer,
1510            self.subject, // CanonicalDid implements Display
1511            hex::encode(self.device_public_key.as_bytes()),
1512            hex::encode(self.identity_signature.as_bytes()),
1513            hex::encode(self.device_signature.as_bytes()),
1514            self.revoked_at,
1515            self.expires_at,
1516            self.note
1517        )
1518    }
1519}
1520
1521// =============================================================================
1522// Threshold Signatures (FROST) - Future Implementation
1523// =============================================================================
1524
1525/// Policy for threshold signature operations (M-of-N).
1526///
1527/// This struct defines the parameters for FROST (Flexible Round-Optimized
1528/// Schnorr Threshold) signature operations. FROST enables M-of-N threshold
1529/// signing where at least M participants must cooperate to produce a valid
1530/// signature, but no single participant can sign alone.
1531///
1532/// # Protocol Choice: FROST
1533///
1534/// FROST was chosen over alternatives for several reasons:
1535/// - **Ed25519 native**: Works with existing Ed25519 key infrastructure
1536/// - **Round-optimized**: Only 2 rounds for signing (vs 3+ for alternatives)
1537/// - **Rust ecosystem**: `frost-ed25519` crate from ZcashFoundation is mature
1538/// - **Security**: Proven secure under discrete log assumption
1539///
1540/// # Key Generation Approaches
1541///
1542/// Two approaches exist for generating threshold key shares:
1543///
1544/// 1. **Trusted Dealer**: One party generates the key and distributes shares
1545///    - Simpler to implement
1546///    - Single point of failure during key generation
1547///    - Appropriate for org-controlled scenarios
1548///
1549/// 2. **Distributed Key Generation (DKG)**: Participants jointly generate key
1550///    - No single party ever sees the full key
1551///    - More complex, requires additional round-trips
1552///    - Better for trustless scenarios
1553///
1554/// # Integration with Auths
1555///
1556/// Threshold policies can be attached to high-value operations like:
1557/// - `sign-release`: Release signing requires M-of-N approvers
1558/// - `rotate-keys`: Key rotation requires multi-party approval
1559/// - `manage-members`: Adding admins requires quorum
1560///
1561/// # Example
1562///
1563/// ```ignore
1564/// let policy = ThresholdPolicy {
1565///     threshold: 2,
1566///     signers: vec![
1567///         "did:key:alice".to_string(),
1568///         "did:key:bob".to_string(),
1569///         "did:key:carol".to_string(),
1570///     ],
1571///     policy_id: "release-signing-v1".to_string(),
1572///     scope: Some(Capability::sign_release()),
1573///     ceremony_endpoint: Some("wss://auths.example/ceremony".to_string()),
1574/// };
1575/// // 2-of-3: Any 2 of Alice, Bob, Carol can sign releases
1576/// ```
1577///
1578/// # Storage
1579///
1580/// Key shares are NOT stored in Git refs (they are secrets). Options:
1581/// - Platform keychain (macOS Keychain, Windows Credential Manager)
1582/// - Hardware security modules (HSMs)
1583/// - Secret managers (Vault, AWS Secrets Manager)
1584///
1585/// The policy itself (public info) is stored in Git at:
1586/// `refs/auths/policies/threshold/<policy_id>`
1587#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1588#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1589pub struct ThresholdPolicy {
1590    /// Minimum signers required (M in M-of-N)
1591    pub threshold: u8,
1592
1593    /// Total authorized signers (N in M-of-N) - DIDs of participants
1594    pub signers: Vec<String>,
1595
1596    /// Unique identifier for this policy
1597    pub policy_id: PolicyId,
1598
1599    /// Scope of operations this policy covers (optional)
1600    #[serde(default, skip_serializing_if = "Option::is_none")]
1601    pub scope: Option<Capability>,
1602
1603    /// Ceremony coordination endpoint (e.g., WebSocket URL for signing rounds)
1604    #[serde(default, skip_serializing_if = "Option::is_none")]
1605    pub ceremony_endpoint: Option<String>,
1606}
1607
1608impl ThresholdPolicy {
1609    /// Create a new threshold policy
1610    pub fn new(threshold: u8, signers: Vec<String>, policy_id: impl Into<PolicyId>) -> Self {
1611        Self {
1612            threshold,
1613            signers,
1614            policy_id: policy_id.into(),
1615            scope: None,
1616            ceremony_endpoint: None,
1617        }
1618    }
1619
1620    /// Check if the policy parameters are valid
1621    pub fn is_valid(&self) -> bool {
1622        // Threshold must be at least 1
1623        if self.threshold < 1 {
1624            return false;
1625        }
1626        // Threshold cannot exceed number of signers
1627        if self.threshold as usize > self.signers.len() {
1628            return false;
1629        }
1630        // Must have at least one signer
1631        if self.signers.is_empty() {
1632            return false;
1633        }
1634        // Policy ID must not be empty
1635        if self.policy_id.is_empty() {
1636            return false;
1637        }
1638        true
1639    }
1640
1641    /// Returns M (threshold) and N (total signers)
1642    pub fn m_of_n(&self) -> (u8, usize) {
1643        (self.threshold, self.signers.len())
1644    }
1645}
1646
1647// =============================================================================
1648// CommitOid newtype (validated)
1649// =============================================================================
1650
1651/// Error type for `CommitOid` construction.
1652#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1653pub enum CommitOidError {
1654    /// The string is empty.
1655    #[error("commit OID is empty")]
1656    Empty,
1657    /// The string length is not 40 (SHA-1) or 64 (SHA-256).
1658    #[error("expected 40 or 64 hex chars, got {0}")]
1659    InvalidLength(usize),
1660    /// The string contains non-hex characters.
1661    #[error("invalid hex character in commit OID")]
1662    InvalidHex,
1663}
1664
1665/// A validated Git commit object identifier (SHA-1 or SHA-256 hex string).
1666///
1667/// Accepts exactly 40 lowercase hex characters (SHA-1) or 64 (SHA-256).
1668#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
1669#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1670#[repr(transparent)]
1671#[serde(try_from = "String")]
1672pub struct CommitOid(String);
1673
1674impl CommitOid {
1675    /// Parses and validates a commit OID string.
1676    ///
1677    /// Args:
1678    /// * `raw`: A hex string that must be exactly 40 or 64 lowercase hex characters.
1679    ///
1680    /// Usage:
1681    /// ```ignore
1682    /// let oid = CommitOid::parse("a".repeat(40))?;
1683    /// ```
1684    pub fn parse(raw: &str) -> Result<Self, CommitOidError> {
1685        let s = raw.trim().to_lowercase();
1686        if s.is_empty() {
1687            return Err(CommitOidError::Empty);
1688        }
1689        if s.len() != 40 && s.len() != 64 {
1690            return Err(CommitOidError::InvalidLength(s.len()));
1691        }
1692        if !s.chars().all(|c| c.is_ascii_hexdigit()) {
1693            return Err(CommitOidError::InvalidHex);
1694        }
1695        Ok(Self(s))
1696    }
1697
1698    /// Creates a `CommitOid` without validation.
1699    ///
1700    /// Only use at deserialization boundaries where the value was previously validated.
1701    pub fn new_unchecked(s: impl Into<String>) -> Self {
1702        Self(s.into())
1703    }
1704
1705    /// Returns the inner string slice.
1706    pub fn as_str(&self) -> &str {
1707        &self.0
1708    }
1709
1710    /// Consumes self and returns the inner `String`.
1711    pub fn into_inner(self) -> String {
1712        self.0
1713    }
1714}
1715
1716impl fmt::Display for CommitOid {
1717    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1718        f.write_str(&self.0)
1719    }
1720}
1721
1722impl AsRef<str> for CommitOid {
1723    fn as_ref(&self) -> &str {
1724        &self.0
1725    }
1726}
1727
1728impl TryFrom<String> for CommitOid {
1729    type Error = CommitOidError;
1730    fn try_from(s: String) -> Result<Self, Self::Error> {
1731        Self::parse(&s)
1732    }
1733}
1734
1735impl TryFrom<&str> for CommitOid {
1736    type Error = CommitOidError;
1737    fn try_from(s: &str) -> Result<Self, Self::Error> {
1738        Self::parse(s)
1739    }
1740}
1741
1742impl FromStr for CommitOid {
1743    type Err = CommitOidError;
1744    fn from_str(s: &str) -> Result<Self, Self::Err> {
1745        Self::parse(s)
1746    }
1747}
1748
1749impl From<CommitOid> for String {
1750    fn from(oid: CommitOid) -> Self {
1751        oid.0
1752    }
1753}
1754
1755impl<'de> Deserialize<'de> for CommitOid {
1756    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
1757        let s = String::deserialize(d)?;
1758        Self::parse(&s).map_err(serde::de::Error::custom)
1759    }
1760}
1761
1762// =============================================================================
1763// PublicKeyHex newtype (validated)
1764// =============================================================================
1765
1766/// Error type for `PublicKeyHex` construction.
1767#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1768pub enum PublicKeyHexError {
1769    /// The hex string has the wrong length (not 64 or 66 chars).
1770    #[error("expected 64 (Ed25519) or 66 (P-256) hex chars, got {0} chars")]
1771    InvalidLength(usize),
1772    /// The string contains non-hex characters.
1773    #[error("invalid hex: {0}")]
1774    InvalidHex(String),
1775}
1776
1777/// A validated hex-encoded public key (64 hex chars for Ed25519, 66 for P-256 compressed).
1778#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
1779#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1780#[repr(transparent)]
1781#[serde(try_from = "String")]
1782pub struct PublicKeyHex(String);
1783
1784impl PublicKeyHex {
1785    /// Parses and validates a hex-encoded public key string.
1786    ///
1787    /// Args:
1788    /// * `raw`: A 64-character hex string encoding 32 bytes.
1789    ///
1790    /// Usage:
1791    /// ```ignore
1792    /// let pk = PublicKeyHex::parse("ab".repeat(32))?;
1793    /// ```
1794    pub fn parse(raw: &str) -> Result<Self, PublicKeyHexError> {
1795        let s = raw.trim().to_lowercase();
1796        let bytes = hex::decode(&s).map_err(|e| PublicKeyHexError::InvalidHex(e.to_string()))?;
1797        if bytes.len() != 32 && bytes.len() != 33 {
1798            return Err(PublicKeyHexError::InvalidLength(s.len()));
1799        }
1800        Ok(Self(s))
1801    }
1802
1803    /// Creates a `PublicKeyHex` without validation.
1804    ///
1805    /// Only use at deserialization boundaries where the value was previously validated.
1806    pub fn new_unchecked(s: impl Into<String>) -> Self {
1807        Self(s.into())
1808    }
1809
1810    /// Returns the inner string slice.
1811    pub fn as_str(&self) -> &str {
1812        &self.0
1813    }
1814
1815    /// Consumes self and returns the inner `String`.
1816    pub fn into_inner(self) -> String {
1817        self.0
1818    }
1819}
1820
1821impl fmt::Display for PublicKeyHex {
1822    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1823        f.write_str(&self.0)
1824    }
1825}
1826
1827impl AsRef<str> for PublicKeyHex {
1828    fn as_ref(&self) -> &str {
1829        &self.0
1830    }
1831}
1832
1833impl TryFrom<String> for PublicKeyHex {
1834    type Error = PublicKeyHexError;
1835    fn try_from(s: String) -> Result<Self, Self::Error> {
1836        Self::parse(&s)
1837    }
1838}
1839
1840impl TryFrom<&str> for PublicKeyHex {
1841    type Error = PublicKeyHexError;
1842    fn try_from(s: &str) -> Result<Self, Self::Error> {
1843        Self::parse(s)
1844    }
1845}
1846
1847impl FromStr for PublicKeyHex {
1848    type Err = PublicKeyHexError;
1849    fn from_str(s: &str) -> Result<Self, Self::Err> {
1850        Self::parse(s)
1851    }
1852}
1853
1854impl From<PublicKeyHex> for String {
1855    fn from(pk: PublicKeyHex) -> Self {
1856        pk.0
1857    }
1858}
1859
1860impl<'de> Deserialize<'de> for PublicKeyHex {
1861    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
1862        let s = String::deserialize(d)?;
1863        Self::parse(&s).map_err(serde::de::Error::custom)
1864    }
1865}
1866
1867// =============================================================================
1868// PolicyId newtype (unvalidated)
1869// =============================================================================
1870
1871/// An opaque policy identifier.
1872///
1873/// No validation — wraps any `String`. Use where policy IDs are passed around
1874/// without needing to inspect their content.
1875#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1876#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1877#[serde(transparent)]
1878pub struct PolicyId(String);
1879
1880impl PolicyId {
1881    /// Creates a new PolicyId.
1882    pub fn new(s: impl Into<String>) -> Self {
1883        Self(s.into())
1884    }
1885
1886    /// Returns the inner string slice.
1887    pub fn as_str(&self) -> &str {
1888        &self.0
1889    }
1890}
1891
1892impl Deref for PolicyId {
1893    type Target = str;
1894    fn deref(&self) -> &str {
1895        &self.0
1896    }
1897}
1898
1899impl fmt::Display for PolicyId {
1900    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1901        f.write_str(&self.0)
1902    }
1903}
1904
1905impl From<String> for PolicyId {
1906    fn from(s: String) -> Self {
1907        Self(s)
1908    }
1909}
1910
1911impl From<&str> for PolicyId {
1912    fn from(s: &str) -> Self {
1913        Self(s.to_string())
1914    }
1915}
1916
1917impl PartialEq<str> for PolicyId {
1918    fn eq(&self, other: &str) -> bool {
1919        self.0 == other
1920    }
1921}
1922
1923impl PartialEq<&str> for PolicyId {
1924    fn eq(&self, other: &&str) -> bool {
1925        self.0 == *other
1926    }
1927}
1928
1929#[cfg(test)]
1930#[allow(clippy::disallowed_methods)]
1931mod tests {
1932    use super::*;
1933    use crate::AttestationBuilder;
1934
1935    // ========================================================================
1936    // Capability serialization tests
1937    // ========================================================================
1938
1939    #[test]
1940    fn capability_serializes_to_snake_case() {
1941        assert_eq!(
1942            serde_json::to_string(&Capability::sign_commit()).unwrap(),
1943            r#""sign_commit""#
1944        );
1945        assert_eq!(
1946            serde_json::to_string(&Capability::sign_release()).unwrap(),
1947            r#""sign_release""#
1948        );
1949        assert_eq!(
1950            serde_json::to_string(&Capability::manage_members()).unwrap(),
1951            r#""manage_members""#
1952        );
1953        assert_eq!(
1954            serde_json::to_string(&Capability::rotate_keys()).unwrap(),
1955            r#""rotate_keys""#
1956        );
1957    }
1958
1959    #[test]
1960    fn capability_deserializes_from_snake_case() {
1961        assert_eq!(
1962            serde_json::from_str::<Capability>(r#""sign_commit""#).unwrap(),
1963            Capability::sign_commit()
1964        );
1965        assert_eq!(
1966            serde_json::from_str::<Capability>(r#""sign_release""#).unwrap(),
1967            Capability::sign_release()
1968        );
1969        assert_eq!(
1970            serde_json::from_str::<Capability>(r#""manage_members""#).unwrap(),
1971            Capability::manage_members()
1972        );
1973        assert_eq!(
1974            serde_json::from_str::<Capability>(r#""rotate_keys""#).unwrap(),
1975            Capability::rotate_keys()
1976        );
1977    }
1978
1979    #[test]
1980    fn capability_custom_serializes_as_string() {
1981        let cap = Capability::parse("acme:deploy").unwrap();
1982        assert_eq!(serde_json::to_string(&cap).unwrap(), r#""acme:deploy""#);
1983    }
1984
1985    #[test]
1986    fn capability_custom_deserializes_unknown_strings() {
1987        // Unknown strings become custom capabilities
1988        let cap: Capability = serde_json::from_str(r#""custom-capability""#).unwrap();
1989        assert_eq!(cap, Capability::parse("custom-capability").unwrap());
1990    }
1991
1992    // ========================================================================
1993    // Capability parse() validation tests
1994    // ========================================================================
1995
1996    #[test]
1997    fn capability_parse_accepts_valid_strings() {
1998        assert!(Capability::parse("deploy").is_ok());
1999        assert!(Capability::parse("acme:deploy").is_ok());
2000        assert!(Capability::parse("my-custom-cap").is_ok());
2001        assert!(Capability::parse("org:team:action").is_ok());
2002        assert!(Capability::parse("with_underscore").is_ok()); // underscore allowed
2003    }
2004
2005    #[test]
2006    fn capability_parse_rejects_invalid_strings() {
2007        // Empty
2008        assert!(matches!(Capability::parse(""), Err(CapabilityError::Empty)));
2009
2010        // Too long
2011        assert!(matches!(
2012            Capability::parse(&"a".repeat(65)),
2013            Err(CapabilityError::TooLong(65))
2014        ));
2015
2016        // Invalid characters
2017        assert!(matches!(
2018            Capability::parse("has spaces"),
2019            Err(CapabilityError::InvalidChars(_))
2020        ));
2021        assert!(matches!(
2022            Capability::parse("has.dot"),
2023            Err(CapabilityError::InvalidChars(_))
2024        ));
2025    }
2026
2027    #[test]
2028    fn capability_parse_rejects_reserved_namespace() {
2029        assert!(matches!(
2030            Capability::parse("auths:custom"),
2031            Err(CapabilityError::ReservedNamespace)
2032        ));
2033        assert!(matches!(
2034            Capability::parse("auths:sign_commit"),
2035            Err(CapabilityError::ReservedNamespace)
2036        ));
2037    }
2038
2039    #[test]
2040    fn capability_parse_normalizes_to_lowercase() {
2041        let cap = Capability::parse("DEPLOY").unwrap();
2042        assert_eq!(cap.as_str(), "deploy");
2043
2044        let cap = Capability::parse("ACME:Deploy").unwrap();
2045        assert_eq!(cap.as_str(), "acme:deploy");
2046    }
2047
2048    #[test]
2049    fn capability_parse_trims_whitespace() {
2050        let cap = Capability::parse("  deploy  ").unwrap();
2051        assert_eq!(cap.as_str(), "deploy");
2052    }
2053
2054    // ========================================================================
2055    // Capability equality and hashing tests
2056    // ========================================================================
2057
2058    #[test]
2059    fn capability_is_hashable() {
2060        use std::collections::HashSet;
2061        let mut set = HashSet::new();
2062        set.insert(Capability::sign_commit());
2063        set.insert(Capability::sign_release());
2064        set.insert(Capability::parse("test").unwrap());
2065        assert_eq!(set.len(), 3);
2066        assert!(set.contains(&Capability::sign_commit()));
2067    }
2068
2069    #[test]
2070    fn capability_equality_with_different_construction_paths() {
2071        // Well-known constructor equals deserialized
2072        let from_constructor = Capability::sign_commit();
2073        let from_deser: Capability = serde_json::from_str(r#""sign_commit""#).unwrap();
2074        assert_eq!(from_constructor, from_deser);
2075
2076        // Parse equals deserialized for custom capabilities
2077        let from_parse = Capability::parse("acme:deploy").unwrap();
2078        let from_deser: Capability = serde_json::from_str(r#""acme:deploy""#).unwrap();
2079        assert_eq!(from_parse, from_deser);
2080    }
2081
2082    // ========================================================================
2083    // Capability display and accessor tests
2084    // ========================================================================
2085
2086    #[test]
2087    fn capability_display_matches_canonical_form() {
2088        assert_eq!(Capability::sign_commit().to_string(), "sign_commit");
2089        assert_eq!(Capability::sign_release().to_string(), "sign_release");
2090        assert_eq!(Capability::manage_members().to_string(), "manage_members");
2091        assert_eq!(Capability::rotate_keys().to_string(), "rotate_keys");
2092        assert_eq!(
2093            Capability::parse("acme:deploy").unwrap().to_string(),
2094            "acme:deploy"
2095        );
2096    }
2097
2098    #[test]
2099    fn capability_as_str_returns_canonical_form() {
2100        assert_eq!(Capability::sign_commit().as_str(), "sign_commit");
2101        assert_eq!(Capability::sign_release().as_str(), "sign_release");
2102        assert_eq!(Capability::manage_members().as_str(), "manage_members");
2103        assert_eq!(Capability::rotate_keys().as_str(), "rotate_keys");
2104        assert_eq!(
2105            Capability::parse("acme:deploy").unwrap().as_str(),
2106            "acme:deploy"
2107        );
2108    }
2109
2110    #[test]
2111    fn capability_is_well_known() {
2112        assert!(Capability::sign_commit().is_well_known());
2113        assert!(Capability::sign_release().is_well_known());
2114        assert!(Capability::manage_members().is_well_known());
2115        assert!(Capability::rotate_keys().is_well_known());
2116        assert!(!Capability::parse("custom").unwrap().is_well_known());
2117    }
2118
2119    #[test]
2120    fn capability_namespace() {
2121        assert_eq!(
2122            Capability::parse("acme:deploy").unwrap().namespace(),
2123            Some("acme")
2124        );
2125        assert_eq!(
2126            Capability::parse("org:team:action").unwrap().namespace(),
2127            Some("org")
2128        );
2129        assert_eq!(Capability::parse("deploy").unwrap().namespace(), None);
2130    }
2131
2132    // ========================================================================
2133    // Capability vec serialization tests
2134    // ========================================================================
2135
2136    #[test]
2137    fn capability_vec_serializes_as_array() {
2138        let caps = vec![Capability::sign_commit(), Capability::sign_release()];
2139        let json = serde_json::to_string(&caps).unwrap();
2140        assert_eq!(json, r#"["sign_commit","sign_release"]"#);
2141    }
2142
2143    #[test]
2144    fn capability_vec_deserializes_from_array() {
2145        let json = r#"["sign_commit","manage_members","custom-cap"]"#;
2146        let caps: Vec<Capability> = serde_json::from_str(json).unwrap();
2147        assert_eq!(caps.len(), 3);
2148        assert_eq!(caps[0], Capability::sign_commit());
2149        assert_eq!(caps[1], Capability::manage_members());
2150        assert_eq!(caps[2], Capability::parse("custom-cap").unwrap());
2151    }
2152
2153    // ========================================================================
2154    // Serde roundtrip tests (critical for backward compat)
2155    // ========================================================================
2156
2157    #[test]
2158    fn capability_serde_roundtrip_well_known() {
2159        let caps = vec![
2160            Capability::sign_commit(),
2161            Capability::sign_release(),
2162            Capability::manage_members(),
2163            Capability::rotate_keys(),
2164        ];
2165        for cap in caps {
2166            let json = serde_json::to_string(&cap).unwrap();
2167            let roundtrip: Capability = serde_json::from_str(&json).unwrap();
2168            assert_eq!(cap, roundtrip);
2169        }
2170    }
2171
2172    #[test]
2173    fn capability_serde_roundtrip_custom() {
2174        let caps = vec![
2175            Capability::parse("deploy").unwrap(),
2176            Capability::parse("acme:deploy").unwrap(),
2177            Capability::parse("org:team:action").unwrap(),
2178        ];
2179        for cap in caps {
2180            let json = serde_json::to_string(&cap).unwrap();
2181            let roundtrip: Capability = serde_json::from_str(&json).unwrap();
2182            assert_eq!(cap, roundtrip);
2183        }
2184    }
2185
2186    // Tests for Attestation delegation field
2187
2188    #[test]
2189    fn attestation_old_json_without_delegated_by_deserializes() {
2190        // Simulates an attestation JSON without the delegated_by field.
2191        let old_json = r#"{
2192            "version": 1,
2193            "rid": "test-rid",
2194            "issuer": "did:keri:Eissuer",
2195            "subject": "did:key:zSubject",
2196            "device_public_key": "0102030405060708091011121314151617181920212223242526272829303132",
2197            "identity_signature": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
2198            "device_signature": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
2199            "revoked_at": null,
2200            "timestamp": null
2201        }"#;
2202
2203        let att: Attestation = serde_json::from_str(old_json).unwrap();
2204
2205        assert_eq!(att.delegated_by, None);
2206    }
2207
2208    #[test]
2209    fn attestation_delegated_by_serializes_correctly() {
2210        let att = AttestationBuilder::default()
2211            .rid("test-rid")
2212            .issuer("did:keri:Eissuer")
2213            .subject("did:key:zSubject")
2214            .delegated_by(Some(CanonicalDid::new_unchecked("did:keri:Edelegator")))
2215            .build();
2216
2217        let json = serde_json::to_string(&att).unwrap();
2218        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
2219
2220        assert_eq!(parsed["delegated_by"], "did:keri:Edelegator");
2221    }
2222
2223    #[test]
2224    fn attestation_omits_delegated_by_when_absent() {
2225        let att = AttestationBuilder::default()
2226            .rid("test-rid")
2227            .issuer("did:keri:Eissuer")
2228            .subject("did:key:zSubject")
2229            .build();
2230
2231        let json = serde_json::to_string(&att).unwrap();
2232        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
2233
2234        assert!(parsed.get("delegated_by").is_none());
2235    }
2236
2237    #[test]
2238    fn attestation_delegated_by_roundtrips() {
2239        let original = AttestationBuilder::default()
2240            .rid("test-rid")
2241            .issuer("did:keri:Eissuer")
2242            .subject("did:key:zSubject")
2243            .delegated_by(Some(CanonicalDid::new_unchecked("did:keri:Eadmin")))
2244            .build();
2245
2246        let json = serde_json::to_string(&original).unwrap();
2247        let deserialized: Attestation = serde_json::from_str(&json).unwrap();
2248
2249        assert_eq!(original.delegated_by, deserialized.delegated_by);
2250    }
2251
2252    // Tests for ThresholdPolicy (fn-6.11)
2253
2254    #[test]
2255    fn threshold_policy_new_creates_valid_policy() {
2256        let policy = ThresholdPolicy::new(
2257            2,
2258            vec![
2259                "did:key:alice".to_string(),
2260                "did:key:bob".to_string(),
2261                "did:key:carol".to_string(),
2262            ],
2263            "test-policy".to_string(),
2264        );
2265
2266        assert_eq!(policy.threshold, 2);
2267        assert_eq!(policy.signers.len(), 3);
2268        assert_eq!(policy.policy_id, "test-policy");
2269        assert!(policy.scope.is_none());
2270        assert!(policy.ceremony_endpoint.is_none());
2271    }
2272
2273    #[test]
2274    fn threshold_policy_is_valid_checks_constraints() {
2275        // Valid 2-of-3
2276        let valid = ThresholdPolicy::new(
2277            2,
2278            vec!["a".to_string(), "b".to_string(), "c".to_string()],
2279            "policy".to_string(),
2280        );
2281        assert!(valid.is_valid());
2282
2283        // Invalid: threshold 0
2284        let zero_threshold = ThresholdPolicy::new(0, vec!["a".to_string()], "policy".to_string());
2285        assert!(!zero_threshold.is_valid());
2286
2287        // Invalid: threshold > signers
2288        let too_high = ThresholdPolicy::new(
2289            3,
2290            vec!["a".to_string(), "b".to_string()],
2291            "policy".to_string(),
2292        );
2293        assert!(!too_high.is_valid());
2294
2295        // Invalid: empty signers
2296        let no_signers = ThresholdPolicy::new(1, vec![], "policy".to_string());
2297        assert!(!no_signers.is_valid());
2298
2299        // Invalid: empty policy_id
2300        let no_id = ThresholdPolicy::new(1, vec!["a".to_string()], "".to_string());
2301        assert!(!no_id.is_valid());
2302    }
2303
2304    #[test]
2305    fn threshold_policy_m_of_n_returns_correct_values() {
2306        let policy = ThresholdPolicy::new(
2307            2,
2308            vec!["a".to_string(), "b".to_string(), "c".to_string()],
2309            "policy".to_string(),
2310        );
2311        let (m, n) = policy.m_of_n();
2312        assert_eq!(m, 2);
2313        assert_eq!(n, 3);
2314    }
2315
2316    #[test]
2317    fn threshold_policy_serializes_correctly() {
2318        let mut policy = ThresholdPolicy::new(
2319            2,
2320            vec!["did:key:alice".to_string(), "did:key:bob".to_string()],
2321            "release-policy".to_string(),
2322        );
2323        policy.scope = Some(Capability::sign_release());
2324        policy.ceremony_endpoint = Some("wss://example.com/ceremony".to_string());
2325
2326        let json = serde_json::to_string(&policy).unwrap();
2327        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
2328
2329        assert_eq!(parsed["threshold"], 2);
2330        assert_eq!(parsed["signers"][0], "did:key:alice");
2331        assert_eq!(parsed["policy_id"], "release-policy");
2332        assert_eq!(parsed["scope"], "sign_release");
2333        assert_eq!(parsed["ceremony_endpoint"], "wss://example.com/ceremony");
2334    }
2335
2336    #[test]
2337    fn threshold_policy_without_optional_fields_omits_them() {
2338        let policy =
2339            ThresholdPolicy::new(1, vec!["did:key:alice".to_string()], "policy".to_string());
2340
2341        let json = serde_json::to_string(&policy).unwrap();
2342        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
2343
2344        assert!(parsed.get("scope").is_none());
2345        assert!(parsed.get("ceremony_endpoint").is_none());
2346    }
2347
2348    #[test]
2349    fn threshold_policy_roundtrips() {
2350        let mut original = ThresholdPolicy::new(
2351            3,
2352            vec![
2353                "a".to_string(),
2354                "b".to_string(),
2355                "c".to_string(),
2356                "d".to_string(),
2357            ],
2358            "important-policy".to_string(),
2359        );
2360        original.scope = Some(Capability::rotate_keys());
2361
2362        let json = serde_json::to_string(&original).unwrap();
2363        let deserialized: ThresholdPolicy = serde_json::from_str(&json).unwrap();
2364
2365        assert_eq!(original, deserialized);
2366    }
2367
2368    // Tests for IdentityBundle (CI/CD stateless verification)
2369
2370    #[test]
2371    fn identity_bundle_serializes_correctly() {
2372        let bundle = IdentityBundle {
2373            identity_did: IdentityDID::new_unchecked("did:keri:test123"),
2374            public_key_hex: PublicKeyHex::new_unchecked("aabbccdd"),
2375            curve: Default::default(),
2376            attestation_chain: vec![],
2377            kel: vec![],
2378            bundle_timestamp: DateTime::parse_from_rfc3339("2099-01-01T00:00:00Z")
2379                .unwrap()
2380                .with_timezone(&Utc),
2381            max_valid_for_secs: 86400,
2382        };
2383
2384        let json = serde_json::to_string(&bundle).unwrap();
2385        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
2386
2387        assert_eq!(parsed["identity_did"], "did:keri:test123");
2388        assert_eq!(parsed["public_key_hex"], "aabbccdd");
2389        assert!(parsed["attestation_chain"].as_array().unwrap().is_empty());
2390    }
2391
2392    #[test]
2393    fn identity_bundle_deserializes_correctly() {
2394        let json = r#"{
2395            "identity_did": "did:keri:abc123",
2396            "public_key_hex": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20",
2397            "attestation_chain": [],
2398            "bundle_timestamp": "2099-01-01T00:00:00Z",
2399            "max_valid_for_secs": 86400
2400        }"#;
2401
2402        let bundle: IdentityBundle = serde_json::from_str(json).unwrap();
2403
2404        assert_eq!(bundle.identity_did.as_str(), "did:keri:abc123");
2405        assert_eq!(
2406            bundle.public_key_hex.as_str(),
2407            "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"
2408        );
2409        assert!(bundle.attestation_chain.is_empty());
2410    }
2411
2412    #[test]
2413    fn identity_bundle_roundtrips() {
2414        let attestation = AttestationBuilder::default()
2415            .rid("test-rid")
2416            .issuer("did:keri:Eissuer")
2417            .subject("did:key:zSubject")
2418            .build();
2419
2420        let original = IdentityBundle {
2421            identity_did: IdentityDID::new_unchecked("did:keri:Eexample"),
2422            public_key_hex: PublicKeyHex::new_unchecked(
2423                "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
2424            ),
2425            curve: Default::default(),
2426            attestation_chain: vec![attestation],
2427            kel: vec![],
2428            bundle_timestamp: DateTime::parse_from_rfc3339("2099-01-01T00:00:00Z")
2429                .unwrap()
2430                .with_timezone(&Utc),
2431            max_valid_for_secs: 86400,
2432        };
2433
2434        let json = serde_json::to_string(&original).unwrap();
2435        let deserialized: IdentityBundle = serde_json::from_str(&json).unwrap();
2436
2437        assert_eq!(original.identity_did, deserialized.identity_did);
2438        assert_eq!(original.public_key_hex, deserialized.public_key_hex);
2439        assert_eq!(
2440            original.attestation_chain.len(),
2441            deserialized.attestation_chain.len()
2442        );
2443    }
2444}
2445
2446#[cfg(test)]
2447mod decode_public_key_tests {
2448    use super::*;
2449
2450    #[test]
2451    fn hex_ed25519_32_bytes() {
2452        let hex = "00".repeat(32);
2453        let pk = decode_public_key_hex(&hex, auths_crypto::CurveType::Ed25519).unwrap();
2454        assert_eq!(pk.curve(), auths_crypto::CurveType::Ed25519);
2455        assert_eq!(pk.len(), 32);
2456    }
2457
2458    #[test]
2459    fn hex_p256_33_bytes_compressed() {
2460        // Need a valid-ish compressed SEC1 for try_new to accept: starts with 0x02 or 0x03
2461        let mut bytes = [0u8; 33];
2462        bytes[0] = 0x02;
2463        let hex = hex::encode(bytes);
2464        let pk = decode_public_key_hex(&hex, auths_crypto::CurveType::P256).unwrap();
2465        assert_eq!(pk.curve(), auths_crypto::CurveType::P256);
2466        assert_eq!(pk.len(), 33);
2467    }
2468
2469    #[test]
2470    fn bytes_p256_65_uncompressed() {
2471        let mut bytes = [0u8; 65];
2472        bytes[0] = 0x04;
2473        let pk = decode_public_key_bytes(&bytes, auths_crypto::CurveType::P256).unwrap();
2474        assert_eq!(pk.curve(), auths_crypto::CurveType::P256);
2475        assert_eq!(pk.len(), 65);
2476    }
2477
2478    #[test]
2479    fn rejects_validation_error() {
2480        let err = decode_public_key_bytes(&[0u8; 50], auths_crypto::CurveType::P256).unwrap_err();
2481        assert!(matches!(err, PublicKeyDecodeError::Validation(_)));
2482    }
2483
2484    #[test]
2485    fn rejects_malformed_hex() {
2486        let err = decode_public_key_hex("zz", auths_crypto::CurveType::P256).unwrap_err();
2487        assert!(matches!(err, PublicKeyDecodeError::InvalidHex(_)));
2488    }
2489}