Skip to main content

pic_continuity/
cose.rs

1/*
2 * Copyright Nitro Agility S.r.l.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      https://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17//! Generic COSE_Sign1 envelope (RFC 9052) for CBOR-serializable payloads.
18//!
19//! Crypto-agnostic: signing and verification are supplied as closures, with
20//! Ed25519 / P-256 / P-384 convenience implementations behind features.
21//!
22//! This module is deliberately self-contained — it knows nothing about
23//! Profile 0.2 — so it can be extracted into its own crate the moment a
24//! second consumer needs it.
25
26use coset::{CborSerializable, CoseSign1, CoseSign1Builder, HeaderBuilder, iana};
27use serde::{Serialize, de::DeserializeOwned};
28
29/// Generic COSE_Sign1 signed envelope wrapping a payload `T`.
30#[derive(Debug, Clone)]
31pub struct CoseSigned<T> {
32    inner: CoseSign1,
33    _marker: std::marker::PhantomData<T>,
34}
35
36/// COSE signing and verification errors.
37#[derive(Debug, thiserror::Error)]
38pub enum CoseError {
39    /// The payload could not be serialized to CBOR.
40    #[error("CBOR serialization failed: {0}")]
41    CborSerialize(String),
42
43    /// The payload bytes could not be deserialized from CBOR.
44    #[error("CBOR deserialization failed: {0}")]
45    CborDeserialize(String),
46
47    /// The COSE_Sign1 structure could not be serialized.
48    #[error("COSE serialization failed: {0}")]
49    CoseSerialize(String),
50
51    /// The bytes are not a valid COSE_Sign1 structure.
52    #[error("COSE deserialization failed: {0}")]
53    CoseDeserialize(String),
54
55    /// The signature did not verify.
56    #[error("Signature verification failed")]
57    VerificationFailed,
58
59    /// The COSE_Sign1 structure carries no payload.
60    #[error("Missing payload")]
61    MissingPayload,
62
63    /// Key material could not be parsed.
64    #[error("Invalid key: {0}")]
65    InvalidKey(String),
66
67    /// The signature bytes have the wrong length for the algorithm.
68    #[error("Invalid signature length")]
69    InvalidSignatureLength,
70
71    /// The protected-header algorithm differs from the expected one.
72    #[error("Algorithm mismatch: expected {expected}, got {got}")]
73    AlgorithmMismatch {
74        /// The algorithm the caller required.
75        expected: String,
76        /// The algorithm found in the protected header.
77        got: String,
78    },
79}
80
81/// Supported COSE signing algorithms.
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub enum SigningAlgorithm {
84    /// EdDSA with Ed25519
85    EdDSA,
86    /// ECDSA with P-256 and SHA-256
87    ES256,
88    /// ECDSA with P-384 and SHA-384
89    ES384,
90}
91
92impl std::fmt::Display for SigningAlgorithm {
93    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94        match self {
95            SigningAlgorithm::EdDSA => write!(f, "EdDSA"),
96            SigningAlgorithm::ES256 => write!(f, "ES256"),
97            SigningAlgorithm::ES384 => write!(f, "ES384"),
98        }
99    }
100}
101
102impl SigningAlgorithm {
103    fn to_iana(self) -> iana::Algorithm {
104        match self {
105            SigningAlgorithm::EdDSA => iana::Algorithm::EdDSA,
106            SigningAlgorithm::ES256 => iana::Algorithm::ES256,
107            SigningAlgorithm::ES384 => iana::Algorithm::ES384,
108        }
109    }
110}
111
112impl<T> CoseSigned<T>
113where
114    T: Serialize + DeserializeOwned,
115{
116    /// Returns the key identifier (kid) from the protected header.
117    ///
118    /// The kid can be a SPIFFE ID, DID, URL, or any resolvable identifier
119    /// that can be used to obtain the public key for verification.
120    pub fn kid(&self) -> Option<String> {
121        let kid = &self.inner.protected.header.key_id;
122        if kid.is_empty() {
123            None
124        } else {
125            String::from_utf8(kid.clone()).ok()
126        }
127    }
128
129    /// Returns the signing algorithm from the protected header.
130    pub fn algorithm(&self) -> Option<SigningAlgorithm> {
131        match self.inner.protected.header.alg {
132            Some(coset::RegisteredLabelWithPrivate::Assigned(iana::Algorithm::EdDSA)) => {
133                Some(SigningAlgorithm::EdDSA)
134            }
135            Some(coset::RegisteredLabelWithPrivate::Assigned(iana::Algorithm::ES256)) => {
136                Some(SigningAlgorithm::ES256)
137            }
138            Some(coset::RegisteredLabelWithPrivate::Assigned(iana::Algorithm::ES384)) => {
139                Some(SigningAlgorithm::ES384)
140            }
141            _ => None,
142        }
143    }
144
145    /// Serializes the signed envelope to CBOR bytes.
146    ///
147    /// These are the *exact signed artifact bytes*: every Profile 0.2 hash
148    /// (`root.pca_hash`, `predecessor.hash`) is computed over them.
149    pub fn to_bytes(&self) -> Result<Vec<u8>, CoseError> {
150        self.inner
151            .clone()
152            .to_vec()
153            .map_err(|e| CoseError::CoseSerialize(e.to_string()))
154    }
155
156    /// Deserializes a signed envelope from CBOR bytes.
157    pub fn from_bytes(bytes: &[u8]) -> Result<Self, CoseError> {
158        let inner =
159            CoseSign1::from_slice(bytes).map_err(|e| CoseError::CoseDeserialize(e.to_string()))?;
160        Ok(Self {
161            inner,
162            _marker: std::marker::PhantomData,
163        })
164    }
165
166    /// Extracts the payload without verifying the signature.
167    ///
168    /// Use only where the specification treats the artifact as untrusted
169    /// input to be parsed before validation.
170    pub fn payload_unverified(&self) -> Result<T, CoseError> {
171        let payload = self
172            .inner
173            .payload
174            .as_ref()
175            .ok_or(CoseError::MissingPayload)?;
176
177        ciborium::from_reader(payload.as_slice())
178            .map_err(|e| CoseError::CborDeserialize(e.to_string()))
179    }
180
181    /// Signs a payload using a custom signing function (crypto-agnostic).
182    ///
183    /// The closure receives the to-be-signed bytes and returns the signature.
184    pub fn sign_with<F>(
185        payload: &T,
186        kid: &str,
187        alg: SigningAlgorithm,
188        sign_fn: F,
189    ) -> Result<Self, CoseError>
190    where
191        F: FnOnce(&[u8]) -> Result<Vec<u8>, CoseError>,
192    {
193        let mut cbor_payload = Vec::new();
194        ciborium::into_writer(payload, &mut cbor_payload)
195            .map_err(|e| CoseError::CborSerialize(e.to_string()))?;
196
197        let protected = HeaderBuilder::new()
198            .algorithm(alg.to_iana())
199            .key_id(kid.as_bytes().to_vec())
200            .build();
201
202        let sign1 = CoseSign1Builder::new()
203            .protected(protected)
204            .payload(cbor_payload)
205            .try_create_signature(&[], sign_fn)?
206            .build();
207
208        Ok(Self {
209            inner: sign1,
210            _marker: std::marker::PhantomData,
211        })
212    }
213
214    /// Verifies the signature using a custom verification function.
215    ///
216    /// The closure receives `(data, signature)` and returns `Ok(())` if valid.
217    pub fn verify_with<F>(&self, verify_fn: F) -> Result<T, CoseError>
218    where
219        F: FnOnce(&[u8], &[u8]) -> Result<(), CoseError>,
220    {
221        self.inner
222            .verify_signature(&[], |sig, data| verify_fn(data, sig))?;
223
224        let payload = self
225            .inner
226            .payload
227            .as_ref()
228            .ok_or(CoseError::MissingPayload)?;
229
230        ciborium::from_reader(payload.as_slice())
231            .map_err(|e| CoseError::CborDeserialize(e.to_string()))
232    }
233
234    /// Validates that the envelope's algorithm matches the expected one.
235    pub fn check_algorithm(&self, expected: SigningAlgorithm) -> Result<(), CoseError> {
236        let actual = self.algorithm();
237        if actual != Some(expected) {
238            return Err(CoseError::AlgorithmMismatch {
239                expected: expected.to_string(),
240                got: actual
241                    .map(|a| a.to_string())
242                    .unwrap_or_else(|| "None".to_string()),
243            });
244        }
245        Ok(())
246    }
247}
248
249impl From<coset::CoseError> for CoseError {
250    fn from(e: coset::CoseError) -> Self {
251        CoseError::CoseSerialize(format!("{:?}", e))
252    }
253}
254
255#[cfg(feature = "ed25519")]
256mod ed25519_impl {
257    use super::*;
258    use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
259
260    impl<T> CoseSigned<T>
261    where
262        T: Serialize + DeserializeOwned,
263    {
264        /// Signs payload with Ed25519. Algorithm is set to EdDSA automatically.
265        pub fn sign_ed25519(
266            payload: &T,
267            kid: &str,
268            signing_key: &SigningKey,
269        ) -> Result<Self, CoseError> {
270            Self::sign_with(payload, kid, SigningAlgorithm::EdDSA, |data| {
271                let sig = signing_key.sign(data);
272                Ok(sig.to_bytes().to_vec())
273            })
274        }
275
276        /// Verifies Ed25519 signature and returns the payload.
277        pub fn verify_ed25519(&self, verifying_key: &VerifyingKey) -> Result<T, CoseError> {
278            self.check_algorithm(SigningAlgorithm::EdDSA)?;
279
280            self.verify_with(|data, sig| {
281                let signature =
282                    Signature::from_slice(sig).map_err(|_| CoseError::InvalidSignatureLength)?;
283                verifying_key
284                    .verify(data, &signature)
285                    .map_err(|_| CoseError::VerificationFailed)
286            })
287        }
288    }
289}
290
291#[cfg(feature = "p256")]
292mod p256_impl {
293    use super::*;
294    use p256::ecdsa::{
295        Signature, SigningKey, VerifyingKey, signature::Signer, signature::Verifier,
296    };
297
298    impl<T> CoseSigned<T>
299    where
300        T: Serialize + DeserializeOwned,
301    {
302        /// Signs payload with P-256. Algorithm is set to ES256 automatically.
303        pub fn sign_p256(
304            payload: &T,
305            kid: &str,
306            signing_key: &SigningKey,
307        ) -> Result<Self, CoseError> {
308            Self::sign_with(payload, kid, SigningAlgorithm::ES256, |data| {
309                let sig: Signature = signing_key.sign(data);
310                Ok(sig.to_bytes().to_vec())
311            })
312        }
313
314        /// Verifies P-256 signature and returns the payload.
315        pub fn verify_p256(&self, verifying_key: &VerifyingKey) -> Result<T, CoseError> {
316            self.check_algorithm(SigningAlgorithm::ES256)?;
317
318            self.verify_with(|data, sig| {
319                let signature =
320                    Signature::from_slice(sig).map_err(|_| CoseError::InvalidSignatureLength)?;
321                verifying_key
322                    .verify(data, &signature)
323                    .map_err(|_| CoseError::VerificationFailed)
324            })
325        }
326    }
327}
328
329#[cfg(feature = "p384")]
330mod p384_impl {
331    use super::*;
332    use p384::ecdsa::{
333        Signature, SigningKey, VerifyingKey, signature::Signer, signature::Verifier,
334    };
335
336    impl<T> CoseSigned<T>
337    where
338        T: Serialize + DeserializeOwned,
339    {
340        /// Signs payload with P-384. Algorithm is set to ES384 automatically.
341        pub fn sign_p384(
342            payload: &T,
343            kid: &str,
344            signing_key: &SigningKey,
345        ) -> Result<Self, CoseError> {
346            Self::sign_with(payload, kid, SigningAlgorithm::ES384, |data| {
347                let sig: Signature = signing_key.sign(data);
348                Ok(sig.to_bytes().to_vec())
349            })
350        }
351
352        /// Verifies P-384 signature and returns the payload.
353        pub fn verify_p384(&self, verifying_key: &VerifyingKey) -> Result<T, CoseError> {
354            self.check_algorithm(SigningAlgorithm::ES384)?;
355
356            self.verify_with(|data, sig| {
357                let signature =
358                    Signature::from_slice(sig).map_err(|_| CoseError::InvalidSignatureLength)?;
359                verifying_key
360                    .verify(data, &signature)
361                    .map_err(|_| CoseError::VerificationFailed)
362            })
363        }
364    }
365}