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

org.apache.maven.plugins.gpg.BcSigner Maven / Gradle / Ivy

The newest version!
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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
 *
 *   http://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.apache.maven.plugins.gpg;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.SocketException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.bouncycastle.bcpg.ArmoredOutputStream;
import org.bouncycastle.bcpg.BCPGOutputStream;
import org.bouncycastle.bcpg.HashAlgorithmTags;
import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPrivateKey;
import org.bouncycastle.openpgp.PGPSecretKey;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.openpgp.PGPSignatureGenerator;
import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator;
import org.bouncycastle.openpgp.PGPSignatureSubpacketVector;
import org.bouncycastle.openpgp.PGPUtil;
import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator;
import org.bouncycastle.openpgp.operator.bc.BcPBESecretKeyDecryptorBuilder;
import org.bouncycastle.openpgp.operator.bc.BcPGPContentSignerBuilder;
import org.bouncycastle.openpgp.operator.bc.BcPGPDigestCalculatorProvider;
import org.bouncycastle.util.encoders.Hex;
import org.codehaus.plexus.util.io.CachingOutputStream;
import org.eclipse.aether.RepositorySystemSession;
import org.newsclub.net.unix.AFUNIXSocket;
import org.newsclub.net.unix.AFUNIXSocketAddress;

/**
 * A signer implementation that uses pure Java Bouncy Castle implementation to sign.
 */
@SuppressWarnings("checkstyle:magicnumber")
public class BcSigner extends AbstractGpgSigner {
    public static final String NAME = "bc";

    public interface Loader {
        /**
         * Returns the key ring material, or {@code null}.
         */
        default byte[] loadKeyRingMaterial(RepositorySystemSession session) throws IOException {
            return null;
        }

        /**
         * Returns the key fingerprint, or {@code null}.
         */
        default byte[] loadKeyFingerprint(RepositorySystemSession session) throws IOException {
            return null;
        }

        /**
         * Returns the key password, or {@code null}.
         */
        default char[] loadPassword(RepositorySystemSession session, byte[] fingerprint) throws IOException {
            return null;
        }
    }

    public final class GpgEnvLoader implements Loader {
        @Override
        public byte[] loadKeyRingMaterial(RepositorySystemSession session) {
            String keyMaterial = (String) session.getConfigProperties().get("env." + keyEnvName);
            if (keyMaterial != null) {
                return keyMaterial.getBytes(StandardCharsets.UTF_8);
            }
            return null;
        }

        @Override
        public byte[] loadKeyFingerprint(RepositorySystemSession session) {
            String keyFingerprint = (String) session.getConfigProperties().get("env." + keyFingerprintEnvName);
            if (keyFingerprint != null) {
                if (keyFingerprint.trim().length() == 40) {
                    return Hex.decode(keyFingerprint);
                } else {
                    throw new IllegalArgumentException(
                            "Key fingerprint configuration is wrong (hex encoded, 40 characters)");
                }
            }
            return null;
        }
    }

