-
Notifications
You must be signed in to change notification settings - Fork 229
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
Changes from 6 commits
3f9357c
f9f7d78
45ed1c5
e80cffb
e6e6d3d
64685ae
9e4230d
34dd314
7b0f4b2
022a2a1
5de1a66
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 { | ||
|
||
/** | ||
* 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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
} | ||
} |
There was a problem hiding this comment.
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