use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine;
use jsonwebtoken::Algorithm;
use serde_json::Value;
use subtle::ConstantTimeEq;
use crate::config::is_symmetric;
use crate::error::VerifyError;
pub struct RawJws<'a> {
pub header_b64: &'a str,
pub payload_b64: &'a str,
pub signature_b64: &'a str,
}
pub fn split_compact(token: &str) -> Result<RawJws<'_>, VerifyError> {
let mut parts = token.split('.');
match (parts.next(), parts.next(), parts.next(), parts.next()) {
(Some(h), Some(p), Some(s), None) if !h.is_empty() && !s.is_empty() => Ok(RawJws {
header_b64: h,
payload_b64: p,
signature_b64: s,
}),
_ => Err(VerifyError::Malformed(
"expected exactly 3 non-empty dot-separated compact-JWS segments (header, payload, signature)"
.to_string(),
)),
}
}
pub fn decode_json_segment(segment: &str) -> Result<Value, VerifyError> {
let bytes = URL_SAFE_NO_PAD
.decode(segment)
.map_err(|e| VerifyError::Malformed(format!("invalid base64url: {e}")))?;
serde_json::from_slice(&bytes).map_err(|e| VerifyError::Malformed(format!("invalid JSON: {e}")))
}
pub fn parse_and_check_alg(
header: &Value,
allowed_algs: &[Algorithm],
) -> Result<Algorithm, VerifyError> {
let raw_alg = header
.get("alg")
.and_then(Value::as_str)
.ok_or_else(|| VerifyError::Malformed("header missing alg".to_string()))?;
if raw_alg.eq_ignore_ascii_case("none") {
return Err(VerifyError::BadAlg(
"alg \"none\" is never allowed".to_string(),
));
}
let alg: Algorithm = raw_alg
.parse()
.map_err(|_| VerifyError::BadAlg(format!("unsupported alg {raw_alg:?}")))?;
if is_symmetric(alg) {
return Err(VerifyError::BadAlg(format!(
"{alg:?} is a symmetric algorithm; this scheme is asymmetric-only because the \
device's private key never leaves the device"
)));
}
if !allowed_algs.contains(&alg) {
return Err(VerifyError::BadAlg(format!(
"{alg:?} is not in the configured allow-list"
)));
}
Ok(alg)
}
const PRIVATE_JWK_MEMBERS: &[&str] = &["d", "p", "q", "dp", "dq", "qi", "k"];
pub fn reject_private_jwk_members(jwk_json: &Value) -> Result<(), VerifyError> {
if let Some(obj) = jwk_json.as_object() {
for member in PRIVATE_JWK_MEMBERS {
if obj.contains_key(*member) {
return Err(VerifyError::BadJwk(format!(
"embedded jwk contains private component \"{member}\" — never trust a \
caller-supplied private key"
)));
}
}
}
Ok(())
}
pub fn constant_time_eq(a: &str, b: &str) -> bool {
if a.len() != b.len() {
return false;
}
bool::from(a.as_bytes().ct_eq(b.as_bytes()))
}