Public-key authorization scheme to restrict use of NO_ET

-Running the GATK with the -et NO_ET or -et STDOUT options now
 requires a key issued by us. Our reasons for doing this, and the
 procedure for our users to request keys, are documented here:
 http://www.broadinstitute.org/gsa/wiki/index.php/Phone_home

-A GATK user key is an email address plus a cryptographic signature
 signed using our private key, all wrapped in a GZIP container.
 User keys are validated using the public key we now distribute with
 the GATK. Our private key is kept in a secure location.

-Keys are cryptographically secure in that valid keys definitely
 came from us and keys cannot be fabricated, however keys are not
 "copy-protected" in any way.

-Includes private, standalone utilities to create a new GATK user key
 (GenerateGATKUserKey) and to create a new master public/private key
 pair (GenerateKeyPair). Usage of these tools will be documented on
 the internal wiki shortly.

-Comprehensive unit/integration tests, including tests to ensure the
 continued integrity of the GATK master public/private key pair.

-Generation of new user keys and the new unit/integration tests both
 require access to the GATK private key, which can only be read by
 members of the group "gsagit".
This commit is contained in:
David Roazen 2012-02-22 16:45:20 -05:00
parent 027843d791
commit 0702ee1587
16 changed files with 1551 additions and 15 deletions

View File

@ -47,6 +47,7 @@
<property name="R.package.path" value="org/broadinstitute/sting/utils/R" />
<property name="resource.file" value="StingText.properties" />
<property name="resource.path" value="${java.classes}/StingText.properties" />
<property name="key.dir" value="${public.dir}/keys" />
<property name="scala.public.source.dir" value="${public.dir}/scala/src" />
<property name="scala.private.source.dir" value="${private.dir}/scala/src" />
@ -567,6 +568,7 @@
</fileset>
<fileset dir="${java.classes}" includes="**/commandline/**/*.class"/>
<fileset dir="${java.classes}" includes="**/sting/pipeline/**/*.class"/>
<fileset dir="${java.classes}" includes="**/sting/tools/**/*.class"/>
<fileset dir="${java.classes}" includes="**/sting/jna/**/*.class"/>
<fileset dir="${java.classes}" includes="net/sf/picard/**/*.class"/>
<fileset dir="${java.classes}" includes="net/sf/samtools/**/*.class"/>
@ -615,6 +617,9 @@
<include name="**/gatk/**/*.R"/>
<include name="**/alignment/**/*.R"/>
</fileset>
<fileset dir="${key.dir}">
<include name="**/*.key"/>
</fileset>
<manifest>
<attribute name="Main-Class" value="org.broadinstitute.sting.gatk.CommandLineGATK" />
</manifest>
@ -879,6 +884,7 @@
<pathelement location="${R.tar.dir}" />
<pathelement location="${R.public.scripts.dir}" />
<pathelement location="${R.private.scripts.dir}" />
<pathelement location="${key.dir}" />
<path refid="external.dependencies" />
</path>

View File

@ -35,9 +35,12 @@ import org.broadinstitute.sting.gatk.io.stubs.VCFWriterArgumentTypeDescriptor;
import org.broadinstitute.sting.gatk.phonehome.GATKRunReport;
import org.broadinstitute.sting.gatk.refdata.utils.RMDTriplet;
import org.broadinstitute.sting.gatk.walkers.Walker;
import org.broadinstitute.sting.utils.classloader.JVMUtils;
import org.broadinstitute.sting.utils.crypt.CryptUtils;
import org.broadinstitute.sting.utils.crypt.GATKKey;
import org.broadinstitute.sting.utils.exceptions.UserException;
import org.broadinstitute.sting.utils.text.ListFileUtils;
import java.security.PublicKey;
import java.util.*;
/**
@ -78,6 +81,9 @@ public abstract class CommandLineExecutable extends CommandLineProgram {
Walker<?,?> walker = engine.getWalkerByName(getAnalysisName());
try {
// Make sure a valid GATK user key is present, if required.
authorizeGATKRun();
engine.setArguments(getArgumentCollection());
// File lists can require a bit of additional expansion. Set these explicitly by the engine.
@ -130,6 +136,28 @@ public abstract class CommandLineExecutable extends CommandLineProgram {
return 0;
}
/**
* Authorizes this run of the GATK by checking for a valid GATK user key, if required.
* Currently, a key is required only if running with the -et NO_ET or -et STDOUT options.
*/
private void authorizeGATKRun() {
if ( getArgumentCollection().phoneHomeType == GATKRunReport.PhoneHomeOption.NO_ET ||
getArgumentCollection().phoneHomeType == GATKRunReport.PhoneHomeOption.STDOUT ) {
if ( getArgumentCollection().gatkKeyFile == null ) {
throw new UserException("Running with the -et NO_ET or -et STDOUT option requires a GATK Key file. " +
"Please see http://www.broadinstitute.org/gsa/wiki/index.php/Phone_home " +
"for more information and instructions on how to obtain a key.");
}
else {
PublicKey gatkPublicKey = CryptUtils.loadGATKDistributedPublicKey();
GATKKey gatkUserKey = new GATKKey(gatkPublicKey, getArgumentCollection().gatkKeyFile);
if ( ! gatkUserKey.isValid() ) {
throw new UserException.KeySignatureVerificationException(getArgumentCollection().gatkKeyFile);
}
}
}
}
/**
* Generate the GATK run report for this walker using the current GATKEngine, if -et is enabled.

View File

@ -65,9 +65,12 @@ public class GATKArgumentCollection {
@Argument(fullName = "read_buffer_size", shortName = "rbs", doc="Number of reads per SAM file to buffer in memory", required = false)
public Integer readBufferSize = null;
@Argument(fullName = "phone_home", shortName = "et", doc="What kind of GATK run report should we generate? Standard is the default, can be verbose or NO_ET so nothing is posted to the run repository", required = false)
@Argument(fullName = "phone_home", shortName = "et", doc="What kind of GATK run report should we generate? STANDARD is the default, can be NO_ET so nothing is posted to the run repository. Please see http://www.broadinstitute.org/gsa/wiki/index.php/Phone_home for details.", required = false)
public GATKRunReport.PhoneHomeOption phoneHomeType = GATKRunReport.PhoneHomeOption.STANDARD;
@Argument(fullName = "gatk_key", shortName = "K", doc="GATK Key file. Required if running with -et NO_ET. Please see http://www.broadinstitute.org/gsa/wiki/index.php/Phone_home for details.", required = false)
public File gatkKeyFile = null;
@Argument(fullName = "read_filter", shortName = "rf", doc = "Specify filtration criteria to apply to each read individually", required = false)
public List<String> readFilters = new ArrayList<String>();

View File

@ -154,9 +154,7 @@ public class GATKRunReport {
/** Standard option. Writes to local repository if it can be found, or S3 otherwise */
STANDARD,
/** Force output to STDOUT. For debugging only */
STDOUT,
/** Force output to S3. For debugging only */
AWS_S3 // todo -- remove me -- really just for testing purposes
STDOUT
}
private static final DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH.mm.ss");
@ -239,11 +237,8 @@ public class GATKRunReport {
case STDOUT:
postReportToStream(System.out);
break;
case AWS_S3:
postReportToAWSS3();
break;
default:
exceptDuringRunReport("BUG: unexcepted PhoneHomeOption ");
exceptDuringRunReport("BUG: unexpected PhoneHomeOption ");
break;
}
}

View File

