atagen

malice

public
Star0
Watch0
a6320fb35dc65d5edb74bcf9692c022cf00b436d
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git

Blame: crates/malice-push/src/vapid.rs

aa3dce5114d0atagen2026-10-26 06:09 UTC1
use std::{
aa3dce5114d0atagen2026-10-26 06:09 UTC2
    io::{ErrorKind, Write},
aa3dce5114d0atagen2026-10-26 06:09 UTC3
    path::Path,
aa3dce5114d0atagen2026-10-26 06:09 UTC4
    time::{SystemTime, UNIX_EPOCH},
aa3dce5114d0atagen2026-10-26 06:09 UTC5
};
aa3dce5114d0atagen2026-10-26 06:09 UTC6
aa3dce5114d0atagen2026-10-26 06:09 UTC7
use aws_lc_rs::{
aa3dce5114d0atagen2026-10-26 06:09 UTC8
    rand::SystemRandom,
aa3dce5114d0atagen2026-10-26 06:09 UTC9
    signature::{ECDSA_P256_SHA256_FIXED_SIGNING, EcdsaKeyPair, KeyPair},
aa3dce5114d0atagen2026-10-26 06:09 UTC10
};
aa3dce5114d0atagen2026-10-26 06:09 UTC11
use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
aa3dce5114d0atagen2026-10-26 06:09 UTC12
aa3dce5114d0atagen2026-10-26 06:09 UTC13
const TOKEN_LIFETIME_SECS: u64 = 12 * 60 * 60;
aa3dce5114d0atagen2026-10-26 06:09 UTC14
aa3dce5114d0atagen2026-10-26 06:09 UTC15
/// The server's VAPID identity (RFC 8292), an ES256 key kept on disk.
aa3dce5114d0atagen2026-10-26 06:09 UTC16
pub struct Vapid {
aa3dce5114d0atagen2026-10-26 06:09 UTC17
    key: EcdsaKeyPair,
aa3dce5114d0atagen2026-10-26 06:09 UTC18
    public: String,
aa3dce5114d0atagen2026-10-26 06:09 UTC19
}
aa3dce5114d0atagen2026-10-26 06:09 UTC20
aa3dce5114d0atagen2026-10-26 06:09 UTC21
impl Vapid {
aa3dce5114d0atagen2026-10-26 06:09 UTC22
    /// Reads the PKCS#8 key at `path`, or creates it owner-only.
aa3dce5114d0atagen2026-10-26 06:09 UTC23
    pub fn load_or_create(path: &Path) -> Result<Self, String> {
aa3dce5114d0atagen2026-10-26 06:09 UTC24
        match std::fs::read(path) {
aa3dce5114d0atagen2026-10-26 06:09 UTC25
            Ok(bytes) => return Self::from_pkcs8(&bytes),
aa3dce5114d0atagen2026-10-26 06:09 UTC26
            Err(error) if error.kind() != ErrorKind::NotFound => {
aa3dce5114d0atagen2026-10-26 06:09 UTC27
                return Err(format!("reading the VAPID key: {error}"));
aa3dce5114d0atagen2026-10-26 06:09 UTC28
            }
aa3dce5114d0atagen2026-10-26 06:09 UTC29
            Err(_) => {}
aa3dce5114d0atagen2026-10-26 06:09 UTC30
        }
aa3dce5114d0atagen2026-10-26 06:09 UTC31
        let document =
aa3dce5114d0atagen2026-10-26 06:09 UTC32
            EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, &SystemRandom::new())
aa3dce5114d0atagen2026-10-26 06:09 UTC33
                .map_err(|_| "generating the VAPID key failed".to_owned())?;
aa3dce5114d0atagen2026-10-26 06:09 UTC34
        let mut options = std::fs::OpenOptions::new();
aa3dce5114d0atagen2026-10-26 06:09 UTC35
        options.write(true).create_new(true);
aa3dce5114d0atagen2026-10-26 06:09 UTC36
        #[cfg(unix)]
aa3dce5114d0atagen2026-10-26 06:09 UTC37
        {
aa3dce5114d0atagen2026-10-26 06:09 UTC38
            use std::os::unix::fs::OpenOptionsExt;
aa3dce5114d0atagen2026-10-26 06:09 UTC39
            options.mode(0o600);
aa3dce5114d0atagen2026-10-26 06:09 UTC40
        }
aa3dce5114d0atagen2026-10-26 06:09 UTC41
        match options.open(path) {
aa3dce5114d0atagen2026-10-26 06:09 UTC42
            Ok(mut file) => {
aa3dce5114d0atagen2026-10-26 06:09 UTC43
                if let Err(error) = file
aa3dce5114d0atagen2026-10-26 06:09 UTC44
                    .write_all(document.as_ref())
aa3dce5114d0atagen2026-10-26 06:09 UTC45
                    .and_then(|_| file.sync_all())
aa3dce5114d0atagen2026-10-26 06:09 UTC46
                {
aa3dce5114d0atagen2026-10-26 06:09 UTC47
                    let _ = std::fs::remove_file(path);
aa3dce5114d0atagen2026-10-26 06:09 UTC48
                    return Err(format!("writing the VAPID key: {error}"));
aa3dce5114d0atagen2026-10-26 06:09 UTC49
                }
aa3dce5114d0atagen2026-10-26 06:09 UTC50
                Self::from_pkcs8(document.as_ref())
aa3dce5114d0atagen2026-10-26 06:09 UTC51
            }
aa3dce5114d0atagen2026-10-26 06:09 UTC52
            Err(error) if error.kind() == ErrorKind::AlreadyExists => {
aa3dce5114d0atagen2026-10-26 06:09 UTC53
                let bytes = std::fs::read(path)
aa3dce5114d0atagen2026-10-26 06:09 UTC54
                    .map_err(|error| format!("reading the VAPID key: {error}"))?;
aa3dce5114d0atagen2026-10-26 06:09 UTC55
                Self::from_pkcs8(&bytes)
aa3dce5114d0atagen2026-10-26 06:09 UTC56
            }
aa3dce5114d0atagen2026-10-26 06:09 UTC57
            Err(error) => Err(format!("creating the VAPID key: {error}")),
aa3dce5114d0atagen2026-10-26 06:09 UTC58
        }
aa3dce5114d0atagen2026-10-26 06:09 UTC59
    }
