Skip to main content

basil_cose/
types.rs

1// SPDX-FileCopyrightText: 2026 OpenBasil Contributors
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! Validated identifier newtypes and byte-carrying parameter structs.
6//!
7//! Everything a caller passes is a named struct or a closed enum: no bare
8//! `&[u8]`/`&str` in public positions. Constructors validate; decoded wire
9//! values pass through the same constructors, so an in-range value is an
10//! invariant of the type.
11
12use alloc::string::String;
13use alloc::vec::Vec;
14
15use crate::error::ProfileError;
16
17/// COSE `kid` (bstr, 1..=128 bytes). Basil catalog names are UTF-8; other
18/// consumers may use raw byte ids.
19#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
20pub struct KeyId(Vec<u8>);
21
22impl KeyId {
23    /// Build a key id from the UTF-8 bytes of a catalog name.
24    ///
25    /// # Errors
26    /// [`ProfileError::KeyIdLength`] if the name is empty or longer than 128
27    /// bytes.
28    pub fn from_text(id: &str) -> Result<Self, ProfileError> {
29        Self::from_bytes(id.as_bytes().to_vec())
30    }
31
32    /// Build a key id from raw bytes.
33    ///
34    /// # Errors
35    /// [`ProfileError::KeyIdLength`] if not 1..=128 bytes.
36    pub fn from_bytes(id: Vec<u8>) -> Result<Self, ProfileError> {
37        if id.is_empty() || id.len() > 128 {
38            return Err(ProfileError::KeyIdLength { actual: id.len() });
39        }
40        Ok(Self(id))
41    }
42
43    /// The catalog name, when the id is valid UTF-8.
44    #[must_use]
45    pub fn as_catalog_name(&self) -> Option<&str> {
46        core::str::from_utf8(&self.0).ok()
47    }
48
49    /// The raw id bytes.
50    #[must_use]
51    pub fn as_bytes(&self) -> &[u8] {
52        &self.0
53    }
54}
55
56/// CWT `cti` (bstr, 1..=64 bytes, sender-unique inside the replay window).
57#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
58pub struct MessageId(Vec<u8>);
59
60impl MessageId {
61    /// Build a message id from raw bytes.
62    ///
63    /// # Errors
64    /// [`ProfileError::MessageIdLength`] if not 1..=64 bytes.
65    pub fn from_bytes(id: Vec<u8>) -> Result<Self, ProfileError> {
66        if id.is_empty() || id.len() > 64 {
67            return Err(ProfileError::MessageIdLength { actual: id.len() });
68        }
69        Ok(Self(id))
70    }
71
72    /// The raw id bytes.
73    #[must_use]
74    pub fn as_bytes(&self) -> &[u8] {
75        &self.0
76    }
77}
78
79/// CWT `iss`/`aud` subject (tstr, non-empty).
80#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
81pub struct Subject(String);
82
83impl Subject {
84    /// Build a subject string.
85    ///
86    /// # Errors
87    /// [`ProfileError::EmptySubject`] if the string is empty.
88    pub fn new(subject: String) -> Result<Self, ProfileError> {
89        if subject.is_empty() {
90            return Err(ProfileError::EmptySubject);
91        }
92        Ok(Self(subject))
93    }
94
95    /// The subject string.
96    #[must_use]
97    pub fn as_str(&self) -> &str {
98        &self.0
99    }
100}
101
102/// The `-70005` response subject (tstr, non-empty).
103#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
104pub struct ResponseSubject(String);
105
106impl ResponseSubject {
107    /// Build a response subject string.
108    ///
109    /// # Errors
110    /// [`ProfileError::EmptySubject`] if the string is empty.
111    pub fn new(subject: String) -> Result<Self, ProfileError> {
112        if subject.is_empty() {
113            return Err(ProfileError::EmptySubject);
114        }
115        Ok(Self(subject))
116    }
117
118    /// The subject string.
119    #[must_use]
120    pub fn as_str(&self) -> &str {
121        &self.0
122    }
123}
124
125/// Content type (COSE header 3, tstr media type of `type/subtype` form).
126///
127/// Registry values for basil live in `basil-proto` (for example
128/// `application/basil.sign-request`); clients can register their own strings. The
129/// tstr content type is a media type per RFC 9052, so the profile requires
130/// the `type/subtype` shape with no surrounding whitespace.
131#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
132pub struct ContentType(String);
133
134impl ContentType {
135    /// Build a content type.
136    ///
137    /// # Errors
138    /// [`ProfileError::ContentTypeForm`] unless the string is non-empty,
139    /// contains exactly one `/`, and has no leading/trailing whitespace.
140    pub fn new(content_type: String) -> Result<Self, ProfileError> {
141        if content_type.is_empty()
142            || content_type.trim() != content_type
143            || content_type.matches('/').count() != 1
144        {
145            return Err(ProfileError::ContentTypeForm);
146        }
147        Ok(Self(content_type))
148    }
149
150    /// The content-type string.
151    #[must_use]
152    pub fn as_str(&self) -> &str {
153        &self.0
154    }
155}
156
157/// Seconds since the Unix epoch (CWT `iat`/`exp`).
158#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
159pub struct UnixTime(pub i64);
160
161/// Caller-supplied `external_aad` for exactly one COSE layer. Empty is the
162/// explicit default, not an implicit one.
163#[derive(Debug, Clone, PartialEq, Eq)]
164pub struct ExternalAad(Vec<u8>);
165
166impl ExternalAad {
167    /// No external AAD for this layer.
168    #[must_use]
169    pub const fn empty() -> Self {
170        Self(Vec::new())
171    }
172
173    /// External AAD from protocol-bound bytes.
174    #[must_use]
175    pub const fn from_bytes(bytes: Vec<u8>) -> Self {
176        Self(bytes)
177    }
178
179    /// The AAD bytes.
180    #[must_use]
181    pub fn as_bytes(&self) -> &[u8] {
182        &self.0
183    }
184}
185
186/// Per-layer AAD for the sealed (two-layer) construction.
187#[derive(Debug, Clone, PartialEq, Eq)]
188pub struct SealedAad {
189    /// Fed to the `Sig_structure` of the outer `COSE_Sign1`.
190    pub signature: ExternalAad,
191    /// Fed to the `Enc_structure` of the embedded `COSE_Encrypt`.
192    pub encryption: ExternalAad,
193}
194
195impl SealedAad {
196    /// Empty AAD on both layers (the basil invocation default).
197    #[must_use]
198    pub const fn empty() -> Self {
199        Self {
200            signature: ExternalAad::empty(),
201            encryption: ExternalAad::empty(),
202        }
203    }
204}
205
206/// A raw signature over `Sig_structure` bytes.
207#[derive(Debug, Clone, PartialEq, Eq)]
208pub struct Signature(Vec<u8>);
209
210impl Signature {
211    /// Wrap signature bytes.
212    ///
213    /// # Errors
214    /// [`ProfileError::EmptySignature`] if the bytes are empty.
215    pub fn from_bytes(bytes: Vec<u8>) -> Result<Self, ProfileError> {
216        if bytes.is_empty() {
217            return Err(ProfileError::EmptySignature);
218        }
219        Ok(Self(bytes))
220    }
221
222    /// The raw signature bytes.
223    #[must_use]
224    pub fn as_bytes(&self) -> &[u8] {
225        &self.0
226    }
227}
228
229/// Complete tagged COSE bytes: the output of every build entry point.
230#[derive(Debug, Clone, PartialEq, Eq)]
231pub struct CoseBytes(Vec<u8>);
232
233impl CoseBytes {
234    pub(crate) const fn new(bytes: Vec<u8>) -> Self {
235        Self(bytes)
236    }
237
238    /// The complete tagged COSE bytes.
239    #[must_use]
240    pub fn as_bytes(&self) -> &[u8] {
241        &self.0
242    }
243
244    /// Consume into the raw byte vector.
245    #[must_use]
246    pub fn into_vec(self) -> Vec<u8> {
247        self.0
248    }
249}
250
251impl AsRef<[u8]> for CoseBytes {
252    fn as_ref(&self) -> &[u8] {
253        &self.0
254    }
255}