Skip to content

Commit cbca7ef

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 2557fb5 commit cbca7ef

File tree

4 files changed

+328
-0
lines changed

4 files changed

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

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)