feat: prepare ARR for controlled public deployment

This commit is contained in:
Wyndham ARR
2026-07-29 16:38:05 +08:00
commit a701de9f0e
271 changed files with 48472 additions and 0 deletions

View File

@@ -0,0 +1,232 @@
"""HMAC-authenticated result envelopes for untrusted remote callbacks/final content."""
from __future__ import annotations
import hashlib
import hmac
import json
import secrets
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Any, Callable, Dict, Mapping, Optional, Tuple
from arr_ingestion.contracts import OPAQUE_ID_RE, SHA256_RE
from arr_processing.contracts import ProcessingResult, canonical_json_bytes
from arr_processing.errors import ProcessingError
SIGNED_ENVELOPE_VERSION = "arr-processing-signed-1"
SIGNATURE_FIELDS = {
"algorithm",
"key_id",
"issued_at",
"nonce",
"value",
}
SIGNED_FIELDS = {"signed_envelope_version", "payload", "signature"}
MAX_SIGNED_RESULT_BYTES = 512 * 1024
def _duplicate_rejecting_object(pairs: list[Tuple[str, Any]]) -> Dict[str, Any]:
value: Dict[str, Any] = {}
for key, item in pairs:
if key in value:
raise ValueError("duplicate JSON key")
value[key] = item
return value
def _reject_constant(_value: str) -> None:
raise ValueError("non-finite JSON number")
@dataclass(frozen=True)
class VerifiedSignedResult:
result: ProcessingResult
signed_sha256: str
key_id: str
issued_at: datetime
nonce: str
class SignedResultCodec:
"""Sign and verify canonical result payloads without exposing the shared secret."""
def __init__(
self,
keys: Mapping[str, bytes],
*,
signing_key_id: Optional[str] = None,
max_age_seconds: int = 600,
future_skew_seconds: int = 30,
clock: Optional[Callable[[], datetime]] = None,
) -> None:
normalized: Dict[str, bytes] = {}
for key_id, secret in keys.items():
if (
not isinstance(key_id, str)
or not OPAQUE_ID_RE.fullmatch(key_id)
or not isinstance(secret, bytes)
or len(secret) < 32
):
raise ValueError("result signing key configuration is invalid")
normalized[key_id] = bytes(secret)
if not normalized:
raise ValueError("at least one result signing key is required")
if signing_key_id is not None and signing_key_id not in normalized:
raise ValueError("signing key identifier is unavailable")
if max_age_seconds < 1 or future_skew_seconds < 0:
raise ValueError("signature age policy is invalid")
self._keys = normalized
self._signing_key_id = signing_key_id
self._max_age_seconds = max_age_seconds
self._future_skew_seconds = future_skew_seconds
self._clock = clock or (lambda: datetime.now(timezone.utc))
def sign(
self,
result: ProcessingResult,
*,
issued_at: Optional[datetime] = None,
nonce: Optional[str] = None,
) -> bytes:
if self._signing_key_id is None:
raise ValueError("codec is verify-only")
validated_result = ProcessingResult.from_dict(result.to_dict())
if validated_result != result:
raise ValueError("processing result is not canonical")
timestamp = self._normalize_time(issued_at or self._clock())
nonce_value = nonce or ("nonce_" + secrets.token_urlsafe(18))
if not OPAQUE_ID_RE.fullmatch(nonce_value):
raise ValueError("signature nonce is invalid")
issued_text = timestamp.isoformat(timespec="seconds").replace("+00:00", "Z")
payload = validated_result.to_dict()
value = self._signature_value(
self._keys[self._signing_key_id],
payload,
issued_text,
nonce_value,
)
return canonical_json_bytes(
{
"signed_envelope_version": SIGNED_ENVELOPE_VERSION,
"payload": payload,
"signature": {
"algorithm": "hmac-sha256",
"key_id": self._signing_key_id,
"issued_at": issued_text,
"nonce": nonce_value,
"value": value,
},
}
)
def verify(self, raw: bytes) -> VerifiedSignedResult:
if not isinstance(raw, bytes) or not raw or len(raw) > MAX_SIGNED_RESULT_BYTES:
raise ProcessingError(
"PROCESSING_SIGNATURE_INVALID", "signed processing result is invalid"
)
try:
text = raw.decode("utf-8", errors="strict")
parsed = json.loads(
text,
object_pairs_hook=_duplicate_rejecting_object,
parse_constant=_reject_constant,
)
except (UnicodeError, ValueError, json.JSONDecodeError):
raise ProcessingError(
"PROCESSING_SIGNATURE_INVALID", "signed processing result is invalid"
) from None
if not isinstance(parsed, Mapping) or set(parsed) != SIGNED_FIELDS:
raise ProcessingError(
"PROCESSING_SIGNATURE_INVALID", "signed processing result is invalid"
)
if parsed.get("signed_envelope_version") != SIGNED_ENVELOPE_VERSION:
raise ProcessingError(
"PROCESSING_SIGNATURE_INVALID", "signed envelope version is unsupported"
)
signature = parsed.get("signature")
if not isinstance(signature, Mapping) or set(signature) != SIGNATURE_FIELDS:
raise ProcessingError(
"PROCESSING_SIGNATURE_INVALID", "processing result signature is invalid"
)
algorithm = signature.get("algorithm")
key_id = signature.get("key_id")
issued_text = signature.get("issued_at")
nonce = signature.get("nonce")
value = signature.get("value")
if (
algorithm != "hmac-sha256"
or not isinstance(key_id, str)
or key_id not in self._keys
or not isinstance(issued_text, str)
or not isinstance(nonce, str)
or not OPAQUE_ID_RE.fullmatch(nonce)
or not isinstance(value, str)
or not SHA256_RE.fullmatch(value)
):
raise ProcessingError(
"PROCESSING_SIGNATURE_INVALID", "processing result signature is invalid"
)
issued_at = self._parse_time(issued_text)
now = self._normalize_time(self._clock())
age = (now - issued_at).total_seconds()
if age > self._max_age_seconds or age < -self._future_skew_seconds:
raise ProcessingError(
"PROCESSING_SIGNATURE_EXPIRED", "processing result signature is outside its window"
)
result = ProcessingResult.from_dict(parsed.get("payload"))
expected = self._signature_value(
self._keys[key_id],
result.to_dict(),
issued_text,
nonce,
)
if not hmac.compare_digest(value, expected):
raise ProcessingError(
"PROCESSING_SIGNATURE_INVALID", "processing result signature is invalid"
)
return VerifiedSignedResult(
result=result,
signed_sha256=hashlib.sha256(raw).hexdigest(),
key_id=key_id,
issued_at=issued_at,
nonce=nonce,
)
@staticmethod
def _signature_value(
secret: bytes,
payload: Mapping[str, Any],
issued_at: str,
nonce: str,
) -> str:
signed = b"\n".join(
(
SIGNED_ENVELOPE_VERSION.encode("ascii"),
issued_at.encode("ascii"),
nonce.encode("ascii"),
canonical_json_bytes(payload),
)
)
return hmac.new(secret, signed, hashlib.sha256).hexdigest()
@staticmethod
def _parse_time(value: str) -> datetime:
try:
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
except ValueError:
raise ProcessingError(
"PROCESSING_SIGNATURE_INVALID", "processing result signature time is invalid"
) from None
if parsed.tzinfo is None or parsed.utcoffset() is None:
raise ProcessingError(
"PROCESSING_SIGNATURE_INVALID", "processing result signature time is invalid"
)
return parsed.astimezone(timezone.utc)
@staticmethod
def _normalize_time(value: datetime) -> datetime:
if value.tzinfo is None or value.utcoffset() is None:
raise ValueError("signature clock must return a timezone-aware datetime")
return value.astimezone(timezone.utc)