Skip to content
This repository has been archived by the owner on Jun 20, 2023. It is now read-only.

Commit

Permalink
Add support for extensible records (and v2 signature)
Browse files Browse the repository at this point in the history
  • Loading branch information
aschmahmann committed May 11, 2021
1 parent 5976a80 commit 3deb032
Show file tree
Hide file tree
Showing 9 changed files with 912 additions and 92 deletions.
2 changes: 1 addition & 1 deletion examples/embed.go
Expand Up @@ -15,7 +15,7 @@ import (
func CreateEntryWithEmbed(ipfsPath string, publicKey crypto.PubKey, privateKey crypto.PrivKey) (*pb.IpnsEntry, error) {
ipfsPathByte := []byte(ipfsPath)
eol := time.Now().Add(time.Hour * 48)
entry, err := ipns.Create(privateKey, ipfsPathByte, 1, eol)
entry, err := ipns.Create(privateKey, ipfsPathByte, 1, eol, 0)
if err != nil {
return nil, err
}
Expand Down
5 changes: 4 additions & 1 deletion go.mod
@@ -1,12 +1,15 @@
module github.com/ipfs/go-ipns

require (
github.com/gogo/protobuf v1.3.1
github.com/gogo/protobuf v1.3.2
github.com/ipfs/go-ipfs-util v0.0.1
github.com/ipfs/go-log v0.0.1
github.com/ipld/go-ipld-prime v0.9.0
github.com/libp2p/go-libp2p-core v0.2.5
github.com/libp2p/go-libp2p-peerstore v0.1.4
github.com/libp2p/go-libp2p-record v0.1.2
github.com/multiformats/go-multicodec v0.2.0
github.com/pkg/errors v0.8.1
)

go 1.12
86 changes: 64 additions & 22 deletions go.sum

Large diffs are not rendered by default.

236 changes: 230 additions & 6 deletions ipns.go
Expand Up @@ -3,20 +3,40 @@ package ipns
import (
"bytes"
"fmt"
"sort"
"time"

"github.com/pkg/errors"

"github.com/ipld/go-ipld-prime"
_ "github.com/ipld/go-ipld-prime/codec/dagcbor" // used to import the DagCbor encoder/decoder
ipldcodec "github.com/ipld/go-ipld-prime/multicodec"
"github.com/ipld/go-ipld-prime/node/basic"

"github.com/multiformats/go-multicodec"

"github.com/gogo/protobuf/proto"

pb "github.com/ipfs/go-ipns/pb"

u "github.com/ipfs/go-ipfs-util"
ic "github.com/libp2p/go-libp2p-core/crypto"
peer "github.com/libp2p/go-libp2p-core/peer"
)

const (
validity = "Validity"
validityType = "ValidityType"
value = "Value"
sequence = "Sequence"
ttl = "TTL"
)

// Create creates a new IPNS entry and signs it with the given private key.
//
// This function does not embed the public key. If you want to do that, use
// `EmbedPublicKey`.
func Create(sk ic.PrivKey, val []byte, seq uint64, eol time.Time) (*pb.IpnsEntry, error) {
func Create(sk ic.PrivKey, val []byte, seq uint64, eol time.Time, ttl time.Duration) (*pb.IpnsEntry, error) {
entry := new(pb.IpnsEntry)

entry.Value = val
Expand All @@ -25,20 +45,112 @@ func Create(sk ic.PrivKey, val []byte, seq uint64, eol time.Time) (*pb.IpnsEntry
entry.Sequence = &seq
entry.Validity = []byte(u.FormatRFC3339(eol))

sig, err := sk.Sign(ipnsEntryDataForSig(entry))
ttlNs := uint64(ttl.Nanoseconds())
entry.Ttl = proto.Uint64(ttlNs)

cborData, err := createCborDataForIpnsEntry(entry)
if err != nil {
return nil, err
}
entry.Data = cborData

sig1, err := sk.Sign(ipnsEntryDataForSigV1(entry))
if err != nil {
return nil, errors.Wrap(err, "could not compute signature data")
}
entry.SignatureV1 = sig1

sig2Data, err := ipnsEntryDataForSigV2(entry)
if err != nil {
return nil, err
}
sig2, err := sk.Sign(sig2Data)
if err != nil {
return nil, err
}
entry.Signature = sig
entry.SignatureV2 = sig2

return entry, nil
}

func createCborDataForIpnsEntry(e *pb.IpnsEntry) ([]byte, error) {
m := make(map[string]ipld.Node)
var keys []string
m[value] = basicnode.NewBytes(e.GetValue())
keys = append(keys, value)

m[validity] = basicnode.NewBytes(e.GetValidity())
keys = append(keys, validity)

m[validityType] = basicnode.NewInt(int64(e.GetValidityType()))
keys = append(keys, validityType)

m[sequence] = basicnode.NewInt(int64(e.GetSequence()))
keys = append(keys, sequence)

m[ttl] = basicnode.NewInt(int64(e.GetTtl()))
keys = append(keys, ttl)

sort.Sort(cborMapKeyString_RFC7049(keys))

newNd := basicnode.Prototype__Map{}.NewBuilder()
ma, err := newNd.BeginMap(int64(len(keys)))
if err != nil {
return nil, err
}

for _, k := range keys {
if err := ma.AssembleKey().AssignString(k); err != nil {
return nil, err
}
if err := ma.AssembleValue().AssignNode(m[k]); err != nil {
return nil, err
}
}

if err := ma.Finish(); err != nil {
return nil, err
}

nd := newNd.Build()

enc, err := ipldcodec.LookupEncoder(uint64(multicodec.DagCbor))
if err != nil {
return nil, err
}

buf := new(bytes.Buffer)
if err := enc(nd, buf); err != nil {
return nil, err
}

return buf.Bytes(), nil
}

// Validates validates the given IPNS entry against the given public key.
func Validate(pk ic.PubKey, entry *pb.IpnsEntry) error {
// Check the ipns record signature with the public key
if ok, err := pk.Verify(ipnsEntryDataForSig(entry), entry.GetSignature()); err != nil || !ok {
return ErrSignature

// Check v2 signature if it's available, otherwise use the v1 signature
if entry.GetSignatureV2() != nil {
sig2Data, err := ipnsEntryDataForSigV2(entry)
if err != nil {
return fmt.Errorf("could not compute signature data: %w", err)
}
if ok, err := pk.Verify(sig2Data, entry.GetSignatureV2()); err != nil || !ok {
return ErrSignature
}

// TODO: If we switch from pb.IpnsEntry to a more generic IpnsRecord type then perhaps we should only check
// this if there is no v1 signature. In the meanwhile this helps avoid some potential rough edges around people
// checking the entry fields instead of doing CBOR decoding everywhere.
if err := validateCborDataMatchesPbData(entry); err != nil {
return err
}
} else {
if ok, err := pk.Verify(ipnsEntryDataForSigV1(entry), entry.GetSignatureV1()); err != nil || !ok {
return ErrSignature
}
}

eol, err := GetEOL(entry)
Expand All @@ -51,6 +163,87 @@ func Validate(pk ic.PubKey, entry *pb.IpnsEntry) error {
return nil
}

// TODO: Most of this function could probably be replaced with codegen
func validateCborDataMatchesPbData(entry *pb.IpnsEntry) error {
if len(entry.GetData()) == 0 {
return fmt.Errorf("record data is missing")
}

dec, err := ipldcodec.LookupDecoder(uint64(multicodec.DagCbor))
if err != nil {
return err
}

ndbuilder := basicnode.Prototype__Map{}.NewBuilder()
if err := dec(ndbuilder, bytes.NewReader(entry.GetData())); err != nil {
return err
}

fullNd := ndbuilder.Build()
nd, err := fullNd.LookupByString(value)
if err != nil {
return err
}
ndBytes, err := nd.AsBytes()
if err != nil {
return err
}
if !bytes.Equal(entry.GetValue(), ndBytes) {
return fmt.Errorf("field \"%v\" did not match between protobuf and CBOR", value)
}

nd, err = fullNd.LookupByString(validity)
if err != nil {
return err
}
ndBytes, err = nd.AsBytes()
if err != nil {
return err
}
if !bytes.Equal(entry.GetValidity(), ndBytes) {
return fmt.Errorf("field \"%v\" did not match between protobuf and CBOR", validity)
}

nd, err = fullNd.LookupByString(validityType)
if err != nil {
return err
}
ndInt, err := nd.AsInt()
if err != nil {
return err
}
if int64(entry.GetValidityType()) != ndInt {
return fmt.Errorf("field \"%v\" did not match between protobuf and CBOR", validityType)
}

nd, err = fullNd.LookupByString(sequence)
if err != nil {
return err
}
ndInt, err = nd.AsInt()
if err != nil {
return err
}

if entry.GetSequence() != uint64(ndInt) {
return fmt.Errorf("field \"%v\" did not match between protobuf and CBOR", sequence)
}

nd, err = fullNd.LookupByString("TTL")
if err != nil {
return err
}
ndInt, err = nd.AsInt()
if err != nil {
return err
}
if entry.GetTtl() != uint64(ndInt) {
return fmt.Errorf("field \"%v\" did not match between protobuf and CBOR", ttl)
}

return nil
}

// GetEOL returns the EOL of this IPNS entry
//
// This function returns ErrUnrecognizedValidity if the validity type of the
Expand Down Expand Up @@ -130,6 +323,16 @@ func ExtractPublicKey(pid peer.ID, entry *pb.IpnsEntry) (ic.PubKey, error) {
// `bytes.Compare`). You must do this if you are implementing a libp2p record
// validator (or you can just use the one provided for you by this package).
func Compare(a, b *pb.IpnsEntry) (int, error) {
aHasV2Sig := a.GetSignatureV2() != nil
bHasV2Sig := b.GetSignatureV2() != nil

// Having a newer signature version is better than an older signature version
if aHasV2Sig && !bHasV2Sig {
return 1, nil
} else if !aHasV2Sig && bHasV2Sig {
return -1, nil
}

as := a.GetSequence()
bs := b.GetSequence()

Expand Down Expand Up @@ -158,11 +361,32 @@ func Compare(a, b *pb.IpnsEntry) (int, error) {
return 0, nil
}

func ipnsEntryDataForSig(e *pb.IpnsEntry) []byte {
func ipnsEntryDataForSigV1(e *pb.IpnsEntry) []byte {
return bytes.Join([][]byte{
e.Value,
e.Validity,
[]byte(fmt.Sprint(e.GetValidityType())),
},
[]byte{})
}

func ipnsEntryDataForSigV2(e *pb.IpnsEntry) ([]byte, error) {
dataForSig := []byte("ipns-signature:")
dataForSig = append(dataForSig, e.Data...)

return dataForSig, nil
}

type cborMapKeyString_RFC7049 []string

func (x cborMapKeyString_RFC7049) Len() int { return len(x) }
func (x cborMapKeyString_RFC7049) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
func (x cborMapKeyString_RFC7049) Less(i, j int) bool {
li, lj := len(x[i]), len(x[j])
if li == lj {
return x[i] < x[j]
}
return li < lj
}

var _ sort.Interface = (cborMapKeyString_RFC7049)(nil)
4 changes: 2 additions & 2 deletions ipns_test.go
Expand Up @@ -23,7 +23,7 @@ func TestEmbedPublicKey(t *testing.T) {
t.Fatal(err)
}

e, err := Create(priv, []byte("/a/b"), 0, time.Now().Add(1*time.Hour))
e, err := Create(priv, []byte("/a/b"), 0, time.Now().Add(1*time.Hour), 0)
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -54,7 +54,7 @@ func ExampleCreate() {

// Create an IPNS record that expires in one hour and points to the IPFS address
// /ipfs/Qme1knMqwt1hKZbc1BmQFmnm9f36nyQGwXxPGVpVJ9rMK5
ipnsRecord, err := Create(privateKey, []byte("/ipfs/Qme1knMqwt1hKZbc1BmQFmnm9f36nyQGwXxPGVpVJ9rMK5"), 0, time.Now().Add(1*time.Hour))
ipnsRecord, err := Create(privateKey, []byte("/ipfs/Qme1knMqwt1hKZbc1BmQFmnm9f36nyQGwXxPGVpVJ9rMK5"), 0, time.Now().Add(1*time.Hour), 0)
if err != nil {
panic(err)
}
Expand Down

0 comments on commit 3deb032

Please sign in to comment.