apple / app-store-server-library-python

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Support Xcode-signed JWS in SignedDataVerifier

WFT opened this issue · comments

Feature request: I think SignedDataVerifier should support an Environment.XCODE environment which verifies JWS values signed by Xcode StoreKit testing.

The problem

Transaction JWS produced by StoreKit testing have a few differences from normal signed JWS:

  • Environment is Xcode
  • x5c claim is 1 certificate long (because the Xcode certificate is a root cert)

So trying to use SignedDataVerifier to verify such a claim will immediately throw an INVALID_CHAIN_LENGTH error. If this were to be bypassed, I imagine we'd run into an error about the JWS having the wrong environment.

Use case

When testing our iOS app during development, we use a local development server. To really test this well I want to use almost exactly the same code paths for StoreKit testing as I do for production.

Right now there’s no way to get a SignedDataVerifier to actually verify & decode a transaction or renewal info JWS.

I want code that looks like this:

def _get_verifier() -> SignedDataVerifier:
    if is_in_local_testing_environment():
        return SignedDataVerifier(
            root_certificates=[load_xcode_testing_certificate()],
            enable_online_checks=False,
            environment=Environment.XCODE,
            bundle_id='my.bundle.id'
        )
    else:
        # Production & Sandbox/staging cases
        return SignedDataVerifier(...)

@app.post('/api/app-store/transaction')
def process_transaction():
    new_transaction = request.json['transaction']
    verifier = _get_verifier()
    verified_transaction = verifier.verify_and_decode_signed_transaction(new_transaction)
    response = deliver_content(verified_transaction)
    return response

Workaround

Right now my test server can't actually just use the same code for both cases, so we'll have to do something like the following:

import jwt
import cattrs

def _verify_transaction(signed_transaction: str) -> JWSTransactionDecodedPayload:
    if is_in_local_testing_environment():
        # Just for simplicity; really I should load the signing key & verify the whole chain.
        # It doesn't really matter in my case, but it may matter in general.
        data = jwt.decode(signed_transaction, options={'verify_signature':False})
        return cattrs.structure(data, JWSTransactionDecodedPayload)
    else:
        # Production & Sandbox/staging cases
        verifier = SignedDataVerifier(...)
        return verifier.verify_and_decode_signed_transaction(signed_transaction)

@app.post('/api/app-store/transaction')
def process_transaction():
    new_transaction = request.json['transaction']
    verifier = _get_verifier()
    verified_transaction = verifier.verify_and_decode_signed_transaction(new_transaction)
    response = deliver_content(verified_transaction)
    return response

This looks not too bad, but there are a couple problems:

  1. I have to repeat this for each kind of JWS (except notifications since I can't get those on my local server anyway)
  2. Now I'm not testing my use of the library, I'm testing my use of the cattrs and jwt libraries.

Proposed solutions

  1. Support an Environment.XCODE, which disables checks for chain length & whatever else would normally fail with Xcode testing
  2. Alternatively, add a subclass of SignedDataVerifier called StoreKitTestingSignedDataVerifier which overrides the relevant functions (at least _decode_signed_object).

@WFT Xcode support has now been added and unit tests now exist with Xcode data of various types

Thank you!