Skip to main content

bitwarden_crypto/signing/
cose.rs

1//! This file contains helper functions to aid in COSE deserialization
2
3use coset::{
4    CoseKey, Label, ProtectedHeader, RegisteredLabel,
5    iana::{AkpKeyParameter, EllipticCurve, EnumI64, OkpKeyParameter},
6};
7use ml_dsa::B32;
8
9use super::SigningNamespace;
10use crate::{
11    CryptoError, KEY_ID_SIZE,
12    cose::SIGNING_NAMESPACE,
13    error::{EncodingError, SignatureError},
14    keys::KeyId,
15    signing::signing_key::ML_DSA_SEED_SIZE,
16};
17
18/// Helper function to extract the namespace from a `ProtectedHeader`. The namespace is a custom
19/// header set on the protected headers of the signature object.
20pub(super) fn namespace(
21    protected_header: &ProtectedHeader,
22) -> Result<SigningNamespace, CryptoError> {
23    let namespace = protected_header
24        .header
25        .rest
26        .iter()
27        .find_map(|(key, value)| {
28            if let Label::Int(key) = key
29                && *key == SIGNING_NAMESPACE
30            {
31                return value.as_integer();
32            }
33            None
34        })
35        .ok_or(SignatureError::InvalidNamespace)?;
36
37    SigningNamespace::try_from(i128::from(namespace))
38}
39
40/// Helper function to extract the content type from a `ProtectedHeader`. The content type is a
41/// standardized header set on the protected headers of the signature object. Currently we only
42/// support registered values, but PrivateUse values are also allowed in the COSE specification.
43pub(super) fn content_type(
44    protected_header: &ProtectedHeader,
45) -> Result<coset::iana::CoapContentFormat, CryptoError> {
46    protected_header
47        .header
48        .content_type
49        .as_ref()
50        .and_then(|ct| match ct {
51            RegisteredLabel::Assigned(content_format) => Some(*content_format),
52            _ => None,
53        })
54        .ok_or_else(|| SignatureError::InvalidSignature.into())
55}
56
57/// Helper function to extract the key ID from a `CoseKey`. The key ID is a standardized header
58/// and always set in bitwarden-crypto generated encrypted messages or signatures.
59pub(super) fn key_id(cose_key: &CoseKey) -> Result<KeyId, EncodingError> {
60    let key_id: [u8; KEY_ID_SIZE] = cose_key
61        .key_id
62        .as_slice()
63        .try_into()
64        .map_err(|_| EncodingError::InvalidValue("key id length"))?;
65    let key_id: KeyId = key_id.into();
66    Ok(key_id)
67}
68
69/// Helper function to parse a ed25519 signing key from a `CoseKey`.
70pub(super) fn ed25519_signing_key(
71    cose_key: &CoseKey,
72) -> Result<ed25519_dalek::SigningKey, EncodingError> {
73    // https://www.rfc-editor.org/rfc/rfc9053.html#name-octet-key-pair
74    let d = okp_d(cose_key)?;
75    let crv = okp_curve(cose_key)?;
76    if crv == EllipticCurve::Ed25519.to_i64().into() {
77        Ok(ed25519_dalek::SigningKey::from_bytes(
78            d.try_into()
79                .map_err(|_| EncodingError::InvalidCoseEncoding)?,
80        ))
81    } else {
82        Err(EncodingError::UnsupportedValue("OKP curve"))
83    }
84}
85
86/// Helper function to parse a ed25519 verifying key from a `CoseKey`.
87pub(super) fn ed25519_verifying_key(
88    cose_key: &CoseKey,
89) -> Result<ed25519_dalek::VerifyingKey, EncodingError> {
90    // https://www.rfc-editor.org/rfc/rfc9053.html#name-octet-key-pair
91    let x = okp_x(cose_key)?;
92    let crv = okp_curve(cose_key)?;
93    if crv == EllipticCurve::Ed25519.to_i64().into() {
94        ed25519_dalek::VerifyingKey::from_bytes(
95            x.try_into()
96                .map_err(|_| EncodingError::InvalidValue("ed25519 OKP verifying key"))?,
97        )
98        .map_err(|_| EncodingError::InvalidValue("ed25519 OKP verifying key"))
99    } else {
100        Err(EncodingError::UnsupportedValue("OKP curve"))
101    }
102}
103
104/// Helper function to look up a parameter in a `CoseKey` by its registered label.
105fn cose_param(
106    cose_key: &CoseKey,
107    param: impl EnumI64 + Copy,
108) -> Option<&coset::cbor::value::Value> {
109    cose_key.params.iter().find_map(|(key, value)| match key {
110        Label::Int(i) if EnumI64::from_i64(*i) == Some(param) => Some(value),
111        _ => None,
112    })
113}
114
115/// Helper function to parse the private key `d` from a `CoseKey`.
116fn okp_d(cose_key: &CoseKey) -> Result<&[u8], EncodingError> {
117    // https://www.rfc-editor.org/rfc/rfc9053.html#name-octet-key-pair
118    cose_param(cose_key, OkpKeyParameter::D)
119        .and_then(|v| v.as_bytes().map(Vec::as_slice))
120        .ok_or(EncodingError::MissingValue("OKP private key"))
121}
122
123/// Helper function to parse the public key `x` from a `CoseKey`.
124fn okp_x(cose_key: &CoseKey) -> Result<&[u8], EncodingError> {
125    // https://www.rfc-editor.org/rfc/rfc9053.html#name-octet-key-pair
126    cose_param(cose_key, OkpKeyParameter::X)
127        .and_then(|v| v.as_bytes().map(Vec::as_slice))
128        .ok_or(EncodingError::MissingValue("OKP public key"))
129}
130
131/// Helper function to parse the OKP curve from a `CoseKey`.
132fn okp_curve(cose_key: &CoseKey) -> Result<i128, EncodingError> {
133    // https://www.rfc-editor.org/rfc/rfc9053.html#name-octet-key-pair
134    cose_param(cose_key, OkpKeyParameter::Crv)
135        .and_then(|v| v.as_integer().map(i128::from))
136        .ok_or(EncodingError::MissingValue("OKP curve"))
137}
138
139/// Helper function to parse the private key from an AKP `CoseKey`.
140fn akp_priv(cose_key: &CoseKey) -> Result<&[u8], EncodingError> {
141    cose_param(cose_key, AkpKeyParameter::Priv)
142        .and_then(|v| v.as_bytes().map(Vec::as_slice))
143        .ok_or(EncodingError::MissingValue("AKP private key"))
144}
145
146/// Helper function to parse the public key from an AKP `CoseKey`.
147fn akp_pub(cose_key: &CoseKey) -> Result<&[u8], EncodingError> {
148    cose_param(cose_key, AkpKeyParameter::Pub)
149        .and_then(|v| v.as_bytes().map(Vec::as_slice))
150        .ok_or(EncodingError::MissingValue("AKP public key"))
151}
152
153/// Helper function to parse an ML-DSA signing key from a `CoseKey`. The `Priv` parameter
154/// contains the 32-byte seed, from which the full key pair is deterministically derived.
155pub(super) fn mldsa_seed(cose_key: &CoseKey) -> Result<B32, EncodingError> {
156    let priv_bytes = akp_priv(cose_key)?;
157    let seed: [u8; ML_DSA_SEED_SIZE] = priv_bytes
158        .try_into()
159        .map_err(|_| EncodingError::InvalidValue("ML-DSA seed length"))?;
160    Ok(seed.into())
161}
162
163/// Helper function to parse an ML-DSA-44 verifying key from a `CoseKey`.
164pub(super) fn mldsa44_verifying_key(
165    cose_key: &CoseKey,
166) -> Result<ml_dsa::VerifyingKey<ml_dsa::MlDsa44>, EncodingError> {
167    let pub_bytes = akp_pub(cose_key)?;
168    let vk_encoded = ml_dsa::EncodedVerifyingKey::<ml_dsa::MlDsa44>::try_from(pub_bytes)
169        .map_err(|_| EncodingError::InvalidValue("ML-DSA-44 verifying key length"))?;
170    Ok(ml_dsa::VerifyingKey::<ml_dsa::MlDsa44>::decode(&vk_encoded))
171}