Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,9 @@ Bound from the `Ashlar:Autonomy` section by `AddAshlarAutonomy(configuration)`
| Variable | Description | Default |
|----------|-------------|---------|
| `ASHLAR_CERT_DEV_HMAC_KEY` | HMAC key for signing **and verifying** brick and composition certification records (`CertificationRecordSigner`, `CompositionCertificationRecordSigner`, `Ashlar.Certification.Contracts.CertificationRecordSigning`). **Unset means the COMMITTED, PUBLIC dev key** `CertificationRecordSigning.DefaultDevKey`: anyone with the source can forge a record that verifies, so certificates then prove integrity against accident, not against an adversary. Both signers log a warning at construction while the dev key is in effect (`UsesDevKey`). Same-owner cross-project reuse works by sharing this value; cross-organization trust needs the Ed25519 key below or PKI | unset (dev key; warns) |
| `ASHLAR_CERT_ED25519_KEY` | Base64 Ed25519 private key; when set, records are dual-signed and carry the public key. **`CertificationVerifyOptions.Default` and `.Strict` require Ed25519 signatures** (`RequireEd25519Signature = true`) and trust-loop schema v2+ (`MinimumSchemaVersion = 2`), fail-closed; only `.Legacy` accepts HMAC-only records (see limitations 7–8 **CLOSED** 2026-09-06 in `docs/certification-evidence.md`). **Trusted-key pinning exists** via `TrustedEd25519PublicKeys` but is optional; **without pinning, a self-consistent record signed with an attacker's own keypair still verifies** (limitation 1: dev HMAC under committed key remains forgeable). Limitation 9 is CLOSED as of 2026-09-13: a `CertificationRecordSigner` holding a real key now keys the composition lane too, through the shipped DI registration. Production paths use Strict | unset (HMAC-only) |
| `ASHLAR_CERT_ED25519_KEY` | Base64 Ed25519 private key; when set, records are dual-signed and carry the public key. **`CertificationVerifyOptions.Default` and `.Strict` require Ed25519 signatures** (`RequireEd25519Signature = true`) and trust-loop schema v2+ (`MinimumSchemaVersion = 2`), fail-closed; only `.Legacy` accepts HMAC-only records (see limitations 7–8 **CLOSED** 2026-09-06 in `docs/certification-evidence.md`). **Trusted-key pinning is now configurable** — see `ASHLAR_CERT_TRUSTED_ED25519_KEYS` below — but it is still OPT-IN: **with no pinning set configured, a self-consistent record signed with an attacker's own keypair still verifies** (limitation 1: dev HMAC under committed key remains forgeable). Limitation 9 is CLOSED as of 2026-09-13: a `CertificationRecordSigner` holding a real key now keys the composition lane too, through the shipped DI registration. Production paths use Strict | unset (HMAC-only) |
| `ASHLAR_CERT_TRUSTED_ED25519_KEYS` | Base64 raw 32-byte Ed25519 **PUBLIC** keys this host accepts as record signers, separated by commas, semicolons or whitespace (newlines included, so the list can be mounted as a multi-line secret). This is the switch that makes "signed" mean "signed by someone we accept": `CertificationRecordEd25519.VerifySignature` checks the signature against the public key the RECORD carries, so requiring a signature without pinning only forces an attacker to sign rather than strip. Read once per process by `CertificationTrustPolicy.Ambient`, which the record store, the certified brick registry, the hot-swap host, the composition constituent checker, the self-extend admission edge, the state-log verifier and the adaptation wiring all apply in place of the bare `Strict` preset. **There is deliberately no configuration path here for a private key** — that is the minting capability and belongs to the certifier. Present-but-unusable configuration (no keys, a blank entry, a non-Base64 value, a key that is not 32 raw bytes) throws `CertificationTrustConfigurationException` at resolution rather than degrading to an empty set, because an empty set reads as pinning OFF everywhere downstream. Keys are normalized to their canonical Base64 encoding, since enforcement is an ordinal string comparison against what the record carries | unset (**unpinned**: any keypair verifies) |
| `ASHLAR_CERT_PINNING_REQUIRED` | Asserts that pinning must be in effect, so a deployment that expected it fails at startup instead of verifying unpinned for its lifetime. Accepts `1/true/yes/on` and `0/false/no/off`, case-insensitively. When it asserts pinning and `ASHLAR_CERT_TRUSTED_ED25519_KEYS` supplies no key, resolution throws. **A value that is neither — a typo, or a deployment template that rendered the variable with nothing substituted into it — also throws**, rather than being read as "not required": the switch that demands pinning must not be the thing that silently disables it. Only an ABSENT variable means "no assertion made" | unset (no assertion) |

## Workload scaling (`Ashlar:WorkloadScaling:*`, `ASHLAR_WORKLOAD_*`)

Expand Down
32 changes: 31 additions & 1 deletion docs/certification-evidence.md
Original file line number Diff line number Diff line change
Expand Up @@ -642,7 +642,7 @@ redundant guard is exactly what a careful proposer writes.

## Known v0 limitations

1. **Dev HMAC signer, not PKI.** `CertificationRecordSigner` uses a development HMAC key, not a public-key infrastructure. This becomes more load-bearing in the composition phase because trust chains from constituent atom signatures — a forged or weak constituent record undermines the whole composition admission path. Unless `ASHLAR_CERT_DEV_HMAC_KEY` is set, the key is the COMMITTED constant `CertificationRecordSigning.DefaultDevKey`, so every record verifiable here is forgeable by anyone with the source; both signers now warn at construction while that is the case (`UsesDevKey`), and `ASHLAR_CERT_ED25519_KEY` adds a real signature on top. **Ed25519 is now required on Default/Strict verification paths** (limitations 7–8 CLOSED as of 2026-09-06), but it is still not a complete operator trust root on its own: without `TrustedEd25519PublicKeys` pinning, a self-consistent attacker-signed record can verify; HMAC remains forgeable under the committed dev key (this row); and compositions ARE keyed by the operator as of 2026-09-13 (limitation 9 CLOSED — the composition signer takes the injected brick signer as its key holder and the shipped DI registration supplies it, so a host key reaches both lanes; see the CLOSING NOTE below). What remains in this row is the committed dev key itself, not composition reachability.
1. **Dev HMAC signer, not PKI.** `CertificationRecordSigner` uses a development HMAC key, not a public-key infrastructure. This becomes more load-bearing in the composition phase because trust chains from constituent atom signatures — a forged or weak constituent record undermines the whole composition admission path. Unless `ASHLAR_CERT_DEV_HMAC_KEY` is set, the key is the COMMITTED constant `CertificationRecordSigning.DefaultDevKey`, so every record verifiable here is forgeable by anyone with the source; both signers now warn at construction while that is the case (`UsesDevKey`), and `ASHLAR_CERT_ED25519_KEY` adds a real signature on top. **Ed25519 is now required on Default/Strict verification paths** (limitations 7–8 CLOSED as of 2026-09-06), but it is still not a complete operator trust root on its own: without `TrustedEd25519PublicKeys` pinning, a self-consistent attacker-signed record can verify — and pinning is now CONFIGURABLE (`ASHLAR_CERT_TRUSTED_ED25519_KEYS`, see `docs/Configuration.md`) but still opt-in, and nothing in this repository or in CI sets it; HMAC remains forgeable under the committed dev key (this row); and compositions ARE keyed by the operator as of 2026-09-13 (limitation 9 CLOSED — the composition signer takes the injected brick signer as its key holder and the shipped DI registration supplies it, so a host key reaches both lanes; see the CLOSING NOTE below). What remains in this row is the committed dev key itself, not composition reachability.

2. **Composition seam check is TYPE-level only.** The seam validator checks producer/consumer type compatibility (e.g. `string` vs `int`) but not semantic mismatches where types align (e.g. file path vs URL, both `string`). Graph-mutation teeth only partially compensate for this gap.

Expand Down Expand Up @@ -746,6 +746,36 @@ redundant guard is exactly what a careful proposer writes.
two-operator-identity split — **resolved 2026-08-27 to a single identity**, see
`_handoff/readiness/DECISION-identity-split.md`).

