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