Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -115,14 +115,16 @@ jobs:
./pkg/atls/... \
./pkg/clients/... \
./pkg/agtp/... \
./pkg/production
./pkg/production \
./cmd/redis-failover-redteam

- name: Run Direct-Agent security red-team tests
run: |
GOTOOLCHAIN=go1.26.0+auto go test -v -race -count=1 \
./pkg/atls/identitypolicy \
./pkg/clients \
./pkg/production
./pkg/production \
./cmd/redis-failover-redteam

- name: Run protected-change consumer integration
run: |
Expand Down
7 changes: 7 additions & 0 deletions .github/workflows/security-red-team.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,14 @@ on:
- "pkg/atls/**"
- "pkg/clients/**"
- "pkg/production/**"
- "cmd/redis-failover-redteam/**"
- "examples/a2a-multiprocess/**"
- "examples/protected-change-consumer/**"
- "docs/draft06-a2a-profile.md"
- "docs/API_COMPATIBILITY.md"
- "docs/production-deployment-profile.md"
- "docs/azure-sev-snp-attestation-bridge.md"
- "docs/redis-failover-runbook.md"
- "docs/security-red-team-tests.md"
- "docs/live-red-team-report.md"
push:
Expand All @@ -35,11 +38,14 @@ on:
- "pkg/atls/**"
- "pkg/clients/**"
- "pkg/production/**"
- "cmd/redis-failover-redteam/**"
- "examples/a2a-multiprocess/**"
- "examples/protected-change-consumer/**"
- "docs/draft06-a2a-profile.md"
- "docs/API_COMPATIBILITY.md"
- "docs/production-deployment-profile.md"
- "docs/azure-sev-snp-attestation-bridge.md"
- "docs/redis-failover-runbook.md"
- "docs/security-red-team-tests.md"
- "docs/live-red-team-report.md"

Expand Down Expand Up @@ -68,5 +74,6 @@ jobs:
./pkg/atls/sbaipv2 \
./pkg/clients \
./pkg/production \
./cmd/redis-failover-redteam \
./examples/protected-change-consumer \
./examples/a2a-multiprocess
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## Unreleased

- Add a pinned-issuer Azure SEV-SNP Attestation token bridge with exact ASB
binder challenge, measurement, policy, SVN, debug, migration, key, and
freshness checks.
- Add optional same-connection Redis/Valkey `WAIT` acknowledgement after a
successful replay insert.
- Add a two-phase real Redis/Valkey failover qualification command and
deployment runbooks for Azure hardware attestation and replay HA.

## v1.0.0

- Add the supported Direct-Agent v1 production composition.
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ build-igvm:
product-security-gate:
go mod verify
GOTOOLCHAIN=go1.26.0+auto go test $(DIRECT_AGENT_CORE_PKGS)
GOTOOLCHAIN=go1.26.0+auto go test -v -race -count=1 ./pkg/atls/identitypolicy ./pkg/clients ./pkg/production $(PRODUCTION_CONSUMER_PKGS)
GOTOOLCHAIN=go1.26.0+auto go test -v -race -count=1 ./pkg/atls/identitypolicy ./pkg/clients ./pkg/production ./cmd/redis-failover-redteam $(PRODUCTION_CONSUMER_PKGS)
$(MAKE) fuzz-smoke
$(GOVULNCHECK) ./...

Expand Down
13 changes: 10 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ CWT/COSE, and gateway-route policy experiments.
- `docs/API_COMPATIBILITY.md`: supported v1 API and compatibility policy.
- `docs/production-deployment-profile.md`: fixed production choices for trust,
revocation, attestation, distributed replay, and exact action binding.
- `docs/azure-sev-snp-attestation-bridge.md`: unreleased Azure Attestation
token-to-ASB bridge boundary and live confidential-VM qualification.
- `docs/redis-failover-runbook.md`: private multi-node replay topology,
replication acknowledgement, and real failover gate.
- `formal/`: ProVerif and TLA+ models, recorded results, and
model-to-implementation traceability.
- `pkg/clients`, `pkg/atls`, and `pkg/atls/identitypolicy`: Direct-Agent
Expand Down Expand Up @@ -113,12 +117,15 @@ The release evidence covers:
for compact JWT/JWS parsing, and deterministic acceptance invariants;
- route-assertion policy tests and a local HTTP route-assertion harness for the
documented gateway boundary.
- a production composition with current trust/revocation snapshots, signed
attestation-result policy, and TLS-only Redis/Valkey SETNX replay;
- a production composition with current trust/revocation snapshots and
TLS-only Redis/Valkey SETNX replay, plus an unreleased Azure SEV-SNP
token-to-result bridge tested with signed synthetic tokens and optional
same-connection Redis replica acknowledgement;
- an independent protected-change HTTPS consumer that rejects a changed
action, wrong TLS session, replay, revoked grant, attestation mismatch, and
replay-store outage; and
- a 20-client TLS replay-store race that requires exactly one SETNX winner.
- a 20-client TLS replay-store race that requires exactly one SETNX winner and
a replica acknowledgement for that accepted write.

