agisboye / app-store-server-api

A Node.js client for the App Store Server API
MIT License
210 stars 32 forks source link

Skip check for Apple root CA in sandbox environment #25

Closed willbattel closed 1 year ago

willbattel commented 1 year ago

Just wanted to float this for discussion...

When testing StoreKit with Xcode using a StoreKit Configuration file (henceforth referring to as SKC), it appears that the transactions do not include Apple's signature like their production transactions do. However, if you don't use an SKC then the sandbox transactions do get Apple's signature. It is only when using an SKC do the transactions seem to skip the root CA. I'm guessing that this means that using an SKC results in transactions being generated completely locally, whereas otherwise- even in the sandbox environment- they are generated remotely by Apple and signed with their root CA.

What this means for us is that, because our app relies on our backend for tracking user entitlements, we cannot decode and process test transactions generated by our SKC because they are missing Apple's certificate in validateCertificates. The only way to test transactions with our backend is to not use an SKC, which isn't the end of the world but floods Xcode with errors such as Finance Authentication Error. It seems that Xcode wants developers to use an SKC when possible, but as far as I can tell it's not required.

I'm wondering what the implications would be if validateCertificates was changed such that Apple's root CA was only required in the production environment. This would make testing easier, but I'm not certain if there would be negative side effects. The only thing I can think of at the moment would be the potential to forge fake sandbox transactions.

Also curious to know how other developers handle SKCs with their workflows in general. Maybe the solution isn't to modify validateCertificates but instead to have some other flow for SKC transactions entirely.

Thanks.

agisboye commented 1 year ago

When using SKC, transactions are generated and signed locally by Xcode. This explains why you won't see Apples CA in the signature chain.

Personally I haven't used the combination of SKC and server-controlled entitlements but I can see why you would do so (and want to end-to-end test things).

I'd prefer to not introduce any breaking changes or to deviate from the data model of the App Store Server API (for example by introducing a third environment in addition to Production and Sandbox). Let's discuss the options:

  1. Add an argument to the AppStoreServerAPI class that disables signature checks altogether. It's a footgun and some users will inevitably disable checks during development and forget to re-enable them before deploying.

  2. Allow passing in a root CA fingerprint (SHA256 of the cert) to be used instead of Apple's. According to this, it's possible to save the root CA used by Xcode for local transactions. If you forget to change the fingerprint before deploying, all sandbox and production transactions will fail (which is better than any transaction being considered valid).

Both options will require some changes to decoding-functions, which could be a problem as they are exported by the package.

code28 commented 1 year ago

We just came across the same problem when using this library. For now we included another generic JWT library to parse the transaction in our development environment and we'd be happy to have a better solution, included here!

I get your point, that some users will forget to re-enable the checks before deploying. Still I think, that a library should not remove functionality to protect users from doing mistakes. 🙃 Even more, those users would probably also implement some kind of workaround which is even worse.

But I like the second idea better anyway: Allow passing in a root CA fingerprint. For each SKC, there is a cert (valid for a year), so this should work! I think we should make sure, that one can pass multiple root CA fingerprints, so that multiple SKCs or one SKC plus the Apple cert (e.g. for sandbox) are supported.

Regarding the breaking changes in the API: That could be an optional parameter, so when using the old decoding function signature, the Apple Root CA is used, so it's not completely breaking. An alternative is to introduce new decoding functions in the sense of decodeTransactionFromCustomCA or something.

If we agree on a strategy, I'd also be willing to help with a PR, if you (both @willbattel and @agisboye) don't want to or don't have time at the moment.

agisboye commented 1 year ago

I've made some changes to the decoding functions to allow passing in a custom root certificate. See custom-root-cert.

It means you can do something like

const LOCAL_ROOT_FINGERPRINT = "AA:BB:CC:DD:..."
const fingerprint = (process.env.NODE_ENV === "production") ? APPLE_ROOT_CA_G3_FINGERPRINT : LOCAL_ROOT_FINGERPRINT
const transactions = await decodeTransactions(response.signedTransactions, fingerprint)

It defaults to Apple's cert which means it's fully backwards compatible (the fingerprint argument can be omitted). I haven't had a chance to test it yet but let me know how it works for you.

code28 commented 1 year ago

Great, thanks a lot! We'll test that as soon as possible and get back to you.

code28 commented 1 year ago

Okay, we finally managed to get time to test it. Seems to work like a charm 👍 From my point of view, it could be merged! :)

code28 commented 1 year ago

Can you say something about when (or if?) you're gonna merge it, @agisboye ? We're looking forward to using the new code.

agisboye commented 1 year ago

It's available in v0.8.0 🙏🏼