163 lines
3.8 KiB
Go
163 lines
3.8 KiB
Go
package did
|
|
|
|
import (
|
|
"fmt"
|
|
"net/url"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
// Specifications:
|
|
// - https://www.w3.org/TR/did-1.0/
|
|
// - https://www.w3.org/TR/did-1.1/
|
|
|
|
const JsonLdContext = "https://www.w3.org/ns/did/v1"
|
|
|
|
// Decoder is a function decoding a DID string representation ("did:example:foo") into a DID.
|
|
type Decoder func(didStr string) (DID, error)
|
|
|
|
// RegisterMethod registers a DID decoder for a given DID method.
|
|
// Method must be the DID method (for example, "key" in did:key).
|
|
func RegisterMethod(method string, decoder Decoder) {
|
|
decodersMu.Lock()
|
|
defer decodersMu.Unlock()
|
|
if !checkMethod(method) {
|
|
panic("invalid method")
|
|
}
|
|
if decoders[method] != nil {
|
|
panic("did decoder already registered")
|
|
}
|
|
decoders[method] = decoder
|
|
}
|
|
|
|
// Parse attempts to decode a DID from its string representation.
|
|
func Parse(didStr string) (DID, error) {
|
|
decodersMu.RLock()
|
|
defer decodersMu.RUnlock()
|
|
|
|
if !strings.HasPrefix(didStr, "did:") {
|
|
return nil, fmt.Errorf("%w: must start with \"did:\"", ErrInvalidDid)
|
|
}
|
|
|
|
method, suffix, ok := strings.Cut(didStr[len("did:"):], ":")
|
|
if !ok {
|
|
return nil, fmt.Errorf("%w: must have a method and an identifier", ErrInvalidDid)
|
|
}
|
|
|
|
if !checkMethodSpecificId(suffix) {
|
|
return nil, fmt.Errorf("%w: invalid identifier characters", ErrInvalidDid)
|
|
}
|
|
|
|
decoder, ok := decoders[method]
|
|
if !ok {
|
|
return nil, fmt.Errorf("%w: %s", ErrMethodNotSupported, method)
|
|
}
|
|
|
|
return decoder(didStr)
|
|
}
|
|
|
|
// MustParse is like Parse but panics instead of returning an error.
|
|
func MustParse(didStr string) DID {
|
|
did, err := Parse(didStr)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return did
|
|
}
|
|
|
|
// HasValidDIDSyntax tells if the given string representation conforms to DID syntax.
|
|
// This does NOT verify that the method is supported by this library.
|
|
func HasValidDIDSyntax(didStr string) bool {
|
|
if !strings.HasPrefix(didStr, "did:") {
|
|
return false
|
|
}
|
|
method, suffix, ok := strings.Cut(didStr[len("did:"):], ":")
|
|
if !ok {
|
|
return false
|
|
}
|
|
return checkMethod(method) && checkMethodSpecificId(suffix)
|
|
}
|
|
|
|
func checkMethod(method string) bool {
|
|
if len(method) == 0 {
|
|
return false
|
|
}
|
|
for _, char := range method {
|
|
isAlpha := 'a' <= char && char <= 'z'
|
|
isDigit := '0' <= char && char <= '9'
|
|
if !isAlpha && !isDigit {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func checkMethodSpecificId(suffix string) bool {
|
|
if len(suffix) == 0 {
|
|
return false
|
|
}
|
|
|
|
segments := strings.Split(suffix, ":")
|
|
for i, segment := range segments {
|
|
if i == len(segments)-1 && len(segment) == 0 {
|
|
// last segment can't be empty
|
|
return false
|
|
}
|
|
var percentExpected int
|
|
for _, char := range segment {
|
|
if percentExpected > 0 {
|
|
switch {
|
|
case char >= '0' && char <= '9':
|
|
percentExpected--
|
|
case char >= 'a' && char <= 'f':
|
|
percentExpected--
|
|
case char >= 'A' && char <= 'F':
|
|
percentExpected--
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
switch {
|
|
case char >= 'a' && char <= 'z': // nothing to do
|
|
case char >= 'A' && char <= 'Z': // nothing to do
|
|
case char >= '0' && char <= '9': // nothing to do
|
|
case char == '.': // nothing to do
|
|
case char == '-': // nothing to do
|
|
case char == '_': // nothing to do
|
|
case char == '%':
|
|
percentExpected = 2
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
if percentExpected > 0 {
|
|
// unfinished percent encoding
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// HasValidDidUrlSyntax tells if the given string representation conforms to DID URL syntax.
|
|
// This does NOT verify that the method is supported by this library.
|
|
func HasValidDidUrlSyntax(didUrlStr string) bool {
|
|
cutPos := strings.IndexAny(didUrlStr, "/#?")
|
|
if cutPos == -1 {
|
|
return HasValidDIDSyntax(didUrlStr)
|
|
}
|
|
|
|
base, rest := didUrlStr[:cutPos], didUrlStr[cutPos+1:]
|
|
if HasValidDIDSyntax(base) == false {
|
|
return false
|
|
}
|
|
|
|
_, err := url.Parse("example.com" + rest)
|
|
return err == nil
|
|
}
|
|
|
|
var (
|
|
decodersMu sync.RWMutex
|
|
decoders = map[string]Decoder{}
|
|
)
|