1use canton_proto::com::daml::ledger::api::v2::admin as pb;
22use canton_proto::com::digitalasset::canton::crypto::v30 as crypto;
23use canton_proto::com::digitalasset::canton::protocol::v30 as topo;
24use canton_signer::PublicKey;
25use prost::Message as _;
26use sha2::{Digest as _, Sha256};
27
28#[derive(Clone, PartialEq, prost::Message)]
32struct UntypedVersionedMessage {
33 #[prost(bytes = "vec", tag = "1")]
34 data: Vec<u8>,
35 #[prost(int32, tag = "2")]
36 version: i32,
37}
38
39const HASH_PURPOSE_TOPOLOGY_TRANSACTION_SIGNATURE: u32 = 11;
42const HASH_PURPOSE_MULTI_TOPOLOGY_TRANSACTION: u32 = 55;
45const MULTIHASH_SHA256_PREFIX: [u8; 2] = [0x12, 0x20];
48
49fn evidence(digest: &[u8]) -> Vec<u8> {
50 let mut out = Vec::with_capacity(MULTIHASH_SHA256_PREFIX.len() + digest.len());
51 out.extend_from_slice(&MULTIHASH_SHA256_PREFIX);
52 out.extend_from_slice(digest);
53 out
54}
55
56fn transaction_hash(serialized: &[u8]) -> Vec<u8> {
59 let mut digest = Sha256::new();
60 digest.update(HASH_PURPOSE_TOPOLOGY_TRANSACTION_SIGNATURE.to_be_bytes());
61 digest.update(serialized);
62 evidence(&digest.finalize())
63}
64
65pub(crate) fn multi_hash(transactions: &[Vec<u8>]) -> Vec<u8> {
72 let mut covered: Vec<Vec<u8>> = transactions.iter().map(|t| transaction_hash(t)).collect();
73 covered.sort();
74 covered.dedup();
75 let mut digest = Sha256::new();
76 digest.update(HASH_PURPOSE_MULTI_TOPOLOGY_TRANSACTION.to_be_bytes());
77 digest.update(
78 u32::try_from(covered.len())
79 .unwrap_or(u32::MAX)
80 .to_be_bytes(),
81 );
82 for hash in &covered {
83 digest.update(u32::try_from(hash.len()).unwrap_or(u32::MAX).to_be_bytes());
84 digest.update(hash);
85 }
86 evidence(&digest.finalize())
87}
88
89fn decode_transaction(serialized: &[u8]) -> canton_core::Result<topo::TopologyTransaction> {
90 let envelope = UntypedVersionedMessage::decode(serialized).map_err(|e| {
91 canton_core::Error::UnexpectedResponse(format!(
92 "an onboarding transaction is not a versioned message: {e}"
93 ))
94 })?;
95 topo::TopologyTransaction::decode(envelope.data.as_slice()).map_err(|e| {
96 canton_core::Error::UnexpectedResponse(format!(
97 "an onboarding transaction (protocol version {}) does not decode: {e}",
98 envelope.version
99 ))
100 })
101}
102
103fn refuse(what: impl std::fmt::Display) -> canton_core::Error {
104 canton_core::Error::UnexpectedResponse(format!(
105 "the participant's onboarding transactions would authorize more than this party's \
106 own key: {what}; refusing to sign them"
107 ))
108}
109
110fn is_the_registered_key(candidate: Option<&crypto::SigningPublicKey>, key: &PublicKey) -> bool {
111 let Some(candidate) = candidate else {
112 return false;
113 };
114 let format_matches = match key.format() {
115 canton_signer::KeyFormat::Raw => candidate.format == crypto::CryptoKeyFormat::Raw as i32,
116 _ => true,
118 };
119 format_matches && candidate.public_key == key.data()
120}
121
122#[allow(deprecated)]
127fn check_transaction(
128 transaction: &topo::TopologyTransaction,
129 party_id: &str,
130 fingerprint: &str,
131 key: &PublicKey,
132) -> canton_core::Result<()> {
133 use topo::topology_mapping::Mapping;
134 if transaction.operation != topo::enums::TopologyChangeOp::AddReplace as i32 {
135 return Err(refuse(format!(
136 "a transaction with operation {} rather than an addition",
137 transaction.operation
138 )));
139 }
140 let mapping = transaction
141 .mapping
142 .as_ref()
143 .and_then(|m| m.mapping.as_ref())
144 .ok_or_else(|| refuse("a transaction without a mapping"))?;
145 match mapping {
146 Mapping::NamespaceDelegation(delegation) => {
147 if delegation.namespace != fingerprint {
148 return Err(refuse(format!(
149 "a namespace delegation for `{}`, not this key's namespace `{fingerprint}`",
150 delegation.namespace
151 )));
152 }
153 if !is_the_registered_key(delegation.target_key.as_ref(), key) {
154 return Err(refuse(
155 "a namespace delegation to a key other than the one being registered",
156 ));
157 }
158 }
159 Mapping::PartyToKeyMapping(mapping) => {
160 if mapping.party != party_id {
161 return Err(refuse(format!(
162 "a key mapping for `{}`, not `{party_id}`",
163 mapping.party
164 )));
165 }
166 if mapping.signing_keys.len() != 1
167 || !is_the_registered_key(mapping.signing_keys.first(), key)
168 {
169 return Err(refuse(
170 "a key mapping naming a signing key other than the one being registered",
171 ));
172 }
173 if mapping.threshold > 1 {
174 return Err(refuse(format!(
175 "a key mapping with threshold {}",
176 mapping.threshold
177 )));
178 }
179 }
180 Mapping::PartyToParticipant(hosting) => {
181 if hosting.party != party_id {
182 return Err(refuse(format!(
183 "a hosting mapping for `{}`, not `{party_id}`",
184 hosting.party
185 )));
186 }
187 if hosting.threshold > 1 {
188 return Err(refuse(format!(
189 "a hosting mapping with threshold {}",
190 hosting.threshold
191 )));
192 }
193 for host in &hosting.participants {
194 let allowed = host.permission
195 == topo::enums::ParticipantPermission::Confirmation as i32
196 || host.permission == topo::enums::ParticipantPermission::Observation as i32;
197 if !allowed {
198 return Err(refuse(format!(
199 "participant `{}` hosting the party with permission {} (only \
200 confirmation or observation is a hosted external party)",
201 host.participant_uid, host.permission
202 )));
203 }
204 }
205 }
206 _ => return Err(refuse("a mapping of a kind onboarding does not need")),
207 }
208 Ok(())
209}
210
211#[derive(Clone, Debug)]
221pub struct ExternalPartyTopology {
222 party_id: String,
223 public_key_fingerprint: String,
224 transactions: Vec<Vec<u8>>,
225 multi_hash: Vec<u8>,
226}
227
228impl ExternalPartyTopology {
229 pub(crate) fn from_response(
230 response: pb::GenerateExternalPartyTopologyResponse,
231 public_key: &PublicKey,
232 ) -> canton_core::Result<Self> {
233 if response.multi_hash.is_empty() {
234 return Err(canton_core::Error::UnexpectedResponse(
235 "the participant returned no multi-hash to sign".to_string(),
236 ));
237 }
238 if response.public_key_fingerprint.is_empty() {
239 return Err(canton_core::Error::UnexpectedResponse(
240 "the participant returned no key fingerprint, so nothing could \
241 sign as this party"
242 .to_string(),
243 ));
244 }
245 if response.topology_transactions.is_empty() {
246 return Err(canton_core::Error::UnexpectedResponse(
247 "the participant returned no onboarding transactions, so there would be \
248 nothing to submit and the multi-hash would cover nothing"
249 .to_string(),
250 ));
251 }
252 match response.party_id.rsplit_once("::") {
255 Some((_, namespace)) if namespace == response.public_key_fingerprint => {}
256 Some((_, namespace)) => {
257 return Err(canton_core::Error::UnexpectedResponse(format!(
258 "the participant offered party `{}`, whose namespace `{namespace}` is not the \
259 fingerprint `{}` of the key being registered — nothing this caller holds \
260 could sign as it",
261 response.party_id, response.public_key_fingerprint
262 )));
263 }
264 None => {
265 return Err(canton_core::Error::UnexpectedResponse(format!(
266 "the participant returned `{}`, which is not a party id",
267 response.party_id
268 )));
269 }
270 }
271 let recomputed = multi_hash(&response.topology_transactions);
278 if recomputed != response.multi_hash {
279 return Err(canton_core::Error::UnexpectedResponse(
280 "the participant's multi-hash is not the hash of the onboarding transactions \
281 it returned; refusing to sign it"
282 .to_string(),
283 ));
284 }
285 for serialized in &response.topology_transactions {
286 let transaction = decode_transaction(serialized)?;
287 check_transaction(
288 &transaction,
289 &response.party_id,
290 &response.public_key_fingerprint,
291 public_key,
292 )?;
293 }
294 Ok(Self {
295 party_id: response.party_id,
296 public_key_fingerprint: response.public_key_fingerprint,
297 transactions: response.topology_transactions,
298 multi_hash: response.multi_hash,
299 })
300 }
301
302 #[must_use]
307 pub fn party_id(&self) -> &str {
308 &self.party_id
309 }
310
311 #[must_use]
316 pub fn public_key_fingerprint(&self) -> &str {
317 &self.public_key_fingerprint
318 }
319
320 #[must_use]
323 pub fn multi_hash(&self) -> &[u8] {
324 &self.multi_hash
325 }
326
327 #[must_use]
334 pub fn transactions(&self) -> &[Vec<u8>] {
335 &self.transactions
336 }
337}
338
339#[cfg(test)]
340#[allow(clippy::unwrap_used, clippy::expect_used, deprecated)]
341mod tests {
342 use super::{ExternalPartyTopology, UntypedVersionedMessage, multi_hash};
343 use canton_proto::com::daml::ledger::api::v2::admin as pb;
344 use canton_proto::com::digitalasset::canton::crypto::v30 as crypto;
345 use canton_proto::com::digitalasset::canton::protocol::v30 as topo;
346 use canton_signer::{KeyFormat, KeySpec, PublicKey};
347 use prost::Message as _;
348 use topo::topology_mapping::Mapping;
349
350 const FINGERPRINT: &str = "1220ab";
351 const PARTY: &str = "alice::1220ab";
352
353 fn key() -> PublicKey {
354 PublicKey::new(KeyFormat::Raw, vec![7; 32], KeySpec::EcCurve25519)
355 }
356
357 fn signing_key(bytes: &[u8]) -> crypto::SigningPublicKey {
358 crypto::SigningPublicKey {
359 format: crypto::CryptoKeyFormat::Raw as i32,
360 public_key: bytes.to_vec(),
361 ..Default::default()
362 }
363 }
364
365 fn transaction(mapping: Mapping, operation: topo::enums::TopologyChangeOp) -> Vec<u8> {
366 let transaction = topo::TopologyTransaction {
367 operation: operation as i32,
368 serial: 1,
369 mapping: Some(topo::TopologyMapping {
370 mapping: Some(mapping),
371 }),
372 };
373 UntypedVersionedMessage {
374 data: transaction.encode_to_vec(),
375 version: 30,
376 }
377 .encode_to_vec()
378 }
379
380 fn add(mapping: Mapping) -> Vec<u8> {
381 transaction(mapping, topo::enums::TopologyChangeOp::AddReplace)
382 }
383
384 fn delegation(namespace: &str, to: &[u8]) -> Mapping {
385 Mapping::NamespaceDelegation(topo::NamespaceDelegation {
386 namespace: namespace.to_string(),
387 target_key: Some(signing_key(to)),
388 restriction: Some(topo::namespace_delegation::Restriction::CanSignAllMappings(
389 topo::namespace_delegation::CanSignAllMappings {},
390 )),
391 ..Default::default()
392 })
393 }
394
395 fn key_mapping(party: &str, keys: &[&[u8]]) -> Mapping {
396 Mapping::PartyToKeyMapping(topo::PartyToKeyMapping {
397 party: party.to_string(),
398 threshold: 1,
399 signing_keys: keys.iter().map(|k| signing_key(k)).collect(),
400 })
401 }
402
403 fn hosting(party: &str, hosts: &[(&str, topo::enums::ParticipantPermission)]) -> Mapping {
404 Mapping::PartyToParticipant(topo::PartyToParticipant {
405 party: party.to_string(),
406 threshold: 1,
407 participants: hosts
408 .iter()
409 .map(
410 |(uid, permission)| topo::party_to_participant::HostingParticipant {
411 participant_uid: (*uid).to_string(),
412 permission: *permission as i32,
413 onboarding: None,
414 },
415 )
416 .collect(),
417 ..Default::default()
418 })
419 }
420
421 fn honest_set() -> Vec<Vec<u8>> {
423 vec![
424 add(delegation(FINGERPRINT, &[7; 32])),
425 add(key_mapping(PARTY, &[&[7; 32]])),
426 add(hosting(
427 PARTY,
428 &[(
429 "participant1::1220cc",
430 topo::enums::ParticipantPermission::Confirmation,
431 )],
432 )),
433 ]
434 }
435
436 fn response(
437 party_id: &str,
438 fingerprint: &str,
439 transactions: Vec<Vec<u8>>,
440 ) -> pb::GenerateExternalPartyTopologyResponse {
441 pb::GenerateExternalPartyTopologyResponse {
442 party_id: party_id.to_string(),
443 public_key_fingerprint: fingerprint.to_string(),
444 multi_hash: multi_hash(&transactions),
445 topology_transactions: transactions,
446 }
447 }
448
449 fn refused(response: pb::GenerateExternalPartyTopologyResponse) -> String {
450 let error = ExternalPartyTopology::from_response(response, &key()).expect_err("refused");
451 assert!(
452 matches!(error, canton_core::Error::UnexpectedResponse(_)),
453 "{error:?}"
454 );
455 error.to_string()
456 }
457
458 #[test]
462 fn an_honest_onboarding_set_is_accepted() {
463 let topology = ExternalPartyTopology::from_response(
464 response(PARTY, FINGERPRINT, honest_set()),
465 &key(),
466 )
467 .expect("the honest set");
468 assert_eq!(topology.party_id(), PARTY);
469 assert_eq!(topology.public_key_fingerprint(), FINGERPRINT);
470 assert_eq!(topology.multi_hash().len(), 34);
471 assert_eq!(&topology.multi_hash()[..2], &[0x12, 0x20]);
472 }
473
474 #[test]
478 fn the_multi_hash_is_over_the_sorted_set_of_transaction_hashes() {
479 let set = honest_set();
480 let mut reversed = set.clone();
481 reversed.reverse();
482 assert_eq!(multi_hash(&set), multi_hash(&reversed));
483 assert_ne!(multi_hash(&set), multi_hash(&set[..2]));
484 let single = multi_hash(&[Vec::new()]);
487 assert_eq!(single.len(), 34);
488 assert_ne!(&single[2..], &multi_hash(&[vec![1]])[2..]);
489 }
490
491 #[test]
492 fn a_multi_hash_that_is_not_the_hash_of_the_transactions_is_refused() {
493 let mut response = response(PARTY, FINGERPRINT, honest_set());
494 response.multi_hash[10] ^= 0xff;
495 let message = refused(response);
496 assert!(message.contains("multi-hash"), "{message}");
497 }
498
499 #[test]
500 fn a_second_host_with_submission_rights_is_refused() {
501 let mut set = honest_set();
502 set.push(add(hosting(
503 PARTY,
504 &[(
505 "attacker::1220ee",
506 topo::enums::ParticipantPermission::Submission,
507 )],
508 )));
509 let message = refused(response(PARTY, FINGERPRINT, set));
510 assert!(
511 message.contains("submission") || message.contains("permission 1"),
512 "{message}"
513 );
514 }
515
516 #[test]
517 fn a_delegation_to_another_key_is_refused() {
518 let mut set = honest_set();
519 set.push(add(delegation(FINGERPRINT, &[9; 32])));
520 let message = refused(response(PARTY, FINGERPRINT, set));
521 assert!(message.contains("delegation"), "{message}");
522 let set = vec![add(delegation("1220ff", &[7; 32]))];
523 let message = refused(response(PARTY, FINGERPRINT, set));
524 assert!(message.contains("1220ff"), "{message}");
525 }
526
527 #[test]
528 fn a_key_mapping_naming_an_extra_key_or_another_party_is_refused() {
529 let set = vec![add(key_mapping(PARTY, &[&[7; 32], &[9; 32]]))];
530 assert!(refused(response(PARTY, FINGERPRINT, set)).contains("signing key"));
531 let set = vec![add(key_mapping("mallory::1220ab", &[&[7; 32]]))];
532 assert!(refused(response(PARTY, FINGERPRINT, set)).contains("mallory"));
533 }
534
535 #[test]
536 fn a_mapping_onboarding_does_not_need_and_a_removal_are_refused() {
537 let set = vec![add(
538 Mapping::VettedPackages(topo::VettedPackages::default()),
539 )];
540 assert!(refused(response(PARTY, FINGERPRINT, set)).contains("kind"));
541 let set = vec![transaction(
542 delegation(FINGERPRINT, &[7; 32]),
543 topo::enums::TopologyChangeOp::Remove,
544 )];
545 assert!(refused(response(PARTY, FINGERPRINT, set)).contains("operation"));
546 }
547
548 #[test]
549 fn bytes_that_are_not_a_transaction_are_refused() {
550 let message = refused(response(PARTY, FINGERPRINT, vec![vec![0xff, 0xff, 0xff]]));
551 assert!(
552 message.contains("versioned message") || message.contains("decode"),
553 "{message}"
554 );
555 }
556
557 #[test]
560 fn a_party_under_a_namespace_this_key_does_not_control_is_refused() {
561 let message = refused(response("alice::1220ff", FINGERPRINT, honest_set()));
562 assert!(
563 message.contains("1220ff") && message.contains("1220ab"),
564 "{message}"
565 );
566 }
567
568 #[test]
569 fn a_response_that_is_not_a_party_id_is_refused() {
570 let message = refused(response("alice", FINGERPRINT, honest_set()));
571 assert!(message.contains("not a party id"), "{message}");
572 }
573}