Skip to content

Flow 19 – PKCS#7/CMS Structural Analysis

Demonstrates the keyless inspection side of the PKCS#7 migration surface: the SDK reports the structure of a legacy PKCS#7/CMS artifact — format, recipients, signers, content-encryption parameters, support status, and the recommended JOSE migration path — without any decryption key. Structural analysis never touches key material, so it is the safe first step when assessing a body of legacy CMS files for migration.

Why analysis needs no key:

  • Envelope metadata only: the CMS header exposes the content type, recipient and signer descriptors, and the content-encryption algorithm in the clear.
  • No key and no option: analyzePkcs7 takes the Base64 file only and analyzePkcs7Stream the file only — analysis decrypts nothing and validates nothing, so the structure report never depends on a key.
  • Data-plane only: no keystore import, no key provisioning — those are control-plane concerns handled outside the SDK (see Flow 20).

Steps:

  1. Load the packaged, non-sensitive EnvelopedData fixture (single recipient, zero signers)
  2. Compact analysis: analyzePkcs7 over the Base64 of the fixture (no key)
  3. Streaming analysis: analyzePkcs7Stream over the same fixture materialized to a file
  4. Print the structured report and confirm the two modes agree on the detected structure

API Endpoints:

  • POST /api/v3/migration/analyze-pkcs7 (compact analysis)
  • POST /api/v3/migration/stream/analyze-pkcs7 (streaming analysis)

When to use:

  • Inventorying a corpus of legacy .p7m / .p7s files before a migration project
  • Deciding the target JOSE format (the response recommends JWE/JWS per structure)
  • Confirming which recipient/signer keys a later conversion will require — without holding any key yet

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/ExampleScenario19.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.secure.client.model.Pkcs7AnalysisRequest;
import co.ankatech.secure.client.model.Pkcs7AnalysisResponse;
import co.ankatech.secure.client.model.RecipientMetadata;
import co.ankatech.secure.client.model.SignerMetadata;

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

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 19 — PKCS#7/CMS Structural Analysis (data-plane).
 *
 * <p>Demonstrates the keyless inspection side of the PKCS#7 migration surface: the SDK
 * reports the structure of a legacy PKCS#7/CMS artifact (format, recipients, signers,
 * encryption parameters, support status and the recommended JOSE migration path) WITHOUT
 * any decryption key. Structural analysis never touches key material, so it is the safe
 * first step when assessing a body of legacy CMS files for migration.</p>
 *
 * <h3>Why analysis needs no key</h3>
 * <ul>
 *   <li><strong>Envelope metadata only:</strong> the CMS header exposes content type,
 *       recipient and signer descriptors, and the content-encryption algorithm in the clear.</li>
 *   <li><strong>The recipient-key id is optional:</strong> supplying one would let the platform
 *       additionally validate decryptability, but the structure report stands on its own.</li>
 *   <li><strong>Data-plane only:</strong> no keystore import, no key provisioning &mdash; those
 *       are control-plane concerns handled outside the SDK (see {@link ExampleScenario20}).</li>
 * </ul>
 *
 * <h3>Steps:</h3>
 * <ol>
 *   <li>Load the packaged, non-sensitive EnvelopedData fixture (single recipient, zero signers)</li>
 *   <li>Compact analysis: {@code analyzePkcs7} over the Base64 of the fixture (no key)</li>
 *   <li>Streaming analysis: {@code analyzePkcs7Stream} over the same fixture materialized to a file</li>
 *   <li>Print the structured report and confirm the two modes agree on the detected structure</li>
 * </ol>
 *
 * <h3>API Endpoints:</h3>
 * <ul>
 *   <li>POST /api/v3/migration/analyze-pkcs7 (compact analysis)</li>
 *   <li>POST /api/v3/migration/stream/analyze-pkcs7 (streaming analysis)</li>
 * </ul>
 *
 * <p><strong>Prerequisite:</strong> a {@code cli.properties} for a data-plane actor of the
 * provisioned playground. No pre-provisioned key and no recipient-key id are required for
 * analysis &mdash; the conversion counterpart that needs a recipient key is
 * {@link ExampleScenario20}.</p>
 *
 * @see ExampleUtil
 * @see Pkcs7ExampleFixture
 * @see ExampleScenario20
 * @see AuthenticatedSdk
 */
