bh_jws_utils/
jwk.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::collections::HashSet;
17
18use crate::check_256bit_len;
19use crate::openssl_ec_priv_key_to_jwk;
20use crate::public_key_from_jwk_es256;
21use crate::CryptoError;
22use crate::EcPrivate;
23use crate::FormatError;
24use base64::engine::general_purpose::URL_SAFE_NO_PAD;
25use base64::Engine;
26use bherror::traits::ForeignError;
27use openssl::bn::BigNum;
28use openssl::ec::EcKey;
29use openssl::pkey::Private;
30use secrecy::ExposeSecret;
31use secrecy::SecretString;
32use serde::Deserializer;
33use serde::Serializer;
34use serde::{Deserialize, Serialize};
35use serde_json::{Map, Value};
36
37/// A JSON object meant to represent a public JWK.
38///
39/// Since this is a type alias, no aspects of the schema are enforced; this is
40/// left to any end-consumers of the public key, such as
41/// [`SignatureVerifier`](crate::SignatureVerifier).
42pub type JwkPublic = Map<String, Value>;
43
44/// Struct representing private JWK for keys which use elliptic curve algorithms
45/// It contains public JWK and a private key part `d` of JWK.
46///
47/// Note: Reason for having a special struct for private JWK is handling of
48///       private key part more carefully (storing in in `SecretString`).
49#[derive(Serialize, Deserialize)]
50pub struct EcJwkPrivate {
51    /// Public part of JWK. [RFC7518]
52    ///
53    /// [RFC7518]: https://datatracker.ietf.org/doc/html/rfc7518#section-6.2.1
54    #[serde(flatten)]
55    pub jwk_public: JwkPublic,
56    /// Private key part of JWK - "d" parameter containing the Elliptic
57    /// Curve private key value. [RFC7518]
58    ///
59    /// [RFC7518]: https://datatracker.ietf.org/doc/html/rfc7518#section-6.2.2.1
60    #[serde(
61        rename = "d",
62        serialize_with = "serialize_secret_string",
63        deserialize_with = "deserialize_secret_string"
64    )]
65    pub private_key_part: SecretString,
66}
67
68fn serialize_secret_string<S>(secret: &SecretString, serializer: S) -> Result<S::Ok, S::Error>
69where
70    S: Serializer,
71{
72    serializer.serialize_str(secret.expose_secret())
73}
74
75fn deserialize_secret_string<'de, D>(deserializer: D) -> Result<SecretString, D::Error>
76where
77    D: Deserializer<'de>,
78{
79    let s = String::deserialize(deserializer)?;
80    Ok(SecretString::from(s))
81}
82
83impl EcJwkPrivate {
84    /// Constructs a JWK JSON object for provided **private** key.
85    /// **Note**: only ECDSA keys using P-256 curve are supported!
86    pub fn from_openssl(private_key: &EcPrivate) -> bherror::Result<Self, CryptoError> {
87        openssl_ec_priv_key_to_jwk(private_key, None)
88    }
89
90    /// Constructs a `EcKey<Private>` from private JWK.
91    pub fn to_openssl(&self) -> bherror::Result<EcKey<Private>, FormatError> {
92        let d = URL_SAFE_NO_PAD
93            .decode(self.private_key_part.expose_secret())
94            .foreign_err(|| {
95                FormatError::JwkParsingFailed("decoding private key part failed".to_string())
96            })?;
97        let d = BigNum::from_slice(check_256bit_len(&d)?).foreign_err(|| {
98            FormatError::JwkParsingFailed("Failed to construct BigNum".to_string())
99        })?;
100
101        let public_key = public_key_from_jwk_es256(&self.jwk_public)?;
102
103        EcPrivate::from_private_components(public_key.group(), d.as_ref(), public_key.public_key())
104            .foreign_err(|| {
105                FormatError::JwkParsingFailed("private key construction failed".to_string())
106            })
107    }
108}
109
110/// Models JWK Set. A JSON object that represents a set of JWKs.
111///
112/// If any of the JWKs in the JWK Set have parameter `kid` then all of them
113/// should have `kid` parameter and different keys within the JWK Set SHOULD use
114/// distinct `kid` values.
115///
116/// NOTE: The notion of different keys can be somewhat subtle. The [RFC] gives
117/// the following example - different keys might use the same `kid` value if
118/// they have different "kty" (key type) values but are considered to be
119/// equivalent alternatives by the application using them. This implementation
120/// currently does not support this example, uniqueness of keys is checked if
121/// they contain `kid` values and equality between them is checked using only
122/// `kid` values.
123///
124/// For more details see [RFC7517][RFC].
125///
126/// [RFC]: https://datatracker.ietf.org/doc/html/rfc7517#section-5
127#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
128#[serde(try_from = "JwkSetUnverified")]
129pub struct JwkSet {
130    // TODO(issues/13): keys member variable should not be public because it could be modified such
131    //                  that JwkSet is an invalid one
132    /// Underlying representation of the JWK Set.
133    pub keys: Vec<JwkPublic>,
134}
135
136impl JwkSet {
137    /// Create empty `JwkSet`.
138    pub fn empty() -> Self {
139        JwkSet { keys: vec![] }
140    }
141}
142
143/// This is a "shadow" type whose sole purpose of existence is to be able to
144/// verify validity of deserialized [JwkSet] without writing deserialization
145/// manually. This is achieved with misuse of `TryFrom` trait. For more info see
146/// this [github issue].
147///
148/// [github issue]: https://github.com/serde-rs/serde/issues/642
149#[derive(Deserialize, Debug)]
150struct JwkSetUnverified {
151    // TODO(issues/13) change this to set if it does not include a lot of work
152    keys: Vec<JwkPublic>,
153}
154
155impl TryFrom<JwkSetUnverified> for JwkSet {
156    type Error = &'static str;
157
158    fn try_from(value: JwkSetUnverified) -> std::result::Result<Self, Self::Error> {
159        let keys = value.keys;
160        let jwk_with_kid_cnt = keys.iter().filter(|jwk| jwk.contains_key("kid")).count();
161
162        if jwk_with_kid_cnt == 0 {
163            return Ok(JwkSet { keys });
164        }
165        if jwk_with_kid_cnt != keys.len() {
166            return Err("Some of the provided JWKs contain kid parameter values and some don't");
167        }
168
169        let mut uniq = HashSet::new();
170        for key in keys.iter() {
171            if !uniq.insert(
172                key.get("kid")
173                    .unwrap() // safe unwrap because of all jwks contain `kid` value
174                    .as_str()
175                    .ok_or("JWK contains a `kid` parameter that is not a string")?,
176            ) {
177                return Err("Provided JWKs contain duplicate kid parameter values");
178            }
179        }
180
181        Ok(JwkSet { keys })
182    }
183}
184
185#[cfg(test)]
186pub(crate) mod tests {
187    use serde_json::json;
188
189    use crate::JwkSet;
190
191    // https://datatracker.ietf.org/doc/html/rfc7517#appendix-A.1
192    #[test]
193    fn jwk_set_example_serialization() {
194        let jwk_set = json!({"keys":
195          [
196            {"kty":"EC",
197             "crv":"P-256",
198             "x":"MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4",
199             "y":"4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM",
200             "use":"enc",
201             "kid":"1"},
202
203            {"kty":"RSA",
204             "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx
205      4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMs
206      tn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2
207      QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbI
208      SD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqb
209      w0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw",
210             "e":"AQAB",
211             "alg":"RS256",
212             "kid":"2011-04-29"}
213          ]
214        });
215
216        let deserialized = serde_json::from_str::<JwkSet>(jwk_set.to_string().as_str()).unwrap();
217        let serialized = serde_json::to_string(&deserialized).unwrap();
218
219        let keys = deserialized.keys;
220        assert_eq!(
221            keys.first().unwrap().get("kid").unwrap().as_str().unwrap(),
222            "1"
223        );
224        assert_eq!(
225            keys.get(1).unwrap().get("kid").unwrap().as_str().unwrap(),
226            "2011-04-29"
227        );
228        assert_eq!(serialized, jwk_set.to_string().as_str());
229    }
230
231    #[test]
232    fn invalid_jwk_set_duplicate_kid() {
233        let jwk_set = json!({"keys":
234          [
235            { "kid": "1" },
236            { "kid": "1" }
237          ]
238        });
239
240        let error = serde_json::from_str::<JwkSet>(jwk_set.to_string().as_str());
241
242        assert_eq!(
243            error.unwrap_err().to_string(),
244            "Provided JWKs contain duplicate kid parameter values"
245        );
246    }
247
248    #[test]
249    fn jwk_without_kid() {
250        let jwk_set = json!({"keys":
251          [
252            { "key": "1" },
253            { "key": "1" }
254          ]
255        });
256
257        let keys = serde_json::from_str::<JwkSet>(jwk_set.to_string().as_str())
258            .unwrap()
259            .keys;
260
261        assert_eq!(
262            keys.first().unwrap().get("key").unwrap().as_str().unwrap(),
263            "1"
264        );
265        assert_eq!(
266            keys.get(1).unwrap().get("key").unwrap().as_str().unwrap(),
267            "1"
268        );
269    }
270
271    #[test]
272    fn invalid_jwk_set_some_jwks_without_kid() {
273        let jwk_set = json!({"keys":
274          [
275            { "kid": "1" },
276            { "key": "1" }
277          ]
278        });
279
280        let error = serde_json::from_str::<JwkSet>(jwk_set.to_string().as_str());
281
282        assert_eq!(
283            error.unwrap_err().to_string(),
284            "Some of the provided JWKs contain kid parameter values and some don't"
285        );
286    }
287}