JWCrypto Integration
hsmkey provides seamless integration with jwcrypto for JSON Web operations. This guide covers JWS (signing), JWE (encryption), and JWT (tokens).
Overview
The HSMJWK class extends jwcrypto’s JWK class, allowing HSM-backed keys
to be used anywhere a regular JWK is expected. The private key operations
(signing, decryption) are performed inside the HSM, while public key
operations use the exported public key material.
HSMJWK Class
Creating an HSMJWK
Load a key from the HSM:
from hsmkey import HSMJWK, hsm_session
with hsm_session(module_path, token_label, pin) as session:
# Load by label
key = HSMJWK.from_hsm(session, key_label="my-key")
# Load by ID
key = HSMJWK.from_hsm(session, key_id=b'\x01')
# With additional JWK parameters
key = HSMJWK.from_hsm(
session,
key_label="my-key",
kid="unique-key-id",
use="sig",
key_ops=["sign", "verify"]
)
Exporting Public Keys
Export the public key (private key export is blocked):
# Export as dict
public_jwk = key.export_public(as_dict=True)
print(public_jwk)
# {'kty': 'RSA', 'n': '...', 'e': 'AQAB', 'kid': 'my-key'}
# Export as JSON string
public_json = key.export_public()
# Private key export raises an error
try:
key.export_private() # Raises HSMSessionError
except HSMSessionError:
print("Private key cannot be exported from HSM")
JWS Signing
Supported Algorithms
RSA:
RS256, RS384, RS512 (RSASSA-PKCS1-v1_5)
PS256, PS384, PS512 (RSASSA-PSS)
ECDSA:
ES256 (P-256)
ES384 (P-384)
ES512 (P-521)
EdDSA:
EdDSA (Ed25519, Ed448)
Basic Signing
from jwcrypto.jws import JWS
from jwcrypto.common import json_encode
from hsmkey import HSMJWK, hsm_session
with hsm_session(module_path, token_label, pin) as session:
key = HSMJWK.from_hsm(session, key_label="rsa-2048")
payload = b'{"sub": "user@example.com"}'
jws = JWS(payload)
jws.add_signature(
key,
alg="RS256",
protected=json_encode({"alg": "RS256"})
)
# Compact serialization (3 parts)
token = jws.serialize(compact=True)
# JSON serialization
json_token = jws.serialize()
Signing with Multiple Keys
jws = JWS(payload)
# Add signature with RSA key
jws.add_signature(
rsa_key,
alg="RS256",
protected=json_encode({"alg": "RS256", "kid": "rsa-key"})
)
# Add signature with EC key
jws.add_signature(
ec_key,
alg="ES256",
protected=json_encode({"alg": "ES256", "kid": "ec-key"})
)
# JSON serialization supports multiple signatures
multi_sig_token = jws.serialize()
Verification
# Verify with HSM key
jws = JWS()
jws.deserialize(token, key)
verified_payload = jws.payload
# Verify with exported public key (software verification)
from jwcrypto.jwk import JWK
public_jwk = JWK(**hsm_key.export_public(as_dict=True))
jws = JWS()
jws.deserialize(token, public_jwk)
JWE Encryption
Supported Algorithms
Key Encryption:
RSA-OAEP (RSA with OAEP padding using SHA-1)
Content Encryption:
A128GCM, A192GCM, A256GCM
A128CBC-HS256, A192CBC-HS384, A256CBC-HS512
Encryption
import json
from jwcrypto.jwe import JWE
from hsmkey import HSMJWK, hsm_session
with hsm_session(module_path, token_label, pin) as session:
key = HSMJWK.from_hsm(session, key_label="rsa-2048")
plaintext = b"Confidential data"
jwe = JWE(
plaintext,
protected=json.dumps({"alg": "RSA-OAEP", "enc": "A256GCM"})
)
jwe.add_recipient(key)
# Compact serialization (5 parts)
encrypted = jwe.serialize(compact=True)
Decryption
jwe = JWE()
jwe.deserialize(encrypted, key)
decrypted = jwe.payload
Hybrid Encryption Pattern
Encrypt with public key (software), decrypt with HSM:
from jwcrypto.jwk import JWK
with hsm_session(module_path, token_label, pin) as session:
hsm_key = HSMJWK.from_hsm(session, key_label="rsa-2048")
# Get public key for encryption (can be distributed)
public_jwk = JWK(**hsm_key.export_public(as_dict=True))
# Encrypt with public key (no HSM needed)
jwe = JWE(
b"Secret message",
protected=json.dumps({"alg": "RSA-OAEP", "enc": "A256GCM"})
)
jwe.add_recipient(public_jwk)
encrypted = jwe.serialize(compact=True)
# Decrypt with HSM (private key never exported)
jwe2 = JWE()
jwe2.deserialize(encrypted, hsm_key)
plaintext = jwe2.payload
JWT Tokens
Signed JWT
import time
from jwcrypto.jwt import JWT
from hsmkey import HSMJWK, hsm_session
with hsm_session(module_path, token_label, pin) as session:
key = HSMJWK.from_hsm(session, key_label="rsa-2048", kid="jwt-key")
claims = {
"iss": "https://auth.example.com",
"sub": "user123",
"aud": "https://api.example.com",
"iat": int(time.time()),
"exp": int(time.time()) + 3600,
}
token = JWT(
header={"alg": "RS256", "kid": "jwt-key"},
claims=claims
)
token.make_signed_token(key)
jwt_string = token.serialize()
JWT Verification
verified = JWT(jwt=jwt_string, key=key)
claims = json.loads(verified.claims)
Encrypted JWT
claims = {
"sub": "user123",
"sensitive_data": "confidential"
}
token = JWT(
header={"alg": "RSA-OAEP", "enc": "A256GCM"},
claims=claims
)
token.make_encrypted_token(key)
encrypted_jwt = token.serialize()
# Decrypt
decrypted = JWT(jwt=encrypted_jwt, key=key, expected_type="JWE")
claims = json.loads(decrypted.claims)
Nested JWT (Signed then Encrypted)
# First, sign with one key
inner = JWT(header={"alg": "ES256"}, claims=claims)
inner.make_signed_token(signing_key)
signed_jwt = inner.serialize()
# Then, encrypt with another key
outer = JWT(
header={"alg": "RSA-OAEP", "enc": "A256GCM", "cty": "JWT"},
claims=signed_jwt
)
outer.make_encrypted_token(encryption_key)
nested_jwt = outer.serialize()
# To verify: decrypt then verify
decrypted_outer = JWT(jwt=nested_jwt, key=encryption_key, expected_type="JWE")
inner_jwt = decrypted_outer.claims
verified_inner = JWT(jwt=inner_jwt, key=signing_key)
final_claims = json.loads(verified_inner.claims)
HSMJWKSet for Key Management
Manage multiple HSM keys:
from hsmkey import HSMJWKSet, hsm_session
with hsm_session(module_path, token_label, pin) as session:
jwk_set = HSMJWKSet(session)
# Add keys
rsa_key = jwk_set.add_key(key_label="rsa-2048", kid="rsa-signing")
ec_key = jwk_set.add_key(key_label="ec-p256", kid="ec-signing")
# Get key by ID
key = jwk_set.get_key("rsa-signing")
# Iterate over keys
for key in jwk_set:
print(key.export_public(as_dict=True))
# Export public JWKS for .well-known/jwks.json
public_keys = [k.export_public(as_dict=True) for k in jwk_set]
jwks = {"keys": public_keys}
Key Rotation Pattern
with hsm_session(module_path, token_label, pin) as session:
jwk_set = HSMJWKSet(session)
# Old key for verification of existing tokens
old_key = jwk_set.add_key(key_label="rsa-2048", kid="key-v1")
# New key for signing new tokens
new_key = jwk_set.add_key(key_label="rsa-3072", kid="key-v2")
# Sign new tokens with new key
jws = JWS(payload)
jws.add_signature(
new_key,
alg="RS384",
protected=json_encode({"alg": "RS384", "kid": "key-v2"})
)
# Old tokens can still be verified
old_token = "..."
old_key = jwk_set.get_key("key-v1")
if old_key:
jws = JWS()
jws.deserialize(old_token, old_key)
Error Handling
from hsmkey import HSMJWK, hsm_session
from hsmkey.exceptions import HSMKeyNotFoundError, HSMSessionError
with hsm_session(module_path, token_label, pin) as session:
try:
key = HSMJWK.from_hsm(session, key_label="nonexistent")
except HSMKeyNotFoundError as e:
print(f"Key not found: {e}")
try:
key = HSMJWK.from_hsm(session, key_label="rsa-2048")
key.export_private() # Attempt to export private key
except HSMSessionError as e:
print(f"Operation not allowed: {e}")
Best Practices
Use Context Managers: Always use
hsm_sessionfor automatic cleanupCache Keys: Load keys once and reuse within a session
Use Key IDs: Always specify
kidfor key identificationLimit Session Scope: Keep sessions short to release HSM resources
Handle Errors: Catch specific exceptions for better error handling