diff --git a/.gitignore b/.gitignore
new file mode 100644
index 000000000..d32efdc17
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,124 @@
+# compiled python for systems tests
+*.pyc
+
+# NetBeans specific #
+private/
+build/
+dist/
+.nb-gradle-properties
+
+# Eclipse specific #
+.classpath
+.project
+.settings/
+.checkstyle
+
+# IDEA specific #
+*.iml
+.idea
+cmake-build-debug/
+
+# Gradle specific #
+.gradle
+
+# Class Files #
+*.class
+classes/
+
+# Package Files #
+*.war
+*.ear
+*.rpm
+*.deb
+
+# Specific RPMs used for testing
+!HIRS_Utils/src/test/resources/repository/vim-common-7.2.411-1.8.el6.x86_64.rpm
+!HIRS_Utils/src/test/resources/repository/kernel-2.6.32-642.6.1.el6.x86_64.rpm
+!HIRS_Utils/src/test/resources/testrepo/*
+!Systems_Tests/resources/**
+
+# DEB Sources #
+DEB_SOURCES/
+
+# RPM Files #
+BUILD/
+BUILDROOT/
+SOURCES/
+SPECS/
+RPM/
+SRPM/
+PLUGIN_SOURCE/
+
+# C++ Files #
+*.o
+
+# Vagrant Files #
+.vagrant/
+.vagrantfile.swp
+vagrant/tmp/
+
+# tpm_module #
+tpm_module/tpm_module
+main.o
+main.d
+
+# Misc Files #
+*~
+bin/
+!package/extras/*/bin
+*/test-output/
+
+# MAC OSX Finder Files #
+.DS_Store
+
+# Log Files #
+*.log
+
+/.nb-gradle/
+
+
+# rejected diff applications
+*.rej
+
+# cmake artifacts from manual build
+cmake_install.cmake
+HIRS_ProvisionerTPM2/*.cmake
+HIRS_ProvisionerTPM2/HIRS_ProvisionerTPM2.cbp
+HIRS_ProvisionerTPM2/CMakeCache.txt
+Makefile
+CMakeFiles/
+HIRS_ProvisionerTPM2/DartConfiguration.tcl
+HIRS_ProvisionerTPM2/lib/cpplint-download/
+HIRS_ProvisionerTPM2/lib/cpplint/
+HIRS_ProvisionerTPM2/lib/cpr-build/
+HIRS_ProvisionerTPM2/lib/cpr-download/
+HIRS_ProvisionerTPM2/lib/cpr-src/
+HIRS_ProvisionerTPM2/lib/googletest-build/
+HIRS_ProvisionerTPM2/lib/googletest-download/
+HIRS_ProvisionerTPM2/lib/googletest-src/
+HIRS_ProvisionerTPM2/lib/*.a
+HIRS_ProvisionerTPM2/lib/*.so
+HIRS_ProvisionerTPM2/install_manifest.txt
+HIRS_ProvisionerTPM2/src/libTPM2_PROVISIONER_LIBRARY.a
+HIRS_ProvisionerTPM2/test/CTestTestfile.cmake
+
+# C++ Doxygen Documentation
+HIRS_ProvisionerTPM2/docs/html/
+HIRS_ProvisionerTPM2/docs/latex/
+HIRS_ProvisionerTPM2/CMakeDoxyfile.in
+
+# C++ Style Checker
+HIRS_ProvisionerTPM2/lint
+
+/*/out
+
+HIRS_ProvisionerTPM2/cmake-build-debug
+
+# autogenerated protobuf files
+*.pb.cc
+*.pb.h
+HIRS_AttestationCA/src/main/java/hirs/attestationca/configuration/provisionerTpm2/ProvisionerTpm2.java
+
+# these files are copied over by ProvisionerTPM2 CMake build
+HIRS_ProvisionerTPM2/config/logging.properties
+HIRS_ProvisionerTPM2/scripts/tpm_aca_provision
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 000000000..f60e40106
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,31 @@
+# NOTE: if you are editing this, try using the lint tool to check your work before pushing: https://forge.outer.jhuapl.edu/ci/lint
+
+before_script:
+ - echo "running CI jobs for HIRS"
+
+stages:
+ - build
+
+gradle_build:
+ stage: build
+ script: ./gradlew build
+ artifacts:
+ when: on_failure
+ untracked: true
+ expire_in: 3 days
+
+rpm_build_centos6:
+ stage: build
+ script: ONLY_BUILD_EL6_RPMS=true ./package/package.centos.sh
+ artifacts:
+ paths:
+ - package/rpm/RPMS/
+ expire_in: 3 days
+
+rpm_build_centos7:
+ stage: build
+ script: ONLY_BUILD_EL7_RPMS=true ./package/package.centos.sh
+ artifacts:
+ paths:
+ - package/rpm/RPMS/
+ expire_in: 3 days
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 000000000..92bf03137
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,6 @@
+All contributions to this project will be released as follows:
+
+1. If you are a U.S. government employee, then your changes are exempt from copyright in the U.S. and will be released under the [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) [Universal license](https://creativecommons.org/publicdomain/zero/1.0/legalcode) worldwide.
+1. If you are a not a U.S. government employee, then your changes will be released under the [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) [Universal license](https://creativecommons.org/publicdomain/zero/1.0/legalcode) in the U.S. and worldwide.
+
+By submitting a pull request, you are agreeing to comply with this waiver of copyright interest.
\ No newline at end of file
diff --git a/DISCLAIMER.md b/DISCLAIMER.md
new file mode 100644
index 000000000..3f84d4205
--- /dev/null
+++ b/DISCLAIMER.md
@@ -0,0 +1,9 @@
+## Disclaimer of Warranty
+This Work is provided "as is." Any express or implied warranties, including but not limited to, the implied warranties of merchantability and fitness for a particular purpose are disclaimed. In no event shall the United States Government be liable for any direct, indirect, incidental, special, exemplary or consequential damages (including, but not limited to, procurement of substitute goods or services, loss of use, data or profits, or business interruption) however caused and on any theory of liability, whether in contract, strict liability, or tort (including negligence or otherwise) arising in any way out of the use of this Guidance, even if advised of the possibility of such damage.
+
+The User of this Work agrees to hold harmless and indemnify the United States Government, its agents and employees from every claim or liability (whether in tort or in contract), including attorneys' fees, court costs, and expenses, arising in direct consequence of Recipient's use of the item, including, but not limited to, claims or liabilities made for injury to or death of personnel of User or third parties, damage to or destruction of property of User or third parties, and infringement or other violations of intellectual property or technical data rights.
+
+Nothing in this Work is intended to constitute an endorsement, explicit or implied, by the United States Government of any particular manufacturer's product or service.
+
+## Disclaimer of Endorsement
+Reference herein to any specific commercial product, process, or service by trade name, trademark, manufacturer, or otherwise, in this Work does not constitute an endorsement, recommendation, or favoring by the United States Government and shall not be used for advertising or product endorsement purposes.
\ No newline at end of file
diff --git a/HIRS_AttestationCA/build.gradle b/HIRS_AttestationCA/build.gradle
new file mode 100644
index 000000000..fff6e9eb3
--- /dev/null
+++ b/HIRS_AttestationCA/build.gradle
@@ -0,0 +1,83 @@
+apply plugin: 'war'
+apply plugin: 'checkstyle'
+
+evaluationDependsOn(':HIRS_Utils')
+
+sourceCompatibility = 1.8
+
+dependencies {
+ compile project(':TPM_Utils')
+ compile project(':HIRS_Structs')
+ compile project(':HIRS_Utils')
+
+ compile libs.bouncy_castle
+ compile libs.commons_codec
+ compile libs.commons_lang
+ compile libs.spring_webmvc
+ compile libs.log4j2
+ compile libs.log4j2_web
+ compile libs.protobuf_java
+
+ providedCompile libs.servlet_api
+
+ testCompile project(':HIRS_Utils').sourceSets.test.output
+ testCompile project(':HIRS_Utils').sourceSets.test.resources
+
+ testCompile libs.commons_lang
+ testCompile libs.spring_test
+ testCompile libs.mockito
+ testCompile libs.testng
+ testCompile libs.hsqldb
+}
+
+task generateProtoBuf(type:Exec) {
+ workingDir 'config'
+
+ commandLine './genJavaProtoBuf.sh'
+}
+
+compileJava.dependsOn generateProtoBuf
+
+ext.configDir = new File(projectDir, 'config')
+ext.checkstyleConfigDir = "$configDir/checkstyle"
+checkstyle {
+ toolVersion = '5.7'
+ configFile = checkstyleConfigFile
+ configProperties.put('basedir', checkstyleConfigDir)
+ ignoreFailures = false
+ showViolations = true
+}
+
+war {
+ archiveName = 'HIRS_AttestationCA.war'
+}
+
+publishing {
+ publications {
+ maven(MavenPublication) {
+ artifactId 'hirs-attestationca'
+ artifact jar
+ pom.withXml {
+ def dependenciesNode = asNode().appendNode('dependencies')
+
+ configurations.runtime.allDependencies.each {
+ if (it.group != null && it.name != null) {
+ def dependencyNode = dependenciesNode.appendNode('dependency')
+ dependencyNode.appendNode('groupId', it.group)
+ dependencyNode.appendNode('artifactId', it.name)
+ dependencyNode.appendNode('version', it.version)
+
+ if (it.excludeRules.size() > 0) {
+ def exclusionsNode = dependencyNode.appendNode('exclusions')
+ it.excludeRules.each { rule ->
+ def exclusionNode = exclusionsNode.appendNode('exclusion')
+ exclusionNode.appendNode('groupId', rule.group)
+ exclusionNode.appendNode('artifactId', rule.module)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/HIRS_AttestationCA/config/checkstyle/suppressions.xml b/HIRS_AttestationCA/config/checkstyle/suppressions.xml
new file mode 100644
index 000000000..977db1998
--- /dev/null
+++ b/HIRS_AttestationCA/config/checkstyle/suppressions.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/HIRS_AttestationCA/config/genJavaProtoBuf.sh b/HIRS_AttestationCA/config/genJavaProtoBuf.sh
new file mode 100755
index 000000000..536912177
--- /dev/null
+++ b/HIRS_AttestationCA/config/genJavaProtoBuf.sh
@@ -0,0 +1,16 @@
+#!/bin/bash
+
+# Script to generate protobuf Java code. Called by gradle to compile the
+# protobuf spec file to Java source. Generates the file
+# hirs/attestationca/configuration/provisionerTpm2/ProvisionerTpm2.java.
+
+dir=$(pwd)
+# Relative paths are different when building locally versus on CI
+if [[ "$dir" == *"package"* ]]; then
+ SRC_DIR=$dir/../../../../../../HIRS_ProvisionerTPM2/src
+ DEST_DIR=$dir/../src/main/java
+else
+ SRC_DIR=../../HIRS_ProvisionerTPM2/src
+ DEST_DIR=../src/main/java
+fi
+protoc -I=$SRC_DIR --java_out=$DEST_DIR $SRC_DIR/ProvisionerTpm2.proto
diff --git a/HIRS_AttestationCA/src/main/java/hirs/attestationca/AbstractAttestationCertificateAuthority.java b/HIRS_AttestationCA/src/main/java/hirs/attestationca/AbstractAttestationCertificateAuthority.java
new file mode 100644
index 000000000..f14c706e8
--- /dev/null
+++ b/HIRS_AttestationCA/src/main/java/hirs/attestationca/AbstractAttestationCertificateAuthority.java
@@ -0,0 +1,1458 @@
+package hirs.attestationca;
+
+import com.google.protobuf.ByteString;
+
+import com.google.protobuf.InvalidProtocolBufferException;
+import hirs.data.persist.AppraisalStatus;
+import hirs.data.persist.BIOSComponentInfo;
+import hirs.data.persist.BaseboardComponentInfo;
+import hirs.data.persist.ChassisComponentInfo;
+import hirs.data.persist.Device;
+import hirs.data.persist.DeviceInfoReport;
+import hirs.data.persist.FirmwareInfo;
+import hirs.data.persist.HardDriveComponentInfo;
+import hirs.data.persist.HardwareInfo;
+import hirs.data.persist.MemoryComponentInfo;
+import hirs.data.persist.NICComponentInfo;
+import hirs.data.persist.NetworkInfo;
+import hirs.data.persist.OSInfo;
+import hirs.data.persist.ProcessorComponentInfo;
+import hirs.data.persist.SupplyChainValidationSummary;
+import hirs.data.persist.TPMInfo;
+import hirs.persist.DBManager;
+import hirs.persist.TPM2ProvisionerState;
+import org.apache.commons.codec.binary.Hex;
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.asn1.x509.Extension;
+import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
+import org.bouncycastle.cert.X509CertificateHolder;
+import org.bouncycastle.cert.X509v3CertificateBuilder;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.bouncycastle.operator.ContentSigner;
+import org.bouncycastle.operator.OperatorCreationException;
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
+import org.springframework.util.SerializationUtils;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.Mac;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.OAEPParameterSpec;
+import javax.crypto.spec.PSource;
+import javax.crypto.spec.SecretKeySpec;
+import java.io.IOException;
+import java.math.BigInteger;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.KeyFactory;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.SecureRandom;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.RSAPublicKey;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.MGF1ParameterSpec;
+import java.security.spec.RSAPublicKeySpec;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Set;
+
+import hirs.attestationca.configuration.provisionerTpm2.ProvisionerTpm2;
+import hirs.attestationca.service.SupplyChainValidationService;
+import hirs.data.persist.certificate.Certificate;
+import hirs.data.persist.certificate.EndorsementCredential;
+import hirs.data.persist.certificate.IssuedAttestationCertificate;
+import hirs.data.persist.certificate.PlatformCredential;
+import hirs.data.service.DeviceRegister;
+import hirs.persist.CertificateManager;
+import hirs.persist.DeviceManager;
+import hirs.structs.converters.SimpleStructBuilder;
+import hirs.structs.converters.StructConverter;
+import hirs.structs.elements.aca.IdentityRequestEnvelope;
+import hirs.structs.elements.aca.IdentityResponseEnvelope;
+import hirs.structs.elements.aca.SymmetricAttestation;
+import hirs.structs.elements.tpm.EncryptionScheme;
+import hirs.structs.elements.tpm.IdentityProof;
+import hirs.structs.elements.tpm.IdentityRequest;
+import hirs.structs.elements.tpm.SymmetricKey;
+import hirs.structs.elements.tpm.SymmetricKeyParams;
+
+import hirs.utils.HexUtils;
+
+/**
+ * Provides base implementation of common tasks of an ACA that are required for attestation of an
+ * Identity Request.
+ */
+public abstract class AbstractAttestationCertificateAuthority
+ implements AttestationCertificateAuthority {
+
+ /**
+ * Logger instance for for subclass instances.
+ */
+ protected static final Logger LOG = LogManager.getLogger(AttestationCertificateAuthority.class);
+
+ /**
+ * Defines the well known exponent. https://en.wikipedia.org/wiki/65537_(number)#Applications
+ */
+ private static final BigInteger EXPONENT = new BigInteger("010001",
+ AttestationCertificateAuthority.DEFAULT_IV_SIZE);
+
+ /**
+ * Number of bytes to include in the TPM2.0 nonce.
+ */
+ public static final int NONCE_LENGTH = 20;
+
+ private static final int SEED_LENGTH = 32;
+ private static final int MAX_SECRET_LENGTH = 32;
+ private static final int RSA_MODULUS_LENGTH = 256;
+ private static final int AES_KEY_LENGTH_BYTES = 16;
+ private static final int HMAC_KEY_LENGTH_BYTES = 32;
+ private static final int HMAC_SIZE_LENGTH_BYTES = 2;
+ private static final int TPM2_CREDENTIAL_BLOB_SIZE = 392;
+
+ // Constants used to parse out the ak name from the ak public data. Used in generateAkName
+ private static final String AK_NAME_PREFIX = "000b";
+ private static final String AK_NAME_HASH_PREFIX =
+ "0001000b00050072000000100014000b0800000000000100";
+
+ private static final int MAC_BYTES = 6;
+
+ /**
+ * Container wired ACA private key.
+ */
+ private final PrivateKey privateKey;
+
+ /**
+ * Container wired ACA certificate.
+ */
+ private final X509Certificate acaCertificate;
+
+ /**
+ * Container wired {@link StructConverter} to be used in serialization / deserialization of TPM
+ * data structures.
+ */
+ private final StructConverter structConverter;
+
+ /**
+ * A handle to the service used to validate the supply chain.
+ */
+ private final SupplyChainValidationService supplyChainValidationService;
+
+ /**
+ * Container wired application configuration property identifying the number of days that
+ * certificates issued by this ACA are valid for.
+ */
+ private final Integer validDays;
+
+ private final CertificateManager certificateManager;
+ private final DeviceRegister deviceRegister;
+ private final DeviceManager deviceManager;
+ private final DBManager tpm2ProvisionerStateDBManager;
+
+ /**
+ * Constructor.
+ * @param supplyChainValidationService the supply chain service
+ * @param privateKey the ACA private key
+ * @param acaCertificate the ACA certificate
+ * @param structConverter the struct converter
+ * @param certificateManager the certificate manager
+ * @param deviceRegister the device register
+ * @param validDays the number of days issued certs are valid
+ * @param deviceManager the device manager
+ * @param tpm2ProvisionerStateDBManager the DBManager for persisting provisioner state
+ */
+ @SuppressWarnings("checkstyle:parameternumber")
+ public AbstractAttestationCertificateAuthority(
+ final SupplyChainValidationService supplyChainValidationService,
+ final PrivateKey privateKey, final X509Certificate acaCertificate,
+ final StructConverter structConverter,
+ final CertificateManager certificateManager,
+ final DeviceRegister deviceRegister, final int validDays,
+ final DeviceManager deviceManager,
+ final DBManager tpm2ProvisionerStateDBManager) {
+ this.supplyChainValidationService = supplyChainValidationService;
+ this.privateKey = privateKey;
+ this.acaCertificate = acaCertificate;
+ this.structConverter = structConverter;
+ this.certificateManager = certificateManager;
+ this.deviceRegister = deviceRegister;
+ this.validDays = validDays;
+ this.deviceManager = deviceManager;
+ this.tpm2ProvisionerStateDBManager = tpm2ProvisionerStateDBManager;
+ }
+
+ /**
+ * Basic implementation of the ACA processIdentityRequest method.
+ *
+ * @param identityRequest cannot be null
+ * @return an identity response for the specified request
+ */
+ @Override
+ public byte[] processIdentityRequest(final byte[] identityRequest) {
+ if (ArrayUtils.isEmpty(identityRequest)) {
+ throw new IllegalArgumentException("identityRequest cannot be null or empty");
+ }
+
+ LOG.debug("received request to process identity request");
+
+ // translate the bytes into the challenge
+ IdentityRequestEnvelope challenge =
+ structConverter.convert(identityRequest, IdentityRequestEnvelope.class);
+
+ //
+ byte[] identityProof = unwrapIdentityRequest(challenge.getRequest());
+ // the decrypted symmetric blob should be in the format of an IdentityProof. Use the
+ // struct converter to generate it.
+ IdentityProof proof = structConverter.convert(identityProof, IdentityProof.class);
+
+
+ // convert the credential into an actual key.
+ LOG.debug("assembling public endorsement key");
+ PublicKey ekPublicKey = null;
+
+ // attempt to find an endorsement credential to validate
+ EndorsementCredential endorsementCredential = null;
+
+ // first check the identity request for the endorsement credential
+ byte[] ecBytesFromIdentityRequest = proof.getEndorsementCredential();
+ if (ArrayUtils.isNotEmpty(ecBytesFromIdentityRequest)) {
+ endorsementCredential = CredentialManagementHelper.storeEndorsementCredential(
+ this.certificateManager, ecBytesFromIdentityRequest
+ );
+ try {
+ ekPublicKey = assemblePublicKey(
+ Certificate.getPublicKeyModulus(
+ endorsementCredential.getX509Certificate()).toByteArray());
+ } catch (IOException e) {
+ LOG.error("Could not retrieve the public key modulus from the EK cert");
+ }
+ } else if (ArrayUtils.isNotEmpty(challenge.getEndorsementCredentialModulus())) {
+ LOG.warn("EKC was not in the identity proof from the client. Checking for uploads.");
+ // Check if the EC was uploaded
+ ekPublicKey =
+ assemblePublicKey(new String(challenge.getEndorsementCredentialModulus()));
+ endorsementCredential = getEndorsementCredential(ekPublicKey);
+ } else {
+ LOG.warn("Zero-length endorsement credential received in identity request.");
+ }
+
+ // get platform credential from the identity request
+ HashSet platformCredentials = new HashSet<>();
+ byte[] pcBytesFromIdentityRequest = proof.getPlatformCredential();
+ if (ArrayUtils.isNotEmpty(pcBytesFromIdentityRequest)) {
+ platformCredentials.add(CredentialManagementHelper.storePlatformCredential(
+ this.certificateManager, pcBytesFromIdentityRequest
+ ));
+ } else if (endorsementCredential != null) {
+ // if none in the identity request, look for uploaded platform credentials
+ LOG.warn("PC was not in the identity proof from the client. Checking for uploads.");
+ platformCredentials.addAll(getPlatformCredentials(endorsementCredential));
+ } else {
+ // if none in the identity request, look for uploaded platform credentials
+ LOG.warn("Zero-length platform credential received in identity request.");
+ }
+
+ LOG.debug("Processing serialized device info report structure of length {}",
+ challenge.getDeviceInfoReportLength());
+
+ DeviceInfoReport deviceInfoReport = (DeviceInfoReport)
+ SerializationUtils.deserialize(challenge.getDeviceInfoReport());
+
+ if (deviceInfoReport == null) {
+ LOG.error("Failed to deserialize Device Info Report");
+ throw new IllegalArgumentException("Device Info Report failed to deserialize "
+ + "from Identity Request");
+ }
+
+ LOG.info("Processing Device Info Report");
+ // store device and device info report.
+ Device device = this.deviceRegister.saveOrUpdateDevice(deviceInfoReport);
+
+ // perform supply chain validation. Note: It's possible that this should be done earlier
+ // in this method.
+ SupplyChainValidationSummary summary =
+ supplyChainValidationService.validateSupplyChain(endorsementCredential,
+ platformCredentials, device);
+
+ // update the validation result in the device
+ device.setSupplyChainStatus(summary.getOverallValidationResult());
+ deviceManager.updateDevice(device);
+
+ // check if supply chain validation succeeded.
+ // If it did not, do not provide the IdentityResponseEnvelope
+ if (summary.getOverallValidationResult() == AppraisalStatus.Status.PASS) {
+ IdentityResponseEnvelope identityResponse =
+ generateIdentityResponseEnvelopeAndStoreIssuedCert(challenge,
+ ekPublicKey, endorsementCredential, platformCredentials, device);
+
+ return structConverter.convert(identityResponse);
+ } else {
+ LOG.error("Supply chain validation did not succeed. Result is: "
+ + summary.getOverallValidationResult());
+ return new byte[]{};
+ }
+ }
+
+ /**
+ * Given a successful supply chain validation, generate an Identity Response envelope and
+ * the issued certificate. The issued cert is stored in the database. The identity response
+ * envelope is returned, and sent back to the client using the struct converter.
+ * @param challenge the identity request envelope
+ * @param ekPublicKey the EK public key
+ * @param endorsementCredential the endorsement credential
+ * @param platformCredentials the set of platform credentials
+ * @param device the device associated
+ * @return the identity response envelope
+ */
+ private IdentityResponseEnvelope generateIdentityResponseEnvelopeAndStoreIssuedCert(
+ final IdentityRequestEnvelope challenge, final PublicKey ekPublicKey,
+ final EndorsementCredential endorsementCredential,
+ final Set platformCredentials, final Device device) {
+ // decrypt the asymmetric / symmetric blobs
+ LOG.debug("unwrapping identity request");
+ byte[] identityProof = unwrapIdentityRequest(challenge.getRequest());
+
+ // the decrypted symmetric blob should be in the format of an IdentityProof. Use the
+ // struct converter to generate it.
+ IdentityProof proof = structConverter.convert(identityProof, IdentityProof.class);
+
+ // generate a session key and convert to byte array
+ LOG.debug("generating symmetric key for response");
+ SymmetricKey sessionKey = generateSymmetricKey();
+
+ // generate the asymmetric contents for the identity response
+ LOG.debug("generating asymmetric contents for response");
+ byte[] asymmetricContents = generateAsymmetricContents(proof, sessionKey, ekPublicKey);
+
+ // generate the identity credential
+ LOG.debug("generating credential from identity proof");
+ // transform the public key struct into a public key
+ PublicKey publicKey = assemblePublicKey(proof.getIdentityKey().getStorePubKey().getKey());
+ X509Certificate credential = generateCredential(publicKey, endorsementCredential,
+ platformCredentials, device.getDeviceInfo()
+ .getNetworkInfo()
+ .getIpAddress()
+ .getHostName());
+
+ // generate the attestation using the credential and the key for this session
+ LOG.debug("generating symmetric response");
+ SymmetricAttestation attestation = generateAttestation(credential, sessionKey);
+
+ // construct the response with the both the asymmetric contents and the CA attestation
+ IdentityResponseEnvelope identityResponse =
+ new SimpleStructBuilder<>(IdentityResponseEnvelope.class)
+ .set("asymmetricContents", asymmetricContents)
+ .set("symmetricAttestation", attestation).build();
+
+ // save new attestation certificate
+ byte[] derEncodedAttestationCertificate = getDerEncodedCertificate(credential);
+ saveAttestationCertificate(derEncodedAttestationCertificate, endorsementCredential,
+ platformCredentials, device);
+
+ return identityResponse;
+ }
+
+ /**
+ * Basic implementation of the ACA processIdentityClaimTpm2 method. Parses the claim,
+ * stores the device info, performs supply chain validation, generates a nonce,
+ * and wraps that nonce with the makecredential process before returning it to the client.
+ *
+ * @param identityClaim the request to process, cannot be null
+ * @return an identity claim response for the specified request containing a wrapped blob
+ */
+ public byte[] processIdentityClaimTpm2(final byte[] identityClaim) {
+
+ LOG.info("Got identity claim");
+
+ if (ArrayUtils.isEmpty(identityClaim)) {
+ throw new IllegalArgumentException("identityClaim cannot be null or empty");
+ }
+
+ // attempt to deserialize Protobuf IdentityClaim
+ ProvisionerTpm2.IdentityClaim claim = parseIdentityClaim(identityClaim);
+
+ AppraisalStatus.Status validationResult = doSupplyChainValidation(claim);
+
+ if (validationResult == AppraisalStatus.Status.PASS) {
+
+ RSAPublicKey ekPub = parsePublicKey(claim.getEkPublicArea().toByteArray());
+ RSAPublicKey akPub = parsePublicKey(claim.getAkPublicArea().toByteArray());
+ byte[] nonce = generateRandomBytes(NONCE_LENGTH);
+ ByteString blobStr = tpm20MakeCredential(ekPub, akPub, nonce);
+
+ String strNonce = HexUtils.byteArrayToHexString(nonce);
+ LOG.info("Sending nonce: " + strNonce);
+ LOG.info("Persisting claim of length: " + identityClaim.length);
+
+ tpm2ProvisionerStateDBManager.save(new TPM2ProvisionerState(nonce, identityClaim));
+
+ // Package response
+ ProvisionerTpm2.IdentityClaimResponse response
+ = ProvisionerTpm2.IdentityClaimResponse.newBuilder()
+ .setCredentialBlob(blobStr).build();
+
+ return response.toByteArray();
+ } else {
+ LOG.error("Supply chain validation did not succeed. Result is: "
+ + validationResult);
+ return new byte[]{};
+ }
+ }
+
+ /**
+ * Performs supply chain validation.
+ *
+ * @param claim the identity claim
+ * @return the {@link AppraisalStatus} of the supply chain validation
+ */
+ private AppraisalStatus.Status doSupplyChainValidation(
+ final ProvisionerTpm2.IdentityClaim claim) {
+ // attempt to find an endorsement credential to validate
+ EndorsementCredential endorsementCredential = parseEcFromIdentityClaim(claim);
+
+ // attempt to find platform credentials to validate
+ Set platformCredentials = parsePcsFromIdentityClaim(claim,
+ endorsementCredential);
+
+ // Parse and save device info
+ ProvisionerTpm2.DeviceInfo dv = claim.getDv();
+ Device device = processDeviceInfo(dv);
+
+ // perform supply chain validation
+ SupplyChainValidationSummary summary = supplyChainValidationService.validateSupplyChain(
+ endorsementCredential, platformCredentials, device);
+
+ // update the validation result in the device
+ AppraisalStatus.Status validationResult = summary.getOverallValidationResult();
+ device.setSupplyChainStatus(validationResult);
+ deviceManager.updateDevice(device);
+ return validationResult;
+ }
+
+ /**
+ * Basic implementation of the ACA processCertificateRequest method.
+ * Parses the nonce, validates its correctness, generates the signed,
+ * public attestation certificate, stores it, and returns it to the client.
+ *
+ * @param certificateRequest request containing nonce from earlier identity
+ * claim handshake
+ * @return a certificateResponse containing the signed certificate
+ */
+ public byte[] processCertificateRequest(final byte[] certificateRequest) {
+ LOG.info("Got certificate request");
+
+ if (ArrayUtils.isEmpty(certificateRequest)) {
+ throw new IllegalArgumentException("certificateRequest cannot be null or empty");
+ }
+
+ // attempt to deserialize Protobuf CertificateRequest
+ ProvisionerTpm2.CertificateRequest request;
+ try {
+ request = ProvisionerTpm2.CertificateRequest.parseFrom(certificateRequest);
+ } catch (InvalidProtocolBufferException ipbe) {
+ throw new IdentityProcessingException(
+ "Could not deserialize certificate request", ipbe);
+ }
+
+ // attempt to retrieve provisioner state based on nonce in request
+ TPM2ProvisionerState tpm2ProvisionerState = getTpm2ProvisionerState(request);
+ if (tpm2ProvisionerState != null) {
+ // Reparse Identity Claim to gather necessary components
+ byte[] identityClaim = tpm2ProvisionerState.getIdentityClaim();
+ ProvisionerTpm2.IdentityClaim claim = parseIdentityClaim(identityClaim);
+
+ // Get attestation public key
+ RSAPublicKey akPub = parsePublicKey(claim.getAkPublicArea().toByteArray());
+
+ // Get Endorsement Credential if it exists
+ EndorsementCredential endorsementCredential = parseEcFromIdentityClaim(claim);
+
+ // Get Platform Credentials if they exist
+ Set platformCredentials = parsePcsFromIdentityClaim(claim,
+ endorsementCredential);
+
+ // Get device name and device
+ String deviceName = claim.getDv().getNw().getHostname();
+ Device device = deviceManager.getDevice(deviceName);
+
+ // Create signed, attestation certificate
+ X509Certificate attestationCertificate = generateCredential(akPub,
+ endorsementCredential, platformCredentials, deviceName);
+ byte[] derEncodedAttestationCertificate = getDerEncodedCertificate(
+ attestationCertificate);
+
+ // We validated the nonce and made use of the identity claim so state can be deleted
+ tpm2ProvisionerStateDBManager.delete(tpm2ProvisionerState);
+
+ // Package the signed certificate into a response
+ ByteString certificateBytes = ByteString.copyFrom(derEncodedAttestationCertificate);
+ ProvisionerTpm2.CertificateResponse response = ProvisionerTpm2.CertificateResponse
+ .newBuilder().setCertificate(certificateBytes).build();
+
+ saveAttestationCertificate(derEncodedAttestationCertificate, endorsementCredential,
+ platformCredentials, device);
+
+ return response.toByteArray();
+ } else {
+ LOG.error("Could not process credential request. Invalid nonce provided: "
+ + request.getNonce().toString());
+ throw new IdentityProcessingException("Invalid nonce given in request");
+ }
+ }
+
+ /**
+ * Parse public key from public data segment generated by TPM 2.0.
+ * @param publicArea the public area segment to parse
+ * @return the RSA public key of the supplied public data
+ */
+ RSAPublicKey parsePublicKey(final byte[] publicArea) {
+ int pubLen = publicArea.length;
+ if (pubLen < RSA_MODULUS_LENGTH) {
+ throw new IdentityProcessingException(
+ "EK or AK public data segment is not long enough");
+ }
+ // public data ends with 256 byte modulus
+ byte[] modulus = HexUtils.subarray(publicArea,
+ pubLen - RSA_MODULUS_LENGTH,
+ pubLen - 1);
+ RSAPublicKey pub = (RSAPublicKey) assemblePublicKey(modulus);
+ return pub;
+ }
+
+ /**
+ * Converts a protobuf DeviceInfo object to a HIRS Utils DeviceInfoReport object.
+ * @param dv the protobuf serialized device info to convert
+ * @return a HIRS Utils DeviceInfoReport representation of dv
+ */
+ private DeviceInfoReport parseDeviceInfo(final ProvisionerTpm2.DeviceInfo dv) {
+ // Get network info
+ ProvisionerTpm2.NetworkInfo nwProto = dv.getNw();
+
+ InetAddress ip = null;
+ try {
+ ip = InetAddress.getByName(nwProto.getIpAddress());
+ } catch (UnknownHostException e) {
+ LOG.error("Unable to parse IP address: ", e);
+ }
+ String[] macAddressParts = nwProto.getMacAddress().split(":");
+
+ // convert mac hex string to byte values
+ byte[] macAddressBytes = new byte[MAC_BYTES];
+ if (macAddressParts.length == MAC_BYTES) {
+ for (int i = 0; i < MAC_BYTES; i++) {
+ Integer hex = HexUtils.hexToInt(macAddressParts[i]);
+ macAddressBytes[i] = hex.byteValue();
+ }
+ }
+ NetworkInfo nw = new NetworkInfo(nwProto.getHostname(), ip, macAddressBytes);
+
+ // Get firmware info
+ ProvisionerTpm2.FirmwareInfo fwProto = dv.getFw();
+ FirmwareInfo fw = new FirmwareInfo(fwProto.getBiosVendor(), fwProto.getBiosVersion(),
+ fwProto.getBiosReleaseDate());
+
+ // Get OS info
+ ProvisionerTpm2.OsInfo osProto = dv.getOs();
+ OSInfo os = new OSInfo(osProto.getOsName(), osProto.getOsVersion(), osProto.getOsArch(),
+ osProto.getDistribution(), osProto.getDistributionRelease());
+
+ // Get hardware info
+ ProvisionerTpm2.HardwareInfo hwProto = dv.getHw();
+ // Make sure chassis info has at least one chassis
+ String firstChassisSerialNumber = DeviceInfoReport.NOT_SPECIFIED;
+ if (hwProto.getChassisInfoCount() > 0) {
+ firstChassisSerialNumber = hwProto.getChassisInfo(0).getSerialNumber();
+ }
+ // Make sure baseboard info has at least one baseboard
+ String firstBaseboardSerialNumber = DeviceInfoReport.NOT_SPECIFIED;
+ if (hwProto.getBaseboardInfoCount() > 0) {
+ firstBaseboardSerialNumber = hwProto.getBaseboardInfo(0).getSerialNumber();
+ }
+ HardwareInfo hw = new HardwareInfo(hwProto.getManufacturer(), hwProto.getProductName(),
+ hwProto.getProductVersion(), hwProto.getSystemSerialNumber(),
+ firstChassisSerialNumber, firstBaseboardSerialNumber);
+
+
+ // Get TPM info, currently unimplemented
+ TPMInfo tpm = new TPMInfo();
+
+ // Create final report
+ DeviceInfoReport dvReport = new DeviceInfoReport(nw, os, fw, hw, tpm);
+
+ for (ProvisionerTpm2.ComponentInfo pbCompInfo : hwProto.getChassisInfoList()) {
+ dvReport.getChassisInfo().add(new ChassisComponentInfo(
+ pbCompInfo.getManufacturer(),
+ pbCompInfo.getModel(),
+ pbCompInfo.getSerialNumber(),
+ pbCompInfo.getRevision()));
+ }
+
+ for (ProvisionerTpm2.ComponentInfo pbCompInfo : hwProto.getBaseboardInfoList()) {
+ dvReport.getBaseboardInfo().add(new BaseboardComponentInfo(
+ pbCompInfo.getManufacturer(),
+ pbCompInfo.getModel(),
+ pbCompInfo.getSerialNumber(),
+ pbCompInfo.getRevision()));
+ }
+
+ for (ProvisionerTpm2.ComponentInfo pbCompInfo : hwProto.getProcessorInfoList()) {
+ dvReport.getProcessorInfo().add(new ProcessorComponentInfo(
+ pbCompInfo.getManufacturer(),
+ pbCompInfo.getModel(),
+ pbCompInfo.getSerialNumber(),
+ pbCompInfo.getRevision()));
+ }
+
+ for (ProvisionerTpm2.ComponentInfo pbCompInfo : hwProto.getBiosOrUefiInfoList()) {
+ dvReport.getBiosInfo().add(new BIOSComponentInfo(
+ pbCompInfo.getManufacturer(),
+ pbCompInfo.getModel(),
+ pbCompInfo.getRevision()));
+ }
+
+ for (ProvisionerTpm2.ComponentInfo pbCompInfo : hwProto.getNicInfoList()) {
+ dvReport.getNicInfo().add(new NICComponentInfo(
+ pbCompInfo.getManufacturer(),
+ pbCompInfo.getModel(),
+ pbCompInfo.getSerialNumber(),
+ pbCompInfo.getRevision()));
+ }
+
+ for (ProvisionerTpm2.ComponentInfo pbCompInfo : hwProto.getHardDriveInfoList()) {
+ dvReport.getHardDriveInfo().add(new HardDriveComponentInfo(
+ pbCompInfo.getManufacturer(),
+ pbCompInfo.getModel(),
+ pbCompInfo.getSerialNumber(),
+ pbCompInfo.getRevision()));
+ }
+
+ for (ProvisionerTpm2.ComponentInfo pbCompInfo : hwProto.getMemoryInfoList()) {
+ dvReport.getMemoryInfo().add(new MemoryComponentInfo(
+ pbCompInfo.getManufacturer(),
+ pbCompInfo.getModel(),
+ pbCompInfo.getSerialNumber(),
+ pbCompInfo.getRevision()));
+ }
+
+ return dvReport;
+ }
+
+ private Device processDeviceInfo(final ProvisionerTpm2.DeviceInfo dv) {
+ DeviceInfoReport deviceInfoReport = parseDeviceInfo(dv);
+
+ if (deviceInfoReport == null) {
+ LOG.error("Failed to deserialize Device Info Report");
+ throw new IllegalArgumentException("Device Info Report failed to deserialize "
+ + "from Identity Claim");
+ }
+
+ LOG.info("Processing Device Info Report");
+ // store device and device info report.
+ return this.deviceRegister.saveOrUpdateDevice(deviceInfoReport);
+ }
+
+ /**
+ * Gets the Endorsement Credential from the DB given the EK public key.
+ * @param ekPublicKey the EK public key
+ * @return the Endorsement credential, if found, otherwise null
+ */
+ private EndorsementCredential getEndorsementCredential(final PublicKey ekPublicKey) {
+ LOG.debug("Searching for endorsement credential based on public key: " + ekPublicKey);
+
+ if (ekPublicKey == null) {
+ throw new IllegalArgumentException("Cannot look up an EC given a null public key");
+ }
+
+ EndorsementCredential credential = null;
+
+ try {
+ credential = EndorsementCredential.select(this.certificateManager)
+ .byPublicKeyModulus(Certificate.getPublicKeyModulus(ekPublicKey))
+ .getCertificate();
+ } catch (IOException e) {
+ LOG.error("Could not extract public key modulus", e);
+ }
+
+ if (credential == null) {
+ LOG.warn("Unable to find endorsement credential for public key.");
+ } else {
+ LOG.debug("Endorsement credential found.");
+ }
+
+ return credential;
+ }
+
+ private Set getPlatformCredentials(final EndorsementCredential ec) {
+ Set credentials = null;
+
+ if (ec == null) {
+ LOG.warn("Cannot look for platform credential(s). Endorsement credential was null.");
+ } else {
+ LOG.debug("Searching for platform credential(s) based on holder serial number: "
+ + ec.getSerialNumber());
+ credentials = PlatformCredential.select(this.certificateManager)
+ .byHolderSerialNumber(ec.getSerialNumber())
+ .getCertificates();
+ if (credentials == null || credentials.isEmpty()) {
+ LOG.warn("No platform credential(s) found");
+ } else {
+ LOG.debug("Platform Credential(s) found: " + credentials.size());
+ }
+ }
+
+ return credentials;
+ }
+
+ @Override
+ public byte[] getPublicKey() {
+ return acaCertificate.getPublicKey().getEncoded();
+ }
+
+ /**
+ * Unwraps a given identityRequest. That is to say, decrypt the asymmetric portion of a data
+ * structure to determine the method to decrypt the symmetric portion.
+ *
+ * @param identityRequest
+ * to be decrypted
+ * @return the decrypted symmetric portion of an identity request.
+ */
+ byte[] unwrapIdentityRequest(final byte[] identityRequest) {
+ IdentityRequest request = structConverter.convert(identityRequest, IdentityRequest.class);
+
+ // in case the TPM did not specify the IV, it must be extracted from the symmetric blob.
+ // the IV will then be the the first block of the cipher text.
+ final byte[] iv;
+ SymmetricKeyParams symmetricKeyParams = request.getSymmetricAlgorithm();
+ if (symmetricKeyParams != null && symmetricKeyParams.getParams() != null) {
+ iv = symmetricKeyParams.getParams().getIv();
+ } else {
+ iv = extractInitialValue(request);
+ }
+
+ // determine the encryption scheme from the algorithm
+ EncryptionScheme asymmetricScheme =
+ EncryptionScheme.fromInt(request.getAsymmetricAlgorithm().getEncryptionScheme());
+
+ // decrypt the asymmetric blob
+ byte[] decryptedAsymmetricBlob =
+ decryptAsymmetricBlob(request.getAsymmetricBlob(), asymmetricScheme);
+
+ // construct our symmetric key structure from the decrypted asymmetric blob
+ SymmetricKey symmetricKey =
+ structConverter.convert(decryptedAsymmetricBlob, SymmetricKey.class);
+
+ byte[] decryptedSymmetricBlob =
+ decryptSymmetricBlob(request.getSymmetricBlob(), symmetricKey.getKey(), iv,
+ "AES/CBC/PKCS5Padding");
+
+ // decrypt the symmetric blob
+ return decryptedSymmetricBlob;
+ }
+
+ /**
+ * Will attempt to decrypt the asymmetric blob that originated from an
+ * {@link hirs.structs.elements.tpm.IdentityRequest} using the cipher transformation.
+ *
+ * @param asymmetricBlob
+ * to be decrypted
+ * @param scheme
+ * to decrypt with
+ * @return decrypted blob
+ */
+ byte[] decryptAsymmetricBlob(final byte[] asymmetricBlob, final EncryptionScheme scheme) {
+ try {
+ // create a cipher from the specified transformation
+ Cipher cipher = Cipher.getInstance(scheme.toString());
+
+ switch (scheme) {
+ case OAEP:
+ OAEPParameterSpec spec =
+ new OAEPParameterSpec("Sha1", "MGF1", MGF1ParameterSpec.SHA1,
+ new PSource.PSpecified("".getBytes()));
+
+ cipher.init(Cipher.PRIVATE_KEY, privateKey, spec);
+ break;
+ default:
+ // initialize the cipher to decrypt using the ACA private key.
+ cipher.init(Cipher.DECRYPT_MODE, privateKey);
+ }
+
+ cipher.update(asymmetricBlob);
+
+ return cipher.doFinal();
+ } catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException
+ | BadPaddingException | IllegalBlockSizeException
+ | InvalidAlgorithmParameterException e) {
+ throw new IdentityProcessingException(
+ "Encountered error while decrypting asymmetric blob of an identity request: "
+ + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Will attempt to decrypt the symmetric blob that originated from an
+ * {@link hirs.structs.elements.tpm.IdentityRequest} using the specified symmetric key
+ * and cipher transformation.
+ *
+ * @param symmetricBlob
+ * to be decrypted
+ * @param symmetricKey
+ * to use to decrypt
+ * @param iv
+ * to use with decryption cipher
+ * @param transformation
+ * of the cipher
+ * @return decrypted symmetric blob
+ */
+ byte[] decryptSymmetricBlob(final byte[] symmetricBlob, final byte[] symmetricKey,
+ final byte[] iv, final String transformation) {
+ try {
+ // create a cipher from the specified transformation
+ Cipher cipher = Cipher.getInstance(transformation);
+
+ // generate a key specification to initialize the cipher
+ SecretKeySpec keySpec = new SecretKeySpec(symmetricKey, "AES");
+
+ // initialize the cipher to decrypt using the symmetric key
+ cipher.init(Cipher.DECRYPT_MODE, keySpec, new IvParameterSpec(iv));
+
+ // decrypt the symmetric blob
+ return cipher.doFinal(symmetricBlob);
+ } catch (IllegalBlockSizeException | InvalidKeyException | NoSuchAlgorithmException
+ | BadPaddingException | NoSuchPaddingException
+ | InvalidAlgorithmParameterException e) {
+ throw new IdentityProcessingException(
+ "Encountered error while decrypting symmetric blob of an identity request: "
+ + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Constructs a public key where the modulus is in raw form.
+ *
+ * @param modulus
+ * in byte array form
+ * @return public key using specific modulus and the well known exponent
+ */
+ PublicKey assemblePublicKey(final byte[] modulus) {
+ return assemblePublicKey(Hex.encodeHexString(modulus));
+ }
+
+ /**
+ * Constructs a public key where the modulus is Hex encoded.
+ *
+ * @param modulus
+ * hex encoded modulus
+ * @return public key using specific modulus and the well known exponent
+ */
+ PublicKey assemblePublicKey(final String modulus) {
+ return assemblePublicKey(new BigInteger(modulus,
+ AttestationCertificateAuthority.DEFAULT_IV_SIZE));
+ }
+
+ /**
+ * Assembles a public key using a defined big int modulus and the well known exponent.
+ */
+ private PublicKey assemblePublicKey(final BigInteger modulus) {
+
+ // generate a key spec using mod and exp
+ RSAPublicKeySpec keySpec = new RSAPublicKeySpec(modulus, EXPONENT);
+
+ // create the public key
+ try {
+ KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+ return keyFactory.generatePublic(keySpec);
+ } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
+ throw new IdentityProcessingException(
+ "Encountered unexpected error creating public key: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * @return {@link SymmetricKey} using random bytes
+ */
+ SymmetricKey generateSymmetricKey() {
+
+ // create a session key for the CA contents
+ byte[] responseSymmetricKey =
+ generateRandomBytes(AttestationCertificateAuthority.DEFAULT_IV_SIZE);
+
+ // create a symmetric key struct for the CA contents
+ SymmetricKey sessionKey =
+ new SimpleStructBuilder<>(SymmetricKey.class)
+ .set("algorithmId", SymmetricKey.ALGORITHM_AES)
+ .set("encryptionScheme", SymmetricKey.SCHEME_CBC)
+ .set("key", responseSymmetricKey).build();
+ return sessionKey;
+ }
+
+ /**
+ * Generate asymmetric contents part of the identity response.
+ *
+ * @param proof
+ * identity requests symmetric contents, otherwise, the identity proof
+ * @param symmetricKey
+ * identity response session key
+ * @param publicKey
+ * of the EK certificate contained within the identity proof
+ * @return encrypted asymmetric contents
+ */
+ byte[] generateAsymmetricContents(final IdentityProof proof, final SymmetricKey symmetricKey,
+ final PublicKey publicKey) {
+ try {
+ // obtain the identity key from the identity proof
+ byte[] identityKey = structConverter.convert(proof.getIdentityKey());
+ byte[] sessionKey = structConverter.convert(symmetricKey);
+
+ // create a SHA1 digest of the identity key
+ MessageDigest md = MessageDigest.getInstance("SHA-1");
+ md.update(identityKey);
+
+ // generate the digest
+ byte[] identityDigest = md.digest();
+
+ // combine the session key with the digest of the identity key
+ byte[] asymmetricContents = ArrayUtils.addAll(sessionKey, identityDigest);
+
+ // encrypt the asymmetric contents and return
+ OAEPParameterSpec oaepSpec =
+ new OAEPParameterSpec("Sha1", "MGF1", MGF1ParameterSpec.SHA1,
+ new PSource.PSpecified("TCPA".getBytes()));
+
+ // initialize the asymmetric cipher using the default OAEP transformation
+ Cipher cipher = Cipher.getInstance(EncryptionScheme.OAEP.toString());
+
+ // initialize the cipher using the public spec with the additional OAEP specification
+ cipher.init(Cipher.PUBLIC_KEY, publicKey, oaepSpec);
+
+ return cipher.doFinal(asymmetricContents);
+ } catch (NoSuchAlgorithmException | IllegalBlockSizeException | NoSuchPaddingException
+ | InvalidKeyException | BadPaddingException
+ | InvalidAlgorithmParameterException e) {
+ throw new IdentityProcessingException(
+ "Encountered error while generating ACA session key: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Generate the Identity Response using the identity credential and the session key.
+ *
+ * @param credential
+ * the identity credential
+ * @param symmetricKey
+ * generated session key for this request/response chain
+ * @return identity response for an identity request
+ */
+ SymmetricAttestation generateAttestation(final X509Certificate credential,
+ final SymmetricKey symmetricKey) {
+
+ try {
+ // initialize the symmetric cipher
+ Cipher aesCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+
+ // generate a key specification to initialize the cipher
+ SecretKeySpec keySpec = new SecretKeySpec(symmetricKey.getKey(), "AES");
+
+ // fill IV with random bytes
+ byte[] credentialIV = generateRandomBytes(
+ AttestationCertificateAuthority.DEFAULT_IV_SIZE);
+
+ // create IV encryption parameter specification
+ IvParameterSpec ivParameterSpec = new IvParameterSpec(credentialIV);
+
+ // initialize the cipher to decrypt using the symmetric key
+ aesCipher.init(Cipher.ENCRYPT_MODE, keySpec, ivParameterSpec);
+
+ // encrypt the credential
+ byte[] encryptedCredential = aesCipher.doFinal(credential.getEncoded());
+
+ // prepend the IV to the encrypted credential
+ byte[] credentialBytes = ArrayUtils.addAll(credentialIV, encryptedCredential);
+
+ // create attestation for identity response that contains the credential
+ SymmetricAttestation attestation =
+ new SimpleStructBuilder<>(SymmetricAttestation.class)
+ .set("credential", credentialBytes)
+ .set("algorithm",
+ new SimpleStructBuilder<>(SymmetricKeyParams.class)
+ .set("algorithmId", SymmetricKeyParams.ALGORITHM_AES)
+ .set("encryptionScheme",
+ SymmetricKeyParams.SCHEME_CBC_PKCS5PADDING)
+ .set("signatureScheme", 0).build()).build();
+
+ return attestation;
+
+ } catch (BadPaddingException | IllegalBlockSizeException | NoSuchAlgorithmException
+ | InvalidKeyException | InvalidAlgorithmParameterException | NoSuchPaddingException
+ | CertificateEncodingException e) {
+ throw new IdentityProcessingException(
+ "Encountered error while generating Identity Response: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Generates a credential using the specified public key.
+ *
+ * @param publicKey
+ * cannot be null
+ * @param endorsementCredential
+ * the endorsement credential
+ * @param platformCredentials
+ * the set of platform credentials
+ * @param deviceName
+ * The host name used in the subject alternative name
+ * @return identity credential
+ */
+ X509Certificate generateCredential(final PublicKey publicKey,
+ final EndorsementCredential endorsementCredential,
+ final Set platformCredentials,
+ final String deviceName) {
+ try {
+ // have the certificate expire in the configured number of days
+ Calendar expiry = Calendar.getInstance();
+ expiry.add(Calendar.DAY_OF_YEAR, validDays);
+
+ X500Name issuer =
+ new X500Name(acaCertificate.getSubjectX500Principal().getName());
+ Date notBefore = new Date();
+ Date notAfter = expiry.getTime();
+ BigInteger serialNumber = BigInteger.valueOf(System.currentTimeMillis());
+
+ SubjectPublicKeyInfo subjectPublicKeyInfo =
+ SubjectPublicKeyInfo.getInstance(publicKey.getEncoded());
+
+ // The subject should be left blank, per spec
+ X509v3CertificateBuilder builder =
+ new X509v3CertificateBuilder(issuer, serialNumber,
+ notBefore, notAfter, null /* subjectName */, subjectPublicKeyInfo);
+
+ Extension subjectAlternativeName =
+ IssuedCertificateAttributeHelper.buildSubjectAlternativeNameFromCerts(
+ endorsementCredential, platformCredentials, deviceName);
+
+ builder.addExtension(subjectAlternativeName);
+ // identify cert as an AIK with this extension
+ if (null != IssuedCertificateAttributeHelper.EXTENDED_KEY_USAGE_EXTENSION) {
+ builder.addExtension(IssuedCertificateAttributeHelper.EXTENDED_KEY_USAGE_EXTENSION);
+ } else {
+ LOG.warn("Failed to build extended key usage extension and add to AIK");
+ throw new IllegalStateException("Extended Key Usage attribute unavailable. "
+ + "Unable to issue certificates");
+ }
+
+ ContentSigner signer = new JcaContentSignerBuilder("SHA1WithRSA")
+ .setProvider("BC").build(privateKey);
+ X509CertificateHolder holder = builder.build(signer);
+ X509Certificate certificate = new JcaX509CertificateConverter()
+ .setProvider("BC").getCertificate(holder);
+ return certificate;
+ } catch (IOException | OperatorCreationException | CertificateException e) {
+ throw new IdentityProcessingException("Encountered error while generating "
+ + "identity credential: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Performs the first step of the TPM 2.0 identity claim process. Takes an ek, ak, and secret
+ * and then generates a seed that is used to generate AES and HMAC keys. Parses the ak name.
+ * Encrypts the seed with the public ek. Uses the AES key to encrypt the secret. Uses the HMAC
+ * key to generate an HMAC to cover the encrypted secret and the ak name. The output is an
+ * encrypted blob that acts as the first part of a challenge-response authentication mechanism
+ * to validate an identity claim.
+ *
+ * Equivalent to calling tpm2_makecredential using tpm2_tools.
+ *
+ * @param ek endorsement key in the identity claim
+ * @param ak attestation key in the identity claim
+ * @param secret a nonce
+ * @return the encrypted blob forming the identity claim challenge
+ */
+ protected ByteString tpm20MakeCredential(final RSAPublicKey ek, final RSAPublicKey ak,
+ final byte[] secret) {
+ // check size of the secret
+ if (secret.length > MAX_SECRET_LENGTH) {
+ throw new IllegalArgumentException("Secret must be " + MAX_SECRET_LENGTH
+ + " bytes or smaller.");
+ }
+
+ // generate a random 32 byte seed
+ byte[] seed = generateRandomBytes(SEED_LENGTH);
+
+ try {
+ // encrypt seed with pubEk
+ Cipher asymCipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");
+ OAEPParameterSpec oaepSpec = new OAEPParameterSpec("SHA-256", "MGF1",
+ MGF1ParameterSpec.SHA256, new PSource.PSpecified("IDENTITY\0".getBytes()));
+ asymCipher.init(Cipher.PUBLIC_KEY, ek, oaepSpec);
+ asymCipher.update(seed);
+ byte[] encSeed = asymCipher.doFinal();
+
+ // generate ak name from akMod
+ byte[] akModTemp = ak.getModulus().toByteArray();
+ byte[] akMod = new byte[RSA_MODULUS_LENGTH];
+ int startpos = 0;
+ // BigIntegers are signed, so a modulus that has a first bit of 1
+ // will be padded with a zero byte that must be removed
+ if (akModTemp[0] == 0x00) {
+ startpos = 1;
+ }
+ System.arraycopy(akModTemp, startpos, akMod, 0, RSA_MODULUS_LENGTH);
+ byte[] akName = generateAkName(akMod);
+
+ // generate AES and HMAC keys from seed
+ byte[] aesKey = cryptKDFa(seed, "STORAGE", akName, AES_KEY_LENGTH_BYTES);
+ byte[] hmacKey = cryptKDFa(seed, "INTEGRITY", null, HMAC_KEY_LENGTH_BYTES);
+
+ // use two bytes to add a size prefix on secret
+ ByteBuffer b;
+ b = ByteBuffer.allocate(2);
+ b.putShort((short) (secret.length));
+ byte[] secretLength = b.array();
+ byte[] secretBytes = new byte[secret.length + 2];
+ System.arraycopy(secretLength, 0, secretBytes, 0, 2);
+ System.arraycopy(secret, 0, secretBytes, 2, secret.length);
+
+ // encrypt size prefix + secret with AES key
+ Cipher symCipher = Cipher.getInstance("AES/CFB/NoPadding");
+ byte[] defaultIv = HexUtils.hexStringToByteArray("00000000000000000000000000000000");
+ IvParameterSpec ivSpec = new IvParameterSpec(defaultIv);
+ symCipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(aesKey, "AES"), ivSpec);
+ byte[] encSecret = symCipher.doFinal(secretBytes);
+
+ // generate HMAC covering encrypted secret and ak name
+ Mac integrityHmac = Mac.getInstance("HmacSHA256");
+ SecretKeySpec integrityKey = new SecretKeySpec(hmacKey, integrityHmac.getAlgorithm());
+ integrityHmac.init(integrityKey);
+ byte[] message = new byte[encSecret.length + akName.length];
+ System.arraycopy(encSecret, 0, message, 0, encSecret.length);
+ System.arraycopy(akName, 0, message, encSecret.length, akName.length);
+ integrityHmac.update(message);
+ byte[] integrity = integrityHmac.doFinal();
+ b = ByteBuffer.allocate(2);
+ b.putShort((short) (HMAC_SIZE_LENGTH_BYTES + HMAC_KEY_LENGTH_BYTES + encSecret.length));
+ byte[] topSize = b.array();
+
+ // return ordered blob of assembled credentials
+ byte[] bytesToReturn = assembleCredential(topSize, integrity, encSecret, encSeed);
+ return ByteString.copyFrom(bytesToReturn);
+
+ } catch (BadPaddingException | IllegalBlockSizeException | NoSuchAlgorithmException
+ | InvalidKeyException | InvalidAlgorithmParameterException
+ | NoSuchPaddingException e) {
+ throw new IdentityProcessingException(
+ "Encountered error while making credential: " + e.getMessage(), e);
+ }
+ }
+
+ @SuppressWarnings("magicnumber")
+ private byte[] assembleCredential(final byte[] topSize, final byte[] integrityHmac,
+ final byte[] encryptedSecret,
+ final byte[] encryptedSeed) {
+ /*
+ * Credential structure breakdown with endianness:
+ * 0-1 topSize (2), LE
+ * 2-3 hashsize (2), BE always 0x0020
+ * 4-35 integrity HMac (32)
+ * 36-133 (98 = 32*3 +2) of zeros, copy over from encSecret starting at [36]
+ * 134-135 (2) LE size, always 0x0001
+ * 136-391 (256) copy over with encSeed
+ * */
+ byte[] credentialBlob = new byte[TPM2_CREDENTIAL_BLOB_SIZE];
+ credentialBlob[0] = topSize[1];
+ credentialBlob[1] = topSize[0];
+ credentialBlob[2] = 0x00;
+ credentialBlob[3] = 0x20;
+ System.arraycopy(integrityHmac, 0, credentialBlob, 4, 32);
+ for (int i = 0; i < 98; i++) {
+ credentialBlob[36 + i] = 0x00;
+ }
+ System.arraycopy(encryptedSecret, 0, credentialBlob, 36, encryptedSecret.length);
+ credentialBlob[134] = 0x00;
+ credentialBlob[135] = 0x01;
+ System.arraycopy(encryptedSeed, 0, credentialBlob, 136, 256);
+ // return the result
+ return credentialBlob;
+ }
+
+ /**
+ * Determines the AK name from the AK Modulus.
+ * @param akModulus modulus of an attestation key
+ * @return the ak name byte array
+ * @throws NoSuchAlgorithmException Underlying SHA256 method used a bad algorithm
+ */
+ byte[] generateAkName(final byte[] akModulus) throws NoSuchAlgorithmException {
+ byte[] namePrefix = HexUtils.hexStringToByteArray(AK_NAME_PREFIX);
+ byte[] hashPrefix = HexUtils.hexStringToByteArray(AK_NAME_HASH_PREFIX);
+ byte[] toHash = new byte[hashPrefix.length + akModulus.length];
+ System.arraycopy(hashPrefix, 0, toHash, 0, hashPrefix.length);
+ System.arraycopy(akModulus, 0, toHash, hashPrefix.length, akModulus.length);
+ byte[] nameHash = sha256hash(toHash);
+ byte[] toReturn = new byte[namePrefix.length + nameHash.length];
+ System.arraycopy(namePrefix, 0, toReturn, 0, namePrefix.length);
+ System.arraycopy(nameHash, 0, toReturn, namePrefix.length, nameHash.length);
+ return toReturn;
+ }
+
+ /**
+ * This replicates the TPM 2.0 CryptKDFa function to an extent. It will only work for generation
+ * that uses SHA-256, and will only generate values of 32 B or less. Counters above zero and
+ * multiple contexts are not supported in this implementation. This should work for all uses of
+ * the KDF for TPM2_MakeCredential.
+ *
+ * @param seed random value used to generate the key
+ * @param label first portion of message used to generate key
+ * @param context second portion of message used to generate key
+ * @param sizeInBytes size of key to generate in bytes
+ * @return the derived key
+ * @throws NoSuchAlgorithmException Wrong crypto algorithm selected
+ * @throws InvalidKeyException Invalid key used
+ */
+ @SuppressWarnings("magicnumber")
+ private byte[] cryptKDFa(final byte[] seed, final String label, final byte[] context,
+ final int sizeInBytes)
+ throws NoSuchAlgorithmException, InvalidKeyException {
+ ByteBuffer b;
+ b = ByteBuffer.allocate(4);
+ b.putInt(1);
+ byte[] counter = b.array();
+ // get the label
+ String labelWithEnding = label;
+ if (label.charAt(label.length() - 1) != "\0".charAt(0)) {
+ labelWithEnding = label + "\0";
+ }
+ byte[] labelBytes = labelWithEnding.getBytes();
+ b = ByteBuffer.allocate(4);
+ b.putInt(sizeInBytes * 8);
+ byte[] desiredSizeInBits = b.array();
+ int sizeOfMessage = 8 + labelBytes.length;
+ if (context != null) {
+ sizeOfMessage += context.length;
+ }
+ byte[] message = new byte[sizeOfMessage];
+ int marker = 0;
+ System.arraycopy(counter, 0, message, marker, 4);
+ marker += 4;
+ System.arraycopy(labelBytes, 0, message, marker, labelBytes.length);
+ marker += labelBytes.length;
+ if (context != null) {
+ System.arraycopy(context, 0, message, marker, context.length);
+ marker += context.length;
+ }
+ System.arraycopy(desiredSizeInBits, 0, message, marker, 4);
+ Mac hmac;
+ byte[] toReturn = null;
+
+ hmac = Mac.getInstance("HmacSHA256");
+ SecretKeySpec hmacKey = new SecretKeySpec(seed, hmac.getAlgorithm());
+ hmac.init(hmacKey);
+ hmac.update(message);
+ byte[] hmacResult = hmac.doFinal();
+ toReturn = new byte[sizeInBytes];
+ System.arraycopy(hmacResult, 0, toReturn, 0, sizeInBytes);
+ return toReturn;
+ }
+
+ /**
+ * Computes the sha256 hash of the given blob.
+ * @param blob byte array to take the hash of
+ * @return sha256 hash of blob
+ * @throws NoSuchAlgorithmException improper algorithm selected
+ */
+ private byte[] sha256hash(final byte[] blob) throws NoSuchAlgorithmException {
+ byte[] toReturn = null;
+ MessageDigest md = MessageDigest.getInstance("SHA-256");
+ md.update(blob);
+ toReturn = md.digest();
+ return toReturn;
+ }
+
+ /**
+ * Generates a array of random bytes.
+ *
+ * @param numberOfBytes
+ * to be generated
+ * @return byte array filled with the specified number of bytes.
+ */
+ private byte[] generateRandomBytes(final int numberOfBytes) {
+ byte[] bytes = new byte[numberOfBytes];
+ SecureRandom random = new SecureRandom();
+ random.nextBytes(bytes);
+ return bytes;
+ }
+
+ /**
+ * Extracts the IV from the identity request. That is, take the first block of data from the
+ * symmetric blob and treat that as the IV. This modifies the original symmetric block.
+ *
+ * @param identityRequest
+ * to extract the IV from
+ * @return the IV from the identity request
+ */
+ private byte[] extractInitialValue(final IdentityRequest identityRequest) {
+
+ // make a reference to the symmetric blob
+ byte[] symmetricBlob = identityRequest.getSymmetricBlob();
+
+ // create the IV
+ byte[] iv = new byte[AttestationCertificateAuthority.DEFAULT_IV_SIZE];
+
+ // initialize a new symmetric blob with the length of the original minus the IV
+ byte[] updatedBlob = new byte[symmetricBlob.length - iv.length];
+
+ // copy the IV out of the original symmetric blob
+ System.arraycopy(symmetricBlob, 0, iv, 0, iv.length);
+
+ // copy everything but the IV out of the original blob into the new blob
+ System.arraycopy(symmetricBlob, iv.length, updatedBlob, 0, updatedBlob.length);
+
+ // reassign the symmetric blob to the request.
+ identityRequest.setSymmetricBlob(updatedBlob);
+
+ return iv;
+ }
+
+ /**
+ * Helper method to unwrap the certificate request sent by the client and verify the
+ * provided nonce.
+ *
+ * @param request Client Certificate Request containing nonce to complete identity claim
+ * @return the {@link TPM2ProvisionerState} if valid nonce provided / null, otherwise
+ */
+ private TPM2ProvisionerState getTpm2ProvisionerState(
+ final ProvisionerTpm2.CertificateRequest request) {
+ if (request.hasNonce()) {
+ byte[] nonce = request.getNonce().toByteArray();
+ return TPM2ProvisionerState.getTPM2ProvisionerState(tpm2ProvisionerStateDBManager,
+ nonce);
+ }
+ return null;
+ }
+
+ /**
+ * Helper method to parse a byte array into an {@link ProvisionerTpm2.IdentityClaim}.
+ *
+ * @param identityClaim byte array that should be converted to a Protobuf IdentityClaim
+ * object
+ * @throws {@link IdentityProcessingException} if byte array could not be parsed
+ * @return the Protobuf generated Identity Claim object
+ */
+ private ProvisionerTpm2.IdentityClaim parseIdentityClaim(final byte[] identityClaim) {
+ try {
+ return ProvisionerTpm2.IdentityClaim.parseFrom(identityClaim);
+ } catch (InvalidProtocolBufferException ipbe) {
+ throw new IdentityProcessingException(
+ "Could not deserialize identity claim", ipbe);
+ }
+ }
+
+ /**
+ * Helper method to parse an Endorsement Credential from a Protobuf generated
+ * IdentityClaim. Persists the Endorsement Credential if it does not already exist.
+ *
+ * @param identityClaim a Protobuf generated Identity Claim object
+ * @return the Endorsement Credential, if one exists, null otherwise
+ */
+ private EndorsementCredential parseEcFromIdentityClaim(
+ final ProvisionerTpm2.IdentityClaim identityClaim) {
+ if (identityClaim.hasEndorsementCredential()) {
+ return CredentialManagementHelper.storeEndorsementCredential(
+ this.certificateManager,
+ identityClaim.getEndorsementCredential().toByteArray());
+ } else {
+ LOG.warn("No endorsement credential received in identity claim.");
+ }
+ return null;
+ }
+
+ /**
+ * Helper method to parse a set of Platform Credentials from a Protobuf generated
+ * IdentityClaim and Endorsement Credential. Persists the Platform Credentials if they
+ * do not already exist.
+ *
+ * @param identityClaim a Protobuf generated Identity Claim object
+ * @param endorsementCredential an endorsement credential to check if platform credentials
+ * exist
+ * @return the Set of Platform Credentials, if they exist, an empty set otherwise
+ */
+ private Set parsePcsFromIdentityClaim(
+ final ProvisionerTpm2.IdentityClaim identityClaim,
+ final EndorsementCredential endorsementCredential) {
+ Set platformCredentials = new HashSet<>();
+ if (identityClaim.getPlatformCredentialCount() > 0) {
+ for (ByteString platformCredential : identityClaim.getPlatformCredentialList()) {
+ if (!platformCredential.isEmpty()) {
+ platformCredentials.add(CredentialManagementHelper.storePlatformCredential(
+ this.certificateManager, platformCredential.toByteArray()));
+ }
+ }
+ } else if (endorsementCredential != null) {
+ // if none in the identity claim, look for uploaded platform credentials
+ LOG.warn("PC was not in the identity claim from the client. Checking for uploads.");
+ platformCredentials.addAll(getPlatformCredentials(endorsementCredential));
+ } else {
+ LOG.warn("No platform credential received in identity claim.");
+ }
+ return platformCredentials;
+ }
+
+ /**
+ * Helper method to extract a DER encoded ASN.1 certificate from an X509 certificate.
+ *
+ * @param certificate the X509 certificate to be converted to DER encoding
+ * @throws {@link IdentityProcessingException} if error occurs during encoding retrieval
+ * @return the byte array representing the DER encoded certificate
+ */
+ private byte[] getDerEncodedCertificate(final X509Certificate certificate) {
+ try {
+ return certificate.getEncoded();
+ } catch (CertificateEncodingException e) {
+ LOG.error("Error converting certificate to ASN.1 DER Encoding.", e);
+ throw new IdentityProcessingException(
+ "Encountered error while converting X509 Certificate: "
+ + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Helper method to create an {@link IssuedAttestationCertificate} object, set its
+ * corresponding device and persist it.
+ *
+ * @param derEncodedAttestationCertificate the byte array representing the Attestation
+ * certificate
+ * @param endorsementCredential the endorsement credential used to generate the AC
+ * @param platformCredentials the platform credentials used to generate the AC
+ * @param device the device to which the attestation certificate is tied
+ * @throws {@link IdentityProcessingException} if error occurs in persisting the Attestation
+ * Certificate
+ */
+ private void saveAttestationCertificate(final byte[] derEncodedAttestationCertificate,
+ final EndorsementCredential endorsementCredential,
+ final Set platformCredentials,
+ final Device device) {
+ try {
+ // save issued certificate
+ IssuedAttestationCertificate attCert = new IssuedAttestationCertificate(
+ derEncodedAttestationCertificate, endorsementCredential, platformCredentials);
+ attCert.setDevice(device);
+ certificateManager.save(attCert);
+ } catch (Exception e) {
+ LOG.error("Error saving generated Attestation Certificate to database.", e);
+ throw new IdentityProcessingException(
+ "Encountered error while storing Attestation Certificate: "
+ + e.getMessage(), e);
+ }
+ }
+}
diff --git a/HIRS_AttestationCA/src/main/java/hirs/attestationca/AcaDbInit.java b/HIRS_AttestationCA/src/main/java/hirs/attestationca/AcaDbInit.java
new file mode 100644
index 000000000..b5bc03da3
--- /dev/null
+++ b/HIRS_AttestationCA/src/main/java/hirs/attestationca/AcaDbInit.java
@@ -0,0 +1,72 @@
+package hirs.attestationca;
+
+import hirs.appraiser.SupplyChainAppraiser;
+import hirs.data.persist.DeviceGroup;
+import hirs.data.persist.SupplyChainPolicy;
+import hirs.persist.AppraiserManager;
+import hirs.persist.DeviceGroupManager;
+import hirs.persist.PolicyManager;
+
+import static hirs.attestationca.AbstractAttestationCertificateAuthority.LOG;
+
+/**
+ * Utility class that simply holds logic to seed the ACA's database with its
+ * default entries.
+ */
+public final class AcaDbInit {
+ // prevent construction
+ private AcaDbInit() { }
+
+ /**
+ * Insert the ACA's default entries into the DB. This class is invoked after successful
+ * install of the HIRS_AttestationCA RPM.
+ *
+ * @param appraiserManager the AppraiserManager to use to persist appraisers
+ * @param deviceGroupManager the DeviceGroupManager to use to persist device groups
+ * @param policyManager the PolicyManager to use to persist policies
+ */
+ public static synchronized void insertDefaultEntries(
+ final AppraiserManager appraiserManager,
+ final DeviceGroupManager deviceGroupManager,
+ final PolicyManager policyManager
+ ) {
+ LOG.info("Ensuring default ACA database entries are present.");
+
+ // Ensure the default group exists. It may have already been created by the Server RPM
+ DeviceGroup defaultGroup = deviceGroupManager.getDeviceGroup(DeviceGroup.DEFAULT_GROUP);
+ if (defaultGroup == null) {
+ LOG.info("Default group not found; saving...");
+ defaultGroup = deviceGroupManager.saveDeviceGroup(new DeviceGroup(
+ DeviceGroup.DEFAULT_GROUP,
+ "This is the default group"
+ ));
+ LOG.info("Saved default group.");
+ }
+
+ // If the SupplyChainAppraiser exists, do not attempt to re-save the supply chain appraiser
+ // or SupplyChainPolicy
+ SupplyChainAppraiser supplyChainAppraiser = (SupplyChainAppraiser)
+ appraiserManager.getAppraiser(SupplyChainAppraiser.NAME);
+ if (supplyChainAppraiser != null) {
+ LOG.info("Supply chain appraiser is present; not inserting any more entries.");
+ LOG.info("ACA database initialization complete.");
+ return;
+ }
+
+ // Create the SupplyChainAppraiser
+ LOG.info("Saving supply chain appraiser...");
+ supplyChainAppraiser = (SupplyChainAppraiser)
+ appraiserManager.saveAppraiser(new SupplyChainAppraiser());
+
+ // Create the SupplyChainPolicy
+ LOG.info("Saving default supply chain policy...");
+ SupplyChainPolicy supplyChainPolicy = new SupplyChainPolicy(
+ SupplyChainPolicy.DEFAULT_POLICY
+ );
+ policyManager.savePolicy(supplyChainPolicy);
+ policyManager.setDefaultPolicy(supplyChainAppraiser, supplyChainPolicy);
+ policyManager.setPolicy(supplyChainAppraiser, defaultGroup, supplyChainPolicy);
+
+ LOG.info("ACA database initialization complete.");
+ }
+}
diff --git a/HIRS_AttestationCA/src/main/java/hirs/attestationca/AttestationCertificateAuthority.java b/HIRS_AttestationCA/src/main/java/hirs/attestationca/AttestationCertificateAuthority.java
new file mode 100644
index 000000000..f1f2a0da2
--- /dev/null
+++ b/HIRS_AttestationCA/src/main/java/hirs/attestationca/AttestationCertificateAuthority.java
@@ -0,0 +1,52 @@
+package hirs.attestationca;
+
+/**
+ * Defines the responsibilities of the Attestation Certificate Authority.
+ */
+public interface AttestationCertificateAuthority {
+
+ /**
+ * The default size for IV blocks.
+ */
+ int DEFAULT_IV_SIZE = 16;
+
+ /**
+ * Processes a given {@link hirs.structs.elements.aca.IdentityRequestEnvelope} and
+ * generates a {@link hirs.structs.elements.aca.IdentityResponseEnvelope}. In most cases,
+ * a client will generate the request using the TPM "Collate Identity" process.
+ *
+ * @param identityRequest generated during the collate identity process with a Tpm
+ * @return response for the request
+ */
+ byte[] processIdentityRequest(byte[] identityRequest);
+
+ /**
+ * Processes a given
+ * {@link hirs.attestationca.configuration.provisionerTpm2.ProvisionerTpm2.IdentityClaim} and
+ * generates a response containing an encrypted nonce to be returned by the client in
+ * a future handshake request.
+ *
+ * @param identityClaim generated during the create identity claim process on a TPM2 Provisioner
+ * @return response for the request
+ */
+ byte[] processIdentityClaimTpm2(byte[] identityClaim);
+
+ /**
+ * Processes a given
+ * {@link hirs.attestationca.configuration.provisionerTpm2.ProvisionerTpm2.CertificateRequest}
+ * and generates a response containing the signed, public certificate for
+ * the client's desired attestation key, if the correct nonce is supplied.
+ *
+ * @param certificateRequest request containing nonce from earlier identity
+ * claim handshake
+ * @return response for the request
+ */
+ byte[] processCertificateRequest(byte[] certificateRequest);
+
+ /**
+ * Issues the PK of the ACA public/private key pair.
+ *
+ * @return public key of the attestation certificate authority
+ */
+ byte[] getPublicKey();
+}
diff --git a/HIRS_AttestationCA/src/main/java/hirs/attestationca/CredentialManagementHelper.java b/HIRS_AttestationCA/src/main/java/hirs/attestationca/CredentialManagementHelper.java
new file mode 100644
index 000000000..562387c46
--- /dev/null
+++ b/HIRS_AttestationCA/src/main/java/hirs/attestationca/CredentialManagementHelper.java
@@ -0,0 +1,122 @@
+package hirs.attestationca;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import hirs.data.persist.certificate.EndorsementCredential;
+import hirs.data.persist.certificate.PlatformCredential;
+import hirs.persist.CertificateManager;
+import hirs.persist.DBManagerException;
+
+
+/**
+ * Utility class which includes credential management functions used by the ACA.
+ */
+public final class CredentialManagementHelper {
+ private static final Logger LOG = LogManager.getLogger(CredentialManagementHelper.class);
+
+ private CredentialManagementHelper() {
+
+ }
+
+ /**
+ * Parses and stores the EK in the cert manager. If the cert is already present and archived,
+ * it is unarchived.
+ * @param certificateManager the certificate manager used for storage
+ * @param endorsementBytes the raw EK bytes used for parsing
+ * @return the parsed, valid EK
+ * @throws IllegalArgumentException if the provided bytes are not a valid EK.
+ */
+ public static EndorsementCredential storeEndorsementCredential(
+ final CertificateManager certificateManager,
+ final byte[] endorsementBytes) throws IllegalArgumentException {
+
+ if (null == certificateManager) {
+ throw new IllegalArgumentException("null certificate manager");
+ }
+
+ if (null == endorsementBytes) {
+ throw new IllegalArgumentException("null endorsement credential bytes");
+ }
+
+ if (endorsementBytes.length <= 1) {
+ throw new IllegalArgumentException(
+ String.format("%d-length byte array given for endorsement credential",
+ endorsementBytes.length)
+ );
+ }
+
+ LOG.info("Parsing Endorsement Credential of length " + endorsementBytes.length);
+
+ EndorsementCredential endorsementCredential =
+ EndorsementCredential.parseWithPossibleHeader(endorsementBytes);
+ int certificateHash = endorsementCredential.getCertificateHash();
+ EndorsementCredential existingCredential =
+ EndorsementCredential.select(certificateManager).includeArchived()
+ .byHashCode(certificateHash).getCertificate();
+ if (null == existingCredential) {
+ LOG.info("No Endorsement Credential found with hash: " + certificateHash);
+ return (EndorsementCredential) certificateManager.save(endorsementCredential);
+ } else if (existingCredential.isArchived()) {
+ // if the EK is stored in the DB and it's archived, unarchive.
+ LOG.info("Unarchiving credential");
+ existingCredential.restore();
+ existingCredential.resetCreateTime();
+ certificateManager.update(existingCredential);
+ }
+ return existingCredential;
+ }
+
+ /**
+ * Parses and stores the PC in the cert manager. If the cert is already present and archived,
+ * it is unarchived.
+ * @param certificateManager the certificate manager used for storage
+ * @param platformBytes the raw PC bytes used for parsing
+ * @return the parsed, valid PC, or null if the provided bytes are not a valid EK.
+ */
+ public static PlatformCredential storePlatformCredential(
+ final CertificateManager certificateManager,
+ final byte[] platformBytes) {
+
+ if (null == certificateManager) {
+ throw new IllegalArgumentException("null certificate manager");
+ }
+
+ if (null == platformBytes) {
+ throw new IllegalArgumentException("null platform credential bytes");
+ }
+
+ if (platformBytes.length == 0) {
+ throw new IllegalArgumentException(
+ "zero-length byte array given for platform credential"
+ );
+ }
+
+ LOG.info("Parsing Platform Credential of length " + platformBytes.length);
+ try {
+ PlatformCredential platformCredential =
+ PlatformCredential.parseWithPossibleHeader(platformBytes);
+ if (null == platformCredential) {
+ return null;
+ }
+ PlatformCredential existingCredential =
+ PlatformCredential.select(certificateManager)
+ .byHashCode(platformCredential.getCertificateHash()).getCertificate();
+ if (null == existingCredential) {
+ return (PlatformCredential) certificateManager.save(platformCredential);
+ } else if (existingCredential.isArchived()) {
+ // if the PC is stored in the DB and it's archived, unarchive.
+ LOG.info("Unarchiving credential");
+ existingCredential.restore();
+ certificateManager.update(existingCredential);
+ return existingCredential;
+ }
+
+ return existingCredential;
+ } catch (DBManagerException dbe) {
+ LOG.error("Error retrieving or saving platform credential", dbe);
+ } catch (Exception e) {
+ LOG.error("Error parsing platform credential", e);
+ }
+ return null;
+ }
+}
diff --git a/HIRS_AttestationCA/src/main/java/hirs/attestationca/IdentityProcessingException.java b/HIRS_AttestationCA/src/main/java/hirs/attestationca/IdentityProcessingException.java
new file mode 100644
index 000000000..f455992a1
--- /dev/null
+++ b/HIRS_AttestationCA/src/main/java/hirs/attestationca/IdentityProcessingException.java
@@ -0,0 +1,27 @@
+package hirs.attestationca;
+
+/**
+ * Generic exception thrown while a {@link AttestationCertificateAuthority} is processing a newly
+ * submitted Identity.
+ */
+public class IdentityProcessingException extends RuntimeException {
+ /**
+ * Constructs a generic instance of this exception using the specified reason.
+ *
+ * @param reason for the exception
+ */
+ public IdentityProcessingException(final String reason) {
+ super(reason);
+ }
+
+ /**
+ * Constructs a instance of this exception with the specified reason and backing root
+ * exception.
+ *
+ * @param reason for this exception
+ * @param rootException causing this exception
+ */
+ public IdentityProcessingException(final String reason, final Throwable rootException) {
+ super(reason, rootException);
+ }
+}
diff --git a/HIRS_AttestationCA/src/main/java/hirs/attestationca/InitializationListener.java b/HIRS_AttestationCA/src/main/java/hirs/attestationca/InitializationListener.java
new file mode 100644
index 000000000..2be1d35b4
--- /dev/null
+++ b/HIRS_AttestationCA/src/main/java/hirs/attestationca/InitializationListener.java
@@ -0,0 +1,41 @@
+package hirs.attestationca;
+
+import org.hibernate.SessionFactory;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+import org.springframework.orm.hibernate4.LocalSessionFactoryBean;
+
+import javax.servlet.ServletContextEvent;
+import javax.servlet.ServletContextListener;
+import hirs.persist.DBAppraiserManager;
+import hirs.persist.DBDeviceGroupManager;
+import hirs.persist.DBPolicyManager;
+import hirs.persist.PersistenceConfiguration;
+import hirs.utils.HIRSProfiles;
+
+/**
+ * Simply holds a contextInitialized method which will be called when the web app starts.
+ */
+public class InitializationListener implements ServletContextListener {
+ @Override
+ public void contextInitialized(final ServletContextEvent event) {
+ AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
+ context.getEnvironment().addActiveProfile(HIRSProfiles.SERVER);
+
+ // register the database configuration and refresh the context
+ context.register(PersistenceConfiguration.class);
+ context.refresh();
+
+ // obtain reference to hibernate session factory
+ SessionFactory sessionFactory = context.getBean(LocalSessionFactoryBean.class).getObject();
+ AcaDbInit.insertDefaultEntries(
+ new DBAppraiserManager(sessionFactory),
+ new DBDeviceGroupManager(sessionFactory),
+ new DBPolicyManager(sessionFactory)
+ );
+ }
+
+ @Override
+ public void contextDestroyed(final ServletContextEvent event) {
+
+ }
+}
diff --git a/HIRS_AttestationCA/src/main/java/hirs/attestationca/IssuedCertificateAttributeHelper.java b/HIRS_AttestationCA/src/main/java/hirs/attestationca/IssuedCertificateAttributeHelper.java
new file mode 100644
index 000000000..4c98c3ad1
--- /dev/null
+++ b/HIRS_AttestationCA/src/main/java/hirs/attestationca/IssuedCertificateAttributeHelper.java
@@ -0,0 +1,174 @@
+package hirs.attestationca;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.bouncycastle.asn1.ASN1ObjectIdentifier;
+import org.bouncycastle.asn1.DEROctetString;
+import org.bouncycastle.asn1.DERUTF8String;
+import org.bouncycastle.asn1.x500.AttributeTypeAndValue;
+import org.bouncycastle.asn1.x500.RDN;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.asn1.x500.X500NameBuilder;
+import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
+import org.bouncycastle.asn1.x509.Extension;
+import org.bouncycastle.asn1.x509.Extensions;
+import org.bouncycastle.asn1.x509.GeneralName;
+import org.bouncycastle.asn1.x509.GeneralNames;
+import org.bouncycastle.asn1.x509.GeneralNamesBuilder;
+import org.bouncycastle.asn1.x509.KeyPurposeId;
+import org.bouncycastle.asn1.x509.TBSCertificate;
+import org.bouncycastle.asn1.x509.AttributeCertificateInfo;
+import org.springframework.util.CollectionUtils;
+
+import java.io.IOException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.X509Certificate;
+import java.util.Collection;
+import hirs.data.persist.certificate.EndorsementCredential;
+import hirs.data.persist.certificate.PlatformCredential;
+
+/**
+ * Builds extensions based on Platform and Endorsement credentials to provide in an issued
+ * certificate.
+ */
+public final class IssuedCertificateAttributeHelper {
+
+ private static final String TPM_ID_LABEL_OID = "2.23.133.2.15";
+
+ /**
+ * Object Identifier TCPA at TPM ID Label.
+ */
+ public static final ASN1ObjectIdentifier TCPA_AT_TPM_ID_LABEL =
+ new ASN1ObjectIdentifier(TPM_ID_LABEL_OID);
+ /**
+ * The extended key usage extension.
+ */
+ public static final Extension EXTENDED_KEY_USAGE_EXTENSION;
+ private static final Logger LOG = LogManager.getLogger(IssuedCertificateAttributeHelper.class);
+ private static final ASN1ObjectIdentifier TCG_KP_AIK_CERTIFICATE_ATTRIBUTE =
+ new ASN1ObjectIdentifier("2.23.133.8.3");
+
+ static {
+ // Generates an extension that identifies a cert as an AIK cert
+ Extension extension = null;
+ try {
+ extension = new Extension(Extension.extendedKeyUsage, true,
+ new ExtendedKeyUsage(new KeyPurposeId[] {
+ KeyPurposeId.getInstance(TCG_KP_AIK_CERTIFICATE_ATTRIBUTE)}).getEncoded());
+ } catch (IOException e) {
+ LOG.error("Error generating extended key usage extension");
+ }
+ EXTENDED_KEY_USAGE_EXTENSION = extension;
+ }
+
+ private IssuedCertificateAttributeHelper() {
+ // do not construct publicly
+ }
+
+ /**
+ * Builds the subject alternative name based on the supplied certificates.
+ * @param endorsementCredential the endorsement credential
+ * @param platformCredentials the platform credentials
+ * @param hostName the host name
+ * @return the subject alternative name extension
+ * @throws IOException an IO exception occurs building the extension
+ * @throws IllegalArgumentException if the host name is null
+ */
+ public static Extension buildSubjectAlternativeNameFromCerts(
+ final EndorsementCredential endorsementCredential,
+ final Collection platformCredentials, final String hostName)
+ throws IOException, IllegalArgumentException {
+
+ if (StringUtils.isEmpty(hostName)) {
+ LOG.error("null host name");
+ throw new IllegalArgumentException("must provide host name");
+ }
+
+ // assemble AIK cert SAN, using info from EC and PC
+ X500NameBuilder nameBuilder = new X500NameBuilder();
+ populateEndorsementCredentialAttributes(endorsementCredential, nameBuilder);
+ if (!CollectionUtils.isEmpty(platformCredentials)) {
+ for (PlatformCredential platformCredential : platformCredentials) {
+ populatePlatformCredentialAttributes(platformCredential, nameBuilder);
+ }
+ }
+
+ // add the OID for the TCG-required TPM ID label
+ DERUTF8String idLabel = new DERUTF8String(hostName);
+ nameBuilder.addRDN(new AttributeTypeAndValue(TCPA_AT_TPM_ID_LABEL, idLabel));
+
+ // put everything into the SAN, usable by the certificate builder
+ GeneralNamesBuilder genNamesBuilder = new GeneralNamesBuilder();
+ genNamesBuilder.addName(new GeneralName(nameBuilder.build()));
+ DEROctetString sanContent =
+ new DEROctetString(genNamesBuilder.build().getEncoded());
+ Extension subjectAlternativeName = new Extension(Extension.subjectAlternativeName,
+ true, sanContent);
+
+ return subjectAlternativeName;
+ }
+
+ private static void populatePlatformCredentialAttributes(
+ final PlatformCredential platformCredential,
+ final X500NameBuilder nameBuilder) throws IOException {
+ if (null == platformCredential) {
+ return;
+ }
+
+ final RDN[] rdns;
+ try {
+ LOG.debug("Applying platform credential attributes to SAN");
+ AttributeCertificateInfo platformCredentialAttributeHolders =
+ platformCredential.getAttributeCertificate().getAcinfo();
+ rdns = ((X500Name) GeneralNames.fromExtensions(
+ platformCredentialAttributeHolders.getExtensions(),
+ Extension.subjectAlternativeName).getNames()[0].getName()).getRDNs();
+ } catch (IllegalArgumentException e) {
+ LOG.error("Unable to extract attributes from platform credential", e);
+ return;
+ }
+
+ populateRdnAttributesInNameBuilder(nameBuilder, rdns);
+ }
+
+ private static void populateEndorsementCredentialAttributes(
+ final EndorsementCredential endorsementCredential, final X500NameBuilder nameBuilder) {
+ if (null == endorsementCredential) {
+ return;
+ }
+
+ final RDN[] rdns;
+ try {
+ LOG.debug("Applying endorsement credential attributes to SAN");
+ X509Certificate endorsementX509 = endorsementCredential.getX509Certificate();
+ TBSCertificate tbsCertificate = TBSCertificate.getInstance(
+ endorsementX509.getTBSCertificate());
+ Extensions extensions = tbsCertificate.getExtensions();
+ GeneralNames names = GeneralNames.fromExtensions(extensions,
+ Extension.subjectAlternativeName);
+ if (names != null) {
+ X500Name x500 = (X500Name) names.getNames()[0].getName();
+ rdns = x500.getRDNs();
+ populateRdnAttributesInNameBuilder(nameBuilder, rdns);
+ } else {
+ LOG.error("No RDNs in endorsement credential attributes");
+ return;
+ }
+ } catch (CertificateEncodingException e) {
+ LOG.error("Certificate encoding exception", e);
+ return;
+ } catch (IOException e) {
+ LOG.error("Error creating x509 cert from endorsement credential", e);
+ return;
+ }
+
+ }
+
+ private static void populateRdnAttributesInNameBuilder(final X500NameBuilder nameBuilder,
+ final RDN[] rdns) {
+ for (final RDN rdn : rdns) {
+ nameBuilder.addRDN(rdn.getTypesAndValues()[0]);
+ }
+ }
+}
diff --git a/HIRS_AttestationCA/src/main/java/hirs/attestationca/configuration/AttestationCertificateAuthorityConfiguration.java b/HIRS_AttestationCA/src/main/java/hirs/attestationca/configuration/AttestationCertificateAuthorityConfiguration.java
new file mode 100644
index 000000000..550502a40
--- /dev/null
+++ b/HIRS_AttestationCA/src/main/java/hirs/attestationca/configuration/AttestationCertificateAuthorityConfiguration.java
@@ -0,0 +1,251 @@
+package hirs.attestationca.configuration;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.springframework.beans.factory.BeanInitializationException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.context.annotation.PropertySource;
+import org.springframework.context.annotation.PropertySources;
+import org.springframework.context.annotation.Scope;
+import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
+import org.springframework.core.env.Environment;
+import org.springframework.orm.hibernate4.LocalSessionFactoryBean;
+import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
+import org.springframework.web.servlet.config.annotation.EnableWebMvc;
+import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
+
+import javax.annotation.PostConstruct;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.PrivateKey;
+import java.security.Security;
+import java.security.cert.X509Certificate;
+import hirs.persist.DBDeviceGroupManager;
+import hirs.persist.DBDeviceManager;
+import hirs.persist.DeviceGroupManager;
+import hirs.persist.DeviceManager;
+import hirs.persist.HibernateConfiguration;
+import hirs.structs.converters.SimpleStructConverter;
+import hirs.structs.converters.StructConverter;
+import hirs.utils.LogConfigurationUtil;
+
+/**
+ * Provides application context configuration for the Attestation Certificate Authority
+ * application. The properties are processed in order and as such, the last property file read in
+ * will override properties that may had already been defined previously. In other words, the
+ * 'defaults.properties' file provides a basic standard of properties that can be overrode by the
+ */
+@Configuration
+@PropertySources({
+ @PropertySource(value = "classpath:defaults.properties"),
+
+ // detects if file exists, if not, ignore errors
+ @PropertySource(value = "file:/etc/hirs/aca/aca.properties",
+ ignoreResourceNotFound = true)
+})
+@ComponentScan({ "hirs.attestationca", "hirs.attestationca.service", "hirs.validation",
+ "hirs.data.service" })
+@Import(HibernateConfiguration.class)
+@EnableWebMvc
+public class AttestationCertificateAuthorityConfiguration extends WebMvcConfigurerAdapter {
+
+ private static final Logger LOG =
+ LogManager.getLogger(AttestationCertificateAuthorityConfiguration.class);
+
+ static {
+ try {
+ LogConfigurationUtil.applyConfiguration();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static final String CLIENT_FILES_PATH = "file:/etc/hirs/aca/client-files/";
+
+ @Value("${aca.directories.certificates}")
+ private String certificatesLocation;
+
+ @Value("${aca.keyStore.location}")
+ private String keyStoreLocation;
+
+ @Value("${aca.keyStore.password:''}")
+ private String keyStorePassword;
+
+ @Value("${aca.keyStore.alias}")
+ private String keyAlias;
+
+ @Autowired
+ private Environment environment;
+
+ @Autowired
+ private LocalSessionFactoryBean sessionFactory;
+
+
+ /**
+ * @return bean to resolve injected annotation.Value
+ * property expressions for beans.
+ */
+ @Bean
+ public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
+ return new PropertySourcesPlaceholderConfigurer();
+ }
+
+ /**
+ * Initialization of the ACA. Detects environment and runs configuration methods as required.
+ * This method is intended to be invoked by the Spring application context.
+ */
+ @PostConstruct
+ void initialize() {
+
+ // ensure that Bouncy Castle is registered as a security provider
+ Security.addProvider(new BouncyCastleProvider());
+
+ // obtain path to ACA configuration
+ Path certificatesPath = Paths.get(certificatesLocation);
+
+ // create base directories if they do not exist
+ try {
+ Files.createDirectories(certificatesPath);
+ } catch (IOException e) {
+ throw new BeanInitializationException(
+ "Encountered error while initializing ACA directories: " + e.getMessage(), e);
+ }
+
+ // create the ACA key store if it doesn't exist
+ Path keyStorePath = Paths.get(keyStoreLocation);
+ if (!Files.exists(keyStorePath)) {
+ throw new IllegalStateException(
+ String.format("ACA Key Store not found at %s. Consult the HIRS User "
+ + "Guide for ACA installation instructions.", keyStoreLocation));
+ }
+ }
+
+ /**
+ * @return the {@link PrivateKey} of the ACA
+ */
+ @Bean
+ public PrivateKey privateKey() {
+
+ // obtain the key store
+ KeyStore keyStore = keyStore();
+
+ try {
+
+ // load the key from the key store
+ PrivateKey acaKey = (PrivateKey) keyStore.getKey(keyAlias,
+ keyStorePassword.toCharArray());
+
+ // break early if the certificate is not available.
+ if (acaKey == null) {
+ throw new BeanInitializationException(String.format("Key with alias "
+ + "%s was not in KeyStore %s. Ensure that the KeyStore has the "
+ + "specified certificate. ", keyAlias, keyStoreLocation));
+ }
+ return acaKey;
+ } catch (Exception e) {
+ throw new BeanInitializationException("Encountered error loading ACA private key "
+ + "from key store: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * @return the {@link X509Certificate} of the ACA
+ */
+ @Bean
+ public X509Certificate acaCertificate() {
+ KeyStore keyStore = keyStore();
+
+ try {
+ X509Certificate acaCertificate = (X509Certificate) keyStore.getCertificate(keyAlias);
+
+ // break early if the certificate is not available.
+ if (acaCertificate == null) {
+ throw new BeanInitializationException(String.format("Certificate with alias "
+ + "%s was not in KeyStore %s. Ensure that the KeyStore has the "
+ + "specified certificate. ", keyAlias, keyStoreLocation));
+ }
+
+ return acaCertificate;
+ } catch (KeyStoreException e) {
+ throw new BeanInitializationException("Encountered error loading ACA certificate "
+ + "from key store: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * @return the {@link java.security.KeyStore} that contains the certificates for the ACA.
+ */
+ @Bean
+ public KeyStore keyStore() {
+ Path keyStorePath = Paths.get(keyStoreLocation);
+
+ // attempt to open the key store. if that fails, log a meaningful message before failing.
+ try {
+ KeyStore keyStore = KeyStore.getInstance("JKS");
+ keyStore.load(Files.newInputStream(keyStorePath), keyStorePassword.toCharArray());
+ return keyStore;
+ } catch (Exception e) {
+ LOG.error(String.format(
+ "Encountered error while loading ACA key store. The most common issue is "
+ + "that configured password does not work on the configured key"
+ + " store %s.", keyStorePath));
+ LOG.error(String.format("Exception message: %s", e.getMessage()));
+ throw new BeanInitializationException(e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Prototyped {@link StructConverter}. In other words, all instances returned by this method
+ * will be configured identically, but subsequent invocations will return a new instance.
+ *
+ * @return ready to use {@link StructConverter}.
+ */
+ @Bean
+ @Scope("prototype")
+ public static StructConverter structConverter() {
+ return new SimpleStructConverter();
+ }
+
+ /**
+ * Creates a {@link DeviceGroupManager} ready to use.
+ *
+ * @return {@link DeviceGroupManager}
+ */
+ @Bean
+ public DeviceGroupManager deviceGroupManager() {
+ return new DBDeviceGroupManager(sessionFactory.getObject());
+ }
+
+ /**
+ * Creates a {@link DeviceManager} ready to use.
+ *
+ * @return {@link DeviceManager}
+ */
+ @Bean
+ public DeviceManager deviceManager() {
+ return new DBDeviceManager(sessionFactory.getObject());
+ }
+
+ @Override
+ public void addResourceHandlers(final ResourceHandlerRegistry resourceHandlerRegistry) {
+ resourceHandlerRegistry.addResourceHandler("/client-files/**")
+ .addResourceLocations(CLIENT_FILES_PATH);
+ }
+
+ @Override
+ public void configureDefaultServletHandling(final DefaultServletHandlerConfigurer configurer) {
+ configurer.enable();
+ }
+
+}
diff --git a/HIRS_AttestationCA/src/main/java/hirs/attestationca/configuration/package-info.java b/HIRS_AttestationCA/src/main/java/hirs/attestationca/configuration/package-info.java
new file mode 100644
index 000000000..3a01817c3
--- /dev/null
+++ b/HIRS_AttestationCA/src/main/java/hirs/attestationca/configuration/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Includes the Spring Framework application context configuration classes.
+ */
+package hirs.attestationca.configuration;
diff --git a/HIRS_AttestationCA/src/main/java/hirs/attestationca/package-info.java b/HIRS_AttestationCA/src/main/java/hirs/attestationca/package-info.java
new file mode 100644
index 000000000..2ff65a17d
--- /dev/null
+++ b/HIRS_AttestationCA/src/main/java/hirs/attestationca/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * Base package that includes common exceptions, interfaces and base implementations for and related
+ * to the ACA.
+ */
+package hirs.attestationca;
diff --git a/HIRS_AttestationCA/src/main/java/hirs/attestationca/rest/RestfulAttestationCertificateAuthority.java b/HIRS_AttestationCA/src/main/java/hirs/attestationca/rest/RestfulAttestationCertificateAuthority.java
new file mode 100644
index 000000000..10ba5ef1b
--- /dev/null
+++ b/HIRS_AttestationCA/src/main/java/hirs/attestationca/rest/RestfulAttestationCertificateAuthority.java
@@ -0,0 +1,145 @@
+package hirs.attestationca.rest;
+
+import hirs.attestationca.IdentityProcessingException;
+import hirs.persist.DBManager;
+import hirs.persist.TPM2ProvisionerState;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import hirs.attestationca.AbstractAttestationCertificateAuthority;
+import hirs.attestationca.service.SupplyChainValidationService;
+import hirs.data.service.DeviceRegister;
+import hirs.persist.CertificateManager;
+import hirs.persist.DeviceManager;
+import hirs.structs.converters.StructConverter;
+
+/**
+ * Restful implementation of the {@link hirs.attestationca.AttestationCertificateAuthority}.
+ * Exposes the ACA methods as REST endpoints.
+ */
+@RestController
+@RequestMapping("/")
+public class RestfulAttestationCertificateAuthority
+ extends AbstractAttestationCertificateAuthority {
+
+ /**
+ * Constructor.
+ * @param supplyChainValidationService the supply chain service
+ * @param privateKey the ACA private key
+ * @param acaCertificate the ACA certificate
+ * @param structConverter the struct converter
+ * @param certificateManager the certificate manager
+ * @param deviceRegister the device register
+ * @param validDays the number of days issued certs are valid
+ * @param deviceManager the device manager
+ * @param tpm2ProvisionerStateDBManager the DBManager for persisting provisioner state
+ */
+ @SuppressWarnings({ "checkstyle:parameternumber" })
+ @Autowired
+ public RestfulAttestationCertificateAuthority(
+ final SupplyChainValidationService supplyChainValidationService,
+ final PrivateKey privateKey, final X509Certificate acaCertificate,
+ final StructConverter structConverter,
+ final CertificateManager certificateManager,
+ final DeviceRegister deviceRegister,
+ final DeviceManager deviceManager,
+ final DBManager tpm2ProvisionerStateDBManager,
+ @Value("${aca.certificates.validity}") final int validDays) {
+ super(supplyChainValidationService, privateKey, acaCertificate, structConverter,
+ certificateManager, deviceRegister, validDays, deviceManager,
+ tpm2ProvisionerStateDBManager);
+ }
+
+ /*
+ * (non-javadoc)
+ *
+ * Wrap the {@link AbstractAttestationCertificateAuthority#processIdentityRequest(byte[])}
+ * with a Spring {@link RequestMapping}. Effectively, this method then will allow spring to
+ * serialize and deserialize the request and responses on method invocation and
+ * return, respectively.
+ */
+ @Override
+ @ResponseBody
+ @RequestMapping(value = "/identity-request/process", method = RequestMethod.POST,
+ consumes = MediaType.APPLICATION_OCTET_STREAM_VALUE,
+ produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
+ public byte[] processIdentityRequest(@RequestBody final byte[] request) {
+ return super.processIdentityRequest(request);
+ }
+
+ /**
+ * Listener for identity requests from TPM 2.0 provisioning.
+ * @param request The request object from the provisioner.
+ * @return The response to the provisioner.
+ */
+ @Override
+ @ResponseBody
+ @RequestMapping(value = "/identity-claim-tpm2/process",
+ method = RequestMethod.POST,
+ consumes = MediaType.APPLICATION_OCTET_STREAM_VALUE,
+ produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
+ public byte[] processIdentityClaimTpm2(@RequestBody final byte[] request) {
+ return super.processIdentityClaimTpm2(request);
+ }
+
+ /**
+ * Endpoint for processing certificate requests for TPM 2.0 provisioning.
+ *
+ * @param request The credential request from the client provisioner.
+ * @return The response to the client provisioner.
+ */
+ @Override
+ @ResponseBody
+ @RequestMapping(value = "/request-certificate-tpm2",
+ method = RequestMethod.POST,
+ consumes = MediaType.APPLICATION_OCTET_STREAM_VALUE,
+ produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
+ public byte[] processCertificateRequest(@RequestBody final byte[] request) {
+ return super.processCertificateRequest(request);
+ }
+
+ /*
+ * (non-javadoc)
+ *
+ * Wrap the {@link AbstractAttestationCertificateAuthority#getPublicKey()} with a Spring
+ * {@link RequestMapping} such that Spring can serialize the certificate to be returned to an
+ * HTTP Request.
+ */
+ @Override
+ @ResponseBody
+ @RequestMapping(value = "/public-key", method = RequestMethod.GET,
+ produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
+ public byte[] getPublicKey() {
+ return super.getPublicKey();
+ }
+
+ /**
+ * Handle processing of exceptions for ACA REST API.
+ * @param e exception thrown during invocation of ACA REST API
+ * @return exception thrown during invocation of ACA REST API
+ */
+ @ExceptionHandler
+ @ResponseBody
+ @ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR)
+ public Exception handleException(final Exception e) {
+ if (e instanceof IdentityProcessingException) {
+ LOG.error("Processing exception while provisioning", e.getMessage(), e);
+ } else {
+ LOG.error(String.format("Encountered unexpected error while processing identity "
+ + "claim: %s", e.getMessage()), e);
+ }
+ return e;
+ }
+
+}
diff --git a/HIRS_AttestationCA/src/main/java/hirs/attestationca/rest/package-info.java b/HIRS_AttestationCA/src/main/java/hirs/attestationca/rest/package-info.java
new file mode 100644
index 000000000..f64c15451
--- /dev/null
+++ b/HIRS_AttestationCA/src/main/java/hirs/attestationca/rest/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * RESTful implementations of the {@link hirs.attestationca.AttestationCertificateAuthority}.
+ */
+package hirs.attestationca.rest;
diff --git a/HIRS_AttestationCA/src/main/java/hirs/attestationca/service/SupplyChainValidationService.java b/HIRS_AttestationCA/src/main/java/hirs/attestationca/service/SupplyChainValidationService.java
new file mode 100644
index 000000000..f3d6de859
--- /dev/null
+++ b/HIRS_AttestationCA/src/main/java/hirs/attestationca/service/SupplyChainValidationService.java
@@ -0,0 +1,28 @@
+package hirs.attestationca.service;
+
+import java.util.Set;
+import hirs.data.persist.Device;
+import hirs.data.persist.SupplyChainValidationSummary;
+import hirs.data.persist.certificate.EndorsementCredential;
+import hirs.data.persist.certificate.PlatformCredential;
+
+
+/**
+ * Interface defining a component that will perform supply chain validations, which yields a
+ * {@link SupplyChainValidationSummary}.
+ */
+public interface SupplyChainValidationService {
+ /**
+ * The "main" method of supply chain validation. Takes the credentials from an identity
+ * request and validates the supply chain in accordance to the current supply chain
+ * policy.
+ *
+ * @param ec The endorsement credential from the identity request.
+ * @param pc The set of platform credentials from the identity request.
+ * @param device The device to be validated.
+ * @return True if validation is successful, false otherwise.
+ */
+ SupplyChainValidationSummary validateSupplyChain(EndorsementCredential ec,
+ Set pc,
+ Device device);
+}
diff --git a/HIRS_AttestationCA/src/main/java/hirs/attestationca/service/SupplyChainValidationServiceImpl.java b/HIRS_AttestationCA/src/main/java/hirs/attestationca/service/SupplyChainValidationServiceImpl.java
new file mode 100644
index 000000000..b820da03e
--- /dev/null
+++ b/HIRS_AttestationCA/src/main/java/hirs/attestationca/service/SupplyChainValidationServiceImpl.java
@@ -0,0 +1,373 @@
+package hirs.attestationca.service;
+
+import java.io.IOException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateException;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Import;
+import org.springframework.stereotype.Service;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+import org.apache.logging.log4j.Level;
+import hirs.appraiser.Appraiser;
+import hirs.appraiser.SupplyChainAppraiser;
+import hirs.data.persist.AppraisalStatus;
+import hirs.data.persist.Device;
+import hirs.data.persist.DeviceInfoReport;
+import hirs.data.persist.SupplyChainPolicy;
+import hirs.data.persist.SupplyChainValidation;
+import hirs.data.persist.SupplyChainValidationSummary;
+import hirs.data.persist.certificate.Certificate;
+import hirs.data.persist.certificate.CertificateAuthorityCredential;
+import hirs.data.persist.certificate.EndorsementCredential;
+import hirs.data.persist.certificate.PlatformCredential;
+import hirs.persist.AppraiserManager;
+import hirs.persist.CertificateManager;
+import hirs.persist.CertificateSelector;
+import hirs.persist.CrudManager;
+import hirs.persist.DBManagerException;
+import hirs.persist.PersistenceConfiguration;
+import hirs.persist.PolicyManager;
+import hirs.validation.CredentialValidator;
+
+/**
+ * The main executor of supply chain verification tasks. The AbstractAttestationCertificateAuthority
+ * will feed it the PC, EC, other relevant certificates, and serial numbers of the provisioning
+ * task, and it will then manipulate the data as necessary, retrieve useful certs, and arrange
+ * for actual validation by the SupplyChainValidator.
+ */
+@Service
+@Import(PersistenceConfiguration.class)
+public class SupplyChainValidationServiceImpl implements SupplyChainValidationService {
+
+ private PolicyManager policyManager;
+ private AppraiserManager appraiserManager;
+ private CertificateManager certificateManager;
+ private CredentialValidator supplyChainCredentialValidator;
+ private CrudManager supplyChainValidatorSummaryManager;
+
+ private static final Logger LOGGER =
+ LogManager.getLogger(SupplyChainValidationServiceImpl.class);
+
+
+ /**
+ * Constructor.
+ * @param policyManager the policy manager
+ * @param appraiserManager the appraiser manager
+ * @param certificateManager the cert manager
+ * @param supplyChainValidatorSummaryManager the summary manager
+ * @param supplyChainCredentialValidator the credential validator
+ */
+ @Autowired
+ public SupplyChainValidationServiceImpl(final PolicyManager policyManager,
+ final AppraiserManager appraiserManager,
+ final CertificateManager certificateManager,
+ final CrudManager supplyChainValidatorSummaryManager,
+ final CredentialValidator supplyChainCredentialValidator) {
+ this.policyManager = policyManager;
+ this.appraiserManager = appraiserManager;
+ this.certificateManager = certificateManager;
+ this.supplyChainValidatorSummaryManager = supplyChainValidatorSummaryManager;
+ this.supplyChainCredentialValidator = supplyChainCredentialValidator;
+ }
+
+ /**
+ * The "main" method of supply chain validation. Takes the credentials from an identity
+ * request and validates the supply chain in accordance to the current supply chain
+ * policy.
+ *
+ * @param ec The endorsement credential from the identity request.
+ * @param pcs The platform credentials from the identity request.
+ * @param device The device to be validated.
+ * @return A summary of the validation results.
+ */
+ @Override
+ public SupplyChainValidationSummary validateSupplyChain(final EndorsementCredential ec,
+ final Set pcs,
+ final Device device) {
+ final Appraiser supplyChainAppraiser = appraiserManager.getAppraiser(
+ SupplyChainAppraiser.NAME);
+ SupplyChainPolicy policy = (SupplyChainPolicy) policyManager.getDefaultPolicy(
+ supplyChainAppraiser);
+ boolean acceptExpiredCerts = policy.isExpiredCertificateValidationEnabled();
+
+ List validations = new ArrayList<>();
+
+ // validate all supply chain pieces. Potentially, a policy setting could be made
+ // to dictate stopping after the first validation failure.
+
+ // Validate the Endorsement Credential
+ if (policy.isEcValidationEnabled()) {
+ validations.add(validateEndorsementCredential(ec, acceptExpiredCerts));
+ // store the device with the credential
+ if (null != ec) {
+ ec.setDevice(device);
+ this.certificateManager.update(ec);
+ }
+ }
+
+ // Validate Platform Credential signatures
+ if (policy.isPcValidationEnabled()) {
+ // Ensure there are platform credentials to validate
+ if (pcs == null || pcs.isEmpty()) {
+ LOGGER.error("There were no Platform Credentials to validate.");
+ validations.add(buildValidationRecord(
+ SupplyChainValidation.ValidationType.PLATFORM_CREDENTIAL,
+ AppraisalStatus.Status.FAIL,
+ "Platform credential(s) missing", null, Level.ERROR));
+ } else {
+ Iterator it = pcs.iterator();
+ while (it.hasNext()) {
+ PlatformCredential pc = it.next();
+ KeyStore trustedCa = getCaChain(pc);
+ validations.add(validatePlatformCredential(pc, trustedCa, acceptExpiredCerts));
+ if (null != pc) {
+ pc.setDevice(device);
+ this.certificateManager.update(pc);
+ }
+ }
+ }
+ }
+
+ // Validate Platform Credential attributes
+ if (policy.isPcAttributeValidationEnabled()) {
+ // Ensure there are platform credentials to validate
+ if (pcs == null || pcs.isEmpty()) {
+ LOGGER.error("There were no Platform Credentials to validate attributes.");
+ validations.add(buildValidationRecord(
+ SupplyChainValidation.ValidationType.PLATFORM_CREDENTIAL_ATTRIBUTES,
+ AppraisalStatus.Status.FAIL,
+ "Platform credential(s) missing. Cannot validate attributes",
+ null, Level.ERROR));
+ } else {
+ Iterator it = pcs.iterator();
+ while (it.hasNext()) {
+ PlatformCredential pc = it.next();
+ validations.add(validatePlatformCredentialAttributes(pc, device.getDeviceInfo(),
+ ec));
+ if (null != pc) {
+ pc.setDevice(device);
+ this.certificateManager.update(pc);
+ }
+ }
+ }
+ }
+
+ // Generate validation summary, save it, and return it.
+ SupplyChainValidationSummary summary =
+ new SupplyChainValidationSummary(device, validations);
+ try {
+ supplyChainValidatorSummaryManager.save(summary);
+ } catch (DBManagerException ex) {
+ LOGGER.error("Failed to save Supply chain summary", ex);
+ }
+ return summary;
+ }
+
+ private SupplyChainValidation validateEndorsementCredential(final EndorsementCredential ec,
+ final boolean acceptExpiredCerts) {
+ final SupplyChainValidation.ValidationType validationType
+ = SupplyChainValidation.ValidationType.ENDORSEMENT_CREDENTIAL;
+ LOGGER.info("Validating endorsement credential");
+ if (ec == null) {
+ LOGGER.error("No endorsement credential to validate");
+ return buildValidationRecord(validationType,
+ AppraisalStatus.Status.FAIL, "Endorsement credential is missing",
+ null, Level.ERROR);
+ }
+
+ KeyStore ecStore = getCaChain(ec);
+ AppraisalStatus result = supplyChainCredentialValidator.
+ validateEndorsementCredential(ec, ecStore, acceptExpiredCerts);
+ switch (result.getAppStatus()) {
+ case PASS:
+ return buildValidationRecord(validationType, AppraisalStatus.Status.PASS,
+ result.getMessage(), ec, Level.INFO);
+ case FAIL:
+ return buildValidationRecord(validationType, AppraisalStatus.Status.FAIL,
+ result.getMessage(), ec, Level.WARN);
+ case ERROR:
+ return buildValidationRecord(validationType, AppraisalStatus.Status.ERROR,
+ result.getMessage(), ec, Level.ERROR);
+ default:
+ return buildValidationRecord(validationType, AppraisalStatus.Status.ERROR,
+ result.getMessage(), ec, Level.ERROR);
+ }
+ }
+
+ private SupplyChainValidation validatePlatformCredential(final PlatformCredential pc,
+ final KeyStore
+ trustedCertificateAuthority,
+ final boolean acceptExpiredCerts) {
+ final SupplyChainValidation.ValidationType validationType
+ = SupplyChainValidation.ValidationType.PLATFORM_CREDENTIAL;
+
+ if (pc == null) {
+ LOGGER.error("No platform credential to validate");
+ return buildValidationRecord(validationType,
+ AppraisalStatus.Status.FAIL, "Empty Platform credential", null, Level.ERROR);
+ }
+ LOGGER.info("Validating Platform Credential");
+ AppraisalStatus result = supplyChainCredentialValidator.validatePlatformCredential(pc,
+ trustedCertificateAuthority, acceptExpiredCerts);
+ switch (result.getAppStatus()) {
+ case PASS:
+ return buildValidationRecord(validationType, AppraisalStatus.Status.PASS,
+ result.getMessage(), pc, Level.INFO);
+ case FAIL:
+ return buildValidationRecord(validationType, AppraisalStatus.Status.FAIL,
+ result.getMessage(), pc, Level.WARN);
+ case ERROR:
+ return buildValidationRecord(validationType, AppraisalStatus.Status.ERROR,
+ result.getMessage(), pc, Level.ERROR);
+ default:
+ return buildValidationRecord(validationType, AppraisalStatus.Status.ERROR,
+ result.getMessage(), pc, Level.ERROR);
+ }
+ }
+
+ private SupplyChainValidation validatePlatformCredentialAttributes(final PlatformCredential pc,
+ final DeviceInfoReport deviceInfoReport,
+ final EndorsementCredential ec) {
+ final SupplyChainValidation.ValidationType validationType
+ = SupplyChainValidation.ValidationType.PLATFORM_CREDENTIAL_ATTRIBUTES;
+
+ if (pc == null) {
+ LOGGER.error("No platform credential to validate");
+ return buildValidationRecord(validationType,
+ AppraisalStatus.Status.FAIL, "Platform credential is missing",
+ null, Level.ERROR);
+ }
+ LOGGER.info("Validating platform credential attributes");
+ AppraisalStatus result = supplyChainCredentialValidator.
+ validatePlatformCredentialAttributes(pc, deviceInfoReport, ec);
+ switch (result.getAppStatus()) {
+ case PASS:
+ return buildValidationRecord(validationType, AppraisalStatus.Status.PASS,
+ result.getMessage(), pc, Level.INFO);
+ case FAIL:
+ return buildValidationRecord(validationType, AppraisalStatus.Status.FAIL,
+ result.getMessage(), pc, Level.WARN);
+ case ERROR:
+ return buildValidationRecord(validationType, AppraisalStatus.Status.ERROR,
+ result.getMessage(), pc, Level.ERROR);
+ default:
+ return buildValidationRecord(validationType, AppraisalStatus.Status.ERROR,
+ result.getMessage(), pc, Level.ERROR);
+ }
+ }
+
+ /**
+ * Creates a supply chain validation record and logs the validation
+ * message at the specified log level.
+ * @param validationType the type of validation
+ * @param result the appraisal status
+ * @param message the validation message to include in the summary and log
+ * @param certificate the certificate associated with the validation
+ * @param logLevel the log level
+ * @return a SupplyChainValidation
+ */
+ private SupplyChainValidation buildValidationRecord(
+ final SupplyChainValidation.ValidationType validationType,
+ final AppraisalStatus.Status result, final String message,
+ final Certificate certificate, final Level logLevel) {
+
+ List certificateList = new ArrayList<>();
+ if (null != certificate) {
+ certificateList.add(certificate);
+ }
+
+ LOGGER.log(logLevel, message);
+ return new SupplyChainValidation(validationType, result, certificateList, message);
+ }
+
+ /**
+ * This method is used to retrieve the entire CA chain (up to a
+ * trusted self-signed certificate) for the given certificate. This method will look up
+ * CA certificates that have a matching issuer organization as the given certificate, and will
+ * perform that operation recursively until all certificates for all relevant organizations
+ * have been retrieved. For that reason, the returned set of certificates may be larger
+ * than the the single trust chain for the queried certificate, but is guaranteed to include
+ * the trust chain if it exists in this class' CertificateManager.
+ * Returns the certificate authority credentials in a KeyStore.
+ *
+ * @param credential the credential whose CA chain should be retrieved
+ * @return A keystore ontaining all relevant CA credentials to the given certificate's
+ * organization
+ */
+ public KeyStore getCaChain(final Certificate credential) {
+ KeyStore caKeyStore = null;
+ try {
+ caKeyStore = caCertSetToKeystore(getCaChainRec(credential, Collections.emptySet()));
+ } catch (KeyStoreException | IOException e) {
+ LOGGER.error("Unable to assemble CA keystore", e);
+ }
+ return caKeyStore;
+ }
+
+ /**
+ * This is a recursive method which is used to retrieve the entire CA chain (up to a
+ * trusted self-signed certificate) for the given certificate. This method will look up
+ * CA certificates that have a matching issuer organization as the given certificate, and will
+ * perform that operation recursively until all certificates for all relevant organizations
+ * have been retrieved. For that reason, the returned set of certificates may be larger
+ * than the the single trust chain for the queried certificate, but is guaranteed to include
+ * the trust chain if it exists in this class' CertificateManager.
+ *
+ * Implementation notes:
+ * 1. Queries for CA certs with a subject org matching the given (argument's) issuer org
+ * 2. Add that org to queriedOrganizations, so we don't search for that organization again
+ * 3. For each returned CA cert, add that cert to the result set, and recurse with that as the
+ * argument (to go up the chain), if and only if we haven't already queried for that
+ * organization (which prevents infinite loops on certs with an identical subject and
+ * issuer org)
+ *
+ * @param credential the credential whose CA chain should be retrieved
+ * @param previouslyQueriedOrganizations a list of organizations to refrain from querying
+ * @return a Set containing all relevant CA credentials to the given certificate's organization
+ */
+ private Set getCaChainRec(
+ final Certificate credential,
+ final Set previouslyQueriedOrganizations
+ ) {
+ CertificateSelector caSelector =
+ CertificateAuthorityCredential.select(certificateManager)
+ .bySubjectOrganization(credential.getIssuerOrganization());
+ Set certAuthsWithMatchingOrg = caSelector.getCertificates();
+
+ Set queriedOrganizations = new HashSet<>(previouslyQueriedOrganizations);
+ queriedOrganizations.add(credential.getIssuerOrganization());
+
+ HashSet caCreds = new HashSet<>();
+ for (CertificateAuthorityCredential cred : certAuthsWithMatchingOrg) {
+ caCreds.add(cred);
+ if (!queriedOrganizations.contains(cred.getIssuerOrganization())) {
+ caCreds.addAll(getCaChainRec(cred, queriedOrganizations));
+ }
+ }
+ return caCreds;
+ }
+
+ private KeyStore caCertSetToKeystore(final Set certs)
+ throws KeyStoreException, IOException {
+ KeyStore keyStore = KeyStore.getInstance("JKS");
+ try {
+ keyStore.load(null, "".toCharArray());
+ for (Certificate cert : certs) {
+ keyStore.setCertificateEntry(cert.getId().toString(), cert.getX509Certificate());
+ }
+ } catch (IOException | CertificateException | NoSuchAlgorithmException e) {
+ throw new IOException("Could not create and populate keystore", e);
+ }
+
+ return keyStore;
+ }
+}
diff --git a/HIRS_AttestationCA/src/main/java/hirs/attestationca/service/package-info.java b/HIRS_AttestationCA/src/main/java/hirs/attestationca/service/package-info.java
new file mode 100644
index 000000000..594a5e064
--- /dev/null
+++ b/HIRS_AttestationCA/src/main/java/hirs/attestationca/service/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * Contains the main functionality of the SupplyChainValidationService. Executes the actual
+ * validation based on the current supply chain policy.
+ */
+package hirs.attestationca.service;
diff --git a/HIRS_AttestationCA/src/main/resources/applicationContext.xml b/HIRS_AttestationCA/src/main/resources/applicationContext.xml
new file mode 100644
index 000000000..cab9bf305
--- /dev/null
+++ b/HIRS_AttestationCA/src/main/resources/applicationContext.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
diff --git a/HIRS_AttestationCA/src/main/resources/defaults.properties b/HIRS_AttestationCA/src/main/resources/defaults.properties
new file mode 100644
index 000000000..6983bf866
--- /dev/null
+++ b/HIRS_AttestationCA/src/main/resources/defaults.properties
@@ -0,0 +1,37 @@
+# General notes:
+# Properties are processed using an expression processor. That said, properties can inherit the
+# values of other properties.
+#
+# In example the processor will resolve the value of aca.both as 'hello world!'.
+# aca.hello = hello
+# aca.world = world!
+# aca.both = ${aca.hello} ${aca.world}
+#
+# ACA Directories
+# root: the root directory of ACA related files
+# certificates: the directory for ACA certificate files
+aca.directories.root = /etc/hirs/aca
+aca.directories.certificates = ${aca.directories.root}/certificates
+
+# ACA certificate related properties. These are generic properties that apply to the creation of
+# any certificate that the ACA is responsible for creating.
+# validity: the number of days that credentials generated by the ACA are valid.
+aca.certificates.validity = 3652
+
+# ACA key store properties
+# alias: the alias to reference the ACA key and certificate by
+# location: the absolute path to the ACA key store.
+# password: key store password
+aca.keyStore.alias = HIRS_ACA_KEY
+aca.keyStore.location = ${aca.directories.certificates}/keyStore.jks
+aca.keyStore.password =
+
+# ACA setup/initialization properties. These properties are used exclusively by the ACA
+# initialization process. Generally these properties do not need to be modified
+#
+# keySize: the default key size of the ACA key pair stored within the trust store
+# subjectName: the CN of the generate X509 certificate
+# expiration: the number of days that the generated X509 certificate will expire
+aca.setup.keyStore.keySize = 2048
+aca.setup.keyStore.subjectName = HIRS_AttestationCA_Endorsement
+aca.setup.keyStore.expiration = ${aca.certificates.validity}
diff --git a/HIRS_AttestationCA/src/main/resources/log4j2.xml b/HIRS_AttestationCA/src/main/resources/log4j2.xml
new file mode 100644
index 000000000..e68f1a725
--- /dev/null
+++ b/HIRS_AttestationCA/src/main/resources/log4j2.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+ %d{yyyy-MM-dd HH:mm:ss} [%C.%M] %-5p : %m%n
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/HIRS_AttestationCA/src/main/resources/setup.properties b/HIRS_AttestationCA/src/main/resources/setup.properties
new file mode 100644
index 000000000..c017806f7
--- /dev/null
+++ b/HIRS_AttestationCA/src/main/resources/setup.properties
@@ -0,0 +1,8 @@
+#used in the initial setup of the client-files properties files upon WAR deployment
+
+attestationca.url=https://***replace***:8443/HIRS_AttestationCA
+attestationca.cert.subjectname=HIRS_AttestationCA
+attestationca.cert.password=***replace***
+endorsementca.cert.subjectname=HIRS_AttestationCA_Endorsement
+cert.validitydays = 3652
+aik.auth=0000000000000000000000000000000000000000
diff --git a/HIRS_AttestationCA/src/main/webapp/WEB-INF/web.xml b/HIRS_AttestationCA/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000..d2715d51f
--- /dev/null
+++ b/HIRS_AttestationCA/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,43 @@
+
+
+
+ HIRS Attestation Certificate Authority
+
+
+
+ log4j.configurationFile
+ classpath:log4j2.xml
+
+
+
+
+ log4jExposeWebAppRoot
+ false
+
+
+
+
+ dispatcher
+ org.springframework.web.servlet.DispatcherServlet
+
+ contextConfigLocation
+ classpath:applicationContext.xml
+
+
+
+
+
+ dispatcher
+ /
+
+
+
+ hirs.attestationca.InitializationListener
+
+
+
+
diff --git a/HIRS_AttestationCA/src/test/java/hirs/attestationca/AbstractAttestationCertificateAuthorityTest.java b/HIRS_AttestationCA/src/test/java/hirs/attestationca/AbstractAttestationCertificateAuthorityTest.java
new file mode 100644
index 000000000..61b18e5c4
--- /dev/null
+++ b/HIRS_AttestationCA/src/test/java/hirs/attestationca/AbstractAttestationCertificateAuthorityTest.java
@@ -0,0 +1,904 @@
+package hirs.attestationca;
+
+import com.google.protobuf.ByteString;
+import hirs.utils.HexUtils;
+import org.apache.commons.codec.binary.Hex;
+import org.apache.commons.lang3.ArrayUtils;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.asn1.x509.Extension;
+import org.bouncycastle.asn1.x509.GeneralNames;
+import org.bouncycastle.asn1.x509.TBSCertificate;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.springframework.test.util.ReflectionTestUtils;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeSuite;
+import org.testng.annotations.BeforeTest;
+import org.testng.annotations.Test;
+
+import javax.crypto.Cipher;
+import javax.crypto.KeyGenerator;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.OAEPParameterSpec;
+import javax.crypto.spec.PSource;
+import javax.crypto.spec.SecretKeySpec;
+import javax.security.auth.x500.X500Principal;
+import java.io.IOException;
+import java.math.BigInteger;
+import java.net.URISyntaxException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.SecureRandom;
+import java.security.Security;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.RSAPublicKey;
+import java.security.spec.MGF1ParameterSpec;
+import java.util.Calendar;
+
+import hirs.structs.converters.StructConverter;
+import hirs.structs.elements.aca.SymmetricAttestation;
+import hirs.structs.elements.tpm.AsymmetricKeyParams;
+import hirs.structs.elements.tpm.AsymmetricPublicKey;
+import hirs.structs.elements.tpm.EncryptionScheme;
+import hirs.structs.elements.tpm.IdentityProof;
+import hirs.structs.elements.tpm.IdentityRequest;
+import hirs.structs.elements.tpm.StorePubKey;
+import hirs.structs.elements.tpm.SymmetricKey;
+import hirs.structs.elements.tpm.SymmetricKeyParams;
+import hirs.structs.elements.tpm.SymmetricSubParams;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+
+/**
+ * Test suite for {@link AbstractAttestationCertificateAuthority}.
+ */
+public class AbstractAttestationCertificateAuthorityTest {
+
+ // object in test
+ private AbstractAttestationCertificateAuthority aca;
+
+ private static final String EK_PUBLIC_PATH = "/tpm2/ek.pub";
+ private static final String AK_PUBLIC_PATH = "/tpm2/ak.pub";
+ private static final String AK_NAME_PATH = "/tpm2/ak.name";
+ private static final String TEST_NONCE_BLOB_PATH = "test/nonce.blob";
+ private static final String EK_MODULUS_HEX = "a3 b5 c2 1c 57 be 40 c4 3c 78 90 0d 00 81 01 78"
+ + "13 ca 02 ec b6 75 89 60 ca 60 9b 10 b6 b4 d0 0b"
+ + "4d e4 68 ad 01 a6 91 e2 56 20 5e cf 16 fe 77 ae"
+ + "1f 13 d7 ac a1 91 0b 68 f6 07 cf c2 4b 5e c1 2c"
+ + "4c fe 3a c9 62 7e 10 02 5b 33 c8 c2 1a cd 2e 7f"
+ + "dd 7c 43 ac a9 5f b1 d6 07 56 4f 72 9b 0a 00 6c"
+ + "f6 8d 23 a1 84 ca c1 7f 5a 8b ef 0e 23 11 90 00"
+ + "30 f2 99 e9 94 59 c6 b0 fe b2 5c 0c c7 b4 76 69"
+ + "6c f1 b7 d8 e5 60 d6 61 9f ab 7c 17 ce a4 74 6d"
+ + "8c cd e6 9e 6e bb 64 52 a7 c3 bf ac 07 e8 5e 3e"
+ + "ae eb dc c5 95 37 26 6a 5d a6 a2 12 52 fa 03 43"
+ + "b2 62 2d 87 8c a7 06 8f d6 3f 63 b6 2d 73 c4 9d"
+ + "9d d6 55 0e bb db b1 eb dd c5 4b 8f c3 17 cb 3b"
+ + "c3 bf f6 7f 13 44 de 8e d7 b9 f1 a7 15 56 8f 6c"
+ + "cd f2 4c 86 99 39 19 88 d3 4a 2f 38 c4 c4 37 39"
+ + "85 6f 41 98 19 14 a4 1f 95 bc 04 ef 74 c2 0d f3";
+ private static final String AK_MODULUS_HEX = "d7 c9 f0 e3 ac 1b 4a 1e 3c 9d 2d 57 02 e9 2a 93"
+ + "b0 c0 e1 50 af e4 61 11 31 73 a1 96 b8 d6 d2 1c"
+ + "40 40 c8 a6 46 a4 10 4b d1 06 74 32 f6 e3 8a 55"
+ + "1e 03 c0 3e cc 75 04 c6 44 88 b6 ad 18 c9 45 65"
+ + "0d be c5 45 22 bd 24 ad 32 8c be 83 a8 9b 1b d9"
+ + "e0 c8 d9 ec 14 67 55 1b fe 68 dd c7 f7 33 e4 cd"
+ + "87 bd ba 9a 07 e7 74 eb 57 ef 80 9c 6d ee f9 35"
+ + "52 67 36 e2 53 98 46 a5 4e 8f 17 41 8d ff eb bb"
+ + "9c d2 b4 df 57 f8 7f 31 ef 2e 2d 6e 06 7f 05 ed"
+ + "3f e9 6f aa b4 b7 5a f9 6d ba ff 2b 5e f7 c1 05"
+ + "90 68 1f b6 4b 38 67 f7 92 d8 73 51 6e 08 19 ad"
+ + "ca 35 48 a7 c1 fb cb 01 9a 28 03 c9 fe bb 49 2f"
+ + "88 3f a1 e7 a8 69 f0 f8 e8 78 db d3 6d c5 80 8d"
+ + "c2 e4 8a af 4b c2 ac 48 2a 44 63 6e 39 b0 8f dd"
+ + "e4 b3 a3 f9 2a b1 c8 d9 3d 6b c4 08 b0 16 c4 e7"
+ + "c7 2f f5 94 c6 43 3e ee 9b 8a da e7 31 d1 54 dd";
+ private static final String AK_NAME_HEX = "00 0b 6e 8f 79 1c 7e 16 96 1b 11 71 65 9c e0 cd"
+ + "ae 0d 4d aa c5 41 be 58 89 74 67 55 96 c2 5e 38"
+ + "e2 94";
+
+ // test key pair
+ private KeyPair keyPair;
+
+ /**
+ * Registers bouncy castle as a security provider. Normally the JEE container will handle this,
+ * but since the tests are not instantiating a container, have the unit test runner setup the
+ * provider.
+ */
+ @BeforeClass
+ public static void setupTests() {
+ Security.addProvider(new BouncyCastleProvider());
+ }
+
+ /**
+ * Instantiates an anonymous instance of the {@link AbstractAttestationCertificateAuthority}.
+ * This is sufficient as this class does not have any unimplemented or abstract methods.
+ */
+ @BeforeTest
+ public void setup() {
+ aca = new AbstractAttestationCertificateAuthority(null, keyPair.getPrivate(),
+ null, null, null, null, 1,
+ null, null) {
+ };
+ }
+
+ /**
+ * Generates a key pair that can be used by the test suite.
+ *
+ * @throws Exception during key generation
+ */
+ @BeforeSuite
+ public void suiteSetup() throws Exception {
+ KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
+ keyPairGenerator.initialize(2048);
+ keyPair = keyPairGenerator.generateKeyPair();
+ }
+
+ /**
+ * Tests {@link AbstractAttestationCertificateAuthority#processIdentityRequest(byte[])}
+ * where the byte array is null. Expects an illegal argument exception to be thrown.
+ */
+ @Test(expectedExceptions = IllegalArgumentException.class)
+ public void testProcessIdentityRequestNullRequest() {
+ aca.processIdentityRequest(null);
+ }
+
+ /**
+ * Tests {@link AbstractAttestationCertificateAuthority#processIdentityClaimTpm2(byte[])}
+ * where the byte array is null. Expects an illegal argument exception to be thrown.
+ */
+ @Test(expectedExceptions = IllegalArgumentException.class)
+ public void testProcessIdentityClaimTpm2NullRequest() {
+ aca.processIdentityClaimTpm2(null);
+ }
+
+ /**
+ * Tests {@link AbstractAttestationCertificateAuthority#getPublicKey()}.
+ */
+ @Test
+ public void testGetPublicKey() {
+
+ // encoded byte array to be returned by public key
+ byte[] encoded = new byte[]{0, 1, 0, 1, 0};
+
+ // create mocks for testing
+ X509Certificate acaCertificate = mock(X509Certificate.class);
+ PublicKey publicKey = mock(PublicKey.class);
+
+ // assign the aca certificate to the aca
+ ReflectionTestUtils.setField(aca, "acaCertificate", acaCertificate);
+
+ // return a mocked public key
+ when(acaCertificate.getPublicKey()).thenReturn(publicKey);
+
+ // return test byte array
+ when(publicKey.getEncoded()).thenReturn(encoded);
+
+ // assert what the ACA returns is as expected
+ assertEquals(aca.getPublicKey(), encoded);
+
+ // verify mock interactions
+ verify(acaCertificate).getPublicKey();
+ verify(publicKey).getEncoded();
+
+ // verify no other interactions with mocks
+ verifyNoMoreInteractions(acaCertificate, publicKey);
+ }
+
+ /**
+ * Tests {@link AbstractAttestationCertificateAuthority#unwrapIdentityRequest(byte[])}.
+ *
+ * @throws Exception during aca processing
+ */
+ @Test
+ public void testUnwrapIdentityRequest() throws Exception {
+ // create a key generator to generate a "shared" secret
+ KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
+ keyGenerator.init(128);
+
+ // test variables
+ byte[] request = new byte[1];
+ byte[] iv = new byte[16];
+ byte[] asymmetricBlob = new byte[]{1, 2, 3};
+ byte[] symmetricBlob = new byte[]{3, 2, 1};
+ byte[] secretKey = keyGenerator.generateKey().getEncoded();
+
+ // fill the IV with random bytes
+ SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
+ random.nextBytes(iv);
+
+ // encrypt the asymmetric blob
+ byte[] encryptedAsymmetricBlob =
+ encryptBlob(asymmetricBlob, EncryptionScheme.OAEP.toString());
+
+ // encrypt the symmetric blob
+ byte[] encryptedSymmetricBlob =
+ encryptBlob(symmetricBlob, secretKey, iv, "AES/CBC/PKCS5Padding");
+
+ // create some mocks for the tests
+ IdentityRequest identityRequest = mock(IdentityRequest.class);
+ AsymmetricKeyParams asymmetricKeyParams = mock(AsymmetricKeyParams.class);
+ SymmetricKeyParams symmetricKeyParams = mock(SymmetricKeyParams.class);
+ SymmetricSubParams symmetricSubParams = mock(SymmetricSubParams.class);
+ StructConverter structConverter = mock(StructConverter.class);
+ SymmetricKey symmetricKey = mock(SymmetricKey.class);
+
+ // assign the mocked struct converter to the test object
+ ReflectionTestUtils.setField(aca, "structConverter", structConverter);
+
+ // when converting our test request byte array, return our mocked identity request
+ // when converting our test asymmetric blob to a symmetric key, return the mocked key
+ when(structConverter.convert(request, IdentityRequest.class)).thenReturn(identityRequest);
+ when(structConverter.convert(asymmetricBlob, SymmetricKey.class)).thenReturn(symmetricKey);
+
+ // mock out the identity request by returning other mocks as needed
+ when(identityRequest.getSymmetricAlgorithm()).thenReturn(symmetricKeyParams);
+ when(identityRequest.getAsymmetricAlgorithm()).thenReturn(asymmetricKeyParams);
+ when(identityRequest.getAsymmetricBlob()).thenReturn(encryptedAsymmetricBlob);
+ when(identityRequest.getSymmetricBlob()).thenReturn(encryptedSymmetricBlob);
+
+ // use OAEP encryption scheme when asked
+ when(asymmetricKeyParams.getEncryptionScheme()).thenReturn(
+ (short) EncryptionScheme.OAEP_VALUE);
+
+ // use the mocked sub params when asked
+ when(symmetricKeyParams.getParams()).thenReturn(symmetricSubParams);
+
+ // use the test IV when asked
+ when(symmetricSubParams.getIv()).thenReturn(iv);
+
+ // use the test secret key when asked
+ when(symmetricKey.getKey()).thenReturn(secretKey);
+
+ // perform test
+ byte[] unwrappedRequest = aca.unwrapIdentityRequest(request);
+
+ // verify test results
+ assertEquals(unwrappedRequest, symmetricBlob);
+
+ // verify mock interactions
+ verify(structConverter).convert(request, IdentityRequest.class);
+ verify(structConverter).convert(asymmetricBlob, SymmetricKey.class);
+ verify(identityRequest).getSymmetricAlgorithm();
+ verify(identityRequest).getAsymmetricAlgorithm();
+ verify(identityRequest).getAsymmetricBlob();
+ verify(identityRequest).getSymmetricBlob();
+ verify(asymmetricKeyParams).getEncryptionScheme();
+ verify(symmetricKeyParams, times(2)).getParams();
+ verify(symmetricSubParams).getIv();
+ verify(symmetricKey).getKey();
+ verifyNoMoreInteractions(identityRequest, symmetricKeyParams, symmetricSubParams,
+ asymmetricKeyParams, structConverter, symmetricKey);
+ }
+
+ /**
+ * Tests {@link AbstractAttestationCertificateAuthority#unwrapIdentityRequest(byte[])}.
+ *
+ * @throws Exception during aca processing
+ */
+ @Test
+ public void testUnwrapIdentityRequestNoKeyParams() throws Exception {
+ // create a key generator to generate a "shared" secret
+ KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
+ keyGenerator.init(128);
+
+ // test variables
+ byte[] request = new byte[1];
+ byte[] iv = new byte[16];
+ byte[] asymmetricBlob = new byte[]{1, 2, 3};
+ byte[] symmetricBlob = new byte[]{3, 2, 1};
+ byte[] secretKey = keyGenerator.generateKey().getEncoded();
+
+ // fill the IV with random bytes
+ SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
+ random.nextBytes(iv);
+
+ // encrypt the asymmetric blob
+ byte[] encryptedAsymmetricBlob =
+ encryptBlob(asymmetricBlob, EncryptionScheme.OAEP.toString());
+
+ // encrypt the symmetric blob
+ byte[] encryptedSymmetricBlob =
+ encryptBlob(symmetricBlob, secretKey, iv, "AES/CBC/PKCS5Padding");
+
+ // since there is no symmetric key params, put the IV in front of encrypted blob.
+ byte[] totalBlob = ArrayUtils.addAll(iv, encryptedSymmetricBlob);
+
+ // create some mocks for the tests
+ IdentityRequest identityRequest = mock(IdentityRequest.class);
+ AsymmetricKeyParams asymmetricKeyParams = mock(AsymmetricKeyParams.class);
+ StructConverter structConverter = mock(StructConverter.class);
+ SymmetricKey symmetricKey = mock(SymmetricKey.class);
+
+ // assign the mocked struct converter to the test object
+ ReflectionTestUtils.setField(aca, "structConverter", structConverter);
+
+ // when converting our test request byte array, return our mocked identity request
+ // when converting our test asymmetric blob to a symmetric key, return the mocked key
+ when(structConverter.convert(request, IdentityRequest.class)).thenReturn(identityRequest);
+ when(structConverter.convert(asymmetricBlob, SymmetricKey.class)).thenReturn(symmetricKey);
+
+ // mock out the identity request by returning other mocks as needed
+ when(identityRequest.getSymmetricAlgorithm()).thenReturn(null);
+ when(identityRequest.getAsymmetricAlgorithm()).thenReturn(asymmetricKeyParams);
+ when(identityRequest.getAsymmetricBlob()).thenReturn(encryptedAsymmetricBlob);
+
+ // the first request should return IV + encrypted blob. Subsequent requests should return
+ // just the encrypted portion of the blob
+ when(identityRequest.getSymmetricBlob()).thenReturn(totalBlob)
+ .thenReturn(encryptedSymmetricBlob);
+
+ // use OAEP encryption scheme when asked
+ when(asymmetricKeyParams.getEncryptionScheme()).thenReturn(
+ (short) EncryptionScheme.OAEP_VALUE);
+
+ // use the test secret key when asked
+ when(symmetricKey.getKey()).thenReturn(secretKey);
+
+ // perform test
+ byte[] unwrappedRequest = aca.unwrapIdentityRequest(request);
+
+ // verify test results
+ assertEquals(unwrappedRequest, symmetricBlob);
+
+ // verify mock interactions
+ verify(structConverter).convert(request, IdentityRequest.class);
+ verify(structConverter).convert(asymmetricBlob, SymmetricKey.class);
+ verify(identityRequest).getSymmetricAlgorithm();
+ verify(identityRequest).getAsymmetricAlgorithm();
+ verify(identityRequest).getAsymmetricBlob();
+ verify(identityRequest, times(2)).getSymmetricBlob();
+ verify(identityRequest).setSymmetricBlob(encryptedSymmetricBlob);
+ verify(asymmetricKeyParams).getEncryptionScheme();
+ verify(symmetricKey).getKey();
+ verifyNoMoreInteractions(identityRequest, asymmetricKeyParams, structConverter,
+ symmetricKey);
+ }
+
+ /**
+ * Tests {@link AbstractAttestationCertificateAuthority#decryptAsymmetricBlob(byte[],
+ * EncryptionScheme)}.
+ *
+ * @throws Exception during aca processing
+ */
+ @Test
+ public void testDecryptAsymmetricBlob() throws Exception {
+
+ // test encryption transformation
+ EncryptionScheme encryptionScheme = EncryptionScheme.PKCS1;
+
+ // test variables
+ byte[] expected = "test".getBytes();
+
+ // encrypt the expected value using same algorithm as the ACA.
+ byte[] encrypted = encryptBlob(expected, encryptionScheme.toString());
+
+ // perform the decryption and assert that the decrypted bytes equal the expected bytes
+ assertEquals(aca.decryptAsymmetricBlob(encrypted, encryptionScheme), expected);
+ }
+
+ /**
+ * Tests {@link AbstractAttestationCertificateAuthority#decryptSymmetricBlob(
+ * byte[], byte[], byte[], String)}.
+ *
+ * @throws Exception during aca processing
+ */
+ @Test
+ public void testDecryptSymmetricBlob() throws Exception {
+ // test encryption transformation
+ String transformation = "AES/CBC/PKCS5Padding";
+
+ // test variables
+ byte[] expected = "test".getBytes();
+
+ // create a key generator to generate a "shared" secret
+ KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
+ keyGenerator.init(128);
+
+ // use some random bytes as the IV to encrypt and subsequently decrypt with
+ byte[] randomBytes = new byte[16];
+
+ // generate the random bytes
+ SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
+ random.nextBytes(randomBytes);
+
+ // the shared secret
+ byte[] secretKey = keyGenerator.generateKey().getEncoded();
+
+ // encrypt the expected value with the private key being the shared secret
+ byte[] encrypted = encryptBlob(expected, secretKey, randomBytes, transformation);
+
+ // perform the decryption using the generated shared secert, random bytes as an IV, and the
+ // AES CBC transformation for the cipher. then assert the decrypted results are the same
+ // as our expected value.
+ assertEquals(aca.decryptSymmetricBlob(encrypted, secretKey, randomBytes, transformation),
+ expected);
+ }
+
+ /**
+ * Tests {@link AbstractAttestationCertificateAuthority#generateSymmetricKey()}.
+ */
+ @Test
+ public void testGenerateSymmetricKey() {
+ // perform the test
+ SymmetricKey symmetricKey = aca.generateSymmetricKey();
+
+ // assert the symmetric algorithm, scheme, and key size are all set appropriately
+ assertTrue(symmetricKey.getAlgorithmId() == 6);
+ assertTrue(symmetricKey.getEncryptionScheme() == 255);
+ assertTrue(symmetricKey.getKeySize() == symmetricKey.getKey().length);
+ }
+
+ /**
+ * Tests {@link AbstractAttestationCertificateAuthority#generateAsymmetricContents(
+ * IdentityProof, SymmetricKey, PublicKey)}.
+ *
+ * @throws Exception during aca processing
+ */
+ @Test
+ public void testGenerateAsymmetricContents() throws Exception {
+
+ // mocks for test
+ IdentityProof proof = mock(IdentityProof.class);
+ AsymmetricPublicKey publicKey = mock(AsymmetricPublicKey.class);
+ StructConverter structConverter = mock(StructConverter.class);
+ SymmetricKey symmetricKey = mock(SymmetricKey.class);
+
+ // assign the mocked struct converter to the test object
+ ReflectionTestUtils.setField(aca, "structConverter", structConverter);
+
+ // "encoded" identity proof (returned by struct converter)
+ byte[] identityProofEncoded = new byte[]{0, 0, 1, 1};
+
+ // generate a random session key to be used for encryption and decryption
+ byte[] sessionKey = new byte[16];
+ SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
+ random.nextBytes(sessionKey);
+
+ // when requesting the identity key from the proof, return the mocked public key
+ when(proof.getIdentityKey()).thenReturn(publicKey);
+
+ // when requesting to convert the public key, return the encoded identity proof
+ when(structConverter.convert(publicKey)).thenReturn(identityProofEncoded);
+ when(structConverter.convert(symmetricKey)).thenReturn(sessionKey);
+
+ // perform the test
+ byte[] result = aca.generateAsymmetricContents(proof, symmetricKey, keyPair.getPublic());
+
+ // verify mock interactions
+ verify(proof).getIdentityKey();
+ verify(structConverter).convert(publicKey);
+ verify(structConverter).convert(symmetricKey);
+ verifyZeroInteractions(proof, structConverter, publicKey, symmetricKey);
+
+ // decrypt the result
+ byte[] decryptedResult = decryptBlob(result);
+
+ // create a SHA1 digest of the identity key
+ MessageDigest md = MessageDigest.getInstance("SHA-1");
+ md.update(identityProofEncoded);
+
+ // generate the digest
+ byte[] identityDigest = md.digest();
+
+ // the decrypted asymmetric contents should be the session key and a SHA-1 hash of the
+ // encoded identity proof.
+ byte[] expected = ArrayUtils.addAll(sessionKey, identityDigest);
+
+ // compare the two byte arrays
+ assertEquals(decryptedResult, expected);
+ }
+
+ /**
+ * Tests {@link AbstractAttestationCertificateAuthority#generateAttestation(X509Certificate,
+ * SymmetricKey)}.
+ *
+ * @throws Exception during aca processing
+ */
+ @Test
+ public void testGenerateAttestation() throws Exception {
+
+ // create some mocks for the unit tests
+ X509Certificate certificate = mock(X509Certificate.class);
+ SymmetricKey symmetricKey = mock(SymmetricKey.class);
+
+ // create a key generator to generate a secret key
+ KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
+ keyGenerator.init(128);
+
+ // obtain the key from the generator
+ byte[] secretKey = keyGenerator.generateKey().getEncoded();
+
+ // use our public key for encryption
+ when(symmetricKey.getKey()).thenReturn(secretKey);
+
+ // just use the existing public key for the credential
+ when(certificate.getEncoded()).thenReturn(keyPair.getPublic().getEncoded());
+
+ // perform the actual test
+ SymmetricAttestation attestation = aca.generateAttestation(certificate, symmetricKey);
+
+ // validate that the attestation is not null
+ assertNotNull(attestation);
+
+ // validate the attestation algorithm
+ assertNotNull(attestation.getAlgorithm());
+ assertTrue(attestation.getAlgorithm().getAlgorithmId() == 6);
+ assertTrue(attestation.getAlgorithm().getEncryptionScheme() == 0x1);
+ assertTrue(attestation.getAlgorithm().getSignatureScheme() == 0);
+ assertTrue(attestation.getAlgorithm().getParamsSize() == 0);
+
+ // validate the attestation credential
+ assertNotNull(attestation.getCredential());
+
+ // validate that the credential size is the size of the actual credential block
+ assertTrue(attestation.getCredential().length == attestation.getCredentialSize());
+
+ // create containers for the 2 parts of the credential
+ byte[] iv = new byte[16];
+ byte[] credential = new byte[attestation.getCredential().length - iv.length];
+
+ // siphon off the first 16 bytes for the IV
+ System.arraycopy(attestation.getCredential(), 0, iv, 0, iv.length);
+
+ // the rest is the actual encrypted credential
+ System.arraycopy(attestation.getCredential(), iv.length, credential, 0, credential.length);
+
+ // decrypt the credential
+ byte[] decrypted = decryptBlob(credential, secretKey, iv, "AES/CBC/PKCS5Padding");
+
+ // assert that the decrypted credential is our public key
+ assertEquals(keyPair.getPublic().getEncoded(), decrypted);
+
+ // verify that the mocks were interacted with appropriately
+ verify(symmetricKey).getKey();
+ verify(certificate).getEncoded();
+ verifyNoMoreInteractions(certificate, symmetricKey);
+ }
+
+ /**
+ * Tests {@link AbstractAttestationCertificateAuthority#
+ * AbstractAttestationCertificateAuthority(SupplyChainValidationService, PrivateKey,
+ * X509Certificate, StructConverter, CertificateManager, DeviceRegister, int,
+ * DeviceManager, DBManager)}.
+ *
+ * @throws Exception during subject alternative name checking if cert formatting is bad
+ */
+ @Test
+ public void testGenerateCredential() throws Exception {
+ // test variables
+ final String identityProofLabelString = "label";
+ byte[] identityProofLabel = identityProofLabelString.getBytes();
+ byte[] modulus = ((RSAPublicKey) keyPair.getPublic()).getModulus().toByteArray();
+ X500Principal principal = new X500Principal("CN=TEST, OU=TEST, O=TEST, C=TEST");
+ int validDays = 1;
+
+ // create mocks for testing
+ IdentityProof identityProof = mock(IdentityProof.class);
+ AsymmetricPublicKey asymmetricPublicKey = mock(AsymmetricPublicKey.class);
+ StorePubKey storePubKey = mock(StorePubKey.class);
+ X509Certificate acaCertificate = mock(X509Certificate.class);
+
+ // assign ACA fields
+ ReflectionTestUtils.setField(aca, "validDays", validDays);
+ ReflectionTestUtils.setField(aca, "acaCertificate", acaCertificate);
+
+ // prepare identity proof interactions
+ when(identityProof.getLabel()).thenReturn(identityProofLabel);
+
+ // prepare other mocks
+ when(acaCertificate.getSubjectX500Principal()).thenReturn(principal);
+ when(acaCertificate.getIssuerX500Principal()).thenReturn(principal);
+
+ // perform the test
+ X509Certificate certificate = aca.generateCredential(keyPair.getPublic(),
+ null,
+ null,
+ "exampleIdLabel");
+
+ // grab the modulus from the generate certificate
+ byte[] resultMod = ((RSAPublicKey) certificate.getPublicKey()).getModulus().toByteArray();
+
+ // today and tomorrow, when the certificate should be valid for
+ Calendar today = Calendar.getInstance();
+ Calendar tomorrow = Calendar.getInstance();
+ tomorrow.add(Calendar.DATE, 1);
+
+ // validate the certificate
+ assertTrue(certificate.getIssuerX500Principal().toString().contains("CN=TEST"));
+ assertTrue(certificate.getIssuerX500Principal().toString().contains("OU=TEST"));
+ assertTrue(certificate.getIssuerX500Principal().toString().contains("O=TEST"));
+ assertTrue(certificate.getIssuerX500Principal().toString().contains("C=TEST"));
+
+ // validate the format of the subject and subject alternative name
+ assertEquals(certificate.getSubjectX500Principal().getName(), "");
+ assertEquals(((X500Name) GeneralNames.fromExtensions(((TBSCertificate.getInstance(
+ certificate.getTBSCertificate()).getExtensions())), Extension.
+ subjectAlternativeName).getNames()[0].getName()).getRDNs(
+ IssuedCertificateAttributeHelper.TCPA_AT_TPM_ID_LABEL)[0].getFirst()
+ .getValue().toString(), "exampleIdLabel");
+
+ assertEquals(resultMod, modulus);
+
+ // obtain the expiration dates from the certificate
+ Calendar beforeDate = Calendar.getInstance();
+ Calendar afterDate = Calendar.getInstance();
+ beforeDate.setTime(certificate.getNotBefore());
+ afterDate.setTime(certificate.getNotAfter());
+
+ // assert the dates are set correctly
+ assertEquals(beforeDate.get(Calendar.DATE), today.get(Calendar.DATE));
+ assertEquals(afterDate.get(Calendar.DATE), tomorrow.get(Calendar.DATE));
+
+ // validate mock interactions
+ verify(acaCertificate).getSubjectX500Principal();
+ verifyNoMoreInteractions(identityProof, asymmetricPublicKey, storePubKey, acaCertificate);
+ }
+
+ /**
+ * Tests {@link AbstractAttestationCertificateAuthority#assemblePublicKey(byte[])}.
+ */
+ @Test
+ public void testAssemblePublicKeyUsingByteArray() {
+ // obtain the expected modulus from the existing public key
+ final BigInteger modulus = ((RSAPublicKey) keyPair.getPublic()).getModulus();
+
+ // perform test
+ RSAPublicKey publicKey = (RSAPublicKey) aca.assemblePublicKey(modulus.toByteArray());
+
+ // assert that the exponent and the modulus are the same. the exponents should be the well
+ // known prime, 101
+ assertTrue(publicKey.getPublicExponent().equals(new BigInteger("010001", 16)));
+ assertTrue(publicKey.getModulus().equals(modulus));
+ }
+
+ /**
+ * Tests {@link AbstractAttestationCertificateAuthority#assemblePublicKey(String)}.
+ */
+ @Test
+ public void testAssemblePublicKeyUsingHexEncodedString() {
+ // obtain the expected modulus from the existing public key
+ final BigInteger modulus = ((RSAPublicKey) keyPair.getPublic()).getModulus();
+
+ // encode our existing public key into hex
+ final String modulusString = Hex.encodeHexString(
+ ((RSAPublicKey) keyPair.getPublic()).getModulus().toByteArray());
+
+ // perform test
+ RSAPublicKey publicKey = (RSAPublicKey) aca.assemblePublicKey(modulusString);
+
+ // assert that the exponent and the modulus are the same. the exponents should be the well
+ // known prime, 101.
+ assertTrue(publicKey.getPublicExponent().equals(new BigInteger("010001", 16)));
+ assertTrue(publicKey.getModulus().equals(modulus));
+ }
+
+ /**
+ * Tests parsing the EK from the TPM2 output file.
+ * @throws URISyntaxException incorrect resource path
+ * @throws IOException unable to read from file
+ */
+ @Test
+ public void testParseEk() throws URISyntaxException, IOException {
+ Path ekPath = Paths.get(getClass().getResource(
+ EK_PUBLIC_PATH).toURI());
+
+ byte[] ekFile = Files.readAllBytes(ekPath);
+
+ RSAPublicKey ek = aca.parsePublicKey(ekFile);
+ assertTrue(ek.getPublicExponent().equals(new BigInteger("010001", 16)));
+
+ byte[] mod = ek.getModulus().toByteArray();
+ // big integer conversion is signed so it can add a 0 byte
+ if (mod[0] == 0) {
+ byte[] tmp = new byte[mod.length - 1];
+ System.arraycopy(mod, 1, tmp, 0, mod.length - 1);
+ mod = tmp;
+ }
+ String hex = HexUtils.byteArrayToHexString(mod);
+ String realMod = EK_MODULUS_HEX.replaceAll("\\s+", "");
+ assertEquals(hex, realMod);
+ }
+
+ /**
+ * Tests parsing the AK public key from the TPM2 output file.
+ * @throws URISyntaxException incorrect resource path
+ * @throws IOException unable to read from file
+ */
+ @Test
+ public void testParseAk() throws URISyntaxException, IOException {
+ Path akPath = Paths.get(getClass().getResource(
+ AK_PUBLIC_PATH).toURI());
+
+ byte[] akFile = Files.readAllBytes(akPath);
+
+ RSAPublicKey ak = aca.parsePublicKey(akFile);
+ assertTrue(ak.getPublicExponent().equals(new BigInteger("010001", 16)));
+
+ byte[] mod = ak.getModulus().toByteArray();
+ // big integer conversion is signed so it can add a 0 byte
+ if (mod[0] == 0) {
+ byte[] tmp = new byte[mod.length - 1];
+ System.arraycopy(mod, 1, tmp, 0, mod.length - 1);
+ mod = tmp;
+ }
+ String hex = HexUtils.byteArrayToHexString(mod);
+ String realMod = AK_MODULUS_HEX.replaceAll("\\s+", "");
+ assertEquals(hex, realMod);
+ }
+
+ /**
+ * Tests parsing the AK name from the TPM2 output file.
+ * @throws URISyntaxException incorrect resource path
+ * @throws IOException unable to read from file
+ * @throws NoSuchAlgorithmException inavlid algorithm
+ */
+ @Test
+ public void testGenerateAkName() throws URISyntaxException, IOException,
+ NoSuchAlgorithmException {
+ Path akNamePath = Paths.get(getClass().getResource(
+ AK_NAME_PATH).toURI());
+
+ byte[] akNameFileBytes = Files.readAllBytes(akNamePath);
+ String realHex = HexUtils.byteArrayToHexString(akNameFileBytes);
+
+ String realMod = AK_MODULUS_HEX.replaceAll("\\s+", "");
+ byte[] akName = aca.generateAkName(HexUtils.hexStringToByteArray(realMod));
+
+ String hex = HexUtils.byteArrayToHexString(akName);
+ String realName = AK_NAME_HEX.replaceAll("\\s+", "");
+ assertEquals(hex, realName);
+ assertEquals(hex, realHex);
+ }
+
+ /**
+ * Method to generate a make credential output file for use in manual testing. Feed to
+ * a TPM 2.0 or emulator using the activate credential command to ensure proper parsing.
+ * Must be performed manually. To use, copy the TPM's ek and ak into
+ * HIRS_AttestationCA/src/test/resources/tpm2/test/ and ensure the variables akPubPath
+ * and ekPubPath are correct. Your output file will be
+ * HIRS_AttestationCA/src/test/resources/tpm2/test/make.blob and the nonce used will be
+ * output as HIRS_AttestationCA/src/test/resources/tpm2/test/secret.blob
+ * @throws URISyntaxException invalid file path
+ * @throws IOException unable to read file
+ */
+ @Test(enabled = false)
+ public void testMakeCredential() throws URISyntaxException, IOException {
+ Path akPubPath = Paths.get(getClass().getResource(
+ AK_PUBLIC_PATH).toURI());
+ Path ekPubPath = Paths.get(getClass().getResource(
+ EK_PUBLIC_PATH).toURI());
+
+ byte[] ekPubFile = Files.readAllBytes(ekPubPath);
+ byte[] akPubFile = Files.readAllBytes(akPubPath);
+
+ RSAPublicKey ekPub = aca.parsePublicKey(ekPubFile);
+ RSAPublicKey akPub = aca.parsePublicKey(akPubFile);
+
+ // prepare the nonce and wrap it with keys
+ byte[] nonce = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
+ 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31};
+ ByteString blob = aca.tpm20MakeCredential(ekPub, akPub, nonce);
+
+ Path resources = Paths.get(getClass().getResource(
+ "/").toURI()).getParent().getParent().getParent().getParent();
+ Path makeBlob = resources.resolve("src/test/resources/tpm2/test/make.blob");
+ Files.write(makeBlob, blob.toByteArray());
+
+ Path secretPath = resources.resolve("src/test/resources/tpm2/test/secret.blob");
+ Files.write(secretPath, nonce);
+ }
+
+ /**
+ * Test helper method that encrypts a blob using the specified transformation and the test key
+ * pair public key.
+ *
+ * @param blob to be encrypted
+ * @param transformation used by a cipher to encrypt
+ * @return encrypted blob
+ * @throws Exception during the encryption process
+ */
+ private byte[] encryptBlob(byte[] blob, String transformation) throws Exception {
+ // initialize a cipher using the specified transformation
+ Cipher cipher = Cipher.getInstance(transformation);
+
+ // use our generated public key to encrypt
+ cipher.init(Cipher.ENCRYPT_MODE, keyPair.getPublic());
+
+ // return the cipher text
+ return cipher.doFinal(blob);
+ }
+
+ /**
+ * Test helper method that encrypts a blob using a shared key and IV using the specified
+ * transformation.
+ *
+ * @param blob to be encrypted
+ * @param key shared key
+ * @param iv to encrypt with
+ * @param transformation of the encryption cipher
+ * @return encrypted blob
+ * @throws Exception
+ */
+ private byte[] encryptBlob(byte[] blob, byte[] key, byte[] iv, String transformation)
+ throws Exception {
+ // initialize a cipher using the specified transformation
+ Cipher cipher = Cipher.getInstance(transformation);
+
+ // generate a secret key specification using the key and AES.
+ SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
+
+ // create IV parameter for key specification
+ IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
+
+ // encrypt using the key specification with the generated IV
+ cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivParameterSpec);
+
+ // return the cipher text
+ return cipher.doFinal(blob);
+ }
+
+ /**
+ * Test helper method to decrypt blobs.
+ *
+ * @param blob to be decrypted
+ * @return decrypted blob
+ * @throws Exception
+ */
+ private byte[] decryptBlob(byte[] blob) throws Exception {
+ // initialize a cipher using the specified transformation
+ Cipher cipher = Cipher.getInstance(EncryptionScheme.OAEP.toString());
+
+ OAEPParameterSpec spec = new OAEPParameterSpec("Sha1", "MGF1",
+ MGF1ParameterSpec.SHA1, new PSource.PSpecified("TCPA".getBytes()));
+
+ // use our generated public key to encrypt
+ cipher.init(Cipher.PRIVATE_KEY, keyPair.getPrivate(), spec);
+
+ // return the cipher text
+ return cipher.doFinal(blob);
+ }
+
+ /**
+ * Test helper method that decrypts a blob using a shared key and IV using the specified.
+ * transformation.
+ *
+ * @param blob to be decrypted
+ * @param key shared key
+ * @param iv to decrypt with
+ * @param transformation of the decryption cipher
+ * @return decrypted blob
+ * @throws Exception
+ */
+ private byte[] decryptBlob(byte[] blob, byte[] key, byte[] iv, String transformation)
+ throws Exception {
+ // initialize a cipher using the specified transformation
+ Cipher cipher = Cipher.getInstance(transformation);
+
+ // generate a secret key specification using the key and AES.
+ SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
+
+ // create IV parameter for key specification
+ IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
+
+ // encrypt using the key specification with the generated IV
+ cipher.init(Cipher.DECRYPT_MODE, keySpec, ivParameterSpec);
+
+ // return the cipher text
+ return cipher.doFinal(blob);
+ }
+
+}
diff --git a/HIRS_AttestationCA/src/test/java/hirs/attestationca/CredentialManagementHelperTest.java b/HIRS_AttestationCA/src/test/java/hirs/attestationca/CredentialManagementHelperTest.java
new file mode 100644
index 000000000..f0deb2712
--- /dev/null
+++ b/HIRS_AttestationCA/src/test/java/hirs/attestationca/CredentialManagementHelperTest.java
@@ -0,0 +1,104 @@
+package hirs.attestationca;
+
+import org.apache.commons.io.IOUtils;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import hirs.data.persist.certificate.Certificate;
+import hirs.persist.CertificateManager;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+/**
+ * Unit tests for {@see CredentialManagementHelper}.
+ */
+public class CredentialManagementHelperTest {
+
+ private CertificateManager certMan;
+
+ private static final String EK_HEADER_TRUNCATED
+ = "/certificates/nuc-1/ek_cert_7_byte_header_removed.cer";
+ private static final String EK_UNTOUCHED
+ = "/certificates/nuc-1/ek_cert_untouched.cer";
+
+ /**
+ * Setup mocks.
+ */
+ @BeforeMethod
+ public void setUp() {
+ certMan = mock(CertificateManager.class);
+ }
+
+ /**
+ * Tests exception generated if providing a null cert manager.
+ */
+ @Test(expectedExceptions = IllegalArgumentException.class)
+ public void processNullCertMan() {
+ CredentialManagementHelper.storeEndorsementCredential(null, null);
+ }
+
+ /**
+ * Tests exception generated when providing a null EK byte array.
+ */
+ @Test(expectedExceptions = IllegalArgumentException.class)
+ public void processNullEndorsementCredential() {
+ CredentialManagementHelper.storeEndorsementCredential(certMan, null);
+ }
+
+ /**
+ * Tests exception generated when providing an empty array of bytes as the EK.
+ */
+ @Test(expectedExceptions = IllegalArgumentException.class)
+ public void processEmptyEndorsementCredential() {
+ CredentialManagementHelper.storeEndorsementCredential(certMan, new byte[0]);
+ }
+
+ /**
+ * Tests processing an invalid EK (too small of an array).
+ */
+ @Test(expectedExceptions = IllegalArgumentException.class)
+ public void processInvalidEndorsementCredentialCase1() {
+ byte[] ekBytes = new byte[] {1};
+ CredentialManagementHelper.storeEndorsementCredential(certMan, ekBytes);
+ }
+
+ /**
+ * Tests processing an invalid EK (garbage bytes of a reasonable length).
+ */
+ @Test(expectedExceptions = IllegalArgumentException.class)
+ public void processInvalidEndorsementCredentialCase2() {
+ byte[] ekBytes = new byte[] {1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0};
+ CredentialManagementHelper.storeEndorsementCredential(certMan, ekBytes);
+ }
+
+ /**
+ * Tests processing a valid EK with the 7 byte header in tact.
+ * @throws IOException if an IO error occurs
+ */
+ @Test
+ public void parseUntouchedEndorsementCredential() throws IOException {
+ String path = CredentialManagementHelperTest.class.getResource(EK_UNTOUCHED).getPath();
+ byte[] ekBytes = IOUtils.toByteArray(new FileInputStream(path));
+
+ CredentialManagementHelper.storeEndorsementCredential(certMan, ekBytes);
+ verify(certMan).save(any(Certificate.class));
+ }
+
+ /**
+ * Tests processing a valid EK with the 7 byte header already stripped.
+ * @throws IOException if an IO error occurs
+ */
+ @Test
+ public void parseHeaderTruncatedEndorsementCredential() throws IOException {
+ String path = CredentialManagementHelperTest.class.getResource(EK_HEADER_TRUNCATED)
+ .getPath();
+ byte[] ekBytes = IOUtils.toByteArray(new FileInputStream(path));
+
+ CredentialManagementHelper.storeEndorsementCredential(certMan, ekBytes);
+ verify(certMan).save(any(Certificate.class));
+ }
+}
diff --git a/HIRS_AttestationCA/src/test/java/hirs/attestationca/IssuedCertificateAttributeHelperTest.java b/HIRS_AttestationCA/src/test/java/hirs/attestationca/IssuedCertificateAttributeHelperTest.java
new file mode 100644
index 000000000..7393b61ca
--- /dev/null
+++ b/HIRS_AttestationCA/src/test/java/hirs/attestationca/IssuedCertificateAttributeHelperTest.java
@@ -0,0 +1,204 @@
+package hirs.attestationca;
+
+import org.bouncycastle.asn1.DERSequence;
+import org.bouncycastle.asn1.DERSet;
+import org.bouncycastle.asn1.DERTaggedObject;
+import org.bouncycastle.asn1.DLSequence;
+import org.bouncycastle.asn1.x509.Extension;
+import org.testng.annotations.Test;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import hirs.data.persist.certificate.EndorsementCredential;
+import hirs.data.persist.certificate.PlatformCredential;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+
+/**
+ * Tests for {@see IssuedCertificateAttributeHelper}.
+ */
+public class IssuedCertificateAttributeHelperTest {
+
+ private static final String NUC1_EC = "/certificates/nuc-1/tpmcert.pem";
+
+ private static final String INTEL_PC = "/certificates/platform_certs_2/"
+ + "Intel_pc.pem";
+
+ private static final String TEST_HOSTNAME = "box1";
+
+ private static final String TPM_MANUFACTURER = "2.23.133.2.1";
+
+ private static final String TPM_MODEL = "2.23.133.2.2";
+
+ private static final String TPM_VERSION = "2.23.133.2.3";
+
+ private static final String TPM_ID_LABEL_OID = "2.23.133.2.15";
+
+ private static final String PLATFORM_MANUFACTURER = "2.23.133.2.4";
+
+ private static final String PLATFORM_MODEL = "2.23.133.2.5";
+
+ private static final String PLATFORM_VERSION = "2.23.133.2.6";
+
+ /**
+ * Test that provide a null host name and is rejected.
+ * @throws IOException an IO error occurs
+ */
+ @Test(expectedExceptions = IllegalArgumentException.class)
+ public void rejectNullHostName() throws IOException {
+ IssuedCertificateAttributeHelper.buildSubjectAlternativeNameFromCerts(null, null, "");
+ }
+
+ /**
+ * Test that subject alt name can be built without an EC or PC.
+ * @throws IOException an IO error occurs
+ */
+ @Test
+ public void buildAttributesNoEndorsementNoPlatform() throws IOException {
+ Extension subjectAlternativeName =
+ IssuedCertificateAttributeHelper.buildSubjectAlternativeNameFromCerts(
+ null, null, TEST_HOSTNAME);
+
+ Map subjectAlternativeNameAttrMap = getSubjectAlternativeNameAttributes(
+ subjectAlternativeName);
+
+ assertNull(subjectAlternativeNameAttrMap.get(TPM_MANUFACTURER));
+ assertNull(subjectAlternativeNameAttrMap.get(TPM_MODEL));
+ assertNull(subjectAlternativeNameAttrMap.get(TPM_VERSION));
+ assertNull(subjectAlternativeNameAttrMap.get(PLATFORM_MANUFACTURER));
+ assertNull(subjectAlternativeNameAttrMap.get(PLATFORM_MODEL));
+ assertNull(subjectAlternativeNameAttrMap.get(PLATFORM_VERSION));
+ assertEquals(subjectAlternativeNameAttrMap.get(TPM_ID_LABEL_OID), TEST_HOSTNAME);
+ }
+
+ /**
+ * Test that subject alt name can be built with an EC but no PC.
+ * @throws IOException an IO error occurs
+ * @throws URISyntaxException unrecognized URI for EC Path
+ */
+ @Test
+ public void buildAttributesEndorsementNoPlatform() throws IOException, URISyntaxException {
+ Path endorsementCredentialPath = Paths.get(getClass().getResource(
+ NUC1_EC).toURI());
+ EndorsementCredential endorsementCredential = new EndorsementCredential(
+ endorsementCredentialPath);
+ Extension subjectAlternativeName =
+ IssuedCertificateAttributeHelper.buildSubjectAlternativeNameFromCerts(
+ endorsementCredential, null, TEST_HOSTNAME);
+
+ Map subjectAlternativeNameAttrMap = getSubjectAlternativeNameAttributes(
+ subjectAlternativeName);
+
+ assertEquals(subjectAlternativeNameAttrMap.get(TPM_MANUFACTURER),
+ endorsementCredential.getManufacturer());
+ assertEquals(subjectAlternativeNameAttrMap.get(TPM_MODEL),
+ endorsementCredential.getModel());
+ assertEquals(subjectAlternativeNameAttrMap.get(TPM_VERSION),
+ endorsementCredential.getVersion());
+ assertNull(subjectAlternativeNameAttrMap.get(PLATFORM_MANUFACTURER));
+ assertNull(subjectAlternativeNameAttrMap.get(PLATFORM_MODEL));
+ assertNull(subjectAlternativeNameAttrMap.get(PLATFORM_VERSION));
+ assertEquals(subjectAlternativeNameAttrMap.get(TPM_ID_LABEL_OID),
+ TEST_HOSTNAME);
+ }
+
+ /**
+ * Test that subject alt name can be built with an PC but no EC.
+ * @throws IOException an IO error occurs
+ * @throws URISyntaxException unrecognized URI for PC Path
+ */
+ @Test
+ public void buildAttributesPlatformNoEndorsement() throws IOException, URISyntaxException {
+ Path platformCredentialPath = Paths.get(getClass().getResource(
+ INTEL_PC).toURI());
+ PlatformCredential platformCredential = new PlatformCredential(
+ platformCredentialPath);
+ List platformCredentialList = new ArrayList<>();
+ platformCredentialList.add(platformCredential);
+ Extension subjectAlternativeName =
+ IssuedCertificateAttributeHelper.buildSubjectAlternativeNameFromCerts(
+ null, platformCredentialList, TEST_HOSTNAME);
+
+ Map subjectAlternativeNameAttrMap = getSubjectAlternativeNameAttributes(
+ subjectAlternativeName);
+
+ assertNull(subjectAlternativeNameAttrMap.get(TPM_MANUFACTURER));
+ assertNull(subjectAlternativeNameAttrMap.get(TPM_MODEL));
+ assertNull(subjectAlternativeNameAttrMap.get(TPM_VERSION));
+ assertEquals(subjectAlternativeNameAttrMap.get(PLATFORM_MANUFACTURER),
+ platformCredential.getManufacturer());
+ assertEquals(subjectAlternativeNameAttrMap.get(PLATFORM_MODEL),
+ platformCredential.getModel());
+ assertEquals(subjectAlternativeNameAttrMap.get(PLATFORM_VERSION),
+ platformCredential.getVersion());
+ assertEquals(subjectAlternativeNameAttrMap.get(TPM_ID_LABEL_OID),
+ TEST_HOSTNAME);
+ }
+
+ /**
+ * Test that subject alt name can be built with a PC and an EC.
+ * @throws IOException an IO error occurs
+ * @throws URISyntaxException unrecognized URI for EC or PC Path
+ */
+ @Test
+ public void buildAttributesPlatformAndEndorsement() throws IOException, URISyntaxException {
+ Path endorsementCredentialPath = Paths.get(getClass().getResource(
+ NUC1_EC).toURI());
+ Path platformCredentialPath = Paths.get(getClass().getResource(
+ INTEL_PC).toURI());
+ EndorsementCredential endorsementCredential = new EndorsementCredential(
+ endorsementCredentialPath);
+ PlatformCredential platformCredential = new PlatformCredential(
+ platformCredentialPath);
+ List platformCredentialList = new ArrayList<>();
+ platformCredentialList.add(platformCredential);
+ Extension subjectAlternativeName =
+ IssuedCertificateAttributeHelper.buildSubjectAlternativeNameFromCerts(
+ endorsementCredential, platformCredentialList, TEST_HOSTNAME);
+
+ Map subjectAlternativeNameAttrMap = getSubjectAlternativeNameAttributes(
+ subjectAlternativeName);
+
+ assertEquals(subjectAlternativeNameAttrMap.get(TPM_MANUFACTURER),
+ endorsementCredential.getManufacturer());
+ assertEquals(subjectAlternativeNameAttrMap.get(TPM_MODEL),
+ endorsementCredential.getModel());
+ assertEquals(subjectAlternativeNameAttrMap.get(TPM_VERSION),
+ endorsementCredential.getVersion());
+ assertEquals(subjectAlternativeNameAttrMap.get(PLATFORM_MANUFACTURER),
+ platformCredential.getManufacturer());
+ assertEquals(subjectAlternativeNameAttrMap.get(PLATFORM_MODEL),
+ platformCredential.getModel());
+ assertEquals(subjectAlternativeNameAttrMap.get(PLATFORM_VERSION),
+ platformCredential.getVersion());
+ assertEquals(subjectAlternativeNameAttrMap.get(TPM_ID_LABEL_OID),
+ TEST_HOSTNAME);
+ }
+
+ private Map getSubjectAlternativeNameAttributes(
+ Extension subjectAlternativeName) {
+ Map subjectAlternativeNameAttrMap = new HashMap<>();
+
+ DLSequence dlSequence = (DLSequence) subjectAlternativeName.getParsedValue();
+ DERTaggedObject derTaggedObject = (DERTaggedObject) dlSequence.getObjectAt(0);
+ DERSequence derSequence = (DERSequence) derTaggedObject.getObject();
+
+ Enumeration enumeration = derSequence.getObjects();
+ while (enumeration.hasMoreElements()) {
+ DERSet set = (DERSet) enumeration.nextElement();
+ DERSequence innerDerSequence = (DERSequence) set.getObjectAt(0);
+
+ subjectAlternativeNameAttrMap.put(innerDerSequence.getObjectAt(0).toString(),
+ innerDerSequence.getObjectAt(1).toString());
+ }
+ return subjectAlternativeNameAttrMap;
+ }
+}
diff --git a/HIRS_AttestationCA/src/test/java/hirs/attestationca/service/SupplyChainValidationServiceImplTest.java b/HIRS_AttestationCA/src/test/java/hirs/attestationca/service/SupplyChainValidationServiceImplTest.java
new file mode 100644
index 000000000..a622fbdd3
--- /dev/null
+++ b/HIRS_AttestationCA/src/test/java/hirs/attestationca/service/SupplyChainValidationServiceImplTest.java
@@ -0,0 +1,736 @@
+package hirs.attestationca.service;
+
+import hirs.appraiser.SupplyChainAppraiser;
+import hirs.data.persist.AppraisalStatus;
+import hirs.data.persist.Device;
+import hirs.data.persist.DeviceGroup;
+import hirs.data.persist.DeviceInfoReport;
+import hirs.data.persist.SpringPersistenceTest;
+import hirs.data.persist.SupplyChainPolicy;
+import hirs.data.persist.SupplyChainValidation;
+import hirs.data.persist.SupplyChainValidationSummary;
+import hirs.data.persist.certificate.Certificate;
+import hirs.data.persist.certificate.CertificateAuthorityCredential;
+import hirs.data.persist.certificate.DeviceAssociatedCertificate;
+import hirs.data.persist.certificate.EndorsementCredential;
+import hirs.data.persist.certificate.PlatformCredential;
+import hirs.persist.AppraiserManager;
+import hirs.persist.CertificateManager;
+import hirs.persist.CrudManager;
+import hirs.persist.DBCertificateManager;
+import hirs.persist.DBDeviceGroupManager;
+import hirs.persist.DBDeviceManager;
+import hirs.persist.DeviceGroupManager;
+import hirs.persist.DeviceManager;
+import hirs.persist.PolicyManager;
+import hirs.validation.CredentialValidator;
+import hirs.validation.SupplyChainCredentialValidator;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import java.io.IOException;
+import java.math.BigInteger;
+import java.net.URISyntaxException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.cert.X509Certificate;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.UUID;
+
+import static hirs.data.persist.AppraisalStatus.Status.FAIL;
+import static hirs.data.persist.AppraisalStatus.Status.PASS;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Tests for the {@see SupplyChainValidationServiceImpl}.
+ */
+public class SupplyChainValidationServiceImplTest extends SpringPersistenceTest {
+ private static final String NUC1_EC = "/certificates/nuc-1/tpmcert.pem";
+ private static final String STM_ROOT_CA = "/certificates/stMicroCaCerts/stmtpmekroot.crt";
+ private static final String GS_ROOT_CA = "/certificates/stMicroCaCerts/gstpmroot.crt";
+ private static final String INTEL_CA = "/certificates/IntelSigningKey_20April2017.pem";
+ private static final String NUC_PC = "/certificates/platform_certs_2/Intel_pc.pem";
+ private static final String STM_TPM_EK_INTERMEDIATE_CA_02 =
+ "/certificates/STM TPM EK Intermediate CA.CER";
+ private static final String NUC_EC = "/certificates/nuc_ec.pem";
+
+ @Mock
+ private PolicyManager policyManager;
+
+ @Mock
+ private AppraiserManager appraiserManager;
+
+ @Mock
+ private CertificateManager certificateManager;
+
+ @Mock
+ private CredentialValidator supplyChainCredentialValidator;
+
+ @Mock
+ private CrudManager supplyChainValidationSummaryDBManager;
+
+ @InjectMocks
+ private SupplyChainValidationServiceImpl service;
+
+ // mocked
+ private SupplyChainPolicy policy;
+ private PlatformCredential pc;
+ private EndorsementCredential ec;
+ private HashSet pcs;
+ private Device device;
+
+ /**
+ * Sets up the mocks.
+ *
+ @throws IOException won't actually throw, the method is being mocked instead of actually
+ * called
+ */
+ @BeforeMethod
+ public void beforeClass() throws IOException {
+ MockitoAnnotations.initMocks(this);
+
+ device = mock(Device.class);
+
+ SupplyChainAppraiser appraiser = mock(SupplyChainAppraiser.class);
+ policy = mock(SupplyChainPolicy.class);
+
+ when(appraiserManager.getAppraiser(SupplyChainAppraiser.NAME)).thenReturn(appraiser);
+ when(policyManager.getDefaultPolicy(appraiser)).thenReturn(policy);
+
+ // mock endorsement credential
+ ec = mock(EndorsementCredential.class);
+ when(ec.getEncodedPublicKey()).thenReturn(new byte[] {0x0});
+ when(ec.getIssuerOrganization()).thenReturn("STMicroelectronics NV");
+
+ Set resultEcs = new HashSet<>();
+ resultEcs.add(ec);
+
+ // mock platform credential
+ X509Certificate cert = mock(X509Certificate.class);
+ pc = mock(PlatformCredential.class);
+ when(pc.getId()).thenReturn(UUID.randomUUID());
+ when(pc.getX509Certificate()).thenReturn(cert);
+ when(pc.getSerialNumber()).thenReturn(BigInteger.ONE);
+ when(pc.getIssuerOrganization()).thenReturn("STMicroelectronics NV");
+ when(ec.getSubjectOrganization()).thenReturn("STMicroelectronics NV");
+ pcs = new HashSet();
+ pcs.add(pc);
+
+ Set resultPcs = new HashSet<>();
+ resultPcs.add(pc);
+
+ // mock credential retrieval
+ when(certificateManager.get(any(EndorsementCredential.Selector.class)))
+ .thenReturn(resultEcs);
+ when(certificateManager.get(any(PlatformCredential.Selector.class)))
+ .thenReturn(resultPcs);
+ when(certificateManager.get(any(CertificateAuthorityCredential.Selector.class)))
+ .thenReturn(Collections.emptySet());
+ }
+
+ /**
+ * Remove test certificates and close the session factory.
+ */
+ @AfterMethod
+ public void teardown() {
+ DBCertificateManager certMan = new DBCertificateManager(sessionFactory);
+ DBDeviceManager deviceMan = new DBDeviceManager(sessionFactory);
+ DBDeviceGroupManager groupMan = new DBDeviceGroupManager(sessionFactory);
+
+ certMan.deleteAll();
+ deviceMan.deleteAll();
+ groupMan.deleteAll();
+ }
+ /**
+ * All validations enabled, all pass.
+ */
+ @Test
+ public final void testFullSuccessfulValidation() {
+ when(policy.isEcValidationEnabled()).thenReturn(true);
+ when(policy.isPcValidationEnabled()).thenReturn(true);
+ when(policy.isPcAttributeValidationEnabled()).thenReturn(true);
+ when(policy.isExpiredCertificateValidationEnabled()).thenReturn(true);
+
+ doReturn(new AppraisalStatus(PASS, "")).when(supplyChainCredentialValidator).
+ validateEndorsementCredential(eq(ec), any(KeyStore.class), eq(true));
+ doReturn(new AppraisalStatus(PASS, "")).when(supplyChainCredentialValidator)
+ .validatePlatformCredential(eq(pc), any(KeyStore.class), eq(true));
+ doReturn(new AppraisalStatus(PASS, "")).when(supplyChainCredentialValidator)
+ .validatePlatformCredentialAttributes(eq(pc), any(DeviceInfoReport.class),
+ any(EndorsementCredential.class));
+
+ Assert.assertEquals(service.validateSupplyChain(ec, pcs,
+ device).getOverallValidationResult(), PASS);
+ verify(supplyChainValidationSummaryDBManager).save(any(SupplyChainValidationSummary.class));
+
+ // verify the certs were updated with the test device object and saved in the cert man
+ ArgumentCaptor certificatesCaptor
+ = ArgumentCaptor.forClass(DeviceAssociatedCertificate.class);
+ verify(certificateManager, times(3)).update(certificatesCaptor.capture());
+
+ List certificateArgs = certificatesCaptor.getAllValues();
+ for (DeviceAssociatedCertificate certArg : certificateArgs) {
+ verify(certArg, atLeast(1)).setDevice(device);
+ }
+ }
+
+ /**
+ * All validations enabled, fail EC.
+ */
+ @Test
+ public final void testFailEcValidation() {
+ when(policy.isEcValidationEnabled()).thenReturn(true);
+ when(policy.isPcValidationEnabled()).thenReturn(true);
+ when(policy.isPcAttributeValidationEnabled()).thenReturn(true);
+ when(policy.isExpiredCertificateValidationEnabled()).thenReturn(true);
+
+ doReturn(new AppraisalStatus(FAIL, "")).when(supplyChainCredentialValidator).
+ validateEndorsementCredential(eq(ec), any(KeyStore.class), any(Boolean.class));
+ doReturn(new AppraisalStatus(PASS, "")).when(supplyChainCredentialValidator)
+ .validatePlatformCredential(eq(pc), any(KeyStore.class), eq(true));
+ doReturn(new AppraisalStatus(PASS, "")).when(supplyChainCredentialValidator)
+ .validatePlatformCredentialAttributes(eq(pc), any(DeviceInfoReport.class),
+ any(EndorsementCredential.class));
+
+ Assert.assertEquals(service.validateSupplyChain(ec, pcs,
+ device).getOverallValidationResult(), FAIL);
+ verify(supplyChainValidationSummaryDBManager).save(any(SupplyChainValidationSummary.class));
+ }
+
+ /**
+ * All validations enabled, fail Pc Cert.
+ */
+ @Test
+ public final void testFailPcValidation() {
+ when(policy.isEcValidationEnabled()).thenReturn(true);
+ when(policy.isPcValidationEnabled()).thenReturn(true);
+ when(policy.isPcAttributeValidationEnabled()).thenReturn(true);
+ when(policy.isExpiredCertificateValidationEnabled()).thenReturn(true);
+
+ doReturn(new AppraisalStatus(PASS, "")).when(supplyChainCredentialValidator).
+ validateEndorsementCredential(eq(ec), any(KeyStore.class), eq(true));
+ doReturn(new AppraisalStatus(FAIL, "")).when(supplyChainCredentialValidator).
+ validatePlatformCredential(eq(pc), any(KeyStore.class), eq(true));
+ doReturn(new AppraisalStatus(PASS, "")).when(supplyChainCredentialValidator)
+ .validatePlatformCredentialAttributes(eq(pc), any(DeviceInfoReport.class),
+ any(EndorsementCredential.class));
+ Assert.assertEquals(service.validateSupplyChain(ec, pcs,
+ device).getOverallValidationResult(), FAIL);
+ verify(supplyChainValidationSummaryDBManager).save(any(SupplyChainValidationSummary.class));
+ }
+
+ /**
+ * All validations enabled, Pc Attrib. fails.
+ */
+ @Test
+ public final void testFailPcAttributeValidation() {
+ when(policy.isEcValidationEnabled()).thenReturn(true);
+ when(policy.isPcValidationEnabled()).thenReturn(true);
+ when(policy.isPcAttributeValidationEnabled()).thenReturn(true);
+ when(policy.isExpiredCertificateValidationEnabled()).thenReturn(true);
+
+ doReturn(new AppraisalStatus(PASS, "")).when(supplyChainCredentialValidator).
+ validateEndorsementCredential(eq(ec), any(KeyStore.class), eq(true));
+ doReturn(new AppraisalStatus(PASS, "")).when(supplyChainCredentialValidator).
+ validatePlatformCredential(eq(pc), any(KeyStore.class), eq(true));
+ doReturn(new AppraisalStatus(FAIL, "")).when(supplyChainCredentialValidator).
+ validatePlatformCredentialAttributes(eq(pc), any(DeviceInfoReport.class),
+ any(EndorsementCredential.class));
+
+ Assert.assertEquals(service.validateSupplyChain(ec, pcs,
+ device).getOverallValidationResult(), FAIL);
+ verify(supplyChainValidationSummaryDBManager).save(any(SupplyChainValidationSummary.class));
+ }
+
+ /**
+ * Ec not enabled, all others pass.
+ */
+ @Test
+ public final void testNoEcValidation() {
+ when(policy.isEcValidationEnabled()).thenReturn(false);
+ when(policy.isPcValidationEnabled()).thenReturn(true);
+ when(policy.isPcAttributeValidationEnabled()).thenReturn(true);
+ when(policy.isExpiredCertificateValidationEnabled()).thenReturn(true);
+
+ doReturn(new AppraisalStatus(FAIL, "")).when(supplyChainCredentialValidator).
+ validateEndorsementCredential(eq(ec), any(KeyStore.class), eq(true));
+ doReturn(new AppraisalStatus(PASS, "")).when(supplyChainCredentialValidator).
+ validatePlatformCredential(eq(pc), any(KeyStore.class), eq(true));
+ doReturn(new AppraisalStatus(PASS, "")).when(supplyChainCredentialValidator).
+ validatePlatformCredentialAttributes(eq(pc), any(DeviceInfoReport.class),
+ any(EndorsementCredential.class));
+
+ Assert.assertEquals(service.validateSupplyChain(ec, pcs,
+ device).getOverallValidationResult(), PASS);
+ verify(supplyChainValidationSummaryDBManager).save(any(SupplyChainValidationSummary.class));
+ }
+
+ /**
+ * Pc cert not enabled, all others pass.
+ */
+ @Test
+ public final void testNoPcValidation() {
+ when(policy.isEcValidationEnabled()).thenReturn(true);
+ when(policy.isPcValidationEnabled()).thenReturn(false);
+ when(policy.isPcAttributeValidationEnabled()).thenReturn(true);
+ when(policy.isExpiredCertificateValidationEnabled()).thenReturn(true);
+
+ doReturn(new AppraisalStatus(PASS, "")).when(supplyChainCredentialValidator).
+ validateEndorsementCredential(eq(ec), any(KeyStore.class), eq(true));
+ doReturn(new AppraisalStatus(FAIL, "")).when(supplyChainCredentialValidator).
+ validatePlatformCredential(eq(pc), any(KeyStore.class), eq(true));
+ doReturn(new AppraisalStatus(PASS, "")).when(supplyChainCredentialValidator).
+ validatePlatformCredentialAttributes(eq(pc), any(DeviceInfoReport.class),
+ any(EndorsementCredential.class));
+
+ Assert.assertEquals(service.validateSupplyChain(ec, pcs,
+ device).getOverallValidationResult(), PASS);
+ verify(supplyChainValidationSummaryDBManager).save(any(SupplyChainValidationSummary.class));
+ }
+
+ /**
+ * Pc attrib not enabled, all others pass.
+ */
+ @Test
+ public final void testNoPcAttributeValidation() {
+ when(policy.isEcValidationEnabled()).thenReturn(true);
+ when(policy.isPcValidationEnabled()).thenReturn(true);
+ when(policy.isPcAttributeValidationEnabled()).thenReturn(false);
+ when(policy.isExpiredCertificateValidationEnabled()).thenReturn(true);
+
+ doReturn(new AppraisalStatus(PASS, "")).when(supplyChainCredentialValidator).
+ validateEndorsementCredential(eq(ec), any(KeyStore.class), eq(true));
+ doReturn(new AppraisalStatus(PASS, "")).when(supplyChainCredentialValidator).
+ validatePlatformCredential(eq(pc), any(KeyStore.class), eq(true));
+ doReturn(new AppraisalStatus(FAIL, "")).when(supplyChainCredentialValidator).
+ validatePlatformCredentialAttributes(eq(pc), any(DeviceInfoReport.class),
+ any(EndorsementCredential.class));
+
+ Assert.assertEquals(service.validateSupplyChain(ec, pcs,
+ device).getOverallValidationResult(), PASS);
+ verify(supplyChainValidationSummaryDBManager).save(any(SupplyChainValidationSummary.class));
+ }
+
+
+
+ /**
+ * All enabled, EC is null.
+ */
+ @Test
+ public final void testNullEcValidation() {
+ when(policy.isEcValidationEnabled()).thenReturn(true);
+ Assert.assertEquals(service.validateSupplyChain(null, pcs,
+ device).getOverallValidationResult(), FAIL);
+ }
+
+ /**
+ * All enabled, PC is null. Then PC set is empty.
+ */
+ @Test
+ public final void testNullPcValidation() {
+ when(policy.isEcValidationEnabled()).thenReturn(false);
+ when(policy.isPcValidationEnabled()).thenReturn(true);
+ Assert.assertEquals(service.validateSupplyChain(ec, null,
+ device).getOverallValidationResult(), FAIL);
+ final HashSet emptySet = new HashSet<>();
+ Assert.assertEquals(service.validateSupplyChain(ec, emptySet,
+ device).getOverallValidationResult(), FAIL);
+ }
+
+ /**
+ * All enabled, PC is null. Then PC set is empty.
+ */
+ @Test
+ public final void testNullPcAttributeValidation() {
+ when(policy.isEcValidationEnabled()).thenReturn(false);
+ when(policy.isPcValidationEnabled()).thenReturn(false);
+ when(policy.isPcAttributeValidationEnabled()).thenReturn(true);
+ Assert.assertEquals(service.validateSupplyChain(ec, null,
+ device).getOverallValidationResult(), FAIL);
+ final HashSet emptySet = new HashSet<>();
+ Assert.assertEquals(service.validateSupplyChain(ec, emptySet,
+ device).getOverallValidationResult(), FAIL);
+ }
+
+ /**
+ * Puts an EC, STM CA, and GS CA in the DB, attempts to retrieve the CAs from the EC.
+ * @throws URISyntaxException failed to parse certificate file location.
+ * @throws IOException couldn't create certificates from file.
+ * @throws KeyStoreException was unable to retrieve keystore.
+ */
+ @Test
+ public final void testGetCaChain() throws URISyntaxException, IOException, KeyStoreException {
+ CertificateManager realCertMan = new DBCertificateManager(sessionFactory);
+
+ // the main service in this class only uses mocked managers, we need a real DB certificate
+ // manager for this test, so we make a second service.
+ SupplyChainValidationServiceImpl mostlyMockedService = new SupplyChainValidationServiceImpl(
+ policyManager,
+ appraiserManager,
+ realCertMan,
+ supplyChainValidationSummaryDBManager,
+ supplyChainCredentialValidator
+ );
+
+ CertificateAuthorityCredential globalSignCaCert = new CertificateAuthorityCredential(
+ Files.readAllBytes(Paths.get(getClass().getResource(
+ GS_ROOT_CA).toURI())));
+
+ CertificateAuthorityCredential rootCa = new CertificateAuthorityCredential(
+ Files.readAllBytes(Paths.get(getClass().getResource(
+ STM_ROOT_CA).toURI()))
+ );
+
+ EndorsementCredential endorsementCredential = new EndorsementCredential(
+ Files.readAllBytes(Paths.get(getClass().getResource(
+ NUC1_EC).toURI())));
+
+ realCertMan.save(endorsementCredential);
+ realCertMan.save(rootCa);
+ realCertMan.save(globalSignCaCert);
+
+ KeyStore ks = mostlyMockedService.getCaChain(endorsementCredential);
+
+ String stmCaAlias = rootCa.getId().toString();
+ String gsCaAlias = globalSignCaCert.getId().toString();
+
+ Assert.assertNotNull(ks.getCertificate(stmCaAlias));
+ Assert.assertNotNull(ks.getCertificate(gsCaAlias));
+ Assert.assertEquals(ks.size(), 2);
+
+ realCertMan.delete(endorsementCredential);
+ realCertMan.delete(rootCa);
+ realCertMan.delete(globalSignCaCert);
+ }
+
+ /**
+ * Puts an EC, and STM CA in the DB, attempts to retrieve the CAs from the EC. The STM CA
+ * points to a GS CA that is not present.
+ * @throws URISyntaxException failed to parse certificate file location.
+ * @throws IOException couldn't create certificates from file.
+ * @throws KeyStoreException was unable to retrieve keystore.
+ */
+ @Test
+ public final void testGetNotFullCaChain() throws URISyntaxException, IOException,
+ KeyStoreException {
+ CertificateManager realCertMan = new DBCertificateManager(sessionFactory);
+
+ // the main service in this class only uses mocked managers, we need a real DB certificate
+ // manager for this test, so we make a second service.
+ SupplyChainValidationServiceImpl mostlyMockedService = new SupplyChainValidationServiceImpl(
+ policyManager,
+ appraiserManager,
+ realCertMan,
+ supplyChainValidationSummaryDBManager,
+ supplyChainCredentialValidator
+ );
+
+ CertificateAuthorityCredential rootCa = new CertificateAuthorityCredential(
+ Files.readAllBytes(Paths.get(getClass().getResource(
+ STM_ROOT_CA).toURI()))
+ );
+
+ EndorsementCredential endorsementCredential = new EndorsementCredential(
+ Files.readAllBytes(Paths.get(getClass().getResource(
+ NUC1_EC).toURI())));
+
+ realCertMan.save(endorsementCredential);
+ realCertMan.save(rootCa);
+
+ KeyStore ks = mostlyMockedService.getCaChain(endorsementCredential);
+
+ String stmCaAlias = rootCa.getId().toString();
+
+ Assert.assertNotNull(ks.getCertificate(stmCaAlias));
+ Assert.assertEquals(ks.size(), 1);
+
+ realCertMan.delete(endorsementCredential);
+ realCertMan.delete(rootCa);
+ }
+
+ /**
+ * Puts an EC in the DB, attempts to retrieve the CA from the EC.
+ * @throws URISyntaxException failed to parse certificate file location.
+ * @throws IOException couldn't create certificates from file.
+ * @throws KeyStoreException was unable to retrieve keystore.
+ */
+ @Test
+ public final void testGetEmptyCaChain() throws URISyntaxException, IOException,
+ KeyStoreException {
+ CertificateManager realCertMan = new DBCertificateManager(sessionFactory);
+
+ // the main service in this class only uses mocked managers, we need a real DB certificate
+ // manager for this test, so we make a second service.
+ SupplyChainValidationServiceImpl mostlyMockedService = new SupplyChainValidationServiceImpl(
+ policyManager,
+ appraiserManager,
+ realCertMan,
+ supplyChainValidationSummaryDBManager,
+ supplyChainCredentialValidator
+ );
+
+ EndorsementCredential endorsementCredential = new EndorsementCredential(
+ Files.readAllBytes(Paths.get(getClass().getResource(
+ NUC1_EC).toURI())));
+
+ realCertMan.save(endorsementCredential);
+
+ KeyStore ks = mostlyMockedService.getCaChain(endorsementCredential);
+
+ Assert.assertEquals(ks.size(), 0);
+
+ realCertMan.delete(endorsementCredential);
+ }
+
+ /**
+ * Puts an EC, STM CA, GS CA, and an Intel CA in the DB, attempts to retrieve the CAs
+ * from the EC.
+ * @throws URISyntaxException failed to parse certificate file location.
+ * @throws IOException couldn't create certificates from file.
+ * @throws KeyStoreException was unable to retrieve keystore.
+ */
+ @Test
+ public final void testGetCaChainWithExtraCerts() throws URISyntaxException, IOException,
+ KeyStoreException {
+ CertificateManager realCertMan = new DBCertificateManager(sessionFactory);
+
+ // the main service in this class only uses mocked managers, we need a real DB certificate
+ // manager for this test, so we make a second service.
+ SupplyChainValidationServiceImpl mostlyMockedService = new SupplyChainValidationServiceImpl(
+ policyManager,
+ appraiserManager,
+ realCertMan,
+ supplyChainValidationSummaryDBManager,
+ supplyChainCredentialValidator
+ );
+
+ CertificateAuthorityCredential globalSignCaCert = new CertificateAuthorityCredential(
+ Files.readAllBytes(Paths.get(getClass().getResource(
+ GS_ROOT_CA).toURI())));
+
+ CertificateAuthorityCredential rootCa = new CertificateAuthorityCredential(
+ Files.readAllBytes(Paths.get(getClass().getResource(
+ STM_ROOT_CA).toURI()))
+ );
+
+ CertificateAuthorityCredential intelCa = new CertificateAuthorityCredential(
+ Files.readAllBytes(Paths.get(getClass().getResource(
+ INTEL_CA).toURI()))
+ );
+
+ EndorsementCredential endorsementCredential = new EndorsementCredential(
+ Files.readAllBytes(Paths.get(getClass().getResource(
+ NUC1_EC).toURI())));
+
+ realCertMan.save(endorsementCredential);
+ realCertMan.save(rootCa);
+ realCertMan.save(globalSignCaCert);
+ realCertMan.save(intelCa);
+
+ KeyStore ks = mostlyMockedService.getCaChain(endorsementCredential);
+
+ String stmCaAlias = rootCa.getId().toString();
+ String gsCaAlias = globalSignCaCert.getId().toString();
+
+ Assert.assertNotNull(ks.getCertificate(stmCaAlias));
+ Assert.assertNotNull(ks.getCertificate(gsCaAlias));
+ Assert.assertEquals(ks.size(), 2);
+
+ realCertMan.delete(endorsementCredential);
+ realCertMan.delete(rootCa);
+ realCertMan.delete(globalSignCaCert);
+ realCertMan.delete(intelCa);
+ }
+
+ /**
+ * Puts an Intel PC and Intel CA in the DB, attempts to retrieve the CA from the PC.
+ * @throws URISyntaxException failed to parse certificate file location.
+ * @throws IOException couldn't create certificates from file.
+ * @throws KeyStoreException was unable to retrieve keystore.
+ */
+ @Test
+ public final void testGetPcCaChain() throws URISyntaxException, IOException, KeyStoreException {
+ CertificateManager realCertMan = new DBCertificateManager(sessionFactory);
+
+ // the main service in this class only uses mocked managers, we need a real DB certificate
+ // manager for this test, so we make a second service.
+ SupplyChainValidationServiceImpl mostlyMockedService = new SupplyChainValidationServiceImpl(
+ policyManager,
+ appraiserManager,
+ realCertMan,
+ supplyChainValidationSummaryDBManager,
+ supplyChainCredentialValidator
+ );
+
+ CertificateAuthorityCredential intelCa = new CertificateAuthorityCredential(
+ Files.readAllBytes(Paths.get(getClass().getResource(
+ INTEL_CA).toURI()))
+ );
+
+ PlatformCredential platformCredential = new PlatformCredential(
+ Files.readAllBytes(Paths.get(getClass().getResource(
+ NUC_PC).toURI())));
+
+ realCertMan.save(platformCredential);
+ realCertMan.save(intelCa);
+
+ KeyStore ks = mostlyMockedService.getCaChain(platformCredential);
+
+ String intelCaAlias = intelCa.getId().toString();
+
+ Assert.assertNotNull(ks.getCertificate(intelCaAlias));
+ Assert.assertEquals(ks.size(), 1);
+
+ realCertMan.delete(platformCredential);
+ realCertMan.delete(intelCa);
+ }
+
+ /**
+ * Puts an Intel PC, STM CA, and GS CA in the DB, attempts to retrieve the CAs from the PC. None
+ * should match.
+ * @throws URISyntaxException failed to parse certificate file location.
+ * @throws IOException couldn't create certificates from file.
+ * @throws KeyStoreException was unable to retrieve keystore.
+ */
+ @Test
+ public final void testGetPcCaChainNoMatches() throws URISyntaxException, IOException,
+ KeyStoreException {
+ CertificateManager realCertMan = new DBCertificateManager(sessionFactory);
+
+ // the main service in this class only uses mocked managers, we need a real DB certificate
+ // manager for this test, so we make a second service.
+ SupplyChainValidationServiceImpl mostlyMockedService = new SupplyChainValidationServiceImpl(
+ policyManager,
+ appraiserManager,
+ realCertMan,
+ supplyChainValidationSummaryDBManager,
+ supplyChainCredentialValidator
+ );
+
+ CertificateAuthorityCredential globalSignCaCert = new CertificateAuthorityCredential(
+ Files.readAllBytes(Paths.get(getClass().getResource(
+ GS_ROOT_CA).toURI())));
+
+ CertificateAuthorityCredential rootCa = new CertificateAuthorityCredential(
+ Files.readAllBytes(Paths.get(getClass().getResource(
+ STM_ROOT_CA).toURI()))
+ );
+
+ PlatformCredential platformCredential = new PlatformCredential(
+ Files.readAllBytes(Paths.get(getClass().getResource(
+ NUC_PC).toURI())));
+
+ realCertMan.save(platformCredential);
+ realCertMan.save(rootCa);
+ realCertMan.save(globalSignCaCert);
+
+ KeyStore ks = mostlyMockedService.getCaChain(platformCredential);
+
+ Assert.assertEquals(ks.size(), 0);
+
+ realCertMan.delete(platformCredential);
+ realCertMan.delete(rootCa);
+ realCertMan.delete(globalSignCaCert);
+ }
+
+ /**
+ * Puts an STM intermediate CA, STM 'root' CA, and GlobalSign root CA into the in-memory
+ * database, and then runs supply chain validation on a given endorsement credential.
+ *
+ * @throws URISyntaxException if building the path to a certificate resource fails
+ * @throws IOException if there is a problem deserializing a certificate
+ */
+ @Test
+ public void testVerifyEcAgainstCaChain() throws URISyntaxException, IOException {
+ when(policy.isEcValidationEnabled()).thenReturn(true);
+ when(policy.isPcValidationEnabled()).thenReturn(false);
+ when(policy.isPcAttributeValidationEnabled()).thenReturn(false);
+
+ CertificateManager realCertMan = new DBCertificateManager(sessionFactory);
+ Device storedDevice = getStoredTestDevice();
+
+ SupplyChainValidationServiceImpl mostlyMockedService = new SupplyChainValidationServiceImpl(
+ policyManager,
+ appraiserManager,
+ realCertMan,
+ supplyChainValidationSummaryDBManager,
+ new SupplyChainCredentialValidator()
+ );
+
+ CertificateAuthorityCredential stmEkRootCa = new CertificateAuthorityCredential(
+ Files.readAllBytes(Paths.get(getClass().getResource(
+ STM_ROOT_CA).toURI())));
+
+ CertificateAuthorityCredential stmTpmEkIntermediateCA = new CertificateAuthorityCredential(
+ Files.readAllBytes(Paths.get(getClass().getResource(
+ STM_TPM_EK_INTERMEDIATE_CA_02).toURI())));
+
+ CertificateAuthorityCredential globalSignTpmRoot = new CertificateAuthorityCredential(
+ Files.readAllBytes(Paths.get(getClass().getResource(
+ GS_ROOT_CA).toURI()))
+ );
+
+ realCertMan.save(stmTpmEkIntermediateCA);
+ realCertMan.save(globalSignTpmRoot);
+ realCertMan.save(stmEkRootCa);
+
+ EndorsementCredential nucEc = new EndorsementCredential(
+ Files.readAllBytes(Paths.get(getClass().getResource(NUC_EC).toURI()))
+ );
+
+ realCertMan.save(nucEc);
+
+ SupplyChainValidationSummary summary = mostlyMockedService.validateSupplyChain(
+ nucEc, Collections.emptySet(), storedDevice
+ );
+
+ Assert.assertEquals(summary.getOverallValidationResult(), PASS);
+ for (SupplyChainValidation validation : summary.getValidations()) {
+ Assert.assertEquals(
+ validation.getValidationType(),
+ SupplyChainValidation.ValidationType.ENDORSEMENT_CREDENTIAL
+ );
+ }
+
+ // verify the EC was updated with the test device object and saved in the cert man
+ EndorsementCredential updatedStoredEc =
+ EndorsementCredential.select(realCertMan).bySerialNumber(nucEc.getSerialNumber())
+ .getCertificate();
+
+ Assert.assertEquals(updatedStoredEc.getDevice().getId(), storedDevice.getId());
+
+ realCertMan.delete(stmTpmEkIntermediateCA);
+ realCertMan.delete(globalSignTpmRoot);
+ realCertMan.delete(stmEkRootCa);
+ realCertMan.delete(nucEc);
+ }
+
+ private Device getStoredTestDevice() {
+ DeviceManager deviceManager = new DBDeviceManager(sessionFactory);
+ DeviceGroupManager deviceGroupManager = new DBDeviceGroupManager(sessionFactory);
+
+ DeviceGroup testGroup = new DeviceGroup("group1");
+ Device testDevice = new Device("SCVSI-test");
+
+ testDevice.setDeviceGroup(deviceGroupManager.saveDeviceGroup(testGroup));
+ return deviceManager.saveDevice(testDevice);
+ }
+}
diff --git a/HIRS_AttestationCA/src/test/resources/certificates/IntelSigningKey_20April2017.pem b/HIRS_AttestationCA/src/test/resources/certificates/IntelSigningKey_20April2017.pem
new file mode 100644
index 000000000..bb74bdaa5
--- /dev/null
+++ b/HIRS_AttestationCA/src/test/resources/certificates/IntelSigningKey_20April2017.pem
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDrzCCApegAwIBAgIEJVDCpzANBgkqhkiG9w0BAQUFADCBhzELMAkGA1UEBhMCVVMxCzAJBgNV
+BAgTAkNBMRQwEgYDVQQHEwtTYW50YSBDbGFyYTEaMBgGA1UEChMRSW50ZWwgQ29ycG9yYXRpb24x
+ITAfBgNVBAsTGFRyYW5zcGFyZW50IFN1cHBseSBDaGFpbjEWMBQGA1UEAxMNd3d3LmludGVsLmNv
+bTAeFw0xNzA0MTkwMDAyMTBaFw0zNzEwMzEwMDAyMTBaMIGHMQswCQYDVQQGEwJVUzELMAkGA1UE
+CBMCQ0ExFDASBgNVBAcTC1NhbnRhIENsYXJhMRowGAYDVQQKExFJbnRlbCBDb3Jwb3JhdGlvbjEh
+MB8GA1UECxMYVHJhbnNwYXJlbnQgU3VwcGx5IENoYWluMRYwFAYDVQQDEw13d3cuaW50ZWwuY29t
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAo7Cu8Y3uWKoTQW/RnmNJG5h3PlYdvE2B
+v0c2+WUz0AprrVMpUvbJaNoMx47ev9CYvoxuJU0t9JjMm2i1u6Ol6VlK5yAWqr/qySvkryKTUuSx
+7sOtkayF4LqD16LlTIAmUhC0KabbrEitpBa1BtKsy8yhGjbm+bYs0D19jjqHH9rOT3I8j04tc5a4
+r86dPuCZX8RJTV8tZd51ULFhl+70rIwPY/Ecbl5lzx786v9xW3XnC4/XplvMJXw2wkUo8G98qyeD
+Odvha4Y49NswlqyUHnAlk+n4bIF7cvSDi+vb4YRaV2g5u2K3290ePUcNyzGTYgdpBnvOQlna/IZ0
+NifXQQIDAQABoyEwHzAdBgNVHQ4EFgQUIQWBMO10djQycITwJ5XfZw6L13MwDQYJKoZIhvcNAQEF
+BQADggEBADgcp+GI5S6HSVhQD7wyqKIBCLUk5nC0FzE0W1vBuLrwCagXHXwMC09uDXOXtfNOoQVZ
+duS3oaju5i40lSbFmp7V2hewQo6vlum/M05Lg/O7vndKxnKss/PeT5jMyjJ4t00HQ3KEm2aZL1BR
+AJZjbH+hZ08bh5pY5uP7husjjcur0xqhFw9Afq4SUbXtA47QBaUuJTtBwTf2r/Nesq4a6zRAtqF/
+gmGD8VOSahBrtuIS30GRgcpsk6kKO4fLCvCZwyJ2/Mn+ySMB3+G6XqgDekkykCnnvMVUr+nzasug
+d/4I62L3eZ2j5Xpa3O1kFZ+zIMrbLRh7Bu8OdjFdvodwmRU=
+-----END CERTIFICATE-----
diff --git a/HIRS_AttestationCA/src/test/resources/certificates/Intel_nuc_pc.cer b/HIRS_AttestationCA/src/test/resources/certificates/Intel_nuc_pc.cer
new file mode 100644
index 000000000..b2a05a2a6
Binary files /dev/null and b/HIRS_AttestationCA/src/test/resources/certificates/Intel_nuc_pc.cer differ
diff --git a/HIRS_AttestationCA/src/test/resources/certificates/STM TPM EK Intermediate CA.CER b/HIRS_AttestationCA/src/test/resources/certificates/STM TPM EK Intermediate CA.CER
new file mode 100644
index 000000000..3b258a9b4
Binary files /dev/null and b/HIRS_AttestationCA/src/test/resources/certificates/STM TPM EK Intermediate CA.CER differ
diff --git a/HIRS_AttestationCA/src/test/resources/certificates/nuc-1/ek_cert_7_byte_header_removed.cer b/HIRS_AttestationCA/src/test/resources/certificates/nuc-1/ek_cert_7_byte_header_removed.cer
new file mode 100644
index 000000000..f14093b1f
Binary files /dev/null and b/HIRS_AttestationCA/src/test/resources/certificates/nuc-1/ek_cert_7_byte_header_removed.cer differ
diff --git a/HIRS_AttestationCA/src/test/resources/certificates/nuc-1/ek_cert_untouched.cer b/HIRS_AttestationCA/src/test/resources/certificates/nuc-1/ek_cert_untouched.cer
new file mode 100644
index 000000000..fbcbe8fba
Binary files /dev/null and b/HIRS_AttestationCA/src/test/resources/certificates/nuc-1/ek_cert_untouched.cer differ
diff --git a/HIRS_AttestationCA/src/test/resources/certificates/nuc-1/tpmcert.pem b/HIRS_AttestationCA/src/test/resources/certificates/nuc-1/tpmcert.pem
new file mode 100755
index 000000000..e0fd403dc
--- /dev/null
+++ b/HIRS_AttestationCA/src/test/resources/certificates/nuc-1/tpmcert.pem
@@ -0,0 +1,26 @@
+-----BEGIN CERTIFICATE-----
+MIIEXjCCA0agAwIBAgIUS5gujeW5kYvYdMJZlIUT6s3F0cwwDQYJKoZIhvcNAQEF
+BQAwVTELMAkGA1UEBhMCQ0gxHjAcBgNVBAoTFVNUTWljcm9lbGVjdHJvbmljcyBO
+VjEmMCQGA1UEAxMdU1RNIFRQTSBFSyBJbnRlcm1lZGlhdGUgQ0EgMDIwHhcNMTQw
+MjIyMDAwMDAwWhcNMjQwMjIyMDAwMDAwWjAAMIIBNzAiBgkqhkiG9w0BAQcwFaIT
+MBEGCSqGSIb3DQEBCQQEVENQQQOCAQ8AMIIBCgKCAQEAsdTxu5pRjEOgA0tCNYgn
+NmAqLzIxBTBft4pMBGdEk922dvBLvQySN13YnvVF6FnYCc0Y+5hSAZiRCcXpr/M3
+6wx5YkePCPss06KQMujy3X9jwxTU0cDbKTjKCmFpQqCqiGIk2f7mss8yIABlwT3R
+cBBbcDpGn2wYi5s9UhUfCOQ6D7qEPKJEi5IQC7/oyu5zT5FMUANdsebxrYpALcKK
+8/mp5Rwj+xmaAg/+OC9jIeFGLYYu/hQr/1BPYSVicfuIFdc/0VzyJO5KMRozvV3I
+2dbzQwqUD4xUxPR+f7VC+3p641Mb7WobIZH7wJm2k0M8HWeErytA66WtAoueU89O
+iQIDAQABo4IBZDCCAWAwHwYDVR0jBBgwFoAUVx+Aa0fM55v6NZR87Yi40QBa4J4w
+QgYDVR0gBDswOTA3BgRVHSAAMC8wLQYIKwYBBQUHAgEWIWh0dHA6Ly93d3cuc3Qu
+Y29tL1RQTS9yZXBvc2l0b3J5LzBVBgNVHREBAf8ESzBJpEcwRTEWMBQGBWeBBQIB
+DAtpZDo1MzU0NEQyMDEXMBUGBWeBBQICDAxTVDMzWlAyNFBWU1AxEjAQBgVngQUC
+AwwHaWQ6MEQwQzB/BgNVHQkEeDB2MBYGBWeBBQIQMQ0wCwwDMS4yAgECAgF0MCAG
+BWeBBQISMRcwFQIBAAEB/6ADCgEBoQMKAQCiAwoBADA6BgNVBTQxMzAkMCIGCSqG
+SIb3DQEBBzAVohMwEQYJKoZIhvcNAQEJBARUQ1BBMAswCQYFKw4DAhoFADAMBgNV
+HRMBAf8EAjAAMBMGA1UdJQEB/wQJMAcGBWeBBQgBMA0GCSqGSIb3DQEBBQUAA4IB
+AQAb50G/d9D18ahy6RScXObaazgrNZHcF0otH9W1uJzXgSQPjFFYbHAh2+EGI8uD
+90Hj9XgZYmcGv0pUHcFw7msNamr3c/Or8+pLPnu5OZtr4jCEZ7/Z75v0Z825Ov8R
+N+JIxB9RT0Yd3KAPQsp4d45NHWOPBQPgBi/pW/eJqPO2MJD0uraRqAlNrUD3ppc7
+xxsmOoOhyUFcs14KyrgIWNazx+4EElAKU3PthU70cszFAQM2hw/EYBfRwQ5rVZd7
+V2x9hMC4POgACE6gVIDV/mHoZe6AfGQKveblJEX9gOccI28vnT14d0CwhN/SvgZF
+JigA9V7w26ecFRWXpm79utMU
+-----END CERTIFICATE-----
diff --git a/HIRS_AttestationCA/src/test/resources/certificates/nuc_ec.pem b/HIRS_AttestationCA/src/test/resources/certificates/nuc_ec.pem
new file mode 100755
index 000000000..eba757bdb
--- /dev/null
+++ b/HIRS_AttestationCA/src/test/resources/certificates/nuc_ec.pem
@@ -0,0 +1,26 @@
+-----BEGIN CERTIFICATE-----
+MIIEXjCCA0agAwIBAgIUBwCBhWf/NXkWkNLUBJRd9WsObccwDQYJKoZIhvcNAQEF
+BQAwVTELMAkGA1UEBhMCQ0gxHjAcBgNVBAoTFVNUTWljcm9lbGVjdHJvbmljcyBO
+VjEmMCQGA1UEAxMdU1RNIFRQTSBFSyBJbnRlcm1lZGlhdGUgQ0EgMDIwHhcNMTQw
+MjIzMDAwMDAwWhcNMjQwMjIzMDAwMDAwWjAAMIIBNzAiBgkqhkiG9w0BAQcwFaIT
+MBEGCSqGSIb3DQEBCQQEVENQQQOCAQ8AMIIBCgKCAQEAvcrvqYUTomoUC5zk5Jhd
+myVzoEe94eXX1YFHyElCCpLM4/86ZbADKTHeGwygR4AWClb0Jmmloj+aIRUY3pZD
+2GVxDnmD9CBS+60doM1cN0+D01hhg7J/dnaigAbFxPZauSyV9XfTqq1MQlxWpUEf
+J4IALc+MhVd0kqSzzqSDxoneu83w1Ssvmah73wqWpansqQRYr1D7ABbkvouO56iu
+4z6UditUSbrk3FrZBs+e73tzy9OAzQBg617kU+BKhHCRuRIPYk3tPXHq53Y7Jwvf
+CkiEVWAU+MEMZJc/RRIOnWdSdDMxHZVnaxywrC8KUKZ1G3id/GVJfeivPxZVRBdh
+sQIDAQABo4IBZDCCAWAwHwYDVR0jBBgwFoAUVx+Aa0fM55v6NZR87Yi40QBa4J4w
+QgYDVR0gBDswOTA3BgRVHSAAMC8wLQYIKwYBBQUHAgEWIWh0dHA6Ly93d3cuc3Qu
+Y29tL1RQTS9yZXBvc2l0b3J5LzBVBgNVHREBAf8ESzBJpEcwRTEWMBQGBWeBBQIB
+DAtpZDo1MzU0NEQyMDEXMBUGBWeBBQICDAxTVDMzWlAyNFBWU1AxEjAQBgVngQUC
+AwwHaWQ6MEQwQzB/BgNVHQkEeDB2MBYGBWeBBQIQMQ0wCwwDMS4yAgECAgF0MCAG
+BWeBBQISMRcwFQIBAAEB/6ADCgEBoQMKAQCiAwoBADA6BgNVBTQxMzAkMCIGCSqG
+SIb3DQEBBzAVohMwEQYJKoZIhvcNAQEJBARUQ1BBMAswCQYFKw4DAhoFADAMBgNV
+HRMBAf8EAjAAMBMGA1UdJQEB/wQJMAcGBWeBBQgBMA0GCSqGSIb3DQEBBQUAA4IB
+AQAAbZng7i2L22p05GpbURYk6o7bYH3LZ+nusEGvi0tRkpqr9Qc8vMp8fgYQMaZV
+8QiDa5JfYD3vzOjQBRvUdqz8UrzemsuErk4w3yzsBh2lIY54jcXWmJFVk4HVp2wV
+xL5EysIII9Fkt2gfcoPSGyIDX4p83Vou5nhNOQPowahMuS6BUfcBKzMM7pK40GUj
+N+cijK61zPkvAQArEkAnVNuTxvLS41WW3x1kTtkLUPuTh7SynNAYwoVfl19uNPOs
+UTxDrFA7But7Vo0xoj+zSBQqzk0Gp3Pldw6mOUc3uI1UBmVtQGRy7cgsbLJ3bu/6
+fLuuAG5/ywVpo4MiG+PkFYXh
+-----END CERTIFICATE-----
diff --git a/HIRS_AttestationCA/src/test/resources/certificates/platform_certs_2/Intel_pc.pem b/HIRS_AttestationCA/src/test/resources/certificates/platform_certs_2/Intel_pc.pem
new file mode 100644
index 000000000..9ea77f12c
Binary files /dev/null and b/HIRS_AttestationCA/src/test/resources/certificates/platform_certs_2/Intel_pc.pem differ
diff --git a/HIRS_AttestationCA/src/test/resources/certificates/stMicroCaCerts/gstpmroot.crt b/HIRS_AttestationCA/src/test/resources/certificates/stMicroCaCerts/gstpmroot.crt
new file mode 100644
index 000000000..f8ff7e0d3
Binary files /dev/null and b/HIRS_AttestationCA/src/test/resources/certificates/stMicroCaCerts/gstpmroot.crt differ
diff --git a/HIRS_AttestationCA/src/test/resources/certificates/stMicroCaCerts/stmtpmekroot.crt b/HIRS_AttestationCA/src/test/resources/certificates/stMicroCaCerts/stmtpmekroot.crt
new file mode 100644
index 000000000..1fad0434f
Binary files /dev/null and b/HIRS_AttestationCA/src/test/resources/certificates/stMicroCaCerts/stmtpmekroot.crt differ
diff --git a/HIRS_AttestationCA/src/test/resources/tpm2/ak.name b/HIRS_AttestationCA/src/test/resources/tpm2/ak.name
new file mode 100644
index 000000000..88c49f5cf
Binary files /dev/null and b/HIRS_AttestationCA/src/test/resources/tpm2/ak.name differ
diff --git a/HIRS_AttestationCA/src/test/resources/tpm2/ak.pub b/HIRS_AttestationCA/src/test/resources/tpm2/ak.pub
new file mode 100644
index 000000000..876532d3c
Binary files /dev/null and b/HIRS_AttestationCA/src/test/resources/tpm2/ak.pub differ
diff --git a/HIRS_AttestationCA/src/test/resources/tpm2/ek.pub b/HIRS_AttestationCA/src/test/resources/tpm2/ek.pub
new file mode 100644
index 000000000..ef64b8552
Binary files /dev/null and b/HIRS_AttestationCA/src/test/resources/tpm2/ek.pub differ
diff --git a/HIRS_AttestationCAPortal/build.gradle b/HIRS_AttestationCAPortal/build.gradle
new file mode 100644
index 000000000..83f92147e
--- /dev/null
+++ b/HIRS_AttestationCAPortal/build.gradle
@@ -0,0 +1,93 @@
+apply plugin: 'checkstyle'
+apply plugin: 'findbugs'
+apply plugin: 'java'
+apply plugin: 'jacoco'
+apply plugin: 'pmd'
+apply plugin: 'war'
+
+sourceCompatibility = 1.8
+
+repositories {
+ flatDir {
+ dirs 'libs'
+ }
+}
+
+dependencies {
+ providedCompile libs.servlet_api
+
+ compile(project(':HIRS_Utils')) {
+ exclude module: "javassist"
+ }
+ compile(project(':HIRS_AttestationCA'))
+
+ compile libs.commons_io
+ compile libs.commons_lang
+ compile libs.commons_upload
+ compile libs.gson
+ compile libs.guava // for com.google.common
+ compile libs.hibernate
+ compile libs.http
+ compile libs.jstl
+ compile libs.log4j2
+ compile libs.log4j2_web
+ compile libs.servlet_api
+ compile libs.spring_webmvc
+ compile 'org.springframework:spring-context-support:4.2.1.RELEASE'
+ compile 'org.hibernate:hibernate-validator:5.3.4.Final'
+
+ compileOnly libs.checkstyle
+ compileOnly libs.findbugs
+
+ runtime fileTree(dir: 'build/plugins', include: ['*.jar'])
+
+ testCompile 'org.hamcrest:hamcrest-all:1.3'
+ // override the servlet API for testing. Required for Spring Integration tests
+ testCompile 'javax.servlet:javax.servlet-api:3.1.0'
+
+ testCompile libs.hsqldb
+ testCompile libs.spring_test
+ testCompile libs.testng
+ testCompile libs.mockito
+ testCompile libs.testng
+ testCompile 'org.skyscreamer:jsonassert:1.2.3' // for .andExpect(content().json(json))
+
+ testRuntime 'com.jayway.jsonpath:json-path:2.1.0'
+}
+
+test {
+ useTestNG()
+}
+
+copyVersion.dependsOn compileJava
+war.dependsOn copyVersion
+war.dependsOn addPlugins
+
+war {
+ from(buildDir) {
+ include 'VERSION'
+ into 'WEB-INF/classes'
+ }
+ archiveName = 'HIRS_AttestationCAPortal.war'
+}
+
+ext.configDir = new File(projectDir, 'config')
+
+ext.checkstyleConfigDir = "$configDir/checkstyle"
+
+checkstyle {
+ toolVersion = '5.7'
+ configFile = checkstyleConfigFile
+ configProperties.put('basedir', checkstyleConfigDir)
+ ignoreFailures = false
+ showViolations = true
+}
+
+ext.findbugsConfigDir = "$configDir/findbugs"
+
+findbugs {
+ toolVersion = '3.0.0'
+ ignoreFailures = false
+ effort = 'max'
+ excludeFilter = new File(findbugsConfigDir, 'suppressions.xml')
+}
diff --git a/HIRS_AttestationCAPortal/config/checkstyle/suppressions.xml b/HIRS_AttestationCAPortal/config/checkstyle/suppressions.xml
new file mode 100644
index 000000000..ec0b37b09
--- /dev/null
+++ b/HIRS_AttestationCAPortal/config/checkstyle/suppressions.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
diff --git a/HIRS_AttestationCAPortal/config/findbugs/suppressions.xml b/HIRS_AttestationCAPortal/config/findbugs/suppressions.xml
new file mode 100644
index 000000000..be58a39bb
--- /dev/null
+++ b/HIRS_AttestationCAPortal/config/findbugs/suppressions.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/HIRS_AttestationCAPortal/libs/rendersnake-1.8.jar b/HIRS_AttestationCAPortal/libs/rendersnake-1.8.jar
new file mode 100644
index 000000000..d1c7c6f06
Binary files /dev/null and b/HIRS_AttestationCAPortal/libs/rendersnake-1.8.jar differ
diff --git a/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/datatables/Column.java b/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/datatables/Column.java
new file mode 100644
index 000000000..00bc840f1
--- /dev/null
+++ b/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/datatables/Column.java
@@ -0,0 +1,172 @@
+package hirs.attestationca.portal.datatables;
+
+import javax.validation.constraints.NotNull;
+
+import org.hibernate.validator.constraints.NotBlank;
+
+/**
+ * Java representation of a jQuery DataTables Column.
+ */
+public class Column {
+
+ /**
+ * Default constructor.
+ */
+ public Column() {
+ }
+
+ /**
+ * Constructor.
+ * @param data the data
+ * @param name the name
+ * @param searchable true if searchable
+ * @param orderable true if orderable
+ * @param search the Search structure.
+ */
+ public Column(final String data, final String name, final boolean searchable,
+ final boolean orderable, final Search search) {
+ this.data = data;
+ this.name = name;
+ this.searchable = searchable;
+ this.orderable = orderable;
+ this.search = search;
+ }
+
+ /**
+ * Column's data source.
+ *
+ * @see http://datatables.net/reference/option/columns.data
+ */
+ @NotBlank
+ private String data;
+
+ /**
+ * Column's name.
+ *
+ * @see http://datatables.net/reference/option/columns.name
+ */
+ private String name;
+
+ /**
+ * Flag to indicate if this column is searchable (true) or not (false).
+ *
+ * @see http://datatables.net/reference/option/columns.searchable
+ */
+ @NotNull
+ private boolean searchable;
+
+ /**
+ * Flag to indicate if this column is orderable (true) or not (false).
+ *
+ * @see http://datatables.net/reference/option/columns.orderable
+ */
+ @NotNull
+ private boolean orderable;
+
+ /**
+ * Search value to apply to this specific column.
+ */
+ @NotNull
+ private Search search;
+
+ /**
+ *
+ * @return the data
+ */
+ public String getData() {
+ return data;
+ }
+
+ /**
+ * Sets the data.
+ * @param data the data
+ */
+ public void setData(final String data) {
+ this.data = data;
+ }
+
+ /**
+ *
+ * @return the name
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Sets the name.
+ * @param name the name
+ */
+ public void setName(final String name) {
+ this.name = name;
+ }
+
+ /**
+ * Gets the searchable flag.
+ * @return true if searchable, false otherwise
+ */
+ public boolean isSearchable() {
+ return searchable;
+ }
+
+ /**
+ * Sets the searchable flag.
+ * @param searchable true if searchable, false otherwise
+ */
+ public void setSearchable(final boolean searchable) {
+ this.searchable = searchable;
+ }
+
+
+ /**
+ *
+ * @return true if orderable, false otherwise
+ */
+ public boolean isOrderable() {
+ return orderable;
+ }
+
+ /**
+ * Sets the orderable flag.
+ * @param orderable true if orderable, false otherwise
+ */
+ public void setOrderable(final boolean orderable) {
+ this.orderable = orderable;
+ }
+
+ /**
+ *
+ * @return the search
+ */
+ public Search getSearch() {
+ return search;
+ }
+
+ /**
+ * Sets the search.
+ * @param search the search
+ */
+ public void setSearch(final Search search) {
+ this.search = search;
+ }
+
+ /**
+ * Set the search value to apply to this column.
+ *
+ * @param searchValue if any, the search value to apply
+ */
+ public void setSearchValue(final String searchValue) {
+ this.search.setValue(searchValue);
+ }
+
+ @Override
+ public String toString() {
+ return "Column{"
+ + "data='" + data + '\''
+ + ", name='" + name + '\''
+ + ", searchable=" + searchable
+ + ", orderable=" + orderable
+ + ", search=" + search
+ + '}';
+ }
+}
diff --git a/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/datatables/DataTableInput.java b/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/datatables/DataTableInput.java
new file mode 100644
index 000000000..5371e0555
--- /dev/null
+++ b/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/datatables/DataTableInput.java
@@ -0,0 +1,295 @@
+package hirs.attestationca.portal.datatables;
+
+import org.apache.commons.lang3.StringUtils;
+import org.hibernate.validator.constraints.NotEmpty;
+import org.springframework.util.CollectionUtils;
+
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotNull;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Represents a data table input in a jQuery DataTable.
+ */
+public class DataTableInput {
+
+ private static final int DEFAULT_LENGTH = 10;
+
+ /**
+ * Default Constructor.
+ */
+ public DataTableInput() {
+ }
+
+ /**
+ * Constructor.
+ * @param draw the draw counter
+ * @param start the paging start indicator
+ * @param length the number of records in current draw
+ * @param search the search parameter
+ * @param order the orderings
+ * @param columns the columns of the input
+ */
+ public DataTableInput(final Integer draw, final Integer start, final Integer length,
+ final Search search, final List order,
+ final List columns) {
+ this.draw = draw;
+ this.start = start;
+ this.length = length;
+ this.search = search;
+ this.order.addAll(order);
+ this.columns.addAll(columns);
+ }
+
+ /**
+ * Draw counter. This is used by DataTables to ensure that the Ajax returns from server-side
+ * processing requests are drawn in sequence by DataTables (Ajax requests are asynchronous and
+ * thus can return out of sequence). This is used as part of the draw return parameter (see
+ * below).
+ */
+ @NotNull
+ @Min(0)
+ private int draw = 1;
+
+ /**
+ * Paging first record indicator. This is the start point in the current data set
+ * (0 index based - i.e. 0 is the first record).
+ */
+ @NotNull
+ @Min(0)
+ private int start = 0;
+
+ /**
+ * Number of records that the table can display in the current draw. It is expected that the
+ * number of records returned will be equal to this number,
+ * unless the server has fewer records to return. Note that this can be -1 to indicate that
+ * all records should be returned (although that
+ * negates any benefits of server-side processing!)
+ */
+ @NotNull
+ @Min(-1)
+ private int length = DEFAULT_LENGTH;
+
+ /**
+ * Global search parameter.
+ */
+ @NotNull
+ private Search search = new Search();
+
+ /**
+ * Order parameter.
+ */
+ @NotEmpty
+ private List order = new ArrayList<>();
+
+ /**
+ * Per-column search parameter.
+ */
+ @NotEmpty
+ private List columns = new ArrayList<>();
+
+
+ /**
+ *
+ * @return the draw counter
+ */
+ public int getDraw() {
+ return draw;
+ }
+
+ /**
+ * Sets the draw counter.
+ * @param draw the draw counter
+ */
+ public void setDraw(final int draw) {
+ this.draw = draw;
+ }
+
+ /**
+ * Gets the start indicator.
+ * @return the start indicator
+ */
+ public int getStart() {
+ return start;
+ }
+
+ /**
+ * Gets the start indicator.
+ * @param start the start indicator
+ */
+ public void setStart(final int start) {
+ this.start = start;
+ }
+
+ /**
+ *
+ * @return the table length for the current draw
+ */
+ public int getLength() {
+ return length;
+ }
+
+ /**
+ * Sets the table length for the current draw.
+ * @param length the table length for the current draw
+ */
+ public void setLength(final int length) {
+ this.length = length;
+ }
+
+ /**
+ *
+ * @return the Search
+ */
+ public Search getSearch() {
+ return search;
+ }
+
+ /**
+ * Sets the search.
+ * @param search the search
+ */
+ public void setSearch(final Search search) {
+ this.search = search;
+ }
+
+ /**
+ *
+ * @return the orders
+ */
+ public List getOrder() {
+ return order;
+ }
+
+ /**
+ * Sets the orders.
+ * @param order the orders
+ */
+ public void setOrder(final List order) {
+ this.order.clear();
+ this.order.addAll(order);
+ }
+
+ /**
+ * Gets the table columns.
+ * @return the columns
+ */
+ public List getColumns() {
+ return columns;
+ }
+
+ /**
+ * Sets the table columns.
+ * @param columns the columns
+ */
+ public void setColumns(final List columns) {
+ this.columns.clear();
+ this.columns.addAll(columns);
+ }
+
+ /**
+ *
+ * @return a {@link Map} of {@link Column} indexed by name
+ */
+ public Map getColumnsAsMap() {
+ Map map = new HashMap();
+ for (Column column : columns) {
+ map.put(column.getData(), column);
+ }
+ return map;
+ }
+
+ /**
+ * Find a column by its name.
+ *
+ * @param columnName the name of the column
+ * @return the given Column, or null if not found
+ */
+ public Column getColumn(final String columnName) {
+ if (columnName == null) {
+ return null;
+ }
+ for (Column column : columns) {
+ if (columnName.equals(column.getData())) {
+ return column;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Add a new column.
+ *
+ * @param columnName the name of the column
+ * @param searchable whether the column is searchable or not
+ * @param orderable whether the column is orderable or not
+ * @param searchValue if any, the search value to apply
+ */
+ public void addColumn(final String columnName, final boolean searchable,
+ final boolean orderable, final String searchValue) {
+ this.columns.add(new Column(columnName, "", searchable, orderable,
+ new Search(searchValue, false)));
+ }
+
+ /**
+ * Add an order on the given column.
+ *
+ * @param columnName the name of the column
+ * @param ascending whether the sorting is ascending or descending
+ */
+ public void addOrder(final String columnName, final boolean ascending) {
+ if (columnName == null) {
+ return;
+ }
+ for (int i = 0; i < columns.size(); i++) {
+ if (!columnName.equals(columns.get(i).getData())) {
+ continue;
+ }
+ order.add(new Order(i, ascending));
+ }
+ }
+
+ /**
+ * Gets the order column name, given the order ordinal value.
+ * @return the order column name
+ */
+ public String getOrderColumnName() {
+ // attempt to get the column property based on the order index.
+ String orderColumnName = "id";
+ List orders = getOrder();
+ if (!CollectionUtils.isEmpty(orders)) {
+ int orderColumnIndex = orders.get(0).getColumn();
+
+ final Column column = getColumns().get(orderColumnIndex);
+
+ // use the column's name as the order field for hibernate if set,
+ // otherwise, use the columns' data field
+ if (StringUtils.isNotEmpty(column.getName())) {
+ orderColumnName = column.getName();
+ } else {
+ orderColumnName = column.getData();
+ }
+ }
+ return orderColumnName;
+ }
+
+
+ /**
+ * Generates a string for this object.
+ * @return the string
+ */
+ @Override
+ public String toString() {
+ return "DataTableInput{"
+ + "draw=" + draw
+ + ", start=" + start
+ + ", length=" + length
+ + ", search=" + search
+ + ", order=" + order
+ + ", columns=" + columns
+ + '}';
+ }
+}
diff --git a/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/datatables/DataTableResponse.java b/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/datatables/DataTableResponse.java
new file mode 100644
index 000000000..5a9a7ad2b
--- /dev/null
+++ b/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/datatables/DataTableResponse.java
@@ -0,0 +1,120 @@
+package hirs.attestationca.portal.datatables;
+
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import hirs.FilteredRecordsList;
+
+/**
+ * A Wrapper for Data Table JSON responses. Allows Spring to serialize a data object with additional
+ * meta data required by data tables.
+ *
+ * @param the type of object that is being wrapped.
+ */
+public final class DataTableResponse {
+
+ private List data = new LinkedList();
+ private int draw;
+ private long recordsTotal;
+ private long recordsFiltered;
+
+ /**
+ * Default constructor.
+ */
+ public DataTableResponse() {
+
+ }
+
+ /**
+ * Builds a data table response using a FilteredRecordList.
+ * @param recordList the filtered recordd list
+ * @param inputQuery the data table input (used for draw)
+ */
+ public DataTableResponse(final FilteredRecordsList recordList,
+ final DataTableInput inputQuery) {
+ this(recordList, inputQuery.getDraw(),
+ recordList.getRecordsTotal(), recordList.getRecordsFiltered());
+ }
+
+ /**
+ * Constructs a data table response using the specified data with the data table specific
+ * information.
+ *
+ * @param data that is to be displayed by data table
+ * @param draw the originating draw request ID (usually from a web request)
+ * @param recordsTotal total number of records inside the data
+ * @param recordsFiltered number of records excluded from the request
+ */
+ public DataTableResponse(final List data, final int draw, final long recordsTotal,
+ final long recordsFiltered) {
+ setData(data);
+ this.draw = draw;
+ this.recordsTotal = recordsTotal;
+ this.recordsFiltered = recordsFiltered;
+ }
+
+ /**
+ * Gets the data table data.
+ * @return the data
+ */
+ public List getData() {
+ return Collections.unmodifiableList(data);
+ }
+
+ /**
+ * Sets the data table data.
+ * @param data the data
+ */
+ public void setData(final List data) {
+ this.data.clear();
+ this.data.addAll(data);
+ }
+
+ /**
+ * Gets the table draw index of the table.
+ * @return the draw index
+ */
+ public int getDraw() {
+ return draw;
+ }
+
+ /**
+ * Sets the table draw index of the table.
+ * @param draw the draw index
+ */
+ public void setDraw(final int draw) {
+ this.draw = draw;
+ }
+
+ /**
+ * Gets the total records.
+ * @return the total records count
+ */
+ public long getRecordsTotal() {
+ return recordsTotal;
+ }
+
+ /**
+ * Sets the total record count.
+ * @param recordsTotal the total records count
+ */
+ public void setRecordsTotal(final long recordsTotal) {
+ this.recordsTotal = recordsTotal;
+ }
+
+ /**
+ * Gets the total filtered record count.
+ * @return the total filtered record count
+ */
+ public long getRecordsFiltered() {
+ return recordsFiltered;
+ }
+
+ /**
+ * Sets the total filtered record count.
+ * @param recordsFiltered the total filtered record count
+ */
+ public void setRecordsFiltered(final long recordsFiltered) {
+ this.recordsFiltered = recordsFiltered;
+ }
+}
diff --git a/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/datatables/DataTableView.java b/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/datatables/DataTableView.java
new file mode 100644
index 000000000..94e606e20
--- /dev/null
+++ b/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/datatables/DataTableView.java
@@ -0,0 +1,45 @@
+package hirs.attestationca.portal.datatables;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import java.util.Map;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.springframework.http.MediaType;
+import org.springframework.web.servlet.view.AbstractUrlBasedView;
+
+/**
+ * Serializes the DataTableResponse from the view as JSON and writes it to the HTTP response.
+ *
+ */
+public class DataTableView extends AbstractUrlBasedView {
+
+ private static final Gson GSON = new GsonBuilder().create();
+ private static final String MODEL_FIELD;
+ static {
+ final String name = DataTableResponse.class.getSimpleName();
+ MODEL_FIELD = name.substring(0, 1).toLowerCase() + name.substring(1);
+ }
+
+ /**
+ * Serializes the DataTableResponse from the view as JSON and writes it to the HTTP response.
+ *
+ * @param model combined output Map (never {@code null}), with dynamic values taking precedence
+ * over static attributes
+ * @param request current HTTP request
+ * @param response current HTTP response
+ * @throws Exception if rendering failed
+ */
+ @Override
+ protected void renderMergedOutputModel(
+ final Map model,
+ final HttpServletRequest request,
+ final HttpServletResponse response) throws Exception {
+ response.setContentType(MediaType.APPLICATION_JSON_VALUE);
+ DataTableResponse dataTable = (DataTableResponse) model.get(MODEL_FIELD);
+ ServletOutputStream out = response.getOutputStream();
+ String json = GSON.toJson(dataTable);
+ out.print(json);
+ }
+}
diff --git a/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/datatables/Order.java b/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/datatables/Order.java
new file mode 100644
index 000000000..e27e85826
--- /dev/null
+++ b/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/datatables/Order.java
@@ -0,0 +1,111 @@
+package hirs.attestationca.portal.datatables;
+
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Pattern;
+
+
+/**
+ * Represents a column ordering with regards to a jQuery DataTable.
+ */
+public class Order {
+
+ /**
+ * Constructor.
+ */
+ public Order() {
+ }
+
+ /**
+ * Constructor.
+ * @param column the column index
+ * @param dir the order direction
+ */
+ public Order(final int column, final String dir) {
+ this.column = column;
+ this.dir = dir;
+ }
+
+ /**
+ * Constructor.
+ * @param column the column index
+ * @param isAscending true if ascending order
+ */
+ public Order(final int column, final boolean isAscending) {
+ this.column = column;
+ if (isAscending) {
+ this.dir = "asc";
+ } else {
+ this.dir = "desc";
+ }
+ }
+
+
+ /**
+ * Column to which ordering should be applied. This is an index reference
+ * to the columns array of information that is also submitted to the server.
+ */
+ @NotNull
+ @Min(0)
+ private int column;
+
+ /**
+ * Ordering direction for this column. It will be asc or desc to indicate ascending ordering or
+ * descending ordering, respectively.
+ */
+ @NotNull
+ @Pattern(regexp = "(desc|asc)")
+ private String dir;
+
+
+ /**
+ * Gets the column index.
+ * @return the column index
+ */
+ public int getColumn() {
+ return column;
+ }
+
+ /**
+ * Sets the column index.
+ * @param column the column index
+ */
+ public void setColumn(final int column) {
+ this.column = column;
+ }
+
+ /**
+ * Gets the direction order.
+ * @return the direction order
+ */
+ public String getDir() {
+ return dir;
+ }
+
+ /**
+ * Sets the direction order.
+ * @param dir the direction order
+ */
+ public void setDir(final String dir) {
+ this.dir = dir;
+ }
+
+ /**
+ *
+ * @return true if ascending order, false otherwise.
+ */
+ public boolean isAscending() {
+ if (dir.equalsIgnoreCase("asc")) {
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return "Order{"
+ + "column=" + column
+ + ", dir='" + dir + '\''
+ + '}';
+ }
+}
diff --git a/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/datatables/OrderedListQueryDataTableAdapter.java b/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/datatables/OrderedListQueryDataTableAdapter.java
new file mode 100644
index 000000000..eeed889a9
--- /dev/null
+++ b/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/datatables/OrderedListQueryDataTableAdapter.java
@@ -0,0 +1,78 @@
+package hirs.attestationca.portal.datatables;
+
+import org.hibernate.Criteria;
+import org.springframework.util.CollectionUtils;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import hirs.FilteredRecordsList;
+import hirs.persist.CriteriaModifier;
+import hirs.persist.OrderedListQuerier;
+
+/**
+ * A class to adapt the Javascript DataTable java class abstractions to the DBManager's getting
+ * of ordered lists.
+ * @param The type of object to query
+ */
+public final class OrderedListQueryDataTableAdapter {
+
+ private OrderedListQueryDataTableAdapter() {
+ // do not construct
+ }
+
+ /**
+ * Gets the ordered list of records using a default, no-op criteria modifier.
+ * @param clazz the type of objects to query for
+ * @param dbManager the db manager to execute the actual query
+ * @param dataTableInput the JS DataTable query abstraction
+ * @param orderColumnName the name of the column (java object field name) to query on
+ * @param the parameter type
+ * @return the filtered record list
+ */
+ public static FilteredRecordsList getOrderedList(final Class extends T> clazz,
+ final OrderedListQuerier dbManager,
+ final DataTableInput dataTableInput,
+ final String orderColumnName) {
+
+ CriteriaModifier defaultModifier = new CriteriaModifier() {
+ @Override
+ public void modify(final Criteria criteria) {
+ // Do nothing
+ }
+ };
+ return getOrderedList(clazz, dbManager, dataTableInput, orderColumnName, defaultModifier);
+ }
+
+ /**
+ * Gets the ordered list of records.
+ * @param clazz the type of objects to query for
+ * @param dbManager the db manager to execute the actual query
+ * @param dataTableInput the JS DataTable query abstraction
+ * @param orderColumnName the name of the column (java object field name) to query on
+ * @param criteriaModifier the criteria modifier
+ * @param the parameter type
+ * @return the filtered record list
+ */
+ public static FilteredRecordsList getOrderedList(final Class extends T> clazz,
+ final OrderedListQuerier dbManager, final DataTableInput dataTableInput,
+ final String orderColumnName,
+ final CriteriaModifier criteriaModifier) {
+
+ Map searchableColumnMap = new HashMap<>();
+ for (Column column : dataTableInput.getColumns()) {
+ searchableColumnMap.put(column.getData(), column.isSearchable());
+ }
+
+ List orders = dataTableInput.getOrder();
+ boolean isAscending = true;
+ if (!CollectionUtils.isEmpty(orders)) {
+ isAscending = orders.get(0).isAscending();
+ }
+
+ return dbManager.getOrderedList(clazz, orderColumnName, isAscending,
+ dataTableInput.getStart(), dataTableInput.getLength(),
+ dataTableInput.getSearch().getValue(),
+ searchableColumnMap, criteriaModifier);
+ }
+}
diff --git a/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/datatables/Search.java b/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/datatables/Search.java
new file mode 100644
index 000000000..7fd350312
--- /dev/null
+++ b/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/datatables/Search.java
@@ -0,0 +1,89 @@
+package hirs.attestationca.portal.datatables;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * Represents a jQuery DataTables search parameter.
+ */
+public class Search {
+
+ /**
+ * Default Constructor.
+ */
+ public Search() {
+ }
+
+ /**
+ * Constructor for a non-regex search.
+ * @param value the search value
+ */
+ public Search(final String value) {
+ this(value, false);
+ }
+
+ /**
+ * Constructor.
+ * @param value the search value
+ * @param regex the search regex
+ */
+ public Search(final String value, final boolean regex) {
+ this.value = value;
+ this.regex = regex;
+ }
+
+ /**
+ * Global search value. To be applied to all columns which have searchable as true.
+ */
+ @NotNull
+ private String value = "";
+
+ /**
+ * true if the global filter should be treated as a regular expression for advanced searching,
+ * false otherwise. Note that normally server-side processing scripts will not perform regular
+ * expression searching for performance reasons on large data sets,
+ * but it is technically possible and at the discretion of your script.
+ */
+ @NotNull
+ private boolean regex;
+
+
+ /**
+ *
+ * @return the global search value, applied to all columns.
+ */
+ public String getValue() {
+ return value;
+ }
+
+ /**
+ * Sets the global search value.
+ * @param value the global search value
+ */
+ public void setValue(final String value) {
+ this.value = value;
+ }
+
+ /**
+ *
+ * @return true if search should be treated as a regex, false otherwise
+ */
+ public boolean isRegex() {
+ return regex;
+ }
+
+ /**
+ * Sets the regex flag.
+ * @param regex true if the search should be treated as a regex, false otherwise
+ */
+ public void setRegex(final boolean regex) {
+ this.regex = regex;
+ }
+
+ @Override
+ public String toString() {
+ return "Search{"
+ + "value='" + value + '\''
+ + ", regex=" + regex
+ + '}';
+ }
+}
diff --git a/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/datatables/package-info.java b/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/datatables/package-info.java
new file mode 100644
index 000000000..f2a5b36eb
--- /dev/null
+++ b/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/datatables/package-info.java
@@ -0,0 +1,9 @@
+/**
+ * This packages provides classes to serve jQuery dataTable requests.
+ *
+ * DataTableInput, Column, Order, and Search from:
+ * https://github.com/darrachequesne/spring-data-jpa-datatables
+ *
+ * The rest of the package was unnecessary and introduced dependency conflicts.
+ */
+package hirs.attestationca.portal.datatables;
diff --git a/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/model/PolicyPageModel.java b/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/model/PolicyPageModel.java
new file mode 100644
index 000000000..1c0d39d13
--- /dev/null
+++ b/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/model/PolicyPageModel.java
@@ -0,0 +1,155 @@
+package hirs.attestationca.portal.model;
+
+import hirs.data.persist.SupplyChainPolicy;
+
+/**
+ * PolicyPage model object to demonstrate data exchange between policy.jsp page
+ * form form and controller.
+ */
+public class PolicyPageModel {
+
+ // Variables to communicate policy settings to page
+ private boolean enableEcValidation;
+ private boolean enablePcCertificateValidation;
+ private boolean enablePcCertificateAttributeValidation;
+
+ // Variables to get policy settings from page
+ private String pcValidate;
+ private String pcAttributeValidate;
+ private String ecValidate;
+
+ /**
+ * Constructor. Sets fields from policy.
+ *
+ * @param policy The supply chain policy
+ */
+ public PolicyPageModel(final SupplyChainPolicy policy) {
+ this.enableEcValidation = policy.isEcValidationEnabled();
+ this.enablePcCertificateValidation = policy.isPcValidationEnabled();
+ this.enablePcCertificateAttributeValidation = policy.isPcAttributeValidationEnabled();
+ }
+
+ /**
+ * Default constructor required by Spring.
+ */
+ public PolicyPageModel() {
+ }
+
+ /**
+ * Gets the EC Validation state.
+ *
+ * @return the validation state.
+ */
+ public boolean getEnableEcValidation() {
+ return enableEcValidation;
+ }
+
+ /**
+ * Gets the Platform Certificate Validation state.
+ *
+ * @return the validation state.
+ */
+ public boolean getEnablePcCertificateValidation() {
+ return enablePcCertificateValidation;
+ }
+
+ /**
+ * Gets the Platform Certificate AttributeValidation state.
+ *
+ * @return the validation state.
+ */
+ public boolean getEnablePcCertificateAttributeValidation() {
+ return enablePcCertificateAttributeValidation;
+ }
+
+ /**
+ * Gets the EC Validation value.
+ *
+ * @return the model string representation of this field (checked or unchecked)
+ */
+ public String getEcValidate() {
+ return ecValidate;
+ }
+
+ /**
+ * Gets the Platform Certificate Validation value.
+ *
+ * @return the model string representation of this field (checked or unchecked)
+ */
+ public String getPcValidate() {
+ return pcValidate;
+ }
+
+ /**
+ * Gets the platform certificate attribute validation value.
+ *
+ * @return the model string representation of this field (checked or unchecked)
+ */
+ public String getPcAttributeValidate() {
+ return pcAttributeValidate;
+ }
+
+ /**
+ * Sets the EC Validation state.
+ *
+ * @param enableEcValidation true if performing validation, false otherwise
+ */
+ public void setEnableEcValidation(final boolean enableEcValidation) {
+ this.enableEcValidation = enableEcValidation;
+ }
+
+ /**
+ * Sets the Platform Certificate Validation state.
+ *
+ * @param enablePcCertificateValidation true if performing validation, false otherwise
+ */
+ public void setEnablePcCertificateValidation(final boolean enablePcCertificateValidation) {
+ this.enablePcCertificateValidation = enablePcCertificateValidation;
+ }
+
+ /**
+ * Sets the Platform Certificate Attribute Validation state.
+ *
+ * @param enablePcCertificateAttributeValidation true if performing validation, false otherwise
+ */
+ public void setEnablePcCertificateAttributeValidation(
+ final boolean enablePcCertificateAttributeValidation) {
+ this.enablePcCertificateAttributeValidation = enablePcCertificateAttributeValidation;
+ }
+
+ /**
+ * Sets the Platform Certificate Validation state.
+ *
+ * @param pcValidate "checked" if enabling validation, false otherwise
+ */
+ public void setPcValidate(final String pcValidate) {
+ this.pcValidate = pcValidate;
+ }
+
+ /**
+ * Sets the EC Validation state.
+ *
+ * @param ecValidate "checked" if enabling validation, false otherwise
+ */
+ public void setEcValidate(final String ecValidate) {
+ this.ecValidate = ecValidate;
+ }
+
+ /**
+ * Sets the PC Attribute Validation state.
+ *
+ * @param pcAttributeValidate "checked" if enabling validation, false otherwise
+ */
+ public void setPcAttributeValidate(final String pcAttributeValidate) {
+ this.pcAttributeValidate = pcAttributeValidate;
+ }
+
+ @Override
+ public String toString() {
+ return "PolicyPageModel{"
+ + "enableEcValidation=" + enableEcValidation
+ + ", enablePcCertificateValidation=" + enablePcCertificateValidation
+ + ", enablePcCertificateAttributeValidation="
+ + enablePcCertificateAttributeValidation + '}';
+ }
+}
diff --git a/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/model/package-info.java b/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/model/package-info.java
new file mode 100644
index 000000000..ccb9c352b
--- /dev/null
+++ b/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/model/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * This packages provides data model classes for the HIRS Attestation CA portal.
+ */
+package hirs.attestationca.portal.model;
diff --git a/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/package-info.java b/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/package-info.java
new file mode 100644
index 000000000..e45b219af
--- /dev/null
+++ b/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Root Package for HIRS Attestation CA Portal.
+ */
+package hirs.attestationca.portal;
diff --git a/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/page/CommonPageConfiguration.java b/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/page/CommonPageConfiguration.java
new file mode 100644
index 000000000..f8d343b0a
--- /dev/null
+++ b/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/page/CommonPageConfiguration.java
@@ -0,0 +1,80 @@
+package hirs.attestationca.portal.page;
+
+import hirs.attestationca.portal.datatables.DataTableView;
+import hirs.attestationca.portal.persistence.PersistenceConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
+import org.springframework.web.multipart.commons.CommonsMultipartResolver;
+import org.springframework.web.servlet.ViewResolver;
+import org.springframework.web.servlet.config.annotation.EnableWebMvc;
+import org.springframework.web.servlet.view.InternalResourceViewResolver;
+import org.springframework.web.servlet.view.UrlBasedViewResolver;
+
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Specifies the location to scan for page controllers, view resolver for JSON data, and view
+ * resolver to map view names to jsp files.
+ */
+@Configuration
+@EnableWebMvc
+@ComponentScan("hirs.attestationca.portal.page.controllers")
+@Import({ PersistenceConfiguration.class })
+public class CommonPageConfiguration {
+
+
+ /**
+ * @return bean to resolve injected annotation.Value
+ * property expressions for beans.
+ */
+ @Bean
+ public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
+ return new PropertySourcesPlaceholderConfigurer();
+ }
+
+ /**
+ * Makes all URLs that end in "dataTable" use DataTableView to serialize DataTableResponse.
+ *
+ * @return ViewResolver that uses DataTableView.
+ */
+ @Bean
+ public ViewResolver dataTableViewResolver() {
+ UrlBasedViewResolver resolver = new UrlBasedViewResolver();
+ resolver.setViewClass(DataTableView.class);
+ resolver.setViewNames("*dataTable");
+ resolver.setOrder(0);
+ return resolver;
+ }
+
+ /**
+ * Maps view names to the appropriate jsp file.
+ *
+ * Only seems to apply to GET requests.
+ *
+ * @return a ViewResolver bean containing the mapping.
+ */
+ @Bean
+ public ViewResolver pageViewResolver() {
+ InternalResourceViewResolver resolver = new InternalResourceViewResolver();
+ resolver.setPrefix("/WEB-INF/jsp/");
+ resolver.setSuffix(".jsp");
+ return resolver;
+ }
+
+ /**
+ * Creates a Spring Resolver for Multi-part form uploads. This is required
+ * for spring controllers to be able to process Spring MultiPartFiles
+ *
+ * @return bean to handle multipart form requests
+ */
+ @Bean
+ public CommonsMultipartResolver multipartResolver() {
+ CommonsMultipartResolver resolver = new CommonsMultipartResolver();
+ resolver.setDefaultEncoding(StandardCharsets.UTF_8.name());
+ return resolver;
+ }
+
+}
diff --git a/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/page/Page.java b/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/page/Page.java
new file mode 100644
index 000000000..57f6b1470
--- /dev/null
+++ b/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/page/Page.java
@@ -0,0 +1,209 @@
+package hirs.attestationca.portal.page;
+
+import hirs.utils.VersionHelper;
+
+/**
+ * Contains attributes required to display a portal page and its menu link.
+ */
+public enum Page {
+
+ /**
+ * Site landing page.
+ */
+ INDEX("HIRS Attestation CA", "Version: " + VersionHelper.getVersion(),
+ null, false, false, null, null),
+ /**
+ * Page to import and manage trust chains.
+ */
+ TRUST_CHAIN("Trust Chain Management", "ic_store",
+ null, "certificate-request/"),
+ /**
+ * Page to display and manage endorsement key credentials.
+ */
+ ENDORSEMENT_KEY_CREDENTIALS("Endorsement Key Credentials", "ic_vpn_key",
+ "first", "certificate-request/"),
+ /**
+ * Page to display and manage platform credentials.
+ */
+ PLATFORM_CREDENTIALS("Platform Credentials", "ic_important_devices",
+ null, "certificate-request/"),
+ /**
+ * Page to display issued certificates.
+ */
+ ISSUED_CERTIFICATES("Issued Attestation Certificates", "ic_library_books",
+ null, "certificate-request/"),
+ /**
+ * Page to display certificate validation reports.
+ */
+ VALIDATION_REPORTS("Validation Reports", "ic_assignment", "first"),
+ /**
+ * Non-menu page to display certificate. Reachable from all certificate pages.
+ */
+ CERTIFICATE_DETAILS("Certificate Details", "", null, true, false, null, null),
+ /**
+ * Page to display registered devices.
+ */
+ DEVICES("Devices", "ic_devices", "first"),
+ /**
+ * Page that manages Attestation CA Policy.
+ */
+ POLICY("Policy", "ic_subtitles"),
+ /**
+ * Help page.
+ */
+ HELP("Help", "ic_live_help");
+
+ private final String title;
+ private final String subtitle;
+ private final String icon;
+
+ private final boolean hasMenu;
+ private final String menuLinkClass;
+ private final boolean inMenu;
+
+ private final String prefixPath;
+ private final String viewName;
+
+ /**
+ * Constructor for Page.
+ *
+ * @param title title of the page
+ * @param subtitle subtitle of the page
+ * @param icon icon for the page
+ * @param hasMenu the page has its own menu
+ * @param inMenu the page appears in a menu
+ * @param menuLinkClass the category to which this page belongs
+ * @param prefixPath prefix path that appears in the URL for this page
+ */
+ Page(final String title,
+ final String subtitle,
+ final String icon,
+ final boolean hasMenu,
+ final boolean inMenu,
+ final String menuLinkClass,
+ final String prefixPath) {
+ this.title = title;
+ this.subtitle = subtitle;
+ this.icon = icon;
+ this.hasMenu = hasMenu;
+ this.menuLinkClass = menuLinkClass;
+ this.inMenu = inMenu;
+ this.prefixPath = prefixPath;
+
+ viewName = this.name().toLowerCase().replaceAll("_", "-");
+ }
+
+ /**
+ * Constructor for Page.
+ *
+ * @param title title of the page
+ * @param icon icon for the page
+ * @param menuLinkClass the category to which this page belongs
+ * @param prefixPath prefix path that appears in the URL for this page
+ */
+ Page(final String title,
+ final String icon,
+ final String menuLinkClass,
+ final String prefixPath) {
+ this(title, null, icon, true, true, menuLinkClass, prefixPath);
+ }
+
+ /**
+ * Constructor for Page.
+ *
+ * @param title title of the page
+ * @param icon icon for the page
+ * @param menuLinkClass the category to which this page belongs
+ */
+ Page(final String title,
+ final String icon,
+ final String menuLinkClass) {
+ this(title, null, icon, true, true, menuLinkClass, null);
+ }
+
+ /**
+ * Constructor for Page.
+ *
+ * @param title title of the page
+ * @param icon icon for the page
+ */
+ Page(final String title,
+ final String icon) {
+ this(title, null, icon, true, true, null, null);
+ }
+
+ /**
+ * Returns the title of the page.
+ *
+ * @return the title of the page.
+ */
+ public String getTitle() {
+ return title;
+ }
+
+ /**
+ * Returns the subtitle of the page.
+ *
+ * @return the subtitle of the page.
+ */
+ public String getSubtitle() {
+ return subtitle;
+ }
+
+ /**
+ * Returns the base filename of the icon for page. E.g. "ic_my_icon", which will be appended
+ * with appropriate size string (_24dp/_48dp) and file extension (.png) when used.
+ *
+ * @return the base filename of the icon for page.
+ */
+ public String getIcon() {
+ return icon;
+ }
+
+ /**
+ * Returns true if the page should be displayed in the navigation menu.
+ *
+ * @return true if the page should be displayed in the navigation menu.
+ */
+ public boolean getInMenu() {
+ return inMenu;
+ }
+
+ /**
+ * Returns the css class to add to the menu link to display it appropriately. E.g. "first" if
+ * the link is the first in a group to separate it visually from the previous group.
+ *
+ * @return he class to add to the menu link to display it appropriately.
+ */
+ public String getMenuLinkClass() {
+ return menuLinkClass;
+ }
+
+ /**
+ * Returns true if the page should display the navigation menu.
+ *
+ * @return true if the page should display the navigation menu.
+ */
+ public boolean getHasMenu() {
+ return hasMenu;
+ }
+
+ /**
+ * Return the page's view name.
+ *
+ * @return the page's view name
+ */
+ public String getViewName() {
+ return viewName;
+ }
+
+ /**
+ * Return the page's view name.
+ *
+ * @return the page's view name
+ */
+ public String getPrefixPath() {
+ return prefixPath;
+ }
+
+}
diff --git a/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/page/PageConfiguration.java b/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/page/PageConfiguration.java
new file mode 100644
index 000000000..ba398cf28
--- /dev/null
+++ b/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/page/PageConfiguration.java
@@ -0,0 +1,14 @@
+package hirs.attestationca.portal.page;
+
+import hirs.attestationca.configuration.AttestationCertificateAuthorityConfiguration;
+import org.springframework.context.annotation.Import;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
+
+/**
+ * Main Spring configuration class for the ACA Portal. Uses the Common page configuration,
+ * as well as the ACA configuration for accessing the ACA certificate.
+ */
+@Import({ CommonPageConfiguration.class, AttestationCertificateAuthorityConfiguration.class })
+public class PageConfiguration extends WebMvcConfigurerAdapter {
+
+}
diff --git a/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/page/PageController.java b/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/page/PageController.java
new file mode 100644
index 000000000..db3c43bea
--- /dev/null
+++ b/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/page/PageController.java
@@ -0,0 +1,172 @@
+package hirs.attestationca.portal.page;
+
+import hirs.utils.BannerConfiguration;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Optional;
+import org.apache.http.client.utils.URIBuilder;
+import org.springframework.ui.ExtendedModelMap;
+import org.springframework.ui.Model;
+import org.springframework.ui.ModelMap;
+import org.springframework.web.bind.annotation.ModelAttribute;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.mvc.support.RedirectAttributes;
+import org.springframework.web.servlet.view.RedirectView;
+
+/**
+ * Abstract class to provide common functionality for page Controllers.
+ *
+ * @param
PageParams class used by the subclass.
+ */
+public abstract class PageController
{
+
+ /**
+ * Reserved attribute used by page.tag to identify a page's general
+ * information.
+ */
+ public static final String PAGE_ATTRIBUTE = "page";
+
+ /**
+ * Reserved attribute used by page.tag to identify the page collection used
+ * for navigation.
+ */
+ public static final String PAGES_ATTRIBUTE = "pages";
+
+ /**
+ * Reserved attribute used by page.tag to identify the banner information.
+ */
+ public static final String BANNER_ATTRIBUTE = "banner";
+
+ /**
+ * Reserved attribute used by page.tag to identify the messages the page
+ * should display.
+ */
+ public static final String MESSAGES_ATTRIBUTE = "messages";
+
+ private final Page page;
+
+ /**
+ * Constructor requiring the Page's display and routing specification.
+ *
+ * @param page The page specification for this controller.
+ */
+ public PageController(final Page page) {
+ this.page = page;
+ }
+
+
+ /**
+ * Returns the path for the view and the data model for the page.
+ *
+ * @param params The object to map url parameters into.
+ * @param model The data model for the request. Can contain data from
+ * redirect.
+ * @return the path for the view and data model for the page.
+ */
+ @RequestMapping
+ public abstract ModelAndView initPage(@ModelAttribute final P params, final Model model);
+
+ /**
+ * Creates a generic ModelAndView containing this page's configuration and
+ * the list of other pages for navigational purposes.
+ *
+ * @return A generic ModelAndView containing basic information for the page.
+ */
+ protected final ModelAndView getBaseModelAndView() {
+ return getBaseModelAndView(page);
+ }
+
+ /**
+ * Creates a generic ModelAndView containing the specify page
+ * configuration and the list of other pages for navigational
+ * purposes.
+ *
+ * @param newPage new page to get the model and view
+ * @return A generic ModelAndView containing basic information for the page.
+ */
+ protected final ModelAndView getBaseModelAndView(final Page newPage) {
+
+ ModelMap modelMap = new ExtendedModelMap();
+
+ // add page information
+ modelMap.addAttribute(PAGE_ATTRIBUTE, newPage);
+
+ // add other pages for navigation
+ modelMap.addAttribute(PAGES_ATTRIBUTE, Page.values());
+
+ // add banner information
+ try {
+ BannerConfiguration banner = new BannerConfiguration();
+ modelMap.addAttribute(BANNER_ATTRIBUTE, banner);
+ } catch (IOException ex) {
+ modelMap.addAttribute(BANNER_ATTRIBUTE, null);
+ }
+
+ return new ModelAndView(newPage.getViewName(), modelMap);
+
+ }
+
+ /**
+ * Redirects back to this controller's page with the specified data.
+ *
+ * @param params The url parameters to pass to the page.
+ * @param model The model data to pass to the page.
+ * @param attr The request's RedirectAttributes to hold the model data.
+ * @return RedirectView back to the page with the specified parameters.
+ * @throws URISyntaxException if malformed URI
+ */
+ protected final RedirectView redirectToSelf(
+ final P params,
+ final Map model,
+ final RedirectAttributes attr) throws URISyntaxException {
+
+ return redirectTo(page, params, model, attr);
+ }
+
+ /**
+ * Redirects controller's page with the specified data.
+ *
+ * @param newPage new page to get the model and view
+ * @param params The url parameters to pass to the page.
+ * @param model The model data to pass to the page.
+ * @param attr The request's RedirectAttributes to hold the model data.
+ * @return RedirectView back to the page with the specified parameters.
+ * @throws URISyntaxException if malformed URI
+ */
+ protected final RedirectView redirectTo(
+ final Page newPage,
+ final P params,
+ final Map model,
+ final RedirectAttributes attr) throws URISyntaxException {
+
+ // create uri with specified parameters
+ URIBuilder uri = new URIBuilder("../" + newPage.getViewName());
+
+ if (params != null) {
+ for (Entry e : params.asMap().entrySet()) {
+ Object v = Optional.ofNullable(e.getValue()).orElse("");
+ uri.addParameter(e.getKey(), v.toString());
+ }
+ }
+
+ // create view
+ RedirectView redirect = new RedirectView(uri.toString());
+
+ // do not put model attributes in the url
+ redirect.setExposeModelAttributes(false);
+
+ // add model data to forward to redirected page
+ if (model != null) {
+ for (Entry e : model.entrySet()) {
+ attr.addFlashAttribute(e.getKey(), e.getValue());
+ }
+ }
+
+ return redirect;
+
+ }
+
+}
diff --git a/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/page/PageMessages.java b/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/page/PageMessages.java
new file mode 100644
index 000000000..437070b76
--- /dev/null
+++ b/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/page/PageMessages.java
@@ -0,0 +1,70 @@
+package hirs.attestationca.portal.page;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Encapsulates error, success, and informational messages to display on a page.
+ */
+public class PageMessages {
+
+ private final List error = new ArrayList<>();
+ private final List success = new ArrayList<>();
+ private final List info = new ArrayList<>();
+
+ /**
+ * Returns the list of error messages.
+ *
+ * @return the list of error messages.
+ */
+ public List getError() {
+ return Collections.unmodifiableList(error);
+ }
+
+ /**
+ * Adds an error message.
+ *
+ * @param error the error message to add
+ */
+ public void addError(final String error) {
+ this.error.add(error);
+ }
+
+ /**
+ * Returns the list of success messages.
+ *
+ * @return the list of success messages.
+ */
+ public List getSuccess() {
+ return Collections.unmodifiableList(success);
+ }
+
+ /**
+ * Adds a success message.
+ *
+ * @param success the success message to add
+ */
+ public void addSuccess(final String success) {
+ this.success.add(success);
+ }
+
+ /**
+ * Returns the list of informational messages.
+ *
+ * @return the list of informational messages.
+ */
+ public List getInfo() {
+ return Collections.unmodifiableList(info);
+ }
+
+ /**
+ * Adds an informational message.
+ *
+ * @param info the informational message to add
+ */
+ public void addInfo(final String info) {
+ this.info.add(info);
+ }
+
+}
diff --git a/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/page/PageParams.java b/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/page/PageParams.java
new file mode 100644
index 000000000..0771facdd
--- /dev/null
+++ b/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/page/PageParams.java
@@ -0,0 +1,17 @@
+package hirs.attestationca.portal.page;
+
+import java.util.LinkedHashMap;
+
+/**
+ * Interface for a page's url parameters.
+ */
+public interface PageParams {
+
+ /**
+ * Allows PageController to iterate over the url parameters.
+ *
+ * @return map containing the object's url parameters.
+ */
+ LinkedHashMap asMap();
+
+}
diff --git a/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/page/controllers/CertificateDetailsPageController.java b/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/page/controllers/CertificateDetailsPageController.java
new file mode 100644
index 000000000..5e00d2e7b
--- /dev/null
+++ b/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/page/controllers/CertificateDetailsPageController.java
@@ -0,0 +1,122 @@
+package hirs.attestationca.portal.page.controllers;
+
+import hirs.attestationca.portal.page.PageController;
+import hirs.attestationca.portal.page.PageMessages;
+import hirs.attestationca.portal.page.params.CertificateDetailsPageParams;
+import hirs.attestationca.portal.util.CertificateStringMapBuilder;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.servlet.ModelAndView;
+
+import java.util.HashMap;
+import java.util.UUID;
+
+import static hirs.attestationca.portal.page.Page.CERTIFICATE_DETAILS;
+import hirs.persist.CertificateManager;
+import java.io.IOException;
+
+/**
+ * Controller for the Certificate Details page.
+ */
+@Controller
+@RequestMapping("/certificate-details")
+public class CertificateDetailsPageController extends PageController {
+
+ /**
+ * Model attribute name used by initPage for the initial data passed to the page.
+ */
+ static final String INITIAL_DATA = "initialData";
+
+ private final CertificateManager certificateManager;
+ private static final Logger LOGGER =
+ LogManager.getLogger(CertificateDetailsPageController.class);
+ /**
+ * Constructor providing the Page's display and routing specification.
+ * @param certificateManager the certificate manager
+ */
+ @Autowired
+ public CertificateDetailsPageController(final CertificateManager certificateManager) {
+ super(CERTIFICATE_DETAILS);
+ this.certificateManager = certificateManager;
+ }
+
+ /**
+ * Returns the path for the view and the data model for the page.
+ *
+ * @param params The object to map url parameters into.
+ * @param model The data model for the request. Can contain data from redirect.
+ * @return the path for the view and data model for the page.
+ */
+ @Override
+ @RequestMapping
+ public ModelAndView initPage(final CertificateDetailsPageParams params, final Model model) {
+ // get the basic information to render the page
+ ModelAndView mav = getBaseModelAndView();
+ PageMessages messages = new PageMessages();
+
+ // Map with the certificate information
+ HashMap data = new HashMap<>();
+
+ // Check if parameters were set
+ if (params.getId() == null) {
+ String typeError = "ID was not provided";
+ messages.addError(typeError);
+ LOGGER.error(typeError);
+ mav.addObject(MESSAGES_ATTRIBUTE, messages);
+ } else if (params.getType() == null) {
+ String typeError = "Type was not provided";
+ messages.addError(typeError);
+ LOGGER.error(typeError);
+ mav.addObject(MESSAGES_ATTRIBUTE, messages);
+ } else {
+ try {
+ String type = params.getType().toLowerCase();
+ UUID uuid = UUID.fromString(params.getId());
+ switch(type) {
+ case "certificateauthority":
+ data.putAll(CertificateStringMapBuilder.getCertificateAuthorityInformation(
+ uuid, certificateManager));
+ break;
+ case "endorsement":
+ data.putAll(CertificateStringMapBuilder.getEndorsementInformation(uuid,
+ certificateManager));
+ break;
+ case "platform":
+ data.putAll(CertificateStringMapBuilder.getPlatformInformation(uuid,
+ certificateManager));
+ break;
+ case "issued":
+ data.putAll(CertificateStringMapBuilder.getIssuedInformation(uuid,
+ certificateManager));
+ break;
+ default:
+ String typeError = "Invalid certificate type: " + params.getType();
+ messages.addError(typeError);
+ LOGGER.error(typeError);
+ mav.addObject(MESSAGES_ATTRIBUTE, messages);
+ break;
+ }
+ } catch (IllegalArgumentException | IOException ex) {
+ String uuidError = "Failed to parse ID from: " + params.getId();
+ messages.addError(uuidError);
+ LOGGER.error(uuidError, ex);
+ }
+
+ if (data.isEmpty()) {
+ String notFoundMessage = "Unable to find certificate with ID: " + params.getId();
+ messages.addError(notFoundMessage);
+ LOGGER.warn(notFoundMessage);
+ mav.addObject(MESSAGES_ATTRIBUTE, messages);
+ } else {
+ mav.addObject(INITIAL_DATA, data);
+ }
+ }
+
+ // return the model and view
+ return mav;
+ }
+}
diff --git a/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/page/controllers/CertificateRequestPageController.java b/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/page/controllers/CertificateRequestPageController.java
new file mode 100644
index 000000000..dec3304e6
--- /dev/null
+++ b/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/page/controllers/CertificateRequestPageController.java
@@ -0,0 +1,675 @@
+package hirs.attestationca.portal.page.controllers;
+
+import hirs.attestationca.portal.datatables.DataTableInput;
+import hirs.attestationca.portal.datatables.DataTableResponse;
+import hirs.attestationca.portal.datatables.OrderedListQueryDataTableAdapter;
+import hirs.attestationca.portal.page.Page;
+import hirs.attestationca.portal.page.PageController;
+import hirs.attestationca.portal.page.PageMessages;
+import hirs.attestationca.portal.page.params.NoPageParams;
+import hirs.attestationca.portal.util.CertificateStringMapBuilder;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.X509Certificate;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.logging.log4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.servlet.ModelAndView;
+
+import static org.apache.logging.log4j.LogManager.getLogger;
+import org.hibernate.Criteria;
+import org.hibernate.criterion.Restrictions;
+import org.hibernate.sql.JoinType;
+import hirs.FilteredRecordsList;
+import hirs.data.persist.certificate.Certificate;
+import hirs.data.persist.certificate.CertificateAuthorityCredential;
+import hirs.data.persist.certificate.EndorsementCredential;
+import hirs.data.persist.certificate.IssuedAttestationCertificate;
+import hirs.data.persist.certificate.PlatformCredential;
+import hirs.persist.CertificateManager;
+import hirs.persist.CriteriaModifier;
+import hirs.persist.CrudManager;
+import hirs.persist.DBManagerException;
+import hirs.persist.OrderedListQuerier;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.multipart.MultipartFile;
+import org.springframework.web.servlet.mvc.support.RedirectAttributes;
+import org.springframework.web.servlet.view.RedirectView;
+
+/**
+ * Controller for the Device page.
+ */
+@Controller
+@RequestMapping("/certificate-request")
+public class CertificateRequestPageController extends PageController {
+
+ private final CertificateManager certificateManager;
+ private final OrderedListQuerier dataTableQuerier;
+
+ private CertificateAuthorityCredential certificateAuthorityCredential;
+
+ private static final Logger LOGGER = getLogger(CertificateRequestPageController.class);
+
+ private static final String TRUSTCHAIN = "trust-chain";
+ private static final String PLATFORMCREDENTIAL = "platform-credentials";
+ private static final String ENDORSEMENTCREDENTIAL = "endorsement-key-credentials";
+ private static final String ISSUEDCERTIFICATES = "issued-certificates";
+
+ /**
+ * Model attribute name used by initPage for the aca cert info.
+ */
+ static final String ACA_CERT_DATA = "acaCertData";
+
+ /**
+ * Constructor providing the Page's display and routing specification.
+ *
+ * @param certificateManager the certificate manager
+ * @param crudManager the CRUD manager for certificates
+ * @param acaCertificate the ACA's X509 certificate
+ */
+ @Autowired
+ public CertificateRequestPageController(
+ final CertificateManager certificateManager,
+ final CrudManager crudManager,
+ final X509Certificate acaCertificate) {
+ super(Page.TRUST_CHAIN);
+ this.certificateManager = certificateManager;
+ this.dataTableQuerier = crudManager;
+
+ try {
+ certificateAuthorityCredential
+ = new CertificateAuthorityCredential(acaCertificate.getEncoded());
+ } catch (IOException e) {
+ LOGGER.error("Failed to read ACA certificate", e);
+ } catch (CertificateEncodingException e) {
+ LOGGER.error("Error getting encoded ACA certificate", e);
+ }
+ }
+
+ /**
+ * Returns the path for the view and the data model for the page.
+ *
+ * @param params The object to map url parameters into.
+ * @param model The data model for the request. Can contain data from
+ * redirect.
+ * @return the path for the view and data model for the page.
+ */
+ @Override
+ @RequestMapping
+ public ModelAndView initPage(final NoPageParams params, final Model model) {
+ return getBaseModelAndView();
+ }
+
+ /**
+ * Returns the path for the view and the data model for the page.
+ *
+ * @param certificateType String containing the certificate type
+ * @param params The object to map url parameters into.
+ * @param model The data model for the request. Can contain data from
+ * redirect.
+ * @return the path for the view and data model for the page.
+ */
+ @RequestMapping("/{certificateType}")
+ public ModelAndView initPage(@PathVariable("certificateType") final String certificateType,
+ final NoPageParams params, final Model model) {
+
+ ModelAndView mav = null;
+ HashMap data = new HashMap<>();
+ // add page information
+ switch (certificateType) {
+ case PLATFORMCREDENTIAL:
+ mav = getBaseModelAndView(Page.PLATFORM_CREDENTIALS);
+ break;
+ case ENDORSEMENTCREDENTIAL:
+ mav = getBaseModelAndView(Page.ENDORSEMENT_KEY_CREDENTIALS);
+ break;
+ case ISSUEDCERTIFICATES:
+ mav = getBaseModelAndView(Page.ISSUED_CERTIFICATES);
+ break;
+ case TRUSTCHAIN:
+ mav = getBaseModelAndView(Page.TRUST_CHAIN);
+ // Map with the ACA certificate information
+ data.putAll(CertificateStringMapBuilder.getCertificateAuthorityInformation(
+ certificateAuthorityCredential, this.certificateManager));
+ mav.addObject(ACA_CERT_DATA, data);
+ break;
+ default:
+ // send to an error page
+ break;
+ }
+
+ return mav;
+ }
+
+ /**
+ * Queries for the list of Certificates and returns a data table response
+ * with the records.
+ *
+ * @param certificateType String containing the certificate type
+ * @param input the DataTables search/query parameters
+ * @return the data table
+ */
+ @ResponseBody
+ @RequestMapping(value = "/{certificateType}/list",
+ produces = MediaType.APPLICATION_JSON_VALUE,
+ method = RequestMethod.GET)
+ @SuppressWarnings("unchecked")
+ public DataTableResponse extends Certificate> getTableData(
+ @PathVariable("certificateType") final String certificateType,
+ final DataTableInput input) {
+
+ LOGGER.debug("Handling list request: " + input);
+
+ // attempt to get the column property based on the order index.
+ String orderColumnName = input.getOrderColumnName();
+
+ LOGGER.debug("Ordering on column: " + orderColumnName);
+
+ // check that the alert is not archived and that it is in the specified report
+ CriteriaModifier criteriaModifier = new CriteriaModifier() {
+ @Override
+ public void modify(final Criteria criteria) {
+ criteria.add(Restrictions.isNull("archivedTime"));
+
+ // add a device alias if this query includes the device table
+ // for getting the device (e.g. device name).
+ // use left join, since device may be null. Query will return all
+ // Certs of this type, whether it has a Device or not (device field may be null)
+ if (hasDeviceTableToJoin(certificateType)) {
+ criteria.createAlias("device", "device", JoinType.LEFT_OUTER_JOIN);
+ }
+
+ }
+ };
+
+ FilteredRecordsList records
+ = OrderedListQueryDataTableAdapter.getOrderedList(
+ getCertificateClass(certificateType), dataTableQuerier,
+ input, orderColumnName, criteriaModifier);
+
+ // special parsing for platform credential
+ // Add the EndorsementCredential for each PlatformCredential based on the
+ // serial number. (pc.HolderSerialNumber = ec.SerialNumber)
+ if (certificateType.equals(PLATFORMCREDENTIAL)) {
+ EndorsementCredential associatedEC;
+
+ if (!records.isEmpty()) {
+ // loop all the platform certificates
+ for (int i = 0; i < records.size(); i++) {
+ PlatformCredential pc = (PlatformCredential) records.get(i);
+ // find the EC using the PC's "holder serial number"
+ associatedEC = EndorsementCredential
+ .select(certificateManager)
+ .bySerialNumber(pc.getHolderSerialNumber())
+ .getCertificate();
+
+ if (associatedEC != null) {
+ LOGGER.debug("EC ID for holder s/n " + pc
+ .getHolderSerialNumber() + " = " + associatedEC.getId());
+ }
+
+ pc.setEndorsementCredential(associatedEC);
+ }
+ }
+ }
+
+ LOGGER.debug("Returning list of size: " + records.size());
+ return new DataTableResponse<>(records, input);
+ }
+
+ /**
+ * Archives (soft delete) the credential.
+ *
+ * @param certificateType String containing the certificate type
+ * @param id the UUID of the cert to delete
+ * @param attr RedirectAttributes used to forward data back to the original
+ * page.
+ * @return redirect to this page
+ * @throws URISyntaxException if malformed URI
+ */
+ @RequestMapping(value = "/{certificateType}/delete", method = RequestMethod.POST)
+ public RedirectView delete(
+ @PathVariable("certificateType") final String certificateType,
+ @RequestParam final String id,
+ final RedirectAttributes attr) throws URISyntaxException {
+ LOGGER.info("Handling request to delete " + id);
+
+ Map model = new HashMap<>();
+ PageMessages messages = new PageMessages();
+
+ try {
+ UUID uuid = UUID.fromString(id);
+ Certificate certificate = getCertificateById(certificateType, uuid, certificateManager);
+ if (null == certificate) {
+ // Use the term "record" here to avoid user confusion b/t cert and cred
+ String notFoundMessage = "Unable to locate record with ID: " + uuid;
+ messages.addError(notFoundMessage);
+ LOGGER.warn(notFoundMessage);
+ } else {
+ certificate.archive();
+ certificateManager.update(certificate);
+
+ String deleteCompletedMessage = "Certificate successfully deleted";
+ messages.addInfo(deleteCompletedMessage);
+ LOGGER.info(deleteCompletedMessage);
+ }
+ } catch (IllegalArgumentException ex) {
+ String uuidError = "Failed to parse ID from: " + id;
+ messages.addError(uuidError);
+ LOGGER.error(uuidError, ex);
+ } catch (DBManagerException ex) {
+ String dbError = "Failed to archive cert: " + id;
+ messages.addError(dbError);
+ LOGGER.error(dbError, ex);
+ }
+
+ model.put(MESSAGES_ATTRIBUTE, messages);
+ return redirectTo(getCertificatePage(certificateType), new NoPageParams(), model, attr);
+ }
+
+ /**
+ * Handles request to download the cert by writing it to the response stream
+ * for download.
+ *
+ * @param certificateType String containing the certificate type
+ * @param id the UUID of the cert to download
+ * @param response the response object (needed to update the header with the
+ * file name)
+ * @throws java.io.IOException when writing to response output stream
+ */
+ @RequestMapping(value = "/{certificateType}/download", method = RequestMethod.GET)
+ public void download(
+ @PathVariable("certificateType") final String certificateType,
+ @RequestParam final String id,
+ final HttpServletResponse response)
+ throws IOException {
+ LOGGER.info("Handling request to download " + id);
+
+ try {
+ UUID uuid = UUID.fromString(id);
+ Certificate certificate = getCertificateById(certificateType, uuid, certificateManager);
+ if (null == certificate) {
+ // Use the term "record" here to avoid user confusion b/t cert and cred
+ String notFoundMessage = "Unable to locate record with ID: " + uuid;
+ LOGGER.warn(notFoundMessage);
+ // send a 404 error when invalid certificate
+ response.sendError(HttpServletResponse.SC_NOT_FOUND);
+ } else {
+ StringBuilder fileName = new StringBuilder("filename=\"");
+ fileName.append(getCertificateClass(certificateType).getSimpleName());
+ fileName.append("_");
+ fileName.append(certificate.getSerialNumber());
+ fileName.append(".cer\"");
+
+ // Set filename for download.
+ response.setHeader("Content-Disposition", "attachment;" + fileName);
+ response.setContentType("application/octet-stream");
+
+ // write cert to output stream
+ response.getOutputStream().write(certificate.getRawBytes());
+ }
+ } catch (IllegalArgumentException ex) {
+ String uuidError = "Failed to parse ID from: " + id;
+ LOGGER.error(uuidError, ex);
+ // send a 404 error when invalid certificate
+ response.sendError(HttpServletResponse.SC_NOT_FOUND);
+ }
+
+ }
+
+ /**
+ * Handles request to download the ACA cert by writing it to the response
+ * stream for download.
+ *
+ * @param response the response object (needed to update the header with the
+ * file name)
+ *
+ * @throws java.io.IOException when writing to response output stream
+ */
+ @ResponseBody
+ @RequestMapping(value = "/trust-chain/download-aca-cert", method = RequestMethod.GET)
+ public void downloadAcaCertificate(final HttpServletResponse response)
+ throws IOException {
+
+ // Set filename for download.
+ response.setHeader("Content-Disposition", "attachment; filename=\"hirs-aca-cert.cer\"");
+ response.setContentType("application/octet-stream");
+
+ // write cert to output stream
+ response.getOutputStream().write(certificateAuthorityCredential.getRawBytes());
+ }
+
+ /**
+ * Upload and processes a credential.
+ *
+ * @param certificateType String containing the certificate type
+ * @param files the files to process
+ * @param attr the redirection attributes
+ * @return the redirection view
+ * @throws URISyntaxException if malformed URI
+ */
+ @RequestMapping(value = "/{certificateType}/upload", method = RequestMethod.POST)
+ protected RedirectView upload(
+ @PathVariable("certificateType") final String certificateType,
+ @RequestParam("file") final MultipartFile[] files,
+ final RedirectAttributes attr) throws URISyntaxException {
+
+ Map model = new HashMap<>();
+ PageMessages messages = new PageMessages();
+
+ for (MultipartFile file :files) {
+ //Parse certificate
+ Certificate certificate = parseCertificate(certificateType, file, messages);
+
+ //Store only if it was parsed
+ if (certificate != null) {
+ storeCertificate(
+ certificateType,
+ file.getOriginalFilename(),
+ messages, certificate,
+ certificateManager);
+ }
+ }
+
+ //Add messages to the model
+ model.put(MESSAGES_ATTRIBUTE, messages);
+
+ return redirectTo(getCertificatePage(certificateType), new NoPageParams(), model, attr);
+ }
+
+ /**
+ * Get the page based on the certificate type.
+ *
+ * @param certificateType String containing the certificate type
+ * @return the page for the certificate type.
+ */
+ private static Page getCertificatePage(final String certificateType) {
+ // get page information (default to TRUST_CHAIN)
+ switch (certificateType) {
+ case PLATFORMCREDENTIAL:
+ return Page.PLATFORM_CREDENTIALS;
+ case ENDORSEMENTCREDENTIAL:
+ return Page.ENDORSEMENT_KEY_CREDENTIALS;
+ case ISSUEDCERTIFICATES:
+ return Page.ISSUED_CERTIFICATES;
+ case TRUSTCHAIN:
+ default:
+ return Page.TRUST_CHAIN;
+ }
+ }
+
+ /**
+ * Gets the concrete certificate class type to query for.
+ *
+ * @param certificateType String containing the certificate type
+ * @return the certificate class type
+ */
+ private static Class extends Certificate> getCertificateClass(final String certificateType) {
+ switch (certificateType) {
+ case PLATFORMCREDENTIAL:
+ return PlatformCredential.class;
+ case ENDORSEMENTCREDENTIAL:
+ return EndorsementCredential.class;
+ case ISSUEDCERTIFICATES:
+ return IssuedAttestationCertificate.class;
+ case TRUSTCHAIN:
+ return CertificateAuthorityCredential.class;
+ default:
+ throw new IllegalArgumentException(
+ String.format("Unknown certificate type: %s", certificateType));
+ }
+ }
+
+ /**
+ * Get flag indicating if a device-name join/alias is required for
+ * displaying the table data. This will be true if displaying a cert that is
+ * associated with a device.
+ *
+ * @param certificateType String containing the certificate type
+ * @return true if the list criteria modifier requires aliasing the device
+ * table, false otherwise.
+ */
+ private boolean hasDeviceTableToJoin(final String certificateType) {
+
+ boolean hasDevice = true;
+ // Trust_Chain Credential do not contain the device table to join.
+ if (certificateType.equals(TRUSTCHAIN)) {
+ hasDevice = false;
+ }
+ return hasDevice;
+ }
+
+ /**
+ * Gets the certificate by ID.
+ *
+ * @param certificateType String containing the certificate type
+ * @param uuid the ID of the cert
+ * @param certificateManager the certificate manager to query
+ * @return the certificate or null if none is found
+ */
+ private Certificate getCertificateById(
+ final String certificateType,
+ final UUID uuid,
+ final CertificateManager certificateManager) {
+
+ switch (certificateType) {
+ case PLATFORMCREDENTIAL:
+ return PlatformCredential
+ .select(certificateManager)
+ .byEntityId(uuid)
+ .getCertificate();
+ case ENDORSEMENTCREDENTIAL:
+ return EndorsementCredential
+ .select(certificateManager)
+ .byEntityId(uuid)
+ .getCertificate();
+ case ISSUEDCERTIFICATES:
+ return IssuedAttestationCertificate
+ .select(certificateManager)
+ .byEntityId(uuid)
+ .getCertificate();
+ case TRUSTCHAIN:
+ return CertificateAuthorityCredential
+ .select(certificateManager)
+ .byEntityId(uuid)
+ .getCertificate();
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * Gets the certificate by the hash code of its bytes. Looks for both
+ * archived and unarchived certificates.
+ *
+ * @param certificateType String containing the certificate type
+ * @param certificateHash the hash of the certificate's bytes
+ * @param certificateManager the certificate manager to query
+ * @return the certificate or null if none is found
+ */
+ private Certificate getCertificateByHash(
+ final String certificateType,
+ final int certificateHash,
+ final CertificateManager certificateManager) {
+
+ switch (certificateType) {
+ case PLATFORMCREDENTIAL:
+ return PlatformCredential
+ .select(certificateManager)
+ .includeArchived()
+ .byHashCode(certificateHash)
+ .getCertificate();
+ case ENDORSEMENTCREDENTIAL:
+ return EndorsementCredential
+ .select(certificateManager)
+ .includeArchived()
+ .byHashCode(certificateHash)
+ .getCertificate();
+ case TRUSTCHAIN:
+ return CertificateAuthorityCredential
+ .select(certificateManager)
+ .includeArchived()
+ .byHashCode(certificateHash)
+ .getCertificate();
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * Parses an uploaded file into a certificate and populates the given model
+ * with error messages if parsing fails.
+ *
+ * @param certificateType String containing the certificate type
+ * @param file the file being uploaded from the portal
+ * @param model the map of page elements to populate with error messages
+ * upon failure
+ * @param messages contains any messages that will be display on the page
+ * @return the parsed certificate or null if parsing failed.
+ */
+ private Certificate parseCertificate(
+ final String certificateType,
+ final MultipartFile file,
+ final PageMessages messages) {
+
+ LOGGER.info("Received File of Size: " + file.getSize());
+
+ byte[] fileBytes;
+ String fileName = file.getOriginalFilename();
+
+ // build the certificate from the uploaded bytes
+ try {
+ fileBytes = file.getBytes();
+ } catch (IOException e) {
+ final String failMessage = "Failed to read uploaded file ("
+ + fileName + "): ";
+ LOGGER.error(failMessage, e);
+ messages.addError(failMessage + e.getMessage());
+ return null;
+ }
+ try {
+ switch (certificateType) {
+ case PLATFORMCREDENTIAL:
+ return new PlatformCredential(fileBytes);
+ case ENDORSEMENTCREDENTIAL:
+ return new EndorsementCredential(fileBytes);
+ case TRUSTCHAIN:
+ return new CertificateAuthorityCredential(fileBytes);
+ default:
+ final String failMessage = "Failed to parse uploaded file ("
+ + fileName + "). Invalid certificate type: "
+ + certificateType;
+ LOGGER.error(failMessage);
+ messages.addError(failMessage);
+ return null;
+ }
+ } catch (IOException e) {
+ final String failMessage = "Failed to parse uploaded file ("
+ + fileName + "): ";
+ LOGGER.error(failMessage, e);
+ messages.addError(failMessage + e.getMessage());
+ return null;
+ } catch (IllegalArgumentException e) {
+ final String failMessage = "Certificate format not recognized("
+ + fileName + "): ";
+ LOGGER.error(failMessage, e);
+ messages.addError(failMessage + e.getMessage());
+ return null;
+ }
+ }
+
+ /**
+ * Store the given certificate in the database.
+ *
+ * @param certificateType String containing the certificate type
+ * @param fileName contain the name of the file of the certificate to
+ * be stored
+ * @param messages contains any messages that will be display on the page
+ * @param certificate the certificate to store
+ * @param certificateManager the DB manager to use
+ * @return the messages for the page
+ */
+ private void storeCertificate(
+ final String certificateType,
+ final String fileName,
+ final PageMessages messages,
+ final Certificate certificate,
+ final CertificateManager certificateManager) {
+
+ Certificate existingCertificate;
+
+ // look for an identical certificate in the database
+ try {
+ existingCertificate = getCertificateByHash(
+ certificateType,
+ certificate.getCertificateHash(),
+ certificateManager);
+ } catch (DBManagerException e) {
+ final String failMessage = "Querying for existing certificate failed ("
+ + fileName + "): ";
+ messages.addError(failMessage + e.getMessage());
+ LOGGER.error(failMessage, e);
+ return;
+ }
+
+ try {
+ // save the new certificate if no match is found
+ if (existingCertificate == null) {
+ certificateManager.save(certificate);
+
+ final String successMsg = "New certificate successfully uploaded ("
+ + fileName + ")";
+ messages.addSuccess(successMsg);
+ LOGGER.info(successMsg);
+ return;
+ }
+ } catch (DBManagerException e) {
+ final String failMessage = "Storing new certificate failed ("
+ + fileName + "): ";
+ messages.addError(failMessage + e.getMessage());
+ LOGGER.error(failMessage, e);
+ return;
+ }
+
+ try {
+ // if an identical certificate is archived, update the existing certificate to
+ // unarchive it and change the creation date
+ if (existingCertificate.isArchived()) {
+
+ existingCertificate.restore();
+ existingCertificate.resetCreateTime();
+ certificateManager.update(existingCertificate);
+
+ final String successMsg = "Pre-existing certificate found and unarchived ("
+ + fileName + ")";
+ messages.addSuccess(successMsg);
+ LOGGER.info(successMsg);
+ return;
+ }
+ } catch (DBManagerException e) {
+ final String failMessage = "Found an identical pre-existing certificate in the "
+ + "archive, but failed to unarchive it (" + fileName + "): ";
+ messages.addError(failMessage + e.getMessage());
+ LOGGER.error(failMessage, e);
+ return;
+ }
+
+ // if an identical certificate is already unarchived, do nothing and show a fail message
+ final String failMessage
+ = "Storing certificate failed: an identical certificate already exists ("
+ + fileName + ")";
+ messages.addError(failMessage);
+ LOGGER.error(failMessage);
+ }
+}
diff --git a/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/page/controllers/DevicesPageController.java b/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/page/controllers/DevicesPageController.java
new file mode 100644
index 000000000..068072313
--- /dev/null
+++ b/HIRS_AttestationCAPortal/src/main/java/hirs/attestationca/portal/page/controllers/DevicesPageController.java
@@ -0,0 +1,173 @@
+package hirs.attestationca.portal.page.controllers;
+
+import hirs.attestationca.portal.datatables.DataTableInput;
+import hirs.attestationca.portal.datatables.DataTableResponse;
+import hirs.attestationca.portal.datatables.OrderedListQueryDataTableAdapter;
+import static hirs.attestationca.portal.page.Page.DEVICES;
+import hirs.attestationca.portal.page.PageController;
+import hirs.attestationca.portal.page.params.NoPageParams;
+import org.apache.logging.log4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.servlet.ModelAndView;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.UUID;
+
+import static org.apache.logging.log4j.LogManager.getLogger;
+import org.hibernate.criterion.Restrictions;
+import hirs.FilteredRecordsList;
+import hirs.data.persist.Device;
+import hirs.data.persist.certificate.Certificate;
+import hirs.data.persist.certificate.DeviceAssociatedCertificate;
+import hirs.persist.DBManager;
+import hirs.persist.DeviceManager;
+
+/**
+ * Controller for the Device page.
+ */
+@Controller
+@RequestMapping("/devices")
+public class DevicesPageController extends PageController {
+
+ private final DeviceManager deviceManager;
+ private final DBManager certificateDBManager;
+ private static final Logger LOGGER = getLogger(DevicesPageController.class);
+
+ /**
+ * Constructor providing the Page's display and routing specification.
+ * @param deviceManager the device manager
+ * @param certificateDBManager the certificate DB manager
+ */
+ @Autowired
+ public DevicesPageController(
+ final DeviceManager deviceManager,
+ final DBManager certificateDBManager) {
+ super(DEVICES);
+ this.deviceManager = deviceManager;
+ this.certificateDBManager = certificateDBManager;
+ }
+
+ /**
+ * Returns the path for the view and the data model for the page.
+ *
+ * @param params The object to map url parameters into.
+ * @param model The data model for the request. Can contain data from redirect.
+ * @return the path for the view and data model for the page.
+ */
+ @Override
+ @RequestMapping
+ public ModelAndView initPage(final NoPageParams params, final Model model) {
+ return getBaseModelAndView();
+ }
+
+ /**
+ * Returns the list of devices using the datatable input for paging, ordering, and
+ * filtering.
+ * @param input the data tables input
+ * @return the data tables response, including the result set and paging information
+ */
+ @ResponseBody
+ @RequestMapping(value = "list", produces = MediaType.APPLICATION_JSON_VALUE,
+ method = RequestMethod.GET)
+ public DataTableResponse> getTableData(
+ final DataTableInput input) {
+ LOGGER.debug("Handling request for device list");
+ String orderColumnName = input.getOrderColumnName();
+
+ // get all the devices
+ FilteredRecordsList deviceList =
+ OrderedListQueryDataTableAdapter.getOrderedList(Device.class,
+ deviceManager, input, orderColumnName);
+
+ FilteredRecordsList> record
+ = retrieveDevicesAndAssociatedCertificates(deviceList);
+
+ return new DataTableResponse<>(record, input);
+ }
+
+ /**
+ * Returns the list of devices combined with the certificates.
+ * @param deviceList list containing the devices
+ * @return a record list after the device and certificate was mapped together.
+ */
+ private FilteredRecordsList> retrieveDevicesAndAssociatedCertificates(
+ final FilteredRecordsList deviceList) {
+ FilteredRecordsList> records = new FilteredRecordsList<>();
+ // hashmap containing the device-certificate relationship
+ HashMap deviceCertMap = new HashMap<>();
+ Device device;
+ Certificate certificate;
+
+ // parse if there is a Device
+ if (!deviceList.isEmpty()) {
+ // get a list of Certificates that contains the device IDs from the list
+ List certificateList = certificateDBManager.getList(
+ Certificate.class,
+ Restrictions.in("device.id", getDevicesIds(deviceList).toArray()));
+
+ // loop all the devices
+ for (int i = 0; i < deviceList.size(); i++) {
+ // hashmap containing the list of certificates based on the certificate type
+ HashMap> certificatePropertyMap = new HashMap<>();
+
+ device = deviceList.get(i);
+ deviceCertMap.put("device", device);
+
+ // loop all the certificates and combined the ones that match the ID
+ for (int j = 0; j < certificateList.size(); j++) {
+ certificate = certificateList.get(j);
+
+ // set the certificate if it's the same ID
+ if (device.getId().equals(
+ ((DeviceAssociatedCertificate) certificate).getDevice().getId())) {
+ String certificateId = certificate.getClass().getSimpleName();
+ // create a new list for the certificate type if does not exist
+ // else add it to the current certificate type list
+ List