Skip to content

Flow 20 – PKCS#7 → JOSE Migration

Converts a legacy PKCS#7/CMS EnvelopedData artifact to a modern JOSE JWE. Unlike the keyless analysis in Flow 19, converting EnvelopedData requires the platform to actually decrypt the CMS envelope and re-wrap it — so it needs the recipient's private key, referenced by its platform key id.

Where the recipient key comes from (control-plane prerequisite):

  • The recipient key is matched by issuer DN + serial number, not by algorithm — so it cannot be discovered from the algorithm catalogue.
  • Importing that key from a legacy keystore is a control-plane operation handled outside the data-plane SDK (an admin-scoped actor on the admin migration surface).
  • This data-plane example therefore reads the resulting key id from the explicit pkcs7.decryptionKid property and skips cleanly (before any server call) if it is absent.
  • The authenticated actor needs the in-place REENCRYPT grant on that kid, requested with the kid twice (assetKids: [kid, kid]) and stored as [kid]; a pair grant [kid, other-kid] also covers it. There is no wildcard REENCRYPT grant. A conversion is authorized by REENCRYPT / RESIGN grants, never by DECRYPT — see the Operation → Capability Grant Matrix.

Steps:

  1. Resolve the recipient key id from the pkcs7.decryptionKid property (skip cleanly when blank)
  2. Compact convert: convertPkcs7ToJose (AUTO format → EnvelopedData becomes JWE)
  3. Streaming convert: convertPkcs7ToJoseStream to a JWE output file
  4. End-to-end: performMigrationWorkflow (analyze + convert in one call)

Target-format contract:

TargetFormatEnum.AUTO detects the structure: EnvelopedData → JWE, SignedData → JWS, both → nested. Forcing JWS on EnvelopedData is refused 422 with errorCode INCOMPATIBLE_TARGET_FORMAT (there is no signature to convert); this example uses AUTO / JWE. The fixture has one recipient, so serialization AUTO yields the Compact JWE; a file with several recipients yields the General JSON JWE, one entry per recipient.

API Endpoints:

  • POST /api/v3/migration/convert-pkcs7-to-jose (compact conversion)
  • POST /api/v3/migration/stream/convert-pkcs7-to-jose (streaming conversion)

When to use:

  • Migrating legacy encrypted CMS payloads to JWE as part of a crypto-agility programme
  • Re-wrapping archived EnvelopedData under a modern, post-quantum-capable JOSE envelope
  • Driving an end-to-end analyze-then-convert step over a batch of legacy files

Dependency — this example imports co.ankatech.ankasecure.sdk.examples.ExampleUtil and co.ankatech.ankasecure.sdk.examples.Pkcs7ExampleFixture. If you have not copied those classes yet, see example-util.md.


Prerequisites

The flow examples are data-plane only. They never create, rotate, or delete keys — they resolve pre-provisioned playground keys and operate on them. Before running any flow example you must:

  1. Provision a fresh demo-cli playground. Use the ankasecure-demo-provisioning tool to provision the demo-cli playground. This seeds the cryptographic keys the examples operate on and the cli-reference@demo-cli actor that holds capability grants on them.
  2. Use the emitted cli.properties. The provisioning tool writes a cli.properties file that carries the ankasecure.demo.kids catalogue line — the comma-separated list of provisioned key ids, in YAML file order. The examples load this file to discover which keys exist.
  3. Authenticate as cli-reference@demo-cli. Authenticate the SDK as the cli-reference@demo-cli actor — the all-operations actor of the demo-cli playground — so the resolved keys carry the capability grants each operation needs.

Key selection in the examples is by algorithm or by exact name:

  • kidForAlgorithm(alg) returns a pre-provisioned key id matching the requested algorithm — used by the single-key operation flows.
  • requireKid(name) asserts that an exact, named cross-kid endpoint is present in the ankasecure.demo.kids catalogue — used by the cross-kid REENCRYPT / RESIGN flows that must operate on a specific granted source/target pair.

If cli.properties is missing the ankasecure.demo.kids line, the examples fail fast with a clear message instructing you to (re-)provision the demo-cli playground — they will not silently fall back.

PKCS#7 / CMS examples (Flow 19 & Flow 20)

