cold-start unlock: key wrap/unwrap, locked-mode daemon, IPC unlock methods
- internal/webauthn/keywrap.go: HKDF-SHA256 + AES-256-GCM WrapKey/UnwrapKey - internal/ipc/: MethodStoreEncryptionKey/MethodUnlock wire, api structs, server dispatch callbacks (WrapKeyFn/UnlockFn), client stubs - internal/config/config.go: DefaultWrappedKeyPath() method - cmd/mavend/main.go: locked-mode boot path - detects wrapped key, starts locked with lockedAPI stub, wires UnlockFn that opens store + replaces CoreAPI on passkey assertion. env-key path stores WrapKeyFn for enrollment. make test green (303+, -race)
This commit is contained in:
@@ -283,6 +283,19 @@ type TickTrace struct {
|
||||
Rules []RuleTrace `json:"rules"`
|
||||
}
|
||||
|
||||
// storeEncryptionKeyReq — passkey credential public key for wrapping the store
|
||||
// encryption key at enrollment time. Called by mavweb after RegisterFinish.
|
||||
type storeEncryptionKeyReq struct {
|
||||
PublicKey []byte `json:"public_key"`
|
||||
}
|
||||
|
||||
// unlockReq — passkey credential public key for unwrapping the store
|
||||
// encryption key at cold-start. mavend reads the wrapped blob from its own
|
||||
// configured path; the public key is the other half needed for unwrapping.
|
||||
type unlockReq struct {
|
||||
PublicKey []byte `json:"public_key"`
|
||||
}
|
||||
|
||||
// ErrToolNotFound — no tool row with this name (re-exported store sentinel for
|
||||
// wire round-tripping via errors.Is).
|
||||
var ErrToolNotFound = errors.New("ipc: tool not found")
|
||||
|
||||
@@ -337,6 +337,14 @@ func (c *Client) AssertStepUp(ctx context.Context) error {
|
||||
return c.call(ctx, MethodAssertStepUp, nil, nil)
|
||||
}
|
||||
|
||||
func (c *Client) StoreEncryptionKey(ctx context.Context, publicKey []byte) error {
|
||||
return c.call(ctx, MethodStoreEncryptionKey, storeEncryptionKeyReq{PublicKey: publicKey}, nil)
|
||||
}
|
||||
|
||||
func (c *Client) Unlock(ctx context.Context, publicKey []byte) error {
|
||||
return c.call(ctx, MethodUnlock, unlockReq{PublicKey: publicKey}, nil)
|
||||
}
|
||||
|
||||
func (c *Client) LookupTool(ctx context.Context, name string) (Tool, error) {
|
||||
var t Tool
|
||||
if err := c.call(ctx, MethodLookupTool, lookupToolReq{Name: name}, &t); err != nil {
|
||||
|
||||
@@ -302,10 +302,29 @@ type Server struct {
|
||||
// MethodAssertStepUp returns ErrUnknownMethod (same as pre-stepup floor).
|
||||
StepUp StepUpFunc
|
||||
|
||||
// WrapKeyFn — wraps the in-memory store encryption key with a passkey
|
||||
// credential public key (HKDF-AESGCM) and writes the wrapped blob to disk.
|
||||
// Set by the daemon; nil ⇒ MethodStoreEncryptionKey returns ErrUnknownMethod.
|
||||
WrapKeyFn WrapKeyFunc
|
||||
|
||||
// UnlockFn — unwraps the store encryption key from the wrapped blob using
|
||||
// the passkey credential public key, opens the encrypted store, and wires
|
||||
// the rest of the daemon (voice, loop, delivery). Set by the daemon when
|
||||
// in locked mode; nil ⇒ MethodUnlock returns ErrUnknownMethod.
|
||||
UnlockFn UnlockFunc
|
||||
|
||||
// now is injected so tests can drive time; the loop already works in
|
||||
// absolute ts supplied by callers, so this isn't load-bearing for live ops.
|
||||
}
|
||||
|
||||
// WrapKeyFunc — wraps the store encryption key with the given credential
|
||||
// public key and persists the wrapped blob.
|
||||
type WrapKeyFunc func(ctx context.Context, publicKey []byte) error
|
||||
|
||||
// UnlockFunc — unwraps the store encryption key using the given credential
|
||||
// public key and completes daemon initialization.
|
||||
type UnlockFunc func(ctx context.Context, publicKey []byte) error
|
||||
|
||||
// CheckFunc — the auth hook signature. Wired by the daemon (auth.Gate.Check
|
||||
// satisfies this); dispatch calls it once per request after param-unmarshal
|
||||
// independence (it gets the raw params, may unmarshal what it needs — ipc
|
||||
@@ -672,6 +691,26 @@ func (s *Server) dispatch(ctx context.Context, req Request) (json.RawMessage, er
|
||||
}
|
||||
return nil, fmt.Errorf("%w: %s", ErrUnknownMethod, req.Method)
|
||||
|
||||
case MethodStoreEncryptionKey:
|
||||
if s.WrapKeyFn != nil {
|
||||
var p storeEncryptionKeyReq
|
||||
if err := unmarshalParams(req.Params, &p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return marshalResult(nil), s.WrapKeyFn(ctx, p.PublicKey)
|
||||
}
|
||||
return nil, fmt.Errorf("%w: %s", ErrUnknownMethod, req.Method)
|
||||
|
||||
case MethodUnlock:
|
||||
if s.UnlockFn != nil {
|
||||
var p unlockReq
|
||||
if err := unmarshalParams(req.Params, &p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return marshalResult(nil), s.UnlockFn(ctx, p.PublicKey)
|
||||
}
|
||||
return nil, fmt.Errorf("%w: %s", ErrUnknownMethod, req.Method)
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("%w: %s", ErrUnknownMethod, req.Method)
|
||||
}
|
||||
@@ -713,6 +752,12 @@ func (s *Server) Close() error {
|
||||
// Path returns the filesystem path of the listening socket.
|
||||
func (s *Server) Path() string { return s.path }
|
||||
|
||||
// SetAPI atomically replaces the CoreAPI the server dispatches to. Used by
|
||||
// the daemon's unlock path: in locked mode a dummy API returns errors for all
|
||||
// store methods; after unlock, the real store API is swapped in. Safe to call
|
||||
// while the server is serving (dispatch reads s.api once per request).
|
||||
func (s *Server) SetAPI(api CoreAPI) { s.api = api }
|
||||
|
||||
func parentDir(p string) string {
|
||||
if i := lastIndexByte(p, '/'); i >= 0 {
|
||||
if i == 0 {
|
||||
|
||||
@@ -33,6 +33,8 @@ const (
|
||||
MethodEnableTool Method = "enable_tool"
|
||||
MethodDisableTool Method = "disable_tool"
|
||||
MethodAssertStepUp Method = "assert_stepup"
|
||||
MethodStoreEncryptionKey Method = "store_encryption_key"
|
||||
MethodUnlock Method = "unlock"
|
||||
MethodLookupTool Method = "lookup_tool"
|
||||
MethodListTools Method = "list_tools"
|
||||
MethodRevertFact Method = "revert_fact"
|
||||
|
||||
Reference in New Issue
Block a user