Skip to main content

arminius_lib_db_keys/
server_keys.rs

1// SPDX-FileCopyrightText: 2025 BWI GmbH
2//
3// SPDX-License-Identifier: Apache-2.0
4//! The server key queries.
5
6mod tests;
7
8use std::time::SystemTime;
9
10use arminius_lib_db::{Database, DatabaseError, ResultExt as _, TransactedDb};
11use ruma::{
12	KeyId, OwnedServerName, OwnedServerSigningKeyId, ServerName, ServerSigningKeyId,
13	ServerSigningKeyVersion, SigningKeyAlgorithm,
14	api::federation::discovery::{OldVerifyKey, ServerSigningKeys},
15	serde::{Base64, Raw},
16	signatures::Ed25519KeyPair,
17};
18
19/// Error related to managing server keys
20#[derive(thiserror::Error, Debug)]
21pub enum ServerKeyError {
22	/// A database error occurred
23	#[error("Database error: {0}")]
24	DatabaseError(#[from] DatabaseError),
25	/// An error related to the key format occurred
26	#[error("Key error: {0}")]
27	KeyError(#[from] ruma::signatures::Ed25519KeyPairParseError),
28	/// No key was found in the database
29	#[error("Key not found")]
30	NotFound,
31	/// A configuration error occurred
32	#[error("Configuration error: {0}")]
33	ConfigurationError(String),
34}
35
36impl From<sqlx::Error> for ServerKeyError {
37	fn from(err: sqlx::Error) -> Self {
38		ServerKeyError::DatabaseError(DatabaseError::from(err))
39	}
40}
41
42/// A server key from the database, and its expiration
43#[derive(Debug, Eq, PartialEq)]
44pub struct ServerKey {
45	/// The actual key
46	pub key: Vec<u8>,
47	/// Time at which the key will/did expire
48	///
49	/// With federation, you often need to check the expiration of the key.
50	/// Incoming federation requests should not be accepted with expired keys.
51	/// But old messages may be signed with keys that have since been expired,
52	/// and that's completely fine.
53	pub expires_at: chrono::DateTime<chrono::Utc>,
54}
55
56/// Queries for managing server signing keys
57// Queries are usually used as extension traits so suggest `import as _`.
58#[rust_analyzer::prefer_underscore_import]
59pub trait ServerKeyQueries: Database {
60	/// Fetches the server key from the database, or an error if it does not
61	/// exist
62	#[tracing::instrument(skip(self))]
63	async fn fetch_active_server_key(&mut self) -> Result<Ed25519KeyPair, ServerKeyError> {
64		let res = sqlx::query!(
65			"
66		SELECT
67			key_pair, key_version
68		FROM
69			server_private_keys
70		WHERE
71			key_pair IS NOT NULL
72		"
73		)
74		.fetch_optional(self.connection().await?)
75		.await?
76		.ok_or(ServerKeyError::NotFound)?;
77
78		Ed25519KeyPair::from_der(
79			&res.key_pair.ok_or(ServerKeyError::ConfigurationError(
80				"Signing key in database invalid".to_owned(),
81			))?[..],
82			res.key_version,
83		)
84		.map_err(ServerKeyError::from)
85	}
86
87	/// Gets all previous signing keys
88	#[tracing::instrument(skip(self))]
89	async fn fetch_expired_verify_keys(
90		&mut self,
91	) -> Result<Vec<(OwnedServerSigningKeyId, OldVerifyKey)>, ServerKeyError> {
92		Ok(sqlx::query!(
93			"SELECT expired_at, verify_key, key_version FROM server_private_keys WHERE key_pair IS NULL;"
94		)
95		.fetch_all(self.connection().await?)
96		.await?
97		.iter()
98		.map(|row| {
99			let expired_at = row.expired_at.ok_or(ServerKeyError::ConfigurationError(
100				"Expired key does not have expiry timestamp set".to_owned(),
101			))?;
102
103			let date =
104				ruma::MilliSecondsSinceUnixEpoch::from_system_time(SystemTime::from(expired_at))
105					.ok_or(ServerKeyError::ConfigurationError(
106						"Invalid timestamp for expired key".to_owned(),
107					))?;
108			let b64 = Base64::new(row.verify_key.clone());
109			let name = ServerSigningKeyId::from_parts(
110				SigningKeyAlgorithm::Ed25519,
111				&ServerSigningKeyVersion::parse(row.key_version.clone()).map_err(|_| {
112					ServerKeyError::ConfigurationError("Invalid server key version".to_owned())
113				})?,
114			);
115			let verify_key = OldVerifyKey::new(date, b64);
116			Ok(Some((name, verify_key)))
117		})
118		.collect::<Result<Vec<_>, ServerKeyError>>()?
119		.into_iter()
120		.flatten()
121		.collect::<Vec<_>>())
122	}
123
124	/// Fetch foreign server keys
125	#[tracing::instrument(skip(self))]
126	async fn fetch_foreign_server_keys(
127		&mut self,
128		server_names: &ServerName,
129		key_id: &KeyId<SigningKeyAlgorithm, ServerSigningKeyVersion>,
130	) -> Result<Option<ServerKey>, DatabaseError> {
131		let algorithm = key_id.algorithm();
132		Ok(sqlx::query!(
133			"
134			SELECT
135			    public_key,
136			    expires_at
137			FROM
138			    server_public_keys
139			WHERE
140			    server_name = $1
141			    AND key_algorithm = $2
142			    AND key_name = $3
143			",
144			server_names.as_str(),
145			algorithm.as_str(),
146			key_id.key_name().as_str(),
147		)
148		.fetch_optional(self.connection().await?)
149		.await?
150		.map(|row| ServerKey { key: row.public_key, expires_at: row.expires_at }))
151	}
152
153	/// Upsert a server key
154	///
155	/// If the key already exists, update the `expires_at` value
156	/// If another key exists under the same HS, but with the same name, returns
157	/// an error. This is a serious misconfiguration from the other HS, and not
158	/// really something we can handle in a sane manner.
159	/// The spec does not give any instructions for handling this either.
160	#[tracing::instrument(skip(self))]
161	async fn upsert_server_key(
162		&mut self,
163		server_name: &ServerName,
164		key_id: &KeyId<SigningKeyAlgorithm, ServerSigningKeyVersion>,
165		server_key: &ServerKey,
166	) -> Result<(), DatabaseError> {
167		let algorithm = key_id.algorithm();
168		let result = sqlx::query!(
169			"
170			INSERT INTO server_public_keys (server_name, key_algorithm, key_name, public_key, expires_at)
171			    VALUES ($1, $2, $3, $4, $5)
172			ON CONFLICT (server_name, key_name, key_algorithm)
173			    DO UPDATE SET
174			        expires_at = excluded.expires_at
175			    WHERE
176			        server_public_keys.public_key = excluded.public_key
177			",
178			server_name.as_str(),
179			algorithm.as_str(),
180			key_id.key_name().as_str(),
181			server_key.key,
182			server_key.expires_at,
183		)
184		.execute(self.connection().await?)
185		.await?;
186		if result.rows_affected() == 0 {
187			Err(DatabaseError::InvalidData(format!(
188				"A key {key_id} for the server {server_name} already exists in the database, but now another key with the same ID is being written",
189			)))
190		} else {
191			Ok(())
192		}
193	}
194
195	/// Fetch a cached key response
196	#[tracing::instrument(skip(self))]
197	async fn fetch_key_response(
198		&mut self,
199		server_names: &[OwnedServerName],
200	) -> Result<Vec<Raw<ServerSigningKeys>>, DatabaseError> {
201		let names: Vec<String> = server_names.iter().map(ToString::to_string).collect();
202		sqlx::query!(
203			"
204			SELECT
205			    response
206			FROM
207			    server_key_responses
208			WHERE
209			    server_name IN (
210			        SELECT
211			            *
212			        FROM
213			            UNNEST($1::text[]))
214			",
215			&names[..]
216		)
217		.fetch_all(self.connection().await?)
218		.await?
219		.into_iter()
220		.map(|row| {
221			Raw::<ServerSigningKeys>::from_json_string(row.response)
222				.invalid_data("Could not create raw value")
223		})
224		.collect::<Result<Vec<_>, _>>()
225	}
226
227	/// Idempotently inserts a signing key coming from a homeserver import.
228	///
229	/// `key_id` is the full key id (e.g. `"ed25519:1"`); `key_pair_der` is
230	/// the PKCS#8 v1 DER encoding of the key pair; `verify_key` is the 32-byte
231	/// raw ed25519 public key.
232	///
233	/// The `private_key` field in the export is the unpadded standard-base64
234	/// encoding of the raw 32-byte ed25519 seed, matching Synapse's signing-key
235	/// file format. The importer decodes that seed, wraps it in a PKCS#8 v1
236	/// header to produce `key_pair_der`, and derives `verify_key` from it
237	/// before calling this method.
238	///
239	/// Returns `true` if a new row was inserted, `false` if the key was already
240	/// present (idempotent; re-importing the same key is a no-op).
241	#[tracing::instrument(skip(self, key_pair_der, verify_key))]
242	async fn import_private_key(
243		&mut self,
244		key_id: &KeyId<SigningKeyAlgorithm, ServerSigningKeyVersion>,
245		key_pair_der: &[u8],
246		verify_key: &[u8],
247	) -> Result<bool, DatabaseError> {
248		let result = sqlx::query!(
249			r#"
250			INSERT INTO server_private_keys (key_version, key_pair, verify_key)
251				VALUES ($1, $2, $3)
252			ON CONFLICT (key_version)
253				DO NOTHING
254			"#,
255			key_id.as_str(),
256			key_pair_der,
257			verify_key,
258		)
259		.execute(self.connection().await?)
260		.await?;
261		Ok(result.rows_affected() > 0)
262	}
263
264	/// Upsert a server key response from another HS
265	///
266	/// Replaces any previous response from the same HS
267	#[tracing::instrument(skip(self))]
268	async fn upsert_key_response(
269		&mut self,
270		server_name: &ServerName,
271		key_response: Raw<ServerSigningKeys>,
272	) -> Result<(), DatabaseError> {
273		// Because a new request is only made when `expires_at` is in the past,
274		// every new request made to the server key endpoint will have a
275		// different value of `expires_at`, which needs to be in the future,
276		// giving us a new response, which means that
277		// the value of `response` will change each time this query is called.
278		sqlx::query!(
279			"
280			INSERT INTO server_key_responses (server_name, response)
281			    VALUES ($1, $2)
282			ON CONFLICT (server_name)
283			    DO UPDATE SET
284			        response = excluded.response
285			",
286			server_name.as_str(),
287			key_response.json().get()
288		)
289		.execute(self.connection().await?)
290		.await?;
291		Ok(())
292	}
293}
294
295/// Server keys related queries that must operate within a transaction
296pub trait TransactedServerKeyQueries: ServerKeyQueries {
297	/// Deletes all signing keys from the database, and generates a new one
298	async fn rotate_server_keys(&mut self) -> Result<(), ServerKeyError> {
299		// Question: Should we generally be using zeroed around our keys?
300
301		// Generating a keypair in ruma necessarily consists of creating a DER
302		// certificate, and then deserializing it
303
304		let der = Ed25519KeyPair::generate();
305
306		// We deserialize the recently created keypair, only in order to get the
307		// first eight bytes of the public key, in order to use it as a name.
308		let tmp_key_pair = Ed25519KeyPair::from_der(der.as_ref(), String::new()).map_err(|_| {
309			ServerKeyError::ConfigurationError("Server keys have invalid format".to_owned())
310		})?;
311		let name: String = bs58::encode(tmp_key_pair.public_key())
312			.with_alphabet(bs58::Alphabet::BITCOIN)
313			.into_string()
314			.chars()
315			.take(8)
316			.collect();
317
318		sqlx::query!(
319			"UPDATE server_private_keys SET key_pair = NULL, expired_at = NOW() WHERE key_pair IS NOT NULL;"
320		)
321		.execute(&mut *self.connection().await?)
322		.await?;
323
324		let der_ref: &[u8] = der.as_ref();
325
326		sqlx::query!(
327			"INSERT INTO server_private_keys (key_version, key_pair, verify_key) VALUES ($1, $2, $3)",
328			name,
329			der_ref,
330			&tmp_key_pair.public_key(),
331		)
332		.execute(&mut *self.connection().await?)
333		.await?;
334		Ok(())
335	}
336
337	/// If there is no signing key in the database, generate one and put it
338	/// there.
339	async fn generate_key_if_not_exists(&mut self) -> Result<(), ServerKeyError> {
340		if self.fetch_active_server_key().await.is_ok() {
341			Ok(())
342		} else {
343			self.rotate_server_keys().await
344		}
345	}
346}
347
348impl<T: Database> ServerKeyQueries for T {}
349
350impl<'c> TransactedServerKeyQueries for TransactedDb<'c> {}