mirror of
https://github.com/nsacyber/HIRS.git
synced 2025-04-11 13:20:23 +00:00
* This is an update to the display of the Reference Integrity Manifest code base that'll allow a user to upload a swidtag. This code includes some additions from #217, slightly modified. * This code update include changes to import, archive and delete a swidtag into the RIM object. * Updated the code with additional checks on the uploaded file locations. Added the number associated with the PCR value to the detail page. * This change fixes the bug that caused the rim detail page to go blank if the associated event log file associated with the resource file doesn't exist. Co-authored-by: lareine <lareine@tycho.ncsc.mil>
This commit is contained in:
parent
5dbbbafafe
commit
21db725815
HIRS_AttestationCAPortal/src/main
java/hirs/attestationca/portal/page/controllers
webapp/WEB-INF/jsp
HIRS_Utils/src/main/java/hirs
@ -1,14 +1,22 @@
|
||||
|
||||
package hirs.attestationca.portal.page.controllers;
|
||||
|
||||
import hirs.data.persist.ReferenceManifest;
|
||||
import hirs.data.persist.SwidResource;
|
||||
import hirs.persist.ReferenceManifestManager;
|
||||
import hirs.tpm.eventlog.TCGEventLogProcessor;
|
||||
import hirs.attestationca.portal.page.Page;
|
||||
import hirs.attestationca.portal.page.PageController;
|
||||
import hirs.attestationca.portal.page.PageMessages;
|
||||
import hirs.attestationca.portal.page.params.ReferenceManifestDetailsPageParams;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
@ -24,14 +32,15 @@ import org.springframework.web.servlet.ModelAndView;
|
||||
@Controller
|
||||
@RequestMapping("/rim-details")
|
||||
public class ReferenceManifestDetailsPageController
|
||||
extends PageController<ReferenceManifestDetailsPageParams> {
|
||||
extends PageController<ReferenceManifestDetailsPageParams> {
|
||||
|
||||
private final ReferenceManifestManager referenceManifestManager;
|
||||
private static final Logger LOGGER =
|
||||
LogManager.getLogger(ReferenceManifestDetailsPageController.class);
|
||||
private static final Logger LOGGER
|
||||
= LogManager.getLogger(ReferenceManifestDetailsPageController.class);
|
||||
|
||||
/**
|
||||
* Constructor providing the Page's display and routing specification.
|
||||
*
|
||||
* @param referenceManifestManager the reference manifest manager
|
||||
*/
|
||||
@Autowired
|
||||
@ -92,12 +101,14 @@ extends PageController<ReferenceManifestDetailsPageParams> {
|
||||
/**
|
||||
* This method takes the place of an entire class for a string builder.
|
||||
* Gathers all information and returns it for displays.
|
||||
*
|
||||
* @param uuid database reference for the requested RIM.
|
||||
* @param referenceManifestManager the reference manifest manager.
|
||||
* @return mapping of the RIM information from the database.
|
||||
* @throws java.io.IOException error for reading file bytes.
|
||||
*/
|
||||
public static HashMap<String, Object> getRimDetailInfo(final UUID uuid,
|
||||
final ReferenceManifestManager referenceManifestManager) {
|
||||
final ReferenceManifestManager referenceManifestManager) throws IOException {
|
||||
HashMap<String, Object> data = new HashMap<>();
|
||||
|
||||
ReferenceManifest rim = ReferenceManifest
|
||||
@ -140,7 +151,33 @@ extends PageController<ReferenceManifestDetailsPageParams> {
|
||||
|
||||
// checkout later
|
||||
data.put("rimType", rim.getRimType());
|
||||
data.put("swidFiles", rim.parseResource());
|
||||
List<SwidResource> resources = rim.parseResource();
|
||||
String resourceFilename = null;
|
||||
TCGEventLogProcessor logProcessor = new TCGEventLogProcessor();
|
||||
|
||||
try {
|
||||
for (SwidResource swidRes : resources) {
|
||||
resourceFilename = swidRes.getName();
|
||||
Path logPath = Paths.get(String.format("%s/%s",
|
||||
SwidResource.RESOURCE_UPLOAD_FOLDER,
|
||||
resourceFilename));
|
||||
if (Files.exists(logPath)) {
|
||||
logProcessor = new TCGEventLogProcessor(
|
||||
Files.readAllBytes(logPath));
|
||||
swidRes.setPcrValues(Arrays.asList(
|
||||
logProcessor.getExpectedPCRValues()));
|
||||
} else {
|
||||
swidRes.setPcrValues(Arrays.asList(
|
||||
logProcessor.getExpectedPCRValues()));
|
||||
}
|
||||
}
|
||||
} catch (NoSuchFileException nsfEx) {
|
||||
LOGGER.error(String.format("File Not found!: %s",
|
||||
resourceFilename));
|
||||
LOGGER.error(nsfEx);
|
||||
}
|
||||
|
||||
data.put("swidFiles", resources);
|
||||
} else {
|
||||
LOGGER.error(String.format("Unable to find Reference Integrity "
|
||||
+ "Manifest with ID: %s", uuid));
|
||||
|
@ -14,6 +14,7 @@ import hirs.persist.DBManagerException;
|
||||
import hirs.persist.ReferenceManifestManager;
|
||||
import hirs.persist.CriteriaModifier;
|
||||
import hirs.data.persist.ReferenceManifest;
|
||||
import hirs.data.persist.SwidResource;
|
||||
import hirs.data.persist.certificate.Certificate;
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
@ -21,9 +22,14 @@ import java.net.URISyntaxException;
|
||||
import java.text.DateFormat;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
@ -44,36 +50,36 @@ import org.springframework.web.servlet.ModelAndView;
|
||||
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||
import org.springframework.web.servlet.view.RedirectView;
|
||||
|
||||
|
||||
/**
|
||||
* Controller for the Reference Manifest page.
|
||||
*/
|
||||
@Controller
|
||||
@RequestMapping("/reference-manifests")
|
||||
public class ReferenceManifestPageController
|
||||
extends PageController<NoPageParams> {
|
||||
extends PageController<NoPageParams> {
|
||||
|
||||
private static final String BIOS_RELEASE_DATE_FORMAT = "yyyy-MM-dd";
|
||||
|
||||
private final BiosDateValidator biosValidator;
|
||||
private final ReferenceManifestManager referenceManifestManager;
|
||||
private static final Logger LOGGER =
|
||||
LogManager.getLogger(ReferenceManifestPageController.class);
|
||||
private static final Logger LOGGER
|
||||
= LogManager.getLogger(ReferenceManifestPageController.class);
|
||||
|
||||
/**
|
||||
* This class was created for the purposes of avoiding findbugs message:
|
||||
* As the JavaDoc states, DateFormats are inherently unsafe for
|
||||
* multi-threaded use. The detector has found a call to an instance
|
||||
* of DateFormat that has been obtained via a static field.
|
||||
* This looks suspicious.
|
||||
* This class was created for the purposes of avoiding findbugs message: As
|
||||
* the JavaDoc states, DateFormats are inherently unsafe for multi-threaded
|
||||
* use. The detector has found a call to an instance of DateFormat that has
|
||||
* been obtained via a static field. This looks suspicious.
|
||||
*
|
||||
* This class can have uses elsewhere but for now it will remain here.
|
||||
*/
|
||||
private static final class BiosDateValidator {
|
||||
|
||||
private final String dateFormat;
|
||||
|
||||
/**
|
||||
* Default constructor that sets the format to parse against.
|
||||
*
|
||||
* @param dateFormat
|
||||
*/
|
||||
public BiosDateValidator(final String dateFormat) {
|
||||
@ -82,6 +88,7 @@ extends PageController<NoPageParams> {
|
||||
|
||||
/**
|
||||
* Validates a date by attempting to parse based on format provided.
|
||||
*
|
||||
* @param date string of the given date
|
||||
* @return true if the format matches
|
||||
*/
|
||||
@ -102,6 +109,7 @@ extends PageController<NoPageParams> {
|
||||
|
||||
/**
|
||||
* Constructor providing the Page's display and routing specification.
|
||||
*
|
||||
* @param referenceManifestManager the reference manifest manager
|
||||
*/
|
||||
@Autowired
|
||||
@ -113,11 +121,12 @@ extends PageController<NoPageParams> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path for the view and the data model for the page.
|
||||
* Returns the filePath 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.
|
||||
* @param model The data model for the request. Can contain data from
|
||||
* redirect.
|
||||
* @return the filePath for the view and data model for the page.
|
||||
*/
|
||||
@Override
|
||||
public ModelAndView initPage(final NoPageParams params,
|
||||
@ -126,11 +135,12 @@ extends PageController<NoPageParams> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of RIMs using the data table input for paging,
|
||||
* ordering, and filtering.
|
||||
* Returns the list of RIMs using the data table input for paging, ordering,
|
||||
* and filtering.
|
||||
*
|
||||
* @param input the data tables input
|
||||
* @return the data tables response, including the result set
|
||||
* and paging information
|
||||
* @return the data tables response, including the result set and paging
|
||||
* information
|
||||
*/
|
||||
@ResponseBody
|
||||
@RequestMapping(value = "/list",
|
||||
@ -157,7 +167,6 @@ extends PageController<NoPageParams> {
|
||||
referenceManifestManager,
|
||||
input, orderColumnName, criteriaModifier);
|
||||
|
||||
|
||||
LOGGER.debug("Returning list of size: " + records.size());
|
||||
return new DataTableResponse<>(records, input);
|
||||
}
|
||||
@ -177,17 +186,44 @@ extends PageController<NoPageParams> {
|
||||
final RedirectAttributes attr) throws URISyntaxException, Exception {
|
||||
Map<String, Object> model = new HashMap<>();
|
||||
PageMessages messages = new PageMessages();
|
||||
List<MultipartFile> rims = new ArrayList<>();
|
||||
String fileName;
|
||||
Path filePath;
|
||||
|
||||
// loop through the files
|
||||
for (MultipartFile file : files) {
|
||||
fileName = file.getOriginalFilename();
|
||||
if (fileName.toLowerCase().endsWith("swidtag")) {
|
||||
rims.add(file);
|
||||
} else {
|
||||
filePath = Paths.get(String.format("%s/%s",
|
||||
SwidResource.RESOURCE_UPLOAD_FOLDER,
|
||||
file.getOriginalFilename()));
|
||||
if (Files.notExists(Paths.get(SwidResource.RESOURCE_UPLOAD_FOLDER))) {
|
||||
Files.createDirectory(Paths.get(SwidResource.RESOURCE_UPLOAD_FOLDER));
|
||||
}
|
||||
if (Files.notExists(filePath)) {
|
||||
Files.createFile(filePath);
|
||||
}
|
||||
|
||||
Files.write(filePath, file.getBytes());
|
||||
|
||||
String uploadCompletedMessage = String.format(
|
||||
"%s successfully uploaded", file.getOriginalFilename());
|
||||
messages.addSuccess(uploadCompletedMessage);
|
||||
LOGGER.info(uploadCompletedMessage);
|
||||
}
|
||||
}
|
||||
|
||||
for (MultipartFile file : rims) {
|
||||
//Parse reference manifests
|
||||
ReferenceManifest rims = parseRIMs(file, messages);
|
||||
ReferenceManifest rim = parseRIM(file, messages);
|
||||
|
||||
//Store only if it was parsed
|
||||
if (rims != null) {
|
||||
if (rim != null) {
|
||||
storeManifest(file.getOriginalFilename(),
|
||||
messages,
|
||||
rims,
|
||||
rim,
|
||||
referenceManifestManager);
|
||||
}
|
||||
}
|
||||
@ -300,22 +336,23 @@ extends PageController<NoPageParams> {
|
||||
* @throws IllegalArgumentException
|
||||
*/
|
||||
private ReferenceManifest getRimFromDb(final String id) throws IllegalArgumentException {
|
||||
UUID uuid = UUID.fromString(id);
|
||||
UUID uuid = UUID.fromString(id);
|
||||
|
||||
return ReferenceManifest
|
||||
.select(referenceManifestManager)
|
||||
.byEntityId(uuid).getRIM();
|
||||
return ReferenceManifest
|
||||
.select(referenceManifestManager)
|
||||
.byEntityId(uuid).getRIM();
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes the rim files provided and returns a {@link ReferenceManifest}
|
||||
* object.
|
||||
*
|
||||
* @param file the provide user file via browser.
|
||||
* @param messages the object that handles displaying information to the
|
||||
* user.
|
||||
* @return a single or collection of reference manifest files.
|
||||
*/
|
||||
private ReferenceManifest parseRIMs(
|
||||
private ReferenceManifest parseRIM(
|
||||
final MultipartFile file,
|
||||
final PageMessages messages) {
|
||||
|
||||
@ -348,6 +385,7 @@ extends PageController<NoPageParams> {
|
||||
|
||||
/**
|
||||
* Stores the {@link ReferenceManifest} objects.
|
||||
*
|
||||
* @param fileName name of the file given
|
||||
* @param messages message object for user display of statuses
|
||||
* @param referenceManifest the object to store
|
||||
|
@ -107,51 +107,73 @@
|
||||
</div>
|
||||
<div id="directorycollapse" class="panel-collapse collapse in" role="tabpanel" aria-labelledby="headingOne" aria-expanded="true">
|
||||
<div class="panel-body">
|
||||
|
||||
<div class="panel-heading" role="tab" id="headingThree">
|
||||
<h3 class="panel-title">
|
||||
<a role="button" data-toggle="collapse" data-parent="#directorycollapse" class="collapsed"
|
||||
href="#filescollapse" aria-expanded="false" aria-controls="filescollapse">
|
||||
Files
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div id="filescollapse" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingThree" aria-expanded="true">
|
||||
<c:if test="${not empty initialData.swidFiles}">
|
||||
<div id="componentIdentifier" class="row">
|
||||
<c:forEach items="${initialData.swidFiles}" var="resource">
|
||||
<div class="component col col-md-10" style="padding-left: 20px">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<span data-toggle="tooltip" data-placement="top" title="Resource File">${resource.getName()}
|
||||
</span>
|
||||
</div>
|
||||
<div class="component col col-md-10">
|
||||
<span class="fieldHeader">File Size:</span>
|
||||
<span class="fieldValue">${resource.getSize()}</span><br/>
|
||||
<span class="fieldHeader">Hash:</span>
|
||||
<span class="fieldValue" style="overflow-wrap: break-word">${resource.getHashValue()}</span><br/>
|
||||
<c:if test="${not empty resource.getRimFormat()}">
|
||||
<span class="fieldHeader">RIM Format:</span>
|
||||
<span class="fieldValue">${resource.getRimFormat()}</span><br/>
|
||||
</c:if>
|
||||
<c:if test="${not empty resource.getRimType()}">
|
||||
<span class="fieldHeader">RIM Type:</span>
|
||||
<span class="fieldValue">${resource.getRimType()}</span><br/>
|
||||
</c:if>
|
||||
<c:if test="${not empty resource.getRimUriGlobal()}">
|
||||
<span class="fieldHeader">URI Global:</span>
|
||||
<span class="fieldValue">${resource.getRimUriGlobal()}</span><br/>
|
||||
</c:if>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</c:forEach>
|
||||
</div>
|
||||
</c:if>
|
||||
</div>
|
||||
|
||||
<div class="panel-heading" role="tab" id="headingThree">
|
||||
<h3 class="panel-title">
|
||||
<a role="button" data-toggle="collapse" data-parent="#directorycollapse" class="collapsed"
|
||||
href="#filescollapse" aria-expanded="false" aria-controls="filescollapse">
|
||||
Files
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div id="filescollapse" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingThree" aria-expanded="true">
|
||||
<c:if test="${not empty initialData.swidFiles}">
|
||||
<div id="componentIdentifier" class="row">
|
||||
<c:forEach items="${initialData.swidFiles}" var="resource">
|
||||
<div class="component col col-md-10" style="padding-left: 20px">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<span data-toggle="tooltip" data-placement="top" title="Resource File">${resource.getName()}
|
||||
</span>
|
||||
</div>
|
||||
<div class="component col col-md-10">
|
||||
<span class="fieldHeader">File Size:</span>
|
||||
<span class="fieldValue">${resource.getSize()}</span><br/>
|
||||
<span class="fieldHeader">Hash:</span>
|
||||
<span class="fieldValue" style="overflow-wrap: break-word">${resource.getHashValue()}</span><br/>
|
||||
<c:if test="${not empty resource.getRimFormat()}">
|
||||
<span class="fieldHeader">RIM Format:</span>
|
||||
<span class="fieldValue">${resource.getRimFormat()}</span><br/>
|
||||
</c:if>
|
||||
<c:if test="${not empty resource.getRimType()}">
|
||||
<span class="fieldHeader">RIM Type:</span>
|
||||
<span class="fieldValue">${resource.getRimType()}</span><br/>
|
||||
</c:if>
|
||||
<c:if test="${not empty resource.getRimUriGlobal()}">
|
||||
<span class="fieldHeader">URI Global:</span>
|
||||
<span class="fieldValue">${resource.getRimUriGlobal()}</span><br/>
|
||||
</c:if>
|
||||
<c:if test="${not empty resource.getPcrValues()}">
|
||||
<div class="panel-body">
|
||||
<div class="component" role="tab" id="pcrValues">
|
||||
<a role="button" data-toggle="collapse" data-parent="#directorycollapse" class="collapsed"
|
||||
href="#pcrscollapse" aria-expanded="false" aria-controls="pcrscollapse">
|
||||
Expected PCR Values
|
||||
</a>
|
||||
</div>
|
||||
<div id="pcrscollapse" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingThree" aria-expanded="true">
|
||||
<div>
|
||||
<c:forEach items="${resource.getPcrMap()}" var="pcrValue">
|
||||
<div id="componentIdentifier" class="row">
|
||||
<div>
|
||||
<span>${pcrValue.key}</span>
|
||||
<span style="overflow-wrap: break-word">${pcrValue.value}</span>
|
||||
</div>
|
||||
</div>
|
||||
</c:forEach>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</c:if>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</c:forEach>
|
||||
</div>
|
||||
</c:if>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -160,4 +182,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</jsp:body>
|
||||
</my:page>
|
||||
</my:page>
|
||||
|
@ -3,7 +3,11 @@ package hirs.data.persist;
|
||||
import com.google.common.base.Preconditions;
|
||||
import hirs.utils.xjc.File;
|
||||
import java.util.Map;
|
||||
import java.util.List;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Collections;
|
||||
import java.math.BigInteger;
|
||||
import java.text.DecimalFormat;
|
||||
import javax.xml.namespace.QName;
|
||||
|
||||
/**
|
||||
@ -12,9 +16,20 @@ import javax.xml.namespace.QName;
|
||||
*/
|
||||
public class SwidResource {
|
||||
|
||||
private static final String CATALINA_HOME = System.getProperty("catalina.base");
|
||||
private static final String TOMCAT_UPLOAD_DIRECTORY
|
||||
= "/webapps/HIRS_AttestationCAPortal/upload/";
|
||||
|
||||
/**
|
||||
* String holder for location for storing binaries.
|
||||
*/
|
||||
public static final String RESOURCE_UPLOAD_FOLDER
|
||||
= CATALINA_HOME + TOMCAT_UPLOAD_DIRECTORY;
|
||||
|
||||
private String name, size;
|
||||
|
||||
private String rimFormat, rimType, rimUriGlobal, hashValue;
|
||||
private List<String> pcrValues;
|
||||
|
||||
/**
|
||||
* Default constructor.
|
||||
@ -26,6 +41,7 @@ public class SwidResource {
|
||||
rimType = null;
|
||||
rimUriGlobal = null;
|
||||
hashValue = null;
|
||||
pcrValues = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -112,4 +128,40 @@ public class SwidResource {
|
||||
public String getHashValue() {
|
||||
return hashValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for the list of PCR Values.
|
||||
* @return an unmodifiable list
|
||||
*/
|
||||
public List<String> getPcrValues() {
|
||||
return Collections.unmodifiableList(pcrValues);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for the list of associated PCR Values.
|
||||
* @param pcrValues a collection of PCRs
|
||||
*/
|
||||
public void setPcrValues(final List<String> pcrValues) {
|
||||
this.pcrValues = pcrValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for a generated map of the PCR values.
|
||||
* @return mapping of PCR# to the actual value.
|
||||
*/
|
||||
public LinkedHashMap<String, String> getPcrMap() {
|
||||
LinkedHashMap<String, String> innerMap = new LinkedHashMap<>();
|
||||
DecimalFormat df = new DecimalFormat("00");
|
||||
|
||||
if (!this.pcrValues.isEmpty()) {
|
||||
long iterate = 0;
|
||||
String pcrNum;
|
||||
for (String string : this.pcrValues) {
|
||||
pcrNum = df.format(iterate++);
|
||||
innerMap.put(String.format("PCR%s:", pcrNum), string);
|
||||
}
|
||||
}
|
||||
|
||||
return innerMap;
|
||||
}
|
||||
}
|
||||
|
@ -1,138 +1,11 @@
|
||||
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.
|
||||
* 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<TpmPcrEvent> eventList = new ArrayList<>();
|
||||
public class CryptoAgileEventLog {
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
|
@ -1,134 +1,11 @@
|
||||
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.
|
||||
* 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<TpmPcrEvent> eventList = new ArrayList<TpmPcrEvent>();
|
||||
/**
|
||||
* 2 dimensional array holding the PCR values.
|
||||
*/
|
||||
private final byte[][] pcrList1 = new byte[PCR_COUNT][PCR_LENGTH];
|
||||
public class SHA1EventLog {
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +1,173 @@
|
||||
package hirs.tpm.eventlog;
|
||||
|
||||
import hirs.utils.HexUtils;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
/**
|
||||
* 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
|
||||
public class TCGEventLog {
|
||||
|
||||
private static final Logger LOGGER
|
||||
= LogManager.getLogger(TCGEventLog.class);
|
||||
|
||||
/**
|
||||
* Init value for SHA 256 values.
|
||||
*/
|
||||
String[] getExpectedPCRValues();
|
||||
/** Retrieves a single expected PCR values.
|
||||
* @param index the PCR reference
|
||||
* @return a String holding an expected PCR value
|
||||
public static final String INIT_SHA256_LIST = "00000000000000000000000000"
|
||||
+ "00000000000000000000000000000000000000";
|
||||
/**
|
||||
* Init value for SHA 1 values.
|
||||
*/
|
||||
String getExpectedPCRValue(int index);
|
||||
public static final String INIT_SHA1_LIST = "0000000000000000000000000000000000000000";
|
||||
|
||||
/**
|
||||
* PFP defined EV_NO_ACTION identifier.
|
||||
*/
|
||||
public static final int NO_ACTION_EVENT = 0x00000003;
|
||||
/**
|
||||
* String value of SHA1 hash.
|
||||
*/
|
||||
public static final String HASH_STRING = "SHA1";
|
||||
/**
|
||||
* String value of SHA256 hash.
|
||||
*/
|
||||
public static final String HASH256_STRING = "SHA-256";
|
||||
/**
|
||||
* Each PCR bank holds 24 registers.
|
||||
*/
|
||||
public static final int PCR_COUNT = 24;
|
||||
/**
|
||||
* 2 dimensional array holding the PCR values.
|
||||
*/
|
||||
private final byte[][] pcrList;
|
||||
/**
|
||||
* List of parsed events within the log.
|
||||
*/
|
||||
private final ArrayList<TpmPcrEvent> eventList = new ArrayList<>();
|
||||
|
||||
private int pcrLength;
|
||||
private String hashType;
|
||||
private String initValue;
|
||||
|
||||
/**
|
||||
* Default blank object constructor.
|
||||
*/
|
||||
public TCGEventLog() {
|
||||
this.pcrList = new byte[PCR_COUNT][TpmPcrEvent.SHA1_LENGTH];
|
||||
initValue = INIT_SHA1_LIST;
|
||||
pcrLength = TpmPcrEvent.SHA1_LENGTH;
|
||||
initPcrList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Default constructor for just the rawlog that'll set up SHA1 Log.
|
||||
* @param rawlog data for the event log file
|
||||
* @throws IOException IO Stream for the event log
|
||||
*/
|
||||
public TCGEventLog(final byte[] rawlog) throws IOException {
|
||||
this(rawlog, TpmPcrEvent.SHA1_LENGTH, HASH_STRING, INIT_SHA1_LIST);
|
||||
}
|
||||
|
||||
/**
|
||||
* Default constructor for specific log.
|
||||
* @param rawlog data for the event log file
|
||||
* @param pcrLength determined by SHA1 or 256
|
||||
* @param hashType the type of algorithm
|
||||
* @param initValue the default blank value
|
||||
* @throws IOException IO Stream for the event log
|
||||
*/
|
||||
public TCGEventLog(final byte[] rawlog, final int pcrLength,
|
||||
final String hashType, final String initValue) throws IOException {
|
||||
this.pcrLength = pcrLength;
|
||||
this.pcrList = new byte[PCR_COUNT][pcrLength];
|
||||
this.hashType = hashType;
|
||||
this.initValue = initValue;
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* This method puts blank values in the pcrList.
|
||||
*/
|
||||
private void initPcrList() {
|
||||
for (int i = 0; i < PCR_COUNT; i++) { // Initialize the PCRlist1 array
|
||||
System.arraycopy(HexUtils.hexStringToByteArray(
|
||||
initValue),
|
||||
0, pcrList[i], 0, pcrLength);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
initPcrList();
|
||||
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 = extendPCR(pcrList[currentEvent.getPcrIndex()],
|
||||
currentEvent.getEventDigest());
|
||||
System.arraycopy(extendedPCR, 0, pcrList[currentEvent.getPcrIndex()],
|
||||
0, currentEvent.getDigestLength());
|
||||
}
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
LOGGER.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extends a 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[] extendPCR(final byte[] currentValue, final byte[] newEvent)
|
||||
throws NoSuchAlgorithmException {
|
||||
MessageDigest md = MessageDigest.getInstance(hashType);
|
||||
md.update(HexUtils.hexStringToByteArray(HexUtils.byteArrayToHexString(currentValue)
|
||||
+ HexUtils.byteArrayToHexString(newEvent)));
|
||||
return md.digest();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(pcrList[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(pcrList[index]);
|
||||
}
|
||||
}
|
||||
|
@ -20,10 +20,6 @@ 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.
|
||||
*/
|
||||
@ -37,6 +33,13 @@ public class TCGEventLogProcessor {
|
||||
*/
|
||||
private static final int SIG_SIZE = 16;
|
||||
|
||||
/**
|
||||
* Default Constructor.
|
||||
*/
|
||||
public TCGEventLogProcessor() {
|
||||
tcgLog = new TCGEventLog();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
@ -45,9 +48,10 @@ public class TCGEventLogProcessor {
|
||||
*/
|
||||
public TCGEventLogProcessor(final byte[] rawLog) throws IOException {
|
||||
if (isLogCrytoAgile(rawLog)) {
|
||||
tcgLog = new CryptoAgileEventLog(rawLog);
|
||||
tcgLog = new TCGEventLog(rawLog, TpmPcrEvent.SHA256_LENGTH,
|
||||
TCGEventLog.HASH256_STRING, TCGEventLog.INIT_SHA256_LIST);
|
||||
} else {
|
||||
tcgLog = new SHA1EventLog(rawLog);
|
||||
tcgLog = new TCGEventLog(rawLog);
|
||||
algorithm = "SHA";
|
||||
}
|
||||
}
|
||||
@ -80,8 +84,8 @@ public class TCGEventLogProcessor {
|
||||
*/
|
||||
public TpmWhiteListBaseline createTPMBaseline(final String name) {
|
||||
TpmWhiteListBaseline baseline = new TpmWhiteListBaseline(name);
|
||||
TPMMeasurementRecord record = null;
|
||||
String pcrValue = "";
|
||||
TPMMeasurementRecord record;
|
||||
String pcrValue;
|
||||
for (int i = 0; i < TpmPcrEvent.PCR_COUNT; i++) {
|
||||
if (algorithm.compareToIgnoreCase("SHA1") == 0) { // Log Was SHA1 Format
|
||||
pcrValue = tcgLog.getExpectedPCRValue(i);
|
||||
@ -112,15 +116,13 @@ public class TCGEventLogProcessor {
|
||||
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) {
|
||||
if (eventID != TCGEventLog.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);
|
||||
|
||||
return sig.equals("Spec ID Event03");
|
||||
}
|
||||
}
|
||||
|
@ -167,16 +167,6 @@ public class TpmPcrEvent {
|
||||
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.
|
||||
*
|
||||
|
@ -41,7 +41,7 @@ public class TpmPcrEvent1 extends TpmPcrEvent {
|
||||
setEventType(unit32Data);
|
||||
byte[] eventDigest = new byte[SHA1_LENGTH];
|
||||
is.read(eventDigest);
|
||||
setDigest(eventDigest);
|
||||
setEventDigest(eventDigest);
|
||||
is.read(unit32Data);
|
||||
int eventSize = HexUtils.leReverseInt(unit32Data);
|
||||
byte[] eventContent = new byte[eventSize];
|
||||
|
@ -84,7 +84,7 @@ public class TpmPcrEvent2 extends TpmPcrEvent {
|
||||
hashAlg = new TcgTpmtHa(is);
|
||||
hashlist.add(hashAlg);
|
||||
if (hashAlg.getHashName().compareToIgnoreCase("TPM_ALG_SHA256") == 0) {
|
||||
setDigest(hashAlg.getDigest());
|
||||
setEventDigest(hashAlg.getDigest());
|
||||
}
|
||||
}
|
||||
is.read(rawInt);
|
||||
|
Loading…
x
Reference in New Issue
Block a user