1use 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
15pub const MAX_ATTESTATION_JSON_SIZE: usize = 64 * 1024;
17
18pub const MAX_JSON_BATCH_SIZE: usize = 1024 * 1024;
20
21pub const MAX_PUBLIC_KEY_HEX_LEN: usize = 64;
23pub const MAX_SIGNATURE_HEX_LEN: usize = 128;
25pub 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#[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 pub fn new(s: impl Into<String>) -> Self {
49 Self(s.into())
50 }
51
52 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#[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 Admin,
115 Member,
117 Readonly,
119}
120
121impl Role {
122 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 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#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
167#[error("unknown role: '{0}' (expected admin, member, or readonly)")]
168pub struct RoleParseError(String);
169
170#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
179pub struct Ed25519PublicKey([u8; 32]);
180
181impl Ed25519PublicKey {
182 pub fn from_bytes(bytes: [u8; 32]) -> Self {
184 Self(bytes)
185 }
186
187 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 pub fn as_bytes(&self) -> &[u8; 32] {
205 &self.0
206 }
207
208 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#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
266pub enum Ed25519KeyError {
267 #[error("expected 32 bytes, got {0}")]
269 InvalidLength(usize),
270 #[error("invalid hex: {0}")]
272 InvalidHex(String),
273}
274
275pub const ATTESTATION_VERSION: u32 = 2;
285
286#[derive(Debug, Clone, PartialEq, Eq)]
295pub struct TypedSignature([u8; 64]);
296
297pub type Ed25519Signature = TypedSignature;
299
300impl TypedSignature {
301 pub fn from_bytes(bytes: [u8; 64]) -> Self {
303 Self(bytes)
304 }
305
306 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 pub fn empty() -> Self {
316 Self([0u8; 64])
317 }
318
319 pub fn is_empty(&self) -> bool {
321 self.0 == [0u8; 64]
322 }
323
324 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#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
391#[error("expected 64 bytes, got {0}")]
392pub struct SignatureLengthError(pub usize);
393
394#[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 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#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
450pub enum InvalidKeyError {
451 #[error("invalid key length for {curve}: expected {expected}, got {actual}")]
453 InvalidLength {
454 curve: auths_crypto::CurveType,
456 expected: &'static str,
458 actual: usize,
460 },
461}
462
463impl DevicePublicKey {
464 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 pub fn ed25519(bytes: &[u8; 32]) -> Self {
497 Self {
498 curve: auths_crypto::CurveType::Ed25519,
499 bytes: bytes.to_vec(),
500 }
501 }
502
503 pub fn p256(bytes: &[u8]) -> Result<Self, InvalidKeyError> {
507 Self::try_new(auths_crypto::CurveType::P256, bytes)
508 }
509
510 pub fn curve(&self) -> auths_crypto::CurveType {
512 self.curve
513 }
514
515 pub fn as_bytes(&self) -> &[u8] {
517 &self.bytes
518 }
519
520 pub fn is_zero(&self) -> bool {
522 self.bytes.iter().all(|&b| b == 0)
523 }
524
525 pub fn len(&self) -> usize {
527 self.bytes.len()
528 }
529
530 pub fn is_empty(&self) -> bool {
532 self.bytes.is_empty()
533 }
534
535 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#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
576pub enum PublicKeyDecodeError {
577 #[error("invalid hex: {0}")]
579 InvalidHex(String),
580
581 #[error("invalid public key length {len} — expected 32 (Ed25519) or 33/65 (P-256)")]
584 InvalidLength {
585 len: usize,
587 },
588
589 #[error("DevicePublicKey validation failed: {0}")]
592 Validation(String),
593}
594
595pub 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
620pub 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#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
641pub enum SignatureVerifyError {
642 #[error("signature verification failed: {0}")]
644 VerificationFailed(String),
645
646 #[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 let value = serde_json::Value::deserialize(d)?;
677
678 if let Some(s) = value.as_str() {
679 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 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#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
754#[serde(rename_all = "snake_case")]
755pub enum SignatureAlgorithm {
756 #[default]
758 Ed25519,
759 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#[derive(Debug, Clone, PartialEq, Eq)]
786pub struct EcdsaP256PublicKey(Vec<u8>);
787
788impl EcdsaP256PublicKey {
789 pub fn from_der(der: &[u8]) -> Result<Self, EcdsaP256Error> {
794 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 pub fn as_der(&self) -> &[u8] {
812 &self.0
813 }
814
815 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#[derive(Debug, Clone, PartialEq, Eq)]
863pub struct EcdsaP256Signature(Vec<u8>);
864
865impl EcdsaP256Signature {
866 pub fn from_der(der: &[u8]) -> Result<Self, EcdsaP256Error> {
868 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 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#[derive(Debug, Clone, thiserror::Error)]
903pub enum EcdsaP256Error {
904 #[error("invalid ECDSA P-256 key: {0}")]
906 InvalidKey(String),
907 #[error("invalid ECDSA P-256 signature: {0}")]
909 InvalidSignature(String),
910}
911
912#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
917#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
918pub struct IdentityBundle {
919 pub identity_did: IdentityDID,
921 pub public_key_hex: PublicKeyHex,
923 #[serde(default)]
927 #[cfg_attr(feature = "schema", schemars(with = "String"))]
928 pub curve: auths_crypto::CurveType,
929 pub attestation_chain: Vec<Attestation>,
931 #[serde(default, skip_serializing_if = "Vec::is_empty")]
937 pub kel: Vec<auths_keri::Event>,
938 #[serde(default, skip_serializing_if = "Vec::is_empty")]
945 pub kel_attachments: Vec<String>,
946 #[serde(default, skip_serializing_if = "Vec::is_empty")]
953 pub device_kels: Vec<BundleDeviceKel>,
954 pub bundle_timestamp: DateTime<Utc>,
956 pub max_valid_for_secs: u64,
958}
959
960impl IdentityBundle {
961 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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
984#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
985pub struct BundleDeviceKel {
986 pub did: String,
988 pub kel: Vec<auths_keri::Event>,
990 pub kel_attachments: Vec<String>,
992}
993
994#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
996#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
997pub struct Attestation {
998 pub version: u32,
1000 pub rid: ResourceId,
1002 pub issuer: CanonicalDid,
1004 pub subject: CanonicalDid,
1006 pub device_public_key: DevicePublicKey,
1008 #[serde(default, skip_serializing_if = "Ed25519Signature::is_empty")]
1010 pub identity_signature: Ed25519Signature,
1011 pub device_signature: Ed25519Signature,
1013 #[serde(default, skip_serializing_if = "Option::is_none")]
1015 pub revoked_at: Option<DateTime<Utc>>,
1016 #[serde(skip_serializing_if = "Option::is_none")]
1018 pub expires_at: Option<DateTime<Utc>>,
1019 pub timestamp: Option<DateTime<Utc>>,
1021 #[serde(skip_serializing_if = "Option::is_none")]
1023 pub note: Option<String>,
1024 #[serde(skip_serializing_if = "Option::is_none")]
1026 pub payload: Option<Value>,
1027
1028 #[serde(skip_serializing_if = "Option::is_none")]
1030 pub commit_sha: Option<String>,
1031
1032 #[serde(skip_serializing_if = "Option::is_none")]
1034 pub commit_message: Option<String>,
1035
1036 #[serde(skip_serializing_if = "Option::is_none")]
1038 pub author: Option<String>,
1039
1040 #[serde(skip_serializing_if = "Option::is_none")]
1042 pub oidc_binding: Option<OidcBinding>,
1043
1044 #[serde(default, skip_serializing_if = "Option::is_none")]
1046 pub delegated_by: Option<CanonicalDid>,
1047
1048 #[serde(default, skip_serializing_if = "Option::is_none")]
1051 pub signer_type: Option<SignerType>,
1052
1053 #[serde(default, skip_serializing_if = "Option::is_none")]
1056 pub environment_claim: Option<Value>,
1057}
1058
1059#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1065#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1066pub struct OidcBinding {
1067 pub issuer: String,
1069 pub subject: String,
1071 pub audience: String,
1073 pub token_exp: i64,
1075 #[serde(default, skip_serializing_if = "Option::is_none")]
1077 pub platform: Option<String>,
1078 #[serde(default, skip_serializing_if = "Option::is_none")]
1080 pub jti: Option<String>,
1081 #[serde(default, skip_serializing_if = "Option::is_none")]
1083 pub normalized_claims: Option<serde_json::Map<String, serde_json::Value>>,
1084}
1085
1086#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1091#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1092#[non_exhaustive]
1093pub enum SignerType {
1094 Human,
1096 Agent,
1098 Workload,
1100}
1101
1102#[derive(Debug, Clone, Serialize)]
1112pub struct VerifiedAttestation(Attestation);
1113
1114impl VerifiedAttestation {
1115 pub fn inner(&self) -> &Attestation {
1117 &self.0
1118 }
1119
1120 pub fn into_inner(self) -> Attestation {
1122 self.0
1123 }
1124
1125 #[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#[derive(Serialize, Debug)]
1150pub struct CanonicalAttestationData<'a> {
1151 pub version: u32,
1153 pub rid: &'a str,
1155 pub issuer: &'a CanonicalDid,
1157 pub subject: &'a CanonicalDid,
1159 #[serde(with = "hex::serde")]
1161 pub device_public_key: &'a [u8],
1162 pub payload: &'a Option<Value>,
1164 pub timestamp: &'a Option<DateTime<Utc>>,
1166 pub expires_at: &'a Option<DateTime<Utc>>,
1168 pub revoked_at: &'a Option<DateTime<Utc>>,
1170 pub note: &'a Option<String>,
1172
1173 #[serde(skip_serializing_if = "Option::is_none")]
1175 pub delegated_by: Option<&'a CanonicalDid>,
1176 #[serde(skip_serializing_if = "Option::is_none")]
1178 pub signer_type: Option<&'a SignerType>,
1179 #[serde(skip_serializing_if = "Option::is_none")]
1181 pub commit_sha: Option<&'a str>,
1182 #[serde(skip_serializing_if = "Option::is_none")]
1187 pub oidc_binding: Option<&'a OidcBinding>,
1188}
1189
1190pub 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 pub fn is_revoked(&self) -> bool {
1210 self.revoked_at.is_some()
1211 }
1212
1213 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 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 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, 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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1341#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1342pub struct ThresholdPolicy {
1343 pub threshold: u8,
1345
1346 pub signers: Vec<String>,
1348
1349 pub policy_id: PolicyId,
1351
1352 #[serde(default, skip_serializing_if = "Option::is_none")]
1354 pub scope: Option<Capability>,
1355
1356 #[serde(default, skip_serializing_if = "Option::is_none")]
1358 pub ceremony_endpoint: Option<String>,
1359}
1360
1361impl ThresholdPolicy {
1362 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 pub fn is_valid(&self) -> bool {
1375 if self.threshold < 1 {
1377 return false;
1378 }
1379 if self.threshold as usize > self.signers.len() {
1381 return false;
1382 }
1383 if self.signers.is_empty() {
1385 return false;
1386 }
1387 if self.policy_id.is_empty() {
1389 return false;
1390 }
1391 true
1392 }
1393
1394 pub fn m_of_n(&self) -> (u8, usize) {
1396 (self.threshold, self.signers.len())
1397 }
1398}
1399
1400#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1406pub enum CommitOidError {
1407 #[error("commit OID is empty")]
1409 Empty,
1410 #[error("expected 40 or 64 hex chars, got {0}")]
1412 InvalidLength(usize),
1413 #[error("invalid hex character in commit OID")]
1415 InvalidHex,
1416}
1417
1418#[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 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 pub fn new_unchecked(s: impl Into<String>) -> Self {
1455 Self(s.into())
1456 }
1457
1458 pub fn as_str(&self) -> &str {
1460 &self.0
1461 }
1462
1463 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#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1521pub enum PublicKeyHexError {
1522 #[error("expected 64 (Ed25519) or 66 (P-256) hex chars, got {0} chars")]
1524 InvalidLength(usize),
1525 #[error("invalid hex: {0}")]
1527 InvalidHex(String),
1528}
1529
1530#[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 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 pub fn new_unchecked(s: impl Into<String>) -> Self {
1560 Self(s.into())
1561 }
1562
1563 pub fn as_str(&self) -> &str {
1565 &self.0
1566 }
1567
1568 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#[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 pub fn new(s: impl Into<String>) -> Self {
1636 Self(s.into())
1637 }
1638
1639 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 #[test]
1691 fn attestation_old_json_without_delegated_by_deserializes() {
1692 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 #[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 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 let zero_threshold = ThresholdPolicy::new(0, vec!["a".to_string()], "policy".to_string());
1787 assert!(!zero_threshold.is_valid());
1788
1789 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 let no_signers = ThresholdPolicy::new(1, vec![], "policy".to_string());
1799 assert!(!no_signers.is_valid());
1800
1801 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 #[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 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}