Improve error logging in MinIo client
[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             LOGGER.error("Failed to create directory '{}'", folder, e);
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                 LOGGER.error("Signature is not recognized");
203                 throw new SecurityManagerException("Signature is not recognized");
204             }
205
206             try (final InputStream inputStream = artifactStorageManager.get(artifactInfo)) {
207                 if (!findCSARandExtract(inputStream, target)) {
208                     fail = true;
209                     return false;
210                 }
211             }
212             final var verify = verify(packageCert, new CMSSignedData(new CMSProcessableFile(target.toFile()), ContentInfo.getInstance(parsedObject)));
213             fail = !verify;
214             return verify;
215         } catch (final IOException e) {
216             fail = true;
217             LOGGER.error(e.getMessage(), e);
218             throw new SecurityManagerException(UNEXPECTED_ERROR_OCCURRED_DURING_SIGNATURE_VALIDATION, e);
219         } catch (final CMSException e) {
220             fail = true;
221             LOGGER.error(e.getMessage(), e);
222             throw new SecurityManagerException(COULD_NOT_VERIFY_SIGNATURE, e);
223         } catch (final SecurityManagerException e) {
224             fail = true;
225             LOGGER.error(e.getMessage(), e);
226             throw e;
227         } finally {
228             deleteFile(target);
229             if (fail) {
230                 artifactStorageManager.delete(artifactInfo);
231             }
232         }
233     }
234
235     private void deleteFile(final Path filePath) {
236         try {
237             Files.delete(filePath);
238         } catch (final IOException e) {
239             LOGGER.warn("Failed to delete '{}' after verifying package signed data", filePath, e);
240         }
241     }
242
243     private boolean verify(final byte[] packageCert, final CMSSignedData signedData) throws SecurityManagerException {
244         final SignerInformation firstSigner = signedData.getSignerInfos().getSigners().iterator().next();
245         final X509Certificate cert;
246         Collection<X509CertificateHolder> certs;
247         if (packageCert == null) {
248             certs = signedData.getCertificates().getMatches(null);
249             cert = readSignCert(certs, firstSigner)
250                 .orElseThrow(() -> new SecurityManagerException("No certificate found in cms signature that should contain one!"));
251         } else {
252             try {
253                 certs = parseCertsFromPem(packageCert);
254             } catch (final IOException e) {
255                 LOGGER.error("Failed to parse certificate from PEM", e);
256                 throw new SecurityManagerException("Failed to parse certificate from PEM", e);
257             }
258             cert = readSignCert(certs, firstSigner)
259                 .orElseThrow(() -> new SecurityManagerException("No matching certificate found in certificate file that should contain one!"));
260         }
261         trustedCertificatesFromPackage = readTrustedCerts(certs, firstSigner);
262         if (verifyCertificate(cert, getTrustedCertificates()) == null) {
263             return false;
264         }
265         try {
266             return firstSigner.verify(new JcaSimpleSignerInfoVerifierBuilder().build(cert));
267         } catch (CMSException | OperatorCreationException e) {
268             LOGGER.error("Failed to verify package signed data", e);
269             throw new SecurityManagerException("Failed to verify package signed data", e);
270         }
271     }
272
273     private boolean findCSARandExtract(final InputStream inputStream, final Path target) throws IOException {
274         final AtomicBoolean found = new AtomicBoolean(false);
275
276         final var zipInputStream = new ZipInputStream(inputStream);
277         ZipEntry zipEntry;
278         byte[] buffer = new byte[2048];
279         while ((zipEntry = zipInputStream.getNextEntry()) != null) {
280             final var entryName = zipEntry.getName();
281             if (!zipEntry.isDirectory() && entryName.toLowerCase().endsWith(".csar")) {
282                 try (final FileOutputStream fos = new FileOutputStream(target.toFile());
283                     final BufferedOutputStream bos = new BufferedOutputStream(fos, buffer.length)) {
284
285                     int len;
286                     while ((len = zipInputStream.read(buffer)) > 0) {
287                         bos.write(buffer, 0, len);
288                     }
289                 }
290                 found.set(true);
291             }
292         }
293         return found.get();
294     }
295
296     private Optional<X509Certificate> readSignCert(final Collection<X509CertificateHolder> certs, final SignerInformation firstSigner) {
297         return certs.stream().filter(crt -> firstSigner.getSID().match(crt)).findAny().map(this::loadCertificate);
298     }
299
300     private Set<X509Certificate> readTrustedCerts(final Collection<X509CertificateHolder> certs, final SignerInformation firstSigner) {
301         return certs.stream().filter(crt -> !firstSigner.getSID().match(crt)).map(this::loadCertificate).filter(Predicate.not(this::isSelfSigned))
302             .collect(Collectors.toSet());
303     }
304
305     private Set<X509CertificateHolder> parseCertsFromPem(final byte[] packageCert) throws IOException {
306         final ByteArrayInputStream packageCertStream = new ByteArrayInputStream(packageCert);
307         final PEMParser pemParser = new PEMParser(new InputStreamReader(packageCertStream));
308         Object readObject = pemParser.readObject();
309         Set<X509CertificateHolder> allCerts = new HashSet<>();
310         while (readObject != null) {
311             if (readObject instanceof X509CertificateHolder) {
312                 allCerts.add((X509CertificateHolder) readObject);
313             }
314             readObject = pemParser.readObject();
315         }
316         return allCerts;
317     }
318
319     private void processCertificateDir() throws SecurityManagerException {
320         if (!certificateDirectory.exists() || !certificateDirectory.isDirectory()) {
321             LOGGER.error("Issue with certificate directory, check if exists!");
322             return;
323         }
324         File[] files = certificateDirectory.listFiles();
325         if (files == null) {
326             LOGGER.error("Certificate directory is empty!");
327             return;
328         }
329         for (File f : files) {
330             trustedCertificates.add(loadCertificate(f));
331         }
332     }
333
334     private File getcertDirectory(String sdcCertDir) {
335         String certDirLocation = sdcCertDir;
336         if (certDirLocation == null) {
337             certDirLocation = CERTIFICATE_DEFAULT_LOCATION;
338         }
339         return new File(certDirLocation);
340     }
341
342     private X509Certificate loadCertificate(File certFile) throws SecurityManagerException {
343         try (FileInputStream fi = new FileInputStream(certFile)) {
344             return loadCertificateFactory(fi);
345         } catch (IOException e) {
346             LOGGER.error("Error during loading Certificate from file!", e);
347             throw new SecurityManagerException("Error during loading Certificate from file!", e);
348         }
349     }
350
351     private X509Certificate loadCertificate(X509CertificateHolder cert) {
352         try {
353             return loadCertificateFactory(new ByteArrayInputStream(cert.getEncoded()));
354         } catch (IOException | SecurityManagerException e) {
355             LOGGER.error("Error during loading Certificate from bytes!", e);
356             throw new RuntimeException("Error during loading Certificate from bytes!", e);
357         }
358     }
359
360     private X509Certificate loadCertificateFactory(InputStream in) throws SecurityManagerException {
361         try {
362             CertificateFactory factory = CertificateFactory.getInstance("X.509");
363             return (X509Certificate) factory.generateCertificate(in);
364         } catch (CertificateException e) {
365             LOGGER.error("Error during loading Certificate from bytes!", e);
366             throw new SecurityManagerException("Error during loading Certificate from bytes!", e);
367         }
368     }
369
370     private PKIXCertPathBuilderResult verifyCertificate(final X509Certificate cert,
371                                                         final Set<X509Certificate> additionalCerts) throws SecurityManagerException {
372         if (null == cert) {
373             LOGGER.error("The certificate is empty!");
374             throw new SecurityManagerException("The certificate is empty!");
375         }
376         if (isExpired(cert)) {
377             LOGGER.error("The certificate expired on: {}", cert.getNotAfter());
378             throw new SecurityManagerException("The certificate expired on: " + cert.getNotAfter());
379         }
380         if (isSelfSigned(cert)) {
381             LOGGER.error("The certificate is self-signed.");
382             throw new SecurityManagerException("The certificate is self-signed.");
383         }
384         Set<X509Certificate> trustedRootCerts = new HashSet<>();
385         Set<X509Certificate> intermediateCerts = new HashSet<>();
386         for (X509Certificate additionalCert : additionalCerts) {
387             if (isSelfSigned(additionalCert)) {
388                 trustedRootCerts.add(additionalCert);
389             } else {
390                 intermediateCerts.add(additionalCert);
391             }
392         }
393         try {
394             return verifyCertificate(cert, trustedRootCerts, intermediateCerts);
395         } catch (final GeneralSecurityException e) {
396             LOGGER.error("Failed to verify certificate", e);
397             throw new SecurityManagerException("Failed to verify certificate", e);
398         }
399     }
400
401     private PKIXCertPathBuilderResult verifyCertificate(X509Certificate cert, Set<X509Certificate> allTrustedRootCerts,
402                                                         Set<X509Certificate> allIntermediateCerts) throws GeneralSecurityException {
403         // Create the selector that specifies the starting certificate
404         X509CertSelector selector = new X509CertSelector();
405         selector.setCertificate(cert);
406         // Create the trust anchors (set of root CA certificates)
407         Set<TrustAnchor> trustAnchors = new HashSet<>();
408         for (X509Certificate trustedRootCert : allTrustedRootCerts) {
409             trustAnchors.add(new TrustAnchor(trustedRootCert, null));
410         }
411         // Configure the PKIX certificate builder algorithm parameters
412         PKIXBuilderParameters pkixParams;
413         try {
414             pkixParams = new PKIXBuilderParameters(trustAnchors, selector);
415         } catch (InvalidAlgorithmParameterException ex) {
416             LOGGER.error("No root CA has been found for this certificate", ex);
417             throw new InvalidAlgorithmParameterException("No root CA has been found for this certificate", ex);
418         }
419         // Not supporting CRL checks for now
420         pkixParams.setRevocationEnabled(false);
421         Set<X509Certificate> certSet = new HashSet<>();
422         certSet.add(cert);
423         pkixParams.addCertStore(createCertStore(certSet));
424         pkixParams.addCertStore(createCertStore(allIntermediateCerts));
425         pkixParams.addCertStore(createCertStore(allTrustedRootCerts));
426         CertPathBuilder builder = CertPathBuilder.getInstance(CertPathBuilder.getDefaultType(), BouncyCastleProvider.PROVIDER_NAME);
427         return (PKIXCertPathBuilderResult) builder.build(pkixParams);
428     }
429
430     private CertStore createCertStore(Set<X509Certificate> certificateSet)
431         throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, NoSuchProviderException {
432         return CertStore.getInstance("Collection", new CollectionCertStoreParameters(certificateSet), BouncyCastleProvider.PROVIDER_NAME);
433     }
434
435     private boolean isExpired(X509Certificate cert) {
436         try {
437             cert.checkValidity();
438         } catch (CertificateExpiredException e) {
439             LOGGER.error(e.getMessage(), e);
440             return true;
441         } catch (CertificateNotYetValidException e) {
442             LOGGER.error(e.getMessage(), e);
443             return false;
444         }
445         return false;
446     }
447
448     private boolean isSelfSigned(X509Certificate cert) {
449         return cert.getIssuerDN().equals(cert.getSubjectDN());
450     }
451
452     /**
453      * Initialization on demand class / synchronized singleton pattern.
454      */
455     private static class SecurityManagerInstanceHolder {
456
457         private static final SecurityManager instance = new SecurityManager();
458     }
459 }