quantum_sign/policy/
mod.rs

1#![forbid(unsafe_code)]
2
3use serde::{Deserialize, Serialize};
4use sha2::{Digest, Sha256};
5use std::{fs, path::Path};
6
7/// Policy definition for signing operations.
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct Policy {
10    /// Default signature algorithm (e.g., "mldsa-87").
11    pub default_alg: String,
12    /// Artifact digest algorithm (e.g., "sha512").
13    pub digest_alg: String,
14    /// Allowed signature algorithm identifiers.
15    pub allow_algs: Vec<String>,
16    /// Required signature structure (m-of-n or required kids).
17    pub required_signatures: RequiredSignatures,
18    /// Whether offline verification is acceptable.
19    pub offline_ok: bool,
20    /// Enforce FIPS-only digest algorithms.
21    pub require_fips_only: bool,
22    /// Optional comments (ignored by canonical hash).
23    pub comments: Option<String>,
24}
25
26/// Canonical hash of a policy (for binding to signatures).
27pub fn canonical_hash(policy: &Policy) -> [u8; 32] {
28    // Serialize a reduced, order-stable view. Sort allow_algs and RequiredKids for order-invariant hashing.
29    #[derive(Serialize)]
30    enum CanonRequiredSignatures {
31        Quorum { m: u8, n: u8 },
32        RequiredKids { required: Vec<String> },
33    }
34
35    #[derive(Serialize)]
36    struct Canon {
37        default_alg: String,
38        digest_alg: String,
39        allow_algs: Vec<String>,
40        required_signatures: CanonRequiredSignatures,
41        offline_ok: bool,
42        require_fips_only: bool,
43    }
44    let mut allow = policy.allow_algs.clone();
45    allow.sort();
46    let canon_rs = match &policy.required_signatures {
47        RequiredSignatures::Quorum { m, n } => CanonRequiredSignatures::Quorum { m: *m, n: *n },
48        RequiredSignatures::RequiredKids { required } => {
49            let mut req = required.clone();
50            req.sort();
51            req.dedup();
52            CanonRequiredSignatures::RequiredKids { required: req }
53        }
54    };
55    let canon = Canon {
56        default_alg: policy.default_alg.clone(),
57        digest_alg: policy.digest_alg.clone(),
58        allow_algs: allow,
59        required_signatures: canon_rs,
60        offline_ok: policy.offline_ok,
61        require_fips_only: policy.require_fips_only,
62    };
63    let json = serde_json::to_vec(&canon).expect("serialize policy");
64    let mut h = Sha256::new();
65    h.update(json);
66    let mut out = [0u8; 32];
67    out.copy_from_slice(&h.finalize());
68    out
69}
70
71/// Quorum policy (m-of-n) or explicit required signers.
72#[derive(Debug, Clone, Serialize, Deserialize)]
73#[serde(untagged)]
74pub enum RequiredSignatures {
75    Quorum { m: u8, n: u8 },
76    RequiredKids { required: Vec<String> },
77}
78
79
80/// Error while loading or parsing a policy file.
81#[derive(Debug)]
82pub enum Error {
83    Io(std::io::Error),
84    Parse(String),
85    Unsupported(&'static str),
86}
87
88impl From<std::io::Error> for Error {
89    fn from(err: std::io::Error) -> Self {
90        Error::Io(err)
91    }
92}
93
94/// Policy validation failures (enforced at runtime).
95#[derive(Debug)]
96pub enum ValidationError {
97    FipsRequired,
98    InvalidQuorum {
99        m: u8,
100        n: u8,
101    },
102    QuorumUnsatisfied {
103        required_m: u8,
104        total_n: u8,
105        collected: usize,
106    },
107    Level5Requirement(String),
108}
109
110/// Explicit serialization formats.
111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
112pub enum Format {
113    Json,
114    Yaml,
115}
116
117/// Deserialize policy from a string using the provided (or inferred) format.
118pub fn load_policy_str(contents: &str, fmt: Option<Format>) -> Result<Policy, Error> {
119    match fmt.unwrap_or(Format::Json) {
120        Format::Json => {
121            #[cfg(feature = "json")]
122            {
123                serde_json::from_str::<Policy>(contents).map_err(|e| Error::Parse(e.to_string()))
124            }
125            #[cfg(not(feature = "json"))]
126            {
127                Err(Error::Unsupported("json feature disabled"))
128            }
129        }
130        Format::Yaml => {
131            #[cfg(feature = "yaml")]
132            {
133                serde_yaml::from_str::<Policy>(contents).map_err(|e| Error::Parse(e.to_string()))
134            }
135            #[cfg(not(feature = "yaml"))]
136            {
137                Err(Error::Unsupported("yaml feature disabled"))
138            }
139        }
140    }
141}
142
143/// Load policy from disk, inferring format by extension (`.json`, `.yaml`, `.yml`).
144pub fn load_policy_file(path: &Path) -> Result<Policy, Error> {
145    let data = fs::read_to_string(path)?;
146    let fmt = match path.extension().and_then(|s| s.to_str()) {
147        Some("yaml") | Some("yml") => Some(Format::Yaml),
148        _ => Some(Format::Json),
149    };
150    load_policy_str(&data, fmt)
151}
152
153// reserved for potential future policy-level algorithm gating