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    /// [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}