Skip to content

Commit

Permalink
Generate DID URL from components (#166)
Browse files Browse the repository at this point in the history
  • Loading branch information
tomdaffurn authored Aug 10, 2024
1 parent da214f6 commit e1c708d
Show file tree
Hide file tree
Showing 6 changed files with 157 additions and 39 deletions.
30 changes: 24 additions & 6 deletions dids/did/did.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,6 @@ type DID struct {
// Spec: https://www.w3.org/TR/did-core/#did-syntax
URI string

// URL represents the DID URI + A network location identifier for a specific resource
// Spec: https://www.w3.org/TR/did-core/#did-url-syntax
URL string

// Method specifies the DID method in the URI, which indicates the underlying
// method-specific identifier scheme (e.g., jwk, dht, key, etc.).
// Spec: https://www.w3.org/TR/did-core/#method-schemes
Expand Down Expand Up @@ -48,8 +44,31 @@ type DID struct {
Fragment string
}

// URL represents the DID URI + A network location identifier for a specific resource
// Spec: https://www.w3.org/TR/did-core/#did-url-syntax
func (d DID) URL() string {
url := d.URI
if len(d.Params) > 0 {
var pairs []string
for key, value := range d.Params {
pairs = append(pairs, fmt.Sprintf("%s=%s", key, value))
}
url += ";" + strings.Join(pairs, ";")
}
if len(d.Path) > 0 {
url += "/" + d.Path
}
if len(d.Query) > 0 {
url += "?" + d.Query
}
if len(d.Fragment) > 0 {
url += "#" + d.Fragment
}
return url
}

func (d DID) String() string {
return d.URL
return d.URL()
}

// MarshalText will convert the given DID's URL into a byte array
Expand Down Expand Up @@ -114,7 +133,6 @@ func Parse(input string) (DID, error) {

did := DID{
URI: "did:" + match[1] + ":" + match[2],
URL: input,
Method: match[1],
ID: match[2],
}
Expand Down
91 changes: 59 additions & 32 deletions dids/did/did_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,16 @@ func TestParse(t *testing.T) {
output: map[string]interface{}{
"method": "example",
"id": "123456789abcdefghi",
"uri": "did:example:123456789abcdefghi",
},
},
{
input: "did:example:123456789abcdefghi;foo=bar;baz=qux",
output: map[string]interface{}{
"method": "example",
"id": "123456789abcdefghi",
"alternate": "did:example:123456789abcdefghi;baz=qux;foo=bar",
"method": "example",
"id": "123456789abcdefghi",
"uri": "did:example:123456789abcdefghi",
"params": map[string]string{
"foo": "bar",
"baz": "qux",
Expand All @@ -48,6 +51,7 @@ func TestParse(t *testing.T) {
output: map[string]interface{}{
"method": "example",
"id": "123456789abcdefghi",
"uri": "did:example:123456789abcdefghi",
"query": "foo=bar&baz=qux",
},
},
Expand All @@ -56,6 +60,7 @@ func TestParse(t *testing.T) {
output: map[string]interface{}{
"method": "example",
"id": "123456789abcdefghi",
"uri": "did:example:123456789abcdefghi",
"fragment": "keys-1",
},
},
Expand All @@ -64,62 +69,78 @@ func TestParse(t *testing.T) {
output: map[string]interface{}{
"method": "example",
"id": "123456789abcdefghi",
"uri": "did:example:123456789abcdefghi",
"query": "foo=bar&baz=qux",
"fragment": "keys-1",
},
},
{
input: "did:example:123456789abcdefghi;foo=bar;baz=qux?foo=bar&baz=qux#keys-1",
input: "did:example:123456789abcdefghi;foo=bar;baz=qux?p1=v1&p2=v2#keys-1",
output: map[string]interface{}{
"method": "example",
"id": "123456789abcdefghi",
"params": map[string]string{"foo": "bar", "baz": "qux"},
"query": "foo=bar&baz=qux",
"fragment": "keys-1",
"alternate": "did:example:123456789abcdefghi;baz=quxfoo=bar;?p1=v1&p2=v2#keys-1",
"method": "example",
"id": "123456789abcdefghi",
"uri": "did:example:123456789abcdefghi",
"params": map[string]string{"foo": "bar", "baz": "qux"},
"query": "p1=v1&p2=v2",
"fragment": "keys-1",
},
},
}

for _, v := range vectors {
did, err := did.Parse(v.input)
t.Run(v.input, func(t *testing.T) {
did, err := did.Parse(v.input)

if v.error && err == nil {
t.Errorf("expected error, got nil")
}
if v.error && err == nil {
t.Errorf("expected error, got nil")
}

if err != nil {
if !v.error {
t.Errorf("failed to parse did: %s", err.Error())
if err != nil {
if !v.error {
t.Errorf("failed to parse did: %s", err.Error())
}
return
}
continue
}

assert.Equal[interface{}](t, v.output["method"], did.Method)
assert.Equal[interface{}](t, v.output["id"], did.ID)
// The Params map doesn't have a reliable order, so check both
alt, ok := v.output["alternate"]
if ok {
firstOrder := v.input == did.URL()
secondOrder := alt == did.URL()
assert.True(t, firstOrder || secondOrder, "expected one of the orders to match")
} else {
assert.Equal[interface{}](t, v.input, did.URL())
}
assert.Equal[interface{}](t, v.output["method"], did.Method)
assert.Equal[interface{}](t, v.output["id"], did.ID)
assert.Equal[interface{}](t, v.output["uri"], did.URI)

if v.output["params"] != nil {
params, ok := v.output["params"].(map[string]string)
assert.True(t, ok, "expected params to be map[string]string")
if v.output["params"] != nil {
params, ok := v.output["params"].(map[string]string)
assert.True(t, ok, "expected params to be map[string]string")

for k, v := range params {
assert.Equal[interface{}](t, v, did.Params[k])
for k, v := range params {
assert.Equal[interface{}](t, v, did.Params[k])
}
}
}

if v.output["query"] != nil {
assert.Equal[interface{}](t, v.output["query"], did.Query)
}
if v.output["query"] != nil {
assert.Equal[interface{}](t, v.output["query"], did.Query)
}

if v.output["fragment"] != nil {
assert.Equal[interface{}](t, v.output["fragment"], did.Fragment)
}
if v.output["fragment"] != nil {
assert.Equal[interface{}](t, v.output["fragment"], did.Fragment)
}
})
}
}

func TestDID_ScanValueRoundtrip(t *testing.T) {
tests := []struct {
object did.DID
raw string
alt string
wantErr bool
}{
{
Expand All @@ -128,6 +149,7 @@ func TestDID_ScanValueRoundtrip(t *testing.T) {
},
{
raw: "did:example:123456789abcdefghi;foo=bar;baz=qux",
alt: "did:example:123456789abcdefghi;baz=qux;foo=bar",
object: did.MustParse("did:example:123456789abcdefghi;foo=bar;baz=qux"),
},
{
Expand All @@ -144,6 +166,7 @@ func TestDID_ScanValueRoundtrip(t *testing.T) {
},
{
raw: "did:example:123456789abcdefghi;foo=bar;baz=qux?foo=bar&baz=qux#keys-1",
alt: "did:example:123456789abcdefghi;baz=qux;foo=bar?foo=bar&baz=qux#keys-1",
object: did.MustParse("did:example:123456789abcdefghi;foo=bar;baz=qux?foo=bar&baz=qux#keys-1"),
},
}
Expand All @@ -159,7 +182,11 @@ func TestDID_ScanValueRoundtrip(t *testing.T) {
assert.NoError(t, err)
actual, ok := value.(string)
assert.True(t, ok)
assert.Equal(t, tt.raw, actual)
if tt.alt != "" {
assert.True(t, actual == tt.raw || actual == tt.alt)
} else {
assert.Equal(t, tt.raw, actual)
}
})
}
}
18 changes: 18 additions & 0 deletions dids/diddht/diddht_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
"github.com/tbd54566975/web5-go/dids/did"
"net/http"
"net/http/httptest"
"testing"
Expand Down Expand Up @@ -160,6 +161,23 @@ func TestDHTResolve(t *testing.T) {
assert.EqualError(t, err, test.expectedErrorMessage)

test.assertResult(t, &result.Document)

parsedDID, err := did.Parse(test.didURI)
assert.NoError(t, err)
assert.Equal(t, test.didURI, parsedDID.URI)

// String -> Parse roundtrip
stringParsedDID, err := did.Parse(parsedDID.String())
assert.NoError(t, err)
assert.Equal(t, parsedDID, stringParsedDID)

// Value -> Scan roundtrip
value, err := parsedDID.Value()
assert.NoError(t, err)
var scannedDID did.DID
err = scannedDID.Scan(value)
assert.NoError(t, err)
assert.Equal(t, parsedDID, scannedDID)
})

}
Expand Down
25 changes: 25 additions & 0 deletions dids/didjwk/didjwk_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package didjwk_test

import (
"fmt"
"github.com/tbd54566975/web5-go/dids/did"
"testing"

"github.com/alecthomas/assert/v2"
Expand All @@ -23,6 +24,30 @@ func TestCreate(t *testing.T) {
assert.Equal(t, "did:jwk:"+did.ID, did.URI)
}

func TestParse(t *testing.T) {
source, err := didjwk.Create()
assert.NoError(t, err)
original := source.DID

// URI -> Parse
parseURIDID, err := did.Parse(original.URI)
assert.NoError(t, err)
assert.Equal(t, original, parseURIDID)

// String -> Parse
parseStringDID, err := did.Parse(original.String())
assert.NoError(t, err)
assert.Equal(t, original, parseStringDID)

// Value -> Scan
var scanDID did.DID
value, err := original.Value()
assert.NoError(t, err)
err = scanDID.Scan(value)
assert.NoError(t, err)
assert.Equal(t, original, scanDID)
}

func TestResolveDIDJWK(t *testing.T) {
resolver := &didjwk.Resolver{}
result, err := resolver.Resolve("did:jwk:eyJraWQiOiJ1cm46aWV0ZjpwYXJhbXM6b2F1dGg6andrLXRodW1icHJpbnQ6c2hhLTI1NjpGZk1iek9qTW1RNGVmVDZrdndUSUpqZWxUcWpsMHhqRUlXUTJxb2JzUk1NIiwia3R5IjoiT0tQIiwiY3J2IjoiRWQyNTUxOSIsImFsZyI6IkVkRFNBIiwieCI6IkFOUmpIX3p4Y0tCeHNqUlBVdHpSYnA3RlNWTEtKWFE5QVBYOU1QMWo3azQifQ")
Expand Down
30 changes: 30 additions & 0 deletions dids/didweb/didweb_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,36 @@ func TestCreate(t *testing.T) {
document := bearerDID.Document
assert.Equal(t, "did:web:localhost%3A8080", document.ID)
assert.Equal(t, 1, len(document.VerificationMethod))

did := bearerDID.DID
assert.Equal(t, "web", did.Method)
assert.Equal(t, "localhost%3A8080", did.ID)
assert.Equal(t, "did:web:localhost%3A8080", did.URI)
assert.Equal(t, "did:web:localhost%3A8080", did.URL())
}

func TestParse(t *testing.T) {
bearerDID, err := didweb.Create("localhost:8080")
assert.NoError(t, err)
original := bearerDID.DID

// URI -> Parse
parseURIDID, err := did.Parse(original.URI)
assert.NoError(t, err)
assert.Equal(t, original, parseURIDID)

// String -> Parse
parseStringDID, err := did.Parse(original.String())
assert.NoError(t, err)
assert.Equal(t, original, parseStringDID)

// Value -> Scan
var scanDID did.DID
value, err := original.Value()
assert.NoError(t, err)
err = scanDID.Scan(value)
assert.NoError(t, err)
assert.Equal(t, original, scanDID)
}

func TestCreate_WithOptions(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion jws/jws.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ func (jws Decoded) Verify() error {
return fmt.Errorf("failed to resolve DID: %w", err)
}

vmSelector := didcore.ID(did.URL)
vmSelector := didcore.ID(did.URL())
verificationMethod, err := resolutionResult.Document.SelectVerificationMethod(vmSelector)
if err != nil {
return fmt.Errorf("kid does not match any verification method %w", err)
Expand Down

0 comments on commit e1c708d

Please sign in to comment.