JOSE Technical Reference
Target Audience: Developers, Solution Architects, Integration Engineers
Reading Time: 20 minutes
Prerequisites: Understanding of public-key cryptography, JSON, REST APIs
Executive Summary
ANKASecure implements JOSE (JSON Object Signing and Encryption) as its foundational cryptographic format for all API operations. This document provides a technical deep dive into JOSE standards (RFCs 7515-7519) and how ANKASecure extends them for post-quantum cryptography and streaming operations.
Why JOSE? See the strategic rationale in From PKCS#7 to JWS/JWE: The Evolution of API Security*.
Table of Contents
- What is JOSE?
- JWS (JSON Web Signature)
- JWE (JSON Web Encryption)
- JWT (JSON Web Token)
- Nested Tokens (Sign-Then-Encrypt)
- ANKASecure Extensions
- RFC References
What is JOSE?
JOSE = JSON Object Signing and Encryption
JOSE is a framework of IETF standards for representing cryptographic operations in JSON format, designed for modern API-first architectures.
Core Standards
| Standard | RFC | Purpose |
|---|---|---|
| JWS | 7515 | Digitally sign JSON payloads |
| JWE | 7516 | Encrypt JSON payloads |
| JWA | 7518 | Define algorithms for signing and encryption |
| JWT | 7519 | Compact, URL-safe tokens with claims |
Design Principles
- Human-readable: Base64url-encoded JSON, not binary ASN.1
- Compact: URL-safe representation for tokens
- Crypto-agile: Extensible algorithm identifiers in headers
- Language-agnostic: Standard JSON parsing, widespread library support
- API-native: Designed for OAuth 2.0, OpenID Connect, REST APIs
JOSE vs PKCS#7
| Aspect | PKCS#7 (CMS) | JOSE |
|---|---|---|
| Format | ASN.1 binary | JSON (base64url) |
| Readability | Requires specialized tools | Human-readable |
| Primary Use Case | Files, documents, S/MIME | APIs, tokens, microservices |
| Cloud Support | Limited | Native across all platforms |
| OAuth/OIDC | Not supported | Standard foundation |
| Crypto Agility | Rigid ASN.1 structures | Extensible algorithm headers |
| Zero Trust | Certificate-based trust | Identity claims support |
ANKASecure choice: JOSE enables API-first architecture, cloud-native deployment, and seamless post-quantum algorithm integration.
JWS (JSON Web Signature)
RFC 7515 - Digital signatures for JSON payloads
Purpose
Provide integrity and authenticity for data by computing a cryptographic signature over a payload.
Structure
JWS consists of three parts:
Example (RSA-PSS-256):
eyJhbGciOiJQUzI1NiIsImtpZCI6Im15LXJzYS1rZXkifQ
.
eyJkYXRhIjoidGhpcyBpcyBteSBwYXlsb2FkIn0
.
dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk...
JOSE Header
The header contains metadata about the signature operation:
{
"alg": "PS256", // Algorithm: RSA-PSS with SHA-256
"kid": "my-rsa-key", // Stable KID (the identity, suffix-free)
"ank_kv": 2, // ANKA Key Version — material version (ank_ prefix)
"typ": "JWT", // Type (optional)
"cty": "application/json" // Content Type (optional)
}
The ank_kv claim (ANKA Key Version)
Every JOSE token AnkaSecure emits carries ank_kv in the protected header, where the token's own integrity protection covers it: the AES-GCM tag of a JWE, the signature of a JWS — a composite JWS included, because its composite signature is computed over the JWS Signing Input, protected header and all. It is a positive integer naming the material version of the Stable KID that was active at encrypt/sign time. On decrypt/verify the platform resolves (tenant, kid, ank_kv) → material. This is a custom claim registered per RFC 7515 §4.2; the ank_ prefix claims product ownership. It is informative, not in crit — a verifier that does not understand it does not reject the token. The kid is always the Stable KID: rotation never changes it (the version moves in ank_kv), and a composite token carries it once, in its protected header (no :classical / :pqc suffixes).
The crit parameter
crit lists header parameters that a recipient MUST understand (RFC 7515 §4.1.11, applied to JWE by RFC 7516 §4.1.13). ANKASecure applies both halves of that rule to every header it reads:
- Every protected header is checked. A General JSON JWS carries one protected header per signature, and an unrecognised extension in any of them is refused, not only in the first. A JWE has one protected header, shared by all recipients.
critmust be integrity protected, so it may appear only in a protected header. Acritmember in a per-signature or per-recipientheader, in the shared JWSheader, or in a detached (streaming) JWE header outsideprotected— its root, itsunprotectedmember, a recipient header — is refused.- No member escapes judgement. A JWE carries a closed set of members on every route, buffered and streaming (see Members and Header Locations). A shared
unprotectedheader or a JSONaadmember is refused with422JWE_ROOT_MEMBER_FORBIDDENbefore any key is used, so acritinsideunprotectedis refused together with the member that carries it; nothing is dropped unread. - What is understood depends on the plane. The buffered routes (
/api/v3/crypto/verify,/resign,/decrypt,/reencrypt,/decrypt-verify) understand no critical extension: anycritentry is refused with400. The streaming JWS routes (/api/v3/crypto/stream/verify,/stream/resign) and the interoperability verify understand exactly one,b64(RFC 7797), which their detached signatures carry as"crit": ["b64"]; any other entry is refused with400. A detached JWE header whosecritis misplaced is refused with422, the streaming plane's malformed-header status. - A refused header is never read as a missing
kid. On/reencryptand/resign,sourceKidnames the source key when the header carries nokid; it does not turn acritrefusal into an accepted request.
Common algorithms:
| Algorithm | Description | Security Level |
|---|---|---|
RS256 | RSA PKCS#1 v1.5 with SHA-256 | 112-bit (RSA-2048) |
PS256 | RSA-PSS with SHA-256 | 112-bit (RSA-2048) |
ES256 | ECDSA P-256 with SHA-256 | 128-bit |
ES384 | ECDSA P-384 with SHA-384 | 192-bit |
EdDSA | Ed25519 or Ed448 | 128-bit / 224-bit |
ML-DSA-65 | Post-quantum signature (Dilithium) | 192-bit (NIST Level 3) |
ECDSA signature encoding
An ECDSA JWS Signature is the two integers R and S of the signature, each written big-endian at a fixed length and concatenated, R first and then S (RFC 7518 §3.4). It is not the ASN.1 DER SEQUENCE that a Java, OpenSSL or PKCS#11 ECDSA engine returns; a base64url signature that begins MEQ, MEU or MGQ is DER and is not a JWS Signature. Each integer takes the byte length of the curve's order, with leading zero bytes kept, so the length is the same for every signature made with the key:
| Curve | alg emitted | R and S | JWS Signature |
|---|---|---|---|
| P-256 | ES256 | 32 bytes each | 64 bytes |
| P-384 | ES384 | 48 bytes each | 96 bytes |
| P-521 | ES512 | 66 bytes each | 132 bytes |
| brainpoolP256r1 | ESB256 | 32 bytes each | 64 bytes |
| brainpoolP384r1 | ESB384 | 48 bytes each | 96 bytes |
| brainpoolP512r1 | ESB512 | 64 bytes each | 128 bytes |
The rule is the same on every ANKASecure JWS route: buffered and streaming sign, verify and re-sign, the signature inside sign-encrypt and decrypt-verify, the interoperability verify with a caller-supplied public key, and the PKCS#7 to JOSE conversion. A verifier accepts the R-then-S concatenation at exactly the length above and treats any other value, a DER encoding included, as an invalid signature, as a conformant JOSE library does. The rule governs a JWS Signature that IS an ECDSA signature; it does not reach inside a composite signature, whose ECDSA component is a DER Ecdsa-Sig-Value carried within the one composite value (see Composite signature algorithms).
For the NIST curves any JOSE library verifies these signatures. For the brainpool curves the bytes follow the same rule (the one RFC 9053 §2.1 states for every curve and BSI TR-03111 calls the plain format), and the alg names the curve: RFC 7518 defines ES256, ES384 and ES512 for P-256, P-384 and P-521 only, so a brainpool key signs under the fully-specified identifiers of RFC 9864 (ESB256, ESB384, ESB512), which RFC 9864 registers for COSE only (see SIMPLE brainpool ECDSA). A JOSE library that does not know those identifiers refuses the alg; verify brainpool signatures with a library that admits them or takes the curve from the key.
RSA signature padding
The alg names the padding, and a signature is valid only under the padding its alg names (RFC 7518 §3.3, §3.5):
alg | Padding | Hash, MGF1 hash | Salt length |
|---|---|---|---|
RS256 / RS384 / RS512 | RSASSA-PKCS1-v1_5 | SHA-256 / 384 / 512 | n/a |
PS256 / PS384 / PS512 | RSASSA-PSS | SHA-256 / 384 / 512, the same hash for MGF1 | the hash length (32 / 48 / 64 bytes) |
ANKASecure signs every RSA key as PS256, on every route: buffered and streaming sign and re-sign, the signature inside sign-encrypt, the PKCS#7 to JOSE conversion, and a composite re-sign whose target is a simple key. A key the platform manages is bound to that one alg: a signature whose header names any other alg, or a PS256 header over a PKCS#1 v1.5 value, verifies as invalid (RFC 8725 §3.1). The interoperability verify takes the caller's public key, so it honours the header: an RSA key verifies any RS* or PS* signature, each under its own padding, and an EC key only the ES* of its curve.
A header alg the key is not bound to
To a key, a signature under an alg it is not bound to is simply not its signature. Every route therefore answers it exactly as it answers a signature that does not verify under the right alg, so the two cases cannot be told apart from the response:
| Route | Wrong alg, or a bad signature under the right one |
|---|---|
| Buffered and streaming verify, the interoperability verify | the verdict is invalid (false) |
Buffered decrypt-verify | 422 "Signature verification failed"; no plaintext is returned |
Streaming /api/v3/crypto/stream/decrypt-verify | PART 1 carries the whole decrypted payload and the trailing verdict part is INVALID; the client commits nothing |
Buffered and streaming resign, including the simple-key source of a composite re-sign | 400, "The old signature is not valid for the provided data." |
The streaming decrypt-verify cannot withhold its payload part: the payload is streamed before the signature, which follows it in the inner JWS, is read. The verdict part is what the client acts on.
The interoperability verify also proves that the caller's public key is the algorithm named in the request: an EC key must be on the named curve, and an ML-DSA or EdDSA key must be the named parameter set. A key that is not, for example a secp256k1 key sent as P-256, is refused with 422 and is never verified as the curve its label names.
Compact Serialization (Standard JWT)
Used for authentication tokens, small payloads:
Characteristics:
- URL-safe (can be used in HTTP headers, query parameters)
- Single signature only
- Payload embedded in token
- Limited to ~8KB practical size (HTTP header limits)
JSON Serialization (Multiple Signatures)
Used when multiple signers are required:
{
"payload": "eyJkYXRhIjoidGhpcyBpcyBteSBwYXlsb2FkIn0",
"signatures": [
{
"protected": "eyJhbGciOiJQUzI1NiIsImtpZCI6ImtleS0xIn0",
"signature": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk..."
},
{
"protected": "eyJhbGciOiJFUzM4NCIsImtpZCI6ImtleS0yIn0",
"signature": "BwhP0F8Y6Z7QM3K8NmP1v2wX5aR9U0oD2cT6jE4sL3f..."
}
]
}
Use case: Multiple parties signing the same document (e.g., multi-party contracts).
In ANKASecure this shape is an output of a PKCS#7 SignedData conversion with several signers (PKCS#7 to JOSE Conversion), one entry per signer. The sign operations themselves write one entry, for a SIMPLE and a composite key alike: a composite key's two algorithms produce one composite signature, not two entries.
The crypto plane verifies and re-signs a JWS with several signatures when every signer is a key of the caller's tenant:
- Verify checks every signature under its own
kid, read from that entry's own protected header. The token is valid only if all signatures verify; there is no partial success. The response reports each signer insignatureResults[], in token order. - Re-sign verifies every existing signature first, so an invalid co-signature is never carried into the new token. It then replaces only the signature of the rotated key, named by
sourceKid, with one undernewKid, in the same position. The other entries and thepayloadare kept byte for byte. - Each signer needs its own capability grant:
VERIFYon every signer that is verified, andRESIGN(sourceKid → newKid)for the replaced one.
The root of a General JWS carries payload and signatures only (RFC 7515 §7.2.1). A root that also carries header, protected or signature is refused 422 with errorCode JWS_ROOT_MEMBER_FORBIDDEN before any authorization check.
Detached Content Mode (ANKASecure Extension)
Problem: Large files cannot be base64-encoded and embedded in JWS tokens.
Solution: Transmit signature separately from payload.
Format (JSON) — the flattened {"protected","signature"} body, captured from a real POST /api/v3/crypto/stream/sign round-trip with the my-rsa-3072-key RSA key; the signature value is cut after 64 characters (…, 512 characters in full):
{
"protected": "eyJhbGciOiJQUzI1NiIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il0sImtpZCI6Im15LXJzYS0zMDcyLWtleSIsInZlciI6IjIuMCIsImFua19rdiI6MSwidGlkIjoiN2QzYTljMWUtNGIyZi00ZThhLTljNzMtMjc1MDAwMDAwMDExIn0",
"signature": "p8dT_3fDTGUGf4ppiRwVkeTOi2sLjPw9r-hgbJn1VvLCYvvv9OPaUYTn_bLrL9V9…"
}
Decoded protected header:
{
"alg": "PS256",
"b64": false,
"crit": ["b64"],
"kid": "my-rsa-3072-key",
"ver": "2.0",
"ank_kv": 1,
"tid": "7d3a9c1e-4b2f-4e8a-9c73-275000000011"
}
Payload: Transmitted/stored separately (e.g., as a file, stream, or separate HTTP part).
Every key signs this shape. A detached JWS from a SIMPLE key and one from a composite key are both the flattened body above, and both protected headers carry b64: false and crit: ["b64"] (RFC 7797 §3, §6), so the signing input takes the payload octets unencoded. The two protected headers differ beyond alg: both carry kid, ank_kv and, when the local clock stamps it (the default), iat, plus exp when the request set validityDays; a SIMPLE header also carries ver and tid, and a composite header carries nothing else — alg, b64, crit, kid, ank_kv, iat and, when requested, exp, in that order. Neither time claim is listed in crit. A composite detached JWS has one signature value, the composite signature (see Composite signature algorithms). iat and exp in a JWS header are a platform convention rather than registered header parameters, so a generic JOSE library ignores them; see Timestamp Integrity — Composite Signatures*.
Verification:
- Reconstruct the signing input:
ASCII(BASE64URL(UTF8(protected))) || '.' || payload octets(RFC 7797 §3; the payload is not base64url-encoded) - Verify the signature against the reconstructed input
ANKASecure endpoints:
POST /api/v3/crypto/stream/sign→ Produces detached JWS JSONPOST /api/v3/crypto/stream/verify→ Verifies detached JWS against payload stream
Memory efficiency: O(1) constant memory (8 KiB buffer), and no API file-size limit; a proxy in front of the deployment may cap uploads; see Behind a buffering proxy.
See: Detached Signatures & Streaming Explained
Signature Algorithms in ANKASecure
ANKASecure supports both classical and post-quantum signature algorithms:
Classical:
- RSA-PSS (2048, 3072, 4096-bit) — every RSA key signs
PS256on the buffered and the streaming routes alike: RSASSA-PSS with SHA-256, MGF1 with SHA-256 and a 32-octet salt (RFC 7518 §3.5) - ECDSA (P-256, P-384, P-521, secp256k1, brainpoolP256r1, brainpoolP384r1, brainpoolP512r1 — brainpool signs under
ESB256/ESB384/ESB512, see SIMPLE brainpool ECDSA) - EdDSA (Ed25519, Ed448)
Post-Quantum (NIST FIPS 204):
- ML-DSA-44 (Dilithium2) - 128-bit security
- ML-DSA-65 (Dilithium3) - 192-bit security (RECOMMENDED)
- ML-DSA-87 (Dilithium5) - 256-bit security
Stateless Hash-Based (NIST FIPS 205):
- SLH-DSA-SHA2-128S (SPHINCS+ Small)
- SLH-DSA-SHA2-128F (SPHINCS+ Fast)
- SLH-DSA-SHA2-192S, SLH-DSA-SHA2-256S
Alternative PQC:
- FALCON-512, FALCON-1024 (compact signatures)
Composite (Hybrid):
- 14
COMPOSITE_SIGNATUREpairings of ML-DSA-44/65/87 with Ed25519, Ed448, ECDSA P-256/384/521 or ECDSA brainpoolP256r1/384r1/512r1 — for exampleEd25519+ML-DSA-65andP-256+ML-DSA-65 - Each signs one composite signature under a composite JWS
alg(see Composite signature algorithms) - 41 total composite combinations, 27 KEM and 14 signature (see Composite Algorithms Catalog)
JWE (JSON Web Encryption)
RFC 7516 - Encrypted content containers
Purpose
Provide confidentiality for data by encrypting payloads with hybrid encryption (key encapsulation + symmetric encryption).
Structure
JWE consists of five parts:
base64url(JOSE_Header) . base64url(Encrypted_Key) . base64url(IV) . base64url(Ciphertext) . base64url(Authentication_Tag)
Example (RSA-OAEP-512 with AES-GCM-256) — the first segment is the real base64url encoding of the protected header {"alg":"RSA-OAEP-512","enc":"A256GCM","kid":"my-rsa-key"}; the other four segments are placeholders, because a real Encrypted Key, IV, ciphertext and tag exist only for a real key and payload:
eyJhbGciOiJSU0EtT0FFUC01MTIiLCJlbmMiOiJBMjU2R0NNIiwia2lkIjoibXktcnNhLWtleSJ9
.
<Encrypted Key: the RSA-OAEP-512 wrap of the CEK, as long as the RSA modulus>
.
<IV: 96 bits>
.
<Ciphertext>
.
<Authentication Tag: 128 bits>
JOSE Header
{
"alg": "RSA-OAEP-512", // Key management algorithm
"enc": "A256GCM", // Content encryption algorithm
"kid": "my-rsa-key", // Key Identifier
"cty": "application/json" // Content Type (optional)
}
Key management algorithms (alg):
| Algorithm | Description | Security Level |
|---|---|---|
RSA-OAEP-512 | RSA-OAEP with SHA-512 and MGF1-SHA-512 — the only key-management algorithm ANKASecure emits and accepts for an RSA key | 112-bit (RSA-2048), 128-bit (RSA-3072) |
ECDH-ES+A256KW | ECDH-ES with AES Key Wrap — the algorithm ANKASecure emits and accepts for an elliptic-curve (P-256, P-384, P-521) key; Direct Key Agreement (ECDH-ES alone) is never used | 128-256 bit |
ECDH-ES+A256KW (OKP) | ECDH-ES with AES Key Wrap over an X25519 or X448 key (RFC 8037) — the algorithm ANKASecure emits and accepts for a simple XDH key, with an OKP epk and bound apu / apv (see XDH recipients) | 128-bit (X25519), 224-bit (X448) |
ML-KEM-768 | Post-quantum KEM (Kyber768) | 192-bit (NIST Level 3) |
ML-KEM-1024 | Post-quantum KEM (Kyber1024) | 256-bit (NIST Level 5) |
Content encryption algorithms (enc):
| Algorithm | Description | Security Level |
|---|---|---|
A128GCM | AES-GCM 128-bit | 128-bit |
A192GCM | AES-GCM 192-bit | 192-bit |
A256GCM | AES-GCM 256-bit (RECOMMENDED) | 256-bit |
A128CBC-HS256 | AES-CBC + HMAC-SHA256 | 128-bit |
CHACHA20-POLY1305 | ChaCha20 stream cipher | 256-bit |
CAMELLIA-256-GCM | Camellia-GCM (Japan/Saudi Arabia) | 256-bit |
Compact Serialization
Used for encrypted tokens, small payloads:
Characteristics:
- URL-safe
- Single recipient only
- Payload encrypted and embedded
- Practical size limit: ~64KB
JSON Serialization (Multiple Recipients)
Used when encrypting for multiple recipients with different keys:
{
"protected": "eyJlbmMiOiJBMjU2R0NNIn0",
"iv": "48V1_ALb6US04U3b",
"ciphertext": "5eym8TW_c8SuK0ltJ3rpYIzOeDQz7TALvtu6UG9oMo4vpzs9tX_EFShS8iB7j6jiSdiwkIr3ajwQzaBtQD_A",
"tag": "XFBoMYUZodetZdvTiFvSkQ",
"recipients": [
{
"header": {"alg": "RSA-OAEP-512", "kid": "recipient-1"},
"encrypted_key": "OKOawDo13gRp2ojaHV7LFpZcgV7T6DVZKTyKOMTYUmKoTCVJRgckCL9kiMT03JGe..."
},
{
"header": {"alg": "ECDH-ES+A256KW", "kid": "recipient-2"},
"encrypted_key": "a0JHVDQzN1RlM1N0RzBjUzN0WmJYWnBWM1JwWm1samRGMVNaV052Y..."
}
]
}
Use case: Encrypted email to multiple recipients, each with their own decryption key.
In ANKASecure this shape is an output of a PKCS#7 EnvelopedData conversion with several recipients (PKCS#7 to JOSE Conversion). The shared protected header carries ank_kvs, a map from each recipient kid to the material version it was wrapped under; the authentication tag covers it, so it cannot be altered without the decryption failing.
The crypto plane decrypts and re-encrypts a JWE with several recipients when every recipient is a key of the caller's tenant. Two shapes are told apart from the headers alone, before any key is looked up:
- The composite encryption of one composite key: a protected
kidand no recipientkid. It is handled as a composite key, as before. - A multi-key JWE: no protected
kid, and every recipient carries its ownkid.
The operations on a multi-key JWE follow three rules:
- Decrypt uses one recipient, which the caller names in
recipientKid. The member is required for a multi-key JWE and must name one of its recipients, or the request is refused400. The caller needs aDECRYPTgrant on that key alone. The platform never tries recipients one after another: without a grant on the named key the answer is403, and its body names no key. - Re-encrypt always draws a fresh content-encryption key, because every recipient of a JWE shares one (RFC 7516 §7.2.1): reusing it would let the replaced key still read the new ciphertext. It replaces only the rotated recipient, named by
sourceKid, withnewKidin the same position, and wraps the fresh key again to every kept recipient. The kept recipients are exactly the recipients the protectedank_kvsmap names; a recipient entry outside that set refuses the operation. The replaced recipient needsREENCRYPT(sourceKid → newKid), and each kept recipient needsREENCRYPT(sourceKid → kept)— noDECRYPTgrant is involved. - Each key needs its own grant. A grant that covers several of the keys is consumed once per request, not once per key.
Refusals made before any authorization check — from the headers, except the shared-kid row, which is decided on the key that kid resolves to in the caller's tenant:
| Condition | Status | errorCode |
|---|---|---|
A protected kid beside recipient kids (RFC 7516 §7.2.1 header disjointness) | 400 invalid-input | — |
A recipient without a kid | 422 | JWE_RECIPIENT_KID_MISSING |
A protected kid that does not resolve, in the caller's tenant, to a composite key whose components the recipients match | 422 | JWE_SHARED_KID_NOT_COMPOSITE |
A kid that repeats across entries | 422 | JOSE_ACTOR_KID_REPEATED |
A symmetric, direct or password-based alg (HS*, A*KW, PBES2-*, dir, ECDH-ES) on any actor | 422 | JOSE_SYMMETRIC_ACTOR |
More actors than ankasecure.crypto.jose.max-actors (default 16) | 422 | JOSE_ACTOR_LIMIT_EXCEEDED |
Re-encrypt: recipients that are not exactly the set the protected ank_kvs records | 422 | JWE_RECIPIENT_SET_UNPROTECTED |
A recipient kid of a multi-key JWE that resolves to a composite key is refused 422 with JWE_COMPOSITE_RECIPIENT when that key is resolved, after its own authorization check.
Multi-recipient encryption does not prove who produced the content
Every recipient of a JWE holds the content-encryption key once it has unwrapped its own entry, so any recipient can produce new content that decrypts correctly for all the others. Being able to decrypt a multi-recipient JWE says nothing about which of the recipients wrote it. Only a signature proves origin: sign the content, then encrypt it (a nested JWE(JWS)), and verify the inner signature after decrypting.
Members and Header Locations
Every JWE the platform reads — a buffered jweToken or a streaming JWET header, addressed to one key, to a composite key or to several keys — is judged against closed member sets where the body is bound, before any grant or lifecycle decision and before any key is used:
| Location | Admitted members | Any other member |
|---|---|---|
| Root | protected, recipients, iv, ciphertext, tag. On a streaming header also anka_composite_mode; there ciphertext is absent or null (it travels in the body) and tag is absent, null or the Base64URL tag | 422, JWE_ROOT_MEMBER_FORBIDDEN |
| Each recipient | header, encrypted_key | 422, JWE_RECIPIENT_MEMBER_FORBIDDEN |
RFC 7516 §7.2.1 allows a JSON aad member and a shared unprotected header. This platform reads neither, so both are refused rather than ignored: a member nobody reads is a part of the token nobody judged.
The header locations are disjoint, as §7.2.1 requires: "The Header Parameter names in the three locations MUST be disjoint." A name present both in protected and in a recipient header is refused whatever the two values. On the buffered routes that is 400 invalid-input for every shape; on the streaming routes it is 422 as a malformed header for a one-recipient JWE and 400 invalid-input for a composite JWE or a JWE addressed to several keys. An identical copy is not tolerated.
alg and kid therefore have one owner, read identically on both planes:
- One key: in
protected, which is what the platform writes — the one recipient carries itsencrypted_keyalone, and nothing at all foralgdir. A JWE whosealgandkidsit only in the recipientheader(the General layout a PKCS#7 conversion writes forserializationJSON) is also read, on the buffered and the streaming routes, after itskidpasses its grant and lifecycle decisions and only when thatalgis the key's own (422JWE_ALG_KEY_MISMATCHotherwise;422JWE_ALG_NOT_PROTECTEDwhen the key is symmetric or composite, whosealgmust be protected). Analgin neither location is refused. - A composite key:
kid,ank_kvandanka_kdfinprotected; each recipientheadercarries its componentalgonly. - Several keys: no
kidinprotected; each recipientheadercarries its ownalgandkid.
XDH recipients
A simple X25519 or X448 key is a key-agreement recipient under alg ECDH-ES+A256KW (RFC 8037 §3.2). The content encryption key is wrapped with AES Key Wrap under a key derived by the Concat KDF of RFC 7518 §4.6.2, over SHA-256, from the X25519 or X448 shared secret. The recipient's key-agreement members sit in the protected header when the JWE has one recipient (the compact form, and a JSON form with one recipient), and in that recipient's unprotected header when it has several (RFC 7516 §7.2.1):
| Member | Required | Value |
|---|---|---|
alg | yes | ECDH-ES+A256KW |
epk | yes | the ephemeral public key, {"kty":"OKP","crv":"X25519" \| "X448","x":"<base64url u>"} |
apv | yes | base64url of the recipient key's u-coordinate |
apu | no | absent, empty, or base64url of the ephemeral u-coordinate (the same bytes as epk.x) |
apv binds the recipient key into the derived key: a recipient that omits it, or whose apv is not the u-coordinate of the key its kid names, is refused as a malformed header before any key is derived. apu is optional, as RFC 7518 §4.6.1.2 allows: when it is absent or empty, the Concat KDF takes an empty PartyUInfo, and the ephemeral key is still bound through the shared secret, and through the protected header, which the content encryption authenticates, wherever epk sits in the protected header (one recipient). In a recipient's unprotected header (several recipients) the shared secret alone binds it. A present, non-empty apu other than epk.x is refused. ANKASecure itself always writes both. Each refusal is a malformed header (400 invalid-input on the buffered routes, 422 on the streaming routes) whose detail names the member. An ephemeral key whose u-coordinate is 0, 1 or a point of small order is refused with 400 invalid-input (RFC 7748 §6.1). A recipient that uses the key family's own name (alg X25519 or X448) on a simple key is refused with 422 JWE_KEY_TRANSPORT_RETIRED. A composite key's X25519 or X448 component recipient is not a simple recipient: it keeps its component alg.
An ECDH-ES+A256KW recipient of an EC key (P-256, P-384, P-521 or a Brainpool curve) carries an EC epk of the key's curve. An OKP epk, the X25519/X448 shape, or an EC epk of another curve, is refused as a malformed header naming epk.kty or epk.crv, before any key agreement.
Encrypting to an X25519 key with Nimbus JOSE+JWT. Nimbus's X25519Encrypter generates the ephemeral key inside encrypt, so the caller cannot set apu; it sets apv to the recipient's u-coordinate, which is the x member of the key's public JWK. Nimbus supports X25519 only (there is no X448 encrypter), and its X25519 support needs Google Tink on the classpath (checked on nimbus-jose-jwt 10.9.1).
OctetKeyPair recipient = OctetKeyPair.parse(publicJwkOfTheKey); // {"kty":"OKP","crv":"X25519","x":...}
JWEHeader header = new JWEHeader.Builder(JWEAlgorithm.ECDH_ES_A256KW, EncryptionMethod.A256GCM)
.keyID("acme-x25519-archive")
.agreementPartyVInfo(recipient.getX()) // apv = the recipient's u
.build();
JWEObject jwe = new JWEObject(header, new Payload(plaintextBytes));
jwe.encrypt(new X25519Encrypter(recipient.toPublicJWK()));
String compact = jwe.serialize();
Empty Encrypted Key (direct encryption)
With Direct Encryption (alg = dir) the key is used as the content encryption key and nothing is wrapped, and with Direct Key Agreement (alg = ECDH-ES) the content encryption key is derived; in both cases the JWE Encrypted Key is empty (RFC 7516 §5.1, step 5). The two serializations represent that differently:
- General JSON: the
encrypted_keymember is absent. RFC 7516 §7.2.1: "Theencrypted_keymember MUST be present and contain the value BASE64URL(JWE Encrypted Key) when the JWE Encrypted Key value is non-empty; otherwise, it MUST be absent." - Compact: the second segment is empty, so the token has two consecutive dots (RFC 7516 §7.1).
In ANKASecure every symmetric key (AES-GCM, ChaCha20-Poly1305, Camellia-GCM, ARIA-GCM, SEED-GCM) encrypts with alg = dir. No ANKASecure key uses ECDH-ES without key wrapping: EC keys use ECDH-ES+A256KW, whose encrypted key is non-empty. A symmetric-key JWE therefore looks like this on the wire, on the buffered and the streaming routes alike:
{
"protected": "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIiwia2lkIjoibXktYWVzLTI1Ni1rZXkiLCJhbmtfa3YiOjF9",
"recipients": [
{}
],
"iv": "48V1_ALb6US04U3b",
"ciphertext": "5eym8TW_c8SuK0ltJ3rpYIzO",
"tag": "XFBoMYUZodetZdvTiFvSkQ"
}
The protected header decodes to {"alg":"dir","enc":"A256GCM","kid":"my-aes-256-key","ank_kv":1}, so the one recipient carries nothing: it has no key to wrap, and a recipient header could only repeat a protected name, which Members and Header Locations refuses.
On input, a recipient without encrypted_key is accepted, and an empty-string encrypted_key is read as the same empty key; ANKASecure never emits the empty string. A recipient whose algorithm wraps or encapsulates a key (RSA-OAEP-512, ECDH-ES+A256KW, ML-KEM, every composite component) must carry a non-empty encrypted_key; without it the request is refused.
Detached Content Mode (ANKASecure Extension)
Problem: Large files encrypted with JWE create gigantic base64-encoded ciphertexts.
Solution: Transmit encrypted content separately from JWE header.
Format (Multipart):
Part 1 - JWET (JWE Token) Header (JSON):
{
"protected": "eyJhbGciOiJSU0EtT0FFUC01MTIiLCJlbmMiOiJBMjU2R0NNIiwia2lkIjoibXktcnNhLWtleSJ9",
"encrypted_key": "OKOawDo13gRp2ojaHV7LFpZcgV7T6DVZKTyKOMTYUmKoTCVJRgckCL9kiMT03JGe...",
"iv": "48V1_ALb6US04U3b",
"tag": "XFBoMYUZodetZdvTiFvSkQ"
}
Part 2 - Ciphertext Stream (binary, application/octet-stream):
ANKASecure endpoints:
POST /api/v3/crypto/stream/encrypt→ Produces multipart (JWET header + ciphertext stream)POST /api/v3/crypto/stream/decrypt→ Accepts multipart (JWET header + ciphertext stream)
Memory efficiency: O(1) constant memory (8 KiB buffer), and no API file-size limit; a proxy in front of the deployment may cap uploads; see Behind a buffering proxy.
See: Streaming Architecture Overview*
Encryption Algorithms in ANKASecure
Classical KEMs:
- RSA, as
RSA-OAEP-512(2048, 3072, 4096-bit) - Elliptic curve, as
ECDH-ES+A256KW(P-256, P-384, P-521) - XDH (X25519, X448), as
ECDH-ES+A256KWwith an OKPepk
Post-Quantum KEMs (NIST FIPS 203):
- ML-KEM-512 (Kyber512) - 128-bit security
- ML-KEM-768 (Kyber768) - 192-bit security (RECOMMENDED)
- ML-KEM-1024 (Kyber1024) - 256-bit security
Composite (Hybrid):
- RSA-2048 + ML-KEM-512
- ECDH-ES-P-256 + ML-KEM-768
- X25519 + ML-KEM-768
- 41 total composite combinations
Content Encryption:
- AES-GCM (128, 192, 256-bit)
- AES-CBC-HMAC (128, 192, 256-bit)
- ChaCha20-Poly1305
- Camellia-GCM (128, 192, 256-bit)
- ARIA-GCM (128, 192, 256-bit - Korea)
- SEED-GCM (128-bit - Korea)
JWT (JSON Web Token)
RFC 7519 - Compact tokens for authentication and authorization
Purpose
Represent claims (assertions) about an entity in a compact, URL-safe format suitable for HTTP headers, query parameters, and cookies.
Common use cases:
- Bearer tokens (OAuth 2.0)
- Identity tokens (OpenID Connect)
- API authentication
- Service-to-service authentication
Structure
JWT is a JWS Compact Serialization with a JSON payload containing claims:
Example:
eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImF1dGgta2V5In0
.
eyJpc3MiOiJodHRwczovL2F1dGguYW5rYXRlY2guY28iLCJzdWIiOiJ1c2VyMTIzIiwiYXVkIjoiaHR0cHM6Ly9hcGkuYW5rYXRlY2guY28iLCJleHAiOjE3MzcwNDgwMDAsIm5iZiI6MTczNzA0NDQwMCwiaWF0IjoxNzM3MDQ0NDAwLCJqdGkiOiJ0b2tlbi11dWlkLTEyMyIsInRlbmFudElkIjoidGVuYW50LWEiLCJzY29wZXMiOlsiY3J5cHRvOmVuY3J5cHQiLCJjcnlwdG86c2lnbiJdfQ
.
dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk...
Standard Claims (RFC 7519)
Registered claims (defined by RFC 7519):
| Claim | Name | Type | Purpose |
|---|---|---|---|
iss | Issuer | String | Who issued the token (e.g., https://auth.ankatech.co) |
sub | Subject | String | Who the token is about (e.g., user ID) |
aud | Audience | String or Array | Who should accept the token (e.g., https://api.ankatech.co) |
exp | Expiration Time | NumericDate | When token expires (Unix timestamp) |
nbf | Not Before | NumericDate | When token becomes valid (Unix timestamp) |
iat | Issued At | NumericDate | When token was issued (Unix timestamp) |
jti | JWT ID | String | Unique token identifier (for revocation tracking) |
NumericDate: Seconds since Unix epoch (1970-01-01T00:00:00Z).
ANKASecure Custom Claims
ANKASecure extends JWT with custom claims for multi-tenant authorization:
{
"iss": "https://auth.ankatech.co",
"aud": "https://api.ankatech.co",
"sub": "user-123",
"exp": 1737048000,
"nbf": 1737044400,
"iat": 1737044400,
"jti": "token-uuid-abc123",
// ANKASecure custom claims
"tenantId": "tenant-a", // Tenant isolation
"userId": "user-123", // User identifier
"appId": "app-crypto-client", // Application identifier
"scopes": [ // Granted permissions (dotted, never colon-separated)
"secure.encrypt",
"secure.decrypt",
"secure.sign",
"secure.verifySignature",
"admin.keys.read"
],
"roles": ["TENANT_KEY_MANAGER"], // Composite roles, with no ROLE_ prefix
"email": "[email protected]", // User email (optional)
"name": "John Doe" // User display name (optional)
}
JWT Validation in ANKASecure
All ANKASecure APIs validate JWTs with mandatory checks:
- Signature verification: Valid signature from trusted issuer
- Issuer (
iss): Matches configured trusted issuers (multi-issuer support) - Audience (
aud): Matches service audience (e.g.,https://api.ankatech.co) - Expiration (
exp): Token not expired (exp > current_time) - Not Before (
nbf): Token already valid (nbf <= current_time) - Tenant claim (
tenantId): Present and non-empty - Scopes: Required scopes for operation present in
scopesarray
Validation implemented in: ankasecure-common-security library (shared across all services).
See: Authentication System*
Nested Tokens (Sign-Then-Encrypt)
Purpose
Provide both integrity/authenticity (signature) and confidentiality (encryption) for the same payload.
Use cases:
- Confidential signed documents
- Encrypted audit logs with integrity
- Privacy-preserving authenticated data
Architecture
Recipient workflow:
Nested Token (JWE)
→ Decrypt (get JWS)
→ Verify signature (get original payload)
→ Authenticated and confidential payload
Format
Outer layer (JWE header):
{
"alg": "RSA-OAEP-512",
"enc": "A256GCM",
"kid": "recipient-key",
"cty": "JWT" // Content Type = JWT (indicates nested JWS)
}
Inner layer (JWS, encrypted):
Full nested token:
Compact Nested Token Example
The first segment is the real base64url encoding of the outer header above ({"alg":"RSA-OAEP-512","enc":"A256GCM","kid":"recipient-key","cty":"JWT"}); the other four are placeholders:
eyJhbGciOiJSU0EtT0FFUC01MTIiLCJlbmMiOiJBMjU2R0NNIiwia2lkIjoicmVjaXBpZW50LWtleSIsImN0eSI6IkpXVCJ9
.
<Encrypted Key: the RSA-OAEP-512 wrap of the CEK>
.
<IV: 96 bits>
.
<Ciphertext: the encrypted inner JWS>
.
<Authentication Tag: 128 bits>
Decrypted content (inner JWS):
eyJhbGciOiJQUzI1NiIsImtpZCI6InNpZ25lci1rZXkifQ
.
eyJkYXRhIjoidGhpcyBpcyBteSBwYXlsb2FkIn0
.
dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk...
ANKASecure Support
Compact mode (≤5MB):
POST /api/v3/crypto/sign-encrypt
Request:
{
"data": "base64-encoded payload",
"signKid": "my-signing-key",
"encryptKid": "recipient-key"
}
Response (abridged):
{
"jweToken": { "protected": "...", "recipients": [ ... ], "iv": "...", "ciphertext": "...", "tag": "..." },
"signAlgorithmUsed": "ML-DSA-65",
"encryptionAlgorithmUsed": "ML-KEM-768"
}
See: Flow 17 - Sign-Then-Encrypt Nested Ops
A nested token converted from a PKCS#7 SignedAndEnvelopedData can carry several recipients outside and several signers inside. Decrypt-and-verify opens it with the recipient the caller names in recipientKid and then verifies every inner signature, each under its own grant; the result is valid only if all of them verify. The decrypting recipient must not also be one of the inner signers.
Security Considerations
Order matters:
- ✅ Sign-Then-Encrypt (recommended): Signature protected by encryption, no metadata leakage
- ⚠️ Encrypt-Then-Sign: Signature visible, reveals metadata (signer identity, algorithm)
ANKASecure default: Sign-Then-Encrypt (JWE wrapping JWS).
ANKASecure Extensions
1. Post-Quantum Algorithm Support
ANKASecure extends JWA (RFC 7518) with NIST-standardized post-quantum algorithms:
New alg values:
ML-KEM-512,ML-KEM-768,ML-KEM-1024(FIPS 203 - Key Encapsulation)ML-DSA-44,ML-DSA-65,ML-DSA-87(FIPS 204 - Digital Signatures)SLH-DSA-SHA2-128S,SLH-DSA-SHA2-192S,SLH-DSA-SHA2-256S(FIPS 205 - Hash-Based Signatures)FALCON-512,FALCON-1024(Compact signatures)
Example JWS header (ML-DSA-65):
Example JWE header (ML-KEM-768):
2. Composite (Hybrid) Algorithm Identifiers
ANKASecure names each composite pairing by its two components; the construction corresponds to the IETF composite Internet-Drafts (draft-ietf-lamps-pq-composite-kem-21, draft-ietf-lamps-pq-composite-sigs-19):
Format: <Classical>+<PostQuantum>
Examples:
X25519+ML-KEM-512- X25519 + ML-KEM-512 (128-bit security)P-256+ML-KEM-768- P-256 (as ECDH-KEM) + ML-KEM-768 (L1: the pairing level is the weakest component's)X25519+ML-KEM-768- X25519 + ML-KEM-768 (L1: the pairing level is the weakest component's)P-256+ML-DSA-65- ECDSA-P-256 + ML-DSA-65 (L1: the pairing level is the weakest component's)Ed25519+ML-DSA-44- Ed25519 + ML-DSA-44 (128-bit security)
JWE protected header (composite KEM) — the pairing name (P-384+ML-KEM-768) is the key's algorithm, not a header value:
{
"enc": "A256GCM",
"kid": "my-hybrid-key",
"anka_kdf": "HKDF-SHA256" // the key's KDF token (HKDF-SHA256 | HKDF-SHA384 | HKDF-SHA512 | CatKDF | CasKDF), protected
}
There is no alg in the protected header: each of the two recipient headers carries only its component alg (P-384, then ML-KEM-768), as RFC 7516 §7.2.1 requires the header locations to be disjoint.
JWS protected header (composite signature) — the header carries the composite JOSE alg of the pairing, never the catalogue pairing name. Decoded from a POST /api/v3/crypto/resign response whose target key invoice-signer-2026 is a P-256+ML-DSA-44 key:
iat is stamped by the local clock (present by default); exp follows it only on the operations that carry validityDays (/sign, /sign-encrypt, /stream/sign-encrypt, /stream/resign), which /resign does not. See Timestamp Integrity — Composite Signatures*.
A composite JWS carries one signature: the composite signature value over the JWS Signing Input, computed as the next section describes. There is no second signature and no unprotected anka_* member.
Composite signature algorithms
Every COMPOSITE_SIGNATURE pairing signs with the composite ML-DSA construction of draft-ietf-lamps-pq-composite-sigs-19 §3.2, as draft-ietf-jose-pq-composite-sigs-04 §4.2 applies it to JOSE. M is the RFC 7515 §5.1 JWS Signing Input:
M = ASCII(BASE64URL(UTF8(protected))) || '.' || BASE64URL(payload)
M' = Prefix || Label || 0x00 || PH(M) # Prefix = "CompositeAlgorithmSignatures2025"
signature = ML-DSA.Sign(M', ctx = Label) || Trad.Sign(M')
The ML-DSA component comes first; an ECDSA component is a DER Ecdsa-Sig-Value (JOSE draft §4.5.1); the signature is valid only if both components verify (JOSE draft §4.3). The Label and the pre-hash PH are fixed per pairing:
Pairing (key alg) | Composite JWS alg | Label | PH | Standing of the name |
|---|---|---|---|---|
Ed25519+ML-DSA-44 | ML-DSA-44-Ed25519 | COMPSIG-MLDSA44-Ed25519-SHA512 | SHA-512 | JOSE draft Table 5 |
Ed25519+ML-DSA-65 | ML-DSA-65-Ed25519 | COMPSIG-MLDSA65-Ed25519-SHA512 | SHA-512 | JOSE draft Table 5 |
Ed448+ML-DSA-87 | ML-DSA-87-Ed448 | COMPSIG-MLDSA87-Ed448-SHAKE256 | SHAKE256 (64 octets) | JOSE draft Table 5 |
P-256+ML-DSA-44 | ML-DSA-44-ES256 | COMPSIG-MLDSA44-ECDSA-P256-SHA256 | SHA-256 | JOSE draft Table 5 |
P-256+ML-DSA-65 | ML-DSA-65-ES256 | COMPSIG-MLDSA65-ECDSA-P256-SHA512 | SHA-512 | JOSE draft Table 5 |
P-384+ML-DSA-87 | ML-DSA-87-ES384 | COMPSIG-MLDSA87-ECDSA-P384-SHA512 | SHA-512 | JOSE draft Table 5 |
P-384+ML-DSA-65 | ML-DSA-65-ES384 | COMPSIG-MLDSA65-ECDSA-P384-SHA512 | SHA-512 | LAMPS §6 algorithm; JOSE name derived, unregistered |
P-521+ML-DSA-87 | ML-DSA-87-ES512 | COMPSIG-MLDSA87-ECDSA-P521-SHA512 | SHA-512 | LAMPS §6 algorithm; JOSE name derived, unregistered |
brainpoolP256r1+ML-DSA-65 | ML-DSA-65-ESB256 | COMPSIG-MLDSA65-ECDSA-BP256-SHA512 | SHA-512 | LAMPS §6 algorithm; JOSE name derived, unregistered |
brainpoolP384r1+ML-DSA-87 | ML-DSA-87-ESB384 | COMPSIG-MLDSA87-ECDSA-BP384-SHA512 | SHA-512 | LAMPS §6 algorithm; JOSE name derived, unregistered |
Ed448+ML-DSA-65 | https://ankatech.co/jose/alg/ML-DSA-65-Ed448 | ANKATECH-COMPSIG-MLDSA65-Ed448-SHAKE256 | SHAKE256 (64 octets) | ANKATech-defined |
P-384+ML-DSA-44 | https://ankatech.co/jose/alg/ML-DSA-44-ES384 | ANKATECH-COMPSIG-MLDSA44-ECDSA-P384-SHA512 | SHA-512 | ANKATech-defined |
P-521+ML-DSA-65 | https://ankatech.co/jose/alg/ML-DSA-65-ES512 | ANKATECH-COMPSIG-MLDSA65-ECDSA-P521-SHA512 | SHA-512 | ANKATech-defined |
brainpoolP512r1+ML-DSA-87 | https://ankatech.co/jose/alg/ML-DSA-87-ESB512 | ANKATECH-COMPSIG-MLDSA87-ECDSA-BP512-SHA512 | SHA-512 | ANKATech-defined |
What each group claims, and what it does not:
- The 6 JOSE draft pairings carry the
algnames draft-ietf-jose-pq-composite-sigs-04 defines in its Table 5, with the Labels of its Table 7. A JOSE library that implements that draft knows these names. - The 4 LAMPS pairings are algorithms draft-ietf-lamps-pq-composite-sigs-19 defines in its §6, with their IETF Labels. The JOSE draft gives them no
alg, so their names apply the naming rule of its Table 5 (ML-DSA-<n>-<JOSE name of the traditional algorithm>). These four names, and the brainpool namesESB256/ESB384they build on, are not registered in JOSE. - The 4 ANKATech pairings have no IETF definition. They use the same construction with an ANKATech-defined Label outside the IETF
COMPSIG-namespace, and eachalgis a collision-resistant name under ANKATech's domain, as RFC 7515 §4.1.1 asks of an unregisteredalg. No IETF claim is made for them.
The IANA JOSE registry holds no composite signature algorithm and no brainpool algorithm.
Test-vector conformance
What the platform's construction has been checked against, and nothing more. Every statement below is the result of a Core API known-answer test run on the construction; none is a certification.
- JOSE draft examples (the 6 JOSE draft pairings). The construction verifies all six JWS examples of draft-ietf-jose-pq-composite-sigs-04 Appendix A.1 (
ML-DSA-44-ES256,ML-DSA-44-Ed25519,ML-DSA-65-ES256,ML-DSA-65-Ed25519,ML-DSA-87-ES384,ML-DSA-87-Ed448), with keys derived from the printed seeds, and its M' equals each example's printed message representative. Each example with one octet flipped in either component is refused. - LAMPS test vectors (the 4 LAMPS pairings). The construction verifies the draft-ietf-lamps-pq-composite-sigs-19 Appendix E vectors of the four pairings the JOSE draft does not name (
P-384+ML-DSA-65,P-521+ML-DSA-87,brainpoolP256r1+ML-DSA-65,brainpoolP384r1+ML-DSA-87), and refuses each with one flipped octet per component. - Message representative layout. M' equals the LAMPS Appendix D example byte for byte. Appendix D prints one pairing,
id-MLDSA65-ECDSA-P256-SHA512(P-256+ML-DSA-65); its second example, with an 8-octet application context, does not apply to JOSE, which fixes the context to the empty string. - An independent implementation. For each of the 10 IETF-labelled pairings, on the buffered and the detached plane, a signature the platform produces verifies under Bouncy Castle's LAMPS composite signature provider, and a signature that provider produces verifies under the platform's construction (40 cross-checks, all passing).
The claim stops at those pairings. An IETF conformance statement is made only for the 10 IETF-labelled pairings, and a JOSE conformance statement only for the 6 names of the JOSE draft's Table 5; the 4 LAMPS-derived JOSE names are unregistered, so no JOSE conformance is claimed for them. The 4 ANKATech-defined pairings have no external test vectors: no standard defines them, so they are checked only against the platform's own pinned M' values, sign-and-verify round trips and per-component verification, and no conformance of any kind is claimed for them.
Passing these vectors does not change the properties the drafts themselves state: the construction is not strongly unforgeable when a component is ECDSA, it gives weak rather than strong non-separability (JOSE draft §6.3), and against a quantum adversary the classical component adds no security. The Bouncy Castle provider is not a FIPS 140-3 validated module.
Assembling the composite public key
The platform publishes a composite key's component public keys; it does not export a composite (AKP) JWK. A relying party that verifies with a library implementing draft-ietf-jose-pq-composite-sigs-04 assembles the composite public key itself, per the draft's SerializePublicKey (§3, §4.1): the ML-DSA raw public key followed by the classical public key, which is the X9.62 uncompressed point (leading 0x04) for an ECDSA component, or the raw 32-octet Ed25519 / 57-octet Ed448 key for an EdDSA component.
SIMPLE brainpool ECDSA: ESB256, ESB384, ESB512
A SIMPLE (single-algorithm) brainpool ECDSA key signs under ESB256, ESB384 or ESB512 — the fully-specified identifiers RFC 9864 §2.1 defines for ECDSA on brainpoolP256r1 with SHA-256, brainpoolP384r1 with SHA-384 and brainpoolP512r1 with SHA-512. RFC 9864 registers them in the COSE registry only; their use in JOSE is not registered. ES256 / ES384 / ES512 name the P-curves alone (RFC 7518 §3.4), so a brainpool key never signs under them.
A SIMPLE ECDSA signature is the fixed-length concatenation of R and S:
JWS alg | Curve | Signature length |
|---|---|---|
ES256 | P-256 | 64 octets |
ES384 | P-384 | 96 octets |
ES512 | P-521 | 132 octets |
ESB256 | brainpoolP256r1 | 64 octets |
ESB384 | brainpoolP384r1 | 96 octets |
ESB512 | brainpoolP512r1 | 128 octets (RFC 5639 §3.7: a 512-bit field) |
Every SIMPLE ECDSA verifier checks that the protected alg is the key's own before verifying, and refuses a signature of any other length.
See:
- Composite Hybrid Keys Architecture*
- IETF Composite ML-KEM and Composite Signatures — Alignment
- Flow 18 - Composite Hybrid Keys Usage
3. Flavor Dispatching (Internal Header)
ANKASecure adds internal metadata for routing cryptographic operations:
Custom header field: x-anka-flavor
Values:
simple- Single algorithm (symmetric, asymmetric)composite- Composite hybrid key (classical + post-quantum)
Example (internal processing):
{
"alg": "P-384+ML-KEM-768",
"enc": "A256GCM",
"kid": "my-hybrid-key",
"x-anka-flavor": "composite" // Internal routing
}
Purpose: Route requests to appropriate service layer (SymmetricCryptoService, AsymmetricCryptoService, CompositeCryptoService).
See: Flavor Dispatching*
4. KID-to-UUID Mapping
ANKASecure internally maps public KIDs to internal UUIDs for tenant isolation:
External API (client-visible KID):
Internal mapping (server-side):
kid: "my-encryption-key"
→ uuid: "550e8400-e29b-41d4-a716-446655440000"
→ tenant: "tenant-a"
→ algorithm: "P-384+ML-KEM-768"
→ keystore: "/srv/anka-secure/environments/prod/volumes/keystores/tenant-a/my-encryption-key.p12"
Purpose:
- Tenant isolation (same KID can exist in multiple tenants)
- Algorithm abstraction (KID doesn't reveal algorithm)
- Crypto-agility (change algorithm without changing KID)
See: KID-to-UUID Mapping*
5. Detached Content for Streaming
ANKASecure extends both JWS and JWE with detached content mode for large files:
JWS Detached (JSON format) — the flattened body with RFC 7797 b64: false and crit: ["b64"] in the protected header, for every key (captured, signature cut after 64 characters; see Detached Content Mode):
{
"protected": "eyJhbGciOiJQUzI1NiIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il0sImtpZCI6Im15LXJzYS0zMDcyLWtleSIsInZlciI6IjIuMCIsImFua19rdiI6MSwidGlkIjoiN2QzYTljMWUtNGIyZi00ZThhLTljNzMtMjc1MDAwMDAwMDExIn0",
"signature": "p8dT_3fDTGUGf4ppiRwVkeTOi2sLjPw9r-hgbJn1VvLCYvvv9OPaUYTn_bLrL9V9…"
}
JWE Detached (Multipart format):
- Part 1: JWET header (JSON)
- Part 2: Ciphertext stream (binary)
Memory model: O(1) constant memory (8 KiB single-pass, 2 MiB dual-thread).
Endpoints:
/api/v3/crypto/stream/sign,/api/v3/crypto/stream/verify/api/v3/crypto/stream/encrypt,/api/v3/crypto/stream/decrypt/api/v3/crypto/stream/resign,/api/v3/crypto/stream/reencrypt
See: Streaming Architecture Overview*
6. PKCS#7 Interoperability
ANKASecure provides conversion from legacy PKCS#7/CMS to JOSE format:
Supported conversions — every signer and every recipient of the input is carried:
- PKCS#7 SignedData (N signers) → JWS
- PKCS#7 EnvelopedData (M recipients) → JWE
- PKCS#7 SignedAndEnvelopedData → JWE whose plaintext is a JWS (nested)
Every actor must be a key of the tenant, so every result can be decrypted, verified, re-encrypted and re-signed on the crypto plane under the rules in JSON Serialization (Multiple Signatures) and JSON Serialization (Multiple Recipients). A file whose signer or recipient is known only by a certificate is not convertible; see PKCS#7 to JOSE Conversion.
Buffered route — POST /api/v3/migration/convert-pkcs7-to-jose, a JSON body:
{
"pkcs7Data": "MIIGHgYJKoZIhvcNAQcCoIIGDzCCBgsCAQAxggE...",
"decryptionKid": "legacy-recipient-key",
"serialization": "AUTO",
"targetFormat": "AUTO",
"algorithmPolicy": { "allowLegacyHash": false }
}
Only pkcs7Data (the Base64 DER structure) is required. decryptionKid is optional and read only for an encrypted structure; serialization is AUTO, COMPACT or JSON; targetFormat is AUTO, JWS, JWE or JWE_JWS_NESTED. Any other member, at the top level or inside algorithmPolicy, is refused with 400 naming it. The response carries joseToken, serialization, structure, decision, signers, recipients and algorithmTransformations.
Streaming route — POST /api/v3/migration/stream/convert-pkcs7-to-jose, multipart/form-data with a metadata part (the same JSON members, without pkcs7Data) followed by a file part (the binary PKCS#7). It streams the JOSE output and returns no per-actor metadata.
The request members, the response members, every refusal and both routes' abort contracts are in PKCS#7 to JOSE Conversion.
See: PKCS#7 to JOSE Conversion
RFC References
Core JOSE Standards
| RFC | Title | URL |
|---|---|---|
| RFC 7515 | JSON Web Signature (JWS) | https://datatracker.ietf.org/doc/html/rfc7515 |
| RFC 7516 | JSON Web Encryption (JWE) | https://datatracker.ietf.org/doc/html/rfc7516 |
| RFC 7518 | JSON Web Algorithms (JWA) | https://datatracker.ietf.org/doc/html/rfc7518 |
| RFC 7519 | JSON Web Token (JWT) | https://datatracker.ietf.org/doc/html/rfc7519 |
Related Standards
| RFC | Title | Relevance |
|---|---|---|
| RFC 8037 | CFRG Elliptic Curve Signatures and Key Agreement | EdDSA, X25519, X448 algorithms |
| RFC 8725 | JSON Web Token Best Current Practices | Security recommendations |
| RFC 7807 | Problem Details for HTTP APIs | Error responses |
NIST Post-Quantum Standards
| FIPS | Title | Relevance |
|---|---|---|
| FIPS 203 | Module-Lattice-Based Key-Encapsulation Mechanism | ML-KEM (Kyber) |
| FIPS 204 | Module-Lattice-Based Digital Signature Algorithm | ML-DSA (Dilithium) |
| FIPS 205 | Stateless Hash-Based Digital Signature Algorithm | SLH-DSA (SPHINCS+) |
IETF Drafts (ANKASecure Implementation)
| Draft | Title | Relevance |
|---|---|---|
| draft-ietf-lamps-pq-composite-kem-21 | Composite ML-KEM for use in X.509 Public Key Infrastructure | Composite key architecture; the component pairs of 8 of its 12 composites |
| draft-ietf-lamps-pq-composite-sigs-19 | Composite Module-Lattice-Based Digital Signature Algorithm (ML-DSA) for use in X.509 Public Key Infrastructure | The composite signature construction of every COMPOSITE_SIGNATURE pairing; the Labels of 10 of them |
| draft-ietf-jose-pq-composite-sigs-04 | PQ/T Hybrid Composite Signatures for JOSE and COSE | M = the JWS Signing Input, the DER ECDSA component, the alg names of 6 pairings |
| RFC 7797 | JWS Unencoded Payload Option | b64:false on detached and streaming composite signatures |
| RFC 9864 | Fully-Specified Algorithms for JOSE and COSE | ESB256 / ESB384 / ESB512 for SIMPLE brainpool ECDSA (COSE-registered only) |
Quick Reference
Algorithm Selection Matrix
| Security Level | Signature | Key Encapsulation | Content Encryption |
|---|---|---|---|
| 128-bit (NIST-1) | ML-DSA-44 | ML-KEM-512 | AES-GCM-128 |
| 192-bit (NIST-3) | ML-DSA-65 | ML-KEM-768 | AES-GCM-192 |
| 256-bit (NIST-5) | ML-DSA-87 | ML-KEM-1024 | AES-GCM-256 |
Recommended for production: 192-bit security level (ML-DSA-65 + ML-KEM-768 + AES-GCM-256).
Common Operations
| Operation | Endpoint | Input Format | Output Format |
|---|---|---|---|
| Sign (compact) | POST /api/v3/crypto/sign | JSON payload | JWS compact |
| Sign (streaming) | POST /api/v3/crypto/stream/sign | Multipart stream | JWS JSON (detached) |
| Encrypt (compact) | POST /api/v3/crypto/encrypt | JSON payload | JWE compact |
| Encrypt (streaming) | POST /api/v3/crypto/stream/encrypt | Stream | Multipart (JWET + ciphertext) |
| Verify JWT | POST /api/v3/crypto/verify | JWS compact | Validation result |
| Decrypt JWT | POST /api/v3/crypto/decrypt | JWE compact | Plaintext payload |
| Re-encrypt | POST /api/v3/crypto/reencrypt | Old JWE | New JWE (new algorithm) |
| Re-sign | POST /api/v3/crypto/resign | Old JWS | New JWS (new algorithm) |
Size Limits
| Mode | Maximum Payload Size | Memory Usage |
|---|---|---|
| Compact | 5 MB | O(n) - full payload in memory |
| Streaming (detached) | No API limit | O(1) - constant 8 KiB / 2 MiB |
"No API limit" is the API's own bound; a proxy in front of the deployment may cap uploads; see Behind a buffering proxy.
Recommendation: Use streaming endpoints for files >1 MB.
Related Documentation
Foundational Concepts
Architecture
- Authentication System*
- Composite Keys Architecture*
- PQC Transport Layer*
Implementation
- Core API Endpoints*
- Streaming Architecture*
- Composite Keys API*
SDK Examples
- Flow 1 - Asymmetric Encrypt/Decrypt (Streaming)
- Flow 2 - Detached-JWS Sign/Verify (Streaming)
- Flow 14 - In-memory JWE/JWS Quick-start
- Flow 17 - Sign-Then-Encrypt Nested Ops
- Flow 18 - Composite Hybrid Keys Usage
Compliance & Standards
- IETF Composite ML-KEM and Composite Signatures — Alignment
- FIPS 140-3, Random Bit Generation and Approved Algorithms
- Standards & Regulatory Alignment
Strategic Context
- From PKCS#7 to JWS/JWE: The Evolution of API Security*
Document Status: ✅ ACTIVE
Last Updated: January 2025
Document Location: 02-concepts/jose-technical-reference.md
Target Audience: Developers, Solution Architects, Integration Engineers
Feedback: For corrections or additions, contact the documentation team.