Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DID ION Update, Recover, and Deactivate Operations #77

Merged
merged 33 commits into from
Oct 25, 2023
Merged

Conversation

andresuribe87
Copy link
Contributor

@andresuribe87 andresuribe87 commented Oct 11, 2023

Overview

Fixes #1 by implementing did ion updates.

Includes the updates of PR #88, which fixes #3 and #2.

Description

After this PR, all ion type of updates will be supported.

How Has This Been Tested?

Unit test that mimics https://github.com/decentralized-identity/ion-sdk/blob/main/tests/IonRequest.spec.ts#L38 as much as possible.

@codecov-commenter
Copy link

codecov-commenter commented Oct 11, 2023

Codecov Report

Merging #77 (02ea4b3) into main (de73ad8) will increase coverage by 6.40%.
The diff coverage is 94.09%.

@@            Coverage Diff             @@
##             main      #77      +/-   ##
==========================================
+ Coverage   63.17%   69.58%   +6.40%     
==========================================
  Files          21       21              
  Lines         964     1200     +236     
  Branches      123      136      +13     
==========================================
+ Hits          609      835     +226     
  Misses        294      294              
- Partials       61       71      +10     
Components Coverage Δ
credentials 65.76% <50.00%> (-0.68%) ⬇️
crypto 33.33% <50.00%> (-0.25%) ⬇️
dids 92.81% <95.68%> (+3.73%) ⬆️
common 60.83% <ø> (ø)

@bradleydwyer bradleydwyer self-requested a review October 11, 2023 08:02
Copy link
Contributor

@nitro-neal nitro-neal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, just make sure the sign is working the same way as #63

Copy link
Contributor Author

@andresuribe87 andresuribe87 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the latest changes from the base branch now

crypto/src/main/kotlin/web5/sdk/crypto/Crypto.kt Outdated Show resolved Hide resolved
crypto/src/main/kotlin/web5/sdk/crypto/KeyManager.kt Outdated Show resolved Hide resolved
public class UpdateDidIonOptions(
public val didString: String,
public val updateKeyAlias: String,
public val servicesToAdd: Iterable<Service>? = null,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious as to why the adds are more generic than the remove (Iterable vs Set). Is order important for add operations?

JsonSubTypes.Type(ReplaceAction::class, name = "replace"),
JsonSubTypes.Type(RemoveServicesAction::class, name = "remove-services"),
JsonSubTypes.Type(AddPublicKeysAction::class, name = "add-public-keys"),
JsonSubTypes.Type(RemovePublicKeysAction::class, name = "remove-public-keys"),
)
public sealed class PatchAction

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious why the sealed class route vs a marker interface?

@@ -151,6 +169,11 @@ public data class OperationSuffixDataObject(
*/
public typealias Commitment = String

/**

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: IMHO these comments are super obvious. I don't think they add value here and make the code less readable.

public val servicesToAdd: Iterable<Service>? = null,
public val idsOfServicesToRemove: Set<String>? = null,
public val publicKeysToAdd: Iterable<PublicKey>? = null,
public val idsOfPublicKeysToRemove: Set<String>? = null,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it might be preferable to just create empty collections by default rather than cater for the possibility of a null collection

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed

* @param publicKeysToAdd PublicKeys to add to the DID document.
* @param idsOfPublicKeysToRemove Keys to remove from the DID document.
*/
public class UpdateDidIonOptions(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A possible alternative to this impl might be

public data class UpdateDidIonOptions(
  val didString: String,
  val updateKeyAlias: String,
  val servicesToAdd: List<Service> = emptyList(),
  val idsOfServicesToRemove: List<String> = emptyList(),
  val publicKeysToAdd: List<PublicKey> = emptyList(),
  val idsOfPublicKeysToRemove: List<String> = emptyList()
) {
  internal fun toPatches(): List<PatchAction> {
    fun <T> MutableList<PatchAction>.addIfNotEmpty(list: List<T>, action: (List<T>) -> PatchAction) {
      list.takeIf { it.isNotEmpty() }?.let { this.add(action(it)) }
    }

    return buildList {
      addIfNotEmpty(servicesToAdd, ::AddServicesAction)
      addIfNotEmpty(idsOfServicesToRemove, ::RemoveServicesAction)
      addIfNotEmpty(publicKeysToAdd, ::AddPublicKeysAction)
      addIfNotEmpty(idsOfPublicKeysToRemove, ::RemovePublicKeysAction)
    }
  }
}

For a rough and untested stab at it. I think this covers the comments below.

updateKeyAlias = newUpdateKeyAlias,
)
}
throw Exception("received error response '$opBody'")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RuntimeException or even better a custom runtime exception type.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file is an interop issue with Java. The declared package doesn't match the file location

@andresuribe87
Copy link
Contributor Author

@bradleydwyer you made comments on a very old commit. Can you take another stab at it, please?

