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")]
935 pub kel: Vec<serde_json::Value>,
936 pub bundle_timestamp: DateTime<Utc>,
938 pub max_valid_for_secs: u64,
940}
941
942impl IdentityBundle {
943 pub fn check_freshness(&self, now: DateTime<Utc>) -> Result<(), AttestationError> {
953 let age = (now - self.bundle_timestamp).num_seconds().max(0) as u64;
954 if age > self.max_valid_for_secs {
955 return Err(AttestationError::BundleExpired {
956 age_secs: age,
957 max_secs: self.max_valid_for_secs,
958 });
959 }
960 Ok(())
961 }
962}
963
964#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
966#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
967pub struct Attestation {
968 pub version: u32,
970 pub rid: ResourceId,
972 pub issuer: CanonicalDid,
974 pub subject: CanonicalDid,
976 pub device_public_key: DevicePublicKey,
978 #[serde(default, skip_serializing_if = "Ed25519Signature::is_empty")]
980 pub identity_signature: Ed25519Signature,
981 pub device_signature: Ed25519Signature,
983 #[serde(default, skip_serializing_if = "Option::is_none")]
985 pub revoked_at: Option<DateTime<Utc>>,
986 #[serde(skip_serializing_if = "Option::is_none")]
988 pub expires_at: Option<DateTime<Utc>>,
989 pub timestamp: Option<DateTime<Utc>>,
991 #[serde(skip_serializing_if = "Option::is_none")]
993 pub note: Option<String>,
994 #[serde(skip_serializing_if = "Option::is_none")]
996 pub payload: Option<Value>,
997
998 #[serde(skip_serializing_if = "Option::is_none")]
1000 pub commit_sha: Option<String>,
1001
1002 #[serde(skip_serializing_if = "Option::is_none")]
1004 pub commit_message: Option<String>,
1005
1006 #[serde(skip_serializing_if = "Option::is_none")]
1008 pub author: Option<String>,
1009
1010 #[serde(skip_serializing_if = "Option::is_none")]
1012 pub oidc_binding: Option<OidcBinding>,
1013
1014 #[serde(default, skip_serializing_if = "Option::is_none")]
1016 pub delegated_by: Option<CanonicalDid>,
1017
1018 #[serde(default, skip_serializing_if = "Option::is_none")]
1021 pub signer_type: Option<SignerType>,
1022
1023 #[serde(default, skip_serializing_if = "Option::is_none")]
1026 pub environment_claim: Option<Value>,
1027}
1028
1029#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1035#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1036pub struct OidcBinding {
1037 pub issuer: String,
1039 pub subject: String,
1041 pub audience: String,
1043 pub token_exp: i64,
1045 #[serde(default, skip_serializing_if = "Option::is_none")]
1047 pub platform: Option<String>,
1048 #[serde(default, skip_serializing_if = "Option::is_none")]
1050 pub jti: Option<String>,
1051 #[serde(default, skip_serializing_if = "Option::is_none")]
1053 pub normalized_claims: Option<serde_json::Map<String, serde_json::Value>>,
1054}
1055
1056#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1061#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1062#[non_exhaustive]
1063pub enum SignerType {
1064 Human,
1066 Agent,
1068 Workload,
1070}
1071
1072#[derive(Debug, Clone, Serialize)]
1082pub struct VerifiedAttestation(Attestation);
1083
1084impl VerifiedAttestation {
1085 pub fn inner(&self) -> &Attestation {
1087 &self.0
1088 }
1089
1090 pub fn into_inner(self) -> Attestation {
1092 self.0
1093 }
1094
1095 #[doc(hidden)]
1101 pub fn dangerous_from_unchecked(attestation: Attestation) -> Self {
1102 Self(attestation)
1103 }
1104
1105 pub(crate) fn from_verified(attestation: Attestation) -> Self {
1106 Self(attestation)
1107 }
1108}
1109
1110impl std::ops::Deref for VerifiedAttestation {
1111 type Target = Attestation;
1112
1113 fn deref(&self) -> &Attestation {
1114 &self.0
1115 }
1116}
1117
1118#[derive(Serialize, Debug)]
1120pub struct CanonicalAttestationData<'a> {
1121 pub version: u32,
1123 pub rid: &'a str,
1125 pub issuer: &'a CanonicalDid,
1127 pub subject: &'a CanonicalDid,
1129 #[serde(with = "hex::serde")]
1131 pub device_public_key: &'a [u8],
1132 pub payload: &'a Option<Value>,
1134 pub timestamp: &'a Option<DateTime<Utc>>,
1136 pub expires_at: &'a Option<DateTime<Utc>>,
1138 pub revoked_at: &'a Option<DateTime<Utc>>,
1140 pub note: &'a Option<String>,
1142
1143 #[serde(skip_serializing_if = "Option::is_none")]
1145 pub delegated_by: Option<&'a CanonicalDid>,
1146 #[serde(skip_serializing_if = "Option::is_none")]
1148 pub signer_type: Option<&'a SignerType>,
1149 #[serde(skip_serializing_if = "Option::is_none")]
1151 pub commit_sha: Option<&'a str>,
1152}
1153
1154pub fn canonicalize_attestation_data(
1159 data: &CanonicalAttestationData,
1160) -> Result<Vec<u8>, AttestationError> {
1161 let canonical_json_string = json_canon::to_string(data).map_err(|e| {
1162 AttestationError::SerializationError(format!("Failed to create canonical JSON: {}", e))
1163 })?;
1164 debug!(
1165 "Generated canonical data (standard): {}",
1166 canonical_json_string
1167 );
1168 Ok(canonical_json_string.into_bytes())
1169}
1170
1171impl Attestation {
1172 pub fn is_revoked(&self) -> bool {
1174 self.revoked_at.is_some()
1175 }
1176
1177 pub fn from_json(json_bytes: &[u8]) -> Result<Self, AttestationError> {
1181 if json_bytes.len() > MAX_ATTESTATION_JSON_SIZE {
1182 return Err(AttestationError::InputTooLarge(format!(
1183 "attestation JSON is {} bytes, max {}",
1184 json_bytes.len(),
1185 MAX_ATTESTATION_JSON_SIZE
1186 )));
1187 }
1188 serde_json::from_slice(json_bytes)
1189 .map_err(|e| AttestationError::SerializationError(e.to_string()))
1190 }
1191
1192 pub fn canonical_data(&self) -> CanonicalAttestationData<'_> {
1203 CanonicalAttestationData {
1204 version: self.version,
1205 rid: &self.rid,
1206 issuer: &self.issuer,
1207 subject: &self.subject,
1208 device_public_key: self.device_public_key.as_bytes(),
1209 payload: &self.payload,
1210 timestamp: &self.timestamp,
1211 expires_at: &self.expires_at,
1212 revoked_at: &self.revoked_at,
1213 note: &self.note,
1214 delegated_by: self.delegated_by.as_ref(),
1215 signer_type: self.signer_type.as_ref(),
1216 commit_sha: self.commit_sha.as_deref(),
1217 }
1218 }
1219
1220 pub fn to_debug_string(&self) -> String {
1222 format!(
1223 "RID: {}\nIssuer DID: {}\nSubject DID: {}\nDevice PK: {}\nIdentity Sig: {}\nDevice Sig: {}\nRevoked At: {:?}\nExpires: {:?}\nNote: {:?}",
1224 self.rid,
1225 self.issuer,
1226 self.subject, hex::encode(self.device_public_key.as_bytes()),
1228 hex::encode(self.identity_signature.as_bytes()),
1229 hex::encode(self.device_signature.as_bytes()),
1230 self.revoked_at,
1231 self.expires_at,
1232 self.note
1233 )
1234 }
1235}
1236
1237#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1304#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1305pub struct ThresholdPolicy {
1306 pub threshold: u8,
1308
1309 pub signers: Vec<String>,
1311
1312 pub policy_id: PolicyId,
1314
1315 #[serde(default, skip_serializing_if = "Option::is_none")]
1317 pub scope: Option<Capability>,
1318
1319 #[serde(default, skip_serializing_if = "Option::is_none")]
1321 pub ceremony_endpoint: Option<String>,
1322}
1323
1324impl ThresholdPolicy {
1325 pub fn new(threshold: u8, signers: Vec<String>, policy_id: impl Into<PolicyId>) -> Self {
1327 Self {
1328 threshold,
1329 signers,
1330 policy_id: policy_id.into(),
1331 scope: None,
1332 ceremony_endpoint: None,
1333 }
1334 }
1335
1336 pub fn is_valid(&self) -> bool {
1338 if self.threshold < 1 {
1340 return false;
1341 }
1342 if self.threshold as usize > self.signers.len() {
1344 return false;
1345 }
1346 if self.signers.is_empty() {
1348 return false;
1349 }
1350 if self.policy_id.is_empty() {
1352 return false;
1353 }
1354 true
1355 }
1356
1357 pub fn m_of_n(&self) -> (u8, usize) {
1359 (self.threshold, self.signers.len())
1360 }
1361}
1362
1363#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1369pub enum CommitOidError {
1370 #[error("commit OID is empty")]
1372 Empty,
1373 #[error("expected 40 or 64 hex chars, got {0}")]
1375 InvalidLength(usize),
1376 #[error("invalid hex character in commit OID")]
1378 InvalidHex,
1379}
1380
1381#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
1385#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1386#[repr(transparent)]
1387#[serde(try_from = "String")]
1388pub struct CommitOid(String);
1389
1390impl CommitOid {
1391 pub fn parse(raw: &str) -> Result<Self, CommitOidError> {
1401 let s = raw.trim().to_lowercase();
1402 if s.is_empty() {
1403 return Err(CommitOidError::Empty);
1404 }
1405 if s.len() != 40 && s.len() != 64 {
1406 return Err(CommitOidError::InvalidLength(s.len()));
1407 }
1408 if !s.chars().all(|c| c.is_ascii_hexdigit()) {
1409 return Err(CommitOidError::InvalidHex);
1410 }
1411 Ok(Self(s))
1412 }
1413
1414 pub fn new_unchecked(s: impl Into<String>) -> Self {
1418 Self(s.into())
1419 }
1420
1421 pub fn as_str(&self) -> &str {
1423 &self.0
1424 }
1425
1426 pub fn into_inner(self) -> String {
1428 self.0
1429 }
1430}
1431
1432impl fmt::Display for CommitOid {
1433 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1434 f.write_str(&self.0)
1435 }
1436}
1437
1438impl AsRef<str> for CommitOid {
1439 fn as_ref(&self) -> &str {
1440 &self.0
1441 }
1442}
1443
1444impl TryFrom<String> for CommitOid {
1445 type Error = CommitOidError;
1446 fn try_from(s: String) -> Result<Self, Self::Error> {
1447 Self::parse(&s)
1448 }
1449}
1450
1451impl TryFrom<&str> for CommitOid {
1452 type Error = CommitOidError;
1453 fn try_from(s: &str) -> Result<Self, Self::Error> {
1454 Self::parse(s)
1455 }
1456}
1457
1458impl FromStr for CommitOid {
1459 type Err = CommitOidError;
1460 fn from_str(s: &str) -> Result<Self, Self::Err> {
1461 Self::parse(s)
1462 }
1463}
1464
1465impl From<CommitOid> for String {
1466 fn from(oid: CommitOid) -> Self {
1467 oid.0
1468 }
1469}
1470
1471impl<'de> Deserialize<'de> for CommitOid {
1472 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
1473 let s = String::deserialize(d)?;
1474 Self::parse(&s).map_err(serde::de::Error::custom)
1475 }
1476}
1477
1478#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1484pub enum PublicKeyHexError {
1485 #[error("expected 64 (Ed25519) or 66 (P-256) hex chars, got {0} chars")]
1487 InvalidLength(usize),
1488 #[error("invalid hex: {0}")]
1490 InvalidHex(String),
1491}
1492
1493#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
1495#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1496#[repr(transparent)]
1497#[serde(try_from = "String")]
1498pub struct PublicKeyHex(String);
1499
1500impl PublicKeyHex {
1501 pub fn parse(raw: &str) -> Result<Self, PublicKeyHexError> {
1511 let s = raw.trim().to_lowercase();
1512 let bytes = hex::decode(&s).map_err(|e| PublicKeyHexError::InvalidHex(e.to_string()))?;
1513 if bytes.len() != 32 && bytes.len() != 33 {
1514 return Err(PublicKeyHexError::InvalidLength(s.len()));
1515 }
1516 Ok(Self(s))
1517 }
1518
1519 pub fn new_unchecked(s: impl Into<String>) -> Self {
1523 Self(s.into())
1524 }
1525
1526 pub fn as_str(&self) -> &str {
1528 &self.0
1529 }
1530
1531 pub fn into_inner(self) -> String {
1533 self.0
1534 }
1535}
1536
1537impl fmt::Display for PublicKeyHex {
1538 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1539 f.write_str(&self.0)
1540 }
1541}
1542
1543impl AsRef<str> for PublicKeyHex {
1544 fn as_ref(&self) -> &str {
1545 &self.0
1546 }
1547}
1548
1549impl TryFrom<String> for PublicKeyHex {
1550 type Error = PublicKeyHexError;
1551 fn try_from(s: String) -> Result<Self, Self::Error> {
1552 Self::parse(&s)
1553 }
1554}
1555
1556impl TryFrom<&str> for PublicKeyHex {
1557 type Error = PublicKeyHexError;
1558 fn try_from(s: &str) -> Result<Self, Self::Error> {
1559 Self::parse(s)
1560 }
1561}
1562
1563impl FromStr for PublicKeyHex {
1564 type Err = PublicKeyHexError;
1565 fn from_str(s: &str) -> Result<Self, Self::Err> {
1566 Self::parse(s)
1567 }
1568}
1569
1570impl From<PublicKeyHex> for String {
1571 fn from(pk: PublicKeyHex) -> Self {
1572 pk.0
1573 }
1574}
1575
1576impl<'de> Deserialize<'de> for PublicKeyHex {
1577 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
1578 let s = String::deserialize(d)?;
1579 Self::parse(&s).map_err(serde::de::Error::custom)
1580 }
1581}
1582
1583#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1592#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1593#[serde(transparent)]
1594pub struct PolicyId(String);
1595
1596impl PolicyId {
1597 pub fn new(s: impl Into<String>) -> Self {
1599 Self(s.into())
1600 }
1601
1602 pub fn as_str(&self) -> &str {
1604 &self.0
1605 }
1606}
1607
1608impl Deref for PolicyId {
1609 type Target = str;
1610 fn deref(&self) -> &str {
1611 &self.0
1612 }
1613}
1614
1615impl fmt::Display for PolicyId {
1616 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1617 f.write_str(&self.0)
1618 }
1619}
1620
1621impl From<String> for PolicyId {
1622 fn from(s: String) -> Self {
1623 Self(s)
1624 }
1625}
1626
1627impl From<&str> for PolicyId {
1628 fn from(s: &str) -> Self {
1629 Self(s.to_string())
1630 }
1631}
1632
1633impl PartialEq<str> for PolicyId {
1634 fn eq(&self, other: &str) -> bool {
1635 self.0 == other
1636 }
1637}
1638
1639impl PartialEq<&str> for PolicyId {
1640 fn eq(&self, other: &&str) -> bool {
1641 self.0 == *other
1642 }
1643}
1644
1645#[cfg(test)]
1646#[allow(clippy::disallowed_methods)]
1647mod tests {
1648 use super::*;
1649 use crate::AttestationBuilder;
1650
1651 #[test]
1654 fn attestation_old_json_without_delegated_by_deserializes() {
1655 let old_json = r#"{
1657 "version": 1,
1658 "rid": "test-rid",
1659 "issuer": "did:keri:Eissuer",
1660 "subject": "did:key:zSubject",
1661 "device_public_key": "0102030405060708091011121314151617181920212223242526272829303132",
1662 "identity_signature": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
1663 "device_signature": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
1664 "revoked_at": null,
1665 "timestamp": null
1666 }"#;
1667
1668 let att: Attestation = serde_json::from_str(old_json).unwrap();
1669
1670 assert_eq!(att.delegated_by, None);
1671 }
1672
1673 #[test]
1674 fn attestation_delegated_by_serializes_correctly() {
1675 let att = AttestationBuilder::default()
1676 .rid("test-rid")
1677 .issuer("did:keri:Eissuer")
1678 .subject("did:key:zSubject")
1679 .delegated_by(Some(CanonicalDid::new_unchecked("did:keri:Edelegator")))
1680 .build();
1681
1682 let json = serde_json::to_string(&att).unwrap();
1683 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1684
1685 assert_eq!(parsed["delegated_by"], "did:keri:Edelegator");
1686 }
1687
1688 #[test]
1689 fn attestation_omits_delegated_by_when_absent() {
1690 let att = AttestationBuilder::default()
1691 .rid("test-rid")
1692 .issuer("did:keri:Eissuer")
1693 .subject("did:key:zSubject")
1694 .build();
1695
1696 let json = serde_json::to_string(&att).unwrap();
1697 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1698
1699 assert!(parsed.get("delegated_by").is_none());
1700 }
1701
1702 #[test]
1703 fn attestation_delegated_by_roundtrips() {
1704 let original = AttestationBuilder::default()
1705 .rid("test-rid")
1706 .issuer("did:keri:Eissuer")
1707 .subject("did:key:zSubject")
1708 .delegated_by(Some(CanonicalDid::new_unchecked("did:keri:Eadmin")))
1709 .build();
1710
1711 let json = serde_json::to_string(&original).unwrap();
1712 let deserialized: Attestation = serde_json::from_str(&json).unwrap();
1713
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 curve: Default::default(),
1841 attestation_chain: vec![],
1842 kel: vec![],
1843 bundle_timestamp: DateTime::parse_from_rfc3339("2099-01-01T00:00:00Z")
1844 .unwrap()
1845 .with_timezone(&Utc),
1846 max_valid_for_secs: 86400,
1847 };
1848
1849 let json = serde_json::to_string(&bundle).unwrap();
1850 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1851
1852 assert_eq!(parsed["identity_did"], "did:keri:test123");
1853 assert_eq!(parsed["public_key_hex"], "aabbccdd");
1854 assert!(parsed["attestation_chain"].as_array().unwrap().is_empty());
1855 }
1856
1857 #[test]
1858 fn identity_bundle_deserializes_correctly() {
1859 let json = r#"{
1860 "identity_did": "did:keri:abc123",
1861 "public_key_hex": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20",
1862 "attestation_chain": [],
1863 "bundle_timestamp": "2099-01-01T00:00:00Z",
1864 "max_valid_for_secs": 86400
1865 }"#;
1866
1867 let bundle: IdentityBundle = serde_json::from_str(json).unwrap();
1868
1869 assert_eq!(bundle.identity_did.as_str(), "did:keri:abc123");
1870 assert_eq!(
1871 bundle.public_key_hex.as_str(),
1872 "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"
1873 );
1874 assert!(bundle.attestation_chain.is_empty());
1875 }
1876
1877 #[test]
1878 fn identity_bundle_roundtrips() {
1879 let attestation = AttestationBuilder::default()
1880 .rid("test-rid")
1881 .issuer("did:keri:Eissuer")
1882 .subject("did:key:zSubject")
1883 .build();
1884
1885 let original = IdentityBundle {
1886 identity_did: IdentityDID::new_unchecked("did:keri:Eexample"),
1887 public_key_hex: PublicKeyHex::new_unchecked(
1888 "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
1889 ),
1890 curve: Default::default(),
1891 attestation_chain: vec![attestation],
1892 kel: vec![],
1893 bundle_timestamp: DateTime::parse_from_rfc3339("2099-01-01T00:00:00Z")
1894 .unwrap()
1895 .with_timezone(&Utc),
1896 max_valid_for_secs: 86400,
1897 };
1898
1899 let json = serde_json::to_string(&original).unwrap();
1900 let deserialized: IdentityBundle = serde_json::from_str(&json).unwrap();
1901
1902 assert_eq!(original.identity_did, deserialized.identity_did);
1903 assert_eq!(original.public_key_hex, deserialized.public_key_hex);
1904 assert_eq!(
1905 original.attestation_chain.len(),
1906 deserialized.attestation_chain.len()
1907 );
1908 }
1909}
1910
1911#[cfg(test)]
1912mod decode_public_key_tests {
1913 use super::*;
1914
1915 #[test]
1916 fn hex_ed25519_32_bytes() {
1917 let hex = "00".repeat(32);
1918 let pk = decode_public_key_hex(&hex, auths_crypto::CurveType::Ed25519).unwrap();
1919 assert_eq!(pk.curve(), auths_crypto::CurveType::Ed25519);
1920 assert_eq!(pk.len(), 32);
1921 }
1922
1923 #[test]
1924 fn hex_p256_33_bytes_compressed() {
1925 let mut bytes = [0u8; 33];
1927 bytes[0] = 0x02;
1928 let hex = hex::encode(bytes);
1929 let pk = decode_public_key_hex(&hex, auths_crypto::CurveType::P256).unwrap();
1930 assert_eq!(pk.curve(), auths_crypto::CurveType::P256);
1931 assert_eq!(pk.len(), 33);
1932 }
1933
1934 #[test]
1935 fn bytes_p256_65_uncompressed() {
1936 let mut bytes = [0u8; 65];
1937 bytes[0] = 0x04;
1938 let pk = decode_public_key_bytes(&bytes, auths_crypto::CurveType::P256).unwrap();
1939 assert_eq!(pk.curve(), auths_crypto::CurveType::P256);
1940 assert_eq!(pk.len(), 65);
1941 }
1942
1943 #[test]
1944 fn rejects_validation_error() {
1945 let err = decode_public_key_bytes(&[0u8; 50], auths_crypto::CurveType::P256).unwrap_err();
1946 assert!(matches!(err, PublicKeyDecodeError::Validation(_)));
1947 }
1948
1949 #[test]
1950 fn rejects_malformed_hex() {
1951 let err = decode_public_key_hex("zz", auths_crypto::CurveType::P256).unwrap_err();
1952 assert!(matches!(err, PublicKeyDecodeError::InvalidHex(_)));
1953 }
1954}