fix(auth): sign served decisions before recording and returning them
Some checks failed
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Build and Publish Container Image / build-and-push (push) Failing after 1m27s

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a140c4-354a-7823-8f0d-8f717397f786
This commit is contained in:
tegwick 2026-10-16 07:51:43 +02:00
parent 62ac2b4e99
commit ec21518e25
11 changed files with 361 additions and 14 deletions

View file

@ -3,6 +3,7 @@ package decision
import (
"bytes"
"context"
"crypto/ed25519"
"crypto/sha256"
"encoding/hex"
"encoding/json"
@ -18,18 +19,21 @@ import (
"github.com/netkingdom/access-engine/internal/callerauth"
"github.com/netkingdom/access-engine/internal/policy"
"github.com/netkingdom/access-engine/internal/registry"
"github.com/netkingdom/access-engine/internal/sign"
"github.com/netkingdom/access-engine/pkg/api"
)
// Engine evaluates deterministic standalone authorization checks against a
// local registry and one validated policy package.
type Engine struct {
store *registry.Store
policy *policy.Package
mu sync.RWMutex
history map[string]api.DecisionEnvelope
log DecisionRecorder
clock func() time.Time
store *registry.Store
policy *policy.Package
mu sync.RWMutex
history map[string]api.DecisionEnvelope
log DecisionRecorder
clock func() time.Time
signingKey ed25519.PrivateKey
signingKID string
}
// DecisionRecorder persists decision envelopes.
@ -91,6 +95,19 @@ func NewEngine(store *registry.Store, policyPackage *policy.Package) (*Engine, e
}, nil
}
// SetEnvelopeSigningKey installs an owner-custodied key before serving requests.
// The key is copied so caller mutation cannot change the admitted signer.
func (e *Engine) SetEnvelopeSigningKey(key ed25519.PrivateKey, kid string) error {
if len(key) != ed25519.PrivateKeySize || strings.TrimSpace(kid) == "" {
return fmt.Errorf("valid Ed25519 private key and kid are required")
}
e.mu.Lock()
defer e.mu.Unlock()
e.signingKey = append(ed25519.PrivateKey(nil), key...)
e.signingKID = kid
return nil
}
// SetDecisionLog attaches a local decision recorder to the engine.
func (e *Engine) SetDecisionLog(log DecisionRecorder) {
e.mu.Lock()
@ -124,7 +141,16 @@ func (e *Engine) decide(ctx context.Context, request api.CheckRequest) (api.Deci
return api.DecisionEnvelope{}, err
}
return e.envelope(ctx, normalized, request, expectation, facts), nil
envelope := e.envelope(ctx, normalized, request, expectation, facts)
e.mu.RLock()
key, kid := e.signingKey, e.signingKID
e.mu.RUnlock()
if len(key) != 0 {
if err := sign.Sign(&envelope, key, kid); err != nil {
return api.DecisionEnvelope{}, fmt.Errorf("decision signing failed: %w", err)
}
}
return envelope, nil
}
// Check evaluates one subject/action/resource request. The decision is