Skip to main content

app_store_server_library/
signed_data_verifier.rs

1use base64::engine::general_purpose::STANDARD;
2use base64::{DecodeError, Engine};
3use serde::de::DeserializeOwned;
4
5use crate::chain_verifier::{ChainVerificationFailureReason, ChainVerifier, ChainVerifierError};
6use crate::crypto::{jws, CryptoProvider};
7use crate::models::app_store_environment::Environment;
8use crate::models::app_transaction::AppTransaction;
9use crate::models::decoded_realtime_request_body::DecodedRealtimeRequestBody;
10use crate::models::decoded_signed_data::DecodedSignedData;
11use crate::models::jws_renewal_info_decoded_payload::JWSRenewalInfoDecodedPayload;
12use crate::models::jws_transaction_decoded_payload::JWSTransactionDecodedPayload;
13use crate::models::response_body_v2_decoded_payload::ResponseBodyV2DecodedPayload;
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("InvalidAppAppleId")]
27    InvalidAppAppleId,
28
29    #[error("InternalChainVerifierError")]
30    InternalChainVerifierError(#[from] ChainVerifierError),
31
32    #[error("InternalDecodeError: [{0}]")]
33    InternalDecodeError(#[from] DecodeError),
34
35    #[error("InternalDeserializationError: [{0}]")]
36    InternalDeserializationError(#[from] serde_json::Error),
37
38    #[error("InternalJWSError: [{0}]")]
39    InternalJWSError(#[from] crate::crypto::jws::JwsError),
40}
41
42const EXPECTED_CHAIN_LENGTH: usize = 3;
43
44///A verifier and decoder class designed to decode signed data from the App Store.
45pub struct SignedDataVerifier {
46    environment: Environment,
47    bundle_id: String,
48    app_apple_id: Option<i64>,
49    enable_online_checks: bool,
50    chain_verifier: ChainVerifier,
51}
52
53impl SignedDataVerifier {
54    /// Creates a new `SignedDataVerifier` instance with the specified parameters.
55    ///
56    /// # Arguments
57    ///
58    /// * `root_certificates` - A vector of DER-encoded root certificates used for verification.
59    /// * `environment` - The environment (e.g., `Environment::PRODUCTION` or `Environment::SANDBOX`).
60    /// * `bundle_id` - The bundle ID associated with the application.
61    /// * `app_apple_id` - An optional Apple ID associated with the application. Required when
62    ///   `environment` is `Environment::Production`.
63    /// * `enable_online_checks` - Whether to check certificate expiration against the
64    ///   current date rather than the JWS's signed date, and cache the verified public key.
65    ///
66    /// # Returns
67    ///
68    /// - `Ok(SignedDataVerifier)` on success.
69    /// - `Err(SignedDataVerifierError::InvalidAppAppleId)` if `environment` is
70    ///   `Environment::Production` and `app_apple_id` is `None`.
71    pub fn new(
72        root_certificates: Vec<Vec<u8>>,
73        environment: Environment,
74        bundle_id: String,
75        app_apple_id: Option<i64>,
76        enable_online_checks: bool,
77    ) -> Result<Self, SignedDataVerifierError> {
78        if environment == Environment::Production && app_apple_id.is_none() {
79            return Err(SignedDataVerifierError::InvalidAppAppleId);
80        }
81
82        Ok(SignedDataVerifier {
83            environment,
84            bundle_id,
85            app_apple_id,
86            enable_online_checks,
87            chain_verifier: ChainVerifier::new(root_certificates),
88        })
89    }
90}
91
92impl SignedDataVerifier {
93    /// Verifies and decodes a signedRenewalInfo obtained from the App Store Server API, an App Store Server Notification, or from a device
94    /// See [JWSRenewalInfo](https://developer.apple.com/documentation/appstoreserverapi/jwsrenewalinfo)
95    ///
96    /// # Arguments
97    ///
98    /// * `signed_renewal_info` - The signed renewal info string to verify and decode.
99    ///
100    /// # Returns
101    ///
102    /// - `Ok(JWSRenewalInfoDecodedPayload)` if verification and decoding are successful.
103    /// - `Err(SignedDataVerifierError)` if verification or decoding fails, with error details.
104    pub fn verify_and_decode_renewal_info(
105        &self,
106        signed_renewal_info: &str,
107    ) -> Result<JWSRenewalInfoDecodedPayload, SignedDataVerifierError> {
108        let decoded_renewal_info: JWSRenewalInfoDecodedPayload = self.decode_signed_object(signed_renewal_info)?;
109
110        if decoded_renewal_info
111            .environment
112            .as_ref()
113            != Some(&self.environment)
114        {
115            return Err(SignedDataVerifierError::InvalidEnvironment);
116        }
117
118        Ok(decoded_renewal_info)
119    }
120
121    ///  Verifies and decodes a signedTransaction obtained from the App Store Server API, an App Store Server Notification, or from a device
122    ///  See [JWSTransaction](https://developer.apple.com/documentation/appstoreserverapi/jwstransaction)
123    ///
124    /// # Arguments
125    ///
126    /// * `signed_transaction` - The signed transaction string to verify and decode.
127    ///
128    /// # Returns
129    ///
130    /// - `Ok(JWSTransactionDecodedPayload)` if verification and decoding are successful.
131    /// - `Err(SignedDataVerifierError)` if verification or decoding fails, with error details.
132    pub fn verify_and_decode_signed_transaction(
133        &self,
134        signed_transaction: &str,
135    ) -> Result<JWSTransactionDecodedPayload, SignedDataVerifierError> {
136        let decoded_signed_tx: JWSTransactionDecodedPayload = self.decode_signed_object(signed_transaction)?;
137
138        if decoded_signed_tx.bundle_id.as_ref() != Some(&self.bundle_id) {
139            return Err(SignedDataVerifierError::InvalidAppIdentifier);
140        }
141
142        if decoded_signed_tx.environment.as_ref() != Some(&self.environment) {
143            return Err(SignedDataVerifierError::InvalidEnvironment);
144        }
145
146        Ok(decoded_signed_tx)
147    }
148
149    ///  Verifies and decodes an App Store Server Notification signedPayload
150    ///  See [signedPayload](https://developer.apple.com/documentation/appstoreservernotifications/signedpayload)
151    ///
152    /// # Arguments
153    ///
154    /// * `signed_payload` - The signed notification string to verify and decode.
155    ///
156    /// # Returns
157    ///
158    /// - `Ok(ResponseBodyV2DecodedPayload)` if verification and decoding are successful.
159    /// - `Err(SignedDataVerifierError)` if verification or decoding fails, with error details.
160    pub fn verify_and_decode_notification(
161        &self,
162        signed_payload: &str,
163    ) -> Result<ResponseBodyV2DecodedPayload, SignedDataVerifierError> {
164        let decoded_signed_notification: ResponseBodyV2DecodedPayload = self.decode_signed_object(signed_payload)?;
165
166        let bundle_id;
167        let app_apple_id;
168        let environment;
169
170        if let Some(data) = &decoded_signed_notification.data {
171            bundle_id = data.bundle_id.clone();
172            app_apple_id = data.app_apple_id;
173            environment = data.environment.clone();
174        } else if let Some(summary) = &decoded_signed_notification.summary {
175            bundle_id = summary.bundle_id.clone();
176            app_apple_id = summary.app_apple_id;
177            environment = summary.environment.clone();
178        } else if let Some(external_purchase_token) = &decoded_signed_notification.external_purchase_token {
179            bundle_id = external_purchase_token
180                .bundle_id
181                .clone();
182            app_apple_id = external_purchase_token.app_apple_id;
183
184            if let Some(external_purchase_id) = &external_purchase_token.external_purchase_id {
185                if external_purchase_id.starts_with("SANDBOX") {
186                    environment = Some(Environment::Sandbox)
187                } else {
188                    environment = Some(Environment::Production)
189                }
190            } else {
191                environment = Some(Environment::Production)
192            }
193        } else if let Some(app_data) = &decoded_signed_notification.app_data {
194            bundle_id = app_data.bundle_id.clone();
195            app_apple_id = app_data.app_apple_id;
196            environment = app_data.environment.clone();
197        } else {
198            bundle_id = None;
199            app_apple_id = None;
200            environment = None;
201        }
202
203        self.verify_notification_app_identifier_and_environment(bundle_id, app_apple_id, environment)?;
204
205        Ok(decoded_signed_notification)
206    }
207
208    fn verify_notification_app_identifier_and_environment(
209        &self,
210        bundle_id: Option<String>,
211        app_apple_id: Option<i64>,
212        environment: Option<Environment>,
213    ) -> Result<(), SignedDataVerifierError> {
214        if self.environment == Environment::LocalTesting {
215            return Ok(());
216        }
217
218        if bundle_id.as_deref() != Some(self.bundle_id.as_str())
219            || (self.environment == Environment::Production && self.app_apple_id != app_apple_id)
220        {
221            return Err(SignedDataVerifierError::InvalidAppIdentifier);
222        }
223
224        if environment.as_ref() != Some(&self.environment) {
225            return Err(SignedDataVerifierError::InvalidEnvironment);
226        }
227
228        Ok(())
229    }
230
231    ///Verifies and decodes a signed AppTransaction
232    ///See [AppTransaction](https://developer.apple.com/documentation/storekit/apptransaction)
233    ///
234    /// # Arguments
235    ///
236    /// * `signed_app_transaction` - The signed app transaction string to verify and decode.
237    ///
238    /// # Returns
239    ///
240    /// - `Ok(AppTransaction)` if verification and decoding are successful.
241    /// - `Err(SignedDataVerifierError)` if verification or decoding fails, with error details.
242    pub fn verify_and_decode_app_transaction(
243        &self,
244        signed_app_transaction: &str,
245    ) -> Result<AppTransaction, SignedDataVerifierError> {
246        let decoded_app_transaction: AppTransaction = self.decode_signed_object(signed_app_transaction)?;
247
248        if decoded_app_transaction
249            .bundle_id
250            .as_ref()
251            != Some(&self.bundle_id)
252            || (self.environment == Environment::Production
253                && self.app_apple_id != decoded_app_transaction.app_apple_id)
254        {
255            return Err(SignedDataVerifierError::InvalidAppIdentifier);
256        }
257
258        if decoded_app_transaction
259            .receipt_type
260            .as_ref()
261            != Some(&self.environment)
262        {
263            return Err(SignedDataVerifierError::InvalidEnvironment);
264        }
265
266        Ok(decoded_app_transaction)
267    }
268
269    ///Verifies and decodes a realtime request the App Store sends to your Get Retention Message endpoint.
270    ///
271    /// # Arguments
272    ///
273    /// * `signed_payload` - The payload the App Store server sends to your server.
274    ///
275    /// # Returns
276    ///
277    /// - `Ok(DecodedRealtimeRequestBody)` if verification and decoding are successful.
278    /// - `Err(SignedDataVerifierError)` if verification or decoding fails, with error details.
279    pub fn verify_and_decode_realtime_request(
280        &self,
281        signed_payload: &str,
282    ) -> Result<DecodedRealtimeRequestBody, SignedDataVerifierError> {
283        let decoded_realtime_request: DecodedRealtimeRequestBody = self.decode_signed_object(signed_payload)?;
284
285        if self.environment == Environment::Production
286            && self.app_apple_id != Some(decoded_realtime_request.app_apple_id)
287        {
288            return Err(SignedDataVerifierError::InvalidAppIdentifier);
289        }
290
291        if self.environment != decoded_realtime_request.environment {
292            return Err(SignedDataVerifierError::InvalidEnvironment);
293        }
294
295        Ok(decoded_realtime_request)
296    }
297
298    /// Private method used for decoding a signed object (internal use).
299    fn decode_signed_object<T: DeserializeOwned + DecodedSignedData>(
300        &self,
301        signed_obj: &str,
302    ) -> Result<T, SignedDataVerifierError> {
303        // Data is not signed by the App Store, and verification should be skipped.
304        // The environment MUST be checked in the public method calling this.
305        if self.environment == Environment::Xcode || self.environment == Environment::LocalTesting {
306            let _ = jws::decode_header(signed_obj)?;
307            return Ok(jws::decode_payload(signed_obj)?);
308        }
309
310        let header = jws::decode_header(signed_obj)?;
311
312        if header.alg.as_deref() != Some("ES256") {
313            return Err(SignedDataVerifierError::VerificationFailure);
314        }
315
316        let Some(x5c) = header.x5c else {
317            return Err(SignedDataVerifierError::VerificationFailure);
318        };
319
320        if x5c.len() != EXPECTED_CHAIN_LENGTH {
321            return Err(SignedDataVerifierError::InternalChainVerifierError(
322                ChainVerifierError::VerificationFailure(ChainVerificationFailureReason::InvalidChainLength),
323            ));
324        }
325
326        let chain: Vec<Vec<u8>> = x5c
327            .iter()
328            .map(|c| STANDARD.decode(c))
329            .collect::<Result<_, DecodeError>>()?;
330
331        let decoded_body: T = jws::decode_payload(signed_obj)?;
332
333        let effective_date = if self.enable_online_checks {
334            chrono::Utc::now().timestamp() as u64
335        } else {
336            match decoded_body.signed_date_optional() {
337                Some(date) => date.timestamp() as u64,
338                None => chrono::Utc::now().timestamp() as u64,
339            }
340        };
341
342        let spki = self.chain_verifier.verify(
343            &chain[0],
344            &chain[1],
345            Some(effective_date),
346            self.enable_online_checks,
347        )?;
348
349        let signature_bytes = jws::decode_signature_bytes(signed_obj)?;
350        let raw: [u8; 64] = signature_bytes
351            .try_into()
352            .map_err(|_| SignedDataVerifierError::VerificationFailure)?;
353
354        let provider = CryptoProvider::default_provider();
355        let public_key = provider
356            .p256_signing
357            .public_key(&spki)
358            .map_err(|_| SignedDataVerifierError::VerificationFailure)?;
359
360        let signing_input = jws::signing_input(signed_obj)?;
361        public_key
362            .is_valid_signature(&raw, signing_input.as_bytes())
363            .map_err(|_| SignedDataVerifierError::VerificationFailure)?;
364
365        Ok(decoded_body)
366    }
367}
368
369#[cfg(test)]
370mod tests {
371    use super::*;
372    use crate::chain_verifier::ChainVerificationFailureReason::InvalidChainLength;
373
374    #[test]
375    fn test_invalid_chain_length() {
376        // The length check happens before any signature verification, so a
377        // minimal unsigned JWS with a 4-element x5c is enough to exercise it
378        // through the public API.
379        let header = jws::b64url_encode(br#"{"alg":"ES256","x5c":["YQ","YQ","YQ","YQ"]}"#);
380        let payload = jws::b64url_encode(b"{}");
381        let signature = jws::b64url_encode(b"sig");
382        let jws_token = format!("{header}.{payload}.{signature}");
383
384        let verifier = SignedDataVerifier::new(
385            vec![Vec::new()],
386            Environment::Production,
387            "com.example".into(),
388            Some(1234),
389            false,
390        )
391        .expect("valid config");
392
393        let result = verifier.verify_and_decode_app_transaction(&jws_token);
394
395        assert!(matches!(
396            result.expect_err("expect error"),
397            SignedDataVerifierError::InternalChainVerifierError(ChainVerifierError::VerificationFailure(
398                InvalidChainLength
399            ))
400        ));
401    }
402}