Skip to content
Clone
b7cd2b1
96 lines 3.1 KB Rust
use std::sync::OnceLock;
use config::identity::service_auth::ServiceAuthConfig;
use sha2::{Digest, Sha256};
use crate::JwtError;
pub const SERVICE_HEADER: &str = "gitdata-service-id";
pub const KEY_HEADER: &str = "gitdata-service-key";
static CONFIG: OnceLock<ServiceAuthConfig> = OnceLock::new();
pub fn install(config: ServiceAuthConfig) -> Result<(), JwtError> {
config.validate().map_err(JwtError::Config)?;
match CONFIG.set(config) {
Ok(()) => Ok(()),
Err(rejected) => {
let installed = CONFIG
.get()
.ok_or_else(|| JwtError::Config("service identity installation raced".into()))?;
if installed == &rejected {
Ok(())
} else {
Err(JwtError::Config(
"service identity configuration differs".into(),
))
}
}
}
}
pub fn credentials() -> Result<(&'static str, &'static str), JwtError> {
let config = CONFIG
.get()
.ok_or_else(|| JwtError::Config("service_auth is required".into()))?;
if config.service_id.is_empty() || config.key.len() < 32 {
return Err(JwtError::Config(
"service_auth requires service_id and a key of at least 32 bytes".into(),
));
}
Ok((&config.service_id, &config.key))
}
pub fn verify(service: Option<&str>, key: Option<&str>) -> Result<String, JwtError> {
let config = CONFIG.get().ok_or(JwtError::Missing)?;
let service = service.ok_or(JwtError::Missing)?;
let supplied = key.ok_or(JwtError::Missing)?;
let expected = config.peers.get(service).ok_or(JwtError::Revoked)?;
if !matches_digest(expected, supplied) {
return Err(JwtError::Revoked);
}
Ok(service.to_owned())
}
fn matches_digest(expected: &str, supplied: &str) -> bool {
let left = format!("sha256:{:x}", Sha256::digest(supplied.as_bytes()));
crypt::constant_time_eq(left.as_bytes(), expected.to_ascii_lowercase().as_bytes())
}
pub fn attach(metadata: &mut tonic::metadata::MetadataMap) -> Result<(), JwtError> {
let (service, key) = credentials()?;
metadata.insert(
SERVICE_HEADER,
service.parse().map_err(|_| JwtError::Malformed)?,
);
metadata.insert(KEY_HEADER, key.parse().map_err(|_| JwtError::Malformed)?);
Ok(())
}
pub fn from_metadata(metadata: &tonic::metadata::MetadataMap) -> Result<String, JwtError> {
verify(
metadata
.get(SERVICE_HEADER)
.and_then(|value| value.to_str().ok()),
metadata
.get(KEY_HEADER)
.and_then(|value| value.to_str().ok()),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn verification_material_cannot_be_replayed_as_a_credential() {
let secret = "fixture-service-key-with-32-bytes-minimum";
let digest = format!("sha256:{:x}", Sha256::digest(secret.as_bytes()));
assert!(matches_digest(&digest, secret));
assert!(!matches_digest(&digest, &digest));
assert!(!matches_digest(
&digest,
"another-service-key-with-32-bytes-minimum"
));
assert!(!matches_digest("", secret));
}
}