marginally more working

This commit is contained in:
Michael Muré
2025-03-13 11:26:39 +01:00
parent 581b06738c
commit 63c51774b6
8 changed files with 292 additions and 141 deletions

14
.gitignore vendored Normal file
View File

@@ -0,0 +1,14 @@
.idea
*.iml
out
gen
*.exe
*.exe~
*.dll
*.so
*.dylib
*.test
*.out
go.work
go.work.sum
.env

View File

@@ -1,6 +1,7 @@
package did_key
import (
"encoding/json"
"net/url"
"github.com/INFURA/go-did"
@@ -14,17 +15,38 @@ type document struct {
}
func (d document) MarshalJSON() ([]byte, error) {
// TODO implement me
panic("implement me")
return json.Marshal(struct {
Context []string `json:"@context"`
ID string `json:"id"`
AlsoKnownAs []string `json:"alsoKnownAs,omitempty"`
Controller string `json:"controller,omitempty"`
VerificationMethod []did.VerificationMethod `json:"verificationMethod,omitempty"`
Authentication []string `json:"authentication,omitempty"`
AssertionMethod []string `json:"assertionMethod,omitempty"`
KeyAgreement []string `json:"keyAgreement,omitempty"`
CapabilityInvocation []string `json:"capabilityInvocation,omitempty"`
CapabilityDelegation []string `json:"capabilityDelegation,omitempty"`
}{
Context: []string{did.JsonLdContext, d.verification.JsonLdContext()},
ID: d.id.String(),
AlsoKnownAs: nil,
Controller: d.id.String(),
VerificationMethod: []did.VerificationMethod{d.verification},
Authentication: []string{d.verification.ID()},
AssertionMethod: []string{d.verification.ID()},
KeyAgreement: []string{d.verification.ID()},
CapabilityInvocation: []string{d.verification.ID()},
CapabilityDelegation: []string{d.verification.ID()},
})
}
func (d document) ID() did.DID {
return d.id
}
func (d document) Controller() did.DID {
func (d document) Controllers() []did.DID {
// no external controller possible for did:key
return d.id
return []did.DID{d.id}
}
func (d document) AlsoKnownAs() []url.URL {

View File

@@ -3,47 +3,54 @@ package did_key
import (
"fmt"
"net/url"
"strings"
mbase "github.com/multiformats/go-multibase"
varint "github.com/multiformats/go-varint"
"github.com/multiformats/go-varint"
"github.com/INFURA/go-did"
"github.com/INFURA/go-did/verifications/ed25519"
)
type multicodecCode uint64
// Signature algorithms from the [did:key specification]
//
// [did:key specification]: https://w3c-ccg.github.io/did-method-key/#signature-method-creation-algorithm
const (
X25519 multicodecCode = 0xec
Ed25519 multicodecCode = 0xed
P256 multicodecCode = 0x1200
P384 multicodecCode = 0x1201
P521 multicodecCode = 0x1202
Secp256k1 multicodecCode = 0xe7
RSA multicodecCode = 0x1205
)
// Specification: https://w3c-ccg.github.io/did-method-key/
func Decode(identifier string) (did.DID, error) {
// baseCodec, bytes, err := mbase.Decode(identifier)
_, bytes, err := mbase.Decode(identifier)
const keyPrefix = "did:key:"
if !strings.HasPrefix(identifier, keyPrefix) {
return nil, fmt.Errorf("must start with 'did:key'")
}
baseCodec, bytes, err := mbase.Decode(identifier[len(keyPrefix):])
if err != nil {
return nil, fmt.Errorf("%w: %w", did.ErrInvalidDid, err)
}
// if baseCodec != mbase.Base58BTC {
// return nil, fmt.Errorf("%w: not Base58BTC encoded", did.ErrInvalidDid)
// }
code, _, err := varint.FromUvarint(bytes)
// the specification enforces that encoding
if baseCodec != mbase.Base58BTC {
return nil, fmt.Errorf("%w: not Base58BTC encoded", did.ErrInvalidDid)
}
code, read, err := varint.FromUvarint(bytes)
if err != nil {
return nil, fmt.Errorf("%w: %w", did.ErrInvalidDid, err)
}
switch multicodecCode(code) {
case Ed25519, P256, Secp256k1, RSA:
return DidKey{bytes: string(bytes), code: multicodecCode(code)}, nil
}
d := DidKey{identifier: identifier}
switch code {
case ed25519.MultibaseCode:
d.verification, err = ed25519.NewVerificationKey2020(identifier, bytes[read:], d)
// case P256: // TODO
// case Secp256k1: // TODO
// case RSA: // TODO
default:
return nil, fmt.Errorf("%w: unsupported did:key multicodec: 0x%x", did.ErrInvalidDid, code)
}
if err != nil {
return nil, fmt.Errorf("%w: %w", did.ErrInvalidDid, err)
}
return d, nil
}
func init() {
did.RegisterMethod("key", Decode)
@@ -52,9 +59,8 @@ func init() {
var _ did.DID = &DidKey{}
type DidKey struct {
// TODO: store a verification method instead
code multicodecCode
bytes string // as string instead of []byte to allow the == operator
identifier string // cached value
verification did.VerificationMethod
}
func (d DidKey) Method() string {
@@ -74,11 +80,12 @@ func (d DidKey) Fragment() string {
}
func (d DidKey) Document() (did.Document, error) {
// TODO implement me
panic("implement me")
return document{
id: d,
verification: d.verification,
}, nil
}
func (d DidKey) String() string {
key, _ := mbase.Encode(mbase.Base58BTC, []byte(d.bytes))
return "did:key:" + key
return d.identifier
}

54
did.go
View File

@@ -6,16 +6,13 @@ import (
"sync"
)
// Decoder is a function decoding an identifier ("foo" in "did:example:foo") into a DID.
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)
var (
decodersMu sync.RWMutex
decoders = map[string]Decoder{}
)
// RegisterMethod registers a DID decoder for a given DID method..
// Method must be the DID method (for example "key" in did:key).
// 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()
@@ -28,24 +25,21 @@ func RegisterMethod(method string, decoder Decoder) {
decoders[method] = decoder
}
// Parse returns the DID from the string representation or an error if
// the prefix and method are incorrect, if an unknown encryption algorithm
// is specified or if the method-specific-identifier's bytes don't
// represent a public key for the specified encryption algorithm.
func Parse(str string) (DID, error) {
// Parse attempts to decode a DID from its string representation.
func Parse(identifier string) (DID, error) {
decodersMu.RLock()
defer decodersMu.RUnlock()
if !strings.HasPrefix(str, "did:") {
if !strings.HasPrefix(identifier, "did:") {
return nil, fmt.Errorf("%w: must start with \"did:\"", ErrInvalidDid)
}
method, identifier, ok := strings.Cut(str[len("did:"):], ":")
method, suffix, ok := strings.Cut(identifier[len("did:"):], ":")
if !ok {
return nil, fmt.Errorf("%w: must have a method and an identifier", ErrInvalidDid)
}
if !checkIdentifier(identifier) {
if !checkSuffix(suffix) {
return nil, fmt.Errorf("%w: invalid identifier characters", ErrInvalidDid)
}
@@ -58,14 +52,27 @@ func Parse(str string) (DID, error) {
}
// MustParse is like Parse but panics instead of returning an error.
func MustParse(str string) DID {
did, err := Parse(str)
func MustParse(identifier string) DID {
did, err := Parse(identifier)
if err != nil {
panic(err)
}
return did
}
// HasValidSyntax 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:") {
return false
}
method, suffix, ok := strings.Cut(identifier[len("did:"):], ":")
if !ok {
return false
}
return checkMethod(method) && checkSuffix(suffix)
}
func checkMethod(method string) bool {
if len(method) == 0 {
return false
@@ -80,14 +87,19 @@ func checkMethod(method string) bool {
return true
}
func checkIdentifier(identifier string) bool {
if len(identifier) == 0 {
func checkSuffix(suffix string) bool {
if len(suffix) == 0 {
return false
}
// TODO
// for _, char := range identifier {
// for _, char := range suffix {
//
// }
return true
}
var (
decodersMu sync.RWMutex
decoders = map[string]Decoder{}
)

View File

@@ -5,7 +5,8 @@ import (
"net/url"
)
type DID interface { // --> implementation for each DID type: key, pkh ..
// DID is a decoded (i.e. from a string) Decentralized Identifiers.
type DID interface {
Method() string
Path() string
Query() url.Values
@@ -15,13 +16,15 @@ type DID interface { // --> implementation for each DID type: key, pkh ..
String() string // return the full DID URL, with path, query, fragment
}
type Document interface { // --> compact implementation, get serialized into json only if necessary
// Document is the interface for a DID document. It represents the "resolved" state of a DID.
type Document interface {
json.Marshaler
// ID is the identifier of the Document, which is the DID itself.
ID() DID
// Controller is the DID that is authorized to make changes to the Document. It's often the same as ID.
Controller() DID
// Controllers is the set of DID that is authorized to make changes to the Document. It's often the same as ID.
Controllers() []DID
// AlsoKnownAs returns an optional set of URL describing ???TODO
AlsoKnownAs() []url.URL
@@ -55,18 +58,26 @@ type Document interface { // --> compact implementation, get serialized into jso
// https://www.w3.org/TR/did-extensions-properties/#service-types
}
type VerificationMethod interface { // --> implementation for each method
// VerificationMethod is a common interface for a cryptographic signature verification method.
// For example, Ed25519VerificationKey2020 implements the Ed25519 signature verification.
type VerificationMethod interface {
json.Marshaler
json.Unmarshaler
// ID is a string identifier for the VerificationMethod
// ID is a string identifier for the VerificationMethod. It can be referenced in a Document.
ID() string
// Type is a string identifier of a verification method.
// See https://www.w3.org/TR/did-extensions-properties/#verification-method-types
Type() string
// ???? TODO
Controller() DID
// Verify that 'sig' is the signed hash of 'data'
// Controller is a DID able to control the VerificationMethod.
// This is not necessarily the same as for DID itself or the Document.
Controller() string
// JsonLdContext reports the JSON-LD context definition required for this verification method.
JsonLdContext() string
// Verify checks that 'sig' is a valid signature of 'data'.
Verify(data []byte, sig []byte) bool
}

View File

@@ -1,75 +0,0 @@
package Ed25519
import (
"crypto/ed25519"
"encoding/json"
"errors"
"github.com/INFURA/go-did"
)
var _ did.VerificationMethod = &VerificationKey2020{}
type VerificationKey2020 struct {
id string
pubkey ed25519.PublicKey
controller did.DID
}
func NewVerificationKey2020(id string, pubkey []byte, controller did.DID) (*VerificationKey2020, error) {
if len(pubkey) != ed25519.PublicKeySize {
return nil, errors.New("invalid ed25519 public key size")
}
return &VerificationKey2020{
id: id,
pubkey: pubkey,
controller: controller,
}, nil
}
func (v VerificationKey2020) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
ID string `json:"id"`
Type string `json:"type"`
Controller string `json:"controller"`
PublicKeyMultibase string `json:"publicKeyMultibase"`
}{
ID: v.ID(),
Type: v.Type(),
Controller: v.Controller().String(),
PublicKeyMultibase: v,
})
/*
{
"id": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK#z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
"type": "Ed25519VerificationKey2020",
"controller": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
"publicKeyMultibase": "z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
}
*/
}
func (v VerificationKey2020) UnmarshalJSON(bytes []byte) error {
// TODO implement me
panic("implement me")
}
func (v VerificationKey2020) ID() string {
return v.id
}
func (v VerificationKey2020) Type() string {
return "Ed25519VerificationKey2020"
}
func (v VerificationKey2020) Controller() did.DID {
return v.controller
}
func (v VerificationKey2020) Verify(data []byte, sig []byte) bool {
return ed25519.Verify(v.pubkey, data, sig)
}

View File

@@ -0,0 +1,132 @@
package ed25519
import (
"crypto/ed25519"
"encoding/json"
"errors"
"fmt"
mbase "github.com/multiformats/go-multibase"
"github.com/multiformats/go-varint"
"github.com/INFURA/go-did"
)
const (
MultibaseCode = uint64(0xed)
JsonLdContext = "https://w3id.org/security/suites/ed25519-2020/v1"
)
var _ did.VerificationMethod = &VerificationKey2020{}
type VerificationKey2020 struct {
id string
pubkey ed25519.PublicKey
controller string
}
func NewVerificationKey2020(id string, pubkey []byte, controller did.DID) (*VerificationKey2020, error) {
if len(pubkey) != ed25519.PublicKeySize {
return nil, errors.New("invalid ed25519 public key size")
}
return &VerificationKey2020{
id: id,
pubkey: pubkey,
controller: controller.String(),
}, nil
}
func (v VerificationKey2020) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
ID string `json:"id"`
Type string `json:"type"`
Controller string `json:"controller"`
PublicKeyMultibase string `json:"publicKeyMultibase"`
}{
ID: v.ID(),
Type: v.Type(),
Controller: v.Controller(),
PublicKeyMultibase: encodePubkey(v.pubkey),
})
}
func (v *VerificationKey2020) UnmarshalJSON(bytes []byte) error {
aux := struct {
ID string `json:"id"`
Type string `json:"type"`
Controller string `json:"controller"`
PublicKeyMultibase string `json:"publicKeyMultibase"`
}{}
err := json.Unmarshal(bytes, &aux)
if err != nil {
return err
}
if aux.Type != v.Type() {
return errors.New("invalid type")
}
v.id = aux.ID
if len(v.id) == 0 {
return errors.New("invalid id")
}
v.pubkey, err = decodePubkey(aux.PublicKeyMultibase)
if err != nil {
return fmt.Errorf("invalid publicKeyMultibase: %w", err)
}
v.controller = aux.Controller
if !did.HasValidSyntax(v.controller) {
return errors.New("invalid controller")
}
return nil
}
func (v VerificationKey2020) ID() string {
return v.id
}
func (v VerificationKey2020) Type() string {
return "Ed25519VerificationKey2020"
}
func (v VerificationKey2020) Controller() string {
return v.controller
}
func (v VerificationKey2020) JsonLdContext() string {
return JsonLdContext
}
func (v VerificationKey2020) Verify(data []byte, sig []byte) bool {
return ed25519.Verify(v.pubkey, data, sig)
}
func encodePubkey(pubkey ed25519.PublicKey) string {
// can only fail with an invalid encoding, but it's hardcoded
bytes, _ := mbase.Encode(mbase.Base58BTC, append(varint.ToUvarint(MultibaseCode), pubkey...))
return bytes
}
func decodePubkey(encoded string) (ed25519.PublicKey, error) {
baseCodec, bytes, err := mbase.Decode(encoded)
if err != nil {
return nil, err
}
// the specification enforces that encoding
if baseCodec != mbase.Base58BTC {
return nil, fmt.Errorf("not Base58BTC encoded")
}
code, read, err := varint.FromUvarint(bytes)
if err != nil {
return nil, err
}
if code != MultibaseCode {
return nil, fmt.Errorf("invalid code")
}
if read != 2 {
return nil, fmt.Errorf("unexpected multibase")
}
if len(bytes)-read != ed25519.PublicKeySize {
return nil, fmt.Errorf("invalid ed25519 public key size")
}
return bytes[read:], nil
}

View File

@@ -0,0 +1,28 @@
package ed25519_test
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/require"
_ "github.com/INFURA/go-did/did-key"
"github.com/INFURA/go-did/verifications/ed25519"
)
func TestJson(t *testing.T) {
data := `{
"id": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK#z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
"type": "Ed25519VerificationKey2020",
"controller": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
"publicKeyMultibase": "z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
}`
var vm ed25519.VerificationKey2020
err := json.Unmarshal([]byte(data), &vm)
require.NoError(t, err)
bytes, err := json.Marshal(vm)
require.NoError(t, err)
require.JSONEq(t, data, string(bytes))
}