fix(auth): sign served decisions before recording and returning them
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a140c4-354a-7823-8f0d-8f717397f786
This commit is contained in:
parent
62ac2b4e99
commit
ec21518e25
11 changed files with 361 additions and 14 deletions
42
cmd/access-engine/envelope_signing.go
Normal file
42
cmd/access-engine/envelope_signing.go
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/netkingdom/access-engine/internal/decision"
|
||||
)
|
||||
|
||||
// configureEnvelopeSigning reads a mounted seed once at startup. Never accept
|
||||
// the secret in argv, an environment value, an HTTP request or a log message.
|
||||
func configureEnvelopeSigning(engine *decision.Engine, path, kid string) error {
|
||||
if path == "" && kid == "" {
|
||||
return nil
|
||||
}
|
||||
if path == "" || strings.TrimSpace(kid) == "" {
|
||||
return fmt.Errorf("envelope signing requires key file and kid")
|
||||
}
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("envelope signing key file unavailable")
|
||||
}
|
||||
defer file.Close()
|
||||
info, err := file.Stat()
|
||||
if err != nil || !info.Mode().IsRegular() || info.Mode().Perm()&0027 != 0 {
|
||||
return fmt.Errorf("envelope signing key must be a regular file without group-write or world permissions")
|
||||
}
|
||||
raw, err := io.ReadAll(io.LimitReader(file, 129))
|
||||
if err != nil || len(raw) > 128 {
|
||||
return fmt.Errorf("invalid envelope signing key file")
|
||||
}
|
||||
encoded := strings.TrimSpace(string(raw))
|
||||
seed, err := base64.RawURLEncoding.Strict().DecodeString(encoded)
|
||||
if err != nil || len(seed) != ed25519.SeedSize {
|
||||
return fmt.Errorf("invalid envelope signing seed")
|
||||
}
|
||||
return engine.SetEnvelopeSigningKey(ed25519.NewKeyFromSeed(seed), kid)
|
||||
}
|
||||
55
cmd/access-engine/envelope_signing_test.go
Normal file
55
cmd/access-engine/envelope_signing_test.go
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEnvelopeSigningStartupConfiguration(t *testing.T) {
|
||||
engine, err := buildEngine(context.Background(), examplePath("registry_snapshot.json"), examplePath("policy_package.md"), "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
path := filepath.Join(t.TempDir(), "seed")
|
||||
if err := os.WriteFile(path, []byte(base64.RawURLEncoding.EncodeToString(make([]byte, 32))), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := configureEnvelopeSigning(engine, path, "synthetic"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, input := range []struct{ path, kid string }{{path, ""}, {"", "kid"}, {path + "missing", "kid"}} {
|
||||
if configureEnvelopeSigning(engine, input.path, input.kid) == nil {
|
||||
t.Fatal("invalid startup config accepted")
|
||||
}
|
||||
}
|
||||
if err := os.Chmod(path, 0440); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := configureEnvelopeSigning(engine, path, "synthetic"); err != nil {
|
||||
t.Fatalf("projected 0440 mount refused: %v", err)
|
||||
}
|
||||
if err := os.Chmod(path, 0620); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if configureEnvelopeSigning(engine, path, "kid") == nil {
|
||||
t.Fatal("group-writable key accepted")
|
||||
}
|
||||
if err := os.Chmod(path, 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if configureEnvelopeSigning(engine, path, "kid") == nil {
|
||||
t.Fatal("world-readable key accepted")
|
||||
}
|
||||
if err := os.Chmod(path, 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(path, []byte("invalid seed"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if configureEnvelopeSigning(engine, path, "kid") == nil {
|
||||
t.Fatal("invalid seed accepted")
|
||||
}
|
||||
}
|
||||
|
|
@ -330,6 +330,8 @@ func runServe(args []string, stdout, stderr io.Writer) int {
|
|||
registryPath := fs.String("registry", "", "registry snapshot JSON file")
|
||||
policyPath := fs.String("policy", "", "policy package Markdown file")
|
||||
logPath := fs.String("log", "", "optional JSONL decision log path")
|
||||
signingKeyFile := fs.String("envelope-signing-key-file", "", "owner-mounted base64url Ed25519 seed file")
|
||||
signingKID := fs.String("envelope-signing-kid", "", "independently admitted envelope signing key id")
|
||||
outboxDir := fs.String("outbox-dir", "", "durable emission outbox directory (FLEX-WP-0031); exclusive with --log")
|
||||
emissionSource := fs.String("emission-source", "", "audit-core source name for emitted decision events, e.g. flex-auth.tenant-engine")
|
||||
auditURL := fs.String("audit-url", "", "admitted audit-core /v1/events endpoint; empty disables drain and heartbeats")
|
||||
|
|
@ -369,6 +371,9 @@ func runServe(args []string, stdout, stderr io.Writer) int {
|
|||
if err != nil {
|
||||
return fail(stderr, err)
|
||||
}
|
||||
if err := configureEnvelopeSigning(engine, *signingKeyFile, *signingKID); err != nil {
|
||||
return fail(stderr, err)
|
||||
}
|
||||
var outbox *emission.Outbox
|
||||
if *outboxDir != "" {
|
||||
outbox, err = emission.Open(*outboxDir, *emissionSource)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue