Skip to main content

netget/server/acme/
jws.rs

1//! RFC 7515 flattened JWS as ACME uses it (RFC 8555 ยง6.2): parsing, verification for ES256,
2//! ES384, RS256 and EdDSA, RFC 7638 JWK thumbprints, and ES256 signing for the client.
3use anyhow::{bail, ensure, Context, Result};
4use base64::engine::general_purpose::URL_SAFE_NO_PAD;
5use base64::Engine;
6use ring::signature::{self, KeyPair};
7use serde_json::{json, Map, Value};
8
9pub fn b64(bytes: &[u8]) -> String {
10    URL_SAFE_NO_PAD.encode(bytes)
11}
12
13pub fn unb64(s: &str) -> Result<Vec<u8>> {
14    ensure!(
15        s.bytes()
16            .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_'),
17        "not base64url without padding"
18    );
19    Ok(URL_SAFE_NO_PAD.decode(s)?)
20}
21
22pub fn sha256(bytes: &[u8]) -> Vec<u8> {
23    ring::digest::digest(&ring::digest::SHA256, bytes)
24        .as_ref()
25        .to_vec()
26}
27
28/// One verified-shape ACME request body, before the signature is checked.
29#[derive(Debug, Clone)]
30pub struct Jws {
31    pub alg: String,
32    pub nonce: String,
33    pub url: String,
34    /// Exactly one of `jwk` and `kid` is present.
35    pub jwk: Option<Map<String, Value>>,
36    pub kid: Option<String>,
37    /// The decoded payload; empty for POST-as-GET.
38    pub payload: Vec<u8>,
39    signing_input: Vec<u8>,
40    signature: Vec<u8>,
41}
42
43impl Jws {
44    pub fn parse(body: &[u8]) -> Result<Self> {
45        let v: Value = serde_json::from_slice(body).context("body is not JSON")?;
46        let o = v.as_object().context("JWS is a JSON object")?;
47        ensure!(
48            !o.contains_key("signatures") && !o.contains_key("header"),
49            "only the flattened JWS serialization with a protected header is accepted"
50        );
51        let field = |k: &str| {
52            o.get(k)
53                .and_then(Value::as_str)
54                .with_context(|| format!("JWS {k} missing"))
55        };
56        let (protected_b64, payload_b64, sig_b64) =
57            (field("protected")?, field("payload")?, field("signature")?);
58        let protected: Value = serde_json::from_slice(&unb64(protected_b64)?)
59            .context("protected header is not JSON")?;
60        let p = protected
61            .as_object()
62            .context("protected header is an object")?;
63        let s = |k: &str| p.get(k).and_then(Value::as_str).map(str::to_owned);
64        let alg = s("alg").context("protected header has no alg")?;
65        let nonce = s("nonce").context("protected header has no nonce")?;
66        let url = s("url").context("protected header has no url")?;
67        let jwk = p
68            .get("jwk")
69            .map(|j| j.as_object().cloned().context("jwk is an object"))
70            .transpose()?;
71        let kid = s("kid");
72        ensure!(jwk.is_some() != kid.is_some(), "exactly one of jwk and kid");
73        Ok(Self {
74            alg,
75            nonce,
76            url,
77            jwk,
78            kid,
79            payload: unb64(payload_b64)?,
80            signing_input: format!("{protected_b64}.{payload_b64}").into_bytes(),
81            signature: unb64(sig_b64)?,
82        })
83    }
84
85    pub fn payload_json(&self) -> Result<Value> {
86        ensure!(!self.payload.is_empty(), "payload is empty");
87        serde_json::from_slice(&self.payload).context("payload is not JSON")
88    }
89
90    /// Verify against `jwk` (the request's own, or the account's for a `kid` request).
91    pub fn verify(&self, jwk: &Map<String, Value>) -> Result<()> {
92        let key = PublicKey::from_jwk(jwk)?;
93        ensure!(
94            key.alg_ok(&self.alg),
95            "alg {} does not match the {} key",
96            self.alg,
97            key.kind()
98        );
99        key.verify(&self.alg, &self.signing_input, &self.signature)
100    }
101}
102
103/// The public key of a JWK, validated.
104pub enum PublicKey {
105    Ec { crv: &'static str, point: Vec<u8> },
106    Rsa { n: Vec<u8>, e: Vec<u8> },
107    Ed25519(Vec<u8>),
108}
109
110impl PublicKey {
111    pub fn from_jwk(jwk: &Map<String, Value>) -> Result<Self> {
112        let s = |k: &str| {
113            jwk.get(k)
114                .and_then(Value::as_str)
115                .with_context(|| format!("jwk {k} missing"))
116        };
117        match s("kty")? {
118            "EC" => {
119                let (crv, len) = match s("crv")? {
120                    "P-256" => ("P-256", 32),
121                    "P-384" => ("P-384", 48),
122                    other => bail!("unsupported EC curve {other}"),
123                };
124                let (x, y) = (unb64(s("x")?)?, unb64(s("y")?)?);
125                ensure!(
126                    x.len() == len && y.len() == len,
127                    "EC coordinates have the wrong length"
128                );
129                let mut point = vec![4u8];
130                point.extend(x);
131                point.extend(y);
132                Ok(Self::Ec { crv, point })
133            }
134            "RSA" => {
135                let (n, e) = (unb64(s("n")?)?, unb64(s("e")?)?);
136                ensure!(
137                    (256..=1024).contains(&n.len()) && !e.is_empty() && e.len() <= 8,
138                    "RSA key must be 2048 to 8192 bits"
139                );
140                Ok(Self::Rsa { n, e })
141            }
142            "OKP" => {
143                ensure!(s("crv")? == "Ed25519", "only Ed25519 OKP keys");
144                let x = unb64(s("x")?)?;
145                ensure!(x.len() == 32, "Ed25519 key is 32 bytes");
146                Ok(Self::Ed25519(x))
147            }
148            other => bail!("unsupported key type {other}"),
149        }
150    }
151
152    pub fn kind(&self) -> &'static str {
153        match self {
154            Self::Ec { crv, .. } => crv,
155            Self::Rsa { .. } => "RSA",
156            Self::Ed25519(_) => "Ed25519",
157        }
158    }
159
160    fn alg_ok(&self, alg: &str) -> bool {
161        matches!(
162            (self, alg),
163            (Self::Ec { crv: "P-256", .. }, "ES256")
164                | (Self::Ec { crv: "P-384", .. }, "ES384")
165                | (Self::Rsa { .. }, "RS256")
166                | (Self::Ed25519(_), "EdDSA")
167        )
168    }
169
170    fn verify(&self, alg: &str, msg: &[u8], sig: &[u8]) -> Result<()> {
171        let ok = match (self, alg) {
172            (Self::Ec { point, .. }, "ES256") => {
173                signature::UnparsedPublicKey::new(&signature::ECDSA_P256_SHA256_FIXED, point)
174                    .verify(msg, sig)
175                    .is_ok()
176            }
177            (Self::Ec { point, .. }, "ES384") => {
178                signature::UnparsedPublicKey::new(&signature::ECDSA_P384_SHA384_FIXED, point)
179                    .verify(msg, sig)
180                    .is_ok()
181            }
182            (Self::Rsa { n, e }, "RS256") => signature::RsaPublicKeyComponents { n, e }
183                .verify(&signature::RSA_PKCS1_2048_8192_SHA256, msg, sig)
184                .is_ok(),
185            (Self::Ed25519(x), "EdDSA") => {
186                signature::UnparsedPublicKey::new(&signature::ED25519, x)
187                    .verify(msg, sig)
188                    .is_ok()
189            }
190            _ => false,
191        };
192        ensure!(ok, "JWS signature does not verify");
193        Ok(())
194    }
195}
196
197/// RFC 7638 thumbprint: SHA-256 over the required members in lexicographic order.
198pub fn thumbprint(jwk: &Map<String, Value>) -> Result<String> {
199    let s = |k: &str| {
200        jwk.get(k)
201            .and_then(Value::as_str)
202            .with_context(|| format!("jwk {k} missing"))
203    };
204    let canonical = match s("kty")? {
205        "EC" => json!({"crv": s("crv")?, "kty": "EC", "x": s("x")?, "y": s("y")?}),
206        "RSA" => json!({"e": s("e")?, "kty": "RSA", "n": s("n")?}),
207        "OKP" => json!({"crv": s("crv")?, "kty": "OKP", "x": s("x")?}),
208        other => bail!("unsupported key type {other}"),
209    };
210    // Members are written in lexicographic order and serialised without whitespace.
211    Ok(b64(&sha256(canonical.to_string().as_bytes())))
212}
213
214/// An ES256 account key for the client.
215pub struct AccountKey {
216    pair: signature::EcdsaKeyPair,
217    rng: ring::rand::SystemRandom,
218}
219
220impl AccountKey {
221    pub fn generate() -> Result<Self> {
222        let rng = ring::rand::SystemRandom::new();
223        let pkcs8 = signature::EcdsaKeyPair::generate_pkcs8(
224            &signature::ECDSA_P256_SHA256_FIXED_SIGNING,
225            &rng,
226        )
227        .map_err(|_| anyhow::anyhow!("key generation failed"))?;
228        let pair = signature::EcdsaKeyPair::from_pkcs8(
229            &signature::ECDSA_P256_SHA256_FIXED_SIGNING,
230            pkcs8.as_ref(),
231            &rng,
232        )
233        .map_err(|_| anyhow::anyhow!("key generation failed"))?;
234        Ok(Self { pair, rng })
235    }
236
237    pub fn jwk(&self) -> Map<String, Value> {
238        let p = self.pair.public_key().as_ref();
239        json!({"crv": "P-256", "kty": "EC", "x": b64(&p[1..33]), "y": b64(&p[33..65])})
240            .as_object()
241            .cloned()
242            .unwrap_or_default()
243    }
244
245    /// A flattened JWS for `url`; `kid` selects kid over the embedded jwk; `None` payload is
246    /// POST-as-GET.
247    pub fn sign(
248        &self,
249        url: &str,
250        nonce: &str,
251        kid: Option<&str>,
252        payload: Option<&Value>,
253    ) -> Result<Value> {
254        let mut protected = json!({"alg": "ES256", "nonce": nonce, "url": url});
255        match kid {
256            Some(k) => protected["kid"] = json!(k),
257            None => protected["jwk"] = Value::Object(self.jwk()),
258        }
259        let protected_b64 = b64(protected.to_string().as_bytes());
260        let payload_b64 = payload
261            .map(|p| b64(p.to_string().as_bytes()))
262            .unwrap_or_default();
263        let sig = self
264            .pair
265            .sign(
266                &self.rng,
267                format!("{protected_b64}.{payload_b64}").as_bytes(),
268            )
269            .map_err(|_| anyhow::anyhow!("signing failed"))?;
270        Ok(
271            json!({"protected": protected_b64, "payload": payload_b64, "signature": b64(sig.as_ref())}),
272        )
273    }
274}