Skip to main content

prikk_object/
signature.rs

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