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>
This commit is contained in:
anthonyrawlins
2025-09-06 07:56:26 +10:00
parent 543ab216f9
commit 9bdcbe0447
4730 changed files with 1480093 additions and 1916 deletions

View File

@@ -0,0 +1,7 @@
Copyright 2018 Marten Seemann
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,37 @@
package libp2ptls
import (
"crypto/tls"
ci "github.com/libp2p/go-libp2p/core/crypto"
"github.com/libp2p/go-libp2p/core/network"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/libp2p/go-libp2p/core/sec"
)
type conn struct {
*tls.Conn
localPeer peer.ID
remotePeer peer.ID
remotePubKey ci.PubKey
connectionState network.ConnectionState
}
var _ sec.SecureConn = &conn{}
func (c *conn) LocalPeer() peer.ID {
return c.localPeer
}
func (c *conn) RemotePeer() peer.ID {
return c.remotePeer
}
func (c *conn) RemotePublicKey() ci.PubKey {
return c.remotePubKey
}
func (c *conn) ConnState() network.ConnectionState {
return c.connectionState
}

View File

@@ -0,0 +1,271 @@
package libp2ptls
import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"errors"
"fmt"
"math/big"
"os"
"runtime/debug"
"time"
ic "github.com/libp2p/go-libp2p/core/crypto"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/libp2p/go-libp2p/core/sec"
)
const certValidityPeriod = 100 * 365 * 24 * time.Hour // ~100 years
const certificatePrefix = "libp2p-tls-handshake:"
const alpn string = "libp2p"
var extensionID = getPrefixedExtensionID([]int{1, 1})
var extensionCritical bool // so we can mark the extension critical in tests
type signedKey struct {
PubKey []byte
Signature []byte
}
// Identity is used to secure connections
type Identity struct {
config tls.Config
}
// IdentityConfig is used to configure an Identity
type IdentityConfig struct {
CertTemplate *x509.Certificate
}
// IdentityOption transforms an IdentityConfig to apply optional settings.
type IdentityOption func(r *IdentityConfig)
// WithCertTemplate specifies the template to use when generating a new certificate.
func WithCertTemplate(template *x509.Certificate) IdentityOption {
return func(c *IdentityConfig) {
c.CertTemplate = template
}
}
// NewIdentity creates a new identity
func NewIdentity(privKey ic.PrivKey, opts ...IdentityOption) (*Identity, error) {
config := IdentityConfig{}
for _, opt := range opts {
opt(&config)
}
var err error
if config.CertTemplate == nil {
config.CertTemplate, err = certTemplate()
if err != nil {
return nil, err
}
}
cert, err := keyToCertificate(privKey, config.CertTemplate)
if err != nil {
return nil, err
}
return &Identity{
config: tls.Config{
MinVersion: tls.VersionTLS13,
InsecureSkipVerify: true, // This is not insecure here. We will verify the cert chain ourselves.
ClientAuth: tls.RequireAnyClientCert,
Certificates: []tls.Certificate{*cert},
VerifyPeerCertificate: func(_ [][]byte, _ [][]*x509.Certificate) error {
panic("tls config not specialized for peer")
},
NextProtos: []string{alpn},
SessionTicketsDisabled: true,
},
}, nil
}
// ConfigForPeer creates a new single-use tls.Config that verifies the peer's
// certificate chain and returns the peer's public key via the channel. If the
// peer ID is empty, the returned config will accept any peer.
//
// It should be used to create a new tls.Config before securing either an
// incoming or outgoing connection.
func (i *Identity) ConfigForPeer(remote peer.ID) (*tls.Config, <-chan ic.PubKey) {
keyCh := make(chan ic.PubKey, 1)
// We need to check the peer ID in the VerifyPeerCertificate callback.
// The tls.Config it is also used for listening, and we might also have concurrent dials.
// Clone it so we can check for the specific peer ID we're dialing here.
conf := i.config.Clone()
// We're using InsecureSkipVerify, so the verifiedChains parameter will always be empty.
// We need to parse the certificates ourselves from the raw certs.
conf.VerifyPeerCertificate = func(rawCerts [][]byte, _ [][]*x509.Certificate) (err error) {
defer func() {
if rerr := recover(); rerr != nil {
fmt.Fprintf(os.Stderr, "panic when processing peer certificate in TLS handshake: %s\n%s\n", rerr, debug.Stack())
err = fmt.Errorf("panic when processing peer certificate in TLS handshake: %s", rerr)
}
}()
defer close(keyCh)
chain := make([]*x509.Certificate, len(rawCerts))
for i := 0; i < len(rawCerts); i++ {
cert, err := x509.ParseCertificate(rawCerts[i])
if err != nil {
return err
}
chain[i] = cert
}
pubKey, err := PubKeyFromCertChain(chain)
if err != nil {
return err
}
if remote != "" && !remote.MatchesPublicKey(pubKey) {
peerID, err := peer.IDFromPublicKey(pubKey)
if err != nil {
peerID = peer.ID(fmt.Sprintf("(not determined: %s)", err.Error()))
}
return sec.ErrPeerIDMismatch{Expected: remote, Actual: peerID}
}
keyCh <- pubKey
return nil
}
return conf, keyCh
}
// PubKeyFromCertChain verifies the certificate chain and extract the remote's public key.
func PubKeyFromCertChain(chain []*x509.Certificate) (ic.PubKey, error) {
if len(chain) != 1 {
return nil, errors.New("expected one certificates in the chain")
}
cert := chain[0]
pool := x509.NewCertPool()
pool.AddCert(cert)
var found bool
var keyExt pkix.Extension
// find the libp2p key extension, skipping all unknown extensions
for _, ext := range cert.Extensions {
if extensionIDEqual(ext.Id, extensionID) {
keyExt = ext
found = true
for i, oident := range cert.UnhandledCriticalExtensions {
if oident.Equal(ext.Id) {
// delete the extension from UnhandledCriticalExtensions
cert.UnhandledCriticalExtensions = append(cert.UnhandledCriticalExtensions[:i], cert.UnhandledCriticalExtensions[i+1:]...)
break
}
}
break
}
}
if !found {
return nil, errors.New("expected certificate to contain the key extension")
}
if _, err := cert.Verify(x509.VerifyOptions{Roots: pool}); err != nil {
// If we return an x509 error here, it will be sent on the wire.
// Wrap the error to avoid that.
return nil, fmt.Errorf("certificate verification failed: %s", err)
}
var sk signedKey
if _, err := asn1.Unmarshal(keyExt.Value, &sk); err != nil {
return nil, fmt.Errorf("unmarshalling signed certificate failed: %s", err)
}
pubKey, err := ic.UnmarshalPublicKey(sk.PubKey)
if err != nil {
return nil, fmt.Errorf("unmarshalling public key failed: %s", err)
}
certKeyPub, err := x509.MarshalPKIXPublicKey(cert.PublicKey)
if err != nil {
return nil, err
}
valid, err := pubKey.Verify(append([]byte(certificatePrefix), certKeyPub...), sk.Signature)
if err != nil {
return nil, fmt.Errorf("signature verification failed: %s", err)
}
if !valid {
return nil, errors.New("signature invalid")
}
return pubKey, nil
}
// GenerateSignedExtension uses the provided private key to sign the public key, and returns the
// signature within a pkix.Extension.
// This extension is included in a certificate to cryptographically tie it to the libp2p private key.
func GenerateSignedExtension(sk ic.PrivKey, pubKey crypto.PublicKey) (pkix.Extension, error) {
keyBytes, err := ic.MarshalPublicKey(sk.GetPublic())
if err != nil {
return pkix.Extension{}, err
}
certKeyPub, err := x509.MarshalPKIXPublicKey(pubKey)
if err != nil {
return pkix.Extension{}, err
}
signature, err := sk.Sign(append([]byte(certificatePrefix), certKeyPub...))
if err != nil {
return pkix.Extension{}, err
}
value, err := asn1.Marshal(signedKey{
PubKey: keyBytes,
Signature: signature,
})
if err != nil {
return pkix.Extension{}, err
}
return pkix.Extension{Id: extensionID, Critical: extensionCritical, Value: value}, nil
}
// keyToCertificate generates a new ECDSA private key and corresponding x509 certificate.
// The certificate includes an extension that cryptographically ties it to the provided libp2p
// private key to authenticate TLS connections.
func keyToCertificate(sk ic.PrivKey, certTmpl *x509.Certificate) (*tls.Certificate, error) {
certKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, err
}
// after calling CreateCertificate, these will end up in Certificate.Extensions
extension, err := GenerateSignedExtension(sk, certKey.Public())
if err != nil {
return nil, err
}
certTmpl.ExtraExtensions = append(certTmpl.ExtraExtensions, extension)
certDER, err := x509.CreateCertificate(rand.Reader, certTmpl, certTmpl, certKey.Public(), certKey)
if err != nil {
return nil, err
}
return &tls.Certificate{
Certificate: [][]byte{certDER},
PrivateKey: certKey,
}, nil
}
// certTemplate returns the template for generating an Identity's TLS certificates.
func certTemplate() (*x509.Certificate, error) {
bigNum := big.NewInt(1 << 62)
sn, err := rand.Int(rand.Reader, bigNum)
if err != nil {
return nil, err
}
subjectSN, err := rand.Int(rand.Reader, bigNum)
if err != nil {
return nil, err
}
return &x509.Certificate{
SerialNumber: sn,
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(certValidityPeriod),
// According to RFC 3280, the issuer field must be set,
// see https://datatracker.ietf.org/doc/html/rfc3280#section-4.1.2.4.
Subject: pkix.Name{SerialNumber: subjectSN.String()},
}, nil
}