    public final class GpgConfLoader implements Loader {
        /**
         * Maximum file size allowed to load (as we load it into heap).
         * 

* This barrier exists to prevent us to load big/huge files, if this code is pointed at one * (by mistake or by malicious intent). * * @see Large Keys */ private static final long MAX_SIZE = 64 * 1000 + 1L; @Override public byte[] loadKeyRingMaterial(RepositorySystemSession session) throws IOException { Path keyPath = Paths.get(keyFilePath); if (!keyPath.isAbsolute()) { keyPath = Paths.get(System.getProperty("user.home")) .resolve(keyPath) .toAbsolutePath(); } if (Files.isRegularFile(keyPath)) { if (Files.size(keyPath) < MAX_SIZE) { return Files.readAllBytes(keyPath); } else { throw new IOException("Refusing to load file " + keyPath + "; is larger than 64 kB"); } } return null; } @Override public byte[] loadKeyFingerprint(RepositorySystemSession session) { if (keyFingerprint != null) { if (keyFingerprint.trim().length() == 40) { return Hex.decode(keyFingerprint); } else { throw new IllegalArgumentException( "Key fingerprint configuration is wrong (hex encoded, 40 characters)"); } } return null; } } public final class GpgAgentPasswordLoader implements Loader { @Override public char[] loadPassword(RepositorySystemSession session, byte[] fingerprint) throws IOException { if (!useAgent) { return null; } List socketLocations = Arrays.stream(agentSocketLocations.split(",")) .filter(s -> s != null && !s.isEmpty()) .collect(Collectors.toList()); for (String socketLocation : socketLocations) { try { Path socketLocationPath = Paths.get(socketLocation); if (!socketLocationPath.isAbsolute()) { socketLocationPath = Paths.get(System.getProperty("user.home")) .resolve(socketLocationPath) .toAbsolutePath(); } return load(fingerprint, socketLocationPath); } catch (SocketException e) { // try next location } } return null; } private char[] load(byte[] fingerprint, Path socketPath) throws IOException { try (AFUNIXSocket sock = AFUNIXSocket.newInstance()) { sock.connect(AFUNIXSocketAddress.of(socketPath)); try (BufferedReader in = new BufferedReader(new InputStreamReader(sock.getInputStream())); OutputStream os = sock.getOutputStream()) { expectOK(in); String display = System.getenv("DISPLAY"); if (display != null) { os.write(("OPTION display=" + display + "\n").getBytes()); os.flush(); expectOK(in); } String term = System.getenv("TERM"); if (term != null) { os.write(("OPTION ttytype=" + term + "\n").getBytes()); os.flush(); expectOK(in); } String hexKeyFingerprint = Hex.toHexString(fingerprint); String displayFingerprint = hexKeyFingerprint.toUpperCase(Locale.ROOT); // https://unix.stackexchange.com/questions/71135/how-can-i-find-out-what-keys-gpg-agent-has-cached-like-how-ssh-add-l-shows-yo String instruction = "GET_PASSPHRASE " + (!isInteractive ? "--no-ask " : "") + hexKeyFingerprint + " " + "X " + "GnuPG+Passphrase " + "Please+enter+the+passphrase+to+unlock+the+OpenPGP+secret+key+with+fingerprint:+" + displayFingerprint + "+to+use+it+for+signing+Maven+Artifacts\n"; os.write((instruction).getBytes()); os.flush(); return mayExpectOK(in); } } } private void expectOK(BufferedReader in) throws IOException { String response = in.readLine(); if (!response.startsWith("OK")) { throw new IOException("Expected OK but got this instead: " + response); } } private char[] mayExpectOK(BufferedReader in) throws IOException { String response = in.readLine(); if (response.startsWith("ERR")) { return null; } else if (!response.startsWith("OK")) { throw new IOException("Expected OK/ERR but got this instead: " + response); } return new String(Hex.decode( response.substring(Math.min(response.length(), 3)).trim())) .toCharArray(); } } private final RepositorySystemSession session; private final String keyEnvName; private final String keyFingerprintEnvName; private final String agentSocketLocations; private final String keyFilePath; private final String keyFingerprint; private PGPSecretKey secretKey; private PGPPrivateKey privateKey; private PGPSignatureSubpacketVector hashSubPackets; public BcSigner( RepositorySystemSession session, String keyEnvName, String keyFingerprintEnvName, String agentSocketLocations, String keyFilePath, String keyFingerprint) { this.session = session; this.keyEnvName = keyEnvName; this.keyFingerprintEnvName = keyFingerprintEnvName; this.agentSocketLocations = agentSocketLocations; this.keyFilePath = keyFilePath; this.keyFingerprint = keyFingerprint; } @Override public String signerName() { return NAME; } @Override public void prepare() throws MojoFailureException { try { List loaders = Stream.of(new GpgEnvLoader(), new GpgConfLoader(), new GpgAgentPasswordLoader()) .collect(Collectors.toList()); byte[] keyRingMaterial = null; for (Loader loader : loaders) { keyRingMaterial = loader.loadKeyRingMaterial(session); if (keyRingMaterial != null) { break; } } if (keyRingMaterial == null) { throw new MojoFailureException("Key ring material not found"); } byte[] fingerprint = null; for (Loader loader : loaders) { fingerprint = loader.loadKeyFingerprint(session); if (fingerprint != null) { break; } } PGPSecretKeyRingCollection pgpSecretKeyRingCollection = new PGPSecretKeyRingCollection( PGPUtil.getDecoderStream(new ByteArrayInputStream(keyRingMaterial)), new BcKeyFingerprintCalculator()); PGPSecretKey secretKey = null; for (PGPSecretKeyRing ring : pgpSecretKeyRingCollection) { for (PGPSecretKey key : ring) { if (!key.isPrivateKeyEmpty()) { if (fingerprint == null || Arrays.equals(fingerprint, key.getFingerprint())) { secretKey = key; break; } } } } if (secretKey == null) { throw new MojoFailureException("Secret key not found"); } if (secretKey.isPrivateKeyEmpty()) { throw new MojoFailureException("Private key not found in Secret key"); } long validSeconds = secretKey.getPublicKey().getValidSeconds(); if (validSeconds > 0) { LocalDateTime expireDateTime = secretKey .getPublicKey() .getCreationTime() .toInstant() .atZone(ZoneId.systemDefault()) .toLocalDateTime() .plusSeconds(validSeconds); if (LocalDateTime.now().isAfter(expireDateTime)) { throw new MojoFailureException("Secret key expired at: " + expireDateTime); } } char[] keyPassword = passphrase != null ? passphrase.toCharArray() : null; final boolean keyPassNeeded = secretKey.getKeyEncryptionAlgorithm() != SymmetricKeyAlgorithmTags.NULL; if (keyPassNeeded && keyPassword == null) { for (Loader loader : loaders) { keyPassword = loader.loadPassword(session, secretKey.getFingerprint()); if (keyPassword != null) { break; } } if (keyPassword == null) { throw new MojoFailureException("Secret key is encrypted but no passphrase provided"); } } this.secretKey = secretKey; this.privateKey = secretKey.extractPrivateKey( new BcPBESecretKeyDecryptorBuilder(new BcPGPDigestCalculatorProvider()).build(keyPassword)); if (keyPassword != null) { Arrays.fill(keyPassword, ' '); } PGPSignatureSubpacketGenerator subPacketGenerator = new PGPSignatureSubpacketGenerator(); subPacketGenerator.setIssuerFingerprint(false, secretKey); this.hashSubPackets = subPacketGenerator.generate(); } catch (PGPException | IOException e) { throw new MojoFailureException(e); } } @Override public String getKeyInfo() { Iterator userIds = secretKey.getPublicKey().getUserIDs(); if (userIds.hasNext()) { return userIds.next(); } return Hex.toHexString(secretKey.getPublicKey().getFingerprint()); } @Override protected void generateSignatureForFile(File file, File signature) throws MojoExecutionException { try (InputStream in = Files.newInputStream(file.toPath()); OutputStream out = new CachingOutputStream(signature.toPath())) { PGPSignatureGenerator sGen = new PGPSignatureGenerator( new BcPGPContentSignerBuilder(secretKey.getPublicKey().getAlgorithm(), HashAlgorithmTags.SHA512)); sGen.init(PGPSignature.BINARY_DOCUMENT, privateKey); sGen.setHashedSubpackets(hashSubPackets); int len; byte[] buffer = new byte[8 * 1024]; while ((len = in.read(buffer)) >= 0) { sGen.update(buffer, 0, len); } try (BCPGOutputStream bcpgOutputStream = new BCPGOutputStream(new ArmoredOutputStream(out))) { sGen.generate().encode(bcpgOutputStream); } } catch (PGPException | IOException e) { throw new MojoExecutionException(e); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy