use std::io::{Cursor, Read, Seek, SeekFrom};
use anyhow::{anyhow, Result};
use base64::prelude::*;
use byteorder::{LittleEndian, ReadBytesExt};
use chrono::prelude::*;
use lazy_static::lazy_static;
use oid_registry::{OID_PKCS1_RSAENCRYPTION, OID_PKCS1_SHA256WITHRSA};
use rsa::{pkcs1::DecodeRsaPublicKey, pkcs1v15::Pkcs1v15Sign, RsaPublicKey};
use sgx_isa::{AttributesFlags, Report};
use sha2::{digest::Update as _, Digest, Sha256};
use thiserror::Error;
use x509_parser::prelude::*;
use crate::common::{
sgx::{EnclaveIdentity, MrEnclave, MrSigner, VerifiedQuote},
time::{insecure_posix_time, update_insecure_posix_time},
};
#[derive(Error, Debug)]
enum AVRError {
#[error("failed to parse report body")]
MalformedReportBody,
#[error("report body did not contain timestamp")]
MissingTimestamp,
#[error("failed to parse timestamp")]
MalformedTimestamp,
#[error("timestamp differs by more than 1 day")]
TimestampOutOfRange,
#[error("rejecting quote status ({status:?})")]
QuoteStatusInvalid { status: String },
#[error("debug enclaves not allowed")]
DebugEnclave,
#[error("production enclaves not allowed")]
ProductionEnclave,
#[error("AVR did not contain quote status")]
MissingQuoteStatus,
#[error("AVR did not contain quote body")]
MissingQuoteBody,
#[error("failed to parse quote")]
MalformedQuote,
#[error("unable to find exactly 2 certificates")]
ChainNotTwoCertificates,
#[error("malformed certificate PEM")]
MalformedCertificatePEM,
#[error("malformed certificate DER")]
MalformedCertificateDER,
#[error("expired certificate")]
ExpiredCertificate,
#[error("invalid signature")]
InvalidSignature,
#[error("IAS quotes are disabled by policy")]
Disabled,
#[error("blacklisted IAS quote GID")]
BlacklistedGID,
#[error("TCB evaluation data number is invalid")]
TCBEvaluationDataNumberInvalid,
}
pub const QUOTE_CONTEXT_LEN: usize = 8;
pub type QuoteContext = [u8; QUOTE_CONTEXT_LEN];
const IAS_TRUST_ANCHOR_PEM: &str = r#"-----BEGIN CERTIFICATE-----
MIIFSzCCA7OgAwIBAgIJANEHdl0yo7CUMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV
BAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAYBgNV
BAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQDDCdJbnRlbCBTR1ggQXR0ZXN0
YXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwIBcNMTYxMTE0MTUzNzMxWhgPMjA0OTEy
MzEyMzU5NTlaMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwL
U2FudGEgQ2xhcmExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQD
DCdJbnRlbCBTR1ggQXR0ZXN0YXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwggGiMA0G
CSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCfPGR+tXc8u1EtJzLA10Feu1Wg+p7e
LmSRmeaCHbkQ1TF3Nwl3RmpqXkeGzNLd69QUnWovYyVSndEMyYc3sHecGgfinEeh
rgBJSEdsSJ9FpaFdesjsxqzGRa20PYdnnfWcCTvFoulpbFR4VBuXnnVLVzkUvlXT
L/TAnd8nIZk0zZkFJ7P5LtePvykkar7LcSQO85wtcQe0R1Raf/sQ6wYKaKmFgCGe
NpEJUmg4ktal4qgIAxk+QHUxQE42sxViN5mqglB0QJdUot/o9a/V/mMeH8KvOAiQ
byinkNndn+Bgk5sSV5DFgF0DffVqmVMblt5p3jPtImzBIH0QQrXJq39AT8cRwP5H
afuVeLHcDsRp6hol4P+ZFIhu8mmbI1u0hH3W/0C2BuYXB5PC+5izFFh/nP0lc2Lf
6rELO9LZdnOhpL1ExFOq9H/B8tPQ84T3Sgb4nAifDabNt/zu6MmCGo5U8lwEFtGM
RoOaX4AS+909x00lYnmtwsDVWv9vBiJCXRsCAwEAAaOByTCBxjBgBgNVHR8EWTBX
MFWgU6BRhk9odHRwOi8vdHJ1c3RlZHNlcnZpY2VzLmludGVsLmNvbS9jb250ZW50
L0NSTC9TR1gvQXR0ZXN0YXRpb25SZXBvcnRTaWduaW5nQ0EuY3JsMB0GA1UdDgQW
BBR4Q3t2pn680K9+QjfrNXw7hwFRPDAfBgNVHSMEGDAWgBR4Q3t2pn680K9+Qjfr
NXw7hwFRPDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkq
hkiG9w0BAQsFAAOCAYEAeF8tYMXICvQqeXYQITkV2oLJsp6J4JAqJabHWxYJHGir
IEqucRiJSSx+HjIJEUVaj8E0QjEud6Y5lNmXlcjqRXaCPOqK0eGRz6hi+ripMtPZ
sFNaBwLQVV905SDjAzDzNIDnrcnXyB4gcDFCvwDFKKgLRjOB/WAqgscDUoGq5ZVi
zLUzTqiQPmULAQaB9c6Oti6snEFJiCQ67JLyW/E83/frzCmO5Ru6WjU4tmsmy8Ra
Ud4APK0wZTGtfPXU7w+IBdG5Ez0kE1qzxGQaL4gINJ1zMyleDnbuS8UicjJijvqA
152Sq049ESDz+1rRGc2NVEqh1KaGXmtXvqxXcTB+Ljy5Bw2ke0v8iGngFBPqCTVB
3op5KBG3RjbF6RRSzwzuWfL7QErNC8WEy5yDVARzTA5+xmBc388v9Dm21HGfcC8O
DD+gT9sSpssq0ascmvH49MOgjt1yoysLtdCtJW/9FZpoOypaHx0R+mJTLwPXVMrv
DaVzWh5aiEx+idkSGMnX
-----END CERTIFICATE-----"#;
const PEM_CERTIFICATE_LABEL: &str = "CERTIFICATE";
const IAS_TS_FMT: &str = "%FT%T%.6f";
lazy_static! {
static ref IAS_TRUST_ANCHOR: Vec<u8> = {
let pem = match parse_x509_pem(IAS_TRUST_ANCHOR_PEM.as_bytes()) {
Ok((rem, pem)) => {
assert!(rem.is_empty(), "anchor PEM has trailing garbage");
assert!(
pem.label == PEM_CERTIFICATE_LABEL,
"PEM does not contain a certificate: '{:?}'",
pem.label
);
pem
}
err => panic!("failed to decode anchor PEM: {:?}", err),
};
pem.contents.to_vec()
};
}
#[derive(Clone, Debug, Default, PartialEq, Eq, cbor::Encode, cbor::Decode)]
pub struct QuotePolicy {
#[cbor(optional)]
pub disabled: bool,
#[cbor(optional)]
pub allowed_quote_statuses: Vec<i64>, #[cbor(optional)]
pub gid_blacklist: Vec<u32>,
#[cbor(optional)]
pub min_tcb_evaluation_data_number: u32,
}
#[derive(Default, Debug)]
struct QuoteBody {
version: u16,
signature_type: u16,
gid: u32,
isv_svn_qe: u16,
isv_svn_pce: u16,
basename: [u8; 32],
report_body: Report,
}
#[allow(clippy::unused_io_amount)]
impl QuoteBody {
fn decode(quote_body: &[u8]) -> Result<QuoteBody> {
let mut reader = Cursor::new(quote_body);
let mut quote_body: QuoteBody = QuoteBody::default();
quote_body.version = reader.read_u16::<LittleEndian>()?;
quote_body.signature_type = reader.read_u16::<LittleEndian>()?;
quote_body.gid = reader.read_u32::<LittleEndian>()?;
quote_body.isv_svn_qe = reader.read_u16::<LittleEndian>()?;
quote_body.isv_svn_pce = reader.read_u16::<LittleEndian>()?;
reader.seek(SeekFrom::Current(4))?; reader.read_exact(&mut quote_body.basename)?;
let mut report_buf = vec![0; Report::UNPADDED_SIZE];
reader.read(&mut report_buf)?;
quote_body.report_body = match Report::try_copy_from(&report_buf) {
Some(r) => r,
None => return Err(AVRError::MalformedReportBody.into()),
};
Ok(quote_body)
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq, cbor::Encode, cbor::Decode)]
pub struct AVR {
pub body: Vec<u8>,
pub signature: Vec<u8>,
pub certificate_chain: Vec<u8>,
}
#[derive(Debug, Clone)]
pub(crate) struct ParsedAVR {
body: serde_json::Value,
}
impl ParsedAVR {
pub(crate) fn new(avr: &AVR) -> Result<Self> {
let body = match serde_json::from_slice(&avr.body) {
Ok(avr_body) => avr_body,
_ => return Err(AVRError::MalformedReportBody.into()),
};
Ok(Self { body })
}
fn isv_enclave_quote_status(&self) -> Result<String> {
match self.body["isvEnclaveQuoteStatus"].as_str() {
Some(status) => Ok(status.to_string()),
None => Err(AVRError::MissingQuoteStatus.into()),
}
}
fn isv_enclave_quote_body(&self) -> Result<String> {
match self.body["isvEnclaveQuoteBody"].as_str() {
Some(quote_body) => Ok(quote_body.to_string()),
None => Err(AVRError::MissingQuoteBody.into()),
}
}
fn tcb_evaluation_data_number(&self) -> Result<u32> {
match self.body["tcbEvaluationDataNumber"].as_u64() {
None => Ok(0),
Some(eval_num) if eval_num > u32::MAX.into() => {
Err(AVRError::TCBEvaluationDataNumberInvalid.into())
}
Some(eval_num) => Ok(eval_num as u32),
}
}
fn timestamp(&self) -> Result<i64> {
let timestamp = match self.body["timestamp"].as_str() {
Some(timestamp) => timestamp,
None => {
return Err(AVRError::MissingTimestamp.into());
}
};
parse_avr_timestamp(timestamp)
}
}
pub fn verify(avr: &AVR, policy: &QuotePolicy) -> Result<VerifiedQuote> {
if policy.disabled {
return Err(AVRError::Disabled.into());
}
let unsafe_skip_avr_verification = option_env!("OASIS_UNSAFE_SKIP_AVR_VERIFY").is_some();
let unsafe_lax_avr_verification = option_env!("OASIS_UNSAFE_LAX_AVR_VERIFY").is_some();
let timestamp_now = insecure_posix_time();
if !unsafe_skip_avr_verification {
validate_avr_signature(
&avr.certificate_chain,
&avr.body,
&avr.signature,
timestamp_now as u64,
)?;
}
let avr_body = ParsedAVR::new(avr)?;
let timestamp = avr_body.timestamp()?;
if !timestamp_is_fresh(timestamp_now, timestamp) {
return Err(AVRError::TimestampOutOfRange.into());
}
let quote_status = avr_body.isv_enclave_quote_status()?;
match quote_status.as_str() {
"OK" | "SW_HARDENING_NEEDED" => {}
"GROUP_OUT_OF_DATE" | "CONFIGURATION_NEEDED" | "CONFIGURATION_AND_SW_HARDENING_NEEDED" => {
if !unsafe_lax_avr_verification {
return Err(AVRError::QuoteStatusInvalid {
status: quote_status.to_owned(),
}
.into());
}
}
_ => {
return Err(AVRError::QuoteStatusInvalid {
status: quote_status.to_owned(),
}
.into());
}
};
let quote_body = avr_body.isv_enclave_quote_body()?;
let quote_body = match BASE64_STANDARD.decode(quote_body) {
Ok(quote_body) => quote_body,
_ => return Err(AVRError::MalformedQuote.into()),
};
let quote_body = match QuoteBody::decode("e_body) {
Ok(quote_body) => quote_body,
_ => return Err(AVRError::MalformedQuote.into()),
};
if policy
.gid_blacklist
.iter()
.any(|gid| gid == "e_body.gid)
{
return Err(AVRError::BlacklistedGID.into());
}
let is_debug = quote_body
.report_body
.attributes
.flags
.contains(AttributesFlags::DEBUG);
let allow_debug = option_env!("OASIS_UNSAFE_ALLOW_DEBUG_ENCLAVES").is_some();
if is_debug && !allow_debug {
return Err(AVRError::DebugEnclave.into());
} else if !is_debug && allow_debug {
return Err(AVRError::ProductionEnclave.into());
}
if avr_body.tcb_evaluation_data_number()? < policy.min_tcb_evaluation_data_number {
return Err(AVRError::TCBEvaluationDataNumberInvalid.into());
}
update_insecure_posix_time(timestamp);
Ok(VerifiedQuote {
report_data: quote_body.report_body.reportdata.to_vec(),
identity: EnclaveIdentity {
mr_enclave: MrEnclave::from(quote_body.report_body.mrenclave.to_vec()),
mr_signer: MrSigner::from(quote_body.report_body.mrsigner.to_vec()),
},
timestamp,
})
}
fn parse_avr_timestamp(timestamp: &str) -> Result<i64> {
let timestamp_unix = match NaiveDateTime::parse_from_str(timestamp, IAS_TS_FMT) {
Ok(timestamp) => timestamp.and_utc().timestamp(),
_ => return Err(AVRError::MalformedTimestamp.into()),
};
Ok(timestamp_unix)
}
fn validate_avr_signature(
cert_chain: &[u8],
message: &[u8],
signature: &[u8],
unix_time: u64,
) -> Result<()> {
let raw_pem = percent_encoding::percent_decode(cert_chain).decode_utf8()?;
let mut cert_ders = Vec::new();
for pem in pem::Pem::iter_from_buffer(raw_pem.as_bytes()) {
let pem = match pem {
Ok(p) => p,
Err(_) => return Err(AVRError::MalformedCertificatePEM.into()),
};
if pem.label != PEM_CERTIFICATE_LABEL {
return Err(AVRError::MalformedCertificatePEM.into());
}
cert_ders.push(pem.contents);
}
if cert_ders.len() != 2 {
return Err(AVRError::ChainNotTwoCertificates.into());
}
let time = ASN1Time::from_timestamp(unix_time as i64)?;
if cert_ders[1] != *IAS_TRUST_ANCHOR {
return Err(anyhow!("AVR certificate chain trust anchor mismatch"));
}
let anchor = match parse_x509_certificate(&cert_ders[1]) {
Ok((_, cert)) => cert,
Err(_) => return Err(AVRError::MalformedCertificateDER.into()),
};
if !anchor.validity().is_valid_at(time) {
return Err(AVRError::ExpiredCertificate.into());
}
let anchor_pk = extract_certificate_rsa_public_key(&anchor)?;
if !check_certificate_rsa_signature(&anchor, &anchor_pk) {
return Err(anyhow!(
"AVR certificate chain trust anchor has invalid signature"
));
}
if !anchor.tbs_certificate.is_ca() {
return Err(anyhow!("AVR certificate trust anchor is not a CA"));
}
let leaf = match parse_x509_certificate(&cert_ders[0]) {
Ok((rem, cert)) => {
if !rem.is_empty() {
return Err(AVRError::MalformedCertificateDER.into());
}
cert
}
Err(_) => return Err(AVRError::MalformedCertificateDER.into()),
};
if !check_certificate_rsa_signature(&leaf, &anchor_pk) {
return Err(anyhow!("invalid leaf certificate signature"));
}
if !leaf.validity().is_valid_at(time) {
return Err(AVRError::ExpiredCertificate.into());
}
match leaf.tbs_certificate.key_usage()? {
Some(ku) => {
if !ku.value.digital_signature() {
return Err(anyhow!("leaf certificate can't sign"));
}
}
None => {
return Err(anyhow!("leaf cert missing key usage"));
}
}
let leaf_pk = extract_certificate_rsa_public_key(&leaf)?;
let scheme = Pkcs1v15Sign::new::<sha2::Sha256>();
let digest = Sha256::new().chain(message).finalize();
let signature = BASE64_STANDARD.decode(signature)?;
leaf_pk
.verify(scheme, &digest, &signature)
.map_err(|_| AVRError::InvalidSignature)?;
Ok(())
}
fn extract_certificate_rsa_public_key(cert: &X509Certificate) -> Result<RsaPublicKey> {
let cert_spki = &cert.tbs_certificate.subject_pki;
if cert_spki.algorithm.algorithm != OID_PKCS1_RSAENCRYPTION {
return Err(anyhow!("invalid certificate public key algorithm"));
}
match RsaPublicKey::from_pkcs1_der(&cert_spki.subject_public_key.data) {
Ok(pk) => Ok(pk),
Err(err) => Err(anyhow!("invalid certificate public key: {:?}", err)),
}
}
fn check_certificate_rsa_signature(cert: &X509Certificate, public_key: &RsaPublicKey) -> bool {
if cert.signature_algorithm.algorithm != OID_PKCS1_SHA256WITHRSA {
return false;
}
let scheme = Pkcs1v15Sign::new::<sha2::Sha256>();
let digest = Sha256::new()
.chain(cert.tbs_certificate.as_ref())
.finalize();
public_key
.verify(scheme, &digest, &cert.signature_value.data)
.is_ok()
}
pub(crate) fn timestamp_is_fresh(now: i64, timestamp: i64) -> bool {
(now - timestamp).abs() < 60 * 60 * 24
}
#[cfg(test)]
mod tests {
use super::*;
const IAS_CERT_CHAIN: &[u8] =
include_bytes!("../../../testdata/avr_certificates_urlencoded.pem");
#[test]
fn test_validate_avr_signature() {
const MSG: &[u8] = include_bytes!("../../../testdata/avr_body_group_out_of_date.json");
const SIG: &[u8] = include_bytes!("../../../testdata/avr_signature_group_out_of_date.sig");
const SIG_AT: u64 = 1522447346; let result = validate_avr_signature(IAS_CERT_CHAIN, MSG, SIG, SIG_AT);
assert!(result.is_ok());
let result = validate_avr_signature(IAS_CERT_CHAIN, MSG, SIG, 0);
assert!(result.is_err());
let bad_msg: &mut [u8] = &mut MSG.to_owned();
bad_msg[0] ^= 0x23;
let result = validate_avr_signature(IAS_CERT_CHAIN, bad_msg, SIG, SIG_AT);
assert!(result.is_err());
let bad_sig = BASE64_STANDARD.decode(SIG).unwrap();
let bad_sig = &mut bad_sig.to_owned();
bad_sig[0] ^= 0x42;
let bad_sig = BASE64_STANDARD.encode(bad_sig);
let result = validate_avr_signature(IAS_CERT_CHAIN, MSG, bad_sig.as_bytes(), SIG_AT);
assert!(result.is_err());
let timestamp = parse_avr_timestamp("2018-03-30T22:02:26.123456").unwrap();
assert_eq!(timestamp, SIG_AT as i64);
}
#[test]
fn test_decode_avr_v4() {
const AVR: &[u8] = include_bytes!(
"../../../../go/common/sgx/ias/testdata/avr_v4_body_sw_hardening_needed.json"
);
const SIG: &[u8] = include_bytes!(
"../../../../go/common/sgx/ias/testdata/avr_v4_body_sw_hardening_needed.sig"
);
const SIG_AT: u64 = 1589188875; const ID: &str = "323119119247496566074708526703373820736";
const NONCE: &str = "biNMqBAuTPF2hp/0fXa4P3splRkLHJf0";
const EPID_PSEUDONYM: &str = "uAFRLXADu90LsPq9Btgx8MWUPOzmDHE51pwLlUlU3hzFUk2EmvWpF6fZsyokOVkQUJ0UwZk0nCF8XPaCcSmLwqXAzLa+n/K7TdwlxKofEyTgG8da8mmrShNoFw3BSD74wSA4aAc753IfrbnnmuYk00lkmSUOTzqsqHlAORcweqg=";
let result = validate_avr_signature(IAS_CERT_CHAIN, AVR, SIG, SIG_AT);
assert!(result.is_ok());
let raw_avr = AVR {
body: AVR.to_vec(),
signature: SIG.to_vec(),
certificate_chain: IAS_CERT_CHAIN.to_vec(),
};
let avr = ParsedAVR::new(&raw_avr).expect("parsing raw AVR should succeed");
assert_eq!(avr.body["id"].as_str().expect("id should be present"), ID);
assert_eq!(
avr.timestamp().expect("timestamp should exist"),
SIG_AT as i64
);
assert_eq!(
avr.body["version"].as_u64().expect("version should exist"),
4
);
assert_eq!(
avr.isv_enclave_quote_status()
.expect("isv enclave quote status should exist"),
"SW_HARDENING_NEEDED"
);
let isv_enclave_quote_body = BASE64_STANDARD
.decode(
avr.isv_enclave_quote_body()
.expect("isv enclave quote body should exist"),
)
.expect("decoding isv enclave quote body should succeed");
assert_eq!(isv_enclave_quote_body.len(), 432);
assert!(avr.body.get("revocationReason").is_none());
assert!(avr.body.get("pseManifestStatus").is_none());
assert!(avr.body.get("pseManifestHash").is_none());
assert!(avr.body.get("platformInfoBlob").is_none());
assert_eq!(
avr.body["nonce"].as_str().expect("nonce should exist"),
NONCE
);
assert_eq!(
avr.body["epidPseudonym"]
.as_str()
.expect("epid pseudonym should exist"),
EPID_PSEUDONYM
);
assert_eq!(
avr.body["advisoryURL"]
.as_str()
.expect("advisory URL should exist"),
"https://security-center.intel.com"
);
for (avr_id, known_id) in avr.body["advisoryIDs"]
.as_array()
.expect("advisory ID array should exist")
.iter()
.zip(["INTEL-SA-00334"])
{
assert_eq!(
avr_id.as_str().expect("advisory ID should be a string"),
known_id
);
}
}
#[test]
fn test_decode_avr_v5() {
const AVR: &[u8] = include_bytes!(
"../../../../go/common/sgx/ias/testdata/avr_v5_body_sw_hardening_needed.json"
);
const SIG: &[u8] = include_bytes!(
"../../../../go/common/sgx/ias/testdata/avr_v5_body_sw_hardening_needed.sig"
);
const SIG_AT: u64 = 1695829918; const ID: &str = "325753020347524304899139732345489823748";
const EPID_PSEUDONYM: &str = "twLvZuBD1sOHsNPsHGbZOVlGh9rXw9XzVQTVKUuvsqypw0iWcFKwR7aNoHmDSoeFc/+pH6LLCI2bQBKx/ygwXphePD4GTTRwBi9EIBFRlURTk4p4NosbA7xcCG4hRuCDaEKPtAX6XHjNKEvWA+4f1aAfD7jwOtGAzHeaqBldaD8=";
let result = validate_avr_signature(IAS_CERT_CHAIN, AVR, SIG, SIG_AT);
assert!(result.is_ok());
let raw_avr = AVR {
body: AVR.to_vec(),
signature: SIG.to_vec(),
certificate_chain: IAS_CERT_CHAIN.to_vec(),
};
let avr = ParsedAVR::new(&raw_avr).expect("parsing raw AVR should succeed");
assert_eq!(avr.body["id"].as_str().expect("id should be present"), ID);
assert_eq!(
avr.timestamp().expect("timestamp should exist"),
SIG_AT as i64
);
assert_eq!(
avr.body["version"].as_u64().expect("version should exist"),
5
);
assert_eq!(
avr.isv_enclave_quote_status()
.expect("isv enclave quote status should exist"),
"SW_HARDENING_NEEDED"
);
let isv_enclave_quote_body = BASE64_STANDARD
.decode(
avr.isv_enclave_quote_body()
.expect("isv enclave quote body should exist"),
)
.expect("decoding isv enclave quote body should succeed");
assert_eq!(isv_enclave_quote_body.len(), 432);
assert!(avr.body.get("revocationReason").is_none());
assert!(avr.body.get("pseManifestStatus").is_none());
assert!(avr.body.get("pseManifestHash").is_none());
assert!(avr.body.get("platformInfoBlob").is_none());
assert!(avr.body.get("nonce").is_none());
assert_eq!(
avr.body["epidPseudonym"]
.as_str()
.expect("epid pseudonym should exist"),
EPID_PSEUDONYM
);
assert_eq!(
avr.body["advisoryURL"]
.as_str()
.expect("advisory URL should exist"),
"https://security-center.intel.com"
);
for (avr_id, known_id) in avr.body["advisoryIDs"]
.as_array()
.expect("advisory ID array should exist")
.iter()
.zip(["INTEL-SA-00334", "INTEL-SA-00615"])
{
assert_eq!(
avr_id.as_str().expect("advisory ID should be a string"),
known_id
);
}
}
}