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)]
150pub enum AnySignature {
151 K256(k256::ecdsa::Signature),
153 P256(p256::ecdsa::Signature),
155}
156
157#[derive(Debug, thiserror::Error)]
159pub enum AnySignatureError {
160 #[error("secp256k1 signature verification failed")]
162 K256(#[source] k256::ecdsa::Error),
163 #[error("P-256 signature verification failed")]
165 P256(#[source] p256::ecdsa::Error),
166 #[error("Signature and key use mismatched curves")]
168 CurveMismatch,
169}
170
171#[derive(Debug, Clone)]
173pub struct ParsedMultikey {
174 pub verifying_key: AnyVerifyingKey,
176}
177
178#[derive(Debug, Error)]
180pub enum IdentityError {
181 #[error("Invalid handle format")]
183 InvalidHandle,
184
185 #[error("Handle could not be resolved")]
187 HandleUnresolvable {
188 dns_error: Option<Box<IdentityError>>,
190 http_error: Option<Box<IdentityError>>,
192 },
193
194 #[error("DNS lookup failed")]
196 DnsLookupFailed {
197 #[source]
199 source: Box<IdentityError>,
200 },
201
202 #[error("DNS backend error")]
204 DnsBackend(#[from] hickory_resolver::ResolveError),
205
206 #[error("HTTP fallback for handle resolution failed")]
208 HandleHttpFallbackFailed {
209 #[source]
211 source: Box<IdentityError>,
212 },
213
214 #[error("Unsupported DID method: {method}")]
216 UnsupportedDidMethod { method: String },
217
218 #[error("DID resolution failed with status {status}")]
220 DidResolutionFailed {
221 status: u16,
223 body: String,
225 },
226
227 #[error("DNS record for {handle} has no did= entry")]
229 DnsNoDidRecord {
230 handle: String,
232 },
233
234 #[error("Invalid DID body: {body}")]
236 InvalidDidBody {
237 body: String,
239 },
240
241 #[error("DID document decode failed")]
243 DidDocumentDecodeFailed {
244 source_name: String,
246 source_bytes: Arc<[u8]>,
248 #[source]
250 cause: serde_json::Error,
251 },
252
253 #[error("Multikey decoding failed")]
255 MultikeyDecodeFailed {
256 #[source]
258 source: Box<IdentityError>,
259 },
260
261 #[error("Unsupported multibase encoding")]
263 UnsupportedMultibase(String),
264
265 #[error("Unsupported curve")]
267 UnsupportedCurve { codec_prefix: Vec<u8> },
268
269 #[error("Invalid multikey length")]
271 MultikeyLengthInvalid,
272
273 #[error("HTTP transport error")]
275 HttpTransport(#[from] reqwest::Error),
276}
277
278#[async_trait]
282pub trait HttpClient: Send + Sync {
283 async fn get_bytes(&self, url: &Url) -> Result<(u16, Vec<u8>), IdentityError>;
285}
286
287#[async_trait]
291pub trait DnsResolver: Send + Sync {
292 async fn txt_lookup(&self, name: &str) -> Result<Vec<String>, IdentityError>;
294}
295
296pub struct RealHttpClient {
298 inner: reqwest::Client,
299}
300
301impl RealHttpClient {
302 pub fn new() -> Result<Self, IdentityError> {
307 let client = reqwest::Client::builder()
308 .use_rustls_tls()
309 .user_agent(APP_USER_AGENT)
310 .timeout(std::time::Duration::from_secs(10))
311 .build()?;
312 Ok(Self { inner: client })
313 }
314
315 pub fn from_client(client: reqwest::Client) -> Self {
319 Self { inner: client }
320 }
321}
322
323#[async_trait]
324impl HttpClient for RealHttpClient {
325 async fn get_bytes(&self, url: &Url) -> Result<(u16, Vec<u8>), IdentityError> {
326 let response = self.inner.get(url.clone()).send().await?;
327 let status = response.status().as_u16();
328 let bytes = response.bytes().await?;
329 Ok((status, bytes.to_vec()))
330 }
331}
332
333pub struct RealDnsResolver {
335 inner: hickory_resolver::TokioResolver,
336}
337
338impl RealDnsResolver {
339 pub fn new() -> Self {
341 let resolver = hickory_resolver::Resolver::builder_tokio()
342 .expect("failed to build DNS resolver")
343 .build();
344 Self { inner: resolver }
345 }
346}
347
348impl Default for RealDnsResolver {
349 fn default() -> Self {
350 Self::new()
351 }
352}
353
354#[async_trait]
355impl DnsResolver for RealDnsResolver {
356 async fn txt_lookup(&self, name: &str) -> Result<Vec<String>, IdentityError> {
357 let lookup = self.inner.txt_lookup(name).await?;
358 lookup
359 .iter()
360 .map(|record| {
361 let text = record
362 .iter()
363 .map(|data| {
364 String::from_utf8(data.to_vec()).unwrap_or_else(|_| {
365 tracing::debug!(
366 target = "atproto_devtool::identity",
367 "dropping non-UTF-8 TXT record data"
368 );
369 String::new()
370 })
371 })
372 .collect::<Vec<_>>()
373 .join("");
374 Ok(text)
375 })
376 .collect()
377 }
378}
379
380pub async fn resolve_handle(
385 handle: &str,
386 http: &dyn HttpClient,
387 dns: &dyn DnsResolver,
388) -> Result<Did, IdentityError> {
389 if handle.is_empty()
391 || handle.starts_with('.')
392 || handle.ends_with('.')
393 || !handle.is_ascii()
394 || !handle.contains('.')
395 {
396 return Err(IdentityError::InvalidHandle);
397 }
398
399 tracing::debug!(
400 target = "atproto_devtool::identity",
401 handle = %handle,
402 "resolving handle"
403 );
404
405 let dns_name = format!("_atproto.{handle}");
407
408 let dns_error_opt = match dns.txt_lookup(&dns_name).await {
409 Ok(records) => {
410 for record in records {
412 let trimmed = record.trim();
413 if let Some(did_str) = trimmed.strip_prefix("did=") {
414 let did = Did(did_str.to_string());
415 tracing::debug!(
416 target = "atproto_devtool::identity",
417 did = %did,
418 "resolved handle via DNS"
419 );
420 return Ok(did);
421 }
422 }
423 Some(Box::new(IdentityError::DnsNoDidRecord {
425 handle: handle.to_string(),
426 }))
427 }
428 Err(e) => Some(Box::new(e)),
429 };
430
431 let url = format!("https://{handle}/.well-known/atproto-did");
433 let url = url
434 .parse::<Url>()
435 .map_err(|_| IdentityError::InvalidHandle)?;
436
437 let http_error_opt = match http.get_bytes(&url).await {
438 Ok((200, bytes)) => {
439 let did_str = String::from_utf8_lossy(&bytes).trim().to_string();
440 if !did_str.is_empty() && did_str.starts_with("did:") {
441 let did = Did(did_str);
442 tracing::debug!(
443 target = "atproto_devtool::identity",
444 did = %did,
445 "resolved handle via HTTPS"
446 );
447 return Ok(did);
448 } else {
449 Some(Box::new(IdentityError::HandleHttpFallbackFailed {
450 source: Box::new(IdentityError::InvalidDidBody { body: did_str }),
451 }))
452 }
453 }
454 Ok((status, bytes)) => Some(Box::new(IdentityError::HandleHttpFallbackFailed {
455 source: Box::new(IdentityError::DidResolutionFailed {
456 status,
457 body: String::from_utf8_lossy(&bytes).to_string(),
458 }),
459 })),
460 Err(e) => Some(Box::new(IdentityError::HandleHttpFallbackFailed {
461 source: Box::new(e),
462 })),
463 };
464
465 Err(IdentityError::HandleUnresolvable {
467 dns_error: dns_error_opt,
468 http_error: http_error_opt,
469 })
470}
471
472pub async fn resolve_did(
476 did: &Did,
477 http: &dyn HttpClient,
478) -> Result<RawDidDocument, IdentityError> {
479 tracing::debug!(
480 target = "atproto_devtool::identity",
481 did = %did,
482 "resolving DID"
483 );
484
485 let (url, source_name) = match did.method() {
486 DidMethod::Plc => {
487 let did_str = &did.0;
490 let url_str = format!("https://plc.directory/{did_str}");
491 let url = url_str
492 .parse::<Url>()
493 .map_err(|_| IdentityError::DidResolutionFailed {
494 status: 400,
495 body: "Invalid DID format".to_string(),
496 })?;
497 (url.clone(), url.to_string())
498 }
499 DidMethod::Web => {
500 let rest = did.0.strip_prefix("did:web:").unwrap_or("");
502 let parts: Vec<&str> = rest.split(':').collect();
503
504 if parts.is_empty() {
505 return Err(IdentityError::DidResolutionFailed {
506 status: 400,
507 body: "Invalid did:web format".to_string(),
508 });
509 }
510
511 let host = parts[0];
512 let path_parts = &parts[1..];
513
514 let url_str = if path_parts.is_empty() {
515 format!("https://{host}/.well-known/did.json")
516 } else {
517 let path = path_parts
518 .iter()
519 .map(|p| percent_decode_str(p).unwrap_or_default())
520 .collect::<Vec<_>>()
521 .join("/");
522 format!("https://{host}/{path}/did.json")
523 };
524
525 let url = url_str
526 .parse::<Url>()
527 .map_err(|_| IdentityError::DidResolutionFailed {
528 status: 400,
529 body: "Invalid URL".to_string(),
530 })?;
531 (url.clone(), url.to_string())
532 }
533 DidMethod::Other => {
534 return Err(IdentityError::UnsupportedDidMethod {
535 method: did.0.clone(),
536 });
537 }
538 };
539
540 let (status, bytes) = http.get_bytes(&url).await?;
541
542 if status != 200 {
543 return Err(IdentityError::DidResolutionFailed {
544 status,
545 body: String::from_utf8_lossy(&bytes).to_string(),
546 });
547 }
548
549 tracing::debug!(
550 target = "atproto_devtool::identity",
551 bytes_len = bytes.len(),
552 "fetched DID document"
553 );
554
555 let parsed = serde_json::from_slice::<DidDocument>(&bytes).map_err(|e| {
556 IdentityError::DidDocumentDecodeFailed {
557 source_name: source_name.clone(),
558 source_bytes: Arc::from(bytes.clone()),
559 cause: e,
560 }
561 })?;
562
563 Ok(RawDidDocument {
564 parsed,
565 source_bytes: Arc::from(bytes),
566 source_name,
567 })
568}
569
570pub fn find_service<'a>(
574 doc: &'a DidDocument,
575 id_fragment: &str,
576 expected_type: &str,
577) -> Option<&'a Service> {
578 let services = doc.service.as_ref()?;
579
580 for service in services {
581 let frag = service.id.rsplit_once('#').map(|(_, f)| f);
585 if frag == Some(id_fragment) && service.type_ == expected_type {
586 return Some(service);
587 }
588 }
589
590 None
591}
592
593pub fn parse_multikey(raw: &str) -> Result<ParsedMultikey, IdentityError> {
600 tracing::debug!(target = "atproto_devtool::identity", "parsing multikey");
601
602 let multibase_str = raw.strip_prefix("did:key:").unwrap_or(raw);
603
604 let (base, bytes) =
605 multibase::decode(multibase_str).map_err(|_| IdentityError::MultikeyDecodeFailed {
606 source: Box::new(IdentityError::UnsupportedMultibase(
607 "failed to decode multibase".to_string(),
608 )),
609 })?;
610
611 if base != multibase::Base::Base58Btc {
613 return Err(IdentityError::UnsupportedMultibase(
614 "multikey must use base58btc encoding".to_string(),
615 ));
616 }
617
618 if bytes.len() < 2 {
619 return Err(IdentityError::MultikeyLengthInvalid);
620 }
621
622 let curve_bytes = [bytes[0], bytes[1]];
624 let rest = &bytes[2..];
625
626 match curve_bytes {
627 [0xe7, 0x01] => {
629 if rest.len() != 33 {
630 return Err(IdentityError::MultikeyLengthInvalid);
631 }
632 let key = k256::ecdsa::VerifyingKey::from_sec1_bytes(rest).map_err(|_| {
633 IdentityError::MultikeyDecodeFailed {
634 source: Box::new(IdentityError::MultikeyLengthInvalid),
635 }
636 })?;
637 tracing::debug!(
638 target = "atproto_devtool::identity",
639 curve = "secp256k1",
640 "parsed multikey"
641 );
642 Ok(ParsedMultikey {
643 verifying_key: AnyVerifyingKey::K256(key),
644 })
645 }
646 [0x80, 0x24] => {
648 if rest.len() != 33 {
649 return Err(IdentityError::MultikeyLengthInvalid);
650 }
651 let key = p256::ecdsa::VerifyingKey::from_sec1_bytes(rest).map_err(|_| {
652 IdentityError::MultikeyDecodeFailed {
653 source: Box::new(IdentityError::MultikeyLengthInvalid),
654 }
655 })?;
656 tracing::debug!(
657 target = "atproto_devtool::identity",
658 curve = "p256",
659 "parsed multikey"
660 );
661 Ok(ParsedMultikey {
662 verifying_key: AnyVerifyingKey::P256(key),
663 })
664 }
665 _ => Err(IdentityError::UnsupportedCurve {
666 codec_prefix: curve_bytes.to_vec(),
667 }),
668 }
669}
670
671#[derive(Debug, Clone, PartialEq, Eq)]
673pub struct PlcHistoricKey {
674 pub key_id: String,
676 pub operation_cid: String,
678 pub introduced_at: String,
680 pub nullified: bool,
682}
683
684pub async fn plc_history_for_fragment(
693 did: &Did,
694 fragment: &str,
695 http: &dyn HttpClient,
696) -> Result<Vec<PlcHistoricKey>, IdentityError> {
697 debug_assert!(
698 did.method() == DidMethod::Plc,
699 "plc_history_for_fragment called with non-plc DID: {did}"
700 );
701
702 if did.method() != DidMethod::Plc {
703 return Err(IdentityError::UnsupportedDidMethod {
704 method: format!("{:?}", did.method()),
705 });
706 }
707
708 let audit_url = format!("https://plc.directory/{did}/log/audit");
710 let url = Url::parse(&audit_url).map_err(|_| IdentityError::DidResolutionFailed {
711 status: 400,
712 body: "Invalid PLC audit URL".to_string(),
713 })?;
714
715 let (status, bytes) = http.get_bytes(&url).await?;
716
717 if status != 200 {
718 return Err(IdentityError::DidResolutionFailed {
719 status,
720 body: format!("PLC audit log fetch returned status {status}"),
721 });
722 }
723
724 let operations: Vec<serde_json::Value> =
726 serde_json::from_slice(&bytes).map_err(|cause| IdentityError::DidDocumentDecodeFailed {
727 source_name: "plc audit log".to_string(),
728 source_bytes: Arc::from(bytes.into_boxed_slice()),
729 cause,
730 })?;
731
732 let mut historic_keys: Vec<PlcHistoricKey> = Vec::new();
733
734 for op in operations {
739 let vm = match op
741 .get("operation")
742 .and_then(|o| o.get("verificationMethods"))
743 {
744 Some(vm) => vm,
745 None => continue,
746 };
747
748 if let Some(multikey_value) = vm.get(fragment) {
750 let multikey_str = match multikey_value.as_str() {
751 Some(s) => s.to_string(),
752 None => continue,
753 };
754
755 let operation_cid = op
756 .get("cid")
757 .and_then(|c| c.as_str())
758 .unwrap_or("unknown")
759 .to_string();
760
761 let introduced_at = op
763 .get("operation")
764 .and_then(|o| o.get("createdAt"))
765 .and_then(|c| c.as_str())
766 .unwrap_or("unknown")
767 .to_string();
768
769 let nullified = op
770 .get("nullified")
771 .and_then(|n| n.as_bool())
772 .unwrap_or(false);
773
774 if historic_keys.iter().any(|k| k.key_id == multikey_str) {
775 continue;
776 }
777
778 historic_keys.push(PlcHistoricKey {
779 key_id: multikey_str,
780 operation_cid,
781 introduced_at,
782 nullified,
783 });
784 }
785 }
786
787 Ok(historic_keys)
788}
789
790fn percent_decode_str(s: &str) -> Result<String, IdentityError> {
792 let decoded = percent_encoding::percent_decode_str(s)
793 .decode_utf8()
794 .map_err(|_| IdentityError::DidResolutionFailed {
795 status: 400,
796 body: "Invalid UTF-8 in percent-encoded path".to_string(),
797 })?;
798 Ok(decoded.to_string())
799}
800
801#[cfg(test)]
802mod tests {
803 use super::*;
804 use k256::ecdsa::SigningKey as K256SigningKey;
805 use k256::ecdsa::signature::hazmat::PrehashSigner;
806 use p256::ecdsa::SigningKey as P256SigningKey;
807 use std::collections::HashMap;
808
809 #[derive(Clone)]
811 enum Response {
812 Http(u16, Vec<u8>),
814 Transport(String),
816 }
817
818 struct FakeHttpClient {
820 responses: HashMap<String, Response>,
821 }
822
823 #[async_trait]
824 impl HttpClient for FakeHttpClient {
825 async fn get_bytes(&self, url: &Url) -> Result<(u16, Vec<u8>), IdentityError> {
826 match self.responses.get(url.as_str()).cloned() {
827 Some(Response::Http(status, body)) => Ok((status, body)),
828 Some(Response::Transport(message)) => {
829 Err(IdentityError::DidResolutionFailed {
833 status: 0,
834 body: format!("Transport error: {message}"),
835 })
836 }
837 None => Err(IdentityError::DidResolutionFailed {
838 status: 404,
839 body: "Not found".to_string(),
840 }),
841 }
842 }
843 }
844
845 struct FakeDnsResolver {
847 records: HashMap<String, Vec<String>>,
848 }
849
850 #[async_trait]
851 impl DnsResolver for FakeDnsResolver {
852 async fn txt_lookup(&self, name: &str) -> Result<Vec<String>, IdentityError> {
853 self.records
854 .get(name)
855 .cloned()
856 .ok_or_else(|| IdentityError::DnsLookupFailed {
857 source: Box::new(IdentityError::InvalidHandle),
858 })
859 }
860 }
861
862 #[tokio::test]
863 async fn resolve_handle_via_dns() {
864 let mut records = HashMap::new();
865 records.insert(
866 "_atproto.alice.example".to_string(),
867 vec!["did=did:plc:abc123".to_string()],
868 );
869 let dns = FakeDnsResolver { records };
870 let http = FakeHttpClient {
871 responses: HashMap::new(),
872 };
873
874 let result = resolve_handle("alice.example", &http, &dns).await;
875
876 assert!(result.is_ok());
877 let did = result.unwrap();
878 assert_eq!(did.0, "did:plc:abc123");
879 }
880
881 #[tokio::test]
882 async fn resolve_handle_via_https_fallback() {
883 let dns = FakeDnsResolver {
884 records: HashMap::new(),
885 };
886 let mut responses = HashMap::new();
887 responses.insert(
888 "https://alice.example/.well-known/atproto-did".to_string(),
889 Response::Http(200, b"did:plc:abc123\n".to_vec()),
890 );
891 let http = FakeHttpClient { responses };
892
893 let result = resolve_handle("alice.example", &http, &dns).await;
894
895 assert!(result.is_ok());
896 let did = result.unwrap();
897 assert_eq!(did.0, "did:plc:abc123");
898 }
899
900 #[tokio::test]
901 async fn resolve_handle_both_paths_fail() {
902 let dns = FakeDnsResolver {
903 records: HashMap::new(),
904 };
905 let http = FakeHttpClient {
906 responses: HashMap::new(),
907 };
908
909 let result = resolve_handle("alice.example", &http, &dns).await;
910
911 assert!(result.is_err());
912 match result.unwrap_err() {
913 IdentityError::HandleUnresolvable {
914 dns_error,
915 http_error,
916 } => {
917 assert!(dns_error.is_some());
918 assert!(http_error.is_some());
919 }
920 _ => panic!("Expected HandleUnresolvable error"),
921 }
922 }
923
924 #[tokio::test]
925 async fn resolve_did_plc_success() {
926 let plc_doc = include_bytes!("../../tests/fixtures/identity/plc_bsky_labeler.json");
927 let mut responses = HashMap::new();
928 responses.insert(
929 "https://plc.directory/did:plc:test-labeler".to_string(),
930 Response::Http(200, plc_doc.to_vec()),
931 );
932 let http = FakeHttpClient { responses };
933
934 let did = Did("did:plc:test-labeler".to_string());
935 let raw_doc = resolve_did(&did, &http).await.expect("resolve_did");
936 assert_eq!(raw_doc.parsed.id, "did:plc:test-labeler");
937 assert!(raw_doc.source_bytes.as_ref() == plc_doc);
938 assert_eq!(
939 raw_doc.source_name,
940 "https://plc.directory/did:plc:test-labeler"
941 );
942
943 let services = raw_doc.parsed.service.as_ref().expect("services");
945 assert!(
946 services.iter().any(|s| s.type_ == "AtprotoLabeler"),
947 "fixture must contain a labeler service"
948 );
949 assert!(
950 services
951 .iter()
952 .any(|s| s.type_ == "AtprotoPersonalDataServer"),
953 "fixture must contain a PDS service"
954 );
955
956 let vms = raw_doc
958 .parsed
959 .verification_method
960 .as_ref()
961 .expect("verificationMethod");
962 assert!(
963 vms.iter().any(|vm| vm.id == "#atproto"),
964 "fixture must contain a repo signing key"
965 );
966 assert!(
967 vms.iter().any(|vm| vm.id == "#atproto_label"),
968 "fixture must contain a label signing key"
969 );
970 }
971
972 #[tokio::test]
973 async fn resolve_did_web_success() {
974 let web_doc = include_bytes!("../../tests/fixtures/identity/web_example.json");
975 let mut responses = HashMap::new();
976 responses.insert(
977 "https://example.com/.well-known/did.json".to_string(),
978 Response::Http(200, web_doc.to_vec()),
979 );
980 let http = FakeHttpClient { responses };
981
982 let did = Did("did:web:example.com".to_string());
983 let result = resolve_did(&did, &http).await;
984
985 assert!(result.is_ok());
986 let raw_doc = result.unwrap();
987 assert_eq!(raw_doc.parsed.id, "did:web:example.com");
988 assert_eq!(
989 raw_doc.source_name,
990 "https://example.com/.well-known/did.json"
991 );
992 }
993
994 #[tokio::test]
995 async fn resolve_did_decode_failure_preserves_bytes() {
996 let bad_json = b"not valid json";
997 let mut responses = HashMap::new();
998 responses.insert(
999 "https://plc.directory/did:plc:bad".to_string(),
1000 Response::Http(200, bad_json.to_vec()),
1001 );
1002 let http = FakeHttpClient { responses };
1003
1004 let did = Did("did:plc:bad".to_string());
1005 let result = resolve_did(&did, &http).await;
1006
1007 assert!(result.is_err());
1008 match result.unwrap_err() {
1009 IdentityError::DidDocumentDecodeFailed {
1010 source_name: _,
1011 source_bytes,
1012 cause: _,
1013 } => {
1014 assert_eq!(source_bytes.as_ref(), bad_json);
1015 }
1016 _ => panic!("Expected DidDocumentDecodeFailed error"),
1017 }
1018 }
1019
1020 #[test]
1021 fn find_service_matches_both_id_forms() {
1022 let doc = DidDocument {
1023 id: "did:plc:abc".to_string(),
1024 also_known_as: None,
1025 verification_method: None,
1026 service: Some(vec![
1027 Service {
1028 id: "did:plc:abc#atproto_labeler".to_string(),
1029 type_: "AtprotoLabeler".to_string(),
1030 service_endpoint: "https://example.com/labeler".to_string(),
1031 },
1032 Service {
1033 id: "#atproto_pds".to_string(),
1034 type_: "AtprotoPersonalDataServer".to_string(),
1035 service_endpoint: "https://example.com/pds".to_string(),
1036 },
1037 Service {
1039 id: "#xatproto_labeler".to_string(),
1040 type_: "OtherType".to_string(),
1041 service_endpoint: "https://example.com/other".to_string(),
1042 },
1043 ]),
1044 };
1045
1046 let labeler = find_service(&doc, "atproto_labeler", "AtprotoLabeler");
1047 assert!(labeler.is_some());
1048 let labeler = labeler.unwrap();
1049 assert_eq!(labeler.id, "did:plc:abc#atproto_labeler");
1050
1051 let pds = find_service(&doc, "atproto_pds", "AtprotoPersonalDataServer");
1052 assert!(pds.is_some());
1053 let pds = pds.unwrap();
1054 assert_eq!(pds.id, "#atproto_pds");
1055
1056 let wrong = find_service(&doc, "atproto_labeler", "OtherType");
1058 assert!(wrong.is_none());
1059 }
1060
1061 #[test]
1062 fn find_service_type_mismatch_returns_none() {
1063 let doc = DidDocument {
1064 id: "did:plc:abc".to_string(),
1065 also_known_as: None,
1066 verification_method: None,
1067 service: Some(vec![Service {
1068 id: "#atproto_labeler".to_string(),
1069 type_: "AtprotoLabeler".to_string(),
1070 service_endpoint: "https://example.com/labeler".to_string(),
1071 }]),
1072 };
1073
1074 let result = find_service(&doc, "atproto_labeler", "WrongType");
1075 assert!(result.is_none());
1076 }
1077
1078 #[test]
1079 fn parse_multikey_k256() {
1080 let multikey = include_str!("../../tests/fixtures/identity/multikey_k256.txt").trim();
1083
1084 let result = parse_multikey(multikey);
1085 assert!(result.is_ok());
1086
1087 let parsed = result.unwrap();
1088
1089 match &parsed.verifying_key {
1091 AnyVerifyingKey::K256(key) => {
1092 let sec1_bytes = key.to_sec1_bytes();
1093 assert_eq!(sec1_bytes.len(), 33); let expected_hex =
1098 "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798";
1099 let actual_hex: String = sec1_bytes.iter().map(|b| format!("{b:02x}")).collect();
1100 assert_eq!(actual_hex, expected_hex);
1101 }
1102 _ => panic!("Expected K256 verifying key"),
1103 }
1104 }
1105
1106 #[test]
1107 fn parse_multikey_p256() {
1108 let multikey = include_str!("../../tests/fixtures/identity/multikey_p256.txt").trim();
1111
1112 let result = parse_multikey(multikey);
1113 assert!(result.is_ok());
1114
1115 let parsed = result.unwrap();
1116
1117 match &parsed.verifying_key {
1119 AnyVerifyingKey::P256(key) => {
1120 let sec1_bytes = key.to_encoded_point(true).as_bytes().to_vec();
1122 assert_eq!(sec1_bytes.len(), 33);
1123 let expected_hex =
1125 "026b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296";
1126 let actual_hex: String = sec1_bytes.iter().map(|b| format!("{b:02x}")).collect();
1127 assert_eq!(actual_hex, expected_hex);
1128 }
1129 _ => panic!("Expected P256 verifying key"),
1130 }
1131 }
1132
1133 #[test]
1134 fn parse_multikey_unsupported_curve() {
1135 let mut unsupported_bytes = vec![0x01, 0x00];
1137 unsupported_bytes.extend_from_slice(&[0; 33]); let multikey = multibase::encode(multibase::Base::Base58Btc, unsupported_bytes);
1140
1141 let result = parse_multikey(&multikey);
1142 assert!(result.is_err());
1143
1144 match result.unwrap_err() {
1145 IdentityError::UnsupportedCurve { codec_prefix: _ } => {}
1146 _ => panic!("Expected UnsupportedCurve error"),
1147 }
1148 }
1149
1150 #[test]
1151 fn parse_multikey_not_base58btc() {
1152 let mut key_bytes = vec![0xe7, 0x01];
1154 key_bytes.extend_from_slice(&[0; 33]);
1155 let hex_str: String = key_bytes.iter().map(|b| format!("{b:02x}")).collect();
1157 let multikey = format!("f{hex_str}");
1158
1159 let result = parse_multikey(&multikey);
1160 assert!(result.is_err());
1161
1162 match result.unwrap_err() {
1163 IdentityError::UnsupportedMultibase(_) => {}
1164 _ => panic!("Expected UnsupportedMultibase error"),
1165 }
1166 }
1167
1168 #[test]
1169 fn parse_multikey_accepts_did_key_prefix() {
1170 let bare = include_str!("../../tests/fixtures/identity/multikey_k256.txt").trim();
1174 let did_key = format!("did:key:{bare}");
1175
1176 let from_bare = parse_multikey(bare).expect("bare multikey should parse");
1177 let from_did_key = parse_multikey(&did_key).expect("did:key multikey should parse");
1178
1179 assert!(matches!(from_bare.verifying_key, AnyVerifyingKey::K256(_)));
1180 assert!(matches!(
1181 from_did_key.verifying_key,
1182 AnyVerifyingKey::K256(_)
1183 ));
1184 }
1185
1186 #[test]
1187 fn parse_multikey_wrong_length() {
1188 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);
1194
1195 let result = parse_multikey(&multikey);
1196 assert!(result.is_err());
1197
1198 match result.unwrap_err() {
1200 IdentityError::MultikeyLengthInvalid => {
1201 }
1203 e => panic!("Expected MultikeyLengthInvalid, got {e:?}"),
1204 }
1205 }
1206
1207 #[test]
1208 fn verify_prehash_k256_valid() {
1209 let signing_key = K256SigningKey::random(&mut k256::elliptic_curve::rand_core::OsRng);
1211 let verifying_key = signing_key.verifying_key();
1212
1213 let prehash = *b"01234567890123456789012345678901";
1215
1216 let signature = signing_key.sign_prehash(&prehash).expect("signing failed");
1218
1219 let any_key = AnyVerifyingKey::K256(*verifying_key);
1221 let any_sig = AnySignature::K256(signature);
1222
1223 assert!(any_key.verify_prehash(&prehash, &any_sig).is_ok());
1225 }
1226
1227 #[test]
1228 fn verify_prehash_p256_valid() {
1229 let signing_key = P256SigningKey::random(&mut p256::elliptic_curve::rand_core::OsRng);
1231 let verifying_key = signing_key.verifying_key();
1232
1233 let prehash = *b"01234567890123456789012345678901";
1235
1236 let signature = signing_key.sign_prehash(&prehash).expect("signing failed");
1238
1239 let any_key = AnyVerifyingKey::P256(*verifying_key);
1241 let any_sig = AnySignature::P256(signature);
1242
1243 assert!(any_key.verify_prehash(&prehash, &any_sig).is_ok());
1245 }
1246
1247 #[test]
1248 fn verify_prehash_curve_mismatch() {
1249 let k256_signing_key = K256SigningKey::random(&mut k256::elliptic_curve::rand_core::OsRng);
1251 let k256_key = AnyVerifyingKey::K256(*k256_signing_key.verifying_key());
1252
1253 let p256_signing_key = P256SigningKey::random(&mut p256::elliptic_curve::rand_core::OsRng);
1254 let prehash = *b"01234567890123456789012345678901";
1255 let p256_sig = p256_signing_key
1256 .sign_prehash(&prehash)
1257 .expect("signing failed");
1258 let p256_any_sig = AnySignature::P256(p256_sig);
1259
1260 assert!(k256_key.verify_prehash(&prehash, &p256_any_sig).is_err());
1262
1263 let p256_signing_key_2 =
1265 P256SigningKey::random(&mut p256::elliptic_curve::rand_core::OsRng);
1266 let p256_key = AnyVerifyingKey::P256(*p256_signing_key_2.verifying_key());
1267
1268 let k256_signing_key_2 =
1269 K256SigningKey::random(&mut k256::elliptic_curve::rand_core::OsRng);
1270 let k256_sig = k256_signing_key_2
1271 .sign_prehash(&prehash)
1272 .expect("signing failed");
1273 let k256_any_sig = AnySignature::K256(k256_sig);
1274
1275 assert!(p256_key.verify_prehash(&prehash, &k256_any_sig).is_err());
1277 }
1278
1279 #[tokio::test]
1280 async fn plc_history_parses_rotation_fixture() {
1281 let fixture_bytes =
1283 include_bytes!("../../tests/fixtures/identity/plc_audit_log_with_rotation.json");
1284 let mut responses = HashMap::new();
1285 responses.insert(
1286 "https://plc.directory/did:plc:test/log/audit".to_string(),
1287 Response::Http(200, fixture_bytes.to_vec()),
1288 );
1289 let http = FakeHttpClient { responses };
1290
1291 let did = Did("did:plc:test".to_string());
1292 let result = plc_history_for_fragment(&did, "atproto_label", &http)
1293 .await
1294 .expect("plc_history should succeed");
1295
1296 assert_eq!(result.len(), 2);
1298 assert_eq!(
1299 result[0].key_id,
1300 "z3u1HhU9Dn1R1TDe8kSmEMCrJ5B8t9K7c7N2L3xX7y"
1301 );
1302 assert!(!result[0].nullified);
1303 assert_eq!(
1304 result[1].key_id,
1305 "z3u1HhU9Dn1R1TDe8kSmEMCrJ5B8t9K7c7N2L3xX7z"
1306 );
1307 assert!(!result[1].nullified);
1308 }
1309
1310 #[tokio::test]
1311 async fn plc_history_dedupes_repeated_key() {
1312 let key = "did:key:zQ3shw6eSipD1cnrmmokVWvKCuE6Yc9j2jAjWJ9nWpuF4yQKV";
1316 let log = serde_json::json!([
1317 {"cid": "op3", "operation": {"verificationMethods": {"atproto_label": key}}},
1318 {"cid": "op2", "operation": {"verificationMethods": {"atproto_label": key}}},
1319 {"cid": "op1", "operation": {"verificationMethods": {"atproto_label": key}}},
1320 ]);
1321 let mut responses = HashMap::new();
1322 responses.insert(
1323 "https://plc.directory/did:plc:dedupe/log/audit".to_string(),
1324 Response::Http(200, serde_json::to_vec(&log).unwrap()),
1325 );
1326 let http = FakeHttpClient { responses };
1327
1328 let did = Did("did:plc:dedupe".to_string());
1329 let result = plc_history_for_fragment(&did, "atproto_label", &http)
1330 .await
1331 .expect("plc_history should succeed");
1332
1333 assert_eq!(result.len(), 1);
1334 assert_eq!(result[0].key_id, key);
1335 assert_eq!(result[0].operation_cid, "op3");
1337 }
1338
1339 #[tokio::test]
1340 #[should_panic(expected = "plc_history_for_fragment called with non-plc DID")]
1341 async fn plc_history_unsupported_method_errors() {
1342 let mut responses = HashMap::new();
1345 responses.insert(
1346 "https://plc.directory/did:web:example.com/log/audit".to_string(),
1347 Response::Http(200, b"[]".to_vec()),
1348 );
1349 let http = FakeHttpClient { responses };
1350
1351 let did = Did("did:web:example.com".to_string());
1352 let _result = plc_history_for_fragment(&did, "atproto_label", &http).await;
1353 }
1354
1355 #[tokio::test]
1356 async fn plc_history_transport_error_propagates() {
1357 let mut responses = HashMap::new();
1359 responses.insert(
1360 "https://plc.directory/did:plc:test/log/audit".to_string(),
1361 Response::Transport("connection refused".to_string()),
1362 );
1363 let http = FakeHttpClient { responses };
1364
1365 let did = Did("did:plc:test".to_string());
1366 let result = plc_history_for_fragment(&did, "atproto_label", &http).await;
1367
1368 assert!(result.is_err());
1369 match result.unwrap_err() {
1371 IdentityError::DidResolutionFailed { status, body } => {
1372 assert_eq!(status, 0);
1373 assert_eq!(body, "Transport error: connection refused");
1374 }
1375 e => panic!("Expected DidResolutionFailed with status 0, got {e:?}"),
1376 }
1377 }
1378}