Skip to content

Commit b791ce4

Browse files
committed
feat(keyresolver): add X25519 and service resolution via go-trust
- Add X25519Resolver interface for key agreement key resolution - Add ServiceResolver interface for DIDComm service endpoint discovery - Implement ResolveX25519 and ResolveService in GoTrustResolver - Delegates to go-trust via AuthZEN for network DIDs - Extracts keyAgreement and service from trust_metadata - Implement ResolveX25519 and ResolveService in LocalResolver - Supports did:peer:2 inline services (S purpose code) - Supports did:peer:2 encryption keys (E purpose code) - Fallback Ed25519→X25519 conversion for did:key - Update SmartResolver to route X25519/Service resolution - Local DIDs resolved locally, network DIDs via go-trust - Update DIDComm resolver to use new keyresolver methods - ResolveKeyAgreement now uses keyAgreement from trust_metadata - ResolveService now fully implemented via go-trust All network DID methods (did:web, did:webvh, etc.) are delegated to go-trust via AuthZEN. Only self-contained DIDs (did:key, did:jwk, did:peer) are resolved locally.
1 parent 2ea8a4b commit b791ce4

File tree

4 files changed

+652
-18
lines changed

4 files changed

+652
-18
lines changed

pkg/didcomm/resolver.go

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -87,39 +87,52 @@ type DIDCommService struct {
8787

8888
// ResolveKeyAgreement resolves key agreement keys for a DID.
8989
// These keys are used for encryption (ECDH key exchange).
90+
// For network DIDs, this delegates to go-trust via AuthZEN to extract keyAgreement keys
91+
// from the resolved DID document. For local DIDs (did:key, did:jwk, did:peer), resolution
92+
// is performed locally.
9093
func (r *Resolver) ResolveKeyAgreement(ctx context.Context, did string) ([]KeyAgreementKey, error) {
91-
// For now, we use the existing resolver to get the DID document
92-
// and extract key agreement keys from it.
93-
// This is a simplified implementation that will be expanded.
94+
// First, try to resolve X25519 key directly from keyAgreement section
95+
// This delegates to go-trust for network DIDs and uses local resolution for self-contained DIDs
96+
x25519Key, err := r.smart.ResolveX25519(did)
97+
if err == nil {
98+
return []KeyAgreementKey{
99+
{
100+
ID: did + "#key-agreement-1",
101+
Type: "X25519KeyAgreementKey2020",
102+
Controller: did,
103+
PublicKey: x25519Key,
104+
},
105+
}, nil
106+
}
94107

95-
// Try to resolve as Ed25519 first (many DIDs use Ed25519 for both signing and key agreement via conversion)
96-
edKey, err := r.smart.ResolveEd25519(did)
108+
// Fallback: try ECDSA for P-256/P-384 key agreement
109+
ecKey, err := r.smart.ResolveECDSA(did)
97110
if err == nil {
98-
// Convert Ed25519 to X25519 for key agreement
99-
x25519Key, err := ed25519PublicKeyToX25519(edKey)
111+
ecdhKey, err := ecdsaPublicKeyToECDH(ecKey)
100112
if err == nil {
101113
return []KeyAgreementKey{
102114
{
103115
ID: did + "#key-agreement-1",
104-
Type: "X25519KeyAgreementKey2020",
116+
Type: "JsonWebKey2020",
105117
Controller: did,
106-
PublicKey: x25519Key,
118+
PublicKey: ecdhKey,
107119
},
108120
}, nil
109121
}
110122
}
111123

112-
// Try ECDSA resolver for P-256/P-384 via SmartResolver's ResolveECDSA method
113-
ecKey, err := r.smart.ResolveECDSA(did)
124+
// Last resort: try Ed25519 to X25519 conversion
125+
// This works for DIDs that only have a signing key (Ed25519) and no explicit keyAgreement
126+
edKey, err := r.smart.ResolveEd25519(did)
114127
if err == nil {
115-
ecdhKey, err := ecdsaPublicKeyToECDH(ecKey)
128+
x25519Key, err := ed25519PublicKeyToX25519(edKey)
116129
if err == nil {
117130
return []KeyAgreementKey{
118131
{
119132
ID: did + "#key-agreement-1",
120-
Type: "JsonWebKey2020",
133+
Type: "X25519KeyAgreementKey2020",
121134
Controller: did,
122-
PublicKey: ecdhKey,
135+
PublicKey: x25519Key,
123136
},
124137
}, nil
125138
}
@@ -163,11 +176,20 @@ func (r *Resolver) ResolveVerification(ctx context.Context, did string) ([]Verif
163176
}
164177

165178
// ResolveService resolves DIDCommMessaging service endpoints for a DID.
179+
// For network DIDs, this delegates to go-trust via AuthZEN to extract service endpoints
180+
// from the resolved DID document. For did:peer:2, services are extracted from the inline data.
166181
func (r *Resolver) ResolveService(ctx context.Context, did string) (*DIDCommService, error) {
167-
// This requires full DID document resolution.
168-
// For now, return an error indicating service resolution is not yet implemented.
169-
// Full implementation will parse the DID document and extract DIDCommMessaging services.
170-
return nil, fmt.Errorf("%w: service resolution not yet implemented", ErrServiceNotFound)
182+
svc, err := r.smart.ResolveService(did)
183+
if err != nil {
184+
return nil, fmt.Errorf("%w: %v", ErrServiceNotFound, err)
185+
}
186+
187+
return &DIDCommService{
188+
ID: svc.ID,
189+
ServiceEndpoint: svc.ServiceEndpoint,
190+
RoutingKeys: svc.RoutingKeys,
191+
Accept: svc.Accept,
192+
}, nil
171193
}
172194

173195
// ed25519PublicKeyToX25519 converts an Ed25519 public key to X25519 for key agreement.

pkg/keyresolver/did_helpers.go

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
package keyresolver
44

55
import (
6+
"crypto/ecdh"
67
"crypto/ecdsa"
78
"crypto/ed25519"
89
"crypto/elliptic"
@@ -112,6 +113,250 @@ func ExtractECDSAFromMetadata(metadata any, verificationMethod string) (*ecdsa.P
112113
return nil, fmt.Errorf("ECDSA verification method not found: %s", verificationMethod)
113114
}
114115

116+
// ExtractX25519FromMetadata extracts an X25519 key agreement key from trust_metadata.
117+
// It looks for keys in the keyAgreement section of the DID document.
118+
func ExtractX25519FromMetadata(metadata any, did string) (*ecdh.PublicKey, error) {
119+
doc, ok := metadata.(map[string]any)
120+
if !ok {
121+
return nil, fmt.Errorf("invalid metadata format: expected map, got %T", metadata)
122+
}
123+
124+
// Get keyAgreement section
125+
keyAgreement, err := getKeyAgreementMethods(doc)
126+
if err != nil {
127+
return nil, err
128+
}
129+
130+
for _, ka := range keyAgreement {
131+
kaMap, ok := ka.(map[string]any)
132+
if !ok {
133+
continue
134+
}
135+
136+
// Try publicKeyJwk (preferred for X25519)
137+
if jwk, ok := kaMap["publicKeyJwk"].(map[string]any); ok {
138+
key, err := JWKToX25519(jwk)
139+
if err == nil {
140+
return key, nil
141+
}
142+
}
143+
144+
// Try publicKeyMultibase (X25519 multicodec is 0xec)
145+
if multibase, ok := kaMap["publicKeyMultibase"].(string); ok {
146+
key, err := decodeMultikeyX25519(multibase)
147+
if err == nil {
148+
return key, nil
149+
}
150+
}
151+
}
152+
153+
return nil, fmt.Errorf("X25519 key agreement key not found for: %s", did)
154+
}
155+
156+
// ExtractServiceFromMetadata extracts a DIDCommMessaging service from trust_metadata.
157+
func ExtractServiceFromMetadata(metadata any, did string) (*DIDCommService, error) {
158+
doc, ok := metadata.(map[string]any)
159+
if !ok {
160+
return nil, fmt.Errorf("invalid metadata format: expected map, got %T", metadata)
161+
}
162+
163+
// Get service section
164+
services, err := getServices(doc)
165+
if err != nil {
166+
return nil, err
167+
}
168+
169+
// Find DIDCommMessaging service
170+
for _, svc := range services {
171+
svcMap, ok := svc.(map[string]any)
172+
if !ok {
173+
continue
174+
}
175+
176+
svcType, _ := svcMap["type"].(string)
177+
if svcType != "DIDCommMessaging" {
178+
continue
179+
}
180+
181+
return parseServiceMap(svcMap)
182+
}
183+
184+
return nil, fmt.Errorf("DIDCommMessaging service not found for: %s", did)
185+
}
186+
187+
// getKeyAgreementMethods extracts the keyAgreement section from a DID document.
188+
func getKeyAgreementMethods(doc map[string]any) ([]any, error) {
189+
// Direct array of verification methods
190+
if kas, ok := doc["keyAgreement"].([]any); ok {
191+
return resolveKeyAgreementRefs(kas, doc)
192+
}
193+
194+
// Try as array of maps
195+
if kas, ok := doc["keyAgreement"].([]map[string]any); ok {
196+
result := make([]any, len(kas))
197+
for i, ka := range kas {
198+
result[i] = ka
199+
}
200+
return result, nil
201+
}
202+
203+
return nil, fmt.Errorf("no keyAgreement section found in metadata")
204+
}
205+
206+
// resolveKeyAgreementRefs resolves keyAgreement references to full verification methods.
207+
// keyAgreement can contain either full verification methods or string references.
208+
func resolveKeyAgreementRefs(kas []any, doc map[string]any) ([]any, error) {
209+
vms, _ := getVerificationMethods(doc)
210+
result := make([]any, 0, len(kas))
211+
212+
for _, ka := range kas {
213+
switch v := ka.(type) {
214+
case map[string]any:
215+
// Already a full verification method
216+
result = append(result, v)
217+
case string:
218+
// Reference to a verification method - resolve it
219+
for _, vm := range vms {
220+
vmMap, ok := vm.(map[string]any)
221+
if !ok {
222+
continue
223+
}
224+
if id, ok := vmMap["id"].(string); ok && (id == v || strings.HasSuffix(v, "#"+id)) {
225+
result = append(result, vmMap)
226+
break
227+
}
228+
}
229+
}
230+
}
231+
232+
if len(result) == 0 {
233+
return nil, fmt.Errorf("no keyAgreement methods resolved")
234+
}
235+
236+
return result, nil
237+
}
238+
239+
// getServices extracts the service section from a DID document.
240+
func getServices(doc map[string]any) ([]any, error) {
241+
// Direct array
242+
if svcs, ok := doc["service"].([]any); ok {
243+
return svcs, nil
244+
}
245+
246+
// Try as array of maps
247+
if svcs, ok := doc["service"].([]map[string]any); ok {
248+
result := make([]any, len(svcs))
249+
for i, svc := range svcs {
250+
result[i] = svc
251+
}
252+
return result, nil
253+
}
254+
255+
return nil, fmt.Errorf("no service section found in metadata")
256+
}
257+
258+
// parseServiceMap parses a DIDCommMessaging service entry from a map.
259+
func parseServiceMap(svcMap map[string]any) (*DIDCommService, error) {
260+
svc := &DIDCommService{}
261+
262+
// ID
263+
if id, ok := svcMap["id"].(string); ok {
264+
svc.ID = id
265+
}
266+
267+
// ServiceEndpoint can be string, array, or object
268+
switch ep := svcMap["serviceEndpoint"].(type) {
269+
case string:
270+
svc.ServiceEndpoint = ep
271+
case []any:
272+
if len(ep) > 0 {
273+
if s, ok := ep[0].(string); ok {
274+
svc.ServiceEndpoint = s
275+
} else if obj, ok := ep[0].(map[string]any); ok {
276+
if uri, ok := obj["uri"].(string); ok {
277+
svc.ServiceEndpoint = uri
278+
}
279+
}
280+
}
281+
case map[string]any:
282+
if uri, ok := ep["uri"].(string); ok {
283+
svc.ServiceEndpoint = uri
284+
}
285+
}
286+
287+
// RoutingKeys
288+
if rks, ok := svcMap["routingKeys"].([]any); ok {
289+
for _, rk := range rks {
290+
if s, ok := rk.(string); ok {
291+
svc.RoutingKeys = append(svc.RoutingKeys, s)
292+
}
293+
}
294+
}
295+
296+
// Accept
297+
if accepts, ok := svcMap["accept"].([]any); ok {
298+
for _, a := range accepts {
299+
if s, ok := a.(string); ok {
300+
svc.Accept = append(svc.Accept, s)
301+
}
302+
}
303+
}
304+
305+
if svc.ServiceEndpoint == "" {
306+
return nil, fmt.Errorf("service has no endpoint")
307+
}
308+
309+
return svc, nil
310+
}
311+
312+
// JWKToX25519 extracts an X25519 public key from a JWK.
313+
func JWKToX25519(jwk map[string]any) (*ecdh.PublicKey, error) {
314+
kty, _ := jwk["kty"].(string)
315+
crv, _ := jwk["crv"].(string)
316+
317+
if kty != "OKP" || crv != "X25519" {
318+
return nil, fmt.Errorf("not an X25519 JWK: kty=%s, crv=%s", kty, crv)
319+
}
320+
321+
x, ok := jwk["x"].(string)
322+
if !ok {
323+
return nil, fmt.Errorf("missing x coordinate in X25519 JWK")
324+
}
325+
326+
pubBytes, err := base64.RawURLEncoding.DecodeString(x)
327+
if err != nil {
328+
return nil, fmt.Errorf("failed to decode x coordinate: %w", err)
329+
}
330+
331+
return ecdh.X25519().NewPublicKey(pubBytes)
332+
}
333+
334+
// decodeMultikeyX25519 decodes an X25519 key from multibase format.
335+
// X25519 multicodec is 0xec (236), varint encoded as 0xec 0x01
336+
func decodeMultikeyX25519(multikey string) (*ecdh.PublicKey, error) {
337+
if len(multikey) == 0 {
338+
return nil, fmt.Errorf("empty multikey")
339+
}
340+
341+
// Decode multibase
342+
_, decoded, err := multibase.Decode(multikey)
343+
if err != nil {
344+
return nil, fmt.Errorf("failed to decode multibase: %w", err)
345+
}
346+
347+
// Check length (2 bytes multicodec + 32 bytes key)
348+
if len(decoded) != 34 {
349+
return nil, fmt.Errorf("invalid multikey length: expected 34, got %d", len(decoded))
350+
}
351+
352+
// Check X25519 multicodec prefix (0xec, 0x01)
353+
if decoded[0] != 0xec || decoded[1] != 0x01 {
354+
return nil, fmt.Errorf("not an X25519 multikey: multicodec 0x%02x%02x", decoded[0], decoded[1])
355+
}
356+
357+
return ecdh.X25519().NewPublicKey(decoded[2:])
358+
}
359+
115360
// getVerificationMethods extracts the verification methods array from a DID document.
116361
func getVerificationMethods(doc map[string]any) ([]any, error) {
117362
// Standard DID document format

0 commit comments

Comments
 (0)