app_store_server_library/
signed_data_verifier.rs

1use base64::engine::general_purpose::STANDARD;
2use base64::{DecodeError, Engine};
3
4use crate::chain_verifier::{ChainVerifier, 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::primitives::retention_messaging::decoded_realtime_request_body::DecodedRealtimeRequestBody;
11use crate::utils::{base64_url_to_base64, StringExt};
12use jsonwebtoken::{Algorithm, DecodingKey, Validation};
13use serde::de::DeserializeOwned;
14use crate::chain_verifier::ChainVerificationFailureReason::InvalidChainLength;
15
16#[derive(thiserror::Error, Debug)]
17pub enum SignedDataVerifierError {
18    #[error("VerificationFailure")]
19    VerificationFailure,
20
21    #[error("InvalidAppIdentifier")]
22    InvalidAppIdentifier,
23
24    #[error("InvalidEnvironment")]
25    InvalidEnvironment,
26
27    #[error("InternalChainVerifierError")]
28    InternalChainVerifierError(#[from] ChainVerifierError),
29
30    #[error("InternalDecodeError: [{0}]")]
31    InternalDecodeError(#[from] DecodeError),
32
33    #[error("InternalDeserializationError: [{0}]")]
34    InternalDeserializationError(#[from] serde_json::Error),
35
36    #[error("InternalJWTError: [{0}]")]
37    InternalJWTError(#[from] jsonwebtoken::errors::Error),
38}
39
40const EXPECTED_CHAIN_LENGTH: usize = 3;
41
42/// A verifier for signed data, commonly used for verifying and decoding
43/// signed Apple server notifications and transactions.
44pub struct SignedDataVerifier {
45    environment: Environment,
46    bundle_id: String,
47    app_apple_id: Option<i64>,
48    chain_verifier: ChainVerifier,
49}
50
51impl SignedDataVerifier {
52    /// Creates a new `SignedDataVerifier` instance with the specified parameters.
53    ///
54    /// # Arguments
55    ///
56    /// * `root_certificates` - A vector of DER-encoded root certificates used for verification.
57    /// * `environment` - The environment (e.g., `Environment::PRODUCTION` or `Environment::SANDBOX`).
58    /// * `bundle_id` - The bundle ID associated with the application.
59    /// * `app_apple_id` - An optional Apple ID associated with the application.
60    ///
61    /// # Returns
62    ///
63    /// A new `SignedDataVerifier` instance.
64    pub fn new(
65        root_certificates: Vec<Vec<u8>>,
66        environment: Environment,
67        bundle_id: String,
68        app_apple_id: Option<i64>,
69    ) -> Self {
70        let chain_verifier = ChainVerifier::new(root_certificates);
71
72        SignedDataVerifier {
73            environment,
74            bundle_id,
75            app_apple_id,
76            chain_verifier
77        }
78    }
79}
80
81impl SignedDataVerifier {
82    /// Verifies and decodes a signed renewal info.
83    ///
84    /// This method takes a signed renewal info string, verifies its authenticity and
85    /// integrity, and returns the decoded payload as a `JWSRenewalInfoDecodedPayload`
86    /// if the verification is successful.
87    ///
88    /// # Arguments
89    ///
90    /// * `signed_renewal_info` - The signed renewal info string to verify and decode.
91    ///
92    /// # Returns
93    ///
94    /// - `Ok(JWSRenewalInfoDecodedPayload)` if verification and decoding are successful.
95    /// - `Err(SignedDataVerifierError)` if verification or decoding fails, with error details.
96    pub fn verify_and_decode_renewal_info(
97        &self,
98        signed_renewal_info: &str,
99    ) -> Result<JWSRenewalInfoDecodedPayload, SignedDataVerifierError> {
100        Ok(self.decode_signed_object(signed_renewal_info)?)
101    }
102
103    /// Verifies and decodes a signed transaction.
104    ///
105    /// This method takes a signed transaction string, verifies its authenticity and
106    /// integrity, and returns the decoded payload as a `JWSTransactionDecodedPayload`
107    /// if the verification is successful.
108    ///
109    /// # Arguments
110    ///
111    /// * `signed_transaction` - The signed transaction string to verify and decode.
112    ///
113    /// # Returns
114    ///
115    /// - `Ok(JWSTransactionDecodedPayload)` if verification and decoding are successful.
116    /// - `Err(SignedDataVerifierError)` if verification or decoding fails, with error details.
117    pub fn verify_and_decode_signed_transaction(
118        &self,
119        signed_transaction: &str,
120    ) -> Result<JWSTransactionDecodedPayload, SignedDataVerifierError> {
121        let decoded_signed_tx: JWSTransactionDecodedPayload = self.decode_signed_object(signed_transaction)?;
122
123        if decoded_signed_tx.bundle_id.as_ref() != Some(&self.bundle_id) {
124            return Err(SignedDataVerifierError::InvalidAppIdentifier);
125        }
126
127        if decoded_signed_tx.environment.as_ref() != Some(&self.environment) {
128            return Err(SignedDataVerifierError::InvalidEnvironment);
129        }
130
131        Ok(decoded_signed_tx)
132    }
133
134    /// Verifies and decodes a signed notification.
135    ///
136    /// This method takes a signed notification string, verifies its authenticity and
137    /// integrity, and returns the decoded payload as a `ResponseBodyV2DecodedPayload`
138    /// if the verification is successful.
139    ///
140    /// # Arguments
141    ///
142    /// * `signed_payload` - The signed notification string to verify and decode.
143    ///
144    /// # Returns
145    ///
146    /// - `Ok(ResponseBodyV2DecodedPayload)` if verification and decoding are successful.
147    /// - `Err(SignedDataVerifierError)` if verification or decoding fails, with error details.
148    pub fn verify_and_decode_notification(
149        &self,
150        signed_payload: &str,
151    ) -> Result<ResponseBodyV2DecodedPayload, SignedDataVerifierError> {
152        let decoded_signed_notification: ResponseBodyV2DecodedPayload = self.decode_signed_object(signed_payload)?;
153
154        let bundle_id;
155        let app_apple_id;
156        let environment;
157
158        if let Some(data) = &decoded_signed_notification.data {
159            bundle_id = data.bundle_id.clone();
160            app_apple_id = data.app_apple_id.clone();
161            environment = data.environment.clone();
162        } else if let Some(summary) = &decoded_signed_notification.summary {
163            bundle_id = summary.bundle_id.clone();
164            app_apple_id = summary.app_apple_id.clone();
165            environment = summary.environment.clone();
166        } else if let Some(external_purchase_token) = &decoded_signed_notification.external_purchase_token {
167            bundle_id = external_purchase_token
168                .bundle_id
169                .clone();
170            app_apple_id = external_purchase_token
171                .app_apple_id
172                .clone();
173
174            if let Some(external_purchase_id) = &external_purchase_token.external_purchase_id {
175                if external_purchase_id.starts_with("SANDBOX") {
176                    environment = Some(Environment::Sandbox)
177                } else {
178                    environment = Some(Environment::Production)
179                }
180            } else {
181                environment = Some(Environment::Production)
182            }
183        } else if let Some(app_data) = &decoded_signed_notification.app_data {
184            bundle_id = app_data.bundle_id.clone();
185            app_apple_id = app_data.app_apple_id.clone();
186            environment = app_data.environment.clone();
187        } else {
188            bundle_id = None;
189            app_apple_id = None;
190            environment = None;
191        }
192
193        self.verify_notification_app_identifier_and_environment(bundle_id, app_apple_id, environment)?;
194
195        Ok(decoded_signed_notification)
196    }
197
198    fn verify_notification_app_identifier_and_environment(
199        &self,
200        bundle_id: Option<String>,
201        app_apple_id: Option<i64>,
202        environment: Option<Environment>,
203    ) -> Result<(), SignedDataVerifierError> {
204        if let Some(bundle_id) = bundle_id {
205            if bundle_id != self.bundle_id {
206                return Err(SignedDataVerifierError::InvalidAppIdentifier);
207            }
208        }
209
210        if self.environment == Environment::Production && self.app_apple_id != app_apple_id {
211            return Err(SignedDataVerifierError::InvalidAppIdentifier);
212        }
213
214        if let Some(environment) = environment {
215            if self.environment != Environment::LocalTesting && self.environment != environment {
216                return Err(SignedDataVerifierError::InvalidEnvironment);
217            }
218        }
219
220        Ok(())
221    }
222
223    /// Verifies and decodes a signed app transaction.
224    ///
225    /// This method takes a signed app transaction string, verifies its authenticity and
226    /// integrity, and returns the decoded payload as an `AppTransaction`
227    /// if the verification is successful.
228    ///
229    /// # Arguments
230    ///
231    /// * `signed_app_transaction` - The signed app transaction string to verify and decode.
232    ///
233    /// # Returns
234    ///
235    /// - `Ok(AppTransaction)` if verification and decoding are successful.
236    /// - `Err(SignedDataVerifierError)` if verification or decoding fails, with error details.
237    pub fn verify_and_decode_app_transaction(
238        &self,
239        signed_app_transaction: &str,
240    ) -> Result<AppTransaction, SignedDataVerifierError> {
241        let decoded_app_transaction: AppTransaction = self.decode_signed_object(signed_app_transaction)?;
242
243        if decoded_app_transaction
244            .bundle_id
245            .as_ref()
246            != Some(&self.bundle_id)
247        {
248            return Err(SignedDataVerifierError::InvalidAppIdentifier);
249        }
250
251        if decoded_app_transaction
252            .receipt_type
253            .as_ref()
254            != Some(&self.environment)
255        {
256            return Err(SignedDataVerifierError::InvalidEnvironment);
257        }
258
259        Ok(decoded_app_transaction)
260    }
261
262    /// Verifies and decodes a realtime request the App Store sends to your Get Retention Message endpoint.
263    ///
264    /// This method takes a signed realtime request string, verifies its authenticity and
265    /// integrity, and returns the decoded payload as a `DecodedRealtimeRequestBody`
266    /// if the verification is successful.
267    ///
268    /// # Arguments
269    ///
270    /// * `signed_payload` - The payload the App Store server sends to your server.
271    ///
272    /// # Returns
273    ///
274    /// - `Ok(DecodedRealtimeRequestBody)` if verification and decoding are successful.
275    /// - `Err(SignedDataVerifierError)` if verification or decoding fails, with error details.
276    pub fn verify_and_decode_realtime_request(
277        &self,
278        signed_payload: &str,
279    ) -> Result<DecodedRealtimeRequestBody, SignedDataVerifierError> {
280        let decoded_realtime_request: DecodedRealtimeRequestBody = self.decode_signed_object(signed_payload)?;
281
282        if self.environment == Environment::Production && self.app_apple_id != Some(decoded_realtime_request.app_apple_id) {
283            return Err(SignedDataVerifierError::InvalidAppIdentifier);
284        }
285
286        if self.environment != decoded_realtime_request.environment {
287            return Err(SignedDataVerifierError::InvalidEnvironment);
288        }
289
290        Ok(decoded_realtime_request)
291    }
292
293    /// Private method used for decoding a signed object (internal use).
294    fn decode_signed_object<T: DeserializeOwned>(&self, signed_obj: &str) -> Result<T, SignedDataVerifierError> {
295        // Data is not signed by the App Store, and verification should be skipped
296        // The environment MUST be checked in the public method calling this
297        if self.environment == Environment::Xcode || self.environment == Environment::LocalTesting {
298            const EXPECTED_JWT_SEGMENTS: usize = 3;
299
300            let body_segments: Vec<&str> = signed_obj.split('.').collect();
301
302            if body_segments.len() != EXPECTED_JWT_SEGMENTS {
303                return Err(SignedDataVerifierError::VerificationFailure);
304            }
305
306            let _ = jsonwebtoken::decode_header(&signed_obj)?;
307            let body_base64 = base64_url_to_base64(body_segments[1]);
308            let body_data = STANDARD.decode(body_base64)?;
309            let decoded_body = serde_json::from_slice(&body_data)?;
310            return Ok(decoded_body);
311        }
312
313        let header = jsonwebtoken::decode_header(signed_obj)?;
314
315        let Some(x5c) = header.x5c else {
316            return Err(SignedDataVerifierError::VerificationFailure);
317        };
318
319        if x5c.is_empty() {
320            return Err(SignedDataVerifierError::VerificationFailure);
321        }
322
323        let x5c: Result<Vec<Vec<u8>>, DecodeError> = x5c
324            .iter()
325            .map(|c| c.as_der_bytes())
326            .collect();
327        let chain = x5c?;
328
329        if header.alg != Algorithm::ES256 {
330            return Err(SignedDataVerifierError::VerificationFailure);
331        }
332
333        let pub_key = self.verify_chain(&chain, None)?;
334        let pub_key = &pub_key[pub_key.len() - 65..];
335
336        let decoding_key = DecodingKey::from_ec_der(pub_key);
337        let claims: [&str; 0] = [];
338
339        let mut validator = Validation::new(Algorithm::ES256);
340        validator.validate_exp = false;
341        validator.set_required_spec_claims(&claims);
342
343        let payload = jsonwebtoken::decode::<T>(signed_obj, &decoding_key, &validator)?;
344        Ok(payload.claims)
345    }
346
347    fn verify_chain(&self, chain: &Vec<Vec<u8>>, effective_date: Option<u64>) -> Result<Vec<u8>, ChainVerifierError> {
348        if chain.len() != EXPECTED_CHAIN_LENGTH {
349            return Err(ChainVerifierError::VerificationFailure(InvalidChainLength))
350        }
351
352        let leaf = &chain[0];
353        let intermediate = &chain[1];
354
355        Ok(self.chain_verifier.verify(leaf, intermediate, effective_date)?)
356    }
357}
358
359#[cfg(test)]
360mod tests {
361    use crate::chain_verifier::ChainVerificationFailureReason::InvalidChainLength;
362    use super::*;
363
364    #[test]
365    fn test_invalid_chain_length() -> Result<(), ChainVerifierError> {
366        let root = Vec::new();
367        let leaf = Vec::new();
368        let intermediate = Vec::new();
369
370        let chain = vec![leaf, intermediate, Vec::new(), Vec::new()];
371        let verifier = SignedDataVerifier::new(vec![root], Environment::Production, "com.example".into(), Some(1234));
372        let public_key = verifier.verify_chain(&chain, None);
373
374        assert!(
375            matches!(
376                public_key.expect_err("Expect error"),
377                ChainVerifierError::VerificationFailure(InvalidChainLength)
378            )
379        );
380        Ok(())
381    }
382}