use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub enum SignAlgorithm {
#[serde(rename = "EdDSA", alias = "eddsa")]
EdDSA,
#[serde(rename = "ES256", alias = "es256")]
ES256,
}
pub const OPAQUE_SIGNING_DOMAIN_V1: &[u8] = b"vti.vta.opaque-signing.v1";
#[must_use]
pub fn opaque_signing_input(payload: &[u8]) -> Vec<u8> {
let mut input = Vec::with_capacity(OPAQUE_SIGNING_DOMAIN_V1.len() + 1 + payload.len());
input.extend_from_slice(OPAQUE_SIGNING_DOMAIN_V1);
input.push(0x00);
input.extend_from_slice(payload);
input
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SigningDomain {
ProtocolDefined,
Opaque,
}
impl SigningDomain {
#[must_use]
pub fn signing_input(self, payload: &[u8]) -> std::borrow::Cow<'_, [u8]> {
match self {
Self::ProtocolDefined => std::borrow::Cow::Borrowed(payload),
Self::Opaque => std::borrow::Cow::Owned(opaque_signing_input(payload)),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct SignRequestBody {
#[serde(rename = "keyId", alias = "key_id")]
pub key_id: String,
pub payload: String,
pub algorithm: SignAlgorithm,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct SignResultBody {
#[serde(rename = "keyId", alias = "key_id")]
pub key_id: String,
pub signature: String,
pub algorithm: SignAlgorithm,
}
impl std::fmt::Display for SignAlgorithm {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SignAlgorithm::EdDSA => write!(f, "eddsa"),
SignAlgorithm::ES256 => write!(f, "es256"),
}
}
}
#[cfg(test)]
mod signing_domain_tests {
use super::*;
#[test]
fn an_opaque_payload_is_not_signed_as_presented() {
let payload = b"{\"alg\":\"none\"}";
assert_ne!(
SigningDomain::Opaque.signing_input(payload).as_ref(),
payload,
"an opaque payload is framed before signing, or the signature over it \
verifies as whatever the payload happens to be"
);
}
#[test]
fn protocol_defined_bytes_are_signed_as_presented() {
let payload = b"canonicalised proof input";
assert_eq!(
SigningDomain::ProtocolDefined
.signing_input(payload)
.as_ref(),
payload,
"framing these would produce a proof a conforming verifier rejects"
);
}
#[test]
fn the_two_domains_never_agree_on_the_bytes() {
for payload in [
b"".as_slice(),
b"x".as_slice(),
OPAQUE_SIGNING_DOMAIN_V1, ] {
assert_ne!(
SigningDomain::Opaque.signing_input(payload),
SigningDomain::ProtocolDefined.signing_input(payload),
);
}
}
#[test]
fn the_framing_cannot_be_forged_by_a_chosen_payload() {
assert!(
!OPAQUE_SIGNING_DOMAIN_V1.contains(&0x00),
"a NUL in the tag would make the separator ambiguous"
);
let mut impersonating = OPAQUE_SIGNING_DOMAIN_V1.to_vec();
impersonating.push(0x00);
impersonating.extend_from_slice(b"payload");
assert_ne!(
SigningDomain::Opaque.signing_input(&impersonating),
SigningDomain::Opaque.signing_input(b"payload"),
"framing is applied once by the VTA, not something a caller can pre-apply"
);
}
#[test]
fn a_verifier_can_reconstruct_what_was_signed() {
let payload = b"an assertion";
assert_eq!(
opaque_signing_input(payload),
SigningDomain::Opaque.signing_input(payload).into_owned(),
);
}
}