use std::net::SocketAddr;
use std::time::Duration;
use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};
use serde_json::json;
use url::Url;
use k256::elliptic_curve::rand_core;
use crate::commands::test::labeler::create_report::did_doc_server::DidDocServer;
use crate::common::identity::{AnySigningKey, Did, encode_multikey};
use crate::common::jwt::{self, JwtClaims, JwtHeader};
struct GetrandomRng;
impl rand_core::RngCore for GetrandomRng {
fn next_u32(&mut self) -> u32 {
let mut b = [0u8; 4];
getrandom::getrandom(&mut b).expect("OS CSPRNG");
u32::from_le_bytes(b)
}
fn next_u64(&mut self) -> u64 {
let mut b = [0u8; 8];
getrandom::getrandom(&mut b).expect("OS CSPRNG");
u64::from_le_bytes(b)
}
fn fill_bytes(&mut self, dest: &mut [u8]) {
getrandom::getrandom(dest).expect("OS CSPRNG");
}
fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand_core::Error> {
getrandom::getrandom(dest).map_err(|_| rand_core::Error::new("getrandom failed"))
}
}
impl rand_core::CryptoRng for GetrandomRng {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, clap::ValueEnum)]
pub enum SelfMintCurve {
#[default]
Es256k,
Es256,
}
pub struct SelfMintSigner {
signing_key: AnySigningKey,
issuer_did: Did,
did_doc_server: DidDocServer,
}
impl SelfMintSigner {
pub async fn spawn(curve: SelfMintCurve) -> std::io::Result<Self> {
let signing_key = match curve {
SelfMintCurve::Es256k => {
AnySigningKey::K256(k256::ecdsa::SigningKey::random(&mut GetrandomRng))
}
SelfMintCurve::Es256 => {
AnySigningKey::P256(p256::ecdsa::SigningKey::random(&mut GetrandomRng))
}
};
let verifying_key = signing_key.verifying_key();
let multikey = encode_multikey(&verifying_key);
let issuer_capture: std::sync::Arc<std::sync::Mutex<Option<Did>>> =
std::sync::Arc::new(std::sync::Mutex::new(None));
let issuer_capture_clone = issuer_capture.clone();
let multikey_for_builder = multikey.clone();
let server = DidDocServer::spawn(move |addr| {
let did = did_for(addr);
let did_doc = json!({
"@context": ["https://www.w3.org/ns/did/v1"],
"id": did.0,
"alsoKnownAs": [],
"verificationMethod": [{
"id": format!("{}#atproto", did.0),
"type": "Multikey",
"controller": did.0,
"publicKeyMultibase": multikey_for_builder,
}],
"service": [],
});
let bytes = serde_json::to_vec(&did_doc).expect("static JSON serializes");
*issuer_capture_clone.lock().unwrap() = Some(did);
bytes
})
.await?;
let issuer_did = issuer_capture
.lock()
.unwrap()
.take()
.expect("body-builder runs synchronously before spawn returns");
Ok(Self {
signing_key,
issuer_did,
did_doc_server: server,
})
}
pub fn issuer_did(&self) -> &Did {
&self.issuer_did
}
pub fn did_doc_url(&self) -> url::Url {
let mut u = base_url(self.did_doc_server.local_addr());
u.set_path("/.well-known/did.json");
u
}
pub fn sign_jwt(&self, mut claims: JwtClaims) -> String {
claims.iss = self.issuer_did.0.clone();
let header = JwtHeader::for_signing_key(&self.signing_key);
jwt::encode_compact(&header, &claims, &self.signing_key)
.expect("encode_compact is infallible for well-formed structs")
}
pub fn valid_claims_template(
&self,
labeler_did: &Did,
lxm: &str,
now_unix_secs: i64,
exp_after: Duration,
) -> JwtClaims {
JwtClaims {
iss: self.issuer_did.0.clone(),
aud: labeler_did.0.clone(),
exp: now_unix_secs + exp_after.as_secs() as i64,
iat: now_unix_secs,
lxm: lxm.to_string(),
jti: crate::commands::test::labeler::create_report::sentinel::new_run_id(),
}
}
}
pub(crate) fn did_for(addr: SocketAddr) -> Did {
assert!(addr.is_ipv4(), "self-mint DidDocServer is IPv4-only");
let host = addr.ip().to_string();
let port = addr.port();
let encoded_hostport = format!("{host}{}{port}", utf8_percent_encode(":", NON_ALPHANUMERIC));
Did(format!("did:web:{encoded_hostport}"))
}
pub(crate) fn base_url(addr: SocketAddr) -> Url {
Url::parse(&format!("http://{addr}")).expect("SocketAddr Display is always a valid authority")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::common::identity::{AnyVerifyingKey, parse_multikey};
use crate::common::jwt::verify_compact;
#[test]
fn self_mint_did_encodes_colon() {
let addr: SocketAddr = "127.0.0.1:5000".parse().unwrap();
let did = did_for(addr);
assert_eq!(did.0, "did:web:127.0.0.1%3A5000");
}
#[test]
fn self_mint_base_url_uses_http() {
let addr: SocketAddr = "127.0.0.1:5000".parse().unwrap();
let url = base_url(addr);
assert_eq!(url.as_str(), "http://127.0.0.1:5000/");
}
async fn round_trip(curve: SelfMintCurve, expected_alg: &str) {
let signer = SelfMintSigner::spawn(curve).await.expect("spawn");
let url = signer.did_doc_url();
let client = reqwest::Client::new();
let resp = client.get(url).send().await.expect("http");
assert_eq!(resp.status(), 200);
let doc: serde_json::Value = resp.json().await.expect("json");
assert_eq!(
doc["id"],
serde_json::Value::String(signer.issuer_did().0.clone())
);
let vm = doc["verificationMethod"][0].clone();
let multikey = vm["publicKeyMultibase"]
.as_str()
.expect("multikey")
.to_string();
let parsed = parse_multikey(&multikey).expect("parse multikey");
let vkey: AnyVerifyingKey = parsed.verifying_key;
let claims = signer.valid_claims_template(
&Did("did:plc:aaa22222222222222222bbbbbb".to_string()),
"com.atproto.moderation.createReport",
1_776_000_000,
Duration::from_secs(60),
);
let token = signer.sign_jwt(claims.clone());
let (header, decoded_claims) = verify_compact(&token, &vkey).expect("verify");
assert_eq!(header.alg, expected_alg);
assert_eq!(decoded_claims.iss, signer.issuer_did().0);
assert_eq!(decoded_claims.aud, claims.aud);
assert_eq!(decoded_claims.lxm, "com.atproto.moderation.createReport");
}
#[tokio::test]
async fn self_mint_signer_es256k_round_trips() {
round_trip(SelfMintCurve::Es256k, "ES256K").await;
}
#[tokio::test]
async fn self_mint_signer_es256_round_trips() {
round_trip(SelfMintCurve::Es256, "ES256").await;
}
}