Major integrations and fixes: - Added BACKBEAT SDK integration for P2P operation timing - Implemented beat-aware status tracking for distributed operations - Added Docker secrets support for secure license management - Resolved KACHING license validation via HTTPS/TLS - Updated docker-compose configuration for clean stack deployment - Disabled rollback policies to prevent deployment failures - Added license credential storage (CHORUS-DEV-MULTI-001) Technical improvements: - BACKBEAT P2P operation tracking with phase management - Enhanced configuration system with file-based secrets - Improved error handling for license validation - Clean separation of KACHING and CHORUS deployment stacks 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
420 lines
10 KiB
Go
420 lines
10 KiB
Go
package ipns
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"sort"
|
|
"time"
|
|
|
|
"github.com/multiformats/go-multicodec"
|
|
"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"
|
|
basicnode "github.com/ipld/go-ipld-prime/node/basic"
|
|
|
|
"github.com/gogo/protobuf/proto"
|
|
|
|
pb "github.com/ipfs/boxo/ipns/pb"
|
|
|
|
u "github.com/ipfs/boxo/util"
|
|
ic "github.com/libp2p/go-libp2p/core/crypto"
|
|
"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, ttl time.Duration) (*pb.IpnsEntry, error) {
|
|
entry := new(pb.IpnsEntry)
|
|
|
|
entry.Value = val
|
|
typ := pb.IpnsEntry_EOL
|
|
entry.ValidityType = &typ
|
|
entry.Sequence = &seq
|
|
entry.Validity = []byte(u.FormatRFC3339(eol))
|
|
|
|
ttlNs := uint64(ttl.Nanoseconds())
|
|
entry.Ttl = proto.Uint64(ttlNs)
|
|
|
|
cborData, err := createCborDataForIpnsEntry(entry)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
entry.Data = cborData
|
|
|
|
// For now we still create V1 signatures. These are deprecated, and not
|
|
// used during verification anymore (Validate func requires SignatureV2),
|
|
// but setting it here allows legacy nodes (e.g., go-ipfs < v0.9.0) to
|
|
// still resolve IPNS published by modern nodes.
|
|
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.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
|
|
}
|
|
|
|
// ValidateWithPeerID validates the given IPNS entry against the given peer ID.
|
|
func ValidateWithPeerID(pid peer.ID, entry *pb.IpnsEntry) error {
|
|
pk, err := ExtractPublicKey(pid, entry)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return Validate(pk, entry)
|
|
}
|
|
|
|
// Validates validates the given IPNS entry against the given public key.
|
|
func Validate(pk ic.PubKey, entry *pb.IpnsEntry) error {
|
|
// Make sure max size is respected
|
|
if entry.Size() > MaxRecordSize {
|
|
return ErrRecordSize
|
|
}
|
|
|
|
// Check the ipns record signature with the public key
|
|
if entry.GetSignatureV2() == nil {
|
|
// always error if no valid signature could be found
|
|
return ErrSignature
|
|
}
|
|
|
|
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.
|
|
// See https://github.com/ipfs/boxo/ipns/pull/42 for next steps here
|
|
if err := validateCborDataMatchesPbData(entry); err != nil {
|
|
return err
|
|
}
|
|
|
|
eol, err := GetEOL(entry)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if time.Now().After(eol) {
|
|
return ErrExpiredRecord
|
|
}
|
|
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
|
|
// record isn't EOL. Otherwise, it returns an error if it can't parse the EOL.
|
|
func GetEOL(entry *pb.IpnsEntry) (time.Time, error) {
|
|
if entry.GetValidityType() != pb.IpnsEntry_EOL {
|
|
return time.Time{}, ErrUnrecognizedValidity
|
|
}
|
|
return u.ParseRFC3339(string(entry.GetValidity()))
|
|
}
|
|
|
|
// EmbedPublicKey embeds the given public key in the given ipns entry. While not
|
|
// strictly required, some nodes (e.g., DHT servers) may reject IPNS entries
|
|
// that don't embed their public keys as they may not be able to validate them
|
|
// efficiently.
|
|
func EmbedPublicKey(pk ic.PubKey, entry *pb.IpnsEntry) error {
|
|
// Try extracting the public key from the ID. If we can, *don't* embed
|
|
// it.
|
|
id, err := peer.IDFromPublicKey(pk)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := id.ExtractPublicKey(); err != peer.ErrNoPublicKey {
|
|
// Either a *real* error or nil.
|
|
return err
|
|
}
|
|
|
|
// We failed to extract the public key from the peer ID, embed it in the
|
|
// record.
|
|
pkBytes, err := ic.MarshalPublicKey(pk)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
entry.PubKey = pkBytes
|
|
return nil
|
|
}
|
|
|
|
// UnmarshalIpnsEntry unmarshalls an IPNS entry from a slice of bytes.
|
|
func UnmarshalIpnsEntry(data []byte) (*pb.IpnsEntry, error) {
|
|
var entry pb.IpnsEntry
|
|
err := proto.Unmarshal(data, &entry)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &entry, nil
|
|
}
|
|
|
|
// ExtractPublicKey extracts a public key matching `pid` from the IPNS record,
|
|
// if possible.
|
|
//
|
|
// This function returns (nil, nil) when no public key can be extracted and
|
|
// nothing is malformed.
|
|
func ExtractPublicKey(pid peer.ID, entry *pb.IpnsEntry) (ic.PubKey, error) {
|
|
if entry.PubKey != nil {
|
|
pk, err := ic.UnmarshalPublicKey(entry.PubKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unmarshaling pubkey in record: %s", err)
|
|
}
|
|
|
|
expPid, err := peer.IDFromPublicKey(pk)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not regenerate peerID from pubkey: %s", err)
|
|
}
|
|
|
|
if pid != expPid {
|
|
return nil, ErrPublicKeyMismatch
|
|
}
|
|
return pk, nil
|
|
}
|
|
|
|
return pid.ExtractPublicKey()
|
|
}
|
|
|
|
// Compare compares two IPNS entries. It returns:
|
|
//
|
|
// * -1 if a is older than b
|
|
// * 0 if a and b cannot be ordered (this doesn't mean that they are equal)
|
|
// * +1 if a is newer than b
|
|
//
|
|
// It returns an error when either a or b are malformed.
|
|
//
|
|
// NOTE: It *does not* validate the records, the caller is responsible for calling
|
|
// `Validate` first.
|
|
//
|
|
// NOTE: If a and b cannot be ordered by this function, you can determine their
|
|
// order by comparing their serialized byte representations (using
|
|
// `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()
|
|
|
|
if as > bs {
|
|
return 1, nil
|
|
} else if as < bs {
|
|
return -1, nil
|
|
}
|
|
|
|
at, err := u.ParseRFC3339(string(a.GetValidity()))
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
bt, err := u.ParseRFC3339(string(b.GetValidity()))
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
if at.After(bt) {
|
|
return 1, nil
|
|
} else if bt.After(at) {
|
|
return -1, nil
|
|
}
|
|
|
|
return 0, nil
|
|
}
|
|
|
|
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)
|