• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

jreleaser / jreleaser / #555

22 Nov 2025 01:39PM UTC coverage: 48.203% (-0.05%) from 48.253%
#555

push

github

aalmiray
feat(jdks): Allow filtering by platform

Closes #2000

Co-authored-by: Ixchel Ruiz <ixchelruiz@yahoo.com>

0 of 42 new or added lines in 5 files covered. (0.0%)

128 existing lines in 8 files now uncovered.

26013 of 53965 relevant lines covered (48.2%)

0.48 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

45.45
/core/jreleaser-engine/src/main/java/org/jreleaser/engine/sign/Signer.java
1
/*
2
 * SPDX-License-Identifier: Apache-2.0
3
 *
4
 * Copyright 2020-2025 The JReleaser authors.
5
 *
6
 * Licensed under the Apache License, Version 2.0 (the "License");
7
 * you may not use this file except in compliance with the License.
8
 * You may obtain a copy of the License at
9
 *
10
 *     https://www.apache.org/licenses/LICENSE-2.0
11
 *
12
 * Unless required by applicable law or agreed to in writing, software
13
 * distributed under the License is distributed on an "AS IS" BASIS,
14
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
 * See the License for the specific language governing permissions and
16
 * limitations under the License.
17
 */
18
package org.jreleaser.engine.sign;
19

20
import org.bouncycastle.jce.provider.BouncyCastleProvider;
21
import org.bouncycastle.openpgp.PGPCompressedData;
22
import org.bouncycastle.openpgp.PGPException;
23
import org.bouncycastle.openpgp.PGPObjectFactory;
24
import org.bouncycastle.openpgp.PGPPublicKey;
25
import org.bouncycastle.openpgp.PGPSignature;
26
import org.bouncycastle.openpgp.PGPSignatureGenerator;
27
import org.bouncycastle.openpgp.PGPUtil;
28
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider;
29
import org.jreleaser.bundle.RB;
30
import org.jreleaser.model.api.signing.Keyring;
31
import org.jreleaser.model.api.signing.SigningException;
32
import org.jreleaser.model.internal.JReleaserContext;
33
import org.jreleaser.model.internal.catalog.sbom.SbomCataloger;
34
import org.jreleaser.model.internal.common.Artifact;
35
import org.jreleaser.model.internal.distributions.Distribution;
36
import org.jreleaser.model.internal.signing.Signing;
37
import org.jreleaser.model.internal.util.Artifacts;
38
import org.jreleaser.model.spi.catalog.sbom.SbomCatalogerProcessorHelper;
39
import org.jreleaser.sdk.signing.GpgCommandSigner;
40
import org.jreleaser.sdk.signing.SigningUtils;
41
import org.jreleaser.sdk.tool.Cosign;
42
import org.jreleaser.sdk.tool.ToolException;
43
import org.jreleaser.util.Algorithm;
44

45
import java.io.BufferedInputStream;
46
import java.io.IOException;
47
import java.io.InputStream;
48
import java.nio.file.Files;
49
import java.nio.file.Path;
50
import java.util.ArrayList;
51
import java.util.List;
52
import java.util.function.Predicate;
53

54
import static java.nio.file.Files.newInputStream;
55
import static java.util.stream.Collectors.toList;
56
import static org.jreleaser.model.api.signing.Signing.KEY_SKIP_SIGNING;
57
import static org.jreleaser.util.StringUtils.isNotBlank;
58

59
/**
60
 * @author Andres Almiray
61
 * @since 0.1.0
62
 */
