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>
163 lines
4.7 KiB
Go
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
|
|
}
|