aa3dce5114d0atagen2026-10-26 06:09 UTC60
aa3dce5114d0atagen2026-10-26 06:09 UTC61
    fn from_pkcs8(bytes: &[u8]) -> Result<Self, String> {
aa3dce5114d0atagen2026-10-26 06:09 UTC62
        let key = EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, bytes)
aa3dce5114d0atagen2026-10-26 06:09 UTC63
            .map_err(|_| "the VAPID key file is not a P-256 PKCS#8 key".to_owned())?;
aa3dce5114d0atagen2026-10-26 06:09 UTC64
        let public = URL_SAFE_NO_PAD.encode(key.public_key().as_ref());
aa3dce5114d0atagen2026-10-26 06:09 UTC65
        Ok(Self { key, public })
aa3dce5114d0atagen2026-10-26 06:09 UTC66
    }
aa3dce5114d0atagen2026-10-26 06:09 UTC67
aa3dce5114d0atagen2026-10-26 06:09 UTC68
    /// The uncompressed public point, base64url, as `applicationServerKey` takes it.
aa3dce5114d0atagen2026-10-26 06:09 UTC69
    pub fn public_key(&self) -> &str {
aa3dce5114d0atagen2026-10-26 06:09 UTC70
        &self.public
aa3dce5114d0atagen2026-10-26 06:09 UTC71
    }
aa3dce5114d0atagen2026-10-26 06:09 UTC72
aa3dce5114d0atagen2026-10-26 06:09 UTC73
    /// The `Authorization` header for a push service at `audience` (its origin).