@ -0,0 +1,390 @@
/*
* Copyright (c) 2012, The Broad Institute
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
package org.broadinstitute.sting.utils.crypt;
import org.broadinstitute.sting.utils.exceptions.ReviewedStingException;
import org.broadinstitute.sting.utils.io.IOUtils;
import javax.crypto.Cipher;
import java.io.File;
import java.io.InputStream;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Arrays;
/**
* A set of cryptographic utility methods and constants.
*
* Contains methods to:
*
* -Create a public/private key pair
* -Read and write public/private keys to/from files/streams
* -Load the GATK master private/public keys
* -Encrypt/decrypt data
*
* Also contains constants that control the cryptographic defaults
* throughout the GATK.
*
* @author David Roazen
*/
public class CryptUtils {
// ---------------------------------------------------------------------------------
// Constants (these control the default cryptographic settings throughout the GATK):
// ---------------------------------------------------------------------------------
/**
* Default key length in bits of newly-created keys. 2048 bits provides a good balance between
* security and speed.
*/
public static final int DEFAULT_KEY_LENGTH = 2048;
/**
* Default encryption algorithm to use, when none is specified.
*/
public static final String DEFAULT_ENCRYPTION_ALGORITHM = "RSA";
/**
* Default random-number generation algorithm to use, when none is specified.
*/
public static final String DEFAULT_RANDOM_NUMBER_GENERATION_ALGORITHM = "SHA1PRNG";
/**
* Name of the public key file distributed with the GATK. This file is packaged
* into the GATK jar, and we use the system ClassLoader to find it.
*/
public static final String GATK_DISTRIBUTED_PUBLIC_KEY_FILE_NAME = "GATK_public.key";
/**
* Location of the master copy of the GATK private key.
*/
public static final String GATK_MASTER_PRIVATE_KEY_FILE = "/humgen/gsa-hpprojects/GATK/data/gatk_master_keys/GATK_private.key";
/**
* Location of the master copy of the GATK public key. This file should always be the same as
* the public key file distributed with the GATK (and there are automated tests to ensure that it is).
*/
public static final String GATK_MASTER_PUBLIC_KEY_FILE = "/humgen/gsa-hpprojects/GATK/data/gatk_master_keys/GATK_public.key";
/**
* Directory where generated GATK user keys are stored. See the GATKKey class for more information.
*/
public static final String GATK_USER_KEY_DIRECTORY = "/humgen/gsa-hpprojects/GATK/data/gatk_user_keys/";
// -----------------------
// Utility Methods:
// -----------------------
/**
* Generate a new public/private key pair using the default encryption settings defined above.
*
* @return A new public/private key pair created using the default settings
*/
public static KeyPair generateKeyPair() {
return generateKeyPair(DEFAULT_KEY_LENGTH, DEFAULT_ENCRYPTION_ALGORITHM, DEFAULT_RANDOM_NUMBER_GENERATION_ALGORITHM);
}
/**
* Generate a new public/private key pair using custom encryption settings.
*
* @param keyLength Length of the key in bits
* @param encryptionAlgorithm Encryption algorithm to use
* @param randNumberAlgorithm Random-number generation algorithm to use
* @return A new public/private key pair, created according to the specified parameters
*/
public static KeyPair generateKeyPair( int keyLength, String encryptionAlgorithm, String randNumberAlgorithm ) {
try {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance(encryptionAlgorithm);
SecureRandom randomnessSource = createRandomnessSource(randNumberAlgorithm);
keyGen.initialize(keyLength, randomnessSource);
return keyGen.generateKeyPair();
}
catch ( NoSuchAlgorithmException e ) {
throw new ReviewedStingException(String.format("Could not find an implementation of the requested encryption algorithm %s", encryptionAlgorithm), e);
}
catch ( Exception e ) {
throw new ReviewedStingException("Error while generating key pair", e);
}
}
/**
* Create a source of randomness using the default random-number generation algorithm.
*
* @return A randomness source that uses the default algorithm
*/
public static SecureRandom createRandomnessSource() {
return createRandomnessSource(DEFAULT_RANDOM_NUMBER_GENERATION_ALGORITHM);
}
/**
* Create a source of randomness using a custom random-number generation algorithm.
*
* @param randAlgorithm The random-number generation algorithm to use
* @return A randomness sources that uses the specified algorithm
*/
public static SecureRandom createRandomnessSource ( String randAlgorithm ) {
try {
return SecureRandom.getInstance(randAlgorithm);
}
catch ( NoSuchAlgorithmException e ) {
throw new ReviewedStingException(String.format("Could not find an implementation of the requested random-number generation algorithm %s", randAlgorithm), e);
}
}
/**
* Writes a public/private key pair to disk
*
* @param keyPair The key pair we're writing to disk
* @param privateKeyFile Location to write the private key
* @param publicKeyFile Location to write the public key
*/
public static void writeKeyPair ( KeyPair keyPair, File privateKeyFile, File publicKeyFile ) {
writeKey(keyPair.getPrivate(), privateKeyFile);
writeKey(keyPair.getPublic(), publicKeyFile);
}
/**
* Writes an arbitrary key to disk
*
* @param key The key to write
* @param destination Location to write the key to
*/
public static void writeKey ( Key key, File destination ) {
IOUtils.writeByteArrayToFile(key.getEncoded(), destination);
}
/**
* Reads in a public key created using the default encryption algorithm from a file.
*
* @param source File containing the public key
* @return The public key read
*/
public static PublicKey readPublicKey ( File source ) {
return decodePublicKey(IOUtils.readFileIntoByteArray(source), DEFAULT_ENCRYPTION_ALGORITHM);
}
/**
* Reads in a public key created using the default encryption algorithm from a stream.
*
* @param source Stream attached to the public key
* @return The public key read
*/
public static PublicKey readPublicKey ( InputStream source ) {
return decodePublicKey(IOUtils.readStreamIntoByteArray(source), DEFAULT_ENCRYPTION_ALGORITHM);
}
/**
* Decodes the raw bytes of a public key into a usable object.
*
* @param rawKey The encoded bytes of a public key as read from, eg., a file. The
* key must be in the standard X.509 format for a public key.
* @param encryptionAlgorithm The encryption algorithm used to create the public key
* @return The public key as a usable object
*/
public static PublicKey decodePublicKey ( byte[] rawKey, String encryptionAlgorithm ) {
try {
KeySpec keySpec = new X509EncodedKeySpec(rawKey);
KeyFactory keyFactory = KeyFactory.getInstance(encryptionAlgorithm);
return keyFactory.generatePublic(keySpec);
}
catch ( NoSuchAlgorithmException e ) {
throw new ReviewedStingException(String.format("Could not find an implementation of the requested encryption algorithm %s", encryptionAlgorithm), e);
}
catch ( InvalidKeySpecException e ) {
throw new ReviewedStingException("Unable to use X.509 key specification to decode the given key", e);
}
}
/**
* Reads in a private key created using the default encryption algorithm from a file.
*
* @param source File containing the private key
* @return The private key read
*/
public static PrivateKey readPrivateKey ( File source ) {
return decodePrivateKey(IOUtils.readFileIntoByteArray(source), DEFAULT_ENCRYPTION_ALGORITHM);
}
/**
* Reads in a private key created using the default encryption algorithm from a stream.
*
* @param source Stream attached to the private key
* @return The private key read
*/
public static PrivateKey readPrivateKey ( InputStream source ) {
return decodePrivateKey(IOUtils.readStreamIntoByteArray(source), DEFAULT_ENCRYPTION_ALGORITHM);
}
/**
* Decodes the raw bytes of a private key into a usable object.
*
* @param rawKey The encoded bytes of a private key as read from, eg., a file. The
* key must be in the standard PKCS #8 format for a private key.
* @param encryptionAlgorithm The encryption algorithm used to create the private key
* @return The private key as a usable object
*/
public static PrivateKey decodePrivateKey ( byte[] rawKey, String encryptionAlgorithm ) {
try {
KeySpec keySpec = new PKCS8EncodedKeySpec(rawKey);
KeyFactory keyFactory = KeyFactory.getInstance(encryptionAlgorithm);
return keyFactory.generatePrivate(keySpec);
}
catch ( NoSuchAlgorithmException e ) {
throw new ReviewedStingException(String.format("Could not find an implementation of the requested encryption algorithm %s", encryptionAlgorithm), e);
}
catch ( InvalidKeySpecException e ) {
throw new ReviewedStingException("Unable to use the PKCS #8 key specification to decode the given key", e);
}
}
/**
* Loads the copy of the GATK public key that is distributed with the GATK. Uses the system
* ClassLoader to locate the public key file, which should be stored at the root of the GATK
* jar file.
*
* @return The GATK public key as a usable object
*/
public static PublicKey loadGATKDistributedPublicKey() {
InputStream publicKeyInputStream = ClassLoader.getSystemResourceAsStream(GATK_DISTRIBUTED_PUBLIC_KEY_FILE_NAME);
if ( publicKeyInputStream == null ) {
throw new ReviewedStingException(String.format("Could not locate the GATK public key %s in the classpath",
GATK_DISTRIBUTED_PUBLIC_KEY_FILE_NAME));
}
return readPublicKey(publicKeyInputStream);
}
/**
* Loads the master copy of the GATK private key. You must have the appropriate UNIX permissions
* to do this!
*
* @return The GATK master private key as a usable object
*/
public static PrivateKey loadGATKMasterPrivateKey() {
return readPrivateKey(new File(GATK_MASTER_PRIVATE_KEY_FILE));
}
/**
* Loads the master copy of the GATK public key. This should always be the same as the
* public key distributed with the GATK returned by loadGATKDistributedPublicKey().
*
* @return The GATK master public key as a usable object
*/
public static PublicKey loadGATKMasterPublicKey() {
return readPublicKey(new File(GATK_MASTER_PUBLIC_KEY_FILE));
}
/**
* Encrypts the given data using the key provided.
*
* @param data The data to encrypt, as a byte array
* @param encryptKey The key with which to encrypt the data
* @return The encrypted version of the provided data
*/
public static byte[] encryptData ( byte[] data, Key encryptKey ) {
return transformDataUsingCipher(data, encryptKey, Cipher.ENCRYPT_MODE);
}
/**
* Decrypts the given data using the key provided.
*
* @param encryptedData Data to decrypt, as a byte array
* @param decryptKey The key with which to decrypt the data
* @return The decrypted version of the provided data
*/
public static byte[] decryptData ( byte[] encryptedData, Key decryptKey ) {
return transformDataUsingCipher(encryptedData, decryptKey, Cipher.DECRYPT_MODE);
}
/**
* Helper method for encryption/decryption that takes data and processes it using
* the given key
*
* @param data Data to encrypt/decrypt
* @param key Key to use to encrypt/decrypt the data
* @param cipherMode Specifies whether we are encrypting or decrypting
* @return The encrypted/decrypted data
*/
private static byte[] transformDataUsingCipher ( byte[] data, Key key, int cipherMode ) {
try {
Cipher cipher = Cipher.getInstance(key.getAlgorithm());
cipher.init(cipherMode, key);
return cipher.doFinal(data);
}
catch ( NoSuchAlgorithmException e ) {
throw new ReviewedStingException(String.format("Could not find an implementation of the requested algorithm %s",
key.getAlgorithm()), e);
}
catch ( InvalidKeyException e ) {
throw new ReviewedStingException("Key is invalid", e);
}
catch ( GeneralSecurityException e ) {
throw new ReviewedStingException("Error during encryption", e);
}
}
/**
* Tests whether the public/private keys provided can each decrypt data encrypted by
* the other key -- ie., tests whether these two keys are part of the same public/private
* key pair.
*
* @param privateKey The private key to test
* @param publicKey The public key to test
* @return True if the keys are part of the same key pair and can decrypt each other's
* encrypted data, otherwise false.
*/
public static boolean keysDecryptEachOther ( PrivateKey privateKey, PublicKey publicKey ) {
byte[] plainText = "Test PlainText".getBytes();
byte[] dataEncryptedUsingPrivateKey = CryptUtils.encryptData(plainText, privateKey);
byte[] dataEncryptedUsingPublicKey = CryptUtils.encryptData(plainText, publicKey);
byte[] privateKeyDataDecryptedWithPublicKey = CryptUtils.decryptData(dataEncryptedUsingPrivateKey, publicKey);
byte[] publicKeyDataDecryptedWithPrivateKey = CryptUtils.decryptData(dataEncryptedUsingPublicKey, privateKey);
// Make sure we actually transformed the data during encryption:
if ( Arrays.equals(plainText, dataEncryptedUsingPrivateKey) ||
Arrays.equals(plainText, dataEncryptedUsingPublicKey) ||
Arrays.equals(dataEncryptedUsingPrivateKey, dataEncryptedUsingPublicKey) ) {
return false;
}
// Make sure that we were able to recreate the original plaintext using
// both the public key on the private-key-encrypted data and the private
// key on the public-key-encrypted data:
if ( ! Arrays.equals(plainText, privateKeyDataDecryptedWithPublicKey) ||
! Arrays.equals(plainText, publicKeyDataDecryptedWithPrivateKey) ) {
return false;
}
return true;
}
}

