Add bearer challenge-response proofs
This commit is contained in:
parent
43433dbf1d
commit
72cd018a3e
11 changed files with 444 additions and 10 deletions
|
|
@ -177,8 +177,10 @@ Roadmap items should be actionable and checkable:
|
||||||
SSH bytes or connect to sshd/admin shell yet.
|
SSH bytes or connect to sshd/admin shell yet.
|
||||||
- Resource secret epoch metadata can be created, rotated, and listed locally.
|
- Resource secret epoch metadata can be created, rotated, and listed locally.
|
||||||
Bearer access metadata can be created/listed/revoked as resource-scoped auth
|
Bearer access metadata can be created/listed/revoked as resource-scoped auth
|
||||||
ops and must not allow trust graph mutation capabilities. Payload encryption,
|
ops and must not allow trust graph mutation capabilities. Bearer
|
||||||
key envelopes, and bearer challenge-response are still roadmap work.
|
challenge/proof/verify commands exist for resource-scoped possession checks.
|
||||||
|
Payload encryption, key envelopes, and wiring bearer proofs into remote module
|
||||||
|
authorization are still roadmap work.
|
||||||
- SSH revocations can be exported as JSONL, OpenSSH KRL specification text, or
|
- SSH revocations can be exported as JSONL, OpenSSH KRL specification text, or
|
||||||
binary OpenSSH KRL files generated through `ssh-keygen`. JSONL and OpenSSH KRL
|
binary OpenSSH KRL files generated through `ssh-keygen`. JSONL and OpenSSH KRL
|
||||||
specification imports are supported; binary KRL import is unsupported because
|
specification imports are supported; binary KRL import is unsupported because
|
||||||
|
|
|
||||||
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -1294,6 +1294,7 @@ dependencies = [
|
||||||
name = "geth-secrets"
|
name = "geth-secrets"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"blake3",
|
||||||
"geth-types",
|
"geth-types",
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,9 @@ The bootstrap implementation provides:
|
||||||
- `geth secret rotate <resource>`
|
- `geth secret rotate <resource>`
|
||||||
- `geth secret bearer create <resource> --capability <capability>`
|
- `geth secret bearer create <resource> --capability <capability>`
|
||||||
- `geth secret bearer list`
|
- `geth secret bearer list`
|
||||||
|
- `geth secret bearer challenge <resource> --capability <capability>`
|
||||||
|
- `geth secret bearer prove <secret> <resource> --nonce <nonce> --capability <capability>`
|
||||||
|
- `geth secret bearer verify <secret> <resource> --nonce <nonce> --response <response> --capability <capability>`
|
||||||
- `geth secret bearer revoke <resource> <secret>`
|
- `geth secret bearer revoke <resource> <secret>`
|
||||||
- `geth auth explain <subject> <resource> <capability>`
|
- `geth auth explain <subject> <resource> <capability>`
|
||||||
- `geth auth grant <subject> <resource> <capability> [--grant-id <id>]`
|
- `geth auth grant <subject> <resource> <capability> [--grant-id <id>]`
|
||||||
|
|
|
||||||
|
|
@ -209,6 +209,29 @@ pub enum SecretBearerCommand {
|
||||||
expires_at_ms: Option<i64>,
|
expires_at_ms: Option<i64>,
|
||||||
},
|
},
|
||||||
List,
|
List,
|
||||||
|
Challenge {
|
||||||
|
resource: String,
|
||||||
|
#[arg(long = "capability", required = true)]
|
||||||
|
capabilities: Vec<String>,
|
||||||
|
},
|
||||||
|
Prove {
|
||||||
|
secret: String,
|
||||||
|
resource: String,
|
||||||
|
#[arg(long)]
|
||||||
|
nonce: String,
|
||||||
|
#[arg(long = "capability", required = true)]
|
||||||
|
capabilities: Vec<String>,
|
||||||
|
},
|
||||||
|
Verify {
|
||||||
|
secret: String,
|
||||||
|
resource: String,
|
||||||
|
#[arg(long)]
|
||||||
|
nonce: String,
|
||||||
|
#[arg(long)]
|
||||||
|
response: String,
|
||||||
|
#[arg(long = "capability", required = true)]
|
||||||
|
capabilities: Vec<String>,
|
||||||
|
},
|
||||||
Revoke {
|
Revoke {
|
||||||
resource: String,
|
resource: String,
|
||||||
secret: String,
|
secret: String,
|
||||||
|
|
@ -606,6 +629,37 @@ fn request_for_command(command: Command) -> Result<ControlRequest> {
|
||||||
expires_at_ms,
|
expires_at_ms,
|
||||||
},
|
},
|
||||||
SecretBearerCommand::List => ControlRequest::SecretBearerList,
|
SecretBearerCommand::List => ControlRequest::SecretBearerList,
|
||||||
|
SecretBearerCommand::Challenge {
|
||||||
|
resource,
|
||||||
|
capabilities,
|
||||||
|
} => ControlRequest::SecretBearerChallenge {
|
||||||
|
resource,
|
||||||
|
capabilities,
|
||||||
|
},
|
||||||
|
SecretBearerCommand::Prove {
|
||||||
|
secret,
|
||||||
|
resource,
|
||||||
|
capabilities,
|
||||||
|
nonce,
|
||||||
|
} => ControlRequest::SecretBearerProve {
|
||||||
|
secret,
|
||||||
|
resource,
|
||||||
|
capabilities,
|
||||||
|
nonce,
|
||||||
|
},
|
||||||
|
SecretBearerCommand::Verify {
|
||||||
|
secret,
|
||||||
|
resource,
|
||||||
|
capabilities,
|
||||||
|
nonce,
|
||||||
|
response,
|
||||||
|
} => ControlRequest::SecretBearerVerify {
|
||||||
|
secret,
|
||||||
|
resource,
|
||||||
|
capabilities,
|
||||||
|
nonce,
|
||||||
|
response,
|
||||||
|
},
|
||||||
SecretBearerCommand::Revoke { resource, secret } => {
|
SecretBearerCommand::Revoke { resource, secret } => {
|
||||||
ControlRequest::SecretBearerRevoke { resource, secret }
|
ControlRequest::SecretBearerRevoke { resource, secret }
|
||||||
}
|
}
|
||||||
|
|
@ -1171,6 +1225,50 @@ fn print_response(response: ControlResponse, json: bool) -> Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ControlResponse::SecretBearerChallenge { challenge } => {
|
||||||
|
println!("bearer challenge");
|
||||||
|
println!("resource: {}", challenge.resource);
|
||||||
|
println!("nonce: {}", challenge.nonce);
|
||||||
|
println!("issued_at_ms: {}", challenge.issued_at.0);
|
||||||
|
println!(
|
||||||
|
"capabilities: {}",
|
||||||
|
challenge
|
||||||
|
.capabilities
|
||||||
|
.iter()
|
||||||
|
.map(ToString::to_string)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(",")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ControlResponse::SecretBearerProof { proof } => {
|
||||||
|
println!("bearer proof");
|
||||||
|
println!("secret: {}", proof.secret);
|
||||||
|
println!("resource: {}", proof.resource);
|
||||||
|
println!("nonce: {}", proof.nonce);
|
||||||
|
println!("response: {}", proof.response);
|
||||||
|
println!(
|
||||||
|
"capabilities: {}",
|
||||||
|
proof
|
||||||
|
.capabilities
|
||||||
|
.iter()
|
||||||
|
.map(ToString::to_string)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(",")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ControlResponse::SecretBearerVerified {
|
||||||
|
secret,
|
||||||
|
resource,
|
||||||
|
capabilities,
|
||||||
|
verified,
|
||||||
|
reason,
|
||||||
|
} => {
|
||||||
|
println!("bearer verified: {verified}");
|
||||||
|
println!("secret: {secret}");
|
||||||
|
println!("resource: {resource}");
|
||||||
|
println!("capabilities: {}", capabilities.join(","));
|
||||||
|
println!("reason: {reason}");
|
||||||
|
}
|
||||||
ControlResponse::SecretBearerRevoked { resource, secret } => {
|
ControlResponse::SecretBearerRevoked { resource, secret } => {
|
||||||
println!("revoked bearer secret: {secret}");
|
println!("revoked bearer secret: {secret}");
|
||||||
println!("resource: {resource}");
|
println!("resource: {resource}");
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ use geth_kv::{KvEntry, KvResource, KvSyncEntry};
|
||||||
use geth_pipe::{PipeConnection, PipeListener};
|
use geth_pipe::{PipeConnection, PipeListener};
|
||||||
use geth_pubsub::PubsubMessage;
|
use geth_pubsub::PubsubMessage;
|
||||||
use geth_resource::ResourceDescriptor;
|
use geth_resource::ResourceDescriptor;
|
||||||
use geth_secrets::{BearerAccess, ResourceMasterSecret};
|
use geth_secrets::{BearerAccess, BearerChallenge, BearerProof, ResourceMasterSecret};
|
||||||
use geth_ssh_identity::{
|
use geth_ssh_identity::{
|
||||||
SshCertApproval, SshCertRequest, SshCertificateRecord, SshRevocationEntry,
|
SshCertApproval, SshCertRequest, SshCertificateRecord, SshRevocationEntry,
|
||||||
};
|
};
|
||||||
|
|
@ -115,6 +115,23 @@ pub enum ControlRequest {
|
||||||
expires_at_ms: Option<i64>,
|
expires_at_ms: Option<i64>,
|
||||||
},
|
},
|
||||||
SecretBearerList,
|
SecretBearerList,
|
||||||
|
SecretBearerChallenge {
|
||||||
|
resource: String,
|
||||||
|
capabilities: Vec<String>,
|
||||||
|
},
|
||||||
|
SecretBearerProve {
|
||||||
|
secret: String,
|
||||||
|
resource: String,
|
||||||
|
capabilities: Vec<String>,
|
||||||
|
nonce: String,
|
||||||
|
},
|
||||||
|
SecretBearerVerify {
|
||||||
|
secret: String,
|
||||||
|
resource: String,
|
||||||
|
capabilities: Vec<String>,
|
||||||
|
nonce: String,
|
||||||
|
response: String,
|
||||||
|
},
|
||||||
SecretBearerRevoke {
|
SecretBearerRevoke {
|
||||||
resource: String,
|
resource: String,
|
||||||
secret: String,
|
secret: String,
|
||||||
|
|
@ -384,6 +401,19 @@ pub enum ControlResponse {
|
||||||
SecretBearerList {
|
SecretBearerList {
|
||||||
access: Vec<BearerAccess>,
|
access: Vec<BearerAccess>,
|
||||||
},
|
},
|
||||||
|
SecretBearerChallenge {
|
||||||
|
challenge: BearerChallenge,
|
||||||
|
},
|
||||||
|
SecretBearerProof {
|
||||||
|
proof: BearerProof,
|
||||||
|
},
|
||||||
|
SecretBearerVerified {
|
||||||
|
secret: String,
|
||||||
|
resource: String,
|
||||||
|
capabilities: Vec<String>,
|
||||||
|
verified: bool,
|
||||||
|
reason: String,
|
||||||
|
},
|
||||||
SecretBearerRevoked {
|
SecretBearerRevoked {
|
||||||
resource: String,
|
resource: String,
|
||||||
secret: String,
|
secret: String,
|
||||||
|
|
@ -943,6 +973,30 @@ mod tests {
|
||||||
response
|
response
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let request = ControlRequest::SecretBearerVerify {
|
||||||
|
secret: "bearer:test".to_owned(),
|
||||||
|
resource: "resource:cas:local".to_owned(),
|
||||||
|
capabilities: vec!["cas.fetch".to_owned()],
|
||||||
|
nonce: "nonce".to_owned(),
|
||||||
|
response: "response".to_owned(),
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
decode_request(&encode_request(&request).expect("encode")).expect("decode"),
|
||||||
|
request
|
||||||
|
);
|
||||||
|
|
||||||
|
let response = ControlResponse::SecretBearerVerified {
|
||||||
|
secret: "bearer:test".to_owned(),
|
||||||
|
resource: "resource:cas:local".to_owned(),
|
||||||
|
capabilities: vec!["cas.fetch".to_owned()],
|
||||||
|
verified: true,
|
||||||
|
reason: "ok".to_owned(),
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
decode_response(&encode_response(&response).expect("encode")).expect("decode"),
|
||||||
|
response
|
||||||
|
);
|
||||||
|
|
||||||
let request = ControlRequest::CasFetch {
|
let request = ControlRequest::CasFetch {
|
||||||
node: "node:peer".to_owned(),
|
node: "node:peer".to_owned(),
|
||||||
hash: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".into(),
|
hash: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".into(),
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ use geth_kv::{KvEntry, KvResource, KvSyncEntry};
|
||||||
use geth_pipe::{PipeConnection, PipeListener};
|
use geth_pipe::{PipeConnection, PipeListener};
|
||||||
use geth_pubsub::PubsubMessage;
|
use geth_pubsub::PubsubMessage;
|
||||||
use geth_resource::ResourceDescriptor;
|
use geth_resource::ResourceDescriptor;
|
||||||
use geth_secrets::{BearerAccess, ResourceMasterSecret};
|
use geth_secrets::{BearerAccess, BearerChallenge, BearerProof, ResourceMasterSecret};
|
||||||
use geth_ssh_identity::{
|
use geth_ssh_identity::{
|
||||||
SshCertApproval, SshCertKind, SshCertRequest, SshCertRequestStatus, SshCertificateRecord,
|
SshCertApproval, SshCertKind, SshCertRequest, SshCertRequestStatus, SshCertificateRecord,
|
||||||
SshRevocationEntry, SshRevocationExportFormat, SshRevocationKind, build_ssh_cert_sign_command,
|
SshRevocationEntry, SshRevocationExportFormat, SshRevocationKind, build_ssh_cert_sign_command,
|
||||||
|
|
@ -3011,6 +3011,114 @@ pub fn handle_request(
|
||||||
ControlRequest::SecretBearerList => Ok(ControlResponse::SecretBearerList {
|
ControlRequest::SecretBearerList => Ok(ControlResponse::SecretBearerList {
|
||||||
access: load_bearer_access(&store)?,
|
access: load_bearer_access(&store)?,
|
||||||
}),
|
}),
|
||||||
|
ControlRequest::SecretBearerChallenge {
|
||||||
|
resource,
|
||||||
|
capabilities,
|
||||||
|
} => {
|
||||||
|
ensure_resource_exists(&store, &resource)?;
|
||||||
|
let capabilities = capabilities
|
||||||
|
.into_iter()
|
||||||
|
.map(Capability::new)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
geth_secrets::validate_bearer_capabilities(&capabilities)?;
|
||||||
|
let issued_at = UnixMillis(geth_store::now_ms());
|
||||||
|
let nonce = geth_crypto::blake3_hex(
|
||||||
|
format!("{}\0{}\0{}", node.agent_id, resource, issued_at.0).as_bytes(),
|
||||||
|
);
|
||||||
|
Ok(ControlResponse::SecretBearerChallenge {
|
||||||
|
challenge: BearerChallenge {
|
||||||
|
resource: ResourceId::new(resource),
|
||||||
|
capabilities,
|
||||||
|
nonce,
|
||||||
|
issued_at,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
ControlRequest::SecretBearerProve {
|
||||||
|
secret,
|
||||||
|
resource,
|
||||||
|
capabilities,
|
||||||
|
nonce,
|
||||||
|
} => {
|
||||||
|
ensure_resource_exists(&store, &resource)?;
|
||||||
|
let capabilities = capabilities
|
||||||
|
.into_iter()
|
||||||
|
.map(Capability::new)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
geth_secrets::validate_bearer_capabilities(&capabilities)?;
|
||||||
|
let secret = geth_types::SecretId::new(secret);
|
||||||
|
let resource = ResourceId::new(resource);
|
||||||
|
let response = geth_secrets::bearer_response(&secret, &resource, &capabilities, &nonce);
|
||||||
|
Ok(ControlResponse::SecretBearerProof {
|
||||||
|
proof: BearerProof {
|
||||||
|
secret,
|
||||||
|
resource,
|
||||||
|
capabilities,
|
||||||
|
nonce,
|
||||||
|
response,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
ControlRequest::SecretBearerVerify {
|
||||||
|
secret,
|
||||||
|
resource,
|
||||||
|
capabilities,
|
||||||
|
nonce,
|
||||||
|
response,
|
||||||
|
} => {
|
||||||
|
ensure_resource_exists(&store, &resource)?;
|
||||||
|
let requested_capabilities = capabilities
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.map(Capability::new)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
geth_secrets::validate_bearer_capabilities(&requested_capabilities)?;
|
||||||
|
let secret_id = geth_types::SecretId::new(secret.clone());
|
||||||
|
let resource_id = ResourceId::new(resource.clone());
|
||||||
|
let access = load_bearer_access(&store)?.into_iter().find(|access| {
|
||||||
|
access.secret == secret_id
|
||||||
|
&& access.resource == resource_id
|
||||||
|
&& access
|
||||||
|
.expires_at
|
||||||
|
.is_none_or(|expires_at| expires_at.0 >= geth_store::now_ms())
|
||||||
|
});
|
||||||
|
let (verified, reason) = if let Some(access) = access {
|
||||||
|
let has_capabilities = requested_capabilities
|
||||||
|
.iter()
|
||||||
|
.all(|capability| access.capabilities.contains(capability));
|
||||||
|
if !has_capabilities {
|
||||||
|
(
|
||||||
|
false,
|
||||||
|
"bearer secret lacks requested capabilities".to_owned(),
|
||||||
|
)
|
||||||
|
} else if geth_secrets::verify_bearer_response(
|
||||||
|
&secret_id,
|
||||||
|
&resource_id,
|
||||||
|
&requested_capabilities,
|
||||||
|
&nonce,
|
||||||
|
&response,
|
||||||
|
) {
|
||||||
|
(
|
||||||
|
true,
|
||||||
|
"bearer proof verified for resource-scoped capabilities".to_owned(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
false,
|
||||||
|
"bearer proof response did not match challenge".to_owned(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(false, "bearer access not found or expired".to_owned())
|
||||||
|
};
|
||||||
|
Ok(ControlResponse::SecretBearerVerified {
|
||||||
|
secret,
|
||||||
|
resource,
|
||||||
|
capabilities,
|
||||||
|
verified,
|
||||||
|
reason,
|
||||||
|
})
|
||||||
|
}
|
||||||
ControlRequest::SecretBearerRevoke { resource, secret } => {
|
ControlRequest::SecretBearerRevoke { resource, secret } => {
|
||||||
ensure_resource_exists(&store, &resource)?;
|
ensure_resource_exists(&store, &resource)?;
|
||||||
let created_at = UnixMillis(geth_store::now_ms());
|
let created_at = UnixMillis(geth_store::now_ms());
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ rust-version.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
blake3.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
geth-types = { path = "../geth-types" }
|
geth-types = { path = "../geth-types" }
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,23 @@ pub struct BearerAccess {
|
||||||
pub may_delegate: bool,
|
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 {
|
impl BearerAccess {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn resource_scoped(
|
pub fn resource_scoped(
|
||||||
|
|
@ -76,6 +93,43 @@ pub fn validate_bearer_capabilities(capabilities: &[Capability]) -> Result<(), S
|
||||||
Ok(())
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
@ -96,4 +150,35 @@ mod tests {
|
||||||
Err(SecretsError::ForbiddenBearerCapability(_))
|
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
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1224,7 +1224,7 @@ fn bearer_access_create_list_revoke_uses_resource_scoped_auth_ops() {
|
||||||
geth_control::ControlRequest::SecretBearerCreate {
|
geth_control::ControlRequest::SecretBearerCreate {
|
||||||
resource: "resource:cas:local".to_owned(),
|
resource: "resource:cas:local".to_owned(),
|
||||||
capabilities: vec!["cas.fetch".to_owned(), "cas.pin".to_owned()],
|
capabilities: vec!["cas.fetch".to_owned(), "cas.pin".to_owned()],
|
||||||
expires_at_ms: Some(1234),
|
expires_at_ms: Some(4_102_444_800_000),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.expect("create bearer");
|
.expect("create bearer");
|
||||||
|
|
@ -1232,7 +1232,10 @@ fn bearer_access_create_list_revoke_uses_resource_scoped_auth_ops() {
|
||||||
geth_control::ControlResponse::SecretBearerCreated { access } => {
|
geth_control::ControlResponse::SecretBearerCreated { access } => {
|
||||||
assert_eq!(access.resource.to_string(), "resource:cas:local");
|
assert_eq!(access.resource.to_string(), "resource:cas:local");
|
||||||
assert_eq!(access.capabilities.len(), 2);
|
assert_eq!(access.capabilities.len(), 2);
|
||||||
assert_eq!(access.expires_at.map(|expires_at| expires_at.0), Some(1234));
|
assert_eq!(
|
||||||
|
access.expires_at.map(|expires_at| expires_at.0),
|
||||||
|
Some(4_102_444_800_000)
|
||||||
|
);
|
||||||
assert!(!access.may_delegate);
|
assert!(!access.may_delegate);
|
||||||
access.secret.to_string()
|
access.secret.to_string()
|
||||||
}
|
}
|
||||||
|
|
@ -1250,6 +1253,79 @@ fn bearer_access_create_list_revoke_uses_resource_scoped_auth_ops() {
|
||||||
other => panic!("unexpected response: {other:?}"),
|
other => panic!("unexpected response: {other:?}"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let response = geth_node::handle_request(
|
||||||
|
&node,
|
||||||
|
geth_control::ControlRequest::SecretBearerChallenge {
|
||||||
|
resource: "resource:cas:local".to_owned(),
|
||||||
|
capabilities: vec!["cas.fetch".to_owned()],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("create bearer challenge");
|
||||||
|
let nonce = match response {
|
||||||
|
geth_control::ControlResponse::SecretBearerChallenge { challenge } => {
|
||||||
|
assert_eq!(challenge.resource.to_string(), "resource:cas:local");
|
||||||
|
assert_eq!(challenge.capabilities[0].to_string(), "cas.fetch");
|
||||||
|
challenge.nonce
|
||||||
|
}
|
||||||
|
other => panic!("unexpected response: {other:?}"),
|
||||||
|
};
|
||||||
|
let response = geth_node::handle_request(
|
||||||
|
&node,
|
||||||
|
geth_control::ControlRequest::SecretBearerProve {
|
||||||
|
secret: secret.clone(),
|
||||||
|
resource: "resource:cas:local".to_owned(),
|
||||||
|
capabilities: vec!["cas.fetch".to_owned()],
|
||||||
|
nonce: nonce.clone(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("prove bearer challenge");
|
||||||
|
let proof_response = match response {
|
||||||
|
geth_control::ControlResponse::SecretBearerProof { proof } => {
|
||||||
|
assert_eq!(proof.secret.to_string(), secret);
|
||||||
|
assert_eq!(proof.resource.to_string(), "resource:cas:local");
|
||||||
|
assert!(!proof.response.is_empty());
|
||||||
|
proof.response
|
||||||
|
}
|
||||||
|
other => panic!("unexpected response: {other:?}"),
|
||||||
|
};
|
||||||
|
let response = geth_node::handle_request(
|
||||||
|
&node,
|
||||||
|
geth_control::ControlRequest::SecretBearerVerify {
|
||||||
|
secret: secret.clone(),
|
||||||
|
resource: "resource:cas:local".to_owned(),
|
||||||
|
capabilities: vec!["cas.fetch".to_owned()],
|
||||||
|
nonce: nonce.clone(),
|
||||||
|
response: proof_response.clone(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("verify bearer proof");
|
||||||
|
match response {
|
||||||
|
geth_control::ControlResponse::SecretBearerVerified { verified, .. } => {
|
||||||
|
assert!(verified);
|
||||||
|
}
|
||||||
|
other => panic!("unexpected response: {other:?}"),
|
||||||
|
}
|
||||||
|
let response = geth_node::handle_request(
|
||||||
|
&node,
|
||||||
|
geth_control::ControlRequest::SecretBearerVerify {
|
||||||
|
secret: secret.clone(),
|
||||||
|
resource: "resource:cas:local".to_owned(),
|
||||||
|
capabilities: vec!["cas.pin".to_owned()],
|
||||||
|
nonce,
|
||||||
|
response: proof_response,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("verify bearer proof for wrong capability");
|
||||||
|
match response {
|
||||||
|
geth_control::ControlResponse::SecretBearerVerified {
|
||||||
|
verified, reason, ..
|
||||||
|
} => {
|
||||||
|
assert!(!verified);
|
||||||
|
assert!(reason.contains("did not match"));
|
||||||
|
}
|
||||||
|
other => panic!("unexpected response: {other:?}"),
|
||||||
|
}
|
||||||
|
|
||||||
geth_node::handle_request(
|
geth_node::handle_request(
|
||||||
&node,
|
&node,
|
||||||
geth_control::ControlRequest::SecretBearerRevoke {
|
geth_control::ControlRequest::SecretBearerRevoke {
|
||||||
|
|
|
||||||
|
|
@ -268,8 +268,11 @@ key envelopes, bearer secrets, and rotation. Revocation for private data is
|
||||||
modeled initially as secret epoch rotation. The daemon persists resource secret
|
modeled initially as secret epoch rotation. The daemon persists resource secret
|
||||||
epoch metadata through `secret create/rotate/status`. Bearer access is recorded
|
epoch metadata through `secret create/rotate/status`. Bearer access is recorded
|
||||||
as resource-scoped auth operations and rejects trust-mutation capabilities such
|
as resource-scoped auth operations and rejects trust-mutation capabilities such
|
||||||
as `auth.delegate`, `auth.revoke`, and `node.enroll`. The daemon does not yet
|
as `auth.delegate`, `auth.revoke`, and `node.enroll`. Bearer challenge/proof
|
||||||
store payload key material, encrypt resource data, or distribute key envelopes.
|
commands derive deterministic BLAKE3 keyed responses from the bearer secret,
|
||||||
|
resource, nonce, and requested capabilities, then verify them against active
|
||||||
|
resource-scoped bearer grants. The daemon does not yet store payload key
|
||||||
|
material, encrypt resource data, or distribute key envelopes.
|
||||||
|
|
||||||
## Multi-User Direction
|
## Multi-User Direction
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -220,8 +220,11 @@ resource-scoped capability decisions.
|
||||||
- `[x]` Bearer secrets grant only resource-scoped capabilities.
|
- `[x]` Bearer secrets grant only resource-scoped capabilities.
|
||||||
- `[x]` Bearer principals cannot mutate trust graph state by default.
|
- `[x]` Bearer principals cannot mutate trust graph state by default.
|
||||||
- `[x]` Tests verify bearer access does not imply node identity.
|
- `[x]` Tests verify bearer access does not imply node identity.
|
||||||
- `[ ]` Future completion requires bearer challenge-response proof instead
|
- `[x]` `geth secret bearer challenge/prove/verify` exercises
|
||||||
of metadata-only local records.
|
resource-scoped bearer challenge-response proofs.
|
||||||
|
- `[x]` Tests verify valid bearer proofs and capability-scoped proof denial.
|
||||||
|
- `[ ]` Future completion wires bearer proof verification into remote module
|
||||||
|
authorization paths.
|
||||||
|
|
||||||
- `[~]` SSH certificate and revocation lifecycle.
|
- `[~]` SSH certificate and revocation lifecycle.
|
||||||
Acceptance criteria:
|
Acceptance criteria:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue