Skip to content
Registry StackDocsDevelopment (unreleased)

Approve the initial product baseline

View as Markdown

Use this guide after registryctl build to turn the two unsigned product-lane inputs into one ApprovedBaselineSetV1 that a deployment operator can consume.

Use this procedure for the first governed approval of a project environment. For a later change, compare against the current approved set and follow Approve a changed source.

The public Relay and consultation Relay lanes keep separate anchors, signing keys, signed bundles, and approval decisions. The approved-set owner assembles verified lane outputs but does not sign on behalf of a lane.

Evidence Gateway and Registry Mint are not registryctl-generated products and do not participate in this trust-anchor and approved-set pipeline at all: each validates its own deployment inputs directly, with evidence check and mint check. The current evidencectl new command starts an incomplete OpenAPI authoring workspace; it does not create Evidence Gateway deployment inputs. For a complete Evidence Gateway project, evidencectl fixtures run drives the offline fixture checks. See Configure Evidence Gateway and Configure Registry Mint for those procedures.

Use this procedure only when the selected build binds an operator-managed source and reports Next: registryctl trust anchor create --help. Do not sign a build that contains a project-local file dataset. Bind the operator-managed source in a separate governed environment, rerun its fixtures and review, and build that environment first.

From the authored project, test, review, and build the target environment:

Terminal window
registryctl test --environment local
registryctl check --environment local --explain
registryctl review compare --environment local
registryctl build --environment local

An initial comparison identifies both lanes as requiring approval. The build creates one unsigned input under each of these directories:

.registry-stack/build/local/signing-inputs/relay-public
.registry-stack/build/local/signing-inputs/relay-consultation

The optional local evaluation-key procedure requires OpenSSL with Ed25519 support and Python 3. Production trust owners use their approved custody tooling instead.

Prepare a fresh handoff directory. Each lane trust owner also needs an approved public JSON Web Key (JWK) file and the matching private key through an explicit file: or op:// locator.

Terminal window
mkdir operator-handoff

Do not put a private signing key in the generated build or the handoff directory.

The maintained HTTP starter declares only the local environment. For a local evaluation, this procedure creates one independent Ed25519 key pair per lane using OpenSSL and the Python 3 standard library:

Terminal window
umask 077
python3 - <<'PY'
from pathlib import Path
Path("evaluation-keys").mkdir(mode=0o700)
PY
for lane in relay-public relay-consultation; do
openssl genpkey \
-algorithm ED25519 \
-outform DER \
-out "evaluation-keys/$lane.private.der"
openssl pkey \
-in "evaluation-keys/$lane.private.der" \
-inform DER \
-pubout \
-outform DER \
-out "evaluation-keys/$lane.public.der"
LANE="$lane" python3 - <<'PY'
import base64
import json
import os
from pathlib import Path
root = Path("evaluation-keys")
lane = os.environ["LANE"]
private_der_path = root / f"{lane}.private.der"
public_der_path = root / f"{lane}.public.der"
private_der = private_der_path.read_bytes()
public_der = public_der_path.read_bytes()
private_prefix = bytes.fromhex("302e020100300506032b657004220420")
public_prefix = bytes.fromhex("302a300506032b6570032100")
if (
len(private_der) != len(private_prefix) + 32
or not private_der.startswith(private_prefix)
or len(public_der) != len(public_prefix) + 32
or not public_der.startswith(public_prefix)
):
raise SystemExit("openssl emitted an unexpected Ed25519 key encoding")
def encode(value):
return base64.urlsafe_b64encode(value).rstrip(b"=").decode("ascii")
public = {
"crv": "Ed25519",
"kty": "OKP",
"x": encode(public_der[-32:]),
}
private = {
**public,
"d": encode(private_der[-32:]),
}
for kind, value in (("private", private), ("public", public)):
path = root / f"{lane}.{kind}.jwk"
path.write_text(
json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n",
encoding="utf-8",
)
path.chmod(0o600)
private_der_path.unlink()
public_der_path.unlink()
PY
done

The procedure refuses to reuse an existing evaluation-keys directory. It leaves each JWK owner-readable and owner-writable only.

The public Relay trust owner creates the lane anchor, signs the exact build input, and verifies the result:

Terminal window
registryctl trust anchor create \
--lane relay-public \
--input .registry-stack/build/local/signing-inputs/relay-public \
--public-key evaluation-keys/relay-public.public.jwk \
--threshold 1 \
--output-file operator-handoff/relay-public-anchor.json
registryctl trust bundle sign \
--lane relay-public \
--input .registry-stack/build/local/signing-inputs/relay-public \
--anchor operator-handoff/relay-public-anchor.json \
--key file:evaluation-keys/relay-public.private.jwk \
--output-dir operator-handoff/relay-public-bundle
registryctl trust bundle verify \
--bundle-dir operator-handoff/relay-public-bundle \
--anchor operator-handoff/relay-public-bundle/anchor.json

Verification must identify the signed input as the relay-public lane under that anchor.

The consultation Relay trust owner repeats the procedure with its own key and anchor:

