From 776812fce81cce092bbe0d3317231ba9564f4031 Mon Sep 17 00:00:00 2001 From: Alex Baker Date: Fri, 27 Oct 2023 15:26:02 -0700 Subject: [PATCH] Add support for Xcode and LocalTesting environments --- .../itunes/storekit/model/AppTransaction.java | 34 ++++++ .../itunes/storekit/model/Environment.java | 4 +- .../model/JWSRenewalInfoDecodedPayload.java | 5 + .../model/JWSTransactionDecodedPayload.java | 6 + .../XcodeCompatibleTimestampDeserializer.java | 20 ++++ .../verification/SignedDataVerifier.java | 19 +++- .../itunes/storekit/util/TestingUtility.java | 33 ++++++ .../XcodeSignedDataVerifierTest.java | 106 ++++++++++++++++++ src/test/resources/certs/testCA.der | Bin 0 -> 390 bytes .../resources/xcode/xcode-app-receipt-empty | 1 + .../xcode/xcode-app-receipt-with-transaction | 1 + .../xcode/xcode-signed-app-transaction | 1 + .../resources/xcode/xcode-signed-renewal-info | 1 + .../resources/xcode/xcode-signed-transaction | 1 + 14 files changed, 228 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/apple/itunes/storekit/model/XcodeCompatibleTimestampDeserializer.java create mode 100644 src/test/java/com/apple/itunes/storekit/util/TestingUtility.java create mode 100644 src/test/java/com/apple/itunes/storekit/verification/XcodeSignedDataVerifierTest.java create mode 100644 src/test/resources/certs/testCA.der create mode 100644 src/test/resources/xcode/xcode-app-receipt-empty create mode 100644 src/test/resources/xcode/xcode-app-receipt-with-transaction create mode 100644 src/test/resources/xcode/xcode-signed-app-transaction create mode 100644 src/test/resources/xcode/xcode-signed-renewal-info create mode 100644 src/test/resources/xcode/xcode-signed-transaction diff --git a/src/main/java/com/apple/itunes/storekit/model/AppTransaction.java b/src/main/java/com/apple/itunes/storekit/model/AppTransaction.java index 003ed450..01f8bbc1 100644 --- a/src/main/java/com/apple/itunes/storekit/model/AppTransaction.java +++ b/src/main/java/com/apple/itunes/storekit/model/AppTransaction.java @@ -1,7 +1,9 @@ package com.apple.itunes.storekit.model; +import com.google.gson.annotations.JsonAdapter; import com.google.gson.annotations.SerializedName; +import java.util.Objects; import java.util.UUID; /** @@ -35,8 +37,10 @@ public class AppTransaction implements DecodedSignedData { @SerializedName(SERIALIZED_NAME_VERSION_EXTERNAL_IDENTIFIER) private Long versionExternalIdentifier; @SerializedName(SERIALIZED_NAME_RECEIPT_CREATION_DATE) + @JsonAdapter(XcodeCompatibleTimestampDeserializer.class) private Long receiptCreationDate; @SerializedName(SERIALIZED_NAME_ORIGINAL_PURCHASE_DATE) + @JsonAdapter(XcodeCompatibleTimestampDeserializer.class) private Long originalPurchaseDate; @SerializedName(SERIALIZED_NAME_ORIGINAL_APPLICATION_VERSION) private String originalApplicationVersion; @@ -45,6 +49,7 @@ public class AppTransaction implements DecodedSignedData { @SerializedName(SERIALIZED_NAME_DEVICE_VERIFICATION_NONCE) private UUID deviceVerificationNonce; @SerializedName(SERIALIZED_NAME_PREORDER_DATE) + @JsonAdapter(XcodeCompatibleTimestampDeserializer.class) private Long preorderDate; /** @@ -250,5 +255,34 @@ public AppTransaction preorderDate(Long preorderDate) { public Long getSignedDate() { return getReceiptCreationDate(); } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + AppTransaction that = (AppTransaction) o; + return Objects.equals(receiptType, that.receiptType) && Objects.equals(appAppleId, that.appAppleId) && Objects.equals(bundleId, that.bundleId) && Objects.equals(applicationVersion, that.applicationVersion) && Objects.equals(versionExternalIdentifier, that.versionExternalIdentifier) && Objects.equals(originalPurchaseDate, that.originalPurchaseDate) && Objects.equals(originalApplicationVersion, that.originalApplicationVersion) && Objects.equals(deviceVerification, that.deviceVerification) && Objects.equals(deviceVerificationNonce, that.deviceVerificationNonce) && Objects.equals(preorderDate, that.preorderDate); + } + + @Override + public int hashCode() { + return Objects.hash(receiptType, appAppleId, bundleId, applicationVersion, versionExternalIdentifier, originalPurchaseDate, originalApplicationVersion, deviceVerification, deviceVerificationNonce, preorderDate); + } + + @Override + public String toString() { + return "AppTransaction{" + + "receiptType='" + receiptType + '\'' + + ", appAppleId=" + appAppleId + + ", bundleId='" + bundleId + '\'' + + ", applicationVersion='" + applicationVersion + '\'' + + ", versionExternalIdentifier=" + versionExternalIdentifier + + ", originalPurchaseDate=" + originalPurchaseDate + + ", originalApplicationVersion='" + originalApplicationVersion + '\'' + + ", deviceVerification='" + deviceVerification + '\'' + + ", deviceVerificationNonce=" + deviceVerificationNonce + + ", preorderDate=" + preorderDate + + '}'; + } } diff --git a/src/main/java/com/apple/itunes/storekit/model/Environment.java b/src/main/java/com/apple/itunes/storekit/model/Environment.java index 7adf0481..a7c48c55 100644 --- a/src/main/java/com/apple/itunes/storekit/model/Environment.java +++ b/src/main/java/com/apple/itunes/storekit/model/Environment.java @@ -18,7 +18,9 @@ public enum Environment { SANDBOX("Sandbox"), - PRODUCTION("Production"); + PRODUCTION("Production"), + XCODE("Xcode"), + LOCAL_TESTING("LocalTesting"); private final String value; diff --git a/src/main/java/com/apple/itunes/storekit/model/JWSRenewalInfoDecodedPayload.java b/src/main/java/com/apple/itunes/storekit/model/JWSRenewalInfoDecodedPayload.java index 1e77f2ab..7ef898b6 100644 --- a/src/main/java/com/apple/itunes/storekit/model/JWSRenewalInfoDecodedPayload.java +++ b/src/main/java/com/apple/itunes/storekit/model/JWSRenewalInfoDecodedPayload.java @@ -2,6 +2,7 @@ package com.apple.itunes.storekit.model; +import com.google.gson.annotations.JsonAdapter; import com.google.gson.annotations.SerializedName; import java.util.Objects; @@ -41,18 +42,22 @@ public class JWSRenewalInfoDecodedPayload implements DecodedSignedData { @SerializedName(SERIALIZED_NAME_PRICE_INCREASE_STATUS) private PriceIncreaseStatus priceIncreaseStatus; @SerializedName(SERIALIZED_NAME_GRACE_PERIOD_EXPIRES_DATE) + @JsonAdapter(XcodeCompatibleTimestampDeserializer.class) private Long gracePeriodExpiresDate; @SerializedName(SERIALIZED_NAME_OFFER_TYPE) private OfferType offerType; @SerializedName(SERIALIZED_NAME_OFFER_IDENTIFIER) private String offerIdentifier; @SerializedName(SERIALIZED_NAME_SIGNED_DATE) + @JsonAdapter(XcodeCompatibleTimestampDeserializer.class) private Long signedDate; @SerializedName(SERIALIZED_NAME_ENVIRONMENT) private Environment environment; @SerializedName(SERIALIZED_NAME_RECENT_SUBSCRIPTION_START_DATE) + @JsonAdapter(XcodeCompatibleTimestampDeserializer.class) private Long recentSubscriptionStartDate; @SerializedName(SERIALIZED_NAME_RENEWAL_DATE) + @JsonAdapter(XcodeCompatibleTimestampDeserializer.class) private Long renewalDate; diff --git a/src/main/java/com/apple/itunes/storekit/model/JWSTransactionDecodedPayload.java b/src/main/java/com/apple/itunes/storekit/model/JWSTransactionDecodedPayload.java index 232d3c62..2aca2909 100644 --- a/src/main/java/com/apple/itunes/storekit/model/JWSTransactionDecodedPayload.java +++ b/src/main/java/com/apple/itunes/storekit/model/JWSTransactionDecodedPayload.java @@ -2,6 +2,7 @@ package com.apple.itunes.storekit.model; +import com.google.gson.annotations.JsonAdapter; import com.google.gson.annotations.SerializedName; import java.util.Objects; @@ -49,10 +50,13 @@ public class JWSTransactionDecodedPayload implements DecodedSignedData { @SerializedName(SERIALIZED_NAME_SUBSCRIPTION_GROUP_IDENTIFIER) private String subscriptionGroupIdentifier; @SerializedName(SERIALIZED_NAME_PURCHASE_DATE) + @JsonAdapter(XcodeCompatibleTimestampDeserializer.class) private Long purchaseDate; @SerializedName(SERIALIZED_NAME_ORIGINAL_PURCHASE_DATE) + @JsonAdapter(XcodeCompatibleTimestampDeserializer.class) private Long originalPurchaseDate; @SerializedName(SERIALIZED_NAME_EXPIRES_DATE) + @JsonAdapter(XcodeCompatibleTimestampDeserializer.class) private Long expiresDate; @SerializedName(SERIALIZED_NAME_QUANTITY) private Integer quantity; @@ -63,10 +67,12 @@ public class JWSTransactionDecodedPayload implements DecodedSignedData { @SerializedName(SERIALIZED_NAME_IN_APP_OWNERSHIP_TYPE) private InAppOwnershipType inAppOwnershipType; @SerializedName(SERIALIZED_NAME_SIGNED_DATE) + @JsonAdapter(XcodeCompatibleTimestampDeserializer.class) private Long signedDate; @SerializedName(SERIALIZED_NAME_REVOCATION_REASON) private RevocationReason revocationReason; @SerializedName(SERIALIZED_NAME_REVOCATION_DATE) + @JsonAdapter(XcodeCompatibleTimestampDeserializer.class) private Long revocationDate; @SerializedName(SERIALIZED_NAME_IS_UPGRADED) private Boolean isUpgraded; diff --git a/src/main/java/com/apple/itunes/storekit/model/XcodeCompatibleTimestampDeserializer.java b/src/main/java/com/apple/itunes/storekit/model/XcodeCompatibleTimestampDeserializer.java new file mode 100644 index 00000000..ced0186b --- /dev/null +++ b/src/main/java/com/apple/itunes/storekit/model/XcodeCompatibleTimestampDeserializer.java @@ -0,0 +1,20 @@ +// Copyright (c) 2023 Apple Inc. Licensed under MIT License. + +package com.apple.itunes.storekit.model; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; + +import java.lang.reflect.Type; + +/** + * Xcode may sometimes return timestamps as floating point numbers not integers. This class allows parsing those receipts + */ +class XcodeCompatibleTimestampDeserializer implements JsonDeserializer { + @Override + public Long deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + return json.getAsJsonPrimitive().getAsNumber().longValue(); + } +} diff --git a/src/main/java/com/apple/itunes/storekit/verification/SignedDataVerifier.java b/src/main/java/com/apple/itunes/storekit/verification/SignedDataVerifier.java index 32ac34aa..a992995b 100644 --- a/src/main/java/com/apple/itunes/storekit/verification/SignedDataVerifier.java +++ b/src/main/java/com/apple/itunes/storekit/verification/SignedDataVerifier.java @@ -75,7 +75,11 @@ public JWSTransactionDecodedPayload verifyAndDecodeTransaction(String signedTran * @throws VerificationException Thrown if the data could not be verified */ public JWSRenewalInfoDecodedPayload verifyAndDecodeRenewalInfo(String signedRenewalInfo) throws VerificationException { - return decodeSignedObject(signedRenewalInfo, JWSRenewalInfoDecodedPayload.class); + JWSRenewalInfoDecodedPayload renewalInfo = decodeSignedObject(signedRenewalInfo, JWSRenewalInfoDecodedPayload.class); + if (!this.environment.equals(renewalInfo.getEnvironment())) { + throw new VerificationException(Status.INVALID_ENVIRONMENT); + } + return renewalInfo; } /** @@ -120,12 +124,16 @@ public AppTransaction verifyAndDecodeAppTransaction(String signedAppTransaction) protected T decodeSignedObject(String signedObject, Class clazz) throws VerificationException { try { DecodedJWT unverifiedJWT = JWT.decode(signedObject); + if (Environment.XCODE.equals(this.environment) || Environment.LOCAL_TESTING.equals(this.environment)) { + // Data is not signed by the App Store, and verification should be skipped + // The environment MUST be checked in the public method calling this + return parseJWTPayload(clazz, unverifiedJWT); + } String[] x5cChain = unverifiedJWT.getHeaderClaim("x5c").asArray(String.class); if (x5cChain == null) { throw new VerificationException(Status.VERIFICATION_FAILURE, "x5c claim was null"); } - String payload = new String(Base64.getUrlDecoder().decode(unverifiedJWT.getPayload())); - T decodedData = gson.fromJson(payload, clazz); + T decodedData = parseJWTPayload(clazz, unverifiedJWT); Date effectiveDate = this.enableOnlineChecks || decodedData.getSignedDate() == null ? new Date() : new Date(decodedData.getSignedDate()); PublicKey signingKey = chainVerifier.verifyChain(x5cChain, enableOnlineChecks, effectiveDate); if ("ES256".equals(unverifiedJWT.getAlgorithm())) { @@ -140,4 +148,9 @@ protected T decodeSignedObject(String signedObject throw new VerificationException(Status.VERIFICATION_FAILURE, e); } } + + protected T parseJWTPayload(Class clazz, DecodedJWT jwt) { + String payload = new String(Base64.getUrlDecoder().decode(jwt.getPayload())); + return gson.fromJson(payload, clazz); + } } diff --git a/src/test/java/com/apple/itunes/storekit/util/TestingUtility.java b/src/test/java/com/apple/itunes/storekit/util/TestingUtility.java new file mode 100644 index 00000000..9fe03df8 --- /dev/null +++ b/src/test/java/com/apple/itunes/storekit/util/TestingUtility.java @@ -0,0 +1,33 @@ +// Copyright (c) 2023 Apple Inc. Licensed under MIT License. + +package com.apple.itunes.storekit.util; + +import com.apple.itunes.storekit.model.Environment; +import com.apple.itunes.storekit.verification.SignedDataVerifier; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Set; + +public class TestingUtility { + + public static String readFile(String file) throws IOException { + return new String(readBytes(file), StandardCharsets.UTF_8); + } + + public static byte[] readBytes(String file) throws IOException { + try (InputStream stream = TestingUtility.class.getClassLoader().getResourceAsStream(file)) { + return stream.readAllBytes(); + } + } + + public static SignedDataVerifier getSignedPayloadVerifier(Environment environment, String bundleId) throws IOException { + return new SignedDataVerifier(Set.of(new ByteArrayInputStream(readBytes("certs/testCA.der"))), bundleId, 1234L, environment, false); + } + + public static SignedDataVerifier getSignedPayloadVerifier() throws IOException { + return getSignedPayloadVerifier(Environment.LOCAL_TESTING, "com.example"); + } +} diff --git a/src/test/java/com/apple/itunes/storekit/verification/XcodeSignedDataVerifierTest.java b/src/test/java/com/apple/itunes/storekit/verification/XcodeSignedDataVerifierTest.java new file mode 100644 index 00000000..2c7536c6 --- /dev/null +++ b/src/test/java/com/apple/itunes/storekit/verification/XcodeSignedDataVerifierTest.java @@ -0,0 +1,106 @@ +// Copyright (c) 2023 Apple Inc. Licensed under MIT License. + +package com.apple.itunes.storekit.verification; + +import com.apple.itunes.storekit.model.AppTransaction; +import com.apple.itunes.storekit.model.AutoRenewStatus; +import com.apple.itunes.storekit.model.Environment; +import com.apple.itunes.storekit.model.InAppOwnershipType; +import com.apple.itunes.storekit.model.JWSRenewalInfoDecodedPayload; +import com.apple.itunes.storekit.model.JWSTransactionDecodedPayload; +import com.apple.itunes.storekit.model.OfferType; +import com.apple.itunes.storekit.model.TransactionReason; +import com.apple.itunes.storekit.model.Type; +import com.apple.itunes.storekit.util.TestingUtility; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.UUID; + +public class XcodeSignedDataVerifierTest { + + private final String XCODE_BUNDLE_ID = "com.example.naturelab.backyardbirds.example"; + + @Test + public void testXcodeSignedAppTransaction() throws IOException, VerificationException { + SignedDataVerifier verifier = TestingUtility.getSignedPayloadVerifier(Environment.XCODE, XCODE_BUNDLE_ID); + String encodedAppTransaction = TestingUtility.readFile("xcode/xcode-signed-app-transaction"); + + AppTransaction appTransaction = verifier.verifyAndDecodeAppTransaction(encodedAppTransaction); + + Assertions.assertNotNull(appTransaction); + Assertions.assertNull(appTransaction.getAppAppleId()); + Assertions.assertEquals(XCODE_BUNDLE_ID, appTransaction.getBundleId()); + Assertions.assertEquals("1", appTransaction.getApplicationVersion()); + Assertions.assertNull(appTransaction.versionExternalIdentifier()); + Assertions.assertEquals(-62135769600000L, appTransaction.originalPurchaseDate()); + Assertions.assertEquals("1", appTransaction.getOriginalApplicationVersion()); + Assertions.assertEquals("cYUsXc53EbYc0pOeXG5d6/31LGHeVGf84sqSN0OrJi5u/j2H89WWKgS8N0hMsMlf", appTransaction.getDeviceVerification()); + Assertions.assertEquals(UUID.fromString("48c8b92d-ce0d-4229-bedf-e61b4f9cfc92"), appTransaction.getDeviceVerificationNonce()); + Assertions.assertNull(appTransaction.getPreorderDate()); + } + + @Test + public void testXcodeSignedTransaction() throws IOException, VerificationException { + SignedDataVerifier verifier = TestingUtility.getSignedPayloadVerifier(Environment.XCODE, XCODE_BUNDLE_ID); + String encodedTransactino = TestingUtility.readFile("xcode/xcode-signed-transaction"); + + JWSTransactionDecodedPayload transaction = verifier.verifyAndDecodeTransaction(encodedTransactino); + + Assertions.assertEquals("0", transaction.getOriginalTransactionId()); + Assertions.assertEquals("0", transaction.getTransactionId()); + Assertions.assertEquals("0", transaction.getWebOrderLineItemId()); + Assertions.assertEquals(XCODE_BUNDLE_ID, transaction.getBundleId()); + Assertions.assertEquals("pass.premium", transaction.getProductId()); + Assertions.assertEquals("6F3A93AB", transaction.getSubscriptionGroupIdentifier()); + Assertions.assertEquals(1697679936049L, transaction.getPurchaseDate()); + Assertions.assertEquals(1697679936049L, transaction.getOriginalPurchaseDate()); + Assertions.assertEquals(1700358336049L, transaction.getExpiresDate()); + Assertions.assertEquals(1, transaction.getQuantity()); + Assertions.assertEquals(Type.AUTO_RENEWABLE_SUBSCRIPTION, transaction.getType()); + Assertions.assertNull(transaction.getAppAccountToken()); + Assertions.assertEquals(InAppOwnershipType.PURCHASED, transaction.getInAppOwnershipType()); + Assertions.assertEquals(1697679936056L, transaction.getSignedDate()); + Assertions.assertNull(transaction.getRevocationReason()); + Assertions.assertNull(transaction.getRevocationDate()); + Assertions.assertFalse(transaction.getIsUpgraded()); + Assertions.assertEquals(OfferType.INTRODUCTORY_OFFER, transaction.getOfferType()); + Assertions.assertNull(transaction.getOfferIdentifier()); + Assertions.assertEquals(Environment.XCODE, transaction.getEnvironment()); + Assertions.assertEquals("USA", transaction.getStorefront()); + Assertions.assertEquals("143441", transaction.getStorefrontId()); + Assertions.assertEquals(TransactionReason.PURCHASE, transaction.getTransactionReason()); + } + + @Test + public void testXcodeSignedRenewalInfo() throws IOException, VerificationException { + SignedDataVerifier verifier = TestingUtility.getSignedPayloadVerifier(Environment.XCODE, XCODE_BUNDLE_ID); + String encodedRenewalInfo = TestingUtility.readFile("xcode/xcode-signed-renewal-info"); + + JWSRenewalInfoDecodedPayload renewalInfo = verifier.verifyAndDecodeRenewalInfo(encodedRenewalInfo); + + System.out.println(renewalInfo); + Assertions.assertNull(renewalInfo.getExpirationIntent()); + Assertions.assertEquals("0", renewalInfo.getOriginalTransactionId()); + Assertions.assertEquals("pass.premium", renewalInfo.getAutoRenewProductId()); + Assertions.assertEquals("pass.premium", renewalInfo.getProductId()); + Assertions.assertEquals(AutoRenewStatus.ON, renewalInfo.getAutoRenewStatus()); + Assertions.assertNull(renewalInfo.getIsInBillingRetryPeriod()); + Assertions.assertNull(renewalInfo.getPriceIncreaseStatus()); + Assertions.assertNull(renewalInfo.getGracePeriodExpiresDate()); + Assertions.assertNull(renewalInfo.getOfferType()); + Assertions.assertNull(renewalInfo.getOfferIdentifier()); + Assertions.assertEquals(1697679936711L, renewalInfo.getSignedDate()); + Assertions.assertEquals(Environment.XCODE, renewalInfo.getEnvironment()); + Assertions.assertEquals(1697679936049L, renewalInfo.getRecentSubscriptionStartDate()); + Assertions.assertEquals(1700358336049L, renewalInfo.getRenewalDate()); + } + + @Test + public void testXcodeSignedAppTransactionWithProductionEnvironment() throws IOException { + SignedDataVerifier verifier = TestingUtility.getSignedPayloadVerifier(Environment.PRODUCTION, XCODE_BUNDLE_ID); + String encodedAppTransaction = TestingUtility.readFile("xcode/xcode-signed-app-transaction"); + Assertions.assertThrows(VerificationException.class, () -> verifier.verifyAndDecodeAppTransaction(encodedAppTransaction)); + } +} diff --git a/src/test/resources/certs/testCA.der b/src/test/resources/certs/testCA.der new file mode 100644 index 0000000000000000000000000000000000000000..54a42b6819d776228bc61c5284cd96772880748c GIT binary patch literal 390 zcmXqLVr(*KV$@u~%*4pV#L2K#<_Xgy`B~8hTx=X#Z64=rS(up(%nZ2=IN6v(S=fY` zLW2#34Fo|P4jwM&#GK5u{Gz{e{M+Syo&BN50dt+ZE=&qlsT1Og8A{FO-k5MQs=_w7 zyOCvI8uQkU%M*VusJrt@g-M}e;))Yz>rEL>Y42@cbuReRzD|=8<-a%nT$#49@cPf? FG5{`Qc$NSF literal 0 HcmV?d00001 diff --git a/src/test/resources/xcode/xcode-app-receipt-empty b/src/test/resources/xcode/xcode-app-receipt-empty new file mode 100644 index 00000000..f5acce89 --- /dev/null +++ b/src/test/resources/xcode/xcode-app-receipt-empty @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/src/test/resources/xcode/xcode-app-receipt-with-transaction b/src/test/resources/xcode/xcode-app-receipt-with-transaction new file mode 100644 index 00000000..6b2d6e7b --- /dev/null +++ b/src/test/resources/xcode/xcode-app-receipt-with-transaction @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/src/test/resources/xcode/xcode-signed-app-transaction b/src/test/resources/xcode/xcode-signed-app-transaction new file mode 100644 index 00000000..3d113267 --- /dev/null +++ b/src/test/resources/xcode/xcode-signed-app-transaction @@ -0,0 +1 @@ +eyJ4NWMiOlsiTUlJQnpEQ0NBWEdnQXdJQkFnSUJBVEFLQmdncWhrak9QUVFEQWpCSU1TSXdJQVlEVlFRREV4bFRkRzl5WlV0cGRDQlVaWE4wYVc1bklHbHVJRmhqYjJSbE1TSXdJQVlEVlFRS0V4bFRkRzl5WlV0cGRDQlVaWE4wYVc1bklHbHVJRmhqYjJSbE1CNFhEVEl6TVRBeE9UQXhORFV6TmxvWERUSTBNVEF4T0RBeE5EVXpObG93U0RFaU1DQUdBMVVFQXhNWlUzUnZjbVZMYVhRZ1ZHVnpkR2x1WnlCcGJpQllZMjlrWlRFaU1DQUdBMVVFQ2hNWlUzUnZjbVZMYVhRZ1ZHVnpkR2x1WnlCcGJpQllZMjlrWlRCWk1CTUdCeXFHU000OUFnRUdDQ3FHU000OUF3RUhBMElBQktYRVFnWWpDb3VQdFRzdEdyS3BZOEk1M25IN3JiREhuY0lMR25vZ1NBdWxJSTNzXC91Zk0wZzlEYzNCY3I0OTdBVWd6R1R2V3Bpd0p4cGVCMzcxTmdWK2pUREJLTUJJR0ExVWRFd0VCXC93UUlNQVlCQWY4Q0FRQXdKQVlEVlIwUkJCMHdHNEVaVTNSdmNtVkxhWFFnVkdWemRHbHVaeUJwYmlCWVkyOWtaVEFPQmdOVkhROEJBZjhFQkFNQ0I0QXdDZ1lJS29aSXpqMEVBd0lEU1FBd1JnSWhBTVp2VllKNjRDRitoMmZtc213dnpBY2VQcklEMTNycElKR0JFVytXZ3BwdEFpRUF4V2l5NCtUMXp0MzdWc3UwdmI2WXVtMCtOTHREcUhsSzZycE1jdjZKZm5BPSJdLCJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IkFwcGxlX1hjb2RlX0tleSJ9.eyJidW5kbGVJZCI6ImNvbS5leGFtcGxlLm5hdHVyZWxhYi5iYWNreWFyZGJpcmRzLmV4YW1wbGUiLCJhcHBsaWNhdGlvblZlcnNpb24iOiIxIiwiZGV2aWNlVmVyaWZpY2F0aW9uTm9uY2UiOiI0OGM4YjkyZC1jZTBkLTQyMjktYmVkZi1lNjFiNGY5Y2ZjOTIiLCJyZWNlaXB0VHlwZSI6Ilhjb2RlIiwicmVjZWlwdENyZWF0aW9uRGF0ZSI6MTY5NzY4MDEyMjI1Ny40NDcsImRldmljZVZlcmlmaWNhdGlvbiI6ImNZVXNYYzUzRWJZYzBwT2VYRzVkNlwvMzFMR0hlVkdmODRzcVNOME9ySmk1dVwvajJIODlXV0tnUzhOMGhNc01sZiIsInJlcXVlc3REYXRlIjoxNjk3NjgwMTIyMjU3LjQ0Nywib3JpZ2luYWxBcHBsaWNhdGlvblZlcnNpb24iOiIxIiwib3JpZ2luYWxQdXJjaGFzZURhdGUiOi02MjEzNTc2OTYwMDAwMH0.Dpdk_VsO2MUCevwyS407alJpPc1Nq_UIP9EiDHaQBxlyi35NFnsKUVNuFNcGWrGRCCImnb4QGBKHfQC2i4sPCg \ No newline at end of file diff --git a/src/test/resources/xcode/xcode-signed-renewal-info b/src/test/resources/xcode/xcode-signed-renewal-info new file mode 100644 index 00000000..d18123c4 --- /dev/null +++ b/src/test/resources/xcode/xcode-signed-renewal-info @@ -0,0 +1 @@ +eyJraWQiOiJBcHBsZV9YY29kZV9LZXkiLCJ0eXAiOiJKV1QiLCJ4NWMiOlsiTUlJQnpEQ0NBWEdnQXdJQkFnSUJBVEFLQmdncWhrak9QUVFEQWpCSU1TSXdJQVlEVlFRREV4bFRkRzl5WlV0cGRDQlVaWE4wYVc1bklHbHVJRmhqYjJSbE1TSXdJQVlEVlFRS0V4bFRkRzl5WlV0cGRDQlVaWE4wYVc1bklHbHVJRmhqYjJSbE1CNFhEVEl6TVRBeE9UQXhORFV6TmxvWERUSTBNVEF4T0RBeE5EVXpObG93U0RFaU1DQUdBMVVFQXhNWlUzUnZjbVZMYVhRZ1ZHVnpkR2x1WnlCcGJpQllZMjlrWlRFaU1DQUdBMVVFQ2hNWlUzUnZjbVZMYVhRZ1ZHVnpkR2x1WnlCcGJpQllZMjlrWlRCWk1CTUdCeXFHU000OUFnRUdDQ3FHU000OUF3RUhBMElBQktYRVFnWWpDb3VQdFRzdEdyS3BZOEk1M25IN3JiREhuY0lMR25vZ1NBdWxJSTNzXC91Zk0wZzlEYzNCY3I0OTdBVWd6R1R2V3Bpd0p4cGVCMzcxTmdWK2pUREJLTUJJR0ExVWRFd0VCXC93UUlNQVlCQWY4Q0FRQXdKQVlEVlIwUkJCMHdHNEVaVTNSdmNtVkxhWFFnVkdWemRHbHVaeUJwYmlCWVkyOWtaVEFPQmdOVkhROEJBZjhFQkFNQ0I0QXdDZ1lJS29aSXpqMEVBd0lEU1FBd1JnSWhBTVp2VllKNjRDRitoMmZtc213dnpBY2VQcklEMTNycElKR0JFVytXZ3BwdEFpRUF4V2l5NCtUMXp0MzdWc3UwdmI2WXVtMCtOTHREcUhsSzZycE1jdjZKZm5BPSJdLCJhbGciOiJFUzI1NiJ9.eyJkZXZpY2VWZXJpZmljYXRpb24iOiJ1K1cxb1FUcXZGSE9RK1pCZTRRMHhQTUMyOGtxRUZ2YmJzRVBwTEtEVlJGdjFHSkdlZ21yTkhWb09ZTU9QdmIyIiwicHJvZHVjdElkIjoicGFzcy5wcmVtaXVtIiwiZGV2aWNlVmVyaWZpY2F0aW9uTm9uY2UiOiIzNDM5OTE5ZS04N2M5LTQ3YjYtYWVlZS0yODIzZjdhOWQzYzMiLCJyZW5ld2FsRGF0ZSI6MTcwMDM1ODMzNjA0OS43Mjk3LCJvcmlnaW5hbFRyYW5zYWN0aW9uSWQiOiIwIiwicmVjZW50U3Vic2NyaXB0aW9uU3RhcnREYXRlIjoxNjk3Njc5OTM2MDQ5LjcyOTcsImF1dG9SZW5ld1N0YXR1cyI6MSwic2lnbmVkRGF0ZSI6MTY5NzY3OTkzNjcxMS4wNzQ3LCJlbnZpcm9ubWVudCI6Ilhjb2RlIiwiYXV0b1JlbmV3UHJvZHVjdElkIjoicGFzcy5wcmVtaXVtIn0.WnT3aB9Lwjbr0ICUGn_5CdglzedVd7eOkrqirhcWFvwJZzN1FajuMV6gFEbgD82aL0Ix6HGZcwkNDlVNLvYOEQ \ No newline at end of file diff --git a/src/test/resources/xcode/xcode-signed-transaction b/src/test/resources/xcode/xcode-signed-transaction new file mode 100644 index 00000000..6daf5179 --- /dev/null +++ b/src/test/resources/xcode/xcode-signed-transaction @@ -0,0 +1 @@ +eyJraWQiOiJBcHBsZV9YY29kZV9LZXkiLCJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsIng1YyI6WyJNSUlCekRDQ0FYR2dBd0lCQWdJQkFUQUtCZ2dxaGtqT1BRUURBakJJTVNJd0lBWURWUVFERXhsVGRHOXlaVXRwZENCVVpYTjBhVzVuSUdsdUlGaGpiMlJsTVNJd0lBWURWUVFLRXhsVGRHOXlaVXRwZENCVVpYTjBhVzVuSUdsdUlGaGpiMlJsTUI0WERUSXpNVEF4T1RBeE5EVXpObG9YRFRJME1UQXhPREF4TkRVek5sb3dTREVpTUNBR0ExVUVBeE1aVTNSdmNtVkxhWFFnVkdWemRHbHVaeUJwYmlCWVkyOWtaVEVpTUNBR0ExVUVDaE1aVTNSdmNtVkxhWFFnVkdWemRHbHVaeUJwYmlCWVkyOWtaVEJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUFCS1hFUWdZakNvdVB0VHN0R3JLcFk4STUzbkg3cmJESG5jSUxHbm9nU0F1bElJM3NcL3VmTTBnOURjM0JjcjQ5N0FVZ3pHVHZXcGl3SnhwZUIzNzFOZ1YralREQktNQklHQTFVZEV3RUJcL3dRSU1BWUJBZjhDQVFBd0pBWURWUjBSQkIwd0c0RVpVM1J2Y21WTGFYUWdWR1Z6ZEdsdVp5QnBiaUJZWTI5a1pUQU9CZ05WSFE4QkFmOEVCQU1DQjRBd0NnWUlLb1pJemowRUF3SURTUUF3UmdJaEFNWnZWWUo2NENGK2gyZm1zbXd2ekFjZVBySUQxM3JwSUpHQkVXK1dncHB0QWlFQXhXaXk0K1QxenQzN1ZzdTB2YjZZdW0wK05MdERxSGxLNnJwTWN2NkpmbkE9Il19.eyJpbkFwcE93bmVyc2hpcFR5cGUiOiJQVVJDSEFTRUQiLCJwdXJjaGFzZURhdGUiOjE2OTc2Nzk5MzYwNDkuNzI5Nywic3Vic2NyaXB0aW9uR3JvdXBJZGVudGlmaWVyIjoiNkYzQTkzQUIiLCJzaWduZWREYXRlIjoxNjk3Njc5OTM2MDU2LjQ4NSwib3JpZ2luYWxQdXJjaGFzZURhdGUiOjE2OTc2Nzk5MzYwNDkuNzI5NywiaXNVcGdyYWRlZCI6ZmFsc2UsImRldmljZVZlcmlmaWNhdGlvbiI6InNHRG5wZytvemI4dXdEU3VDRFoyb1ZabzFDS3JiQjh1alI4VnhDeGh5a1J3eUJJSzZ4NlhDeUVSbTh5V3J6RTgiLCJvZmZlclR5cGUiOjEsInF1YW50aXR5IjoxLCJ0cmFuc2FjdGlvbklkIjoiMCIsInR5cGUiOiJBdXRvLVJlbmV3YWJsZSBTdWJzY3JpcHRpb24iLCJ0cmFuc2FjdGlvblJlYXNvbiI6IlBVUkNIQVNFIiwicHJvZHVjdElkIjoicGFzcy5wcmVtaXVtIiwiZXhwaXJlc0RhdGUiOjE3MDAzNTgzMzYwNDkuNzI5NywiZW52aXJvbm1lbnQiOiJYY29kZSIsInN0b3JlZnJvbnRJZCI6IjE0MzQ0MSIsIm9yaWdpbmFsVHJhbnNhY3Rpb25JZCI6IjAiLCJidW5kbGVJZCI6ImNvbS5leGFtcGxlLm5hdHVyZWxhYi5iYWNreWFyZGJpcmRzLmV4YW1wbGUiLCJkZXZpY2VWZXJpZmljYXRpb25Ob25jZSI6IjdlZGVhODdkLTk4ZjAtNDJkMC05NjgyLTQ5Y2E4MTAyMmY3MyIsIndlYk9yZGVyTGluZUl0ZW1JZCI6IjAiLCJzdG9yZWZyb250IjoiVVNBIn0.rkJYnvujStteRkMHhoIR2ThmNFnyKcx5XxIakXYdh-1oKtEVEU5zQAiONaLDpBDO5JhLLrTbfp7LS5tMiqmgHw \ No newline at end of file