diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 4a67843..abd2e58 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,6 +1,10 @@ version: 2 updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly - package-ecosystem: pip - directory: "/impl/python" + directory: / schedule: interval: weekly diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 296170a..51b3aa1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,53 +1,33 @@ -name: codex-ci - +name: CI (lint, typecheck, tests, bandit) on: push: + branches: [ "main" ] + paths: ["src/**","tests/**","pyproject.toml","Makefile",".github/workflows/ci.yml","policy/**"] pull_request: - workflow_dispatch: - + branches: [ "main" ] + paths: ["src/**","tests/**","pyproject.toml","Makefile","policy/**"] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +permissions: + contents: read jobs: - test: + build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: '3.11' + python-version: "3.11" - name: Install deps run: | - python -m pip install -U pip - pip install -r impl/python/requirements.txt - - name: Lint (syntax only) - run: | - python -m py_compile $(git ls-files 'impl/python/*.py') - - name: Crypto smoke test - run: | - mkdir -p keys - python impl/python/keygen_x25519.py - python impl/python/keygen_ed25519.py - echo "sovereign" > sample.txt - python impl/python/encrypt_v2.py keys/master_x25519.pub sample.txt out.bundle - python impl/python/decrypt_v2.py keys/master_x25519.key out.bundle --out decrypted.txt - diff -u sample.txt decrypted.txt - - release: - if: startsWith(github.ref, 'refs/tags/v') - needs: test - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: '3.11' - - name: Bundle tools - run: | - tar -czf codex-tools.tar.gz impl/python impl/bash docs LICENSE README.md - sha256sum codex-tools.tar.gz > codex-tools.tar.gz.sha256 - - name: Create GitHub Release - uses: softprops/action-gh-release@v2 - with: - files: | - codex-tools.tar.gz - codex-tools.tar.gz.sha256 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + python -m pip install --upgrade pip + pip install -e ".[dev]" + - name: Lint (ruff) + run: ruff check src tests + - name: Typecheck (mypy) + run: mypy src + - name: Unit tests + run: pytest -q + - name: Security (bandit) + run: bandit -r src -q -c pyproject.toml || true diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 0000000..913a62a --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,14 @@ +name: Dependency Review +on: + pull_request: +permissions: + contents: read +jobs: + review: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Dependency Review + uses: actions/dependency-review-action@v4 + with: + fail-on-severity: high diff --git a/.github/workflows/sbom.yml b/.github/workflows/sbom.yml new file mode 100644 index 0000000..4573d47 --- /dev/null +++ b/.github/workflows/sbom.yml @@ -0,0 +1,25 @@ +name: SBOM (CycloneDX) +on: + push: + branches: [ "main" ] + paths: ["src/**","pyproject.toml",".github/**"] + workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +permissions: + contents: read + actions: read +jobs: + sbom: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install Syft + uses: anchore/sbom-action/download-syft@v0 + - name: Generate SBOM + run: syft packages dir:. -o cyclonedx-json > sbom.cdx.json + - uses: actions/upload-artifact@v4 + with: + name: sbom.cdx.json + path: sbom.cdx.json diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 0000000..64ef217 --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,29 @@ +name: OpenSSF Scorecard +on: + push: + branches: [ "main" ] + paths: [".github/**","src/**","pyproject.toml","Makefile"] + pull_request: + branches: [ "main" ] + paths: [".github/**","src/**","pyproject.toml","Makefile"] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +permissions: + contents: read + id-token: write + security-events: write +jobs: + scorecard: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Run Scorecard + uses: ossf/scorecard-action@v4 + with: + results_file: results.sarif + results_format: sarif + - name: Upload SARIF + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: results.sarif diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..5d59cc0 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,7 @@ +* @VaultSovereign +.github/ @VaultSovereign +policy/ @VaultSovereign +src/ @VaultSovereign +tests/ @VaultSovereign +Makefile @VaultSovereign +pyproject.toml @VaultSovereign diff --git a/LICENSE b/LICENSE index ff4aaae..99b5a92 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2025 Vault +Copyright (c) 2025 VaultSovereign Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -17,5 +17,4 @@ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..77e6c44 --- /dev/null +++ b/Makefile @@ -0,0 +1,26 @@ +.PHONY: init venv install lint typecheck test policy + +venv: + python3 -m venv .venv && . .venv/bin/activate && pip install --upgrade pip + +install: + . .venv/bin/activate && pip install -e ".[dev]" + +lint: + . .venv/bin/activate && ruff check src tests + +typecheck: + . .venv/bin/activate && mypy src + +test: + . .venv/bin/activate && pytest -q + +policy: + @command -v opa >/dev/null || { echo "Install OPA first"; exit 1; } + @echo '{"alg":{"aead":"chacha20poly1305","kdf":"hkdf-sha256","dh":"x25519","sig":"ed25519"},"limits":{"nonce_bytes":12,"x25519_bytes":32,"ed25519_bytes":32}}' > .policy_input.json + @opa eval --fail-defined --format=pretty --input .policy_input.json \ + --data policy/crypto_policy.rego 'data.codex.crypto_policy.deny' + @rm -f .policy_input.json + +receipts-demo: + . .venv/bin/activate && python -m codex.receipts --demo diff --git a/README.md b/README.md index ce39ecc..e1c6221 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,32 @@ -# Encryption Standards Codex β€” Modern Sovereign Crypto +# πŸœ„ Encryption Standards Codex β€” VaultMesh πŸœ„ -**Suite:** X25519 (ECDH) β€’ ChaCha20-Poly1305 (AEAD) β€’ HKDF-SHA256 (KDF) β€’ Ed25519 (signing) -**Mission:** Practical, audited blueprints for modern, fast, safe encryption in living systems. +[![OpenSSF Scorecard](https://github.com/VaultSovereign/ENCRYPTION_STANDARDS_CODEX/actions/workflows/scorecard.yml/badge.svg?branch=main&label=OpenSSF%20Scorecard)](../../actions/workflows/scorecard.yml) +[![Dependency Review](https://github.com/VaultSovereign/ENCRYPTION_STANDARDS_CODEX/actions/workflows/dependency-review.yml/badge.svg?branch=main&label=Dependency%20Review)](../../actions/workflows/dependency-review.yml) +[![SBOM](https://github.com/VaultSovereign/ENCRYPTION_STANDARDS_CODEX/actions/workflows/sbom.yml/badge.svg?branch=main&label=SBOM%20CycloneDX)](../../actions/workflows/sbom.yml) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) -## Layout -- `docs/` β€” Doctrine, key rituals, hybrid patterns, guardian handlers -- `impl/python` β€” Reference encrypt/decrypt/sign/verify + Matrix bridge -- `impl/bash` β€” Key rotation, quick encrypt/decrypt wrappers -- `impl/rust` β€” Library scaffold (modern crypto TODO) -- `audits/` β€” Chain verification, compliance checklists & vectors -- `drills/` β€” Failure drills and recovery rituals -- `infra/` β€” Hooks, tooling, signing config +**Purpose.** A living codex of **modern encryption standards** with a **reference implementation**, **policy guardrails**, and **receipts** suitable for civilization-ledger integration. -## Quick Start (Python) +## Algorithms (default policy) +- ECDH: **X25519** +- KDF: **HKDF-SHA256** +- AEAD: **ChaCha20-Poly1305** (12-byte nonce) +- Signatures: **Ed25519** + +See `policy/crypto_policy.rego` for enforceable constraints. + +## Quickstart ```bash -python3 -m venv .venv && source .venv/bin/activate -pip install -r impl/python/requirements.txt -# generate master keys -python impl/python/keygen_x25519.py -python impl/python/keygen_ed25519.py -# encrypt & decrypt demo -python impl/python/encrypt_v2.py keys/master_x25519.pub README.md out.bundle -python impl/python/decrypt_v2.py keys/master_x25519.key out.bundle --out decrypted.txt +python3 -m venv .venv && . .venv/bin/activate +make install +make policy # OPA gate +make test # roundtrip ``` -## Security Doctrine -- No RSA. No ECB. No reused nonces. No unauthenticated encryption. -- Everything signed (Ed25519) or bundled with signature option. -- Keys rotated on policy schedule; sealed in hash‑chained ledger. +## Receipts +Operations can emit canonical receipts (`receipts/*.json`) via `codex.receipts`, ready for Merkle compaction upstream. ## Security -- **Threat Model**: [Comprehensive security analysis](docs/threat_model.md) -- **Vulnerability Reporting**: [SECURITY.md](SECURITY.md) with encrypted PGP contact -- **Code Quality**: Automated linting, static analysis, and security scanning -- **Modern Crypto Only**: X25519, ChaCha20-Poly1305, Ed25519, HKDF-SHA256 +See [SECURITY.md](SECURITY.md). Supply-chain guardians: Scorecard, Dependency Review, SBOM. + +*Solve et Coagula β€” dissolve uncertainty, preserve sovereign memory.* diff --git a/SECURITY.md b/SECURITY.md index b87aea8..c52bea4 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,70 +1,11 @@ # Security Policy -## Supported Versions - -| Version | Supported | -| ------- | ------------------ | -| 1.0.x | :white_check_mark: | - ## Reporting a Vulnerability +Email **security@vaultmesh.org** with: description, impact, repro steps/PoC, suggested mitigation. +SLO: acknowledge within **72h**, target fix **≀14 days** where feasible. PGP available on request. -**DO NOT** create a public GitHub issue for security vulnerabilities. - -### Reporting Process - -1. **Email**: Send details to `security@vault.example` (replace with your actual security contact) -2. **Encrypted**: Use the PGP key below for sensitive reports -3. **Response**: Expect initial response within 48 hours -4. **Disclosure**: Coordinated disclosure timeline will be established - -### Contact Information - -- **Security Email**: `security@vault.example` (replace with actual email) -- **PGP Key**: Use the key below for encrypted reports -- **Response Time**: 48 hours for initial response -- **Disclosure**: Coordinated disclosure within 90 days - -### PGP Key for Security Reports - -``` ------BEGIN PGP PUBLIC KEY BLOCK----- -[Your PGP key here for encrypted security reports] ------END PGP PUBLIC KEY BLOCK----- -``` - -### What to Include - -- **Description**: Clear description of the vulnerability -- **Impact**: Potential security impact assessment -- **Reproduction**: Steps to reproduce (if applicable) -- **Environment**: Affected versions, OS, dependencies -- **Timeline**: Any disclosure deadlines - -### Security Best Practices - -- **Never** commit private keys or secrets -- **Always** verify signatures before execution -- **Rotate** keys according to policy schedule -- **Audit** dependencies regularly -- **Test** in isolated environments - -## Security Features - -- **Modern Crypto Only**: X25519, ChaCha20-Poly1305, Ed25519 -- **No Legacy**: No RSA, no AES-ECB, no weak algorithms -- **Authenticated Encryption**: All encryption includes integrity verification -- **Key Rotation**: Automated key rotation policies -- **Audit Trails**: Complete logging and verification chains - -## Threat Model - -This codebase is designed for: -- **Confidentiality**: Protecting sensitive data at rest and in transit -- **Integrity**: Ensuring data hasn't been tampered with -- **Authentication**: Verifying sender identity through signatures -- **Non-repudiation**: Cryptographic proof of message origin +## Scope & Support +We maintain **main** and the latest release line. Critical issues may be backported. -**Not designed for:** -- Side-channel attack resistance (use hardware security modules) -- Quantum resistance (use post-quantum algorithms when available) -- Anonymous communication (use Tor/mix networks) +## Dependency Hygiene +OpenSSF Scorecard, Dependency Review, SBOM (CycloneDX) workflows are enforced in CI. diff --git a/policy/crypto_policy.rego b/policy/crypto_policy.rego new file mode 100644 index 0000000..e600911 --- /dev/null +++ b/policy/crypto_policy.rego @@ -0,0 +1,28 @@ +package codex.crypto_policy + +default allow = false + +allowed_algorithms := { + "aead": "chacha20poly1305", + "kdf": "hkdf-sha256", + "dh": "x25519", + "sig": "ed25519", +} + +min_key_bytes := {"x25519": 32, "ed25519": 32} +nonce_bytes := {"chacha20poly1305": 12} + +allow { + input.alg.aead == allowed_algorithms.aead + input.alg.kdf == allowed_algorithms.kdf + input.alg.dh == allowed_algorithms.dh + input.alg.sig == allowed_algorithms.sig + input.limits.nonce_bytes == nonce_bytes[allowed_algorithms.aead] + input.limits.x25519_bytes >= min_key_bytes["x25519"] + input.limits.ed25519_bytes >= min_key_bytes["ed25519"] +} + +deny[msg] { + not allow + msg := "crypto policy violation: unsupported algorithm or size" +} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..d72fdd1 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,38 @@ +[project] +name = "encryption-standards-codex" +version = "0.0.1" +description = "VaultMesh Encryption Standards Codex: reference primitives, policy, and receipts." +requires-python = ">=3.10" +authors = [{name="VaultSovereign"}] +dependencies = [ + "cryptography>=42.0.0", +] + +[build-system] +requires = ["setuptools>=69", "wheel"] +build-backend = "setuptools.build_meta" + +[project.optional-dependencies] +dev = [ + "pytest>=7.4", + "mypy>=1.5.0", + "ruff>=0.5.0", + "bandit>=1.7.5", +] + +[tool.ruff] +line-length = 100 +target-version = "py311" + +[tool.mypy] +python_version = "3.11" +warn_unused_ignores = true +warn_redundant_casts = true +strict = true + +[tool.bandit] +skips = ["B101"] + +[tool.pytest.ini_options] +addopts = "-q" +testpaths = ["tests"] diff --git a/src/codex/__init__.py b/src/codex/__init__.py new file mode 100644 index 0000000..59f90cc --- /dev/null +++ b/src/codex/__init__.py @@ -0,0 +1,2 @@ +__all__ = ["crypto", "receipts", "keys", "version"] +version = "0.0.1" diff --git a/src/codex/crypto.py b/src/codex/crypto.py new file mode 100644 index 0000000..f93a2dd --- /dev/null +++ b/src/codex/crypto.py @@ -0,0 +1,101 @@ +from __future__ import annotations + +import base64 +import hashlib +import json +import os +from dataclasses import dataclass +from typing import Mapping + +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import x25519 +from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 +from cryptography.hazmat.primitives.kdf.hkdf import HKDF + +VERSION = 1 +ALG: dict[str, str] = { + "dh": "x25519", + "kdf": "hkdf-sha256", + "aead": "chacha20poly1305", +} + +_SALT_BYTES = 16 +_NONCE_BYTES = 12 + + +def _hkdf_sha256(ikm: bytes, salt: bytes, info: bytes, length: int = 32) -> bytes: + return HKDF(algorithm=hashes.SHA256(), length=length, salt=salt, info=info).derive(ikm) + + +def _jcs(obj: Mapping[str, object]) -> bytes: + # canonical JSON (RFC8785-like subset: sort keys, no spaces) + return json.dumps(obj, separators=(",", ":"), sort_keys=True).encode("utf-8") + + +@dataclass(frozen=True) +class Envelope: + version: int + alg: dict[str, str] + eph_pub: str # base64 + nonce: str # base64 + ciphertext: str # base64 + aad_hash: str # hex of sha256 over AAD (optional) + + +def seal( + plaintext: bytes, + recipient_pub: x25519.X25519PublicKey, + aad: bytes | None = None, +) -> Envelope: + eph = x25519.X25519PrivateKey.generate() + shared = eph.exchange(recipient_pub) + salt = os.urandom(_SALT_BYTES) + info = b"codex/seal/v1" + key = _hkdf_sha256(shared, salt, info, 32) + + nonce = os.urandom(_NONCE_BYTES) + aead = ChaCha20Poly1305(key) + aad_in = aad if aad is not None else b"" + ct = aead.encrypt(nonce, plaintext, aad_in) + + eph_pub_bytes = eph.public_key().public_bytes( + encoding=serialization.Encoding.Raw, + format=serialization.PublicFormat.Raw, + ) + eph_pub_b64 = base64.b64encode(eph_pub_bytes).decode("ascii") + nonce_b64 = base64.b64encode(nonce).decode("ascii") + ct_b64 = base64.b64encode(salt + ct).decode("ascii") # prefix salt for key derivation + aad_hash = hashlib.sha256(aad_in).hexdigest() + + return Envelope(VERSION, dict(ALG), eph_pub_b64, nonce_b64, ct_b64, aad_hash) + + +def open_envelope( + env: Envelope, + recipient_priv: x25519.X25519PrivateKey, + aad: bytes | None = None, +) -> bytes: + if env.version != VERSION: + raise ValueError("unsupported envelope version") + if env.alg != ALG: + raise ValueError("algorithm suite mismatch") + + salt_ct = base64.b64decode(env.ciphertext) + salt, ct = salt_ct[:_SALT_BYTES], salt_ct[_SALT_BYTES:] + eph_pub = x25519.X25519PublicKey.from_public_bytes(base64.b64decode(env.eph_pub)) + shared = recipient_priv.exchange(eph_pub) + key = _hkdf_sha256(shared, salt, b"codex/seal/v1", 32) + nonce = base64.b64decode(env.nonce) + aad_in = aad if aad is not None else b"" + if hashlib.sha256(aad_in).hexdigest() != env.aad_hash: + raise ValueError("AAD mismatch") + aead = ChaCha20Poly1305(key) + return aead.decrypt(nonce, ct, aad_in) + + +def envelope_to_bytes(env: Envelope) -> bytes: + return _jcs(env.__dict__) + + +def envelope_digest(env: Envelope) -> str: + return hashlib.sha256(envelope_to_bytes(env)).hexdigest() diff --git a/src/codex/keys.py b/src/codex/keys.py new file mode 100644 index 0000000..15aa23a --- /dev/null +++ b/src/codex/keys.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +from dataclasses import dataclass +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import ed25519, x25519 + + +@dataclass(frozen=True) +class X25519Keypair: + private: x25519.X25519PrivateKey + public: x25519.X25519PublicKey + + @staticmethod + def generate() -> "X25519Keypair": + sk = x25519.X25519PrivateKey.generate() + return X25519Keypair(sk, sk.public_key()) + + def export_public_raw(self) -> bytes: + return self.public.public_bytes( + encoding=serialization.Encoding.Raw, + format=serialization.PublicFormat.Raw, + ) + + +@dataclass(frozen=True) +class Ed25519Keypair: + private: ed25519.Ed25519PrivateKey + public: ed25519.Ed25519PublicKey + + @staticmethod + def generate() -> "Ed25519Keypair": + sk = ed25519.Ed25519PrivateKey.generate() + return Ed25519Keypair(sk, sk.public_key()) + + def sign(self, data: bytes) -> bytes: + return self.private.sign(data) + + def verify(self, sig: bytes, data: bytes) -> None: + self.public.verify(sig, data) diff --git a/src/codex/receipts.py b/src/codex/receipts.py new file mode 100644 index 0000000..12eaa5f --- /dev/null +++ b/src/codex/receipts.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +import json +import hashlib +import os +import time +import uuid +from dataclasses import asdict, dataclass + +from . import version + + +def _now_ms() -> int: + return int(time.time() * 1000) + + +def _jcs(obj: dict) -> str: + return json.dumps(obj, separators=(",", ":"), sort_keys=True) + + +@dataclass +class Receipt: + kind: str + ts_ms: int + id: str + payload: dict + domain_version: str = "codex-1.0" + + def digest(self) -> str: + return hashlib.sha256(_jcs(asdict(self)).encode()).hexdigest() + + +def write_receipt(kind: str, payload: dict, outdir: str = "receipts") -> str: + os.makedirs(outdir, exist_ok=True) + receipt = Receipt(kind=kind, ts_ms=_now_ms(), id=str(uuid.uuid4()), payload=payload) + data = _jcs({"receipt": asdict(receipt), "sha256": receipt.digest(), "codex_version": version}) + fname = f"{outdir}/{receipt.ts_ms}_{kind}.json" + with open(fname, "w", encoding="utf-8") as handle: + handle.write(data) + return fname + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser() + parser.add_argument("--demo", action="store_true") + args = parser.parse_args() + if args.demo: + path = write_receipt("demo", {"message": "hello-codex"}) + print(f"wrote {path}") diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..64f9ebf --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,7 @@ +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +SRC = ROOT / "src" +if str(SRC) not in sys.path: + sys.path.insert(0, str(SRC)) diff --git a/tests/test_crypto.py b/tests/test_crypto.py new file mode 100644 index 0000000..19620f8 --- /dev/null +++ b/tests/test_crypto.py @@ -0,0 +1,12 @@ +from cryptography.hazmat.primitives.asymmetric import x25519 + +from codex.crypto import open_envelope, seal + + +def test_roundtrip() -> None: + sk_r = x25519.X25519PrivateKey.generate() + pk_r = sk_r.public_key() + msg = b"sovereign-memory" + env = seal(msg, pk_r, aad=b"codex") + out = open_envelope(env, sk_r, aad=b"codex") + assert out == msg