app_store_server_library/
jws_signature_creator.rs1use 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
72struct 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
133pub struct PromotionalOfferV2SignatureCreator {
135 base: JWSSignatureCreator,
136}
137
138impl PromotionalOfferV2SignatureCreator {
139 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 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
203pub struct IntroductoryOfferEligibilitySignatureCreator {
205 base: JWSSignatureCreator,
206}
207
208impl IntroductoryOfferEligibilitySignatureCreator {
209 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 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
274pub struct AdvancedCommerceInAppSignatureCreator {
276 base: JWSSignatureCreator,
277}
278
279impl AdvancedCommerceInAppSignatureCreator {
280 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 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}