use crate::invoice::Signed;
use super::signature::KeyRing;
use super::{Invoice, Signature, SignatureError, SignatureRole};
use ed25519_dalek::{PublicKey, Signature as EdSignature};
use tracing::debug;
use std::borrow::{Borrow, BorrowMut};
use std::fmt::Debug;
use std::str::FromStr;
const GREEDY_VERIFICATION_ROLES: &[SignatureRole] = &[SignatureRole::Creator];
const CREATIVE_INTEGITY_ROLES: &[SignatureRole] = &[SignatureRole::Creator];
const AUTHORITATIVE_INTEGRITY_ROLES: &[SignatureRole] =
&[SignatureRole::Creator, SignatureRole::Approver];
const EXHAUSTIVE_VERIFICATION_ROLES: &[SignatureRole] = &[
SignatureRole::Creator,
SignatureRole::Approver,
SignatureRole::Host,
SignatureRole::Proxy,
];
pub trait Verified: super::sealed::Sealed {}
#[derive(Debug, Clone)]
pub enum VerificationStrategy {
CreativeIntegrity,
AuthoritativeIntegrity,
GreedyVerification,
ExhaustiveVerification,
MultipleAttestation(Vec<SignatureRole>),
MultipleAttestationGreedy(Vec<SignatureRole>),
}
impl Default for VerificationStrategy {
fn default() -> Self {
VerificationStrategy::GreedyVerification
}
}
impl FromStr for VerificationStrategy {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.is_empty() {
return Err("Cannot parse VerificationStrategy from an empty string");
}
let normalized = s.trim().to_lowercase();
let parts: Vec<&str> = normalized.splitn(2, '[').collect();
match parts[0] {
"creativeintegrity" => Ok(Self::CreativeIntegrity),
"authoritativeintegrity" => Ok(Self::AuthoritativeIntegrity),
"greedyverification" => Ok(Self::GreedyVerification),
"exhaustiveverification" => Ok(Self::ExhaustiveVerification),
"multipleattestation" => Ok(Self::MultipleAttestation(parse_roles(parts.get(1))?)),
"multipleattestationgreedy" => {
Ok(Self::MultipleAttestationGreedy(parse_roles(parts.get(1))?))
}
_ => Err("Unknown verification strategy"),
}
}
}
impl<'de> serde::Deserialize<'de> for VerificationStrategy {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_string(StrategyVisitor)
}
}
struct StrategyVisitor;
impl<'de> serde::de::Visitor<'de> for StrategyVisitor {
type Value = VerificationStrategy;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a valid verification strategy value")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
match v.parse::<VerificationStrategy>() {
Ok(s) => Ok(s),
Err(e) => Err(E::custom(e)),
}
}
fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
self.visit_str(&v)
}
}
fn parse_roles(r: Option<&&str>) -> Result<Vec<SignatureRole>, &'static str> {
let raw = r.ok_or("Multiple attestation strategy is missing roles")?;
if !raw.ends_with(']') {
return Err("Missing closing ']' on roles");
}
raw.trim_end_matches(']')
.split(',')
.map(|role| role.parse::<SignatureRole>())
.collect::<Result<Vec<_>, _>>()
}
impl VerificationStrategy {
fn verify_signature(&self, sig: &Signature, cleartext: &[u8]) -> Result<(), SignatureError> {
let pk = base64::decode(sig.key.as_bytes())
.map_err(|_| SignatureError::CorruptKey(sig.key.clone()))?;
let sig_block = base64::decode(sig.signature.as_bytes())
.map_err(|_| SignatureError::CorruptSignature(sig.key.clone()))?;
let pubkey =
PublicKey::from_bytes(&pk).map_err(|_| SignatureError::CorruptKey(sig.key.clone()))?;
let ed_sig = EdSignature::try_from(sig_block.as_slice())
.map_err(|_| SignatureError::CorruptSignature(sig.key.clone()))?;
pubkey
.verify_strict(cleartext, &ed_sig)
.map_err(|_| SignatureError::Unverified(sig.key.clone()))
}
pub fn verify<I>(
&self,
invoice: I,
keyring: &KeyRing,
) -> Result<VerifiedInvoice<I>, SignatureError>
where
I: Borrow<Invoice> + Into<Invoice>,
{
let inv = invoice.borrow();
let (roles, all_valid, all_verified, all_roles) = match self {
VerificationStrategy::GreedyVerification => {
(GREEDY_VERIFICATION_ROLES, true, true, true)
}
VerificationStrategy::CreativeIntegrity => (CREATIVE_INTEGITY_ROLES, false, true, true),
VerificationStrategy::AuthoritativeIntegrity => {
(AUTHORITATIVE_INTEGRITY_ROLES, false, false, false)
}
VerificationStrategy::ExhaustiveVerification => {
(EXHAUSTIVE_VERIFICATION_ROLES, true, true, false)
}
VerificationStrategy::MultipleAttestation(a) => (a.as_slice(), false, true, true),
VerificationStrategy::MultipleAttestationGreedy(a) => (a.as_slice(), true, true, true),
};
match inv.signature.as_ref() {
None => {
debug!(id = %inv.bindle.id, "No signatures on invoice");
Err(SignatureError::Unverified(
"No signatures found on invoice. At least one signature is required"
.to_string(),
))
}
Some(signatures) => {
let mut known_key = false;
let mut filled_roles: Vec<SignatureRole> = vec![];
for s in signatures {
debug!(by = %s.by, "Checking signature");
let target_role = roles.contains(&s.role);
if !all_valid && !target_role {
debug!("Not a target role, and not running all_valid");
continue;
}
let role = s.role.clone();
let cleartext = inv.cleartext(&s.by, &role);
self.verify_signature(s, cleartext.as_bytes())?;
debug!("Signature verified");
if !target_role && !all_verified {
debug!("Not a target role, not checking for verification");
continue;
} else if all_roles {
filled_roles.push(role);
}
let pubkey = base64::decode(&s.key)
.map_err(|_| SignatureError::CorruptKey(s.key.to_string()))?;
let pko = PublicKey::from_bytes(pubkey.as_slice())
.map_err(|_| SignatureError::CorruptKey(s.key.to_string()))?;
debug!("Looking for key");
if keyring.contains(&pko) {
debug!("Found key {}", s.by);
known_key = true;
} else if all_verified {
return Err(SignatureError::Unverified(
"strategy requires that all signatures for role(s) must be verified"
.to_owned(),
));
}
}
if !known_key {
debug!("No known key");
return Err(SignatureError::NoKnownKey);
}
if all_roles {
for should_role in roles {
if !filled_roles.contains(should_role) {
return Err(SignatureError::Unverified(format!(
"No signature found for role {:?}",
should_role,
)));
}
}
}
Ok(VerifiedInvoice(invoice))
}
}
}
}
pub struct VerifiedInvoice<T: Into<crate::Invoice>>(T);
impl<T: Into<crate::Invoice>> Verified for VerifiedInvoice<T> {}
impl<T: Into<crate::Invoice>> super::sealed::Sealed for VerifiedInvoice<T> {}
impl<T> Signed for VerifiedInvoice<T>
where
T: Signed + Into<crate::Invoice>,
{
fn signed(self) -> crate::Invoice {
self.0.signed()
}
}
impl<T> Borrow<crate::Invoice> for VerifiedInvoice<T>
where
T: Into<crate::Invoice> + Borrow<crate::Invoice>,
{
fn borrow(&self) -> &crate::Invoice {
self.0.borrow()
}
}
impl<T> BorrowMut<crate::Invoice> for VerifiedInvoice<T>
where
T: Into<crate::Invoice> + BorrowMut<crate::Invoice>,
{
fn borrow_mut(&mut self) -> &mut crate::Invoice {
self.0.borrow_mut()
}
}
pub(crate) struct NoopVerified<T: Into<crate::Invoice>>(pub(crate) T);
impl<T: Into<crate::Invoice>> Verified for NoopVerified<T> {}
impl<T: Into<crate::Invoice>> super::sealed::Sealed for NoopVerified<T> {}
impl<T> Signed for NoopVerified<T>
where
T: Signed + Into<crate::Invoice>,
{
fn signed(self) -> crate::Invoice {
self.0.signed()
}
}
#[allow(clippy::from_over_into)]
impl<T: Into<crate::Invoice>> Into<crate::Invoice> for NoopVerified<T> {
fn into(self) -> crate::Invoice {
self.0.into()
}
}
#[allow(clippy::from_over_into)]
impl<T: Into<crate::Invoice>> Into<crate::Invoice> for VerifiedInvoice<T> {
fn into(self) -> crate::Invoice {
self.0.into()
}
}
impl<T> Debug for VerifiedInvoice<T>
where
T: Debug + Into<crate::Invoice>,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
self.0.fmt(f)
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::invoice::*;
use std::convert::TryInto;
#[test]
fn test_parse_verification_strategies() {
let strat = "CreativeIntegrity"
.parse::<VerificationStrategy>()
.expect("should parse");
assert!(
matches!(strat, VerificationStrategy::CreativeIntegrity),
"Should parse to the correct type"
);
let strat = "AuthoritativeIntegrity"
.parse::<VerificationStrategy>()
.expect("should parse");
assert!(
matches!(strat, VerificationStrategy::AuthoritativeIntegrity),
"Should parse to the correct type"
);
let strat = "GreedyVerification"
.parse::<VerificationStrategy>()
.expect("should parse");
assert!(
matches!(strat, VerificationStrategy::GreedyVerification),
"Should parse to the correct type"
);
let strat = "ExhaustiveVerification"
.parse::<VerificationStrategy>()
.expect("should parse");
assert!(
matches!(strat, VerificationStrategy::ExhaustiveVerification),
"Should parse to the correct type"
);
let strat = "MultipleAttestation[Creator, Host, Approver]"
.parse::<VerificationStrategy>()
.expect("should parse");
assert!(
matches!(strat, VerificationStrategy::MultipleAttestation(_)),
"Should parse to the correct type"
);
let strat = "MultipleAttestationGreedy[Creator, Host, Approver]"
.parse::<VerificationStrategy>()
.expect("should parse");
match strat {
VerificationStrategy::MultipleAttestationGreedy(roles) => {
assert!(
roles.contains(&SignatureRole::Creator),
"Roles should contain creator"
);
assert!(
roles.contains(&SignatureRole::Host),
"Roles should contain host"
);
assert!(
roles.contains(&SignatureRole::Approver),
"Roles should contain approver"
);
}
_ => panic!("Wrong type returned"),
}
let strat = "CrEaTiVeInTeGrItY"
.parse::<VerificationStrategy>()
.expect("mixed case should parse");
assert!(
matches!(strat, VerificationStrategy::CreativeIntegrity),
"Should parse to the correct type"
);
let strat = " multipleAttestAtion[Creator, Host, Approver] "
.parse::<VerificationStrategy>()
.expect("extra spaces should parse");
assert!(
matches!(strat, VerificationStrategy::MultipleAttestation(_)),
"Should parse to the correct type"
);
"nopenopenope"
.parse::<VerificationStrategy>()
.expect_err("non-existent strategy shouldn't parse");
"Creative Integrity"
.parse::<VerificationStrategy>()
.expect_err("spacing in the middle shouldn't parse");
"MultipleAttestationCreator, Host, Approver]"
.parse::<VerificationStrategy>()
.expect_err("missing start brace shouldn't parse");
"MultipleAttestation[Creator, Host, Approver"
.parse::<VerificationStrategy>()
.expect_err("missing end brace shouldn't parse");
"MultipleAttestation[Blah, Host, Approver]"
.parse::<VerificationStrategy>()
.expect_err("Invalid role shouldn't parse");
}
#[test]
fn test_strategy_deserialize() {
#[derive(serde::Deserialize)]
struct StrategyMock {
verification_strategy: VerificationStrategy,
}
let toml_value = r#"
verification_strategy = "MultipleAttestation[Creator, Host, Approver]"
"#;
let mock: StrategyMock = toml::from_str(toml_value).expect("toml should parse");
assert!(
matches!(
mock.verification_strategy,
VerificationStrategy::MultipleAttestation(_)
),
"Should parse to the correct type"
);
let toml_value = r#"
verification_strategy = "CreativeIntegrity"
"#;
let mock: StrategyMock = toml::from_str(toml_value).expect("toml should parse");
assert!(
matches!(
mock.verification_strategy,
VerificationStrategy::CreativeIntegrity
),
"Should parse to the correct type"
);
let json_value = r#"
{
"verification_strategy": "MultipleAttestation[Creator, Host, Approver]"
}
"#;
let mock: StrategyMock = serde_json::from_str(json_value).expect("json should parse");
assert!(
matches!(
mock.verification_strategy,
VerificationStrategy::MultipleAttestation(_)
),
"Should parse to the correct type"
);
}
#[test]
fn test_verification_strategies() {
let invoice = r#"
bindleVersion = "1.0.0"
[bindle]
name = "arecebo"
version = "1.2.3"
[[parcel]]
[parcel.label]
sha256 = "aaabbbcccdddeeefff"
name = "telescope.gif"
mediaType = "image/gif"
size = 123_456
[[parcel]]
[parcel.label]
sha256 = "111aaabbbcccdddeee"
name = "telescope.txt"
mediaType = "text/plain"
size = 123_456
"#;
let invoice: crate::Invoice = toml::from_str(invoice).expect("a nice clean parse");
let key_creator = SecretKeyEntry::new("Test Creator", vec![SignatureRole::Creator]);
let key_approver = SecretKeyEntry::new("Test Approver", vec![SignatureRole::Approver]);
let key_host = SecretKeyEntry::new("Test Host", vec![SignatureRole::Host]);
let key_proxy = SecretKeyEntry::new("Test Proxy", vec![SignatureRole::Proxy]);
let keyring_keys = vec![
key_approver.clone().try_into().expect("convert to pubkey"),
key_host.clone().try_into().expect("convert to pubkey"),
key_creator.clone().try_into().expect("convert to pubkey"),
key_proxy.clone().try_into().expect("convert to pubkey"),
];
let keyring = KeyRing::new(keyring_keys);
{
let mut inv = invoice.clone();
inv.sign(SignatureRole::Host, &key_host)
.expect("signed as host");
VerificationStrategy::CreativeIntegrity
.verify(inv.clone(), &keyring)
.expect_err("inv should not pass: Requires creator");
VerificationStrategy::AuthoritativeIntegrity
.verify(inv.clone(), &keyring)
.expect_err("inv should not pass: Requires creator or approver");
VerificationStrategy::GreedyVerification
.verify(inv.clone(), &keyring)
.expect_err("inv should not pass: Requires creator and all valid");
VerificationStrategy::MultipleAttestationGreedy(vec![SignatureRole::Host])
.verify(inv.clone(), &keyring)
.expect("inv should pass: Only requires host");
VerificationStrategy::MultipleAttestationGreedy(vec![
SignatureRole::Host,
SignatureRole::Proxy,
])
.verify(inv.clone(), &keyring)
.expect_err("inv should not pass: Requires proxy");
VerificationStrategy::ExhaustiveVerification
.verify(inv, &keyring)
.expect("inv should not pass: Requires that all signatures must be verified");
}
{
let mut inv = invoice.clone();
inv.sign(SignatureRole::Host, &key_host)
.expect("signed as host");
inv.sign(SignatureRole::Creator, &key_creator)
.expect("signed as creator");
VerificationStrategy::CreativeIntegrity
.verify(inv.clone(), &keyring)
.expect("inv should pass: Signed by creator");
VerificationStrategy::AuthoritativeIntegrity
.verify(inv.clone(), &keyring)
.expect("inv should pass: Signed by creator");
VerificationStrategy::GreedyVerification
.verify(inv.clone(), &keyring)
.expect("inv should pass: Requires creator and all valid");
VerificationStrategy::MultipleAttestationGreedy(vec![SignatureRole::Host])
.verify(inv.clone(), &keyring)
.expect("inv should pass: Only requires host");
VerificationStrategy::MultipleAttestationGreedy(vec![
SignatureRole::Host,
SignatureRole::Proxy,
])
.verify(inv.clone(), &keyring)
.expect_err("inv should not pass: Requires proxy");
VerificationStrategy::ExhaustiveVerification
.verify(inv, &keyring)
.expect("inv should not pass: Requires that all signatures must be verified");
}
{
let mut inv = invoice.clone();
inv.sign(SignatureRole::Host, &key_host)
.expect("signed as host");
inv.sign(SignatureRole::Approver, &key_approver)
.expect("signed as approver");
VerificationStrategy::CreativeIntegrity
.verify(inv.clone(), &keyring)
.expect_err("inv should not pass: not signed by creator");
VerificationStrategy::AuthoritativeIntegrity
.verify(inv.clone(), &keyring)
.expect("inv should pass: Signed by approver");
VerificationStrategy::GreedyVerification
.verify(inv.clone(), &keyring)
.expect_err("inv should not pass: Requires creator and all valid");
VerificationStrategy::MultipleAttestationGreedy(vec![SignatureRole::Host])
.verify(inv.clone(), &keyring)
.expect("inv should pass: Only requires host");
VerificationStrategy::MultipleAttestationGreedy(vec![
SignatureRole::Host,
SignatureRole::Proxy,
])
.verify(inv.clone(), &keyring)
.expect_err("inv should not pass: Requires proxy");
VerificationStrategy::ExhaustiveVerification
.verify(inv, &keyring)
.expect("inv should not pass: Requires that all signatures must be verified");
}
{
let mut inv = invoice.clone();
inv.sign(SignatureRole::Host, &key_host)
.expect("signed as host");
inv.sign(SignatureRole::Creator, &key_creator)
.expect("signed as creator");
inv.sign(SignatureRole::Proxy, &key_proxy)
.expect("signed as proxy");
VerificationStrategy::CreativeIntegrity
.verify(inv.clone(), &keyring)
.expect("inv should pass: signed by creator");
VerificationStrategy::AuthoritativeIntegrity
.verify(inv.clone(), &keyring)
.expect("inv should pass: Signed by creator");
VerificationStrategy::GreedyVerification
.verify(inv.clone(), &keyring)
.expect("inv should pass: Requires creator and all valid");
VerificationStrategy::MultipleAttestationGreedy(vec![SignatureRole::Host])
.verify(inv.clone(), &keyring)
.expect("inv should pass: Only requires host");
VerificationStrategy::MultipleAttestationGreedy(vec![
SignatureRole::Host,
SignatureRole::Proxy,
])
.verify(inv.clone(), &keyring)
.expect("inv should pass: Requires proxy");
VerificationStrategy::ExhaustiveVerification
.verify(inv, &keyring)
.expect("inv should pass: Requires that all signatures must be verified");
}
println!("Signed by creator, host, and unknown key");
{
let mut inv = invoice;
inv.sign(SignatureRole::Host, &key_host)
.expect("signed as host");
inv.sign(SignatureRole::Creator, &key_creator)
.expect("signed as creator");
let key_anon = SecretKeyEntry::new("Unknown key", vec![SignatureRole::Approver]);
inv.sign(SignatureRole::Approver, &key_anon)
.expect("signed with unknown key");
VerificationStrategy::CreativeIntegrity
.verify(inv.clone(), &keyring)
.expect("inv should pass: signed by creator");
VerificationStrategy::AuthoritativeIntegrity
.verify(inv.clone(), &keyring)
.expect("inv should pass: Signed by creator");
VerificationStrategy::GreedyVerification
.verify(inv.clone(), &keyring)
.expect_err(
"inv should not pass: Requires creator and all known, anon is not known",
);
VerificationStrategy::MultipleAttestation(vec![SignatureRole::Host])
.verify(inv.clone(), &keyring)
.expect("inv should pass: Only requires host");
VerificationStrategy::MultipleAttestationGreedy(vec![
SignatureRole::Host,
SignatureRole::Approver,
])
.verify(inv.clone(), &keyring)
.expect_err("inv should not pass: Requires approver to be known");
VerificationStrategy::ExhaustiveVerification
.verify(inv, &keyring)
.expect_err("inv should not pass: Requires that all signatures must be verified");
}
}
}