Skip to content

Commit

Permalink
did:web support (#2382)
Browse files Browse the repository at this point in the history
  • Loading branch information
reinkrul authored Aug 11, 2023
1 parent ca88707 commit e167fd3
Show file tree
Hide file tree
Showing 7 changed files with 519 additions and 0 deletions.
9 changes: 9 additions & 0 deletions vdr/didweb/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Test files
The standard Go HTTPS testserver's TLS certificate is valid for 127.0.0.1, not localhost.
But did:web DIDs can't contain an IP address, so we need a certificate for localhost. This is found in cert.pem and key.pem.

`cert.pem` and `key.pem` were generated using (given GOROOT `/usr/local/go`):

```shell
go run /usr/local/go/src/crypto/tls/generate_cert.go --rsa-bits 2048 --host 127.0.0.1,::1,localhost,example.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h
```
20 changes: 20 additions & 0 deletions vdr/didweb/cert.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDRTCCAi2gAwIBAgIRAMdZIyG37eMn/3IHRJ6pBCswDQYJKoZIhvcNAQELBQAw
EjEQMA4GA1UEChMHQWNtZSBDbzAgFw03MDAxMDEwMDAwMDBaGA8yMDg0MDEyOTE2
MDAwMFowEjEQMA4GA1UEChMHQWNtZSBDbzCCASIwDQYJKoZIhvcNAQEBBQADggEP
ADCCAQoCggEBAMV0fZSX0bmuKGG3rQjHzwRwChlvJbKywGd7JiUFHPfBu0MDTU4K
iZrqZlEuUt8utIq0ZmjafxDfOpIsymMqjT+TAY7IXGLC/KvDnV7amKnXgdQM4AAC
nmyhYlx2VpIIlghvoy8onLIgkVt4Jfze/YRzCReVixrMe2tPLQ3j1a4L8vrFRiGf
jmt5GmwF/Be70a+DsVyqIVmgzevAVA0rnOKXO6vtXl95fVcVbjHcNvn3lq1uPJLQ
3sYD+vBaIPj9GRBwLqXakWkflYI0yHUmvMpVqdAZs3b2zcUltyGRJYaU3wSYCOIo
omko8clhE1LY59HvuaGDn1Tt2MemXqefePkCAwEAAaOBkzCBkDAOBgNVHQ8BAf8E
BAMCAqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0TAQH/BAUwAwEB/zAdBgNV
HQ4EFgQUVNOaROq9mOCAci+C3AlmE2RcgVUwOQYDVR0RBDIwMIIJbG9jYWxob3N0
ggtleGFtcGxlLmNvbYcEfwAAAYcQAAAAAAAAAAAAAAAAAAAAATANBgkqhkiG9w0B
AQsFAAOCAQEArJBUPtDC9fXF7NXPr3LZIGPtB8EmpiT1oKa0lpQq3m5Th6jPuNP5
jxTsN76uQ8Zad0yXPhdCIHgntqqceezxIZ75BJoaTOfk7WNtjhpLbBX0pu103VL3
+JAqohOJjwC09HeoRXuWdyWXRwa1XqVXgyKURRGxnw72hh2tGv0ANqEXVsXyIe9h
ISgL+jx+ui9xgnObxqiy/UdxKID7gmwaRmwSiQy4nc8hBxqBGt8/Afo4tTGFwTNz
USX5e8FspLA6xoUD5rCPFnwduL8cUdLq+5fwT0b0t1tW5oOzPr07mM0OngS7c43t
/5iGk45NUAydhK6MEh7B+OIPTMJouFtU6Q==
-----END CERTIFICATE-----
28 changes: 28 additions & 0 deletions vdr/didweb/key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDFdH2Ul9G5rihh
t60Ix88EcAoZbyWyssBneyYlBRz3wbtDA01OComa6mZRLlLfLrSKtGZo2n8Q3zqS
LMpjKo0/kwGOyFxiwvyrw51e2pip14HUDOAAAp5soWJcdlaSCJYIb6MvKJyyIJFb
eCX83v2EcwkXlYsazHtrTy0N49WuC/L6xUYhn45reRpsBfwXu9Gvg7FcqiFZoM3r
wFQNK5zilzur7V5feX1XFW4x3Db595atbjyS0N7GA/rwWiD4/RkQcC6l2pFpH5WC
NMh1JrzKVanQGbN29s3FJbchkSWGlN8EmAjiKKJpKPHJYRNS2OfR77mhg59U7djH
pl6nn3j5AgMBAAECggEAINWYNGdylp/hUy6J9ZXUVPaUl1omOKsE17BgzXMmOATd
MO2Ro1KZQ0uLLCC54ycPGqmZBgKfcpzMTpZoKUlgJ5w4fBfRVRL7lUx4FNfg3w1Z
J3vkm9vToFjN1HZROwN2f3yg9Cyasfw8b6txFbW3Dplaf7N8aD5sn5GQ+mhSlhhX
WpoQZXbsJ3t/JwP9diUgYGnoIDEFOVoIakQWKNeczKdtiAERpMfStSeN4Cu1fT7d
E+SkOrz5FhGvIkfgY4tJW2lbYOW4JaRStyOurC0AZKuTVNow2FI9rb15ZzLp0KB7
VZsTw0sWatNuiia8GPcA0bLIs7wzs3+AWf6APh9mKQKBgQD7Dec7phMeEpd9dZUS
iEf/JDBfCSldeYjz3+JLYFXG1tEP0fDBVp767iR8oYpvvq6mlIy/kXbI26SMxzN6
ajQt5NCeSGIIQnMUo7N3Mmkzp0Ct+RuL4APQLHsZgdeU1i6ewaqymnINiCAp2FbI
/Oofztl9t0P/7pxfBXKgjIU29wKBgQDJWEehibS16qv+J45zxowlegefU23YccCE
9OG8QxYRVHLSjcTy92+jslHLX+RF9tM+HXHPtOKp7bC58Sw5w11DhoPTls8Y7aCz
5zv6TVGbU5EzpK/wL9jefsy+frCRZ5Jd/5fwssNeAdSlm7FNZYEt8c6gHQ+TAPOo
bDTpXF4jjwKBgQC82tSblm7DLJExG4asjkA6ump401d+rbJMYprEwQ9FqMtT70YA
6rxlX0erSYnuTa7sOMs4QKDur+u0yxT6fXILJBmbODAmrnYLjKmwfQeOh76sILyM
GFRGAXAI3BfkKsqfOmjCOlSZwVEQqWF/iGJG0z/gxkAtAr427M4x4ANGOQKBgHUG
PWPzULgnNF4dCZva+5vQqFt/NyoFO3tLhWRRraLW7YHZam45SIbhXs8Q5fGQO0kv
/fVWUiOoBf6c4TKVjUBxD2/MiIQZoTzPGjop9FOOJ6fXgXbdqHPxSPkzU1a/1v+R
TfNVQ14BPGIg8tVkOMfGcmz3VxT/CZ+LfNlhmUmbAoGBAJMzyud3SxfKnEevA2Ed
hEdEnbtdLtldSHNHBmE4+jS6TGzfUwyIbLN1UCroIDV68xFKeRhxPYV1mS/i7jxs
jI2ATKmjHD48DdDftVdfwPKvEAWFjDs+IhIe5eeSLDWQUspyOqe1NjOSWqeg4FHe
7TjqUjeYZEwmCNeqy5wZ2c/p
-----END PRIVATE KEY-----
155 changes: 155 additions & 0 deletions vdr/didweb/web.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/*
* Copyright (C) 2023 Nuts community
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/

package didweb

import (
"crypto/tls"
"errors"
"fmt"
"github.com/nuts-foundation/go-did/did"
"github.com/nuts-foundation/nuts-node/vdr/types"
"io"
"mime"
"net"
"net/http"
"net/url"
"strings"
"time"
)

// MethodName is the DID method name used by did:web
const MethodName = "web"

var _ types.DIDResolver = (*Resolver)(nil)

// Resolver is a DID resolver for the did:web method.
type Resolver struct {
HttpClient *http.Client
}

// NewResolver creates a new did:web Resolver with default TLS configuration.
func NewResolver() *Resolver {
transport := http.DefaultTransport
if httpTransport, ok := transport.(*http.Transport); ok {
// Might not be http.Transport in testing
httpTransport = httpTransport.Clone()
httpTransport.TLSClientConfig = &tls.Config{
MinVersion: tls.VersionTLS12,
}
transport = httpTransport
}
return &Resolver{
HttpClient: &http.Client{
Transport: transport,
Timeout: 5 * time.Second,
},
}
}

// Resolve implements the DIDResolver interface.
func (w Resolver) Resolve(id did.DID, _ *types.ResolveMetadata) (*did.Document, *types.DocumentMetadata, error) {
if id.Method != "web" {
return nil, nil, errors.New("DID is not did:web")
}

var baseID = id.ID
var path string
subpathIdx := strings.Index(id.ID, ":")
if subpathIdx == -1 {
path = "/.well-known/did.json"
} else {
// subpaths are encoded as / -> :
baseID = id.ID[:subpathIdx]
path = id.ID[subpathIdx:]
path = strings.ReplaceAll(path, ":", "/") + "/did.json"
// Paths can't be empty; it should not contain subsequent slashes, or end with a slash
if strings.HasSuffix(path, "/") || strings.Contains(path, "//") {
return nil, nil, fmt.Errorf("invalid did:web: contains empty path elements")
}
}

unescapedID, err := url.PathUnescape(baseID)
if err != nil {
return nil, nil, fmt.Errorf("invalid did:web: %w", err)
}
targetURL := "https://" + unescapedID + path

// Use url.Parse() to check that;
// - the DID does not contain a sneaky percent-encoded path or other illegal stuff
// - the DID does not contain an IP address
parsedURL, err := url.Parse(targetURL)
if err != nil {
// came from a DID, not sure how it could fail
return nil, nil, err
}
if parsedURL.Host != unescapedID {
return nil, nil, fmt.Errorf("invalid did:web: illegal characters in domain name")
}
parsedIP := net.ParseIP(parsedURL.Hostname())
if parsedIP != nil {
return nil, nil, fmt.Errorf("invalid did:web: ID must be a domain name, not IP address")
}

// TODO: Support DNS over HTTPS (DOH), https://www.rfc-editor.org/rfc/rfc8484
httpResponse, err := w.HttpClient.Get(targetURL)
if err != nil {
return nil, nil, fmt.Errorf("did:web HTTP error: %w", err)
}
defer httpResponse.Body.Close()
if !(httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300) {
return nil, nil, fmt.Errorf("did:web non-ok HTTP status: %s", httpResponse.Status)
}

ct, _, err := mime.ParseMediaType(httpResponse.Header.Get("Content-Type"))
if err != nil {
return nil, nil, fmt.Errorf("did:web invalid content-type: %w", err)
}
switch ct {
case "application/did+ld+json":
// We don't do JSON-LD processing, as the spec suggests we may do when encountering a JSON-LD DID document.
// Reason is we currently don't see use cases for custom JSON-LD contexts adding information (e.g. aliasing fields or values)
// to the DID document that breaks the interpretation of the DID document, when we don't actually process it as JSON-LD.
// Maybe a future use case would be defining custom verification methods (e.g. obscure key types),
// but those won't be supported out of the box by the Nuts node anyway, so no need to understand those.
fallthrough
case "application/did+json":
fallthrough
case "application/json":
// This is OK
default:
return nil, nil, fmt.Errorf("did:web unsupported content-type: %s", ct)
}

// Read document
data, err := io.ReadAll(httpResponse.Body)
if err != nil {
return nil, nil, fmt.Errorf("did:web HTTP response read error: %w", err)
}
var document did.Document
err = document.UnmarshalJSON(data)
if err != nil {
return nil, nil, fmt.Errorf("did:web JSON unmarshal error: %w", err)
}

if !document.ID.Equals(id.WithoutURL()) {
return nil, nil, fmt.Errorf("did:web document ID mismatch: %s != %s", document.ID, id.WithoutURL())
}

return &document, &types.DocumentMetadata{}, nil
}
Loading

0 comments on commit e167fd3

Please sign in to comment.