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
28const SIGN_COMMIT: &str = "sign_commit";
30const SIGN_RELEASE: &str = "sign_release";
31const MANAGE_MEMBERS: &str = "manage_members";
32const ROTATE_KEYS: &str = "rotate_keys";
33
34#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
44#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
45#[serde(transparent)]
46pub struct ResourceId(String);
47
48impl ResourceId {
49 pub fn new(s: impl Into<String>) -> Self {
51 Self(s.into())
52 }
53
54 pub fn as_str(&self) -> &str {
56 &self.0
57 }
58}
59
60impl Deref for ResourceId {
61 type Target = str;
62 fn deref(&self) -> &str {
63 &self.0
64 }
65}
66
67impl fmt::Display for ResourceId {
68 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69 f.write_str(&self.0)
70 }
71}
72
73impl From<String> for ResourceId {
74 fn from(s: String) -> Self {
75 Self(s)
76 }
77}
78
79impl From<&str> for ResourceId {
80 fn from(s: &str) -> Self {
81 Self(s.to_string())
82 }
83}
84
85impl PartialEq<str> for ResourceId {
86 fn eq(&self, other: &str) -> bool {
87 self.0 == other
88 }
89}
90
91impl PartialEq<&str> for ResourceId {
92 fn eq(&self, other: &&str) -> bool {
93 self.0 == *other
94 }
95}
96
97impl PartialEq<String> for ResourceId {
98 fn eq(&self, other: &String) -> bool {
99 self.0 == *other
100 }
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
112#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
113#[serde(rename_all = "lowercase")]
114pub enum Role {
115 Admin,
117 Member,
119 Readonly,
121}
122
123impl Role {
124 pub fn as_str(&self) -> &str {
126 match self {
127 Role::Admin => "admin",
128 Role::Member => "member",
129 Role::Readonly => "readonly",
130 }
131 }
132
133 pub fn default_capabilities(&self) -> Vec<Capability> {
135 match self {
136 Role::Admin => vec![
137 Capability::sign_commit(),
138 Capability::sign_release(),
139 Capability::manage_members(),
140 Capability::rotate_keys(),
141 ],
142 Role::Member => vec![Capability::sign_commit(), Capability::sign_release()],
143 Role::Readonly => vec![],
144 }
145 }
146}
147
148impl fmt::Display for Role {
149 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150 f.write_str(self.as_str())
151 }
152}
153
154impl FromStr for Role {
155 type Err = RoleParseError;
156
157 fn from_str(s: &str) -> Result<Self, Self::Err> {
158 match s.trim().to_lowercase().as_str() {
159 "admin" => Ok(Role::Admin),
160 "member" => Ok(Role::Member),
161 "readonly" => Ok(Role::Readonly),
162 other => Err(RoleParseError(other.to_string())),
163 }
164 }
165}
166
167#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
169#[error("unknown role: '{0}' (expected admin, member, or readonly)")]
170pub struct RoleParseError(String);
171
172#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
181pub struct Ed25519PublicKey([u8; 32]);
182
183impl Ed25519PublicKey {
184 pub fn from_bytes(bytes: [u8; 32]) -> Self {
186 Self(bytes)
187 }
188
189 pub fn try_from_slice(slice: &[u8]) -> Result<Self, Ed25519KeyError> {
199 let arr: [u8; 32] = slice
200 .try_into()
201 .map_err(|_| Ed25519KeyError::InvalidLength(slice.len()))?;
202 Ok(Self(arr))
203 }
204
205 pub fn as_bytes(&self) -> &[u8; 32] {
207 &self.0
208 }
209
210 pub fn is_zero(&self) -> bool {
212 self.0 == [0u8; 32]
213 }
214}
215
216impl Serialize for Ed25519PublicKey {
217 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
218 s.serialize_str(&hex::encode(self.0))
219 }
220}
221
222impl<'de> Deserialize<'de> for Ed25519PublicKey {
223 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
224 let s = String::deserialize(d)?;
225 let bytes =
226 hex::decode(&s).map_err(|e| serde::de::Error::custom(format!("invalid hex: {e}")))?;
227 let arr: [u8; 32] = bytes.try_into().map_err(|v: Vec<u8>| {
228 serde::de::Error::custom(format!("expected 32 bytes, got {}", v.len()))
229 })?;
230 Ok(Self(arr))
231 }
232}
233
234impl fmt::Display for Ed25519PublicKey {
235 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
236 f.write_str(&hex::encode(self.0))
237 }
238}
239
240impl AsRef<[u8]> for Ed25519PublicKey {
241 fn as_ref(&self) -> &[u8] {
242 &self.0
243 }
244}
245
246#[cfg(feature = "schema")]
247impl schemars::JsonSchema for Ed25519PublicKey {
248 fn schema_name() -> String {
249 "Ed25519PublicKey".to_owned()
250 }
251
252 fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
253 schemars::schema::SchemaObject {
254 instance_type: Some(schemars::schema::InstanceType::String.into()),
255 format: Some("hex".to_owned()),
256 metadata: Some(Box::new(schemars::schema::Metadata {
257 description: Some("Ed25519 public key (32 bytes, hex-encoded)".to_owned()),
258 ..Default::default()
259 })),
260 ..Default::default()
261 }
262 .into()
263 }
264}
265
266#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
268pub enum Ed25519KeyError {
269 #[error("expected 32 bytes, got {0}")]
271 InvalidLength(usize),
272 #[error("invalid hex: {0}")]
274 InvalidHex(String),
275}
276
277#[derive(Debug, Clone, PartialEq, Eq)]
283pub struct Ed25519Signature([u8; 64]);
284
285impl Ed25519Signature {
286 pub fn from_bytes(bytes: [u8; 64]) -> Self {
288 Self(bytes)
289 }
290
291 pub fn try_from_slice(slice: &[u8]) -> Result<Self, SignatureLengthError> {
293 let arr: [u8; 64] = slice
294 .try_into()
295 .map_err(|_| SignatureLengthError(slice.len()))?;
296 Ok(Self(arr))
297 }
298
299 pub fn empty() -> Self {
301 Self([0u8; 64])
302 }
303
304 pub fn is_empty(&self) -> bool {
306 self.0 == [0u8; 64]
307 }
308
309 pub fn as_bytes(&self) -> &[u8; 64] {
311 &self.0
312 }
313}
314
315impl Default for Ed25519Signature {
316 fn default() -> Self {
317 Self::empty()
318 }
319}
320
321impl std::fmt::Display for Ed25519Signature {
322 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
323 write!(f, "{}", hex::encode(self.0))
324 }
325}
326
327impl AsRef<[u8]> for Ed25519Signature {
328 fn as_ref(&self) -> &[u8] {
329 &self.0
330 }
331}
332
333#[cfg(feature = "schema")]
334impl schemars::JsonSchema for Ed25519Signature {
335 fn schema_name() -> String {
336 "Ed25519Signature".to_owned()
337 }
338
339 fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
340 schemars::schema::SchemaObject {
341 instance_type: Some(schemars::schema::InstanceType::String.into()),
342 format: Some("hex".to_owned()),
343 metadata: Some(Box::new(schemars::schema::Metadata {
344 description: Some("Ed25519 signature (64 bytes, hex-encoded)".to_owned()),
345 ..Default::default()
346 })),
347 ..Default::default()
348 }
349 .into()
350 }
351}
352
353impl serde::Serialize for Ed25519Signature {
354 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
355 serializer.serialize_str(&hex::encode(self.0))
356 }
357}
358
359impl<'de> serde::Deserialize<'de> for Ed25519Signature {
360 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
361 let s = String::deserialize(deserializer)?;
362 if s.is_empty() {
363 return Ok(Self::empty());
364 }
365 let bytes = hex::decode(&s).map_err(serde::de::Error::custom)?;
366 Self::try_from_slice(&bytes).map_err(serde::de::Error::custom)
367 }
368}
369
370#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
372#[error("expected 64 bytes, got {0}")]
373pub struct SignatureLengthError(pub usize);
374
375#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
381pub enum CapabilityError {
382 #[error("capability is empty")]
384 Empty,
385 #[error("capability exceeds 64 chars: {0}")]
387 TooLong(usize),
388 #[error("invalid characters in capability '{0}': only alphanumeric, ':', '-', '_' allowed")]
390 InvalidChars(String),
391 #[error(
393 "reserved namespace 'auths:' — use well-known constructors or choose a different prefix"
394 )]
395 ReservedNamespace,
396 #[error("the '{0}' prefix is reserved for infrastructure capabilities")]
398 ReservedInfraNamespace(String),
399}
400
401#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
429#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
430#[serde(try_from = "String", into = "String")]
431pub struct Capability(String);
432
433impl Capability {
434 pub const MAX_LEN: usize = 64;
436
437 const RESERVED_PREFIX: &'static str = "auths:";
439
440 const RESERVED_INFRA_PREFIXES: &'static [&'static str] =
442 &["compute:", "network:", "storage:", "runtime:", "env:"];
443
444 #[inline]
452 pub fn sign_commit() -> Self {
453 Self(SIGN_COMMIT.to_string())
454 }
455
456 #[inline]
460 pub fn sign_release() -> Self {
461 Self(SIGN_RELEASE.to_string())
462 }
463
464 #[inline]
468 pub fn manage_members() -> Self {
469 Self(MANAGE_MEMBERS.to_string())
470 }
471
472 #[inline]
476 pub fn rotate_keys() -> Self {
477 Self(ROTATE_KEYS.to_string())
478 }
479
480 pub fn parse(raw: &str) -> Result<Self, CapabilityError> {
512 let canonical = raw.trim().to_lowercase();
513
514 if canonical.is_empty() {
515 return Err(CapabilityError::Empty);
516 }
517 if canonical.len() > Self::MAX_LEN {
518 return Err(CapabilityError::TooLong(canonical.len()));
519 }
520 if !canonical
521 .chars()
522 .all(|c| c.is_alphanumeric() || c == ':' || c == '-' || c == '_')
523 {
524 return Err(CapabilityError::InvalidChars(canonical));
525 }
526 if canonical.starts_with(Self::RESERVED_PREFIX) {
527 return Err(CapabilityError::ReservedNamespace);
528 }
529 for prefix in Self::RESERVED_INFRA_PREFIXES {
530 if canonical.starts_with(prefix) {
531 return Err(CapabilityError::ReservedInfraNamespace(prefix.to_string()));
532 }
533 }
534
535 Ok(Self(canonical))
536 }
537
538 #[deprecated(since = "0.2.0", note = "Use parse() for better error handling")]
546 pub fn custom(s: impl Into<String>) -> Option<Self> {
547 Self::parse(&s.into()).ok()
548 }
549
550 #[deprecated(since = "0.2.0", note = "Use parse() for validation")]
556 pub fn validate_custom(s: &str) -> bool {
557 Self::parse(s).is_ok()
558 }
559
560 #[inline]
569 pub fn as_str(&self) -> &str {
570 &self.0
571 }
572
573 pub fn is_well_known(&self) -> bool {
575 matches!(
576 self.0.as_str(),
577 SIGN_COMMIT | SIGN_RELEASE | MANAGE_MEMBERS | ROTATE_KEYS
578 )
579 }
580
581 pub fn namespace(&self) -> Option<&str> {
583 self.0.split(':').next().filter(|_| self.0.contains(':'))
584 }
585}
586
587impl fmt::Display for Capability {
588 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
589 f.write_str(&self.0)
590 }
591}
592
593impl TryFrom<String> for Capability {
594 type Error = CapabilityError;
595
596 fn try_from(s: String) -> Result<Self, Self::Error> {
597 let canonical = s.trim().to_lowercase();
598
599 if canonical.is_empty() {
600 return Err(CapabilityError::Empty);
601 }
602 if canonical.len() > Self::MAX_LEN {
603 return Err(CapabilityError::TooLong(canonical.len()));
604 }
605 if !canonical
606 .chars()
607 .all(|c| c.is_alphanumeric() || c == ':' || c == '-' || c == '_')
608 {
609 return Err(CapabilityError::InvalidChars(canonical));
610 }
611
612 Ok(Self(canonical))
615 }
616}
617
618impl std::str::FromStr for Capability {
619 type Err = CapabilityError;
620
621 fn from_str(s: &str) -> Result<Self, Self::Err> {
640 let normalized = s.trim().to_lowercase().replace('-', "_");
641 match normalized.as_str() {
642 "sign_commit" | "signcommit" => Ok(Capability::sign_commit()),
643 "sign_release" | "signrelease" => Ok(Capability::sign_release()),
644 "manage_members" | "managemembers" => Ok(Capability::manage_members()),
645 "rotate_keys" | "rotatekeys" => Ok(Capability::rotate_keys()),
646 _ => Capability::parse(&normalized),
647 }
648 }
649}
650
651impl From<Capability> for String {
652 fn from(cap: Capability) -> Self {
653 cap.0
654 }
655}
656
657#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
662#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
663pub struct IdentityBundle {
664 pub identity_did: IdentityDID,
666 pub public_key_hex: PublicKeyHex,
668 pub attestation_chain: Vec<Attestation>,
670 pub bundle_timestamp: DateTime<Utc>,
672 pub max_valid_for_secs: u64,
674}
675
676impl IdentityBundle {
677 pub fn check_freshness(&self, now: DateTime<Utc>) -> Result<(), AttestationError> {
687 let age = (now - self.bundle_timestamp).num_seconds().max(0) as u64;
688 if age > self.max_valid_for_secs {
689 return Err(AttestationError::BundleExpired {
690 age_secs: age,
691 max_secs: self.max_valid_for_secs,
692 });
693 }
694 Ok(())
695 }
696}
697
698#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
700#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
701pub struct Attestation {
702 pub version: u32,
704 pub rid: ResourceId,
706 pub issuer: CanonicalDid,
708 pub subject: CanonicalDid,
710 pub device_public_key: Ed25519PublicKey,
712 #[serde(default, skip_serializing_if = "Ed25519Signature::is_empty")]
714 pub identity_signature: Ed25519Signature,
715 pub device_signature: Ed25519Signature,
717 #[serde(default, skip_serializing_if = "Option::is_none")]
719 pub revoked_at: Option<DateTime<Utc>>,
720 #[serde(skip_serializing_if = "Option::is_none")]
722 pub expires_at: Option<DateTime<Utc>>,
723 pub timestamp: Option<DateTime<Utc>>,
725 #[serde(skip_serializing_if = "Option::is_none")]
727 pub note: Option<String>,
728 #[serde(skip_serializing_if = "Option::is_none")]
730 pub payload: Option<Value>,
731
732 #[serde(skip_serializing_if = "Option::is_none")]
734 pub commit_sha: Option<String>,
735
736 #[serde(skip_serializing_if = "Option::is_none")]
738 pub commit_message: Option<String>,
739
740 #[serde(skip_serializing_if = "Option::is_none")]
742 pub author: Option<String>,
743
744 #[serde(skip_serializing_if = "Option::is_none")]
746 pub oidc_binding: Option<OidcBinding>,
747
748 #[serde(default, skip_serializing_if = "Option::is_none")]
750 pub role: Option<Role>,
751
752 #[serde(default, skip_serializing_if = "Vec::is_empty")]
754 pub capabilities: Vec<Capability>,
755
756 #[serde(default, skip_serializing_if = "Option::is_none")]
758 pub delegated_by: Option<CanonicalDid>,
759
760 #[serde(default, skip_serializing_if = "Option::is_none")]
763 pub signer_type: Option<SignerType>,
764
765 #[serde(default, skip_serializing_if = "Option::is_none")]
768 pub environment_claim: Option<Value>,
769}
770
771#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
777#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
778pub struct OidcBinding {
779 pub issuer: String,
781 pub subject: String,
783 pub audience: String,
785 pub token_exp: i64,
787 #[serde(default, skip_serializing_if = "Option::is_none")]
789 pub platform: Option<String>,
790 #[serde(default, skip_serializing_if = "Option::is_none")]
792 pub jti: Option<String>,
793 #[serde(default, skip_serializing_if = "Option::is_none")]
795 pub normalized_claims: Option<serde_json::Map<String, serde_json::Value>>,
796}
797
798#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
803#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
804#[non_exhaustive]
805pub enum SignerType {
806 Human,
808 Agent,
810 Workload,
812}
813
814#[derive(Debug, Clone, Serialize)]
824pub struct VerifiedAttestation(Attestation);
825
826impl VerifiedAttestation {
827 pub fn inner(&self) -> &Attestation {
829 &self.0
830 }
831
832 pub fn into_inner(self) -> Attestation {
834 self.0
835 }
836
837 #[doc(hidden)]
843 pub fn dangerous_from_unchecked(attestation: Attestation) -> Self {
844 Self(attestation)
845 }
846
847 pub(crate) fn from_verified(attestation: Attestation) -> Self {
848 Self(attestation)
849 }
850}
851
852impl std::ops::Deref for VerifiedAttestation {
853 type Target = Attestation;
854
855 fn deref(&self) -> &Attestation {
856 &self.0
857 }
858}
859
860#[derive(Serialize, Debug)]
862pub struct CanonicalAttestationData<'a> {
863 pub version: u32,
865 pub rid: &'a str,
867 pub issuer: &'a CanonicalDid,
869 pub subject: &'a CanonicalDid,
871 #[serde(with = "hex::serde")]
873 pub device_public_key: &'a [u8],
874 pub payload: &'a Option<Value>,
876 pub timestamp: &'a Option<DateTime<Utc>>,
878 pub expires_at: &'a Option<DateTime<Utc>>,
880 pub revoked_at: &'a Option<DateTime<Utc>>,
882 pub note: &'a Option<String>,
884
885 #[serde(skip_serializing_if = "Option::is_none")]
887 pub role: Option<&'a str>,
888 #[serde(skip_serializing_if = "Option::is_none")]
890 pub capabilities: Option<&'a Vec<Capability>>,
891 #[serde(skip_serializing_if = "Option::is_none")]
893 pub delegated_by: Option<&'a CanonicalDid>,
894 #[serde(skip_serializing_if = "Option::is_none")]
896 pub signer_type: Option<&'a SignerType>,
897}
898
899pub fn canonicalize_attestation_data(
904 data: &CanonicalAttestationData,
905) -> Result<Vec<u8>, AttestationError> {
906 let canonical_json_string = json_canon::to_string(data).map_err(|e| {
907 AttestationError::SerializationError(format!("Failed to create canonical JSON: {}", e))
908 })?;
909 debug!(
910 "Generated canonical data (standard): {}",
911 canonical_json_string
912 );
913 Ok(canonical_json_string.into_bytes())
914}
915
916impl Attestation {
917 pub fn is_revoked(&self) -> bool {
919 self.revoked_at.is_some()
920 }
921
922 pub fn from_json(json_bytes: &[u8]) -> Result<Self, AttestationError> {
926 if json_bytes.len() > MAX_ATTESTATION_JSON_SIZE {
927 return Err(AttestationError::InputTooLarge(format!(
928 "attestation JSON is {} bytes, max {}",
929 json_bytes.len(),
930 MAX_ATTESTATION_JSON_SIZE
931 )));
932 }
933 serde_json::from_slice(json_bytes)
934 .map_err(|e| AttestationError::SerializationError(e.to_string()))
935 }
936
937 pub fn to_debug_string(&self) -> String {
939 format!(
940 "RID: {}\nIssuer DID: {}\nSubject DID: {}\nDevice PK: {}\nIdentity Sig: {}\nDevice Sig: {}\nRevoked At: {:?}\nExpires: {:?}\nNote: {:?}",
941 self.rid,
942 self.issuer,
943 self.subject, hex::encode(self.device_public_key.as_bytes()),
945 hex::encode(self.identity_signature.as_bytes()),
946 hex::encode(self.device_signature.as_bytes()),
947 self.revoked_at,
948 self.expires_at,
949 self.note
950 )
951 }
952}
953
954#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1021#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1022pub struct ThresholdPolicy {
1023 pub threshold: u8,
1025
1026 pub signers: Vec<String>,
1028
1029 pub policy_id: PolicyId,
1031
1032 #[serde(default, skip_serializing_if = "Option::is_none")]
1034 pub scope: Option<Capability>,
1035
1036 #[serde(default, skip_serializing_if = "Option::is_none")]
1038 pub ceremony_endpoint: Option<String>,
1039}
1040
1041impl ThresholdPolicy {
1042 pub fn new(threshold: u8, signers: Vec<String>, policy_id: impl Into<PolicyId>) -> Self {
1044 Self {
1045 threshold,
1046 signers,
1047 policy_id: policy_id.into(),
1048 scope: None,
1049 ceremony_endpoint: None,
1050 }
1051 }
1052
1053 pub fn is_valid(&self) -> bool {
1055 if self.threshold < 1 {
1057 return false;
1058 }
1059 if self.threshold as usize > self.signers.len() {
1061 return false;
1062 }
1063 if self.signers.is_empty() {
1065 return false;
1066 }
1067 if self.policy_id.is_empty() {
1069 return false;
1070 }
1071 true
1072 }
1073
1074 pub fn m_of_n(&self) -> (u8, usize) {
1076 (self.threshold, self.signers.len())
1077 }
1078}
1079
1080#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1086pub enum CommitOidError {
1087 #[error("commit OID is empty")]
1089 Empty,
1090 #[error("expected 40 or 64 hex chars, got {0}")]
1092 InvalidLength(usize),
1093 #[error("invalid hex character in commit OID")]
1095 InvalidHex,
1096}
1097
1098#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
1102#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1103#[repr(transparent)]
1104#[serde(try_from = "String")]
1105pub struct CommitOid(String);
1106
1107impl CommitOid {
1108 pub fn parse(raw: &str) -> Result<Self, CommitOidError> {
1118 let s = raw.trim().to_lowercase();
1119 if s.is_empty() {
1120 return Err(CommitOidError::Empty);
1121 }
1122 if s.len() != 40 && s.len() != 64 {
1123 return Err(CommitOidError::InvalidLength(s.len()));
1124 }
1125 if !s.chars().all(|c| c.is_ascii_hexdigit()) {
1126 return Err(CommitOidError::InvalidHex);
1127 }
1128 Ok(Self(s))
1129 }
1130
1131 pub fn new_unchecked(s: impl Into<String>) -> Self {
1135 Self(s.into())
1136 }
1137
1138 pub fn as_str(&self) -> &str {
1140 &self.0
1141 }
1142
1143 pub fn into_inner(self) -> String {
1145 self.0
1146 }
1147}
1148
1149impl fmt::Display for CommitOid {
1150 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1151 f.write_str(&self.0)
1152 }
1153}
1154
1155impl AsRef<str> for CommitOid {
1156 fn as_ref(&self) -> &str {
1157 &self.0
1158 }
1159}
1160
1161impl TryFrom<String> for CommitOid {
1162 type Error = CommitOidError;
1163 fn try_from(s: String) -> Result<Self, Self::Error> {
1164 Self::parse(&s)
1165 }
1166}
1167
1168impl TryFrom<&str> for CommitOid {
1169 type Error = CommitOidError;
1170 fn try_from(s: &str) -> Result<Self, Self::Error> {
1171 Self::parse(s)
1172 }
1173}
1174
1175impl FromStr for CommitOid {
1176 type Err = CommitOidError;
1177 fn from_str(s: &str) -> Result<Self, Self::Err> {
1178 Self::parse(s)
1179 }
1180}
1181
1182impl From<CommitOid> for String {
1183 fn from(oid: CommitOid) -> Self {
1184 oid.0
1185 }
1186}
1187
1188impl<'de> Deserialize<'de> for CommitOid {
1189 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
1190 let s = String::deserialize(d)?;
1191 Self::parse(&s).map_err(serde::de::Error::custom)
1192 }
1193}
1194
1195#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1201pub enum PublicKeyHexError {
1202 #[error("expected 64 hex chars (32 bytes), got {0} chars")]
1204 InvalidLength(usize),
1205 #[error("invalid hex: {0}")]
1207 InvalidHex(String),
1208}
1209
1210#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
1214#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1215#[repr(transparent)]
1216#[serde(try_from = "String")]
1217pub struct PublicKeyHex(String);
1218
1219impl PublicKeyHex {
1220 pub fn parse(raw: &str) -> Result<Self, PublicKeyHexError> {
1230 let s = raw.trim().to_lowercase();
1231 let bytes = hex::decode(&s).map_err(|e| PublicKeyHexError::InvalidHex(e.to_string()))?;
1232 if bytes.len() != 32 {
1233 return Err(PublicKeyHexError::InvalidLength(s.len()));
1234 }
1235 Ok(Self(s))
1236 }
1237
1238 pub fn new_unchecked(s: impl Into<String>) -> Self {
1242 Self(s.into())
1243 }
1244
1245 pub fn as_str(&self) -> &str {
1247 &self.0
1248 }
1249
1250 pub fn into_inner(self) -> String {
1252 self.0
1253 }
1254
1255 pub fn to_ed25519(&self) -> Result<Ed25519PublicKey, Ed25519KeyError> {
1263 let bytes = hex::decode(&self.0).map_err(|e| Ed25519KeyError::InvalidHex(e.to_string()))?;
1264 Ed25519PublicKey::try_from_slice(&bytes)
1265 }
1266}
1267
1268impl fmt::Display for PublicKeyHex {
1269 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1270 f.write_str(&self.0)
1271 }
1272}
1273
1274impl AsRef<str> for PublicKeyHex {
1275 fn as_ref(&self) -> &str {
1276 &self.0
1277 }
1278}
1279
1280impl TryFrom<String> for PublicKeyHex {
1281 type Error = PublicKeyHexError;
1282 fn try_from(s: String) -> Result<Self, Self::Error> {
1283 Self::parse(&s)
1284 }
1285}
1286
1287impl TryFrom<&str> for PublicKeyHex {
1288 type Error = PublicKeyHexError;
1289 fn try_from(s: &str) -> Result<Self, Self::Error> {
1290 Self::parse(s)
1291 }
1292}
1293
1294impl FromStr for PublicKeyHex {
1295 type Err = PublicKeyHexError;
1296 fn from_str(s: &str) -> Result<Self, Self::Err> {
1297 Self::parse(s)
1298 }
1299}
1300
1301impl From<PublicKeyHex> for String {
1302 fn from(pk: PublicKeyHex) -> Self {
1303 pk.0
1304 }
1305}
1306
1307impl<'de> Deserialize<'de> for PublicKeyHex {
1308 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
1309 let s = String::deserialize(d)?;
1310 Self::parse(&s).map_err(serde::de::Error::custom)
1311 }
1312}
1313
1314#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1323#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1324#[serde(transparent)]
1325pub struct PolicyId(String);
1326
1327impl PolicyId {
1328 pub fn new(s: impl Into<String>) -> Self {
1330 Self(s.into())
1331 }
1332
1333 pub fn as_str(&self) -> &str {
1335 &self.0
1336 }
1337}
1338
1339impl Deref for PolicyId {
1340 type Target = str;
1341 fn deref(&self) -> &str {
1342 &self.0
1343 }
1344}
1345
1346impl fmt::Display for PolicyId {
1347 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1348 f.write_str(&self.0)
1349 }
1350}
1351
1352impl From<String> for PolicyId {
1353 fn from(s: String) -> Self {
1354 Self(s)
1355 }
1356}
1357
1358impl From<&str> for PolicyId {
1359 fn from(s: &str) -> Self {
1360 Self(s.to_string())
1361 }
1362}
1363
1364impl PartialEq<str> for PolicyId {
1365 fn eq(&self, other: &str) -> bool {
1366 self.0 == other
1367 }
1368}
1369
1370impl PartialEq<&str> for PolicyId {
1371 fn eq(&self, other: &&str) -> bool {
1372 self.0 == *other
1373 }
1374}
1375
1376#[cfg(test)]
1377#[allow(clippy::disallowed_methods)]
1378mod tests {
1379 use super::*;
1380 use crate::AttestationBuilder;
1381
1382 #[test]
1387 fn capability_serializes_to_snake_case() {
1388 assert_eq!(
1389 serde_json::to_string(&Capability::sign_commit()).unwrap(),
1390 r#""sign_commit""#
1391 );
1392 assert_eq!(
1393 serde_json::to_string(&Capability::sign_release()).unwrap(),
1394 r#""sign_release""#
1395 );
1396 assert_eq!(
1397 serde_json::to_string(&Capability::manage_members()).unwrap(),
1398 r#""manage_members""#
1399 );
1400 assert_eq!(
1401 serde_json::to_string(&Capability::rotate_keys()).unwrap(),
1402 r#""rotate_keys""#
1403 );
1404 }
1405
1406 #[test]
1407 fn capability_deserializes_from_snake_case() {
1408 assert_eq!(
1409 serde_json::from_str::<Capability>(r#""sign_commit""#).unwrap(),
1410 Capability::sign_commit()
1411 );
1412 assert_eq!(
1413 serde_json::from_str::<Capability>(r#""sign_release""#).unwrap(),
1414 Capability::sign_release()
1415 );
1416 assert_eq!(
1417 serde_json::from_str::<Capability>(r#""manage_members""#).unwrap(),
1418 Capability::manage_members()
1419 );
1420 assert_eq!(
1421 serde_json::from_str::<Capability>(r#""rotate_keys""#).unwrap(),
1422 Capability::rotate_keys()
1423 );
1424 }
1425
1426 #[test]
1427 fn capability_custom_serializes_as_string() {
1428 let cap = Capability::parse("acme:deploy").unwrap();
1429 assert_eq!(serde_json::to_string(&cap).unwrap(), r#""acme:deploy""#);
1430 }
1431
1432 #[test]
1433 fn capability_custom_deserializes_unknown_strings() {
1434 let cap: Capability = serde_json::from_str(r#""custom-capability""#).unwrap();
1436 assert_eq!(cap, Capability::parse("custom-capability").unwrap());
1437 }
1438
1439 #[test]
1444 fn capability_parse_accepts_valid_strings() {
1445 assert!(Capability::parse("deploy").is_ok());
1446 assert!(Capability::parse("acme:deploy").is_ok());
1447 assert!(Capability::parse("my-custom-cap").is_ok());
1448 assert!(Capability::parse("org:team:action").is_ok());
1449 assert!(Capability::parse("with_underscore").is_ok()); }
1451
1452 #[test]
1453 fn capability_parse_rejects_invalid_strings() {
1454 assert!(matches!(Capability::parse(""), Err(CapabilityError::Empty)));
1456
1457 assert!(matches!(
1459 Capability::parse(&"a".repeat(65)),
1460 Err(CapabilityError::TooLong(65))
1461 ));
1462
1463 assert!(matches!(
1465 Capability::parse("has spaces"),
1466 Err(CapabilityError::InvalidChars(_))
1467 ));
1468 assert!(matches!(
1469 Capability::parse("has.dot"),
1470 Err(CapabilityError::InvalidChars(_))
1471 ));
1472 }
1473
1474 #[test]
1475 fn capability_parse_rejects_reserved_namespace() {
1476 assert!(matches!(
1477 Capability::parse("auths:custom"),
1478 Err(CapabilityError::ReservedNamespace)
1479 ));
1480 assert!(matches!(
1481 Capability::parse("auths:sign_commit"),
1482 Err(CapabilityError::ReservedNamespace)
1483 ));
1484 }
1485
1486 #[test]
1487 fn capability_parse_normalizes_to_lowercase() {
1488 let cap = Capability::parse("DEPLOY").unwrap();
1489 assert_eq!(cap.as_str(), "deploy");
1490
1491 let cap = Capability::parse("ACME:Deploy").unwrap();
1492 assert_eq!(cap.as_str(), "acme:deploy");
1493 }
1494
1495 #[test]
1496 fn capability_parse_trims_whitespace() {
1497 let cap = Capability::parse(" deploy ").unwrap();
1498 assert_eq!(cap.as_str(), "deploy");
1499 }
1500
1501 #[test]
1506 fn capability_is_hashable() {
1507 use std::collections::HashSet;
1508 let mut set = HashSet::new();
1509 set.insert(Capability::sign_commit());
1510 set.insert(Capability::sign_release());
1511 set.insert(Capability::parse("test").unwrap());
1512 assert_eq!(set.len(), 3);
1513 assert!(set.contains(&Capability::sign_commit()));
1514 }
1515
1516 #[test]
1517 fn capability_equality_with_different_construction_paths() {
1518 let from_constructor = Capability::sign_commit();
1520 let from_deser: Capability = serde_json::from_str(r#""sign_commit""#).unwrap();
1521 assert_eq!(from_constructor, from_deser);
1522
1523 let from_parse = Capability::parse("acme:deploy").unwrap();
1525 let from_deser: Capability = serde_json::from_str(r#""acme:deploy""#).unwrap();
1526 assert_eq!(from_parse, from_deser);
1527 }
1528
1529 #[test]
1534 fn capability_display_matches_canonical_form() {
1535 assert_eq!(Capability::sign_commit().to_string(), "sign_commit");
1536 assert_eq!(Capability::sign_release().to_string(), "sign_release");
1537 assert_eq!(Capability::manage_members().to_string(), "manage_members");
1538 assert_eq!(Capability::rotate_keys().to_string(), "rotate_keys");
1539 assert_eq!(
1540 Capability::parse("acme:deploy").unwrap().to_string(),
1541 "acme:deploy"
1542 );
1543 }
1544
1545 #[test]
1546 fn capability_as_str_returns_canonical_form() {
1547 assert_eq!(Capability::sign_commit().as_str(), "sign_commit");
1548 assert_eq!(Capability::sign_release().as_str(), "sign_release");
1549 assert_eq!(Capability::manage_members().as_str(), "manage_members");
1550 assert_eq!(Capability::rotate_keys().as_str(), "rotate_keys");
1551 assert_eq!(
1552 Capability::parse("acme:deploy").unwrap().as_str(),
1553 "acme:deploy"
1554 );
1555 }
1556
1557 #[test]
1558 fn capability_is_well_known() {
1559 assert!(Capability::sign_commit().is_well_known());
1560 assert!(Capability::sign_release().is_well_known());
1561 assert!(Capability::manage_members().is_well_known());
1562 assert!(Capability::rotate_keys().is_well_known());
1563 assert!(!Capability::parse("custom").unwrap().is_well_known());
1564 }
1565
1566 #[test]
1567 fn capability_namespace() {
1568 assert_eq!(
1569 Capability::parse("acme:deploy").unwrap().namespace(),
1570 Some("acme")
1571 );
1572 assert_eq!(
1573 Capability::parse("org:team:action").unwrap().namespace(),
1574 Some("org")
1575 );
1576 assert_eq!(Capability::parse("deploy").unwrap().namespace(), None);
1577 }
1578
1579 #[test]
1584 fn capability_vec_serializes_as_array() {
1585 let caps = vec![Capability::sign_commit(), Capability::sign_release()];
1586 let json = serde_json::to_string(&caps).unwrap();
1587 assert_eq!(json, r#"["sign_commit","sign_release"]"#);
1588 }
1589
1590 #[test]
1591 fn capability_vec_deserializes_from_array() {
1592 let json = r#"["sign_commit","manage_members","custom-cap"]"#;
1593 let caps: Vec<Capability> = serde_json::from_str(json).unwrap();
1594 assert_eq!(caps.len(), 3);
1595 assert_eq!(caps[0], Capability::sign_commit());
1596 assert_eq!(caps[1], Capability::manage_members());
1597 assert_eq!(caps[2], Capability::parse("custom-cap").unwrap());
1598 }
1599
1600 #[test]
1605 fn capability_serde_roundtrip_well_known() {
1606 let caps = vec![
1607 Capability::sign_commit(),
1608 Capability::sign_release(),
1609 Capability::manage_members(),
1610 Capability::rotate_keys(),
1611 ];
1612 for cap in caps {
1613 let json = serde_json::to_string(&cap).unwrap();
1614 let roundtrip: Capability = serde_json::from_str(&json).unwrap();
1615 assert_eq!(cap, roundtrip);
1616 }
1617 }
1618
1619 #[test]
1620 fn capability_serde_roundtrip_custom() {
1621 let caps = vec![
1622 Capability::parse("deploy").unwrap(),
1623 Capability::parse("acme:deploy").unwrap(),
1624 Capability::parse("org:team:action").unwrap(),
1625 ];
1626 for cap in caps {
1627 let json = serde_json::to_string(&cap).unwrap();
1628 let roundtrip: Capability = serde_json::from_str(&json).unwrap();
1629 assert_eq!(cap, roundtrip);
1630 }
1631 }
1632
1633 #[test]
1636 fn attestation_old_json_without_org_fields_deserializes() {
1637 let old_json = r#"{
1639 "version": 1,
1640 "rid": "test-rid",
1641 "issuer": "did:keri:Eissuer",
1642 "subject": "did:key:zSubject",
1643 "device_public_key": "0102030405060708091011121314151617181920212223242526272829303132",
1644 "identity_signature": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
1645 "device_signature": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
1646 "revoked_at": null,
1647 "timestamp": null
1648 }"#;
1649
1650 let att: Attestation = serde_json::from_str(old_json).unwrap();
1651
1652 assert_eq!(att.role, None);
1654 assert!(att.capabilities.is_empty());
1655 assert_eq!(att.delegated_by, None);
1656 }
1657
1658 #[test]
1659 fn attestation_with_org_fields_serializes_correctly() {
1660 let att = AttestationBuilder::default()
1661 .rid("test-rid")
1662 .issuer("did:keri:Eissuer")
1663 .subject("did:key:zSubject")
1664 .role(Some(Role::Admin))
1665 .capabilities(vec![
1666 Capability::sign_commit(),
1667 Capability::manage_members(),
1668 ])
1669 .delegated_by(Some(CanonicalDid::new_unchecked("did:keri:Edelegator")))
1670 .build();
1671
1672 let json = serde_json::to_string(&att).unwrap();
1673 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1674
1675 assert_eq!(parsed["role"], "admin");
1676 assert_eq!(parsed["capabilities"][0], "sign_commit");
1677 assert_eq!(parsed["capabilities"][1], "manage_members");
1678 assert_eq!(parsed["delegated_by"], "did:keri:Edelegator");
1679 }
1680
1681 #[test]
1682 fn attestation_without_org_fields_omits_them_in_json() {
1683 let att = AttestationBuilder::default()
1684 .rid("test-rid")
1685 .issuer("did:keri:Eissuer")
1686 .subject("did:key:zSubject")
1687 .build();
1688
1689 let json = serde_json::to_string(&att).unwrap();
1690 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1691
1692 assert!(parsed.get("role").is_none());
1694 assert!(parsed.get("capabilities").is_none());
1695 assert!(parsed.get("delegated_by").is_none());
1696 }
1697
1698 #[test]
1699 fn attestation_with_org_fields_roundtrips() {
1700 let original = AttestationBuilder::default()
1701 .rid("test-rid")
1702 .issuer("did:keri:Eissuer")
1703 .subject("did:key:zSubject")
1704 .role(Some(Role::Member))
1705 .capabilities(vec![Capability::sign_commit(), Capability::sign_release()])
1706 .delegated_by(Some(CanonicalDid::new_unchecked("did:keri:Eadmin")))
1707 .build();
1708
1709 let json = serde_json::to_string(&original).unwrap();
1710 let deserialized: Attestation = serde_json::from_str(&json).unwrap();
1711
1712 assert_eq!(original.role, deserialized.role);
1713 assert_eq!(original.capabilities, deserialized.capabilities);
1714 assert_eq!(original.delegated_by, deserialized.delegated_by);
1715 }
1716
1717 #[test]
1720 fn threshold_policy_new_creates_valid_policy() {
1721 let policy = ThresholdPolicy::new(
1722 2,
1723 vec![
1724 "did:key:alice".to_string(),
1725 "did:key:bob".to_string(),
1726 "did:key:carol".to_string(),
1727 ],
1728 "test-policy".to_string(),
1729 );
1730
1731 assert_eq!(policy.threshold, 2);
1732 assert_eq!(policy.signers.len(), 3);
1733 assert_eq!(policy.policy_id, "test-policy");
1734 assert!(policy.scope.is_none());
1735 assert!(policy.ceremony_endpoint.is_none());
1736 }
1737
1738 #[test]
1739 fn threshold_policy_is_valid_checks_constraints() {
1740 let valid = ThresholdPolicy::new(
1742 2,
1743 vec!["a".to_string(), "b".to_string(), "c".to_string()],
1744 "policy".to_string(),
1745 );
1746 assert!(valid.is_valid());
1747
1748 let zero_threshold = ThresholdPolicy::new(0, vec!["a".to_string()], "policy".to_string());
1750 assert!(!zero_threshold.is_valid());
1751
1752 let too_high = ThresholdPolicy::new(
1754 3,
1755 vec!["a".to_string(), "b".to_string()],
1756 "policy".to_string(),
1757 );
1758 assert!(!too_high.is_valid());
1759
1760 let no_signers = ThresholdPolicy::new(1, vec![], "policy".to_string());
1762 assert!(!no_signers.is_valid());
1763
1764 let no_id = ThresholdPolicy::new(1, vec!["a".to_string()], "".to_string());
1766 assert!(!no_id.is_valid());
1767 }
1768
1769 #[test]
1770 fn threshold_policy_m_of_n_returns_correct_values() {
1771 let policy = ThresholdPolicy::new(
1772 2,
1773 vec!["a".to_string(), "b".to_string(), "c".to_string()],
1774 "policy".to_string(),
1775 );
1776 let (m, n) = policy.m_of_n();
1777 assert_eq!(m, 2);
1778 assert_eq!(n, 3);
1779 }
1780
1781 #[test]
1782 fn threshold_policy_serializes_correctly() {
1783 let mut policy = ThresholdPolicy::new(
1784 2,
1785 vec!["did:key:alice".to_string(), "did:key:bob".to_string()],
1786 "release-policy".to_string(),
1787 );
1788 policy.scope = Some(Capability::sign_release());
1789 policy.ceremony_endpoint = Some("wss://example.com/ceremony".to_string());
1790
1791 let json = serde_json::to_string(&policy).unwrap();
1792 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1793
1794 assert_eq!(parsed["threshold"], 2);
1795 assert_eq!(parsed["signers"][0], "did:key:alice");
1796 assert_eq!(parsed["policy_id"], "release-policy");
1797 assert_eq!(parsed["scope"], "sign_release");
1798 assert_eq!(parsed["ceremony_endpoint"], "wss://example.com/ceremony");
1799 }
1800
1801 #[test]
1802 fn threshold_policy_without_optional_fields_omits_them() {
1803 let policy =
1804 ThresholdPolicy::new(1, vec!["did:key:alice".to_string()], "policy".to_string());
1805
1806 let json = serde_json::to_string(&policy).unwrap();
1807 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1808
1809 assert!(parsed.get("scope").is_none());
1810 assert!(parsed.get("ceremony_endpoint").is_none());
1811 }
1812
1813 #[test]
1814 fn threshold_policy_roundtrips() {
1815 let mut original = ThresholdPolicy::new(
1816 3,
1817 vec![
1818 "a".to_string(),
1819 "b".to_string(),
1820 "c".to_string(),
1821 "d".to_string(),
1822 ],
1823 "important-policy".to_string(),
1824 );
1825 original.scope = Some(Capability::rotate_keys());
1826
1827 let json = serde_json::to_string(&original).unwrap();
1828 let deserialized: ThresholdPolicy = serde_json::from_str(&json).unwrap();
1829
1830 assert_eq!(original, deserialized);
1831 }
1832
1833 #[test]
1836 fn identity_bundle_serializes_correctly() {
1837 let bundle = IdentityBundle {
1838 identity_did: IdentityDID::new_unchecked("did:keri:test123"),
1839 public_key_hex: PublicKeyHex::new_unchecked("aabbccdd"),
1840 attestation_chain: vec![],
1841 bundle_timestamp: DateTime::parse_from_rfc3339("2099-01-01T00:00:00Z")
1842 .unwrap()
1843 .with_timezone(&Utc),
1844 max_valid_for_secs: 86400,
1845 };
1846
1847 let json = serde_json::to_string(&bundle).unwrap();
1848 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1849
1850 assert_eq!(parsed["identity_did"], "did:keri:test123");
1851 assert_eq!(parsed["public_key_hex"], "aabbccdd");
1852 assert!(parsed["attestation_chain"].as_array().unwrap().is_empty());
1853 }
1854
1855 #[test]
1856 fn identity_bundle_deserializes_correctly() {
1857 let json = r#"{
1858 "identity_did": "did:keri:abc123",
1859 "public_key_hex": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20",
1860 "attestation_chain": [],
1861 "bundle_timestamp": "2099-01-01T00:00:00Z",
1862 "max_valid_for_secs": 86400
1863 }"#;
1864
1865 let bundle: IdentityBundle = serde_json::from_str(json).unwrap();
1866
1867 assert_eq!(bundle.identity_did.as_str(), "did:keri:abc123");
1868 assert_eq!(
1869 bundle.public_key_hex.as_str(),
1870 "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"
1871 );
1872 assert!(bundle.attestation_chain.is_empty());
1873 }
1874
1875 #[test]
1876 fn identity_bundle_roundtrips() {
1877 let attestation = AttestationBuilder::default()
1878 .rid("test-rid")
1879 .issuer("did:keri:Eissuer")
1880 .subject("did:key:zSubject")
1881 .build();
1882
1883 let original = IdentityBundle {
1884 identity_did: IdentityDID::new_unchecked("did:keri:Eexample"),
1885 public_key_hex: PublicKeyHex::new_unchecked(
1886 "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
1887 ),
1888 attestation_chain: vec![attestation],
1889 bundle_timestamp: DateTime::parse_from_rfc3339("2099-01-01T00:00:00Z")
1890 .unwrap()
1891 .with_timezone(&Utc),
1892 max_valid_for_secs: 86400,
1893 };
1894
1895 let json = serde_json::to_string(&original).unwrap();
1896 let deserialized: IdentityBundle = serde_json::from_str(&json).unwrap();
1897
1898 assert_eq!(original.identity_did, deserialized.identity_did);
1899 assert_eq!(original.public_key_hex, deserialized.public_key_hex);
1900 assert_eq!(
1901 original.attestation_chain.len(),
1902 deserialized.attestation_chain.len()
1903 );
1904 }
1905}