Missing Key for a PKCS#7 Signer or Recipient
URI: https://docs.ankatech.co/errors/missing-private-key
HTTP Status: 422 Unprocessable Entity
When You See This
Your PKCS#7/CMS file is valid and supported, but one or more of its signers or recipients have no key of your tenant admissible for what they need. The conversion resolves every signer and every recipient to a tenant key by the issuer and serial number of the key's certificate, and refuses the request once, naming every actor it could not resolve.
Both convert endpoints return it — POST /api/v3/migration/convert-pkcs7-to-jose and POST /api/v3/migration/stream/convert-pkcs7-to-jose — with the same body.
Decided before any key is used, and before any output on the streaming endpoint
Every actor is resolved before the first private key is used. On the streaming endpoint this refusal is an HTTP 422 with this problem document before the first byte of the conversion output, whatever the size of the upload — a 1 KB file and a 5 GB file behave identically. The signers are resolved in a first pass over the uploaded file, because PKCS#7 places the signer information after the content.
So you will never receive a partial conversion because of a missing key. If the response begins, every key was found.
What Each Actor Needs
| Actor | requirement | Key the tenant must hold |
|---|---|---|
| A signer | sign | a key admissible to sign: its private key imported with its certificate — the conversion re-signs with it |
| The recipient that decrypts | decrypt | a key admissible to decrypt and encrypt, imported with its private key: it opens the content, and the new content-encryption key is wrapped to it too |
| Any other recipient | encrypt | a key admissible to encrypt: a keystore import with its private key, or the encryption key of a DUAL import |
The recipient that decrypts is the one decryptionKid names or, when decryptionKid is absent, the first recipient in encoded order whose tenant key admits both decryption and encryption.
A certificate imported alone is a verify-only trust anchor (key_ops ["verify"]) and satisfies none of the three requirements, so an actor held only as a certificate is listed here; see When the Actor Is a Third Party. The same holds for a restricted key — one imported under IMPORT_ONLY whose certificate, or a CA certificate in its chain, has expired, or whose chain failed PKIX validation: a restricted key is read-only, never signs and is never encrypted to.
Response Example
{
"type": "https://docs.ankatech.co/errors/missing-private-key",
"title": "Missing Key for a PKCS#7 Signer or Recipient",
"status": 422,
"detail": "One or more signers or recipients of the PKCS#7 structure have no key of this tenant 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. Each one is listed in unresolvedActors with its requirement; import through importEndpoint a key admissible for it, with its private part (a certificate imported alone only verifies), and retry.",
"instance": "/api/v3/migration/convert-pkcs7-to-jose",
"correlationId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"timestamp": 1768472400,
"extensions": {
"errorCode": "MISSING_ACTOR_KEYS",
"unresolvedActors": [
{ "index": 1, "role": "signer", "requirement": "sign", "issuerDN": "CN=Legacy Legal Signer,O=Example Corp,C=US", "serialNumber": "0x1B2C3D4E5F607182" },
{ "index": 2, "role": "recipient", "requirement": "encrypt", "issuerDN": "CN=Records Office,O=Example Corp,C=US", "serialNumber": "0x5A6B7C8D9E0F1021" }
],
"importEndpoint": "POST /api/v3/admin/tenants/{tenantId}/keys/import-keystore"
}
}
The detail is a fixed string: nothing about the request or the server is interpolated into it. Read the actors from extensions:
| Member | Meaning |
|---|---|
errorCode | See the table below. |
unresolvedActors[] | Every actor without a key, in encoded order. index is its zero-based position in its own set (SignerInfos or RecipientInfos), role is signer or recipient, requirement is sign, decrypt or encrypt, and issuerDN / serialNumber are the values the file carries. An over-long DN or serial is cut and ends with …. |
importEndpoint | The operation that imports the missing keys. |
No kid or alias of any tenant appears in the body, so a key held by another tenant and a key held by nobody produce the same response.
Error Codes
| Code | When |
|---|---|
MISSING_SIGNER_KEY | Every unresolved actor is a signer |
MISSING_RECIPIENT_PRIVATE_KEY | Every unresolved actor has requirement decrypt. With decryptionKid, the named recipient's key does not admit decryption and encryption. Without it, no recipient's key admits both, so there is no decrypting recipient, and every recipient is listed with requirement decrypt |
MISSING_RECIPIENT_KEY | Every unresolved actor is a recipient other than the decrypting one, without a key admissible to encrypt |
MISSING_ACTOR_KEYS | The unresolved actors do not share one of the three codes above |
The codes are the same on both convert endpoints. A client may branch on errorCode without knowing which endpoint it called; unresolvedActors[] is the complete answer in every case.
How to Resolve
Step 1: Analyze Your PKCS#7 File
POST /api/v3/migration/analyze-pkcs7 lists every key a file needs, before any conversion is attempted:
{
"recommendations": {
"requiredKeys": [
{
"purpose": "encryption",
"issuerDN": "CN=John Doe,O=AnkaTech,C=US",
"serialNumber": "0x4A3F2E1D5B6C7A8B",
"algorithm": "RSA",
"importHint": "Use POST /api/v3/admin/tenants/{tenantId}/keys/import-keystore to import the private key and certificate of this recipient (a certificate imported alone only verifies and is not admissible as a recipient)"
}
],
"decryptionRequirement": "ONE_RECIPIENT_PRIVATE_KEY"
}
}
purpose is signing for a signer and encryption for a recipient; decryptionRequirement says that one recipient's private key is needed as well.
Step 2: Import the Required Keys
Import the PKCS#12 file containing the private key and certificate — for every requirement, encrypt included. A certificate imported alone only verifies:
curl -sS -X POST \
"$ANKA_BASE_URL/api/v3/admin/tenants/$TENANT_ID/keys/import-keystore" \
-H "Authorization: Bearer $TOKEN" \
-F 'metadata={"kid":"recipient-key","kidStrategy":"AUTO","validationMode":"STRICT"};type=application/json' \
-F "[email protected]" \
-F "password=$KEYSTORE_PASSWORD"
The operation takes multipart/form-data, not a JSON body: the keystore is sent as a file part rather than base64-encoded into a field.
Step 3: Retry Conversion
decryptionKid is optional; omit it to let the conversion pick the first recipient whose key admits decryption:
POST /api/v3/migration/convert-pkcs7-to-jose
Content-Type: application/json
{
"pkcs7Data": "<base64-encoded-pkcs7>",
"serialization": "AUTO",
"targetFormat": "AUTO"
}
When the Actor Is a Third Party
Some actors cannot be resolved by importing anything: a counterparty that co-signed the file, or an external addressee the content was also encrypted to, whose certificate you hold but whose private key you never will. Such a file is not convertible, and importing the certificate does not change that:
- A signer is re-signed under its own key, which needs its private key. A certificate alone is a verify-only trust anchor (
key_ops["verify"]): it checks a signature, it cannot produce one. - A recipient receives a new content-encryption key wrapped under its tenant key, and a certificate imported alone is never encrypted to. A file with no recipient of your own cannot be opened at all.
Step 1 tells you before you convert: every actor is in recommendations.requiredKeys, and one you cannot hold a private key for will reappear in this refusal's unresolvedActors[]. 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. See Actors known only by a certificate.
Certificate Matching Rules
An actor matches a tenant key when the key's certificate carries the same issuer and serial number:
-
Issuer Distinguished Name — compared as a parsed X.500 name in its RFC 2253 canonical form: insensitive to case and insignificant whitespace in string values and to the order of the members of one multi-valued RDN, but sensitive to the order of the RDNs themselves, which X.501 defines as significant.
-
Serial Number — compared as an integer, not as a string.
A signer additionally resolves only to a key whose certificate carries the same public key as the certificate that verified its CMS signature.
Not This Error
- A
decryptionKidwhose certificate matches none of the recipients →400invalid-input - A
decryptionKidthe tenant does not hold →404not-found - A key that was found but could not decrypt the content →
422decryption-failed - A key that was found but the tenant's key-lifecycle policy refuses for its leg →
403forbidden, withextensions.actornaming the actor - An actor whose certificate is held under more than one tenant key admissible for its role →
422unprocessable-entitywitherrorCodeAMBIGUOUS_ACTOR_KEY; thedetailnames the actor's index, issuer and serial, never a kid. Remove the duplicate import.
Best Practices
- Always analyze before converting - Use
POST /api/v3/migration/analyze-pkcs7first - Import with certificate chain - Ensure PKCS#12 includes full chain
- Use descriptive kids - Name keys to indicate purpose (e.g., "legacy-recipient-2024")
- Keep certificates valid - Use
STRICTvalidation mode in production; a key imported restricted (an expired certificate or a failed chain underIMPORT_ONLY) is not admissible to any actor