File Keychain ACL: is there a supported way to read back the complete stored authorization condition?

I have a follow-up question about file-based Keychain ACLs, this time specifically about inspecting the authorization state after it has been configured.

I’ve been looking through the public SecAccess / SecACL / SecTrustedApplication APIs. I can enumerate ACL entries and their authorizations, and SecACLCopyContents gives me the trusted applications associated with an ACL.

What I haven’t been able to establish is whether the complete stored caller-matching condition can be read back through a supported API.

In particular, SecTrustedApplicationCopyData returns opaque application-identifying data. Is that data intended to be a complete representation of the condition that the Keychain will use to recognize that trusted application?

Or can the stored trusted-application condition contain additional code-signing requirements or other matching constraints that are not represented by SecTrustedApplicationCopyData?

My higher-level requirement is to verify, after configuring a private key, that its effective authorization scope is no broader than an independently reviewed policy. Ideally I would like to compare:

  • the authorized private-key operations;
  • all trusted-application subjects and their complete matching conditions;
  • prompt-related state; and
  • partition-list constraints.

Is there a public and supported API, or an Apple-provided tool with a supported machine-readable contract, that can enumerate the complete effective authorization state of a private key in a file-based Keychain?

I’m deliberately not trying to infer this from the current executable’s path, designated requirement, hash, security dump-keychain output, or representative negative tests unless Apple considers one of those to be the supported way to establish the stored authorization state.

I realise from the existing guidance, including TN3137 and the discussion around file-based Keychains, that this is legacy technology. If complete supported introspection simply isn’t available for this model, knowing that limitation would also answer my question.

Thanks.

Answered by DTS Engineer in 905813022
Is there a public and supported API … that can enumerate the complete … authorization state … in a file-based Keychain?

No.

There are a bunch of APIs that let you dig into the details here, but you eventually run into the limits of what’s documented. For example:

  • If you use the public API to dig deep enough into a SecAccess object, you’ll find two ACL entries whose authorisations (SecACLCopyAuthorizations) are kSecACLAuthorizationIntegrity and kSecACLAuthorizationPartitionID. Those entries encode important state in their description property! There’s no reasonable way to interpret those descriptions without straying way off the supported path, and the second one is critical to your goal.
  • It’s easy to interpret the bytes returned by SecTrustedApplicationCopyData (it’s basically a path) but that’s not the only state held by a trusted application object. If you rummage through the Darwin open source you’ll find that these objects also store a code signing requirement, but access to that is done via SPI not API.

I have a test project that I use to dump ACLs and I’ve included the relevant bits of it below.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"


@diagnose(DeprecatedDeclaration, as: ignored)
func dumpPrivateKeyACL(label: String) throws {
    let key = try secCall { SecItemCopyMatching([
        kSecClass: kSecClassKey,
        kSecAttrKeyClass: kSecAttrKeyClassPrivate,
        kSecAttrLabel: label,
        kSecReturnRef: true,
    ] as NSDictionary, $0) } as! SecKey
    let access = try secCall { SecKeychainItemCopyAccess(key.keychainItem, $0) }
    
    let (uid, gid, ownerType, allAuthorizations) = try SecAccessCopyOwnerAndACL(access)
    if let uid {
        print("uid: \(uid)")
    }
    if let gid {
        print("gid: \(gid)")
    }
    if let ownerType {
        print("ownerType: \(ownerType)")
        if ownerType & UInt32(kSecUseOnlyUID) != 0 {
            print("  kSecUseOnlyUID")
        }
        if ownerType & UInt32(kSecUseOnlyGID) != 0 {
            print("  kSecUseOnlyGID")
        }
        if ownerType & UInt32(kSecHonorRoot) != 0 {
            print("  kSecHonorRoot")
        }
    }
    let allAuthorizationIdentifiers = allAuthorizations.map { authorizationIdentifiersByAuthorization[$0] ?? $0 }.joined(separator: ", ")
    print("allAuthorizations: \(allAuthorizationIdentifiers)")
    let acls = try SecAccessCopyACLList(access)
    
    print("acls:")
    for (i, acl) in zip(0..., acls) {
        print("  [\(i)]:")

        let authorizations = try secCall { SecACLCopyAuthorizations(acl) } as! [String]
        let authorizationIdentifiers = authorizations.map { authorizationIdentifiersByAuthorization[$0] ?? $0 }.joined(separator: ", ")
        print("    authorizations: \(authorizationIdentifiers)")

        let (apps, description, promptSelector) = try SecACLCopyContents(acl)
        let appSummary = switch apps?.count {
            case nil: " all"
            case 0?: " none"
            default: ""
            }
        print("    apps:\(appSummary)")
        for (i, app) in zip(0..., apps ?? []) {
            let data = try secCall { SecTrustedApplicationCopyData(app, $0) }
            print("      [\(i)]: \((data as NSData).debugDescription)")
        }
        print("    description: '\(description)'")
        let (promptSelectorStr, unknown) = promptSelector.names(using: [
            (.requirePassphase, "requirePassphase"),
            (.unsigned, "unsigned"),
            (.unsignedAct, "unsignedAct"),
            (.invalid, "invalid"),
            (.invalidAct, "invalidAct"),
        ])
        let unknownStr = if unknown.isEmpty { "" } else { "+ 0x\(String(unknown.rawValue, radix: 16))" }
        print("    promptSelector: \(promptSelectorStr)\(unknownStr)")
        
    }
}

