| 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.InputStream; | |
| 8 | import java.io.OutputStream; | |
| 9 | import java.nio.file.Files; | |
| 10 | import java.nio.file.Path; | |
| 11 | import java.security.GeneralSecurityException; | |
| 12 | import java.security.PrivateKey; | |
| 13 | import java.security.Signature; | |
| 14 | import java.util.List; | |
| 15 | import java.util.Locale; | |
| 16 | import java.util.function.Supplier; | |
| 17 | import java.util.logging.Level; | |
| 18 | import java.util.logging.Logger; | |
| 19 | ||
| 20 | import zeroecho.core.context.SignatureContext; | |
| 21 | import zeroecho.core.spec.VoidSpec; | |
| 22 | import zeroecho.core.storage.KeyringStore; | |
| 23 | import zeroecho.core.tag.TagEngine; | |
| 24 | import zeroecho.core.tag.TagEngineBuilder; | |
| 25 | import zeroecho.sdk.builders.TagTrailerDataContentBuilder; | |
| 26 | import zeroecho.sdk.content.api.DataContent; | |
| 27 | import zeroecho.sdk.content.builtin.PlainFile; | |
| 28 | import zeroecho.sdk.hybrid.signature.HybridSignatureContexts; | |
| 29 | import zeroecho.sdk.hybrid.signature.HybridSignatureProfile; | |
| 30 | import zeroecho.sdk.util.BouncyCastleActivator; | |
| 31 | ||
| 32 | /** | |
| 33 | * Produces a signed envelope of the evidence-pack manifest using the ZeroEcho | |
| 34 | * cryptographic toolkit (version 1.1.1). | |
| 35 | * | |
| 36 | * <p> | |
| 37 | * The signing key is read from a ZeroEcho {@link KeyringStore} — a plaintext | |
| 38 | * UTF-8 keyring file, <em>not</em> a JDK PKCS12/JKS keystore and not anything | |
| 39 | * produced by {@code keytool}. The keyring is loaded with | |
| 40 | * {@link KeyringStore#load(Path)} and the private key is resolved by alias. | |
| 41 | * Generate a keyring with MethodAtlas's {@code -gen-signing-key} mode (see | |
| 42 | * {@link SigningKeyGenerator}) or with ZeroEcho's own {@code -K --generate} | |
| 43 | * command-line tool. | |
| 44 | * </p> | |
| 45 | * | |
| 46 | * <h2>Single-algorithm signing</h2> | |
| 47 | * <p> | |
| 48 | * When the signature algorithm contains no {@value #HYBRID_SEP} separator the | |
| 49 | * manifest is signed with one algorithm. The algorithm defaults to | |
| 50 | * {@value #DEFAULT_ALGO}; when no explicit algorithm is supplied the value | |
| 51 | * stored alongside the key in the keyring is used. RSA keys are signed with | |
| 52 | * RSA-PSS (SHA-256) and ECDSA keys with the P-256 curve; every other algorithm | |
| 53 | * (Ed25519, Ed448, SPHINCS+, ML-DSA, SLH-DSA, …) is dispatched through | |
| 54 | * {@link TagEngineBuilder#signature(String, java.security.Key, zeroecho.core.spec.ContextSpec)}. | |
| 55 | * </p> | |
| 56 | * | |
| 57 | * <h2>Hybrid signing</h2> | |
| 58 | * <p> | |
| 59 | * A signature algorithm of the form {@code classic+pqc} (for example | |
| 60 | * {@code Ed25519+SPHINCS+}) selects a hybrid composite that signs with both a | |
| 61 | * classical and a post-quantum primitive. The key alias must then carry both | |
| 62 | * component aliases separated by {@value #ALIAS_SEP} | |
| 63 | * ({@code classicAlias/pqcAlias}). Verification of a hybrid envelope requires | |
| 64 | * both component signatures (AND rule). | |
| 65 | * </p> | |
| 66 | * | |
| 67 | * <h2>Thread-safety and lifecycle</h2> | |
| 68 | * <p> | |
| 69 | * Instances are <strong>not</strong> thread-safe and are single-use: create one | |
| 70 | * signer per {@link #sign(Path, Path)} call. Hybrid signers in particular hold a | |
| 71 | * stateful {@link SignatureContext} that must not be reused across streams. The | |
| 72 | * signer is {@link AutoCloseable}; callers should use it in a | |
| 73 | * try-with-resources block (or call {@link #close()}) so the owned hybrid | |
| 74 | * context is released — close it even if {@link #sign(Path, Path)} is never | |
| 75 | * invoked. | |
| 76 | * </p> | |
| 77 | * | |
| 78 | * <p> | |
| 79 | * Package-private because only {@link EvidencePackCommand} composes this signer. | |
| 80 | * </p> | |
| 81 | * | |
| 82 | * @see SigningKeyGenerator | |
| 83 | * @see <a href="https://gitea.egothor.org/Egothor/ZeroEcho">ZeroEcho</a> | |
| 84 | * @since 4.0.0 | |
| 85 | */ | |
| 86 | final class ZeroEchoSigner implements AutoCloseable { | |
| 87 | ||
| 88 | private static final Logger LOG = Logger.getLogger(ZeroEchoSigner.class.getName()); | |
| 89 | ||
| 90 | /** Stream buffer size used when copying through the signing pipeline. */ | |
| 91 | /* default */ static final int BUFFER_SIZE = 8192; | |
| 92 | ||
| 93 | /** | |
| 94 | * Upper bound on the manifest body buffered by the hybrid DoS guard — 16 MiB | |
| 95 | * is far more than any realistic SHA-256 manifest. | |
| 96 | */ | |
| 97 | /* default */ static final int MAX_BODY_BYTES = 16 * 1024 * 1024; | |
| 98 | ||
| 99 | /** Default single-algorithm identifier when none is supplied and none is stored. */ | |
| 100 | /* default */ static final String DEFAULT_ALGO = "Ed25519"; | |
| 101 | ||
| 102 | /** Separator that distinguishes hybrid algorithm specifications ({@code classic+pqc}). */ | |
| 103 | /* default */ static final String HYBRID_SEP = "+"; | |
| 104 | ||
| 105 | /** Separator used inside the key-alias parameter to split classical and PQC aliases. */ | |
| 106 | /* default */ static final String ALIAS_SEP = "/"; | |
| 107 | ||
| 108 | /** Version of the ZeroEcho library recorded in {@code pack-meta.json}. */ | |
| 109 | /* default */ static final String ZEROECHO_LIB_VERSION = "1.1.1"; | |
| 110 | ||
| 111 | /** Factory that supplies a fresh signing engine for the manifest pipeline. */ | |
| 112 | private final Supplier<TagEngine<Signature>> engineFactory; | |
| 113 | ||
| 114 | /** Resolved algorithm string, reported by callers in pack metadata. */ | |
| 115 | private final String algorithm; | |
| 116 | ||
| 117 | /** Effective keystore alias actually used to load the signing key. */ | |
| 118 | private final String resolvedAlias; | |
| 119 | ||
| 120 | /** | |
| 121 | * Closeable cryptographic context owned by this signer, or {@code null}. The | |
| 122 | * hybrid path eagerly creates a {@link SignatureContext} that this signer | |
| 123 | * retains so {@link #close()} can release it; the single-algorithm path has | |
| 124 | * no owned context (its per-stream engines are created and owned by the | |
| 125 | * signing pipeline). | |
| 126 | */ | |
| 127 | private final SignatureContext ownedContext; | |
| 128 | ||
| 129 | /** | |
| 130 | * Private constructor; instances are obtained through | |
| 131 | * {@link #fromKeyringFile(Path, String, String)} or | |
| 132 | * {@link #fromKeyringText(String, String, String)}. | |
| 133 | * | |
| 134 | * @param engineFactory supplier of the signing engine; must not be {@code null} | |
| 135 | * @param algorithm algorithm string (e.g. {@code "Ed25519"}) | |
| 136 | * @param resolvedAlias keyring alias actually used | |
| 137 | * @param ownedContext closeable context this signer owns, or {@code null} | |
| 138 | * when there is none to release | |
| 139 | */ | |
| 140 | private ZeroEchoSigner(Supplier<TagEngine<Signature>> engineFactory, String algorithm, String resolvedAlias, | |
| 141 | SignatureContext ownedContext) { | |
| 142 | this.engineFactory = engineFactory; | |
| 143 | this.algorithm = algorithm; | |
| 144 | this.resolvedAlias = resolvedAlias; | |
| 145 | this.ownedContext = ownedContext; | |
| 146 | } | |
| 147 | ||
| 148 | /** | |
| 149 | * Releases the cryptographic context this signer owns. A no-op for the | |
| 150 | * single-algorithm path; for hybrid signers it closes the retained | |
| 151 | * {@link SignatureContext}. Failures to close are logged at {@code FINE} and | |
| 152 | * swallowed, since {@code close()} is best-effort cleanup and must not mask a | |
| 153 | * signing result. | |
| 154 | */ | |
| 155 | @Override | |
| 156 | public void close() { | |
| 157 |
2
1. close : removed conditional - replaced equality check with false → KILLED 2. close : removed conditional - replaced equality check with true → KILLED |
if (ownedContext != null) { |
| 158 | try { | |
| 159 |
1
1. close : removed call to zeroecho/core/context/SignatureContext::close → KILLED |
ownedContext.close(); |
| 160 | } catch (IOException e) { | |
| 161 | if (LOG.isLoggable(Level.FINE)) { | |
| 162 | LOG.log(Level.FINE, "Failed to close hybrid signature context", e); | |
| 163 | } | |
| 164 | } | |
| 165 | } | |
| 166 | } | |
| 167 | ||
| 168 | /** | |
| 169 | * Returns the algorithm string that will be reported in pack metadata. | |
| 170 | * | |
| 171 | * @return algorithm identifier; never {@code null} | |
| 172 | */ | |
| 173 | /* default */ String algorithm() { | |
| 174 |
1
1. algorithm : replaced return value with "" for org/egothor/methodatlas/evidence/ZeroEchoSigner::algorithm → KILLED |
return algorithm; |
| 175 | } | |
| 176 | ||
| 177 | /** | |
| 178 | * Returns the keyring alias actually used to retrieve the signing key. | |
| 179 | * | |
| 180 | * @return alias; never {@code null} once construction succeeded | |
| 181 | */ | |
| 182 | /* default */ String resolvedAlias() { | |
| 183 |
1
1. resolvedAlias : replaced return value with "" for org/egothor/methodatlas/evidence/ZeroEchoSigner::resolvedAlias → KILLED |
return resolvedAlias; |
| 184 | } | |
| 185 | ||
| 186 | /** | |
| 187 | * Creates a signer from a ZeroEcho keyring file on disk. This is the path | |
| 188 | * used for interactive CLI signing, where the keyring file is protected by | |
| 189 | * file-system permissions or ACLs. | |
| 190 | * | |
| 191 | * @param keyringFile path to the ZeroEcho keyring; must not be {@code null} | |
| 192 | * @param keyAlias alias to load; {@code null} means use the first | |
| 193 | * alias in the keyring; for hybrid the format is | |
| 194 | * {@code classicAlias/pqcAlias} | |
| 195 | * @param signatureAlgorithm algorithm identifier; {@code null} means derive it | |
| 196 | * from the keyring entry (single-algorithm only); a | |
| 197 | * value of the form {@code classic+pqc} selects | |
| 198 | * hybrid signing | |
| 199 | * @return signer ready to sign; never {@code null} | |
| 200 | * @throws IOException if reading the keyring or building the | |
| 201 | * signing context fails | |
| 202 | * @throws GeneralSecurityException if a key cannot be materialised | |
| 203 | */ | |
| 204 | /* default */ static ZeroEchoSigner fromKeyringFile(Path keyringFile, String keyAlias, | |
| 205 | String signatureAlgorithm) throws IOException, GeneralSecurityException { | |
| 206 |
1
1. fromKeyringFile : replaced return value with null for org/egothor/methodatlas/evidence/ZeroEchoSigner::fromKeyringFile → KILLED |
return build(KeyringStore.load(keyringFile), keyAlias, signatureAlgorithm); |
| 207 | } | |
| 208 | ||
| 209 | /** | |
| 210 | * Creates a signer from in-memory keyring text. This is the path used in | |
| 211 | * CI/CD pipelines, where the keyring content is delivered through a platform | |
| 212 | * secret (an environment variable) and is parsed in memory so the private | |
| 213 | * key never touches the runner's disk. | |
| 214 | * | |
| 215 | * @param keyringText full keyring content (including the | |
| 216 | * {@code # KeyringStore v1} header), typically the | |
| 217 | * value of a CI secret variable; must not be | |
| 218 | * {@code null} or blank | |
| 219 | * @param keyAlias alias to load; {@code null} means use the first | |
| 220 | * alias in the keyring; for hybrid the format is | |
| 221 | * {@code classicAlias/pqcAlias} | |
| 222 | * @param signatureAlgorithm algorithm identifier; {@code null} means derive it | |
| 223 | * from the keyring entry; a value of the form | |
| 224 | * {@code classic+pqc} selects hybrid signing | |
| 225 | * @return signer ready to sign; never {@code null} | |
| 226 | * @throws IOException if the keyring text is malformed or building | |
| 227 | * the signing context fails | |
| 228 | * @throws GeneralSecurityException if a key cannot be materialised | |
| 229 | */ | |
| 230 | /* default */ static ZeroEchoSigner fromKeyringText(String keyringText, String keyAlias, | |
| 231 | String signatureAlgorithm) throws IOException, GeneralSecurityException { | |
| 232 |
4
1. fromKeyringText : removed conditional - replaced equality check with true → SURVIVED 2. fromKeyringText : removed conditional - replaced equality check with false → SURVIVED 3. fromKeyringText : removed conditional - replaced equality check with true → KILLED 4. fromKeyringText : removed conditional - replaced equality check with false → KILLED |
if (keyringText == null || keyringText.isBlank()) { |
| 233 | throw new IOException("Keyring content is empty"); | |
| 234 | } | |
| 235 | KeyringStore keyring = new KeyringStore(); | |
| 236 |
1
1. fromKeyringText : removed call to zeroecho/core/storage/KeyringStore::importText → KILLED |
keyring.importText(keyringText, true); |
| 237 |
1
1. fromKeyringText : replaced return value with null for org/egothor/methodatlas/evidence/ZeroEchoSigner::fromKeyringText → KILLED |
return build(keyring, keyAlias, signatureAlgorithm); |
| 238 | } | |
| 239 | ||
| 240 | /** | |
| 241 | * Builds a signer from an already-loaded keyring, dispatching to the single | |
| 242 | * or hybrid construction path based on {@code signatureAlgorithm}. | |
| 243 | * | |
| 244 | * @param keyring loaded keyring store; must not be {@code null} | |
| 245 | * @param keyAlias alias to load, or {@code null} for the first alias | |
| 246 | * @param signatureAlgorithm algorithm identifier, or {@code null} to derive it | |
| 247 | * @return configured signer | |
| 248 | * @throws IOException if the alias/algorithm pairing is invalid | |
| 249 | * @throws GeneralSecurityException if a key cannot be materialised | |
| 250 | */ | |
| 251 | private static ZeroEchoSigner build(KeyringStore keyring, String keyAlias, String signatureAlgorithm) | |
| 252 | throws IOException, GeneralSecurityException { | |
| 253 | // SPHINCS+ and the other PQC primitives are provided by Bouncy Castle; | |
| 254 | // init is idempotent and harmless for classical algorithms. | |
| 255 |
1
1. build : removed call to zeroecho/sdk/util/BouncyCastleActivator::init → SURVIVED |
BouncyCastleActivator.init(); |
| 256 |
4
1. build : removed conditional - replaced equality check with false → KILLED 2. build : removed conditional - replaced equality check with false → KILLED 3. build : removed conditional - replaced equality check with true → KILLED 4. build : removed conditional - replaced equality check with true → KILLED |
if (signatureAlgorithm != null && signatureAlgorithm.contains(HYBRID_SEP)) { |
| 257 |
1
1. build : replaced return value with null for org/egothor/methodatlas/evidence/ZeroEchoSigner::build → KILLED |
return buildHybrid(keyring, keyAlias, signatureAlgorithm); |
| 258 | } | |
| 259 |
1
1. build : replaced return value with null for org/egothor/methodatlas/evidence/ZeroEchoSigner::build → KILLED |
return buildSingle(keyring, keyAlias, signatureAlgorithm); |
| 260 | } | |
| 261 | ||
| 262 | /** | |
| 263 | * Signs {@code inputFile} and writes the resulting signed envelope to | |
| 264 | * {@code outputFile}. The output contains the original bytes followed by a | |
| 265 | * ZeroEcho signature trailer. | |
| 266 | * | |
| 267 | * @param inputFile manifest to sign | |
| 268 | * @param outputFile destination for the signed envelope | |
| 269 | * @return {@code outputFile} on success | |
| 270 | * @throws IOException if reading, signing, or writing fails | |
| 271 | * @throws GeneralSecurityException if the cryptographic step fails | |
| 272 | */ | |
| 273 | // ZeroEcho's builders surface configuration failures as unchecked exceptions | |
| 274 | // (IllegalStateException from the engine factory, IllegalArgumentException for | |
| 275 | // bad parameters); translating them to IOException keeps the CLI from crashing | |
| 276 | // mid-pack with a raw stack trace. | |
| 277 | @SuppressWarnings("PMD.AvoidCatchingGenericException") | |
| 278 | /* default */ Path sign(Path inputFile, Path outputFile) throws IOException, GeneralSecurityException { | |
| 279 | try { | |
| 280 | DataContent tail = new TagTrailerDataContentBuilder<>(engineFactory) | |
| 281 | .bufferSize(BUFFER_SIZE) | |
| 282 | .build(true); | |
| 283 |
1
1. sign : removed call to zeroecho/sdk/content/api/DataContent::setInput → KILLED |
tail.setInput(new PlainFile(inputFile.toUri().toURL())); |
| 284 | try (InputStream in = tail.getStream(); | |
| 285 | OutputStream out = Files.newOutputStream(outputFile)) { | |
| 286 | in.transferTo(out); | |
| 287 | } | |
| 288 |
1
1. sign : replaced return value with null for org/egothor/methodatlas/evidence/ZeroEchoSigner::sign → KILLED |
return outputFile; |
| 289 | } catch (RuntimeException e) { | |
| 290 | throw new IOException("Failed to sign manifest with ZeroEcho (" + algorithm + ")", e); | |
| 291 | } | |
| 292 | } | |
| 293 | ||
| 294 | // ------------------------------------------------------------------------- | |
| 295 | // Construction helpers | |
| 296 | // ------------------------------------------------------------------------- | |
| 297 | ||
| 298 | /** | |
| 299 | * Builds a single-algorithm signer, deriving the algorithm from the keyring | |
| 300 | * entry when {@code explicitAlgorithm} is {@code null}. | |
| 301 | * | |
| 302 | * @param keyring loaded keyring store | |
| 303 | * @param keyAlias requested alias, or {@code null} for the first alias | |
| 304 | * @param explicitAlgorithm caller-supplied algorithm, or {@code null} to derive it | |
| 305 | * @return configured signer | |
| 306 | * @throws IOException if the keyring is empty | |
| 307 | * @throws GeneralSecurityException if the private key cannot be materialised | |
| 308 | */ | |
| 309 | private static ZeroEchoSigner buildSingle(KeyringStore keyring, String keyAlias, String explicitAlgorithm) | |
| 310 | throws IOException, GeneralSecurityException { | |
| 311 | String alias = resolveAlias(keyring, keyAlias); | |
| 312 | KeyringStore.PrivateWithId entry = keyring.getPrivateWithId(alias); | |
| 313 |
2
1. buildSingle : removed conditional - replaced equality check with true → KILLED 2. buildSingle : removed conditional - replaced equality check with false → KILLED |
String algo = explicitAlgorithm != null ? explicitAlgorithm : entry.algorithm(); |
| 314 | Supplier<TagEngine<Signature>> factory = engineFactoryFor(algo, entry.key()); | |
| 315 |
1
1. buildSingle : replaced return value with null for org/egothor/methodatlas/evidence/ZeroEchoSigner::buildSingle → KILLED |
return new ZeroEchoSigner(factory, algo, alias, null); |
| 316 | } | |
| 317 | ||
| 318 | /** | |
| 319 | * Builds a hybrid (classical + post-quantum) signer. | |
| 320 | * | |
| 321 | * @param keyring loaded keyring store | |
| 322 | * @param keyAlias combined alias {@code classicAlias/pqcAlias}; must not be {@code null} | |
| 323 | * @param algo combined algorithm {@code classic+pqc} | |
| 324 | * @return configured signer | |
| 325 | * @throws IOException if the alias is not a valid hybrid pair | |
| 326 | * @throws GeneralSecurityException if either private key cannot be materialised | |
| 327 | */ | |
| 328 | private static ZeroEchoSigner buildHybrid(KeyringStore keyring, String keyAlias, String algo) | |
| 329 | throws IOException, GeneralSecurityException { | |
| 330 | String[] algoParts = splitPair(algo, HYBRID_SEP, | |
| 331 | "Hybrid algorithm must be 'classic+pqc' (e.g. Ed25519+SPHINCS+): " + algo); | |
| 332 |
4
1. buildHybrid : removed conditional - replaced equality check with true → SURVIVED 2. buildHybrid : removed conditional - replaced equality check with false → TIMED_OUT 3. buildHybrid : removed conditional - replaced equality check with false → KILLED 4. buildHybrid : removed conditional - replaced equality check with true → KILLED |
if (keyAlias == null || !keyAlias.contains(ALIAS_SEP)) { |
| 333 | throw new IOException("Hybrid signing requires a 'classicAlias/pqcAlias' key alias via " | |
| 334 | + "-evidence-pack-key-alias; got: " + keyAlias); | |
| 335 | } | |
| 336 | String[] aliasParts = splitPair(keyAlias, ALIAS_SEP, | |
| 337 | "Hybrid key alias must be 'classicAlias/pqcAlias': " + keyAlias); | |
| 338 | ||
| 339 | PrivateKey classicKey = keyring.getPrivate(aliasParts[0]); | |
| 340 | PrivateKey pqcKey = keyring.getPrivate(aliasParts[1]); | |
| 341 | ||
| 342 | HybridSignatureProfile profile = new HybridSignatureProfile( | |
| 343 | algoParts[0], algoParts[1], null, null, HybridSignatureProfile.VerifyRule.AND); | |
| 344 | SignatureContext context = HybridSignatureContexts.sign(profile, classicKey, pqcKey, MAX_BODY_BYTES); | |
| 345 | // The hybrid context is stateful and single-use; the signer is documented as | |
| 346 | // single-use, so reusing the same instance across the one sign() call is safe. | |
| 347 | // The signer retains the context and releases it in close(). | |
| 348 |
2
1. buildHybrid : replaced return value with null for org/egothor/methodatlas/evidence/ZeroEchoSigner::buildHybrid → KILLED 2. lambda$buildHybrid$0 : replaced return value with null for org/egothor/methodatlas/evidence/ZeroEchoSigner::lambda$buildHybrid$0 → KILLED |
return new ZeroEchoSigner(() -> context, algo, keyAlias, context); |
| 349 | } | |
| 350 | ||
| 351 | /** | |
| 352 | * Resolves the per-algorithm ZeroEcho engine factory. RSA keys use RSA-PSS | |
| 353 | * (SHA-256, 32-byte salt) and ECDSA keys use the P-256 curve, matching the | |
| 354 | * dedicated {@link TagEngineBuilder} factories; every other identifier is | |
| 355 | * dispatched through the generic | |
| 356 | * {@link TagEngineBuilder#signature(String, java.security.Key, zeroecho.core.spec.ContextSpec)} | |
| 357 | * factory, which covers Ed25519, Ed448, SPHINCS+, ML-DSA, and SLH-DSA. | |
| 358 | * | |
| 359 | * @param algo canonical algorithm string | |
| 360 | * @param privateKey loaded private key | |
| 361 | * @return supplier producing fresh signing engines | |
| 362 | */ | |
| 363 | private static Supplier<TagEngine<Signature>> engineFactoryFor(String algo, PrivateKey privateKey) { | |
| 364 | String normalised = algo.toUpperCase(Locale.ROOT); | |
| 365 |
2
1. engineFactoryFor : Changed switch default to be first case → KILLED 2. engineFactoryFor : replaced return value with null for org/egothor/methodatlas/evidence/ZeroEchoSigner::engineFactoryFor → KILLED |
return switch (normalised) { |
| 366 | case "RSA", "RSA-PSS", "RSASSA-PSS" -> TagEngineBuilder.rsaSign(privateKey, null); | |
| 367 | case "ECDSA" -> TagEngineBuilder.ecdsaSign(privateKey, null); | |
| 368 | default -> TagEngineBuilder.signature(algo, privateKey, VoidSpec.INSTANCE); | |
| 369 | }; | |
| 370 | } | |
| 371 | ||
| 372 | /** | |
| 373 | * Returns the alias to use: {@code keyAlias} when non-null, otherwise the | |
| 374 | * first alias in the keyring. | |
| 375 | * | |
| 376 | * @param keyring keyring to query | |
| 377 | * @param keyAlias requested alias, or {@code null} | |
| 378 | * @return resolved alias | |
| 379 | * @throws IOException if the keyring contains no aliases | |
| 380 | */ | |
| 381 | private static String resolveAlias(KeyringStore keyring, String keyAlias) throws IOException { | |
| 382 |
2
1. resolveAlias : removed conditional - replaced equality check with false → SURVIVED 2. resolveAlias : removed conditional - replaced equality check with true → KILLED |
if (keyAlias != null) { |
| 383 |
1
1. resolveAlias : replaced return value with "" for org/egothor/methodatlas/evidence/ZeroEchoSigner::resolveAlias → KILLED |
return keyAlias; |
| 384 | } | |
| 385 | List<String> aliases = keyring.aliases(); | |
| 386 |
2
1. resolveAlias : removed conditional - replaced equality check with false → SURVIVED 2. resolveAlias : removed conditional - replaced equality check with true → KILLED |
if (aliases.isEmpty()) { |
| 387 | throw new IOException("Keyring contains no aliases"); | |
| 388 | } | |
| 389 |
1
1. resolveAlias : replaced return value with "" for org/egothor/methodatlas/evidence/ZeroEchoSigner::resolveAlias → KILLED |
return aliases.get(0); |
| 390 | } | |
| 391 | ||
| 392 | /** | |
| 393 | * Splits {@code value} into exactly two non-blank halves around {@code sep}. | |
| 394 | * | |
| 395 | * @param value value to split | |
| 396 | * @param sep literal separator | |
| 397 | * @param message error message used when the split does not yield two halves | |
| 398 | * @return a two-element array of trimmed, non-blank parts | |
| 399 | * @throws IOException if {@code value} does not split into two non-blank parts | |
| 400 | */ | |
| 401 | private static String[] splitPair(String value, String sep, String message) throws IOException { | |
| 402 | int idx = value.indexOf(sep); | |
| 403 |
7
1. splitPair : changed conditional boundary → SURVIVED 2. splitPair : removed conditional - replaced comparison check with false → TIMED_OUT 3. splitPair : changed conditional boundary → TIMED_OUT 4. splitPair : Replaced integer subtraction with addition → TIMED_OUT 5. splitPair : removed conditional - replaced comparison check with true → TIMED_OUT 6. splitPair : removed conditional - replaced comparison check with false → KILLED 7. splitPair : removed conditional - replaced comparison check with true → KILLED |
if (idx <= 0 || idx >= value.length() - sep.length()) { |
| 404 | throw new IOException(message); | |
| 405 | } | |
| 406 | String first = value.substring(0, idx).trim(); | |
| 407 |
1
1. splitPair : Replaced integer addition with subtraction → KILLED |
String second = value.substring(idx + sep.length()).trim(); |
| 408 |
4
1. splitPair : removed conditional - replaced equality check with false → TIMED_OUT 2. splitPair : removed conditional - replaced equality check with true → TIMED_OUT 3. splitPair : removed conditional - replaced equality check with false → KILLED 4. splitPair : removed conditional - replaced equality check with true → KILLED |
if (first.isEmpty() || second.isEmpty()) { |
| 409 | throw new IOException(message); | |
| 410 | } | |
| 411 |
1
1. splitPair : replaced return value with null for org/egothor/methodatlas/evidence/ZeroEchoSigner::splitPair → KILLED |
return new String[] { first, second }; |
| 412 | } | |
| 413 | } | |
Mutations | ||
| 157 |
1.1 2.2 |
|
| 159 |
1.1 |
|
| 174 |
1.1 |
|
| 183 |
1.1 |
|
| 206 |
1.1 |
|
| 232 |
1.1 2.2 3.3 4.4 |
|
| 236 |
1.1 |
|
| 237 |
1.1 |
|
| 255 |
1.1 |
|
| 256 |
1.1 2.2 3.3 4.4 |
|
| 257 |
1.1 |
|
| 259 |
1.1 |
|
| 283 |
1.1 |
|
| 288 |
1.1 |
|
| 313 |
1.1 2.2 |
|
| 315 |
1.1 |
|
| 332 |
1.1 2.2 3.3 4.4 |
|
| 348 |
1.1 2.2 |
|
| 365 |
1.1 2.2 |
|
| 382 |
1.1 2.2 |
|
| 383 |
1.1 |
|
| 386 |
1.1 2.2 |
|
| 389 |
1.1 |
|
| 403 |
1.1 2.2 3.3 4.4 5.5 6.6 7.7 |
|
| 407 |
1.1 |
|
| 408 |
1.1 2.2 3.3 4.4 |
|
| 411 |
1.1 |