app_store_server_library/
signed_data_verifier.rs

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
35/// A verifier for signed data, commonly used for verifying and decoding
36/// signed Apple server notifications and transactions.
37pub 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    /// Creates a new `SignedDataVerifier` instance with the specified parameters.
46    ///
47    /// # Arguments
48    ///
49    /// * `root_certificates` - A vector of DER-encoded root certificates used for verification.
50    /// * `environment` - The environment (e.g., `Environment::PRODUCTION` or `Environment::SANDBOX`).
51    /// * `bundle_id` - The bundle ID associated with the application.
52    /// * `app_apple_id` - An optional Apple ID associated with the application.
53    ///
54    /// # Returns
55    ///
56    /// A new `SignedDataVerifier` instance.
57    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    /// Verifies and decodes a signed renewal info.
74    ///
75    /// This method takes a signed renewal info string, verifies its authenticity and
76    /// integrity, and returns the decoded payload as a `JWSRenewalInfoDecodedPayload`
77    /// if the verification is successful.
78    ///
79    /// # Arguments
80    ///
81    /// * `signed_renewal_info` - The signed renewal info string to verify and decode.
82    ///
83    /// # Returns
84    ///
85    /// - `Ok(JWSRenewalInfoDecodedPayload)` if verification and decoding are successful.
86    /// - `Err(SignedDataVerifierError)` if verification or decoding fails, with error details.
87    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    /// Verifies and decodes a signed transaction.
95    ///
96    /// This method takes a signed transaction string, verifies its authenticity and
97    /// integrity, and returns the decoded payload as a `JWSTransactionDecodedPayload`
98    /// if the verification is successful.
99    ///
100    /// # Arguments
101    ///
102    /// * `signed_transaction` - The signed transaction string to verify and decode.
103    ///
104    /// # Returns
105    ///
106    /// - `Ok(JWSTransactionDecodedPayload)` if verification and decoding are successful.
107    /// - `Err(SignedDataVerifierError)` if verification or decoding fails, with error details.
108    pub fn verify_and_decode_signed_transaction(
109        &self,
110        signed_transaction: &str,
111    ) -> Result<JWSTransactionDecodedPayload, SignedDataVerifierError> {
112        let decoded_signed_tx: JWSTransactionDecodedPayload = self.decode_signed_object(signed_transaction)?;
113
114        if decoded_signed_tx.bundle_id.as_ref() != Some(&self.bundle_id) {
115            return Err(SignedDataVerifierError::InvalidAppIdentifier);
116        }
117
118        if decoded_signed_tx.environment.as_ref() != Some(&self.environment) {
119            return Err(SignedDataVerifierError::InvalidEnvironment);
120        }
121
122        Ok(decoded_signed_tx)
123    }
124
125    /// Verifies and decodes a signed notification.
126    ///
127    /// This method takes a signed notification string, verifies its authenticity and
128    /// integrity, and returns the decoded payload as a `ResponseBodyV2DecodedPayload`
129    /// if the verification is successful.
130    ///
131    /// # Arguments
132    ///
133    /// * `signed_payload` - The signed notification string to verify and decode.
134    ///
135    /// # Returns
136    ///
137    /// - `Ok(ResponseBodyV2DecodedPayload)` if verification and decoding are successful.
138    /// - `Err(SignedDataVerifierError)` if verification or decoding fails, with error details.
139    pub fn verify_and_decode_notification(
140        &self,
141        signed_payload: &str,
142    ) -> Result<ResponseBodyV2DecodedPayload, SignedDataVerifierError> {
143        let decoded_signed_notification: ResponseBodyV2DecodedPayload = self.decode_signed_object(signed_payload)?;
144
145        let bundle_id;
146        let app_apple_id;
147        let environment;
148
149        if let Some(data) = &decoded_signed_notification.data {
150            bundle_id = data.bundle_id.clone();
151            app_apple_id = data.app_apple_id.clone();
152            environment = data.environment.clone();
153        } else if let Some(summary) = &decoded_signed_notification.summary {
154            bundle_id = summary.bundle_id.clone();
155            app_apple_id = summary.app_apple_id.clone();
156            environment = summary.environment.clone();
157        } else if let Some(external_purchase_token) = &decoded_signed_notification.external_purchase_token {
158            bundle_id = external_purchase_token.bundle_id.clone();
159            app_apple_id = external_purchase_token.app_apple_id.clone();
160
161            if let Some(external_purchase_id) = &external_purchase_token.external_purchase_id {
162                if external_purchase_id.starts_with("SANDBOX") {
163                    environment = Some(Environment::Sandbox)
164                } else {
165                    environment = Some(Environment::Production)
166                }
167            } else {
168                environment = Some(Environment::Production)
169            }
170        } else {
171            bundle_id = None;
172            app_apple_id = None;
173            environment = None;
174        }
175
176        self.verify_notification_app_identifier_and_environment(bundle_id, app_apple_id, environment)?;
177
178        Ok(decoded_signed_notification)
179    }
180
181    fn verify_notification_app_identifier_and_environment(
182        &self,
183        bundle_id: Option<String>,
184        app_apple_id: Option<i64>,
185        environment: Option<Environment>,
186    ) -> Result<(), SignedDataVerifierError> {
187        if let Some(bundle_id) = bundle_id {
188            if bundle_id != self.bundle_id {
189                return Err(SignedDataVerifierError::InvalidAppIdentifier);
190            }
191        }
192
193        if self.environment == Environment::Production && self.app_apple_id != app_apple_id {
194            return Err(SignedDataVerifierError::InvalidAppIdentifier);
195        }
196
197        if let Some(environment) = environment {
198            if self.environment != Environment::LocalTesting && self.environment != environment {
199                return Err(SignedDataVerifierError::InvalidEnvironment);
200            }
201        }
202
203        Ok(())
204    }
205
206    /// Verifies and decodes a signed notification.
207    ///
208    /// This method takes a signed notification string, verifies its authenticity and
209    /// integrity, and returns the decoded payload as a `ResponseBodyV2DecodedPayload`
210    /// if the verification is successful.
211    ///
212    /// # Arguments
213    ///
214    /// * `signed_payload` - The signed notification string to verify and decode.
215    ///
216    /// # Returns
217    ///
218    /// - `Ok(ResponseBodyV2DecodedPayload)` if verification and decoding are successful.
219    /// - `Err(SignedDataVerifierError)` if verification or decoding fails, with error details.
220    pub fn verify_and_decode_app_transaction(
221        &self,
222        signed_app_transaction: &str,
223    ) -> Result<AppTransaction, SignedDataVerifierError> {
224        let decoded_app_transaction: AppTransaction = self.decode_signed_object(signed_app_transaction)?;
225
226        if decoded_app_transaction.bundle_id.as_ref() != Some(&self.bundle_id) {
227            return Err(SignedDataVerifierError::InvalidAppIdentifier);
228        }
229
230        if decoded_app_transaction.receipt_type.as_ref() != Some(&self.environment) {
231            return Err(SignedDataVerifierError::InvalidEnvironment);
232        }
233
234        Ok(decoded_app_transaction)
235    }
236
237    /// Private method used for decoding a signed object (internal use).
238    fn decode_signed_object<T: DeserializeOwned>(&self, signed_obj: &str) -> Result<T, SignedDataVerifierError> {
239        // Data is not signed by the App Store, and verification should be skipped
240        // The environment MUST be checked in the public method calling this
241        if self.environment == Environment::Xcode || self.environment == Environment::LocalTesting {
242            const EXPECTED_JWT_SEGMENTS: usize = 3;
243
244            let body_segments: Vec<&str> = signed_obj.split('.').collect();
245
246            if body_segments.len() != EXPECTED_JWT_SEGMENTS {
247                return Err(SignedDataVerifierError::VerificationFailure);
248            }
249
250            let _ = jsonwebtoken::decode_header(&signed_obj)?;
251            let body_data = base64_url_to_base64(body_segments[1]);
252
253            let decoded_body = match STANDARD.decode(body_data) {
254                Ok(decoded_body) => match serde_json::from_slice(&decoded_body) {
255                    Ok(decoded) => decoded,
256                    Err(_) => return Err(SignedDataVerifierError::VerificationFailure),
257                },
258                Err(_) => return Err(SignedDataVerifierError::VerificationFailure),
259            };
260
261            return Ok(decoded_body);
262        }
263
264        let header = jsonwebtoken::decode_header(signed_obj)?;
265
266        let Some(x5c) = header.x5c else {
267            return Err(SignedDataVerifierError::VerificationFailure);
268        };
269
270        if x5c.is_empty() {
271            return Err(SignedDataVerifierError::VerificationFailure);
272        }
273
274        let x5c: Result<Vec<Vec<u8>>, DecodeError> = x5c.iter().map(|c| c.as_der_bytes()).collect();
275        let chain = x5c?;
276
277        if header.alg != Algorithm::ES256 {
278            return Err(SignedDataVerifierError::VerificationFailure);
279        }
280
281        let pub_key = verify_chain(&chain, &self.root_certificates, None)?;
282        let pub_key = &pub_key[pub_key.len() - 65..];
283
284        let decoding_key = DecodingKey::from_ec_der(pub_key);
285        let claims: [&str; 0] = [];
286
287        let mut validator = Validation::new(Algorithm::ES256);
288        validator.validate_exp = false;
289        validator.set_required_spec_claims(&claims);
290
291        let payload = jsonwebtoken::decode::<T>(signed_obj, &decoding_key, &validator).expect("Expect Payload");
292        return Ok(payload.claims);
293    }
294}