Offline JSON Web Token decoder and (optional) signature verifier — shipped two ways:
jwtdecode.exe— single ~2.4 MB native command-line executable (.NET 10 Native AOT, no runtime needed).JwtDecoderPowerShell module — binary module for PowerShell 7.4+ with two cmdlets.
Both share the same hardened decoding + verification core. Both are 100% offline.
| CLI exe | PowerShell module | |
|---|---|---|
| Runtime needed | None (Native AOT) | PowerShell 7.4+ / .NET 8 |
| Single file | Yes (jwtdecode.exe) |
DLL + manifest installed to PSModulePath |
| Output | Formatted text | Rich PSObject ($jwt.Payload.sub dot-access) |
| Pipeline-friendly | stdin | Yes (Get-Content x.jwt | ConvertFrom-JsonWebToken) |
| Algorithms verified | HS/RS/PS/ES 256/384/512 | HS/RS/PS/ES 256/384/512 |
# decode a token (positional argument)
jwtdecode eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
# read the token from a file
jwtdecode --file token.jwt
# pipe the token from stdin
Get-Content token.jwt | jwtdecode
# show everything, including raw base64 segments and signature bytes
jwtdecode --file token.jwt --detailed
# query a single claim (JSON form by default)
jwtdecode --file token.jwt --query payload.sub # -> "1234567890"
# unwrap the string value with --raw
jwtdecode --file token.jwt --query payload.sub --raw # -> 1234567890
# shorthand: bare names default to payload.<name>
jwtdecode --file token.jwt -q sub
# query multiple paths in one call (comma-separated)
jwtdecode --file token.jwt -q payload.sub,header.alg,payload.exp
# index into arrays and walk nested objects
jwtdecode --file token.jwt -q 'payload.roles[0],payload.address.city'
# verify the signature with a key file
jwtdecode --file token.jwt --verify --key-file hs256-secret.txt
jwtdecode --file token.jwt --verify --key-file rs256-public.pem
# pipe the key on stdin (token must come from --file or a positional argument)
type rs256-public.pem | jwtdecode --file token.jwt --verify --key-file -Build and install (offline; no PSGallery required):
.\tools\Install-JwtDecoderModule.ps1 -BuildThen in any PowerShell session:
Import-Module JwtDecoder
# Decode
$jwt = ConvertFrom-JsonWebToken $token
$jwt.Algorithm # HS256
$jwt.Payload.sub # 1234567890
$jwt.Expiration # [DateTimeOffset]
$jwt | ConvertFrom-JsonWebToken -Detailed | Format-List
# Query a single claim by path (returns the typed .NET value)
Get-JsonWebTokenClaim $token -Name payload.sub # 1234567890
Get-JsonWebTokenClaim $token -Name sub # shorthand for payload.sub
Get-JsonWebTokenClaim $token -Name header.alg # HS256
# Multiple paths — pass an array or a comma-separated string
Get-JsonWebTokenClaim $token -Name payload.sub, payload.name, header.alg
Get-JsonWebTokenClaim $token -Name 'payload.roles[0]', 'payload.address.city'
# Verify — three parameter sets
Test-JsonWebTokenSignature -Token $token -KeyFile .\hs256-secret.txt
Test-JsonWebTokenSignature -Token $token -Secret (Read-Host -AsSecureString)
$rsa = [System.Security.Cryptography.RSA]::Create()
$rsa.ImportFromPem((Get-Content .\rsa-public.pem -Raw))
Test-JsonWebTokenSignature -Token $rsToken -PublicKey $rsa| Option | Description |
|---|---|
<token> |
Positional argument; the JWT to decode. |
--file <path> |
Read the JWT from a file. |
| (stdin) | If neither positional nor --file is given, read from stdin. |
-d, --detailed |
Also print raw segments and signature bytes (hex). |
-q, --query <p> |
Print only the value(s) at the given path(s). Comma-separated for multiple. |
--raw |
With --query, unwrap string scalars (no JSON quotes). |
--verify |
Verify the signature. Requires --key-file. |
--key-file <path> |
Key file (HMAC raw secret or PEM-encoded RSA / EC PUBLIC key). Use - to read the key bytes from stdin (the token must then come from --file or a positional argument). |
-h, --help |
Show help. |
-v, --version |
Show version. |
| Path | Meaning |
|---|---|
payload.sub |
The sub claim in the payload. |
header.alg |
The alg parameter in the JOSE header. |
sub |
Shorthand for payload.sub (bare names default to payload). |
payload.roles[0] |
First element of the roles array. |
payload.address.city |
Nested property walk. |
payload."x5t#S256" |
Use quoted segments for keys with non-identifier chars. |
payload |
The whole payload object. |
payload.sub,header.alg |
Multiple paths in one call; one value per line. |
Output is JSON-encoded by default — string values are emitted with their JSON quotes preserved and control characters left as \uXXXX escapes (terminal-injection-safe). Pass --raw to unwrap string scalars. Objects and arrays are always emitted as compact JSON. Missing paths exit with code 2.
| Cmdlet | Purpose |
|---|---|
ConvertFrom-JsonWebToken [-Token] <string> [-Detailed] |
Decode a JWT to a DecodedJsonWebToken. Pipeline-friendly. |
ConvertFrom-JsonWebToken -Path <string> [-Detailed] |
Decode a JWT read from a file. |
Get-JsonWebTokenClaim [-Token] <string> -Name <string[]> |
Return the value(s) at one or more query paths as typed PowerShell objects. |
Get-JsonWebTokenClaim -Path <string> -Name <string[]> |
Same, reading the token from a file. |
Test-JsonWebTokenSignature -Token <string> -KeyFile <string> |
Verify using a key file (HMAC raw or PEM public). |
Test-JsonWebTokenSignature -Token <string> -Secret <SecureString> |
Verify HMAC using a SecureString secret. |
Test-JsonWebTokenSignature -Token <string> -PublicKey <RSA|ECDsa> |
Verify using an already-loaded asymmetric public key. |
The format is inferred from the JWT's alg claim and cross-checked against the file content (this prevents the algorithm-confusion attack):
| Algorithm family | Key file contents |
|---|---|
HS256/384/512 |
Raw secret bytes. A single trailing \n or \r\n is stripped. PEM-looking files are refused. |
RS256/384/512 |
PEM-encoded RSA public key. Private keys are refused. |
PS256/384/512 |
PEM-encoded RSA public key. Private keys are refused. |
ES256/384/512 |
PEM-encoded EC public key; curve must match (ES256↔P-256, ES384↔P-384, ES512↔P-521). |
Size caps: token ≤ 1 MiB, key file ≤ 64 KiB, decoded JWT segment ≤ 256 KiB.
Bearer prefixes and surrounding quotes are stripped automatically from the input token.
| Code | Meaning |
|---|---|
| 0 | Success. |
| 1 | Unexpected error. |
| 2 | Invalid input (bad token, bad arguments, missing/unreadable file). |
| 3 | Signature verification failed. |
Requires the .NET 10 SDK (CLI) and / or .NET 8 SDK (PS module). For the CLI's AOT publish also requires the MSVC C++ build tools.
# Solution-wide build
dotnet build
# --- CLI ---
dotnet run --project src\JwtDecoder -- <token> # iterate
dotnet publish src\JwtDecoder -c Release -r win-x64 # single exe
# -> src\JwtDecoder\bin\Release\net10.0\win-x64\publish\jwtdecode.exe
# --- PowerShell module ---
dotnet publish src\JwtDecoder.PowerShell -c Release # module DLL + manifest
# -> src\JwtDecoder.PowerShell\bin\Release\net8.0\publish\
# Build + install in one step (offline; no PSGallery):
.\tools\Install-JwtDecoderModule.ps1 -Build # CurrentUser scope
.\tools\Install-JwtDecoderModule.ps1 -Build -Scope AllUsers -ForceIf dotnet publish for the CLI fails with vswhere.exe is not recognized, prepend the VS Installer directory to PATH for the session:
$env:PATH = "C:\Program Files (x86)\Microsoft Visual Studio\Installer;" + $env:PATHJwtDecoder.slnx
src\
JwtDecoder.Core\ # net8.0 shared library, AOT-safe, zero NuGet deps
Jwt.cs # JWT parsing (base64url, JSON, dup-key rejection, size caps)
KeyMaterial.cs # key wrapper with shared-vs-owned crypto provider semantics
KeyLoader.cs # PEM / raw-secret loading; algorithm-confusion + private-key guards
Verifier.cs # HMAC / RSA / ECDsa verification (curve+length checks)
JwtDecoder\ # net10.0 CLI exe, Native AOT, references Core
Program.cs # entry, top-level error handling, exit codes, bounded I/O, forced GC
Cli.cs # argument parsing + help text
Output.cs # simplified + detailed formatters (terminal-escape safe)
JwtDecoder.PowerShell\ # net8.0 binary module, references Core
ConvertFromJsonWebTokenCommand.cs
TestJsonWebTokenSignatureCommand.cs
OutputTypes.cs # DecodedJsonWebToken, JsonWebTokenVerification
PsConversion.cs # JsonElement -> PSObject (dot-access)
JwtDecoder.psd1 # module manifest (PSGallery-ready)
README.md # module-local docs
tools\
Install-JwtDecoderModule.ps1 # offline install into PSModulePath
samples\
generate-samples.ps1 # HS/RS/PS/ES happy-path test tokens + keys (pwsh 7)
generate-attack-samples.ps1 # algorithm-confusion / wrong-curve / oversized / etc.
jwtdecode and the JwtDecoder PowerShell module are 100% offline. But cloud‑issued JWTs often have a public key you don't already have on disk — it lives at a JWKS endpoint, often discoverable via OIDC. To bridge that gap without compromising the offline guarantee of the trusted binaries, there's a separate companion:
| Component | Network capable? | How it ships |
|---|---|---|
jwtdecode.exe / JwtDecoder PowerShell module |
No (zero sockets — verified in CI) | as today |
jwksfetch.exe / JwtDecoder.Jwks PowerShell module / JwtDecoder.JwksFetcher NuGet |
Yes (HTTPS only, hardened) | separate binary / module / package |
The companion fetches one JWKS over HTTPS (or runs OIDC discovery), validates it strictly, picks the JWK matching the JWT's kid/alg/curve, and emits one PEM block. The trusted binary takes that PEM via stdin and verifies as usual.
# Direct JWKS URL
jwksfetch --jwks-url https://login.example.com/.well-known/jwks.json --token-file token.jwt |
jwtdecode --file token.jwt --verify --key-file -
# Or via OIDC discovery (appends /.well-known/openid-configuration to the issuer)
jwksfetch --from-issuer https://login.example.com --token-file token.jwt |
jwtdecode --file token.jwt --verify --key-file -
# File-based two-step (no TOCTOU on token.jwt between the two reads)
jwksfetch --jwks-url https://login.example.com/keys --token-file token.jwt > key.pem
jwtdecode --file token.jwt --verify --key-file key.pemThe pipe carries only a PEM public key. jwtdecode.exe opens no sockets and its binary contains no networking imports (verified at every CI run, see Security notes below).
Import-Module JwtDecoder # offline (unchanged)
Import-Module JwtDecoder.Jwks # network-capable (explicit opt-in)
# Explicit (deterministic Dispose)
$jwk = Get-JsonWebKey -Token $token -Issuer https://login.example.com
try {
Test-JsonWebTokenSignature -Token $token -PublicKey $jwk.PublicKey
} finally { $jwk.Dispose() }
# Pipeline one-liner
Get-JsonWebKey -Token $token -JwksUri https://login.example.com/keys |
Test-JsonWebTokenSignature -Token $token- HTTPS only — no
--insecureflag, ever. TLS 1.2 / 1.3 only; HTTP/3 not negotiated. - SSRF deny‑list applied to every hop — IP literals AND DNS‑resolved addresses (IPv4‑mapped IPv6 unmapped first). Refused: loopback, link‑local (incl.
169.254.169.254cloud metadata), 10/8, 172.16/12, 192.168/16, 0/8, 100.64/10;::1,fc00::/7,fe80::/10,fec0::/10, multicast; the literal hostnamelocalhost. - DNS rebinding defended via
SocketsHttpHandler.ConnectCallback— we resolve, validate, then connect byIPEndPointso the IP that survives validation is the IP TLS handshakes with. Skipped transparently when a proxy is in use (the proxy joins the trust chain). UseProxy = falseby default. AmbientHTTPS_PROXYis ignored unless you pass--use-system-proxyor--proxy <url>.- Bearer token is
byte[]‑based and stripped on every redirect, even same‑host. For OIDC discovery the bearer is NOT sent on the discovery hop unless--bearer-token-discoveryis given (opt‑in). - Manual redirect loop with a cap (default 3, max 5); each hop is re‑validated against SSRF. Non‑HTTPS targets and same‑URL revisits refused.
AutomaticDecompression = None; responses withContent-Encodingare refused.- JWKS strictness: recursive duplicate‑key JSON rejection,
kty=octrefused, any private component refused,x5u/jkurefused,x5cignored,use=encrefused,key_opsmust containverifyif present, RSA modulus ≥ 2048 bits with odd exponent ≥ 3, ECcrv/coordinate length bound by the JOSE binding. - OIDC: issuer field MUST match the requested URL after canonicalization (lowercase host, default port stripped, single trailing slash stripped); same strict JSON path for both the metadata and the JWKS document.
- No on‑disk cache.
- Custom
--header-filerejects dangerous header names (Host,Authorization,Proxy-Authorization,Cookie,Connection,TE,Trailer,Transfer-Encoding,Upgrade,Expect,Content-Length) and CR/LF/NUL in values.
The offline guarantee belongs to jwtdecode.exe alone, not to the pipeline. When you pipe jwksfetch | jwtdecode, you've added a network‑capable process to your trust chain. The jwksfetch binary applies a defensible set of guards (above) — but it IS opening sockets. If your threat model strictly forbids any network activity in your verification path, don't use the companion; obtain the public key out of band and feed it to jwtdecode --key-file <path> directly. The same advice applies in PowerShell: the JwtDecoder module stays offline; importing JwtDecoder.Jwks loads System.Net.Http into the process for the session.
jwksfetch --helpfor the full CLI surface.JwtDecoder.JwksFetcheron nuget.org for embedding the JWKS pipeline in your own .NET app. Lock‑step exact‑pinned toJwtDecoder.Core.src/JwtDecoder.JwksFetcher/README.md(shipped with the nupkg) for the library‑level docs.src/JwtDecoder.Jwks.PowerShell/README.mdfor the cmdlet's deep dive.
The offline promise of jwtdecode is not a claim made on a feature checklist. It is enforced by a layered scanner that runs in CI on every artifact-producing workflow (gating the upload), and that you can run yourself in under a minute against any binary you downloaded — with no need to trust our reading of the output.
| In scope (offline-by-construction) | Out of scope (network-capable by design) |
|---|---|
jwtdecode.exe (Native AOT CLI) |
jwksfetch.exe |
JwtDecoder PowerShell module |
JwtDecoder.Jwks PowerShell module |
JwtDecoder.Core NuGet package |
JwtDecoder.JwksFetcher NuGet package |
A normally-functioning binary in the left column cannot open a socket, resolve a hostname, or otherwise reach the network. Its only I/O channels are filesystem reads (token / key file), stdin (token bytes or PEM key bytes for --key-file -), stdout (decoded JSON / verification result), and stderr (errors / warnings). No DNS lookup. No HTTP. No proxy. No telemetry.
The right column carries all networking — pipe jwksfetch | jwtdecode --key-file - and the network trust boundary moves to whoever produced the PEM that jwtdecode reads from stdin.
Five independent checks. Four are pass/fail (A, B, D, E); any one failure aborts the workflow and the artifact never ships. The fifth (C) is informational only — explicitly documented as a heuristic so the strict signals stay credible.
| # | Layer | Tool | What it proves |
|---|---|---|---|
| A | Managed IL grep | ilspycmd 10.1.0.8386 (version-pinned for supply-chain hygiene) |
No reference in JwtDecoder.Core.dll or the pre-AOT jwtdecode.dll to [System.Net.Http], [System.Net.Sockets], [System.Net.WebSockets], [System.Net.NetworkInformation], [System.Net.Mail], [System.Net.Primitives], [System.Net.NameResolution], [System.Net.Security], [System.Net.Quic], [System.Web*], System.Net.WebClient/WebRequest/HttpWebRequest/Dns, P/Invoke into ws2_32 / wininet / winhttp / urlmon / iphlpapi / dnsapi / libcurl / libssl / libssh2 / nghttp2, or reflective load attempts (ldstr "System.Net.Http", NativeLibrary::Load …ws2_32, etc.). |
| B | Native AOT import inspection | dumpbin /imports (Windows), objdump -p + objdump -T (Linux), otool -L + nm -u (macOS) |
The compiled AOT exe links no networking library (Windows DLL or *nix shared object) and imports no forbidden socket symbol (socket, connect, getaddrinfo, gethostbyname, WSAStartup, etc., including glibc __GI_socket aliases, Windows __imp_socket@4 decorations, and glibc-versioned socket@GLIBC_2.2.5 suffixes). On Unix, libc / libSystem cannot be library-deny-listed (they're universally NEEDED) so the symbol scan is the only signal — empty output or tool failure is fail-closed. |
| C | Raw-bytes string heuristic | PowerShell -match over the AOT exe bytes |
Lists any networking type-name strings present anywhere in the binary. Explicitly marked informational: type-name strings often appear in BCL infrastructure that is not reachable from jwtdecode's entrypoint. Layer-B is the authoritative signal for the AOT exe; Layer-C is shown for transparency only. |
| D | Transitive NuGet package check | dotnet list package --include-transitive |
Neither JwtDecoder.Core.csproj nor JwtDecoder.csproj pulls a forbidden package (System.Net.*, Microsoft.Extensions.Http, RestSharp, Flurl*, etc.). Catches the supply-chain case where a future PR adds a dependency that itself transitively depends on networking. |
| E | Scan-vs-upload SHA-256 | PowerShell Get-FileHash |
The exact binary the scanner inspected is the exact binary uploaded as the release artifact — bit-for-bit. Defends against any build step between scan and upload that could swap in a different binary. |
The IL dumps from layer A and the native imports listing from layer B are uploaded as a transparency artifact with every release, named offline-guarantee-<rid>-Release-<sha>. Download it from the workflow run page and re-grep it yourself — you don't have to trust our reading of the output.
Plus, every published AOT binary carries a Sigstore-backed build-provenance attestation — gh attestation verify <file> --owner ShikeChen-MS confirms the binary came from this repo's CI, signed by GitHub's OIDC identity for this workflow at this commit.
You need three things, all open and stable:
- The published
jwtdecodebinary for your platform — from the GitHub Release. - A clone of this repository at the tag matching that binary (for the verifier script).
- The .NET SDK ≥ 10.0.x (only needed once, to install
ilspycmd).
# 1. Clone at the matching tag
git clone --depth 1 --branch <tag> https://github.com/ShikeChen-MS/JwtDecoder_local_cli.git
cd JwtDecoder_local_cli
# 2. Windows only: make `dumpbin` reachable. (Skip on Linux/macOS — objdump/nm
# are pre-installed.) The simplest path is to run from a Developer
# PowerShell for Visual Studio. Or, fully scripted:
$vsInstall = & "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" `
-latest -property installationPath
$dumpbin = Get-ChildItem "$vsInstall\VC\Tools\MSVC" -Recurse -Filter dumpbin.exe |
Where-Object { $_.FullName -match 'HostX64\\x64' } | Select-Object -First 1
$env:PATH = "$($dumpbin.Directory.FullName);$env:PATH"
# 3. Run the verifier. It auto-installs the pinned ilspycmd if needed.
pwsh -File tools/Verify-OfflineGuarantee.ps1 `
-CoreDllPath <path>\JwtDecoder.Core.dll `
-ManagedCliDllPath <path>\jwtdecode.dll `
-AotExePath <path>\jwtdecode.exe `
-CoreProjectPath src\JwtDecoder.Core\JwtDecoder.Core.csproj `
-CliProjectPath src\JwtDecoder\JwtDecoder.csprojExpected verdict at the bottom of the output:
[A] Managed IL inspection (ilspycmd)
PASS JwtDecoder.Core IL contains no networking references.
PASS jwtdecode IL contains no networking references.
[B] Native AOT binary import inspection
PASS AOT binary links no forbidden native libraries.
PASS AOT binary imports no forbidden socket function names.
[C] Raw-bytes string heuristic (informational)
WARN AOT binary contains networking-type name strings (not necessarily reachable): …
WARN Layer-C is heuristic. Layer-B (native imports) is the authoritative signal.
[D] Transitive NuGet package check
PASS JwtDecoder.Core has no forbidden transitive packages.
PASS jwtdecode (CLI) has no forbidden transitive packages.
[E] Scan-vs-upload SHA-256 integrity
PASS Scan-vs-upload SHA-256 match (…).
OFFLINE GUARANTEE: PASS
The Layer-C WARN is normal — see the table above. The transparency artifacts (raw IL dump, raw native imports listing) are written to ci-artifacts/disasm/ so you can re-grep them with your own tooling.
If you don't trust our deny-list, point ilspycmd at the binary and look for anything networking-related:
dotnet tool install -g ilspycmd --version 10.1.0.8386
ilspycmd -il <path>\jwtdecode.dll | Select-String -Pattern 'System\.Net|pinvokeimpl|NativeLibrary::Load'A clean run prints nothing. If you find a hit that isn't already on $forbiddenIlPatterns in tools/Verify-OfflineGuarantee.ps1, please file an issue — that means our deny-list is incomplete and the verifier could give false PASSes. We want to know.
jwksfetch.exe, theJwtDecoder.JwksPowerShell module, and theJwtDecoder.JwksFetcherNuGet package are network-capable by design. Importing the PS module into a session loadsSystem.Net.Httpinto that session. The trust boundary is documented above (Trust boundary — read this twice).- The verifier runs on the binary we built, not on a binary an attacker substituted on disk. Combine it with the Sigstore attestation check above (
gh attestation verify …) to close the substitution gap. - The verifier does not protect against runtime bugs (parser errors, oversized-input handling, etc.). Those are unrelated security concerns — see the Security notes section below, and file bugs separately.
This tool is built for a no-compromise threat model: offline-only, security-first, drop-the-feature-if-it-weakens-anything.
Network isolation
- The
jwtdecodeprocess opens no sockets and resolves no hosts. See the dedicated Offline guarantee section above for exactly how this is enforced in CI and how you can audit it yourself.
Algorithm-confusion guard (CVE-pattern: JWT alg=HS256 + RSA public key as MAC secret)
- If the JWT's
algisHS*and the supplied key file looks like PEM, the tool refuses to verify and exits with code2. This blocks the well-known class of attacks where an attacker repurposes a published RSA/EC public key as an HMAC shared secret.
Public-keys-only for asymmetric verification
- PEM files containing any
PRIVATE KEYlabel are refused. Verification only needs the public key — loading the private key would unnecessarily expose secret material.
Strict algorithm/curve binding
- The
algheader is required to be a non-empty string. alg: noneis always reported as INVALID with a security warning; exit code3.- ECDSA curve is enforced:
ES256↔P-256,ES384↔P-384,ES512↔P-521. Mismatch is refused. - ECDSA signature length is enforced (
64/96/132raw R‖S bytes — DER signatures are rejected). - Duplicate top-level JOSE header / payload property names are rejected to prevent parser-differential ambiguity.
Memory hygiene
- HMAC secret bytes, decoded header/payload bytes, signature bytes, signing input, derived MAC bytes, and intermediate base64 buffers are all explicitly zeroed (
CryptographicOperations.ZeroMemory) before release. - PEM key data is read as
byte[]and decoded into a mutablechar[]; both buffers are zeroed afterImportFromPem. The PEM is never copied into an immutablestring. - HMAC newline-trimming copies into an exact-sized buffer instead of slicing — the original read buffer is zeroed; no residual secret data is left on the heap.
RSA/ECDsaprovider instances are disposed viausing(releasing native key handles) inside atry { } finally { }so disposal runs even if verification throws.- Constant-time comparison (
CryptographicOperations.FixedTimeEquals) is used for HMAC verification. - An aggressive compacting GC is forced before process exit (
GCCollectionMode.Aggressive, blocking, compacting, twice, with LOH compaction) to reclaim any zeroed-but-still-reachable buffers. - Caveat the tool cannot fix: tokens passed as a positional argument may persist in shell history / process listings. Prefer
--fileor stdin for sensitive tokens.
DoS hardening
- Token input (file or stdin) is capped at 1 MiB.
- Key file is capped at 64 KiB.
- Each decoded base64url segment is capped at 256 KiB before JSON parsing.
- JSON parsing uses default
MaxDepth = 64, disallows comments, disallows trailing commas.
Terminal-injection guard
- String claim values are emitted using JSON-escape (
JsonElement.GetRawText()), preserving\u001b…rather than emitting raw ESC bytes. - Property names are scanned for ASCII / C1 control characters and re-escaped as
\uXXXXbefore display.
Out-of-range time claims
iat/nbf/expvalues outsideDateTimeOffsetrange are displayed as "out of representable range" and excluded fromEXPIRED/VALIDevaluation rather than crashing.
Exit codes
- Input validation, missing / oversized / unreadable files, unsupported algorithms → exit
2. - A well-formed key with a non-matching signature → exit
3. - Unexpected internal errors → exit
1(no normal code path should reach this).