Skip to content

v1.0.0-rc15

Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 22 Jul 20:56
· 18 commits to main since this release
v1.0.0-rc15
abf6982

Security

  • Scoped id-token: write (and packages/contents: write) from workflow
    level down to the specific jobs that need them
    (.github/workflows/ci.yml,
    image.yml,
    release.yml).
    All three workflows granted
    id-token: write at workflow scope, so every job in them could mint a
    Sigstore-attributable OIDC token — including jobs that never sign. Job-level
    permissions blocks replace workflow inheritance, so the token now reaches
    only the Cosign signing jobs (check, image, build); the publish job
    (release) keeps contents: write but not id-token, and any future job
    added without a block inherits the minimal contents: read default. Surfaced
    by the pre-red-team security assessment (finding SC-3). No image change.
  • Bumped google.golang.org/grpc v1.80.0 → v1.82.1 to clear a High
    Dependabot alert (GHSA gRPC-Go xDS RBAC / HTTP/2, CVSS 8.8).
    gRPC is an
    indirect dependency pulled transitively via goose and the OTLP/gRPC
    telemetry exporters. The advisory affects gRPC's xDS RBAC authorization engine
    and HTTP/2 transport server; this server never runs a gRPC server and does
    not use xDS (our only gRPC use is the OTLP exporter client), so the vulnerable
    code is not reachable — this is a hygiene bump to keep the public alert page
    clean. Pulled golang.org/x/oauth2 and genproto/googleapis/api forward in
    step (MVS; both permissively licensed). No first-party code change.
  • Made the Docker image build hermetic (Dockerfile). The
    builder stage now compiles with -mod=vendor against the committed vendor/
    tree and no longer runs go mod download, so the image build never contacts
    the module proxy. This removes the proxy-flake failure mode that hit PR #47's
    image job and makes the compiled dependency set supply-chain-deterministic
    (what is reviewed in vendor/ is exactly what is built). No change to the
    compiled binary. Spotted during the pre-red-team security assessment.
  • Masked the caller email in the public key-request log line
    (internal/mcp/keys_request_http.go).
    The
    unauthenticated POST /api/v1/keys/request handler logged the full requester
    email at INFO; it now logs only a masked form (a***@example.com) via a new
    logging.SafeEmail helper. request_id already links to the full stored
    record, so no operator capability is lost. Surfaced by the security assessment
    (finding LG-3).
  • Bounded the upstream broadcast error persisted to the write-audit trail
    (internal/mcp/tools_evm_write.go).
    The
    node-returned error string on a failed broadcast is now capped at 512
    characters before it reaches write_audit.Error or the audit log. The client
    never sees this error (SafeForClient collapses it), but an unbounded reply
    from a hostile/MITM'd node could otherwise bloat the audit column and log sink.
    Surfaced by the security assessment (finding LG-2).
  • KEY_HMAC_PEPPER now has a boot-time length floor
    (internal/config/config.go).
    A set pepper
    (active or previous) shorter than 32 characters fails boot with
    ErrPepperTooShort. Previously any non-empty value was accepted, so a weak,
    brute-forceable pepper silently weakened the key-hash MAC while giving the
    false confidence of "peppered." Unset (unpeppered) mode is unchanged. Operators
    running a pepper shorter than 32 characters must rotate to a ≥ 32-character
    high-entropy secret before upgrading. Surfaced by the pre-red-team security
    assessment (finding KS-2).
  • Hardened the untrusted RPC-node boundary against denial of service
    (internal/evm, internal/anchor).
    Node/RPC
    responses are untrusted (a plaintext http:// endpoint is permitted, so a
    hostile or MITM'd node controls the bytes). Two gaps are closed: (EV-1) every
    response body is now capped at 32 MiB via a limiting http.RoundTripper, so an
    unbounded reply can no longer exhaust process memory before decode; and (EV-2)
    the node-response decode/normalize paths now run under recover(), converting
    a malformed-response panic into an ErrNodeResponseDecode error instead of
    crashing the stdio process. This mirrors the recover() + size cap already on
    the caller-transaction decode path and satisfies supplement invariant INV-6.
    Surfaced by the pre-red-team security assessment (findings EV-1, EV-2).
  • Fuzz-hardened the untrusted decode boundary
    (internal/evm/decode_fuzz_test.go,
    internal/anchor/decode_fuzz_test.go).

    Go native fuzz targets for the caller-tx decode (11.3M execs) and the anchor
    ABI decode (3.4M execs). The caller path held every invariant (no panic
    escapes recover(); CanonicalRaw is a signer fixed-point — the
    parser-differential defense). Fuzzing also corrects an earlier
    characterization
    : the defiweb ABI decoder is not bounds-checked — an
    unguarded probe panicked it in 1.6s with a 29-byte input (slice bounds out of range [-8388608:], a length prefix decoded as a negative index). The EV-2
    anchor guard therefore defends a real, trivially-craftable hostile-node DoS,
    not a hypothetical one; that crashing input is now a regression seed.
  • Pinned the CI license-check tool: go-licenses@latest@v1.6.0
    (.github/workflows/ci.yml).
    An unpinned
    go install ...@latest resolves to whatever the module proxy serves at run
    time and executes arbitrary Go on the runner — a supply-chain sink,
    particularly in a job that carries id-token: write. It sat directly beside a
    deliberately-pinned govulncheck@v1.3.0; this closes the asymmetry. v1.6.0
    verified to run clean under the pinned Go toolchain. Surfaced by the pre-red-team
    security assessment (finding SC-2). No image change.

Fixed

  • Corrected an over-broad logging claim in the README. The Observability
    section stated sensitive data "is redacted in all log output"; in fact the
    SafeAddr/SafeURL/SafeTxData helpers redact on debug lines, while the
    write-audit broadcast lines (INFO/WARN) deliberately record the recovered
    on-chain signer, destination, and value in full (public on-chain data forming
    the audit trail). README now matches the code and cross-references
    docs/DATA_HANDLING.md. Surfaced by the security assessment (finding LG-1,
    doc leg). The in-code posture is now resolved (see Documentation below):
    logging these identifiers is retained by design; the log sink's retention is
    documented as the operator's responsibility.

Documentation

  • Corrected the audit-attribution claim in docs/SECURITY_CONSUMER_GUIDANCE.md
    (finding CG-1). "Trace which API key created or modified an offending record"
    was false: the write-audit is keyed by the recovered on-chain signer (there
    is no client_id column, and anonymous keyless writes carry no API key). The
    claim now describes signer-based attribution, with the authenticated caller's
    client_id in the audit log line.
  • Documented the write-audit log sink as outside the retention purge
    (finding LG-1, docs/DATA_HANDLING.md § 8.3). The in-process purge deletes DB
    rows only; the same signer/destination/value appear in structured audit log
    lines governed by the operator's log-pipeline retention. Logging them is
    deliberate (operator abuse-forensics; values are already public on-chain) and
    accepted — a published retention period must configure log retention to match.
  • Documented the per-signer quota as a soft ceiling under concurrency
    (finding EA-1, docs/DATA_HANDLING.md § 8.2 + SignerQuotaStore godoc). The
    non-atomic CountIncrement can over-admit past MCP_SIGNER_WRITE_RATE by
    roughly the concurrency width; an accepted trade-off for a coarse, gas-bounded
    anti-abuse throttle (an exact cap would need a per-signer lock on every
    broadcast). No behavior change.

Verifying signatures

Each binary is shipped with a Cosign keyless signature (via Sigstore OIDC) and a SHA-256 checksum.

# Verify checksum
shasum -a 256 -c nvnm-mcp-server-v1.0.0-rc15-<os>-<arch>.sha256

# Verify Cosign signature
cosign verify-blob \
  --certificate nvnm-mcp-server-v1.0.0-rc15-<os>-<arch>.cert.pem \
  --signature nvnm-mcp-server-v1.0.0-rc15-<os>-<arch>.sig \
  --certificate-identity-regexp 'https://github.com/NVNM-Chain/nvnm-mcp-server/.*' \
  --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
  nvnm-mcp-server-v1.0.0-rc15-<os>-<arch>