*Update: the trust root is now the operator's own setting, not a deferred file.*
`CertificationTrustPolicy` reads `ASHLAR_CERT_TRUSTED_ED25519_KEYS` (Base64 raw PUBLIC
keys; any configuration source, not only the environment) and hands the resulting
`CertificationVerifyOptions` to every production verifier, and
`ASHLAR_CERT_PINNING_REQUIRED` makes "pinning is in effect" a checkable startup
assertion rather than a hope. This is a THIRD location for trust material, chosen over
`~/.ashlar/keys/trusted/` precisely because of the enumeration hazard below, and it
lives in a PACKABLE library (`Ashlar.Certification.Contracts`), so an external consumer
of that package has its trust decisions influenced by ambient process environment it
did not opt into — stated here because it is the kind of thing that should not be
discovered by surprise. **Nothing in this repository or in CI sets either variable**, so
every deployment that has not configured one still verifies unpinned: this row is
downgraded from an open hole to a *configurable* one, not closed.

*How much of that wiring is itself verified.* Seven production verifiers apply the
policy, and they are not equally covered. Five are falsifiable behaviourally — the file
record store, the certified brick registry, the hot-swap verify-at-load path, the
composition constituent check and the self-extend admission edge each take a
`CertificationTrustPolicy` a test can hand them, and each has a fact that reddens when
its call site reverts to the bare preset. Two are not: `StateLogVerifier`'s five-argument
overload and the adaptation DI factory resolve the process-wide `Ambient` directly, and
with nothing configured `Ambient.Strict` is *reference-identical* to the `Strict` preset,
so reverting either one is invisible to every test that configures no policy. That is not
hypothetical — five of these seven sites were in that state when pinning was first wired
in. `PinnedWiringSiteConventionTests` now asserts on shipped IL that all seven still call
`CertificationTrustPolicy.Ambient`, so the wiring being DELETED reddens; it does not prove
the pinned set is *enforced* at the two sites with no behavioural fact, and both of those
lines carry a comment saying so. Giving those two a policy parameter is the fix, and it is
a public API change on a packable library.

**Pin against `operator.pub` only.** Do *not* enumerate `~/.ashlar/keys/trusted/`:
`OperatorKey.Generate(rotate: true)` writes the *previous* public key into it
(`src/Ashlar.Manifest/Signing/OperatorKey.cs:45-52`) and there is no revocation (`:28`),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,24 @@ namespace Ashlar.BackgroundAgents.Security;
public sealed class SelfProducedBrickCertificationPolicy : IPolicy
{
private readonly ICertificationRecordStore _certificationStore;

public SelfProducedBrickCertificationPolicy(ICertificationRecordStore certificationStore)
private readonly CertificationVerifyOptions _verifyOptions;

/// <summary>Initializes the policy.</summary>
/// <param name="certificationStore">Store consulted for the written brick's record.</param>
/// <param name="trustPolicy">
/// Operator trust configuration supplying the pinned signer set. Defaults to
/// <see cref="CertificationTrustPolicy.Ambient"/>, so a host that configured
/// <c>ASHLAR_CERT_TRUSTED_ED25519_KEYS</c> pins this edge as well; with nothing configured
/// this is the <c>Strict</c> preset exactly as before. Without a pinning set, "certified"
/// here means only that the record is self-consistent, which a brick the agent signed with
/// its own keypair also is.
/// </param>
public SelfProducedBrickCertificationPolicy(
ICertificationRecordStore certificationStore,
CertificationTrustPolicy? trustPolicy = null)
{
_certificationStore = certificationStore ?? throw new ArgumentNullException(nameof(certificationStore));
_verifyOptions = (trustPolicy ?? CertificationTrustPolicy.Ambient).Strict;
}

public bool Approve(ToolCall call, WorldSnapshot s, out string reason)
Expand Down Expand Up @@ -57,7 +71,7 @@ public bool Approve(ToolCall call, WorldSnapshot s, out string reason)
var trust = CertificationTrustVerifier.Verify(
CertificationRecordMapper.ToData(record),
content,
options: CertificationVerifyOptions.Strict);
options: _verifyOptions);
if (!trust.Trusted)
{
reason = $"Self-produced brick admission refused for '{brickId}': {trust.FailureCode} — {trust.Reason}";
Expand Down
Loading
Loading