pic_pca/
coset.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//! COSE_Sign1 signing for PIC payloads.
18//!
19//! Provides a generic `CoseSigned<T>` wrapper for signing and verifying
20//! any CBOR-serializable payload using COSE_Sign1 envelope (RFC 9052).
21
22use coset::{iana, CborSerializable, CoseSign1, CoseSign1Builder, HeaderBuilder};
23use serde::{de::DeserializeOwned, Serialize};
24
25// ============================================================================
26// Core Types
27// ============================================================================
28
29/// Generic COSE_Sign1 signed envelope.
30///
31/// Wraps any serializable payload `T` with a COSE_Sign1 signature.
32#[derive(Debug, Clone)]
33pub struct CoseSigned<T> {
34    inner: CoseSign1,
35    _marker: std::marker::PhantomData<T>,
36}
37
38/// COSE signing and verification errors.
39#[derive(Debug, thiserror::Error)]
40pub enum CoseError {
41    #[error("CBOR serialization failed: {0}")]
42    CborSerialize(String),
43
44    #[error("CBOR deserialization failed: {0}")]
45    CborDeserialize(String),
46
47    #[error("COSE serialization failed: {0}")]
48    CoseSerialize(String),
49
50    #[error("COSE deserialization failed: {0}")]
51    CoseDeserialize(String),
52
53    #[error("Signature verification failed")]
54    VerificationFailed,
55
56    #[error("Missing payload")]
57    MissingPayload,
58
59    #[error("Invalid key: {0}")]
60    InvalidKey(String),
61
62    #[error("Invalid signature length")]
63    InvalidSignatureLength,
64
65    #[error("Algorithm mismatch: expected {expected}, got {got}")]
66    AlgorithmMismatch { expected: String, got: String },
67}
68
69/// Supported COSE signing algorithms.
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum SigningAlgorithm {
72    /// EdDSA with Ed25519
73    EdDSA,
74    /// ECDSA with P-256 and SHA-256
75    ES256,
76    /// ECDSA with P-384 and SHA-384
77    ES384,
78}
79
80impl std::fmt::Display for SigningAlgorithm {
81    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82        match self {
83            SigningAlgorithm::EdDSA => write!(f, "EdDSA"),
84            SigningAlgorithm::ES256 => write!(f, "ES256"),
85            SigningAlgorithm::ES384 => write!(f, "ES384"),
86        }
87    }
88}
89
90impl SigningAlgorithm {
91    fn to_iana(&self) -> iana::Algorithm {
92        match self {
93            SigningAlgorithm::EdDSA => iana::Algorithm::EdDSA,
94            SigningAlgorithm::ES256 => iana::Algorithm::ES256,
95            SigningAlgorithm::ES384 => iana::Algorithm::ES384,
96        }
97    }
98}
99
100// ============================================================================
101// Core Implementation
102// ============================================================================
103
104impl<T> CoseSigned<T>
105where
106    T: Serialize + DeserializeOwned,
107{
108    /// Returns the issuer (kid) from the protected header.
109    pub fn issuer(&self) -> Option<String> {
110        let kid = &self.inner.protected.header.key_id;
111        if kid.is_empty() {
112            None
113        } else {
114            String::from_utf8(kid.clone()).ok()
115        }
116    }
117
118    /// Returns the signing algorithm from the protected header.
119    pub fn algorithm(&self) -> Option<SigningAlgorithm> {
120        match self.inner.protected.header.alg {
121            Some(coset::RegisteredLabelWithPrivate::Assigned(iana::Algorithm::EdDSA)) => {
122                Some(SigningAlgorithm::EdDSA)
123            }
124            Some(coset::RegisteredLabelWithPrivate::Assigned(iana::Algorithm::ES256)) => {
125                Some(SigningAlgorithm::ES256)
126            }
127            Some(coset::RegisteredLabelWithPrivate::Assigned(iana::Algorithm::ES384)) => {
128                Some(SigningAlgorithm::ES384)
129            }
130            _ => None,
131        }
132    }
133
134    /// Serializes the signed envelope to CBOR bytes.
135    pub fn to_bytes(&self) -> Result<Vec<u8>, CoseError> {
136        self.inner
137            .clone()
138            .to_vec()
139            .map_err(|e| CoseError::CoseSerialize(e.to_string()))
140    }
141
142    /// Deserializes a signed envelope from CBOR bytes.
143    pub fn from_bytes(bytes: &[u8]) -> Result<Self, CoseError> {
144        let inner = CoseSign1::from_slice(bytes)
145            .map_err(|e| CoseError::CoseDeserialize(e.to_string()))?;
146        Ok(Self {
147            inner,
148            _marker: std::marker::PhantomData,
149        })
150    }
151
152    /// Extracts the payload without verifying the signature.
153    ///
154    /// Use with caution: this bypasses signature verification.
155    pub fn payload_unverified(&self) -> Result<T, CoseError> {
156        let payload = self
157            .inner
158            .payload
159            .as_ref()
160            .ok_or(CoseError::MissingPayload)?;
161
162        ciborium::from_reader(payload.as_slice())
163            .map_err(|e| CoseError::CborDeserialize(e.to_string()))
164    }
165
166    /// Signs a payload using a custom signing function (crypto-agnostic).
167    ///
168    /// The closure receives the to-be-signed bytes and returns the signature.
169    pub fn sign_with<F>(
170        payload: &T,
171        issuer: &str,
172        alg: SigningAlgorithm,
173        sign_fn: F,
174    ) -> Result<Self, CoseError>
175    where
176        F: FnOnce(&[u8]) -> Result<Vec<u8>, CoseError>,
177    {
178        let mut cbor_payload = Vec::new();
179        ciborium::into_writer(payload, &mut cbor_payload)
180            .map_err(|e| CoseError::CborSerialize(e.to_string()))?;
181
182        let protected = HeaderBuilder::new()
183            .algorithm(alg.to_iana())
184            .key_id(issuer.as_bytes().to_vec())
185            .build();
186
187        let sign1 = CoseSign1Builder::new()
188            .protected(protected)
189            .payload(cbor_payload)
190            .try_create_signature(&[], sign_fn)?
191            .build();
192
193        Ok(Self {
194            inner: sign1,
195            _marker: std::marker::PhantomData,
196        })
197    }
198
199    /// Verifies the signature using a custom verification function (crypto-agnostic).
200    ///
201    /// The closure receives `(data, signature)` and returns `Ok(())` if valid.
202    pub fn verify_with<F>(&self, verify_fn: F) -> Result<T, CoseError>
203    where
204        F: FnOnce(&[u8], &[u8]) -> Result<(), CoseError>,
205    {
206        // coset passes (sig, data), we reorder to (data, sig) for consistency
207        self.inner
208            .verify_signature(&[], |sig, data| verify_fn(data, sig))?;
209
210        let payload = self
211            .inner
212            .payload
213            .as_ref()
214            .ok_or(CoseError::MissingPayload)?;
215
216        ciborium::from_reader(payload.as_slice())
217            .map_err(|e| CoseError::CborDeserialize(e.to_string()))
218    }
219
220    /// Validates that the envelope's algorithm matches the expected one.
221    fn check_algorithm(&self, expected: SigningAlgorithm) -> Result<(), CoseError> {
222        let actual = self.algorithm();
223        if actual != Some(expected) {
224            return Err(CoseError::AlgorithmMismatch {
225                expected: expected.to_string(),
226                got: actual
227                    .map(|a| a.to_string())
228                    .unwrap_or_else(|| "None".to_string()),
229            });
230        }
231        Ok(())
232    }
233}
234
235impl From<coset::CoseError> for CoseError {
236    fn from(e: coset::CoseError) -> Self {
237        CoseError::CoseSerialize(format!("{:?}", e))
238    }
239}
240
241// ============================================================================
242// Ed25519 Implementation
243// ============================================================================
244
245#[cfg(feature = "ed25519")]
246mod ed25519_impl {
247    use super::*;
248    use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
249
250    impl<T> CoseSigned<T>
251    where
252        T: Serialize + DeserializeOwned,
253    {
254        /// Signs payload with Ed25519. Algorithm is set to EdDSA automatically.
255        pub fn sign_ed25519(
256            payload: &T,
257            issuer: &str,
258            signing_key: &SigningKey,
259        ) -> Result<Self, CoseError> {
260            Self::sign_with(payload, issuer, SigningAlgorithm::EdDSA, |data| {
261                let sig = signing_key.sign(data);
262                Ok(sig.to_bytes().to_vec())
263            })
264        }
265
266        /// Verifies payload with Ed25519. Returns error if algorithm is not EdDSA.
267        pub fn verify_ed25519(&self, verifying_key: &VerifyingKey) -> Result<T, CoseError> {
268            self.check_algorithm(SigningAlgorithm::EdDSA)?;
269
270            self.verify_with(|data, sig| {
271                let signature =
272                    Signature::from_slice(sig).map_err(|_| CoseError::InvalidSignatureLength)?;
273
274                verifying_key
275                    .verify(data, &signature)
276                    .map_err(|_| CoseError::VerificationFailed)
277            })
278        }
279    }
280}
281
282// ============================================================================
283// P-256 (ES256) Implementation
284// ============================================================================
285
286#[cfg(feature = "p256")]
287mod p256_impl {
288    use super::*;
289    use p256::ecdsa::{signature::Signer, signature::Verifier, Signature, SigningKey, VerifyingKey};
290
291    impl<T> CoseSigned<T>
292    where
293        T: Serialize + DeserializeOwned,
294    {
295        /// Signs payload with P-256. Algorithm is set to ES256 automatically.
296        pub fn sign_p256(
297            payload: &T,
298            issuer: &str,
299            signing_key: &SigningKey,
300        ) -> Result<Self, CoseError> {
301            Self::sign_with(payload, issuer, SigningAlgorithm::ES256, |data| {
302                let sig: Signature = signing_key.sign(data);
303                Ok(sig.to_bytes().to_vec())
304            })
305        }
306
307        /// Verifies payload with P-256. Returns error if algorithm is not ES256.
308        pub fn verify_p256(&self, verifying_key: &VerifyingKey) -> Result<T, CoseError> {
309            self.check_algorithm(SigningAlgorithm::ES256)?;
310
311            self.verify_with(|data, sig| {
312                let signature =
313                    Signature::from_slice(sig).map_err(|_| CoseError::InvalidSignatureLength)?;
314
315                verifying_key
316                    .verify(data, &signature)
317                    .map_err(|_| CoseError::VerificationFailed)
318            })
319        }
320    }
321}
322
323// ============================================================================
324// P-384 (ES384) Implementation
325// ============================================================================
326
327#[cfg(feature = "p384")]
328mod p384_impl {
329    use super::*;
330    use p384::ecdsa::{signature::Signer, signature::Verifier, Signature, SigningKey, VerifyingKey};
331
332    impl<T> CoseSigned<T>
333    where
334        T: Serialize + DeserializeOwned,
335    {
336        /// Signs payload with P-384. Algorithm is set to ES384 automatically.
337        pub fn sign_p384(
338            payload: &T,
339            issuer: &str,
340            signing_key: &SigningKey,
341        ) -> Result<Self, CoseError> {
342            Self::sign_with(payload, issuer, SigningAlgorithm::ES384, |data| {
343                let sig: Signature = signing_key.sign(data);
344                Ok(sig.to_bytes().to_vec())
345            })
346        }
347
348        /// Verifies payload with P-384. Returns error if algorithm is not ES384.
349        pub fn verify_p384(&self, verifying_key: &VerifyingKey) -> Result<T, CoseError> {
350            self.check_algorithm(SigningAlgorithm::ES384)?;
351
352            self.verify_with(|data, sig| {
353                let signature =
354                    Signature::from_slice(sig).map_err(|_| CoseError::InvalidSignatureLength)?;
355
356                verifying_key
357                    .verify(data, &signature)
358                    .map_err(|_| CoseError::VerificationFailed)
359            })
360        }
361    }
362}
363
364// ============================================================================
365// Type Aliases
366// ============================================================================
367
368use crate::pca::PcaPayload;
369use crate::poc::PocPayload;
370
371/// COSE-signed PCA payload.
372pub type SignedPca = CoseSigned<PcaPayload>;
373
374/// COSE-signed PoC payload.
375pub type SignedPoc = CoseSigned<PocPayload>;
376
377// ============================================================================
378// Tests
379// ============================================================================
380
381#[cfg(test)]
382mod tests {
383    use super::*;
384    use crate::pca::{Executor, ExecutorBinding};
385
386    fn sample_pca() -> PcaPayload {
387        PcaPayload {
388            hop: "gateway".into(),
389            p_0: "https://idp.example.com/users/alice".into(),
390            ops: vec!["read:/user/*".into()],
391            executor: Executor {
392                binding: ExecutorBinding::new().with("org", "acme"),
393            },
394            provenance: None,
395            constraints: None,
396        }
397    }
398
399    #[test]
400    fn test_sign_with_and_verify_with() {
401        let pca = sample_pca();
402
403        let signed: SignedPca = CoseSigned::sign_with(
404            &pca,
405            "https://cat.example.com",
406            SigningAlgorithm::EdDSA,
407            |_data| Ok(vec![0xAB; 64]),
408        )
409        .unwrap();
410
411        assert_eq!(signed.issuer(), Some("https://cat.example.com".into()));
412        assert_eq!(signed.algorithm(), Some(SigningAlgorithm::EdDSA));
413
414        let verified = signed.verify_with(|_data, _sig| Ok(())).unwrap();
415
416        assert_eq!(verified.hop, pca.hop);
417        assert_eq!(verified.p_0, pca.p_0);
418    }
419
420    #[test]
421    fn test_roundtrip_bytes() {
422        let pca = sample_pca();
423
424        let signed: SignedPca = CoseSigned::sign_with(
425            &pca,
426            "issuer-1",
427            SigningAlgorithm::ES256,
428            |_| Ok(vec![0xCD; 64]),
429        )
430        .unwrap();
431
432        let bytes = signed.to_bytes().unwrap();
433        let restored: SignedPca = CoseSigned::from_bytes(&bytes).unwrap();
434
435        assert_eq!(restored.issuer(), Some("issuer-1".into()));
436    }
437
438    #[test]
439    fn test_payload_unverified() {
440        let pca = sample_pca();
441
442        let signed: SignedPca = CoseSigned::sign_with(
443            &pca,
444            "issuer",
445            SigningAlgorithm::EdDSA,
446            |_| Ok(vec![0x00; 64]),
447        )
448        .unwrap();
449
450        let extracted = signed.payload_unverified().unwrap();
451        assert_eq!(extracted.hop, "gateway");
452    }
453
454    #[test]
455    #[cfg(feature = "ed25519")]
456    fn test_ed25519_sign_verify() {
457        use ed25519_dalek::SigningKey;
458        use rand::rngs::OsRng;
459
460        let pca = sample_pca();
461
462        let signing_key = SigningKey::generate(&mut OsRng);
463        let verifying_key = signing_key.verifying_key();
464
465        let signed: SignedPca =
466            CoseSigned::sign_ed25519(&pca, "ed25519-issuer", &signing_key).unwrap();
467
468        assert_eq!(signed.algorithm(), Some(SigningAlgorithm::EdDSA));
469
470        let verified = signed.verify_ed25519(&verifying_key).unwrap();
471
472        assert_eq!(verified.hop, pca.hop);
473        assert_eq!(verified.p_0, pca.p_0);
474    }
475
476    #[test]
477    #[cfg(feature = "ed25519")]
478    fn test_ed25519_wrong_key_fails() {
479        use ed25519_dalek::SigningKey;
480        use rand::rngs::OsRng;
481
482        let pca = sample_pca();
483
484        let signing_key = SigningKey::generate(&mut OsRng);
485        let wrong_verifying_key = SigningKey::generate(&mut OsRng).verifying_key();
486
487        let signed: SignedPca = CoseSigned::sign_ed25519(&pca, "issuer", &signing_key).unwrap();
488
489        let result = signed.verify_ed25519(&wrong_verifying_key);
490        assert!(result.is_err());
491    }
492
493    #[test]
494    #[cfg(feature = "ed25519")]
495    fn test_algorithm_mismatch() {
496        use ed25519_dalek::SigningKey;
497        use rand::rngs::OsRng;
498
499        let pca = sample_pca();
500
501        let signed: SignedPca = CoseSigned::sign_with(
502            &pca,
503            "issuer",
504            SigningAlgorithm::ES256,
505            |_| Ok(vec![0x00; 64]),
506        )
507        .unwrap();
508
509        let verifying_key = SigningKey::generate(&mut OsRng).verifying_key();
510        let result = signed.verify_ed25519(&verifying_key);
511
512        assert!(matches!(result, Err(CoseError::AlgorithmMismatch { .. })));
513    }
514
515    #[test]
516    #[cfg(feature = "p256")]
517    fn test_p256_sign_verify() {
518        use p256::ecdsa::SigningKey;
519        use rand::rngs::OsRng;
520
521        let pca = sample_pca();
522
523        let signing_key = SigningKey::random(&mut OsRng);
524        let verifying_key = signing_key.verifying_key();
525
526        let signed: SignedPca =
527            CoseSigned::sign_p256(&pca, "p256-issuer", &signing_key).unwrap();
528
529        assert_eq!(signed.algorithm(), Some(SigningAlgorithm::ES256));
530
531        let verified = signed.verify_p256(verifying_key).unwrap();
532
533        assert_eq!(verified.hop, pca.hop);
534    }
535
536    #[test]
537    #[cfg(feature = "p256")]
538    fn test_p256_wrong_key_fails() {
539        use p256::ecdsa::SigningKey;
540        use rand::rngs::OsRng;
541
542        let pca = sample_pca();
543
544        let signing_key = SigningKey::random(&mut OsRng);
545        let wrong_verifying_key = SigningKey::random(&mut OsRng).verifying_key();
546
547        let signed: SignedPca = CoseSigned::sign_p256(&pca, "issuer", &signing_key).unwrap();
548
549        let result = signed.verify_p256(wrong_verifying_key);
550        assert!(result.is_err());
551    }
552
553    #[test]
554    #[cfg(feature = "p384")]
555    fn test_p384_sign_verify() {
556        use p384::ecdsa::SigningKey;
557        use rand::rngs::OsRng;
558
559        let pca = sample_pca();
560
561        let signing_key = SigningKey::random(&mut OsRng);
562        let verifying_key = signing_key.verifying_key();
563
564        let signed: SignedPca =
565            CoseSigned::sign_p384(&pca, "p384-issuer", &signing_key).unwrap();
566
567        assert_eq!(signed.algorithm(), Some(SigningAlgorithm::ES384));
568
569        let verified = signed.verify_p384(verifying_key).unwrap();
570
571        assert_eq!(verified.hop, pca.hop);
572    }
573
574    #[test]
575    #[cfg(feature = "p384")]
576    fn test_p384_wrong_key_fails() {
577        use p384::ecdsa::SigningKey;
578        use rand::rngs::OsRng;
579
580        let pca = sample_pca();
581
582        let signing_key = SigningKey::random(&mut OsRng);
583        let wrong_verifying_key = SigningKey::random(&mut OsRng).verifying_key();
584
585        let signed: SignedPca = CoseSigned::sign_p384(&pca, "issuer", &signing_key).unwrap();
586
587        let result = signed.verify_p384(wrong_verifying_key);
588        assert!(result.is_err());
589    }
590
591    #[test]
592    #[cfg(all(feature = "ed25519", feature = "p256"))]
593    fn test_cross_algorithm_ed25519_vs_p256() {
594        use ed25519_dalek::SigningKey as Ed25519SigningKey;
595        use p256::ecdsa::SigningKey as P256SigningKey;
596        use rand::rngs::OsRng;
597
598        let pca = sample_pca();
599
600        let ed_key = Ed25519SigningKey::generate(&mut OsRng);
601        let signed: SignedPca = CoseSigned::sign_ed25519(&pca, "issuer", &ed_key).unwrap();
602
603        let p256_key = P256SigningKey::random(&mut OsRng);
604        let result = signed.verify_p256(p256_key.verifying_key());
605
606        assert!(matches!(result, Err(CoseError::AlgorithmMismatch { .. })));
607    }
608
609    #[test]
610    #[cfg(all(feature = "ed25519", feature = "p384"))]
611    fn test_cross_algorithm_ed25519_vs_p384() {
612        use ed25519_dalek::SigningKey as Ed25519SigningKey;
613        use p384::ecdsa::SigningKey as P384SigningKey;
614        use rand::rngs::OsRng;
615
616        let pca = sample_pca();
617
618        let ed_key = Ed25519SigningKey::generate(&mut OsRng);
619        let signed: SignedPca = CoseSigned::sign_ed25519(&pca, "issuer", &ed_key).unwrap();
620
621        let p384_key = P384SigningKey::random(&mut OsRng);
622        let result = signed.verify_p384(p384_key.verifying_key());
623
624        assert!(matches!(result, Err(CoseError::AlgorithmMismatch { .. })));
625    }
626}