Skip to content

Flow 12 --- ML-DSA-65 ➜ SLH-DSA-SHA2-128S Detached-JWS Re-sign (Streaming)

This scenario performs a zero-plaintext-exposure migration from an ML-DSA-65 signature to a SLH-DSA-SHA2-128S signature — lattice to hash-based, both post-quantum — all with streaming helpers:

  • Resolve the pre-provisioned ML-DSA-65 key and stream-sign a document → detached JWS.

  • Resolve the pre-provisioned SLH-DSA-SHA2-128S key.

  • Re-sign the existing JWS on the server (ML-DSA-65 → SLH-DSA-SHA2-128S) --- no payload re-upload, no Base64 bloat.

  • Verify the new SLH-DSA JWS in streaming mode.

  • Inspect rich metadata for every step (keys requested, algorithm, warnings).

Key points

  • Uses signFileStream → resignFileStream → verifySignatureStream for GB-scale artefacts.

  • Detached JWS keeps payload binary, avoiding Base64 overhead in transfers.

  • Proves you can migrate to SLH-DSA-SHA2-128S (a hash-based signature) without ever exposing or re-hashing plaintext.

Prerequisites

The flow examples are data-plane only. They never create, rotate, or delete keys — they resolve pre-provisioned playground keys and operate on them. Before running any flow example you must:

  1. Provision a fresh demo-cli playground. Use the ankasecure-demo-provisioning tool to provision the demo-cli playground. This seeds the cryptographic keys the examples operate on and the cli-reference@demo-cli actor that holds capability grants on them.
  2. Use the emitted cli.properties. The provisioning tool writes a cli.properties file that carries the ankasecure.demo.kids catalogue line — the comma-separated list of provisioned key ids, in YAML file order. The examples load this file to discover which keys exist.
  3. Authenticate as cli-reference@demo-cli. Authenticate the SDK as the cli-reference@demo-cli actor — the all-operations actor of the demo-cli playground — so the resolved keys carry the capability grants each operation needs.

Key selection in the examples is by algorithm or by exact name:

  • kidForAlgorithm(alg) returns a pre-provisioned key id matching the requested algorithm — used by the single-key operation flows.
  • requireKid(name) asserts that an exact, named cross-kid endpoint is present in the ankasecure.demo.kids catalogue — used by the cross-kid REENCRYPT / RESIGN flows that must operate on a specific granted source/target pair.

If cli.properties is missing the ankasecure.demo.kids line, the examples fail fast with a clear message instructing you to (re-)provision the demo-cli playground — they will not silently fall back.

PKCS#7 / CMS examples (Flow 19 & Flow 20)

The PKCS#7 interop examples operate on a packaged, non-sensitive CMS EnvelopedData fixture — you do not supply a PKCS#7 file:

  • Fixture: src/main/resources/pkcs7/enveloped-data-sample.p7m, loaded from the classpath by Pkcs7ExampleFixture. It is a single-recipient, zero-signer EnvelopedData artifact from the QA test signer; it carries no private key and no secret.
  • Flow 19 (analysis) needs no key. Structural analysis (analyzePkcs7 / analyzePkcs7Stream) inspects the CMS envelope metadata only, so it runs with no decryption key and no extra configuration.
  • Flow 20 (conversion) needs a pre-provisioned recipient key. Converting EnvelopedData to JWE requires the platform to decrypt the envelope, so it needs the recipient's private key referenced by the pkcs7.decryptionKid property in cli.properties. That key is matched by issuer DN + serial number and is imported by a control-plane step (outside the data-plane SDK); until it is provisioned, Flow 20's conversion calls fail at the server while Flow 19 still runs. The calling actor also needs the in-place REENCRYPT grant on that kid, requested with the kid twice (assetKids: [kid, kid]) and stored as [kid]: a conversion is authorized by REENCRYPT / RESIGN grants, never by DECRYPT, and no wildcard REENCRYPT grant exists.
  • Data-plane only. Like every flow example, these never import keystores or perform any key-lifecycle operation.

When to use it

  • Long-lived archives --- refresh classical RSA signatures to PQC without touching the data, ensuring future-proof authenticity.

  • Compliance pipelines --- stream re-sign terabyte data sets during nightly jobs with constant RAM.

  • Incremental hardening --- migrate gradually: keep ML-DSA tokens for existing consumers, add SLH-DSA tokens for forward security.

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/ExampleScenario12.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.
 */

package co.ankatech.ankasecure.sdk.examples;

import co.ankatech.ankasecure.sdk.AuthenticatedSdk;
import co.ankatech.ankasecure.sdk.model.ResignResult;
import co.ankatech.ankasecure.sdk.model.SignResult;
import co.ankatech.ankasecure.sdk.model.VerifySignatureResult;
import co.ankatech.ankasecure.sdk.util.FileIO;

import java.nio.file.Path;
import java.text.MessageFormat;
import java.util.Properties;

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

