Skip to main content

app_store_server_library/
jws_signature_creator.rs

1use base64::engine::general_purpose::STANDARD as BASE64;
2use base64::Engine;
3use chrono::Utc;
4use serde::{Deserialize, Serialize};
5use thiserror::Error;
6use uuid::Uuid;
7
8use crate::crypto::{jws, CryptoError, CryptoProvider, P256PrivateKey};
9use crate::models::advanced_commerce_in_app_request::AdvancedCommerceInAppRequest;
10
11#[derive(Error, Debug)]
12pub enum JWSSignatureCreatorError {
13    #[error("InvalidPrivateKey")]
14    InvalidPrivateKey,
15
16    #[error("SigningError: [{0}]")]
17    SigningError(String),
18
19    #[error("SerializationError: [{0}]")]
20    SerializationError(#[from] serde_json::Error),
21}
22
23impl From<CryptoError> for JWSSignatureCreatorError {
24    fn from(e: CryptoError) -> Self {
25        match e {
26            CryptoError::KeyError(_) => Self::InvalidPrivateKey,
27            CryptoError::SigningError(m) | CryptoError::VerificationError(m) => Self::SigningError(m),
28        }
29    }
30}
31
32#[derive(Debug, Serialize, Deserialize)]
33struct BasePayload {
34    nonce: String,
35    iss: String,
36    bid: String,
37    aud: String,
38    iat: i64,
39}
40
41#[derive(Debug, Serialize, Deserialize)]
42struct PromotionalOfferV2Payload {
43    #[serde(flatten)]
44    base: BasePayload,
45    #[serde(rename = "productId")]
46    product_id: String,
47    #[serde(rename = "offerIdentifier")]
48    offer_identifier: String,
49    #[serde(rename = "transactionId", skip_serializing_if = "Option::is_none")]
50    transaction_id: Option<String>,
51}
52
53#[derive(Debug, Serialize, Deserialize)]
54struct IntroductoryOfferEligibilityPayload {
55    #[serde(flatten)]
56    base: BasePayload,
57    #[serde(rename = "productId")]
58    product_id: String,
59    #[serde(rename = "allowIntroductoryOffer")]
60    allow_introductory_offer: bool,
61    #[serde(rename = "transactionId")]
62    transaction_id: String,
63}
64
65#[derive(Debug, Serialize, Deserialize)]
66struct AdvancedCommerceInAppPayload {
67    #[serde(flatten)]
68    base: BasePayload,
69    request: String,
70}
71
72/// Base struct for creating JWS signatures for App Store requests
73struct JWSSignatureCreator {
74    audience: String,
75    signing_key: Box<dyn P256PrivateKey>,
76    key_id: String,
77    issuer_id: String,
78    bundle_id: String,
79}
80
81impl JWSSignatureCreator {
82    fn new(
83        audience: String,
84        signing_key: &str,
85        key_id: String,
86        issuer_id: String,
87        bundle_id: String,
88    ) -> Result<Self, JWSSignatureCreatorError> {
89        let provider = CryptoProvider::default_provider();
90        let key = provider
91            .p256_signing
92            .private_key(signing_key)
93            .map_err(|_| JWSSignatureCreatorError::InvalidPrivateKey)?;
94
95        Ok(Self {
96            audience,
97            signing_key: key,
98            key_id,
99            issuer_id,
100            bundle_id,
101        })
102    }
103
104    fn get_base_payload(&self) -> BasePayload {
105        BasePayload {
106            nonce: Uuid::new_v4().to_string(),
107            iss: self.issuer_id.clone(),
108            bid: self.bundle_id.clone(),
109            aud: self.audience.clone(),
110            iat: Utc::now().timestamp(),
111        }
112    }
113
114    fn create_signature<T: Serialize>(&self, payload: &T) -> Result<String, JWSSignatureCreatorError> {
115        let header = serde_json::json!({
116            "alg": "ES256",
117            "kid": self.key_id,
118            "typ": "JWT",
119        });
120
121        let encoded_header = jws::b64url_encode(&serde_json::to_vec(&header)?);
122        let encoded_payload = jws::b64url_encode(&serde_json::to_vec(payload)?);
123        let signing_input = format!("{encoded_header}.{encoded_payload}");
124
125        let (raw, _) = self
126            .signing_key
127            .signature(signing_input.as_bytes())?;
128
129        Ok(jws::encode_compact(&encoded_header, &encoded_payload, &raw))
130    }
131}
132
133/// Creator for Promotional AdvancedCommerceOffer V2 signatures
134pub struct PromotionalOfferV2SignatureCreator {
135    base: JWSSignatureCreator,
136}
137
138impl PromotionalOfferV2SignatureCreator {
139    /// Creates a new `PromotionalOfferV2SignatureCreator` instance.
140    ///
141    /// # Arguments
142    ///
143    /// * `signing_key` - Your private key downloaded from App Store Connect (in PEM format)
144    /// * `key_id` - Your key ID from the Keys page in App Store Connect
145    /// * `issuer_id` - Your issuer ID from the Keys page in App Store Connect
146    /// * `bundle_id` - Your app's bundle ID
147    ///
148    /// # Returns
149    ///
150    /// A `Result` containing the `PromotionalOfferV2SignatureCreator` instance or an error.
151    pub fn new(
152        signing_key: &str,
153        key_id: String,
154        issuer_id: String,
155        bundle_id: String,
156    ) -> Result<Self, JWSSignatureCreatorError> {
157        let base = JWSSignatureCreator::new(
158            "promotional-offer".to_string(),
159            signing_key,
160            key_id,
161            issuer_id,
162            bundle_id,
163        )?;
164
165        Ok(Self { base })
166    }
167
168    /// Creates a promotional offer V2 signature.
169    ///
170    /// # Arguments
171    ///
172    /// * `product_id` - The unique identifier of the product
173    /// * `offer_identifier` - The promotional offer identifier that you set up in App Store Connect
174    /// * `transaction_id` - The unique identifier of any transaction that belongs to the customer.
175    ///   You can use the customer's appTransactionId, even for customers who haven't made any
176    ///   In-App Purchases in your app. This field is optional, but recommended.
177    ///
178    /// # Returns
179    ///
180    /// A `Result` containing the signed JWS string or an error.
181    ///
182    /// # References
183    ///
184    /// [Generating JWS to sign App Store requests](https://developer.apple.com/documentation/storekit/generating-jws-to-sign-app-store-requests)
185    pub fn create_signature(
186        &self,
187        product_id: &str,
188        offer_identifier: &str,
189        transaction_id: Option<String>,
190    ) -> Result<String, JWSSignatureCreatorError> {
191        let base_payload = self.base.get_base_payload();
192        let payload = PromotionalOfferV2Payload {
193            base: base_payload,
194            product_id: product_id.to_string(),
195            offer_identifier: offer_identifier.to_string(),
196            transaction_id,
197        };
198
199        self.base.create_signature(&payload)
200    }
201}
202
203/// Creator for Introductory AdvancedCommerceOffer Eligibility signatures
204pub struct IntroductoryOfferEligibilitySignatureCreator {
205    base: JWSSignatureCreator,
206}
207
208impl IntroductoryOfferEligibilitySignatureCreator {
209    /// Creates a new `IntroductoryOfferEligibilitySignatureCreator` instance.
210    ///
211    /// # Arguments
212    ///
213    /// * `signing_key` - Your private key downloaded from App Store Connect (in PEM format)
214    /// * `key_id` - Your key ID from the Keys page in App Store Connect
215    /// * `issuer_id` - Your issuer ID from the Keys page in App Store Connect
216    /// * `bundle_id` - Your app's bundle ID
217    ///
218    /// # Returns
219    ///
220    /// A `Result` containing the `IntroductoryOfferEligibilitySignatureCreator` instance or an error.
221    pub fn new(
222        signing_key: &str,
223        key_id: String,
224        issuer_id: String,
225        bundle_id: String,
226    ) -> Result<Self, JWSSignatureCreatorError> {
227        let base = JWSSignatureCreator::new(
228            "introductory-offer-eligibility".to_string(),
229            signing_key,
230            key_id,
231            issuer_id,
232            bundle_id,
233        )?;
234
235        Ok(Self { base })
236    }
237
238    /// Creates an introductory offer eligibility signature.
239    ///
240    /// # Arguments
241    ///
242    /// * `product_id` - The unique identifier of the product
243    /// * `allow_introductory_offer` - A boolean value that determines whether the customer
244    ///   is eligible for an introductory offer
245    /// * `transaction_id` - The unique identifier of any transaction that belongs to the customer.
246    ///   You can use the customer's appTransactionId, even for customers who haven't made any
247    ///   In-App Purchases in your app.
248    ///
249    /// # Returns
250    ///
251    /// A `Result` containing the signed JWS string or an error.
252    ///
253    /// # References
254    ///
255    /// [Generating JWS to sign App Store requests](https://developer.apple.com/documentation/storekit/generating-jws-to-sign-app-store-requests)
256    pub fn create_signature(
257        &self,
258        product_id: &str,
259        allow_introductory_offer: bool,
260        transaction_id: &str,
261    ) -> Result<String, JWSSignatureCreatorError> {
262        let base_payload = self.base.get_base_payload();
263        let payload = IntroductoryOfferEligibilityPayload {
264            base: base_payload,
265            product_id: product_id.to_string(),
266            allow_introductory_offer,
267            transaction_id: transaction_id.to_string(),
268        };
269
270        self.base.create_signature(&payload)
271    }
272}
273
274/// Creator for Advanced Commerce In-App signatures
275pub struct AdvancedCommerceInAppSignatureCreator {
276    base: JWSSignatureCreator,
277}
278
279impl AdvancedCommerceInAppSignatureCreator {
280    /// Creates a new `AdvancedCommerceInAppSignatureCreator` instance.
281    ///
282    /// # Arguments
283    ///
284    /// * `signing_key` - Your private key downloaded from App Store Connect (in PEM format)
285    /// * `key_id` - Your key ID from the Keys page in App Store Connect
286    /// * `issuer_id` - Your issuer ID from the Keys page in App Store Connect
287    /// * `bundle_id` - Your app's bundle ID
288    ///
289    /// # Returns
290    ///
291    /// A `Result` containing the `AdvancedCommerceInAppSignatureCreator` instance or an error.
292    pub fn new(
293        signing_key: &str,
294        key_id: String,
295        issuer_id: String,
296        bundle_id: String,
297    ) -> Result<Self, JWSSignatureCreatorError> {
298        let base = JWSSignatureCreator::new(
299            "advanced-commerce-api".to_string(),
300            signing_key,
301            key_id,
302            issuer_id,
303            bundle_id,
304        )?;
305
306        Ok(Self { base })
307    }
308
309    /// Creates an Advanced Commerce in-app signed request.
310    ///
311    /// # Arguments
312    ///
313    /// * `advanced_commerce_in_app_request` - The request to be signed.
314    ///
315    /// # Returns
316    ///
317    /// A `Result` containing the signed JWS string or an error.
318    ///
319    /// # References
320    ///
321    /// [Generating JWS to sign App Store requests](https://developer.apple.com/documentation/storekit/generating-jws-to-sign-app-store-requests)
322    pub fn create_signature<T: AdvancedCommerceInAppRequest>(
323        &self,
324        advanced_commerce_in_app_request: &T,
325    ) -> Result<String, JWSSignatureCreatorError> {
326        let json_data = serde_json::to_vec(advanced_commerce_in_app_request)?;
327        let base64_encoded_body = BASE64.encode(&json_data);
328
329        let base_payload = self.base.get_base_payload();
330        let payload = AdvancedCommerceInAppPayload {
331            base: base_payload,
332            request: base64_encoded_body,
333        };
334
335        self.base.create_signature(&payload)
336    }
337}