GenSigningKeyCommand.java

1
// SPDX-License-Identifier: Apache-2.0
2
// Copyright 2026 Egothor
3
// Copyright 2026 Accenture
4
package org.egothor.methodatlas.evidence;
5
6
import java.io.IOException;
7
import java.io.PrintWriter;
8
import java.nio.file.Path;
9
import java.nio.file.Paths;
10
import java.security.GeneralSecurityException;
11
12
/**
13
 * CLI command handler for the {@code -gen-signing-key} mode, which creates a
14
 * ZeroEcho keyring containing a fresh signing key pair for evidence-pack
15
 * signing.
16
 *
17
 * <p>
18
 * The mode is recognised and dispatched by {@code MethodAtlasApp} before the
19
 * normal scan-argument parsing, mirroring how {@code -diff} is handled. It is
20
 * self-contained: it parses its own small set of options and never participates
21
 * in the scan pipeline.
22
 * </p>
23
 *
24
 * <h2>Usage</h2>
25
 * <pre>{@code
26
 * methodatlas -gen-signing-key <keyring-file> [-key-alias <alias>] \
27
 *             [-key-algo <algorithm>] [-overwrite]
28
 * }</pre>
29
 *
30
 * <p>
31
 * The produced keyring is a plaintext file holding the private key in clear
32
 * text; protect it with file-system permissions and keep it out of version
33
 * control and out of any distributed evidence pack. See {@link SigningKeyGenerator}.
34
 * </p>
35
 *
36
 * @see SigningKeyGenerator
37
 * @see ZeroEchoSigner
38
 * @since 4.0.0
39
 */
