bh_jws_utils/
x509_chain.rs

1// Copyright (C) 2020-2025  The Blockhouse Technology Limited (TBTL).
2//
3// This program is free software: you can redistribute it and/or modify it
4// under the terms of the GNU Affero General Public License as published by
5// the Free Software Foundation, either version 3 of the License, or (at your
6// option) any later version.
7//
8// This program is distributed in the hope that it will be useful, but
9// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
10// or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public
11// License for more details.
12//
13// You should have received a copy of the GNU Affero General Public License
14// along with this program.  If not, see <https://www.gnu.org/licenses/>.
15
16use bherror::traits::{ForeignBoxed as _, ForeignError, PropagateError};
17use bhx5chain::X5Chain;
18
19use crate::{
20    openssl_impl::public_key_from_jwk_es256, BoxError, CryptoError, HasJwkKid, HasX5Chain,
21    JwkPublic, Signer, SigningAlgorithm,
22};
23
24/// [`Signer`] decorator with an X.509 certificate chain associated with
25/// the key pair.
26///
27/// Useful in contexts which require distributing the certificate chain with the
28/// signature (e.g. the `x5c` JWT header parameter).
29#[derive(Debug)]
30pub struct SignerWithChain<S> {
31    pub(crate) signer: S,
32    pub(crate) x5chain: X5Chain,
33}
34
35impl<S: Signer> SignerWithChain<S> {
36    /// Construct a new instance by pairing up a [`Signer`] with the [`X5Chain`]
37    /// for its public key.
38    ///
39    /// # Errors
40    ///
41    /// Returns an error if the public keys of the [`Signer`] and [`X5Chain`]'s
42    /// leaf certificate do not match.
43    ///
44    /// Currently, due to limited support for signing algorithms, returns an
45    /// error if the key algorithm is not supported.
46    pub fn new(signer: S, x5chain: X5Chain) -> bherror::Result<Self, CryptoError> {
47        public_key_matches(&signer, &x5chain)?;
48
49        Ok(Self { signer, x5chain })
50    }
51
52    /// Returns a reference to the contained [`X5Chain`].
53    pub fn certificate_chain(&self) -> &X5Chain {
54        &self.x5chain
55    }
56
57    /// Get the public key in JWK format.
58    pub fn public_jwk(&self) -> bherror::Result<JwkPublic, CryptoError> {
59        self.signer
60            .public_jwk()
61            .map_err(|boxed_error| downcast_or_chain(boxed_error, || CryptoError::CryptoBackend))
62    }
63}
64
65fn public_key_matches<S: Signer>(
66    signer: &S,
67    x5chain: &X5Chain,
68) -> bherror::Result<(), CryptoError> {
69    let signer_public_key = signer_public_key_openssl(signer)?;
70
71    let leaf_public_key = x5chain
72        .leaf_certificate_key()
73        .with_err(|| CryptoError::InvalidX5Chain)?;
74
75    if !leaf_public_key.public_eq(&signer_public_key) {
76        return Err(bherror::Error::root(CryptoError::PublicKeyMismatch));
77    }
78
79    Ok(())
80}
81
82fn signer_public_key_openssl<S: Signer>(
83    signer: &S,
84) -> bherror::Result<openssl::pkey::PKey<openssl::pkey::Public>, CryptoError> {
85    let signer_public_jwk = signer
86        .public_jwk()
87        .map_err(|boxed_error| downcast_or_chain(boxed_error, || CryptoError::CryptoBackend))?;
88
89    match signer.algorithm() {
90        SigningAlgorithm::Es256 => {
91            let signer_public_key = public_key_from_jwk_es256(&signer_public_jwk)
92                .with_err(|| CryptoError::InvalidPublicKey)?;
93            Ok(openssl::pkey::PKey::from_ec_key(signer_public_key)
94                .foreign_err(|| CryptoError::CryptoBackend)?)
95        }
96        _ => Err(bherror::Error::root(CryptoError::Unsupported(
97            "only ES256 is currently supported".to_owned(),
98        ))),
99    }
100}
101
102/// Either successfully downcast to the specified [`bherror::BhError`], or chain
103/// a new such error onto this one.
104fn downcast_or_chain<E, F>(boxed_error: BoxError, f: F) -> bherror::Error<E>
105where
106    E: bherror::BhError,
107    F: FnOnce() -> E,
108{
109    match boxed_error.downcast() {
110        Ok(boxed_downcast_error) => *boxed_downcast_error,
111        original_error_result @ Err(_) => original_error_result.foreign_boxed_err(f).unwrap_err(),
112    }
113}
114
115impl<S: Signer> Signer for SignerWithChain<S> {
116    fn algorithm(&self) -> SigningAlgorithm {
117        self.signer.algorithm()
118    }
119
120    fn sign(&self, message: &[u8]) -> Result<Vec<u8>, BoxError> {
121        self.signer.sign(message)
122    }
123
124    fn public_jwk(&self) -> Result<JwkPublic, BoxError> {
125        self.signer.public_jwk()
126    }
127}
128
129impl<S: Signer> HasX5Chain for SignerWithChain<S> {
130    fn x5chain(&self) -> X5Chain {
131        self.x5chain.clone()
132    }
133}
134
135impl<S: HasJwkKid> HasJwkKid for SignerWithChain<S> {
136    fn jwk_kid(&self) -> &str {
137        self.signer.jwk_kid()
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use crate::{CryptoError, Es256Signer};
144
145    use super::SignerWithChain;
146
147    #[test]
148    fn signer_with_chain_construction() {
149        let correct_key = Es256Signer::generate("correct".into()).unwrap();
150        let certificate_chain = bhx5chain::Builder::dummy()
151            .generate_x5chain(&correct_key.public_key_pem().unwrap(), None)
152            .unwrap();
153
154        let _signer = SignerWithChain::new(correct_key, certificate_chain.clone()).unwrap();
155
156        // Generate a different key pair, not corresponding to the leaf certificate
157        let incorrect_key = Es256Signer::generate("incorrect".into()).unwrap();
158
159        // `Es256Signer` does not impl Debug, so this is a workaround
160        let Err(error) = SignerWithChain::new(incorrect_key, certificate_chain) else {
161            unreachable!()
162        };
163        assert_eq!(error.error, CryptoError::PublicKeyMismatch);
164    }
165}