Add support for DID services and did:plc
This commit is contained in:
@@ -13,7 +13,8 @@ import (
|
||||
// Specification: https://www.w3.org/TR/cid-1.0/#Multikey
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
var _ did.Document = &document{}
|
||||
|
||||
type document struct {
|
||||
id did.DID
|
||||
id string
|
||||
signature did.VerificationMethodSignature
|
||||
keyAgreement did.VerificationMethodKeyAgreement
|
||||
}
|
||||
@@ -38,7 +38,7 @@ func (d document) MarshalJSON() ([]byte, error) {
|
||||
CapabilityDelegation []string `json:"capabilityDelegation,omitempty"`
|
||||
}{
|
||||
Context: d.Context(),
|
||||
ID: d.id.String(),
|
||||
ID: d.id,
|
||||
AlsoKnownAs: nil,
|
||||
VerificationMethod: vms,
|
||||
Authentication: []string{d.signature.ID()},
|
||||
@@ -58,7 +58,7 @@ func (d document) Context() []string {
|
||||
}
|
||||
|
||||
func (d document) ID() string {
|
||||
return d.id.String()
|
||||
return d.id
|
||||
}
|
||||
|
||||
func (d document) Controllers() []string {
|
||||
@@ -102,6 +102,10 @@ func (d document) CapabilityDelegation() []did.VerificationMethodSignature {
|
||||
return []did.VerificationMethodSignature{d.signature}
|
||||
}
|
||||
|
||||
func (d document) Services() did.Services {
|
||||
return nil
|
||||
}
|
||||
|
||||
func stringSet(values ...string) []string {
|
||||
res := make([]string, 0, len(values))
|
||||
loop:
|
||||
|
||||
@@ -66,7 +66,7 @@ func (d DidKey) Method() string {
|
||||
func (d DidKey) Document(opts ...did.ResolutionOption) (did.Document, error) {
|
||||
params := did.CollectResolutionOpts(opts)
|
||||
|
||||
doc := document{id: d}
|
||||
doc := document{id: d.String()}
|
||||
mainVmId := fmt.Sprintf("did:key:%s#%s", d.msi, d.msi)
|
||||
|
||||
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