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 {
184            bundle_id = None;
185            app_apple_id = None;
186            environment = None;
187        }
188
189        self.verify_notification_app_identifier_and_environment(bundle_id, app_apple_id, environment)?;
190
191        Ok(decoded_signed_notification)
192    }
193
194    fn verify_notification_app_identifier_and_environment(
195        &self,
196        bundle_id: Option<String>,
197        app_apple_id: Option<i64>,
198        environment: Option<Environment>,
199    ) -> Result<(), SignedDataVerifierError> {
200        if let Some(bundle_id) = bundle_id {
201            if bundle_id != self.bundle_id {
202                return Err(SignedDataVerifierError::InvalidAppIdentifier);
203            }
204        }
205
206        if self.environment == Environment::Production && self.app_apple_id != app_apple_id {
207            return Err(SignedDataVerifierError::InvalidAppIdentifier);
208        }
209
210        if let Some(environment) = environment {
211            if self.environment != Environment::LocalTesting && self.environment != environment {
212                return Err(SignedDataVerifierError::InvalidEnvironment);
213            }
214        }
215
216        Ok(())
217    }
218
219    /// Verifies and decodes a signed app transaction.
220    ///
221    /// This method takes a signed app transaction string, verifies its authenticity and
222    /// integrity, and returns the decoded payload as an `AppTransaction`
223    /// if the verification is successful.
224    ///
225    /// # Arguments
226    ///
227    /// * `signed_app_transaction` - The signed app transaction string to verify and decode.
228    ///
229    /// # Returns
230    ///
231    /// - `Ok(AppTransaction)` if verification and decoding are successful.
232    /// - `Err(SignedDataVerifierError)` if verification or decoding fails, with error details.
233    pub fn verify_and_decode_app_transaction(
234        &self,
235        signed_app_transaction: &str,
236    ) -> Result<AppTransaction, SignedDataVerifierError> {
237        let decoded_app_transaction: AppTransaction = self.decode_signed_object(signed_app_transaction)?;
238
239        if decoded_app_transaction
240            .bundle_id
241            .as_ref()
242            != Some(&self.bundle_id)
243        {
244            return Err(SignedDataVerifierError::InvalidAppIdentifier);
245        }
246
247        if decoded_app_transaction
248            .receipt_type
249            .as_ref()
250            != Some(&self.environment)
251        {
252            return Err(SignedDataVerifierError::InvalidEnvironment);
253        }
254
255        Ok(decoded_app_transaction)
256    }
257
258    /// Verifies and decodes a realtime request the App Store sends to your Get Retention Message endpoint.
259    ///
260    /// This method takes a signed realtime request string, verifies its authenticity and
261    /// integrity, and returns the decoded payload as a `DecodedRealtimeRequestBody`
262    /// if the verification is successful.
263    ///
264    /// # Arguments
265    ///
266    /// * `signed_payload` - The payload the App Store server sends to your server.
267    ///
268    /// # Returns
269    ///
270    /// - `Ok(DecodedRealtimeRequestBody)` if verification and decoding are successful.
271    /// - `Err(SignedDataVerifierError)` if verification or decoding fails, with error details.
272    pub fn verify_and_decode_realtime_request(
273        &self,
274        signed_payload: &str,
275    ) -> Result<DecodedRealtimeRequestBody, SignedDataVerifierError> {
276        let decoded_realtime_request: DecodedRealtimeRequestBody = self.decode_signed_object(signed_payload)?;
277
278        if self.environment == Environment::Production && self.app_apple_id != Some(decoded_realtime_request.app_apple_id) {
279            return Err(SignedDataVerifierError::InvalidAppIdentifier);
280        }
281
282        if self.environment != decoded_realtime_request.environment {
283            return Err(SignedDataVerifierError::InvalidEnvironment);
284        }
285
286        Ok(decoded_realtime_request)
287    }
288
289    /// Private method used for decoding a signed object (internal use).
290    fn decode_signed_object<T: DeserializeOwned>(&self, signed_obj: &str) -> Result<T, SignedDataVerifierError> {
291        // Data is not signed by the App Store, and verification should be skipped
292        // The environment MUST be checked in the public method calling this
293        if self.environment == Environment::Xcode || self.environment == Environment::LocalTesting {
294            const EXPECTED_JWT_SEGMENTS: usize = 3;
295
296            let body_segments: Vec<&str> = signed_obj.split('.').collect();
297
298            if body_segments.len() != EXPECTED_JWT_SEGMENTS {
299                return Err(SignedDataVerifierError::VerificationFailure);
300            }
301
302            let _ = jsonwebtoken::decode_header(&signed_obj)?;
303            let body_base64 = base64_url_to_base64(body_segments[1]);
304            let body_data = STANDARD.decode(body_base64)?;
305            let decoded_body = serde_json::from_slice(&body_data)?;
306            return Ok(decoded_body);
307        }
308
309        let header = jsonwebtoken::decode_header(signed_obj)?;
310
311        let Some(x5c) = header.x5c else {
312            return Err(SignedDataVerifierError::VerificationFailure);
313        };
314
315        if x5c.is_empty() {
316            return Err(SignedDataVerifierError::VerificationFailure);
317        }
318
319        let x5c: Result<Vec<Vec<u8>>, DecodeError> = x5c
320            .iter()
321            .map(|c| c.as_der_bytes())
322            .collect();
323        let chain = x5c?;
324
325        if header.alg != Algorithm::ES256 {
326            return Err(SignedDataVerifierError::VerificationFailure);
327        }
328
329        let pub_key = self.verify_chain(&chain, None)?;
330        let pub_key = &pub_key[pub_key.len() - 65..];
331
332        let decoding_key = DecodingKey::from_ec_der(pub_key);
333        let claims: [&str; 0] = [];
334
335        let mut validator = Validation::new(Algorithm::ES256);
336        validator.validate_exp = false;
337        validator.set_required_spec_claims(&claims);
338
339        let payload = jsonwebtoken::decode::<T>(signed_obj, &decoding_key, &validator)?;
340        Ok(payload.claims)
341    }
342
343    fn verify_chain(&self, chain: &Vec<Vec<u8>>, effective_date: Option<u64>) -> Result<Vec<u8>, ChainVerifierError> {
344        if chain.len() != EXPECTED_CHAIN_LENGTH {
345            return Err(ChainVerifierError::VerificationFailure(InvalidChainLength))
346        }
347
348        let leaf = &chain[0];
349        let intermediate = &chain[1];
350
351        Ok(self.chain_verifier.verify(leaf, intermediate, effective_date)?)
352    }
353}
354
355#[cfg(test)]
356mod tests {
357    use crate::chain_verifier::ChainVerificationFailureReason::InvalidChainLength;
358    use super::*;
359
360    #[test]
361    fn test_invalid_chain_length() -> Result<(), ChainVerifierError> {
362        let root = Vec::new();
363        let leaf = Vec::new();
364        let intermediate = Vec::new();
365
366        let chain = vec![leaf, intermediate, Vec::new(), Vec::new()];
367        let verifier = SignedDataVerifier::new(vec![root], Environment::Production, "com.example".into(), Some(1234));
368        let public_key = verifier.verify_chain(&chain, None);
369
370        assert!(
371            matches!(
372                public_key.expect_err("Expect error"),
373                ChainVerifierError::VerificationFailure(InvalidChainLength)
374            )
375        );
376        Ok(())
377    }
378}