flesh out jwt code

This commit is contained in:
Conrad Ludgate
2024-08-12 09:01:04 +01:00
parent 9f38ab39c6
commit 262378e561
4 changed files with 380 additions and 102 deletions
Generated
+147 -3
View File
@@ -1548,6 +1548,33 @@ dependencies = [
"typenum",
]
[[package]]
name = "curve25519-dalek"
version = "4.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
dependencies = [
"cfg-if",
"cpufeatures",
"curve25519-dalek-derive",
"digest",
"fiat-crypto",
"rustc_version",
"subtle",
"zeroize",
]
[[package]]
name = "curve25519-dalek-derive"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.52",
]
[[package]]
name = "darling"
version = "0.20.1"
@@ -1629,6 +1656,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c"
dependencies = [
"const-oid",
"pem-rfc7468",
"zeroize",
]
@@ -1781,10 +1809,48 @@ checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c"
dependencies = [
"der 0.6.1",
"elliptic-curve 0.12.3",
"rfc6979",
"rfc6979 0.3.1",
"signature 1.6.4",
]
[[package]]
name = "ecdsa"
version = "0.16.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
dependencies = [
"der 0.7.8",
"digest",
"elliptic-curve 0.13.8",
"rfc6979 0.4.0",
"signature 2.2.0",
"spki 0.7.3",
]
[[package]]
name = "ed25519"
version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
dependencies = [
"pkcs8 0.10.2",
"signature 2.2.0",
]
[[package]]
name = "ed25519-dalek"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871"
dependencies = [
"curve25519-dalek",
"ed25519",
"serde",
"sha2",
"subtle",
"zeroize",
]
[[package]]
name = "either"
version = "1.8.1"
@@ -1823,6 +1889,9 @@ dependencies = [
"ff 0.13.0",
"generic-array",
"group 0.13.0",
"hkdf",
"pem-rfc7468",
"pkcs8 0.10.2",
"rand_core 0.6.4",
"sec1 0.7.3",
"subtle",
@@ -1988,6 +2057,12 @@ dependencies = [
"subtle",
]
[[package]]
name = "fiat-crypto"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
[[package]]
name = "filetime"
version = "0.2.22"
@@ -2404,6 +2479,15 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46"
[[package]]
name = "hkdf"
version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
dependencies = [
"hmac",
]
[[package]]
name = "hmac"
version = "0.12.1"
@@ -2885,6 +2969,20 @@ dependencies = [
"simple_asn1",
]
[[package]]
name = "k256"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f01b677d82ef7a676aa37e099defd83a28e15687112cafdd112d60236b6115b"
dependencies = [
"cfg-if",
"ecdsa 0.16.9",
"elliptic-curve 0.13.8",
"once_cell",
"sha2",
"signature 2.2.0",
]
[[package]]
name = "kqueue"
version = "1.0.7"
@@ -3586,7 +3684,7 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594"
dependencies = [
"ecdsa",
"ecdsa 0.14.8",
"elliptic-curve 0.12.3",
"sha2",
]
@@ -3597,8 +3695,10 @@ version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b"
dependencies = [
"ecdsa 0.16.9",
"elliptic-curve 0.13.8",
"primeorder",
"sha2",
]
[[package]]
@@ -3607,8 +3707,24 @@ version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70786f51bcc69f6a4c0360e063a4cac5419ef7c5cd5b3c99ad70f3be5ba79209"
dependencies = [
"ecdsa 0.16.9",
"elliptic-curve 0.13.8",
"primeorder",
"sha2",
]
[[package]]
name = "p521"
version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fc9e2161f1f215afdfce23677034ae137bbd45016a880c2eb3ba8eb95f085b2"
dependencies = [
"base16ct 0.2.0",
"ecdsa 0.16.9",
"elliptic-curve 0.13.8",
"primeorder",
"rand_core 0.6.4",
"sha2",
]
[[package]]
@@ -3972,6 +4088,15 @@ dependencies = [
"serde",
]
[[package]]
name = "pem-rfc7468"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
dependencies = [
"base64ct",
]
[[package]]
name = "percent-encoding"
version = "2.2.0"
@@ -4388,6 +4513,7 @@ version = "0.1.0"
dependencies = [
"ahash",
"anyhow",
"arc-swap",
"async-compression",
"async-trait",
"atomic-take",
@@ -4405,6 +4531,8 @@ dependencies = [
"consumption_metrics",
"crossbeam-deque",
"dashmap",
"ecdsa 0.16.9",
"ed25519-dalek",
"env_logger",
"fallible-iterator",
"framed-websockets",
@@ -4427,13 +4555,16 @@ dependencies = [
"itertools 0.10.5",
"jose-jwa",
"jose-jwk",
"jsonwebtoken",
"k256",
"lasso",
"md5",
"measured",
"metrics",
"once_cell",
"opentelemetry",
"p256 0.13.2",
"p384",
"p521",
"parking_lot 0.12.1",
"parquet",
"parquet_derive",
@@ -4454,6 +4585,7 @@ dependencies = [
"reqwest-retry",
"reqwest-tracing",
"routerify",
"rsa",
"rstest",
"rustc-hash",
"rustls 0.22.4",
@@ -4463,6 +4595,7 @@ dependencies = [
"serde",
"serde_json",
"sha2",
"signature 2.2.0",
"smallvec",
"smol_str",
"socket2 0.5.5",
@@ -4965,6 +5098,16 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rfc6979"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2"
dependencies = [
"hmac",
"subtle",
]
[[package]]
name = "ring"
version = "0.16.20"
@@ -5390,6 +5533,7 @@ dependencies = [
"base16ct 0.2.0",
"der 0.7.8",
"generic-array",
"pkcs8 0.10.2",
"subtle",
"zeroize",
]
+14 -4
View File
@@ -11,6 +11,7 @@ testing = []
[dependencies]
ahash.workspace = true
anyhow.workspace = true
arc-swap.workspace = true
async-compression.workspace = true
async-trait.workspace = true
atomic-take.workspace = true
@@ -46,9 +47,6 @@ http-body-util = { version = "0.1" }
indexmap.workspace = true
ipnet.workspace = true
itertools.workspace = true
jose-jwa = "0.1.2"
jose-jwk = "0.1.2"
jsonwebtoken.workspace = true
lasso = { workspace = true, features = ["multi-threaded"] }
md5.workspace = true
measured = { workspace = true, features = ["lasso"] }
@@ -76,7 +74,7 @@ rustls.workspace = true
scopeguard.workspace = true
serde.workspace = true
serde_json.workspace = true
sha2 = { workspace = true, features = ["asm"] }
sha2 = { workspace = true, features = ["asm", "oid"] }
smol_str.workspace = true
smallvec.workspace = true
socket2.workspace = true
@@ -106,6 +104,18 @@ x509-parser.workspace = true
postgres-protocol.workspace = true
redis.workspace = true
# jwt stuff
jose-jwa = "0.1.2"
jose-jwk = { version = "0.1.2", features = ["p256", "p384", "rsa"] }
signature = "2"
ecdsa = "0.16"
p256 = "0.13"
p384 = "0.13"
p521 = "0.13"
k256 = "0.13"
ed25519-dalek = "2"
rsa = "0.9"
workspace_hack.workspace = true
[dev-dependencies]
+1 -1
View File
@@ -1,7 +1,7 @@
mod classic;
mod hacks;
pub mod jwt;
mod link;
mod jwt;
use std::net::IpAddr;
use std::sync::Arc;
+218 -94
View File
@@ -1,17 +1,25 @@
use std::time::Duration;
use std::{sync::Arc, time::Duration};
use anyhow::{bail, Context};
use anyhow::{bail, ensure, Context};
use arc_swap::{access::Access, ArcSwap, Guard};
use dashmap::{mapref::entry::Entry, DashMap};
use jsonwebtoken::{jwk::JwkSet, Algorithm, DecodingKey, TokenData, Validation};
use hmac::digest::generic_array::GenericArray;
// use jose_jwa::S;
use jose_jwk::crypto::KeyInfo;
use tokio::time::Instant;
use crate::intern::EndpointIdInt;
#[derive(Default)]
pub struct JWKCache {
client: reqwest::Client,
// I don't like this. jsonwebtoken crate is horrendous with the amount of string allocations.. oh well.
map: DashMap<EndpointIdInt, JWKCacheEntry>,
map: DashMap<EndpointIdInt, Arc<JWKCacheEntryLock>>,
}
pub struct JWKCacheEntryLock {
cached: ArcSwap<JWKCacheEntry>,
lookup: tokio::sync::Mutex<()>,
}
pub struct JWKCacheEntry {
@@ -19,13 +27,46 @@ pub struct JWKCacheEntry {
/// Should refetch when new key IDs are seen only every 5 minutes or so
last_retrieved: Instant,
/// jwks urls
urls: Vec<url::Url>,
// /// jwks urls
// urls: Vec<url::Url>,
/// cplane will return multiple JWKs urls that we need to scrape.
key_sets: Vec<jose_jwk::JwkSet>,
}
impl JWKCacheEntryLock {
async fn renew_jwks(
&self,
client: &reqwest::Client,
endpoint: EndpointIdInt,
) -> Arc<JWKCacheEntry> {
let _guard = self.lookup.lock().await;
// double check that no one beat us to updating the cache.
let now = Instant::now();
let guard = self.cached.load_full();
let last_update = now.duration_since(guard.last_retrieved);
if last_update < Duration::from_secs(300) {
return guard;
}
todo!("refetch jwks from cplane");
// fetch from cplane
// fetch jwks
// entry.last_retrieved = now;
let x = Arc::new(JWKCacheEntry {
last_retrieved: now,
key_sets: vec![],
});
self.cached.swap(x.clone());
x
}
}
const MIN_RENEW: Duration = Duration::from_secs(300);
const MAX_RENEW: Duration = Duration::from_secs(3600);
impl JWKCache {
pub async fn check_jwt(
&self,
@@ -48,82 +89,197 @@ impl JWKCache {
let header = serde_json::from_slice::<JWTHeader>(&header)
.context("not a valid compact JWT encoding")?;
// let header = jsonwebtoken::decode_header(&jwt).context("could not parse JWT")?;
ensure!(header.typ == "JWT");
let kid = header.kid.context("missing key id")?;
let entry = match self.map.get(&endpoint) {
Some(entry) => entry,
Some(entry) => Arc::clone(&*entry),
None => match self.map.entry(endpoint) {
Entry::Occupied(entry) => entry.into_ref().downgrade(),
Entry::Occupied(entry) => Arc::clone(&*entry.into_ref()),
Entry::Vacant(_) => todo!("fetch jwks from cplane"),
},
};
let now = Instant::now();
if now.duration_since(entry.last_retrieved) > Duration::from_secs(3600) {
todo!("refetch jwks from cplane")
// drop(entry) - don't want to hold the lock while running long network operations
// fetch from cplane
// fetch jwks
// entry.last_retrieved = now;
// check if the cached JWKs need updating.
let mut guard = {
let now = Instant::now();
let guard = entry.cached.load();
let last_update = now.duration_since(guard.last_retrieved);
// can we use arcswap compare_and_swap here?
}
if last_update > MAX_RENEW {
// it's been too long since we checked the keys. wait for them to update.
entry.renew_jwks(&self.client, endpoint).await
} else if last_update > MIN_RENEW {
// every 5 minutes we should spawn a job to eagerly update the token.
let jwk = entry.key_sets.iter().flat_map(|jwks| &jwks.keys).find(|jwk| {
jwk.prm.kid.as_deref() == Some(kid) && jwk.prm.alg == Some(header.alg)
});
let entry = entry.clone();
let client = self.client.clone();
tokio::spawn(async move {
entry.renew_jwks(&client, endpoint).await;
});
let key = match jwk {
Some(jwk) if jwk.is_supported() => DecodingKey::from_jwk(jwk).context("invalid key")?,
None if now.duration_since(entry.last_retrieved) > Duration::from_secs(300) => {
// refetch jwks
todo!("fetch jwks again");
}
_ => {
bail!("jwk not found");
Guard::into_inner(guard)
} else {
Guard::into_inner(guard)
}
};
let mut validation = Validation::new(Algorithm::ES256);
// support all public key algorithms
validation.algorithms = vec![
Algorithm::ES256,
Algorithm::ES384,
Algorithm::RS256,
Algorithm::RS384,
Algorithm::RS512,
Algorithm::PS256,
Algorithm::PS384,
Algorithm::PS512,
Algorithm::EdDSA,
];
// get the key from the JWKs if possible. If not, wait for the keys to update.
let jwk = loop {
let jwk = guard
.key_sets
.iter()
.flat_map(|jwks| &jwks.keys)
.find(|jwk| {
jwk.prm.kid.as_deref() == Some(kid) && jwk.key.is_supported(&header.alg)
});
// should we require some set of common claims?
// eg `exp`?
validation.required_spec_claims.clear();
validation.leeway = 60;
match jwk {
Some(jwk) => break jwk,
None if guard.last_retrieved.elapsed() > MIN_RENEW => {
guard = entry.renew_jwks(&self.client, endpoint).await
}
_ => {
bail!("jwk not found");
}
}
};
validation.validate_exp = true;
validation.validate_nbf = true;
// we don't have an audience - I want cplane to add this field
validation.validate_aud = false;
let sig = base64::decode_config(signature, base64::URL_SAFE_NO_PAD)
.context("not a valid compact JWT encoding")?;
match &jwk.key {
jose_jwk::Key::Ec(key) => {
verify_ec_signature(header_payload.as_bytes(), &sig, key)?;
}
jose_jwk::Key::Rsa(key) => {
verify_rsa_signature(header_payload.as_bytes(), &sig, key, &jwk.prm.alg)?;
}
jose_jwk::Key::Okp(key) => {
verify_okp_signature(header_payload.as_bytes(), &sig, key)?;
}
key => bail!("unsupported key type {key:?}"),
};
// subject can be anything - we support all users
validation.sub = None;
// don't know the audience - I want cplane to add this field
validation.aud = None;
// don't know the issuer - I want cplane to add this field
validation.iss = None;
// ew...
let token: TokenData<serde_json::Value> =
jsonwebtoken::decode(&jwt, &key, &validation).context("invalid token")?;
// TODO: verify iss, exp, nbf, etc...
Ok(())
}
}
fn verify_ec_signature(data: &[u8], sig: &[u8], key: &jose_jwk::Ec) -> anyhow::Result<()> {
use ecdsa::Signature;
use signature::Verifier;
match key.crv {
jose_jwk::EcCurves::P256 => {
let pk =
p256::PublicKey::try_from(key).map_err(|_| anyhow::anyhow!("invalid P256 key"))?;
let key = p256::ecdsa::VerifyingKey::from(&pk);
let sig = Signature::from_slice(sig)?;
key.verify(data, &sig)?;
}
jose_jwk::EcCurves::P384 => {
let pk =
p384::PublicKey::try_from(key).map_err(|_| anyhow::anyhow!("invalid P384 key"))?;
let key = p384::ecdsa::VerifyingKey::from(&pk);
let sig = Signature::from_slice(sig)?;
key.verify(data, &sig)?;
}
jose_jwk::EcCurves::P521 => {
ensure!(key.x.len() == 66 && key.y.len() == 66);
let x = GenericArray::from_slice(&key.x);
let y = GenericArray::from_slice(&key.y);
let encoded = p521::EncodedPoint::from_affine_coordinates(x, y, false);
let key = p521::ecdsa::VerifyingKey::from_encoded_point(&encoded)?;
let sig = Signature::from_slice(sig)?;
key.verify(data, &sig)?;
}
jose_jwk::EcCurves::P256K => {
ensure!(key.x.len() == 32 && key.y.len() == 32);
let x = GenericArray::from_slice(&key.x);
let y = GenericArray::from_slice(&key.y);
let encoded = k256::EncodedPoint::from_affine_coordinates(x, y, false);
let key = k256::ecdsa::VerifyingKey::from_encoded_point(&encoded)?;
let sig = Signature::from_slice(sig)?;
key.verify(data, &sig)?;
}
key => bail!("unsupported ec key type {key:?}"),
}
Ok(())
}
fn verify_okp_signature(data: &[u8], sig: &[u8], key: &jose_jwk::Okp) -> anyhow::Result<()> {
use ed25519_dalek::Signature;
use signature::Verifier;
match key.crv {
jose_jwk::OkpCurves::Ed25519 => {
let x = <&[u8; 32]>::try_from(&**key.x)?;
let key = ed25519_dalek::VerifyingKey::from_bytes(x)?;
let sig = Signature::from_slice(sig)?;
key.verify(data, &sig)?;
}
// jose_jwk::OkpCurves::Ed448 => todo!(),
// jose_jwk::OkpCurves::X25519 => todo!(),
// jose_jwk::OkpCurves::X448 => todo!(),
key => bail!("unsupported octet key pair curve type {key:?}"),
}
Ok(())
}
fn verify_rsa_signature(
data: &[u8],
sig: &[u8],
key: &jose_jwk::Rsa,
alg: &Option<jose_jwa::Algorithm>,
) -> anyhow::Result<()> {
use jose_jwa::{Algorithm, Signing};
use rsa::{Pkcs1v15Sign, Pss, RsaPublicKey};
use sha2::Digest;
let key = RsaPublicKey::try_from(key).map_err(|_| anyhow::anyhow!("invalid RSA key"))?;
match alg {
Some(Algorithm::Signing(Signing::Ps256)) => {
let hashed = sha2::Sha256::digest(data);
let scheme = Pss::new::<sha2::Sha256>();
key.verify(scheme, &hashed, sig)?;
}
Some(Algorithm::Signing(Signing::Ps384)) => {
let hashed = sha2::Sha384::digest(data);
let scheme = Pss::new::<sha2::Sha384>();
key.verify(scheme, &hashed, sig)?;
}
Some(Algorithm::Signing(Signing::Ps512)) => {
let hashed = sha2::Sha512::digest(data);
let scheme = Pss::new::<sha2::Sha512>();
key.verify(scheme, &hashed, sig)?;
}
Some(Algorithm::Signing(Signing::Rs256)) => {
let hashed = sha2::Sha256::digest(data);
let scheme = Pkcs1v15Sign::new::<sha2::Sha256>();
key.verify(scheme, &hashed, sig)?;
}
Some(Algorithm::Signing(Signing::Rs384)) => {
let hashed = sha2::Sha384::digest(data);
let scheme = Pkcs1v15Sign::new::<sha2::Sha384>();
key.verify(scheme, &hashed, sig)?;
}
Some(Algorithm::Signing(Signing::Rs512)) => {
let hashed = sha2::Sha512::digest(data);
let scheme = Pkcs1v15Sign::new::<sha2::Sha512>();
key.verify(scheme, &hashed, sig)?;
}
_ => bail!("invalid RSA signing algorithm"),
};
Ok(())
}
/// <https://datatracker.ietf.org/doc/html/rfc7515#section-4.1>
#[derive(serde::Deserialize)]
struct JWTHeader<'a> {
@@ -134,35 +290,3 @@ struct JWTHeader<'a> {
/// key id, must be provided for our usecase
kid: Option<&'a str>,
}
enum JwsAlg {
// we do not support symmetric key algorithms such as HMAC.
// RSASSA PKCS1-v1_5: <https://www.rfc-editor.org/rfc/rfc7518#section-3.3>
/// RSASSA-PKCS1-v1_5 using SHA-256
RS256,
/// RSASSA-PKCS1-v1_5 using SHA-384
RS384,
/// RSASSA-PKCS1-v1_5 using SHA-512
RS512,
// ECDSA: <https://www.rfc-editor.org/rfc/rfc7518#section-3.4>
/// ECDSA using P-256 and SHA-256
ES256,
/// ECDSA using P-384 and SHA-384
ES384,
/// ECDSA using P-521 and SHA-512
ES512,
// RSASSA-PSS: <https://www.rfc-editor.org/rfc/rfc7518#section-3.5>
/// RSASSA-PSS using SHA-256 and MGF1 with SHA-256
PS256,
/// RSASSA-PSS using SHA-384 and MGF1 with SHA-384
PS384,
/// RSASSA-PSS using SHA-512 and MGF1 with SHA-512
PS512,
// EdDSA: <https://datatracker.ietf.org/doc/html/rfc8037#section-3.1>
/// Edwards-curve Digital Signature Algorithm (EdDSA)
EdDSA,
}