The PKCS#7 interop examples operate on a packaged, non-sensitive CMS EnvelopedData fixture — you do not supply a PKCS#7 file:

  • Fixture: src/main/resources/pkcs7/enveloped-data-sample.p7m, loaded from the classpath by Pkcs7ExampleFixture. It is a single-recipient, zero-signer EnvelopedData artifact from the QA test signer; it carries no private key and no secret.
  • Flow 19 (analysis) needs no key. Structural analysis (analyzePkcs7 / analyzePkcs7Stream) inspects the CMS envelope metadata only, so it runs with no decryption key and no extra configuration.
  • Flow 20 (conversion) needs a pre-provisioned recipient key. Converting EnvelopedData to JWE requires the platform to decrypt the envelope, so it needs the recipient's private key referenced by the pkcs7.decryptionKid property in cli.properties. That key is matched by issuer DN + serial number and is imported by a control-plane step (outside the data-plane SDK); until it is provisioned, Flow 20's conversion calls fail at the server while Flow 19 still runs. The calling actor also needs the in-place REENCRYPT grant on that kid, requested with the kid twice (assetKids: [kid, kid]) and stored as [kid]: a conversion is authorized by REENCRYPT / RESIGN grants, never by DECRYPT, and no wildcard REENCRYPT grant exists.
  • Data-plane only. Like every flow example, these never import keystores or perform any key-lifecycle operation.

Complete Java Implementation

Source: src/main/java/co/ankatech/ankasecure/sdk/examples/ExampleScenario20.java

/*
 * Copyright 2025 ANKATech Solutions Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package co.ankatech.ankasecure.sdk.examples;

import co.ankatech.ankasecure.sdk.AuthenticatedSdk;
import co.ankatech.ankasecure.sdk.model.MigrationWorkflowResult;
import co.ankatech.secure.client.model.Pkcs7ConversionRequest;
import co.ankatech.secure.client.model.Pkcs7ConversionResponse;
import co.ankatech.secure.client.model.Pkcs7ConversionStreamRequest;

import java.io.File;
import java.util.Properties;

import static co.ankatech.ankasecure.sdk.examples.ExampleUtil.TEMP_DIR;
import static co.ankatech.ankasecure.sdk.examples.ExampleUtil.authenticate;
import static co.ankatech.ankasecure.sdk.examples.ExampleUtil.fatal;
import static co.ankatech.ankasecure.sdk.examples.ExampleUtil.loadProperties;

/**
 * Scenario 20 — PKCS#7 → JOSE Conversion / Migration (data-plane).
 *
 * <p>Converts a legacy PKCS#7/CMS <em>EnvelopedData</em> artifact to a modern JOSE JWE.
 * Unlike the keyless analysis in {@link ExampleScenario19}, conversion of EnvelopedData
 * requires the platform to actually <em>decrypt</em> the CMS envelope and re-wrap it &mdash;
 * so it needs the recipient's private key, referenced by its platform key id.</p>
 *
 * <h3>Where the recipient key comes from (control-plane prerequisite)</h3>
 * <ul>
 *   <li>The recipient key is matched by <strong>issuer&nbsp;DN + serial number</strong>, NOT by
 *       algorithm &mdash; so it cannot be discovered from the algorithm catalogue.</li>
 *   <li>Importing that key from a legacy keystore is a <strong>control-plane</strong> operation
 *       handled outside the data-plane SDK (the admin migration surface, an admin-scoped actor).</li>
 *   <li>This data-plane example therefore reads the resulting key id from the explicit
 *       {@code pkcs7.decryptionKid} property and skips cleanly (before any server call) if it is absent.</li>
 * </ul>
 *
 * <h3>Capability Grants the calling actor needs (control-plane prerequisite)</h3>
 * <p>The {@code migration.convertPkcs7} scope admits the route; the keys are admitted by the calling
 * Cryptographic Actor's Capability Grants, which the platform decides per leg with the
 * re-operation capabilities, never with ENCRYPT, DECRYPT or SIGN:</p>
 * <ul>
 *   <li><strong>{@code REENCRYPT}</strong> over {@code [decryptionKid, recipient]} for every recipient of
 *       the EnvelopedData, the decrypting key's own pair included. A REENCRYPT/RESIGN grant is requested
 *       with exactly two {@code assetKids} and admits any request whose kids it contains. This
 *       single-recipient fixture needs the in-place grant, requested with the kid twice
 *       ({@code assetKids: [decryptionKid, decryptionKid]}) and stored as {@code [decryptionKid]}; N
 *       recipients need a pair grant {@code [decryptionKid, recipient]} to each other recipient. There
 *       is no wildcard re-operation grant.</li>
 *   <li><strong>{@code RESIGN}</strong> over each signer's in-place pair ({@code assetKids:
 *       [signer, signer]}), for a SignedData or SignedAndEnvelopedData input.</li>
 * </ul>
 * <p>The tenant, the Application and the Actor must be active. A grant is created on the control plane
 * ({@code POST /api/v3/admin/tenants/{tenantId}/capability-grants}), outside this SDK; an actor without
 * it is refused with HTTP&nbsp;403 before any key is resolved.</p>
 *
 * <h3>Steps:</h3>
 * <ol>
 *   <li>Resolve the recipient key id from the {@code pkcs7.decryptionKid} property (skips cleanly when blank)</li>
 *   <li>Compact convert: {@code convertPkcs7ToJose} (AUTO format &rarr; EnvelopedData becomes JWE)</li>
 *   <li>Streaming convert: {@code convertPkcs7ToJoseStream} to a JWE output file</li>
 *   <li>End-to-end: {@code performMigrationWorkflow} (analyze + convert in one call)</li>
 * </ol>
 *
 * <h3>Target-format contract</h3>
 * <p>{@code TargetFormatEnum.AUTO} detects the structure: EnvelopedData &rarr; JWE, SignedData
 * &rarr; JWS, both &rarr; nested. Forcing {@code JWS} on EnvelopedData is a 400 contract error
 * (there is no signature to convert); this example uses AUTO / JWE.</p>
 *
 * <h3>API Endpoints:</h3>
 * <ul>
 *   <li>POST /api/v3/migration/convert-pkcs7-to-jose (compact conversion)</li>
 *   <li>POST /api/v3/migration/stream/convert-pkcs7-to-jose (streaming conversion)</li>
 * </ul>
 *
 * <p><strong>Prerequisite:</strong> a {@code cli.properties} for a data-plane actor holding the
 * {@code REENCRYPT} grant above, plus a {@code pkcs7.decryptionKid} entry naming a recipient key already
 * provisioned on the platform. Until that key and grant exist the conversion calls are refused by the
 * server; the analysis counterpart {@link ExampleScenario19} needs no key and runs unconditionally.</p>
 *
 * @see ExampleUtil
 * @see Pkcs7ExampleFixture
 * @see ExampleScenario19
 * @see AuthenticatedSdk
 */
public final class ExampleScenario20 {

    /** Property naming the pre-provisioned recipient key id used to decrypt the EnvelopedData. */
    private static final String DECRYPTION_KID_PROPERTY = "pkcs7.decryptionKid";

    /** No instantiation &mdash; this class only exposes {@link #main(String[])}. */
    private ExampleScenario20() { }

    /**
     * Runs the PKCS#7 &rarr; JOSE conversion / migration scenario.
     *
     * @param args command-line arguments (ignored)
     */
    public static void main(String[] args) {
        System.out.println("===== SCENARIO 20: PKCS#7 -> JOSE CONVERSION / MIGRATION =====");
        System.out.println("Purpose: Convert legacy EnvelopedData to a modern JWE (decrypt + re-wrap)");
        System.out.println("Modes  : compact + streaming convert, plus performMigrationWorkflow");
        System.out.println("Key    : recipient key id from the 'pkcs7.decryptionKid' property");
        System.out.println();

        try {
            ExampleUtil.prepareWorkingDir();

            Properties props = loadProperties();
            String recipientKid = props.getProperty(DECRYPTION_KID_PROPERTY);
            if (recipientKid == null || recipientKid.isBlank()) {
                // Deferred state: skip cleanly (no throw) so the aggregate runners are not aborted.
                printDeferredSkip();
                System.out.println("===== SCENARIO 20 END =====");
                return;
            }
            AuthenticatedSdk sdk = authenticate(props);
            runScenario(sdk, recipientKid.trim());

            System.out.println("===== SCENARIO 20 END =====");

        } catch (Exception ex) {
            fatal("Scenario 20 failed", ex);
        }
    }

