Skip to main content

identity_kit/methods/
jwk.rs

1//! `did:jwk`: a DID that is a base64url-encoded JSON Web Key.
2//!
3//! The [did:jwk method] is a deterministic transformation of a public JWK into a DID document:
4//! the method-specific id is `base64url(JWK)`, the document has exactly one verification method,
5//! `#0`, of type `JsonWebKey2020`, and the verification relationships follow the key's `use`
6//! member (all five when it is absent, no `keyAgreement` for `sig`, only `keyAgreement` for
7//! `enc`). The method cannot update or deactivate.
8//!
9//! # Canonical form
10//!
11//! The method does not fix how the JWK is serialized, so one key has many valid `did:jwk`s. The
12//! specification names the JSON Canonicalization Scheme ([RFC 8785]) as helpful when the DID is
13//! not stored in its serialized form, and [`DidJwk::did`] uses it: members sorted, no
14//! whitespace. The result is the DID every other JCS implementation derives for the same key,
15//! including the specification's P-256 example. Resolution accepts any serialization, so a DID
16//! created another way still resolves; it is just not the one [`DidJwk::did`] produces. An
17//! application holding DIDs created another way, such as this crate's before 0.2, should keep
18//! those strings rather than derive them again.
19//!
20//! [RFC 8785]: https://www.rfc-editor.org/rfc/rfc8785
21//!
22//! [did:jwk method]: https://github.com/quartzjer/did-jwk/blob/main/spec.md
23
24use crate::did::{
25    Context, DEFAULT_MAX_LEN, Did, DidDocument, DidError, DocumentError, MethodType,
26    ResolutionError, ResolutionErrorKind, ResolutionOptions, ResolutionResult,
27    VerificationMaterial, VerificationMethod, VerificationRelationship,
28};
29use crate::jose::Jwk;
30use crate::resolver::{DidProducer, DidResolver, Hosting, MethodCapabilities, ProduceError};
31
32use super::check_accept;
33
34/// The `did:jwk` method: a [`DidResolver`] that expands the DID and a [`DidProducer`] that
35/// encodes one public key.
36///
37/// # Examples
38///
39/// ```
40/// use identity_kit::did::{Did, VerificationRelationship};
41/// use identity_kit::jose::Jwk;
42/// use identity_kit::methods::DidJwk;
43///
44/// let jwk: Jwk = serde_json::from_str(
45///     r#"{"kty":"OKP","crv":"Ed25519","x":"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"}"#,
46/// )?;
47/// let did = DidJwk::did(&jwk)?;
48/// assert!(did.as_str().starts_with("did:jwk:eyJ"));
49///
50/// let document = DidJwk::document(&did)?;
51/// let key = did.join("#0")?;
52/// let method = document.resolve_verification_method(&key, Some(VerificationRelationship::Authentication))?;
53/// assert_eq!(method.public_key_jwk(), Some(&jwk));
54/// # Ok::<(), Box<dyn std::error::Error>>(())
55/// ```
56#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
57pub struct DidJwk;
58
59impl DidJwk {
60    /// The method name, `jwk`.
61    pub const METHOD: &'static str = "jwk";
62
63    /// The fragment of the one verification method, `0`.
64    pub const FRAGMENT: &'static str = "0";
65
66    /// The longest `did:jwk` whose document this method can represent: the verification
67    /// method id `did#0` must fit [`DEFAULT_MAX_LEN`] bytes.
68    pub const MAX_DID_LEN: usize = DEFAULT_MAX_LEN - 1 - Self::FRAGMENT.len();
69
70    /// The `@context` of every `did:jwk` document.
71    pub const CONTEXT: [&'static str; 2] = [
72        Context::DID_V1,
73        "https://w3id.org/security/suites/jws-2020/v1",
74    ];
75
76    /// The `did:jwk` of the public part of `jwk`: `base64url` of its JSON Canonicalization
77    /// Scheme form ([RFC 8785]), so members are sorted, and extension members too, recursively.
78    /// Private parameters never reach the DID.
79    ///
80    /// Any other serialization of the same key is another valid DID, which
81    /// [`document`](Self::document) resolves but this function never produces; see the
82    /// [module documentation](self#canonical-form).
83    ///
84    /// # Errors
85    ///
86    /// - [`DidError::TooLong`] when the encoded key exceeds [`MAX_DID_LEN`](Self::MAX_DID_LEN)
87    ///   bytes, as a JWK carrying an `x5c` chain or large extension members may; base64url is
88    ///   otherwise valid DID syntax.
89    /// - [`DidError::UnsupportedNumber`] when an extension member holds a number with no finite
90    ///   double value, which only `serde_json`'s `arbitrary_precision` feature keeps.
91    ///
92    /// # Examples
93    ///
94    /// ```
95    /// use identity_kit::jose::Jwk;
96    /// use identity_kit::methods::DidJwk;
97    ///
98    /// // The key of the did:jwk specification's P-256 example, members in any order: its DID
99    /// // is the specification's, which is in JCS order.
100    /// let jwk: Jwk = serde_json::from_str(
101    ///     r#"{
102    ///       "kty": "EC",
103    ///       "crv": "P-256",
104    ///       "x": "acbIQiuMs3i8_uszEjJ2tpTtRM4EU3yz91PH6CdH2V0",
105    ///       "y": "_KcyLj9vWMptnmKtm46GqDz8wf74I5LKgrl2GzH3nSE"
106    ///     }"#,
107    /// )?;
108    /// assert_eq!(
109    ///     DidJwk::did(&jwk)?.as_str(),
110    ///     "did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9"
111    /// );
112    /// # Ok::<(), Box<dyn std::error::Error>>(())
113    /// ```
114    ///
115    /// # Panics
116    ///
117    /// Never: a [`Jwk`] always serializes to JSON.
118    ///
119    /// [RFC 8785]: https://www.rfc-editor.org/rfc/rfc8785
120    pub fn did(jwk: &Jwk) -> Result<Did, DidError> {
121        let value = serde_json::to_value(jwk.public()).expect("a JWK serializes");
122        let json =
123            super::jcs::to_string(&value).map_err(|unsupported| DidError::UnsupportedNumber {
124                number: unsupported.0,
125            })?;
126        let id = crate::jose::base64url::encode(json.as_bytes());
127        Did::parse_with_max_len(&format!("did:jwk:{id}"), Self::MAX_DID_LEN)
128    }
129
130    /// The DID document `did` expands to (the method's Read operation).
131    ///
132    /// # Errors
133    ///
134    /// A [`ResolutionError`] whose kind is:
135    ///
136    /// - [`MethodNotSupported`](ResolutionErrorKind::MethodNotSupported) when `did` is not a
137    ///   `did:jwk`.
138    /// - [`InvalidDid`](ResolutionErrorKind::InvalidDid) when the method-specific id is not
139    ///   base64url of a JSON Web Key, the key carries private parameters, or the DID is longer
140    ///   than [`MAX_DID_LEN`](Self::MAX_DID_LEN). The detail says which, and the base64url or
141    ///   JSON error is the source.
142    pub fn document(did: &Did) -> Result<DidDocument, ResolutionError> {
143        let invalid = |detail: &str| {
144            ResolutionError::new(ResolutionErrorKind::InvalidDid).with_detail(detail)
145        };
146        if did.method() != Self::METHOD {
147            return Err(
148                ResolutionError::new(ResolutionErrorKind::MethodNotSupported)
149                    .with_detail(format!("`{did}` is not a did:jwk")),
150            );
151        }
152        if did.as_str().len() > Self::MAX_DID_LEN {
153            return Err(invalid(&format!(
154                "the DID is longer than {} bytes",
155                Self::MAX_DID_LEN
156            )));
157        }
158        let json = crate::jose::base64url::decode(did.method_specific_id()).map_err(|error| {
159            invalid("the method-specific id is not base64url").with_source(error)
160        })?;
161        let jwk: Jwk = serde_json::from_slice(&json).map_err(|error| {
162            invalid("the method-specific id does not encode a JSON Web Key").with_source(error)
163        })?;
164        if jwk.is_private() {
165            return Err(invalid("the JSON Web Key carries private parameters"));
166        }
167        build(did, jwk)
168            .map_err(|error| invalid("the JSON Web Key yields no DID document").with_source(error))
169    }
170}
171
172/// The document of `did` for `jwk`, whose relationships follow `use`.
173fn build(did: &Did, jwk: Jwk) -> Result<DidDocument, DocumentError> {
174    let relationships: &[VerificationRelationship] = match jwk.key_use() {
175        Some("sig") => &[
176            VerificationRelationship::Authentication,
177            VerificationRelationship::AssertionMethod,
178            VerificationRelationship::CapabilityInvocation,
179            VerificationRelationship::CapabilityDelegation,
180        ],
181        Some("enc") => &[VerificationRelationship::KeyAgreement],
182        _ => VerificationRelationship::ALL,
183    };
184    let method = VerificationMethod::new(
185        did.to_url().with_fragment(DidJwk::FRAGMENT),
186        MethodType::JSON_WEB_KEY_2020,
187        did.clone(),
188        VerificationMaterial::PublicKeyJwk(Box::new(jwk)),
189    );
190    DidDocument::builder(did.clone())
191        .context(Context::uris(DidJwk::CONTEXT))
192        .verification_method(method, relationships.iter().copied())
193        .build()
194}
195
196#[async_trait::async_trait]
197impl DidResolver for DidJwk {
198    fn method(&self) -> &'static str {
199        Self::METHOD
200    }
201
202    /// Expands `did` with [`DidJwk::document`]; `options` may only ask for a JSON
203    /// representation.
204    async fn resolve(
205        &self,
206        did: &Did,
207        options: &ResolutionOptions,
208    ) -> Result<ResolutionResult, ResolutionError> {
209        check_accept(options)?;
210        Self::document(did).map(ResolutionResult::new)
211    }
212}
213
214#[async_trait::async_trait]
215impl DidProducer for DidJwk {
216    type Params = ();
217
218    fn method(&self) -> &'static str {
219        Self::METHOD
220    }
221
222    /// Embedded, one key, never updated, verification method `#0`.
223    fn capabilities(&self) -> MethodCapabilities {
224        MethodCapabilities::new(Hosting::Embedded)
225            .with_single_key(true)
226            .with_fixed_fragment(Self::FRAGMENT)
227    }
228
229    /// The document [`DidJwk::did`] of the one key in `keys` resolves to, which holds the key in
230    /// its canonical form.
231    async fn produce(&self, keys: &[Jwk], _: ()) -> Result<DidDocument, ProduceError> {
232        let jwk = super::single_key(keys)?;
233        let did = Self::did(jwk).map_err(|error| ProduceError::UnsupportedKey {
234            index: 0,
235            reason: "it has no `did:jwk`".to_owned(),
236            source: Some(Box::new(error)),
237        })?;
238        // The document of the key as the DID encodes it, so that it is the document the DID
239        // resolves to: JCS writes `1.0` as `1`, which reads back as an integer.
240        Self::document(&did).map_err(|error| ProduceError::UnsupportedKey {
241            index: 0,
242            reason: "its `did:jwk` does not resolve".to_owned(),
243            source: Some(Box::new(error)),
244        })
245    }
246}