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.sigstore;
20  
21  import java.io.IOException;
22  import java.io.UncheckedIOException;
23  import java.nio.file.Files;
24  import java.nio.file.Path;
25  import java.security.GeneralSecurityException;
26  import java.security.cert.X509Certificate;
27  import java.time.temporal.ChronoUnit;
28  import java.util.ArrayList;
29  import java.util.Collection;
30  import java.util.Collections;
31  import java.util.List;
32  import java.util.concurrent.atomic.AtomicBoolean;
33  import java.util.function.Predicate;
34  
35  import dev.sigstore.KeylessSigner;
36  import dev.sigstore.KeylessSignerException;
37  import dev.sigstore.bundle.Bundle;
38  import dev.sigstore.encryption.certificates.Certificates;
39  import dev.sigstore.trustroot.SigstoreConfigurationException;
40  import org.eclipse.aether.artifact.Artifact;
41  import org.eclipse.aether.generator.sigstore.internal.FulcioOidHelper;
42  import org.eclipse.aether.spi.artifact.generator.ArtifactGenerator;
43  import org.eclipse.aether.spi.io.PathProcessor;
44  import org.eclipse.aether.util.artifact.SubArtifact;
45  import org.slf4j.Logger;
46  import org.slf4j.LoggerFactory;
47  
48  final class SigstoreSignatureArtifactGenerator implements ArtifactGenerator {
49      private static final String ARTIFACT_EXTENSION = ".sigstore.json";
50      private final Logger logger = LoggerFactory.getLogger(getClass());
51      private final PathProcessor pathProcessor;
52      private final List<Artifact> artifacts;
53      private final Predicate<Artifact> signableArtifactPredicate;
54      private final boolean publicStaging;
55      private final List<Path> signatureTempFiles;
56      private final AtomicBoolean closed;
57  
58      SigstoreSignatureArtifactGenerator(
59              PathProcessor pathProcessor,
60              Collection<Artifact> artifacts,
61              Predicate<Artifact> signableArtifactPredicate,
62              boolean publicStaging) {
63          this.pathProcessor = pathProcessor;
64          this.artifacts = new ArrayList<>(artifacts);
65          this.signableArtifactPredicate = signableArtifactPredicate;
66          this.publicStaging = publicStaging;
67          this.signatureTempFiles = new ArrayList<>();
68          this.closed = new AtomicBoolean(false);
69          logger.debug("Created sigstore generator (publicStaging={})", publicStaging);
70      }
71  
72      @Override
73      public String generatorId() {
74          return SigstoreSignatureArtifactGeneratorFactory.NAME;
75      }
76  
77      @Override
78      public synchronized Collection<? extends Artifact> generate(Collection<? extends Artifact> generatedArtifacts) {
79          try {
80              artifacts.addAll(generatedArtifacts);
81  
82              // Determine, per artifact, which signable artifacts still need a signature. A pre-existing signature
83              // skips only the artifact it covers; it must not disable signing of the whole artifact set, which
84              // would silently publish partially unsigned releases.
85              ArrayList<Artifact> artifactsToSign = new ArrayList<>();
86              for (Artifact artifact : artifacts) {
87                  if (isSignatureArtifact(artifact)) {
88                      continue; // never sign a signature
89                  }
90                  if (!signableArtifactPredicate.test(artifact)) {
91                      continue;
92                  }
93                  if (hasSignature(artifact)) {
94                      logger.debug("Sigstore signature already present for {}, not signing it again", artifact);
95                      continue;
96                  }
97                  artifactsToSign.add(artifact);
98              }
99              if (artifactsToSign.isEmpty()) {
100                 logger.debug("Sigstore signatures are present for all signable artifacts, nothing to sign");
101                 return Collections.emptyList();
102             }
103             if (artifacts.stream().anyMatch(this::isSignatureArtifact)) {
104                 logger.info(
105                         "Sigstore signatures are present for some artifacts only; signing the remaining {} artifact(s)",
106                         artifactsToSign.size());
107             }
108 
109             // sign relevant artifacts
110             ArrayList<Artifact> result = new ArrayList<>();
111             ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader();
112             Thread.currentThread().setContextClassLoader(KeylessSigner.class.getClassLoader());
113             try (KeylessSigner signer = publicStaging
114                     ? KeylessSigner.builder().sigstoreStagingDefaults().build()
115                     : KeylessSigner.builder().sigstorePublicDefaults().build()) {
116                 for (Artifact artifact : artifactsToSign) {
117                     Path fileToSign = artifact.getPath();
118                     Path signatureTempFile = Files.createTempFile("signer-sigstore", "tmp");
119                     signatureTempFiles.add(signatureTempFile);
120 
121                     logger.debug("Signing " + artifact);
122                     long start = System.currentTimeMillis();
123                     Bundle bundle = signer.signFile(fileToSign);
124 
125                     X509Certificate cert = (X509Certificate)
126                             bundle.getCertPath().getCertificates().get(0);
127                     long durationMinutes = Certificates.validity(cert, ChronoUnit.MINUTES);
128 
129                     logger.debug("  Fulcio certificate (valid for "
130                             + durationMinutes
131                             + " m) obtained for "
132                             + cert.getSubjectAlternativeNames()
133                                     .iterator()
134                                     .next()
135                                     .get(1)
136                             + " (by "
137                             + FulcioOidHelper.getIssuerV2(cert)
138                             + " IdP)");
139 
140                     pathProcessor.write(signatureTempFile, bundle.toJson());
141 
142                     long duration = System.currentTimeMillis() - start;
143                     logger.debug("  > Rekor entry "
144                             + bundle.getEntries().get(0).getLogIndex()
145                             + " obtained in "
146                             + duration
147                             + " ms, saved to "
148                             + signatureTempFile);
149 
150                     result.add(new SubArtifact(
151                             artifact,
152                             artifact.getClassifier(),
153                             artifact.getExtension() + ARTIFACT_EXTENSION,
154                             signatureTempFile.toFile()));
155                 }
156             } finally {
157                 Thread.currentThread().setContextClassLoader(originalClassLoader);
158             }
159             logger.info("Signed {} artifacts with Sigstore", result.size());
160             return result;
161         } catch (SigstoreConfigurationException e) {
162             throw new IllegalArgumentException("Configuration problem", e);
163         } catch (GeneralSecurityException e) {
164             throw new IllegalArgumentException("Preparation problem", e);
165         } catch (KeylessSignerException e) {
166             throw new IllegalStateException("Processing problem", e);
167         } catch (IOException e) {
168             throw new UncheckedIOException("IO problem", e);
169         }
170     }
171 
172     private boolean isSignatureArtifact(Artifact artifact) {
173         return artifact.getExtension().endsWith(ARTIFACT_EXTENSION);
174     }
175 
176     private boolean hasSignature(Artifact artifact) {
177         String signatureExtension = artifact.getExtension() + ARTIFACT_EXTENSION;
178         return artifacts.stream()
179                 .anyMatch(a -> a.getExtension().equals(signatureExtension)
180                         && a.getClassifier().equals(artifact.getClassifier())
181                         && a.getArtifactId().equals(artifact.getArtifactId())
182                         && a.getGroupId().equals(artifact.getGroupId())
183                         && a.getVersion().equals(artifact.getVersion()));
184     }
185 
186     @Override
187     public void close() {
188         if (closed.compareAndSet(false, true)) {
189             signatureTempFiles.forEach(p -> {
190                 try {
191                     Files.deleteIfExists(p);
192                 } catch (IOException e) {
193                     p.toFile().deleteOnExit();
194                 }
195             });
196         }
197     }
198 }