Skip to content

feat(did): add did:web key resolver with SSRF protection (RFC-008 §17.1)#71

Open
beonde wants to merge 1 commit intomainfrom
feat/did-web-resolver
Open

feat(did): add did:web key resolver with SSRF protection (RFC-008 §17.1)#71
beonde wants to merge 1 commit intomainfrom
feat/did-web-resolver

Conversation

@beonde
Copy link
Copy Markdown
Member

@beonde beonde commented May 6, 2026

Summary

Implements the did:web key resolver for RFC-008 §17.1, enabling cross-organization authority chain verification. This is PR E from the RFC-008 implementation plan.

What it does

Allows the envelope verifier to resolve public keys from did:web identifiers by fetching DID documents over HTTPS. This is needed for authority chains that span multiple organizations (each org hosts their own did.json).

Security (RFC-008 §17.1 + RFC-002 SSRF)

The WebResolver includes comprehensive SSRF protection:

  • HTTPS required in production (HTTP only for tests via AllowHTTP flag)
  • IP range blocking: 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, ::1, link-local
  • Hostname blocking: localhost, metadata.google.internal, 169.254.169.254
  • No redirect following: prevents SSRF via 302 to internal IP
  • Response size limits: 64KB max (configurable)
  • Request timeouts: 10s default
  • Document caching: 5min TTL (reduces network exposure)

Files Changed

File Change
pkg/did/document.go New — DID Document, VerificationMethod, JWK types
pkg/did/web_resolver.go New — WebResolver with SSRF-safe dialer
pkg/did/web_resolver_test.go New — 16 tests
pkg/envelope/verifier.go Modified — Add NewCompositeKeyResolver()

Key Formats Supported

  • Ed25519VerificationKey2020 with publicKeyMultibase (base58btc)
  • JsonWebKey2020 with publicKeyJwk (OKP/Ed25519)

Usage

webResolver := &did.WebResolver{
    Client:     httpClient,
    MaxDocSize: 65536,
}
verifier := &envelope.Verifier{
    KeyResolver: envelope.NewCompositeKeyResolver(webResolver),
}

Tests (16 tests)

  • Basic resolve, path segment URL construction, key ID matching
  • SSRF: private IPs, localhost, metadata endpoint, HTTPS enforcement
  • Size limits, timeout, caching, multibase/JWK decoding, error cases

Dependencies

  • No new Go dependencies
  • Builds on PR D (gateway chain verification) but can merge independently

Implements WebResolver for resolving did:web identifiers by fetching
DID documents over HTTPS and extracting public keys.

New files:
- pkg/did/document.go: DID Document, VerificationMethod, JWK types
- pkg/did/web_resolver.go: WebResolver with SSRF-safe HTTP client
- pkg/did/web_resolver_test.go: 16 tests covering all security requirements

Security features (RFC-008 §17.1):
- HTTPS required in production (AllowHTTP=false by default)
- SSRF protection: blocks localhost, 127.0.0.0/8, 10.0.0.0/8,
  172.16.0.0/12, 192.168.0.0/16, link-local, metadata endpoints
- Response size limits (default 64KB)
- Request timeouts (default 10 seconds)
- No redirect following (prevents SSRF via redirect)
- Document caching with configurable TTL (default 5 minutes)

Key formats supported:
- Ed25519VerificationKey2020 (publicKeyMultibase, base58btc)
- JsonWebKey2020 (publicKeyJwk, OKP/Ed25519)

Also adds NewCompositeKeyResolver to pkg/envelope/verifier.go that
routes did:key to the local resolver and did:web to WebResolver.

Tests:
- Basic resolve with JWK document
- Path segment URL construction (did:web:example.com:agents:worker)
- Key ID fragment matching in multi-key documents
- SSRF: localhost, private IPs (10/172.16/192.168/169.254/::1)
- SSRF: blocked hostnames (metadata.google.internal)
- Oversized document rejection
- Timeout enforcement
- Cache hit verification (no second HTTP request)
- Multibase key decoding
- Invalid JWK curve rejection
- HTTPS requirement enforcement
- Composite resolver delegation
Copilot AI review requested due to automatic review settings May 6, 2026 04:37
@codecov
Copy link
Copy Markdown

codecov Bot commented May 6, 2026

Codecov Report

❌ Patch coverage is 60.11905% with 67 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
pkg/did/web_resolver.go 63.12% 39 Missing and 20 partials ⚠️
pkg/envelope/verifier.go 0.00% 8 Missing ⚠️

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds did:web key resolution support to enable envelope authority chain verification across organizations by fetching DID documents over HTTP(S), and wires it into envelope verification via a composite key resolver.

Changes:

  • Introduces minimal DID Document types (Document, VerificationMethod, JWK) used for extracting keys.
  • Adds a did.WebResolver that fetches and caches DID documents and supports Ed25519 keys via publicKeyMultibase and publicKeyJwk.
  • Adds envelope.NewCompositeKeyResolver() to resolve did:key locally and did:web via the new resolver, plus a new resolver-focused test suite.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 7 comments.

File Description
pkg/envelope/verifier.go Adds a composite key resolver to route did:key to local parsing and did:web to the web resolver.
pkg/did/document.go Defines minimal DID Document/JWK structs used by the resolver.
pkg/did/web_resolver.go Implements did:web document fetching, SSRF-related transport logic, caching, and key extraction/decoding.
pkg/did/web_resolver_test.go Adds tests covering key extraction, SSRF-related helpers, caching, and error scenarios.

Comment thread pkg/envelope/verifier.go
}
if parsed.IsKeyDID() {
return DefaultKeyResolver(ctx, didStr, kid)
}
Comment thread pkg/did/web_resolver.go
Comment on lines +105 to +107
if r.CacheTTL == 0 {
r.CacheTTL = DefaultCacheTTL
}
Comment thread pkg/did/web_resolver.go
Comment on lines +109 to +113
if r.Client != nil {
r.client = r.Client
} else {
r.client = &http.Client{
Timeout: DefaultResolveTimeout,
Comment thread pkg/did/web_resolver.go
Comment on lines +309 to +310
return dialer.DialContext(ctx, network, addr)
}
Comment thread pkg/did/web_resolver.go
Comment on lines +185 to +189
if strings.HasPrefix(kid, didStr+"#") || strings.HasPrefix(kid, "#") {
targetID = kid
} else {
targetID = didStr + "#" + kid
}
Comment on lines +48 to +76
// Setup test server
didStr := "did:web:example.com"
kid := "key-0"
pub, _, docBytes := testKeyAndDocument(t, didStr, kid)

server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/.well-known/did.json", r.URL.Path)
w.Header().Set("Content-Type", "application/did+json")
w.Write(docBytes)
}))
defer server.Close()

resolver := &WebResolver{
Client: server.Client(),
AllowHTTP: true, // test server uses HTTP
CacheTTL: time.Minute,
}
resolver.initOnce.Do(resolver.init)

// Override document URL by testing with a DID that points to our test server
// We'll directly test resolveDocument with the test server URL
ctx := context.Background()
doc, err := resolver.resolveDocument(ctx, server.URL+"/.well-known/did.json")
require.NoError(t, err)
assert.Equal(t, didStr, doc.ID)

// Extract key
key, err := resolver.extractKey(doc, didStr, kid)
require.NoError(t, err)
Comment on lines +365 to +369
// NewCompositeKeyResolver is in pkg/envelope - test the integration pattern
func NewCompositeKeyResolver(webResolver *WebResolver) func(ctx context.Context, didStr string, kid string) (interface{}, error) {
return func(ctx context.Context, didStr string, kid string) (interface{}, error) {
parsed, err := Parse(didStr)
if err != nil {
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants