use hmac::{Hmac, Mac};
use sha2::Sha256;
use subtle::ConstantTimeEq;
use crate::refinement::TrustProof;
type HmacSha256 = Hmac<Sha256>;
#[derive(Debug)]
pub enum TrustError {
MalformedSignature(&'static str),
SignatureMismatch,
InvalidKey(String),
ExchangeFailed(String),
UnsupportedProof(String),
}
impl std::fmt::Display for TrustError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::MalformedSignature(ctx) => {
write!(f, "malformed signature: {ctx}")
}
Self::SignatureMismatch => {
write!(f, "signature mismatch (constant-time compare)")
}
Self::InvalidKey(m) => write!(f, "invalid key: {m}"),
Self::ExchangeFailed(m) => write!(f, "exchange failed: {m}"),
Self::UnsupportedProof(m) => write!(f, "unsupported proof: {m}"),
}
}
}
impl std::error::Error for TrustError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VerifiedPayload {
pub proof: TrustProof,
pub key_id: String,
}
pub fn verify_hmac_sha256(
payload: &[u8],
tag: &[u8],
key: &[u8],
key_id: &str,
) -> Result<VerifiedPayload, TrustError> {
if tag.len() != 32 {
return Err(TrustError::MalformedSignature(
"HMAC-SHA256 tag must be exactly 32 bytes",
));
}
let mut mac = HmacSha256::new_from_slice(key).map_err(|_| {
TrustError::InvalidKey(
"HMAC-SHA256 accepts any key length; this error is unreachable"
.into(),
)
})?;
mac.update(payload);
mac.verify_slice(tag).map_err(|_| TrustError::SignatureMismatch)?;
Ok(VerifiedPayload {
proof: TrustProof::Hmac,
key_id: key_id.to_string(),
})
}
pub fn verify_hmac_sha256_hex(
payload: &[u8],
tag_hex: &str,
key: &[u8],
key_id: &str,
) -> Result<VerifiedPayload, TrustError> {
let tag = hex_decode(tag_hex.strip_prefix("sha256=").unwrap_or(tag_hex))
.ok_or(TrustError::MalformedSignature(
"HMAC-SHA256 hex tag did not decode",
))?;
verify_hmac_sha256(payload, &tag, key, key_id)
}
fn hex_decode(s: &str) -> Option<Vec<u8>> {
if s.len() % 2 != 0 {
return None;
}
let mut out = Vec::with_capacity(s.len() / 2);
for chunk in s.as_bytes().chunks(2) {
let hi = hex_nibble(chunk[0])?;
let lo = hex_nibble(chunk[1])?;
out.push((hi << 4) | lo);
}
Some(out)
}
fn hex_nibble(c: u8) -> Option<u8> {
match c {
b'0'..=b'9' => Some(c - b'0'),
b'a'..=b'f' => Some(c - b'a' + 10),
b'A'..=b'F' => Some(c - b'A' + 10),
_ => None,
}
}
pub fn verify_ed25519(
payload: &[u8],
signature: &[u8],
public_key: &[u8],
key_id: &str,
) -> Result<VerifiedPayload, TrustError> {
if public_key.len() != 32 {
return Err(TrustError::InvalidKey(
"Ed25519 public key must be 32 bytes".into(),
));
}
if signature.len() != 64 {
return Err(TrustError::MalformedSignature(
"Ed25519 signature must be 64 bytes",
));
}
let pk_array: [u8; 32] = public_key.try_into().map_err(|_| {
TrustError::InvalidKey("public key length invariant violated".into())
})?;
let pk = ed25519_dalek::VerifyingKey::from_bytes(&pk_array)
.map_err(|e| TrustError::InvalidKey(e.to_string()))?;
let sig_array: [u8; 64] = signature.try_into().map_err(|_| {
TrustError::MalformedSignature("signature length invariant violated")
})?;
let sig = ed25519_dalek::Signature::from_bytes(&sig_array);
pk.verify_strict(payload, &sig)
.map_err(|_| TrustError::SignatureMismatch)?;
Ok(VerifiedPayload {
proof: TrustProof::Ed25519,
key_id: key_id.to_string(),
})
}
pub async fn verify_jwt_signature(
token: &str,
verifier: &crate::jwt_verifier::JwtVerifier,
) -> Result<VerifiedPayload, TrustError> {
let verified = verifier.verify(token).await.map_err(|e| match e {
crate::jwt_verifier::JwtVerifyError::UnsupportedAlg(a) => {
TrustError::UnsupportedProof(format!("alg={a}"))
}
other => TrustError::ExchangeFailed(other.to_string()),
})?;
let key_id = verified
.jti
.clone()
.or_else(|| verified.sub.clone())
.unwrap_or_else(|| "<anonymous>".to_string());
Ok(VerifiedPayload {
proof: TrustProof::JwtSig,
key_id,
})
}
pub struct OAuthCodeExchangeRequest<'a> {
pub token_endpoint: &'a str,
pub client_id: &'a str,
pub client_secret: Option<&'a str>,
pub redirect_uri: &'a str,
pub code: &'a str,
pub code_verifier: &'a str,
}
#[derive(Debug, Clone, serde::Deserialize)]
pub struct OAuthTokenResponse {
pub access_token: String,
#[serde(default)]
pub token_type: String,
#[serde(default)]
pub expires_in: Option<u64>,
#[serde(default)]
pub refresh_token: Option<String>,
#[serde(default)]
pub scope: Option<String>,
#[serde(default)]
pub id_token: Option<String>,
}
pub async fn verify_oauth_code_exchange(
req: OAuthCodeExchangeRequest<'_>,
) -> Result<(VerifiedPayload, OAuthTokenResponse), TrustError> {
let mut form = vec![
("grant_type", "authorization_code"),
("code", req.code),
("redirect_uri", req.redirect_uri),
("client_id", req.client_id),
("code_verifier", req.code_verifier),
];
if let Some(secret) = req.client_secret {
form.push(("client_secret", secret));
}
let client = reqwest::Client::new();
let resp = client
.post(req.token_endpoint)
.form(&form)
.send()
.await
.map_err(|e| TrustError::ExchangeFailed(e.to_string()))?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
return Err(TrustError::ExchangeFailed(format!(
"HTTP {status}: {body}"
)));
}
let token: OAuthTokenResponse = resp
.json()
.await
.map_err(|e| TrustError::ExchangeFailed(format!("body parse: {e}")))?;
Ok((
VerifiedPayload {
proof: TrustProof::OAuthCodeExchange,
key_id: req.client_id.to_string(),
},
token,
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hmac_roundtrip() {
let key = b"super-secret-key";
let payload = b"order#42|amount=100.00";
let mut mac = HmacSha256::new_from_slice(key).unwrap();
mac.update(payload);
let tag = mac.finalize().into_bytes();
let vp =
verify_hmac_sha256(payload, &tag, key, "key-v1").unwrap();
assert_eq!(vp.proof, TrustProof::Hmac);
assert_eq!(vp.key_id, "key-v1");
}
#[test]
fn hmac_rejects_tampered_payload() {
let key = b"super-secret-key";
let mut mac = HmacSha256::new_from_slice(key).unwrap();
mac.update(b"original");
let tag = mac.finalize().into_bytes();
let err = verify_hmac_sha256(b"tampered", &tag, key, "k").unwrap_err();
matches!(err, TrustError::SignatureMismatch);
}
#[test]
fn hmac_rejects_wrong_length_tag() {
let err = verify_hmac_sha256(b"x", &[0u8; 16], b"k", "k").unwrap_err();
matches!(err, TrustError::MalformedSignature(_));
}
#[test]
fn hmac_hex_decode_roundtrip() {
let key = b"k";
let mut mac = HmacSha256::new_from_slice(key).unwrap();
mac.update(b"hello");
let tag = mac.finalize().into_bytes();
let hex_tag = tag.iter().map(|b| format!("{b:02x}")).collect::<String>();
let vp = verify_hmac_sha256_hex(b"hello", &hex_tag, key, "k").unwrap();
assert_eq!(vp.proof, TrustProof::Hmac);
let prefixed = format!("sha256={hex_tag}");
let vp2 =
verify_hmac_sha256_hex(b"hello", &prefixed, key, "k").unwrap();
assert_eq!(vp2.proof, TrustProof::Hmac);
}
#[test]
fn ed25519_roundtrip() {
use ed25519_dalek::{Signer, SigningKey};
let seed: [u8; 32] = rand::random();
let sk = SigningKey::from_bytes(&seed);
let pk = sk.verifying_key();
let payload = b"sigstore-attestation";
let sig = sk.sign(payload);
let vp = verify_ed25519(
payload,
&sig.to_bytes(),
pk.as_bytes(),
"sigstore-key-1",
)
.unwrap();
assert_eq!(vp.proof, TrustProof::Ed25519);
assert_eq!(vp.key_id, "sigstore-key-1");
}
#[test]
fn ed25519_rejects_tampered_payload() {
use ed25519_dalek::{Signer, SigningKey};
let seed: [u8; 32] = rand::random();
let sk = SigningKey::from_bytes(&seed);
let pk = sk.verifying_key();
let sig = sk.sign(b"original");
let err = verify_ed25519(
b"tampered",
&sig.to_bytes(),
pk.as_bytes(),
"k",
)
.unwrap_err();
matches!(err, TrustError::SignatureMismatch);
}
#[test]
fn ed25519_rejects_wrong_key_length() {
let err = verify_ed25519(b"x", &[0u8; 64], b"too_short", "k").unwrap_err();
matches!(err, TrustError::InvalidKey(_));
}
#[test]
fn subtle_compare_still_available_for_adopters() {
let a = [1u8, 2, 3];
let b = [1u8, 2, 3];
assert!(bool::from(a.ct_eq(&b)));
}
}