stemedb/sdk/go/steme/signer.go
jordan 1ce4004807 feat: Complete Phase 2 (The Cortex) - query, lens, and API layers
This commit adds the read path (Cortex) to complement the write path (Spine):

## Crates
- stemedb-api: HTTP API with axum + utoipa OpenAPI
  - /v1/assert, /v1/query, /v1/epoch, /v1/skeptic, /v1/trace, /v1/audit
  - Metered endpoints with quota enforcement
  - Ed25519 signature verification
- stemedb-lens: Truth resolution lenses
  - RecencyLens, ConsensusLens, ConfidenceLens
  - VoteAwareConsensusLens (Ballot Box pattern)
  - TrustAwareAuthorityLens (The Hive pattern)
  - SkepticLens (conflict analysis)
  - EpochAwareLens (paradigm-safe queries)
- stemedb-query: Query engine with materialized views

## Storage Extensions
- VoteStore: Vote aggregation with cached counts
- TrustRankStore: Agent reputation with decay
- AuditStore: Query audit trail
- IndexStore: SP/P/S index structures
- SupersessionStore: Epoch supersession chains

## SDKs
- sdk/go/steme: Go HTTP client with Ed25519 signing
- sdk/go/adk: ADK-Go tools for AI agents

## Documentation
- Updated CLAUDE.md, architecture.md, roadmap.md
- New ai-lookup entries for all services
- Use case docs for consumer health intelligence
- Arena roadmap for simulation advancement

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 13:22:44 -07:00

163 lines
4.7 KiB
Go

package steme
import (
"crypto/ed25519"
"crypto/rand"
"encoding/hex"
"fmt"
"os"
"time"
)
// Signer handles Ed25519 signing for assertions.
//
// All assertions submitted to StemeDB must be signed by at least one agent.
// The Signer abstracts the cryptographic details and provides a simple API.
type Signer struct {
privateKey ed25519.PrivateKey
publicKey ed25519.PublicKey
}
// NewSigner creates a Signer from a 32-byte Ed25519 private key seed.
//
// The seed should be exactly 32 bytes. The Ed25519 private key is derived
// from this seed, and the public key is computed from the private key.
//
// Returns ErrInvalidKeySize if the seed is not 32 bytes.
func NewSigner(seed []byte) (*Signer, error) {
if len(seed) != ed25519.SeedSize {
return nil, fmt.Errorf("%w: got %d bytes", ErrInvalidKeySize, len(seed))
}
privateKey := ed25519.NewKeyFromSeed(seed)
publicKey := privateKey.Public().(ed25519.PublicKey)
return &Signer{
privateKey: privateKey,
publicKey: publicKey,
}, nil
}
// NewSignerFromHex creates a Signer from a hex-encoded 32-byte seed.
//
// The hex string should be exactly 64 characters (32 bytes).
//
// Returns ErrInvalidHex if hex decoding fails.
// Returns ErrInvalidKeySize if the decoded seed is not 32 bytes.
func NewSignerFromHex(hexSeed string) (*Signer, error) {
seed, err := hex.DecodeString(hexSeed)
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrInvalidHex, err)
}
return NewSigner(seed)
}
// GenerateSigner generates a new random Ed25519 keypair.
//
// This is useful for testing or creating new agent identities.
// The private key seed can be retrieved with Seed() for persistence.
func GenerateSigner() (*Signer, error) {
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
return nil, fmt.Errorf("steme: failed to generate keypair: %w", err)
}
return &Signer{
privateKey: privateKey,
publicKey: publicKey,
}, nil
}
// SignerFromEnv creates a Signer from an environment variable.
//
// The environment variable should contain a hex-encoded 32-byte seed.
//
// Returns an error if the variable is not set, contains invalid hex,
// or is not exactly 32 bytes when decoded.
func SignerFromEnv(envVar string) (*Signer, error) {
hexSeed := os.Getenv(envVar)
if hexSeed == "" {
return nil, fmt.Errorf("steme: environment variable %s not set", envVar)
}
return NewSignerFromHex(hexSeed)
}
// PublicKey returns the hex-encoded Ed25519 public key (32 bytes).
//
// This is the agent_id used in StemeDB.
func (s *Signer) PublicKey() string {
return hex.EncodeToString(s.publicKey)
}
// PublicKeyBytes returns the raw Ed25519 public key bytes.
func (s *Signer) PublicKeyBytes() []byte {
return s.publicKey
}
// Seed returns the hex-encoded Ed25519 private key seed (32 bytes).
//
// This should be stored securely for key recovery.
func (s *Signer) Seed() string {
// Ed25519 private key is 64 bytes: [32-byte seed][32-byte public key]
// Extract the seed portion
return hex.EncodeToString(s.privateKey.Seed())
}
// Sign creates an Ed25519 signature over the message bytes.
//
// The signature is 64 bytes and is hex-encoded in the returned string.
func (s *Signer) Sign(message []byte) string {
signature := ed25519.Sign(s.privateKey, message)
return hex.EncodeToString(signature)
}
// CreateSignature creates a SignatureEntry for an assertion.
//
// The message should be the canonical serialization of the assertion
// (subject:predicate:object:confidence:source_hash).
//
// The timestamp is set to the current Unix epoch.
func (s *Signer) CreateSignature(message []byte) SignatureEntry {
timestamp := uint64(time.Now().Unix())
return SignatureEntry{
AgentID: s.PublicKey(),
Signature: s.Sign(message),
Timestamp: timestamp,
}
}
// Verify verifies an Ed25519 signature.
//
// This is useful for validating signatures received from other agents.
//
// Returns ErrInvalidPublicKeySize if the public key is not 32 bytes.
// Returns ErrInvalidSignatureSize if the signature is not 64 bytes.
func Verify(publicKeyHex, signatureHex string, message []byte) error {
publicKey, err := hex.DecodeString(publicKeyHex)
if err != nil {
return fmt.Errorf("%w: invalid public key hex: %v", ErrInvalidHex, err)
}
if len(publicKey) != ed25519.PublicKeySize {
return fmt.Errorf("%w: public key is %d bytes", ErrInvalidPublicKeySize, len(publicKey))
}
signature, err := hex.DecodeString(signatureHex)
if err != nil {
return fmt.Errorf("%w: invalid signature hex: %v", ErrInvalidHex, err)
}
if len(signature) != ed25519.SignatureSize {
return fmt.Errorf("%w: signature is %d bytes", ErrInvalidSignatureSize, len(signature))
}
if !ed25519.Verify(publicKey, message, signature) {
return fmt.Errorf("steme: signature verification failed")
}
return nil
}