40
public final class GenSigningKeyCommand {
41
42
    /** Mode flag that selects this command. */
43
    public static final String FLAG_GEN_SIGNING_KEY = "-gen-signing-key";
44
45
    private static final String FLAG_KEY_ALIAS = "-key-alias";
46
    private static final String FLAG_KEY_ALGO = "-key-algo";
47
    private static final String FLAG_OVERWRITE = "-overwrite";
48
49
    /** Default alias used when {@code -key-alias} is omitted. */
50
    private static final String DEFAULT_ALIAS = "methodatlas-signing";
51
52
    /** Exit code returned on success. */
53
    private static final int EXIT_OK = 0;
54
55
    /** Exit code returned when the arguments are invalid. */
56
    private static final int EXIT_BAD_ARGS = 2;
57
58
    private final Path keyringFile;
59
    private final String alias;
60
    private final String algorithm;
61
    private final boolean overwrite;
62
63
    /**
64
     * Creates a new command.
65
     *
66
     * @param keyringFile target keyring file; must not be {@code null}
67
     * @param alias       base alias for the generated key pair
68
     * @param algorithm   algorithm id, or {@code null} for the default
69
     * @param overwrite   whether to replace an existing alias
70
     */
71
    private GenSigningKeyCommand(Path keyringFile, String alias, String algorithm, boolean overwrite) {
72
        this.keyringFile = keyringFile;
73
        this.alias = alias;
74
        this.algorithm = algorithm;
75
        this.overwrite = overwrite;
76
    }
77
78
    /**
79
     * Parses {@code -gen-signing-key} arguments and runs the command.
80
     *
81
     * @param args full command-line arguments, including the
82
     *             {@code -gen-signing-key} flag and its value
83
     * @param out  writer that receives the success summary
84
     * @return {@code 0} on success, {@code 2} when the arguments are invalid
85
     * @throws IOException if the keyring cannot be written, an alias collides
86
     *                     without {@code -overwrite}, or key generation fails
87
     */
88
    public static int run(String[] args, PrintWriter out) throws IOException {
89
        final GenSigningKeyCommand command;
90
        try {
91
            command = parse(args);
92
        } catch (IllegalArgumentException e) {
93 1 1. run : removed call to java/io/PrintStream::println → SURVIVED
            System.err.println("gen-signing-key: " + e.getMessage());
94 1 1. run : removed call to java/io/PrintStream::println → SURVIVED
            System.err.println("Usage: -gen-signing-key <keyring-file> [-key-alias <alias>] "
95
                    + "[-key-algo <algorithm>] [-overwrite]");
96 1 1. run : replaced int return with 0 for org/egothor/methodatlas/evidence/GenSigningKeyCommand::run → KILLED
            return EXIT_BAD_ARGS;
97
        }
98 1 1. run : replaced int return with 0 for org/egothor/methodatlas/evidence/GenSigningKeyCommand::run → KILLED
        return command.execute(out);
99
    }
100
101
    /**
102
     * Parses the command-line arguments into a command instance.
103
     *
104
     * @param args full command-line arguments
105
     * @return parsed command
106
     * @throws IllegalArgumentException if the keyring value is missing or a flag
107
     *                                  lacks its required value
108
     */
109
    @SuppressWarnings("PMD.AvoidReassigningLoopVariables") // ++i consumes a flag's value, matching CliArgs
110
    private static GenSigningKeyCommand parse(String... args) {
111
        Path keyringFile = null;
112
        String alias = DEFAULT_ALIAS;
113
        String algorithm = null;
114
        boolean overwrite = false;
115
116 3 1. parse : removed conditional - replaced comparison check with false → KILLED
2. parse : changed conditional boundary → KILLED
3. parse : removed conditional - replaced comparison check with true → KILLED
        for (int i = 0; i < args.length; i++) {
117 1 1. parse : Changed switch default to be first case → KILLED
            switch (args[i]) {
118 1 1. parse : Changed increment from 1 to -1 → KILLED
                case FLAG_GEN_SIGNING_KEY -> keyringFile = Paths.get(value(args, ++i, FLAG_GEN_SIGNING_KEY));
119 1 1. parse : Changed increment from 1 to -1 → TIMED_OUT
                case FLAG_KEY_ALIAS -> alias = value(args, ++i, FLAG_KEY_ALIAS);
120 1 1. parse : Changed increment from 1 to -1 → TIMED_OUT
                case FLAG_KEY_ALGO -> algorithm = value(args, ++i, FLAG_KEY_ALGO);
121
                case FLAG_OVERWRITE -> overwrite = true;
122
                default -> {
123
                    // Ignore unrelated tokens so the mode can be combined with
124
                    // a leading program name or stray scan arguments.
125
                }
126
            }
127
        }
128 2 1. parse : removed conditional - replaced equality check with false → SURVIVED
2. parse : removed conditional - replaced equality check with true → KILLED
        if (keyringFile == null) {
129
            throw new IllegalArgumentException("missing keyring file after " + FLAG_GEN_SIGNING_KEY);
130
        }
131 1 1. parse : replaced return value with null for org/egothor/methodatlas/evidence/GenSigningKeyCommand::parse → KILLED
        return new GenSigningKeyCommand(keyringFile, alias, algorithm, overwrite);
132
    }
133
134
    /**
135
     * Reads the value following a flag at index {@code i}.
136
     *
137
     * @param args command-line arguments
138
     * @param i    index of the value (already advanced past the flag)
139
     * @param flag the flag whose value is being read, for diagnostics
140
     * @return the value token
141
     * @throws IllegalArgumentException if the value is missing
142
     */
143
    private static String value(String[] args, int i, String flag) {
144 3 1. value : removed conditional - replaced comparison check with true → KILLED
2. value : changed conditional boundary → KILLED
3. value : removed conditional - replaced comparison check with false → KILLED
        if (i >= args.length) {
145
            throw new IllegalArgumentException("missing value after " + flag);
146
        }
147 1 1. value : replaced return value with "" for org/egothor/methodatlas/evidence/GenSigningKeyCommand::value → KILLED
        return args[i];
148
    }
149
150
    /**
151
     * Generates the key pair and prints a summary plus the matching
152
     * evidence-pack invocation.
153
     *
154
     * @param out writer that receives the success summary
155
     * @return {@code 0} on success
156
     * @throws IOException if the keyring cannot be written, an alias collides
157
     *                     without {@code -overwrite}, or key generation fails
158
     */
159
    private int execute(PrintWriter out) throws IOException {
160
        final SigningKeyGenerator.GeneratedKey generated;
161
        try {
162
            generated = SigningKeyGenerator.generate(keyringFile, alias, algorithm, overwrite);
163
        } catch (IllegalArgumentException e) {
164 1 1. execute : removed call to java/io/PrintStream::println → SURVIVED
            System.err.println("gen-signing-key: " + e.getMessage());
165 1 1. execute : replaced int return with 0 for org/egothor/methodatlas/evidence/GenSigningKeyCommand::execute → KILLED
            return EXIT_BAD_ARGS;
166
        } catch (GeneralSecurityException e) {
167
            throw new IOException("Signing-key generation failed: " + e.getMessage(), e);
168
        }
169
170 1 1. execute : removed call to java/io/PrintWriter::println → KILLED
        out.println("Generated " + generated.algorithm() + " signing key in "
171
                + generated.keyringFile().toAbsolutePath());
172 1 1. execute : removed call to java/io/PrintWriter::println → SURVIVED
        out.println("  public alias:  " + generated.publicAlias());
173 1 1. execute : removed call to java/io/PrintWriter::println → SURVIVED
        out.println("  private alias: " + generated.privateAlias());
174 1 1. execute : removed call to java/io/PrintWriter::println → SURVIVED
        out.println("  public key:    " + generated.publicKeyPem().toAbsolutePath() + " (X.509 PEM, for verifiers)");
175 1 1. execute : removed call to java/io/PrintWriter::println → SURVIVED
        out.println("The keyring holds the private key in clear text — keep it private and out of version control.");
176 1 1. execute : removed call to java/io/PrintWriter::println → SURVIVED
        out.println("Sign an evidence pack with:");
177 1 1. execute : removed call to java/io/PrintWriter::println → SURVIVED
        out.println("  -evidence-pack <framework> -evidence-pack-keyring " + generated.keyringFile()
178
                + " -evidence-pack-key-alias " + alias);
179
        return EXIT_OK;
180
    }
181
}

