use serde::{Deserialize, Serialize};
use crate::UseResult;
use super::{
canonical_json, capability_error, parse_contract, CapabilityDescriptor, CAPABILITY_ERROR,
};
use crate::plugin::canonical_digest;
pub const CAPABILITY_DESCRIPTION_SIGNATURE_SCHEMA_V1: &str =
"a3s.use.capability-description-signature.v1";
pub const CAPABILITY_DESCRIPTION_SIGNATURE_ALGORITHM_ED25519: &str = "ed25519";
const CAPABILITY_SIGNATURE_DOMAIN: &[u8] = b"a3s.use.capability-description-signature.v1\0";
const MAX_CAPABILITY_SIGNATURE_ID_BYTES: usize = 256;
const MAX_CAPABILITY_SIGNATURE_HEX_BYTES: usize = 128;
const MAX_CAPABILITY_SIGNATURE_LIFETIME_SECONDS: u64 = 31_536_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum CapabilityDescriptionSignatureAlgorithm {
Ed25519,
}
impl CapabilityDescriptionSignatureAlgorithm {
pub const fn as_str(self) -> &'static str {
match self {
Self::Ed25519 => CAPABILITY_DESCRIPTION_SIGNATURE_ALGORITHM_ED25519,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct CapabilityDescriptionSignaturePayload {
pub schema: String,
pub descriptor: CapabilityDescriptor,
pub descriptor_digest: String,
pub signer_id: String,
pub key_id: String,
pub algorithm: CapabilityDescriptionSignatureAlgorithm,
pub issued_at_unix_seconds: u64,
pub expires_at_unix_seconds: u64,
}
impl CapabilityDescriptionSignaturePayload {
#[allow(clippy::too_many_arguments)]
pub fn new(
descriptor: CapabilityDescriptor,
signer_id: impl Into<String>,
key_id: impl Into<String>,
algorithm: CapabilityDescriptionSignatureAlgorithm,
issued_at_unix_seconds: u64,
expires_at_unix_seconds: u64,
) -> UseResult<Self> {
let payload = Self {
schema: CAPABILITY_DESCRIPTION_SIGNATURE_SCHEMA_V1.to_owned(),
descriptor_digest: descriptor.descriptor_digest()?,
descriptor,
signer_id: signer_id.into(),
key_id: key_id.into(),
algorithm,
issued_at_unix_seconds,
expires_at_unix_seconds,
};
payload.validate()?;
Ok(payload)
}
pub fn canonical_bytes(&self) -> UseResult<Vec<u8>> {
self.validate()?;
let mut bytes = CAPABILITY_SIGNATURE_DOMAIN.to_vec();
bytes.extend(canonical_json(
self,
"capability description signature payload",
CAPABILITY_ERROR,
)?);
Ok(bytes)
}
pub fn validate(&self) -> UseResult<()> {
if self.schema != CAPABILITY_DESCRIPTION_SIGNATURE_SCHEMA_V1
|| !valid_identity(&self.signer_id)
|| !valid_identity(&self.key_id)
|| self.algorithm != CapabilityDescriptionSignatureAlgorithm::Ed25519
|| self.issued_at_unix_seconds == 0
|| self.expires_at_unix_seconds <= self.issued_at_unix_seconds
|| self
.expires_at_unix_seconds
.saturating_sub(self.issued_at_unix_seconds)
> MAX_CAPABILITY_SIGNATURE_LIFETIME_SECONDS
{
return Err(capability_error(
"The capability description signature identity or validity window is invalid.",
));
}
self.descriptor.validate()?;
if self.descriptor.descriptor_digest()? != self.descriptor_digest {
return Err(capability_error(
"The capability description signature does not match its descriptor.",
));
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct SignedCapabilityDescription {
#[serde(flatten)]
pub payload: CapabilityDescriptionSignaturePayload,
pub signature: String,
}
impl SignedCapabilityDescription {
pub fn from_parts(
payload: CapabilityDescriptionSignaturePayload,
signature: impl Into<String>,
) -> UseResult<Self> {
let envelope = Self {
payload,
signature: signature.into(),
};
envelope.validate()?;
Ok(envelope)
}
pub fn from_json(input: &[u8]) -> UseResult<Self> {
parse_contract(
input,
"signed capability description",
CAPABILITY_ERROR,
Self::validate,
)
}
pub fn validate(&self) -> UseResult<()> {
self.payload.validate()?;
if !valid_signature_hex(&self.signature) {
return Err(capability_error(
"The capability description signature encoding is invalid.",
));
}
Ok(())
}
pub fn signing_bytes(&self) -> UseResult<Vec<u8>> {
self.payload.canonical_bytes()
}
pub fn canonical_bytes(&self) -> UseResult<Vec<u8>> {
self.validate()?;
canonical_json(self, "signed capability description", CAPABILITY_ERROR)
}
pub fn signature_digest(&self) -> UseResult<String> {
self.validate()?;
let signature = decode_hex(&self.signature)?;
Ok(canonical_digest(&signature))
}
pub fn descriptor(&self) -> &CapabilityDescriptor {
&self.payload.descriptor
}
pub fn signer_id(&self) -> &str {
&self.payload.signer_id
}
pub fn key_id(&self) -> &str {
&self.payload.key_id
}
pub fn algorithm(&self) -> CapabilityDescriptionSignatureAlgorithm {
self.payload.algorithm
}
pub fn issued_at_unix_seconds(&self) -> u64 {
self.payload.issued_at_unix_seconds
}
pub fn expires_at_unix_seconds(&self) -> u64 {
self.payload.expires_at_unix_seconds
}
pub fn signature_bytes(&self) -> UseResult<Vec<u8>> {
self.validate()?;
decode_hex(&self.signature)
}
}
fn valid_identity(value: &str) -> bool {
!value.is_empty()
&& value.len() <= MAX_CAPABILITY_SIGNATURE_ID_BYTES
&& value
.as_bytes()
.first()
.is_some_and(u8::is_ascii_alphanumeric)
&& value.bytes().all(|byte| {
byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b':' | b'/' | b'@')
})
&& !value
.split('/')
.any(|segment| segment.is_empty() || matches!(segment, "." | ".."))
}
fn valid_signature_hex(value: &str) -> bool {
value.len() == MAX_CAPABILITY_SIGNATURE_HEX_BYTES
&& value
.bytes()
.all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f'))
}
fn decode_hex(value: &str) -> UseResult<Vec<u8>> {
if !valid_signature_hex(value) {
return Err(capability_error(
"The capability description signature encoding is invalid.",
));
}
value
.as_bytes()
.chunks_exact(2)
.map(|pair| {
let high = hex_nibble(pair[0]).ok_or_else(|| {
capability_error("The capability description signature encoding is invalid.")
})?;
let low = hex_nibble(pair[1]).ok_or_else(|| {
capability_error("The capability description signature encoding is invalid.")
})?;
Ok((high << 4) | low)
})
.collect()
}
fn hex_nibble(byte: u8) -> Option<u8> {
match byte {
b'0'..=b'9' => Some(byte - b'0'),
b'a'..=b'f' => Some(byte - b'a' + 10),
_ => None,
}
}