keys.rs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  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. /// Whether `id` is storage-safe: non-empty `[A-Za-z0-9_-]+`.
  49. ///
  50. /// A `.` would collide with the `<kid>.<digest>` storage format and split
  51. /// stored hashes at the wrong boundary, so it is rejected here and at config
  52. /// load; empty and whitespace-only values never reach this check as valid.
  53. #[must_use]
  54. pub fn is_valid_key_id(id: &str) -> bool {
  55. !id.is_empty()
  56. && id
  57. .bytes()
  58. .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_')
  59. }
  60. impl fmt::Display for KeyId {
  61. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  62. f.write_str(&self.0)
  63. }
  64. }
  65. impl fmt::Debug for KeyId {
  66. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  67. write!(f, "KeyId({})", self.0)
  68. }
  69. }
  70. impl From<&str> for KeyId {
  71. fn from(value: &str) -> Self {
  72. Self::new(value)
  73. }
  74. }
  75. impl From<String> for KeyId {
  76. fn from(value: String) -> Self {
  77. Self::new(value)
  78. }
  79. }
  80. /// A single secret bound to a [`KeyId`].
  81. #[derive(Clone)]
  82. pub struct VersionedSecret {
  83. id: KeyId,
  84. secret: SecretString,
  85. }
  86. impl VersionedSecret {
  87. /// The key id this secret is stored under.
  88. #[must_use]
  89. pub fn id(&self) -> &KeyId {
  90. &self.id
  91. }
  92. /// The wrapped secret (call [`ExposeSecret::expose_secret`] to read bytes).
  93. #[must_use]
  94. pub fn secret(&self) -> &SecretString {
  95. &self.secret
  96. }
  97. /// Borrow the secret's bytes as a string slice.
  98. #[must_use]
  99. pub fn expose(&self) -> &str {
  100. self.secret.expose_secret()
  101. }
  102. }
  103. impl fmt::Debug for VersionedSecret {
  104. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  105. f.debug_struct("VersionedSecret")
  106. .field("id", &self.id)
  107. .field("secret", &"[REDACTED]")
  108. .finish()
  109. }
  110. }
  111. /// Errors constructing a [`KeyRing`].
  112. #[derive(Debug, Clone, PartialEq, Eq, Error)]
  113. pub enum KeyRingError {
  114. /// No token-pepper key material was supplied.
  115. #[error("token pepper key material is required")]
  116. MissingTokenPepper,
  117. /// No TOTP-encryption key material was supplied.
  118. #[error("TOTP encryption key material is required")]
  119. MissingTotpKey,
  120. /// No PII field-encryption key material was supplied.
  121. #[error("PII encryption key material is required")]
  122. MissingPiiKey,
  123. /// A key id was empty.
  124. #[error("key id must not be empty")]
  125. EmptyKeyId,
  126. /// A key id contained characters outside `[A-Za-z0-9_-]`.
  127. #[error("invalid key id: {id}")]
  128. InvalidKeyId {
  129. /// The rejected value.
  130. id: String,
  131. },
  132. /// The same key id was supplied twice within one purpose chain.
  133. #[error("duplicate key id: {id}")]
  134. DuplicateKeyId {
  135. /// The duplicated id.
  136. id: String,
  137. },
  138. }
  139. /// Versioned key ring for token peppers, TOTP-encryption keys, and PII keys.
  140. ///
  141. /// Every chain is ordered newest-first; the first element is the active key.
  142. /// The `Debug` impl never prints key material.
  143. #[derive(Clone)]
  144. pub struct KeyRing {
  145. peppers: Vec<VersionedSecret>,
  146. totp_keys: Vec<VersionedSecret>,
  147. pii_keys: Vec<VersionedSecret>,
  148. }
  149. impl KeyRing {
  150. /// Build a key ring from newest-first pepper, TOTP-encryption, and PII chains.
  151. ///
  152. /// Each chain must be non-empty and free of duplicate ids.
  153. pub fn from_parts(
  154. peppers: Vec<(KeyId, SecretString)>,
  155. totp_keys: Vec<(KeyId, SecretString)>,
  156. pii_keys: Vec<(KeyId, SecretString)>,
  157. ) -> Result<Self, KeyRingError> {
  158. Ok(Self {
  159. peppers: build_chain(peppers, KeyRingError::MissingTokenPepper)?,
  160. totp_keys: build_chain(totp_keys, KeyRingError::MissingTotpKey)?,
  161. pii_keys: build_chain(pii_keys, KeyRingError::MissingPiiKey)?,
  162. })
  163. }
  164. /// The active token pepper (newest key id), if any.
  165. #[must_use]
  166. pub fn active_pepper(&self) -> Option<(&KeyId, &SecretString)> {
  167. self.peppers.first().map(|k| (k.id(), k.secret()))
  168. }
  169. /// Look up a retained token pepper by key id.
  170. #[must_use]
  171. pub fn pepper_by_id(&self, id: &str) -> Option<&SecretString> {
  172. find(&self.peppers, id)
  173. }
  174. /// Iterate token peppers ordered newest-first.
  175. pub fn peppers_newest_first(&self) -> impl Iterator<Item = (&KeyId, &SecretString)> {
  176. self.peppers.iter().map(|k| (k.id(), k.secret()))
  177. }
  178. /// The active TOTP-encryption key (newest key id), if any.
  179. #[must_use]
  180. pub fn active_totp_key(&self) -> Option<(&KeyId, &SecretString)> {
  181. self.totp_keys.first().map(|k| (k.id(), k.secret()))
  182. }
  183. /// Look up a retained TOTP-encryption key by key id.
  184. #[must_use]
  185. pub fn totp_key_by_id(&self, id: &str) -> Option<&SecretString> {
  186. find(&self.totp_keys, id)
  187. }
  188. /// Iterate TOTP-encryption keys ordered newest-first.
  189. pub fn totp_keys_newest_first(&self) -> impl Iterator<Item = (&KeyId, &SecretString)> {
  190. self.totp_keys.iter().map(|k| (k.id(), k.secret()))
  191. }
  192. /// The active PII-encryption key (newest key id), if any.
  193. #[must_use]
  194. pub fn active_pii_key(&self) -> Option<(&KeyId, &SecretString)> {
  195. self.pii_keys.first().map(|k| (k.id(), k.secret()))
  196. }
  197. /// Look up a retained PII-encryption key by key id.
  198. #[must_use]
  199. pub fn pii_key_by_id(&self, id: &str) -> Option<&SecretString> {
  200. find(&self.pii_keys, id)
  201. }
  202. /// Iterate PII-encryption keys ordered newest-first.
  203. pub fn pii_keys_newest_first(&self) -> impl Iterator<Item = (&KeyId, &SecretString)> {
  204. self.pii_keys.iter().map(|k| (k.id(), k.secret()))
  205. }
  206. }
  207. impl fmt::Debug for KeyRing {
  208. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  209. let pepper_ids: Vec<&str> = self.peppers.iter().map(|k| k.id().as_str()).collect();
  210. let totp_ids: Vec<&str> = self.totp_keys.iter().map(|k| k.id().as_str()).collect();
  211. let pii_ids: Vec<&str> = self.pii_keys.iter().map(|k| k.id().as_str()).collect();
  212. f.debug_struct("KeyRing")
  213. .field("token_pepper_ids", &pepper_ids)
  214. .field("totp_key_ids", &totp_ids)
  215. .field("pii_key_ids", &pii_ids)
  216. .finish()
  217. }
  218. }
  219. fn build_chain(
  220. chain: Vec<(KeyId, SecretString)>,
  221. empty_error: KeyRingError,
  222. ) -> Result<Vec<VersionedSecret>, KeyRingError> {
  223. if chain.is_empty() {
  224. return Err(empty_error);
  225. }
  226. let mut out: Vec<VersionedSecret> = Vec::with_capacity(chain.len());
  227. for (id, secret) in chain {
  228. if id.is_empty() {
  229. return Err(KeyRingError::EmptyKeyId);
  230. }
  231. if !is_valid_key_id(id.as_str()) {
  232. return Err(KeyRingError::InvalidKeyId {
  233. id: id.as_str().to_owned(),
  234. });
  235. }
  236. if out.iter().any(|existing| existing.id() == &id) {
  237. return Err(KeyRingError::DuplicateKeyId {
  238. id: id.as_str().to_owned(),
  239. });
  240. }
  241. out.push(VersionedSecret { id, secret });
  242. }
  243. Ok(out)
  244. }
  245. fn find<'a>(chain: &'a [VersionedSecret], id: &str) -> Option<&'a SecretString> {
  246. chain
  247. .iter()
  248. .find(|k| k.id().as_str() == id)
  249. .map(VersionedSecret::secret)
  250. }
  251. #[cfg(test)]
  252. mod tests {
  253. use secrecy::ExposeSecret;
  254. use super::*;
  255. fn pepper(id: &str, value: &str) -> (KeyId, SecretString) {
  256. (KeyId::new(id), SecretString::from(value))
  257. }
  258. #[test]
  259. fn keyring_requires_a_token_pepper() -> Result<(), KeyRingError> {
  260. let result = KeyRing::from_parts(
  261. Vec::new(),
  262. vec![pepper("v1", "totp-secret")],
  263. vec![pepper("v1", "pii-secret")],
  264. );
  265. assert!(matches!(result, Err(KeyRingError::MissingTokenPepper)));
  266. Ok(())
  267. }
  268. #[test]
  269. fn keyring_requires_a_totp_key() -> Result<(), KeyRingError> {
  270. let result = KeyRing::from_parts(
  271. vec![pepper("v1", "pepper-secret")],
  272. Vec::new(),
  273. vec![pepper("v1", "pii-secret")],
  274. );
  275. assert!(matches!(result, Err(KeyRingError::MissingTotpKey)));
  276. Ok(())
  277. }
  278. #[test]
  279. fn keyring_requires_a_pii_key() -> Result<(), KeyRingError> {
  280. let result = KeyRing::from_parts(
  281. vec![pepper("v1", "pepper-secret")],
  282. vec![pepper("v1", "totp-secret")],
  283. Vec::new(),
  284. );
  285. assert!(matches!(result, Err(KeyRingError::MissingPiiKey)));
  286. Ok(())
  287. }
  288. #[test]
  289. fn keyring_rejects_duplicate_ids() {
  290. let duplicate_pepper = KeyRing::from_parts(
  291. vec![pepper("v1", "a"), pepper("v1", "b")],
  292. vec![pepper("v1", "c")],
  293. vec![pepper("v1", "d")],
  294. );
  295. assert!(matches!(
  296. duplicate_pepper,
  297. Err(KeyRingError::DuplicateKeyId { .. })
  298. ));
  299. // A duplicate inside the PII chain is rejected the same way.
  300. let duplicate_pii = KeyRing::from_parts(
  301. vec![pepper("v1", "a")],
  302. vec![pepper("v1", "b")],
  303. vec![pepper("v2", "c"), pepper("v2", "d")],
  304. );
  305. assert!(matches!(
  306. duplicate_pii,
  307. Err(KeyRingError::DuplicateKeyId { .. })
  308. ));
  309. }
  310. #[test]
  311. fn keyring_resolves_retained_ids_newest_first() -> Result<(), KeyRingError> {
  312. let ring = KeyRing::from_parts(
  313. vec![pepper("v2", "new"), pepper("v1", "old")],
  314. vec![pepper("v1", "totp")],
  315. vec![pepper("v1", "pii")],
  316. )?;
  317. let active = ring.active_pepper();
  318. assert!(matches!(active, Some((id, _)) if id.as_str() == "v2"));
  319. let retained = ring.pepper_by_id("v1");
  320. assert!(matches!(retained, Some(secret) if secret.expose_secret() == "old"));
  321. assert!(ring.pepper_by_id("v9").is_none());
  322. Ok(())
  323. }
  324. #[test]
  325. fn keyring_resolves_retained_pii_id() -> Result<(), KeyRingError> {
  326. let ring = KeyRing::from_parts(
  327. vec![pepper("v1", "pepper-secret")],
  328. vec![pepper("v1", "totp-secret")],
  329. vec![
  330. pepper("v2", "new-pii-secret"),
  331. pepper("v1", "old-pii-secret"),
  332. ],
  333. )?;
  334. let active = ring.active_pii_key();
  335. assert!(matches!(active, Some((id, _)) if id.as_str() == "v2"));
  336. let retained = ring.pii_key_by_id("v1");
  337. assert!(matches!(retained, Some(secret) if secret.expose_secret() == "old-pii-secret"));
  338. assert!(ring.pii_key_by_id("v9").is_none());
  339. let ids: Vec<&str> = ring
  340. .pii_keys_newest_first()
  341. .map(|(id, _)| id.as_str())
  342. .collect();
  343. assert_eq!(ids, vec!["v2", "v1"]);
  344. // `Debug` exposes ids only, never material from any chain.
  345. let debug = format!("{ring:?}");
  346. assert!(debug.contains("v2"), "debug must include the pii key id");
  347. assert!(!debug.contains("new-pii-secret") && !debug.contains("old-pii-secret"));
  348. assert!(!debug.contains("pepper-secret") && !debug.contains("totp-secret"));
  349. Ok(())
  350. }
  351. #[test]
  352. fn keyring_rejects_dotted_ids() {
  353. let dotted = KeyRing::from_parts(
  354. vec![pepper("v1.evil", "a")],
  355. vec![pepper("v1", "b")],
  356. vec![pepper("v1", "c")],
  357. );
  358. assert!(matches!(dotted, Err(KeyRingError::InvalidKeyId { .. })));
  359. }
  360. #[test]
  361. fn keygen_purpose_documents_no_as_signing_key() {
  362. assert!(KEYGEN_PURPOSE.contains("never an AS signing key"));
  363. assert!(KEYGEN_PURPOSE.contains("no jwks_uri"));
  364. }
  365. }