From 089d8c3fb0a277351a55371dff8c2b27bd3f4ed5 Mon Sep 17 00:00:00 2001 From: Bogumil Zebek Date: Wed, 17 Apr 2019 07:56:27 +0200 Subject: Security TC op2 Change-Id: I247c1223b5731c8dbea1480ca88db1cff78cb633 Issue-ID: VNFSDK-342 Signed-off-by: Zebek Bogumil --- .../main/java/org/onap/cvc/csar/CSARArchive.java | 68 ++----- .../main/java/org/onap/cvc/csar/FileArchive.java | 215 +++++++++++++++++++++ .../org/onap/cvc/csar/ZipFileContentValidator.java | 52 +++++ .../org/onap/cvc/csar/cc/VTPValidateCSARBase.java | 17 +- .../cvc/csar/cc/sol004/VTPValidateCSARR787965.java | 82 ++++++++ .../onap/cvc/csar/rsa/RSACertificateValidator.java | 43 +++++ .../onap/cvc/csar/rsa/X509RsaCertification.java | 66 +++++++ 7 files changed, 489 insertions(+), 54 deletions(-) create mode 100644 csarvalidation/src/main/java/org/onap/cvc/csar/FileArchive.java create mode 100644 csarvalidation/src/main/java/org/onap/cvc/csar/ZipFileContentValidator.java create mode 100644 csarvalidation/src/main/java/org/onap/cvc/csar/cc/sol004/VTPValidateCSARR787965.java create mode 100644 csarvalidation/src/main/java/org/onap/cvc/csar/rsa/RSACertificateValidator.java create mode 100644 csarvalidation/src/main/java/org/onap/cvc/csar/rsa/X509RsaCertification.java (limited to 'csarvalidation/src/main/java/org') diff --git a/csarvalidation/src/main/java/org/onap/cvc/csar/CSARArchive.java b/csarvalidation/src/main/java/org/onap/cvc/csar/CSARArchive.java index ed53fc8..448469d 100644 --- a/csarvalidation/src/main/java/org/onap/cvc/csar/CSARArchive.java +++ b/csarvalidation/src/main/java/org/onap/cvc/csar/CSARArchive.java @@ -15,23 +15,18 @@ */ package org.onap.cvc.csar; -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.FilenameFilter; import java.io.IOException; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; +import java.util.Optional; import org.apache.commons.io.FileUtils; import org.yaml.snakeyaml.Yaml; @@ -48,11 +43,11 @@ import com.fasterxml.jackson.databind.ObjectMapper; public class CSARArchive implements AutoCloseable { public static final String SOL0004_2_4_1 = "V2.4.1 (2018-02)"; - public String getSOL004Version() { return SOL0004_2_4_1; } + private FileArchive.Workspace workspace; protected Path tempDir; public static final String TEMP_DIR = "/tmp"; @@ -124,6 +119,10 @@ public class CSARArchive implements AutoCloseable { public static final String CSAR_Archive = "CSAR Archive"; + public FileArchive.Workspace getWorkspace() { + return this.workspace; + } + public enum Mode { WITH_TOSCA_META_DIR, WITHOUT_TOSCA_META_DIR @@ -260,7 +259,6 @@ public class CSARArchive implements AutoCloseable { this.lineNumber = lineNo; this.file = file; - //this.message = "More than one file exists in the archive root for " + fileName + ", It should be only one. Files: " + files; } } @@ -298,7 +296,6 @@ public class CSARArchive implements AutoCloseable { this.message += ". " + message; } - this.file = file; this.lineNumber = lineNo; } } @@ -878,41 +875,6 @@ public class CSARArchive implements AutoCloseable { this.manifest = manifest; } - private void unzip(String csarPath, Path destination) throws IOException { - File csarFile = new File(csarPath); - - if (!csarFile.exists()) { - throw new RuntimeException(csarPath + " does not exist"); - } - - try (ZipInputStream zipInputStream = new ZipInputStream(new BufferedInputStream(new FileInputStream(csarFile)))){ - - ZipEntry entry; - while ((entry = zipInputStream.getNextEntry()) != null) { - - File filePath = new File(destination + File.separator + entry.getName()); - - if(entry.isDirectory()){ - filePath.mkdirs(); - } else { - extract(zipInputStream, filePath); - } - } - } - } - - private void extract(ZipInputStream csar, File filePath) throws IOException { - byte[] buffer = new byte[2048]; - try (FileOutputStream fos = new FileOutputStream(filePath); - BufferedOutputStream bos = new BufferedOutputStream(fos, buffer.length)) { - - int len; - while ((len = csar.read(buffer)) > 0) { - bos.write(buffer, 0, len); - } - } - } - public String getProductName() { if (this.toscaMeta.getMode().equals(Mode.WITH_TOSCA_META_DIR)) { @@ -1317,11 +1279,14 @@ public class CSARArchive implements AutoCloseable { } public void init(String csarPath) throws IOException { - this.tempDir = Paths.get(TEMP_DIR + File.separator + System.currentTimeMillis()); - this.tempDir.toFile().mkdirs(); + this.workspace = new FileArchive(TEMP_DIR).unpack(csarPath); - this.unzip(csarPath, this.tempDir); + final Optional pathToCsarFolder = workspace.getPathToCsarFolder(); + if (pathToCsarFolder.isPresent()) { + this.tempDir = pathToCsarFolder.get(); + } } + public void parse() throws IOException { //Find out the mode of CSAR this.setMode(); @@ -1338,7 +1303,10 @@ public class CSARArchive implements AutoCloseable { public void cleanup() throws IOException { //remove temp dir - FileUtils.deleteDirectory(this.tempDir.toFile()); + final Optional rootFolder = workspace.getRootFolder(); + if(rootFolder.isPresent()) { + FileUtils.deleteDirectory(rootFolder.get().toFile()); + } } public File getFileFromCsar(String path) { @@ -1347,8 +1315,6 @@ public class CSARArchive implements AutoCloseable { @Override public void close() throws Exception { - if(this.tempDir != null){ - cleanup(); - } + cleanup(); } } diff --git a/csarvalidation/src/main/java/org/onap/cvc/csar/FileArchive.java b/csarvalidation/src/main/java/org/onap/cvc/csar/FileArchive.java new file mode 100644 index 0000000..86904b1 --- /dev/null +++ b/csarvalidation/src/main/java/org/onap/cvc/csar/FileArchive.java @@ -0,0 +1,215 @@ +/* + * Copyright 2019 Nokia + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.onap.cvc.csar; + + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Optional; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + + + +public class FileArchive { + + private static final String ZIP_POSTFIX = "zip"; + private static final String CSAR_POSTFIX = ".csar"; + private static final String CERT_POSTFIX = ".cert"; + private static final String CMS_POSTFIX = ".cms"; + private final String tempDir; + + FileArchive(String tempDir){ + this.tempDir = tempDir; + } + + Workspace unpack(String pathToFile) throws IOException { + File fileArchive = new File(pathToFile); + if (!fileArchive.exists()) { + throw new IllegalArgumentException(String.format("%s does not exist", fileArchive.getName())); + } + + String path = String.format("%s%s%d", tempDir, File.separator, System.currentTimeMillis()); + Optional workspaceFolderPath = createWorkspaceFolder(path); + if(workspaceFolderPath.isPresent()) { + final Path destination = workspaceFolderPath.get(); + unzip(fileArchive, destination); + + if (pathToFile.endsWith(ZIP_POSTFIX)) { + return createZipWorkspace(path, destination); + }else { + return Workspace.forCsar(destination); + } + } + + return Workspace.empty(); + } + + private Workspace createZipWorkspace(String path, Path workspaceFolderPath) throws IOException { + + Optional pathToCsarFile = findFile(workspaceFolderPath, CSAR_POSTFIX); + Optional pathToCertFile = findFile(workspaceFolderPath, CERT_POSTFIX); + Optional pathToCmsFile = findFile(workspaceFolderPath, CMS_POSTFIX); + + Path workspaceCsarPath = new File(String.format("%s%scsar", path, File.separator)).toPath(); + if (pathToCsarFile.isPresent()) { + final Path csarFilePath = pathToCsarFile.get(); + unzip(csarFilePath.toFile(), workspaceCsarPath); + + return Workspace.forZip( + workspaceFolderPath, + workspaceCsarPath, + pathToCertFile.orElse(null), + pathToCmsFile.orElse(null), + csarFilePath + ); + } + + + return Workspace.forZip(workspaceFolderPath); + } + + private Optional findFile(Path workspaceFolderPath, String filePostfix) throws IOException { + try(Stream files = Files.find( + workspaceFolderPath, + 1, + (p, b)->p.getFileName().toString().endsWith(filePostfix))){ + return files.findFirst(); + } + } + + private Optional createWorkspaceFolder(String path) { + Path destination = Paths.get(path); + if(destination.toFile().mkdirs()){ + return Optional.of(destination); + } + + return Optional.empty(); + } + + private void unzip(File file, Path destination) throws IOException { + + try (ZipInputStream zipInputStream = new ZipInputStream(new BufferedInputStream(new FileInputStream(file)))){ + + ZipEntry entry; + while ((entry = zipInputStream.getNextEntry()) != null) { + + File filePath = new File(destination + File.separator + entry.getName()); + + if(entry.isDirectory()){ + filePath.mkdirs(); + } else { + extract(zipInputStream, filePath); + } + } + } + } + + private void extract(ZipInputStream csar, File filePath) throws IOException { + byte[] buffer = new byte[2048]; + try (FileOutputStream fos = new FileOutputStream(filePath); + BufferedOutputStream bos = new BufferedOutputStream(fos, buffer.length)) { + + int len; + while ((len = csar.read(buffer)) > 0) { + bos.write(buffer, 0, len); + } + } + } + + public static class Workspace{ + private boolean isZip; + private Path rootFolder; + private Path pathToCsarFolder; + private Path certFile; + private Path cmsFile; + private Path csarFile; + + private Workspace(boolean isZip, Path rootFolder, + Path pathToCsarFolder, + Path certFile, Path cmsFile, + Path csarFile) { + this.isZip = isZip; + this.rootFolder = rootFolder; + this.pathToCsarFolder = pathToCsarFolder; + this.certFile = certFile; + this.cmsFile = cmsFile; + this.csarFile = csarFile; + } + + private Workspace() { + } + + private Workspace(boolean isZip, Path rootFolder){ + this.isZip = isZip; + this.rootFolder = rootFolder; + this.pathToCsarFolder = rootFolder; + } + + static Workspace empty(){ + return new Workspace(); + } + + static Workspace forCsar(Path workspaceFolder) { + return new Workspace(false, workspaceFolder); + } + + static Workspace forZip(Path rootFolder) { + return new Workspace(true, rootFolder); + } + + + static Workspace forZip(Path rootFolder, Path pathToCsarWorkspace, + Path certFile, Path cmsFile, Path csarFile) { + return new Workspace(true, rootFolder, pathToCsarWorkspace, certFile, cmsFile, csarFile); + } + + public boolean isZip() { + return isZip; + } + + public Optional getPathToCsarFolder() { + return Optional.ofNullable(pathToCsarFolder); + } + + public Optional getPathToCertFile() { + return Optional.ofNullable(certFile); + } + + public Optional getRootFolder() { + return Optional.ofNullable(rootFolder); + } + + public Optional getPathToCmsFile() { + return Optional.ofNullable(cmsFile); + } + + public Optional getPathToCsarFile() { + return Optional.ofNullable(csarFile); + } + } + +} diff --git a/csarvalidation/src/main/java/org/onap/cvc/csar/ZipFileContentValidator.java b/csarvalidation/src/main/java/org/onap/cvc/csar/ZipFileContentValidator.java new file mode 100644 index 0000000..801d8cf --- /dev/null +++ b/csarvalidation/src/main/java/org/onap/cvc/csar/ZipFileContentValidator.java @@ -0,0 +1,52 @@ +/* + * Copyright 2019 Nokia + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.onap.cvc.csar; + +import java.util.ArrayList; +import java.util.List; + +public class ZipFileContentValidator { + + public static class CSARErrorCertMissing extends CSARArchive.CSARError { + CSARErrorCertMissing() { + super("0x1008"); + this.message = "Missing. Cert file is not available!"; + } + } + + public static class CSARErrorCMSMissing extends CSARArchive.CSARError { + CSARErrorCMSMissing() { + super("0x1009"); + this.message = "Missing. CMS file is not available!"; + } + } + + public List validate(FileArchive.Workspace workspace){ + final ArrayList retValue = new ArrayList<>(); + + if(!workspace.getPathToCertFile().isPresent()){ + retValue.add(new CSARErrorCertMissing()); + } + + if(!workspace.getPathToCmsFile().isPresent()){ + retValue.add(new CSARErrorCMSMissing()); + } + + return retValue; + } +} diff --git a/csarvalidation/src/main/java/org/onap/cvc/csar/cc/VTPValidateCSARBase.java b/csarvalidation/src/main/java/org/onap/cvc/csar/cc/VTPValidateCSARBase.java index d879ff3..bd771e0 100644 --- a/csarvalidation/src/main/java/org/onap/cvc/csar/cc/VTPValidateCSARBase.java +++ b/csarvalidation/src/main/java/org/onap/cvc/csar/cc/VTPValidateCSARBase.java @@ -16,20 +16,24 @@ package org.onap.cvc.csar.cc; -import java.util.ArrayList; -import java.util.List; - import org.onap.cli.fw.cmd.OnapCommand; import org.onap.cli.fw.error.OnapCommandException; import org.onap.cli.fw.error.OnapCommandExecutionFailed; import org.onap.cvc.csar.CSARArchive; import org.onap.cvc.csar.CSARArchive.CSARError; +import org.onap.cvc.csar.FileArchive; +import org.onap.cvc.csar.ZipFileContentValidator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; +import java.util.List; + public abstract class VTPValidateCSARBase extends OnapCommand { protected static final Logger LOG = LoggerFactory.getLogger(VTPValidateCSARBase.class); + private final ZipFileContentValidator zipFileContentValidator = new ZipFileContentValidator(); + protected abstract void validateCSAR(CSARArchive csar) throws Exception; protected abstract String getVnfReqsNo(); @@ -43,7 +47,14 @@ public abstract class VTPValidateCSARBase extends OnapCommand { //execute try (CSARArchive csar = this.createArchiveInstance()){ + csar.init(path); + + FileArchive.Workspace workspace = csar.getWorkspace(); + if(workspace.isZip()) { + errors.addAll(zipFileContentValidator.validate(workspace)); + } + csar.parse(); errors.addAll(csar.getErrors()); diff --git a/csarvalidation/src/main/java/org/onap/cvc/csar/cc/sol004/VTPValidateCSARR787965.java b/csarvalidation/src/main/java/org/onap/cvc/csar/cc/sol004/VTPValidateCSARR787965.java new file mode 100644 index 0000000..ede1b6c --- /dev/null +++ b/csarvalidation/src/main/java/org/onap/cvc/csar/cc/sol004/VTPValidateCSARR787965.java @@ -0,0 +1,82 @@ +/* + * Copyright 2019 Nokia + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.onap.cvc.csar.cc.sol004; + + +import org.onap.cli.fw.error.OnapCommandException; +import org.onap.cli.fw.schema.OnapCommandSchema; +import org.onap.cvc.csar.CSARArchive; +import org.onap.cvc.csar.FileArchive; +import org.onap.cvc.csar.cc.VTPValidatePnfCSARBase; +import org.onap.cvc.csar.rsa.RSACertificateValidator; +import org.onap.cvc.csar.rsa.X509RsaCertification; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Base64; +import java.util.Optional; + +@OnapCommandSchema(schema = "vtp-validate-csar-r787965.yaml") +public class VTPValidateCSARR787965 extends VTPValidatePnfCSARBase { + + private static final Logger LOG = LoggerFactory.getLogger(VTPValidateCSARR787965.class); + + public static class CSARErrorInvalidSignature extends CSARArchive.CSARError { + CSARErrorInvalidSignature() { + super("0x3001"); + this.message = "Invalid CSAR signature!"; + } + } + + @Override + protected void validateCSAR(CSARArchive csar) throws OnapCommandException { + + try { + final RSACertificateValidator rsaCertificateValidator = new RSACertificateValidator(new X509RsaCertification()); + + FileArchive.Workspace workspace = csar.getWorkspace(); + final Optional pathToCsarFile = workspace.getPathToCsarFile(); + final Optional pathToCertFile = workspace.getPathToCertFile(); + final Optional pathToCmsFile = workspace.getPathToCmsFile(); + + if (workspace.isZip() && pathToCsarFile.isPresent() && pathToCertFile.isPresent() && pathToCmsFile.isPresent()) { + byte[] csarContent = Files.readAllBytes(pathToCsarFile.get()); + String signature = Base64.getEncoder().encodeToString(Files.readAllBytes(pathToCmsFile.get())); + String publicCertification = Base64.getEncoder().encodeToString(Files.readAllBytes(pathToCertFile.get())); + + if (!rsaCertificateValidator.isValid(csarContent, signature, publicCertification)) { + this.errors.add(new CSARErrorInvalidSignature()); + } + } + + } catch (Exception e) { + LOG.error("Internal VTPValidateCSARR787965 command error", e); + throw new OnapCommandException("0x3000", "Internal VTPValidateCSARR787965 command error. See logs."); + } + + } + + @Override + protected String getVnfReqsNo() { + return "R787965"; + } + + +} diff --git a/csarvalidation/src/main/java/org/onap/cvc/csar/rsa/RSACertificateValidator.java b/csarvalidation/src/main/java/org/onap/cvc/csar/rsa/RSACertificateValidator.java new file mode 100644 index 0000000..022f697 --- /dev/null +++ b/csarvalidation/src/main/java/org/onap/cvc/csar/rsa/RSACertificateValidator.java @@ -0,0 +1,43 @@ +/* + * Copyright 2019 Nokia + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.onap.cvc.csar.rsa; + + +import java.security.PublicKey; + +public class RSACertificateValidator { + + private final X509RsaCertification x509RsaCertification; + + public RSACertificateValidator(X509RsaCertification x509RsaCertification) { + this.x509RsaCertification = x509RsaCertification; + } + + public boolean isValid(byte [] content, String signature, String publicCertificateContent) throws Exception { + + String publicCert = extractPublicKeyCertificate(publicCertificateContent); + final PublicKey publicKey = this.x509RsaCertification.generatePublicKey(publicCert); + + return this.x509RsaCertification.verify(content,signature,publicKey); + } + + private String extractPublicKeyCertificate(String publicCertificateContent) { + String publicCert = publicCertificateContent.replace("-----BEGIN CERTIFICATE-----\n", ""); + return publicCert.replace("-----END CERTIFICATE-----\n", ""); + } +} diff --git a/csarvalidation/src/main/java/org/onap/cvc/csar/rsa/X509RsaCertification.java b/csarvalidation/src/main/java/org/onap/cvc/csar/rsa/X509RsaCertification.java new file mode 100644 index 0000000..8395221 --- /dev/null +++ b/csarvalidation/src/main/java/org/onap/cvc/csar/rsa/X509RsaCertification.java @@ -0,0 +1,66 @@ +/* + * Copyright 2019 Nokia + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.onap.cvc.csar.rsa; + +import org.apache.commons.codec.binary.Base64; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; + +public class X509RsaCertification { + + private static final Logger LOG = LoggerFactory.getLogger(X509RsaCertification.class); + + PublicKey generatePublicKey(String cert) throws CertificateException { + byte[] encodedCert = cert.getBytes(StandardCharsets.UTF_8); + byte[] decodedCert = Base64.decodeBase64(encodedCert); + CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + InputStream in = new ByteArrayInputStream(decodedCert); + X509Certificate certificate = (X509Certificate) certFactory.generateCertificate(in); + + LOG.info(String.format("Subject DN : %s", certificate.getSubjectDN().getName())); + LOG.info(String.format("Issuer : %s", certificate.getIssuerDN().getName())); + LOG.info(String.format("Not After: %s", certificate.getNotAfter())); + LOG.info(String.format("Not Before: %s", certificate.getNotBefore())); + LOG.info(String.format("version: %d", certificate.getVersion())); + LOG.info(String.format("serial number : %s", certificate.getSerialNumber())); + + return certificate.getPublicKey(); + } + + boolean verify(byte[] content, String signature, PublicKey publicKey) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { + Signature publicSignature = Signature.getInstance("SHA256withRSA"); + publicSignature.initVerify(publicKey); + publicSignature.update(content); + + byte[] signatureBytes = java.util.Base64.getDecoder().decode(signature); + + return publicSignature.verify(signatureBytes); + } +} -- cgit 1.2.3-korg