keys.rs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  1. //! Versioned [`KeyRing`] for token peppers, TOTP-encryption keys, and PII keys.
  2. //!
  3. //! Holds versioned key ids for the token pepper, the TOTP-encryption key, and
  4. //! the PII field-encryption key
  5. //! (newest first, bounded retry across retained ids). Opaque tokens are stored
  6. //! under `v<N>` key ids and verification retries across retained key ids,
  7. //! ordered newest-first. Sessions are opaque Redis ids, so there is no session
  8. //! signing key, and — because this server signs nothing — there is no AS
  9. //! JWT-signing key and no `jwks_uri`.
  10. //!
  11. //! # `keygen` purpose
  12. //!
  13. //! The `keygen` CLI subcommand (todo 14) generates an **ES256 keypair for test
  14. //! fixtures and client-side DPoP keys only**. It is never an AS signing key:
  15. //! the server publishes no JWKS (`jwks_uri`) and emits no AS-signed artifact.
  16. //! See [`KEYGEN_PURPOSE`].
  17. //!
  18. //! Key material never appears in logs: [`KeyRing::fmt`] prints key ids only.
  19. use std::fmt;
  20. use secrecy::{ExposeSecret, SecretString};
  21. use thiserror::Error;
  22. /// Machine-readable statement of the `keygen` command's purpose.
  23. ///
  24. /// `keygen` emits an ES256 keypair for **test fixtures and client-side DPoP
  25. /// keys only**. It is never an authorization-server signing key, and the
  26. /// server publishes no `jwks_uri`.
  27. pub const KEYGEN_PURPOSE: &str =
  28. "test-fixtures/client-dpop-only; never an AS signing key; no jwks_uri";
  29. /// Stable identifier for a versioned key (for example `v1`, `v2`).
  30. #[derive(Clone, PartialEq, Eq, Hash)]
  31. pub struct KeyId(String);
  32. impl KeyId {
  33. /// Construct a key id from any string-like value.
  34. pub fn new(id: impl Into<String>) -> Self {
  35. Self(id.into())
  36. }
  37. /// Borrow the id as a string slice.
  38. #[must_use]
  39. pub fn as_str(&self) -> &str {
  40. &self.0
  41. }
  42. /// Whether the id is empty.
  43. #[must_use]
  44. pub fn is_empty(&self) -> bool {
  45. self.0.is_empty()
  46. }
  47. }
  48. impl fmt::Display for KeyId {
  49. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  50. f.write_str(&self.0)
  51. }
  52. }
  53. impl fmt::Debug for KeyId {
  54. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  55. write!(f, "KeyId({})", self.0)
  56. }
  57. }
  58. impl From<&str> for KeyId {
  59. fn from(value: &str) -> Self {
  60. Self::new(value)
  61. }
  62. }
  63. impl From<String> for KeyId {
  64. fn from(value: String) -> Self {
  65. Self::new(value)
  66. }
  67. }
  68. /// A single secret bound to a [`KeyId`].
  69. #[derive(Clone)]
  70. pub struct VersionedSecret {
  71. id: KeyId,
  72. secret: SecretString,
  73. }
  74. impl VersionedSecret {
  75. /// The key id this secret is stored under.
  76. #[must_use]
  77. pub fn id(&self) -> &KeyId {
  78. &self.id
  79. }
  80. /// The wrapped secret (call [`ExposeSecret::expose_secret`] to read bytes).
  81. #[must_use]
  82. pub fn secret(&self) -> &SecretString {
  83. &self.secret
  84. }
  85. /// Borrow the secret's bytes as a string slice.
  86. #[must_use]
  87. pub fn expose(&self) -> &str {
  88. self.secret.expose_secret()
  89. }
  90. }
  91. impl fmt::Debug for VersionedSecret {
  92. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  93. f.debug_struct("VersionedSecret")
  94. .field("id", &self.id)
  95. .field("secret", &"[REDACTED]")
  96. .finish()
  97. }
  98. }
  99. /// Errors constructing a [`KeyRing`].
  100. #[derive(Debug, Clone, PartialEq, Eq, Error)]
  101. pub enum KeyRingError {
  102. /// No token-pepper key material was supplied.
  103. #[error("token pepper key material is required")]
  104. MissingTokenPepper,
  105. /// No TOTP-encryption key material was supplied.
  106. #[error("TOTP encryption key material is required")]
  107. MissingTotpKey,
  108. /// No PII field-encryption key material was supplied.
  109. #[error("PII encryption key material is required")]
  110. MissingPiiKey,
  111. /// A key id was empty.
  112. #[error("key id must not be empty")]
  113. EmptyKeyId,
  114. /// The same key id was supplied twice within one purpose chain.
  115. #[error("duplicate key id: {id}")]
  116. DuplicateKeyId {
  117. /// The duplicated id.
  118. id: String,
  119. },
  120. }
  121. /// Versioned key ring for token peppers, TOTP-encryption keys, and PII keys.
  122. ///
  123. /// Every chain is ordered newest-first; the first element is the active key.
  124. /// The `Debug` impl never prints key material.
  125. #[derive(Clone)]
  126. pub struct KeyRing {
  127. peppers: Vec<VersionedSecret>,
  128. totp_keys: Vec<VersionedSecret>,
  129. pii_keys: Vec<VersionedSecret>,
  130. }
  131. impl KeyRing {
  132. /// Build a key ring from newest-first pepper, TOTP-encryption, and PII chains.
  133. ///
  134. /// Each chain must be non-empty and free of duplicate ids.
  135. pub fn from_parts(
  136. peppers: Vec<(KeyId, SecretString)>,
  137. totp_keys: Vec<(KeyId, SecretString)>,
  138. pii_keys: Vec<(KeyId, SecretString)>,
  139. ) -> Result<Self, KeyRingError> {
  140. Ok(Self {
  141. peppers: build_chain(peppers, KeyRingError::MissingTokenPepper)?,
  142. totp_keys: build_chain(totp_keys, KeyRingError::MissingTotpKey)?,
  143. pii_keys: build_chain(pii_keys, KeyRingError::MissingPiiKey)?,
  144. })
  145. }
  146. /// The active token pepper (newest key id), if any.
  147. #[must_use]
  148. pub fn active_pepper(&self) -> Option<(&KeyId, &SecretString)> {
  149. self.peppers.first().map(|k| (k.id(), k.secret()))
  150. }
  151. /// Look up a retained token pepper by key id.
  152. #[must_use]
  153. pub fn pepper_by_id(&self, id: &str) -> Option<&SecretString> {
  154. find(&self.peppers, id)
  155. }
  156. /// Iterate token peppers ordered newest-first.
  157. pub fn peppers_newest_first(&self) -> impl Iterator<Item = (&KeyId, &SecretString)> {
  158. self.peppers.iter().map(|k| (k.id(), k.secret()))
  159. }
  160. /// The active TOTP-encryption key (newest key id), if any.
  161. #[must_use]
  162. pub fn active_totp_key(&self) -> Option<(&KeyId, &SecretString)> {
  163. self.totp_keys.first().map(|k| (k.id(), k.secret()))
  164. }
  165. /// Look up a retained TOTP-encryption key by key id.
  166. #[must_use]
  167. pub fn totp_key_by_id(&self, id: &str) -> Option<&SecretString> {
  168. find(&self.totp_keys, id)
  169. }
  170. /// Iterate TOTP-encryption keys ordered newest-first.
  171. pub fn totp_keys_newest_first(&self) -> impl Iterator<Item = (&KeyId, &SecretString)> {
  172. self.totp_keys.iter().map(|k| (k.id(), k.secret()))
  173. }
  174. /// The active PII-encryption key (newest key id), if any.
  175. #[must_use]
  176. pub fn active_pii_key(&self) -> Option<(&KeyId, &SecretString)> {
  177. self.pii_keys.first().map(|k| (k.id(), k.secret()))
  178. }
  179. /// Look up a retained PII-encryption key by key id.
  180. #[must_use]
  181. pub fn pii_key_by_id(&self, id: &str) -> Option<&SecretString> {
  182. find(&self.pii_keys, id)
  183. }
  184. /// Iterate PII-encryption keys ordered newest-first.
  185. pub fn pii_keys_newest_first(&self) -> impl Iterator<Item = (&KeyId, &SecretString)> {
  186. self.pii_keys.iter().map(|k| (k.id(), k.secret()))
  187. }
  188. }
  189. impl fmt::Debug for KeyRing {
  190. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  191. let pepper_ids: Vec<&str> = self.peppers.iter().map(|k| k.id().as_str()).collect();
  192. let totp_ids: Vec<&str> = self.totp_keys.iter().map(|k| k.id().as_str()).collect();
  193. let pii_ids: Vec<&str> = self.pii_keys.iter().map(|k| k.id().as_str()).collect();
  194. f.debug_struct("KeyRing")
  195. .field("token_pepper_ids", &pepper_ids)
  196. .field("totp_key_ids", &totp_ids)
  197. .field("pii_key_ids", &pii_ids)
  198. .finish()
  199. }
  200. }
  201. fn build_chain(
  202. chain: Vec<(KeyId, SecretString)>,
  203. empty_error: KeyRingError,
  204. ) -> Result<Vec<VersionedSecret>, KeyRingError> {
  205. if chain.is_empty() {
  206. return Err(empty_error);
  207. }
  208. let mut out: Vec<VersionedSecret> = Vec::with_capacity(chain.len());
  209. for (id, secret) in chain {
  210. if id.is_empty() {
  211. return Err(KeyRingError::EmptyKeyId);
  212. }
  213. if out.iter().any(|existing| existing.id() == &id) {
  214. return Err(KeyRingError::DuplicateKeyId {
  215. id: id.as_str().to_owned(),
  216. });
  217. }
  218. out.push(VersionedSecret { id, secret });
  219. }
  220. Ok(out)
  221. }
  222. fn find<'a>(chain: &'a [VersionedSecret], id: &str) -> Option<&'a SecretString> {
  223. chain
  224. .iter()
  225. .find(|k| k.id().as_str() == id)
  226. .map(VersionedSecret::secret)
  227. }
  228. #[cfg(test)]
  229. mod tests {
  230. use secrecy::ExposeSecret;
  231. use super::*;
  232. fn pepper(id: &str, value: &str) -> (KeyId, SecretString) {
  233. (KeyId::new(id), SecretString::from(value))
  234. }
  235. #[test]
  236. fn keyring_requires_a_token_pepper() -> Result<(), KeyRingError> {
  237. let result = KeyRing::from_parts(
  238. Vec::new(),
  239. vec![pepper("v1", "totp-secret")],
  240. vec![pepper("v1", "pii-secret")],
  241. );
  242. assert!(matches!(result, Err(KeyRingError::MissingTokenPepper)));
  243. Ok(())
  244. }
  245. #[test]
  246. fn keyring_requires_a_totp_key() -> Result<(), KeyRingError> {
  247. let result = KeyRing::from_parts(
  248. vec![pepper("v1", "pepper-secret")],
  249. Vec::new(),
  250. vec![pepper("v1", "pii-secret")],
  251. );
  252. assert!(matches!(result, Err(KeyRingError::MissingTotpKey)));
  253. Ok(())
  254. }
  255. #[test]
  256. fn keyring_requires_a_pii_key() -> Result<(), KeyRingError> {
  257. let result = KeyRing::from_parts(
  258. vec![pepper("v1", "pepper-secret")],
  259. vec![pepper("v1", "totp-secret")],
  260. Vec::new(),
  261. );
  262. assert!(matches!(result, Err(KeyRingError::MissingPiiKey)));
  263. Ok(())
  264. }
  265. #[test]
  266. fn keyring_rejects_duplicate_ids() {
  267. let duplicate_pepper = KeyRing::from_parts(
  268. vec![pepper("v1", "a"), pepper("v1", "b")],
  269. vec![pepper("v1", "c")],
  270. vec![pepper("v1", "d")],
  271. );
  272. assert!(matches!(
  273. duplicate_pepper,
  274. Err(KeyRingError::DuplicateKeyId { .. })
  275. ));
  276. // A duplicate inside the PII chain is rejected the same way.
  277. let duplicate_pii = KeyRing::from_parts(
  278. vec![pepper("v1", "a")],
  279. vec![pepper("v1", "b")],
  280. vec![pepper("v2", "c"), pepper("v2", "d")],
  281. );
  282. assert!(matches!(
  283. duplicate_pii,
  284. Err(KeyRingError::DuplicateKeyId { .. })
  285. ));
  286. }
  287. #[test]
  288. fn keyring_resolves_retained_ids_newest_first() -> Result<(), KeyRingError> {
  289. let ring = KeyRing::from_parts(
  290. vec![pepper("v2", "new"), pepper("v1", "old")],
  291. vec![pepper("v1", "totp")],
  292. vec![pepper("v1", "pii")],
  293. )?;
  294. let active = ring.active_pepper();
  295. assert!(matches!(active, Some((id, _)) if id.as_str() == "v2"));
  296. let retained = ring.pepper_by_id("v1");
  297. assert!(matches!(retained, Some(secret) if secret.expose_secret() == "old"));
  298. assert!(ring.pepper_by_id("v9").is_none());
  299. Ok(())
  300. }
  301. #[test]
  302. fn keyring_resolves_retained_pii_id() -> Result<(), KeyRingError> {
  303. let ring = KeyRing::from_parts(
  304. vec![pepper("v1", "pepper-secret")],
  305. vec![pepper("v1", "totp-secret")],
  306. vec![
  307. pepper("v2", "new-pii-secret"),
  308. pepper("v1", "old-pii-secret"),
  309. ],
  310. )?;
  311. let active = ring.active_pii_key();
  312. assert!(matches!(active, Some((id, _)) if id.as_str() == "v2"));
  313. let retained = ring.pii_key_by_id("v1");
  314. assert!(matches!(retained, Some(secret) if secret.expose_secret() == "old-pii-secret"));
  315. assert!(ring.pii_key_by_id("v9").is_none());
  316. let ids: Vec<&str> = ring
  317. .pii_keys_newest_first()
  318. .map(|(id, _)| id.as_str())
  319. .collect();
  320. assert_eq!(ids, vec!["v2", "v1"]);
  321. // `Debug` exposes ids only, never material from any chain.
  322. let debug = format!("{ring:?}");
  323. assert!(debug.contains("v2"), "debug must include the pii key id");
  324. assert!(!debug.contains("new-pii-secret") && !debug.contains("old-pii-secret"));
  325. assert!(!debug.contains("pepper-secret") && !debug.contains("totp-secret"));
  326. Ok(())
  327. }
  328. #[test]
  329. fn keygen_purpose_documents_no_as_signing_key() {
  330. assert!(KEYGEN_PURPOSE.contains("never an AS signing key"));
  331. assert!(KEYGEN_PURPOSE.contains("no jwks_uri"));
  332. }
  333. }