use crate::{
agents::{decode_base64, ForAgent},
errors::AtomicResult,
urls,
utils::check_timestamp_fresh,
Storelike,
};
#[derive(serde::Deserialize)]
pub struct AuthValues {
#[serde(rename = "https://atomicdata.dev/properties/auth/publicKey")]
pub public_key: String,
#[serde(rename = "https://atomicdata.dev/properties/auth/timestamp")]
pub timestamp: i64,
#[serde(rename = "https://atomicdata.dev/properties/auth/signature")]
pub signature: String,
#[serde(rename = "https://atomicdata.dev/properties/auth/requestedSubject")]
pub requested_subject: String,
#[serde(rename = "https://atomicdata.dev/properties/auth/agent")]
pub agent_subject: String,
}
#[tracing::instrument(skip_all)]
pub fn check_auth_signature(subject: &str, auth_header: &AuthValues) -> AtomicResult<()> {
let agent_pubkey = decode_base64(&auth_header.public_key)?;
let message = format!("{} {}", subject, &auth_header.timestamp);
let pubkey_bytes: [u8; 32] = agent_pubkey
.try_into()
.map_err(|_| "Ed25519 public key must be 32 bytes")?;
let verifying_key = ed25519_dalek::VerifyingKey::from_bytes(&pubkey_bytes)
.map_err(|e| format!("Invalid public key: {}", e))?;
let signature_bytes = decode_base64(&auth_header.signature)?;
let sig_bytes: [u8; 64] = signature_bytes
.try_into()
.map_err(|_| "Ed25519 signature must be 64 bytes")?;
let sig = ed25519_dalek::Signature::from_bytes(&sig_bytes);
use ed25519_dalek::Verifier;
let result = verifying_key.verify(message.as_bytes(), &sig);
if result.is_err() {
if let Ok(url) = url::Url::parse(subject) {
if url.query().is_some() {
let mut url_no_query = url.clone();
url_no_query.set_query(None);
let message_no_query = format!("{} {}", url_no_query, &auth_header.timestamp);
if verifying_key
.verify(message_no_query.as_bytes(), &sig)
.is_ok()
{
return Ok(());
}
}
}
return Err(format!(
"Incorrect signature for auth headers. This could be due to an error during signing or serialization of the commit. Compare this to the serialized message in the client: {}",
message,
)
.into());
}
Ok(())
}
const ACCEPTABLE_TIME_DIFFERENCE: i64 = 10000;
pub const AUTH_MAX_AGE_MS: i64 = 5 * 60 * 1000;
#[tracing::instrument(skip_all)]
pub async fn get_agent_from_auth_values_and_check(
auth_header_values: Option<AuthValues>,
store: &impl Storelike,
) -> AtomicResult<ForAgent> {
if let Some(auth_vals) = auth_header_values {
check_auth_signature(&auth_vals.requested_subject, &auth_vals)
.map_err(|e| format!("Error checking authentication headers. {}", e))?;
check_timestamp_fresh(
auth_vals.timestamp,
ACCEPTABLE_TIME_DIFFERENCE,
AUTH_MAX_AGE_MS,
)
.map_err(|e| format!("Authentication timestamp rejected. {}", e))?;
let agent_subject = crate::Subject::from_raw(auth_vals.agent_subject.trim(), None);
let public_key_trimmed = auth_vals.public_key.trim();
if agent_subject.is_did() {
let did_pubkey = agent_subject
.as_str()
.strip_prefix(crate::subject::DID_AD_AGENT_PREFIX)
.unwrap_or_else(|| agent_subject.as_str());
if public_keys_match(did_pubkey, public_key_trimmed) {
return Ok(ForAgent::AgentSubject(agent_subject));
} else {
return Err(format!(
"The public key in the auth headers '{}' does not match the DID subject '{}'",
public_key_trimmed, auth_vals.agent_subject
)
.into());
}
}
if let Some(path_key) = crate::agents::legacy_agent_pubkey(agent_subject.as_str()) {
if public_keys_match(&path_key, public_key_trimmed) {
return Ok(ForAgent::AgentSubject(agent_subject));
}
return Err(format!(
"The public key in the auth headers '{}' does not match the agent subject '{}'",
public_key_trimmed, auth_vals.agent_subject
)
.into());
}
let normalized_agent = store.normalize_subject(&agent_subject);
if !normalized_agent.is_local() {
return Err(format!(
"Agent subject '{}' is hosted elsewhere and cannot be used to authenticate here; sign in with a did:ad:agent identity",
auth_vals.agent_subject
)
.into());
}
let agent_resource = store.get_resource(&normalized_agent).await?;
let found_public_key = agent_resource.get(urls::PUBLIC_KEY)?;
if !public_keys_match(found_public_key.to_string().trim(), public_key_trimmed) {
Err(
"The public key in the auth headers does not match the public key in the agent"
.to_string()
.into(),
)
} else {
Ok(ForAgent::AgentSubject(agent_subject))
}
} else {
Ok(ForAgent::Public)
}
}
pub fn public_keys_match(a: &str, b: &str) -> bool {
if a == b {
return true;
}
matches!(
(
crate::agents::decode_base64(a),
crate::agents::decode_base64(b),
),
(Ok(da), Ok(db)) if da == db
)
}
#[cfg(test)]
mod test {
use super::public_keys_match;
#[test]
fn public_keys_match_across_base64_alphabets() {
let standard = "gJRZVTGPngaG3mSPA/e6LEewKixYpZtuUYQhNg+t7Y4=";
let url_safe = "gJRZVTGPngaG3mSPA_e6LEewKixYpZtuUYQhNg-t7Y4";
assert!(
public_keys_match(standard, url_safe),
"standard and url-safe encodings of the same key should match"
);
assert!(public_keys_match(standard, standard));
assert!(public_keys_match(url_safe, url_safe));
}
#[test]
fn public_keys_match_rejects_different_keys() {
let a = "gJRZVTGPngaG3mSPA_e6LEewKixYpZtuUYQhNg-t7Y4";
let b = "AAAAVTGPngaG3mSPA_e6LEewKixYpZtuUYQhNg-t7Y4";
assert!(!public_keys_match(a, b));
}
}