[#25] Make ACA exception handling more descriptive

This commit is contained in:
apldev3 2018-10-11 22:03:25 -04:00
parent 6847c814af
commit 87be5a396b
16 changed files with 322 additions and 61 deletions

View File

@ -3,6 +3,9 @@ package hirs.attestationca;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import hirs.attestationca.exceptions.CertificateProcessingException;
import hirs.attestationca.exceptions.IdentityProcessingException;
import hirs.attestationca.exceptions.UnexpectedServerException;
import hirs.data.persist.AppraisalStatus;
import hirs.data.persist.BIOSComponentInfo;
import hirs.data.persist.BaseboardComponentInfo;
@ -238,7 +241,8 @@ public abstract class AbstractAttestationCertificateAuthority
if (publicKeyModulus != null) {
ekPublicKey = assemblePublicKey(publicKeyModulus.toByteArray());
} else {
throw new IllegalArgumentException("TPM 1.2 Provisioning requires RSA EKC");
throw new IdentityProcessingException("TPM 1.2 Provisioning requires EK "
+ "Credentials to be created with RSA");
}
} catch (IOException e) {
LOG.error("Could not retrieve the public key modulus from the EK cert");
@ -277,7 +281,7 @@ public abstract class AbstractAttestationCertificateAuthority
if (deviceInfoReport == null) {
LOG.error("Failed to deserialize Device Info Report");
throw new IllegalArgumentException("Device Info Report failed to deserialize "
throw new IdentityProcessingException("Device Info Report failed to deserialize "
+ "from Identity Request");
}
@ -382,7 +386,8 @@ public abstract class AbstractAttestationCertificateAuthority
LOG.info("Got identity claim");
if (ArrayUtils.isEmpty(identityClaim)) {
throw new IllegalArgumentException("identityClaim cannot be null or empty");
LOG.error("Identity claim empty throwing exception.");
throw new IdentityProcessingException("identityClaim cannot be null or empty");
}
// attempt to deserialize Protobuf IdentityClaim
@ -466,7 +471,7 @@ public abstract class AbstractAttestationCertificateAuthority
try {
request = ProvisionerTpm2.CertificateRequest.parseFrom(certificateRequest);
} catch (InvalidProtocolBufferException ipbe) {
throw new IdentityProcessingException(
throw new CertificateProcessingException(
"Could not deserialize certificate request", ipbe);
}
@ -512,7 +517,7 @@ public abstract class AbstractAttestationCertificateAuthority
} else {
LOG.error("Could not process credential request. Invalid nonce provided: "
+ request.getNonce().toString());
throw new IdentityProcessingException("Invalid nonce given in request");
throw new CertificateProcessingException("Invalid nonce given in request");
}
}
@ -524,7 +529,7 @@ public abstract class AbstractAttestationCertificateAuthority
RSAPublicKey parsePublicKey(final byte[] publicArea) {
int pubLen = publicArea.length;
if (pubLen < RSA_MODULUS_LENGTH) {
throw new IdentityProcessingException(
throw new IllegalArgumentException(
"EK or AK public data segment is not long enough");
}
// public data ends with 256 byte modulus
@ -661,7 +666,7 @@ public abstract class AbstractAttestationCertificateAuthority
if (deviceInfoReport == null) {
LOG.error("Failed to deserialize Device Info Report");
throw new IllegalArgumentException("Device Info Report failed to deserialize "
throw new IdentityProcessingException("Device Info Report failed to deserialize "
+ "from Identity Claim");
}
@ -882,7 +887,7 @@ public abstract class AbstractAttestationCertificateAuthority
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePublic(keySpec);
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
throw new IdentityProcessingException(
throw new UnexpectedServerException(
"Encountered unexpected error creating public key: " + e.getMessage(), e);
}
}
@ -948,7 +953,7 @@ public abstract class AbstractAttestationCertificateAuthority
} catch (NoSuchAlgorithmException | IllegalBlockSizeException | NoSuchPaddingException
| InvalidKeyException | BadPaddingException
| InvalidAlgorithmParameterException e) {
throw new IdentityProcessingException(
throw new CertificateProcessingException(
"Encountered error while generating ACA session key: " + e.getMessage(), e);
}
}
@ -1004,7 +1009,7 @@ public abstract class AbstractAttestationCertificateAuthority
} catch (BadPaddingException | IllegalBlockSizeException | NoSuchAlgorithmException
| InvalidKeyException | InvalidAlgorithmParameterException | NoSuchPaddingException
| CertificateEncodingException e) {
throw new IdentityProcessingException(
throw new CertificateProcessingException(
"Encountered error while generating Identity Response: " + e.getMessage(), e);
}
}
@ -1066,7 +1071,7 @@ public abstract class AbstractAttestationCertificateAuthority
.setProvider("BC").getCertificate(holder);
return certificate;
} catch (IOException | OperatorCreationException | CertificateException e) {
throw new IdentityProcessingException("Encountered error while generating "
throw new CertificateProcessingException("Encountered error while generating "
+ "identity credential: " + e.getMessage(), e);
}
}
@ -1159,7 +1164,8 @@ public abstract class AbstractAttestationCertificateAuthority
| InvalidKeyException | InvalidAlgorithmParameterException
| NoSuchPaddingException e) {
throw new IdentityProcessingException(
"Encountered error while making credential: " + e.getMessage(), e);
"Encountered error while making the identity claim challenge: "
+ e.getMessage(), e);
}
}
@ -1418,7 +1424,7 @@ public abstract class AbstractAttestationCertificateAuthority
* Helper method to extract a DER encoded ASN.1 certificate from an X509 certificate.
*
* @param certificate the X509 certificate to be converted to DER encoding
* @throws {@link IdentityProcessingException} if error occurs during encoding retrieval
* @throws {@link UnexpectedServerException} if error occurs during encoding retrieval
* @return the byte array representing the DER encoded certificate
*/
private byte[] getDerEncodedCertificate(final X509Certificate certificate) {
@ -1426,7 +1432,7 @@ public abstract class AbstractAttestationCertificateAuthority
return certificate.getEncoded();
} catch (CertificateEncodingException e) {
LOG.error("Error converting certificate to ASN.1 DER Encoding.", e);
throw new IdentityProcessingException(
throw new UnexpectedServerException(
"Encountered error while converting X509 Certificate: "
+ e.getMessage(), e);
}
@ -1441,7 +1447,7 @@ public abstract class AbstractAttestationCertificateAuthority
* @param endorsementCredential the endorsement credential used to generate the AC
* @param platformCredentials the platform credentials used to generate the AC
* @param device the device to which the attestation certificate is tied
* @throws {@link IdentityProcessingException} if error occurs in persisting the Attestation
* @throws {@link CertificateProcessingException} if error occurs in persisting the Attestation
* Certificate
*/
private void saveAttestationCertificate(final byte[] derEncodedAttestationCertificate,
@ -1456,7 +1462,7 @@ public abstract class AbstractAttestationCertificateAuthority
certificateManager.save(attCert);
} catch (Exception e) {
LOG.error("Error saving generated Attestation Certificate to database.", e);
throw new IdentityProcessingException(
throw new CertificateProcessingException(
"Encountered error while storing Attestation Certificate: "
+ e.getMessage(), e);
}

View File

@ -0,0 +1,45 @@
package hirs.attestationca;
/**
* A simple POJO that will provide a clean error message to clients making
* REST requests to the ACA. It is to be serialized to JSON for the return message.
*/
public class AcaRestError {
private String error;
/**
* Basic constructor necessary for Jackson JSON serialization to work properly.
*/
public AcaRestError() {
// Don't remove this constructor as it's required for JSON mapping
}
/**
* Parameterized constructor for creating this class normally.
*
* @param error the error message to store in this object
*/
public AcaRestError(final String error) {
this.error = error;
}
/**
* Simple getter to get the error message stored in this object.
*
* @return the error message
*/
public String getError() {
return error;
}
/**
* Simple setter to get the error message stored in this object.
*
* @param error the new error message to store in this object
*/
public void setError(final String error) {
this.error = error;
}
}

View File

@ -54,8 +54,8 @@ import hirs.utils.LogConfigurationUtil;
@PropertySource(value = "file:/etc/hirs/aca/aca.properties",
ignoreResourceNotFound = true)
})
@ComponentScan({ "hirs.attestationca", "hirs.attestationca.service", "hirs.validation",
"hirs.data.service" })
@ComponentScan({ "hirs.attestationca", "hirs.attestationca.service", "hirs.attestationca.rest",
"hirs.validation", "hirs.data.service" })
@Import(HibernateConfiguration.class)
@EnableWebMvc
public class AttestationCertificateAuthorityConfiguration extends WebMvcConfigurerAdapter {

View File

@ -0,0 +1,27 @@
package hirs.attestationca.exceptions;
/**
* Generic exception thrown while a {@link hirs.attestationca.AttestationCertificateAuthority}
* is processing a newly created Attestation Certificate for a validated identity.
*/
public class CertificateProcessingException extends RuntimeException {
/**
* Constructs a generic instance of this exception using the specified reason.
*
* @param reason for the exception
*/
public CertificateProcessingException(final String reason) {
super(reason);
}
/**
* Constructs a instance of this exception with the specified reason and backing root
* exception.
*
* @param reason for this exception
* @param rootException causing this exception
*/
public CertificateProcessingException(final String reason, final Throwable rootException) {
super(reason, rootException);
}
}

View File

@ -1,8 +1,8 @@
package hirs.attestationca;
package hirs.attestationca.exceptions;
/**
* Generic exception thrown while a {@link AttestationCertificateAuthority} is processing a newly
* submitted Identity.
* Generic exception thrown while a {@link hirs.attestationca.AttestationCertificateAuthority}
* is processing a newly submitted Identity.
*/
public class IdentityProcessingException extends RuntimeException {
/**

View File

@ -0,0 +1,27 @@
package hirs.attestationca.exceptions;
/**
* Generic exception thrown when a {@link hirs.attestationca.AttestationCertificateAuthority}
* encounters an unexpected condition that can't be handled.
*/
public class UnexpectedServerException extends RuntimeException {
/**
* Constructs a generic instance of this exception using the specified reason.
*
* @param reason for the exception
*/
public UnexpectedServerException(final String reason) {
super(reason);
}
/**
* Constructs a instance of this exception with the specified reason and backing root
* exception.
*
* @param reason for this exception
* @param rootException causing this exception
*/
public UnexpectedServerException(final String reason, final Throwable rootException) {
super(reason, rootException);
}
}

View File

@ -0,0 +1,4 @@
/**
* Custom exceptions of the {@link hirs.attestationca.AttestationCertificateAuthority}.
*/
package hirs.attestationca.exceptions;

View File

@ -0,0 +1,72 @@
package hirs.attestationca.rest;
import hirs.attestationca.AcaRestError;
import hirs.attestationca.exceptions.CertificateProcessingException;
import hirs.attestationca.exceptions.IdentityProcessingException;
import hirs.attestationca.exceptions.UnexpectedServerException;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
/**
* Handle processing of exceptions for ACA REST API.
*/
@ControllerAdvice
public class AttestationCertificateAuthorityExceptionHandler
extends ResponseEntityExceptionHandler {
private static final Logger LOGGER = LogManager.getLogger(
AttestationCertificateAuthorityExceptionHandler.class);
/**
* Method to handle errors of the type {@link CertificateProcessingException},
* {@link IdentityProcessingException}, and {@link IllegalArgumentException}
* that are thrown when performing a RESTful operation.
*
* @param ex exception that was thrown
* @param request the web request that started the RESTful operation
* @return the response entity that will form the message returned to the client
*/
@ExceptionHandler({ CertificateProcessingException.class, IdentityProcessingException.class,
IllegalArgumentException.class })
public final ResponseEntity<Object> handleExpectedExceptions(final Exception ex,
final WebRequest request) {
LOGGER.error(String.format("The ACA has encountered an expected exception: %s",
ex.getMessage()), ex);
return handleGeneralException(ex, HttpStatus.BAD_REQUEST, request);
}
/**
* Method to handle errors of the type {@link IllegalStateException} and
* {@link UnexpectedServerException} that are thrown when performing a RESTful operation.
*
* @param ex exception that was thrown
* @param request the web request that started the RESTful operation
* @return the response entity that will form the message returned to the client
*/
@ExceptionHandler({ IllegalStateException.class, UnexpectedServerException.class })
public final ResponseEntity<Object> handleUnexpectedExceptions(final Exception ex,
final WebRequest request) {
LOGGER.error(String.format("The ACA has encountered an unexpected exception: %s",
ex.getMessage()), ex);
return handleGeneralException(ex, HttpStatus.INTERNAL_SERVER_ERROR, request);
}
private ResponseEntity<Object> handleGeneralException(final Exception ex,
final HttpStatus responseStatus,
final WebRequest request) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return handleExceptionInternal(ex, new AcaRestError(ex.getMessage()),
headers, responseStatus, request);
}
}

View File

@ -1,18 +1,14 @@
package hirs.attestationca.rest;
import hirs.attestationca.IdentityProcessingException;
import hirs.persist.DBManager;
import hirs.persist.TPM2ProvisionerState;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import java.security.PrivateKey;
@ -72,8 +68,7 @@ public class RestfulAttestationCertificateAuthority
@Override
@ResponseBody
@RequestMapping(value = "/identity-request/process", method = RequestMethod.POST,
consumes = MediaType.APPLICATION_OCTET_STREAM_VALUE,
produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
consumes = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public byte[] processIdentityRequest(@RequestBody final byte[] request) {
return super.processIdentityRequest(request);
}
@ -87,8 +82,7 @@ public class RestfulAttestationCertificateAuthority
@ResponseBody
@RequestMapping(value = "/identity-claim-tpm2/process",
method = RequestMethod.POST,
consumes = MediaType.APPLICATION_OCTET_STREAM_VALUE,
produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
consumes = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public byte[] processIdentityClaimTpm2(@RequestBody final byte[] request) {
return super.processIdentityClaimTpm2(request);
}
@ -103,8 +97,7 @@ public class RestfulAttestationCertificateAuthority
@ResponseBody
@RequestMapping(value = "/request-certificate-tpm2",
method = RequestMethod.POST,
consumes = MediaType.APPLICATION_OCTET_STREAM_VALUE,
produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
consumes = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public byte[] processCertificateRequest(@RequestBody final byte[] request) {
return super.processCertificateRequest(request);
}
@ -118,28 +111,9 @@ public class RestfulAttestationCertificateAuthority
*/
@Override
@ResponseBody
@RequestMapping(value = "/public-key", method = RequestMethod.GET,
produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
@RequestMapping(value = "/public-key", method = RequestMethod.GET)
public byte[] getPublicKey() {
return super.getPublicKey();
}
/**
* Handle processing of exceptions for ACA REST API.
* @param e exception thrown during invocation of ACA REST API
* @return exception thrown during invocation of ACA REST API
*/
@ExceptionHandler
@ResponseBody
@ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR)
public Exception handleException(final Exception e) {
if (e instanceof IdentityProcessingException) {
LOG.error("Processing exception while provisioning", e.getMessage(), e);
} else {
LOG.error(String.format("Encountered unexpected error while processing identity "
+ "claim: %s", e.getMessage()), e);
}
return e;
}
}

View File

@ -324,7 +324,7 @@ public class SupplyChainValidationServiceImpl implements SupplyChainValidationSe
*
* @param credential the credential whose CA chain should be retrieved
* @return A keystore ontaining all relevant CA credentials to the given certificate's
* organization
* organization or null if the keystore can't be assembled
*/
public KeyStore getCaChain(final Certificate credential) {
KeyStore caKeyStore = null;

View File

@ -1,6 +1,7 @@
package hirs.attestationca;
import com.google.protobuf.ByteString;
import hirs.attestationca.exceptions.IdentityProcessingException;
import hirs.utils.HexUtils;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.lang3.ArrayUtils;
@ -158,9 +159,9 @@ public class AbstractAttestationCertificateAuthorityTest {
/**
* Tests {@link AbstractAttestationCertificateAuthority#processIdentityClaimTpm2(byte[])}
* where the byte array is null. Expects an illegal argument exception to be thrown.
* where the byte array is null. Expects an identity processing exception to be thrown.
*/
@Test(expectedExceptions = IllegalArgumentException.class)
@Test(expectedExceptions = IdentityProcessingException.class)
public void testProcessIdentityClaimTpm2NullRequest() {
aca.processIdentityClaimTpm2(null);
}

View File

@ -20,6 +20,7 @@ class RestfulClientProvisioner {
static const char * const PROP_FILE_LOC;
static const char * const PROP_ACA_FQDN;
static const char * const PROP_ACA_PORT;
static const char * const ACA_ERROR_FIELDNAME;
/**

View File

@ -47,6 +47,27 @@ namespace file_utils {
int readSize);
} // namespace file_utils
namespace json_utils {
/**
* Utility class that provides functions to parse information from ACA
* output.
*/
class JSONFieldParser {
public:
/**
* Parses the target field of the provided JSON object as a string.
*
* @param jsonObject the JSON-formatted object
* @param jsonFieldName the name of the field to parse from the JSON object
* @return the value of the target field in the JSON object
*/
static std::string parseJsonStringField(const std::string& jsonObject,
const std::string& jsonFieldName);
};
} // namespace json_utils
namespace string_utils {
/**
* Converts a binary string to a hex string.

View File

@ -19,6 +19,7 @@ using hirs::pb::IdentityClaimResponse;
using hirs::pb::CertificateRequest;
using hirs::pb::CertificateResponse;
using hirs::properties::Properties;
using hirs::json_utils::JSONFieldParser;
using hirs::string_utils::binaryToHex;
using std::string;
using std::stringstream;
@ -31,6 +32,8 @@ const char * const RestfulClientProvisioner::PROP_ACA_FQDN
= "ATTESTATION_CA_FQDN";
const char * const RestfulClientProvisioner::PROP_ACA_PORT
= "ATTESTATION_CA_PORT";
const char * const RestfulClientProvisioner::ACA_ERROR_FIELDNAME
= "error";
RestfulClientProvisioner::RestfulClientProvisioner() {
Properties props(PROP_FILE_LOC);
@ -66,7 +69,9 @@ string RestfulClientProvisioner::sendIdentityClaim(
+ "process"},
cpr::Body{identityClaimByteString},
cpr::Header{{"Content-Type",
"application/octet-stream"}},
"application/octet-stream"},
{"Accept",
"application/octet-stream, application/json"}},
cpr::VerifySsl{false});
// Check ACA response, should be 200 if successful
@ -91,8 +96,11 @@ string RestfulClientProvisioner::sendIdentityClaim(
} else {
stringstream errormsg;
errormsg << "Couldn't communicate with ACA server. "
<< "Received response code: " << to_string(r.status_code);
errormsg << "Error communicating with ACA server. "
<< "Received response code: " << to_string(r.status_code)
<< "\n\nError message fom ACA was: "
<< JSONFieldParser::parseJsonStringField(r.text,
ACA_ERROR_FIELDNAME);
throw HirsRuntimeException(errormsg.str(),
"RestfulClientProvisioner::sendIdentityClaim");
}
@ -110,7 +118,9 @@ string RestfulClientProvisioner::sendAttestationCertificateRequest(
+ "/request-certificate-tpm2"},
cpr::Body{certificateRequestByteString},
cpr::Header{{"Content-Type",
"application/octet-stream"}},
"application/octet-stream"},
{"Accept",
"application/octet-stream, application/json"}},
cpr::VerifySsl{false});
// Check ACA response, should be 200 if successful
@ -131,9 +141,11 @@ string RestfulClientProvisioner::sendAttestationCertificateRequest(
} else {
stringstream errormsg;
errormsg << "Couldn't communicate with ACA server. "
errormsg << "Error communicating with ACA server. "
<< "Received response code: " << to_string(r.status_code)
<< "\n\nWith message body: " << r.text;
<< "\n\nError message from ACA was: "
<< JSONFieldParser::parseJsonStringField(r.text,
ACA_ERROR_FIELDNAME);
throw HirsRuntimeException(errormsg.str(),
"RestfulClientProvisioner::sendAttestationCertificateRequest");
}

View File

@ -34,6 +34,25 @@ using hirs::exception::HirsRuntimeException;
namespace hirs {
namespace json_utils {
string JSONFieldParser::parseJsonStringField(const std::string &jsonObject,
const std::string &jsonFieldName) {
stringstream regexPatternStream;
regexPatternStream << "(?i)\\\""
<< jsonFieldName
<< "\\\"\\s*:\\s*\\\"(.*)\\\"";
string value;
if (RE2::PartialMatch(jsonObject, regexPatternStream.str(), &value)) {
return value;
} else {
return "";
}
}
} // namespace json_utils
namespace file_utils {
/**

View File

@ -12,6 +12,7 @@
using hirs::file_utils::dirExists;
using hirs::file_utils::fileExists;
using hirs::json_utils::JSONFieldParser;
using hirs::string_utils::binaryToHex;
using hirs::string_utils::contains;
using hirs::string_utils::longToHex;
@ -58,6 +59,57 @@ class UtilsTest : public :: testing::Test {
const char UtilsTest::kFileName[] = "bitsAndBytes";
TEST_F(UtilsTest, ParseJsonFieldSuccess) {
stringstream jsonObject;
jsonObject << R"({"error":"identityClaim cannot be null or empty"})";
string errorMessage = JSONFieldParser::parseJsonStringField(
jsonObject.str(), "error");
string expectedOutput = "identityClaim cannot be null or empty";
ASSERT_EQ(expectedOutput, errorMessage);
}
TEST_F(UtilsTest, ParseJsonFieldSuccessCaseInsensitive) {
stringstream jsonObject;
jsonObject << R"({"ERROR":"identityClaim cannot be null or empty"})";
string errorMessage = JSONFieldParser::parseJsonStringField(
jsonObject.str(), "error");
string expectedOutput = "identityClaim cannot be null or empty";
ASSERT_EQ(expectedOutput, errorMessage);
}
TEST_F(UtilsTest, ParseJsonFieldSuccessWhiteSpaces) {
stringstream jsonObject;
jsonObject << R"({"error" : "identityClaim cannot be null or empty"})";
string errorMessage = JSONFieldParser::parseJsonStringField(
jsonObject.str(), "error");
string expectedOutput = "identityClaim cannot be null or empty";
ASSERT_EQ(expectedOutput, errorMessage);
}
TEST_F(UtilsTest, ParseJsonFieldSuccessMultiJsonFields) {
stringstream jsonObject;
jsonObject << R"({"error" : "identityClaim cannot be null or empty",)"
<< "\n" << R"("endpoint":"url.com"})";
string errorMessage = JSONFieldParser::parseJsonStringField(
jsonObject.str(), "error");
string expectedOutput = "identityClaim cannot be null or empty";
ASSERT_EQ(expectedOutput, errorMessage);
}
TEST_F(UtilsTest, ParseJsonFieldInvalidJson) {
stringstream jsonObject;
jsonObject << R"({error:"identityClaim cannot be null or empty"})";
string errorMessage = JSONFieldParser::parseJsonStringField(
jsonObject.str(), "error");
string expectedOutput = "";
ASSERT_EQ(expectedOutput, errorMessage);
}
TEST_F(UtilsTest, DirectoryExists) {
mkdir(kFileName, 0755);
ASSERT_TRUE(dirExists(kFileName));