use std::time::Duration;
use kovra_core::{
AccessRequest, AgentScope, AuditAction, AuditEvent, AuditSink, Clock, ConfirmOutcome,
ConfirmRequest, Confirmer, Coordinate, Decision, Operation, Origin, Sensitivity, Surface,
decide, fingerprint, public_key_blob, sign_ssh_agent,
};
use zeroize::Zeroizing;
use crate::error::AgentError;
use crate::protocol::{
Identity, Request, encode_failure, encode_identities_answer, encode_sign_response,
};
pub struct KeypairEntry {
pub coordinate: Coordinate,
pub project: Option<String>,
pub environment: String,
pub sensitivity: Sensitivity,
pub public_openssh: String,
pub private_openssh: Zeroizing<String>,
}
impl KeypairEntry {
fn canonical(&self) -> String {
self.coordinate.canonical_path().unwrap_or_else(|_| {
format!(
"{}/{}/{}",
self.environment, self.coordinate.component, self.coordinate.key
)
})
}
fn addressable(&self, scope: &AgentScope) -> bool {
scope.addresses(&self.coordinate, self.project.as_deref())
}
fn comment(&self) -> String {
format!("kovra:{}", self.canonical())
}
}
pub struct Session<'a> {
pub keys: &'a [KeypairEntry],
pub scope: &'a AgentScope,
pub confirmer: &'a dyn Confirmer,
pub audit: &'a dyn AuditSink,
pub clock: &'a dyn Clock,
pub confirm_timeout: Duration,
pub requesting_process: Option<String>,
}
impl Session<'_> {
pub fn handle(&self, request: &Request) -> Result<Vec<u8>, AgentError> {
match request {
Request::RequestIdentities => Ok(self.identities_answer()),
Request::SignRequest {
key_blob,
data,
flags,
} => self.sign(key_blob, data, *flags),
}
}
fn identities_answer(&self) -> Vec<u8> {
let mut identities = Vec::new();
for k in self.keys {
if !k.addressable(self.scope) {
continue; }
if let Ok(blob) = public_key_blob(&k.public_openssh) {
identities.push(Identity {
key_blob: blob,
comment: k.comment(),
});
}
}
encode_identities_answer(&identities)
}
fn sign(&self, key_blob: &[u8], data: &[u8], flags: u32) -> Result<Vec<u8>, AgentError> {
let key = match self.match_key(key_blob) {
Some(k) => k,
None => return Ok(encode_failure()),
};
let canonical = key.canonical();
if !key.addressable(self.scope) {
self.record(
AuditAction::OutOfScopeAttempt,
"unaddressable",
&canonical,
&key.environment,
None,
);
return Ok(encode_failure());
}
let request = AccessRequest {
coordinate: &key.coordinate,
project: key.project.as_deref(),
sensitivity: key.sensitivity,
revealable: false,
operation: Operation::Inject,
surface: Surface::Cli,
origin: Origin::Human,
};
match decide(&request, self.scope) {
Decision::Allow => {
let sig = self.sign_in_memory(key, data, flags)?;
self.record(
AuditAction::Inject,
"sign",
&canonical,
&key.environment,
Some(key),
);
Ok(encode_sign_response(&sig))
}
Decision::RequireConfirmation => {
let mut req = ConfirmRequest::new(
&canonical,
key.sensitivity,
&key.environment,
Origin::Human,
)
.with_command(format!("ssh-agent sign {canonical}"));
if let Some(proc) = &self.requesting_process {
req = req.with_requesting_process(proc.clone());
}
match self.confirmer.confirm(&req, self.confirm_timeout) {
ConfirmOutcome::Approved => {
let sig = self.sign_in_memory(key, data, flags)?;
self.record(
AuditAction::Approve,
"approved",
&canonical,
&key.environment,
Some(key),
);
Ok(encode_sign_response(&sig))
}
ConfirmOutcome::Denied => {
self.record(
AuditAction::Deny,
"denied",
&canonical,
&key.environment,
None,
);
Ok(encode_failure())
}
ConfirmOutcome::TimedOut => {
self.record(
AuditAction::Timeout,
"timeout",
&canonical,
&key.environment,
None,
);
Ok(encode_failure())
}
}
}
Decision::Deny(_) | Decision::Unaddressable => {
self.record(
AuditAction::Deny,
"denied",
&canonical,
&key.environment,
None,
);
Ok(encode_failure())
}
}
}
fn sign_in_memory(
&self,
key: &KeypairEntry,
data: &[u8],
flags: u32,
) -> Result<Vec<u8>, AgentError> {
Ok(sign_ssh_agent(&key.private_openssh, data, flags)?)
}
fn match_key(&self, key_blob: &[u8]) -> Option<&KeypairEntry> {
self.keys.iter().find(|k| {
public_key_blob(&k.public_openssh)
.map(|b| b == key_blob)
.unwrap_or(false)
})
}
fn record(
&self,
action: AuditAction,
result: &str,
canonical: &str,
environment: &str,
key: Option<&KeypairEntry>,
) {
let mut ev = AuditEvent::new(self.clock, action, result)
.at(canonical, environment)
.by(Origin::Human);
if let Some(k) = key {
ev = ev.with_fingerprint(fingerprint(k.public_openssh.as_bytes()));
}
let _ = self.audit.record(&ev);
}
}