use {
crate::AppleCodesignError,
base64::{engine::general_purpose::STANDARD as STANDARD_ENGINE, Engine},
log::warn,
reqwest::blocking::{Client, ClientBuilder},
serde::{Deserialize, Serialize},
std::collections::HashMap,
};
pub const APPLE_TICKET_LOOKUP_URL: &str = "https://api.apple-cloudkit.com/database/1/com.apple.gk.ticket-delivery/production/public/records/lookup";
#[derive(Clone, Debug, Serialize)]
pub struct TicketLookupRequest {
pub records: Vec<TicketLookupRequestRecord>,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TicketLookupRequestRecord {
pub record_name: String,
}
#[derive(Clone, Debug, Deserialize)]
pub struct TicketLookupResponse {
pub records: Vec<TicketLookupResponseRecord>,
}
impl TicketLookupResponse {
pub fn signed_ticket(&self, record_name: &str) -> Result<Vec<u8>, AppleCodesignError> {
let record = self
.records
.iter()
.find(|r| r.record_name() == record_name)
.ok_or_else(|| {
AppleCodesignError::NotarizationRecordNotInResponse(record_name.to_string())
})?;
match record {
TicketLookupResponseRecord::Success(r) => r
.signed_ticket_data()
.ok_or(AppleCodesignError::NotarizationRecordNoSignedTicket)?,
TicketLookupResponseRecord::Failure(r) => {
Err(AppleCodesignError::NotarizationLookupFailure(
r.server_error_code.clone(),
r.reason.clone(),
))
}
}
}
}
#[derive(Clone, Debug, Deserialize)]
#[serde(untagged)]
pub enum TicketLookupResponseRecord {
Success(TicketLookupResponseRecordSuccess),
Failure(TicketLookupResponseRecordFailure),
}
impl TicketLookupResponseRecord {
pub fn record_name(&self) -> &str {
match self {
Self::Success(r) => &r.record_name,
Self::Failure(r) => &r.record_name,
}
}
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TicketLookupResponseRecordSuccess {
pub record_name: String,
pub created: TicketRecordEvent,
pub deleted: bool,
pub fields: HashMap<String, Field>,
pub modified: TicketRecordEvent,
pub record_change_tag: String,
pub record_type: String,
}
impl TicketLookupResponseRecordSuccess {
pub fn signed_ticket_data(&self) -> Option<Result<Vec<u8>, AppleCodesignError>> {
match self.fields.get("signedTicket") {
Some(field) => {
if field.typ == "BYTES" {
Some(
STANDARD_ENGINE
.decode(&field.value)
.map_err(AppleCodesignError::NotarizationRecordDecodeFailure),
)
} else {
Some(Err(
AppleCodesignError::NotarizationRecordSignedTicketNotBytes(
field.typ.clone(),
),
))
}
}
None => None,
}
}
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TicketLookupResponseRecordFailure {
pub record_name: String,
pub reason: String,
pub server_error_code: String,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TicketRecordEvent {
#[serde(rename = "deviceID")]
pub device_id: String,
pub timestamp: u64,
pub user_record_name: String,
}
#[derive(Clone, Debug, Deserialize)]
pub struct Field {
#[serde(rename = "type")]
pub typ: String,
pub value: String,
}
pub fn default_client() -> Result<Client, AppleCodesignError> {
Ok(ClientBuilder::default()
.user_agent("apple-codesign crate (https://crates.io/crates/apple-codesign)")
.build()?)
}
pub fn lookup_notarization_tickets<'a>(
client: &Client,
record_names: impl Iterator<Item = &'a str>,
) -> Result<TicketLookupResponse, AppleCodesignError> {
let body = TicketLookupRequest {
records: record_names
.map(|x| {
warn!("looking up notarization ticket for {}", x);
TicketLookupRequestRecord {
record_name: x.to_string(),
}
})
.collect::<Vec<_>>(),
};
let req = client
.post(APPLE_TICKET_LOOKUP_URL)
.header("Accept", "application/json")
.header("Content-Type", "application/json")
.json(&body);
let response = req.send()?;
let body = response.bytes()?;
let response = serde_json::from_slice::<TicketLookupResponse>(&body)?;
Ok(response)
}
pub fn lookup_notarization_ticket(
client: &Client,
record_name: &str,
) -> Result<TicketLookupResponse, AppleCodesignError> {
lookup_notarization_tickets(client, std::iter::once(record_name))
}
#[cfg(test)]
mod test {
use super::*;
const PYOXIDIZER_APP_RECORD: &str = "2/2/1b747faf223750de74febed7929f14a73af8c933";
const DEADBEEF: &str = "2/2/deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
#[test]
fn lookup_ticket() -> Result<(), AppleCodesignError> {
let client = default_client()?;
let res = lookup_notarization_ticket(&client, PYOXIDIZER_APP_RECORD)?;
assert!(matches!(
&res.records[0],
TicketLookupResponseRecord::Success(_)
));
let ticket = res.signed_ticket(PYOXIDIZER_APP_RECORD)?;
assert_eq!(&ticket[0..4], b"s8ch");
let res = lookup_notarization_ticket(&client, DEADBEEF)?;
assert!(matches!(
&res.records[0],
TicketLookupResponseRecord::Failure(_)
));
assert!(matches!(
res.signed_ticket(DEADBEEF),
Err(AppleCodesignError::NotarizationLookupFailure(_, _))
));
Ok(())
}
}