1use ed25519_dalek::{Signature, Verifier, VerifyingKey};
2
3use crate::chain::Clock;
4use crate::crypto::{hasher_cert_fp, hasher_cert_sig};
5use crate::error::A1Error;
6use crate::identity::Signer;
7use crate::intent::IntentHash;
8use crate::registry::fresh_nonce;
9use crate::SubScopeProof;
10
11#[cfg(feature = "wire")]
12use crate::cert_extensions::CertExtensions;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16#[repr(u8)]
17pub enum CertVersion {
18 V1 = 1,
19}
20
21impl CertVersion {
22 pub fn as_u8(self) -> u8 {
23 self as u8
24 }
25}
26
27pub const CERT_VERSION: u8 = 1;
28
29#[derive(Clone, Debug)]
30#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
31pub struct DelegationCert {
32 pub version: u8,
33 pub delegator_pk: VerifyingKey,
34 pub delegate_pk: VerifyingKey,
35 pub scope_root: IntentHash,
36 pub scope_proof: SubScopeProof,
37 pub nonce: [u8; 16],
38 pub issued_at: u64,
39 pub expiration_unix: u64,
40 pub max_depth: u8,
41 #[cfg(not(feature = "wire"))]
42 #[cfg_attr(feature = "serde", serde(skip))]
43 pub extensions_hash: Option<[u8; 32]>,
44 #[cfg(feature = "wire")]
45 #[serde(default)]
46 pub extensions: CertExtensions,
47 pub signature: Signature,
48}
49
50impl DelegationCert {
51 #[allow(clippy::too_many_arguments)]
52 #[inline(always)]
53 pub fn signable_bytes(
54 version: u8,
55 delegator_pk: &VerifyingKey,
56 delegate_pk: &VerifyingKey,
57 scope_root: &IntentHash,
58 scope_proof: &SubScopeProof,
59 nonce: &[u8; 16],
60 issued_at: u64,
61 expiration_unix: u64,
62 max_depth: u8,
63 ext_commitment: &[u8; 32],
64 ) -> Vec<u8> {
65 let mut h = hasher_cert_sig(version);
67 h.update(b"a1::dyolo::cert::sig::v2.8.0");
68 h.update(delegator_pk.as_bytes());
69 h.update(delegate_pk.as_bytes());
70 h.update(scope_root);
71 h.update(&scope_proof.commitment());
72 h.update(nonce);
73 h.update(&issued_at.to_be_bytes());
74 h.update(&expiration_unix.to_be_bytes());
75 h.update(&[max_depth]);
76 h.update(ext_commitment);
77 h.finalize().as_bytes().to_vec()
80 }
81
82 #[allow(clippy::too_many_arguments)]
83 pub(crate) fn issue(
84 delegator: &dyn Signer,
85 delegate_pk: VerifyingKey,
86 scope_root: IntentHash,
87 scope_proof: SubScopeProof,
88 nonce: [u8; 16],
89 issued_at: u64,
90 expiration_unix: u64,
91 max_depth: u8,
92 #[cfg(feature = "wire")] extensions: CertExtensions,
93 #[cfg(not(feature = "wire"))] extensions_hash: Option<[u8; 32]>,
94 ) -> Self {
95 let delegator_pk = delegator.verifying_key();
96
97 #[cfg(feature = "wire")]
98 let ext_commit = extensions.commitment();
99 #[cfg(not(feature = "wire"))]
100 let ext_commit = extensions_hash.unwrap_or_else(|| {
101 let mut h = crate::crypto::derive_key("a1::dyolo::cert::ext::v2.8.0", CERT_VERSION);
102 h.update(&0u64.to_le_bytes());
103 h.finalize().into()
104 });
105
106 let msg = Self::signable_bytes(
107 CERT_VERSION,
108 &delegator_pk,
109 &delegate_pk,
110 &scope_root,
111 &scope_proof,
112 &nonce,
113 issued_at,
114 expiration_unix,
115 max_depth,
116 &ext_commit,
117 );
118 Self {
119 version: CERT_VERSION,
120 delegator_pk,
121 delegate_pk,
122 scope_root,
123 scope_proof,
124 nonce,
125 issued_at,
126 expiration_unix,
127 max_depth,
128 #[cfg(not(feature = "wire"))]
129 extensions_hash,
130 #[cfg(feature = "wire")]
131 extensions,
132 signature: delegator.sign_message(&msg),
133 }
134 }
135
136 pub fn verify_signature(&self) -> bool {
137 #[cfg(feature = "wire")]
138 let ext_commit = self.extensions.commitment();
139 #[cfg(not(feature = "wire"))]
140 let ext_commit = self.extensions_hash.unwrap_or_else(|| {
141 let mut h = crate::crypto::derive_key("a1::dyolo::cert::ext::v2.8.0", self.version);
142 h.update(&0u64.to_le_bytes());
143 h.finalize().into()
144 });
145
146 let msg = Self::signable_bytes(
147 self.version,
148 &self.delegator_pk,
149 &self.delegate_pk,
150 &self.scope_root,
151 &self.scope_proof,
152 &self.nonce,
153 self.issued_at,
154 self.expiration_unix,
155 self.max_depth,
156 &ext_commit,
157 );
158 self.delegator_pk.verify(&msg, &self.signature).is_ok()
159 }
160
161 #[must_use]
162 pub fn fingerprint(&self) -> [u8; 32] {
163 let mut h = hasher_cert_fp(self.version);
164 h.update(b"a1::dyolo::cert::fp::v2.8.0");
165 h.update(&self.signature.to_bytes());
166 h.finalize().into()
167 }
168
169 pub fn fingerprint_hex(&self) -> String {
170 hex::encode(self.fingerprint())
171 }
172
173 pub fn ttl_secs(&self) -> u64 {
174 self.expiration_unix.saturating_sub(self.issued_at)
175 }
176}
177
178pub struct CertBuilder {
181 delegate_pk: VerifyingKey,
182 scope_root: IntentHash,
183 scope_proof: SubScopeProof,
184 nonce: [u8; 16],
185 issued_at: u64,
186 expiration_unix: u64,
187 max_depth: u8,
188 #[cfg(feature = "wire")]
189 extensions: CertExtensions,
190 #[cfg(not(feature = "wire"))]
191 extensions_hash: Option<[u8; 32]>,
192}
193
194impl CertBuilder {
195 pub fn new(
196 delegate_pk: VerifyingKey,
197 scope_root: IntentHash,
198 issued_at: u64,
199 expiration_unix: u64,
200 ) -> Self {
201 Self {
202 delegate_pk,
203 scope_root,
204 scope_proof: SubScopeProof::full_passthrough(),
205 nonce: fresh_nonce(),
206 issued_at,
207 expiration_unix,
208 max_depth: 16,
209 #[cfg(feature = "wire")]
210 extensions: CertExtensions::new(),
211 #[cfg(not(feature = "wire"))]
212 extensions_hash: None,
213 }
214 }
215
216 pub fn scope_proof(mut self, proof: SubScopeProof) -> Self {
217 self.scope_proof = proof;
218 self
219 }
220
221 pub fn nonce(mut self, nonce: [u8; 16]) -> Self {
222 self.nonce = nonce;
223 self
224 }
225
226 pub fn max_depth(mut self, depth: u8) -> Self {
227 self.max_depth = depth;
228 self
229 }
230
231 #[cfg(feature = "wire")]
232 pub fn extensions(mut self, ext: CertExtensions) -> Self {
233 self.extensions = ext;
234 self
235 }
236
237 #[cfg(not(feature = "wire"))]
238 pub fn extensions_hash(mut self, hash: [u8; 32]) -> Self {
239 self.extensions_hash = Some(hash);
240 self
241 }
242
243 pub fn build(self, delegator: &dyn Signer) -> Result<DelegationCert, A1Error> {
244 if self.issued_at >= self.expiration_unix {
245 return Err(A1Error::WireFormatError(format!(
246 "issued_at ({}) must be strictly less than expiration_unix ({})",
247 self.issued_at, self.expiration_unix
248 )));
249 }
250 Ok(DelegationCert::issue(
251 delegator,
252 self.delegate_pk,
253 self.scope_root,
254 self.scope_proof,
255 self.nonce,
256 self.issued_at,
257 self.expiration_unix,
258 self.max_depth,
259 #[cfg(feature = "wire")]
260 self.extensions,
261 #[cfg(not(feature = "wire"))]
262 self.extensions_hash,
263 ))
264 }
265
266 pub fn sign(self, delegator: &dyn Signer) -> DelegationCert {
267 self.build(delegator)
268 .expect("invalid certificate configuration: issued_at must be before expiration_unix")
269 }
270
271 #[cfg(feature = "async")]
272 #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
273 pub async fn build_async(
274 self,
275 delegator: &dyn crate::identity::AsyncSigner,
276 ) -> Result<DelegationCert, A1Error> {
277 if self.issued_at >= self.expiration_unix {
278 return Err(A1Error::WireFormatError(format!(
279 "issued_at ({}) must be strictly less than expiration_unix ({})",
280 self.issued_at, self.expiration_unix
281 )));
282 }
283 let delegator_pk = delegator.verifying_key();
284
285 #[cfg(feature = "wire")]
286 let ext_commit = self.extensions.commitment();
287 #[cfg(not(feature = "wire"))]
288 let ext_commit = self.extensions_hash.unwrap_or_else(|| {
289 let mut h = crate::crypto::derive_key("a1::dyolo::cert::ext::v2.8.0", CERT_VERSION);
290 h.update(&0u64.to_le_bytes());
291 h.finalize().into()
292 });
293
294 let msg = DelegationCert::signable_bytes(
295 CERT_VERSION,
296 &delegator_pk,
297 &self.delegate_pk,
298 &self.scope_root,
299 &self.scope_proof,
300 &self.nonce,
301 self.issued_at,
302 self.expiration_unix,
303 self.max_depth,
304 &ext_commit,
305 );
306
307 Ok(DelegationCert {
308 version: CERT_VERSION,
309 delegator_pk,
310 delegate_pk: self.delegate_pk,
311 scope_root: self.scope_root,
312 scope_proof: self.scope_proof,
313 nonce: self.nonce,
314 issued_at: self.issued_at,
315 expiration_unix: self.expiration_unix,
316 max_depth: self.max_depth,
317 #[cfg(not(feature = "wire"))]
318 extensions_hash: self.extensions_hash,
319 #[cfg(feature = "wire")]
320 extensions: self.extensions,
321 signature: delegator.sign_message(&msg).await,
322 })
323 }
324
325 #[cfg(feature = "async")]
326 #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
327 pub async fn sign_async(self, delegator: &dyn crate::identity::AsyncSigner) -> DelegationCert {
328 self.build_async(delegator)
329 .await
330 .expect("invalid certificate configuration: issued_at must be before expiration_unix")
331 }
332}
333
334#[derive(Debug, Clone)]
356#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
357pub struct CertBundle {
358 pub certs: Vec<DelegationCert>,
359 pub issued_at: u64,
360}
361
362impl CertBundle {
363 pub fn issue(delegator: &dyn Signer, issued_at: u64, builders: Vec<CertBuilder>) -> Self {
364 let certs = builders.into_iter().map(|b| b.sign(delegator)).collect();
365 Self { certs, issued_at }
366 }
367
368 pub fn from_certs(certs: Vec<DelegationCert>, clock: &dyn Clock) -> Self {
369 let issued_at = clock.unix_now();
370 Self { certs, issued_at }
371 }
372
373 pub fn len(&self) -> usize {
374 self.certs.len()
375 }
376 pub fn is_empty(&self) -> bool {
377 self.certs.is_empty()
378 }
379
380 pub fn fingerprints(&self) -> Vec<[u8; 32]> {
381 self.certs.iter().map(|c| c.fingerprint()).collect()
382 }
383}