Add bearer challenge-response proofs

This commit is contained in:
Eric Wendland 2026-05-19 19:08:08 +02:00
commit 72cd018a3e
11 changed files with 444 additions and 10 deletions

View file

@ -29,6 +29,23 @@ pub struct BearerAccess {
pub may_delegate: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct BearerChallenge {
pub resource: ResourceId,
pub capabilities: Vec<Capability>,
pub nonce: String,
pub issued_at: UnixMillis,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct BearerProof {
pub secret: SecretId,
pub resource: ResourceId,
pub capabilities: Vec<Capability>,
pub nonce: String,
pub response: String,
}
impl BearerAccess {
#[must_use]
pub fn resource_scoped(
@ -76,6 +93,43 @@ pub fn validate_bearer_capabilities(capabilities: &[Capability]) -> Result<(), S
Ok(())
}
#[must_use]
pub fn bearer_response(
secret: &SecretId,
resource: &ResourceId,
capabilities: &[Capability],
nonce: &str,
) -> String {
let key = *blake3::hash(secret.as_str().as_bytes()).as_bytes();
let mut capability_strings = capabilities
.iter()
.map(|capability| capability.as_str())
.collect::<Vec<_>>();
capability_strings.sort_unstable();
let mut message = String::new();
message.push_str(resource.as_str());
message.push('\0');
message.push_str(nonce);
for capability in capability_strings {
message.push('\0');
message.push_str(capability);
}
blake3::keyed_hash(&key, message.as_bytes())
.to_hex()
.to_string()
}
#[must_use]
pub fn verify_bearer_response(
secret: &SecretId,
resource: &ResourceId,
capabilities: &[Capability],
nonce: &str,
response: &str,
) -> bool {
bearer_response(secret, resource, capabilities, nonce) == response
}
#[cfg(test)]
mod tests {
use super::*;
@ -96,4 +150,35 @@ mod tests {
Err(SecretsError::ForbiddenBearerCapability(_))
));
}
#[test]
fn bearer_response_is_resource_scoped_and_capability_scoped() {
let secret = SecretId::new("bearer:test");
let resource = ResourceId::new("resource:kv:prefs");
let capabilities = vec![Capability::new("kv.read"), Capability::new("kv.write")];
let response = bearer_response(&secret, &resource, &capabilities, "nonce");
assert!(verify_bearer_response(
&secret,
&resource,
&capabilities,
"nonce",
&response
));
assert!(!verify_bearer_response(
&secret,
&ResourceId::new("resource:kv:other"),
&capabilities,
"nonce",
&response
));
assert!(!verify_bearer_response(
&secret,
&resource,
&[Capability::new("kv.read")],
"nonce",
&response
));
}
}