did: add and complete DID and DID-URL syntax check

This commit is contained in:
Michael Muré
2025-06-11 17:18:54 +02:00
parent be5313d5fc
commit 7fabdda104
4 changed files with 164 additions and 20 deletions

89
did.go
View File

@@ -2,6 +2,7 @@ package did
import (
"fmt"
"net/url"
"strings"
"sync"
)
@@ -9,7 +10,7 @@ import (
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(identifier string) (DID, error)
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).
@@ -26,20 +27,20 @@ func RegisterMethod(method string, decoder Decoder) {
}
// Parse attempts to decode a DID from its string representation.
func Parse(identifier string) (DID, error) {
func Parse(didStr string) (DID, error) {
decodersMu.RLock()
defer decodersMu.RUnlock()
if !strings.HasPrefix(identifier, "did:") {
if !strings.HasPrefix(didStr, "did:") {
return nil, fmt.Errorf("%w: must start with \"did:\"", ErrInvalidDid)
}
method, suffix, ok := strings.Cut(identifier[len("did:"):], ":")
method, suffix, ok := strings.Cut(didStr[len("did:"):], ":")
if !ok {
return nil, fmt.Errorf("%w: must have a method and an identifier", ErrInvalidDid)
}
if !checkSuffix(suffix) {
if !checkMethodSpecificId(suffix) {
return nil, fmt.Errorf("%w: invalid identifier characters", ErrInvalidDid)
}
@@ -48,29 +49,29 @@ func Parse(identifier string) (DID, error) {
return nil, fmt.Errorf("%w: %s", ErrMethodNotSupported, method)
}
return decoder(identifier)
return decoder(didStr)
}
// MustParse is like Parse but panics instead of returning an error.
func MustParse(identifier string) DID {
did, err := Parse(identifier)
func MustParse(didStr string) DID {
did, err := Parse(didStr)
if err != nil {
panic(err)
}
return did
}
// HasValidSyntax tells if the given string representation conforms to DID syntax.
// HasValidDIDSyntax tells if the given string representation conforms to DID syntax.
// This does NOT verify that the method is supported by this library.
func HasValidSyntax(identifier string) bool {
if !strings.HasPrefix(identifier, "did:") {
func HasValidDIDSyntax(didStr string) bool {
if !strings.HasPrefix(didStr, "did:") {
return false
}
method, suffix, ok := strings.Cut(identifier[len("did:"):], ":")
method, suffix, ok := strings.Cut(didStr[len("did:"):], ":")
if !ok {
return false
}
return checkMethod(method) && checkSuffix(suffix)
return checkMethod(method) && checkMethodSpecificId(suffix)
}
func checkMethod(method string) bool {
@@ -87,18 +88,70 @@ func checkMethod(method string) bool {
return true
}
func checkSuffix(suffix string) bool {
func checkMethodSpecificId(suffix string) bool {
if len(suffix) == 0 {
return false
}
// TODO
// for _, char := range suffix {
//
// }
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{}