Skip to content

feat: add io.jwt.verify_* builtins (13 JWS signature verifiers, OPA 1.17)#115

Merged
r6e merged 2 commits into
mainfrom
feat/io-jwt-verify
Jun 20, 2026
Merged

feat: add io.jwt.verify_* builtins (13 JWS signature verifiers, OPA 1.17)#115
r6e merged 2 commits into
mainfrom
feat/io-jwt-verify

Conversation

@r6e

@r6e r6e commented Jun 20, 2026

Copy link
Copy Markdown
Owner

Summary

Adds the 13 io.jwt.verify_* builtins — verify_{hs,rs,ps,es}{256,384,512} and verify_eddsa — verifying a compact JWS signature against a key and returning a boolean, mirroring OPA 1.17's builtinJWTVerify including its three-way outcome (undefined / false / true). The next member of the planned io.jwt.* family after io.jwt.decode (#112).

The algorithms differ only by data in VERIFY_ALGORITHMS; the signature decode reuses Base64Url.strict_decode. HMAC uses OpenSSL.secure_compare; ECDSA converts the fixed-width r‖s JWS signature (es512 = P-521 / 66-byte halves) to DER. Like OPA, verification uses only the signing-input bytes + signature + key — the header/payload are not parsed.

Outcome taxonomy (byte-exact to OPA)

  • undefined — not three segments, a non-base64url signature, or (asymmetric) no usable public key (an unsupported/private key, a malformed/incomplete key, or a JWK Set in which any key is unbuildable).
  • false — well-formed but the signature does not verify (wrong key, wrong key type, wrong-length ECDSA signature; the header alg is ignored; an empty HMAC secret verifies normally → false; an empty JWK Set → false).
  • true — the signature verifies.

Key handling (the hard part)

Matches OPA's getKeyFromCertOrJWK, derived from an exhaustive key-format differential against opa eval rather than sampled. The PEM string is dispatched by its single block's type via a linear scan (Go's pem.Decode semantics): only a CERTIFICATE or a PKIX/SPKI PUBLIC KEY block whose trailing content is [ \t]*(\r\n|\n)? is accepted. The parsed key must be one x509.ParsePKIXPublicKey accepts: RSA, EC on a NIST curve (P-224/256/384/521 — secp256k1/brainpool rejected), or Ed25519. So PKCS#1 RSA PUBLIC KEY, bare DER, private-key blocks, RSASSA-PSS/DSA/PQC keys, trailing data, a second block, and a smuggled header are all undefined. JWK / JWK Set is the fallback (a Set verifies if any key does, undefined if any key is unbuildable; a private d JWK is rejected).

Totality / security

Holds for attacker-controlled tokens and key material: malformed/private/unsupported keys → undefined, verification-time errors → false. The PEM scan is linear (no ReDoS) and encoding-guarded (ascii_compatible? && valid_encoding?), so no input raises out of the registry boundary.

Review

Converged through 5 adversarial panel rounds (security, wire-format/OPA cross-validation, architecture, general, simplifier). The panel drove the key-parsing through several rewrites — switching to enumerate-the-grammar-first was what converged it. Findings fixed: private/PKCS#1/DER/RSA-PSS/PQC/non-NIST-curve key acceptance, cert-path bypass, header-smuggle, multi-block/trailing-data, a ReDoS (O(n²) backreference regex → linear), and an invalid-UTF-8 totality bug. A final-round CRLF "finding" was traced to opa eval flakiness (disproven by a 15× stable differential; the golden generator now requires 4-consecutive-stable captures).

86 goldens captured from opa eval 1.17 across the algorithm × key-format × taxonomy matrix. All gates green (rspec 1654/0, rubocop, reek, steep). Gem-stricter residuals (DSA/X25519 keys, EC/OKP private JWKs → undefined where OPA returns false) are documented as safe-direction.

🤖 Generated with Claude Code

….17)

Adds io.jwt.verify_{hs,rs,ps,es}{256,384,512} and io.jwt.verify_eddsa —
verifying a compact JWS signature against a key and returning a boolean,
mirroring OPA's builtinJWTVerify including its three-way outcome:
  - undefined: not three segments, a non-base64url signature, or (asymmetric)
    no usable public key (an unsupported/private key, a malformed/incomplete
    key, or a JWK Set in which any key is unbuildable);
  - false: well-formed but the signature does not verify (wrong key, wrong key
    type, wrong-length ECDSA signature; the header alg is ignored; an empty
    HMAC secret verifies normally and so is false; an empty JWK Set has no key
    and so is false);
  - true: the signature verifies.

