Skip to main content

prikk_object/
signature.rs

1//! Signature metadata and signed-byte construction.
2
3use prikk_error::{PrikkError, Result};
4use std::cmp::Ordering;
5
6use crate::{CanonicalEncode, CanonicalWriter, ObjectId, ObjectType};
7
8/// Signature domain string.
9pub const SIGNATURE_DOMAIN: &[u8] = b"prikk.sig.v1";
10
11/// Maximum byte length for a role-bound signature key id.
12pub const SIGNATURE_KEY_ID_MAX_LEN: usize = 128;
13
14/// Required byte length for a version-1 Ed25519 signature.
15pub const ED25519_SIGNATURE_LEN: usize = 64;
16
17/// Supported signature algorithms.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
19#[repr(u16)]
20pub enum SignatureAlgorithm {
21    /// Ed25519. The only v1 signing and verification algorithm.
22    Ed25519 = 1,
23}
24
25impl SignatureAlgorithm {
26    /// Stable u16 code.
27    #[must_use]
28    pub const fn code(self) -> u16 {
29        self as u16
30    }
31
32    /// Parse a stable u16 code.
33    pub fn from_code(code: u16) -> Result<Self> {
34        match code {
35            1 => Ok(Self::Ed25519),
36            other => Err(PrikkError::InvalidSignature(format!(
37                "unknown signature algorithm code: {other}"
38            ))),
39        }
40    }
41}
42
43/// Role bound into signature preimages.
44#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
45#[repr(u16)]
46pub enum SignerRole {
47    /// Author of a patch.
48    Author = 1,
49    /// Maintainer publishing/sealing a block or ref state.
50    Maintainer = 2,
51    /// Continuous-integration actor.
52    Ci = 3,
53    /// Audit plugin or audit policy signer.
54    Audit = 4,
55}
56
57impl SignerRole {
58    /// Stable u16 code.
59    #[must_use]
60    pub const fn code(self) -> u16 {
61        self as u16
62    }
63
64    /// Parse a stable u16 code.
65    pub fn from_code(code: u16) -> Result<Self> {
66        match code {
67            1 => Ok(Self::Author),
68            2 => Ok(Self::Maintainer),
69            3 => Ok(Self::Ci),
70            4 => Ok(Self::Audit),
71            other => Err(PrikkError::InvalidSignature(format!(
72                "unknown signer role code: {other}"
73            ))),
74        }
75    }
76}
77
78/// Signature attached to an object envelope.
79///
80/// Standalone canonical encoding is structural field encoding only. Strict semantic admission is an
81/// [`crate::ObjectEnvelope`] responsibility.
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub struct Signature {
84    /// Signature algorithm.
85    pub algorithm: SignatureAlgorithm,
86    /// Key identifier.
87    pub key_id: String,
88    /// Raw signature bytes.
89    pub signature_bytes: Vec<u8>,
90    /// Advisory signing timestamp. Do not use as authoritative audit time.
91    pub created_at: u64,
92    /// Signer role.
93    pub signer_role: SignerRole,
94}
95
96impl Signature {
97    /// Validate a key id used in role-bound signature preimages.
98    pub fn validate_key_id(key_id: &str) -> Result<()> {
99        if key_id.is_empty() {
100            return Err(PrikkError::InvalidSignature(
101                "signature key_id must not be empty".to_string(),
102            ));
103        }
104        if key_id.len() > SIGNATURE_KEY_ID_MAX_LEN {
105            return Err(PrikkError::InvalidSignature(format!(
106                "signature key_id must be at most {SIGNATURE_KEY_ID_MAX_LEN} bytes"
107            )));
108        }
109        if !key_id
110            .bytes()
111            .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-' || byte == b'_')
112        {
113            return Err(PrikkError::InvalidSignature(
114                "signature key_id must contain only ASCII letters, digits, '-' or '_'".to_string(),
115            ));
116        }
117        Ok(())
118    }
119
120    /// Build the bytes to sign for an object ID and role.
121    pub fn signed_bytes(
122        algorithm: SignatureAlgorithm,
123        object_type: ObjectType,
124        object_id: ObjectId,
125        signer_role: SignerRole,
126        key_id: &str,
127    ) -> Result<Vec<u8>> {
128        Self::validate_key_id(key_id)?;
129        let key_id_len = u16::try_from(key_id.len()).map_err(|_| {
130            PrikkError::InvalidSignature(
131                "signature key_id is too long for the signature preimage length field".to_string(),
132            )
133        })?;
134        let mut out =
135            Vec::with_capacity(SIGNATURE_DOMAIN.len() + 2 + 2 + 32 + 2 + 2 + key_id.len());
136        out.extend_from_slice(SIGNATURE_DOMAIN);
137        out.extend_from_slice(&algorithm.code().to_be_bytes());
138        out.extend_from_slice(&object_type.code().to_be_bytes());
139        out.extend_from_slice(object_id.as_bytes());
140        out.extend_from_slice(&signer_role.code().to_be_bytes());
141        out.extend_from_slice(&key_id_len.to_be_bytes());
142        out.extend_from_slice(key_id.as_bytes());
143        Ok(out)
144    }
145
146    /// Validate local structural constraints.
147    pub fn validate(&self) -> Result<()> {
148        Self::validate_key_id(&self.key_id)?;
149        if self.signature_bytes.is_empty() {
150            return Err(PrikkError::InvalidSignature(
151                "signature bytes must not be empty".to_string(),
152            ));
153        }
154        Ok(())
155    }
156
157    /// Validate the registered algorithm's syntactic signature shape.
158    pub fn validate_shape(&self) -> Result<()> {
159        match self.algorithm {
160            SignatureAlgorithm::Ed25519 if self.signature_bytes.len() == ED25519_SIGNATURE_LEN => {
161                Ok(())
162            }
163            SignatureAlgorithm::Ed25519 => Err(PrikkError::InvalidSignature(format!(
164                "Ed25519 signature must be {ED25519_SIGNATURE_LEN} bytes, got {}",
165                self.signature_bytes.len()
166            ))),
167        }
168    }
169
170    /// Compare signatures by the canonical envelope tuple.
171    #[must_use]
172    pub fn canonical_cmp(&self, other: &Self) -> Ordering {
173        self.key_id
174            .as_bytes()
175            .cmp(other.key_id.as_bytes())
176            .then_with(|| self.signer_role.code().cmp(&other.signer_role.code()))
177            .then_with(|| self.algorithm.code().cmp(&other.algorithm.code()))
178            .then_with(|| self.signature_bytes.cmp(&other.signature_bytes))
179    }
180}
181
182impl CanonicalEncode for Signature {
183    fn encode_canonical(&self, writer: &mut CanonicalWriter) -> Result<()> {
184        writer.field_u32(1, self.algorithm.code() as u32)?;
185        writer.field_string(2, &self.key_id)?;
186        writer.field_bytes(3, &self.signature_bytes)?;
187        writer.field_u64(4, self.created_at)?;
188        writer.field_u32(5, self.signer_role.code() as u32)?;
189        Ok(())
190    }
191}