jwt_compact/
jwk.rs

1//! Basic support of [JSON Web Keys](https://tools.ietf.org/html/rfc7517.html) (JWK).
2//!
3//! The functionality defined in this module allows converting between
4//! the [generic JWK format](JsonWebKey) and key presentation specific for the crypto backend.
5//! [`JsonWebKey`]s can be (de)serialized using [`serde`] infrastructure, and can be used
6//! to compute key thumbprint as per [RFC 7638].
7//!
8//! [`serde`]: https://crates.io/crates/serde
9//! [RFC 7638]: https://tools.ietf.org/html/rfc7638
10//!
11//! # Examples
12//!
13//! ```
14//! use jwt_compact::{alg::Hs256Key, jwk::JsonWebKey};
15//! use sha2::Sha256;
16//!
17//! # fn main() -> anyhow::Result<()> {
18//! // Load a key from the JWK presentation.
19//! let json_str = r#"
20//!     { "kty": "oct", "k": "t-bdv41MJXExXnpquHBuDn7n1YGyX7gLQchVHAoNu50" }
21//! "#;
22//! let jwk: JsonWebKey<'_> = serde_json::from_str(json_str)?;
23//! let key = Hs256Key::try_from(&jwk)?;
24//!
25//! // Convert `key` back to JWK.
26//! let jwk_from_key = JsonWebKey::from(&key);
27//! assert_eq!(jwk_from_key, jwk);
28//! println!("{}", serde_json::to_string(&jwk)?);
29//!
30//! // Compute the key thumbprint.
31//! let thumbprint = jwk_from_key.thumbprint::<Sha256>();
32//! # Ok(())
33//! # }
34//! ```
35
36use alloc::{
37    borrow::Cow,
38    string::{String, ToString},
39    vec::Vec,
40};
41use core::fmt;
42
43use serde::{Deserialize, Deserializer, Serialize, Serializer};
44use sha2::digest::{Digest, Output};
45
46use crate::alg::SecretBytes;
47
48/// Type of a [`JsonWebKey`].
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
50#[non_exhaustive]
51pub enum KeyType {
52    /// Public or private RSA key. Corresponds to the `RSA` value of the `kty` field for JWKs.
53    Rsa,
54    /// Public or private key in an ECDSA crypto system. Corresponds to the `EC` value
55    /// of the `kty` field for JWKs.
56    EllipticCurve,
57    /// Symmetric key. Corresponds to the `oct` value of the `kty` field for JWKs.
58    Symmetric,
59    /// Generic asymmetric keypair. Corresponds to the `OKP` value of the `kty` field for JWKs.
60    KeyPair,
61}
62
63impl fmt::Display for KeyType {
64    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
65        formatter.write_str(match self {
66            Self::Rsa => "RSA",
67            Self::EllipticCurve => "EC",
68            Self::Symmetric => "oct",
69            Self::KeyPair => "OKP",
70        })
71    }
72}
73
74/// Errors that can occur when transforming a [`JsonWebKey`] into the presentation specific for
75/// a crypto backend, using the [`TryFrom`] trait.
76#[derive(Debug)]
77#[non_exhaustive]
78pub enum JwkError {
79    /// Required field is absent from JWK.
80    NoField(String),
81    /// Key type (the `kty` field) is not as expected.
82    UnexpectedKeyType {
83        /// Expected key type.
84        expected: KeyType,
85        /// Actual key type.
86        actual: KeyType,
87    },
88    /// JWK field has an unexpected value.
89    UnexpectedValue {
90        /// Field name.
91        field: String,
92        /// Expected value of the field.
93        expected: String,
94        /// Actual value of the field.
95        actual: String,
96    },
97    /// JWK field has an unexpected byte length.
98    UnexpectedLen {
99        /// Field name.
100        field: String,
101        /// Expected byte length of the field.
102        expected: usize,
103        /// Actual byte length of the field.
104        actual: usize,
105    },
106    /// Signing and verifying keys do not match.
107    MismatchedKeys,
108    /// Custom error specific to a crypto backend.
109    Custom(anyhow::Error),
110}
111
112impl fmt::Display for JwkError {
113    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
114        match self {
115            Self::UnexpectedKeyType { expected, actual } => {
116                write!(
117                    formatter,
118                    "unexpected key type: {actual} (expected {expected})"
119                )
120            }
121            Self::NoField(field) => write!(formatter, "field `{field}` is absent from JWK"),
122            Self::UnexpectedValue {
123                field,
124                expected,
125                actual,
126            } => {
127                write!(
128                    formatter,
129                    "field `{field}` has unexpected value (expected: {expected}, got: {actual})"
130                )
131            }
132            Self::UnexpectedLen {
133                field,
134                expected,
135                actual,
136            } => {
137                write!(
138                    formatter,
139                    "field `{field}` has unexpected length (expected: {expected}, got: {actual})"
140                )
141            }
142            Self::MismatchedKeys => {
143                formatter.write_str("private and public keys encoded in JWK do not match")
144            }
145            Self::Custom(err) => fmt::Display::fmt(err, formatter),
146        }
147    }
148}
149
150impl core::error::Error for JwkError {
151    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
152        match self {
153            Self::Custom(err) => Some(err.as_ref()),
154            _ => None,
155        }
156    }
157}
158
159impl JwkError {
160    /// Creates a `Custom` error variant.
161    pub fn custom(err: impl Into<anyhow::Error>) -> Self {
162        Self::Custom(err.into())
163    }
164
165    pub(crate) fn key_type(jwk: &JsonWebKey<'_>, expected: KeyType) -> Self {
166        let actual = jwk.key_type();
167        debug_assert_ne!(actual, expected);
168        Self::UnexpectedKeyType { actual, expected }
169    }
170}
171
172impl Serialize for SecretBytes<'_> {
173    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
174        base64url::serialize(self.as_ref(), serializer)
175    }
176}
177
178impl<'de> Deserialize<'de> for SecretBytes<'_> {
179    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
180        base64url::deserialize(deserializer).map(SecretBytes::new)
181    }
182}
183
184/// Basic [JWK] functionality: (de)serialization and creating thumbprints.
185///
186/// See [RFC 7518] for the details about the fields for various key types.
187///
188/// [`Self::thumbprint()`] and the [`Display`](fmt::Display) implementation
189/// allow to get the overall presentation of the key. The latter returns JSON serialization
190/// of the key with fields ordered alphabetically. That is, this output for verifying keys
191/// can be used to compute key thumbprints.
192///
193/// # Serialization
194///
195/// For human-readable formats (e.g., JSON, TOML, YAML), byte fields in `JsonWebKey`
196/// and embedded types ([`SecretBytes`], [`RsaPrivateParts`], [`RsaPrimeFactor`]) will be
197/// serialized in base64-url encoding with no padding, as per the JWK spec.
198/// For other formats (e.g., CBOR), byte fields will be serialized as byte sequences.
199///
200/// Because of [the limitations](https://github.com/pyfisch/cbor/issues/3)
201/// of the CBOR support in `serde`, a `JsonWebKey` serialized in CBOR is **not** compliant
202/// with the [CBOR Object Signing and Encryption spec][COSE] (COSE). It can still be a good
203/// way to decrease the serialized key size.
204///
205/// # Conversions
206///
207/// A JWK can be obtained from signing and verifying keys defined in the [`alg`](crate::alg)
208/// module via [`From`] / [`Into`] traits. Conversion from a JWK to a specific key is fallible
209/// and can be performed via [`TryFrom`] with [`JwkError`] as an error
210/// type.
211///
212/// As a part of conversion for asymmetric signing keys, it is checked whether
213/// the signing and verifying parts of the JWK match; [`JwkError::MismatchedKeys`] is returned
214/// otherwise. This check is **not** performed for verifying keys even if the necessary data
215/// is present in the provided JWK.
216///
217/// ⚠ **Warning.** Conversions for private RSA keys are not fully compliant with [RFC 7518].
218/// See the docs for the relevant `impl`s for more details.
219///
220/// [RFC 7518]: https://tools.ietf.org/html/rfc7518#section-6
221/// [JWK]: https://tools.ietf.org/html/rfc7517.html
222/// [COSE]: https://tools.ietf.org/html/rfc8152
223#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
224#[serde(tag = "kty")]
225#[non_exhaustive]
226pub enum JsonWebKey<'a> {
227    /// Public or private RSA key. Has `kty` field set to `RSA`.
228    #[serde(rename = "RSA")]
229    Rsa {
230        /// Key modulus (`n`).
231        #[serde(rename = "n", with = "base64url")]
232        modulus: Cow<'a, [u8]>,
233        /// Public exponent (`e`).
234        #[serde(rename = "e", with = "base64url")]
235        public_exponent: Cow<'a, [u8]>,
236        /// Private RSA parameters. Only present for private keys.
237        #[serde(flatten)]
238        private_parts: Option<RsaPrivateParts<'a>>,
239    },
240    /// Public or private key in an ECDSA crypto system. Has `kty` field set to `EC`.
241    #[serde(rename = "EC")]
242    EllipticCurve {
243        /// Curve name (`crv`), such as `secp256k1`.
244        #[serde(rename = "crv")]
245        curve: Cow<'a, str>,
246        /// `x` coordinate of the curve point.
247        #[serde(with = "base64url")]
248        x: Cow<'a, [u8]>,
249        /// `y` coordinate of the curve point.
250        #[serde(with = "base64url")]
251        y: Cow<'a, [u8]>,
252        /// Secret scalar (`d`); not present for public keys.
253        #[serde(rename = "d", default, skip_serializing_if = "Option::is_none")]
254        secret: Option<SecretBytes<'a>>,
255    },
256    /// Generic symmetric key, e.g. for `HS256` algorithm. Has `kty` field set to `oct`.
257    #[serde(rename = "oct")]
258    Symmetric {
259        /// Bytes representing this key.
260        #[serde(rename = "k")]
261        secret: SecretBytes<'a>,
262    },
263    /// Generic asymmetric keypair. This key type is used e.g. for Ed25519 keys.
264    #[serde(rename = "OKP")]
265    KeyPair {
266        /// Curve name (`crv`), such as `Ed25519`.
267        #[serde(rename = "crv")]
268        curve: Cow<'a, str>,
269        /// Public key. For Ed25519, this is the standard 32-byte public key presentation
270        /// (`x` coordinate of a point on the curve + sign).
271        #[serde(with = "base64url")]
272        x: Cow<'a, [u8]>,
273        /// Secret key (`d`). For Ed25519, this is the seed.
274        #[serde(rename = "d", default, skip_serializing_if = "Option::is_none")]
275        secret: Option<SecretBytes<'a>>,
276    },
277}
278
279impl JsonWebKey<'_> {
280    /// Gets the type of this key.
281    pub fn key_type(&self) -> KeyType {
282        match self {
283            Self::Rsa { .. } => KeyType::Rsa,
284            Self::EllipticCurve { .. } => KeyType::EllipticCurve,
285            Self::Symmetric { .. } => KeyType::Symmetric,
286            Self::KeyPair { .. } => KeyType::KeyPair,
287        }
288    }
289
290    /// Returns `true` if this key can be used for signing (has [`SecretBytes`] fields).
291    pub fn is_signing_key(&self) -> bool {
292        match self {
293            Self::Rsa { private_parts, .. } => private_parts.is_some(),
294            Self::EllipticCurve { secret, .. } | Self::KeyPair { secret, .. } => secret.is_some(),
295            Self::Symmetric { .. } => true,
296        }
297    }
298
299    /// Returns a copy of this key with parts not necessary for signature verification removed.
300    #[must_use]
301    pub fn to_verifying_key(&self) -> Self {
302        match self {
303            Self::Rsa {
304                modulus,
305                public_exponent,
306                ..
307            } => Self::Rsa {
308                modulus: modulus.clone(),
309                public_exponent: public_exponent.clone(),
310                private_parts: None,
311            },
312
313            Self::EllipticCurve { curve, x, y, .. } => Self::EllipticCurve {
314                curve: curve.clone(),
315                x: x.clone(),
316                y: y.clone(),
317                secret: None,
318            },
319
320            Self::Symmetric { secret } => Self::Symmetric {
321                secret: secret.clone(),
322            },
323
324            Self::KeyPair { curve, x, .. } => Self::KeyPair {
325                curve: curve.clone(),
326                x: x.clone(),
327                secret: None,
328            },
329        }
330    }
331
332    /// Computes a thumbprint of this JWK. The result complies with the key thumbprint defined
333    /// in [RFC 7638].
334    ///
335    /// [RFC 7638]: https://tools.ietf.org/html/rfc7638
336    pub fn thumbprint<D: Digest>(&self) -> Output<D> {
337        let hashed_key = if self.is_signing_key() {
338            Cow::Owned(self.to_verifying_key())
339        } else {
340            Cow::Borrowed(self)
341        };
342        D::digest(hashed_key.to_string().as_bytes())
343    }
344}
345
346impl fmt::Display for JsonWebKey<'_> {
347    // TODO: Not the most efficient approach
348    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
349        let json_value = serde_json::to_value(self).expect("Cannot convert JsonWebKey to JSON");
350        let json_value = json_value.as_object().unwrap();
351        // ^ unwrap() is safe: `JsonWebKey` serialization is always an object.
352
353        let mut json_entries: Vec<_> = json_value.iter().collect();
354        json_entries.sort_unstable_by_key(|(x, _)| *x);
355
356        formatter.write_str("{")?;
357        let field_count = json_entries.len();
358        for (i, (name, value)) in json_entries.into_iter().enumerate() {
359            write!(formatter, "\"{name}\":{value}")?;
360            if i + 1 < field_count {
361                formatter.write_str(",")?;
362            }
363        }
364        formatter.write_str("}")
365    }
366}
367
368/// Parts of [`JsonWebKey::Rsa`] that are specific to private keys.
369///
370/// # Serialization
371///
372/// Fields of this struct are serialized using the big endian presentation
373/// with the minimum necessary number of bytes. See [`JsonWebKey` notes](JsonWebKey#serialization)
374/// on encoding.
375#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
376pub struct RsaPrivateParts<'a> {
377    /// Private exponent (`d`).
378    #[serde(rename = "d")]
379    pub private_exponent: SecretBytes<'a>,
380    /// First prime factor (`p`).
381    #[serde(rename = "p")]
382    pub prime_factor_p: SecretBytes<'a>,
383    /// Second prime factor (`q`).
384    #[serde(rename = "q")]
385    pub prime_factor_q: SecretBytes<'a>,
386    /// First factor CRT exponent (`dp`).
387    #[serde(rename = "dp", default, skip_serializing_if = "Option::is_none")]
388    pub p_crt_exponent: Option<SecretBytes<'a>>,
389    /// Second factor CRT exponent (`dq`).
390    #[serde(rename = "dq", default, skip_serializing_if = "Option::is_none")]
391    pub q_crt_exponent: Option<SecretBytes<'a>>,
392    /// CRT coefficient of the second factor (`qi`).
393    #[serde(rename = "qi", default, skip_serializing_if = "Option::is_none")]
394    pub q_crt_coefficient: Option<SecretBytes<'a>>,
395    /// Other prime factors.
396    #[serde(rename = "oth", default, skip_serializing_if = "Vec::is_empty")]
397    pub other_prime_factors: Vec<RsaPrimeFactor<'a>>,
398}
399
400/// Block for an additional prime factor in [`RsaPrivateParts`].
401///
402/// # Serialization
403///
404/// Fields of this struct are serialized using the big endian presentation
405/// with the minimum necessary number of bytes. See [`JsonWebKey` notes](JsonWebKey#serialization)
406/// on encoding.
407#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
408pub struct RsaPrimeFactor<'a> {
409    /// Prime factor (`r`).
410    #[serde(rename = "r")]
411    pub factor: SecretBytes<'a>,
412    /// Factor CRT exponent (`d`).
413    #[serde(rename = "d", default, skip_serializing_if = "Option::is_none")]
414    pub crt_exponent: Option<SecretBytes<'a>>,
415    /// Factor CRT coefficient (`t`).
416    #[serde(rename = "t", default, skip_serializing_if = "Option::is_none")]
417    pub crt_coefficient: Option<SecretBytes<'a>>,
418}
419
420#[cfg(any(
421    feature = "es256k",
422    feature = "k256",
423    feature = "p256",
424    feature = "exonum-crypto",
425    feature = "ed25519-dalek",
426    feature = "ed25519-compact"
427))]
428mod helpers {
429    use super::{JsonWebKey, JwkError};
430    use crate::{Algorithm, alg::SigningKey};
431
432    impl JsonWebKey<'_> {
433        pub(crate) fn ensure_curve(curve: &str, expected: &str) -> Result<(), JwkError> {
434            if curve == expected {
435                Ok(())
436            } else {
437                Err(JwkError::UnexpectedValue {
438                    field: "crv".into(),
439                    expected: expected.into(),
440                    actual: curve.into(),
441                })
442            }
443        }
444
445        pub(crate) fn ensure_len(
446            field: &str,
447            bytes: &[u8],
448            expected_len: usize,
449        ) -> Result<(), JwkError> {
450            if bytes.len() == expected_len {
451                Ok(())
452            } else {
453                Err(JwkError::UnexpectedLen {
454                    field: field.into(),
455                    expected: expected_len,
456                    actual: bytes.len(),
457                })
458            }
459        }
460
461        /// Ensures that the provided signing key matches the verifying key restored from the same JWK.
462        /// This is useful when implementing [`TryFrom`] conversion from `JsonWebKey` for private keys.
463        pub(crate) fn ensure_key_match<Alg, K>(&self, signing_key: K) -> Result<K, JwkError>
464        where
465            Alg: Algorithm<SigningKey = K>,
466            K: SigningKey<Alg>,
467            Alg::VerifyingKey: for<'jwk> TryFrom<&'jwk Self, Error = JwkError> + PartialEq,
468        {
469            let verifying_key = <Alg::VerifyingKey>::try_from(self)?;
470            if verifying_key == signing_key.to_verifying_key() {
471                Ok(signing_key)
472            } else {
473                Err(JwkError::MismatchedKeys)
474            }
475        }
476    }
477}
478
479mod base64url {
480    use alloc::{borrow::Cow, vec::Vec};
481    use core::fmt;
482
483    use base64ct::{Base64UrlUnpadded, Encoding};
484    use serde::{
485        Deserializer, Serializer,
486        de::{Error as DeError, Unexpected, Visitor},
487    };
488
489    pub fn serialize<S>(value: &[u8], serializer: S) -> Result<S::Ok, S::Error>
490    where
491        S: Serializer,
492    {
493        if serializer.is_human_readable() {
494            serializer.serialize_str(&Base64UrlUnpadded::encode_string(value))
495        } else {
496            serializer.serialize_bytes(value)
497        }
498    }
499
500    pub fn deserialize<'de, D>(deserializer: D) -> Result<Cow<'static, [u8]>, D::Error>
501    where
502        D: Deserializer<'de>,
503    {
504        struct Base64Visitor;
505
506        impl Visitor<'_> for Base64Visitor {
507            type Value = Vec<u8>;
508
509            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
510                formatter.write_str("base64url-encoded data")
511            }
512
513            fn visit_str<E: DeError>(self, value: &str) -> Result<Self::Value, E> {
514                Base64UrlUnpadded::decode_vec(value)
515                    .map_err(|_| E::invalid_value(Unexpected::Str(value), &self))
516            }
517
518            fn visit_bytes<E: DeError>(self, value: &[u8]) -> Result<Self::Value, E> {
519                Ok(value.to_vec())
520            }
521
522            fn visit_byte_buf<E: DeError>(self, value: Vec<u8>) -> Result<Self::Value, E> {
523                Ok(value)
524            }
525        }
526
527        struct BytesVisitor;
528
529        impl Visitor<'_> for BytesVisitor {
530            type Value = Vec<u8>;
531
532            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
533                formatter.write_str("byte buffer")
534            }
535
536            fn visit_bytes<E: DeError>(self, value: &[u8]) -> Result<Self::Value, E> {
537                Ok(value.to_vec())
538            }
539
540            fn visit_byte_buf<E: DeError>(self, value: Vec<u8>) -> Result<Self::Value, E> {
541                Ok(value)
542            }
543        }
544
545        let maybe_bytes = if deserializer.is_human_readable() {
546            deserializer.deserialize_str(Base64Visitor)
547        } else {
548            deserializer.deserialize_bytes(BytesVisitor)
549        };
550        maybe_bytes.map(Cow::Owned)
551    }
552}
553
554#[cfg(test)]
555mod tests {
556    use assert_matches::assert_matches;
557
558    use super::*;
559    use crate::alg::Hs256Key;
560
561    fn create_jwk() -> JsonWebKey<'static> {
562        JsonWebKey::KeyPair {
563            curve: Cow::Borrowed("Ed25519"),
564            x: Cow::Borrowed(b"test"),
565            secret: None,
566        }
567    }
568
569    #[test]
570    fn serializing_jwk() {
571        let jwk = create_jwk();
572
573        let json = serde_json::to_value(&jwk).unwrap();
574        assert_eq!(
575            json,
576            serde_json::json!({ "crv": "Ed25519", "kty": "OKP", "x": "dGVzdA" })
577        );
578
579        let restored: JsonWebKey<'_> = serde_json::from_value(json).unwrap();
580        assert_eq!(restored, jwk);
581    }
582
583    #[test]
584    fn jwk_deserialization_errors() {
585        let missing_field_json = r#"{"crv":"Ed25519"}"#;
586        let missing_field_err = serde_json::from_str::<JsonWebKey<'_>>(missing_field_json)
587            .unwrap_err()
588            .to_string();
589        assert!(
590            missing_field_err.contains("missing field `kty`"),
591            "{missing_field_err}"
592        );
593
594        let base64_json = r#"{"crv":"Ed25519","kty":"OKP","x":"??"}"#;
595        let base64_err = serde_json::from_str::<JsonWebKey<'_>>(base64_json)
596            .unwrap_err()
597            .to_string();
598        assert!(
599            base64_err.contains("invalid value: string \"??\""),
600            "{base64_err}"
601        );
602        assert!(
603            base64_err.contains("base64url-encoded data"),
604            "{base64_err}"
605        );
606    }
607
608    #[test]
609    fn extra_jwk_fields() {
610        #[derive(Debug, Serialize, Deserialize)]
611        struct ExtendedJsonWebKey<'a, T> {
612            #[serde(flatten)]
613            base: JsonWebKey<'a>,
614            #[serde(flatten)]
615            extra: T,
616        }
617
618        #[derive(Debug, Deserialize)]
619        struct Extra {
620            #[serde(rename = "kid")]
621            key_id: String,
622            #[serde(rename = "use")]
623            key_use: KeyUse,
624        }
625
626        #[derive(Debug, Deserialize, PartialEq)]
627        enum KeyUse {
628            #[serde(rename = "sig")]
629            Signature,
630            #[serde(rename = "enc")]
631            Encryption,
632        }
633
634        let json_str = r#"
635            { "kty": "oct", "kid": "my-unique-key", "k": "dGVzdA", "use": "sig" }
636        "#;
637        let jwk: ExtendedJsonWebKey<'_, Extra> = serde_json::from_str(json_str).unwrap();
638
639        assert_matches!(&jwk.base, JsonWebKey::Symmetric { secret } if secret.as_ref() == b"test");
640        assert_eq!(jwk.extra.key_id, "my-unique-key");
641        assert_eq!(jwk.extra.key_use, KeyUse::Signature);
642
643        let key = Hs256Key::try_from(&jwk.base).unwrap();
644        let jwk_from_key = JsonWebKey::from(&key);
645
646        assert_matches!(
647            jwk_from_key,
648            JsonWebKey::Symmetric { secret } if secret.as_ref() == b"test"
649        );
650    }
651
652    #[test]
653    #[cfg(feature = "ciborium")]
654    fn jwk_with_cbor() {
655        let key = JsonWebKey::KeyPair {
656            curve: Cow::Borrowed("Ed25519"),
657            x: Cow::Borrowed(b"public"),
658            secret: Some(SecretBytes::borrowed(b"private")),
659        };
660        let mut bytes = Vec::new();
661        ciborium::into_writer(&key, &mut bytes).unwrap();
662        assert!(bytes.windows(6).any(|window| window == b"public"));
663        assert!(bytes.windows(7).any(|window| window == b"private"));
664
665        let restored: JsonWebKey<'_> = ciborium::from_reader(&bytes[..]).unwrap();
666        assert_eq!(restored, key);
667    }
668}