use polyc_crypto::routine_grant_assertion::AssertsActor;
use polyc_crypto::{Signer, sensitive::Sensitive};
use polyc_proto::proto::polychrome::agent::v1::AssertedAttribution;
use polyc_proto::proto::polychrome::approval::v1::{ApprovalResponseRequest, AssertedApproval};
use polyc_proto::proto::polychrome::persona::v1::ExternalIdentity;
#[derive(Debug, thiserror::Error)]
pub enum CredentialError {
#[error("edge signing key is not valid hex")]
InvalidHex,
#[error("edge signing key: {0}")]
InvalidSigningKey(#[from] polyc_crypto::SignerError),
}
pub struct EdgeCredentials {
edge_id: String,
bearer: Sensitive<String>,
signer: Signer,
}
impl EdgeCredentials {
pub fn from_parts(
edge_id: String,
bearer: String,
signing_key_hex: &str,
) -> Result<Self, CredentialError> {
let key_bytes =
polyc_crypto::hex::decode(signing_key_hex).ok_or(CredentialError::InvalidHex)?;
let signer = Signer::from_key_bytes(&key_bytes)?;
Ok(Self {
edge_id,
bearer: Sensitive::new(bearer),
signer,
})
}
#[must_use]
pub fn edge_id(&self) -> &str {
&self.edge_id
}
#[must_use]
pub fn bearer(&self) -> &str {
self.bearer.expose()
}
pub fn sign_assertion(&self, a: &mut AssertedAttribution) {
polyc_crypto::edge_identity::sign_edge_assertion_into(&self.signer, a);
}
pub fn attach_approval_assertion(
&self,
request: &mut ApprovalResponseRequest,
responder: ExternalIdentity,
) {
polyc_crypto::approval_assertion::attach_approval_assertion(
&self.signer,
request,
AssertedApproval {
edge_id: self.edge_id.clone(),
responder: buffa::MessageField::some(responder),
signature_hex: String::new(),
__buffa_unknown_fields: buffa::UnknownFields::default(),
},
);
}
#[must_use]
pub fn attach_routine_grant_assertion<M: AssertsActor>(
&self,
request: M,
actor: ExternalIdentity,
) -> M {
let issued_at_unix_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| u64::try_from(d.as_millis()).unwrap_or(u64::MAX));
polyc_crypto::routine_grant_assertion::attach_actor_assertion(
&self.signer,
request,
AssertedApproval {
edge_id: self.edge_id.clone(),
responder: buffa::MessageField::some(actor),
signature_hex: String::new(),
__buffa_unknown_fields: buffa::UnknownFields::default(),
},
uuid::Uuid::new_v4().to_string(),
issued_at_unix_ms,
)
}
}
#[derive(Debug, thiserror::Error)]
pub enum EdgeCredentialsError {
#[error(
"edge credentials are unconfigured (POLYCHROME_EDGE_ID / POLYCHROME_EDGE_BEARER_KEY / \
POLYCHROME_EDGE_SIGNING_KEY_HEX) and the control plane at {agent_addr:?} is not a \
loopback address — refusing to start rather than dial it unauthenticated"
)]
Unconfigured {
agent_addr: String,
},
#[error(transparent)]
Invalid(#[from] CredentialError),
}
pub fn edge_credentials_from_env_or_fail(
agent_addr: &str,
edge_id: Option<&str>,
bearer: Option<&str>,
signing_key_hex: Option<&str>,
) -> Result<Option<EdgeCredentials>, EdgeCredentialsError> {
let edge_id = edge_id.filter(|s| !s.is_empty());
let bearer = bearer.filter(|s| !s.is_empty());
let signing_key_hex = signing_key_hex.filter(|s| !s.is_empty());
if let (Some(edge_id), Some(bearer), Some(signing_key_hex)) = (edge_id, bearer, signing_key_hex)
{
let creds =
EdgeCredentials::from_parts(edge_id.to_owned(), bearer.to_owned(), signing_key_hex)?;
return Ok(Some(creds));
}
if is_loopback_addr(agent_addr) {
tracing::warn!(
"edge credentials are unconfigured (POLYCHROME_EDGE_ID / \
POLYCHROME_EDGE_BEARER_KEY / POLYCHROME_EDGE_SIGNING_KEY_HEX) — dialing the \
control plane unauthenticated; an enforcing control plane will reject these calls"
);
return Ok(None);
}
Err(EdgeCredentialsError::Unconfigured {
agent_addr: agent_addr.to_owned(),
})
}
fn is_loopback_addr(addr: &str) -> bool {
let Ok(uri) = addr.parse::<http::Uri>() else {
return false;
};
let Some(host) = uri.host() else {
return false;
};
host.eq_ignore_ascii_case("localhost")
|| host
.parse::<std::net::IpAddr>()
.is_ok_and(|ip| ip.is_loopback())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn all_three_present_builds_credentials() {
let creds = edge_credentials_from_env_or_fail(
"https://control-plane.example.com",
Some("slack"),
Some("pc_slack_test-secret"),
Some(&polyc_crypto::hex::lower(&[7u8; 32])),
)
.expect("build succeeds")
.expect("credentials present");
assert_eq!(creds.edge_id(), "slack");
assert_eq!(creds.bearer(), "pc_slack_test-secret");
}
#[test]
fn unconfigured_loopback_warns_and_returns_none() {
let creds = edge_credentials_from_env_or_fail("http://127.0.0.1:8080", None, None, None)
.expect("loopback falls back rather than erroring");
assert!(creds.is_none());
}
#[test]
fn unconfigured_localhost_hostname_returns_none() {
let creds = edge_credentials_from_env_or_fail("http://localhost:8080", None, None, None)
.expect("localhost hostname counts as loopback");
assert!(creds.is_none());
}
#[test]
fn unconfigured_non_loopback_fails_fast() {
let err = edge_credentials_from_env_or_fail(
"https://control-plane.example.com",
None,
None,
None,
)
.err()
.expect("a non-loopback unconfigured dial must fail fast");
assert!(matches!(err, EdgeCredentialsError::Unconfigured { .. }));
}
#[test]
fn partially_configured_non_loopback_fails_fast() {
let err = edge_credentials_from_env_or_fail(
"https://control-plane.example.com",
Some("slack"),
None,
None,
)
.err()
.expect("a partial configuration must fail fast against a remote address");
assert!(matches!(err, EdgeCredentialsError::Unconfigured { .. }));
}
#[test]
fn empty_strings_count_as_unconfigured_not_as_configured() {
let err = edge_credentials_from_env_or_fail(
"https://control-plane.example.com",
Some(""),
Some(""),
Some(""),
)
.err()
.expect("empty values are unconfigured, so a remote dial must fail fast");
assert!(matches!(err, EdgeCredentialsError::Unconfigured { .. }));
}
#[test]
fn an_empty_bearer_alone_is_still_unconfigured() {
let err = edge_credentials_from_env_or_fail(
"https://control-plane.example.com",
Some("slack"),
Some(""),
Some(&polyc_crypto::hex::lower(&[7u8; 32])),
)
.err()
.expect("a blank bearer must fail fast, not dial unauthenticated");
assert!(matches!(err, EdgeCredentialsError::Unconfigured { .. }));
}
#[test]
fn invalid_signing_key_surfaces_as_invalid() {
let err = edge_credentials_from_env_or_fail(
"http://127.0.0.1:8080",
Some("slack"),
Some("pc_slack_test-secret"),
Some("not-hex"),
)
.err()
.expect("malformed signing key must not silently fall back");
assert!(matches!(err, EdgeCredentialsError::Invalid(_)));
}
}