public final class ExampleScenario19 {

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

    /**
     * Runs the PKCS#7/CMS structural-analysis scenario.
     *
     * @param args command-line arguments (ignored)
     */
    public static void main(String[] args) {
        System.out.println("===== SCENARIO 19: PKCS#7/CMS STRUCTURAL ANALYSIS =====");
        System.out.println("Purpose: Inspect a legacy PKCS#7/CMS artifact WITHOUT any decryption key");
        System.out.println("Modes  : compact (analyzePkcs7) + streaming (analyzePkcs7Stream)");
        System.out.println("Key    : none — analysis is key-independent (data-plane)");
        System.out.println();

        try {
            Properties props = loadProperties();
            AuthenticatedSdk sdk = authenticate(props);
            runScenario(sdk);

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

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

    private static void runScenario(AuthenticatedSdk sdk) throws Exception {

        // ============================================================
        // PHASE 1: Compact analysis — Base64 payload, no key
        // ============================================================

        System.out.println("[Step 1/3] Compact analysis (analyzePkcs7), no decryption key...");
        Pkcs7AnalysisRequest compactRequest = new Pkcs7AnalysisRequest()
                .pkcs7Data(Pkcs7ExampleFixture.envelopedDataBase64());
        // Intentionally NO recipient-key id: structural analysis is key-independent.
        Pkcs7AnalysisResponse compact = sdk.analyzePkcs7(compactRequest);
        printAnalysis("compact", compact);
        System.out.println();

        // ============================================================
        // PHASE 2: Streaming analysis — same fixture as a file
        // ============================================================

        System.out.println("[Step 2/3] Streaming analysis (analyzePkcs7Stream) over a file...");
        File pkcs7File = Pkcs7ExampleFixture.materializeToTempFile();
        // The streaming call takes no options either: analysis needs no key.
        Pkcs7AnalysisResponse stream = sdk.analyzePkcs7Stream(pkcs7File);
        printAnalysis("stream", stream);
        System.out.println();

        // ============================================================
        // PHASE 3: Parity check between the two modes
        // ============================================================

        System.out.println("[Step 3/3] Confirming compact and streaming agree on the structure...");
        String compactStructure = structureOf(compact);
        String streamStructure = structureOf(stream);
        if (compactStructure.equals(streamStructure)) {
            System.out.println("           ✅ Both modes detected: " + compactStructure);
        } else {
            System.out.println("           ❌ Mode mismatch — compact=" + compactStructure
                    + " stream=" + streamStructure);
        }
    }

    /** Prints the structured analysis report using the real {@link Pkcs7AnalysisResponse} accessors. */
    private static void printAnalysis(final String mode, final Pkcs7AnalysisResponse resp) {
        System.out.println("           [" + mode + "] PKCS#7 analysis:");

        if (resp.getDetectedFormat() != null) {
            System.out.println("             Standard      : " + resp.getDetectedFormat().getStandard());
            System.out.println("             Structure     : " + resp.getDetectedFormat().getStructure());
            System.out.println("             Content-Type  : " + resp.getDetectedFormat().getContentTypeOID());
            System.out.println("             Encoding      : " + resp.getDetectedFormat().getEncoding());
        }

        // Zero signers is the EXPECTED shape for EnvelopedData — not a fault.
        List<SignerMetadata> signers = resp.getSigners();
        List<RecipientMetadata> recipients = resp.getRecipients();
        System.out.println("             Signers       : " + (signers == null ? 0 : signers.size()));
        System.out.println("             Recipients    : " + (recipients == null ? 0 : recipients.size()));
        if (recipients != null) {
            for (RecipientMetadata r : recipients) {
                System.out.println("               - recipient[" + r.getIndex() + "] "
                        + r.getKeyEncryptionAlgorithm() + " / issuer=" + r.getIssuerDN()
                        + " / serial=" + r.getSerialNumber());
            }
        }

        if (resp.getEncryption() != null) {
            System.out.println("             Content-Enc   : " + resp.getEncryption().getContentEncryptionAlgorithm()
                    + " (iv " + resp.getEncryption().getIvLength() + " bytes)");
        }

        if (resp.getSupportStatus() != null) {
            System.out.println("             Supported     : " + resp.getSupportStatus().getSupported());
            // When NOT supported, print the reasons instead of crashing (graceful degradation).
            if (Boolean.FALSE.equals(resp.getSupportStatus().getSupported())) {
                System.out.println("             Reasons       : " + resp.getSupportStatus().getReasons());
            }
        }

        if (resp.getRecommendations() != null) {
            System.out.println("             Migration     : " + resp.getRecommendations().getMigrationPath()
                    + " (complexity " + resp.getRecommendations().getEstimatedComplexity() + ")");
            if (resp.getRecommendations().getRequiredKeys() != null) {
                System.out.println("             Required keys : "
                        + resp.getRecommendations().getRequiredKeys().size());
            }
        }
    }

    /** Null-safe detected-structure string for the parity check. */
    private static String structureOf(final Pkcs7AnalysisResponse resp) {
        if (resp.getDetectedFormat() == null || resp.getDetectedFormat().getStructure() == null) {
            return "UNKNOWN";
        }
        return resp.getDetectedFormat().getStructure().toString();
    }
}

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

# Compile
javac -cp "ankasecure-sdk-3.4.0.jar:." ExampleScenario19.java

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

Expected Output:

===== SCENARIO 19: PKCS#7/CMS STRUCTURAL ANALYSIS =====
Purpose: Inspect a legacy PKCS#7/CMS artifact WITHOUT any decryption key
Modes  : compact (analyzePkcs7) + streaming (analyzePkcs7Stream)
Key    : none — analysis is key-independent (data-plane)

[Step 1/3] Compact analysis (analyzePkcs7), no decryption key...
           [compact] PKCS#7 analysis:
             Standard      : PKCS#7
             Structure     : EnvelopedData
             Content-Type  : 1.2.840.113549.1.7.3
             Encoding      : DER
             Signers       : 0
             Recipients    : 1
               - recipient[0] RSA / issuer=CN=ANKASecure Demo Recipient (Encryption),OU=QA,O=AnkaTech,C=CO / serial=0xFAB1E7D0F206D9D9
             Content-Enc   : AES-256-CBC (iv 16 bytes)
             Supported     : true
             Migration     : CONVERT_TO_JWE_COMPACT (complexity LOW)
             Required keys : 1

[Step 2/3] Streaming analysis (analyzePkcs7Stream) over a file...
           [stream] PKCS#7 analysis:
             Standard      : PKCS#7
             Structure     : EnvelopedData
             Content-Type  : 1.2.840.113549.1.7.3
             Encoding      : DER
             Signers       : 0
             Recipients    : 1
               - recipient[0] RSA / issuer=CN=ANKASecure Demo Recipient (Encryption),OU=QA,O=AnkaTech,C=CO / serial=0xFAB1E7D0F206D9D9
             Content-Enc   : AES-256-CBC (iv 16 bytes)
             Supported     : true
             Migration     : CONVERT_TO_JWE_COMPACT (complexity LOW)
             Required keys : 1

[Step 3/3] Confirming compact and streaming agree on the structure...
           ✅ Both modes detected: EnvelopedData
===== SCENARIO 19 END =====

Key Concepts

The structured analysis response

analyzePkcs7 / analyzePkcs7Stream return a structured Pkcs7AnalysisResponse, not a flat string. The example reads it through real accessors:

  • getDetectedFormat().getStandard() / .getStructure() — e.g. PKCS#7 / EnvelopedData
  • getRecipients() and getSigners() — descriptor lists; zero signers is the expected shape for EnvelopedData, not a fault
  • getEncryption().getContentEncryptionAlgorithm() — e.g. AES-256-CBC
  • getSupportStatus().getSupported() — when false, print getReasons() instead of crashing
  • getRecommendations().getMigrationPath() — e.g. CONVERT_TO_JWE_COMPACT; the suffix is _JSON when the file carries more than one signer or more than one recipient
  • getRecommendations().getRequiredKeys() — one entry per signer (purpose signing) and per recipient (purpose encryption); getDecryptionRequirement() is ONE_RECIPIENT_PRIVATE_KEY for an encrypted structure

Key-independence

Analysis reads only the envelope; it never decrypts. This is why Flow 19 runs against a freshly provisioned environment with no extra setup, whereas Flow 20 (conversion) needs the recipient key.

Compact vs streaming

The compact call sends the artifact Base64-encoded in the request body; the streaming call uploads it as a file. Both return the same structured report — the example asserts they agree on the detected structure.