This repository has no description
6.3 kB
199 lines
1use std::borrow::Cow;
2use std::sync::{Arc, Mutex};
3
4use base64::Engine;
5use base64::engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD};
6use bytes::Bytes;
7use http::StatusCode;
8use k256::ecdsa::{Signature, SigningKey, signature::Signer};
9use knot_runtime::{HttpResponse, K256Signer, ManualClock, SeededEntropy, UnixMicros};
10use knot_types::crypto::{KeyCodec, PublicKey as CryptoKey};
11use knot_types::{AccountDid, KnotId, Nsid, OwnerDid, RepoDid, RepoRkey};
12use serde_json::Value;
13use url::Url;
14
15use crate::{MintNonce, ServiceJwt};
16
17pub(crate) const SQUID: &str = "did:plc:squid";
18pub(crate) const LIMPET: &str = "did:plc:limpet";
19pub(crate) const KNOT: &str = "did:web:nel.pet";
20pub(crate) const METHOD: &str = "sh.tangled.knot.addMember";
21
22pub(crate) fn did(value: &str) -> AccountDid {
23 AccountDid::new(value).unwrap()
24}
25
26pub(crate) fn repo_did(value: &str) -> RepoDid {
27 RepoDid::new(value).unwrap()
28}
29
30pub(crate) fn owner_did(value: &str) -> OwnerDid {
31 OwnerDid::new(value).unwrap()
32}
33
34pub(crate) fn knot_did(value: &str) -> KnotId {
35 KnotId::new(value).unwrap()
36}
37
38pub(crate) fn handle(value: &str) -> knot_types::Handle {
39 knot_types::Handle::new_owned(value).unwrap()
40}
41
42pub(crate) fn member_method() -> Nsid {
43 Nsid::new_owned(METHOD).unwrap()
44}
45
46pub(crate) fn plc() -> crate::PlcDirectory {
47 crate::PlcDirectory::new(Url::parse("https://plc.directory/").unwrap()).unwrap()
48}
49
50pub(crate) fn clock() -> ManualClock {
51 ManualClock::new(UnixMicros::new(1_000_000_000))
52}
53
54pub(crate) fn signer(seed: u8) -> SigningKey {
55 SigningKey::from_bytes(&[seed; 32].into()).unwrap()
56}
57
58pub(crate) fn sec1(signing: &SigningKey) -> Vec<u8> {
59 signing
60 .verifying_key()
61 .to_encoded_point(true)
62 .as_bytes()
63 .to_vec()
64}
65
66pub(crate) fn k256_public(signing: &SigningKey) -> CryptoKey<'static> {
67 CryptoKey {
68 codec: KeyCodec::Secp256k1,
69 bytes: Cow::Owned(sec1(signing)),
70 }
71}
72
73pub(crate) enum MethodKind {
74 Multikey,
75 LegacyK256,
76 LegacyP256,
77 None,
78}
79
80pub(crate) struct DocSpec<'a> {
81 pub id: &'a str,
82 pub signing: &'a SigningKey,
83 pub handle: &'a str,
84 pub pds: &'a str,
85 pub method: MethodKind,
86}
87
88pub(crate) fn did_doc(spec: DocSpec) -> Bytes {
89 let raw = sec1(spec.signing);
90 let legacy = || format!("z{}", bs58::encode(&raw).into_string());
91 let method_entry: Option<(&'static str, String)> = match spec.method {
92 MethodKind::Multikey => Some(("Multikey", knot_types::crypto::multikey(0xe7, &raw))),
93 MethodKind::LegacyK256 => Some(("EcdsaSecp256k1VerificationKey2019", legacy())),
94 MethodKind::LegacyP256 => Some(("EcdsaSecp256r1VerificationKey2019", legacy())),
95 MethodKind::None => None,
96 };
97 let verification_method: Value = match method_entry {
98 Some((kind, multibase)) => serde_json::json!([{
99 "id": format!("{}#atproto", spec.id),
100 "type": kind,
101 "controller": spec.id,
102 "publicKeyMultibase": multibase,
103 }]),
104 None => serde_json::json!([]),
105 };
106 let body = serde_json::json!({
107 "id": spec.id,
108 "alsoKnownAs": [format!("at://{}", spec.handle)],
109 "verificationMethod": verification_method,
110 "service": [{
111 "id": "#atproto_pds",
112 "type": "AtprotoPersonalDataServer",
113 "serviceEndpoint": spec.pds,
114 }]
115 });
116 Bytes::from(serde_json::to_vec(&body).unwrap())
117}
118
119pub(crate) fn ssh_line(algo: &str, material: &[u8], comment: &str) -> String {
120 let ssh_string = |bytes: &[u8]| [&(bytes.len() as u32).to_be_bytes()[..], bytes].concat();
121 let blob = [ssh_string(algo.as_bytes()), ssh_string(material)].concat();
122 format!("{algo} {} {comment}", STANDARD.encode(blob))
123}
124
125pub(crate) fn list_body(lines: &[String], cursor: Option<&str>) -> Bytes {
126 let records: Vec<_> = lines
127 .iter()
128 .enumerate()
129 .map(|(index, line)| {
130 serde_json::json!({
131 "uri": format!("at://{SQUID}/sh.tangled.publicKey/{index}"),
132 "value": { "$type": "sh.tangled.publicKey", "key": line, "name": "k", "createdAt": "2026-06-08T00:00:00Z" }
133 })
134 })
135 .collect();
136 let cursor_field: Value = cursor.map_or(Value::Null, |value| serde_json::json!(value));
137 let body = serde_json::json!({ "records": records, "cursor": cursor_field });
138 Bytes::from(serde_json::to_vec(&body).unwrap())
139}
140
141pub(crate) fn ok(body: Bytes) -> HttpResponse {
142 status(StatusCode::OK, body)
143}
144
145pub(crate) fn status(status: StatusCode, body: Bytes) -> HttpResponse {
146 HttpResponse {
147 status,
148 headers: http::HeaderMap::new(),
149 body,
150 }
151}
152
153pub(crate) fn mint(signing: &SigningKey, claims: &Value) -> ServiceJwt {
154 mint_with_header(signing, br#"{"alg":"ES256K","typ":"JWT"}"#, claims)
155}
156
157pub(crate) fn mint_with_header(signing: &SigningKey, header: &[u8], claims: &Value) -> ServiceJwt {
158 let header_b64 = URL_SAFE_NO_PAD.encode(header);
159 let payload = URL_SAFE_NO_PAD.encode(serde_json::to_vec(claims).unwrap());
160 let signing_input = format!("{header_b64}.{payload}");
161 let signature: Signature = signing.sign(signing_input.as_bytes());
162 ServiceJwt::new(format!(
163 "{signing_input}.{}",
164 URL_SAFE_NO_PAD.encode(signature.to_bytes())
165 ))
166 .expect("minted token is structurally a JWT")
167}
168
169pub(crate) fn runtime_signer(seed: u64) -> K256Signer {
170 K256Signer::generate(&SeededEntropy::new(seed))
171}
172
173pub(crate) fn entropy(seed: u64) -> SeededEntropy {
174 SeededEntropy::new(seed)
175}
176
177pub(crate) fn repo_nonce(seed: u64) -> MintNonce {
178 MintNonce::mint(
179 &entropy(seed),
180 &owner_did("did:plc:nel"),
181 &RepoRkey::new("anemone").unwrap(),
182 )
183}
184
185pub(crate) fn member_pointer() -> knot_lexicons::sh_tangled::knot::member::Member {
186 knot_lexicons::sh_tangled::knot::member::Member {
187 created_at: knot_types::Datetime::raw_str("2026-06-11T00:00:00Z"),
188 domain: "knot.nel.pet".into(),
189 subject: knot_types::Did::new_owned("did:plc:lyna").unwrap(),
190 extra_data: None,
191 }
192}
193
194pub(crate) type UrlLog = Arc<Mutex<Vec<Url>>>;
195
196pub(crate) fn recorder() -> (UrlLog, UrlLog) {
197 let urls: UrlLog = Arc::new(Mutex::new(Vec::new()));
198 (urls.clone(), urls)
199}