Mutations

93

1.1
Location : run
Killed by : none
removed call to java/io/PrintStream::println → SURVIVED
Covering tests

94

1.1
Location : run
Killed by : none
removed call to java/io/PrintStream::println → SURVIVED
Covering tests

96

1.1
Location : run
Killed by : org.egothor.methodatlas.evidence.GenSigningKeyCommandTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.evidence.GenSigningKeyCommandTest]/[method:returnsBadArgsWhenKeyringValueMissing()]
replaced int return with 0 for org/egothor/methodatlas/evidence/GenSigningKeyCommand::run → KILLED

98

1.1
Location : run
Killed by : org.egothor.methodatlas.evidence.GenSigningKeyCommandTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.evidence.GenSigningKeyCommandTest]/[method:returnsBadArgsForUnsupportedAlgorithm()]
replaced int return with 0 for org/egothor/methodatlas/evidence/GenSigningKeyCommand::run → KILLED

116

1.1
Location : parse
Killed by : org.egothor.methodatlas.evidence.GenSigningKeyCommandTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.evidence.GenSigningKeyCommandTest]/[method:generatesDefaultKeyringWithDefaultAlias()]
removed conditional - replaced comparison check with false → KILLED

2.2
Location : parse
Killed by : org.egothor.methodatlas.evidence.GenSigningKeyCommandTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.evidence.GenSigningKeyCommandTest]/[method:returnsBadArgsForUnsupportedAlgorithm()]
changed conditional boundary → KILLED

3.3
Location : parse
Killed by : org.egothor.methodatlas.evidence.GenSigningKeyCommandTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.evidence.GenSigningKeyCommandTest]/[method:returnsBadArgsForUnsupportedAlgorithm()]
removed conditional - replaced comparison check with true → KILLED

117

1.1
Location : parse
Killed by : org.egothor.methodatlas.evidence.GenSigningKeyCommandTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.evidence.GenSigningKeyCommandTest]/[method:generatesDefaultKeyringWithDefaultAlias()]
Changed switch default to be first case → KILLED

