PKCS#7 to JOSE Format Conversion
Purpose: Converting legacy PKCS#7/CMS containers to modern JOSE format
Version: 3.0.0
Overview
The PKCS#7 to JOSE conversion feature enables migration of legacy S/MIME and other PKCS#7-based systems to ANKASecure's modern JOSE-based format.
What this conversion does:
- Container transformation: PKCS#7 container → JWE/JWS container, with the same keys — each actor's imported tenant key, matched by the issuer and serial number of its certificate
- Signatures are produced again, not translated: each signer's content is re-signed under the JWS
algits matched key signs with (PS256for RSA;ES256/ES384/ES512orESB256/ESB384/ESB512for EC, by curve) - Encrypted content is decrypted and encrypted again: the content is always re-encrypted under
A256GCM, whatever cipher the PKCS#7 used, and a new content-encryption key is wrapped to every recipient under the key-managementalgits matched key wraps with (RSA-OAEP-512for RSA)
What this conversion does NOT do:
- ❌ Does NOT convert to PQC algorithms (ML-KEM, ML-DSA)
- ❌ Does NOT change RSA/ECDSA keys
- ❌ Does NOT re-encrypt with quantum-resistant algorithms
For PQC conversion: After PKCS#7→JOSE conversion, use POST /api/v3/crypto/reencrypt or POST /api/v3/crypto/resign to transform from classical to PQC algorithms.
For technical details on JOSE formats, see JOSE Technical Reference.
For strategic rationale (why JOSE vs PKCS#7), see From PKCS#7 to JWS/JWE: The Evolution of API Security*.
Security Considerations
Plaintext handling during conversion:
- ✅ Plaintext never sent to client (conversion is server-side)
- ✅ Decrypted plaintext never written to disk (the streaming endpoint spools the uploaded file itself to local disk — see Streaming mode below)
- ✅ Plaintext never transmitted over network
- ✅ Compact mode accumulates full file (bounded by
migration.max-upload-bytes, default 5 MB) but zeros immediately after conversion - ⚠️ Plaintext exists transiently in server memory during conversion. In streaming mode the footprint is bounded by the pipeline's chunk buffers for
EnvelopedData; the signed shapes additionally buffer the signature computation. See Memory and size behaviour for what each shape actually costs.
Memory zeroing:
- ✅ All compact endpoints (reencrypt, resign, PKCS7) zero plaintext buffers immediately after use
- ✅ Defense-in-depth memory security applied throughout platform
Streaming mode:
- ✅ Chunk-by-chunk streaming for
EnvelopedData— a single pass with bounded memory, whatever the number of recipients - ✅ The outer JOSE container is detached (multipart for JWE, JSON for JWS)
- ✅ USPTO patent architecture with BoundedByteQueue pipelines
- ✅ The result is accepted by
/api/v3/crypto/stream/reencryptand/api/v3/crypto/stream/resign, with one actor or several, in the Compact form and in the General JSON form alike — see Operating on a converted result - ⚠️ A
200status line is negotiated before the first payload byte, so it is not by itself a statement of success; see the abort contract - ⚠️ The uploaded file is spooled to local disk by the servlet container before the conversion runs, because the signed shapes re-read it once per signer. For
SignedDatathat file carries the signed content in clear; the decrypted content of an encrypted structure is never written to disk - ✅ Uploads up to 100 MB (a larger one is refused
413payload-too-large; a proxy in front of the deployment may cap uploads lower; see Behind a buffering proxy) — and the signed shapes cost one pass per signer plus one or three over it; see Memory and size behaviour - ⚠️ The streaming endpoint is under the streaming concurrency bound: when it is full, globally or for the tenant, the request is refused
429withRetry-Afterbefore the upload is read
Supported PKCS#7 Types
Every signer and every recipient of the input is carried. A structure with N signers converts to a JWS with N signatures and a structure with M recipients to a JWE with M recipient entries, in the order the SignerInfos and RecipientInfos are encoded. Every actor of the file is admitted and reported, but the token carries one entry per distinct tenant key, in first-occurrence order: two SignerInfos or two RecipientInfos held under one key produce one signature or one recipient, so a kid never repeats inside a token. The number of actors never makes a structure unsupported; one conversion accepts at most 16 signers and recipients together (ankasecure.crypto.jose.max-actors, the one actor bound the crypto plane also enforces), and above it the request is refused 422 ACTOR_LIMIT_EXCEEDED before any key is looked up.
Compact Mode (≤5MB files)
Supported structures (output under serialization AUTO):
| PKCS#7 Type | Actors | JOSE Output | Format |
|---|---|---|---|
| SignedData | N signers | JWS | one signer: Compact header.payload.signature; several: General JSON, one entry in signatures per signer |
| EnvelopedData | M recipients | JWE | one recipient: Compact header.encKey.iv.ciphertext.tag; several: General JSON, one entry in recipients per recipient |
| SignedAndEnvelopedData (RFC 2315 §11) | N outer signers, M recipients | JWE whose plaintext is a JWS | Compact on both layers for at most one signer and one recipient; General JSON on both layers otherwise |
JWS payload (compact endpoint): the JWS payload is a JWT claims set, not the raw content. The signed content travels base64url-encoded in the data claim, beside tid (the tenant) and iat (the signing time, when the service's JWS clock is enabled, which is the default). No exp is set. A verifier recovers the content by decoding data. This applies to the SignedData JWS and to the inner JWS of a SignedAndEnvelopedData result, in every serialization.
Streaming Mode (any file size)
No ceiling applies to the upload, but the cost is not uniform across the three structures — see Memory and size behaviour.
Supported structures (output under serialization AUTO):
| PKCS#7 Type | Actors | JOSE Output | Format |
|---|---|---|---|
| SignedData | N signers | Detached JWS (RFC 7797) | JSON (application/octet-stream): one signer — the flattened JSON serialization (RFC 7515 §7.2.2); several — General, a signatures array |
| EnvelopedData | M recipients | JWE, detached outer container | Multipart (header + ciphertext); the header carries one entry in recipients per recipient |
| SignedAndEnvelopedData (RFC 2315 §11) | N outer signers, M recipients | JWE whose plaintext is an attached JWS, one signature per outer signer | Multipart (header + ciphertext) |
JWS payload (streaming endpoint): the signature covers the raw content, with no claims wrapper. The SignedData JWS is detached with "b64":false and "crit":["b64"] (RFC 7797): the content is not in the output and a verifier supplies it. The inner JWS of a SignedAndEnvelopedData result is attached and its payload is the base64url encoding of the content itself. The two endpoints therefore do not produce the same JWS payload for the same file.
Serialization
The serialization request member (exact case) selects the JOSE serialization of the result. The decision is the same function of the actor counts on both endpoints:
serialization | At most one signer and at most one recipient | More than one signer or recipient |
|---|---|---|
AUTO (default) | the single-actor form | General JSON |
COMPACT | the single-actor form | refused 422, errorCode INCOMPATIBLE_SERIALIZATION, before any key is resolved |
JSON | General JSON, with one entry | General JSON |
The single-actor form differs by endpoint. On the compact endpoint it is the Compact serialization (RFC 7515 §7.1 / RFC 7516 §7.1). On the streaming endpoint the outer container is detached and therefore JSON-serialized by construction, so the single-actor form is the flattened detached JWS for SignedData and the multipart JWE with a one-recipient header for the enveloped structures — not RFC 7515 §7.1 Compact. A nested result uses the same serialization on both layers.
Any other value — json, Compact, or another JOSE serialization name — is refused 400 invalid-input naming serialization, before the file is decoded (compact) or read (streaming).
Signature Verification
The CMS signature of every signer that the conversion re-signs is verified, always. A signer resolves only to a tenant key whose certificate carries the same public key as the certificate that verified its CMS signature, so a tenant key cannot be made to re-sign content its owner did not sign. A signature that does not verify is refused 422 unprocessable-entity, and the detail names the index of the first failing signer.
No request member switches this verification on or off. The conversion request defines pkcs7Data, decryptionKid, serialization, targetFormat and algorithmPolicy (allowLegacyHash) only; any other member — validateCertificates, validateTimestamp or algorithmPolicy.contentEncryptionUpgrade included — is refused 400 invalid-input with a detail naming the member (never its value), on both endpoints and at the root as well as inside algorithmPolicy.
SignedAndEnvelopedData (RFC 2315 §11)
The signers of a SignedAndEnvelopedData are the outer signerInfos of RFC 2315 §11, and the decrypted content is carried into the JWS payload verbatim. RFC 2315 §11.2 requires each signer's encryptedDigest to be encrypted a second time under the content-encryption key, and defines that digest encryption for RSA signers only. The conversion verifies the outer signers in that form only:
- A signer whose
encryptedDigestis not encrypted under the content-encryption key — the form some producers, OpenSSL among them, write — is refused422witherrorCodeSIGNED_AND_ENVELOPED_DIGEST_NOT_ENCRYPTED. - A signer that does not hold an RSA key is refused
422witherrorCodeUNSUPPORTED_SIGNED_AND_ENVELOPED_SIGNER_KEY. - A
SignedAndEnvelopedDatawith no outer signer at all has no signature to convert and is refused422witherrorCodeSIGNED_AND_ENVELOPED_WITHOUT_SIGNER. - The content cipher must be AES-128/192/256-CBC or 3DES (DES-EDE3-CBC); any other, or parameters the named cipher cannot use, is refused
422witherrorCodeUNSUPPORTED_CONTENT_ENCRYPTION.
These refusals are decided before any signature is computed and, on the streaming endpoint, before any response byte. All of them except unusable cipher parameters are decided from the structure alone, so analysis reports them too, with the same code, as ERROR reasons (supportStatus.supported false).
The SignedAndEnvelopedData inner JWS is attached, not detached
The outer JOSE container is detached — the conversion result carries no PKCS#7 metadata (signers, recipients, algorithm details). The inner JWS is not: the signed content travels inside the JWS payload, so the inner signature verifies without a separately supplied content part. A client written against a detached inner JWS — one that expects to feed the content in alongside the signature — has nothing to feed it.
Current Limitations
- Current limitation: a file with a signer or a recipient known only by a certificate is not convertible; see Actors known only by a certificate.
- Current limitation: timestamps are detected and reported (
TIMESTAMP_DETECTED) but not validated. - Current limitation: certificate chains are not validated — no PKIX path validation; the CMS signature of every re-signed signer is always verified.
- Current limitation: signers must be identified by issuer and serial number; a signer identified by
SubjectKeyIdentifieris refused422 UNSUPPORTED_SIGNER_IDENTIFIER. - Current limitation: recipients must use RSA key transport (
KeyTransRecipientInfo) named by issuer and serial number; any other recipient type, and a key-transport recipient identified bySubjectKeyIdentifier, is refused422 UNSUPPORTED_RECIPIENT_TYPEon every endpoint, analysis included. - Current limitation:
SignedAndEnvelopedDatasigners are converted only in the RFC 2315 §11.2 form and only with RSA keys, and its content cipher must be AES-CBC or 3DES (see SignedAndEnvelopedData). - Current limitation: at most 16 signers and recipients together per conversion (
ACTOR_LIMIT_EXCEEDED). - Current limitation: an
EnvelopedDatacarries no integrity check of its own. When the decrypting recipient's key is wrong, the content decrypts to correctly padded garbage with a probability of about 1/256 (as for any wrong content key under CBC), and the conversion then succeeds over content that is not the plaintext. ASignedAndEnvelopedDatacatches that case at its signer verification; see Decryption failures and RFC 3218.
Target Format Selection
The targetFormat request field selects the JOSE output and must be compatible with the parsed PKCS#7 structure:
AUTO(default): auto-detect the output from the parsed structure (never raises a compatibility error).JWS: force JWS — valid only forSignedData.JWE: force JWE — valid only forEnvelopedData.JWE_JWS_NESTED: force nested JWE(JWS) — valid only forSignedAndEnvelopedData.
An explicit targetFormat incompatible with the parsed structure (e.g. JWS requested for EnvelopedData) is rejected with 422 unsupported-pkcs7-format (errorCode = INCOMPATIBLE_TARGET_FORMAT). Both the compact and streaming endpoints enforce this.
Prerequisites
Before converting PKCS#7 files, the tenant must hold a key for every signer and recipient, each matched by the issuer and serial number of the key's certificate:
- Each signer: its private key imported with its certificate — a key admissible to
sign. The conversion re-signs with it. - The recipient that decrypts (the one
decryptionKidnames, or else the first recipient in encoded order whose tenant key admits both): a key admissible todecryptandencrypt, imported with its private key. It opens the content and, like every recipient, the new content-encryption key is wrapped to it. - Every other recipient: a tenant key admissible to
encrypt— a keystore import with its private key, or the encryption key of aDUALimport.
A certificate imported alone is a verify-only trust anchor (key_ops ["verify"]), so it is admissible neither as a signer nor as a recipient; see Actors known only by a certificate.
A key admits an operation when it is not restricted and its key_ops (when present and non-empty) name it. A restricted key is read-only: it never signs and is never encrypted to, so it is refused as a signer or a recipient exactly like a missing key. See Validation modes for which imports produce a restricted key.
When an actor's certificate is held under more than one tenant key admissible for its role, the conversion cannot choose between them and is refused 422 unprocessable-entity with errorCode AMBIGUOUS_ACTOR_KEY; the detail names the actor's index, issuer and serial, never a kid. Remove the duplicate import and retry.
POST /api/v3/migration/analyze-pkcs7 lists every key a file needs in recommendations.requiredKeys.
Actors Known Only by a Certificate
A file is not convertible when one of its signers or recipients is a third party whose certificate the tenant holds but whose private key it does not — a counterparty's signing certificate, an external addressee the archive was also encrypted to — and when none of its recipients is a key the tenant holds with its private part.
Why the conversion cannot carry such an actor:
- A signer. The conversion does not translate a signature; it signs again under the signer's key. Re-signing needs that signer's private key, and a certificate does not contain it.
- A recipient. The content is encrypted again under a fresh content-encryption key wrapped to every recipient carried. A certificate imported alone is a verify-only trust anchor (
key_ops["verify"]): the platform uses it to check a signature, never to encrypt to it or to decrypt with it. With no recipient of the tenant's own, nothing can open the content at all.
Importing the certificate therefore does not make the actor convertible. Find out before converting:
- Analyze first.
POST /api/v3/migration/analyze-pkcs7needs no key and lists every actor of the file inrecommendations.requiredKeys, each with itspurpose(signingorencryption). Compare that list with the keys you hold with their private part. - The convert refusal names the missing actors. A conversion attempted anyway is refused
422 missing-private-keybefore any byte is written, andextensions.unresolvedActors[]names every actor that has no admissible tenant key (index,role,requirement,issuerDN,serialNumber). See Missing Key.
Keep the original PKCS#7 file: it remains the evidence of what the third party signed and to whom the content was addressed, and its signatures verify against the third party's certificate with any CMS implementation.
Import Keystores
Endpoint: POST /api/v3/admin/tenants/{tenantId}/keys/import-keystore
Purpose: Import signer and/or recipient private keys from PKCS#12 (.p12/.pfx) or JKS (.jks) files
Request example: the operation takes multipart/form-data — a metadata JSON part, a keystore file part, and an optional password part. The keystore is sent as a file, never base64-encoded into a JSON field.
curl -sS -X POST \
"$ANKA_BASE_URL/api/v3/admin/tenants/$TENANT_ID/keys/import-keystore" \
-H "Authorization: Bearer $TOKEN" \
-F 'metadata={"kid":"imported-keys","validationMode":"IMPORT_ONLY"};type=application/json' \
-F "[email protected]" \
-F "password=$KEYSTORE_PASSWORD"
Validation modes:
STRICT: Only import valid, non-expired certificatesIMPORT_ONLY: Import expired certificates (allow decryption/verification of legacy data)SKIP: Skip validation entirely
Current limitation: under IMPORT_ONLY, a key whose end-entity certificate or a CA certificate in its chain has expired, or whose chain fails PKIX validation, is imported restricted (read-only). A restricted key cannot sign and cannot be encrypted to, so the conversion refuses it as an unresolved signer or recipient (422 missing-private-key) — including the decrypting recipient, which is also encrypted to. IMPORT_ONLY therefore suits a key whose certificate is still valid; a key imported restricted serves decryption and verification of legacy data outside this conversion.
Output: Keys stored under the kid you choose. The conversion matches a key to an actor by the issuer DN and serial number of the key's certificate, never by kid.
Complete Migration Workflow
graph TD
Start["Legacy PKCS#7 File<br/>.p7m/.p7s/.p7e"] -->|Step 1| Import["POST /api/v3/admin/tenants/{tenantId}/keys/import-keystore<br/>Import signer/recipient keys"]
Import -->|Step 2| Analyze["POST /api/v3/migration/analyze-pkcs7<br/>Understand structure"]
Analyze -->|Step 3| Convert["POST /api/v3/migration/convert-pkcs7-to-jose<br/>Convert to JWE/JWS format"]
Convert -->|Output| JOSE["JWE/JWS, Compact or General JSON<br/>Classical algorithms RSA/AES-GCM"]
JOSE -->|Step 4 Optional| Decision{"Need PQC<br/>Protection?"}
Decision -->|Yes - Encrypted data| Reencrypt["POST /api/v3/crypto/reencrypt<br/>RSA → ML-KEM"]
Decision -->|Yes - Signed data| Resign["POST /api/v3/crypto/resign<br/>RSA/ECDSA → ML-DSA"]
Decision -->|No| Done["Migration Complete<br/>Modern JOSE format"]
Reencrypt --> PQC["JWE/JWS with PQC<br/>ML-KEM/ML-DSA algorithms"]
Resign --> PQC
style Start fill:#ffcccc,stroke:#333,stroke-width:2px
style JOSE fill:#ffd43b,stroke:#333,stroke-width:2px
style PQC fill:#51cf66,stroke:#333,stroke-width:2px
style Done fill:#51cf66,stroke:#333,stroke-width:2px Critical note: PKCS#7→JOSE conversion (Step 3) is always required first. PQC transformation (Step 4) is a separate optional operation on the already-converted JOSE data.
Step 4 accepts every result, with one actor or several; see Operating on a converted result.
Endpoint Reference
Analyze PKCS#7 Structure (Compact)
Endpoint: POST /api/v3/migration/analyze-pkcs7
Purpose: Inspect PKCS#7 structure before conversion
Size limit: the configured migration upload limit, migration.max-upload-bytes (default 5 MB), measured on the decoded Base64; the JSON request body itself is admitted up to 4 × ⌈limit ÷ 3⌉ + 65,536 bytes and a larger one is refused 413 before it is read
Request:
pkcs7Data is the only member: analysis decrypts nothing and needs no key or option. A member the request does not define is refused 400 invalid-input with a detail naming the member (never its value).
Response (complete — one signer; every member the service returns, including the null ones. Only certificatePEM is shortened, after its first base64 line):
{
"detectedFormat": {
"standard": "PKCS#7",
"structure": "SignedData",
"contentTypeOID": "1.2.840.113549.1.7.2",
"encoding": "BER",
"payloadEmbedded": true
},
"signers": [
{
"index": 0,
"issuerDN": "CN=Legacy Finance Signer,O=Example Corp,C=US",
"serialNumber": "0x4A3F2E1D5B6C7A8B",
"signatureAlgorithmOID": "1.2.840.113549.1.1.11",
"signatureAlgorithm": "SHA256withRSA",
"digestAlgorithmOID": "2.16.840.1.101.3.4.2.1",
"digestAlgorithm": "SHA-256",
"certificatePresent": true,
"certificatePEM": "-----BEGIN CERTIFICATE-----\nMIIDHDCCAgSgAwIBAgIISj8uHVtseoswDQYJKoZIhvcNAQELBQAwRDELMAkGA1UE\n...\n-----END CERTIFICATE-----",
"certificateChainLength": 1,
"keyUsage": [
"digitalSignature",
"nonRepudiation"
],
"timestampPresent": false,
"timestampTSA": null,
"timestampGenTime": null
}
],
"recipients": [],
"supportStatus": {
"supported": true,
"reasons": []
},
"recommendations": {
"migrationPath": "CONVERT_TO_JWS_COMPACT",
"requiredKeys": [
{
"purpose": "signing",
"issuerDN": "CN=Legacy Finance Signer,O=Example Corp,C=US",
"serialNumber": "0x4A3F2E1D5B6C7A8B",
"algorithm": "SHA256withRSA",
"importHint": "Use POST /api/v3/admin/tenants/{tenantId}/keys/import-keystore to import the private key and certificate of this signer"
}
],
"estimatedComplexity": "LOW"
},
"limits": {
"maxActors": 16
}
}
Every signer and recipient is listed. A serialNumber is 0x followed by the upper-case hexadecimal of the serial's DER INTEGER content octets, as the certificate carries them — so a serial whose first octet is 80-FF keeps the leading 00 DER gives it (for example 0x05DC377EC0F2EA, or 0x009F8E7D6C5B4A3B2C). Matching is done on the serial number itself, never on this text.
migrationPath names the form a conversion under AUTO produces (_COMPACT for at most one signer and one recipient, _JSON otherwise), requiredKeys names one tenant key per actor (purpose signing or encryption), and decryptionRequirement (ONE_RECIPIENT_PRIVATE_KEY) is present for an encrypted structure.
supportStatus runs every check a conversion runs without a key and reports each refusal with the conversion's own code, so supported is false exactly when the conversion would refuse the structure before any key is used. The reasons are listed in the order the conversion decides them, so the first ERROR reason is the refusal a conversion of the same bytes answers:
ERROR— the structure refusals: for aSignedData,INVALID_PKCS7(what the conversion answers400 invalid-pkcs7: no signer, or no encapsulated content); for aSignedAndEnvelopedData,UNSUPPORTED_CONTENT_ENCRYPTION, thenSIGNED_AND_ENVELOPED_WITHOUT_SIGNER,UNSUPPORTED_SIGNED_AND_ENVELOPED_SIGNER_KEYandSIGNED_AND_ENVELOPED_DIGEST_NOT_ENCRYPTEDERROR—ACTOR_LIMIT_EXCEEDED, a file above the actor boundWARNING—LEGACY_ALGORITHM_REJECTED, a SHA-1/MD5 signer, which the conversion refuses unlessalgorithmPolicy.allowLegacyHashistrueINFO—TIMESTAMP_DETECTED, an RFC 3161 timestamp, reported and not validated
limits.maxActors states the actor bound — the most signers and recipients, counted together, one conversion accepts — read from the same ankasecure.crypto.jose.max-actors property the conversion enforces (default 16), so a client discovers the bound from the analysis instead of hard-coding it. See Analyze Operations for a multi-signer and a multi-recipient example.
Analyze PKCS#7 Structure (Streaming)
Endpoint: POST /api/v3/migration/stream/analyze-pkcs7
Purpose: Inspect large PKCS#7 files (>5MB)
Size limit: none. The parse is incremental and the payload is never accumulated, so heap is a function of the chunk size and not of the file. The former 100 MB application ceiling was deleted, not raised.
Content-Type: multipart/form-data
Parts — the order below is part of the contract, not a presentation choice:
metadata(required): an empty JSON object,{}— the part defines no memberfile(required): Binary PKCS#7 file
The server reads the body in a single pass and streams file straight through, so it cannot rewind to collect a metadata part that arrives after the payload. A body whose file part comes first — or that omits metadata entirely, which a single-pass parser cannot distinguish from one sent late — is refused with 400, type .../errors/multipart-part-order, before any payload byte is read.
Why an empty metadata part is still required
Analysis is purely structural and takes no key and no option, so the part defines no member, and a member it carries is refused 400 invalid-input naming the member. The part is nonetheless required, and first, because the streaming contract is the same on every streaming endpoint — a small JSON part, then the payload, read in a single pass. See Analyze Operations.
Convert PKCS#7 to JOSE (Compact)
Endpoint: POST /api/v3/migration/convert-pkcs7-to-jose
Purpose: Convert PKCS#7 files to JWE/JWS format
Size limit: the configured migration upload limit, migration.max-upload-bytes (default 5 MB), measured on the decoded Base64; the JSON request body itself is admitted up to 4 × ⌈limit ÷ 3⌉ + 65,536 bytes and a larger one is refused 413 before it is read
Scope required: SCOPE_migration.convertPkcs7
Convert PKCS#7 to JOSE (Streaming)
Endpoint: POST /api/v3/migration/stream/convert-pkcs7-to-jose
Purpose: Convert PKCS#7 files of up to 100 MB using streaming architecture (a proxy in front of the deployment may cap uploads lower; see Behind a buffering proxy)
Size limit: 100 MB per part and 101 MB per request, the container's multipart bound (spring.servlet.multipart); a larger upload is refused 413 payload-too-large before the handler runs. Within it, what varies by structure is the cost, not the ceiling — see Memory and size behaviour.
Status codes: 200, 400, 401, 402, 403, 404, 409, 413, 422, 429, 500, 503. This endpoint does not return 413: nothing on its path enforces a payload ceiling. The 413 in the error table belongs to the compact endpoint only. The 402 is a platform-wide licensing refusal rather than a conversion outcome — conversion is a cryptographic write, so it is refused while the deployment licence is not in effect; see Deployment License Expired. It is therefore absent from the error table, which lists conversion conditions only.
Input Content-Type: multipart/form-data
Input Parts:
metadata(required, first): a JSON object with the conversion options —decryptionKid(optional),serializationandtargetFormat(both optional; an absent member isAUTO) andalgorithmPolicy(allowLegacyHash). A member the part does not define is refused400 invalid-inputnaming it, before the upload is readfile(required): Binary PKCS#7 file
Output Format (varies by PKCS#7 type):
- SignedData →
application/octet-stream(detached JWS, JSON serialization: flattened for one signer underAUTOorCOMPACT, General — asignaturesarray — otherwise) - EnvelopedData →
multipart/mixed(JWE: header part, then the ciphertext part) - SignedAndEnvelopedData →
multipart/mixed(a JWE whose plaintext is an attached JWS — the signed content is inside the JWS payload). The inner JWS is the attached Compact serialization for one signer underAUTOorCOMPACT, and the General JSON serialization otherwise
Output Example (SignedData, one signer):
Output Example (SignedData, two signers):
{"signatures":[{"protected":"eyJhbGc...","signature":"dGVz..."},{"protected":"eyJhbGc...","signature":"c2ln..."}]}
An RSA signer's streaming signature is RSASSA-PSS whenever its protected header says PS256, and an ECDSA signature is the fixed-length concatenation of R and S that RFC 7518 §3.4 specifies, so the output verifies with standard JOSE libraries.
Output Example (EnvelopedData):
------ankatech-<UUID>
Content-Type: application/jose+json
{"protected":"...","iv":"...","ciphertext":null,"tag":null,"recipients":[...]}
------ankatech-<UUID>
Content-Type: application/octet-stream
<binary ciphertext stream>
------ankatech-<UUID>--
The header part shown is abridged; it carries one recipients entry per recipient, and its ciphertext and tag are null because the encrypted content travels in the second part.
Compatibility: the result is accepted by /api/v3/crypto/stream/reencrypt and /api/v3/crypto/stream/resign (and by /stream/decrypt, /stream/verify and /stream/decrypt-verify). A single-actor result is read in both of its forms:
- A one-signer detached JWS — the flattened form, or the one-entry General form (
{"signatures":[{"protected":…,"signature":…}]}) thatserializationJSONproduces — is read alike by/stream/verifyand/stream/resign. A body with no signature at all is refused422 unprocessable-entitywitherrorCodeJWS_SIGNATURE_MISSING. - A one-recipient JWE whose
algandkidsit only in the unprotected recipientheader(the General layout of RFC 7516 §7.2.1, whichserializationJSONproduces) is read by/stream/decrypt,/stream/reencryptand/stream/decrypt-verify— and, as a bufferedjweToken, by/decrypt,/reencryptand/decrypt-verify— after itskidis authorised and resolved. Itsprotectedheader carries neither name: the header locations are disjoint, and a name in both is refused. Itsalgmust be exactly the key-management algorithm the platform uses for that key (RSA-OAEP-512for an RSA key); any other value is refused422 unprocessable-entitywitherrorCodeJWE_ALG_KEY_MISMATCH(on a streaming route, before any body byte). A symmetric or composite key in that layout is refused withJWE_ALG_NOT_PROTECTED, on the buffered and the streaming routes alike.
A result with several signatures or several recipients is read there too, under the rules in Operating on a converted result: the recipient to decrypt with is named in recipientKid, and the actor a re-encrypt or re-sign replaces in sourceKid.
The abort contract (streaming endpoint)
A streaming response sends its status line before the first payload byte. That single fact decides how every failure on this endpoint is reported, and it splits them into two sets.
Before the first body byte — a status and a problem document. Every failure that can be decided without reading the content bytes is answered as an HTTP status carrying an application/problem+json body: an invalid or unsupported PKCS#7 structure, a serialization or targetFormat the structure cannot take, more actors than one conversion accepts, a decryptionKid that names none of the recipients, an unresolvable kid, a signer whose CMS signature does not verify, a refused or unusable key, a key-protection backend refusal, and a signer or recipient without a tenant key — the last of these at every input size, because every actor is resolved before anything is written. A SignedAndEnvelopedData decryption failure is also decided here: its outer signers are verified under the content-encryption key before anything is signed or written. For an EnvelopedData whose recipient uses RSA-OAEP, an encrypted key that does not unwrap is also refused here, when the envelope is opened.
While the output is being emitted — a problem document until the response is committed, an aborted response after. This applies to a content-decryption failure of an EnvelopedData: it converts in a single pass, so its content is decrypted while the output is being written. That includes a wrong or corrupted RSA PKCS #1 v1.5 encrypted key, which is replaced by a random key and therefore fails only at the content (see Decryption failures and RFC 3218). While the response is still uncommitted — its bytes have not yet left the server's response buffer — the failure is answered as the 422 decryption-failed problem document, exactly as before the first byte. Once the response is committed there is no status left to change, and the response is aborted.
A failure discovered once the response is committed ends it without completing its framing: the chunked terminator is never sent, and a multipart/mixed response never receives its closing boundary. No problem document is ever written into the body. There is no status to change and no trailing error object to look for.
A 200 plus a completed read is not sufficient. A short response is a failure.
What a client must do: treat a response that ends without its terminator or closing boundary as an error, and discard the partial output. Do not write it over a previous good file, do not hand it to a downstream consumer, and do not promote a temporary file to its final name until the response has completed.
Why a partial body can look perfectly valid. The emitted streaming JWE is authenticated by a single terminal GCM tag — there is no per-chunk authentication and no declared plaintext length. Every byte before the tag decrypts cleanly. A truncated response therefore yields a byte-perfect prefix of the real plaintext and fails only at the very end, when the tag is verified and is absent. A consumer that streams plaintext to disk as it decrypts will have written that prefix before it learns the stream was cut. The Java SDK handles this for you: it writes to a sidecar file and promotes it with an atomic rename only on a complete response, deleting it otherwise.
Memory and size behaviour
The upload is bounded at 100 MB per part and 101 MB per request: the container spools it to disk before the conversion reads it, so the bound is what keeps the spool directory from filling (a proxy in front of the deployment may cap uploads lower; see Behind a buffering proxy). Within that bound there is a cost, and it differs by structure:
| PKCS#7 type | Passes over the upload | Memory |
|---|---|---|
EnvelopedData (M recipients) | 1 | bounded — the content is decrypted and re-encrypted as a stream, with no whole-plaintext buffer |
SignedData (N signers) | N + 1 | bounded per pass |
SignedAndEnvelopedData (N signers) | N + 3 — a keyless structure pass, a verification pass, one signing pass per signer and the emission | bounded per pass |
The signed shapes read the uploaded file once per signer plus once more because RFC 2315 places the signer metadata after the content: the signers cannot be resolved from a header peek at any size, and each signature is computed in its own pass. The first pass reads through the content to reach the signer information and resolve every key — which is why a missing key is a 422 before any byte is written, at every size. Expect the wall-clock time to scale with the signer count.
The upload is spooled to local disk by the servlet container before the handler runs, so every later pass reopens a local file rather than re-reading the network. If the spooled file changes between passes, the conversion fails as a server fault (500), because the file is server-held.
Decryption failures and RFC 3218
Every decryption failure of an EnvelopedData or a SignedAndEnvelopedData, on both endpoints, is the same refusal: 422 decryption-failed, errorCode DECRYPTION_FAILED, and one fixed detail that names no step and no provider error.
PKCS #1 v1.5 key transport (rsaEncryption) — random filling. RFC 3218 §2.3.2 ("Random Filling") answers the million-message attack on PKCS #1 v1.5: when the victim detects an improperly formatted block, "instead of returning an error he substitutes a randomly generated message" — in CMS, "a randomly generated CEK of appropriate length" — and continues; the substitute must be "cryptographically random". The conversion applies this to the unwrap of the decrypting recipient — the only recipient whose encrypted key is unwrapped; every other recipient is only encrypted to — when that recipient is an rsaEncryption recipient, on every structure and both endpoints: an encrypted key that does not unwrap, that unwraps to a block of the wrong length, or that unwraps to a key the content cipher cannot take is replaced by a fresh random key of the content cipher's length (for RC2 and CAST5, whose parameters do not fix the length, only when the unwrap fails or the recovered length is outside the range the cipher allows). The failure then surfaces only where a wrong key surfaces — at the content, or for a SignedAndEnvelopedData at an outer signer's encrypted digest — exactly as a corrupted ciphertext does.
What this does not guarantee:
- RSA-OAEP (
id-RSAES-OAEP) recipients are not random-filled, because OAEP is itself one of the RFC 3218 §2.3 countermeasures (§2.3.3: "Implementations using OAEP are not susceptible to the MMA."), so the §2.3.2 random filling is not applied to them: an encrypted key that does not unwrap is refused immediately, with the same body. On the streaming endpoint anEnvelopedDatatherefore refuses an OAEP unwrap failure when the envelope is opened, before any body byte (always aproblem+json422), and a content failure while the output is being emitted (the same422while the response is still uncommitted, an aborted response once it is committed). The two can be told apart only when the content failure arrives after the response is committed. On the compact endpoint both are refused before any response byte. - A structural CMS failure raised before any key is used is refused immediately with the same body.
- The timing of a refusal is not made constant.
- A random or wrong content key decrypts to a correctly padded block with a probability of about 1/256. A
SignedAndEnvelopedDatathen fails its signer verification; anEnvelopedData, which carries no integrity check, converts content that is not the plaintext. Current limitation: the conversion cannot detect that case for anEnvelopedData.
On the streaming endpoint, an EnvelopedData content failure — a PKCS #1 v1.5 key that did not unwrap included — is discovered while the output is being emitted: it is the 422 problem document while the response is still uncommitted, and aborts the response once it is committed (see The abort contract). A SignedAndEnvelopedData decryption failure is refused before any byte.
Content semantics: the decrypted content is emitted verbatim
Both the compact and the streaming endpoint place the decrypted PKCS#7 content into the JOSE container byte for byte. The converter does not inspect the plaintext, does not recognise it as a nested structure, and does not extract anything from it. A round trip through the conversion returns exactly the bytes the PKCS#7 content carried.
This matters for any plaintext that is itself DER: a certificate, a nested ContentInfo, a signed bundle. Such a payload comes back whole, not replaced by something found inside it.
Request/Response Examples
Every response below is complete: it carries every member the service returns for that case. Only the long values are truncated: pkcs7Data and the segments of the JWS joseTokens with ..., and the JWE joseTokens of Examples 2 and 3 after their protected header, with an explicit (truncated …) marker naming what their ciphertext carries. The response carries no top-level warnings member; each entry of signers[] and recipients[] carries its own warnings array, empty in these cases.
Example 1: SignedData (1 signer, no encryption)
PKCS#7 type: SignedData
Input: Signed document (.p7s)
Output: JWS Compact token
Request:
{
"pkcs7Data": "MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgEFADCABgkqhkiG9w0BBwGggCSABIIB...",
"serialization": "AUTO",
"targetFormat": "AUTO",
"algorithmPolicy": {
"allowLegacyHash": false
}
}
Response:
{
"joseToken": "eyJhbGciOiJQUzI1NiIsImtpZCI6ImZpbmFuY2Utc2lnbmVyLXJzYS0yMDIwIn0.eyJkYXRhIjoiUVc1cmFWTmxZM1Z5WlE....kTn9mQ2vXw...",
"serialization": "COMPACT",
"structure": "JWS",
"decision": {
"signerCount": 1,
"recipientCount": 0,
"chosenFormat": "JWS",
"chosenSerialization": "COMPACT",
"reason": "1 signer(s) and 0 recipient(s) detected; JWS in Compact serialization (the single-actor form)"
},
"signers": [
{
"originalIssuerDN": "CN=Legacy Finance Signer,O=Example Corp,C=US",
"originalSerialNumber": "0x4A3F2E1D5B6C7A8B",
"assignedKid": "finance-signer-rsa-2020",
"algorithm": "PS256",
"certificateMatched": true,
"warnings": []
}
],
"recipients": [],
"algorithmTransformations": [
{
"original": "SHA256withRSA (OID: 1.2.840.113549.1.1.11)",
"transformed": "PS256",
"reason": "Re-signed with the matched key under the JWS alg that key signs with"
}
]
}
Example 2: EnvelopedData (1 recipient, encrypted)
PKCS#7 type: EnvelopedData
Input: Encrypted file (.p7e)
Output: JWE Compact token
decryptionKid is optional here: without it, the first recipient in encoded order whose tenant key admits decryption and encryption decrypts.
Request:
{
"pkcs7Data": "MIIGHgYJKoZIhvcNAQcDoIIGDzCCBgsCAQAxggE...",
"decryptionKid": "records-office-rsa",
"serialization": "AUTO",
"targetFormat": "AUTO"
}
Response:
{
"joseToken": "eyJhbGciOiJSU0EtT0FFUC01MTIiLCJlbmMiOiJBMjU2R0NNIi... (truncated: Compact JWE whose ciphertext is the decrypted PKCS#7 content)",
"serialization": "COMPACT",
"structure": "JWE",
"decision": {
"signerCount": 0,
"recipientCount": 1,
"chosenFormat": "JWE",
"chosenSerialization": "COMPACT",
"reason": "0 signer(s) and 1 recipient(s) detected; JWE in Compact serialization (the single-actor form)"
},
"signers": [],
"recipients": [
{
"originalIssuerDN": "CN=Records Office,O=Example Corp,C=US",
"originalSerialNumber": "0x5A6B7C8D9E0F1021",
"assignedKid": "records-office-rsa",
"algorithm": "RSA-OAEP-512",
"warnings": []
}
],
"algorithmTransformations": [
{
"original": "AES-256-CBC (OID: 2.16.840.1.101.3.4.1.42)",
"transformed": "A256GCM",
"reason": "Decrypted and encrypted again under the JWE content encryption the conversion emits"
}
]
}
Example 3: SignedAndEnvelopedData (1 signer + 1 recipient)
PKCS#7 type: SignedAndEnvelopedData
Input: Signed and encrypted file (.p7m)
Output: JWE(JWS) nested Compact token
Request:
{
"pkcs7Data": "MIIHkwYJKoZIhvcNAQcEoIIHhDCCB4ACAQExggE...",
"decryptionKid": "records-office-rsa",
"serialization": "AUTO",
"targetFormat": "AUTO",
"algorithmPolicy": {
"allowLegacyHash": false
}
}
Response:
{
"joseToken": "eyJhbGciOiJSU0EtT0FFUC01MTIiLCJlbmMiOiJBMjU2R0NNIi... (truncated: Compact JWE whose ciphertext is the encrypted inner JWS)",
"serialization": "COMPACT",
"structure": "JWE_JWS_NESTED",
"decision": {
"signerCount": 1,
"recipientCount": 1,
"chosenFormat": "JWE_JWS_NESTED",
"chosenSerialization": "COMPACT",
"reason": "1 signer(s) and 1 recipient(s) detected; JWE_JWS_NESTED in Compact serialization (the single-actor form)"
},
"signers": [
{
"originalIssuerDN": "CN=Legacy Finance Signer,O=Example Corp,C=US",
"originalSerialNumber": "0x4A3F2E1D5B6C7A8B",
"assignedKid": "finance-signer-rsa-2020",
"algorithm": "PS256",
"certificateMatched": true,
"warnings": []
}
],
"recipients": [
{
"originalIssuerDN": "CN=Records Office,O=Example Corp,C=US",
"originalSerialNumber": "0x5A6B7C8D9E0F1021",
"assignedKid": "records-office-rsa",
"algorithm": "RSA-OAEP-512",
"warnings": []
}
],
"algorithmTransformations": [
{
"original": "SHA256withRSA (OID: 1.2.840.113549.1.1.11)",
"transformed": "PS256",
"reason": "Re-signed with the matched key under the JWS alg that key signs with"
},
{
"original": "AES-256-CBC (OID: 2.16.840.1.101.3.4.1.42)",
"transformed": "A256GCM",
"reason": "Decrypted and encrypted again under the JWE content encryption the conversion emits"
}
]
}
Example 4: SignedData (2 signers, General JSON)
PKCS#7 type: SignedData with two signers
Output: JWS in the General JSON serialization, one signature per signer
Request: as Example 1; under serialization AUTO the two signers yield General JSON, and COMPACT would be refused 422 INCOMPATIBLE_SERIALIZATION.
Response (joseToken is the General JSON document as a string):
{
"joseToken": "{\"payload\":\"eyJkYXRhIjoiUVc1cmFWTmxZM1Z5WlE...\",\"signatures\":[{\"protected\":\"eyJhbGciOiJQUzI1NiIsImtpZCI6ImZpbmFuY2Utc2lnbmVyLXJzYS0yMDIwIn0\",\"signature\":\"kTn9mQ2vXw...\"},{\"protected\":\"eyJhbGciOiJQUzI1NiIsImtpZCI6ImxlZ2FsLXNpZ25lci1wc3MtMjAyMSJ9\",\"signature\":\"b3RoZXJzaWc...\"}]}",
"serialization": "JSON",
"structure": "JWS",
"decision": {
"signerCount": 2,
"recipientCount": 0,
"chosenFormat": "JWS",
"chosenSerialization": "JSON",
"reason": "2 signer(s) and 0 recipient(s) detected; JWS in General JSON serialization, one entry per actor"
},
"signers": [
{ "originalIssuerDN": "CN=Legacy Finance Signer,O=Example Corp,C=US", "originalSerialNumber": "0x4A3F2E1D5B6C7A8B", "assignedKid": "finance-signer-rsa-2020", "algorithm": "PS256", "certificateMatched": true, "warnings": [] },
{ "originalIssuerDN": "CN=Legacy Legal Signer,O=Example Corp,C=US", "originalSerialNumber": "0x1B2C3D4E5F607182", "assignedKid": "legal-signer-pss-2021", "algorithm": "PS256", "certificateMatched": true, "warnings": [] }
],
"recipients": [],
"algorithmTransformations": [
{ "original": "SHA256withRSA (OID: 1.2.840.113549.1.1.11)", "transformed": "PS256", "reason": "Re-signed with the matched key under the JWS alg that key signs with" },
{ "original": "RSASSA-PSS (OID: 1.2.840.113549.1.1.10)", "transformed": "PS256", "reason": "Re-signed with the matched key under the JWS alg that key signs with" }
]
}
An EnvelopedData with several recipients converts the same way to a General JSON JWE with one entry in recipients per recipient. Only the decrypting recipient's key decrypts, but every recipient needs a tenant key admissible to encrypt (see Prerequisites).
Algorithm Transformations
The conversion reports what it emitted, read back from the emitted token: signers[].algorithm, recipients[].algorithm and algorithmTransformations[].transformed equal the alg and enc in the emitted headers, never a value mapped from the PKCS#7 identifiers. algorithmTransformations[].original names the PKCS#7 algorithm and its OID.
Signature Algorithms
The PKCS#7 signature is not translated: the content is re-signed with the imported key matched to the signer (issuer and serial number), so the JOSE alg is the one that key signs with, whatever digest the PKCS#7 signer used. signers[].algorithm and algorithmTransformations[].transformed report that alg, the same value as in the protected header of the emitted JWS (the inner JWS for JWE_JWS_NESTED):
| Matched key | JOSE alg | Notes |
|---|---|---|
| RSA (any size) | PS256 | RSASSA-PSS, SHA-256, MGF1-SHA-256, 32-byte salt (RFC 7518 §3.5) |
| EC P-256 | ES256 | R and S concatenated, 64 bytes (RFC 7518 §3.4) |
| EC P-384 | ES384 | R and S concatenated, 96 bytes |
| EC P-521 | ES512 | R and S concatenated, 132 bytes |
| EC brainpoolP256r1 / brainpoolP384r1 / brainpoolP512r1 | ESB256 / ESB384 / ESB512 | R and S concatenated, 64 / 96 / 128 bytes (RFC 9864 §2.1) |
A SHA256withRSA or SHA512withRSA PKCS#7 signature with an RSA key therefore converts to PS256, not RS256 or RS512.
Legacy hash policy:
- If
algorithmPolicy.allowLegacyHash = false(default): a signer on SHA-1 or MD5 is refused422 LEGACY_ALGORITHM_REJECTED - If
algorithmPolicy.allowLegacyHash = true: the signer is converted like any other — its CMS signature is verified and the content is re-signed under thealgof the table above
Content Encryption Algorithms
The PKCS#7 content cipher is not translated either: the content is decrypted and encrypted again under A256GCM (AES-256-GCM, RFC 7518 §5.3), whatever cipher the file used. There is no option to preserve the original cipher.
| PKCS#7 content cipher | Converted from EnvelopedData | Converted from SignedAndEnvelopedData | Emitted enc |
|---|---|---|---|
| AES-128-CBC / AES-192-CBC / AES-256-CBC | Yes | Yes | A256GCM |
| 3DES (DES-EDE3-CBC) | Yes | Yes | A256GCM |
| RC2-CBC | Yes | No — UNSUPPORTED_CONTENT_ENCRYPTION | A256GCM |
| CAST5-CBC | Yes | No — UNSUPPORTED_CONTENT_ENCRYPTION | A256GCM |
RC2 and CAST5 carry a key size in their parameters, and a file whose parameters encode one their RFC does not define is refused 422 unsupported-pkcs7-format with errorCode UNSUPPORTED_CONTENT_ENCRYPTION on every structure, EnvelopedData included: RC2 effective key bits outside 1 to 1024 (RFC 2268 §6, which also defines their RC2Version encoding), or CAST5 parameters that are not an 8-octet IV followed by a keyLength of 40 to 128 bits in 1-octet increments (RFC 2984 §2.1). Every parse decides it — both analyses and both conversions — before any key is looked up or used, so analysis does not report it as a supportStatus reason: the analysis itself is refused.
Key Encryption Algorithms
A new content-encryption key is wrapped to every recipient under the key-management alg its matched key wraps with. PKCS#7 recipients are RSA key transport, so the matched keys are RSA:
| PKCS#7 key transport | Matched key | Emitted JOSE alg |
|---|---|---|
rsaEncryption (PKCS #1 v1.5) or RSA-OAEP | RSA (any size) | RSA-OAEP-512 |
Common Scenarios
Scenario 1: S/MIME Email Migration
Source: Email with S/MIME signed attachment (.p7s)
Workflow:
- Extract .p7s attachment from email
- Import the signer's PKCS#12 (private key and certificate):
POST /api/v3/admin/tenants/{tenantId}/keys/import-keystore - Analyze S/MIME structure:
POST /api/v3/migration/analyze-pkcs7 - Convert to JWS:
POST /api/v3/migration/convert-pkcs7-to-jose - Store JWS token in modern system
Result: Email attachment migrated from S/MIME SignedData to JWS (Compact for one signer, General JSON for several)
Scenario 2: SecureFile Archive Conversion
Source: SecureFile encrypted archive (.p7m)
Workflow:
- Import the recipient's and the signer's PKCS#12:
POST /api/v3/admin/tenants/{tenantId}/keys/import-keystore. Current limitation: a key imported underIMPORT_ONLYwhose certificate has expired or fails PKIX validation is restricted and refused by the conversion; see Import Keystores - Analyze SecureFile structure:
POST /api/v3/migration/analyze-pkcs7- Expects: SignedAndEnvelopedData with 1 signer + 1 recipient
- Convert to JWE(JWS):
POST /api/v3/migration/convert-pkcs7-to-jose(decryptionKidoptional) - Verify decryption: Decrypt JWE to verify plaintext integrity
Result: SecureFile .p7m migrated to JWE(JWS) Compact nested token. The outer signers must carry the RFC 2315 §11.2 digest encryption; see SignedAndEnvelopedData.
Scenario 3: Multi-Signer Document
Source: PKCS#7 file with multiple signers (e.g., multi-party authorization document)
Workflow:
- Analyze structure:
POST /api/v3/migration/analyze-pkcs7—signers[]lists every signer andrecommendations.requiredKeysnames the key each one needs (purposesigning) - Import every signer's PKCS#12 (private key and certificate)
- Convert:
POST /api/v3/migration/convert-pkcs7-to-josewithserializationAUTOorJSON - If a signer has no tenant key admissible to sign, the conversion is refused
422 missing-private-key, andextensions.unresolvedActors[]names every such signer by index, issuer DN and serial number
Result: a General JSON JWS with one signature per signer, in encoded order. Verify checks every signature and reports each signer; re-sign replaces only the signature of the rotated key; see Operating on a converted result.
Next Steps After Conversion
After converting PKCS#7 to JOSE, you have several options:
Option 1: Use JOSE Tokens Directly
Every result is ready to use with ANKASecure's crypto operations, with one actor or several; see Operating on a converted result.
- Decrypt JWE:
POST /api/v3/crypto/decrypt - Verify JWS:
POST /api/v3/crypto/verify - Re-sign under another key:
POST /api/v3/crypto/resign
The synchronous crypto operations take the token as a General JSON object (RFC 7516 §7.2 / RFC 7515 §7.2) in jweToken or jwsToken, not as a string. A result converted with serialization JSON is that object once its joseToken string is parsed; a Compact result is restructured into it — for a JWE, protected is the first segment, recipients[0].encrypted_key the second, then iv, ciphertext and tag; for a JWS, protected and signature go into signatures[0] beside payload.
Operating on a Converted Result
The crypto plane opens, verifies, re-encrypts and re-signs every conversion result — one signer or several, one recipient or several, nested or not — on the synchronous and the streaming routes alike, because every actor of a converted token is a key of the tenant. Four rules apply when a token names several keys:
- Decrypt with a recipient on which you hold a
DECRYPTgrant, named inrecipientKid. The member is required when the JWE is addressed to several keys; for the composite encryption of one composite key, whose recipients are that key's components, it is optional. The platform never tries the other recipients: without a grant on the named key the answer is403, and its body names no key. - Verify requires every signature to be valid. The response reports each signer in
signatureResults[], in token order, and the token is valid only if all of them verify. - Re-sign verifies every existing signature first, then replaces only the signature of the rotated key, named in
sourceKid, with one undernewKid. The other signatures and the payload are carried byte for byte. - Re-encrypt always draws a fresh content-encryption key and replaces only the rotated recipient, named in
sourceKid, withnewKid. The kept recipients are exactly the recipients the token's protectedank_kvsmap names, and each one needsREENCRYPT(sourceKid → kept).
Each key needs its own grant — DECRYPT on the named recipient, VERIFY on every signer verified, RESIGN(sourceKid → newKid) or REENCRYPT(sourceKid → newKid) on the replaced actor — and a grant that covers several of the keys is consumed once per request. Decrypt-and-verify combines rules 1 and 2, and the decrypting recipient must not also be an inner signer.
The crypto audit event of the operation lists every key it used in usedKeys (canonical generation 4), so a key's audit history finds the operation whichever key you ask for — the decrypting recipient, any one of five signers, or a recipient that was only re-wrapped.
Multi-recipient encryption does not prove who produced the content
Every recipient of a JWE can unwrap the one content-encryption key all recipients share, so any recipient can produce new content that decrypts correctly for all the others. Decrypting a multi-recipient JWE therefore says nothing about which recipient wrote it. Only a signature proves origin: convert a SignedAndEnvelopedData to a nested JWE(JWS), or sign before encrypting, and verify the signature after decrypting.
The header rules and the 422 refusal codes are in the JOSE Technical Reference.
Option 2: Convert to PQC Algorithms
If you need quantum-resistant protection:
For encrypted data (JWE tokens):
POST /api/v3/crypto/reencrypt
{
"jweToken": {
"protected": "eyJhbGciOiJSU0EtT0FFUC01MTIiLCJlbmMiOiJBMjU2R0NNIi...",
"recipients": [ { "encrypted_key": "OKOawDo13gRp..." } ],
"iv": "48V1_ALb6US04U3b",
"ciphertext": "5eym8TW_c8SuK0lt...",
"tag": "XFBoMYUZodetZdvTiFvSkQ"
},
"newKid": "ml-kem-768-key"
}
Output: JWE encrypted with ML-KEM-768 instead of RSA. The source key is read from the token's kid; sourceKid names it when the header carries none.
For signed data (JWS tokens):
POST /api/v3/crypto/resign
{
"jwsToken": {
"payload": "eyJkYXRhIjoiUVc1cmFWTmxZM1Z5WlE...",
"signatures": [
{ "protected": "eyJhbGciOiJQUzI1NiIsImtpZCI6ImZpbmFuY2Utc2lnbmVyLXJzYS0yMDIwIn0", "signature": "kTn9mQ2vXw..." }
]
},
"newKid": "ml-dsa-65-key"
}
Output: JWS signed with ML-DSA-65 instead of RSA. Both requests are abridged (token values truncated); the target algorithm is the algorithm of the newKid key.
Troubleshooting
Error Response Contract
The convert endpoints return a precise 4xx status for client-caused conditions; 500 is reserved for genuinely-unexpected server faults. Client conditions (unresolvable kid/certificate, malformed input, unsupported PKCS#7 feature, oversize payload, wrong or corrupted recipient key) are never reported as 500.
The Endpoint column says which of the two convert endpoints can return each row. The two status sets are not identical: only the compact endpoint enforces a payload ceiling, and only the streaming endpoint is under the streaming concurrency bound. Both reach the key-protection seam that unwraps the actors' keys, so both can answer its 409, 429 and 503.
| HTTP status | Problem type | Endpoint | Condition |
|---|---|---|---|
| 404 | not-found | both | The supplied decryptionKid names a key this tenant does not hold, or one whose entry carries no X.509 certificate. No cross-tenant existence oracle — an unknown and a cross-tenant kid both return 404, with a fixed title and a detail that names no kid. An actor whose tenant key is not found by its certificate is never a 404: it is 422 missing-private-key. |
| 400 | invalid-input / invalid-pkcs7 | both | Malformed request body, a member the body does not define — at its root or inside algorithmPolicy, named in the detail (never its value), validateCertificates, validateTimestamp and contentEncryptionUpgrade included — a serialization or targetFormat outside its vocabulary (exact case), a decryptionKid whose certificate matches none of the recipients (invalid-input), or bytes that are not valid PKCS#7/CMS (invalid-pkcs7) — on the streaming endpoint including a declared ASN.1 length above the structure budget (1 MiB), refused before it is allocated with the body a malformed file receives. decryptionKid is optional: its absence is not an error. |
| 413 | payload-too-large | compact only | The decoded PKCS#7 payload exceeds the configured migration upload limit, migration.max-upload-bytes (default 5 MB). The detail states the effective limit in bytes — the decoded limit, or the JSON body ceiling derived from it when the request body is over that. Use the streaming endpoint for larger files. The streaming endpoint has no payload ceiling and never returns 413. |
| 409 | key-protection-backend-not-configured | both | The environment's key-protection backend is not configured, so no key can be unwrapped. extensions.setupState names the missing setup step. Raised at the key-material seam, before any payload byte. |
| 422 | unsupported-pkcs7-format | both | extensions.errorCode names the condition: INCOMPATIBLE_SERIALIZATION (COMPACT with more than one signer or recipient), ACTOR_LIMIT_EXCEEDED (more than 16 signers and recipients together), UNSUPPORTED_SIGNER_IDENTIFIER (a signer identified by SubjectKeyIdentifier), UNSUPPORTED_RECIPIENT_TYPE (a recipient that is not RSA key transport), LEGACY_ALGORITHM_REJECTED (a SHA-1/MD5 signer without allowLegacyHash), INCOMPATIBLE_TARGET_FORMAT (an explicit targetFormat that does not match the structure), SIGNED_AND_ENVELOPED_DIGEST_NOT_ENCRYPTED and UNSUPPORTED_SIGNED_AND_ENVELOPED_SIGNER_KEY (a SignedAndEnvelopedData signer outside the RFC 2315 §11.2 form), SIGNED_AND_ENVELOPED_WITHOUT_SIGNER (a SignedAndEnvelopedData with no outer signer), or UNSUPPORTED_CONTENT_ENCRYPTION (a SignedAndEnvelopedData content cipher other than AES-CBC or 3DES, or parameters the cipher cannot use; on every structure, RC2/CAST5 parameters outside RFC 2268 §6 / RFC 2984 §2.1 (undefined effective key size, malformed CAST5 parameters)). On the streaming endpoint each is decided before any byte, at every input size. |
| 422 | missing-private-key | both | One or more signers or recipients have no tenant key admissible for what they need — a signer a key that signs, the decrypting recipient a key that decrypts and encrypts, any other recipient a key that encrypts. A certificate imported alone (verify-only) and a restricted key are not admissible. extensions.unresolvedActors[] names every such actor (index, role, requirement, issuerDN, serialNumber), and extensions.errorCode is MISSING_SIGNER_KEY, MISSING_RECIPIENT_PRIVATE_KEY, MISSING_RECIPIENT_KEY, or MISSING_ACTOR_KEYS when they differ. The detail is fixed. Raised before any byte is written, at every input size. See Missing Key. |
| 422 | unprocessable-entity | both | The CMS signature of a signer does not verify (always checked); the detail names the index of the first failing signer. With errorCode AMBIGUOUS_ACTOR_KEY: an actor's certificate is held under more than one tenant key admissible for its role; the detail names the actor's index, issuer and serial, never a kid. |
| 422 | decryption-failed | both | The content could not be decrypted with the decrypting recipient's key — wrong key, or a corrupted encrypted key, encrypted content, content-encryption algorithm or, for SignedAndEnvelopedData, encrypted digest. Every decryption step answers the same status, errorCode (DECRYPTION_FAILED) and fixed detail, so the response never says which step failed. A PKCS #1 v1.5 encrypted key that does not unwrap is replaced by a random key (RFC 3218 §2.3.2) and fails at the content; see Decryption failures and RFC 3218 for what this does and does not guarantee. The body is identical on both endpoints, extensions included (errorCode, possibleCauses, resolution). On the streaming endpoint an EnvelopedData content failure is raised while the output is being emitted: it is this problem document while the response is still uncommitted and an aborted response once it is committed. |
| 403 | forbidden | both | A missing migration.convertPkcs7 scope; a tenant that does not admit cryptographic operations (SUSPENDED, CLOSED) or a SUSPENDED / DISABLED calling Application or Cryptographic Actor (effective-status containment, decided before any grant); a caller that is not a Cryptographic Actor; a calling Actor holding no REENCRYPT / RESIGN grant for what the upload needs, or none from the named decryptionKid (refused before any key is looked up, with no extensions.actor, answered the same whether or not that key exists); or a refused leg of one signer or recipient — by the Capability Grant resolver (REENCRYPT from the opening recipient to every recipient, its own pair included, and RESIGN from every signer to itself; DECRYPT, ENCRYPT and SIGN grants admit no conversion, and a REENCRYPT or RESIGN grant is requested with exactly two assetKids, so N recipients need N-1 pair grants [opener, recipient], a single recipient the in-place grant assetKids: [kid, kid] (stored as [kid]) and each signer RESIGN [signer, signer]; there is no wildcard REENCRYPT or RESIGN grant), or by the tenant's key-lifecycle policy. A refused leg is named in extensions.actor by index, role (signer / recipient) and the refused leg (sign / decrypt / encrypt). No kid is named, and nothing has been decrypted or signed. |
| 429 | key-protection-rate-limited | both | A key-protection rate limit or a cloud KMS quota refused the unwrap — the backend is healthy and deliberately refusing, neither a setup problem nor an outage. Retry-After carries the interval. |
| 429 | streaming-concurrency-limit | streaming | The streaming concurrency bound is full, globally or for the tenant, answered before the upload is read. Retry-After carries the interval. Branch on type. |
| 500 | internal-error | both | Genuinely-unexpected server fault only. |
| 503 | key-protection-backend-unavailable / containment | both | Either the calling Application/Actor effective status could not be verified (the containment gate fails closed), or the key-protection backend is transiently unavailable or misconfigured. |
Streaming endpoint: POST /api/v3/migration/stream/convert-pkcs7-to-jose returns every status above as a response status with an application/problem+json body, decided before the first payload byte — except an EnvelopedData content-decryption failure, which is raised while the output is being emitted and is answered this way only while the response is still uncommitted. A problem document is never written into a committed response body: a failure after the response is committed aborts it instead — see The abort contract.
All error bodies follow RFC 9457 Problem Details (application/problem+json).
Error: "Missing Key for a PKCS#7 Signer or Recipient" (422 missing-private-key)
Cause: One or more signers or recipients have no tenant key admissible for their role
Solution:
- Read
extensions.unresolvedActors[]: each entry names the actor'sindex,role,requirement(sign,decryptorencrypt),issuerDNandserialNumber - Import, through
POST /api/v3/admin/tenants/{tenantId}/keys/import-keystore, a key admissible for the requirement, with its private key:signfor a signer;decrypt(a key that decrypts and encrypts) for the decrypting recipient;encryptfor any other recipient (a keystore import with its private key, or the encryption key of aDUALimport). A certificate imported alone is verify-only and never satisfies any of the three - Retry conversion
An actor listed there that is a third party — a signer or a recipient whose private key you do not hold and cannot import — makes the file not convertible; importing its certificate does not change that. Keep the original PKCS#7 file; see Actors known only by a certificate.
requirement decrypt (errorCode MISSING_RECIPIENT_PRIVATE_KEY when it is the only kind) has two causes. With decryptionKid, the named recipient's key does not admit decryption and encryption, and that recipient is listed. Without decryptionKid, no recipient's key admits both, so there is no decrypting recipient at all, and every recipient is listed with requirement decrypt.
Error: "INCOMPATIBLE_SERIALIZATION" (422 unsupported-pkcs7-format)
Cause: serialization COMPACT was requested for a file with more than one signer or more than one recipient; the Compact serialization carries one of each at most
Solution:
- Use
AUTO(default) orJSONto receive the General JSON serialization
Error: "ACTOR_LIMIT_EXCEEDED" (422 unsupported-pkcs7-format)
Cause: The file carries more than 16 signers and recipients together
Solution:
- Analyze reports the same code as an
ERRORreason, and itslimits.maxActorsstates the bound; the file cannot be converted in one request
Error: "Incompatible targetFormat" (422 unsupported-pkcs7-format, INCOMPATIBLE_TARGET_FORMAT)
Cause: An explicit targetFormat was requested that does not match the parsed PKCS#7 structure — for example JWS for an EnvelopedData message, JWE for SignedData, or JWE_JWS_NESTED for a structure that is not SignedAndEnvelopedData. The compatible mapping is JWS↔SignedData, JWE↔EnvelopedData, JWE_JWS_NESTED↔SignedAndEnvelopedData.
Solution:
- Use
targetFormat: "AUTO"(default) to let the converter detect the output from the structure, or - Set
targetFormatto the value compatible with the analyzed structure — runPOST /api/v3/migration/analyze-pkcs7first to confirm the structure.
Streaming note: The streaming endpoint validates targetFormat and rejects an incompatible value with the same 422, returned as a response status with an application/problem+json body before any payload byte — at every input size, because the structure is detected from the PKCS#7 header alone. Previously the streaming endpoint ignored targetFormat and silently produced the auto-detected output.
Error: "UNSUPPORTED_CONTENT_ENCRYPTION" (422 unsupported-pkcs7-format)
Cause: A SignedAndEnvelopedData names a content cipher other than AES-128/192/256-CBC or 3DES (DES-EDE3-CBC) — RC2 and CAST5 among them — or carries content-encryption parameters the named cipher cannot use. An EnvelopedData in RC2 or CAST5 converts, unless its parameters encode a key size the cipher's RFC does not define: RC2/CAST5 parameters outside RFC 2268 §6 / RFC 2984 §2.1 (undefined effective key size, malformed CAST5 parameters) are refused on every structure, by analysis too, before any key is used.
Solution:
- Analyze reports the unsupported cipher as an
ERRORreason with the same code before you convert - The file cannot be converted as it is; re-encrypt it with AES-CBC or 3DES at its source, or re-export it when the parameters are the problem
- RC2/CAST5 parameters outside their RFC are not reported as a reason: the analysis answers the same
422itself
Error: "decryptionKid matches none of the recipients" (400 invalid-input)
Cause: decryptionKid names a tenant key whose certificate matches none of the file's recipients. decryptionKid is optional: when it is absent, the first recipient in encoded order whose tenant key admits decryption decrypts
Solution:
- Analyze file first to identify the recipients:
POST /api/v3/migration/analyze-pkcs7 - Omit
decryptionKid, or pass the kid of a key whose certificate matches one of the recipients
Error: "Decryption failed" (422 decryption-failed)
Cause: A matching recipient private key was located, but it could not decrypt the PKCS#7 EnvelopedData / SignedAndEnvelopedData content. Typically the private key does not match the public key the file was encrypted to, or the encrypted key, the encrypted content, the content-encryption algorithm or (for SignedAndEnvelopedData) an encrypted digest is corrupted or truncated. There is no fallback to another recipient.
Note on the detail message: Every decryption step answers the same status, the same errorCode (DECRYPTION_FAILED) and the same fixed detail, on both endpoints. It discloses no cryptographic internals (which step failed, algorithm, padding state, or key material). For a PKCS #1 v1.5 recipient the conversion goes further and random-fills the content-encryption key (RFC 3218 §2.3.2), so a key-transport block that does not unwrap fails at the content like a wrong key or a corrupted ciphertext. The guarantee has limits — RSA-OAEP recipients, timing, and the ~1/256 case in which a wrong key yields padded garbage; see Decryption failures and RFC 3218.
Solution:
- Re-run
POST /api/v3/migration/analyze-pkcs7to confirm the recipient'sissuerDNandserialNumber - Export the public key of the tenant key matched to that recipient (
GET /api/v3/admin/tenants/{tenantId}/keys/{kid}/export, scopeadmin.keys.read) and compare itspublicKey(Base64 SubjectPublicKeyInfo) with the public key of the recipient's certificate; the file names that certificate by issuer and serial number only. Select or import the correct key - Retry conversion with the matching
decryptionKid - If the correct key is confirmed, rule out corruption — re-export/re-download the source PKCS#7 file and confirm it was not truncated
Related Documentation
- Analyze Operations - Certificate and compatibility analysis
- Convert Operations - Overview of format and algorithm conversion
- API Reference - Complete endpoint reference
Version: 3.0.0
Last Updated: 2026-09-28
Status: Production Ready