1use bynk_project::lock::{base64_decode, base64_encode};
9use ring::signature::{ED25519, Ed25519KeyPair, KeyPair, UnparsedPublicKey};
10
11pub use bynk_registry_core::protocol::{
12 Config, IndexLine, KeyEntry, PROTOCOL, PROTOCOL_DELEGATION, Requirement, Signers,
13 WITHDRAWN_MAX, canonical, delegation_payload, fingerprint, key_bytes, revocation_payload,
14 rotation_payload,
15};
16
17pub trait ConfigTrust {
19 fn signers(&self, chain: &[String]) -> Signers;
21 fn chain_from(&self, trusted: &str) -> Option<Vec<String>>;
23 fn follow_chain(&self, trusted: &str) -> Option<&str>;
25}
26
27impl ConfigTrust for Config {
28 fn signers(&self, chain: &[String]) -> Signers {
29 self.signers_with(chain, &verify)
30 }
31
32 fn chain_from(&self, trusted: &str) -> Option<Vec<String>> {
33 self.chain_from_with(trusted, &verify)
34 }
35
36 fn follow_chain(&self, trusted: &str) -> Option<&str> {
37 self.follow_chain_with(trusted, &verify)
38 }
39}
40
41pub fn verify(key: &str, payload: &[u8], sig: &str) -> bool {
43 let (Some(pk), Some(sig)) = (key_bytes(key), base64_decode(sig)) else {
44 return false;
45 };
46 UnparsedPublicKey::new(&ED25519, pk)
47 .verify(payload, &sig)
48 .is_ok()
49}
50
51pub struct SigningKey(Ed25519KeyPair);
53
54impl SigningKey {
55 pub fn generate() -> Result<(SigningKey, Vec<u8>), String> {
57 let rng = ring::rand::SystemRandom::new();
58 let doc = Ed25519KeyPair::generate_pkcs8(&rng).map_err(|_| "could not generate a key")?;
59 let pair =
60 Ed25519KeyPair::from_pkcs8(doc.as_ref()).map_err(|_| "could not read the key")?;
61 Ok((SigningKey(pair), doc.as_ref().to_vec()))
62 }
63
64 pub fn from_pkcs8(doc: &[u8]) -> Result<SigningKey, String> {
66 Ed25519KeyPair::from_pkcs8(doc)
67 .map(SigningKey)
68 .map_err(|_| "the key file isn't an ed25519 PKCS#8 key".to_string())
69 }
70
71 pub fn public(&self) -> String {
73 format!("ed25519:{}", base64_encode(self.0.public_key().as_ref()))
74 }
75
76 pub fn sign(&self, payload: &[u8]) -> String {
78 base64_encode(self.0.sign(payload).as_ref())
79 }
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85
86 fn line(name: &str) -> IndexLine {
87 IndexLine {
88 name: name.into(),
89 version: "1.4.2".into(),
90 checksum: "sha256-abc".into(),
91 deps: vec![Requirement {
92 package: "acme/fx".into(),
93 range: "^2".into(),
94 }],
95 npm: vec![],
96 toolchain: None,
97 yanked: false,
98 withdrawn: None,
99 sig: String::new(),
100 }
101 }
102
103 #[test]
104 fn a_signature_binds_the_name_and_the_registry() {
105 let (key, _) = SigningKey::generate().unwrap();
106 let mut l = line("acme/money");
107 l.sig = key.sign(&l.payload("acme"));
108 assert!(verify(&key.public(), &l.payload("acme"), &l.sig));
109 assert!(!verify(&key.public(), &l.payload("globex"), &l.sig));
111 let mut other = l.clone();
112 other.name = "acme/payments".into();
113 assert!(!verify(&key.public(), &other.payload("acme"), &l.sig));
114 let mut yanked = l.clone();
116 yanked.yanked = true;
117 assert!(verify(&key.public(), &yanked.payload("acme"), &l.sig));
118 let mut withdrawn = yanked.clone();
120 withdrawn.withdrawn = Some("a takedown".into());
121 assert!(verify(&key.public(), &withdrawn.payload("acme"), &l.sig));
122 assert!(!l.render().contains("withdrawn"));
123 assert!(withdrawn.render().contains(r#""withdrawn":"a takedown""#));
124 assert_eq!(IndexLine::parse(&withdrawn.render()).unwrap(), withdrawn);
125 assert_eq!(IndexLine::parse(&l.render()).unwrap(), l);
127 }
128
129 #[test]
130 fn a_present_optional_field_must_be_a_string() {
131 use serde_json::json;
132 let base: serde_json::Value = serde_json::from_str(&line("acme/money").render()).unwrap();
133 let with = |k: &str, v: serde_json::Value| {
134 let mut b = base.clone();
135 b[k] = v;
136 IndexLine::parse(&b.to_string())
137 };
138 let ok = with("toolchain", json!(">=0.9")).unwrap();
140 assert_eq!(ok.toolchain.as_deref(), Some(">=0.9"));
141 assert_eq!(with("toolchain", json!(null)).unwrap().toolchain, None);
142 let ok = with("withdrawn", json!("a takedown")).unwrap();
143 assert_eq!(ok.withdrawn.as_deref(), Some("a takedown"));
144 let mut absent = base.clone();
145 absent.as_object_mut().unwrap().remove("toolchain");
146 assert_eq!(
147 IndexLine::parse(&absent.to_string()).unwrap().toolchain,
148 None
149 );
150 let tagged = json!({"kind": "Some", "value": "a takedown"});
152 assert_eq!(
153 with("withdrawn", tagged.clone()).unwrap_err(),
154 "acme/money 1.4.2's `withdrawn` is an object, not a string"
155 );
156 assert_eq!(
157 with("toolchain", json!({"kind": "None"})).unwrap_err(),
158 "acme/money 1.4.2's `toolchain` is an object, not a string or `null`"
159 );
160 assert!(with("toolchain", tagged).is_err());
161 assert_eq!(
162 with("withdrawn", json!(null)).unwrap_err(),
163 "acme/money 1.4.2's `withdrawn` is `null`, not a string"
164 );
165 assert!(with("withdrawn", json!(true)).is_err());
166 assert!(with("toolchain", json!(1)).is_err());
167 }
168
169 #[test]
170 fn the_canonical_payload_is_sorted_and_compact() {
171 let p = String::from_utf8(line("acme/money").payload("acme")).unwrap();
172 assert_eq!(
173 p,
174 r#"{"checksum":"sha256-abc","deps":[{"package":"acme/fx","range":"^2"}],"name":"acme/money","npm":[],"registry":"acme","toolchain":null,"version":"1.4.2"}"#
175 );
176 }
177
178 #[test]
179 fn the_key_chain_is_followed_from_the_trusted_key() {
180 let (k1, _) = SigningKey::generate().unwrap();
181 let (k2, _) = SigningKey::generate().unwrap();
182 let (k3, _) = SigningKey::generate().unwrap();
183 let config = Config {
184 id: "acme".into(),
185 dl: "dl".into(),
186 api: None,
187 token_auth: false,
188 keys: vec![
189 KeyEntry {
190 key: k1.public(),
191 sig: None,
192 },
193 KeyEntry {
194 key: k2.public(),
195 sig: Some(k1.sign(&rotation_payload("acme", &k2.public()))),
196 },
197 KeyEntry {
198 key: k3.public(),
199 sig: Some(k2.sign(&rotation_payload("acme", &k3.public()))),
200 },
201 ],
202 delegations: vec![],
203 revoked: vec![],
204 };
205 let fp1 = fingerprint(&k1.public()).unwrap();
206 assert_eq!(config.follow_chain(&fp1), Some(k3.public().as_str()));
207 let mut broken = config.clone();
209 broken.keys[2].sig = Some(k1.sign(&rotation_payload("acme", &k3.public())));
210 assert_eq!(broken.follow_chain(&fp1), None);
211 let (stranger, _) = SigningKey::generate().unwrap();
213 assert_eq!(
214 config.follow_chain(&fingerprint(&stranger.public()).unwrap()),
215 None
216 );
217 assert_eq!(Config::parse(config.render().as_bytes()).unwrap(), config);
218 }
219
220 #[test]
221 fn delegates_sign_lines_until_revoked_and_only_chain_keys_grant_or_revoke() {
222 let (root, _) = SigningKey::generate().unwrap();
223 let (d1, _) = SigningKey::generate().unwrap();
224 let (d2, _) = SigningKey::generate().unwrap();
225 let entry = |k: &SigningKey, sig: String| KeyEntry {
226 key: k.public(),
227 sig: Some(sig),
228 };
229 let mut config = Config {
230 id: "bynk".into(),
231 dl: "dl".into(),
232 api: None,
233 token_auth: false,
234 keys: vec![KeyEntry {
235 key: root.public(),
236 sig: None,
237 }],
238 delegations: vec![
239 entry(&d1, root.sign(&delegation_payload("bynk", &d1.public()))),
240 entry(&d2, d1.sign(&delegation_payload("bynk", &d2.public()))),
242 ],
243 revoked: vec![],
244 };
245 let chain = vec![root.public()];
246 let s = config.signers(&chain);
247 assert_eq!(s.keys, vec![root.public(), d1.public()]);
248 assert_eq!(s.ignored.len(), 1, "{:?}", s.ignored);
249
250 config.revoked = vec![entry(
252 &d1,
253 d1.sign(&revocation_payload("bynk", &d1.public())),
254 )];
255 assert!(config.signers(&chain).keys.contains(&d1.public()));
256 config.revoked = vec![entry(
257 &d1,
258 root.sign(&revocation_payload("bynk", &d1.public())),
259 )];
260 let s = config.signers(&chain);
261 assert_eq!(s.keys, vec![root.public()]);
262 assert_eq!(s.revoked, vec![d1.public()]);
263
264 let mut chain_revoked = config.clone();
267 chain_revoked.revoked = vec![entry(
268 &root,
269 root.sign(&revocation_payload("bynk", &root.public())),
270 )];
271 let s = chain_revoked.signers(&chain);
272 assert!(s.revoked.is_empty() && s.keys.contains(&root.public()));
273 assert!(
274 s.ignored.iter().any(|i| i.contains("only delegates")),
275 "{:?}",
276 s.ignored
277 );
278
279 let mut other = config.clone();
282 other.id = "acme".into();
283 other.revoked.clear();
284 assert_eq!(other.signers(&chain).keys, vec![root.public()]);
285
286 let text = config.render();
289 assert!(text.contains("\"protocol\": 2"), "{text}");
290 assert_eq!(Config::parse(text.as_bytes()).unwrap(), config);
291 let downgraded = text.replace("\"protocol\": 2", "\"protocol\": 1");
292 assert!(Config::parse(downgraded.as_bytes()).is_err());
293 config.delegations.clear();
294 config.revoked.clear();
295 assert!(config.render().contains("\"protocol\": 1"));
296 }
297}