View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.eclipse.aether.generator.gnupg;
20  
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.io.OutputStream;
24  import java.io.UncheckedIOException;
25  import java.nio.file.Files;
26  import java.nio.file.Path;
27  import java.util.ArrayList;
28  import java.util.Collection;
29  import java.util.Collections;
30  import java.util.List;
31  import java.util.concurrent.atomic.AtomicBoolean;
32  import java.util.function.Predicate;
33  
34  import org.bouncycastle.bcpg.ArmoredOutputStream;
35  import org.bouncycastle.bcpg.BCPGOutputStream;
36  import org.bouncycastle.bcpg.HashAlgorithmTags;
37  import org.bouncycastle.openpgp.PGPException;
38  import org.bouncycastle.openpgp.PGPPrivateKey;
39  import org.bouncycastle.openpgp.PGPSecretKey;
40  import org.bouncycastle.openpgp.PGPSignature;
41  import org.bouncycastle.openpgp.PGPSignatureGenerator;
42  import org.bouncycastle.openpgp.PGPSignatureSubpacketVector;
43  import org.bouncycastle.openpgp.operator.bc.BcPGPContentSignerBuilder;
44  import org.eclipse.aether.artifact.Artifact;
45  import org.eclipse.aether.spi.artifact.generator.ArtifactGenerator;
46  import org.eclipse.aether.util.artifact.SubArtifact;
47  import org.slf4j.Logger;
48  import org.slf4j.LoggerFactory;
49  
50  final class GnupgSignatureArtifactGenerator implements ArtifactGenerator {
51      private static final String ARTIFACT_EXTENSION = ".asc";
52      private final Logger logger = LoggerFactory.getLogger(getClass());
53      private final List<Artifact> artifacts;
54      private final Predicate<Artifact> signableArtifactPredicate;
55      private final PGPSecretKey secretKey;
56      private final PGPPrivateKey privateKey;
57      private final PGPSignatureSubpacketVector hashSubPackets;
58      private final String keyInfo;
59      private final List<Path> signatureTempFiles;
60      private final AtomicBoolean closed;
61  
62      GnupgSignatureArtifactGenerator(
63              Collection<Artifact> artifacts,
64              Predicate<Artifact> signableArtifactPredicate,
65              PGPSecretKey secretKey,
66              PGPPrivateKey privateKey,
67              PGPSignatureSubpacketVector hashSubPackets,
68              String keyInfo) {
69          this.artifacts = new ArrayList<>(artifacts);
70          this.signableArtifactPredicate = signableArtifactPredicate;
71          this.secretKey = secretKey;
72          this.privateKey = privateKey;
73          this.hashSubPackets = hashSubPackets;
74          this.keyInfo = keyInfo;
75          this.signatureTempFiles = new ArrayList<>();
76          this.closed = new AtomicBoolean(false);
77          logger.debug("Created generator using key {}", keyInfo);
78      }
79  
80      @Override
81      public String generatorId() {
82          return GnupgSignatureArtifactGeneratorFactory.NAME;
83      }
84  
85      @Override
86      public synchronized Collection<? extends Artifact> generate(Collection<? extends Artifact> generatedArtifacts) {
87          try {
88              artifacts.addAll(generatedArtifacts);
89  
90              // Determine, per artifact, which signable artifacts still need a signature. A pre-existing signature
91              // (e.g. produced by maven-gpg-plugin) skips only the artifact it covers; it must not disable signing
92              // of the whole artifact set, which would silently publish partially unsigned releases.
93              ArrayList<Artifact> artifactsToSign = new ArrayList<>();
94              for (Artifact artifact : artifacts) {
95                  if (isSignatureArtifact(artifact)) {
96                      continue; // never sign a signature
97                  }
98                  if (!signableArtifactPredicate.test(artifact)) {
99                      continue;
100                 }
101                 if (hasSignature(artifact)) {
102                     logger.debug("GPG signature already present for {}, not signing it again", artifact);
103                     continue;
104                 }
105                 artifactsToSign.add(artifact);
106             }
107             if (artifactsToSign.isEmpty()) {
108                 logger.debug("GPG signatures are present for all signable artifacts, nothing to sign");
109                 return Collections.emptyList();
110             }
111             if (artifacts.stream().anyMatch(this::isSignatureArtifact)) {
112                 logger.info(
113                         "GPG signatures are present for some artifacts only; signing the remaining {} artifact(s) with key {}",
114                         artifactsToSign.size(),
115                         keyInfo);
116             }
117 
118             // sign relevant artifacts
119             ArrayList<Artifact> result = new ArrayList<>();
120             for (Artifact artifact : artifactsToSign) {
121                 Path signatureTempFile = Files.createTempFile("signer-pgp", "tmp");
122                 signatureTempFiles.add(signatureTempFile);
123                 try (InputStream artifactContent = Files.newInputStream(artifact.getPath());
124                         OutputStream signatureContent = Files.newOutputStream(signatureTempFile)) {
125                     sign(artifactContent, signatureContent);
126                 }
127                 result.add(new SubArtifact(
128                         artifact,
129                         artifact.getClassifier(),
130                         artifact.getExtension() + ARTIFACT_EXTENSION,
131                         signatureTempFile.toFile()));
132             }
133             logger.debug("Signed {} artifacts with key {}", result.size(), keyInfo);
134             return result;
135         } catch (IOException e) {
136             throw new UncheckedIOException(e);
137         }
138     }
139 
140     private boolean isSignatureArtifact(Artifact artifact) {
141         return artifact.getExtension().endsWith(ARTIFACT_EXTENSION);
142     }
143 
144     private boolean hasSignature(Artifact artifact) {
145         String signatureExtension = artifact.getExtension() + ARTIFACT_EXTENSION;
146         return artifacts.stream()
147                 .anyMatch(a -> a.getExtension().equals(signatureExtension)
148                         && a.getClassifier().equals(artifact.getClassifier())
149                         && a.getArtifactId().equals(artifact.getArtifactId())
150                         && a.getGroupId().equals(artifact.getGroupId())
151                         && a.getVersion().equals(artifact.getVersion()));
152     }
153 
154     @Override
155     public void close() {
156         if (closed.compareAndSet(false, true)) {
157             signatureTempFiles.forEach(p -> {
158                 try {
159                     Files.deleteIfExists(p);
160                 } catch (IOException e) {
161                     p.toFile().deleteOnExit();
162                 }
163             });
164         }
165     }
166 
167     private void sign(InputStream content, OutputStream signature) throws IOException {
168         PGPSignatureGenerator sGen = new PGPSignatureGenerator(
169                 new BcPGPContentSignerBuilder(secretKey.getPublicKey().getAlgorithm(), HashAlgorithmTags.SHA512));
170         try {
171             sGen.init(PGPSignature.BINARY_DOCUMENT, privateKey);
172             sGen.setHashedSubpackets(hashSubPackets);
173             int len;
174             byte[] buffer = new byte[8 * 1024];
175             while ((len = content.read(buffer)) >= 0) {
176                 sGen.update(buffer, 0, len);
177             }
178             try (BCPGOutputStream bcpgOutputStream = new BCPGOutputStream(new ArmoredOutputStream(signature))) {
179                 sGen.generate().encode(bcpgOutputStream);
180             }
181         } catch (PGPException e) {
182             throw new IllegalStateException(e);
183         }
184     }
185 }