Skip to main content

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}