#![forbid(unsafe_code)]
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::{fs, path::Path};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Policy {
pub default_alg: String,
pub digest_alg: String,
pub allow_algs: Vec<String>,
pub required_signatures: RequiredSignatures,
pub offline_ok: bool,
pub require_fips_only: bool,
pub comments: Option<String>,
}
pub fn canonical_hash(policy: &Policy) -> [u8; 32] {
#[derive(Serialize)]
enum CanonRequiredSignatures {
Quorum { m: u8, n: u8 },
RequiredKids { required: Vec<String> },
}
#[derive(Serialize)]
struct Canon {
default_alg: String,
digest_alg: String,
allow_algs: Vec<String>,
required_signatures: CanonRequiredSignatures,
offline_ok: bool,
require_fips_only: bool,
}
let mut allow = policy.allow_algs.clone();
allow.sort();
let canon_rs = match &policy.required_signatures {
RequiredSignatures::Quorum { m, n } => CanonRequiredSignatures::Quorum { m: *m, n: *n },
RequiredSignatures::RequiredKids { required } => {
let mut req = required.clone();
req.sort();
req.dedup();
CanonRequiredSignatures::RequiredKids { required: req }
}
};
let canon = Canon {
default_alg: policy.default_alg.clone(),
digest_alg: policy.digest_alg.clone(),
allow_algs: allow,
required_signatures: canon_rs,
offline_ok: policy.offline_ok,
require_fips_only: policy.require_fips_only,
};
let json = serde_json::to_vec(&canon).expect("serialize policy");
let mut h = Sha256::new();
h.update(json);
let mut out = [0u8; 32];
out.copy_from_slice(&h.finalize());
out
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum RequiredSignatures {
Quorum { m: u8, n: u8 },
RequiredKids { required: Vec<String> },
}
#[derive(Debug)]
pub enum Error {
Io(std::io::Error),
Parse(String),
Unsupported(&'static str),
}
impl From<std::io::Error> for Error {
fn from(err: std::io::Error) -> Self {
Error::Io(err)
}
}
#[derive(Debug)]
pub enum ValidationError {
FipsRequired,
InvalidQuorum {
m: u8,
n: u8,
},
QuorumUnsatisfied {
required_m: u8,
total_n: u8,
collected: usize,
},
Level5Requirement(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Format {
Json,
Yaml,
}
pub fn load_policy_str(contents: &str, fmt: Option<Format>) -> Result<Policy, Error> {
match fmt.unwrap_or(Format::Json) {
Format::Json => {
#[cfg(feature = "json")]
{
serde_json::from_str::<Policy>(contents).map_err(|e| Error::Parse(e.to_string()))
}
#[cfg(not(feature = "json"))]
{
Err(Error::Unsupported("json feature disabled"))
}
}
Format::Yaml => {
#[cfg(feature = "yaml")]
{
serde_yaml::from_str::<Policy>(contents).map_err(|e| Error::Parse(e.to_string()))
}
#[cfg(not(feature = "yaml"))]
{
Err(Error::Unsupported("yaml feature disabled"))
}
}
}
}
pub fn load_policy_file(path: &Path) -> Result<Policy, Error> {
let data = fs::read_to_string(path)?;
let fmt = match path.extension().and_then(|s| s.to_str()) {
Some("yaml") | Some("yml") => Some(Format::Yaml),
_ => Some(Format::Json),
};
load_policy_str(&data, fmt)
}