Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Elide dynamic config model verification #1354

Merged
merged 11 commits into from
Jun 10, 2020
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions elide-contrib/elide-dynamic-config-helpers/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
<mdkt.compiler.version>1.3.0</mdkt.compiler.version>
<commons-io.version>2.6</commons-io.version>
<commons-cli.version>1.4</commons-cli.version>
<commons-compress.version>1.20</commons-compress.version>
</properties>

<dependencies>
Expand Down Expand Up @@ -75,6 +76,11 @@
<artifactId>commons-io</artifactId>
<version>${commons-io.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>${commons-compress.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/*
* Copyright 2020, Yahoo Inc.
* Licensed under the Apache License, Version 2.0
* See LICENSE file in project root for terms.
*/
package com.yahoo.elide.contrib.dynamicconfighelpers.verify;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.MissingOptionException;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
import org.apache.commons.io.FileUtils;

import lombok.extern.slf4j.Slf4j;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.Certificate;
import java.util.Base64;

/**
* Util class to Verify model tar.gz file's RSA signature with available public key in key store.
*/
@Slf4j
public class DynamicConfigVerifier {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Java doc comment explaining what this is for


/**
* Main Method to Verify Signature of Model Tar file.
* @param args : expects 3 arguments.
* @throws ParseException
* @throws IOException
* @throws KeyStoreException
* @throws FileNotFoundException
* @throws SignatureException
* @throws NoSuchAlgorithmException
* @throws InvalidKeyException
* @throws MissingOptionException
*/
public static void main(String[] args) throws ParseException, InvalidKeyException, NoSuchAlgorithmException,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll want a way to programmatically do this as well as run this from the command line.

Copy link
Collaborator Author

@AvaniMakwana AvaniMakwana Jun 9, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

public static boolean verify(String fileContent, String signature, PublicKey publicKey) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {

verify function can be called programmatically to verify the tar signature

SignatureException, FileNotFoundException, KeyStoreException, IOException {

Options options = prepareOptions();
CommandLine cli = new DefaultParser().parse(options, args);

if (cli.hasOption("help")) {
printHelp(options);
return;
}
if (!cli.hasOption("tarFile") || !cli.hasOption("signatureFile") || !cli.hasOption("publicKeyName")) {
printHelp(options);
throw new MissingOptionException("Missing required option");
}

String modelTarFile = cli.getOptionValue("tarFile");
String signatureFile = cli.getOptionValue("signatureFile");
String publicKeyName = cli.getOptionValue("publicKeyName");

if (verify(readTarContents(modelTarFile), signatureFile, getPublicKey(publicKeyName))) {
log.info("Successfully Validated " + modelTarFile);
}
else {
log.error("Could not verify " + modelTarFile + " with details provided");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should set the exit status on failure to something that will cause this to abort shell scripts.

}
}

/**
* Verify signature of tar.gz.
* @param fileContent : content Of all config files
* @param signature : file containing signature
* @param publicKey : public key name
* @return whether the file can be verified by given key and signature
* @throws NoSuchAlgorithmException
* @throws InvalidKeyException
* @throws SignatureException
*/
public static boolean verify(String fileContent, String signature, PublicKey publicKey)
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {

Signature publicSignature;

publicSignature = Signature.getInstance("SHA256withRSA");
publicSignature.initVerify(publicKey);
publicSignature.update(fileContent.getBytes(StandardCharsets.UTF_8));
byte[] signatureBytes = Base64.getDecoder().decode(signature);
return publicSignature.verify(signatureBytes);
}

/**
* Read Content of all files.
* @param archiveFile : tar.gz file path
* @return appended content of all files in tar
* @throws FileNotFoundException
* @throws IOException
*/
public static String readTarContents(String archiveFile) throws FileNotFoundException, IOException {
StringBuffer sb = new StringBuffer();
TarArchiveInputStream archive = new TarArchiveInputStream(
new GzipCompressorInputStream(new BufferedInputStream(new FileInputStream(archiveFile))));
TarArchiveEntry entry;
while ((entry = archive.getNextTarEntry()) != null) {
sb.append(FileUtils.readFileToString(new File(entry.getName()), StandardCharsets.UTF_8));
}
return sb.toString();
}

/**
* Retrieve public key from Key Store.
* @param keyName : name of the public key
* @return publickey
*/
private static PublicKey getPublicKey(String keyName) throws KeyStoreException {
PublicKey publicKey = null;
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
Certificate cert = keyStore.getCertificate(keyName);
publicKey = cert.getPublicKey();
return publicKey;
}

/**
* Define Arguments.
*/
private static final Options prepareOptions() {
Options options = new Options();
options.addOption(new Option("h", "help", false, "Print a help message and exit."));
options.addOption(new Option("t", "tarFile", true, "Path of the tar.gz file"));
options.addOption(new Option("s", "signatureFile", true, "Path of the file containing the signature"));
options.addOption(new Option("p", "publicKeyName", true, "Name of public key in keystore"));
return options;
}

/**
* Print Help.
*/
private static void printHelp(Options options) {
HelpFormatter formatter = new HelpFormatter();
formatter.printHelp(
"java -cp <Jar File> com.yahoo.elide.contrib.dynamicconfighelpers.verify.DynamicConfigVerifier",
options);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* Copyright 2020, Yahoo Inc.
* Licensed under the Apache License, Version 2.0
* See LICENSE file in project root for terms.
*/
package com.yahoo.elide.contrib.dynamicconfighelpers.verify;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import org.apache.commons.cli.MissingArgumentException;
import org.apache.commons.cli.MissingOptionException;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

import java.nio.charset.StandardCharsets;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.SecureRandom;
import java.security.Signature;
import java.util.Base64;

public class DynamicConfigVerifiesTest {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to add a test that creates a gzipped tarball, signs it, and then verifies your functions can do the reverse and verify the signature.


private static KeyPair kp;
private static String signature;

@BeforeAll
public static void setUp() throws Exception {
kp = generateKeyPair();
signature = sign("testing-signature", kp.getPrivate());
}

@Test
public void testValidSignature() throws Exception {
assertTrue(DynamicConfigVerifier.verify("testing-signature", signature, kp.getPublic()));
}

@Test
public void testInvalidSignature() throws Exception {
assertFalse(DynamicConfigVerifier.verify("invalid-signature", signature, kp.getPublic()));
}

@Test
public void testHelpArguments() {
assertDoesNotThrow(() -> DynamicConfigVerifier.main(new String[] { "-h" }));
assertDoesNotThrow(() -> DynamicConfigVerifier.main(new String[] { "--help" }));
}

@Test
public void testNoArguments() {
Exception e = assertThrows(MissingOptionException.class, () -> DynamicConfigVerifier.main(null));
assertTrue(e.getMessage().startsWith("Missing required option"));
}

@Test
public void testOneEmptyArguments() {
Exception e = assertThrows(MissingOptionException.class,
() -> DynamicConfigVerifier.main(new String[] { "" }));
assertTrue(e.getMessage().startsWith("Missing required option"));
}

@Test
public void testMissingArgumentValue() {
Exception e = assertThrows(MissingArgumentException.class,
() -> DynamicConfigVerifier.main(new String[] { "--tarFile" }));
assertTrue(e.getMessage().startsWith("Missing argument for option"));
e = assertThrows(MissingArgumentException.class, () -> DynamicConfigVerifier.main(new String[] { "-t" }));
assertTrue(e.getMessage().startsWith("Missing argument for option"));
}

private static KeyPair generateKeyPair() throws Exception {
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
generator.initialize(2048, new SecureRandom());
KeyPair pair = generator.generateKeyPair();
return pair;
}

private static String sign(String data, PrivateKey privateKey) throws Exception {
Signature privateSignature = Signature.getInstance("SHA256withRSA");
privateSignature.initSign(privateKey);
privateSignature.update(data.getBytes(StandardCharsets.UTF_8));
byte[] signature = privateSignature.sign();
return Base64.getEncoder().encodeToString(signature);
}
}