Skip to content

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:

  1. Generate a brand-new ML-DSA-87 signing key.

  2. Export its full JSON metadata (contains the public key).

  3. Stream-sign a document -- server returns a detached JWS.

  4. 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 its publicKey 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&nbsp;15 – Streamed Sign&nbsp;/&nbsp;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&nbsp;JWS).</li>
 *   <li>Verify the signature with {@code verifySignatureUtilityStream} using
 *       only the exported key.</li>
 * </ol>
 *
 * <p><b>Implementation notes (Java&nbsp;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

mvn -q compile exec:java\
  -Dexec.mainClass="co.ankatech.ankasecure.sdk.examples.ExampleScenario15"

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.