1use std::fmt::Display;
4
5use exo_authority::permission::Permission;
6use exo_core::{Did, Hash256, PublicKey, Timestamp, hash::hash_structured};
7use serde::{Deserialize, Serialize};
8
9use crate::{
10 GenesisCeremonyConfig, Result, RootError, RootPublicKeyPackage,
11 dkg::validate_public_key_package,
12 signing::{RootSignature, validate_root_signer_ids},
13 verify_root_signature,
14};
15
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
18pub struct RootIssuerDelegation {
19 pub issuer_did: Did,
21 pub issuer_public_key: PublicKey,
23 pub granted_permissions: Vec<Permission>,
25 pub effective_at: Timestamp,
27 pub expires_at: Option<Timestamp>,
29 pub purpose: String,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
35pub struct RootTrustBundle {
36 pub config: GenesisCeremonyConfig,
38 pub public_key_package: RootPublicKeyPackage,
40 pub issuer_delegation: RootIssuerDelegation,
42 pub transcript_hash: Hash256,
44 pub root_signature: RootSignature,
46 pub bundle_id: Hash256,
48}
49
50#[derive(Serialize)]
51struct RootArtifactPayload<'a> {
52 domain: &'static str,
53 config_hash: Hash256,
54 public_key_package_hash: Hash256,
55 transcript_hash: Hash256,
56 issuer_delegation_hash: Hash256,
57 issuer_did: &'a Did,
58 signer_ids: &'a [u16],
59}
60
61#[derive(Serialize)]
62struct RootBundleIdPayload<'a> {
63 domain: &'static str,
64 artifact_payload_hash: Hash256,
65 root_signature: &'a RootSignature,
66}
67
68fn canonical_bytes<T: Serialize>(value: &T) -> Result<Vec<u8>> {
69 let mut bytes = Vec::new();
70 ciborium::into_writer(value, &mut bytes).map_err(canonical_encoding_error)?;
71 Ok(bytes)
72}
73
74fn structured_hash<T: Serialize>(value: &T) -> Result<Hash256> {
75 hash_structured(value).map_err(canonical_encoding_error)
76}
77
78fn canonical_encoding_error(error: impl Display) -> RootError {
79 RootError::CanonicalEncoding {
80 detail: error.to_string(),
81 }
82}
83
84impl RootIssuerDelegation {
85 pub fn root_artifact_payload(
88 &self,
89 config: &GenesisCeremonyConfig,
90 public_key_package: &RootPublicKeyPackage,
91 transcript_hash: Hash256,
92 ) -> Result<Vec<u8>> {
93 self.root_artifact_payload_for_signers(
94 config,
95 public_key_package,
96 transcript_hash,
97 config.signing_set.as_slice(),
98 )
99 }
100
101 pub fn root_artifact_payload_for_signers(
104 &self,
105 config: &GenesisCeremonyConfig,
106 public_key_package: &RootPublicKeyPackage,
107 transcript_hash: Hash256,
108 signer_ids: &[u16],
109 ) -> Result<Vec<u8>> {
110 config.validate()?;
111 validate_root_signer_ids(config, signer_ids)?;
112 if self.purpose.trim().is_empty() {
113 return Err(RootError::BundleRejected {
114 reason: "issuer delegation purpose must not be empty".to_owned(),
115 });
116 }
117 if self.granted_permissions.is_empty() {
118 return Err(RootError::BundleRejected {
119 reason: "issuer delegation must grant at least one permission".to_owned(),
120 });
121 }
122 let payload = RootArtifactPayload {
123 domain: "EXOCHAIN_ROOT_ARTIFACT_V1",
124 config_hash: structured_hash(config)?,
125 public_key_package_hash: structured_hash(public_key_package)?,
126 transcript_hash,
127 issuer_delegation_hash: structured_hash(self)?,
128 issuer_did: &self.issuer_did,
129 signer_ids,
130 };
131 canonical_bytes(&payload)
132 }
133}
134
135fn bundle_id(
136 delegation: &RootIssuerDelegation,
137 config: &GenesisCeremonyConfig,
138 public_key_package: &RootPublicKeyPackage,
139 transcript_hash: Hash256,
140 root_signature: &RootSignature,
141) -> Result<Hash256> {
142 let artifact_payload = delegation.root_artifact_payload_for_signers(
143 config,
144 public_key_package,
145 transcript_hash,
146 root_signature.signer_ids.as_slice(),
147 )?;
148 let id_payload = RootBundleIdPayload {
149 domain: "EXOCHAIN_ROOT_BUNDLE_V1",
150 artifact_payload_hash: Hash256::digest(&artifact_payload),
151 root_signature,
152 };
153 structured_hash(&id_payload)
154}
155
156pub fn assemble_root_bundle(
158 config: GenesisCeremonyConfig,
159 public_key_package: RootPublicKeyPackage,
160 issuer_delegation: RootIssuerDelegation,
161 transcript_hash: Hash256,
162 root_signature: RootSignature,
163) -> Result<RootTrustBundle> {
164 validate_public_key_package(&config, &public_key_package)?;
165 validate_root_signer_ids(&config, root_signature.signer_ids.as_slice())?;
166 let payload = issuer_delegation.root_artifact_payload_for_signers(
167 &config,
168 &public_key_package,
169 transcript_hash,
170 root_signature.signer_ids.as_slice(),
171 )?;
172 verify_root_signature(
173 &public_key_package.root_public_key,
174 &payload,
175 root_signature.signature.as_slice(),
176 )?;
177 let bundle_id = bundle_id(
178 &issuer_delegation,
179 &config,
180 &public_key_package,
181 transcript_hash,
182 &root_signature,
183 )?;
184 Ok(RootTrustBundle {
185 config,
186 public_key_package,
187 issuer_delegation,
188 transcript_hash,
189 root_signature,
190 bundle_id,
191 })
192}
193
194pub fn verify_root_bundle(bundle: &RootTrustBundle) -> Result<()> {
196 bundle.config.validate()?;
197 validate_public_key_package(&bundle.config, &bundle.public_key_package)?;
198 validate_root_signer_ids(&bundle.config, bundle.root_signature.signer_ids.as_slice())?;
199 let payload = bundle.issuer_delegation.root_artifact_payload_for_signers(
200 &bundle.config,
201 &bundle.public_key_package,
202 bundle.transcript_hash,
203 bundle.root_signature.signer_ids.as_slice(),
204 )?;
205 verify_root_signature(
206 &bundle.public_key_package.root_public_key,
207 &payload,
208 bundle.root_signature.signature.as_slice(),
209 )?;
210 let expected_id = bundle_id(
211 &bundle.issuer_delegation,
212 &bundle.config,
213 &bundle.public_key_package,
214 bundle.transcript_hash,
215 &bundle.root_signature,
216 )?;
217 if expected_id != bundle.bundle_id {
218 return Err(RootError::BundleRejected {
219 reason: "bundle identifier does not match contents".to_owned(),
220 });
221 }
222 Ok(())
223}
224
225#[cfg(test)]
226mod tests {
227 use std::collections::BTreeMap;
228
229 use exo_core::{Did, PublicKey};
230 use frost_ristretto255 as frost;
231 use rand::{SeedableRng, rngs::StdRng};
232
233 use super::*;
234 use crate::{
235 CertifierContact, RootKeyPackage,
236 dkg::{deserialize_frost, serialize_frost},
237 run_complete_dkg,
238 };
239
240 #[derive(Serialize)]
241 struct LegacyRootArtifactPayload<'a> {
242 domain: &'static str,
243 config_hash: Hash256,
244 public_key_package_hash: Hash256,
245 transcript_hash: Hash256,
246 issuer_delegation_hash: Hash256,
247 issuer_did: &'a Did,
248 }
249
250 fn test_config() -> GenesisCeremonyConfig {
251 let certifiers = (1..=13)
252 .map(|identifier| {
253 let byte = u8::try_from(identifier).expect("identifier fits");
254 CertifierContact {
255 did: Did::new(&format!("did:exo:bundle-unit-{identifier:02}"))
256 .expect("valid did"),
257 frost_identifier: identifier,
258 signing_public_key: PublicKey::from_bytes([byte; 32]),
259 transport_public_key: [byte; 32],
260 }
261 })
262 .collect();
263 GenesisCeremonyConfig {
264 ceremony_id: "bundle-root".into(),
265 network_id: "unit-net".into(),
266 repo_commit: "d8927686a34bdc28ba36d53938f665685d2c4c04".into(),
267 constitution_hash: Hash256::digest(b"constitution"),
268 threshold: 7,
269 max_signers: 13,
270 created_at: Timestamp::new(1, 0),
271 certifiers,
272 signing_set: (1..=7).collect(),
273 }
274 }
275
276 fn issuer_delegation() -> RootIssuerDelegation {
277 RootIssuerDelegation {
278 issuer_did: Did::new("did:exo:bundle-avc-issuer").expect("valid did"),
279 issuer_public_key: PublicKey::from_bytes([0x44; 32]),
280 granted_permissions: vec![Permission::Govern, Permission::Delegate],
281 effective_at: Timestamp::new(1_785_000_010_000, 0),
282 expires_at: None,
283 purpose: "Delegate operational AVC issuing authority".into(),
284 }
285 }
286
287 fn legacy_unbound_root_artifact_payload(
288 delegation: &RootIssuerDelegation,
289 config: &GenesisCeremonyConfig,
290 public_key_package: &RootPublicKeyPackage,
291 transcript_hash: Hash256,
292 ) -> Result<Vec<u8>> {
293 let payload = LegacyRootArtifactPayload {
294 domain: "EXOCHAIN_ROOT_ARTIFACT_V1",
295 config_hash: structured_hash(config)?,
296 public_key_package_hash: structured_hash(public_key_package)?,
297 transcript_hash,
298 issuer_delegation_hash: structured_hash(delegation)?,
299 issuer_did: &delegation.issuer_did,
300 };
301 canonical_bytes(&payload)
302 }
303
304 fn raw_threshold_signature_without_signer_policy<R>(
305 public_key_package: &RootPublicKeyPackage,
306 shares: BTreeMap<u16, RootKeyPackage>,
307 message: &[u8],
308 rng: &mut R,
309 ) -> RootSignature
310 where
311 R: frost::rand_core::RngCore + frost::rand_core::CryptoRng,
312 {
313 let public: frost::keys::PublicKeyPackage =
314 deserialize_frost(public_key_package.public_key_package.as_slice())
315 .expect("public key package");
316 let mut key_packages = BTreeMap::new();
317 let mut signing_nonces = BTreeMap::new();
318 let mut signing_commitments = BTreeMap::new();
319
320 for (identifier, share) in &shares {
321 let frost_identifier = frost::Identifier::try_from(*identifier).expect("frost id");
322 let key_package: frost::keys::KeyPackage =
323 deserialize_frost(share.key_package.as_slice()).expect("key package");
324 let (nonces, commitments) = frost::round1::commit(key_package.signing_share(), rng);
325 signing_nonces.insert(frost_identifier, nonces);
326 signing_commitments.insert(frost_identifier, commitments);
327 key_packages.insert(frost_identifier, key_package);
328 }
329
330 let signing_package = frost::SigningPackage::new(signing_commitments, message);
331 let mut signature_shares = BTreeMap::new();
332 for (identifier, key_package) in &key_packages {
333 let share =
334 frost::round2::sign(&signing_package, &signing_nonces[identifier], key_package)
335 .expect("signature share");
336 signature_shares.insert(*identifier, share);
337 }
338 let aggregate =
339 frost::aggregate(&signing_package, &signature_shares, &public).expect("aggregate");
340 let signer_ids = shares.keys().copied().collect();
341 RootSignature {
342 signature: serialize_frost(&aggregate).expect("signature encoding"),
343 signer_ids,
344 }
345 }
346
347 #[test]
348 fn canonical_error_conversion_is_diagnostic() {
349 let error = canonical_encoding_error("encoder failed");
350 assert!(error.to_string().contains("encoder failed"));
351 }
352
353 #[test]
354 fn root_bundle_rejects_relabelled_signature_when_signer_metadata_was_unsigned() {
355 let config = test_config();
356 let mut rng = StdRng::seed_from_u64(700);
357 let dkg = run_complete_dkg(&config, &mut rng).expect("dkg");
358 let delegation = issuer_delegation();
359 let transcript_hash = Hash256::digest(b"transcript");
360 let legacy_payload = legacy_unbound_root_artifact_payload(
361 &delegation,
362 &config,
363 &dkg.public_key_package,
364 transcript_hash,
365 )
366 .expect("legacy payload");
367 let actual_signers = [1, 2, 3, 4, 5, 6, 8]
368 .into_iter()
369 .map(|identifier| {
370 (
371 identifier,
372 dkg.key_packages
373 .get(&identifier)
374 .expect("key package")
375 .clone(),
376 )
377 })
378 .collect();
379 let mut root_signature = raw_threshold_signature_without_signer_policy(
380 &dkg.public_key_package,
381 actual_signers,
382 &legacy_payload,
383 &mut rng,
384 );
385 assert_eq!(root_signature.signer_ids, vec![1, 2, 3, 4, 5, 6, 8]);
386
387 root_signature.signer_ids = config.signing_set.clone();
388
389 assert!(
390 assemble_root_bundle(
391 config,
392 dkg.public_key_package,
393 delegation,
394 transcript_hash,
395 root_signature,
396 )
397 .is_err(),
398 "bundle assembly must reject a threshold signature whose claimed signer metadata was not covered by the signed artifact"
399 );
400 }
401}