All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jreleaser.engine.sign.Signer Maven / Gradle / Ivy

The newest version!
/*
 * SPDX-License-Identifier: Apache-2.0
 *
 * Copyright 2020-2024 The JReleaser authors.
 *
 * 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
 *
 *     https://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 org.jreleaser.engine.sign;

import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openpgp.PGPCompressedData;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPObjectFactory;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.openpgp.PGPSignatureGenerator;
import org.bouncycastle.openpgp.PGPUtil;
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider;
import org.jreleaser.bundle.RB;
import org.jreleaser.model.api.signing.Keyring;
import org.jreleaser.model.api.signing.SigningException;
import org.jreleaser.model.internal.JReleaserContext;
import org.jreleaser.model.internal.catalog.sbom.SbomCataloger;
import org.jreleaser.model.internal.common.Artifact;
import org.jreleaser.model.internal.distributions.Distribution;
import org.jreleaser.model.internal.signing.Signing;
import org.jreleaser.model.internal.util.Artifacts;
import org.jreleaser.model.spi.catalog.sbom.SbomCatalogerProcessorHelper;
import org.jreleaser.sdk.signing.GpgCommandSigner;
import org.jreleaser.sdk.signing.SigningUtils;
import org.jreleaser.sdk.tool.Cosign;
import org.jreleaser.sdk.tool.ToolException;
import org.jreleaser.util.Algorithm;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;

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

/**
 * @author Andres Almiray
 * @since 0.1.0
 */
public final class Signer {
    private Signer() {
        // noop
    }

    public static void sign(JReleaserContext context) throws SigningException {
        context.getLogger().info(RB.$("signing.header"));
        context.getLogger().increaseIndent();
        context.getLogger().setPrefix("sign");

        if (!context.getModel().getSigning().isEnabled()) {
            context.getLogger().info(RB.$("signing.not.enabled"));
            context.getLogger().restorePrefix();
            context.getLogger().decreaseIndent();
            return;
        }

        try {
            if (context.getModel().getSigning().getMode() == org.jreleaser.model.Signing.Mode.COMMAND) {
                cmdSign(context);
            } else if (context.getModel().getSigning().getMode() == org.jreleaser.model.Signing.Mode.COSIGN) {
                cosignSign(context);
            } else {
                bcSign(context);
            }
        } finally {
            context.getLogger().restorePrefix();
            context.getLogger().decreaseIndent();
        }
    }

    private static void cmdSign(JReleaserContext context) throws SigningException {
        List files = collectArtifacts(context, pair -> SigningUtils.isValid(context.asImmutable(), null, pair));
        if (files.isEmpty()) {
            context.getLogger().info(RB.$("signing.no.match"));
            return;
        }

        files = files.stream()
            .filter(SigningUtils.FilePair::isInvalid)
            .collect(toList());

        if (files.isEmpty()) {
            context.getLogger().info(RB.$("signing.up.to.date"));
            return;
        }

        sign(context, files);
        if (context.getModel().getSigning().isVerify()) {
            verify(context, files);
        }
    }

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

        Cosign cosign = new Cosign(context.asImmutable(), signing.getCosign().getVersion());
        try {
            if (!cosign.setup()) {
                context.getLogger().warn(RB.$("tool_unavailable", "cosign"));
                return;
            }
        } catch (ToolException e) {
            throw new SigningException(e.getMessage(), e);
        }

        String privateKey = signing.getCosign().getPrivateKeyFile();
        String publicKey = signing.getCosign().getPublicKeyFile();

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

        boolean forceSign = false;
        if (null == privateKeyFile) {
            privateKeyFile = signing.getCosign().getResolvedPrivateKeyFilePath(context);
            publicKeyFile = privateKeyFile.resolveSibling("cosign.pub");
            if (!Files.exists(privateKeyFile)) {
                privateKeyFile = cosign.generateKeyPair(password);
                forceSign = true;
            }
        }
        Path thePublicKeyFile = publicKeyFile;

        List files = collectArtifacts(context, forceSign, pair -> isValid(context, cosign, thePublicKeyFile, pair));
        if (files.isEmpty()) {
            context.getLogger().info(RB.$("signing.no.match"));
            return;
        }

        files = files.stream()
            .filter(SigningUtils.FilePair::isInvalid)
            .collect(toList());

        if (files.isEmpty()) {
            context.getLogger().info(RB.$("signing.up.to.date"));
            return;
        }

        if (!cosign.checkPassword(privateKeyFile, password)) {
            context.getLogger().warn(RB.$("WARN_cosign_password_does_not_match", "cosign"));
            return;
        }

