1use ring::{hkdf, hmac};
2use sofie_core::encoding::base64::{self, URL_SAFE};
3
4use crate::SigningKeys;
5use crate::session::KEY_LENGTH;
6
7const SIGNING_PURPOSE: &[u8] = b"sofie-signature-v1";
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub enum Signed {
12 Link,
13 StoredFile,
14 PrincipalToken,
15 UploadSeal,
16 ShieldBlock,
17}
18
19const PURPOSES: [Signed; 5] = [
20 Signed::Link,
21 Signed::StoredFile,
22 Signed::PrincipalToken,
23 Signed::UploadSeal,
24 Signed::ShieldBlock,
25];
26
27impl Signed {
28 const fn info(self) -> &'static [u8] {
29 match self {
30 Self::Link => b"link",
31 Self::StoredFile => b"stored-file",
32 Self::PrincipalToken => b"principal-token",
33 Self::UploadSeal => b"upload-seal",
34 Self::ShieldBlock => b"shield-block",
35 }
36 }
37}
38
39pub(crate) struct PurposeKeys([hmac::Key; PURPOSES.len()]);
40
41impl PurposeKeys {
42 fn of(&self, purpose: Signed) -> &hmac::Key {
43 &self.0[purpose as usize]
44 }
45}
46
47impl SigningKeys {
48 #[must_use]
49 pub fn sign(&self, purpose: Signed, message: &[u8]) -> String {
50 let tag = hmac::sign(self.first().signing.of(purpose), message);
51 base64::encode(&URL_SAFE, tag.as_ref())
52 }
53
54 #[must_use]
55 pub fn verifies(&self, purpose: Signed, message: &[u8], signature: &str) -> bool {
56 base64::decode(&URL_SAFE, signature).is_some_and(|tag| {
57 self.all()
58 .any(|key| hmac::verify(key.signing.of(purpose), message, &tag).is_ok())
59 })
60 }
61}
62
63pub(crate) fn purpose_keys(key: &[u8; KEY_LENGTH]) -> PurposeKeys {
64 let secret = hkdf::Salt::new(hkdf::HKDF_SHA256, &[]).extract(key);
65 PurposeKeys(PURPOSES.map(|purpose| {
66 secret
67 .expand(&[SIGNING_PURPOSE, purpose.info()], hmac::HMAC_SHA256)
68 .map(hmac::Key::from)
69 .expect("HKDF-SHA256 expands to the 32 bytes of an HMAC-SHA256 key")
70 }))
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76 use crate::session::tests::{claims, keys};
77
78 #[test]
79 fn a_signature_verifies_only_its_message_under_a_key_still_held() {
80 let message = b"/files/1?expires=1700000000";
81 let signature = keys(1).sign(Signed::Link, message);
82 let rotated = SigningKeys::new(&[[2; KEY_LENGTH], [1; KEY_LENGTH]]).unwrap();
83
84 assert!(keys(1).verifies(Signed::Link, message, &signature));
85 assert!(rotated.verifies(Signed::Link, message, &signature));
86 assert!(!keys(1).verifies(Signed::Link, b"/files/2?expires=1700000000", &signature));
87 assert!(!keys(1).verifies(Signed::StoredFile, message, &signature));
88 assert!(!keys(2).verifies(Signed::Link, message, &signature));
89 assert!(!keys(1).verifies(Signed::Link, message, "not base64!"));
90 assert!(!keys(1).verifies(Signed::Link, message, &claims().seal(&keys(1)).unwrap()));
91 }
92}