use std::fmt;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine as _;
use crate::jwt::{Es256Signer, Es256Verifier, Jwk, PublicJwk};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Violation {
pub check: &'static str,
pub detail: String,
}
impl fmt::Display for Violation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}", self.check, self.detail)
}
}
pub const CHECKS: &[&str] = &[
VERIFIER_RFC7515_A3,
VERIFIER_REJECTS_A_FOREIGN_KEY,
VERIFIER_REJECTS_A_TAMPERED_INPUT,
VERIFIER_REJECTS_A_TAMPERED_SIGNATURE,
VERIFIER_REJECTS_DER,
SIGNER_SIGNS,
SIGNER_IS_NOT_DER,
SIGNER_VERIFIES_UNDER_ITS_OWN_JWK,
SIGNER_REJECTED_BY_ANOTHER_KEY,
SIGNER_BINDS_THE_SIGNING_INPUT,
SIGNER_PUBLIC_JWK_IS_STABLE,
SIGNER_PUBLIC_JWK_IS_ES256,
SIGNER_PUBLIC_JWK_HAS_A_KID,
SIGNER_IS_NOT_THE_PUBLISHED_EXAMPLE_KEY,
];
const VERIFIER_RFC7515_A3: &str = "verifier/rfc7515_appendix_a3_vector";
const VERIFIER_REJECTS_A_FOREIGN_KEY: &str = "verifier/rejects_a_foreign_key";
const VERIFIER_REJECTS_A_TAMPERED_INPUT: &str = "verifier/rejects_a_tampered_signing_input";
const VERIFIER_REJECTS_A_TAMPERED_SIGNATURE: &str = "verifier/rejects_a_tampered_signature";
const VERIFIER_REJECTS_DER: &str = "verifier/rejects_the_der_encoding";
const SIGNER_SIGNS: &str = "signer/signs";
const SIGNER_IS_NOT_DER: &str = "signer/output_is_not_der";
const SIGNER_VERIFIES_UNDER_ITS_OWN_JWK: &str = "signer/verifies_under_its_own_public_jwk";
const SIGNER_REJECTED_BY_ANOTHER_KEY: &str = "signer/does_not_verify_under_another_key";
const SIGNER_BINDS_THE_SIGNING_INPUT: &str = "signer/binds_the_signing_input";
const SIGNER_PUBLIC_JWK_IS_STABLE: &str = "signer/public_jwk_is_stable";
const SIGNER_PUBLIC_JWK_IS_ES256: &str = "signer/public_jwk_is_an_es256_p256_key";
const SIGNER_PUBLIC_JWK_HAS_A_KID: &str = "signer/public_jwk_has_a_kid";
const SIGNER_IS_NOT_THE_PUBLISHED_EXAMPLE_KEY: &str = "signer/is_not_the_published_example_key";
const A3_SIGNING_INPUT: &str = concat!(
"eyJhbGciOiJFUzI1NiJ9",
".",
"eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ"
);
const A3_X: &str = "f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU";
const A3_Y: &str = "x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0";
const A3_SIGNATURE: &str =
"DtEhU3ljbEg8L38VWAfUAqOyKAM6-Xx-F4GawxaepmXFCgfTjDxw5djxLa8ISlSApmWQxfKTUJqPP3-Kg6NU1Q";
const OTHER_X: &str = "MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4";
const OTHER_Y: &str = "4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM";
const INPUT_A: &str = "oauth-as.signer-conformance.a";
const INPUT_B: &str = "oauth-as.signer-conformance.b";
pub struct SignerConformance<S, V> {
signer: S,
verifier: V,
}
impl<S: Es256Signer, V: Es256Verifier> SignerConformance<S, V> {
pub fn new(signer: S, verifier: V) -> Self {
SignerConformance { signer, verifier }
}
pub async fn run(&self) -> Vec<Violation> {
let mut out = Vec::new();
self.check_verifier(&mut out);
self.check_signer(&mut out).await;
out
}
fn check_verifier(&self, out: &mut Vec<Violation>) {
let key = jwk(A3_X, A3_Y);
let signature = decode(A3_SIGNATURE);
if !self
.verifier
.verify(&key, A3_SIGNING_INPUT.as_bytes(), &signature)
{
out.push(Violation {
check: VERIFIER_RFC7515_A3,
detail: "the RFC 7515 appendix A.3 ES256 vector did not verify. Either the \
verifier is not ES256 (ECDSA/P-256/SHA-256), or it expects a signature \
encoding other than the 64-byte fixed-width R || S of RFC 7518 s3.4"
.to_string(),
});
}
if self.verifier.verify(
&jwk(OTHER_X, OTHER_Y),
A3_SIGNING_INPUT.as_bytes(),
&signature,
) {
out.push(Violation {
check: VERIFIER_REJECTS_A_FOREIGN_KEY,
detail: "a valid signature verified under a DIFFERENT public key. The verifier is \
not using the key it was given, so every signature verifies under every \
key and no token is bound to any issuer"
.to_string(),
});
}
let mut tampered = A3_SIGNING_INPUT.as_bytes().to_vec();
let last = tampered.len() - 1;
tampered[last] ^= 0x01;
if self.verifier.verify(&key, &tampered, &signature) {
out.push(Violation {
check: VERIFIER_REJECTS_A_TAMPERED_INPUT,
detail: "a signature verified over a signing input that was not the one signed. \
The verifier is not covering the whole message, so a token's claims can \
be edited without invalidating it"
.to_string(),
});
}
let mut bad_signature = signature.clone();
bad_signature[0] ^= 0x01;
if self
.verifier
.verify(&key, A3_SIGNING_INPUT.as_bytes(), &bad_signature)
{
out.push(Violation {
check: VERIFIER_REJECTS_A_TAMPERED_SIGNATURE,
detail: "a corrupted signature verified. The verifier is not checking the \
signature at all, which makes every unsigned token a valid one"
.to_string(),
});
}
if self
.verifier
.verify(&key, A3_SIGNING_INPUT.as_bytes(), &der(&signature))
{
out.push(Violation {
check: VERIFIER_REJECTS_DER,
detail: "the ASN.1 DER encoding of a valid signature verified. RFC 7518 s3.4 \
fixes ES256 as the 64-byte fixed-width R || S and admits no other \
encoding; accepting both gives one signature two forms"
.to_string(),
});
}
}
async fn check_signer(&self, out: &mut Vec<Violation>) {
let published = self.signer.public_jwk();
self.check_published_key(&published, out);
if published.kid.is_empty() {
out.push(Violation {
check: SIGNER_PUBLIC_JWK_HAS_A_KID,
detail: "public_jwk().kid is empty. Every token this server signs carries it \
(RFC 7515 s4.1.4), and key rotation selects on it"
.to_string(),
});
}
if self.signer.public_jwk() != published {
out.push(Violation {
check: SIGNER_PUBLIC_JWK_IS_STABLE,
detail: "public_jwk() returned two different keys on two calls. It must return a \
value cached at construction: JwtConfig reads it once, so a key that \
changes afterwards is advertised nowhere"
.to_string(),
});
}
if published.x == A3_X && published.y == A3_Y {
out.push(Violation {
check: SIGNER_IS_NOT_THE_PUBLISHED_EXAMPLE_KEY,
detail: "the signing key is the RFC 7515 appendix A.3 example key, whose private \
half is printed in the RFC. Anyone can forge every token this server \
issues"
.to_string(),
});
}
let signature = match self.signer.sign(INPUT_A.as_bytes()).await {
Ok(signature) => signature,
Err(e) => {
out.push(Violation {
check: SIGNER_SIGNS,
detail: format!("the signer refused to sign: {e}"),
});
return;
}
};
if signature[0] == 0x30 && signature[2] == 0x02 && (0x40..=0x48).contains(&signature[1]) {
out.push(Violation {
check: SIGNER_IS_NOT_DER,
detail: "the signature looks like an ASN.1 DER SEQUENCE (it begins 0x30 with a \
consistent length byte). RFC 7518 s3.4 requires the 64-byte fixed-width \
R || S concatenation; most KMS APIs and OpenSSL return DER by default \
and it must be converted"
.to_string(),
});
}
let public = published.to_public_jwk();
if !self
.verifier
.verify(&public, INPUT_A.as_bytes(), &signature)
{
out.push(Violation {
check: SIGNER_VERIFIES_UNDER_ITS_OWN_JWK,
detail: "the signature did not verify under the signer's OWN public_jwk(). Either \
public_jwk() is not the public half of the signing key, or the signature \
is not ES256 over the bytes it was handed. Every token this server \
issues would fail verification against its own published JWKS"
.to_string(),
});
}
if self
.verifier
.verify(&jwk(OTHER_X, OTHER_Y), INPUT_A.as_bytes(), &signature)
{
out.push(Violation {
check: SIGNER_REJECTED_BY_ANOTHER_KEY,
detail: "the signature verified under a key that did not produce it. A signer \
that returns a constant, or a verifier that ignores its key, would both \
land here"
.to_string(),
});
}
match self.signer.sign(INPUT_B.as_bytes()).await {
Ok(other) => {
let covers_its_own = self.verifier.verify(&public, INPUT_B.as_bytes(), &other);
let crosses = self.verifier.verify(&public, INPUT_A.as_bytes(), &other)
|| self
.verifier
.verify(&public, INPUT_B.as_bytes(), &signature);
if !covers_its_own || crosses {
out.push(Violation {
check: SIGNER_BINDS_THE_SIGNING_INPUT,
detail: "a signature is not bound to the input it was made over: either a \
second input's signature did not cover that input, or one \
input's signature verified over the other's. The signer is not \
signing the bytes it was given (a fixed message, a double hash, \
or a constant), so the signature says nothing about the token \
that carries it"
.to_string(),
});
}
}
Err(e) => out.push(Violation {
check: SIGNER_SIGNS,
detail: format!("the signer signed once and then refused: {e}"),
}),
}
}
fn check_published_key(&self, published: &Jwk, out: &mut Vec<Violation>) {
let mut wrong = Vec::new();
if published.kty != "EC" {
wrong.push(format!("kty is {:?}, must be \"EC\"", published.kty));
}
if published.crv != "P-256" {
wrong.push(format!("crv is {:?}, must be \"P-256\"", published.crv));
}
if published.alg != "ES256" {
wrong.push(format!("alg is {:?}, must be \"ES256\"", published.alg));
}
if published.use_ != "sig" {
wrong.push(format!("use is {:?}, must be \"sig\"", published.use_));
}
for (name, value) in [("x", &published.x), ("y", &published.y)] {
match URL_SAFE_NO_PAD.decode(value) {
Ok(bytes) if bytes.len() == 32 => {}
Ok(bytes) => wrong.push(format!(
"{name} decodes to {} bytes, must be exactly 32 with leading zeros kept \
(RFC 7518 s6.2.1.2)",
bytes.len()
)),
Err(_) => wrong.push(format!("{name} is not unpadded base64url")),
}
}
if !wrong.is_empty() {
out.push(Violation {
check: SIGNER_PUBLIC_JWK_IS_ES256,
detail: format!(
"public_jwk() is not a usable ES256 JWK: {}",
wrong.join("; ")
),
});
}
}
}
fn jwk(x: &str, y: &str) -> PublicJwk {
PublicJwk::from_coordinates(x, y).expect("the harness's own fixed vectors are well formed")
}
fn decode(b64: &str) -> Vec<u8> {
URL_SAFE_NO_PAD
.decode(b64)
.expect("the harness's own fixed vectors are base64url")
}
fn der(fixed_width: &[u8]) -> Vec<u8> {
fn integer(value: &[u8], out: &mut Vec<u8>) {
let start = value
.iter()
.position(|b| *b != 0)
.unwrap_or(value.len() - 1);
let body = &value[start..];
let pad = usize::from(body[0] & 0x80 != 0);
out.push(0x02);
out.push((body.len() + pad) as u8);
if pad == 1 {
out.push(0x00);
}
out.extend_from_slice(body);
}
let mut body = Vec::with_capacity(72);
integer(&fixed_width[..32], &mut body);
integer(&fixed_width[32..], &mut body);
let mut out = Vec::with_capacity(body.len() + 2);
out.push(0x30);
out.push(body.len() as u8);
out.extend_from_slice(&body);
out
}