/**
 * Scenario 12 — ML-DSA-65 → SLH-DSA-SHA2-128S Streaming Re-sign (Detached JWS).
 *
 * <p>This scenario migrates a detached JWS signature from an ML-DSA-65 key to a
 * SLH-DSA-SHA2-128S key, fully server-side &mdash; no payload re-upload.</p>
 *
 * <p>The SDK is data-plane-only: it does not create keys. This scenario operates
 * on the <strong>pre-provisioned granted cross-kid pair</strong> of the
 * demo playground &mdash; source {@code cli-resign3-source}
 * ({@code ML-DSA-65}) and target {@code cli-resign3-target}
 * ({@code SLH-DSA-SHA2-128S}). The playground actor holds the cross-kid
 * RESIGN grant on this pair plus the atomic sign/verify grants on each endpoint.</p>
 *
 * <ol>
 *   <li>Resolve the granted {@code ML-DSA-65} source and {@code SLH-DSA-SHA2-128S} target kids.</li>
 *   <li>Stream-sign a file under the source.</li>
 *   <li>Re-sign the existing detached JWS on the server
 *       (ML-DSA-65 &rarr; SLH-DSA-SHA2-128S).</li>
 *   <li>Stream-verify the new SLH-DSA signature.</li>
 * </ol>
 *
 * <p><b>Implementation notes (Java&nbsp;21+):</b></p>
 * <ul>
 *   <li>All filesystem work relies on {@link java.nio.file.Path}.</li>
 *   <li>UTF-8 encoding is enforced explicitly for deterministic behaviour.</li>
 *   <li>Temporary artefacts reside under <kbd>temp_files/</kbd>.</li>
 * </ul>
 *
 * <p><b>Thread-safety:</b> the class is stateless and immutable.</p>
 *
 * @see ExampleUtil
 * @see ExamplePlaygroundKeys
 * @see AuthenticatedSdk
 */
public final class ExampleScenario12 {

    /** No instantiation &mdash; this class only exposes {@link #main(String[])}. */
    private ExampleScenario12() { }

    /**
     * Runs the ML-DSA-65 to SLH-DSA-SHA2-128S streaming re-sign scenario.
     *
     * <p>Loads CLI properties, authenticates against ANKASecure&copy;,
     * and delegates to the scenario logic. On any unrecoverable error
     * the JVM terminates via
     * {@link ExampleUtil#fatal(String, Throwable)}.</p>
     *
     * @param args command-line arguments (ignored)
     */
    public static void main(final String[] args) {

        System.out.println("===== SCENARIO 12 START =====");
        System.out.println("""
                Purpose :
                  * Migrate a detached ML-DSA-65 signature to SLH-DSA-SHA2-128S
                  * Uses the pre-provisioned granted cross-kid pair
                    (cli-resign3-source / cli-resign3-target)
                Steps   :
                  1) Resolve granted ML-DSA-65 source + SLH-DSA-SHA2-128S target kids
                  2) Stream-sign payload under source
                  3) Re-sign on server
                  4) Verify SLH-DSA signature
                --------------------------------------------------------------""");

        try {
            prepareWorkingDir();

            Properties            props      = loadProperties();
            AuthenticatedSdk      sdk        = authenticate(props);
            ExamplePlaygroundKeys playground = ExamplePlaygroundKeys.from(props, sdk);

            runScenario(sdk, playground);

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

        System.out.println("===== SCENARIO 12 END =====");
    }

    /* ====================================================================== */
    /**
     * Executes Scenario&nbsp;12 on the granted cross-kid pair.
     *
     * @param sdk        an authenticated {@link AuthenticatedSdk} instance
     * @param playground the pre-provisioned playground key resolver; must not be {@code null}
     * @throws Exception if any step fails
     */
    private static void runScenario(final AuthenticatedSdk sdk,
                                    final ExamplePlaygroundKeys playground) throws Exception {

        /* -- working paths -------------------------------------------------- */
        Path dataFile = TEMP_DIR.resolve("scenario12_data.txt");
        Path oldSig   = TEMP_DIR.resolve("scenario12_mldsa.sig");
        Path newSig   = TEMP_DIR.resolve("scenario12_slhdsa.sig");

        /* 1 -- payload ------------------------------------------------------ */
        FileIO.writeUtf8(dataFile,
                "Scenario-12 - ML-DSA-65 -> SLH-DSA-SHA2-128S streaming re-sign demo.");
        System.out.println("[1] Data file ready          -> " + dataFile.toAbsolutePath());

        /* 2 -- resolve granted cross-kid pair (data-plane SDK: no key creation) */
        final String oldKid = playground.requireKid("cli-resign3-source"); // ML-DSA-65
        final String newKid = playground.requireKid("cli-resign3-target"); // SLH-DSA-SHA2-128S
        System.out.println("[2] Source kid (ML-DSA-65)            -> " + oldKid);
        System.out.println("[2] Target kid (SLH-DSA-SHA2-128S)    -> " + newKid);

        /* 3 -- stream-sign under ML-DSA-65 source --------------------------- */
        SignResult oldMeta = sdk.signFileStream(oldKid, dataFile, oldSig);
        System.out.println("[3] ML-DSA signature stored  -> " + oldSig.toAbsolutePath());
        printSignMeta(oldMeta);

        /* 4 ── server re-sign (stream) ----------------------------------- */
        ResignResult reMeta = sdk.resignFileStream(
                oldSig,     // existing detached JWS header+sig
                newKid,     // replacement key
                dataFile,   // payload to sign
                newSig);    // output file
        System.out.println("[4] New signature written    -> " + newSig.toAbsolutePath());
        printResignMeta(reMeta);

        /* 5 ── stream-verify new JWS ------------------------------------- */
        VerifySignatureResult verMeta = sdk.verifySignatureStream(dataFile, newSig);
        System.out.println(MessageFormat.format(
                "[5] Signature valid?         -> {0}", verMeta.isValid()));
        printVerifyMeta(verMeta);

        System.out.println(verMeta.isValid()
                ? "[6] SUCCESS - SLH-DSA-SHA2-128S JWS verified."
                : "[6] FAILURE - verification failed.");
    }
}

How to run

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

Console milestones

  • ML-DSA-65 key resolution from the playground

  • Detached-JWS stream-sign → scenario12_mldsa.sig

  • SLH-DSA-SHA2-128S key resolution from the playground

  • Server-side stream re-sign → scenario12_slhdsa.sig

  • Stream verification → SUCCESS confirmation


Where next?