did_ion/sidetree/
mod.rs

1use core::fmt;
2use std::borrow::Cow;
3
4use base64::Engine;
5use json_patch::Patch;
6use serde::{Deserialize, Serialize};
7use ssi_dids_core::{
8    document::{service::Endpoint as ServiceEndpoint, Service},
9    registration::{DIDDocumentOperation, DIDDocumentOperationKind, DIDTransactionCreationError},
10};
11use ssi_jwk::{Base64urlUInt, JWK};
12use ssi_verification_methods::ProofPurpose;
13
14mod client;
15mod did;
16mod operation;
17mod resolver;
18
19pub use client::*;
20pub use did::*;
21pub use operation::*;
22pub use resolver::*;
23
24const MULTIHASH_SHA2_256_PREFIX: &[u8] = &[0x12];
25const MULTIHASH_SHA2_256_SIZE: &[u8] = &[0x20];
26
27/// Verification method type for Create operation
28///
29/// This is used when converting JWK to [verification method map][vmm] for the Create operation.
30///
31/// Reference: [Sidetree §12.1.1 `add-public-keys`][apk] Step 3.2
32///
33/// [apk]: https://identity.foundation/sidetree/spec/v1.0.0/#add-public-keys
34/// [vmm]: https://www.w3.org/TR/did-core/#verification-methods
35pub const VERIFICATION_METHOD_TYPE: &str = "JsonWebSignature2020";
36
37#[derive(Debug, thiserror::Error)]
38#[error("key generation failed")]
39pub struct KeyGenerationFailed;
40
41#[derive(Debug, thiserror::Error)]
42pub enum CreateError {
43    #[error("same update and recovery keys")]
44    SameUpdateAndRecoveryKeys,
45
46    #[error(transparent)]
47    KeyGenerationFailed(#[from] KeyGenerationFailed),
48
49    #[error("invalid update key")]
50    InvalidUpdateKey,
51
52    #[error("invalid recovery key")]
53    InvalidRecoveryKey,
54}
55
56impl From<CreateError> for DIDTransactionCreationError {
57    fn from(value: CreateError) -> Self {
58        match value {
59            CreateError::SameUpdateAndRecoveryKeys => {
60                DIDTransactionCreationError::SameUpdateAndRecoveryKeys
61            }
62            CreateError::KeyGenerationFailed(_) => DIDTransactionCreationError::KeyGenerationFailed,
63            CreateError::InvalidUpdateKey => DIDTransactionCreationError::InvalidUpdateKey,
64            CreateError::InvalidRecoveryKey => DIDTransactionCreationError::InvalidRecoveryKey,
65        }
66    }
67}
68
69#[derive(Debug, thiserror::Error)]
70pub enum UpdateError {
71    #[error("invalid update key")]
72    InvalidUpdateKey,
73
74    #[error("update key unchanged")]
75    UpdateKeyUnchanged,
76
77    #[error("signature failed")]
78    SignatureFailed,
79}
80
81impl From<UpdateError> for DIDTransactionCreationError {
82    fn from(value: UpdateError) -> Self {
83        match value {
84            UpdateError::InvalidUpdateKey => Self::InvalidUpdateKey,
85            UpdateError::UpdateKeyUnchanged => Self::UpdateKeyUnchanged,
86            UpdateError::SignatureFailed => Self::SignatureFailed,
87        }
88    }
89}
90
91#[derive(Debug, thiserror::Error)]
92pub enum DeactivateError {
93    #[error("invalid recovery key")]
94    InvalidRecoveryKey,
95
96    #[error("signature failed")]
97    SignatureFailed,
98}
99
100impl From<DeactivateError> for DIDTransactionCreationError {
101    fn from(value: DeactivateError) -> Self {
102        match value {
103            DeactivateError::InvalidRecoveryKey => Self::InvalidRecoveryKey,
104            DeactivateError::SignatureFailed => Self::SignatureFailed,
105        }
106    }
107}
108
109#[derive(Debug, thiserror::Error)]
110pub enum RecoverError {
111    #[error("invalid recovery key")]
112    InvalidRecoveryKey,
113
114    #[error("recovery key unchanged")]
115    RecoveryKeyUnchanged,
116
117    #[error("signature failed")]
118    SignatureFailed,
119
120    #[error(transparent)]
121    KeyGenerationFailed(#[from] KeyGenerationFailed),
122}
123
124impl From<RecoverError> for DIDTransactionCreationError {
125    fn from(value: RecoverError) -> Self {
126        match value {
127            RecoverError::InvalidRecoveryKey => Self::InvalidRecoveryKey,
128            RecoverError::RecoveryKeyUnchanged => Self::RecoveryKeyUnchanged,
129            RecoverError::SignatureFailed => Self::SignatureFailed,
130            RecoverError::KeyGenerationFailed(_) => Self::KeyGenerationFailed,
131        }
132    }
133}
134
135/// Parameters for a Sidetree client implementation
136///
137/// This trait consistest of the subset of parameters defined in [Sidetree §5. Default Parameters][default-params] that are needed to implemented a Sidetree client, that is a client to the [Sidetree REST API][sidetree-rest].
138///
139/// [default-params]: https://identity.foundation/sidetree/spec/v1.0.0/#default-parameters
140/// [sidetree-rest]: https://identity.foundation/sidetree/api/
141pub trait Sidetree {
142    /// [`HASH_PROTOCOL`](https://identity.foundation/sidetree/spec/v1.0.0/#hash-protocol)
143    ///
144    /// This should be implemented using [hash_algorithm].
145    ///
146    /// Default implementation calls [hash_protocol_algorithm] and returns the concatenation of the
147    /// prefix and hash.
148    ///
149    /// This function must correspond with [hash_algorithm]. To ensure that correspondence,
150    /// implementers may want to override [hash_protocol_algorithm] instead of this function.
151    ///
152    /// [hash_algorithm]: Self::hash_algorithm
153    /// [hash_protocol_algorithm]: Self::hash_protocol_algorithm
154    fn hash_protocol(data: &[u8]) -> Vec<u8> {
155        let (prefix, hash) = Self::hash_protocol_algorithm(data);
156        [prefix, hash].concat()
157    }
158
159    /// [`HASH_ALGORITHM`](https://identity.foundation/sidetree/spec/v1.0.0/#hash-algorithm)
160    ///
161    /// Default implementation calls [hash_protocol_algorithm] and returns the hash, discarding the
162    /// prefix.
163    ///
164    /// This function must correspond with [hash_protocol]. To ensure that correspondence,
165    /// implementers may want to override [hash_protocol_algorithm] instead of this function.
166    ///
167    /// [hash_protocol]: Self::hash_protocol
168    /// [hash_protocol_algorithm]: Self::hash_protocol_algorithm
169    fn hash_algorithm(data: &[u8]) -> Vec<u8> {
170        let (_prefix, hash) = Self::hash_protocol_algorithm(data);
171        hash
172    }
173
174    /// Combination of [hash_protocol] and [hash_algorithm]
175    ///
176    /// Returns multihash prefix and hash.
177    ///
178    /// Default implementation: SHA-256 (`sha2-256`)
179    ///
180    /// [hash_protocol] and [hash_algorithm] must correspond, and their default implementations
181    /// call this function ([hash_protocol_algorithm]). Implementers are therefore encouraged to
182    /// overwrite this function ([hash_protocol_algorithm]) rather than those ([hash_protocol] and
183    /// [hash_algorithm]).
184    ///
185    /// [hash_protocol]: Self::hash_protocol
186    /// [hash_algorithm]: Self::hash_algorithm
187    /// [hash_protocol_algorithm]: Self::hash_protocol_algorithm
188    fn hash_protocol_algorithm(data: &[u8]) -> (Vec<u8>, Vec<u8>) {
189        use sha2::{Digest, Sha256};
190        let mut hasher = Sha256::new();
191        hasher.update(data);
192        let hash = hasher.finalize().to_vec();
193        (
194            [MULTIHASH_SHA2_256_PREFIX, MULTIHASH_SHA2_256_SIZE].concat(),
195            hash,
196        )
197    }
198
199    /// [`DATA_ENCODING_SCHEME`](https://identity.foundation/sidetree/spec/v1.0.0/#data-encoding-scheme)
200    fn data_encoding_scheme(data: &[u8]) -> String {
201        base64::prelude::BASE64_URL_SAFE_NO_PAD.encode(data)
202    }
203
204    /// Generate a new keypair ([KEY_ALGORITHM][ka])
205    ///
206    /// [ka]: https://identity.foundation/sidetree/spec/v1.0.0/#key-algorithm
207    fn generate_key() -> JWK;
208
209    /// Ensure that a keypair is valid for this Sidetree DID Method
210    ///
211    /// Check that the key uses this Sidetree DID method's [KEY_ALGORITHM][ka].
212    ///
213    /// [ka]: https://identity.foundation/sidetree/spec/v1.0.0/#key-algorithm
214    fn validate_key(key: &JWK) -> bool;
215
216    /// [`SIGNATURE_ALGORITHM`](https://identity.foundation/sidetree/spec/v1.0.0/#sig-algorithm) (JWS alg)
217    const SIGNATURE_ALGORITHM: ssi_jwk::Algorithm;
218
219    /// [`REVEAL_VALUE`](https://identity.foundation/sidetree/spec/v1.0.0/#reveal-value)
220    fn reveal_value(commitment_value: &[u8]) -> String {
221        // The spec implies that REVEAL_VALUE uses HASH_PROTOCOL, in §6.2.1:
222        //   "Use the implementation’s HASH_PROTOCOL to hash the canonicalized public key to generate the REVEAL_VALUE"
223        //   https://identity.foundation/sidetree/spec/v1.0.0/#public-key-commitment-scheme
224        let hash = Self::hash_protocol(commitment_value);
225        Self::data_encoding_scheme(&hash)
226    }
227
228    /// [`MAX_OPERATION_HASH_LENGTH`](https://identity.foundation/sidetree/spec/v1.0.0/#max-operation-hash-length)
229    const MAX_OPERATION_HASH_LENGTH: usize = 100;
230
231    /// [`NONCE_SIZE`](https://identity.foundation/sidetree/spec/v1.0.0/#nonce-size)
232    const NONCE_SIZE: usize = 16;
233
234    /// Method name for Sidetree-based DID
235    ///
236    /// Mentioned in [Sidetree §9. DID URI Composition](https://identity.foundation/sidetree/spec/v1.0.0/#did-uri-composition)
237    const METHOD: &'static str;
238
239    /// Network instance
240    ///
241    /// Additional segment after the method-id (METHOD), as a prefix for the method-specific-id
242    /// (DID Suffix), identifiying a network instance. e.g. "testnet"
243    ///
244    /// Mentioned in [Note 1](https://identity.foundation/sidetree/spec/v1.0.0/#note-1)
245    const NETWORK: Option<&'static str> = None;
246
247    /// Maximum length of `controller` property
248    ///
249    /// Reference: [Sidetree §12.1.1 `add-public-keys`](https://identity.foundation/sidetree/spec/v1.0.0/#add-public-keys)
250    const MAX_CONTROLLER_LENGTH: Option<usize> = None;
251
252    /// Maximum length of `publicKeyMultibase` property
253    ///
254    /// Reference: [Sidetree §12.1.1 `add-public-keys`](https://identity.foundation/sidetree/spec/v1.0.0/#add-public-keys)
255    const MAX_PKMB_LENGTH: Option<usize> = None;
256
257    /// Hash and encode data
258    ///
259    /// [Sidetree §6.1 Hashing Process](https://identity.foundation/sidetree/spec/#hashing-process)
260    fn hash(data: &[u8]) -> String {
261        let hash = Self::hash_protocol(data);
262        /*
263        ensure!(
264            hash.len() <= Self::MAX_OPERATION_HASH_LENGTH,
265            "Hash is too long"
266        );
267        */
268        Self::data_encoding_scheme(&hash)
269    }
270
271    /// [Public Key Commitment Scheme (Sidetree §6.2.1)][pkcs]
272    ///
273    /// [pkcs]: https://identity.foundation/sidetree/spec/v1.0.0/#public-key-commitment-scheme
274    fn commitment_scheme(pkjwk: &PublicKeyJwk) -> String {
275        let canonicalized_public_key = json_canonicalization_scheme(&pkjwk).unwrap();
276        // Note: hash_algorithm called here instead of reveal_value, since the underlying hash is
277        // used, not the encoded/prefixed one.
278        let reveal_value = Self::hash_algorithm(canonicalized_public_key.as_bytes());
279        Self::hash(&reveal_value)
280    }
281
282    /// Create a Sidetree-based DID using existing keys
283    ///
284    /// This function creates a Sidetree-based DID using existing public keys for
285    /// the update key and recovery key and respective
286    /// [commitments][].
287    ///
288    /// Sidetree specifies in ([§11.1 Create][create]) that creating a Sidetree DID involves
289    /// generating a Update keypair and Recovery keypair. That is implemented in [Self::create].
290    ///
291    /// **Note**: The Sidetree specification ([§6.2.1 Public Key Commitment
292    /// Scheme][pkcs]) recommends not reusing public keys across different commitment invocations, and
293    /// requires not using public key JWK payloads across commitment invocations.
294    ///
295    /// [commitments]: https://identity.foundation/sidetree/spec/v1.0.0/#commitment
296    /// [create]: https://identity.foundation/sidetree/spec/v1.0.0/#create
297    /// [pkcs]: https://identity.foundation/sidetree/spec/v1.0.0/#public-key-commitment-scheme
298    fn create_existing(
299        update_pk: &PublicKeyJwk,
300        recovery_pk: &PublicKeyJwk,
301        patches: Vec<DIDStatePatch>,
302    ) -> Result<Operation, CreateError> {
303        if update_pk == recovery_pk {
304            return Err(CreateError::SameUpdateAndRecoveryKeys);
305        }
306
307        let update_commitment = Self::commitment_scheme(update_pk);
308
309        let create_operation_delta_object = Delta {
310            patches,
311            update_commitment,
312        };
313        let delta_string = json_canonicalization_scheme(&create_operation_delta_object).unwrap();
314        let delta_hash = Self::hash(delta_string.as_bytes());
315
316        let recovery_commitment = Self::commitment_scheme(recovery_pk);
317
318        let create_operation_suffix_data_object = SuffixData {
319            r#type: None,
320            delta_hash,
321            recovery_commitment,
322            anchor_origin: None,
323        };
324
325        let create_operation = CreateOperation {
326            suffix_data: create_operation_suffix_data_object,
327            delta: create_operation_delta_object,
328        };
329
330        Ok(Operation::Create(create_operation))
331    }
332
333    /// Create a Sidetree-based DID
334    ///
335    /// Generate keypairs and construct a Create Operation according to [Sidetree §11.1
336    /// Create][create]. Returns the private keys and the create operation.
337    ///
338    /// [create]: https://identity.foundation/sidetree/spec/v1.0.0/#create
339    fn create(patches: Vec<DIDStatePatch>) -> Result<(Operation, JWK, JWK), CreateError> {
340        let update_keypair = Self::generate_key();
341        let recovery_keypair = Self::generate_key();
342        let update_pk = PublicKeyJwk::try_from(update_keypair.to_public())
343            .map_err(|_| CreateError::InvalidUpdateKey)?;
344        let recovery_pk = PublicKeyJwk::try_from(recovery_keypair.to_public())
345            .map_err(|_| CreateError::InvalidRecoveryKey)?;
346        let create_op = Self::create_existing(&update_pk, &recovery_pk, patches)?;
347        Ok((create_op, update_keypair, recovery_keypair))
348    }
349
350    /// Create a Sidetree-based DID
351    ///
352    /// Construct a DID Update Operation according to [Sidetree §11.2
353    /// Update][update]. Returns the update operation.
354    ///
355    /// Unlike [Self::create] and [Self::recover], this does not generate keys, since the specification does not
356    /// call for that here. Instead, the caller must generate a new update keypair, and pass
357    /// its public key in the `new_update_pk` argument.
358    ///
359    /// Using a `update_key` with a [JWK Nonce][jwkn] is not yet supported.
360    ///
361    /// [update]: https://identity.foundation/sidetree/spec/v1.0.0/#update
362    /// [jwkn]: https://identity.foundation/sidetree/spec/#jwk-nonce
363    fn update(
364        did_suffix: DIDSuffix,
365        update_key: &JWK,
366        new_update_pk: &PublicKeyJwk,
367        patches: Vec<DIDStatePatch>,
368    ) -> Result<UpdateOperation, UpdateError> {
369        let update_pk = PublicKeyJwk::try_from(update_key.to_public())
370            .map_err(|_| UpdateError::InvalidUpdateKey)?;
371        let canonicalized_update_pk = json_canonicalization_scheme(&update_pk).unwrap();
372        let update_reveal_value = Self::reveal_value(canonicalized_update_pk.as_bytes());
373
374        if new_update_pk == &update_pk {
375            return Err(UpdateError::UpdateKeyUnchanged);
376        }
377
378        let new_update_commitment = Self::commitment_scheme(new_update_pk);
379
380        let update_operation_delta_object = Delta {
381            patches,
382            update_commitment: new_update_commitment,
383        };
384
385        let delta_string = json_canonicalization_scheme(&update_operation_delta_object).unwrap();
386        let delta_hash = Self::hash(delta_string.as_bytes());
387
388        let algorithm = Self::SIGNATURE_ALGORITHM;
389        let claims = UpdateClaims {
390            update_key: update_pk,
391            delta_hash,
392        };
393        let signed_data = ssi_jwt::encode_sign(algorithm, &claims, update_key)
394            .map_err(|_| UpdateError::SignatureFailed)?;
395        let update_op = UpdateOperation {
396            did_suffix,
397            reveal_value: update_reveal_value,
398            delta: update_operation_delta_object,
399            signed_data,
400        };
401
402        Ok(update_op)
403    }
404
405    /// Recover a Sidetree-based DID using existing keys
406    ///
407    /// Like [Self::recover] but does not generate or handle the new update key pair and recovery
408    /// key pair; instead, their public keys must be provided by the caller in the `new_update_pk`
409    /// and `new_recovery_pk` arguments.
410    ///
411    /// Returns the constructed DID Recover operation.
412    fn recover_existing(
413        did_suffix: DIDSuffix,
414        recovery_key: &JWK,
415        new_update_pk: &PublicKeyJwk,
416        new_recovery_pk: &PublicKeyJwk,
417        patches: Vec<DIDStatePatch>,
418    ) -> Result<Operation, RecoverError> {
419        let recovery_pk = PublicKeyJwk::try_from(recovery_key.to_public())
420            .map_err(|_| RecoverError::InvalidRecoveryKey)?;
421
422        if new_recovery_pk == &recovery_pk {
423            return Err(RecoverError::RecoveryKeyUnchanged);
424        }
425
426        let canonicalized_recovery_pk = json_canonicalization_scheme(&recovery_pk).unwrap();
427        let recover_reveal_value = Self::reveal_value(canonicalized_recovery_pk.as_bytes());
428        let new_update_commitment = Self::commitment_scheme(new_update_pk);
429        let new_recovery_commitment = Self::commitment_scheme(new_recovery_pk);
430
431        let recover_operation_delta_object = Delta {
432            patches,
433            update_commitment: new_update_commitment,
434        };
435
436        let delta_string = json_canonicalization_scheme(&recover_operation_delta_object).unwrap();
437        let delta_hash = Self::hash(delta_string.as_bytes());
438
439        let algorithm = Self::SIGNATURE_ALGORITHM;
440        let claims = RecoveryClaims {
441            recovery_commitment: new_recovery_commitment,
442            recovery_key: recovery_pk,
443            delta_hash,
444            anchor_origin: None,
445        };
446        let signed_data = ssi_jwt::encode_sign(algorithm, &claims, recovery_key)
447            .map_err(|_| RecoverError::SignatureFailed)?;
448        let recover_op = RecoverOperation {
449            did_suffix,
450            reveal_value: recover_reveal_value,
451            delta: recover_operation_delta_object,
452            signed_data,
453        };
454        Ok(Operation::Recover(recover_op))
455    }
456
457    /// Recover a Sidetree-based DID
458    ///
459    /// Generate keypairs and construct a Recover Operation according to [Sidetree §11.3
460    /// Recover][recover]. Returns the recover operation.
461    ///
462    /// [recover]: https://identity.foundation/sidetree/spec/v1.0.0/#recover
463    fn recover(
464        did_suffix: DIDSuffix,
465        recovery_key: &JWK,
466        patches: Vec<DIDStatePatch>,
467    ) -> Result<(Operation, JWK, JWK), RecoverError> {
468        let new_update_keypair = Self::generate_key();
469        let new_update_pk = PublicKeyJwk::try_from(new_update_keypair.to_public()).unwrap();
470
471        let new_recovery_keypair = Self::generate_key();
472        let new_recovery_pk = PublicKeyJwk::try_from(new_recovery_keypair.to_public()).unwrap();
473
474        let recover_op = Self::recover_existing(
475            did_suffix,
476            recovery_key,
477            &new_update_pk,
478            &new_recovery_pk,
479            patches,
480        )?;
481
482        Ok((recover_op, new_update_keypair, new_recovery_keypair))
483    }
484
485    /// Deactivate a Sidetree-based DID
486    ///
487    /// Construct a Deactivate Operation according to [Sidetree §11.4
488    /// Deactivate][deactivate]. Returns the deactivate operation.
489    ///
490    /// [deactivate]: https://identity.foundation/sidetree/spec/v1.0.0/#deactivate
491    fn deactivate(
492        did_suffix: DIDSuffix,
493        recovery_key: JWK,
494    ) -> Result<DeactivateOperation, DeactivateError> {
495        let recovery_pk = PublicKeyJwk::try_from(recovery_key.to_public())
496            .map_err(|_| DeactivateError::InvalidRecoveryKey)?;
497        let canonicalized_recovery_pk = json_canonicalization_scheme(&recovery_pk).unwrap();
498        let recover_reveal_value = Self::reveal_value(canonicalized_recovery_pk.as_bytes());
499        let algorithm = Self::SIGNATURE_ALGORITHM;
500        let claims = DeactivateClaims {
501            did_suffix: did_suffix.clone(),
502            recovery_key: recovery_pk,
503        };
504        let signed_data = ssi_jwt::encode_sign(algorithm, &claims, &recovery_key)
505            .map_err(|_| DeactivateError::SignatureFailed)?;
506        let recover_op = DeactivateOperation {
507            did_suffix,
508            reveal_value: recover_reveal_value,
509            signed_data,
510        };
511        Ok(recover_op)
512    }
513
514    /// Serialize and hash [Suffix Data][SuffixData], to generate a [Short-Form Sidetree
515    /// DID][SidetreeDID::Short] ([`DIDSuffix`]).
516    ///
517    /// Reference: <https://identity.foundation/sidetree/spec/v1.0.0/#did-uri-composition>
518    fn serialize_suffix_data(suffix_data: &SuffixData) -> DIDSuffix {
519        let string = json_canonicalization_scheme(suffix_data).unwrap();
520        let hash = Self::hash(string.as_bytes());
521        DIDSuffix(hash)
522    }
523
524    /// Check that a DID Suffix looks valid
525    fn validate_did_suffix(suffix: &DIDSuffix) -> Result<(), InvalidSidetreeDIDSuffix> {
526        let bytes = base64::prelude::BASE64_URL_SAFE_NO_PAD
527            .decode(&suffix.0)
528            .map_err(|_| InvalidSidetreeDIDSuffix::Base64)?;
529
530        if bytes.len() != 34 {
531            return Err(InvalidSidetreeDIDSuffix::Length(bytes.len()));
532        }
533
534        if &bytes[0..1] != MULTIHASH_SHA2_256_PREFIX || &bytes[1..2] != MULTIHASH_SHA2_256_SIZE {
535            return Err(InvalidSidetreeDIDSuffix::Prefix);
536        }
537
538        Ok(())
539    }
540}
541
542/// [`JSON_CANONICALIZATION_SCHEME`](https://identity.foundation/sidetree/spec/v1.0.0/#json-canonicalization-scheme)
543fn json_canonicalization_scheme<T: Serialize + ?Sized>(
544    value: &T,
545) -> Result<String, serde_json::Error> {
546    serde_jcs::to_string(value)
547}
548
549#[derive(Debug, thiserror::Error)]
550pub enum InvalidSidetreeDIDSuffix {
551    #[error("invalid base64")]
552    Base64,
553
554    #[error("unexpected DID suffix length ({0})")]
555    Length(usize),
556
557    #[error("unexpected DID suffix prefix")]
558    Prefix,
559}
560
561/// Public key as JWK or Multibase
562///
563/// Property of a public key / verification method containing public key data,
564/// as part of a [PublicKeyEntry][].
565///
566/// per [Sidetree §12.1.1 `add-public-keys`: Step 4][apk].
567///
568/// [apk]: https://identity.foundation/sidetree/spec/v1.0.0/#add-public-keys
569#[derive(Debug, Serialize, Deserialize, Clone)]
570#[serde(rename_all = "camelCase")]
571pub enum PublicKey {
572    /// [`publicKeyJwk`](https://www.w3.org/TR/did-core/#dfn-publickeyjwk) as defined in DID Core.
573    ///
574    /// JSON Web Key (JWK) is specified in [RFC 7517](https://datatracker.ietf.org/doc/html/rfc7517).
575    PublicKeyJwk(PublicKeyJwk),
576
577    /// [`publicKeyMultibase`](https://www.w3.org/TR/did-core/#dfn-publickeymultibase) as defined in DID Core.
578    ///
579    /// Maximum length may be set in [Sidetree::MAX_PKMB_LENGTH].
580    PublicKeyMultibase(String),
581}
582
583/// Public Key Entry
584///
585/// Used by the [`add-public-keys`](DIDStatePatch::AddPublicKeys) and
586/// [`replace`](DIDStatePatch::Replace) DID state patch actions.
587///
588/// Specified in [Sidetree §12.1.1 `add-public-keys`][apk].
589///
590/// [apk]: https://identity.foundation/sidetree/spec/v1.0.0/#add-public-keys
591#[derive(Debug, Serialize, Deserialize, Clone)]
592#[serde(rename_all = "camelCase")]
593pub struct PublicKeyEntry {
594    /// `id` property
595    ///
596    /// Maximum length: 50 in Base64url
597    pub id: String,
598
599    /// Verification method type
600    pub r#type: String,
601
602    /// Verification method controller (DID)
603    ///
604    /// Maximum length may be set in [Sidetree::MAX_CONTROLLER_LENGTH].
605    #[serde(skip_serializing_if = "Option::is_none")]
606    pub controller: Option<String>,
607
608    /// `publicKeyJwk` or `publicKeyMultibase` property
609    #[serde(flatten)]
610    pub public_key: PublicKey,
611
612    /// Verification relationships
613    ///
614    /// Defined in [DID Core](https://www.w3.org/TR/did-core/#verification-relationships).
615    ///
616    /// Corresponds to [`proofPurpose`](https://www.w3.org/TR/did-core/#verification-relationships) in VC Data Model.
617    pub purposes: Vec<ProofPurpose>,
618}
619
620#[derive(Debug, thiserror::Error)]
621#[error("invalid public key entry")]
622pub struct InvalidPublicKeyEntry(pub JWK);
623
624impl TryFrom<JWK> for PublicKeyEntry {
625    type Error = InvalidPublicKeyEntry;
626
627    fn try_from(jwk: JWK) -> Result<Self, Self::Error> {
628        let Ok(id) = jwk.thumbprint() else {
629            return Err(InvalidPublicKeyEntry(jwk));
630        };
631
632        let Ok(pkjwk) = PublicKeyJwk::try_from(jwk.to_public()) else {
633            return Err(InvalidPublicKeyEntry(jwk));
634        };
635
636        let public_key = PublicKey::PublicKeyJwk(pkjwk);
637        Ok(PublicKeyEntry {
638            id,
639            r#type: VERIFICATION_METHOD_TYPE.to_owned(),
640            controller: None,
641            public_key,
642            purposes: vec![
643                ProofPurpose::Assertion,
644                ProofPurpose::Authentication,
645                ProofPurpose::KeyAgreement,
646                ProofPurpose::CapabilityInvocation,
647                ProofPurpose::CapabilityDelegation,
648            ],
649        })
650    }
651}
652
653/// Service Endpoint Entry
654///
655/// Used by the [`add-services`](DIDStatePatch::AddServices) and
656/// [`replace`](DIDStatePatch::Replace) DID state patch actions.
657///
658/// Specified in [Sidetree §12.1.3 `add-services`][as].
659///
660/// [as]: https://identity.foundation/sidetree/spec/v1.0.0/#add-services
661#[derive(Debug, Serialize, Deserialize, Clone)]
662#[serde(rename_all = "camelCase")]
663pub struct ServiceEndpointEntry {
664    /// `id` property
665    ///
666    /// Maximum length: 50 in Base64Url
667    pub id: String,
668
669    /// Service type
670    ///
671    /// Maximum length: 30 in Base64Url
672    pub r#type: String,
673
674    /// Service endpoint URL or object
675    pub service_endpoint: ServiceEndpoint,
676}
677
678/// DID PKI metadata state
679///
680/// Used by the [`replace`](DIDStatePatch::Replace) DID state patch.
681#[derive(Debug, Serialize, Deserialize, Clone, Default)]
682#[serde(rename_all = "camelCase")]
683pub struct DocumentState {
684    /// Public key entries
685    #[serde(skip_serializing_if = "Option::is_none")]
686    pub public_keys: Option<Vec<PublicKeyEntry>>,
687
688    /// Services
689    #[serde(skip_serializing_if = "Option::is_none")]
690    pub services: Option<Vec<ServiceEndpointEntry>>,
691}
692
693/// [DID State Patch][dsp] using a [Sidetree Standard Patch action][spa]
694///
695/// [dsp]: https://identity.foundation/sidetree/spec/v1.0.0/#did-state-patches
696/// [spa]: https://identity.foundation/sidetree/spec/v1.0.0/#standard-patch-actions
697#[derive(Debug, Serialize, Deserialize, Clone)]
698#[serde(tag = "action")]
699#[serde(rename_all = "kebab-case")]
700pub enum DIDStatePatch {
701    /// [`add-public-keys`][apk] Patch Action
702    ///
703    /// [apk]: https://identity.foundation/sidetree/spec/v1.0.0/#add-public-keys
704    AddPublicKeys {
705        /// Keys to add or over overwrite
706        #[serde(rename = "publicKeys")]
707        public_keys: Vec<PublicKeyEntry>,
708    },
709
710    /// [`remove-public-keys`][rpk] Patch Action
711    ///
712    /// [rpk]: https://identity.foundation/sidetree/spec/v1.0.0/#remove-public-keys
713    RemovePublicKeys {
714        /// IDs of keys to remove
715        ids: Vec<String>,
716    },
717
718    /// [`add-services`][as] Patch Action
719    ///
720    /// [as]: https://identity.foundation/sidetree/spec/v1.0.0/#add-services
721    AddServices {
722        /// Service entries to add
723        services: Vec<ServiceEndpointEntry>,
724    },
725
726    /// [`remove-services`][rs] Patch Action
727    ///
728    /// [rs]: https://identity.foundation/sidetree/spec/v1.0.0/#remove-services
729    RemoveServices {
730        /// IDs of service endpoints to remove
731        ids: Vec<String>,
732    },
733
734    /// [`replace`][r] Patch Action
735    ///
736    /// [r]: https://identity.foundation/sidetree/spec/v1.0.0/#replace
737    Replace {
738        /// Reset DID state
739        document: DocumentState,
740    },
741
742    /// [`ietf-json-patch`][ijp] Patch Action
743    ///
744    /// [ijp]: https://identity.foundation/sidetree/spec/v1.0.0/#ietf-json-patch
745    ///
746    IetfJsonPatch {
747        /// JSON Patches according to [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902).
748        patches: Patch,
749    },
750}
751
752/// Create/Update/Recover Delta Object
753///
754/// ### References
755/// - [Sidetree §11.1 Create - Create Operation Delta Object][codo]
756/// - [Sidetree §11.2 Update - Update Operation Delta Object][uodo]
757/// - [Sidetree §11.3 Recover - Recover Operation Delta Object][rodo]
758///
759/// [codo]: https://identity.foundation/sidetree/spec/v1.0.0/#create-delta-object
760/// [uodo]: https://identity.foundation/sidetree/spec/v1.0.0/#update-delta-object
761/// [rodo]: https://identity.foundation/sidetree/spec/v1.0.0/#recover-delta-object
762#[derive(Debug, Serialize, Deserialize, Clone)]
763#[serde(rename_all = "camelCase")]
764pub struct Delta {
765    /// DID state patches to apply.
766    pub patches: Vec<DIDStatePatch>,
767
768    /// Update commitment generated as part of a Sidetree Create or Update operation.
769    pub update_commitment: String,
770}
771
772/// Public Key JWK (JSON Web Key)
773///
774/// Wraps [ssi_jwk::JWK], while allowing a `nonce` property, and disallowing private key
775/// properties ("d").
776///
777/// Sidetree may allow a `nonce` property in public key JWKs ([§6.2.2 JWK Nonce][jwkn]).
778///
779/// [jwkn]: https://identity.foundation/sidetree/spec/#jwk-nonce
780#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
781#[serde(rename_all = "camelCase")]
782pub struct PublicKeyJwk {
783    #[serde(skip_serializing_if = "Option::is_none")]
784    pub nonce: Option<Base64urlUInt>,
785    #[serde(flatten)]
786    jwk: serde_json::Value,
787}
788
789/// Error resulting from [converting JWK to PublicKeyJwk][PublicKeyJwk::try_from]
790#[derive(thiserror::Error, Debug)]
791pub enum PublicKeyJwkFromJWKError {
792    /// Public Key JWK must not contain private key parameters (e.g. "d")
793    #[error("Public Key JWK must not contain private key parameters")]
794    PrivateKeyParameters,
795}
796
797/// Error resulting from attempting to convert [PublicKeyJwk] to JWK
798#[derive(thiserror::Error, Debug)]
799pub enum JWKFromPublicKeyJwkError {
800    /// Unable to convert [`serde_json::Value`] to JWK
801    #[error("Unable to convert Value to JWK")]
802    FromValue(#[from] serde_json::Error),
803}
804
805impl TryFrom<JWK> for PublicKeyJwk {
806    type Error = PublicKeyJwkFromJWKError;
807    fn try_from(jwk: JWK) -> Result<Self, Self::Error> {
808        let jwk_value = serde_json::to_value(jwk).unwrap();
809        if jwk_value.get("d").is_some() {
810            return Err(PublicKeyJwkFromJWKError::PrivateKeyParameters);
811        };
812        Ok(Self {
813            jwk: jwk_value,
814            nonce: None,
815        })
816    }
817}
818
819/// Convert [PublicKeyJwk] to [JWK].
820///
821/// Note: `nonce` property is dropped.
822impl TryFrom<PublicKeyJwk> for JWK {
823    type Error = JWKFromPublicKeyJwkError;
824    fn try_from(pkjwk: PublicKeyJwk) -> Result<Self, Self::Error> {
825        let jwk = serde_json::from_value(pkjwk.jwk).map_err(JWKFromPublicKeyJwkError::FromValue)?;
826        Ok(jwk)
827    }
828}
829
830fn b64len(s: &str) -> usize {
831    base64::prelude::BASE64_URL_SAFE_NO_PAD.encode(s).len()
832}
833
834impl DIDStatePatch {
835    /// Convert a [DID Document Operation][ddo] and DID to a Sidetree [DID State Patch][dsp].
836    ///
837    /// [ddp]: https://identity.foundation/did-registration/#diddocumentoperation
838    /// [dsp]: https://identity.foundation/sidetree/spec/v1.0.0/#did-state-patches
839    fn try_from_with_did<S: Sidetree>(
840        did_doc_op: DIDDocumentOperation,
841        did: &SidetreeDID<S>,
842    ) -> Result<Self, DIDTransactionCreationError> {
843        match did_doc_op {
844            DIDDocumentOperation::SetDidDocument(_doc) => {
845                Err(DIDTransactionCreationError::UnimplementedDocumentOperation(
846                    DIDDocumentOperationKind::SetDidDocument,
847                ))
848            }
849            DIDDocumentOperation::AddToDidDocument(_props) => {
850                Err(DIDTransactionCreationError::UnimplementedDocumentOperation(
851                    DIDDocumentOperationKind::AddToDidDocument,
852                ))
853            }
854            DIDDocumentOperation::RemoveFromDidDocument(_props) => {
855                Err(DIDTransactionCreationError::UnimplementedDocumentOperation(
856                    DIDDocumentOperationKind::RemoveFromDidDocument,
857                ))
858            }
859            DIDDocumentOperation::SetVerificationMethod { vmm, purposes } => {
860                let sub_id = did_url_to_id(&vmm.id, did)?;
861                let mut value = serde_json::to_value(vmm).unwrap();
862                value["id"] = serde_json::Value::String(sub_id);
863                value["purposes"] = serde_json::to_value(purposes).unwrap();
864                let entry: PublicKeyEntry = serde_json::from_value(value)
865                    .map_err(|_| DIDTransactionCreationError::InvalidVerificationMethod)?;
866                // TODO: allow omitted controller property
867                Ok(DIDStatePatch::AddPublicKeys {
868                    public_keys: vec![entry],
869                })
870            }
871            DIDDocumentOperation::SetService(service) => {
872                let Service {
873                    id,
874                    type_,
875                    service_endpoint,
876                    property_set,
877                } = service;
878
879                if !property_set.is_empty() {
880                    return Err(DIDTransactionCreationError::UnsupportedServiceProperty);
881                }
882
883                let service_endpoint = match service_endpoint {
884                    None => return Err(DIDTransactionCreationError::MissingServiceEndpoint),
885                    Some(values) => match values.into_single() {
886                        Some(value) => value,
887                        None => return Err(DIDTransactionCreationError::AmbiguousServiceEndpoint),
888                    },
889                };
890
891                let sub_id = did_url_to_id(&id, did)?;
892                let service_type = match type_.into_single() {
893                    Some(type_) => type_,
894                    None => return Err(DIDTransactionCreationError::AmbiguousServiceType),
895                };
896
897                if b64len(&service_type) > 30 {
898                    return Err(DIDTransactionCreationError::UnsupportedService {
899                        reason: Cow::Borrowed("Sidetree service type must contain no more than 30 Base64Url-encoded characters")
900                    });
901                }
902
903                if b64len(&sub_id) > 50 {
904                    return Err(DIDTransactionCreationError::UnsupportedService {
905                        reason: Cow::Borrowed("Sidetree service id must contain no more than 50 Base64Url-encoded characters")
906                    });
907                }
908
909                let entry = ServiceEndpointEntry {
910                    id: sub_id,
911                    r#type: service_type,
912                    service_endpoint,
913                };
914
915                Ok(DIDStatePatch::AddServices {
916                    services: vec![entry],
917                })
918            }
919            DIDDocumentOperation::RemoveVerificationMethod(did_url) => {
920                let id = did_url.to_string();
921                Ok(DIDStatePatch::RemovePublicKeys { ids: vec![id] })
922            }
923            DIDDocumentOperation::RemoveService(did_url) => {
924                let id = did_url.to_string();
925                Ok(DIDStatePatch::RemoveServices { ids: vec![id] })
926            }
927        }
928    }
929}
930
931/// Convert a DID URL to an object id given a DID
932///
933/// Object id is an id of a [ServiceEndpointEntry] or [PublicKeyEntry].
934fn did_url_to_id<S: Sidetree>(
935    did_url: &str,
936    did: &SidetreeDID<S>,
937) -> Result<String, DIDTransactionCreationError> {
938    let did_string = did.to_string();
939    let unprefixed = did_url
940        .strip_prefix(&did_string)
941        .ok_or(DIDTransactionCreationError::InvalidDIDURL)?;
942    let fragment = unprefixed
943        .strip_prefix('#')
944        .ok_or(DIDTransactionCreationError::InvalidDIDURL)?;
945    Ok(fragment.to_string())
946}
947
948#[derive(Debug, Serialize, Deserialize, Clone)]
949pub struct SidetreeAPIError {
950    // List of error codes: https://github.com/decentralized-identity/sidetree/blob/v1.0.0/lib/core/versions/1.0/ErrorCode.ts
951    pub code: String,
952    pub message: Option<String>,
953}
954
955impl fmt::Display for SidetreeAPIError {
956    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
957        write!(f, "Sidetree error {}", self.code)?;
958        if let Some(ref message) = self.message {
959            write!(f, ": {}", message)?;
960        }
961        Ok(())
962    }
963}
964
965#[cfg(test)]
966mod tests {
967    use std::str::FromStr;
968
969    use crate::ion::is_secp256k1;
970
971    use super::*;
972    use serde_json::json;
973    use ssi_jwk::Algorithm;
974
975    struct Example;
976
977    impl Sidetree for Example {
978        fn generate_key() -> JWK {
979            JWK::generate_secp256k1()
980        }
981        fn validate_key(key: &JWK) -> bool {
982            is_secp256k1(key)
983        }
984        const SIGNATURE_ALGORITHM: Algorithm = Algorithm::ES256K;
985        const METHOD: &'static str = "sidetree";
986    }
987
988    /// <https://identity.foundation/sidetree/spec/v1.0.0/#did>
989    static LONGFORM_DID: &str = "did:sidetree:EiDyOQbbZAa3aiRzeCkV7LOx3SERjjH93EXoIM3UoN4oWg:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJwdWJsaWNLZXlNb2RlbDFJZCIsInB1YmxpY0tleUp3ayI6eyJjcnYiOiJzZWNwMjU2azEiLCJrdHkiOiJFQyIsIngiOiJ0WFNLQl9ydWJYUzdzQ2pYcXVwVkpFelRjVzNNc2ptRXZxMVlwWG45NlpnIiwieSI6ImRPaWNYcWJqRnhvR0otSzAtR0oxa0hZSnFpY19EX09NdVV3a1E3T2w2bmsifSwicHVycG9zZXMiOlsiYXV0aGVudGljYXRpb24iLCJrZXlBZ3JlZW1lbnQiXSwidHlwZSI6IkVjZHNhU2VjcDI1NmsxVmVyaWZpY2F0aW9uS2V5MjAxOSJ9XSwic2VydmljZXMiOlt7ImlkIjoic2VydmljZTFJZCIsInNlcnZpY2VFbmRwb2ludCI6Imh0dHA6Ly93d3cuc2VydmljZTEuY29tIiwidHlwZSI6InNlcnZpY2UxVHlwZSJ9XX19XSwidXBkYXRlQ29tbWl0bWVudCI6IkVpREtJa3dxTzY5SVBHM3BPbEhrZGI4Nm5ZdDBhTnhTSFp1MnItYmhFem5qZEEifSwic3VmZml4RGF0YSI6eyJkZWx0YUhhc2giOiJFaUNmRFdSbllsY0Q5RUdBM2RfNVoxQUh1LWlZcU1iSjluZmlxZHo1UzhWRGJnIiwicmVjb3ZlcnlDb21taXRtZW50IjoiRWlCZk9aZE10VTZPQnc4UGs4NzlRdFotMkotOUZiYmpTWnlvYUFfYnFENHpoQSJ9fQ";
990    static SHORTFORM_DID: &str = "did:sidetree:EiDyOQbbZAa3aiRzeCkV7LOx3SERjjH93EXoIM3UoN4oWg";
991
992    lazy_static::lazy_static! {
993
994        /// <https://identity.foundation/sidetree/spec/v1.0.0/#create-2>
995        static ref CREATE_OPERATION: Operation = serde_json::from_value(json!({
996          "type": "create",
997          "suffixData": {
998            "deltaHash": "EiCfDWRnYlcD9EGA3d_5Z1AHu-iYqMbJ9nfiqdz5S8VDbg",
999            "recoveryCommitment": "EiBfOZdMtU6OBw8Pk879QtZ-2J-9FbbjSZyoaA_bqD4zhA"
1000          },
1001          "delta": {
1002            "updateCommitment": "EiDKIkwqO69IPG3pOlHkdb86nYt0aNxSHZu2r-bhEznjdA",
1003            "patches": [
1004              {
1005                "action": "replace",
1006                "document": {
1007                  "publicKeys": [
1008                    {
1009                      "id": "publicKeyModel1Id",
1010                      "type": "EcdsaSecp256k1VerificationKey2019",
1011                      "publicKeyJwk": {
1012                        "kty": "EC",
1013                        "crv": "secp256k1",
1014                        "x": "tXSKB_rubXS7sCjXqupVJEzTcW3MsjmEvq1YpXn96Zg",
1015                        "y": "dOicXqbjFxoGJ-K0-GJ1kHYJqic_D_OMuUwkQ7Ol6nk"
1016                      },
1017                      "purposes": [
1018                        "authentication",
1019                        "keyAgreement"
1020                      ]
1021                    }
1022                  ],
1023                  "services": [
1024                    {
1025                      "id": "service1Id",
1026                      "type": "service1Type",
1027                      "serviceEndpoint": "http://www.service1.com"
1028                    }
1029                  ]
1030                }
1031              }
1032            ]
1033          }
1034        })).unwrap();
1035
1036        /// <https://identity.foundation/sidetree/spec/v1.0.0/#update-2>
1037        static ref UPDATE_OPERATION: Operation = serde_json::from_value(json!({
1038          "type": "update",
1039          "didSuffix": "EiDyOQbbZAa3aiRzeCkV7LOx3SERjjH93EXoIM3UoN4oWg",
1040          "revealValue": "EiBkRSeixqX-PhOij6PIpuGfPld5Nif5MxcrgtGCw-t6LA",
1041          "delta": {
1042            "patches": [
1043              {
1044                "action": "add-public-keys",
1045                "publicKeys": [
1046                  {
1047                    "id": "additional-key",
1048                    "type": "EcdsaSecp256k1VerificationKey2019",
1049                    "publicKeyJwk": {
1050                      "kty": "EC",
1051                      "crv": "secp256k1",
1052                      "x": "aN75CTjy3VCgGAJDNJHbcb55hO8CobEKzgCNrUeOwAY",
1053                      "y": "K9FhCEpa_jG09pB6qriXrgSvKzXm6xtxBvZzIoXXWm4"
1054                    },
1055                    "purposes": [
1056                      "authentication",
1057                      "assertionMethod",
1058                      "capabilityInvocation",
1059                      "capabilityDelegation",
1060                      "keyAgreement"
1061                    ]
1062                  }
1063                ]
1064              }
1065            ],
1066            "updateCommitment": "EiDOrcmPtfMHuwIWN6YoihdeIPxOKDHy3D6sdMXu_7CN0w"
1067          },
1068          "signedData": "eyJhbGciOiJFUzI1NksifQ.eyJ1cGRhdGVLZXkiOnsia3R5IjoiRUMiLCJjcnYiOiJzZWNwMjU2azEiLCJ4Ijoid2Z3UUNKM09ScVZkbkhYa1Q4UC1MZ19HdHhCRWhYM3R5OU5VbnduSHJtdyIsInkiOiJ1aWU4cUxfVnVBblJEZHVwaFp1eExPNnFUOWtQcDNLUkdFSVJsVHBXcmZVIn0sImRlbHRhSGFzaCI6IkVpQ3BqTjQ3ZjBNcTZ4RE5VS240aFNlZ01FcW9EU19ycFEyOVd5MVY3M1ZEYncifQ.RwZK1DG5zcr4EsrRImzStb0VX5j2ZqApXZnuoAkA3IoRdErUscNG8RuxNZ0FjlJtjMJ0a-kn-_MdtR0wwvWVgg"
1069        })).unwrap();
1070
1071        /// <https://identity.foundation/sidetree/spec/v1.0.0/#recover-2>
1072        static ref RECOVER_OPERATION: Operation = serde_json::from_value(json!({
1073          "type": "recover",
1074          "didSuffix": "EiDyOQbbZAa3aiRzeCkV7LOx3SERjjH93EXoIM3UoN4oWg",
1075          "revealValue": "EiAJ-97Is59is6FKAProwDo870nmwCeP8n5nRRFwPpUZVQ",
1076          "signedData": "eyJhbGciOiJFUzI1NksifQ.eyJkZWx0YUhhc2giOiJFaUNTem1ZSk0yWGpaWE00a1Q0bGpKcEVGTjVmVkM1QVNWZ3hSekVtMEF2OWp3IiwicmVjb3ZlcnlLZXkiOnsia3R5IjoiRUMiLCJjcnYiOiJzZWNwMjU2azEiLCJ4IjoibklxbFJDeDBleUJTWGNRbnFEcFJlU3Y0enVXaHdDUldzc29jOUxfbmo2QSIsInkiOiJpRzI5Vks2bDJVNXNLQlpVU0plUHZ5RnVzWGdTbEsyZERGbFdhQ004RjdrIn0sInJlY292ZXJ5Q29tbWl0bWVudCI6IkVpQ3NBN1NHTE5lZGE1SW5sb3Fub2tVY0pGejZ2S1Q0SFM1ZGNLcm1ubEpocEEifQ.lxWnrg5jaeCAhYuz1fPhidKw6Z2cScNlEc6SWcs15DtJbrHZFxl5IezGJ3cWdOSS2DlzDl4M1ZF8dDE9kRwFeQ",
1077          "delta": {
1078            "patches": [
1079              {
1080                "action": "replace",
1081                "document": {
1082                  "publicKeys": [
1083                    {
1084                      "id": "newKey",
1085                      "type": "EcdsaSecp256k1VerificationKey2019",
1086                      "publicKeyJwk": {
1087                        "kty": "EC",
1088                        "crv": "secp256k1",
1089                        "x": "JUWp0pAMGevNLhqq_Qmd48izuLYfO5XWpjSmy5btkjc",
1090                        "y": "QYaSu1NHYnxR4qfk-RkXb4NQnQf1X3XQCpDYuibvlNc"
1091                      },
1092                      "purposes": [
1093                        "authentication",
1094                        "assertionMethod",
1095                        "capabilityInvocation",
1096                        "capabilityDelegation",
1097                        "keyAgreement"
1098                      ]
1099                    }
1100                  ],
1101                  "services": [
1102                    {
1103                      "id": "serviceId123",
1104                      "type": "someType",
1105                      "serviceEndpoint": "https://www.url.com"
1106                    }
1107                  ]
1108                }
1109              }
1110            ],
1111            "updateCommitment": "EiD6_csybTfxELBoMgkE9O2BTCmhScG_RW_qaZQkIkJ_aQ"
1112          }
1113        })).unwrap();
1114
1115        /// <https://identity.foundation/sidetree/spec/v1.0.0/#deactivate-2>
1116        static ref DEACTIVATE_OPERATION: Operation = serde_json::from_value(json!({
1117          "type": "deactivate",
1118          "didSuffix": "EiDyOQbbZAa3aiRzeCkV7LOx3SERjjH93EXoIM3UoN4oWg",
1119          "revealValue": "EiB-dib5oumdaDGH47TB17Qg1nHza036bTIGibQOKFUY2A",
1120          "signedData": "eyJhbGciOiJFUzI1NksifQ.eyJkaWRTdWZmaXgiOiJFaUR5T1FiYlpBYTNhaVJ6ZUNrVjdMT3gzU0VSampIOTNFWG9JTTNVb040b1dnIiwicmVjb3ZlcnlLZXkiOnsia3R5IjoiRUMiLCJjcnYiOiJzZWNwMjU2azEiLCJ4IjoiSk1ucF9KOW5BSGFkTGpJNmJfNVU3M1VwSEZqSEZTVHdtc1ZUUG9FTTVsMCIsInkiOiJ3c1QxLXN0UWJvSldPeEJyUnVINHQwVV9zX1lSQy14WXQyRkFEVUNHR2M4In19.ARTZrvupKdShOFNAJ4EWnsuaONKBgXUiwY5Ct10a9IXIp1uFsg0UyDnZGZtJT2v2bgtmYsQBmT6L9kKaaDcvUQ"
1121        })).unwrap();
1122    }
1123
1124    #[test]
1125    fn test_did_parse_format() {
1126        let longform_did = SidetreeDID::<Example>::from_str(LONGFORM_DID).unwrap();
1127        let shortform_did = SidetreeDID::<Example>::from_str(SHORTFORM_DID).unwrap();
1128        assert_eq!(longform_did.to_string(), LONGFORM_DID);
1129        assert_eq!(shortform_did.to_string(), SHORTFORM_DID);
1130        assert!(LONGFORM_DID.starts_with(SHORTFORM_DID));
1131    }
1132
1133    #[test]
1134    fn test_longform_did_construction() {
1135        let create_operation = match &*CREATE_OPERATION {
1136            Operation::Create(op) => op,
1137            _ => panic!("Expected Create Operation"),
1138        };
1139        let did: SidetreeDID<Example> = create_operation.to_sidetree_did();
1140        assert_eq!(did.to_string(), LONGFORM_DID);
1141    }
1142
1143    #[test]
1144    fn test_update_verify_reveal() {
1145        let create_pvo = CREATE_OPERATION
1146            .clone()
1147            .partial_verify::<Example>()
1148            .unwrap();
1149        let update_pvo = UPDATE_OPERATION
1150            .clone()
1151            .partial_verify::<Example>()
1152            .unwrap();
1153        update_pvo.follows::<Example>(&create_pvo).unwrap();
1154    }
1155
1156    #[test]
1157    fn test_recover_verify_reveal() {
1158        let create_pvo = CREATE_OPERATION
1159            .clone()
1160            .partial_verify::<Example>()
1161            .unwrap();
1162        let recover_pvo = RECOVER_OPERATION
1163            .clone()
1164            .partial_verify::<Example>()
1165            .unwrap();
1166        recover_pvo.follows::<Example>(&create_pvo).unwrap();
1167    }
1168
1169    #[test]
1170    fn test_deactivate_verify_reveal() {
1171        let recover_pvo = RECOVER_OPERATION
1172            .clone()
1173            .partial_verify::<Example>()
1174            .unwrap();
1175        let deactivate_pvo = DEACTIVATE_OPERATION
1176            .clone()
1177            .partial_verify::<Example>()
1178            .unwrap();
1179        deactivate_pvo.follows::<Example>(&recover_pvo).unwrap();
1180    }
1181}