NAME
Crypt::JWS - JSON Web Signatures over OpenSSL's libcrypto
VERSION
Version 0.01
SYNOPSIS
use Crypt::JWS qw(sign verify);
use Crypt::JWS::Key;
my $key = Crypt::JWS::Key->generate('ES256');
my $token = sign($key, $payload_bytes, alg => 'ES256',
kid => $key->thumbprint, typ => 'at+jwt');
my $payload = verify($token, $key, algs => ['ES256']);
# undef on any failure - tampering, wrong key, alg outside the
# allowlist, malformed token
DESCRIPTION
Crypt::JWS signs and verifies JWS compact serializations (RFC 7515) using OpenSSL's libcrypto.
Supported algorithms: RS256/384/512, ES256/384/512, HS256/384/512. All three families run through one EVP code path. The none algorithm has no code path at all, and verify requires a caller-supplied algs allowlist - the token header's alg can only select among algorithms the caller already agreed to, and the key's type is cross-checked against the algorithm family in C, so RSA-key-as-HMAC-secret confusion attacks are structurally impossible.
OpenSSL 1.1 and 3.x are both supported; the divergent key import APIs are chosen by compile-time feature probes.
FUNCTIONS
sign ($key, $payload, %opts)
Returns the compact serialization. alg is required. Optional kid and typ land in the protected header; extra_headers merges beneath them and can never clobber them. The key must carry a private part and match the algorithm family.
verify ($token, $key, algs => \@allowlist)
Returns the payload bytes, or undef on any failure - one bit of information, never a reason. $key may be a coderef called with the decoded header (kid routing against a JWKS set); it returns the key to use or undef.
peek ($token)
The decoded protected header, without any validation whatsoever. For routing decisions only; never trust its contents.
sha256 / sha384 / sha512 ($bytes)
hmac_sha256 / hmac_sha384 / hmac_sha512 ($key, $bytes)
b64url ($bytes) / b64url_decode ($str)
Unpadded base64url (RFC 4648 section 5); decode tolerates padding and the standard alphabet.
ct_eq ($a, $b)
Constant-time equality via CRYPTO_memcmp.
random_bytes ($n)
CSPRNG bytes via RAND_bytes; croaks rather than degrades on failure.
C ABI
Crypt::JWS exposes a small C ABI so that other XS modules can sign, verify, and digest entirely in C, with no per-call Perl dispatch. The motivating consumers are PDF signing (PDF::Make) and any server that wants JWS operations on a hot path.
The header is distributed through ExtUtils::Depends - a consumer does not copy it. This dist is an ExtUtils::Depends provider (it also consumes File::Raw::JSON the same way): building installs jws_abi.h and writes Crypt::JWS::Install::Files, so a dependent's Makefile.PL that says
my $pkg = ExtUtils::Depends->new('My::Consumer', 'Crypt::JWS');
WriteMakefile( ..., $pkg->get_makefile_vars );
picks up jws_abi.h on its include path automatically.
This is an integration surface for XS authors, not part of the Perl API. Perl callers should use "FUNCTIONS" and Crypt::JWS::Key.
The table
The contract lives in include/jws_abi.h (installed via ExtUtils::Depends):
#define JWS_ABI_VERSION 1
typedef struct jws_abi {
int version; /* == JWS_ABI_VERSION */
/* key lifecycle - NULL on failure */
void *(*key_from_pem)(pTHX_ const char *pem, STRLEN len);
void *(*key_from_oct)(pTHX_ const unsigned char *secret, STRLEN len);
void (*key_free)(pTHX_ void *key);
int (*key_is_private)(pTHX_ void *key);
/* raw JWS ops over signing-input bytes; alg is "RS256" etc. */
SV *(*sign)(pTHX_ void *key, const char *alg, STRLEN alglen,
const unsigned char *input, STRLEN inlen);
int (*verify)(pTHX_ void *key, const char *alg, STRLEN alglen,
const unsigned char *input, STRLEN inlen,
const unsigned char *sig, STRLEN siglen);
/* primitives */
SV *(*sha256)(pTHX_ const unsigned char *in, STRLEN len);
SV *(*hmac_sha256)(pTHX_ const unsigned char *key, STRLEN keylen,
const unsigned char *in, STRLEN len);
SV *(*b64url)(pTHX_ const unsigned char *in, STRLEN len);
SV *(*b64url_decode)(pTHX_ const char *in, STRLEN len);
int (*ct_eq)(pTHX_ const unsigned char *a, STRLEN alen,
const unsigned char *b, STRLEN blen);
SV *(*random_bytes)(pTHX_ STRLEN n);
} jws_abi;
The table is append-only: new entries go at the end, JWS_ABI_VERSION bumps, existing offsets never move. A consumer written against version N keeps working against every later version.
Crypt::JWS::_abi_ptr
my $iv = Crypt::JWS::_abi_ptr;
Returns the address of the process-wide jws_abi table as an integer (an IV). A consumer calls this once at BOOT, INT2PTRs it to a const jws_abi *, and checks ->version == JWS_ABI_VERSION before using it. Not intended to be called from Perl for any other purpose. Crypt::JWS::_abi_selftest exercises the table end to end and is what t/06-abi.t runs.
Functions and ownership
Keys are opaque pointers. key_from_pem parses a private or public PEM (RSA and EC only); key_from_oct wraps a shared secret for the HS* algorithms; both return NULL on failure and everything they return must eventually go through key_free. key_is_private reports whether a signing operation is possible.
sign takes the raw signing-input bytes (the consumer builds b64url(header).b64url(payload) itself, or signs arbitrary bytes for non-JWS uses) and returns the signature bytes as an SV with a reference count of one owned by the caller - sv_2mortal it or SvREFCNT_dec when done - or NULL when the key type does not match the algorithm family, the key is public, or OpenSSL fails. ES signatures are returned in JOSE r||s form. verify returns 1 for a valid signature and 0 for anything else, and never says why. The key-type/algorithm cross-check applies to both: an oct key never verifies an RS/ES token and vice versa.
Every SV-returning primitive follows the same ownership rule (+1, caller frees). b64url_decode returns NULL on invalid input; random_bytes croaks rather than degrade when the CSPRNG fails.
Example: signing detached bytes from a consumer
Vendor nothing - add Crypt::JWS via ExtUtils::Depends (above), resolve the table at boot, then sign wherever needed:
#include "jws_abi.h" /* found via ExtUtils::Depends, not copied */
static const jws_abi *JWS = NULL;
MODULE = My::Consumer PACKAGE = My::Consumer
BOOT:
{
IV p = 0; dSP;
eval_pv("require Crypt::JWS;", FALSE);
PUSHMARK(SP); PUTBACK;
if (call_pv("Crypt::JWS::_abi_ptr", G_SCALAR|G_EVAL) > 0) {
SPAGAIN; p = POPi; PUTBACK;
}
if (p) {
const jws_abi *a = INT2PTR(const jws_abi *, p);
if (a && a->version == JWS_ABI_VERSION) JWS = a;
}
/* JWS == NULL => Crypt::JWS is absent or too old; croak at
* first use, or disable the feature. */
}
# at boot, load the signing key once:
# void *key = JWS->key_from_pem(aTHX_ pem, pem_len);
#
# per operation:
# SV *sig = JWS->sign(aTHX_ key, "ES256", 5, input, input_len);
# ... use SvPVbyte(sig, len) ... then SvREFCNT_dec(sig);
#
# at teardown:
# JWS->key_free(aTHX_ key);
SEE ALSO
Crypt::JWS::Key - key import, generation, export, thumbprints
AUTHOR
LNATION, <email at lnation.org>
LICENSE AND COPYRIGHT
This software is Copyright (c) 2026 by LNATION <email@lnation.org>.
This is free software, licensed under:
The Artistic License 2.0 (GPL Compatible)