extern crate reqwest;
extern crate serde;
use anyhow::anyhow;
use base64::{engine::general_purpose::URL_SAFE, Engine as _};
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
use reqwest::blocking::Client;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
pub enum SigningMethod {
Text,
}
const PROVENANCE_PREAMBLE: &str = "~~🔏";
const PROVENANCE_POSTAMBLE: &str = "🔏~~";
const PROVENANCE_VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Default, Debug)]
pub struct SignerDetails {
pub verification_url: String,
pub verification_key: VerifyingKey,
}
#[derive(Default, Debug, Serialize, Deserialize)]
pub struct SignerDetailsFromServer {
pub verification_url: String,
pub verification_key_b64: String,
pub metadata: HashMap<String, String>,
}
#[derive(Default, Debug, Serialize, Deserialize)]
pub struct KeyDetails {
pub verification: String,
pub signing: String,
}
#[derive(Eq, Hash, PartialEq, Debug, Clone)]
pub struct Username(String);
pub struct Base64Signature(pub String);
impl TryFrom<Base64Signature> for Signature {
type Error = anyhow::Error;
fn try_from(base64_signature: Base64Signature) -> Result<Self, Self::Error> {
let Ok(bytes_of_base64) = URL_SAFE.decode(base64_signature.0.as_bytes()) else {
return Err(anyhow!(
"Couldn't convert {} into bytes",
base64_signature.0
));
};
if bytes_of_base64.len() != ed25519_dalek::SIGNATURE_LENGTH {
return Err(anyhow!(
"Base64Signature needs to be {} bytes long, but is {} bytes long",
ed25519_dalek::SIGNATURE_LENGTH,
bytes_of_base64.len(),
));
}
let known_length_slice: &[u8; ed25519_dalek::SIGNATURE_LENGTH] =
bytes_of_base64.as_slice().try_into()?;
Ok(Signature::from_bytes(known_length_slice))
}
}
pub struct Base64VerifyingKey(pub String);
impl TryFrom<Base64VerifyingKey> for VerifyingKey {
type Error = anyhow::Error;
fn try_from(base64_verifying_key: Base64VerifyingKey) -> Result<Self, Self::Error> {
let Ok(bytes_of_base64) = URL_SAFE.decode(base64_verifying_key.0.as_bytes()) else {
return Err(anyhow!(
"Couldn't convert {} into bytes",
base64_verifying_key.0
));
};
if bytes_of_base64.len() != ed25519_dalek::SECRET_KEY_LENGTH {
return Err(anyhow!(
"Base64VerifyingKey needs to be {} bytes long, but is {} bytes long",
ed25519_dalek::SECRET_KEY_LENGTH,
bytes_of_base64.len(),
));
}
let known_length_slice: &[u8; ed25519_dalek::SECRET_KEY_LENGTH] =
bytes_of_base64.as_slice().try_into()?;
Ok(VerifyingKey::from_bytes(known_length_slice)?)
}
}
pub struct Base64SigningKey(pub String);
impl TryFrom<Base64SigningKey> for SigningKey {
type Error = anyhow::Error;
fn try_from(base64_signing_key: Base64SigningKey) -> Result<Self, Self::Error> {
let Ok(bytes_of_base64) = URL_SAFE.decode(base64_signing_key.0.as_bytes()) else {
return Err(anyhow!(
"Couldn't convert {} into bytes",
base64_signing_key.0
));
};
if bytes_of_base64.len() != ed25519_dalek::PUBLIC_KEY_LENGTH {
return Err(anyhow!(
"Base64SigningKey needs to be {} bytes long, but is {} bytes long",
ed25519_dalek::PUBLIC_KEY_LENGTH,
bytes_of_base64.len(),
));
}
let known_length_slice: &[u8; ed25519_dalek::PUBLIC_KEY_LENGTH] =
bytes_of_base64.as_slice().try_into()?;
Ok(SigningKey::from_bytes(known_length_slice))
}
}
fn get_verifying_key_from_url(url: &str, client: &Client) -> anyhow::Result<VerifyingKey> {
let response = client.get(url).send()?;
if !response.status().is_success() {
return Err(anyhow!(
"GET request to {url} failed: {}",
response.status()
));
}
let signer_details: SignerDetailsFromServer = response.json()?;
Base64VerifyingKey(signer_details.verification_key_b64).try_into()
}
pub fn verify(signed_doc: &str) -> anyhow::Result<(SignerDetails, String)> {
let split = signed_doc.split_once('\n');
let Some((first, doc)) = split else {
return Err(anyhow!(
"Document has only one line, therefore cannot be signed"
));
};
let words = first.split(' ').collect::<Vec<_>>();
let [preamble, version, url, signature_b64, postamble] = words[..] else {
return Err(anyhow!(
"Document doesn't have five space-separated words in first line"
));
};
if url.is_empty() {
return Err(anyhow!("URL cannot be empty"));
}
if signature_b64.is_empty() {
return Err(anyhow!("Signature cannot be empty"));
}
if preamble != PROVENANCE_PREAMBLE {
return Err(anyhow!(
"Document preamble is '{preamble}', not '{PROVENANCE_PREAMBLE}'"
));
}
if version != PROVENANCE_VERSION {
return Err(anyhow!(
"Document version is '{version}', not '{PROVENANCE_VERSION}'"
));
}
if postamble != PROVENANCE_POSTAMBLE {
return Err(anyhow!(
"Document postamble is '{postamble}', not '{PROVENANCE_POSTAMBLE}'"
));
}
let signature: Signature = Base64Signature(signature_b64.to_string()).try_into()?;
let client = reqwest::blocking::Client::new();
let verification_key = get_verifying_key_from_url(url, &client)?;
if verification_key.verify(doc.as_bytes(), &signature).is_err() {
return Err(anyhow!(
"Document signature '{signature}' could not be verified"
));
}
Ok((
SignerDetails {
verification_url: url.to_string(),
verification_key,
},
doc.to_string(),
))
}
pub fn sign(doc: &str, signing_key: SigningKey, url: &str) -> String {
let signature = signing_key.sign(doc.as_bytes());
let encoded_signature = Base64Signature(URL_SAFE.encode(signature.to_bytes()));
format_doc(url, encoded_signature, doc)
}
pub fn format_doc(url: &str, encoded_signature: Base64Signature, doc: &str) -> String {
format!(
"{PROVENANCE_PREAMBLE} {PROVENANCE_VERSION} {url} {} {PROVENANCE_POSTAMBLE}\n{doc}",
encoded_signature.0
)
}
#[cfg(test)]
mod tests {
use super::*;
use rand::rngs::OsRng;
use rand::Rng;
fn generate_keys_for_user(
url: &str,
username: &Username,
client: &Client,
) -> anyhow::Result<KeyDetails> {
let response = client
.get(format!("{url}/generate_key/{}", username.0))
.send()?;
let Ok(key_details) = response.json() else {
return Err(anyhow!("Failed to get keys for {}", username.0));
};
Ok(key_details)
}
#[test]
fn verification_fails_if_no_newline() {
assert!(verify("document text here").is_err());
}
#[test]
fn verification_fails_if_bad_start() {
assert!(
verify(format!("<!PROVENANCE_PREAMBLE!> {PROVENANCE_VERSION} url signature {PROVENANCE_POSTAMBLE}\ndocument text here").as_str())
.is_err()
);
}
#[test]
fn verification_fails_if_bad_ending() {
assert!(
verify(format!("{PROVENANCE_PREAMBLE} {PROVENANCE_VERSION} url signature <!PROVENANCE_POSTAMBLE!>\ndocument text here").as_str())
.is_err()
);
}
#[test]
fn verification_fails_if_bad_version() {
assert!(verify(
format!("{PROVENANCE_PREAMBLE} <!PROVENANCE_VERSION!> url signature {PROVENANCE_POSTAMBLE}\ndocument text here").as_str(),
).is_err());
}
#[test]
fn verification_fails_if_signature_is_empty() {
assert!(verify(
format!("{PROVENANCE_PREAMBLE} {PROVENANCE_VERSION} url {PROVENANCE_POSTAMBLE}\ndocument text here").as_str(),
).is_err());
}
#[test]
fn verification_fails_if_url_is_empty() {
assert!(verify(
format!("{PROVENANCE_PREAMBLE} {PROVENANCE_VERSION} signature {PROVENANCE_POSTAMBLE}\ndocument text here").as_str(),
).is_err());
}
#[test]
fn verification_fails_if_wrong_number_of_args() {
assert!(verify("one two three four\ndocument text here").is_err());
}
#[test]
fn verification_fails_during_signature_from_slice() {
let url = "http://localhost:8000/provenance/beyarkay";
let encoded_signature =
Base64Signature(URL_SAFE.encode("not a valid signature".as_bytes()));
let doc = "Document text here";
assert!(verify(format_doc(url, encoded_signature, doc).as_str()).is_err());
}
#[test]
fn verification_fails_during_base64_decoding() {
let url = "http://localhost:8000/provenance/beyarkay";
let badly_encoded_signature = Base64Signature("!exclamations!arent!base64!".to_string());
let doc = "Document text here";
assert!(verify(format_doc(url, badly_encoded_signature, doc).as_str()).is_err());
}
#[test]
fn verification_fails_if_bad_key() {
let url = "http://localhost:8000/provenance/beyarkay";
let doc = "document text here";
let mut csprng = OsRng;
let signing_key = SigningKey::generate(&mut csprng);
let encoded_signature =
Base64Signature(URL_SAFE.encode(signing_key.sign(doc.as_bytes()).to_bytes()));
assert!(verify(format_doc(url, encoded_signature, doc).as_str()).is_err());
}
#[test]
fn verification_fails_if_bad_doc() {
let url = "http://localhost:8000/provenance/beyarkay";
let doc = "document text here";
let client = reqwest::blocking::Client::new();
let mut random_numbers = OsRng;
let key_details = generate_keys_for_user(
"http://localhost:8000",
&Username(format!("user_{}", random_numbers.gen_range(0..1_000_000))),
&client,
)
.unwrap();
let signing_key: SigningKey = Base64SigningKey(key_details.signing).try_into().unwrap();
let signature = signing_key.sign(doc.as_bytes());
let encoded_signature = Base64Signature(URL_SAFE.encode(signature.to_bytes()));
let mutated_doc = format!("{doc}and then some extra data");
assert!(verify(format_doc(url, encoded_signature, &mutated_doc).as_str()).is_err());
}
#[test]
fn verification_succeeds() {
let mut random_numbers = OsRng;
let username = Username(format!("user_{}", random_numbers.gen_range(0..1_000_000)));
let provenance_url = format!("http://localhost:8000/provenance/{}", username.0);
let doc = "document text here";
let client = reqwest::blocking::Client::new();
let key_details =
generate_keys_for_user("http://localhost:8000", &username, &client).unwrap();
let signing_key: SigningKey = Base64SigningKey(key_details.signing).try_into().unwrap();
let signature = signing_key.sign(doc.as_bytes());
let encoded_signature = Base64Signature(URL_SAFE.encode(signature.to_bytes()));
assert!(verify(format_doc(&provenance_url, encoded_signature, doc).as_str()).is_ok());
}
#[test]
fn multiple_signers() {
let client = reqwest::blocking::Client::new();
let mut random_numbers = OsRng;
let mut usernames: Vec<Username> = (0..10)
.map(|_| Username(format!("user_{}", random_numbers.gen_range(0..1_000_000))))
.collect();
let mut signing_keys: Vec<SigningKey> = usernames
.iter()
.map(|username| {
let key_details =
generate_keys_for_user("http://localhost:8000", username, &client).unwrap();
Base64SigningKey(key_details.signing).try_into().unwrap()
})
.collect();
let mut doc = "This is the document that's passing through lots of hands".to_string();
for (signing_key, username) in signing_keys.iter().zip(usernames.iter()) {
let provenance_url = format!("http://localhost:8000/provenance/{}", username.0);
let signature = signing_key.sign(doc.as_bytes());
let encoded_signature = Base64Signature(URL_SAFE.encode(signature.to_bytes()));
doc = format_doc(&provenance_url, encoded_signature, &doc);
assert!(verify(&doc).is_ok());
}
usernames.reverse();
signing_keys.reverse();
for (signing_key, username) in signing_keys.iter().zip(usernames.iter()) {
let verified = verify(&doc).unwrap();
let details = verified.0;
doc = verified.1;
assert_eq!(
details.verification_url,
format!("http://localhost:8000/provenance/{}", username.0)
);
assert_eq!(details.verification_key, signing_key.verifying_key());
}
}
#[test]
fn docstring_test() {
use crate::sign;
use ed25519_dalek::SigningKey;
let url = "http://localhost:8000/provenance/beyarkay";
let doc = "Some document that I definitely wrote";
let base64_signing_key =
Base64SigningKey("-5TaFC0xFOj_hf7mlvVaLKKpVFTaXUrLDzRqaaf7gFw=".to_string());
let signing_key: SigningKey = base64_signing_key.try_into().unwrap();
let _signed_doc = sign(doc, signing_key, url);
}
}