pub use ed25519_dalek::{Keypair, PublicKey, Signature as EdSignature, Signer};
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[cfg(not(target_arch = "wasm32"))]
use tokio::io::AsyncWriteExt;
use tracing::error;
use std::convert::TryFrom;
use std::fmt::{Display, Formatter, Result as FmtResult};
#[cfg(not(target_arch = "wasm32"))]
use std::path::Path;
use std::str::FromStr;
pub const KEY_RING_VERSION: &str = "1.0";
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct Signature {
pub by: String,
pub signature: String,
pub key: String,
pub role: SignatureRole,
pub at: u64,
}
#[derive(Error, Debug)]
pub enum SignatureError {
#[error("signatures `{0}` cannot be verified")]
Unverified(String),
#[error("failed to sign the invoice with the given key")]
SigningFailed,
#[error("key is corrupt for `{0}`")]
CorruptKey(String),
#[error("signature block is corrupt for key {0}")]
CorruptSignature(String),
#[error("unknown signing key {0}")]
UnknownSigningKey(String),
#[error("none of the signatures are made with a known key")]
NoKnownKey,
#[error("cannot sign the data again with a key that has already signed the data")]
DuplicateSignature,
#[error("no suitable key for signing data")]
NoSuitableKey,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum SignatureRole {
Creator,
Proxy,
Host,
Approver,
}
impl Display for SignatureRole {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
write!(
f,
"{}",
match self {
Self::Creator => "creator",
Self::Proxy => "proxy",
Self::Host => "host",
Self::Approver => "approver",
}
)
}
}
impl FromStr for SignatureRole {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let normalized = s.trim().to_lowercase();
match normalized.as_str() {
"c" | "creator" => Ok(Self::Creator),
"h" | "host" => Ok(Self::Host),
"a" | "approver" => Ok(Self::Approver),
"p" | "proxy" => Ok(Self::Proxy),
_ => Err("Invalid SignatureRole, should be one of: Creator, Proxy, Host, Approver"),
}
}
}
#[async_trait::async_trait]
pub trait KeyRingLoader {
async fn load(&self) -> anyhow::Result<KeyRing>;
}
#[async_trait::async_trait]
pub trait KeyRingSaver {
async fn save(&self, keyring: &KeyRing) -> anyhow::Result<()>;
}
#[cfg(not(target_arch = "wasm32"))]
#[async_trait::async_trait]
impl<T: AsRef<Path> + Sync> KeyRingLoader for T {
async fn load(&self) -> anyhow::Result<KeyRing> {
let raw_data = tokio::fs::read(self).await.map_err(|e| {
anyhow::anyhow!(
"failed to read TOML file {}: {}",
self.as_ref().display(),
e
)
})?;
let res: KeyRing = toml::from_slice(&raw_data)?;
Ok(res)
}
}
#[cfg(not(target_arch = "wasm32"))]
#[async_trait::async_trait]
impl<T: AsRef<Path> + Sync> KeyRingSaver for T {
async fn save(&self, keyring: &KeyRing) -> anyhow::Result<()> {
let mut opts = tokio::fs::OpenOptions::new();
opts.create(true).write(true).truncate(true);
#[cfg(target_family = "unix")]
opts.mode(0o600);
let mut file = opts.open(self).await?;
file.write_all(&toml::to_vec(keyring)?).await?;
file.flush().await?;
Ok(())
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct KeyRing {
pub version: String,
pub key: Vec<KeyEntry>,
}
impl Default for KeyRing {
fn default() -> Self {
Self {
version: KEY_RING_VERSION.to_owned(),
key: vec![],
}
}
}
impl KeyRing {
pub fn new(keys: Vec<KeyEntry>) -> Self {
KeyRing {
version: KEY_RING_VERSION.to_owned(),
key: keys,
}
}
pub fn add_entry(&mut self, entry: KeyEntry) {
self.key.push(entry)
}
pub fn contains(&self, key: &PublicKey) -> bool {
for k in self.key.iter() {
match k.public_key() {
Err(e) => tracing::warn!(%e, "Error parsing key"),
Ok(pk) if pk == *key => return true,
_ => {}
}
tracing::debug!("No match. Moving on.");
}
tracing::debug!("No more keys to check");
false
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct KeyEntry {
pub label: String,
pub roles: Vec<SignatureRole>,
pub key: String,
pub label_signature: Option<String>,
}
impl KeyEntry {
pub fn new(label: &str, roles: Vec<SignatureRole>, public_key: PublicKey) -> Self {
let key = base64::encode(public_key.to_bytes());
KeyEntry {
label: label.to_owned(),
roles,
key,
label_signature: None,
}
}
pub fn sign_label(&mut self, key: Keypair) {
let sig = key.sign(self.label.as_bytes());
self.label_signature = Some(base64::encode(sig.to_bytes()));
}
pub fn verify_label(self, key: PublicKey) -> anyhow::Result<()> {
match self.label_signature {
None => {
tracing::log::info!("Label was not signed. Skipping.");
Ok(())
}
Some(txt) => {
let decoded_txt = base64::decode(txt)?;
let sig = EdSignature::try_from(decoded_txt.as_slice())?;
key.verify_strict(self.label.as_bytes(), &sig)?;
Ok(())
}
}
}
pub(crate) fn public_key(&self) -> Result<PublicKey, SignatureError> {
let rawbytes = base64::decode(&self.key).map_err(|_e| {
SignatureError::CorruptKey("Base64 decoding of the public key failed".to_owned())
})?;
let pk = PublicKey::from_bytes(rawbytes.as_slice()).map_err(|e| {
error!(%e, "Error loading public key");
SignatureError::CorruptKey("Could not load keypair".to_owned())
})?;
Ok(pk)
}
}
impl TryFrom<SecretKeyEntry> for KeyEntry {
type Error = SignatureError;
fn try_from(secret: SecretKeyEntry) -> std::result::Result<Self, SignatureError> {
let skey = secret.key()?;
let mut s = Self {
label: secret.label,
roles: secret.roles,
key: base64::encode(skey.public.to_bytes()),
label_signature: None,
};
s.sign_label(skey);
Ok(s)
}
}
impl TryFrom<&SecretKeyEntry> for KeyEntry {
type Error = SignatureError;
fn try_from(secret: &SecretKeyEntry) -> std::result::Result<Self, SignatureError> {
let skey = secret.key()?;
let mut s = Self {
label: secret.label.clone(),
roles: secret.roles.clone(),
key: base64::encode(skey.public.to_bytes()),
label_signature: None,
};
s.sign_label(skey);
Ok(s)
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct SecretKeyEntry {
pub label: String,
pub keypair: String,
pub roles: Vec<SignatureRole>,
}
impl SecretKeyEntry {
pub fn new(label: &str, roles: Vec<SignatureRole>) -> Self {
let mut rng = rand::rngs::OsRng {};
let rawkey = Keypair::generate(&mut rng);
let keypair = base64::encode(rawkey.to_bytes());
Self {
label: label.to_owned(),
keypair,
roles,
}
}
pub(crate) fn key(&self) -> Result<Keypair, SignatureError> {
let rawbytes = base64::decode(&self.keypair).map_err(|_e| {
SignatureError::CorruptKey("Base64 decoding of the keypair failed".to_owned())
})?;
let keypair = Keypair::from_bytes(&rawbytes).map_err(|e| {
tracing::log::error!("Error loading key: {}", e);
SignatureError::CorruptKey("Could not load keypair".to_owned())
})?;
Ok(keypair)
}
}
pub trait SecretKeyStorage {
fn get_first_matching(
&self,
role: &SignatureRole,
label_match: Option<&LabelMatch>,
) -> Option<&SecretKeyEntry>;
fn get_all_matching(
&self,
role: &SignatureRole,
label_match: Option<&LabelMatch>,
) -> Vec<&SecretKeyEntry>;
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct SecretKeyFile {
pub version: String,
pub key: Vec<SecretKeyEntry>,
}
impl Default for SecretKeyFile {
fn default() -> Self {
Self {
version: KEY_RING_VERSION.to_owned(),
key: vec![],
}
}
}
#[cfg(not(target_arch = "wasm32"))]
impl SecretKeyFile {
pub async fn load_file(path: impl AsRef<Path>) -> anyhow::Result<SecretKeyFile> {
let raw = tokio::fs::read(path).await?;
let t = toml::from_slice(&raw)?;
Ok(t)
}
pub async fn save_file(&self, dest: impl AsRef<Path>) -> anyhow::Result<()> {
let out = toml::to_vec(self)?;
let mut opts = tokio::fs::OpenOptions::new();
opts.create(true).write(true);
#[cfg(target_family = "unix")]
opts.mode(0o600);
let mut file = opts.open(dest).await?;
file.write_all(&out).await?;
file.flush().await?;
Ok(())
}
}
pub enum LabelMatch {
FullMatch(String),
PartialMatch(String),
}
impl SecretKeyStorage for SecretKeyFile {
fn get_first_matching(
&self,
role: &SignatureRole,
label_match: Option<&LabelMatch>,
) -> Option<&SecretKeyEntry> {
self.key.iter().find(|k| {
k.roles.contains(role)
&& match label_match {
Some(LabelMatch::FullMatch(label)) => k.label.eq(label),
Some(LabelMatch::PartialMatch(label)) => k.label.contains(label),
None => true,
}
})
}
fn get_all_matching(
&self,
role: &SignatureRole,
label_match: Option<&LabelMatch>,
) -> Vec<&SecretKeyEntry> {
self.key
.iter()
.filter(|k| {
k.roles.contains(role)
&& match label_match {
Some(LabelMatch::FullMatch(label)) => k.label.eq(label),
Some(LabelMatch::PartialMatch(label)) => k.label.contains(label),
None => true,
}
})
.collect()
}
}
#[cfg(test)]
mod test {
use super::*;
use ed25519_dalek::Keypair;
#[test]
fn test_parse_role() {
"Creator".parse::<SignatureRole>().expect("should parse");
"Proxy".parse::<SignatureRole>().expect("should parse");
"Host".parse::<SignatureRole>().expect("should parse");
"Approver".parse::<SignatureRole>().expect("should parse");
"CrEaToR"
.parse::<SignatureRole>()
.expect("mixed case should parse");
" ProxY "
.parse::<SignatureRole>()
.expect("extra spacing should parse");
"yipyipyip"
.parse::<SignatureRole>()
.expect_err("non-existent shouldn't parse");
}
#[test]
fn test_sign_label() {
let mut rng = rand::rngs::OsRng {};
let keypair = Keypair::generate(&mut rng);
let mut ke = KeyEntry {
label: "Matt Butcher <matt@example.com>".to_owned(),
key: "jTtZIzQCfZh8xy6st40xxLwxVw++cf0C0cMH3nJBF+c=".to_owned(),
roles: vec![SignatureRole::Host],
label_signature: None,
};
let pubkey = keypair.public;
ke.sign_label(keypair);
assert!(ke.label_signature.is_some());
ke.verify_label(pubkey).expect("verification failed");
}
#[tokio::test]
async fn test_secret_keys() {
let mut kr = SecretKeyFile::default();
assert_eq!(kr.key.len(), 0);
kr.key
.push(SecretKeyEntry::new("test", vec![SignatureRole::Proxy]));
assert_eq!(kr.key.len(), 1);
let outdir = tempfile::tempdir().expect("created a temp dir");
let dest = outdir.path().join("testkey.toml");
kr.save_file(&dest)
.await
.expect("Should write new key to file");
#[cfg(target_family = "unix")]
{
use std::os::unix::fs::PermissionsExt;
let metadata = tokio::fs::metadata(&dest).await.unwrap();
assert_eq!(
metadata.permissions().mode() & 0o00600,
0o600,
"Permissions of saved key should be 0600"
)
}
let newfile = SecretKeyFile::load_file(dest)
.await
.expect("Should load key from file");
assert_eq!(newfile.key.len(), 1);
}
}