Terminal window
registryctl trust anchor create \
--lane relay-consultation \
--input .registry-stack/build/local/signing-inputs/relay-consultation \
--public-key evaluation-keys/relay-consultation.public.jwk \
--threshold 1 \
--output-file operator-handoff/relay-consultation-anchor.json
registryctl trust bundle sign \
--lane relay-consultation \
--input .registry-stack/build/local/signing-inputs/relay-consultation \
--anchor operator-handoff/relay-consultation-anchor.json \
--key file:evaluation-keys/relay-consultation.private.jwk \
--output-dir operator-handoff/relay-consultation-bundle
registryctl trust bundle verify \
--bundle-dir operator-handoff/relay-consultation-bundle \
--anchor operator-handoff/relay-consultation-bundle/anchor.json

Verification must identify the signed input as the relay-consultation lane under that anchor.

After both lane owners transfer their verified outputs, the approved-set owner assembles the initial set:

Terminal window
registryctl trust approved-set assemble \
--environment local \
--relay-public operator-handoff/relay-public-bundle \
--relay-consultation operator-handoff/relay-consultation-bundle \
--output-file operator-handoff/approved-set.v1.json

Registryctl verifies the two lane bundles and writes one approved-set file. It reports the approved-set digest and the next registryctl deploy generate command.

The assembled document declares approved-set schema version 2.0. Current registryctl refuses an approved set that declares schema version 1.0, carries a notary lane, or binds cross-lane interface digests. It reports why the document is refused and names re-approval as the remedy. A refused set cannot be repaired by editing the file: the removed cross-lane interface digests are no longer verified, so honoring the old approval would keep a signed integrity claim that nothing enforces. Re-run this procedure from the current lane bundles to issue a schema version 2.0 set.

Do not create another version-1 anchor when an approved lane changes signer. Pin the current lane anchor and current approved set as the preceding trust state, then authorize the transition with the current threshold. For example, rotate the consultation Relay anchor and replace only that lane:

Terminal window
CURRENT_APPROVED_SET=operator-inputs/current-approved-set.v1.json
CURRENT_ANCHOR=operator-handoff/relay-consultation-bundle/anchor.json
ROTATED_TRUST=operator-handoff/relay-consultation-anchor.v2
NEXT_SIGNING_INPUT=.registry-stack/build/local/signing-inputs/relay-consultation
NEXT_BUNDLE=operator-handoff/relay-consultation-bundle.v2
registryctl build \
--environment local \
--against "$CURRENT_APPROVED_SET" \
--rotate-anchor relay-consultation
registryctl trust anchor rotate \
--current-anchor "$CURRENT_ANCHOR" \
--next-public-key evaluation-keys/relay-consultation.public.jwk \
--next-public-key operator-inputs/relay-consultation-next.public.jwk \
--next-threshold 1 \
--key file:evaluation-keys/relay-consultation.private.jwk \
--output-dir "$ROTATED_TRUST"
registryctl trust bundle sign \
--lane relay-consultation \
--input "$NEXT_SIGNING_INPUT" \
--anchor "$ROTATED_TRUST/anchor.json" \
--key file:operator-inputs/relay-consultation-next.private.jwk \
--against "$CURRENT_APPROVED_SET" \
--output-dir "$NEXT_BUNDLE"
registryctl trust bundle verify \
--bundle-dir "$NEXT_BUNDLE" \
--anchor "$NEXT_BUNDLE/anchor.json"
registryctl trust approved-set assemble \
--from "$CURRENT_APPROVED_SET" \
--relay-consultation "$NEXT_BUNDLE" \
--output-file operator-handoff/approved-set.v2.json
registryctl deploy generate \
--approved-set operator-handoff/approved-set.v2.json \
--output-dir operator-handoff/registry-stack.v2
registryctl deploy verify --package operator-handoff/registry-stack.v2

The explicit build selector emits the unchanged consultation input and binds the reviewed update to that lane’s anchor rotation. trust anchor rotate verifies that the transition is authorized by the pinned preceding anchor; the repeated public-key options keep the current signer during the overlap. trust bundle sign --against advances from the pinned preceding approved set, and assembly keeps the unchanged public Relay lane from that set. Deployment verification does not accept the rotated anchor into an existing product state. For thresholds, overlap windows, and the required preview, stop, audited acceptance, exact verification, and startup order, follow Rotate credentials and trust.

Inspect the result without printing key material:

Terminal window
test -f operator-handoff/approved-set.v1.json
registryctl deploy generate \
--approved-set operator-handoff/approved-set.v1.json \
--output-dir operator-handoff/registry-stack
GENERATED_CLOSURE_SHA256="<independently-recorded-generated-closure-sha256>"
registryctl deploy verify \
--package operator-handoff/registry-stack \
--expected-closure-sha256 "$GENERATED_CLOSURE_SHA256"

Keep the approved-set digest and generated closure root outside the package before transfer. This check binds the generated package to that external record, but does not accept it for initialization. Continue with Run the generated single-node Compose package to supply and check operator files before one-time initialization and ordinary startup.

SymptomCauseFix
Anchor creation rejects the public keyThe key is not a supported public JWK or does not match the selected threshold.Export the lane owner’s approved public JWK and retry into a fresh output path.
Bundle signing refuses the keyThe locator is missing file: or op://, or the key does not satisfy the anchor.Use an explicit locator for the matching private key.
Bundle verification failsThe bundle, lane, input, or anchor does not belong to the same approval.Stop the handoff and return the exact input to that lane’s trust owner.
Approved-set assembly reports a missing laneInitial assembly requires one verified public Relay bundle and one verified consultation Relay bundle.Supply both verified lane directories.