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 /// [RFC 8785]: https://www.rfc-editor.org/rfc/rfc8785
116 pub fn did(jwk: &Jwk) -> Result<Did, DidError> {
117 let value = serde_json::to_value(jwk.public()).expect("a JWK serializes");
118 let json =
119 super::jcs::to_string(&value).map_err(|unsupported| DidError::UnsupportedNumber {
120 number: unsupported.0,
121 })?;
122 let id = crate::jose::base64url::encode(json.as_bytes());
123 Did::parse_with_max_len(&format!("did:jwk:{id}"), Self::MAX_DID_LEN)
124 }
125
126 /// The DID document `did` expands to (the method's Read operation).
127 ///
128 /// # Errors
129 ///
130 /// A [`ResolutionError`] whose kind is:
131 ///
132 /// - [`MethodNotSupported`](ResolutionErrorKind::MethodNotSupported) when `did` is not a
133 /// `did:jwk`.
134 /// - [`InvalidDid`](ResolutionErrorKind::InvalidDid) when the method-specific id is not
135 /// base64url of a JSON Web Key, the key carries private parameters, or the DID is longer
136 /// than [`MAX_DID_LEN`](Self::MAX_DID_LEN). The detail says which, and the base64url or
137 /// JSON error is the source.
138 pub fn document(did: &Did) -> Result<DidDocument, ResolutionError> {
139 let invalid = |detail: &str| {
140 ResolutionError::new(ResolutionErrorKind::InvalidDid).with_detail(detail)
141 };
142 if did.method() != Self::METHOD {
143 return Err(
144 ResolutionError::new(ResolutionErrorKind::MethodNotSupported)
145 .with_detail(format!("`{did}` is not a did:jwk")),
146 );
147 }
148 if did.as_str().len() > Self::MAX_DID_LEN {
149 return Err(invalid(&format!(
150 "the DID is longer than {} bytes",
151 Self::MAX_DID_LEN
152 )));
153 }
154 let json = crate::jose::base64url::decode(did.method_specific_id()).map_err(|error| {
155 invalid("the method-specific id is not base64url").with_source(error)
156 })?;
157 let jwk: Jwk = serde_json::from_slice(&json).map_err(|error| {
158 invalid("the method-specific id does not encode a JSON Web Key").with_source(error)
159 })?;
160 if jwk.is_private() {
161 return Err(invalid("the JSON Web Key carries private parameters"));
162 }
163 build(did, jwk)
164 .map_err(|error| invalid("the JSON Web Key yields no DID document").with_source(error))
165 }
166}
167
168/// The document of `did` for `jwk`, whose relationships follow `use`.
169fn build(did: &Did, jwk: Jwk) -> Result<DidDocument, DocumentError> {
170 let relationships: &[VerificationRelationship] = match jwk.key_use() {
171 Some("sig") => &[
172 VerificationRelationship::Authentication,
173 VerificationRelationship::AssertionMethod,
174 VerificationRelationship::CapabilityInvocation,
175 VerificationRelationship::CapabilityDelegation,
176 ],
177 Some("enc") => &[VerificationRelationship::KeyAgreement],
178 _ => VerificationRelationship::ALL,
179 };
180 let method = VerificationMethod::new(
181 did.to_url().with_fragment(DidJwk::FRAGMENT),
182 MethodType::JSON_WEB_KEY_2020,
183 did.clone(),
184 VerificationMaterial::PublicKeyJwk(Box::new(jwk)),
185 );
186 DidDocument::builder(did.clone())
187 .context(Context::uris(DidJwk::CONTEXT))
188 .verification_method(method, relationships.iter().copied())
189 .build()
190}
191
192#[async_trait::async_trait]
193impl DidResolver for DidJwk {
194 fn method(&self) -> &'static str {
195 Self::METHOD
196 }
197
198 /// Expands `did` with [`DidJwk::document`]; `options` may only ask for a JSON
199 /// representation.
200 async fn resolve(
201 &self,
202 did: &Did,
203 options: &ResolutionOptions,
204 ) -> Result<ResolutionResult, ResolutionError> {
205 check_accept(options)?;
206 Self::document(did).map(ResolutionResult::new)
207 }
208}
209
210#[async_trait::async_trait]
211impl DidProducer for DidJwk {
212 type Params = ();
213
214 fn method(&self) -> &'static str {
215 Self::METHOD
216 }
217
218 /// Embedded, one key, never updated, verification method `#0`.
219 fn capabilities(&self) -> MethodCapabilities {
220 MethodCapabilities::new(Hosting::Embedded)
221 .with_single_key(true)
222 .with_fixed_fragment(Self::FRAGMENT)
223 }
224
225 /// The document [`DidJwk::did`] of the one key in `keys` resolves to, which holds the key in
226 /// its canonical form.
227 async fn produce(&self, keys: &[Jwk], _: ()) -> Result<DidDocument, ProduceError> {
228 let jwk = super::single_key(keys)?;
229 let unsupported = |reason: String| ProduceError::UnsupportedKey { index: 0, reason };
230 let did = Self::did(jwk).map_err(|error| unsupported(error.to_string()))?;
231 // The document of the key as the DID encodes it, so that it is the document the DID
232 // resolves to: JCS writes `1.0` as `1`, which reads back as an integer.
233 Self::document(&did).map_err(|error| unsupported(error.to_string()))
234 }
235}