Verify normalized task approval facts and preserve signed source types
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a140c4-354a-7823-8f0d-8f717397f786
This commit is contained in:
tegwick 2026-10-16 17:28:03 +02:00
parent 5a2099f400
commit 4dd004a225
18 changed files with 756 additions and 60 deletions

View file

@ -1,7 +1,7 @@
# Access Engine decision verifier
The API types and `internal/sign/sign.go` are unchanged producer source, pinned
by revision and SHA-256 in `producer-source.json`. License attribution and the
by source revision/proposal and SHA-256 in `producer-source.json`. License attribution and the
original license text are in `LICENSE.access-engine`. This is source vendoring
necessitated by the producer's Go `internal` package boundary, not a separately
maintained crypto implementation. There are no external Go dependencies.
@ -33,3 +33,23 @@ For the complete reproducible CLI build and offline fresh-prefix installation,
use [the release bundle contract](../docs/cli-release-bundle.md). This replaces
manual helper copying for packaged deployments; package and key admission remain
separate owner decisions.
The trusted-service successor snapshot follows the frozen Access B2/B3 proposal lineage,
identified by its patch SHA-256 in `producer-source.json`; this is source review,
not a claim of published producer revision or runtime/key admission. The wrapper
uses producer-owned `sign.DecodeEnvelope` to preserve JSON numeric lexemes in
signed maps. It retains exact authenticated producer JSON through strict
preparation and CAS reauthentication; Python does not recreate signed bytes.
Approval's fourteen native binding vectors are signature interoperability cases,
not an expansion of existing request numeric admission. Signed
`approval_observation` supplements the original immutable carried claim;
strict handoff checks its native digest and observation/deadline bounds. It never
replaces original context or freshness.
Successor R2 execution builder correspondence emits the producer's fixed public
DeclaredServiceBinding only for the typed prod/platform secrets-engine service
lane before approval issuance. Both reference tenants are explicit to match
existing producer normalization. It is not caller authentication; independently
signed enforced caller, exact original/native/descriptor joins and all existing
approval, expiry and CAS gates still apply. Actual production source-server
interoperability receipts are separate from native runtime admission.

View file

@ -4,10 +4,12 @@
package sign
import (
"bytes"
"crypto/ed25519"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"github.com/netkingdom/access-engine/pkg/api"
)
@ -74,3 +76,20 @@ func Verify(envelope api.DecisionEnvelope, keys map[string]ed25519.PublicKey) er
}
return nil
}
// DecodeEnvelope preserves JSON numeric kinds/lexemes emitted by task inputs.
// Legacy producer envelopes remain byte-compatible. It does not invent another
// signature canonicalization: CanonicalBytes still calls typed json.Marshal.
func DecodeEnvelope(raw []byte) (api.DecisionEnvelope, error) {
var envelope api.DecisionEnvelope
decoder := json.NewDecoder(bytes.NewReader(raw))
decoder.UseNumber()
decoder.DisallowUnknownFields()
if err := decoder.Decode(&envelope); err != nil {
return envelope, err
}
if _, err := decoder.Token(); err != io.EOF {
return api.DecisionEnvelope{}, fmt.Errorf("trailing envelope data")
}
return envelope, nil
}

View file