private let authorizationIdentifiersByAuthorization: [String: String] = [
    kSecACLAuthorizationAny as String: ".any",
    … and so on …
]

/// Wraps `SecAccessCopyOwnerAndACL` to make is nicer to call from Swift.

@available(macOS, deprecated: 10.10)
private func SecAccessCopyOwnerAndACL(_ access: SecAccess) throws -> (
    uid: uid_t?,
    gid: gid_t?,
    ownerType: SecAccessOwnerType?,
    aclAuthorizations: [String]
) {
    // These doesn’t seem to be any way to tell whether the return `uid`, `gid`,
    // and `ownerType` values were actually populated, so call the routine twice
    // with different initial values and then only return the value if it’s
    // populated to the same value in each case.

    var uid = uid_t.min
    var gid = gid_t.min
    var ownerType = SecAccessOwnerType.min
    let aclAuthorizations = try secCall { SecAccessCopyOwnerAndACL(access, &uid, &gid, &ownerType, $0) } as! [String]

    var uid2 = uid_t.max
    var gid2 = gid_t.max
    var ownerType2 = SecAccessOwnerType.max
    _ = try secCall { SecAccessCopyOwnerAndACL(access, &uid2, &gid2, &ownerType2, $0) }

    let uidQ: uid_t? = if uid2 == uid { uid } else { nil }
    let gidQ: gid_t? = if gid2 == gid { gid } else { nil }
    let ownerTypeQ: SecAccessOwnerType? = if ownerType2 == ownerType { ownerType } else { nil }
    return (uidQ, gidQ, ownerTypeQ, aclAuthorizations)
}

/// Wraps `SecAccessCopyACLList` to make is nicer to call from Swift.

@available(macOS, deprecated: 10.10)
private func SecAccessCopyACLList(_ access: SecAccess) throws -> [SecACL] {
    try secCall { SecAccessCopyACLList(access, $0) } as! [SecACL]
}

/// Wraps `SecACLCopyContents` to make is nicer to call from Swift.

@available(macOS, deprecated: 10.10)
private func SecACLCopyContents(_ acl: SecACL) throws -> (
    applicationList: [SecTrustedApplication]?,
    description: String,
    promptSelector: SecKeychainPromptSelector
) {
    var apps: CFArray? = nil
    var description: CFString? = nil
    var promptSelector: SecKeychainPromptSelector = []
    try secCall { SecACLCopyContents(acl, &apps, &description, &promptSelector) }
    return (apps.map { $0 as! [SecTrustedApplication] }, description! as String, promptSelector)
}

extension SecKey {
    
    fileprivate var keychainItem: SecKeychainItem {
        unsafeBitCast(self, to: SecKeychainItem.self)
    }
}
Is there a public and supported API … that can enumerate the complete … authorization state … in a file-based Keychain?

No.

There are a bunch of APIs that let you dig into the details here, but you eventually run into the limits of what’s documented. For example:

  • If you use the public API to dig deep enough into a SecAccess object, you’ll find two ACL entries whose authorisations (SecACLCopyAuthorizations) are kSecACLAuthorizationIntegrity and kSecACLAuthorizationPartitionID. Those entries encode important state in their description property! There’s no reasonable way to interpret those descriptions without straying way off the supported path, and the second one is critical to your goal.
  • It’s easy to interpret the bytes returned by SecTrustedApplicationCopyData (it’s basically a path) but that’s not the only state held by a trusted application object. If you rummage through the Darwin open source you’ll find that these objects also store a code signing requirement, but access to that is done via SPI not API.

I have a test project that I use to dump ACLs and I’ve included the relevant bits of it below.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"


