Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Incorporate did_web test vectors #172

Merged
merged 35 commits into from
Jan 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
dfc5507
adding satisfy pd
nitro-neal Nov 20, 2023
744dad6
new documentation
nitro-neal Nov 20, 2023
5e1c646
add doc
nitro-neal Nov 20, 2023
7067c25
adding to support filter array on single path
nitro-neal Nov 20, 2023
ee106e0
adding createPresentationFromCredentials
nitro-neal Nov 30, 2023
2d6eedc
merge
nitro-neal Nov 30, 2023
74cd5e1
Update credentials/src/main/kotlin/web5/sdk/credentials/PresentationE…
nitro-neal Dec 5, 2023
66e9e6f
Update credentials/src/main/kotlin/web5/sdk/credentials/model/Present…
nitro-neal Dec 5, 2023
6734551
Update credentials/src/main/kotlin/web5/sdk/credentials/model/Present…
nitro-neal Dec 5, 2023
c8380f7
updates
nitro-neal Dec 5, 2023
61279a2
Merge branch 'main' into validate-def
nitro-neal Dec 5, 2023
e095bc9
Merge branch 'main' into validate-def
nitro-neal Dec 6, 2023
c573e36
adding select credentials and test vectors
nitro-neal Dec 7, 2023
d8ecb4a
fix
nitro-neal Dec 7, 2023
5bc317a
added underscore
nitro-neal Dec 8, 2023
ff56c35
merge
nitro-neal Dec 11, 2023
3b74af4
updates
nitro-neal Dec 11, 2023
40b69f9
uypdate
nitro-neal Dec 11, 2023
f3f0ac5
merge
nitro-neal Dec 12, 2023
c8a30c0
merge and updates
nitro-neal Dec 12, 2023
77d0118
clean up
nitro-neal Dec 12, 2023
ee432c4
Merge remote-tracking branch 'origin/pex-select-creds' into did_web_t…
andresuribe87 Dec 14, 2023
7c5ee35
Use the did_web test vector.
andresuribe87 Dec 14, 2023
c9f39d0
Post merge
andresuribe87 Dec 20, 2023
29b1226
Post post merge
andresuribe87 Dec 20, 2023
7abe689
DRY
andresuribe87 Dec 20, 2023
c4fe9d7
Warnings
andresuribe87 Dec 20, 2023
c18d108
Rename
andresuribe87 Dec 20, 2023
399ef24
hash
andresuribe87 Dec 21, 2023
2783f68
Merge branch 'main' into did_web_test_vectors
andresuribe87 Dec 27, 2023
32ba68a
Merge branch 'main' into did_web_test_vectors
andresuribe87 Jan 12, 2024
7e699a3
Internal error for other exceptions.
andresuribe87 Jan 12, 2024
1b510a7
Make didDoc default to null.
andresuribe87 Jan 12, 2024
cb911e9
rename file
andresuribe87 Jan 18, 2024
e2adca5
Merge from main
andresuribe87 Jan 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 32 additions & 12 deletions credentials/src/main/kotlin/web5/sdk/credentials/util/JwtUtil.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ import web5.sdk.common.Convert
import web5.sdk.crypto.Crypto
import web5.sdk.dids.Did
import web5.sdk.dids.DidResolvers
import web5.sdk.dids.exceptions.DidResolutionException
import web5.sdk.dids.findAssertionMethodById
import java.security.SignatureException

private const val JsonWebKey2020 = "JsonWebKey2020"