View File

@ -0,0 +1,349 @@
/*
* Copyright (c) 2012, The Broad Institute
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
package org.broadinstitute.sting.utils.crypt;
import org.broadinstitute.sting.utils.exceptions.ReviewedStingException;
import org.broadinstitute.sting.utils.exceptions.UserException;
import org.broadinstitute.sting.utils.io.IOUtils;
import java.io.*;
import java.security.*;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
/**
* Class to represent a GATK user key.
*
* A GATK user key contains an email address and a cryptographic signature.
* The signature is the SHA-1 hash of the email address encrypted using
* the GATK master private key. The GATK master public key (distributed
* with the GATK) is used to decrypt the signature and validate the key
* at the start of each GATK run that requires a key.
*
* Keys are cryptographically secure in that valid keys definitely come
* from us and cannot be fabricated, however nothing prevents keys from
* being shared between users.
*
* GATK user keys have the following on-disk format:
*
* GZIP Container:
* Email address
* NUL byte (delimiter)
* Cryptographic Signature (encrypted SHA-1 hash of email address)
*
* The key data is wrapped within a GZIP container to placate over-zealous
* email filters (since keys must often be emailed) and also to provide an
* additional integrity check via the built-in GZIP CRC.
*
* @author David Roazen
*/
public class GATKKey {
/**
* Private key used to sign the GATK key. Required only when creating a new
* key from scratch, not when loading an existing key from disk.
*/
private PrivateKey privateKey;
/**
* Public key used to validate the GATK key.
*/
private PublicKey publicKey;
/**
* The user's email address, stored within the key and signed.
*/
private String emailAddress;
/**
* The cryptographic signature of the email address. By default, this is
* the SHA-1 hash of the email address encrypted using the RSA algorithm.
*/
private byte[] signature;
/**
* The combination of hash/encryption algorithms to use to generate the signature.
* By default this is "SHA1withRSA"
*/
private String signingAlgorithm;
/**
* Default hash/encryption algorithms to use to sign the key.
*/
public static final String DEFAULT_SIGNING_ALGORITHM = "SHA1withRSA";
/**
* Byte value used to separate the email address from its signature in the key file.
*/
public static final byte GATK_KEY_SECTIONAL_DELIMITER = 0;
// -----------------------
// Constructors:
// -----------------------
/**
* Constructor to create a new GATK key from scratch using an email address
* and public/private key pair. The private key is used for signing, and the
* public key is used to validate the newly-created key.
*
* @param privateKey Private key used to sign the new GATK key
* @param publicKey Public key used to validate the new GATK key
* @param emailAddress The user's email address, which we will store in the key and sign
*/
public GATKKey ( PrivateKey privateKey, PublicKey publicKey, String emailAddress ) {
this(privateKey, publicKey, emailAddress, DEFAULT_SIGNING_ALGORITHM);
}
/**
* Constructor to create a new GATK key from scratch using an email address
* and public/private key pair, and additionally specify the signing algorithm
* to use. The private key is used for signing, and the public key is used to
* validate the newly-created key.
*
* @param privateKey Private key used to sign the new GATK key
* @param publicKey Public key used to validate the new GATK key
* @param emailAddress The user's email address, which we will store in the key and sign
* @param signingAlgorithm The combination of hash and encryption algorithms to use to sign the key
*/
public GATKKey ( PrivateKey privateKey, PublicKey publicKey, String emailAddress, String signingAlgorithm ) {
if ( privateKey == null || publicKey == null || emailAddress == null || emailAddress.length() == 0 || signingAlgorithm == null ) {
throw new ReviewedStingException("Cannot construct GATKKey using null/empty arguments");
}
this.privateKey = privateKey;
this.publicKey = publicKey;
this.emailAddress = emailAddress;
this.signingAlgorithm = signingAlgorithm;
validateEmailAddress();
generateSignature();
if ( ! isValid() ) {
throw new ReviewedStingException("Newly-generated GATK key fails validation -- this should never happen!");
}
}
/**
* Constructor to load an existing GATK key from a file.
*
* During loading, the key file is checked for integrity, but not cryptographic
* validity (which must be done through a subsequent call to isValid()).
*
* @param publicKey Public key that will be used to validate the loaded GATK key
* in subsequent calls to isValid()
* @param keyFile File containing the GATK key to load
*/
public GATKKey ( PublicKey publicKey, File keyFile ) {
this(publicKey, keyFile, DEFAULT_SIGNING_ALGORITHM);
}
/**
* Constructor to load an existing GATK key from a file, and additionally specify
* the signing algorithm used to sign the key being loaded.
*
* During loading, the key file is checked for integrity, but not cryptographic
* validity (which must be done through a subsequent call to isValid()).
*
* @param publicKey Public key that will be used to validate the loaded GATK key
* in subsequent calls to isValid()
* @param keyFile File containing the GATK key to load
* @param signingAlgorithm The combination of hash and encryption algorithms used to sign the key
*/
public GATKKey ( PublicKey publicKey, File keyFile, String signingAlgorithm ) {
if ( publicKey == null || keyFile == null || signingAlgorithm == null ) {
throw new ReviewedStingException("Cannot construct GATKKey using null arguments");
}
this.publicKey = publicKey;
this.signingAlgorithm = signingAlgorithm;
readKey(keyFile);
}
// -----------------------
// Public API Methods:
// -----------------------
/**
* Writes out this key to a file in the format described at the top of this class,
* encapsulating the key within a GZIP container.
*
* @param destination File to write the key to
*/
public void writeKey ( File destination ) {
try {
byte[] keyBytes = marshalKeyData();
IOUtils.writeByteArrayToStream(keyBytes, new GZIPOutputStream(new FileOutputStream(destination)));
}
catch ( IOException e ) {
throw new UserException.CouldNotCreateOutputFile(destination, e);
}
}
/**
* Checks whether the signature of this key is cryptographically valid (ie., can be
* decrypted by the public key to produce a valid SHA-1 hash of the email address
* in the key).
*
* @return True if the key's signature passes validation, otherwise false
*/
public boolean isValid() {
try {
Signature sig = Signature.getInstance(signingAlgorithm);
sig.initVerify(publicKey);
sig.update(emailAddress.getBytes());
return sig.verify(signature);
}
catch ( NoSuchAlgorithmException e ) {
throw new ReviewedStingException(String.format("Signing algorithm %s not found", signingAlgorithm), e);
}
catch ( InvalidKeyException e ) {
// If the GATK public key is invalid, it's likely our problem, not the user's:
throw new ReviewedStingException(String.format("Public key %s is invalid", publicKey), e);
}
catch ( SignatureException e ) {
throw new UserException.UnreadableKeyException("Signature is invalid or signing algorithm was unable to process the input data", e);
}
}
// -----------------------
// Private Helper Methods:
// -----------------------
/**
* Helper method that creates a signature for this key using the combination of
* hash/encryption algorithms specified at construction time.
*/
private void generateSignature() {
try {
Signature sig = Signature.getInstance(signingAlgorithm);
sig.initSign(privateKey, CryptUtils.createRandomnessSource());
sig.update(emailAddress.getBytes());
signature = sig.sign();
}
catch ( NoSuchAlgorithmException e ) {
throw new ReviewedStingException(String.format("Signing algorithm %s not found", signingAlgorithm), e);
}
catch ( InvalidKeyException e ) {
throw new ReviewedStingException(String.format("Private key %s is invalid", privateKey), e);
}
catch ( SignatureException e ) {
throw new ReviewedStingException(String.format("Error creating signature for email address %s", emailAddress), e);
}
}
/**
* Helper method that reads in a GATK key from a file. Should not be called directly --
* use the appropriate constructor above.
*
* @param source File to read the key from
*/
private void readKey ( File source ) {
try {
byte[] keyBytes = IOUtils.readStreamIntoByteArray(new GZIPInputStream(new FileInputStream(source)));
// As a sanity check, compare the number of bytes read to the uncompressed file size
// stored in the GZIP ISIZE field. If they don't match, the key must be corrupt:
if ( keyBytes.length != IOUtils.getGZIPFileUncompressedSize(source) ) {
throw new UserException.UnreadableKeyException("Number of bytes read does not match the uncompressed size specified in the GZIP ISIZE field");
}
unmarshalKeyData(keyBytes);
}
catch ( FileNotFoundException e ) {
throw new UserException.CouldNotReadInputFile(source, e);
}
catch ( IOException e ) {
throw new UserException.UnreadableKeyException(source, e);
}
catch ( UserException.CouldNotReadInputFile e ) {
throw new UserException.UnreadableKeyException(source, e);
}
}
/**
* Helper method that assembles the email address and signature into a format
* suitable for writing to disk.
*
* @return The aggregated key data, ready to be written to disk
*/
private byte[] marshalKeyData() {
byte[] emailAddressBytes = emailAddress.getBytes();
byte[] assembledKey = new byte[emailAddressBytes.length + 1 + signature.length];
System.arraycopy(emailAddressBytes, 0, assembledKey, 0, emailAddressBytes.length);
assembledKey[emailAddressBytes.length] = GATK_KEY_SECTIONAL_DELIMITER;
System.arraycopy(signature, 0, assembledKey, emailAddressBytes.length + 1, signature.length);
return assembledKey;
}
/**
* Helper method that parses the raw key data from disk into its component
* email address and signature. Performs some basic validation in the process.
*
* @param keyBytes The raw, uncompressed key data read from disk
*/
private void unmarshalKeyData ( byte[] keyBytes ) {
int delimiterPosition = -1;
for ( int i = 0; i < keyBytes.length; i++ ) {
if ( keyBytes[i] == GATK_KEY_SECTIONAL_DELIMITER ) {
delimiterPosition = i;
break;
}
}
if ( delimiterPosition == -1 ) {
throw new UserException.UnreadableKeyException("Malformed GATK key contains no sectional delimiter");
}
else if ( delimiterPosition == 0 ) {
throw new UserException.UnreadableKeyException("Malformed GATK key contains no email address");
}
else if ( delimiterPosition == keyBytes.length - 1 ) {
throw new UserException.UnreadableKeyException("Malformed GATK key contains no signature");
}
byte[] emailAddressBytes = new byte[delimiterPosition];
System.arraycopy(keyBytes, 0, emailAddressBytes, 0, delimiterPosition);
emailAddress = new String(emailAddressBytes);
signature = new byte[keyBytes.length - delimiterPosition - 1];
System.arraycopy(keyBytes, delimiterPosition + 1, signature, 0, keyBytes.length - delimiterPosition - 1);
}
/**
* Helper method that ensures that the user's email address does not contain the NUL byte, which we
* reserve as a delimiter within each key file.
*/
private void validateEmailAddress() {
for ( byte b : emailAddress.getBytes() ) {
if ( b == GATK_KEY_SECTIONAL_DELIMITER ) {
throw new UserException(String.format("Email address must not contain a byte with value %d", GATK_KEY_SECTIONAL_DELIMITER));
}
}
}
}

