Skip to content

Flow 17 – Sign-Then-Encrypt Nested Operations (Compact)

Demonstrates the proper sequence for authenticated encryption: signing plaintext first (inner layer) then encrypting the signature (outer layer). This is the recommended pattern for secure messaging where both authenticity and confidentiality are required.

This is the compact (whole-artifact-in-memory) variant. For the bounded-memory, large-file counterpart with a fail-closed end-of-stream integrity verdict, see Flow 21 – Sign-Then-Encrypt Nested Operations (Streaming).

Why Sign-THEN-Encrypt?

  • Authenticity + Confidentiality: Signature proves sender identity, encryption protects content
  • Secure messaging pattern: Standard in protocols like S/MIME and PGP
  • Order matters: Sign-then-encrypt prevents signature stripping attacks
  • Nested JOSE tokens: Creates JWE(JWS) structure per RFC 7516

Steps:

  1. Resolve a pre-provisioned ML-DSA-65 signing key (post-quantum digital signature)
  2. Resolve a pre-provisioned ML-KEM-768 encryption key (post-quantum key encapsulation)
  3. Create plaintext message file
  4. Sign-then-encrypt: Sign with ML-DSA → Encrypt with ML-KEM (creates JWE(JWS))
  5. Decrypt-then-verify: Decrypt with ML-KEM → Verify with ML-DSA (reverses process)
  6. Validate: Original plaintext matches recovered plaintext
  7. Validate: Signature is cryptographically valid

Key Algorithms:

  • ML-DSA-65: NIST FIPS 204 post-quantum signature (192-bit security)
  • ML-KEM-768: NIST FIPS 203 post-quantum encryption (192-bit security)

API Endpoints:

  • POST /api/v3/crypto/sign-encrypt (compact sign-then-encrypt operation)
  • POST /api/v3/crypto/decrypt-verify (compact decrypt-then-verify operation)

The keys are pre-provisioned in the control plane. The data-plane SDK never generates keys — key lifecycle (generate/rotate/export) lives in cli-admin, not the SDK.

Token Structure:

JWE Header (ML-KEM-768 encrypted symmetric key)
Encrypted Payload:
  JWS Header (ML-DSA-65 signature metadata)
  JWS Payload: Original plaintext
  JWS Signature: ML-DSA-65 signature bytes

When to use:

  • Secure email with S/MIME-like guarantees (authenticity + confidentiality)
  • Encrypted contracts requiring proof of signer identity
  • Secure messaging protocols (end-to-end encrypted chat)
  • Any scenario requiring both "who signed it" and "nobody else can read it"
  • Payloads below the server-discovered maxPlaintextBytes limit (default 4 MB); for larger files use the streaming variant in Flow 21

Dependency — this example imports co.ankatech.ankasecure.sdk.examples.ExampleUtil. If you have not copied that class 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.
  • 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/ExampleScenario17.java

package co.ankatech.ankasecure.sdk.examples;

import co.ankatech.ankasecure.sdk.AuthenticatedSdk;
import co.ankatech.ankasecure.sdk.model.DecryptVerifyResult;
import co.ankatech.ankasecure.sdk.model.SignEncryptResult;
import co.ankatech.ankasecure.sdk.util.FileIO;

import java.nio.file.Path;
import java.util.Properties;

import static co.ankatech.ankasecure.sdk.examples.ExampleUtil.*;

/**
 * Scenario 17 — Sign-Then-Encrypt Nested Operations, Compact (data-plane).
 *
 * <p>Demonstrates the proper sequence for authenticated encryption: signing plaintext first (inner layer)
 * then encrypting the signature (outer layer). This is the recommended pattern for secure messaging
 * where both authenticity and confidentiality are required.</p>
 *
 * <h3>Why Sign-THEN-Encrypt?</h3>
 * <ul>
 *   <li><strong>Authenticity + Confidentiality:</strong> Signature proves sender identity, encryption protects content</li>
 *   <li><strong>Secure messaging pattern:</strong> Standard in protocols like S/MIME and PGP</li>
 *   <li><strong>Order matters:</strong> Sign-then-encrypt prevents signature stripping attacks</li>
 *   <li><strong>Nested JOSE tokens:</strong> Creates JWE(JWS) structure per RFC 7516</li>
 * </ul>
 *
 * <h3>Steps:</h3>
 * <ol>
 *   <li>Resolve a pre-provisioned ML-DSA-65 signing key (post-quantum digital signature)</li>
 *   <li>Resolve a pre-provisioned ML-KEM-768 encryption key (post-quantum key encapsulation)</li>
 *   <li>Create plaintext message file</li>
 *   <li>Sign-then-encrypt: Sign with ML-DSA &rarr; Encrypt with ML-KEM (creates JWE(JWS))</li>
 *   <li>Decrypt-then-verify: Decrypt with ML-KEM &rarr; Verify with ML-DSA (reverses process)</li>
 *   <li>Validate: Original plaintext matches recovered plaintext</li>
 *   <li>Validate: Signature is cryptographically valid</li>
 * </ol>
 *
 * <h3>Key Algorithms:</h3>
 * <ul>
 *   <li><strong>ML-DSA-65:</strong> NIST FIPS 204 post-quantum signature (192-bit security)</li>
 *   <li><strong>ML-KEM-768:</strong> NIST FIPS 203 post-quantum encryption (192-bit security)</li>
 * </ul>
 *
 * <h3>API Endpoints:</h3>
 * <ul>
 *   <li>POST /api/v3/crypto/sign-encrypt (compact sign-then-encrypt operation)</li>
 *   <li>POST /api/v3/crypto/decrypt-verify (compact decrypt-then-verify operation)</li>
 * </ul>
 *
 * <p>This scenario uses the <strong>compact</strong> nested-op endpoints. For the
 * streaming (bounded-memory, large-file) counterpart with the end-of-stream
 * integrity verdict, see {@link ExampleScenario21}.</p>
 *
 * <h3>Token Structure:</h3>
 * <pre>
 * JWE Header (ML-KEM-768 encrypted symmetric key)
 *   &darr;
 * Encrypted Payload:
 *   JWS Header (ML-DSA-65 signature metadata)
 *     &darr;
 *   JWS Payload: Original plaintext
 *     &darr;
 *   JWS Signature: ML-DSA-65 signature bytes
 * </pre>
 *
 * <p><strong>Prerequisite:</strong> a {@code cli.properties} for the
 * {@code cli-reference} actor of the provisioned {@code demo-cli} playground,
 * carrying the {@code ankasecure.demo.kids} catalogue (emitted by the
 * demo-provisioning tool). The SDK is data-plane-only — key lifecycle
 * (generate/rotate/export) lives in {@code cli-admin}, not the SDK.</p>
 *
 * @author ANKATech Solutions Inc.
 * @since 3.0.0
 * @see ExampleUtil
 * @see ExamplePlaygroundKeys
 * @see AuthenticatedSdk
 */
public final class ExampleScenario17 {

    private static final Path TEMP_DIR = Path.of("temp_files");

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

    /**
     * Runs the sign-then-encrypt nested operations scenario on pre-provisioned keys.
     *
     * <p>Loads CLI properties, authenticates against ANKASecure&copy;, resolves
     * the pre-provisioned playground keys, and delegates to the scenario logic.
     * On any unrecoverable error the JVM terminates via
     * {@link ExampleUtil#fatal(String, Throwable)}.</p>
     *
     * @param args command-line arguments (ignored)
     */
    public static void main(String[] args) {
        System.out.println("===== SCENARIO 17: SIGN-THEN-ENCRYPT NESTED OPERATIONS (COMPACT) =====");
        System.out.println("Purpose: Demonstrate authenticated encryption workflow with post-quantum algorithms");
        System.out.println("Pattern: Sign-THEN-encrypt (recommended), Decrypt-THEN-verify");
        System.out.println("Keys   : pre-provisioned ML-DSA-65 (sign) + ML-KEM-768 (encrypt)");
        System.out.println();

        try {
            prepareWorkingDir();
            Properties props = loadProperties();
            AuthenticatedSdk sdk = authenticate(props);
            ExamplePlaygroundKeys playground = ExamplePlaygroundKeys.from(props, sdk);
            runScenario(sdk, playground);

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

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

    private static void runScenario(AuthenticatedSdk sdk, ExamplePlaygroundKeys playground) throws Exception {

        // ============================================================
        // PHASE 1: Resolve pre-provisioned keys (data-plane SDK: no key creation)
        // ============================================================

        System.out.println("[Step 1/7] Resolving pre-provisioned ML-DSA-65 signing key...");
        final String signKid = playground.kidForAlgorithm("ML-DSA-65");
        System.out.println("           Signing key ID: " + signKid);
        System.out.println();

        System.out.println("[Step 2/7] Resolving pre-provisioned ML-KEM-768 encryption key...");
        final String encKid = playground.kidForAlgorithm("ML-KEM-768");
        System.out.println("           Encryption key ID: " + encKid);
        System.out.println();

        // ============================================================
        // PHASE 2: Create Plaintext
        // ============================================================

        System.out.println("[Step 3/7] Creating plaintext message...");
        final Path plainFile = TEMP_DIR.resolve("scenario17_plain.txt");
        final String originalMessage = "Scenario-17: Sign-then-encrypt nested operations with ML-DSA-65 + ML-KEM-768.";
        FileIO.writeUtf8(plainFile, originalMessage);
        System.out.println("           File: " + plainFile);
        System.out.println("           Message: \"" + originalMessage + "\"");
        System.out.println();

        // ============================================================
        // PHASE 3: Sign-Then-Encrypt (Sender Side)
        // ============================================================

        System.out.println("[Step 4/7] Performing sign-then-encrypt operation...");
        System.out.println("           Why this order? Sign-THEN-encrypt prevents signature stripping attacks");
        System.out.println("           and ensures authenticity is verified before content exposure.");
        System.out.println();

        final Path nestedFile = TEMP_DIR.resolve("scenario17_nested.jwe");

        // Sign-then-encrypt: Creates JWE(JWS(plaintext))
        // Inner layer: ML-DSA-65 signature over plaintext
        // Outer layer: ML-KEM-768 encryption of the JWS token
        SignEncryptResult signEncryptMeta = sdk.signThenEncryptFileCompact(signKid, encKid, plainFile, nestedFile);

        System.out.println("           Output file: " + nestedFile);
        printSignEncryptMeta(signEncryptMeta);
        System.out.println();

        // ============================================================
        // PHASE 4: Decrypt-Then-Verify (Receiver Side)
        // ============================================================

        System.out.println("[Step 5/7] Performing decrypt-then-verify operation...");
        System.out.println("           Reversing the process: decrypt outer layer, then verify inner signature");
        System.out.println();

        final Path recoveredFile = TEMP_DIR.resolve("scenario17_recovered.txt");

        // Decrypt-then-verify: Extracts plaintext from JWE(JWS)
        // Keys are automatically extracted from JWE and JWS headers (kid claims)
        // Outer layer: Decrypt JWE to get JWS (key auto-extracted)
        // Inner layer: Verify JWS signature to get plaintext (key auto-extracted)
        // Compact variant (whole artifact in memory). The streaming counterpart
        // (bounded memory + end-of-stream integrity verdict) is ExampleScenario21.
        DecryptVerifyResult decryptVerifyMeta = sdk.decryptThenVerifyFileCompact(nestedFile, recoveredFile);

        System.out.println("           Output file: " + recoveredFile);
        printDecryptVerifyMeta(decryptVerifyMeta);
        System.out.println();

        // ============================================================
        // PHASE 5: Validation
        // ============================================================

        System.out.println("[Step 6/7] Validating signature...");
        if (decryptVerifyMeta.isSignatureValid()) {
            System.out.println("           ✅ Signature is VALID");
            System.out.println("           Authenticity confirmed: Message was signed by " + signKid);
        } else {
            System.out.println("           ❌ Signature is INVALID");
            System.out.println("           WARNING: Message may have been tampered with!");
        }
        System.out.println();

        System.out.println("[Step 7/7] Validating plaintext recovery...");
        final String recoveredMessage = FileIO.readUtf8(recoveredFile);

        if (originalMessage.equals(recoveredMessage)) {
            System.out.println("           ✅ Plaintext matches original");
            System.out.println("           Confidentiality confirmed: Message correctly recovered");
        } else {
            System.out.println("           ❌ Plaintext does NOT match");
            System.out.println("           Expected: \"" + originalMessage + "\"");
            System.out.println("           Got:      \"" + recoveredMessage + "\"");
        }
        System.out.println();

        // ============================================================
        // FINAL STATUS
        // ============================================================

        if (decryptVerifyMeta.isSignatureValid() && originalMessage.equals(recoveredMessage)) {
            System.out.println("╔═══════════════════════════════════════════════════════════════╗");
            System.out.println("║               ✅ SCENARIO 17 SUCCESSFUL                        ║");
            System.out.println("║                                                               ║");
            System.out.println("║  Authenticated encryption complete:                           ║");
            System.out.println("║  • Authenticity: Signature verified (ML-DSA-65)               ║");
            System.out.println("║  • Confidentiality: Content decrypted (ML-KEM-768)            ║");
            System.out.println("║  • Integrity: Plaintext matches original                      ║");
            System.out.println("╚═══════════════════════════════════════════════════════════════╝");
        } else {
            System.out.println("❌ SCENARIO 17 FAILED - Validation errors detected");
        }
    }
}

Running This Example

# Compile
javac -cp "ankasecure-sdk-3.0.0.jar:." ExampleScenario17.java

# Run
java -cp "ankasecure-sdk-3.0.0.jar:." co.ankatech.ankasecure.sdk.examples.ExampleScenario17

Expected Output:

===== SCENARIO 17: SIGN-THEN-ENCRYPT NESTED OPERATIONS (COMPACT) =====
Purpose: Demonstrate authenticated encryption workflow with post-quantum algorithms
Pattern: Sign-THEN-encrypt (recommended), Decrypt-THEN-verify
Keys   : pre-provisioned ML-DSA-65 (sign) + ML-KEM-768 (encrypt)

[Step 1/7] Resolving pre-provisioned ML-DSA-65 signing key...
           Signing key ID: cli-ml-dsa-65

[Step 2/7] Resolving pre-provisioned ML-KEM-768 encryption key...
           Encryption key ID: cli-ml-kem-768

[Step 3/7] Creating plaintext message...
           File: temp_files/scenario17_plain.txt
           Message: "Scenario-17: Sign-then-encrypt nested operations with ML-DSA-65 + ML-KEM-768."

[Step 4/7] Performing sign-then-encrypt operation...
           Why this order? Sign-THEN-encrypt prevents signature stripping attacks
           and ensures authenticity is verified before content exposure.

           Output file: temp_files/scenario17_nested.jwe
           Sign key   : cli-ml-dsa-65
           Encrypt key: cli-ml-kem-768
           Algorithms : ML-DSA-65 (sign) + ML-KEM-768 (encrypt)

[Step 5/7] Performing decrypt-then-verify operation...
           Reversing the process: decrypt outer layer, then verify inner signature

           Output file: temp_files/scenario17_recovered.txt
           Decryption successful
           Signature verification: VALID

[Step 6/7] Validating signature...
           ✅ Signature is VALID
           Authenticity confirmed: Message was signed by cli-ml-dsa-65

[Step 7/7] Validating plaintext recovery...
           ✅ Plaintext matches original
           Confidentiality confirmed: Message correctly recovered

╔═══════════════════════════════════════════════════════════════╗
║               ✅ SCENARIO 17 SUCCESSFUL                        ║
║                                                               ║
║  Authenticated encryption complete:                           ║
║  • Authenticity: Signature verified (ML-DSA-65)               ║
║  • Confidentiality: Content decrypted (ML-KEM-768)            ║
║  • Integrity: Plaintext matches original                      ║
╚═══════════════════════════════════════════════════════════════╝
===== SCENARIO 17 END =====

Key Concepts

Authenticated Encryption

Authenticated encryption combines two security properties:

  1. Confidentiality: Only authorized recipients can read the message (encryption)
  2. Authenticity: Recipients can verify who sent the message (digital signature)

The order of operations matters:

  • Sign-THEN-encrypt (recommended): Prevents signature stripping, ensures signature validation before content exposure
  • Encrypt-THEN-sign (discouraged): Vulnerable to signature replacement attacks

Nested JOSE Tokens

This pattern creates a JWE token where the encrypted payload is itself a JWS token:

JWE (outer layer - confidentiality)
└── JWS (inner layer - authenticity)
    └── Plaintext (protected message)

The recipient must:

  1. Decrypt the JWE (requires encryption private key)
  2. Verify the JWS (requires signing public key or private key)
  3. Extract the plaintext

Post-Quantum Security

Using ML-DSA-65 + ML-KEM-768 provides:

  • 192-bit security level (equivalent to AES-192)
  • Quantum resistance for both signing and encryption
  • NIST standardization (FIPS 203 + FIPS 204)
  • Future-proof against quantum computer attacks