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)