1use base64::engine::general_purpose::STANDARD;
2use base64::{DecodeError, Engine};
3
4use crate::chain_verifier::{verify_chain, ChainVerifierError};
5use crate::primitives::app_transaction::AppTransaction;
6use crate::primitives::environment::Environment;
7use crate::primitives::jws_renewal_info_decoded_payload::JWSRenewalInfoDecodedPayload;
8use crate::primitives::jws_transaction_decoded_payload::JWSTransactionDecodedPayload;
9use crate::primitives::response_body_v2_decoded_payload::ResponseBodyV2DecodedPayload;
10use crate::utils::{base64_url_to_base64, StringExt};
11use jsonwebtoken::{Algorithm, DecodingKey, Validation};
12use serde::de::DeserializeOwned;
13
14#[derive(thiserror::Error, Debug, PartialEq)]
15pub enum SignedDataVerifierError {
16 #[error("VerificationFailure")]
17 VerificationFailure,
18
19 #[error("InvalidAppIdentifier")]
20 InvalidAppIdentifier,
21
22 #[error("InvalidEnvironment")]
23 InvalidEnvironment,
24
25 #[error("InternalChainVerifierError")]
26 InternalChainVerifierError(#[from] ChainVerifierError),
27
28 #[error("InternalDecodeError: [{0}]")]
29 InternalDecodeError(#[from] DecodeError),
30
31 #[error("InternalJWTError: [{0}]")]
32 InternalJWTError(#[from] jsonwebtoken::errors::Error),
33}
34
35pub struct SignedDataVerifier {
38 root_certificates: Vec<Vec<u8>>,
39 environment: Environment,
40 bundle_id: String,
41 app_apple_id: Option<i64>,
42}
43
44impl SignedDataVerifier {
45 pub fn new(
58 root_certificates: Vec<Vec<u8>>,
59 environment: Environment,
60 bundle_id: String,
61 app_apple_id: Option<i64>,
62 ) -> Self {
63 return SignedDataVerifier {
64 root_certificates,
65 environment,
66 bundle_id,
67 app_apple_id,
68 };
69 }
70}
71
72impl SignedDataVerifier {
73 pub fn verify_and_decode_renewal_info(
88 &self,
89 signed_renewal_info: &str,
90 ) -> Result<JWSRenewalInfoDecodedPayload, SignedDataVerifierError> {
91 Ok(self.decode_signed_object(signed_renewal_info)?)
92 }
93
94 pub fn verify_and_decode_signed_transaction(
109 &self,
110 signed_transaction: &str,
111 ) -> Result<JWSTransactionDecodedPayload, SignedDataVerifierError> {
112 let decoded_signed_tx: JWSTransactionDecodedPayload =
113 self.decode_signed_object(signed_transaction)?;
114
115 if decoded_signed_tx.bundle_id.as_ref() != Some(&self.bundle_id) {
116 return Err(SignedDataVerifierError::InvalidAppIdentifier);
117 }
118
119 if decoded_signed_tx.environment.as_ref() != Some(&self.environment) {
120 return Err(SignedDataVerifierError::InvalidEnvironment);
121 }
122
123 Ok(decoded_signed_tx)
124 }
125
126 pub fn verify_and_decode_notification(
141 &self,
142 signed_payload: &str,
143 ) -> Result<ResponseBodyV2DecodedPayload, SignedDataVerifierError> {
144 let decoded_signed_notification: ResponseBodyV2DecodedPayload =
145 self.decode_signed_object(signed_payload)?;
146
147 let bundle_id;
148 let app_apple_id;
149 let environment;
150
151 if let Some(data) = &decoded_signed_notification.data {
152 bundle_id = data.bundle_id.clone();
153 app_apple_id = data.app_apple_id.clone();
154 environment = data.environment.clone();
155 } else if let Some(summary) = &decoded_signed_notification.summary {
156 bundle_id = summary.bundle_id.clone();
157 app_apple_id = summary.app_apple_id.clone();
158 environment = summary.environment.clone();
159 } else if let Some(external_purchase_token) = &decoded_signed_notification.external_purchase_token {
160 bundle_id = external_purchase_token.bundle_id.clone();
161 app_apple_id = external_purchase_token.app_apple_id.clone();
162
163 if let Some(external_purchase_id) = &external_purchase_token.external_purchase_id {
164 if external_purchase_id.starts_with("SANDBOX") {
165 environment = Some(Environment::Sandbox)
166 } else {
167 environment = Some(Environment::Production)
168 }
169 } else {
170 environment = Some(Environment::Production)
171 }
172 } else {
173 bundle_id = None;
174 app_apple_id = None;
175 environment = None;
176 }
177
178 self.verify_notification_app_identifier_and_environment(bundle_id, app_apple_id, environment)?;
179
180 Ok(decoded_signed_notification)
181 }
182
183 fn verify_notification_app_identifier_and_environment(
184 &self,
185 bundle_id: Option<String>,
186 app_apple_id: Option<i64>,
187 environment: Option<Environment>,
188 ) -> Result<(), SignedDataVerifierError> {
189 if let Some(bundle_id) = bundle_id {
190 if bundle_id != self.bundle_id {
191 return Err(SignedDataVerifierError::InvalidAppIdentifier);
192 }
193 }
194
195 if self.environment == Environment::Production && self.app_apple_id != app_apple_id {
196 return Err(SignedDataVerifierError::InvalidAppIdentifier);
197 }
198
199 if let Some(environment) = environment {
200 if self.environment != Environment::LocalTesting && self.environment != environment {
201 return Err(SignedDataVerifierError::InvalidEnvironment);
202 }
203 }
204
205 Ok(())
206 }
207
208 pub fn verify_and_decode_app_transaction(
223 &self,
224 signed_app_transaction: &str,
225 ) -> Result<AppTransaction, SignedDataVerifierError> {
226 let decoded_app_transaction: AppTransaction =
227 self.decode_signed_object(signed_app_transaction)?;
228
229 if decoded_app_transaction.bundle_id.as_ref() != Some(&self.bundle_id) {
230 return Err(SignedDataVerifierError::InvalidAppIdentifier);
231 }
232
233 if decoded_app_transaction.receipt_type.as_ref() != Some(&self.environment) {
234 return Err(SignedDataVerifierError::InvalidEnvironment);
235 }
236
237 Ok(decoded_app_transaction)
238 }
239
240 fn decode_signed_object<T: DeserializeOwned>(
242 &self,
243 signed_obj: &str,
244 ) -> Result<T, SignedDataVerifierError> {
245 if self.environment == Environment::Xcode || self.environment == Environment::LocalTesting {
248 const EXPECTED_JWT_SEGMENTS: usize = 3;
249
250 let body_segments: Vec<&str> = signed_obj.split('.').collect();
251
252 if body_segments.len() != EXPECTED_JWT_SEGMENTS {
253 return Err(SignedDataVerifierError::VerificationFailure);
254 }
255
256 let _ = jsonwebtoken::decode_header(&signed_obj)?;
257 let body_data = base64_url_to_base64(body_segments[1]);
258
259 let decoded_body = match STANDARD.decode(body_data) {
260 Ok(decoded_body) => match serde_json::from_slice(&decoded_body) {
261 Ok(decoded) => decoded,
262 Err(_) => return Err(SignedDataVerifierError::VerificationFailure),
263 },
264 Err(_) => return Err(SignedDataVerifierError::VerificationFailure),
265 };
266
267 return Ok(decoded_body);
268 }
269
270 let header = jsonwebtoken::decode_header(signed_obj)?;
271
272 let Some(x5c) = header.x5c else {
273 return Err(SignedDataVerifierError::VerificationFailure);
274 };
275
276 if x5c.is_empty() {
277 return Err(SignedDataVerifierError::VerificationFailure);
278 }
279
280 let x5c: Result<Vec<Vec<u8>>, DecodeError> = x5c.iter().map(|c| c.as_der_bytes()).collect();
281 let chain = x5c?;
282
283 if header.alg != Algorithm::ES256 {
284 return Err(SignedDataVerifierError::VerificationFailure);
285 }
286
287 let pub_key = verify_chain(&chain, &self.root_certificates, None)?;
288 let pub_key = &pub_key[pub_key.len() - 65..];
289
290 let decoding_key = DecodingKey::from_ec_der(pub_key);
291 let claims: [&str; 0] = [];
292
293 let mut validator = Validation::new(Algorithm::ES256);
294 validator.validate_exp = false;
295 validator.set_required_spec_claims(&claims);
296
297 let payload = jsonwebtoken::decode::<T>(signed_obj, &decoding_key, &validator)
298 .expect("Expect Payload");
299 return Ok(payload.claims);
300 }
301}
302
303#[cfg(test)]
304mod tests {
305 use super::*;
306 use crate::primitives::auto_renew_status::AutoRenewStatus;
307 use crate::primitives::expiration_intent::ExpirationIntent;
308 use crate::primitives::in_app_ownership_type::InAppOwnershipType;
309 use crate::primitives::notification_type_v2::NotificationTypeV2;
310 use crate::primitives::offer_discount_type::OfferDiscountType;
311 use crate::primitives::offer_type::OfferType;
312 use crate::primitives::price_increase_status::PriceIncreaseStatus;
313 use crate::primitives::product_type::ProductType;
314 use crate::primitives::purchase_platform::PurchasePlatform;
315 use crate::primitives::revocation_reason::RevocationReason;
316 use crate::primitives::status::Status;
317 use crate::primitives::subtype::Subtype;
318 use crate::primitives::transaction_reason::TransactionReason;
319 use ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING;
320 use serde_json::{Map, Value};
321 use std::fs;
322 use crate::primitives::consumption_request_reason::ConsumptionRequestReason;
323
324 const ROOT_CA_BASE64_ENCODED: &str = "MIIBgjCCASmgAwIBAgIJALUc5ALiH5pbMAoGCCqGSM49BAMDMDYxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQHDAlDdXBlcnRpbm8wHhcNMjMwMTA1MjEzMDIyWhcNMzMwMTAyMjEzMDIyWjA2MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTESMBAGA1UEBwwJQ3VwZXJ0aW5vMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc+/Bl+gospo6tf9Z7io5tdKdrlN1YdVnqEhEDXDShzdAJPQijamXIMHf8xWWTa1zgoYTxOKpbuJtDplz1XriTaMgMB4wDAYDVR0TBAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwCgYIKoZIzj0EAwMDRwAwRAIgemWQXnMAdTad2JDJWng9U4uBBL5mA7WI05H7oH7c6iQCIHiRqMjNfzUAyiu9h6rOU/K+iTR0I/3Y/NSWsXHX+acc";
325
326 const TEST_NOTIFICATION: &str = "eyJ4NWMiOlsiTUlJQm9EQ0NBVWFnQXdJQkFnSUJDekFLQmdncWhrak9QUVFEQWpCTk1Rc3dDUVlEVlFRR0V3SlZVekVUTUJFR0ExVUVDQXdLUTJGc2FXWnZjbTVwWVRFU01CQUdBMVVFQnd3SlEzVndaWEowYVc1dk1SVXdFd1lEVlFRS0RBeEpiblJsY20xbFpHbGhkR1V3SGhjTk1qTXdNVEEwTVRZek56TXhXaGNOTXpJeE1qTXhNVFl6TnpNeFdqQkZNUXN3Q1FZRFZRUUdFd0pWVXpFVE1CRUdBMVVFQ0F3S1EyRnNhV1p2Y201cFlURVNNQkFHQTFVRUJ3d0pRM1Z3WlhKMGFXNXZNUTB3Q3dZRFZRUUtEQVJNWldGbU1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRTRyV0J4R21GYm5QSVBRSTB6c0JLekx4c2o4cEQydnFicjB5UElTVXgyV1F5eG1yTnFsOWZoSzhZRUV5WUZWNysrcDVpNFlVU1Ivbzl1UUlnQ1BJaHJLTWZNQjB3Q1FZRFZSMFRCQUl3QURBUUJnb3Foa2lHOTJOa0Jnc0JCQUlUQURBS0JnZ3Foa2pPUFFRREFnTklBREJGQWlFQWtpRVprb0ZNa2o0Z1huK1E5alhRWk1qWjJnbmpaM2FNOE5ZcmdmVFVpdlFDSURKWVowRmFMZTduU0lVMkxXTFRrNXRYVENjNEU4R0pTWWYvc1lSeEVGaWUiLCJNSUlCbHpDQ0FUMmdBd0lCQWdJQkJqQUtCZ2dxaGtqT1BRUURBakEyTVFzd0NRWURWUVFHRXdKVlV6RVRNQkVHQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVTTUJBR0ExVUVCd3dKUTNWd1pYSjBhVzV2TUI0WERUSXpNREV3TkRFMk1qWXdNVm9YRFRNeU1USXpNVEUyTWpZd01Wb3dUVEVMTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFnTUNrTmhiR2xtYjNKdWFXRXhFakFRQmdOVkJBY01DVU4xY0dWeWRHbHViekVWTUJNR0ExVUVDZ3dNU1c1MFpYSnRaV1JwWVhSbE1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRUZRM2xYMnNxTjlHSXdBaWlNUURRQy9reW5TZ1g0N1J3dmlET3RNWFh2eUtkUWU2Q1BzUzNqbzJ1UkR1RXFBeFdlT2lDcmpsRFdzeXo1d3dkVTBndGFxTWxNQ013RHdZRFZSMFRCQWd3QmdFQi93SUJBREFRQmdvcWhraUc5Mk5rQmdJQkJBSVRBREFLQmdncWhrak9QUVFEQWdOSUFEQkZBaUVBdm56TWNWMjY4Y1JiMS9GcHlWMUVoVDNXRnZPenJCVVdQNi9Ub1RoRmF2TUNJRmJhNXQ2WUt5MFIySkR0eHF0T2pKeTY2bDZWN2QvUHJBRE5wa21JUFcraSIsIk1JSUJYRENDQVFJQ0NRQ2ZqVFVHTERuUjlqQUtCZ2dxaGtqT1BRUURBekEyTVFzd0NRWURWUVFHRXdKVlV6RVRNQkVHQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVTTUJBR0ExVUVCd3dKUTNWd1pYSjBhVzV2TUI0WERUSXpNREV3TkRFMk1qQXpNbG9YRFRNek1ERXdNVEUyTWpBek1sb3dOakVMTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFnTUNrTmhiR2xtYjNKdWFXRXhFakFRQmdOVkJBY01DVU4xY0dWeWRHbHViekJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUFCSFB2d1pmb0tMS2FPclgvV2U0cU9iWFNuYTVUZFdIVlo2aElSQTF3MG9jM1FDVDBJbzJwbHlEQjMvTVZsazJ0YzRLR0U4VGlxVzdpYlE2WmM5VjY0azB3Q2dZSUtvWkl6ajBFQXdNRFNBQXdSUUloQU1USGhXdGJBUU4waFN4SVhjUDRDS3JEQ0gvZ3N4V3B4NmpUWkxUZVorRlBBaUIzNW53azVxMHpjSXBlZnZZSjBNVS95R0dIU1dlejBicTBwRFlVTy9ubUR3PT0iXSwidHlwIjoiSldUIiwiYWxnIjoiRVMyNTYifQ.eyJkYXRhIjp7ImFwcEFwcGxlSWQiOjEyMzQsImVudmlyb25tZW50IjoiU2FuZGJveCIsImJ1bmRsZUlkIjoiY29tLmV4YW1wbGUifSwibm90aWZpY2F0aW9uVVVJRCI6IjlhZDU2YmQyLTBiYzYtNDJlMC1hZjI0LWZkOTk2ZDg3YTFlNiIsInNpZ25lZERhdGUiOjE2ODEzMTQzMjQwMDAsIm5vdGlmaWNhdGlvblR5cGUiOiJURVNUIn0.VVXYwuNm2Y3XsOUva-BozqatRCsDuykA7xIe_CCRw6aIAAxJ1nb2sw871jfZ6dcgNhUuhoZ93hfbc1v_5zB7Og";
327 const MISSING_X5C_HEADER_CLAIM: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsIng1Y3dyb25nIjpbIk1JSUJvRENDQVVhZ0F3SUJBZ0lCRERBS0JnZ3Foa2pPUFFRREF6QkZNUXN3Q1FZRFZRUUdFd0pWVXpFTE1Ba0dBMVVFQ0F3Q1EwRXhFakFRQmdOVkJBY01DVU4xY0dWeWRHbHViekVWTUJNR0ExVUVDZ3dNU1c1MFpYSnRaV1JwWVhSbE1CNFhEVEl6TURFd05USXhNekV6TkZvWERUTXpNREV3TVRJeE16RXpORm93UFRFTE1Ba0dBMVVFQmhNQ1ZWTXhDekFKQmdOVkJBZ01Ba05CTVJJd0VBWURWUVFIREFsRGRYQmxjblJwYm04eERUQUxCZ05WQkFvTUJFeGxZV1l3V1RBVEJnY3Foa2pPUFFJQkJnZ3Foa2pPUFFNQkJ3TkNBQVRpdFlIRWFZVnVjOGc5QWpUT3dFck12R3lQeWtQYStwdXZUSThoSlRIWlpETEdhczJxWDErRXJ4Z1FUSmdWWHY3Nm5tTGhoUkpIK2oyNUFpQUk4aUdzb3k4d0xUQUpCZ05WSFJNRUFqQUFNQTRHQTFVZER3RUIvd1FFQXdJSGdEQVFCZ29xaGtpRzkyTmtCZ3NCQkFJRkFEQUtCZ2dxaGtqT1BRUURBd05JQURCRkFpQlg0YytUMEZwNW5KNVFSQ2xSZnU1UFNCeVJ2TlB0dWFUc2swdlBCM1dBSUFJaEFOZ2FhdUFqL1lQOXMwQWtFaHlKaHhRTy82UTJ6b3VaK0gxQ0lPZWhuTXpRIiwiTUlJQm56Q0NBVVdnQXdJQkFnSUJDekFLQmdncWhrak9QUVFEQXpBMk1Rc3dDUVlEVlFRR0V3SlZVekVUTUJFR0ExVUVDQXdLUTJGc2FXWnZjbTVwWVRFU01CQUdBMVVFQnd3SlEzVndaWEowYVc1dk1CNFhEVEl6TURFd05USXhNekV3TlZvWERUTXpNREV3TVRJeE16RXdOVm93UlRFTE1Ba0dBMVVFQmhNQ1ZWTXhDekFKQmdOVkJBZ01Ba05CTVJJd0VBWURWUVFIREFsRGRYQmxjblJwYm04eEZUQVRCZ05WQkFvTURFbHVkR1Z5YldWa2FXRjBaVEJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUFCQlVONVY5cktqZlJpTUFJb2pFQTBBdjVNcDBvRitPMGNMNGd6clRGMTc4aW5VSHVnajdFdDQ2TnJrUTdoS2dNVm5qb2dxNDVRMXJNcytjTUhWTklMV3FqTlRBek1BOEdBMVVkRXdRSU1BWUJBZjhDQVFBd0RnWURWUjBQQVFIL0JBUURBZ0VHTUJBR0NpcUdTSWIzWTJRR0FnRUVBZ1VBTUFvR0NDcUdTTTQ5QkFNREEwZ0FNRVVDSVFDbXNJS1lzNDF1bGxzc0hYNHJWdmVVVDBaN0lzNS9oTEsxbEZQVHR1bjNoQUlnYzIrMlJHNStnTmNGVmNzK1hKZUVsNEdaK29qbDNST09tbGwreWU3ZHluUT0iLCJNSUlCZ2pDQ0FTbWdBd0lCQWdJSkFMVWM1QUxpSDVwYk1Bb0dDQ3FHU000OUJBTURNRFl4Q3pBSkJnTlZCQVlUQWxWVE1STXdFUVlEVlFRSURBcERZV3hwWm05eWJtbGhNUkl3RUFZRFZRUUhEQWxEZFhCbGNuUnBibTh3SGhjTk1qTXdNVEExTWpFek1ESXlXaGNOTXpNd01UQXlNakV6TURJeVdqQTJNUXN3Q1FZRFZRUUdFd0pWVXpFVE1CRUdBMVVFQ0F3S1EyRnNhV1p2Y201cFlURVNNQkFHQTFVRUJ3d0pRM1Z3WlhKMGFXNXZNRmt3RXdZSEtvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUVjKy9CbCtnb3NwbzZ0ZjlaN2lvNXRkS2RybE4xWWRWbnFFaEVEWERTaHpkQUpQUWlqYW1YSU1IZjh4V1dUYTF6Z29ZVHhPS3BidUp0RHBsejFYcmlUYU1nTUI0d0RBWURWUjBUQkFVd0F3RUIvekFPQmdOVkhROEJBZjhFQkFNQ0FRWXdDZ1lJS29aSXpqMEVBd01EUndBd1JBSWdlbVdRWG5NQWRUYWQySkRKV25nOVU0dUJCTDVtQTdXSTA1SDdvSDdjNmlRQ0lIaVJxTWpOZnpVQXlpdTloNnJPVS9LK2lUUjBJLzNZL05TV3NYSFgrYWNjIl19.eyJkYXRhIjp7ImJ1bmRsZUlkIjoiY29tLmV4YW1wbGUifSwibm90aWZpY2F0aW9uVVVJRCI6IjlhZDU2YmQyLTBiYzYtNDJlMC1hZjI0LWZkOTk2ZDg3YTFlNiIsIm5vdGlmaWNhdGlvblR5cGUiOiJURVNUIn0.1TFhjDR4WwQJNgizVGYXz3WE3ajxTdH1wKLQQ71MtrkadSxxOo3yPo_6L9Z03unIU7YK-NRNzSIb5bh5WqTprQ";
328 const WRONG_BUNDLE_ID: &str = "eyJ4NWMiOlsiTUlJQm9EQ0NBVWFnQXdJQkFnSUJEREFLQmdncWhrak9QUVFEQXpCRk1Rc3dDUVlEVlFRR0V3SlZVekVMTUFrR0ExVUVDQXdDUTBFeEVqQVFCZ05WQkFjTUNVTjFjR1Z5ZEdsdWJ6RVZNQk1HQTFVRUNnd01TVzUwWlhKdFpXUnBZWFJsTUI0WERUSXpNREV3TlRJeE16RXpORm9YRFRNek1ERXdNVEl4TXpFek5Gb3dQVEVMTUFrR0ExVUVCaE1DVlZNeEN6QUpCZ05WQkFnTUFrTkJNUkl3RUFZRFZRUUhEQWxEZFhCbGNuUnBibTh4RFRBTEJnTlZCQW9NQkV4bFlXWXdXVEFUQmdjcWhrak9QUUlCQmdncWhrak9QUU1CQndOQ0FBVGl0WUhFYVlWdWM4ZzlBalRPd0VyTXZHeVB5a1BhK3B1dlRJOGhKVEhaWkRMR2FzMnFYMStFcnhnUVRKZ1ZYdjc2bm1MaGhSSkgrajI1QWlBSThpR3NveTh3TFRBSkJnTlZIUk1FQWpBQU1BNEdBMVVkRHdFQi93UUVBd0lIZ0RBUUJnb3Foa2lHOTJOa0Jnc0JCQUlGQURBS0JnZ3Foa2pPUFFRREF3TklBREJGQWlCWDRjK1QwRnA1bko1UVJDbFJmdTVQU0J5UnZOUHR1YVRzazB2UEIzV0FJQUloQU5nYWF1QWovWVA5czBBa0VoeUpoeFFPLzZRMnpvdVorSDFDSU9laG5NelEiLCJNSUlCbnpDQ0FVV2dBd0lCQWdJQkN6QUtCZ2dxaGtqT1BRUURBekEyTVFzd0NRWURWUVFHRXdKVlV6RVRNQkVHQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVTTUJBR0ExVUVCd3dKUTNWd1pYSjBhVzV2TUI0WERUSXpNREV3TlRJeE16RXdOVm9YRFRNek1ERXdNVEl4TXpFd05Wb3dSVEVMTUFrR0ExVUVCaE1DVlZNeEN6QUpCZ05WQkFnTUFrTkJNUkl3RUFZRFZRUUhEQWxEZFhCbGNuUnBibTh4RlRBVEJnTlZCQW9NREVsdWRHVnliV1ZrYVdGMFpUQlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDlBd0VIQTBJQUJCVU41VjlyS2pmUmlNQUlvakVBMEF2NU1wMG9GK08wY0w0Z3pyVEYxNzhpblVIdWdqN0V0NDZOcmtRN2hLZ01WbmpvZ3E0NVExck1zK2NNSFZOSUxXcWpOVEF6TUE4R0ExVWRFd1FJTUFZQkFmOENBUUF3RGdZRFZSMFBBUUgvQkFRREFnRUdNQkFHQ2lxR1NJYjNZMlFHQWdFRUFnVUFNQW9HQ0NxR1NNNDlCQU1EQTBnQU1FVUNJUUNtc0lLWXM0MXVsbHNzSFg0clZ2ZVVUMFo3SXM1L2hMSzFsRlBUdHVuM2hBSWdjMisyUkc1K2dOY0ZWY3MrWEplRWw0R1orb2psM1JPT21sbCt5ZTdkeW5RPSIsIk1JSUJnakNDQVNtZ0F3SUJBZ0lKQUxVYzVBTGlINXBiTUFvR0NDcUdTTTQ5QkFNRE1EWXhDekFKQmdOVkJBWVRBbFZUTVJNd0VRWURWUVFJREFwRFlXeHBabTl5Ym1saE1SSXdFQVlEVlFRSERBbERkWEJsY25ScGJtOHdIaGNOTWpNd01UQTFNakV6TURJeVdoY05Nek13TVRBeU1qRXpNREl5V2pBMk1Rc3dDUVlEVlFRR0V3SlZVekVUTUJFR0ExVUVDQXdLUTJGc2FXWnZjbTVwWVRFU01CQUdBMVVFQnd3SlEzVndaWEowYVc1dk1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRWMrL0JsK2dvc3BvNnRmOVo3aW81dGRLZHJsTjFZZFZucUVoRURYRFNoemRBSlBRaWphbVhJTUhmOHhXV1RhMXpnb1lUeE9LcGJ1SnREcGx6MVhyaVRhTWdNQjR3REFZRFZSMFRCQVV3QXdFQi96QU9CZ05WSFE4QkFmOEVCQU1DQVFZd0NnWUlLb1pJemowRUF3TURSd0F3UkFJZ2VtV1FYbk1BZFRhZDJKREpXbmc5VTR1QkJMNW1BN1dJMDVIN29IN2M2aVFDSUhpUnFNak5melVBeWl1OWg2ck9VL0sraVRSMEkvM1kvTlNXc1hIWCthY2MiXSwidHlwIjoiSldUIiwiYWxnIjoiRVMyNTYifQ.eyJkYXRhIjp7ImJ1bmRsZUlkIjoiY29tLmV4YW1wbGUud3JvbmcifSwibm90aWZpY2F0aW9uVVVJRCI6IjlhZDU2YmQyLTBiYzYtNDJlMC1hZjI0LWZkOTk2ZDg3YTFlNiIsIm5vdGlmaWNhdGlvblR5cGUiOiJURVNUIn0.WWE31hTB_mcv2O_lf-xI-MNY3d8txc0MzpqFx4QnYDfFIxB95Lo2Fm3r46YSjLLdL7xCWdEJrJP5bHgRCejAGg";
329 const RENEWAL_INFO: &str = "eyJ4NWMiOlsiTUlJQm9EQ0NBVWFnQXdJQkFnSUJEREFLQmdncWhrak9QUVFEQXpCRk1Rc3dDUVlEVlFRR0V3SlZVekVMTUFrR0ExVUVDQXdDUTBFeEVqQVFCZ05WQkFjTUNVTjFjR1Z5ZEdsdWJ6RVZNQk1HQTFVRUNnd01TVzUwWlhKdFpXUnBZWFJsTUI0WERUSXpNREV3TlRJeE16RXpORm9YRFRNek1ERXdNVEl4TXpFek5Gb3dQVEVMTUFrR0ExVUVCaE1DVlZNeEN6QUpCZ05WQkFnTUFrTkJNUkl3RUFZRFZRUUhEQWxEZFhCbGNuUnBibTh4RFRBTEJnTlZCQW9NQkV4bFlXWXdXVEFUQmdjcWhrak9QUUlCQmdncWhrak9QUU1CQndOQ0FBVGl0WUhFYVlWdWM4ZzlBalRPd0VyTXZHeVB5a1BhK3B1dlRJOGhKVEhaWkRMR2FzMnFYMStFcnhnUVRKZ1ZYdjc2bm1MaGhSSkgrajI1QWlBSThpR3NveTh3TFRBSkJnTlZIUk1FQWpBQU1BNEdBMVVkRHdFQi93UUVBd0lIZ0RBUUJnb3Foa2lHOTJOa0Jnc0JCQUlGQURBS0JnZ3Foa2pPUFFRREF3TklBREJGQWlCWDRjK1QwRnA1bko1UVJDbFJmdTVQU0J5UnZOUHR1YVRzazB2UEIzV0FJQUloQU5nYWF1QWovWVA5czBBa0VoeUpoeFFPLzZRMnpvdVorSDFDSU9laG5NelEiLCJNSUlCbnpDQ0FVV2dBd0lCQWdJQkN6QUtCZ2dxaGtqT1BRUURBekEyTVFzd0NRWURWUVFHRXdKVlV6RVRNQkVHQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVTTUJBR0ExVUVCd3dKUTNWd1pYSjBhVzV2TUI0WERUSXpNREV3TlRJeE16RXdOVm9YRFRNek1ERXdNVEl4TXpFd05Wb3dSVEVMTUFrR0ExVUVCaE1DVlZNeEN6QUpCZ05WQkFnTUFrTkJNUkl3RUFZRFZRUUhEQWxEZFhCbGNuUnBibTh4RlRBVEJnTlZCQW9NREVsdWRHVnliV1ZrYVdGMFpUQlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDlBd0VIQTBJQUJCVU41VjlyS2pmUmlNQUlvakVBMEF2NU1wMG9GK08wY0w0Z3pyVEYxNzhpblVIdWdqN0V0NDZOcmtRN2hLZ01WbmpvZ3E0NVExck1zK2NNSFZOSUxXcWpOVEF6TUE4R0ExVWRFd1FJTUFZQkFmOENBUUF3RGdZRFZSMFBBUUgvQkFRREFnRUdNQkFHQ2lxR1NJYjNZMlFHQWdFRUFnVUFNQW9HQ0NxR1NNNDlCQU1EQTBnQU1FVUNJUUNtc0lLWXM0MXVsbHNzSFg0clZ2ZVVUMFo3SXM1L2hMSzFsRlBUdHVuM2hBSWdjMisyUkc1K2dOY0ZWY3MrWEplRWw0R1orb2psM1JPT21sbCt5ZTdkeW5RPSIsIk1JSUJnakNDQVNtZ0F3SUJBZ0lKQUxVYzVBTGlINXBiTUFvR0NDcUdTTTQ5QkFNRE1EWXhDekFKQmdOVkJBWVRBbFZUTVJNd0VRWURWUVFJREFwRFlXeHBabTl5Ym1saE1SSXdFQVlEVlFRSERBbERkWEJsY25ScGJtOHdIaGNOTWpNd01UQTFNakV6TURJeVdoY05Nek13TVRBeU1qRXpNREl5V2pBMk1Rc3dDUVlEVlFRR0V3SlZVekVUTUJFR0ExVUVDQXdLUTJGc2FXWnZjbTVwWVRFU01CQUdBMVVFQnd3SlEzVndaWEowYVc1dk1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRWMrL0JsK2dvc3BvNnRmOVo3aW81dGRLZHJsTjFZZFZucUVoRURYRFNoemRBSlBRaWphbVhJTUhmOHhXV1RhMXpnb1lUeE9LcGJ1SnREcGx6MVhyaVRhTWdNQjR3REFZRFZSMFRCQVV3QXdFQi96QU9CZ05WSFE4QkFmOEVCQU1DQVFZd0NnWUlLb1pJemowRUF3TURSd0F3UkFJZ2VtV1FYbk1BZFRhZDJKREpXbmc5VTR1QkJMNW1BN1dJMDVIN29IN2M2aVFDSUhpUnFNak5melVBeWl1OWg2ck9VL0sraVRSMEkvM1kvTlNXc1hIWCthY2MiXSwidHlwIjoiSldUIiwiYWxnIjoiRVMyNTYifQ.eyJlbnZpcm9ubWVudCI6IlNhbmRib3giLCJzaWduZWREYXRlIjoxNjcyOTU2MTU0MDAwfQ.FbK2OL-t6l4892W7fzWyus_g9mIl2CzWLbVt7Kgcnt6zzVulF8bzovgpe0v_y490blROGixy8KDoe2dSU53-Xw";
330 const TRANSACTION_INFO: &str = "eyJ4NWMiOlsiTUlJQm9EQ0NBVWFnQXdJQkFnSUJDekFLQmdncWhrak9QUVFEQWpCTk1Rc3dDUVlEVlFRR0V3SlZVekVUTUJFR0ExVUVDQXdLUTJGc2FXWnZjbTVwWVRFU01CQUdBMVVFQnd3SlEzVndaWEowYVc1dk1SVXdFd1lEVlFRS0RBeEpiblJsY20xbFpHbGhkR1V3SGhjTk1qTXdNVEEwTVRZek56TXhXaGNOTXpJeE1qTXhNVFl6TnpNeFdqQkZNUXN3Q1FZRFZRUUdFd0pWVXpFVE1CRUdBMVVFQ0F3S1EyRnNhV1p2Y201cFlURVNNQkFHQTFVRUJ3d0pRM1Z3WlhKMGFXNXZNUTB3Q3dZRFZRUUtEQVJNWldGbU1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRTRyV0J4R21GYm5QSVBRSTB6c0JLekx4c2o4cEQydnFicjB5UElTVXgyV1F5eG1yTnFsOWZoSzhZRUV5WUZWNysrcDVpNFlVU1Ivbzl1UUlnQ1BJaHJLTWZNQjB3Q1FZRFZSMFRCQUl3QURBUUJnb3Foa2lHOTJOa0Jnc0JCQUlUQURBS0JnZ3Foa2pPUFFRREFnTklBREJGQWlFQWtpRVprb0ZNa2o0Z1huK1E5alhRWk1qWjJnbmpaM2FNOE5ZcmdmVFVpdlFDSURKWVowRmFMZTduU0lVMkxXTFRrNXRYVENjNEU4R0pTWWYvc1lSeEVGaWUiLCJNSUlCbHpDQ0FUMmdBd0lCQWdJQkJqQUtCZ2dxaGtqT1BRUURBakEyTVFzd0NRWURWUVFHRXdKVlV6RVRNQkVHQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVTTUJBR0ExVUVCd3dKUTNWd1pYSjBhVzV2TUI0WERUSXpNREV3TkRFMk1qWXdNVm9YRFRNeU1USXpNVEUyTWpZd01Wb3dUVEVMTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFnTUNrTmhiR2xtYjNKdWFXRXhFakFRQmdOVkJBY01DVU4xY0dWeWRHbHViekVWTUJNR0ExVUVDZ3dNU1c1MFpYSnRaV1JwWVhSbE1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRUZRM2xYMnNxTjlHSXdBaWlNUURRQy9reW5TZ1g0N1J3dmlET3RNWFh2eUtkUWU2Q1BzUzNqbzJ1UkR1RXFBeFdlT2lDcmpsRFdzeXo1d3dkVTBndGFxTWxNQ013RHdZRFZSMFRCQWd3QmdFQi93SUJBREFRQmdvcWhraUc5Mk5rQmdJQkJBSVRBREFLQmdncWhrak9QUVFEQWdOSUFEQkZBaUVBdm56TWNWMjY4Y1JiMS9GcHlWMUVoVDNXRnZPenJCVVdQNi9Ub1RoRmF2TUNJRmJhNXQ2WUt5MFIySkR0eHF0T2pKeTY2bDZWN2QvUHJBRE5wa21JUFcraSIsIk1JSUJYRENDQVFJQ0NRQ2ZqVFVHTERuUjlqQUtCZ2dxaGtqT1BRUURBekEyTVFzd0NRWURWUVFHRXdKVlV6RVRNQkVHQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVTTUJBR0ExVUVCd3dKUTNWd1pYSjBhVzV2TUI0WERUSXpNREV3TkRFMk1qQXpNbG9YRFRNek1ERXdNVEUyTWpBek1sb3dOakVMTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFnTUNrTmhiR2xtYjNKdWFXRXhFakFRQmdOVkJBY01DVU4xY0dWeWRHbHViekJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUFCSFB2d1pmb0tMS2FPclgvV2U0cU9iWFNuYTVUZFdIVlo2aElSQTF3MG9jM1FDVDBJbzJwbHlEQjMvTVZsazJ0YzRLR0U4VGlxVzdpYlE2WmM5VjY0azB3Q2dZSUtvWkl6ajBFQXdNRFNBQXdSUUloQU1USGhXdGJBUU4waFN4SVhjUDRDS3JEQ0gvZ3N4V3B4NmpUWkxUZVorRlBBaUIzNW53azVxMHpjSXBlZnZZSjBNVS95R0dIU1dlejBicTBwRFlVTy9ubUR3PT0iXSwidHlwIjoiSldUIiwiYWxnIjoiRVMyNTYifQ.eyJlbnZpcm9ubWVudCI6IlNhbmRib3giLCJidW5kbGVJZCI6ImNvbS5leGFtcGxlIiwic2lnbmVkRGF0ZSI6MTY3Mjk1NjE1NDAwMH0.PnHWpeIJZ8f2Q218NSGLo_aR0IBEJvC6PxmxKXh-qfYTrZccx2suGl223OSNAX78e4Ylf2yJCG2N-FfU-NIhZQ";
331 const XCODE_BUNDLE_ID: &str = "com.example.naturelab.backyardbirds.example";
332
333 #[test]
334 fn test_app_store_server_notification_decoding() {
335 let verifier = get_signed_data_verifier(Environment::Sandbox, "com.example", None);
336 let notification = verifier
337 .verify_and_decode_notification(TEST_NOTIFICATION)
338 .unwrap();
339 assert_eq!(notification.notification_type, NotificationTypeV2::Test);
340 }
341
342 #[test]
343 fn test_app_store_server_notification_decoding_production() {
344 let verifier = get_signed_data_verifier(Environment::Production, "com.example", None);
345 let error = verifier
346 .verify_and_decode_notification(TEST_NOTIFICATION)
347 .err()
348 .unwrap();
349
350 assert_eq!(error, SignedDataVerifierError::InvalidEnvironment);
351 }
352
353 #[test]
354 fn test_missing_x5c_header() {
355 let verifier = get_signed_data_verifier(Environment::Sandbox, "com.example", None);
356 let result = verifier.verify_and_decode_notification(MISSING_X5C_HEADER_CLAIM);
357 assert_eq!(
358 result.err().unwrap(),
359 SignedDataVerifierError::VerificationFailure
360 );
361 }
362
363 #[test]
364 fn test_wrong_bundle_id_for_server_notification() {
365 let verifier = get_signed_data_verifier(Environment::Sandbox, "com.example", None);
366 let result = verifier.verify_and_decode_notification(WRONG_BUNDLE_ID);
367 assert_eq!(
368 result.err().unwrap(),
369 SignedDataVerifierError::InvalidAppIdentifier
370 );
371 }
372
373 #[test]
374 fn test_wrong_app_apple_id_for_server_notification() {
375 let verifier = get_signed_data_verifier(Environment::Production, "com.example", Some(1235));
376 let result = verifier.verify_and_decode_notification(TEST_NOTIFICATION);
377 assert_eq!(
378 result.err().unwrap(),
379 SignedDataVerifierError::InvalidAppIdentifier
380 );
381 }
382
383 #[test]
384 fn test_renewal_info_decoding() {
385 let verifier = get_signed_data_verifier(Environment::Sandbox, "com.example", None);
386 let renewal_info = verifier
387 .verify_and_decode_renewal_info(RENEWAL_INFO)
388 .unwrap();
389 assert_eq!(renewal_info.environment, Some(Environment::Sandbox));
390 }
406
407 #[test]
408 fn test_external_purchase_token_notification_decoding() {
409 let signed_notification =
410 create_signed_data_from_json("resources/models/signedExternalPurchaseTokenNotification.json");
411
412 let signed_data_verifier = get_signed_data_verifier(Environment::LocalTesting, "com.example", Some(55555));
413
414 match signed_data_verifier.verify_and_decode_notification(&signed_notification) {
415 Ok(notification) => {
416
417 assert_eq!(NotificationTypeV2::ExternalPurchaseToken, notification.notification_type);
418 assert_eq!(Subtype::Unreported, notification.subtype.expect("Expect subtype"));
419 assert_eq!("002e14d5-51f5-4503-b5a8-c3a1af68eb20", ¬ification.notification_uuid);
420 assert_eq!("2.0", ¬ification.version.expect("Expect version"));
421 assert_eq!(
422 1698148900,
423 notification.signed_date.expect("Expect signed_date").timestamp()
424 );
425 assert!(notification.data.is_none());
426 assert!(notification.summary.is_none());
427 assert!(notification.external_purchase_token.is_some());
428
429 if let Some(external_purchase_token) = notification.external_purchase_token {
430 assert_eq!("b2158121-7af9-49d4-9561-1f588205523e", &external_purchase_token.external_purchase_id.expect("Expect external_purchase_id"));
431 assert_eq!(1698148950, external_purchase_token.token_creation_date.unwrap().timestamp());
432 assert_eq!(55555, external_purchase_token.app_apple_id.unwrap());
433 assert_eq!("com.example", &external_purchase_token.bundle_id.unwrap());
434 } else {
435 panic!("External purchase token is expected to be Some, but it was None");
436 }
437 }
438 Err(err) => {
439 panic!("Failed to verify and decode app transaction: {:?}", err)
440 }
441 }
442 }
443
444 #[test]
445 fn test_external_purchase_token_sanbox_notification_decoding() {
446 let signed_notification =
447 create_signed_data_from_json("resources/models/signedExternalPurchaseTokenSandboxNotification.json");
448
449 let signed_data_verifier = get_signed_data_verifier(Environment::LocalTesting, "com.example", Some(55555));
450
451 match signed_data_verifier.verify_and_decode_notification(&signed_notification) {
452 Ok(notification) => {
453
454 assert_eq!(NotificationTypeV2::ExternalPurchaseToken, notification.notification_type);
455 assert_eq!(Subtype::Unreported, notification.subtype.expect("Expect subtype"));
456 assert_eq!("002e14d5-51f5-4503-b5a8-c3a1af68eb20", ¬ification.notification_uuid);
457 assert_eq!("2.0", ¬ification.version.expect("Expect version"));
458 assert_eq!(
459 1698148900,
460 notification.signed_date.expect("Expect signed_date").timestamp()
461 );
462 assert!(notification.data.is_none());
463 assert!(notification.summary.is_none());
464 assert!(notification.external_purchase_token.is_some());
465
466 if let Some(external_purchase_token) = notification.external_purchase_token {
467 assert_eq!("SANDBOX_b2158121-7af9-49d4-9561-1f588205523e", &external_purchase_token.external_purchase_id.expect("Expect external_purchase_id"));
468 assert_eq!(1698148950, external_purchase_token.token_creation_date.unwrap().timestamp());
469 assert_eq!(55555, external_purchase_token.app_apple_id.unwrap());
470 assert_eq!("com.example", &external_purchase_token.bundle_id.unwrap());
471 } else {
472 panic!("External purchase token is expected to be Some, but it was None");
473 }
474 }
475 Err(err) => {
476 panic!("Failed to verify and decode app transaction: {:?}", err)
477 }
478 }
479 }
480
481 #[test]
482 fn test_transaction_info_decoding() {
483 let verifier = get_signed_data_verifier(Environment::Sandbox, "com.example", None);
484 let notification = verifier
485 .verify_and_decode_signed_transaction(TRANSACTION_INFO)
486 .unwrap();
487 assert_eq!(notification.environment, Some(Environment::Sandbox));
488 }
489
490 #[test]
491 fn test_malformed_jwt_with_too_many_parts() {
492 let verifier = get_signed_data_verifier(Environment::Sandbox, "com.example", None);
493 let result = verifier.verify_and_decode_notification("a.b.c.d");
494 assert!(result
495 .err()
496 .unwrap()
497 .to_string()
498 .contains("InternalJWTError"));
499 }
500
501 #[test]
502 fn test_malformed_jwt_with_malformed_data() {
503 let verifier = get_signed_data_verifier(Environment::Sandbox, "com.example", None);
504 let result = verifier.verify_and_decode_notification("a.b.c");
505 assert!(result
506 .err()
507 .unwrap()
508 .to_string()
509 .contains("InternalJWTError"));
510 }
511
512 fn get_signed_data_verifier(
513 environment: Environment,
514 bundle_id: &str,
515 app_apple_id: Option<i64>,
516 ) -> SignedDataVerifier {
517 let verifier = SignedDataVerifier::new(
518 vec![ROOT_CA_BASE64_ENCODED.as_der_bytes().unwrap()],
519 environment,
520 bundle_id.to_string(),
521 app_apple_id.or(Some(1234)),
522 );
523
524 verifier
525 }
526
527 #[test]
528 fn test_decoded_payloads_app_transaction_decoding() {
529 let signed_app_transaction = create_signed_data_from_json("resources/models/appTransaction.json");
530
531 let signed_data_verifier = get_default_signed_data_verifier();
532
533 match signed_data_verifier.verify_and_decode_app_transaction(&signed_app_transaction) {
534 Ok(app_transaction) => {
535 assert_eq!(
536 Some(&Environment::LocalTesting),
537 app_transaction.receipt_type.as_ref()
538 );
539 assert_eq!(
540 531412,
541 app_transaction.app_apple_id.expect("Expect app_apple_id")
542 );
543 assert_eq!(
544 "com.example",
545 app_transaction.bundle_id.expect("Expect bundle_id")
546 );
547 assert_eq!(
548 "1.2.3",
549 app_transaction
550 .application_version
551 .expect("Expect application_version")
552 );
553 assert_eq!(
554 512,
555 app_transaction
556 .version_external_identifier
557 .expect("Expect version_external_identifier")
558 );
559 assert_eq!(
560 1698148900,
561 app_transaction
562 .receipt_creation_date
563 .expect("Expect receipt_creation_date")
564 .timestamp()
565 );
566 assert_eq!(
567 1698148800,
568 app_transaction
569 .original_purchase_date
570 .expect("Expect original_purchase_date")
571 .timestamp()
572 );
573 assert_eq!(
574 "1.1.2",
575 app_transaction
576 .original_application_version
577 .expect("Expect original_application_version")
578 );
579 assert_eq!(
580 "device_verification_value",
581 app_transaction
582 .device_verification
583 .expect("Expect device_verification")
584 );
585 assert_eq!(
586 "48ccfa42-7431-4f22-9908-7e88983e105a",
587 app_transaction
588 .device_verification_nonce
589 .expect("Expect device_verification_nonce")
590 .to_string()
591 );
592 assert_eq!(
593 1698148700,
594 app_transaction
595 .preorder_date
596 .expect("Expect preorder_date")
597 .timestamp()
598 );
599 assert_eq!(
600 "71134",
601 app_transaction
602 .app_transaction_id
603 .expect("Expect app_transaction_id")
604 .to_string()
605 );
606 assert_eq!(
607 PurchasePlatform::IOS,
608 app_transaction
609 .original_platform
610 .expect("Expect original_platform")
611 );
612 }
613 Err(err) => panic!("Failed to verify and decode app transaction: {:?}", err),
614 }
615 }
616
617 #[test]
618 fn test_decoded_payloads_transaction_decoding() {
619 let signed_transaction = create_signed_data_from_json("resources/models/signedTransaction.json");
620
621 let signed_data_verifier = get_default_signed_data_verifier();
622
623 match signed_data_verifier.verify_and_decode_signed_transaction(&signed_transaction) {
624 Ok(transaction) => {
625 assert_eq!(
626 "12345",
627 transaction
628 .original_transaction_id
629 .as_deref()
630 .expect("Expect original_transaction_id")
631 );
632 assert_eq!(
633 "23456",
634 transaction
635 .transaction_id
636 .as_deref()
637 .expect("Expect transaction_id")
638 );
639 assert_eq!(
640 "34343",
641 transaction
642 .web_order_line_item_id
643 .as_deref()
644 .expect("Expect web_order_line_item_id")
645 );
646 assert_eq!(
647 "com.example",
648 transaction.bundle_id.as_deref().expect("Expect bundle_id")
649 );
650 assert_eq!(
651 "com.example.product",
652 transaction
653 .product_id
654 .as_deref()
655 .expect("Expect product_id")
656 );
657 assert_eq!(
658 "55555",
659 transaction
660 .subscription_group_identifier
661 .as_deref()
662 .expect("Expect subscription_group_identifier")
663 );
664 assert_eq!(
665 1698148800,
666 transaction
667 .original_purchase_date
668 .expect("Expect original_purchase_date")
669 .timestamp()
670 );
671 assert_eq!(
672 1698148900,
673 transaction
674 .purchase_date
675 .expect("Expect purchase_date")
676 .timestamp()
677 );
678 assert_eq!(
679 1698148950,
680 transaction
681 .revocation_date
682 .expect("Expect revocation_date")
683 .timestamp()
684 );
685 assert_eq!(
686 1698149000,
687 transaction
688 .expires_date
689 .expect("Expect expires_date")
690 .timestamp()
691 );
692 assert_eq!(1, transaction.quantity.expect("Expect quantity"));
693 assert_eq!(
694 ProductType::AutoRenewableSubscription,
695 transaction.r#type.expect("Expect type")
696 );
697 assert_eq!(
698 "7e3fb20b-4cdb-47cc-936d-99d65f608138",
699 transaction
700 .app_account_token
701 .expect("Expect app_account_token")
702 .to_string()
703 );
704 assert_eq!(
705 InAppOwnershipType::Purchased,
706 transaction
707 .in_app_ownership_type
708 .expect("Expect in_app_ownership_type")
709 );
710 assert_eq!(
711 1698148900,
712 transaction
713 .signed_date
714 .expect("Expect signed_date")
715 .timestamp()
716 );
717 assert_eq!(
718 RevocationReason::RefundedDueToIssue,
719 transaction
720 .revocation_reason
721 .expect("Expect revocation_reason")
722 );
723 assert_eq!(
724 "abc.123",
725 transaction
726 .offer_identifier
727 .as_deref()
728 .expect("Expect offer_identifier")
729 );
730 assert!(transaction.is_upgraded.unwrap_or_default());
731 assert_eq!(
732 OfferType::IntroductoryOffer,
733 transaction.offer_type.expect("Expect offer_type")
734 );
735 assert_eq!(
736 "USA",
737 transaction
738 .storefront
739 .as_deref()
740 .expect("Expect storefront")
741 );
742 assert_eq!(
743 "143441",
744 transaction
745 .storefront_id
746 .as_deref()
747 .expect("Expect storefront_id")
748 );
749 assert_eq!(
750 TransactionReason::Purchase,
751 transaction
752 .transaction_reason
753 .expect("Expect transaction_reason")
754 );
755 assert_eq!(
756 Environment::LocalTesting,
757 transaction.environment.expect("Expect environment")
758 );
759 assert_eq!(10990, transaction.price.expect("Expect price"));
760 assert_eq!(
761 "USD",
762 transaction.currency.as_deref().expect("Expect currency")
763 );
764 assert_eq!(
765 OfferDiscountType::PayAsYouGo,
766 transaction
767 .offer_discount_type
768 .expect("Expect offer_discount_type")
769 );
770 assert_eq!(
771 "71134",
772 transaction
773 .app_transaction_id
774 .expect("Expect app_transaction_id")
775 .to_string()
776 );
777 assert_eq!(
778 "P1Y",
779 transaction
780 .offer_period
781 .expect("Expect offer_period")
782 .to_string()
783 );
784 }
785 Err(err) => panic!("Failed to verify and decode signed transaction: {:?}", err),
786 }
787 }
788
789 #[test]
790 fn test_decoded_payloads_renewal_info_decoding() {
791 let signed_renewal_info = create_signed_data_from_json("resources/models/signedRenewalInfo.json");
792
793 let signed_data_verifier = get_default_signed_data_verifier();
794
795 match signed_data_verifier.verify_and_decode_renewal_info(&signed_renewal_info) {
796 Ok(renewal_info) => {
797 assert_eq!(
798 ExpirationIntent::CustomerCancelled,
799 renewal_info
800 .expiration_intent
801 .expect("Expect expiration_intent")
802 );
803 assert_eq!(
804 "12345",
805 renewal_info
806 .original_transaction_id
807 .as_deref()
808 .expect("Expect original_transaction_id")
809 );
810 assert_eq!(
811 "com.example.product.2",
812 renewal_info
813 .auto_renew_product_id
814 .as_deref()
815 .expect("Expect auto_renew_product_id")
816 );
817 assert_eq!(
818 "com.example.product",
819 renewal_info
820 .product_id
821 .as_deref()
822 .expect("Expect product_id")
823 );
824 assert_eq!(
825 AutoRenewStatus::On,
826 renewal_info
827 .auto_renew_status
828 .expect("Expect auto_renew_status")
829 );
830 assert!(renewal_info.is_in_billing_retry_period.unwrap_or_default());
831 assert_eq!(
832 PriceIncreaseStatus::CustomerHasNotResponded,
833 renewal_info
834 .price_increase_status
835 .expect("Expect price_increase_status")
836 );
837 assert_eq!(
838 1698148900,
839 renewal_info
840 .grace_period_expires_date
841 .expect("Expect grace_period_expires_date")
842 .timestamp()
843 );
844 assert_eq!(
845 OfferType::PromotionalOffer,
846 renewal_info.offer_type.expect("Expect offer_type")
847 );
848 assert_eq!(
849 "abc.123",
850 renewal_info
851 .offer_identifier
852 .as_deref()
853 .expect("Expect offer_identifier")
854 );
855 assert_eq!(
856 1698148800,
857 renewal_info
858 .signed_date
859 .expect("Expect signed_date")
860 .timestamp()
861 );
862 assert_eq!(
863 Environment::LocalTesting,
864 renewal_info.environment.expect("Expect environment")
865 );
866 assert_eq!(
867 1698148800,
868 renewal_info
869 .recent_subscription_start_date
870 .expect("Expect recent_subscription_start_date")
871 .timestamp()
872 );
873 assert_eq!(
874 1698148850,
875 renewal_info
876 .renewal_date
877 .expect("Expect renewal_date")
878 .timestamp()
879 );
880 assert_eq!(
881 "71134",
882 renewal_info
883 .app_transaction_id
884 .expect("Expect app_transaction_id")
885 .to_string()
886 );
887 assert_eq!(
888 "P1Y",
889 renewal_info
890 .offer_period
891 .expect("Expect offer_period")
892 .to_string()
893 );
894 assert_eq!(
895 "7e3fb20b-4cdb-47cc-936d-99d65f608138",
896 renewal_info
897 .app_account_token
898 .expect("Expect app_account_token")
899 .to_string()
900 );
901 }
902 Err(err) => panic!("Failed to verify and decode renewal info: {:?}", err),
903 }
904 }
905
906 #[test]
907 fn test_decoded_payloads_notification_decoding() {
908 let signed_notification = create_signed_data_from_json("resources/models/signedNotification.json");
909
910 let signed_data_verifier = get_default_signed_data_verifier();
911
912 match signed_data_verifier.verify_and_decode_notification(&signed_notification) {
913 Ok(notification) => {
914 assert_eq!(
915 NotificationTypeV2::Subscribed,
916 notification.notification_type
917 );
918 assert_eq!(
919 Subtype::InitialBuy,
920 notification.subtype.expect("Expect subtype")
921 );
922 assert_eq!(
923 "002e14d5-51f5-4503-b5a8-c3a1af68eb20",
924 notification.notification_uuid
925 );
926 assert_eq!(
927 "2.0",
928 notification.version.as_deref().expect("Expect version")
929 );
930 assert_eq!(
931 1698148900,
932 notification
933 .signed_date
934 .expect("Expect signed_date")
935 .timestamp()
936 );
937 assert!(notification.data.is_some());
938 assert!(notification.summary.is_none());
939 assert!(notification.external_purchase_token.is_none());
940
941 if let Some(data) = notification.data {
942 assert_eq!(
943 Environment::LocalTesting,
944 data.environment.expect("Expect environment")
945 );
946 assert_eq!(41234, data.app_apple_id.expect("Expect app_apple_id"));
947 assert_eq!(
948 "com.example",
949 data.bundle_id.as_deref().expect("Expect bundle_id")
950 );
951 assert_eq!(
952 "1.2.3",
953 data.bundle_version
954 .as_deref()
955 .expect("Expect bundle_version")
956 );
957 assert_eq!(
958 "signed_transaction_info_value",
959 data.signed_transaction_info
960 .as_deref()
961 .expect("Expect signed_transaction_info")
962 );
963 assert_eq!(
964 "signed_renewal_info_value",
965 data.signed_renewal_info
966 .as_deref()
967 .expect("Expect signed_renewal_info")
968 );
969 assert_eq!(Status::Active, data.status.expect("Expect status"));
970 assert!(data.consumption_request_reason.is_none());
971 } else {
972 panic!("Data field is expected to be present in the notification");
973 }
974 }
975 Err(err) => panic!("Failed to verify and decode notification: {:?}", err),
976 }
977 }
978
979 #[test]
980 fn test_consumption_request_notification_decoding() {
981 let signed_notification = create_signed_data_from_json("resources/models/signedConsumptionRequestNotification.json");
982
983 let signed_data_verifier = get_default_signed_data_verifier();
984
985 match signed_data_verifier.verify_and_decode_notification(&signed_notification) {
986 Ok(notification) => {
987 assert_eq!(NotificationTypeV2::ConsumptionRequest, notification.notification_type);
988 assert!(notification.subtype.is_none());
989 assert_eq!("002e14d5-51f5-4503-b5a8-c3a1af68eb20", notification.notification_uuid);
990 assert_eq!("2.0", notification.version.unwrap());
991 assert_eq!(1698148900, notification.signed_date.unwrap().timestamp());
992 assert!(notification.data.is_some());
993 assert!(notification.summary.is_none());
994 assert!(notification.external_purchase_token.is_none());
995
996 if let Some(data) = notification.data {
997 assert_eq!(Environment::LocalTesting, data.environment.unwrap());
998 assert_eq!(41234, data.app_apple_id.unwrap());
999 assert_eq!("com.example", data.bundle_id.unwrap());
1000 assert_eq!("1.2.3", data.bundle_version.unwrap());
1001 assert_eq!("signed_transaction_info_value", data.signed_transaction_info.unwrap());
1002 assert_eq!("signed_renewal_info_value", data.signed_renewal_info.unwrap());
1003 assert_eq!(Status::Active, data.status.unwrap());
1004 assert_eq!(ConsumptionRequestReason::UnintendedPurchase, data.consumption_request_reason.unwrap());
1005 }
1006 }
1007 Err(err) => panic!(
1008 "Failed to verify and decode consumption request notification: {:?}",
1009 err
1010 ),
1011 }
1012 }
1013
1014 #[test]
1015 fn test_summary_notification_decoding() {
1016 let signed_summary_notification =
1017 create_signed_data_from_json("resources/models/signedSummaryNotification.json");
1018
1019 let signed_data_verifier = get_default_signed_data_verifier();
1020
1021 match signed_data_verifier.verify_and_decode_notification(&signed_summary_notification) {
1022 Ok(notification) => {
1023 assert_eq!(
1024 NotificationTypeV2::RenewalExtension,
1025 notification.notification_type
1026 );
1027 assert_eq!(
1028 Subtype::Summary,
1029 notification.subtype.expect("Expect subtype")
1030 );
1031 assert_eq!(
1032 "002e14d5-51f5-4503-b5a8-c3a1af68eb20",
1033 notification.notification_uuid
1034 );
1035 assert_eq!(
1036 "2.0",
1037 notification.version.as_deref().expect("Expect version")
1038 );
1039 assert_eq!(
1040 1698148900,
1041 notification
1042 .signed_date
1043 .expect("Expect signed_date")
1044 .timestamp()
1045 );
1046 assert!(notification.data.is_none());
1047 assert!(notification.summary.is_some());
1048 assert!(notification.external_purchase_token.is_none());
1049
1050 if let Some(summary) = notification.summary {
1051 assert_eq!(
1052 Environment::LocalTesting,
1053 summary.environment.expect("Expect environment")
1054 );
1055 assert_eq!(41234, summary.app_apple_id.expect("Expect app_apple_id"));
1056 assert_eq!(
1057 "com.example",
1058 summary.bundle_id.as_deref().expect("Expect bundle_id")
1059 );
1060 assert_eq!(
1061 "com.example.product",
1062 summary.product_id.as_deref().expect("Expect product_id")
1063 );
1064 assert_eq!(
1065 "efb27071-45a4-4aca-9854-2a1e9146f265",
1066 summary.request_identifier
1067 );
1068 assert_eq!(vec!["CAN", "USA", "MEX"], summary.storefront_country_codes);
1069 assert_eq!(5, summary.succeeded_count);
1070 assert_eq!(2, summary.failed_count);
1071 } else {
1072 panic!("Summary field is expected to be present in the notification");
1073 }
1074 }
1075 Err(err) => panic!(
1076 "Failed to verify and decode summary notification: {:?}",
1077 err
1078 ),
1079 }
1080 }
1081
1082 #[test]
1083 fn test_xcode_signed_app_transaction() {
1084 let verifier = get_signed_data_verifier(Environment::Xcode, XCODE_BUNDLE_ID, None);
1085 let encoded_app_transaction = fs::read_to_string("resources/xcode/xcode-signed-app-transaction").expect("Failed to read file");
1086
1087 if let Ok(app_transaction) = verifier.verify_and_decode_app_transaction(&encoded_app_transaction) {
1088 assert_eq!(XCODE_BUNDLE_ID, app_transaction.bundle_id.as_deref().expect("Expect bundle_id"));
1089 assert_eq!("1", app_transaction.application_version.as_deref().expect("Expect application_version"));
1090 assert_eq!(None, app_transaction.version_external_identifier);
1091 assert_eq!(-62135769600000, app_transaction.original_purchase_date.expect("Expect value").timestamp_millis());
1092 assert_eq!("1", app_transaction.original_application_version.as_deref().expect("Expect original_application_version"));
1093 assert_eq!("cYUsXc53EbYc0pOeXG5d6/31LGHeVGf84sqSN0OrJi5u/j2H89WWKgS8N0hMsMlf", app_transaction.device_verification.as_deref().expect("Expect device_verification"));
1094 assert_eq!("48c8b92d-ce0d-4229-bedf-e61b4f9cfc92", app_transaction.device_verification_nonce.expect("Expect device_verification_nonce").to_string());
1095 assert_eq!(None, app_transaction.preorder_date);
1096 assert_eq!(Environment::Xcode, app_transaction.receipt_type.unwrap());
1097 } else {
1098 panic!("Failed to verify and decode app transaction");
1099 }
1100 }
1101
1102 #[test]
1103 fn test_xcode_signed_transaction() {
1104 let verifier = get_signed_data_verifier(Environment::Xcode, XCODE_BUNDLE_ID, None);
1105 let encoded_app_transaction = fs::read_to_string("resources/xcode/xcode-signed-transaction").expect("Failed to read file");
1106
1107 if let Ok(transaction) = verifier.verify_and_decode_signed_transaction(&encoded_app_transaction) {
1108 assert_eq!("0", transaction.original_transaction_id.as_deref().expect("Expect original_transaction_id"));
1109 assert_eq!("0", transaction.transaction_id.as_deref().expect("Expect transaction_id"));
1110 assert_eq!("0", transaction.web_order_line_item_id.as_deref().expect("Expect web_order_line_item_id"));
1111 assert_eq!(XCODE_BUNDLE_ID, transaction.bundle_id.as_deref().expect("Expect bundle_id"));
1112 assert_eq!("pass.premium", transaction.product_id.as_deref().expect("Expect product_id"));
1113 assert_eq!("6F3A93AB", transaction.subscription_group_identifier.as_deref().expect("Expect subscription_group_identifier"));
1114 assert_eq!(1697679936049, transaction.purchase_date.unwrap().timestamp_millis());
1115 assert_eq!(1697679936049, transaction.original_purchase_date.unwrap().timestamp_millis());
1116 assert_eq!(1700358336049, transaction.expires_date.unwrap().timestamp_millis());
1117 assert_eq!(1, transaction.quantity.expect("Expect quantity"));
1118 assert_eq!(ProductType::AutoRenewableSubscription, transaction.r#type.expect("Expect type"));
1119 assert_eq!(None, transaction.app_account_token);
1120 assert_eq!(InAppOwnershipType::Purchased, transaction.in_app_ownership_type.expect("Expect in_app_ownership_type"));
1121 assert_eq!(1697679936056, transaction.signed_date.unwrap().timestamp_millis());
1122 assert_eq!(None, transaction.revocation_reason);
1123 assert_eq!(None, transaction.revocation_date);
1124 assert!(!transaction.is_upgraded.unwrap_or(false));
1125 assert_eq!(OfferType::IntroductoryOffer, transaction.offer_type.expect("Expect offer_type"));
1126 assert_eq!(None, transaction.offer_identifier);
1127 assert_eq!(Environment::Xcode, transaction.environment.expect("Expect environment"));
1128 assert_eq!("USA", transaction.storefront.expect("Expect storefront"));
1129 assert_eq!("143441", transaction.storefront_id.as_deref().expect("Expect storefront_id"));
1130 assert_eq!(TransactionReason::Purchase, transaction.transaction_reason.expect("Expect transaction_reason"));
1131 } else {
1132 panic!("Failed to verify and decode signed transaction");
1133 }
1134 }
1135
1136 #[test]
1137 fn test_xcode_signed_renewal_info() {
1138 let verifier = get_signed_data_verifier(Environment::Xcode, XCODE_BUNDLE_ID, None);
1139 let encoded_renewal_info = fs::read_to_string("resources/xcode/xcode-signed-renewal-info").expect("Failed to read file");
1140
1141 if let Ok(renewal_info) = verifier.verify_and_decode_renewal_info(&encoded_renewal_info) {
1142 assert_eq!(None, renewal_info.expiration_intent);
1143 assert_eq!("0", renewal_info.original_transaction_id.as_deref().expect("Expect original_transaction_id"));
1144 assert_eq!("pass.premium", renewal_info.auto_renew_product_id.as_deref().expect("Expect auto_renew_product_id"));
1145 assert_eq!("pass.premium", renewal_info.product_id.as_deref().expect("Expect product_id"));
1146 assert_eq!(AutoRenewStatus::On, renewal_info.auto_renew_status.expect("Expect auto_renew_status"));
1147 assert_eq!(None, renewal_info.is_in_billing_retry_period);
1148 assert_eq!(None, renewal_info.price_increase_status);
1149 assert_eq!(None, renewal_info.grace_period_expires_date);
1150 assert_eq!(None, renewal_info.offer_type);
1151 assert_eq!(None, renewal_info.offer_identifier);
1152 assert_eq!(1697679936711, renewal_info.signed_date.unwrap().timestamp_millis());
1153 assert_eq!(Environment::Xcode, renewal_info.environment.expect("Expect environment"));
1154 assert_eq!(1697679936049, renewal_info.recent_subscription_start_date.unwrap().timestamp_millis());
1155 assert_eq!(1700358336049, renewal_info.renewal_date.unwrap().timestamp_millis());
1156 } else {
1157 panic!("Failed to verify and decode signed renewal info");
1158 }
1159 }
1160
1161 #[test]
1162 fn test_xcode_signed_app_transaction_with_production_environment() {
1163 let verifier = get_signed_data_verifier(Environment::Production, XCODE_BUNDLE_ID, None);
1164 let encoded_app_transaction = fs::read_to_string("resources/xcode/xcode-signed-app-transaction").expect("Failed to read file");
1165
1166 if let Err(_) = verifier.verify_and_decode_app_transaction(&encoded_app_transaction) {
1167 return;
1168 }
1169 panic!("Expected VerificationException, but no exception was raised");
1170 }
1171
1172 fn get_default_signed_data_verifier() -> SignedDataVerifier {
1173 return get_signed_data_verifier(Environment::LocalTesting, "com.example", None);
1174 }
1175
1176 fn create_signed_data_from_json(path: &str) -> String {
1177 let json_payload = fs::read_to_string(path).expect("Failed to read JSON file");
1178 let json: Map<String, Value> =
1179 serde_json::from_str(json_payload.as_str()).expect("Expect JSON");
1180
1181 let header = jsonwebtoken::Header::new(Algorithm::ES256);
1182 let private_key = generate_p256_private_key();
1183 let key = jsonwebtoken::EncodingKey::from_ec_der(private_key.as_ref());
1184 let payload = jsonwebtoken::encode(&header, &json, &key).expect("Failed to encode JWT");
1185 payload
1186 }
1187
1188 fn generate_p256_private_key() -> Vec<u8> {
1189 let rng = ring::rand::SystemRandom::new();
1190 let private_key =
1191 ring::signature::EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, &rng)
1192 .expect("Failed to generate private key");
1193
1194 private_key.as_ref().to_vec()
1195 }
1196}