        sign(context, files, cosign, privateKeyFile, password);
        verify(context, files, cosign, publicKeyFile);
    }

    private static void bcSign(JReleaserContext context) throws SigningException {
        Keyring keyring = context.createKeyring();

        List files = collectArtifacts(context, pair -> SigningUtils.isValid(context.asImmutable(), keyring, pair));
        if (files.isEmpty()) {
            context.getLogger().info(RB.$("signing.no.match"));
            return;
        }

        files = files.stream()
            .filter(SigningUtils.FilePair::isInvalid)
            .collect(toList());

        if (files.isEmpty()) {
            context.getLogger().info(RB.$("signing.up.to.date"));
            return;
        }

        sign(context, keyring, files);
        if (context.getModel().getSigning().isVerify()) {
            verify(context, keyring, files);
        }
    }


    private static void verify(JReleaserContext context, Keyring keyring, List files) throws SigningException {
        if (null == keyring) {
            verify(context, files);
            return;
        }

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

        for (SigningUtils.FilePair pair : files) {
            pair.setValid(verify(context, keyring, pair));

            if (!pair.isValid()) {
                throw new SigningException(RB.$("ERROR_signing_verify_file",
                    context.relativizeToBasedir(pair.getInputFile()),
                    context.relativizeToBasedir(pair.getSignatureFile())));
            }
        }
    }

    private static void verify(JReleaserContext context, List files) throws SigningException {
        context.getLogger().debug(RB.$("signing.verify.signatures"), files.size());

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

            if (!pair.isValid()) {
                throw new SigningException(RB.$("ERROR_signing_verify_file",
                    context.relativizeToBasedir(pair.getInputFile()),
                    context.relativizeToBasedir(pair.getSignatureFile())));
            }
        }
    }

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

        context.getLogger().debug("{}",
            context.relativizeToBasedir(filePair.getSignatureFile()));

        try (InputStream sigInputStream = PGPUtil.getDecoderStream(
            new BufferedInputStream(
                newInputStream(filePair.getSignatureFile())))) {
            PGPObjectFactory pgpObjFactory = new PGPObjectFactory(sigInputStream, keyring.getKeyFingerPrintCalculator());
            Iterable pgpSigList = null;

            Object obj = pgpObjFactory.nextObject();
            if (obj instanceof PGPCompressedData) {
                PGPCompressedData c1 = (PGPCompressedData) obj;
                pgpObjFactory = new PGPObjectFactory(c1.getDataStream(), keyring.getKeyFingerPrintCalculator());
                pgpSigList = (Iterable) pgpObjFactory.nextObject();
            } else {
                pgpSigList = (Iterable) obj;
            }

            PGPSignature sig = (PGPSignature) pgpSigList.iterator().next();
            try (InputStream fileInputStream = new BufferedInputStream(newInputStream(filePair.getInputFile()))) {
                PGPPublicKey pubKey = keyring.readPublicKey();
                sig.init(new JcaPGPContentVerifierBuilderProvider()
                    .setProvider(BouncyCastleProvider.PROVIDER_NAME), pubKey);

                int ch;
                while ((ch = fileInputStream.read()) >= 0) {
                    sig.update((byte) ch);
                }
            }

            return sig.verify();
        } catch (IOException | PGPException e) {
            throw new SigningException(RB.$("ERROR_signing_verify_signature",
                context.relativizeToBasedir(filePair.getInputFile())), e);
        } finally {
            context.getLogger().restorePrefix();
        }
    }

    private static void sign(JReleaserContext context, List files,
                             Cosign cosign, Path privateKeyFile, String password) throws SigningException {
        Path signaturesDirectory = context.getSignaturesDirectory();

        try {
            Files.createDirectories(signaturesDirectory);
        } catch (IOException e) {
            throw new SigningException(RB.$("ERROR_signing_create_signature_dir"), e);
        }

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

        for (SigningUtils.FilePair pair : files) {
            cosign.signBlob(privateKeyFile, password, pair.getInputFile(), signaturesDirectory);
        }
    }

    private static void verify(JReleaserContext context, List files,
                               Cosign cosign, Path publicKeyFile) throws SigningException {
        context.getLogger().debug(RB.$("signing.verify.signatures"), files.size());

        context.getLogger().setPrefix("verify");
        try {
            for (SigningUtils.FilePair pair : files) {
                cosign.verifyBlob(publicKeyFile, pair.getSignatureFile(), pair.getInputFile());
                pair.setValid(true);

                if (!pair.isValid()) {
                    throw new SigningException(RB.$("ERROR_signing_verify_file",
                        context.relativizeToBasedir(pair.getInputFile()),
                        context.relativizeToBasedir(pair.getSignatureFile())));
                }
            }
        } finally {
            context.getLogger().restorePrefix();
        }
    }

    private static void sign(JReleaserContext context, List files) throws SigningException {
        Path signaturesDirectory = context.getSignaturesDirectory();

        try {
            Files.createDirectories(signaturesDirectory);
        } catch (IOException e) {
            throw new SigningException(RB.$("ERROR_signing_create_signature_dir"), e);
        }

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

        GpgCommandSigner commandSigner = SigningUtils.initCommandSigner(context.asImmutable());

        for (SigningUtils.FilePair pair : files) {
            SigningUtils.sign(context.asImmutable(), commandSigner, pair.getInputFile(), pair.getSignatureFile());
        }
    }

    private static void sign(JReleaserContext context, Keyring keyring, List files) throws SigningException {
        Path signaturesDirectory = context.getSignaturesDirectory();

        try {
            Files.createDirectories(signaturesDirectory);
        } catch (IOException e) {
            throw new SigningException(RB.$("ERROR_signing_create_signature_dir"), e);
        }

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

        PGPSignatureGenerator signatureGenerator = SigningUtils.initSignatureGenerator(context.asImmutable(), keyring);

        for (SigningUtils.FilePair pair : files) {
            SigningUtils.sign(context.asImmutable(), signatureGenerator, pair.getInputFile(), pair.getSignatureFile());
        }
    }

    private static List collectArtifacts(JReleaserContext context, Predicate validator) {
        return collectArtifacts(context, false, validator);
    }

    private static List collectArtifacts(JReleaserContext context, boolean forceSign, Predicate validator) {
        List files = new ArrayList<>();

        Signing signing = context.getModel().getSigning();
        Path signaturesDirectory = context.getSignaturesDirectory();

        String extension = ".sig";
        if (signing.getMode() != org.jreleaser.model.Signing.Mode.COSIGN) {
            extension = signing.isArmored() ? ".asc" : ".sig";
        }

        if (signing.isFiles()) {
            for (Artifact artifact : Artifacts.resolveFiles(context)) {
                if (!artifact.isActiveAndSelected() || artifact.extraPropertyIsTrue(KEY_SKIP_SIGNING) ||
                    artifact.isOptional(context) && !artifact.resolvedPathExists()) continue;
                Path input = artifact.getEffectivePath(context);
                Path output = signaturesDirectory.resolve(input.getFileName().toString().concat(extension));
                SigningUtils.FilePair pair = new SigningUtils.FilePair(input, output);
                if (!forceSign) pair.setValid(validator.test(pair));
                files.add(pair);
            }
        }

        if (signing.isArtifacts()) {
            for (Distribution distribution : context.getModel().getActiveDistributions()) {
                if (distribution.extraPropertyIsTrue(KEY_SKIP_SIGNING)) continue;
                for (Artifact artifact : distribution.getArtifacts()) {
                    if (!artifact.isActiveAndSelected() || artifact.extraPropertyIsTrue(KEY_SKIP_SIGNING)) continue;
                    Path input = artifact.getEffectivePath(context, distribution);
                    if (artifact.isOptional(context) && !artifact.resolvedPathExists()) continue;
                    Path output = signaturesDirectory.resolve(input.getFileName().toString().concat(extension));
                    SigningUtils.FilePair pair = new SigningUtils.FilePair(input, output);
                    if (!forceSign) pair.setValid(validator.test(pair));
                    files.add(pair);
                }
            }
        }

        if (signing.isCatalogs()) {
            List> catalogers = context.getModel().getCatalog().getSbom().findAllActiveSbomCatalogers();
            for (SbomCataloger cataloger : catalogers) {
                if (!cataloger.getPack().isEnabled()) continue;
                for (Artifact artifact : SbomCatalogerProcessorHelper.resolveArtifacts(context, cataloger)) {
                    Path input = artifact.getEffectivePath(context);
                    Path output = signaturesDirectory.resolve(input.getFileName().toString().concat(extension));
                    SigningUtils.FilePair pair = new SigningUtils.FilePair(input, output);
                    if (!forceSign) pair.setValid(validator.test(pair));
                    files.add(pair);
                }
            }
        }

        if (signing.isChecksums()) {
            for (Algorithm algorithm : context.getModel().getChecksum().getAlgorithms()) {
                Path checksums = context.getChecksumsDirectory()
                    .resolve(context.getModel().getChecksum().getResolvedName(context, algorithm));
                if (Files.exists(checksums)) {
                    Path output = signaturesDirectory.resolve(checksums.getFileName().toString().concat(extension));
                    SigningUtils.FilePair pair = new SigningUtils.FilePair(checksums, output);
                    if (!forceSign) pair.setValid(validator.test(pair));
                    files.add(pair);
                }
            }
        }

        return files;
    }

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

        if (pair.getInputFile().toFile().lastModified() > pair.getSignatureFile().toFile().lastModified()) {
            context.getLogger().debug(RB.$("signing.file.newer"),
                context.relativizeToBasedir(pair.getInputFile()),
                context.relativizeToBasedir(pair.getSignatureFile()));
            return false;
        }

        try {
            cosign.verifyBlob(publicKeyFile, pair.getSignatureFile(), pair.getInputFile());
            return true;
        } catch (SigningException e) {
            return false;
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy