This PR is to address issue #308. The ACA was pulling Issuer Certificates using the organization RDN of the subject string and getting this from the issuer string of the EC or PC. This presents a problem because it isn't a required field. The organization field cannot be null or empty. Pulling objects from a DB using null or empty would produce bad results. The main change of this issue (which has not been full tested) is pulling using the AKI for the db lookup. If this fails, instead of falling back on potentially left out fields like the O= RDN, the ACA takes the issuer/subject fields, breaks them apart and sorts them based on the key. It also changes the case. This way the lookup can be assured to match in case of some random situation in which the issuer or subject field don't match because RDN keys are just in different positions of the string.

This commit is contained in:
Cyrus 2020-12-11 14:47:46 -05:00
parent 408060b1e0
commit 62c7ca2d90
22 changed files with 315 additions and 239 deletions

View File

@ -67,11 +67,11 @@ import javax.crypto.spec.OAEPParameterSpec;
import javax.crypto.spec.PSource;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
@ -411,11 +411,8 @@ public abstract class AbstractAttestationCertificateAuthority
// and later tpm20MakeCredential function
RSAPublicKey ekPub = parsePublicKey(claim.getEkPublicArea().toByteArray());
AppraisalStatus.Status validationResult = AppraisalStatus.Status.FAIL;
try {
validationResult = doSupplyChainValidation(claim, ekPub);
} catch (Exception ex) {
LOG.error(ex);
}
validationResult = doSupplyChainValidation(claim, ekPub);
if (validationResult == AppraisalStatus.Status.PASS) {
RSAPublicKey akPub = parsePublicKey(claim.getAkPublicArea().toByteArray());
@ -555,19 +552,15 @@ public abstract class AbstractAttestationCertificateAuthority
if (request.getQuote() != null && !request.getQuote().isEmpty()) {
parseTPMQuote(request.getQuote().toStringUtf8());
TPMInfo savedInfo = device.getDeviceInfo().getTPMInfo();
TPMInfo tpmInfo = null;
try {
tpmInfo = new TPMInfo(savedInfo.getTPMMake(),
TPMInfo tpmInfo = new TPMInfo(savedInfo.getTPMMake(),
savedInfo.getTPMVersionMajor(),
savedInfo.getTPMVersionMinor(),
savedInfo.getTPMVersionRevMajor(),
savedInfo.getTPMVersionRevMinor(),
savedInfo.getPcrValues(),
this.tpmQuoteHash.getBytes("UTF-8"),
this.tpmQuoteSignature.getBytes("UTF-8"));
} catch (UnsupportedEncodingException e) {
LOG.error(e);
}
this.tpmQuoteHash.getBytes(StandardCharsets.UTF_8),
this.tpmQuoteSignature.getBytes(StandardCharsets.UTF_8));
DeviceInfoReport dvReport = new DeviceInfoReport(
device.getDeviceInfo().getNetworkInfo(),
device.getDeviceInfo().getOSInfo(),
@ -859,18 +852,14 @@ public abstract class AbstractAttestationCertificateAuthority
// Get TPM info, currently unimplemented
TPMInfo tpm;
try {
tpm = new TPMInfo(DeviceInfoReport.NOT_SPECIFIED,
(short) 0,
(short) 0,
(short) 0,
(short) 0,
this.pcrValues.getBytes("UTF-8"),
this.tpmQuoteHash.getBytes("UTF-8"),
this.tpmQuoteSignature.getBytes("UTF-8"));
} catch (UnsupportedEncodingException e) {
tpm = new TPMInfo();
}
tpm = new TPMInfo(DeviceInfoReport.NOT_SPECIFIED,
(short) 0,
(short) 0,
(short) 0,
(short) 0,
this.pcrValues.getBytes(StandardCharsets.UTF_8),
this.tpmQuoteHash.getBytes(StandardCharsets.UTF_8),
this.tpmQuoteSignature.getBytes(StandardCharsets.UTF_8));
// Create final report
DeviceInfoReport dvReport = new DeviceInfoReport(nw, os, fw, hw, tpm,
@ -1272,7 +1261,13 @@ public abstract class AbstractAttestationCertificateAuthority
IssuedCertificateAttributeHelper.buildSubjectAlternativeNameFromCerts(
endorsementCredential, platformCredentials, deviceName);
Extension authKeyIdentifier = IssuedCertificateAttributeHelper
.buildAuthorityKeyIdentifier(endorsementCredential);
builder.addExtension(subjectAlternativeName);
if (authKeyIdentifier != null) {
builder.addExtension(authKeyIdentifier);
}
// identify cert as an AIK with this extension
if (IssuedCertificateAttributeHelper.EXTENDED_KEY_USAGE_EXTENSION != null) {
builder.addExtension(IssuedCertificateAttributeHelper.EXTENDED_KEY_USAGE_EXTENSION);

View File

@ -4,12 +4,14 @@ 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.ASN1OctetString;
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.AuthorityKeyIdentifier;
import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.Extensions;
@ -19,7 +21,6 @@ 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;
@ -66,6 +67,28 @@ public final class IssuedCertificateAttributeHelper {
// do not construct publicly
}
/**
* This method builds the AKI extension that will be stored in the generated
* Attestation Issued Certificate.
* @param endorsementCredential EK object to pull AKI from.
* @return the AKI extension.
* @throws IOException on bad get instance for AKI.
*/
public static Extension buildAuthorityKeyIdentifier(
final EndorsementCredential endorsementCredential) throws IOException {
byte[] extValue = endorsementCredential.getX509Certificate()
.getExtensionValue(Extension.authorityKeyIdentifier.getId());
if (extValue == null) {
return null;
}
byte[] authExtension = ASN1OctetString.getInstance(extValue).getOctets();
AuthorityKeyIdentifier aki = AuthorityKeyIdentifier.getInstance(authExtension);
return new Extension(Extension.authorityKeyIdentifier, true, aki.getEncoded());
}
/**
* Builds the subject alternative name based on the supplied certificates.
* @param endorsementCredential the endorsement credential
@ -88,10 +111,8 @@ public final class IssuedCertificateAttributeHelper {
// 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);
}
for (PlatformCredential platformCredential : platformCredentials) {
populatePlatformCredentialAttributes(platformCredential, nameBuilder);
}
// add the OID for the TCG-required TPM ID label
@ -112,7 +133,7 @@ public final class IssuedCertificateAttributeHelper {
private static void populatePlatformCredentialAttributes(
final PlatformCredential platformCredential,
final X500NameBuilder nameBuilder) throws IOException {
if (null == platformCredential) {
if (platformCredential == null) {
return;
}
@ -134,7 +155,7 @@ public final class IssuedCertificateAttributeHelper {
private static void populateEndorsementCredentialAttributes(
final EndorsementCredential endorsementCredential, final X500NameBuilder nameBuilder) {
if (null == endorsementCredential) {
if (endorsementCredential == null) {
return;
}

View File

@ -16,10 +16,12 @@ import hirs.data.persist.PCRPolicy;
import hirs.data.persist.ArchivableEntity;
import hirs.tpm.eventlog.TCGEventLog;
import hirs.tpm.eventlog.TpmPcrEvent;
import hirs.utils.BouncyCastleUtils;
import hirs.utils.ReferenceManifestValidator;
import hirs.validation.SupplyChainCredentialValidator;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bouncycastle.util.encoders.Hex;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Import;
import org.springframework.stereotype.Service;
@ -51,7 +53,6 @@ import hirs.data.persist.ReferenceManifest;
import hirs.persist.AppraiserManager;
import hirs.persist.CertificateManager;
import hirs.persist.ReferenceManifestManager;
import hirs.persist.CertificateSelector;
import hirs.persist.CrudManager;
import hirs.persist.DBManagerException;
import hirs.persist.PersistenceConfiguration;
@ -803,27 +804,49 @@ public class SupplyChainValidationServiceImpl implements SupplyChainValidationSe
* 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
* @param previouslyQueriedSubjects a list of organizations to refrain
* from querying
* @return a Set containing all relevant CA credentials to the given
* certificate's organization
*/
private Set<CertificateAuthorityCredential> getCaChainRec(
final Certificate credential,
final Set<String> previouslyQueriedOrganizations
) {
CertificateSelector<CertificateAuthorityCredential> caSelector
= CertificateAuthorityCredential.select(certificateManager)
.bySubjectOrganization(credential.getIssuerOrganization());
Set<CertificateAuthorityCredential> certAuthsWithMatchingOrg = caSelector.getCertificates();
final Set<String> previouslyQueriedSubjects) {
CertificateAuthorityCredential skiCA = null;
Set<CertificateAuthorityCredential> certAuthsWithMatchingIssuer = new HashSet<>();
if (credential.getAuthKeyId() != null
&& !credential.getAuthKeyId().isEmpty()) {
byte[] bytes = Hex.decode(credential.getAuthKeyId());
skiCA = CertificateAuthorityCredential
.select(certificateManager)
.bySubjectKeyIdentifier(bytes).getCertificate();
}
Set<String> queriedOrganizations = new HashSet<>(previouslyQueriedOrganizations);
queriedOrganizations.add(credential.getIssuerOrganization());
if (skiCA == null) {
if (credential.getIssuerSorted() == null
|| credential.getIssuerSorted().isEmpty()) {
certAuthsWithMatchingIssuer = CertificateAuthorityCredential
.select(certificateManager)
.bySubject(credential.getIssuer())
.getCertificates();
} else {
//Get certificates by subject organization
certAuthsWithMatchingIssuer = CertificateAuthorityCredential
.select(certificateManager)
.bySubjectSorted(credential.getIssuerSorted())
.getCertificates();
}
} else {
certAuthsWithMatchingIssuer.add(skiCA);
}
Set<String> queriedOrganizations = new HashSet<>(previouslyQueriedSubjects);
queriedOrganizations.add(credential.getIssuer());
HashSet<CertificateAuthorityCredential> caCreds = new HashSet<>();
for (CertificateAuthorityCredential cred : certAuthsWithMatchingOrg) {
for (CertificateAuthorityCredential cred : certAuthsWithMatchingIssuer) {
caCreds.add(cred);
if (!queriedOrganizations.contains(cred.getIssuerOrganization())) {
if (!BouncyCastleUtils.x500NameCompare(cred.getIssuer(),
cred.getSubject())) {
caCreds.addAll(getCaChainRec(cred, queriedOrganizations));
}
}

View File

@ -119,7 +119,7 @@ public class SupplyChainValidationServiceImplTest extends SpringPersistenceTest
// mock endorsement credential
ec = mock(EndorsementCredential.class);
when(ec.getEncodedPublicKey()).thenReturn(new byte[] {0x0});
when(ec.getIssuerOrganization()).thenReturn("STMicroelectronics NV");
when(ec.getIssuerSorted()).thenReturn("STMicroelectronics NV");
Set<Certificate> resultEcs = new HashSet<>();
resultEcs.add(ec);
@ -131,8 +131,8 @@ public class SupplyChainValidationServiceImplTest extends SpringPersistenceTest
when(pc.getX509Certificate()).thenReturn(cert);
when(pc.getSerialNumber()).thenReturn(BigInteger.ONE);
when(pc.getPlatformSerial()).thenReturn(String.valueOf(Integer.MIN_VALUE));
when(pc.getIssuerOrganization()).thenReturn("STMicroelectronics NV");
when(ec.getSubjectOrganization()).thenReturn("STMicroelectronics NV");
when(pc.getIssuerSorted()).thenReturn("STMicroelectronics NV");
when(ec.getSubjectSorted()).thenReturn("STMicroelectronics NV");
pcs = new HashSet<PlatformCredential>();
pcs.add(pc);
@ -142,8 +142,8 @@ public class SupplyChainValidationServiceImplTest extends SpringPersistenceTest
when(delta.getId()).thenReturn(UUID.randomUUID());
when(delta.getX509Certificate()).thenReturn(deltaCert);
//when(delta.getSerialNumber()).thenReturn(BigInteger.ONE);
when(delta.getIssuerOrganization()).thenReturn("STMicroelectronics NV");
when(delta.getSubjectOrganization()).thenReturn("STMicroelectronics NV");
when(delta.getIssuerSorted()).thenReturn("STMicroelectronics NV");
when(delta.getSubjectSorted()).thenReturn("STMicroelectronics NV");
Set<Certificate> resultPcs = new HashSet<>();
resultPcs.add(pc);

View File

@ -7,6 +7,7 @@ import java.io.IOException;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import java.util.List;
import java.util.Comparator;
@ -21,6 +22,8 @@ import hirs.data.persist.certificate.attributes.ComponentIdentifier;
import hirs.data.persist.certificate.attributes.PlatformConfiguration;
import hirs.persist.CertificateManager;
import hirs.utils.BouncyCastleUtils;
import org.bouncycastle.util.encoders.Hex;
import java.util.Collections;
/**
@ -134,20 +137,35 @@ public final class CertificateStringMapBuilder {
public static Certificate containsAllChain(
final Certificate certificate,
final CertificateManager certificateManager) {
Set<CertificateAuthorityCredential> issuerCertificates;
Set<CertificateAuthorityCredential> issuerCertificates = new HashSet<>();
CertificateAuthorityCredential skiCA = null;
//Check if there is a subject organization
if (certificate.getIssuerOrganization() == null
|| certificate.getIssuerOrganization().isEmpty()) {
//Get certificates by subject
issuerCertificates = CertificateAuthorityCredential.select(certificateManager)
.bySubject(certificate.getIssuer())
.getCertificates();
if (certificate.getAuthKeyId() != null
&& !certificate.getAuthKeyId().isEmpty()) {
byte[] bytes = Hex.decode(certificate.getAuthKeyId());
skiCA = CertificateAuthorityCredential
.select(certificateManager)
.bySubjectKeyIdentifier(bytes).getCertificate();
} else {
//Get certificates by subject organization
issuerCertificates = CertificateAuthorityCredential.select(certificateManager)
.bySubjectOrganization(certificate.getIssuerOrganization())
.getCertificates();
LOGGER.info(String.format("Certificate (%s) for %s has no authority key identifier.",
certificate.getClass().toString(), certificate.getSubject()));
}
if (skiCA == null) {
if (certificate.getIssuerSorted() == null
|| certificate.getIssuerSorted().isEmpty()) {
//Get certificates by subject
issuerCertificates = CertificateAuthorityCredential.select(certificateManager)
.bySubject(certificate.getIssuer())
.getCertificates();
} else {
//Get certificates by subject organization
issuerCertificates = CertificateAuthorityCredential.select(certificateManager)
.bySubjectSorted(certificate.getIssuerSorted())
.getCertificates();
}
} else {
issuerCertificates.add(skiCA);
}
for (Certificate issuerCert : issuerCertificates) {

View File

@ -130,7 +130,7 @@ int provision() {
RestfulClientProvisioner provisioner;
string nonceBlob = provisioner.sendIdentityClaim(identityClaim);
if (nonceBlob == "") {
cout << "----> Provisioning failed.";
cout << "----> Provisioning failed." << endl;
cout << "Please refer to the Attestation CA for details." << endl;
return 0;
}

View File

@ -32,6 +32,7 @@ import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.cert.CertificateException;
@ -362,7 +363,7 @@ public class DeviceInfoCollector extends AbstractCollector {
if (debianRelease.exists()) {
try {
reader = new BufferedReader(new InputStreamReader(
new FileInputStream(debianRelease), "UTF-8"));
new FileInputStream(debianRelease), StandardCharsets.UTF_8));
while ((line = reader.readLine()) != null) {
String[] ubuntuTokens = line.split("=");
if (ubuntuTokens.length == 2) {
@ -394,7 +395,7 @@ public class DeviceInfoCollector extends AbstractCollector {
} else if (redhatRelease.exists()) {
try {
reader = new BufferedReader(new InputStreamReader(
new FileInputStream(redhatRelease), "UTF-8"));
new FileInputStream(redhatRelease), StandardCharsets.UTF_8));
while ((line = reader.readLine()) != null) {
String[] redhatTokens = line.split("release");
if (redhatTokens.length == 2) {
@ -543,7 +544,7 @@ public class DeviceInfoCollector extends AbstractCollector {
Process quoteProcess = processBuilder.start();
quoteReader
= new BufferedReader(new InputStreamReader(
quoteProcess.getInputStream(), "utf-8"));
quoteProcess.getInputStream(), StandardCharsets.UTF_8));
} catch (IOException e) {
LOGGER.info("IOException occurred while attempting to read "
+ "tpm_version command, assume the TPM is not present and "

View File

@ -12,7 +12,7 @@ import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
import org.apache.logging.log4j.Logger;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
@ -100,8 +100,7 @@ public final class PCRPolicy extends Policy {
LOGGER.info("Validating quote from associated device.");
boolean validated = false;
short localityAtRelease = 0;
Charset charset = Charset.forName("UTF-8");
String quoteString = new String(tpmQuote, charset);
String quoteString = new String(tpmQuote, StandardCharsets.UTF_8);
TPMMeasurementRecord[] measurements = new TPMMeasurementRecord[baselinePcrs.length];
try {

View File

@ -62,8 +62,10 @@ import java.security.cert.X509Certificate;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.ListIterator;
import java.util.Objects;
import org.bouncycastle.asn1.x509.CRLDistPoint;
import org.bouncycastle.asn1.x509.DistributionPoint;
@ -163,6 +165,12 @@ public abstract class Certificate extends ArchivableEntity {
public static final String ISSUER_FIELD = "issuer";
@Column(nullable = false)
private final String issuer;
/**
* Holds the name of the 'issuerSorted' field.
*/
public static final String ISSUER_SORTED_FIELD = "issuerSorted";
@Column
private final String issuerSorted;
/**
* Holds the name of the 'subject' field.
@ -170,20 +178,12 @@ public abstract class Certificate extends ArchivableEntity {
public static final String SUBJECT_FIELD = "subject";
@Column(nullable = true)
private final String subject;
/**
* Holds the name of the 'issuerOrganization' field.
* Holds the name of the 'subjectSorted' field.
*/
public static final String ISSUER_ORGANIZATION_FIELD = "issuerOrganization";
public static final String SUBJECT_SORTED_FIELD = "subjectSorted";
@Column
private String issuerOrganization = null;
/**
* Holds the name of the 'subjectOrganization' field.
*/
public static final String SUBJECT_ORGANIZATION_FIELD = "subjectOrganization";
@Column
private String subjectOrganization = null;
private final String subjectSorted;
/**
* Holds the name of the 'encodedPublicKey' field.
@ -255,12 +255,15 @@ public abstract class Certificate extends ArchivableEntity {
private String keyUsage;
private String extendedKeyUsage;
private byte[] policyConstraints;
/**
* Holds the name of the 'authorityKeyIdentifier' field.
*/
public static final String AUTHORITY_KEY_ID_FIELD = "authorityKeyIdentifier";
private String authorityKeyIdentifier;
private String authorityInfoAccess;
private String crlPoints;
private int publicKeySize;
/**
* Default constructor necessary for Hibernate.
*/
@ -269,6 +272,8 @@ public abstract class Certificate extends ArchivableEntity {
this.serialNumber = BigInteger.ZERO;
this.issuer = null;
this.subject = null;
this.issuerSorted = null;
this.subjectSorted = null;
this.encodedPublicKey = null;
this.publicKeyModulusHexValue = null;
@ -359,8 +364,8 @@ public abstract class Certificate extends ArchivableEntity {
this.beginValidity = x509Certificate.getNotBefore();
this.endValidity = x509Certificate.getNotAfter();
this.holderSerialNumber = BigInteger.ZERO;
this.issuerOrganization = getOrganization(this.issuer);
this.subjectOrganization = getOrganization(this.subject);
this.issuerSorted = parseSortDNs(this.issuer);
this.subjectSorted = parseSortDNs(this.subject);
this.policyConstraints = x509Certificate
.getExtensionValue(POLICY_CONSTRAINTS);
authKeyIdentifier = AuthorityKeyIdentifier
@ -395,7 +400,7 @@ public abstract class Certificate extends ArchivableEntity {
// Set null values (Attribute certificates do not have this values)
this.subject = null;
this.subjectOrganization = null;
this.subjectSorted = null;
this.encodedPublicKey = null;
this.publicKeyModulusHexValue = null;
this.publicKeySize = 0;
@ -434,7 +439,7 @@ public abstract class Certificate extends ArchivableEntity {
this.signature = attCert.getSignatureValue().getBytes();
this.issuer = getAttributeCertificateIssuerNames(
attCertInfo.getIssuer())[0].toString();
this.issuerOrganization = getOrganization(this.issuer);
this.issuerSorted = parseSortDNs(this.issuer);
// Parse notBefore and notAfter dates
this.beginValidity = recoverDate(attCertInfo
@ -536,39 +541,6 @@ public abstract class Certificate extends ArchivableEntity {
return CertificateType.INVALID_CERTIFICATE;
}
/**
* Extracts the organization field out of a distinguished name. Returns null if
* no organization field exists.
* @param distinguishedName distinguished name to extract the organization from
* @return the value of the organization field
*/
protected static String getOrganization(final String distinguishedName) {
String organization = null;
// Return null for empty strings
if (distinguishedName.isEmpty()) {
return null;
}
// Parse string to X500Name
X500Name name = new X500Name(distinguishedName);
if (name.getRDNs(RFC4519Style.o).length > 0) {
RDN rdn = name.getRDNs(RFC4519Style.o)[0];
// For multivalue check the RDNs Attributes
if (rdn.isMultiValued()) {
for (AttributeTypeAndValue att: rdn.getTypesAndValues()) {
if (RFC4519Style.o.equals(att.getType())) {
organization = att.getValue().toString();
}
}
} else {
organization = rdn.getFirst().getValue().toString();
}
}
return organization;
}
private boolean isPEM(final String possiblePEM) {
return possiblePEM.contains(PEM_HEADER) || possiblePEM.contains(PEM_ATTRIBUTE_HEADER);
}
@ -785,41 +757,39 @@ public abstract class Certificate extends ArchivableEntity {
* @throws IOException if there is an issue deserializing either certificate
*/
public boolean isIssuer(final Certificate issuer) throws IOException {
CertificateType cType = issuer.getCertificateType();
if (cType != CertificateType.X509_CERTIFICATE) {
throw new IllegalArgumentException("issuer cert must be X509Certificate");
}
boolean isIssuer = false;
X509Certificate issuerX509 = issuer.getX509Certificate();
// Validate if it's the issuer
switch (getCertificateType()) {
case X509_CERTIFICATE:
X509Certificate certX509 = getX509Certificate();
try {
certX509.verify(issuerX509.getPublicKey());
isIssuer = true;
} catch (CertificateException | NoSuchAlgorithmException | InvalidKeyException
| NoSuchProviderException | SignatureException e) {
LOGGER.error(e);
}
break;
case ATTRIBUTE_CERTIFICATE:
AttributeCertificate attCert = getAttributeCertificate();
String algorith = "SHA256withRSA";
try {
Signature sig = Signature.getInstance(algorith);
sig.initVerify(issuerX509.getPublicKey());
sig.update(attCert.getAcinfo().getEncoded());
isIssuer = sig.verify(attCert.getSignatureValue().getBytes());
} catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) {
LOGGER.error(e);
}
break;
default:
break;
// only run if of the correct type, otherwise false
if (issuer.getCertificateType() == CertificateType.X509_CERTIFICATE) {
X509Certificate issuerX509 = issuer.getX509Certificate();
// Validate if it's the issuer
switch (getCertificateType()) {
case X509_CERTIFICATE:
X509Certificate certX509 = getX509Certificate();
try {
certX509.verify(issuerX509.getPublicKey());
isIssuer = true;
} catch (CertificateException | NoSuchAlgorithmException | InvalidKeyException
| NoSuchProviderException | SignatureException e) {
LOGGER.error(e);
}
break;
case ATTRIBUTE_CERTIFICATE:
AttributeCertificate attCert = getAttributeCertificate();
String algorithm = "SHA256withRSA";
try {
Signature sig = Signature.getInstance(algorithm);
sig.initVerify(issuerX509.getPublicKey());
sig.update(attCert.getAcinfo().getEncoded());
isIssuer = sig.verify(attCert.getSignatureValue().getBytes());
} catch (NoSuchAlgorithmException
| InvalidKeyException
| SignatureException e) {
LOGGER.error(e);
}
break;
default:
break;
}
}
return isIssuer;
@ -1055,17 +1025,17 @@ public abstract class Certificate extends ArchivableEntity {
}
/**
* @return this certificate's associated issuer organization
* @return this certificate's associated issuer sorted
*/
public String getIssuerOrganization() {
return issuerOrganization;
public String getIssuerSorted() {
return issuerSorted;
}
/**
* @return this certificate's associated subject organization
* @return this certificate's associated subject sorted
*/
public String getSubjectOrganization() {
return subjectOrganization;
public String getSubjectSorted() {
return subjectSorted;
}
/**
@ -1187,6 +1157,36 @@ public abstract class Certificate extends ArchivableEntity {
}
}
/**
* This method is to take the DNs from certificates and sort them in an order
* that will be used to lookup issuer certificates. This will not be stored in
* the certificate, just the DB for lookup.
* @param distinguishedName the original DN string.
* @return a modified string of sorted DNs
*/
public static String parseSortDNs(final String distinguishedName) {
StringBuilder sb = new StringBuilder();
String dnsString;
if (distinguishedName == null || distinguishedName.isEmpty()) {
sb.append("BLANK");
} else {
dnsString = distinguishedName.trim();
dnsString = dnsString.toLowerCase();
List<String> dnValArray = Arrays.asList(dnsString.split(","));
Collections.sort(dnValArray);
ListIterator<String> dnListIter = dnValArray.listIterator();
while (dnListIter.hasNext()) {
sb.append(dnListIter.next());
if (dnListIter.hasNext()) {
sb.append(",");
}
}
}
return sb.toString();
}
/**
* Retrieve the X509 Name array from the issuer in an Attribute Certificate.
*

View File

@ -2,6 +2,7 @@ package hirs.data.persist.certificate;
import hirs.persist.CertificateManager;
import hirs.persist.CertificateSelector;
import org.apache.commons.codec.binary.Hex;
import javax.persistence.Column;
import javax.persistence.Entity;
@ -15,7 +16,8 @@ import java.util.Arrays;
*/
@Entity
public class CertificateAuthorityCredential extends Certificate {
@SuppressWarnings("PMD.AvoidUsingHardCodedIP") // this is not an IP address; PMD thinks it is
@SuppressWarnings("PMD.AvoidUsingHardCodedIP")
private static final String SUBJECT_KEY_IDENTIFIER_EXTENSION = "2.5.29.14";
/**
@ -26,9 +28,12 @@ public class CertificateAuthorityCredential extends Certificate {
@Column
private final byte[] subjectKeyIdentifier;
/*
@Column
private String subjectKeyIdString;
/**
* this field is part of the TCG CA specification, but has not yet been found in
* manufacturer-provided CAs, and is therefore not currently parsed
* manufacturer-provided CAs, and is therefore not currently parsed.
*/
@Column
private String credentialType = "TCPA Trusted Platform Module Endorsement";
@ -82,6 +87,9 @@ public class CertificateAuthorityCredential extends Certificate {
super(certificateBytes);
this.subjectKeyIdentifier =
getX509Certificate().getExtensionValue(SUBJECT_KEY_IDENTIFIER_EXTENSION);
if (this.subjectKeyIdentifier != null) {
this.subjectKeyIdString = Hex.encodeHexString(this.subjectKeyIdentifier);
}
}
/**
@ -119,12 +127,20 @@ public class CertificateAuthorityCredential extends Certificate {
* @return this certificate's subject key identifier.
*/
public byte[] getSubjectKeyIdentifier() {
if (null != subjectKeyIdentifier) {
if (subjectKeyIdentifier != null) {
return subjectKeyIdentifier.clone();
}
return null;
}
/**
* Getter for the string rep of the ID.
* @return a string
*/
public String getSubjectKeyIdString() {
return this.subjectKeyIdString;
}
@Override
@SuppressWarnings("checkstyle:avoidinlineconditionals")
public boolean equals(final Object o) {

View File

@ -175,7 +175,8 @@ public abstract class CertificateSelector<T extends Certificate> {
public CertificateSelector<T> byIssuer(final String issuer) {
Preconditions.checkArgument(
StringUtils.isNotEmpty(issuer),
"issuer cannot be null or empty."
String.format("%s: issuer cannot be null or empty.",
this.certificateClass.toString())
);
setFieldValue(Certificate.ISSUER_FIELD, issuer);
@ -192,7 +193,8 @@ public abstract class CertificateSelector<T extends Certificate> {
public CertificateSelector<T> bySubject(final String subject) {
Preconditions.checkArgument(
StringUtils.isNotEmpty(subject),
"subject cannot be null or empty."
String.format("%s: subject cannot be null or empty.",
this.certificateClass.toString())
);
setFieldValue(Certificate.SUBJECT_FIELD, subject);
@ -200,36 +202,38 @@ public abstract class CertificateSelector<T extends Certificate> {
}
/**
* Specify an issuer organization string that certificates must have to be considered
* Specify the sorted issuer string that certificates must have to be considered
* as matching.
*
* @param organization certificate issuer organization string to query, not empty or null
* @param issuerSorted certificate issuer organization string to query, not empty or null
* @return this instance (for chaining further calls)
*/
public CertificateSelector<T> byIssuerOrganization(final String organization) {
public CertificateSelector<T> byIssuerSorted(final String issuerSorted) {
Preconditions.checkArgument(
StringUtils.isNotEmpty(organization),
"organization cannot be null or empty."
StringUtils.isNotEmpty(issuerSorted),
String.format("%s: issuerSorted cannot be null or empty.",
this.certificateClass.toString())
);
setFieldValue(Certificate.ISSUER_ORGANIZATION_FIELD, organization);
setFieldValue(Certificate.ISSUER_SORTED_FIELD, issuerSorted);
return this;
}
/**
* Specify a subject organization string that certificates must have to be considered
* Specify the sorted subject string that certificates must have to be considered
* as matching.
*
* @param organization certificate subject organization string to query, not empty or null
* @param subjectSorted certificate subject organization string to query, not empty or null
* @return this instance (for chaining further calls)
*/
public CertificateSelector<T> bySubjectOrganization(final String organization) {
public CertificateSelector<T> bySubjectSorted(final String subjectSorted) {
Preconditions.checkArgument(
StringUtils.isNotEmpty(organization),
"organization cannot be null or empty."
StringUtils.isNotEmpty(subjectSorted),
String.format("%s: subjectSorted cannot be null or empty.",
this.certificateClass.toString())
);
setFieldValue(Certificate.SUBJECT_ORGANIZATION_FIELD, organization);
setFieldValue(Certificate.SUBJECT_SORTED_FIELD, subjectSorted);
return this;
}
@ -243,7 +247,8 @@ public abstract class CertificateSelector<T extends Certificate> {
public CertificateSelector<T> byEncodedPublicKey(final byte[] encodedPublicKey) {
Preconditions.checkArgument(
ArrayUtils.isNotEmpty(encodedPublicKey),
"publicKey cannot be null or empty."
String.format("%s: publicKey cannot be null or empty.",
this.certificateClass.toString())
);
setFieldValue(
@ -254,6 +259,23 @@ public abstract class CertificateSelector<T extends Certificate> {
return this;
}
/**
* Specify the authority key identifier to find certificate(s).
* @param authorityKeyIdentifier the string of the AKI associated with the certificate.
* @return this instance
*/
public CertificateSelector<T> byAuthorityKeyIdentifier(final String authorityKeyIdentifier) {
Preconditions.checkArgument(
StringUtils.isNotEmpty(authorityKeyIdentifier),
String.format("%s: authorityKeyIdentifier cannot be null or empty.",
this.certificateClass.toString())
);
setFieldValue(Certificate.AUTHORITY_KEY_ID_FIELD, authorityKeyIdentifier);
return this;
}
/**
* Specify a public key modulus that certificates must have to be considered
* as matching.
@ -264,7 +286,8 @@ public abstract class CertificateSelector<T extends Certificate> {
public CertificateSelector<T> byPublicKeyModulus(final BigInteger publicKeyModulus) {
Preconditions.checkArgument(
publicKeyModulus != null,
"Public key modulus cannot be null"
String.format("%s: Public key modulus cannot be null",
this.certificateClass.toString())
);
setFieldValue(
@ -428,8 +451,7 @@ public abstract class CertificateSelector<T extends Certificate> {
// construct and execute query
private Set<T> execute() {
Set<T> results = certificateManager.get(this);
return results;
return certificateManager.get(this);
}
/**

View File

@ -2,8 +2,8 @@ package hirs.tpm.eventlog;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
@ -350,9 +350,8 @@ public final class TCGEventLog {
*
* @param log The Event Log
* @return true if EfiSpecIDEvent is found and indicates that the format is crypto agile
* @throws UnsupportedEncodingException if parsing error occurs.
*/
private boolean isLogCrytoAgile(final byte[] log) throws UnsupportedEncodingException {
private boolean isLogCrytoAgile(final byte[] log) {
byte[] eType = new byte[UefiConstants.SIZE_4];
System.arraycopy(log, UefiConstants.SIZE_4, eType, 0, UefiConstants.SIZE_4);
byte[] eventType = HexUtils.leReverseByte(eType);
@ -361,8 +360,10 @@ public final class TCGEventLog {
return false;
} // Event Type should be EV_NO_ACTION
byte[] signature = new byte[SIG_SIZE];
System.arraycopy(log, SIG_OFFSET, signature, 0, SIG_SIZE); // should be "Spec ID Event03"
String sig = new String(signature, "UTF-8").substring(0, SIG_SIZE - 1); // remove null char
// should be "Spec ID Event03"
System.arraycopy(log, SIG_OFFSET, signature, 0, SIG_SIZE);
// remove null char
String sig = new String(signature, StandardCharsets.UTF_8).substring(0, SIG_SIZE - 1);
return sig.equals("Spec ID Event03");
}

View File

@ -1,6 +1,7 @@
package hirs.tpm.eventlog.events;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import hirs.tpm.eventlog.uefi.UefiConstants;
import hirs.utils.HexUtils;
@ -38,7 +39,7 @@ public class EvCompactHash {
if (event.length == UefiConstants.SIZE_4) { // older PFP defines as 4 byte ESI pointer.
eventInfo = " ESI = " + HexUtils.byteArrayToHexString(event);
} else { // otherwise assume the event content is a string
eventInfo = " " + new String(event, "UTF-8");
eventInfo = " " + new String(event, StandardCharsets.UTF_8);
}
return eventInfo;
}

View File

@ -1,6 +1,6 @@
package hirs.tpm.eventlog.events;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import hirs.tpm.eventlog.TcgTpmtHa;
@ -67,13 +67,13 @@ public class EvEfiSpecIdEvent {
/**
* EvEfiSpecIdEvent Constructor.
* @param efiSpecId byte array holding the spec ID Event.
* @throws UnsupportedEncodingException if input fails to parse.
*/
public EvEfiSpecIdEvent(final byte[] efiSpecId) throws UnsupportedEncodingException {
public EvEfiSpecIdEvent(final byte[] efiSpecId) {
byte[] signatureBytes = new byte[UefiConstants.SIZE_16];
System.arraycopy(efiSpecId, 0, signatureBytes, 0, UefiConstants.SIZE_16);
signature = HexUtils.byteArrayToHexString(signatureBytes);
signature = new String(signatureBytes, "UTF-8").substring(0, UefiConstants.SIZE_15);
signature = new String(signatureBytes, StandardCharsets.UTF_8)
.substring(0, UefiConstants.SIZE_15);
byte[] platformClassBytes = new byte[UefiConstants.SIZE_4];
System.arraycopy(efiSpecId, UefiConstants.OFFSET_16, platformClassBytes, 0,
@ -167,12 +167,12 @@ public class EvEfiSpecIdEvent {
*/
public String toString() {
String specInfo = "";
if (signature == "Spec ID Event#") {
if (signature.equals("Spec ID Event#")) {
specInfo += "Platform Profile Specification version = " + vMaj + "." + vMin
+ " using errata version" + errata;
} else {
specInfo = "EV_NO_ACTION event named " + signature
+ " ecncountered but support for processing it has not been added to this application";
+ " encountered but support for processing it has not been added to this application";
}
return specInfo;
}

View File

@ -1,6 +1,7 @@
package hirs.tpm.eventlog.events;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import hirs.tpm.eventlog.uefi.UefiConstants;
import hirs.tpm.eventlog.uefi.UefiGuid;
@ -25,10 +26,9 @@ public class EvSCrtmVersion {
/**
* Checks if event data is null and if not it converts to a String.
* @param data byte array holding the vent content.
* @throws UnsupportedEncodingException if parsing issues exist.
* @return String representation of the version.
*/
public String sCrtmVersion(final byte[] data) throws UnsupportedEncodingException {
public String sCrtmVersion(final byte[] data) {
UefiGuid guid = null;
if (data == null) {
description = "invalid content event data";
@ -42,7 +42,7 @@ public class EvSCrtmVersion {
} else if (data.length < UefiConstants.SIZE_4) {
description = HexUtils.byteArrayToHexString(data);
} else if (EvPostCode.isAscii(data)) {
description = new String(data, "UTF-8");
description = new String(data, StandardCharsets.UTF_8);
} else {
description = "Unknown Version format";
}

View File

@ -1,6 +1,7 @@
package hirs.tpm.eventlog.uefi;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import hirs.utils.HexUtils;
@ -276,10 +277,8 @@ private String hardDriveSubType(final byte[] path, final int offset) {
* @param path
* @param offset
* @return file path info.
* @throws UnsupportedEncodingException
*/
private String filePathSubType(final byte[] path, final int offset)
throws UnsupportedEncodingException {
private String filePathSubType(final byte[] path, final int offset) {
subType = "File Path = ";
byte[] lengthBytes = new byte[UefiConstants.SIZE_2];
System.arraycopy(path, 2 + offset, lengthBytes, 0, UefiConstants.SIZE_2);
@ -287,7 +286,7 @@ private String filePathSubType(final byte[] path, final int offset)
byte[] filePath = new byte[subTypeLength];
System.arraycopy(path, UefiConstants.OFFSET_4 + offset, filePath, 0, subTypeLength);
byte[] fileName = convertChar16tobyteArray(filePath);
subType += new String(fileName, "UTF-8");
subType += new String(fileName, StandardCharsets.UTF_8);
return subType;
}

View File

@ -1,6 +1,6 @@
package hirs.tpm.eventlog.uefi;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import hirs.utils.HexUtils;
/**
@ -39,9 +39,8 @@ public class UefiPartition {
/**
* Processes a UEFI defined partition entry.
* @param table byte array holding the partition table.
* @throws UnsupportedEncodingException if parsing of the data fails.
*/
public UefiPartition(final byte[] table) throws UnsupportedEncodingException {
public UefiPartition(final byte[] table) {
byte[] partitionGuidBytes = new byte[UefiConstants.SIZE_16];
System.arraycopy(table, 0, partitionGuidBytes, 0, UefiConstants.SIZE_16);
partitionTypeGUID = new UefiGuid(partitionGuidBytes);
@ -56,7 +55,7 @@ public class UefiPartition {
System.arraycopy(table, UefiConstants.PART_NAME_LENGTH, partitionNameBytes,
0, UefiConstants.UEFI_PT_LENGTH);
byte[] pName = convertChar16tobyteArray(partitionNameBytes);
partitionName = new String(pName, "UTF-8").trim();
partitionName = new String(pName, StandardCharsets.UTF_8).trim();
}
/**

View File

@ -3,6 +3,7 @@ package hirs.tpm.eventlog.uefi;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.util.ArrayList;
@ -24,7 +25,7 @@ public class UefiVariable {
/** UEFI defined variable identifier GUID. */
private UefiGuid uefiGuid = null;
/** List of Signature lists. */
private ArrayList<UefiSignatureList> certSuperList = new ArrayList<UefiSignatureList>();
private ArrayList<UefiSignatureList> certSuperList = new ArrayList<>();
/** Name of the UEFI variable. */
private String varName = "";
/** UEFI defined Boot Variable. */
@ -70,15 +71,15 @@ public UefiVariable(final byte[] variableData)
uefiVaribelData = new byte[variableLength];
System.arraycopy(variableData, UefiConstants.OFFSET_32
+ nlength * UefiConstants.SIZE_2, uefiVaribelData, 0, variableLength);
varName = new String(name, "UTF-8");
varName = new String(name, StandardCharsets.UTF_8);
String tmpName = varName;
if (varName.contains("Boot00")) {
tmpName = "Boot00";
}
switch (tmpName) {
case "PK": processSigList(uefiVaribelData); break;
case "KEK": processSigList(uefiVaribelData); break;
case "db": processSigList(uefiVaribelData); break;
case "PK":
case "KEK":
case "db":
case "dbx": processSigList(uefiVaribelData); break;
case "Boot00": bootv = new UefiBootVariable(uefiVaribelData); break;
case "BootOrder": booto = new UefiBootOrder(uefiVaribelData); break;
@ -147,14 +148,15 @@ public String toString() {
tmpName = varName;
}
switch (tmpName) {
case "Shim": efiVariable.append(printCert(uefiVaribelData, 0)); break;
case "Shim":
case "MokList": efiVariable.append(printCert(uefiVaribelData, 0)); break;
case "Boot00": efiVariable.append(bootv.toString()); break;
case "BootOrder": efiVariable.append(booto.toString()); break;
case "SecureBoot": efiVariable.append(sb.toString()); break;
default:
if (!tmpName.isEmpty()) {
efiVariable.append("Data not provided for UEFI variable named " + tmpName + " ");
efiVariable.append(String.format("Data not provided for UEFI variable named %s ",
tmpName));
} else {
efiVariable.append("Data not provided ");
}

View File

@ -5,6 +5,7 @@ import org.apache.commons.exec.DefaultExecuteResultHandler;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
/**
* An implementation of ExecResult that facilitates working with
@ -84,7 +85,7 @@ public class AsynchronousExecResult implements ExecResult {
checkFinished();
if (stdOut != null) {
return ((ByteArrayOutputStream) stdOut).toString("UTF-8");
return ((ByteArrayOutputStream) stdOut).toString(StandardCharsets.UTF_8.toString());
} else {
return null;
}

View File

@ -481,28 +481,6 @@ public class CertificateTest {
);
}
/**
* Tests that Certificate's getOrganization method can properly parse an organization
* from a comma separated RDN.
* @throws IOException unable to parse organization
*/
@Test
public void testGetSingleOrganization() throws IOException {
String parsedOrg = Certificate.getOrganization(RDN_COMMA_SEPARATED);
Assert.assertEquals(RDN_COMMA_SEPARATED_ORGANIZATION, parsedOrg);
}
/**
* Tests that Certificate's getOrganization method can properly parse an organization
* from a multivalue RDN.
* @throws IOException unable to parse organization
*/
@Test
public void testGetMultiRdnOrganization() throws IOException {
String parsedOrg = Certificate.getOrganization(RDN_MULTIVALUE);
Assert.assertEquals(RDN_MULTIVALUE_ORGANIZATION, parsedOrg);
}
/**
* Construct a CertificateAuthorityCredential from the given parameters.
*

View File

@ -100,7 +100,7 @@ public class CertificateSelectorTest extends SpringPersistenceTest {
*/
@Test(expectedExceptions = IllegalArgumentException.class)
public void testNullByIssuerOrganization() {
CertificateAuthorityCredential.select(certMan).byIssuerOrganization(null);
CertificateAuthorityCredential.select(certMan).byIssuerSorted(null);
}
/**
@ -108,6 +108,6 @@ public class CertificateSelectorTest extends SpringPersistenceTest {
*/
@Test(expectedExceptions = IllegalArgumentException.class)
public void testNullBySubjectOrganization() {
CertificateAuthorityCredential.select(certMan).bySubjectOrganization(null);
CertificateAuthorityCredential.select(certMan).bySubjectSorted(null);
}
}

View File

@ -344,7 +344,7 @@ public class DBCertificateManagerTest extends SpringPersistenceTest {
Set<CertificateAuthorityCredential> retrievedCerts =
CertificateAuthorityCredential.select(certMan)
.bySubjectOrganization(stmEkCert.getIssuerOrganization())
.bySubjectSorted(stmEkCert.getIssuerSorted())
.getCertificates();
Assert.assertEquals(
@ -355,7 +355,7 @@ public class DBCertificateManagerTest extends SpringPersistenceTest {
Set<CertificateAuthorityCredential> secondRetrievedCerts =
CertificateAuthorityCredential.select(certMan)
.bySubjectOrganization(stmRootCaCert.getIssuerOrganization())
.bySubjectSorted(stmRootCaCert.getIssuerSorted())
.getCertificates();
Assert.assertEquals(
@ -380,7 +380,7 @@ public class DBCertificateManagerTest extends SpringPersistenceTest {
Set<CertificateAuthorityCredential> retrievedCerts =
CertificateAuthorityCredential.select(certMan)
.byIssuerOrganization(stmRootCaCert.getIssuerOrganization())
.byIssuerSorted(stmRootCaCert.getIssuerSorted())
.getCertificates();
Assert.assertEquals(
@ -706,7 +706,7 @@ public class DBCertificateManagerTest extends SpringPersistenceTest {
/**
* Tests that a {@link CertificateSelector} can be used to retrieve certificates in various
* forms, including {@link Certificate}, {@link X509Certificate}, and {@link KeyStore}.
* forms, including {@link Certificate}.
*
* @throws IOException if there is a problem creating the certificate
* @throws KeyStoreException if there is a problem constructing the resultant KeyStore