For accepted TLS sessions, the AGTP observed-identity path derives
`tls_exporter_sha256` from the accepted `tls.ConnectionState`. Fixed exporter
Expand Down
229 changes: 229 additions & 0 deletions cmd/redis-failover-redteam/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
// Copyright (c) 2026 ToppyMicroServices OÜ
// SPDX-License-Identifier: Apache-2.0

package main

import (
"context"
"crypto/rand"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"os"
"strings"
"time"

"github.com/thinksyncs/agents-secure-binding/pkg/production"
)

const stateVersion = "asb.redis-failover-evidence/v1"

var (
errReplayAcceptedAfterFailover = errors.New("replay key was accepted after failover")
errEvidenceExpired = errors.New("failover evidence TTL expired before verification")
)

type options struct {
Phase string
StateFile string
Address string
ServerName string
CAFile string
ClientCertificate string
ClientKey string
KeyPrefix string
RequiredReplicas int
ReplicationTimeout time.Duration
OperationTimeout time.Duration
TTL time.Duration
}

type evidenceState struct {
Version string `json:"version"`
ReplayKey string `json:"replay_key"`
ReplayKeySHA256 string `json:"replay_key_sha256"`
SeededAt time.Time `json:"seeded_at"`
ExpiresAt time.Time `json:"expires_at"`
}

type setNXStore interface {
SetNX(context.Context, string, time.Duration) (bool, error)
}

func main() {
opts := options{}
flag.StringVar(&opts.Phase, "phase", "", "test phase: seed or verify")
flag.StringVar(&opts.StateFile, "state-file", "", "private state file shared across failover")
flag.StringVar(&opts.Address, "address", "", "stable private Redis/Valkey endpoint")
flag.StringVar(&opts.ServerName, "server-name", "", "TLS server name")
flag.StringVar(&opts.CAFile, "ca-file", "", "PEM CA bundle for the replay service")
flag.StringVar(&opts.ClientCertificate, "client-certificate", "", "optional PEM client certificate")
flag.StringVar(&opts.ClientKey, "client-key", "", "optional PEM client private key")
flag.StringVar(&opts.KeyPrefix, "key-prefix", "asb:redis-failover:v1:", "isolated Redis key prefix")
flag.IntVar(&opts.RequiredReplicas, "required-replicas", 1, "replica acknowledgements required by WAIT")
flag.DurationVar(&opts.ReplicationTimeout, "replication-timeout", time.Second, "WAIT timeout")
flag.DurationVar(&opts.OperationTimeout, "operation-timeout", 5*time.Second, "total replay operation timeout")
flag.DurationVar(&opts.TTL, "ttl", 30*time.Minute, "failover evidence TTL")
flag.Parse()

if err := execute(context.Background(), opts, time.Now(), rand.Reader); err != nil {
fmt.Fprintf(os.Stderr, "redis failover red-team failed: %v\n", err)
os.Exit(1)
}
}

func execute(ctx context.Context, opts options, now time.Time, randomness io.Reader) error {
if strings.TrimSpace(opts.StateFile) == "" || strings.TrimSpace(opts.Address) == "" ||
strings.TrimSpace(opts.ServerName) == "" || strings.TrimSpace(opts.CAFile) == "" {
return errors.New("state-file, address, server-name, and ca-file are required")
}
tlsConfig, err := loadTLSConfig(opts)
if err != nil {
return err
}
store := production.RedisSetNXStore{
Address: opts.Address,
Username: os.Getenv("ASB_REDIS_USERNAME"),
Password: os.Getenv("ASB_REDIS_PASSWORD"),
KeyPrefix: opts.KeyPrefix,
TLSConfig: tlsConfig,
OperationTimeout: opts.OperationTimeout,
RequiredReplicaAcknowledgements: opts.RequiredReplicas,
ReplicationTimeout: opts.ReplicationTimeout,
}
return runPhase(ctx, opts, store, now, randomness)
}

