Giter Club home page Giter Club logo

devicecheck-appattest's Introduction

Apple App Attest Validation

Maven central Build status Code coverage License Written in Kotlin JVM 11 required

Server-side library to validate the authenticity of Apple App Attest artifacts, including

  1. attestation statements,
  2. assertions, and
  3. receipts (plus requesting a new one from Apple).

The project targets the JVM in version 11 or later. The library is written purely in Kotlin while leveraging coroutines for asynchronous execution where meaningful. The implementation relies on only two third party dependencies: Bouncy Castle (CMS, ASN.1 parsing) and Jackson (CBOR decoding). The software is available under the conditions of the Apache 2.0 license enabling its usage in most circumstances.

The implementation follows the steps outlined in the articles "Validating Apps That Connect to Your Server" and "Assessing Fraud Risk" at Apple Developer.

Getting Started

The library is published to Maven Central.

Gradle (Kotlin)

dependencies {
    implementation("ch.veehait.devicecheck:devicecheck-appattest:$latestVersion")
}

Gradle (Groovy)

dependencies {
    implementation "ch.veehait.devicecheck:devicecheck-appattest:$latestVersion"
}

Maven

<dependencies>
  <!-- ... -->
  <dependency>
    <groupId>ch.veehait</groupId>
    <artifactId>devicecheck-appattest</artifactId>
    <version>${latestVersion}</version>
  </dependency>
  <!-- ... -->
</dependencies>

Usage

Verify the Attestation

An iOS app creates an attestationObject for a key created through DCAppAttestService.generateKey() by calling DCAppAttestService.attestKey(). Make sure the clientDataHash comprises a payload which includes a challenge you created within your backend prior to the app's call to attestKey. A good challenge is created randomly, only used once (i.e., one challenge per attestation) and large enough to prevent guessing.

let service = DCAppAttestService.shared

service.generateKey { keyId, error in
    guard error == nil else { /* Handle the error. */ }
    // Store keyId for subsequent operations.
}

service.attestKey(keyId, clientDataHash: hash) { attestationObject, error in
    guard error == nil else { /* Handle error and return. */ }
    // Send attestationObject to your server for verification.
}

The server implementation receives the attestationObject, e.g., Base64 encoded, and the keyId. The keyId returned from DCAppAttestService.generateKey() is already Base64 encoded (or more precisely, it is the Base64 encoded SHA-256 digest of the public key of the generated key).

To validate the authenticity of the attestationObject, instantiate an AttestationValidator for the App which calls DCAppAttestService.

// Create an instance of AppleAppAttest specific to a given iOS app, development team and
// Apple Appattest environment
val appleAppAttest = AppleAppAttest(
    app = App("6MURL8TA57", "de.vincent-haupert.apple-appattest-poc"),
    appleAppAttestEnvironment = AppleAppAttestEnvironment.DEVELOPMENT,
)

// Create an AttestationValidator instance
val attestationValidator = appleAppAttest.createAttestationValidator()

// Validate a single attestation object. Throws an AttestationException if a validation
// error occurs.
val result: ValidatedAttestation = attestationValidator.validate(
    attestationObject = Base64.getDecoder().decode("o2NmbXRvYXBwbGUtYXBwYXR0ZXN0Z2F ..."),
    keyIdBase64 = "XGr5wqmUab/9M4b5vxa6KkPOigfeEWDaw7tuK02aJ6c=",
    serverChallenge = "wurzelpfropf".toByteArray(),
)

If the method call returns, the validation has passed and you can now trust the returned result which contains references to the attestation certificate and the verified receipt. You use the public key of the attestation certificate for the verification of assertions and the receipt for obtaining a fraud risk metric.

Also refer to AttestationValidatorTest.

Verify the Assertion

As soon as you validated the attestation statement, your app may leverage the attested public key to create assertions for arbitrary payloads using the App Attest service:

service.generateAssertion(keyId, clientDataHash: clientDataHash) { assertionObject, error in
    guard error == nil else { /* Handle the error. */ }
    // Send the assertion and request to your server.
}

It is worthwhile to note that the returned assertionObject does not contain the keyId by itself. You have to include it in the data which accompanies the assertionObject. Make sure to not rely on the keyId to establish a link to any identity in your systems prior to verifying the assertion's authenticity by calling AssertionValidator.validate():

