Separate bearer ids from private tokens

This commit is contained in:
Eric Wendland 2026-05-20 13:10:34 +02:00
commit 460acab67b
9 changed files with 198 additions and 62 deletions

View file

@ -1724,11 +1724,12 @@ fn bearer_proof(
capability: &str,
nonce: &str,
) -> Option<BearerProof> {
bearer_secret.map(|secret| {
let secret = geth_types::SecretId::new(secret);
bearer_secret.map(|token| {
let token = geth_types::SecretId::new(token);
let secret = geth_secrets::bearer_id_for_token(&token);
let resource = ResourceId::new(resource.to_owned());
let capabilities = vec![Capability::new(capability.to_owned())];
let response = geth_secrets::bearer_response(&secret, &resource, &capabilities, nonce);
let response = geth_secrets::bearer_response(&token, &resource, &capabilities, nonce);
BearerProof {
secret,
resource,
@ -1793,16 +1794,6 @@ fn explain_peer_or_bearer(
"bearer proof does not include the requested capability".to_owned(),
));
}
if !geth_secrets::verify_bearer_response(
&proof.secret,
&proof.resource,
&proof.capabilities,
nonce,
&proof.response,
) {
return Ok(denied("bearer proof response is invalid".to_owned()));
}
let now = UnixMillis(geth_store::now_ms());
let Some(access) = load_bearer_access(store)?.into_iter().find(|access| {
access.secret == proof.secret
@ -1813,6 +1804,20 @@ fn explain_peer_or_bearer(
"bearer access is not active for requested resource".to_owned(),
));
};
let Some(token_hash) = access.token_hash.as_deref() else {
return Ok(denied(
"bearer access does not have a token verifier".to_owned(),
));
};
if !geth_secrets::verify_bearer_response_with_token_hash(
token_hash,
&proof.resource,
&proof.capabilities,
nonce,
&proof.response,
) {
return Ok(denied("bearer proof response is invalid".to_owned()));
}
if !access
.capabilities
.iter()
@ -3307,11 +3312,12 @@ pub fn handle_request(
.collect::<Vec<_>>();
geth_secrets::validate_bearer_capabilities(&capabilities)?;
let created_at = UnixMillis(geth_store::now_ms());
let secret = geth_types::SecretId::new(format!(
"bearer:{}",
let token = geth_types::SecretId::new(format!(
"gbt_{}",
geth_crypto::blake3_hex(
format!(
"{resource}\0{}\0{}",
"{}\0{resource}\0{}\0{}",
node.agent_id,
capabilities
.iter()
.map(ToString::to_string)
@ -3322,6 +3328,8 @@ pub fn handle_request(
.as_bytes()
)
));
let secret = geth_secrets::bearer_id_for_token(&token);
let token_hash = geth_secrets::bearer_token_hash(&token);
let access = BearerAccess::resource_scoped(
secret.clone(),
ResourceId::new(resource.clone()),
@ -3333,6 +3341,7 @@ pub fn handle_request(
created_at,
kind: AuthOpKind::BearerAccessCreate {
secret,
token_hash: Some(token_hash),
capabilities,
expires_at: expires_at_ms.map(UnixMillis),
},
@ -3340,6 +3349,7 @@ pub fn handle_request(
store_auth_op(&store, &op)?;
Ok(ControlResponse::SecretBearerCreated {
access: BearerAccess {
token: Some(token),
expires_at: expires_at_ms.map(UnixMillis),
..access
},
@ -3383,9 +3393,10 @@ pub fn handle_request(
.map(Capability::new)
.collect::<Vec<_>>();
geth_secrets::validate_bearer_capabilities(&capabilities)?;
let secret = geth_types::SecretId::new(secret);
let token = geth_types::SecretId::new(secret);
let secret = geth_secrets::bearer_id_for_token(&token);
let resource = ResourceId::new(resource);
let response = geth_secrets::bearer_response(&secret, &resource, &capabilities, &nonce);
let response = geth_secrets::bearer_response(&token, &resource, &capabilities, &nonce);
Ok(ControlResponse::SecretBearerProof {
proof: BearerProof {
secret,
@ -3410,7 +3421,8 @@ pub fn handle_request(
.map(Capability::new)
.collect::<Vec<_>>();
geth_secrets::validate_bearer_capabilities(&requested_capabilities)?;
let secret_id = geth_types::SecretId::new(secret.clone());
let token = geth_types::SecretId::new(secret.clone());
let secret_id = geth_secrets::bearer_id_for_token(&token);
let resource_id = ResourceId::new(resource.clone());
let access = load_bearer_access(&store)?.into_iter().find(|access| {
access.secret == secret_id
@ -3428,13 +3440,15 @@ pub fn handle_request(
false,
"bearer secret lacks requested capabilities".to_owned(),
)
} else if geth_secrets::verify_bearer_response(
&secret_id,
&resource_id,
&requested_capabilities,
&nonce,
&response,
) {
} else if access.token_hash.as_deref().is_some_and(|token_hash| {
geth_secrets::verify_bearer_response_with_token_hash(
token_hash,
&resource_id,
&requested_capabilities,
&nonce,
&response,
)
}) {
(
true,
"bearer proof verified for resource-scoped capabilities".to_owned(),
@ -4458,6 +4472,8 @@ fn load_bearer_access(store: &Store) -> Result<Vec<BearerAccess>, NodeError> {
.into_values()
.map(|record| BearerAccess {
secret: record.secret,
token: None,
token_hash: record.token_hash,
resource: record.resource,
capabilities: record.capabilities,
expires_at: record.expires_at,
@ -5482,7 +5498,10 @@ mod tests {
)
.expect("create remote CAS bearer access")
{
ControlResponse::SecretBearerCreated { access } => access.secret.to_string(),
ControlResponse::SecretBearerCreated { access } => access
.token
.expect("private bearer token on create")
.to_string(),
other => panic!("unexpected bearer create response: {other:?}"),
};
let bearer_fetch = handle_request_async(