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