Add support for DID services and did:plc
This commit is contained in:
@@ -121,9 +121,10 @@ func main() {
|
|||||||
|
|
||||||
### Supported DID Methods
|
### Supported DID Methods
|
||||||
|
|
||||||
| Method | Status | Description |
|
| Method | Controller | Verifier | Description |
|
||||||
|-----------|--------|------------------------------------------|
|
|-----------|------------|----------|----------------------------------------------------|
|
||||||
| `did:key` | ✅ | Self-contained DIDs based on public keys |
|
| `did:key` | ✅ | ✅ | Self-contained DIDs based on public keys |
|
||||||
|
| `did:plc` | ❌ | ✅ | Bluesky's DID with rotation and a public directory |
|
||||||
|
|
||||||
### Supported Verification Method Types
|
### Supported Verification Method Types
|
||||||
|
|
||||||
|
|||||||
15
controller/did-key/key.go
Normal file
15
controller/did-key/key.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package didkeyctl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ucan-wg/go-did-it"
|
||||||
|
"github.com/ucan-wg/go-did-it/crypto"
|
||||||
|
didkey "github.com/ucan-wg/go-did-it/verifiers/did-key"
|
||||||
|
)
|
||||||
|
|
||||||
|
func FromPublicKey(pub crypto.PublicKey) did.DID {
|
||||||
|
return didkey.FromPublicKey(pub)
|
||||||
|
}
|
||||||
|
|
||||||
|
func FromPrivateKey(priv crypto.PrivateKey) did.DID {
|
||||||
|
return didkey.FromPrivateKey(priv)
|
||||||
|
}
|
||||||
4
did.go
4
did.go
@@ -7,6 +7,10 @@ import (
|
|||||||
"sync"
|
"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"
|
const JsonLdContext = "https://www.w3.org/ns/did/v1"
|
||||||
|
|
||||||
// Decoder is a function decoding a DID string representation ("did:example:foo") into a DID.
|
// Decoder is a function decoding a DID string representation ("did:example:foo") into a DID.
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ type Document struct {
|
|||||||
keyAgreement []did.VerificationMethodKeyAgreement
|
keyAgreement []did.VerificationMethodKeyAgreement
|
||||||
capabilityInvocation []did.VerificationMethodSignature
|
capabilityInvocation []did.VerificationMethodSignature
|
||||||
capabilityDelegation []did.VerificationMethodSignature
|
capabilityDelegation []did.VerificationMethodSignature
|
||||||
|
services did.Services
|
||||||
}
|
}
|
||||||
|
|
||||||
type aux struct {
|
type aux struct {
|
||||||
@@ -39,6 +40,7 @@ type aux struct {
|
|||||||
KeyAgreement []json.RawMessage `json:"keyAgreement,omitempty"`
|
KeyAgreement []json.RawMessage `json:"keyAgreement,omitempty"`
|
||||||
CapabilityInvocation []json.RawMessage `json:"capabilityInvocation,omitempty"`
|
CapabilityInvocation []json.RawMessage `json:"capabilityInvocation,omitempty"`
|
||||||
CapabilityDelegation []json.RawMessage `json:"capabilityDelegation,omitempty"`
|
CapabilityDelegation []json.RawMessage `json:"capabilityDelegation,omitempty"`
|
||||||
|
Services did.Services `json:"service,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// FromJsonReader decodes an arbitrary Json DID Document into a usable did.Document.
|
// FromJsonReader decodes an arbitrary Json DID Document into a usable did.Document.
|
||||||
@@ -69,6 +71,7 @@ func fromAux(aux *aux) (*Document, error) {
|
|||||||
res := Document{
|
res := Document{
|
||||||
context: aux.Context,
|
context: aux.Context,
|
||||||
id: aux.Id,
|
id: aux.Id,
|
||||||
|
services: aux.Services,
|
||||||
}
|
}
|
||||||
|
|
||||||
// id
|
// id
|
||||||
@@ -191,7 +194,7 @@ func resolveVerificationMethods[T did.VerificationMethod](doc *Document, msgs []
|
|||||||
func (d Document) MarshalJSON() ([]byte, error) {
|
func (d Document) MarshalJSON() ([]byte, error) {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
data := aux{Context: d.context, Id: d.id}
|
data := aux{Context: d.context, Id: d.id, Services: d.services}
|
||||||
|
|
||||||
// alsoKnownAs
|
// alsoKnownAs
|
||||||
data.AlsoKnownAs = make([]string, len(d.alsoKnownAs))
|
data.AlsoKnownAs = make([]string, len(d.alsoKnownAs))
|
||||||
@@ -309,3 +312,7 @@ func (d Document) CapabilityInvocation() []did.VerificationMethodSignature {
|
|||||||
func (d Document) CapabilityDelegation() []did.VerificationMethodSignature {
|
func (d Document) CapabilityDelegation() []did.VerificationMethodSignature {
|
||||||
return d.capabilityDelegation
|
return d.capabilityDelegation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d Document) Services() did.Services {
|
||||||
|
return d.services
|
||||||
|
}
|
||||||
|
|||||||
@@ -44,6 +44,15 @@ func TestRoundTrip(t *testing.T) {
|
|||||||
require.Equal(t, jsonwebkey.Type, doc.verificationMethods["did:example:123#NjQ6Y_ZMj6IUK_XkgCDwtKHlNTUTVjEYOWZtxhp1n-E"].Type())
|
require.Equal(t, jsonwebkey.Type, doc.verificationMethods["did:example:123#NjQ6Y_ZMj6IUK_XkgCDwtKHlNTUTVjEYOWZtxhp1n-E"].Type())
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "plc",
|
||||||
|
strDoc: plcDoc,
|
||||||
|
assertion: func(t *testing.T, doc *Document) {
|
||||||
|
require.Equal(t, "did:plc:ewvi7nxzyoun6zhxrhs64oiz", doc.ID())
|
||||||
|
require.Len(t, doc.VerificationMethods(), 1)
|
||||||
|
require.Len(t, doc.Services(), 1)
|
||||||
|
},
|
||||||
|
},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
doc, err := FromJsonBytes([]byte(tc.strDoc))
|
doc, err := FromJsonBytes([]byte(tc.strDoc))
|
||||||
@@ -198,6 +207,32 @@ const jsonWebKeyDoc = `
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const plcDoc = `{
|
||||||
|
"@context":[
|
||||||
|
"https://www.w3.org/ns/did/v1",
|
||||||
|
"https://w3id.org/security/multikey/v1"
|
||||||
|
],
|
||||||
|
"id":"did:plc:ewvi7nxzyoun6zhxrhs64oiz",
|
||||||
|
"alsoKnownAs":[
|
||||||
|
"at://atproto.com"
|
||||||
|
],
|
||||||
|
"verificationMethod":[
|
||||||
|
{
|
||||||
|
"id":"did:plc:ewvi7nxzyoun6zhxrhs64oiz#atproto",
|
||||||
|
"type":"Multikey",
|
||||||
|
"controller":"did:plc:ewvi7nxzyoun6zhxrhs64oiz",
|
||||||
|
"publicKeyMultibase":"zQ3shunBKsXixLxKtC5qeSG9E4J5RkGN57im31pcTzbNQnm5w"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"service":[
|
||||||
|
{
|
||||||
|
"id":"#atproto_pds",
|
||||||
|
"type":"AtprotoPersonalDataServer",
|
||||||
|
"serviceEndpoint":"https://enoki.us-east.host.bsky.network"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`
|
||||||
|
|
||||||
// requireDocEqual compare two DID JSON document but ignore the ordering inside arrays of VerificationMethods
|
// requireDocEqual compare two DID JSON document but ignore the ordering inside arrays of VerificationMethods
|
||||||
func requireDocEqual(t *testing.T, expected, actual string) {
|
func requireDocEqual(t *testing.T, expected, actual string) {
|
||||||
propsExpected := map[string]json.RawMessage{}
|
propsExpected := map[string]json.RawMessage{}
|
||||||
|
|||||||
@@ -16,4 +16,7 @@ var (
|
|||||||
var (
|
var (
|
||||||
// ErrNotFound indicates that the DID resolver was unable to find the DID document for the given DID.
|
// ErrNotFound indicates that the DID resolver was unable to find the DID document for the given DID.
|
||||||
ErrNotFound = fmt.Errorf("did not found")
|
ErrNotFound = fmt.Errorf("did not found")
|
||||||
|
|
||||||
|
// ErrResolutionFailure indicates that the DID resolver failed to resolve the DID, in a way that is not ErrNotFound
|
||||||
|
ErrResolutionFailure = fmt.Errorf("resolution failure")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ type Document interface {
|
|||||||
// Controllers is the set of DID that is authorized to make changes to the Document. It's often the same as ID.
|
// Controllers is the set of DID that is authorized to make changes to the Document. It's often the same as ID.
|
||||||
Controllers() []string
|
Controllers() []string
|
||||||
|
|
||||||
// AlsoKnownAs returns an optional set of URL describing ???TODO
|
// AlsoKnownAs returns an optional set of URL describing different identifier for the DID subject,
|
||||||
|
// for different purpose or different time.
|
||||||
AlsoKnownAs() []*url.URL
|
AlsoKnownAs() []*url.URL
|
||||||
|
|
||||||
// VerificationMethods returns all the VerificationMethod known in the document.
|
// VerificationMethods returns all the VerificationMethod known in the document.
|
||||||
@@ -70,8 +71,10 @@ type Document interface {
|
|||||||
// capability to another party, such as delegating the authority to access a specific HTTP API to a subordinate.
|
// capability to another party, such as delegating the authority to access a specific HTTP API to a subordinate.
|
||||||
CapabilityDelegation() []VerificationMethodSignature
|
CapabilityDelegation() []VerificationMethodSignature
|
||||||
|
|
||||||
// TODO: Service
|
// Services are means of communicating or interacting with the DID subject or associated entities
|
||||||
// https://www.w3.org/TR/did-extensions-properties/#service-types
|
// via one or more endpoints. Examples include discovery services, agent services, social networking
|
||||||
|
// services, file storage services, and verifiable credential repository services.
|
||||||
|
Services() Services
|
||||||
}
|
}
|
||||||
|
|
||||||
// VerificationMethod is a common interface for a cryptographic signature verification method.
|
// VerificationMethod is a common interface for a cryptographic signature verification method.
|
||||||
|
|||||||
40
options.go
40
options.go
@@ -1,7 +1,21 @@
|
|||||||
package did
|
package did
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
type ResolutionOpts struct {
|
type ResolutionOpts struct {
|
||||||
|
ctx context.Context
|
||||||
hintVerificationMethod []string
|
hintVerificationMethod []string
|
||||||
|
client HttpClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opts *ResolutionOpts) Context() context.Context {
|
||||||
|
if opts.ctx != nil {
|
||||||
|
return opts.ctx
|
||||||
|
}
|
||||||
|
return context.Background()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (opts *ResolutionOpts) HasVerificationMethodHint(hint string) bool {
|
func (opts *ResolutionOpts) HasVerificationMethodHint(hint string) bool {
|
||||||
@@ -13,6 +27,13 @@ func (opts *ResolutionOpts) HasVerificationMethodHint(hint string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (opts *ResolutionOpts) HttpClient() HttpClient {
|
||||||
|
if opts.client != nil {
|
||||||
|
return opts.client
|
||||||
|
}
|
||||||
|
return http.DefaultClient
|
||||||
|
}
|
||||||
|
|
||||||
func CollectResolutionOpts(opts []ResolutionOption) ResolutionOpts {
|
func CollectResolutionOpts(opts []ResolutionOption) ResolutionOpts {
|
||||||
res := ResolutionOpts{}
|
res := ResolutionOpts{}
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
@@ -23,6 +44,14 @@ func CollectResolutionOpts(opts []ResolutionOption) ResolutionOpts {
|
|||||||
|
|
||||||
type ResolutionOption func(opts *ResolutionOpts)
|
type ResolutionOption func(opts *ResolutionOpts)
|
||||||
|
|
||||||
|
// WithResolutionContext provides a go context to use for the resolution.
|
||||||
|
// This context can be used for deadline or cancellation.
|
||||||
|
func WithResolutionContext(ctx context.Context) ResolutionOption {
|
||||||
|
return func(opts *ResolutionOpts) {
|
||||||
|
opts.ctx = ctx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// WithResolutionHintVerificationMethod adds a hint for the type of verification method to be used
|
// WithResolutionHintVerificationMethod adds a hint for the type of verification method to be used
|
||||||
// when resolving and constructing the DID Document, if possible.
|
// when resolving and constructing the DID Document, if possible.
|
||||||
// Hints are expected to be VerificationMethod string types, like ed25519vm.Type.
|
// Hints are expected to be VerificationMethod string types, like ed25519vm.Type.
|
||||||
@@ -39,3 +68,14 @@ func WithResolutionHintVerificationMethod(hint string) ResolutionOption {
|
|||||||
opts.hintVerificationMethod = append(opts.hintVerificationMethod, hint)
|
opts.hintVerificationMethod = append(opts.hintVerificationMethod, hint)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type HttpClient interface {
|
||||||
|
Do(req *http.Request) (*http.Response, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithHttpClient provides an HttpClient to be used during resolution.
|
||||||
|
func WithHttpClient(client HttpClient) ResolutionOption {
|
||||||
|
return func(opts *ResolutionOpts) {
|
||||||
|
opts.client = client
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
155
service.go
Normal file
155
service.go
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
package did
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Specification: https://www.w3.org/TR/cid-1.0/#services
|
||||||
|
// List of service types and their fields: https://www.w3.org/TR/did-extensions-properties/#service-types
|
||||||
|
|
||||||
|
// Services is a collection of Service.
|
||||||
|
type Services []Service
|
||||||
|
|
||||||
|
// ServiceById retrieves a Service from the Services slice by its id.
|
||||||
|
// Returns the Service and true if found, otherwise returns an empty Service and false.
|
||||||
|
func (ss Services) ServiceById(id string) (Service, bool) {
|
||||||
|
for _, s := range ss {
|
||||||
|
if s.Id == id {
|
||||||
|
return s, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Service{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceByType returns zero or one Service matching the given type.
|
||||||
|
// If there is more than one service for that type, the first match is returned.
|
||||||
|
func (ss Services) ServiceByType(_type string) (Service, bool) {
|
||||||
|
for _, s := range ss {
|
||||||
|
if s.HasType(_type) {
|
||||||
|
return s, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Service{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Service is a means of communicating or interacting with the DID subject or associated entities
|
||||||
|
// via one or more service endpoints.
|
||||||
|
// It can have one or more types.
|
||||||
|
type Service struct {
|
||||||
|
Id string
|
||||||
|
Types []string
|
||||||
|
Endpoints []any // either strEndpoint or mapEndpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Service) HasType(_type string) bool {
|
||||||
|
for _, t := range s.Types {
|
||||||
|
if t == _type {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Service) MarshalJSON() ([]byte, error) {
|
||||||
|
var aux struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Type any `json:"type"`
|
||||||
|
Endpoint any `json:"serviceEndpoint"`
|
||||||
|
}
|
||||||
|
|
||||||
|
aux.Id = s.Id
|
||||||
|
|
||||||
|
switch len(s.Types) {
|
||||||
|
case 0:
|
||||||
|
return nil, fmt.Errorf("service type is required")
|
||||||
|
case 1:
|
||||||
|
aux.Type = s.Types[0]
|
||||||
|
default:
|
||||||
|
aux.Type = s.Types
|
||||||
|
}
|
||||||
|
|
||||||
|
switch len(s.Endpoints) {
|
||||||
|
case 0:
|
||||||
|
return nil, fmt.Errorf("service endpoint is required")
|
||||||
|
case 1:
|
||||||
|
aux.Endpoint = s.Endpoints[0]
|
||||||
|
default:
|
||||||
|
aux.Endpoint = s.Endpoints
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Marshal(aux)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) UnmarshalJSON(bytes []byte) error {
|
||||||
|
var aux struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Type json.RawMessage `json:"type"`
|
||||||
|
Endpoint json.RawMessage `json:"serviceEndpoint"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err := json.Unmarshal(bytes, &aux)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(aux.Id) == 0 {
|
||||||
|
return fmt.Errorf("service id is required")
|
||||||
|
}
|
||||||
|
s.Id = aux.Id
|
||||||
|
|
||||||
|
s.Types, err = unmarshalSingleOrArray[string](aux.Type)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(s.Types) == 0 {
|
||||||
|
return fmt.Errorf("service type is required")
|
||||||
|
}
|
||||||
|
for _, _type := range s.Types {
|
||||||
|
if len(_type) == 0 {
|
||||||
|
return fmt.Errorf("invalid service type: must not be empty string")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.Endpoints, err = unmarshalSingleOrArray[any](aux.Endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(s.Endpoints) == 0 {
|
||||||
|
return fmt.Errorf("service endpoint is required")
|
||||||
|
}
|
||||||
|
for i, endpoint := range s.Endpoints {
|
||||||
|
switch endpoint := endpoint.(type) {
|
||||||
|
case string:
|
||||||
|
s.Endpoints[i] = StrEndpoint(endpoint)
|
||||||
|
case map[string]any:
|
||||||
|
s.Endpoints[i] = MapEndpoint(endpoint)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("endpoint must be %T or %T", StrEndpoint(""), MapEndpoint{})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type StrEndpoint string
|
||||||
|
|
||||||
|
type MapEndpoint map[string]any
|
||||||
|
|
||||||
|
func unmarshalSingleOrArray[T any](data json.RawMessage) ([]T, error) {
|
||||||
|
if data == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var single T
|
||||||
|
if err := json.Unmarshal(data, &single); err == nil {
|
||||||
|
return []T{single}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var array []T
|
||||||
|
if err := json.Unmarshal(data, &array); err == nil {
|
||||||
|
return array, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("must be %T or array of %T", single, single)
|
||||||
|
}
|
||||||
107
service_test.go
Normal file
107
service_test.go
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
package did
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestServicesJsonRountrip(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "LinkedDomains",
|
||||||
|
input: `[
|
||||||
|
{
|
||||||
|
"id":"did:example:123#foo",
|
||||||
|
"type": "LinkedDomains",
|
||||||
|
"serviceEndpoint": {
|
||||||
|
"origins": ["https://foo.example.com", "https://identity.foundation"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":"did:example:123#bar",
|
||||||
|
"type": "LinkedDomains",
|
||||||
|
"serviceEndpoint": "https://bar.example.com"
|
||||||
|
}
|
||||||
|
]`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "LinkedVerifiablePresentation",
|
||||||
|
input: `[
|
||||||
|
{
|
||||||
|
"id": "did:example:123#foo",
|
||||||
|
"type": "LinkedVerifiablePresentation",
|
||||||
|
"serviceEndpoint": "https://bar.example.com/verifiable-presentation.jsonld"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "did:example:123#baz",
|
||||||
|
"type": "LinkedVerifiablePresentation",
|
||||||
|
"serviceEndpoint": "ipfs://bafybeihkoviema7g3gxyt6la7vd5ho32ictqbilu3wnlo3rs7ewhnp7lly/verifiable-presentation.jwt"
|
||||||
|
}
|
||||||
|
]`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "WotThing",
|
||||||
|
input: `[{
|
||||||
|
"id": "did:example:wotdiscoveryexample#td",
|
||||||
|
"type": "WotThing",
|
||||||
|
"serviceEndpoint":
|
||||||
|
"https://wot.example.com/.well-known/wot"
|
||||||
|
}]`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multi types",
|
||||||
|
input: `[
|
||||||
|
{
|
||||||
|
"id": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK#node",
|
||||||
|
"type": [
|
||||||
|
"DIDCommMessaging",
|
||||||
|
"CredentialRepositoryService",
|
||||||
|
"RevocationList2020Status",
|
||||||
|
"TrustRegistryService"
|
||||||
|
],
|
||||||
|
"serviceEndpoint": "https://node.blockchain-network.com/api/v1"
|
||||||
|
}
|
||||||
|
]`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multi types, map values",
|
||||||
|
input: `[
|
||||||
|
{
|
||||||
|
"id": "did:web:wallet.example.com#wallet-service",
|
||||||
|
"type": [
|
||||||
|
"VerifiableCredentialService",
|
||||||
|
"OpenIdConnectVersion1.0Service",
|
||||||
|
"DIDCommMessaging",
|
||||||
|
"CredentialRepositoryService"
|
||||||
|
],
|
||||||
|
"serviceEndpoint": {
|
||||||
|
"credentialIssue": "https://wallet.example.com/credentials/issue",
|
||||||
|
"credentialVerify": "https://wallet.example.com/credentials/verify",
|
||||||
|
"credentialStore": "https://wallet.example.com/vault",
|
||||||
|
"oidcAuth": "https://wallet.example.com/auth",
|
||||||
|
"oidcToken": "https://wallet.example.com/token",
|
||||||
|
"didcommInbox": "https://wallet.example.com/didcomm/inbox",
|
||||||
|
"didcommOutbox": "https://wallet.example.com/didcomm/outbox"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
var services []Service
|
||||||
|
err := json.Unmarshal([]byte(tc.input), &services)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
rt, err := json.Marshal(services)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.JSONEq(t, tc.input, string(rt))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,7 +13,8 @@ import (
|
|||||||
// Specification: https://www.w3.org/TR/cid-1.0/#Multikey
|
// Specification: https://www.w3.org/TR/cid-1.0/#Multikey
|
||||||
|
|
||||||
const (
|
const (
|
||||||
JsonLdContext = "https://www.w3.org/ns/cid/v1"
|
// This is apparently the right context despite the spec above saying otherwise.
|
||||||
|
JsonLdContext = "https://w3id.org/security/multikey/v1"
|
||||||
Type = "Multikey"
|
Type = "Multikey"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
var _ did.Document = &document{}
|
var _ did.Document = &document{}
|
||||||
|
|
||||||
type document struct {
|
type document struct {
|
||||||
id did.DID
|
id string
|
||||||
signature did.VerificationMethodSignature
|
signature did.VerificationMethodSignature
|
||||||
keyAgreement did.VerificationMethodKeyAgreement
|
keyAgreement did.VerificationMethodKeyAgreement
|
||||||
}
|
}
|
||||||
@@ -38,7 +38,7 @@ func (d document) MarshalJSON() ([]byte, error) {
|
|||||||
CapabilityDelegation []string `json:"capabilityDelegation,omitempty"`
|
CapabilityDelegation []string `json:"capabilityDelegation,omitempty"`
|
||||||
}{
|
}{
|
||||||
Context: d.Context(),
|
Context: d.Context(),
|
||||||
ID: d.id.String(),
|
ID: d.id,
|
||||||
AlsoKnownAs: nil,
|
AlsoKnownAs: nil,
|
||||||
VerificationMethod: vms,
|
VerificationMethod: vms,
|
||||||
Authentication: []string{d.signature.ID()},
|
Authentication: []string{d.signature.ID()},
|
||||||
@@ -58,7 +58,7 @@ func (d document) Context() []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d document) ID() string {
|
func (d document) ID() string {
|
||||||
return d.id.String()
|
return d.id
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d document) Controllers() []string {
|
func (d document) Controllers() []string {
|
||||||
@@ -102,6 +102,10 @@ func (d document) CapabilityDelegation() []did.VerificationMethodSignature {
|
|||||||
return []did.VerificationMethodSignature{d.signature}
|
return []did.VerificationMethodSignature{d.signature}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d document) Services() did.Services {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func stringSet(values ...string) []string {
|
func stringSet(values ...string) []string {
|
||||||
res := make([]string, 0, len(values))
|
res := make([]string, 0, len(values))
|
||||||
loop:
|
loop:
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ func (d DidKey) Method() string {
|
|||||||
func (d DidKey) Document(opts ...did.ResolutionOption) (did.Document, error) {
|
func (d DidKey) Document(opts ...did.ResolutionOption) (did.Document, error) {
|
||||||
params := did.CollectResolutionOpts(opts)
|
params := did.CollectResolutionOpts(opts)
|
||||||
|
|
||||||
doc := document{id: d}
|
doc := document{id: d.String()}
|
||||||
mainVmId := fmt.Sprintf("did:key:%s#%s", d.msi, d.msi)
|
mainVmId := fmt.Sprintf("did:key:%s#%s", d.msi, d.msi)
|
||||||
|
|
||||||
switch pub := d.pubkey.(type) {
|
switch pub := d.pubkey.(type) {
|
||||||
|
|||||||
98
verifiers/did-plc/document.go
Normal file
98
verifiers/did-plc/document.go
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
package didplc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/go-did-it"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ did.Document = &document{}
|
||||||
|
|
||||||
|
type document struct {
|
||||||
|
id string
|
||||||
|
alsoKnownAs []*url.URL
|
||||||
|
signatures []did.VerificationMethodSignature
|
||||||
|
services did.Services
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d document) MarshalJSON() ([]byte, error) {
|
||||||
|
akas := make([]string, len(d.alsoKnownAs))
|
||||||
|
for i, aka := range d.alsoKnownAs {
|
||||||
|
akas[i] = aka.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Marshal(struct {
|
||||||
|
Context []string `json:"@context"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
AlsoKnownAs []string `json:"alsoKnownAs,omitempty"`
|
||||||
|
Controller string `json:"controller,omitempty"`
|
||||||
|
VerificationMethod []did.VerificationMethodSignature `json:"verificationMethod,omitempty"`
|
||||||
|
Services did.Services `json:"service,omitempty"`
|
||||||
|
}{
|
||||||
|
Context: d.Context(),
|
||||||
|
ID: d.id,
|
||||||
|
AlsoKnownAs: akas,
|
||||||
|
VerificationMethod: d.signatures,
|
||||||
|
Services: d.services,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d document) Context() []string {
|
||||||
|
res := make([]string, 0, 1+len(d.signatures))
|
||||||
|
res = append(res, did.JsonLdContext)
|
||||||
|
loop:
|
||||||
|
for _, method := range d.signatures {
|
||||||
|
for _, item := range res {
|
||||||
|
if method.JsonLdContext() == item {
|
||||||
|
continue loop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res = append(res, method.JsonLdContext())
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d document) ID() string {
|
||||||
|
return d.id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d document) Controllers() []string {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d document) AlsoKnownAs() []*url.URL {
|
||||||
|
return d.alsoKnownAs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d document) VerificationMethods() map[string]did.VerificationMethod {
|
||||||
|
res := make(map[string]did.VerificationMethod)
|
||||||
|
for _, signature := range d.signatures {
|
||||||
|
res[signature.ID()] = signature
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d document) Authentication() []did.VerificationMethodSignature {
|
||||||
|
return d.signatures
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d document) Assertion() []did.VerificationMethodSignature {
|
||||||
|
return d.signatures
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d document) KeyAgreement() []did.VerificationMethodKeyAgreement {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d document) CapabilityInvocation() []did.VerificationMethodSignature {
|
||||||
|
return d.signatures
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d document) CapabilityDelegation() []did.VerificationMethodSignature {
|
||||||
|
return d.signatures
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d document) Services() did.Services {
|
||||||
|
return d.services
|
||||||
|
}
|
||||||
74
verifiers/did-plc/document_test.go
Normal file
74
verifiers/did-plc/document_test.go
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
package didplc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/go-did-it"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDocument(t *testing.T) {
|
||||||
|
// current resolved /data for did:plc:ewvi7nxzyoun6zhxrhs64oiz
|
||||||
|
resolvedData := `{"did":"did:plc:ewvi7nxzyoun6zhxrhs64oiz","verificationMethods":{"atproto":"did:key:zQ3shunBKsXixLxKtC5qeSG9E4J5RkGN57im31pcTzbNQnm5w"},"rotationKeys":["did:key:zQ3shhCGUqDKjStzuDxPkTxN6ujddP4RkEKJJouJGRRkaLGbg","did:key:zQ3shpKnbdPx3g3CmPf5cRVTPe1HtSwVn5ish3wSnDPQCbLJK"],"alsoKnownAs":["at://atproto.com"],"services":{"atproto_pds":{"type":"AtprotoPersonalDataServer","endpoint":"https://enoki.us-east.host.bsky.network"}}}`
|
||||||
|
|
||||||
|
// as resolved by https://plc.directory/did:plc:ewvi7nxzyoun6zhxrhs64oiz
|
||||||
|
// the original json had an additional
|
||||||
|
// "https://w3id.org/security/suites/secp256k1-2019/v1" context that
|
||||||
|
// I removed as it's just wrong
|
||||||
|
expectedJson := `
|
||||||
|
{
|
||||||
|
"@context":[
|
||||||
|
"https://www.w3.org/ns/did/v1",
|
||||||
|
"https://w3id.org/security/multikey/v1"
|
||||||
|
],
|
||||||
|
"id":"did:plc:ewvi7nxzyoun6zhxrhs64oiz",
|
||||||
|
"alsoKnownAs":[
|
||||||
|
"at://atproto.com"
|
||||||
|
],
|
||||||
|
"verificationMethod":[
|
||||||
|
{
|
||||||
|
"id":"did:plc:ewvi7nxzyoun6zhxrhs64oiz#atproto",
|
||||||
|
"type":"Multikey",
|
||||||
|
"controller":"did:plc:ewvi7nxzyoun6zhxrhs64oiz",
|
||||||
|
"publicKeyMultibase":"zQ3shunBKsXixLxKtC5qeSG9E4J5RkGN57im31pcTzbNQnm5w"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"service":[
|
||||||
|
{
|
||||||
|
"id":"#atproto_pds",
|
||||||
|
"type":"AtprotoPersonalDataServer",
|
||||||
|
"serviceEndpoint":"https://enoki.us-east.host.bsky.network"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
mockClient := &MockHTTPClient{resp: resolvedData}
|
||||||
|
|
||||||
|
d, err := did.Parse("did:plc:ewvi7nxzyoun6zhxrhs64oiz")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
doc, err := d.Document(did.WithHttpClient(mockClient))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
docBytes, err := json.Marshal(doc)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.JSONEq(t, expectedJson, string(docBytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
type MockHTTPClient struct {
|
||||||
|
resp string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockHTTPClient) Do(req *http.Request) (*http.Response, error) {
|
||||||
|
return &http.Response{
|
||||||
|
StatusCode: http.StatusOK,
|
||||||
|
Body: io.NopCloser(strings.NewReader(m.resp)),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
215
verifiers/did-plc/plc.go
Normal file
215
verifiers/did-plc/plc.go
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
package didplc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/go-did-it"
|
||||||
|
allkeys "github.com/ucan-wg/go-did-it/crypto/_allkeys"
|
||||||
|
"github.com/ucan-wg/go-did-it/crypto/ed25519"
|
||||||
|
"github.com/ucan-wg/go-did-it/crypto/p256"
|
||||||
|
"github.com/ucan-wg/go-did-it/crypto/p384"
|
||||||
|
"github.com/ucan-wg/go-did-it/crypto/p521"
|
||||||
|
"github.com/ucan-wg/go-did-it/crypto/rsa"
|
||||||
|
"github.com/ucan-wg/go-did-it/crypto/secp256k1"
|
||||||
|
ed25519vm "github.com/ucan-wg/go-did-it/verifiers/_methods/ed25519"
|
||||||
|
"github.com/ucan-wg/go-did-it/verifiers/_methods/jsonwebkey"
|
||||||
|
"github.com/ucan-wg/go-did-it/verifiers/_methods/multikey"
|
||||||
|
p256vm "github.com/ucan-wg/go-did-it/verifiers/_methods/p256"
|
||||||
|
secp256k1vm "github.com/ucan-wg/go-did-it/verifiers/_methods/secp256k1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Specification: https://web.plc.directory/spec/v0.1/did-plc
|
||||||
|
|
||||||
|
const DefaultRegistry = "https://plc.directory"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
did.RegisterMethod("plc", Decode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ did.DID = DidPlc{}
|
||||||
|
|
||||||
|
type DidPlc struct {
|
||||||
|
msi string // method-specific identifier, i.e. "12345" in "did:plc:12345"
|
||||||
|
}
|
||||||
|
|
||||||
|
func Decode(identifier string) (did.DID, error) {
|
||||||
|
const plcPrefix = "did:plc:"
|
||||||
|
|
||||||
|
if !strings.HasPrefix(identifier, plcPrefix) {
|
||||||
|
return nil, fmt.Errorf("%w: must start with 'did:plc'", did.ErrInvalidDid)
|
||||||
|
}
|
||||||
|
|
||||||
|
msi := identifier[len(plcPrefix):]
|
||||||
|
|
||||||
|
if len(msi) != 24 {
|
||||||
|
return nil, fmt.Errorf("%w: incorrect did:plc identifier length", did.ErrInvalidDid)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, char := range msi {
|
||||||
|
switch {
|
||||||
|
case char >= 'a' && char <= 'z':
|
||||||
|
case char >= '2' && char <= '7':
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("%w: did:plc identifier contains invalid character", did.ErrInvalidDid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return DidPlc{msi: msi}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d DidPlc) Method() string {
|
||||||
|
return "plc"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d DidPlc) Document(opts ...did.ResolutionOption) (did.Document, error) {
|
||||||
|
params := did.CollectResolutionOpts(opts)
|
||||||
|
identifier := d.String()
|
||||||
|
|
||||||
|
u, err := url.JoinPath(DefaultRegistry, identifier, "data")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req, err := http.NewRequestWithContext(params.Context(), "GET", u, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := params.HttpClient().Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: %w", did.ErrResolutionFailure, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var aux struct {
|
||||||
|
Did string `json:"did"`
|
||||||
|
VerificationMethods map[string]string `json:"verificationMethods"`
|
||||||
|
// RotationKeys []string `json:"rotationKeys"`
|
||||||
|
AlsoKnownAs []string `json:"alsoKnownAs"`
|
||||||
|
Services map[string]struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Endpoint string `json:"endpoint"`
|
||||||
|
} `json:"services"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// limit at 1MB
|
||||||
|
err = json.NewDecoder(io.LimitReader(res.Body, 1<<20)).Decode(&aux)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: %w", did.ErrResolutionFailure, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if aux.Did != identifier {
|
||||||
|
return nil, fmt.Errorf("%w: did:plc identifier mismatch", did.ErrResolutionFailure)
|
||||||
|
}
|
||||||
|
|
||||||
|
doc := &document{
|
||||||
|
id: aux.Did,
|
||||||
|
alsoKnownAs: make([]*url.URL, len(aux.AlsoKnownAs)),
|
||||||
|
signatures: make([]did.VerificationMethodSignature, 0, len(aux.VerificationMethods)),
|
||||||
|
services: make(did.Services, 0, len(aux.Services)),
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, aka := range aux.AlsoKnownAs {
|
||||||
|
doc.alsoKnownAs[i], err = url.Parse(aka)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: %w", did.ErrResolutionFailure, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for vmName, data := range aux.VerificationMethods {
|
||||||
|
// decode the did:key. It's a similar handling as in the did:key implementation, but:
|
||||||
|
// - the VM identifier is different
|
||||||
|
// - did:plc doesn't seem to care about key agreement VM
|
||||||
|
const keyPrefix = "did:key:"
|
||||||
|
|
||||||
|
if !strings.HasPrefix(data, keyPrefix) {
|
||||||
|
return nil, fmt.Errorf("%w: must start with 'did:key'", did.ErrInvalidDid)
|
||||||
|
}
|
||||||
|
msi := data[len(keyPrefix):]
|
||||||
|
|
||||||
|
pub, err := allkeys.PublicKeyFromPublicKeyMultibase(msi)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: %w", did.ErrInvalidDid, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
vmId := fmt.Sprintf("%s#%s", doc.id, vmName)
|
||||||
|
|
||||||
|
switch pub := pub.(type) {
|
||||||
|
case ed25519.PublicKey:
|
||||||
|
switch {
|
||||||
|
case params.HasVerificationMethodHint(jsonwebkey.Type):
|
||||||
|
doc.signatures = append(doc.signatures, jsonwebkey.NewJsonWebKey2020(vmId, pub, d))
|
||||||
|
case params.HasVerificationMethodHint(multikey.Type):
|
||||||
|
doc.signatures = append(doc.signatures, multikey.NewMultiKey(vmId, pub, d))
|
||||||
|
default:
|
||||||
|
if params.HasVerificationMethodHint(ed25519vm.Type2018) {
|
||||||
|
doc.signatures = append(doc.signatures, ed25519vm.NewVerificationKey2018(vmId, pub, d))
|
||||||
|
} else {
|
||||||
|
doc.signatures = append(doc.signatures, ed25519vm.NewVerificationKey2020(vmId, pub, d))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case *p256.PublicKey:
|
||||||
|
switch {
|
||||||
|
case params.HasVerificationMethodHint(jsonwebkey.Type):
|
||||||
|
doc.signatures = append(doc.signatures, jsonwebkey.NewJsonWebKey2020(vmId, pub, d))
|
||||||
|
case params.HasVerificationMethodHint(p256vm.Type2021):
|
||||||
|
doc.signatures = append(doc.signatures, p256vm.NewKey2021(vmId, pub, d))
|
||||||
|
default:
|
||||||
|
doc.signatures = append(doc.signatures, multikey.NewMultiKey(vmId, pub, d))
|
||||||
|
}
|
||||||
|
|
||||||
|
case *secp256k1.PublicKey:
|
||||||
|
switch {
|
||||||
|
case params.HasVerificationMethodHint(jsonwebkey.Type):
|
||||||
|
doc.signatures = append(doc.signatures, jsonwebkey.NewJsonWebKey2020(vmId, pub, d))
|
||||||
|
case params.HasVerificationMethodHint(secp256k1vm.Type2019):
|
||||||
|
doc.signatures = append(doc.signatures, secp256k1vm.NewVerificationKey2019(vmId, pub, d))
|
||||||
|
default:
|
||||||
|
doc.signatures = append(doc.signatures, multikey.NewMultiKey(vmId, pub, d))
|
||||||
|
}
|
||||||
|
|
||||||
|
case *p384.PublicKey, *p521.PublicKey, *rsa.PublicKey:
|
||||||
|
switch {
|
||||||
|
case params.HasVerificationMethodHint(jsonwebkey.Type):
|
||||||
|
doc.signatures = append(doc.signatures, jsonwebkey.NewJsonWebKey2020(vmId, pub, d))
|
||||||
|
default:
|
||||||
|
doc.signatures = append(doc.signatures, multikey.NewMultiKey(vmId, pub, d))
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported public key: %T", pub)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for id, service := range aux.Services {
|
||||||
|
doc.services = append(doc.services, did.Service{
|
||||||
|
Id: "#" + id,
|
||||||
|
Types: []string{service.Type},
|
||||||
|
Endpoints: []any{did.StrEndpoint(service.Endpoint)},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return doc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d DidPlc) String() string {
|
||||||
|
return fmt.Sprintf("did:plc:%s", d.msi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d DidPlc) ResolutionIsExpensive() bool {
|
||||||
|
// requires an external HTTP request
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d DidPlc) Equal(d2 did.DID) bool {
|
||||||
|
if d2, ok := d2.(DidPlc); ok {
|
||||||
|
return d.msi == d2.msi
|
||||||
|
}
|
||||||
|
if d2, ok := d2.(*DidPlc); ok {
|
||||||
|
return d.msi == d2.msi
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
43
verifiers/did-plc/plc_test.go
Normal file
43
verifiers/did-plc/plc_test.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package didplc_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/go-did-it"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseDIDPlc(t *testing.T) {
|
||||||
|
str := "did:plc:ewvi7nxzyoun6zhxrhs64oiz"
|
||||||
|
d, err := did.Parse(str)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, str, d.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIncorrectDIDPlc(t *testing.T) {
|
||||||
|
tests := []string{
|
||||||
|
"did:plc:ewvi7nxzyoun6zhxrhs64oi", // too short
|
||||||
|
"did:plc:ewvi7nxzyoun6zhxrhs64oizz", // too long
|
||||||
|
"did:plc:ewvi7nxzyoun6zhxrhs64oi0", // wrong char
|
||||||
|
"did:plc:ewvi7nxzyoun6zhxrhs64oiz:", // extra :
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt, func(t *testing.T) {
|
||||||
|
_, err := did.Parse(tt)
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMustParseDIDPlc(t *testing.T) {
|
||||||
|
str := "did:plc:ewvi7nxzyoun6zhxrhs64oiz"
|
||||||
|
require.NotPanics(t, func() {
|
||||||
|
d := did.MustParse(str)
|
||||||
|
require.Equal(t, str, d.String())
|
||||||
|
})
|
||||||
|
str = "did:plc:ewvi7nxzyoun6zhxrhs6" // too short
|
||||||
|
require.Panics(t, func() {
|
||||||
|
did.MustParse(str)
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user