Like OPA, verification uses only the signing-input bytes, the decoded
signature, and the key — the header/payload are not parsed, so a token whose
segments are not JSON objects still verifies.

Key handling matches OPA's getKeyFromCertOrJWK, derived from an exhaustive
key-format differential against `opa eval`: the PEM string is dispatched by
its single block's type (mirroring Go's pem.Decode), so only a CERTIFICATE or
a PKIX/SPKI "PUBLIC KEY" block is accepted — PKCS#1 "RSA PUBLIC KEY", bare
DER, a private-key block, trailing bytes after the block, a second block, and
a smuggled header are all undefined. The parsed key must be one Go's
x509.ParsePKIXPublicKey accepts: RSA, EC on a NIST curve (P-224/256/384/521 —
secp256k1/brainpool are undefined), or Ed25519 (not RSASSA-PSS/DSA/DH/PQC). A
JWK / JWK Set is the fallback; a Set verifies if any key does but is undefined
if any key is unbuildable, and a private JWK ("d" member) is rejected. HMAC
uses OpenSSL.secure_compare; ECDSA converts the fixed-width r‖s JWS signature
(es512 is P-521 / 66-byte halves) to DER.

The algorithms differ only by data in VERIFY_ALGORITHMS; the base64url
signature decode is the shared Base64Url.strict_decode. Totality holds for
attacker-controlled tokens and key material.

80 goldens captured from `opa eval` 1.17 across the algorithm × key-format ×
taxonomy matrix; ECDSA round-trip and malformed-key/encoding fuzzing confirm
the DER encoding and crash-safety.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 20, 2026 02:31

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR extends the io.jwt.* builtin family by adding OPA 1.17-compatible compact JWS signature verification builtins (io.jwt.verify_*), including asymmetric key handling via PEM (cert/SPKI) and JWK/JWK Set inputs, plus a golden-fixture driven spec suite to validate OPA parity.

Changes:

  • Added 13 io.jwt.verify_* builtins and supporting verification/key-parsing implementation (HMAC/RSA-PSS/ECDSA/EdDSA).
  • Added JWK/JWK Set parsing for building OpenSSL public keys used by verification.
  • Added OPA-derived golden fixtures and RSpec coverage for verification outcomes and registration/type behavior.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
lib/ruby/rego/builtins/jwt/verify.rb Implements io.jwt.verify_* builtins, signature verification logic, and PEM public key extraction.
lib/ruby/rego/builtins/jwt/jwk.rb Adds JWK/JWK Set parsing to produce OpenSSL public keys for verification.
lib/ruby/rego/builtins/jwt.rb Wires verification into the JWT builtin module and registers the new builtins.
spec/ruby/rego/builtins/jwt_verify_spec.rb Adds RSpec coverage that asserts verification behavior against OPA-derived goldens.
spec/fixtures/jwt_verify/goldens.json Adds OPA-captured golden cases covering algorithm/key-format/taxonomy matrix.
sig/ruby/rego.rbs Updates RBI/RBS signatures for the new JWT verification API surface and helpers.
sig/openssl_ext.rbs Adds missing OpenSSL RBS typing for OpenSSL::PKey.new_raw_public_key used by OKP (Ed25519) JWK handling.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/ruby/rego/builtins/jwt/jwk.rb
…terial

Mirrors the PEM path's guard: a key string that is not ASCII-compatible and
valid in its own encoding (UTF-16/32, invalid UTF-8) is not a usable JWK, so
map it to nil (undefined) before JSON.parse. Behavior-preserving — such keys
already mapped to undefined via JSON::ParserError on the current json — but it
no longer relies on JSON.parse's exact exception taxonomy to keep the
registry's totality guarantee for attacker-controlled key material (Copilot).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@r6e r6e merged commit 7dd9a60 into main Jun 20, 2026
12 checks passed
@r6e r6e deleted the feat/io-jwt-verify branch June 20, 2026 02:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants