mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-10-15 11:10:30 +00:00
badge-service: accept a list of Apple roots (#7707)
* badge-service: accept a list of Apple roots * badge-service: document root_certificates --------- Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
This commit is contained in:
@@ -213,25 +213,30 @@ two included, answers `method_not_allowed`.
|
||||
|
||||
Each store is verified only when its section is present; without one, every receipt from that store
|
||||
is answered `provider_not_configured` and the app keeps the purchase to present again. A section
|
||||
missing a key, an unreadable root certificate or service account key, or either section beside
|
||||
missing a key, a root certificate that cannot be read or is not a root, an unreadable service
|
||||
account key, or either section beside
|
||||
`[dev] accept_unverified_store_receipts = on`, stops the service at startup.
|
||||
|
||||
```ini
|
||||
[apple]
|
||||
bundle_id = chat.simplex.app
|
||||
root_certificate = /etc/simplex-badge-service/AppleRootCA-G3.cer
|
||||
root_certificates = /etc/simplex-badge-service/AppleRootCA-G3.cer
|
||||
|
||||
[google]
|
||||
package_name = chat.simplex.app
|
||||
service_account_file = /etc/simplex-badge-service/play-service-account.json
|
||||
```
|
||||
|
||||
- `[apple]` verifies the signed transaction offline, against Apple Root CA - G3 in DER as Apple
|
||||
publishes it at <https://www.apple.com/certificateauthority/AppleRootCA-G3.cer>. A transaction
|
||||
for another bundle id, or a chain this root does not verify, is answered `internal` and logged as
|
||||
an error rather than refused, since `bundle_id` or `root_certificate` may be the wrong one; check
|
||||
both when those errors appear, unless the purchase was made in Xcode's StoreKit testing, which
|
||||
the App Store does not sign, so no root verifies it.
|
||||
- `[apple]` verifies the signed transaction offline, against the roots `root_certificates` lists,
|
||||
comma-separated, each in DER. Today that is Apple Root CA - G3 as Apple publishes it at
|
||||
<https://www.apple.com/certificateauthority/AppleRootCA-G3.cer>. Each must be self-signed.
|
||||
Verification reads no validity period, so an expired root keeps verifying. A transaction for
|
||||
another bundle id, or an intermediate no listed root signed, is answered `internal` and logged as
|
||||
an error rather than refused, since `bundle_id` may be wrong or a root missing; check both when those errors appear, unless the purchase was made in Xcode's
|
||||
StoreKit testing, which the App Store does not sign, so no root verifies it. When Apple signs
|
||||
under a new root, list it beside the old one rather than in its place: a purchase signed under the
|
||||
old root still verifies, and one answered `internal` meanwhile is credited when the app presents
|
||||
it again.
|
||||
- `[google]` asks the Google Play Developer API about each purchase, and acknowledges it before
|
||||
crediting it, as the service account whose JSON key `service_account_file` holds. The account
|
||||
needs both Play Console permissions Google's
|
||||
|
||||
@@ -42,11 +42,12 @@ idle_seconds = 60
|
||||
|
||||
; verifies App Store purchases offline. Omit the whole section and the service answers every App Store
|
||||
; receipt provider_not_configured, which the app retries later.
|
||||
; root_certificate is Apple Root CA - G3 as Apple publishes it, in DER:
|
||||
; https://www.apple.com/certificateauthority/AppleRootCA-G3.cer
|
||||
; root_certificates lists, comma-separated, the roots trusted to sign receipts, each in DER. Today that
|
||||
; is Apple Root CA - G3 as Apple publishes it: https://www.apple.com/certificateauthority/AppleRootCA-G3.cer
|
||||
; When Apple adds a root, list it beside the old one rather than in its place.
|
||||
;[apple]
|
||||
;bundle_id = chat.simplex.app
|
||||
;root_certificate = /etc/simplex-badge-service/AppleRootCA-G3.cer
|
||||
;root_certificates = /etc/simplex-badge-service/AppleRootCA-G3.cer
|
||||
|
||||
; verifies Play purchases with the Google Play Developer API. Omit the whole section and the service answers
|
||||
; every Play receipt provider_not_configured, which the app retries later.
|
||||
|
||||
@@ -28,6 +28,8 @@ import Control.Monad (mfilter, when)
|
||||
import Data.Attoparsec.Text (Parser, endOfInput, isEndOfLine, parseOnly, satisfy, skipMany, skipSpace, skipWhile)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Ini (Ini, iniGlobals, iniParser, keys, lookupValue, sections)
|
||||
import Data.List.NonEmpty (NonEmpty)
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import Data.Maybe (fromMaybe, isJust)
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
@@ -95,7 +97,7 @@ data PollConfig = PollConfig {pWaitingSeconds :: Int, pIdleSeconds :: Int}
|
||||
|
||||
data AppleStoreConfig = AppleStoreConfig
|
||||
{ aBundleId :: Text,
|
||||
aRootCertificate :: FilePath
|
||||
aRootCertificates :: NonEmpty FilePath
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
@@ -190,7 +192,7 @@ knownSettings =
|
||||
("btcpay", ["host", "api_key", "store_id", "webhook_secret", "expiry_minutes", "speed_policy", "payment_tolerance"]),
|
||||
("stripe", ["secret_key", "publishable_key", "webhook_secret", "session_minutes"]),
|
||||
("poll", ["waiting_seconds", "idle_seconds"]),
|
||||
("apple", ["bundle_id", "root_certificate"]),
|
||||
("apple", ["bundle_id", "root_certificates"]),
|
||||
("google", ["package_name", "service_account_file"]),
|
||||
("group", ["display_name", "description"]),
|
||||
("dev", ["accept_unverified_store_receipts"]),
|
||||
@@ -336,8 +338,12 @@ parseConfig ini = do
|
||||
| not (hasSection "apple") = Right Nothing
|
||||
| otherwise = do
|
||||
aBundleId <- required "apple" "bundle_id"
|
||||
aRootCertificate <- T.unpack <$> required "apple" "root_certificate"
|
||||
pure (Just AppleStoreConfig {aBundleId, aRootCertificate})
|
||||
aRootCertificates <- rootCertificates
|
||||
pure (Just AppleStoreConfig {aBundleId, aRootCertificates})
|
||||
rootCertificates = do
|
||||
v <- required "apple" "root_certificates"
|
||||
maybe (Left "apple.root_certificates must list at least one file") Right $
|
||||
L.nonEmpty $ map T.unpack $ filter (not . T.null) $ map T.strip $ T.splitOn "," v
|
||||
googleSection
|
||||
| not (hasSection "google") = Right Nothing
|
||||
| otherwise = do
|
||||
|
||||
@@ -141,8 +141,8 @@ configureStoreVerifier serviceCfg env@ServiceState {storeVerifier = v} = case se
|
||||
logWarn "[dev] accept_unverified_store_receipts is on: any store receipt is accepted without verification"
|
||||
pure env {storeVerifier = mockStoreVerifier}
|
||||
Just ServiceConfig {appleStore, playStore} -> do
|
||||
apple <- forM appleStore $ \AppleStoreConfig {aBundleId, aRootCertificate} ->
|
||||
(\root -> verifyAppleTransaction root aBundleId) <$> orExit (readAppleRoot aRootCertificate)
|
||||
apple <- forM appleStore $ \AppleStoreConfig {aBundleId, aRootCertificates} ->
|
||||
(\roots -> verifyAppleTransaction roots aBundleId) <$> mapM (orExit . readAppleRoot) aRootCertificates
|
||||
google <- forM playStore $ orExit . playStoreVerifier
|
||||
forM_ appleStore $ \AppleStoreConfig {aBundleId} -> logInfo $ "verifying App Store receipts for " <> aBundleId
|
||||
forM_ playStore $ \PlayStoreConfig {gPackageName} -> logInfo $ "verifying Play purchases for " <> gPackageName
|
||||
|
||||
@@ -25,6 +25,7 @@ import qualified Data.ByteString as B
|
||||
import qualified Data.ByteString.Base64 as B64
|
||||
import qualified Data.ByteString.Base64.URL as B64U
|
||||
import Data.Foldable (toList)
|
||||
import Data.List.NonEmpty (NonEmpty)
|
||||
import Data.Maybe (isJust)
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
@@ -36,10 +37,10 @@ import Data.X509.Validation (SignatureFailure (..), SignatureVerification (..),
|
||||
import Simplex.Chat.PaymentService.Types (CurrencyAmount (..))
|
||||
import Simplex.Messaging.Util (eitherToMaybe)
|
||||
|
||||
verifyAppleTransaction :: SignedCertificate -> Text -> Text -> Either StoreRefusal VerifiedStoreTransaction
|
||||
verifyAppleTransaction root ourBundleId jws = case T.splitOn "." jws of
|
||||
verifyAppleTransaction :: NonEmpty SignedCertificate -> Text -> Text -> Either StoreRefusal VerifiedStoreTransaction
|
||||
verifyAppleTransaction roots ourBundleId jws = case T.splitOn "." jws of
|
||||
[header, payload, signature] -> do
|
||||
leaf <- signingChain root =<< certificateChain header
|
||||
leaf <- signingChain roots =<< certificateChain header
|
||||
sig <- jwsSignature signature
|
||||
key <- leafKey leaf
|
||||
unless (ECDSA.verify SHA256 key sig (encodeUtf8 $ header <> "." <> payload)) $
|
||||
@@ -65,14 +66,14 @@ certificateChain header = do
|
||||
J.String c | Right bs <- B64.decode (encodeUtf8 c) -> Right bs
|
||||
_ -> refused "an x5c certificate is not base64"
|
||||
|
||||
-- | The root in the chain is ignored: trust comes from the configured one alone.
|
||||
signingChain :: SignedCertificate -> [ByteString] -> Either StoreRefusal SignedCertificate
|
||||
signingChain root = \case
|
||||
-- | The root in the chain is ignored: trust comes from the configured ones alone.
|
||||
signingChain :: NonEmpty SignedCertificate -> [ByteString] -> Either StoreRefusal SignedCertificate
|
||||
signingChain roots = \case
|
||||
[leafDer, intermediateDer, _] -> do
|
||||
leaf <- certificate leafDer
|
||||
intermediate <- certificate intermediateDer
|
||||
-- the configured root may be stale or wrong as likely as the receipt forged, so this retries rather than refuses
|
||||
unless (signedBy root intermediate) $ failed "the intermediate is not signed by the configured root"
|
||||
-- a root may be missing or wrong as likely as the receipt forged, so this retries rather than refuses
|
||||
unless (any (`signedBy` intermediate) roots) $ failed "the intermediate is not signed by a configured root"
|
||||
case verifySignedSignature leaf (certPubKey $ getCertificate intermediate) of
|
||||
SignaturePass -> Right ()
|
||||
SignatureFailed SignatureInvalid -> refused "the leaf is not signed by the intermediate"
|
||||
@@ -84,7 +85,6 @@ signingChain root = \case
|
||||
_ -> failed "x5c is not leaf, intermediate and root"
|
||||
where
|
||||
certificate = either (const $ failed "an x5c certificate does not decode") Right . decodeSignedCertificate
|
||||
signedBy issuer cert = verifySignedSignature cert (certPubKey $ getCertificate issuer) == SignaturePass
|
||||
marked oid cert = case certExtensions (getCertificate cert) of
|
||||
Extensions (Just exts) -> any ((== oid) . extRawOID) exts
|
||||
Extensions Nothing -> False
|
||||
@@ -175,8 +175,15 @@ refused = Left . SRInvalid
|
||||
failed :: Text -> Either StoreRefusal a
|
||||
failed = Left . SRVerifierFailed . ("apple verifier: " <>)
|
||||
|
||||
signedBy :: SignedCertificate -> SignedCertificate -> Bool
|
||||
signedBy issuer cert = verifySignedSignature cert (certPubKey $ getCertificate issuer) == SignaturePass
|
||||
|
||||
readAppleRoot :: FilePath -> IO (Either String SignedCertificate)
|
||||
readAppleRoot path =
|
||||
try (B.readFile path) >>= \case
|
||||
Left (_ :: IOException) -> pure $ Left $ path <> ": could not be read"
|
||||
Right der -> pure $ either (const $ Left $ path <> ": is not a DER certificate") Right $ decodeSignedCertificate der
|
||||
Right der -> pure $ either (const $ Left $ path <> ": is not a DER certificate") checkRoot $ decodeSignedCertificate der
|
||||
where
|
||||
checkRoot root
|
||||
| signedBy root root = Right root
|
||||
| otherwise = Left $ path <> ": is not a self-signed root certificate"
|
||||
|
||||
Binary file not shown.
@@ -44,7 +44,7 @@ The client never states what is signed. The tier is that of whatever funded the
|
||||
- **App Store.** Apple is verified offline, so it has no "not yet".
|
||||
- `receipt_invalid`: a JWS that is not three base64url parts or names no `transactionId`; an `x5c` certificate that is not base64; a JWS signature that is not 64 bytes; a leaf whose signature does not verify against the intermediate; a JWS signature that does not verify against the leaf; a transaction with a `revocationDate`, refunded or revoked.
|
||||
- `test_purchase`: refused by the service after the verifier vouched for it, a `Sandbox` transaction.
|
||||
- `internal`: whatever this service's own configuration decides — an intermediate whose signature does not verify against the configured root, and another app's `bundleId`, since the configured root or bundle id may be the wrong one as likely as the receipt forged; a transaction from Xcode's StoreKit testing, which Xcode signs and not the App Store, so the configured root does not verify it; evidence in a shape this service does not know — a header that does not name `ES256` or carries no `x5c` of exactly three certificates, a certificate that does not decode, a leaf signature the intermediate's key cannot check, an algorithm or key type the library does not support, a chain without Apple's receipt-signing marker extensions, a leaf key that is not P-256, a payload with a missing or mistyped field or an unknown `environment`; the verifier's own failure or overrun; and, refused by the service, a verified transaction other than the one claimed, or a quantity other than 1.
|
||||
- `internal`: whatever this service's own configuration decides — an intermediate whose signature verifies against none of the configured roots, and another app's `bundleId`, since a root may be missing or the bundle id wrong as likely as the receipt forged; a transaction from Xcode's StoreKit testing, which Xcode signs and not the App Store, so no configured root verifies it; evidence in a shape this service does not know — a header that does not name `ES256` or carries no `x5c` of exactly three certificates, a certificate that does not decode, a leaf signature the intermediate's key cannot check, an algorithm or key type the library does not support, a chain without Apple's receipt-signing marker extensions, a leaf key that is not P-256, a payload with a missing or mistyped field or an unknown `environment`; the verifier's own failure or overrun; and, refused by the service, a verified transaction other than the one claimed, or a quantity other than 1.
|
||||
- `product_unavailable`: a product this service does not price — the verifier vouched for the transaction either way.
|
||||
- `provider_not_configured`: the deployment has no `[apple]` section.
|
||||
- **Google Play.** Only a purchase record is a verdict.
|
||||
|
||||
@@ -37,11 +37,12 @@ waiting_seconds = 3
|
||||
idle_seconds = 60
|
||||
|
||||
; optional; without it every App Store receipt is answered provider_not_configured, which the app retries later.
|
||||
; root_certificate is https://www.apple.com/certificateauthority/AppleRootCA-G3.cer, in DER;
|
||||
; uncomment its mount in compose.yml.
|
||||
; root_certificates lists, comma-separated, the roots trusted to sign receipts, each in DER and mounted
|
||||
; in compose.yml. Today that is https://www.apple.com/certificateauthority/AppleRootCA-G3.cer.
|
||||
; When Apple adds a root, list it beside the old one rather than in its place.
|
||||
;[apple]
|
||||
;bundle_id = chat.simplex.app
|
||||
;root_certificate = /etc/simplex-badge-service/AppleRootCA-G3.cer
|
||||
;root_certificates = /etc/simplex-badge-service/AppleRootCA-G3.cer
|
||||
|
||||
; optional; without it every Play receipt is answered provider_not_configured, which the app retries later.
|
||||
; service_account_file is the JSON key of a service account with the Play Console permissions "View
|
||||
|
||||
@@ -8,6 +8,7 @@ import BadgeService.Config
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Either (isLeft)
|
||||
import Data.Ini (readIniFile)
|
||||
import Data.List.NonEmpty (NonEmpty (..))
|
||||
import qualified Data.Text as T
|
||||
import qualified Data.Text.IO as T
|
||||
import Simplex.Messaging.Encoding.String (strDecode)
|
||||
@@ -386,7 +387,7 @@ withDev :: T.Text -> (Either String ServiceConfig -> IO a) -> IO a
|
||||
withDev keys act = withIni (fullIni <> "[dev]\n" <> keys) $ \p -> readServiceConfig p >>= act
|
||||
|
||||
appleIni, googleIni :: T.Text
|
||||
appleIni = "[apple]\nbundle_id = chat.simplex.app\nroot_certificate = /etc/badges/AppleRootCA-G3.cer\n"
|
||||
appleIni = "[apple]\nbundle_id = chat.simplex.app\nroot_certificates = /etc/badges/AppleRootCA-G3.cer ,/etc/badges/AppleRootCA-G4.cer,\n"
|
||||
googleIni = "[google]\npackage_name = chat.simplex.app\nservice_account_file = /etc/badges/play.json\n"
|
||||
|
||||
testStoresAbsent :: IO ()
|
||||
@@ -398,7 +399,7 @@ testStoresAbsent = withIni fullIni $ \p -> do
|
||||
testStoresRead :: IO ()
|
||||
testStoresRead = withIni (fullIni <> appleIni <> googleIni) $ \p -> do
|
||||
Right cfg <- readServiceConfig p
|
||||
appleStore cfg `shouldBe` Just AppleStoreConfig {aBundleId = "chat.simplex.app", aRootCertificate = "/etc/badges/AppleRootCA-G3.cer"}
|
||||
appleStore cfg `shouldBe` Just AppleStoreConfig {aBundleId = "chat.simplex.app", aRootCertificates = "/etc/badges/AppleRootCA-G3.cer" :| ["/etc/badges/AppleRootCA-G4.cer"]}
|
||||
playStore cfg
|
||||
`shouldBe` Just
|
||||
PlayStoreConfig
|
||||
@@ -411,7 +412,7 @@ testStoresRead = withIni (fullIni <> appleIni <> googleIni) $ \p -> do
|
||||
testStoresIncomplete :: IO ()
|
||||
testStoresIncomplete = do
|
||||
refused (T.replace "bundle_id = chat.simplex.app\n" "" appleIni) "apple.bundle_id"
|
||||
refused (T.replace "root_certificate = /etc/badges/AppleRootCA-G3.cer\n" "" appleIni) "apple.root_certificate"
|
||||
refused (T.replace "root_certificates = /etc/badges/AppleRootCA-G3.cer ,/etc/badges/AppleRootCA-G4.cer,\n" "" appleIni) "apple.root_certificates"
|
||||
refused (T.replace "package_name = chat.simplex.app\n" "" googleIni) "google.package_name"
|
||||
refused (T.replace "service_account_file = /etc/badges/play.json\n" "" googleIni) "google.service_account_file"
|
||||
where
|
||||
|
||||
@@ -25,8 +25,10 @@ import qualified Data.ByteString as B
|
||||
import qualified Data.ByteString.Base64 as B64
|
||||
import qualified Data.ByteString.Base64.URL as B64U
|
||||
import qualified Data.ByteString.Lazy as LB
|
||||
import Data.Either (isRight)
|
||||
import Data.Hourglass (Date (..), DateTime (..), Month (..), TimeOfDay (..))
|
||||
import Data.List (isInfixOf)
|
||||
import Data.List.NonEmpty (NonEmpty (..))
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Data.Text.Encoding (decodeUtf8)
|
||||
@@ -42,6 +44,7 @@ badgeStoreVerifierTests :: Spec
|
||||
badgeStoreVerifierTests = describe "badge store verifiers" $ do
|
||||
describe "App Store signed transactions" $ do
|
||||
it "vouches for a transaction a chain to the trusted root signed, naming the transaction it was claimed by" testAppleVerdict
|
||||
it "verifies a transaction under any of the trusted roots" testAppleAnyTrustedRoot
|
||||
it "fails, rather than refuses, an intermediate the trusted root did not sign, since the root may be misconfigured" testAppleRootNotTrusted
|
||||
it "refuses a leaf the intermediate did not sign, though the intermediate is genuine" testAppleForgedLeaf
|
||||
it "fails, rather than refuses, a leaf signature the library cannot check" testAppleLeafUncheckable
|
||||
@@ -55,7 +58,7 @@ badgeStoreVerifierTests = describe "badge store verifiers" $ do
|
||||
it "fails, rather than refuses, a payload Apple signed in a shape it does not know" testAppleUnknownShape
|
||||
it "fails, rather than refuses, a leaf holding a key it cannot read" testAppleLeafKey
|
||||
it "records the price in the currency's minor units, or not at all" testApplePrice
|
||||
it "reads the trusted root from a DER file, and reports one it cannot read" testReadAppleRoot
|
||||
it "reads the trusted root from a DER file, and reports one it cannot read or that is not a root" testReadAppleRoot
|
||||
it "has nothing to acknowledge" testAppleNoAcknowledgement
|
||||
describe "Google Play purchases" $ do
|
||||
it "vouches for a purchased token, asking Play about that token in this app" testPlayPurchased
|
||||
@@ -108,7 +111,10 @@ ourBundleId :: Text
|
||||
ourBundleId = "chat.simplex.app"
|
||||
|
||||
appleVerdict :: SignedCertificate -> Text -> IO (Either StoreRefusal VerifiedStoreTransaction)
|
||||
appleVerdict root jws = seamVerdict noStoreVerifier {verifyApple = Just (verifyAppleTransaction root ourBundleId)} SPApple {jws}
|
||||
appleVerdict root = appleVerdictUnder (root :| [])
|
||||
|
||||
appleVerdictUnder :: NonEmpty SignedCertificate -> Text -> IO (Either StoreRefusal VerifiedStoreTransaction)
|
||||
appleVerdictUnder roots jws = seamVerdict noStoreVerifier {verifyApple = Just (verifyAppleTransaction roots ourBundleId)} SPApple {jws}
|
||||
|
||||
data TestChain = TestChain
|
||||
{ rootCert :: SignedCertificate,
|
||||
@@ -223,6 +229,14 @@ testAppleVerdict = do
|
||||
`shouldReturn` Right VerifiedStoreTransaction {transactionRef = "2000000900000001", productId = "BADGE_SUPPORTER_01", quantity = 1, testPurchase = False, paid = Just (CurrencyAmount 700, "USD"), alreadyAcknowledged = False}
|
||||
appleTransactionId jws `shouldBe` Just "2000000900000001"
|
||||
|
||||
testAppleAnyTrustedRoot :: IO ()
|
||||
testAppleAnyTrustedRoot = do
|
||||
old <- trustedChain
|
||||
new <- newChain otherRootName [intermediateMarker] [receiptLeafMarker]
|
||||
forM_ [old, new] $ \chain -> do
|
||||
jws <- signedBy chain $ transactionPayload [] []
|
||||
fmap transactionRef <$> appleVerdictUnder (rootCert old :| [rootCert new]) jws `shouldReturn` Right "2000000900000001"
|
||||
|
||||
testAppleRootNotTrusted :: IO ()
|
||||
testAppleRootNotTrusted = do
|
||||
trusted <- trustedChain
|
||||
@@ -345,7 +359,8 @@ testApplePrice = do
|
||||
|
||||
testReadAppleRoot :: IO ()
|
||||
testReadAppleRoot = do
|
||||
TestChain {rootCert} <- trustedChain
|
||||
TestChain {rootCert, intermediateCert} <- trustedChain
|
||||
readAppleRoot ("apps" </> "simplex-badge-service" </> "test-fixtures" </> "apple" </> "AppleRootCA-G3.cer") >>= (`shouldSatisfy` isRight)
|
||||
createDirectoryIfMissing True "tests/tmp"
|
||||
withTempDirectory "tests/tmp" "apple-root" $ \d -> do
|
||||
B.writeFile (d </> "root.cer") (encodeSignedObject rootCert)
|
||||
@@ -353,6 +368,8 @@ testReadAppleRoot = do
|
||||
B.writeFile (d </> "root.pem") "-----BEGIN CERTIFICATE-----\n"
|
||||
readAppleRoot (d </> "root.pem") >>= (`shouldSatisfy` either ("not a DER certificate" `isInfixOf`) (const False))
|
||||
readAppleRoot (d </> "absent.cer") >>= (`shouldSatisfy` either ("could not be read" `isInfixOf`) (const False))
|
||||
B.writeFile (d </> "intermediate.cer") (encodeSignedObject intermediateCert)
|
||||
readAppleRoot (d </> "intermediate.cer") >>= (`shouldSatisfy` either ("not a self-signed root certificate" `isInfixOf`) (const False))
|
||||
|
||||
testAppleNoAcknowledgement :: IO ()
|
||||
testAppleNoAcknowledgement = do
|
||||
@@ -360,7 +377,7 @@ testAppleNoAcknowledgement = do
|
||||
jws <- signedBy chain $ transactionPayload [] []
|
||||
withFakePlay $ \fake -> do
|
||||
verifier <- playVerifier fake
|
||||
acknowledgement verifier {verifyApple = Just (verifyAppleTransaction (rootCert chain) ourBundleId)} SPApple {jws} `shouldReturn` Nothing
|
||||
acknowledgement verifier {verifyApple = Just (verifyAppleTransaction (rootCert chain :| []) ourBundleId)} SPApple {jws} `shouldReturn` Nothing
|
||||
|
||||
-- * Google Play
|
||||
|
||||
|
||||
Reference in New Issue
Block a user