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:
analyzePkcs7takes the Base64 file only andanalyzePkcs7Streamthe 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:
- Load the packaged, non-sensitive EnvelopedData fixture (single recipient, zero signers)
- Compact analysis:
analyzePkcs7over the Base64 of the fixture (no key) - Streaming analysis:
analyzePkcs7Streamover the same fixture materialized to a file - 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/.p7sfiles 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.ExampleUtilandco.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:
- Provision a fresh
demo-cliplayground. Use theankasecure-demo-provisioningtool to provision thedemo-cliplayground. This seeds the cryptographic keys the examples operate on and thecli-reference@demo-cliactor that holds capability grants on them. - Use the emitted
cli.properties. The provisioning tool writes acli.propertiesfile that carries theankasecure.demo.kidscatalogue line — the comma-separated list of provisioned key ids, in YAML file order. The examples load this file to discover which keys exist. - Authenticate as
cli-reference@demo-cli. Authenticate the SDK as thecli-reference@demo-cliactor — the all-operations actor of thedemo-cliplayground — 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 theankasecure.demo.kidscatalogue — 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 byPkcs7ExampleFixture. 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.decryptionKidproperty incli.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-placeREENCRYPTgrant on that kid, requested with the kid twice (assetKids: [kid, kid]) and stored as[kid]: a conversion is authorized byREENCRYPT/RESIGNgrants, never byDECRYPT, and no wildcardREENCRYPTgrant 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 — 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 — 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 — 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 — 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/EnvelopedDatagetRecipients()andgetSigners()— descriptor lists; zero signers is the expected shape for EnvelopedData, not a faultgetEncryption().getContentEncryptionAlgorithm()— e.g.AES-256-CBCgetSupportStatus().getSupported()— whenfalse, printgetReasons()instead of crashinggetRecommendations().getMigrationPath()— e.g.CONVERT_TO_JWE_COMPACT; the suffix is_JSONwhen the file carries more than one signer or more than one recipientgetRecommendations().getRequiredKeys()— one entry per signer (purposesigning) and per recipient (purposeencryption);getDecryptionRequirement()isONE_RECIPIENT_PRIVATE_KEYfor 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.
Related Examples
- Flow 20 – PKCS#7 → JOSE Migration — convert the analyzed EnvelopedData to a JWE
- Flow 5 – ML-KEM-512 Compact Encrypt/Decrypt — the modern JWE target format