bh_jws_utils/
utils.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 std::cell::Cell;
17
18use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
19use bherror::traits::{ErrorContext as _, ForeignError as _, PropagateError as _};
20
21use crate::{
22    openssl_ec_pub_key_to_jwk, CryptoError, JwkPublic, SignatureVerifier, Signer, SigningAlgorithm,
23};
24
25/// Type alias for a boxed error.
26pub type BoxError = Box<dyn std::error::Error + Send + Sync>;
27
28/// Create payload for a `JWS`, given its header and claims.
29///
30/// The payload is constructed by concatenating the header and claims by `.`
31/// character, i.e. `<header>.<claims>`, as defined [here].
32///
33/// [here]: https://www.rfc-editor.org/rfc/rfc7515.html#section-5.1
34pub fn construct_jws_payload(header: &str, claims: &str) -> String {
35    format!("{header}.{claims}")
36}
37
38/// Returns the `base64url`-encoded string of the given `input`.
39pub fn base64_url_encode<T: AsRef<[u8]>>(input: T) -> String {
40    URL_SAFE_NO_PAD.encode(input)
41}
42
43/// Utility function that delegates to [`jwt::SignWithKey`] while allowing
44/// proper propagation of errors from both the foreign trait and the [`Signer`].
45pub(crate) fn sign_jwt<UnsignedJwt, SignedJwt, S>(
46    unsigned_jwt: UnsignedJwt,
47    signer: &S,
48) -> Result<SignedJwt, BoxError>
49where
50    UnsignedJwt: jwt::SignWithKey<SignedJwt>,
51    S: Signer + ?Sized,
52{
53    let signer_wrapper = ErrorHolder::new(signer);
54    unsigned_jwt
55        .sign_with_key(&signer_wrapper)
56        .map_err(signer_wrapper.combine_error())
57}
58
59impl<T: Signer + ?Sized> jwt::SigningAlgorithm for ErrorHolder<&'_ T> {
60    fn algorithm_type(&self) -> jwt::AlgorithmType {
61        self.inner.algorithm().into()
62    }
63
64    fn sign(&self, header: &str, claims: &str) -> Result<String, jwt::Error> {
65        let message = construct_jws_payload(header, claims);
66
67        match self.inner.sign(message.as_bytes()) {
68            Ok(signature_bytes) => Ok(base64_url_encode(signature_bytes)),
69            Err(error) => Err(self.store_error(error)),
70        }
71    }
72}
73
74/// Utility function that delegates to [`jwt::VerifyWithKey`] while allowing
75/// proper propagation of errors from both the foreign trait and the
76/// [`SignatureVerifier`].
77pub(crate) fn verify_jwt_signature<UnverifiedJwt, VerifiedJwt, V>(
78    unverified_jwt: UnverifiedJwt,
79    verifier: &V,
80    public_key: &JwkPublic,
81) -> Result<VerifiedJwt, BoxError>
82where
83    UnverifiedJwt: jwt::VerifyWithKey<VerifiedJwt>,
84    V: SignatureVerifier + ?Sized,
85{
86    let verifier_wrapper = ErrorHolder::new(VerifierWrapper {
87        verifier,
88        public_key,
89    });
90    unverified_jwt
91        .verify_with_key(&verifier_wrapper)
92        .map_err(verifier_wrapper.combine_error())
93}
94
95/// Adapter for implementing [jwt::VerifyingAlgorithm], for internal use.
96struct VerifierWrapper<'a, T: SignatureVerifier + ?Sized> {
97    verifier: &'a T,
98    public_key: &'a JwkPublic,
99}
100
101impl<T: SignatureVerifier + ?Sized> jwt::VerifyingAlgorithm
102    for ErrorHolder<VerifierWrapper<'_, T>>
103{
104    fn algorithm_type(&self) -> jwt::AlgorithmType {
105        self.inner.verifier.algorithm().into()
106    }
107
108    fn verify_bytes(
109        &self,
110        header: &str,
111        claims: &str,
112        signature: &[u8],
113    ) -> Result<bool, jwt::Error> {
114        let message = construct_jws_payload(header, claims);
115
116        self.inner
117            .verifier
118            .verify(message.as_bytes(), signature, self.inner.public_key)
119            .map_err(|error| self.store_error(error))
120    }
121}
122
123/// Helper wrapper for collecting errors from signer/verifier implementations
124/// which cannot be piped through `jwt:Error`.
125struct ErrorHolder<T> {
126    inner: T,
127    /// Interior-mutable slot for the error returned by the wrapped signer, if any.
128    /// `jwt::Error` doesn't let us convey it, so we have to do it in a roundabout way...
129    error: Cell<Option<BoxError>>,
130}
131
132impl<T> ErrorHolder<T> {
133    fn new(inner: T) -> Self {
134        Self {
135            inner,
136            error: Cell::new(None),
137        }
138    }
139
140    fn store_error(&self, error: BoxError) -> jwt::Error {
141        let previous = self.error.replace(Some(error));
142        debug_assert!(previous.is_none());
143
144        // Not really "correct", but we need to return *something*
145        // The caller should recover the true error from the wrapper instead...
146        jwt::Error::InvalidSignature
147    }
148
149    /// Check whether an underlying error occurred, returning it if it did, or
150    /// returning the [`jwt::Error`] if not.
151    ///
152    /// The caller SHOULD call this function as a finalizer to recover the
153    /// true error if it is present, rather than the one returned by
154    /// `jwt` crate trait impls.
155    fn combine_error(self) -> impl FnOnce(jwt::Error) -> BoxError {
156        |jwt_error| {
157            if let Some(underlying_error) = self.error.into_inner() {
158                debug_assert!(matches!(jwt_error, jwt::Error::InvalidSignature));
159                underlying_error
160            } else {
161                Box::new(jwt_error)
162            }
163        }
164    }
165}
166
167/// Retrieve public JWK from the provided x5chain certificate chain leaf.
168///
169/// Currently, only `Es256` is supported.
170pub fn public_jwk_from_x5chain_leaf(
171    x5chain: &bhx5chain::X5Chain,
172    alg: &SigningAlgorithm,
173    kid: Option<&str>,
174) -> bherror::Result<JwkPublic, CryptoError> {
175    let pkey = x5chain
176        .leaf_certificate_key()
177        .with_err(|| CryptoError::InvalidX5Chain)
178        .ctx(|| "invalid public key from certificate")?;
179
180    match (alg, pkey.id()) {
181        (SigningAlgorithm::Es256, openssl::pkey::Id::EC) => {
182            let ec_key = pkey
183                .ec_key()
184                .foreign_err(|| CryptoError::CryptoBackend)
185                .ctx(|| "invalid EC key")?;
186
187            openssl_ec_pub_key_to_jwk(&ec_key, kid).ctx(|| "unable to construct JWK")
188        }
189        _ => Err(bherror::Error::root(CryptoError::Unsupported(
190            "only Es256 is currently supported".to_string(),
191        ))),
192    }
193}