Skip to content

Flow 24 – Sign-Then-Encrypt Nested Operations

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.

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. Generate ML-DSA-65 signing key (post-quantum digital signature)
  2. Generate 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/key-management/keys (generate signing key)
  • POST /api/key-management/keys (generate encryption key)
  • POST /api/crypto/stream/sign-encrypt (sign-then-encrypt operation)
  • POST /api/crypto/stream/decrypt-verify (decrypt-then-verify operation)

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"

Dependency — this example imports co.ankatech.ankasecure.sdk.examples.ExampleUtil. If you have not copied that class yet, see example_util.md.


Complete Java Implementation

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

package co.ankatech.ankasecure.sdk.examples;

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

import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.List;
import java.util.Properties;

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

/**
 * <h2>Scenario 24: Sign-Then-Encrypt Nested Operations</h2>
 * <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>Generate ML-DSA-65 signing key (post-quantum digital signature)</li>
 *   <li>Generate ML-KEM-768 encryption key (post-quantum key encapsulation)</li>
 *   <li>Create plaintext message file</li>
 *   <li>Sign-then-encrypt: Sign with ML-DSA → Encrypt with ML-KEM (creates JWE(JWS))</li>
 *   <li>Decrypt-then-verify: Decrypt with ML-KEM → 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/key-management/keys (generate signing key)</li>
 *   <li>POST /api/key-management/keys (generate encryption key)</li>
 *   <li>POST /api/crypto/stream/sign-encrypt (sign-then-encrypt operation)</li>
 *   <li>POST /api/crypto/stream/decrypt-verify (decrypt-then-verify operation)</li>
 * </ul>
 *
 * <h3>Token Structure:</h3>
 * <pre>
 * 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
 * </pre>
 *
 * @since 3.0.0
 */
public final class ExampleScenario24 {

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

    private ExampleScenario24() {
        /* static only */
    }

    public static void main(String[] args) {
        System.out.println("===== SCENARIO 24: SIGN-THEN-ENCRYPT NESTED OPERATIONS =====");
        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();

        try {
            ensureTempDir(TEMP_DIR);
            Properties props = loadProperties();
            AnkaSecureSdk sdk = authenticate(props);
            runScenario(sdk);

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

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

    private static void runScenario(AnkaSecureSdk sdk) throws Exception {

        // ============================================================
        // PHASE 1: Key Generation
        // ============================================================

        System.out.println("[Step 1/7] Generating ML-DSA-65 signing key...");
        final String signKid = "sc24_sign_mlsa65_" + System.currentTimeMillis();
        sdk.generateKey(new GenerateKeySpec()
                .setKid(signKid)
                .setKty("ML-DSA")
                .setAlg("ML-DSA-65")
                .setKeyOps(List.of("sign", "verify")));
        System.out.println("           Signing key ID: " + signKid);
        System.out.println();

        System.out.println("[Step 2/7] Generating ML-KEM-768 encryption key...");
        final String encKid = "sc24_enc_kem768_" + System.currentTimeMillis();
        sdk.generateKey(new GenerateKeySpec()
                .setKid(encKid)
                .setKty("ML-KEM")
                .setAlg("ML-KEM-768")
                .setKeyOps(List.of("encrypt", "decrypt")));
        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("scenario24_plain.txt");
        final String originalMessage = "Scenario-24: 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("scenario24_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.signThenEncryptFileStream(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("scenario24_recovered.txt");

        // Decrypt-then-verify: Extracts plaintext from JWE(JWS)
        // Outer layer: Decrypt JWE with ML-KEM-768 to get JWS
        // Inner layer: Verify JWS signature with ML-DSA-65 to get plaintext
        DecryptVerifyResult decryptVerifyMeta = sdk.decryptThenVerifyFileStream(encKid, signKid, 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 24 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 24 FAILED - Validation errors detected");
        }
    }
}

Running This Example

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

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

Expected Output:

===== SCENARIO 24: SIGN-THEN-ENCRYPT NESTED OPERATIONS =====
Purpose: Demonstrate authenticated encryption workflow with post-quantum algorithms
Pattern: Sign-THEN-encrypt (recommended), Decrypt-THEN-verify

[Step 1/7] Generating ML-DSA-65 signing key...
           Signing key ID: sc24_sign_mlsa65_1735152000000

[Step 2/7] Generating ML-KEM-768 encryption key...
           Encryption key ID: sc24_enc_kem768_1735152000100

[Step 3/7] Creating plaintext message...
           File: temp_files/scenario24_plain.txt
           Message: "Scenario-24: 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/scenario24_nested.jwe
           ✓ Sign key: sc24_sign_mlsa65_1735152000000
           ✓ Encrypt key: sc24_enc_kem768_1735152000100
           ✓ 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/scenario24_recovered.txt
           ✓ Decryption successful
           ✓ Signature verification: VALID

[Step 6/7] Validating signature...
           ✅ Signature is VALID
           Authenticity confirmed: Message was signed by sc24_sign_mlsa65_1735152000000

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

╔═══════════════════════════════════════════════════════════════╗
║               ✅ SCENARIO 24 SUCCESSFUL                        ║
║                                                               ║
║  Authenticated encryption complete:                           ║
║  • Authenticity: Signature verified (ML-DSA-65)               ║
║  • Confidentiality: Content decrypted (ML-KEM-768)            ║
║  • Integrity: Plaintext matches original                      ║
╚═══════════════════════════════════════════════════════════════╝
===== SCENARIO 24 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

  • Flow 6 - ML-DSA-87 Compact Sign/Verify (signing only)
  • Flow 5 - ML-KEM-512 Compact Encrypt/Decrypt (encryption only)
  • Flow 25 - External Key Interoperability (B2B authenticated encryption)