63
public final class Signer {
64
    private Signer() {
65
        // noop
66
    }
67

68
    public static void sign(JReleaserContext context) throws SigningException {
69
        context.getLogger().info(RB.$("signing.header"));
1✔
70
        context.getLogger().increaseIndent();
1✔
71
        context.getLogger().setPrefix("sign");
1✔
72

73
        if (!context.getModel().getSigning().isEnabled()) {
1✔
74
            context.getLogger().info(RB.$("signing.not.enabled"));
1✔
75
            context.getLogger().restorePrefix();
1✔
76
            context.getLogger().decreaseIndent();
1✔
77
            return;
1✔
78
        }
79

80
        try {
81
            if (context.getModel().getSigning().getMode() == org.jreleaser.model.Signing.Mode.COMMAND) {
1✔
82
                cmdSign(context);
×
83
            } else if (context.getModel().getSigning().getMode() == org.jreleaser.model.Signing.Mode.COSIGN) {
1✔
84
                cosignSign(context);
×
85
            } else {
86
                bcSign(context);
1✔
87
            }
88
        } finally {
89
            context.getLogger().restorePrefix();
1✔
90
            context.getLogger().decreaseIndent();
1✔
91
        }
92
    }
1✔
93

94
    private static void cmdSign(JReleaserContext context) throws SigningException {
95
        List<SigningUtils.FilePair> files = collectArtifacts(context, pair -> SigningUtils.isValid(context.asImmutable(), null, pair));
×
96
        if (files.isEmpty()) {
×
97
            context.getLogger().info(RB.$("signing.no.match"));
×
98
            return;
×
99
        }
100

101
        files = files.stream()
×
102
            .filter(SigningUtils.FilePair::isInvalid)
×
103
            .collect(toList());
×
104

105
        if (files.isEmpty()) {
×
106
            context.getLogger().info(RB.$("signing.up.to.date"));
×
107
            return;
×
108
        }
109

110
        sign(context, files);
×
111
        if (context.getModel().getSigning().isVerify()) {
×
112
            verify(context, files);
×
113
        }
114
    }
×
115

116
    private static void cosignSign(JReleaserContext context) throws SigningException {
117
        Signing signing = context.getModel().getSigning();
×
118

119
        Cosign cosign = new Cosign(context.asImmutable(), signing.getCosign().getVersion());
×
120
        try {
121
            if (!cosign.setup()) {
×
122
                context.getLogger().warn(RB.$("tool_unavailable", "cosign"));
×
123
                return;
×
124
            }
125
        } catch (ToolException e) {
×
126
            throw new SigningException(e.getMessage(), e);
×
127
        }
×
128

129
        String privateKey = signing.getCosign().getPrivateKeyFile();
×
130
        String publicKey = signing.getCosign().getPublicKeyFile();
×
131

132
        Path privateKeyFile = isNotBlank(privateKey) ? context.getBasedir().resolve(privateKey) : null;
×
133
        Path publicKeyFile = isNotBlank(publicKey) ? context.getBasedir().resolve(publicKey) : null;
×
134
        String password = signing.getPassphrase();
×
135

136
        boolean forceSign = false;
×
137
        if (null == privateKeyFile) {
×
138
            privateKeyFile = signing.getCosign().getResolvedPrivateKeyFilePath(context);
×
139
            publicKeyFile = privateKeyFile.resolveSibling("cosign.pub");
×
140
            if (!Files.exists(privateKeyFile)) {
×
141
                privateKeyFile = cosign.generateKeyPair(password);
×
142
                forceSign = true;
×
143
            }
144
        }
145
        Path thePublicKeyFile = publicKeyFile;
×
146

147
        List<SigningUtils.FilePair> files = collectArtifacts(context, forceSign, pair -> isValid(context, cosign, thePublicKeyFile, pair));
×
148
        if (files.isEmpty()) {
×
149
            context.getLogger().info(RB.$("signing.no.match"));
×
150
            return;
×
151
        }
152

153
        files = files.stream()
×
154
            .filter(SigningUtils.FilePair::isInvalid)
×
155
            .collect(toList());
×
156

157
        if (files.isEmpty()) {
×
158
            context.getLogger().info(RB.$("signing.up.to.date"));
×
159
            return;
×
160
        }
161

162
        if (!cosign.checkPassword(privateKeyFile, password)) {
×
163
            context.getLogger().warn(RB.$("WARN_cosign_password_does_not_match", "cosign"));
×
164
            return;
×
165
        }
166

167
        sign(context, files, cosign, privateKeyFile, password);
×
168
        verify(context, files, cosign, publicKeyFile);
×
169
    }
×
170

171
    private static void bcSign(JReleaserContext context) throws SigningException {
172
        Keyring keyring = context.createKeyring();
1✔
173

174
        List<SigningUtils.FilePair> files = collectArtifacts(context, pair -> SigningUtils.isValid(context.asImmutable(), keyring, pair));
1✔
175
        if (files.isEmpty()) {
1✔
176
            context.getLogger().info(RB.$("signing.no.match"));
×
177
            return;
×
178
        }
179

180
        files = files.stream()
1✔
181
            .filter(SigningUtils.FilePair::isInvalid)
1✔
182
            .collect(toList());
1✔
183

184
        if (files.isEmpty()) {
1✔
UNCOV
185
            context.getLogger().info(RB.$("signing.up.to.date"));
×
UNCOV
186
            return;
×
187
        }
188

189
        sign(context, keyring, files);
1✔
190
        if (context.getModel().getSigning().isVerify()) {
1✔
191
            verify(context, keyring, files);
1✔
192
        }
193
    }
1✔
194

195

196
    private static void verify(JReleaserContext context, Keyring keyring, List<SigningUtils.FilePair> files) throws SigningException {
197
        if (null == keyring) {
1✔
198
            verify(context, files);
×
199
            return;
×
200
        }
201

202
        context.getLogger().debug(RB.$("signing.verify.signatures"), files.size());
1✔
203

204
        for (SigningUtils.FilePair pair : files) {
1✔
205
            pair.setValid(verify(context, keyring, pair));
1✔
206

207
            if (!pair.isValid()) {
1✔
208
                throw new SigningException(RB.$("ERROR_signing_verify_file",
×
209
                    context.relativizeToBasedir(pair.getInputFile()),
×
210
                    context.relativizeToBasedir(pair.getSignatureFile())));
×
211
            }
212
        }
1✔
213
    }
1✔
214

215
    private static void verify(JReleaserContext context, List<SigningUtils.FilePair> files) throws SigningException {
216
        context.getLogger().debug(RB.$("signing.verify.signatures"), files.size());
×
217

218
        for (SigningUtils.FilePair pair : files) {
×
219
            pair.setValid(SigningUtils.verify(context.asImmutable(), pair));
×
220

221
            if (!pair.isValid()) {
×
222
                throw new SigningException(RB.$("ERROR_signing_verify_file",
×
223
                    context.relativizeToBasedir(pair.getInputFile()),
×
224
                    context.relativizeToBasedir(pair.getSignatureFile())));
×
225
            }
226
        }
×
227
    }
×
228

229
    private static boolean verify(JReleaserContext context, Keyring keyring, SigningUtils.FilePair filePair) throws SigningException {
230
        context.getLogger().setPrefix("verify");
1✔
231

232
        context.getLogger().debug("{}",
1✔
233
            context.relativizeToBasedir(filePair.getSignatureFile()));
1✔
234

235
        try (InputStream sigInputStream = PGPUtil.getDecoderStream(
1✔
236
            new BufferedInputStream(
237
                newInputStream(filePair.getSignatureFile())))) {
1✔
238
            PGPObjectFactory pgpObjFactory = new PGPObjectFactory(sigInputStream, keyring.getKeyFingerPrintCalculator());
1✔
239
            Iterable<?> pgpSigList = null;
1✔
240

241
            Object obj = pgpObjFactory.nextObject();
1✔
242
            if (obj instanceof PGPCompressedData) {
1✔
243
                PGPCompressedData c1 = (PGPCompressedData) obj;
1✔
244
                pgpObjFactory = new PGPObjectFactory(c1.getDataStream(), keyring.getKeyFingerPrintCalculator());
1✔
245
                pgpSigList = (Iterable<?>) pgpObjFactory.nextObject();
1✔
246
            } else {
1✔
247
                pgpSigList = (Iterable<?>) obj;
×
248
            }
249

250
            PGPSignature sig = (PGPSignature) pgpSigList.iterator().next();
1✔
251
            try (InputStream fileInputStream = new BufferedInputStream(newInputStream(filePair.getInputFile()))) {
1✔
252
                PGPPublicKey pubKey = keyring.readPublicKey();
1✔
253
                sig.init(new JcaPGPContentVerifierBuilderProvider()
1✔
254
                    .setProvider(BouncyCastleProvider.PROVIDER_NAME), pubKey);
1✔
255

256
                int ch;
257
                while ((ch = fileInputStream.read()) >= 0) {
1✔
258
                    sig.update((byte) ch);
1✔
259
                }
260
            }
261

262
            return sig.verify();
1✔
263
        } catch (IOException | PGPException e) {
×
264
            throw new SigningException(RB.$("ERROR_signing_verify_signature",
×
265
                context.relativizeToBasedir(filePair.getInputFile())), e);
×
266
        } finally {
267
            context.getLogger().restorePrefix();
1✔
268
        }
269
    }
270

271
    private static void sign(JReleaserContext context, List<SigningUtils.FilePair> files,
272
                             Cosign cosign, Path privateKeyFile, String password) throws SigningException {
273
        Path signaturesDirectory = context.getSignaturesDirectory();
×
274

275
        try {
276
            Files.createDirectories(signaturesDirectory);
×
277
        } catch (IOException e) {
×
278
            throw new SigningException(RB.$("ERROR_signing_create_signature_dir"), e);
×
279
        }
×
280

281
        context.getLogger().debug(RB.$("signing.signing.files"),
×
282
            files.size(), context.relativizeToBasedir(signaturesDirectory));
×
283

284
        for (SigningUtils.FilePair pair : files) {
×
285
            cosign.signBlob(privateKeyFile, password, pair.getInputFile(), signaturesDirectory);
×
286
        }
×
287
    }
×
288

289
    private static void verify(JReleaserContext context, List<SigningUtils.FilePair> files,
290
                               Cosign cosign, Path publicKeyFile) throws SigningException {
291
        context.getLogger().debug(RB.$("signing.verify.signatures"), files.size());
×
292

293
        context.getLogger().setPrefix("verify");
×
294
        try {
295
            for (SigningUtils.FilePair pair : files) {
×
296
                cosign.verifyBlob(publicKeyFile, pair.getSignatureFile(), pair.getInputFile());
×
297
                pair.setValid(true);
×
298

299
                if (!pair.isValid()) {
×
300
                    throw new SigningException(RB.$("ERROR_signing_verify_file",
×
301
                        context.relativizeToBasedir(pair.getInputFile()),
×
302
                        context.relativizeToBasedir(pair.getSignatureFile())));
×
303
                }
304
            }
×
305
        } finally {
306
            context.getLogger().restorePrefix();
×
307
        }
308
    }
×
309

310
    private static void sign(JReleaserContext context, List<SigningUtils.FilePair> files) throws SigningException {
311
        Path signaturesDirectory = context.getSignaturesDirectory();
×
312

313
        try {
314
            Files.createDirectories(signaturesDirectory);
×
315
        } catch (IOException e) {
×
316
            throw new SigningException(RB.$("ERROR_signing_create_signature_dir"), e);
×
317
        }
×
318

319
        context.getLogger().debug(RB.$("signing.signing.files"),
×
320
            files.size(), context.relativizeToBasedir(signaturesDirectory));
×
321

322
        GpgCommandSigner commandSigner = SigningUtils.initCommandSigner(context.asImmutable());
×
323

324
        for (SigningUtils.FilePair pair : files) {
×
325
            SigningUtils.sign(context.asImmutable(), commandSigner, pair.getInputFile(), pair.getSignatureFile());
×
326
        }
×
327
    }
×
328

329
    private static void sign(JReleaserContext context, Keyring keyring, List<SigningUtils.FilePair> files) throws SigningException {
330
        Path signaturesDirectory = context.getSignaturesDirectory();
1✔
331

332
        try {
333
            Files.createDirectories(signaturesDirectory);
1✔
334
        } catch (IOException e) {
×
335
            throw new SigningException(RB.$("ERROR_signing_create_signature_dir"), e);
×
336
        }
1✔
337

338
        context.getLogger().debug(RB.$("signing.signing.files"),
1✔
339
            files.size(), context.relativizeToBasedir(signaturesDirectory));
1✔
340

341
        PGPSignatureGenerator signatureGenerator = SigningUtils.initSignatureGenerator(context.asImmutable(), keyring);
1✔
342

343
        for (SigningUtils.FilePair pair : files) {
1✔
344
            SigningUtils.sign(context.asImmutable(), signatureGenerator, pair.getInputFile(), pair.getSignatureFile());
1✔
345
        }
1✔
346
    }
1✔
347

348
    private static List<SigningUtils.FilePair> collectArtifacts(JReleaserContext context, Predicate<SigningUtils.FilePair> validator) {
349
        return collectArtifacts(context, false, validator);
1✔
350
    }
351

352
    private static List<SigningUtils.FilePair> collectArtifacts(JReleaserContext context, boolean forceSign, Predicate<SigningUtils.FilePair> validator) {
353
        List<SigningUtils.FilePair> files = new ArrayList<>();
1✔
354

355
        Signing signing = context.getModel().getSigning();
1✔
356
        Path signaturesDirectory = context.getSignaturesDirectory();
1✔
357

358
        String extension = ".sig";
1✔
359
        if (signing.getMode() != org.jreleaser.model.Signing.Mode.COSIGN) {
1✔
360
            extension = signing.isArmored() ? ".asc" : ".sig";
1✔
361
        }
362

363
        if (signing.isFiles()) {
1✔
364
            for (Artifact artifact : Artifacts.resolveFiles(context)) {
1✔
365
                if (!artifact.isActiveAndSelected() || artifact.extraPropertyIsTrue(KEY_SKIP_SIGNING) ||
1✔
366
                    artifact.isOptional(context) && !artifact.resolvedPathExists()) continue;
×
367
                Path input = artifact.getEffectivePath(context);
×
368
                Path output = signaturesDirectory.resolve(input.getFileName().toString().concat(extension));
×
369
                SigningUtils.FilePair pair = new SigningUtils.FilePair(input, output);
×
370
                if (!forceSign) pair.setValid(validator.test(pair));
×
371
                files.add(pair);
×
372
            }
×
373
        }
374

375
        if (signing.isArtifacts()) {
1✔
376
            for (Distribution distribution : context.getModel().getActiveDistributions()) {
1✔
377
                if (distribution.extraPropertyIsTrue(KEY_SKIP_SIGNING)) continue;
1✔
378
                for (Artifact artifact : distribution.getArtifacts()) {
1✔
379
                    if (!artifact.isActiveAndSelected() || artifact.extraPropertyIsTrue(KEY_SKIP_SIGNING)) continue;
1✔
380
                    Path input = artifact.getEffectivePath(context, distribution);
1✔
381
                    if (artifact.isOptional(context) && !artifact.resolvedPathExists()) continue;
1✔
382
                    Path output = signaturesDirectory.resolve(input.getFileName().toString().concat(extension));
1✔
383
                    SigningUtils.FilePair pair = new SigningUtils.FilePair(input, output);
1✔
384
                    if (!forceSign) pair.setValid(validator.test(pair));
1✔
385
                    files.add(pair);
1✔
386
                }
1✔
387
            }
1✔
388
        }
389

390
        if (signing.isCatalogs()) {
1✔
391
            List<? extends SbomCataloger<?>> catalogers = context.getModel().getCatalog().getSbom().findAllActiveSbomCatalogers();
1✔
392
            for (SbomCataloger<?> cataloger : catalogers) {
1✔
393
                if (!cataloger.getPack().isEnabled()) continue;
1✔
394
                for (Artifact artifact : SbomCatalogerProcessorHelper.resolveArtifacts(context, cataloger)) {
1✔
395
                    Path input = artifact.getEffectivePath(context);
1✔
396
                    Path output = signaturesDirectory.resolve(input.getFileName().toString().concat(extension));
1✔
397
                    SigningUtils.FilePair pair = new SigningUtils.FilePair(input, output);
1✔
398
                    if (!forceSign) pair.setValid(validator.test(pair));
1✔
399
                    files.add(pair);
1✔
400
                }
1✔
401
            }
1✔
402
        }
403

404
        if (signing.isChecksums()) {
1✔
405
            for (Algorithm algorithm : context.getModel().getChecksum().getAlgorithms()) {
1✔
406
                Path checksums = context.getChecksumsDirectory()
1✔
407
                    .resolve(context.getModel().getChecksum().getResolvedName(context, algorithm));
1✔
408
                if (Files.exists(checksums)) {
1✔
409
                    Path output = signaturesDirectory.resolve(checksums.getFileName().toString().concat(extension));
1✔
410
                    SigningUtils.FilePair pair = new SigningUtils.FilePair(checksums, output);
1✔
411
                    if (!forceSign) pair.setValid(validator.test(pair));
1✔
412
                    files.add(pair);
1✔
413
                }
414
            }
1✔
415
        }
416

417
        return files;
1✔
418
    }
419

420
    private static boolean isValid(JReleaserContext context, Cosign cosign, Path publicKeyFile, SigningUtils.FilePair pair) {
421
        if (Files.notExists(pair.getSignatureFile())) {
×
422
            context.getLogger().debug(RB.$("signing.signature.not.exist"),
×
423
                context.relativizeToBasedir(pair.getSignatureFile()));
×
424
            return false;
×
425
        }
426

427
        if (pair.getInputFile().toFile().lastModified() > pair.getSignatureFile().toFile().lastModified()) {
×
428
            context.getLogger().debug(RB.$("signing.file.newer"),
×
429
                context.relativizeToBasedir(pair.getInputFile()),
×
430
                context.relativizeToBasedir(pair.getSignatureFile()));
×
431
            return false;
×
432
        }
433

434
        try {
435
            cosign.verifyBlob(publicKeyFile, pair.getSignatureFile(), pair.getInputFile());
×
436
            return true;
×
437
        } catch (SigningException e) {
×
438
            return false;
×
439
        }
440
    }
441
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc