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

/*
 * Copyright 2025 ANKATech Solutions Inc
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * SPDX-License-Identifier: Apache-2.0
 */
/* *****************************************************************************
 * FILE: ExampleScenario15.java
 * ****************************************************************************/
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 co.ankatech.ankasecure.sdk.util.FileIO;
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.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Base64;
import java.util.Properties;

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

/**
 * <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
 * @since 2.1.0
 */
public final class ExampleScenario15 {

    /* ====================================================================== */
    /** 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 = loadProperties();
            AnkaSecureSdk sdk = 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");
        FileIO.writeUtf8(
                data,
                "Scenario-15 payload – streamed ML-DSA-87 demo.");
        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.");
    }

    /* ====================================================================== */
    /** 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);
        }
    }

    /**
     * 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 ANKATech Solutions INC. All rights reserved.