Skip to main content

bh_sd_jwt/
models.rs

1// Copyright (C) 2020-2026  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 bh_jws_utils::{jwt, JwkPublic, JwtVerifier as _, SignatureVerifier, SigningAlgorithm};
17use bherror::{
18    traits::{ForeignBoxed, ForeignError, PropagateError},
19    Error,
20};
21pub use iref::Uri;
22pub use jwt::claims::SecondsSinceEpoch;
23use serde::{Deserialize, Serialize};
24pub use serde_json::{Map, Value};
25use yoke::Yoke;
26
27use crate::error::{FormatError, Result, SignatureError};
28mod disclosure;
29mod error;
30mod path;
31pub(crate) mod path_map;
32
33pub use disclosure::*;
34pub(crate) use error::*;
35pub use path::*;
36
37use crate::{
38    utils::SD_ALG_FIELD_NAME, Hasher, HashingAlgorithm, IssuerJwt, IssuerJwtHeader,
39    IssuerPublicKeyLookup,
40};
41
42/// The `cnf` claim of the SD-JWT, containing the public key to bind with the credential.
43///
44/// See the [draft] and [RFC7800] for details.
45///
46/// [draft]: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-selective-disclosure-jwt-08#section-5.1.2
47/// [RFC7800]: https://www.rfc-editor.org/rfc/rfc7800.html#section-3
48#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
49pub struct CnfClaim {
50    /// Public key bound to the credential.
51    pub jwk: JwkPublic,
52}
53
54/// A JSON object, i.e. a mapping from [`String`] to [`Value`].
55pub type JsonObject = Map<String, Value>;
56
57/// Panics if the argument is not a JSON object.
58#[inline(always)]
59pub(crate) fn into_object(value: Value) -> JsonObject {
60    if let Value::Object(object) = value {
61        object
62    } else {
63        panic!("Argument wasn't an object")
64    }
65}
66
67/// Helper macro with the same syntax as [`serde_json::json`] specialized for
68/// constructing JSON objects.
69///
70/// It will construct a more specific type ([`serde_json::Map<String,Value>`])
71/// than just [`serde_json::Value`] when constructing an object, and panic if
72/// the syntax is valid JSON but not an object.
73#[macro_export]
74macro_rules! json_object {
75    ($stuff:tt) => {
76        match ::serde_json::json!($stuff) {
77            ::serde_json::Value::Object(o) => o,
78            _ => unreachable!("JSON literal wasn't an object"),
79        }
80    };
81}
82
83pub(crate) const SD: &str = "_sd";
84pub(crate) const ELLIPSIS: &str = "...";
85pub(crate) static RESERVED_CLAIM_NAMES: &[&str] = &[SD, SD_ALG_FIELD_NAME, ELLIPSIS];
86
87/// SD-JWT in parsed form for the issuance flow.
88pub(crate) struct ParsedSdJwtIssuance<State> {
89    pub(crate) jwt: jwt::Token<IssuerJwtHeader, IssuerJwt, State>,
90    pub(crate) disclosures: Vec<Disclosure>,
91}
92
93/// SD-JWT in parsed form created by the issuer to be handed to the holder.
94#[cfg_attr(test, derive(Debug))]
95pub struct IssuedSdJwt(pub(crate) ParsedSdJwtIssuance<jwt::token::Signed>);
96
97impl IssuedSdJwt {
98    /// Serialize the issued SD-JWT into the Compact Serialization format.
99    pub fn into_string_compact(self) -> String {
100        crate::SdJwt::new(
101            self.0.jwt.into(),
102            self.0
103                .disclosures
104                .into_iter()
105                .map(Disclosure::into_string)
106                .collect(),
107        )
108        .to_string()
109    }
110}
111
112/// SD-JWT (in parsed form), but not yet validated in any other way.
113#[cfg_attr(test, derive(Debug))]
114pub(crate) struct SdJwtUnverified<'a>(pub(crate) ParsedSdJwtIssuance<jwt::Unverified<'a>>);
115
116impl SdJwtUnverified<'_> {
117    pub(crate) async fn verify<'a>(
118        self,
119        issuer_public_key_lookup: &impl IssuerPublicKeyLookup,
120        get_signature_verifier: impl FnOnce(SigningAlgorithm) -> Option<&'a dyn SignatureVerifier>,
121    ) -> Result<(SdJwtSignatureVerified, SigningAlgorithm, JwkPublic), SignatureError> {
122        // !!! Start of direct access to not-yet-integrity-verified fields
123        let (verifier, alg, key) = self
124            .get_signature_verifier_and_public_key(issuer_public_key_lookup, get_signature_verifier)
125            .await?;
126        // !!! End of direct access to not-yet-integrity-verified fields
127
128        let unverified_jwt = self.0.jwt;
129        let jwt = verifier
130            .verify_jwt_signature(unverified_jwt, &key)
131            .foreign_boxed_err(|| SignatureError::InvalidJwtSignature)?;
132
133        let disclosures = self.0.disclosures;
134
135        Ok((
136            SdJwtSignatureVerified(ParsedSdJwtIssuance { jwt, disclosures }),
137            alg,
138            key,
139        ))
140    }
141
142    /// Directly access not-yet-integrity-verified fields in order to look up the
143    /// public key and signature verifier implementation.
144    ///
145    /// This is sound because:
146    ///
147    /// - The issuer public key lookup implementation must be (by contract) such that it only
148    ///   obtains public keys from trusted sources, so an attacker cannot point us to completely
149    ///   arbitrary public keys, but only those whose corresponding private keys are deemed not
150    ///   under control of attackers;
151    ///
152    /// - The signature verifier implementations are all for known and
153    ///   known-secure asymmetric signature algorithms, i.e. there is no
154    ///   possibility of e.g. `alg: none`.
155    ///   At worst the attacker could:
156    ///
157    ///   - point us to a different secure algorithm which doesn't correspond
158    ///     to the public key's and will as such result in a verification error
159    ///     within `verify_with_key`, or
160    ///
161    ///   - point us to an algorithm which does match a _different_ (but still trusted)
162    ///     public key, using which the signature will not verify correctly for reasons
163    ///     mentioned above.
164    async fn get_signature_verifier_and_public_key<'a>(
165        &self,
166        issuer_public_key_lookup: &impl IssuerPublicKeyLookup,
167        get_signature_verifier: impl FnOnce(SigningAlgorithm) -> Option<&'a dyn SignatureVerifier>,
168    ) -> Result<(&'a dyn SignatureVerifier, SigningAlgorithm, JwkPublic), SignatureError> {
169        let key = issuer_public_key_lookup
170            .lookup(&self.0.jwt.claims().iss, self.0.jwt.header())
171            .await
172            .with_err(|| SignatureError::PublicKeyLookupFailed)?;
173
174        let alleged_signing_algorithm = self.0.jwt.header().alg;
175        let verifier = get_signature_verifier(alleged_signing_algorithm).ok_or_else(|| {
176            Error::root(SignatureError::MissingSignatureVerifier(
177                alleged_signing_algorithm,
178            ))
179        })?;
180
181        Ok((verifier, alleged_signing_algorithm, key))
182    }
183}
184
185impl crate::SdJwt {
186    /// Further parse the SD-JWT's JWT and disclosures into a to-be-verified form.
187    pub(crate) fn parse(&self) -> Result<SdJwtUnverified<'_>, FormatError> {
188        // Despite the documentation of `jwt::Token::parse_unverified`
189        // (rightfully) not recommending using it (to prevent reading the
190        // contents without prior verification), we need to do this in order to
191        // get access to the header and the `iss` claim which we will need for
192        // the public key lookup.
193
194        // NB: this call wants to borrow the jwt, rather than own it, while
195        // `SdJwt` takes ownership over the string it is parsed from, which is
196        // why this whole fn exists as a `&self` method on `SdJwt` rather than a
197        // `from_str` method on `IssuedSdJwtUnverified` so that the jwt string
198        // remains on the stack when calling this whole fn. Prehaps this could be
199        // fixed by making `SdJwt` generic over ownership/borrowing (e.g. via `Cow`)?
200        let jwt =
201            jwt::Token::parse_unverified(&self.jwt).foreign_err(|| FormatError::NonParseableJwt)?;
202
203        let disclosures = self
204            .disclosures
205            .iter()
206            .cloned()
207            .map(Disclosure::try_from)
208            .collect::<Result<Vec<_>, _>>()?;
209
210        Ok(SdJwtUnverified(ParsedSdJwtIssuance { jwt, disclosures }))
211    }
212
213    pub(crate) async fn to_signature_verified_sd_jwt<'a>(
214        &self,
215        issuer_public_key_lookup: &impl IssuerPublicKeyLookup,
216        get_signature_verifier: impl FnOnce(SigningAlgorithm) -> Option<&'a dyn SignatureVerifier>,
217    ) -> Result<(SdJwtSignatureVerified, SigningAlgorithm, JwkPublic), crate::Error> {
218        self.parse()
219            .match_err(|format_error| crate::Error::Format(format_error.clone()))?
220            .verify(issuer_public_key_lookup, get_signature_verifier)
221            .await
222            .match_err(|signature_error| crate::Error::Signature(signature_error.clone()))
223    }
224}
225
226/// The SD-JWT (in parsed form) verified against the issuer's public key, but not otherwise.
227///
228/// NB: keep the field private to this module to enforce correctness-by-construction!
229pub(crate) struct SdJwtSignatureVerified(ParsedSdJwtIssuance<jwt::Verified>);
230
231impl SdJwtSignatureVerified {
232    /// Decode the SD-JWT, reconstructing the original payload.
233    ///
234    /// CAUTION: do not expose or kb checks could be circumvented
235    pub(crate) fn into_decoded(
236        self,
237        get_hasher: impl Fn(HashingAlgorithm) -> Option<Box<dyn Hasher>>,
238    ) -> Result<SdJwtDecoded, crate::Error> {
239        SdJwtDecoded::new(self, get_hasher)
240    }
241}
242
243/// The SD-JWT (in parsed form) fully decoded and verified except for key binding.
244///
245/// NB: keep the fields private to this module to enforce correctness-by-construction!
246pub(crate) struct SdJwtDecoded {
247    decoded_claims: IssuerJwt,
248    disclosures_by_path: Yoke<DisclosureByPathTable<'static>, Vec<Disclosure>>,
249    hasher: Box<dyn Hasher>,
250}
251
252impl SdJwtDecoded {
253    pub(crate) fn new(
254        verified_sd_jwt: SdJwtSignatureVerified,
255        get_hasher: impl Fn(HashingAlgorithm) -> Option<Box<dyn Hasher>>,
256    ) -> Result<Self, crate::Error> {
257        let ParsedSdJwtIssuance { jwt, disclosures } = verified_sd_jwt.0;
258        // Put the whole payload into the decoder to automatically handle
259        // duplicate keys hidden in disclosures in the root object.
260        // TODO this could be optimized to not deep copy the whole claims during `to_object`
261        let full_payload = jwt.claims().to_object();
262
263        let mut owned_output = None;
264
265        // Let the constructed map borrow from the disclosure Vec
266        let disclosures_by_path = Yoke::try_attach_to_cart(disclosures, |disclosures| {
267            let (decoded_claims, hasher, disclosures_by_path) =
268                crate::decoder::decode_disclosed_claims(&full_payload, disclosures, get_hasher)
269                    .match_err(|err| crate::Error::Decoding(err.clone()))?;
270
271            owned_output = Some((decoded_claims, hasher));
272
273            Ok(disclosures_by_path)
274        })?;
275
276        let (decoded_claims, hasher) = owned_output.unwrap();
277
278        let decoded_claims = serde_json::from_value(decoded_claims.into())
279            .foreign_err(|| crate::Error::Format(FormatError::InvalidVcSchema))?;
280
281        Ok(Self {
282            decoded_claims,
283            disclosures_by_path,
284            hasher,
285        })
286    }
287
288    /// Pre-computed map from disclosure path in the reconstructed model to disclosure
289    pub(crate) fn disclosures_by_path(&self) -> &DisclosureByPathTable<'_> {
290        self.disclosures_by_path.get()
291    }
292
293    pub(crate) fn claims(&self) -> &IssuerJwt {
294        &self.decoded_claims
295    }
296
297    pub(crate) fn into_claims(self) -> IssuerJwt {
298        self.decoded_claims
299    }
300
301    pub(crate) fn key_binding_public_key(&self) -> &JwkPublic {
302        &self.decoded_claims.cnf.jwk
303    }
304
305    pub(crate) fn hasher(&self) -> &dyn Hasher {
306        &*self.hasher
307    }
308}
309
310// TODO(issues/55) unit tests (e.g. signature verification)