@ -2,13 +2,11 @@
package main
import (
"bytes"
"crypto/ed25519"
"encoding/base64"
"encoding/json"
"fmt"
"github.com/netkingdom/access-engine/internal/sign"
"github.com/netkingdom/access-engine/pkg/api"
"io"
"os"
)
@ -23,10 +21,8 @@ func run() error {
if err := json.NewDecoder(io.LimitReader(os.Stdin, 1048577)).Decode(&in); err != nil {
return err
}
var envelope api.DecisionEnvelope
decoder := json.NewDecoder(bytes.NewReader(in.Envelope))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&envelope); err != nil {
envelope, err := sign.DecodeEnvelope(in.Envelope)
if err != nil {
return err
}
keys := map[string]ed25519.PublicKey{}

View file

@ -635,16 +635,30 @@ type Obligation struct {
// DecisionProvenance captures evaluator and policy provenance.
type DecisionProvenance struct {
Evaluator string `json:"evaluator" yaml:"evaluator"`
Mode string `json:"mode" yaml:"mode"`
PolicyPackage string `json:"policy_package,omitempty" yaml:"policy_package,omitempty"`
PolicyVersion string `json:"policy_version,omitempty" yaml:"policy_version,omitempty"`
PolicyPackageDigest string `json:"policy_package_digest,omitempty" yaml:"policy_package_digest,omitempty"`
RegistrySnapshotDigest string `json:"registry_snapshot_digest,omitempty" yaml:"registry_snapshot_digest,omitempty"`
DirectoryETag string `json:"directory_etag,omitempty" yaml:"directory_etag,omitempty"`
InputClaimDigests map[string]string `json:"input_claim_digests,omitempty" yaml:"input_claim_digests,omitempty"`
DecisionTime string `json:"decision_time,omitempty" yaml:"decision_time,omitempty"`
Caller *CallerProvenance `json:"caller,omitempty" yaml:"caller,omitempty"`
Evaluator string `json:"evaluator" yaml:"evaluator"`
Mode string `json:"mode" yaml:"mode"`
PolicyPackage string `json:"policy_package,omitempty" yaml:"policy_package,omitempty"`
PolicyVersion string `json:"policy_version,omitempty" yaml:"policy_version,omitempty"`
PolicyPackageDigest string `json:"policy_package_digest,omitempty" yaml:"policy_package_digest,omitempty"`
RegistrySnapshotDigest string `json:"registry_snapshot_digest,omitempty" yaml:"registry_snapshot_digest,omitempty"`
DirectoryETag string `json:"directory_etag,omitempty" yaml:"directory_etag,omitempty"`
InputClaimDigests map[string]string `json:"input_claim_digests,omitempty" yaml:"input_claim_digests,omitempty"`
DecisionTime string `json:"decision_time,omitempty" yaml:"decision_time,omitempty"`
ApprovalObservation *ApprovalObservationProvenance `json:"approval_observation,omitempty" yaml:"approval_observation,omitempty"`
Caller *CallerProvenance `json:"caller,omitempty" yaml:"caller,omitempty"`
}
// ApprovalObservationProvenance binds fresh server-owned fact evidence without
// rewriting the original carried claim/context or either PDP request digest.
type ApprovalObservationProvenance struct {
SourceID string `json:"source_id"`
SourceRevision string `json:"source_revision"`
ConfigurationDigest string `json:"configuration_digest"`
ClaimDigest string `json:"claim_digest"`
NativeBindingDigest string `json:"native_binding_digest"`
ObservedAt string `json:"observed_at"`
NotAfter string `json:"not_after"`
ValidatedAt string `json:"validated_at"`
}
// CallerProvenance records how the request was authenticated to flex-auth.

View file

@ -1,15 +1,21 @@
{
"upstream": "https://github.com/netkingdom/access-engine",
"revision": "62ac2b4e99dec2005c7c407fe633612578a7907d",
"revision": "B2/B3 R3 retained-registry source proposal over B1 R2; canonical native-prep base 919ce25",
"files": {
"pkg/api/canonical.go": "21e754617c2b82d673cef31b7075f8d177e4367782b63b20b37125587bf9e300",
"pkg/api/canonical.go": "a122a2e8cd524333b13c5939c7a3685e41efbe47497a64e0c7297c92b7f10d19",
"pkg/api/caring.go": "1907156f0f889e7b9e9c14970cf7a86a20d00478a496efaf92c7231f27fec5e3",
"pkg/api/resource_manifest.go": "f0fa4080287d389ed48c2b1d6af96cc62e57ef72c902333f677f964420be133f",
"pkg/api/doc.go": "0638f06ae47369899dd1e2dd5dd23a7200e7dbb3b505b04f4d3c78c0a074def9",
"internal/sign/sign.go": "dad2d0c9f6d1fb2fd68e774949ccdf954382acd075191b5ddfea2c3cc2f4cb62"
"internal/sign/sign.go": "0df9515922e90775ebbfac798361dcafb20da8a4213796b44e246b8a92046cde"
},
"fixtures": {
"decision_rotate_signed.json": "a7bc499f3583f53551cf163109a3c108188345100176092adc31eccefc222fd8",
"decision_rotate_signed_tampered.json": "7f2d2e9372a9617bd4b197033f09a5e33a08b202046de8109582da1dc90cb527"
},
"source_proposal": {
"patch_sha256": "46bfeb706d3a88bf6054309cc9303eb360a91e57dda3b0fcbb7b80d551a75db6",
"predecessor_patch_sha256": "ed85a55b54f8380a657c0688d6f216699a0873b1b94b4c0c2e09332620c8ebf4",
"api_sign_bytes_unchanged": true,
"status": "source proposal; no runtime or key admission"
}
}

View file

@ -6,7 +6,7 @@ import (
"crypto/ed25519"
"encoding/json"
"github.com/netkingdom/access-engine/internal/sign"
"github.com/netkingdom/access-engine/pkg/api"
"io"
"os"
)
@ -17,8 +17,12 @@ func main() {
seed = 0x43
kid = "next-admitted"
}
var env api.DecisionEnvelope
if json.NewDecoder(os.Stdin).Decode(&env) != nil {
raw, err := io.ReadAll(os.Stdin)
if err != nil {
os.Exit(1)
}
env, err := sign.DecodeEnvelope(raw)
if err != nil {
os.Exit(1)
}
if sign.Sign(&env, ed25519.NewKeyFromSeed(bytes.Repeat([]byte{seed}, 32)), kid) != nil {