Memory and Thought Alex's home on the web

DAG JOSE week one update

This has been the first week I’ve worked on the dag-jose implementation for Go (which I introduced here). I’ve been focusing on exploring the go-ipld-prime codebase and building a proof of concept implementation of the dag-jose format. I’ve build a basic library which reads JWS’s written by the javascript implementation and verifies them, and writes JWS’s which the javascript implementation is able to read and verify.

This proof of concept implementation can be found in this repo. The README and comments there go into more detail if you’re interested.

I’m going to briefly walk through the API which looks most promising, this will necessarily include some explanation of what go-ipld-prime is and how it works. Note that I am still getting to grips with the go-ipld-prime codebase so if something looks incorrect or weird it is most likely a mistake on my part.

Registering format encoders and decoders

Encoders and decoders are registered in a global registry in the cidlink package. We register the dag-jose encoder and decoder in the init function of the dagjose module. That means that you just nead to import dagjose somewhere in your program to get the encoder and decoder registered.

In practice the encoder and decoder are effectively the same as the dag-cbor encoder and decoder, most of the work in this library is in implementing ipld.NodeBuilder and ipld.Node.

Reading JWS objects

go-ipld-prime is where all new development on IPLD in Go is happening. It’s completely agnostic about how you retrieve data, the API revolves around creating a ipld.Link and then calling Link.Load(..) to retrieve the ipld.Node the link points to; in practice the link will be a CID. Link.Load takes an ipld.NodeBuilder as an argument, this is an interface which knows how to create an ipld.Node from the data the Link points to. This means that the type of NodeBuilder you pass to Load determines the underlying type of Node you will get back.

Given all this there are two extension points we will need to provide in order to load JWS objects. We will need some data structure which implements ipld.Node to return from the Link.Load(..) and we will need an implementation of ipld.NodeBuilder which creates that Node implementation. To make this concrete, this is what that API currently looks like:

import (
    "dagjoseroundtrip/dagjose" // Name of experimental dagjose package, importing registers the encoder and decoder
    cidlink "github.com/ipld/go-ipld-prime/linking/cid"
)
// Here we're creating a `CID` which points to a JWS written with the javascript implementation
jwsCid, err := cid.Decode(
    "bagcqcerafzecmo67npzj56gfyukh3tbwxywgfsmxirr4oq57s6sddou4p5dq"
)
if err != nil {
    panic(err)
}
// cidlink.Link is an implementation of `ipld.Link` backed by a CID
jwsLnk := cidlink.Link{Cid: jwsCid}


// This is the `NodeBuilder` which knows how to build a `dagjose.DagJOSE` object
builder := dagjose.NewBuilder()
err = lnk.Load(
    context.Background(),
    ipld.LinkContext{},
    builder,
    <an implementation of ipld.Loader, which knows how to get the block data from IPFS>,
)
if err != nil {
    panic(err)
}
n := builder.Build()
jwsNode := n.(*dagjose.DagJOSE), nil

This dagjose.DagJOSE object currently provides a single method: GeneralJSONSerialization which returns the JSON serialization of the JOSE object as a string, this can be used to interface with the go-jose library, like so:

privateKey := <obtain an instance of gojose.PrivateKey>
sig, err := gojose.ParseSigned(jwsNode.GeneralJSONSerialization())
if err != nil {
    panic(err)
}
verifiedPayload, err := sig.Verify(privateKey.Public())
if err != nil {
    panic(err)
}

Writing JWS objects

Writing JWS objects is handled almost entirely by the ipld.Node implementation. ipld.Node is a general interface which describes what any datum in the IPLD data model can look like. By implementing ipld.Node for dagjose.DagJOSE we can then just pass the dagjose.DagJOSE to the dag-cbor encoder, which knows how to write an ipld.Node out to CBOR. This means that to write a JWS you do this:

dagJws, err := dagjose.NewDagJWS("<the general JSON serialization of a JWS>")
if err != nil {
    panic(err)
}
linkBuilder := cidlink.LinkBuilder{Prefix: cid.Prefix{
    Version:  1,    // Usually '1'.
    Codec:    0x85, // 0x71 means "dag-jose" -- See the multicodecs table: https://github.com/multiformats/multicodec/
    MhType:   0x15, // 0x15 means "sha3-384" -- See the multicodecs table: https://github.com/multiformats/multicodec/
    MhLength: 48,   // sha3-224 hash has a 48-byte sum.
}}
link, err := linkBuilder.Build(
    context.Background(),
    ipld.LinkContext{},
    dagJws,
    <an implementation of `ipld.Storer` which knows how to store the raw block data>
)
if err != nil {
    panic(err)
}

By specifying 0x85 as the multicodec of the link we are creating we tell cidlink that it needs to use the encoder we have registered, and the encoder we have registered just passes through to the dag-cbor encoder, which works because we have implemented ipld.Node for the dagjws.DagJOSE struct.

Next Steps

I’ve got a working interop test with the javascript codebase, but currently the implementation is very exploratory and contains several hacks. The next step is to pull this exploratory code together into a proper library. We also need to update the specification to reflect some decisions we have made about exactly how to represent JOSE objects in IPLD, I will be working to get those updates included in the specification PR

That’s all for now!