118

1.1
Location : parse
Killed by : org.egothor.methodatlas.evidence.GenSigningKeyCommandTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.evidence.GenSigningKeyCommandTest]/[method:returnsBadArgsWhenKeyringValueMissing()]
Changed increment from 1 to -1 → KILLED

119

1.1
Location : parse
Killed by : none
Changed increment from 1 to -1 → TIMED_OUT

120

1.1
Location : parse
Killed by : none
Changed increment from 1 to -1 → TIMED_OUT

128

1.1
Location : parse
Killed by : none
removed conditional - replaced equality check with false → SURVIVED
Covering tests

2.2
Location : parse
Killed by : org.egothor.methodatlas.evidence.GenSigningKeyCommandTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.evidence.GenSigningKeyCommandTest]/[method:generatesDefaultKeyringWithDefaultAlias()]
removed conditional - replaced equality check with true → KILLED

131

1.1
Location : parse
Killed by : org.egothor.methodatlas.evidence.GenSigningKeyCommandTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.evidence.GenSigningKeyCommandTest]/[method:returnsBadArgsForUnsupportedAlgorithm()]
replaced return value with null for org/egothor/methodatlas/evidence/GenSigningKeyCommand::parse → KILLED

144

1.1
Location : value
Killed by : org.egothor.methodatlas.evidence.GenSigningKeyCommandTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.evidence.GenSigningKeyCommandTest]/[method:generatesDefaultKeyringWithDefaultAlias()]
removed conditional - replaced comparison check with true → KILLED

2.2
Location : value
Killed by : org.egothor.methodatlas.evidence.GenSigningKeyCommandTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.evidence.GenSigningKeyCommandTest]/[method:returnsBadArgsWhenKeyringValueMissing()]
changed conditional boundary → KILLED

3.3
Location : value
Killed by : org.egothor.methodatlas.evidence.GenSigningKeyCommandTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.evidence.GenSigningKeyCommandTest]/[method:returnsBadArgsWhenKeyringValueMissing()]
removed conditional - replaced comparison check with false → KILLED

147

1.1
Location : value
Killed by : org.egothor.methodatlas.evidence.GenSigningKeyCommandTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.evidence.GenSigningKeyCommandTest]/[method:generatesDefaultKeyringWithDefaultAlias()]
replaced return value with "" for org/egothor/methodatlas/evidence/GenSigningKeyCommand::value → KILLED

164

1.1
Location : execute
Killed by : none
removed call to java/io/PrintStream::println → SURVIVED
Covering tests

165

1.1
Location : execute
Killed by : org.egothor.methodatlas.evidence.GenSigningKeyCommandTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.evidence.GenSigningKeyCommandTest]/[method:returnsBadArgsForUnsupportedAlgorithm()]
replaced int return with 0 for org/egothor/methodatlas/evidence/GenSigningKeyCommand::execute → KILLED

170

1.1
Location : execute
Killed by : org.egothor.methodatlas.evidence.GenSigningKeyCommandTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.evidence.GenSigningKeyCommandTest]/[method:generatesDefaultKeyringWithDefaultAlias()]
removed call to java/io/PrintWriter::println → KILLED

172

1.1
Location : execute
Killed by : none
removed call to java/io/PrintWriter::println → SURVIVED
Covering tests

173

1.1
Location : execute
Killed by : none
removed call to java/io/PrintWriter::println → SURVIVED
Covering tests

174

1.1
Location : execute
Killed by : none
removed call to java/io/PrintWriter::println → SURVIVED
Covering tests

175

1.1
Location : execute
Killed by : none
removed call to java/io/PrintWriter::println → SURVIVED
Covering tests

176

1.1
Location : execute
Killed by : none
removed call to java/io/PrintWriter::println → SURVIVED
Covering tests

177

1.1
Location : execute
Killed by : none
removed call to java/io/PrintWriter::println → SURVIVED
Covering tests

Active mutators

Tests examined


Report generated by PIT 1.22.1