184 lines
5.1 KiB
Rust
184 lines
5.1 KiB
Rust
use geth_types::{Capability, PrincipalId, ResourceId, SecretId, UnixMillis};
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
pub const RESOURCE_SECRET_SIGNATURE_NAMESPACE: &str = "geth.resource-secret.v1@geth.local";
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct ResourceMasterSecret {
|
|
pub id: SecretId,
|
|
pub resource: ResourceId,
|
|
pub epoch: u64,
|
|
pub created_at: UnixMillis,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct ResourceKeyEnvelope {
|
|
pub secret: SecretId,
|
|
pub recipient: PrincipalId,
|
|
pub epoch: u64,
|
|
pub algorithm: String,
|
|
pub ciphertext: Vec<u8>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct BearerAccess {
|
|
pub secret: SecretId,
|
|
pub resource: ResourceId,
|
|
pub capabilities: Vec<Capability>,
|
|
pub expires_at: Option<UnixMillis>,
|
|
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(
|
|
secret: SecretId,
|
|
resource: ResourceId,
|
|
capabilities: Vec<Capability>,
|
|
) -> Self {
|
|
Self {
|
|
secret,
|
|
resource,
|
|
capabilities,
|
|
expires_at: None,
|
|
may_delegate: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum SecretsError {
|
|
#[error("bearer access must grant at least one capability")]
|
|
EmptyBearerCapabilities,
|
|
#[error("capability is not allowed for bearer access: {0}")]
|
|
ForbiddenBearerCapability(String),
|
|
}
|
|
|
|
pub fn validate_bearer_capabilities(capabilities: &[Capability]) -> Result<(), SecretsError> {
|
|
if capabilities.is_empty() {
|
|
return Err(SecretsError::EmptyBearerCapabilities);
|
|
}
|
|
for capability in capabilities {
|
|
let capability = capability.as_str();
|
|
if matches!(
|
|
capability,
|
|
"auth.delegate"
|
|
| "auth.revoke"
|
|
| "trust.modify"
|
|
| "ssh_proxy.admin_shell"
|
|
| "node.enroll"
|
|
) {
|
|
return Err(SecretsError::ForbiddenBearerCapability(
|
|
capability.to_owned(),
|
|
));
|
|
}
|
|
}
|
|
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::*;
|
|
|
|
#[test]
|
|
fn bearer_capabilities_reject_trust_mutation() {
|
|
assert!(validate_bearer_capabilities(&["kv.read".into(), "kv.write".into()]).is_ok());
|
|
assert!(matches!(
|
|
validate_bearer_capabilities(&[]),
|
|
Err(SecretsError::EmptyBearerCapabilities)
|
|
));
|
|
assert!(matches!(
|
|
validate_bearer_capabilities(&["auth.delegate".into()]),
|
|
Err(SecretsError::ForbiddenBearerCapability(_))
|
|
));
|
|
assert!(matches!(
|
|
validate_bearer_capabilities(&["node.enroll".into()]),
|
|
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
|
|
));
|
|
}
|
|
}
|