Files
did-it/verifiers/did-web/web.go

156 lines
3.4 KiB
Go

package did_web
import (
"fmt"
"io"
"net"
"net/http"
"net/url"
"regexp"
"strconv"
"strings"
"code.sonr.org/go/did-it"
"code.sonr.org/go/did-it/document"
)
// Specification: https://w3c-ccg.github.io/did-method-web/
func init() {
did.RegisterMethod("web", Decode)
}
var _ did.DID = DidWeb{}
type DidWeb struct {
msi string // method-specific identifier, i.e. "12345" in "did:web:12345"
parts []string
}
func Decode(identifier string) (did.DID, error) {
const webPrefix = "did:web:"
if !strings.HasPrefix(identifier, webPrefix) {
return nil, fmt.Errorf("%w: must start with 'did:web'", did.ErrInvalidDid)
}
msi := identifier[len(webPrefix):]
if len(msi) == 0 {
return nil, fmt.Errorf("%w: empty did:web identifier", did.ErrInvalidDid)
}
parts := strings.Split(msi, ":")
host, err := url.PathUnescape(parts[0])
if err != nil {
return nil, fmt.Errorf("%w: %w", did.ErrInvalidDid, err)
}
if !isValidHost(host) {
return nil, fmt.Errorf("%w: invalid host", did.ErrInvalidDid)
}
parts[0] = host
for i := 1; i < len(parts); i++ {
parts[i], err = url.PathUnescape(parts[i])
if err != nil {
return nil, fmt.Errorf("%w: %w", did.ErrInvalidDid, err)
}
}
return DidWeb{msi: msi, parts: parts}, nil
}
func (d DidWeb) Method() string {
return "web"
}
func (d DidWeb) Document(opts ...did.ResolutionOption) (did.Document, error) {
params := did.CollectResolutionOpts(opts)
var u string
var err error
if len(d.parts) == 1 {
u, err = url.JoinPath("https://"+d.parts[0], ".well-known/did.json")
} else {
parts := append(d.parts[1:], "did.json")
u, err = url.JoinPath("https://"+d.parts[0], parts...)
}
if err != nil {
return nil, fmt.Errorf("%w: %w", did.ErrResolutionFailure, err)
}
req, err := http.NewRequestWithContext(params.Context(), "GET", u, nil)
if err != nil {
return nil, fmt.Errorf("%w: %w", did.ErrResolutionFailure, err)
}
req.Header.Set("User-Agent", "go-did-it")
res, err := params.HttpClient().Do(req)
if err != nil {
return nil, fmt.Errorf("%w: %w", did.ErrResolutionFailure, err)
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%w: HTTP %d", did.ErrResolutionFailure, res.StatusCode)
}
// limit at 1MB to avoid abuse
limiter := io.LimitReader(res.Body, 1<<20)
doc, err := document.FromJsonReader(limiter)
if err != nil {
return nil, fmt.Errorf("%w: %w", did.ErrResolutionFailure, err)
}
if doc.ID() != d.String() {
return nil, fmt.Errorf("%w: did:web identifier mismatch", did.ErrResolutionFailure)
}
return doc, nil
}
func (d DidWeb) String() string {
return fmt.Sprintf("did:web:%s", d.msi)
}
func (d DidWeb) ResolutionIsExpensive() bool {
// requires an external HTTP request
return true
}
func (d DidWeb) Equal(d2 did.DID) bool {
if d2, ok := d2.(DidWeb); ok {
return d.msi == d2.msi
}
if d2, ok := d2.(*DidWeb); ok {
return d.msi == d2.msi
}
return false
}
var domainRegexp = regexp.MustCompile(`^(?i)[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+\.?$`)
func isValidHost(host string) bool {
h, port, err := net.SplitHostPort(host)
if err == nil {
portInt, err := strconv.Atoi(port)
if err != nil {
return false
}
if portInt < 0 || portInt > 65535 {
return false
}
host = h
}
if !domainRegexp.MatchString(host) {
return false
}
if ip := net.ParseIP(host); ip != nil {
// disallow IP addresses
return false
}
return true
}