Integrate go-varsig and go-did-it

- go-varsig provides a varsig V1 implementation
- go-did-it provides a complete and extensible DID implementation
This commit is contained in:
Michael Muré
2025-07-31 14:43:42 +02:00
parent 947add66c5
commit 33e8a8a821
74 changed files with 317 additions and 2736 deletions

View File

@@ -9,7 +9,6 @@ import (
"github.com/multiformats/go-multihash"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gotest.tools/v3/golden"
"github.com/ucan-wg/go-ucan/token/internal/envelope"
)
@@ -17,11 +16,11 @@ import (
func TestCidFromBytes(t *testing.T) {
t.Parallel()
expData := golden.Get(t, "example.dagcbor")
expData := exampleDagCbor
expHash, err := multihash.Sum(expData, uint64(multicodec.Sha2_256), -1)
require.NoError(t, err)
data, err := envelope.ToDagCbor(examplePrivKey(t), newExample(t))
data, err := envelope.ToDagCbor(examplePrivKey(t), newExample())
require.NoError(t, err)
id, err := envelope.CIDFromBytes(data)

View File

@@ -7,6 +7,8 @@ import (
"sync"
"testing"
"github.com/MetaMask/go-did-it/crypto"
"github.com/MetaMask/go-did-it/crypto/ed25519"
"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/codec/dagcbor"
"github.com/ipld/go-ipld-prime/datamodel"
@@ -14,32 +16,30 @@ import (
"github.com/ipld/go-ipld-prime/node/basicnode"
"github.com/ipld/go-ipld-prime/node/bindnode"
"github.com/ipld/go-ipld-prime/schema"
"github.com/libp2p/go-libp2p/core/crypto"
"github.com/stretchr/testify/require"
"gotest.tools/v3/golden"
"github.com/ucan-wg/go-ucan/token/internal/envelope"
)
const (
exampleCID = "zdpuAyw6R5HvKSPzztuzXNYFx3ZGoMHMuAsXL6u3xLGQriRXQ"
exampleDID = "did:key:z6MkpuK2Amsu1RqcLGgmHHQHhvmeXCCBVsM4XFSg2cCyg4Nh"
exampleGreeting = "world"
examplePrivKeyCfg = "CAESQP9v2uqECTuIi45dyg3znQvsryvf2IXmOF/6aws6aCehm0FVrj0zHR5RZSDxWNjcpcJqsGym3sjCungX9Zt5oA4="
exampleSignatureStr = "PZV6A2aI7n+MlyADqcqmWhkuyNrgUCDz+qSLSnI9bpasOwOhKUTx95m5Nu5CO/INa1LqzHGioD9+PVf6qdtTBg"
exampleTag = "ucan/example@v1.0.0-rc.1"
exampleTypeName = "Example"
exampleVarsigHeaderStr = "NO0BcQ"
exampleCID = "zdpuAn4jksvc1gc9PLDqHw2NoFq8CBkRVTTo2xFuW2JUPS5DY"
exampleDID = "did:key:z6MkuqvEtTW9L1E91CY3GmL83muetLAA2h8A5fUHjJgqq2Ab"
exampleGreeting = "world"
examplePrivKeyB64 = "V4hh1lcFV43Y6vyOBEVOFTwl1XS/DR0F/kYcz5i6W/DkrUTG8yx09lOwSf36NCHPKSFYv/T1R3WKjNfndgVucA=="
exampleTag = "ucan/example@v1.0.0-rc.1"
invalidSignatureStr = "PZV6A2aI7n+MlyADqcqmWhkuyNrgUCDz+qSLSnI9bpasOwOhKUTx95m5Nu5CO/INa1LqzHGioD9+PVf6qdtTBK"
exampleDAGCBORFilename = "example.dagcbor"
exampleDAGJSONFilename = "example.dagjson"
)
//go:embed testdata/example.ipldsch
var schemaBytes []byte
//go:embed testdata/example.dagcbor
var exampleDagCbor []byte
//go:embed testdata/example.dagjson
var exampleDagJson []byte
var (
once sync.Once
ts *schema.TypeSystem
@@ -59,7 +59,7 @@ func mustLoadSchema() *schema.TypeSystem {
}
func exampleType() schema.Type {
return mustLoadSchema().TypeByName(exampleTypeName)
return mustLoadSchema().TypeByName("Example")
}
var _ envelope.Tokener = (*Example)(nil)
@@ -69,9 +69,7 @@ type Example struct {
Issuer string
}
func newExample(t *testing.T) *Example {
t.Helper()
func newExample() *Example {
return &Example{
Hello: exampleGreeting,
Issuer: exampleDID,
@@ -86,45 +84,30 @@ func (*Example) Tag() string {
return exampleTag
}
func exampleGoldenNode(t *testing.T) datamodel.Node {
func examplePrivKey(t *testing.T) crypto.PrivateKeySigningBytes {
t.Helper()
cbor := golden.Get(t, exampleDAGCBORFilename)
node, err := ipld.Decode(cbor, dagcbor.Decode)
privBytes, err := base64.StdEncoding.DecodeString(examplePrivKeyB64)
require.NoError(t, err)
return node
}
func examplePrivKey(t *testing.T) crypto.PrivKey {
t.Helper()
privKeyEnc, err := crypto.ConfigDecodeKey(examplePrivKeyCfg)
require.NoError(t, err)
privKey, err := crypto.UnmarshalPrivateKey(privKeyEnc)
privKey, err := ed25519.PrivateKeyFromBytes(privBytes)
require.NoError(t, err)
return privKey
}
func exampleSignature(t *testing.T) []byte {
t.Helper()
sig, err := base64.RawStdEncoding.DecodeString(exampleSignatureStr)
require.NoError(t, err)
return sig
}
func invalidNodeFromGolden(t *testing.T) datamodel.Node {
// nodeWithInvalidSignature creates an IPLD node of a token, with an invalid signature
func nodeWithInvalidSignature(t *testing.T) datamodel.Node {
t.Helper()
invalidSig, err := base64.RawStdEncoding.DecodeString(invalidSignatureStr)
require.NoError(t, err)
envelNode := exampleGoldenNode(t)
cbor := exampleDagCbor
envelNode, err := ipld.Decode(cbor, dagcbor.Decode)
require.NoError(t, err)
sigPayloadNode, err := envelNode.LookupByIndex(1)
require.NoError(t, err)

View File

@@ -3,7 +3,7 @@
// a verified [TokenPayload].
//
// Encoding functions in this package require a private key as a
// parameter so the VarsigHeader can be set and so that a
// parameter so the VarsigBytes can be set and so that a
// cryptographic signature can be generated.
//
// Decoding functions in this package likewise perform the signature
@@ -40,10 +40,10 @@ import (
"github.com/ipld/go-ipld-prime/node/basicnode"
"github.com/ipld/go-ipld-prime/node/bindnode"
"github.com/ipld/go-ipld-prime/schema"
"github.com/libp2p/go-libp2p/core/crypto"
"github.com/ucan-wg/go-varsig"
"github.com/ucan-wg/go-ucan/did"
"github.com/ucan-wg/go-ucan/token/internal/varsig"
"github.com/MetaMask/go-did-it"
"github.com/MetaMask/go-did-it/crypto"
)
const (
@@ -132,7 +132,7 @@ func FromIPLD[T Tokener](node datamodel.Node) (T, error) {
}
// This needs to be done before converting this node to its schema
// representation (afterwards, the field might be renamed os it's safer
// representation (afterwards, the field might be renamed, so it's safer
// to use the wire name).
issuerNode, err := info.tokenPayloadNode.LookupByString("iss")
if err != nil {
@@ -162,7 +162,7 @@ func FromIPLD[T Tokener](node datamodel.Node) (T, error) {
}
// Check that the issuer's DID contains a public key with a type that
// matches the VarsigHeader and then verify the SigPayload.
// matches the VarsigBytes and then verify the SigPayload.
issuer, err := issuerNode.AsString()
if err != nil {
return zero, err
@@ -173,28 +173,36 @@ func FromIPLD[T Tokener](node datamodel.Node) (T, error) {
return zero, err
}
issuerPubKey, err := issuerDID.PubKey()
// TODO: pass resolution options
issuerDoc, err := issuerDID.Document()
if err != nil {
return zero, err
}
issuerVarsigHeader, err := varsig.Encode(issuerPubKey.Type())
vsig, err := varsig.Decode(info.VarsigBytes)
if err != nil {
return zero, fmt.Errorf("failed to decode varsig: %w", err)
}
var data []byte
switch vsig.PayloadEncoding() {
case varsig.PayloadEncodingDAGCBOR:
// TODO: can we use the already serialized CBOR data here, instead of encoding again the payload?
data, err = ipld.Encode(info.sigPayloadNode, dagcbor.Encode)
case varsig.PayloadEncodingDAGJSON:
data, err = ipld.Encode(info.sigPayloadNode, dagjson.Encode)
default:
return zero, errors.New("unsupported payload encoding")
}
if err != nil {
return zero, err
}
if string(info.VarsigHeader) != string(issuerVarsigHeader) {
return zero, errors.New("the VarsigHeader key type doesn't match the issuer's key type")
}
// TODO: use CapabilityDelegation() or CapabilityInvocation()
// TODO: can we use the already serialized CBOR data here, instead of encoding again the payload?
data, err := ipld.Encode(info.sigPayloadNode, dagcbor.Encode)
if err != nil {
return zero, err
}
ok, err = issuerPubKey.Verify(data, info.Signature)
if err != nil || !ok {
ok, _ = did.TryAllVerifyBytes(issuerDoc.CapabilityDelegation(), data, info.Signature, crypto.WithVarsig(vsig))
if !ok {
return zero, errors.New("failed to verify the token's signature")
}
@@ -203,7 +211,7 @@ func FromIPLD[T Tokener](node datamodel.Node) (T, error) {
// Encode marshals a Tokener to the format specified by the provided
// codec.Encoder.
func Encode(privKey crypto.PrivKey, token Tokener, encFn codec.Encoder) ([]byte, error) {
func Encode(privKey crypto.PrivateKeySigningBytes, token Tokener, encFn codec.Encoder) ([]byte, error) {
node, err := ToIPLD(privKey, token)
if err != nil {
return nil, err
@@ -214,7 +222,7 @@ func Encode(privKey crypto.PrivKey, token Tokener, encFn codec.Encoder) ([]byte,
// EncodeWriter is the same as Encode but outputs to an io.Writer instead
// of encoding into a []byte.
func EncodeWriter(w io.Writer, privKey crypto.PrivKey, token Tokener, encFn codec.Encoder) error {
func EncodeWriter(w io.Writer, privKey crypto.PrivateKeySigningBytes, token Tokener, encFn codec.Encoder) error {
node, err := ToIPLD(privKey, token)
if err != nil {
return err
@@ -224,38 +232,36 @@ func EncodeWriter(w io.Writer, privKey crypto.PrivKey, token Tokener, encFn code
}
// ToDagCbor marshals the Tokener to the DAG-CBOR format.
func ToDagCbor(privKey crypto.PrivKey, token Tokener) ([]byte, error) {
func ToDagCbor(privKey crypto.PrivateKeySigningBytes, token Tokener) ([]byte, error) {
return Encode(privKey, token, dagcbor.Encode)
}
// ToDagCborWriter is the same as ToDagCbor but outputs to an io.Writer
// instead of encoding into a []byte.
func ToDagCborWriter(w io.Writer, privKey crypto.PrivKey, token Tokener) error {
func ToDagCborWriter(w io.Writer, privKey crypto.PrivateKeySigningBytes, token Tokener) error {
return EncodeWriter(w, privKey, token, dagcbor.Encode)
}
// ToDagJson marshals the Tokener to the DAG-JSON format.
func ToDagJson(privKey crypto.PrivKey, token Tokener) ([]byte, error) {
func ToDagJson(privKey crypto.PrivateKeySigningBytes, token Tokener) ([]byte, error) {
return Encode(privKey, token, dagjson.Encode)
}
// ToDagJsonWriter is the same as ToDagJson but outputs to an io.Writer
// instead of encoding into a []byte.
func ToDagJsonWriter(w io.Writer, privKey crypto.PrivKey, token Tokener) error {
func ToDagJsonWriter(w io.Writer, privKey crypto.PrivateKeySigningBytes, token Tokener) error {
return EncodeWriter(w, privKey, token, dagjson.Encode)
}
// ToIPLD wraps the Tokener in an IPLD datamodel.Node.
func ToIPLD(privKey crypto.PrivKey, token Tokener) (datamodel.Node, error) {
func ToIPLD(privKey crypto.PrivateKeySigningBytes, token Tokener) (datamodel.Node, error) {
tokenPayloadNode := bindnode.Wrap(token, token.Prototype().Type()).Representation()
varsigHeader, err := varsig.Encode(privKey.Type())
if err != nil {
return nil, err
}
opts := []crypto.SigningOption{crypto.WithPayloadEncoding(varsig.PayloadEncodingDAGCBOR)}
vsig := privKey.Varsig(opts...)
sigPayloadNode, err := qp.BuildMap(basicnode.Prototype.Any, 2, func(ma datamodel.MapAssembler) {
qp.MapEntry(ma, VarsigHeaderKey, qp.Bytes(varsigHeader))
qp.MapEntry(ma, VarsigHeaderKey, qp.Bytes(vsig.Encode()))
qp.MapEntry(ma, token.Tag(), qp.Node(tokenPayloadNode))
})
@@ -264,7 +270,7 @@ func ToIPLD(privKey crypto.PrivKey, token Tokener) (datamodel.Node, error) {
return nil, err
}
signature, err := privKey.Sign(data)
signature, err := privKey.SignToBytes(data, opts...)
if err != nil {
return nil, err
}
@@ -315,7 +321,7 @@ func FindTag(node datamodel.Node) (string, error) {
type Info struct {
Tag string
Signature []byte
VarsigHeader []byte
VarsigBytes []byte
sigPayloadNode datamodel.Node // private, we don't want to expose that
tokenPayloadNode datamodel.Node // private, we don't want to expose that
}
@@ -367,7 +373,7 @@ func Inspect(node datamodel.Node) (Info, error) {
switch {
case key == VarsigHeaderKey:
foundVarsigHeader = true
res.VarsigHeader, err = v.AsBytes()
res.VarsigBytes, err = v.AsBytes()
if err != nil {
return Info{}, err
}

View File

@@ -3,15 +3,16 @@ package envelope_test
import (
"bytes"
"crypto/sha256"
_ "embed"
"encoding/base64"
"os"
"testing"
_ "github.com/MetaMask/go-did-it/verifiers/did-key"
"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/codec/dagcbor"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gotest.tools/v3/golden"
"github.com/ucan-wg/go-ucan/token/internal/envelope"
)
@@ -22,9 +23,7 @@ func TestDecode(t *testing.T) {
t.Run("via FromDagCbor", func(t *testing.T) {
t.Parallel()
data := golden.Get(t, "example.dagcbor")
tkn, err := envelope.FromDagCbor[*Example](data)
tkn, err := envelope.FromDagCbor[*Example](exampleDagCbor)
require.NoError(t, err)
assert.Equal(t, exampleGreeting, tkn.Hello)
assert.Equal(t, exampleDID, tkn.Issuer)
@@ -33,9 +32,7 @@ func TestDecode(t *testing.T) {
t.Run("via FromDagJson", func(t *testing.T) {
t.Parallel()
data := golden.Get(t, "example.dagjson")
tkn, err := envelope.FromDagJson[*Example](data)
tkn, err := envelope.FromDagJson[*Example](exampleDagJson)
require.NoError(t, err)
assert.Equal(t, exampleGreeting, tkn.Hello)
assert.Equal(t, exampleDID, tkn.Issuer)
@@ -48,17 +45,17 @@ func TestEncode(t *testing.T) {
t.Run("via ToDagCbor", func(t *testing.T) {
t.Parallel()
data, err := envelope.ToDagCbor(examplePrivKey(t), newExample(t))
data, err := envelope.ToDagCbor(examplePrivKey(t), newExample())
require.NoError(t, err)
golden.AssertBytes(t, data, exampleDAGCBORFilename)
require.Equal(t, exampleDagCbor, data)
})
t.Run("via ToDagJson", func(t *testing.T) {
t.Parallel()
data, err := envelope.ToDagJson(examplePrivKey(t), newExample(t))
data, err := envelope.ToDagJson(examplePrivKey(t), newExample())
require.NoError(t, err)
golden.Assert(t, string(data), exampleDAGJSONFilename)
require.Equal(t, exampleDagJson, data)
})
}
@@ -68,14 +65,14 @@ func TestRoundtrip(t *testing.T) {
t.Run("via FromDagCbor/ToDagCbor", func(t *testing.T) {
t.Parallel()
dataIn := golden.Get(t, exampleDAGCBORFilename)
dataIn := exampleDagCbor
tkn, err := envelope.FromDagCbor[*Example](dataIn)
require.NoError(t, err)
assert.Equal(t, exampleGreeting, tkn.Hello)
assert.Equal(t, exampleDID, tkn.Issuer)
dataOut, err := envelope.ToDagCbor(examplePrivKey(t), newExample(t))
dataOut, err := envelope.ToDagCbor(examplePrivKey(t), newExample())
require.NoError(t, err)
assert.Equal(t, dataIn, dataOut)
})
@@ -83,7 +80,7 @@ func TestRoundtrip(t *testing.T) {
t.Run("via FromDagCborReader/ToDagCborWriter", func(t *testing.T) {
t.Parallel()
data := golden.Get(t, exampleDAGCBORFilename)
data := exampleDagCbor
tkn, err := envelope.FromDagCborReader[*Example](bytes.NewReader(data))
require.NoError(t, err)
@@ -91,21 +88,21 @@ func TestRoundtrip(t *testing.T) {
assert.Equal(t, exampleDID, tkn.Issuer)
w := &bytes.Buffer{}
require.NoError(t, envelope.ToDagCborWriter(w, examplePrivKey(t), newExample(t)))
require.NoError(t, envelope.ToDagCborWriter(w, examplePrivKey(t), newExample()))
assert.Equal(t, data, w.Bytes())
})
t.Run("via FromDagJson/ToDagJson", func(t *testing.T) {
t.Parallel()
dataIn := golden.Get(t, exampleDAGJSONFilename)
dataIn := exampleDagJson
tkn, err := envelope.FromDagJson[*Example](dataIn)
require.NoError(t, err)
assert.Equal(t, exampleGreeting, tkn.Hello)
assert.Equal(t, exampleDID, tkn.Issuer)
dataOut, err := envelope.ToDagJson(examplePrivKey(t), newExample(t))
dataOut, err := envelope.ToDagJson(examplePrivKey(t), newExample())
require.NoError(t, err)
assert.Equal(t, dataIn, dataOut)
})
@@ -113,7 +110,7 @@ func TestRoundtrip(t *testing.T) {
t.Run("via FromDagJsonReader/ToDagJsonrWriter", func(t *testing.T) {
t.Parallel()
data := golden.Get(t, exampleDAGJSONFilename)
data := exampleDagJson
tkn, err := envelope.FromDagJsonReader[*Example](bytes.NewReader(data))
require.NoError(t, err)
@@ -121,7 +118,7 @@ func TestRoundtrip(t *testing.T) {
assert.Equal(t, exampleDID, tkn.Issuer)
w := &bytes.Buffer{}
require.NoError(t, envelope.ToDagJsonWriter(w, examplePrivKey(t), newExample(t)))
require.NoError(t, envelope.ToDagJsonWriter(w, examplePrivKey(t), newExample()))
assert.Equal(t, data, w.Bytes())
})
}
@@ -129,7 +126,7 @@ func TestRoundtrip(t *testing.T) {
func TestFromIPLD_with_invalid_signature(t *testing.T) {
t.Parallel()
node := invalidNodeFromGolden(t)
node := nodeWithInvalidSignature(t)
tkn, err := envelope.FromIPLD[*Example](node)
assert.Nil(t, tkn)
require.EqualError(t, err, "failed to verify the token's signature")
@@ -158,18 +155,17 @@ func TestHash(t *testing.T) {
func TestInspect(t *testing.T) {
t.Parallel()
data := golden.Get(t, "example.dagcbor")
node, err := ipld.Decode(data, dagcbor.Decode)
node, err := ipld.Decode(exampleDagCbor, dagcbor.Decode)
require.NoError(t, err)
expSig, err := base64.RawStdEncoding.DecodeString("fPqfwL3iFpbw9SvBiq0DIbUurv9o6c36R08tC/yslGrJcwV51ghzWahxdetpEf6T5LCszXX9I/K8khvnmAxjAg")
expSig, err := base64.RawStdEncoding.DecodeString("+xUwgl/5VZcTxx6iePmkrIaZAlxuelHTbeQ5lQIgIV3ZgHS+Jf5BUERB0fvmFfiIfa5A3yMPfEA/7rswYsRRCg")
require.NoError(t, err)
info, err := envelope.Inspect(node)
require.NoError(t, err)
assert.Equal(t, expSig, info.Signature)
assert.Equal(t, "ucan/example@v1.0.0-rc.1", info.Tag)
assert.Equal(t, []byte{0x34, 0xed, 0x1, 0x71}, info.VarsigHeader)
assert.Equal(t, []byte{0x34, 0x1, 0xed, 0x1, 0xed, 0x1, 0x13, 0x71}, info.VarsigBytes)
}
func FuzzInspect(f *testing.F) {

View File

@@ -1 +1,2 @@
X@|úŸÀ½â–ðõ+ÁŠ­!µ.®ÿhéÍúGO- ü¬”jÉssY¨quëiþ“ä°¬Íuý#ò¼’ç˜ c¢ahD4íqxucan/example@v1.0.0-rc.1¢cissx8did:key:z6MkpuK2Amsu1RqcLGgmHHQHhvmeXCCBVsM4XFSg2cCyg4Nhehelloeworld
X@û0_ùU—Ç¢xù¤¬†™\nzQÓmä9• !]Ù€t¾%þAPDAÑûæøˆ}®@ß#|@?î»0bÄQ
¢ahH4ííqxucan/example@v1.0.0-rc.1¢cissx8did:key:z6MkuqvEtTW9L1E91CY3GmL83muetLAA2h8A5fUHjJgqq2Abehelloeworld

View File

@@ -1 +1 @@
[{"/":{"bytes":"fPqfwL3iFpbw9SvBiq0DIbUurv9o6c36R08tC/yslGrJcwV51ghzWahxdetpEf6T5LCszXX9I/K8khvnmAxjAg"}},{"h":{"/":{"bytes":"NO0BcQ"}},"ucan/example@v1.0.0-rc.1":{"hello":"world","iss":"did:key:z6MkpuK2Amsu1RqcLGgmHHQHhvmeXCCBVsM4XFSg2cCyg4Nh"}}]
[{"/":{"bytes":"+xUwgl/5VZcTxx6iePmkrIaZAlxuelHTbeQ5lQIgIV3ZgHS+Jf5BUERB0fvmFfiIfa5A3yMPfEA/7rswYsRRCg"}},{"h":{"/":{"bytes":"NAHtAe0BE3E"}},"ucan/example@v1.0.0-rc.1":{"hello":"world","iss":"did:key:z6MkuqvEtTW9L1E91CY3GmL83muetLAA2h8A5fUHjJgqq2Ab"}}]