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}