53728c0489838d45d4824072ceb0921e7100d67c
[sdc.git] /
1 /*-
2  * ============LICENSE_START=======================================================
3  * SDC
4  * ================================================================================
5  * Copyright (C) 2019, Nordix Foundation. All rights reserved.
6  * ================================================================================
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  *
11  *      http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  * ============LICENSE_END=========================================================
19  */
20 package org.openecomp.sdc.vendorsoftwareproduct.security;
21
22 import com.google.common.collect.ImmutableSet;
23 import java.io.BufferedOutputStream;
24 import java.io.ByteArrayInputStream;
25 import java.io.File;
26 import java.io.FileInputStream;
27 import java.io.FileOutputStream;
28 import java.io.IOException;
29 import java.io.InputStream;
30 import java.io.InputStreamReader;
31 import java.nio.file.Files;
32 import java.nio.file.Path;
33 import java.security.GeneralSecurityException;
34 import java.security.InvalidAlgorithmParameterException;
35 import java.security.NoSuchAlgorithmException;
36 import java.security.NoSuchProviderException;
37 import java.security.Security;
38 import java.security.cert.CertPathBuilder;
39 import java.security.cert.CertStore;
40 import java.security.cert.CertificateException;
41 import java.security.cert.CertificateExpiredException;
42 import java.security.cert.CertificateFactory;
43 import java.security.cert.CertificateNotYetValidException;
44 import java.security.cert.CollectionCertStoreParameters;
45 import java.security.cert.PKIXBuilderParameters;
46 import java.security.cert.PKIXCertPathBuilderResult;
47 import java.security.cert.TrustAnchor;
48 import java.security.cert.X509CertSelector;
49 import java.security.cert.X509Certificate;
50 import java.util.Collection;
51 import java.util.HashSet;
52 import java.util.Optional;
53 import java.util.Set;
54 import java.util.UUID;
55 import java.util.concurrent.atomic.AtomicBoolean;
56 import java.util.function.Predicate;
57 import java.util.stream.Collectors;
58 import java.util.stream.Stream;
59 import java.util.zip.ZipEntry;
60 import java.util.zip.ZipInputStream;
61 import org.bouncycastle.asn1.cms.ContentInfo;
62 import org.bouncycastle.cert.X509CertificateHolder;
63 import org.bouncycastle.cms.CMSException;
64 import org.bouncycastle.cms.CMSProcessableByteArray;
65 import org.bouncycastle.cms.CMSProcessableFile;
66 import org.bouncycastle.cms.CMSSignedData;
67 import org.bouncycastle.cms.SignerInformation;
68 import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder;
69 import org.bouncycastle.jce.provider.BouncyCastleProvider;
70 import org.bouncycastle.openssl.PEMParser;
71 import org.bouncycastle.operator.OperatorCreationException;
72 import org.openecomp.sdc.be.csar.storage.ArtifactInfo;
73 import org.openecomp.sdc.be.csar.storage.ArtifactStorageConfig;
74 import org.openecomp.sdc.be.csar.storage.ArtifactStorageManager;
75 import org.openecomp.sdc.be.csar.storage.StorageFactory;
76 import org.openecomp.sdc.logging.api.Logger;
77 import org.openecomp.sdc.logging.api.LoggerFactory;
78 import org.openecomp.sdc.vendorsoftwareproduct.types.OnboardSignedPackage;
79
80 /**
81  * This is temporary solution. When AAF provides functionality for verifying trustedCertificates, this class should be reviewed Class is responsible
82  * for providing root trustedCertificates from configured location in onboarding container.
83  */
84 public class SecurityManager {
85
86     public static final Set<String> ALLOWED_SIGNATURE_EXTENSIONS = Set.of("cms");
87     public static final Set<String> ALLOWED_CERTIFICATE_EXTENSIONS = Set.of("cert", "crt");
88     private static final String CERTIFICATE_DEFAULT_LOCATION = "cert";
89     private static final Logger LOGGER = LoggerFactory.getLogger(SecurityManager.class);
90     private static final String UNEXPECTED_ERROR_OCCURRED_DURING_SIGNATURE_VALIDATION = "Unexpected error occurred during signature validation!";
91     private static final String COULD_NOT_VERIFY_SIGNATURE = "Could not verify signature!";
92     private static final String EXTERNAL_CSAR_STORE = "externalCsarStore";
93
94     static {
95         if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
96             Security.addProvider(new BouncyCastleProvider());
97         }
98     }
99
100     private Set<X509Certificate> trustedCertificates = new HashSet<>();
101     private Set<X509Certificate> trustedCertificatesFromPackage = new HashSet<>();
102     private File certificateDirectory;
103
104     private SecurityManager() {
105         certificateDirectory = this.getcertDirectory(System.getenv("SDC_CERT_DIR"));
106     }
107
108     // Package level constructor use in tests to avoid power mock
109     SecurityManager(String sdcCertDir) {
110         certificateDirectory = this.getcertDirectory(sdcCertDir);
111     }
112
113     public static SecurityManager getInstance() {
114         return SecurityManagerInstanceHolder.instance;
115     }
116
117     /**
118      * Checks the configured location for available trustedCertificates
119      *
120      * @return set of trustedCertificates
121      * @throws SecurityManagerException
122      */
123     public Set<X509Certificate> getTrustedCertificates() throws SecurityManagerException {
124         //if file number in certificate directory changed reload certs
125         String[] certFiles = certificateDirectory.list();
126         if (certFiles == null) {
127             LOGGER.error("Certificate directory is empty!");
128             return ImmutableSet.copyOf(new HashSet<>());
129         }
130         if (trustedCertificates.size() != certFiles.length) {
131             trustedCertificates = new HashSet<>();
132             processCertificateDir();
133         }
134         if (!trustedCertificatesFromPackage.isEmpty()) {
135             return Stream.concat(trustedCertificatesFromPackage.stream(), trustedCertificates.stream()).collect(Collectors.toUnmodifiableSet());
136         }
137         return ImmutableSet.copyOf(trustedCertificates);
138     }
139
140     /**
141      * Cleans certificate collection
142      */
143     public void cleanTrustedCertificates() {
144         trustedCertificates.clear();
145     }
146
147     /**
148      * Verifies if packaged signed with trusted certificate
149      *
150      * @param messageSyntaxSignature - signature data in cms format
151      * @param packageCert            - package certificate if not part of cms signature, can be null
152      * @param innerPackageFile       data package signed with cms signature
153      * @return true if signature verified
154      * @throws SecurityManagerException
155      */
156     public boolean verifySignedData(final byte[] messageSyntaxSignature,
157                                     final byte[] packageCert,
158                                     final byte[] innerPackageFile) throws SecurityManagerException {
159         try (final ByteArrayInputStream signatureStream = new ByteArrayInputStream(messageSyntaxSignature);
160             final PEMParser pemParser = new PEMParser(new InputStreamReader(signatureStream))) {
161             final Object parsedObject = pemParser.readObject();
162             if (!(parsedObject instanceof ContentInfo)) {
163                 throw new SecurityManagerException("Signature is not recognized");
164             }
165             return verify(packageCert, new CMSSignedData(new CMSProcessableByteArray(innerPackageFile), ContentInfo.getInstance(parsedObject)));
166         } catch (final IOException | CMSException e) {
167             LOGGER.error(e.getMessage(), e);
168             throw new SecurityManagerException(UNEXPECTED_ERROR_OCCURRED_DURING_SIGNATURE_VALIDATION, e);
169         }
170     }
171
172     public boolean verifyPackageSignedData(final OnboardSignedPackage signedPackage, final ArtifactInfo artifactInfo)
173         throws SecurityManagerException {
174         boolean fail = false;
175
176         final StorageFactory storageFactory = new StorageFactory();
177         final ArtifactStorageManager artifactStorageManager = storageFactory.createArtifactStorageManager();
178         final ArtifactStorageConfig storageConfiguration = artifactStorageManager.getStorageConfiguration();
179
180         final var fileContentHandler = signedPackage.getFileContentHandler();
181         byte[] packageCert = null;
182         final Optional<String> certificateFilePath = signedPackage.getCertificateFilePath();
183         if (certificateFilePath.isPresent()) {
184             packageCert = fileContentHandler.getFileContent(certificateFilePath.get());
185         }
186
187         final Path folder = Path.of(storageConfiguration.getTempPath());
188         try {
189             Files.createDirectories(folder);
190         } catch (final IOException e) {
191             fail = true;
192             throw new SecurityManagerException(String.format("Failed to create directory '%s'", folder), e);
193         }
194
195         final var target = folder.resolve(UUID.randomUUID().toString());
196
197         try (final var signatureStream = new ByteArrayInputStream(fileContentHandler.getFileContent(signedPackage.getSignatureFilePath()));
198             final var pemParser = new PEMParser(new InputStreamReader(signatureStream))) {
199             final var parsedObject = pemParser.readObject();
200             if (!(parsedObject instanceof ContentInfo)) {
201                 fail = true;
202                 throw new SecurityManagerException("Signature is not recognized");
203             }
204
205             try (final InputStream inputStream = artifactStorageManager.get(artifactInfo)) {
206                 if (!findCSARandExtract(inputStream, target)) {
207                     fail = true;
208                     return false;
209                 }
210             }
211             final var verify = verify(packageCert, new CMSSignedData(new CMSProcessableFile(target.toFile()), ContentInfo.getInstance(parsedObject)));
212             fail = !verify;
213             return verify;
214         } catch (final IOException e) {
215             fail = true;
216             LOGGER.error(e.getMessage(), e);
217             throw new SecurityManagerException(UNEXPECTED_ERROR_OCCURRED_DURING_SIGNATURE_VALIDATION, e);
218         } catch (final CMSException e) {
219             fail = true;
220             throw new SecurityManagerException(COULD_NOT_VERIFY_SIGNATURE, e);
221         } catch (final SecurityManagerException e) {
222             fail = true;
223             throw e;
224         } finally {
225             deleteFile(target);
226             if (fail) {
227                 artifactStorageManager.delete(artifactInfo);
228             }
229         }
230     }
231
232     private void deleteFile(final Path filePath) {
233         try {
234             Files.delete(filePath);
235         } catch (final IOException e) {
236             LOGGER.warn("Failed to delete '{}' after verifying package signed data", filePath, e);
237         }
238     }
239
240     private boolean verify(final byte[] packageCert, final CMSSignedData signedData) throws SecurityManagerException {
241         final SignerInformation firstSigner = signedData.getSignerInfos().getSigners().iterator().next();
242         final X509Certificate cert;
243         Collection<X509CertificateHolder> certs;
244         if (packageCert == null) {
245             certs = signedData.getCertificates().getMatches(null);
246             cert = readSignCert(certs, firstSigner)
247                 .orElseThrow(() -> new SecurityManagerException("No certificate found in cms signature that should contain one!"));
248         } else {
249             try {
250                 certs = parseCertsFromPem(packageCert);
251             } catch (final IOException e) {
252                 throw new SecurityManagerException("Failed to parse certificate from PEM", e);
253             }
254             cert = readSignCert(certs, firstSigner)
255                 .orElseThrow(() -> new SecurityManagerException("No matching certificate found in certificate file that should contain one!"));
256         }
257         trustedCertificatesFromPackage = readTrustedCerts(certs, firstSigner);
258         if (verifyCertificate(cert, getTrustedCertificates()) == null) {
259             return false;
260         }
261         try {
262             return firstSigner.verify(new JcaSimpleSignerInfoVerifierBuilder().build(cert));
263         } catch (CMSException | OperatorCreationException e) {
264             throw new SecurityManagerException("Failed to verify package signed data", e);
265         }
266     }
267
268     private boolean findCSARandExtract(final InputStream inputStream, final Path target) throws IOException {
269         final AtomicBoolean found = new AtomicBoolean(false);
270
271         final var zipInputStream = new ZipInputStream(inputStream);
272         ZipEntry zipEntry;
273         byte[] buffer = new byte[2048];
274         while ((zipEntry = zipInputStream.getNextEntry()) != null) {
275             final var entryName = zipEntry.getName();
276             if (!zipEntry.isDirectory() && entryName.toLowerCase().endsWith(".csar")) {
277                 try (final FileOutputStream fos = new FileOutputStream(target.toFile());
278                     final BufferedOutputStream bos = new BufferedOutputStream(fos, buffer.length)) {
279
280                     int len;
281                     while ((len = zipInputStream.read(buffer)) > 0) {
282                         bos.write(buffer, 0, len);
283                     }
284                 }
285                 found.set(true);
286             }
287         }
288         return found.get();
289     }
290
291     private Optional<X509Certificate> readSignCert(final Collection<X509CertificateHolder> certs, final SignerInformation firstSigner) {
292         return certs.stream().filter(crt -> firstSigner.getSID().match(crt)).findAny().map(this::loadCertificate);
293     }
294
295     private Set<X509Certificate> readTrustedCerts(final Collection<X509CertificateHolder> certs, final SignerInformation firstSigner) {
296         return certs.stream().filter(crt -> !firstSigner.getSID().match(crt)).map(this::loadCertificate).filter(Predicate.not(this::isSelfSigned))
297             .collect(Collectors.toSet());
298     }
299
300     private Set<X509CertificateHolder> parseCertsFromPem(final byte[] packageCert) throws IOException {
301         final ByteArrayInputStream packageCertStream = new ByteArrayInputStream(packageCert);
302         final PEMParser pemParser = new PEMParser(new InputStreamReader(packageCertStream));
303         Object readObject = pemParser.readObject();
304         Set<X509CertificateHolder> allCerts = new HashSet<>();
305         while (readObject != null) {
306             if (readObject instanceof X509CertificateHolder) {
307                 allCerts.add((X509CertificateHolder) readObject);
308             }
309             readObject = pemParser.readObject();
310         }
311         return allCerts;
312     }
313
314     private void processCertificateDir() throws SecurityManagerException {
315         if (!certificateDirectory.exists() || !certificateDirectory.isDirectory()) {
316             LOGGER.error("Issue with certificate directory, check if exists!");
317             return;
318         }
319         File[] files = certificateDirectory.listFiles();
320         if (files == null) {
321             LOGGER.error("Certificate directory is empty!");
322             return;
323         }
324         for (File f : files) {
325             trustedCertificates.add(loadCertificate(f));
326         }
327     }
328
329     private File getcertDirectory(String sdcCertDir) {
330         String certDirLocation = sdcCertDir;
331         if (certDirLocation == null) {
332             certDirLocation = CERTIFICATE_DEFAULT_LOCATION;
333         }
334         return new File(certDirLocation);
335     }
336
337     private X509Certificate loadCertificate(File certFile) throws SecurityManagerException {
338         try (FileInputStream fi = new FileInputStream(certFile)) {
339             return loadCertificateFactory(fi);
340         } catch (IOException e) {
341             throw new SecurityManagerException("Error during loading Certificate from file!", e);
342         }
343     }
344
345     private X509Certificate loadCertificate(X509CertificateHolder cert) {
346         try {
347             return loadCertificateFactory(new ByteArrayInputStream(cert.getEncoded()));
348         } catch (IOException | SecurityManagerException e) {
349             throw new RuntimeException("Error during loading Certificate from bytes!", e);
350         }
351     }
352
353     private X509Certificate loadCertificateFactory(InputStream in) throws SecurityManagerException {
354         try {
355             CertificateFactory factory = CertificateFactory.getInstance("X.509");
356             return (X509Certificate) factory.generateCertificate(in);
357         } catch (CertificateException e) {
358             throw new SecurityManagerException("Error during loading Certificate from bytes!", e);
359         }
360     }
361
362     private PKIXCertPathBuilderResult verifyCertificate(final X509Certificate cert,
363                                                         final Set<X509Certificate> additionalCerts) throws SecurityManagerException {
364         if (null == cert) {
365             throw new SecurityManagerException("The certificate is empty!");
366         }
367         if (isExpired(cert)) {
368             throw new SecurityManagerException("The certificate expired on: " + cert.getNotAfter());
369         }
370         if (isSelfSigned(cert)) {
371             throw new SecurityManagerException("The certificate is self-signed.");
372         }
373         Set<X509Certificate> trustedRootCerts = new HashSet<>();
374         Set<X509Certificate> intermediateCerts = new HashSet<>();
375         for (X509Certificate additionalCert : additionalCerts) {
376             if (isSelfSigned(additionalCert)) {
377                 trustedRootCerts.add(additionalCert);
378             } else {
379                 intermediateCerts.add(additionalCert);
380             }
381         }
382         try {
383             return verifyCertificate(cert, trustedRootCerts, intermediateCerts);
384         } catch (final GeneralSecurityException e) {
385             throw new SecurityManagerException("Failed to verify certificate", e);
386         }
387     }
388
389     private PKIXCertPathBuilderResult verifyCertificate(X509Certificate cert, Set<X509Certificate> allTrustedRootCerts,
390                                                         Set<X509Certificate> allIntermediateCerts) throws GeneralSecurityException {
391         // Create the selector that specifies the starting certificate
392         X509CertSelector selector = new X509CertSelector();
393         selector.setCertificate(cert);
394         // Create the trust anchors (set of root CA certificates)
395         Set<TrustAnchor> trustAnchors = new HashSet<>();
396         for (X509Certificate trustedRootCert : allTrustedRootCerts) {
397             trustAnchors.add(new TrustAnchor(trustedRootCert, null));
398         }
399         // Configure the PKIX certificate builder algorithm parameters
400         PKIXBuilderParameters pkixParams;
401         try {
402             pkixParams = new PKIXBuilderParameters(trustAnchors, selector);
403         } catch (InvalidAlgorithmParameterException ex) {
404             throw new InvalidAlgorithmParameterException("No root CA has been found for this certificate", ex);
405         }
406         // Not supporting CRL checks for now
407         pkixParams.setRevocationEnabled(false);
408         Set<X509Certificate> certSet = new HashSet<>();
409         certSet.add(cert);
410         pkixParams.addCertStore(createCertStore(certSet));
411         pkixParams.addCertStore(createCertStore(allIntermediateCerts));
412         pkixParams.addCertStore(createCertStore(allTrustedRootCerts));
413         CertPathBuilder builder = CertPathBuilder.getInstance(CertPathBuilder.getDefaultType(), BouncyCastleProvider.PROVIDER_NAME);
414         return (PKIXCertPathBuilderResult) builder.build(pkixParams);
415     }
416
417     private CertStore createCertStore(Set<X509Certificate> certificateSet)
418         throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, NoSuchProviderException {
419         return CertStore.getInstance("Collection", new CollectionCertStoreParameters(certificateSet), BouncyCastleProvider.PROVIDER_NAME);
420     }
421
422     private boolean isExpired(X509Certificate cert) {
423         try {
424             cert.checkValidity();
425         } catch (CertificateExpiredException e) {
426             LOGGER.error(e.getMessage(), e);
427             return true;
428         } catch (CertificateNotYetValidException e) {
429             LOGGER.error(e.getMessage(), e);
430             return false;
431         }
432         return false;
433     }
434
435     private boolean isSelfSigned(X509Certificate cert) {
436         return cert.getIssuerDN().equals(cert.getSubjectDN());
437     }
438
439     /**
440      * Initialization on demand class / synchronized singleton pattern.
441      */
442     private static class SecurityManagerInstanceHolder {
443
444         private static final SecurityManager instance = new SecurityManager();
445     }
446 }