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:
- Generate ML-DSA-65 signing key (post-quantum digital signature)
- Generate ML-KEM-768 encryption key (post-quantum key encapsulation)
- Create plaintext message file
- Sign-then-encrypt: Sign with ML-DSA → Encrypt with ML-KEM (creates JWE(JWS))
- Decrypt-then-verify: Decrypt with ML-KEM → Verify with ML-DSA (reverses process)
- Validate: Original plaintext matches recovered plaintext
- 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:
- Confidentiality: Only authorized recipients can read the message (encryption)
- 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