func runPhase(ctx context.Context, opts options, store setNXStore, now time.Time, randomness io.Reader) error {
if ctx == nil || store == nil || randomness == nil || opts.TTL <= 0 {
return errors.New("invalid failover test configuration")
}
switch opts.Phase {
case "seed":
rawKey := make([]byte, 32)
if _, err := io.ReadFull(randomness, rawKey); err != nil {
return fmt.Errorf("generate replay key: %w", err)
}
replayKey := base64.RawURLEncoding.EncodeToString(rawKey)
accepted, err := store.SetNX(ctx, replayKey, opts.TTL)
if err != nil {
return fmt.Errorf("seed replicated replay state: %w", err)
}
if !accepted {
return errors.New("fresh replay key was already present")
}
digest := sha256.Sum256([]byte(replayKey))
state := evidenceState{
Version: stateVersion,
ReplayKey: replayKey,
ReplayKeySHA256: hex.EncodeToString(digest[:]),
SeededAt: now.UTC(),
ExpiresAt: now.Add(opts.TTL).UTC(),
}
if err := writeState(opts.StateFile, state); err != nil {
return err
}
fmt.Printf("seed passed: replay_key_sha256=%s expires_at=%s\n", state.ReplayKeySHA256, state.ExpiresAt.Format(time.RFC3339))
return nil

case "verify":
state, err := readState(opts.StateFile)
if err != nil {
return err
}
if !now.Before(state.ExpiresAt) {
return errEvidenceExpired
}
remainingTTL := state.ExpiresAt.Sub(now)
accepted, err := store.SetNX(ctx, state.ReplayKey, remainingTTL)
if err != nil {
return fmt.Errorf("verify replay state after failover: %w", err)
}
if accepted {
return errReplayAcceptedAfterFailover
}
fmt.Printf("verify passed: replay_key_sha256=%s remained rejected after failover\n", state.ReplayKeySHA256)
return nil

default:
return errors.New("phase must be seed or verify")
}
}

func loadTLSConfig(opts options) (*tls.Config, error) {
rootPEM, err := os.ReadFile(opts.CAFile)
if err != nil {
return nil, fmt.Errorf("read CA file: %w", err)
}
roots := x509.NewCertPool()
if !roots.AppendCertsFromPEM(rootPEM) {
return nil, errors.New("CA file contains no usable certificates")
}
config := &tls.Config{
RootCAs: roots,
ServerName: opts.ServerName,
MinVersion: tls.VersionTLS13,
}
if (opts.ClientCertificate == "") != (opts.ClientKey == "") {
return nil, errors.New("client-certificate and client-key must be provided together")
}
if opts.ClientCertificate != "" {
certificate, err := tls.LoadX509KeyPair(opts.ClientCertificate, opts.ClientKey)
if err != nil {
return nil, fmt.Errorf("load Redis client certificate: %w", err)
}
config.Certificates = []tls.Certificate{certificate}
}
return config, nil
}

func writeState(path string, state evidenceState) error {
payload, err := json.MarshalIndent(state, "", " ")
if err != nil {
return fmt.Errorf("marshal failover state: %w", err)
}
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600)
if err != nil {
return fmt.Errorf("write failover state: %w", err)
}
if _, err := file.Write(append(payload, '\n')); err != nil {
_ = file.Close()
return fmt.Errorf("write failover state: %w", err)
}
if err := file.Close(); err != nil {
return fmt.Errorf("close failover state: %w", err)
}
return nil
}

func readState(path string) (evidenceState, error) {
info, err := os.Stat(path)
if err != nil {
return evidenceState{}, fmt.Errorf("stat failover state: %w", err)
}
if info.Mode().Perm()&0o077 != 0 {
return evidenceState{}, errors.New("failover state permissions must be 0600 or stricter")
}
payload, err := os.ReadFile(path)
if err != nil {
return evidenceState{}, fmt.Errorf("read failover state: %w", err)
}
var state evidenceState
if err := json.Unmarshal(payload, &state); err != nil {
return evidenceState{}, fmt.Errorf("decode failover state: %w", err)
}
digest := sha256.Sum256([]byte(state.ReplayKey))
if state.Version != stateVersion || state.ReplayKey == "" ||
state.ReplayKeySHA256 != hex.EncodeToString(digest[:]) ||
state.SeededAt.IsZero() || state.ExpiresAt.IsZero() || !state.ExpiresAt.After(state.SeededAt) {
return evidenceState{}, errors.New("invalid failover state")
}
return state, nil
}
Loading
Loading