View File

@@ -0,0 +1,22 @@
package libp2ptls
var extensionPrefix = []int{1, 3, 6, 1, 4, 1, 53594}
// getPrefixedExtensionID returns an Object Identifier
// that can be used in x509 Certificates.
func getPrefixedExtensionID(suffix []int) []int {
return append(extensionPrefix, suffix...)
}
// extensionIDEqual compares two extension IDs.
func extensionIDEqual(a, b []int) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}

View File

@@ -0,0 +1,182 @@
package libp2ptls
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net"
"os"
"runtime/debug"
"github.com/libp2p/go-libp2p/core/canonicallog"
ci "github.com/libp2p/go-libp2p/core/crypto"
"github.com/libp2p/go-libp2p/core/network"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/libp2p/go-libp2p/core/protocol"
"github.com/libp2p/go-libp2p/core/sec"
tptu "github.com/libp2p/go-libp2p/p2p/net/upgrader"
manet "github.com/multiformats/go-multiaddr/net"
)
// ID is the protocol ID (used when negotiating with multistream)
const ID = "/tls/1.0.0"
// Transport constructs secure communication sessions for a peer.
type Transport struct {
identity *Identity
localPeer peer.ID
privKey ci.PrivKey
muxers []protocol.ID
protocolID protocol.ID
}
var _ sec.SecureTransport = &Transport{}
// New creates a TLS encrypted transport
func New(id protocol.ID, key ci.PrivKey, muxers []tptu.StreamMuxer) (*Transport, error) {
localPeer, err := peer.IDFromPrivateKey(key)
if err != nil {
return nil, err
}
muxerIDs := make([]protocol.ID, 0, len(muxers))
for _, m := range muxers {
muxerIDs = append(muxerIDs, m.ID)
}
t := &Transport{
protocolID: id,
localPeer: localPeer,
privKey: key,
muxers: muxerIDs,
}
identity, err := NewIdentity(key)
if err != nil {
return nil, err
}
t.identity = identity
return t, nil
}
// SecureInbound runs the TLS handshake as a server.
// If p is empty, connections from any peer are accepted.
func (t *Transport) SecureInbound(ctx context.Context, insecure net.Conn, p peer.ID) (sec.SecureConn, error) {
config, keyCh := t.identity.ConfigForPeer(p)
muxers := make([]string, 0, len(t.muxers))
for _, muxer := range t.muxers {
muxers = append(muxers, string(muxer))
}
// TLS' ALPN selection lets the server select the protocol, preferring the server's preferences.
// We want to prefer the client's preference though.
getConfigForClient := config.GetConfigForClient
config.GetConfigForClient = func(info *tls.ClientHelloInfo) (*tls.Config, error) {
alpnLoop:
for _, proto := range info.SupportedProtos {
for _, m := range muxers {
if m == proto {
// Match found. Select this muxer, as it's the client's preference.
// There's no need to add the "libp2p" entry here.
config.NextProtos = []string{proto}
break alpnLoop
}
}
}
if getConfigForClient != nil {
return getConfigForClient(info)
}
return config, nil
}
config.NextProtos = append(muxers, config.NextProtos...)
cs, err := t.handshake(ctx, tls.Server(insecure, config), keyCh)
if err != nil {
addr, maErr := manet.FromNetAddr(insecure.RemoteAddr())
if maErr == nil {
canonicallog.LogPeerStatus(100, p, addr, "handshake_failure", "tls", "err", err.Error())
}
insecure.Close()
}
return cs, err
}
// SecureOutbound runs the TLS handshake as a client.
// Note that SecureOutbound will not return an error if the server doesn't
// accept the certificate. This is due to the fact that in TLS 1.3, the client
// sends its certificate and the ClientFinished in the same flight, and can send
// application data immediately afterwards.
// If the handshake fails, the server will close the connection. The client will
// notice this after 1 RTT when calling Read.
func (t *Transport) SecureOutbound(ctx context.Context, insecure net.Conn, p peer.ID) (sec.SecureConn, error) {
config, keyCh := t.identity.ConfigForPeer(p)
muxers := make([]string, 0, len(t.muxers))
for _, muxer := range t.muxers {
muxers = append(muxers, (string)(muxer))
}
// Prepend the prefered muxers list to TLS config.
config.NextProtos = append(muxers, config.NextProtos...)
cs, err := t.handshake(ctx, tls.Client(insecure, config), keyCh)
if err != nil {
insecure.Close()
}
return cs, err
}
func (t *Transport) handshake(ctx context.Context, tlsConn *tls.Conn, keyCh <-chan ci.PubKey) (_sconn sec.SecureConn, err error) {
defer func() {
if rerr := recover(); rerr != nil {
fmt.Fprintf(os.Stderr, "panic in TLS handshake: %s\n%s\n", rerr, debug.Stack())
err = fmt.Errorf("panic in TLS handshake: %s", rerr)
}
}()
// handshaking...
if err := tlsConn.HandshakeContext(ctx); err != nil {
return nil, err
}
// Should be ready by this point, don't block.
var remotePubKey ci.PubKey
select {
case remotePubKey = <-keyCh:
default:
}
if remotePubKey == nil {
return nil, errors.New("go-libp2p tls BUG: expected remote pub key to be set")
}
return t.setupConn(tlsConn, remotePubKey)
}
func (t *Transport) setupConn(tlsConn *tls.Conn, remotePubKey ci.PubKey) (sec.SecureConn, error) {
remotePeerID, err := peer.IDFromPublicKey(remotePubKey)
if err != nil {
return nil, err
}
nextProto := tlsConn.ConnectionState().NegotiatedProtocol
// The special ALPN extension value "libp2p" is used by libp2p versions
// that don't support early muxer negotiation. If we see this sepcial
// value selected, that means we are handshaking with a version that does
// not support early muxer negotiation. In this case return empty nextProto
// to indicate no muxer is selected.
if nextProto == "libp2p" {
nextProto = ""
}
return &conn{
Conn: tlsConn,
localPeer: t.localPeer,
remotePeer: remotePeerID,
remotePubKey: remotePubKey,
connectionState: network.ConnectionState{
StreamMultiplexer: protocol.ID(nextProto),
UsedEarlyMuxerNegotiation: nextProto != "",
},
}, nil
}
func (t *Transport) ID() protocol.ID {
return t.protocolID
}