public class UpdateDidIonOptions(
public val didString: String,
public val updateKeyAlias: String,
public val servicesToAdd: Iterable<Service>? = null,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it might be preferable to just create empty collections by default rather than cater for the possibility of a null collection (#77 (comment))

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

* @param publicKeysToAdd PublicKeys to add to the DID document.
* @param idsOfPublicKeysToRemove Keys to remove from the DID document.
*/
public class UpdateDidIonOptions(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A possible alternative to this impl might be

public data class UpdateDidIonOptions(
  val didString: String,
  val updateKeyAlias: String,
  val servicesToAdd: List<Service> = emptyList(),
  val idsOfServicesToRemove: List<String> = emptyList(),
  val publicKeysToAdd: List<PublicKey> = emptyList(),
  val idsOfPublicKeysToRemove: List<String> = emptyList()
) {
  internal fun toPatches(): List<PatchAction> {
    fun <T> MutableList<PatchAction>.addIfNotEmpty(list: List<T>, action: (List<T>) -> PatchAction) {
      list.takeIf { it.isNotEmpty() }?.let { this.add(action(it)) }
    }

    return buildList {
      addIfNotEmpty(servicesToAdd, ::AddServicesAction)
      addIfNotEmpty(idsOfServicesToRemove, ::RemoveServicesAction)
      addIfNotEmpty(publicKeysToAdd, ::AddPublicKeysAction)
      addIfNotEmpty(idsOfPublicKeysToRemove, ::RemovePublicKeysAction)
    }
  }
}

For a rough and untested stab at it. I think this covers the comments below.

(#77 (comment))

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this, thanks! Adopted! Just made a tiny change to accept iterables everywhere.

public class UpdateDidIonOptions(
public val didString: String,
public val updateKeyAlias: String,
public val servicesToAdd: Iterable<Service>? = null,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious as to why the adds are more generic than the remove (Iterable vs Set). Is order important for add operations?

(#77 (comment))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I looked at the spec it didn't seem like the order was important (or that each type of patch was required to be present)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And in addition to https://github.com/TBD54566975/web5-kt/pull/77/files#r1359995250 was confused at marshalling everything into a List at the end - why not just be a List from the start?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spec says, for the add operation:

In the case a service entry already exists for the given id specified within an add-services Patch Action, the implementation MUST overwrite the existing entry entirely with the incoming patch. To construct an add-services patch, compose an object as follows:

So my understanding is that order is important. Am I reading this correctly?

Re:

was confused at marshalling everything into a List at the end - why not just be a List from the start?

Great question. UpdateIonOptions is part of the public API. As such, my intention was to adopt Postel's law: "Be liberal in what you accept, and conservative in what you send.". By having Iterable, clients can have any sort of collection before the pass in the data.

I went ahead and updated the classes inside Models to follow this pattern as well.

/**
* Type alias for reveal value.
*/
public typealias Reveal = String
Copy link

@bradleydwyer bradleydwyer Oct 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: IMHO these comments are super obvious. I don't think they add value here and make the code less readable. This is also a general comment on things like @param kdoc that adds no detail beyond what is already obvious from the associate type and param name.

(#77 (comment))

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. That said, with the changes to this class, the comments have become more useful.

JsonSubTypes.Type(ReplaceAction::class, name = "replace"),
JsonSubTypes.Type(RemoveServicesAction::class, name = "remove-services"),
JsonSubTypes.Type(AddPublicKeysAction::class, name = "add-public-keys"),
JsonSubTypes.Type(RemovePublicKeysAction::class, name = "remove-public-keys"),
)
public sealed class PatchAction

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious why the sealed class route vs a marker interface?

(#77 (comment))

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No reason - changed in the base PR in 5392a4e

@bradleydwyer
Copy link

bradleydwyer commented Oct 16, 2023

@bradleydwyer you made comments on a very old commit. Can you take another stab at it, please?

@andresuribe87 They were made 5 days ago, but held off hitting submit on the review to avoid distracting from deadline. Have duplicated the comment on the current HEAD with pointers back.

@@ -151,6 +169,11 @@ public data class OperationSuffixDataObject(
*/
public typealias Commitment = String

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Declared but not used in places like

public class MetadataMethod(
  public val published: Boolean,
  public val recoveryCommitment: String,
  public val updateCommitment: String,
)

which I presume it should be?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Absolutely! I massaged this a bit and now all instances are using the Commitment and Reveal classes.

public data class SidetreeUpdateOperation(
public val type: String,
public val didSuffix: String,
public val revealValue: String,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use the Reveal typealias?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@andresuribe87 andresuribe87 changed the title DID ION Update Operation DID ION Update, Recover, and Deactivate Operations Oct 24, 2023
Comment on lines -93 to -95
require(jwk.isPrivate) {
"Importing a non-private key is not permitted"
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the use-case driving the ability to import a public key?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to use the same test vectors from the ion-sdk repository. The update request tests uses the public key of the next update, but not the private key.

See https://github.com/TBD54566975/web5-kt/blob/ad6a91e1a8cac29c752156116fdf253395c5bf7c/dids/src/test/kotlin/web5/sdk/dids/DidIonTest.kt#L292 for where it's used.

And see https://github.com/decentralized-identity/ion-sdk/blob/main/tests/IonRequest.spec.ts#L48 for the original test.

@mistermoe
Copy link
Contributor

mistermoe commented Oct 24, 2023

Took some time to use the current ION implementation last night beyond the default scenario.

There's quite a bit of optionality that ends up creating a strange usage pattern where the caller effectively ends up having to use the keyManger outside ion to create keys for or associated to an ion did. I think it'd make more sense for the optionality to look something like this:

package web5.sdk.dids
import web5.sdk.crypto.InMemoryKeyManager

public fun main() {
  val km = InMemoryKeyManager()
  val opts = CreateDidIonOptions(
    services = listOf(
      Service(...)
    ),
    verificationMethods = listOf(
      IonVerificationMethod(
        alg: JWSAlgorithm.ES256K,
        relationships = listOf(VerificationRelationships.Authentication, VerificationRelationships.Assertion)
      )
    )
  )

  val did = DidIon.create(km)
}

other thoughts:

  • found the name DidIonManager to be confusing. instinctively assuming that it managed multiple ion dids. or was persistently stateful in some way. personally prefer DidIon
  • DidIonHandle was also a bit confusing. the general pattern that seemed to stick was the way in which DidKey is implemented
  • would be nice to use okhttp instead of ktor so that there's some congruency across kotlin repos. tbh though i haven't dug into kotlin multiplatform enough to know whether ktor actually helps here or not in a way that okhttp doesn't.

I think we can merge this PR into main and then address the above as a whole in a separate PR. thoughts?

didResolutionResult.didDocument.assertionMethodVerificationMethodsDereferenced

require(!assertionMethods.isNullOrEmpty()) {
throw SignatureException("no assertion methods found in did document")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
throw SignatureException("no assertion methods found in did document")
throw SignatureException("No assertion methods found in DID document")

@andresuribe87
Copy link
Contributor Author

thanks for the feedback @mistermoe
Will merge and we can address in a separate PR. Some thoughts for now below

Took some time to use the current ION implementation last night beyond the default scenario.

There's quite a bit of optionality that ends up creating a strange usage pattern where the caller effectively ends up having to use the keyManger outside ion to create keys for or associated to an ion did. I think it'd make more sense for the optionality to look something like this:

package web5.sdk.dids
import web5.sdk.crypto.InMemoryKeyManager

public fun main() {
  val km = InMemoryKeyManager()
  val opts = CreateDidIonOptions(
    services = listOf(
      Service(...)
    ),
    verificationMethods = listOf(
      IonVerificationMethod(
        alg: JWSAlgorithm.ES256K,
        relationships = listOf(VerificationRelationships.Authentication, VerificationRelationships.Assertion)
      )
    )
  )

  val did = DidIon.create(km)
}

I agree that there is too much optionality. That said, I'm not a fan of digressing from the VerificationMethod data model described in the spec. Let's chat more about it in the followup PR.

other thoughts:

  • found the name DidIonManager to be confusing. instinctively assuming that it managed multiple ion dids. or was persistently stateful in some way. personally prefer DidIon

Curious what made you think there wasn't any state within the manager?

  • It is stateful (the httpclient and the ionHost are part of the state that's kept).
  • It also has the ability to manage multiple dids (you can update any did).

My take on DidIon is that it doesn't tell me much about which actions I might call from the object. It sounds like you're proposing to use it like a namespace, rather than an actual object?

I originally thought to use DidIonFactory, but decided against it as a manager can update\recover\deactivate. See additional comments into the next point.

  • DidIonHandle was also a bit confusing. the general pattern that seemed to stick was the way in which DidKey is implemented

I actually found DidKey pattern to be confusing :P ; my mental model is that a Did object should follow what's defined as a DID in the spec). As such, a DID isn't aware of it's context, nor does it know what private keys it has access to. It's simply an identifier.

  • would be nice to use okhttp instead of ktor so that there's some congruency across kotlin repos. tbh though i haven't dug into kotlin multiplatform enough to know whether ktor actually helps here or not in a way that okhttp doesn't.

No preference whatsoever. I did an example PR in #86 showcasing what it would look like to support any http framework, which I discourage as it seems like overkill.

I think we can merge this PR into main and then address the above as a whole in a separate PR. thoughts?

@andresuribe87 andresuribe87 merged commit 38dfacb into main Oct 25, 2023
4 checks passed
@andresuribe87 andresuribe87 deleted the did_update_op branch October 25, 2023 02:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Implement did:ion Deactivate Operation Implement did:ion Update Operation
7 participants