/**
* Util class for common shared JWT methods.
*/
Expand All @@ -41,7 +43,16 @@ public object JwtUtil {
*/
public fun sign(did: Did, assertionMethodId: String?, jwtPayload: JWTClaimsSet): String {
val didResolutionResult = DidResolvers.resolve(did.uri)
val assertionMethod = didResolutionResult.didDocument.findAssertionMethodById(assertionMethodId)
val didDocument = didResolutionResult.didDocument
if (didResolutionResult.didResolutionMetadata.error != null || didDocument == null) {
throw DidResolutionException(
"Signature verification failed: " +
"Failed to resolve DID ${did.uri}. " +
"Error: ${didResolutionResult.didResolutionMetadata.error}"
)
}

val assertionMethod = didDocument.findAssertionMethodById(assertionMethodId)

// TODO: ensure that publicKeyJwk is not null
val publicKeyJwk = JWK.parse(assertionMethod.publicKeyJwk)
Expand Down Expand Up @@ -100,29 +111,38 @@ public object JwtUtil {
val parsedDidUrl = DIDURL.fromString(verificationMethodId) // validates vm id which is a DID URL

val didResolutionResult = DidResolvers.resolve(parsedDidUrl.did.didString)
if (didResolutionResult.didResolutionMetadata?.error != null) {
if (didResolutionResult.didResolutionMetadata.error != null) {
throw SignatureException(
"Signature verification failed: " +
"Failed to resolve DID ${parsedDidUrl.did.didString}. " +
"Error: ${didResolutionResult.didResolutionMetadata?.error}"
"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(parsedDidUrl.didUrlString, "#${parsedDidUrl.fragment}")
val assertionMethods = didResolutionResult.didDocument.assertionMethodVerificationMethodsDereferenced
val assertionMethods = didResolutionResult.didDocument?.assertionMethodVerificationMethodsDereferenced
val assertionMethod = assertionMethods?.firstOrNull {
it.id.toString() in verificationMethodIds
} ?: throw SignatureException(
"Signature verification failed: Expected kid in JWS header to dereference " +
"a DID Document Verification Method with an Assertion verification relationship"
)
val id = it.id.toString()
verificationMethodIds.contains(id)
}
?: throw SignatureException(
"Signature verification failed: Expected kid in JWS header to dereference " +
"a DID Document Verification Method with an Assertion verification relationship"
)

require(assertionMethod.isType(JsonWebKey2020) && assertionMethod.publicKeyJwk != null) {
"Signature verification failed: Expected kid in JWS header to dereference " +
"a DID Document Verification Method of type JsonWebKey2020 with a publicKeyJwk"
throw SignatureException(
"Signature verification failed: Expected kid in JWS header to dereference " +
"a DID Document Verification Method of type $JsonWebKey2020 with a publicKeyJwk"
)
}

val publicKeyJwk = JWK.parse(assertionMethod.publicKeyJwk)
val publicKeyMap = assertionMethod.publicKeyJwk
val publicKeyJwk = JWK.parse(publicKeyMap)

val toVerifyBytes = jwt.signingInput
val signatureBytes = jwt.signature.decode()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -452,8 +452,8 @@ class PresentationExchangeTest {

val selectedCreds = PresentationExchange.selectCredentials(listOf(vcJwt), pd)

assertEquals( 1, selectedCreds.size)
Copy link
Contributor

Choose a reason for hiding this comment

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

err I wonder why my formatter is doing this.

for web5-js we have a prettier task - TBD54566975/web5-js#266

should do something similar across all our repos so formatting stays in sync

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Created #178

Copy link
Contributor

Choose a reason for hiding this comment

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

weird, thought this would have been taken care of by ktlint/detekt

assertEquals( vcJwt, selectedCreds[0])
assertEquals(1, selectedCreds.size)
assertEquals(vcJwt, selectedCreds[0])
}

@Test
Expand Down Expand Up @@ -481,8 +481,8 @@ class PresentationExchangeTest {

val selectedCreds = PresentationExchange.selectCredentials(listOf(vcJwt1, vcJwt2), pd)

assertEquals( 2, selectedCreds.size)
assertEquals( listOf(vcJwt1, vcJwt2), selectedCreds)
assertEquals(2, selectedCreds.size)
assertEquals(listOf(vcJwt1, vcJwt2), selectedCreds)
}

@Test
Expand Down Expand Up @@ -520,8 +520,8 @@ class PresentationExchangeTest {

val selectedCreds = PresentationExchange.selectCredentials(listOf(vcJwt1, vcJwt2, vcJwt3), pd)

assertEquals( 2, selectedCreds.size)
assertEquals( listOf(vcJwt1, vcJwt2), selectedCreds)
assertEquals(2, selectedCreds.size)
assertEquals(listOf(vcJwt1, vcJwt2), selectedCreds)
}

@Test
Expand Down Expand Up @@ -549,8 +549,8 @@ class PresentationExchangeTest {

val selectedCreds = PresentationExchange.selectCredentials(listOf(vcJwt1, vcJwt2), pd)

assertEquals( 2, selectedCreds.size)
assertEquals( listOf(vcJwt1, vcJwt2), selectedCreds)
assertEquals(2, selectedCreds.size)
assertEquals(listOf(vcJwt1, vcJwt2), selectedCreds)
}

@Test
Expand Down Expand Up @@ -582,6 +582,7 @@ class Web5TestVectorsPresentationExchange {
)

private val mapper = jacksonObjectMapper()

@Test
fun select_credentials() {
val typeRef = object : TypeReference<TestVectors<SelectCredTestInput, SelectCredTestOutput>>() {}
Expand Down
2 changes: 2 additions & 0 deletions dids/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ dependencies {
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jackson_version")
implementation("com.nimbusds:nimbus-jose-jwt:9.34")
implementation("com.github.multiformats:java-multibase:1.1.0")
implementation("io.github.oshai:kotlin-logging-jvm:6.0.2")

implementation("io.ktor:ktor-client-core:$ktor_version")
implementation("io.ktor:ktor-client-okhttp:$ktor_version")
Expand All @@ -38,4 +39,5 @@ dependencies {
testImplementation("commons-codec:commons-codec:1.16.0")

implementation("dnsjava:dnsjava:3.5.2")
testImplementation(project(mapOf("path" to ":testing")))
Copy link
Contributor

Choose a reason for hiding this comment

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

why was this needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The testing module contains the class web5.sdk.testing.TestVectors, which is used to parse the test-vector files.

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,6 @@ class DidIntegrationTest {
@Test
fun `resolve an existing web did`() {
val did = DidWeb.resolve("did:web:www.linkedin.com")
assertEquals("did:web:www.linkedin.com", did.didDocument.id.toString())
assertEquals("did:web:www.linkedin.com", did.didDocument!!.id.toString())
}
}
2 changes: 1 addition & 1 deletion dids/src/main/kotlin/web5/sdk/dids/DidMethod.kt
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ internal fun <T : Did, O : CreateDidOptions> DidMethod<T, O>.validateKeyMaterial
}
val didResolutionResult = resolve(did)

didResolutionResult.didDocument.allVerificationMethods.forEach {
didResolutionResult.didDocument!!.allVerificationMethods.forEach {
val publicKeyJwk = JWK.parse(it.publicKeyJwk)
val keyAlias = keyManager.getDeterministicAlias(publicKeyJwk)
keyManager.getPublicKey(keyAlias)
Expand Down
35 changes: 29 additions & 6 deletions dids/src/main/kotlin/web5/sdk/dids/DidResolutionResult.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule
import foundation.identity.did.DIDDocument
import web5.sdk.dids.methods.ion.models.MetadataMethod
import java.util.Objects.hash

/**
* Represents the result of DID resolution as per the W3C DID Core specification.
Expand All @@ -18,20 +19,42 @@ import web5.sdk.dids.methods.ion.models.MetadataMethod
*/
public class DidResolutionResult(
@JsonProperty("@context")
public var context: String? = null,
public var didDocument: DIDDocument,
public var didResolutionMetadata: DidResolutionMetadata? = null,
public var didDocumentMetadata: DidDocumentMetadata? = null
public val context: String? = null,
public val didDocument: DIDDocument? = null,
public val didDocumentMetadata: DidDocumentMetadata = DidDocumentMetadata(),
public val didResolutionMetadata: DidResolutionMetadata = DidResolutionMetadata(),
) {
override fun toString(): String {
return objectMapper.writeValueAsString(this)
}

private companion object {
override fun equals(other: Any?): Boolean {
if (other is DidResolutionResult) {
return this.toString() == other.toString()
}
return false
}

override fun hashCode(): Int = hash(context, didDocument, didDocumentMetadata, didResolutionMetadata)

public companion object {
private val objectMapper: ObjectMapper = ObjectMapper().apply {
registerModule(KotlinModule.Builder().build())
setSerializationInclusion(JsonInclude.Include.NON_NULL)
}


/**
* Convenience function that creates a [DidResolutionResult] with [DidResolutionMetadata.error] populated from
* [error].
*/
public fun fromResolutionError(error: ResolutionError): DidResolutionResult {
return DidResolutionResult(
didResolutionMetadata = DidResolutionMetadata(
error = error.value
)
)
}
}
}

Expand All @@ -45,7 +68,7 @@ public class DidResolutionResult(
public class DidResolutionMetadata(
public var contentType: String? = null,
public var error: String? = null,
public var additionalProperties: MutableMap<String, Any> = mutableMapOf()
public var additionalProperties: MutableMap<String, Any>? = null,
)

/**
Expand Down
11 changes: 11 additions & 0 deletions dids/src/main/kotlin/web5/sdk/dids/ResolutionError.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package web5.sdk.dids

/**
* Represents a DID resolution error as described in https://w3c-ccg.github.io/did-resolution/#errors.
*/
public enum class ResolutionError(public val value: String) {
METHOD_NOT_SUPPORTED("methodNotSupported"),
NOT_FOUND("notFound"),
INVALID_DID("invalidDid"),
INTERNAL_ERROR("internalError"),
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,10 @@ package web5.sdk.dids.exceptions
* @param message the exception message detailing the error
*/
public class PkarrRecordResponseException(message: String) : RuntimeException(message)

/**
* Did resolution exception.
*
* @param message the exception message detailing the error
*/
public class DidResolutionException(message: String) : RuntimeException(message)
6 changes: 3 additions & 3 deletions dids/src/main/kotlin/web5/sdk/dids/methods/ion/DidIon.kt
Original file line number Diff line number Diff line change
Expand Up @@ -263,14 +263,14 @@ public sealed class DidIonApi(
val longFormDid = "$shortFormDid:$longFormDidSegment"
val resolutionResult = resolve(longFormDid)

if (!resolutionResult.didResolutionMetadata?.error.isNullOrEmpty()) {
if (!resolutionResult.didResolutionMetadata.error.isNullOrEmpty()) {
throw ResolutionException(
"error when resolving after creation: ${resolutionResult.didResolutionMetadata?.error}"
"error when resolving after creation: ${resolutionResult.didResolutionMetadata.error}"
)
}

return DidIon(
resolutionResult.didDocument.id.toString(),
resolutionResult.didDocument!!.id.toString(),
keyManager,
IonCreationMetadata(
createOp,
Expand Down
45 changes: 34 additions & 11 deletions dids/src/main/kotlin/web5/sdk/dids/methods/web/DidWeb.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,19 @@ package web5.sdk.dids.methods.web
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import foundation.identity.did.DID
import foundation.identity.did.DIDDocument
import foundation.identity.did.parser.ParserException
import io.github.oshai.kotlinlogging.KotlinLogging
import io.ktor.client.HttpClient
import io.ktor.client.engine.HttpClientEngine
import io.ktor.client.engine.okhttp.OkHttp
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.request.get
import io.ktor.client.statement.HttpResponse
import io.ktor.client.statement.bodyAsText
import io.ktor.http.ContentType
import io.ktor.http.contentType
import io.ktor.http.isSuccess
import io.ktor.serialization.jackson.jackson
import jdk.jshell.spi.ExecutionControl.NotImplementedException
import kotlinx.coroutines.runBlocking
import okhttp3.Cache
import okhttp3.HttpUrl.Companion.toHttpUrl
Expand All @@ -24,13 +26,15 @@ import web5.sdk.dids.CreateDidOptions
import web5.sdk.dids.Did
import web5.sdk.dids.DidMethod
import web5.sdk.dids.DidResolutionResult
import web5.sdk.dids.ResolutionError
import web5.sdk.dids.ResolveDidOptions
import web5.sdk.dids.methods.ion.InvalidStatusException
import web5.sdk.dids.validateKeyMaterialInsideKeyManager
import java.io.File
import java.net.InetAddress
import java.net.URL
import java.net.URLDecoder
import java.net.UnknownHostException
import kotlin.text.Charsets.UTF_8

/**
Expand Down Expand Up @@ -93,6 +97,8 @@ public sealed class DidWebApi(
configuration: DidWebApiConfiguration
) : DidMethod<DidWeb, CreateDidOptions> {

private val logger = KotlinLogging.logger {}

private val mapper = jacksonObjectMapper()

private val engine: HttpClientEngine = configuration.engine ?: OkHttp.create {
Expand All @@ -117,12 +123,34 @@ public sealed class DidWebApi(
override val methodName: String = "web"

override fun resolve(did: String, options: ResolveDidOptions?): DidResolutionResult {
val docURL = getDocURL(did)
return try {
resolveInternal(did, options)
} catch (e: Exception) {
logger.warn(e) { "resolving DID $did failed" }
DidResolutionResult.fromResolutionError(ResolutionError.INTERNAL_ERROR)
}
}

private fun resolveInternal(did: String, options: ResolveDidOptions?): DidResolutionResult {
val parsedDid = try {
DID.fromString(did)
} catch (_: ParserException) {
Copy link
Contributor

Choose a reason for hiding this comment

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

should we have another line to catch all other exceptions?

} catch (e: Exception) {

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done at the public function level.

return DidResolutionResult.fromResolutionError(ResolutionError.INVALID_DID)
}

val resp = runBlocking {
client.get(docURL) {
contentType(ContentType.Application.Json)
if (parsedDid.methodName != methodName) {
return DidResolutionResult.fromResolutionError(ResolutionError.METHOD_NOT_SUPPORTED)
}
val docURL = getDocURL(parsedDid)

val resp: HttpResponse = try {
runBlocking {
client.get(docURL) {
contentType(ContentType.Application.Json)
}
}
} catch (_: UnknownHostException) {
return DidResolutionResult.fromResolutionError(ResolutionError.NOT_FOUND)
}

val body = runBlocking { resp.bodyAsText() }
Expand All @@ -140,12 +168,7 @@ public sealed class DidWebApi(
return DidWeb(uri, keyManager, this)
}

private fun getDocURL(didWebStr: String): String {
val parsedDid = DID.fromString(didWebStr)
require(parsedDid.methodName == methodName) {
"$didWebStr is missing prefix \"did:$methodName\""
}

private fun getDocURL(parsedDid: DID): String {
val domainNameWithPath = parsedDid.methodSpecificId.replace(":", "/")
val decodedDomain = URLDecoder.decode(domainNameWithPath, UTF_8)

Expand Down
Loading
Loading