aa3dce5114d0atagen2026-10-26 06:09 UTC74
    pub fn authorization(&self, audience: &str, contact: &str) -> Result<String, String> {
aa3dce5114d0atagen2026-10-26 06:09 UTC75
        let now = SystemTime::now()
aa3dce5114d0atagen2026-10-26 06:09 UTC76
            .duration_since(UNIX_EPOCH)
aa3dce5114d0atagen2026-10-26 06:09 UTC77
            .map_err(|_| "the clock is before 1970".to_owned())?
aa3dce5114d0atagen2026-10-26 06:09 UTC78
            .as_secs();
aa3dce5114d0atagen2026-10-26 06:09 UTC79
        let header = URL_SAFE_NO_PAD.encode(br#"{"typ":"JWT","alg":"ES256"}"#);
aa3dce5114d0atagen2026-10-26 06:09 UTC80
        let claims = serde_json::json!({
aa3dce5114d0atagen2026-10-26 06:09 UTC81
            "aud": audience,
aa3dce5114d0atagen2026-10-26 06:09 UTC82
            "exp": now + TOKEN_LIFETIME_SECS,
aa3dce5114d0atagen2026-10-26 06:09 UTC83
            "sub": contact,
aa3dce5114d0atagen2026-10-26 06:09 UTC84
        });
aa3dce5114d0atagen2026-10-26 06:09 UTC85
        let claims = URL_SAFE_NO_PAD.encode(claims.to_string());
aa3dce5114d0atagen2026-10-26 06:09 UTC86
        let signing_input = format!("{header}.{claims}");
aa3dce5114d0atagen2026-10-26 06:09 UTC87
        let signature = self
aa3dce5114d0atagen2026-10-26 06:09 UTC88
            .key
aa3dce5114d0atagen2026-10-26 06:09 UTC89
            .sign(&SystemRandom::new(), signing_input.as_bytes())
aa3dce5114d0atagen2026-10-26 06:09 UTC90
            .map_err(|_| "signing the VAPID token failed".to_owned())?;
aa3dce5114d0atagen2026-10-26 06:09 UTC91
        Ok(format!(
aa3dce5114d0atagen2026-10-26 06:09 UTC92
            "vapid t={signing_input}.{}, k={}",
aa3dce5114d0atagen2026-10-26 06:09 UTC93
            URL_SAFE_NO_PAD.encode(signature.as_ref()),
aa3dce5114d0atagen2026-10-26 06:09 UTC94
            self.public
aa3dce5114d0atagen2026-10-26 06:09 UTC95
        ))
aa3dce5114d0atagen2026-10-26 06:09 UTC96
    }
aa3dce5114d0atagen2026-10-26 06:09 UTC97
}
aa3dce5114d0atagen2026-10-26 06:09 UTC98
aa3dce5114d0atagen2026-10-26 06:09 UTC99
#[cfg(test)]
aa3dce5114d0atagen2026-10-26 06:09 UTC100
mod tests {
aa3dce5114d0atagen2026-10-26 06:09 UTC101
    use aws_lc_rs::signature::{ECDSA_P256_SHA256_FIXED, UnparsedPublicKey};
aa3dce5114d0atagen2026-10-26 06:09 UTC102
aa3dce5114d0atagen2026-10-26 06:09 UTC103
    use super::*;
aa3dce5114d0atagen2026-10-26 06:09 UTC104
aa3dce5114d0atagen2026-10-26 06:09 UTC105
    #[test]
aa3dce5114d0atagen2026-10-26 06:09 UTC106
    fn key_persists_owner_only_and_signs_verifiable_tokens() {
aa3dce5114d0atagen2026-10-26 06:09 UTC107
        let dir = tempfile::tempdir().unwrap();
aa3dce5114d0atagen2026-10-26 06:09 UTC108
        let path = dir.path().join("vapid.pk8");
aa3dce5114d0atagen2026-10-26 06:09 UTC109
        let first = Vapid::load_or_create(&path).unwrap();
aa3dce5114d0atagen2026-10-26 06:09 UTC110
        let again = Vapid::load_or_create(&path).unwrap();
aa3dce5114d0atagen2026-10-26 06:09 UTC111
        assert_eq!(first.public_key(), again.public_key());
aa3dce5114d0atagen2026-10-26 06:09 UTC112
        let public = URL_SAFE_NO_PAD.decode(first.public_key()).unwrap();
aa3dce5114d0atagen2026-10-26 06:09 UTC113
        assert_eq!(public.len(), 65);
aa3dce5114d0atagen2026-10-26 06:09 UTC114
        assert_eq!(public[0], 4);
aa3dce5114d0atagen2026-10-26 06:09 UTC115
        #[cfg(unix)]
aa3dce5114d0atagen2026-10-26 06:09 UTC116
        {
aa3dce5114d0atagen2026-10-26 06:09 UTC117
            use std::os::unix::fs::PermissionsExt;
aa3dce5114d0atagen2026-10-26 06:09 UTC118
            let mode = std::fs::metadata(&path).unwrap().permissions().mode();
aa3dce5114d0atagen2026-10-26 06:09 UTC119
            assert_eq!(mode & 0o777, 0o600);
aa3dce5114d0atagen2026-10-26 06:09 UTC120
        }
aa3dce5114d0atagen2026-10-26 06:09 UTC121
aa3dce5114d0atagen2026-10-26 06:09 UTC122
        let header = first
aa3dce5114d0atagen2026-10-26 06:09 UTC123
            .authorization("https://fcm.googleapis.com", "mailto:a@example.org")
aa3dce5114d0atagen2026-10-26 06:09 UTC124
            .unwrap();
aa3dce5114d0atagen2026-10-26 06:09 UTC125
        let (token, key) = header
aa3dce5114d0atagen2026-10-26 06:09 UTC126
            .strip_prefix("vapid t=")
aa3dce5114d0atagen2026-10-26 06:09 UTC127
            .unwrap()
aa3dce5114d0atagen2026-10-26 06:09 UTC128
            .split_once(", k=")
aa3dce5114d0atagen2026-10-26 06:09 UTC129
            .unwrap();
aa3dce5114d0atagen2026-10-26 06:09 UTC130
        assert_eq!(key, first.public_key());
aa3dce5114d0atagen2026-10-26 06:09 UTC131
        let (input, signature) = token.rsplit_once('.').unwrap();
aa3dce5114d0atagen2026-10-26 06:09 UTC132
        UnparsedPublicKey::new(&ECDSA_P256_SHA256_FIXED, &public)
aa3dce5114d0atagen2026-10-26 06:09 UTC133
            .verify(
aa3dce5114d0atagen2026-10-26 06:09 UTC134
                input.as_bytes(),
aa3dce5114d0atagen2026-10-26 06:09 UTC135
                &URL_SAFE_NO_PAD.decode(signature).unwrap(),
aa3dce5114d0atagen2026-10-26 06:09 UTC136
            )
aa3dce5114d0atagen2026-10-26 06:09 UTC137
            .unwrap();
aa3dce5114d0atagen2026-10-26 06:09 UTC138
        let claims: serde_json::Value = serde_json::from_slice(
aa3dce5114d0atagen2026-10-26 06:09 UTC139
            &URL_SAFE_NO_PAD
aa3dce5114d0atagen2026-10-26 06:09 UTC140
                .decode(input.split_once('.').unwrap().1)
aa3dce5114d0atagen2026-10-26 06:09 UTC141
                .unwrap(),
aa3dce5114d0atagen2026-10-26 06:09 UTC142
        )
aa3dce5114d0atagen2026-10-26 06:09 UTC143
        .unwrap();
aa3dce5114d0atagen2026-10-26 06:09 UTC144
        assert_eq!(claims["aud"], "https://fcm.googleapis.com");
aa3dce5114d0atagen2026-10-26 06:09 UTC145
        assert_eq!(claims["sub"], "mailto:a@example.org");
aa3dce5114d0atagen2026-10-26 06:09 UTC146
    }
aa3dce5114d0atagen2026-10-26 06:09 UTC147
}