Skip to content

About

JWS-compact JWT verifier (HS256 + ES256) — wraps swift-crypto. Sendable, Foundation-free public API.

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

swift-jwt-verify

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.

Install

.package(url: "https://github.com/bare-swift/swift-jwt-verify.git", from: "0.3.0")
.product(name: "JWTVerify", package: "swift-jwt-verify")

Usage

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)

Signing (v0.2+)

Since v0.2, the package signs as well as verifies. JWTSigner mirrors JWTVerifier's shape — construct once with (algorithm, key) and call sign(_:).

HS256 sign + verify round-trip

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 == payload

ES256 sign + verify round-trip

ES256 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 / ES512 sign + verify round-trip (v0.3+)

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 (Ed25519) sign + verify round-trip (v0.3+)

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)

Multi-key verifier scenarios with kid (v0.3+)

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"}).

Public API

  • JWTVerifier(algorithm: JWTAlgorithm, key: [UInt8]) — value type, Sendable.
  • JWTVerifier.verify(_ token: String) throws(JWTVerifyError) -> Verified
  • JWTSigner(algorithm: JWTAlgorithm, key: [UInt8], kid: String? = nil) — value type, Sendable. (v0.2+; kid v0.3+)
  • JWTSigner.sign(_ payload: [UInt8]) throws(JWTVerifyError) -> String (v0.2+)
  • Verified — headerBytes: [UInt8], payloadBytes: [UInt8].
  • JWTAlgorithm — .hs256, .es256, .es384, .es512, .eddsa.
  • JWTVerifyError typed-throws enum (6 cases).

Supported algorithms

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.

Dependencies

  • swift-base64 0.1.0 — Base64URL codec.
  • swift-crypto 3.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.)

Security notes

  • Algorithm-confusion defense: the verifier checks the JWT header's alg matches the configured algorithm. A token signed with HS256 is rejected by an ES256-configured verifier, and vice versa.
  • The none algorithm 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 || s form (64 bytes) before validation.

Out of scope (deferred to v0.4+)

  • RS256 / RS384 / RS512 / PS256. Pending swift-crypto's RSA API moving out of _RSA experimental SPI.
  • Header customization beyond kid (jku / x5u / cty). v0.3 supports kid only.
  • 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.

Documentation

Full DocC documentation: https://bare-swift.github.io/swift-jwt-verify/

License

Apache 2.0 with LLVM exception. See LICENSE and NOTICE.

About

JWS-compact JWT verifier (HS256 + ES256) — wraps swift-crypto. Sendable, Foundation-free public API.

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages