GenSigningKeyCommand.java

// SPDX-License-Identifier: Apache-2.0
// Copyright 2026 Egothor
// Copyright 2026 Accenture
package org.egothor.methodatlas.evidence;

import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.GeneralSecurityException;

/**
 * CLI command handler for the {@code -gen-signing-key} mode, which creates a
 * ZeroEcho keyring containing a fresh signing key pair for evidence-pack
 * signing.
 *
 * <p>
 * The mode is recognised and dispatched by {@code MethodAtlasApp} before the
 * normal scan-argument parsing, mirroring how {@code -diff} is handled. It is
 * self-contained: it parses its own small set of options and never participates
 * in the scan pipeline.
 * </p>
 *
 * <h2>Usage</h2>
 * <pre>{@code
 * methodatlas -gen-signing-key <keyring-file> [-key-alias <alias>] \
 *             [-key-algo <algorithm>] [-overwrite]
 * }</pre>
 *
 * <p>
 * The produced keyring is a plaintext file holding the private key in clear
 * text; protect it with file-system permissions and keep it out of version
 * control and out of any distributed evidence pack. See {@link SigningKeyGenerator}.
 * </p>
 *
 * @see SigningKeyGenerator
 * @see ZeroEchoSigner
 * @since 4.0.0
 */
public final class GenSigningKeyCommand {

    /** Mode flag that selects this command. */
    public static final String FLAG_GEN_SIGNING_KEY = "-gen-signing-key";

    private static final String FLAG_KEY_ALIAS = "-key-alias";
    private static final String FLAG_KEY_ALGO = "-key-algo";
    private static final String FLAG_OVERWRITE = "-overwrite";

    /** Default alias used when {@code -key-alias} is omitted. */
    private static final String DEFAULT_ALIAS = "methodatlas-signing";

    /** Exit code returned on success. */
    private static final int EXIT_OK = 0;

    /** Exit code returned when the arguments are invalid. */
    private static final int EXIT_BAD_ARGS = 2;

    private final Path keyringFile;
    private final String alias;
    private final String algorithm;
    private final boolean overwrite;

    /**
     * Creates a new command.
     *
     * @param keyringFile target keyring file; must not be {@code null}
     * @param alias       base alias for the generated key pair
     * @param algorithm   algorithm id, or {@code null} for the default
     * @param overwrite   whether to replace an existing alias
     */
    private GenSigningKeyCommand(Path keyringFile, String alias, String algorithm, boolean overwrite) {
        this.keyringFile = keyringFile;
        this.alias = alias;
        this.algorithm = algorithm;
        this.overwrite = overwrite;
    }

    /**
     * Parses {@code -gen-signing-key} arguments and runs the command.
     *
     * @param args full command-line arguments, including the
     *             {@code -gen-signing-key} flag and its value
     * @param out  writer that receives the success summary
     * @return {@code 0} on success, {@code 2} when the arguments are invalid
     * @throws IOException if the keyring cannot be written, an alias collides
     *                     without {@code -overwrite}, or key generation fails
     */
    public static int run(String[] args, PrintWriter out) throws IOException {
        final GenSigningKeyCommand command;
        try {
            command = parse(args);
        } catch (IllegalArgumentException e) {
            System.err.println("gen-signing-key: " + e.getMessage());
            System.err.println("Usage: -gen-signing-key <keyring-file> [-key-alias <alias>] "
                    + "[-key-algo <algorithm>] [-overwrite]");
            return EXIT_BAD_ARGS;
        }
        return command.execute(out);
    }

    /**
     * Parses the command-line arguments into a command instance.
     *
     * @param args full command-line arguments
     * @return parsed command
     * @throws IllegalArgumentException if the keyring value is missing or a flag
     *                                  lacks its required value
     */
    @SuppressWarnings("PMD.AvoidReassigningLoopVariables") // ++i consumes a flag's value, matching CliArgs
    private static GenSigningKeyCommand parse(String... args) {
        Path keyringFile = null;
        String alias = DEFAULT_ALIAS;
        String algorithm = null;
        boolean overwrite = false;

        for (int i = 0; i < args.length; i++) {
            switch (args[i]) {
                case FLAG_GEN_SIGNING_KEY -> keyringFile = Paths.get(value(args, ++i, FLAG_GEN_SIGNING_KEY));
                case FLAG_KEY_ALIAS -> alias = value(args, ++i, FLAG_KEY_ALIAS);
                case FLAG_KEY_ALGO -> algorithm = value(args, ++i, FLAG_KEY_ALGO);
                case FLAG_OVERWRITE -> overwrite = true;
                default -> {
                    // Ignore unrelated tokens so the mode can be combined with
                    // a leading program name or stray scan arguments.
                }
            }
        }
        if (keyringFile == null) {
            throw new IllegalArgumentException("missing keyring file after " + FLAG_GEN_SIGNING_KEY);
        }
        return new GenSigningKeyCommand(keyringFile, alias, algorithm, overwrite);
    }

    /**
     * Reads the value following a flag at index {@code i}.
     *
     * @param args command-line arguments
     * @param i    index of the value (already advanced past the flag)
     * @param flag the flag whose value is being read, for diagnostics
     * @return the value token
     * @throws IllegalArgumentException if the value is missing
     */
    private static String value(String[] args, int i, String flag) {
        if (i >= args.length) {
            throw new IllegalArgumentException("missing value after " + flag);
        }
        return args[i];
    }

    /**
     * Generates the key pair and prints a summary plus the matching
     * evidence-pack invocation.
     *
     * @param out writer that receives the success summary
     * @return {@code 0} on success
     * @throws IOException if the keyring cannot be written, an alias collides
     *                     without {@code -overwrite}, or key generation fails
     */
    private int execute(PrintWriter out) throws IOException {
        final SigningKeyGenerator.GeneratedKey generated;
        try {
            generated = SigningKeyGenerator.generate(keyringFile, alias, algorithm, overwrite);
        } catch (IllegalArgumentException e) {
            System.err.println("gen-signing-key: " + e.getMessage());
            return EXIT_BAD_ARGS;
        } catch (GeneralSecurityException e) {
            throw new IOException("Signing-key generation failed: " + e.getMessage(), e);
        }

        out.println("Generated " + generated.algorithm() + " signing key in "
                + generated.keyringFile().toAbsolutePath());
        out.println("  public alias:  " + generated.publicAlias());
        out.println("  private alias: " + generated.privateAlias());
        out.println("  public key:    " + generated.publicKeyPem().toAbsolutePath() + " (X.509 PEM, for verifiers)");
        out.println("The keyring holds the private key in clear text — keep it private and out of version control.");
        out.println("Sign an evidence pack with:");
        out.println("  -evidence-pack <framework> -evidence-pack-keyring " + generated.keyringFile()
                + " -evidence-pack-key-alias " + alias);
        return EXIT_OK;
    }
}