Flow 15 --- ML-DSA-87 Stream-Sign / Utility-Verify
This scenario performs a post-quantum streaming signature round-trip with a ML-DSA-87 key:
-
Generate a brand-new ML-DSA-87 signing key.
-
Export its full JSON metadata (contains the public key).
-
Stream-sign a document -- server returns a detached JWS.
-
Verify that signature with
verifySignatureUtilityStream
, passing only the public-key data from the export.
Key points
- Uses streaming endpoints so the client never buffers large files in RAM.
- Demonstrates out-of-band verification --- the JWS is validated without access to the original key in the HSM.
- Shows how to decode the exported-key JSON into a
ExportedKeySpec
and feed itspublicKey
to the utility verifier.
When to use it
-
Zero-trust sharing --- send a signed artefact plus a public-key JSON to partners who cannot call the API.
-
Audit pipelines --- verify signatures inside a sandbox that has no secret credentials.
-
Big-file workflows --- CI systems or backup jobs that must sign multi-gigabyte dumps without memory spikes.
Shared helper – this code imports the utility class from
example_util.md (configuration, authentication, JSON).
Complete Java implementation
src/main/java/co/ankatech/ankasecure/sdk/examples/ExampleScenario15.java
/* *****************************************************************************
* FILE: ExampleScenario15.java
* Copyright © 2025 Anka Technologies.
* SPDX-License-Identifier: MIT
* ---------------------------------------------------------------------------
* Scenario 15 – Streamed Sign / Verify with ML-DSA-87
* ---------------------------------------------------------------------------
* * Generate an ML-DSA-87 key.
* * Export its public metadata (contains the public key).
* * Stream-sign a file (detached JWS).
* * Verify the signature via {@code verifySignatureUtilityStream} using only
* the exported key data (out-of-band verification).
*
* All transient artefacts are created under <temp_files/>.
* ****************************************************************************/
package co.ankatech.ankasecure.sdk.examples;
import co.ankatech.ankasecure.sdk.AnkaSecureSdk;
import co.ankatech.ankasecure.sdk.model.ExportedKeySpec;
import co.ankatech.ankasecure.sdk.model.GenerateKeySpec;
import co.ankatech.ankasecure.sdk.model.SignResult;
import co.ankatech.ankasecure.sdk.model.VerifySignatureResult;
import com.google.gson.*;
import java.io.IOException;
import java.io.Reader;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Base64;
import java.util.List;
import java.util.Properties;
/**
* <h2>Scenario 15 – Streamed Sign / Verify (ML-DSA-87)</h2>
*
* <p>This scenario demonstrates <strong>streaming</strong> signing and
* verification with an ML-DSA-87 key, including <em>out-of-band</em>
* verification using only the exported public key:</p>
* <ol>
* <li>Generate an ML-DSA-87 key.</li>
* <li>Export its public metadata.</li>
* <li>Stream-sign a payload (detached JWS).</li>
* <li>Verify the signature with {@code verifySignatureUtilityStream} using
* only the exported key.</li>
* </ol>
*
* <p><b>Implementation notes (Java 21+):</b></p>
* <ul>
* <li>All filesystem access uses the {@link java.nio.file.Path} API.</li>
* <li>UTF-8 encoding is specified explicitly.</li>
* <li>JSON parsing relies on <a href="https://github.com/google/gson">Gson</a>
* with a custom adapter for {@link ZonedDateTime}.</li>
* </ul>
*
* @author ANKATech – Security Engineering
*/
public final class ExampleScenario15 {
/** Working directory for artefacts. */
private static final Path TEMP_DIR = Path.of("temp_files");
/* ====================================================================== */
/** Entry-point. */
public static void main(final String[] args) {
System.out.println("===== SCENARIO 15 START =====");
System.out.println("""
Purpose :
* Stream-sign a file with ML-DSA-87.
* Verify the detached JWS using only the exported public key.
Steps :
1) Generate ML-DSA-87 key
2) Export metadata
3) Stream-sign file
4) Stream-verify with utility
--------------------------------------------------------------""");
try {
ensureTempDir(TEMP_DIR);
Properties props = ExampleUtil.loadProperties();
AnkaSecureSdk sdk = ExampleUtil.authenticate(props);
runScenario(sdk);
} catch (Exception ex) {
fatal("Scenario 15 failed", ex);
}
System.out.println("===== SCENARIO 15 END =====");
}
/* ====================================================================== */
/** Executes the scenario. */
private static void runScenario(final AnkaSecureSdk sdk) throws Exception {
/* 1 ── prepare payload ------------------------------------------ */
Path data = TEMP_DIR.resolve("scenario15_data.txt");
Files.writeString(
data,
"Scenario-15 payload – streamed ML-DSA-87 demo.",
StandardCharsets.UTF_8);
System.out.println("[1] Data file prepared -> " + data.toAbsolutePath());
/* 2 ── generate ML-DSA-87 key ----------------------------------- */
String kid = "sc15_dsa87_" + System.currentTimeMillis();
sdk.generateKey(new GenerateKeySpec()
.setKid(kid)
.setKty("ML-DSA")
.setAlg("ML-DSA-87"));
System.out.println("[2] Key generated -> kid = " + kid);
/* 3 ── export metadata ----------------------------------------- */
Path keyJson = TEMP_DIR.resolve("scenario15_key.json");
sdk.exportKey(kid, keyJson);
System.out.println("[3] Metadata exported -> " + keyJson.toAbsolutePath());
/* 4 ── stream-sign payload ------------------------------------- */
Path sig = TEMP_DIR.resolve("scenario15_detached.sig");
SignResult sMeta = sdk.signFileStream(kid, data, sig);
System.out.println("[4] File signed -> " + sig.toAbsolutePath());
printSignMeta(sMeta);
/* 5 ── verify via utility-stream ------------------------------- */
ExportedKeySpec pub = readExportedKey(keyJson);
String sigB64 = Base64.getEncoder().encodeToString(Files.readAllBytes(sig));
VerifySignatureResult vMeta = sdk.verifySignatureUtilityStream(
pub.getKty(), pub.getAlg(), pub.getPublicKey(), sigB64, data);
System.out.println("[5] Signature valid? -> " + vMeta.isValid());
printVerifyMeta(vMeta);
System.out.println(vMeta.isValid()
? "[6] SUCCESS – signature verified."
: "[6] FAILURE – signature invalid.");
}
/* ====================================================================== */
/** Pretty-prints signing metadata. */
private static void printSignMeta(final SignResult r) {
System.out.println(" * Key requested : " + nullSafe(r.getKeyRequested()));
System.out.println(" * Key used : " + nullSafe(r.getActualKeyUsed()));
System.out.println(" * Algorithm : " + nullSafe(r.getAlgorithmUsed()));
printWarnings(r.getWarnings());
}
/** Pretty-prints verification metadata. */
private static void printVerifyMeta(final VerifySignatureResult r) {
System.out.println(" * Key requested : " + nullSafe(r.getKeyRequested()));
System.out.println(" * Key used : " + nullSafe(r.getActualKeyUsed()));
System.out.println(" * Algorithm : " + nullSafe(r.getAlgorithmUsed()));
printWarnings(r.getWarnings());
}
/** Prints any warnings in a consistent format. */
private static void printWarnings(final List<String> warnings) {
if (warnings != null && !warnings.isEmpty()) {
System.out.println(" * Warnings:");
warnings.forEach(w -> System.out.println(" * " + w));
}
}
/* ====================================================================== */
/** Deserialises an exported key JSON file. */
private static ExportedKeySpec readExportedKey(final Path json) throws IOException {
try (Reader r = Files.newBufferedReader(json, StandardCharsets.UTF_8)) {
Gson gson = new GsonBuilder()
.disableHtmlEscaping()
.registerTypeAdapter(
ZonedDateTime.class,
new JsonDeserializer<ZonedDateTime>() {
@Override
public ZonedDateTime deserialize(
JsonElement el, Type t, JsonDeserializationContext ctx)
throws JsonParseException {
String s = el.getAsString();
return (s == null || s.isBlank())
? null
: ZonedDateTime.parse(
s, DateTimeFormatter.ISO_OFFSET_DATE_TIME);
}
})
.registerTypeAdapter(
ZonedDateTime.class,
(JsonSerializer<ZonedDateTime>) (src, t, ctx) ->
src == null
? JsonNull.INSTANCE
: new JsonPrimitive(
src.format(
DateTimeFormatter.ISO_OFFSET_DATE_TIME)))
.create();
return gson.fromJson(r, ExportedKeySpec.class);
}
}
/* ====================================================================== */
/** Returns a printable string, substituting <code>(none)</code> when blank. */
private static String nullSafe(final String s) {
return (s == null || s.isBlank()) ? "(none)" : s;
}
/**
* Ensures the temporary directory exists.
*
* @param dir the directory to create if absent
*
* @throws IOException if creation fails
*/
private static void ensureTempDir(final Path dir) throws IOException {
if (!Files.exists(dir)) {
Files.createDirectories(dir);
}
}
/** Logs an unrecoverable error and terminates. */
private static void fatal(final String msg, final Throwable t) {
System.err.println(msg);
if (t != null) {
t.printStackTrace(System.err);
}
System.exit(1);
}
/**
* Private constructor prevents instantiation.
*/
private ExampleScenario15() {
/* utility class – no instantiation */
}
}
How to run
Console milestones
-
ML-DSA-87 key creation (
kid = sc15_dsa87_*
) -
Key-metadata export →
sc15_key.json
-
Streaming sign →
sc15_detached.sig
-
Utility-stream verification → SUCCESS
Where next?
© 2025 Anka Technologies. All rights reserved.