iOS 27 extends App Attest with new authenticator data extensions โ launch validation category and bundle version โ plus macOS 27 support and an ACL Blob OID in the leaf certificate. These additions let your server detect unauthorized app modifications, unexpected distribution channels, and compromised macOS security settings.
โข iOS 27 adds two new authenticator data extensions: launchValidationCategory and bundleVersion, surfaced after the standard authenticator data.
โข macOS 27 introduces App Attest support for the first time, with an additional ACL Blob OID in the leaf certificate encoding Secure Enclave key access controls.
โข The ACL Blob OID encodes Full Security Mode and SIP state, giving servers cryptographic evidence of macOS security posture.
โข Previously, only iOS/iPadOS/watchOS/tvOS were supported; macOS was explicitly unsupported before this release.
โข New launch validation category extension reveals if your app is running under TestFlight, App Store, or another unexpected environment, enabling server-side rejection of modified clients.
โข New bundle version extension lets your server confirm the exact distributed version is running, catching re-signed or version-spoofed copies.
โข First-ever macOS support (macOS 27+) with Secure Enclave-backed keys enforcing Full Security Mode and SIP validation via the ACL Blob OID in the attestation certificate.
Shows how to generate a Secure Enclave-backed App Attest key, request an attestation, and parse the new iOS 27 authenticator data extensions (launchValidationCategory and bundleVersion) from the returned attestation object.
import DeviceCheck
import CryptoKit
import Foundation
// MARK: - iOS 27 App Attest Integration
struct AppAttestManager {
private let service = DCAppAttestService.shared
private let keychainKey = "com.example.app.attestKeyID"
// Step 1: Check support before any App Attest call
var isSupported: Bool {
service.isSupported
}
// Step 2: Generate or retrieve a Secure Enclave-backed key
func generateKeyIfNeeded() async throws -> String {
if let stored = KeychainHelper.load(key: keychainKey) {
return stored
}
let keyID = try await service.generateKey()
KeychainHelper.save(key: keychainKey, value: keyID)
return keyID
}
// Step 3: Request attestation and extract iOS 27 extensions
func attest(keyID: String, serverChallenge: Data) async throws -> AttestationResult {
// Hash the challenge per Apple's requirement
let clientDataHash = Data(SHA256.hash(data: serverChallenge))
let attestation = try await service.attestKey(keyID, clientDataHash: clientDataHash)
// Parse the authenticator data extensions introduced in iOS 27
let extensions = try parseAuthenticatorDataExtensions(from: attestation)
return AttestationResult(
attestationObject: attestation,
launchValidationCategory: extensions.launchValidationCategory,
bundleVersion: extensions.bundleVersion
)
}
// Step 4: Generate an assertion to secure an outbound payload
func assert(keyID: String, payload: Data, serverChallenge: Data) async throws -> Data {
let clientDataHash = Data(SHA256.hash(data: payload + serverChallenge))
return try await service.generateAssertion(keyID, clientDataHash: clientDataHash)
}
// iOS 27: parse the new CBOR extensions appended to authenticator data
private func parseAuthenticatorDataExtensions(from attestationObject: Data) throws -> AuthenticatorExtensions {
// In production, use a CBOR library to fully decode the attestation CBOR map.
// The authenticator data is at key "authData" in the top-level CBOR map.
// iOS 27 appends a CBOR extensions map after the fixed 37-byte prefix:
// [32 bytes rpIdHash][1 byte flags][4 bytes signCount][variable credData][CBOR extensions]
// Extension keys: "apple.launch-validation-category", "apple.bundle-version"
//
// Below demonstrates the structure your server-side or app-side CBOR parser targets:
let extensionKeys = [
"apple.launch-validation-category", // e.g. "app-store", "testflight", "developer"
"apple.bundle-version" // e.g. "1.0.3"
]
_ = extensionKeys // used by your CBOR parser
// Placeholder parse result โ replace with real CBOR decoding in production
return AuthenticatorExtensions(
launchValidationCategory: "app-store",
bundleVersion: Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "unknown"
)
}
}
// MARK: - Supporting Types
struct AttestationResult {
let attestationObject: Data // Send this to your server for validation
let launchValidationCategory: String
let bundleVersion: String
}
struct AuthenticatorExtensions {
let launchValidationCategory: String
let bundleVersion: String
}
// Minimal Keychain wrapper
enum KeychainHelper {
static func save(key: String, value: String) {
let data = Data(value.utf8)
let query: [CFString: Any] = [
kSecClass: kSecClassGenericPassword,
kSecAttrAccount: key,
kSecValueData: data
]
SecItemDelete(query as CFDictionary)
SecItemAdd(query as CFDictionary, nil)
}
static func load(key: String) -> String? {
let query: [CFString: Any] = [
kSecClass: kSecClassGenericPassword,
kSecAttrAccount: key,
kSecReturnData: true,
kSecMatchLimit: kSecMatchLimitOne
]
var result: AnyObject?
guard SecItemCopyMatching(query as CFDictionary, &result) == errSecSuccess,
let data = result as? Data else { return nil }
return String(decoding: data, as: UTF8.self)
}
}Extensions in the authenticator data are appended as a new CBOR structure per the WebAuthentication standard โ older server parsers that stop reading after the fixed 37-byte authenticator data prefix will silently drop them. macOS 27 is the first macOS version with App Attest support; isSupported returns false on older macOS. Keys are invalidated on app reinstall or device restore โ do not assume a stored key ID is perpetually valid.
Requires genuine Apple hardware with Secure Enclave. On macOS, Full Security Mode and System Integrity Protection must be enabled for attestation to succeed. Not available in all app extension types โ always gate on DCAppAttestService.isSupported.
More iOS 27 APIs land every week.
Get notified when new capabilities are published โ no noise, just signal.