Files
anthonyrawlins 9bdcbe0447 Integrate BACKBEAT SDK and resolve KACHING license validation
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>
2025-09-06 07:56:26 +10:00

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)