canton_admin/external_party.rs
1//! Allocating a party whose key the participant does not hold.
2//!
3//! An ordinary party is allocated in one call and the participant signs for it
4//! from then on. An *external* party keeps its key elsewhere — which is the
5//! whole point of interactive submission — so allocation is two calls with a
6//! signature in between:
7//!
8//! 1. [`generate_external_party_topology`] hands the participant a public key.
9//! It replies with the party id it would assign, the **fingerprint** it
10//! computed for the key, the onboarding topology transactions, and one hash
11//! over all of them.
12//! 2. The key signs that hash — proving the party controls it — and
13//! [`allocate_external_party`] submits the transactions with the signature.
14//!
15//! The fingerprint from step 1 is what makes a `canton_signer::Ed25519Key` into
16//! a `Signer`: Canton computes it, so a caller cannot know it before asking.
17//!
18//! [`generate_external_party_topology`]: crate::AdminClient::generate_external_party_topology
19//! [`allocate_external_party`]: crate::AdminClient::allocate_external_party
20
21use canton_proto::com::daml::ledger::api::v2::admin as pb;
22
23/// What the participant will do with an external party, once it is asked to.
24///
25/// The result of [`generate_external_party_topology`], and the input to
26/// [`allocate_external_party`]. Nothing has been submitted yet: this is a
27/// proposal, and it becomes real only when the key signs
28/// [`multi_hash`](Self::multi_hash).
29///
30/// [`generate_external_party_topology`]: crate::AdminClient::generate_external_party_topology
31/// [`allocate_external_party`]: crate::AdminClient::allocate_external_party
32#[derive(Clone, Debug)]
33pub struct ExternalPartyTopology {
34 party_id: String,
35 public_key_fingerprint: String,
36 transactions: Vec<Vec<u8>>,
37 multi_hash: Vec<u8>,
38}
39
40impl ExternalPartyTopology {
41 pub(crate) fn from_response(
42 response: pb::GenerateExternalPartyTopologyResponse,
43 ) -> canton_core::Result<Self> {
44 if response.multi_hash.is_empty() {
45 return Err(canton_core::Error::UnexpectedResponse(
46 "the participant returned no multi-hash to sign".to_string(),
47 ));
48 }
49 if response.public_key_fingerprint.is_empty() {
50 return Err(canton_core::Error::UnexpectedResponse(
51 "the participant returned no key fingerprint, so nothing could \
52 sign as this party"
53 .to_string(),
54 ));
55 }
56 if response.topology_transactions.is_empty() {
57 return Err(canton_core::Error::UnexpectedResponse(
58 "the participant returned no onboarding transactions, so there would be \
59 nothing to submit and the multi-hash would cover nothing"
60 .to_string(),
61 ));
62 }
63 // A party id is `<hint>::<namespace>`, and for an external party the
64 // namespace *is* the fingerprint of the key being registered. The SDK
65 // cannot yet recompute the multi-hash — that needs Canton's
66 // hash-purpose scheme, which this workspace does not implement — so
67 // this is the one relation between the returned pieces that can be
68 // checked at all, and it is free.
69 //
70 // What it catches is the ordinary case rather than the adversarial
71 // one: a response assembled for a different key, or a participant that
72 // onboarded a party under a namespace the caller does not hold. The
73 // caller would otherwise learn this by signing, submitting, and being
74 // told the signature is invalid, with nothing pointing at why.
75 match response.party_id.rsplit_once("::") {
76 Some((_, namespace)) if namespace == response.public_key_fingerprint => {}
77 Some((_, namespace)) => {
78 return Err(canton_core::Error::UnexpectedResponse(format!(
79 "the participant offered party `{}`, whose namespace `{namespace}` is not the \
80 fingerprint `{}` of the key being registered — nothing this caller holds \
81 could sign as it",
82 response.party_id, response.public_key_fingerprint
83 )));
84 }
85 None => {
86 return Err(canton_core::Error::UnexpectedResponse(format!(
87 "the participant returned `{}`, which is not a party id",
88 response.party_id
89 )));
90 }
91 }
92 Ok(Self {
93 party_id: response.party_id,
94 public_key_fingerprint: response.public_key_fingerprint,
95 transactions: response.topology_transactions,
96 multi_hash: response.multi_hash,
97 })
98 }
99
100 /// The party id the participant will assign — `<hint>::<namespace>`.
101 ///
102 /// Known before allocation because it is derived from the key, so a caller
103 /// can prepare against it while the signature is still being obtained.
104 #[must_use]
105 pub fn party_id(&self) -> &str {
106 &self.party_id
107 }
108
109 /// The fingerprint Canton computed for the key.
110 ///
111 /// This is what a signature carries in `signed_by`, and what
112 /// `canton_signer::Ed25519Key::into_signer` needs.
113 #[must_use]
114 pub fn public_key_fingerprint(&self) -> &str {
115 &self.public_key_fingerprint
116 }
117
118 /// The hash to sign: one hash over every onboarding transaction, so one
119 /// signature authorizes them all.
120 #[must_use]
121 pub fn multi_hash(&self) -> &[u8] {
122 &self.multi_hash
123 }
124
125 /// The serialized onboarding topology transactions.
126 ///
127 /// Exposed for a caller that wants to inspect what it is about to
128 /// authorize; [`allocate_external_party`] submits them.
129 ///
130 /// [`allocate_external_party`]: crate::AdminClient::allocate_external_party
131 #[must_use]
132 pub fn transactions(&self) -> &[Vec<u8>] {
133 &self.transactions
134 }
135}
136
137#[cfg(test)]
138#[allow(clippy::unwrap_used, clippy::expect_used)]
139mod tests {
140 use super::ExternalPartyTopology;
141 use canton_proto::com::daml::ledger::api::v2::admin as pb;
142
143 fn response(party_id: &str, fingerprint: &str) -> pb::GenerateExternalPartyTopologyResponse {
144 pb::GenerateExternalPartyTopologyResponse {
145 party_id: party_id.to_string(),
146 public_key_fingerprint: fingerprint.to_string(),
147 topology_transactions: vec![vec![1, 2, 3]],
148 multi_hash: vec![0x12, 0x20, 0xaa],
149 }
150 }
151
152 /// The ordinary case: the namespace of the party id is the fingerprint of
153 /// the key being registered, so this caller can sign as it.
154 #[test]
155 fn a_party_whose_namespace_is_the_registered_key_is_accepted() {
156 let topology = ExternalPartyTopology::from_response(response("alice::1220ab", "1220ab"))
157 .expect("the namespace matches the key");
158 assert_eq!(topology.party_id(), "alice::1220ab");
159 assert_eq!(topology.public_key_fingerprint(), "1220ab");
160 }
161
162 /// And the case worth catching: a party under a namespace this caller's key
163 /// does not control. Signing and submitting would be the only other way to
164 /// find out, and the participant's answer would name the signature rather
165 /// than the mismatch.
166 #[test]
167 fn a_party_under_a_namespace_this_key_does_not_control_is_refused() {
168 let error = ExternalPartyTopology::from_response(response("alice::1220ff", "1220ab"))
169 .expect_err("the namespace is another key's");
170 let message = error.to_string();
171 assert!(message.contains("1220ff"), "name the namespace: {message}");
172 assert!(message.contains("1220ab"), "and the key: {message}");
173 }
174
175 /// Something that is not a party id at all.
176 #[test]
177 fn a_response_that_is_not_a_party_id_is_refused() {
178 let error = ExternalPartyTopology::from_response(response("alice", "1220ab"))
179 .expect_err("`alice` is not a party id");
180 assert!(error.to_string().contains("not a party id"), "{error}");
181 }
182}