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::{CborSerializable, CoseSign1, CoseSign1Builder, HeaderBuilder, iana};
23use serde::{Serialize, de::DeserializeOwned};
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 =
145            CoseSign1::from_slice(bytes).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::{
290        Signature, SigningKey, VerifyingKey, signature::Signer, signature::Verifier,
291    };
292
293    impl<T> CoseSigned<T>
294    where
295        T: Serialize + DeserializeOwned,
296    {
297        /// Signs payload with P-256. Algorithm is set to ES256 automatically.
298        pub fn sign_p256(
299            payload: &T,
300            issuer: &str,
301            signing_key: &SigningKey,
302        ) -> Result<Self, CoseError> {
303            Self::sign_with(payload, issuer, SigningAlgorithm::ES256, |data| {
304                let sig: Signature = signing_key.sign(data);
305                Ok(sig.to_bytes().to_vec())
306            })
307        }
308
309        /// Verifies payload with P-256. Returns error if algorithm is not ES256.
310        pub fn verify_p256(&self, verifying_key: &VerifyingKey) -> Result<T, CoseError> {
311            self.check_algorithm(SigningAlgorithm::ES256)?;
312
313            self.verify_with(|data, sig| {
314                let signature =
315                    Signature::from_slice(sig).map_err(|_| CoseError::InvalidSignatureLength)?;
316
317                verifying_key
318                    .verify(data, &signature)
319                    .map_err(|_| CoseError::VerificationFailed)
320            })
321        }
322    }
323}
324
325// ============================================================================
326// P-384 (ES384) Implementation
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            issuer: &str,
344            signing_key: &SigningKey,
345        ) -> Result<Self, CoseError> {
346            Self::sign_with(payload, issuer, SigningAlgorithm::ES384, |data| {
347                let sig: Signature = signing_key.sign(data);
348                Ok(sig.to_bytes().to_vec())
349            })
350        }
351
352        /// Verifies payload with P-384. Returns error if algorithm is not ES384.
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
360                verifying_key
361                    .verify(data, &signature)
362                    .map_err(|_| CoseError::VerificationFailed)
363            })
364        }
365    }
366}
367
368// ============================================================================
369// Type Aliases
370// ============================================================================
371
372use crate::pca::PcaPayload;
373use crate::poc::PocPayload;
374
375/// COSE-signed PCA payload.
376pub type SignedPca = CoseSigned<PcaPayload>;
377
378/// COSE-signed PoC payload.
379pub type SignedPoc = CoseSigned<PocPayload>;
380
381// ============================================================================
382// Tests
383// ============================================================================
384
385#[cfg(test)]
386mod tests {
387    use super::*;
388    use crate::pca::{Executor, ExecutorBinding};
389
390    fn sample_pca() -> PcaPayload {
391        PcaPayload {
392            hop: "gateway".into(),
393            p_0: "https://idp.example.com/users/alice".into(),
394            ops: vec!["read:/user/*".into()],
395            executor: Executor {
396                binding: ExecutorBinding::new().with("org", "acme"),
397            },
398            provenance: None,
399            constraints: None,
400        }
401    }
402
403    #[test]
404    fn test_sign_with_and_verify_with() {
405        let pca = sample_pca();
406
407        let signed: SignedPca = CoseSigned::sign_with(
408            &pca,
409            "https://cat.example.com",
410            SigningAlgorithm::EdDSA,
411            |_data| Ok(vec![0xAB; 64]),
412        )
413        .unwrap();
414
415        assert_eq!(signed.issuer(), Some("https://cat.example.com".into()));
416        assert_eq!(signed.algorithm(), Some(SigningAlgorithm::EdDSA));
417
418        let verified = signed.verify_with(|_data, _sig| Ok(())).unwrap();
419
420        assert_eq!(verified.hop, pca.hop);
421        assert_eq!(verified.p_0, pca.p_0);
422    }
423
424    #[test]
425    fn test_roundtrip_bytes() {
426        let pca = sample_pca();
427
428        let signed: SignedPca =
429            CoseSigned::sign_with(&pca, "issuer-1", SigningAlgorithm::ES256, |_| {
430                Ok(vec![0xCD; 64])
431            })
432            .unwrap();
433
434        let bytes = signed.to_bytes().unwrap();
435        let restored: SignedPca = CoseSigned::from_bytes(&bytes).unwrap();
436
437        assert_eq!(restored.issuer(), Some("issuer-1".into()));
438    }
439
440    #[test]
441    fn test_payload_unverified() {
442        let pca = sample_pca();
443
444        let signed: SignedPca =
445            CoseSigned::sign_with(&pca, "issuer", 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 =
502            CoseSigned::sign_with(&pca, "issuer", SigningAlgorithm::ES256, |_| {
503                Ok(vec![0x00; 64])
504            })
505            .unwrap();
506
507        let verifying_key = SigningKey::generate(&mut OsRng).verifying_key();
508        let result = signed.verify_ed25519(&verifying_key);
509
510        assert!(matches!(result, Err(CoseError::AlgorithmMismatch { .. })));
511    }
512
513    #[test]
514    #[cfg(feature = "p256")]
515    fn test_p256_sign_verify() {
516        use p256::ecdsa::SigningKey;
517        use rand::rngs::OsRng;
518
519        let pca = sample_pca();
520
521        let signing_key = SigningKey::random(&mut OsRng);
522        let verifying_key = signing_key.verifying_key();
523
524        let signed: SignedPca = CoseSigned::sign_p256(&pca, "p256-issuer", &signing_key).unwrap();
525
526        assert_eq!(signed.algorithm(), Some(SigningAlgorithm::ES256));
527
528        let verified = signed.verify_p256(verifying_key).unwrap();
529
530        assert_eq!(verified.hop, pca.hop);
531    }
532
533    #[test]
534    #[cfg(feature = "p256")]
535    fn test_p256_wrong_key_fails() {
536        use p256::ecdsa::SigningKey;
537        use rand::rngs::OsRng;
538
539        let pca = sample_pca();
540
541        let signing_key = SigningKey::random(&mut OsRng);
542        let wrong_signing_key = SigningKey::random(&mut OsRng); // <- binding
543        let wrong_verifying_key = wrong_signing_key.verifying_key();
544
545        let signed: SignedPca = CoseSigned::sign_p256(&pca, "issuer", &signing_key).unwrap();
546
547        let result = signed.verify_p256(wrong_verifying_key);
548        assert!(result.is_err());
549    }
550
551    #[test]
552    #[cfg(feature = "p384")]
553    fn test_p384_sign_verify() {
554        use p384::ecdsa::SigningKey;
555        use rand::rngs::OsRng;
556
557        let pca = sample_pca();
558
559        let signing_key = SigningKey::random(&mut OsRng);
560        let verifying_key = signing_key.verifying_key();
561
562        let signed: SignedPca = CoseSigned::sign_p384(&pca, "p384-issuer", &signing_key).unwrap();
563
564        assert_eq!(signed.algorithm(), Some(SigningAlgorithm::ES384));
565
566        let verified = signed.verify_p384(verifying_key).unwrap();
567
568        assert_eq!(verified.hop, pca.hop);
569    }
570
571    #[test]
572    #[cfg(feature = "p384")]
573    fn test_p384_wrong_key_fails() {
574        use p384::ecdsa::SigningKey;
575        use rand::rngs::OsRng;
576
577        let pca = sample_pca();
578
579        let signing_key = SigningKey::random(&mut OsRng);
580        let wrong_signing_key = SigningKey::random(&mut OsRng); // <- binding
581        let wrong_verifying_key = wrong_signing_key.verifying_key();
582
583        let signed: SignedPca = CoseSigned::sign_p384(&pca, "issuer", &signing_key).unwrap();
584
585        let result = signed.verify_p384(wrong_verifying_key);
586        assert!(result.is_err());
587    }
588
589    #[test]
590    #[cfg(all(feature = "ed25519", feature = "p256"))]
591    fn test_cross_algorithm_ed25519_vs_p256() {
592        use ed25519_dalek::SigningKey as Ed25519SigningKey;
593        use p256::ecdsa::SigningKey as P256SigningKey;
594        use rand::rngs::OsRng;
595
596        let pca = sample_pca();
597
598        let ed_key = Ed25519SigningKey::generate(&mut OsRng);
599        let signed: SignedPca = CoseSigned::sign_ed25519(&pca, "issuer", &ed_key).unwrap();
600
601        let p256_key = P256SigningKey::random(&mut OsRng);
602        let result = signed.verify_p256(p256_key.verifying_key());
603
604        assert!(matches!(result, Err(CoseError::AlgorithmMismatch { .. })));
605    }
606
607    #[test]
608    #[cfg(all(feature = "ed25519", feature = "p384"))]
609    fn test_cross_algorithm_ed25519_vs_p384() {
610        use ed25519_dalek::SigningKey as Ed25519SigningKey;
611        use p384::ecdsa::SigningKey as P384SigningKey;
612        use rand::rngs::OsRng;
613
614        let pca = sample_pca();
615
616        let ed_key = Ed25519SigningKey::generate(&mut OsRng);
617        let signed: SignedPca = CoseSigned::sign_ed25519(&pca, "issuer", &ed_key).unwrap();
618
619        let p384_key = P384SigningKey::random(&mut OsRng);
620        let result = signed.verify_p384(p384_key.verifying_key());
621
622        assert!(matches!(result, Err(CoseError::AlgorithmMismatch { .. })));
623    }
624}