From 9a835d892355728aca34b798cbae54ccc23be469 Mon Sep 17 00:00:00 2001 From: iadgovuser26 Date: Fri, 21 Feb 2020 06:37:43 -0500 Subject: [PATCH] =?UTF-8?q?[222]=20Added=20TCG=20Event=20Log=20Processing?= =?UTF-8?q?=20that=20converts=20TCG=20Event=20Logs=20to=20HIRS=20T?= =?UTF-8?q?=E2=80=A6=20(#223)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added TCG Event Log Processing that converts TCG Event Logs to HIRS TPM Baselines * Some minor formating, syntax and code refactoring updates. * Updated checkstyle failures. * String format was missing additional %s. Co-authored-by: Cyrus <24922493+cyrus-dev@users.noreply.github.com> --- .../tpm/eventlog/CryptoAgileEventLog.java | 138 +++++++++++ .../java/hirs/tpm/eventlog/SHA1EventLog.java | 134 +++++++++++ .../java/hirs/tpm/eventlog/TCGEventLog.java | 16 ++ .../tpm/eventlog/TCGEventLogProcessor.java | 126 ++++++++++ .../java/hirs/tpm/eventlog/TcgTpmtHa.java | 196 ++++++++++++++++ .../java/hirs/tpm/eventlog/TpmPcrEvent.java | 218 ++++++++++++++++++ .../java/hirs/tpm/eventlog/TpmPcrEvent1.java | 51 ++++ .../java/hirs/tpm/eventlog/TpmPcrEvent2.java | 97 ++++++++ .../java/hirs/tpm/eventlog/package-info.java | 6 + .../src/main/java/hirs/utils/HexUtils.java | 38 ++- .../eventlog/TCGEventLogProcessorTest.java | 175 ++++++++++++++ .../java/hirs/tpm/eventlog/package-info.java | 5 + .../src/test/resources/tcgeventlog/TpmLog.bin | Bin 0 -> 7549 bytes .../tcgeventlog/TpmLogExpectedPcrs.txt | 24 ++ .../test/resources/tcgeventlog/TpmLogSHA1.bin | Bin 0 -> 18675 bytes .../tcgeventlog/TpmLogSHA1ExpectedPcrs.txt | 24 ++ 16 files changed, 1247 insertions(+), 1 deletion(-) create mode 100644 HIRS_Utils/src/main/java/hirs/tpm/eventlog/CryptoAgileEventLog.java create mode 100644 HIRS_Utils/src/main/java/hirs/tpm/eventlog/SHA1EventLog.java create mode 100644 HIRS_Utils/src/main/java/hirs/tpm/eventlog/TCGEventLog.java create mode 100644 HIRS_Utils/src/main/java/hirs/tpm/eventlog/TCGEventLogProcessor.java create mode 100644 HIRS_Utils/src/main/java/hirs/tpm/eventlog/TcgTpmtHa.java create mode 100644 HIRS_Utils/src/main/java/hirs/tpm/eventlog/TpmPcrEvent.java create mode 100644 HIRS_Utils/src/main/java/hirs/tpm/eventlog/TpmPcrEvent1.java create mode 100644 HIRS_Utils/src/main/java/hirs/tpm/eventlog/TpmPcrEvent2.java create mode 100644 HIRS_Utils/src/main/java/hirs/tpm/eventlog/package-info.java create mode 100644 HIRS_Utils/src/test/java/hirs/tpm/eventlog/TCGEventLogProcessorTest.java create mode 100644 HIRS_Utils/src/test/java/hirs/tpm/eventlog/package-info.java create mode 100644 HIRS_Utils/src/test/resources/tcgeventlog/TpmLog.bin create mode 100644 HIRS_Utils/src/test/resources/tcgeventlog/TpmLogExpectedPcrs.txt create mode 100644 HIRS_Utils/src/test/resources/tcgeventlog/TpmLogSHA1.bin create mode 100644 HIRS_Utils/src/test/resources/tcgeventlog/TpmLogSHA1ExpectedPcrs.txt diff --git a/HIRS_Utils/src/main/java/hirs/tpm/eventlog/CryptoAgileEventLog.java b/HIRS_Utils/src/main/java/hirs/tpm/eventlog/CryptoAgileEventLog.java new file mode 100644 index 00000000..2f5200d5 --- /dev/null +++ b/HIRS_Utils/src/main/java/hirs/tpm/eventlog/CryptoAgileEventLog.java @@ -0,0 +1,138 @@ +package hirs.tpm.eventlog; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; + +import hirs.utils.HexUtils; + +/** + * Class to handle the "Crypto Agile" Format for TCG Event Logs as defined in the + * TCG Platform Firmware Profile (PFP). + * The Format can provide multiple digests with different algorithm, + * however currently on SHA256 is supported. + * All other are currently ignored. + */ +public class CryptoAgileEventLog implements TCGEventLog { + /** + * SHA256 length = 24 bytes. + */ + private static final int PCR_LENGTH = TpmPcrEvent.SHA256_LENGTH; + /** + * Each PCR bank holds 24 registers. + */ + private static final int PCR_COUNT = 24; + /** + * PFP defined EV_NO_ACTION identifier. + */ + private static final int NO_ACTION_EVENT = 0x00000003; + /** + * 2 dimensional array holding the PCR values. + */ + private byte[][] pcrList = new byte[PCR_COUNT][PCR_LENGTH]; + /** + * List of parsed events within the log. + */ + private ArrayList eventList = new ArrayList<>(); + + /** + * Constructor. + * + * @param rawlog the entire tcg log + * @throws IOException if the input stream cannot access the log data + */ + public CryptoAgileEventLog(final byte[] rawlog) throws IOException { + ByteArrayInputStream is = new ByteArrayInputStream(rawlog); + // process the EfiSpecId Event in SHA1 format per the PFP + TpmPcrEvent1 idEvent = new TpmPcrEvent1(is); + eventList.add(idEvent); + // put all events into an event list for further processing + while (is.available() > 0) { // All other events should be Crypto agile + eventList.add(new TpmPcrEvent2(is)); + } + calculatePCRValues(); + } + + /** + * Returns a single PCR value given an index (PCR Number). + */ + @Override + public String[] getExpectedPCRValues() { + String[] pcrs = new String[PCR_COUNT]; + for (int i = 0; i < PCR_COUNT; i++) { + pcrs[i] = HexUtils.byteArrayToHexString(pcrList[i]); + } + return pcrs; + } + + /** + * Returns all 24 PCR values for display purposes. + * + * @param index pcr index + * @return Returns an array of strings for all 24 PCRs + */ + @Override + public String getExpectedPCRValue(final int index) { + return HexUtils.byteArrayToHexString(pcrList[index]); + } + + /** + * Calculates the "Expected Values for TPM PCRs based upon Event digests in the Event Log. + * Uses the algorithm and eventList passed into the constructor. + * + * @return a 2 dimensional bye array holding the hashes of the PCRs. + */ + private void calculatePCRValues() { + byte[] extendedPCR = null; + for (int i = 0; i < PCR_COUNT; i++) { // Initialize the PCRlist array + System.arraycopy(HexUtils.hexStringToByteArray( + "0000000000000000000000000000000000000000000000000000000000000000"), + 0, pcrList[i], 0, PCR_LENGTH); + } + for (TpmPcrEvent currentEvent : eventList) { + if (currentEvent.getPcrIndex() >= 0) { // Ignore NO_EVENTS which can have a PCR=-1 + try { + if (currentEvent.getEventType() != NO_ACTION_EVENT) { + // Don't include EV_NO_ACTION + extendedPCR = extendPCRsha256(pcrList[currentEvent.getPcrIndex()], + currentEvent.getEventDigest()); + System.arraycopy(extendedPCR, 0, pcrList[currentEvent.getPcrIndex()], + 0, PCR_LENGTH); + } + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } + } + } + } + + /** + * Extends a sha256 hash with a hash of new data. + * + * @param currentValue byte array holding the current hash value + * @param newEvent byte array holding the value to extend + * @return new hash value + * @throws NoSuchAlgorithmException if hash algorithm is not supported. + */ + private byte[] extendPCRsha256(final byte[] currentValue, final byte[] newEvent) + throws NoSuchAlgorithmException { + return sha256hash( + HexUtils.hexStringToByteArray(HexUtils.byteArrayToHexString(currentValue) + + HexUtils.byteArrayToHexString(newEvent))); + } + + /** + * Creates a sha356 hash of a given byte array. + * + * @param blob byte array hold the value to hash. + * @return byte array holding the hash of the input array. + * @throws NoSuchAlgorithmException if hash algorithm is not supported. + */ + private byte[] sha256hash(final byte[] blob) throws NoSuchAlgorithmException { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + md.update(blob); + return md.digest(); + } +} diff --git a/HIRS_Utils/src/main/java/hirs/tpm/eventlog/SHA1EventLog.java b/HIRS_Utils/src/main/java/hirs/tpm/eventlog/SHA1EventLog.java new file mode 100644 index 00000000..ced29ee4 --- /dev/null +++ b/HIRS_Utils/src/main/java/hirs/tpm/eventlog/SHA1EventLog.java @@ -0,0 +1,134 @@ +package hirs.tpm.eventlog; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; + +import hirs.utils.HexUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Class to handle the "SHA1" Format for TCG Event Logs. + * "SHA1" Format is defined in the TCG Platform Firmware Profile (PFP). + * This is to support older versions of UEFI Firmware or OS that create logs with SHA1. + */ +public class SHA1EventLog implements TCGEventLog { + private static final Logger LOGGER + = LogManager.getLogger(TCGEventLog.class); + /** + * SHA256 length = 24 bytes. + */ + private static final int PCR_LENGTH = TpmPcrEvent.SHA1_LENGTH; + /** + * Each PCR bank holds 24 registers. + */ + private static final int PCR_COUNT = 24; + /** + * PFP defined EV_NO_ACTION identifier. + */ + private static final int NO_ACTION_EVENT = 0x00000003; + /** + * List of parsed events within the log. + */ + private final ArrayList eventList = new ArrayList(); + /** + * 2 dimensional array holding the PCR values. + */ + private final byte[][] pcrList1 = new byte[PCR_COUNT][PCR_LENGTH]; + + /** + * Constructor. + * + * @param rawlog the entire tcg log + * @throws IOException if the inspur stream cannot access the log data + */ + public SHA1EventLog(final byte[] rawlog) throws IOException { + ByteArrayInputStream is = new ByteArrayInputStream(rawlog); + // put all events into an event list for further processing + while (is.available() > 0) { + eventList.add(new TpmPcrEvent1(is)); + } + calculatePcrValues(); + } + + /** + * Returns all 24 PCR values for display purposes. + * + * @return Returns an array of strings representing the expected hash values for all 24 PCRs + */ + public String[] getExpectedPCRValues() { + String[] pcrs = new String[PCR_COUNT]; + for (int i = 0; i < PCR_COUNT; i++) { + pcrs[i] = HexUtils.byteArrayToHexString(pcrList1[i]); + } + return pcrs; + } + + /** + * Returns a single PCR value given an index (PCR Number). + * + * @param index pcr index + * @return String representing the PCR contents + */ + public String getExpectedPCRValue(final int index) { + return HexUtils.byteArrayToHexString(pcrList1[index]); + } + + /** + * Calculates the "Expected Values for TPM PCRs based upon Event digests in the Event Log. + * Uses the algorithm and eventList passed into the constructor, + */ + private void calculatePcrValues() { + byte[] extendedPCR = null; + for (int i = 0; i < PCR_COUNT; i++) { // Initialize the PCRlist1 array + System.arraycopy(HexUtils.hexStringToByteArray( + "0000000000000000000000000000000000000000"), + 0, pcrList1[i], 0, PCR_LENGTH); + } + for (TpmPcrEvent currentEvent : eventList) { + if (currentEvent.getPcrIndex() >= 0) { // Ignore NO_EVENTS which can have a PCR=-1 + try { + if (currentEvent.getEventType() != NO_ACTION_EVENT) { + // Don't include EV_NO_ACTION event + extendedPCR = extendPCRsha1(pcrList1[currentEvent.getPcrIndex()], + currentEvent.getEventDigest()); + System.arraycopy(extendedPCR, 0, pcrList1[currentEvent.getPcrIndex()], + 0, currentEvent.getDigestLength()); + } + } catch (NoSuchAlgorithmException e) { + LOGGER.error(e); + } + } + } + } + + /** + * Extends a sha1 hash with a hash of new data. + * + * @param currentValue value to extend + * @param newEvent value to extend with + * @return new hash resultant hash + * @throws NoSuchAlgorithmException if hash algorithm not supported + */ + private byte[] extendPCRsha1(final byte[] currentValue, final byte[] newEvent) + throws NoSuchAlgorithmException { + return sha1hash(HexUtils.hexStringToByteArray(HexUtils.byteArrayToHexString(currentValue) + + HexUtils.byteArrayToHexString(newEvent))); + } + + /** + * Creates a sha1 hash of a given byte array. + * + * @param blob byte array of data to hash + * @return byte array holding the hash of the input array + * @throws NoSuchAlgorithmException id hash algorithm not supported + */ + private byte[] sha1hash(final byte[] blob) throws NoSuchAlgorithmException { + MessageDigest md = MessageDigest.getInstance("SHA1"); + md.update(blob); + return md.digest(); + } +} diff --git a/HIRS_Utils/src/main/java/hirs/tpm/eventlog/TCGEventLog.java b/HIRS_Utils/src/main/java/hirs/tpm/eventlog/TCGEventLog.java new file mode 100644 index 00000000..87d740ac --- /dev/null +++ b/HIRS_Utils/src/main/java/hirs/tpm/eventlog/TCGEventLog.java @@ -0,0 +1,16 @@ +package hirs.tpm.eventlog; + +/** + * Interface for handling different formats of TCG Event logs. + */ +public interface TCGEventLog { + /** Retrieves a all expected PCR values. + * @return String array holding all PCR Values + */ + String[] getExpectedPCRValues(); + /** Retrieves a single expected PCR values. + * @param index the PCR reference + * @return a String holding an expected PCR value + */ + String getExpectedPCRValue(int index); +} diff --git a/HIRS_Utils/src/main/java/hirs/tpm/eventlog/TCGEventLogProcessor.java b/HIRS_Utils/src/main/java/hirs/tpm/eventlog/TCGEventLogProcessor.java new file mode 100644 index 00000000..a38e26b3 --- /dev/null +++ b/HIRS_Utils/src/main/java/hirs/tpm/eventlog/TCGEventLogProcessor.java @@ -0,0 +1,126 @@ +package hirs.tpm.eventlog; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.math.BigInteger; + +import hirs.data.persist.TPMMeasurementRecord; +import hirs.data.persist.TpmWhiteListBaseline; +import hirs.utils.HexUtils; +import hirs.data.persist.Digest; +import hirs.data.persist.DigestAlgorithm;; + +/** + * Class for parsing a TCG EventLogs (both SHA1 and Crypto Agile Formats). + * Also produces a TPM Baseline using he digests within the event log. + * Constructor parses the input byte array into a List of TpmPcrEvents. + */ +public class TCGEventLogProcessor { + /** + * Name of the hash algorithm used to process the Event Log, default is SHA256. + */ + private String algorithm = "SHA256"; + /** + * PFP defined EV_NO_ACTION identifier. + */ + private static final int NO_ACTION_EVENT = 0x00000003; + /** + * Parsed event log array. + */ + private TCGEventLog tcgLog = null; + /** + * EV_NO_ACTION signature offset. + */ + private static final int SIG_OFFSET = 32; + /** + * TEV_NO_ACTION signature size. + */ + private static final int SIG_SIZE = 16; + + /** + * Constructor. + * + * @param rawLog the byte array holding the contents of the TCG Event Log + * @throws IOException if there is a parsing error + */ + public TCGEventLogProcessor(final byte[] rawLog) throws IOException { + if (isLogCrytoAgile(rawLog)) { + tcgLog = new CryptoAgileEventLog(rawLog); + } else { + tcgLog = new SHA1EventLog(rawLog); + algorithm = "SHA"; + } + } + + /** + * Returns all 24 PCR values for display purposes. + * + * @return Returns an array of strings representing the expected hash values for all 24 PCRs + */ + public String[] getExpectedPCRValues() { + return tcgLog.getExpectedPCRValues(); + } + + /** + * Returns a single PCR value given an index (PCR Number). + * + * @param index the PCR index + * @return String representing the PCR contents + */ + public String getExpectedPCRValue(final int index) { + return tcgLog.getExpectedPCRValue(index); + } + + /** + * Creates a TPM baseline using the expected PCR Values. + * Expected PCR Values were Calculated from the EventLog (RIM Support file). + * + * @param name name to call the TPM Baseline + * @return whitelist baseline + */ + public TpmWhiteListBaseline createTPMBaseline(final String name) { + TpmWhiteListBaseline baseline = new TpmWhiteListBaseline(name); + TPMMeasurementRecord record = null; + String pcrValue = ""; + for (int i = 0; i < TpmPcrEvent.PCR_COUNT; i++) { + if (algorithm.compareToIgnoreCase("SHA1") == 0) { // Log Was SHA1 Format + pcrValue = tcgLog.getExpectedPCRValue(i); + byte[] hexValue = HexUtils.hexStringToByteArray(pcrValue); + final Digest hash = new Digest(DigestAlgorithm.SHA1, hexValue); + record = new TPMMeasurementRecord(i, hash); + } else { // Log was Crypto Agile, currently assumes SHA256 + pcrValue = tcgLog.getExpectedPCRValue(i); + byte[] hexValue = HexUtils.hexStringToByteArray(pcrValue); + final Digest hash = new Digest(DigestAlgorithm.SHA256, hexValue); + record = new TPMMeasurementRecord(i, hash); + } + baseline.addToBaseline(record); + } + return baseline; + } + + /** + * Determines if an event is an EfiSpecIdEvent indicating that the log format is crypto agile. + * The EfiSpecIdEvent should be the first event in the TCG TPM Event Log. + * + * @param log The Event Log + * @return true if EfiSpecIDEvent is found and indicates that the format is crypto agile + * @throws UnsupportedEncodingException + */ + private boolean isLogCrytoAgile(final byte[] log) throws UnsupportedEncodingException { + byte[] eType = new byte[TpmPcrEvent.INT_LENGTH]; + System.arraycopy(log, TpmPcrEvent.INT_LENGTH, eType, 0, TpmPcrEvent.INT_LENGTH); + byte[] eventType = HexUtils.leReverseByte(eType); + int eventID = new BigInteger(eventType).intValue(); + if (eventID != NO_ACTION_EVENT) { + 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 + if (sig.equals("Spec ID Event03")) { + return true; + } + return (false); + } +} diff --git a/HIRS_Utils/src/main/java/hirs/tpm/eventlog/TcgTpmtHa.java b/HIRS_Utils/src/main/java/hirs/tpm/eventlog/TcgTpmtHa.java new file mode 100644 index 00000000..d0d5a72b --- /dev/null +++ b/HIRS_Utils/src/main/java/hirs/tpm/eventlog/TcgTpmtHa.java @@ -0,0 +1,196 @@ +package hirs.tpm.eventlog; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.math.BigInteger; + +import hirs.utils.HexUtils; + +/** + * Class to for the TCG defined TPMT_HA structure used to support the Crypto Agile Log format. + *

+ * typedef struct { + * TPMI_ALG_HASH hashAlg; + * TPMU_HA digest; + * } TPMT_HA; + */ +public class TcgTpmtHa { + /** + * TCG Defined Algorithm Identifiers . + */ + private int hashAlgId = 0; + /** + * Length of the hash. + */ + private int hashLength = 0; + /** + * Human readable name of the hash algorithm. + */ + private String hashName = ""; + /** + * Hash data. + */ + private byte[] digest = null; + /** + * TCG ID for SHA1. + */ + private static final int TPM_ALG_SHA1 = 0x04; + /** + * TCG ID for SHA1. + */ + private static final int TPM_ALG_SHA256 = 0x0B; + /** + * TCG ID for SHA 384. + */ + private static final int TPM_ALG_SHA384 = 0x0C; + /** + * TCG ID for SHA512. + */ + private static final int TPM_ALG_SHA_512 = 0x0D; + /** + * TCG ID for Null algorithm. + */ + private static final int TPM_ALG_NULL = 0x10; + /** + * TCG ID for SHA1. + */ + private static final int TPM_ALG_SHA1_LENGTH = 20; + /** + * TCG ID for SHA1. + */ + private static final int TPM_ALG_SHA256_LENGH = 32; + /** + * TCG ID for SHA 384. + */ + private static final int TPM_ALG_SHA384_LENGTH = 48; + /** + * TCG ID for SHA512. + */ + private static final int TPM_ALG_SHA512_LENGTH = 64; + /** + * TCG ID for Null algorithm. + */ + private static final int TPM_ALG_NULL_LENGTH = 0; + + /** + * Constructor. + * + * @param is ByteArrayInputStream holding the TcgTPMT_HA structured data + * @throws IOException if TPMT_HA structure cannot be parsed + */ + public TcgTpmtHa(final ByteArrayInputStream is) throws IOException { + byte[] algID = new byte[2]; + is.read(algID); + byte[] rAlgID = HexUtils.leReverseByte(algID); + hashAlgId = new BigInteger(rAlgID).intValue(); + hashName = tcgAlgIdtoString(algID[0]); + hashLength = tcgAlgLength(algID[0]); + digest = new byte[hashLength]; + is.read(digest); + } + + /** + * Returns the TCG defined algorithm identifier. + * + * @return integer that specifies the algorithm as defined by the TCG + */ + public int getAlgId() { + return hashAlgId; + } + + /** + * Return the length of the Hash. + * + * @return the Hash length + */ + public int getHashLength() { + return hashLength; + } + + /** + * Readable name of the algorithm. + * + * @return Hash algorithm name + */ + public String getHashName() { + return hashName; + } + + /** + * @return digest held by the event + */ + protected byte[] getDigest() { + return digest; + } + + /** + * Readable description of the Algorithm. + * + * @return Readable Algorithm name + */ + @Override + public String toString() { + return String.format("%s hash = %s", hashName, HexUtils.byteArrayToHexString(digest)); + } + + /** + * Returns the hash name via a lookup. + * Lookup based upon section 6.3 for the TPM-Rev-2.0-Part-2-Structures.pdf document. + * Only hash algorithms found in Table 7 are used. + * + * @param algid int to convert to string + */ + private String tcgAlgIdtoString(final int algid) { + String alg; + switch (algid) { + case TPM_ALG_SHA1: + alg = "TPM_ALG_SHA1"; + break; + case TPM_ALG_SHA256: + alg = "TPM_ALG_SHA256"; + break; + case TPM_ALG_SHA384: + alg = "TPM_ALG_SHA384"; + break; + case TPM_ALG_SHA_512: + alg = "TPM_ALG_SHA512"; + break; + case TPM_ALG_NULL: + alg = "TPM_ALG_NULL"; + break; + default: + alg = "Unknown or invalid Hash"; + } + return alg; + } + + /** + * Sets the length of a given TPM ALG Identifier. + * (lookup based upon section 6.3 for the TPM-Rev-2.0-Part-2-Structures.pdf document) + * Only hash algorithms found in Table 7 are used. + * + * @param algId TCG defined Algorithm identifier + * @return length of hash data in bytes + */ + private int tcgAlgLength(final int algId) { + int length; + switch (algId) { + case TPM_ALG_SHA1: + length = TPM_ALG_SHA1_LENGTH; + break; + case TPM_ALG_SHA256: + length = TPM_ALG_SHA256_LENGH; + break; + case TPM_ALG_SHA384: + length = TPM_ALG_SHA384_LENGTH; + break; + case TPM_ALG_SHA_512: + length = TPM_ALG_SHA512_LENGTH; + break; + case TPM_ALG_NULL: + default: + length = TPM_ALG_NULL_LENGTH; + } + return length; + } +} diff --git a/HIRS_Utils/src/main/java/hirs/tpm/eventlog/TpmPcrEvent.java b/HIRS_Utils/src/main/java/hirs/tpm/eventlog/TpmPcrEvent.java new file mode 100644 index 00000000..93724934 --- /dev/null +++ b/HIRS_Utils/src/main/java/hirs/tpm/eventlog/TpmPcrEvent.java @@ -0,0 +1,218 @@ +package hirs.tpm.eventlog; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.math.BigInteger; + +import hirs.utils.HexUtils; + +/** + * Class to process a TCG_PCR_EVENT. + * TCG_PCR_EVENT is used when the Event log uses the SHA1 Format as described in the + * TCG Platform Firmware Profile (PFP) specification. + * typedef struct { + * TCG_PCRINDEX PCRIndex; //PCR Index value that either + * //matches the PCRIndex of a + * //previous extend operation or + * //indicates that this Event Log + * //entry is not associated with + * //an extend operation + * TCG_EVENTTYPE EventType; //See Log event types defined in toStrng() + * TCG_DIGEST digest; //The hash of the event data + * UINT32 EventSize; //Size of the event data + * UINT8 Event[EventSize]; //The event data + * } TCG_PCR_EVENT; + */ +public class TpmPcrEvent { + /** + * Type length = 4 bytes. + */ + public static final int EV_TYPE_SIZE = 4; + /** + * Event Log spec version. + */ + public static final int MIN_SIZE = 32; + /** + * Event Type (byte array). + */ + public static final int INT_LENGTH = 4; + /** + * Event Type (byte array). + */ + public static final int SHA1_LENGTH = 20; + /** + * Event Type (byte array). + */ + public static final int SHA256_LENGTH = 32; + /** + * Each PCR bank holds 24 registers. + */ + public static final int PCR_COUNT = 24; + /** + * PCR index. + */ + private int pcrIndex = -1; + /** + * Event Type (long). + */ + private long eventType = 0; + /** + * Event digest. + */ + private byte[] digest = null; + /** + * Even data. + */ + private byte[] eventContent; + /** + * TCG Event Log spec version. + */ + private static String version = "Unknown"; + /** + * TCG Event Log errata version. + */ + private static String errata = "Unknown"; + /** + * Length (in bytes) of a pcr. + */ + private int digestLength = 0; + + /** + * Constructor. + * + * @param is ByteArrayInputStream holding the event + * @throws IOException when event can't be parsed + */ + public TpmPcrEvent(final ByteArrayInputStream is) throws IOException { + + } + + /** + * Sets the digest from a TCG_PCR_EVENT digest field. + * This can be SHA1 for older event structures or any algorithm for newer structure. + * + * @param digestData cryptographic hash + */ + protected void setEventDigest(final byte[] digestData) { + digest = new byte[digestLength]; + System.arraycopy(digestData, 0, digest, 0, this.digestLength); + } + + /** + * Retrieves the digest from a TCG Event. + * This can be SHA1 for older event structures or any algorithm for newer structure. + * + * @return the digest data for the event + */ + public byte[] getEventDigest() { + byte[] digestCopy = new byte[digestLength]; + System.arraycopy(digest, 0, digestCopy, 0, this.digestLength); + return digestCopy; + } + + /** + * Sets the event PCR index value from a TCG Event. + * + * @param eventIndex TCG Event PCR Index as defined in the PFP + */ + protected void setPcrIndex(final byte[] eventIndex) { + pcrIndex = HexUtils.leReverseInt(eventIndex); + } + + /** + * Gets the event index value from a TCG Event. + * + * @return eventIndex TCG Event Index as defined in the PFP + */ + public int getPcrIndex() { + return pcrIndex; + } + + /** + * Sets the EventType. + * + * @param type byte array holding the PFP defined log event type + */ + protected void setEventType(final byte[] type) { + byte[] evType = HexUtils.leReverseByte(type); + eventType = new BigInteger(evType).longValue(); + } + + /** + * Returns the EventType for the Event. + * + * @return event type + */ + public long getEventType() { + return eventType; + } + + /** + * Returns the version of the TCG Log Event specification pertaining to the log. + * only updated if the event is a TCG_EfiSpecIdEvent. + * + * @return specification version + */ + public String getSpecVersion() { + return version; + } + + /** + * Returns the Errata version of the TCG Log Event specification pertaining to the log. + * only updated if the event is a TCG_EfiSpecIdEvent). + * + * @return Errata version + */ + public String getSpecErrataVersion() { + return errata; + } + + /** + * Sets the event digest value after processing. + * + * @param digestData SHA1 or SHA256 digest to set + */ + protected void setDigest(final byte[] digestData) { + digest = new byte[digestLength]; + System.arraycopy(digestData, 0, digest, 0, digestLength); + } + + /** + * Sets the event content after processing. + * + * @param eventData The PFP defined event content + */ + protected void setEventContent(final byte[] eventData) { + eventContent = new byte[eventData.length]; + System.arraycopy(eventContent, 0, eventData, 0, eventData.length); + } + + /** + * Gets the length of number of bytes in a PCR for the event. + * event log format. + * + * @return byte array holding the events content field + */ + protected byte[] getEventContent() { + return eventContent; + } + + /** + * Sets the Digest Length. + * Also the number of bytes expected within each PCR. + * + * @param length number of bytes in a PCR for the event. + */ + public void setDigestLength(final int length) { + digestLength = length; + } + + /** + * Gets the length of number of bytes in a PCR for the event. + * + * @return Byte Array containing the PFP defined event content + */ + public int getDigestLength() { + return digestLength; + } +} diff --git a/HIRS_Utils/src/main/java/hirs/tpm/eventlog/TpmPcrEvent1.java b/HIRS_Utils/src/main/java/hirs/tpm/eventlog/TpmPcrEvent1.java new file mode 100644 index 00000000..11868ba7 --- /dev/null +++ b/HIRS_Utils/src/main/java/hirs/tpm/eventlog/TpmPcrEvent1.java @@ -0,0 +1,51 @@ +package hirs.tpm.eventlog; + +import hirs.utils.HexUtils; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +/** + * Class to process a TCG_PCR_EVENT. + * TCG_PCR_EVENT is used when the Event log uses the SHA1 Format as described in the + * TCG Platform Firmware Profile specification. + * typedef struct { + * UINT32 PCRIndex; //PCR Index value that either + * //matches the PCRIndex of a + * //previous extend operation or + * //indicates that this Event Log + * //entry is not associated with + * //an extend operation + * UINT32 EventType; //See Log event types + * BYTE digest[20]; //The SHA1 hash of the event data + * UINT32 EventSize; //Size of the event data + * UINT8 Event[1]; // + * } TCG_PCR_EVENT; //The event data structure to be added + */ +public class TpmPcrEvent1 extends TpmPcrEvent { + + /** + * Constructor. + * + * @param is ByteArrayInputStream holding the TCG Log event + * @throws IOException if an error occurs in parsing the event + */ + public TpmPcrEvent1(final ByteArrayInputStream is) throws IOException { + super(is); + setDigestLength(SHA1_LENGTH); + byte[] unit32Data = new byte[INT_LENGTH]; + if (is.available() > MIN_SIZE) { + is.read(unit32Data); + setPcrIndex(unit32Data); + is.read(unit32Data); + setEventType(unit32Data); + byte[] eventDigest = new byte[SHA1_LENGTH]; + is.read(eventDigest); + setDigest(eventDigest); + is.read(unit32Data); + int eventSize = HexUtils.leReverseInt(unit32Data); + byte[] eventContent = new byte[eventSize]; + is.read(eventContent); + } + } +} diff --git a/HIRS_Utils/src/main/java/hirs/tpm/eventlog/TpmPcrEvent2.java b/HIRS_Utils/src/main/java/hirs/tpm/eventlog/TpmPcrEvent2.java new file mode 100644 index 00000000..ad8c1f20 --- /dev/null +++ b/HIRS_Utils/src/main/java/hirs/tpm/eventlog/TpmPcrEvent2.java @@ -0,0 +1,97 @@ +package hirs.tpm.eventlog; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.ArrayList; + +import hirs.utils.HexUtils; + +/** + * Class to process a TCG_PCR_EVENT2 which is used + * when the Event log uses the Crypto Agile (SHA256) format as described in the + * TCG Platform Firmware Profile specification. + * This class will only process SHA-256 digests. + * typedef struct { + * UINT32 PCRIndex; //PCR Index value that either + * //matches the PCRIndex of a + * //previous extend operation or + * //indicates that this Event Log + * //entry is not associated with + * //an extend operation + * UINT32 EventType; //See Log event types + * TPML_DIGEST_VALUES digest; //The hash of the event data + * UINT32 EventSize; //Size of the event data + * BYTE Event[1]; //The event data + * } TCG_PCR_EVENT2; //The event data structure to be added + * typedef struct { + * UINT32 count; + * TPMT_HA digests[HASH_COUNT]; + * } TPML_DIGEST_VALUES; + * typedef struct { + * TPMI_ALG_HASH hashAlg; + * TPMU_HA digest; + * } TPMT_HA; + * typedef union { + * BYTE sha1[SHA1_DIGEST_SIZE]; + * BYTE sha256[SHA256_DIGEST_SIZE]; + * BYTE sha384[SHA384_DIGEST_SIZE]; + * BYTE sha512[SHA512_DIGEST_SIZE]; + * } TPMU_HA; + * define SHA1_DIGEST_SIZE 20 + * define SHA256_DIGEST_SIZE 32 + * define SHA384_DIGEST_SIZE 48 + * define SHA512_DIGEST_SIZE 64 + * typedef TPM_ALG_ID TPMI_ALG_HASH; + * typedef UINT16 TPM_ALG_ID; + * define TPM_ALG_SHA1 (TPM_ALG_ID)(0x0004) + * define TPM_ALG_SHA256 (TPM_ALG_ID)(0x000B) + * define TPM_ALG_SHA384 (TPM_ALG_ID)(0x000C) + * define TPM_ALG_SHA512 (TPM_ALG_ID)(0x000D) + */ +public class TpmPcrEvent2 extends TpmPcrEvent { + /** + * algorithms found. + */ + private int algCount = 0; + + /** + * list of digests. + */ + private ArrayList hashlist = new ArrayList<>(); + + /** + * Constructor. + * + * @param is ByteArrayInputStream holding the TCG Log event + * @throws IOException if an error occurs in parsing the event + */ + public TpmPcrEvent2(final ByteArrayInputStream is) throws IOException { + super(is); + setDigestLength(SHA256_LENGTH); + //TCG_PCR_EVENT2 + byte[] rawInt = new byte[INT_LENGTH]; + if (is.available() > MIN_SIZE) { + is.read(rawInt); + setPcrIndex(rawInt); + is.read(rawInt); + setEventType(rawInt); + // TPML_DIGEST_VALUES + is.read(rawInt); + algCount = HexUtils.leReverseInt(rawInt); + TcgTpmtHa hashAlg = null; + // Process TPMT_HA, + for (int i = 0; i < algCount; i++) { + hashAlg = new TcgTpmtHa(is); + hashlist.add(hashAlg); + if (hashAlg.getHashName().compareToIgnoreCase("TPM_ALG_SHA256") == 0) { + setDigest(hashAlg.getDigest()); + } + } + is.read(rawInt); + int eventSize = HexUtils.leReverseInt(rawInt); + byte[] eventContent = new byte[eventSize]; + is.read(eventContent); + setEventContent(eventContent); + } + } +} diff --git a/HIRS_Utils/src/main/java/hirs/tpm/eventlog/package-info.java b/HIRS_Utils/src/main/java/hirs/tpm/eventlog/package-info.java new file mode 100644 index 00000000..e20b170f --- /dev/null +++ b/HIRS_Utils/src/main/java/hirs/tpm/eventlog/package-info.java @@ -0,0 +1,6 @@ +/** + * Non-persistant classes related to TGC Event Logs. + */ + +package hirs.tpm.eventlog; + diff --git a/HIRS_Utils/src/main/java/hirs/utils/HexUtils.java b/HIRS_Utils/src/main/java/hirs/utils/HexUtils.java index 97516ced..98a1175c 100644 --- a/HIRS_Utils/src/main/java/hirs/utils/HexUtils.java +++ b/HIRS_Utils/src/main/java/hirs/utils/HexUtils.java @@ -1,5 +1,7 @@ package hirs.utils; +import java.math.BigInteger; + /** * Utilities for working with hex strings and byte arrays. */ @@ -40,7 +42,7 @@ public final class HexUtils { * @return hex string representation of array */ public static String byteArrayToHexString(final byte[] b) { - StringBuffer sb = new StringBuffer(); + StringBuilder sb = new StringBuilder(); String returnStr = ""; for (int i = 0; i < b.length; i++) { String singleByte = Integer.toHexString(b[i] & FF_BYTE); @@ -74,4 +76,38 @@ public final class HexUtils { System.arraycopy(b, start, copy, 0, end - start + 1); return copy; } + + /** + * Takes in a byte array and reverses the order. + * @param in byte array to reverse + * @return reversed byte array + */ + public static byte[] leReverseByte(final byte[] in) { + byte[] finished = new byte[in.length]; + for (int i = 0; i < finished.length; i++) { + finished[i] = in[(in.length - 1) - i]; + } + return finished; + } + + /** + * Takes in a byte array and reverses the order then converts to an int. + * @param in byte array to reverse + * @return integer that represents the reversed byte array + */ + public static int leReverseInt(final byte[] in) { + byte[] finished = leReverseByte(in); + return new BigInteger(finished).intValue(); + } + + /** + * Takes in a byte array of 4 bytes and returns a long. + * @param bytes byte array to convert + * @return long representation of the bytes + */ + public static long bytesToLong(final byte[] bytes) { + BigInteger lValue = new BigInteger(bytes); + + return lValue.abs().longValue(); + } } diff --git a/HIRS_Utils/src/test/java/hirs/tpm/eventlog/TCGEventLogProcessorTest.java b/HIRS_Utils/src/test/java/hirs/tpm/eventlog/TCGEventLogProcessorTest.java new file mode 100644 index 00000000..288aa2a6 --- /dev/null +++ b/HIRS_Utils/src/test/java/hirs/tpm/eventlog/TCGEventLogProcessorTest.java @@ -0,0 +1,175 @@ +package hirs.tpm.eventlog; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +import org.apache.commons.io.IOUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.hibernate.Session; + + +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +/* +import org.junit.Test; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +*/ + +import hirs.data.persist.Baseline; +import hirs.data.persist.Digest; +import hirs.data.persist.SpringPersistenceTest; +import hirs.data.persist.TpmWhiteListBaseline; +import hirs.utils.HexUtils; + +/** + * Class for testing TCG Event Log processing. + */ +public class TCGEventLogProcessorTest extends SpringPersistenceTest { + private static final String DEFAULT_EVENT_LOG = "/tcgeventlog/TpmLog.bin"; + private static final String DEFAULT_EXPECTED_PCRS = "/tcgeventlog/TpmLogExpectedPcrs.txt"; + private static final String SHA1_EVENT_LOG = "/tcgeventlog/TpmLogSHA1.bin"; + private static final String SHA1_EXPECTED_PCRS = "/tcgeventlog/TpmLogSHA1ExpectedPcrs.txt"; + private static final Logger LOGGER + = LogManager.getLogger(TCGEventLogProcessorTest.class); + + /** + * Initializes a SessionFactory. The factory is used for an in-memory database that + * is used for testing. + */ + @BeforeClass + public static final void setup() { + LOGGER.debug("retrieving session factory"); + + } + + /** + * Closes the SessionFactory from setup. + */ + @AfterClass + public static final void tearDown() { + LOGGER.debug("closing session factory"); + } + + /** + * Resets the test state to a known good state. This currently only resets the database by + * removing all Baseline objects. + */ + // @AfterMethod + public final void resetTestState() { + LOGGER.debug("reset test state"); + LOGGER.debug("deleting all baselines"); + Session session = sessionFactory.getCurrentSession(); + session.beginTransaction(); + final List baselines = session.createCriteria(Baseline.class).list(); + for (Object o : baselines) { + LOGGER.debug("deleting baseline: {}", o); + session.delete(o); + } + LOGGER.debug("all baselines removed"); + session.getTransaction().commit(); + } + + /** + * Tests the processing of a cryto agile event log. + * @throws IOException when processing the test fails + */ + @Test + public final void testCryptoAgileTCGEventLog() throws IOException { + LOGGER.debug("Testing the parsing of a Crypto Agile formatted TCG Event Log"); + InputStream log, pcrs; + boolean testPass = true; + log = this.getClass().getResourceAsStream(DEFAULT_EVENT_LOG); + byte[] rawLogBytes = IOUtils.toByteArray(log); + TCGEventLogProcessor tlp = new TCGEventLogProcessor(rawLogBytes); + String[] pcrFromLog = tlp.getExpectedPCRValues(); + pcrs = this.getClass().getResourceAsStream(DEFAULT_EXPECTED_PCRS); + Object[] pcrObj = IOUtils.readLines(pcrs).toArray(); + String[] pcrTxt = Arrays.copyOf(pcrObj, pcrObj.length, String[].class); + // Test 1 get all PCRs + for (int i = 0; i < 24; i++) { + if (pcrFromLog[i].compareToIgnoreCase(pcrTxt[i]) != 0) { + testPass = false; + LOGGER.error("\ntestTCGEventLogProcessorParser error with PCR " + i); + } + } + Assert.assertTrue(testPass); + // Test 2 get an individual PCR + String pcr3 = tlp.getExpectedPCRValue(3); + Assert.assertEquals(pcr3, pcrFromLog[3]); + LOGGER.debug("OK. Parsing of a Crypto Agile Format Success"); + } + + /** + * Tests the processing of a SHA1 formatted Event log. + * @throws IOException when processing the test fails + */ + @Test + public final void testSHA1TCGEventLog() throws IOException { + LOGGER.debug("Testing the parsing of a SHA1 formated TCG Event Log"); + InputStream log, pcrs; + boolean testPass = true; + log = this.getClass().getResourceAsStream(SHA1_EVENT_LOG); + byte[] rawLogBytes = IOUtils.toByteArray(log); + TCGEventLogProcessor tlp = new TCGEventLogProcessor(rawLogBytes); + String[] pcrFromLog = tlp.getExpectedPCRValues(); + pcrs = this.getClass().getResourceAsStream(SHA1_EXPECTED_PCRS); + Object[] pcrObj = IOUtils.readLines(pcrs).toArray(); + String[] pcrTxt = Arrays.copyOf(pcrObj, pcrObj.length, String[].class); + // Test 1 get all PCRs + for (int i = 0; i < 24; i++) { + if (pcrFromLog[i].compareToIgnoreCase(pcrTxt[i]) != 0) { + testPass = false; + LOGGER.error("\ntestTCGEventLogProcessorParser error with PCR " + i); + } + } + Assert.assertTrue(testPass); + // Test 2 get an individual PCR + String pcr0 = tlp.getExpectedPCRValue(0); + Assert.assertEquals(pcr0, pcrFromLog[0]); + LOGGER.debug("OK. Parsing of a SHA1 formatted TCG Event Log Success"); + } + + /** + * Tests TPM Baseline creation from a EventLog. + * @throws IOException when processing the test fails + */ + @Test + public final void testTPMBaselineCreate() throws IOException { + LOGGER.debug("Create and save TPM baseline from TCG Event Log test started"); + InputStream log; + boolean testPass = true; + log = this.getClass().getResourceAsStream(DEFAULT_EVENT_LOG); + byte[] rawLogBytes = IOUtils.toByteArray(log); + TCGEventLogProcessor tlp = new TCGEventLogProcessor(rawLogBytes); + String[] pcrFromLog = tlp.getExpectedPCRValues(); + // save it to the test db + LOGGER.debug("Creating and saving a TPM baseline from TCG Event Log"); + Session session = sessionFactory.getCurrentSession(); + session.beginTransaction(); + final TpmWhiteListBaseline b = tlp.createTPMBaseline("TcgEventLogTestBaseline"); + session.save(b); + session.getTransaction().commit(); + // Check that the TPM Baseline contains he correct info + for (int i = 0; i < 24; i++) { + Set records = b.getPCRHashes(i); + for (Digest digest:records) { + String pcrValue = HexUtils.byteArrayToHexString(digest.getDigest()); + if (pcrFromLog[i].compareToIgnoreCase(pcrValue) != 0) { + testPass = false; + LOGGER.error("\testTPMBaselineCreate error with PCR " + i); + } + } + } + Assert.assertTrue(testPass); + LOGGER.debug("OK. Create and save TPM baseline from TCG Event Log was a success"); + } +} diff --git a/HIRS_Utils/src/test/java/hirs/tpm/eventlog/package-info.java b/HIRS_Utils/src/test/java/hirs/tpm/eventlog/package-info.java new file mode 100644 index 00000000..3aab200e --- /dev/null +++ b/HIRS_Utils/src/test/java/hirs/tpm/eventlog/package-info.java @@ -0,0 +1,5 @@ +/** + * Test classes for the hirs.tpm package. + */ + +package hirs.tpm.eventlog; diff --git a/HIRS_Utils/src/test/resources/tcgeventlog/TpmLog.bin b/HIRS_Utils/src/test/resources/tcgeventlog/TpmLog.bin new file mode 100644 index 0000000000000000000000000000000000000000..0b8f1f398d51035bc91afbe8400d4888a28d5669 GIT binary patch literal 7549 zcmds53p`a>+h2!6H)=?dq+LukI^;4cmCNBAxieB{D&BHAZX@U9mIif5nUNZairneu zQp}Wyq6@02K{H)mgj^eyq^o4S&)WN_P+#Ny=J)&heQW>rUhAyAp6CC6*7K~jo_!EP zNEGf5m08fag3om(Oj!gwfa~q2u0;(KK_WD0O+Wlph?h|Ve9UlZ!23O^DykICyO6^xhF!iW#)@Hjb%6?P>>0o5n5keZ!_oTo4uA#OWU*I))Mz&c4)|qjT%7^|5Zq<|bFwt~%T4 zG-1d1*mE~erw5%;uoStY4Wk_!TX$aFdqXNq5;YwVogXzGqeH<2a9M%4$Qk(~AE*rx z4=z81hQc%)DI9-liEdW9rTrx@mwT^M`bB+HqOw=ty zu}$$W2wOVf`9#FbideWY@vhyjWhL{UH0<8;tojkH*Sr_A9juwjPK7CK(#1b5-$)?; zFYkD+(8v#b*)2Z3si(Q*UgDBo3m7d&{P8VRsOh8_uHF`gutgsugBZ<^3e+23vx~=F zB^a`Hr|IG{NWSIXl%$k!i?BX<{?@V=OScZt(NOf?4}R6*Cw1j|L2g%Z3%7J#T&QHi z&Oe)r^#^}j(EBg{{h7+zrtPGsM%5AjeMiN5+_%)y4h(vV*+`Janh!?bEa z$%-KlwX9AQ|J&nG>Vqj1`c2aI^#z0TM;ZMo+dCINOPiba$|^2cV6vX~fH-np9g zOZg&=-tChLo}gO+v|qDpcy`A7J7TYUSY0=GMxv{?QRs4`LywU~_8Y|W*u~oucQ$&B zWsgMHX19xA>kmv>v zN%YIa;0E*eW}a7=w#rP>!6c#~cj6g^D3y`u>_rlsG9|g}R9T~0;d>JJ3hVo2nFb{@ zuQzV;t#_FGltizl(22co!<}q=%JzWV*3ei_Ys++?EVhfUPRyI@d3Gea zVaNQ1Uu1`q2JR-H}ecZe`4CWTSj41 zvrNzTN-{>Ge}64VJap2G!2J)}oITkkwLUY|@{1AQ*u2ez{`wJJ==ToN_IC%%-}8HM zpIU*EPLajo`*oqa!y*ME(WC8Z3%}{>7HD=>8M&-#Xy+jpYq>4u8cDyfXKHpHkS1+! z`kjOEnx2ks#r;dOHBk?>NSz8R%;8N-;9zt-d$8XLcUt>}^N4ocRFpnzIq#aPO*He7 zXN`9XePWKV$fn(MOc5+d2Po*KTVR+~#OZJnZ zZ;yN1sV(~wbz3!9e7o2#T%w@)gr?WJO`wJV2ImY-g=B}0PNEbOR6H+pIufNw&$f<|ewMJK(7{(k zgibd`_HYIpBU7j?0l^upo*gfrA>Lxq<{jc-A?lRxE4I z3XWNWDT_$U>G4gq>F-(4mpR8XfaxdGyzW)X~WY6E>*CVixz>+s$h*X zTpG}FMOrY<37%iWma&lq_+$lKEVzw;u_1U& z6WFRFCOiq~TLtf!z+3|?*M|N@aD5{jqlK1(9s=-0XcDNJDN5#fG@3XPlMEYnE+At zHt)!SXRb;bBe^m(OD;E+nlNeQ-OE(RuVgqaQC!s5^}KSouKsZ~xu*L(F9ij*8B`a9xa+_@3t54R9lO3tbV?H}Ww3h!_k%3Yh}uJGP^Fo$^XhCKqu1P@v` zUL2wA2&=>go&lsClt-4sswQA1sKc5tf=iv;C-6!)h8{z>7DKHA>qG-?JPNP;k!u9W zh>D5Bp8TQpfp-S@9um-?o`7`Sd``Zy`oll&whF^yUH_El#XglbTbp?*$D1pXR%xA? z2cwJ}y}UfU-3Yd^DZ%9Py*!*9{XBTygsTs417XPH`4QH9+yO($eTUNgcfSPiXv}GJ zQo;;=lu~%#A3YY97`+t!N*TSB_;TJ#|f*P3YYu zsl{et|b%^Fd9qX!kW_G6T6>b7O&P;Tt&3RBIM?b`!RdqBGmYV6P#8gs=>aT zV|lf{??m~%b+NDYemmH~YU?;@I9X?ME4TwkhYiyfHcc9wlL)15)F2avL8KF`p6OWg zZ2W+-TlkyTanVX?vz|qs!Tqt5rvI=Fdlnag@UA{5W;*!AmsuR!P$O<&ndVt~leYO* zPDI2tL!~;lReUI4zFsh+68{f_%fJ!pq!(ff3}K!5k~=g3CnmM}2-x==VFmg@KfdwW z{8X9et8#wks~b{h%u(tUy(>$HXApxK#z3X?`>F&~MnM3F&E!77MH|q6gD=~1rNw?i zp=D@DuWGl*l;ZTnZg~YlplgWGLs>|axQSe`wrfLT>ciH z^wjDHZADtFh%)zr({ti06)e7#kT(CptT5a#L(VnB1YM?Z2lQVJA@EPl|8W1uw5~3h z^mCkPzR9jfq7qv!AOi|_K%a`W(fj|%_&4y68vmOe=0_nA%l6chb?Fj+K>(Xt&#;FG zSH;7?yp;1P`!`PYn|wdJTiJbMGAHCPGfR4fbbZ9B*I&Emfe+*aKarXI;t<(Ek9>g} z(ZXNz##hT8$R=`I6GBE!Wm1&Tw#Q?-Q_Z-`p$_r5PlqRxw_i{8mX# zB`rj`gDaOZjUlvfG?@iPe|I+VS>4W}ZPv%U^smcLlA;HT-WWHA#n10wZ!D35w#+-c z07lz#{rr8r@##dKMxQ#Oh64PPhaV2Le|8HV>WW`~lsR@IAyJ!_cq`y6qo>KCcb9Th zJiX{IkvM0c%jbhgSL6kft`O{=D4cw%`jjg^lqd~FzwPAberwLf^arPs0(iDD`YIW2 ziM7$z{eN+S%K7E1UPJ+qTvHDD$!uUU1e{FKaL@2iA52nknB&>g7mA%P?IK&Do94&} zyU2=0qtoZW(P9T5LT>Pxz=G6u18m|Juywl)`~3R<#9nb5D}L%s-sV=`e9oK0C%gVi z3=3>8^byR|zn}4A$*dl7?hUB9FZ=A~o^7~7?BLy8fu6g1Gpnt=qBXLEkgwH9rt^5V zQZb&7NcQSN_NMRq8+`_m6WgPgoSD6f%iQWvwf>I^uZpsXU2j&ZBuS@FPw^f2DS(Rm zY%1{QS^bl^mm>T03`hPRRT0n=C%J$l2ZT*k3zvZo;8X+x=BqN+RFoiyf*k Z3Uxc}%4JljeszIlpGQT``pbIpsYM;7r7Mol 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