    /**
     * Prints an actionable "deferred / skipped" notice when the recipient key id is not configured.
     *
     * <p>Conversion of EnvelopedData needs the recipient's private key (provisioned by a
     * control-plane step outside the data-plane SDK). When {@code pkcs7.decryptionKid} is absent the
     * scenario skips cleanly &mdash; it does NOT throw &mdash; so it can sit in the aggregate runners
     * (such as {@code ExampleComplete}) without aborting them before the
     * recipient key is provisioned. Analysis-only {@link ExampleScenario19} needs no key.</p>
     */
    private static void printDeferredSkip() {
        System.out.println("[skipped] Property '" + DECRYPTION_KID_PROPERTY + "' is not set.");
        System.out.println("          Conversion of EnvelopedData to JWE requires the platform to");
        System.out.println("          decrypt the CMS envelope with the recipient's private key.");
        System.out.println("          Provision that recipient key (a control-plane step, outside the");
        System.out.println("          data-plane SDK), grant this actor REENCRYPT with assetKids");
        System.out.println("          [<key id>, <key id>] (the in-place pair; the conversion is refused");
        System.out.println("          403 without it; ENCRYPT/DECRYPT do not admit it), then set "
                + DECRYPTION_KID_PROPERTY
                + "=<key id> in cli.properties to run the conversion.");
    }

    private static void runScenario(final AuthenticatedSdk sdk, final String recipientKid) throws Exception {

        final File pkcs7File = Pkcs7ExampleFixture.materializeToTempFile();
        System.out.println("Recipient key id (from " + DECRYPTION_KID_PROPERTY + "): " + recipientKid);
        System.out.println();

        // ============================================================
        // PHASE 1: Compact conversion — AUTO target (EnvelopedData -> JWE)
        // ============================================================

        System.out.println("[Step 1/3] Compact conversion (convertPkcs7ToJose), AUTO -> JWE...");
        Pkcs7ConversionRequest compactRequest = new Pkcs7ConversionRequest()
                .pkcs7Data(Pkcs7ExampleFixture.envelopedDataBase64())
                .decryptionKid(recipientKid)
                .targetFormat(Pkcs7ConversionRequest.TargetFormatEnum.AUTO);
        Pkcs7ConversionResponse compact = sdk.convertPkcs7ToJose(compactRequest);
        System.out.println("           Output structure : " + compact.getStructure());
        System.out.println("           Serialization    : " + compact.getSerialization());
        System.out.println("           JOSE token       : " + previewToken(compact.getJoseToken()));
        if (compact.getWarnings() != null && !compact.getWarnings().isEmpty()) {
            System.out.println("           Warnings         : " + compact.getWarnings());
        }
        System.out.println();

        // ============================================================
        // PHASE 2: Streaming conversion — JWE output file
        // ============================================================

        System.out.println("[Step 2/3] Streaming conversion (convertPkcs7ToJoseStream)...");
        final File joseStreamOut = TEMP_DIR.resolve("scenario20_converted.jwe").toFile();
        Pkcs7ConversionStreamRequest streamRequest = new Pkcs7ConversionStreamRequest()
                .decryptionKid(recipientKid)
                .targetFormat(Pkcs7ConversionStreamRequest.TargetFormatEnum.JWE);
        File streamResult = sdk.convertPkcs7ToJoseStream(streamRequest, pkcs7File, joseStreamOut);
        System.out.println("           JWE written to   : " + streamResult);
        System.out.println();

        // ============================================================
        // PHASE 3: End-to-end migration workflow (analyze + convert)
        // ============================================================

        System.out.println("[Step 3/3] End-to-end migration (performMigrationWorkflow)...");
        final File workflowOut = TEMP_DIR.resolve("scenario20_workflow.jwe").toFile();
        Pkcs7ConversionStreamRequest workflowRequest = new Pkcs7ConversionStreamRequest()
                .decryptionKid(recipientKid)
                .targetFormat(Pkcs7ConversionStreamRequest.TargetFormatEnum.AUTO);
        MigrationWorkflowResult result = sdk.performMigrationWorkflow(pkcs7File, workflowRequest, workflowOut);

        // Structured accessors on the workflow result (analysis + JOSE output).
        if (result.getAnalysis() != null && result.getAnalysis().getDetectedFormat() != null) {
            System.out.println("           Analyzed as      : "
                    + result.getAnalysis().getDetectedFormat().getStructure());
        }
        System.out.println("           Requires import  : " + result.requiresKeyImport());
        System.out.println("           JOSE output file : " + result.getJoseOutput());
        System.out.println();

        System.out.println("╔═══════════════════════════════════════════════════════════════╗");
        System.out.println("║               ✅ SCENARIO 20 SUCCESSFUL                        ║");
        System.out.println("║  Legacy EnvelopedData migrated to JWE (compact + stream + flow)║");
        System.out.println("╚═══════════════════════════════════════════════════════════════╝");
    }

