Skip to main content

basil_cose/
sign.rs

1// SPDX-FileCopyrightText: 2026 OpenBasil Contributors
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! The signed construction: a bare `COSE_Sign1` (control RPC / push
6//! surfaces; any sign-only basil surface).
7
8use alloc::vec::Vec;
9
10use crate::claims::{Claims, ProtectedHeaders, ValidationParams};
11use crate::codec::{self, Sign1Layer};
12use crate::error::{BuildError, VerifyError};
13use crate::traits::{Signer, Verifier};
14use crate::types::{ContentType, CoseBytes, ExternalAad, KeyId, Signature};
15
16/// Parameters for [`build_signed`].
17#[derive(Debug, Clone)]
18pub struct SignParams<'a> {
19    /// The payload content type (protected header 3).
20    pub content_type: ContentType,
21    /// The payload to sign (always embedded; detached payloads are not part
22    /// of the v1 profile).
23    pub payload: &'a [u8],
24    /// Optional claims for sign-only messages.
25    pub claims: Option<Claims>,
26    /// The `Sig_structure` external AAD.
27    pub external_aad: ExternalAad,
28}
29
30/// Build a bare signed message.
31///
32/// # Errors
33/// [`BuildError::SenderKeyMismatch`] when a present `sender_key_id` claim
34/// does not equal the signer key id; [`BuildError::ResponseKeyNotText`];
35/// [`BuildError::Sign`] when the signer fails.
36pub async fn build_signed<S: Signer>(
37    params: &SignParams<'_>,
38    signer: &S,
39) -> Result<CoseBytes, BuildError> {
40    build_signed_with_headers(params, &ProtectedHeaders::default(), signer).await
41}
42
43/// Build a bare signed message with additional critical protected headers.
44///
45/// # Errors
46/// [`BuildError::SenderKeyMismatch`] when a present `sender_key_id` claim
47/// does not equal the signer key id; [`BuildError::ResponseKeyNotText`];
48/// [`BuildError::Sign`] when the signer fails.
49pub async fn build_signed_with_headers<S: Signer>(
50    params: &SignParams<'_>,
51    protected_headers: &ProtectedHeaders,
52    signer: &S,
53) -> Result<CoseBytes, BuildError> {
54    if let Some(claims) = &params.claims {
55        if let Some(sender) = &claims.sender_key_id
56            && sender != signer.key_id()
57        {
58            return Err(BuildError::SenderKeyMismatch);
59        }
60        if let Some(k) = &claims.response_key_id
61            && k.as_catalog_name().is_none()
62        {
63            return Err(BuildError::ResponseKeyNotText);
64        }
65    }
66    let protected = codec::encode_sign1_protected_bare_with_headers(
67        signer.algorithm(),
68        signer.key_id(),
69        &params.content_type,
70        params.claims.as_ref(),
71        Some(protected_headers),
72    )
73    .map_err(|codec::CodecError| BuildError::Codec)?;
74    let sig_structure =
75        codec::sig_structure(&protected, params.external_aad.as_bytes(), params.payload)
76            .map_err(|codec::CodecError| BuildError::Codec)?;
77    let signature = signer.sign(&sig_structure).await?;
78    codec::assemble_sign1(&protected, params.payload, signature.as_bytes())
79        .map(CoseBytes::new)
80        .map_err(|codec::CodecError| BuildError::Codec)
81}
82
83/// Parameters for [`verify_signed`].
84#[derive(Debug, Clone)]
85pub struct VerifySignedParams<'a> {
86    /// The `Sig_structure` external AAD the protocol binds.
87    pub external_aad: ExternalAad,
88    /// Temporal/audience/role bounds: supply them iff claims are expected.
89    /// `Some` demands claims; `None` demands their absence.
90    pub validation: Option<&'a ValidationParams>,
91}
92
93/// A verified bare signed message.
94#[derive(Debug, Clone)]
95pub struct VerifiedSigned {
96    /// The payload content type.
97    pub content_type: ContentType,
98    /// The signed payload.
99    pub payload: Vec<u8>,
100    /// Claims, when the message carried them.
101    pub claims: Option<Claims>,
102    /// Additional critical protected headers.
103    pub protected_headers: ProtectedHeaders,
104    /// The outer `kid` that signed the message.
105    pub signer_key_id: KeyId,
106}
107
108/// Verify a bare signed message.
109///
110/// In order: strict decode, signature verification via `verifier`, claims
111/// presence per `params.validation`, then temporal / audience / role checks
112/// and the `sender_key_id == kid` cross-check when claims are present.
113///
114/// # Errors
115/// [`VerifyError::Decode`], verifier failures,
116/// [`VerifyError::ClaimsPresenceMismatch`],
117/// [`VerifyError::SenderKeyMismatch`], [`VerifyError::Claims`].
118pub async fn verify_signed<V: Verifier>(
119    bytes: &[u8],
120    verifier: &V,
121    params: &VerifySignedParams<'_>,
122) -> Result<VerifiedSigned, VerifyError> {
123    let decoded = codec::decode_sign1_strict(bytes, Sign1Layer::Bare)?;
124
125    let sig_structure = codec::sig_structure(
126        &decoded.protected,
127        params.external_aad.as_bytes(),
128        &decoded.payload,
129    )
130    .map_err(|codec::CodecError| VerifyError::SignatureInvalid)?;
131    let signature = Signature::from_bytes(decoded.signature.clone())
132        .map_err(|_| VerifyError::SignatureInvalid)?;
133    verifier
134        .verify(
135            &decoded.kid,
136            decoded.algorithm,
137            &decoded.protected_headers,
138            &sig_structure,
139            &signature,
140        )
141        .await?;
142
143    match (params.validation, &decoded.claims) {
144        (Some(validation), Some(claims)) => {
145            if let Some(sender) = &claims.sender_key_id
146                && sender != &decoded.kid
147            {
148                return Err(VerifyError::SenderKeyMismatch);
149            }
150            claims.validate(validation)?;
151        }
152        (None, None) => {}
153        _ => return Err(VerifyError::ClaimsPresenceMismatch),
154    }
155
156    let Some(content_type) = decoded.content_type else {
157        // Unreachable: the bare decoder requires a content type.
158        return Err(VerifyError::Decode(
159            crate::error::DecodeError::MissingHeader {
160                label: crate::label::HDR_CONTENT_TYPE,
161            },
162        ));
163    };
164    Ok(VerifiedSigned {
165        content_type,
166        payload: decoded.payload,
167        claims: decoded.claims,
168        protected_headers: decoded.protected_headers,
169        signer_key_id: decoded.kid,
170    })
171}