use std::error::Error;
use std::str;
use url::Url;
use percent_encoding::percent_decode_str as urldecode;
use reqwest::{header::HeaderValue, IntoUrl, RequestBuilder};
use serde::{Serialize, Deserialize};
use pkix::pem::{PEM_CERTIFICATE, PemBlock, pem_to_der};
use sgx_pkix::attestation::AttestationEmbeddedIasReport;
use crate::HexPrint;
use crate::api::{IasAdvisoryId, IasVersion, Unverified, VerifyAttestationEvidenceRequest, VerifyAttestationEvidenceResponse};
use crate::verifier::Crypto;
type Result<T> = std::result::Result<T, Box<dyn Error + Send + Sync>>;
trait RequestBuilderExt {
fn apply_credentials(self, ias_client: &Client) -> Self;
}
trait HeaderMapExt {
fn header<H: Header>(&self) -> Result<Option<H>>;
}
impl HeaderMapExt for reqwest::header::HeaderMap {
fn header<H: Header>(&self) -> Result<Option<H>> {
let mut it = self.get_all(H::NAME).iter();
match (it.next(), it.next()) {
(Some(v), None) => return H::from_value(v).map(Some).map_err(|_| format!("Failed to parse header {}", H::NAME).into()),
(None, _) => return Ok(None),
_ => return Err(format!("Multiple values for {} header", H::NAME).into())
}
}
}
trait Header: Sized {
const NAME: &'static str;
fn from_value(v: &HeaderValue) -> std::result::Result<Self, Box<dyn std::error::Error>>;
}
pub struct IasVerificationResult {
pub raw_report: Vec<u8>,
pub signature: Vec<u8>,
pub cert_chain: Vec<Vec<u8>>,
pub advisory_url: Option<String>,
pub advisory_ids: Vec<IasAdvisoryId>,
}
impl IasVerificationResult {
pub fn verify<C: Crypto>(&self, ca_certificates: &[&[u8]]) -> Result<VerifyAttestationEvidenceResponse> {
let cert_chain = self.cert_chain.iter().map(|c| c.clone().into()).collect();
VerifyAttestationEvidenceResponse::from_raw_report::<C>(self.raw_report.as_slice(), &self.signature, &cert_chain, ca_certificates)
.map_err(|e| e.into())
}
}
impl Into<AttestationEmbeddedIasReport<'static, 'static, 'static>> for IasVerificationResult {
fn into(self) -> AttestationEmbeddedIasReport<'static, 'static, 'static> {
AttestationEmbeddedIasReport {
http_body: self.raw_report.into(),
report_sig: self.signature.into(),
certificates: self.cert_chain.into_iter().map(Into::into).collect(),
}
}
}
#[derive(Clone, Debug)]
struct IasReportSignature(pub Vec<u8>);
impl Header for IasReportSignature {
const NAME: &'static str = "X-IASReport-Signature";
fn from_value(v: &HeaderValue) -> std::result::Result<Self, Box<dyn std::error::Error>> {
Ok(IasReportSignature(base64::decode(v.to_str()?)?))
}
}
#[derive(Clone, Debug)]
struct IasReportSigningCertificate(pub String);
impl Header for IasReportSigningCertificate {
const NAME: &'static str = "X-IASReport-Signing-Certificate";
fn from_value(v: &HeaderValue) -> std::result::Result<Self, Box<dyn std::error::Error>> {
Ok(IasReportSigningCertificate(urldecode(v.to_str()?).decode_utf8()?.into_owned()))
}
}
#[derive(Clone, Debug)]
struct AdvisoryUrl(pub String);
impl Header for AdvisoryUrl {
const NAME: &'static str = "Advisory-URL";
fn from_value(v: &HeaderValue) -> std::result::Result<Self, Box<dyn std::error::Error>> {
Ok(AdvisoryUrl(v.to_str()?.to_owned()))
}
}
impl Header for Vec<IasAdvisoryId> {
const NAME: &'static str = "Advisory-IDs";
fn from_value(v: &HeaderValue) -> std::result::Result<Self, Box<dyn std::error::Error>> {
let adv = v.to_str()?
.split(',')
.map(|adv| IasAdvisoryId::from(adv))
.collect();
Ok(adv)
}
}
pub struct Client {
url: Url,
ias_path: String,
subscription_key: Option<String>,
inner: reqwest::Client,
}
static API_FTX_SPID: &'static str = "ftx/spid";
static API_SIGRL: &'static str = "sigrl/";
static API_REPORT: &'static str = "report";
pub struct ClientBuilder {
subscription_key: Option<String>,
has_identity: bool,
use_alt: Option<bool>,
ias_version: IasVersion,
inner: reqwest::ClientBuilder,
}
impl ClientBuilder {
pub fn new() -> Self {
ClientBuilder::new_with_reqwest_builder(Default::default())
}
pub fn new_with_reqwest_builder(builder: reqwest::ClientBuilder) -> Self {
ClientBuilder {
subscription_key: None,
has_identity: false,
use_alt: None,
ias_version: crate::api::LATEST_IAS_VERSION,
inner: builder
}
}
pub fn use_alternate_api_path(mut self, use_alt: bool) -> Self {
self.use_alt = Some(use_alt);
self
}
pub fn ias_version(mut self, version: IasVersion) -> Self {
self.ias_version = version;
self
}
pub fn subscription_key(mut self, subscription_key: String) -> Self {
assert!(!self.has_identity);
self.subscription_key = Some(subscription_key);
self
}
#[cfg(feature = "client-certificate")]
pub fn client_certificate(mut self, identity: reqwest::tls::Identity) -> Self {
assert!(self.subscription_key.is_none());
self.inner = self.inner.identity(identity);
self.has_identity = true;
self
}
pub fn build<U: IntoUrl>(self, url: U) -> Result<Client> {
let mut url = url.into_url()?;
if !(url.scheme() == "https" || url.scheme() == "http") || url.cannot_be_a_base() {
return Err("IAS URL is not a valid HTTPS URL.".into());
}
if !url.path().ends_with("/") {
url.path_segments_mut().expect("checked for cannot be a base").push("");
}
let alt = if self.use_alt.unwrap_or(self.ias_version >= IasVersion::V4) { "" } else { "sgx/" };
let ias_path = format!("attestation/{}v{}/", alt, self.ias_version as u64);
Ok(Client {
url,
ias_path,
subscription_key: self.subscription_key,
inner: self.inner.build()?
})
}
}
impl RequestBuilderExt for RequestBuilder {
fn apply_credentials(self, ias_client: &Client) -> Self {
if let Some(ref subscription_key) = ias_client.subscription_key {
self.header("Ocp-Apim-Subscription-Key", subscription_key)
} else {
self
}
}
}
impl Client {
pub async fn get_spid(&self, report: &[u8]) -> Result<Vec<u8>> {
let res = self.inner.post(self.url.join(API_FTX_SPID)?)
.header("Accept", "application/octet-stream")
.header("Content-Type", "application/octet-stream")
.body(report.to_owned())
.send().await?
.error_for_status()?;
Ok((*res.bytes().await?).into())
}
pub async fn get_sig_rl(&self, gid: &[u8], spid: Option<&[u8]>) -> Result<Vec<u8>> {
let mut url = self.url
.join(&self.ias_path)?
.join(API_SIGRL)?
.join(&HexPrint(&gid).to_string())?;
if let Some(spid) = spid {
url.query_pairs_mut().append_pair("spid", &HexPrint(&spid).to_string());
}
let res = self.inner.get(url)
.apply_credentials(self)
.header("Accept", "application/json")
.send().await?
.error_for_status()?;
Ok((*res.bytes().await?).into())
}
pub async fn verify_quote(&self, quote: &[u8]) -> Result<IasVerificationResult> {
let req = VerifyAttestationEvidenceRequest {
isv_enclave_quote: quote.to_owned(),
pse_manifest: None,
nonce: None,
};
let mut json = vec![];
let mut ser = serde_json::Serializer::new(&mut json);
let ser = serde_bytes_repr::ByteFmtSerializer::base64(&mut ser, base64::Config::new(base64::CharacterSet::Standard, true));
req.serialize(ser).map_err(|e| format!("Error serializing JSON request: {}", e))?;
let res = self.inner.post(self.url.join(&self.ias_path)?.join(API_REPORT)?)
.apply_credentials(self)
.body(json)
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.send().await?
.error_for_status()?;
let signature: Vec<u8> = res.headers().header::<IasReportSignature>()?
.ok_or_else(|| {
format!("no {} header in response", IasReportSignature::NAME)
})?
.0;
let cert_chain: String = res.headers().header::<IasReportSigningCertificate>()?
.ok_or_else(|| {
format!("no {} header in response", IasReportSigningCertificate::NAME)
})?
.0;
let split_certs = PemBlock::new(cert_chain.as_bytes())
.map(|c| {
str::from_utf8(c).map_err(Into::into).and_then(|c| {
pem_to_der(c, Some(PEM_CERTIFICATE))
.ok_or_else(|| { "invalid PEM in report signing certificate chain".into() })
})
})
.collect::<Result<Vec<Vec<u8>>>>()?;
let mut advisory_url = res.headers().header::<AdvisoryUrl>()?.map( |v| v.0 );
let mut advisory_ids: Option<Vec<IasAdvisoryId>> = res.headers().header::<Vec<IasAdvisoryId>>()?;
let raw_report = res.bytes().await?;
debug!("Report body: {}", str::from_utf8(&raw_report).unwrap_or("<invalid UTF-8>"));
let mut deser = serde_json::Deserializer::from_slice(&raw_report);
let deser = serde_bytes_repr::ByteFmtDeserializer::new_base64(&mut deser, base64::Config::new(base64::CharacterSet::Standard, true));
let report: VerifyAttestationEvidenceResponse<Unverified> = VerifyAttestationEvidenceResponse::deserialize(deser)
.map_err(|e| format!("Error deserializing JSON response: {}", e))?;
debug!("IAS verification report: {:?}", report);
match (&advisory_url, &report.advisory_url()) {
(None, Some(new_advisory_url)) => advisory_url = Some(new_advisory_url.clone()),
_ => {}
}
match (&advisory_ids, &report.advisory_ids()) {
(None, Some(new_advisory_ids)) => advisory_ids = Some(new_advisory_ids.to_owned()),
_ => {}
}
Ok(IasVerificationResult {
raw_report: (*raw_report).into(),
signature,
cert_chain: split_certs,
advisory_url,
advisory_ids: advisory_ids.unwrap_or(Vec::new()),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
const GID: &'static [u8] = b"";
const SPID: &'static [u8] = b"";
const IAS_URL: &'static str = "";
const SUBSCRIPTION_KEY: &'static str = "";
const QUOTE: &'static str = "";
#[tokio::test]
#[ignore]
async fn test_get_sig_rl() {
let client = ClientBuilder::new()
.ias_version(IasVersion::V3)
.build(IAS_URL).unwrap();
let _ = client.get_sig_rl(&GID, Some(&SPID)).await.unwrap();
}
#[tokio::test]
#[ignore]
async fn test_get_sig_rl_v4() {
let client = ClientBuilder::new()
.ias_version(IasVersion::V4)
.subscription_key(SUBSCRIPTION_KEY.into())
.build(IAS_URL).unwrap();
let _ = client.get_sig_rl(&GID, Some(&SPID)).await.unwrap();
}
#[tokio::test]
#[ignore]
async fn test_verify_quote_v3_old() {
let client = ClientBuilder::new()
.ias_version(IasVersion::V3)
.build(IAS_URL).unwrap();
let _ = client.verify_quote(&base64::decode("E).unwrap()).await.unwrap();
}
#[tokio::test]
#[ignore]
async fn test_verify_quote_v3() {
let client = ClientBuilder::new()
.ias_version(IasVersion::V3)
.use_alternate_api_path(true)
.subscription_key(SUBSCRIPTION_KEY.into())
.build(IAS_URL).unwrap();
let _ = client.verify_quote(&base64::decode("E).unwrap()).await.unwrap();
}
#[tokio::test]
#[ignore]
async fn test_verify_quote_v4() {
let client = ClientBuilder::new()
.ias_version(IasVersion::V4)
.subscription_key(SUBSCRIPTION_KEY.into())
.build(IAS_URL).unwrap();
let _ = client.verify_quote(&base64::decode("E).unwrap()).await.unwrap();
}
}