Skip to main content

cratefield_push_auth/
lib.rs

1//! `cratefield-push-auth`: the provider tokens the push adapters present
2//! (issue #178).
3//!
4//! APNs signs an ES256 JWT with the `.p8` key from the Apple developer
5//! portal. VAPID (RFC 8292) is the *same* construction with different claims
6//! — `aud` is the push-service origin, `sub` a `mailto:`, `exp` at most 24
7//! hours out — and the public key travels alongside it as `k=`. Google's
8//! service-account flow signs an **RS256** JWT and trades it for a bearer
9//! token. Three adapters, one signer, one cache; extracted here so the next
10//! two adapters do not each grow their own copy of
11//! `base64url(header).base64url(claims).base64url(sig)`.
12//!
13//! Everything is pure Rust and builds for `wasm32-unknown-unknown`: no
14//! `jsonwebtoken`, no `ring`, no OpenSSL. Both signatures are deterministic
15//! (RFC 6979 for ES256, PKCS#1 v1.5 for RS256), so no RNG is needed on a
16//! Workers isolate.
17//!
18//! Verifying tokens is deliberately absent — that is the auth service's job.
19#![doc = include_str!("../README.md")]
20#![forbid(unsafe_code)]
21
22mod cache;
23mod es256;
24mod rs256;
25
26pub use cache::CachedToken;
27pub use es256::Es256Signer;
28pub use rs256::Rs256Signer;
29
30use base64::Engine;
31use base64::engine::general_purpose::URL_SAFE_NO_PAD;
32use serde_json::Value;
33
34/// A private key did not parse.
35#[derive(Debug, thiserror::Error)]
36pub enum KeyError {
37    /// The PEM was not the expected kind of private key. The detail is the
38    /// underlying parser's message; it never contains key material.
39    #[error("invalid {kind} private key: {detail}")]
40    Parse {
41        /// `ES256` or `RS256` — which signer refused it.
42        kind: &'static str,
43        detail: String,
44    },
45}
46
47impl KeyError {
48    fn parse(kind: &'static str, detail: impl std::fmt::Display) -> Self {
49        KeyError::Parse {
50            kind,
51            detail: detail.to_string(),
52        }
53    }
54}
55
56/// The JWS signing input: `base64url(header) "." base64url(claims)`
57/// (RFC 7515 §5.1). Compact serialisation, so the exact bytes signed are the
58/// exact bytes sent.
59fn signing_input(header: &Value, claims: &Value) -> String {
60    format!(
61        "{}.{}",
62        URL_SAFE_NO_PAD.encode(header.to_string()),
63        URL_SAFE_NO_PAD.encode(claims.to_string()),
64    )
65}