X and Y coordinates in App Attestation public key has mismatching length

There seem to be an issue with the DeviceCheck Framework where in rare cases the public key (ECC P-256) embedded inside the attestation object returned from DCAppAttestService.attestKey(_:clientDataHash:completionHandler:) has X and Y coordinates with mismatching length. Sometimes X or Y has 31 bytes instead of the expected 32 bytes.

This can easily be reproduced by generating and attesting multiple keys using DCAppAttestService.generateKey(completionHandler:) and DCAppAttestService.attestKey(_:clientDataHash:completionHandler:). Every now and then the public key embedded inside the attestation object has X and Y coordinates with mismatching length (number of bytes).

Added a Swift snippet at the bottom that shows example on how to generate and detect this.

I would expect the ECC P-256 public key X and Y coordinates to always be 32 bytes long. As mentioned in the Web Authentication spec for example.

I've attached an example attestation object (in base64 encoded CBOR) that has an embedded public key with mismatching X and Y coordinate length (Y is 31 bits, and not the expected 32 bits). The file was generated using the Swift snippet below. The snippet was built using Xcode 14.3 (14E222b) and ran on iPhone XR with iOS 15.7.1 (19H117).

A feedback ticket has also been submitted regarding this issue: FB12235865

Swift snippet to generate and check attestation objects:

import DeviceCheck
import CryptoKit
import SwiftCBOR // https://github.com/valpackett/SwiftCBOR
​
func generateAttestationObjects() {
    for i in 0..<1000 {
        DispatchQueue.main.asyncAfter(deadline: .now() + TimeInterval(i)) {
            DCAppAttestService.shared.generateKey { keyId, error in
                guard let keyId else {
                    print("\(i): Failed to generate key: \(error)")
                    return
                }
​
                print("\(i): Generated keyId: \(keyId)")
​
                DCAppAttestService.shared.attestKey(
                    keyId,
                    clientDataHash: Data(hex: "01020304")!
                ) { attestationObject, error in
                    guard let attestationObject else {
                        print("\(i): Failed to get attestation: \(error)")
                        return
                    }
​
                    do {
                        let attestationObjectBytes = [UInt8](attestationObject)
​
                        if case let .map(decodedAttestationObject) = try CBOR.decode(attestationObjectBytes) {
                            print("\(i): Successfully decoded Attestation object (CBOR)")
​
                            if case let .byteString(authData) = decodedAttestationObject["authData"] {
​
                                let attestedCredentialData = [UInt8](authData.dropFirst(37))
​
                                let credentialIdLengthBuffer = [UInt8](attestedCredentialData[16..<18])
                                let credentialIdLength = Int(credentialIdLengthBuffer.reversed().withUnsafeBytes { $0.load(as: UInt16.self) })
                                let credentialId = [UInt8](attestedCredentialData[18..<(18 + credentialIdLength)])
                                let credentialPublicKeyBuffer = [UInt8](attestedCredentialData.dropFirst(18 + credentialIdLength))
​
                                if let decodedCredentialPublicKey = try CBOR.decode(credentialPublicKeyBuffer) {
​
                                    if
                                        case let .byteString(xCoordinateBuffer) = decodedCredentialPublicKey[-2],
                                        case let .byteString(yCoordinateBuffer) = decodedCredentialPublicKey[-3]
                                    {
                                        let xCoordinateLength = xCoordinateBuffer.count
                                        let yCoordinateLength = yCoordinateBuffer.count
​
                                        if xCoordinateLength != yCoordinateLength {
                                            print("\(i): X/Y Coordinate length mismatch! X: \(xCoordinateLength), Y: \(yCoordinateLength)")
                                        } else if xCoordinateLength != 32 || yCoordinateLength != 32 {
                                            print("\(i): X/Y Coordinate length mismatch! X: \(xCoordinateLength), Y: \(yCoordinateLength)")
                                        } else {
                                            print("\(i): X/Y Coordinates OK")
                                        }
                                    }
                                }
                            }
                        }
                    } catch {
                        print("\(i): Error decoding Attestation object (CBOR): \(error)")
                    }
                }
            }
        }
    }
}

An attestation object with a embedded public key with mismatching X and Y coordinate length (base64 encoded CBOR):

Replies

We see the same issue on a small proportion of AppAttest attestations