View File

@ -132,6 +132,10 @@ public class UserException extends ReviewedStingException {
public CouldNotReadInputFile(File file, Exception e) {
this(file, e.getMessage());
}
public CouldNotReadInputFile(String message) {
super(message);
}
}
@ -151,6 +155,10 @@ public class UserException extends ReviewedStingException {
public CouldNotCreateOutputFile(File file, Exception e) {
super(String.format("Couldn't write file %s because exception %s", file.getAbsolutePath(), e.getMessage()));
}
public CouldNotCreateOutputFile(String message, Exception e) {
super(message, e);
}
}
public static class MissortedBAM extends UserException {
@ -319,4 +327,32 @@ public class UserException extends ReviewedStingException {
"and try again.", null);
}
}
public static class UnreadableKeyException extends UserException {
public UnreadableKeyException ( File f, Exception e ) {
super(String.format("Key file %s cannot be read (possibly the key file is corrupt?). Error was: %s. " +
"Please see http://www.broadinstitute.org/gsa/wiki/index.php/Phone_home for help.",
f.getAbsolutePath(), e.getMessage()));
}
public UnreadableKeyException ( String message, Exception e ) {
this(String.format("%s. Error was: %s", message, e.getMessage()));
}
public UnreadableKeyException ( String message ) {
super(String.format("Key file cannot be read (possibly the key file is corrupt?): %s. " +
"Please see http://www.broadinstitute.org/gsa/wiki/index.php/Phone_home for help.",
message));
}
}
public static class KeySignatureVerificationException extends UserException {
public KeySignatureVerificationException ( File f ) {
super(String.format("The signature in key file %s failed cryptographic verification. " +
"If this key was valid in the past, it's likely been revoked. " +
"Please see http://www.broadinstitute.org/gsa/wiki/index.php/Phone_home " +
"for help.",
f.getAbsolutePath()));
}
}
}

