JWS-compact JWT verifier and signer (HS256 + ES256 + ES384 + ES512 + EdDSA, with optional kid) — Sendable, Foundation-free public API; wraps swift-crypto.
Part of the bare-swift ecosystem. The package name is preserved despite v0.2 adding signing; a possible rename to swift-jwt is a future consideration.
.package(url: "https://github.com/bare-swift/swift-jwt-verify.git", from: "0.3.0").product(name: "JWTVerify", package: "swift-jwt-verify")import JWTVerify
// HS256 (HMAC-SHA256, symmetric):
let hsVerifier = JWTVerifier(algorithm: .hs256, key: secretBytes)
let verified = try hsVerifier.verify(token)
// verified.headerBytes / verified.payloadBytes are the raw decoded bytes
// ES256 (ECDSA P-256, asymmetric):
// publicKey is the 65-byte x963 uncompressed form: 0x04 || x(32B) || y(32B)
let esVerifier = JWTVerifier(algorithm: .es256, key: publicKeyBytes)
let verified = try esVerifier.verify(token)Since v0.2, the package signs as well as verifies. JWTSigner mirrors JWTVerifier's shape — construct once with (algorithm, key) and call sign(_:).
import JWTVerify
let secret: [UInt8] = Array("super-secret-hmac-key-32-bytes!!".utf8)
let payload: [UInt8] = Array(#"{"sub":"alice"}"#.utf8)
let signer = JWTSigner(algorithm: .hs256, key: secret)
let token = try signer.sign(payload)
// token: "eyJhbGciOiJIUzI1NiIs...<...>..."
let verifier = JWTVerifier(algorithm: .hs256, key: secret)
let verified = try verifier.verify(token)
// verified.payloadBytes == payloadES256 uses asymmetric keys. swift-crypto's P256.Signing.PrivateKey provides both forms:
import Crypto
import JWTVerify
let priv = P256.Signing.PrivateKey()
let signer = JWTSigner(
algorithm: .es256,
key: Array(priv.rawRepresentation) // 32-byte raw scalar
)
let verifier = JWTVerifier(
algorithm: .es256,
key: Array(priv.publicKey.x963Representation) // 65-byte uncompressed
)
let token = try signer.sign(payload)
let verified = try verifier.verify(token)ES384 and ES512 follow the same pattern as ES256, with larger key + signature sizes:
import Crypto
import JWTVerify
// ES384: 48-byte private scalar / 97-byte x963 public key / 96-byte signature
let priv = P384.Signing.PrivateKey()
let signer = JWTSigner(algorithm: .es384, key: Array(priv.rawRepresentation))
let verifier = JWTVerifier(algorithm: .es384, key: Array(priv.publicKey.x963Representation))
let token = try signer.sign(payload)
let verified = try verifier.verify(token)
// ES512: 66-byte private scalar / 133-byte x963 public key / 132-byte signature
let priv = P521.Signing.PrivateKey()
let signer = JWTSigner(algorithm: .es512, key: Array(priv.rawRepresentation))
let verifier = JWTVerifier(algorithm: .es512, key: Array(priv.publicKey.x963Representation))EdDSA is notably different from the ECDSA family: 32-byte seed + 32-byte public key (no x963 wrapper), 64-byte signature.
import Crypto
import JWTVerify
let priv = Curve25519.Signing.PrivateKey()
let signer = JWTSigner(
algorithm: .eddsa,
key: Array(priv.rawRepresentation) // 32-byte seed
)
let verifier = JWTVerifier(
algorithm: .eddsa,
key: Array(priv.publicKey.rawRepresentation) // 32-byte raw — NOT x963
)
let token = try signer.sign(payload)
let verified = try verifier.verify(token)For consumers that rotate keys, pass kid: to include a JWS kid ("key ID") header field:
let signer = JWTSigner(algorithm: .es256, key: privateKeyBytes, kid: "2026-Q2")
let token = try signer.sign(payload)
// emitted header: {"alg":"ES256","typ":"JWT","kid":"2026-Q2"}kid lookup on the verify side is the caller's responsibility — JWTVerifier verifies against a single configured key. Adopters typically parse the header kid field themselves and pick the matching JWTVerifier from a keychain.
Default kid: nil preserves v0.2 byte-for-byte canonical header output ({"alg":"...","typ":"JWT"}).
JWTVerifier(algorithm: JWTAlgorithm, key: [UInt8])— value type, Sendable.JWTVerifier.verify(_ token: String) throws(JWTVerifyError) -> VerifiedJWTSigner(algorithm: JWTAlgorithm, key: [UInt8], kid: String? = nil)— value type, Sendable. (v0.2+;kidv0.3+)JWTSigner.sign(_ payload: [UInt8]) throws(JWTVerifyError) -> String(v0.2+)Verified—headerBytes: [UInt8],payloadBytes: [UInt8].JWTAlgorithm—.hs256,.es256,.es384,.es512,.eddsa.JWTVerifyErrortyped-throws enum (6 cases).
| Algorithm | Signer key | Verifier key | Signature |
|---|---|---|---|
.hs256 |
HMAC secret (any length) | Same (symmetric) | 32B |
.es256 |
32B P-256 raw scalar | 65B P-256 x963 (0x04 || x || y) |
64B R||S |
.es384 |
48B P-384 raw scalar | 97B P-384 x963 | 96B R||S |
.es512 |
66B P-521 raw scalar | 133B P-521 x963 | 132B R||S |
.eddsa |
32B Ed25519 seed | 32B Ed25519 raw (no x963 wrapper) | 64B raw |
Other algorithms (RS256, RS384, RS512, PS256) are deferred to v0.4+ pending swift-crypto's RSA SPI stabilization.
swift-base640.1.0 — Base64URL codec.swift-crypto3.0+ — HMAC + ECDSA primitives.
(Foundation is imported internally to bridge to swift-crypto's Data-based API. The public API of swift-jwt-verify is Foundation-free.)
- Algorithm-confusion defense: the verifier checks the JWT header's
algmatches the configured algorithm. A token signed withHS256is rejected by anES256-configured verifier, and vice versa. - The
nonealgorithm is unconditionally rejected (it's not in the supported set). - HMAC compare uses swift-crypto's constant-time
HMAC.isValidAuthenticationCode. - ES256 signatures are decoded from the RFC 7515 § 3.4 raw
r || sform (64 bytes) before validation.
- RS256 / RS384 / RS512 / PS256. Pending swift-crypto's RSA API moving out of
_RSAexperimental SPI. - Header customization beyond
kid(jku/x5u/cty). v0.3 supportskidonly. - Claim builder helpers / JSON encoder integration. Caller produces
[UInt8]JSON payload. - JWK / JWKS endpoint fetching. Caller supplies raw key bytes.
- JSON claim parsing. Use
swift-jwt-claims(Phase 11 Tranche 11D). - JWE / JWS-JSON serialization. v0.3 covers JWS-compact only.
Full DocC documentation: https://bare-swift.github.io/swift-jwt-verify/