app_store_server_library/
signed_data_verifier.rs1use base64::engine::general_purpose::STANDARD;
2use base64::{DecodeError, Engine};
3use serde::de::DeserializeOwned;
4
5use crate::chain_verifier::{ChainVerificationFailureReason, ChainVerifier, ChainVerifierError};
6use crate::crypto::{jws, CryptoProvider};
7use crate::models::app_store_environment::Environment;
8use crate::models::app_transaction::AppTransaction;
9use crate::models::decoded_realtime_request_body::DecodedRealtimeRequestBody;
10use crate::models::decoded_signed_data::DecodedSignedData;
11use crate::models::jws_renewal_info_decoded_payload::JWSRenewalInfoDecodedPayload;
12use crate::models::jws_transaction_decoded_payload::JWSTransactionDecodedPayload;
13use crate::models::response_body_v2_decoded_payload::ResponseBodyV2DecodedPayload;
14
15#[derive(thiserror::Error, Debug)]
16pub enum SignedDataVerifierError {
17 #[error("VerificationFailure")]
18 VerificationFailure,
19
20 #[error("InvalidAppIdentifier")]
21 InvalidAppIdentifier,
22
23 #[error("InvalidEnvironment")]
24 InvalidEnvironment,
25
26 #[error("InvalidAppAppleId")]
27 InvalidAppAppleId,
28
29 #[error("InternalChainVerifierError")]
30 InternalChainVerifierError(#[from] ChainVerifierError),
31
32 #[error("InternalDecodeError: [{0}]")]
33 InternalDecodeError(#[from] DecodeError),
34
35 #[error("InternalDeserializationError: [{0}]")]
36 InternalDeserializationError(#[from] serde_json::Error),
37
38 #[error("InternalJWSError: [{0}]")]
39 InternalJWSError(#[from] crate::crypto::jws::JwsError),
40}
41
42const EXPECTED_CHAIN_LENGTH: usize = 3;
43
44pub struct SignedDataVerifier {
46 environment: Environment,
47 bundle_id: String,
48 app_apple_id: Option<i64>,
49 enable_online_checks: bool,
50 chain_verifier: ChainVerifier,
51}
52
53impl SignedDataVerifier {
54 pub fn new(
72 root_certificates: Vec<Vec<u8>>,
73 environment: Environment,
74 bundle_id: String,
75 app_apple_id: Option<i64>,
76 enable_online_checks: bool,
77 ) -> Result<Self, SignedDataVerifierError> {
78 if environment == Environment::Production && app_apple_id.is_none() {
79 return Err(SignedDataVerifierError::InvalidAppAppleId);
80 }
81
82 Ok(SignedDataVerifier {
83 environment,
84 bundle_id,
85 app_apple_id,
86 enable_online_checks,
87 chain_verifier: ChainVerifier::new(root_certificates),
88 })
89 }
90}
91
92impl SignedDataVerifier {
93 pub fn verify_and_decode_renewal_info(
105 &self,
106 signed_renewal_info: &str,
107 ) -> Result<JWSRenewalInfoDecodedPayload, SignedDataVerifierError> {
108 let decoded_renewal_info: JWSRenewalInfoDecodedPayload = self.decode_signed_object(signed_renewal_info)?;
109
110 if decoded_renewal_info
111 .environment
112 .as_ref()
113 != Some(&self.environment)
114 {
115 return Err(SignedDataVerifierError::InvalidEnvironment);
116 }
117
118 Ok(decoded_renewal_info)
119 }
120
121 pub fn verify_and_decode_signed_transaction(
133 &self,
134 signed_transaction: &str,
135 ) -> Result<JWSTransactionDecodedPayload, SignedDataVerifierError> {
136 let decoded_signed_tx: JWSTransactionDecodedPayload = self.decode_signed_object(signed_transaction)?;
137
138 if decoded_signed_tx.bundle_id.as_ref() != Some(&self.bundle_id) {
139 return Err(SignedDataVerifierError::InvalidAppIdentifier);
140 }
141
142 if decoded_signed_tx.environment.as_ref() != Some(&self.environment) {
143 return Err(SignedDataVerifierError::InvalidEnvironment);
144 }
145
146 Ok(decoded_signed_tx)
147 }
148
149 pub fn verify_and_decode_notification(
161 &self,
162 signed_payload: &str,
163 ) -> Result<ResponseBodyV2DecodedPayload, SignedDataVerifierError> {
164 let decoded_signed_notification: ResponseBodyV2DecodedPayload = self.decode_signed_object(signed_payload)?;
165
166 let bundle_id;
167 let app_apple_id;
168 let environment;
169
170 if let Some(data) = &decoded_signed_notification.data {
171 bundle_id = data.bundle_id.clone();
172 app_apple_id = data.app_apple_id;
173 environment = data.environment.clone();
174 } else if let Some(summary) = &decoded_signed_notification.summary {
175 bundle_id = summary.bundle_id.clone();
176 app_apple_id = summary.app_apple_id;
177 environment = summary.environment.clone();
178 } else if let Some(external_purchase_token) = &decoded_signed_notification.external_purchase_token {
179 bundle_id = external_purchase_token
180 .bundle_id
181 .clone();
182 app_apple_id = external_purchase_token.app_apple_id;
183
184 if let Some(external_purchase_id) = &external_purchase_token.external_purchase_id {
185 if external_purchase_id.starts_with("SANDBOX") {
186 environment = Some(Environment::Sandbox)
187 } else {
188 environment = Some(Environment::Production)
189 }
190 } else {
191 environment = Some(Environment::Production)
192 }
193 } else if let Some(app_data) = &decoded_signed_notification.app_data {
194 bundle_id = app_data.bundle_id.clone();
195 app_apple_id = app_data.app_apple_id;
196 environment = app_data.environment.clone();
197 } else {
198 bundle_id = None;
199 app_apple_id = None;
200 environment = None;
201 }
202
203 self.verify_notification_app_identifier_and_environment(bundle_id, app_apple_id, environment)?;
204
205 Ok(decoded_signed_notification)
206 }
207
208 fn verify_notification_app_identifier_and_environment(
209 &self,
210 bundle_id: Option<String>,
211 app_apple_id: Option<i64>,
212 environment: Option<Environment>,
213 ) -> Result<(), SignedDataVerifierError> {
214 if self.environment == Environment::LocalTesting {
215 return Ok(());
216 }
217
218 if bundle_id.as_deref() != Some(self.bundle_id.as_str())
219 || (self.environment == Environment::Production && self.app_apple_id != app_apple_id)
220 {
221 return Err(SignedDataVerifierError::InvalidAppIdentifier);
222 }
223
224 if environment.as_ref() != Some(&self.environment) {
225 return Err(SignedDataVerifierError::InvalidEnvironment);
226 }
227
228 Ok(())
229 }
230
231 pub fn verify_and_decode_app_transaction(
243 &self,
244 signed_app_transaction: &str,
245 ) -> Result<AppTransaction, SignedDataVerifierError> {
246 let decoded_app_transaction: AppTransaction = self.decode_signed_object(signed_app_transaction)?;
247
248 if decoded_app_transaction
249 .bundle_id
250 .as_ref()
251 != Some(&self.bundle_id)
252 || (self.environment == Environment::Production
253 && self.app_apple_id != decoded_app_transaction.app_apple_id)
254 {
255 return Err(SignedDataVerifierError::InvalidAppIdentifier);
256 }
257
258 if decoded_app_transaction
259 .receipt_type
260 .as_ref()
261 != Some(&self.environment)
262 {
263 return Err(SignedDataVerifierError::InvalidEnvironment);
264 }
265
266 Ok(decoded_app_transaction)
267 }
268
269 pub fn verify_and_decode_realtime_request(
280 &self,
281 signed_payload: &str,
282 ) -> Result<DecodedRealtimeRequestBody, SignedDataVerifierError> {
283 let decoded_realtime_request: DecodedRealtimeRequestBody = self.decode_signed_object(signed_payload)?;
284
285 if self.environment == Environment::Production
286 && self.app_apple_id != Some(decoded_realtime_request.app_apple_id)
287 {
288 return Err(SignedDataVerifierError::InvalidAppIdentifier);
289 }
290
291 if self.environment != decoded_realtime_request.environment {
292 return Err(SignedDataVerifierError::InvalidEnvironment);
293 }
294
295 Ok(decoded_realtime_request)
296 }
297
298 fn decode_signed_object<T: DeserializeOwned + DecodedSignedData>(
300 &self,
301 signed_obj: &str,
302 ) -> Result<T, SignedDataVerifierError> {
303 if self.environment == Environment::Xcode || self.environment == Environment::LocalTesting {
306 let _ = jws::decode_header(signed_obj)?;
307 return Ok(jws::decode_payload(signed_obj)?);
308 }
309
310 let header = jws::decode_header(signed_obj)?;
311
312 if header.alg.as_deref() != Some("ES256") {
313 return Err(SignedDataVerifierError::VerificationFailure);
314 }
315
316 let Some(x5c) = header.x5c else {
317 return Err(SignedDataVerifierError::VerificationFailure);
318 };
319
320 if x5c.len() != EXPECTED_CHAIN_LENGTH {
321 return Err(SignedDataVerifierError::InternalChainVerifierError(
322 ChainVerifierError::VerificationFailure(ChainVerificationFailureReason::InvalidChainLength),
323 ));
324 }
325
326 let chain: Vec<Vec<u8>> = x5c
327 .iter()
328 .map(|c| STANDARD.decode(c))
329 .collect::<Result<_, DecodeError>>()?;
330
331 let decoded_body: T = jws::decode_payload(signed_obj)?;
332
333 let effective_date = if self.enable_online_checks {
334 chrono::Utc::now().timestamp() as u64
335 } else {
336 match decoded_body.signed_date_optional() {
337 Some(date) => date.timestamp() as u64,
338 None => chrono::Utc::now().timestamp() as u64,
339 }
340 };
341
342 let spki = self.chain_verifier.verify(
343 &chain[0],
344 &chain[1],
345 Some(effective_date),
346 self.enable_online_checks,
347 )?;
348
349 let signature_bytes = jws::decode_signature_bytes(signed_obj)?;
350 let raw: [u8; 64] = signature_bytes
351 .try_into()
352 .map_err(|_| SignedDataVerifierError::VerificationFailure)?;
353
354 let provider = CryptoProvider::default_provider();
355 let public_key = provider
356 .p256_signing
357 .public_key(&spki)
358 .map_err(|_| SignedDataVerifierError::VerificationFailure)?;
359
360 let signing_input = jws::signing_input(signed_obj)?;
361 public_key
362 .is_valid_signature(&raw, signing_input.as_bytes())
363 .map_err(|_| SignedDataVerifierError::VerificationFailure)?;
364
365 Ok(decoded_body)
366 }
367}
368
369#[cfg(test)]
370mod tests {
371 use super::*;
372 use crate::chain_verifier::ChainVerificationFailureReason::InvalidChainLength;
373
374 #[test]
375 fn test_invalid_chain_length() {
376 let header = jws::b64url_encode(br#"{"alg":"ES256","x5c":["YQ","YQ","YQ","YQ"]}"#);
380 let payload = jws::b64url_encode(b"{}");
381 let signature = jws::b64url_encode(b"sig");
382 let jws_token = format!("{header}.{payload}.{signature}");
383
384 let verifier = SignedDataVerifier::new(
385 vec![Vec::new()],
386 Environment::Production,
387 "com.example".into(),
388 Some(1234),
389 false,
390 )
391 .expect("valid config");
392
393 let result = verifier.verify_and_decode_app_transaction(&jws_token);
394
395 assert!(matches!(
396 result.expect_err("expect error"),
397 SignedDataVerifierError::InternalChainVerifierError(ChainVerifierError::VerificationFailure(
398 InvalidChainLength
399 ))
400 ));
401 }
402}