2024-09-18 07:50:02 -04:00
|
|
|
// Package envelope provides functions that convert between wire-format
|
|
|
|
|
// encoding of a [UCAN] token's [Envelope] and the Go type representing
|
|
|
|
|
// 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
|
|
|
|
|
// cryptographic signature can be generated.
|
|
|
|
|
//
|
|
|
|
|
// Decoding functions in this package likewise perform the signature
|
|
|
|
|
// verification using a public key extracted from the TokenPayload as
|
2024-09-24 06:46:48 -04:00
|
|
|
// described by requirement two below.
|
2024-09-18 07:50:02 -04:00
|
|
|
//
|
|
|
|
|
// Types that wish to be marshaled and unmarshaled from the using
|
|
|
|
|
// is package have two requirements.
|
|
|
|
|
//
|
|
|
|
|
// 1. The type must implement the Tokener interface.
|
|
|
|
|
//
|
|
|
|
|
// 2. The IPLD Representation of the type must include an "iss"
|
|
|
|
|
// field when the TokenPayload is extracted from the Envelope.
|
|
|
|
|
// This field must contain the string representation of a
|
|
|
|
|
// "did:key" so that a public key can be extracted from the
|
|
|
|
|
//
|
2024-09-18 08:22:28 -04:00
|
|
|
// [Envelope]:https://github.com/ucan-wg/spec#envelope
|
|
|
|
|
// [TokenPayload]: https://github.com/ucan-wg/spec#envelope
|
2024-09-18 07:50:02 -04:00
|
|
|
// [UCAN]: https://ucan.xyz
|
|
|
|
|
package envelope
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"errors"
|
|
|
|
|
"io"
|
|
|
|
|
|
|
|
|
|
"github.com/ipld/go-ipld-prime"
|
|
|
|
|
"github.com/ipld/go-ipld-prime/codec"
|
|
|
|
|
"github.com/ipld/go-ipld-prime/codec/dagcbor"
|
|
|
|
|
"github.com/ipld/go-ipld-prime/codec/dagjson"
|
|
|
|
|
"github.com/ipld/go-ipld-prime/datamodel"
|
|
|
|
|
"github.com/ipld/go-ipld-prime/fluent/qp"
|
|
|
|
|
"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"
|
2024-09-24 13:03:35 +02:00
|
|
|
|
2024-09-18 07:50:02 -04:00
|
|
|
"github.com/ucan-wg/go-ucan/did"
|
|
|
|
|
"github.com/ucan-wg/go-ucan/internal/varsig"
|
|
|
|
|
)
|
|
|
|
|
|
2024-09-18 12:12:44 -04:00
|
|
|
const varsigHeaderKey = "h"
|
|
|
|
|
|
2024-09-18 08:22:28 -04:00
|
|
|
// Tokener must be implemented by types that wish to be enclosed in a
|
|
|
|
|
// UCAN Envelope (presumbably one of the UCAN token types).
|
2024-09-18 07:50:02 -04:00
|
|
|
type Tokener interface {
|
2024-09-18 08:22:28 -04:00
|
|
|
// Prototype provides the schema representation for an IPLD type so
|
|
|
|
|
// that the incoming datamodel.Kinds can be mapped to the appropriate
|
|
|
|
|
// schema.Kinds.
|
2024-09-18 07:50:02 -04:00
|
|
|
Prototype() schema.TypedPrototype
|
2024-09-18 08:22:28 -04:00
|
|
|
|
|
|
|
|
// Tag returns the expected key denoting the name of the IPLD node
|
|
|
|
|
// that should be processed as the token payload while decoding
|
|
|
|
|
// incoming bytes.
|
2024-09-18 07:50:02 -04:00
|
|
|
Tag() string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Decode unmarshals the input data using the format specified by the
|
|
|
|
|
// provided codec.Decoder into a Tokener.
|
|
|
|
|
//
|
|
|
|
|
// An error is returned if the conversion fails, or if the resulting
|
|
|
|
|
// Tokener is invalid.
|
2024-09-24 06:46:48 -04:00
|
|
|
func Decode[T Tokener](b []byte, decFn codec.Decoder) (T, error) {
|
2024-09-18 07:50:02 -04:00
|
|
|
node, err := ipld.Decode(b, decFn)
|
|
|
|
|
if err != nil {
|
2024-09-24 06:46:48 -04:00
|
|
|
return *new(T), err
|
2024-09-18 07:50:02 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return FromIPLD[T](node)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// DecodeReader is the same as Decode, but accept an io.Reader.
|
2024-09-24 06:46:48 -04:00
|
|
|
func DecodeReader[T Tokener](r io.Reader, decFn codec.Decoder) (T, error) {
|
2024-09-18 07:50:02 -04:00
|
|
|
node, err := ipld.DecodeStreaming(r, decFn)
|
|
|
|
|
if err != nil {
|
2024-09-24 06:46:48 -04:00
|
|
|
return *new(T), err
|
2024-09-18 07:50:02 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return FromIPLD[T](node)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// FromDagCbor unmarshals the input data into a Tokener.
|
|
|
|
|
//
|
|
|
|
|
// An error is returned if the conversion fails, or if the resulting
|
|
|
|
|
// Tokener is invalid.
|
2024-09-24 06:46:48 -04:00
|
|
|
func FromDagCbor[T Tokener](b []byte) (T, error) {
|
2024-09-19 13:29:33 -04:00
|
|
|
undef := *new(T)
|
|
|
|
|
|
|
|
|
|
node, err := ipld.Decode(b, dagcbor.Decode)
|
|
|
|
|
if err != nil {
|
2024-09-24 06:46:48 -04:00
|
|
|
return undef, err
|
2024-09-19 13:29:33 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tkn, err := fromIPLD[T](node)
|
|
|
|
|
if err != nil {
|
2024-09-24 06:46:48 -04:00
|
|
|
return undef, err
|
2024-09-19 13:29:33 -04:00
|
|
|
}
|
|
|
|
|
|
2024-09-24 06:46:48 -04:00
|
|
|
return tkn, nil
|
2024-09-18 07:50:02 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// FromDagCborReader is the same as FromDagCbor, but accept an io.Reader.
|
2024-09-24 06:46:48 -04:00
|
|
|
func FromDagCborReader[T Tokener](r io.Reader) (T, error) {
|
2024-09-18 07:50:02 -04:00
|
|
|
return DecodeReader[T](r, dagcbor.Decode)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// FromDagJson unmarshals the input data into a Tokener.
|
|
|
|
|
//
|
|
|
|
|
// An error is returned if the conversion fails, or if the resulting
|
|
|
|
|
// Tokener is invalid.
|
2024-09-24 06:46:48 -04:00
|
|
|
func FromDagJson[T Tokener](b []byte) (T, error) {
|
2024-09-18 07:50:02 -04:00
|
|
|
return Decode[T](b, dagjson.Decode)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// FromDagJsonReader is the same as FromDagJson, but accept an io.Reader.
|
2024-09-24 06:46:48 -04:00
|
|
|
func FromDagJsonReader[T Tokener](r io.Reader) (T, error) {
|
2024-09-18 07:50:02 -04:00
|
|
|
return DecodeReader[T](r, dagjson.Decode)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// FromIPLD unwraps a Tokener from the provided IPLD datamodel.Node.
|
|
|
|
|
//
|
|
|
|
|
// An error is returned if the conversion fails, or if the resulting
|
|
|
|
|
// Tokener is invalid.
|
2024-09-24 06:46:48 -04:00
|
|
|
func FromIPLD[T Tokener](node datamodel.Node) (T, error) {
|
2024-09-18 07:50:02 -04:00
|
|
|
undef := *new(T)
|
|
|
|
|
|
2024-09-19 13:29:33 -04:00
|
|
|
tkn, err := fromIPLD[T](node)
|
2024-09-18 07:50:02 -04:00
|
|
|
if err != nil {
|
2024-09-24 06:46:48 -04:00
|
|
|
return undef, err
|
2024-09-18 07:50:02 -04:00
|
|
|
}
|
|
|
|
|
|
2024-09-24 06:46:48 -04:00
|
|
|
return tkn, nil
|
2024-09-19 13:29:33 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func fromIPLD[T Tokener](node datamodel.Node) (T, error) {
|
|
|
|
|
undef := *new(T)
|
|
|
|
|
|
|
|
|
|
signatureNode, err := node.LookupByIndex(0)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return undef, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
signature, err := signatureNode.AsBytes()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return undef, err
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-18 07:50:02 -04:00
|
|
|
sigPayloadNode, err := node.LookupByIndex(1)
|
|
|
|
|
if err != nil {
|
2024-09-19 13:29:33 -04:00
|
|
|
return undef, err
|
2024-09-18 07:50:02 -04:00
|
|
|
}
|
|
|
|
|
|
2024-09-18 12:12:44 -04:00
|
|
|
varsigHeaderNode, err := sigPayloadNode.LookupByString(varsigHeaderKey)
|
|
|
|
|
if err != nil {
|
2024-09-19 13:29:33 -04:00
|
|
|
return undef, err
|
2024-09-18 07:50:02 -04:00
|
|
|
}
|
|
|
|
|
|
2024-09-18 12:12:44 -04:00
|
|
|
tokenPayloadNode, err := sigPayloadNode.LookupByString(undef.Tag())
|
|
|
|
|
if err != nil {
|
2024-09-19 13:29:33 -04:00
|
|
|
return undef, err
|
2024-09-18 07:50:02 -04:00
|
|
|
}
|
|
|
|
|
|
2024-09-24 13:03:35 +02:00
|
|
|
// This needs to be done before converting this node to its schema
|
2024-09-18 07:50:02 -04:00
|
|
|
// representation (afterwards, the field might be renamed os it's safer
|
|
|
|
|
// to use the wire name).
|
|
|
|
|
issuerNode, err := tokenPayloadNode.LookupByString("iss")
|
|
|
|
|
if err != nil {
|
2024-09-19 13:29:33 -04:00
|
|
|
return undef, err
|
2024-09-18 07:50:02 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Replaces the datamodel.Node in tokenPayloadNode with a
|
|
|
|
|
// schema.TypedNode so that we can cast it to a *token.Token after
|
|
|
|
|
// unwrapping it.
|
|
|
|
|
nb := undef.Prototype().Representation().NewBuilder()
|
|
|
|
|
|
|
|
|
|
err = nb.AssignNode(tokenPayloadNode)
|
|
|
|
|
if err != nil {
|
2024-09-19 13:29:33 -04:00
|
|
|
return undef, err
|
2024-09-18 07:50:02 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tokenPayloadNode = nb.Build()
|
|
|
|
|
|
|
|
|
|
tokenPayload := bindnode.Unwrap(tokenPayloadNode)
|
|
|
|
|
if tokenPayload == nil {
|
2024-09-19 13:29:33 -04:00
|
|
|
return undef, errors.New("failed to Unwrap the TokenPayload")
|
2024-09-18 07:50:02 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tkn, ok := tokenPayload.(T)
|
|
|
|
|
if !ok {
|
2024-09-19 13:29:33 -04:00
|
|
|
return undef, errors.New("failed to assert the TokenPayload type as *token.Token")
|
2024-09-18 07:50:02 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check that the issuer's DID contains a public key with a type that
|
|
|
|
|
// matches the VarsigHeader and then verify the SigPayload.
|
|
|
|
|
issuer, err := issuerNode.AsString()
|
|
|
|
|
if err != nil {
|
2024-09-19 13:29:33 -04:00
|
|
|
return undef, err
|
2024-09-18 07:50:02 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
issuerDID, err := did.Parse(issuer)
|
|
|
|
|
if err != nil {
|
2024-09-19 13:29:33 -04:00
|
|
|
return undef, err
|
2024-09-18 07:50:02 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
issuerPubKey, err := issuerDID.PubKey()
|
|
|
|
|
if err != nil {
|
2024-09-19 13:29:33 -04:00
|
|
|
return undef, err
|
2024-09-18 07:50:02 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
issuerVarsigHeader, err := varsig.Encode(issuerPubKey.Type())
|
|
|
|
|
if err != nil {
|
2024-09-19 13:29:33 -04:00
|
|
|
return undef, err
|
2024-09-18 07:50:02 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
varsigHeader, err := varsigHeaderNode.AsBytes()
|
|
|
|
|
if err != nil {
|
2024-09-19 13:29:33 -04:00
|
|
|
return undef, err
|
2024-09-18 07:50:02 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if string(varsigHeader) != string(issuerVarsigHeader) {
|
2024-09-19 13:29:33 -04:00
|
|
|
return undef, errors.New("the VarsigHeader key type doesn't match the issuer's key type")
|
2024-09-18 07:50:02 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
data, err := ipld.Encode(sigPayloadNode, dagcbor.Encode)
|
|
|
|
|
if err != nil {
|
2024-09-19 13:29:33 -04:00
|
|
|
return undef, err
|
2024-09-18 07:50:02 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ok, err = issuerPubKey.Verify(data, signature)
|
|
|
|
|
if err != nil || !ok {
|
2024-09-19 13:29:33 -04:00
|
|
|
return undef, errors.New("failed to verify the token's signature")
|
2024-09-18 07:50:02 -04:00
|
|
|
}
|
|
|
|
|
|
2024-09-19 13:29:33 -04:00
|
|
|
return tkn, nil
|
2024-09-18 07:50:02 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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) {
|
|
|
|
|
node, err := ToIPLD(privKey, token)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ipld.Encode(node, encFn)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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 {
|
|
|
|
|
node, err := ToIPLD(privKey, token)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ipld.EncodeStreaming(w, node, encFn)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ToDagCbor marshals the Tokener to the DAG-CBOR format.
|
|
|
|
|
func ToDagCbor(privKey crypto.PrivKey, 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 {
|
|
|
|
|
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) {
|
|
|
|
|
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 {
|
|
|
|
|
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) {
|
|
|
|
|
tokenPayloadNode := bindnode.Wrap(token, token.Prototype().Type()).Representation()
|
|
|
|
|
|
|
|
|
|
varsigHeader, err := varsig.Encode(privKey.Type())
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sigPayloadNode, err := qp.BuildMap(basicnode.Prototype.Any, 2, func(ma datamodel.MapAssembler) {
|
2024-09-18 12:12:44 -04:00
|
|
|
qp.MapEntry(ma, varsigHeaderKey, qp.Bytes(varsigHeader))
|
2024-09-18 07:50:02 -04:00
|
|
|
qp.MapEntry(ma, token.Tag(), qp.Node(tokenPayloadNode))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
data, err := ipld.Encode(sigPayloadNode, dagcbor.Encode)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
signature, err := privKey.Sign(data)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return qp.BuildList(basicnode.Prototype.Any, 2, func(la datamodel.ListAssembler) {
|
|
|
|
|
qp.ListEntry(la, qp.Bytes(signature))
|
|
|
|
|
qp.ListEntry(la, qp.Node(sigPayloadNode))
|
|
|
|
|
})
|
|
|
|
|
}
|