CBOR / pq cryptography migration / software architecture golang question

124 views
Skip to first unread message

David Stainton

unread,
Feb 10, 2023, 6:19:55 PM2/10/23
to golang-nuts

Greetings people of golang, cryptographers, software architects,

Hi. I'm a Katzenpost developer, it's a software project, not a legal entity: https://github.com/katzenpost

We use the golang CBOR library: github.com/fxamacker/cbor
Works great!

Jumping right into the nitty gritty here... We have a "directory authority" protocol (similar to Tor's directory authority protocol) which votes and publishes a "network view" document which contains many MixDescriptor objects, one for each network node (known as a mix).
Each mix published several mix keys for future Epochs in their MixDescriptor. So we have a map of these mix keys, like so:


However, currently the map of keys only stores X25519 keys. But ideally we'd like the MixDescriptor struct to be more accommodating to migrating to hybrid post quantum keys.

How might we accomplish this?
Acceptable solutions will not create intermediary types to represent the entire MixDescriptor... but there should be no objections to intermediary types for representing a single field of the MixDescriptor; like a wrapper type for the map of mix keys?

Also... can I just point out now that two of the MixDescriptor fields are interface types representing two different kinds of cryptographic public key, yet we manage to CBOR unmarshal everything just fine without an intermediary type, with this trick here:

https://github.com/katzenpost/katzenpost/blob/main/core/pki/descriptor.go#L123-L127

So we initialize those two fields with valid empty types that implement those interfaces. Fine. But that won't work for a map of interface types, right?

Let's come back to this question soon, and now take a brief digression to discuss katzenpost architecture design:

These mix keys are used by a nested encrypted packet format known as Sphinx (here's the paper https://www.cypherpunks.ca/~iang/pubs/Sphinx_Oakland09.pdf ). Long story short, our Sphinx can use any NIKE (non-interactive key exchange e.g. X25519) and any KEM (key encapsulation mechanism):


Backing up a bit, our "directory authority" publishes a document of this type:

which contains many MixDescriptor objects for the entire network:

Currently all network components use Sphinx with a geometry that is hardcoded. However I've got a work-in-progress branch where I'm changing things to distribute the Sphinx geometry in the directory authority document:

https://github.com/katzenpost/katzenpost/blob/sphinx_geo_pki/core/sphinx/geo/geo.go#L31-L89


The Sphinx Geometry describes it's KEM or NIKE:

So I guess my question is: How shall I marshal/unmarshal a map of public keys where the keys themselves can be any type that satisfies our NIKE interface or KEM inteface?

It would be silly to put a bunch of concrete key maps into MixDescriptor. There will always be more KEMs and NIKEs.

It sounds like I need change the MixKeys field of the MixDescriptor to use a wrapper type instead of a map directly. The wrapper type would have it's own UnmarshalBinary/MarshalBinary (so that cbor.Marshal/Unmarshal can be called) which would represent the map as a list of two tuples: key, value. Easy to convert it to a map.

What do you think of this approach?

type KeyValue struct {
        Key uint64
        Value mixkey.PublicKey
}

...blah blah...

fubar := make([]KeyValue, 123)
cbor.Unmarshal(blob, fubar)

But can this work if mixkey.PublicKey is an interface? Nope. I don't think that works because the nil value of KeyValue struct will have a nil for it's Value field.

Any suggestions?

Cheers,

David

David Stainton

unread,
Feb 10, 2023, 9:42:22 PM2/10/23
to golang-nuts
Upon further reflection I think the simplest solution is to have MixDescriptor represent the maps of mix keys in raw bytes and use a method to unmarshal them when needed.
The unmarshal method could use CBOR tags to figure out which type to unmarshal into.... but it would be easier to implement this where a Sphinx Geometry object is an argument to the method which does the unmarshaling; and using that Sphinx Geometry to determine which concrete cryptographic key type should be used.

eric lagergren

unread,
Feb 11, 2023, 7:45:08 PM2/11/23
to golang-nuts

It’s not super pretty, but a trick I’ve used is

type T struct { data any }
func (t *T) UnmarshalBinary(…) error {
    // decode and assign to data
}

Now T is basically an enum and you can switch on T.data ~similar to Rust’s match or whatever. Then you’d have map[uint64]T.

- eric

Ps: hope you’re doing well!
Reply all
Reply to author
Forward
0 new messages