View File

@ -29,10 +29,13 @@ import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.LineIterator;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.broadinstitute.sting.utils.exceptions.ReviewedStingException;
import org.broadinstitute.sting.utils.exceptions.StingException;
import org.broadinstitute.sting.utils.exceptions.UserException;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.*;
public class IOUtils {
@ -400,4 +403,173 @@ public class IOUtils {
public static boolean isSpecialFile(File file) {
return file != null && (file.getAbsolutePath().startsWith("/dev/") || file.equals(DEV_DIR));
}
/**
* Reads the entirety of the given file into a byte array. Uses a read buffer size of 4096 bytes.
*
* @param source File to read
* @return The contents of the file as a byte array
*/
public static byte[] readFileIntoByteArray ( File source ) {
return readFileIntoByteArray(source, 4096);
}
/**
* Reads the entirety of the given file into a byte array using the requested read buffer size.
*
* @param source File to read
* @param readBufferSize Number of bytes to read in at one time
* @return The contents of the file as a byte array
*/
public static byte[] readFileIntoByteArray ( File source, int readBufferSize ) {
if ( source == null ) {
throw new ReviewedStingException("Source file was null");
}
byte[] fileContents;
try {
fileContents = readStreamIntoByteArray(new FileInputStream(source), readBufferSize);
}
catch ( FileNotFoundException e ) {
throw new UserException.CouldNotReadInputFile(source, e);
}
if ( fileContents.length != source.length() ) {
throw new UserException.CouldNotReadInputFile(String.format("Unable to completely read file %s: read only %d/%d bytes",
source.getAbsolutePath(), fileContents.length, source.length()));
}
return fileContents;
}
/**
* Reads all data from the given stream into a byte array. Uses a read buffer size of 4096 bytes.
*
* @param in Stream to read data from
* @return The contents of the stream as a byte array
*/
public static byte[] readStreamIntoByteArray ( InputStream in ) {
return readStreamIntoByteArray(in, 4096);
}
/**
* Reads all data from the given stream into a byte array using the requested read buffer size.
*
* @param in Stream to read data from
* @param readBufferSize Number of bytes to read in at one time
* @return The contents of the stream as a byte array
*/
public static byte[] readStreamIntoByteArray ( InputStream in, int readBufferSize ) {
if ( in == null ) {
throw new ReviewedStingException("Input stream was null");
}
else if ( readBufferSize <= 0 ) {
throw new ReviewedStingException("Read buffer size must be > 0");
}
// Use a fixed-size buffer for each read, but a dynamically-growing buffer
// to hold the accumulated contents of the file/stream:
byte[] readBuffer = new byte[readBufferSize];
ByteArrayOutputStream fileBuffer = new ByteArrayOutputStream(readBufferSize * 4);
try {
try {
int currentBytesRead;
while ( (currentBytesRead = in.read(readBuffer, 0, readBuffer.length)) >= 0 ) {
fileBuffer.write(readBuffer, 0, currentBytesRead);
}
}
finally {
in.close();
}
}
catch ( IOException e ) {
throw new UserException.CouldNotReadInputFile("I/O error reading from input stream", e);
}
return fileBuffer.toByteArray();
}
/**
* Writes the given array of bytes to a file
*
* @param bytes Data to write
* @param destination File to write the data to
*/
public static void writeByteArrayToFile ( byte[] bytes, File destination ) {
if ( destination == null ) {
throw new ReviewedStingException("Destination file was null");
}
try {
writeByteArrayToStream(bytes, new FileOutputStream(destination));
}
catch ( FileNotFoundException e ) {
throw new UserException.CouldNotCreateOutputFile(destination, e);
}
}
/**
* Writes the given array of bytes to a stream
*
* @param bytes Data to write
* @param out Stream to write the data to
*/
public static void writeByteArrayToStream ( byte[] bytes, OutputStream out ) {
if ( bytes == null || out == null ) {
throw new ReviewedStingException("Data to write or output stream was null");
}
try {
try {
out.write(bytes);
}
finally {
out.close();
}
}
catch ( IOException e ) {
throw new UserException.CouldNotCreateOutputFile("I/O error writing to output stream", e);
}
}
/**
* Determines the uncompressed size of a GZIP file. Uses the GZIP ISIZE field in the last
* 4 bytes of the file to get this information.
*
* @param gzipFile GZIP-format file whose uncompressed size to determine
* @return The uncompressed size (in bytes) of the GZIP file
*/
public static int getGZIPFileUncompressedSize ( File gzipFile ) {
if ( gzipFile == null ) {
throw new ReviewedStingException("GZIP file to examine was null");
}
try {
// The GZIP ISIZE field holds the uncompressed size of the compressed data.
// It occupies the last 4 bytes of any GZIP file:
RandomAccessFile in = new RandomAccessFile(gzipFile, "r");
in.seek(gzipFile.length() - 4);
byte[] sizeBytes = new byte[4];
in.read(sizeBytes, 0, 4);
ByteBuffer byteBuf = ByteBuffer.wrap(sizeBytes);
byteBuf.order(ByteOrder.LITTLE_ENDIAN); // The GZIP spec mandates little-endian byte order
int uncompressedSize = byteBuf.getInt();
// If the size read in is negative, we've overflowed our signed integer:
if ( uncompressedSize < 0 ) {
throw new UserException.CouldNotReadInputFile(String.format("Cannot accurately determine the uncompressed size of file %s " +
"because it's either larger than %d bytes or the GZIP ISIZE field is corrupt",
gzipFile.getAbsolutePath(), Integer.MAX_VALUE));
}
return uncompressedSize;
}
catch ( IOException e ) {
throw new UserException.CouldNotReadInputFile(gzipFile, e);
}
}
}

View File

@ -6,6 +6,7 @@ import org.apache.log4j.Logger;
import org.apache.log4j.PatternLayout;
import org.apache.log4j.spi.LoggingEvent;
import org.broadinstitute.sting.commandline.CommandLineUtils;
import org.broadinstitute.sting.utils.crypt.CryptUtils;
import org.broadinstitute.sting.utils.exceptions.ReviewedStingException;
import org.broadinstitute.sting.utils.io.IOUtils;
@ -87,6 +88,9 @@ public abstract class BaseTest {
public static final File testDirFile = new File("public/testdata/");
public static final String testDir = testDirFile.getAbsolutePath() + "/";
public static final String keysDataLocation = validationDataLocation + "keys/";
public static final String gatkKeyFile = CryptUtils.GATK_USER_KEY_DIRECTORY + "gsamembers_broadinstitute.org.key";
/** before the class starts up */
static {
// setup a basic log configuration

View File

@ -30,6 +30,7 @@ import org.broad.tribble.FeatureCodec;
import org.broad.tribble.Tribble;
import org.broad.tribble.index.Index;
import org.broad.tribble.index.IndexFactory;
import org.broadinstitute.sting.gatk.phonehome.GATKRunReport;
import org.broadinstitute.sting.utils.codecs.vcf.VCFCodec;
import org.broadinstitute.sting.gatk.CommandLineExecutable;
import org.broadinstitute.sting.gatk.CommandLineGATK;
@ -45,7 +46,7 @@ import java.io.File;
import java.util.*;
public class WalkerTest extends BaseTest {
private static final boolean ENABLE_REPORTING = false;
private static final boolean ENABLE_PHONE_HOME_FOR_TESTS = false;
@BeforeMethod
public void initializeRandomGenerator() {
@ -121,11 +122,19 @@ public class WalkerTest extends BaseTest {
}
public class WalkerTestSpec {
// Arguments implicitly included in all Walker command lines, unless explicitly
// disabled using the disableImplicitArgs() method below.
final String IMPLICIT_ARGS = ENABLE_PHONE_HOME_FOR_TESTS ?
String.format("-et %s", GATKRunReport.PhoneHomeOption.STANDARD) :
String.format("-et %s -K %s", GATKRunReport.PhoneHomeOption.NO_ET, gatkKeyFile);
String args = "";
int nOutputFiles = -1;
List<String> md5s = null;
List<String> exts = null;
Class expectedException = null;
boolean includeImplicitArgs = true;
// the default output path for the integration test
private File outputFileLocation = null;
@ -159,6 +168,10 @@ public class WalkerTest extends BaseTest {
this.expectedException = expectedException;
}
public String getArgsWithImplicitArgs() {
return args + (includeImplicitArgs ? " " + IMPLICIT_ARGS : "");
}
public void setOutputFileLocation(File outputFileLocation) {
this.outputFileLocation = outputFileLocation;
}
@ -180,6 +193,9 @@ public class WalkerTest extends BaseTest {
auxillaryFiles.put(expectededMD5sum, outputfile);
}
public void disableImplicitArgs() {
includeImplicitArgs = false;
}
}
protected boolean parameterize() {
@ -213,7 +229,7 @@ public class WalkerTest extends BaseTest {
tmpFiles.add(fl);
}
final String args = String.format(spec.args, tmpFiles.toArray());
final String args = String.format(spec.getArgsWithImplicitArgs(), tmpFiles.toArray());
System.out.println(Utils.dupString('-', 80));
if ( spec.expectsException() ) {
@ -277,13 +293,10 @@ public class WalkerTest extends BaseTest {
* @param args the argument list
* @param expectedException the expected exception or null
*/
public static void executeTest(String name, String args, Class expectedException) {
private void executeTest(String name, String args, Class expectedException) {
CommandLineGATK instance = new CommandLineGATK();
String[] command = Utils.escapeExpressions(args);
// add the logging level to each of the integration test commands
command = Utils.appendArray(command, "-et", ENABLE_REPORTING ? "STANDARD" : "NO_ET");
// run the executable
boolean gotAnException = false;
try {

View File

@ -0,0 +1,177 @@
/*
* Copyright (c) 2012, The Broad Institute
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
package org.broadinstitute.sting.utils.crypt;
import org.broadinstitute.sting.BaseTest;
import org.broadinstitute.sting.utils.exceptions.ReviewedStingException;
import org.broadinstitute.sting.utils.exceptions.UserException;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import org.testng.Assert;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.Key;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Arrays;
public class CryptUtilsUnitTest extends BaseTest {
@Test
public void testGenerateValidKeyPairWithDefaultSettings() {
KeyPair keyPair = CryptUtils.generateKeyPair();
Assert.assertTrue(CryptUtils.keysDecryptEachOther(keyPair.getPrivate(), keyPair.getPublic()));
}
@DataProvider( name = "InvalidKeyPairSettings" )
public Object[][] invalidKeyPairSettingsDataProvider() {
return new Object[][] {
{ -1, CryptUtils.DEFAULT_ENCRYPTION_ALGORITHM, CryptUtils.DEFAULT_RANDOM_NUMBER_GENERATION_ALGORITHM},
{ CryptUtils.DEFAULT_KEY_LENGTH, "Made-up algorithm", CryptUtils.DEFAULT_RANDOM_NUMBER_GENERATION_ALGORITHM},
{ CryptUtils.DEFAULT_KEY_LENGTH, CryptUtils.DEFAULT_ENCRYPTION_ALGORITHM, "Made-up algorithm"}
};
}
@Test( dataProvider = "InvalidKeyPairSettings", expectedExceptions = ReviewedStingException.class )
public void testGenerateKeyPairWithInvalidSettings( int keyLength, String encryptionAlgorithm, String randomNumberGenerationAlgorithm ) {
KeyPair keyPair = CryptUtils.generateKeyPair(keyLength, encryptionAlgorithm, randomNumberGenerationAlgorithm);
}
@Test
public void testGATKMasterKeyPairMutualDecryption() {
Assert.assertTrue(CryptUtils.keysDecryptEachOther(CryptUtils.loadGATKMasterPrivateKey(), CryptUtils.loadGATKMasterPublicKey()));
}
@Test
public void testGATKMasterPrivateKeyWithDistributedPublicKeyMutualDecryption() {
Assert.assertTrue(CryptUtils.keysDecryptEachOther(CryptUtils.loadGATKMasterPrivateKey(), CryptUtils.loadGATKDistributedPublicKey()));
}
@Test
public void testKeyPairWriteThenRead() {
KeyPair keyPair = CryptUtils.generateKeyPair();
File privateKeyFile = createTempFile("testKeyPairWriteThenRead_private", "key");
File publicKeyFile = createTempFile("testKeyPairWriteThenRead_public", "key");
CryptUtils.writeKeyPair(keyPair, privateKeyFile, publicKeyFile);
assertKeysAreEqual(keyPair.getPrivate(), CryptUtils.readPrivateKey(privateKeyFile));
assertKeysAreEqual(keyPair.getPublic(), CryptUtils.readPublicKey(publicKeyFile));
}
@Test
public void testPublicKeyWriteThenReadFromFile() {
File keyFile = createTempFile("testPublicKeyWriteThenReadFromFile", "key");
PublicKey publicKey = CryptUtils.generateKeyPair().getPublic();
CryptUtils.writeKey(publicKey, keyFile);
assertKeysAreEqual(publicKey, CryptUtils.readPublicKey(keyFile));
}
@Test
public void testPublicKeyWriteThenReadFromStream() throws IOException {
File keyFile = createTempFile("testPublicKeyWriteThenReadFromStream", "key");
PublicKey publicKey = CryptUtils.generateKeyPair().getPublic();
CryptUtils.writeKey(publicKey, keyFile);
assertKeysAreEqual(publicKey, CryptUtils.readPublicKey(new FileInputStream(keyFile)));
}
@Test
public void testPrivateKeyWriteThenReadFromFile() {
File keyFile = createTempFile("testPrivateKeyWriteThenReadFromFile", "key");
PrivateKey privateKey = CryptUtils.generateKeyPair().getPrivate();
CryptUtils.writeKey(privateKey, keyFile);
assertKeysAreEqual(privateKey, CryptUtils.readPrivateKey(keyFile));
}
@Test
public void testPrivateKeyWriteThenReadFromStream() throws IOException {
File keyFile = createTempFile("testPrivateKeyWriteThenReadFromStream", "key");
PrivateKey privateKey = CryptUtils.generateKeyPair().getPrivate();
CryptUtils.writeKey(privateKey, keyFile);
assertKeysAreEqual(privateKey, CryptUtils.readPrivateKey(new FileInputStream(keyFile)));
}
@Test( expectedExceptions = UserException.CouldNotReadInputFile.class )
public void testReadNonExistentPublicKey() {
File nonExistentFile = new File("jdshgkdfhg.key");
Assert.assertFalse(nonExistentFile.exists());
CryptUtils.readPublicKey(nonExistentFile);
}
@Test( expectedExceptions = UserException.CouldNotReadInputFile.class )
public void testReadNonExistentPrivateKey() {
File nonExistentFile = new File("jdshgkdfhg.key");
Assert.assertFalse(nonExistentFile.exists());
CryptUtils.readPrivateKey(nonExistentFile);
}
@Test
public void testDecodePublicKey() {
PublicKey originalKey = CryptUtils.generateKeyPair().getPublic();
PublicKey decodedKey = CryptUtils.decodePublicKey(originalKey.getEncoded(), CryptUtils.DEFAULT_ENCRYPTION_ALGORITHM);
assertKeysAreEqual(originalKey, decodedKey);
}
@Test
public void testDecodePrivateKey() {
PrivateKey originalKey = CryptUtils.generateKeyPair().getPrivate();
PrivateKey decodedKey = CryptUtils.decodePrivateKey(originalKey.getEncoded(), CryptUtils.DEFAULT_ENCRYPTION_ALGORITHM);
assertKeysAreEqual(originalKey, decodedKey);
}
@Test
public void testLoadGATKMasterPrivateKey() {
PrivateKey gatkMasterPrivateKey = CryptUtils.loadGATKMasterPrivateKey();
}
@Test
public void testLoadGATKMasterPublicKey() {
PublicKey gatkMasterPublicKey = CryptUtils.loadGATKMasterPublicKey();
}
@Test
public void testLoadGATKDistributedPublicKey() {
PublicKey gatkDistributedPublicKey = CryptUtils.loadGATKDistributedPublicKey();
}
private void assertKeysAreEqual( Key originalKey, Key keyFromDisk ) {
Assert.assertTrue(Arrays.equals(originalKey.getEncoded(), keyFromDisk.getEncoded()));
Assert.assertEquals(originalKey.getAlgorithm(), keyFromDisk.getAlgorithm());
Assert.assertEquals(originalKey.getFormat(), keyFromDisk.getFormat());
}
}

View File

@ -0,0 +1,156 @@
/*
* Copyright (c) 2012, The Broad Institute
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
package org.broadinstitute.sting.utils.crypt;
import org.broadinstitute.sting.WalkerTest;
import org.broadinstitute.sting.gatk.phonehome.GATKRunReport;
import org.broadinstitute.sting.utils.exceptions.UserException;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import java.util.Arrays;
public class GATKKeyIntegrationTest extends WalkerTest {
public static final String BASE_COMMAND = String.format("-T PrintReads -R %s -I %s -o %%s",
testDir + "exampleFASTA.fasta",
testDir + "exampleBAM.bam");
public static final String MD5_UPON_SUCCESSFUL_RUN = "b9dc5bf6753ca2819e70b056eaf61258";
private void runGATKKeyTest ( String testName, String etArg, String keyArg, Class expectedException, String md5 ) {
String command = BASE_COMMAND + String.format(" %s %s", etArg, keyArg);
WalkerTestSpec spec = expectedException != null ?
new WalkerTestSpec(command, 1, expectedException) :
new WalkerTestSpec(command, 1, Arrays.asList(md5));
spec.disableImplicitArgs(); // Turn off automatic inclusion of -et/-K args by WalkerTest
executeTest(testName, spec);
}
@Test
public void testValidKeyNoET() {
runGATKKeyTest("testValidKeyNoET",
"-et " + GATKRunReport.PhoneHomeOption.NO_ET,
"-K " + keysDataLocation + "valid.key",
null,
MD5_UPON_SUCCESSFUL_RUN);
}
@Test
public void testValidKeyETStdout() {
runGATKKeyTest("testValidKeyETStdout",
"-et " + GATKRunReport.PhoneHomeOption.STDOUT,
"-K " + keysDataLocation + "valid.key",
null,
MD5_UPON_SUCCESSFUL_RUN);
}
@Test
public void testValidKeyETStandard() {
runGATKKeyTest("testValidKeyETStandard",
"",
"-K " + keysDataLocation + "valid.key",
null,
MD5_UPON_SUCCESSFUL_RUN);
}
@Test
public void testNoKeyNoET() {
runGATKKeyTest("testNoKeyNoET",
"-et " + GATKRunReport.PhoneHomeOption.NO_ET,
"",
UserException.class,
null);
}
@Test
public void testNoKeyETStdout() {
runGATKKeyTest("testNoKeyETStdout",
"-et " + GATKRunReport.PhoneHomeOption.STDOUT,
"",
UserException.class,
null);
}
@Test
public void testNoKeyETStandard() {
runGATKKeyTest("testNoKeyETStandard",
"",
"",
null,
MD5_UPON_SUCCESSFUL_RUN);
}
@Test
public void testRevokedKey() {
runGATKKeyTest("testRevokedKey",
"-et " + GATKRunReport.PhoneHomeOption.NO_ET,
"-K " + keysDataLocation + "revoked.key",
UserException.KeySignatureVerificationException.class,
null);
}
@DataProvider(name = "CorruptKeyTestData")
public Object[][] corruptKeyDataProvider() {
return new Object[][] {
{ "corrupt_empty.key", UserException.UnreadableKeyException.class },
{ "corrupt_single_byte_file.key", UserException.UnreadableKeyException.class },
{ "corrupt_random_contents.key", UserException.UnreadableKeyException.class },
{ "corrupt_single_byte_deletion.key", UserException.UnreadableKeyException.class },
{ "corrupt_single_byte_insertion.key", UserException.UnreadableKeyException.class },
{ "corrupt_single_byte_change.key", UserException.UnreadableKeyException.class },
{ "corrupt_multi_byte_deletion.key", UserException.UnreadableKeyException.class },
{ "corrupt_multi_byte_insertion.key", UserException.UnreadableKeyException.class },
{ "corrupt_multi_byte_change.key", UserException.UnreadableKeyException.class },
{ "corrupt_bad_isize_field.key", UserException.UnreadableKeyException.class },
{ "corrupt_bad_crc.key", UserException.UnreadableKeyException.class },
{ "corrupt_no_email_address.key", UserException.UnreadableKeyException.class },
{ "corrupt_no_sectional_delimiter.key", UserException.KeySignatureVerificationException.class },
{ "corrupt_no_signature.key", UserException.UnreadableKeyException.class },
{ "corrupt_bad_signature.key", UserException.KeySignatureVerificationException.class },
{ "corrupt_non_gzipped_valid_key.key", UserException.UnreadableKeyException.class }
};
}
@Test(dataProvider = "CorruptKeyTestData")
public void testCorruptKey ( String corruptKeyName, Class expectedException ) {
runGATKKeyTest(String.format("testCorruptKey (%s)", corruptKeyName),
"-et " + GATKRunReport.PhoneHomeOption.NO_ET,
"-K " + keysDataLocation + corruptKeyName,
expectedException,
null);
}
@Test
public void testCorruptButNonRequiredKey() {
runGATKKeyTest("testCorruptButNonRequiredKey",
"",
"-K " + keysDataLocation + "corrupt_random_contents.key",
null,
MD5_UPON_SUCCESSFUL_RUN);
}
}

View File

@ -0,0 +1,113 @@
/*
* Copyright (c) 2012, The Broad Institute
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
package org.broadinstitute.sting.utils.crypt;
import org.broadinstitute.sting.BaseTest;
import org.broadinstitute.sting.utils.exceptions.ReviewedStingException;
import org.broadinstitute.sting.utils.exceptions.UserException;
import org.testng.annotations.Test;
import org.testng.Assert;
import java.io.File;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
public class GATKKeyUnitTest extends BaseTest {
@Test
public void testCreateGATKKeyUsingMasterKeyPair() {
PrivateKey masterPrivateKey = CryptUtils.loadGATKMasterPrivateKey();
PublicKey masterPublicKey = CryptUtils.loadGATKMasterPublicKey();
// We should be able to create a valid GATKKey using our master key pair:
GATKKey key = new GATKKey(masterPrivateKey, masterPublicKey, "foo@bar.com");
Assert.assertTrue(key.isValid());
}
@Test
public void testCreateGATKKeyUsingMasterPrivateKeyAndDistributedPublicKey() {
PrivateKey masterPrivateKey = CryptUtils.loadGATKMasterPrivateKey();
PublicKey distributedPublicKey = CryptUtils.loadGATKDistributedPublicKey();
// We should also be able to create a valid GATKKey using our master private
// key and the public key we distribute with the GATK:
GATKKey key = new GATKKey(masterPrivateKey, distributedPublicKey, "foo@bar.com");
Assert.assertTrue(key.isValid());
}
@Test( expectedExceptions = ReviewedStingException.class )
public void testKeyPairMismatch() {
KeyPair firstKeyPair = CryptUtils.generateKeyPair();
KeyPair secondKeyPair = CryptUtils.generateKeyPair();
// Attempting to create a GATK Key with private and public keys that aren't part of the
// same key pair should immediately trigger a validation failure:
GATKKey key = new GATKKey(firstKeyPair.getPrivate(), secondKeyPair.getPublic(), "foo@bar.com");
}
@Test( expectedExceptions = ReviewedStingException.class )
public void testEncryptionAlgorithmMismatch() {
KeyPair keyPair = CryptUtils.generateKeyPair(CryptUtils.DEFAULT_KEY_LENGTH, "DSA", CryptUtils.DEFAULT_RANDOM_NUMBER_GENERATION_ALGORITHM);
// Attempting to use a DSA private key to create an RSA signature should throw an error:
GATKKey key = new GATKKey(keyPair.getPrivate(), keyPair.getPublic(), "foo@bar.com", "SHA1withRSA");
}
@Test( expectedExceptions = UserException.class )
public void testInvalidEmailAddress() {
String emailAddressWithNulByte = new String(new byte[] { 0 });
KeyPair keyPair = CryptUtils.generateKeyPair();
// Email addresses cannot contain the NUL byte, since it's used as a sectional delimiter in the key file:
GATKKey key = new GATKKey(CryptUtils.loadGATKMasterPrivateKey(), CryptUtils.loadGATKDistributedPublicKey(),
emailAddressWithNulByte);
}
@Test
public void testCreateGATKKeyFromValidKeyFile() {
GATKKey key = new GATKKey(CryptUtils.loadGATKDistributedPublicKey(), new File(keysDataLocation + "valid.key"));
Assert.assertTrue(key.isValid());
}
@Test( expectedExceptions = UserException.UnreadableKeyException.class )
public void testCreateGATKKeyFromCorruptKeyFile() {
GATKKey key = new GATKKey(CryptUtils.loadGATKDistributedPublicKey(), new File(keysDataLocation + "corrupt_random_contents.key"));
}
@Test
public void testCreateGATKKeyFromRevokedKeyFile() {
GATKKey key = new GATKKey(CryptUtils.loadGATKDistributedPublicKey(), new File(keysDataLocation + "revoked.key"));
Assert.assertFalse(key.isValid());
}
@Test( expectedExceptions = UserException.CouldNotReadInputFile.class )
public void testCreateGATKKeyFromNonExistentFile() {
File nonExistentFile = new File("ghfdkgsdhg.key");
Assert.assertFalse(nonExistentFile.exists());
GATKKey key = new GATKKey(CryptUtils.loadGATKDistributedPublicKey(), nonExistentFile);
}
}

View File

@ -27,12 +27,18 @@ package org.broadinstitute.sting.utils.io;
import org.apache.commons.io.FileUtils;
import org.broadinstitute.sting.BaseTest;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import org.broadinstitute.sting.gatk.GenomeAnalysisEngine;
import org.broadinstitute.sting.utils.exceptions.ReviewedStingException;
import org.broadinstitute.sting.utils.exceptions.UserException;
import org.testng.Assert;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
public class IOUtilsUnitTest extends BaseTest {
@ -230,4 +236,90 @@ public class IOUtilsUnitTest extends BaseTest {
Assert.assertFalse(IOUtils.isSpecialFile(new File("/home/user/my.file")));
Assert.assertFalse(IOUtils.isSpecialFile(new File("/devfake/null")));
}
@DataProvider( name = "ByteArrayIOTestData")
public Object[][] byteArrayIOTestDataProvider() {
return new Object[][] {
// file size, read buffer size
{ 0, 4096 },
{ 1, 4096 },
{ 2000, 4096 },
{ 4095, 4096 },
{ 4096, 4096 },
{ 4097, 4096 },
{ 6000, 4096 },
{ 8191, 4096 },
{ 8192, 4096 },
{ 8193, 4096 },
{ 10000, 4096 }
};
}
@Test( dataProvider = "ByteArrayIOTestData" )
public void testWriteThenReadFileIntoByteArray ( int fileSize, int readBufferSize ) throws Exception {
File tempFile = createTempFile(String.format("testWriteThenReadFileIntoByteArray_%d_%d", fileSize, readBufferSize), "tmp");
byte[] dataWritten = getDeterministicRandomData(fileSize);
IOUtils.writeByteArrayToFile(dataWritten, tempFile);
byte[] dataRead = IOUtils.readFileIntoByteArray(tempFile, readBufferSize);
Assert.assertEquals(dataRead.length, dataWritten.length);
Assert.assertTrue(Arrays.equals(dataRead, dataWritten));
}
@Test( dataProvider = "ByteArrayIOTestData" )
public void testWriteThenReadStreamIntoByteArray ( int fileSize, int readBufferSize ) throws Exception {
File tempFile = createTempFile(String.format("testWriteThenReadStreamIntoByteArray_%d_%d", fileSize, readBufferSize), "tmp");
byte[] dataWritten = getDeterministicRandomData(fileSize);
IOUtils.writeByteArrayToStream(dataWritten, new FileOutputStream(tempFile));
byte[] dataRead = IOUtils.readStreamIntoByteArray(new FileInputStream(tempFile), readBufferSize);
Assert.assertEquals(dataRead.length, dataWritten.length);
Assert.assertTrue(Arrays.equals(dataRead, dataWritten));
}
@Test( expectedExceptions = UserException.CouldNotReadInputFile.class )
public void testReadNonExistentFileIntoByteArray() {
File nonExistentFile = new File("djfhsdkjghdfk");
Assert.assertFalse(nonExistentFile.exists());
IOUtils.readFileIntoByteArray(nonExistentFile);
}
@Test( expectedExceptions = ReviewedStingException.class )
public void testReadNullStreamIntoByteArray() {
IOUtils.readStreamIntoByteArray(null);
}
@Test( expectedExceptions = ReviewedStingException.class )
public void testReadStreamIntoByteArrayInvalidBufferSize() throws Exception {
IOUtils.readStreamIntoByteArray(new FileInputStream(createTempFile("testReadStreamIntoByteArrayInvalidBufferSize", "tmp")),
-1);
}
@Test( expectedExceptions = UserException.CouldNotCreateOutputFile.class )
public void testWriteByteArrayToUncreatableFile() {
IOUtils.writeByteArrayToFile(new byte[]{0}, new File("/dev/foo/bar"));
}
@Test( expectedExceptions = ReviewedStingException.class )
public void testWriteNullByteArrayToFile() {
IOUtils.writeByteArrayToFile(null, createTempFile("testWriteNullByteArrayToFile", "tmp"));
}
@Test( expectedExceptions = ReviewedStingException.class )
public void testWriteByteArrayToNullStream() {
IOUtils.writeByteArrayToStream(new byte[]{0}, null);
}
private byte[] getDeterministicRandomData ( int size ) {
GenomeAnalysisEngine.resetRandomGenerator();
Random rand = GenomeAnalysisEngine.getRandomGenerator();
byte[] randomData = new byte[size];
rand.nextBytes(randomData);
return randomData;
}
}

Binary file not shown.

View File

@ -36,6 +36,8 @@
<dir name="org/broadinstitute/sting/utils/R" includes="**/*.tar.gz" />
<!-- All R scripts in org.broadinstitute.sting -->
<dir name="org/broadinstitute/sting" includes="**/*.R" />
<!-- The GATK public key -->
<file path="GATK_public.key" />
</dependencies>
</executable>
<resources>