Skip to content

Commit 9cadce0

Browse files
committed
Add minimal implementation of JWK/JWS.
This change implements a subset of JOSE specifications sufficient for RFC8555: JSON Web Signature with RS256, ES256, ES384 and ES512 algorithms (RFC7515, RFC7518) and JSON Web Key Thumbprint (RFC7638).
1 parent 1141c24 commit 9cadce0

File tree

4 files changed

+333
-0
lines changed

4 files changed

+333
-0
lines changed

Cargo.lock

Lines changed: 47 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,14 @@ rust-version = "1.81.0"
1010
crate-type = ["cdylib"]
1111

1212
[dependencies]
13+
base64 = "0.22.1"
1314
http = "1.3.1"
1415
libc = "0.2.174"
1516
openssl = { version = "0.10.73", features = ["bindgen"] }
1617
openssl-foreign-types = { package = "foreign-types", version = "0.3" }
1718
openssl-sys = { version = "0.9.109", features = ["bindgen"] }
19+
serde = { version = "1.0.219", features = ["derive"] }
20+
serde_json = "1.0.142"
1821
siphasher = { version = "1.0.1", default-features = false }
1922
thiserror = { version = "2.0.12", default-features = false }
2023
zeroize = "1.8.1"

src/jws.rs

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
// Copyright (c) F5, Inc.
2+
//
3+
// This source code is licensed under the Apache License, Version 2.0 license found in the
4+
// LICENSE file in the root directory of this source tree.
5+
6+
use std::borrow::ToOwned;
7+
use std::string::String;
8+
9+
use ngx::collections::{vec, Vec};
10+
use openssl::bn::{BigNum, BigNumContext, BigNumRef};
11+
use openssl::error::ErrorStack;
12+
use openssl::nid::Nid;
13+
use openssl::pkey::{Id, PKey, PKeyRef, Private};
14+
use openssl_foreign_types::ForeignTypeRef;
15+
use serde::{ser::SerializeMap, Serialize, Serializer};
16+
use thiserror::Error;
17+
18+
/// A JWS header, as defined in RFC 8555 Section 6.2.
19+
#[derive(Serialize)]
20+
struct JwsHeader<'a, Jwk: JsonWebKey> {
21+
pub alg: &'a str,
22+
pub nonce: &'a str,
23+
pub url: &'a str,
24+
// Per 8555 6.2, "jwk" and "kid" fields are mutually exclusive.
25+
#[serde(flatten)]
26+
pub key: JwsHeaderKey<'a, Jwk>,
27+
}
28+
29+
#[derive(Serialize)]
30+
#[serde(untagged)]
31+
enum JwsHeaderKey<'a, Jwk: JsonWebKey> {
32+
Jwk { jwk: &'a Jwk },
33+
Kid { kid: &'a str },
34+
}
35+
36+
#[derive(Debug, Error)]
37+
pub enum Error {
38+
#[error("serialize failed: {0}")]
39+
Serialize(#[from] serde_json::Error),
40+
#[error("crypto: {0}")]
41+
Crypto(#[from] openssl::error::ErrorStack),
42+
}
43+
44+
#[derive(Debug, Error)]
45+
pub enum NewKeyError {
46+
#[error("unsupported key algorithm ({0:?})")]
47+
Algorithm(Id),
48+
#[error("unsupported key size ({0})")]
49+
Size(u32),
50+
}
51+
52+
pub trait JsonWebKey: Serialize {
53+
fn alg(&self) -> &str;
54+
fn compute_mac(&self, header: &[u8], payload: &[u8]) -> Result<Vec<u8>, Error>;
55+
fn thumbprint(&self) -> Result<String, Error>;
56+
}
57+
58+
#[derive(Debug)]
59+
pub(crate) struct ShaWithEcdsaKey(PKey<Private>);
60+
61+
#[derive(Debug)]
62+
pub(crate) struct ShaWithRsaKey(PKey<Private>);
63+
64+
#[inline]
65+
pub fn base64url<T: AsRef<[u8]>>(buf: T) -> String {
66+
base64::Engine::encode(&base64::engine::general_purpose::URL_SAFE_NO_PAD, buf)
67+
}
68+
69+
pub fn sign_jws<Jwk: JsonWebKey>(
70+
jwk: &Jwk,
71+
kid: Option<&str>,
72+
url: &str,
73+
nonce: &str,
74+
payload: &[u8],
75+
) -> Result<String, Error> {
76+
let key = match kid {
77+
Some(kid) => JwsHeaderKey::Kid { kid },
78+
None => JwsHeaderKey::Jwk { jwk },
79+
};
80+
81+
let header = JwsHeader {
82+
alg: jwk.alg(),
83+
nonce,
84+
url,
85+
key,
86+
};
87+
88+
let header_json = serde_json::to_vec(&header)?;
89+
let header = base64url(&header_json);
90+
let payload = base64url(payload);
91+
let signature = jwk.compute_mac(header.as_bytes(), payload.as_bytes())?;
92+
let signature = base64url(signature);
93+
94+
Ok(std::format!(
95+
r#"{{"protected":"{header}","payload":"{payload}","signature":"{signature}"}}"#
96+
))
97+
}
98+
99+
impl JsonWebKey for ShaWithEcdsaKey {
100+
fn alg(&self) -> &str {
101+
match self.0.bits() {
102+
256 => "ES256",
103+
384 => "ES384",
104+
521 => "ES512",
105+
_ => unreachable!("unsupported key size"),
106+
}
107+
}
108+
109+
fn compute_mac(&self, header: &[u8], payload: &[u8]) -> Result<Vec<u8>, Error> {
110+
let bits = self.0.bits() as usize;
111+
let pad_to = bits.div_ceil(8);
112+
113+
let md = match bits {
114+
384 => openssl::hash::MessageDigest::sha384(),
115+
521 => openssl::hash::MessageDigest::sha512(),
116+
_ => openssl::hash::MessageDigest::sha256(),
117+
};
118+
119+
let mut signer = openssl::sign::Signer::new(md, &self.0)?;
120+
signer.update(header)?;
121+
signer.update(b".")?;
122+
signer.update(payload)?;
123+
124+
let mut buf = vec![0u8; signer.len()?];
125+
126+
let len = signer.sign(&mut buf)?;
127+
buf.truncate(len);
128+
129+
let sig = openssl::ecdsa::EcdsaSig::from_der(&buf)?;
130+
buf.resize(2 * pad_to, 0);
131+
132+
bn2binpad(sig.r(), &mut buf[0..pad_to])?;
133+
bn2binpad(sig.s(), &mut buf[pad_to..])?;
134+
135+
Ok(buf)
136+
}
137+
138+
fn thumbprint(&self) -> Result<String, Error> {
139+
let data = serde_json::to_vec(self)?;
140+
Ok(base64url(openssl::sha::sha256(&data)))
141+
}
142+
}
143+
144+
impl Serialize for ShaWithEcdsaKey {
145+
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
146+
use serde::ser::Error;
147+
148+
let ec_key = self.0.ec_key().map_err(Error::custom)?;
149+
let group = ec_key.group();
150+
151+
let (crv, bits): (_, usize) = match group.curve_name() {
152+
Some(Nid::X9_62_PRIME256V1) => ("P-256", 256),
153+
Some(Nid::SECP384R1) => ("P-384", 384),
154+
Some(Nid::SECP521R1) => ("P-521", 521),
155+
_ => return Err(Error::custom("unsupported curve")),
156+
};
157+
158+
let mut x = BigNum::new().map_err(Error::custom)?;
159+
let mut y = BigNum::new().map_err(Error::custom)?;
160+
let mut ctx = BigNumContext::new().map_err(Error::custom)?;
161+
ec_key
162+
.public_key()
163+
.affine_coordinates(group, &mut x, &mut y, &mut ctx)
164+
.map_err(Error::custom)?;
165+
166+
let mut buf = vec![0u8; bits.div_ceil(8)];
167+
168+
let x = base64url(bn2binpad(&x, &mut buf).map_err(Error::custom)?);
169+
let y = base64url(bn2binpad(&y, &mut buf).map_err(Error::custom)?);
170+
171+
let mut map = serializer.serialize_map(Some(4))?;
172+
// order is important for thumbprint generation (RFC7638)
173+
map.serialize_entry("crv", crv)?;
174+
map.serialize_entry("kty", "EC")?;
175+
map.serialize_entry("x", &x)?;
176+
map.serialize_entry("y", &y)?;
177+
map.end()
178+
}
179+
}
180+
181+
impl TryFrom<&PKeyRef<Private>> for ShaWithEcdsaKey {
182+
type Error = NewKeyError;
183+
184+
fn try_from(pkey: &PKeyRef<Private>) -> Result<Self, Self::Error> {
185+
if pkey.id() != Id::EC {
186+
return Err(NewKeyError::Algorithm(pkey.id()));
187+
}
188+
189+
let bits = pkey.bits();
190+
if !matches!(bits, 256 | 384 | 521) {
191+
return Err(NewKeyError::Size(bits));
192+
}
193+
194+
Ok(Self(pkey.to_owned()))
195+
}
196+
}
197+
198+
impl JsonWebKey for ShaWithRsaKey {
199+
fn alg(&self) -> &str {
200+
"RS256"
201+
}
202+
203+
fn compute_mac(&self, header: &[u8], payload: &[u8]) -> Result<Vec<u8>, Error> {
204+
let md = openssl::hash::MessageDigest::sha256();
205+
206+
let mut signer = openssl::sign::Signer::new(md, &self.0)?;
207+
signer.update(header)?;
208+
signer.update(b".")?;
209+
signer.update(payload)?;
210+
211+
let mut buf = vec![0u8; signer.len()?];
212+
213+
let len = signer.sign(&mut buf)?;
214+
buf.truncate(len);
215+
216+
Ok(buf)
217+
}
218+
219+
fn thumbprint(&self) -> Result<String, Error> {
220+
let data = serde_json::to_vec(self)?;
221+
Ok(base64url(openssl::sha::sha256(&data)))
222+
}
223+
}
224+
225+
impl Serialize for ShaWithRsaKey {
226+
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
227+
use serde::ser::Error;
228+
229+
let rsa = self.0.rsa().map_err(Error::custom)?;
230+
231+
let num_bytes = rsa.e().num_bytes().max(rsa.n().num_bytes()) as usize;
232+
let mut buf = vec![0u8; num_bytes];
233+
234+
let e = base64url(bn2bin(rsa.e(), &mut buf).map_err(Error::custom)?);
235+
let n = base64url(bn2bin(rsa.n(), &mut buf).map_err(Error::custom)?);
236+
237+
let mut map = serializer.serialize_map(Some(3))?;
238+
// order is important for thumbprint generation (RFC7638)
239+
map.serialize_entry("e", &e)?;
240+
map.serialize_entry("kty", "RSA")?;
241+
map.serialize_entry("n", &n)?;
242+
map.end()
243+
}
244+
}
245+
246+
impl TryFrom<&PKeyRef<Private>> for ShaWithRsaKey {
247+
type Error = NewKeyError;
248+
249+
fn try_from(pkey: &PKeyRef<Private>) -> Result<Self, Self::Error> {
250+
if pkey.id() != Id::RSA {
251+
return Err(NewKeyError::Algorithm(pkey.id()));
252+
}
253+
254+
let bits = pkey.bits();
255+
if bits < 2048 {
256+
return Err(NewKeyError::Size(bits));
257+
}
258+
259+
Ok(Self(pkey.to_owned()))
260+
}
261+
}
262+
263+
/// [openssl] offers [BigNumRef::to_vec()], but we want to avoid an extra allocation.
264+
fn bn2bin<'a>(bn: &BigNumRef, out: &'a mut [u8]) -> Result<&'a [u8], ErrorStack> {
265+
debug_assert!(bn.num_bytes() as usize <= out.len());
266+
let n = unsafe { openssl_sys::BN_bn2bin(bn.as_ptr(), out.as_mut_ptr()) };
267+
if n >= 0 {
268+
Ok(&out[..n as usize])
269+
} else {
270+
Err(ErrorStack::get())
271+
}
272+
}
273+
274+
/// [openssl] offers [BigNumRef::to_vec_padded()], but we want to avoid an extra allocation.
275+
fn bn2binpad<'a>(bn: &BigNumRef, out: &'a mut [u8]) -> Result<&'a [u8], ErrorStack> {
276+
let n = unsafe { openssl_sys::BN_bn2binpad(bn.as_ptr(), out.as_mut_ptr(), out.len() as _) };
277+
if n >= 0 {
278+
Ok(&out[..n as usize])
279+
} else {
280+
Err(ErrorStack::get())
281+
}
282+
}

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use crate::conf::{AcmeMainConfig, AcmeServerConfig, NGX_HTTP_ACME_COMMANDS};
1414
use crate::variables::NGX_HTTP_ACME_VARS;
1515

1616
mod conf;
17+
mod jws;
1718
mod state;
1819
mod time;
1920
mod variables;

0 commit comments

Comments
 (0)