This repository has no description
1use k256::ecdsa::signature::Verifier as _;
2use k256::ecdsa::{Signature as K256Signature, VerifyingKey};
3use knot_runtime::Signature;
4use knot_types::crypto::PublicKey;
5use knot_types::{ActorId, ChangeId, CobId, KnotId, Oid, RepoDid, TypeName, UnixSeconds};
6use serde::Serialize;
7use serde::de::DeserializeOwned;
8
9use crate::error::PayloadError;
10
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum CobHome {
13 Repo(RepoDid),
14 Knot(KnotId),
15}
16
17impl CobHome {
18 pub fn as_str(&self) -> &str {
19 match self {
20 CobHome::Repo(did) => did.as_str(),
21 CobHome::Knot(knot) => knot.as_str(),
22 }
23 }
24
25 fn kind(&self) -> &'static str {
26 match self {
27 CobHome::Repo(_) => "repo",
28 CobHome::Knot(_) => "knot",
29 }
30 }
31}
32
33impl From<&RepoDid> for CobHome {
34 fn from(did: &RepoDid) -> Self {
35 CobHome::Repo(did.clone())
36 }
37}
38
39impl From<&KnotId> for CobHome {
40 fn from(knot: &KnotId) -> Self {
41 CobHome::Knot(knot.clone())
42 }
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct Payload(Vec<u8>);
47
48impl Payload {
49 pub fn new(bytes: Vec<u8>) -> Self {
50 Self(bytes)
51 }
52
53 pub fn as_bytes(&self) -> &[u8] {
54 &self.0
55 }
56}
57
58pub trait ChangePayload: Serialize + DeserializeOwned + Sized {
59 const TYPE: &'static str;
60
61 fn type_name() -> TypeName {
62 TypeName::new(Self::TYPE).expect("ChangePayload::TYPE must be valid nsid")
63 }
64
65 fn encode(&self) -> Result<Vec<u8>, PayloadError> {
66 serde_ipld_dagcbor::to_vec(self).map_err(|error| PayloadError::Encode(error.to_string()))
67 }
68
69 fn decode(bytes: &[u8]) -> Result<Self, PayloadError> {
70 serde_ipld_dagcbor::from_slice(bytes)
71 .map_err(|error| PayloadError::Decode(error.to_string()))
72 }
73}
74
75const SIGNING_CONTEXT: &str = "sh.tangled.knot.cob.change.v1";
76
77#[derive(Serialize)]
78struct SignedChange<'a> {
79 context: &'static str,
80 #[serde(rename = "homeKind")]
81 home_kind: &'static str,
82 home: &'a str,
83 revision: Oid,
84 parents: &'a [ChangeId],
85 #[serde(rename = "typeName")]
86 type_name: &'a TypeName,
87 author: &'a ActorId,
88 timestamp: UnixSeconds,
89 #[serde(skip_serializing_if = "Option::is_none")]
90 object: Option<CobId>,
91}
92
93pub(crate) fn signing_bytes(
94 home: &CobHome,
95 revision: Oid,
96 parents: &[ChangeId],
97 type_name: &TypeName,
98 author: &ActorId,
99 timestamp: UnixSeconds,
100 object: Option<CobId>,
101) -> Vec<u8> {
102 let view = SignedChange {
103 context: SIGNING_CONTEXT,
104 home_kind: home.kind(),
105 home: home.as_str(),
106 revision,
107 parents,
108 type_name,
109 author,
110 timestamp,
111 object,
112 };
113 serde_ipld_dagcbor::to_vec(&view).expect("change signing view always encodes")
114}
115
116pub(crate) fn object_binding(parents: &[ChangeId], object: Option<CobId>) -> Option<CobId> {
117 if parents.is_empty() { None } else { object }
118}
119
120#[allow(clippy::too_many_arguments)]
121pub(crate) fn verify_signature(
122 home: &CobHome,
123 revision: Oid,
124 parents: &[ChangeId],
125 type_name: &TypeName,
126 author: &ActorId,
127 timestamp: UnixSeconds,
128 object: Option<CobId>,
129 signature: &[u8],
130) -> bool {
131 let Ok(public) = PublicKey::decode(author.as_str()) else {
132 return false;
133 };
134 let Ok(verifying) = public.to_k256() else {
135 return false;
136 };
137 let Ok(signature) = K256Signature::from_slice(signature) else {
138 return false;
139 };
140 let message = signing_bytes(
141 home,
142 revision,
143 parents,
144 type_name,
145 author,
146 timestamp,
147 object_binding(parents, object),
148 );
149 VerifyingKey::from(&verifying)
150 .verify(&message, &signature)
151 .is_ok()
152}
153
154#[derive(Debug, Clone, PartialEq, Eq)]
155pub struct Change {
156 pub id: ChangeId,
157 pub revision: Oid,
158 pub parents: Vec<ChangeId>,
159 pub type_name: TypeName,
160 pub author: ActorId,
161 pub signature: Signature,
162 pub payload: Payload,
163 pub timestamp: UnixSeconds,
164}
165
166impl Change {
167 pub fn payload(&self) -> &[u8] {
168 self.payload.as_bytes()
169 }
170
171 pub fn sort_key(&self) -> (UnixSeconds, ChangeId) {
172 (self.timestamp, self.id)
173 }
174
175 pub fn verify(&self, home: &CobHome, expected_author: &ActorId, object: Option<CobId>) -> bool {
176 &self.author == expected_author
177 && verify_signature(
178 home,
179 self.revision,
180 &self.parents,
181 &self.type_name,
182 &self.author,
183 self.timestamp,
184 object,
185 self.signature.as_bytes(),
186 )
187 }
188}
189
190#[cfg(test)]
191mod tests {
192 use knot_runtime::{K256Signer, SeededEntropy, Signer};
193
194 use super::*;
195
196 fn type_name() -> TypeName {
197 TypeName::new("sh.tangled.test.tag").unwrap()
198 }
199
200 fn cob_home() -> CobHome {
201 CobHome::from(&RepoDid::new("did:plc:squid").unwrap())
202 }
203
204 fn signed_change(
205 signer: &K256Signer,
206 revision: Oid,
207 parents: Vec<ChangeId>,
208 timestamp: i64,
209 ) -> Change {
210 let author = ActorId::from_secp256k1(signer.public_key().as_bytes());
211 let timestamp = UnixSeconds::new(timestamp);
212 let bytes = signing_bytes(
213 &cob_home(),
214 revision,
215 &parents,
216 &type_name(),
217 &author,
218 timestamp,
219 object_binding(&parents, None),
220 );
221 Change {
222 id: ChangeId::new(Oid::null()),
223 revision,
224 parents,
225 type_name: type_name(),
226 author,
227 signature: signer.sign(&bytes),
228 payload: Payload::new(Vec::new()),
229 timestamp,
230 }
231 }
232
233 #[test]
234 fn signing_bytes_stay_byte_stable() {
235 let author = {
236 let mut compressed = [0u8; 33];
237 compressed[0] = 0x02;
238 compressed[1] = 0x09;
239 ActorId::from_secp256k1(&compressed)
240 };
241 let parents = vec![ChangeId::new(
242 Oid::from_hex("2222222222222222222222222222222222222222").unwrap(),
243 )];
244 let object = Some(CobId::new(
245 Oid::from_hex("3333333333333333333333333333333333333333").unwrap(),
246 ));
247 let bytes = signing_bytes(
248 &cob_home(),
249 Oid::from_hex("1111111111111111111111111111111111111111").unwrap(),
250 &parents,
251 &type_name(),
252 &author,
253 UnixSeconds::new(1_700_000_000),
254 object_binding(&parents, object),
255 );
256 assert_eq!(
257 knot_types::lowercase_hex(&bytes),
258 "a964686f6d656d6469643a706c633a737175696466617574686f7278317a513373684e317652664257527847397234564c3251796466474e675955715a5a385836743971535359774b636a644a50666f626a65637478283333333333333333333333333333333333333333333333333333333333333333333333333333333367636f6e74657874781d73682e74616e676c65642e6b6e6f742e636f622e6368616e67652e763167706172656e74738178283232323232323232323232323232323232323232323232323232323232323232323232323232323268686f6d654b696e64647265706f687265766973696f6e78283131313131313131313131313131313131313131313131313131313131313131313131313131313168747970654e616d657373682e74616e676c65642e746573742e7461676974696d657374616d701a6553f100"
259 );
260 }
261
262 #[test]
263 fn verify_binds_the_author() {
264 let signer = K256Signer::generate(&SeededEntropy::new(13));
265 let stranger = K256Signer::generate(&SeededEntropy::new(14));
266 let revision = Oid::from_hex("6666666666666666666666666666666666666666").unwrap();
267 let change = signed_change(&signer, revision, Vec::new(), 1);
268 let stranger_actor = ActorId::from_secp256k1(stranger.public_key().as_bytes());
269 assert!(change.verify(&cob_home(), &change.author, None));
270 assert!(
271 !change.verify(&cob_home(), &stranger_actor, None),
272 "a valid signature under an unexpected expected-author is refused"
273 );
274 let foreign = Change {
275 author: stranger_actor,
276 ..change
277 };
278 assert!(
279 !foreign.verify(&cob_home(), &foreign.author, None),
280 "an author swapped to a stranger fails its own signature check"
281 );
282 }
283
284 #[test]
285 fn verify_rejects_a_tampered_transplanted_or_rehomed_change() {
286 let signer = K256Signer::generate(&SeededEntropy::new(10));
287 let revision = Oid::from_hex("1111111111111111111111111111111111111111").unwrap();
288 let genuine = signed_change(&signer, revision, Vec::new(), 1);
289 assert!(
290 genuine.verify(&cob_home(), &genuine.author, None),
291 "genuine root change verifies"
292 );
293
294 let mutators: Vec<fn(Change) -> Change> = vec![
295 |change| {
296 let mut bytes = change.signature.as_bytes().to_vec();
297 bytes[0] ^= 0xff;
298 Change {
299 signature: Signature::from_bytes(bytes),
300 ..change
301 }
302 },
303 |change| Change {
304 parents: vec![ChangeId::new(
305 Oid::from_hex("3333333333333333333333333333333333333333").unwrap(),
306 )],
307 ..change
308 },
309 |change| Change {
310 timestamp: UnixSeconds::new(9_999_999),
311 ..change
312 },
313 ];
314 mutators.into_iter().for_each(|mutate| {
315 let broken = mutate(genuine.clone());
316 assert!(
317 !broken.verify(&cob_home(), &broken.author, None),
318 "a tampered or transplanted change fails verification"
319 );
320 });
321
322 let other_home = CobHome::from(&RepoDid::new("did:plc:limpet").unwrap());
323 assert!(
324 !genuine.verify(&other_home, &genuine.author, None),
325 "change signed for one repo mustn't verify under another repo's home"
326 );
327
328 let author = ActorId::from_secp256k1(signer.public_key().as_bytes());
329 let parent =
330 ChangeId::new(Oid::from_hex("5555555555555555555555555555555555555555").unwrap());
331 let home = CobId::new(Oid::from_hex("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap());
332 let elsewhere =
333 CobId::new(Oid::from_hex("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb").unwrap());
334 let timestamp = UnixSeconds::new(1);
335 let bytes = signing_bytes(
336 &cob_home(),
337 revision,
338 &[parent],
339 &type_name(),
340 &author,
341 timestamp,
342 object_binding(&[parent], Some(home)),
343 );
344 let bound = Change {
345 id: ChangeId::new(Oid::null()),
346 revision,
347 parents: vec![parent],
348 type_name: type_name(),
349 author,
350 signature: signer.sign(&bytes),
351 payload: Payload::new(Vec::new()),
352 timestamp,
353 };
354 assert!(bound.verify(&cob_home(), &bound.author, Some(home)));
355 assert!(
356 !bound.verify(&cob_home(), &bound.author, Some(elsewhere)),
357 "a non-root change is bound to its object"
358 );
359 assert!(!bound.verify(&cob_home(), &bound.author, None));
360 }
361}