// Initialize AppleAppAttest as above

val assertionChallengeValidator = object : AssertionChallengeValidator {
    override fun validate(
        assertionObj: Assertion,
        clientData: ByteArray,
        attestationPublicKey: ECPublicKey,
        challenge: ByteArray,
    ): Boolean = TODO("Your application specific challenge validation routine")
}

val assertionValidator = appleAppAttest.createAssertionValidator(
    assertionChallengeValidator
)

val assertion = assertionValidator.validate(/* ... */)

If the call returns, the app successfully proved control of the attested device. Make sure to include a challenge which suits the security demands of your service. A safe approach is to issue server-side per-assertion challenges, similar to those created for the initial attestation statement (see above).

Also refer to AssertionValidatorTest

Assess Fraud Risk with Receipts

See ReceiptValidatorTest and ReceiptExchangeTest.

Contributions

Your contributions are welcome! Just submit a pull request. Also, if you have a question, feel free to open an issue.

License

Apache 2.0 license

devicecheck-appattest's People

Contributors

veehaitch avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

devicecheck-appattest's Issues

Exceptions are internal

Hi,

I want to use the library in a java backend code.
How can I differentiate the possible exceptions in attestation and assertion phase, since the exception classes are internal?
Is it possible to make exception classes visible outside?

Correctness of `AttestationValidatorImpl.validateAsync`

Hi!

I do not fully grasp some of the intricacies of how the validateAsync function of AttestationValidatorImpl works with couroutines:

1 .What happens id any of lauched child coroutines take longer than the slowes the aysnc produces results? I'd assume that validateAsync returns successfully, not having completed all checks. Is that correct?
2. Any of the launched coroutines throwing an exception should cause the function as a whole to throw an exceptions, since launch is not used to create a root coroutine. Is that the reason for encapsulating the whole body in a coroutineScope?

Otherwise the code is easily comprehensible and a breeze to compare with Apple's documentation!

Possible bug or java compatibility issue in AAGUID comparison

I really appreciate the creation and maintenance of this library! I am using it in a Java implementation but ran into an issue in AttestationValidator.kt verifyAuthenticatorData: the "AAGUID does match neither" exception is being thrown. I find that the values being compared are similar to: expected=12300456-4242-1234-1464-6571236c6f70 and actual=12300456-4242-1234-1400-000000000000 (not the real values).

Observe that the first 16-bytes of the two values are equivalent, as expected. But, the comparison is on the whole 36-bytes on this line: authenticatorData.attestedCredentialData.aaguid != appleAppAttestEnvironment.aaguid.

The comment on Extensions.kt ByteArray.toUUID() function agrees with the Apple documentation "Create a UUID from a [ByteArray] of a length no more than 16 bytes, padded with zeros, if necessary." So, the intention is clear. But, my implementation shows that the actual UUID is padded with zeroes but the expected AAGUID is not padded with zeroes, so the comparison fails.

I patched the library to compare only the first 16-bytes and that makes my attestations work! So, I am pleased about that.

But, do you think the root cause is because I am using the library from Java? What is the proper way to fix it?

Minor issue with exception message on creation time check

// 5. Verify that the receipt’s creation time, given in field 12, is no more than five minutes old.

        // 5. Verify that the receipt’s creation time, given in field 12, is no more than five minutes old.
        //    This helps to thwart replay attacks.
        if (notAfter.isAfter(receiptPayload.creationTime.value)) {
            throw ReceiptException.InvalidPayload("Receipt's creation time is after $notAfter")
        }

right message should be: "Receipt's creation time is before $notAfter"

(yeah, message still not very easy-understandable. best i have in mind - "Receipt's creation time is older than $maxAge")

Incompatibility with current Bouncy Castle

Bouncy castle -jdk15on is not maintained anymore., instead -jdk18on should be used.

The issue here is, introducing any other BC variant into the classpath causes major breakage,
Exceptions like Could not initialize class org.bouncycastle.cms.CMSSignedData happen. Apparently some bouncy-castle-internal algorithm identifiers and/or SPI wirings have changed and BC tries to load algorithm combinations, which do not exist, like sphincs with sha2, for example.

An switch/update to bcpkix-jdk18on should fix everything.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.