Skip to main content

pubhubs/servers/
constellation.rs

1//! Details on the constellation of PubHubs servers
2
3use std::ops::Deref;
4
5use sha2::digest::Digest;
6
7use crate::api;
8use crate::common::{kem, secret};
9use crate::id;
10use crate::phcrypto;
11use crate::servers;
12
13/// Public details on the constellation of PubHubs servers (stored in the [`inner`] field)
14/// paired with an derived [`id`].  [`Deref`]s to [`Inner`].
15///
16/// # Comparing constellations
17///
18/// [`Constellation`] does not implement [`PartialEq`], because there are two valid ways to compare
19/// constellations `c1` and `c2`, namely `c1.id == c2.id` and `c1.inner == c2.inner`, and it should
20/// be clear in the code which one is being used.
21///
22/// [`id`]: Constellation::id
23/// [`inner`]: Constellation::inner
24#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
25pub struct Constellation {
26    /// Identifier for this constellation derived from [`Inner`] using a hash.
27    pub id: id::Id,
28
29    /// When this constellation was first created by pubhubs central.  When two parties
30    /// have different constellations, the party with the oldest constellation should
31    /// update.
32    pub created_at: api::NumericDate,
33
34    #[serde(flatten)]
35    pub inner: Inner,
36}
37
38impl Deref for Constellation {
39    type Target = Inner;
40
41    fn deref(&self) -> &Inner {
42        &self.inner
43    }
44}
45// NOTE: When adding a new field to the constellation make sure it has a default value in the first
46// version so that when the new version of PHC contacts the old versions of the transcryptor and
47// the authentication server, PHC will not crash on missing fields at the transcryptor and the
48// authentication server.
49//
50// (The converse is not necessary:  when an outdated authentication server and transcryptor
51// are running discovery against a freshly updated PHC, PHC's constellation will not be set
52// and will thus not cause the transcryptor or authentication server to crash.)
53#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
54pub struct Inner {
55    pub transcryptor_url: url::Url,
56
57    /// The transcryptor's hybrid post-quantum verifying key, used to verify its JWTs and signatures.
58    pub transcryptor_verifying_key: api::VerifyingKeyBytes,
59
60    /// Hash of the transcryptor's master encryption key part `x_T B`, so the transcryptor can check
61    /// that the correct keypart was used without `x_T B` being exposed in the clear.
62    pub transcryptor_master_enc_key_part_hash: id::Id,
63
64    /// [`kem::EncapKeyBytes::id`] of the transcryptor's encapsulation key.
65    pub transcryptor_encap_key_id: id::Id,
66
67    /// Shared secret PHC encapsulated against the transcryptor's encap key.
68    pub transcryptor_ss_encap: kem::CiphertextBytes,
69
70    pub phc_url: url::Url,
71
72    /// PHC's ed25519 public key (the `ed` half of [`phc_verifying_key`](Self::phc_verifying_key)),
73    /// hex-encoded.  Kept on the wire so hubs predating the hybrid migration can verify the classical
74    /// EdDSA HHPP; see [`api::Ed25519VerifyingKeyHex`].
75    ///
76    /// TODO: remove once all hubs are on >=v3.4.0 (see scripts/check-hubs.py).
77    #[serde(default)]
78    pub phc_jwt_key: api::Ed25519VerifyingKeyHex,
79
80    /// PHC's hybrid post-quantum verifying key, used to verify its JWTs and signatures.
81    pub phc_verifying_key: api::VerifyingKeyBytes,
82
83    /// Hash of PHC's master encryption key part `x_PHC B`.  Published so that a change of PHC's
84    /// part churns the constellation id (the real master key is held off-wire by PHC).
85    pub phc_master_enc_key_part_hash: id::Id,
86
87    pub auths_url: url::Url,
88
89    /// The authentication server's hybrid post-quantum verifying key, used to verify its JWTs and
90    /// signatures.
91    pub auths_verifying_key: api::VerifyingKeyBytes,
92
93    /// [`kem::EncapKeyBytes::id`] of the authentication server's encapsulation key.
94    pub auths_encap_key_id: id::Id,
95
96    /// Shared secret PHC encapsulated against the authentication server's encap key.
97    pub auths_ss_encap: kem::CiphertextBytes,
98
99    pub global_client_url: url::Url,
100
101    /// pubhubs version
102    pub ph_version: Option<String>,
103}
104
105/// Extension methods on [`sha2::Sha256`] used by [`Inner::sha256`] to give the constellation an
106/// unambiguous byte encoding before hashing.
107trait DigestExt: Sized {
108    /// Length-prefix (8-byte big-endian, platform-independent) a variable-length field.
109    fn chain_varlen(self, bytes: &[u8]) -> Self;
110
111    /// A 1/0 presence byte, followed by the length-prefixed bytes when present.
112    fn chain_opt(self, bytes: Option<&[u8]>) -> Self;
113
114    /// Both length-prefixed ciphertext halves (ML-KEM ‖ EC).
115    fn chain_ct(self, ct: &kem::CiphertextBytes) -> Self;
116
117    /// Both length-prefixed halves (ed25519 ‖ ML-DSA) of a hybrid verifying key.
118    fn chain_vk(self, vk: &api::VerifyingKeyBytes) -> Self;
119}
120
121impl DigestExt for sha2::Sha256 {
122    fn chain_varlen(self, bytes: &[u8]) -> Self {
123        self.chain_update(secret::encode_usize(bytes.len()))
124            .chain_update(bytes)
125    }
126
127    fn chain_opt(self, bytes: Option<&[u8]>) -> Self {
128        match bytes {
129            Some(bytes) => self.chain_update([1u8]).chain_varlen(bytes),
130            None => self.chain_update([0u8]),
131        }
132    }
133
134    fn chain_ct(self, ct: &kem::CiphertextBytes) -> Self {
135        self.chain_varlen(ct.ml.as_ref())
136            .chain_varlen(ct.ec.as_ref())
137    }
138
139    fn chain_vk(self, vk: &api::VerifyingKeyBytes) -> Self {
140        self.chain_varlen(vk.ed.as_ref())
141            .chain_varlen(vk.ml.as_ref())
142    }
143}
144
145impl Inner {
146    /// Returns the url of the named server
147    pub fn url(&self, name: servers::Name) -> &url::Url {
148        match name {
149            servers::Name::PubhubsCentral => &self.phc_url,
150            servers::Name::Transcryptor => &self.transcryptor_url,
151            servers::Name::AuthenticationServer => &self.auths_url,
152        }
153    }
154
155    /// Returns a [`sha2::Sha256`] hash of this constellation - used to compute [`Constellation::id`].
156    pub(crate) fn sha256(&self) -> sha2::Sha256 {
157        let Inner {
158            transcryptor_url,
159            transcryptor_verifying_key,
160            transcryptor_master_enc_key_part_hash,
161            transcryptor_encap_key_id,
162            transcryptor_ss_encap,
163
164            phc_url,
165            phc_verifying_key,
166            phc_master_enc_key_part_hash,
167
168            auths_url,
169            auths_verifying_key,
170            auths_encap_key_id,
171            auths_ss_encap,
172
173            global_client_url,
174
175            ph_version,
176
177            // not hashed: this is the `ed` half of `phc_verifying_key`, already covered above.
178            phc_jwt_key: _,
179        } = self;
180
181        // NOTE: it would be easier to serialize self using, say, serde_json, and then hash that,
182        // but it's not evident whether serializing the same constellation twice will give the same
183        // string.
184        //
185        // Framing (see `DigestExt`): fixed-length fields (32-byte id hashes) are hashed directly;
186        // variable-length fields, and the two halves of each hybrid verifying key / KEM ciphertext,
187        // are length-prefixed (`chain_varlen`/`chain_vk`/`chain_ct`) so one field's bytes can't be
188        // read as part of an adjacent one.  The only optional field is `ph_version` (`chain_opt`, a
189        // 1/0 presence byte).
190
191        sha2::Sha256::new()
192            // Hash-format version - BUMP THIS on any change to the framing or fields below, so the
193            // change always alters the constellation id.  (Only PHC computes the id; peers compare.)
194            // v2: jwt keys became hybrid post-quantum (ed25519 ‖ ML-DSA).
195            // v3: dropped the deprecated enc_key / master_enc_key / `*_jwt_key` placeholder fields,
196            // and the verifying-key / KEM / master-key-part-hash fields are no longer optional.
197            .chain_update(3u16.to_be_bytes())
198            .chain_varlen(transcryptor_url.as_str().as_bytes())
199            .chain_vk(transcryptor_verifying_key)
200            .chain_update(transcryptor_master_enc_key_part_hash.as_slice())
201            .chain_update(transcryptor_encap_key_id.as_slice())
202            .chain_ct(transcryptor_ss_encap)
203            .chain_varlen(phc_url.as_str().as_bytes())
204            .chain_vk(phc_verifying_key)
205            .chain_update(phc_master_enc_key_part_hash.as_slice())
206            .chain_varlen(auths_url.as_str().as_bytes())
207            .chain_vk(auths_verifying_key)
208            .chain_update(auths_encap_key_id.as_slice())
209            .chain_ct(auths_ss_encap)
210            .chain_varlen(global_client_url.as_str().as_bytes())
211            .chain_opt(ph_version.as_ref().map(|v| v.as_bytes()))
212    }
213
214    pub fn derive_id(&self) -> id::Id {
215        phcrypto::constellation_id(self)
216    }
217}
218
219/// A full [`Constellation`], or just the [`id::Id`].
220#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
221#[serde(untagged)]
222pub enum ConstellationOrId {
223    Constellation(Box<Constellation>),
224    Id { id: id::Id },
225}
226
227impl ConstellationOrId {
228    /// Returns the [`Constellation`], if any.
229    pub fn constellation(&self) -> Option<&Constellation> {
230        if let Self::Constellation(c) = self {
231            return Some(c);
232        }
233        None
234    }
235
236    /// Returns underlying [`Constellation`], if any.
237    pub fn into_constellation(self) -> Option<Constellation> {
238        if let Self::Constellation(c) = self {
239            return Some(*c);
240        }
241        None
242    }
243
244    /// Returns the [`id::Id`]
245    pub fn id(&self) -> &id::Id {
246        match self {
247            Self::Constellation(c) => &c.id,
248            Self::Id { id } => id,
249        }
250    }
251}