e6fcaff7126201efd2bbeb504d2c0e3d3e3ec8c6
[sdc.git] / openecomp-be / backend / openecomp-sdc-vendor-software-product-manager / src / main / java / org / openecomp / sdc / vendorsoftwareproduct / security / SecurityManager.java
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
93     static {
94         if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
95             Security.addProvider(new BouncyCastleProvider());
96         }
97     }
98
99     private Set<X509Certificate> trustedCertificates = new HashSet<>();
100     private Set<X509Certificate> trustedCertificatesFromPackage = new HashSet<>();
101     private File certificateDirectory;
102
103     private SecurityManager() {
104         certificateDirectory = this.getcertDirectory(System.getenv("SDC_CERT_DIR"));
105     }
106
107     // Package level constructor use in tests to avoid power mock
108     SecurityManager(String sdcCertDir) {
109         certificateDirectory = this.getcertDirectory(sdcCertDir);
110     }
111
112     public static SecurityManager getInstance() {
113         return SecurityManagerInstanceHolder.instance;
114     }
115
116     /**
117      * Checks the configured location for available trustedCertificates
118      *
119      * @return set of trustedCertificates
120      * @throws SecurityManagerException
121      */
122     public Set<X509Certificate> getTrustedCertificates() throws SecurityManagerException {
123         //if file number in certificate directory changed reload certs
124         String[] certFiles = certificateDirectory.list();
125         if (certFiles == null) {
126             LOGGER.error("Certificate directory is empty!");
127             return ImmutableSet.copyOf(new HashSet<>());
128         }
129         if (trustedCertificates.size() != certFiles.length) {
130             trustedCertificates = new HashSet<>();
131             processCertificateDir();
132         }
133         if (!trustedCertificatesFromPackage.isEmpty()) {
134             return Stream.concat(trustedCertificatesFromPackage.stream(), trustedCertificates.stream()).collect(Collectors.toUnmodifiableSet());
135         }
136         return ImmutableSet.copyOf(trustedCertificates);
137     }
138
139     /**
140      * Cleans certificate collection
141      */
142     public void cleanTrustedCertificates() {
143         trustedCertificates.clear();
144     }
145
146     /**
147      * Verifies if packaged signed with trusted certificate
148      *
149      * @param messageSyntaxSignature - signature data in cms format
150      * @param packageCert            - package certificate if not part of cms signature, can be null
151      * @param innerPackageFile       data package signed with cms signature
152      * @return true if signature verified
153      * @throws SecurityManagerException
154      */
155     public boolean verifySignedData(final byte[] messageSyntaxSignature,
156                                     final byte[] packageCert,
157                                     final byte[] innerPackageFile) throws SecurityManagerException {
158         try (final ByteArrayInputStream signatureStream = new ByteArrayInputStream(messageSyntaxSignature);
159             final PEMParser pemParser = new PEMParser(new InputStreamReader(signatureStream))) {
160             final Object parsedObject = pemParser.readObject();
161             if (!(parsedObject instanceof ContentInfo)) {
162                 throw new SecurityManagerException("Signature is not recognized");
163             }
164             return verify(packageCert, new CMSSignedData(new CMSProcessableByteArray(innerPackageFile), ContentInfo.getInstance(parsedObject)));
165         } catch (final IOException | CMSException e) {
166             LOGGER.error(e.getMessage(), e);
167             throw new SecurityManagerException(UNEXPECTED_ERROR_OCCURRED_DURING_SIGNATURE_VALIDATION, e);
168         }
169     }
170
171     public boolean verifyPackageSignedData(final OnboardSignedPackage signedPackage, final ArtifactInfo artifactInfo)
172         throws SecurityManagerException {
173         boolean fail = false;
174
175         final StorageFactory storageFactory = new StorageFactory();
176         final ArtifactStorageManager artifactStorageManager = storageFactory.createArtifactStorageManager();
177         final ArtifactStorageConfig storageConfiguration = artifactStorageManager.getStorageConfiguration();
178
179         final var fileContentHandler = signedPackage.getFileContentHandler();
180         byte[] packageCert = null;
181         final Optional<String> certificateFilePath = signedPackage.getCertificateFilePath();
182         if (certificateFilePath.isPresent()) {
183             packageCert = fileContentHandler.getFileContent(certificateFilePath.get());
184         }
185
186         final Path folder = Path.of(storageConfiguration.getTempPath());
187         try {
188             Files.createDirectories(folder);
189         } catch (final IOException e) {
190             fail = true;
191             throw new SecurityManagerException(String.format("Failed to create directory '%s'", folder), e);
192         }
193
194         final var target = folder.resolve(UUID.randomUUID().toString());
195
196         try (final var signatureStream = new ByteArrayInputStream(fileContentHandler.getFileContent(signedPackage.getSignatureFilePath()));
197             final var pemParser = new PEMParser(new InputStreamReader(signatureStream))) {
198             final var parsedObject = pemParser.readObject();
199             if (!(parsedObject instanceof ContentInfo)) {
200                 fail = true;
201                 throw new SecurityManagerException("Signature is not recognized");
202             }
203
204             try (final InputStream inputStream = artifactStorageManager.get(artifactInfo)) {
205                 if (!findCSARandExtract(inputStream, target)) {
206                     fail = true;
207                     return false;
208                 }
209             }
210             final var verify = verify(packageCert, new CMSSignedData(new CMSProcessableFile(target.toFile()), ContentInfo.getInstance(parsedObject)));
211             fail = !verify;
212             return verify;
213         } catch (final IOException e) {
214             fail = true;
215             LOGGER.error(e.getMessage(), e);
216             throw new SecurityManagerException(UNEXPECTED_ERROR_OCCURRED_DURING_SIGNATURE_VALIDATION, e);
217         } catch (final CMSException e) {
218             fail = true;
219             throw new SecurityManagerException(COULD_NOT_VERIFY_SIGNATURE, e);
220         } catch (final SecurityManagerException e) {
221             fail = true;
222             throw e;
223         } finally {
224             deleteFile(target);
225             if (fail) {
226                 artifactStorageManager.delete(artifactInfo);
227             }
228         }
229     }
230
231     private void deleteFile(final Path filePath) {
232         try {
233             Files.delete(filePath);
234         } catch (final IOException e) {
235             LOGGER.warn("Failed to delete '{}' after verifying package signed data", filePath, e);
236         }
237     }
238
239     private boolean verify(final byte[] packageCert, final CMSSignedData signedData) throws SecurityManagerException {
240         final SignerInformation firstSigner = signedData.getSignerInfos().getSigners().iterator().next();
241         final X509Certificate cert;
242         Collection<X509CertificateHolder> certs;
243         if (packageCert == null) {
244             certs = signedData.getCertificates().getMatches(null);
245             cert = readSignCert(certs, firstSigner)
246                 .orElseThrow(() -> new SecurityManagerException("No certificate found in cms signature that should contain one!"));
247         } else {
248             try {
249                 certs = parseCertsFromPem(packageCert);
250             } catch (final IOException e) {
251                 throw new SecurityManagerException("Failed to parse certificate from PEM", e);
252             }
253             cert = readSignCert(certs, firstSigner)
254                 .orElseThrow(() -> new SecurityManagerException("No matching certificate found in certificate file that should contain one!"));
255         }
256         trustedCertificatesFromPackage = readTrustedCerts(certs, firstSigner);
257         if (verifyCertificate(cert, getTrustedCertificates()) == null) {
258             return false;
259         }
260         try {
261             return firstSigner.verify(new JcaSimpleSignerInfoVerifierBuilder().build(cert));
262         } catch (CMSException | OperatorCreationException e) {
263             throw new SecurityManagerException("Failed to verify package signed data", e);
264         }
265     }
266
267     private boolean findCSARandExtract(final InputStream inputStream, final Path target) throws IOException {
268         final AtomicBoolean found = new AtomicBoolean(false);
269
270         final var zipInputStream = new ZipInputStream(inputStream);
271         ZipEntry zipEntry;
272         byte[] buffer = new byte[2048];
273         while ((zipEntry = zipInputStream.getNextEntry()) != null) {
274             final var entryName = zipEntry.getName();
275             if (!zipEntry.isDirectory() && entryName.toLowerCase().endsWith(".csar")) {
276                 try (final FileOutputStream fos = new FileOutputStream(target.toFile());
277                     final BufferedOutputStream bos = new BufferedOutputStream(fos, buffer.length)) {
278
279                     int len;
280                     while ((len = zipInputStream.read(buffer)) > 0) {
281                         bos.write(buffer, 0, len);
282                     }
283                 }
284                 found.set(true);
285             }
286         }
287         return found.get();
288     }
289
290     private Optional<X509Certificate> readSignCert(final Collection<X509CertificateHolder> certs, final SignerInformation firstSigner) {
291         return certs.stream().filter(crt -> firstSigner.getSID().match(crt)).findAny().map(this::loadCertificate);
292     }
293
294     private Set<X509Certificate> readTrustedCerts(final Collection<X509CertificateHolder> certs, final SignerInformation firstSigner) {
295         return certs.stream().filter(crt -> !firstSigner.getSID().match(crt)).map(this::loadCertificate).filter(Predicate.not(this::isSelfSigned))
296             .collect(Collectors.toSet());
297     }
298
299     private Set<X509CertificateHolder> parseCertsFromPem(final byte[] packageCert) throws IOException {
300         final ByteArrayInputStream packageCertStream = new ByteArrayInputStream(packageCert);
301         final PEMParser pemParser = new PEMParser(new InputStreamReader(packageCertStream));
302         Object readObject = pemParser.readObject();
303         Set<X509CertificateHolder> allCerts = new HashSet<>();
304         while (readObject != null) {
305             if (readObject instanceof X509CertificateHolder) {
306                 allCerts.add((X509CertificateHolder) readObject);
307             }
308             readObject = pemParser.readObject();
309         }
310         return allCerts;
311     }
312
313     private void processCertificateDir() throws SecurityManagerException {
314         if (!certificateDirectory.exists() || !certificateDirectory.isDirectory()) {
315             LOGGER.error("Issue with certificate directory, check if exists!");
316             return;
317         }
318         File[] files = certificateDirectory.listFiles();
319         if (files == null) {
320             LOGGER.error("Certificate directory is empty!");
321             return;
322         }
323         for (File f : files) {
324             trustedCertificates.add(loadCertificate(f));
325         }
326     }
327
328     private File getcertDirectory(String sdcCertDir) {
329         String certDirLocation = sdcCertDir;
330         if (certDirLocation == null) {
331             certDirLocation = CERTIFICATE_DEFAULT_LOCATION;
332         }
333         return new File(certDirLocation);
334     }
335
336     private X509Certificate loadCertificate(File certFile) throws SecurityManagerException {
337         try (FileInputStream fi = new FileInputStream(certFile)) {
338             return loadCertificateFactory(fi);
339         } catch (IOException e) {
340             throw new SecurityManagerException("Error during loading Certificate from file!", e);
341         }
342     }
343
344     private X509Certificate loadCertificate(X509CertificateHolder cert) {
345         try {
346             return loadCertificateFactory(new ByteArrayInputStream(cert.getEncoded()));
347         } catch (IOException | SecurityManagerException e) {
348             throw new RuntimeException("Error during loading Certificate from bytes!", e);
349         }
350     }
351
352     private X509Certificate loadCertificateFactory(InputStream in) throws SecurityManagerException {
353         try {
354             CertificateFactory factory = CertificateFactory.getInstance("X.509");
355             return (X509Certificate) factory.generateCertificate(in);
356         } catch (CertificateException e) {
357             throw new SecurityManagerException("Error during loading Certificate from bytes!", e);
358         }
359     }
360
361     private PKIXCertPathBuilderResult verifyCertificate(final X509Certificate cert,
362                                                         final Set<X509Certificate> additionalCerts) throws SecurityManagerException {
363         if (null == cert) {
364             throw new SecurityManagerException("The certificate is empty!");
365         }
366         if (isExpired(cert)) {
367             throw new SecurityManagerException("The certificate expired on: " + cert.getNotAfter());
368         }
369         if (isSelfSigned(cert)) {
370             throw new SecurityManagerException("The certificate is self-signed.");
371         }
372         Set<X509Certificate> trustedRootCerts = new HashSet<>();
373         Set<X509Certificate> intermediateCerts = new HashSet<>();
374         for (X509Certificate additionalCert : additionalCerts) {
375             if (isSelfSigned(additionalCert)) {
376                 trustedRootCerts.add(additionalCert);
377             } else {
378                 intermediateCerts.add(additionalCert);
379             }
380         }
381         try {
382             return verifyCertificate(cert, trustedRootCerts, intermediateCerts);
383         } catch (final GeneralSecurityException e) {
384             throw new SecurityManagerException("Failed to verify certificate", e);
385         }
386     }
387
388     private PKIXCertPathBuilderResult verifyCertificate(X509Certificate cert, Set<X509Certificate> allTrustedRootCerts,
389                                                         Set<X509Certificate> allIntermediateCerts) throws GeneralSecurityException {
390         // Create the selector that specifies the starting certificate
391         X509CertSelector selector = new X509CertSelector();
392         selector.setCertificate(cert);
393         // Create the trust anchors (set of root CA certificates)
394         Set<TrustAnchor> trustAnchors = new HashSet<>();
395         for (X509Certificate trustedRootCert : allTrustedRootCerts) {
396             trustAnchors.add(new TrustAnchor(trustedRootCert, null));
397         }
398         // Configure the PKIX certificate builder algorithm parameters
399         PKIXBuilderParameters pkixParams;
400         try {
401             pkixParams = new PKIXBuilderParameters(trustAnchors, selector);
402         } catch (InvalidAlgorithmParameterException ex) {
403             throw new InvalidAlgorithmParameterException("No root CA has been found for this certificate", ex);
404         }
405         // Not supporting CRL checks for now
406         pkixParams.setRevocationEnabled(false);
407         Set<X509Certificate> certSet = new HashSet<>();
408         certSet.add(cert);
409         pkixParams.addCertStore(createCertStore(certSet));
410         pkixParams.addCertStore(createCertStore(allIntermediateCerts));
411         pkixParams.addCertStore(createCertStore(allTrustedRootCerts));
412         CertPathBuilder builder = CertPathBuilder.getInstance(CertPathBuilder.getDefaultType(), BouncyCastleProvider.PROVIDER_NAME);
413         return (PKIXCertPathBuilderResult) builder.build(pkixParams);
414     }
415
416     private CertStore createCertStore(Set<X509Certificate> certificateSet)
417         throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, NoSuchProviderException {
418         return CertStore.getInstance("Collection", new CollectionCertStoreParameters(certificateSet), BouncyCastleProvider.PROVIDER_NAME);
419     }
420
421     private boolean isExpired(X509Certificate cert) {
422         try {
423             cert.checkValidity();
424         } catch (CertificateExpiredException e) {
425             LOGGER.error(e.getMessage(), e);
426             return true;
427         } catch (CertificateNotYetValidException e) {
428             LOGGER.error(e.getMessage(), e);
429             return false;
430         }
431         return false;
432     }
433
434     private boolean isSelfSigned(X509Certificate cert) {
435         return cert.getIssuerDN().equals(cert.getSubjectDN());
436     }
437
438     /**
439      * Initialization on demand class / synchronized singleton pattern.
440      */
441     private static class SecurityManagerInstanceHolder {
442
443         private static final SecurityManager instance = new SecurityManager();
444     }
445 }