    /** Returns a short, non-sensitive preview of a JOSE compact token for console output. */
    private static String previewToken(final String token) {
        if (token == null) {
            return "(none)";
        }
        return token.length() <= 32 ? token : token.substring(0, 32) + "...";
    }
}

Fixture loader: src/main/java/co/ankatech/ankasecure/sdk/examples/Pkcs7ExampleFixture.java

/*
 * Copyright 2026 ANKATech Solutions Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package co.ankatech.ankasecure.sdk.examples;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Base64;

/**
 * Classpath loader for the PKCS#7/CMS example fixture used by the data-plane
 * migration scenarios ({@link ExampleScenario19}, {@link ExampleScenario20}).
 *
 * <p>The fixture {@code /pkcs7/enveloped-data-sample.p7m} is a small, non-sensitive
 * BER/DER-encoded CMS <em>EnvelopedData</em> structure (single recipient, zero signers)
 * produced by a test signer. It carries no private key material and no secret;
 * its encrypted payload is a throwaway test value. It lives under
 * {@code src/main/resources} so the examples are self-contained
 * (the reader does not need to supply a PKCS#7 file to follow the walkthrough).</p>
 *
 * <p>Loading the recipient's private key — required to actually <em>decrypt</em> and
 * convert the EnvelopedData to JWE — is a control-plane concern handled outside the
 * data-plane SDK (see {@link ExampleScenario20}); this helper only supplies the
 * ciphertext to analyze/convert.</p>
 *
 * @see ExampleScenario19
 * @see ExampleScenario20
 */
public final class Pkcs7ExampleFixture {

    /** Classpath location of the packaged EnvelopedData sample. */
    private static final String RESOURCE = "/pkcs7/enveloped-data-sample.p7m";

    /** Temp-file name used by {@link #materializeToTempFile()} for the streaming APIs. */
    private static final String TEMP_FILE_NAME = "enveloped-data-sample.p7m";

    /** No instantiation &mdash; static helper only. */
    private Pkcs7ExampleFixture() { }

    /**
     * Loads the raw DER bytes of the packaged EnvelopedData fixture from the classpath.
     *
     * @return the fixture bytes (never {@code null} or empty)
     * @throws IllegalStateException if the resource is missing from the classpath
     * @throws UncheckedIOException  if the resource cannot be read
     */
    public static byte[] envelopedDataBytes() {
        try (InputStream in = Pkcs7ExampleFixture.class.getResourceAsStream(RESOURCE)) {
            if (in == null) {
                throw new IllegalStateException(
                        "PKCS#7 example fixture not found on the classpath: " + RESOURCE
                        + " — ensure src/main/resources/pkcs7/enveloped-data-sample.p7m is packaged.");
            }
            return in.readAllBytes();
        } catch (IOException e) {
            throw new UncheckedIOException("Failed to read PKCS#7 example fixture " + RESOURCE, e);
        }
    }

    /**
     * Returns the fixture as a Base64 string, suitable for the compact-mode request
     * fields {@code Pkcs7AnalysisRequest.pkcs7Data} / {@code Pkcs7ConversionRequest.pkcs7Data}.
     *
     * @return Base64-encoded fixture bytes
     */
    public static String envelopedDataBase64() {
        return Base64.getEncoder().encodeToString(envelopedDataBytes());
    }

    /**
     * Materializes the fixture to a temporary {@link File} under
     * {@link ExampleUtil#TEMP_DIR}, for the streaming APIs that take a {@code java.io.File}.
     *
     * @return a readable temp {@code File} containing the fixture bytes
     * @throws UncheckedIOException if the temp file cannot be written
     */
    public static File materializeToTempFile() {
        try {
            ExampleUtil.ensureTempDir();
            Path target = ExampleUtil.TEMP_DIR.resolve(TEMP_FILE_NAME);
            Files.write(target, envelopedDataBytes());
            return target.toFile();
        } catch (IOException e) {
            throw new UncheckedIOException("Failed to materialize PKCS#7 example fixture to a temp file", e);
        }
    }
}

Running This Example

Set the pre-provisioned recipient key id in cli.properties first:

pkcs7.decryptionKid=<the recipient key id provisioned for the EnvelopedData fixture>
# Compile
javac -cp "ankasecure-sdk-3.4.0.jar:." ExampleScenario20.java

# Run
java -cp "ankasecure-sdk-3.4.0.jar:." co.ankatech.ankasecure.sdk.examples.ExampleScenario20

Expected Output (once the recipient key is provisioned and pkcs7.decryptionKid is set):

===== SCENARIO 20: PKCS#7 -> JOSE CONVERSION / MIGRATION =====
Purpose: Convert legacy EnvelopedData to a modern JWE (decrypt + re-wrap)
Modes  : compact + streaming convert, plus performMigrationWorkflow
Key    : recipient key id from the 'pkcs7.decryptionKid' property

Recipient key id (from pkcs7.decryptionKid): pkcs7-recipient-rsa

[Step 1/3] Compact conversion (convertPkcs7ToJose), AUTO -> JWE...
           Output structure : ENVELOPED_DATA
           Serialization    : COMPACT
           JOSE token       : eyJhbGciOiJSU0EtT0FFUC01MTIiLCJlbmMi...

[Step 2/3] Streaming conversion (convertPkcs7ToJoseStream)...
           JWE written to   : temp_files/scenario20_converted.jwe

[Step 3/3] End-to-end migration (performMigrationWorkflow)...
           Analyzed as      : ENVELOPED_DATA
           Requires import  : false
           JOSE output file : temp_files/scenario20_workflow.jwe

╔═══════════════════════════════════════════════════════════════╗
║               ✅ SCENARIO 20 SUCCESSFUL                        ║
║  Legacy EnvelopedData migrated to JWE (compact + stream + flow)║
╚═══════════════════════════════════════════════════════════════╝
===== SCENARIO 20 END =====

If pkcs7.decryptionKid is not set, the example skips cleanly — before any server call — so it never aborts the aggregate runners (ExampleScenariosE2ETest / "Run All") while the recipient key is still being provisioned:

[skipped] Property 'pkcs7.decryptionKid' is not set.
          Conversion of EnvelopedData to JWE requires the platform to
          decrypt the CMS envelope with the recipient's private key.
          Provision that recipient key (a control-plane step, outside the
          data-plane SDK), then set pkcs7.decryptionKid=<key id> in cli.properties to run the conversion.
===== SCENARIO 20 END =====

Key Concepts

Why conversion needs a key but analysis does not

Flow 19 reads only the CMS envelope metadata, so it needs no key. Conversion of EnvelopedData produces a JWE, which means the platform must decrypt the original envelope (using the recipient's private key) and re-encrypt the recovered content as a JWE. On the wire decryptionKid is optional — without it the first recipient in encoded order whose tenant key admits decryption and encryption decrypts — but this example requires pkcs7.decryptionKid so that it skips cleanly, before any server call, while the recipient key has not been provisioned.

Recipient matching

The recipient is identified by issuer DN + serial number embedded in the CMS, not by algorithm. The example therefore never derives the key id from the algorithm catalogue — it reads an explicit, pre-provisioned pkcs7.decryptionKid.

The migration workflow result

performMigrationWorkflow returns a MigrationWorkflowResult exposing the analysis (getAnalysis()), the written JOSE output file (getJoseOutput()), and a requiresKeyImport() flag — the example prints these through their real accessors.