Skip to content
This repository was archived by the owner on Dec 12, 2024. It is now read-only.

Commit 4030b76

Browse files
fix RegistryDidResolver issues (#19)
1 parent 4d727ed commit 4030b76

File tree

3 files changed

+238
-58
lines changed

3 files changed

+238
-58
lines changed

src/main/kotlin/xyz/block/dap/DapResolver.kt

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,34 @@ package xyz.block.dap
22

33
import xyz.block.moneyaddress.MoneyAddress
44

5-
// This implements the DAP resolution process
6-
// See the [DAP spec](https://github.com/TBD54566975/dap#resolution)
5+
/**
6+
* Resolves a Decentralized Agnostic Paytag (DAP).
7+
* See the [DAP spec](https://github.com/TBD54566975/dap#resolution) for the resolution process.
8+
*
9+
* This wires together the RegistryResolver, RegistryDidResolver, and MoneyAddressResolver.
10+
* The RegistryDidResolver will use the default configuration unless an instance is provided that
11+
* is constructed with a block configuration override.
12+
*/
713
class DapResolver(
8-
private val registryResolver: RegistryResolver = RegistryResolver(),
9-
private val registryDidResolver: RegistryDidResolver = RegistryDidResolver {},
10-
private val moneyAddressResolver: MoneyAddressResolver = MoneyAddressResolver()
14+
private val registryDidResolver: RegistryDidResolver = RegistryDidResolver(),
1115
) {
16+
private val registryResolver: RegistryResolver = RegistryResolver()
17+
private val moneyAddressResolver: MoneyAddressResolver = MoneyAddressResolver()
18+
19+
/**
20+
* Resolves the money addresses for a DAP.
21+
* This does NOT verify the proof of the DID returned by the registry.
22+
*
23+
* @param dap the DAP to resolve
24+
* @return the list of money addresses for the DAP
25+
*/
1226
fun resolveMoneyAddresses(dap: Dap): List<MoneyAddress> {
1327
val registryUrl = registryResolver.resolveRegistryUrl(dap)
14-
val did = registryDidResolver.getDid(registryUrl, dap)
28+
val did = registryDidResolver.getUnprovenDid(registryUrl, dap)
1529
val moneyAddresses = moneyAddressResolver.resolveMoneyAddresses(did)
1630
return moneyAddresses
1731
}
32+
33+
// TODO - either use the registryDidResolver.getProvenDid above, or add a method
34+
// `resolveProvenMoneyAddresses` that verifies the proof of the DID returned by the registry
1835
}

src/main/kotlin/xyz/block/dap/RegistryDidResolver.kt

Lines changed: 107 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package xyz.block.dap
22

3-
import com.fasterxml.jackson.annotation.JsonIgnore
3+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
44
import io.ktor.client.HttpClient
55
import io.ktor.client.engine.HttpClientEngine
66
import io.ktor.client.engine.okhttp.OkHttp
@@ -24,38 +24,66 @@ import java.net.InetAddress
2424
import java.net.URL
2525
import java.net.UnknownHostException
2626

27-
// This implements part of the DAP resolution process.
28-
// See [Resolver] for the full resolution process.
29-
// See the [DAP spec](https://github.com/TBD54566975/dap#resolution)
30-
class RegistryDidResolver(
31-
configuration: RegistryDidResolverConfiguration
32-
) {
33-
private val engine: HttpClientEngine = configuration.engine ?: OkHttp.create {
34-
val appCache = Cache(File("cacheDir", "okhttpcache"), 10 * 1024 * 1024)
35-
val bootstrapClient = OkHttpClient.Builder().cache(appCache).build()
27+
// This uses a modified version of the scheme used in web5-kt to provide customization
28+
// of the configuration (see `DidWeb`).
29+
// The difference is that we provide two overloaded functions for constructing the resolver,
30+
// which look like two overloaded constructors to the caller.
31+
// This is instead of a single function and the use of the `Default` companion object.
32+
// We also don't need to expose both a `RegistryDidResolver` and `RegistryDidResolverApi`.
3633

37-
val dns = DnsOverHttps.Builder()
38-
.client(bootstrapClient)
39-
.url("https://dns.quad9.net/dns-query".toHttpUrl())
40-
.bootstrapDnsHosts(
41-
InetAddress.getByName("9.9.9.9"),
42-
InetAddress.getByName("149.112.112.112")
43-
)
44-
.build()
34+
/**
35+
* A RegistryDidResolver with the default configuration.
36+
*/
37+
fun RegistryDidResolver(): RegistryDidResolver = RegistryDidResolver.default
4538

46-
val client = bootstrapClient.newBuilder().dns(dns).build()
47-
preconfigured = client
48-
}
39+
/**
40+
* Constructs a RegistryDidResolver with the block configuration applied.
41+
*/
42+
fun RegistryDidResolver(
43+
blockConfiguration: RegistryDidResolverConfiguration.() -> Unit
44+
): RegistryDidResolver {
45+
val config = RegistryDidResolverConfiguration().apply(blockConfiguration)
46+
return RegistryDidResolverImpl(config)
47+
}
4948

50-
private val client = HttpClient(engine) {
51-
install(ContentNegotiation) {
52-
jackson { mapper }
49+
// This allows the RegistryDidResolver to be sealed
50+
private class RegistryDidResolverImpl(
51+
configuration: RegistryDidResolverConfiguration
52+
) : RegistryDidResolver(configuration)
53+
54+
/**
55+
* Given the URL for the DAP registry and a DAP, resolves the DID using the registry.
56+
*/
57+
sealed class RegistryDidResolver(
58+
configuration: RegistryDidResolverConfiguration
59+
) {
60+
companion object {
61+
/**
62+
* A singleton RegistryDidResolver with the default configuration
63+
*/
64+
internal val default: RegistryDidResolver by lazy {
65+
RegistryDidResolverImpl(RegistryDidResolverConfiguration())
5366
}
5467
}
5568

56-
private val mapper = Json.jsonMapper
69+
/**
70+
* Resolves the DAP using the registry, to retrieve the DID.
71+
* Does NOT verify Any proof in the response from the registry.
72+
*/
73+
fun getUnprovenDid(dapRegistryUrl: URL, dap: Dap): Did =
74+
getDidWithProof(dapRegistryUrl, dap).did
5775

58-
fun getDid(dapRegistryUrl: URL, dap: Dap): Did {
76+
/*
77+
// TODO - implement proof verification of the DID returned by the registry
78+
// TODO - What should this do if there is no proof?
79+
fun getProvableDid(dapRegistryUrl: URL, dap: Dap): Did {
80+
val didWithProof = getDidWithProof(dapRegistryUrl, dap)
81+
// TODO - verify the proof using web5-kt
82+
return didWithProof.did
83+
}
84+
*/
85+
86+
internal fun getDidWithProof(dapRegistryUrl: URL, dap: Dap): DidWithProof {
5987
val fullUrl = URL("$dapRegistryUrl/daps/${dap.handle}")
6088

6189
val resp: HttpResponse = try {
@@ -64,46 +92,86 @@ class RegistryDidResolver(
6492
contentType(ContentType.Application.Json)
6593
}
6694
}
67-
} catch (e: UnknownHostException) {
68-
throw RegistryDidResolutionException("Failed to reach DAP Registry", e)
95+
} catch (e: Throwable) {
96+
throw RegistryDidResolutionException("Error fetching DAP from registry", e)
6997
}
7098

7199
val body = runBlocking { resp.bodyAsText() }
72100
if (!resp.status.isSuccess()) {
73101
throw RegistryDidResolutionException("Failed to read from DAP registry")
74102
}
75-
val resolutionResponse = mapper.readValue(body, DapRegistryResolutionResponse::class.java)
76-
// TODO - should we verify the proof here?
103+
val resolutionResponse = try {
104+
mapper.readValue(body, DapRegistryResolutionResponse::class.java)
105+
} catch (e: Throwable) {
106+
throw RegistryDidResolutionException("Failed to parse DAP registry response", e)
107+
}
77108
if (resolutionResponse.did == null) {
78109
throw RegistryDidResolutionException("DAP registry did not return a DID")
79110
}
80111

81-
return Did.parse(resolutionResponse.did)
112+
val did = try {
113+
Did.parse(resolutionResponse.did)
114+
} catch (e: Throwable) {
115+
throw RegistryDidResolutionException("Failed to parse DID from DAP registry response", e)
116+
}
117+
return DidWithProof(did, resolutionResponse.proof)
118+
}
119+
120+
private val engine: HttpClientEngine = configuration.engine ?: OkHttp.create {
121+
val appCache = Cache(File("cacheDir", "okhttpcache"), 10 * 1024 * 1024)
122+
val bootstrapClient = OkHttpClient.Builder().cache(appCache).build()
123+
124+
val dns = DnsOverHttps.Builder()
125+
.client(bootstrapClient)
126+
.url("https://dns.quad9.net/dns-query".toHttpUrl())
127+
.bootstrapDnsHosts(
128+
InetAddress.getByName("9.9.9.9"),
129+
InetAddress.getByName("149.112.112.112")
130+
)
131+
.build()
132+
133+
val client = bootstrapClient.newBuilder().dns(dns).build()
134+
preconfigured = client
135+
}
136+
137+
private val client = HttpClient(engine) {
138+
install(ContentNegotiation) {
139+
jackson { mapper }
140+
}
82141
}
142+
143+
private val mapper = Json.jsonMapper
83144
}
84145

85-
class Proof(
146+
internal data class DidWithProof(
147+
val did: Did,
148+
val proof: Proof?
149+
)
150+
151+
@JsonIgnoreProperties(ignoreUnknown = true)
152+
data class Proof(
86153
val id: String,
87154
val handle: String,
88155
val did: String,
89156
val domain: String,
90157
val signature: String
91158
)
92159

93-
class DapRegistryResolutionResponse(
160+
@JsonIgnoreProperties(ignoreUnknown = true)
161+
data class DapRegistryResolutionResponse(
94162
val did: String?,
95-
@JsonIgnore val proof: Proof?
163+
val proof: Proof?
96164
)
97165

166+
/**
167+
* Configuration options for the [RegistryDidResolver].
168+
*
169+
* - [engine] is used to override the default ktor engine, which is [OkHttp].
170+
*/
98171
class RegistryDidResolverConfiguration internal constructor(
99172
var engine: HttpClientEngine? = null
100173
)
101174

102-
fun RegistryDidResolver(configuration: RegistryDidResolverConfiguration.() -> Unit): RegistryDidResolver {
103-
val config = RegistryDidResolverConfiguration().apply(configuration)
104-
return RegistryDidResolver(config)
105-
}
106-
107175
class RegistryDidResolutionException : Throwable {
108176
constructor(message: String, cause: Throwable?) : super(message, cause)
109177
constructor(message: String) : super(message)

src/test/kotlin/xyz/block/dap/RegistryDidResolverTest.kt

Lines changed: 108 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,38 +6,117 @@ import io.ktor.http.HttpHeaders
66
import io.ktor.http.HttpStatusCode
77
import io.ktor.http.headersOf
88
import io.ktor.utils.io.ByteReadChannel
9+
import org.junit.jupiter.api.assertThrows
910
import web5.sdk.dids.didcore.Did
1011
import java.net.URL
11-
import kotlin.test.AfterTest
12-
import kotlin.test.BeforeTest
1312
import kotlin.test.Test
1413
import kotlin.test.assertEquals
1514

1615
class RegistryDidResolverTest {
1716

18-
@BeforeTest
19-
fun beforeTest() {
20-
}
17+
private val registryDidResolver = RegistryDidResolver { engine = mockEngine() }
2118

22-
@AfterTest
23-
fun afterTest() {
19+
@Test
20+
fun testResolvingDidFromRegistryUrl() {
21+
val did = registryDidResolver.getUnprovenDid(VALID_URL, VALID_DAP)
22+
assertEquals(VALID_DID.toString(), did.toString())
2423
}
2524

2625
@Test
27-
fun testResolvingDidFromRegistryUrl() {
28-
val engine = mockEngine()
29-
val registryDidResolver = RegistryDidResolver { engine }
30-
val did = registryDidResolver.getDid(VALID_URL, VALID_DAP)
26+
fun testResolvingDidWithExtraFieldsFromRegistryUrl() {
27+
val did = registryDidResolver.getUnprovenDid(VALID_URL, VALID_DAP_WITH_EXTRA_FIELDS)
3128
assertEquals(VALID_DID.toString(), did.toString())
3229
}
3330

34-
// TODO - tests for error cases
31+
@Test
32+
fun testResolvingDidWithProofFromRegistryUrl() {
33+
val didWithProof = registryDidResolver.getDidWithProof(VALID_URL, VALID_DAP_WITH_PROOF)
34+
assertEquals(VALID_DID.toString(), didWithProof.did.toString())
35+
assertEquals(VALID_PROOF, didWithProof.proof)
36+
}
37+
38+
@Test
39+
fun testErrorsFetchingFromRegistry() {
40+
val errorDaps = listOf(
41+
UNKNOWN_DAP to "Error fetching DAP from registry",
42+
EMPTY_RESPONSE_DAP to "DAP registry did not return a DID",
43+
INVALID_RESPONSE_DAP to "Failed to parse DAP registry response",
44+
INVALID_DID_DAP to "Failed to parse DID from DAP registry response",
45+
)
46+
errorDaps.forEach { (dap, expectedMessage) ->
47+
val exception = assertThrows<RegistryDidResolutionException> {
48+
registryDidResolver.getUnprovenDid(VALID_URL, dap)
49+
}
50+
assertEquals(expectedMessage, exception.message)
51+
}
52+
}
3553

3654
private fun mockEngine() = MockEngine { request ->
3755
when (request.url.toString()) {
3856
"$VALID_URL/daps/${VALID_DAP.handle}" -> {
3957
respond(
40-
content = ByteReadChannel("""{did: "$VALID_DID"}"""),
58+
content = ByteReadChannel("""{"did": "$VALID_DID"}"""),
59+
status = HttpStatusCode.OK,
60+
headers = headersOf(HttpHeaders.ContentType, "application/json")
61+
)
62+
}
63+
64+
"$VALID_URL/daps/${VALID_DAP_WITH_PROOF.handle}" -> {
65+
respond(
66+
content = ByteReadChannel(
67+
"""
68+
{
69+
"did": "$VALID_DID",
70+
"proof": {
71+
"id": "${VALID_PROOF.id}",
72+
"handle": "${VALID_PROOF.handle}",
73+
"did": "${VALID_PROOF.did}",
74+
"domain": "${VALID_PROOF.domain}",
75+
"signature": "${VALID_PROOF.signature}",
76+
"extra-field": "extra-field"
77+
}
78+
}
79+
""".trimMargin()
80+
),
81+
status = HttpStatusCode.OK,
82+
headers = headersOf(HttpHeaders.ContentType, "application/json")
83+
)
84+
}
85+
86+
"$VALID_URL/daps/${VALID_DAP_WITH_EXTRA_FIELDS.handle}" -> {
87+
respond(
88+
content = ByteReadChannel(
89+
"""
90+
{
91+
"did": "$VALID_DID",
92+
"extra-field": "extra-field"
93+
}
94+
""".trimMargin()
95+
),
96+
status = HttpStatusCode.OK,
97+
headers = headersOf(HttpHeaders.ContentType, "application/json")
98+
)
99+
}
100+
101+
"$VALID_URL/daps/${EMPTY_RESPONSE_DAP.handle}" -> {
102+
respond(
103+
content = ByteReadChannel("""{}"""),
104+
status = HttpStatusCode.OK,
105+
headers = headersOf(HttpHeaders.ContentType, "application/json")
106+
)
107+
}
108+
109+
"$VALID_URL/daps/${INVALID_RESPONSE_DAP.handle}" -> {
110+
respond(
111+
content = ByteReadChannel("""invalid-response"""),
112+
status = HttpStatusCode.OK,
113+
headers = headersOf(HttpHeaders.ContentType, "application/json")
114+
)
115+
}
116+
117+
"$VALID_URL/daps/${INVALID_DID_DAP.handle}" -> {
118+
respond(
119+
content = ByteReadChannel("""{"did": "invalid-did"}"""),
41120
status = HttpStatusCode.OK,
42121
headers = headersOf(HttpHeaders.ContentType, "application/json")
43122
)
@@ -50,6 +129,22 @@ class RegistryDidResolverTest {
50129
companion object {
51130
val VALID_URL = URL("https://didpay.me")
52131
val VALID_DAP = Dap("moegrammer", "didpay.me")
132+
val VALID_DAP_WITH_PROOF = Dap("proof", "didpay.me")
133+
val VALID_DAP_WITH_EXTRA_FIELDS = Dap("extra-field", "didpay.me")
134+
53135
val VALID_DID = Did.parse("did:web:didpay.me:moegrammer")
136+
val VALID_PROOF = Proof(
137+
did = VALID_DID.toString(),
138+
domain = "didpay.me",
139+
handle = "moegrammer",
140+
id = "reg_01j13n944betnsesdve4ewc9c7",
141+
signature = "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6ZGlkcGF5Lm1lOm1vZWdyYW1tZXIjMCJ9.." +
142+
"jD38MMEGR4q9mtupJZ_kNEI3RoqKhhBh3fpdkRUskhLIm0Mr3-5Egm0XflSxkFxyby3Mq8DhL71QImojWJQZCw",
143+
)
144+
145+
val UNKNOWN_DAP = Dap("i-don't-know-you", "didpay.me")
146+
val EMPTY_RESPONSE_DAP = Dap("empty-response", "didpay.me")
147+
val INVALID_RESPONSE_DAP = Dap("invalid-response", "didpay.me")
148+
val INVALID_DID_DAP = Dap("invalid-did", "didpay.me")
54149
}
55150
}

0 commit comments

Comments
 (0)