@diagnose(DeprecatedDeclaration, as: ignored)
func dumpPrivateKeyACL(label: String) throws {
    let key = try secCall { SecItemCopyMatching([
        kSecClass: kSecClassKey,
        kSecAttrKeyClass: kSecAttrKeyClassPrivate,
        kSecAttrLabel: label,
        kSecReturnRef: true,
    ] as NSDictionary, $0) } as! SecKey
    let access = try secCall { SecKeychainItemCopyAccess(key.keychainItem, $0) }
    
    let (uid, gid, ownerType, allAuthorizations) = try SecAccessCopyOwnerAndACL(access)
    if let uid {
        print("uid: \(uid)")
    }
    if let gid {
        print("gid: \(gid)")
    }
    if let ownerType {
        print("ownerType: \(ownerType)")
        if ownerType & UInt32(kSecUseOnlyUID) != 0 {
            print("  kSecUseOnlyUID")
        }
        if ownerType & UInt32(kSecUseOnlyGID) != 0 {
            print("  kSecUseOnlyGID")
        }
        if ownerType & UInt32(kSecHonorRoot) != 0 {
            print("  kSecHonorRoot")
        }
    }
    let allAuthorizationIdentifiers = allAuthorizations.map { authorizationIdentifiersByAuthorization[$0] ?? $0 }.joined(separator: ", ")
    print("allAuthorizations: \(allAuthorizationIdentifiers)")
    let acls = try SecAccessCopyACLList(access)
    
    print("acls:")
    for (i, acl) in zip(0..., acls) {
        print("  [\(i)]:")

        let authorizations = try secCall { SecACLCopyAuthorizations(acl) } as! [String]
        let authorizationIdentifiers = authorizations.map { authorizationIdentifiersByAuthorization[$0] ?? $0 }.joined(separator: ", ")
        print("    authorizations: \(authorizationIdentifiers)")

        let (apps, description, promptSelector) = try SecACLCopyContents(acl)
        let appSummary = switch apps?.count {
            case nil: " all"
            case 0?: " none"
            default: ""
            }
        print("    apps:\(appSummary)")
        for (i, app) in zip(0..., apps ?? []) {
            let data = try secCall { SecTrustedApplicationCopyData(app, $0) }
            print("      [\(i)]: \((data as NSData).debugDescription)")
        }
        print("    description: '\(description)'")
        let (promptSelectorStr, unknown) = promptSelector.names(using: [
            (.requirePassphase, "requirePassphase"),
            (.unsigned, "unsigned"),
            (.unsignedAct, "unsignedAct"),
            (.invalid, "invalid"),
            (.invalidAct, "invalidAct"),
        ])
        let unknownStr = if unknown.isEmpty { "" } else { "+ 0x\(String(unknown.rawValue, radix: 16))" }
        print("    promptSelector: \(promptSelectorStr)\(unknownStr)")
        
    }
}

private let authorizationIdentifiersByAuthorization: [String: String] = [
    kSecACLAuthorizationAny as String: ".any",
    … and so on …
]

/// Wraps `SecAccessCopyOwnerAndACL` to make is nicer to call from Swift.

@available(macOS, deprecated: 10.10)
private func SecAccessCopyOwnerAndACL(_ access: SecAccess) throws -> (
    uid: uid_t?,
    gid: gid_t?,
    ownerType: SecAccessOwnerType?,
    aclAuthorizations: [String]
) {
    // These doesn’t seem to be any way to tell whether the return `uid`, `gid`,
    // and `ownerType` values were actually populated, so call the routine twice
    // with different initial values and then only return the value if it’s
    // populated to the same value in each case.

    var uid = uid_t.min
    var gid = gid_t.min
    var ownerType = SecAccessOwnerType.min
    let aclAuthorizations = try secCall { SecAccessCopyOwnerAndACL(access, &uid, &gid, &ownerType, $0) } as! [String]

    var uid2 = uid_t.max
    var gid2 = gid_t.max
    var ownerType2 = SecAccessOwnerType.max
    _ = try secCall { SecAccessCopyOwnerAndACL(access, &uid2, &gid2, &ownerType2, $0) }

    let uidQ: uid_t? = if uid2 == uid { uid } else { nil }
    let gidQ: gid_t? = if gid2 == gid { gid } else { nil }
    let ownerTypeQ: SecAccessOwnerType? = if ownerType2 == ownerType { ownerType } else { nil }
    return (uidQ, gidQ, ownerTypeQ, aclAuthorizations)
}

/// Wraps `SecAccessCopyACLList` to make is nicer to call from Swift.

@available(macOS, deprecated: 10.10)
private func SecAccessCopyACLList(_ access: SecAccess) throws -> [SecACL] {
    try secCall { SecAccessCopyACLList(access, $0) } as! [SecACL]
}

/// Wraps `SecACLCopyContents` to make is nicer to call from Swift.

@available(macOS, deprecated: 10.10)
private func SecACLCopyContents(_ acl: SecACL) throws -> (
    applicationList: [SecTrustedApplication]?,
    description: String,
    promptSelector: SecKeychainPromptSelector
) {
    var apps: CFArray? = nil
    var description: CFString? = nil
    var promptSelector: SecKeychainPromptSelector = []
    try secCall { SecACLCopyContents(acl, &apps, &description, &promptSelector) }
    return (apps.map { $0 as! [SecTrustedApplication] }, description! as String, promptSelector)
}

extension SecKey {
    
    fileprivate var keychainItem: SecKeychainItem {
        unsafeBitCast(self, to: SecKeychainItem.self)
    }
}
File Keychain ACL: is there a supported way to read back the complete stored authorization condition?
 
 
Q