Skip to content

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

  1. What is JOSE?
  2. JWS (JSON Web Signature)
  3. JWE (JSON Web Encryption)
  4. JWT (JSON Web Token)
  5. Nested Tokens (Sign-Then-Encrypt)
  6. ANKASecure Extensions
  7. 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:

base64url(JOSE_Header) . base64url(Payload) . base64url(Signature)

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.
  • crit must be integrity protected, so it may appear only in a protected header. A crit member in a per-signature or per-recipient header, in the shared JWS header, or in a detached (streaming) JWE header outside protected — its root, its unprotected member, 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 unprotected header or a JSON aad member is refused with 422 JWE_ROOT_MEMBER_FORBIDDEN before any key is used, so a crit inside unprotected is 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: any crit entry is refused with 400. 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 with 400. A detached JWE header whose crit is misplaced is refused with 422, the streaming plane's malformed-header status.
  • A refused header is never read as a missing kid. On /reencrypt and /resign, sourceKid names the source key when the header carries no kid; it does not turn a crit refusal 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:

Header.Payload.Signature

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 in signatureResults[], 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 under newKid, in the same position. The other entries and the payload are kept byte for byte.
  • Each signer needs its own capability grant: VERIFY on every signer that is verified, and RESIGN(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:

  1. Reconstruct the signing input: ASCII(BASE64URL(UTF8(protected))) || '.' || payload octets (RFC 7797 §3; the payload is not base64url-encoded)
  2. Verify the signature against the reconstructed input

ANKASecure endpoints:

  • POST /api/v3/crypto/stream/sign → Produces detached JWS JSON
  • POST /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 PS256 on 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_SIGNATURE pairings of ML-DSA-44/65/87 with Ed25519, Ed448, ECDSA P-256/384/521 or ECDSA brainpoolP256r1/384r1/512r1 — for example Ed25519+ML-DSA-65 and P-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:

Header.EncryptedKey.IV.Ciphertext.Tag

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 kid and no recipient kid. It is handled as a composite key, as before.
  • A multi-key JWE: no protected kid, and every recipient carries its own kid.

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 refused 400. The caller needs a DECRYPT grant on that key alone. The platform never tries recipients one after another: without a grant on the named key the answer is 403, 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, with newKid in the same position, and wraps the fresh key again to every kept recipient. The kept recipients are exactly the recipients the protected ank_kvs map names; a recipient entry outside that set refuses the operation. The replaced recipient needs REENCRYPT(sourceKid → newKid), and each kept recipient needs REENCRYPT(sourceKid → kept) — no DECRYPT grant 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 its encrypted_key alone, and nothing at all for alg dir. A JWE whose alg and kid sit only in the recipient header (the General layout a PKCS#7 conversion writes for serialization JSON) is also read, on the buffered and the streaming routes, after its kid passes its grant and lifecycle decisions and only when that alg is the key's own (422 JWE_ALG_KEY_MISMATCH otherwise; 422 JWE_ALG_NOT_PROTECTED when the key is symmetric or composite, whose alg must be protected). An alg in neither location is refused.
  • A composite key: kid, ank_kv and anka_kdf in protected; each recipient header carries its component alg only.
  • Several keys: no kid in protected; each recipient header carries its own alg and kid.

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_key member is absent. RFC 7516 §7.2.1: "The encrypted_key member 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):

[raw binary ciphertext, not base64-encoded]

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+A256KW with an OKP epk

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:

base64url(JOSE_Header) . base64url(Claims) . base64url(Signature)

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:

  1. Signature verification: Valid signature from trusted issuer
  2. Issuer (iss): Matches configured trusted issuers (multi-issuer support)
  3. Audience (aud): Matches service audience (e.g., https://api.ankatech.co)
  4. Expiration (exp): Token not expired (exp > current_time)
  5. Not Before (nbf): Token already valid (nbf <= current_time)
  6. Tenant claim (tenantId): Present and non-empty
  7. Scopes: Required scopes for operation present in scopes array

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

Original Payload
  → Sign (JWS)
    → Encrypt the JWS (JWE wrapping JWS)
      → Final nested token

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):

eyJhbGciOiJQUzI1NiIsImtpZCI6InNpZ25lci1rZXkifQ.eyJkYXRhIjoidGhpcyBpcyBteSBwYXlsb2FkIn0.dBjftJeZ...

Full nested token:

[JWE_Header].[Encrypted_Key].[IV].[Encrypted_JWS_Token].[Tag]

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):

{
  "alg": "ML-DSA-65",
  "kid": "my-pqc-key",
  "typ": "JWT"
}

Example JWE header (ML-KEM-768):

{
  "alg": "ML-KEM-768",
  "enc": "A256GCM",
  "kid": "my-pqc-key"
}

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:

{
  "alg": "ML-DSA-44-ES256",
  "kid": "invoice-signer-2026",
  "ank_kv": 1,
  "iat": 1790647106
}

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 alg names 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 names ESB256 / ESB384 they 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 each alg is a collision-resistant name under ANKATech's domain, as RFC 7515 §4.1.1 asks of an unregistered alg. 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.

composite public key = ML-DSA public key || classical public key

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:

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):

{
  "kid": "my-encryption-key"
}

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
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.


Foundational Concepts

Architecture

  • Authentication System*
  • Composite Keys Architecture*
  • PQC Transport Layer*

Implementation

  • Core API Endpoints*
  • Streaming Architecture*
  • Composite Keys API*

SDK Examples

Compliance & Standards

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.

* restricted content — see higher-tier documentation