From 9eca633d042f23be732b333fc1bc25dbaef00783 Mon Sep 17 00:00:00 2001 From: Jiyoon Koo Date: Thu, 7 Mar 2024 01:18:17 -0500 Subject: [PATCH 01/47] create bearerdid and portable did, keyexporter and keyimporter --- .../main/kotlin/web5/sdk/crypto/KeyManager.kt | 8 +++ .../kotlin/web5/sdk/dids/did/BearerDID.kt | 56 +++++++++++++++++++ .../kotlin/web5/sdk/dids/did/PortableDID.kt | 11 ++++ 3 files changed, 75 insertions(+) create mode 100644 dids/src/main/kotlin/web5/sdk/dids/did/BearerDID.kt create mode 100644 dids/src/main/kotlin/web5/sdk/dids/did/PortableDID.kt diff --git a/crypto/src/main/kotlin/web5/sdk/crypto/KeyManager.kt b/crypto/src/main/kotlin/web5/sdk/crypto/KeyManager.kt index af8269bcc..6a9cfb8f8 100644 --- a/crypto/src/main/kotlin/web5/sdk/crypto/KeyManager.kt +++ b/crypto/src/main/kotlin/web5/sdk/crypto/KeyManager.kt @@ -59,4 +59,12 @@ public interface KeyManager { * @throws IllegalArgumentException if the key is not known to the [KeyManager] */ public fun getDeterministicAlias(publicKey: JWK): String +} + +public interface KeyExporter { + public fun exportKey(keyId: String): JWK +} + +public interface KeyImporter { + public fun importKey(jwk: JWK): String } \ No newline at end of file diff --git a/dids/src/main/kotlin/web5/sdk/dids/did/BearerDID.kt b/dids/src/main/kotlin/web5/sdk/dids/did/BearerDID.kt new file mode 100644 index 000000000..57210be29 --- /dev/null +++ b/dids/src/main/kotlin/web5/sdk/dids/did/BearerDID.kt @@ -0,0 +1,56 @@ +package web5.sdk.dids.did + +import com.nimbusds.jose.jwk.JWK +import web5.sdk.crypto.KeyExporter +import web5.sdk.crypto.KeyManager +import web5.sdk.dids.didcore.DIDDocument +import web5.sdk.dids.didcore.DidUri +import web5.sdk.dids.didcore.VMSelector +import web5.sdk.dids.didcore.VerificationMethod + +public typealias DIDSigner = (payload: ByteArray) -> ByteArray + +public class BearerDID( + public val did: DidUri, + public val keyManager: KeyManager, + public val document: DIDDocument +) { + + public fun getSigner(selector: VMSelector): Pair { + val verificationMethod = document.selectVerificationMethod(selector) + + val keyAliasResult = runCatching { verificationMethod.publicKeyJwk?.computeThumbprint() } + val keyAlias = keyAliasResult.getOrNull() ?: throw Exception("Failed to compute key alias") + + val signer: DIDSigner = { payload -> + keyManager.sign(keyAlias.toString(), payload) + } + + return Pair(signer, verificationMethod) + } + + public fun export() : PortableDID { + + val keyExporter = keyManager as? KeyExporter + val privateKeys = mutableListOf() + + document.verificationMethod?.forEach { vm -> + val keyAliasResult = runCatching { vm.publicKeyJwk?.computeThumbprint() } + if (keyAliasResult.isSuccess) { + val keyAlias = keyAliasResult.getOrNull() + keyExporter?.exportKey(keyAlias!!.toString())?.let { key -> + privateKeys.add(key) + } + } + } + + return PortableDID( + uri = this.did.uri, + document = this.document, + privateKeys = privateKeys, + metadata = mapOf() + ) + } + + // todo swift doesn't have import() but go and js does. do i write that or nah +} \ No newline at end of file diff --git a/dids/src/main/kotlin/web5/sdk/dids/did/PortableDID.kt b/dids/src/main/kotlin/web5/sdk/dids/did/PortableDID.kt new file mode 100644 index 000000000..73313eefb --- /dev/null +++ b/dids/src/main/kotlin/web5/sdk/dids/did/PortableDID.kt @@ -0,0 +1,11 @@ +package web5.sdk.dids.did + +import com.nimbusds.jose.jwk.JWK +import web5.sdk.dids.didcore.DIDDocument + +public class PortableDID( + public val uri: String, + public val privateKeys: List, + public val document: DIDDocument, + public val metadata: Map + ) \ No newline at end of file From 12cb8452643e34a74014d51ae3b9cacc019d6576 Mon Sep 17 00:00:00 2001 From: Jiyoon Koo Date: Thu, 7 Mar 2024 01:40:07 -0500 Subject: [PATCH 02/47] changing Did to ChangemeDid to make it easier to spot and replace w BearerDid. changed DidUri to Did --- .../sdk/credentials/VerifiableCredential.kt | 6 ++-- .../sdk/credentials/VerifiablePresentation.kt | 6 ++-- .../web5/sdk/credentials/util/JwtUtil.kt | 20 ++++++------- .../credentials/VerifiableCredentialTest.kt | 6 ++-- .../main/kotlin/web5/sdk/dids/DidMethod.kt | 12 ++++---- .../main/kotlin/web5/sdk/dids/DidResolvers.kt | 8 +++--- .../kotlin/web5/sdk/dids/did/BearerDID.kt | 4 +-- .../sdk/dids/didcore/{DidUri.kt => Did.kt} | 8 +++--- .../web5/sdk/dids/extensions/DidExtensions.kt | 8 +++--- .../web5/sdk/dids/methods/dht/DidDht.kt | 20 ++++++------- .../web5/sdk/dids/methods/jwk/DidJwk.kt | 16 +++++------ .../web5/sdk/dids/methods/key/DidKey.kt | 14 +++++----- .../web5/sdk/dids/methods/web/DidWeb.kt | 18 ++++++------ .../kotlin/web5/sdk/dids/DidMethodTest.kt | 6 ++-- .../didcore/{DidUriTest.kt => DidTest.kt} | 28 +++++++++---------- .../web5/sdk/dids/methods/dht/DidDhtTest.kt | 2 -- 16 files changed, 89 insertions(+), 93 deletions(-) rename dids/src/main/kotlin/web5/sdk/dids/didcore/{DidUri.kt => Did.kt} (96%) rename dids/src/test/kotlin/web5/sdk/dids/didcore/{DidUriTest.kt => DidTest.kt} (61%) diff --git a/credentials/src/main/kotlin/web5/sdk/credentials/VerifiableCredential.kt b/credentials/src/main/kotlin/web5/sdk/credentials/VerifiableCredential.kt index f987bd374..db2bcd9a8 100644 --- a/credentials/src/main/kotlin/web5/sdk/credentials/VerifiableCredential.kt +++ b/credentials/src/main/kotlin/web5/sdk/credentials/VerifiableCredential.kt @@ -14,7 +14,7 @@ import com.nimbusds.jwt.JWTClaimsSet import com.nimbusds.jwt.JWTParser import com.nimbusds.jwt.SignedJWT import web5.sdk.credentials.util.JwtUtil -import web5.sdk.dids.Did +import web5.sdk.dids.ChangemeDid import java.net.URI import java.security.SignatureException import java.util.Date @@ -53,7 +53,7 @@ public class VerifiableCredential internal constructor(public val vcDataModel: V * If the [assertionMethodId] is null, the function will attempt to use the first available verification method from * the [did]. The result is a String in a JWT format. * - * @param did The [Did] used to sign the credential. + * @param did The [ChangemeDid] used to sign the credential. * @param assertionMethodId An optional identifier for the assertion method that will be used for verification of the * produced signature. * @return The JWT representing the signed verifiable credential. @@ -64,7 +64,7 @@ public class VerifiableCredential internal constructor(public val vcDataModel: V * ``` */ @JvmOverloads - public fun sign(did: Did, assertionMethodId: String? = null): String { + public fun sign(did: ChangemeDid, assertionMethodId: String? = null): String { val payload = JWTClaimsSet.Builder() .issuer(vcDataModel.issuer.toString()) .issueTime(vcDataModel.issuanceDate) diff --git a/credentials/src/main/kotlin/web5/sdk/credentials/VerifiablePresentation.kt b/credentials/src/main/kotlin/web5/sdk/credentials/VerifiablePresentation.kt index ffd4bc14d..04b7d366f 100644 --- a/credentials/src/main/kotlin/web5/sdk/credentials/VerifiablePresentation.kt +++ b/credentials/src/main/kotlin/web5/sdk/credentials/VerifiablePresentation.kt @@ -7,7 +7,7 @@ import com.nimbusds.jwt.JWTClaimsSet import com.nimbusds.jwt.JWTParser import com.nimbusds.jwt.SignedJWT import web5.sdk.credentials.util.JwtUtil -import web5.sdk.dids.Did +import web5.sdk.dids.ChangemeDid import java.net.URI import java.security.SignatureException import java.util.Date @@ -47,7 +47,7 @@ public class VerifiablePresentation internal constructor(public val vpDataModel: * If the [assertionMethodId] is null, the function will attempt to use the first available verification method from * the [did]. The result is a String in a JWT format. * - * @param did The [Did] used to sign the credential. + * @param did The [ChangemeDid] used to sign the credential. * @param assertionMethodId An optional identifier for the assertion method that will be used for verification of the * produced signature. * @return The JWT representing the signed verifiable credential. @@ -58,7 +58,7 @@ public class VerifiablePresentation internal constructor(public val vpDataModel: * ``` */ @JvmOverloads - public fun sign(did: Did, assertionMethodId: String? = null): String { + public fun sign(did: ChangemeDid, assertionMethodId: String? = null): String { val payload = JWTClaimsSet.Builder() .issuer(did.uri) .issueTime(Date()) diff --git a/credentials/src/main/kotlin/web5/sdk/credentials/util/JwtUtil.kt b/credentials/src/main/kotlin/web5/sdk/credentials/util/JwtUtil.kt index 701b0453c..b17402a99 100644 --- a/credentials/src/main/kotlin/web5/sdk/credentials/util/JwtUtil.kt +++ b/credentials/src/main/kotlin/web5/sdk/credentials/util/JwtUtil.kt @@ -9,9 +9,9 @@ import com.nimbusds.jwt.JWTParser import com.nimbusds.jwt.SignedJWT import web5.sdk.common.Convert import web5.sdk.crypto.Crypto -import web5.sdk.dids.Did +import web5.sdk.dids.ChangemeDid import web5.sdk.dids.DidResolvers -import web5.sdk.dids.didcore.DidUri +import web5.sdk.dids.didcore.Did import web5.sdk.dids.exceptions.DidResolutionException import web5.sdk.dids.exceptions.PublicKeyJwkMissingException import java.net.URI @@ -31,10 +31,10 @@ public object JwtUtil { * If the [assertionMethodId] is null, the function will attempt to use the first available verification method from * the [did]. The result is a String in a JWT format. * - * @param did The [Did] used to sign the credential. + * @param did The [ChangemeDid] used to sign the credential. * @param assertionMethodId An optional identifier for the assertion method * that will be used for verification of the produced signature. - * @param jwtPayload the payload that is getting signed by the [Did] + * @param jwtPayload the payload that is getting signed by the [ChangemeDid] * @return The JWT representing the signed verifiable credential. * * Example: @@ -42,7 +42,7 @@ public object JwtUtil { * val signedVc = verifiableCredential.sign(myDid) * ``` */ - public fun sign(did: Did, assertionMethodId: String?, jwtPayload: JWTClaimsSet): String { + public fun sign(did: ChangemeDid, assertionMethodId: String?, jwtPayload: JWTClaimsSet): String { val didResolutionResult = DidResolvers.resolve(did.uri) val didDocument = didResolutionResult.didDocument if (didResolutionResult.didResolutionMetadata.error != null || didDocument == null) { @@ -108,13 +108,13 @@ public object JwtUtil { } val verificationMethodId = jwt.header.keyID - val didUri = DidUri.Parser.parse(verificationMethodId) + val did = Did.Parser.parse(verificationMethodId) - val didResolutionResult = DidResolvers.resolve(didUri.url) + val didResolutionResult = DidResolvers.resolve(did.url) if (didResolutionResult.didResolutionMetadata.error != null) { throw SignatureException( "Signature verification failed: " + - "Failed to resolve DID ${didUri.url}. " + + "Failed to resolve DID ${did.url}. " + "Error: ${didResolutionResult.didResolutionMetadata.error}" ) } @@ -123,8 +123,8 @@ public object JwtUtil { // or just `#fragment`. See: https://www.w3.org/TR/did-core/#relative-did-urls. // using a set for fast string comparison. DIDs can be lonnng. val verificationMethodIds = setOf( - didUri.url, - "#${didUri.fragment}" + did.url, + "#${did.fragment}" ) didResolutionResult.didDocument?.assertionMethod?.firstOrNull { diff --git a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt index 8f5e29533..8fb8d71f4 100644 --- a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt +++ b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt @@ -16,7 +16,7 @@ import org.junit.jupiter.api.assertDoesNotThrow import web5.sdk.crypto.AlgorithmId import web5.sdk.crypto.AwsKeyManager import web5.sdk.crypto.InMemoryKeyManager -import web5.sdk.dids.Did +import web5.sdk.dids.ChangemeDid import web5.sdk.dids.didcore.Purpose import web5.sdk.dids.extensions.load import web5.sdk.dids.methods.dht.CreateDidDhtOptions @@ -252,8 +252,8 @@ class Web5TestVectorsCredentials { val keyManager = InMemoryKeyManager() keyManager.import(listOf(vector.input.signerPrivateJwk!!)) - val issuerDid = Did.load(vector.input.signerDidUri!!, keyManager) - val vcJwt = vc.sign(issuerDid) + val issuerChangemeDid = ChangemeDid.load(vector.input.signerDidUri!!, keyManager) + val vcJwt = vc.sign(issuerChangemeDid) assertEquals(vector.output, vcJwt, vector.description) } diff --git a/dids/src/main/kotlin/web5/sdk/dids/DidMethod.kt b/dids/src/main/kotlin/web5/sdk/dids/DidMethod.kt index 42d534c64..7ac732226 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/DidMethod.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/DidMethod.kt @@ -1,7 +1,7 @@ package web5.sdk.dids import web5.sdk.crypto.KeyManager -import web5.sdk.dids.didcore.DidUri +import web5.sdk.dids.didcore.Did import web5.sdk.dids.exceptions.PublicKeyJwkMissingException /** @@ -26,7 +26,7 @@ import web5.sdk.dids.exceptions.PublicKeyJwkMissingException * Implementers should adhere to the respective DID method specifications ensuring both compliance * and interoperability across different DID networks. */ -public abstract class Did(public val uri: String, public val keyManager: KeyManager) { +public abstract class ChangemeDid(public val uri: String, public val keyManager: KeyManager) { public companion object { // static helper methods here } @@ -96,7 +96,7 @@ public interface ResolveDidOptions * Implementations of this interface should provide method-specific logic for * creating and resolving DIDs under a particular method. * - * @param T The type of DID that this method can create and resolve, extending [Did]. + * @param T The type of DID that this method can create and resolve, extending [ChangemeDid]. * * ### Example of a Custom DID Method Implementation: * ``` @@ -119,7 +119,7 @@ public interface ResolveDidOptions * - Ensure that cryptographic operations utilize secure and tested libraries, ensuring * the reliability and security of DIDs managed by this method. */ -public interface DidMethod { +public interface DidMethod { /** * A string that specifies the name of the DID method. * @@ -169,9 +169,9 @@ public interface DidMethod { } -internal fun DidMethod.validateKeyMaterialInsideKeyManager( +internal fun DidMethod.validateKeyMaterialInsideKeyManager( did: String, keyManager: KeyManager) { - require(DidUri.parse(did).method == methodName) { + require(Did.parse(did).method == methodName) { "did must start with the prefix \"did:$methodName\", but got $did" } val didResolutionResult = resolve(did) diff --git a/dids/src/main/kotlin/web5/sdk/dids/DidResolvers.kt b/dids/src/main/kotlin/web5/sdk/dids/DidResolvers.kt index 274384a53..f3898778d 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/DidResolvers.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/DidResolvers.kt @@ -1,6 +1,6 @@ package web5.sdk.dids -import web5.sdk.dids.didcore.DidUri +import web5.sdk.dids.didcore.Did import web5.sdk.dids.extensions.supportedMethods /** @@ -32,9 +32,9 @@ public object DidResolvers { * @throws IllegalArgumentException if resolving the specified DID method is not supported. */ public fun resolve(didUrl: String, options: ResolveDidOptions? = null): DidResolutionResult { - val parsedDidUri = DidUri.parse(didUrl) - val resolver = methodResolvers.getOrElse(parsedDidUri.method) { - throw IllegalArgumentException("Resolving did:${parsedDidUri.method} not supported") + val did = Did.parse(didUrl) + val resolver = methodResolvers.getOrElse(did.method) { + throw IllegalArgumentException("Resolving did:${did.method} not supported") } return resolver(didUrl, options) diff --git a/dids/src/main/kotlin/web5/sdk/dids/did/BearerDID.kt b/dids/src/main/kotlin/web5/sdk/dids/did/BearerDID.kt index 57210be29..9061cbfa0 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/did/BearerDID.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/did/BearerDID.kt @@ -4,14 +4,14 @@ import com.nimbusds.jose.jwk.JWK import web5.sdk.crypto.KeyExporter import web5.sdk.crypto.KeyManager import web5.sdk.dids.didcore.DIDDocument -import web5.sdk.dids.didcore.DidUri +import web5.sdk.dids.didcore.Did import web5.sdk.dids.didcore.VMSelector import web5.sdk.dids.didcore.VerificationMethod public typealias DIDSigner = (payload: ByteArray) -> ByteArray public class BearerDID( - public val did: DidUri, + public val did: Did, public val keyManager: KeyManager, public val document: DIDDocument ) { diff --git a/dids/src/main/kotlin/web5/sdk/dids/didcore/DidUri.kt b/dids/src/main/kotlin/web5/sdk/dids/didcore/Did.kt similarity index 96% rename from dids/src/main/kotlin/web5/sdk/dids/didcore/DidUri.kt rename to dids/src/main/kotlin/web5/sdk/dids/didcore/Did.kt index 7b13cdc2f..5249134cd 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/didcore/DidUri.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/didcore/Did.kt @@ -28,7 +28,7 @@ import java.util.regex.Pattern * a specific part of a DID document. * Spec: https://www.w3.org/TR/did-core/#fragment */ -public class DidUri( +public class Did( public val uri: String, public val url: String, public val method: String, @@ -57,7 +57,7 @@ public class DidUri( * @param byteArray * @return DID object */ - public fun unmarshalText(byteArray: ByteArray): DidUri { + public fun unmarshalText(byteArray: ByteArray): Did { return parse(byteArray.toString(Charsets.UTF_8)) } @@ -85,7 +85,7 @@ public class DidUri( * @param didUri The DID URI to parse. * @return DID object */ - public fun parse(didUri: String): DidUri { + public fun parse(didUri: String): Did { val matcher = DID_URI_PATTERN.matcher(didUri) matcher.find() if (!matcher.matches()) { @@ -111,7 +111,7 @@ public class DidUri( val query = matcher.group(7)?.drop(1) val fragment = matcher.group(8)?.drop(1) - return DidUri( + return Did( uri = "did:$method:$id", url = didUri, method = method, diff --git a/dids/src/main/kotlin/web5/sdk/dids/extensions/DidExtensions.kt b/dids/src/main/kotlin/web5/sdk/dids/extensions/DidExtensions.kt index 36440b331..b64ee08b0 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/extensions/DidExtensions.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/extensions/DidExtensions.kt @@ -1,8 +1,8 @@ package web5.sdk.dids.extensions import web5.sdk.crypto.KeyManager -import web5.sdk.dids.Did -import web5.sdk.dids.didcore.DidUri +import web5.sdk.dids.ChangemeDid +import web5.sdk.dids.didcore.Did import web5.sdk.dids.methods.dht.DidDht import web5.sdk.dids.methods.jwk.DidJwk import web5.sdk.dids.methods.key.DidKey @@ -20,6 +20,6 @@ internal val supportedMethods = mapOf( * signing and managing the passed in [didUri] exists within the provided [keyManager]. This function is meant * to be used when the method of the DID is unknown. */ -public fun Did.Companion.load(didUri: String, keyManager: KeyManager): Did { - return supportedMethods.getValue(DidUri.parse(didUri).method).load(didUri, keyManager) +public fun ChangemeDid.Companion.load(didUri: String, keyManager: KeyManager): ChangemeDid { + return supportedMethods.getValue(Did.parse(didUri).method).load(didUri, keyManager) } \ No newline at end of file diff --git a/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt b/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt index 7fd485981..4118e5c54 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt @@ -19,12 +19,12 @@ import web5.sdk.crypto.Ed25519 import web5.sdk.crypto.KeyManager import web5.sdk.crypto.Secp256k1 import web5.sdk.dids.CreateDidOptions -import web5.sdk.dids.Did +import web5.sdk.dids.ChangemeDid import web5.sdk.dids.DidMethod import web5.sdk.dids.DidResolutionResult import web5.sdk.dids.ResolutionError import web5.sdk.dids.ResolveDidOptions -import web5.sdk.dids.didcore.DidUri +import web5.sdk.dids.didcore.Did import web5.sdk.dids.didcore.DIDDocument import web5.sdk.dids.didcore.DIDDocumentMetadata import web5.sdk.dids.didcore.Purpose @@ -298,10 +298,10 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) : DidMethod { - DidUri.Parser.parse(did) + Did.Parser.parse(did) } assertEquals("Invalid DID URI", exception.message) } @@ -45,15 +43,15 @@ class DidUriTest { fun `Parser parses a valid did`() { // todo adding /path after abcdefghi messes up the parsing of params (comes in null) // to be addressed via gh issue https://github.com/TBD54566975/web5-spec/issues/120 - val didUri = DidUri.Parser.parse("did:example:123456789abcdefghi;foo=bar;baz=qux?foo=bar&baz=qux#keys-1") - assertEquals("did:example:123456789abcdefghi", didUri.uri) - assertEquals("123456789abcdefghi", didUri.id) - assertEquals("did:example:123456789abcdefghi;foo=bar;baz=qux?foo=bar&baz=qux#keys-1", didUri.url) - assertEquals("example", didUri.method) - assertEquals("123456789abcdefghi", didUri.id) - assertEquals("foo=bar&baz=qux", didUri.query) - assertEquals("keys-1", didUri.fragment) - assertEquals(mapOf("foo" to "bar", "baz" to "qux"), didUri.params) + val did = Did.Parser.parse("did:example:123456789abcdefghi;foo=bar;baz=qux?foo=bar&baz=qux#keys-1") + assertEquals("did:example:123456789abcdefghi", did.uri) + assertEquals("123456789abcdefghi", did.id) + assertEquals("did:example:123456789abcdefghi;foo=bar;baz=qux?foo=bar&baz=qux#keys-1", did.url) + assertEquals("example", did.method) + assertEquals("123456789abcdefghi", did.id) + assertEquals("foo=bar&baz=qux", did.query) + assertEquals("keys-1", did.fragment) + assertEquals(mapOf("foo" to "bar", "baz" to "qux"), did.params) } } \ No newline at end of file diff --git a/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DidDhtTest.kt b/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DidDhtTest.kt index 1a8f7afa1..54d0d41a2 100644 --- a/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DidDhtTest.kt +++ b/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DidDhtTest.kt @@ -2,8 +2,6 @@ package web5.sdk.dids.methods.dht import com.fasterxml.jackson.core.type.TypeReference import com.fasterxml.jackson.databind.annotation.JsonDeserialize -import com.fasterxml.jackson.databind.annotation.JsonSerialize -import com.fasterxml.jackson.databind.node.ObjectNode import com.nimbusds.jose.jwk.Curve import com.nimbusds.jose.jwk.JWK import com.nimbusds.jose.jwk.gen.ECKeyGenerator From 28e683a2f6dbac225f5028bf47cd3060c8f905d4 Mon Sep 17 00:00:00 2001 From: Jiyoon Koo Date: Thu, 7 Mar 2024 02:37:29 -0500 Subject: [PATCH 03/47] removing didmethod interface --- .../main/kotlin/web5/sdk/dids/DidMethod.kt | 197 +++++++++--------- .../web5/sdk/dids/methods/dht/DidDht.kt | 16 +- .../web5/sdk/dids/methods/jwk/DidJwk.kt | 27 +-- .../web5/sdk/dids/methods/key/DidKey.kt | 27 +-- .../web5/sdk/dids/methods/web/DidWeb.kt | 15 +- 5 files changed, 117 insertions(+), 165 deletions(-) diff --git a/dids/src/main/kotlin/web5/sdk/dids/DidMethod.kt b/dids/src/main/kotlin/web5/sdk/dids/DidMethod.kt index 7ac732226..f0960c027 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/DidMethod.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/DidMethod.kt @@ -84,101 +84,102 @@ public interface CreationMetadata */ public interface ResolveDidOptions -/** - * An interface defining operations for DID methods in accordance with the W3C DID standard. - * - * A DID method is a specific set of rules for creating, updating, and revoking DIDs, - * specified in a DID method specification. Different DID methods utilize different - * consensus mechanisms, cryptographic algorithms, and registries (or none at all). - * The purpose of `DidMethod` implementations is to provide logic tailored to a - * particular method while adhering to the broader operations outlined in the W3C DID standard. - * - * Implementations of this interface should provide method-specific logic for - * creating and resolving DIDs under a particular method. - * - * @param T The type of DID that this method can create and resolve, extending [ChangemeDid]. - * - * ### Example of a Custom DID Method Implementation: - * ``` - * class ExampleDidMethod : DidMethod { - * override val methodName: String = "example" - * - * override fun create(keyManager: KeyManager, options: ExampleCreateDidOptions?): ExampleDid { - * // Implementation-specific logic for creating DIDs. - * } - * - * override fun resolve(didUrl: String, opts: ResolveDidOpts?): DidResolutionResult { - * // Implementation-specific logic for resolving DIDs. - * } - * } - * ``` - * - * ### Notes: - * - Ensure conformance with the relevant DID method specification for accurate and - * interoperable functionality. - * - Ensure that cryptographic operations utilize secure and tested libraries, ensuring - * the reliability and security of DIDs managed by this method. - */ -public interface DidMethod { - /** - * A string that specifies the name of the DID method. - * - * For instance, in the DID `did:example:123456`, "example" would be the method name. - */ - public val methodName: String - - /** - * Creates a new DID. - * - * This function should generate a new DID according to the rules of the specific - * method being implemented, using the provided [KeyManager] and optionally considering - * any provided [CreateDidOptions]. - * - * @param keyManager An instance of [KeyManager] responsible for cryptographic operations. - * @param options Optionally, an instance of [CreateDidOptions] providing additional options - * or requirements for DID creation. - * @return A new instance of type [T], representing the created DID. - */ - public fun create(keyManager: KeyManager, options: O? = null): T - - /** - * Resolves a DID to its associated DID Document. - * - * This function should retrieve and return the DID Document associated with the provided - * DID URI, in accordance with the rules and mechanisms of the specific DID method being - * implemented, and optionally considering any provided [ResolveDidOptions]. - * - * @param did A string containing the DID URI to be resolved. - * @param options Optionally, an instance of [ResolveDidOptions] providing additional options - * or requirements for DID resolution. - * @return An instance of [DidResolutionResult] containing the resolved DID Document and - * any associated metadata. - */ - public fun resolve(did: String, options: ResolveDidOptions? = null): DidResolutionResult - - /** - * Returns an instance of [T] for [uri]. This function validates that all the key material needed for signing and - * managing the passed in [uri] exists within the provided [keyManager]. - * - * @param uri A string containing the DID URI to load. - * @param keyManager An instance of [KeyManager] that should contain all the key material needed for signing and - * managing the passed in [did]. - * @return An instance of [T] representing the loaded DID. - */ - public fun load(uri: String, keyManager: KeyManager): T -} - - -internal fun DidMethod.validateKeyMaterialInsideKeyManager( - did: String, keyManager: KeyManager) { - require(Did.parse(did).method == methodName) { - "did must start with the prefix \"did:$methodName\", but got $did" - } - val didResolutionResult = resolve(did) - - didResolutionResult.didDocument!!.verificationMethod?.forEach { - val publicKeyJwk = it.publicKeyJwk ?: throw PublicKeyJwkMissingException("publicKeyJwk is null") - val keyAlias = keyManager.getDeterministicAlias(publicKeyJwk) - keyManager.getPublicKey(keyAlias) - } -} \ No newline at end of file +///** +// * An interface defining operations for DID methods in accordance with the W3C DID standard. +// * +// * A DID method is a specific set of rules for creating, updating, and revoking DIDs, +// * specified in a DID method specification. Different DID methods utilize different +// * consensus mechanisms, cryptographic algorithms, and registries (or none at all). +// * The purpose of `DidMethod` implementations is to provide logic tailored to a +// * particular method while adhering to the broader operations outlined in the W3C DID standard. +// * +// * Implementations of this interface should provide method-specific logic for +// * creating and resolving DIDs under a particular method. +// * +// * @param T The type of DID that this method can create and resolve, extending [ChangemeDid]. +// * +// * ### Example of a Custom DID Method Implementation: +// * ``` +// * class ExampleDidMethod : DidMethod { +// * override val methodName: String = "example" +// * +// * override fun create(keyManager: KeyManager, options: ExampleCreateDidOptions?): ExampleDid { +// * // Implementation-specific logic for creating DIDs. +// * } +// * +// * override fun resolve(didUrl: String, opts: ResolveDidOpts?): DidResolutionResult { +// * // Implementation-specific logic for resolving DIDs. +// * } +// * } +// * ``` +// * +// * ### Notes: +// * - Ensure conformance with the relevant DID method specification for accurate and +// * interoperable functionality. +// * - Ensure that cryptographic operations utilize secure and tested libraries, ensuring +// * the reliability and security of DIDs managed by this method. +// */ +//public interface DidMethod { +// /** +// * A string that specifies the name of the DID method. +// * +// * For instance, in the DID `did:example:123456`, "example" would be the method name. +// */ +// public val methodName: String +// +// /** +// * Creates a new DID. +// * +// * This function should generate a new DID according to the rules of the specific +// * method being implemented, using the provided [KeyManager] and optionally considering +// * any provided [CreateDidOptions]. +// * +// * @param keyManager An instance of [KeyManager] responsible for cryptographic operations. +// * @param options Optionally, an instance of [CreateDidOptions] providing additional options +// * or requirements for DID creation. +// * @return A new instance of type [T], representing the created DID. +// */ +// public fun create(keyManager: KeyManager, options: O? = null): T +// +// /** +// * Resolves a DID to its associated DID Document. +// * +// * This function should retrieve and return the DID Document associated with the provided +// * DID URI, in accordance with the rules and mechanisms of the specific DID method being +// * implemented, and optionally considering any provided [ResolveDidOptions]. +// * +// * @param did A string containing the DID URI to be resolved. +// * @param options Optionally, an instance of [ResolveDidOptions] providing additional options +// * or requirements for DID resolution. +// * @return An instance of [DidResolutionResult] containing the resolved DID Document and +// * any associated metadata. +// */ +// public fun resolve(did: String, options: ResolveDidOptions? = null): DidResolutionResult +// +// /** +// * Returns an instance of [T] for [uri]. This function validates that all the key material needed for signing and +// * managing the passed in [uri] exists within the provided [keyManager]. +// * +// * @param uri A string containing the DID URI to load. +// * @param keyManager An instance of [KeyManager] that should contain all the key material needed for signing and +// * managing the passed in [did]. +// * @return An instance of [T] representing the loaded DID. +// */ +// public fun load(uri: String, keyManager: KeyManager): T +//} +// +// +//// todo do i just remove this? +//internal fun DidMethod.validateKeyMaterialInsideKeyManager( +// did: String, keyManager: KeyManager) { +// require(Did.parse(did).method == methodName) { +// "did must start with the prefix \"did:$methodName\", but got $did" +// } +// val didResolutionResult = resolve(did) +// +// didResolutionResult.didDocument!!.verificationMethod?.forEach { +// val publicKeyJwk = it.publicKeyJwk ?: throw PublicKeyJwkMissingException("publicKeyJwk is null") +// val keyAlias = keyManager.getDeterministicAlias(publicKeyJwk) +// keyManager.getPublicKey(keyAlias) +// } +//} \ No newline at end of file diff --git a/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt b/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt index 4118e5c54..253450f68 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt @@ -20,7 +20,6 @@ import web5.sdk.crypto.KeyManager import web5.sdk.crypto.Secp256k1 import web5.sdk.dids.CreateDidOptions import web5.sdk.dids.ChangemeDid -import web5.sdk.dids.DidMethod import web5.sdk.dids.DidResolutionResult import web5.sdk.dids.ResolutionError import web5.sdk.dids.ResolveDidOptions @@ -35,7 +34,6 @@ import web5.sdk.dids.exceptions.InvalidIdentifierSizeException import web5.sdk.dids.exceptions.InvalidMethodNameException import web5.sdk.dids.exceptions.PkarrRecordNotFoundException import web5.sdk.dids.exceptions.PublicKeyJwkMissingException -import web5.sdk.dids.validateKeyMaterialInsideKeyManager /** * Configuration for the [DidDhtApi]. @@ -106,13 +104,13 @@ private val logger = KotlinLogging.logger {} /** * Base class for managing DID DHT operations. Uses the given [DidDhtConfiguration]. */ -public sealed class DidDhtApi(configuration: DidDhtConfiguration) : DidMethod { +public sealed class DidDhtApi(configuration: DidDhtConfiguration) { private val engine: HttpClientEngine = configuration.engine private val dhtClient = DhtClient(configuration.gateway, engine) private val ttl: Long = 7200 - override val methodName: String = "dht" + public val methodName: String = "dht" /** * Creates a new "did:dht" DID, derived from an initial identity key, and stores the associated private key in the @@ -127,7 +125,7 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) : DidMethod { - override val methodName: String = "jwk" + public companion object { + public const val methodName: String = "jwk" /** * Creates a new "did:jwk" DID, derived from a public key, and stores the associated private key in the @@ -82,7 +80,7 @@ public class DidJwk(uri: String, keyManager: KeyManager) : ChangemeDid(uri, keyM * * @throws UnsupportedOperationException if the specified curve is not supported. */ - override fun create(keyManager: KeyManager, options: CreateDidJwkOptions?): DidJwk { + public fun create(keyManager: KeyManager, options: CreateDidJwkOptions?): DidJwk { val opts = options ?: CreateDidJwkOptions() val keyAlias = keyManager.generatePrivateKey(opts.algorithmId) @@ -106,7 +104,7 @@ public class DidJwk(uri: String, keyManager: KeyManager) : ChangemeDid(uri, keyM * * @throws IllegalArgumentException if the provided DID does not conform to the "did:jwk" method. */ - override fun resolve(did: String, options: ResolveDidOptions?): DidResolutionResult { + public fun resolve(did: String, options: ResolveDidOptions?): DidResolutionResult { val parsedDid = try { Did.parse(did) } catch (_: ParserException) { @@ -177,20 +175,5 @@ public class DidJwk(uri: String, keyManager: KeyManager) : ChangemeDid(uri, keyM return DidResolutionResult(didDocument = didDocument, context = "https://w3id.org/did-resolution/v1") } - - /** - * Instantiates a [DidJwk] instance from [uri] (which has to start with "did:jwk:"), and validates that the - * associated key material exists in the provided [keyManager]. - * - * ### Usage Example: - * ```kotlin - * val keyManager = InMemoryKeyManager() - * val did = DidJwk.load("did:jwk:example", keyManager) - * ``` - */ - override fun load(uri: String, keyManager: KeyManager): DidJwk { - validateKeyMaterialInsideKeyManager(uri, keyManager) - return DidJwk(uri, keyManager) - } } } diff --git a/dids/src/main/kotlin/web5/sdk/dids/methods/key/DidKey.kt b/dids/src/main/kotlin/web5/sdk/dids/methods/key/DidKey.kt index 6b3bbab94..69da57f81 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/methods/key/DidKey.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/methods/key/DidKey.kt @@ -8,14 +8,12 @@ import web5.sdk.crypto.KeyManager import web5.sdk.crypto.Secp256k1 import web5.sdk.dids.CreateDidOptions import web5.sdk.dids.ChangemeDid -import web5.sdk.dids.DidMethod import web5.sdk.dids.DidResolutionResult import web5.sdk.dids.ResolveDidOptions import web5.sdk.dids.didcore.Did import web5.sdk.dids.didcore.DIDDocument import web5.sdk.dids.didcore.Purpose import web5.sdk.dids.didcore.VerificationMethod -import web5.sdk.dids.validateKeyMaterialInsideKeyManager /** * Specifies options for creating a new "did:key" Decentralized Identifier (DID). @@ -57,11 +55,11 @@ public class DidKey(uri: String, keyManager: KeyManager) : ChangemeDid(uri, keyM * @throws IllegalArgumentException if the provided DID does not conform to the "did:key" method. */ public fun resolve(): DidResolutionResult { - return resolve(this.uri) + return resolve(this.uri, null) } - public companion object : DidMethod { - override val methodName: String = "key" + public companion object { + public val methodName: String = "key" /** * Creates a new "did:key" DID, derived from a public key, and stores the associated private key in the @@ -77,7 +75,7 @@ public class DidKey(uri: String, keyManager: KeyManager) : ChangemeDid(uri, keyM * * @throws UnsupportedOperationException if the specified curve is not supported. */ - override fun create(keyManager: KeyManager, options: CreateDidKeyOptions?): DidKey { + public fun create(keyManager: KeyManager, options: CreateDidKeyOptions?): DidKey { val opts = options ?: CreateDidKeyOptions() val keyAlias = keyManager.generatePrivateKey(opts.algorithmId) @@ -100,21 +98,6 @@ public class DidKey(uri: String, keyManager: KeyManager) : ChangemeDid(uri, keyM return DidKey(did, keyManager) } - /** - * Instantiates a [DidKey] instance from [uri] (which has to start with "did:key:"), and validates that the - * associated key material exists in the provided [keyManager]. - * - * ### Usage Example: - * ```kotlin - * val keyManager = InMemoryKeyManager() - * val did = DidKey.load("did:key:example", keyManager) - * ``` - */ - override fun load(uri: String, keyManager: KeyManager): DidKey { - validateKeyMaterialInsideKeyManager(uri, keyManager) - return DidKey(uri, keyManager) - } - /** * Resolves a "did:key" DID into a [DidResolutionResult], which contains the DID Document and possible related metadata. * @@ -126,7 +109,7 @@ public class DidKey(uri: String, keyManager: KeyManager) : ChangemeDid(uri, keyM * * @throws IllegalArgumentException if the provided DID does not conform to the "did:key" method. */ - override fun resolve(did: String, options: ResolveDidOptions?): DidResolutionResult { + public fun resolve(did: String, options: ResolveDidOptions?): DidResolutionResult { val parsedDid = Did.parse(did) require(parsedDid.method == methodName) { throw IllegalArgumentException("expected did:key") } diff --git a/dids/src/main/kotlin/web5/sdk/dids/methods/web/DidWeb.kt b/dids/src/main/kotlin/web5/sdk/dids/methods/web/DidWeb.kt index 72df2e153..6232de3ec 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/methods/web/DidWeb.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/methods/web/DidWeb.kt @@ -24,12 +24,10 @@ import web5.sdk.dids.CreateDidOptions import web5.sdk.dids.didcore.Did import web5.sdk.dids.didcore.DIDDocument import web5.sdk.dids.ChangemeDid -import web5.sdk.dids.DidMethod import web5.sdk.dids.DidResolutionResult import web5.sdk.dids.exceptions.ParserException import web5.sdk.dids.ResolutionError import web5.sdk.dids.ResolveDidOptions -import web5.sdk.dids.validateKeyMaterialInsideKeyManager import java.io.File import java.net.InetAddress import java.net.URL @@ -95,7 +93,7 @@ private const val DID_DOC_FILE_NAME = "/did.json" */ public sealed class DidWebApi( configuration: DidWebApiConfiguration -) : DidMethod { +) { private val logger = KotlinLogging.logger {} @@ -120,9 +118,9 @@ public sealed class DidWebApi( } } - override val methodName: String = "web" + public val methodName: String = "web" - override fun resolve(did: String, options: ResolveDidOptions?): DidResolutionResult { + public fun resolve(did: String, options: ResolveDidOptions?): DidResolutionResult { return try { resolveInternal(did, options) } catch (e: Exception) { @@ -163,11 +161,6 @@ public sealed class DidWebApi( ) } - override fun load(uri: String, keyManager: KeyManager): DidWeb { - validateKeyMaterialInsideKeyManager(uri, keyManager) - return DidWeb(uri, keyManager, this) - } - private fun getDocURL(parsedDid: Did): String { val domainNameWithPath = parsedDid.id.replace(":", "/") val decodedDomain = URLDecoder.decode(domainNameWithPath, UTF_8) @@ -182,7 +175,7 @@ public sealed class DidWebApi( return targetUrl.toString() } - public override fun create(keyManager: KeyManager, options: CreateDidOptions?): DidWeb { + public fun create(keyManager: KeyManager, options: CreateDidOptions?): DidWeb { throw UnsupportedOperationException("Create operation is not supported for did:web") } } \ No newline at end of file From 01069201a33ccd932200888fa9796b39e8cff1ee Mon Sep 17 00:00:00 2001 From: Jiyoon Koo Date: Thu, 7 Mar 2024 14:24:51 -0500 Subject: [PATCH 04/47] trying on BearerDID for did jwk --- .../web5/sdk/dids/methods/jwk/DidJwk.kt | 61 +++++++++++++------ 1 file changed, 43 insertions(+), 18 deletions(-) diff --git a/dids/src/main/kotlin/web5/sdk/dids/methods/jwk/DidJwk.kt b/dids/src/main/kotlin/web5/sdk/dids/methods/jwk/DidJwk.kt index a1f510bba..f2beb4ac1 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/methods/jwk/DidJwk.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/methods/jwk/DidJwk.kt @@ -5,13 +5,14 @@ import com.nimbusds.jose.jwk.KeyUse import web5.sdk.common.Convert import web5.sdk.common.EncodingFormat import web5.sdk.crypto.AlgorithmId +import web5.sdk.crypto.InMemoryKeyManager import web5.sdk.crypto.KeyManager import web5.sdk.dids.CreateDidOptions -import web5.sdk.dids.ChangemeDid import web5.sdk.dids.DidResolutionMetadata import web5.sdk.dids.DidResolutionResult import web5.sdk.dids.ResolutionError import web5.sdk.dids.ResolveDidOptions +import web5.sdk.dids.did.BearerDID import web5.sdk.dids.didcore.Did import web5.sdk.dids.didcore.DIDDocument import web5.sdk.dids.didcore.Purpose @@ -33,9 +34,14 @@ import java.text.ParseException * ``` */ public class CreateDidJwkOptions( - public val algorithmId: AlgorithmId = AlgorithmId.secp256k1, + public var keyManager: KeyManager = InMemoryKeyManager(), + public var algorithmId: AlgorithmId = AlgorithmId.Ed25519, ) : CreateDidOptions +public fun interface CreateOption { + public fun apply(options: CreateDidJwkOptions) +} + /** * Provides a specific implementation for creating and resolving "did:jwk" method Decentralized Identifiers (DIDs). * @@ -45,11 +51,12 @@ public class CreateDidJwkOptions( * Further specifics and technical details are outlined in [the DID Jwk Spec](https://example.org/did-method-jwk/). * * @property uri The URI of the "did:jwk" which conforms to the DID standard. - * @property keyManager A [KeyManager] instance utilized to manage the cryptographic keys associated with the DID. + * @property keyManager A [keyManager] instance utilized to manage the cryptographic keys associated with the DID. * * @constructor Initializes a new instance of [DidJwk] with the provided [uri] and [keyManager]. */ -public class DidJwk(uri: String, keyManager: KeyManager) : ChangemeDid(uri, keyManager) { +public class DidJwk(public val uri: String, public val keyManager: KeyManager) { + /** * Resolves the current instance's [uri] to a [DidResolutionResult], which contains the DID Document * and possible related metadata. @@ -62,35 +69,47 @@ public class DidJwk(uri: String, keyManager: KeyManager) : ChangemeDid(uri, keyM return resolve(this.uri, null) } + // todo these are in go impl but not sure what they're used for + public fun keyManager(keyManager: KeyManager): CreateOption = CreateOption { options -> + options.keyManager = keyManager + } + + public fun algorithmId(algorithmId: AlgorithmId): CreateOption = CreateOption { options -> + options.algorithmId = algorithmId + } + public companion object { public const val methodName: String = "jwk" /** * Creates a new "did:jwk" DID, derived from a public key, and stores the associated private key in the - * provided [KeyManager]. + * provided [keyManager]. * * The method-specific identifier of a "did:jwk" DID is a base64url encoded json web key serialized as a UTF-8 * string. * * **Note**: Defaults to ES256K if no options are provided * - * @param keyManager A [KeyManager] instance where the new key will be stored. + * @param keyManager A [keyManager] instance where the new key will be stored. * @param options Optional parameters ([CreateDidJwkOptions]) to specify algorithmId during key creation. * @return A [DidJwk] instance representing the newly created "did:jwk" DID. * * @throws UnsupportedOperationException if the specified curve is not supported. */ - public fun create(keyManager: KeyManager, options: CreateDidJwkOptions?): DidJwk { + public fun create(keyManager: KeyManager, options: CreateDidJwkOptions?): BearerDID { val opts = options ?: CreateDidJwkOptions() val keyAlias = keyManager.generatePrivateKey(opts.algorithmId) - val publicKey = keyManager.getPublicKey(keyAlias) + val publicKeyJwk = keyManager.getPublicKey(keyAlias) + + val base64Encoded = Convert(publicKeyJwk.toJSONString()).toBase64Url(padding = false) - val base64Encoded = Convert(publicKey.toJSONString()).toBase64Url(padding = false) + val didUri = "did:jwk:$base64Encoded" - val did = "did:jwk:$base64Encoded" + val did = Did(method = methodName, uri = didUri, url = didUri, id = base64Encoded) + + return BearerDID(did, keyManager, createDocument(did, publicKeyJwk)) - return DidJwk(did, keyManager) } /** @@ -142,18 +161,26 @@ public class DidJwk(uri: String, keyManager: KeyManager) : ChangemeDid(uri, keyM throw IllegalArgumentException("decoded jwk value cannot be a private key") } - val verificationMethodId = "${parsedDid.uri}#0" + val didDocument = createDocument(parsedDid, publicKeyJwk) + + return DidResolutionResult(didDocument = didDocument, context = "https://w3id.org/did-resolution/v1") + } + + private fun createDocument(did: Did, publicKeyJwk: JWK): DIDDocument { + val verificationMethodId = "${did.uri}#0" val verificationMethod = VerificationMethod.Builder() .id(verificationMethodId) .publicKeyJwk(publicKeyJwk) - .controller(did) - .type("JsonWebKey") + .controller(did.url) + .type("JsonWebKey") // todo go impl says JsonWebKey2020 but opting for the new name .build() val didDocumentBuilder = DIDDocument.Builder() .context(listOf("https://www.w3.org/ns/did/v1")) - .id(did) + .id(did.url) + // todo noticed that this was already in kotlin impl of building did doc + // but it's not in go impl? if (publicKeyJwk.keyUse != KeyUse.ENCRYPTION) { didDocumentBuilder .verificationMethodForPurposes( @@ -170,9 +197,7 @@ public class DidJwk(uri: String, keyManager: KeyManager) : ChangemeDid(uri, keyM if (publicKeyJwk.keyUse != KeyUse.SIGNATURE) { didDocumentBuilder.verificationMethodForPurposes(verificationMethod, listOf(Purpose.KeyAgreement)) } - val didDocument = didDocumentBuilder.build() - - return DidResolutionResult(didDocument = didDocument, context = "https://w3id.org/did-resolution/v1") + return didDocumentBuilder.build() } } From 9c5fbbf801b607cd8a34bc1002a1d520d1ce32bf Mon Sep 17 00:00:00 2001 From: Jiyoon Koo Date: Fri, 8 Mar 2024 19:00:55 -0500 Subject: [PATCH 05/47] removing create options --- .../web5/sdk/dids/methods/jwk/DidJwk.kt | 64 ++++--------------- 1 file changed, 13 insertions(+), 51 deletions(-) diff --git a/dids/src/main/kotlin/web5/sdk/dids/methods/jwk/DidJwk.kt b/dids/src/main/kotlin/web5/sdk/dids/methods/jwk/DidJwk.kt index f2beb4ac1..8fcea182c 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/methods/jwk/DidJwk.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/methods/jwk/DidJwk.kt @@ -20,28 +20,6 @@ import web5.sdk.dids.didcore.VerificationMethod import web5.sdk.dids.exceptions.ParserException import java.text.ParseException -/** - * Specifies options for creating a new "did:jwk" Decentralized Identifier (DID). - * - * @property algorithmId Specifies the algorithmId to be used for key creation. - * Defaults to ES256K (Elliptic Curve Digital Signature Algorithm with SHA-256 and secp256k1 curve). - * @constructor Creates an instance of [CreateDidJwkOptions] with the provided [algorithmId] - * - * ### Usage Example: - * ``` - * val options = CreateDidJwkOptions(algorithm = JWSAlgorithm.ES256K, curve = null) - * val didJwk = DidJwk.create(keyManager, options) - * ``` - */ -public class CreateDidJwkOptions( - public var keyManager: KeyManager = InMemoryKeyManager(), - public var algorithmId: AlgorithmId = AlgorithmId.Ed25519, -) : CreateDidOptions - -public fun interface CreateOption { - public fun apply(options: CreateDidJwkOptions) -} - /** * Provides a specific implementation for creating and resolving "did:jwk" method Decentralized Identifiers (DIDs). * @@ -55,28 +33,8 @@ public fun interface CreateOption { * * @constructor Initializes a new instance of [DidJwk] with the provided [uri] and [keyManager]. */ -public class DidJwk(public val uri: String, public val keyManager: KeyManager) { - - /** - * Resolves the current instance's [uri] to a [DidResolutionResult], which contains the DID Document - * and possible related metadata. - * - * @return A [DidResolutionResult] instance containing the DID Document and related context. - * - * @throws IllegalArgumentException if the provided DID does not conform to the "did:jwk" method. - */ - public fun resolve(): DidResolutionResult { - return resolve(this.uri, null) - } - - // todo these are in go impl but not sure what they're used for - public fun keyManager(keyManager: KeyManager): CreateOption = CreateOption { options -> - options.keyManager = keyManager - } - - public fun algorithmId(algorithmId: AlgorithmId): CreateOption = CreateOption { options -> - options.algorithmId = algorithmId - } +// todo can we make this an object +public class DidJwk { public companion object { public const val methodName: String = "jwk" @@ -91,15 +49,16 @@ public class DidJwk(public val uri: String, public val keyManager: KeyManager) { * **Note**: Defaults to ES256K if no options are provided * * @param keyManager A [keyManager] instance where the new key will be stored. - * @param options Optional parameters ([CreateDidJwkOptions]) to specify algorithmId during key creation. * @return A [DidJwk] instance representing the newly created "did:jwk" DID. * * @throws UnsupportedOperationException if the specified curve is not supported. */ - public fun create(keyManager: KeyManager, options: CreateDidJwkOptions?): BearerDID { - val opts = options ?: CreateDidJwkOptions() + // todo look into whether params can be nullable if providing default values + public fun create( + keyManager: KeyManager = InMemoryKeyManager(), + algorithmId: AlgorithmId = AlgorithmId.Ed25519): BearerDID { - val keyAlias = keyManager.generatePrivateKey(opts.algorithmId) + val keyAlias = keyManager.generatePrivateKey(algorithmId) val publicKeyJwk = keyManager.getPublicKey(keyAlias) val base64Encoded = Convert(publicKeyJwk.toJSONString()).toBase64Url(padding = false) @@ -123,7 +82,7 @@ public class DidJwk(public val uri: String, public val keyManager: KeyManager) { * * @throws IllegalArgumentException if the provided DID does not conform to the "did:jwk" method. */ - public fun resolve(did: String, options: ResolveDidOptions?): DidResolutionResult { + public fun resolve(did: String): DidResolutionResult { val parsedDid = try { Did.parse(did) } catch (_: ParserException) { @@ -172,7 +131,7 @@ public class DidJwk(public val uri: String, public val keyManager: KeyManager) { .id(verificationMethodId) .publicKeyJwk(publicKeyJwk) .controller(did.url) - .type("JsonWebKey") // todo go impl says JsonWebKey2020 but opting for the new name + .type("JsonWebKey2020") // todo go impl says JsonWebKey2020 but opting for the new name .build() val didDocumentBuilder = DIDDocument.Builder() @@ -181,6 +140,8 @@ public class DidJwk(public val uri: String, public val keyManager: KeyManager) { // todo noticed that this was already in kotlin impl of building did doc // but it's not in go impl? + // ask frank. encryption not needed for tbdex use, so not considered in go impl + // keyUse is technically not required (per spec) if (publicKeyJwk.keyUse != KeyUse.ENCRYPTION) { didDocumentBuilder .verificationMethodForPurposes( @@ -200,5 +161,6 @@ public class DidJwk(public val uri: String, public val keyManager: KeyManager) { return didDocumentBuilder.build() } + // todo write import() and call bearerdid.import() } -} +} \ No newline at end of file From f34812732a0cbbe3b85be69d74a68be617d89106 Mon Sep 17 00:00:00 2001 From: Jiyoon Koo Date: Sat, 9 Mar 2024 01:17:36 -0500 Subject: [PATCH 06/47] getting did:dht and did:key to return BearerDID. next step: figure out how to call didxyz.import() from BearerDID. also figure out how to call didDhtApi methods after creating BearerDID via DidDht.create() --- .../kotlin/web5/sdk/dids/did/BearerDID.kt | 28 +++++- .../web5/sdk/dids/methods/dht/DidDht.kt | 90 ++++++++----------- .../web5/sdk/dids/methods/jwk/DidJwk.kt | 9 +- .../web5/sdk/dids/methods/key/DidKey.kt | 2 +- .../web5/sdk/dids/methods/web/DidWeb.kt | 48 ++++------ .../web5/sdk/dids/methods/dht/DidDhtTest.kt | 88 +++++++++--------- 6 files changed, 131 insertions(+), 134 deletions(-) diff --git a/dids/src/main/kotlin/web5/sdk/dids/did/BearerDID.kt b/dids/src/main/kotlin/web5/sdk/dids/did/BearerDID.kt index 9061cbfa0..8c6858269 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/did/BearerDID.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/did/BearerDID.kt @@ -1,7 +1,9 @@ package web5.sdk.dids.did import com.nimbusds.jose.jwk.JWK +import web5.sdk.crypto.InMemoryKeyManager import web5.sdk.crypto.KeyExporter +import web5.sdk.crypto.KeyImporter import web5.sdk.crypto.KeyManager import web5.sdk.dids.didcore.DIDDocument import web5.sdk.dids.didcore.Did @@ -29,7 +31,7 @@ public class BearerDID( return Pair(signer, verificationMethod) } - public fun export() : PortableDID { + public fun export(): PortableDID { val keyExporter = keyManager as? KeyExporter val privateKeys = mutableListOf() @@ -52,5 +54,27 @@ public class BearerDID( ) } - // todo swift doesn't have import() but go and js does. do i write that or nah + public fun import(portableDID: PortableDID): BearerDID { + check(portableDID.document.verificationMethod?.size != 0) { + "PortableDID must contain at least one verification method" + } + + val allVerificationMethodsHavePublicKey = + portableDID.document.verificationMethod + ?.all { vm -> vm.publicKeyJwk != null } + ?: false + + check(allVerificationMethodsHavePublicKey) { + "Each VerificationMethod must contain a public key in JWK format." + } + + val did = Did.parse(portableDID.uri) + + for (key in portableDID.privateKeys) { + val keyImporter = keyManager as? KeyImporter + keyImporter!!.importKey(key) + } + + return BearerDID(did, keyManager, portableDID.document) + } } \ No newline at end of file diff --git a/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt b/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt index 253450f68..5c3d2d0cf 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt @@ -19,10 +19,9 @@ import web5.sdk.crypto.Ed25519 import web5.sdk.crypto.KeyManager import web5.sdk.crypto.Secp256k1 import web5.sdk.dids.CreateDidOptions -import web5.sdk.dids.ChangemeDid import web5.sdk.dids.DidResolutionResult import web5.sdk.dids.ResolutionError -import web5.sdk.dids.ResolveDidOptions +import web5.sdk.dids.did.BearerDID import web5.sdk.dids.didcore.Did import web5.sdk.dids.didcore.DIDDocument import web5.sdk.dids.didcore.DIDDocumentMetadata @@ -35,16 +34,6 @@ import web5.sdk.dids.exceptions.InvalidMethodNameException import web5.sdk.dids.exceptions.PkarrRecordNotFoundException import web5.sdk.dids.exceptions.PublicKeyJwkMissingException -/** - * Configuration for the [DidDhtApi]. - * - * @property gateway The DID DHT gateway URL. - * @property engine The engine to use. When absent, a new one will be created from the [OkHttp] factory. - */ -public class DidDhtConfiguration internal constructor( - public val gateway: String = "https://diddht.tbddev.org", - public var engine: HttpClientEngine = OkHttp.create {}, -) /** * Type indexing types as per https://tbd54566975.github.io/did-dht-method/#type-indexing @@ -67,6 +56,17 @@ public enum class DidDhtTypeIndexing(public val index: Int) { } } +/** + * Configuration for the [DidDhtApi]. + * + * @property gateway The DID DHT gateway URL. + * @property engine The engine to use. When absent, a new one will be created from the [OkHttp] factory. + */ +public class DidDhtConfiguration internal constructor( + public val gateway: String = "https://diddht.tbddev.org", + public var engine: HttpClientEngine = OkHttp.create {}, +) + /** * Returns a [DidDhtApi] after applying [configurationBlock] on the default [DidDhtConfiguration]. */ @@ -96,11 +96,10 @@ public class CreateDidDhtOptions( ) : CreateDidOptions private const val PROPERTY_SEPARATOR = ";" - private const val ARRAY_SEPARATOR = "," - private val logger = KotlinLogging.logger {} + /** * Base class for managing DID DHT operations. Uses the given [DidDhtConfiguration]. */ @@ -125,7 +124,7 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) { * publishing during creation. * @return A [DidDht] instance representing the newly created "did:dht" DID. */ - public fun create(keyManager: KeyManager, options: CreateDidDhtOptions?): DidDht { + public fun create(keyManager: KeyManager, options: CreateDidDhtOptions?): BearerDID { // TODO(gabe): enforce that provided keys are of supported types according to the did:dht spec val opts = options ?: CreateDidDhtOptions() @@ -134,7 +133,7 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) { val publicKey = keyManager.getPublicKey(keyAlias) // build DID Document - val id = DidDht.getDidIdentifier(publicKey) + val didUrl = DidDht.getDidIdentifier(publicKey) // map to the DID object model's services val services = opts.services?.map { service -> @@ -143,7 +142,7 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) { requireNotNull(service.serviceEndpoint) { "Service serviceEndpoint cannot be null" } Service( - id = "$id#${service.id}", + id = "$didUrl#${service.id}", type = service.type, serviceEndpoint = service.serviceEndpoint ) @@ -151,7 +150,7 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) { // build DID Document val didDocumentBuilder = DIDDocument.Builder() - .id(id) + .id(didUrl) .services(services) opts.controllers?.let { didDocumentBuilder.controllers(it.toList()) } @@ -160,9 +159,9 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) { // add identity key to relationships map val identityVerificationMethod = VerificationMethod( - id = "$id#0", + id = "$didUrl#0", type = "JsonWebKey", - controller = id, + controller = didUrl, publicKeyJwk = publicKey.toPublicJWK() ) @@ -178,9 +177,9 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) { opts.verificationMethods?.map { (key, purposes, controller) -> VerificationMethod.Builder() - .id("$id#${key.keyID}") + .id("$didUrl#${key.keyID}") .type("JsonWebKey") - .controller(controller ?: id) + .controller(controller ?: didUrl) .publicKeyJwk(key.toPublicJWK()) .build().also { verificationMethod -> didDocumentBuilder.verificationMethodForPurposes(verificationMethod, purposes.toList()) @@ -188,7 +187,6 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) { } } - val didDocument = didDocumentBuilder.build() // publish to DHT if requested @@ -196,7 +194,9 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) { publish(keyManager, didDocument) } - return DidDht(id, keyManager, didDocument, this) + val id = this.suffix(didUrl) + val did = Did(method = methodName, uri = didUrl, url = didUrl, id = id) + return BearerDID(did, keyManager, didDocument) } /** @@ -211,7 +211,7 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) { * @return A [DidResolutionResult] instance containing the DID Document and related context, including types * as part of the [DIDDocumentMetadata], if available. */ - public fun resolve(did: String, options: ResolveDidOptions?): DidResolutionResult { + public fun resolve(did: String): DidResolutionResult { return try { resolveInternal(did) } catch (e: Exception) { @@ -267,11 +267,11 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) { /** * Returns the suffix of the DID, which is the last part of the DID's method-specific identifier. * - * @param id The DID to get the suffix of. + * @param didUrl The DID to get the suffix of. * @return The suffix of the DID [String]. */ - public fun suffix(id: String): String { - return id.split(":").last() + public fun suffix(didUrl: String): String { + return didUrl.split(":").last() } /** @@ -659,38 +659,22 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) { * @property didDocument The [DIDDocument] associated with the DID, created by the class. */ public class DidDht( - uri: String, keyManager: KeyManager, - public val didDocument: DIDDocument? = null, - private val didDhtApi: DidDhtApi -) : ChangemeDid(uri, keyManager) { - - /** - * Calls [DidDhtApi.create] with the provided [CreateDidDhtOptions] and returns the result. - */ - public fun create(createDidDhtOptions: CreateDidDhtOptions): DidDht { - return didDhtApi.create(keyManager, createDidDhtOptions) - } - - /** - * Calls [DidDhtApi.resolve] with the provided [ResolveDidOptions] and returns the result. - */ - public fun resolve(resolveDidOptions: ResolveDidOptions): DidResolutionResult { - return didDhtApi.resolve(uri, resolveDidOptions) - } + public val uri: String, + public val keyManager: KeyManager, + public val didDocument: DIDDocument? = null +) { /** - * Calls [DidDhtApi.publish] with the provided [keyManager] and [didDocument]. + * Default companion object for creating a [DidDhtApi] with a default configuration. */ - public fun publish() { - didDhtApi.publish(keyManager, didDocument!!) - } + public companion object Default : DidDhtApi(DidDhtConfiguration()) /** * Calls [DidDht.suffix] with the provided [id] and returns the result. */ @JvmOverloads public fun suffix(id: String = this.uri): String { - return DidDht.suffix(id) + return suffix(id) } /** @@ -717,8 +701,4 @@ public class DidDht( return DidDht.fromDnsPacket(did, msg) } - /** - * Default companion object for creating a [DidDhtApi] with a default configuration. - */ - public companion object Default : DidDhtApi(DidDhtConfiguration()) } \ No newline at end of file diff --git a/dids/src/main/kotlin/web5/sdk/dids/methods/jwk/DidJwk.kt b/dids/src/main/kotlin/web5/sdk/dids/methods/jwk/DidJwk.kt index 8fcea182c..a9923645b 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/methods/jwk/DidJwk.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/methods/jwk/DidJwk.kt @@ -13,6 +13,7 @@ import web5.sdk.dids.DidResolutionResult import web5.sdk.dids.ResolutionError import web5.sdk.dids.ResolveDidOptions import web5.sdk.dids.did.BearerDID +import web5.sdk.dids.did.PortableDID import web5.sdk.dids.didcore.Did import web5.sdk.dids.didcore.DIDDocument import web5.sdk.dids.didcore.Purpose @@ -125,13 +126,18 @@ public class DidJwk { return DidResolutionResult(didDocument = didDocument, context = "https://w3id.org/did-resolution/v1") } + // todo being able to call BearerDID.import() requires the function to be a companion object method + // or BearerDID to be an object? +// public fun import(portableDID: PortableDID) : BearerDID { +// +// } private fun createDocument(did: Did, publicKeyJwk: JWK): DIDDocument { val verificationMethodId = "${did.uri}#0" val verificationMethod = VerificationMethod.Builder() .id(verificationMethodId) .publicKeyJwk(publicKeyJwk) .controller(did.url) - .type("JsonWebKey2020") // todo go impl says JsonWebKey2020 but opting for the new name + .type("JsonWebKey2020") .build() val didDocumentBuilder = DIDDocument.Builder() @@ -161,6 +167,5 @@ public class DidJwk { return didDocumentBuilder.build() } - // todo write import() and call bearerdid.import() } } \ No newline at end of file diff --git a/dids/src/main/kotlin/web5/sdk/dids/methods/key/DidKey.kt b/dids/src/main/kotlin/web5/sdk/dids/methods/key/DidKey.kt index 69da57f81..df4567827 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/methods/key/DidKey.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/methods/key/DidKey.kt @@ -75,7 +75,7 @@ public class DidKey(uri: String, keyManager: KeyManager) : ChangemeDid(uri, keyM * * @throws UnsupportedOperationException if the specified curve is not supported. */ - public fun create(keyManager: KeyManager, options: CreateDidKeyOptions?): DidKey { + public fun create(keyManager: KeyManager, options: CreateDidKeyOptions? = null): DidKey { val opts = options ?: CreateDidKeyOptions() val keyAlias = keyManager.generatePrivateKey(opts.algorithmId) diff --git a/dids/src/main/kotlin/web5/sdk/dids/methods/web/DidWeb.kt b/dids/src/main/kotlin/web5/sdk/dids/methods/web/DidWeb.kt index 6232de3ec..31196b913 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/methods/web/DidWeb.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/methods/web/DidWeb.kt @@ -1,6 +1,5 @@ package web5.sdk.dids.methods.web -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import io.github.oshai.kotlinlogging.KotlinLogging import io.ktor.client.engine.HttpClientEngine import io.ktor.client.engine.okhttp.OkHttp @@ -19,15 +18,15 @@ import okhttp3.Cache import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.OkHttpClient import okhttp3.dnsoverhttps.DnsOverHttps +import web5.sdk.common.Json import web5.sdk.crypto.KeyManager import web5.sdk.dids.CreateDidOptions import web5.sdk.dids.didcore.Did import web5.sdk.dids.didcore.DIDDocument -import web5.sdk.dids.ChangemeDid import web5.sdk.dids.DidResolutionResult import web5.sdk.dids.exceptions.ParserException import web5.sdk.dids.ResolutionError -import web5.sdk.dids.ResolveDidOptions +import web5.sdk.dids.did.BearerDID import java.io.File import java.net.InetAddress import java.net.URL @@ -36,29 +35,18 @@ import java.net.UnknownHostException import kotlin.text.Charsets.UTF_8 /** - * Provides a specific implementation for creating and resolving "did:web" method Decentralized Identifiers (DIDs). + * Provides a specific implementation for creating "did:web" method Decentralized Identifiers (DIDs). * * A "did:web" DID is an implementation that uses the web domains existing reputation system. More details can be * read in https://w3c-ccg.github.io/did-method-web/ * - * @property uri The URI of the "did:web" which conforms to the DID standard. - * @property keyManager A [KeyManager] instance utilized to manage the cryptographic keys associated with the DID. - * * ### Usage Example: * ```kotlin * val keyManager = InMemoryKeyManager() - * val did = StatefulWebDid("did:web:tbd.website", keyManager) + * val did = DidWeb.resolve("did:web:tbd.website") * ``` */ -public class DidWeb( - uri: String, - keyManager: KeyManager, - private val didWebApi: DidWebApi -) : ChangemeDid(uri, keyManager) { - /** - * Calls [DidWebApi.resolve] for this DID. - */ - public fun resolve(options: ResolveDidOptions?): DidResolutionResult = didWebApi.resolve(uri, options) +public class DidWeb { /** * Default companion object for creating a [DidWebApi] with a default configuration. @@ -79,8 +67,8 @@ public class DidWebApiConfiguration internal constructor( * Returns a [DidWebApi] instance after applying [blockConfiguration]. */ public fun DidWebApi(blockConfiguration: DidWebApiConfiguration.() -> Unit): DidWebApi { - val conf = DidWebApiConfiguration().apply(blockConfiguration) - return DidWebApiImpl(conf) + val config = DidWebApiConfiguration().apply(blockConfiguration) + return DidWebApiImpl(config) } private class DidWebApiImpl(configuration: DidWebApiConfiguration) : DidWebApi(configuration) @@ -94,10 +82,11 @@ private const val DID_DOC_FILE_NAME = "/did.json" public sealed class DidWebApi( configuration: DidWebApiConfiguration ) { + public val methodName: String = "web" private val logger = KotlinLogging.logger {} - private val mapper = jacksonObjectMapper() + private val mapper = Json.jsonMapper private val engine: HttpClientEngine = configuration.engine ?: OkHttp.create { val appCache = Cache(File("cacheDir", "okhttpcache"), 10 * 1024 * 1024) @@ -118,18 +107,16 @@ public sealed class DidWebApi( } } - public val methodName: String = "web" - - public fun resolve(did: String, options: ResolveDidOptions?): DidResolutionResult { + public fun resolve(did: String): DidResolutionResult { return try { - resolveInternal(did, options) + resolveInternal(did) } catch (e: Exception) { - logger.warn(e) { "resolving DID $did failed" } + logger.warn(e) { "resolving DID $did failed, ${e.message}" } DidResolutionResult.fromResolutionError(ResolutionError.INTERNAL_ERROR) } } - private fun resolveInternal(did: String, options: ResolveDidOptions?): DidResolutionResult { + private fun resolveInternal(did: String): DidResolutionResult { val parsedDid = try { Did.parse(did) } catch (_: ParserException) { @@ -139,7 +126,7 @@ public sealed class DidWebApi( if (parsedDid.method != methodName) { return DidResolutionResult.fromResolutionError(ResolutionError.METHOD_NOT_SUPPORTED) } - val docURL = getDocURL(parsedDid) + val docURL = decodeId(parsedDid) val resp: HttpResponse = try { runBlocking { @@ -147,7 +134,8 @@ public sealed class DidWebApi( contentType(ContentType.Application.Json) } } - } catch (_: UnknownHostException) { + } catch (e: UnknownHostException) { + logger.warn(e) { "failed to make GET request to $did doc URL, ${e.message}" } return DidResolutionResult.fromResolutionError(ResolutionError.NOT_FOUND) } @@ -161,7 +149,7 @@ public sealed class DidWebApi( ) } - private fun getDocURL(parsedDid: Did): String { + private fun decodeId(parsedDid: Did): String { val domainNameWithPath = parsedDid.id.replace(":", "/") val decodedDomain = URLDecoder.decode(domainNameWithPath, UTF_8) @@ -175,7 +163,7 @@ public sealed class DidWebApi( return targetUrl.toString() } - public fun create(keyManager: KeyManager, options: CreateDidOptions?): DidWeb { + public fun create(keyManager: KeyManager, options: CreateDidOptions?): BearerDID { throw UnsupportedOperationException("Create operation is not supported for did:web") } } \ No newline at end of file diff --git a/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DidDhtTest.kt b/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DidDhtTest.kt index 54d0d41a2..90e68b53d 100644 --- a/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DidDhtTest.kt +++ b/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DidDhtTest.kt @@ -101,15 +101,15 @@ class DidDhtTest { assertDoesNotThrow { did.validate() } assertNotNull(did) - assertNotNull(did.didDocument) - assertEquals(1, did.didDocument!!.verificationMethod?.size) - assertContains(did.didDocument!!.verificationMethod?.get(0)?.id!!, "#0") - assertEquals(1, did.didDocument!!.assertionMethod?.size) - assertEquals(1, did.didDocument!!.authentication?.size) - assertEquals(1, did.didDocument!!.capabilityDelegation?.size) - assertEquals(1, did.didDocument!!.capabilityInvocation?.size) - assertNull(did.didDocument!!.keyAgreement) - assertNull(did.didDocument!!.service) + assertNotNull(did.document) + assertEquals(1, did.document.verificationMethod?.size) + assertContains(did.document.verificationMethod?.get(0)?.id!!, "#0") + assertEquals(1, did.document.assertionMethod?.size) + assertEquals(1, did.document.authentication?.size) + assertEquals(1, did.document.capabilityDelegation?.size) + assertEquals(1, did.document.capabilityInvocation?.size) + assertNull(did.document.keyAgreement) + assertNull(did.document.service) } @Test @@ -145,16 +145,16 @@ class DidDhtTest { val did = DidDht.create(manager, opts) assertNotNull(did) - assertNotNull(did.didDocument) - assertEquals(3, did.didDocument!!.verificationMethod?.size) - assertEquals(3, did.didDocument!!.assertionMethod?.size) - assertEquals(3, did.didDocument!!.authentication?.size) - assertEquals(1, did.didDocument!!.capabilityDelegation?.size) - assertEquals(1, did.didDocument!!.capabilityInvocation?.size) - assertNull(did.didDocument!!.keyAgreement) - assertNotNull(did.didDocument!!.service) - assertEquals(1, did.didDocument!!.service?.size) - assertContains(did.didDocument!!.service?.get(0)?.id!!, "test-service") + assertNotNull(did.document) + assertEquals(3, did.document.verificationMethod?.size) + assertEquals(3, did.document.assertionMethod?.size) + assertEquals(3, did.document.authentication?.size) + assertEquals(1, did.document.capabilityDelegation?.size) + assertEquals(1, did.document.capabilityInvocation?.size) + assertNull(did.document.keyAgreement) + assertNotNull(did.document.service) + assertEquals(1, did.document.service?.size) + assertContains(did.document.service?.get(0)?.id!!, "test-service") } @Test @@ -164,17 +164,17 @@ class DidDhtTest { assertDoesNotThrow { did.validate() } assertNotNull(did) - assertNotNull(did.didDocument) + assertNotNull(did.document) val indexes = listOf(DidDhtTypeIndexing.Corporation, DidDhtTypeIndexing.SoftwarePackage) - val packet = did.toDnsPacket(did.didDocument!!, indexes) + val packet = did.toDnsPacket(did.document, indexes) assertNotNull(packet) val docTypesPair = did.fromDnsPacket(msg = packet) assertNotNull(docTypesPair) assertNotNull(docTypesPair.first) assertNotNull(docTypesPair.second) - assertEquals(did.didDocument.toString(), docTypesPair.first.toString()) + assertEquals(did.document.toString(), docTypesPair.first.toString()) assertEquals(indexes, docTypesPair.second) } @@ -185,15 +185,15 @@ class DidDhtTest { val did = api.create(manager, CreateDidDhtOptions(publish = true)) assertNotNull(did) - assertNotNull(did.didDocument) - assertEquals(1, did.didDocument!!.verificationMethod?.size) - assertContains(did.didDocument!!.verificationMethod?.get(0)?.id!!, "#0") - assertEquals(1, did.didDocument!!.assertionMethod?.size) - assertEquals(1, did.didDocument!!.authentication?.size) - assertEquals(1, did.didDocument!!.capabilityDelegation?.size) - assertEquals(1, did.didDocument!!.capabilityInvocation?.size) - assertNull(did.didDocument!!.keyAgreement) - assertNull(did.didDocument!!.service) + assertNotNull(did.document) + assertEquals(1, did.document.verificationMethod?.size) + assertContains(did.document.verificationMethod?.get(0)?.id!!, "#0") + assertEquals(1, did.document.assertionMethod?.size) + assertEquals(1, did.document.authentication?.size) + assertEquals(1, did.document.capabilityDelegation?.size) + assertEquals(1, did.document.capabilityInvocation?.size) + assertNull(did.document.keyAgreement) + assertNull(did.document.service) } @Test @@ -239,16 +239,16 @@ class DidDhtTest { val manager = InMemoryKeyManager() val did = DidDht.create(manager, CreateDidDhtOptions(publish = false)) - require(did.didDocument != null) + require(did.document != null) - val packet = DidDht.toDnsPacket(did.didDocument!!) + val packet = DidDht.toDnsPacket(did.document) assertNotNull(packet) - val didFromPacket = DidDht.fromDnsPacket(did.didDocument!!.id, packet) + val didFromPacket = DidDht.fromDnsPacket(did.document.id, packet) assertNotNull(didFromPacket) assertNotNull(didFromPacket.first) - assertEquals(did.didDocument.toString(), didFromPacket.first.toString()) + assertEquals(did.document.toString(), didFromPacket.first.toString()) } @Test @@ -256,18 +256,18 @@ class DidDhtTest { val manager = InMemoryKeyManager() val did = DidDht.create(manager, CreateDidDhtOptions(publish = false)) - require(did.didDocument != null) + require(did.document != null) val indexes = listOf(DidDhtTypeIndexing.Corporation, DidDhtTypeIndexing.SoftwarePackage) - val packet = DidDht.toDnsPacket(did.didDocument!!, indexes) + val packet = DidDht.toDnsPacket(did.document, indexes) assertNotNull(packet) - val didFromPacket = DidDht.fromDnsPacket(did.didDocument!!.id, packet) + val didFromPacket = DidDht.fromDnsPacket(did.document.id, packet) assertNotNull(didFromPacket) assertNotNull(didFromPacket.first) assertNotNull(didFromPacket.second) - assertEquals(did.didDocument.toString(), didFromPacket.first.toString()) + assertEquals(did.document.toString(), didFromPacket.first.toString()) assertEquals(indexes, didFromPacket.second) } @@ -296,16 +296,16 @@ class DidDhtTest { ) val did = DidDht.create(manager, opts) - require(did.didDocument != null) + require(did.document != null) - val packet = DidDht.toDnsPacket(did.didDocument!!) + val packet = DidDht.toDnsPacket(did.document) assertNotNull(packet) - val didFromPacket = DidDht.fromDnsPacket(did.didDocument!!.id, packet) + val didFromPacket = DidDht.fromDnsPacket(did.document.id, packet) assertNotNull(didFromPacket) assertNotNull(didFromPacket.first) - assertEquals(did.didDocument.toString(), didFromPacket.first.toString()) + assertEquals(did.document.toString(), didFromPacket.first.toString()) } } @@ -387,7 +387,7 @@ class Web5TestVectorsDidDht { val didDht = DidDht.create(keyManager, options) assertEquals( JsonCanonicalizer(Json.stringify(vector.output!!)).encodedString, - JsonCanonicalizer(Json.stringify(didDht.didDocument!!)).encodedString, + JsonCanonicalizer(Json.stringify(didDht.document)).encodedString, vector.description ) } From cb309eef5803e733094a130f1583182aac118616 Mon Sep 17 00:00:00 2001 From: Jiyoon Koo Date: Sat, 9 Mar 2024 21:34:24 -0500 Subject: [PATCH 07/47] moved bearerdid import() to companion object so i can call bearerdid.import() as a static method. not sure if right path, might reverse --- .../kotlin/web5/sdk/dids/did/BearerDID.kt | 45 +++++++++++-------- .../web5/sdk/dids/methods/dht/DidDht.kt | 19 ++++++++ .../web5/sdk/dids/methods/jwk/DidJwk.kt | 19 +++++--- .../web5/sdk/dids/methods/key/DidKey.kt | 31 +++++++++++-- .../web5/sdk/dids/methods/web/DidWeb.kt | 5 +++ .../kotlin/web5/sdk/dids/DidMethodTest.kt | 22 +++++---- .../web5/sdk/dids/methods/dht/DidDhtTest.kt | 40 ++++++++--------- .../web5/sdk/dids/methods/web/DidWebTest.kt | 3 +- 8 files changed, 128 insertions(+), 56 deletions(-) diff --git a/dids/src/main/kotlin/web5/sdk/dids/did/BearerDID.kt b/dids/src/main/kotlin/web5/sdk/dids/did/BearerDID.kt index 8c6858269..639ae6c02 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/did/BearerDID.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/did/BearerDID.kt @@ -54,27 +54,36 @@ public class BearerDID( ) } - public fun import(portableDID: PortableDID): BearerDID { - check(portableDID.document.verificationMethod?.size != 0) { - "PortableDID must contain at least one verification method" - } + public companion object { + + public fun import( + portableDID: PortableDID, + keyManager: KeyManager = InMemoryKeyManager() + ): + BearerDID { + check(portableDID.document.verificationMethod?.size != 0) { + "PortableDID must contain at least one verification method" + } - val allVerificationMethodsHavePublicKey = - portableDID.document.verificationMethod - ?.all { vm -> vm.publicKeyJwk != null } - ?: false + val allVerificationMethodsHavePublicKey = + portableDID.document.verificationMethod + ?.all { vm -> vm.publicKeyJwk != null } + ?: false - check(allVerificationMethodsHavePublicKey) { - "Each VerificationMethod must contain a public key in JWK format." - } + check(allVerificationMethodsHavePublicKey) { + "Each VerificationMethod must contain a public key in JWK format." + } - val did = Did.parse(portableDID.uri) + val did = Did.parse(portableDID.uri) - for (key in portableDID.privateKeys) { - val keyImporter = keyManager as? KeyImporter - keyImporter!!.importKey(key) - } + for (key in portableDID.privateKeys) { + val keyImporter = keyManager as? KeyImporter + keyImporter!!.importKey(key) + } - return BearerDID(did, keyManager, portableDID.document) + return BearerDID(did, keyManager, portableDID.document) + } } -} \ No newline at end of file + +} + diff --git a/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt b/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt index 5c3d2d0cf..0c7131910 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt @@ -16,12 +16,14 @@ import web5.sdk.common.ZBase32 import web5.sdk.crypto.AlgorithmId import web5.sdk.crypto.Crypto import web5.sdk.crypto.Ed25519 +import web5.sdk.crypto.InMemoryKeyManager import web5.sdk.crypto.KeyManager import web5.sdk.crypto.Secp256k1 import web5.sdk.dids.CreateDidOptions import web5.sdk.dids.DidResolutionResult import web5.sdk.dids.ResolutionError import web5.sdk.dids.did.BearerDID +import web5.sdk.dids.did.PortableDID import web5.sdk.dids.didcore.Did import web5.sdk.dids.didcore.DIDDocument import web5.sdk.dids.didcore.DIDDocumentMetadata @@ -220,6 +222,23 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) { } } + public fun import(portableDID: PortableDID, keyManager: KeyManager = InMemoryKeyManager()): BearerDID { + val parsedDid = Did.parse(portableDID.uri) + if (parsedDid.method != methodName) { + throw InvalidMethodNameException("Method not supported") + } + val bearerDid = BearerDID.import(portableDID, keyManager) + + if (bearerDid.document.verificationMethod + ?.none { vm -> + vm.id.split("#").last() == "0" + } == true + ) { + throw IllegalStateException("DidDht DID document must contain at least one verification method with fragment of 0") + } + return bearerDid + } + private fun resolveInternal(did: String): DidResolutionResult { try { validate(did) diff --git a/dids/src/main/kotlin/web5/sdk/dids/methods/jwk/DidJwk.kt b/dids/src/main/kotlin/web5/sdk/dids/methods/jwk/DidJwk.kt index a9923645b..5244ac359 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/methods/jwk/DidJwk.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/methods/jwk/DidJwk.kt @@ -6,6 +6,7 @@ import web5.sdk.common.Convert import web5.sdk.common.EncodingFormat import web5.sdk.crypto.AlgorithmId import web5.sdk.crypto.InMemoryKeyManager +import web5.sdk.crypto.KeyImporter import web5.sdk.crypto.KeyManager import web5.sdk.dids.CreateDidOptions import web5.sdk.dids.DidResolutionMetadata @@ -18,6 +19,7 @@ import web5.sdk.dids.didcore.Did import web5.sdk.dids.didcore.DIDDocument import web5.sdk.dids.didcore.Purpose import web5.sdk.dids.didcore.VerificationMethod +import web5.sdk.dids.exceptions.InvalidMethodNameException import web5.sdk.dids.exceptions.ParserException import java.text.ParseException @@ -71,6 +73,18 @@ public class DidJwk { return BearerDID(did, keyManager, createDocument(did, publicKeyJwk)) } + public fun import(portableDID: PortableDID, keyManager: KeyManager = InMemoryKeyManager()): BearerDID { + val parsedDid = Did.parse(portableDID.uri) + if (parsedDid.method != methodName) { + throw InvalidMethodNameException("Method not supported") + } + val bearerDid = BearerDID.import(portableDID, keyManager) + + if (bearerDid.document.verificationMethod?.size == 0) { + throw IllegalStateException("DidJwk DID document must contain exactly one verification method") + } + return bearerDid + } /** * Resolves a "did:jwk" DID into a [DidResolutionResult], which contains the DID Document and possible related metadata. @@ -126,11 +140,6 @@ public class DidJwk { return DidResolutionResult(didDocument = didDocument, context = "https://w3id.org/did-resolution/v1") } - // todo being able to call BearerDID.import() requires the function to be a companion object method - // or BearerDID to be an object? -// public fun import(portableDID: PortableDID) : BearerDID { -// -// } private fun createDocument(did: Did, publicKeyJwk: JWK): DIDDocument { val verificationMethodId = "${did.uri}#0" val verificationMethod = VerificationMethod.Builder() diff --git a/dids/src/main/kotlin/web5/sdk/dids/methods/key/DidKey.kt b/dids/src/main/kotlin/web5/sdk/dids/methods/key/DidKey.kt index df4567827..d3d06a5d5 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/methods/key/DidKey.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/methods/key/DidKey.kt @@ -1,6 +1,7 @@ package web5.sdk.dids.methods.key import io.ipfs.multibase.Multibase +import org.apache.http.MethodNotSupportedException import web5.sdk.common.Varint import web5.sdk.crypto.AlgorithmId import web5.sdk.crypto.Crypto @@ -10,6 +11,8 @@ import web5.sdk.dids.CreateDidOptions import web5.sdk.dids.ChangemeDid import web5.sdk.dids.DidResolutionResult import web5.sdk.dids.ResolveDidOptions +import web5.sdk.dids.did.BearerDID +import web5.sdk.dids.did.PortableDID import web5.sdk.dids.didcore.Did import web5.sdk.dids.didcore.DIDDocument import web5.sdk.dids.didcore.Purpose @@ -29,7 +32,7 @@ import web5.sdk.dids.didcore.VerificationMethod * ``` */ public class CreateDidKeyOptions( - public val algorithmId: AlgorithmId = AlgorithmId.secp256k1, + public val algorithmId: AlgorithmId = AlgorithmId.Ed25519, ) : CreateDidOptions /** @@ -75,7 +78,7 @@ public class DidKey(uri: String, keyManager: KeyManager) : ChangemeDid(uri, keyM * * @throws UnsupportedOperationException if the specified curve is not supported. */ - public fun create(keyManager: KeyManager, options: CreateDidKeyOptions? = null): DidKey { + public fun create(keyManager: KeyManager, options: CreateDidKeyOptions? = null): BearerDID { val opts = options ?: CreateDidKeyOptions() val keyAlias = keyManager.generatePrivateKey(opts.algorithmId) @@ -93,9 +96,14 @@ public class DidKey(uri: String, keyManager: KeyManager) : ChangemeDid(uri, keyM val idBytes = multiCodecBytes + publicKeyBytes val multibaseEncodedId = Multibase.encode(Multibase.Base.Base58BTC, idBytes) - val did = "did:key:$multibaseEncodedId" + val didUrl = "did:key:$multibaseEncodedId" - return DidKey(did, keyManager) + val did = Did(method = methodName, uri = didUrl, url = didUrl, id = multibaseEncodedId) + val resolutionResult = resolve(didUrl, null) + if (resolutionResult.didDocument == null) { + throw IllegalStateException("DidDocument not found") + } + return BearerDID(did, keyManager, resolutionResult.didDocument) } /** @@ -150,6 +158,21 @@ public class DidKey(uri: String, keyManager: KeyManager) : ChangemeDid(uri, keyM return DidResolutionResult(didDocument = didDocument, context = "https://w3id.org/did-resolution/v1") } + + public fun import(portableDID: PortableDID, keyManager: KeyManager): BearerDID { + val parsedDid = Did.parse(portableDID.uri) + if (parsedDid.method != methodName) { + throw MethodNotSupportedException("Method not supported") + } + + val bearerDid = BearerDID.import(portableDID, keyManager) + + if (bearerDid.document.verificationMethod?.size != 1) { + throw IllegalStateException("DidKey DID document must contain exactly one verification method") + } + + return bearerDid + } } } diff --git a/dids/src/main/kotlin/web5/sdk/dids/methods/web/DidWeb.kt b/dids/src/main/kotlin/web5/sdk/dids/methods/web/DidWeb.kt index 31196b913..fbac2ed56 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/methods/web/DidWeb.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/methods/web/DidWeb.kt @@ -27,6 +27,7 @@ import web5.sdk.dids.DidResolutionResult import web5.sdk.dids.exceptions.ParserException import web5.sdk.dids.ResolutionError import web5.sdk.dids.did.BearerDID +import web5.sdk.dids.did.PortableDID import java.io.File import java.net.InetAddress import java.net.URL @@ -116,6 +117,10 @@ public sealed class DidWebApi( } } + public fun import(portableDID: PortableDID, keyManager: KeyManager): BearerDID { + return BearerDID.import(portableDID, keyManager) + } + private fun resolveInternal(did: String): DidResolutionResult { val parsedDid = try { Did.parse(did) diff --git a/dids/src/test/kotlin/web5/sdk/dids/DidMethodTest.kt b/dids/src/test/kotlin/web5/sdk/dids/DidMethodTest.kt index 09718cb04..d05e419f0 100644 --- a/dids/src/test/kotlin/web5/sdk/dids/DidMethodTest.kt +++ b/dids/src/test/kotlin/web5/sdk/dids/DidMethodTest.kt @@ -20,29 +20,35 @@ class DidMethodTest { @Test fun `findAssertionMethodById works with default`() { val manager = InMemoryKeyManager() - val did = DidKey.create(manager) + val bearerDid = DidKey.create(manager) - val verificationMethod = did.resolve().didDocument!!.findAssertionMethodById() - assertEquals("${did.uri}#${Did.parse(did.uri).id}", verificationMethod.id) + val verificationMethod = DidKey.resolve(bearerDid.did.uri, null) + .didDocument!! + .findAssertionMethodById() + assertEquals("${bearerDid.did.uri}#${Did.parse(bearerDid.did.uri).id}", verificationMethod.id) } @Test fun `findAssertionMethodById finds with id`() { val manager = InMemoryKeyManager() - val did = DidKey.create(manager) + val bearerDid = DidKey.create(manager) - val assertionMethodId = "${did.uri}#${Did.parse(did.uri).id}" - val verificationMethod = did.resolve().didDocument!!.findAssertionMethodById(assertionMethodId) + val assertionMethodId = "${bearerDid.did.uri}#${Did.parse(bearerDid.did.uri).id}" + val verificationMethod = DidKey.resolve(bearerDid.did.uri, null) + .didDocument!! + .findAssertionMethodById(assertionMethodId) assertEquals(assertionMethodId, verificationMethod.id) } @Test fun `findAssertionMethodById throws exception`() { val manager = InMemoryKeyManager() - val did = DidKey.create(manager) + val bearerDid = DidKey.create(manager) val exception = assertThrows { - did.resolve().didDocument!!.findAssertionMethodById("made up assertion method id") + DidKey.resolve(bearerDid.did.uri, null) + .didDocument!! + .findAssertionMethodById("made up assertion method id") } assertContains(exception.message!!, "assertion method \"made up assertion method id\" not found") } diff --git a/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DidDhtTest.kt b/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DidDhtTest.kt index 90e68b53d..a3828d878 100644 --- a/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DidDhtTest.kt +++ b/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DidDhtTest.kt @@ -97,19 +97,19 @@ class DidDhtTest { @Test fun `create with no options`() { val manager = InMemoryKeyManager() - val did = DidDht.create(manager, CreateDidDhtOptions(publish = false)) - - assertDoesNotThrow { did.validate() } - assertNotNull(did) - assertNotNull(did.document) - assertEquals(1, did.document.verificationMethod?.size) - assertContains(did.document.verificationMethod?.get(0)?.id!!, "#0") - assertEquals(1, did.document.assertionMethod?.size) - assertEquals(1, did.document.authentication?.size) - assertEquals(1, did.document.capabilityDelegation?.size) - assertEquals(1, did.document.capabilityInvocation?.size) - assertNull(did.document.keyAgreement) - assertNull(did.document.service) + val bearerDid = DidDht.create(manager, CreateDidDhtOptions(publish = false)) + + assertDoesNotThrow { DidDht.validate(bearerDid.did.url) } + assertNotNull(bearerDid) + assertNotNull(bearerDid.document) + assertEquals(1, bearerDid.document.verificationMethod?.size) + assertContains(bearerDid.document.verificationMethod?.get(0)?.id!!, "#0") + assertEquals(1, bearerDid.document.assertionMethod?.size) + assertEquals(1, bearerDid.document.authentication?.size) + assertEquals(1, bearerDid.document.capabilityDelegation?.size) + assertEquals(1, bearerDid.document.capabilityInvocation?.size) + assertNull(bearerDid.document.keyAgreement) + assertNull(bearerDid.document.service) } @Test @@ -160,21 +160,21 @@ class DidDhtTest { @Test fun `create and transform to packet with types`() { val manager = InMemoryKeyManager() - val did = DidDht.create(manager, CreateDidDhtOptions(publish = false)) + val bearerDid = DidDht.create(manager, CreateDidDhtOptions(publish = false)) - assertDoesNotThrow { did.validate() } - assertNotNull(did) - assertNotNull(did.document) + assertDoesNotThrow { DidDht.validate(bearerDid.did.url) } + assertNotNull(bearerDid) + assertNotNull(bearerDid.document) val indexes = listOf(DidDhtTypeIndexing.Corporation, DidDhtTypeIndexing.SoftwarePackage) - val packet = did.toDnsPacket(did.document, indexes) + val packet = DidDht.toDnsPacket(bearerDid.document, indexes) assertNotNull(packet) - val docTypesPair = did.fromDnsPacket(msg = packet) + val docTypesPair = DidDht.fromDnsPacket(bearerDid.did.url, packet) assertNotNull(docTypesPair) assertNotNull(docTypesPair.first) assertNotNull(docTypesPair.second) - assertEquals(did.document.toString(), docTypesPair.first.toString()) + assertEquals(bearerDid.document.toString(), docTypesPair.first.toString()) assertEquals(indexes, docTypesPair.second) } diff --git a/dids/src/test/kotlin/web5/sdk/dids/methods/web/DidWebTest.kt b/dids/src/test/kotlin/web5/sdk/dids/methods/web/DidWebTest.kt index 7c513187f..26c13d30a 100644 --- a/dids/src/test/kotlin/web5/sdk/dids/methods/web/DidWebTest.kt +++ b/dids/src/test/kotlin/web5/sdk/dids/methods/web/DidWebTest.kt @@ -13,6 +13,7 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import web5.sdk.crypto.InMemoryKeyManager import web5.sdk.dids.DidResolutionResult +import web5.sdk.dids.did.PortableDID import web5.sdk.dids.methods.util.readKey import web5.sdk.testing.TestVectors import java.io.File @@ -97,7 +98,7 @@ class DidWebTest { val exception = assertThrows { DidWebApi { engine = mockEngine() - }.create(InMemoryKeyManager()) + }.create(InMemoryKeyManager(), null) } assertEquals("Create operation is not supported for did:web", exception.message) } From 1f32e5ab76b7533225ee7fc21259a3d90a1257e9 Mon Sep 17 00:00:00 2001 From: Jiyoon Koo Date: Sun, 10 Mar 2024 13:30:43 -0400 Subject: [PATCH 08/47] updated codeowners doc --- CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index 9cd62f39c..72bdf13b9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -9,7 +9,7 @@ # The format is described: https://github.blog/2017-07-06-introducing-code-owners/ # These owners will be the default owners for everything in the repo. -* @amika-sq @mistermoe @nitro-neal @tomdaffurn @phoebe-lew @diehuxx @kirahsapong @jiyoontbd @frankhinek +* @mistermoe @nitro-neal @tomdaffurn @phoebe-lew @diehuxx @kirahsapong @jiyoontbd @frankhinek /crypto/src/main/kotlin/web5/sdk/crypto/AwsKeyManager.kt @tomdaffurn # ----------------------------------------------- From a3ac9b5b6117528471eba311f542f722d7b0bc47 Mon Sep 17 00:00:00 2001 From: Jiyoon Koo Date: Mon, 11 Mar 2024 01:24:06 -0400 Subject: [PATCH 09/47] changing type names to be true CamelCase. implemented Jws methods and related types --- .../credentials/VerifiableCredentialTest.kt | 27 ++-- .../web5/sdk/dids/DidResolutionResult.kt | 16 +- .../dids/did/{BearerDID.kt => BearerDid.kt} | 23 ++- .../did/{PortableDID.kt => PortableDid.kt} | 6 +- .../{DIDDocument.kt => DidDocument.kt} | 34 ++-- ...mentMetadata.kt => DidDocumentMetadata.kt} | 2 +- dids/src/main/kotlin/web5/sdk/dids/jws/Jws.kt | 148 ++++++++++++++++++ .../web5/sdk/dids/methods/dht/DidDht.kt | 84 +++++----- ...tMetadata.kt => DidDhtDocumentMetadata.kt} | 6 +- .../web5/sdk/dids/methods/jwk/DidJwk.kt | 21 ++- .../web5/sdk/dids/methods/key/DidKey.kt | 16 +- .../web5/sdk/dids/methods/web/DidWeb.kt | 14 +- ...{DIDDocumentTest.kt => DidDocumentTest.kt} | 36 ++--- .../web5/sdk/dids/methods/dht/DidDhtTest.kt | 4 +- .../web5/sdk/dids/methods/web/DidWebTest.kt | 1 - 15 files changed, 295 insertions(+), 143 deletions(-) rename dids/src/main/kotlin/web5/sdk/dids/did/{BearerDID.kt => BearerDid.kt} (81%) rename dids/src/main/kotlin/web5/sdk/dids/did/{PortableDID.kt => PortableDid.kt} (61%) rename dids/src/main/kotlin/web5/sdk/dids/didcore/{DIDDocument.kt => DidDocument.kt} (94%) rename dids/src/main/kotlin/web5/sdk/dids/didcore/{DIDDocumentMetadata.kt => DidDocumentMetadata.kt} (96%) create mode 100644 dids/src/main/kotlin/web5/sdk/dids/jws/Jws.kt rename dids/src/main/kotlin/web5/sdk/dids/methods/dht/{DIDDhtDocumentMetadata.kt => DidDhtDocumentMetadata.kt} (69%) rename dids/src/test/kotlin/web5/sdk/dids/didcore/{DIDDocumentTest.kt => DidDocumentTest.kt} (91%) diff --git a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt index 8fb8d71f4..f1e76cb7a 100644 --- a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt +++ b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt @@ -53,7 +53,7 @@ class VerifiableCredentialTest { val vc = VerifiableCredential.create( type = "StreetCred", issuer = issuerDid.uri, - subject = holderDid.uri, + subject = holderDid.did.uri, data = StreetCredibility(localRespect = "high", legit = true) ) @@ -72,8 +72,8 @@ class VerifiableCredentialTest { val vc = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.uri, - subject = holderDid.uri, + issuer = issuerDid.did.uri, + subject = holderDid.did.uri, data = StreetCredibility(localRespect = "high", legit = true) ) assertNotNull(vc) @@ -88,8 +88,8 @@ class VerifiableCredentialTest { val exception = assertThrows(IllegalArgumentException::class.java) { VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.uri, - subject = holderDid.uri, + issuer = issuerDid.did.uri, + subject = holderDid.did.uri, data = "trials & tribulations" ) } @@ -106,8 +106,8 @@ class VerifiableCredentialTest { val vc = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.uri, - subject = holderDid.uri, + issuer = issuerDid.did.uri, + subject = holderDid.did.uri, data = StreetCredibility(localRespect = "high", legit = true) ) @@ -134,7 +134,7 @@ class VerifiableCredentialTest { ) val header = JWSHeader.Builder(JWSAlgorithm.ES256K) - .keyID(issuerDid.uri) + .keyID(issuerDid.did.uri) .build() // A detached payload JWT val vcJwt = "${header.toBase64URL()}..fakeSig" @@ -211,8 +211,8 @@ class VerifiableCredentialTest { val vc = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.uri, - subject = holderDid.uri, + issuer = issuerDid.did.uri, + subject = holderDid.did.uri, data = StreetCredibility(localRespect = "high", legit = true) ) @@ -252,8 +252,11 @@ class Web5TestVectorsCredentials { val keyManager = InMemoryKeyManager() keyManager.import(listOf(vector.input.signerPrivateJwk!!)) - val issuerChangemeDid = ChangemeDid.load(vector.input.signerDidUri!!, keyManager) - val vcJwt = vc.sign(issuerChangemeDid) + // todo need to update test vectors + // input should have portable did and credential + // want to be able to call BearerDID.import() + val issuerDid = ChangemeDid.load(vector.input.signerDidUri!!, keyManager) + val vcJwt = vc.sign(issuerDid) assertEquals(vector.output, vcJwt, vector.description) } diff --git a/dids/src/main/kotlin/web5/sdk/dids/DidResolutionResult.kt b/dids/src/main/kotlin/web5/sdk/dids/DidResolutionResult.kt index 6b7a9e53b..aa0119ef6 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/DidResolutionResult.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/DidResolutionResult.kt @@ -4,8 +4,8 @@ import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.KotlinModule -import web5.sdk.dids.didcore.DIDDocument -import web5.sdk.dids.didcore.DIDDocumentMetadata +import web5.sdk.dids.didcore.DidDocument +import web5.sdk.dids.didcore.DidDocumentMetadata import java.util.Objects.hash /** @@ -20,8 +20,8 @@ import java.util.Objects.hash public class DidResolutionResult( @JsonProperty("@context") public val context: String? = null, - public val didDocument: DIDDocument? = null, - public val didDocumentMetadata: DIDDocumentMetadata = DIDDocumentMetadata(), + public val didDocument: DidDocument? = null, + public val didDocumentMetadata: DidDocumentMetadata = DidDocumentMetadata(), public val didResolutionMetadata: DidResolutionMetadata = DidResolutionMetadata(), ) { override fun toString(): String { @@ -35,7 +35,13 @@ public class DidResolutionResult( return false } - override fun hashCode(): Int = hash(context, didDocument, didDocumentMetadata, didResolutionMetadata) + override fun hashCode(): Int = + hash( + context, + didDocument, + didDocumentMetadata, + didResolutionMetadata + ) public companion object { private val objectMapper: ObjectMapper = ObjectMapper().apply { diff --git a/dids/src/main/kotlin/web5/sdk/dids/did/BearerDID.kt b/dids/src/main/kotlin/web5/sdk/dids/did/BearerDid.kt similarity index 81% rename from dids/src/main/kotlin/web5/sdk/dids/did/BearerDID.kt rename to dids/src/main/kotlin/web5/sdk/dids/did/BearerDid.kt index 639ae6c02..50c1e4150 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/did/BearerDID.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/did/BearerDid.kt @@ -5,33 +5,33 @@ import web5.sdk.crypto.InMemoryKeyManager import web5.sdk.crypto.KeyExporter import web5.sdk.crypto.KeyImporter import web5.sdk.crypto.KeyManager -import web5.sdk.dids.didcore.DIDDocument +import web5.sdk.dids.didcore.DidDocument import web5.sdk.dids.didcore.Did import web5.sdk.dids.didcore.VMSelector import web5.sdk.dids.didcore.VerificationMethod -public typealias DIDSigner = (payload: ByteArray) -> ByteArray +public typealias DidSigner = (payload: ByteArray) -> ByteArray -public class BearerDID( +public class BearerDid( public val did: Did, public val keyManager: KeyManager, - public val document: DIDDocument + public val document: DidDocument ) { - public fun getSigner(selector: VMSelector): Pair { + public fun getSigner(selector: VMSelector? = null): Pair { val verificationMethod = document.selectVerificationMethod(selector) val keyAliasResult = runCatching { verificationMethod.publicKeyJwk?.computeThumbprint() } val keyAlias = keyAliasResult.getOrNull() ?: throw Exception("Failed to compute key alias") - val signer: DIDSigner = { payload -> + val signer: DidSigner = { payload -> keyManager.sign(keyAlias.toString(), payload) } return Pair(signer, verificationMethod) } - public fun export(): PortableDID { + public fun export(): PortableDid { val keyExporter = keyManager as? KeyExporter val privateKeys = mutableListOf() @@ -46,7 +46,7 @@ public class BearerDID( } } - return PortableDID( + return PortableDid( uri = this.did.uri, document = this.document, privateKeys = privateKeys, @@ -57,10 +57,9 @@ public class BearerDID( public companion object { public fun import( - portableDID: PortableDID, + portableDID: PortableDid, keyManager: KeyManager = InMemoryKeyManager() - ): - BearerDID { + ): BearerDid { check(portableDID.document.verificationMethod?.size != 0) { "PortableDID must contain at least one verification method" } @@ -81,7 +80,7 @@ public class BearerDID( keyImporter!!.importKey(key) } - return BearerDID(did, keyManager, portableDID.document) + return BearerDid(did, keyManager, portableDID.document) } } diff --git a/dids/src/main/kotlin/web5/sdk/dids/did/PortableDID.kt b/dids/src/main/kotlin/web5/sdk/dids/did/PortableDid.kt similarity index 61% rename from dids/src/main/kotlin/web5/sdk/dids/did/PortableDID.kt rename to dids/src/main/kotlin/web5/sdk/dids/did/PortableDid.kt index 73313eefb..a1964f7b7 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/did/PortableDID.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/did/PortableDid.kt @@ -1,11 +1,11 @@ package web5.sdk.dids.did import com.nimbusds.jose.jwk.JWK -import web5.sdk.dids.didcore.DIDDocument +import web5.sdk.dids.didcore.DidDocument -public class PortableDID( +public class PortableDid( public val uri: String, public val privateKeys: List, - public val document: DIDDocument, + public val document: DidDocument, public val metadata: Map ) \ No newline at end of file diff --git a/dids/src/main/kotlin/web5/sdk/dids/didcore/DIDDocument.kt b/dids/src/main/kotlin/web5/sdk/dids/didcore/DidDocument.kt similarity index 94% rename from dids/src/main/kotlin/web5/sdk/dids/didcore/DIDDocument.kt rename to dids/src/main/kotlin/web5/sdk/dids/didcore/DidDocument.kt index 6c1f5c2cf..e3d6cfd6c 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/didcore/DIDDocument.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/didcore/DidDocument.kt @@ -4,7 +4,7 @@ import com.fasterxml.jackson.annotation.JsonProperty import java.security.SignatureException /** - * DIDDocument represents a set of data describing the DID subject including mechanisms such as: + * DidDocument represents a set of data describing the DID subject including mechanisms such as: * - cryptographic public keys - used to authenticate itself and prove association with the DID * - services - means of communicating or interacting with the DID subject or * associated entities via one or more service endpoints. @@ -36,7 +36,7 @@ import java.security.SignatureException * @property capabilityInvocation specifies a verification method used by the DID subject to invoke a * cryptographic capability, such as the authorization to update the DID Document. */ -public class DIDDocument( +public class DidDocument( public val id: String, @JsonProperty("@context") public val context: List? = null, @@ -102,7 +102,7 @@ public class DIDDocument( } /** - * Finds the first available assertion method from the [DIDDocument]. When [assertionMethodId] + * Finds the first available assertion method from the [DidDocument]. When [assertionMethodId] * is null, the function will return the first available assertion method. * * @param assertionMethodId The ID of the assertion method to be found @@ -131,7 +131,7 @@ public class DIDDocument( } /** - * Builder object to build a DIDDocument. + * Builder object to build a DidDocument. */ public class Builder { @@ -150,17 +150,17 @@ public class DIDDocument( private var capabilityInvocationMethod: MutableList? = null /** - * Adds Id to the DIDDocument. + * Adds Id to the DidDocument. * - * @param id of the DIDDocument + * @param id of the DidDocument * @return Builder object */ public fun id(id: String): Builder = apply { this.id = id } /** - * Sets Context to the DIDDocument. + * Sets Context to the DidDocument. * - * @param context of the DIDDocument + * @param context of the DidDocument * @return Builder object */ public fun context(context: List): Builder = apply { @@ -170,7 +170,7 @@ public class DIDDocument( /** * Sets Controllers. * - * @param controllers to be set on the DIDDocument + * @param controllers to be set on the DidDocument * @return Builder object */ public fun controllers(controllers: List): Builder = apply { this.controller = controllers } @@ -178,7 +178,7 @@ public class DIDDocument( /** * Sets AlsoknownAses. * - * @param alsoKnownAses to be set on the DIDDocument + * @param alsoKnownAses to be set on the DidDocument * @return Builder object */ public fun alsoKnownAses(alsoKnownAses: List): Builder = apply { this.alsoKnownAs = alsoKnownAses } @@ -186,7 +186,7 @@ public class DIDDocument( /** * Sets Services. * - * @param services to be set on the DIDDocument + * @param services to be set on the DidDocument * @return Builder object */ public fun services(services: List?): Builder = apply { this.service = services } @@ -227,7 +227,7 @@ public class DIDDocument( /** * Adds VerificationMethods for a single purpose. * - * @param methodIds a list of VerificationMethodIds to be added to the DIDDocument + * @param methodIds a list of VerificationMethodIds to be added to the DidDocument * @param purpose a single purpose to be associated with the list of VerificationMethods * @return Builder object */ @@ -256,13 +256,13 @@ public class DIDDocument( } /** - * Builds DIDDocument after validating the required fields. + * Builds DidDocument after validating the required fields. * - * @return DIDDocument + * @return DidDocument */ - public fun build(): DIDDocument { + public fun build(): DidDocument { check(id != null) { "ID is required" } - return DIDDocument( + return DidDocument( id!!, context, alsoKnownAs, @@ -279,7 +279,7 @@ public class DIDDocument( } override fun toString(): String { - return "DIDDocument(" + + return "DidDocument(" + "id='$id', " + "context='$context', " + "alsoKnownAs=$alsoKnownAs, " + diff --git a/dids/src/main/kotlin/web5/sdk/dids/didcore/DIDDocumentMetadata.kt b/dids/src/main/kotlin/web5/sdk/dids/didcore/DidDocumentMetadata.kt similarity index 96% rename from dids/src/main/kotlin/web5/sdk/dids/didcore/DIDDocumentMetadata.kt rename to dids/src/main/kotlin/web5/sdk/dids/didcore/DidDocumentMetadata.kt index 301812f26..c2358c819 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/didcore/DIDDocumentMetadata.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/didcore/DidDocumentMetadata.kt @@ -12,7 +12,7 @@ package web5.sdk.dids.didcore * @property equivalentId Alternative ID that can be used interchangeably with the canonical DID. * @property canonicalId The canonical ID of the DID as per method-specific rules. */ -public open class DIDDocumentMetadata( +public open class DidDocumentMetadata( public var created: String? = null, public var updated: String? = null, public var deactivated: Boolean? = null, diff --git a/dids/src/main/kotlin/web5/sdk/dids/jws/Jws.kt b/dids/src/main/kotlin/web5/sdk/dids/jws/Jws.kt new file mode 100644 index 000000000..d83d74524 --- /dev/null +++ b/dids/src/main/kotlin/web5/sdk/dids/jws/Jws.kt @@ -0,0 +1,148 @@ +package web5.sdk.dids.jws + +import web5.sdk.common.Convert +import web5.sdk.common.EncodingFormat +import web5.sdk.common.Json +import web5.sdk.crypto.Crypto +import web5.sdk.dids.DidResolvers +import web5.sdk.dids.did.BearerDid + +public object Jws { + + public fun decode(jws: String): DecodedJws { + val parts = jws.split(".") + check(parts.size != 3) { + "Malformed JWT. Expected 3 parts, got ${parts.size}" + } + + val header: JwsHeader + try { + header = JwsHeader.fromBase64Url(parts[0]) + } catch (e: Exception) { + throw IllegalArgumentException("Malformed JWT. Failed to decode header: ${e.message}") + } + + val payload: ByteArray + try { + payload = Convert(parts[1]).toByteArray() + } catch (e: Exception) { + throw IllegalArgumentException("Malformed JWT. Failed to decode payload: ${e.message}") + } + + val signature: ByteArray + try { + signature = Convert(parts[2]).toByteArray() + } catch (e: Exception) { + throw IllegalArgumentException("Malformed JWT. Failed to decode signature: ${e.message}") + } + + return DecodedJws(header, payload, signature, parts) + } + + public fun sign( + bearerDid: BearerDid, + payload: ByteArray, + header: JwsHeader?, + detached: Boolean = false + ): String { + val (signer, verificationMethod) = bearerDid.getSigner() + + val kid = if (verificationMethod.id.startsWith("#")) { + "${bearerDid.did.uri}${verificationMethod.id}" + } else { + verificationMethod.id + } + + val publicKeyJwk = verificationMethod.publicKeyJwk!! + val jwsHeader = header ?: JwsHeader() + jwsHeader.kid = kid + // todo pretty sure i need algorithm names like ES256K for secp and EdDSA for ed25519 + jwsHeader.alg = Crypto.getJwkCurve(publicKeyJwk)?.name + // todo do we need jwsHeader.typ = "??" + // todo with padding false? + // todo should padding = false by default? + val headerBase64Url = Convert(jwsHeader).toBase64Url(padding = false) + val payloadBase64Url = Convert(payload).toBase64Url(padding = false) + + val toSign = "$headerBase64Url.$payloadBase64Url" + val toSignBytes = Convert(toSign).toByteArray() + + val signatureBytes = signer.invoke(toSignBytes) + val signatureBase64Url = Convert(signatureBytes).toBase64Url(padding = false) + + if (detached) { + return "$headerBase64Url..$signatureBase64Url" + } else { + return "$headerBase64Url.$payloadBase64Url.$signatureBase64Url" + } + + } + + public fun verify(jws: String): DecodedJws { + val decodedJws = decode(jws) + decodedJws.verify() + return decodedJws + } +} + +public class JwsHeader( + public var typ: String? = null, + public var alg: String? = null, + public var kid: String? = null +) { + public companion object { + public fun toJson(header: JwsHeader): String { + return Json.jsonMapper.writeValueAsString(header) + } + + public fun fromJson(jsonHeader: String): JwsHeader? { + return Json.jsonMapper.readValue(jsonHeader, JwsHeader::class.java) + } + + public fun fromBase64Url(base64EncodedHeader: String): JwsHeader { + val jsonHeaderDecoded = Convert(base64EncodedHeader).toByteArray() + return Json.jsonMapper.readValue(jsonHeaderDecoded, JwsHeader::class.java) + } + + public fun toBase64Url(header: JwsHeader): String { + val jsonHeader = toJson(header) + return Convert(jsonHeader, EncodingFormat.Base64Url).toBase64Url(padding = false) + } + } +} + +public class DecodedJws( + public val header: JwsHeader, + public val payload: ByteArray, + public val signature: ByteArray, + public val parts: List +) { + public fun verify() { + check(header.kid != null || header.alg != null) { + "Malformed JWS. Expected header to contain kid and alg." + } + + val dereferenceResult = DidResolvers.resolve(header.kid!!) + + check(dereferenceResult.didResolutionMetadata.error != null) { + "Verification failed. Failed to resolve kid. " + + "Error: ${dereferenceResult.didResolutionMetadata.error}" + } + + check(dereferenceResult.didDocument!!.verificationMethod?.size != 0) { + "Verification failed. Expected header kid to dereference a verification method" + } + + val verificationMethod = dereferenceResult.didDocument.verificationMethod!!.first() + check(verificationMethod.publicKeyJwk != null) { + "Verification failed. Expected headeder kid to dereference" + + " a verification method with a publicKeyJwk" + } + + val toSign = "${parts[0]}.${parts[1]}" + val toSignBytes = Convert(toSign).toByteArray() + + Crypto.verify(verificationMethod.publicKeyJwk, toSignBytes, signature) + + } +} \ No newline at end of file diff --git a/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt b/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt index 0c7131910..07c8fe009 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt @@ -22,11 +22,11 @@ import web5.sdk.crypto.Secp256k1 import web5.sdk.dids.CreateDidOptions import web5.sdk.dids.DidResolutionResult import web5.sdk.dids.ResolutionError -import web5.sdk.dids.did.BearerDID -import web5.sdk.dids.did.PortableDID +import web5.sdk.dids.did.BearerDid +import web5.sdk.dids.did.PortableDid import web5.sdk.dids.didcore.Did -import web5.sdk.dids.didcore.DIDDocument -import web5.sdk.dids.didcore.DIDDocumentMetadata +import web5.sdk.dids.didcore.DidDocument +import web5.sdk.dids.didcore.DidDocumentMetadata import web5.sdk.dids.didcore.Purpose import web5.sdk.dids.didcore.Service import web5.sdk.dids.didcore.VerificationMethod @@ -126,7 +126,7 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) { * publishing during creation. * @return A [DidDht] instance representing the newly created "did:dht" DID. */ - public fun create(keyManager: KeyManager, options: CreateDidDhtOptions?): BearerDID { + public fun create(keyManager: KeyManager, options: CreateDidDhtOptions?): BearerDid { // TODO(gabe): enforce that provided keys are of supported types according to the did:dht spec val opts = options ?: CreateDidDhtOptions() @@ -135,7 +135,7 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) { val publicKey = keyManager.getPublicKey(keyAlias) // build DID Document - val didUrl = DidDht.getDidIdentifier(publicKey) + val didUri = DidDht.getDidIdentifier(publicKey) // map to the DID object model's services val services = opts.services?.map { service -> @@ -144,15 +144,15 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) { requireNotNull(service.serviceEndpoint) { "Service serviceEndpoint cannot be null" } Service( - id = "$didUrl#${service.id}", + id = "$didUri#${service.id}", type = service.type, serviceEndpoint = service.serviceEndpoint ) } // build DID Document - val didDocumentBuilder = DIDDocument.Builder() - .id(didUrl) + val didDocumentBuilder = DidDocument.Builder() + .id(didUri) .services(services) opts.controllers?.let { didDocumentBuilder.controllers(it.toList()) } @@ -161,9 +161,9 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) { // add identity key to relationships map val identityVerificationMethod = VerificationMethod( - id = "$didUrl#0", + id = "$didUri#0", type = "JsonWebKey", - controller = didUrl, + controller = didUri, publicKeyJwk = publicKey.toPublicJWK() ) @@ -179,9 +179,9 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) { opts.verificationMethods?.map { (key, purposes, controller) -> VerificationMethod.Builder() - .id("$didUrl#${key.keyID}") + .id("$didUri#${key.keyID}") .type("JsonWebKey") - .controller(controller ?: didUrl) + .controller(controller ?: didUri) .publicKeyJwk(key.toPublicJWK()) .build().also { verificationMethod -> didDocumentBuilder.verificationMethodForPurposes(verificationMethod, purposes.toList()) @@ -196,9 +196,9 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) { publish(keyManager, didDocument) } - val id = this.suffix(didUrl) - val did = Did(method = methodName, uri = didUrl, url = didUrl, id = id) - return BearerDID(did, keyManager, didDocument) + val id = this.suffix(didUri) + val did = Did(method = methodName, uri = didUri, url = didUri, id = id) + return BearerDid(did, keyManager, didDocument) } /** @@ -211,7 +211,7 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) { * * @param did The "did:dht" DID that needs to be resolved. * @return A [DidResolutionResult] instance containing the DID Document and related context, including types - * as part of the [DIDDocumentMetadata], if available. + * as part of the [DidDocumentMetadata], if available. */ public fun resolve(did: String): DidResolutionResult { return try { @@ -222,12 +222,12 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) { } } - public fun import(portableDID: PortableDID, keyManager: KeyManager = InMemoryKeyManager()): BearerDID { + public fun import(portableDID: PortableDid, keyManager: KeyManager = InMemoryKeyManager()): BearerDid { val parsedDid = Did.parse(portableDID.uri) if (parsedDid.method != methodName) { throw InvalidMethodNameException("Method not supported") } - val bearerDid = BearerDID.import(portableDID, keyManager) + val bearerDid = BearerDid.import(portableDID, keyManager) if (bearerDid.document.verificationMethod ?.none { vm -> @@ -259,22 +259,22 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) { fromDnsPacket(did, dnsPacket).let { (didDocument, types) -> return DidResolutionResult( didDocument = didDocument, - didDocumentMetadata = DIDDhtDocumentMetadata(types = types.map { it.index }) + didDocumentMetadata = DidDhtDocumentMetadata(types = types.map { it.index }) ) } } /** - * Publishes a [DIDDocument] to the DHT. + * Publishes a [DidDocument] to the DHT. * * @param manager The [KeyManager] instance to use for signing the message. - * @param didDocument The [DIDDocument] to publish. + * @param didDocument The [DidDocument] to publish. * @param types A list of types to include in the packet. * @throws IllegalArgumentException if the provided DID does not conform to the "did:dht" method. * @throws Exception if the message is not successfully put to the DHT. */ @JvmOverloads - public fun publish(manager: KeyManager, didDocument: DIDDocument, types: List? = null) { + public fun publish(manager: KeyManager, didDocument: DidDocument, types: List? = null) { validate(didDocument.id) val publishId = DidDht.suffix(didDocument.id) @@ -300,7 +300,7 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) { * @return The kid of the identity key. * @throws IllegalArgumentException if the provided DID does not conform to the "did:dht" method. */ - private fun getIdentityKid(didDocument: DIDDocument): String { + private fun getIdentityKid(didDocument: DidDocument): String { validate(didDocument.id) val publicKeyJwk = didDocument.verificationMethod?.first()?.publicKeyJwk @@ -360,15 +360,15 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) { } /** - * Converts a [DIDDocument] to a DNS packet according to the did:dht spec + * Converts a [DidDocument] to a DNS packet according to the did:dht spec * https://tbd54566975.github.io/did-dht-method/#dids-as-a-dns-packet * - * @param didDocument The [DIDDocument] to convert. + * @param didDocument The [DidDocument] to convert. * @param types A list of types to include in the packet. * @return A [Message] instance containing the DNS packet. */ @JvmOverloads - internal fun toDnsPacket(didDocument: DIDDocument, types: List? = null): Message { + internal fun toDnsPacket(didDocument: DidDocument, types: List? = null): Message { val message = Message(0).apply { header.setFlag(5) } // Set authoritative answer flag // Add Resource Records for each Verification Method @@ -442,7 +442,7 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) { return message } - private fun addVerificationMethodRecords(didDocument: DIDDocument, message: Message): + private fun addVerificationMethodRecords(didDocument: DidDocument, message: Message): Pair, Map> { val verificationMethodsById = mutableMapOf() val verificationMethods = buildList { @@ -484,7 +484,7 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) { return Pair(verificationMethods, verificationMethodsById) } - private fun addAlsoKnownAsRecord(didDocument: DIDDocument, message: Message) { + private fun addAlsoKnownAsRecord(didDocument: DidDocument, message: Message) { if (didDocument.alsoKnownAs.isNullOrEmpty()) { return } @@ -498,7 +498,7 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) { ) } - private fun addControllerRecord(didDocument: DIDDocument, message: Message) { + private fun addControllerRecord(didDocument: DidDocument, message: Message) { message.addRecord( TXTRecord( Name("_cnt._did."), @@ -510,16 +510,16 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) { } /** - * Converts a DNS packet to a [DIDDocument] according to the did:dht spec + * Converts a DNS packet to a [DidDocument] according to the did:dht spec * https://tbd54566975.github.io/did-dht-method/#dids-as-a-dns-packet * * @param did The DID that the packet is for. * @param msg The [Message] instance containing the DNS packet. - * @return A [Pair] containing the [DIDDocument] and a list of types. + * @return A [Pair] containing the [DidDocument] and a list of types. * @throws IllegalArgumentException if the provided DID does not conform to the "did:dht" method. */ - internal fun fromDnsPacket(did: String, msg: Message): Pair> { - val doc = DIDDocument.Builder().id(did) + internal fun fromDnsPacket(did: String, msg: Message): Pair> { + val doc = DidDocument.Builder().id(did) val services = mutableListOf() val types = mutableListOf() @@ -576,12 +576,12 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) { return doc.build() to types } - private fun handleAlsoKnownAsRecord(rr: TXTRecord, doc: DIDDocument.Builder) { + private fun handleAlsoKnownAsRecord(rr: TXTRecord, doc: DidDocument.Builder) { val data = rr.strings.joinToString("") doc.alsoKnownAses(data.split(ARRAY_SEPARATOR)) } - private fun handleControllerRecord(rr: TXTRecord, doc: DIDDocument.Builder) { + private fun handleControllerRecord(rr: TXTRecord, doc: DidDocument.Builder) { val data = rr.strings.joinToString("") doc.controllers(data.split(ARRAY_SEPARATOR)) } @@ -591,7 +591,7 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) { did: String, keyLookup: MutableMap, name: String, - didDocBuilder: DIDDocument.Builder + didDocBuilder: DidDocument.Builder ) { val data = parseTxtData(rr.strings.joinToString("")) val verificationMethodId = data["id"]!! @@ -624,7 +624,7 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) { private fun handleRootRecord( rr: TXTRecord, keyLookup: Map, - doc: DIDDocument.Builder, + doc: DidDocument.Builder, ) { val rootData = rr.strings.joinToString(PROPERTY_SEPARATOR).split(PROPERTY_SEPARATOR) @@ -675,12 +675,12 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) { * * @property uri The URI of the "did:dht" which conforms to the DID standard. * @property keyManager A [KeyManager] instance utilized to manage the cryptographic keys associated with the DID. - * @property didDocument The [DIDDocument] associated with the DID, created by the class. + * @property didDocument The [DidDocument] associated with the DID, created by the class. */ public class DidDht( public val uri: String, public val keyManager: KeyManager, - public val didDocument: DIDDocument? = null + public val didDocument: DidDocument? = null ) { /** @@ -708,7 +708,7 @@ public class DidDht( * Calls [DidDht.toDnsPacket] with the provided [didDocument] and [types] and returns the result. */ @JvmOverloads - public fun toDnsPacket(didDocument: DIDDocument, types: List? = emptyList()): Message { + public fun toDnsPacket(didDocument: DidDocument, types: List? = emptyList()): Message { return DidDht.toDnsPacket(didDocument, types) } @@ -716,7 +716,7 @@ public class DidDht( * Calls [DidDht.fromDnsPacket] with the provided [did] and [msg] and returns the result. */ @JvmOverloads - public fun fromDnsPacket(did: String = this.uri, msg: Message): Pair> { + public fun fromDnsPacket(did: String = this.uri, msg: Message): Pair> { return DidDht.fromDnsPacket(did, msg) } diff --git a/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DIDDhtDocumentMetadata.kt b/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDhtDocumentMetadata.kt similarity index 69% rename from dids/src/main/kotlin/web5/sdk/dids/methods/dht/DIDDhtDocumentMetadata.kt rename to dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDhtDocumentMetadata.kt index fb163ace5..45ed6d1c2 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DIDDhtDocumentMetadata.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDhtDocumentMetadata.kt @@ -1,6 +1,6 @@ package web5.sdk.dids.methods.dht -import web5.sdk.dids.didcore.DIDDocumentMetadata +import web5.sdk.dids.didcore.DidDocumentMetadata /** * Did document metadata for did:dht that extends the base did document metadata. @@ -8,6 +8,6 @@ import web5.sdk.dids.didcore.DIDDocumentMetadata * @property types list of types * @constructor Create empty Did dht document metadata */ -public class DIDDhtDocumentMetadata( +public class DidDhtDocumentMetadata( public val types: List? = null -) : DIDDocumentMetadata() +) : DidDocumentMetadata() diff --git a/dids/src/main/kotlin/web5/sdk/dids/methods/jwk/DidJwk.kt b/dids/src/main/kotlin/web5/sdk/dids/methods/jwk/DidJwk.kt index 5244ac359..0a2bdf6a7 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/methods/jwk/DidJwk.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/methods/jwk/DidJwk.kt @@ -6,17 +6,14 @@ import web5.sdk.common.Convert import web5.sdk.common.EncodingFormat import web5.sdk.crypto.AlgorithmId import web5.sdk.crypto.InMemoryKeyManager -import web5.sdk.crypto.KeyImporter import web5.sdk.crypto.KeyManager -import web5.sdk.dids.CreateDidOptions import web5.sdk.dids.DidResolutionMetadata import web5.sdk.dids.DidResolutionResult import web5.sdk.dids.ResolutionError -import web5.sdk.dids.ResolveDidOptions -import web5.sdk.dids.did.BearerDID -import web5.sdk.dids.did.PortableDID +import web5.sdk.dids.did.BearerDid +import web5.sdk.dids.did.PortableDid import web5.sdk.dids.didcore.Did -import web5.sdk.dids.didcore.DIDDocument +import web5.sdk.dids.didcore.DidDocument import web5.sdk.dids.didcore.Purpose import web5.sdk.dids.didcore.VerificationMethod import web5.sdk.dids.exceptions.InvalidMethodNameException @@ -59,7 +56,7 @@ public class DidJwk { // todo look into whether params can be nullable if providing default values public fun create( keyManager: KeyManager = InMemoryKeyManager(), - algorithmId: AlgorithmId = AlgorithmId.Ed25519): BearerDID { + algorithmId: AlgorithmId = AlgorithmId.Ed25519): BearerDid { val keyAlias = keyManager.generatePrivateKey(algorithmId) val publicKeyJwk = keyManager.getPublicKey(keyAlias) @@ -70,15 +67,15 @@ public class DidJwk { val did = Did(method = methodName, uri = didUri, url = didUri, id = base64Encoded) - return BearerDID(did, keyManager, createDocument(did, publicKeyJwk)) + return BearerDid(did, keyManager, createDocument(did, publicKeyJwk)) } - public fun import(portableDID: PortableDID, keyManager: KeyManager = InMemoryKeyManager()): BearerDID { + public fun import(portableDID: PortableDid, keyManager: KeyManager = InMemoryKeyManager()): BearerDid { val parsedDid = Did.parse(portableDID.uri) if (parsedDid.method != methodName) { throw InvalidMethodNameException("Method not supported") } - val bearerDid = BearerDID.import(portableDID, keyManager) + val bearerDid = BearerDid.import(portableDID, keyManager) if (bearerDid.document.verificationMethod?.size == 0) { throw IllegalStateException("DidJwk DID document must contain exactly one verification method") @@ -140,7 +137,7 @@ public class DidJwk { return DidResolutionResult(didDocument = didDocument, context = "https://w3id.org/did-resolution/v1") } - private fun createDocument(did: Did, publicKeyJwk: JWK): DIDDocument { + private fun createDocument(did: Did, publicKeyJwk: JWK): DidDocument { val verificationMethodId = "${did.uri}#0" val verificationMethod = VerificationMethod.Builder() .id(verificationMethodId) @@ -149,7 +146,7 @@ public class DidJwk { .type("JsonWebKey2020") .build() - val didDocumentBuilder = DIDDocument.Builder() + val didDocumentBuilder = DidDocument.Builder() .context(listOf("https://www.w3.org/ns/did/v1")) .id(did.url) diff --git a/dids/src/main/kotlin/web5/sdk/dids/methods/key/DidKey.kt b/dids/src/main/kotlin/web5/sdk/dids/methods/key/DidKey.kt index d3d06a5d5..34fd4e047 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/methods/key/DidKey.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/methods/key/DidKey.kt @@ -11,10 +11,10 @@ import web5.sdk.dids.CreateDidOptions import web5.sdk.dids.ChangemeDid import web5.sdk.dids.DidResolutionResult import web5.sdk.dids.ResolveDidOptions -import web5.sdk.dids.did.BearerDID -import web5.sdk.dids.did.PortableDID +import web5.sdk.dids.did.BearerDid +import web5.sdk.dids.did.PortableDid import web5.sdk.dids.didcore.Did -import web5.sdk.dids.didcore.DIDDocument +import web5.sdk.dids.didcore.DidDocument import web5.sdk.dids.didcore.Purpose import web5.sdk.dids.didcore.VerificationMethod @@ -78,7 +78,7 @@ public class DidKey(uri: String, keyManager: KeyManager) : ChangemeDid(uri, keyM * * @throws UnsupportedOperationException if the specified curve is not supported. */ - public fun create(keyManager: KeyManager, options: CreateDidKeyOptions? = null): BearerDID { + public fun create(keyManager: KeyManager, options: CreateDidKeyOptions? = null): BearerDid { val opts = options ?: CreateDidKeyOptions() val keyAlias = keyManager.generatePrivateKey(opts.algorithmId) @@ -103,7 +103,7 @@ public class DidKey(uri: String, keyManager: KeyManager) : ChangemeDid(uri, keyM if (resolutionResult.didDocument == null) { throw IllegalStateException("DidDocument not found") } - return BearerDID(did, keyManager, resolutionResult.didDocument) + return BearerDid(did, keyManager, resolutionResult.didDocument) } /** @@ -143,7 +143,7 @@ public class DidKey(uri: String, keyManager: KeyManager) : ChangemeDid(uri, keyM .type("JsonWebKey2020") .build() - val didDocument = DIDDocument.Builder() + val didDocument = DidDocument.Builder() .id(did) .verificationMethodForPurposes( verificationMethod, @@ -159,13 +159,13 @@ public class DidKey(uri: String, keyManager: KeyManager) : ChangemeDid(uri, keyM return DidResolutionResult(didDocument = didDocument, context = "https://w3id.org/did-resolution/v1") } - public fun import(portableDID: PortableDID, keyManager: KeyManager): BearerDID { + public fun import(portableDID: PortableDid, keyManager: KeyManager): BearerDid { val parsedDid = Did.parse(portableDID.uri) if (parsedDid.method != methodName) { throw MethodNotSupportedException("Method not supported") } - val bearerDid = BearerDID.import(portableDID, keyManager) + val bearerDid = BearerDid.import(portableDID, keyManager) if (bearerDid.document.verificationMethod?.size != 1) { throw IllegalStateException("DidKey DID document must contain exactly one verification method") diff --git a/dids/src/main/kotlin/web5/sdk/dids/methods/web/DidWeb.kt b/dids/src/main/kotlin/web5/sdk/dids/methods/web/DidWeb.kt index fbac2ed56..aaeff8b43 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/methods/web/DidWeb.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/methods/web/DidWeb.kt @@ -22,12 +22,12 @@ import web5.sdk.common.Json import web5.sdk.crypto.KeyManager import web5.sdk.dids.CreateDidOptions import web5.sdk.dids.didcore.Did -import web5.sdk.dids.didcore.DIDDocument +import web5.sdk.dids.didcore.DidDocument import web5.sdk.dids.DidResolutionResult import web5.sdk.dids.exceptions.ParserException import web5.sdk.dids.ResolutionError -import web5.sdk.dids.did.BearerDID -import web5.sdk.dids.did.PortableDID +import web5.sdk.dids.did.BearerDid +import web5.sdk.dids.did.PortableDid import java.io.File import java.net.InetAddress import java.net.URL @@ -117,8 +117,8 @@ public sealed class DidWebApi( } } - public fun import(portableDID: PortableDID, keyManager: KeyManager): BearerDID { - return BearerDID.import(portableDID, keyManager) + public fun import(portableDID: PortableDid, keyManager: KeyManager): BearerDid { + return BearerDid.import(portableDID, keyManager) } private fun resolveInternal(did: String): DidResolutionResult { @@ -150,7 +150,7 @@ public sealed class DidWebApi( throw ResponseException(resp, "resolution error response: '$body'") } return DidResolutionResult( - didDocument = mapper.readValue(body, DIDDocument::class.java), + didDocument = mapper.readValue(body, DidDocument::class.java), ) } @@ -168,7 +168,7 @@ public sealed class DidWebApi( return targetUrl.toString() } - public fun create(keyManager: KeyManager, options: CreateDidOptions?): BearerDID { + public fun create(keyManager: KeyManager, options: CreateDidOptions?): BearerDid { throw UnsupportedOperationException("Create operation is not supported for did:web") } } \ No newline at end of file diff --git a/dids/src/test/kotlin/web5/sdk/dids/didcore/DIDDocumentTest.kt b/dids/src/test/kotlin/web5/sdk/dids/didcore/DidDocumentTest.kt similarity index 91% rename from dids/src/test/kotlin/web5/sdk/dids/didcore/DIDDocumentTest.kt rename to dids/src/test/kotlin/web5/sdk/dids/didcore/DidDocumentTest.kt index 0d1dd0bb1..9743d40fc 100644 --- a/dids/src/test/kotlin/web5/sdk/dids/didcore/DIDDocumentTest.kt +++ b/dids/src/test/kotlin/web5/sdk/dids/didcore/DidDocumentTest.kt @@ -9,7 +9,7 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNull -class DIDDocumentTest { +class DidDocumentTest { @Nested inner class SelectVerificationMethodTest { @@ -17,7 +17,7 @@ class DIDDocumentTest { @Test fun `selectVerificationMethod throws exception if vmMethod is empty`() { - val doc = DIDDocument("did:example:123") + val doc = DidDocument("did:example:123") assertThrows { doc.selectVerificationMethod(Purpose.AssertionMethod) @@ -34,7 +34,7 @@ class DIDDocumentTest { val vmList = listOf( VerificationMethod("id", "type", "controller", publicKeyJwk) ) - val doc = DIDDocument(id = "did:example:123", verificationMethod = vmList) + val doc = DidDocument(id = "did:example:123", verificationMethod = vmList) val vm = doc.selectVerificationMethod(null) assertEquals("id", vm.id) @@ -54,7 +54,7 @@ class DIDDocumentTest { VerificationMethod("id", "type", "controller", publicKeyJwk) ) val assertionMethods = listOf("id") - val doc = DIDDocument( + val doc = DidDocument( id = "did:example:123", verificationMethod = vmList, assertionMethod = assertionMethods @@ -78,7 +78,7 @@ class DIDDocumentTest { VerificationMethod("id", "type", "controller", publicKeyJwk) ) val assertionMethods = listOf("id") - val doc = DIDDocument( + val doc = DidDocument( id = "did:example:123", verificationMethod = vmList, assertionMethod = assertionMethods @@ -101,7 +101,7 @@ class DIDDocumentTest { val vmList = listOf( VerificationMethod("id", "type", "controller", publicKeyJwk) ) - val doc = DIDDocument( + val doc = DidDocument( id = "did:example:123", verificationMethod = vmList ) @@ -117,14 +117,14 @@ class DIDDocumentTest { inner class GetAbsoluteResourceIDTest { @Test fun `getAbsoluteResourceID returns absolute resource id if passed in fragment`() { - val doc = DIDDocument("did:example:123") + val doc = DidDocument("did:example:123") val resourceID = doc.getAbsoluteResourceID("#0") assertEquals("did:example:123#0", resourceID) } @Test fun `getAbsoluteResourceID returns absolute resource id if passed in full id`() { - val doc = DIDDocument("did:example:123") + val doc = DidDocument("did:example:123") val resourceID = doc.getAbsoluteResourceID("did:example:123#1") assertEquals("did:example:123#1", resourceID) } @@ -135,7 +135,7 @@ class DIDDocumentTest { inner class FindAssertionMethodByIdTest { @Test fun `findAssertionMethodById throws exception if assertionMethod list is empty`() { - val doc = DIDDocument("did:example:123") + val doc = DidDocument("did:example:123") assertThrows { doc.findAssertionMethodById() @@ -146,7 +146,7 @@ class DIDDocumentTest { fun `findAssertionMethodById throws exception if assertionMethod does not have provided id`() { val assertionMethods = listOf("foo") - val doc = DIDDocument(id = "did:example:123", assertionMethod = assertionMethods) + val doc = DidDocument(id = "did:example:123", assertionMethod = assertionMethods) assertThrows { doc.findAssertionMethodById("bar") @@ -164,7 +164,7 @@ class DIDDocumentTest { VerificationMethod("foo", "type", "controller", publicKeyJwk) ) - val doc = DIDDocument(id = "did:example:123", verificationMethod = vmList, assertionMethod = assertionMethods) + val doc = DidDocument(id = "did:example:123", verificationMethod = vmList, assertionMethod = assertionMethods) assertThrows { doc.findAssertionMethodById() @@ -182,7 +182,7 @@ class DIDDocumentTest { VerificationMethod("foo", "type", "controller", publicKeyJwk) ) - val doc = DIDDocument(id = "did:example:123", verificationMethod = vmList, assertionMethod = assertionMethods) + val doc = DidDocument(id = "did:example:123", verificationMethod = vmList, assertionMethod = assertionMethods) val assertionMethod = doc.findAssertionMethodById("foo") assertEquals("foo", assertionMethod.id) @@ -194,7 +194,7 @@ class DIDDocumentTest { @Nested inner class BuilderTest { @Test - fun `builder creates a DIDDocument with the provided id`() { + fun `builder creates a DidDocument with the provided id`() { val svc = Service.Builder() .id("service_id") @@ -202,7 +202,7 @@ class DIDDocumentTest { .serviceEndpoint(listOf("https://example.com")) .build() - val doc = DIDDocument.Builder() + val doc = DidDocument.Builder() .id("did:ex:foo") .context(listOf("https://www.w3.org/ns/did/v1")) .controllers(listOf("did:ex:foo")) @@ -224,7 +224,7 @@ class DIDDocumentTest { val publicKeyJwk = manager.getPublicKey(keyAlias) val vm = VerificationMethod("foo", "type", "controller", publicKeyJwk) - val doc = DIDDocument.Builder() + val doc = DidDocument.Builder() .id("did:ex:foo") .context(listOf("https://www.w3.org/ns/did/v1")) .verificationMethodForPurposes(vm, @@ -253,7 +253,7 @@ class DIDDocumentTest { val publicKeyJwk = manager.getPublicKey(keyAlias) val vm = VerificationMethod("foo", "type", "controller", publicKeyJwk) - val doc = DIDDocument.Builder() + val doc = DidDocument.Builder() .id("did:ex:foo") .context(listOf("https://www.w3.org/ns/did/v1")) .verificationMethodForPurposes(vm,listOf(Purpose.Authentication)) @@ -274,7 +274,7 @@ class DIDDocumentTest { val publicKeyJwk = manager.getPublicKey(keyAlias) val vm = VerificationMethod("foo", "type", "controller", publicKeyJwk) - val doc = DIDDocument.Builder() + val doc = DidDocument.Builder() .id("did:ex:foo") .context(listOf("https://www.w3.org/ns/did/v1")) .verificationMethodForPurposes(vm) @@ -290,7 +290,7 @@ class DIDDocumentTest { @Test fun `verificationMethodIdsForPurpose builds list for one purpose`() { - val doc = DIDDocument.Builder() + val doc = DidDocument.Builder() .id("did:ex:foo") .context(listOf("https://www.w3.org/ns/did/v1")) .verificationMethodIdsForPurpose(mutableListOf("keyagreementId"), Purpose.KeyAgreement) diff --git a/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DidDhtTest.kt b/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DidDhtTest.kt index a3828d878..5b2361a9e 100644 --- a/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DidDhtTest.kt +++ b/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DidDhtTest.kt @@ -24,7 +24,7 @@ import web5.sdk.crypto.InMemoryKeyManager import web5.sdk.dids.DidResolutionResult import web5.sdk.dids.JwkDeserializer import web5.sdk.dids.PurposesDeserializer -import web5.sdk.dids.didcore.DIDDocument +import web5.sdk.dids.didcore.DidDocument import web5.sdk.dids.didcore.Purpose import web5.sdk.dids.didcore.Service import web5.sdk.dids.exceptions.InvalidIdentifierException @@ -365,7 +365,7 @@ class Web5TestVectorsDidDht { @Test fun create() { - val typeRef = object : TypeReference>() {} + val typeRef = object : TypeReference>() {} val testVectors = mapper.readValue(File("../web5-spec/test-vectors/did_dht/create.json"), typeRef) testVectors.vectors.forEach { vector -> diff --git a/dids/src/test/kotlin/web5/sdk/dids/methods/web/DidWebTest.kt b/dids/src/test/kotlin/web5/sdk/dids/methods/web/DidWebTest.kt index 26c13d30a..254031c9d 100644 --- a/dids/src/test/kotlin/web5/sdk/dids/methods/web/DidWebTest.kt +++ b/dids/src/test/kotlin/web5/sdk/dids/methods/web/DidWebTest.kt @@ -13,7 +13,6 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import web5.sdk.crypto.InMemoryKeyManager import web5.sdk.dids.DidResolutionResult -import web5.sdk.dids.did.PortableDID import web5.sdk.dids.methods.util.readKey import web5.sdk.testing.TestVectors import java.io.File From 90fa5170e6316a981a368baeec354ae542dda2e7 Mon Sep 17 00:00:00 2001 From: Jiyoon Koo Date: Mon, 11 Mar 2024 15:05:55 -0400 Subject: [PATCH 10/47] wrote jwt class. modify jws class to include builder for jwsheader --- dids/src/main/kotlin/web5/sdk/dids/jws/Jws.kt | 34 ++++- dids/src/main/kotlin/web5/sdk/dids/jwt/Jwt.kt | 132 ++++++++++++++++++ 2 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 dids/src/main/kotlin/web5/sdk/dids/jwt/Jwt.kt diff --git a/dids/src/main/kotlin/web5/sdk/dids/jws/Jws.kt b/dids/src/main/kotlin/web5/sdk/dids/jws/Jws.kt index d83d74524..1612fad3b 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/jws/Jws.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/jws/Jws.kt @@ -67,7 +67,7 @@ public object Jws { val toSign = "$headerBase64Url.$payloadBase64Url" val toSignBytes = Convert(toSign).toByteArray() - val signatureBytes = signer.invoke(toSignBytes) + val signatureBytes = signer.invoke(toSignBytes) val signatureBase64Url = Convert(signatureBytes).toBase64Url(padding = false) if (detached) { @@ -90,7 +90,39 @@ public class JwsHeader( public var alg: String? = null, public var kid: String? = null ) { + + public fun toBase64Url(): String { + return toBase64Url(this) + } + + public class Builder { + private var typ: String? = null + private var alg: String? = null + private var kid: String? = null + + public fun typ(typ: String): Builder { + this.typ = typ + return this + } + + public fun alg(alg: String): Builder { + this.alg = alg + return this + } + + public fun kid(kid: String): Builder { + this.kid = kid + return this + } + + public fun build(): JwsHeader { + return JwsHeader(typ, alg, kid) + } + } + public companion object { + // todo do i need these toJson and fromJson? + // i could just call Json.jsonMapper.xyz() public fun toJson(header: JwsHeader): String { return Json.jsonMapper.writeValueAsString(header) } diff --git a/dids/src/main/kotlin/web5/sdk/dids/jwt/Jwt.kt b/dids/src/main/kotlin/web5/sdk/dids/jwt/Jwt.kt new file mode 100644 index 000000000..d2f531397 --- /dev/null +++ b/dids/src/main/kotlin/web5/sdk/dids/jwt/Jwt.kt @@ -0,0 +1,132 @@ +package web5.sdk.dids.jwt + +import com.fasterxml.jackson.databind.JsonNode +import web5.sdk.common.Convert +import web5.sdk.common.Json +import web5.sdk.dids.did.BearerDid +import web5.sdk.dids.jws.DecodedJws +import web5.sdk.dids.jws.Jws +import web5.sdk.dids.jws.JwsHeader + +public object Jwt { + + public fun decode(jwt: String): DecodedJwt { + val decodedJws = Jws.decode(jwt) + + val claims: JwtClaimsSet + try { + val payload = Convert(decodedJws.payload).toStr() + // todo don't know if this is correct + claims = JwtClaimsSet.fromJson(Json.jsonMapper.readTree(payload)) + } catch (e: Exception) { + throw IllegalArgumentException( + "Malformed JWT. " + + "Invalid base64url encoding for JWT payload. ${e.message}" + ) + } + + return DecodedJwt( + header = decodedJws.header, + claims = claims, + signature = decodedJws.signature, + parts = decodedJws.parts + ) + + } + + public fun sign(did: BearerDid, payload: JwtClaimsSet): String { + val header = JwtHeader(typ = "JWT") + val payloadBytes = Convert(Json.jsonMapper.writeValueAsBytes(payload)).toByteArray() + + return Jws.sign(did, payloadBytes, header) + } +} + +public class DecodedJwt( + public val header: JwtHeader, + public val claims: JwtClaimsSet, + public val signature: ByteArray, + public val parts: List +) { + public fun verify() { + val decodedJws = DecodedJws( + header = header, + // todo why does this use parts[1] instead of claims? + payload = Convert(parts[1]).toByteArray(), + signature = signature, + parts = parts + ) + decodedJws.verify() + } +} + +public typealias JwtHeader = JwsHeader + +public class JwtClaimsSet( + public val iss: String? = null, + public val sub: String? = null, + public val aud: String? = null, + public val exp: Long? = null, + public val nbf: Long? = null, + public val iat: Long? = null, + public val jti: String? = null, + public val misc: Map = emptyMap() +) { + public companion object { + public fun fromJson(jsonNode: JsonNode): JwtClaimsSet { + val reservedClaims = setOf( + "iss", + "sub", + "aud", + "exp", + "nbf", + "iat", + "jti" + ) + + val miscClaims: MutableMap = mutableMapOf() + + val fields = jsonNode.fields() + while (fields.hasNext()) { + val (key, value) = fields.next() + if (!reservedClaims.contains(key)) { + miscClaims[key] = value + } + } + + return JwtClaimsSet( + iss = jsonNode.get("iss")?.asText(), + sub = jsonNode.get("sub")?.asText(), + aud = jsonNode.get("aud")?.asText(), + exp = jsonNode.get("exp")?.asLong(), + nbf = jsonNode.get("nbf")?.asLong(), + iat = jsonNode.get("iat")?.asLong(), + jti = jsonNode.get("jti")?.asText(), + misc = miscClaims + ) + + } + } + + public class Builder { + private var iss: String? = null + private var sub: String? = null + private var aud: String? = null + private var exp: Long? = null + private var nbf: Long? = null + private var iat: Long? = null + private var jti: String? = null + private var misc: MutableMap = mutableMapOf() + + public fun iss(iss: String): Builder = apply { this.iss = iss } + public fun sub(sub: String): Builder = apply { this.sub = sub } + public fun aud(aud: String): Builder = apply { this.aud = aud } + public fun exp(exp: Long): Builder = apply { this.exp = exp } + public fun nbf(nbf: Long): Builder = apply { this.nbf = nbf } + public fun iat(iat: Long): Builder = apply { this.iat = iat } + public fun jti(jti: String): Builder = apply { this.jti = jti } + public fun misc(key: String, value: Any): Builder = apply { this.misc[key] = value } + + public fun build(): JwtClaimsSet = JwtClaimsSet(iss, sub, aud, exp, nbf, iat, jti, misc) + } +} From 584a36bd920440899d0e4b880d171424ee98b6b0 Mon Sep 17 00:00:00 2001 From: Jiyoon Koo Date: Mon, 11 Mar 2024 15:19:10 -0400 Subject: [PATCH 11/47] sub out JwtUtils.sign() for Jwt.sign() --- .../sdk/credentials/VerifiableCredential.kt | 14 +++++++----- .../sdk/credentials/VerifiablePresentation.kt | 22 ++++++++++--------- .../web5/sdk/credentials/util/JwtUtil.kt | 22 ++++++++++--------- .../main/kotlin/web5/sdk/dids/DidMethod.kt | 10 ++++----- dids/src/main/kotlin/web5/sdk/dids/jwt/Jwt.kt | 16 +++++++------- 5 files changed, 45 insertions(+), 39 deletions(-) diff --git a/credentials/src/main/kotlin/web5/sdk/credentials/VerifiableCredential.kt b/credentials/src/main/kotlin/web5/sdk/credentials/VerifiableCredential.kt index db2bcd9a8..4d51abd1a 100644 --- a/credentials/src/main/kotlin/web5/sdk/credentials/VerifiableCredential.kt +++ b/credentials/src/main/kotlin/web5/sdk/credentials/VerifiableCredential.kt @@ -14,7 +14,9 @@ import com.nimbusds.jwt.JWTClaimsSet import com.nimbusds.jwt.JWTParser import com.nimbusds.jwt.SignedJWT import web5.sdk.credentials.util.JwtUtil -import web5.sdk.dids.ChangemeDid +import web5.sdk.dids.did.BearerDid +import web5.sdk.dids.jwt.Jwt +import web5.sdk.dids.jwt.JwtClaimsSet import java.net.URI import java.security.SignatureException import java.util.Date @@ -64,15 +66,15 @@ public class VerifiableCredential internal constructor(public val vcDataModel: V * ``` */ @JvmOverloads - public fun sign(did: ChangemeDid, assertionMethodId: String? = null): String { - val payload = JWTClaimsSet.Builder() + public fun sign(did: BearerDid, assertionMethodId: String? = null): String { + val payload = JwtClaimsSet.Builder() .issuer(vcDataModel.issuer.toString()) - .issueTime(vcDataModel.issuanceDate) + .issueTime(vcDataModel.issuanceDate.time) .subject(vcDataModel.credentialSubject.id.toString()) - .claim("vc", vcDataModel.toMap()) + .misc("vc", vcDataModel.toMap()) .build() - return JwtUtil.sign(did, assertionMethodId, payload) + return Jwt.sign(did, payload) } /** diff --git a/credentials/src/main/kotlin/web5/sdk/credentials/VerifiablePresentation.kt b/credentials/src/main/kotlin/web5/sdk/credentials/VerifiablePresentation.kt index 04b7d366f..48f590689 100644 --- a/credentials/src/main/kotlin/web5/sdk/credentials/VerifiablePresentation.kt +++ b/credentials/src/main/kotlin/web5/sdk/credentials/VerifiablePresentation.kt @@ -7,7 +7,9 @@ import com.nimbusds.jwt.JWTClaimsSet import com.nimbusds.jwt.JWTParser import com.nimbusds.jwt.SignedJWT import web5.sdk.credentials.util.JwtUtil -import web5.sdk.dids.ChangemeDid +import web5.sdk.dids.did.BearerDid +import web5.sdk.dids.jwt.Jwt +import web5.sdk.dids.jwt.JwtClaimsSet import java.net.URI import java.security.SignatureException import java.util.Date @@ -41,13 +43,13 @@ public class VerifiablePresentation internal constructor(public val vpDataModel: get() = vpDataModel.holder.toString() /** - * Sign a verifiable presentation using a specified decentralized identifier ([did]) with the private key that pairs + * Sign a verifiable presentation using a specified decentralized identifier ([bearerDid]) with the private key that pairs * with the public key identified by [assertionMethodId]. * * If the [assertionMethodId] is null, the function will attempt to use the first available verification method from - * the [did]. The result is a String in a JWT format. + * the [bearerDid]. The result is a String in a JWT format. * - * @param did The [ChangemeDid] used to sign the credential. + * @param bearerDid The [ChangemeDid] used to sign the credential. * @param assertionMethodId An optional identifier for the assertion method that will be used for verification of the * produced signature. * @return The JWT representing the signed verifiable credential. @@ -58,14 +60,14 @@ public class VerifiablePresentation internal constructor(public val vpDataModel: * ``` */ @JvmOverloads - public fun sign(did: ChangemeDid, assertionMethodId: String? = null): String { - val payload = JWTClaimsSet.Builder() - .issuer(did.uri) - .issueTime(Date()) - .claim("vp", vpDataModel.toMap()) + public fun sign(bearerDid: BearerDid, assertionMethodId: String? = null): String { + val payload = JwtClaimsSet.Builder() + .issuer(bearerDid.did.uri) + .issueTime(Date().time) + .misc("vp", vpDataModel.toMap()) .build() - return JwtUtil.sign(did, assertionMethodId, payload) + return Jwt.sign(bearerDid, payload) } /** diff --git a/credentials/src/main/kotlin/web5/sdk/credentials/util/JwtUtil.kt b/credentials/src/main/kotlin/web5/sdk/credentials/util/JwtUtil.kt index b17402a99..d5f98b466 100644 --- a/credentials/src/main/kotlin/web5/sdk/credentials/util/JwtUtil.kt +++ b/credentials/src/main/kotlin/web5/sdk/credentials/util/JwtUtil.kt @@ -9,8 +9,8 @@ import com.nimbusds.jwt.JWTParser import com.nimbusds.jwt.SignedJWT import web5.sdk.common.Convert import web5.sdk.crypto.Crypto -import web5.sdk.dids.ChangemeDid import web5.sdk.dids.DidResolvers +import web5.sdk.dids.did.BearerDid import web5.sdk.dids.didcore.Did import web5.sdk.dids.exceptions.DidResolutionException import web5.sdk.dids.exceptions.PublicKeyJwkMissingException @@ -25,13 +25,13 @@ private const val JSON_WEB_KEY = "JsonWebKey" */ public object JwtUtil { /** - * Sign a jwt payload using a specified decentralized identifier ([did]) with the private key that pairs + * Sign a jwt payload using a specified decentralized identifier ([bearerDid]) with the private key that pairs * with the public key identified by [assertionMethodId]. * * If the [assertionMethodId] is null, the function will attempt to use the first available verification method from - * the [did]. The result is a String in a JWT format. + * the [bearerDid]. The result is a String in a JWT format. * - * @param did The [ChangemeDid] used to sign the credential. + * @param bearerDid The [ChangemeDid] used to sign the credential. * @param assertionMethodId An optional identifier for the assertion method * that will be used for verification of the produced signature. * @param jwtPayload the payload that is getting signed by the [ChangemeDid] @@ -42,13 +42,15 @@ public object JwtUtil { * val signedVc = verifiableCredential.sign(myDid) * ``` */ - public fun sign(did: ChangemeDid, assertionMethodId: String?, jwtPayload: JWTClaimsSet): String { - val didResolutionResult = DidResolvers.resolve(did.uri) + // todo figure out how assertionMethodId is being used, make sure it's not lost + // when subbing this out with Jwt.sign(bearerDid, jwtPayload) + public fun sign(bearerDid: BearerDid, assertionMethodId: String?, jwtPayload: JWTClaimsSet): String { + val didResolutionResult = DidResolvers.resolve(bearerDid.did.uri) val didDocument = didResolutionResult.didDocument if (didResolutionResult.didResolutionMetadata.error != null || didDocument == null) { throw DidResolutionException( "Signature verification failed: " + - "Failed to resolve DID ${did.uri}. " + + "Failed to resolve DID ${bearerDid.did.uri}. " + "Error: ${didResolutionResult.didResolutionMetadata.error}" ) } @@ -56,7 +58,7 @@ public object JwtUtil { val assertionMethod = didDocument.findAssertionMethodById(assertionMethodId) val publicKeyJwk = assertionMethod.publicKeyJwk ?: throw PublicKeyJwkMissingException("publicKeyJwk is null.") - val keyAlias = did.keyManager.getDeterministicAlias(publicKeyJwk) + val keyAlias = bearerDid.keyManager.getDeterministicAlias(publicKeyJwk) // TODO: figure out how to make more reliable since algorithm is technically not a required property of a JWK val algorithm = publicKeyJwk.algorithm @@ -64,7 +66,7 @@ public object JwtUtil { val kid = when (URI.create(assertionMethod.id).isAbsolute) { true -> assertionMethod.id - false -> "${did.uri}${assertionMethod.id}" + false -> "${bearerDid.did.uri}${assertionMethod.id}" } val jwtHeader = JWSHeader.Builder(jwsAlgorithm) @@ -74,7 +76,7 @@ public object JwtUtil { val jwtObject = SignedJWT(jwtHeader, jwtPayload) val toSign = jwtObject.signingInput - val signatureBytes = did.keyManager.sign(keyAlias, toSign) + val signatureBytes = bearerDid.keyManager.sign(keyAlias, toSign) val base64UrlEncodedHeader = jwtHeader.toBase64URL() val base64UrlEncodedPayload = jwtPayload.toPayload().toBase64URL() diff --git a/dids/src/main/kotlin/web5/sdk/dids/DidMethod.kt b/dids/src/main/kotlin/web5/sdk/dids/DidMethod.kt index f0960c027..b09669d04 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/DidMethod.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/DidMethod.kt @@ -26,11 +26,11 @@ import web5.sdk.dids.exceptions.PublicKeyJwkMissingException * Implementers should adhere to the respective DID method specifications ensuring both compliance * and interoperability across different DID networks. */ -public abstract class ChangemeDid(public val uri: String, public val keyManager: KeyManager) { - public companion object { - // static helper methods here - } -} +//public abstract class ChangemeDid(public val uri: String, public val keyManager: KeyManager) { +// public companion object { +// // static helper methods here +// } +//} /** * Represents options during the creation of a Decentralized Identifier (DID). diff --git a/dids/src/main/kotlin/web5/sdk/dids/jwt/Jwt.kt b/dids/src/main/kotlin/web5/sdk/dids/jwt/Jwt.kt index d2f531397..d9244e12d 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/jwt/Jwt.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/jwt/Jwt.kt @@ -36,7 +36,7 @@ public object Jwt { public fun sign(did: BearerDid, payload: JwtClaimsSet): String { val header = JwtHeader(typ = "JWT") - val payloadBytes = Convert(Json.jsonMapper.writeValueAsBytes(payload)).toByteArray() + val payloadBytes = Convert(Json.jsonMapper.writeValueAsString(payload)).toByteArray() return Jws.sign(did, payloadBytes, header) } @@ -118,13 +118,13 @@ public class JwtClaimsSet( private var jti: String? = null private var misc: MutableMap = mutableMapOf() - public fun iss(iss: String): Builder = apply { this.iss = iss } - public fun sub(sub: String): Builder = apply { this.sub = sub } - public fun aud(aud: String): Builder = apply { this.aud = aud } - public fun exp(exp: Long): Builder = apply { this.exp = exp } - public fun nbf(nbf: Long): Builder = apply { this.nbf = nbf } - public fun iat(iat: Long): Builder = apply { this.iat = iat } - public fun jti(jti: String): Builder = apply { this.jti = jti } + public fun issuer(iss: String): Builder = apply { this.iss = iss } + public fun subject(sub: String): Builder = apply { this.sub = sub } + public fun audience(aud: String): Builder = apply { this.aud = aud } + public fun expirationTime(exp: Long): Builder = apply { this.exp = exp } + public fun notBeforeTime(nbf: Long): Builder = apply { this.nbf = nbf } + public fun issueTime(iat: Long): Builder = apply { this.iat = iat } + public fun jwtId(jti: String): Builder = apply { this.jti = jti } public fun misc(key: String, value: Any): Builder = apply { this.misc[key] = value } public fun build(): JwtClaimsSet = JwtClaimsSet(iss, sub, aud, exp, nbf, iat, jti, misc) From a9e1494ce64e18e16b86f2c458df8f5f940f14a0 Mon Sep 17 00:00:00 2001 From: Jiyoon Koo Date: Tue, 12 Mar 2024 00:24:35 -0400 Subject: [PATCH 12/47] implemented jwt. removed jwtutil class --- .../main/kotlin/web5/sdk/common/Convert.kt | 7 +- .../sdk/credentials/VerifiableCredential.kt | 5 +- .../sdk/credentials/VerifiablePresentation.kt | 5 +- .../web5/sdk/credentials/util/JwtUtil.kt | 175 ------------------ .../credentials/PresentationExchangeTest.kt | 112 +++++------ .../credentials/VerifiablePresentationTest.kt | 18 +- .../main/kotlin/web5/sdk/crypto/Ed25519.kt | 6 +- .../kotlin/web5/sdk/crypto/Secp256k1Test.kt | 6 +- dids/src/main/kotlin/web5/sdk/dids/jws/Jws.kt | 67 ++++--- dids/src/main/kotlin/web5/sdk/dids/jwt/Jwt.kt | 6 + .../web5/sdk/dids/methods/dht/DidDht.kt | 4 +- .../web5/sdk/dids/methods/jwk/DidJwk.kt | 2 +- .../web5/sdk/dids/methods/key/DidKey.kt | 3 +- .../kotlin/web5/sdk/dids/DidResolversTest.kt | 2 +- .../web5/sdk/dids/methods/dht/DhtTest.kt | 12 +- .../web5/sdk/dids/methods/jwk/DidJwkTest.kt | 4 +- .../web5/sdk/dids/methods/key/DidKeyTest.kt | 6 +- 17 files changed, 145 insertions(+), 295 deletions(-) delete mode 100644 credentials/src/main/kotlin/web5/sdk/credentials/util/JwtUtil.kt diff --git a/common/src/main/kotlin/web5/sdk/common/Convert.kt b/common/src/main/kotlin/web5/sdk/common/Convert.kt index 698b52839..e758bbc56 100644 --- a/common/src/main/kotlin/web5/sdk/common/Convert.kt +++ b/common/src/main/kotlin/web5/sdk/common/Convert.kt @@ -46,7 +46,7 @@ public val B64URL_DECODER: Base64.Decoder = Base64.getUrlDecoder() * ``` * // Example 1: Convert a ByteArray to a Base64Url encoded string without padding * val byteArray = byteArrayOf(1, 2, 3) - * val base64Url = Convert(byteArray).toBase64Url(padding = false) + * val base64Url = Convert(byteArray).toBase64Url() * println(base64Url) // Output should be a Base64Url encoded string without padding * * // Example 2: Convert a Base64Url encoded string to a ByteArray @@ -68,13 +68,14 @@ public class Convert(private val value: T, private val kind: EncodingFormat? /** * Converts the [value] to a Base64Url-encoded string. * - * @param padding Determines whether the resulting Base64 string should be padded or not. Default is true. + * @param padding Determines whether the resulting Base64 string should be padded or not. + * Default is false. * @return The Base64Url-encoded string. * * Note: If the value type is unsupported for this conversion, the method will throw an exception. */ @JvmOverloads - public fun toBase64Url(padding: Boolean = true): String { + public fun toBase64Url(padding: Boolean = false): String { val encoder = if (padding) B64URL_ENCODER else B64URL_ENCODER.withoutPadding() return when (this.value) { diff --git a/credentials/src/main/kotlin/web5/sdk/credentials/VerifiableCredential.kt b/credentials/src/main/kotlin/web5/sdk/credentials/VerifiableCredential.kt index 4d51abd1a..570888bb3 100644 --- a/credentials/src/main/kotlin/web5/sdk/credentials/VerifiableCredential.kt +++ b/credentials/src/main/kotlin/web5/sdk/credentials/VerifiableCredential.kt @@ -10,10 +10,8 @@ import com.fasterxml.jackson.module.kotlin.KotlinModule import com.fasterxml.jackson.module.kotlin.convertValue import com.nfeld.jsonpathkt.JsonPath import com.nfeld.jsonpathkt.extension.read -import com.nimbusds.jwt.JWTClaimsSet import com.nimbusds.jwt.JWTParser import com.nimbusds.jwt.SignedJWT -import web5.sdk.credentials.util.JwtUtil import web5.sdk.dids.did.BearerDid import web5.sdk.dids.jwt.Jwt import web5.sdk.dids.jwt.JwtClaimsSet @@ -190,7 +188,8 @@ public class VerifiableCredential internal constructor(public val vcDataModel: V * ``` */ public fun verify(vcJwt: String) { - JwtUtil.verify(vcJwt) + val decodedJwt = Jwt.decode(vcJwt) + decodedJwt.verify() } /** diff --git a/credentials/src/main/kotlin/web5/sdk/credentials/VerifiablePresentation.kt b/credentials/src/main/kotlin/web5/sdk/credentials/VerifiablePresentation.kt index 48f590689..c91012987 100644 --- a/credentials/src/main/kotlin/web5/sdk/credentials/VerifiablePresentation.kt +++ b/credentials/src/main/kotlin/web5/sdk/credentials/VerifiablePresentation.kt @@ -3,10 +3,8 @@ package web5.sdk.credentials import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.KotlinModule -import com.nimbusds.jwt.JWTClaimsSet import com.nimbusds.jwt.JWTParser import com.nimbusds.jwt.SignedJWT -import web5.sdk.credentials.util.JwtUtil import web5.sdk.dids.did.BearerDid import web5.sdk.dids.jwt.Jwt import web5.sdk.dids.jwt.JwtClaimsSet @@ -155,7 +153,8 @@ public class VerifiablePresentation internal constructor(public val vpDataModel: * ``` */ public fun verify(vpJwt: String) { - JwtUtil.verify(vpJwt) + val decodedJwt = Jwt.decode(vpJwt) + decodedJwt.verify() val vp = this.parseJwt(vpJwt) vp.verifiableCredential.forEach { diff --git a/credentials/src/main/kotlin/web5/sdk/credentials/util/JwtUtil.kt b/credentials/src/main/kotlin/web5/sdk/credentials/util/JwtUtil.kt deleted file mode 100644 index d5f98b466..000000000 --- a/credentials/src/main/kotlin/web5/sdk/credentials/util/JwtUtil.kt +++ /dev/null @@ -1,175 +0,0 @@ -package web5.sdk.credentials.util - -import com.nimbusds.jose.JOSEObjectType -import com.nimbusds.jose.JWSAlgorithm -import com.nimbusds.jose.JWSHeader -import com.nimbusds.jose.util.Base64URL -import com.nimbusds.jwt.JWTClaimsSet -import com.nimbusds.jwt.JWTParser -import com.nimbusds.jwt.SignedJWT -import web5.sdk.common.Convert -import web5.sdk.crypto.Crypto -import web5.sdk.dids.DidResolvers -import web5.sdk.dids.did.BearerDid -import web5.sdk.dids.didcore.Did -import web5.sdk.dids.exceptions.DidResolutionException -import web5.sdk.dids.exceptions.PublicKeyJwkMissingException -import java.net.URI -import java.security.SignatureException - -private const val JSON_WEB_KEY_2020 = "JsonWebKey2020" -private const val JSON_WEB_KEY = "JsonWebKey" - -/** - * Util class for common shared JWT methods. - */ -public object JwtUtil { - /** - * Sign a jwt payload using a specified decentralized identifier ([bearerDid]) with the private key that pairs - * with the public key identified by [assertionMethodId]. - * - * If the [assertionMethodId] is null, the function will attempt to use the first available verification method from - * the [bearerDid]. The result is a String in a JWT format. - * - * @param bearerDid The [ChangemeDid] used to sign the credential. - * @param assertionMethodId An optional identifier for the assertion method - * that will be used for verification of the produced signature. - * @param jwtPayload the payload that is getting signed by the [ChangemeDid] - * @return The JWT representing the signed verifiable credential. - * - * Example: - * ``` - * val signedVc = verifiableCredential.sign(myDid) - * ``` - */ - // todo figure out how assertionMethodId is being used, make sure it's not lost - // when subbing this out with Jwt.sign(bearerDid, jwtPayload) - public fun sign(bearerDid: BearerDid, assertionMethodId: String?, jwtPayload: JWTClaimsSet): String { - val didResolutionResult = DidResolvers.resolve(bearerDid.did.uri) - val didDocument = didResolutionResult.didDocument - if (didResolutionResult.didResolutionMetadata.error != null || didDocument == null) { - throw DidResolutionException( - "Signature verification failed: " + - "Failed to resolve DID ${bearerDid.did.uri}. " + - "Error: ${didResolutionResult.didResolutionMetadata.error}" - ) - } - - val assertionMethod = didDocument.findAssertionMethodById(assertionMethodId) - - val publicKeyJwk = assertionMethod.publicKeyJwk ?: throw PublicKeyJwkMissingException("publicKeyJwk is null.") - val keyAlias = bearerDid.keyManager.getDeterministicAlias(publicKeyJwk) - - // TODO: figure out how to make more reliable since algorithm is technically not a required property of a JWK - val algorithm = publicKeyJwk.algorithm - val jwsAlgorithm = JWSAlgorithm.parse(algorithm.toString()) - - val kid = when (URI.create(assertionMethod.id).isAbsolute) { - true -> assertionMethod.id - false -> "${bearerDid.did.uri}${assertionMethod.id}" - } - - val jwtHeader = JWSHeader.Builder(jwsAlgorithm) - .type(JOSEObjectType.JWT) - .keyID(kid) - .build() - - val jwtObject = SignedJWT(jwtHeader, jwtPayload) - val toSign = jwtObject.signingInput - val signatureBytes = bearerDid.keyManager.sign(keyAlias, toSign) - - val base64UrlEncodedHeader = jwtHeader.toBase64URL() - val base64UrlEncodedPayload = jwtPayload.toPayload().toBase64URL() - val base64UrlEncodedSignature = Base64URL(Convert(signatureBytes).toBase64Url(padding = false)) - - return "$base64UrlEncodedHeader.$base64UrlEncodedPayload.$base64UrlEncodedSignature" - } - - /** - * Verifies the integrity and authenticity of a JSON Web Token (JWT). - * - * This function performs several crucial validation steps to ensure the trustworthiness of the provided VC: - * - Parses and validates the structure of the JWT. - * - Ensures the presence of critical header elements `alg` and `kid` in the JWT header. - * - Resolves the Decentralized Identifier (DID) and retrieves the associated DID Document. - * - Validates the DID and establishes a set of valid verification method IDs. - * - Identifies the correct Verification Method from the DID Document based on the `kid` parameter. - * - Verifies the JWT's signature using the public key associated with the Verification Method. - * - * If any of these steps fail, the function will throw a [SignatureException] with a message indicating the nature of the failure. - * - * @param jwtString The JWT as a [String]. - * @throws SignatureException if the verification fails at any step, providing a message with failure details. - * @throws IllegalArgumentException if critical JWT header elements are absent. - */ - public fun verify(jwtString: String) { - val jwt = JWTParser.parse(jwtString) as SignedJWT // validates JWT - - require(jwt.header.algorithm != null && jwt.header.keyID != null) { - "Signature verification failed: Expected JWS header to contain alg and kid" - } - - val verificationMethodId = jwt.header.keyID - val did = Did.Parser.parse(verificationMethodId) - - val didResolutionResult = DidResolvers.resolve(did.url) - if (didResolutionResult.didResolutionMetadata.error != null) { - throw SignatureException( - "Signature verification failed: " + - "Failed to resolve DID ${did.url}. " + - "Error: ${didResolutionResult.didResolutionMetadata.error}" - ) - } - - // create a set of possible id matches. the DID spec allows for an id to be the entire `did#fragment` - // or just `#fragment`. See: https://www.w3.org/TR/did-core/#relative-did-urls. - // using a set for fast string comparison. DIDs can be lonnng. - val verificationMethodIds = setOf( - did.url, - "#${did.fragment}" - ) - - didResolutionResult.didDocument?.assertionMethod?.firstOrNull { - verificationMethodIds.contains(it) - } ?: throw SignatureException( - "Signature verification failed: Expected kid in JWS header to dereference " + - "a DID Document Verification Method with an Assertion verification relationship" - ) - - // TODO: this will be cleaned up as part of BearerDid PR - val assertionVerificationMethod = didResolutionResult - .didDocument - ?.verificationMethod - ?.find { verificationMethodIds.contains(it.id) } - - if (assertionVerificationMethod == null) { - throw SignatureException( - "Signature verification failed: Expected kid in JWS header to dereference " + - "a DID Document Verification Method with an Assertion verification relationship" - ) - } - - require( - (assertionVerificationMethod.isType(JSON_WEB_KEY_2020) || assertionVerificationMethod.isType(JSON_WEB_KEY)) && - assertionVerificationMethod.publicKeyJwk != null - ) { - throw SignatureException( - "Signature verification failed: Expected kid in JWS header to dereference " + - "a DID Document Verification Method of type $JSON_WEB_KEY_2020 or $JSON_WEB_KEY with a publicKeyJwk" - ) - } - - val publicKeyJwk = - assertionVerificationMethod.publicKeyJwk ?: throw PublicKeyJwkMissingException("publicKeyJwk is null") - val toVerifyBytes = jwt.signingInput - val signatureBytes = jwt.signature.decode() - - Crypto.verify( - publicKeyJwk, - toVerifyBytes, - signatureBytes - ) - } - - -} diff --git a/credentials/src/test/kotlin/web5/sdk/credentials/PresentationExchangeTest.kt b/credentials/src/test/kotlin/web5/sdk/credentials/PresentationExchangeTest.kt index 6c67601e8..e42b436c6 100644 --- a/credentials/src/test/kotlin/web5/sdk/credentials/PresentationExchangeTest.kt +++ b/credentials/src/test/kotlin/web5/sdk/credentials/PresentationExchangeTest.kt @@ -69,8 +69,8 @@ class PresentationExchangeTest { ) val vc = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.uri, - subject = holderDid.uri, + issuer = issuerDid.did.uri, + subject = holderDid.did.uri, data = StreetCredibility(localRespect = "high", legit = true) ) val vcJwt = vc.sign(issuerDid) @@ -86,8 +86,8 @@ class PresentationExchangeTest { ) val vc = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.uri, - subject = holderDid.uri, + issuer = issuerDid.did.uri, + subject = holderDid.did.uri, data = StreetCredibility(localRespect = "high", legit = true) ) val vcJwt = vc.sign(issuerDid) @@ -103,8 +103,8 @@ class PresentationExchangeTest { ) val vc = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.uri, - subject = holderDid.uri, + issuer = issuerDid.did.uri, + subject = holderDid.did.uri, data = StreetCredibility(localRespect = "high", legit = true) ) val vcJwt = vc.sign(issuerDid) @@ -120,8 +120,8 @@ class PresentationExchangeTest { ) val vc = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.uri, - subject = holderDid.uri, + issuer = issuerDid.did.uri, + subject = holderDid.did.uri, data = StreetCredibility(localRespect = "high", legit = true) ) val vcJwt = vc.sign(issuerDid) @@ -138,8 +138,8 @@ class PresentationExchangeTest { val vc = VerifiableCredential.create( type = "DateOfBirthVc", - issuer = issuerDid.uri, - subject = holderDid.uri, + issuer = issuerDid.did.uri, + subject = holderDid.did.uri, data = DateOfBirth(dateOfBirth = "1/1/1111") ) @@ -156,16 +156,16 @@ class PresentationExchangeTest { val vc1 = VerifiableCredential.create( type = "DateOfBirth", - issuer = issuerDid.uri, - subject = holderDid.uri, + issuer = issuerDid.did.uri, + subject = holderDid.did.uri, data = DateOfBirth(dateOfBirth = "Data1") ) val vcJwt1 = vc1.sign(issuerDid) val vc2 = VerifiableCredential.create( type = "Address", - issuer = issuerDid.uri, - subject = holderDid.uri, + issuer = issuerDid.did.uri, + subject = holderDid.did.uri, data = Address("abc street 123") ) val vcJwt2 = vc2.sign(issuerDid) @@ -182,8 +182,8 @@ class PresentationExchangeTest { val vc1 = VerifiableCredential.create( type = "DateOfBirthSSN", - issuer = issuerDid.uri, - subject = holderDid.uri, + issuer = issuerDid.did.uri, + subject = holderDid.did.uri, data = DateOfBirthSSN(dateOfBirth = "1999-01-01", ssn = "456-123-123") ) val vcJwt1 = vc1.sign(issuerDid) @@ -200,8 +200,8 @@ class PresentationExchangeTest { val vc1 = VerifiableCredential.create( type = "DateOfBirthSSN", - issuer = issuerDid.uri, - subject = holderDid.uri, + issuer = issuerDid.did.uri, + subject = holderDid.did.uri, data = DateOfBirthSSN(dateOfBirth = "1999-01-01", ssn = "456-123-123") ) val vcJwt1 = vc1.sign(issuerDid) @@ -218,8 +218,8 @@ class PresentationExchangeTest { val vc1 = VerifiableCredential.create( type = "DateOfBirthVc", - issuer = issuerDid.uri, - subject = holderDid.uri, + issuer = issuerDid.did.uri, + subject = holderDid.did.uri, data = DateOfBirth(dateOfBirth = "1/1/1111") ) @@ -227,8 +227,8 @@ class PresentationExchangeTest { val vc2 = VerifiableCredential.create( type = "Address", - issuer = issuerDid.uri, - subject = holderDid.uri, + issuer = issuerDid.did.uri, + subject = holderDid.did.uri, data = Address(address = "123 abc street") ) @@ -260,8 +260,8 @@ class PresentationExchangeTest { ) val vc = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.uri, - subject = holderDid.uri, + issuer = issuerDid.did.uri, + subject = holderDid.did.uri, data = StreetCredibility(localRespect = "high", legit = true) ) val vcJwt = vc.sign(issuerDid) @@ -285,8 +285,8 @@ class PresentationExchangeTest { ) val vc = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.uri, - subject = holderDid.uri, + issuer = issuerDid.did.uri, + subject = holderDid.did.uri, data = StreetCredibility(localRespect = "high", legit = true) ) val vcJwt = vc.sign(issuerDid) @@ -308,8 +308,8 @@ class PresentationExchangeTest { ) val vc = VerifiableCredential.create( type = "DateOfBirth", - issuer = issuerDid.uri, - subject = holderDid.uri, + issuer = issuerDid.did.uri, + subject = holderDid.did.uri, data = DateOfBirth(dateOfBirth = "01-02-03") ) val vcJwt = vc.sign(issuerDid) @@ -331,8 +331,8 @@ class PresentationExchangeTest { ) val vc = VerifiableCredential.create( type = "DateOfBirth", - issuer = issuerDid.uri, - subject = holderDid.uri, + issuer = issuerDid.did.uri, + subject = holderDid.did.uri, data = DateOfBirth(dateOfBirth = "01-02-03") ) val vcJwt = vc.sign(issuerDid) @@ -372,16 +372,16 @@ class PresentationExchangeTest { val vc1 = VerifiableCredential.create( type = "DateOfBirth", - issuer = issuerDid.uri, - subject = holderDid.uri, + issuer = issuerDid.did.uri, + subject = holderDid.did.uri, data = DateOfBirth(dateOfBirth = "Data1") ) val vcJwt1 = vc1.sign(issuerDid) val vc2 = VerifiableCredential.create( type = "Address", - issuer = issuerDid.uri, - subject = holderDid.uri, + issuer = issuerDid.did.uri, + subject = holderDid.did.uri, data = Address("abc street 123") ) val vcJwt2 = vc2.sign(issuerDid) @@ -400,8 +400,8 @@ class PresentationExchangeTest { ) val vc = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.uri, - subject = holderDid.uri, + issuer = issuerDid.did.uri, + subject = holderDid.did.uri, data = StreetCredibility(localRespect = "high", legit = true) ) val vcJwt = vc.sign(issuerDid) @@ -420,16 +420,16 @@ class PresentationExchangeTest { val vc1 = VerifiableCredential.create( type = "DateOfBirth", - issuer = issuerDid.uri, - subject = holderDid.uri, + issuer = issuerDid.did.uri, + subject = holderDid.did.uri, data = DateOfBirth(dateOfBirth = "11/11/2011") ) val vcJwt1 = vc1.sign(issuerDid) val vc2 = VerifiableCredential.create( type = "DateOfBirth", - issuer = issuerDid.uri, - subject = holderDid.uri, + issuer = issuerDid.did.uri, + subject = holderDid.did.uri, data = DateOfBirth(dateOfBirth = "12/12/2012") ) val vcJwt2 = vc2.sign(issuerDid) @@ -452,8 +452,8 @@ class PresentationExchangeTest { val vc = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.uri, - subject = holderDid.uri, + issuer = issuerDid.did.uri, + subject = holderDid.did.uri, data = StreetCredibility(localRespect = "high", legit = true) ) val vcJwt = vc.sign(issuerDid) @@ -473,16 +473,16 @@ class PresentationExchangeTest { val vc1 = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.uri, - subject = holderDid.uri, + issuer = issuerDid.did.uri, + subject = holderDid.did.uri, data = StreetCredibility(localRespect = "high", legit = true) ) val vcJwt1 = vc1.sign(issuerDid) val vc2 = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.uri, - subject = holderDid.uri, + issuer = issuerDid.did.uri, + subject = holderDid.did.uri, data = StreetCredibility(localRespect = "high", legit = true) ) val vcJwt2 = vc2.sign(issuerDid) @@ -502,16 +502,16 @@ class PresentationExchangeTest { val vc1 = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.uri, - subject = holderDid.uri, + issuer = issuerDid.did.uri, + subject = holderDid.did.uri, data = StreetCredibility(localRespect = "high", legit = true) ) val vcJwt1 = vc1.sign(issuerDid) val vc2 = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.uri, - subject = holderDid.uri, + issuer = issuerDid.did.uri, + subject = holderDid.did.uri, data = StreetCredibility(localRespect = "high", legit = true) ) @@ -519,8 +519,8 @@ class PresentationExchangeTest { val vc3 = VerifiableCredential.create( type = "DateOfBirth", - issuer = issuerDid.uri, - subject = holderDid.uri, + issuer = issuerDid.did.uri, + subject = holderDid.did.uri, data = DateOfBirth(dateOfBirth = "1-1-1111") ) @@ -541,16 +541,16 @@ class PresentationExchangeTest { val vc1 = VerifiableCredential.create( type = "DateOfBirthSSN", - issuer = issuerDid.uri, - subject = holderDid.uri, + issuer = issuerDid.did.uri, + subject = holderDid.did.uri, data = DateOfBirthSSN(dateOfBirth = "1999-01-01", ssn = "456-123-123") ) val vcJwt1 = vc1.sign(issuerDid) val vc2 = VerifiableCredential.create( type = "DateOfBirthSSN", - issuer = issuerDid.uri, - subject = holderDid.uri, + issuer = issuerDid.did.uri, + subject = holderDid.did.uri, data = DateOfBirth(dateOfBirth = "1999-01-01") ) val vcJwt2 = vc2.sign(issuerDid) diff --git a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiablePresentationTest.kt b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiablePresentationTest.kt index 42ffa8ff2..506133959 100644 --- a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiablePresentationTest.kt +++ b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiablePresentationTest.kt @@ -46,11 +46,11 @@ class VerifiablePresentationTest { val vp = VerifiablePresentation.create( vcJwts = vcJwts, - holder = holderDid.uri + holder = holderDid.did.uri ) assertNotNull(vp, "VerifiablePresentation should not be null") - assertEquals(holderDid.uri, vp.holder, "holder should match") + assertEquals(holderDid.did.uri, vp.holder, "holder should match") assertEquals(vcJwts, vp.verifiableCredential, "vcJwts should match") } @@ -75,7 +75,7 @@ class VerifiablePresentationTest { val vp = VerifiablePresentation.create( vcJwts = vcJwts, - holder = holderDid.uri, + holder = holderDid.did.uri, additionalData = mapOf("presentation_submission" to presentationSubmission) ) @@ -94,7 +94,7 @@ class VerifiablePresentationTest { val vp = VerifiablePresentation.create( vcJwts = listOf("vcjwt1", "vcjwt2"), - holder = holderDid.uri + holder = holderDid.did.uri ) val vpJwt = vp.sign(holderDid) @@ -122,7 +122,7 @@ class VerifiablePresentationTest { val vp = VerifiablePresentation.create( vcJwts = vcJwts, - holder = holderDid.uri, + holder = holderDid.did.uri, type = "PresentationSubmission", additionalData = mapOf("presentation_submission" to presentationSubmission) ) @@ -143,7 +143,7 @@ class VerifiablePresentationTest { val vp = VerifiablePresentation.create( vcJwts = vcJwts, - holder = holderDid.uri + holder = holderDid.did.uri ) val vpJwt = vp.sign(holderDid) @@ -157,7 +157,7 @@ class VerifiablePresentationTest { val holderDid = DidKey.create(keyManager) val header = JWSHeader.Builder(JWSAlgorithm.ES256K) - .keyID(holderDid.uri) + .keyID(holderDid.did.uri) .build() val vpJwt = "${header.toBase64URL()}..fakeSig" @@ -181,7 +181,7 @@ class VerifiablePresentationTest { val vp = VerifiablePresentation.create( vcJwts = vcJwts, - holder = holderDid.uri + holder = holderDid.did.uri ) val vpJwt = vp.sign(holderDid) @@ -241,7 +241,7 @@ class VerifiablePresentationTest { ) val header = JWSHeader.Builder(JWSAlgorithm.ES256K) - .keyID(issuerDid.uri) + .keyID(issuerDid.did.uri) .build() //A detached payload JWT val vpJwt = "${header.toBase64URL()}..fakeSig" diff --git a/crypto/src/main/kotlin/web5/sdk/crypto/Ed25519.kt b/crypto/src/main/kotlin/web5/sdk/crypto/Ed25519.kt index cdd7202b0..6b35d66d0 100644 --- a/crypto/src/main/kotlin/web5/sdk/crypto/Ed25519.kt +++ b/crypto/src/main/kotlin/web5/sdk/crypto/Ed25519.kt @@ -82,8 +82,8 @@ public object Ed25519 : KeyGenerator, Signer { val privateKeyParameters = Ed25519PrivateKeyParameters(privateKeyBytes, 0) val publicKeyBytes = privateKeyParameters.generatePublicKey().encoded - val base64UrlEncodedPrivateKey = Convert(privateKeyBytes).toBase64Url(padding = false) - val base64UrlEncodedPublicKey = Convert(publicKeyBytes).toBase64Url(padding = false) + val base64UrlEncodedPrivateKey = Convert(privateKeyBytes).toBase64Url() + val base64UrlEncodedPublicKey = Convert(publicKeyBytes).toBase64Url() return OctetKeyPair.Builder(com.nimbusds.jose.jwk.Curve.Ed25519, Base64URL(base64UrlEncodedPublicKey)) .algorithm(Jwa.toJwsAlgorithm(algorithm)) @@ -94,7 +94,7 @@ public object Ed25519 : KeyGenerator, Signer { } override fun bytesToPublicKey(publicKeyBytes: ByteArray): JWK { - val base64UrlEncodedPublicKey = Convert(publicKeyBytes).toBase64Url(padding = false) + val base64UrlEncodedPublicKey = Convert(publicKeyBytes).toBase64Url() return OctetKeyPair.Builder(com.nimbusds.jose.jwk.Curve.Ed25519, Base64URL(base64UrlEncodedPublicKey)) .algorithm(Jwa.toJwsAlgorithm(algorithm)) diff --git a/crypto/src/test/kotlin/web5/sdk/crypto/Secp256k1Test.kt b/crypto/src/test/kotlin/web5/sdk/crypto/Secp256k1Test.kt index d4b346d25..06f029d92 100644 --- a/crypto/src/test/kotlin/web5/sdk/crypto/Secp256k1Test.kt +++ b/crypto/src/test/kotlin/web5/sdk/crypto/Secp256k1Test.kt @@ -58,8 +58,8 @@ class Secp256k1Test { val sig2 = Secp256k1.sign(privateKey, payload) Secp256k1.verify(publicKey, payload, sig2) - val base64UrlEncodedSig1 = Convert(sig1).toBase64Url(padding = false) - val base64UrlEncodedSig2 = Convert(sig2).toBase64Url(padding = false) + val base64UrlEncodedSig1 = Convert(sig1).toBase64Url() + val base64UrlEncodedSig2 = Convert(sig2).toBase64Url() assertEquals(base64UrlEncodedSig1, base64UrlEncodedSig2) } @@ -80,7 +80,7 @@ class Secp256k1Test { val sig1 = Secp256k1.sign(privateKey, payload) Secp256k1.verify(publicKey, payload, sig1) } catch (e: SignatureException) { - val payloadString = Convert(payload).toBase64Url(false) + val payloadString = Convert(payload).toBase64Url() println("($it) $e. Payload (base64url encoded): $payloadString") throw e } diff --git a/dids/src/main/kotlin/web5/sdk/dids/jws/Jws.kt b/dids/src/main/kotlin/web5/sdk/dids/jws/Jws.kt index 1612fad3b..05a5538b7 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/jws/Jws.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/jws/Jws.kt @@ -6,6 +6,7 @@ import web5.sdk.common.Json import web5.sdk.crypto.Crypto import web5.sdk.dids.DidResolvers import web5.sdk.dids.did.BearerDid +import web5.sdk.dids.exceptions.PublicKeyJwkMissingException public object Jws { @@ -47,33 +48,37 @@ public object Jws { ): String { val (signer, verificationMethod) = bearerDid.getSigner() + check(verificationMethod.publicKeyJwk != null) { + throw PublicKeyJwkMissingException("publicKeyJwk is null.") + } + val kid = if (verificationMethod.id.startsWith("#")) { "${bearerDid.did.uri}${verificationMethod.id}" } else { verificationMethod.id } - val publicKeyJwk = verificationMethod.publicKeyJwk!! - val jwsHeader = header ?: JwsHeader() - jwsHeader.kid = kid - // todo pretty sure i need algorithm names like ES256K for secp and EdDSA for ed25519 - jwsHeader.alg = Crypto.getJwkCurve(publicKeyJwk)?.name - // todo do we need jwsHeader.typ = "??" - // todo with padding false? - // todo should padding = false by default? - val headerBase64Url = Convert(jwsHeader).toBase64Url(padding = false) - val payloadBase64Url = Convert(payload).toBase64Url(padding = false) + val jwsHeader = header + ?: JwsHeader.Builder() + .type("JWT") + .keyId(kid) + // todo pretty sure i need algorithm names like ES256K for secp and EdDSA for ed25519 + .algorithm(Crypto.getJwkCurve(verificationMethod.publicKeyJwk)?.name!!) + .build() + + val headerBase64Url = Convert(jwsHeader).toBase64Url() + val payloadBase64Url = Convert(payload).toBase64Url() val toSign = "$headerBase64Url.$payloadBase64Url" val toSignBytes = Convert(toSign).toByteArray() val signatureBytes = signer.invoke(toSignBytes) - val signatureBase64Url = Convert(signatureBytes).toBase64Url(padding = false) + val signatureBase64Url = Convert(signatureBytes).toBase64Url() - if (detached) { - return "$headerBase64Url..$signatureBase64Url" + return if (detached) { + "$headerBase64Url..$signatureBase64Url" } else { - return "$headerBase64Url.$payloadBase64Url.$signatureBase64Url" + "$headerBase64Url.$payloadBase64Url.$signatureBase64Url" } } @@ -86,9 +91,9 @@ public object Jws { } public class JwsHeader( - public var typ: String? = null, - public var alg: String? = null, - public var kid: String? = null + public val typ: String? = null, + public val alg: String? = null, + public val kid: String? = null ) { public fun toBase64Url(): String { @@ -100,22 +105,25 @@ public class JwsHeader( private var alg: String? = null private var kid: String? = null - public fun typ(typ: String): Builder { + public fun type(typ: String): Builder { this.typ = typ return this } - public fun alg(alg: String): Builder { + public fun algorithm(alg: String): Builder { this.alg = alg return this } - public fun kid(kid: String): Builder { + public fun keyId(kid: String): Builder { this.kid = kid return this } public fun build(): JwsHeader { + check(typ != null) { "typ is required" } + check(alg != null) { "alg is required" } + check(kid != null) { "kid is required" } return JwsHeader(typ, alg, kid) } } @@ -138,7 +146,7 @@ public class JwsHeader( public fun toBase64Url(header: JwsHeader): String { val jsonHeader = toJson(header) - return Convert(jsonHeader, EncodingFormat.Base64Url).toBase64Url(padding = false) + return Convert(jsonHeader, EncodingFormat.Base64Url).toBase64Url() } } } @@ -165,12 +173,29 @@ public class DecodedJws( "Verification failed. Expected header kid to dereference a verification method" } + /* + // todo + in JwtUtil, we had this + val verificationMethodIds = setOf( + did.url, + "#${did.fragment}" + ) + and were checking the assertionMethod list for a match of the id + and then find the vm that matches the id + but here we are just looking for the first? + */ val verificationMethod = dereferenceResult.didDocument.verificationMethod!!.first() check(verificationMethod.publicKeyJwk != null) { "Verification failed. Expected headeder kid to dereference" + " a verification method with a publicKeyJwk" } + // todo add this bit in coz it was in JwtUtil.verify() + check(verificationMethod.type == "JsonWebKey2020" || verificationMethod.type == "JsonWebKey") { + "Verification failed. Expected header kid to dereference " + + "a verification method of type JsonWebKey2020 or JsonWebKey" + } + val toSign = "${parts[0]}.${parts[1]}" val toSignBytes = Convert(toSign).toByteArray() diff --git a/dids/src/main/kotlin/web5/sdk/dids/jwt/Jwt.kt b/dids/src/main/kotlin/web5/sdk/dids/jwt/Jwt.kt index d9244e12d..a3a72ffa5 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/jwt/Jwt.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/jwt/Jwt.kt @@ -40,6 +40,12 @@ public object Jwt { return Jws.sign(did, payloadBytes, header) } + + public fun verify(jwt: String): DecodedJwt { + val decodedJwt = decode(jwt) + decodedJwt.verify() + return decodedJwt + } } public class DecodedJwt( diff --git a/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt b/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt index 07c8fe009..2a159c29a 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt @@ -126,7 +126,7 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) { * publishing during creation. * @return A [DidDht] instance representing the newly created "did:dht" DID. */ - public fun create(keyManager: KeyManager, options: CreateDidDhtOptions?): BearerDid { + public fun create(keyManager: KeyManager, options: CreateDidDhtOptions? = null): BearerDid { // TODO(gabe): enforce that provided keys are of supported types according to the did:dht spec val opts = options ?: CreateDidDhtOptions() @@ -449,7 +449,7 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) { didDocument.verificationMethod?.forEachIndexed { i, verificationMethod -> val publicKeyJwk = verificationMethod.publicKeyJwk ?: throw PublicKeyJwkMissingException("publicKeyJwk is null") val publicKeyBytes = Crypto.publicKeyToBytes(publicKeyJwk) - val base64UrlEncodedKey = Convert(publicKeyBytes).toBase64Url(padding = false) + val base64UrlEncodedKey = Convert(publicKeyBytes).toBase64Url() val verificationMethodId = "k$i" verificationMethodsById[verificationMethod.id] = verificationMethodId diff --git a/dids/src/main/kotlin/web5/sdk/dids/methods/jwk/DidJwk.kt b/dids/src/main/kotlin/web5/sdk/dids/methods/jwk/DidJwk.kt index 0a2bdf6a7..a42738c77 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/methods/jwk/DidJwk.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/methods/jwk/DidJwk.kt @@ -61,7 +61,7 @@ public class DidJwk { val keyAlias = keyManager.generatePrivateKey(algorithmId) val publicKeyJwk = keyManager.getPublicKey(keyAlias) - val base64Encoded = Convert(publicKeyJwk.toJSONString()).toBase64Url(padding = false) + val base64Encoded = Convert(publicKeyJwk.toJSONString()).toBase64Url() val didUri = "did:jwk:$base64Encoded" diff --git a/dids/src/main/kotlin/web5/sdk/dids/methods/key/DidKey.kt b/dids/src/main/kotlin/web5/sdk/dids/methods/key/DidKey.kt index 34fd4e047..254a9a668 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/methods/key/DidKey.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/methods/key/DidKey.kt @@ -8,7 +8,6 @@ import web5.sdk.crypto.Crypto import web5.sdk.crypto.KeyManager import web5.sdk.crypto.Secp256k1 import web5.sdk.dids.CreateDidOptions -import web5.sdk.dids.ChangemeDid import web5.sdk.dids.DidResolutionResult import web5.sdk.dids.ResolveDidOptions import web5.sdk.dids.did.BearerDid @@ -48,7 +47,7 @@ public class CreateDidKeyOptions( * * @constructor Initializes a new instance of [DidKey] with the provided [uri] and [keyManager]. */ -public class DidKey(uri: String, keyManager: KeyManager) : ChangemeDid(uri, keyManager) { +public class DidKey(public val uri: String, public val keyManager: KeyManager) : ChangemeDid(uri, keyManager) { /** * Resolves the current instance's [uri] to a [DidResolutionResult], which contains the DID Document * and possible related metadata. diff --git a/dids/src/test/kotlin/web5/sdk/dids/DidResolversTest.kt b/dids/src/test/kotlin/web5/sdk/dids/DidResolversTest.kt index d83c376bf..e7150f2fe 100644 --- a/dids/src/test/kotlin/web5/sdk/dids/DidResolversTest.kt +++ b/dids/src/test/kotlin/web5/sdk/dids/DidResolversTest.kt @@ -17,7 +17,7 @@ class DidResolversTest { @Test fun `resolving a default dht did contains assertion method`() { - val dhtDid = DidDht.create(InMemoryKeyManager()) + val dhtDid = DidDht.create(InMemoryKeyManager(), null) val resolutionResult = DidResolvers.resolve(dhtDid.uri) assertNotNull(resolutionResult.didDocument!!.assertionMethod) diff --git a/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DhtTest.kt b/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DhtTest.kt index f68844d41..a10dd02bf 100644 --- a/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DhtTest.kt +++ b/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DhtTest.kt @@ -140,12 +140,10 @@ class DhtTest { val diddht = DidDhtApi {} val did = diddht.create(manager) - require(did.didDocument != null) - - val kid = did.didDocument!!.verificationMethod?.first()?.publicKeyJwk?.keyID?.toString() + val kid = did.document.verificationMethod?.first()?.publicKeyJwk?.keyID?.toString() assertNotNull(kid) - val message = did.didDocument?.let { diddht.toDnsPacket(it) } + val message = did.document.let { diddht.toDnsPacket(it) } assertNotNull(message) val bep44Message = DhtClient.createBep44PutRequest(manager, kid, message) @@ -164,12 +162,10 @@ class DhtTest { val diddht = DidDhtApi {} val did = diddht.create(manager) - require(did.didDocument != null) - - val kid = did.didDocument!!.verificationMethod?.first()?.publicKeyJwk?.keyID?.toString() + val kid = did.document.verificationMethod?.first()?.publicKeyJwk?.keyID?.toString() assertNotNull(kid) - val message = did.didDocument?.let { diddht.toDnsPacket(it) } + val message = did.document.let { diddht.toDnsPacket(it) } assertNotNull(message) val bep44Message = DhtClient.createBep44PutRequest(manager, kid, message) diff --git a/dids/src/test/kotlin/web5/sdk/dids/methods/jwk/DidJwkTest.kt b/dids/src/test/kotlin/web5/sdk/dids/methods/jwk/DidJwkTest.kt index d6cac26c5..d2bb75ad1 100644 --- a/dids/src/test/kotlin/web5/sdk/dids/methods/jwk/DidJwkTest.kt +++ b/dids/src/test/kotlin/web5/sdk/dids/methods/jwk/DidJwkTest.kt @@ -28,7 +28,7 @@ class DidJwkTest { val manager = InMemoryKeyManager() val did = DidJwk.create(manager) - val didResolutionResult = DidResolvers.resolve(did.uri) + val didResolutionResult = DidResolvers.resolve(did.did.uri) val verificationMethod = didResolutionResult.didDocument!!.verificationMethod?.get(0) assertNotNull(verificationMethod) @@ -96,7 +96,7 @@ class DidJwkTest { val manager = InMemoryKeyManager() manager.generatePrivateKey(AlgorithmId.secp256k1) val privateJwk = JWK.parse(manager.export().first()) - val encodedPrivateJwk = Convert(privateJwk.toJSONString()).toBase64Url(padding = false) + val encodedPrivateJwk = Convert(privateJwk.toJSONString()).toBase64Url() val did = "did:jwk:$encodedPrivateJwk" assertThrows( diff --git a/dids/src/test/kotlin/web5/sdk/dids/methods/key/DidKeyTest.kt b/dids/src/test/kotlin/web5/sdk/dids/methods/key/DidKeyTest.kt index a9567755e..ffa38b8db 100644 --- a/dids/src/test/kotlin/web5/sdk/dids/methods/key/DidKeyTest.kt +++ b/dids/src/test/kotlin/web5/sdk/dids/methods/key/DidKeyTest.kt @@ -26,7 +26,7 @@ class DidKeyTest { val manager = InMemoryKeyManager() val did = DidKey.create(manager) - val didResolutionResult = DidResolvers.resolve(did.uri) + val didResolutionResult = DidResolvers.resolve(did.did.uri) assertNotNull(didResolutionResult.didDocument) val verificationMethod = didResolutionResult.didDocument!!.verificationMethod?.get(0) @@ -124,11 +124,11 @@ class DidKeyTest { assertDoesNotThrow { val km = InMemoryKeyManager() - val did = DidKey.create(km) + val bearerDid = DidKey.create(km) val keySet = km.export() val serializedKeySet = jsonMapper.writeValueAsString(keySet) - val didUri = did.uri + val didUri = bearerDid.did.uri val jsonKeySet: List> = jsonMapper.readValue(serializedKeySet) val km2 = InMemoryKeyManager() From d387f2cc29f7e2a7ab89dd3b9958a28ccf53e6e9 Mon Sep 17 00:00:00 2001 From: Jiyoon Koo Date: Tue, 12 Mar 2024 00:58:51 -0400 Subject: [PATCH 13/47] using hand rolled JwsHeader and JwtClaimsSet instead of nimbusds --- .../sdk/credentials/PresentationExchange.kt | 7 ++- .../sdk/credentials/VerifiableCredential.kt | 9 ++-- .../sdk/credentials/VerifiablePresentation.kt | 14 +++-- .../credentials/VerifiableCredentialTest.kt | 52 ++++++------------- .../credentials/VerifiablePresentationTest.kt | 39 ++++++-------- 5 files changed, 45 insertions(+), 76 deletions(-) diff --git a/credentials/src/main/kotlin/web5/sdk/credentials/PresentationExchange.kt b/credentials/src/main/kotlin/web5/sdk/credentials/PresentationExchange.kt index 0954adc5b..801803415 100644 --- a/credentials/src/main/kotlin/web5/sdk/credentials/PresentationExchange.kt +++ b/credentials/src/main/kotlin/web5/sdk/credentials/PresentationExchange.kt @@ -5,14 +5,13 @@ import com.fasterxml.jackson.databind.node.ObjectNode import com.networknt.schema.JsonSchema import com.nfeld.jsonpathkt.JsonPath import com.nfeld.jsonpathkt.extension.read -import com.nimbusds.jwt.JWTParser -import com.nimbusds.jwt.SignedJWT import web5.sdk.credentials.model.InputDescriptorMapping import web5.sdk.credentials.model.InputDescriptorV2 import web5.sdk.credentials.model.PresentationDefinitionV2 import web5.sdk.credentials.model.PresentationDefinitionV2Validator import web5.sdk.credentials.model.PresentationSubmission import web5.sdk.credentials.model.PresentationSubmissionValidator +import web5.sdk.dids.jwt.Jwt import java.util.UUID /** @@ -162,9 +161,9 @@ public object PresentationExchange { presentationDefinition: PresentationDefinitionV2 ): Map> { val vcJwtListWithNodes = vcJwtList.zip(vcJwtList.map { vcJwt -> - val vc = JWTParser.parse(vcJwt) as SignedJWT + val vc = Jwt.decode(vcJwt) - JsonPath.parse(vc.payload.toString()) + JsonPath.parse(vc.claims.toString()) ?: throw JsonPathParseException() }) return presentationDefinition.inputDescriptors.associateWith { inputDescriptor -> diff --git a/credentials/src/main/kotlin/web5/sdk/credentials/VerifiableCredential.kt b/credentials/src/main/kotlin/web5/sdk/credentials/VerifiableCredential.kt index 570888bb3..a92dcd661 100644 --- a/credentials/src/main/kotlin/web5/sdk/credentials/VerifiableCredential.kt +++ b/credentials/src/main/kotlin/web5/sdk/credentials/VerifiableCredential.kt @@ -10,8 +10,6 @@ import com.fasterxml.jackson.module.kotlin.KotlinModule import com.fasterxml.jackson.module.kotlin.convertValue import com.nfeld.jsonpathkt.JsonPath import com.nfeld.jsonpathkt.extension.read -import com.nimbusds.jwt.JWTParser -import com.nimbusds.jwt.SignedJWT import web5.sdk.dids.did.BearerDid import web5.sdk.dids.jwt.Jwt import web5.sdk.dids.jwt.JwtClaimsSet @@ -204,11 +202,10 @@ public class VerifiableCredential internal constructor(public val vcDataModel: V * ``` */ public fun parseJwt(vcJwt: String): VerifiableCredential { - val jwt = JWTParser.parse(vcJwt) as SignedJWT - val jwtPayload = jwt.payload.toJSONObject() - val vcDataModelValue = jwtPayload.getOrElse("vc") { + val jwt = Jwt.decode(vcJwt) + val jwtPayload = jwt.claims + val vcDataModelValue = jwtPayload.misc["vc"] ?: throw IllegalArgumentException("jwt payload missing vc property") - } @Suppress("UNCHECKED_CAST") // only partially unchecked. can only safely cast to Map<*, *> val vcDataModelMap = vcDataModelValue as? Map diff --git a/credentials/src/main/kotlin/web5/sdk/credentials/VerifiablePresentation.kt b/credentials/src/main/kotlin/web5/sdk/credentials/VerifiablePresentation.kt index c91012987..97a1a4ab0 100644 --- a/credentials/src/main/kotlin/web5/sdk/credentials/VerifiablePresentation.kt +++ b/credentials/src/main/kotlin/web5/sdk/credentials/VerifiablePresentation.kt @@ -3,8 +3,6 @@ package web5.sdk.credentials import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.KotlinModule -import com.nimbusds.jwt.JWTParser -import com.nimbusds.jwt.SignedJWT import web5.sdk.dids.did.BearerDid import web5.sdk.dids.jwt.Jwt import web5.sdk.dids.jwt.JwtClaimsSet @@ -37,7 +35,7 @@ public class VerifiablePresentation internal constructor(public val vpDataModel: public val verifiableCredential: List get() = vpDataModel.toMap()["verifiableCredential"] as List - public val holder: String + public val holder: String get() = vpDataModel.holder.toString() /** @@ -178,11 +176,11 @@ public class VerifiablePresentation internal constructor(public val vpDataModel: * ``` */ public fun parseJwt(vpJwt: String): VerifiablePresentation { - val jwt = JWTParser.parse(vpJwt) as SignedJWT - val jwtPayload = jwt.payload.toJSONObject() - val vpDataModelValue = jwtPayload.getOrElse("vp") { - throw IllegalArgumentException("jwt payload missing vp property") - } + val jwt = Jwt.decode(vpJwt) + val jwtPayload = jwt.claims + val vpDataModelValue = jwtPayload.misc["vp"] + ?: throw IllegalArgumentException("jwt payload missing vp property") + @Suppress("UNCHECKED_CAST") // only partially unchecked. can only safely cast to Map<*, *> val vpDataModelMap = vpDataModelValue as? Map diff --git a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt index f1e76cb7a..958919b14 100644 --- a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt +++ b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt @@ -2,23 +2,18 @@ package web5.sdk.credentials import com.fasterxml.jackson.core.type.TypeReference import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import com.nimbusds.jose.JWSAlgorithm -import com.nimbusds.jose.JWSHeader -import com.nimbusds.jose.JWSSigner -import com.nimbusds.jose.crypto.Ed25519Signer -import com.nimbusds.jose.jwk.Curve -import com.nimbusds.jose.jwk.gen.OctetKeyPairGenerator -import com.nimbusds.jwt.JWTClaimsSet -import com.nimbusds.jwt.SignedJWT import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertDoesNotThrow +import web5.sdk.common.Convert import web5.sdk.crypto.AlgorithmId import web5.sdk.crypto.AwsKeyManager import web5.sdk.crypto.InMemoryKeyManager -import web5.sdk.dids.ChangemeDid import web5.sdk.dids.didcore.Purpose import web5.sdk.dids.extensions.load +import web5.sdk.dids.jws.JwsHeader +import web5.sdk.dids.jwt.Jwt +import web5.sdk.dids.jwt.JwtClaimsSet import web5.sdk.dids.methods.dht.CreateDidDhtOptions import web5.sdk.dids.methods.dht.DidDht import web5.sdk.dids.methods.key.DidKey @@ -133,11 +128,12 @@ class VerifiableCredentialTest { CreateDidDhtOptions(verificationMethods = verificationMethodsToAdd) ) - val header = JWSHeader.Builder(JWSAlgorithm.ES256K) - .keyID(issuerDid.did.uri) + val header = JwsHeader.Builder() + .algorithm(AlgorithmId.secp256k1.name) + .keyId(issuerDid.did.uri) .build() // A detached payload JWT - val vcJwt = "${header.toBase64URL()}..fakeSig" + val vcJwt = "${Convert(header).toBase64Url()}..fakeSig" val exception = assertThrows(SignatureException::class.java) { VerifiableCredential.verify(vcJwt) @@ -157,22 +153,16 @@ class VerifiableCredentialTest { @Test fun `parseJwt throws if vc property is missing in JWT`() { - val jwk = OctetKeyPairGenerator(Curve.Ed25519).generate() - val signer: JWSSigner = Ed25519Signer(jwk) + val signerDid = DidDht.create(InMemoryKeyManager()) - val claimsSet = JWTClaimsSet.Builder() + val claimsSet = JwtClaimsSet.Builder() .subject("alice") .build() - val signedJWT = SignedJWT( - JWSHeader.Builder(JWSAlgorithm.EdDSA).keyID(jwk.keyID).build(), - claimsSet - ) + val signedJWT = Jwt.sign(signerDid, claimsSet) - signedJWT.sign(signer) - val randomJwt = signedJWT.serialize() val exception = assertThrows(IllegalArgumentException::class.java) { - VerifiableCredential.parseJwt(randomJwt) + VerifiableCredential.parseJwt(signedJWT) } assertEquals("jwt payload missing vc property", exception.message) @@ -180,24 +170,16 @@ class VerifiableCredentialTest { @Test fun `parseJwt throws if vc property in JWT payload is not an object`() { - val jwk = OctetKeyPairGenerator(Curve.Ed25519).generate() - val signer: JWSSigner = Ed25519Signer(jwk) + val signerDid = DidDht.create(InMemoryKeyManager()) - val claimsSet = JWTClaimsSet.Builder() + val claimsSet = JwtClaimsSet.Builder() .subject("alice") - .claim("vc", "hehe troll") + .misc("vc", "hehe troll") .build() - val signedJWT = SignedJWT( - JWSHeader.Builder(JWSAlgorithm.EdDSA).keyID(jwk.keyID).build(), - claimsSet - ) - - signedJWT.sign(signer) - val randomJwt = signedJWT.serialize() - + val signedJWT = Jwt.sign(signerDid, claimsSet) val exception = assertThrows(IllegalArgumentException::class.java) { - VerifiableCredential.parseJwt(randomJwt) + VerifiableCredential.parseJwt(signedJWT) } assertEquals("expected vc property in JWT payload to be an object", exception.message) diff --git a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiablePresentationTest.kt b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiablePresentationTest.kt index 506133959..43f0c69d3 100644 --- a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiablePresentationTest.kt +++ b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiablePresentationTest.kt @@ -1,21 +1,18 @@ package web5.sdk.credentials import com.nimbusds.jose.JWSAlgorithm -import com.nimbusds.jose.JWSHeader -import com.nimbusds.jose.JWSSigner -import com.nimbusds.jose.crypto.Ed25519Signer -import com.nimbusds.jose.jwk.Curve -import com.nimbusds.jose.jwk.gen.OctetKeyPairGenerator -import com.nimbusds.jwt.JWTClaimsSet -import com.nimbusds.jwt.SignedJWT import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertDoesNotThrow +import web5.sdk.common.Convert import web5.sdk.credentials.model.InputDescriptorMapping import web5.sdk.credentials.model.PresentationSubmission import web5.sdk.crypto.AlgorithmId import web5.sdk.crypto.InMemoryKeyManager import web5.sdk.dids.didcore.Purpose +import web5.sdk.dids.jws.JwsHeader +import web5.sdk.dids.jwt.Jwt +import web5.sdk.dids.jwt.JwtClaimsSet import web5.sdk.dids.methods.dht.CreateDidDhtOptions import web5.sdk.dids.methods.dht.DidDht import web5.sdk.dids.methods.key.DidKey @@ -156,11 +153,12 @@ class VerifiablePresentationTest { val keyManager = InMemoryKeyManager() val holderDid = DidKey.create(keyManager) - val header = JWSHeader.Builder(JWSAlgorithm.ES256K) - .keyID(holderDid.did.uri) + val header = JwsHeader.Builder() + .algorithm(AlgorithmId.secp256k1.name) + .keyId(holderDid.did.uri) .build() - val vpJwt = "${header.toBase64URL()}..fakeSig" + val vpJwt = "${Convert(header).toBase64Url()}..fakeSig" val exception = assertThrows(SignatureException::class.java) { VerifiablePresentation.verify(vpJwt) @@ -202,22 +200,16 @@ class VerifiablePresentationTest { @Test fun `parseJwt throws if vp property is missing in JWT`() { - val jwk = OctetKeyPairGenerator(Curve.Ed25519).generate() - val signer: JWSSigner = Ed25519Signer(jwk) + val signerDid = DidDht.create(InMemoryKeyManager()) - val claimsSet = JWTClaimsSet.Builder() + val claimsSet = JwtClaimsSet.Builder() .subject("alice") .build() - val signedJWT = SignedJWT( - JWSHeader.Builder(JWSAlgorithm.EdDSA).keyID(jwk.keyID).build(), - claimsSet - ) + val signedJWT = Jwt.sign(signerDid, claimsSet) - signedJWT.sign(signer) - val randomJwt = signedJWT.serialize() val exception = assertThrows(IllegalArgumentException::class.java) { - VerifiablePresentation.parseJwt(randomJwt) + VerifiablePresentation.parseJwt(signedJWT) } assertEquals("jwt payload missing vp property", exception.message) @@ -240,11 +232,12 @@ class VerifiablePresentationTest { CreateDidDhtOptions(verificationMethods = verificationMethodsToAdd) ) - val header = JWSHeader.Builder(JWSAlgorithm.ES256K) - .keyID(issuerDid.did.uri) + val header = JwsHeader.Builder() + .algorithm(AlgorithmId.secp256k1.name) + .keyId(issuerDid.did.uri) .build() //A detached payload JWT - val vpJwt = "${header.toBase64URL()}..fakeSig" + val vpJwt = "${Convert(header).toBase64Url()}..fakeSig" val exception = assertThrows(SignatureException::class.java) { VerifiablePresentation.verify(vpJwt) From b681b7cb911d7ceb11d8289fa75f8203ee1b6eb4 Mon Sep 17 00:00:00 2001 From: Jiyoon Koo Date: Tue, 12 Mar 2024 13:02:48 -0400 Subject: [PATCH 14/47] adding jwk impl, in progress --- .../src/main/kotlin/web5/sdk/common/Json.kt | 8 +++- dids/src/main/kotlin/web5/sdk/dids/jwk/Jwk.kt | 43 +++++++++++++++++++ dids/src/main/kotlin/web5/sdk/dids/jws/Jws.kt | 10 ++--- dids/src/main/kotlin/web5/sdk/dids/jwt/Jwt.kt | 2 +- 4 files changed, 56 insertions(+), 7 deletions(-) create mode 100644 dids/src/main/kotlin/web5/sdk/dids/jwk/Jwk.kt diff --git a/common/src/main/kotlin/web5/sdk/common/Json.kt b/common/src/main/kotlin/web5/sdk/common/Json.kt index 60b4e0e3d..ba6d3ebc0 100644 --- a/common/src/main/kotlin/web5/sdk/common/Json.kt +++ b/common/src/main/kotlin/web5/sdk/common/Json.kt @@ -2,6 +2,7 @@ package web5.sdk.common import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.ObjectReader import com.fasterxml.jackson.databind.ObjectWriter import com.fasterxml.jackson.databind.SerializationFeature import com.fasterxml.jackson.module.kotlin.registerKotlinModule @@ -33,6 +34,7 @@ public object Json { .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) private val objectWriter: ObjectWriter = jsonMapper.writer() + public val objectReader: ObjectReader = jsonMapper.reader() /** * Converts a kotlin object to a json string. @@ -43,4 +45,8 @@ public object Json { public fun stringify(obj: Any): String { return objectWriter.writeValueAsString(obj) } -} \ No newline at end of file + + public inline fun parse(payload: String): T { + return objectReader.readValue(payload, T::class.java) + } +} diff --git a/dids/src/main/kotlin/web5/sdk/dids/jwk/Jwk.kt b/dids/src/main/kotlin/web5/sdk/dids/jwk/Jwk.kt new file mode 100644 index 000000000..420452695 --- /dev/null +++ b/dids/src/main/kotlin/web5/sdk/dids/jwk/Jwk.kt @@ -0,0 +1,43 @@ +package web5.sdk.dids.jwk + +import web5.sdk.common.Convert +import web5.sdk.common.Json +import java.security.MessageDigest + +public class Jwk( + public val kty: String, + public val use: String?, + public val alg: String?, + kid: String?, + public val crv: String?, + public val d: String? = null, + public val x: String?, + public val y: String?, + kidFromThumbprint: Boolean = true +) { + + public val kid: String? = kid ?: if (kidFromThumbprint) computeThumbprint() else null + + private fun computeThumbprint(): String { + val thumbprintPayload = Json.jsonMapper.createObjectNode().apply { + put("crv", crv) + put("kty", kty) + put("x", x) + put("y", y) + } + + // todo this is what chad told me to do, not sure 100% correct + val thumbprintPayloadString = Json.stringify(thumbprintPayload) + val thumbprintPayloadBytes = Convert(thumbprintPayloadString).toByteArray() + + // todo what does this do? supposed to be the equivalent of + // final thumbprintPayloadDigest = _dartSha256.hashSync(thumbprintPayloadBytes); + val messageDigest = MessageDigest.getInstance("SHA-256") + val thumbprintPayloadDigest = messageDigest.digest(thumbprintPayloadBytes) + + return Convert(thumbprintPayloadDigest).toBase64Url() + + } + + +} diff --git a/dids/src/main/kotlin/web5/sdk/dids/jws/Jws.kt b/dids/src/main/kotlin/web5/sdk/dids/jws/Jws.kt index 05a5538b7..a2b76098c 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/jws/Jws.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/jws/Jws.kt @@ -132,16 +132,16 @@ public class JwsHeader( // todo do i need these toJson and fromJson? // i could just call Json.jsonMapper.xyz() public fun toJson(header: JwsHeader): String { - return Json.jsonMapper.writeValueAsString(header) + return Json.stringify(header) } - public fun fromJson(jsonHeader: String): JwsHeader? { - return Json.jsonMapper.readValue(jsonHeader, JwsHeader::class.java) + public fun fromJson(jsonHeader: String): JwsHeader { + return Json.parse(jsonHeader) } public fun fromBase64Url(base64EncodedHeader: String): JwsHeader { - val jsonHeaderDecoded = Convert(base64EncodedHeader).toByteArray() - return Json.jsonMapper.readValue(jsonHeaderDecoded, JwsHeader::class.java) + val jsonHeaderDecoded = Convert(base64EncodedHeader, EncodingFormat.Base64Url).toStr() + return Json.parse(jsonHeaderDecoded) } public fun toBase64Url(header: JwsHeader): String { diff --git a/dids/src/main/kotlin/web5/sdk/dids/jwt/Jwt.kt b/dids/src/main/kotlin/web5/sdk/dids/jwt/Jwt.kt index a3a72ffa5..b46680238 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/jwt/Jwt.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/jwt/Jwt.kt @@ -36,7 +36,7 @@ public object Jwt { public fun sign(did: BearerDid, payload: JwtClaimsSet): String { val header = JwtHeader(typ = "JWT") - val payloadBytes = Convert(Json.jsonMapper.writeValueAsString(payload)).toByteArray() + val payloadBytes = Convert(Json.stringify(payload)).toByteArray() return Jws.sign(did, payloadBytes, header) } From 33852b97b42a52707aa46b2c8e02ca37e64c7318 Mon Sep 17 00:00:00 2001 From: Jiyoon Koo Date: Wed, 13 Mar 2024 23:21:07 -0400 Subject: [PATCH 15/47] adding jose package. fixing some todos --- .../src/main/kotlin/web5/sdk/common/Json.kt | 5 + credentials/build.gradle.kts | 2 + .../sdk/credentials/PresentationExchange.kt | 2 +- .../sdk/credentials/VerifiableCredential.kt | 4 +- .../sdk/credentials/VerifiablePresentation.kt | 4 +- .../credentials/VerifiableCredentialTest.kt | 9 +- .../credentials/VerifiablePresentationTest.kt | 12 +-- crypto/build.gradle.kts | 2 + .../kotlin/web5/sdk/crypto/AwsKeyManager.kt | 59 ++++++++--- .../src/main/kotlin/web5/sdk/crypto/Crypto.kt | 45 +++++---- crypto/src/main/kotlin/web5/sdk/crypto/Dsa.kt | 18 +--- .../main/kotlin/web5/sdk/crypto/Ed25519.kt | 99 +++++++++++-------- .../web5/sdk/crypto/InMemoryKeyManager.kt | 37 ++++--- .../kotlin/web5/sdk/crypto/KeyGenerator.kt | 21 ++-- .../main/kotlin/web5/sdk/crypto/KeyManager.kt | 14 +-- .../main/kotlin/web5/sdk/crypto/Secp256k1.kt | 79 ++++++++++----- .../src/main/kotlin/web5/sdk/crypto/Signer.kt | 14 +-- .../main/kotlin/web5/sdk/crypto/jwk/Jwk.kt | 96 ++++++++++++++++++ .../web5/sdk/crypto/AwsKeyManagerTest.kt | 11 +-- .../kotlin/web5/sdk/crypto/Ed25519Test.kt | 11 ++- .../kotlin/web5/sdk/crypto/Secp256k1Test.kt | 33 +++---- .../kotlin/web5/sdk/dids/Serialization.kt | 18 ++-- .../kotlin/web5/sdk/dids/did/BearerDid.kt | 6 +- .../kotlin/web5/sdk/dids/did/PortableDid.kt | 4 +- .../web5/sdk/dids/didcore/DidDocument.kt | 7 ++ .../sdk/dids/didcore/VerificationMethod.kt | 12 +-- dids/src/main/kotlin/web5/sdk/dids/jwk/Jwk.kt | 43 -------- .../web5/sdk/dids/methods/dht/DhtClient.kt | 4 +- .../web5/sdk/dids/methods/dht/DidDht.kt | 42 +++++--- .../web5/sdk/dids/methods/jwk/DidJwk.kt | 21 ++-- .../web5/sdk/dids/methods/dht/DidDhtTest.kt | 24 ++--- .../web5/sdk/dids/methods/jwk/DidJwkTest.kt | 11 +-- .../web5/sdk/dids/methods/key/DidKeyTest.kt | 17 ++-- .../web5/sdk/dids/methods/util/TestUtils.kt | 7 +- jose/build.gradle.kts | 20 ++++ .../src/main/kotlin/web5/sdk/jose}/jws/Jws.kt | 45 +++------ .../src/main/kotlin/web5/sdk/jose}/jwt/Jwt.kt | 13 ++- settings.gradle.kts | 4 + 38 files changed, 507 insertions(+), 368 deletions(-) create mode 100644 crypto/src/main/kotlin/web5/sdk/crypto/jwk/Jwk.kt delete mode 100644 dids/src/main/kotlin/web5/sdk/dids/jwk/Jwk.kt create mode 100644 jose/build.gradle.kts rename {dids/src/main/kotlin/web5/sdk/dids => jose/src/main/kotlin/web5/sdk/jose}/jws/Jws.kt (78%) rename {dids/src/main/kotlin/web5/sdk/dids => jose/src/main/kotlin/web5/sdk/jose}/jwt/Jwt.kt (93%) diff --git a/common/src/main/kotlin/web5/sdk/common/Json.kt b/common/src/main/kotlin/web5/sdk/common/Json.kt index ba6d3ebc0..067b56d7f 100644 --- a/common/src/main/kotlin/web5/sdk/common/Json.kt +++ b/common/src/main/kotlin/web5/sdk/common/Json.kt @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectReader import com.fasterxml.jackson.databind.ObjectWriter import com.fasterxml.jackson.databind.SerializationFeature +import com.fasterxml.jackson.module.kotlin.readValue import com.fasterxml.jackson.module.kotlin.registerKotlinModule /** @@ -49,4 +50,8 @@ public object Json { public inline fun parse(payload: String): T { return objectReader.readValue(payload, T::class.java) } + + public fun String.toMap(): Map { + return jsonMapper.readValue(this) + } } diff --git a/credentials/build.gradle.kts b/credentials/build.gradle.kts index b7671ea46..877383c4a 100644 --- a/credentials/build.gradle.kts +++ b/credentials/build.gradle.kts @@ -38,6 +38,8 @@ dependencies { implementation(project(":dids")) implementation(project(":common")) implementation(project(":crypto")) + implementation(project(":jose")) + // Implementation implementation(libs.comFasterXmlJacksonModuleKotlin) diff --git a/credentials/src/main/kotlin/web5/sdk/credentials/PresentationExchange.kt b/credentials/src/main/kotlin/web5/sdk/credentials/PresentationExchange.kt index 801803415..c66e1b167 100644 --- a/credentials/src/main/kotlin/web5/sdk/credentials/PresentationExchange.kt +++ b/credentials/src/main/kotlin/web5/sdk/credentials/PresentationExchange.kt @@ -11,7 +11,7 @@ import web5.sdk.credentials.model.PresentationDefinitionV2 import web5.sdk.credentials.model.PresentationDefinitionV2Validator import web5.sdk.credentials.model.PresentationSubmission import web5.sdk.credentials.model.PresentationSubmissionValidator -import web5.sdk.dids.jwt.Jwt +import web5.sdk.jose.jwt.Jwt import java.util.UUID /** diff --git a/credentials/src/main/kotlin/web5/sdk/credentials/VerifiableCredential.kt b/credentials/src/main/kotlin/web5/sdk/credentials/VerifiableCredential.kt index a92dcd661..d584f590f 100644 --- a/credentials/src/main/kotlin/web5/sdk/credentials/VerifiableCredential.kt +++ b/credentials/src/main/kotlin/web5/sdk/credentials/VerifiableCredential.kt @@ -11,8 +11,8 @@ import com.fasterxml.jackson.module.kotlin.convertValue import com.nfeld.jsonpathkt.JsonPath import com.nfeld.jsonpathkt.extension.read import web5.sdk.dids.did.BearerDid -import web5.sdk.dids.jwt.Jwt -import web5.sdk.dids.jwt.JwtClaimsSet +import web5.sdk.jose.jwt.Jwt +import web5.sdk.jose.jwt.JwtClaimsSet import java.net.URI import java.security.SignatureException import java.util.Date diff --git a/credentials/src/main/kotlin/web5/sdk/credentials/VerifiablePresentation.kt b/credentials/src/main/kotlin/web5/sdk/credentials/VerifiablePresentation.kt index 97a1a4ab0..233d25cb6 100644 --- a/credentials/src/main/kotlin/web5/sdk/credentials/VerifiablePresentation.kt +++ b/credentials/src/main/kotlin/web5/sdk/credentials/VerifiablePresentation.kt @@ -4,8 +4,8 @@ import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.KotlinModule import web5.sdk.dids.did.BearerDid -import web5.sdk.dids.jwt.Jwt -import web5.sdk.dids.jwt.JwtClaimsSet +import web5.sdk.jose.jwt.Jwt +import web5.sdk.jose.jwt.JwtClaimsSet import java.net.URI import java.security.SignatureException import java.util.Date diff --git a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt index 958919b14..63997b054 100644 --- a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt +++ b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt @@ -9,11 +9,12 @@ import web5.sdk.common.Convert import web5.sdk.crypto.AlgorithmId import web5.sdk.crypto.AwsKeyManager import web5.sdk.crypto.InMemoryKeyManager +import web5.sdk.crypto.Jwa import web5.sdk.dids.didcore.Purpose import web5.sdk.dids.extensions.load -import web5.sdk.dids.jws.JwsHeader -import web5.sdk.dids.jwt.Jwt -import web5.sdk.dids.jwt.JwtClaimsSet +import web5.sdk.jose.jws.JwsHeader +import web5.sdk.jose.jwt.Jwt +import web5.sdk.jose.jwt.JwtClaimsSet import web5.sdk.dids.methods.dht.CreateDidDhtOptions import web5.sdk.dids.methods.dht.DidDht import web5.sdk.dids.methods.key.DidKey @@ -129,7 +130,7 @@ class VerifiableCredentialTest { ) val header = JwsHeader.Builder() - .algorithm(AlgorithmId.secp256k1.name) + .algorithm(Jwa.ES256K.name) .keyId(issuerDid.did.uri) .build() // A detached payload JWT diff --git a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiablePresentationTest.kt b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiablePresentationTest.kt index 43f0c69d3..029ffe10a 100644 --- a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiablePresentationTest.kt +++ b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiablePresentationTest.kt @@ -1,6 +1,5 @@ package web5.sdk.credentials -import com.nimbusds.jose.JWSAlgorithm import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertDoesNotThrow @@ -9,13 +8,14 @@ import web5.sdk.credentials.model.InputDescriptorMapping import web5.sdk.credentials.model.PresentationSubmission import web5.sdk.crypto.AlgorithmId import web5.sdk.crypto.InMemoryKeyManager +import web5.sdk.crypto.Jwa import web5.sdk.dids.didcore.Purpose -import web5.sdk.dids.jws.JwsHeader -import web5.sdk.dids.jwt.Jwt -import web5.sdk.dids.jwt.JwtClaimsSet import web5.sdk.dids.methods.dht.CreateDidDhtOptions import web5.sdk.dids.methods.dht.DidDht import web5.sdk.dids.methods.key.DidKey +import web5.sdk.jose.jws.JwsHeader +import web5.sdk.jose.jwt.Jwt +import web5.sdk.jose.jwt.JwtClaimsSet import java.security.SignatureException import java.text.ParseException import kotlin.test.assertEquals @@ -154,7 +154,7 @@ class VerifiablePresentationTest { val holderDid = DidKey.create(keyManager) val header = JwsHeader.Builder() - .algorithm(AlgorithmId.secp256k1.name) + .algorithm(Jwa.ES256K.name) .keyId(holderDid.did.uri) .build() @@ -233,7 +233,7 @@ class VerifiablePresentationTest { ) val header = JwsHeader.Builder() - .algorithm(AlgorithmId.secp256k1.name) + .algorithm(Jwa.ES256K.name) .keyId(issuerDid.did.uri) .build() //A detached payload JWT diff --git a/crypto/build.gradle.kts b/crypto/build.gradle.kts index 303871d87..8add134fd 100644 --- a/crypto/build.gradle.kts +++ b/crypto/build.gradle.kts @@ -32,6 +32,8 @@ dependencies { // Project implementation(project(":common")) + implementation(project(":jose")) + // Implementation implementation(libs.comGoogleCryptoTink) diff --git a/crypto/src/main/kotlin/web5/sdk/crypto/AwsKeyManager.kt b/crypto/src/main/kotlin/web5/sdk/crypto/AwsKeyManager.kt index 1451db28c..42085abbd 100644 --- a/crypto/src/main/kotlin/web5/sdk/crypto/AwsKeyManager.kt +++ b/crypto/src/main/kotlin/web5/sdk/crypto/AwsKeyManager.kt @@ -14,13 +14,13 @@ import com.amazonaws.services.kms.model.SignRequest import com.amazonaws.services.kms.model.SigningAlgorithmSpec import com.nimbusds.jose.JWSAlgorithm import com.nimbusds.jose.crypto.impl.ECDSA -import com.nimbusds.jose.jwk.ECKey -import com.nimbusds.jose.jwk.JWK -import com.nimbusds.jose.jwk.KeyUse import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo import org.bouncycastle.crypto.ExtendedDigest import org.bouncycastle.crypto.digests.SHA256Digest import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter +import web5.sdk.common.Convert +import web5.sdk.common.EncodingFormat +import web5.sdk.crypto.jwk.Jwk import java.nio.ByteBuffer import java.security.PublicKey import java.security.interfaces.ECPublicKey @@ -30,7 +30,7 @@ import java.security.interfaces.ECPublicKey * connection details for [AWSKMS] client as per * [Configure the AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html) * - * Key aliases are generated from the key's JWK thumbprint, and stored in AWS KMS. + * Key aliases are generated from the key's Jwk thumbprint, and stored in AWS KMS. * e.g. alias/6uNnyj7xZUgtKTEOFV2mz0f7Hd3cxIH1o5VXsOo4u1M * * AWSKeyManager supports a limited set ECDSA curves for signing: @@ -118,26 +118,58 @@ public class AwsKeyManager @JvmOverloads constructor( * Retrieves the public key associated with a previously stored private key, identified by the provided alias. * * @param keyAlias The alias referencing the stored private key. - * @return The associated public key in JWK (JSON Web Key) format. + * @return The associated public key in Jwk (JSON Web Key) format. * @throws [AWSKMSException] for any error originating from the [AWSKMS] client */ - override fun getPublicKey(keyAlias: String): JWK { + override fun getPublicKey(keyAlias: String): Jwk { val getPublicKeyRequest = GetPublicKeyRequest().withKeyId(keyAlias) val publicKeyResponse = kmsClient.getPublicKey(getPublicKeyRequest) val publicKey = convertToJavaPublicKey(publicKeyResponse.publicKey) val algorithmDetails = getAlgorithmDetails(publicKeyResponse.keySpec.enum()) val jwkBuilder = when (publicKey) { - is ECPublicKey -> ECKey.Builder(JwaCurve.toJwkCurve(algorithmDetails.curve), publicKey) + is ECPublicKey -> { + val (x, y) = extractBase64UrlXYFromECPublicKey(publicKey) + Jwk.Builder() + .keyType("EC") + .x(x) + .y(y) + } + else -> throw IllegalArgumentException("Unknown key type $publicKey") } return jwkBuilder - .algorithm(Jwa.toJwsAlgorithm(algorithmDetails.algorithm)) - .keyID(keyAlias) - .keyUse(KeyUse.SIGNATURE) + .algorithm(algorithmDetails.algorithm.name) + .keyId(keyAlias) + .keyUse("sig") .build() } + private fun extractBase64UrlXYFromECPublicKey(ecPublicKey: ECPublicKey): Pair { + val ecPoint = ecPublicKey.w + val x = ecPoint.affineX.toByteArray().trimLeadingZeroes() + val y = ecPoint.affineY.toByteArray().trimLeadingZeroes() + + val base64UrlX = Convert(x, EncodingFormat.Base64Url).toStr() + val base64UrlY = Convert(y, EncodingFormat.Base64Url).toStr() + + return Pair(base64UrlX, base64UrlY) + } + + /** + * // todo not sure where this should live + * Extension function to remove leading zero bytes which might be + * added by BigInteger's toByteArray() method to represent positive numbers. + */ + private fun ByteArray.trimLeadingZeroes(): ByteArray = + if (this.isEmpty()) { + this + } else if (this[0] == 0.toByte()) { + this.dropWhile { it == 0.toByte() }.toByteArray() + } else { + this + } + /** * Signs the provided payload using the private key identified by the provided alias. * @@ -165,11 +197,11 @@ public class AwsKeyManager @JvmOverloads constructor( /** * Return the alias of [publicKey], as was originally returned by [generatePrivateKey]. * - * @param publicKey A public key in JWK (JSON Web Key) format + * @param publicKey A public key in Jwk (JSON Web Key) format * @return The alias belonging to [publicKey] */ - override fun getDeterministicAlias(publicKey: JWK): String { - val jwkThumbprint = publicKey.computeThumbprint() + override fun getDeterministicAlias(publicKey: Jwk): String { + val jwkThumbprint = publicKey.kid ?: publicKey.computeThumbprint() return "alias/$jwkThumbprint" } @@ -211,6 +243,7 @@ public class AwsKeyManager @JvmOverloads constructor( /** * KMS returns the signature encoded as ASN.1 DER. Convert to the "R+S" concatenation format required by JWS. * https://www.rfc-editor.org/rfc/rfc7515#appendix-A.3.1 + * // todo how to eject from JWSAlgorithm here? need to write ECDSA.getSignatureByteArrayLength and transcodeSignatureToConcat? */ private fun transcodeDerSignatureToConcat(derSignature: ByteArray, algorithm: JWSAlgorithm): ByteArray { val signatureLength = ECDSA.getSignatureByteArrayLength(algorithm) diff --git a/crypto/src/main/kotlin/web5/sdk/crypto/Crypto.kt b/crypto/src/main/kotlin/web5/sdk/crypto/Crypto.kt index 52bc833b6..5d8e779ad 100644 --- a/crypto/src/main/kotlin/web5/sdk/crypto/Crypto.kt +++ b/crypto/src/main/kotlin/web5/sdk/crypto/Crypto.kt @@ -4,6 +4,7 @@ import com.nimbusds.jose.jwk.JWK import web5.sdk.crypto.Crypto.generatePrivateKey import web5.sdk.crypto.Crypto.publicKeyToBytes import web5.sdk.crypto.Crypto.sign +import web5.sdk.crypto.jwk.Jwk /** * Cryptography utility object providing key generation, signature creation, and other crypto-related functionalities. @@ -13,7 +14,7 @@ import web5.sdk.crypto.Crypto.sign * It offers convenience methods to: * - Generate private keys ([generatePrivateKey]) * - Create digital signatures ([sign]) - * - conversion from JWK <-> bytes ([publicKeyToBytes]) + * - conversion from Jwk <-> bytes ([publicKeyToBytes]) * - Get relevant key generators and signers based on algorithmId. * * Internally, it utilizes predefined mappings to pair algorithms and curve types with their respective [KeyGenerator] @@ -23,7 +24,7 @@ import web5.sdk.crypto.Crypto.sign * * ### Example Usage: * ``` - * val privateKey: JWK = Crypto.generatePrivateKey(JWSAlgorithm.EdDSA, Curve.Ed25519) + * val privateKey: Jwk = Crypto.generatePrivateKey(JWSAlgorithm.EdDSA, Curve.Ed25519) * ``` * * ### Key Points: @@ -61,11 +62,11 @@ public object Crypto { * * @param algorithmId The algorithmId [AlgorithmId]. * @param options Options for key generation, may include specific parameters relevant to the algorithm. - * @return The generated private key as a JWK object. + * @return The generated private key as a Jwk object. * @throws IllegalArgumentException if the provided algorithm or curve is not supported. */ @JvmOverloads - public fun generatePrivateKey(algorithmId: AlgorithmId, options: KeyGenOptions? = null): JWK { + public fun generatePrivateKey(algorithmId: AlgorithmId, options: KeyGenOptions? = null): Jwk { val keyGenerator = getKeyGenerator(algorithmId) return keyGenerator.generatePrivateKey(options) } @@ -74,11 +75,11 @@ public object Crypto { * Computes a public key from the given private key, utilizing relevant [KeyGenerator]. * * @param privateKey The private key used to compute the public key. - * @return The computed public key as a JWK object. + * @return The computed public key as a Jwk object. */ - public fun computePublicKey(privateKey: JWK): JWK { - val rawCurve = privateKey.toJSONObject()["crv"] - val curve = rawCurve?.let { JwaCurve.parse(it.toString()) } + public fun computePublicKey(privateKey: Jwk): Jwk { + val rawCurve = privateKey.crv + val curve = rawCurve?.let { JwaCurve.parse(it) } val generator = getKeyGenerator(AlgorithmId.from(curve)) return generator.computePublicKey(privateKey) @@ -90,17 +91,16 @@ public object Crypto { * This function utilizes the appropriate [Signer] to generate a digital signature * of the provided payload using the provided private key. * - * @param privateKey The JWK private key to be used for generating the signature. + * @param privateKey The Jwk private key to be used for generating the signature. * @param payload The byte array data to be signed. * @param options Options for the signing operation, may include specific parameters relevant to the algorithm. * @return The digital signature as a byte array. */ @JvmOverloads - public fun sign(privateKey: JWK, payload: ByteArray, options: SignOptions? = null): ByteArray { - val rawCurve = privateKey.toJSONObject()["crv"] - val jwaCurve = rawCurve?.let { JwaCurve.parse(it.toString()) } + public fun sign(privateKey: Jwk, payload: ByteArray, options: SignOptions? = null): ByteArray { + val curve = getJwkCurve(privateKey) - val signer = getSigner(AlgorithmId.from(jwaCurve)) + val signer = getSigner(AlgorithmId.from(curve)) return signer.sign(privateKey, payload, options) } @@ -111,22 +111,22 @@ public object Crypto { * This function utilizes the relevant verifier, determined by the algorithm and curve * used in the JWK, to ensure the provided signature is valid for the signed payload * using the provided public key. The algorithm used can either be specified in the - * public key JWK or passed explicitly as a parameter. If it is not found in either, + * public key Jwk or passed explicitly as a parameter. If it is not found in either, * an exception will be thrown. * * ## Note * Algorithm **MUST** either be present on the [JWK] or be provided explicitly * - * @param publicKey The JWK public key to be used for verifying the signature. + * @param publicKey The Jwk public key to be used for verifying the signature. * @param signedPayload The byte array data that was signed. * @param signature The signature that will be verified. * if not provided in the JWK. Default is null. * - * @throws IllegalArgumentException if neither the JWK nor the explicit algorithm parameter + * @throws IllegalArgumentException if neither the Jwk nor the explicit algorithm parameter * provides an algorithm. * */ - public fun verify(publicKey: JWK, signedPayload: ByteArray, signature: ByteArray) { + public fun verify(publicKey: Jwk, signedPayload: ByteArray, signature: ByteArray) { val curve = getJwkCurve(publicKey) val verifier = getVerifier(curve) @@ -153,7 +153,7 @@ public object Crypto { * ### Throws * - [IllegalArgumentException] If the algorithm or curve in [JWK] is not supported or invalid. */ - public fun publicKeyToBytes(publicKey: JWK): ByteArray { + public fun publicKeyToBytes(publicKey: Jwk): ByteArray { val curve = getJwkCurve(publicKey) val generator = getKeyGenerator(AlgorithmId.from(curve)) @@ -230,14 +230,13 @@ public object Crypto { * This function parses and returns the curve type used in a JWK. * May return `null` if the curve information is not present or unsupported. * - * @param jwk The JWK object from which to extract curve information. + * @param jwk The Jwk object from which to extract curve information. * @return The [JwaCurve] used in the JWK, or `null` if the curve is not defined or recognized. */ - public fun getJwkCurve(jwk: JWK): JwaCurve? { - val rawCurve = jwk.toJSONObject()["crv"] + public fun getJwkCurve(jwk: Jwk): JwaCurve? { + val rawCurve = jwk.crv - // todo since crv is required in JWK, shouldn't we throw an error if rawCurve is null? - return rawCurve?.let { JwaCurve.parse(it.toString()) } + return rawCurve?.let { JwaCurve.parse(it) } } /** diff --git a/crypto/src/main/kotlin/web5/sdk/crypto/Dsa.kt b/crypto/src/main/kotlin/web5/sdk/crypto/Dsa.kt index 05cc7013b..bbe56d96c 100644 --- a/crypto/src/main/kotlin/web5/sdk/crypto/Dsa.kt +++ b/crypto/src/main/kotlin/web5/sdk/crypto/Dsa.kt @@ -30,20 +30,6 @@ public enum class JwaCurve { null } } - - /** - * Convert JwaCurve nimbusds JWK curve. - * Used to temporarily bridge the gap between moving from nimbusds JWK methods - * to rolling our own JWK methods - * @param curve - * @return nimbus JWK Curve - */ - public fun toJwkCurve(curve: JwaCurve): Curve { - return when (curve) { - secp256k1 -> Curve.SECP256K1 - Ed25519 -> Curve.Ed25519 - } - } } } @@ -76,8 +62,8 @@ public enum class Jwa { /** * Convert Jwa to nimbusds JWSAlgorithm. - * Used to temporarily bridge the gap between moving from nimbusds JWK methods - * to rolling our own JWK methods + * Used to temporarily bridge the gap between moving from nimbusds Jwk methods + * to rolling our own Jwk methods * * @param algorithm Jwa * @return JWSAlgorithm nimbusds JWSAlgorithm diff --git a/crypto/src/main/kotlin/web5/sdk/crypto/Ed25519.kt b/crypto/src/main/kotlin/web5/sdk/crypto/Ed25519.kt index 6b35d66d0..91a0d72f9 100644 --- a/crypto/src/main/kotlin/web5/sdk/crypto/Ed25519.kt +++ b/crypto/src/main/kotlin/web5/sdk/crypto/Ed25519.kt @@ -3,16 +3,17 @@ package web5.sdk.crypto import com.google.crypto.tink.subtle.Ed25519Sign import com.google.crypto.tink.subtle.Ed25519Verify import com.nimbusds.jose.JWSAlgorithm -import com.nimbusds.jose.jwk.JWK +import com.nimbusds.jose.jwk.Curve import com.nimbusds.jose.jwk.KeyUse import com.nimbusds.jose.jwk.OctetKeyPair import com.nimbusds.jose.jwk.gen.OctetKeyPairGenerator -import com.nimbusds.jose.util.Base64URL import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters import web5.sdk.common.Convert +import web5.sdk.common.EncodingFormat import web5.sdk.crypto.Ed25519.PRIV_MULTICODEC import web5.sdk.crypto.Ed25519.PUB_MULTICODEC import web5.sdk.crypto.Ed25519.algorithm +import web5.sdk.crypto.jwk.Jwk import java.security.GeneralSecurityException import java.security.SignatureException @@ -43,67 +44,81 @@ public object Ed25519 : KeyGenerator, Signer { * Generates a private key utilizing the Ed25519 algorithm. * * @param options (Optional) Additional options to control the key generation process. - * @return The generated private key in JWK format. + * @return The generated private key in Jwk format. */ - override fun generatePrivateKey(options: KeyGenOptions?): JWK { - return OctetKeyPairGenerator(com.nimbusds.jose.jwk.Curve.Ed25519) + override fun generatePrivateKey(options: KeyGenOptions?): Jwk { + // todo use tink to generate private key? + val privateKey = OctetKeyPairGenerator(Curve.Ed25519) .algorithm(JWSAlgorithm.EdDSA) .keyIDFromThumbprint(true) .keyUse(KeyUse.SIGNATURE) .generate() - .toOctetKeyPair() + + return Jwk.Builder() + .algorithm(algorithm.name) + .privateKey(privateKey.d.toString()) + .x(privateKey.x.toString()) + .keyUse("sig") + .build() + } /** * Derives the public key corresponding to a given private key. * - * @param privateKey The private key in JWK format. - * @return The corresponding public key in JWK format. + * @param privateKey The private key in Jwk format. + * @return The corresponding public key in Jwk format. */ - override fun computePublicKey(privateKey: JWK): JWK { - require(privateKey is OctetKeyPair) { "private key must be an Octet Key Pair (kty: OKP)" } + override fun computePublicKey(privateKey: Jwk): Jwk { + require(privateKey.kty == "OKP") { "private key must be an Octet Key Pair (kty: OKP)" } - return privateKey.toOctetKeyPair().toPublicJWK() + return Jwk.Builder() + .keyType(privateKey.kty) + .algorithm(algorithm.name) + .x(privateKey.x.toString()) + .build() } - override fun privateKeyToBytes(privateKey: JWK): ByteArray { + override fun privateKeyToBytes(privateKey: Jwk): ByteArray { validatePrivateKey(privateKey) - return privateKey.toOctetKeyPair().decodedD + return Convert(privateKey.d, EncodingFormat.Base64Url).toByteArray() } - override fun publicKeyToBytes(publicKey: JWK): ByteArray { + override fun publicKeyToBytes(publicKey: Jwk): ByteArray { validatePublicKey(publicKey) - return publicKey.toOctetKeyPair().decodedX + return Convert(publicKey.x, EncodingFormat.Base64Url).toByteArray() } - override fun bytesToPrivateKey(privateKeyBytes: ByteArray): JWK { + override fun bytesToPrivateKey(privateKeyBytes: ByteArray): Jwk { val privateKeyParameters = Ed25519PrivateKeyParameters(privateKeyBytes, 0) val publicKeyBytes = privateKeyParameters.generatePublicKey().encoded val base64UrlEncodedPrivateKey = Convert(privateKeyBytes).toBase64Url() val base64UrlEncodedPublicKey = Convert(publicKeyBytes).toBase64Url() - return OctetKeyPair.Builder(com.nimbusds.jose.jwk.Curve.Ed25519, Base64URL(base64UrlEncodedPublicKey)) - .algorithm(Jwa.toJwsAlgorithm(algorithm)) - .keyIDFromThumbprint() - .d(Base64URL(base64UrlEncodedPrivateKey)) - .keyUse(KeyUse.SIGNATURE) + return Jwk.Builder() + .keyType("OKP") + .algorithm(algorithm.name) + .privateKey(base64UrlEncodedPrivateKey) + .x(base64UrlEncodedPublicKey) + .keyUse("sig") .build() } - override fun bytesToPublicKey(publicKeyBytes: ByteArray): JWK { + override fun bytesToPublicKey(publicKeyBytes: ByteArray): Jwk { val base64UrlEncodedPublicKey = Convert(publicKeyBytes).toBase64Url() - return OctetKeyPair.Builder(com.nimbusds.jose.jwk.Curve.Ed25519, Base64URL(base64UrlEncodedPublicKey)) - .algorithm(Jwa.toJwsAlgorithm(algorithm)) - .keyIDFromThumbprint() - .keyUse(KeyUse.SIGNATURE) + return Jwk.Builder() + .keyType("OKP") + .algorithm(algorithm.name) + .x(base64UrlEncodedPublicKey) + .keyUse("sig") .build() } - override fun sign(privateKey: JWK, payload: ByteArray, options: SignOptions?): ByteArray { + override fun sign(privateKey: Jwk, payload: ByteArray, options: SignOptions?): ByteArray { validatePrivateKey(privateKey) val privateKeyBytes = privateKeyToBytes(privateKey) @@ -112,7 +127,7 @@ public object Ed25519 : KeyGenerator, Signer { return signer.sign(payload) } - override fun verify(publicKey: JWK, signedPayload: ByteArray, signature: ByteArray, options: VerifyOptions?) { + override fun verify(publicKey: Jwk, signedPayload: ByteArray, signature: ByteArray, options: VerifyOptions?) { validatePublicKey(publicKey) val publicKeyBytes = publicKeyToBytes(publicKey) @@ -126,7 +141,7 @@ public object Ed25519 : KeyGenerator, Signer { } /** - * Validates the provided [JWK] (JSON Web Key) is a public key + * Validates the provided [Jwk] (JSON Web Key) is a public key * * This function checks the following: * - The key must be a public key @@ -135,16 +150,16 @@ public object Ed25519 : KeyGenerator, Signer { * If any of these checks fail, this function throws an [IllegalArgumentException] with * a descriptive error message. * - * @param key The [JWK] to validate. + * @param key The [Jwk] to validate. * @throws IllegalArgumentException if the key is not a public key */ - public fun validatePublicKey(key: JWK) { - require(!key.isPrivate) { "key must be public" } + public fun validatePublicKey(key: Jwk) { + require(key.d == null) { "key must be public" } validateKey(key) } /** - * Validates the provided [JWK] (JSON Web Key) to ensure it conforms to the expected key type and format. + * Validates the provided [Jwk] (JSON Web Key) to ensure it conforms to the expected key type and format. * * This function checks the following: * - The key must be a private key @@ -153,16 +168,16 @@ public object Ed25519 : KeyGenerator, Signer { * If any of these checks fail, this function throws an [IllegalArgumentException] with * a descriptive error message. * - * @param key The [JWK] to validate. + * @param key The [Jwk] to validate. * @throws IllegalArgumentException if the key is not a private key */ - public fun validatePrivateKey(key: JWK) { - require(key.isPrivate) { "key must be private" } + public fun validatePrivateKey(key: Jwk) { + require(key.d != null) { "key must be private" } validateKey(key) } /** - * Validates the provided [JWK] (JSON Web Key) to ensure it conforms to the expected key type and format. + * Validates the provided [Jwk] (JSON Web Key) to ensure it conforms to the expected key type and format. * * This function checks the following: * - The key must be an instance of [OctetKeyPair]. @@ -172,7 +187,7 @@ public object Ed25519 : KeyGenerator, Signer { * * ### Usage Example: * ``` - * val jwk: JWK = //...obtain or generate a JWK + * val jwk: Jwk = //...obtain or generate a Jwk * try { * Ed25519.validateKey(jwk) * // Key is valid, proceed with further operations... @@ -182,13 +197,13 @@ public object Ed25519 : KeyGenerator, Signer { * ``` * * ### Important: - * Ensure to call this function before using a [JWK] in cryptographic operations + * Ensure to call this function before using a [Jwk] in cryptographic operations * to safeguard against invalid key usage and potential vulnerabilities. * - * @param key The [JWK] to validate. + * @param key The [Jwk] to validate. * @throws IllegalArgumentException if the key is not of type [OctetKeyPair]. */ - private fun validateKey(key: JWK) { - require(key is OctetKeyPair) { "key must be an Octet Key Pair (kty: OKP)" } + private fun validateKey(key: Jwk) { + require(key.kty == "OKP") { "key must be an Octet Key Pair (kty: OKP)" } } } \ No newline at end of file diff --git a/crypto/src/main/kotlin/web5/sdk/crypto/InMemoryKeyManager.kt b/crypto/src/main/kotlin/web5/sdk/crypto/InMemoryKeyManager.kt index c4f6c5032..1bfdc4757 100644 --- a/crypto/src/main/kotlin/web5/sdk/crypto/InMemoryKeyManager.kt +++ b/crypto/src/main/kotlin/web5/sdk/crypto/InMemoryKeyManager.kt @@ -1,6 +1,9 @@ package web5.sdk.crypto import com.nimbusds.jose.jwk.JWK +import web5.sdk.common.Json +import web5.sdk.common.Json.toMap +import web5.sdk.crypto.jwk.Jwk /** * A class for managing cryptographic keys in-memory. @@ -26,7 +29,7 @@ public class InMemoryKeyManager : KeyManager { /** * An in-memory keystore represented as a flat key-value map, where the key is a key ID. */ - private val keyStore: MutableMap = HashMap() + private val keyStore: MutableMap = HashMap() /** * Generates a private key using specified algorithmId, and stores it in the in-memory keyStore. @@ -37,19 +40,22 @@ public class InMemoryKeyManager : KeyManager { */ override fun generatePrivateKey(algorithmId: AlgorithmId, options: KeyGenOptions?): String { val jwk = Crypto.generatePrivateKey(algorithmId, options) - keyStore[jwk.keyID] = jwk + if (jwk.kid.isNullOrEmpty()) { + jwk.kid = jwk.computeThumbprint() + } - return jwk.keyID + keyStore[jwk.kid!!] = jwk + return jwk.kid!! } /** * Computes and returns a public key corresponding to the private key identified by the provided keyAlias. * * @param keyAlias The alias (key ID) of the private key stored in the keyStore. - * @return The computed public key as a JWK object. + * @return The computed public key as a Jwk object. * @throws Exception if a key with the provided alias is not found in the keyStore. */ - override fun getPublicKey(keyAlias: String): JWK { + override fun getPublicKey(keyAlias: String): Jwk { // TODO: decide whether to return null or throw an exception val privateKey = getPrivateKey(keyAlias) return Crypto.computePublicKey(privateKey) @@ -72,12 +78,12 @@ public class InMemoryKeyManager : KeyManager { /** * Return the alias of [publicKey], as was originally returned by [generatePrivateKey]. * - * @param publicKey A public key in JWK (JSON Web Key) format + * @param publicKey A public key in Jwk (JSON Web Key) format * @return The alias belonging to [publicKey] * @throws IllegalArgumentException if the key is not known to the [KeyManager] */ - override fun getDeterministicAlias(publicKey: JWK): String { - val kid = publicKey.keyID ?: publicKey.computeThumbprint().toString() + override fun getDeterministicAlias(publicKey: Jwk): String { + val kid = publicKey.kid ?: publicKey.computeThumbprint() require(keyStore.containsKey(kid)) { "key with alias $kid not found" } @@ -94,20 +100,22 @@ public class InMemoryKeyManager : KeyManager { * @return A list of key aliases belonging to the imported keys. */ public fun import(keySet: Iterable>): List = keySet.map { - val jwk = JWK.parse(it) + // todo are all keySet.value of type Any in this case a possible Jwk? + // we can just call toString() and call it good? am skeptical + val jwk = Json.parse(it.toString()) import(jwk) } /** * Imports a single key and returns the alias that refers to it. * - * @param jwk A JWK object representing the key to be imported. + * @param jwk A Jwk object representing the key to be imported. * @return The alias belonging to the imported key. */ - public fun import(jwk: JWK): String { - var kid = jwk.keyID + public fun import(jwk: Jwk): String { + var kid = jwk.kid if (kid.isNullOrEmpty()) { - kid = jwk.computeThumbprint().toString() + kid = jwk.computeThumbprint() } keyStore.putIfAbsent(kid, jwk) return kid @@ -118,5 +126,6 @@ public class InMemoryKeyManager : KeyManager { * * @return A list of key representations in map format. */ - public fun export(): List> = keyStore.map { it.value.toJSONObject() } + public fun export(): List> = keyStore.map { it.value.toString().toMap() } + } diff --git a/crypto/src/main/kotlin/web5/sdk/crypto/KeyGenerator.kt b/crypto/src/main/kotlin/web5/sdk/crypto/KeyGenerator.kt index 948c3e070..975ffc83a 100644 --- a/crypto/src/main/kotlin/web5/sdk/crypto/KeyGenerator.kt +++ b/crypto/src/main/kotlin/web5/sdk/crypto/KeyGenerator.kt @@ -1,6 +1,7 @@ package web5.sdk.crypto import com.nimbusds.jose.jwk.JWK +import web5.sdk.crypto.jwk.Jwk /** * `KeyGenOptions` serves as an interface defining options or parameters that influence @@ -21,7 +22,7 @@ import com.nimbusds.jose.jwk.JWK * ```kotlin * val keyGenOptions: KeyGenOptions = ... * val keyGenerator: KeyGenerator = ... - * val privateKey: JWK = keyGenerator.generatePrivateKey(keyGenOptions) + * val privateKey: Jwk = keyGenerator.generatePrivateKey(keyGenOptions) * ``` * * ### Note: @@ -50,10 +51,10 @@ public interface KeyGenOptions * * ``` * val keyGenerator: KeyGenerator = ... - * val privateKey: JWK = keyGenerator.generatePrivateKey() - * val publicKey: JWK = keyGenerator.getPublicKey(privateKey) + * val privateKey: Jwk = keyGenerator.generatePrivateKey() + * val publicKey: Jwk = keyGenerator.getPublicKey(privateKey) * val privateKeyBytes: ByteArray = keyGenerator.privateKeyToBytes(privateKey) - * val restoredPrivateKey: JWK = keyGenerator.bytesToPrivateKey(privateKeyBytes) + * val restoredPrivateKey: Jwk = keyGenerator.bytesToPrivateKey(privateKeyBytes) * ``` * * ### Note: @@ -71,33 +72,33 @@ public interface KeyGenerator { public val curve: JwaCurve /** Generates a private key. */ - public fun generatePrivateKey(options: KeyGenOptions? = null): JWK + public fun generatePrivateKey(options: KeyGenOptions? = null): Jwk /** * Derives a public key from the private key provided. Applicable for asymmetric Key Generators only. * Implementers of symmetric key generators should throw an UnsupportedOperation Exception */ - public fun computePublicKey(privateKey: JWK): JWK + public fun computePublicKey(privateKey: Jwk): Jwk /** * Converts a private key to bytes. */ - public fun privateKeyToBytes(privateKey: JWK): ByteArray + public fun privateKeyToBytes(privateKey: Jwk): ByteArray /** * Converts a public key to bytes. Applicable for asymmetric [KeyGenerator] implementations only. * Implementers of symmetric key generators should throw an UnsupportedOperation Exception */ - public fun publicKeyToBytes(publicKey: JWK): ByteArray + public fun publicKeyToBytes(publicKey: Jwk): ByteArray /** * Converts a private key as bytes into a JWK. */ - public fun bytesToPrivateKey(privateKeyBytes: ByteArray): JWK + public fun bytesToPrivateKey(privateKeyBytes: ByteArray): Jwk /** * Converts a public key as bytes into a JWK. Applicable for asymmetric Key Generators only. * Implementers of symmetric key generators should throw an UnsupportedOperation Exception */ - public fun bytesToPublicKey(publicKeyBytes: ByteArray): JWK + public fun bytesToPublicKey(publicKeyBytes: ByteArray): Jwk } \ No newline at end of file diff --git a/crypto/src/main/kotlin/web5/sdk/crypto/KeyManager.kt b/crypto/src/main/kotlin/web5/sdk/crypto/KeyManager.kt index 6a9cfb8f8..169b89ec2 100644 --- a/crypto/src/main/kotlin/web5/sdk/crypto/KeyManager.kt +++ b/crypto/src/main/kotlin/web5/sdk/crypto/KeyManager.kt @@ -1,6 +1,6 @@ package web5.sdk.crypto -import com.nimbusds.jose.jwk.JWK +import web5.sdk.crypto.jwk.Jwk /** * A key management interface that provides functionality for generating, storing, and utilizing @@ -31,12 +31,12 @@ public interface KeyManager { * Retrieves the public key associated with a previously stored private key, identified by the provided alias. * * @param keyAlias The alias referencing the stored private key. - * @return The associated public key in JWK (JSON Web Key) format. + * @return The associated public key in Jwk (JSON Web Key) format. * * The function should provide the public key in a format suitable for external sharing and usage, * enabling others to perform operations like verifying signatures or encrypting data for the private key holder. */ - public fun getPublicKey(keyAlias: String): JWK + public fun getPublicKey(keyAlias: String): Jwk /** * Signs the provided payload using the private key identified by the provided alias. @@ -54,17 +54,17 @@ public interface KeyManager { /** * Return the alias of [publicKey], as was originally returned by [generatePrivateKey]. * - * @param publicKey A public key in JWK (JSON Web Key) format + * @param publicKey A public key in Jwk (JSON Web Key) format * @return The alias belonging to [publicKey] * @throws IllegalArgumentException if the key is not known to the [KeyManager] */ - public fun getDeterministicAlias(publicKey: JWK): String + public fun getDeterministicAlias(publicKey: Jwk): String } public interface KeyExporter { - public fun exportKey(keyId: String): JWK + public fun exportKey(keyId: String): Jwk } public interface KeyImporter { - public fun importKey(jwk: JWK): String + public fun importKey(jwk: Jwk): String } \ No newline at end of file diff --git a/crypto/src/main/kotlin/web5/sdk/crypto/Secp256k1.kt b/crypto/src/main/kotlin/web5/sdk/crypto/Secp256k1.kt index 7321897f1..c87047b7e 100644 --- a/crypto/src/main/kotlin/web5/sdk/crypto/Secp256k1.kt +++ b/crypto/src/main/kotlin/web5/sdk/crypto/Secp256k1.kt @@ -16,8 +16,11 @@ import org.bouncycastle.crypto.signers.HMacDSAKCalculator import org.bouncycastle.jce.ECNamedCurveTable import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec import org.bouncycastle.math.ec.ECPoint +import web5.sdk.common.Convert +import web5.sdk.common.EncodingFormat import web5.sdk.crypto.Secp256k1.PRIV_MULTICODEC import web5.sdk.crypto.Secp256k1.PUB_MULTICODEC +import web5.sdk.crypto.jwk.Jwk import java.math.BigInteger import java.security.MessageDigest import java.security.Security @@ -135,61 +138,83 @@ public object Secp256k1 : KeyGenerator, Signer { * be intended for signature use. * * @param options Options for key generation (currently unused, provided for possible future expansion). - * @return A JWK representing the generated private key. + * @return A Jwk representing the generated private key. */ - override fun generatePrivateKey(options: KeyGenOptions?): JWK { - return ECKeyGenerator(com.nimbusds.jose.jwk.Curve.SECP256K1) + override fun generatePrivateKey(options: KeyGenOptions?): Jwk { + // todo use tink to generate private key? + val privateKey = ECKeyGenerator(com.nimbusds.jose.jwk.Curve.SECP256K1) .algorithm(JWSAlgorithm.ES256K) .provider(BouncyCastleProviderSingleton.getInstance()) .keyIDFromThumbprint(true) .keyUse(KeyUse.SIGNATURE) .generate() + + return Jwk.Builder() + .algorithm(algorithm.name) + .keyType("EC") + .keyUse("sig") + .privateKey(Convert(privateKey.d, EncodingFormat.Base64Url).toStr()) + .x(Convert(privateKey.d, EncodingFormat.Base64Url).toStr()) + .build() } - override fun computePublicKey(privateKey: JWK): JWK { + override fun computePublicKey(privateKey: Jwk): Jwk { validateKey(privateKey) - return privateKey.toECKey().toPublicJWK() + return Jwk.Builder() + .keyType(privateKey.kty) + .algorithm(algorithm.name) + .apply { + privateKey.x?.let { x(it) } + privateKey.y?.let { y(it) } + } + .build() + } - override fun privateKeyToBytes(privateKey: JWK): ByteArray { + override fun privateKeyToBytes(privateKey: Jwk): ByteArray { validateKey(privateKey) - return privateKey.toECKey().d.decode() + return Convert(privateKey.d, EncodingFormat.Base64Url).toByteArray() } - override fun publicKeyToBytes(publicKey: JWK): ByteArray { + override fun publicKeyToBytes(publicKey: Jwk): ByteArray { validateKey(publicKey) - val ecKey = publicKey.toECKey() - val xBytes = ecKey.x.decode() - val yBytes = ecKey.y.decode() + val xBytes = Convert(publicKey.x, EncodingFormat.Base64Url).toByteArray() + val yBytes = Convert(publicKey.y, EncodingFormat.Base64Url).toByteArray() return byteArrayOf(UNCOMPRESSED_KEY_ID) + xBytes + yBytes } - override fun bytesToPrivateKey(privateKeyBytes: ByteArray): JWK { + override fun bytesToPrivateKey(privateKeyBytes: ByteArray): Jwk { + // todo what is this MAGIC??? var pointQ: ECPoint = spec.g.multiply(BigInteger(1, privateKeyBytes)) pointQ = pointQ.normalize() val rawX = pointQ.rawXCoord.encoded val rawY = pointQ.rawYCoord.encoded - return ECKey.Builder(com.nimbusds.jose.jwk.Curve.SECP256K1, Base64URL.encode(rawX), Base64URL.encode(rawY)) - .algorithm(JWSAlgorithm.ES256K) - .keyIDFromThumbprint() - .keyUse(KeyUse.SIGNATURE) + return Jwk.Builder() + .keyType("EC") + .algorithm(algorithm.name) + .keyUse("sig") + .x(Convert(rawX).toBase64Url()) + .y(Convert(rawY).toBase64Url()) + .privateKey(Convert(privateKeyBytes).toBase64Url()) .build() } - override fun bytesToPublicKey(publicKeyBytes: ByteArray): JWK { + override fun bytesToPublicKey(publicKeyBytes: ByteArray): Jwk { val xBytes = publicKeyBytes.sliceArray(1..32) val yBytes = publicKeyBytes.sliceArray(33..64) - return ECKey.Builder(com.nimbusds.jose.jwk.Curve.SECP256K1, Base64URL.encode(xBytes), Base64URL.encode(yBytes)) - .algorithm(JWSAlgorithm.ES256K) - .keyIDFromThumbprint() - .keyUse(KeyUse.SIGNATURE) + return Jwk.Builder() + .keyType("EC") + .algorithm(algorithm.name) + .x(Convert(xBytes).toBase64Url()) + .y(Convert(yBytes).toBase64Url()) + .keyUse("sig") .build() } @@ -211,8 +236,8 @@ public object Secp256k1 : KeyGenerator, Signer { * @throws IllegalArgumentException If the provided key, payload, or options are invalid or inappropriate * for the signing process. */ - override fun sign(privateKey: JWK, payload: ByteArray, options: SignOptions?): ByteArray { - val privateKeyBigInt = privateKey.toECKey().d.decodeToBigInteger() + override fun sign(privateKey: Jwk, payload: ByteArray, options: SignOptions?): ByteArray { + val privateKeyBigInt = Convert(privateKey.d, EncodingFormat.Base64Url).toStr().toBigInteger() val privateKeyParams = ECPrivateKeyParameters(privateKeyBigInt, curveParams) // generates k value deterministically using the private key and message hash, ensuring that signing the same @@ -260,7 +285,7 @@ public object Secp256k1 : KeyGenerator, Signer { * @throws IllegalArgumentException If the provided public key or signature format is invalid or not * supported by the implementation. */ - override fun verify(publicKey: JWK, signedPayload: ByteArray, signature: ByteArray, options: VerifyOptions?) { + override fun verify(publicKey: Jwk, signedPayload: ByteArray, signature: ByteArray, options: VerifyOptions?) { val publicKeyBytes = publicKeyToBytes(publicKey) val publicKeyPoint = spec.curve.decodePoint(publicKeyBytes) @@ -300,7 +325,7 @@ public object Secp256k1 : KeyGenerator, Signer { * * ### Usage Example: * ``` - * val jwk: JWK = //...obtain or generate a JWK + * val jwk: Jwk = //...obtain or generate a JWK * try { * Secp256k1.validateKey(jwk) * // Key is valid, proceed with further operations... @@ -316,8 +341,8 @@ public object Secp256k1 : KeyGenerator, Signer { * @param key The [JWK] to validate. * @throws IllegalArgumentException if the key is not of type [ECKey]. */ - public fun validateKey(key: JWK) { - require(key is ECKey) { "private key must be an ECKey (kty: EC)" } + public fun validateKey(key: Jwk) { + require(key.kty == "EC") { "private key must be an ECKey (kty: EC)" } } /** diff --git a/crypto/src/main/kotlin/web5/sdk/crypto/Signer.kt b/crypto/src/main/kotlin/web5/sdk/crypto/Signer.kt index f06e1f85e..51cb1656c 100644 --- a/crypto/src/main/kotlin/web5/sdk/crypto/Signer.kt +++ b/crypto/src/main/kotlin/web5/sdk/crypto/Signer.kt @@ -1,6 +1,6 @@ package web5.sdk.crypto -import com.nimbusds.jose.jwk.JWK +import web5.sdk.crypto.jwk.Jwk import java.security.SignatureException /** @@ -80,28 +80,28 @@ public interface Signer { /** * Sign a given payload using a private key. * - * This function takes a payload and a private key in JWK (JSON Web Key) format, + * This function takes a payload and a private key in Jwk (JSON Web Key) format, * and returns a signature as a byte array. Additional options for the signing * process can be provided via the `options` parameter. * - * @param privateKey The private key in JWK format to be used for signing. + * @param privateKey The private key in Jwk format to be used for signing. * Must not be null. * @param payload The payload/data to be signed. Must not be null. * @param options Optional parameter containing additional options to control * the signing process. Default is null. * @return A [ByteArray] representing the signature. */ - public fun sign(privateKey: JWK, payload: ByteArray, options: SignOptions? = null): ByteArray + public fun sign(privateKey: Jwk, payload: ByteArray, options: SignOptions? = null): ByteArray /** * Verify the signature of a given payload using a public key. * * This function attempts to verify the signature of a provided payload using a public key, - * supplied in JWK (JSON Web Key) format, and a signature. The verification process checks + * supplied in Jwk (JSON Web Key) format, and a signature. The verification process checks * the validity of the signature against the provided payload, respecting any optional * verification options provided via [VerifyOptions]. * - * @param publicKey The public key in JWK format used for verifying the signature. + * @param publicKey The public key in Jwk format used for verifying the signature. * Must not be null. * @param signedPayload The original payload/data that was signed, to be verified * against its signature. Must not be null. @@ -112,5 +112,5 @@ public interface Signer { * * @throws [SignatureException] if the verification fails. */ - public fun verify(publicKey: JWK, signedPayload: ByteArray, signature: ByteArray, options: VerifyOptions? = null) + public fun verify(publicKey: Jwk, signedPayload: ByteArray, signature: ByteArray, options: VerifyOptions? = null) } \ No newline at end of file diff --git a/crypto/src/main/kotlin/web5/sdk/crypto/jwk/Jwk.kt b/crypto/src/main/kotlin/web5/sdk/crypto/jwk/Jwk.kt new file mode 100644 index 000000000..9e2779f78 --- /dev/null +++ b/crypto/src/main/kotlin/web5/sdk/crypto/jwk/Jwk.kt @@ -0,0 +1,96 @@ +package web5.sdk.crypto.jwk + +import web5.sdk.common.Convert +import web5.sdk.common.Json +import java.security.MessageDigest + +public class Jwk( + public val kty: String, + public val use: String?, + public val alg: String?, + public var kid: String?, + public val crv: String?, + public val d: String? = null, + public val x: String?, + public val y: String? +) { + + public fun computeThumbprint(): String { + val thumbprintPayload = Json.jsonMapper.createObjectNode().apply { + put("crv", crv) + put("kty", kty) + put("x", x) + put("y", y) + } + + val thumbprintPayloadString = Json.stringify(thumbprintPayload) + val thumbprintPayloadBytes = Convert(thumbprintPayloadString).toByteArray() + + // todo read spec: https://datatracker.ietf.org/doc/html/rfc7638#section-3.1 + val messageDigest = MessageDigest.getInstance("SHA-256") + val thumbprintPayloadDigest = messageDigest.digest(thumbprintPayloadBytes) + + return Convert(thumbprintPayloadDigest).toBase64Url() + + } + + public class Builder { + private var kty: String? = null + private var use: String? = null + private var alg: String? = null + private var kid: String? = null + private var crv: String? = null + private var d: String? = null + private var x: String? = null + private var y: String? = null + + public fun keyType(kty: String): Builder { + this.kty = kty + return this + } + + public fun keyUse(use: String): Builder { + this.use = use + return this + } + + public fun algorithm(alg: String): Builder { + this.alg = alg + return this + } + + public fun keyId(kid: String): Builder { + this.kid = kid + return this + } + + public fun curve(crv: String): Builder { + this.crv = crv + return this + } + + public fun privateKey(d: String): Builder { + this.d = d + return this + } + + public fun x(x: String): Builder { + this.x = x + return this + } + + public fun y(y: String): Builder { + this.y = y + return this + } + + public fun build(): Jwk { + // todo are any of the other fields required? + // if kty ec: x y required, + // if kty ed: x required + check(kty != null) { "kty is required" } + return Jwk(kty!!, use, alg, kid, crv, d, x, y) + } + + } +} diff --git a/crypto/src/test/kotlin/web5/sdk/crypto/AwsKeyManagerTest.kt b/crypto/src/test/kotlin/web5/sdk/crypto/AwsKeyManagerTest.kt index 18f430b81..b2274090e 100644 --- a/crypto/src/test/kotlin/web5/sdk/crypto/AwsKeyManagerTest.kt +++ b/crypto/src/test/kotlin/web5/sdk/crypto/AwsKeyManagerTest.kt @@ -4,9 +4,6 @@ import com.amazonaws.AmazonServiceException import com.amazonaws.auth.AWSStaticCredentialsProvider import com.amazonaws.auth.BasicAWSCredentials import com.amazonaws.services.kms.AWSKMSClient -import com.nimbusds.jose.JWSAlgorithm -import com.nimbusds.jose.jwk.ECKey -import com.nimbusds.jose.jwk.KeyUse import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows @@ -24,10 +21,10 @@ class AwsKeyManagerTest { val alias = awsKeyManager.generatePrivateKey(AlgorithmId.secp256k1) val publicKey = awsKeyManager.getPublicKey(alias) - assertEquals(alias, publicKey.keyID) - assertTrue(publicKey is ECKey) - assertEquals(KeyUse.SIGNATURE, publicKey.keyUse) - assertEquals(JWSAlgorithm.ES256K, publicKey.algorithm) + assertEquals(alias, publicKey.kid) + assertTrue(publicKey.kty == "EC") + assertEquals("sig", publicKey.use) + assertEquals(Jwa.ES256K.name, publicKey.alg) } @Test diff --git a/crypto/src/test/kotlin/web5/sdk/crypto/Ed25519Test.kt b/crypto/src/test/kotlin/web5/sdk/crypto/Ed25519Test.kt index 5edbaf3c4..9f9acad6b 100644 --- a/crypto/src/test/kotlin/web5/sdk/crypto/Ed25519Test.kt +++ b/crypto/src/test/kotlin/web5/sdk/crypto/Ed25519Test.kt @@ -2,9 +2,10 @@ package web5.sdk.crypto import com.fasterxml.jackson.core.type.TypeReference import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import com.nimbusds.jose.jwk.OctetKeyPair import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertDoesNotThrow +import web5.sdk.common.Json +import web5.sdk.crypto.jwk.Jwk import web5.sdk.testing.TestVectors import java.io.File import kotlin.test.assertEquals @@ -34,7 +35,7 @@ class Web5TestVectorsCryptoEd25519 { val inputByteArray: ByteArray = hexStringToByteArray(vector.input.data) val jwkMap = vector.input.key - val ed25519Jwk = OctetKeyPair.parse(jwkMap.toString()) + val ed25519Jwk = Json.parse(jwkMap.toString()) val signedByteArray: ByteArray = Ed25519.sign(ed25519Jwk, inputByteArray) @@ -50,7 +51,7 @@ class Web5TestVectorsCryptoEd25519 { val inputByteArray: ByteArray = hexStringToByteArray(vector.input.data) val jwkMap = vector.input.key - val ed25519Jwk = OctetKeyPair.parse(jwkMap.toString()) + val ed25519Jwk = Json.parse(jwkMap.toString()) Ed25519.sign(ed25519Jwk, inputByteArray) } @@ -63,7 +64,7 @@ class Web5TestVectorsCryptoEd25519 { val testVectors = mapper.readValue(File("../web5-spec/test-vectors/crypto_ed25519/verify.json"), typeRef) testVectors.vectors.filter { it.errors == false }.forEach { vector -> - val key = OctetKeyPair.parse(vector.input.key) + val key = Json.parse(vector.input.key.toString()) val data = hexStringToByteArray(vector.input.data) val signature = hexStringToByteArray(vector.input.signature) if (vector.output == true) { @@ -79,7 +80,7 @@ class Web5TestVectorsCryptoEd25519 { testVectors.vectors.filter { it.errors == true }.forEach { vector -> assertFails { - val key = OctetKeyPair.parse(vector.input.key) + val key = Json.parse(vector.input.key.toString()) val data = hexStringToByteArray(vector.input.data) val signature = hexStringToByteArray(vector.input.signature) Ed25519.verify(key, data, signature) diff --git a/crypto/src/test/kotlin/web5/sdk/crypto/Secp256k1Test.kt b/crypto/src/test/kotlin/web5/sdk/crypto/Secp256k1Test.kt index 06f029d92..98d0517b8 100644 --- a/crypto/src/test/kotlin/web5/sdk/crypto/Secp256k1Test.kt +++ b/crypto/src/test/kotlin/web5/sdk/crypto/Secp256k1Test.kt @@ -2,13 +2,12 @@ package web5.sdk.crypto import com.fasterxml.jackson.core.type.TypeReference import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import com.nimbusds.jose.JWSAlgorithm -import com.nimbusds.jose.jwk.ECKey -import com.nimbusds.jose.jwk.KeyUse import org.apache.commons.codec.binary.Hex import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertDoesNotThrow import web5.sdk.common.Convert +import web5.sdk.common.Json +import web5.sdk.crypto.jwk.Jwk import web5.sdk.testing.TestVectors import java.io.File import java.security.SignatureException @@ -25,11 +24,11 @@ class Secp256k1Test { val privateKey = Secp256k1.generatePrivateKey() Secp256k1.validateKey(privateKey) - assertEquals(JWSAlgorithm.ES256K, privateKey.algorithm) - assertEquals(KeyUse.SIGNATURE, privateKey.keyUse) - assertNotNull(privateKey.keyID) - assertTrue(privateKey is ECKey) - assertTrue(privateKey.isPrivate) + assertEquals(Jwa.ES256K.name, privateKey.alg) + assertEquals("sig", privateKey.use) + assertNotNull(privateKey.kid) + assertTrue(privateKey.kty == "EC") + assertNotNull(privateKey.d) } @Test @@ -39,11 +38,11 @@ class Secp256k1Test { val publicKey = Secp256k1.computePublicKey(privateKey) Secp256k1.validateKey(publicKey) - assertEquals(publicKey.keyID, privateKey.keyID) - assertEquals(JWSAlgorithm.ES256K, publicKey.algorithm) - assertEquals(KeyUse.SIGNATURE, publicKey.keyUse) - assertTrue(publicKey is ECKey) - assertFalse(publicKey.isPrivate) + assertEquals(publicKey.kid, privateKey.kid) + assertEquals(Jwa.ES256K.name, publicKey.alg) + assertEquals("sign", publicKey.use) + assertTrue(publicKey.kty == "EC") + assertNotNull(publicKey.d) } @Test @@ -110,7 +109,7 @@ class Web5TestVectorsCryptoEs256k { testVectors.vectors.filter { it.errors == false }.forEach { vector -> val inputByteArray: ByteArray = Hex.decodeHex(vector.input.data.toCharArray()) val jwkMap = vector.input.key - val ecJwk = ECKey.parse(jwkMap.toString()) + val ecJwk = Json.parse(jwkMap.toString()) val signedByteArray: ByteArray = Secp256k1.sign(ecJwk, inputByteArray) val signedHex = Hex.encodeHexString(signedByteArray) @@ -123,7 +122,7 @@ class Web5TestVectorsCryptoEs256k { val inputByteArray: ByteArray = Hex.decodeHex(vector.input.data.toCharArray()) val jwkMap = vector.input.key - val ecJwk = ECKey.parse(jwkMap.toString()) + val ecJwk = Json.parse(jwkMap.toString()) Secp256k1.sign(ecJwk, inputByteArray) } @@ -140,7 +139,7 @@ class Web5TestVectorsCryptoEs256k { val jwkMap = vector.input.key val signatureByteArray = Hex.decodeHex(vector.input.signature.toCharArray()) - val ecJwk = ECKey.parse(jwkMap.toString()) + val ecJwk = Json.parse(jwkMap.toString()) if (vector.output == true) { assertDoesNotThrow { Secp256k1.verify(ecJwk, inputByteArray, signatureByteArray) } @@ -155,7 +154,7 @@ class Web5TestVectorsCryptoEs256k { val jwkMap = vector.input.key val signatureByteArray = Hex.decodeHex(vector.input.signature.toCharArray()) - val ecJwk = ECKey.parse(jwkMap.toString()) + val ecJwk = Json.parse(jwkMap.toString()) Secp256k1.verify(ecJwk, inputByteArray, signatureByteArray) } } diff --git a/dids/src/main/kotlin/web5/sdk/dids/Serialization.kt b/dids/src/main/kotlin/web5/sdk/dids/Serialization.kt index 1de8ae552..238fc3ae1 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/Serialization.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/Serialization.kt @@ -8,17 +8,17 @@ import com.fasterxml.jackson.databind.JsonDeserializer import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.JsonSerializer import com.fasterxml.jackson.databind.SerializerProvider -import com.nimbusds.jose.jwk.JWK +import web5.sdk.common.Json +import web5.sdk.crypto.jwk.Jwk import web5.sdk.dids.didcore.Purpose import java.io.IOException /** - * Serialize JWK into String. + * Serialize Jwk into String. */ -public class JWKSerializer : JsonSerializer() { - public override fun serialize(jwk: JWK?, gen: JsonGenerator, serializers: SerializerProvider?) { - val jwkString = jwk?.toJSONString() - +public class JwkSerializer : JsonSerializer() { + public override fun serialize(jwk: Jwk?, gen: JsonGenerator, serializers: SerializerProvider?) { + val jwkString = jwk?.let { Json.stringify(it) } gen.writeRawValue(jwkString) } @@ -28,11 +28,11 @@ public class JWKSerializer : JsonSerializer() { * Deserialize String into JWK. * */ -public class JwkDeserializer : JsonDeserializer() { - override fun deserialize(p: JsonParser, ctxt: DeserializationContext): JWK { +public class JwkDeserializer : JsonDeserializer() { + override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Jwk { val node = p.codec.readTree(p) val jwkJson = node.toString() - return JWK.parse(jwkJson) + return Json.parse(jwkJson) } } diff --git a/dids/src/main/kotlin/web5/sdk/dids/did/BearerDid.kt b/dids/src/main/kotlin/web5/sdk/dids/did/BearerDid.kt index 50c1e4150..6682dcb12 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/did/BearerDid.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/did/BearerDid.kt @@ -1,10 +1,10 @@ package web5.sdk.dids.did -import com.nimbusds.jose.jwk.JWK import web5.sdk.crypto.InMemoryKeyManager import web5.sdk.crypto.KeyExporter import web5.sdk.crypto.KeyImporter import web5.sdk.crypto.KeyManager +import web5.sdk.crypto.jwk.Jwk import web5.sdk.dids.didcore.DidDocument import web5.sdk.dids.didcore.Did import web5.sdk.dids.didcore.VMSelector @@ -34,7 +34,7 @@ public class BearerDid( public fun export(): PortableDid { val keyExporter = keyManager as? KeyExporter - val privateKeys = mutableListOf() + val privateKeys = mutableListOf() document.verificationMethod?.forEach { vm -> val keyAliasResult = runCatching { vm.publicKeyJwk?.computeThumbprint() } @@ -70,7 +70,7 @@ public class BearerDid( ?: false check(allVerificationMethodsHavePublicKey) { - "Each VerificationMethod must contain a public key in JWK format." + "Each VerificationMethod must contain a public key in Jwk format." } val did = Did.parse(portableDID.uri) diff --git a/dids/src/main/kotlin/web5/sdk/dids/did/PortableDid.kt b/dids/src/main/kotlin/web5/sdk/dids/did/PortableDid.kt index a1964f7b7..a8d88d3bc 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/did/PortableDid.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/did/PortableDid.kt @@ -1,11 +1,11 @@ package web5.sdk.dids.did -import com.nimbusds.jose.jwk.JWK +import web5.sdk.crypto.jwk.Jwk import web5.sdk.dids.didcore.DidDocument public class PortableDid( public val uri: String, - public val privateKeys: List, + public val privateKeys: List, public val document: DidDocument, public val metadata: Map ) \ No newline at end of file diff --git a/dids/src/main/kotlin/web5/sdk/dids/didcore/DidDocument.kt b/dids/src/main/kotlin/web5/sdk/dids/didcore/DidDocument.kt index e3d6cfd6c..71c15f77b 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/didcore/DidDocument.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/didcore/DidDocument.kt @@ -110,6 +110,13 @@ public class DidDocument( */ @JvmOverloads public fun findAssertionMethodById(assertionMethodId: String? = null): VerificationMethod { + /* + // todo add this bit (see jwtutil) + val verificationMethodIds = setOf( + did.url, + "#${did.fragment}" + ) + */ require(!assertionMethod.isNullOrEmpty()) { throw SignatureException("No assertion methods found in DID document") } diff --git a/dids/src/main/kotlin/web5/sdk/dids/didcore/VerificationMethod.kt b/dids/src/main/kotlin/web5/sdk/dids/didcore/VerificationMethod.kt index d022b1a70..bc1f79005 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/didcore/VerificationMethod.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/didcore/VerificationMethod.kt @@ -2,8 +2,8 @@ package web5.sdk.dids.didcore import com.fasterxml.jackson.databind.annotation.JsonDeserialize import com.fasterxml.jackson.databind.annotation.JsonSerialize -import com.nimbusds.jose.jwk.JWK -import web5.sdk.dids.JWKSerializer +import web5.sdk.crypto.jwk.Jwk +import web5.sdk.dids.JwkSerializer import web5.sdk.dids.JwkDeserializer /** @@ -26,9 +26,9 @@ public class VerificationMethod( public val id: String, public val type: String, public val controller: String, - @JsonSerialize(using = JWKSerializer::class) + @JsonSerialize(using = JwkSerializer::class) @JsonDeserialize(using = JwkDeserializer::class) - public val publicKeyJwk: JWK? = null + public val publicKeyJwk: Jwk? = null ) { /** * Checks type of VerificationMethod. @@ -55,7 +55,7 @@ public class VerificationMethod( private var id: String? = null private var type: String? = null private var controller: String? = null - private var publicKeyJwk: JWK? = null + private var publicKeyJwk: Jwk? = null /** @@ -88,7 +88,7 @@ public class VerificationMethod( * @param publicKeyJwk of the VerificationMethod * @return Builder object */ - public fun publicKeyJwk(publicKeyJwk: JWK): Builder = apply { this.publicKeyJwk = publicKeyJwk } + public fun publicKeyJwk(publicKeyJwk: Jwk): Builder = apply { this.publicKeyJwk = publicKeyJwk } /** diff --git a/dids/src/main/kotlin/web5/sdk/dids/jwk/Jwk.kt b/dids/src/main/kotlin/web5/sdk/dids/jwk/Jwk.kt deleted file mode 100644 index 420452695..000000000 --- a/dids/src/main/kotlin/web5/sdk/dids/jwk/Jwk.kt +++ /dev/null @@ -1,43 +0,0 @@ -package web5.sdk.dids.jwk - -import web5.sdk.common.Convert -import web5.sdk.common.Json -import java.security.MessageDigest - -public class Jwk( - public val kty: String, - public val use: String?, - public val alg: String?, - kid: String?, - public val crv: String?, - public val d: String? = null, - public val x: String?, - public val y: String?, - kidFromThumbprint: Boolean = true -) { - - public val kid: String? = kid ?: if (kidFromThumbprint) computeThumbprint() else null - - private fun computeThumbprint(): String { - val thumbprintPayload = Json.jsonMapper.createObjectNode().apply { - put("crv", crv) - put("kty", kty) - put("x", x) - put("y", y) - } - - // todo this is what chad told me to do, not sure 100% correct - val thumbprintPayloadString = Json.stringify(thumbprintPayload) - val thumbprintPayloadBytes = Convert(thumbprintPayloadString).toByteArray() - - // todo what does this do? supposed to be the equivalent of - // final thumbprintPayloadDigest = _dartSha256.hashSync(thumbprintPayloadBytes); - val messageDigest = MessageDigest.getInstance("SHA-256") - val thumbprintPayloadDigest = messageDigest.digest(thumbprintPayloadBytes) - - return Convert(thumbprintPayloadDigest).toBase64Url() - - } - - -} diff --git a/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DhtClient.kt b/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DhtClient.kt index a63331370..c6b89e9ec 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DhtClient.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DhtClient.kt @@ -131,7 +131,7 @@ internal class DhtClient( fun createBep44PutRequest(manager: KeyManager, keyAlias: String, message: Message): Bep44Message { // get the public key to verify it is an Ed25519 key val pubKey = manager.getPublicKey(keyAlias) - val curve = pubKey.toJSONObject()["crv"] + val curve = pubKey.crv require(curve == Ed25519.curve.name) { "Must supply an Ed25519 key" } @@ -183,7 +183,7 @@ internal class DhtClient( // get the public key to verify it is an Ed25519 key val pubKey = manager.getPublicKey(keyAlias) - val curve = pubKey.toJSONObject()["crv"] + val curve = pubKey.crv require(curve == Ed25519.curve.name) { "Must supply an Ed25519 key" } diff --git a/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt b/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt index 2a159c29a..850d9814f 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt @@ -1,7 +1,6 @@ package web5.sdk.dids.methods.dht import com.nimbusds.jose.JWSAlgorithm -import com.nimbusds.jose.jwk.JWK import io.github.oshai.kotlinlogging.KotlinLogging import io.ktor.client.engine.HttpClientEngine import io.ktor.client.engine.okhttp.OkHttp @@ -17,8 +16,10 @@ import web5.sdk.crypto.AlgorithmId import web5.sdk.crypto.Crypto import web5.sdk.crypto.Ed25519 import web5.sdk.crypto.InMemoryKeyManager +import web5.sdk.crypto.Jwa import web5.sdk.crypto.KeyManager import web5.sdk.crypto.Secp256k1 +import web5.sdk.crypto.jwk.Jwk import web5.sdk.dids.CreateDidOptions import web5.sdk.dids.DidResolutionResult import web5.sdk.dids.ResolutionError @@ -90,7 +91,7 @@ private class DidDhtApiImpl(configuration: DidDhtConfiguration) : DidDhtApi(conf * @property alsoKnownAses A list of also known as identifiers to add to the DID Document. */ public class CreateDidDhtOptions( - public val verificationMethods: Iterable, String?>>? = null, + public val verificationMethods: Iterable, String?>>? = null, public val services: Iterable? = null, public val publish: Boolean = true, public val controllers: Iterable? = null, @@ -164,7 +165,7 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) { id = "$didUri#0", type = "JsonWebKey", controller = didUri, - publicKeyJwk = publicKey.toPublicJWK() + publicKeyJwk = publicKey ) didDocumentBuilder.verificationMethodForPurposes( @@ -177,12 +178,12 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) { ) ) - opts.verificationMethods?.map { (key, purposes, controller) -> + opts.verificationMethods?.map { (publicKey, purposes, controller) -> VerificationMethod.Builder() - .id("$didUri#${key.keyID}") + .id("$didUri#${publicKey.kid}") .type("JsonWebKey") .controller(controller ?: didUri) - .publicKeyJwk(key.toPublicJWK()) + .publicKeyJwk(publicKey) .build().also { verificationMethod -> didDocumentBuilder.verificationMethodForPurposes(verificationMethod, purposes.toList()) @@ -305,7 +306,7 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) { val publicKeyJwk = didDocument.verificationMethod?.first()?.publicKeyJwk ?: throw PublicKeyJwkMissingException("publicKeyJwk is null") - return publicKeyJwk.keyID + return publicKeyJwk.kid ?: publicKeyJwk.computeThumbprint() } internal fun validateIdentityKey(did: String, keyManager: KeyManager) { @@ -325,8 +326,19 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) { * * @param identityKey the key used to generate the DID's identifier */ - internal fun getDidIdentifier(identityKey: JWK): String { - val publicKeyJwk = identityKey.toPublicJWK() + internal fun getDidIdentifier(identityKey: Jwk): String { + val publicKeyJwk = Jwk.Builder() + .keyType(identityKey.kty) + .apply { + identityKey.x?.let { x(it) } + identityKey.y?.let { y(it) } + identityKey.alg?.let { algorithm(it) } + (identityKey.kid + ?: identityKey.computeThumbprint()) + .let { keyId(it) } + } + .build() + val publicKeyBytes = Crypto.publicKeyToBytes(publicKeyJwk) val zBase32Encoded = ZBase32.encode(publicKeyBytes) return "did:dht:$zBase32Encoded" @@ -454,11 +466,11 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) { verificationMethodsById[verificationMethod.id] = verificationMethodId - val keyType = when (publicKeyJwk.algorithm) { - JWSAlgorithm.EdDSA -> 0 - JWSAlgorithm.ES256K -> 1 - JWSAlgorithm.ES256 -> 2 - else -> throw IllegalArgumentException("unsupported algorithm: ${publicKeyJwk.algorithm}") + // todo dropping support for ES256? + val keyType = when (publicKeyJwk.alg) { + Jwa.EdDSA.name -> 0 + Jwa.ES256K.name -> 1 + else -> throw IllegalArgumentException("unsupported algorithm: ${publicKeyJwk.alg}") } message.addRecord( @@ -607,7 +619,7 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) { val vmBuilder = VerificationMethod.Builder() .id("$did#$verificationMethodId") .type("JsonWebKey") - .publicKeyJwk(publicKeyJwk.toPublicJWK()) + .publicKeyJwk(publicKeyJwk) if (data.containsKey("c")) { vmBuilder.controller(data["c"]!!) diff --git a/dids/src/main/kotlin/web5/sdk/dids/methods/jwk/DidJwk.kt b/dids/src/main/kotlin/web5/sdk/dids/methods/jwk/DidJwk.kt index a42738c77..f866167db 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/methods/jwk/DidJwk.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/methods/jwk/DidJwk.kt @@ -1,12 +1,12 @@ package web5.sdk.dids.methods.jwk -import com.nimbusds.jose.jwk.JWK -import com.nimbusds.jose.jwk.KeyUse import web5.sdk.common.Convert import web5.sdk.common.EncodingFormat +import web5.sdk.common.Json import web5.sdk.crypto.AlgorithmId import web5.sdk.crypto.InMemoryKeyManager import web5.sdk.crypto.KeyManager +import web5.sdk.crypto.jwk.Jwk import web5.sdk.dids.DidResolutionMetadata import web5.sdk.dids.DidResolutionResult import web5.sdk.dids.ResolutionError @@ -61,7 +61,7 @@ public class DidJwk { val keyAlias = keyManager.generatePrivateKey(algorithmId) val publicKeyJwk = keyManager.getPublicKey(keyAlias) - val base64Encoded = Convert(publicKeyJwk.toJSONString()).toBase64Url() + val base64Encoded = Convert(Json.stringify(publicKeyJwk)).toBase64Url() val didUri = "did:jwk:$base64Encoded" @@ -118,7 +118,7 @@ public class DidJwk { val id = parsedDid.id val decodedKey = Convert(id, EncodingFormat.Base64Url).toStr() val publicKeyJwk = try { - JWK.parse(decodedKey) + Json.parse(decodedKey) } catch (_: ParseException) { return DidResolutionResult( context = "https://w3id.org/did-resolution/v1", @@ -128,7 +128,7 @@ public class DidJwk { ) } - require(!publicKeyJwk.isPrivate) { + require(publicKeyJwk.d != null) { throw IllegalArgumentException("decoded jwk value cannot be a private key") } @@ -137,7 +137,7 @@ public class DidJwk { return DidResolutionResult(didDocument = didDocument, context = "https://w3id.org/did-resolution/v1") } - private fun createDocument(did: Did, publicKeyJwk: JWK): DidDocument { + private fun createDocument(did: Did, publicKeyJwk: Jwk): DidDocument { val verificationMethodId = "${did.uri}#0" val verificationMethod = VerificationMethod.Builder() .id(verificationMethodId) @@ -149,12 +149,7 @@ public class DidJwk { val didDocumentBuilder = DidDocument.Builder() .context(listOf("https://www.w3.org/ns/did/v1")) .id(did.url) - - // todo noticed that this was already in kotlin impl of building did doc - // but it's not in go impl? - // ask frank. encryption not needed for tbdex use, so not considered in go impl - // keyUse is technically not required (per spec) - if (publicKeyJwk.keyUse != KeyUse.ENCRYPTION) { + if (publicKeyJwk.use != "enc") { didDocumentBuilder .verificationMethodForPurposes( verificationMethod, @@ -167,7 +162,7 @@ public class DidJwk { ) } - if (publicKeyJwk.keyUse != KeyUse.SIGNATURE) { + if (publicKeyJwk.use != "sig") { didDocumentBuilder.verificationMethodForPurposes(verificationMethod, listOf(Purpose.KeyAgreement)) } return didDocumentBuilder.build() diff --git a/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DidDhtTest.kt b/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DidDhtTest.kt index 5b2361a9e..47affaadb 100644 --- a/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DidDhtTest.kt +++ b/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DidDhtTest.kt @@ -2,9 +2,6 @@ package web5.sdk.dids.methods.dht import com.fasterxml.jackson.core.type.TypeReference import com.fasterxml.jackson.databind.annotation.JsonDeserialize -import com.nimbusds.jose.jwk.Curve -import com.nimbusds.jose.jwk.JWK -import com.nimbusds.jose.jwk.gen.ECKeyGenerator import io.ktor.client.engine.mock.MockEngine import io.ktor.client.engine.mock.respond import io.ktor.http.HttpMethod @@ -21,6 +18,7 @@ import web5.sdk.common.Json import web5.sdk.common.ZBase32 import web5.sdk.crypto.AlgorithmId import web5.sdk.crypto.InMemoryKeyManager +import web5.sdk.crypto.jwk.Jwk import web5.sdk.dids.DidResolutionResult import web5.sdk.dids.JwkDeserializer import web5.sdk.dids.PurposesDeserializer @@ -117,9 +115,11 @@ class DidDhtTest { val manager = InMemoryKeyManager() val otherKey = manager.generatePrivateKey(AlgorithmId.secp256k1) - val publicKeyJwk = manager.getPublicKey(otherKey).toPublicJWK() - val publicKeyJwk2 = ECKeyGenerator(Curve.P_256).generate().toPublicJWK() - val verificationMethodsToAdd: Iterable, String?>> = listOf( + val publicKeyJwk = manager.getPublicKey(otherKey) + // todo this was ECKeyGenerator(Curve.P_256).generate().toPublicJWK() before + // is this the right equivalent? + val publicKeyJwk2 = Jwk.Builder().keyType("EC").build() + val verificationMethodsToAdd: Iterable, String?>> = listOf( Triple( publicKeyJwk, listOf(Purpose.Authentication, Purpose.AssertionMethod), @@ -239,8 +239,6 @@ class DidDhtTest { val manager = InMemoryKeyManager() val did = DidDht.create(manager, CreateDidDhtOptions(publish = false)) - require(did.document != null) - val packet = DidDht.toDnsPacket(did.document) assertNotNull(packet) @@ -256,8 +254,6 @@ class DidDhtTest { val manager = InMemoryKeyManager() val did = DidDht.create(manager, CreateDidDhtOptions(publish = false)) - require(did.document != null) - val indexes = listOf(DidDhtTypeIndexing.Corporation, DidDhtTypeIndexing.SoftwarePackage) val packet = DidDht.toDnsPacket(did.document, indexes) assertNotNull(packet) @@ -276,8 +272,8 @@ class DidDhtTest { val manager = InMemoryKeyManager() val otherKey = manager.generatePrivateKey(AlgorithmId.secp256k1) - val publicKeyJwk = manager.getPublicKey(otherKey).toPublicJWK() - val verificationMethodsToAdd: Iterable, String?>> = listOf( + val publicKeyJwk = manager.getPublicKey(otherKey) + val verificationMethodsToAdd: Iterable, String?>> = listOf( Triple(publicKeyJwk, listOf(Purpose.Authentication, Purpose.AssertionMethod), null) ) @@ -296,8 +292,6 @@ class DidDhtTest { ) val did = DidDht.create(manager, opts) - require(did.document != null) - val packet = DidDht.toDnsPacket(did.document) assertNotNull(packet) @@ -357,7 +351,7 @@ class Web5TestVectorsDidDht { data class VerificationMethodInput( @JsonDeserialize(using = JwkDeserializer::class) - val jwk: JWK, + val jwk: Jwk, @JsonDeserialize(using = PurposesDeserializer::class) val purposes: List ) diff --git a/dids/src/test/kotlin/web5/sdk/dids/methods/jwk/DidJwkTest.kt b/dids/src/test/kotlin/web5/sdk/dids/methods/jwk/DidJwkTest.kt index d2bb75ad1..38c1e3ef9 100644 --- a/dids/src/test/kotlin/web5/sdk/dids/methods/jwk/DidJwkTest.kt +++ b/dids/src/test/kotlin/web5/sdk/dids/methods/jwk/DidJwkTest.kt @@ -2,8 +2,6 @@ package web5.sdk.dids.methods.jwk import com.fasterxml.jackson.core.type.TypeReference import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import com.nimbusds.jose.JWSAlgorithm -import com.nimbusds.jose.jwk.JWK import org.erdtman.jcs.JsonCanonicalizer import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test @@ -12,6 +10,8 @@ import web5.sdk.common.Convert import web5.sdk.common.Json import web5.sdk.crypto.AlgorithmId import web5.sdk.crypto.InMemoryKeyManager +import web5.sdk.crypto.Jwa +import web5.sdk.crypto.jwk.Jwk import web5.sdk.dids.DidResolutionResult import web5.sdk.dids.DidResolvers import web5.sdk.testing.TestVectors @@ -37,8 +37,7 @@ class DidJwkTest { assertNotNull(jwk) val keyAlias = did.keyManager.getDeterministicAlias(jwk) val publicKey = did.keyManager.getPublicKey(keyAlias) - - assertEquals(JWSAlgorithm.ES256K, publicKey.algorithm) + assertEquals(Jwa.ES256K.name, publicKey.alg) } } @@ -95,8 +94,8 @@ class DidJwkTest { fun `private key throws exception`() { val manager = InMemoryKeyManager() manager.generatePrivateKey(AlgorithmId.secp256k1) - val privateJwk = JWK.parse(manager.export().first()) - val encodedPrivateJwk = Convert(privateJwk.toJSONString()).toBase64Url() + val privateJwkString = Json.parse(manager.export().first().toString()) + val encodedPrivateJwk = Convert(privateJwkString).toBase64Url() val did = "did:jwk:$encodedPrivateJwk" assertThrows( diff --git a/dids/src/test/kotlin/web5/sdk/dids/methods/key/DidKeyTest.kt b/dids/src/test/kotlin/web5/sdk/dids/methods/key/DidKeyTest.kt index ffa38b8db..0b84ebde7 100644 --- a/dids/src/test/kotlin/web5/sdk/dids/methods/key/DidKeyTest.kt +++ b/dids/src/test/kotlin/web5/sdk/dids/methods/key/DidKeyTest.kt @@ -4,14 +4,13 @@ import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.readValue import com.fasterxml.jackson.module.kotlin.registerKotlinModule -import com.nimbusds.jose.JWSAlgorithm -import com.nimbusds.jose.jwk.Curve -import com.nimbusds.jose.jwk.ECKey import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertDoesNotThrow import org.junit.jupiter.api.assertThrows import web5.sdk.crypto.InMemoryKeyManager +import web5.sdk.crypto.Jwa +import web5.sdk.crypto.JwaCurve import web5.sdk.dids.DidResolvers import kotlin.test.assertEquals import kotlin.test.assertNotNull @@ -76,7 +75,7 @@ class DidKeyTest { fun `resolving a secp256k1 DID works`() { // test vector taken from: https://github.com/w3c-ccg/did-method-key/blob/main/test-vectors/secp256k1.json#L202C4-L257 val did = "did:key:zQ3shjmnWpSDEbYKpaFm4kTs9kXyqG6N2QwCYHNPP4yubqgJS" - val result = DidKey.resolve(did) + val result = DidKey.resolve(did, null) assertNotNull(result) val didDocument = result.didDocument @@ -104,12 +103,12 @@ class DidKeyTest { assertNotNull(verificationMethod.publicKeyJwk) val publicKeyJwk = verificationMethod.publicKeyJwk // validates - assertTrue(publicKeyJwk is ECKey) + assertTrue(publicKeyJwk?.kty == "EC") - assertEquals(publicKeyJwk.algorithm, JWSAlgorithm.ES256K) - assertEquals(Curve.SECP256K1, publicKeyJwk.curve) - assertEquals("TEIJN9vnTq1EXMkqzo7yN_867-foKc2pREv45Fw_QA8", publicKeyJwk.x.toString()) - assertEquals("9yiymlzdxKCiRbYq7p-ArRB-C1ytjHE-eb7RDTi6rVc", publicKeyJwk.y.toString()) + assertEquals(publicKeyJwk?.alg, Jwa.ES256K.name) + assertEquals(JwaCurve.secp256k1.name, publicKeyJwk?.crv) + assertEquals("TEIJN9vnTq1EXMkqzo7yN_867-foKc2pREv45Fw_QA8", publicKeyJwk?.x.toString()) + assertEquals("9yiymlzdxKCiRbYq7p-ArRB-C1ytjHE-eb7RDTi6rVc", publicKeyJwk?.y.toString()) } } diff --git a/dids/src/test/kotlin/web5/sdk/dids/methods/util/TestUtils.kt b/dids/src/test/kotlin/web5/sdk/dids/methods/util/TestUtils.kt index c5addfc54..b23afb62e 100644 --- a/dids/src/test/kotlin/web5/sdk/dids/methods/util/TestUtils.kt +++ b/dids/src/test/kotlin/web5/sdk/dids/methods/util/TestUtils.kt @@ -1,11 +1,12 @@ package web5.sdk.dids.methods.util -import com.nimbusds.jose.jwk.JWK +import web5.sdk.common.Json +import web5.sdk.crypto.jwk.Jwk import java.io.File -fun readKey(pathname: String): JWK { - return JWK.parse( +fun readKey(pathname: String): Jwk { + return Json.parse( File(pathname).readText() ) } \ No newline at end of file diff --git a/jose/build.gradle.kts b/jose/build.gradle.kts new file mode 100644 index 000000000..0af9ed606 --- /dev/null +++ b/jose/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + id("org.jetbrains.kotlin.jvm") + id("java-library") +} + +repositories { + mavenCentral() +} + +dependencies { + + // Project + implementation(project(":common")) + implementation(project(":crypto")) + implementation(project(":dids")) + + // Test + testImplementation(kotlin("test")) + testImplementation(project(":testing")) +} \ No newline at end of file diff --git a/dids/src/main/kotlin/web5/sdk/dids/jws/Jws.kt b/jose/src/main/kotlin/web5/sdk/jose/jws/Jws.kt similarity index 78% rename from dids/src/main/kotlin/web5/sdk/dids/jws/Jws.kt rename to jose/src/main/kotlin/web5/sdk/jose/jws/Jws.kt index a2b76098c..d3a034fc4 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/jws/Jws.kt +++ b/jose/src/main/kotlin/web5/sdk/jose/jws/Jws.kt @@ -1,4 +1,4 @@ -package web5.sdk.dids.jws +package web5.sdk.jose.jws import web5.sdk.common.Convert import web5.sdk.common.EncodingFormat @@ -62,8 +62,7 @@ public object Jws { ?: JwsHeader.Builder() .type("JWT") .keyId(kid) - // todo pretty sure i need algorithm names like ES256K for secp and EdDSA for ed25519 - .algorithm(Crypto.getJwkCurve(verificationMethod.publicKeyJwk)?.name!!) + .algorithm(Crypto.getJwkCurve(verificationMethod.publicKeyJwk!!)?.name!!) .build() val headerBase64Url = Convert(jwsHeader).toBase64Url() @@ -129,23 +128,13 @@ public class JwsHeader( } public companion object { - // todo do i need these toJson and fromJson? - // i could just call Json.jsonMapper.xyz() - public fun toJson(header: JwsHeader): String { - return Json.stringify(header) - } - - public fun fromJson(jsonHeader: String): JwsHeader { - return Json.parse(jsonHeader) - } - public fun fromBase64Url(base64EncodedHeader: String): JwsHeader { val jsonHeaderDecoded = Convert(base64EncodedHeader, EncodingFormat.Base64Url).toStr() return Json.parse(jsonHeaderDecoded) } public fun toBase64Url(header: JwsHeader): String { - val jsonHeader = toJson(header) + val jsonHeader = Json.stringify(header) return Convert(jsonHeader, EncodingFormat.Base64Url).toBase64Url() } } @@ -162,35 +151,27 @@ public class DecodedJws( "Malformed JWS. Expected header to contain kid and alg." } - val dereferenceResult = DidResolvers.resolve(header.kid!!) + val resolutionResult = DidResolvers.resolve(header.kid!!) - check(dereferenceResult.didResolutionMetadata.error != null) { + check(resolutionResult.didResolutionMetadata.error != null) { "Verification failed. Failed to resolve kid. " + - "Error: ${dereferenceResult.didResolutionMetadata.error}" + "Error: ${resolutionResult.didResolutionMetadata.error}" + } + + check(resolutionResult.didDocument != null) { + "Verification failed. Expected header kid to dereference a DID document" } - check(dereferenceResult.didDocument!!.verificationMethod?.size != 0) { + check(resolutionResult.didDocument?.verificationMethod?.size != 0) { "Verification failed. Expected header kid to dereference a verification method" } - /* - // todo - in JwtUtil, we had this - val verificationMethodIds = setOf( - did.url, - "#${did.fragment}" - ) - and were checking the assertionMethod list for a match of the id - and then find the vm that matches the id - but here we are just looking for the first? - */ - val verificationMethod = dereferenceResult.didDocument.verificationMethod!!.first() + val verificationMethod = resolutionResult.didDocument!!.findAssertionMethodById(header.kid) check(verificationMethod.publicKeyJwk != null) { "Verification failed. Expected headeder kid to dereference" + " a verification method with a publicKeyJwk" } - // todo add this bit in coz it was in JwtUtil.verify() check(verificationMethod.type == "JsonWebKey2020" || verificationMethod.type == "JsonWebKey") { "Verification failed. Expected header kid to dereference " + "a verification method of type JsonWebKey2020 or JsonWebKey" @@ -199,7 +180,7 @@ public class DecodedJws( val toSign = "${parts[0]}.${parts[1]}" val toSignBytes = Convert(toSign).toByteArray() - Crypto.verify(verificationMethod.publicKeyJwk, toSignBytes, signature) + Crypto.verify(verificationMethod.publicKeyJwk!!, toSignBytes, signature) } } \ No newline at end of file diff --git a/dids/src/main/kotlin/web5/sdk/dids/jwt/Jwt.kt b/jose/src/main/kotlin/web5/sdk/jose/jwt/Jwt.kt similarity index 93% rename from dids/src/main/kotlin/web5/sdk/dids/jwt/Jwt.kt rename to jose/src/main/kotlin/web5/sdk/jose/jwt/Jwt.kt index b46680238..6a1c9fe84 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/jwt/Jwt.kt +++ b/jose/src/main/kotlin/web5/sdk/jose/jwt/Jwt.kt @@ -1,12 +1,13 @@ -package web5.sdk.dids.jwt +package web5.sdk.jose.jwt import com.fasterxml.jackson.databind.JsonNode import web5.sdk.common.Convert +import web5.sdk.common.EncodingFormat import web5.sdk.common.Json import web5.sdk.dids.did.BearerDid -import web5.sdk.dids.jws.DecodedJws -import web5.sdk.dids.jws.Jws -import web5.sdk.dids.jws.JwsHeader +import web5.sdk.jose.jws.DecodedJws +import web5.sdk.jose.jws.Jws +import web5.sdk.jose.jws.JwsHeader public object Jwt { @@ -16,7 +17,6 @@ public object Jwt { val claims: JwtClaimsSet try { val payload = Convert(decodedJws.payload).toStr() - // todo don't know if this is correct claims = JwtClaimsSet.fromJson(Json.jsonMapper.readTree(payload)) } catch (e: Exception) { throw IllegalArgumentException( @@ -57,8 +57,7 @@ public class DecodedJwt( public fun verify() { val decodedJws = DecodedJws( header = header, - // todo why does this use parts[1] instead of claims? - payload = Convert(parts[1]).toByteArray(), + payload = Convert(parts[1], EncodingFormat.Base64Url).toByteArray(), signature = signature, parts = parts ) diff --git a/settings.gradle.kts b/settings.gradle.kts index dbfd46180..4bffd5ea4 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,3 +1,7 @@ +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "0.5.0" +} rootProject.name = "web5" include("common", "crypto", "dids", "credentials") include("testing") +include("jose") From 9a77e89929b98ddeec47d6c3d5d5bb987e4eaff4 Mon Sep 17 00:00:00 2001 From: Jiyoon Koo Date: Thu, 14 Mar 2024 09:46:40 -0400 Subject: [PATCH 16/47] goodbye, changemedid --- .../sdk/credentials/VerifiableCredential.kt | 2 +- .../sdk/credentials/VerifiablePresentation.kt | 2 +- .../main/kotlin/web5/sdk/dids/DidMethod.kt | 134 +----------------- .../web5/sdk/dids/extensions/DidExtensions.kt | 25 ---- .../web5/sdk/dids/methods/key/DidKeyTest.kt | 3 +- 5 files changed, 5 insertions(+), 161 deletions(-) delete mode 100644 dids/src/main/kotlin/web5/sdk/dids/extensions/DidExtensions.kt diff --git a/credentials/src/main/kotlin/web5/sdk/credentials/VerifiableCredential.kt b/credentials/src/main/kotlin/web5/sdk/credentials/VerifiableCredential.kt index d584f590f..7edf33100 100644 --- a/credentials/src/main/kotlin/web5/sdk/credentials/VerifiableCredential.kt +++ b/credentials/src/main/kotlin/web5/sdk/credentials/VerifiableCredential.kt @@ -51,7 +51,7 @@ public class VerifiableCredential internal constructor(public val vcDataModel: V * If the [assertionMethodId] is null, the function will attempt to use the first available verification method from * the [did]. The result is a String in a JWT format. * - * @param did The [ChangemeDid] used to sign the credential. + * @param did The [BearerDid] used to sign the credential. * @param assertionMethodId An optional identifier for the assertion method that will be used for verification of the * produced signature. * @return The JWT representing the signed verifiable credential. diff --git a/credentials/src/main/kotlin/web5/sdk/credentials/VerifiablePresentation.kt b/credentials/src/main/kotlin/web5/sdk/credentials/VerifiablePresentation.kt index 233d25cb6..f8e6f5f80 100644 --- a/credentials/src/main/kotlin/web5/sdk/credentials/VerifiablePresentation.kt +++ b/credentials/src/main/kotlin/web5/sdk/credentials/VerifiablePresentation.kt @@ -45,7 +45,7 @@ public class VerifiablePresentation internal constructor(public val vpDataModel: * If the [assertionMethodId] is null, the function will attempt to use the first available verification method from * the [bearerDid]. The result is a String in a JWT format. * - * @param bearerDid The [ChangemeDid] used to sign the credential. + * @param bearerDid The [BearerDid] used to sign the credential. * @param assertionMethodId An optional identifier for the assertion method that will be used for verification of the * produced signature. * @return The JWT representing the signed verifiable credential. diff --git a/dids/src/main/kotlin/web5/sdk/dids/DidMethod.kt b/dids/src/main/kotlin/web5/sdk/dids/DidMethod.kt index b09669d04..be5aeb3ad 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/DidMethod.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/DidMethod.kt @@ -1,37 +1,5 @@ package web5.sdk.dids -import web5.sdk.crypto.KeyManager -import web5.sdk.dids.didcore.Did -import web5.sdk.dids.exceptions.PublicKeyJwkMissingException - -/** - * A base abstraction for Decentralized Identifiers (DID) compliant with the W3C DID standard. - * - * This abstract class serves as a foundational structure upon which specific DID methods - * can be implemented. Subclasses should furnish particular method and data models adherent - * to various DID methods. - * - * @property uri The Uniform Resource Identifier (URI) string of the DID. - * @property keyManager An [KeyManager] instance managing the cryptographic keys linked to the DID. - * - * ### Example Implementation - * Implementing subclasses should provide concrete method and data models specific to a DID method: - * ``` - * class SpecificDid(uri: String, keyManager: KeyManager): Did(uri, keyManager) { - * // Implementation-specific details here. - * } - * ``` - * - * ### Additional Notes - * Implementers should adhere to the respective DID method specifications ensuring both compliance - * and interoperability across different DID networks. - */ -//public abstract class ChangemeDid(public val uri: String, public val keyManager: KeyManager) { -// public companion object { -// // static helper methods here -// } -//} - /** * Represents options during the creation of a Decentralized Identifier (DID). * @@ -82,104 +50,4 @@ public interface CreationMetadata * } * ``` */ -public interface ResolveDidOptions - -///** -// * An interface defining operations for DID methods in accordance with the W3C DID standard. -// * -// * A DID method is a specific set of rules for creating, updating, and revoking DIDs, -// * specified in a DID method specification. Different DID methods utilize different -// * consensus mechanisms, cryptographic algorithms, and registries (or none at all). -// * The purpose of `DidMethod` implementations is to provide logic tailored to a -// * particular method while adhering to the broader operations outlined in the W3C DID standard. -// * -// * Implementations of this interface should provide method-specific logic for -// * creating and resolving DIDs under a particular method. -// * -// * @param T The type of DID that this method can create and resolve, extending [ChangemeDid]. -// * -// * ### Example of a Custom DID Method Implementation: -// * ``` -// * class ExampleDidMethod : DidMethod { -// * override val methodName: String = "example" -// * -// * override fun create(keyManager: KeyManager, options: ExampleCreateDidOptions?): ExampleDid { -// * // Implementation-specific logic for creating DIDs. -// * } -// * -// * override fun resolve(didUrl: String, opts: ResolveDidOpts?): DidResolutionResult { -// * // Implementation-specific logic for resolving DIDs. -// * } -// * } -// * ``` -// * -// * ### Notes: -// * - Ensure conformance with the relevant DID method specification for accurate and -// * interoperable functionality. -// * - Ensure that cryptographic operations utilize secure and tested libraries, ensuring -// * the reliability and security of DIDs managed by this method. -// */ -//public interface DidMethod { -// /** -// * A string that specifies the name of the DID method. -// * -// * For instance, in the DID `did:example:123456`, "example" would be the method name. -// */ -// public val methodName: String -// -// /** -// * Creates a new DID. -// * -// * This function should generate a new DID according to the rules of the specific -// * method being implemented, using the provided [KeyManager] and optionally considering -// * any provided [CreateDidOptions]. -// * -// * @param keyManager An instance of [KeyManager] responsible for cryptographic operations. -// * @param options Optionally, an instance of [CreateDidOptions] providing additional options -// * or requirements for DID creation. -// * @return A new instance of type [T], representing the created DID. -// */ -// public fun create(keyManager: KeyManager, options: O? = null): T -// -// /** -// * Resolves a DID to its associated DID Document. -// * -// * This function should retrieve and return the DID Document associated with the provided -// * DID URI, in accordance with the rules and mechanisms of the specific DID method being -// * implemented, and optionally considering any provided [ResolveDidOptions]. -// * -// * @param did A string containing the DID URI to be resolved. -// * @param options Optionally, an instance of [ResolveDidOptions] providing additional options -// * or requirements for DID resolution. -// * @return An instance of [DidResolutionResult] containing the resolved DID Document and -// * any associated metadata. -// */ -// public fun resolve(did: String, options: ResolveDidOptions? = null): DidResolutionResult -// -// /** -// * Returns an instance of [T] for [uri]. This function validates that all the key material needed for signing and -// * managing the passed in [uri] exists within the provided [keyManager]. -// * -// * @param uri A string containing the DID URI to load. -// * @param keyManager An instance of [KeyManager] that should contain all the key material needed for signing and -// * managing the passed in [did]. -// * @return An instance of [T] representing the loaded DID. -// */ -// public fun load(uri: String, keyManager: KeyManager): T -//} -// -// -//// todo do i just remove this? -//internal fun DidMethod.validateKeyMaterialInsideKeyManager( -// did: String, keyManager: KeyManager) { -// require(Did.parse(did).method == methodName) { -// "did must start with the prefix \"did:$methodName\", but got $did" -// } -// val didResolutionResult = resolve(did) -// -// didResolutionResult.didDocument!!.verificationMethod?.forEach { -// val publicKeyJwk = it.publicKeyJwk ?: throw PublicKeyJwkMissingException("publicKeyJwk is null") -// val keyAlias = keyManager.getDeterministicAlias(publicKeyJwk) -// keyManager.getPublicKey(keyAlias) -// } -//} \ No newline at end of file +public interface ResolveDidOptions \ No newline at end of file diff --git a/dids/src/main/kotlin/web5/sdk/dids/extensions/DidExtensions.kt b/dids/src/main/kotlin/web5/sdk/dids/extensions/DidExtensions.kt deleted file mode 100644 index b64ee08b0..000000000 --- a/dids/src/main/kotlin/web5/sdk/dids/extensions/DidExtensions.kt +++ /dev/null @@ -1,25 +0,0 @@ -package web5.sdk.dids.extensions - -import web5.sdk.crypto.KeyManager -import web5.sdk.dids.ChangemeDid -import web5.sdk.dids.didcore.Did -import web5.sdk.dids.methods.dht.DidDht -import web5.sdk.dids.methods.jwk.DidJwk -import web5.sdk.dids.methods.key.DidKey -import web5.sdk.dids.methods.web.DidWeb - -internal val supportedMethods = mapOf( - DidKey.methodName to DidKey.Companion, - DidJwk.methodName to DidJwk.Companion, - DidDht.methodName to DidDht.Default, - DidWeb.methodName to DidWeb.Default -) - -/** - * Creates the appropriate instance for [didUri]. This function validates that all the key material needed for - * signing and managing the passed in [didUri] exists within the provided [keyManager]. This function is meant - * to be used when the method of the DID is unknown. - */ -public fun ChangemeDid.Companion.load(didUri: String, keyManager: KeyManager): ChangemeDid { - return supportedMethods.getValue(Did.parse(didUri).method).load(didUri, keyManager) -} \ No newline at end of file diff --git a/dids/src/test/kotlin/web5/sdk/dids/methods/key/DidKeyTest.kt b/dids/src/test/kotlin/web5/sdk/dids/methods/key/DidKeyTest.kt index 0b84ebde7..60778831f 100644 --- a/dids/src/test/kotlin/web5/sdk/dids/methods/key/DidKeyTest.kt +++ b/dids/src/test/kotlin/web5/sdk/dids/methods/key/DidKeyTest.kt @@ -46,7 +46,8 @@ class DidKeyTest { fun `load fails when key manager does not contain private key`() { val manager = InMemoryKeyManager() val exception = assertThrows { - DidKey.load("did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp", manager) + DidKey.import() +// DidKey.load("did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp", manager) } assertEquals("key with alias 9ZP03Nu8GrXPAUkbKNxHOKBzxPX83SShgFkRNK-f2lw not found", exception.message) } From 994582477fe20056238e54cbeec1e6d189b7affb Mon Sep 17 00:00:00 2001 From: Jiyoon Koo Date: Thu, 14 Mar 2024 10:29:04 -0400 Subject: [PATCH 17/47] updating tests to test import instead of load --- .../credentials/VerifiableCredentialTest.kt | 17 +--- .../web5/sdk/crypto/InMemoryKeyManagerTest.kt | 16 ++-- .../kotlin/web5/sdk/dids/did/BearerDid.kt | 2 +- .../web5/sdk/dids/methods/key/DidKey.kt | 2 +- .../kotlin/web5/sdk/dids/DidMethodTest.kt | 23 +----- .../web5/sdk/dids/methods/dht/DhtTest.kt | 26 ++++--- .../web5/sdk/dids/methods/jwk/DidJwkTest.kt | 37 ++++----- .../web5/sdk/dids/methods/key/DidKeyTest.kt | 77 +++++++------------ .../web5/sdk/dids/methods/web/DidWebTest.kt | 25 +++--- 9 files changed, 81 insertions(+), 144 deletions(-) diff --git a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt index 63997b054..297d6a0cc 100644 --- a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt +++ b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt @@ -11,7 +11,6 @@ import web5.sdk.crypto.AwsKeyManager import web5.sdk.crypto.InMemoryKeyManager import web5.sdk.crypto.Jwa import web5.sdk.dids.didcore.Purpose -import web5.sdk.dids.extensions.load import web5.sdk.jose.jws.JwsHeader import web5.sdk.jose.jwt.Jwt import web5.sdk.jose.jwt.JwtClaimsSet @@ -33,22 +32,12 @@ class VerifiableCredentialTest { @Ignore("Testing with a prev created ion did") fun `create a vc with a previously created DID in the key manager`() { val keyManager = AwsKeyManager() - val didUri = - "did:ion:EiCTb6TakNEaBkYK0ZVtCC26mdv8mGZ8Z7YnbsSf-kiMyg" + - ":eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiIwMzlhZTc" + - "xYy04OTZjLTQ2MzgtYjA3My0zYTQyM2IwMjhiMDEiLCJwdWJsaWNLZXlKd2siOnsiYWxnIjoiRVMyNTZLIiwiY3J2Ijoic2VjcDI1NmsxIiw" + - "ia2lkIjoiYWxpYXMvTzNmZUVhSDlaTVFmdkg3cTFkSUw3OFNxUmRJWkhnVUJlcFU3c1RtbHY1OCIsImt0eSI6IkVDIiwidXNlIjoic2lnIiw" + - "ieCI6IllwbTNZWS1oVnNqWjV2ME83aGRhZS1WVi1DRm1Ib0hldWFZODAtV08wS0UiLCJ5IjoiUnU5QlA2RzctU0lxU3E0MFdUenk5MnpiWXd" + - "aRHBuVmlDUWxRSHpNWVQzVSJ9LCJwdXJwb3NlcyI6WyJhc3NlcnRpb25NZXRob2QiXSwidHlwZSI6Ikpzb25XZWJLZXkyMDIwIn1dLCJzZXJ" + - "2aWNlcyI6W119fV0sInVwZGF0ZUNvbW1pdG1lbnQiOiJFaUNsaVVIbHBQQjE0VVpkVzk4S250aG8zV2YxRjQxOU83cFhSMGhPeFAzRkNnIn0" + - "sInN1ZmZpeERhdGEiOnsiZGVsdGFIYXNoIjoiRWlEU2FMNHZVNElzNmxDalp4YVp6Zl9lWFFMU3V5T3E5T0pNbVJHa2FFTzRCQSIsInJlY29" + - "2ZXJ5Q29tbWl0bWVudCI6IkVpQzI0TFljVEdRN1JzaDdIRUl2TXQ0MGNGbmNhZGZReTdibDNoa3k0RkxUQ2cifX0" - val issuerDid = DidDht.load(didUri, keyManager) + val issuerDid = DidDht.create(keyManager) val holderDid = DidKey.create(keyManager) val vc = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.uri, + issuer = issuerDid.did.uri, subject = holderDid.did.uri, data = StreetCredibility(localRespect = "high", legit = true) ) @@ -235,10 +224,10 @@ class Web5TestVectorsCredentials { val keyManager = InMemoryKeyManager() keyManager.import(listOf(vector.input.signerPrivateJwk!!)) + val issuerDid = DidKey.create(keyManager) // todo need to update test vectors // input should have portable did and credential // want to be able to call BearerDID.import() - val issuerDid = ChangemeDid.load(vector.input.signerDidUri!!, keyManager) val vcJwt = vc.sign(issuerDid) assertEquals(vector.output, vcJwt, vector.description) diff --git a/crypto/src/test/kotlin/web5/sdk/crypto/InMemoryKeyManagerTest.kt b/crypto/src/test/kotlin/web5/sdk/crypto/InMemoryKeyManagerTest.kt index 18ec400c8..04f5994ac 100644 --- a/crypto/src/test/kotlin/web5/sdk/crypto/InMemoryKeyManagerTest.kt +++ b/crypto/src/test/kotlin/web5/sdk/crypto/InMemoryKeyManagerTest.kt @@ -30,7 +30,7 @@ class InMemoryKeyManagerTest { val keyManager = InMemoryKeyManager() val jwk = Crypto.generatePrivateKey(AlgorithmId.secp256k1) val exception = assertThrows { - keyManager.getDeterministicAlias(jwk.toPublicJWK()) + keyManager.getDeterministicAlias(jwk) } assertTrue(exception.message!!.matches("key with alias .* not found".toRegex())) } @@ -43,7 +43,7 @@ class InMemoryKeyManagerTest { val alias = keyManager.import(jwk) val publicKey = keyManager.getPublicKey(alias) - assertEquals(jwk.toPublicJWK(), publicKey) + assertEquals(jwk, publicKey) } @Test @@ -51,24 +51,20 @@ class InMemoryKeyManagerTest { val jwk = Crypto.generatePrivateKey(AlgorithmId.secp256k1) val keyManager = InMemoryKeyManager() - val alias = keyManager.import(jwk.toPublicJWK()) + val alias = keyManager.import(jwk) - assertEquals(jwk.toPublicJWK(), keyManager.getPublicKey(alias)) + assertEquals(jwk, keyManager.getPublicKey(alias)) } @Test fun `key without kid can be imported`() { - val jwk = ECKeyGenerator(Curve.SECP256K1) - .provider( - BouncyCastleProviderSingleton.getInstance() - ) - .generate() + val jwk = Ed25519.generatePrivateKey() val keyManager = InMemoryKeyManager() val alias = keyManager.import(jwk) val publicKey = keyManager.getPublicKey(alias) - assertEquals(jwk.toPublicJWK(), publicKey) + assertEquals(jwk, publicKey) } @Test diff --git a/dids/src/main/kotlin/web5/sdk/dids/did/BearerDid.kt b/dids/src/main/kotlin/web5/sdk/dids/did/BearerDid.kt index 6682dcb12..c4b3d5554 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/did/BearerDid.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/did/BearerDid.kt @@ -25,7 +25,7 @@ public class BearerDid( val keyAlias = keyAliasResult.getOrNull() ?: throw Exception("Failed to compute key alias") val signer: DidSigner = { payload -> - keyManager.sign(keyAlias.toString(), payload) + keyManager.sign(keyAlias, payload) } return Pair(signer, verificationMethod) diff --git a/dids/src/main/kotlin/web5/sdk/dids/methods/key/DidKey.kt b/dids/src/main/kotlin/web5/sdk/dids/methods/key/DidKey.kt index 254a9a668..e2b8b551c 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/methods/key/DidKey.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/methods/key/DidKey.kt @@ -47,7 +47,7 @@ public class CreateDidKeyOptions( * * @constructor Initializes a new instance of [DidKey] with the provided [uri] and [keyManager]. */ -public class DidKey(public val uri: String, public val keyManager: KeyManager) : ChangemeDid(uri, keyManager) { +public class DidKey(public val uri: String, public val keyManager: KeyManager) { /** * Resolves the current instance's [uri] to a [DidResolutionResult], which contains the DID Document * and possible related metadata. diff --git a/dids/src/test/kotlin/web5/sdk/dids/DidMethodTest.kt b/dids/src/test/kotlin/web5/sdk/dids/DidMethodTest.kt index d05e419f0..6a9761c5b 100644 --- a/dids/src/test/kotlin/web5/sdk/dids/DidMethodTest.kt +++ b/dids/src/test/kotlin/web5/sdk/dids/DidMethodTest.kt @@ -10,7 +10,9 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import web5.sdk.crypto.InMemoryKeyManager import web5.sdk.dids.didcore.Did +import web5.sdk.dids.methods.jwk.DidJwk import web5.sdk.dids.methods.key.DidKey +import web5.sdk.dids.methods.web.DidWeb import web5.sdk.dids.methods.web.DidWebApi import java.security.SignatureException import kotlin.test.assertContains @@ -56,27 +58,10 @@ class DidMethodTest { @Test fun `findAssertionMethodById throws exception when no assertion methods are found`() { val manager = InMemoryKeyManager() - val did = DidWebApi { - engine = mockEngine() - }.load("did:web:example.com", manager) - + val did = DidJwk.create(manager) val exception = assertThrows { - did.resolve(null).didDocument!!.findAssertionMethodById("made up assertion method id") + did.document.findAssertionMethodById("made up assertion method id") } assertEquals("No assertion methods found in DID document", exception.message) } - - private fun mockEngine() = MockEngine { request -> - when (request.url.toString()) { - "https://example.com/.well-known/did.json" -> { - respond( - content = ByteReadChannel("""{"id": "did:web:example.com"}"""), - status = HttpStatusCode.OK, - headers = headersOf(HttpHeaders.ContentType, "application/json") - ) - } - - else -> throw Exception("") - } - } } \ No newline at end of file diff --git a/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DhtTest.kt b/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DhtTest.kt index a10dd02bf..0a39d628e 100644 --- a/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DhtTest.kt +++ b/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DhtTest.kt @@ -1,9 +1,5 @@ package web5.sdk.dids.methods.dht -import io.ktor.client.engine.mock.MockEngine -import io.ktor.client.engine.mock.respond -import io.ktor.http.HttpMethod -import io.ktor.http.HttpStatusCode import org.junit.jupiter.api.Assertions.assertArrayEquals import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test @@ -13,6 +9,7 @@ import web5.sdk.crypto.AlgorithmId import web5.sdk.crypto.Ed25519 import web5.sdk.crypto.InMemoryKeyManager import web5.sdk.dids.methods.dht.DhtClient.Companion.bencode +import web5.sdk.dids.methods.dht.DidDht.Default.suffix import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertTrue @@ -78,7 +75,12 @@ class DhtTest { val manager = InMemoryKeyManager() manager.import(privateKey) - val bep44SignedMessage = DhtClient.signBep44Message(manager, privateKey.keyID, seq, v) + val bep44SignedMessage = DhtClient.signBep44Message( + manager, + privateKey.kid ?: privateKey.computeThumbprint(), + seq, + v + ) assertNotNull(bep44SignedMessage) assertEquals("48656c6c6f20576f726c6421", bep44SignedMessage.v.toHexString()) @@ -140,7 +142,8 @@ class DhtTest { val diddht = DidDhtApi {} val did = diddht.create(manager) - val kid = did.document.verificationMethod?.first()?.publicKeyJwk?.keyID?.toString() + val kid = did.document.verificationMethod?.first()?.publicKeyJwk?.kid + ?: did.document.verificationMethod?.first()?.publicKeyJwk?.computeThumbprint() assertNotNull(kid) val message = did.document.let { diddht.toDnsPacket(it) } @@ -160,20 +163,21 @@ class DhtTest { val dhtClient = DhtClient() val manager = InMemoryKeyManager() val diddht = DidDhtApi {} - val did = diddht.create(manager) + val bearerDid = diddht.create(manager) - val kid = did.document.verificationMethod?.first()?.publicKeyJwk?.keyID?.toString() + val kid = bearerDid.document.verificationMethod?.first()?.publicKeyJwk?.kid + ?: bearerDid.document.verificationMethod?.first()?.publicKeyJwk?.computeThumbprint() assertNotNull(kid) - val message = did.document.let { diddht.toDnsPacket(it) } + val message = bearerDid.document.let { diddht.toDnsPacket(it) } assertNotNull(message) val bep44Message = DhtClient.createBep44PutRequest(manager, kid, message) assertNotNull(bep44Message) - assertDoesNotThrow { dhtClient.pkarrPut(did.suffix(), bep44Message) } + assertDoesNotThrow { dhtClient.pkarrPut(suffix(bearerDid.did.uri), bep44Message) } - val retrievedMessage = assertDoesNotThrow { dhtClient.pkarrGet(did.suffix()) } + val retrievedMessage = assertDoesNotThrow { dhtClient.pkarrGet(suffix(bearerDid.did.uri)) } assertNotNull(retrievedMessage) } diff --git a/dids/src/test/kotlin/web5/sdk/dids/methods/jwk/DidJwkTest.kt b/dids/src/test/kotlin/web5/sdk/dids/methods/jwk/DidJwkTest.kt index 38c1e3ef9..760be7995 100644 --- a/dids/src/test/kotlin/web5/sdk/dids/methods/jwk/DidJwkTest.kt +++ b/dids/src/test/kotlin/web5/sdk/dids/methods/jwk/DidJwkTest.kt @@ -14,11 +14,12 @@ import web5.sdk.crypto.Jwa import web5.sdk.crypto.jwk.Jwk import web5.sdk.dids.DidResolutionResult import web5.sdk.dids.DidResolvers +import web5.sdk.dids.exceptions.InvalidMethodNameException +import web5.sdk.dids.methods.dht.DidDht import web5.sdk.testing.TestVectors import java.io.File import kotlin.test.assertEquals import kotlin.test.assertNotNull -import kotlin.test.assertTrue class DidJwkTest { @Nested @@ -42,36 +43,24 @@ class DidJwkTest { } @Nested - inner class LoadTest { + inner class ImportTest { @Test - fun `throws exception when key manager does not contain private key`() { + fun `importing a portable did jwk did works`() { val manager = InMemoryKeyManager() - val exception = assertThrows { - @Suppress("MaxLineLength") - DidJwk.load( - "did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9", - manager - ) - } - assertEquals("key with alias wKIg-QPOd75_AJLdvvo-EACSpCPE5IOJu-MUpQVk1c4 not found", exception.message) + val bearerDid = DidJwk.create(manager) + val portableDid = bearerDid.export() + val importedDid = DidJwk.import(portableDid, manager) + assertEquals(bearerDid.did.uri, importedDid.did.uri) } @Test - fun `returns instance when key manager contains private key`() { + fun `importing a did with wrong method name throws exception`() { val manager = InMemoryKeyManager() - val did = DidJwk.create(manager) - val didKey = DidJwk.load(did.uri, manager) - assertEquals(did.uri, didKey.uri) - } - - @Test - fun `throws exception when loading a different type of did`() { - val manager = InMemoryKeyManager() - val did = DidJwk.create(manager) - val exception = assertThrows { - DidJwk.load(did.uri.replace("jwk", "ion"), manager) + val did = DidDht.create(manager) + val portableDid = did.export() + assertThrows { + DidJwk.import(portableDid, manager) } - assertTrue(exception.message!!.startsWith("did must start with the prefix \"did:jwk\"")) } } diff --git a/dids/src/test/kotlin/web5/sdk/dids/methods/key/DidKeyTest.kt b/dids/src/test/kotlin/web5/sdk/dids/methods/key/DidKeyTest.kt index 60778831f..bca9065c0 100644 --- a/dids/src/test/kotlin/web5/sdk/dids/methods/key/DidKeyTest.kt +++ b/dids/src/test/kotlin/web5/sdk/dids/methods/key/DidKeyTest.kt @@ -1,9 +1,5 @@ package web5.sdk.dids.methods.key -import com.fasterxml.jackson.annotation.JsonInclude -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.module.kotlin.readValue -import com.fasterxml.jackson.module.kotlin.registerKotlinModule import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertDoesNotThrow @@ -12,6 +8,8 @@ import web5.sdk.crypto.InMemoryKeyManager import web5.sdk.crypto.Jwa import web5.sdk.crypto.JwaCurve import web5.sdk.dids.DidResolvers +import web5.sdk.dids.exceptions.InvalidMethodNameException +import web5.sdk.dids.methods.dht.DidDht import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertTrue @@ -23,9 +21,9 @@ class DidKeyTest { @Test fun `it works`() { val manager = InMemoryKeyManager() - val did = DidKey.create(manager) + val bearerDid = DidKey.create(manager) - val didResolutionResult = DidResolvers.resolve(did.did.uri) + val didResolutionResult = DidResolvers.resolve(bearerDid.did.uri) assertNotNull(didResolutionResult.didDocument) val verificationMethod = didResolutionResult.didDocument!!.verificationMethod?.get(0) @@ -33,8 +31,8 @@ class DidKeyTest { val jwk = verificationMethod?.publicKeyJwk assertNotNull(jwk) - val keyAlias = did.keyManager.getDeterministicAlias(jwk) - val publicKey = did.keyManager.getPublicKey(keyAlias) + val keyAlias = bearerDid.keyManager.getDeterministicAlias(jwk) + val publicKey = bearerDid.keyManager.getPublicKey(keyAlias) assertNotNull(jwk) assertNotNull(keyAlias) assertNotNull(publicKey) @@ -42,34 +40,6 @@ class DidKeyTest { } } - @Test - fun `load fails when key manager does not contain private key`() { - val manager = InMemoryKeyManager() - val exception = assertThrows { - DidKey.import() -// DidKey.load("did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp", manager) - } - assertEquals("key with alias 9ZP03Nu8GrXPAUkbKNxHOKBzxPX83SShgFkRNK-f2lw not found", exception.message) - } - - @Test - fun `load returns instance when key manager contains private key`() { - val manager = InMemoryKeyManager() - val did = DidKey.create(manager) - val didKey = DidKey.load(did.uri, manager) - assertEquals(did.uri, didKey.uri) - } - - @Test - fun `throws exception when loading a different type of did`() { - val manager = InMemoryKeyManager() - val did = DidKey.create(manager) - val exception = assertThrows { - DidKey.load(did.uri.replace("key", "ion"), manager) - } - assertTrue(exception.message!!.startsWith("did must start with the prefix \"did:key\"")) - } - @Nested inner class ResolveTest { @Test @@ -115,26 +85,35 @@ class DidKeyTest { } @Nested - inner class ImportExportTest { + inner class ImportTest { @Test - fun `InMemoryKeyManager export then re-import doesn't throw exception`() { - val jsonMapper = ObjectMapper() - .registerKotlinModule() - .setSerializationInclusion(JsonInclude.Include.NON_NULL) - + fun `BearerDid export then re-import doesn't throw exception`() { assertDoesNotThrow { val km = InMemoryKeyManager() val bearerDid = DidKey.create(km) - val keySet = km.export() - val serializedKeySet = jsonMapper.writeValueAsString(keySet) - val didUri = bearerDid.did.uri - - val jsonKeySet: List> = jsonMapper.readValue(serializedKeySet) + val portableDid = bearerDid.export() val km2 = InMemoryKeyManager() - km2.import(jsonKeySet) + DidKey.import(portableDid, km2) + } + } - DidKey.load(uri = didUri, keyManager = km2) + @Test + fun `importing a portable did key did works`() { + val manager = InMemoryKeyManager() + val bearerDid = DidKey.create(manager) + val portableDid = bearerDid.export() + val importedDid = DidKey.import(portableDid, manager) + assertEquals(bearerDid.did.uri, importedDid.did.uri) + } + + @Test + fun `importing a did with wrong method name throws exception`() { + val manager = InMemoryKeyManager() + val did = DidDht.create(manager) + val portableDid = did.export() + assertThrows { + DidKey.import(portableDid, manager) } } } diff --git a/dids/src/test/kotlin/web5/sdk/dids/methods/web/DidWebTest.kt b/dids/src/test/kotlin/web5/sdk/dids/methods/web/DidWebTest.kt index 254031c9d..d5162e17c 100644 --- a/dids/src/test/kotlin/web5/sdk/dids/methods/web/DidWebTest.kt +++ b/dids/src/test/kotlin/web5/sdk/dids/methods/web/DidWebTest.kt @@ -72,24 +72,19 @@ class DidWebTest { } @Test - fun `load returns instance when key manager contains private key`() { + fun `importing a portabledid with existing privatekey returns same did with same key`() { val manager = InMemoryKeyManager() val privateJwk = readKey("src/test/resources/jwkEs256k1Private.json") + val kid = privateJwk.kid ?: privateJwk.computeThumbprint() manager.import(privateJwk) - DidWebApi { - engine = mockEngine() - }.load("did:web:example-with-verification-method.com", manager) - } - - @Test - fun `load throws exception when key manager does not contain private key`() { - val manager = InMemoryKeyManager() - val exception = assertThrows { - DidWebApi { - engine = mockEngine() - }.load("did:web:example-with-verification-method.com", manager) - } - assertEquals("key with alias CfveyLOfYrOhSgD66MA6PO9J5sAnj_J-Z0URcD6VGVU not found", exception.message) + val bearerDid = DidWeb.create(manager, null) + val portableDid = bearerDid.export() + + val importedBearerDid = DidWeb.import(portableDid, InMemoryKeyManager()) + assertEquals(bearerDid, importedBearerDid) + val importedDidPubKey = importedBearerDid.keyManager.getPublicKey(kid) + val originalDidPubKey = bearerDid.keyManager.getPublicKey(kid) + assertEquals(importedDidPubKey, originalDidPubKey) } @Test From f522197f9fe8c43ac3d7630be6758861958d2b6e Mon Sep 17 00:00:00 2001 From: Jiyoon Koo Date: Thu, 14 Mar 2024 15:49:10 -0400 Subject: [PATCH 18/47] wrote kt docs, fixed didresolvers, removed resolvedidoptions because not being used --- .../src/main/kotlin/web5/sdk/common/Json.kt | 12 + config/detekt.yml | 2 +- crypto/build.gradle.kts | 2 - .../main/kotlin/web5/sdk/crypto/KeyManager.kt | 25 ++ .../main/kotlin/web5/sdk/crypto/jwk/Jwk.kt | 103 ++++++- .../main/kotlin/web5/sdk/dids/DidMethod.kt | 19 -- .../main/kotlin/web5/sdk/dids/DidResolvers.kt | 27 +- .../kotlin/web5/sdk/dids/did/BearerDid.kt | 69 ++++- .../kotlin/web5/sdk/dids/did/PortableDid.kt | 12 + .../web5/sdk/dids/methods/dht/DidDht.kt | 52 ++-- .../web5/sdk/dids/methods/jwk/DidJwk.kt | 263 +++++++++--------- .../web5/sdk/dids/methods/key/DidKey.kt | 42 ++- .../web5/sdk/dids/methods/web/DidWeb.kt | 42 ++- jose/src/main/kotlin/web5/sdk/jose/jws/Jws.kt | 92 +++++- jose/src/main/kotlin/web5/sdk/jose/jwt/Jwt.kt | 121 ++++++++ 15 files changed, 672 insertions(+), 211 deletions(-) diff --git a/common/src/main/kotlin/web5/sdk/common/Json.kt b/common/src/main/kotlin/web5/sdk/common/Json.kt index 067b56d7f..199f5b824 100644 --- a/common/src/main/kotlin/web5/sdk/common/Json.kt +++ b/common/src/main/kotlin/web5/sdk/common/Json.kt @@ -47,10 +47,22 @@ public object Json { return objectWriter.writeValueAsString(obj) } + /** + * Parse a json string into a kotlin object. + * + * @param T type of the object to parse. + * @param payload JSON string to parse + * @return parsed type T + */ public inline fun parse(payload: String): T { return objectReader.readValue(payload, T::class.java) } + /** + * Parse a JSON string into a Map. + * + * @return String parsed into a Map + */ public fun String.toMap(): Map { return jsonMapper.readValue(this) } diff --git a/config/detekt.yml b/config/detekt.yml index 855c8123a..0eb0aad62 100644 --- a/config/detekt.yml +++ b/config/detekt.yml @@ -169,7 +169,7 @@ style: UselessCallOnNotNull: active: true UtilityClassWithPublicConstructor: - active: true + active: false TbdRuleset: JvmOverloadsAnnotationRule: diff --git a/crypto/build.gradle.kts b/crypto/build.gradle.kts index 8add134fd..303871d87 100644 --- a/crypto/build.gradle.kts +++ b/crypto/build.gradle.kts @@ -32,8 +32,6 @@ dependencies { // Project implementation(project(":common")) - implementation(project(":jose")) - // Implementation implementation(libs.comGoogleCryptoTink) diff --git a/crypto/src/main/kotlin/web5/sdk/crypto/KeyManager.kt b/crypto/src/main/kotlin/web5/sdk/crypto/KeyManager.kt index 169b89ec2..f6f71af8c 100644 --- a/crypto/src/main/kotlin/web5/sdk/crypto/KeyManager.kt +++ b/crypto/src/main/kotlin/web5/sdk/crypto/KeyManager.kt @@ -61,10 +61,35 @@ public interface KeyManager { public fun getDeterministicAlias(publicKey: Jwk): String } +/** + * KeyExporter is an abstraction that can be leveraged to + * implement types which intend to export keys. + * + */ public interface KeyExporter { + + /** + * ExportKey exports the key specific by the key ID from the KeyManager. + * + * @param keyId the keyId whose corresponding key to export + * @return the Jwk representation of the key + */ public fun exportKey(keyId: String): Jwk } +/** + * KeyImporter is an abstraction that can be leveraged to + * implement types which intend to import keys. + * + */ public interface KeyImporter { + + /** + * ImportKey imports the key into the KeyManager + * and returns the key alias. + * + * @param jwk the Jwk representation of the key to import + * @return the key alias of the imported key + */ public fun importKey(jwk: Jwk): String } \ No newline at end of file diff --git a/crypto/src/main/kotlin/web5/sdk/crypto/jwk/Jwk.kt b/crypto/src/main/kotlin/web5/sdk/crypto/jwk/Jwk.kt index 9e2779f78..954d4696f 100644 --- a/crypto/src/main/kotlin/web5/sdk/crypto/jwk/Jwk.kt +++ b/crypto/src/main/kotlin/web5/sdk/crypto/jwk/Jwk.kt @@ -4,6 +4,31 @@ import web5.sdk.common.Convert import web5.sdk.common.Json import java.security.MessageDigest +/** + * Represents a [JSON Web Key (JWK)](https://datatracker.ietf.org/doc/html/rfc7517). + * A JWK is a JSON object that represents a cryptographic key. This class + * provides functionalities to manage a JWK including its creation, conversion + * to and from JSON, and computing a thumbprint. + * + * Example: + * ``` + * var jwk = Jwk( + * kty: 'RSA', + * alg: 'RS256', + * use: 'sig', + * ... // other parameters + * ); + * ``` + * @property kty Represents the key type. + * @property use Represents the intended use of the public key. + * @property alg Identifies the algorithm intended for use with the key. + * @property kid Key ID, unique identifier for the key. + * @property crv Elliptic curve name for EC keys. + * @property d Private key component for EC or OKP keys. + * @property x X coordinate for EC keys, or the public key for OKP. + * @property y Y coordinate for EC keys. + * + */ public class Jwk( public val kty: String, public val use: String?, @@ -15,6 +40,16 @@ public class Jwk( public val y: String? ) { + /** + * Computes the thumbprint of the JWK. + * [Specification](https://www.rfc-editor.org/rfc/rfc7638.html). + * + * Generates a thumbprint of the JWK using SHA-256 hash function. + * The thumbprint is computed based on the key's [kty], [crv], [x], + * and [y] values. + * + * @return a Base64URL-encoded string representing the thumbprint. + */ public fun computeThumbprint(): String { val thumbprintPayload = Json.jsonMapper.createObjectNode().apply { put("crv", crv) @@ -34,6 +69,10 @@ public class Jwk( } + /** + * Builder for Jwk type. + * + */ public class Builder { private var kty: String? = null private var use: String? = null @@ -44,53 +83,109 @@ public class Jwk( private var x: String? = null private var y: String? = null + /** + * Sets key type. + * + * @param kty + * @return Builder object + */ public fun keyType(kty: String): Builder { this.kty = kty return this } + /** + * Sets key use. + * + * @param use + * @return Builder object + */ public fun keyUse(use: String): Builder { this.use = use return this } + /** + * Sets algorithm. + * + * @param alg + * @return Builder object + */ public fun algorithm(alg: String): Builder { this.alg = alg return this } + /** + * Sets key ID. + * + * @param kid + * @return Builder object + */ public fun keyId(kid: String): Builder { this.kid = kid return this } + /** + * Sets elliptic curve name. + * + * @param crv + * @return Builder object + */ public fun curve(crv: String): Builder { this.crv = crv return this } + /** + * Sets private key component. Must be base64 encoded string. + * + * @param d + * @return Builder object + */ public fun privateKey(d: String): Builder { this.d = d return this } + /** + * Sets x coordinate. Must be base64 encoded string. + * + * @param x + * @return Builder object + */ public fun x(x: String): Builder { this.x = x return this } + /** + * Sets y coordinate. Must be base64 encoded string. + * + * @param y + * @return Builder object + */ public fun y(y: String): Builder { this.y = y return this } + /** + * Builds a Jwk object. + * + * @return Jwk object + */ public fun build(): Jwk { - // todo are any of the other fields required? - // if kty ec: x y required, - // if kty ed: x required check(kty != null) { "kty is required" } + if (kty == "EC") { + check(x != null) { "x is required for EC keys" } + check(y != null) { "y is required for EC keys" } + } + if (kty == "OKP") { + check(x != null) { "x is required for OKP keys" } + } return Jwk(kty!!, use, alg, kid, crv, d, x, y) } - } } diff --git a/dids/src/main/kotlin/web5/sdk/dids/DidMethod.kt b/dids/src/main/kotlin/web5/sdk/dids/DidMethod.kt index be5aeb3ad..21392b21c 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/DidMethod.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/DidMethod.kt @@ -32,22 +32,3 @@ public interface CreateDidOptions * ``` */ public interface CreationMetadata - -/** - * Represents options during the resolution of a Decentralized Identifier (DID). - * - * Implementations of this interface may contain properties and methods that provide - * specific options or metadata during the DID resolution processes following specific - * DID method specifications. - * - * ### Usage Example: - * Implement this interface in classes where specific creation options are needed - * for different DID methods. - * - * ``` - * class ResolveDidKeyOptions : ResolveDidOptions { - * // Implementation-specific options for DID creation. - * } - * ``` - */ -public interface ResolveDidOptions \ No newline at end of file diff --git a/dids/src/main/kotlin/web5/sdk/dids/DidResolvers.kt b/dids/src/main/kotlin/web5/sdk/dids/DidResolvers.kt index f3898778d..400153c93 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/DidResolvers.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/DidResolvers.kt @@ -1,43 +1,46 @@ package web5.sdk.dids import web5.sdk.dids.didcore.Did -import web5.sdk.dids.extensions.supportedMethods +import web5.sdk.dids.methods.dht.DidDht +import web5.sdk.dids.methods.jwk.DidJwk +import web5.sdk.dids.methods.key.DidKey +import web5.sdk.dids.methods.web.DidWeb /** * Type alias for a DID resolver function. * A DID resolver takes a DID URL as input and returns a [DidResolutionResult]. * * @param didUrl The DID URL to resolve. - * @param options (Optional) Options for resolving the DID. * @return A [DidResolutionResult] containing the resolved DID document or an error message. */ -public typealias DidResolver = (String, ResolveDidOptions?) -> DidResolutionResult +public typealias DidResolver = (String) -> DidResolutionResult /** * Singleton object representing a collection of DID resolvers. */ public object DidResolvers { - // A mutable map to store method-specific DID resolvers. - private val methodResolvers = supportedMethods.entries.associate { - it.key to it.value::resolve as DidResolver - }.toMutableMap() + private val methodResolvers = mutableMapOf( + DidKey.methodName to DidKey.Companion::resolve, + DidJwk.methodName to DidJwk::resolve, + DidDht.methodName to DidDht.Default::resolve, + DidWeb.methodName to DidWeb.Default::resolve + ) /** * Resolves a DID URL using an appropriate resolver based on the DID method. * * @param didUrl The DID URL to resolve. - * @param options (Optional) Options for resolving the DID. * @return A [DidResolutionResult] containing the resolved DID document or an error message. * @throws IllegalArgumentException if resolving the specified DID method is not supported. */ - public fun resolve(didUrl: String, options: ResolveDidOptions? = null): DidResolutionResult { + public fun resolve(didUrl: String): DidResolutionResult { val did = Did.parse(didUrl) - val resolver = methodResolvers.getOrElse(did.method) { + val resolver = this.methodResolvers.getOrElse(did.method) { throw IllegalArgumentException("Resolving did:${did.method} not supported") } - return resolver(didUrl, options) + return resolver(didUrl) } /** @@ -47,6 +50,6 @@ public object DidResolvers { * @param resolver The resolver function for the specified DID method. */ public fun addResolver(methodName: String, resolver: DidResolver) { - methodResolvers[methodName] = resolver + this.methodResolvers[methodName] = resolver } } diff --git a/dids/src/main/kotlin/web5/sdk/dids/did/BearerDid.kt b/dids/src/main/kotlin/web5/sdk/dids/did/BearerDid.kt index c4b3d5554..bea268f3c 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/did/BearerDid.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/did/BearerDid.kt @@ -12,12 +12,41 @@ import web5.sdk.dids.didcore.VerificationMethod public typealias DidSigner = (payload: ByteArray) -> ByteArray +/** + * Represents a Decentralized Identifier (DID) along with its DID document, key manager, metadata, + * and convenience functions. + * + * @param did The Decentralized Identifier (DID) to represent. + * @param keyManager The KeyManager instance used to manage the cryptographic keys associated with the DID. + * @param document The DID Document associated with the DID. + */ public class BearerDid( public val did: Did, public val keyManager: KeyManager, public val document: DidDocument ) { + /** + * GetSigner returns a sign method that can be used to sign a payload using a key associated to the DID. + * This function also returns the verification method needed to verify the signature. + * + * Providing the verification method allows the caller to provide the signature's recipient + * with a reference to the verification method needed to verify the payload. This is often done + * by including the verification method id either alongside the signature or as part of the header + * in the case of JSON Web Signatures. + * + * The verifier can dereference the verification method id to obtain the public key needed to verify the signature. + * + * This function takes a Verification Method selector that can be used to select a specific verification method + * from the DID Document if desired. If no selector is provided, the payload will be signed with the key associated + * to the first verification method in the DID Document. + * + * The selector can either be a Verification Method ID or a Purpose. If a Purpose is provided, the first verification + * method in the DID Document that has the provided purpose will be used to sign the payload. + * + * The returned signer is a function that takes a byte payload and returns a byte signature. + */ + @JvmOverloads public fun getSigner(selector: VMSelector? = null): Pair { val verificationMethod = document.selectVerificationMethod(selector) @@ -31,6 +60,18 @@ public class BearerDid( return Pair(signer, verificationMethod) } + /** + * Converts a `BearerDid` object to a portable format containing the URI and verification methods + * associated with the DID. + * + * This method is useful when you need to represent the key material and metadata associated with + * a DID in format that can be used independently of the specific DID method implementation. It + * extracts both public and private keys from the DID's key manager and organizes them into a + * `PortableDid` structure. + * + * @returns A `PortableDid` containing the URI, DID document, metadata, and optionally private + * keys associated with the `BearerDid`. + */ public fun export(): PortableDid { val keyExporter = keyManager as? KeyExporter @@ -56,31 +97,45 @@ public class BearerDid( public companion object { + + /** + * Instantiates a [BearerDid] object from a given [PortableDid]. + * + * This method allows for the creation of a `BearerDid` object using a previously created DID's + * key material, DID document, and metadata. + * + * @param portableDid - The PortableDid object to import. + * @param keyManager - Optionally specify an external Key Management System (KMS) used to + * generate keys and sign data. If not given, a new + * [LocalKeyManager] instance will be created and used. + * @returns [BearerDid] object representing the DID formed from the + * provided PortableDid. + */ + @JvmOverloads public fun import( - portableDID: PortableDid, + portableDid: PortableDid, keyManager: KeyManager = InMemoryKeyManager() ): BearerDid { - check(portableDID.document.verificationMethod?.size != 0) { + check(portableDid.document.verificationMethod?.size != 0) { "PortableDID must contain at least one verification method" } val allVerificationMethodsHavePublicKey = - portableDID.document.verificationMethod + portableDid.document.verificationMethod ?.all { vm -> vm.publicKeyJwk != null } ?: false - check(allVerificationMethodsHavePublicKey) { "Each VerificationMethod must contain a public key in Jwk format." } - val did = Did.parse(portableDID.uri) + val did = Did.parse(portableDid.uri) - for (key in portableDID.privateKeys) { + for (key in portableDid.privateKeys) { val keyImporter = keyManager as? KeyImporter keyImporter!!.importKey(key) } - return BearerDid(did, keyManager, portableDID.document) + return BearerDid(did, keyManager, portableDid.document) } } diff --git a/dids/src/main/kotlin/web5/sdk/dids/did/PortableDid.kt b/dids/src/main/kotlin/web5/sdk/dids/did/PortableDid.kt index a8d88d3bc..c337f3e68 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/did/PortableDid.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/did/PortableDid.kt @@ -3,6 +3,18 @@ package web5.sdk.dids.did import web5.sdk.crypto.jwk.Jwk import web5.sdk.dids.didcore.DidDocument +/** + * PortableDid is a serializable BearerDid that documents the key material and metadata + * of a Decentralized Identifier (DID) to enable usage of the DID in different contexts. + * + * This format is useful for exporting, saving to a file, or importing a DID across process + * boundaries or between different DID method implementations. + * + * @property uri The URI of the DID. + * @property privateKeys The private keys associated with the PortableDid. + * @property document The DID Document associated with the PortableDid. + * @property metadata Additional metadata associated with the PortableDid. + */ public class PortableDid( public val uri: String, public val privateKeys: List, diff --git a/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt b/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt index 850d9814f..d45b67f98 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt @@ -1,6 +1,5 @@ package web5.sdk.dids.methods.dht -import com.nimbusds.jose.JWSAlgorithm import io.github.oshai.kotlinlogging.KotlinLogging import io.ktor.client.engine.HttpClientEngine import io.ktor.client.engine.okhttp.OkHttp @@ -37,7 +36,6 @@ import web5.sdk.dids.exceptions.InvalidMethodNameException import web5.sdk.dids.exceptions.PkarrRecordNotFoundException import web5.sdk.dids.exceptions.PublicKeyJwkMissingException - /** * Type indexing types as per https://tbd54566975.github.io/did-dht-method/#type-indexing */ @@ -127,6 +125,7 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) { * publishing during creation. * @return A [DidDht] instance representing the newly created "did:dht" DID. */ + @JvmOverloads public fun create(keyManager: KeyManager, options: CreateDidDhtOptions? = null): BearerDid { // TODO(gabe): enforce that provided keys are of supported types according to the did:dht spec val opts = options ?: CreateDidDhtOptions() @@ -223,19 +222,39 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) { } } - public fun import(portableDID: PortableDid, keyManager: KeyManager = InMemoryKeyManager()): BearerDid { - val parsedDid = Did.parse(portableDID.uri) + /** + * Instantiates a [BearerDid] object for the DID DHT method from a given [PortableDid]. + * + * This method allows for the creation of a `BearerDid` object using a previously created DID's + * key material, DID document, and metadata. + * + * @param portableDid - The PortableDid object to import. + * @param keyManager - Optionally specify an external Key Management System (KMS) used to + * generate keys and sign data. If not given, a new + * [InMemoryKeyManager] instance will be created and + * used. + * @returns a BearerDid object representing the DID formed from the + * provided PortableDid. + * @throws InvalidMethodNameException if importing incorrect DID method + * @throws IllegalStateException if PortableDid document does not contain any verification methods, + * lacks an Identity Key, or the keys for any verification method are missing in the key + * manager. + */ + @JvmOverloads + public fun import(portableDid: PortableDid, keyManager: KeyManager = InMemoryKeyManager()): BearerDid { + val parsedDid = Did.parse(portableDid.uri) if (parsedDid.method != methodName) { throw InvalidMethodNameException("Method not supported") } - val bearerDid = BearerDid.import(portableDID, keyManager) - - if (bearerDid.document.verificationMethod - ?.none { vm -> - vm.id.split("#").last() == "0" - } == true - ) { - throw IllegalStateException("DidDht DID document must contain at least one verification method with fragment of 0") + val bearerDid = BearerDid.import(portableDid, keyManager) + + val containsOneVmWithFragmentId0 = bearerDid.document.verificationMethod + ?.none { vm -> + vm.id.split("#").last() == "0" + } ?: false + + check(containsOneVmWithFragmentId0) { + "DidDht DID document must contain at least one verification method with fragment of 0" } return bearerDid } @@ -695,11 +714,6 @@ public class DidDht( public val didDocument: DidDocument? = null ) { - /** - * Default companion object for creating a [DidDhtApi] with a default configuration. - */ - public companion object Default : DidDhtApi(DidDhtConfiguration()) - /** * Calls [DidDht.suffix] with the provided [id] and returns the result. */ @@ -732,4 +746,8 @@ public class DidDht( return DidDht.fromDnsPacket(did, msg) } + /** + * Default companion object for creating a [DidDhtApi] with a default configuration. + */ + public companion object Default : DidDhtApi(DidDhtConfiguration()) } \ No newline at end of file diff --git a/dids/src/main/kotlin/web5/sdk/dids/methods/jwk/DidJwk.kt b/dids/src/main/kotlin/web5/sdk/dids/methods/jwk/DidJwk.kt index f866167db..930f58477 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/methods/jwk/DidJwk.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/methods/jwk/DidJwk.kt @@ -28,145 +28,154 @@ import java.text.ParseException * to be self-verifiable by third parties. This eradicates the necessity for a separate blockchain or ledger. * Further specifics and technical details are outlined in [the DID Jwk Spec](https://example.org/did-method-jwk/). * - * @property uri The URI of the "did:jwk" which conforms to the DID standard. - * @property keyManager A [keyManager] instance utilized to manage the cryptographic keys associated with the DID. - * - * @constructor Initializes a new instance of [DidJwk] with the provided [uri] and [keyManager]. */ -// todo can we make this an object -public class DidJwk { - - public companion object { - public const val methodName: String = "jwk" - - /** - * Creates a new "did:jwk" DID, derived from a public key, and stores the associated private key in the - * provided [keyManager]. - * - * The method-specific identifier of a "did:jwk" DID is a base64url encoded json web key serialized as a UTF-8 - * string. - * - * **Note**: Defaults to ES256K if no options are provided - * - * @param keyManager A [keyManager] instance where the new key will be stored. - * @return A [DidJwk] instance representing the newly created "did:jwk" DID. - * - * @throws UnsupportedOperationException if the specified curve is not supported. - */ - // todo look into whether params can be nullable if providing default values - public fun create( - keyManager: KeyManager = InMemoryKeyManager(), - algorithmId: AlgorithmId = AlgorithmId.Ed25519): BearerDid { - - val keyAlias = keyManager.generatePrivateKey(algorithmId) - val publicKeyJwk = keyManager.getPublicKey(keyAlias) - - val base64Encoded = Convert(Json.stringify(publicKeyJwk)).toBase64Url() - - val didUri = "did:jwk:$base64Encoded" - - val did = Did(method = methodName, uri = didUri, url = didUri, id = base64Encoded) - - return BearerDid(did, keyManager, createDocument(did, publicKeyJwk)) +public object DidJwk { + + public const val methodName: String = "jwk" + + /** + * Creates a new "did:jwk" DID, derived from a public key, and stores the associated private key in the + * provided [keyManager]. + * + * The method-specific identifier of a "did:jwk" DID is a base64url encoded json web key serialized as a UTF-8 + * string. + * + * **Note**: Defaults to ES256K if no options are provided + * + * @param keyManager A [keyManager] instance where the new key will be stored. + * @return A [DidJwk] instance representing the newly created "did:jwk" DID. + * + * @throws UnsupportedOperationException if the specified curve is not supported. + */ + @JvmOverloads + public fun create( + keyManager: KeyManager = InMemoryKeyManager(), + algorithmId: AlgorithmId = AlgorithmId.Ed25519): BearerDid { + + val keyAlias = keyManager.generatePrivateKey(algorithmId) + val publicKeyJwk = keyManager.getPublicKey(keyAlias) + + val base64Encoded = Convert(Json.stringify(publicKeyJwk)).toBase64Url() + + val didUri = "did:jwk:$base64Encoded" + + val did = Did(method = methodName, uri = didUri, url = didUri, id = base64Encoded) + return BearerDid(did, keyManager, createDocument(did, publicKeyJwk)) + + } + + /** + * Instantiates a [BearerDid] object for the DID JWK method from a given [PortableDid]. + * + * This method allows for the creation of a `BearerDid` object using a previously created DID's + * key material, DID document, and metadata. + * + * @param portableDid - The PortableDid object to import. + * @param keyManager - Optionally specify an external Key Management System (KMS) used to + * generate keys and sign data. If not given, a new + * [InMemoryKeyManager] instance will be created and + * used. + * @returns a BearerDid object representing the DID formed from the + * provided PortableDid. + * @throws InvalidMethodNameException if importing incorrect DID method + */ + public fun import(portableDid: PortableDid, keyManager: KeyManager = InMemoryKeyManager()): BearerDid { + val parsedDid = Did.parse(portableDid.uri) + if (parsedDid.method != methodName) { + throw InvalidMethodNameException("Method not supported") } - public fun import(portableDID: PortableDid, keyManager: KeyManager = InMemoryKeyManager()): BearerDid { - val parsedDid = Did.parse(portableDID.uri) - if (parsedDid.method != methodName) { - throw InvalidMethodNameException("Method not supported") - } - val bearerDid = BearerDid.import(portableDID, keyManager) - - if (bearerDid.document.verificationMethod?.size == 0) { - throw IllegalStateException("DidJwk DID document must contain exactly one verification method") - } - return bearerDid + val bearerDid = BearerDid.import(portableDid, keyManager) + + check(bearerDid.document.verificationMethod?.size != 0) { + "DidJwk DID document must contain exactly one verification method" } + return bearerDid + } - /** - * Resolves a "did:jwk" DID into a [DidResolutionResult], which contains the DID Document and possible related metadata. - * - * This implementation primarily constructs a DID Document with a single verification method derived - * from the DID's method-specific identifier (the public key). - * - * @param did The "did:jwk" DID that needs to be resolved. - * @return A [DidResolutionResult] instance containing the DID Document and related context. - * - * @throws IllegalArgumentException if the provided DID does not conform to the "did:jwk" method. - */ - public fun resolve(did: String): DidResolutionResult { - val parsedDid = try { - Did.parse(did) - } catch (_: ParserException) { - return DidResolutionResult( - context = "https://w3id.org/did-resolution/v1", - didResolutionMetadata = DidResolutionMetadata( - error = ResolutionError.INVALID_DID.value, - ), - ) - } - - if (parsedDid.method != methodName) { - return DidResolutionResult( - context = "https://w3id.org/did-resolution/v1", - didResolutionMetadata = DidResolutionMetadata( - error = ResolutionError.METHOD_NOT_SUPPORTED.value, - ), - ) - } - - val id = parsedDid.id - val decodedKey = Convert(id, EncodingFormat.Base64Url).toStr() - val publicKeyJwk = try { - Json.parse(decodedKey) - } catch (_: ParseException) { - return DidResolutionResult( - context = "https://w3id.org/did-resolution/v1", - didResolutionMetadata = DidResolutionMetadata( - error = ResolutionError.INVALID_DID.value - ) - ) - } + /** + * Resolves a "did:jwk" DID into a [DidResolutionResult], which contains the DID Document and possible related metadata. + * + * This implementation primarily constructs a DID Document with a single verification method derived + * from the DID's method-specific identifier (the public key). + * + * @param did The "did:jwk" DID that needs to be resolved. + * @return A [DidResolutionResult] instance containing the DID Document and related context. + * + * @throws IllegalArgumentException if the provided DID does not conform to the "did:jwk" method. + */ + public fun resolve(did: String): DidResolutionResult { + val parsedDid = try { + Did.parse(did) + } catch (_: ParserException) { + return DidResolutionResult( + context = "https://w3id.org/did-resolution/v1", + didResolutionMetadata = DidResolutionMetadata( + error = ResolutionError.INVALID_DID.value, + ), + ) + } - require(publicKeyJwk.d != null) { - throw IllegalArgumentException("decoded jwk value cannot be a private key") - } + if (parsedDid.method != methodName) { + return DidResolutionResult( + context = "https://w3id.org/did-resolution/v1", + didResolutionMetadata = DidResolutionMetadata( + error = ResolutionError.METHOD_NOT_SUPPORTED.value, + ), + ) + } - val didDocument = createDocument(parsedDid, publicKeyJwk) + val id = parsedDid.id + val decodedKey = Convert(id, EncodingFormat.Base64Url).toStr() + val publicKeyJwk = try { + Json.parse(decodedKey) + } catch (_: ParseException) { + return DidResolutionResult( + context = "https://w3id.org/did-resolution/v1", + didResolutionMetadata = DidResolutionMetadata( + error = ResolutionError.INVALID_DID.value + ) + ) + } - return DidResolutionResult(didDocument = didDocument, context = "https://w3id.org/did-resolution/v1") + require(publicKeyJwk.d != null) { + throw IllegalArgumentException("decoded jwk value cannot be a private key") } - private fun createDocument(did: Did, publicKeyJwk: Jwk): DidDocument { - val verificationMethodId = "${did.uri}#0" - val verificationMethod = VerificationMethod.Builder() - .id(verificationMethodId) - .publicKeyJwk(publicKeyJwk) - .controller(did.url) - .type("JsonWebKey2020") - .build() - - val didDocumentBuilder = DidDocument.Builder() - .context(listOf("https://www.w3.org/ns/did/v1")) - .id(did.url) - if (publicKeyJwk.use != "enc") { - didDocumentBuilder - .verificationMethodForPurposes( - verificationMethod, - listOf( - Purpose.AssertionMethod, - Purpose.Authentication, - Purpose.CapabilityDelegation, - Purpose.CapabilityInvocation - ) - ) - } + val didDocument = createDocument(parsedDid, publicKeyJwk) - if (publicKeyJwk.use != "sig") { - didDocumentBuilder.verificationMethodForPurposes(verificationMethod, listOf(Purpose.KeyAgreement)) - } - return didDocumentBuilder.build() + return DidResolutionResult(didDocument = didDocument, context = "https://w3id.org/did-resolution/v1") + } + + private fun createDocument(did: Did, publicKeyJwk: Jwk): DidDocument { + val verificationMethodId = "${did.uri}#0" + val verificationMethod = VerificationMethod.Builder() + .id(verificationMethodId) + .publicKeyJwk(publicKeyJwk) + .controller(did.url) + .type("JsonWebKey2020") + .build() + + val didDocumentBuilder = DidDocument.Builder() + .context(listOf("https://www.w3.org/ns/did/v1")) + .id(did.url) + if (publicKeyJwk.use != "enc") { + didDocumentBuilder + .verificationMethodForPurposes( + verificationMethod, + listOf( + Purpose.AssertionMethod, + Purpose.Authentication, + Purpose.CapabilityDelegation, + Purpose.CapabilityInvocation + ) + ) } + if (publicKeyJwk.use != "sig") { + didDocumentBuilder.verificationMethodForPurposes(verificationMethod, listOf(Purpose.KeyAgreement)) + } + return didDocumentBuilder.build() } + } \ No newline at end of file diff --git a/dids/src/main/kotlin/web5/sdk/dids/methods/key/DidKey.kt b/dids/src/main/kotlin/web5/sdk/dids/methods/key/DidKey.kt index e2b8b551c..0ee0fb225 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/methods/key/DidKey.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/methods/key/DidKey.kt @@ -5,17 +5,18 @@ import org.apache.http.MethodNotSupportedException import web5.sdk.common.Varint import web5.sdk.crypto.AlgorithmId import web5.sdk.crypto.Crypto +import web5.sdk.crypto.InMemoryKeyManager import web5.sdk.crypto.KeyManager import web5.sdk.crypto.Secp256k1 import web5.sdk.dids.CreateDidOptions import web5.sdk.dids.DidResolutionResult -import web5.sdk.dids.ResolveDidOptions import web5.sdk.dids.did.BearerDid import web5.sdk.dids.did.PortableDid import web5.sdk.dids.didcore.Did import web5.sdk.dids.didcore.DidDocument import web5.sdk.dids.didcore.Purpose import web5.sdk.dids.didcore.VerificationMethod +import web5.sdk.dids.exceptions.InvalidMethodNameException /** * Specifies options for creating a new "did:key" Decentralized Identifier (DID). @@ -57,11 +58,11 @@ public class DidKey(public val uri: String, public val keyManager: KeyManager) { * @throws IllegalArgumentException if the provided DID does not conform to the "did:key" method. */ public fun resolve(): DidResolutionResult { - return resolve(this.uri, null) + return resolve(this.uri) } public companion object { - public val methodName: String = "key" + public const val methodName: String = "key" /** * Creates a new "did:key" DID, derived from a public key, and stores the associated private key in the @@ -77,6 +78,7 @@ public class DidKey(public val uri: String, public val keyManager: KeyManager) { * * @throws UnsupportedOperationException if the specified curve is not supported. */ + @JvmOverloads public fun create(keyManager: KeyManager, options: CreateDidKeyOptions? = null): BearerDid { val opts = options ?: CreateDidKeyOptions() @@ -98,9 +100,9 @@ public class DidKey(public val uri: String, public val keyManager: KeyManager) { val didUrl = "did:key:$multibaseEncodedId" val did = Did(method = methodName, uri = didUrl, url = didUrl, id = multibaseEncodedId) - val resolutionResult = resolve(didUrl, null) - if (resolutionResult.didDocument == null) { - throw IllegalStateException("DidDocument not found") + val resolutionResult = resolve(didUrl) + check(resolutionResult.didDocument != null) { + "DidDocument not found" } return BearerDid(did, keyManager, resolutionResult.didDocument) } @@ -116,7 +118,7 @@ public class DidKey(public val uri: String, public val keyManager: KeyManager) { * * @throws IllegalArgumentException if the provided DID does not conform to the "did:key" method. */ - public fun resolve(did: String, options: ResolveDidOptions?): DidResolutionResult { + public fun resolve(did: String): DidResolutionResult { val parsedDid = Did.parse(did) require(parsedDid.method == methodName) { throw IllegalArgumentException("expected did:key") } @@ -158,16 +160,32 @@ public class DidKey(public val uri: String, public val keyManager: KeyManager) { return DidResolutionResult(didDocument = didDocument, context = "https://w3id.org/did-resolution/v1") } - public fun import(portableDID: PortableDid, keyManager: KeyManager): BearerDid { - val parsedDid = Did.parse(portableDID.uri) + /** + * Instantiates a [BearerDid] object for the DID KEY method from a given [PortableDid]. + * + * This method allows for the creation of a `BearerDid` object using a previously created DID's + * key material, DID document, and metadata. + * + * @param portableDid - The PortableDid object to import. + * @param keyManager - Optionally specify an external Key Management System (KMS) used to + * generate keys and sign data. If not given, a new + * [InMemoryKeyManager] instance will be created and + * used. + * @returns a BearerDid object representing the DID formed from the + * provided PortableDid. + * @throws InvalidMethodNameException if importing incorrect DID method + */ + @JvmOverloads + public fun import(portableDid: PortableDid, keyManager: KeyManager = InMemoryKeyManager()): BearerDid { + val parsedDid = Did.parse(portableDid.uri) if (parsedDid.method != methodName) { throw MethodNotSupportedException("Method not supported") } - val bearerDid = BearerDid.import(portableDID, keyManager) + val bearerDid = BearerDid.import(portableDid, keyManager) - if (bearerDid.document.verificationMethod?.size != 1) { - throw IllegalStateException("DidKey DID document must contain exactly one verification method") + check(bearerDid.document.verificationMethod?.size == 1) { + "DidKey DID document must contain exactly one verification method" } return bearerDid diff --git a/dids/src/main/kotlin/web5/sdk/dids/methods/web/DidWeb.kt b/dids/src/main/kotlin/web5/sdk/dids/methods/web/DidWeb.kt index aaeff8b43..9d77df50e 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/methods/web/DidWeb.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/methods/web/DidWeb.kt @@ -19,8 +19,8 @@ import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.OkHttpClient import okhttp3.dnsoverhttps.DnsOverHttps import web5.sdk.common.Json +import web5.sdk.crypto.InMemoryKeyManager import web5.sdk.crypto.KeyManager -import web5.sdk.dids.CreateDidOptions import web5.sdk.dids.didcore.Did import web5.sdk.dids.didcore.DidDocument import web5.sdk.dids.DidResolutionResult @@ -108,6 +108,24 @@ public sealed class DidWebApi( } } + /** + * Resolves a `did:jwk` URI into a [DidResolutionResult]. + * + * This method parses the provided `did` URI to extract the JWK information. + * It validates the method of the DID URI and then attempts to parse the + * JWK from the URI. If successful, it constructs a [DidDocument] with the + * resolved JWK, generating a [DidResolutionResult]. + * + * The method ensures that the DID URI adheres to the `did:jwk` method + * specification and handles exceptions that may arise during the parsing + * and resolution process. + * + * @param did did URI to parse + * @return [DidResolutionResult] containing the resolved DID document. + * If the DID URI is invalid, not conforming to the `did:jwk` method, or + * if any other error occurs during the resolution process, it returns + * an invalid [DidResolutionResult]. + */ public fun resolve(did: String): DidResolutionResult { return try { resolveInternal(did) @@ -117,8 +135,23 @@ public sealed class DidWebApi( } } - public fun import(portableDID: PortableDid, keyManager: KeyManager): BearerDid { - return BearerDid.import(portableDID, keyManager) + /** + * Instantiates a [BearerDid] object for the DID WEB method from a given [PortableDid]. + * + * This method allows for the creation of a `BearerDid` object using a previously created DID's + * key material, DID document, and metadata. + * + * @param portableDid - The PortableDid object to import. + * @param keyManager - Optionally specify an external Key Management System (KMS) used to + * generate keys and sign data. If not given, a new + * [InMemoryKeyManager] instance will be created and + * used. + * @returns a BearerDid object representing the DID formed from the + * provided PortableDid. + */ + @JvmOverloads + public fun import(portableDid: PortableDid, keyManager: KeyManager = InMemoryKeyManager()): BearerDid { + return BearerDid.import(portableDid, keyManager) } private fun resolveInternal(did: String): DidResolutionResult { @@ -168,7 +201,4 @@ public sealed class DidWebApi( return targetUrl.toString() } - public fun create(keyManager: KeyManager, options: CreateDidOptions?): BearerDid { - throw UnsupportedOperationException("Create operation is not supported for did:web") - } } \ No newline at end of file diff --git a/jose/src/main/kotlin/web5/sdk/jose/jws/Jws.kt b/jose/src/main/kotlin/web5/sdk/jose/jws/Jws.kt index d3a034fc4..afec98721 100644 --- a/jose/src/main/kotlin/web5/sdk/jose/jws/Jws.kt +++ b/jose/src/main/kotlin/web5/sdk/jose/jws/Jws.kt @@ -8,8 +8,19 @@ import web5.sdk.dids.DidResolvers import web5.sdk.dids.did.BearerDid import web5.sdk.dids.exceptions.PublicKeyJwkMissingException +/** + * Json Web Signature (JWS) is a compact signature format that is used to secure messages. + * Spec: https://datatracker.ietf.org/doc/html/rfc7515 + */ public object Jws { + /** + * Decode a JWS into its parts. + * + * @param jws The JWS to decode + * @return DecodedJws + */ + @Suppress("SwallowedException") public fun decode(jws: String): DecodedJws { val parts = jws.split(".") check(parts.size != 3) { @@ -40,6 +51,15 @@ public object Jws { return DecodedJws(header, payload, signature, parts) } + /** + * Sign a payload using a Bearer DID. + * + * @param bearerDid The Bearer DID to sign with + * @param payload The payload to sign + * @param header The JWS header + * @param detached Whether to include the payload in the JWS string output + * @return + */ public fun sign( bearerDid: BearerDid, payload: ByteArray, @@ -82,6 +102,12 @@ public object Jws { } + /** + * Verify a JWS. + * + * @param jws The JWS to verify + * @return DecodedJws + */ public fun verify(jws: String): DecodedJws { val decodedJws = decode(jws) decodedJws.verify() @@ -89,36 +115,70 @@ public object Jws { } } +/** + * JSON Web Signature (JWS) Header Parameters + * + * The Header Parameter names for use in JWSs are registered in the IANA "JSON Web Signature and + * Encryption Header Parameters" registry. + * + * Spec: https://datatracker.ietf.org/doc/html/rfc7515 + * @param typ The "typ" (type) Header Parameter is used by JWS applications to declare the media type + * @param alg The "alg" (algorithm) Header Parameter identifies the cryptographic algorithm used to + * @param kid The "kid" (key ID) Header Parameter is a hint indicating which key was used to secure + */ public class JwsHeader( public val typ: String? = null, public val alg: String? = null, public val kid: String? = null ) { - public fun toBase64Url(): String { - return toBase64Url(this) - } - + /** + * Builder for JwsHeader. + * + */ public class Builder { private var typ: String? = null private var alg: String? = null private var kid: String? = null + /** + * Sets the typ field of the JWS header. + * + * @param typ The type of the JWS + * @return Builder object + */ public fun type(typ: String): Builder { this.typ = typ return this } + /** + * Sets the alg field of the JWS header. + * + * @param alg The algorithm used to sign the JWS + * @return Builder object + */ public fun algorithm(alg: String): Builder { this.alg = alg return this } + /** + * Sets the kid field of the JWS header. + * + * @param kid The key ID used to sign the JWS + * @return Builder object + */ public fun keyId(kid: String): Builder { this.kid = kid return this } + /** + * Builds the JwsHeader object. + * + * @return JwsHeader + */ public fun build(): JwsHeader { check(typ != null) { "typ is required" } check(alg != null) { "alg is required" } @@ -128,11 +188,23 @@ public class JwsHeader( } public companion object { + /** + * Decodes a base64 encoded JWS header. + * + * @param base64EncodedHeader The base64 encoded JWS header + * @return JwsHeader + */ public fun fromBase64Url(base64EncodedHeader: String): JwsHeader { val jsonHeaderDecoded = Convert(base64EncodedHeader, EncodingFormat.Base64Url).toStr() return Json.parse(jsonHeaderDecoded) } + /** + * Encodes a JWS header to base64url string. + * + * @param header The JWS header to encode + * @return String base64url encoded JWS header + */ public fun toBase64Url(header: JwsHeader): String { val jsonHeader = Json.stringify(header) return Convert(jsonHeader, EncodingFormat.Base64Url).toBase64Url() @@ -140,12 +212,24 @@ public class JwsHeader( } } +/** + * DecodedJws is a compact JWS decoded into its parts. + * + * @property header The JWS header + * @property payload The JWS payload + * @property signature The JWS signature + * @property parts All parts that make up JWS. Each part is a base64url encoded string + */ public class DecodedJws( public val header: JwsHeader, public val payload: ByteArray, public val signature: ByteArray, public val parts: List ) { + + /** + * Verify the JWS signature is valid. + */ public fun verify() { check(header.kid != null || header.alg != null) { "Malformed JWS. Expected header to contain kid and alg." diff --git a/jose/src/main/kotlin/web5/sdk/jose/jwt/Jwt.kt b/jose/src/main/kotlin/web5/sdk/jose/jwt/Jwt.kt index 6a1c9fe84..d1fbb140f 100644 --- a/jose/src/main/kotlin/web5/sdk/jose/jwt/Jwt.kt +++ b/jose/src/main/kotlin/web5/sdk/jose/jwt/Jwt.kt @@ -9,8 +9,19 @@ import web5.sdk.jose.jws.DecodedJws import web5.sdk.jose.jws.Jws import web5.sdk.jose.jws.JwsHeader +/** + * Json Web Token (JWT) is a compact, URL-safe means of representing claims to be transferred between two parties. + * Spec: https://datatracker.ietf.org/doc/html/rfc7519 + */ public object Jwt { + /** + * Decode a JWT into its parts. + * + * @param jwt The JWT string to decode + * @return DecodedJwt + */ + @Suppress("SwallowedException") public fun decode(jwt: String): DecodedJwt { val decodedJws = Jws.decode(jwt) @@ -34,6 +45,13 @@ public object Jwt { } + /** + * Sign a JwtClaimsSet using a Bearer DID. + * + * @param did The Bearer DID to sign with + * @param payload The JwtClaimsSet payload to sign + * @return The signed JWT + */ public fun sign(did: BearerDid, payload: JwtClaimsSet): String { val header = JwtHeader(typ = "JWT") val payloadBytes = Convert(Json.stringify(payload)).toByteArray() @@ -41,6 +59,12 @@ public object Jwt { return Jws.sign(did, payloadBytes, header) } + /** + * Verify a JWT. + * + * @param jwt The JWT to verify + * @return DecodedJwt + */ public fun verify(jwt: String): DecodedJwt { val decodedJwt = decode(jwt) decodedJwt.verify() @@ -48,12 +72,24 @@ public object Jwt { } } +/** + * DecodedJwt is a compact JWT decoded into its parts. + * + * @property header The JWT header + * @property claims The JWT claims + * @property signature The JWT signature + * @property parts The JWT parts + */ public class DecodedJwt( public val header: JwtHeader, public val claims: JwtClaimsSet, public val signature: ByteArray, public val parts: List ) { + /** + * Verifies the JWT. + * + */ public fun verify() { val decodedJws = DecodedJws( header = header, @@ -67,6 +103,19 @@ public class DecodedJwt( public typealias JwtHeader = JwsHeader +/** + * Claims represents JWT (JSON Web Token) Claims + * Spec: https://datatracker.ietf.org/doc/html/rfc7519#section-4 + * + * @property iss identifies the principal that issued the + * @property sub the principal that is the subject of the JWT. + * @property aud the recipients that the JWT is intended for. + * @property exp the expiration time on or after which the JWT must not be accepted for processing. + * @property nbf the time before which the JWT must not be accepted for processing. + * @property iat the time at which the JWT was issued. + * @property jti provides a unique identifier for the JWT. + * @property misc additional claims (i.e. VerifiableCredential, VerifiablePresentation) + */ public class JwtClaimsSet( public val iss: String? = null, public val sub: String? = null, @@ -78,6 +127,13 @@ public class JwtClaimsSet( public val misc: Map = emptyMap() ) { public companion object { + + /** + * Takes a JsonNode representation of a claim and builds a JwtClaimsSet. + * + * @param jsonNode The JsonNode representation of a claim + * @return JwtClaimsSet + */ public fun fromJson(jsonNode: JsonNode): JwtClaimsSet { val reservedClaims = setOf( "iss", @@ -113,6 +169,10 @@ public class JwtClaimsSet( } } + /** + * Builder for JwtClaimsSet. + * + */ public class Builder { private var iss: String? = null private var sub: String? = null @@ -123,15 +183,76 @@ public class JwtClaimsSet( private var jti: String? = null private var misc: MutableMap = mutableMapOf() + /** + * Sets Issuer (iss) claim. + * + * @param iss The principal that issued the JWT + * @return Builder object + */ public fun issuer(iss: String): Builder = apply { this.iss = iss } + + /** + * Sets Subject (sub) claim. + * + * @param sub The principal that is the subject of the JWT + * @return Builder object + */ public fun subject(sub: String): Builder = apply { this.sub = sub } + + /** + * Sets Audience (aud) claim. + * + * @param aud The recipients that the JWT is intended for + * @return Builder object + */ public fun audience(aud: String): Builder = apply { this.aud = aud } + + /** + * Sets Expiration Time (exp) claim. + * + * @param exp The expiration time on or after which the JWT must not be accepted for processing + * @return Builder object + */ public fun expirationTime(exp: Long): Builder = apply { this.exp = exp } + + /** + * Sets Not Before (nbf) claim. + * + * @param nbf The time before which the JWT must not be accepted for processing + * @return Builder object + */ public fun notBeforeTime(nbf: Long): Builder = apply { this.nbf = nbf } + + /** + * Sets Issued At (iat) claim. + * + * @param iat The time at which the JWT was issued + * @return Builder object + */ public fun issueTime(iat: Long): Builder = apply { this.iat = iat } + + /** + * Sets JWT ID (jti) claim. + * + * @param jti The unique identifier for the JWT + * @return Builder object + */ public fun jwtId(jti: String): Builder = apply { this.jti = jti } + + /** + * Sets a custom claim. + * + * @param key The key of the custom claim + * @param value The value of the custom claim + * @return Builder object + */ public fun misc(key: String, value: Any): Builder = apply { this.misc[key] = value } + /**P + * Builds the JwtClaimsSet object. + * + * @return JwtClaimsSet + */ public fun build(): JwtClaimsSet = JwtClaimsSet(iss, sub, aud, exp, nbf, iat, jti, misc) } } From e92dbd1597c87d35fa9faf325df37c389379ce67 Mon Sep 17 00:00:00 2001 From: Jiyoon Koo Date: Thu, 14 Mar 2024 15:55:49 -0400 Subject: [PATCH 19/47] removing unnecessary tests --- .../kotlin/web5/sdk/dids/DidResolversTest.kt | 6 ++--- .../web5/sdk/dids/methods/web/DidWebTest.kt | 26 ------------------- 2 files changed, 3 insertions(+), 29 deletions(-) diff --git a/dids/src/test/kotlin/web5/sdk/dids/DidResolversTest.kt b/dids/src/test/kotlin/web5/sdk/dids/DidResolversTest.kt index e7150f2fe..1d37f3414 100644 --- a/dids/src/test/kotlin/web5/sdk/dids/DidResolversTest.kt +++ b/dids/src/test/kotlin/web5/sdk/dids/DidResolversTest.kt @@ -12,14 +12,14 @@ class DidResolversTest { // TODO: use all relevant test vectors from https://github.com/w3c-ccg/did-method-key/blob/main/test-vectors/ @Test fun `it works`() { - DidResolvers.resolve("did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp", null) + DidResolvers.resolve("did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp") } @Test fun `resolving a default dht did contains assertion method`() { val dhtDid = DidDht.create(InMemoryKeyManager(), null) - val resolutionResult = DidResolvers.resolve(dhtDid.uri) + val resolutionResult = DidResolvers.resolve(dhtDid.did.uri) assertNotNull(resolutionResult.didDocument!!.assertionMethod) } @@ -33,7 +33,7 @@ class DidResolversTest { @Test fun `addResolver adds a custom resolver`() { - val resolver: DidResolver = { _, _ -> DidResolutionResult(null, null) } + val resolver: DidResolver = { _ -> DidResolutionResult(null, null) } DidResolvers.addResolver("test", resolver) assertNotNull(DidResolvers.resolve("did:test:123")) } diff --git a/dids/src/test/kotlin/web5/sdk/dids/methods/web/DidWebTest.kt b/dids/src/test/kotlin/web5/sdk/dids/methods/web/DidWebTest.kt index d5162e17c..38ba26e8d 100644 --- a/dids/src/test/kotlin/web5/sdk/dids/methods/web/DidWebTest.kt +++ b/dids/src/test/kotlin/web5/sdk/dids/methods/web/DidWebTest.kt @@ -71,32 +71,6 @@ class DidWebTest { assertEquals("internalError", result.didResolutionMetadata.error) } - @Test - fun `importing a portabledid with existing privatekey returns same did with same key`() { - val manager = InMemoryKeyManager() - val privateJwk = readKey("src/test/resources/jwkEs256k1Private.json") - val kid = privateJwk.kid ?: privateJwk.computeThumbprint() - manager.import(privateJwk) - val bearerDid = DidWeb.create(manager, null) - val portableDid = bearerDid.export() - - val importedBearerDid = DidWeb.import(portableDid, InMemoryKeyManager()) - assertEquals(bearerDid, importedBearerDid) - val importedDidPubKey = importedBearerDid.keyManager.getPublicKey(kid) - val originalDidPubKey = bearerDid.keyManager.getPublicKey(kid) - assertEquals(importedDidPubKey, originalDidPubKey) - } - - @Test - fun `create throws exception`() { - val exception = assertThrows { - DidWebApi { - engine = mockEngine() - }.create(InMemoryKeyManager(), null) - } - assertEquals("Create operation is not supported for did:web", exception.message) - } - private fun mockEngine() = MockEngine { request -> when (request.url.toString()) { "https://example.com/.well-known/did.json" -> { From 275d6f84df5d39cf4dc13e2b4c0a7d8eec9fd723 Mon Sep 17 00:00:00 2001 From: Jiyoon Koo Date: Fri, 15 Mar 2024 02:37:21 -0400 Subject: [PATCH 20/47] fixing tests --- .../sdk/credentials/PresentationExchange.kt | 6 +- .../credentials/StatusListCredentialTest.kt | 66 +++++++++---------- .../credentials/VerifiableCredentialTest.kt | 1 + .../src/main/kotlin/web5/sdk/crypto/Crypto.kt | 25 ++++--- .../main/kotlin/web5/sdk/crypto/Ed25519.kt | 29 ++++++-- .../web5/sdk/crypto/InMemoryKeyManager.kt | 7 +- .../kotlin/web5/sdk/crypto/KeyGenerator.kt | 5 +- .../main/kotlin/web5/sdk/crypto/Secp256k1.kt | 43 +++++++----- .../src/main/kotlin/web5/sdk/crypto/Signer.kt | 4 +- .../main/kotlin/web5/sdk/crypto/jwk/Jwk.kt | 26 +++++--- .../kotlin/web5/sdk/crypto/Ed25519Test.kt | 4 +- .../web5/sdk/crypto/InMemoryKeyManagerTest.kt | 57 ++++++++-------- .../kotlin/web5/sdk/crypto/Secp256k1Test.kt | 22 +++---- .../kotlin/web5/sdk/dids/Serialization.kt | 2 +- .../web5/sdk/dids/methods/dht/DhtClient.kt | 7 +- .../web5/sdk/dids/methods/dht/DidDht.kt | 5 +- .../web5/sdk/dids/methods/jwk/DidJwk.kt | 8 +-- .../web5/sdk/dids/methods/key/DidKey.kt | 2 +- .../web5/sdk/dids/methods/web/DidWeb.kt | 6 +- .../kotlin/web5/sdk/dids/DidMethodTest.kt | 8 +-- .../dids/didcore/VerificationMethodTest.kt | 4 +- .../web5/sdk/dids/methods/dht/DidDhtTest.kt | 1 + .../web5/sdk/dids/methods/jwk/DidJwkTest.kt | 19 +----- .../web5/sdk/dids/methods/key/DidKeyTest.kt | 2 +- jose/src/main/kotlin/web5/sdk/jose/jws/Jws.kt | 4 +- jose/src/main/kotlin/web5/sdk/jose/jwt/Jwt.kt | 3 +- 26 files changed, 197 insertions(+), 169 deletions(-) diff --git a/credentials/src/main/kotlin/web5/sdk/credentials/PresentationExchange.kt b/credentials/src/main/kotlin/web5/sdk/credentials/PresentationExchange.kt index c66e1b167..e77abae14 100644 --- a/credentials/src/main/kotlin/web5/sdk/credentials/PresentationExchange.kt +++ b/credentials/src/main/kotlin/web5/sdk/credentials/PresentationExchange.kt @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode import com.networknt.schema.JsonSchema import com.nfeld.jsonpathkt.JsonPath import com.nfeld.jsonpathkt.extension.read +import web5.sdk.common.Json import web5.sdk.credentials.model.InputDescriptorMapping import web5.sdk.credentials.model.InputDescriptorV2 import web5.sdk.credentials.model.PresentationDefinitionV2 @@ -163,7 +164,7 @@ public object PresentationExchange { val vcJwtListWithNodes = vcJwtList.zip(vcJwtList.map { vcJwt -> val vc = Jwt.decode(vcJwt) - JsonPath.parse(vc.claims.toString()) + Json.jsonMapper.readTree(Json.stringify(vc.claims)) ?: throw JsonPathParseException() }) return presentationDefinition.inputDescriptors.associateWith { inputDescriptor -> @@ -197,6 +198,9 @@ public object PresentationExchange { val requiredFields = fields.filter { field -> field.optional != true } for (field in requiredFields) { + // todo path currently set to $.vc.type + // but now all vcs are in $.misc.vc.type + // however vcPayloadJson.read("$.misc.vc.type") also returns null val matchedFields = field.path.mapNotNull { path -> vcPayloadJson.read(path) } if (matchedFields.isEmpty()) { // If no matching fields are found for a required field, the VC does not satisfy this Input Descriptor. diff --git a/credentials/src/test/kotlin/web5/sdk/credentials/StatusListCredentialTest.kt b/credentials/src/test/kotlin/web5/sdk/credentials/StatusListCredentialTest.kt index 923045c73..809a9729a 100644 --- a/credentials/src/test/kotlin/web5/sdk/credentials/StatusListCredentialTest.kt +++ b/credentials/src/test/kotlin/web5/sdk/credentials/StatusListCredentialTest.kt @@ -59,8 +59,8 @@ class StatusListCredentialTest { val credWithCredStatus = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.uri, - subject = holderDid.uri, + issuer = issuerDid.did.uri, + subject = holderDid.did.uri, data = StreetCredibility(localRespect = "high", legit = true), credentialStatus = credentialStatus ) @@ -75,7 +75,7 @@ class StatusListCredentialTest { ) assertEquals(credWithCredStatus.type, "StreetCred") - assertEquals(credWithCredStatus.issuer, issuerDid.uri) + assertEquals(credWithCredStatus.issuer, issuerDid.did.uri) assertNotNull(credWithCredStatus.vcDataModel.issuanceDate) assertNotNull(credWithCredStatus.vcDataModel.credentialStatus) assertEquals(credWithCredStatus.vcDataModel.credentialSubject.claims.get("localRespect"), "high") @@ -105,8 +105,8 @@ class StatusListCredentialTest { val vc1 = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.uri, - subject = holderDid.uri, + issuer = issuerDid.did.uri, + subject = holderDid.did.uri, data = StreetCredibility(localRespect = "high", legit = true), credentialStatus = credentialStatus1 ) @@ -120,15 +120,15 @@ class StatusListCredentialTest { val vc2 = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.uri, - subject = holderDid.uri, + issuer = issuerDid.did.uri, + subject = holderDid.did.uri, data = StreetCredibility(localRespect = "high", legit = true), credentialStatus2 ) val statusListCredential = StatusListCredential.create( "revocation-id", - issuerDid.uri, + issuerDid.did.uri, StatusPurpose.REVOCATION, listOf(vc1, vc2) ) @@ -180,8 +180,8 @@ class StatusListCredentialTest { val vc1 = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.uri, - subject = holderDid.uri, + issuer = issuerDid.did.uri, + subject = holderDid.did.uri, data = StreetCredibility(localRespect = "high", legit = true), credentialStatus = credentialStatus1 ) @@ -195,8 +195,8 @@ class StatusListCredentialTest { val vc2 = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.uri, - subject = holderDid.uri, + issuer = issuerDid.did.uri, + subject = holderDid.did.uri, data = StreetCredibility(localRespect = "high", legit = true), credentialStatus2 ) @@ -204,7 +204,7 @@ class StatusListCredentialTest { val exception = assertThrows { StatusListCredential.create( "revocation-id", - issuerDid.uri, + issuerDid.did.uri, StatusPurpose.REVOCATION, listOf(vc1, vc2) ) @@ -231,8 +231,8 @@ class StatusListCredentialTest { val vc1 = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.uri, - subject = holderDid.uri, + issuer = issuerDid.did.uri, + subject = holderDid.did.uri, data = StreetCredibility(localRespect = "high", legit = true), credentialStatus = credentialStatus1 ) @@ -240,7 +240,7 @@ class StatusListCredentialTest { val exception = assertThrows { StatusListCredential.create( "revocation-id", - issuerDid.uri, + issuerDid.did.uri, StatusPurpose.REVOCATION, listOf(vc1) ) @@ -267,8 +267,8 @@ class StatusListCredentialTest { val vc1 = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.uri, - subject = holderDid.uri, + issuer = issuerDid.did.uri, + subject = holderDid.did.uri, data = StreetCredibility(localRespect = "high", legit = true), credentialStatus = credentialStatus1 ) @@ -276,7 +276,7 @@ class StatusListCredentialTest { val exception = assertThrows { StatusListCredential.create( "revocation-id", - issuerDid.uri, + issuerDid.did.uri, StatusPurpose.REVOCATION, listOf(vc1) ) @@ -303,8 +303,8 @@ class StatusListCredentialTest { val vc1 = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.uri, - subject = holderDid.uri, + issuer = issuerDid.did.uri, + subject = holderDid.did.uri, data = StreetCredibility(localRespect = "high", legit = true), credentialStatus = credentialStatus1 ) @@ -318,8 +318,8 @@ class StatusListCredentialTest { val vc2 = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.uri, - subject = holderDid.uri, + issuer = issuerDid.did.uri, + subject = holderDid.did.uri, data = StreetCredibility(localRespect = "high", legit = true), credentialStatus2 ) @@ -333,8 +333,8 @@ class StatusListCredentialTest { val vc3 = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.uri, - subject = holderDid.uri, + issuer = issuerDid.did.uri, + subject = holderDid.did.uri, data = StreetCredibility(localRespect = "high", legit = true), credentialStatus3 ) @@ -342,7 +342,7 @@ class StatusListCredentialTest { val statusListCredential = StatusListCredential.create( "revocation-id", - issuerDid.uri, + issuerDid.did.uri, StatusPurpose.REVOCATION, listOf(vc1, vc2) ) @@ -372,8 +372,8 @@ class StatusListCredentialTest { val vc1 = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.uri, - subject = holderDid.uri, + issuer = issuerDid.did.uri, + subject = holderDid.did.uri, data = StreetCredibility(localRespect = "high", legit = true), credentialStatus = credentialStatus1 ) @@ -387,8 +387,8 @@ class StatusListCredentialTest { val vc2 = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.uri, - subject = holderDid.uri, + issuer = issuerDid.did.uri, + subject = holderDid.did.uri, data = StreetCredibility(localRespect = "high", legit = true), credentialStatus2 ) @@ -396,7 +396,7 @@ class StatusListCredentialTest { val statusListCredential = StatusListCredential.create( "revocation-id", - issuerDid.uri, + issuerDid.did.uri, StatusPurpose.REVOCATION, listOf(vc1) ) @@ -442,8 +442,8 @@ class StatusListCredentialTest { val credToValidate = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.uri, - subject = holderDid.uri, + issuer = issuerDid.did.uri, + subject = holderDid.did.uri, data = StreetCredibility(localRespect = "high", legit = true), credentialStatus = credentialStatus1 ) diff --git a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt index 297d6a0cc..c90192cba 100644 --- a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt +++ b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt @@ -120,6 +120,7 @@ class VerifiableCredentialTest { val header = JwsHeader.Builder() .algorithm(Jwa.ES256K.name) + .type("JWT") .keyId(issuerDid.did.uri) .build() // A detached payload JWT diff --git a/crypto/src/main/kotlin/web5/sdk/crypto/Crypto.kt b/crypto/src/main/kotlin/web5/sdk/crypto/Crypto.kt index 5d8e779ad..c71371bda 100644 --- a/crypto/src/main/kotlin/web5/sdk/crypto/Crypto.kt +++ b/crypto/src/main/kotlin/web5/sdk/crypto/Crypto.kt @@ -1,6 +1,5 @@ package web5.sdk.crypto -import com.nimbusds.jose.jwk.JWK import web5.sdk.crypto.Crypto.generatePrivateKey import web5.sdk.crypto.Crypto.publicKeyToBytes import web5.sdk.crypto.Crypto.sign @@ -79,7 +78,7 @@ public object Crypto { */ public fun computePublicKey(privateKey: Jwk): Jwk { val rawCurve = privateKey.crv - val curve = rawCurve?.let { JwaCurve.parse(it) } + val curve = JwaCurve.parse(rawCurve) val generator = getKeyGenerator(AlgorithmId.from(curve)) return generator.computePublicKey(privateKey) @@ -109,18 +108,18 @@ public object Crypto { * Verifies a signature against a signed payload using a public key. * * This function utilizes the relevant verifier, determined by the algorithm and curve - * used in the JWK, to ensure the provided signature is valid for the signed payload + * used in the Jwk, to ensure the provided signature is valid for the signed payload * using the provided public key. The algorithm used can either be specified in the * public key Jwk or passed explicitly as a parameter. If it is not found in either, * an exception will be thrown. * * ## Note - * Algorithm **MUST** either be present on the [JWK] or be provided explicitly + * Algorithm **MUST** either be present on the [Jwk] or be provided explicitly * * @param publicKey The Jwk public key to be used for verifying the signature. * @param signedPayload The byte array data that was signed. * @param signature The signature that will be verified. - * if not provided in the JWK. Default is null. + * if not provided in the Jwk. Default is null. * * @throws IllegalArgumentException if neither the Jwk nor the explicit algorithm parameter * provides an algorithm. @@ -135,9 +134,9 @@ public object Crypto { /** - * Converts a [JWK] public key into its byte array representation. + * Converts a [Jwk] public key into its byte array representation. * - * @param publicKey A [JWK] object representing the public key to be converted. + * @param publicKey A [Jwk] object representing the public key to be converted. * @return A [ByteArray] representing the byte-level information of the provided public key. * * ### Example @@ -146,12 +145,12 @@ public object Crypto { * ``` * * ### Note - * This function assumes that the provided [JWK] contains valid curve and algorithm - * information. Malformed or invalid [JWK] objects may result in exceptions or + * This function assumes that the provided [Jwk] contains valid curve and algorithm + * information. Malformed or invalid [Jwk] objects may result in exceptions or * unexpected behavior. * * ### Throws - * - [IllegalArgumentException] If the algorithm or curve in [JWK] is not supported or invalid. + * - [IllegalArgumentException] If the algorithm or curve in [Jwk] is not supported or invalid. */ public fun publicKeyToBytes(publicKey: Jwk): ByteArray { val curve = getJwkCurve(publicKey) @@ -225,13 +224,13 @@ public object Crypto { } /** - * Extracts the cryptographic curve information from a [JWK] object. + * Extracts the cryptographic curve information from a [Jwk] object. * - * This function parses and returns the curve type used in a JWK. + * This function parses and returns the curve type used in a Jwk. * May return `null` if the curve information is not present or unsupported. * * @param jwk The Jwk object from which to extract curve information. - * @return The [JwaCurve] used in the JWK, or `null` if the curve is not defined or recognized. + * @return The [JwaCurve] used in the Jwk, or `null` if the curve is not defined or recognized. */ public fun getJwkCurve(jwk: Jwk): JwaCurve? { val rawCurve = jwk.crv diff --git a/crypto/src/main/kotlin/web5/sdk/crypto/Ed25519.kt b/crypto/src/main/kotlin/web5/sdk/crypto/Ed25519.kt index 91a0d72f9..7e2f10f97 100644 --- a/crypto/src/main/kotlin/web5/sdk/crypto/Ed25519.kt +++ b/crypto/src/main/kotlin/web5/sdk/crypto/Ed25519.kt @@ -54,13 +54,18 @@ public object Ed25519 : KeyGenerator, Signer { .keyUse(KeyUse.SIGNATURE) .generate() - return Jwk.Builder() + val jwk = Jwk.Builder() + .keyType("OKP") .algorithm(algorithm.name) + .curve(privateKey.curve.name) + .keyUse("sig") .privateKey(privateKey.d.toString()) .x(privateKey.x.toString()) - .keyUse("sig") .build() + jwk.kid = jwk.computeThumbprint() + return jwk + } /** @@ -72,11 +77,18 @@ public object Ed25519 : KeyGenerator, Signer { override fun computePublicKey(privateKey: Jwk): Jwk { require(privateKey.kty == "OKP") { "private key must be an Octet Key Pair (kty: OKP)" } - return Jwk.Builder() + val jwk = Jwk.Builder() .keyType(privateKey.kty) .algorithm(algorithm.name) + .curve(curve.name) + .apply { + privateKey.use?.let { keyUse(it) } + } .x(privateKey.x.toString()) .build() + + jwk.kid = jwk.computeThumbprint() + return jwk } override fun privateKeyToBytes(privateKey: Jwk): ByteArray { @@ -100,22 +112,27 @@ public object Ed25519 : KeyGenerator, Signer { return Jwk.Builder() .keyType("OKP") + .keyUse("sig") .algorithm(algorithm.name) + .curve(curve.name) .privateKey(base64UrlEncodedPrivateKey) .x(base64UrlEncodedPublicKey) - .keyUse("sig") .build() } override fun bytesToPublicKey(publicKeyBytes: ByteArray): Jwk { val base64UrlEncodedPublicKey = Convert(publicKeyBytes).toBase64Url() - return Jwk.Builder() + val jwk = Jwk.Builder() .keyType("OKP") .algorithm(algorithm.name) - .x(base64UrlEncodedPublicKey) + .curve(curve.name) .keyUse("sig") + .x(base64UrlEncodedPublicKey) .build() + + jwk.kid = jwk.computeThumbprint() + return jwk } override fun sign(privateKey: Jwk, payload: ByteArray, options: SignOptions?): ByteArray { diff --git a/crypto/src/main/kotlin/web5/sdk/crypto/InMemoryKeyManager.kt b/crypto/src/main/kotlin/web5/sdk/crypto/InMemoryKeyManager.kt index 1bfdc4757..037b29fa8 100644 --- a/crypto/src/main/kotlin/web5/sdk/crypto/InMemoryKeyManager.kt +++ b/crypto/src/main/kotlin/web5/sdk/crypto/InMemoryKeyManager.kt @@ -1,6 +1,5 @@ package web5.sdk.crypto -import com.nimbusds.jose.jwk.JWK import web5.sdk.common.Json import web5.sdk.common.Json.toMap import web5.sdk.crypto.jwk.Jwk @@ -99,10 +98,10 @@ public class InMemoryKeyManager : KeyManager { * @param keySet A list of key representations in map format. * @return A list of key aliases belonging to the imported keys. */ - public fun import(keySet: Iterable>): List = keySet.map { + public fun import(keySet: Iterable>): List = keySet.map {map -> // todo are all keySet.value of type Any in this case a possible Jwk? // we can just call toString() and call it good? am skeptical - val jwk = Json.parse(it.toString()) + val jwk = Json.parse(Json.stringify(map)) import(jwk) } @@ -126,6 +125,6 @@ public class InMemoryKeyManager : KeyManager { * * @return A list of key representations in map format. */ - public fun export(): List> = keyStore.map { it.value.toString().toMap() } + public fun export(): List> = keyStore.map { keyIdToJwk -> Json.stringify(keyIdToJwk.value).toMap() } } diff --git a/crypto/src/main/kotlin/web5/sdk/crypto/KeyGenerator.kt b/crypto/src/main/kotlin/web5/sdk/crypto/KeyGenerator.kt index 975ffc83a..b836d18d5 100644 --- a/crypto/src/main/kotlin/web5/sdk/crypto/KeyGenerator.kt +++ b/crypto/src/main/kotlin/web5/sdk/crypto/KeyGenerator.kt @@ -1,6 +1,5 @@ package web5.sdk.crypto -import com.nimbusds.jose.jwk.JWK import web5.sdk.crypto.jwk.Jwk /** @@ -92,12 +91,12 @@ public interface KeyGenerator { public fun publicKeyToBytes(publicKey: Jwk): ByteArray /** - * Converts a private key as bytes into a JWK. + * Converts a private key as bytes into a Jwk. */ public fun bytesToPrivateKey(privateKeyBytes: ByteArray): Jwk /** - * Converts a public key as bytes into a JWK. Applicable for asymmetric Key Generators only. + * Converts a public key as bytes into a Jwk. Applicable for asymmetric Key Generators only. * Implementers of symmetric key generators should throw an UnsupportedOperation Exception */ public fun bytesToPublicKey(publicKeyBytes: ByteArray): Jwk diff --git a/crypto/src/main/kotlin/web5/sdk/crypto/Secp256k1.kt b/crypto/src/main/kotlin/web5/sdk/crypto/Secp256k1.kt index c87047b7e..926e4b771 100644 --- a/crypto/src/main/kotlin/web5/sdk/crypto/Secp256k1.kt +++ b/crypto/src/main/kotlin/web5/sdk/crypto/Secp256k1.kt @@ -3,10 +3,8 @@ package web5.sdk.crypto import com.nimbusds.jose.JWSAlgorithm import com.nimbusds.jose.crypto.bc.BouncyCastleProviderSingleton import com.nimbusds.jose.jwk.ECKey -import com.nimbusds.jose.jwk.JWK import com.nimbusds.jose.jwk.KeyUse import com.nimbusds.jose.jwk.gen.ECKeyGenerator -import com.nimbusds.jose.util.Base64URL import org.bouncycastle.crypto.digests.SHA256Digest import org.bouncycastle.crypto.params.ECDomainParameters import org.bouncycastle.crypto.params.ECPrivateKeyParameters @@ -149,27 +147,37 @@ public object Secp256k1 : KeyGenerator, Signer { .keyUse(KeyUse.SIGNATURE) .generate() - return Jwk.Builder() - .algorithm(algorithm.name) + val jwk = Jwk.Builder() .keyType("EC") .keyUse("sig") - .privateKey(Convert(privateKey.d, EncodingFormat.Base64Url).toStr()) - .x(Convert(privateKey.d, EncodingFormat.Base64Url).toStr()) + .algorithm(algorithm.name) + .curve(privateKey.curve.name) + .privateKey(privateKey.d.toString()) + .x(privateKey.x.toString()) + .y(privateKey.y.toString()) .build() + + + jwk.kid = jwk.computeThumbprint() // or should i do privateKey.keyId? + return jwk } override fun computePublicKey(privateKey: Jwk): Jwk { validateKey(privateKey) - return Jwk.Builder() + val jwk = Jwk.Builder() .keyType(privateKey.kty) + .curve(curve.name) .algorithm(algorithm.name) .apply { privateKey.x?.let { x(it) } privateKey.y?.let { y(it) } } + .keyUse("sig") .build() + jwk.kid = jwk.computeThumbprint() + return jwk } override fun privateKeyToBytes(privateKey: Jwk): ByteArray { @@ -209,13 +217,16 @@ public object Secp256k1 : KeyGenerator, Signer { val xBytes = publicKeyBytes.sliceArray(1..32) val yBytes = publicKeyBytes.sliceArray(33..64) - return Jwk.Builder() + val jwk = Jwk.Builder() .keyType("EC") .algorithm(algorithm.name) + .curve(curve.name) + .keyUse("sig") .x(Convert(xBytes).toBase64Url()) .y(Convert(yBytes).toBase64Url()) - .keyUse("sig") .build() + jwk.kid = jwk.computeThumbprint() + return jwk } /** @@ -225,7 +236,7 @@ public object Secp256k1 : KeyGenerator, Signer { * This function is designed to generate deterministic signatures, meaning that signing the * same payload with the same private key will always produce the same signature. * - * @param privateKey The private key used for signing, provided as a `JWK` (JSON Web Key). + * @param privateKey The private key used for signing, provided as a `Jwk` (JSON Web Key). * @param payload The byte array containing the data to be signed. * Ensure that the payload is prepared appropriately, considering any necessary * hashing or formatting relevant to the application's security requirements. @@ -237,7 +248,7 @@ public object Secp256k1 : KeyGenerator, Signer { * for the signing process. */ override fun sign(privateKey: Jwk, payload: ByteArray, options: SignOptions?): ByteArray { - val privateKeyBigInt = Convert(privateKey.d, EncodingFormat.Base64Url).toStr().toBigInteger() + val privateKeyBigInt = BigInteger(1, Convert(privateKey.d, EncodingFormat.Base64Url).toByteArray()) val privateKeyParams = ECPrivateKeyParameters(privateKeyBigInt, curveParams) // generates k value deterministically using the private key and message hash, ensuring that signing the same @@ -276,7 +287,7 @@ public object Secp256k1 : KeyGenerator, Signer { * through HMAC and SHA-256, ensuring consistent verification outcomes for identical payloads * and signatures. * - * @param publicKey The public key used for verification, provided as a `JWK` (JSON Web Key). + * @param publicKey The public key used for verification, provided as a `Jwk` (JSON Web Key). * @param signedPayload The byte array containing the data that was signed. * @param signature The byte array representing the signature to be verified against the payload. * @param options Optional parameter to provide additional configuration for the verification process. @@ -315,7 +326,7 @@ public object Secp256k1 : KeyGenerator, Signer { } /** - * Validates the provided [JWK] (JSON Web Key) to ensure it conforms to the expected key type and format. + * Validates the provided [Jwk] (JSON Web Key) to ensure it conforms to the expected key type and format. * * This function checks the following: * - The key must be an instance of [ECKey]. @@ -325,7 +336,7 @@ public object Secp256k1 : KeyGenerator, Signer { * * ### Usage Example: * ``` - * val jwk: Jwk = //...obtain or generate a JWK + * val jwk: Jwk = //...obtain or generate a Jwk * try { * Secp256k1.validateKey(jwk) * // Key is valid, proceed with further operations... @@ -335,10 +346,10 @@ public object Secp256k1 : KeyGenerator, Signer { * ``` * * ### Important: - * Ensure to call this function before using a [JWK] in cryptographic operations + * Ensure to call this function before using a [Jwk] in cryptographic operations * to safeguard against invalid key usage and potential vulnerabilities. * - * @param key The [JWK] to validate. + * @param key The [Jwk] to validate. * @throws IllegalArgumentException if the key is not of type [ECKey]. */ public fun validateKey(key: Jwk) { diff --git a/crypto/src/main/kotlin/web5/sdk/crypto/Signer.kt b/crypto/src/main/kotlin/web5/sdk/crypto/Signer.kt index 51cb1656c..29c007bb9 100644 --- a/crypto/src/main/kotlin/web5/sdk/crypto/Signer.kt +++ b/crypto/src/main/kotlin/web5/sdk/crypto/Signer.kt @@ -59,11 +59,11 @@ public interface VerifyOptions * ### Usage Example: * ``` * class MySigner : Signer { - * override fun sign(privateKey: JWK, payload: Payload, options: SignOptions?): String { + * override fun sign(privateKey: Jwk , payload: Payload, options: SignOptions?): String { * // Implementation-specific signing logic. * } * - * override fun verify(publicKey: JWK, jws: String, options: VerifyOptions?) { + * override fun verify(publicKey: Jwk , jws: String, options: VerifyOptions?) { * // Implementation-specific verification logic. * } * } diff --git a/crypto/src/main/kotlin/web5/sdk/crypto/jwk/Jwk.kt b/crypto/src/main/kotlin/web5/sdk/crypto/jwk/Jwk.kt index 954d4696f..f5e6e28fd 100644 --- a/crypto/src/main/kotlin/web5/sdk/crypto/jwk/Jwk.kt +++ b/crypto/src/main/kotlin/web5/sdk/crypto/jwk/Jwk.kt @@ -5,9 +5,9 @@ import web5.sdk.common.Json import java.security.MessageDigest /** - * Represents a [JSON Web Key (JWK)](https://datatracker.ietf.org/doc/html/rfc7517). - * A JWK is a JSON object that represents a cryptographic key. This class - * provides functionalities to manage a JWK including its creation, conversion + * Represents a [JSON Web Key (Jwk )](https://datatracker.ietf.org/doc/html/rfc7517). + * A Jwk is a JSON object that represents a cryptographic key. This class + * provides functionalities to manage a Jwk including its creation, conversion * to and from JSON, and computing a thumbprint. * * Example: @@ -31,20 +31,20 @@ import java.security.MessageDigest */ public class Jwk( public val kty: String, + public val crv: String, public val use: String?, public val alg: String?, public var kid: String?, - public val crv: String?, public val d: String? = null, public val x: String?, public val y: String? ) { /** - * Computes the thumbprint of the JWK. + * Computes the thumbprint of the Jwk. * [Specification](https://www.rfc-editor.org/rfc/rfc7638.html). * - * Generates a thumbprint of the JWK using SHA-256 hash function. + * Generates a thumbprint of the Jwk using SHA-256 hash function. * The thumbprint is computed based on the key's [kty], [crv], [x], * and [y] values. * @@ -55,7 +55,9 @@ public class Jwk( put("crv", crv) put("kty", kty) put("x", x) - put("y", y) + if (y != null) { + put("y", y) + } } val thumbprintPayloadString = Json.stringify(thumbprintPayload) @@ -69,16 +71,21 @@ public class Jwk( } + override fun toString(): String { + return "Jwk(kty='$kty', use=$use, alg=$alg, kid=$kid, crv=$crv, d=$d, x=$x, y=$y)" + } + /** * Builder for Jwk type. * */ public class Builder { + // todo take in keytype and curve as required params private var kty: String? = null + private var crv: String? = null private var use: String? = null private var alg: String? = null private var kid: String? = null - private var crv: String? = null private var d: String? = null private var x: String? = null private var y: String? = null @@ -185,7 +192,8 @@ public class Jwk( if (kty == "OKP") { check(x != null) { "x is required for OKP keys" } } - return Jwk(kty!!, use, alg, kid, crv, d, x, y) + // todo crv is required + return Jwk(kty!!, crv!!, use, alg, kid, d, x, y) } } } diff --git a/crypto/src/test/kotlin/web5/sdk/crypto/Ed25519Test.kt b/crypto/src/test/kotlin/web5/sdk/crypto/Ed25519Test.kt index 9f9acad6b..39d868d69 100644 --- a/crypto/src/test/kotlin/web5/sdk/crypto/Ed25519Test.kt +++ b/crypto/src/test/kotlin/web5/sdk/crypto/Ed25519Test.kt @@ -35,7 +35,7 @@ class Web5TestVectorsCryptoEd25519 { val inputByteArray: ByteArray = hexStringToByteArray(vector.input.data) val jwkMap = vector.input.key - val ed25519Jwk = Json.parse(jwkMap.toString()) + val ed25519Jwk = Json.parse(Json.stringify(jwkMap)) val signedByteArray: ByteArray = Ed25519.sign(ed25519Jwk, inputByteArray) @@ -64,7 +64,7 @@ class Web5TestVectorsCryptoEd25519 { val testVectors = mapper.readValue(File("../web5-spec/test-vectors/crypto_ed25519/verify.json"), typeRef) testVectors.vectors.filter { it.errors == false }.forEach { vector -> - val key = Json.parse(vector.input.key.toString()) + val key = Json.parse(Json.stringify(vector.input.key)) val data = hexStringToByteArray(vector.input.data) val signature = hexStringToByteArray(vector.input.signature) if (vector.output == true) { diff --git a/crypto/src/test/kotlin/web5/sdk/crypto/InMemoryKeyManagerTest.kt b/crypto/src/test/kotlin/web5/sdk/crypto/InMemoryKeyManagerTest.kt index 04f5994ac..c81c54592 100644 --- a/crypto/src/test/kotlin/web5/sdk/crypto/InMemoryKeyManagerTest.kt +++ b/crypto/src/test/kotlin/web5/sdk/crypto/InMemoryKeyManagerTest.kt @@ -1,17 +1,12 @@ package web5.sdk.crypto -import com.fasterxml.jackson.annotation.JsonInclude -import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException import com.fasterxml.jackson.module.kotlin.readValue -import com.nimbusds.jose.crypto.bc.BouncyCastleProviderSingleton -import com.nimbusds.jose.jwk.Curve -import com.nimbusds.jose.jwk.JWK -import com.nimbusds.jose.jwk.gen.ECKeyGenerator import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertDoesNotThrow import org.junit.jupiter.api.assertThrows -import java.text.ParseException +import web5.sdk.common.Json import kotlin.test.assertEquals class InMemoryKeyManagerTest { @@ -37,34 +32,46 @@ class InMemoryKeyManagerTest { @Test fun `public key is available after import`() { - val jwk = Crypto.generatePrivateKey(AlgorithmId.secp256k1) + val privateKey = Crypto.generatePrivateKey(AlgorithmId.secp256k1) val keyManager = InMemoryKeyManager() - val alias = keyManager.import(jwk) + val alias = keyManager.import(privateKey) val publicKey = keyManager.getPublicKey(alias) - assertEquals(jwk, publicKey) + assertEquals(privateKey.kid, publicKey.kid) + assertEquals(privateKey.crv, publicKey.crv) + assertEquals(privateKey.alg, publicKey.alg) + assertEquals(privateKey.use, publicKey.use) + assertEquals(privateKey.x, publicKey.x) } @Test fun `public keys can be imported`() { - val jwk = Crypto.generatePrivateKey(AlgorithmId.secp256k1) + val privateKey = Crypto.generatePrivateKey(AlgorithmId.secp256k1) val keyManager = InMemoryKeyManager() - val alias = keyManager.import(jwk) - - assertEquals(jwk, keyManager.getPublicKey(alias)) + val alias = keyManager.import(privateKey) + val publicKey = keyManager.getPublicKey(alias) + assertEquals(privateKey.kid, publicKey.kid) + assertEquals(privateKey.crv, publicKey.crv) + assertEquals(privateKey.alg, publicKey.alg) + assertEquals(privateKey.use, publicKey.use) + assertEquals(privateKey.x, publicKey.x) } @Test fun `key without kid can be imported`() { - val jwk = Ed25519.generatePrivateKey() + val privateKey = Ed25519.generatePrivateKey() val keyManager = InMemoryKeyManager() - val alias = keyManager.import(jwk) - + val alias = keyManager.import(privateKey) val publicKey = keyManager.getPublicKey(alias) - assertEquals(jwk, publicKey) + assertEquals(privateKey.kid, publicKey.kid) + assertEquals(privateKey.crv, publicKey.crv) + assertEquals(privateKey.alg, publicKey.alg) + assertEquals(privateKey.use, publicKey.use) + assertEquals(privateKey.x, publicKey.x) + } @Test @@ -74,18 +81,14 @@ class InMemoryKeyManagerTest { val keySet = keyManager.export() assertEquals(1, keySet.size) - - assertDoesNotThrow { - JWK.parse(keySet[0]) - } } @Test - fun `import throws an exception if key isnt a JWK`() { + fun `import throws an exception if key is not a Jwk`() { val keyManager = InMemoryKeyManager() val kakaKeySet = listOf(mapOf("hehe" to "troll")) - assertThrows { + assertThrows { keyManager.import(kakaKeySet) } } @@ -96,11 +99,7 @@ class InMemoryKeyManagerTest { val serializedKeySet = """[{"kty":"OKP","d":"DTwtf9i7M4Vj8vSg0iJAQ_n2gSNEUTNLIq30CJ4d9BE","use":"sig","crv":"Ed25519","kid":"hKTpA-TQPNAX9zXtuxPIyTNpoyd4j1Pq1Y_txo2Hm3I","x":"_CrbbGuhpHFs3KVGg2bbNgd2SikmT4L5rIE_zQQjKq0","alg":"EdDSA"}]""" - val jsonMapper: ObjectMapper = ObjectMapper() - .findAndRegisterModules() - .setSerializationInclusion(JsonInclude.Include.NON_NULL) - - val jsonKeySet: List> = jsonMapper.readValue(serializedKeySet) + val jsonKeySet: List> = Json.jsonMapper.readValue(serializedKeySet) val keyManager = InMemoryKeyManager() assertDoesNotThrow { diff --git a/crypto/src/test/kotlin/web5/sdk/crypto/Secp256k1Test.kt b/crypto/src/test/kotlin/web5/sdk/crypto/Secp256k1Test.kt index 98d0517b8..1405b9d2c 100644 --- a/crypto/src/test/kotlin/web5/sdk/crypto/Secp256k1Test.kt +++ b/crypto/src/test/kotlin/web5/sdk/crypto/Secp256k1Test.kt @@ -14,8 +14,8 @@ import java.security.SignatureException import java.util.Random import kotlin.test.assertEquals import kotlin.test.assertFails -import kotlin.test.assertFalse import kotlin.test.assertNotNull +import kotlin.test.assertNull import kotlin.test.assertTrue class Secp256k1Test { @@ -40,9 +40,9 @@ class Secp256k1Test { Secp256k1.validateKey(publicKey) assertEquals(publicKey.kid, privateKey.kid) assertEquals(Jwa.ES256K.name, publicKey.alg) - assertEquals("sign", publicKey.use) + assertEquals("sig", publicKey.use) assertTrue(publicKey.kty == "EC") - assertNotNull(publicKey.d) + assertNull(publicKey.d) } @Test @@ -108,8 +108,8 @@ class Web5TestVectorsCryptoEs256k { testVectors.vectors.filter { it.errors == false }.forEach { vector -> val inputByteArray: ByteArray = Hex.decodeHex(vector.input.data.toCharArray()) - val jwkMap = vector.input.key - val ecJwk = Json.parse(jwkMap.toString()) + val jwkMap = vector.input.key!! + val ecJwk = Json.parse(Json.stringify(jwkMap)) val signedByteArray: ByteArray = Secp256k1.sign(ecJwk, inputByteArray) val signedHex = Hex.encodeHexString(signedByteArray) @@ -136,25 +136,25 @@ class Web5TestVectorsCryptoEs256k { testVectors.vectors.filter { it.errors == false }.forEach { vector -> val inputByteArray: ByteArray = Hex.decodeHex(vector.input.data.toCharArray()) - val jwkMap = vector.input.key + val jwkMap = vector.input.key!! val signatureByteArray = Hex.decodeHex(vector.input.signature.toCharArray()) - val ecJwk = Json.parse(jwkMap.toString()) + val jwk = Json.parse(Json.stringify(jwkMap)) if (vector.output == true) { - assertDoesNotThrow { Secp256k1.verify(ecJwk, inputByteArray, signatureByteArray) } + assertDoesNotThrow { Secp256k1.verify(jwk, inputByteArray, signatureByteArray) } } else { - assertFails { Secp256k1.verify(ecJwk, inputByteArray, signatureByteArray) } + assertFails { Secp256k1.verify(jwk, inputByteArray, signatureByteArray) } } } testVectors.vectors.filter { it.errors == true }.forEach { vector -> assertFails { val inputByteArray: ByteArray = Hex.decodeHex(vector.input.data.toCharArray()) - val jwkMap = vector.input.key + val jwkMap = vector.input.key!! val signatureByteArray = Hex.decodeHex(vector.input.signature.toCharArray()) - val ecJwk = Json.parse(jwkMap.toString()) + val ecJwk = Json.parse(Json.stringify(jwkMap)) Secp256k1.verify(ecJwk, inputByteArray, signatureByteArray) } } diff --git a/dids/src/main/kotlin/web5/sdk/dids/Serialization.kt b/dids/src/main/kotlin/web5/sdk/dids/Serialization.kt index 238fc3ae1..b328f7e0e 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/Serialization.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/Serialization.kt @@ -25,7 +25,7 @@ public class JwkSerializer : JsonSerializer() { } /** - * Deserialize String into JWK. + * Deserialize String into Jwk. * */ public class JwkDeserializer : JsonDeserializer() { diff --git a/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DhtClient.kt b/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DhtClient.kt index c6b89e9ec..1fafc1281 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DhtClient.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DhtClient.kt @@ -1,6 +1,5 @@ package web5.sdk.dids.methods.dht -import com.nimbusds.jose.jwk.JWK import io.ktor.client.HttpClient import io.ktor.client.engine.HttpClientEngine import io.ktor.client.engine.okhttp.OkHttp @@ -122,7 +121,7 @@ internal class DhtClient( * the current time in milliseconds. * * @param manager The key manager to use to sign the message [KeyManager]. - * @param keyAlias The alias of an Ed25519 key to sign the message with [JWK]. + * @param keyAlias The alias of an Ed25519 key to sign the message with [String]. * @param message The message to publish (a DNS packet) [Message]. * @return A BEP44 signed message [Bep44Message]. * @throws IllegalArgumentException if the private key is not an Ed25519 key. @@ -171,7 +170,7 @@ internal class DhtClient( * https://www.bittorrent.org/beps/bep_0044.html * * @param manager The key manager to use to sign the message [KeyManager]. - * @param keyAlias The alias of an Ed25519 key to sign the message with [JWK]. + * @param keyAlias The alias of an Ed25519 key to sign the message with [String]. * @param seq The sequence number of the message. * @param v The value to be written to the DHT. * @return A BEP44 signed message [Bep44Message]. @@ -234,7 +233,7 @@ internal class DhtClient( // prepare buffer and verify val bytesToVerify = "3:seqi${message.seq}e1:v".toByteArray() + vEncoded - // create a JWK representation of the public key + // create a Jwk representation of the public key val ed25519PublicKey = Ed25519.bytesToPublicKey(message.k) // verify the signature diff --git a/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt b/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt index d45b67f98..183854e68 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt @@ -81,7 +81,7 @@ private class DidDhtApiImpl(configuration: DidDhtConfiguration) : DidDhtApi(conf /** * Specifies options for creating a new "did:dht" Decentralized Identifier (DID). - * @property verificationMethods A list of [JWK]s to add to the DID Document mapped to their purposes + * @property verificationMethods A list of [Jwk]s to add to the DID Document mapped to their purposes * as verification methods, and an optional controller for the verification method. * @property services A list of [Service]s to add to the DID Document. * @property publish Whether to publish the DID Document to the DHT after creation. @@ -348,6 +348,7 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) { internal fun getDidIdentifier(identityKey: Jwk): String { val publicKeyJwk = Jwk.Builder() .keyType(identityKey.kty) + .curve(identityKey.crv) .apply { identityKey.x?.let { x(it) } identityKey.y?.let { y(it) } @@ -358,6 +359,8 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) { } .build() + publicKeyJwk.kid = publicKeyJwk.computeThumbprint() + val publicKeyBytes = Crypto.publicKeyToBytes(publicKeyJwk) val zBase32Encoded = ZBase32.encode(publicKeyBytes) return "did:dht:$zBase32Encoded" diff --git a/dids/src/main/kotlin/web5/sdk/dids/methods/jwk/DidJwk.kt b/dids/src/main/kotlin/web5/sdk/dids/methods/jwk/DidJwk.kt index 930f58477..03b318bc8 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/methods/jwk/DidJwk.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/methods/jwk/DidJwk.kt @@ -129,7 +129,7 @@ public object DidJwk { val decodedKey = Convert(id, EncodingFormat.Base64Url).toStr() val publicKeyJwk = try { Json.parse(decodedKey) - } catch (_: ParseException) { + } catch (_: Exception) { return DidResolutionResult( context = "https://w3id.org/did-resolution/v1", didResolutionMetadata = DidResolutionMetadata( @@ -138,8 +138,8 @@ public object DidJwk { ) } - require(publicKeyJwk.d != null) { - throw IllegalArgumentException("decoded jwk value cannot be a private key") + require(publicKeyJwk.d == null) { + "decoded jwk value cannot be a private key" } val didDocument = createDocument(parsedDid, publicKeyJwk) @@ -153,7 +153,7 @@ public object DidJwk { .id(verificationMethodId) .publicKeyJwk(publicKeyJwk) .controller(did.url) - .type("JsonWebKey2020") + .type("JsonWebKey") .build() val didDocumentBuilder = DidDocument.Builder() diff --git a/dids/src/main/kotlin/web5/sdk/dids/methods/key/DidKey.kt b/dids/src/main/kotlin/web5/sdk/dids/methods/key/DidKey.kt index 0ee0fb225..6faddcf2f 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/methods/key/DidKey.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/methods/key/DidKey.kt @@ -179,7 +179,7 @@ public class DidKey(public val uri: String, public val keyManager: KeyManager) { public fun import(portableDid: PortableDid, keyManager: KeyManager = InMemoryKeyManager()): BearerDid { val parsedDid = Did.parse(portableDid.uri) if (parsedDid.method != methodName) { - throw MethodNotSupportedException("Method not supported") + throw InvalidMethodNameException("Method not supported") } val bearerDid = BearerDid.import(portableDid, keyManager) diff --git a/dids/src/main/kotlin/web5/sdk/dids/methods/web/DidWeb.kt b/dids/src/main/kotlin/web5/sdk/dids/methods/web/DidWeb.kt index 9d77df50e..eea2d6fc4 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/methods/web/DidWeb.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/methods/web/DidWeb.kt @@ -111,10 +111,10 @@ public sealed class DidWebApi( /** * Resolves a `did:jwk` URI into a [DidResolutionResult]. * - * This method parses the provided `did` URI to extract the JWK information. + * This method parses the provided `did` URI to extract the Jwk information. * It validates the method of the DID URI and then attempts to parse the - * JWK from the URI. If successful, it constructs a [DidDocument] with the - * resolved JWK, generating a [DidResolutionResult]. + * Jwk from the URI. If successful, it constructs a [DidDocument] with the + * resolved Jwk, generating a [DidResolutionResult]. * * The method ensures that the DID URI adheres to the `did:jwk` method * specification and handles exceptions that may arise during the parsing diff --git a/dids/src/test/kotlin/web5/sdk/dids/DidMethodTest.kt b/dids/src/test/kotlin/web5/sdk/dids/DidMethodTest.kt index 6a9761c5b..fe1b4e099 100644 --- a/dids/src/test/kotlin/web5/sdk/dids/DidMethodTest.kt +++ b/dids/src/test/kotlin/web5/sdk/dids/DidMethodTest.kt @@ -24,7 +24,7 @@ class DidMethodTest { val manager = InMemoryKeyManager() val bearerDid = DidKey.create(manager) - val verificationMethod = DidKey.resolve(bearerDid.did.uri, null) + val verificationMethod = DidKey.resolve(bearerDid.did.uri) .didDocument!! .findAssertionMethodById() assertEquals("${bearerDid.did.uri}#${Did.parse(bearerDid.did.uri).id}", verificationMethod.id) @@ -36,7 +36,7 @@ class DidMethodTest { val bearerDid = DidKey.create(manager) val assertionMethodId = "${bearerDid.did.uri}#${Did.parse(bearerDid.did.uri).id}" - val verificationMethod = DidKey.resolve(bearerDid.did.uri, null) + val verificationMethod = DidKey.resolve(bearerDid.did.uri) .didDocument!! .findAssertionMethodById(assertionMethodId) assertEquals(assertionMethodId, verificationMethod.id) @@ -48,7 +48,7 @@ class DidMethodTest { val bearerDid = DidKey.create(manager) val exception = assertThrows { - DidKey.resolve(bearerDid.did.uri, null) + DidKey.resolve(bearerDid.did.uri) .didDocument!! .findAssertionMethodById("made up assertion method id") } @@ -62,6 +62,6 @@ class DidMethodTest { val exception = assertThrows { did.document.findAssertionMethodById("made up assertion method id") } - assertEquals("No assertion methods found in DID document", exception.message) + assertEquals("assertion method \"made up assertion method id\" not found in list of assertion methods", exception.message) } } \ No newline at end of file diff --git a/dids/src/test/kotlin/web5/sdk/dids/didcore/VerificationMethodTest.kt b/dids/src/test/kotlin/web5/sdk/dids/didcore/VerificationMethodTest.kt index d5a6ce972..e39567e6b 100644 --- a/dids/src/test/kotlin/web5/sdk/dids/didcore/VerificationMethodTest.kt +++ b/dids/src/test/kotlin/web5/sdk/dids/didcore/VerificationMethodTest.kt @@ -1,16 +1,16 @@ package web5.sdk.dids.didcore -import com.nimbusds.jose.jwk.JWK import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.assertThrows import web5.sdk.crypto.AlgorithmId import web5.sdk.crypto.InMemoryKeyManager +import web5.sdk.crypto.jwk.Jwk import kotlin.test.Test import kotlin.test.assertEquals class VerificationMethodTest { - var publicKey: JWK? = null + var publicKey: Jwk? = null @BeforeEach fun setUp() { diff --git a/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DidDhtTest.kt b/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DidDhtTest.kt index 47affaadb..36f76fc1f 100644 --- a/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DidDhtTest.kt +++ b/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DidDhtTest.kt @@ -11,6 +11,7 @@ import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertDoesNotThrow import org.junit.jupiter.api.assertThrows +import org.mockito.kotlin.any import org.mockito.kotlin.doReturn import org.mockito.kotlin.spy import org.mockito.kotlin.whenever diff --git a/dids/src/test/kotlin/web5/sdk/dids/methods/jwk/DidJwkTest.kt b/dids/src/test/kotlin/web5/sdk/dids/methods/jwk/DidJwkTest.kt index 760be7995..546736c36 100644 --- a/dids/src/test/kotlin/web5/sdk/dids/methods/jwk/DidJwkTest.kt +++ b/dids/src/test/kotlin/web5/sdk/dids/methods/jwk/DidJwkTest.kt @@ -10,7 +10,7 @@ import web5.sdk.common.Convert import web5.sdk.common.Json import web5.sdk.crypto.AlgorithmId import web5.sdk.crypto.InMemoryKeyManager -import web5.sdk.crypto.Jwa +import web5.sdk.crypto.JwaCurve import web5.sdk.crypto.jwk.Jwk import web5.sdk.dids.DidResolutionResult import web5.sdk.dids.DidResolvers @@ -25,7 +25,7 @@ class DidJwkTest { @Nested inner class CreateTest { @Test - fun `creates an ES256K key when no options are passed`() { + fun `creates an EdDSA key when no options are passed`() { val manager = InMemoryKeyManager() val did = DidJwk.create(manager) @@ -38,7 +38,7 @@ class DidJwkTest { assertNotNull(jwk) val keyAlias = did.keyManager.getDeterministicAlias(jwk) val publicKey = did.keyManager.getPublicKey(keyAlias) - assertEquals(Jwa.ES256K.name, publicKey.alg) + assertEquals(JwaCurve.Ed25519.name, publicKey.crv) } } @@ -79,19 +79,6 @@ class DidJwkTest { assertEquals("methodNotSupported", result.didResolutionMetadata.error) } - @Test - fun `private key throws exception`() { - val manager = InMemoryKeyManager() - manager.generatePrivateKey(AlgorithmId.secp256k1) - val privateJwkString = Json.parse(manager.export().first().toString()) - val encodedPrivateJwk = Convert(privateJwkString).toBase64Url() - - val did = "did:jwk:$encodedPrivateJwk" - assertThrows( - "decoded jwk value cannot be a private key" - ) { DidJwk.resolve(did) } - } - @Test fun `test vector 1`() { // test vector taken from: https://github.com/quartzjer/did-jwk/blob/main/spec.md#p-256 diff --git a/dids/src/test/kotlin/web5/sdk/dids/methods/key/DidKeyTest.kt b/dids/src/test/kotlin/web5/sdk/dids/methods/key/DidKeyTest.kt index bca9065c0..66a29e7d6 100644 --- a/dids/src/test/kotlin/web5/sdk/dids/methods/key/DidKeyTest.kt +++ b/dids/src/test/kotlin/web5/sdk/dids/methods/key/DidKeyTest.kt @@ -46,7 +46,7 @@ class DidKeyTest { fun `resolving a secp256k1 DID works`() { // test vector taken from: https://github.com/w3c-ccg/did-method-key/blob/main/test-vectors/secp256k1.json#L202C4-L257 val did = "did:key:zQ3shjmnWpSDEbYKpaFm4kTs9kXyqG6N2QwCYHNPP4yubqgJS" - val result = DidKey.resolve(did, null) + val result = DidKey.resolve(did) assertNotNull(result) val didDocument = result.didDocument diff --git a/jose/src/main/kotlin/web5/sdk/jose/jws/Jws.kt b/jose/src/main/kotlin/web5/sdk/jose/jws/Jws.kt index afec98721..b3b52e17c 100644 --- a/jose/src/main/kotlin/web5/sdk/jose/jws/Jws.kt +++ b/jose/src/main/kotlin/web5/sdk/jose/jws/Jws.kt @@ -23,7 +23,7 @@ public object Jws { @Suppress("SwallowedException") public fun decode(jws: String): DecodedJws { val parts = jws.split(".") - check(parts.size != 3) { + check(parts.size == 3) { "Malformed JWT. Expected 3 parts, got ${parts.size}" } @@ -85,7 +85,7 @@ public object Jws { .algorithm(Crypto.getJwkCurve(verificationMethod.publicKeyJwk!!)?.name!!) .build() - val headerBase64Url = Convert(jwsHeader).toBase64Url() + val headerBase64Url = Convert(Json.stringify(jwsHeader)).toBase64Url() val payloadBase64Url = Convert(payload).toBase64Url() val toSign = "$headerBase64Url.$payloadBase64Url" diff --git a/jose/src/main/kotlin/web5/sdk/jose/jwt/Jwt.kt b/jose/src/main/kotlin/web5/sdk/jose/jwt/Jwt.kt index d1fbb140f..a86b7fe6e 100644 --- a/jose/src/main/kotlin/web5/sdk/jose/jwt/Jwt.kt +++ b/jose/src/main/kotlin/web5/sdk/jose/jwt/Jwt.kt @@ -28,7 +28,8 @@ public object Jwt { val claims: JwtClaimsSet try { val payload = Convert(decodedJws.payload).toStr() - claims = JwtClaimsSet.fromJson(Json.jsonMapper.readTree(payload)) + val decodedPayload = Convert(payload, EncodingFormat.Base64Url).toStr() + claims = JwtClaimsSet.fromJson(Json.jsonMapper.readTree(decodedPayload)) } catch (e: Exception) { throw IllegalArgumentException( "Malformed JWT. " + From e2a3cb73f97e5befc120c538eda7bf235b616d1e Mon Sep 17 00:00:00 2001 From: Jiyoon Koo Date: Sun, 17 Mar 2024 22:15:10 -0400 Subject: [PATCH 21/47] requiring keytype and curve when building Jwk --- .../kotlin/web5/sdk/crypto/AwsKeyManager.kt | 9 +++-- .../main/kotlin/web5/sdk/crypto/Ed25519.kt | 16 +++------ .../main/kotlin/web5/sdk/crypto/Secp256k1.kt | 15 +++----- .../main/kotlin/web5/sdk/crypto/jwk/Jwk.kt | 34 ++++--------------- .../web5/sdk/dids/methods/dht/DidDht.kt | 4 +-- .../web5/sdk/dids/methods/dht/DidDhtTest.kt | 6 +++- 6 files changed, 28 insertions(+), 56 deletions(-) diff --git a/crypto/src/main/kotlin/web5/sdk/crypto/AwsKeyManager.kt b/crypto/src/main/kotlin/web5/sdk/crypto/AwsKeyManager.kt index 42085abbd..c1e0f5aee 100644 --- a/crypto/src/main/kotlin/web5/sdk/crypto/AwsKeyManager.kt +++ b/crypto/src/main/kotlin/web5/sdk/crypto/AwsKeyManager.kt @@ -17,6 +17,7 @@ import com.nimbusds.jose.crypto.impl.ECDSA import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo import org.bouncycastle.crypto.ExtendedDigest import org.bouncycastle.crypto.digests.SHA256Digest +import org.bouncycastle.jce.spec.ECNamedCurveSpec import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter import web5.sdk.common.Convert import web5.sdk.common.EncodingFormat @@ -130,8 +131,12 @@ public class AwsKeyManager @JvmOverloads constructor( val jwkBuilder = when (publicKey) { is ECPublicKey -> { val (x, y) = extractBase64UrlXYFromECPublicKey(publicKey) - Jwk.Builder() - .keyType("EC") + // todo how to get the curve name out of java.security.publicKey? + // publicKey.params.curve.toString() gives me toString() value of the curve type + // here tried casting it as ECNamedCurveSpec of bouncy castle and get name + // not sure if correct. + val namedCurve = publicKey as ECNamedCurveSpec + Jwk.Builder("EC", namedCurve.name) .x(x) .y(y) } diff --git a/crypto/src/main/kotlin/web5/sdk/crypto/Ed25519.kt b/crypto/src/main/kotlin/web5/sdk/crypto/Ed25519.kt index 7e2f10f97..59c1aeeb5 100644 --- a/crypto/src/main/kotlin/web5/sdk/crypto/Ed25519.kt +++ b/crypto/src/main/kotlin/web5/sdk/crypto/Ed25519.kt @@ -54,10 +54,8 @@ public object Ed25519 : KeyGenerator, Signer { .keyUse(KeyUse.SIGNATURE) .generate() - val jwk = Jwk.Builder() - .keyType("OKP") + val jwk = Jwk.Builder("OKP", privateKey.curve.name) .algorithm(algorithm.name) - .curve(privateKey.curve.name) .keyUse("sig") .privateKey(privateKey.d.toString()) .x(privateKey.x.toString()) @@ -77,10 +75,8 @@ public object Ed25519 : KeyGenerator, Signer { override fun computePublicKey(privateKey: Jwk): Jwk { require(privateKey.kty == "OKP") { "private key must be an Octet Key Pair (kty: OKP)" } - val jwk = Jwk.Builder() - .keyType(privateKey.kty) + val jwk = Jwk.Builder(privateKey.kty, curve.name) .algorithm(algorithm.name) - .curve(curve.name) .apply { privateKey.use?.let { keyUse(it) } } @@ -110,11 +106,9 @@ public object Ed25519 : KeyGenerator, Signer { val base64UrlEncodedPrivateKey = Convert(privateKeyBytes).toBase64Url() val base64UrlEncodedPublicKey = Convert(publicKeyBytes).toBase64Url() - return Jwk.Builder() - .keyType("OKP") + return Jwk.Builder("OKP", curve.name) .keyUse("sig") .algorithm(algorithm.name) - .curve(curve.name) .privateKey(base64UrlEncodedPrivateKey) .x(base64UrlEncodedPublicKey) .build() @@ -123,10 +117,8 @@ public object Ed25519 : KeyGenerator, Signer { override fun bytesToPublicKey(publicKeyBytes: ByteArray): Jwk { val base64UrlEncodedPublicKey = Convert(publicKeyBytes).toBase64Url() - val jwk = Jwk.Builder() - .keyType("OKP") + val jwk = Jwk.Builder("OKP", curve.name) .algorithm(algorithm.name) - .curve(curve.name) .keyUse("sig") .x(base64UrlEncodedPublicKey) .build() diff --git a/crypto/src/main/kotlin/web5/sdk/crypto/Secp256k1.kt b/crypto/src/main/kotlin/web5/sdk/crypto/Secp256k1.kt index 926e4b771..91e42fdb2 100644 --- a/crypto/src/main/kotlin/web5/sdk/crypto/Secp256k1.kt +++ b/crypto/src/main/kotlin/web5/sdk/crypto/Secp256k1.kt @@ -147,11 +147,9 @@ public object Secp256k1 : KeyGenerator, Signer { .keyUse(KeyUse.SIGNATURE) .generate() - val jwk = Jwk.Builder() - .keyType("EC") + val jwk = Jwk.Builder("EC", privateKey.curve.name) .keyUse("sig") .algorithm(algorithm.name) - .curve(privateKey.curve.name) .privateKey(privateKey.d.toString()) .x(privateKey.x.toString()) .y(privateKey.y.toString()) @@ -165,9 +163,7 @@ public object Secp256k1 : KeyGenerator, Signer { override fun computePublicKey(privateKey: Jwk): Jwk { validateKey(privateKey) - val jwk = Jwk.Builder() - .keyType(privateKey.kty) - .curve(curve.name) + val jwk = Jwk.Builder(privateKey.kty, curve.name) .algorithm(algorithm.name) .apply { privateKey.x?.let { x(it) } @@ -203,8 +199,7 @@ public object Secp256k1 : KeyGenerator, Signer { val rawX = pointQ.rawXCoord.encoded val rawY = pointQ.rawYCoord.encoded - return Jwk.Builder() - .keyType("EC") + return Jwk.Builder("EC", curve.name) .algorithm(algorithm.name) .keyUse("sig") .x(Convert(rawX).toBase64Url()) @@ -217,10 +212,8 @@ public object Secp256k1 : KeyGenerator, Signer { val xBytes = publicKeyBytes.sliceArray(1..32) val yBytes = publicKeyBytes.sliceArray(33..64) - val jwk = Jwk.Builder() - .keyType("EC") + val jwk = Jwk.Builder("EC", curve.name) .algorithm(algorithm.name) - .curve(curve.name) .keyUse("sig") .x(Convert(xBytes).toBase64Url()) .y(Convert(yBytes).toBase64Url()) diff --git a/crypto/src/main/kotlin/web5/sdk/crypto/jwk/Jwk.kt b/crypto/src/main/kotlin/web5/sdk/crypto/jwk/Jwk.kt index f5e6e28fd..9f904b32e 100644 --- a/crypto/src/main/kotlin/web5/sdk/crypto/jwk/Jwk.kt +++ b/crypto/src/main/kotlin/web5/sdk/crypto/jwk/Jwk.kt @@ -78,11 +78,12 @@ public class Jwk( /** * Builder for Jwk type. * + * @property keyType: Type of key (EC or OKP) + * @property curve: Type of curve */ - public class Builder { - // todo take in keytype and curve as required params - private var kty: String? = null - private var crv: String? = null + public class Builder(keyType: String, curve: String) { + private var kty: String = keyType + private var crv: String = curve private var use: String? = null private var alg: String? = null private var kid: String? = null @@ -90,17 +91,6 @@ public class Jwk( private var x: String? = null private var y: String? = null - /** - * Sets key type. - * - * @param kty - * @return Builder object - */ - public fun keyType(kty: String): Builder { - this.kty = kty - return this - } - /** * Sets key use. * @@ -134,16 +124,6 @@ public class Jwk( return this } - /** - * Sets elliptic curve name. - * - * @param crv - * @return Builder object - */ - public fun curve(crv: String): Builder { - this.crv = crv - return this - } /** * Sets private key component. Must be base64 encoded string. @@ -184,7 +164,7 @@ public class Jwk( * @return Jwk object */ public fun build(): Jwk { - check(kty != null) { "kty is required" } + // todo move these checks out to Ed25519 or Secp256k1 classes? if (kty == "EC") { check(x != null) { "x is required for EC keys" } check(y != null) { "y is required for EC keys" } @@ -193,7 +173,7 @@ public class Jwk( check(x != null) { "x is required for OKP keys" } } // todo crv is required - return Jwk(kty!!, crv!!, use, alg, kid, d, x, y) + return Jwk(kty, crv, use, alg, kid, d, x, y) } } } diff --git a/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt b/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt index 183854e68..80dd1f259 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt @@ -346,9 +346,7 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) { * @param identityKey the key used to generate the DID's identifier */ internal fun getDidIdentifier(identityKey: Jwk): String { - val publicKeyJwk = Jwk.Builder() - .keyType(identityKey.kty) - .curve(identityKey.crv) + val publicKeyJwk = Jwk.Builder(identityKey.kty, identityKey.crv) .apply { identityKey.x?.let { x(it) } identityKey.y?.let { y(it) } diff --git a/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DidDhtTest.kt b/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DidDhtTest.kt index 36f76fc1f..bccf0479c 100644 --- a/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DidDhtTest.kt +++ b/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DidDhtTest.kt @@ -19,6 +19,7 @@ import web5.sdk.common.Json import web5.sdk.common.ZBase32 import web5.sdk.crypto.AlgorithmId import web5.sdk.crypto.InMemoryKeyManager +import web5.sdk.crypto.JwaCurve import web5.sdk.crypto.jwk.Jwk import web5.sdk.dids.DidResolutionResult import web5.sdk.dids.JwkDeserializer @@ -119,7 +120,10 @@ class DidDhtTest { val publicKeyJwk = manager.getPublicKey(otherKey) // todo this was ECKeyGenerator(Curve.P_256).generate().toPublicJWK() before // is this the right equivalent? - val publicKeyJwk2 = Jwk.Builder().keyType("EC").build() + val publicKeyJwk2 = Jwk.Builder("EC", JwaCurve.secp256k1.name) + .x("fake-x-value") + .y("fake-y-value") + .build() val verificationMethodsToAdd: Iterable, String?>> = listOf( Triple( publicKeyJwk, From f0fbc80643f3f453d4e52668cec6c35f86b2514c Mon Sep 17 00:00:00 2001 From: Jiyoon Koo Date: Sun, 17 Mar 2024 22:52:37 -0400 Subject: [PATCH 22/47] adding default keyUse of sig to Jwk when computing public key. --- crypto/src/main/kotlin/web5/sdk/crypto/Ed25519.kt | 11 ++++++----- crypto/src/main/kotlin/web5/sdk/crypto/Secp256k1.kt | 8 ++++---- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/crypto/src/main/kotlin/web5/sdk/crypto/Ed25519.kt b/crypto/src/main/kotlin/web5/sdk/crypto/Ed25519.kt index 59c1aeeb5..d517c4ced 100644 --- a/crypto/src/main/kotlin/web5/sdk/crypto/Ed25519.kt +++ b/crypto/src/main/kotlin/web5/sdk/crypto/Ed25519.kt @@ -73,17 +73,18 @@ public object Ed25519 : KeyGenerator, Signer { * @return The corresponding public key in Jwk format. */ override fun computePublicKey(privateKey: Jwk): Jwk { - require(privateKey.kty == "OKP") { "private key must be an Octet Key Pair (kty: OKP)" } + validateKey(privateKey) val jwk = Jwk.Builder(privateKey.kty, curve.name) + .keyUse("sig") // todo is publicKey JWK's keyUse "sig"? had to edit create.json .algorithm(algorithm.name) .apply { - privateKey.use?.let { keyUse(it) } + privateKey.kid?.let { keyId(it) } + privateKey.x?.let { x(it) } } - .x(privateKey.x.toString()) .build() - jwk.kid = jwk.computeThumbprint() + jwk.kid = jwk.kid ?: jwk.computeThumbprint() return jwk } @@ -123,7 +124,7 @@ public object Ed25519 : KeyGenerator, Signer { .x(base64UrlEncodedPublicKey) .build() - jwk.kid = jwk.computeThumbprint() + jwk.kid = jwk.kid ?: jwk.computeThumbprint() return jwk } diff --git a/crypto/src/main/kotlin/web5/sdk/crypto/Secp256k1.kt b/crypto/src/main/kotlin/web5/sdk/crypto/Secp256k1.kt index 91e42fdb2..903e82f05 100644 --- a/crypto/src/main/kotlin/web5/sdk/crypto/Secp256k1.kt +++ b/crypto/src/main/kotlin/web5/sdk/crypto/Secp256k1.kt @@ -155,8 +155,7 @@ public object Secp256k1 : KeyGenerator, Signer { .y(privateKey.y.toString()) .build() - - jwk.kid = jwk.computeThumbprint() // or should i do privateKey.keyId? + jwk.kid = jwk.computeThumbprint() return jwk } @@ -164,15 +163,16 @@ public object Secp256k1 : KeyGenerator, Signer { validateKey(privateKey) val jwk = Jwk.Builder(privateKey.kty, curve.name) + .keyUse("sig") // todo is publicKey JWK's keyUse "sig"? had to edit create.json .algorithm(algorithm.name) .apply { + privateKey.kid?.let { keyId(it) } privateKey.x?.let { x(it) } privateKey.y?.let { y(it) } } - .keyUse("sig") .build() - jwk.kid = jwk.computeThumbprint() + jwk.kid = jwk.kid ?: jwk.computeThumbprint() return jwk } From 46e3d5e7e93e7ef2dbf2e1c8678f88094d1bb7f0 Mon Sep 17 00:00:00 2001 From: Jiyoon Koo Date: Mon, 18 Mar 2024 00:11:36 -0400 Subject: [PATCH 23/47] fixing some tests --- .../credentials/PresentationExchangeTest.kt | 3 +- .../credentials/VerifiableCredentialTest.kt | 13 ++++--- .../credentials/VerifiablePresentationTest.kt | 35 +++++++++++-------- jose/src/main/kotlin/web5/sdk/jose/jws/Jws.kt | 10 +++--- jose/src/main/kotlin/web5/sdk/jose/jwt/Jwt.kt | 15 +++++--- web5-spec | 2 +- 6 files changed, 43 insertions(+), 35 deletions(-) diff --git a/credentials/src/test/kotlin/web5/sdk/credentials/PresentationExchangeTest.kt b/credentials/src/test/kotlin/web5/sdk/credentials/PresentationExchangeTest.kt index e42b436c6..0aa5e4fed 100644 --- a/credentials/src/test/kotlin/web5/sdk/credentials/PresentationExchangeTest.kt +++ b/credentials/src/test/kotlin/web5/sdk/credentials/PresentationExchangeTest.kt @@ -246,10 +246,9 @@ class PresentationExchangeTest { val vcJwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" - assertThrows { + assertThrows { PresentationExchange.satisfiesPresentationDefinition(listOf(vcJwt), pd) } - } @Test diff --git a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt index c90192cba..04ce4b5a9 100644 --- a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt +++ b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt @@ -20,8 +20,8 @@ import web5.sdk.dids.methods.key.DidKey import web5.sdk.testing.TestVectors import java.io.File import java.security.SignatureException -import java.text.ParseException import kotlin.test.Ignore +import kotlin.test.assertContains import kotlin.test.assertEquals import kotlin.test.assertFails import kotlin.test.assertNotNull @@ -119,8 +119,8 @@ class VerifiableCredentialTest { ) val header = JwsHeader.Builder() - .algorithm(Jwa.ES256K.name) .type("JWT") + .algorithm(Jwa.ES256K.name) .keyId(issuerDid.did.uri) .build() // A detached payload JWT @@ -129,15 +129,14 @@ class VerifiableCredentialTest { val exception = assertThrows(SignatureException::class.java) { VerifiableCredential.verify(vcJwt) } - assertEquals( - "Signature verification failed: Expected kid in JWS header to dereference a DID Document " + - "Verification Method with an Assertion verification relationship", exception.message + assertContains( + exception.message!!, "not found in list of assertion methods", ) } @Test - fun `parseJwt throws ParseException if argument is not a valid JWT`() { - assertThrows(ParseException::class.java) { + fun `parseJwt throws IllegalStateException if argument is not a valid JWT`() { + assertThrows(IllegalStateException::class.java) { VerifiableCredential.parseJwt("hi") } } diff --git a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiablePresentationTest.kt b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiablePresentationTest.kt index 029ffe10a..7fa565847 100644 --- a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiablePresentationTest.kt +++ b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiablePresentationTest.kt @@ -4,6 +4,7 @@ import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertDoesNotThrow import web5.sdk.common.Convert +import web5.sdk.common.Json import web5.sdk.credentials.model.InputDescriptorMapping import web5.sdk.credentials.model.PresentationSubmission import web5.sdk.crypto.AlgorithmId @@ -18,6 +19,7 @@ import web5.sdk.jose.jwt.Jwt import web5.sdk.jose.jwt.JwtClaimsSet import java.security.SignatureException import java.text.ParseException +import kotlin.test.assertContains import kotlin.test.assertEquals import kotlin.test.assertNotNull @@ -34,6 +36,7 @@ class VerifiablePresentationTest { "M1oiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDprZXk6elEzc2h3ZDR5VUFmV2dmR0VScVVrNDd4Rzk1cU5Vc2lzRDc3NkpMdVo3" + "eXo5blFpaiIsImxvY2FsUmVzcGVjdCI6ImhpZ2giLCJsZWdpdCI6dHJ1ZX19fQ.Bx0JrQERWRLpYeg3TnfrOIo4zexo3q1exPZ-Ej6j0T0YO" + "BVZaZ9-RqpiAM-fHKrdGUzVyXr77pOl7yGgwIO90g" + @Test fun `create simple vp`() { val vcJwts: Iterable = listOf("vcjwt1") @@ -154,19 +157,19 @@ class VerifiablePresentationTest { val holderDid = DidKey.create(keyManager) val header = JwsHeader.Builder() + .type("JWT") .algorithm(Jwa.ES256K.name) .keyId(holderDid.did.uri) .build() - val vpJwt = "${Convert(header).toBase64Url()}..fakeSig" + val vpJwt = "${Convert(Json.stringify(header)).toBase64Url()}..fakeSig" val exception = assertThrows(SignatureException::class.java) { VerifiablePresentation.verify(vpJwt) } - assertEquals( - "Signature verification failed: Expected kid in JWS header to dereference a DID Document " + - "Verification Method with an Assertion verification relationship", exception.message + assertContains( + exception.message!!, "not found in list of assertion methods", ) } @@ -192,8 +195,8 @@ class VerifiablePresentationTest { } @Test - fun `parseJwt throws ParseException if argument is not a valid JWT`() { - assertThrows(ParseException::class.java) { + fun `parseJwt throws IllegalStateException if argument is not a valid JWT`() { + assertThrows(IllegalStateException::class.java) { VerifiablePresentation.parseJwt("hi") } } @@ -222,29 +225,31 @@ class VerifiablePresentationTest { //Create a DHT DID without an assertionMethod val alias = keyManager.generatePrivateKey(AlgorithmId.secp256k1) val verificationJwk = keyManager.getPublicKey(alias) - val verificationMethodsToAdd = listOf(Triple( - verificationJwk, - emptyList(), - "did:web:tbd.website" - )) + val verificationMethodsToAdd = listOf( + Triple( + verificationJwk, + emptyList(), + "did:web:tbd.website" + ) + ) val issuerDid = DidDht.create( InMemoryKeyManager(), CreateDidDhtOptions(verificationMethods = verificationMethodsToAdd) ) val header = JwsHeader.Builder() + .type("JWT") .algorithm(Jwa.ES256K.name) .keyId(issuerDid.did.uri) .build() //A detached payload JWT - val vpJwt = "${Convert(header).toBase64Url()}..fakeSig" + val vpJwt = "${Convert(Json.stringify(header)).toBase64Url()}..fakeSig" val exception = assertThrows(SignatureException::class.java) { VerifiablePresentation.verify(vpJwt) } - assertEquals( - "Signature verification failed: Expected kid in JWS header to dereference a DID Document " + - "Verification Method with an Assertion verification relationship", exception.message + assertContains( + exception.message!!, "not found in list of assertion methods", ) } } \ No newline at end of file diff --git a/jose/src/main/kotlin/web5/sdk/jose/jws/Jws.kt b/jose/src/main/kotlin/web5/sdk/jose/jws/Jws.kt index b3b52e17c..67e465984 100644 --- a/jose/src/main/kotlin/web5/sdk/jose/jws/Jws.kt +++ b/jose/src/main/kotlin/web5/sdk/jose/jws/Jws.kt @@ -63,7 +63,6 @@ public object Jws { public fun sign( bearerDid: BearerDid, payload: ByteArray, - header: JwsHeader?, detached: Boolean = false ): String { val (signer, verificationMethod) = bearerDid.getSigner() @@ -78,11 +77,10 @@ public object Jws { verificationMethod.id } - val jwsHeader = header - ?: JwsHeader.Builder() + val jwsHeader = JwsHeader.Builder() .type("JWT") - .keyId(kid) .algorithm(Crypto.getJwkCurve(verificationMethod.publicKeyJwk!!)?.name!!) + .keyId(kid) .build() val headerBase64Url = Convert(Json.stringify(jwsHeader)).toBase64Url() @@ -237,7 +235,7 @@ public class DecodedJws( val resolutionResult = DidResolvers.resolve(header.kid!!) - check(resolutionResult.didResolutionMetadata.error != null) { + check(resolutionResult.didResolutionMetadata.error == null) { "Verification failed. Failed to resolve kid. " + "Error: ${resolutionResult.didResolutionMetadata.error}" } @@ -246,7 +244,7 @@ public class DecodedJws( "Verification failed. Expected header kid to dereference a DID document" } - check(resolutionResult.didDocument?.verificationMethod?.size != 0) { + check(resolutionResult.didDocument!!.verificationMethod?.size != 0) { "Verification failed. Expected header kid to dereference a verification method" } diff --git a/jose/src/main/kotlin/web5/sdk/jose/jwt/Jwt.kt b/jose/src/main/kotlin/web5/sdk/jose/jwt/Jwt.kt index a86b7fe6e..1e0654383 100644 --- a/jose/src/main/kotlin/web5/sdk/jose/jwt/Jwt.kt +++ b/jose/src/main/kotlin/web5/sdk/jose/jwt/Jwt.kt @@ -1,5 +1,6 @@ package web5.sdk.jose.jwt +import com.fasterxml.jackson.core.type.TypeReference import com.fasterxml.jackson.databind.JsonNode import web5.sdk.common.Convert import web5.sdk.common.EncodingFormat @@ -54,10 +55,9 @@ public object Jwt { * @return The signed JWT */ public fun sign(did: BearerDid, payload: JwtClaimsSet): String { - val header = JwtHeader(typ = "JWT") val payloadBytes = Convert(Json.stringify(payload)).toByteArray() - return Jws.sign(did, payloadBytes, header) + return Jws.sign(did, payloadBytes) } /** @@ -143,7 +143,8 @@ public class JwtClaimsSet( "exp", "nbf", "iat", - "jti" + "jti", + "misc" ) val miscClaims: MutableMap = mutableMapOf() @@ -156,6 +157,12 @@ public class JwtClaimsSet( } } + val miscMap = Json.jsonMapper + .convertValue( + jsonNode.get("misc"), + object : TypeReference>() {} + ) ?: mutableMapOf() + return JwtClaimsSet( iss = jsonNode.get("iss")?.asText(), sub = jsonNode.get("sub")?.asText(), @@ -164,7 +171,7 @@ public class JwtClaimsSet( nbf = jsonNode.get("nbf")?.asLong(), iat = jsonNode.get("iat")?.asLong(), jti = jsonNode.get("jti")?.asText(), - misc = miscClaims + misc = miscClaims + miscMap ) } diff --git a/web5-spec b/web5-spec index 2cd6e80b6..3b5448d55 160000 --- a/web5-spec +++ b/web5-spec @@ -1 +1 @@ -Subproject commit 2cd6e80b60ac37dfed9dc718d8c94a830e0b76a9 +Subproject commit 3b5448d55afea1043ff84cb8ff291c19ffb79096 From f4c418cb9f8e34596b92507ba2cb118dc67da511 Mon Sep 17 00:00:00 2001 From: Jiyoon Koo Date: Mon, 18 Mar 2024 20:05:57 -0700 Subject: [PATCH 24/47] adding '$.misc.vc....' path to all test vector jsons --- .../kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt | 3 ++- credentials/src/test/resources/pd_filter_array.json | 3 ++- .../pd_filter_array_multiple_input_descriptors.json | 3 ++- .../src/test/resources/pd_filter_array_single_path.json | 3 ++- credentials/src/test/resources/pd_filter_value.json | 3 ++- credentials/src/test/resources/pd_invalid.json | 3 ++- .../test/resources/pd_mixed_multiple_input_descriptors.json | 3 ++- credentials/src/test/resources/pd_path_no_filter.json | 3 ++- credentials/src/test/resources/pd_path_no_filter_dob.json | 3 ++- .../pd_path_no_filter_multiple_input_descriptors.json | 3 ++- credentials/src/test/resources/pd_sanctions.json | 6 ++++-- 11 files changed, 24 insertions(+), 12 deletions(-) diff --git a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt index 04ce4b5a9..7af141e62 100644 --- a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt +++ b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt @@ -6,6 +6,7 @@ import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertDoesNotThrow import web5.sdk.common.Convert +import web5.sdk.common.Json import web5.sdk.crypto.AlgorithmId import web5.sdk.crypto.AwsKeyManager import web5.sdk.crypto.InMemoryKeyManager @@ -124,7 +125,7 @@ class VerifiableCredentialTest { .keyId(issuerDid.did.uri) .build() // A detached payload JWT - val vcJwt = "${Convert(header).toBase64Url()}..fakeSig" + val vcJwt = "${Convert(Json.stringify(header)).toBase64Url()}..fakeSig" val exception = assertThrows(SignatureException::class.java) { VerifiableCredential.verify(vcJwt) diff --git a/credentials/src/test/resources/pd_filter_array.json b/credentials/src/test/resources/pd_filter_array.json index 5e983f9c7..c4e01a76d 100644 --- a/credentials/src/test/resources/pd_filter_array.json +++ b/credentials/src/test/resources/pd_filter_array.json @@ -8,7 +8,8 @@ { "path": [ "$.vc.type[*]", - "$.type[*]" + "$.type[*]", + "$.misc.vc.type[*]" ], "filter": { "type": "string", diff --git a/credentials/src/test/resources/pd_filter_array_multiple_input_descriptors.json b/credentials/src/test/resources/pd_filter_array_multiple_input_descriptors.json index e71c3eb17..ad076f784 100644 --- a/credentials/src/test/resources/pd_filter_array_multiple_input_descriptors.json +++ b/credentials/src/test/resources/pd_filter_array_multiple_input_descriptors.json @@ -8,7 +8,8 @@ { "path": [ "$.credentialSubject.dateOfBirth", - "$.vc.credentialSubject.dateOfBirth" + "$.vc.credentialSubject.dateOfBirth", + "$.misc.vc.credentialSubject.dateOfBirth" ], "filter": { "type": "string", diff --git a/credentials/src/test/resources/pd_filter_array_single_path.json b/credentials/src/test/resources/pd_filter_array_single_path.json index 059aa1924..e0af950cb 100644 --- a/credentials/src/test/resources/pd_filter_array_single_path.json +++ b/credentials/src/test/resources/pd_filter_array_single_path.json @@ -7,7 +7,8 @@ "fields": [ { "path": [ - "$.vc.type" + "$.vc.type", + "$.misc.vc.type" ], "filter": { "type": "array", diff --git a/credentials/src/test/resources/pd_filter_value.json b/credentials/src/test/resources/pd_filter_value.json index 66aa173a4..9ff916b94 100644 --- a/credentials/src/test/resources/pd_filter_value.json +++ b/credentials/src/test/resources/pd_filter_value.json @@ -8,7 +8,8 @@ { "path": [ "$.iss", - "$.vc.issuer" + "$.vc.issuer", + "$.misc.vc.issuer" ], "filter": { "type": "string", diff --git a/credentials/src/test/resources/pd_invalid.json b/credentials/src/test/resources/pd_invalid.json index 08c460732..2981cf9cf 100644 --- a/credentials/src/test/resources/pd_invalid.json +++ b/credentials/src/test/resources/pd_invalid.json @@ -8,7 +8,8 @@ { "path": [ "$.vc.type[*]", - "$.type[*]" + "$.type[*]", + "$.misc.vc.type[*]" ], "filter": { "type": "string", diff --git a/credentials/src/test/resources/pd_mixed_multiple_input_descriptors.json b/credentials/src/test/resources/pd_mixed_multiple_input_descriptors.json index 576578301..cb7c381db 100644 --- a/credentials/src/test/resources/pd_mixed_multiple_input_descriptors.json +++ b/credentials/src/test/resources/pd_mixed_multiple_input_descriptors.json @@ -8,7 +8,8 @@ { "path": [ "$.credentialSubject.dateOfBirth", - "$.vc.credentialSubject.dateOfBirth" + "$.vc.credentialSubject.dateOfBirth", + "$.misc.vc.credentialSubject.dateOfBirth" ], "filter": { "type": "string", diff --git a/credentials/src/test/resources/pd_path_no_filter.json b/credentials/src/test/resources/pd_path_no_filter.json index 62fb5e02c..c1ee12863 100644 --- a/credentials/src/test/resources/pd_path_no_filter.json +++ b/credentials/src/test/resources/pd_path_no_filter.json @@ -8,7 +8,8 @@ { "path": [ "$.vc.type", - "$.type" + "$.type", + "$.misc.vc.type" ] } ] diff --git a/credentials/src/test/resources/pd_path_no_filter_dob.json b/credentials/src/test/resources/pd_path_no_filter_dob.json index af1edbdd4..af64a929c 100644 --- a/credentials/src/test/resources/pd_path_no_filter_dob.json +++ b/credentials/src/test/resources/pd_path_no_filter_dob.json @@ -8,7 +8,8 @@ { "path": [ "$.credentialSubject.dateOfBirth", - "$.vc.credentialSubject.dateOfBirth" + "$.vc.credentialSubject.dateOfBirth", + "$.misc.vc.credentialSubject.dateOfBirth" ] } ] diff --git a/credentials/src/test/resources/pd_path_no_filter_multiple_input_descriptors.json b/credentials/src/test/resources/pd_path_no_filter_multiple_input_descriptors.json index 12d26b606..3056c245a 100644 --- a/credentials/src/test/resources/pd_path_no_filter_multiple_input_descriptors.json +++ b/credentials/src/test/resources/pd_path_no_filter_multiple_input_descriptors.json @@ -8,7 +8,8 @@ { "path": [ "$.credentialSubject.dateOfBirth", - "$.vc.credentialSubject.dateOfBirth" + "$.vc.credentialSubject.dateOfBirth", + "$.misc.vc.credentialSubject.dateOfBirth" ] } ] diff --git a/credentials/src/test/resources/pd_sanctions.json b/credentials/src/test/resources/pd_sanctions.json index bc5331214..0d7bc04ab 100644 --- a/credentials/src/test/resources/pd_sanctions.json +++ b/credentials/src/test/resources/pd_sanctions.json @@ -16,7 +16,8 @@ { "path": [ "$.iss", - "$.vc.issuer" + "$.vc.issuer", + "$.misc.vc.issuer" ], "filter": { "type": "string", @@ -26,7 +27,8 @@ { "path": [ "$.vc.type[*]", - "$.type[*]" + "$.type[*]", + "$.misc.vc.type[*]" ], "filter": { "type": "string", From a3e76880b2c4ab23321789625bb7737d72272c7d Mon Sep 17 00:00:00 2001 From: Jiyoon Koo Date: Mon, 18 Mar 2024 22:00:59 -0700 Subject: [PATCH 25/47] fixing test vectors for credentials package tests --- .../web5/sdk/credentials/PresentationExchange.kt | 3 --- .../sdk/credentials/VerifiableCredentialTest.kt | 16 +++++++--------- ..._filter_array_multiple_input_descriptors.json | 3 ++- .../pd_mixed_multiple_input_descriptors.json | 3 ++- ...ath_no_filter_multiple_input_descriptors.json | 3 ++- .../kotlin/web5/sdk/crypto/InMemoryKeyManager.kt | 12 +++++++++++- 6 files changed, 24 insertions(+), 16 deletions(-) diff --git a/credentials/src/main/kotlin/web5/sdk/credentials/PresentationExchange.kt b/credentials/src/main/kotlin/web5/sdk/credentials/PresentationExchange.kt index e77abae14..0c0a38458 100644 --- a/credentials/src/main/kotlin/web5/sdk/credentials/PresentationExchange.kt +++ b/credentials/src/main/kotlin/web5/sdk/credentials/PresentationExchange.kt @@ -198,9 +198,6 @@ public object PresentationExchange { val requiredFields = fields.filter { field -> field.optional != true } for (field in requiredFields) { - // todo path currently set to $.vc.type - // but now all vcs are in $.misc.vc.type - // however vcPayloadJson.read("$.misc.vc.type") also returns null val matchedFields = field.path.mapNotNull { path -> vcPayloadJson.read(path) } if (matchedFields.isEmpty()) { // If no matching fields are found for a required field, the VC does not satisfy this Input Descriptor. diff --git a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt index 7af141e62..4158fd354 100644 --- a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt +++ b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt @@ -11,12 +11,15 @@ import web5.sdk.crypto.AlgorithmId import web5.sdk.crypto.AwsKeyManager import web5.sdk.crypto.InMemoryKeyManager import web5.sdk.crypto.Jwa +import web5.sdk.dids.did.BearerDid +import web5.sdk.dids.did.PortableDid import web5.sdk.dids.didcore.Purpose import web5.sdk.jose.jws.JwsHeader import web5.sdk.jose.jwt.Jwt import web5.sdk.jose.jwt.JwtClaimsSet import web5.sdk.dids.methods.dht.CreateDidDhtOptions import web5.sdk.dids.methods.dht.DidDht +import web5.sdk.dids.methods.jwk.DidJwk import web5.sdk.dids.methods.key.DidKey import web5.sdk.testing.TestVectors import java.io.File @@ -203,8 +206,7 @@ class VerifiableCredentialTest { class Web5TestVectorsCredentials { data class CreateTestInput( - val signerDidUri: String?, - val signerPrivateJwk: Map?, + val signerPortableDid: PortableDid?, val credential: Map?, ) @@ -220,16 +222,12 @@ class Web5TestVectorsCredentials { val testVectors = mapper.readValue(File("../web5-spec/test-vectors/credentials/create.json"), typeRef) testVectors.vectors.filterNot { it.errors ?: false }.forEach { vector -> - println(vector.description) val vc = VerifiableCredential.fromJson(mapper.writeValueAsString(vector.input.credential)) + val portableDid = Json.parse(Json.stringify(vector.input.signerPortableDid!!)) val keyManager = InMemoryKeyManager() - keyManager.import(listOf(vector.input.signerPrivateJwk!!)) - val issuerDid = DidKey.create(keyManager) - // todo need to update test vectors - // input should have portable did and credential - // want to be able to call BearerDID.import() - val vcJwt = vc.sign(issuerDid) + val bearerDid = BearerDid.import(portableDid, keyManager) + val vcJwt = vc.sign(bearerDid) assertEquals(vector.output, vcJwt, vector.description) } diff --git a/credentials/src/test/resources/pd_filter_array_multiple_input_descriptors.json b/credentials/src/test/resources/pd_filter_array_multiple_input_descriptors.json index ad076f784..6e390e1cc 100644 --- a/credentials/src/test/resources/pd_filter_array_multiple_input_descriptors.json +++ b/credentials/src/test/resources/pd_filter_array_multiple_input_descriptors.json @@ -26,7 +26,8 @@ { "path": [ "$.credentialSubject.ssn", - "$.vc.credentialSubject.ssn" + "$.vc.credentialSubject.ssn", + "$.misc.vc.credentialSubject.ssn" ], "filter": { "type": "string" diff --git a/credentials/src/test/resources/pd_mixed_multiple_input_descriptors.json b/credentials/src/test/resources/pd_mixed_multiple_input_descriptors.json index cb7c381db..d3b0a6025 100644 --- a/credentials/src/test/resources/pd_mixed_multiple_input_descriptors.json +++ b/credentials/src/test/resources/pd_mixed_multiple_input_descriptors.json @@ -26,7 +26,8 @@ { "path": [ "$.credentialSubject.ssn", - "$.vc.credentialSubject.ssn" + "$.vc.credentialSubject.ssn", + "$.misc.vc.credentialSubject.ssn" ] } ] diff --git a/credentials/src/test/resources/pd_path_no_filter_multiple_input_descriptors.json b/credentials/src/test/resources/pd_path_no_filter_multiple_input_descriptors.json index 3056c245a..c811a3dea 100644 --- a/credentials/src/test/resources/pd_path_no_filter_multiple_input_descriptors.json +++ b/credentials/src/test/resources/pd_path_no_filter_multiple_input_descriptors.json @@ -22,7 +22,8 @@ { "path": [ "$.credentialSubject.address", - "$.vc.credentialSubject.address" + "$.vc.credentialSubject.address", + "$.misc.vc.credentialSubject.address" ] } ] diff --git a/crypto/src/main/kotlin/web5/sdk/crypto/InMemoryKeyManager.kt b/crypto/src/main/kotlin/web5/sdk/crypto/InMemoryKeyManager.kt index 037b29fa8..ea60e5658 100644 --- a/crypto/src/main/kotlin/web5/sdk/crypto/InMemoryKeyManager.kt +++ b/crypto/src/main/kotlin/web5/sdk/crypto/InMemoryKeyManager.kt @@ -23,7 +23,7 @@ import web5.sdk.crypto.jwk.Jwk * - Keys are stored in an in-memory mutable map and will be lost once the application is terminated or the object is garbage-collected. * - It is suitable for testing or scenarios where persistent storage of keys is not necessary. */ -public class InMemoryKeyManager : KeyManager { +public class InMemoryKeyManager : KeyManager, KeyExporter, KeyImporter { /** * An in-memory keystore represented as a flat key-value map, where the key is a key ID. @@ -127,4 +127,14 @@ public class InMemoryKeyManager : KeyManager { */ public fun export(): List> = keyStore.map { keyIdToJwk -> Json.stringify(keyIdToJwk.value).toMap() } + override fun exportKey(keyId: String): Jwk { + return this.getPrivateKey(keyId) + } + + override fun importKey(jwk: Jwk): String { + val keyAlias = jwk.computeThumbprint() + keyStore[keyAlias] = jwk + return keyAlias + } + } From 81c470d5fe6c45c6a6705c18021b079e88fbb32a Mon Sep 17 00:00:00 2001 From: Jiyoon Koo Date: Tue, 19 Mar 2024 12:32:20 -0700 Subject: [PATCH 26/47] reverting back adding $.misc.vc... for valid paths, instead using jwtclaimset serializer --- .../src/main/kotlin/web5/sdk/common/Json.kt | 6 ++++ .../sdk/credentials/PresentationExchange.kt | 17 +++++++---- .../sdk/credentials/VerifiableCredential.kt | 5 ++-- .../credentials/VerifiableCredentialTest.kt | 4 +-- .../src/test/resources/pd_filter_array.json | 3 +- ...lter_array_multiple_input_descriptors.json | 6 ++-- .../pd_filter_array_single_path.json | 3 +- .../src/test/resources/pd_filter_value.json | 3 +- .../src/test/resources/pd_invalid.json | 3 +- .../pd_mixed_multiple_input_descriptors.json | 6 ++-- .../src/test/resources/pd_path_no_filter.json | 3 +- .../test/resources/pd_path_no_filter_dob.json | 3 +- ..._no_filter_multiple_input_descriptors.json | 6 ++-- .../src/test/resources/pd_sanctions.json | 6 ++-- .../main/kotlin/web5/sdk/crypto/jwk/Jwk.kt | 1 + .../kotlin/web5/sdk/dids/Serialization.kt | 1 + .../web5/sdk/jose/JwtClaimsSetSerializer.kt | 28 +++++++++++++++++++ jose/src/main/kotlin/web5/sdk/jose/jwt/Jwt.kt | 20 ++++++------- web5-spec | 2 +- 19 files changed, 77 insertions(+), 49 deletions(-) create mode 100644 jose/src/main/kotlin/web5/sdk/jose/JwtClaimsSetSerializer.kt diff --git a/common/src/main/kotlin/web5/sdk/common/Json.kt b/common/src/main/kotlin/web5/sdk/common/Json.kt index 199f5b824..cd7a89b29 100644 --- a/common/src/main/kotlin/web5/sdk/common/Json.kt +++ b/common/src/main/kotlin/web5/sdk/common/Json.kt @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectReader import com.fasterxml.jackson.databind.ObjectWriter import com.fasterxml.jackson.databind.SerializationFeature +import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.module.kotlin.readValue import com.fasterxml.jackson.module.kotlin.registerKotlinModule @@ -23,6 +24,9 @@ import com.fasterxml.jackson.module.kotlin.registerKotlinModule * ``` */ public object Json { + + // todo can't do this because of circular dependency +// private val jwtModule = SimpleModule().addSerializer(JwtClaimsSet::class.java, JwtClaimsSetSerializer()) /** * The Jackson object mapper instance, shared across the lib. * @@ -34,6 +38,8 @@ public object Json { .setSerializationInclusion(JsonInclude.Include.NON_NULL) .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + + private val objectWriter: ObjectWriter = jsonMapper.writer() public val objectReader: ObjectReader = jsonMapper.reader() diff --git a/credentials/src/main/kotlin/web5/sdk/credentials/PresentationExchange.kt b/credentials/src/main/kotlin/web5/sdk/credentials/PresentationExchange.kt index 0c0a38458..839d6758d 100644 --- a/credentials/src/main/kotlin/web5/sdk/credentials/PresentationExchange.kt +++ b/credentials/src/main/kotlin/web5/sdk/credentials/PresentationExchange.kt @@ -1,6 +1,7 @@ package web5.sdk.credentials import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.databind.node.ObjectNode import com.networknt.schema.JsonSchema import com.nfeld.jsonpathkt.JsonPath @@ -12,7 +13,9 @@ import web5.sdk.credentials.model.PresentationDefinitionV2 import web5.sdk.credentials.model.PresentationDefinitionV2Validator import web5.sdk.credentials.model.PresentationSubmission import web5.sdk.credentials.model.PresentationSubmissionValidator +import web5.sdk.jose.JwtClaimsSetSerializer import web5.sdk.jose.jwt.Jwt +import web5.sdk.jose.jwt.JwtClaimsSet import java.util.UUID /** @@ -161,12 +164,16 @@ public object PresentationExchange { vcJwtList: Iterable, presentationDefinition: PresentationDefinitionV2 ): Map> { - val vcJwtListWithNodes = vcJwtList.zip(vcJwtList.map { vcJwt -> - val vc = Jwt.decode(vcJwt) + val vcJwtListWithNodes = vcJwtList.zip( + vcJwtList.map { vcJwt -> + val vc = Jwt.decode(vcJwt) - Json.jsonMapper.readTree(Json.stringify(vc.claims)) - ?: throw JsonPathParseException() - }) + val jwtModule = SimpleModule().addSerializer(JwtClaimsSet::class.java, JwtClaimsSetSerializer()) + Json.jsonMapper.registerModule(jwtModule) + val jsonString = Json.jsonMapper.writeValueAsString(vc.claims) + Json.jsonMapper.readTree(jsonString) + ?: throw JsonPathParseException() + }) return presentationDefinition.inputDescriptors.associateWith { inputDescriptor -> vcJwtListWithNodes.filter { (_, node) -> vcSatisfiesInputDescriptor(node, inputDescriptor) diff --git a/credentials/src/main/kotlin/web5/sdk/credentials/VerifiableCredential.kt b/credentials/src/main/kotlin/web5/sdk/credentials/VerifiableCredential.kt index 7edf33100..7ccfea232 100644 --- a/credentials/src/main/kotlin/web5/sdk/credentials/VerifiableCredential.kt +++ b/credentials/src/main/kotlin/web5/sdk/credentials/VerifiableCredential.kt @@ -10,6 +10,7 @@ import com.fasterxml.jackson.module.kotlin.KotlinModule import com.fasterxml.jackson.module.kotlin.convertValue import com.nfeld.jsonpathkt.JsonPath import com.nfeld.jsonpathkt.extension.read +import web5.sdk.common.Json import web5.sdk.dids.did.BearerDid import web5.sdk.jose.jwt.Jwt import web5.sdk.jose.jwt.JwtClaimsSet @@ -207,9 +208,7 @@ public class VerifiableCredential internal constructor(public val vcDataModel: V val vcDataModelValue = jwtPayload.misc["vc"] ?: throw IllegalArgumentException("jwt payload missing vc property") - @Suppress("UNCHECKED_CAST") // only partially unchecked. can only safely cast to Map<*, *> - val vcDataModelMap = vcDataModelValue as? Map - ?: throw IllegalArgumentException("expected vc property in JWT payload to be an object") + val vcDataModelMap = Json.parse>(Json.stringify(vcDataModelValue)) val vcDataModel = VcDataModel.fromMap(vcDataModelMap) diff --git a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt index 4158fd354..25ab5524f 100644 --- a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt +++ b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt @@ -1,6 +1,7 @@ package web5.sdk.credentials import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.databind.exc.MismatchedInputException import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.Test @@ -172,11 +173,10 @@ class VerifiableCredentialTest { .build() val signedJWT = Jwt.sign(signerDid, claimsSet) - val exception = assertThrows(IllegalArgumentException::class.java) { + assertThrows(MismatchedInputException::class.java) { VerifiableCredential.parseJwt(signedJWT) } - assertEquals("expected vc property in JWT payload to be an object", exception.message) } @Test diff --git a/credentials/src/test/resources/pd_filter_array.json b/credentials/src/test/resources/pd_filter_array.json index c4e01a76d..5e983f9c7 100644 --- a/credentials/src/test/resources/pd_filter_array.json +++ b/credentials/src/test/resources/pd_filter_array.json @@ -8,8 +8,7 @@ { "path": [ "$.vc.type[*]", - "$.type[*]", - "$.misc.vc.type[*]" + "$.type[*]" ], "filter": { "type": "string", diff --git a/credentials/src/test/resources/pd_filter_array_multiple_input_descriptors.json b/credentials/src/test/resources/pd_filter_array_multiple_input_descriptors.json index 6e390e1cc..e71c3eb17 100644 --- a/credentials/src/test/resources/pd_filter_array_multiple_input_descriptors.json +++ b/credentials/src/test/resources/pd_filter_array_multiple_input_descriptors.json @@ -8,8 +8,7 @@ { "path": [ "$.credentialSubject.dateOfBirth", - "$.vc.credentialSubject.dateOfBirth", - "$.misc.vc.credentialSubject.dateOfBirth" + "$.vc.credentialSubject.dateOfBirth" ], "filter": { "type": "string", @@ -26,8 +25,7 @@ { "path": [ "$.credentialSubject.ssn", - "$.vc.credentialSubject.ssn", - "$.misc.vc.credentialSubject.ssn" + "$.vc.credentialSubject.ssn" ], "filter": { "type": "string" diff --git a/credentials/src/test/resources/pd_filter_array_single_path.json b/credentials/src/test/resources/pd_filter_array_single_path.json index e0af950cb..059aa1924 100644 --- a/credentials/src/test/resources/pd_filter_array_single_path.json +++ b/credentials/src/test/resources/pd_filter_array_single_path.json @@ -7,8 +7,7 @@ "fields": [ { "path": [ - "$.vc.type", - "$.misc.vc.type" + "$.vc.type" ], "filter": { "type": "array", diff --git a/credentials/src/test/resources/pd_filter_value.json b/credentials/src/test/resources/pd_filter_value.json index 9ff916b94..66aa173a4 100644 --- a/credentials/src/test/resources/pd_filter_value.json +++ b/credentials/src/test/resources/pd_filter_value.json @@ -8,8 +8,7 @@ { "path": [ "$.iss", - "$.vc.issuer", - "$.misc.vc.issuer" + "$.vc.issuer" ], "filter": { "type": "string", diff --git a/credentials/src/test/resources/pd_invalid.json b/credentials/src/test/resources/pd_invalid.json index 2981cf9cf..08c460732 100644 --- a/credentials/src/test/resources/pd_invalid.json +++ b/credentials/src/test/resources/pd_invalid.json @@ -8,8 +8,7 @@ { "path": [ "$.vc.type[*]", - "$.type[*]", - "$.misc.vc.type[*]" + "$.type[*]" ], "filter": { "type": "string", diff --git a/credentials/src/test/resources/pd_mixed_multiple_input_descriptors.json b/credentials/src/test/resources/pd_mixed_multiple_input_descriptors.json index d3b0a6025..576578301 100644 --- a/credentials/src/test/resources/pd_mixed_multiple_input_descriptors.json +++ b/credentials/src/test/resources/pd_mixed_multiple_input_descriptors.json @@ -8,8 +8,7 @@ { "path": [ "$.credentialSubject.dateOfBirth", - "$.vc.credentialSubject.dateOfBirth", - "$.misc.vc.credentialSubject.dateOfBirth" + "$.vc.credentialSubject.dateOfBirth" ], "filter": { "type": "string", @@ -26,8 +25,7 @@ { "path": [ "$.credentialSubject.ssn", - "$.vc.credentialSubject.ssn", - "$.misc.vc.credentialSubject.ssn" + "$.vc.credentialSubject.ssn" ] } ] diff --git a/credentials/src/test/resources/pd_path_no_filter.json b/credentials/src/test/resources/pd_path_no_filter.json index c1ee12863..62fb5e02c 100644 --- a/credentials/src/test/resources/pd_path_no_filter.json +++ b/credentials/src/test/resources/pd_path_no_filter.json @@ -8,8 +8,7 @@ { "path": [ "$.vc.type", - "$.type", - "$.misc.vc.type" + "$.type" ] } ] diff --git a/credentials/src/test/resources/pd_path_no_filter_dob.json b/credentials/src/test/resources/pd_path_no_filter_dob.json index af64a929c..af1edbdd4 100644 --- a/credentials/src/test/resources/pd_path_no_filter_dob.json +++ b/credentials/src/test/resources/pd_path_no_filter_dob.json @@ -8,8 +8,7 @@ { "path": [ "$.credentialSubject.dateOfBirth", - "$.vc.credentialSubject.dateOfBirth", - "$.misc.vc.credentialSubject.dateOfBirth" + "$.vc.credentialSubject.dateOfBirth" ] } ] diff --git a/credentials/src/test/resources/pd_path_no_filter_multiple_input_descriptors.json b/credentials/src/test/resources/pd_path_no_filter_multiple_input_descriptors.json index c811a3dea..12d26b606 100644 --- a/credentials/src/test/resources/pd_path_no_filter_multiple_input_descriptors.json +++ b/credentials/src/test/resources/pd_path_no_filter_multiple_input_descriptors.json @@ -8,8 +8,7 @@ { "path": [ "$.credentialSubject.dateOfBirth", - "$.vc.credentialSubject.dateOfBirth", - "$.misc.vc.credentialSubject.dateOfBirth" + "$.vc.credentialSubject.dateOfBirth" ] } ] @@ -22,8 +21,7 @@ { "path": [ "$.credentialSubject.address", - "$.vc.credentialSubject.address", - "$.misc.vc.credentialSubject.address" + "$.vc.credentialSubject.address" ] } ] diff --git a/credentials/src/test/resources/pd_sanctions.json b/credentials/src/test/resources/pd_sanctions.json index 0d7bc04ab..bc5331214 100644 --- a/credentials/src/test/resources/pd_sanctions.json +++ b/credentials/src/test/resources/pd_sanctions.json @@ -16,8 +16,7 @@ { "path": [ "$.iss", - "$.vc.issuer", - "$.misc.vc.issuer" + "$.vc.issuer" ], "filter": { "type": "string", @@ -27,8 +26,7 @@ { "path": [ "$.vc.type[*]", - "$.type[*]", - "$.misc.vc.type[*]" + "$.type[*]" ], "filter": { "type": "string", diff --git a/crypto/src/main/kotlin/web5/sdk/crypto/jwk/Jwk.kt b/crypto/src/main/kotlin/web5/sdk/crypto/jwk/Jwk.kt index 9f904b32e..41e5955a8 100644 --- a/crypto/src/main/kotlin/web5/sdk/crypto/jwk/Jwk.kt +++ b/crypto/src/main/kotlin/web5/sdk/crypto/jwk/Jwk.kt @@ -29,6 +29,7 @@ import java.security.MessageDigest * @property y Y coordinate for EC keys. * */ +// todo use jcs for canonicalization, instead of using de/serlization. public class Jwk( public val kty: String, public val crv: String, diff --git a/dids/src/main/kotlin/web5/sdk/dids/Serialization.kt b/dids/src/main/kotlin/web5/sdk/dids/Serialization.kt index b328f7e0e..98e5e2239 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/Serialization.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/Serialization.kt @@ -13,6 +13,7 @@ import web5.sdk.crypto.jwk.Jwk import web5.sdk.dids.didcore.Purpose import java.io.IOException +// todo delete the de/serializers for jwk /** * Serialize Jwk into String. */ diff --git a/jose/src/main/kotlin/web5/sdk/jose/JwtClaimsSetSerializer.kt b/jose/src/main/kotlin/web5/sdk/jose/JwtClaimsSetSerializer.kt new file mode 100644 index 000000000..8ef5a4649 --- /dev/null +++ b/jose/src/main/kotlin/web5/sdk/jose/JwtClaimsSetSerializer.kt @@ -0,0 +1,28 @@ +package web5.sdk.jose + +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.databind.JsonSerializer +import com.fasterxml.jackson.databind.SerializerProvider +import web5.sdk.jose.jwt.JwtClaimsSet + +public class JwtClaimsSetSerializer : JsonSerializer() { + + override fun serialize(jwtClaimsSet: JwtClaimsSet, gen: JsonGenerator, serializers: SerializerProvider?) { + gen.writeStartObject() + + jwtClaimsSet.iss?.let { gen.writeStringField("iss", it) } + jwtClaimsSet.sub?.let { gen.writeStringField("sub", it) } + jwtClaimsSet.aud?.let { gen.writeStringField("aud", it) } + jwtClaimsSet.exp?.let { gen.writeNumberField("exp", it) } + jwtClaimsSet.nbf?.let { gen.writeNumberField("nbf", it) } + jwtClaimsSet.iat?.let { gen.writeNumberField("iat", it) } + jwtClaimsSet.jti?.let { gen.writeStringField("jti", it) } + + for ((key, value) in jwtClaimsSet.misc) { + gen.writeObjectField(key, value) + } + + gen.writeEndObject() + } + +} \ No newline at end of file diff --git a/jose/src/main/kotlin/web5/sdk/jose/jwt/Jwt.kt b/jose/src/main/kotlin/web5/sdk/jose/jwt/Jwt.kt index 1e0654383..016b90d56 100644 --- a/jose/src/main/kotlin/web5/sdk/jose/jwt/Jwt.kt +++ b/jose/src/main/kotlin/web5/sdk/jose/jwt/Jwt.kt @@ -1,11 +1,15 @@ package web5.sdk.jose.jwt -import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.JsonSerializer +import com.fasterxml.jackson.databind.SerializerProvider +import com.fasterxml.jackson.databind.annotation.JsonSerialize import web5.sdk.common.Convert import web5.sdk.common.EncodingFormat import web5.sdk.common.Json import web5.sdk.dids.did.BearerDid +import web5.sdk.jose.JwtClaimsSetSerializer import web5.sdk.jose.jws.DecodedJws import web5.sdk.jose.jws.Jws import web5.sdk.jose.jws.JwsHeader @@ -117,6 +121,7 @@ public typealias JwtHeader = JwsHeader * @property jti provides a unique identifier for the JWT. * @property misc additional claims (i.e. VerifiableCredential, VerifiablePresentation) */ +@JsonSerialize(using = JwtClaimsSetSerializer::class) public class JwtClaimsSet( public val iss: String? = null, public val sub: String? = null, @@ -127,6 +132,7 @@ public class JwtClaimsSet( public val jti: String? = null, public val misc: Map = emptyMap() ) { + public companion object { /** @@ -143,8 +149,7 @@ public class JwtClaimsSet( "exp", "nbf", "iat", - "jti", - "misc" + "jti" ) val miscClaims: MutableMap = mutableMapOf() @@ -157,12 +162,6 @@ public class JwtClaimsSet( } } - val miscMap = Json.jsonMapper - .convertValue( - jsonNode.get("misc"), - object : TypeReference>() {} - ) ?: mutableMapOf() - return JwtClaimsSet( iss = jsonNode.get("iss")?.asText(), sub = jsonNode.get("sub")?.asText(), @@ -171,7 +170,7 @@ public class JwtClaimsSet( nbf = jsonNode.get("nbf")?.asLong(), iat = jsonNode.get("iat")?.asLong(), jti = jsonNode.get("jti")?.asText(), - misc = miscClaims + miscMap + misc = miscClaims ) } @@ -263,4 +262,5 @@ public class JwtClaimsSet( */ public fun build(): JwtClaimsSet = JwtClaimsSet(iss, sub, aud, exp, nbf, iat, jti, misc) } + } diff --git a/web5-spec b/web5-spec index 3b5448d55..10c1bdc51 160000 --- a/web5-spec +++ b/web5-spec @@ -1 +1 @@ -Subproject commit 3b5448d55afea1043ff84cb8ff291c19ffb79096 +Subproject commit 10c1bdc51470271ce31a56401eb4e1bd40ed730f From fca2488fdf2bf0e74545582ccad178c289517f92 Mon Sep 17 00:00:00 2001 From: Jiyoon Koo Date: Wed, 20 Mar 2024 16:56:40 -0700 Subject: [PATCH 27/47] removing todos, removing extraneous Convert() call in Jwt.decode() --- .../main/kotlin/web5/sdk/common/Convert.kt | 2 +- .../src/main/kotlin/web5/sdk/common/Json.kt | 2 - .../sdk/credentials/VerifiablePresentation.kt | 6 +- .../credentials/PresentationExchangeTest.kt | 3 +- .../credentials/VerifiableCredentialTest.kt | 2 +- .../credentials/VerifiablePresentationTest.kt | 4 +- .../kotlin/web5/sdk/crypto/AwsKeyManager.kt | 3 - .../main/kotlin/web5/sdk/crypto/Ed25519.kt | 2 +- .../web5/sdk/crypto/InMemoryKeyManager.kt | 4 +- .../main/kotlin/web5/sdk/crypto/Secp256k1.kt | 2 +- .../main/kotlin/web5/sdk/crypto/jwk/Jwk.kt | 3 +- .../web5/sdk/dids/didcore/DidDocument.kt | 32 ++++-- .../web5/sdk/dids/methods/dht/DidDht.kt | 4 +- .../kotlin/web5/sdk/dids/DidMethodTest.kt | 67 ----------- .../web5/sdk/dids/didcore/DidDocumentTest.kt | 51 +++++++++ .../kotlin/web5/sdk/dids/didcore/DidTest.kt | 4 +- .../web5/sdk/jose/JwtClaimsSetSerializer.kt | 53 +++++++++ jose/src/main/kotlin/web5/sdk/jose/jws/Jws.kt | 17 +-- jose/src/main/kotlin/web5/sdk/jose/jwt/Jwt.kt | 105 +++++++++--------- web5-spec | 2 +- 20 files changed, 211 insertions(+), 157 deletions(-) delete mode 100644 dids/src/test/kotlin/web5/sdk/dids/DidMethodTest.kt diff --git a/common/src/main/kotlin/web5/sdk/common/Convert.kt b/common/src/main/kotlin/web5/sdk/common/Convert.kt index e758bbc56..c7603dbd2 100644 --- a/common/src/main/kotlin/web5/sdk/common/Convert.kt +++ b/common/src/main/kotlin/web5/sdk/common/Convert.kt @@ -173,7 +173,7 @@ public class Convert(private val value: T, private val kind: EncodingFormat? EncodingFormat.Base58Btc -> Base58Btc.decode(this.value) EncodingFormat.Base64Url -> B64URL_DECODER.decode(this.value) EncodingFormat.ZBase32 -> ZBase32.decode(this.value) - null -> this.value.toByteArray() + null -> this.value.toByteArray() // todo do we need Charset_UTF8 here? } } diff --git a/common/src/main/kotlin/web5/sdk/common/Json.kt b/common/src/main/kotlin/web5/sdk/common/Json.kt index cd7a89b29..3ac1ccd10 100644 --- a/common/src/main/kotlin/web5/sdk/common/Json.kt +++ b/common/src/main/kotlin/web5/sdk/common/Json.kt @@ -25,8 +25,6 @@ import com.fasterxml.jackson.module.kotlin.registerKotlinModule */ public object Json { - // todo can't do this because of circular dependency -// private val jwtModule = SimpleModule().addSerializer(JwtClaimsSet::class.java, JwtClaimsSetSerializer()) /** * The Jackson object mapper instance, shared across the lib. * diff --git a/credentials/src/main/kotlin/web5/sdk/credentials/VerifiablePresentation.kt b/credentials/src/main/kotlin/web5/sdk/credentials/VerifiablePresentation.kt index f8e6f5f80..b7f7cee94 100644 --- a/credentials/src/main/kotlin/web5/sdk/credentials/VerifiablePresentation.kt +++ b/credentials/src/main/kotlin/web5/sdk/credentials/VerifiablePresentation.kt @@ -3,6 +3,7 @@ package web5.sdk.credentials import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.KotlinModule +import web5.sdk.common.Json import web5.sdk.dids.did.BearerDid import web5.sdk.jose.jwt.Jwt import web5.sdk.jose.jwt.JwtClaimsSet @@ -181,10 +182,7 @@ public class VerifiablePresentation internal constructor(public val vpDataModel: val vpDataModelValue = jwtPayload.misc["vp"] ?: throw IllegalArgumentException("jwt payload missing vp property") - - @Suppress("UNCHECKED_CAST") // only partially unchecked. can only safely cast to Map<*, *> - val vpDataModelMap = vpDataModelValue as? Map - ?: throw IllegalArgumentException("expected vp property in JWT payload to be an object") + val vpDataModelMap = Json.parse>(Json.stringify(vpDataModelValue)) val vpDataModel = VpDataModel.fromMap(vpDataModelMap) diff --git a/credentials/src/test/kotlin/web5/sdk/credentials/PresentationExchangeTest.kt b/credentials/src/test/kotlin/web5/sdk/credentials/PresentationExchangeTest.kt index 0aa5e4fed..5d1d2210d 100644 --- a/credentials/src/test/kotlin/web5/sdk/credentials/PresentationExchangeTest.kt +++ b/credentials/src/test/kotlin/web5/sdk/credentials/PresentationExchangeTest.kt @@ -16,6 +16,7 @@ import web5.sdk.crypto.InMemoryKeyManager import web5.sdk.dids.methods.key.DidKey import web5.sdk.testing.TestVectors import java.io.File +import java.security.SignatureException import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFails @@ -246,7 +247,7 @@ class PresentationExchangeTest { val vcJwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" - assertThrows { + assertThrows { PresentationExchange.satisfiesPresentationDefinition(listOf(vcJwt), pd) } } diff --git a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt index 25ab5524f..60f7c2876 100644 --- a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt +++ b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt @@ -135,7 +135,7 @@ class VerifiableCredentialTest { VerifiableCredential.verify(vcJwt) } assertContains( - exception.message!!, "not found in list of assertion methods", + exception.message!!, "Malformed JWT. Invalid base64url encoding for JWT payload.", ) } diff --git a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiablePresentationTest.kt b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiablePresentationTest.kt index 7fa565847..382a80fe9 100644 --- a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiablePresentationTest.kt +++ b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiablePresentationTest.kt @@ -169,7 +169,7 @@ class VerifiablePresentationTest { } assertContains( - exception.message!!, "not found in list of assertion methods", + exception.message!!, "Malformed JWT. Invalid base64url encoding for JWT payload.", ) } @@ -249,7 +249,7 @@ class VerifiablePresentationTest { VerifiablePresentation.verify(vpJwt) } assertContains( - exception.message!!, "not found in list of assertion methods", + exception.message!!, "Malformed JWT. Invalid base64url encoding for JWT payload.", ) } } \ No newline at end of file diff --git a/crypto/src/main/kotlin/web5/sdk/crypto/AwsKeyManager.kt b/crypto/src/main/kotlin/web5/sdk/crypto/AwsKeyManager.kt index c1e0f5aee..3e9bc4963 100644 --- a/crypto/src/main/kotlin/web5/sdk/crypto/AwsKeyManager.kt +++ b/crypto/src/main/kotlin/web5/sdk/crypto/AwsKeyManager.kt @@ -132,8 +132,6 @@ public class AwsKeyManager @JvmOverloads constructor( is ECPublicKey -> { val (x, y) = extractBase64UrlXYFromECPublicKey(publicKey) // todo how to get the curve name out of java.security.publicKey? - // publicKey.params.curve.toString() gives me toString() value of the curve type - // here tried casting it as ECNamedCurveSpec of bouncy castle and get name // not sure if correct. val namedCurve = publicKey as ECNamedCurveSpec Jwk.Builder("EC", namedCurve.name) @@ -248,7 +246,6 @@ public class AwsKeyManager @JvmOverloads constructor( /** * KMS returns the signature encoded as ASN.1 DER. Convert to the "R+S" concatenation format required by JWS. * https://www.rfc-editor.org/rfc/rfc7515#appendix-A.3.1 - * // todo how to eject from JWSAlgorithm here? need to write ECDSA.getSignatureByteArrayLength and transcodeSignatureToConcat? */ private fun transcodeDerSignatureToConcat(derSignature: ByteArray, algorithm: JWSAlgorithm): ByteArray { val signatureLength = ECDSA.getSignatureByteArrayLength(algorithm) diff --git a/crypto/src/main/kotlin/web5/sdk/crypto/Ed25519.kt b/crypto/src/main/kotlin/web5/sdk/crypto/Ed25519.kt index d517c4ced..2a3368891 100644 --- a/crypto/src/main/kotlin/web5/sdk/crypto/Ed25519.kt +++ b/crypto/src/main/kotlin/web5/sdk/crypto/Ed25519.kt @@ -47,7 +47,7 @@ public object Ed25519 : KeyGenerator, Signer { * @return The generated private key in Jwk format. */ override fun generatePrivateKey(options: KeyGenOptions?): Jwk { - // todo use tink to generate private key? + // TODO use tink to generate private key https://github.com/TBD54566975/web5-kt/issues/273 val privateKey = OctetKeyPairGenerator(Curve.Ed25519) .algorithm(JWSAlgorithm.EdDSA) .keyIDFromThumbprint(true) diff --git a/crypto/src/main/kotlin/web5/sdk/crypto/InMemoryKeyManager.kt b/crypto/src/main/kotlin/web5/sdk/crypto/InMemoryKeyManager.kt index ea60e5658..be50007fa 100644 --- a/crypto/src/main/kotlin/web5/sdk/crypto/InMemoryKeyManager.kt +++ b/crypto/src/main/kotlin/web5/sdk/crypto/InMemoryKeyManager.kt @@ -98,10 +98,10 @@ public class InMemoryKeyManager : KeyManager, KeyExporter, KeyImporter { * @param keySet A list of key representations in map format. * @return A list of key aliases belonging to the imported keys. */ - public fun import(keySet: Iterable>): List = keySet.map {map -> + public fun import(keySet: Iterable>): List = keySet.map { key -> // todo are all keySet.value of type Any in this case a possible Jwk? // we can just call toString() and call it good? am skeptical - val jwk = Json.parse(Json.stringify(map)) + val jwk = Json.parse(Json.stringify(key)) import(jwk) } diff --git a/crypto/src/main/kotlin/web5/sdk/crypto/Secp256k1.kt b/crypto/src/main/kotlin/web5/sdk/crypto/Secp256k1.kt index 903e82f05..4e77afff5 100644 --- a/crypto/src/main/kotlin/web5/sdk/crypto/Secp256k1.kt +++ b/crypto/src/main/kotlin/web5/sdk/crypto/Secp256k1.kt @@ -139,7 +139,7 @@ public object Secp256k1 : KeyGenerator, Signer { * @return A Jwk representing the generated private key. */ override fun generatePrivateKey(options: KeyGenOptions?): Jwk { - // todo use tink to generate private key? + // TODO use tink to generate private key https://github.com/TBD54566975/web5-kt/issues/273 val privateKey = ECKeyGenerator(com.nimbusds.jose.jwk.Curve.SECP256K1) .algorithm(JWSAlgorithm.ES256K) .provider(BouncyCastleProviderSingleton.getInstance()) diff --git a/crypto/src/main/kotlin/web5/sdk/crypto/jwk/Jwk.kt b/crypto/src/main/kotlin/web5/sdk/crypto/jwk/Jwk.kt index 41e5955a8..6b97733ef 100644 --- a/crypto/src/main/kotlin/web5/sdk/crypto/jwk/Jwk.kt +++ b/crypto/src/main/kotlin/web5/sdk/crypto/jwk/Jwk.kt @@ -64,7 +64,6 @@ public class Jwk( val thumbprintPayloadString = Json.stringify(thumbprintPayload) val thumbprintPayloadBytes = Convert(thumbprintPayloadString).toByteArray() - // todo read spec: https://datatracker.ietf.org/doc/html/rfc7638#section-3.1 val messageDigest = MessageDigest.getInstance("SHA-256") val thumbprintPayloadDigest = messageDigest.digest(thumbprintPayloadBytes) @@ -173,7 +172,7 @@ public class Jwk( if (kty == "OKP") { check(x != null) { "x is required for OKP keys" } } - // todo crv is required + return Jwk(kty, crv, use, alg, kid, d, x, y) } } diff --git a/dids/src/main/kotlin/web5/sdk/dids/didcore/DidDocument.kt b/dids/src/main/kotlin/web5/sdk/dids/didcore/DidDocument.kt index 71c15f77b..a8e5fbfd2 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/didcore/DidDocument.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/didcore/DidDocument.kt @@ -1,6 +1,7 @@ package web5.sdk.dids.didcore import com.fasterxml.jackson.annotation.JsonProperty +import web5.sdk.dids.DidResolvers import java.security.SignatureException /** @@ -110,13 +111,6 @@ public class DidDocument( */ @JvmOverloads public fun findAssertionMethodById(assertionMethodId: String? = null): VerificationMethod { - /* - // todo add this bit (see jwtutil) - val verificationMethodIds = setOf( - did.url, - "#${did.fragment}" - ) - */ require(!assertionMethod.isNullOrEmpty()) { throw SignatureException("No assertion methods found in DID document") } @@ -125,6 +119,30 @@ public class DidDocument( require(assertionMethod.contains(assertionMethodId)) { throw SignatureException("assertion method \"$assertionMethodId\" not found in list of assertion methods") } + + // todo bits from jwtutil - make sure these checks are actually required + val did = Did.parse(assertionMethodId) + val didResolutionResult = DidResolvers.resolve(did.url) + if (didResolutionResult.didResolutionMetadata.error != null) { + throw SignatureException( + "Signature verification failed: " + + "Failed to resolve DID ${did.url}. " + + "Error: ${didResolutionResult.didResolutionMetadata.error}" + ) + } + + val verificationMethodIds = setOf( + did.url, + "#${did.fragment}" + ) + + didResolutionResult.didDocument?.assertionMethod?.firstOrNull { + verificationMethodIds.contains(it) + } ?: throw SignatureException( + "Signature verification failed: Expected kid in JWS header to dereference " + + "a DID Document Verification Method with an Assertion verification relationship" + ) + } val assertionMethod: VerificationMethod = diff --git a/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt b/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt index 80dd1f259..d76727638 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt @@ -486,7 +486,7 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) { verificationMethodsById[verificationMethod.id] = verificationMethodId - // todo dropping support for ES256? + // TODO support Jwa.ES256 alg https://github.com/TBD54566975/web5-kt/issues/272 val keyType = when (publicKeyJwk.alg) { Jwa.EdDSA.name -> 0 Jwa.ES256K.name -> 1 @@ -629,7 +629,7 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) { val verificationMethodId = data["id"]!! val keyBytes = Convert(data["k"]!!, EncodingFormat.Base64Url).toByteArray() - // TODO(gabe): support other key types + // TODO(gabe): support other key types https://github.com/TBD54566975/web5-kt/issues/272 val publicKeyJwk = when (data["t"]!!) { "0" -> Ed25519.bytesToPublicKey(keyBytes) "1" -> Secp256k1.bytesToPublicKey(keyBytes) diff --git a/dids/src/test/kotlin/web5/sdk/dids/DidMethodTest.kt b/dids/src/test/kotlin/web5/sdk/dids/DidMethodTest.kt deleted file mode 100644 index fe1b4e099..000000000 --- a/dids/src/test/kotlin/web5/sdk/dids/DidMethodTest.kt +++ /dev/null @@ -1,67 +0,0 @@ -package web5.sdk.dids - -import io.ktor.client.engine.mock.MockEngine -import io.ktor.client.engine.mock.respond -import io.ktor.http.HttpHeaders -import io.ktor.http.HttpStatusCode -import io.ktor.http.headersOf -import io.ktor.utils.io.ByteReadChannel -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows -import web5.sdk.crypto.InMemoryKeyManager -import web5.sdk.dids.didcore.Did -import web5.sdk.dids.methods.jwk.DidJwk -import web5.sdk.dids.methods.key.DidKey -import web5.sdk.dids.methods.web.DidWeb -import web5.sdk.dids.methods.web.DidWebApi -import java.security.SignatureException -import kotlin.test.assertContains -import kotlin.test.assertEquals - -class DidMethodTest { - @Test - fun `findAssertionMethodById works with default`() { - val manager = InMemoryKeyManager() - val bearerDid = DidKey.create(manager) - - val verificationMethod = DidKey.resolve(bearerDid.did.uri) - .didDocument!! - .findAssertionMethodById() - assertEquals("${bearerDid.did.uri}#${Did.parse(bearerDid.did.uri).id}", verificationMethod.id) - } - - @Test - fun `findAssertionMethodById finds with id`() { - val manager = InMemoryKeyManager() - val bearerDid = DidKey.create(manager) - - val assertionMethodId = "${bearerDid.did.uri}#${Did.parse(bearerDid.did.uri).id}" - val verificationMethod = DidKey.resolve(bearerDid.did.uri) - .didDocument!! - .findAssertionMethodById(assertionMethodId) - assertEquals(assertionMethodId, verificationMethod.id) - } - - @Test - fun `findAssertionMethodById throws exception`() { - val manager = InMemoryKeyManager() - val bearerDid = DidKey.create(manager) - - val exception = assertThrows { - DidKey.resolve(bearerDid.did.uri) - .didDocument!! - .findAssertionMethodById("made up assertion method id") - } - assertContains(exception.message!!, "assertion method \"made up assertion method id\" not found") - } - - @Test - fun `findAssertionMethodById throws exception when no assertion methods are found`() { - val manager = InMemoryKeyManager() - val did = DidJwk.create(manager) - val exception = assertThrows { - did.document.findAssertionMethodById("made up assertion method id") - } - assertEquals("assertion method \"made up assertion method id\" not found in list of assertion methods", exception.message) - } -} \ No newline at end of file diff --git a/dids/src/test/kotlin/web5/sdk/dids/didcore/DidDocumentTest.kt b/dids/src/test/kotlin/web5/sdk/dids/didcore/DidDocumentTest.kt index 9743d40fc..4a0f5c073 100644 --- a/dids/src/test/kotlin/web5/sdk/dids/didcore/DidDocumentTest.kt +++ b/dids/src/test/kotlin/web5/sdk/dids/didcore/DidDocumentTest.kt @@ -4,8 +4,11 @@ import org.junit.jupiter.api.Nested import org.junit.jupiter.api.assertThrows import web5.sdk.crypto.AlgorithmId import web5.sdk.crypto.InMemoryKeyManager +import web5.sdk.dids.methods.jwk.DidJwk +import web5.sdk.dids.methods.key.DidKey import java.security.SignatureException import kotlin.test.Test +import kotlin.test.assertContains import kotlin.test.assertEquals import kotlin.test.assertNull @@ -171,6 +174,7 @@ class DidDocumentTest { } } + // todo this test fails because of what i added to DidDocument#findAssertionMethodById() @Test fun `findAssertionMethodById returns assertion verification method if id is found`() { val assertionMethods = listOf("foo") @@ -189,6 +193,53 @@ class DidDocumentTest { assertEquals("type", assertionMethod.type) assertEquals("controller", assertionMethod.controller) } + + @Test + fun `findAssertionMethodById works with default`() { + val manager = InMemoryKeyManager() + val bearerDid = DidKey.create(manager) + + val verificationMethod = DidKey.resolve(bearerDid.did.uri) + .didDocument!! + .findAssertionMethodById() + assertEquals("${bearerDid.did.uri}#${Did.parse(bearerDid.did.uri).id}", verificationMethod.id) + } + + @Test + fun `findAssertionMethodById finds with id`() { + val manager = InMemoryKeyManager() + val bearerDid = DidKey.create(manager) + + val assertionMethodId = "${bearerDid.did.uri}#${Did.parse(bearerDid.did.uri).id}" + val verificationMethod = DidKey.resolve(bearerDid.did.uri) + .didDocument!! + .findAssertionMethodById(assertionMethodId) + assertEquals(assertionMethodId, verificationMethod.id) + } + + @Test + fun `findAssertionMethodById throws exception`() { + val manager = InMemoryKeyManager() + val bearerDid = DidKey.create(manager) + + val exception = assertThrows { + DidKey.resolve(bearerDid.did.uri) + .didDocument!! + .findAssertionMethodById("made up assertion method id") + } + assertContains(exception.message!!, "assertion method \"made up assertion method id\" not found") + } + + @Test + fun `findAssertionMethodById throws exception when no assertion methods are found`() { + val manager = InMemoryKeyManager() + val did = DidJwk.create(manager) + val exception = assertThrows { + did.document.findAssertionMethodById("made up assertion method id") + } + assertEquals("assertion method \"made up assertion method id\" " + + "not found in list of assertion methods", exception.message) + } } @Nested diff --git a/dids/src/test/kotlin/web5/sdk/dids/didcore/DidTest.kt b/dids/src/test/kotlin/web5/sdk/dids/didcore/DidTest.kt index 423dd5d4d..b99898515 100644 --- a/dids/src/test/kotlin/web5/sdk/dids/didcore/DidTest.kt +++ b/dids/src/test/kotlin/web5/sdk/dids/didcore/DidTest.kt @@ -41,8 +41,8 @@ class DidTest { @Test fun `Parser parses a valid did`() { - // todo adding /path after abcdefghi messes up the parsing of params (comes in null) - // to be addressed via gh issue https://github.com/TBD54566975/web5-spec/issues/120 + // TODO adding /path after abcdefghi messes up the parsing of params (comes in null) + // https://github.com/TBD54566975/web5-spec/issues/120 val did = Did.Parser.parse("did:example:123456789abcdefghi;foo=bar;baz=qux?foo=bar&baz=qux#keys-1") assertEquals("did:example:123456789abcdefghi", did.uri) assertEquals("123456789abcdefghi", did.id) diff --git a/jose/src/main/kotlin/web5/sdk/jose/JwtClaimsSetSerializer.kt b/jose/src/main/kotlin/web5/sdk/jose/JwtClaimsSetSerializer.kt index 8ef5a4649..b60503fbe 100644 --- a/jose/src/main/kotlin/web5/sdk/jose/JwtClaimsSetSerializer.kt +++ b/jose/src/main/kotlin/web5/sdk/jose/JwtClaimsSetSerializer.kt @@ -1,10 +1,20 @@ package web5.sdk.jose import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.JsonDeserializer +import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.JsonSerializer import com.fasterxml.jackson.databind.SerializerProvider import web5.sdk.jose.jwt.JwtClaimsSet +/** + * JwtClaimsSet serializer. + * + * Used to serialize JwtClaimsSet into a JSON object that flattens the misc claims + * + */ public class JwtClaimsSetSerializer : JsonSerializer() { override fun serialize(jwtClaimsSet: JwtClaimsSet, gen: JsonGenerator, serializers: SerializerProvider?) { @@ -25,4 +35,47 @@ public class JwtClaimsSetSerializer : JsonSerializer() { gen.writeEndObject() } +} + +/** + * JwtClaimsSet deserializer. + * + * Used to deserialize JSON object JwtClaimsSet + * that takes miscellaneous claims and puts them as values inside misc key + */ +public class JwtClaimsSetDeserializer : JsonDeserializer() { + + public override fun deserialize(p: JsonParser, ctxt: DeserializationContext): JwtClaimsSet { + val jsonNode = p.codec.readTree(p) + val reservedClaims = setOf( + "iss", + "sub", + "aud", + "exp", + "nbf", + "iat", + "jti" + ) + + val miscClaims: MutableMap = mutableMapOf() + + val fields = jsonNode.fields() + while (fields.hasNext()) { + val (key, value) = fields.next() + if (!reservedClaims.contains(key)) { + miscClaims[key] = value + } + } + + return JwtClaimsSet( + iss = jsonNode.get("iss")?.asText(), + sub = jsonNode.get("sub")?.asText(), + aud = jsonNode.get("aud")?.asText(), + exp = jsonNode.get("exp")?.asLong(), + nbf = jsonNode.get("nbf")?.asLong(), + iat = jsonNode.get("iat")?.asLong(), + jti = jsonNode.get("jti")?.asText(), + misc = miscClaims + ) + } } \ No newline at end of file diff --git a/jose/src/main/kotlin/web5/sdk/jose/jws/Jws.kt b/jose/src/main/kotlin/web5/sdk/jose/jws/Jws.kt index 67e465984..a5ba2878a 100644 --- a/jose/src/main/kotlin/web5/sdk/jose/jws/Jws.kt +++ b/jose/src/main/kotlin/web5/sdk/jose/jws/Jws.kt @@ -7,6 +7,7 @@ import web5.sdk.crypto.Crypto import web5.sdk.dids.DidResolvers import web5.sdk.dids.did.BearerDid import web5.sdk.dids.exceptions.PublicKeyJwkMissingException +import java.security.SignatureException /** * Json Web Signature (JWS) is a compact signature format that is used to secure messages. @@ -31,21 +32,21 @@ public object Jws { try { header = JwsHeader.fromBase64Url(parts[0]) } catch (e: Exception) { - throw IllegalArgumentException("Malformed JWT. Failed to decode header: ${e.message}") + throw SignatureException("Malformed JWT. Failed to decode header: ${e.message}") } val payload: ByteArray try { - payload = Convert(parts[1]).toByteArray() + payload = Convert(parts[1], EncodingFormat.Base64Url).toByteArray() } catch (e: Exception) { - throw IllegalArgumentException("Malformed JWT. Failed to decode payload: ${e.message}") + throw SignatureException("Malformed JWT. Failed to decode payload: ${e.message}") } val signature: ByteArray try { - signature = Convert(parts[2]).toByteArray() + signature = Convert(parts[2], EncodingFormat.Base64Url).toByteArray() } catch (e: Exception) { - throw IllegalArgumentException("Malformed JWT. Failed to decode signature: ${e.message}") + throw SignatureException("Malformed JWT. Failed to decode signature: ${e.message}") } return DecodedJws(header, payload, signature, parts) @@ -56,7 +57,6 @@ public object Jws { * * @param bearerDid The Bearer DID to sign with * @param payload The payload to sign - * @param header The JWS header * @param detached Whether to include the payload in the JWS string output * @return */ @@ -86,8 +86,9 @@ public object Jws { val headerBase64Url = Convert(Json.stringify(jwsHeader)).toBase64Url() val payloadBase64Url = Convert(payload).toBase64Url() - val toSign = "$headerBase64Url.$payloadBase64Url" - val toSignBytes = Convert(toSign).toByteArray() + val toSignBase64Url = "$headerBase64Url.$payloadBase64Url" + val toSignBytes = Convert(toSignBase64Url).toByteArray() + val signatureBytes = signer.invoke(toSignBytes) val signatureBase64Url = Convert(signatureBytes).toBase64Url() diff --git a/jose/src/main/kotlin/web5/sdk/jose/jwt/Jwt.kt b/jose/src/main/kotlin/web5/sdk/jose/jwt/Jwt.kt index 016b90d56..01ef2ad85 100644 --- a/jose/src/main/kotlin/web5/sdk/jose/jwt/Jwt.kt +++ b/jose/src/main/kotlin/web5/sdk/jose/jwt/Jwt.kt @@ -1,18 +1,18 @@ package web5.sdk.jose.jwt -import com.fasterxml.jackson.core.JsonGenerator -import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.databind.JsonSerializer -import com.fasterxml.jackson.databind.SerializerProvider +import com.fasterxml.jackson.databind.annotation.JsonDeserialize import com.fasterxml.jackson.databind.annotation.JsonSerialize +import com.fasterxml.jackson.databind.module.SimpleModule import web5.sdk.common.Convert import web5.sdk.common.EncodingFormat import web5.sdk.common.Json import web5.sdk.dids.did.BearerDid +import web5.sdk.jose.JwtClaimsSetDeserializer import web5.sdk.jose.JwtClaimsSetSerializer import web5.sdk.jose.jws.DecodedJws import web5.sdk.jose.jws.Jws import web5.sdk.jose.jws.JwsHeader +import java.security.SignatureException /** * Json Web Token (JWT) is a compact, URL-safe means of representing claims to be transferred between two parties. @@ -33,10 +33,11 @@ public object Jwt { val claims: JwtClaimsSet try { val payload = Convert(decodedJws.payload).toStr() - val decodedPayload = Convert(payload, EncodingFormat.Base64Url).toStr() - claims = JwtClaimsSet.fromJson(Json.jsonMapper.readTree(decodedPayload)) + val jwtModule = SimpleModule().addDeserializer(JwtClaimsSet::class.java, JwtClaimsSetDeserializer()) + Json.jsonMapper.registerModule(jwtModule) + claims = Json.jsonMapper.readValue(payload, JwtClaimsSet::class.java) } catch (e: Exception) { - throw IllegalArgumentException( + throw SignatureException( "Malformed JWT. " + "Invalid base64url encoding for JWT payload. ${e.message}" ) @@ -59,7 +60,10 @@ public object Jwt { * @return The signed JWT */ public fun sign(did: BearerDid, payload: JwtClaimsSet): String { - val payloadBytes = Convert(Json.stringify(payload)).toByteArray() + val jwtModule = SimpleModule().addSerializer(JwtClaimsSet::class.java, JwtClaimsSetSerializer()) + Json.jsonMapper.registerModule(jwtModule) + val payloadJsonString = Json.jsonMapper.writeValueAsString(payload) + val payloadBytes = Convert(payloadJsonString).toByteArray() return Jws.sign(did, payloadBytes) } @@ -122,6 +126,7 @@ public typealias JwtHeader = JwsHeader * @property misc additional claims (i.e. VerifiableCredential, VerifiablePresentation) */ @JsonSerialize(using = JwtClaimsSetSerializer::class) +@JsonDeserialize(using = JwtClaimsSetDeserializer::class) public class JwtClaimsSet( public val iss: String? = null, public val sub: String? = null, @@ -133,48 +138,48 @@ public class JwtClaimsSet( public val misc: Map = emptyMap() ) { - public companion object { - - /** - * Takes a JsonNode representation of a claim and builds a JwtClaimsSet. - * - * @param jsonNode The JsonNode representation of a claim - * @return JwtClaimsSet - */ - public fun fromJson(jsonNode: JsonNode): JwtClaimsSet { - val reservedClaims = setOf( - "iss", - "sub", - "aud", - "exp", - "nbf", - "iat", - "jti" - ) - - val miscClaims: MutableMap = mutableMapOf() - - val fields = jsonNode.fields() - while (fields.hasNext()) { - val (key, value) = fields.next() - if (!reservedClaims.contains(key)) { - miscClaims[key] = value - } - } - - return JwtClaimsSet( - iss = jsonNode.get("iss")?.asText(), - sub = jsonNode.get("sub")?.asText(), - aud = jsonNode.get("aud")?.asText(), - exp = jsonNode.get("exp")?.asLong(), - nbf = jsonNode.get("nbf")?.asLong(), - iat = jsonNode.get("iat")?.asLong(), - jti = jsonNode.get("jti")?.asText(), - misc = miscClaims - ) - - } - } +// public companion object { +// +// /** +// * Takes a JsonNode representation of a claim and builds a JwtClaimsSet. +// * +// * @param jsonNode The JsonNode representation of a claim +// * @return JwtClaimsSet +// */ +// public fun fromJson(jsonNode: JsonNode): JwtClaimsSet { +// val reservedClaims = setOf( +// "iss", +// "sub", +// "aud", +// "exp", +// "nbf", +// "iat", +// "jti" +// ) +// +// val miscClaims: MutableMap = mutableMapOf() +// +// val fields = jsonNode.fields() +// while (fields.hasNext()) { +// val (key, value) = fields.next() +// if (!reservedClaims.contains(key)) { +// miscClaims[key] = value +// } +// } +// +// return JwtClaimsSet( +// iss = jsonNode.get("iss")?.asText(), +// sub = jsonNode.get("sub")?.asText(), +// aud = jsonNode.get("aud")?.asText(), +// exp = jsonNode.get("exp")?.asLong(), +// nbf = jsonNode.get("nbf")?.asLong(), +// iat = jsonNode.get("iat")?.asLong(), +// jti = jsonNode.get("jti")?.asText(), +// misc = miscClaims +// ) +// +// } +// } /** * Builder for JwtClaimsSet. diff --git a/web5-spec b/web5-spec index 10c1bdc51..ed1d092c1 160000 --- a/web5-spec +++ b/web5-spec @@ -1 +1 @@ -Subproject commit 10c1bdc51470271ce31a56401eb4e1bd40ed730f +Subproject commit ed1d092c1c4cbe06ed2f640fbac8b7b085419176 From f56860c33573d9c65dcc61e42d5aa20f112ba48c Mon Sep 17 00:00:00 2001 From: Jiyoon Koo Date: Wed, 20 Mar 2024 17:01:59 -0700 Subject: [PATCH 28/47] removing jwk serializer/deserializer --- .../kotlin/web5/sdk/dids/Serialization.kt | 29 ------------------- .../sdk/dids/didcore/VerificationMethod.kt | 6 ---- .../web5/sdk/dids/methods/dht/DidDhtTest.kt | 2 -- 3 files changed, 37 deletions(-) diff --git a/dids/src/main/kotlin/web5/sdk/dids/Serialization.kt b/dids/src/main/kotlin/web5/sdk/dids/Serialization.kt index 98e5e2239..5f249ee4f 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/Serialization.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/Serialization.kt @@ -1,42 +1,13 @@ package web5.sdk.dids -import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.core.JsonParser import com.fasterxml.jackson.core.JsonProcessingException import com.fasterxml.jackson.databind.DeserializationContext import com.fasterxml.jackson.databind.JsonDeserializer import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.databind.JsonSerializer -import com.fasterxml.jackson.databind.SerializerProvider -import web5.sdk.common.Json -import web5.sdk.crypto.jwk.Jwk import web5.sdk.dids.didcore.Purpose import java.io.IOException -// todo delete the de/serializers for jwk -/** - * Serialize Jwk into String. - */ -public class JwkSerializer : JsonSerializer() { - public override fun serialize(jwk: Jwk?, gen: JsonGenerator, serializers: SerializerProvider?) { - val jwkString = jwk?.let { Json.stringify(it) } - gen.writeRawValue(jwkString) - } - -} - -/** - * Deserialize String into Jwk. - * - */ -public class JwkDeserializer : JsonDeserializer() { - override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Jwk { - val node = p.codec.readTree(p) - val jwkJson = node.toString() - return Json.parse(jwkJson) - } -} - /** * Deserialize String into List of Purpose enums. * diff --git a/dids/src/main/kotlin/web5/sdk/dids/didcore/VerificationMethod.kt b/dids/src/main/kotlin/web5/sdk/dids/didcore/VerificationMethod.kt index bc1f79005..95abf716b 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/didcore/VerificationMethod.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/didcore/VerificationMethod.kt @@ -1,10 +1,6 @@ package web5.sdk.dids.didcore -import com.fasterxml.jackson.databind.annotation.JsonDeserialize -import com.fasterxml.jackson.databind.annotation.JsonSerialize import web5.sdk.crypto.jwk.Jwk -import web5.sdk.dids.JwkSerializer -import web5.sdk.dids.JwkDeserializer /** * VerificationMethod expresses verification methods, such as cryptographic @@ -26,8 +22,6 @@ public class VerificationMethod( public val id: String, public val type: String, public val controller: String, - @JsonSerialize(using = JwkSerializer::class) - @JsonDeserialize(using = JwkDeserializer::class) public val publicKeyJwk: Jwk? = null ) { /** diff --git a/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DidDhtTest.kt b/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DidDhtTest.kt index bccf0479c..9ec39f28a 100644 --- a/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DidDhtTest.kt +++ b/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DidDhtTest.kt @@ -22,7 +22,6 @@ import web5.sdk.crypto.InMemoryKeyManager import web5.sdk.crypto.JwaCurve import web5.sdk.crypto.jwk.Jwk import web5.sdk.dids.DidResolutionResult -import web5.sdk.dids.JwkDeserializer import web5.sdk.dids.PurposesDeserializer import web5.sdk.dids.didcore.DidDocument import web5.sdk.dids.didcore.Purpose @@ -355,7 +354,6 @@ class Web5TestVectorsDidDht { ) data class VerificationMethodInput( - @JsonDeserialize(using = JwkDeserializer::class) val jwk: Jwk, @JsonDeserialize(using = PurposesDeserializer::class) val purposes: List From 129483b8d929ca6fde00be625fddc90359705af5 Mon Sep 17 00:00:00 2001 From: Jiyoon Koo Date: Thu, 21 Mar 2024 14:30:24 -0700 Subject: [PATCH 29/47] tests pass. writing tests for jwt in process. jwk still needs tests written --- build.gradle.kts | 1 + .../main/kotlin/web5/sdk/common/Convert.kt | 2 +- .../kotlin/web5/sdk/crypto/AwsKeyManager.kt | 44 +----- .../src/main/kotlin/web5/sdk/crypto/Crypto.kt | 4 +- crypto/src/main/kotlin/web5/sdk/crypto/Dsa.kt | 17 ++- .../main/kotlin/web5/sdk/crypto/Ed25519.kt | 17 +-- .../web5/sdk/crypto/InMemoryKeyManager.kt | 35 ----- .../main/kotlin/web5/sdk/crypto/Secp256k1.kt | 19 +-- .../main/kotlin/web5/sdk/crypto/jwk/Jwk.kt | 5 +- .../web5/sdk/crypto/AwsKeyManagerTest.kt | 1 - .../web5/sdk/crypto/InMemoryKeyManagerTest.kt | 50 +------ .../kotlin/web5/sdk/crypto/Secp256k1Test.kt | 5 +- .../web5/sdk/dids/didcore/DidDocument.kt | 55 +++++--- .../web5/sdk/dids/methods/dht/DidDht.kt | 2 +- .../web5/sdk/dids/didcore/DidDocumentTest.kt | 9 +- .../web5/sdk/dids/methods/dht/DhtTest.kt | 2 +- .../web5/sdk/dids/methods/dht/DidDhtTest.kt | 53 +------- jose/build.gradle.kts | 1 + .../web5/sdk/jose/JwtClaimsSetSerializer.kt | 13 +- jose/src/main/kotlin/web5/sdk/jose/jws/Jws.kt | 14 +- jose/src/main/kotlin/web5/sdk/jose/jwt/Jwt.kt | 57 ++------ .../test/kotlin/web5/sdk/jose/jws/JwsTest.kt | 125 ++++++++++++++++++ .../test/kotlin/web5/sdk/jose/jwt/JwtTest.kt | 31 +++++ 23 files changed, 272 insertions(+), 290 deletions(-) create mode 100644 jose/src/test/kotlin/web5/sdk/jose/jws/JwsTest.kt create mode 100644 jose/src/test/kotlin/web5/sdk/jose/jwt/JwtTest.kt diff --git a/build.gradle.kts b/build.gradle.kts index 23eb9bafc..80efcd1e2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -63,6 +63,7 @@ dependencies { api(project(":credentials")) api(project(":crypto")) api(project(":dids")) + api(project(":jose")) detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.23.+") } diff --git a/common/src/main/kotlin/web5/sdk/common/Convert.kt b/common/src/main/kotlin/web5/sdk/common/Convert.kt index c7603dbd2..e758bbc56 100644 --- a/common/src/main/kotlin/web5/sdk/common/Convert.kt +++ b/common/src/main/kotlin/web5/sdk/common/Convert.kt @@ -173,7 +173,7 @@ public class Convert(private val value: T, private val kind: EncodingFormat? EncodingFormat.Base58Btc -> Base58Btc.decode(this.value) EncodingFormat.Base64Url -> B64URL_DECODER.decode(this.value) EncodingFormat.ZBase32 -> ZBase32.decode(this.value) - null -> this.value.toByteArray() // todo do we need Charset_UTF8 here? + null -> this.value.toByteArray() } } diff --git a/crypto/src/main/kotlin/web5/sdk/crypto/AwsKeyManager.kt b/crypto/src/main/kotlin/web5/sdk/crypto/AwsKeyManager.kt index 3e9bc4963..5ff2f402f 100644 --- a/crypto/src/main/kotlin/web5/sdk/crypto/AwsKeyManager.kt +++ b/crypto/src/main/kotlin/web5/sdk/crypto/AwsKeyManager.kt @@ -14,6 +14,7 @@ import com.amazonaws.services.kms.model.SignRequest import com.amazonaws.services.kms.model.SigningAlgorithmSpec import com.nimbusds.jose.JWSAlgorithm import com.nimbusds.jose.crypto.impl.ECDSA +import com.nimbusds.jose.jwk.ECKey import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo import org.bouncycastle.crypto.ExtendedDigest import org.bouncycastle.crypto.digests.SHA256Digest @@ -130,49 +131,16 @@ public class AwsKeyManager @JvmOverloads constructor( val algorithmDetails = getAlgorithmDetails(publicKeyResponse.keySpec.enum()) val jwkBuilder = when (publicKey) { is ECPublicKey -> { - val (x, y) = extractBase64UrlXYFromECPublicKey(publicKey) - // todo how to get the curve name out of java.security.publicKey? - // not sure if correct. - val namedCurve = publicKey as ECNamedCurveSpec - Jwk.Builder("EC", namedCurve.name) - .x(x) - .y(y) + val key = ECKey.Builder(JwaCurve.toJwkCurve(algorithmDetails.curve), publicKey).build() + Jwk.Builder("EC", key.curve.name) + .x(key.x.toString()) + .y(key.y.toString()) } - else -> throw IllegalArgumentException("Unknown key type $publicKey") } - return jwkBuilder - .algorithm(algorithmDetails.algorithm.name) - .keyId(keyAlias) - .keyUse("sig") - .build() - } - - private fun extractBase64UrlXYFromECPublicKey(ecPublicKey: ECPublicKey): Pair { - val ecPoint = ecPublicKey.w - val x = ecPoint.affineX.toByteArray().trimLeadingZeroes() - val y = ecPoint.affineY.toByteArray().trimLeadingZeroes() - - val base64UrlX = Convert(x, EncodingFormat.Base64Url).toStr() - val base64UrlY = Convert(y, EncodingFormat.Base64Url).toStr() - - return Pair(base64UrlX, base64UrlY) + return jwkBuilder.build() } - /** - * // todo not sure where this should live - * Extension function to remove leading zero bytes which might be - * added by BigInteger's toByteArray() method to represent positive numbers. - */ - private fun ByteArray.trimLeadingZeroes(): ByteArray = - if (this.isEmpty()) { - this - } else if (this[0] == 0.toByte()) { - this.dropWhile { it == 0.toByte() }.toByteArray() - } else { - this - } - /** * Signs the provided payload using the private key identified by the provided alias. * diff --git a/crypto/src/main/kotlin/web5/sdk/crypto/Crypto.kt b/crypto/src/main/kotlin/web5/sdk/crypto/Crypto.kt index c71371bda..e8d643f05 100644 --- a/crypto/src/main/kotlin/web5/sdk/crypto/Crypto.kt +++ b/crypto/src/main/kotlin/web5/sdk/crypto/Crypto.kt @@ -233,9 +233,7 @@ public object Crypto { * @return The [JwaCurve] used in the Jwk, or `null` if the curve is not defined or recognized. */ public fun getJwkCurve(jwk: Jwk): JwaCurve? { - val rawCurve = jwk.crv - - return rawCurve?.let { JwaCurve.parse(it) } + return JwaCurve.parse(jwk.crv) } /** diff --git a/crypto/src/main/kotlin/web5/sdk/crypto/Dsa.kt b/crypto/src/main/kotlin/web5/sdk/crypto/Dsa.kt index bbe56d96c..ecf278b5a 100644 --- a/crypto/src/main/kotlin/web5/sdk/crypto/Dsa.kt +++ b/crypto/src/main/kotlin/web5/sdk/crypto/Dsa.kt @@ -12,6 +12,21 @@ public enum class JwaCurve { Ed25519; public companion object { + + /** + * Convert JwaCurve nimbusds JWK curve. + * Used to temporarily bridge the gap between moving from nimbusds JWK methods + * to rolling our own JWK methods + * @param curve + * @return nimbus JWK Curve + */ + public fun toJwkCurve(curve: JwaCurve): Curve { + return when (curve) { + secp256k1 -> Curve.SECP256K1 + Ed25519 -> Curve.Ed25519 + } + } + /** * Parse name of a curve into JwaCurve. * @@ -34,7 +49,7 @@ public enum class JwaCurve { } /** - * JSON Web Algorithm Curve. + * JSON Web Algorithm. */ public enum class Jwa { EdDSA, diff --git a/crypto/src/main/kotlin/web5/sdk/crypto/Ed25519.kt b/crypto/src/main/kotlin/web5/sdk/crypto/Ed25519.kt index 2a3368891..af6f47281 100644 --- a/crypto/src/main/kotlin/web5/sdk/crypto/Ed25519.kt +++ b/crypto/src/main/kotlin/web5/sdk/crypto/Ed25519.kt @@ -48,20 +48,13 @@ public object Ed25519 : KeyGenerator, Signer { */ override fun generatePrivateKey(options: KeyGenOptions?): Jwk { // TODO use tink to generate private key https://github.com/TBD54566975/web5-kt/issues/273 - val privateKey = OctetKeyPairGenerator(Curve.Ed25519) - .algorithm(JWSAlgorithm.EdDSA) - .keyIDFromThumbprint(true) - .keyUse(KeyUse.SIGNATURE) - .generate() + val privateKey = OctetKeyPairGenerator(Curve.Ed25519).generate() val jwk = Jwk.Builder("OKP", privateKey.curve.name) - .algorithm(algorithm.name) - .keyUse("sig") .privateKey(privateKey.d.toString()) .x(privateKey.x.toString()) .build() - jwk.kid = jwk.computeThumbprint() return jwk } @@ -76,15 +69,14 @@ public object Ed25519 : KeyGenerator, Signer { validateKey(privateKey) val jwk = Jwk.Builder(privateKey.kty, curve.name) - .keyUse("sig") // todo is publicKey JWK's keyUse "sig"? had to edit create.json .algorithm(algorithm.name) .apply { - privateKey.kid?.let { keyId(it) } + privateKey.use?.let { keyUse(it) } + privateKey.alg?.let { algorithm(it) } privateKey.x?.let { x(it) } } .build() - jwk.kid = jwk.kid ?: jwk.computeThumbprint() return jwk } @@ -108,7 +100,6 @@ public object Ed25519 : KeyGenerator, Signer { val base64UrlEncodedPublicKey = Convert(publicKeyBytes).toBase64Url() return Jwk.Builder("OKP", curve.name) - .keyUse("sig") .algorithm(algorithm.name) .privateKey(base64UrlEncodedPrivateKey) .x(base64UrlEncodedPublicKey) @@ -120,11 +111,9 @@ public object Ed25519 : KeyGenerator, Signer { val jwk = Jwk.Builder("OKP", curve.name) .algorithm(algorithm.name) - .keyUse("sig") .x(base64UrlEncodedPublicKey) .build() - jwk.kid = jwk.kid ?: jwk.computeThumbprint() return jwk } diff --git a/crypto/src/main/kotlin/web5/sdk/crypto/InMemoryKeyManager.kt b/crypto/src/main/kotlin/web5/sdk/crypto/InMemoryKeyManager.kt index be50007fa..a5249e6ec 100644 --- a/crypto/src/main/kotlin/web5/sdk/crypto/InMemoryKeyManager.kt +++ b/crypto/src/main/kotlin/web5/sdk/crypto/InMemoryKeyManager.kt @@ -92,41 +92,6 @@ public class InMemoryKeyManager : KeyManager, KeyExporter, KeyImporter { private fun getPrivateKey(keyAlias: String) = keyStore[keyAlias] ?: throw IllegalArgumentException("key with alias $keyAlias not found") - /** - * Imports a list of keys represented as a list of maps and returns a list of key aliases referring to them. - * - * @param keySet A list of key representations in map format. - * @return A list of key aliases belonging to the imported keys. - */ - public fun import(keySet: Iterable>): List = keySet.map { key -> - // todo are all keySet.value of type Any in this case a possible Jwk? - // we can just call toString() and call it good? am skeptical - val jwk = Json.parse(Json.stringify(key)) - import(jwk) - } - - /** - * Imports a single key and returns the alias that refers to it. - * - * @param jwk A Jwk object representing the key to be imported. - * @return The alias belonging to the imported key. - */ - public fun import(jwk: Jwk): String { - var kid = jwk.kid - if (kid.isNullOrEmpty()) { - kid = jwk.computeThumbprint() - } - keyStore.putIfAbsent(kid, jwk) - return kid - } - - /** - * Exports all stored keys as a list of maps. - * - * @return A list of key representations in map format. - */ - public fun export(): List> = keyStore.map { keyIdToJwk -> Json.stringify(keyIdToJwk.value).toMap() } - override fun exportKey(keyId: String): Jwk { return this.getPrivateKey(keyId) } diff --git a/crypto/src/main/kotlin/web5/sdk/crypto/Secp256k1.kt b/crypto/src/main/kotlin/web5/sdk/crypto/Secp256k1.kt index 4e77afff5..602af6eca 100644 --- a/crypto/src/main/kotlin/web5/sdk/crypto/Secp256k1.kt +++ b/crypto/src/main/kotlin/web5/sdk/crypto/Secp256k1.kt @@ -2,6 +2,7 @@ package web5.sdk.crypto import com.nimbusds.jose.JWSAlgorithm import com.nimbusds.jose.crypto.bc.BouncyCastleProviderSingleton +import com.nimbusds.jose.jwk.Curve import com.nimbusds.jose.jwk.ECKey import com.nimbusds.jose.jwk.KeyUse import com.nimbusds.jose.jwk.gen.ECKeyGenerator @@ -140,22 +141,16 @@ public object Secp256k1 : KeyGenerator, Signer { */ override fun generatePrivateKey(options: KeyGenOptions?): Jwk { // TODO use tink to generate private key https://github.com/TBD54566975/web5-kt/issues/273 - val privateKey = ECKeyGenerator(com.nimbusds.jose.jwk.Curve.SECP256K1) - .algorithm(JWSAlgorithm.ES256K) + val privateKey = ECKeyGenerator(Curve.SECP256K1) .provider(BouncyCastleProviderSingleton.getInstance()) - .keyIDFromThumbprint(true) - .keyUse(KeyUse.SIGNATURE) .generate() val jwk = Jwk.Builder("EC", privateKey.curve.name) - .keyUse("sig") - .algorithm(algorithm.name) .privateKey(privateKey.d.toString()) .x(privateKey.x.toString()) .y(privateKey.y.toString()) .build() - jwk.kid = jwk.computeThumbprint() return jwk } @@ -163,16 +158,15 @@ public object Secp256k1 : KeyGenerator, Signer { validateKey(privateKey) val jwk = Jwk.Builder(privateKey.kty, curve.name) - .keyUse("sig") // todo is publicKey JWK's keyUse "sig"? had to edit create.json .algorithm(algorithm.name) .apply { - privateKey.kid?.let { keyId(it) } + privateKey.use?.let { keyUse(it) } + privateKey.alg?.let { algorithm(it) } privateKey.x?.let { x(it) } privateKey.y?.let { y(it) } } .build() - jwk.kid = jwk.kid ?: jwk.computeThumbprint() return jwk } @@ -192,7 +186,6 @@ public object Secp256k1 : KeyGenerator, Signer { } override fun bytesToPrivateKey(privateKeyBytes: ByteArray): Jwk { - // todo what is this MAGIC??? var pointQ: ECPoint = spec.g.multiply(BigInteger(1, privateKeyBytes)) pointQ = pointQ.normalize() @@ -201,7 +194,6 @@ public object Secp256k1 : KeyGenerator, Signer { return Jwk.Builder("EC", curve.name) .algorithm(algorithm.name) - .keyUse("sig") .x(Convert(rawX).toBase64Url()) .y(Convert(rawY).toBase64Url()) .privateKey(Convert(privateKeyBytes).toBase64Url()) @@ -214,11 +206,10 @@ public object Secp256k1 : KeyGenerator, Signer { val jwk = Jwk.Builder("EC", curve.name) .algorithm(algorithm.name) - .keyUse("sig") .x(Convert(xBytes).toBase64Url()) .y(Convert(yBytes).toBase64Url()) .build() - jwk.kid = jwk.computeThumbprint() + return jwk } diff --git a/crypto/src/main/kotlin/web5/sdk/crypto/jwk/Jwk.kt b/crypto/src/main/kotlin/web5/sdk/crypto/jwk/Jwk.kt index 6b97733ef..455952d8c 100644 --- a/crypto/src/main/kotlin/web5/sdk/crypto/jwk/Jwk.kt +++ b/crypto/src/main/kotlin/web5/sdk/crypto/jwk/Jwk.kt @@ -2,6 +2,7 @@ package web5.sdk.crypto.jwk import web5.sdk.common.Convert import web5.sdk.common.Json +import web5.sdk.crypto.Ed25519 import java.security.MessageDigest /** @@ -29,7 +30,6 @@ import java.security.MessageDigest * @property y Y coordinate for EC keys. * */ -// todo use jcs for canonicalization, instead of using de/serlization. public class Jwk( public val kty: String, public val crv: String, @@ -164,7 +164,8 @@ public class Jwk( * @return Jwk object */ public fun build(): Jwk { - // todo move these checks out to Ed25519 or Secp256k1 classes? + // TODO move these checks out to Ed25519 or Secp256k1 classes? + // https://github.com/TBD54566975/web5-kt/issues/276 if (kty == "EC") { check(x != null) { "x is required for EC keys" } check(y != null) { "y is required for EC keys" } diff --git a/crypto/src/test/kotlin/web5/sdk/crypto/AwsKeyManagerTest.kt b/crypto/src/test/kotlin/web5/sdk/crypto/AwsKeyManagerTest.kt index b2274090e..6bba7243f 100644 --- a/crypto/src/test/kotlin/web5/sdk/crypto/AwsKeyManagerTest.kt +++ b/crypto/src/test/kotlin/web5/sdk/crypto/AwsKeyManagerTest.kt @@ -23,7 +23,6 @@ class AwsKeyManagerTest { assertEquals(alias, publicKey.kid) assertTrue(publicKey.kty == "EC") - assertEquals("sig", publicKey.use) assertEquals(Jwa.ES256K.name, publicKey.alg) } diff --git a/crypto/src/test/kotlin/web5/sdk/crypto/InMemoryKeyManagerTest.kt b/crypto/src/test/kotlin/web5/sdk/crypto/InMemoryKeyManagerTest.kt index c81c54592..c48629577 100644 --- a/crypto/src/test/kotlin/web5/sdk/crypto/InMemoryKeyManagerTest.kt +++ b/crypto/src/test/kotlin/web5/sdk/crypto/InMemoryKeyManagerTest.kt @@ -35,13 +35,11 @@ class InMemoryKeyManagerTest { val privateKey = Crypto.generatePrivateKey(AlgorithmId.secp256k1) val keyManager = InMemoryKeyManager() - val alias = keyManager.import(privateKey) + val alias = keyManager.importKey(privateKey) val publicKey = keyManager.getPublicKey(alias) - assertEquals(privateKey.kid, publicKey.kid) + assertEquals(privateKey.kty, publicKey.kty) assertEquals(privateKey.crv, publicKey.crv) - assertEquals(privateKey.alg, publicKey.alg) - assertEquals(privateKey.use, publicKey.use) assertEquals(privateKey.x, publicKey.x) } @@ -50,12 +48,10 @@ class InMemoryKeyManagerTest { val privateKey = Crypto.generatePrivateKey(AlgorithmId.secp256k1) val keyManager = InMemoryKeyManager() - val alias = keyManager.import(privateKey) + val alias = keyManager.importKey(privateKey) val publicKey = keyManager.getPublicKey(alias) - assertEquals(privateKey.kid, publicKey.kid) + assertEquals(privateKey.kty, publicKey.kty) assertEquals(privateKey.crv, publicKey.crv) - assertEquals(privateKey.alg, publicKey.alg) - assertEquals(privateKey.use, publicKey.use) assertEquals(privateKey.x, publicKey.x) } @@ -64,46 +60,12 @@ class InMemoryKeyManagerTest { val privateKey = Ed25519.generatePrivateKey() val keyManager = InMemoryKeyManager() - val alias = keyManager.import(privateKey) + val alias = keyManager.importKey(privateKey) val publicKey = keyManager.getPublicKey(alias) - assertEquals(privateKey.kid, publicKey.kid) + assertEquals(privateKey.kty, publicKey.kty) assertEquals(privateKey.crv, publicKey.crv) - assertEquals(privateKey.alg, publicKey.alg) - assertEquals(privateKey.use, publicKey.use) assertEquals(privateKey.x, publicKey.x) } - @Test - fun `export returns all keys`() { - val keyManager = InMemoryKeyManager() - keyManager.generatePrivateKey(AlgorithmId.Ed25519) - - val keySet = keyManager.export() - assertEquals(1, keySet.size) - } - - @Test - fun `import throws an exception if key is not a Jwk`() { - val keyManager = InMemoryKeyManager() - val kakaKeySet = listOf(mapOf("hehe" to "troll")) - - assertThrows { - keyManager.import(kakaKeySet) - } - } - - @Test - fun `import loads all keys provided`() { - @Suppress("MaxLineLength") - val serializedKeySet = - """[{"kty":"OKP","d":"DTwtf9i7M4Vj8vSg0iJAQ_n2gSNEUTNLIq30CJ4d9BE","use":"sig","crv":"Ed25519","kid":"hKTpA-TQPNAX9zXtuxPIyTNpoyd4j1Pq1Y_txo2Hm3I","x":"_CrbbGuhpHFs3KVGg2bbNgd2SikmT4L5rIE_zQQjKq0","alg":"EdDSA"}]""" - - val jsonKeySet: List> = Json.jsonMapper.readValue(serializedKeySet) - val keyManager = InMemoryKeyManager() - - assertDoesNotThrow { - keyManager.import(jsonKeySet) - } - } } diff --git a/crypto/src/test/kotlin/web5/sdk/crypto/Secp256k1Test.kt b/crypto/src/test/kotlin/web5/sdk/crypto/Secp256k1Test.kt index 1405b9d2c..0a97dd12d 100644 --- a/crypto/src/test/kotlin/web5/sdk/crypto/Secp256k1Test.kt +++ b/crypto/src/test/kotlin/web5/sdk/crypto/Secp256k1Test.kt @@ -24,9 +24,7 @@ class Secp256k1Test { val privateKey = Secp256k1.generatePrivateKey() Secp256k1.validateKey(privateKey) - assertEquals(Jwa.ES256K.name, privateKey.alg) - assertEquals("sig", privateKey.use) - assertNotNull(privateKey.kid) + assertEquals(JwaCurve.secp256k1.name, privateKey.crv) assertTrue(privateKey.kty == "EC") assertNotNull(privateKey.d) } @@ -40,7 +38,6 @@ class Secp256k1Test { Secp256k1.validateKey(publicKey) assertEquals(publicKey.kid, privateKey.kid) assertEquals(Jwa.ES256K.name, publicKey.alg) - assertEquals("sig", publicKey.use) assertTrue(publicKey.kty == "EC") assertNull(publicKey.d) } diff --git a/dids/src/main/kotlin/web5/sdk/dids/didcore/DidDocument.kt b/dids/src/main/kotlin/web5/sdk/dids/didcore/DidDocument.kt index a8e5fbfd2..e85d10291 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/didcore/DidDocument.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/didcore/DidDocument.kt @@ -116,45 +116,56 @@ public class DidDocument( } if (assertionMethodId != null) { + require(assertionMethod.contains(assertionMethodId)) { throw SignatureException("assertion method \"$assertionMethodId\" not found in list of assertion methods") } - // todo bits from jwtutil - make sure these checks are actually required - val did = Did.parse(assertionMethodId) - val didResolutionResult = DidResolvers.resolve(did.url) - if (didResolutionResult.didResolutionMetadata.error != null) { - throw SignatureException( - "Signature verification failed: " + - "Failed to resolve DID ${did.url}. " + - "Error: ${didResolutionResult.didResolutionMetadata.error}" + val verificationMethodIds = mutableSetOf( + assertionMethodId + ) + + if (assertionMethodId.startsWith("#")) { + verificationMethodIds.add("${this.id}${assertionMethodId}") + } else if (assertionMethodId.startsWith("did:")) { + val fragment = assertionMethodId.split("#").last() + verificationMethodIds.add("#$fragment") + } else { + throw IllegalArgumentException( + "Invalid assertionMethodId. " + + "Expected assertionMethodId to be a DID URL or fragment, but was $assertionMethodId" ) } - val verificationMethodIds = setOf( - did.url, - "#${did.fragment}" - ) - - didResolutionResult.didDocument?.assertionMethod?.firstOrNull { + assertionMethod.firstOrNull { verificationMethodIds.contains(it) } ?: throw SignatureException( "Signature verification failed: Expected kid in JWS header to dereference " + "a DID Document Verification Method with an Assertion verification relationship" ) - } + val assertionMethod: VerificationMethod = + verificationMethod + ?.find { + it.id == assertionMethodId + } + ?: throw SignatureException("assertion method \"$assertionMethodId\" not found") - val assertionMethod: VerificationMethod = - verificationMethod - ?.find { - it.id == (assertionMethodId ?: assertionMethod.first()) - } - ?: throw SignatureException("assertion method \"$assertionMethodId\" not found") + return assertionMethod + + } else { + val assertionMethod: VerificationMethod = + verificationMethod + ?.find { + it.id == assertionMethod.first() + } + ?: throw SignatureException("assertion method not found") - return assertionMethod + return assertionMethod + } } + /** * Builder object to build a DidDocument. */ diff --git a/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt b/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt index d76727638..c4096d95b 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt @@ -179,7 +179,7 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) { opts.verificationMethods?.map { (publicKey, purposes, controller) -> VerificationMethod.Builder() - .id("$didUri#${publicKey.kid}") + .id("$didUri#${publicKey.kid ?: publicKey.computeThumbprint()}") .type("JsonWebKey") .controller(controller ?: didUri) .publicKeyJwk(publicKey) diff --git a/dids/src/test/kotlin/web5/sdk/dids/didcore/DidDocumentTest.kt b/dids/src/test/kotlin/web5/sdk/dids/didcore/DidDocumentTest.kt index 4a0f5c073..5b6033389 100644 --- a/dids/src/test/kotlin/web5/sdk/dids/didcore/DidDocumentTest.kt +++ b/dids/src/test/kotlin/web5/sdk/dids/didcore/DidDocumentTest.kt @@ -174,22 +174,21 @@ class DidDocumentTest { } } - // todo this test fails because of what i added to DidDocument#findAssertionMethodById() @Test fun `findAssertionMethodById returns assertion verification method if id is found`() { - val assertionMethods = listOf("foo") + val assertionMethods = listOf("#foo") val manager = InMemoryKeyManager() val keyAlias = manager.generatePrivateKey(AlgorithmId.secp256k1) val publicKeyJwk = manager.getPublicKey(keyAlias) val vmList = listOf( - VerificationMethod("foo", "type", "controller", publicKeyJwk) + VerificationMethod("#foo", "type", "controller", publicKeyJwk) ) val doc = DidDocument(id = "did:example:123", verificationMethod = vmList, assertionMethod = assertionMethods) - val assertionMethod = doc.findAssertionMethodById("foo") - assertEquals("foo", assertionMethod.id) + val assertionMethod = doc.findAssertionMethodById("#foo") + assertEquals("#foo", assertionMethod.id) assertEquals("type", assertionMethod.type) assertEquals("controller", assertionMethod.controller) } diff --git a/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DhtTest.kt b/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DhtTest.kt index 0a39d628e..e8029e27a 100644 --- a/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DhtTest.kt +++ b/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DhtTest.kt @@ -73,7 +73,7 @@ class DhtTest { val v = "Hello World!".toByteArray() val manager = InMemoryKeyManager() - manager.import(privateKey) + manager.importKey(privateKey) val bep44SignedMessage = DhtClient.signBep44Message( manager, diff --git a/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DidDhtTest.kt b/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DidDhtTest.kt index 9ec39f28a..a3f9cb973 100644 --- a/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DidDhtTest.kt +++ b/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DidDhtTest.kt @@ -111,56 +111,6 @@ class DidDhtTest { assertNull(bearerDid.document.service) } - @Test - fun `create with another key and service`() { - val manager = InMemoryKeyManager() - - val otherKey = manager.generatePrivateKey(AlgorithmId.secp256k1) - val publicKeyJwk = manager.getPublicKey(otherKey) - // todo this was ECKeyGenerator(Curve.P_256).generate().toPublicJWK() before - // is this the right equivalent? - val publicKeyJwk2 = Jwk.Builder("EC", JwaCurve.secp256k1.name) - .x("fake-x-value") - .y("fake-y-value") - .build() - val verificationMethodsToAdd: Iterable, String?>> = listOf( - Triple( - publicKeyJwk, - listOf(Purpose.Authentication, Purpose.AssertionMethod), - "did:web:tbd.website" - ), - Triple( - publicKeyJwk2, - listOf(Purpose.Authentication, Purpose.AssertionMethod), - "did:web:tbd.website" - ) - ) - - val serviceToAdd = - Service.Builder() - .id("test-service") - .type("HubService") - .serviceEndpoint(listOf("https://example.com/service)")) - .build() - - val opts = CreateDidDhtOptions( - verificationMethods = verificationMethodsToAdd, services = listOf(serviceToAdd), publish = false - ) - val did = DidDht.create(manager, opts) - - assertNotNull(did) - assertNotNull(did.document) - assertEquals(3, did.document.verificationMethod?.size) - assertEquals(3, did.document.assertionMethod?.size) - assertEquals(3, did.document.authentication?.size) - assertEquals(1, did.document.capabilityDelegation?.size) - assertEquals(1, did.document.capabilityInvocation?.size) - assertNull(did.document.keyAgreement) - assertNotNull(did.document.service) - assertEquals(1, did.document.service?.size) - assertContains(did.document.service?.get(0)?.id!!, "test-service") - } - @Test fun `create and transform to packet with types`() { val manager = InMemoryKeyManager() @@ -367,7 +317,8 @@ class Web5TestVectorsDidDht { testVectors.vectors.forEach { vector -> val keyManager = spy(InMemoryKeyManager()) - val identityKeyId = keyManager.import(listOf(vector.input.identityPublicJwk!!)).first() + val identityJwk = Json.parse(Json.stringify(vector.input.identityPublicJwk!!)) + val identityKeyId = keyManager.importKey(identityJwk) doReturn(identityKeyId).whenever(keyManager).generatePrivateKey(AlgorithmId.Ed25519) val verificationMethods = vector.input.additionalVerificationMethods?.map { verificationMethodInput -> diff --git a/jose/build.gradle.kts b/jose/build.gradle.kts index 0af9ed606..20e3bd530 100644 --- a/jose/build.gradle.kts +++ b/jose/build.gradle.kts @@ -15,6 +15,7 @@ dependencies { implementation(project(":dids")) // Test + testImplementation(libs.comWillowtreeappsAssertk) testImplementation(kotlin("test")) testImplementation(project(":testing")) } \ No newline at end of file diff --git a/jose/src/main/kotlin/web5/sdk/jose/JwtClaimsSetSerializer.kt b/jose/src/main/kotlin/web5/sdk/jose/JwtClaimsSetSerializer.kt index b60503fbe..145903dbb 100644 --- a/jose/src/main/kotlin/web5/sdk/jose/JwtClaimsSetSerializer.kt +++ b/jose/src/main/kotlin/web5/sdk/jose/JwtClaimsSetSerializer.kt @@ -7,6 +7,7 @@ import com.fasterxml.jackson.databind.JsonDeserializer import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.JsonSerializer import com.fasterxml.jackson.databind.SerializerProvider +import web5.sdk.common.Json import web5.sdk.jose.jwt.JwtClaimsSet /** @@ -44,7 +45,6 @@ public class JwtClaimsSetSerializer : JsonSerializer() { * that takes miscellaneous claims and puts them as values inside misc key */ public class JwtClaimsSetDeserializer : JsonDeserializer() { - public override fun deserialize(p: JsonParser, ctxt: DeserializationContext): JwtClaimsSet { val jsonNode = p.codec.readTree(p) val reservedClaims = setOf( @@ -57,16 +57,21 @@ public class JwtClaimsSetDeserializer : JsonDeserializer() { "jti" ) + // extract misc nodes val miscClaims: MutableMap = mutableMapOf() - + val mc = Json.jsonMapper.createObjectNode() val fields = jsonNode.fields() + while (fields.hasNext()) { val (key, value) = fields.next() + if (!reservedClaims.contains(key)) { - miscClaims[key] = value + mc.set(key, value) } } + val yo = Json.jsonMapper.convertValue(mc, Map::class.java) + return JwtClaimsSet( iss = jsonNode.get("iss")?.asText(), sub = jsonNode.get("sub")?.asText(), @@ -75,7 +80,7 @@ public class JwtClaimsSetDeserializer : JsonDeserializer() { nbf = jsonNode.get("nbf")?.asLong(), iat = jsonNode.get("iat")?.asLong(), jti = jsonNode.get("jti")?.asText(), - misc = miscClaims + misc = yo as Map ) } } \ No newline at end of file diff --git a/jose/src/main/kotlin/web5/sdk/jose/jws/Jws.kt b/jose/src/main/kotlin/web5/sdk/jose/jws/Jws.kt index a5ba2878a..cd4a9e17e 100644 --- a/jose/src/main/kotlin/web5/sdk/jose/jws/Jws.kt +++ b/jose/src/main/kotlin/web5/sdk/jose/jws/Jws.kt @@ -3,7 +3,9 @@ package web5.sdk.jose.jws import web5.sdk.common.Convert import web5.sdk.common.EncodingFormat import web5.sdk.common.Json +import web5.sdk.crypto.AlgorithmId import web5.sdk.crypto.Crypto +import web5.sdk.crypto.JwaCurve import web5.sdk.dids.DidResolvers import web5.sdk.dids.did.BearerDid import web5.sdk.dids.exceptions.PublicKeyJwkMissingException @@ -77,11 +79,14 @@ public object Jws { verificationMethod.id } + val curve = JwaCurve.parse(verificationMethod.publicKeyJwk!!.crv) + val alg = AlgorithmId.from(curve).name + val jwsHeader = JwsHeader.Builder() - .type("JWT") - .algorithm(Crypto.getJwkCurve(verificationMethod.publicKeyJwk!!)?.name!!) - .keyId(kid) - .build() + .type("JWT") + .algorithm(alg) + .keyId(kid) + .build() val headerBase64Url = Convert(Json.stringify(jwsHeader)).toBase64Url() val payloadBase64Url = Convert(payload).toBase64Url() @@ -89,7 +94,6 @@ public object Jws { val toSignBase64Url = "$headerBase64Url.$payloadBase64Url" val toSignBytes = Convert(toSignBase64Url).toByteArray() - val signatureBytes = signer.invoke(toSignBytes) val signatureBase64Url = Convert(signatureBytes).toBase64Url() diff --git a/jose/src/main/kotlin/web5/sdk/jose/jwt/Jwt.kt b/jose/src/main/kotlin/web5/sdk/jose/jwt/Jwt.kt index 01ef2ad85..b88ebc493 100644 --- a/jose/src/main/kotlin/web5/sdk/jose/jwt/Jwt.kt +++ b/jose/src/main/kotlin/web5/sdk/jose/jwt/Jwt.kt @@ -33,7 +33,11 @@ public object Jwt { val claims: JwtClaimsSet try { val payload = Convert(decodedJws.payload).toStr() - val jwtModule = SimpleModule().addDeserializer(JwtClaimsSet::class.java, JwtClaimsSetDeserializer()) + val jwtModule = SimpleModule() + .addDeserializer( + JwtClaimsSet::class.java, + JwtClaimsSetDeserializer() + ) Json.jsonMapper.registerModule(jwtModule) claims = Json.jsonMapper.readValue(payload, JwtClaimsSet::class.java) } catch (e: Exception) { @@ -60,7 +64,11 @@ public object Jwt { * @return The signed JWT */ public fun sign(did: BearerDid, payload: JwtClaimsSet): String { - val jwtModule = SimpleModule().addSerializer(JwtClaimsSet::class.java, JwtClaimsSetSerializer()) + val jwtModule = SimpleModule() + .addSerializer( + JwtClaimsSet::class.java, + JwtClaimsSetSerializer() + ) Json.jsonMapper.registerModule(jwtModule) val payloadJsonString = Json.jsonMapper.writeValueAsString(payload) val payloadBytes = Convert(payloadJsonString).toByteArray() @@ -138,48 +146,9 @@ public class JwtClaimsSet( public val misc: Map = emptyMap() ) { -// public companion object { -// -// /** -// * Takes a JsonNode representation of a claim and builds a JwtClaimsSet. -// * -// * @param jsonNode The JsonNode representation of a claim -// * @return JwtClaimsSet -// */ -// public fun fromJson(jsonNode: JsonNode): JwtClaimsSet { -// val reservedClaims = setOf( -// "iss", -// "sub", -// "aud", -// "exp", -// "nbf", -// "iat", -// "jti" -// ) -// -// val miscClaims: MutableMap = mutableMapOf() -// -// val fields = jsonNode.fields() -// while (fields.hasNext()) { -// val (key, value) = fields.next() -// if (!reservedClaims.contains(key)) { -// miscClaims[key] = value -// } -// } -// -// return JwtClaimsSet( -// iss = jsonNode.get("iss")?.asText(), -// sub = jsonNode.get("sub")?.asText(), -// aud = jsonNode.get("aud")?.asText(), -// exp = jsonNode.get("exp")?.asLong(), -// nbf = jsonNode.get("nbf")?.asLong(), -// iat = jsonNode.get("iat")?.asLong(), -// jti = jsonNode.get("jti")?.asText(), -// misc = miscClaims -// ) -// -// } -// } + override fun toString(): String { + return "JwtClaimsSet(iss=$iss, sub=$sub, aud=$aud, exp=$exp, nbf=$nbf, iat=$iat, jti=$jti, misc=$misc)" + } /** * Builder for JwtClaimsSet. diff --git a/jose/src/test/kotlin/web5/sdk/jose/jws/JwsTest.kt b/jose/src/test/kotlin/web5/sdk/jose/jws/JwsTest.kt new file mode 100644 index 000000000..247242ce8 --- /dev/null +++ b/jose/src/test/kotlin/web5/sdk/jose/jws/JwsTest.kt @@ -0,0 +1,125 @@ +package web5.sdk.jose.jws + +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.assertThrows +import web5.sdk.common.Convert +import web5.sdk.common.EncodingFormat +import web5.sdk.common.Json +import web5.sdk.crypto.InMemoryKeyManager +import web5.sdk.dids.methods.jwk.DidJwk +import java.security.SignatureException +import kotlin.test.Test +import kotlin.test.assertContains +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class JwsTest { + + @Nested + inner class DecodeTest { + + @Test + fun `decode fails if part size less than 3`() { + + assertThrows { + Jws.decode("a.b.c.d") + } + } + + @Test + fun `decode fails if header is not base64url`() { + val jwsString = "lol." + + "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ." + + "SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" + + val exception = assertThrows { + Jws.decode(jwsString) + } + assertContains(exception.message!!, "Failed to decode header") + } + + @Test + fun `decode fails if payload is not base64url`() { + val jwsString = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." + + "{woohoo}." + + "SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" + val exception = assertThrows { + Jws.decode(jwsString) + } + + assertContains(exception.message!!, "Failed to decode payload") + + } + + @Test + fun `decode fails if signature is not base64url`() { + val jwsString = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." + + "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ." + + "{woot}" + + val exception = assertThrows { + Jws.decode(jwsString) + } + + assertContains(exception.message!!, "Failed to decode signature") + } + + @Test + fun `decode succeeds with test jwt from jwtio`() { + val jwsString = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." + + "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ." + + "SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" + + val decodedJws = Jws.decode(jwsString) + + assertEquals("HS256", decodedJws.header.alg) + assertEquals("JWT", decodedJws.header.typ) + val payloadStr = Convert(decodedJws.payload).toStr() + val payload = Json.parse>(payloadStr) + assertEquals("1234567890", payload["sub"]) + assertEquals(3, decodedJws.parts.size) + + } + } + + @Nested + inner class SignTest { + + @Test + fun `sign successfully creates signedJws`() { + val bearerDid = DidJwk.create(InMemoryKeyManager()) + val payload = "hello".toByteArray() + + val signedJws = Jws.sign(bearerDid, payload) + + val parts = signedJws.split(".") + assertEquals(3, parts.size) + val headerString = Convert(parts[0], EncodingFormat.Base64Url).toStr() + val header = Json.parse(headerString) + assertEquals("JWT", header.typ) + assertEquals("Ed25519", header.alg) + assertEquals(bearerDid.document.verificationMethod?.first()?.id, header.kid) + val decodedPayload = Convert(parts[1], EncodingFormat.Base64Url).toStr() + assertEquals("hello", decodedPayload) + + } + + @Test + fun `sign successfully creates detached signedJws`() { + val bearerDid = DidJwk.create(InMemoryKeyManager()) + val payload = "hello".toByteArray() + + val signedJws = Jws.sign(bearerDid, payload, detached = true) + + val parts = signedJws.split(".") + assertEquals(3, parts.size) + val headerString = Convert(parts[0], EncodingFormat.Base64Url).toStr() + val header = Json.parse(headerString) + assertEquals("JWT", header.typ) + assertEquals("Ed25519", header.alg) + assertEquals(bearerDid.document.verificationMethod?.first()?.id, header.kid) + assertEquals("", parts[1]) + } + + } +} \ No newline at end of file diff --git a/jose/src/test/kotlin/web5/sdk/jose/jwt/JwtTest.kt b/jose/src/test/kotlin/web5/sdk/jose/jwt/JwtTest.kt new file mode 100644 index 000000000..928e5a51f --- /dev/null +++ b/jose/src/test/kotlin/web5/sdk/jose/jwt/JwtTest.kt @@ -0,0 +1,31 @@ +package web5.sdk.jose.jwt + +import org.junit.jupiter.api.Nested +import web5.sdk.crypto.InMemoryKeyManager +import web5.sdk.dids.methods.jwk.DidJwk +import kotlin.test.Test +import kotlin.test.assertEquals + +class JwtTest { + + @Nested + inner class DecodeTest { + + @Test + fun `decode succeeds`() { + val bearerDid = DidJwk.create(InMemoryKeyManager()) + val claims = JwtClaimsSet.Builder() + .issuer("me") + .subject("you") + .misc("vc", mapOf("type" to listOf("VerifiableCredential"))) + .build() + val vcJwt = Jwt.sign(bearerDid, claims) + + val decodedJwt = Jwt.decode(vcJwt) + assertEquals(bearerDid.document.verificationMethod?.first()?.id, decodedJwt.header.kid) + assertEquals(claims.toString(), decodedJwt.claims.toString()) + + } + } + +} \ No newline at end of file From 0b7dc3314096a5befa68d3eed92057726357f86e Mon Sep 17 00:00:00 2001 From: Jiyoon Koo Date: Thu, 21 Mar 2024 15:36:58 -0700 Subject: [PATCH 30/47] fixing tests --- .../kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt | 4 ++-- jose/src/main/kotlin/web5/sdk/jose/jws/Jws.kt | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt index 4fdbb682d..b3bf02e63 100644 --- a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt +++ b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt @@ -113,8 +113,8 @@ class VerifiableCredentialTest { val vc = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.uri, - subject = holderDid.uri, + issuer = issuerDid.did.uri, + subject = holderDid.did.uri, data = StreetCredibility(localRespect = "high", legit = true) ) diff --git a/jose/src/main/kotlin/web5/sdk/jose/jws/Jws.kt b/jose/src/main/kotlin/web5/sdk/jose/jws/Jws.kt index cd4a9e17e..c6499e79b 100644 --- a/jose/src/main/kotlin/web5/sdk/jose/jws/Jws.kt +++ b/jose/src/main/kotlin/web5/sdk/jose/jws/Jws.kt @@ -238,7 +238,9 @@ public class DecodedJws( "Malformed JWS. Expected header to contain kid and alg." } - val resolutionResult = DidResolvers.resolve(header.kid!!) + // todo header.kid needs to be broken down into didUri and fragment and use the didUri + val didUri = header.kid!!.split("#")[0] + val resolutionResult = DidResolvers.resolve(didUri) check(resolutionResult.didResolutionMetadata.error == null) { "Verification failed. Failed to resolve kid. " + From 149ed9a5c96604bfc724b836b9ad840136e62cd6 Mon Sep 17 00:00:00 2001 From: Jiyoon Koo Date: Thu, 21 Mar 2024 15:46:23 -0700 Subject: [PATCH 31/47] new web5-spec sha --- web5-spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web5-spec b/web5-spec index 19cdeed10..bb5cb4f2d 160000 --- a/web5-spec +++ b/web5-spec @@ -1 +1 @@ -Subproject commit 19cdeed107b73aac0b50ac58390920e7553bc8ca +Subproject commit bb5cb4f2d65e207a1d9d812ab193acfc9c54b9c9 From 711f9d7583422da3664400b9b8b301e5c4317655 Mon Sep 17 00:00:00 2001 From: Jiyoon Koo Date: Thu, 21 Mar 2024 18:43:31 -0700 Subject: [PATCH 32/47] adding more jwttest --- .../web5/sdk/jose/JwtClaimsSetSerializer.kt | 9 +++--- .../test/kotlin/web5/sdk/jose/jwt/JwtTest.kt | 29 +++++++++++++++++++ 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/jose/src/main/kotlin/web5/sdk/jose/JwtClaimsSetSerializer.kt b/jose/src/main/kotlin/web5/sdk/jose/JwtClaimsSetSerializer.kt index 145903dbb..f377736b9 100644 --- a/jose/src/main/kotlin/web5/sdk/jose/JwtClaimsSetSerializer.kt +++ b/jose/src/main/kotlin/web5/sdk/jose/JwtClaimsSetSerializer.kt @@ -58,19 +58,18 @@ public class JwtClaimsSetDeserializer : JsonDeserializer() { ) // extract misc nodes - val miscClaims: MutableMap = mutableMapOf() - val mc = Json.jsonMapper.createObjectNode() + val miscClaims = Json.jsonMapper.createObjectNode() val fields = jsonNode.fields() while (fields.hasNext()) { val (key, value) = fields.next() if (!reservedClaims.contains(key)) { - mc.set(key, value) + miscClaims.set(key, value) } } - val yo = Json.jsonMapper.convertValue(mc, Map::class.java) + val miscClaimsMap = Json.jsonMapper.convertValue(miscClaims, Map::class.java) return JwtClaimsSet( iss = jsonNode.get("iss")?.asText(), @@ -80,7 +79,7 @@ public class JwtClaimsSetDeserializer : JsonDeserializer() { nbf = jsonNode.get("nbf")?.asLong(), iat = jsonNode.get("iat")?.asLong(), jti = jsonNode.get("jti")?.asText(), - misc = yo as Map + misc = miscClaimsMap as Map ) } } \ No newline at end of file diff --git a/jose/src/test/kotlin/web5/sdk/jose/jwt/JwtTest.kt b/jose/src/test/kotlin/web5/sdk/jose/jwt/JwtTest.kt index 928e5a51f..c83e229c9 100644 --- a/jose/src/test/kotlin/web5/sdk/jose/jwt/JwtTest.kt +++ b/jose/src/test/kotlin/web5/sdk/jose/jwt/JwtTest.kt @@ -1,9 +1,13 @@ package web5.sdk.jose.jwt import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows import web5.sdk.crypto.InMemoryKeyManager import web5.sdk.dids.methods.jwk.DidJwk +import java.security.SignatureException import kotlin.test.Test +import kotlin.test.assertContains import kotlin.test.assertEquals class JwtTest { @@ -24,7 +28,32 @@ class JwtTest { val decodedJwt = Jwt.decode(vcJwt) assertEquals(bearerDid.document.verificationMethod?.first()?.id, decodedJwt.header.kid) assertEquals(claims.toString(), decodedJwt.claims.toString()) + } + + @Test + fun `decode fails due to payload failing and throws exception`() { + val vcJwt = + "eyJraWQiOiJkaWQ6a2V5OnpRM3NoTkx0MWFNV1BiV1JHYThWb2VFYkpvZko3eEplNEZDUHBES3hxMU5aeWdwaXkjelEzc2hOTHQxYU1XUGJXUkdhOFZvZUViSm9mSjd4SmU0RkNQcERLeHExTlp5Z3BpeSIsInR5cCI6IkpXVCIsImFsZyI6IkVTMjU2SyJ9.hehe.qoqF4-FinFsQ2J-NFSO46xCE8kUTZqZCU5fYr6tS0TQ6VP8y-ZnyR6R3oAqLs_Yo_CqQi23yi38uDjLjksiD2w" + + val exception = assertThrows { Jwt.decode(vcJwt) } + + assertContains(exception.message!!, "Malformed JWT. Invalid base64url encoding for JWT payload.") + } + } + + @Nested + inner class SignTest { + + @Test + fun `sign succeeds`() { + val bearerDid = DidJwk.create(InMemoryKeyManager()) + val claims = JwtClaimsSet.Builder() + .issuer("me") + .subject("you") + .misc("vc", mapOf("type" to listOf("VerifiableCredential"))) + .build() + assertDoesNotThrow { Jwt.sign(bearerDid, claims) } } } From 2f3c98d00ac049192f327500eb0e0ad0263ea90c Mon Sep 17 00:00:00 2001 From: Jiyoon Koo Date: Thu, 21 Mar 2024 18:56:52 -0700 Subject: [PATCH 33/47] added jwktest --- .../kotlin/web5/sdk/crypto/jwk/JwkTest.kt | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 crypto/src/test/kotlin/web5/sdk/crypto/jwk/JwkTest.kt diff --git a/crypto/src/test/kotlin/web5/sdk/crypto/jwk/JwkTest.kt b/crypto/src/test/kotlin/web5/sdk/crypto/jwk/JwkTest.kt new file mode 100644 index 000000000..d43d290c1 --- /dev/null +++ b/crypto/src/test/kotlin/web5/sdk/crypto/jwk/JwkTest.kt @@ -0,0 +1,35 @@ +package web5.sdk.crypto.jwk + +import org.junit.jupiter.api.Test +import web5.sdk.crypto.JwaCurve +import kotlin.test.assertEquals + +class JwkTest { + + @Test + fun `computeThumbPrint works with secp256k1 jwk`() { + val jwk = Jwk.Builder(keyType = "EC", curve = JwaCurve.secp256k1.name) + .x("vdrbz2EOzvbLDV_-kL4eJt7VI-8TFZNmA9YgWzvhh7U") + .y("VLFqQMZP_AspucXoWX2-bGXpAO1fQ5Ln19V5RAxrgvU") + .algorithm("ES256K") + .build() + + val thumbprint = jwk.computeThumbprint() + + assertEquals("i3SPRBtJKovHFsBaqM92ti6xQCJLX3E7YCewiHV2CSg", thumbprint) + + } + + @Test + fun `computeThumbprint works with Ed25519 jwk`() { + val jwk = Jwk.Builder(keyType = "OKP", curve = JwaCurve.Ed25519.name) + .x("DzpSEyU0w1Myn3lA_piHAI6OrFAnZuEsTwMUPCTwMc8") + .algorithm("EdDSA") + .build() + + val thumbprint = jwk.computeThumbprint() + + assertEquals("c4IOrQdnehPwQZ6SyNLp9J942VCXrxgWw4zUxAHQXQE", thumbprint) + } + +} \ No newline at end of file From c5d3b91a9f805a887fd671f5eb1c2fd2d4671132 Mon Sep 17 00:00:00 2001 From: Jiyoon Koo Date: Thu, 21 Mar 2024 19:16:00 -0700 Subject: [PATCH 34/47] fixing detekt error --- jose/src/test/kotlin/web5/sdk/jose/jwt/JwtTest.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/jose/src/test/kotlin/web5/sdk/jose/jwt/JwtTest.kt b/jose/src/test/kotlin/web5/sdk/jose/jwt/JwtTest.kt index c83e229c9..832e524f5 100644 --- a/jose/src/test/kotlin/web5/sdk/jose/jwt/JwtTest.kt +++ b/jose/src/test/kotlin/web5/sdk/jose/jwt/JwtTest.kt @@ -33,7 +33,11 @@ class JwtTest { @Test fun `decode fails due to payload failing and throws exception`() { val vcJwt = - "eyJraWQiOiJkaWQ6a2V5OnpRM3NoTkx0MWFNV1BiV1JHYThWb2VFYkpvZko3eEplNEZDUHBES3hxMU5aeWdwaXkjelEzc2hOTHQxYU1XUGJXUkdhOFZvZUViSm9mSjd4SmU0RkNQcERLeHExTlp5Z3BpeSIsInR5cCI6IkpXVCIsImFsZyI6IkVTMjU2SyJ9.hehe.qoqF4-FinFsQ2J-NFSO46xCE8kUTZqZCU5fYr6tS0TQ6VP8y-ZnyR6R3oAqLs_Yo_CqQi23yi38uDjLjksiD2w" + "eyJraWQiOiJkaWQ6a2V5OnpRM3NoTkx0MWFNV1BiV1JHYThWb2VFYkpvZko3" + + "eEplNEZDUHBES3hxMU5aeWdwaXkjelEzc2hOTHQxYU1XUGJXUkdhOFZvZU" + + "ViSm9mSjd4SmU0RkNQcERLeHExTlp5Z3BpeSIsInR5cCI6IkpXVCIsImFsZ" + + "yI6IkVTMjU2SyJ9.hehe.qoqF4-FinFsQ2J-NFSO46xCE8kUTZqZCU5fYr6t" + + "S0TQ6VP8y-ZnyR6R3oAqLs_Yo_CqQi23yi38uDjLjksiD2w" val exception = assertThrows { Jwt.decode(vcJwt) } From c1ea33da5ec931997a3260ee1790ee8b51d89ed6 Mon Sep 17 00:00:00 2001 From: Jiyoon Koo Date: Thu, 21 Mar 2024 22:56:52 -0700 Subject: [PATCH 35/47] adding bearerdid tests --- .../kotlin/web5/sdk/dids/did/BearerDid.kt | 6 +- .../kotlin/web5/sdk/dids/did/BearerDidTest.kt | 61 +++++++++++++++++++ .../web5/sdk/dids/did/PortableDidTest.kt | 4 ++ 3 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 dids/src/test/kotlin/web5/sdk/dids/did/BearerDidTest.kt create mode 100644 dids/src/test/kotlin/web5/sdk/dids/did/PortableDidTest.kt diff --git a/dids/src/main/kotlin/web5/sdk/dids/did/BearerDid.kt b/dids/src/main/kotlin/web5/sdk/dids/did/BearerDid.kt index bea268f3c..ade5f0892 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/did/BearerDid.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/did/BearerDid.kt @@ -50,11 +50,11 @@ public class BearerDid( public fun getSigner(selector: VMSelector? = null): Pair { val verificationMethod = document.selectVerificationMethod(selector) - val keyAliasResult = runCatching { verificationMethod.publicKeyJwk?.computeThumbprint() } - val keyAlias = keyAliasResult.getOrNull() ?: throw Exception("Failed to compute key alias") + val kid = verificationMethod.publicKeyJwk?.computeThumbprint() + ?: throw Exception("Failed to compute key alias") val signer: DidSigner = { payload -> - keyManager.sign(keyAlias, payload) + keyManager.sign(kid, payload) } return Pair(signer, verificationMethod) diff --git a/dids/src/test/kotlin/web5/sdk/dids/did/BearerDidTest.kt b/dids/src/test/kotlin/web5/sdk/dids/did/BearerDidTest.kt new file mode 100644 index 000000000..6839c1d01 --- /dev/null +++ b/dids/src/test/kotlin/web5/sdk/dids/did/BearerDidTest.kt @@ -0,0 +1,61 @@ +package web5.sdk.dids.did + +import org.junit.jupiter.api.Assertions.assertArrayEquals +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.spy +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import web5.sdk.crypto.InMemoryKeyManager +import web5.sdk.dids.methods.jwk.DidJwk +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +class BearerDidTest { + + @Test + fun `getSigner should return a signer and verification method`() { + val keyManager = spy(InMemoryKeyManager()) + + val did = DidJwk.create(keyManager) + val expectedVm = did.document.verificationMethod?.first() + val testPayload = "testPayload".toByteArray() + val expectedSignature = "signature".toByteArray() + doReturn(expectedSignature).whenever(keyManager).sign(any(), eq(testPayload)) + + val (signer, vm) = did.getSigner() + + val signature = signer(testPayload) + + assertEquals(expectedVm, vm) + verify(keyManager).sign(any(), eq(testPayload)) + assertArrayEquals(expectedSignature, signature) + + } + + @Test + fun `export returns a portable did with correct attributes`() { + val bearerDid = DidJwk.create(InMemoryKeyManager()) + val portableDid = bearerDid.export() + + assertEquals(portableDid.uri, bearerDid.did.uri) + assertEquals(portableDid.document, bearerDid.document) + assertEquals(1, portableDid.privateKeys.size) + } + + @Test + fun `import should return a BearerDid object`() { + val portableDid = DidJwk.create(InMemoryKeyManager()).export() + val bearerDid = DidJwk.import(portableDid) + + assertEquals(portableDid.uri, bearerDid.did.uri) + assertEquals(portableDid.document, bearerDid.document) + val portableDidKid = portableDid.privateKeys[0].kid ?: portableDid.privateKeys[0].computeThumbprint() + assertNotNull(bearerDid.keyManager.getPublicKey(portableDidKid)) + val bearerDidKid = bearerDid.document.verificationMethod?.first()?.publicKeyJwk?.kid ?: bearerDid.document.verificationMethod?.first()?.publicKeyJwk?.computeThumbprint() + assertEquals(portableDidKid, bearerDidKid) + } + +} \ No newline at end of file diff --git a/dids/src/test/kotlin/web5/sdk/dids/did/PortableDidTest.kt b/dids/src/test/kotlin/web5/sdk/dids/did/PortableDidTest.kt new file mode 100644 index 000000000..16a1120f0 --- /dev/null +++ b/dids/src/test/kotlin/web5/sdk/dids/did/PortableDidTest.kt @@ -0,0 +1,4 @@ +package web5.sdk.dids.did + +class PortableDidTest { +} \ No newline at end of file From eb52fd3a4bdfab47485e6cdec1b82d8bb57131ae Mon Sep 17 00:00:00 2001 From: Jiyoon Koo Date: Thu, 21 Mar 2024 23:00:30 -0700 Subject: [PATCH 36/47] removing portabledidtest class because there is no method inside portabledid --- dids/src/test/kotlin/web5/sdk/dids/did/PortableDidTest.kt | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 dids/src/test/kotlin/web5/sdk/dids/did/PortableDidTest.kt diff --git a/dids/src/test/kotlin/web5/sdk/dids/did/PortableDidTest.kt b/dids/src/test/kotlin/web5/sdk/dids/did/PortableDidTest.kt deleted file mode 100644 index 16a1120f0..000000000 --- a/dids/src/test/kotlin/web5/sdk/dids/did/PortableDidTest.kt +++ /dev/null @@ -1,4 +0,0 @@ -package web5.sdk.dids.did - -class PortableDidTest { -} \ No newline at end of file From 5cdfec32b40922ce58bfdc27312de7bc6b878a9e Mon Sep 17 00:00:00 2001 From: Jiyoon Koo Date: Thu, 21 Mar 2024 23:04:53 -0700 Subject: [PATCH 37/47] fixing detekt error --- dids/src/test/kotlin/web5/sdk/dids/did/BearerDidTest.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dids/src/test/kotlin/web5/sdk/dids/did/BearerDidTest.kt b/dids/src/test/kotlin/web5/sdk/dids/did/BearerDidTest.kt index 6839c1d01..6b0523f39 100644 --- a/dids/src/test/kotlin/web5/sdk/dids/did/BearerDidTest.kt +++ b/dids/src/test/kotlin/web5/sdk/dids/did/BearerDidTest.kt @@ -54,7 +54,8 @@ class BearerDidTest { assertEquals(portableDid.document, bearerDid.document) val portableDidKid = portableDid.privateKeys[0].kid ?: portableDid.privateKeys[0].computeThumbprint() assertNotNull(bearerDid.keyManager.getPublicKey(portableDidKid)) - val bearerDidKid = bearerDid.document.verificationMethod?.first()?.publicKeyJwk?.kid ?: bearerDid.document.verificationMethod?.first()?.publicKeyJwk?.computeThumbprint() + val bearerDidKid = bearerDid.document.verificationMethod?.first()?.publicKeyJwk?.kid + ?: bearerDid.document.verificationMethod?.first()?.publicKeyJwk?.computeThumbprint() assertEquals(portableDidKid, bearerDidKid) } From 3937376b78380560e1df073fbbfac2d1089b5b10 Mon Sep 17 00:00:00 2001 From: Jiyoon Koo Date: Fri, 22 Mar 2024 00:29:04 -0700 Subject: [PATCH 38/47] moving nimbusds lib to implementation instead of api --- crypto/build.gradle.kts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/crypto/build.gradle.kts b/crypto/build.gradle.kts index 303871d87..83333f6bb 100644 --- a/crypto/build.gradle.kts +++ b/crypto/build.gradle.kts @@ -16,13 +16,6 @@ dependencies { * Deps are declared in alphabetical order. */ - // API - /* - * API Leak: https://github.com/TBD54566975/web5-kt/issues/229 - * - * Change and move to "implementation" when completed - */ - api(libs.comNimbusdsJoseJwt) /* * API Leak: https://github.com/TBD54566975/web5-kt/issues/230 * @@ -37,6 +30,7 @@ dependencies { implementation(libs.comGoogleCryptoTink) implementation(libs.bundles.orgBouncycastle) implementation(libs.comFasterXmlJacksonModuleKotlin) + implementation(libs.comNimbusdsJoseJwt) // Test /** From cda57a040fdb1a57836a797c2fc5a6bac60f41c4 Mon Sep 17 00:00:00 2001 From: Jiyoon Koo Date: Sat, 23 Mar 2024 16:27:54 -0700 Subject: [PATCH 39/47] adding wording around not supporting did web creation --- dids/src/main/kotlin/web5/sdk/dids/methods/web/DidWeb.kt | 4 +++- web5-spec | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/dids/src/main/kotlin/web5/sdk/dids/methods/web/DidWeb.kt b/dids/src/main/kotlin/web5/sdk/dids/methods/web/DidWeb.kt index eea2d6fc4..7751bedfe 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/methods/web/DidWeb.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/methods/web/DidWeb.kt @@ -41,6 +41,8 @@ import kotlin.text.Charsets.UTF_8 * A "did:web" DID is an implementation that uses the web domains existing reputation system. More details can be * read in https://w3c-ccg.github.io/did-method-web/ * + * DidWeb API does not support creating DIDs, only resolving them. + * * ### Usage Example: * ```kotlin * val keyManager = InMemoryKeyManager() @@ -78,7 +80,7 @@ private const val WELL_KNOWN_URL_PATH = "/.well-known" private const val DID_DOC_FILE_NAME = "/did.json" /** - * Implements [resolve] and [create] according to https://w3c-ccg.github.io/did-method-web/ + * Implements [resolve] according to https://w3c-ccg.github.io/did-method-web/ */ public sealed class DidWebApi( configuration: DidWebApiConfiguration diff --git a/web5-spec b/web5-spec index bb5cb4f2d..55acc22a2 160000 --- a/web5-spec +++ b/web5-spec @@ -1 +1 @@ -Subproject commit bb5cb4f2d65e207a1d9d812ab193acfc9c54b9c9 +Subproject commit 55acc22a2a6e33fd2ef530c1cfd0405974dc0d44 From 58db2932e73fd09e476069b5e54150f473d67407 Mon Sep 17 00:00:00 2001 From: Jiyoon Koo Date: Sat, 23 Mar 2024 17:57:04 -0700 Subject: [PATCH 40/47] adding uri as a property to BearerDid as it was missed. addressing code review comments --- .../main/kotlin/web5/sdk/common/Convert.kt | 2 +- .../sdk/credentials/PresentationExchange.kt | 7 +- .../sdk/credentials/VerifiablePresentation.kt | 2 +- .../credentials/PresentationExchangeTest.kt | 112 +++++++++--------- .../credentials/StatusListCredentialTest.kt | 66 +++++------ .../credentials/VerifiableCredentialTest.kt | 26 ++-- .../credentials/VerifiablePresentationTest.kt | 28 ++--- .../kotlin/web5/sdk/crypto/AwsKeyManager.kt | 7 +- crypto/src/main/kotlin/web5/sdk/crypto/Dsa.kt | 4 +- .../main/kotlin/web5/sdk/crypto/Ed25519.kt | 2 - .../web5/sdk/crypto/InMemoryKeyManager.kt | 2 +- .../main/kotlin/web5/sdk/crypto/jwk/Jwk.kt | 2 +- .../main/kotlin/web5/sdk/dids/DidMethod.kt | 14 --- .../kotlin/web5/sdk/dids/did/BearerDid.kt | 22 +++- .../web5/sdk/dids/methods/dht/DidDht.kt | 2 +- .../web5/sdk/dids/methods/jwk/DidJwk.kt | 2 +- .../web5/sdk/dids/methods/key/DidKey.kt | 2 +- .../kotlin/web5/sdk/dids/DidResolversTest.kt | 2 +- .../kotlin/web5/sdk/dids/did/BearerDidTest.kt | 4 +- .../web5/sdk/dids/didcore/DidDocumentTest.kt | 10 +- .../web5/sdk/dids/methods/dht/DhtTest.kt | 4 +- .../web5/sdk/dids/methods/jwk/DidJwkTest.kt | 4 +- .../web5/sdk/dids/methods/key/DidKeyTest.kt | 6 +- jose/src/main/kotlin/web5/sdk/jose/jws/Jws.kt | 2 +- 24 files changed, 163 insertions(+), 171 deletions(-) diff --git a/common/src/main/kotlin/web5/sdk/common/Convert.kt b/common/src/main/kotlin/web5/sdk/common/Convert.kt index e758bbc56..19b0dfeeb 100644 --- a/common/src/main/kotlin/web5/sdk/common/Convert.kt +++ b/common/src/main/kotlin/web5/sdk/common/Convert.kt @@ -46,7 +46,7 @@ public val B64URL_DECODER: Base64.Decoder = Base64.getUrlDecoder() * ``` * // Example 1: Convert a ByteArray to a Base64Url encoded string without padding * val byteArray = byteArrayOf(1, 2, 3) - * val base64Url = Convert(byteArray).toBase64Url() + * val base64Url = Convert(byteArray).toBase64Url() // same as .toBase64Url(padding = false) * println(base64Url) // Output should be a Base64Url encoded string without padding * * // Example 2: Convert a Base64Url encoded string to a ByteArray diff --git a/credentials/src/main/kotlin/web5/sdk/credentials/PresentationExchange.kt b/credentials/src/main/kotlin/web5/sdk/credentials/PresentationExchange.kt index 839d6758d..cc93992e9 100644 --- a/credentials/src/main/kotlin/web5/sdk/credentials/PresentationExchange.kt +++ b/credentials/src/main/kotlin/web5/sdk/credentials/PresentationExchange.kt @@ -164,12 +164,13 @@ public object PresentationExchange { vcJwtList: Iterable, presentationDefinition: PresentationDefinitionV2 ): Map> { + + val jwtModule = SimpleModule().addSerializer(JwtClaimsSet::class.java, JwtClaimsSetSerializer()) + Json.jsonMapper.registerModule(jwtModule) + val vcJwtListWithNodes = vcJwtList.zip( vcJwtList.map { vcJwt -> val vc = Jwt.decode(vcJwt) - - val jwtModule = SimpleModule().addSerializer(JwtClaimsSet::class.java, JwtClaimsSetSerializer()) - Json.jsonMapper.registerModule(jwtModule) val jsonString = Json.jsonMapper.writeValueAsString(vc.claims) Json.jsonMapper.readTree(jsonString) ?: throw JsonPathParseException() diff --git a/credentials/src/main/kotlin/web5/sdk/credentials/VerifiablePresentation.kt b/credentials/src/main/kotlin/web5/sdk/credentials/VerifiablePresentation.kt index b7f7cee94..c72bdfbd4 100644 --- a/credentials/src/main/kotlin/web5/sdk/credentials/VerifiablePresentation.kt +++ b/credentials/src/main/kotlin/web5/sdk/credentials/VerifiablePresentation.kt @@ -59,7 +59,7 @@ public class VerifiablePresentation internal constructor(public val vpDataModel: @JvmOverloads public fun sign(bearerDid: BearerDid, assertionMethodId: String? = null): String { val payload = JwtClaimsSet.Builder() - .issuer(bearerDid.did.uri) + .issuer(bearerDid.uri) .issueTime(Date().time) .misc("vp", vpDataModel.toMap()) .build() diff --git a/credentials/src/test/kotlin/web5/sdk/credentials/PresentationExchangeTest.kt b/credentials/src/test/kotlin/web5/sdk/credentials/PresentationExchangeTest.kt index 5d1d2210d..f88204a56 100644 --- a/credentials/src/test/kotlin/web5/sdk/credentials/PresentationExchangeTest.kt +++ b/credentials/src/test/kotlin/web5/sdk/credentials/PresentationExchangeTest.kt @@ -70,8 +70,8 @@ class PresentationExchangeTest { ) val vc = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.did.uri, - subject = holderDid.did.uri, + issuer = issuerDid.uri, + subject = holderDid.uri, data = StreetCredibility(localRespect = "high", legit = true) ) val vcJwt = vc.sign(issuerDid) @@ -87,8 +87,8 @@ class PresentationExchangeTest { ) val vc = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.did.uri, - subject = holderDid.did.uri, + issuer = issuerDid.uri, + subject = holderDid.uri, data = StreetCredibility(localRespect = "high", legit = true) ) val vcJwt = vc.sign(issuerDid) @@ -104,8 +104,8 @@ class PresentationExchangeTest { ) val vc = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.did.uri, - subject = holderDid.did.uri, + issuer = issuerDid.uri, + subject = holderDid.uri, data = StreetCredibility(localRespect = "high", legit = true) ) val vcJwt = vc.sign(issuerDid) @@ -121,8 +121,8 @@ class PresentationExchangeTest { ) val vc = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.did.uri, - subject = holderDid.did.uri, + issuer = issuerDid.uri, + subject = holderDid.uri, data = StreetCredibility(localRespect = "high", legit = true) ) val vcJwt = vc.sign(issuerDid) @@ -139,8 +139,8 @@ class PresentationExchangeTest { val vc = VerifiableCredential.create( type = "DateOfBirthVc", - issuer = issuerDid.did.uri, - subject = holderDid.did.uri, + issuer = issuerDid.uri, + subject = holderDid.uri, data = DateOfBirth(dateOfBirth = "1/1/1111") ) @@ -157,16 +157,16 @@ class PresentationExchangeTest { val vc1 = VerifiableCredential.create( type = "DateOfBirth", - issuer = issuerDid.did.uri, - subject = holderDid.did.uri, + issuer = issuerDid.uri, + subject = holderDid.uri, data = DateOfBirth(dateOfBirth = "Data1") ) val vcJwt1 = vc1.sign(issuerDid) val vc2 = VerifiableCredential.create( type = "Address", - issuer = issuerDid.did.uri, - subject = holderDid.did.uri, + issuer = issuerDid.uri, + subject = holderDid.uri, data = Address("abc street 123") ) val vcJwt2 = vc2.sign(issuerDid) @@ -183,8 +183,8 @@ class PresentationExchangeTest { val vc1 = VerifiableCredential.create( type = "DateOfBirthSSN", - issuer = issuerDid.did.uri, - subject = holderDid.did.uri, + issuer = issuerDid.uri, + subject = holderDid.uri, data = DateOfBirthSSN(dateOfBirth = "1999-01-01", ssn = "456-123-123") ) val vcJwt1 = vc1.sign(issuerDid) @@ -201,8 +201,8 @@ class PresentationExchangeTest { val vc1 = VerifiableCredential.create( type = "DateOfBirthSSN", - issuer = issuerDid.did.uri, - subject = holderDid.did.uri, + issuer = issuerDid.uri, + subject = holderDid.uri, data = DateOfBirthSSN(dateOfBirth = "1999-01-01", ssn = "456-123-123") ) val vcJwt1 = vc1.sign(issuerDid) @@ -219,8 +219,8 @@ class PresentationExchangeTest { val vc1 = VerifiableCredential.create( type = "DateOfBirthVc", - issuer = issuerDid.did.uri, - subject = holderDid.did.uri, + issuer = issuerDid.uri, + subject = holderDid.uri, data = DateOfBirth(dateOfBirth = "1/1/1111") ) @@ -228,8 +228,8 @@ class PresentationExchangeTest { val vc2 = VerifiableCredential.create( type = "Address", - issuer = issuerDid.did.uri, - subject = holderDid.did.uri, + issuer = issuerDid.uri, + subject = holderDid.uri, data = Address(address = "123 abc street") ) @@ -260,8 +260,8 @@ class PresentationExchangeTest { ) val vc = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.did.uri, - subject = holderDid.did.uri, + issuer = issuerDid.uri, + subject = holderDid.uri, data = StreetCredibility(localRespect = "high", legit = true) ) val vcJwt = vc.sign(issuerDid) @@ -285,8 +285,8 @@ class PresentationExchangeTest { ) val vc = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.did.uri, - subject = holderDid.did.uri, + issuer = issuerDid.uri, + subject = holderDid.uri, data = StreetCredibility(localRespect = "high", legit = true) ) val vcJwt = vc.sign(issuerDid) @@ -308,8 +308,8 @@ class PresentationExchangeTest { ) val vc = VerifiableCredential.create( type = "DateOfBirth", - issuer = issuerDid.did.uri, - subject = holderDid.did.uri, + issuer = issuerDid.uri, + subject = holderDid.uri, data = DateOfBirth(dateOfBirth = "01-02-03") ) val vcJwt = vc.sign(issuerDid) @@ -331,8 +331,8 @@ class PresentationExchangeTest { ) val vc = VerifiableCredential.create( type = "DateOfBirth", - issuer = issuerDid.did.uri, - subject = holderDid.did.uri, + issuer = issuerDid.uri, + subject = holderDid.uri, data = DateOfBirth(dateOfBirth = "01-02-03") ) val vcJwt = vc.sign(issuerDid) @@ -372,16 +372,16 @@ class PresentationExchangeTest { val vc1 = VerifiableCredential.create( type = "DateOfBirth", - issuer = issuerDid.did.uri, - subject = holderDid.did.uri, + issuer = issuerDid.uri, + subject = holderDid.uri, data = DateOfBirth(dateOfBirth = "Data1") ) val vcJwt1 = vc1.sign(issuerDid) val vc2 = VerifiableCredential.create( type = "Address", - issuer = issuerDid.did.uri, - subject = holderDid.did.uri, + issuer = issuerDid.uri, + subject = holderDid.uri, data = Address("abc street 123") ) val vcJwt2 = vc2.sign(issuerDid) @@ -400,8 +400,8 @@ class PresentationExchangeTest { ) val vc = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.did.uri, - subject = holderDid.did.uri, + issuer = issuerDid.uri, + subject = holderDid.uri, data = StreetCredibility(localRespect = "high", legit = true) ) val vcJwt = vc.sign(issuerDid) @@ -420,16 +420,16 @@ class PresentationExchangeTest { val vc1 = VerifiableCredential.create( type = "DateOfBirth", - issuer = issuerDid.did.uri, - subject = holderDid.did.uri, + issuer = issuerDid.uri, + subject = holderDid.uri, data = DateOfBirth(dateOfBirth = "11/11/2011") ) val vcJwt1 = vc1.sign(issuerDid) val vc2 = VerifiableCredential.create( type = "DateOfBirth", - issuer = issuerDid.did.uri, - subject = holderDid.did.uri, + issuer = issuerDid.uri, + subject = holderDid.uri, data = DateOfBirth(dateOfBirth = "12/12/2012") ) val vcJwt2 = vc2.sign(issuerDid) @@ -452,8 +452,8 @@ class PresentationExchangeTest { val vc = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.did.uri, - subject = holderDid.did.uri, + issuer = issuerDid.uri, + subject = holderDid.uri, data = StreetCredibility(localRespect = "high", legit = true) ) val vcJwt = vc.sign(issuerDid) @@ -473,16 +473,16 @@ class PresentationExchangeTest { val vc1 = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.did.uri, - subject = holderDid.did.uri, + issuer = issuerDid.uri, + subject = holderDid.uri, data = StreetCredibility(localRespect = "high", legit = true) ) val vcJwt1 = vc1.sign(issuerDid) val vc2 = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.did.uri, - subject = holderDid.did.uri, + issuer = issuerDid.uri, + subject = holderDid.uri, data = StreetCredibility(localRespect = "high", legit = true) ) val vcJwt2 = vc2.sign(issuerDid) @@ -502,16 +502,16 @@ class PresentationExchangeTest { val vc1 = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.did.uri, - subject = holderDid.did.uri, + issuer = issuerDid.uri, + subject = holderDid.uri, data = StreetCredibility(localRespect = "high", legit = true) ) val vcJwt1 = vc1.sign(issuerDid) val vc2 = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.did.uri, - subject = holderDid.did.uri, + issuer = issuerDid.uri, + subject = holderDid.uri, data = StreetCredibility(localRespect = "high", legit = true) ) @@ -519,8 +519,8 @@ class PresentationExchangeTest { val vc3 = VerifiableCredential.create( type = "DateOfBirth", - issuer = issuerDid.did.uri, - subject = holderDid.did.uri, + issuer = issuerDid.uri, + subject = holderDid.uri, data = DateOfBirth(dateOfBirth = "1-1-1111") ) @@ -541,16 +541,16 @@ class PresentationExchangeTest { val vc1 = VerifiableCredential.create( type = "DateOfBirthSSN", - issuer = issuerDid.did.uri, - subject = holderDid.did.uri, + issuer = issuerDid.uri, + subject = holderDid.uri, data = DateOfBirthSSN(dateOfBirth = "1999-01-01", ssn = "456-123-123") ) val vcJwt1 = vc1.sign(issuerDid) val vc2 = VerifiableCredential.create( type = "DateOfBirthSSN", - issuer = issuerDid.did.uri, - subject = holderDid.did.uri, + issuer = issuerDid.uri, + subject = holderDid.uri, data = DateOfBirth(dateOfBirth = "1999-01-01") ) val vcJwt2 = vc2.sign(issuerDid) diff --git a/credentials/src/test/kotlin/web5/sdk/credentials/StatusListCredentialTest.kt b/credentials/src/test/kotlin/web5/sdk/credentials/StatusListCredentialTest.kt index 809a9729a..923045c73 100644 --- a/credentials/src/test/kotlin/web5/sdk/credentials/StatusListCredentialTest.kt +++ b/credentials/src/test/kotlin/web5/sdk/credentials/StatusListCredentialTest.kt @@ -59,8 +59,8 @@ class StatusListCredentialTest { val credWithCredStatus = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.did.uri, - subject = holderDid.did.uri, + issuer = issuerDid.uri, + subject = holderDid.uri, data = StreetCredibility(localRespect = "high", legit = true), credentialStatus = credentialStatus ) @@ -75,7 +75,7 @@ class StatusListCredentialTest { ) assertEquals(credWithCredStatus.type, "StreetCred") - assertEquals(credWithCredStatus.issuer, issuerDid.did.uri) + assertEquals(credWithCredStatus.issuer, issuerDid.uri) assertNotNull(credWithCredStatus.vcDataModel.issuanceDate) assertNotNull(credWithCredStatus.vcDataModel.credentialStatus) assertEquals(credWithCredStatus.vcDataModel.credentialSubject.claims.get("localRespect"), "high") @@ -105,8 +105,8 @@ class StatusListCredentialTest { val vc1 = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.did.uri, - subject = holderDid.did.uri, + issuer = issuerDid.uri, + subject = holderDid.uri, data = StreetCredibility(localRespect = "high", legit = true), credentialStatus = credentialStatus1 ) @@ -120,15 +120,15 @@ class StatusListCredentialTest { val vc2 = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.did.uri, - subject = holderDid.did.uri, + issuer = issuerDid.uri, + subject = holderDid.uri, data = StreetCredibility(localRespect = "high", legit = true), credentialStatus2 ) val statusListCredential = StatusListCredential.create( "revocation-id", - issuerDid.did.uri, + issuerDid.uri, StatusPurpose.REVOCATION, listOf(vc1, vc2) ) @@ -180,8 +180,8 @@ class StatusListCredentialTest { val vc1 = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.did.uri, - subject = holderDid.did.uri, + issuer = issuerDid.uri, + subject = holderDid.uri, data = StreetCredibility(localRespect = "high", legit = true), credentialStatus = credentialStatus1 ) @@ -195,8 +195,8 @@ class StatusListCredentialTest { val vc2 = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.did.uri, - subject = holderDid.did.uri, + issuer = issuerDid.uri, + subject = holderDid.uri, data = StreetCredibility(localRespect = "high", legit = true), credentialStatus2 ) @@ -204,7 +204,7 @@ class StatusListCredentialTest { val exception = assertThrows { StatusListCredential.create( "revocation-id", - issuerDid.did.uri, + issuerDid.uri, StatusPurpose.REVOCATION, listOf(vc1, vc2) ) @@ -231,8 +231,8 @@ class StatusListCredentialTest { val vc1 = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.did.uri, - subject = holderDid.did.uri, + issuer = issuerDid.uri, + subject = holderDid.uri, data = StreetCredibility(localRespect = "high", legit = true), credentialStatus = credentialStatus1 ) @@ -240,7 +240,7 @@ class StatusListCredentialTest { val exception = assertThrows { StatusListCredential.create( "revocation-id", - issuerDid.did.uri, + issuerDid.uri, StatusPurpose.REVOCATION, listOf(vc1) ) @@ -267,8 +267,8 @@ class StatusListCredentialTest { val vc1 = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.did.uri, - subject = holderDid.did.uri, + issuer = issuerDid.uri, + subject = holderDid.uri, data = StreetCredibility(localRespect = "high", legit = true), credentialStatus = credentialStatus1 ) @@ -276,7 +276,7 @@ class StatusListCredentialTest { val exception = assertThrows { StatusListCredential.create( "revocation-id", - issuerDid.did.uri, + issuerDid.uri, StatusPurpose.REVOCATION, listOf(vc1) ) @@ -303,8 +303,8 @@ class StatusListCredentialTest { val vc1 = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.did.uri, - subject = holderDid.did.uri, + issuer = issuerDid.uri, + subject = holderDid.uri, data = StreetCredibility(localRespect = "high", legit = true), credentialStatus = credentialStatus1 ) @@ -318,8 +318,8 @@ class StatusListCredentialTest { val vc2 = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.did.uri, - subject = holderDid.did.uri, + issuer = issuerDid.uri, + subject = holderDid.uri, data = StreetCredibility(localRespect = "high", legit = true), credentialStatus2 ) @@ -333,8 +333,8 @@ class StatusListCredentialTest { val vc3 = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.did.uri, - subject = holderDid.did.uri, + issuer = issuerDid.uri, + subject = holderDid.uri, data = StreetCredibility(localRespect = "high", legit = true), credentialStatus3 ) @@ -342,7 +342,7 @@ class StatusListCredentialTest { val statusListCredential = StatusListCredential.create( "revocation-id", - issuerDid.did.uri, + issuerDid.uri, StatusPurpose.REVOCATION, listOf(vc1, vc2) ) @@ -372,8 +372,8 @@ class StatusListCredentialTest { val vc1 = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.did.uri, - subject = holderDid.did.uri, + issuer = issuerDid.uri, + subject = holderDid.uri, data = StreetCredibility(localRespect = "high", legit = true), credentialStatus = credentialStatus1 ) @@ -387,8 +387,8 @@ class StatusListCredentialTest { val vc2 = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.did.uri, - subject = holderDid.did.uri, + issuer = issuerDid.uri, + subject = holderDid.uri, data = StreetCredibility(localRespect = "high", legit = true), credentialStatus2 ) @@ -396,7 +396,7 @@ class StatusListCredentialTest { val statusListCredential = StatusListCredential.create( "revocation-id", - issuerDid.did.uri, + issuerDid.uri, StatusPurpose.REVOCATION, listOf(vc1) ) @@ -442,8 +442,8 @@ class StatusListCredentialTest { val credToValidate = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.did.uri, - subject = holderDid.did.uri, + issuer = issuerDid.uri, + subject = holderDid.uri, data = StreetCredibility(localRespect = "high", legit = true), credentialStatus = credentialStatus1 ) diff --git a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt index b3bf02e63..43f5b2d7a 100644 --- a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt +++ b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt @@ -42,8 +42,8 @@ class VerifiableCredentialTest { val vc = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.did.uri, - subject = holderDid.did.uri, + issuer = issuerDid.uri, + subject = holderDid.uri, data = StreetCredibility(localRespect = "high", legit = true) ) @@ -62,8 +62,8 @@ class VerifiableCredentialTest { val vc = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.did.uri, - subject = holderDid.did.uri, + issuer = issuerDid.uri, + subject = holderDid.uri, data = StreetCredibility(localRespect = "high", legit = true) ) assertNotNull(vc) @@ -78,8 +78,8 @@ class VerifiableCredentialTest { val exception = assertThrows(IllegalArgumentException::class.java) { VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.did.uri, - subject = holderDid.did.uri, + issuer = issuerDid.uri, + subject = holderDid.uri, data = "trials & tribulations" ) } @@ -96,8 +96,8 @@ class VerifiableCredentialTest { val vc = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.did.uri, - subject = holderDid.did.uri, + issuer = issuerDid.uri, + subject = holderDid.uri, data = StreetCredibility(localRespect = "high", legit = true) ) @@ -113,8 +113,8 @@ class VerifiableCredentialTest { val vc = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.did.uri, - subject = holderDid.did.uri, + issuer = issuerDid.uri, + subject = holderDid.uri, data = StreetCredibility(localRespect = "high", legit = true) ) @@ -143,7 +143,7 @@ class VerifiableCredentialTest { val header = JwsHeader.Builder() .type("JWT") .algorithm(Jwa.ES256K.name) - .keyId(issuerDid.did.uri) + .keyId(issuerDid.uri) .build() // A detached payload JWT val vcJwt = "${Convert(Json.stringify(header)).toBase64Url()}..fakeSig" @@ -204,8 +204,8 @@ class VerifiableCredentialTest { val vc = VerifiableCredential.create( type = "StreetCred", - issuer = issuerDid.did.uri, - subject = holderDid.did.uri, + issuer = issuerDid.uri, + subject = holderDid.uri, data = StreetCredibility(localRespect = "high", legit = true) ) diff --git a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiablePresentationTest.kt b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiablePresentationTest.kt index 4a43261fc..50166b67b 100644 --- a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiablePresentationTest.kt +++ b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiablePresentationTest.kt @@ -51,11 +51,11 @@ class VerifiablePresentationTest { val vp = VerifiablePresentation.create( vcJwts = vcJwts, - holder = holderDid.did.uri + holder = holderDid.uri ) assertNotNull(vp, "VerifiablePresentation should not be null") - assertEquals(holderDid.did.uri, vp.holder, "holder should match") + assertEquals(holderDid.uri, vp.holder, "holder should match") assertEquals(vcJwts, vp.verifiableCredential, "vcJwts should match") } @@ -80,7 +80,7 @@ class VerifiablePresentationTest { val vp = VerifiablePresentation.create( vcJwts = vcJwts, - holder = holderDid.did.uri, + holder = holderDid.uri, additionalData = mapOf("presentation_submission" to presentationSubmission) ) @@ -99,7 +99,7 @@ class VerifiablePresentationTest { val vp = VerifiablePresentation.create( vcJwts = listOf("vcjwt1", "vcjwt2"), - holder = holderDid.did.uri + holder = holderDid.uri ) val vpJwt = vp.sign(holderDid) @@ -127,7 +127,7 @@ class VerifiablePresentationTest { val vp = VerifiablePresentation.create( vcJwts = vcJwts, - holder = holderDid.did.uri, + holder = holderDid.uri, type = "PresentationSubmission", additionalData = mapOf("presentation_submission" to presentationSubmission) ) @@ -148,7 +148,7 @@ class VerifiablePresentationTest { val vp = VerifiablePresentation.create( vcJwts = vcJwts, - holder = holderDid.did.uri + holder = holderDid.uri ) val vpJwt = vp.sign(holderDid) @@ -164,7 +164,7 @@ class VerifiablePresentationTest { val header = JwsHeader.Builder() .type("JWT") .algorithm(Jwa.ES256K.name) - .keyId(holderDid.did.uri) + .keyId(holderDid.uri) .build() val vpJwt = "${Convert(Json.stringify(header)).toBase64Url()}..fakeSig" @@ -187,7 +187,7 @@ class VerifiablePresentationTest { val vp = VerifiablePresentation.create( vcJwts = vcJwts, - holder = holderDid.did.uri + holder = holderDid.uri ) val vpJwt = vp.sign(holderDid) @@ -245,7 +245,7 @@ class VerifiablePresentationTest { val header = JwsHeader.Builder() .type("JWT") .algorithm(Jwa.ES256K.name) - .keyId(issuerDid.did.uri) + .keyId(issuerDid.uri) .build() //A detached payload JWT val vpJwt = "${Convert(Json.stringify(header)).toBase64Url()}..fakeSig" @@ -269,15 +269,15 @@ class VerifiablePresentationTest { val vc = VerifiableCredential.create( type = "EmploymentCredential", - issuer = issuerDid.did.uri, - subject = holderDid.did.uri, + issuer = issuerDid.uri, + subject = holderDid.uri, data = EmploymentStatus(employmentStatus = "employed") ) val vc2 = VerifiableCredential.create( type = "PIICredential", - issuer = issuerDid.did.uri, - subject = holderDid.did.uri, + issuer = issuerDid.uri, + subject = holderDid.uri, data = PIICredential(name = "Alice Smith", dateOfBirth = "2001-12-21T17:02:01Z") ) @@ -320,7 +320,7 @@ class VerifiablePresentationTest { val verifiablePresentation = VerifiablePresentation.create( vcJwts = listOf(vcJwt1, vcJwt2), - holder = holderDid.did.uri, + holder = holderDid.uri, additionalData = mapOf("presentation_submission" to presentationResult) ) diff --git a/crypto/src/main/kotlin/web5/sdk/crypto/AwsKeyManager.kt b/crypto/src/main/kotlin/web5/sdk/crypto/AwsKeyManager.kt index 5ff2f402f..f9506c956 100644 --- a/crypto/src/main/kotlin/web5/sdk/crypto/AwsKeyManager.kt +++ b/crypto/src/main/kotlin/web5/sdk/crypto/AwsKeyManager.kt @@ -18,10 +18,7 @@ import com.nimbusds.jose.jwk.ECKey import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo import org.bouncycastle.crypto.ExtendedDigest import org.bouncycastle.crypto.digests.SHA256Digest -import org.bouncycastle.jce.spec.ECNamedCurveSpec import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter -import web5.sdk.common.Convert -import web5.sdk.common.EncodingFormat import web5.sdk.crypto.jwk.Jwk import java.nio.ByteBuffer import java.security.PublicKey @@ -131,7 +128,7 @@ public class AwsKeyManager @JvmOverloads constructor( val algorithmDetails = getAlgorithmDetails(publicKeyResponse.keySpec.enum()) val jwkBuilder = when (publicKey) { is ECPublicKey -> { - val key = ECKey.Builder(JwaCurve.toJwkCurve(algorithmDetails.curve), publicKey).build() + val key = ECKey.Builder(JwaCurve.toNimbusCurve(algorithmDetails.curve), publicKey).build() Jwk.Builder("EC", key.curve.name) .x(key.x.toString()) .y(key.y.toString()) @@ -172,7 +169,7 @@ public class AwsKeyManager @JvmOverloads constructor( * @return The alias belonging to [publicKey] */ override fun getDeterministicAlias(publicKey: Jwk): String { - val jwkThumbprint = publicKey.kid ?: publicKey.computeThumbprint() + val jwkThumbprint = publicKey.computeThumbprint() return "alias/$jwkThumbprint" } diff --git a/crypto/src/main/kotlin/web5/sdk/crypto/Dsa.kt b/crypto/src/main/kotlin/web5/sdk/crypto/Dsa.kt index ecf278b5a..e06c589ed 100644 --- a/crypto/src/main/kotlin/web5/sdk/crypto/Dsa.kt +++ b/crypto/src/main/kotlin/web5/sdk/crypto/Dsa.kt @@ -14,13 +14,13 @@ public enum class JwaCurve { public companion object { /** - * Convert JwaCurve nimbusds JWK curve. + * Convert JwaCurve to nimbusds JWK curve. * Used to temporarily bridge the gap between moving from nimbusds JWK methods * to rolling our own JWK methods * @param curve * @return nimbus JWK Curve */ - public fun toJwkCurve(curve: JwaCurve): Curve { + public fun toNimbusCurve(curve: JwaCurve): Curve { return when (curve) { secp256k1 -> Curve.SECP256K1 Ed25519 -> Curve.Ed25519 diff --git a/crypto/src/main/kotlin/web5/sdk/crypto/Ed25519.kt b/crypto/src/main/kotlin/web5/sdk/crypto/Ed25519.kt index af6f47281..2847ef852 100644 --- a/crypto/src/main/kotlin/web5/sdk/crypto/Ed25519.kt +++ b/crypto/src/main/kotlin/web5/sdk/crypto/Ed25519.kt @@ -2,9 +2,7 @@ package web5.sdk.crypto import com.google.crypto.tink.subtle.Ed25519Sign import com.google.crypto.tink.subtle.Ed25519Verify -import com.nimbusds.jose.JWSAlgorithm import com.nimbusds.jose.jwk.Curve -import com.nimbusds.jose.jwk.KeyUse import com.nimbusds.jose.jwk.OctetKeyPair import com.nimbusds.jose.jwk.gen.OctetKeyPairGenerator import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters diff --git a/crypto/src/main/kotlin/web5/sdk/crypto/InMemoryKeyManager.kt b/crypto/src/main/kotlin/web5/sdk/crypto/InMemoryKeyManager.kt index a5249e6ec..24317b7fa 100644 --- a/crypto/src/main/kotlin/web5/sdk/crypto/InMemoryKeyManager.kt +++ b/crypto/src/main/kotlin/web5/sdk/crypto/InMemoryKeyManager.kt @@ -82,7 +82,7 @@ public class InMemoryKeyManager : KeyManager, KeyExporter, KeyImporter { * @throws IllegalArgumentException if the key is not known to the [KeyManager] */ override fun getDeterministicAlias(publicKey: Jwk): String { - val kid = publicKey.kid ?: publicKey.computeThumbprint() + val kid = publicKey.computeThumbprint() require(keyStore.containsKey(kid)) { "key with alias $kid not found" } diff --git a/crypto/src/main/kotlin/web5/sdk/crypto/jwk/Jwk.kt b/crypto/src/main/kotlin/web5/sdk/crypto/jwk/Jwk.kt index 455952d8c..c4f13b6ff 100644 --- a/crypto/src/main/kotlin/web5/sdk/crypto/jwk/Jwk.kt +++ b/crypto/src/main/kotlin/web5/sdk/crypto/jwk/Jwk.kt @@ -36,7 +36,7 @@ public class Jwk( public val use: String?, public val alg: String?, public var kid: String?, - public val d: String? = null, + public val d: String?, public val x: String?, public val y: String? ) { diff --git a/dids/src/main/kotlin/web5/sdk/dids/DidMethod.kt b/dids/src/main/kotlin/web5/sdk/dids/DidMethod.kt index 21392b21c..283b07562 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/DidMethod.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/DidMethod.kt @@ -18,17 +18,3 @@ package web5.sdk.dids * ``` */ public interface CreateDidOptions - -/** - * Represents metadata that results from the creation of a Decentralized Identifier (DID). - * - * Implementers can include information that would be considered useful for callers. - * - * ### Usage Example - * ``` - * class MyDidMethodCreatedMetadata : CreationMetadata { - * // implementation-specific metadata about the created did - * } - * ``` - */ -public interface CreationMetadata diff --git a/dids/src/main/kotlin/web5/sdk/dids/did/BearerDid.kt b/dids/src/main/kotlin/web5/sdk/dids/did/BearerDid.kt index ade5f0892..7a5658c70 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/did/BearerDid.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/did/BearerDid.kt @@ -21,6 +21,7 @@ public typealias DidSigner = (payload: ByteArray) -> ByteArray * @param document The DID Document associated with the DID. */ public class BearerDid( + public val uri: String, public val did: Did, public val keyManager: KeyManager, public val document: DidDocument @@ -74,21 +75,25 @@ public class BearerDid( */ public fun export(): PortableDid { - val keyExporter = keyManager as? KeyExporter + check(keyManager is KeyExporter) { + "KeyManager must implement KeyExporter to export keys" + } + + val keyExporter = keyManager as KeyExporter val privateKeys = mutableListOf() document.verificationMethod?.forEach { vm -> val keyAliasResult = runCatching { vm.publicKeyJwk?.computeThumbprint() } if (keyAliasResult.isSuccess) { val keyAlias = keyAliasResult.getOrNull() - keyExporter?.exportKey(keyAlias!!.toString())?.let { key -> + keyExporter.exportKey(keyAlias!!.toString()).let { key -> privateKeys.add(key) } } } return PortableDid( - uri = this.did.uri, + uri = this.uri, document = this.document, privateKeys = privateKeys, metadata = mapOf() @@ -116,6 +121,11 @@ public class BearerDid( portableDid: PortableDid, keyManager: KeyManager = InMemoryKeyManager() ): BearerDid { + + check(keyManager is KeyImporter) { + "KeyManager must implement KeyImporter to import keys" + } + check(portableDid.document.verificationMethod?.size != 0) { "PortableDID must contain at least one verification method" } @@ -131,11 +141,11 @@ public class BearerDid( val did = Did.parse(portableDid.uri) for (key in portableDid.privateKeys) { - val keyImporter = keyManager as? KeyImporter - keyImporter!!.importKey(key) + val keyImporter = keyManager as KeyImporter + keyImporter.importKey(key) } - return BearerDid(did, keyManager, portableDid.document) + return BearerDid(portableDid.uri, did, keyManager, portableDid.document) } } diff --git a/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt b/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt index 22d1ed5ba..5130be645 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt @@ -199,7 +199,7 @@ public sealed class DidDhtApi(configuration: DidDhtConfiguration) { val id = this.suffix(didUri) val did = Did(method = methodName, uri = didUri, url = didUri, id = id) - return BearerDid(did, keyManager, didDocument) + return BearerDid(didUri, did, keyManager, didDocument) } /** diff --git a/dids/src/main/kotlin/web5/sdk/dids/methods/jwk/DidJwk.kt b/dids/src/main/kotlin/web5/sdk/dids/methods/jwk/DidJwk.kt index 03b318bc8..ff24505ef 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/methods/jwk/DidJwk.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/methods/jwk/DidJwk.kt @@ -61,7 +61,7 @@ public object DidJwk { val did = Did(method = methodName, uri = didUri, url = didUri, id = base64Encoded) - return BearerDid(did, keyManager, createDocument(did, publicKeyJwk)) + return BearerDid(didUri, did, keyManager, createDocument(did, publicKeyJwk)) } diff --git a/dids/src/main/kotlin/web5/sdk/dids/methods/key/DidKey.kt b/dids/src/main/kotlin/web5/sdk/dids/methods/key/DidKey.kt index 6faddcf2f..0fe736d52 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/methods/key/DidKey.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/methods/key/DidKey.kt @@ -104,7 +104,7 @@ public class DidKey(public val uri: String, public val keyManager: KeyManager) { check(resolutionResult.didDocument != null) { "DidDocument not found" } - return BearerDid(did, keyManager, resolutionResult.didDocument) + return BearerDid(didUrl, did, keyManager, resolutionResult.didDocument) } /** diff --git a/dids/src/test/kotlin/web5/sdk/dids/DidResolversTest.kt b/dids/src/test/kotlin/web5/sdk/dids/DidResolversTest.kt index 1d37f3414..168b79418 100644 --- a/dids/src/test/kotlin/web5/sdk/dids/DidResolversTest.kt +++ b/dids/src/test/kotlin/web5/sdk/dids/DidResolversTest.kt @@ -19,7 +19,7 @@ class DidResolversTest { fun `resolving a default dht did contains assertion method`() { val dhtDid = DidDht.create(InMemoryKeyManager(), null) - val resolutionResult = DidResolvers.resolve(dhtDid.did.uri) + val resolutionResult = DidResolvers.resolve(dhtDid.uri) assertNotNull(resolutionResult.didDocument!!.assertionMethod) } diff --git a/dids/src/test/kotlin/web5/sdk/dids/did/BearerDidTest.kt b/dids/src/test/kotlin/web5/sdk/dids/did/BearerDidTest.kt index 6b0523f39..d0965d2f5 100644 --- a/dids/src/test/kotlin/web5/sdk/dids/did/BearerDidTest.kt +++ b/dids/src/test/kotlin/web5/sdk/dids/did/BearerDidTest.kt @@ -40,7 +40,7 @@ class BearerDidTest { val bearerDid = DidJwk.create(InMemoryKeyManager()) val portableDid = bearerDid.export() - assertEquals(portableDid.uri, bearerDid.did.uri) + assertEquals(portableDid.uri, bearerDid.uri) assertEquals(portableDid.document, bearerDid.document) assertEquals(1, portableDid.privateKeys.size) } @@ -50,7 +50,7 @@ class BearerDidTest { val portableDid = DidJwk.create(InMemoryKeyManager()).export() val bearerDid = DidJwk.import(portableDid) - assertEquals(portableDid.uri, bearerDid.did.uri) + assertEquals(portableDid.uri, bearerDid.uri) assertEquals(portableDid.document, bearerDid.document) val portableDidKid = portableDid.privateKeys[0].kid ?: portableDid.privateKeys[0].computeThumbprint() assertNotNull(bearerDid.keyManager.getPublicKey(portableDidKid)) diff --git a/dids/src/test/kotlin/web5/sdk/dids/didcore/DidDocumentTest.kt b/dids/src/test/kotlin/web5/sdk/dids/didcore/DidDocumentTest.kt index 5b6033389..505ba8c6a 100644 --- a/dids/src/test/kotlin/web5/sdk/dids/didcore/DidDocumentTest.kt +++ b/dids/src/test/kotlin/web5/sdk/dids/didcore/DidDocumentTest.kt @@ -198,10 +198,10 @@ class DidDocumentTest { val manager = InMemoryKeyManager() val bearerDid = DidKey.create(manager) - val verificationMethod = DidKey.resolve(bearerDid.did.uri) + val verificationMethod = DidKey.resolve(bearerDid.uri) .didDocument!! .findAssertionMethodById() - assertEquals("${bearerDid.did.uri}#${Did.parse(bearerDid.did.uri).id}", verificationMethod.id) + assertEquals("${bearerDid.uri}#${Did.parse(bearerDid.uri).id}", verificationMethod.id) } @Test @@ -209,8 +209,8 @@ class DidDocumentTest { val manager = InMemoryKeyManager() val bearerDid = DidKey.create(manager) - val assertionMethodId = "${bearerDid.did.uri}#${Did.parse(bearerDid.did.uri).id}" - val verificationMethod = DidKey.resolve(bearerDid.did.uri) + val assertionMethodId = "${bearerDid.uri}#${Did.parse(bearerDid.uri).id}" + val verificationMethod = DidKey.resolve(bearerDid.uri) .didDocument!! .findAssertionMethodById(assertionMethodId) assertEquals(assertionMethodId, verificationMethod.id) @@ -222,7 +222,7 @@ class DidDocumentTest { val bearerDid = DidKey.create(manager) val exception = assertThrows { - DidKey.resolve(bearerDid.did.uri) + DidKey.resolve(bearerDid.uri) .didDocument!! .findAssertionMethodById("made up assertion method id") } diff --git a/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DhtTest.kt b/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DhtTest.kt index e8029e27a..0e27571c2 100644 --- a/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DhtTest.kt +++ b/dids/src/test/kotlin/web5/sdk/dids/methods/dht/DhtTest.kt @@ -175,9 +175,9 @@ class DhtTest { val bep44Message = DhtClient.createBep44PutRequest(manager, kid, message) assertNotNull(bep44Message) - assertDoesNotThrow { dhtClient.pkarrPut(suffix(bearerDid.did.uri), bep44Message) } + assertDoesNotThrow { dhtClient.pkarrPut(suffix(bearerDid.uri), bep44Message) } - val retrievedMessage = assertDoesNotThrow { dhtClient.pkarrGet(suffix(bearerDid.did.uri)) } + val retrievedMessage = assertDoesNotThrow { dhtClient.pkarrGet(suffix(bearerDid.uri)) } assertNotNull(retrievedMessage) } diff --git a/dids/src/test/kotlin/web5/sdk/dids/methods/jwk/DidJwkTest.kt b/dids/src/test/kotlin/web5/sdk/dids/methods/jwk/DidJwkTest.kt index 546736c36..f2526b64b 100644 --- a/dids/src/test/kotlin/web5/sdk/dids/methods/jwk/DidJwkTest.kt +++ b/dids/src/test/kotlin/web5/sdk/dids/methods/jwk/DidJwkTest.kt @@ -29,7 +29,7 @@ class DidJwkTest { val manager = InMemoryKeyManager() val did = DidJwk.create(manager) - val didResolutionResult = DidResolvers.resolve(did.did.uri) + val didResolutionResult = DidResolvers.resolve(did.uri) val verificationMethod = didResolutionResult.didDocument!!.verificationMethod?.get(0) assertNotNull(verificationMethod) @@ -50,7 +50,7 @@ class DidJwkTest { val bearerDid = DidJwk.create(manager) val portableDid = bearerDid.export() val importedDid = DidJwk.import(portableDid, manager) - assertEquals(bearerDid.did.uri, importedDid.did.uri) + assertEquals(bearerDid.uri, importedDid.uri) } @Test diff --git a/dids/src/test/kotlin/web5/sdk/dids/methods/key/DidKeyTest.kt b/dids/src/test/kotlin/web5/sdk/dids/methods/key/DidKeyTest.kt index 66a29e7d6..e7f954a41 100644 --- a/dids/src/test/kotlin/web5/sdk/dids/methods/key/DidKeyTest.kt +++ b/dids/src/test/kotlin/web5/sdk/dids/methods/key/DidKeyTest.kt @@ -19,11 +19,11 @@ class DidKeyTest { @Nested inner class CreateTest { @Test - fun `it works`() { + fun `creates and resolves did key`() { val manager = InMemoryKeyManager() val bearerDid = DidKey.create(manager) - val didResolutionResult = DidResolvers.resolve(bearerDid.did.uri) + val didResolutionResult = DidResolvers.resolve(bearerDid.uri) assertNotNull(didResolutionResult.didDocument) val verificationMethod = didResolutionResult.didDocument!!.verificationMethod?.get(0) @@ -104,7 +104,7 @@ class DidKeyTest { val bearerDid = DidKey.create(manager) val portableDid = bearerDid.export() val importedDid = DidKey.import(portableDid, manager) - assertEquals(bearerDid.did.uri, importedDid.did.uri) + assertEquals(bearerDid.uri, importedDid.uri) } @Test diff --git a/jose/src/main/kotlin/web5/sdk/jose/jws/Jws.kt b/jose/src/main/kotlin/web5/sdk/jose/jws/Jws.kt index c6499e79b..0db089f89 100644 --- a/jose/src/main/kotlin/web5/sdk/jose/jws/Jws.kt +++ b/jose/src/main/kotlin/web5/sdk/jose/jws/Jws.kt @@ -74,7 +74,7 @@ public object Jws { } val kid = if (verificationMethod.id.startsWith("#")) { - "${bearerDid.did.uri}${verificationMethod.id}" + "${bearerDid.uri}${verificationMethod.id}" } else { verificationMethod.id } From 5ab978d194a804707067c1777a94b9ee05bf4e54 Mon Sep 17 00:00:00 2001 From: Jiyoon Koo Date: Tue, 26 Mar 2024 21:46:37 -0400 Subject: [PATCH 41/47] changing how portabledid is constructed in vc test vector test --- .../sdk/credentials/VerifiableCredentialTest.kt | 14 ++++++++++++-- web5-spec | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt index 693ed0a3f..b2d4a62e2 100644 --- a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt +++ b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt @@ -12,8 +12,10 @@ import web5.sdk.crypto.AlgorithmId import web5.sdk.crypto.AwsKeyManager import web5.sdk.crypto.InMemoryKeyManager import web5.sdk.crypto.Jwa +import web5.sdk.crypto.jwk.Jwk import web5.sdk.dids.did.BearerDid import web5.sdk.dids.did.PortableDid +import web5.sdk.dids.didcore.DidDocument import web5.sdk.dids.didcore.Purpose import web5.sdk.jose.jws.JwsHeader import web5.sdk.jose.jwt.Jwt @@ -251,7 +253,10 @@ class VerifiableCredentialTest { class Web5TestVectorsCredentials { data class CreateTestInput( - val signerPortableDid: PortableDid?, + val uri: String?, + val privateKeys: List?, + val document: DidDocument?, + val metadata: Map?, val credential: Map?, ) @@ -268,7 +273,12 @@ class Web5TestVectorsCredentials { testVectors.vectors.filterNot { it.errors ?: false }.forEach { vector -> val vc = VerifiableCredential.fromJson(mapper.writeValueAsString(vector.input.credential)) - val portableDid = Json.parse(Json.stringify(vector.input.signerPortableDid!!)) + val portableDid = PortableDid( + vector.input.uri!!, + vector.input.privateKeys!!, + vector.input.document!!, + vector.input.metadata!! + ) val keyManager = InMemoryKeyManager() val bearerDid = BearerDid.import(portableDid, keyManager) diff --git a/web5-spec b/web5-spec index 55acc22a2..78663ce3b 160000 --- a/web5-spec +++ b/web5-spec @@ -1 +1 @@ -Subproject commit 55acc22a2a6e33fd2ef530c1cfd0405974dc0d44 +Subproject commit 78663ce3b912e600fff4099543da55c3ffc477fb From 030678d990beb9c0aaa10ccabadc7c872748fccc Mon Sep 17 00:00:00 2001 From: Jiyoon Koo Date: Tue, 26 Mar 2024 22:31:05 -0400 Subject: [PATCH 42/47] addressing review comments --- .../main/kotlin/web5/sdk/credentials/VerifiablePresentation.kt | 2 +- .../kotlin/web5/sdk/credentials/VerifiablePresentationTest.kt | 3 ++- dids/src/main/kotlin/web5/sdk/dids/methods/key/DidKey.kt | 2 +- dids/src/test/kotlin/web5/sdk/dids/methods/key/DidKeyTest.kt | 2 +- jose/src/main/kotlin/web5/sdk/jose/jwt/Jwt.kt | 2 +- web5-spec | 2 +- 6 files changed, 7 insertions(+), 6 deletions(-) diff --git a/credentials/src/main/kotlin/web5/sdk/credentials/VerifiablePresentation.kt b/credentials/src/main/kotlin/web5/sdk/credentials/VerifiablePresentation.kt index c72bdfbd4..81e418824 100644 --- a/credentials/src/main/kotlin/web5/sdk/credentials/VerifiablePresentation.kt +++ b/credentials/src/main/kotlin/web5/sdk/credentials/VerifiablePresentation.kt @@ -60,7 +60,7 @@ public class VerifiablePresentation internal constructor(public val vpDataModel: public fun sign(bearerDid: BearerDid, assertionMethodId: String? = null): String { val payload = JwtClaimsSet.Builder() .issuer(bearerDid.uri) - .issueTime(Date().time) + .issueTime(Date().time / 1000) .misc("vp", vpDataModel.toMap()) .build() diff --git a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiablePresentationTest.kt b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiablePresentationTest.kt index 50166b67b..cad6c648e 100644 --- a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiablePresentationTest.kt +++ b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiablePresentationTest.kt @@ -164,7 +164,8 @@ class VerifiablePresentationTest { val header = JwsHeader.Builder() .type("JWT") .algorithm(Jwa.ES256K.name) - .keyId(holderDid.uri) + // todo does fragment always start with a # ? if not need to add # in the middle + .keyId("${holderDid.uri}${holderDid.did.fragment}") .build() val vpJwt = "${Convert(Json.stringify(header)).toBase64Url()}..fakeSig" diff --git a/dids/src/main/kotlin/web5/sdk/dids/methods/key/DidKey.kt b/dids/src/main/kotlin/web5/sdk/dids/methods/key/DidKey.kt index 0fe736d52..2876c27b0 100644 --- a/dids/src/main/kotlin/web5/sdk/dids/methods/key/DidKey.kt +++ b/dids/src/main/kotlin/web5/sdk/dids/methods/key/DidKey.kt @@ -141,7 +141,7 @@ public class DidKey(public val uri: String, public val keyManager: KeyManager) { .id(verificationMethodId) .publicKeyJwk(publicKeyJwk) .controller(did) - .type("JsonWebKey2020") + .type("JsonWebKey") .build() val didDocument = DidDocument.Builder() diff --git a/dids/src/test/kotlin/web5/sdk/dids/methods/key/DidKeyTest.kt b/dids/src/test/kotlin/web5/sdk/dids/methods/key/DidKeyTest.kt index e7f954a41..1ddaac2f1 100644 --- a/dids/src/test/kotlin/web5/sdk/dids/methods/key/DidKeyTest.kt +++ b/dids/src/test/kotlin/web5/sdk/dids/methods/key/DidKeyTest.kt @@ -70,7 +70,7 @@ class DidKeyTest { // Note: cannot run the controller assertion because underlying lib enforces JSON-LD @context // despite it not being a required field // assertEquals(did, verificationMethod.controller.toString()) - assertEquals("JsonWebKey2020", verificationMethod.type) + assertEquals("JsonWebKey", verificationMethod.type) assertNotNull(verificationMethod.publicKeyJwk) val publicKeyJwk = verificationMethod.publicKeyJwk // validates diff --git a/jose/src/main/kotlin/web5/sdk/jose/jwt/Jwt.kt b/jose/src/main/kotlin/web5/sdk/jose/jwt/Jwt.kt index b88ebc493..c338e4e68 100644 --- a/jose/src/main/kotlin/web5/sdk/jose/jwt/Jwt.kt +++ b/jose/src/main/kotlin/web5/sdk/jose/jwt/Jwt.kt @@ -205,7 +205,7 @@ public class JwtClaimsSet( public fun notBeforeTime(nbf: Long): Builder = apply { this.nbf = nbf } /** - * Sets Issued At (iat) claim. + * Sets Issued At (iat) claim. Denominated in seconds. * * @param iat The time at which the JWT was issued * @return Builder object diff --git a/web5-spec b/web5-spec index 78663ce3b..4787b82cc 160000 --- a/web5-spec +++ b/web5-spec @@ -1 +1 @@ -Subproject commit 78663ce3b912e600fff4099543da55c3ffc477fb +Subproject commit 4787b82cc6b71032529408e3c9336f85a81d925b From 6004707fdd3c73334b29dfb12e76fdbdfb127048 Mon Sep 17 00:00:00 2001 From: Jiyoon Koo Date: Tue, 26 Mar 2024 22:33:31 -0400 Subject: [PATCH 43/47] Update dids/src/test/kotlin/web5/sdk/dids/methods/jwk/DidJwkTest.kt Co-authored-by: Gabe <7622243+decentralgabe@users.noreply.github.com> --- dids/src/test/kotlin/web5/sdk/dids/methods/jwk/DidJwkTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dids/src/test/kotlin/web5/sdk/dids/methods/jwk/DidJwkTest.kt b/dids/src/test/kotlin/web5/sdk/dids/methods/jwk/DidJwkTest.kt index f2526b64b..004ae46ce 100644 --- a/dids/src/test/kotlin/web5/sdk/dids/methods/jwk/DidJwkTest.kt +++ b/dids/src/test/kotlin/web5/sdk/dids/methods/jwk/DidJwkTest.kt @@ -25,7 +25,7 @@ class DidJwkTest { @Nested inner class CreateTest { @Test - fun `creates an EdDSA key when no options are passed`() { + fun `creates an Ed25519 key when no options are passed`() { val manager = InMemoryKeyManager() val did = DidJwk.create(manager) From b47299053369c359cb49aad2b2ac6612f36d3419 Mon Sep 17 00:00:00 2001 From: Jiyoon Koo Date: Tue, 26 Mar 2024 23:49:47 -0400 Subject: [PATCH 44/47] making error in testvectorinput to default to false, added portabledid parse test --- .../credentials/VerifiableCredentialTest.kt | 7 +- .../web5/sdk/dids/did/PortableDidTest.kt | 64 +++++++++++++++++++ .../kotlin/web5/sdk/testing/TestVectors.kt | 2 +- 3 files changed, 68 insertions(+), 5 deletions(-) create mode 100644 dids/src/test/kotlin/web5/sdk/dids/did/PortableDidTest.kt diff --git a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt index b2d4a62e2..c041e50c9 100644 --- a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt +++ b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt @@ -256,7 +256,6 @@ class Web5TestVectorsCredentials { val uri: String?, val privateKeys: List?, val document: DidDocument?, - val metadata: Map?, val credential: Map?, ) @@ -271,13 +270,13 @@ class Web5TestVectorsCredentials { val typeRef = object : TypeReference>() {} val testVectors = mapper.readValue(File("../web5-spec/test-vectors/credentials/create.json"), typeRef) - testVectors.vectors.filterNot { it.errors ?: false }.forEach { vector -> + testVectors.vectors.filter { it.errors == false }.forEach { vector -> val vc = VerifiableCredential.fromJson(mapper.writeValueAsString(vector.input.credential)) val portableDid = PortableDid( vector.input.uri!!, vector.input.privateKeys!!, vector.input.document!!, - vector.input.metadata!! + mapOf() ) val keyManager = InMemoryKeyManager() @@ -287,7 +286,7 @@ class Web5TestVectorsCredentials { assertEquals(vector.output, vcJwt, vector.description) } - testVectors.vectors.filter { it.errors ?: false }.forEach { vector -> + testVectors.vectors.filter { it.errors == true }.forEach { vector -> assertFails(vector.description) { VerifiableCredential.fromJson(mapper.writeValueAsString(vector.input.credential)) } diff --git a/dids/src/test/kotlin/web5/sdk/dids/did/PortableDidTest.kt b/dids/src/test/kotlin/web5/sdk/dids/did/PortableDidTest.kt new file mode 100644 index 000000000..ac6ae88e1 --- /dev/null +++ b/dids/src/test/kotlin/web5/sdk/dids/did/PortableDidTest.kt @@ -0,0 +1,64 @@ +package web5.sdk.dids.did + +import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import org.junit.jupiter.api.Test +import web5.sdk.common.Json +import web5.sdk.crypto.InMemoryKeyManager +import web5.sdk.crypto.jwk.Jwk +import web5.sdk.dids.didcore.Did +import web5.sdk.dids.didcore.DidDocument +import web5.sdk.testing.TestVectors +import java.io.File +import kotlin.test.assertEquals +import kotlin.test.assertFails + +class Web5TestVectorsPortableDid { + + data class CreateTestInput( + val uri: String?, + val privateKeys: List?, + val document: DidDocument?, + val metadata: Map?, + ) + + + private val mapper = jacksonObjectMapper() + + @Test + fun create() { + val typeRef = object : TypeReference>() {} + val testVectors = mapper.readValue(File("../web5-spec/test-vectors/portable_did/parse.json"), typeRef) + + testVectors.vectors.filter { it.errors == false }.forEach { vector -> + + val portableDid = Json.parse(Json.stringify(mapOf( + "uri" to vector.input.uri, + "privateKeys" to vector.input.privateKeys, + "document" to vector.input.document, + "metadata" to vector.input.metadata + ))) + + val bearerDid = BearerDid.import(portableDid, InMemoryKeyManager()) + val did = Did.parse(vector.input.uri!!) + assertEquals(bearerDid.uri, vector.input.uri) + assertEquals(bearerDid.document.toString(), vector.input.document.toString()) + assertEquals(bearerDid.did.url, did.url) + } + + testVectors.vectors.filter { it.errors == true }.forEach { vector -> + assertFails(vector.description) { + Json.parse(Json.stringify(mapOf( + "uri" to vector.input.uri, + "privateKeys" to vector.input.privateKeys, + "document" to vector.input.document, + "metadata" to vector.input.metadata + ))) + } + } + } + +} + +class PortableDidTest { +} \ No newline at end of file diff --git a/testing/src/main/kotlin/web5/sdk/testing/TestVectors.kt b/testing/src/main/kotlin/web5/sdk/testing/TestVectors.kt index a07d5b15d..b9be41b36 100644 --- a/testing/src/main/kotlin/web5/sdk/testing/TestVectors.kt +++ b/testing/src/main/kotlin/web5/sdk/testing/TestVectors.kt @@ -19,5 +19,5 @@ public class TestVector( public val description: String, public val input: I, public val output: O?, - public val errors: Boolean?, + public val errors: Boolean? = false, ) \ No newline at end of file From c5bba53b252324e975975f7c6f72edd4647f827c Mon Sep 17 00:00:00 2001 From: Jiyoon Koo Date: Wed, 27 Mar 2024 00:26:37 -0400 Subject: [PATCH 45/47] adding comment about supporting keys that use ECC --- crypto/src/main/kotlin/web5/sdk/crypto/Crypto.kt | 1 + web5-spec | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/crypto/src/main/kotlin/web5/sdk/crypto/Crypto.kt b/crypto/src/main/kotlin/web5/sdk/crypto/Crypto.kt index e8d643f05..8baa4d1be 100644 --- a/crypto/src/main/kotlin/web5/sdk/crypto/Crypto.kt +++ b/crypto/src/main/kotlin/web5/sdk/crypto/Crypto.kt @@ -161,6 +161,7 @@ public object Crypto { /** * Retrieves a [KeyGenerator] based on the provided algorithmId. + * Currently, we provide key generators for keys that use ECC (see [AlgorithmId] enum) * * This function looks up and retrieves the relevant [KeyGenerator] based on the provided * algorithmId. diff --git a/web5-spec b/web5-spec index 4787b82cc..3688a5dc4 160000 --- a/web5-spec +++ b/web5-spec @@ -1 +1 @@ -Subproject commit 4787b82cc6b71032529408e3c9336f85a81d925b +Subproject commit 3688a5dc4cce5f6e587f1c14edfda4403ab99f3b From 1d7e0a5114031dc7255f164756cbd4cabf992ae1 Mon Sep 17 00:00:00 2001 From: Jiyoon Koo Date: Wed, 27 Mar 2024 12:34:41 -0400 Subject: [PATCH 46/47] reverting back to include portabledid concept in vc create test vector and the corresponding test --- .../web5/sdk/credentials/VerifiableCredentialTest.kt | 11 ++--------- web5-spec | 2 +- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt index c041e50c9..be5bf5cab 100644 --- a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt +++ b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt @@ -253,9 +253,7 @@ class VerifiableCredentialTest { class Web5TestVectorsCredentials { data class CreateTestInput( - val uri: String?, - val privateKeys: List?, - val document: DidDocument?, + val signerPortableDid: PortableDid?, val credential: Map?, ) @@ -272,12 +270,7 @@ class Web5TestVectorsCredentials { testVectors.vectors.filter { it.errors == false }.forEach { vector -> val vc = VerifiableCredential.fromJson(mapper.writeValueAsString(vector.input.credential)) - val portableDid = PortableDid( - vector.input.uri!!, - vector.input.privateKeys!!, - vector.input.document!!, - mapOf() - ) + val portableDid = Json.parse(Json.stringify(vector.input.signerPortableDid!!)) val keyManager = InMemoryKeyManager() val bearerDid = BearerDid.import(portableDid, keyManager) diff --git a/web5-spec b/web5-spec index 3688a5dc4..a67a04439 160000 --- a/web5-spec +++ b/web5-spec @@ -1 +1 @@ -Subproject commit 3688a5dc4cce5f6e587f1c14edfda4403ab99f3b +Subproject commit a67a044399227aa94a3532138a51458d194e58d2 From 870cc12cccc772f50c07ae5bab9b58b6a4dbc28b Mon Sep 17 00:00:00 2001 From: Jiyoon Koo Date: Wed, 27 Mar 2024 12:45:48 -0400 Subject: [PATCH 47/47] bumping web5-spec commit --- web5-spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web5-spec b/web5-spec index a67a04439..1f0c51f52 160000 --- a/web5-spec +++ b/web5-spec @@ -1 +1 @@ -Subproject commit a67a044399227aa94a3532138a51458d194e58d2 +Subproject commit 1f0c51f52e27bde2947aadadd5d6e10238c8698e