mod api;
mod bindle_spec;
mod condition;
mod group;
mod label;
mod parcel;
mod sealed;
pub mod signature;
pub mod verification;
#[cfg(feature = "client")]
#[doc(inline)]
pub(crate) use api::DeviceAuthorizationExtraFields;
#[cfg(any(feature = "client", feature = "server"))]
#[doc(inline)]
pub(crate) use api::LoginParams;
#[doc(inline)]
pub use api::{
ErrorResponse, HealthResponse, InvoiceCreateResponse, KeyOptions, MissingParcelsResponse,
QueryOptions,
};
#[doc(inline)]
pub use bindle_spec::BindleSpec;
#[doc(inline)]
pub use condition::Condition;
#[doc(inline)]
pub use group::Group;
#[doc(inline)]
pub use label::Label;
#[doc(inline)]
pub use parcel::Parcel;
#[doc(inline)]
pub use signature::{SecretKeyEntry, Signature, SignatureError, SignatureRole};
#[doc(inline)]
pub use verification::VerificationStrategy;
use ed25519_dalek::{Signature as EdSignature, Signer};
use semver::{Version, VersionReq};
use serde::{Deserialize, Serialize};
use tracing::info;
use std::borrow::{Borrow, BorrowMut};
use std::collections::BTreeMap;
use std::fmt::Debug;
use std::time::{SystemTime, UNIX_EPOCH};
use self::verification::Verified;
use crate::BINDLE_VERSION_1;
pub type FeatureMap = BTreeMap<String, BTreeMap<String, String>>;
pub type AnnotationMap = BTreeMap<String, String>;
pub trait Signed: sealed::Sealed {
fn signed(self) -> Invoice;
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct Invoice {
pub bindle_version: String,
pub yanked: Option<bool>,
pub yanked_signature: Option<Vec<Signature>>,
pub bindle: BindleSpec,
pub annotations: Option<AnnotationMap>,
pub parcel: Option<Vec<Parcel>>,
pub group: Option<Vec<Group>>,
pub signature: Option<Vec<Signature>>,
}
impl Invoice {
pub fn new(spec: BindleSpec) -> Self {
Invoice {
bindle_version: BINDLE_VERSION_1.to_owned(),
bindle: spec,
parcel: None,
yanked: None,
yanked_signature: None,
annotations: None,
signature: None,
group: None,
}
}
pub fn name(&self) -> String {
format!("{}/{}", self.bindle.id.name(), self.bindle.id.version())
}
pub fn canonical_name(&self) -> String {
self.bindle.id.sha()
}
pub(crate) fn version_in_range(&self, requirement: &str) -> bool {
version_compare(self.bindle.id.version(), requirement)
}
pub fn has_group(&self, name: &str) -> bool {
let empty = Vec::with_capacity(0);
self.group
.as_ref()
.unwrap_or(&empty)
.iter()
.any(|g| g.name == name)
}
pub fn group_members(&self, name: &str) -> Vec<Parcel> {
if !self.has_group(name) {
info!(name, "no such group");
return vec![];
}
let zero_vec = Vec::with_capacity(0);
self.parcel
.as_ref()
.unwrap_or(&zero_vec)
.iter()
.filter(|p| p.member_of(name))
.cloned()
.collect()
}
fn cleartext(&self, by: &str, role: &SignatureRole) -> String {
let mut buf = vec![
by.to_owned(),
self.bindle.id.name().to_owned(),
self.bindle.id.version_string(),
role.to_string(),
'~'.to_string(),
];
if let Some(list) = self.parcel.as_ref() {
list.iter().for_each(|p| {
buf.push(p.label.sha256.clone());
})
}
buf.join("\n")
}
pub fn sign(
&mut self,
signer_role: SignatureRole,
keyfile: &SecretKeyEntry,
) -> Result<(), SignatureError> {
let signer_name = keyfile.label.clone();
let key = keyfile.key()?;
let encoded_key = base64::encode(key.public.to_bytes());
if let Some(sigs) = self.signature.as_ref() {
for s in sigs {
if s.key == encoded_key {
return Err(SignatureError::DuplicateSignature);
}
}
}
let cleartext = self.cleartext(&signer_name, &signer_role);
let signature: EdSignature = key.sign(cleartext.as_bytes());
let ts = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|_| SignatureError::SigningFailed)?;
let signature_entry = Signature {
by: signer_name,
key: encoded_key,
signature: base64::encode(signature.to_bytes()),
role: signer_role,
at: ts.as_secs(),
};
match self.signature.as_mut() {
Some(signatures) => signatures.push(signature_entry),
None => self.signature = Some(vec![signature_entry]),
};
Ok(())
}
}
pub fn sign<I>(
mut invoice: I,
sign_with: Vec<(SignatureRole, &SecretKeyEntry)>,
) -> Result<SignedInvoice<I>, SignatureError>
where
I: BorrowMut<Invoice> + Into<crate::Invoice>,
{
let inv = invoice.borrow_mut();
for (role, key) in sign_with {
sign_one(inv, role, key)?;
}
Ok(SignedInvoice(invoice))
}
fn sign_one(
inv: &mut Invoice,
signer_role: SignatureRole,
keyfile: &SecretKeyEntry,
) -> Result<(), SignatureError> {
let signer_name = keyfile.label.clone();
let key = keyfile.key()?;
let encoded_key = base64::encode(key.public.to_bytes());
if let Some(sigs) = inv.signature.as_ref() {
for s in sigs {
if s.key == encoded_key {
return Err(SignatureError::DuplicateSignature);
}
}
}
let cleartext = inv.cleartext(&signer_name, &signer_role);
let signature: EdSignature = key.sign(cleartext.as_bytes());
let ts = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|_| SignatureError::SigningFailed)?;
let signature_entry = Signature {
by: signer_name,
key: encoded_key,
signature: base64::encode(signature.to_bytes()),
role: signer_role,
at: ts.as_secs(),
};
match inv.signature.as_mut() {
Some(signatures) => signatures.push(signature_entry),
None => inv.signature = Some(vec![signature_entry]),
};
Ok(())
}
pub struct SignedInvoice<T: Into<Invoice>>(T);
impl<T: Into<Invoice>> Signed for SignedInvoice<T> {
fn signed(self) -> Invoice {
self.0.into()
}
}
impl<T: Into<Invoice>> sealed::Sealed for SignedInvoice<T> {}
impl<T> Verified for SignedInvoice<T> where T: Verified + Into<Invoice> {}
impl<T> Borrow<Invoice> for SignedInvoice<T>
where
T: Into<Invoice> + Borrow<Invoice>,
{
fn borrow(&self) -> &Invoice {
self.0.borrow()
}
}
impl<T> Debug for SignedInvoice<T>
where
T: Debug + Into<Invoice>,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
self.0.fmt(f)
}
}
pub(crate) struct NoopSigned<T: Into<Invoice>>(pub(crate) T);
impl<T: Into<Invoice>> Signed for NoopSigned<T> {
fn signed(self) -> Invoice {
self.0.into()
}
}
impl<T> Verified for NoopSigned<T> where T: Verified + Into<Invoice> {}
impl<T: Into<Invoice>> sealed::Sealed for NoopSigned<T> {}
impl<T> Borrow<Invoice> for NoopSigned<T>
where
T: Into<Invoice> + Borrow<Invoice>,
{
fn borrow(&self) -> &Invoice {
self.0.borrow()
}
}
impl<T> Debug for NoopSigned<T>
where
T: Debug + Into<Invoice>,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
self.0.fmt(f)
}
}
fn version_compare(version: &Version, requirement: &str) -> bool {
if requirement.is_empty() {
return true;
}
if let Ok(v) = Version::parse(requirement) {
return *version == v;
}
match VersionReq::parse(requirement) {
Ok(req) => req.matches(version),
Err(e) => {
tracing::log::error!("SemVer range could not parse: {}", e);
false
}
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::invoice::signature::{KeyEntry, KeyRing};
use std::convert::TryInto;
use std::fs::read;
use std::path::Path;
#[test]
fn test_version_comparisons() {
let reqs = vec!["= 1.2.3", "1.2.3", "1.2.3", "^1.1", "~1.2", ""];
let version = Version::parse("1.2.3").unwrap();
reqs.iter().for_each(|r| {
if !version_compare(&version, r) {
panic!("Should have passed: {}", r)
}
});
let reqs = vec!["2", "%^&%^&%"];
reqs.iter()
.for_each(|r| assert!(!version_compare(&version, r)));
}
#[test]
fn signing_and_verifying() {
let invoice = r#"
bindleVersion = "1.0.0"
[bindle]
name = "aricebo"
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 signer_name1 = "Matt Butcher <matt@example.com>";
let signer_name2 = "Not Matt Butcher <not.matt@example.com>";
let keypair1 = SecretKeyEntry::new(signer_name1, vec![SignatureRole::Creator]);
let keypair2 = SecretKeyEntry::new(signer_name2, vec![SignatureRole::Proxy]);
let keyring = KeyRing::new(vec![(&keypair1).try_into().expect("convert to public key")]);
let signed = sign(
invoice,
vec![
(SignatureRole::Creator, &keypair1),
(SignatureRole::Proxy, &keypair2),
],
)
.expect("Sign the parcels");
let invoice: Invoice = signed.signed();
sign(invoice.clone(), vec![(SignatureRole::Host, &keypair2)])
.expect_err("Should not be able to sign again with the same key");
assert_eq!(2, invoice.signature.as_ref().unwrap().len());
VerificationStrategy::CreativeIntegrity
.verify(invoice.clone(), &keyring)
.expect("with keys on the keyring, this should pass");
let keyring = KeyRing::new(vec![keypair2.try_into().expect("convert to public key")]);
VerificationStrategy::CreativeIntegrity
.verify(invoice, &keyring)
.expect_err("missing the creator key, so verification should fail");
}
#[test]
fn invalid_signatures_should_fail() {
let invoice = r#"
bindleVersion = "1.0.0"
[bindle]
name = "aricebo"
version = "1.2.3"
[[signature]]
by = "Matt Butcher <matt@example.com>"
signature = "T0JWSU9VU0xZIEZBS0UK" # echo "OBVIOUSLY FAKE" | base64
key = "jTtZIzQCfZh8xy6st40xxLwxVw++cf0C0cMH3nJBF+c="
role = "creator"
at = 1611960337
[[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 pubkey = KeyEntry {
key: "jTtZIzQCfZh8xy6st40xxLwxVw++cf0C0cMH3nJBF+c=".to_owned(),
label: "Test Key".to_owned(),
roles: vec![SignatureRole::Host],
label_signature: None,
};
let keyring = KeyRing::new(vec![pubkey]);
match VerificationStrategy::default().verify(invoice, &keyring) {
Err(SignatureError::CorruptSignature(s)) => {
assert_eq!("jTtZIzQCfZh8xy6st40xxLwxVw++cf0C0cMH3nJBF+c=", s)
}
Err(e) => panic!("Unexpected error {:?}", e),
Ok(_) => panic!("Verification should have failed"),
}
}
#[test]
fn invalid_key_should_fail() {
let invoice = r#"
bindleVersion = "1.0.0"
[bindle]
name = "aricebo"
version = "1.2.3"
[[signature]]
by = "Matt Butcher <matt@example.com>"
signature = "x6sI2Qme4xf6IRtHGaoMqMRL0vjvVHLq3ZCaKVkHNr3oCw+kvTrxek7RbuajIgS71zUQew4/vVT4Do0xa49+CQ=="
key = "T0JWSU9VU0xZIEZBS0UK" # echo "OBVIOUSLY FAKE" | base64
role = "creator"
at = 1611960337
[[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 keyring = KeyRing::new(vec![KeyEntry {
key: "jTtZIzQCfZh8xy6st40xxLwxVw++cf0C0cMH3nJBF+c=".to_owned(),
label: "Test Key".to_owned(),
roles: vec![SignatureRole::Creator],
label_signature: None,
}]);
match VerificationStrategy::default().verify(invoice, &keyring) {
Err(SignatureError::CorruptKey(s)) => assert_eq!("T0JWSU9VU0xZIEZBS0UK", s),
Err(e) => panic!("Unexpected error {:?}", e),
Ok(_) => panic!("Verification should have failed"),
}
}
#[test]
fn test_invoice_should_serialize() {
let label = Label {
sha256: "abcdef1234567890987654321".to_owned(),
media_type: "text/toml".to_owned(),
name: "foo.toml".to_owned(),
size: 101,
annotations: None,
feature: None,
origin: None,
};
let parcel = Parcel {
label,
conditions: None,
};
let parcels = Some(vec![parcel]);
let inv = Invoice {
bindle_version: crate::BINDLE_VERSION_1.to_owned(),
bindle: BindleSpec {
id: "foo/1.2.3".parse().unwrap(),
description: Some("bar".to_owned()),
authors: Some(vec!["m butcher".to_owned()]),
},
parcel: parcels,
yanked: None,
yanked_signature: None,
annotations: None,
group: None,
signature: None,
};
let res = toml::to_string(&inv).unwrap();
let inv2 = toml::from_str::<Invoice>(res.as_str()).unwrap();
let b = inv2.bindle;
assert_eq!(b.id.name(), "foo".to_owned());
assert_eq!(b.id.version_string(), "1.2.3");
assert_eq!(b.description.unwrap().as_str(), "bar");
assert_eq!(b.authors.unwrap()[0], "m butcher".to_owned());
let parcels = inv2.parcel.unwrap();
assert_eq!(parcels.len(), 1);
let par = &parcels[0];
let lab = &par.label;
assert_eq!(lab.name, "foo.toml".to_owned());
assert_eq!(lab.media_type, "text/toml".to_owned());
assert_eq!(lab.sha256, "abcdef1234567890987654321".to_owned());
assert_eq!(lab.size, 101);
}
#[test]
fn test_examples_in_spec_parse() {
let test_files = vec![
"test/data/simple-invoice.toml",
"test/data/full-invoice.toml",
"test/data/alt-format-invoice.toml",
];
test_files.iter().for_each(|file| test_parsing_a_file(file));
}
fn test_parsing_a_file(filename: &str) {
let invoice_path = Path::new(filename);
let raw = read(invoice_path).expect("read file contents");
let invoice = toml::from_slice::<Invoice>(&raw).expect("clean parse of invoice");
let _raw2 = toml::to_string_pretty(&invoice).expect("clean serialization of TOML");
}
#[test]
fn parcel_no_groups() {
let invoice = r#"
bindleVersion = "1.0.0"
[bindle]
name = "aricebo"
version = "1.2.3"
[[group]]
name = "images"
[[parcel]]
[parcel.label]
sha256 = "aaabbbcccdddeeefff"
name = "telescope.gif"
mediaType = "image/gif"
size = 123_456
[parcel.conditions]
memberOf = ["telescopes"]
[[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 parcels = invoice.parcel.expect("expected some parcels");
let img = &parcels[0];
let txt = &parcels[1];
assert!(img.member_of("telescopes"));
assert!(!img.is_global_group());
assert!(txt.is_global_group());
assert!(!txt.member_of("telescopes"));
}
#[test]
fn test_group_members() {
let invoice = r#"
bindleVersion = "1.0.0"
[bindle]
name = "aricebo"
version = "1.2.3"
[[group]]
name = "telescopes"
[[parcel]]
[parcel.label]
sha256 = "aaabbbcccdddeeefff"
name = "telescope.gif"
mediaType = "image/gif"
size = 123_456
[parcel.conditions]
memberOf = ["telescopes"]
[[parcel]]
[parcel.label]
sha256 = "aaabbbcccdddeeeggg"
name = "telescope2.gif"
mediaType = "image/gif"
size = 123_456
[parcel.conditions]
memberOf = ["telescopes"]
[[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 members = invoice.group_members("telescopes");
assert_eq!(2, members.len());
}
}