···22//! server, and a reference curve. Exposes a single method for signing
33//! atproto service-auth JWTs with that identity.
4455+use std::net::SocketAddr;
56use std::time::Duration;
6788+use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};
79use serde_json::json;
1010+use url::Url;
811912// Reach `rand_core` through `k256`'s re-export so we pin the same version
1013// that `k256::ecdsa::SigningKey::random` expects.
1114use k256::elliptic_curve::rand_core;
12151316use crate::commands::test::labeler::create_report::did_doc_server::DidDocServer;
1414-use crate::commands::test::labeler::create_report::{self_mint_base_url, self_mint_did_for};
1517use crate::common::identity::{AnySigningKey, Did, encode_multikey};
1618use crate::common::jwt::{self, JwtClaims, JwtHeader};
1719···9193 let multikey_for_builder = multikey.clone();
92949395 let server = DidDocServer::spawn(move |addr| {
9494- let did = self_mint_did_for(addr);
9696+ let did = did_for(addr);
9597 let did_doc = json!({
9698 "@context": ["https://www.w3.org/ns/did/v1"],
9799 "id": did.0,
···130132131133 /// URL the labeler will fetch to resolve the DID document.
132134 pub fn did_doc_url(&self) -> url::Url {
133133- let mut u = self_mint_base_url(self.did_doc_server.local_addr());
135135+ let mut u = base_url(self.did_doc_server.local_addr());
134136 u.set_path("/.well-known/did.json");
135137 u
136138 }
···166168 }
167169}
168170171171+/// Construct `did:web:127.0.0.1%3A{port}` for a self-mint identity bound
172172+/// to the given local `SocketAddr`. The `:` between the IP and the port is
173173+/// percent-encoded per atproto did:web rules.
174174+///
175175+/// Uses the `SocketAddr` IP literally (typically `127.0.0.1`). IPv6
176176+/// loopback would produce `did:web:::1%3A{port}` which the atproto did
177177+/// syntax regex rejects; for v1 the self-mint server is IPv4-only.
178178+pub(crate) fn did_for(addr: SocketAddr) -> Did {
179179+ assert!(addr.is_ipv4(), "self-mint DidDocServer is IPv4-only");
180180+ let host = addr.ip().to_string();
181181+ let port = addr.port();
182182+ // Percent-encode the `:` (and, defensively, any other non-alphanumeric)
183183+ // with the standard set. For the `127.0.0.1:{port}` case this yields
184184+ // exactly `127.0.0.1%3A{port}`.
185185+ let encoded_hostport = format!("{host}{}{port}", utf8_percent_encode(":", NON_ALPHANUMERIC));
186186+ Did(format!("did:web:{encoded_hostport}"))
187187+}
188188+189189+/// Base URL the labeler uses to fetch the self-mint DID document:
190190+/// `http://127.0.0.1:{port}`.
191191+pub(crate) fn base_url(addr: SocketAddr) -> Url {
192192+ Url::parse(&format!("http://{addr}")).expect("SocketAddr Display is always a valid authority")
193193+}
194194+169195#[cfg(test)]
170196mod tests {
171197 use super::*;
172198 use crate::common::identity::{AnyVerifyingKey, parse_multikey};
173199 use crate::common::jwt::verify_compact;
200200+201201+ #[test]
202202+ fn self_mint_did_encodes_colon() {
203203+ let addr: SocketAddr = "127.0.0.1:5000".parse().unwrap();
204204+ let did = did_for(addr);
205205+ assert_eq!(did.0, "did:web:127.0.0.1%3A5000");
206206+ }
207207+208208+ #[test]
209209+ fn self_mint_base_url_uses_http() {
210210+ let addr: SocketAddr = "127.0.0.1:5000".parse().unwrap();
211211+ let url = base_url(addr);
212212+ assert_eq!(url.as_str(), "http://127.0.0.1:5000/");
213213+ }
174214175215 async fn round_trip(curve: SelfMintCurve, expected_alg: &str) {
176216 let signer = SelfMintSigner::spawn(curve).await.expect("spawn");