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
43 changes: 22 additions & 21 deletions applications/control.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from applications.file_scan import FileScanReport, scan_file_source
from applications.operator_authentication import (
CONTROL_OPERATOR_SECRET_ENV,
LocalOperatorAuthorities,
LocalControlOperatorConfiguration,
LocalOperatorConfiguration,
)
from applications.release_promotion import promote_release, release_report_json
Expand All @@ -26,6 +26,7 @@
ActivateFileDeleteObservations,
ContextControl,
ControlOperation,
ControlOperatorAuthority,
FileRootRef,
FileSourceProgress,
RegisterFileSource,
Expand Down Expand Up @@ -161,13 +162,13 @@ def main(argv: Sequence[str] | None = None) -> None:
print(rendered, flush=True)


def local_operator_authorities() -> LocalOperatorAuthorities | None:
"""Construct local operator authority only after complete explicit opt-in."""
def local_control_operator_authority() -> ControlOperatorAuthority | None:
"""Construct routine Control authority without loading release credentials."""

configuration = LocalOperatorConfiguration.load(os.environ)
configuration = LocalControlOperatorConfiguration.load(os.environ)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Retain cross-plane credential collision validation

When the documented interactive workflow sources the complete operators.env, this path now loads only the three Control fields, so a Control secret identical to the dogfood, worker, or release credential is accepted by routine Control subcommands. The previous LocalOperatorConfiguration.load() rejected all four-way collisions, and ADR-0069 requires those credentials to remain distinct; although the scheduled-scan parent performs a separate check, documented direct commands such as registration and activation bypass it. Validate any collocated cross-plane secrets without requiring release credentials to be present.

AGENTS.md reference: AGENTS.md:L22-L26

Useful? React with 👍 / 👎.

if configuration is None:
return None
return configuration.authorities()
return configuration.authority()


def _run_operator_subcommand(
Expand All @@ -179,8 +180,8 @@ def _run_operator_subcommand(
| MultiSourceScanReport
| MultiSourceStatusReport
):
authorities = local_operator_authorities()
if authorities is None:
authority = local_control_operator_authority()
if authority is None:
raise SourceNotAvailable
organization_id = UUID(arguments.organization_id)
opaque_credential = os.environ[CONTROL_OPERATOR_SECRET_ENV]
Expand All @@ -196,7 +197,7 @@ def clock() -> datetime:
if arguments.subcommand == "scan-all":
manifests = _list_sources(
organization_id=organization_id,
authorities=authorities,
authority=authority,
opaque_credential=opaque_credential,
engine=engine,
clock=clock,
Expand All @@ -206,7 +207,7 @@ def clock() -> datetime:
scan_file_source(
organization_id=organization_id,
source_ref=manifest.source_ref,
authority=authorities.control,
authority=authority,
opaque_credential=opaque_credential,
engine=engine,
clock=clock,
Expand All @@ -218,7 +219,7 @@ def clock() -> datetime:
return scan_file_source(
organization_id=organization_id,
source_ref=SourceRef(UUID(arguments.source_ref)),
authority=authorities.control,
authority=authority,
opaque_credential=opaque_credential,
engine=engine,
clock=clock,
Expand All @@ -227,28 +228,28 @@ def clock() -> datetime:
operation = _operation(arguments.subcommand)
control = ContextControl(
store=PostgreSQLControlStore(engine, clock=clock),
authority=authorities.control,
authority=authority,
clock=clock,
)
if arguments.subcommand == "status" and arguments.source_ref is None:
manifests = _list_sources_with_control(
control=control,
organization_id=organization_id,
authorities=authorities,
authority=authority,
opaque_credential=opaque_credential,
)
progress = tuple(
_read_status(
control=control,
organization_id=organization_id,
source_ref=manifest.source_ref,
authorities=authorities,
authority=authority,
opaque_credential=opaque_credential,
)
for manifest in manifests
)
return MultiSourceStatusReport(progress)
with authorities.control.authorize(
with authority.authorize(
opaque_credential=opaque_credential,
operation=operation,
request_id=f"local-{arguments.subcommand}-{uuid4().hex}",
Expand Down Expand Up @@ -319,20 +320,20 @@ def __post_init__(self) -> None:
def _list_sources(
*,
organization_id: UUID,
authorities: LocalOperatorAuthorities,
authority: ControlOperatorAuthority,
opaque_credential: str,
engine: Engine,
clock: Callable[[], datetime],
) -> tuple[SourceManifest, ...]:
control = ContextControl(
store=PostgreSQLControlStore(engine, clock=clock),
authority=authorities.control,
authority=authority,
clock=clock,
)
return _list_sources_with_control(
control=control,
organization_id=organization_id,
authorities=authorities,
authority=authority,
opaque_credential=opaque_credential,
)

Expand All @@ -341,10 +342,10 @@ def _list_sources_with_control(
*,
control: ContextControl,
organization_id: UUID,
authorities: LocalOperatorAuthorities,
authority: ControlOperatorAuthority,
opaque_credential: str,
) -> tuple[SourceManifest, ...]:
with authorities.control.authorize(
with authority.authorize(
opaque_credential=opaque_credential,
operation=ControlOperation.READ_SOURCE,
request_id=f"local-list-sources-{uuid4().hex}",
Expand All @@ -359,10 +360,10 @@ def _read_status(
control: ContextControl,
organization_id: UUID,
source_ref: SourceRef,
authorities: LocalOperatorAuthorities,
authority: ControlOperatorAuthority,
opaque_credential: str,
) -> FileSourceProgress:
with authorities.control.authorize(
with authority.authorize(
opaque_credential=opaque_credential,
operation=ControlOperation.READ_SOURCE_PROGRESS,
request_id=f"local-status-{uuid4().hex}",
Expand Down
37 changes: 35 additions & 2 deletions applications/file_scan.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import hashlib
import hmac
import os
from collections.abc import Callable
from dataclasses import dataclass, replace
from datetime import datetime
Expand All @@ -18,8 +19,11 @@
from applications.operator_authentication import (
CONTROL_OPERATOR_SECRET_ENV,
DOGFOOD_SECRET_ENV,
DOGFOOD_SECRET_FINGERPRINT_ENV,
RELEASE_OPERATOR_SECRET_ENV,
RELEASE_OPERATOR_SECRET_FINGERPRINT_ENV,
WORKER_SECRET_ENV,
local_secret_fingerprint,
)
from engine.control import (
FILE_DELETE_OBSERVATION_CAPABILITY_MANIFEST,
Expand Down Expand Up @@ -355,14 +359,27 @@ def _proof_keys() -> tuple[Ed25519PrivateKey, Ed25519PrivateKey]:
checkpoint_material = _private_key_material(CHECKPOINT_SIGNING_KEY_ENV)
operator_secret_values = (
required_environment(CONTROL_OPERATOR_SECRET_ENV),
required_environment(RELEASE_OPERATOR_SECRET_ENV),
required_environment(DOGFOOD_SECRET_ENV),
required_environment(WORKER_SECRET_ENV),
)
encoded_proof_values = (
provider_material.hex(),
checkpoint_material.hex(),
)
external_fingerprints = tuple(
_external_secret_fingerprint(secret_name, fingerprint_name)
for secret_name, fingerprint_name in (
(RELEASE_OPERATOR_SECRET_ENV, RELEASE_OPERATOR_SECRET_FINGERPRINT_ENV),
(DOGFOOD_SECRET_ENV, DOGFOOD_SECRET_FINGERPRINT_ENV),
)
)
if any(
hmac.compare_digest(
local_secret_fingerprint(proof_value),
external_fingerprint,
)
for proof_value in encoded_proof_values
for external_fingerprint in external_fingerprints
) or any(
hmac.compare_digest(proof_value, operator_secret.lower())
for proof_value in encoded_proof_values
for operator_secret in operator_secret_values
Expand All @@ -386,6 +403,22 @@ def _proof_keys() -> tuple[Ed25519PrivateKey, Ed25519PrivateKey]:
)


def _external_secret_fingerprint(secret_name: str, fingerprint_name: str) -> str:
raw = os.environ.get(secret_name)
if raw is not None:
return local_secret_fingerprint(raw)
fingerprint = required_environment(fingerprint_name)
if len(fingerprint) != 64:
raise SourceNotAvailable
try:
decoded = bytes.fromhex(fingerprint)
except ValueError:
raise SourceNotAvailable from None
if len(decoded) != 32:
raise SourceNotAvailable
return fingerprint.lower()


def _positive_bigint(value: str) -> int:
if not value.isascii() or not value.isdecimal():
raise SourceNotAvailable
Expand Down
76 changes: 74 additions & 2 deletions applications/operator_authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import hashlib
import hmac
from collections.abc import Callable, Mapping
from dataclasses import dataclass, field
Expand All @@ -27,6 +28,10 @@
CONTROL_OPERATOR_OPERATIONS_ENV = "CONTEXT_ENGINE_CONTROL_OPERATOR_OPERATIONS"
DOGFOOD_SECRET_ENV = "CONTEXT_ENGINE_DOGFOOD_SECRET"
WORKER_SECRET_ENV = "CONTEXT_ENGINE_WORKER_LEASE_SIGNING_KEY_HEX"
RELEASE_OPERATOR_SECRET_FINGERPRINT_ENV = (
"CONTEXT_ENGINE_RELEASE_OPERATOR_SECRET_SHA256"
)
DOGFOOD_SECRET_FINGERPRINT_ENV = "CONTEXT_ENGINE_DOGFOOD_SECRET_SHA256"
OPERATOR_ENVIRONMENT_VARIABLES = frozenset(
{
CONTROL_OPERATOR_SECRET_ENV,
Expand Down Expand Up @@ -54,6 +59,70 @@ def __init__(self) -> None:
super().__init__("operator authentication rejected")


def local_secret_fingerprint(value: str) -> str:
"""Fingerprint one local secret for collision checks without delegating it."""

if type(value) is not str or not value:
raise LocalOperatorConfigurationUnavailable
return hashlib.sha256(value.lower().encode("utf-8")).hexdigest()


@dataclass(frozen=True, slots=True)
class LocalControlOperatorConfiguration:
"""The routine Control identity without any release publication credential."""

organization_id: UUID
control_secret: bytes = field(repr=False)
control_operations: frozenset[ControlOperation] = field(repr=False)

@classmethod
def load(
cls,
environment: Mapping[str, str],
) -> LocalControlOperatorConfiguration | None:
names = frozenset(
{
CONTROL_OPERATOR_SECRET_ENV,
OPERATOR_ORGANIZATION_ENV,
CONTROL_OPERATOR_OPERATIONS_ENV,
}
)
configured = names.intersection(environment)
if not configured:
return None
if configured != names:
raise LocalOperatorConfigurationUnavailable
try:
raw_operations = environment[CONTROL_OPERATOR_OPERATIONS_ENV].split(",")
if any(not value or value != value.strip() for value in raw_operations):
raise ValueError
operations = frozenset(ControlOperation(value) for value in raw_operations)
if len(operations) != len(raw_operations):
raise ValueError
return cls(
organization_id=UUID(environment[OPERATOR_ORGANIZATION_ENV]),
control_secret=_secret(environment[CONTROL_OPERATOR_SECRET_ENV]),
control_operations=operations,
)
except (KeyError, TypeError, ValueError, UnicodeError):
raise LocalOperatorConfigurationUnavailable from None

def authority(
self,
*,
clock: Callable[[], datetime] | None = None,
) -> ControlOperatorAuthority:
active_clock = clock or (lambda: datetime.now(UTC))
return ControlOperatorAuthority(
LocalControlOperatorAuthenticator(self, clock=active_clock),
call_ttl=LOCAL_OPERATOR_TTL,
clock=active_clock,
)

def __repr__(self) -> str:
return "LocalControlOperatorConfiguration(<redacted>)"


def _secret(value: object) -> bytes:
if (
type(value) is not str
Expand Down Expand Up @@ -181,11 +250,14 @@ class LocalControlOperatorAuthenticator:

def __init__(
self,
configuration: LocalOperatorConfiguration,
configuration: LocalOperatorConfiguration | LocalControlOperatorConfiguration,
*,
clock: Callable[[], datetime],
) -> None:
if type(configuration) is not LocalOperatorConfiguration:
if type(configuration) not in {
LocalOperatorConfiguration,
LocalControlOperatorConfiguration,
}:
raise TypeError("operator authentication rejected")
if not callable(clock):
raise TypeError("operator authentication rejected")
Expand Down
40 changes: 40 additions & 0 deletions deploy/daily-driver/api.plist.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>${label_prefix}.api</string>
<key>ProgramArguments</key>
<array>
<string>${python}</string>
<string>-m</string>
<string>scripts.daily_driver.jobs</string>
<string>daemon</string>
<string>--service</string>
<string>api</string>
<string>--checkout</string>
<string>${checkout}</string>
<string>--database-environment</string>
<string>${database_environment}</string>
<string>--operator-environment</string>
<string>${operator_environment}</string>
<string>--api-port</string>
<string>${api_port}</string>
</array>
<key>WorkingDirectory</key>
<string>${checkout}</string>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<dict>
<key>SuccessfulExit</key>
<false/>
</dict>
<key>ProcessType</key>
<string>Background</string>
<key>StandardOutPath</key>
<string>${log_root}/api.log</string>
<key>StandardErrorPath</key>
<string>${log_root}/api.error.log</string>
</dict>
</plist>
Loading
Loading