Loading the parity record
Fetching the measured run — the pinned upstream oracle, every symbol and every case.
Fetching the measured run — the pinned upstream oracle, every symbol and every case.
Parity / JWT
Every number on this page was produced by running both implementations over the same cases: the real jsonwebtoken package pinned at 9.0.2 answers first, and its answer is the expectation the Go port is held to. Nothing is a hand-written expectation, so a new upstream release re-scores the port on its own. See JWT for the port's own documentation. Source: github.com/malcolmston/jwt.
Not a generic diagram: every node below names an artefact of this harness — the pinned package it installed, the runner files it started, the case files it streamed, and the counts it wrote out.
| Case group | Cases | Match | Mismatch | Group parity |
|---|---|---|---|---|
| verify-claims | 29 | 25 | 4 | 86.2% |
| crossverify | 27 | 26 | 0 | 96.3% |
| roundtrip | 25 | 24 | 0 | 96.0% |
| reject | 19 | 19 | 0 | 100.0% |
| sign-headers | 15 | 15 | 0 | 100.0% |
| algorithms | 12 | 12 | 0 | 100.0% |
| sign-claim-options | 11 | 9 | 2 | 81.8% |
| sign-hs | 9 | 8 | 0 | 88.9% |
| decode | 8 | 5 | 3 | 62.5% |
Every exported symbol of the upstream package, and what the port offers for it. The upstream list is derived mechanically, never from a README: 1. top-level exports
node -e "const j=require('jsonwebtoken');console.log(Object.keys(j).sort().join('\n'))"
2. own properties of each error class
node -e "
const j=require('jsonwebtoken');
for (const n of ['JsonWebTokenError','NotBeforeError','TokenExpiredError']) {
const C=j[n];
const e = n==='TokenExpiredError'?new C('m',new Date(0))
: n==='NotBeforeError' ?new C('m',new Date(0))
: new C('m',new Error('i'));
console.log(n, JSON.stringify(Object.getOwnPropertyNames(e).sort()));
}"
3. sign() option keys — read straight off sign_options_schem…. A symbol with no case is untested, never a match.
| Upstream symbol | Go symbol | Status | Cases | Note |
|---|---|---|---|---|
| jwt.sign | jwt.Sign, jwt.NewWithClaims + Token.SignedString | match | sign-hs256-exact, sign-hs384-exact, sign-hs512-exact, sign-hs256-minimal-exact, sign-hs256-notimestamp-exact, sign-hs256-default-alg-exact, sign-none-exact, sign-hs256-unknown-alg, parts-*, opt-* | HS\*/none tokens are **byte-identical** when the claim keys are alphabetical; see the ordering deviation below |
| jwt.verify | jwt.Parse, jwt.ParseWithClaims | differs | cross-*, vc-*, rej-*, alg-* (86 cases) | signature and claim semantics agree on all 12 shared algorithms and on the whole rejection surface; diverges on the two security findings above |
| jwt.decode | jwt.ParseUnverified | differs | dec-payload, dec-complete, dec-unverified-bad-signature, dec-no-signature, dec-alg-none, dec-garbage, dec-two-parts, dec-empty | upstream returns null for a token it cannot decode; the port returns an error. Fail-closed, so not a security concern, but callers that branch on null need rewriting |
| jwt.JsonWebTokenError | jwt.ErrInvalidToken, ErrTokenMalformed, ErrSignatureInvalid, ErrTokenUnverifiable, ErrInvalidKeyType, ErrTokenInvalidAudience, ErrTokenInvalidIssuer, ErrTokenInvalidSubject, ErrNoneAlgDisallowed, ErrSigningMethodUnavailable | differs | all rej-*, alg-* | a single JS class vs. a family of errors.Is-testable sentinels. Every message text differs (table below); the harness compares *whether* a call failed |
| jwt.TokenExpiredError | jwt.ErrTokenExpired | match | vc-exp-expired, vc-clocktolerance-insufficient, rej-expired | |
| jwt.NotBeforeError | jwt.ErrTokenNotValidYet | match | vc-nbf-future, rej-not-yet-valid | |
| JsonWebTokenError.name | — | untested | — | JS Error convention; the port uses sentinel identity instead |
| JsonWebTokenError.message | error.Error() | untested | — | see message table |
| JsonWebTokenError.inner | errors.Unwrap | untested | — | the port wraps sentinels, so errors.Is covers the same ground |
| JsonWebTokenError.stack | — | untested | — | JS-only |
| TokenExpiredError.expiredAt | (in the ErrTokenExpired message text) | untested | — | the port formats the expiry into the message rather than exposing a field |
| NotBeforeError.date | (in the ErrTokenNotValidYet message text) | untested | — | as above |
| algorithm | jwt.GetSigningMethod, jwt.SigningMethod* | match | parts-* (12 algs), sign-hs256-default-alg-exact, sign-hs256-unknown-alg | both default to HS256; both reject an unknown name |
| expiresIn | jwt.MapClaims["exp"], RegisteredClaims.ExpiresAt | differs | opt-expiresin-int, opt-all-claims, opt-expiresin-timespan, opt-expiresin-conflict | integer seconds agree; upstream also accepts ms timespan strings ("1h", "2 days") with no port equivalent |
| notBefore | jwt.MapClaims["nbf"], RegisteredClaims.NotBefore | differs | opt-notbefore-int, opt-all-claims, opt-notbefore-timespan | same timespan-string gap |
| audience | jwt.ClaimStrings, RegisteredClaims.Audience | match | opt-audience-string, opt-audience-array, opt-all-claims | string and array forms both round-trip |
| issuer | jwt.RegisteredClaims.Issuer | match | opt-issuer, opt-all-claims | |
| subject | jwt.RegisteredClaims.Subject | match | opt-subject, opt-all-claims | |
| jwtid | jwt.RegisteredClaims.ID | match | opt-jwtid, opt-all-claims | |
| keyid | jwt.Token.SetKID | match | parts-keyid, opt-all-claims | |
| header | jwt.Token.SetHeader, Token.SetType | match | parts-header-extra, parts-header-typ | |
| noTimestamp | omit iat from the claims | match | sign-hs256-notimestamp-exact | |
| encoding | — | missing | — | upstream's payload text encoding; the port always emits UTF-8 JSON |
| mutatePayload | — | missing | — | mutates the caller's JS object in place; meaningless for a Go value |
| allowInsecureKeySizes | — | missing | — | upstream can be told to sign with an RSA key under 2048 bits; the port has no size gate either way |
| allowInvalidAsymmetricKeyTypes | — | missing | — | upstream escape hatch for mismatched key types; the port always type-asserts (ErrInvalidKeyType) |
| algorithms | jwt.WithValidMethods | differs | alg-allowlist-match, alg-allowlist-multi, alg-allowlist-excludes, alg-allowlist-empty, alg-confusion-*, alg-rs256-*, alg-es256-token-es384-allowlist, alg-unknown-in-header, plus every rt-*/cross-* | SECURITY:** an empty allowlist is ignored by the port. Upstream also *infers* an allowlist from the key material when the option is absent; the port does not — see finding 1 |
| audience | jwt.WithAudience | differs | vc-aud-ok, vc-aud-wrong, vc-aud-array-claim-member, vc-aud-array-claim-nonmember, vc-aud-missing-claim, vc-aud-option-array, rej-wrong-audience | single-value agreement is exact, including matching a member of an array-valued aud claim. Upstream also accepts an **array** of acceptable audiences (and a RegExp); WithAudience takes one string |
| issuer | jwt.WithIssuer | differs | vc-iss-ok, vc-iss-wrong, vc-iss-option-array, rej-wrong-issuer | same array gap |
| subject | jwt.WithSubject | match | vc-sub-ok, vc-sub-wrong | |
| jwtid | — | missing | vc-jwtid-ok, vc-jwtid-wrong | the port has no jti-equality option. WithRequiredClaims("jti") only checks presence. vc-jwtid-wrong "matches" only because both sides fail — for different reasons |
| nonce | — | missing | vc-nonce-wrong | MapClaims.GetNonce reads the claim but no parser option compares it. Same accidental match caveat |
| clockTolerance | jwt.WithLeeway | match | vc-clocktolerance-saves, vc-clocktolerance-insufficient, vc-nbf-tolerance | symmetric, applied to exp and nbf identically on both sides |
| clockTimestamp | jwt.WithTimeFunc, jwt.WithClock | match | every vc-*, rej-*, alg-*, rt-*, cross-* case | the harness requires it |
| maxAge | jwt.WithMaxTokenAge | differs | vc-maxage-ok, vc-maxage-exceeded, vc-maxage-timespan | integer seconds agree, including making iat required; upstream also takes ms timespan strings |
| ignoreExpiration | jwt.WithIgnoreExpiration | match | vc-ignoreexpiration | |
| ignoreNotBefore | jwt.WithIgnoreNotBefore | match | vc-ignorenotbefore | |
| complete | jwt.Token.Header + Token.Claims | match | vc-complete-false-shape and every case with complete:true | |
| complete | jwt.Token.Header + Token.Claims from ParseUnverified | match | dec-payload, dec-complete | the port has no signature field in the decoded result; not compared |
| HS256 | jwt.SigningMethodHS256 | match | sign-hs256-exact, parts-hs256, rt-hs256, rt-hs256-wrongkey, cross-upstream-signed-hs256, cross-go-signed-hs256 | byte-identical tokens |
| HS384 | jwt.SigningMethodHS384 | match | sign-hs384-exact, parts-hs384, rt-hs384, rt-hs384-wrongkey, cross-*-hs384 | byte-identical |
| HS512 | jwt.SigningMethodHS512 | match | sign-hs512-exact, parts-hs512, rt-hs512, rt-hs512-wrongkey, cross-*-hs512 | byte-identical |
| RS256 | jwt.SigningMethodRS256 | match | parts-rs256, rt-rs256, rt-rs256-wrongkey, cross-upstream-signed-rs256, cross-go-signed-rs256, cross-rs256-tampered-payload, cross-rs256-wrong-public-key | PKCS#1 v1.5 is deterministic, so the cross-signed tokens are also identical |
| RS384 | jwt.SigningMethodRS384 | match | parts-rs384, rt-rs384, rt-rs384-wrongkey, cross-*-rs384 | |
| RS512 | jwt.SigningMethodRS512 | match | parts-rs512, rt-rs512, rt-rs512-wrongkey, cross-*-rs512 | |
| PS256 | jwt.SigningMethodPS256 | match | parts-ps256, rt-ps256, rt-ps256-wrongkey, cross-*-ps256 | PSS is randomised; compared by verification outcome + decoded claims + signature length, and cross-verified both directions. Salt length = hash length on both sides |
| PS384 | jwt.SigningMethodPS384 | match | parts-ps384, rt-ps384, rt-ps384-wrongkey, cross-*-ps384 | |
| PS512 | jwt.SigningMethodPS512 | match | parts-ps512, rt-ps512, rt-ps512-wrongkey, cross-*-ps512 | |
| ES256 | jwt.SigningMethodES256 | match | parts-es256, rt-es256, rt-es256-wrongkey, cross-*-es256, alg-es256-token-es384-allowlist | ECDSA is randomised; cross-verified both directions. Both use the fixed-width r‖s form (64 bytes), not ASN.1 DER |
| ES384 | jwt.SigningMethodES384 | match | parts-es384, rt-es384, rt-es384-wrongkey, cross-*-es384 | 96-byte signature |
| ES512 | jwt.SigningMethodES512 | match | parts-es512, rt-es512, rt-es512-wrongkey, cross-*-es512 | P-521, 132-byte signature — both sides pad each coordinate to 66 bytes |
| none | jwt.SigningMethodNoneAlg + jwt.WithAllowNone + jwt.UnsafeAllowNoneSignatureType | match | sign-none-exact, dec-alg-none, rej-none-with-secret, rej-none-no-allowlist, rej-none-listed-but-key-given, acc-none-explicit-optin | both refuse alg:none unless it is explicitly opted into, and both refuse it when real key material is supplied. The port requires a *second* opt-in (the UnsafeAllowNoneSignatureType sentinel as the key) |
Every case the harness streamed to both runners, with the exact upstream symbol and Go symbol it exercised. A deliberate, documented difference is a deviation and is counted apart from a mismatch.
| Case | Group | Upstream symbol | Go symbol | Status | Note |
|---|---|---|---|---|---|
| alg-allowlist-match | algorithms | options.algorithms | jwt.WithValidMethods | match | |
| alg-allowlist-multi | algorithms | options.algorithms | jwt.WithValidMethods | match | |
| alg-allowlist-excludes | algorithms | options.algorithms | jwt.WithValidMethods | match | both must fail: token alg is not in the allowlist |
| alg-allowlist-empty | algorithms | options.algorithms | jwt.WithValidMethods | match | both must fail: an empty allowlist permits nothing |
| alg-confusion-hs-token-rs-allowlist | algorithms | options.algorithms | jwt.WithValidMethods | match | both must fail: HS256 token forged with the RSA public-key PEM as the HMAC secret, offered to an RS256 verifier |
| alg-confusion-hs-token-rsa-pubkey-object | algorithms | jwt.verify | jwt.Parse | match | both must fail: HS256 token, RSA public key |
| alg-confusion-hs-token-rsa-pubkey-no-allowlist | algorithms | jwt.verify | jwt.Parse | match | both must fail: no allowlist, but an HS256 token must never verify against an RSA public key |
| alg-confusion-hs-token-pem-secret-no-allowlist | algorithms | jwt.verify (infers algorithms from key material) | jwt.Parse | match | SECURITY: upstream inspects the key material, sees a PEM public key and restricts itself to RS*/PS*, rejecting the HS256 token. The port has no such inference, so with no allowlist it accepts the forged token. |
| alg-rs256-token-hs256-allowlist | algorithms | jwt.verify | jwt.Parse | match | both must fail: RS256 token offered to an HS256 verifier |
| alg-rs256-token-rsa-key-ps256-allowlist | algorithms | jwt.verify | jwt.Parse | match | both must fail: RS256 and PS256 share a key type but are distinct algorithms |
| alg-es256-token-es384-allowlist | algorithms | jwt.verify | jwt.Parse | match | both must fail |
| alg-unknown-in-header | algorithms | jwt.verify | jwt.Parse | match | both must fail: unregistered alg |
| cross-upstream-signed-hs256 | crossverify | jwt.verify | jwt.Parse | match | token minted by jsonwebtoken@9.0.2; both sides must verify it |
| cross-go-signed-hs256 | crossverify | jwt.verify | jwt.Parse | match | token minted by github.com/malcolmston/jwt; both sides must verify it |
| cross-upstream-signed-hs384 | crossverify | jwt.verify | jwt.Parse | match | token minted by jsonwebtoken@9.0.2; both sides must verify it |
| cross-go-signed-hs384 | crossverify | jwt.verify | jwt.Parse | match | token minted by github.com/malcolmston/jwt; both sides must verify it |
| cross-upstream-signed-hs512 | crossverify | jwt.verify | jwt.Parse | match | token minted by jsonwebtoken@9.0.2; both sides must verify it |
| cross-go-signed-hs512 | crossverify | jwt.verify | jwt.Parse | match | token minted by github.com/malcolmston/jwt; both sides must verify it |
| cross-upstream-signed-rs256 | crossverify | jwt.verify | jwt.Parse | match | token minted by jsonwebtoken@9.0.2; both sides must verify it |
| cross-go-signed-rs256 | crossverify | jwt.verify | jwt.Parse | match | token minted by github.com/malcolmston/jwt; both sides must verify it |
| cross-upstream-signed-rs384 | crossverify | jwt.verify | jwt.Parse | match | token minted by jsonwebtoken@9.0.2; both sides must verify it |
| cross-go-signed-rs384 | crossverify | jwt.verify | jwt.Parse | match | token minted by github.com/malcolmston/jwt; both sides must verify it |
| cross-upstream-signed-rs512 | crossverify | jwt.verify | jwt.Parse | match | token minted by jsonwebtoken@9.0.2; both sides must verify it |
| cross-go-signed-rs512 | crossverify | jwt.verify | jwt.Parse | match | token minted by github.com/malcolmston/jwt; both sides must verify it |
| cross-upstream-signed-ps256 | crossverify | jwt.verify | jwt.Parse | match | token minted by jsonwebtoken@9.0.2; both sides must verify it |
| cross-go-signed-ps256 | crossverify | jwt.verify | jwt.Parse | match | token minted by github.com/malcolmston/jwt; both sides must verify it |
| cross-upstream-signed-ps384 | crossverify | jwt.verify | jwt.Parse | match | token minted by jsonwebtoken@9.0.2; both sides must verify it |
| cross-go-signed-ps384 | crossverify | jwt.verify | jwt.Parse | match | token minted by github.com/malcolmston/jwt; both sides must verify it |
| cross-upstream-signed-ps512 | crossverify | jwt.verify | jwt.Parse | match | token minted by jsonwebtoken@9.0.2; both sides must verify it |
| cross-go-signed-ps512 | crossverify | jwt.verify | jwt.Parse | match | token minted by github.com/malcolmston/jwt; both sides must verify it |
| cross-upstream-signed-es256 | crossverify | jwt.verify | jwt.Parse | match | token minted by jsonwebtoken@9.0.2; both sides must verify it |
| cross-go-signed-es256 | crossverify | jwt.verify | jwt.Parse | match | token minted by github.com/malcolmston/jwt; both sides must verify it |
| cross-upstream-signed-es384 | crossverify | jwt.verify | jwt.Parse | match | token minted by jsonwebtoken@9.0.2; both sides must verify it |
| cross-go-signed-es384 | crossverify | jwt.verify | jwt.Parse | match | token minted by github.com/malcolmston/jwt; both sides must verify it |
| cross-upstream-signed-es512 | crossverify | jwt.verify | jwt.Parse | match | token minted by jsonwebtoken@9.0.2; both sides must verify it |
| cross-go-signed-es512 | crossverify | jwt.verify | jwt.Parse | match | token minted by github.com/malcolmston/jwt; both sides must verify it |
| cross-go-signed-eddsa | crossverify | — | jwt.Parse | deviation | EdDSA is port-only; jsonwebtoken@9.0.2 cannot verify it |
| cross-rs256-tampered-payload | crossverify | jwt.verify | jwt.Parse | match | both must fail: payload mutated under an otherwise valid RS256 signature |
| cross-rs256-wrong-public-key | crossverify | jwt.verify | jwt.Parse | match | both must fail: unrelated RSA public key |
| dec-payload | decode | jwt.decode | jwt.ParseUnverified | match | |
| dec-complete | decode | jwt.decode options.complete | jwt.ParseUnverified | match | |
| dec-unverified-bad-signature | decode | jwt.decode | jwt.ParseUnverified | match | decode never checks the signature on either side |
| dec-no-signature | decode | jwt.decode | jwt.ParseUnverified | match | |
| dec-alg-none | decode | jwt.decode | jwt.ParseUnverified | match | |
| dec-garbage | decode | jwt.decode | jwt.ParseUnverified | mismatch | upstream returns null; the port returns an error |
| dec-two-parts | decode | jwt.decode | jwt.ParseUnverified | mismatch | structurally invalid compact serialization |
| dec-empty | decode | jwt.decode | jwt.ParseUnverified | mismatch | |
| rej-tampered-payload | reject | jwt.verify | jwt.Parse | match | both must fail |
| rej-tampered-header-alg | reject | jwt.verify | jwt.Parse | match | both must fail: alg swapped under an HS256 signature |
| rej-wrong-key | reject | jwt.verify | jwt.Parse | match | both must fail |
| rej-empty-secret | reject | jwt.verify | jwt.Parse | match | both must fail |
| rej-truncated-signature | reject | jwt.verify | jwt.Parse | match | both must fail |
| rej-truncated-token | reject | jwt.verify | jwt.Parse | match | both must fail |
| rej-two-parts | reject | jwt.verify | jwt.Parse | match | both must fail |
| rej-four-parts | reject | jwt.verify | jwt.Parse | match | both must fail |
| rej-signature-stripped | reject | jwt.verify | jwt.Parse | match | both must fail: empty signature on a signed alg |
| rej-empty-string | reject | jwt.verify | jwt.Parse | match | both must fail |
| rej-non-base64-payload | reject | jwt.verify | jwt.Parse | match | both must fail |
| rej-none-with-secret | reject | jwt.verify | jwt.ErrNoneAlgDisallowed | match | both must fail: alg:none token offered to an HS256 verifier |
| rej-none-no-allowlist | reject | jwt.verify | jwt.Parse | match | both must fail: 'none' must be opted into explicitly |
| rej-none-listed-but-key-given | reject | jwt.verify | jwt.Parse | match | both must fail: alg none presented with real key material |
| acc-none-explicit-optin | reject | jwt.verify algorithms:['none'] | jwt.WithAllowNone | match | both accept, and only with an explicit opt-in |
| rej-expired | reject | TokenExpiredError | jwt.ErrTokenExpired | match | both must fail |
| rej-not-yet-valid | reject | NotBeforeError | jwt.ErrTokenNotValidYet | match | both must fail |
| rej-wrong-audience | reject | jwt.verify | jwt.Parse | match | both must fail |
| rej-wrong-issuer | reject | jwt.verify | jwt.Parse | match | both must fail |
| rt-hs256 | roundtrip | jwt.sign + jwt.verify | jwt.Sign + jwt.Parse | match | sign then verify in-process; the only safe value comparison for the randomised PS*/ES* signatures |
| rt-hs256-wrongkey | roundtrip | jwt.verify | jwt.Parse | match | both must fail: verification key is unrelated to the signing key |
| rt-hs384 | roundtrip | jwt.sign + jwt.verify | jwt.Sign + jwt.Parse | match | sign then verify in-process; the only safe value comparison for the randomised PS*/ES* signatures |
| rt-hs384-wrongkey | roundtrip | jwt.verify | jwt.Parse | match | both must fail: verification key is unrelated to the signing key |
| rt-hs512 | roundtrip | jwt.sign + jwt.verify | jwt.Sign + jwt.Parse | match | sign then verify in-process; the only safe value comparison for the randomised PS*/ES* signatures |
| rt-hs512-wrongkey | roundtrip | jwt.verify | jwt.Parse | match | both must fail: verification key is unrelated to the signing key |
| rt-rs256 | roundtrip | jwt.sign + jwt.verify | jwt.Sign + jwt.Parse | match | sign then verify in-process; the only safe value comparison for the randomised PS*/ES* signatures |
| rt-rs256-wrongkey | roundtrip | jwt.verify | jwt.Parse | match | both must fail: verification key is unrelated to the signing key |
| rt-rs384 | roundtrip | jwt.sign + jwt.verify | jwt.Sign + jwt.Parse | match | sign then verify in-process; the only safe value comparison for the randomised PS*/ES* signatures |
| rt-rs384-wrongkey | roundtrip | jwt.verify | jwt.Parse | match | both must fail: verification key is unrelated to the signing key |
| rt-rs512 | roundtrip | jwt.sign + jwt.verify | jwt.Sign + jwt.Parse | match | sign then verify in-process; the only safe value comparison for the randomised PS*/ES* signatures |
| rt-rs512-wrongkey | roundtrip | jwt.verify | jwt.Parse | match | both must fail: verification key is unrelated to the signing key |
| rt-ps256 | roundtrip | jwt.sign + jwt.verify | jwt.Sign + jwt.Parse | match | sign then verify in-process; the only safe value comparison for the randomised PS*/ES* signatures |
| rt-ps256-wrongkey | roundtrip | jwt.verify | jwt.Parse | match | both must fail: verification key is unrelated to the signing key |
| rt-ps384 | roundtrip | jwt.sign + jwt.verify | jwt.Sign + jwt.Parse | match | sign then verify in-process; the only safe value comparison for the randomised PS*/ES* signatures |
| rt-ps384-wrongkey | roundtrip | jwt.verify | jwt.Parse | match | both must fail: verification key is unrelated to the signing key |
| rt-ps512 | roundtrip | jwt.sign + jwt.verify | jwt.Sign + jwt.Parse | match | sign then verify in-process; the only safe value comparison for the randomised PS*/ES* signatures |
| rt-ps512-wrongkey | roundtrip | jwt.verify | jwt.Parse | match | both must fail: verification key is unrelated to the signing key |
| rt-es256 | roundtrip | jwt.sign + jwt.verify | jwt.Sign + jwt.Parse | match | sign then verify in-process; the only safe value comparison for the randomised PS*/ES* signatures |
| rt-es256-wrongkey | roundtrip | jwt.verify | jwt.Parse | match | both must fail: verification key is unrelated to the signing key |
| rt-es384 | roundtrip | jwt.sign + jwt.verify | jwt.Sign + jwt.Parse | match | sign then verify in-process; the only safe value comparison for the randomised PS*/ES* signatures |
| rt-es384-wrongkey | roundtrip | jwt.verify | jwt.Parse | match | both must fail: verification key is unrelated to the signing key |
| rt-es512 | roundtrip | jwt.sign + jwt.verify | jwt.Sign + jwt.Parse | match | sign then verify in-process; the only safe value comparison for the randomised PS*/ES* signatures |
| rt-es512-wrongkey | roundtrip | jwt.verify | jwt.Parse | match | both must fail: verification key is unrelated to the signing key |
| rt-eddsa | roundtrip | — | jwt.SigningMethodEdDSA | deviation | the port supports EdDSA (RFC 8037); jsonwebtoken@9.0.2 (jws@3) does not |
| opt-expiresin-int | sign-claim-options | jwt.sign options.expiresIn | jwt.MapClaims["exp"] | match | |
| opt-notbefore-int | sign-claim-options | jwt.sign options.notBefore | jwt.MapClaims["nbf"] | match | |
| opt-audience-string | sign-claim-options | jwt.sign options.audience | jwt.ClaimStrings | match | |
| opt-audience-array | sign-claim-options | jwt.sign options.audience | jwt.ClaimStrings | match | |
| opt-issuer | sign-claim-options | jwt.sign options.issuer | jwt.RegisteredClaims.Issuer | match | |
| opt-subject | sign-claim-options | jwt.sign options.subject | jwt.RegisteredClaims.Subject | match | |
| opt-jwtid | sign-claim-options | jwt.sign options.jwtid | jwt.RegisteredClaims.ID | match | |
| opt-all-claims | sign-claim-options | jwt.sign options.* | jwt.MapClaims + Token.SetKID | match | |
| opt-expiresin-timespan | sign-claim-options | jwt.sign options.expiresIn (string timespan) | — | mismatch | upstream parses 'ms' timespan strings; the port has no sign options at all |
A case can show the port is less safe than the library it ports. Those findings are written into security.json and filed as draft advisories, never as public issues with a working repro.
| Finding | Severity | Affected | Cases | Summary |
|---|---|---|---|---|
| GHSA-2vc9-3gjf-2276 | medium | <= 0.3.0 | — | WithValidMethods with an empty slice is ignored, so a computed-empty allowlist restricts nothing |
| GHSA-75rm-5v4f-887v | medium | <= 0.3.0 | — | Algorithm confusion: HS256 token signed with an RSA public-key PEM is accepted when no allowlist is set |