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
7 changes: 4 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ jobs:
with:
go-version-file: go.mod
- run: |
go install golang.org/x/vuln/cmd/govulncheck@latest
go install golang.org/x/vuln/cmd/govulncheck@d1f380186385b4f64e00313f31743df8e4b89a77 # v1.1.4
govulncheck ./...

go-mod-verify:
Expand Down Expand Up @@ -218,8 +218,9 @@ jobs:
AA_ADMIN_SECRET: live-test-secret-32bytes-long-ok # known test fixture
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install python cryptography (for Ed25519 challenge-response)
run: python3 -m pip install --user cryptography
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
- name: Start broker
run: |
export AA_ADMIN_SECRET="$AA_ADMIN_SECRET"
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@ on:
workflow_dispatch:

permissions:
actions: read
contents: read
security-events: write

jobs:
analyze:
name: analyze
runs-on: ubuntu-latest
permissions:
actions: read
security-events: write
strategy:
fail-fast: false
matrix:
Expand Down
6 changes: 4 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,15 @@ concurrency:

permissions:
contents: read
packages: write # not used for Docker Hub, but future-proof if we mirror to GHCR
id-token: write # required for cosign keyless signing (OIDC)

jobs:
publish:
name: publish-dockerhub
runs-on: ubuntu-latest
permissions:
contents: read
packages: write # not used for Docker Hub, but future-proof if we mirror to GHCR
id-token: write # required for cosign keyless signing (OIDC)
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Security — OSSF Scorecard Tier-1 hardening (2026-05-13)

- Moved top-level write permissions to job level in `.github/workflows/codeql.yml` (`security-events: write`, `actions: read`) and `.github/workflows/release.yml` (`packages: write`, `id-token: write`). Top level keeps `contents: read` only. OSSF Scorecard Token-Permissions check expected to lift from 0 → 10.
- Pinned Docker base images by SHA digest in `Dockerfile`: `golang:1.24-alpine@sha256:8bee1901…1b7191` and `alpine:3.21@sha256:48b0309c…b7abc07d`. Tag names preserved on preceding comment lines (Dockerfile `FROM` syntax rejects trailing inline comments after `AS stage`). Rotated weekly by Dependabot's existing docker ecosystem.
- Pinned `govulncheck` install in `.github/workflows/ci.yml` by commit SHA `d1f3801` (v1.1.4) instead of `@latest`.
- Replaced the Python `cryptography` invocations in `tests/sec-l2b/integration.sh` AND `scripts/smoke/core-contract.sh` with a pure Go helper at `tests/sec-l2b/edsign/main.go` (Go stdlib `crypto/ed25519`, ~50 lines). Removed the `pip install --user cryptography` step from CI; added `actions/setup-go` to `smoke-l25` since the job now builds the helper. **The repo is now Python-free across `.github/`, `tests/sec-l2b/`, and `scripts/smoke/`.** Acceptance verified locally against live broker: `core-contract.sh` 10/10 PASS, `integration.sh` 9 PASS / 1 SKIP (HSTS-TLS) / 0 FAIL.
- Net: OSSF Scorecard Pinned-Dependencies expected to lift from 8 → 10. Overall score expected to lift from 6.2 to ~7.3.

### Fixed — remaining current-surface AgentAuth references (2026-05-13)

- Replaced stale current-surface `AgentAuth` / `agentauth` references with `AgentWrit` / `agentwrit` in CLI text, broker startup output, Go comments, Python SDK examples, config headers, contribution-policy text, and SEC-L2b setup files.
Expand Down
6 changes: 4 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Stage 1: Build broker binary
FROM golang:1.24-alpine AS builder
# Pinned by digest (Dependabot docker ecosystem rotates weekly). Tag: golang:1.24-alpine
FROM golang:1.24-alpine@sha256:8bee1901f1e530bfb4a7850aa7a479d17ae3a18beb6e09064ed54cfd245b7191 AS builder

WORKDIR /app

Expand All @@ -13,7 +14,8 @@ COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -o broker ./cmd/broker

# Stage 2: Broker image
FROM alpine:3.21 AS broker
# Pinned by digest (Dependabot docker ecosystem rotates weekly). Tag: alpine:3.21
FROM alpine:3.21@sha256:48b0309ca019d89d40f670aa1bc06e426dc0931948452e8491e3d65087abc07d AS broker

# OCI image labels — populated by docker/metadata-action in the release workflow.
# Static labels (title/licenses/vendor) are baked in here so they're correct even
Expand Down
104 changes: 41 additions & 63 deletions scripts/smoke/core-contract.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,23 +25,19 @@ set -euo pipefail
# AA_ADMIN_SECRET (default: live-test-secret-32bytes-long-ok)
# BROKER_URL (default: http://localhost:8080)
#
# Dependencies: curl, jq, python3 with cryptography installed.
# python3 + cryptography is the established pattern for challenge-response
# in this repo — see tests/sec-l2b/integration.sh for prior art.
# Dependencies: curl, jq, go (for the Ed25519 helper at tests/sec-l2b/edsign).
# Go stdlib only — matches the project rule that all crypto is Go stdlib
# (.claude/rules/golang.md). See tests/sec-l2b/integration.sh for the same pattern.

BROKER_URL="${BROKER_URL:-http://localhost:8080}"
AA_ADMIN_SECRET="${AA_ADMIN_SECRET:-live-test-secret-32bytes-long-ok}"

for dep in curl jq python3; do
for dep in curl jq go; do
if ! command -v $dep &>/dev/null; then
echo "FAIL: missing dependency: $dep"
exit 1
fi
done
if ! python3 -c 'from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey' &>/dev/null; then
echo "FAIL: python3 cryptography package not installed (pip install cryptography)"
exit 1
fi

step=0
pass() { step=$((step+1)); echo " [$step] PASS: $1"; }
Expand Down Expand Up @@ -79,34 +75,28 @@ NONCE=$(echo "$CHALLENGE_RESP" | jq -r '.nonce // empty')
pass "challenge nonce fetched"

# --- Step 5: Register agent (Ed25519 challenge-response) ---
# Python generates a keypair, signs the hex-decoded nonce, and POSTs to /v1/register.
REG_RESP=$(python3 <<PYEOF
import json, base64, urllib.request, sys
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat

key = Ed25519PrivateKey.generate()
pub_bytes = key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
sig = key.sign(bytes.fromhex("${NONCE}"))

body = json.dumps({
"launch_token": "${LAUNCH_TOKEN}",
"nonce": "${NONCE}",
"public_key": base64.b64encode(pub_bytes).decode(),
"signature": base64.b64encode(sig).decode(),
"orch_id": "smoke-orch",
"task_id": "smoke-task",
"requested_scope": ["read:data:*"],
}).encode()
req = urllib.request.Request("${BROKER_URL}/v1/register", data=body,
headers={"Content-Type": "application/json"})
try:
print(urllib.request.urlopen(req).read().decode())
except urllib.error.HTTPError as e:
print(json.dumps({"error": e.code, "body": e.read().decode()}), file=sys.stderr)
sys.exit(1)
PYEOF
)
# Build the Go Ed25519 helper (stdlib only — see tests/sec-l2b/edsign/main.go).
EDSIGN_BIN="${EDSIGN_BIN:-/tmp/sec-l2b-edsign}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
(cd "${REPO_ROOT}" && go build -o "$EDSIGN_BIN" ./tests/sec-l2b/edsign) \
|| fail "edsign build" "go build of tests/sec-l2b/edsign failed"

SIG_JSON=$("$EDSIGN_BIN" "$NONCE")
PUB_B64=$(echo "$SIG_JSON" | jq -r '.public_key // empty')
SIG_B64=$(echo "$SIG_JSON" | jq -r '.signature // empty')
[[ -n "$PUB_B64" && -n "$SIG_B64" ]] || fail "edsign helper" "missing public_key/signature in: $SIG_JSON"

REG_RESP=$(curl -sf -X POST "$BROKER_URL/v1/register" \
-H "Content-Type: application/json" \
-d "$(jq -nc \
--arg lt "$LAUNCH_TOKEN" \
--arg n "$NONCE" \
--arg pk "$PUB_B64" \
--arg s "$SIG_B64" \
'{launch_token:$lt, nonce:$n, public_key:$pk, signature:$s,
orch_id:"smoke-orch", task_id:"smoke-task", requested_scope:["read:data:*"]}')" \
|| echo "{}")
AGENT_TOKEN=$(echo "$REG_RESP" | jq -r '.access_token // empty')
AGENT_ID=$(echo "$REG_RESP" | jq -r '.agent_id // empty')
[[ -n "$AGENT_TOKEN" ]] || fail "agent registration" "no access_token in response: $REG_RESP"
Expand Down Expand Up @@ -170,33 +160,21 @@ LT2=$(echo "$LT2_RESP" | jq -r '.launch_token // empty')
NONCE2=$(curl -sf "$BROKER_URL/v1/challenge" | jq -r '.nonce // empty')
[[ -n "$NONCE2" ]] || fail "oos setup" "could not fetch second nonce"

OOS_STATUS=$(python3 <<PYEOF
import json, base64, urllib.request, urllib.error
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat

key = Ed25519PrivateKey.generate()
pub_bytes = key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
sig = key.sign(bytes.fromhex("${NONCE2}"))

body = json.dumps({
"launch_token": "${LT2}",
"nonce": "${NONCE2}",
"public_key": base64.b64encode(pub_bytes).decode(),
"signature": base64.b64encode(sig).decode(),
"orch_id": "smoke-orch",
"task_id": "smoke-oos",
"requested_scope": ["admin:all:*"],
}).encode()
req = urllib.request.Request("${BROKER_URL}/v1/register", data=body,
headers={"Content-Type": "application/json"})
try:
urllib.request.urlopen(req)
print("200") # This is a FAILURE — registration should have been denied
except urllib.error.HTTPError as e:
print(e.code)
PYEOF
)
SIG2_JSON=$("$EDSIGN_BIN" "$NONCE2")
PUB2_B64=$(echo "$SIG2_JSON" | jq -r '.public_key // empty')
SIG2_B64=$(echo "$SIG2_JSON" | jq -r '.signature // empty')
[[ -n "$PUB2_B64" && -n "$SIG2_B64" ]] || fail "edsign helper (oos)" "missing public_key/signature in: $SIG2_JSON"

OOS_BODY=$(jq -nc \
--arg lt "$LT2" \
--arg n "$NONCE2" \
--arg pk "$PUB2_B64" \
--arg s "$SIG2_B64" \
'{launch_token:$lt, nonce:$n, public_key:$pk, signature:$s,
orch_id:"smoke-orch", task_id:"smoke-oos", requested_scope:["admin:all:*"]}')
OOS_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BROKER_URL/v1/register" \
-H "Content-Type: application/json" \
-d "$OOS_BODY")
if [[ "$OOS_STATUS" == "403" || "$OOS_STATUS" == "400" ]]; then
pass "out-of-scope registration denied ($OOS_STATUS)"
else
Expand Down
Loading
Loading