1use async_trait::async_trait;
7use k256::ecdsa::signature::hazmat::PrehashVerifier;
8use serde::{Deserialize, Serialize};
9use std::fmt;
10use std::sync::Arc;
11use thiserror::Error;
12use url::Url;
13
14use crate::common::APP_USER_AGENT;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum DidMethod {
19 Plc,
21 Web,
23 Other,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct Did(pub String);
30
31impl Did {
32 pub fn method(&self) -> DidMethod {
34 if self.0.starts_with("did:plc:") {
35 DidMethod::Plc
36 } else if self.0.starts_with("did:web:") {
37 DidMethod::Web
38 } else {
39 DidMethod::Other
40 }
41 }
42}
43
44impl fmt::Display for Did {
45 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46 write!(f, "{}", self.0)
47 }
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
52pub struct VerificationMethod {
53 pub id: String,
55 #[serde(rename = "type")]
57 pub type_: String,
58 pub controller: String,
60 #[serde(rename = "publicKeyMultibase", skip_serializing_if = "Option::is_none")]
62 pub public_key_multibase: Option<String>,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67pub struct Service {
68 pub id: String,
70 #[serde(rename = "type")]
72 pub type_: String,
73 #[serde(rename = "serviceEndpoint")]
75 pub service_endpoint: String,
76}
77
78#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
83pub struct DidDocument {
84 pub id: String,
86 #[serde(rename = "alsoKnownAs", skip_serializing_if = "Option::is_none")]
88 pub also_known_as: Option<Vec<String>>,
89 #[serde(rename = "verificationMethod", skip_serializing_if = "Option::is_none")]
91 pub verification_method: Option<Vec<VerificationMethod>>,
92 #[serde(skip_serializing_if = "Option::is_none")]
94 pub service: Option<Vec<Service>>,
95}
96
97#[derive(Debug, Clone)]
101pub struct RawDidDocument {
102 pub parsed: DidDocument,
104 pub source_bytes: Arc<[u8]>,
106 pub source_name: String,
108}
109
110#[derive(Debug, Clone)]
112pub enum AnyVerifyingKey {
113 K256(k256::ecdsa::VerifyingKey),
115 P256(p256::ecdsa::VerifyingKey),
117}
118
119impl AnyVerifyingKey {
120 pub fn curve_name(&self) -> &'static str {
122 match self {
123 AnyVerifyingKey::K256(_) => "secp256k1",
124 AnyVerifyingKey::P256(_) => "P-256",
125 }
126 }
127
128 pub fn verify_prehash(
132 &self,
133 prehash: &[u8; 32],
134 sig: &AnySignature,
135 ) -> Result<(), AnySignatureError> {
136 match (self, sig) {
137 (AnyVerifyingKey::K256(key), AnySignature::K256(sig)) => key
138 .verify_prehash(prehash, sig)
139 .map_err(AnySignatureError::K256),
140 (AnyVerifyingKey::P256(key), AnySignature::P256(sig)) => key
141 .verify_prehash(prehash, sig)
142 .map_err(AnySignatureError::P256),
143 _ => Err(AnySignatureError::CurveMismatch),
144 }
145 }
146}
147
148#[derive(Debug, Clone)]
154pub enum AnySigningKey {
155 K256(k256::ecdsa::SigningKey),
157 P256(p256::ecdsa::SigningKey),
159}
160
161impl AnySigningKey {
162 pub fn verifying_key(&self) -> AnyVerifyingKey {
164 match self {
165 AnySigningKey::K256(k) => AnyVerifyingKey::K256(*k.verifying_key()),
166 AnySigningKey::P256(k) => AnyVerifyingKey::P256(*k.verifying_key()),
167 }
168 }
169
170 pub fn jwt_alg(&self) -> &'static str {
173 match self {
174 AnySigningKey::K256(_) => "ES256K",
175 AnySigningKey::P256(_) => "ES256",
176 }
177 }
178
179 pub fn sign(&self, msg: &[u8]) -> AnySignature {
186 use sha2::{Digest, Sha256};
187 let prehash: [u8; 32] = Sha256::digest(msg).into();
188 self.sign_prehash(&prehash)
189 }
190
191 pub fn sign_prehash(&self, prehash: &[u8; 32]) -> AnySignature {
193 use k256::ecdsa::signature::hazmat::PrehashSigner as K256PrehashSigner;
194 use p256::ecdsa::signature::hazmat::PrehashSigner as P256PrehashSigner;
195 match self {
196 AnySigningKey::K256(k) => {
197 let sig: k256::ecdsa::Signature = K256PrehashSigner::sign_prehash(k, prehash)
201 .expect("SHA-256 output is always 32 bytes");
202 AnySignature::K256(sig)
203 }
204 AnySigningKey::P256(k) => {
205 let sig: p256::ecdsa::Signature = P256PrehashSigner::sign_prehash(k, prehash)
208 .expect("SHA-256 output is always 32 bytes");
209 let normalized = sig.normalize_s().unwrap_or(sig);
210 AnySignature::P256(normalized)
211 }
212 }
213 }
214}
215
216#[derive(Debug, Clone)]
218pub enum AnySignature {
219 K256(k256::ecdsa::Signature),
221 P256(p256::ecdsa::Signature),
223}
224
225impl AnySignature {
226 pub fn to_jws_bytes(&self) -> [u8; 64] {
231 match self {
232 AnySignature::K256(s) => s.to_bytes().into(),
233 AnySignature::P256(s) => s.to_bytes().into(),
234 }
235 }
236}
237
238#[derive(Debug, thiserror::Error)]
240pub enum AnySignatureError {
241 #[error("secp256k1 signature verification failed")]
243 K256(#[source] k256::ecdsa::Error),
244 #[error("P-256 signature verification failed")]
246 P256(#[source] p256::ecdsa::Error),
247 #[error("Signature and key use mismatched curves")]
249 CurveMismatch,
250}
251
252#[derive(Debug, Clone)]
254pub struct ParsedMultikey {
255 pub verifying_key: AnyVerifyingKey,
257}
258
259#[derive(Debug, Error)]
261pub enum IdentityError {
262 #[error("Invalid handle format")]
264 InvalidHandle,
265
266 #[error("Handle could not be resolved")]
268 HandleUnresolvable {
269 dns_error: Option<Box<IdentityError>>,
271 http_error: Option<Box<IdentityError>>,
273 },
274
275 #[error("DNS lookup failed")]
277 DnsLookupFailed {
278 #[source]
280 source: Box<IdentityError>,
281 },
282
283 #[error("DNS backend error")]
285 DnsBackend(#[from] hickory_resolver::ResolveError),
286
287 #[error("HTTP fallback for handle resolution failed")]
289 HandleHttpFallbackFailed {
290 #[source]
292 source: Box<IdentityError>,
293 },
294
295 #[error("Unsupported DID method: {method}")]
297 UnsupportedDidMethod { method: String },
298
299 #[error("DID resolution failed with status {status}")]
301 DidResolutionFailed {
302 status: u16,
304 body: String,
306 },
307
308 #[error("DNS record for {handle} has no did= entry")]
310 DnsNoDidRecord {
311 handle: String,
313 },
314
315 #[error("Invalid DID body: {body}")]
317 InvalidDidBody {
318 body: String,
320 },
321
322 #[error("DID document decode failed")]
324 DidDocumentDecodeFailed {
325 source_name: String,
327 source_bytes: Arc<[u8]>,
329 #[source]
331 cause: serde_json::Error,
332 },
333
334 #[error("Multikey decoding failed")]
336 MultikeyDecodeFailed {
337 #[source]
339 source: Box<IdentityError>,
340 },
341
342 #[error("Unsupported multibase encoding")]
344 UnsupportedMultibase(String),
345
346 #[error("Unsupported curve")]
348 UnsupportedCurve { codec_prefix: Vec<u8> },
349
350 #[error("Invalid multikey length")]
352 MultikeyLengthInvalid,
353
354 #[error("HTTP transport error")]
356 HttpTransport(#[from] reqwest::Error),
357}
358
359#[async_trait]
363pub trait HttpClient: Send + Sync {
364 async fn get_bytes(&self, url: &Url) -> Result<(u16, Vec<u8>), IdentityError>;
366}
367
368#[async_trait]
372pub trait DnsResolver: Send + Sync {
373 async fn txt_lookup(&self, name: &str) -> Result<Vec<String>, IdentityError>;
375}
376
377pub struct RealHttpClient {
379 inner: reqwest::Client,
380}
381
382impl RealHttpClient {
383 pub fn new() -> Result<Self, IdentityError> {
388 let client = reqwest::Client::builder()
389 .use_rustls_tls()
390 .user_agent(APP_USER_AGENT)
391 .timeout(std::time::Duration::from_secs(10))
392 .build()?;
393 Ok(Self { inner: client })
394 }
395
396 pub fn from_client(client: reqwest::Client) -> Self {
400 Self { inner: client }
401 }
402}
403
404#[async_trait]
405impl HttpClient for RealHttpClient {
406 async fn get_bytes(&self, url: &Url) -> Result<(u16, Vec<u8>), IdentityError> {
407 let response = self.inner.get(url.clone()).send().await?;
408 let status = response.status().as_u16();
409 let bytes = response.bytes().await?;
410 Ok((status, bytes.to_vec()))
411 }
412}
413
414pub struct RealDnsResolver {
416 inner: hickory_resolver::TokioResolver,
417}
418
419impl RealDnsResolver {
420 pub fn new() -> Self {
422 let resolver = hickory_resolver::Resolver::builder_tokio()
423 .expect("failed to build DNS resolver")
424 .build();
425 Self { inner: resolver }
426 }
427}
428
429impl Default for RealDnsResolver {
430 fn default() -> Self {
431 Self::new()
432 }
433}
434
435#[async_trait]
436impl DnsResolver for RealDnsResolver {
437 async fn txt_lookup(&self, name: &str) -> Result<Vec<String>, IdentityError> {
438 let lookup = self.inner.txt_lookup(name).await?;
439 lookup
440 .iter()
441 .map(|record| {
442 let text = record
443 .iter()
444 .map(|data| {
445 String::from_utf8(data.to_vec()).unwrap_or_else(|_| {
446 tracing::debug!(
447 target = "atproto_devtool::identity",
448 "dropping non-UTF-8 TXT record data"
449 );
450 String::new()
451 })
452 })
453 .collect::<Vec<_>>()
454 .join("");
455 Ok(text)
456 })
457 .collect()
458 }
459}
460
461pub async fn resolve_handle(
466 handle: &str,
467 http: &dyn HttpClient,
468 dns: &dyn DnsResolver,
469) -> Result<Did, IdentityError> {
470 if handle.is_empty()
472 || handle.starts_with('.')
473 || handle.ends_with('.')
474 || !handle.is_ascii()
475 || !handle.contains('.')
476 {
477 return Err(IdentityError::InvalidHandle);
478 }
479
480 tracing::debug!(
481 target = "atproto_devtool::identity",
482 handle = %handle,
483 "resolving handle"
484 );
485
486 let dns_name = format!("_atproto.{handle}");
488
489 let dns_error_opt = match dns.txt_lookup(&dns_name).await {
490 Ok(records) => {
491 for record in records {
493 let trimmed = record.trim();
494 if let Some(did_str) = trimmed.strip_prefix("did=") {
495 let did = Did(did_str.to_string());
496 tracing::debug!(
497 target = "atproto_devtool::identity",
498 did = %did,
499 "resolved handle via DNS"
500 );
501 return Ok(did);
502 }
503 }
504 Some(Box::new(IdentityError::DnsNoDidRecord {
506 handle: handle.to_string(),
507 }))
508 }
509 Err(e) => Some(Box::new(e)),
510 };
511
512 let url = format!("https://{handle}/.well-known/atproto-did");
514 let url = url
515 .parse::<Url>()
516 .map_err(|_| IdentityError::InvalidHandle)?;
517
518 let http_error_opt = match http.get_bytes(&url).await {
519 Ok((200, bytes)) => {
520 let did_str = String::from_utf8_lossy(&bytes).trim().to_string();
521 if !did_str.is_empty() && did_str.starts_with("did:") {
522 let did = Did(did_str);
523 tracing::debug!(
524 target = "atproto_devtool::identity",
525 did = %did,
526 "resolved handle via HTTPS"
527 );
528 return Ok(did);
529 } else {
530 Some(Box::new(IdentityError::HandleHttpFallbackFailed {
531 source: Box::new(IdentityError::InvalidDidBody { body: did_str }),
532 }))
533 }
534 }
535 Ok((status, bytes)) => Some(Box::new(IdentityError::HandleHttpFallbackFailed {
536 source: Box::new(IdentityError::DidResolutionFailed {
537 status,
538 body: String::from_utf8_lossy(&bytes).to_string(),
539 }),
540 })),
541 Err(e) => Some(Box::new(IdentityError::HandleHttpFallbackFailed {
542 source: Box::new(e),
543 })),
544 };
545
546 Err(IdentityError::HandleUnresolvable {
548 dns_error: dns_error_opt,
549 http_error: http_error_opt,
550 })
551}
552
553pub async fn resolve_did(
557 did: &Did,
558 http: &dyn HttpClient,
559) -> Result<RawDidDocument, IdentityError> {
560 tracing::debug!(
561 target = "atproto_devtool::identity",
562 did = %did,
563 "resolving DID"
564 );
565
566 let (url, source_name) = match did.method() {
567 DidMethod::Plc => {
568 let did_str = &did.0;
571 let url_str = format!("https://plc.directory/{did_str}");
572 let url = url_str
573 .parse::<Url>()
574 .map_err(|_| IdentityError::DidResolutionFailed {
575 status: 400,
576 body: "Invalid DID format".to_string(),
577 })?;
578 (url.clone(), url.to_string())
579 }
580 DidMethod::Web => {
581 let rest = did.0.strip_prefix("did:web:").unwrap_or("");
583 let parts: Vec<&str> = rest.split(':').collect();
584
585 if parts.is_empty() {
586 return Err(IdentityError::DidResolutionFailed {
587 status: 400,
588 body: "Invalid did:web format".to_string(),
589 });
590 }
591
592 let host = parts[0];
593 let path_parts = &parts[1..];
594
595 let url_str = if path_parts.is_empty() {
596 format!("https://{host}/.well-known/did.json")
597 } else {
598 let path = path_parts
599 .iter()
600 .map(|p| percent_decode_str(p).unwrap_or_default())
601 .collect::<Vec<_>>()
602 .join("/");
603 format!("https://{host}/{path}/did.json")
604 };
605
606 let url = url_str
607 .parse::<Url>()
608 .map_err(|_| IdentityError::DidResolutionFailed {
609 status: 400,
610 body: "Invalid URL".to_string(),
611 })?;
612 (url.clone(), url.to_string())
613 }
614 DidMethod::Other => {
615 return Err(IdentityError::UnsupportedDidMethod {
616 method: did.0.clone(),
617 });
618 }
619 };
620
621 let (status, bytes) = http.get_bytes(&url).await?;
622
623 if status != 200 {
624 return Err(IdentityError::DidResolutionFailed {
625 status,
626 body: String::from_utf8_lossy(&bytes).to_string(),
627 });
628 }
629
630 tracing::debug!(
631 target = "atproto_devtool::identity",
632 bytes_len = bytes.len(),
633 "fetched DID document"
634 );
635
636 let parsed = serde_json::from_slice::<DidDocument>(&bytes).map_err(|e| {
637 IdentityError::DidDocumentDecodeFailed {
638 source_name: source_name.clone(),
639 source_bytes: Arc::from(bytes.clone()),
640 cause: e,
641 }
642 })?;
643
644 Ok(RawDidDocument {
645 parsed,
646 source_bytes: Arc::from(bytes),
647 source_name,
648 })
649}
650
651pub fn find_service<'a>(
655 doc: &'a DidDocument,
656 id_fragment: &str,
657 expected_type: &str,
658) -> Option<&'a Service> {
659 let services = doc.service.as_ref()?;
660
661 for service in services {
662 let frag = service.id.rsplit_once('#').map(|(_, f)| f);
666 if frag == Some(id_fragment) && service.type_ == expected_type {
667 return Some(service);
668 }
669 }
670
671 None
672}
673
674pub fn parse_multikey(raw: &str) -> Result<ParsedMultikey, IdentityError> {
681 tracing::debug!(target = "atproto_devtool::identity", "parsing multikey");
682
683 let multibase_str = raw.strip_prefix("did:key:").unwrap_or(raw);
684
685 let (base, bytes) =
686 multibase::decode(multibase_str).map_err(|_| IdentityError::MultikeyDecodeFailed {
687 source: Box::new(IdentityError::UnsupportedMultibase(
688 "failed to decode multibase".to_string(),
689 )),
690 })?;
691
692 if base != multibase::Base::Base58Btc {
694 return Err(IdentityError::UnsupportedMultibase(
695 "multikey must use base58btc encoding".to_string(),
696 ));
697 }
698
699 if bytes.len() < 2 {
700 return Err(IdentityError::MultikeyLengthInvalid);
701 }
702
703 let curve_bytes = [bytes[0], bytes[1]];
705 let rest = &bytes[2..];
706
707 match curve_bytes {
708 [0xe7, 0x01] => {
710 if rest.len() != 33 {
711 return Err(IdentityError::MultikeyLengthInvalid);
712 }
713 let key = k256::ecdsa::VerifyingKey::from_sec1_bytes(rest).map_err(|_| {
714 IdentityError::MultikeyDecodeFailed {
715 source: Box::new(IdentityError::MultikeyLengthInvalid),
716 }
717 })?;
718 tracing::debug!(
719 target = "atproto_devtool::identity",
720 curve = "secp256k1",
721 "parsed multikey"
722 );
723 Ok(ParsedMultikey {
724 verifying_key: AnyVerifyingKey::K256(key),
725 })
726 }
727 [0x80, 0x24] => {
729 if rest.len() != 33 {
730 return Err(IdentityError::MultikeyLengthInvalid);
731 }
732 let key = p256::ecdsa::VerifyingKey::from_sec1_bytes(rest).map_err(|_| {
733 IdentityError::MultikeyDecodeFailed {
734 source: Box::new(IdentityError::MultikeyLengthInvalid),
735 }
736 })?;
737 tracing::debug!(
738 target = "atproto_devtool::identity",
739 curve = "p256",
740 "parsed multikey"
741 );
742 Ok(ParsedMultikey {
743 verifying_key: AnyVerifyingKey::P256(key),
744 })
745 }
746 _ => Err(IdentityError::UnsupportedCurve {
747 codec_prefix: curve_bytes.to_vec(),
748 }),
749 }
750}
751
752pub fn encode_multikey(key: &AnyVerifyingKey) -> String {
758 const SECP256K1_PUB: &[u8] = &[0xe7, 0x01];
760 const P256_PUB: &[u8] = &[0x80, 0x24];
761
762 let (prefix, compressed): (&[u8], Vec<u8>) = match key {
763 AnyVerifyingKey::K256(k) => {
764 let point = k.to_encoded_point(true);
765 (SECP256K1_PUB, point.as_bytes().to_vec())
766 }
767 AnyVerifyingKey::P256(k) => {
768 let point = k.to_encoded_point(true);
769 (P256_PUB, point.as_bytes().to_vec())
770 }
771 };
772
773 let mut buf = Vec::with_capacity(prefix.len() + compressed.len());
774 buf.extend_from_slice(prefix);
775 buf.extend_from_slice(&compressed);
776 multibase::encode(multibase::Base::Base58Btc, &buf)
777}
778
779#[derive(Debug, Clone, PartialEq, Eq)]
781pub struct PlcHistoricKey {
782 pub key_id: String,
784 pub operation_cid: String,
786 pub introduced_at: String,
788 pub nullified: bool,
790}
791
792pub async fn plc_history_for_fragment(
801 did: &Did,
802 fragment: &str,
803 http: &dyn HttpClient,
804) -> Result<Vec<PlcHistoricKey>, IdentityError> {
805 debug_assert!(
806 did.method() == DidMethod::Plc,
807 "plc_history_for_fragment called with non-plc DID: {did}"
808 );
809
810 if did.method() != DidMethod::Plc {
811 return Err(IdentityError::UnsupportedDidMethod {
812 method: format!("{:?}", did.method()),
813 });
814 }
815
816 let audit_url = format!("https://plc.directory/{did}/log/audit");
818 let url = Url::parse(&audit_url).map_err(|_| IdentityError::DidResolutionFailed {
819 status: 400,
820 body: "Invalid PLC audit URL".to_string(),
821 })?;
822
823 let (status, bytes) = http.get_bytes(&url).await?;
824
825 if status != 200 {
826 return Err(IdentityError::DidResolutionFailed {
827 status,
828 body: format!("PLC audit log fetch returned status {status}"),
829 });
830 }
831
832 let operations: Vec<serde_json::Value> =
834 serde_json::from_slice(&bytes).map_err(|cause| IdentityError::DidDocumentDecodeFailed {
835 source_name: "plc audit log".to_string(),
836 source_bytes: Arc::from(bytes.into_boxed_slice()),
837 cause,
838 })?;
839
840 let mut historic_keys: Vec<PlcHistoricKey> = Vec::new();
841
842 for op in operations {
847 let vm = match op
849 .get("operation")
850 .and_then(|o| o.get("verificationMethods"))
851 {
852 Some(vm) => vm,
853 None => continue,
854 };
855
856 if let Some(multikey_value) = vm.get(fragment) {
858 let multikey_str = match multikey_value.as_str() {
859 Some(s) => s.to_string(),
860 None => continue,
861 };
862
863 let operation_cid = op
864 .get("cid")
865 .and_then(|c| c.as_str())
866 .unwrap_or("unknown")
867 .to_string();
868
869 let introduced_at = op
871 .get("operation")
872 .and_then(|o| o.get("createdAt"))
873 .and_then(|c| c.as_str())
874 .unwrap_or("unknown")
875 .to_string();
876
877 let nullified = op
878 .get("nullified")
879 .and_then(|n| n.as_bool())
880 .unwrap_or(false);
881
882 if historic_keys.iter().any(|k| k.key_id == multikey_str) {
883 continue;
884 }
885
886 historic_keys.push(PlcHistoricKey {
887 key_id: multikey_str,
888 operation_cid,
889 introduced_at,
890 nullified,
891 });
892 }
893 }
894
895 Ok(historic_keys)
896}
897
898fn percent_decode_str(s: &str) -> Result<String, IdentityError> {
900 let decoded = percent_encoding::percent_decode_str(s)
901 .decode_utf8()
902 .map_err(|_| IdentityError::DidResolutionFailed {
903 status: 400,
904 body: "Invalid UTF-8 in percent-encoded path".to_string(),
905 })?;
906 Ok(decoded.to_string())
907}
908
909pub fn is_local_labeler_hostname(url: &Url) -> bool {
922 use url::Host;
923
924 let host = match url.host() {
925 Some(h) => h,
926 None => return false,
927 };
928
929 match host {
930 Host::Ipv4(addr) => addr.is_loopback() || addr.is_private(),
931 Host::Ipv6(addr) => addr.is_loopback(),
932 Host::Domain(domain) => {
933 let lower = domain.to_ascii_lowercase();
934 if lower == "localhost" {
935 return true;
936 }
937 if lower.ends_with(".local") {
938 return true;
939 }
940 false
941 }
942 }
943}
944
945#[cfg(test)]
946mod tests {
947 use super::*;
948 use k256::ecdsa::SigningKey as K256SigningKey;
949 use k256::ecdsa::signature::hazmat::PrehashSigner;
950 use p256::ecdsa::SigningKey as P256SigningKey;
951 use sha2::Digest;
952 use std::collections::HashMap;
953
954 #[derive(Clone)]
956 enum Response {
957 Http(u16, Vec<u8>),
959 Transport(String),
961 }
962
963 struct FakeHttpClient {
965 responses: HashMap<String, Response>,
966 }
967
968 #[async_trait]
969 impl HttpClient for FakeHttpClient {
970 async fn get_bytes(&self, url: &Url) -> Result<(u16, Vec<u8>), IdentityError> {
971 match self.responses.get(url.as_str()).cloned() {
972 Some(Response::Http(status, body)) => Ok((status, body)),
973 Some(Response::Transport(message)) => {
974 Err(IdentityError::DidResolutionFailed {
978 status: 0,
979 body: format!("Transport error: {message}"),
980 })
981 }
982 None => Err(IdentityError::DidResolutionFailed {
983 status: 404,
984 body: "Not found".to_string(),
985 }),
986 }
987 }
988 }
989
990 struct FakeDnsResolver {
992 records: HashMap<String, Vec<String>>,
993 }
994
995 #[async_trait]
996 impl DnsResolver for FakeDnsResolver {
997 async fn txt_lookup(&self, name: &str) -> Result<Vec<String>, IdentityError> {
998 self.records
999 .get(name)
1000 .cloned()
1001 .ok_or_else(|| IdentityError::DnsLookupFailed {
1002 source: Box::new(IdentityError::InvalidHandle),
1003 })
1004 }
1005 }
1006
1007 #[tokio::test]
1008 async fn resolve_handle_via_dns() {
1009 let mut records = HashMap::new();
1010 records.insert(
1011 "_atproto.alice.example".to_string(),
1012 vec!["did=did:plc:abc123".to_string()],
1013 );
1014 let dns = FakeDnsResolver { records };
1015 let http = FakeHttpClient {
1016 responses: HashMap::new(),
1017 };
1018
1019 let result = resolve_handle("alice.example", &http, &dns).await;
1020
1021 assert!(result.is_ok());
1022 let did = result.unwrap();
1023 assert_eq!(did.0, "did:plc:abc123");
1024 }
1025
1026 #[tokio::test]
1027 async fn resolve_handle_via_https_fallback() {
1028 let dns = FakeDnsResolver {
1029 records: HashMap::new(),
1030 };
1031 let mut responses = HashMap::new();
1032 responses.insert(
1033 "https://alice.example/.well-known/atproto-did".to_string(),
1034 Response::Http(200, b"did:plc:abc123\n".to_vec()),
1035 );
1036 let http = FakeHttpClient { responses };
1037
1038 let result = resolve_handle("alice.example", &http, &dns).await;
1039
1040 assert!(result.is_ok());
1041 let did = result.unwrap();
1042 assert_eq!(did.0, "did:plc:abc123");
1043 }
1044
1045 #[tokio::test]
1046 async fn resolve_handle_both_paths_fail() {
1047 let dns = FakeDnsResolver {
1048 records: HashMap::new(),
1049 };
1050 let http = FakeHttpClient {
1051 responses: HashMap::new(),
1052 };
1053
1054 let result = resolve_handle("alice.example", &http, &dns).await;
1055
1056 assert!(result.is_err());
1057 match result.unwrap_err() {
1058 IdentityError::HandleUnresolvable {
1059 dns_error,
1060 http_error,
1061 } => {
1062 assert!(dns_error.is_some());
1063 assert!(http_error.is_some());
1064 }
1065 _ => panic!("Expected HandleUnresolvable error"),
1066 }
1067 }
1068
1069 #[tokio::test]
1070 async fn resolve_did_plc_success() {
1071 let plc_doc = include_bytes!("../../tests/fixtures/identity/plc_bsky_labeler.json");
1072 let mut responses = HashMap::new();
1073 responses.insert(
1074 "https://plc.directory/did:plc:test-labeler".to_string(),
1075 Response::Http(200, plc_doc.to_vec()),
1076 );
1077 let http = FakeHttpClient { responses };
1078
1079 let did = Did("did:plc:test-labeler".to_string());
1080 let raw_doc = resolve_did(&did, &http).await.expect("resolve_did");
1081 assert_eq!(raw_doc.parsed.id, "did:plc:test-labeler");
1082 assert!(raw_doc.source_bytes.as_ref() == plc_doc);
1083 assert_eq!(
1084 raw_doc.source_name,
1085 "https://plc.directory/did:plc:test-labeler"
1086 );
1087
1088 let services = raw_doc.parsed.service.as_ref().expect("services");
1090 assert!(
1091 services.iter().any(|s| s.type_ == "AtprotoLabeler"),
1092 "fixture must contain a labeler service"
1093 );
1094 assert!(
1095 services
1096 .iter()
1097 .any(|s| s.type_ == "AtprotoPersonalDataServer"),
1098 "fixture must contain a PDS service"
1099 );
1100
1101 let vms = raw_doc
1103 .parsed
1104 .verification_method
1105 .as_ref()
1106 .expect("verificationMethod");
1107 assert!(
1108 vms.iter().any(|vm| vm.id == "#atproto"),
1109 "fixture must contain a repo signing key"
1110 );
1111 assert!(
1112 vms.iter().any(|vm| vm.id == "#atproto_label"),
1113 "fixture must contain a label signing key"
1114 );
1115 }
1116
1117 #[tokio::test]
1118 async fn resolve_did_web_success() {
1119 let web_doc = include_bytes!("../../tests/fixtures/identity/web_example.json");
1120 let mut responses = HashMap::new();
1121 responses.insert(
1122 "https://example.com/.well-known/did.json".to_string(),
1123 Response::Http(200, web_doc.to_vec()),
1124 );
1125 let http = FakeHttpClient { responses };
1126
1127 let did = Did("did:web:example.com".to_string());
1128 let result = resolve_did(&did, &http).await;
1129
1130 assert!(result.is_ok());
1131 let raw_doc = result.unwrap();
1132 assert_eq!(raw_doc.parsed.id, "did:web:example.com");
1133 assert_eq!(
1134 raw_doc.source_name,
1135 "https://example.com/.well-known/did.json"
1136 );
1137 }
1138
1139 #[tokio::test]
1140 async fn resolve_did_decode_failure_preserves_bytes() {
1141 let bad_json = b"not valid json";
1142 let mut responses = HashMap::new();
1143 responses.insert(
1144 "https://plc.directory/did:plc:bad".to_string(),
1145 Response::Http(200, bad_json.to_vec()),
1146 );
1147 let http = FakeHttpClient { responses };
1148
1149 let did = Did("did:plc:bad".to_string());
1150 let result = resolve_did(&did, &http).await;
1151
1152 assert!(result.is_err());
1153 match result.unwrap_err() {
1154 IdentityError::DidDocumentDecodeFailed {
1155 source_name: _,
1156 source_bytes,
1157 cause: _,
1158 } => {
1159 assert_eq!(source_bytes.as_ref(), bad_json);
1160 }
1161 _ => panic!("Expected DidDocumentDecodeFailed error"),
1162 }
1163 }
1164
1165 #[test]
1166 fn find_service_matches_both_id_forms() {
1167 let doc = DidDocument {
1168 id: "did:plc:abc".to_string(),
1169 also_known_as: None,
1170 verification_method: None,
1171 service: Some(vec![
1172 Service {
1173 id: "did:plc:abc#atproto_labeler".to_string(),
1174 type_: "AtprotoLabeler".to_string(),
1175 service_endpoint: "https://example.com/labeler".to_string(),
1176 },
1177 Service {
1178 id: "#atproto_pds".to_string(),
1179 type_: "AtprotoPersonalDataServer".to_string(),
1180 service_endpoint: "https://example.com/pds".to_string(),
1181 },
1182 Service {
1184 id: "#xatproto_labeler".to_string(),
1185 type_: "OtherType".to_string(),
1186 service_endpoint: "https://example.com/other".to_string(),
1187 },
1188 ]),
1189 };
1190
1191 let labeler = find_service(&doc, "atproto_labeler", "AtprotoLabeler");
1192 assert!(labeler.is_some());
1193 let labeler = labeler.unwrap();
1194 assert_eq!(labeler.id, "did:plc:abc#atproto_labeler");
1195
1196 let pds = find_service(&doc, "atproto_pds", "AtprotoPersonalDataServer");
1197 assert!(pds.is_some());
1198 let pds = pds.unwrap();
1199 assert_eq!(pds.id, "#atproto_pds");
1200
1201 let wrong = find_service(&doc, "atproto_labeler", "OtherType");
1203 assert!(wrong.is_none());
1204 }
1205
1206 #[test]
1207 fn find_service_type_mismatch_returns_none() {
1208 let doc = DidDocument {
1209 id: "did:plc:abc".to_string(),
1210 also_known_as: None,
1211 verification_method: None,
1212 service: Some(vec![Service {
1213 id: "#atproto_labeler".to_string(),
1214 type_: "AtprotoLabeler".to_string(),
1215 service_endpoint: "https://example.com/labeler".to_string(),
1216 }]),
1217 };
1218
1219 let result = find_service(&doc, "atproto_labeler", "WrongType");
1220 assert!(result.is_none());
1221 }
1222
1223 #[test]
1224 fn parse_multikey_k256() {
1225 let multikey = include_str!("../../tests/fixtures/identity/multikey_k256.txt").trim();
1228
1229 let result = parse_multikey(multikey);
1230 assert!(result.is_ok());
1231
1232 let parsed = result.unwrap();
1233
1234 match &parsed.verifying_key {
1236 AnyVerifyingKey::K256(key) => {
1237 let sec1_bytes = key.to_sec1_bytes();
1238 assert_eq!(sec1_bytes.len(), 33); let expected_hex =
1243 "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798";
1244 let actual_hex = sec1_bytes.iter().fold(String::new(), |mut s, b| {
1245 use std::fmt::Write;
1246 let _ = write!(s, "{b:02x}");
1247 s
1248 });
1249 assert_eq!(actual_hex, expected_hex);
1250 }
1251 _ => panic!("Expected K256 verifying key"),
1252 }
1253 }
1254
1255 #[test]
1256 fn parse_multikey_p256() {
1257 let multikey = include_str!("../../tests/fixtures/identity/multikey_p256.txt").trim();
1260
1261 let result = parse_multikey(multikey);
1262 assert!(result.is_ok());
1263
1264 let parsed = result.unwrap();
1265
1266 match &parsed.verifying_key {
1268 AnyVerifyingKey::P256(key) => {
1269 let sec1_bytes = key.to_encoded_point(true).as_bytes().to_vec();
1271 assert_eq!(sec1_bytes.len(), 33);
1272 let expected_hex =
1274 "026b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296";
1275 let actual_hex = sec1_bytes.iter().fold(String::new(), |mut s, b| {
1276 use std::fmt::Write;
1277 let _ = write!(s, "{b:02x}");
1278 s
1279 });
1280 assert_eq!(actual_hex, expected_hex);
1281 }
1282 _ => panic!("Expected P256 verifying key"),
1283 }
1284 }
1285
1286 #[test]
1287 fn parse_multikey_unsupported_curve() {
1288 let mut unsupported_bytes = vec![0x01, 0x00];
1290 unsupported_bytes.extend_from_slice(&[0; 33]); let multikey = multibase::encode(multibase::Base::Base58Btc, unsupported_bytes);
1293
1294 let result = parse_multikey(&multikey);
1295 assert!(result.is_err());
1296
1297 match result.unwrap_err() {
1298 IdentityError::UnsupportedCurve { codec_prefix: _ } => {}
1299 _ => panic!("Expected UnsupportedCurve error"),
1300 }
1301 }
1302
1303 #[test]
1304 fn parse_multikey_not_base58btc() {
1305 let mut key_bytes = vec![0xe7, 0x01];
1307 key_bytes.extend_from_slice(&[0; 33]);
1308 let hex_str = key_bytes.iter().fold(String::new(), |mut s, b| {
1310 use std::fmt::Write;
1311 let _ = write!(s, "{b:02x}");
1312 s
1313 });
1314 let multikey = format!("f{hex_str}");
1315
1316 let result = parse_multikey(&multikey);
1317 assert!(result.is_err());
1318
1319 match result.unwrap_err() {
1320 IdentityError::UnsupportedMultibase(_) => {}
1321 _ => panic!("Expected UnsupportedMultibase error"),
1322 }
1323 }
1324
1325 #[test]
1326 fn parse_multikey_accepts_did_key_prefix() {
1327 let bare = include_str!("../../tests/fixtures/identity/multikey_k256.txt").trim();
1331 let did_key = format!("did:key:{bare}");
1332
1333 let from_bare = parse_multikey(bare).expect("bare multikey should parse");
1334 let from_did_key = parse_multikey(&did_key).expect("did:key multikey should parse");
1335
1336 assert!(matches!(from_bare.verifying_key, AnyVerifyingKey::K256(_)));
1337 assert!(matches!(
1338 from_did_key.verifying_key,
1339 AnyVerifyingKey::K256(_)
1340 ));
1341 }
1342
1343 #[test]
1344 fn parse_multikey_wrong_length() {
1345 let mut wrong_len_bytes = vec![0x80, 0x24]; wrong_len_bytes.extend_from_slice(&[0; 10]); let multikey = multibase::encode(multibase::Base::Base58Btc, &wrong_len_bytes);
1351
1352 let result = parse_multikey(&multikey);
1353 assert!(result.is_err());
1354
1355 match result.unwrap_err() {
1357 IdentityError::MultikeyLengthInvalid => {
1358 }
1360 e => panic!("Expected MultikeyLengthInvalid, got {e:?}"),
1361 }
1362 }
1363
1364 #[test]
1365 fn verify_prehash_k256_valid() {
1366 let signing_key = K256SigningKey::random(&mut k256::elliptic_curve::rand_core::OsRng);
1368 let verifying_key = signing_key.verifying_key();
1369
1370 let prehash = *b"01234567890123456789012345678901";
1372
1373 let signature = signing_key.sign_prehash(&prehash).expect("signing failed");
1375
1376 let any_key = AnyVerifyingKey::K256(*verifying_key);
1378 let any_sig = AnySignature::K256(signature);
1379
1380 assert!(any_key.verify_prehash(&prehash, &any_sig).is_ok());
1382 }
1383
1384 #[test]
1385 fn verify_prehash_p256_valid() {
1386 let signing_key = P256SigningKey::random(&mut p256::elliptic_curve::rand_core::OsRng);
1388 let verifying_key = signing_key.verifying_key();
1389
1390 let prehash = *b"01234567890123456789012345678901";
1392
1393 let signature = signing_key.sign_prehash(&prehash).expect("signing failed");
1395
1396 let any_key = AnyVerifyingKey::P256(*verifying_key);
1398 let any_sig = AnySignature::P256(signature);
1399
1400 assert!(any_key.verify_prehash(&prehash, &any_sig).is_ok());
1402 }
1403
1404 #[test]
1405 fn verify_prehash_curve_mismatch() {
1406 let k256_signing_key = K256SigningKey::random(&mut k256::elliptic_curve::rand_core::OsRng);
1408 let k256_key = AnyVerifyingKey::K256(*k256_signing_key.verifying_key());
1409
1410 let p256_signing_key = P256SigningKey::random(&mut p256::elliptic_curve::rand_core::OsRng);
1411 let prehash = *b"01234567890123456789012345678901";
1412 let p256_sig = p256_signing_key
1413 .sign_prehash(&prehash)
1414 .expect("signing failed");
1415 let p256_any_sig = AnySignature::P256(p256_sig);
1416
1417 assert!(k256_key.verify_prehash(&prehash, &p256_any_sig).is_err());
1419
1420 let p256_signing_key_2 =
1422 P256SigningKey::random(&mut p256::elliptic_curve::rand_core::OsRng);
1423 let p256_key = AnyVerifyingKey::P256(*p256_signing_key_2.verifying_key());
1424
1425 let k256_signing_key_2 =
1426 K256SigningKey::random(&mut k256::elliptic_curve::rand_core::OsRng);
1427 let k256_sig = k256_signing_key_2
1428 .sign_prehash(&prehash)
1429 .expect("signing failed");
1430 let k256_any_sig = AnySignature::K256(k256_sig);
1431
1432 assert!(p256_key.verify_prehash(&prehash, &k256_any_sig).is_err());
1434 }
1435
1436 #[tokio::test]
1437 async fn plc_history_parses_rotation_fixture() {
1438 let fixture_bytes =
1440 include_bytes!("../../tests/fixtures/identity/plc_audit_log_with_rotation.json");
1441 let mut responses = HashMap::new();
1442 responses.insert(
1443 "https://plc.directory/did:plc:test/log/audit".to_string(),
1444 Response::Http(200, fixture_bytes.to_vec()),
1445 );
1446 let http = FakeHttpClient { responses };
1447
1448 let did = Did("did:plc:test".to_string());
1449 let result = plc_history_for_fragment(&did, "atproto_label", &http)
1450 .await
1451 .expect("plc_history should succeed");
1452
1453 assert_eq!(result.len(), 2);
1455 assert_eq!(
1456 result[0].key_id,
1457 "z3u1HhU9Dn1R1TDe8kSmEMCrJ5B8t9K7c7N2L3xX7y"
1458 );
1459 assert!(!result[0].nullified);
1460 assert_eq!(
1461 result[1].key_id,
1462 "z3u1HhU9Dn1R1TDe8kSmEMCrJ5B8t9K7c7N2L3xX7z"
1463 );
1464 assert!(!result[1].nullified);
1465 }
1466
1467 #[tokio::test]
1468 async fn plc_history_dedupes_repeated_key() {
1469 let key = "did:key:zQ3shw6eSipD1cnrmmokVWvKCuE6Yc9j2jAjWJ9nWpuF4yQKV";
1473 let log = serde_json::json!([
1474 {"cid": "op3", "operation": {"verificationMethods": {"atproto_label": key}}},
1475 {"cid": "op2", "operation": {"verificationMethods": {"atproto_label": key}}},
1476 {"cid": "op1", "operation": {"verificationMethods": {"atproto_label": key}}},
1477 ]);
1478 let mut responses = HashMap::new();
1479 responses.insert(
1480 "https://plc.directory/did:plc:dedupe/log/audit".to_string(),
1481 Response::Http(200, serde_json::to_vec(&log).unwrap()),
1482 );
1483 let http = FakeHttpClient { responses };
1484
1485 let did = Did("did:plc:dedupe".to_string());
1486 let result = plc_history_for_fragment(&did, "atproto_label", &http)
1487 .await
1488 .expect("plc_history should succeed");
1489
1490 assert_eq!(result.len(), 1);
1491 assert_eq!(result[0].key_id, key);
1492 assert_eq!(result[0].operation_cid, "op3");
1494 }
1495
1496 #[tokio::test]
1497 #[should_panic(expected = "plc_history_for_fragment called with non-plc DID")]
1498 async fn plc_history_unsupported_method_errors() {
1499 let mut responses = HashMap::new();
1502 responses.insert(
1503 "https://plc.directory/did:web:example.com/log/audit".to_string(),
1504 Response::Http(200, b"[]".to_vec()),
1505 );
1506 let http = FakeHttpClient { responses };
1507
1508 let did = Did("did:web:example.com".to_string());
1509 let _result = plc_history_for_fragment(&did, "atproto_label", &http).await;
1510 }
1511
1512 #[tokio::test]
1513 async fn plc_history_transport_error_propagates() {
1514 let mut responses = HashMap::new();
1516 responses.insert(
1517 "https://plc.directory/did:plc:test/log/audit".to_string(),
1518 Response::Transport("connection refused".to_string()),
1519 );
1520 let http = FakeHttpClient { responses };
1521
1522 let did = Did("did:plc:test".to_string());
1523 let result = plc_history_for_fragment(&did, "atproto_label", &http).await;
1524
1525 assert!(result.is_err());
1526 match result.unwrap_err() {
1528 IdentityError::DidResolutionFailed { status, body } => {
1529 assert_eq!(status, 0);
1530 assert_eq!(body, "Transport error: connection refused");
1531 }
1532 e => panic!("Expected DidResolutionFailed with status 0, got {e:?}"),
1533 }
1534 }
1535
1536 #[test]
1537 fn any_signing_key_k256_round_trip() {
1538 let key = AnySigningKey::K256(K256SigningKey::from_slice(&[1u8; 32]).expect("valid seed"));
1539 let vkey = key.verifying_key();
1540 let msg = b"test message";
1541 let sig = key.sign(msg);
1542 assert!(vkey.verify_prehash(&[0u8; 32], &sig).is_err()); let sig2 = key.sign(msg);
1546 let hash: [u8; 32] = sha2::Sha256::digest(msg).into();
1547 assert!(vkey.verify_prehash(&hash, &sig2).is_ok());
1548 }
1549
1550 #[test]
1551 fn any_signing_key_p256_round_trip() {
1552 let key = AnySigningKey::P256(P256SigningKey::from_slice(&[2u8; 32]).expect("valid seed"));
1553 let vkey = key.verifying_key();
1554 let msg = b"test message";
1555 let sig = key.sign(msg);
1556 let hash: [u8; 32] = sha2::Sha256::digest(msg).into();
1557 assert!(vkey.verify_prehash(&hash, &sig).is_ok());
1558 }
1559
1560 #[test]
1561 fn any_signing_key_jwt_alg() {
1562 let k256_key =
1563 AnySigningKey::K256(K256SigningKey::from_slice(&[1u8; 32]).expect("valid seed"));
1564 let p256_key =
1565 AnySigningKey::P256(P256SigningKey::from_slice(&[2u8; 32]).expect("valid seed"));
1566
1567 assert_eq!(k256_key.jwt_alg(), "ES256K");
1568 assert_eq!(p256_key.jwt_alg(), "ES256");
1569 }
1570
1571 #[test]
1572 fn any_signature_to_jws_bytes() {
1573 let key = AnySigningKey::K256(K256SigningKey::from_slice(&[1u8; 32]).expect("valid seed"));
1574 let msg = b"test";
1575 let sig = key.sign(msg);
1576 let jws_bytes = sig.to_jws_bytes();
1577 assert_eq!(jws_bytes.len(), 64);
1578 }
1579
1580 #[test]
1581 fn any_signing_key_p256_signature_is_normalized() {
1582 let key = AnySigningKey::P256(P256SigningKey::from_slice(&[3u8; 32]).expect("valid seed"));
1584 let msg = b"test message for normalization";
1585 let vkey = key.verifying_key();
1586
1587 let sig = key.sign(msg);
1589
1590 if let AnySignature::P256(sig_p256) = &sig {
1592 assert!(
1593 sig_p256.normalize_s().is_none(),
1594 "signature should already be low-s (further normalization should return None)"
1595 );
1596 } else {
1597 unreachable!("signing with P256 key must produce P256 signature");
1598 }
1599
1600 use sha2::Digest as _;
1602 let hash: [u8; 32] = sha2::Sha256::digest(msg).into();
1603 assert!(
1604 vkey.verify_prehash(&hash, &sig).is_ok(),
1605 "P256 signature should verify after normalization"
1606 );
1607
1608 let sig_bytes = sig.to_jws_bytes();
1610 assert_eq!(
1611 sig_bytes.len(),
1612 64,
1613 "P256 signature should be 64 bytes after JWS serialization"
1614 );
1615 }
1616
1617 #[test]
1618 fn is_local_labeler_hostname_classifies_expected_hosts() {
1619 let cases: &[(&str, bool)] = &[
1620 ("http://localhost/", true),
1622 ("https://LOCALHOST:8080/foo", true),
1623 ("http://127.0.0.1/", true),
1624 ("http://127.1.2.3/", true),
1625 ("http://[::1]/", true),
1626 ("http://mybox.local/", true),
1628 ("https://mybox.LOCAL:8443/", true),
1629 ("http://10.0.0.1/", true),
1631 ("http://172.16.0.1/", true),
1632 ("http://172.31.255.255/", true),
1633 ("http://192.168.1.100/", true),
1634 ("https://labeler.example.com/", false),
1636 ("http://8.8.8.8/", false),
1637 ("http://172.15.0.1/", false), ("http://172.32.0.1/", false), ("http://11.0.0.1/", false), ("http://172.17.1.1/", true), ];
1642 for (url, expected) in cases {
1643 let parsed = Url::parse(url).expect("test URLs are valid");
1644 assert_eq!(
1645 is_local_labeler_hostname(&parsed),
1646 *expected,
1647 "classification mismatch for {url}"
1648 );
1649 }
1650 }
1651
1652 #[test]
1653 fn encode_multikey_round_trip_k256() {
1654 let signing_key = AnySigningKey::K256(k256::ecdsa::SigningKey::random(
1657 &mut k256::elliptic_curve::rand_core::OsRng,
1658 ));
1659 let original_verifying = signing_key.verifying_key();
1660
1661 let encoded = encode_multikey(&original_verifying);
1663 assert!(
1664 encoded.starts_with('z'),
1665 "multikey should start with 'z' (base58btc)"
1666 );
1667
1668 let parsed = parse_multikey(&encoded).expect("encoded multikey should parse");
1670 match (&original_verifying, &parsed.verifying_key) {
1671 (AnyVerifyingKey::K256(original), AnyVerifyingKey::K256(decoded)) => {
1672 let orig_bytes = original.to_sec1_bytes();
1673 let decoded_bytes = decoded.to_sec1_bytes();
1674 assert_eq!(
1675 orig_bytes, decoded_bytes,
1676 "k256 keys should match after round-trip"
1677 );
1678 }
1679 _ => panic!("Expected K256 keys"),
1680 }
1681 }
1682
1683 #[test]
1684 fn encode_multikey_round_trip_p256() {
1685 let signing_key = AnySigningKey::P256(p256::ecdsa::SigningKey::random(
1688 &mut p256::elliptic_curve::rand_core::OsRng,
1689 ));
1690 let original_verifying = signing_key.verifying_key();
1691
1692 let encoded = encode_multikey(&original_verifying);
1694 assert!(
1695 encoded.starts_with('z'),
1696 "multikey should start with 'z' (base58btc)"
1697 );
1698
1699 let parsed = parse_multikey(&encoded).expect("encoded multikey should parse");
1701 match (&original_verifying, &parsed.verifying_key) {
1702 (AnyVerifyingKey::P256(original), AnyVerifyingKey::P256(decoded)) => {
1703 let orig_bytes = original.to_encoded_point(true).as_bytes().to_vec();
1704 let decoded_bytes = decoded.to_encoded_point(true).as_bytes().to_vec();
1705 assert_eq!(
1706 orig_bytes, decoded_bytes,
1707 "p256 keys should match after round-trip"
1708 );
1709 }
1710 _ => panic!("Expected P256 keys"),
1711 }
1712 }
1713}