Prioritize user privacy and data security in your app. Discuss best practices for data handling, user consent, and security measures to protect user information.

All subtopics
Posts under Privacy & Security topic

Post

Replies

Boosts

Views

Activity

requestTrackingAuthorization stuck in .notDetermined
When developing and testing using my phone I got prompted for allowing app tracking. I later uploaded a build to TestFlight, deleted the old testing app and installed the TestFlight build. I am now stuck in an infinite loop of not getting prompted for allowing app tracking for the app. When entering the app settings the toggle for tracking never appears which leaves me not able to enter the app's content. My guess is that the prompt can only be shown once for the app bundle, but there has to be a way for me to get prompted again without changing the app bundle id. Help is appreciated since this app is scheduled to be published in a week.
0
0
168
May ’25
SecItem: Pitfalls and Best Practices
I regularly help developers with keychain problems, both here on DevForums and in various DTS cases. Over the years I’ve learnt a lot about the API, including many pitfalls and best practices. This post is my attempt to collect that experience in one place. If you have questions or comments about any of this, put them in a new thread and apply the Security tag so that I see it. Share and Enjoy — Quinn “The Eskimo!” @ Developer Technical Support @ Apple let myEmail = "eskimo" + "1" + "@" + "apple.com" SecItem: Pitfalls and Best Practices It’s just four functions, how hard can it be? The SecItem API seems very simple. After all, it only has four function calls, how hard can it be? In reality, things are not that easy. Various factors contribute to making this API much trickier than it might seem at first glance. This post explains some of the keychain’s pitfalls and then goes on to explain various best practices. Before reading this, make sure you understand the fundamentals by reading its companion post, SecItem: Fundamentals. Pitfalls Lets start with some common pitfalls. Queries and Uniqueness Constraints The relationship between query dictionaries and uniqueness constraints is a major source of problems with the keychain API. Consider code like this: var copyResult: CFTypeRef? = nil let query = [ kSecClass: kSecClassGenericPassword, kSecAttrService: "AYS", kSecAttrAccount: "mrgumby", kSecAttrGeneric: Data("SecItemHints".utf8), ] as NSMutableDictionary let err = SecItemCopyMatching(query, &copyResult) if err == errSecItemNotFound { query[kSecValueData] = Data("opendoor".utf8) let err2 = SecItemAdd(query, nil) if err2 == errSecDuplicateItem { fatalError("… can you get here? …") } } Can you get to the fatal error? At first glance this might not seem possible because you’ve run your query and it’s returned errSecItemNotFound. However, the fatal error is possible because the query contains an attribute, kSecAttrGeneric, that does not contribute to the uniqueness. If the keychain contains a generic password whose service (kSecAttrService) and account (kSecAttrAccount) attributes match those supplied but whose generic (kSecAttrGeneric) attribute does not, the SecItemCopyMatching calls will return errSecItemNotFound. However, for a generic password item, of the attributes shown here, only the service and account attributes are included in the uniqueness constraint. If you try to add an item where those attributes match an existing item, the add will fail with errSecDuplicateItem even though the value of the generic attribute is different. The take-home point is that that you should study the attributes that contribute to uniqueness and use them in a way that’s aligned with your view of uniqueness. See the Uniqueness section of SecItem: Fundamentals for a link to the relevant documentation. Erroneous Attributes Each keychain item class supports its own specific set of attributes. For information about the attributes supported by a given class, see SecItem: Fundamentals. I regularly see folks use attributes that aren’t supported by the class they’re working with. For example, the kSecAttrApplicationTag attribute is only supported for key items (kSecClassKey). Using it with a certificate item (kSecClassCertificate) will cause, at best, a runtime error and, at worst, mysterious bugs. This is an easy mistake to make because: The ‘parameter block’ nature of the SecItem API means that the compiler won’t complain if you use an erroneous attribute. On macOS, the shim that connects to the file-based keychain ignores unsupported attributes. Imagine you want to store a certificate for a particular user. You might write code like this: let err = SecItemAdd([ kSecClass: kSecClassCertificate, kSecAttrApplicationTag: Data(name.utf8), kSecValueRef: cert, ] as NSDictionary, nil) The goal is to store the user’s name in the kSecAttrApplicationTag attribute so that you can get back their certificate with code like this: let err = SecItemCopyMatching([ kSecClass: kSecClassCertificate, kSecAttrApplicationTag: Data(name.utf8), kSecReturnRef: true, ] as NSDictionary, &copyResult) On iOS, and with the data protection keychain on macOS, both calls will fail with errSecNoSuchAttr. That makes sense, because the kSecAttrApplicationTag attribute is not supported for certificate items. Unfortunately, the macOS shim that connects the SecItem API to the file-based keychain ignores extraneous attributes. This results in some very bad behaviour: SecItemAdd works, ignoring kSecAttrApplicationTag. SecItemCopyMatching ignores kSecAttrApplicationTag, returning the first certificate that it finds. If you only test with a single user, everything seems to work. But, later on, when you try your code with multiple users, you might get back the wrong result depending on the which certificate the SecItemCopyMatching call happens to discover first. Ouch! Context Matters Some properties change behaviour based on the context. The value type properties are the biggest offender here, as discussed in the Value Type Subtleties section of SecItem: Fundamentals. However, there are others. The one that’s bitten me is kSecMatchLimit: In a query and return dictionary its default value is kSecMatchLimitOne. If you don’t supply a value for kSecMatchLimit, SecItemCopyMatching returns at most one item that matches your query. In a pure query dictionary its default value is kSecMatchLimitAll. For example, if you don’t supply a value for kSecMatchLimit, SecItemDelete will delete all items that match your query. This is a lesson that, once learnt, is never forgotten! Note Although this only applies to the data protection keychain. If you’re on macOS and targeting the file-based keychain, kSecMatchLimit always defaults to kSecMatchLimitOne. This is clearly a bug, but we can’t fix it due to compatibility concerns (r. 105800863). Fun times! Digital Identities Aren’t Real A digital identity is the combination of a certificate and the private key that matches the public key within that certificate. The SecItem API has a digital identity keychain item class, namely kSecClassIdentity. However, the keychain does not store digital identities. When you add a digital identity to the keychain, the system stores its components, the certificate and the private key, separately, using kSecClassCertificate and kSecClassKey respectively. This has a number of non-obvious effects: Adding a certificate can ‘add’ a digital identity. If the new certificate happens to match a private key that’s already in the keychain, the keychain treats that pair as a digital identity. Likewise when you add a private key. Similarly, removing a certificate or private key can ‘remove’ a digital identity. Adding a digital identity will either add a private key, or a certificate, or both, depending on what’s already in the keychain. Removing a digital identity removes its certificate. It might also remove the private key, depending on whether that private key is used by a different digital identity. The system forms a digital identity by matching the kSecAttrApplicationLabel (klbl) attribute of the private key with the kSecAttrPublicKeyHash (pkhh) attribute of the certificate. If you add both items to the keychain and the system doesn’t form an identity, check the value of these attributes. For more information the key attributes, see SecItem attributes for keys. Keys Aren’t Stored in the Secure Enclave Apple platforms let you protect a key with the Secure Enclave (SE). The key is then hardware bound. It can only be used by that specific SE [1]. Earlier versions of the Protecting keys with the Secure Enclave article implied that SE-protected keys were stored in the SE itself. This is not true, and it’s caused a lot of confusion. For example, I once asked the keychain team “How much space does the SE have available to store keys?”, a question that’s complete nonsense once you understand how this works. In reality, SE-protected keys are stored in the standard keychain database alongside all your other keychain items. The difference is that the key is wrapped in such a way that only the SE can use it. So, the key is protected by the SE, not stored in the SE. A while back we updated the docs to clarify this point but the confusion persists. [1] Technically it’s that specific iteration of that specific SE. If you erase the device then the key material needed to use the key is erased and so the key becomes permanently useless. Or at least that’s my understanding of how things work (-: For details like this I defer to Apple Platform Security. Careful With that Shim, Mac Developer As explained in TN3137 On Mac keychain APIs and implementations, macOS has a shim that connects the SecItem API to either the data protection keychain or the file-based keychain depending on the nature of the request. That shim has limitations. Some of those are architectural but others are simply bugs in the shim. For some great examples, see the Investigating Complex Attributes section below. The best way to avoid problems like this is to target the data protection keychain. If you can’t do that, try to avoid exploring the outer reaches of the SecItem API. If you encounter a case that doesn’t make sense, try that same case with the data protection keychain. If it works there but fails with the file-based keychain, please do file a bug against the shim. It’ll be in good company. Here’s some known issues with the shim: It ignores unsupported attributes. See Erroneous Attributes, above, for more background on that. The shim can fan out to both the data protection and the file-based keychain. In that case it has to make a policy decision about how to handle errors. This results in some unexpected behaviour (r. 143405965). For example, if you call SecItemCopyMatching while the keychain is locked, the data protection keychain will fail with errSecInteractionNotAllowed (-25308). OTOH, it’s possible to query for the presence of items in the file-based keychain even when it’s locked. If you do that and there’s no matching item, the file-based keychain fails with errSecItemNotFound (-25300). When the shim gets these conflicting errors, it chooses to return the latter. Whether this is right or wrong depends on your perspective, but it’s certainly confusing, especially if you’re coming at this from the iOS side. If you call SecItemDelete without specifying a match limit (kSecMatchLimit), the data protection keychain deletes all matching items, whereas the file-based keychain just deletes a single match (r. 105800863). While these shim issue have all have bug numbers, there’s no guarantee that any of them will be fixed. Fixing bugs like this is tricky because of binary compatibility concerns. Add-only Attributes Some attributes can only be set when you add an item. These attributes are usually associated with the scope of the item. For example, to protect an item with the Secure Enclave, supply the kSecAttrAccessControl attribute to the SecItemAdd call. Once you do that, however, you can’t change the attribute. Calling SecItemUpdate with a new kSecAttrAccessControl won’t work. Lost Keychain Items A common complaint from developers is that a seemingly minor update to their app has caused it to lose all of its keychain items. Usually this is caused by one of two problems: Entitlement changes Query dictionary confusion Access to keychain items is mediated by various entitlements, as described in Sharing access to keychain items among a collection of apps. If the two versions of your app have different entitlements, one version may not be able to ‘see’ items created by the other. Imagine you have an app with an App ID of SKMME9E2Y8.com.example.waffle-varnisher. Version 1 of your app is signed with the keychain-access-groups entitlement set to [ SKMME9E2Y8.groupA, SKMME9E2Y8.groupB ]. That makes its keychain access group list [ SKMME9E2Y8.groupA, SKMME9E2Y8.groupB, SKMME9E2Y8.com.example.waffle-varnisher ]. If this app creates a new keychain item without specifying kSecAttrAccessGroup, the system places the item into SKMME9E2Y8.groupA. If version 2 of your app removes SKMME9E2Y8.groupA from the keychain-access-groups, it’ll no longer be able to see the keychain items created by version 1. You’ll also see this problem if you change your App ID prefix, as described in App ID Prefix Change and Keychain Access. IMPORTANT When checking for this problem, don’t rely on your .entitlements file. There are many steps between it and your app’s actual entitlements. Rather, run codesign to dump the entitlements of your built app: % codesign -d --entitlements - /path/to/your.app Lost Keychain Items, Redux Another common cause of lost keychain items is confusion about query dictionaries, something discussed in detail in this post and SecItem: Fundamentals. If SecItemCopyMatching isn’t returning the expected item, add some test code to get all the items and their attributes. For example, to dump all the generic password items, run code like this: func dumpGenericPasswords() throws { let itemDicts = try secCall { SecItemCopyMatching([ kSecClass: kSecClassGenericPassword, kSecMatchLimit: kSecMatchLimitAll, kSecReturnAttributes: true, ] as NSDictionary, $0) } as! [[String: Any]] print(itemDicts) } Then compare each item’s attributes against the attributes you’re looking for to see why there was no match. Data Protection and Background Execution Keychain items are subject to data protection. Specifically, an item may or may not be accessible depending on whether specific key material is available. For an in-depth discussion of how this works, see Apple Platform Security. Note This section focuses on iOS but you’ll see similar effects on all Apple platforms. On macOS specifically, the contents of this section only apply to the data protection keychain. The keychain supports three data protection levels: kSecAttrAccessibleWhenUnlocked kSecAttrAccessibleAfterFirstUnlock kSecAttrAccessibleAlways Note There are additional data protection levels, all with the ThisDeviceOnly suffix. Understanding those is not necessary to understanding this pitfall. Each data protection level describes the lifetime of the key material needed to work with items protected in that way. Specifically: The key material needed to work with a kSecAttrAccessibleWhenUnlocked item comes and goes as the user locks and unlocks their device. The key material needed to work with a kSecAttrAccessibleAfterFirstUnlock item becomes available when the device is first unlocked and remains available until the device restarts. The default data protection level is kSecAttrAccessibleWhenUnlocked. If you add an item to the keychain and don’t specify a data protection level, this is what you get [1]. To specify a data protection level when you add an item to the keychain, apply the kSecAttrAccessible attribute. Alternatively, embed the access level within a SecAccessControl object and apply that using the kSecAttrAccessControl attribute. IMPORTANT It’s best practice to set these attributes when you add the item and then never update them. See Add-only Attributes, above, for more on that. If you perform an operation whose data protection is incompatible with the currently available key material, that operation fails with errSecInteractionNotAllowed [2]. There are four fundamental keychain operations, discussed in the SecItem: Fundamentals, and each interacts with data protection in a different way: Copy — If you attempt to access a keychain item whose key material is unavailable, SecItemCopyMatching fails with errSecInteractionNotAllowed. This is an obvious result; the whole point of data protection is to enforce this security policy. Add — If you attempt to add a keychain item whose key material is unavailable, SecItemAdd fails with errSecInteractionNotAllowed. This is less obvious. The reason why this fails is that the system needs the key material to protect (by encryption) the keychain item, and it can’t do that if if that key material isn’t available. Update — If you attempt to update a keychain item whose key material is unavailable, SecItemUpdate fails with errSecInteractionNotAllowed. This result is an obvious consequence of the previous result. Delete — Deleting a keychain item, using SecItemDelete, doesn’t require its key material, and thus a delete will succeed when the item is otherwise unavailable. That last point is a significant pitfall. I regularly see keychain code like this: Read an item holding a critical user credential. If that works, use that credential. If it fails, delete the item and start from a ‘factory reset’ state. The problem is that, if your code ends up running in the background unexpectedly, step 1 fails with errSecInteractionNotAllowed and you turn around and delete the user’s credential. Ouch! Note Even if you didn’t write this code, you might have inherited it from a keychain wrapper library. See Think Before Wrapping, below. There are two paths forward here: If you don’t expect this code to work in the background, check for the errSecInteractionNotAllowed error and non-destructively cancel the operation in that case. If you expect this code to be running in the background, switch to a different data protection level. WARNING For the second path, the most obvious fix is to move from kSecAttrAccessibleWhenUnlocked to kSecAttrAccessibleAfterFirstUnlock. However, this is not a panacea. It’s possible that your app might end up running before first unlock [3]. So, if you choose the second path, you must also make sure to follow the advice for the first path. You can determine whether the device is unlocked using the isProtectedDataAvailable property and its associated notifications. However, it’s best not to use this property as part of your core code, because such preflighting is fundamentally racy. Rather, perform the operation and handle the error gracefully. It might make sense to use isProtectedDataAvailable property as part of debugging, logging, and diagnostic code. [1] For file data protection there’s an entitlement (com.apple.developer.default-data-protection) that controls the default data protection level. There’s no such entitlement for the keychain. That’s actually a good thing! In my experience the file data protection entitlement is an ongoing source of grief. See this thread if you’re curious. [2] This might seem like an odd error but it’s actually pretty reasonable: The operation needs some key material that’s currently unavailable. Only a user action can provide that key material. But the data protection keychain will never prompt the user to unlock their device. Thus you get an error instead. [3] iOS generally avoids running third-party code before first unlock, but there are circumstances where that can happen. The obvious legitimate example of this is a VoIP app, where the user expects their phone to ring even if they haven’t unlocked it since the last restart. There are also other less legitimate examples of this, including historical bugs that caused apps to launch in the background before first unlock. Best Practices With the pitfalls out of the way, let’s talk about best practices. Less Painful Dictionaries I look at a lot of keychain code and it’s amazing how much of it is way more painful than it needs to be. The biggest offender here is the dictionaries. Here are two tips to minimise the pain. First, don’t use CFDictionary. It’s seriously ugly. While the SecItem API is defined in terms of CFDictionary, you don’t have to work with CFDictionary directly. Rather, use NSDictionary and take advantage of the toll-free bridge. For example, consider this CFDictionary code: CFTypeRef keys[4] = { kSecClass, kSecAttrService, kSecMatchLimit, kSecReturnAttributes, }; static const int kTen = 10; CFNumberRef ten = CFNumberCreate(NULL, kCFNumberIntType, &kTen); CFAutorelease(ten); CFTypeRef values[4] = { kSecClassGenericPassword, CFSTR("AYS"), ten, kCFBooleanTrue, }; CFDictionaryRef query = CFDictionaryCreate( NULL, keys, values, 4, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks ); Note This might seem rather extreme but I’ve literally seen code like this, and worse, while helping developers. Contrast this to the equivalent NSDictionary code: NSDictionary * query = @{ (__bridge NSString *) kSecClass: (__bridge NSString *) kSecClassGenericPassword, (__bridge NSString *) kSecAttrService: @"AYS", (__bridge NSString *) kSecMatchLimit: @10, (__bridge NSString *) kSecReturnAttributes: @YES, }; Wow, that’s so much better. Second, if you’re working in Swift, take advantage of its awesome ability to create NSDictionary values from Swift dictionary literals. Here’s the equivalent code in Swift: let query = [ kSecClass: kSecClassGenericPassword, kSecAttrService: "AYS", kSecMatchLimit: 10, kSecReturnAttributes: true, ] as NSDictionary Nice! Avoid Reusing Dictionaries I regularly see folks reuse dictionaries for different SecItem calls. For example, they might have code like this: var copyResult: CFTypeRef? = nil let dict = [ kSecClass: kSecClassGenericPassword, kSecAttrService: "AYS", kSecAttrAccount: "mrgumby", kSecReturnData: true, ] as NSMutableDictionary var err = SecItemCopyMatching(dict, &copyResult) if err == errSecItemNotFound { dict[kSecValueData] = Data("opendoor".utf8) err = SecItemAdd(dict, nil) } This specific example will work, but it’s easy to spot the logic error. kSecReturnData is a return type property and it makes no sense to pass it to a SecItemAdd call whose second parameter is nil. I’m not sure why folks do this. I think it’s because they think that constructing dictionaries is expensive. Regardless, this pattern can lead to all sorts of weird problems. For example, it’s the leading cause of the issue described in the Queries and the Uniqueness Constraints section, above. My advice is that you use a new dictionary for each call. That prevents state from one call accidentally leaking into a subsequent call. For example, I’d rewrite the above as: var copyResult: CFTypeRef? = nil let query = [ kSecClass: kSecClassGenericPassword, kSecAttrService: "AYS", kSecAttrAccount: "mrgumby", kSecReturnData: true, ] as NSMutableDictionary var err = SecItemCopyMatching(query, &copyResult) if err == errSecItemNotFound { let add = [ kSecClass: kSecClassGenericPassword, kSecAttrService: "AYS", kSecAttrAccount: "mrgumby", kSecValueData: Data("opendoor".utf8), ] as NSMutableDictionary err = SecItemAdd(add, nil) } It’s a bit longer, but it’s much easier to track the flow. And if you want to eliminate the repetition, use a helper function: func makeDict() -> NSMutableDictionary { [ kSecClass: kSecClassGenericPassword, kSecAttrService: "AYS", kSecAttrAccount: "mrgumby", ] as NSMutableDictionary } var copyResult: CFTypeRef? = nil let query = makeDict() query[kSecReturnData] = true var err = SecItemCopyMatching(query, &copyResult) if err == errSecItemNotFound { let add = makeDict() query[kSecValueData] = Data("opendoor".utf8) err = SecItemAdd(add, nil) } Think Before Wrapping A lot of folks look at the SecItem API and immediately reach for a wrapper library. A keychain wrapper library might seem like a good idea but there are some serious downsides: It adds another dependency to your project. Different subsystems within your project may use different wrappers. The wrapper can obscure the underlying API. Indeed, its entire raison d’être is to obscure the underlying API. This is problematic if things go wrong. I regularly talk to folks with hard-to-debug keychain problems and the conversation goes something like this: Quinn: What attributes do you use in the query dictionary? J R Developer: What’s a query dictionary? Quinn: OK, so what error are you getting back? J R Developer: It throws WrapperKeychainFailedError. That’s not helpful )-: If you do use a wrapper, make sure it has diagnostic support that includes the values passed to and from the SecItem API. Also make sure that, when it fails, it returns an error that includes the underlying keychain error code. These benefits will be particularly useful if you encounter a keychain problem that only shows up in the field. Wrappers must choose whether to be general or specific. A general wrapper may be harder to understand than the equivalent SecItem calls, and it’ll certainly contain a lot of complex code. On the other hand, a specific wrapper may have a model of the keychain that doesn’t align with your requirements. I recommend that you think twice before using a keychain wrapper. Personally I find the SecItem API relatively easy to call, assuming that: I use the techniques shown in Less Painful Dictionaries, above, to avoid having to deal with CFDictionary. I use my secCall(…) helpers to simplify error handling. For the code, see Calling Security Framework from Swift. If you’re not prepared to take the SecItem API neat, consider writing your own wrapper, one that’s tightly focused on the requirements of your project. For example, in my VPN apps I use the wrapper from this post, which does exactly what I need in about 100 lines of code. Prefer to Update Of the four SecItem functions, SecItemUpdate is the most neglected. Rather than calling SecItemUpdate I regularly see folks delete and then re-add the item. This is a shame because SecItemUpdate has some important benefits: It preserves persistent references. If you delete and then re-add the item, you get a new item with a new persistent reference. It’s well aligned with the fundamental database nature of the keychain. It forces you to think about which attributes uniquely identify your item and which items can be updated without changing the item’s identity. For a cool example of its power, check out Transfer Items Between Keychain Access Groups, below. Understand These Key Attributes Key items have a number of attributes that are similarly named, and it’s important to keep them straight. I created a cheat sheet for this, namely, SecItem attributes for keys. You wouldn’t believe how often I consult this! Investigating Complex Attributes Some attributes have values where the format is not obvious. For example, the kSecAttrIssuer attributed is documented as: The corresponding value is of type CFData and contains the X.500 issuer name of a certificate. What exactly does that mean? If I want to search the keychain for all certificates issued by a specific certificate authority, what value should I supply? One way to figure this out is to add a certificate to the keychain, read the attributes back, and then dump the kSecAttrIssuer value. For example: let cert: SecCertificate = … let attrs = try secCall { SecItemAdd([ kSecValueRef: cert, kSecReturnAttributes: true, ] as NSDictionary, $0) } as! [String: Any] let issuer = attrs[kSecAttrIssuer as String] as! NSData print((issuer as NSData).debugDescription) // prints: <3110300e 06035504 030c074d 6f757365 4341310b 30090603 55040613 024742> Those bytes represent the contents of a X.509 Name ASN.1 structure with DER encoding. This is without the outer SEQUENCE element, so if you dump it as ASN.1 you’ll get a nice dump of the first SET and then a warning about extra stuff at the end of the file: % xxd issuer.asn1 00000000: 3110 300e 0603 5504 030c 074d 6f75 7365 1.0...U....Mouse 00000010: 4341 310b 3009 0603 5504 0613 0247 42 CA1.0...U....GB % dumpasn1 -p issuer.asn1 SET { SEQUENCE { OBJECT IDENTIFIER commonName (2 5 4 3) UTF8String 'MouseCA' } } Warning: Further data follows ASN.1 data at position 18. Note For details on the Name structure, see section 4.1.2.4 of RFC 5280. Amusingly, if you run the same test against the file-based keychain you’ll… crash. OK, that’s not amusing. It turns out that the code above doesn’t work when targeting the file-based keychain because SecItemAdd doesn’t return a dictionary but rather an array of dictionaries (r. 21111543). Once you get past that, however, you’ll see it print: <301f3110 300e0603 5504030c 074d6f75 73654341 310b3009 06035504 06130247 42> Which is different! Dumping it as ASN.1 shows that it’s the full Name structure, including the outer SEQUENCE element: % xxd issuer-file-based.asn1 00000000: 301f 3110 300e 0603 5504 030c 074d 6f75 0.1.0...U....Mou 00000010: 7365 4341 310b 3009 0603 5504 0613 0247 seCA1.0...U....G 00000020: 42 B % dumpasn1 -p issuer-file-based.asn1 SEQUENCE { SET { SEQUENCE { OBJECT IDENTIFIER commonName (2 5 4 3) UTF8String 'MouseCA' } } SET { SEQUENCE { OBJECT IDENTIFIER countryName (2 5 4 6) PrintableString 'GB' } } } This difference in behaviour between the data protection and file-based keychains is a known bug (r. 26391756) but in this case it’s handy because the file-based keychain behaviour makes it easier to understand the data protection keychain behaviour. Import, Then Add It’s possible to import data directly into the keychain. For example, you might use this code to add a certificate: let certData: Data = … try secCall { SecItemAdd([ kSecClass: kSecClassCertificate, kSecValueData: certData, ] as NSDictionary, nil) } However, it’s better to import the data and then add the resulting credential reference. For example: let certData: Data = … let cert = try secCall { SecCertificateCreateWithData(nil, certData as NSData) } try secCall { SecItemAdd([ kSecValueRef: cert, ] as NSDictionary, nil) } There are two advantages to this: If you get an error, you know whether the problem was with the import step or the add step. It ensures that the resulting keychain item has the correct attributes. This is especially important for keys. These can be packaged in a wide range of formats, so it’s vital to know whether you’re interpreting the key data correctly. I see a lot of code that adds key data directly to the keychain. That’s understandable because, back in the day, this was the only way to import a key on iOS. Fortunately, that’s not been the case since the introduction of SecKeyCreateWithData in iOS 10 and aligned releases. For more information about importing keys, see Importing Cryptographic Keys. App Groups on the Mac Sharing access to keychain items among a collection of apps explains that three entitlements determine your keychain access: keychain-access-groups application-identifier (com.apple.application-identifier on macOS) com.apple.security.application-groups In the discussion of the last item says: You can use app group names as keychain access group names, without adding them to the Keychain access groups entitlement. That’s true, but it’s also potentially misleading. This affordance works all the time on iOS and its child platforms. But on the Mac it only works if your entitlements are validated by a provisioning profile. For more on that topic, see App Groups: macOS vs iOS: Working Towards Harmony. Transfer Items Between Keychain Access Groups In some cases you might want to move a bunch of keychain items from one app group to another, for example, when preparing for an App ID prefix change. This is easier than you might first think. For example, to move all the generic password items for a particular service between oldGroup and newGroup, run this code: try secCall { SecItemUpdate([ kSecClass: kSecClassGenericPassword, kSecUseDataProtectionKeychain: true, kSecAttrAccessGroup: oldGroup, kSecAttrService: "MyService", ] as NSDictionary, [ kSecAttrAccessGroup: newGroup, ] as NSDictionary) } This snippet highlights both the power and the subtlety of the SecItem API. The first parameter to SecItemUpdate is a pure query dictionary. It selects all the generic password items for MyService that are in the old keychain access group. In contrast, the second parameter is an update dictionary, which in this case just changes a single attribute. See SecItem: Fundamentals for a deeper explanation of these concepts. This call is atomic from your perspective [1]. The call will either fail or all the selected items will move as one. IMPORTANT Bulk operations like this are risky. That’s not because the keychain item will do the wrong thing, but rather because you have to be very careful what you ask for. If, for example, your query dictionary matches more than you intended, you might end up moving items unexpectedly. Be careful when crafting this code, and test it thoroughly. [1] It may even be atomic in a wider sense, given that the keychain is currently implemented as an SQLite database. Revision History 2026-04-02 Added the Transfer Items Between Keychain Access Groups section. Updated the App Groups on the Mac section to account for recent changes to app groups on the Mac. Made other minor editorial changes. 2025-06-29 Added the Data Protection and Background Execution section. Made other minor editorial changes. 2025-02-03 Added another specific example to the Careful With that Shim, Mac Developer section. 2025-01-29 Added somes specific examples to the Careful With that Shim, Mac Developer section. 2025-01-23 Added the Import, Then Add section. 2024-08-29 Added a discussion of identity formation to the Digital Identities Aren’t Real section. 2024-04-11 Added the App Groups on the Mac section. 2023-10-25 Added the Lost Keychain Items and Lost Keychain Items, Redux sections. 2023-09-22 Made minor editorial changes. 2023-09-12 Fixed various bugs in the revision history. Added the Erroneous Attributes section. 2023-02-22 Fixed the link to the VPNKeychain post. Corrected the name of the Context Matters section. Added the Investigating Complex Attributes section. 2023-01-28 First posted.
0
0
4k
1w
why prepareInterfaceToProvideCredential does call
we develop extension "Autofill Credential Provider" function for passkey. 1.first step registe passkey 2.second step authenticate with passkey step 1 &amp; step 2 has finished and run success with provideCredentialWithoutUserInteraction. But we want to prepare our interface for use to input password and select passkey what the want. however the func prepareInterfaceToProvideCredential in ASCredentialProviderViewController does call? what i missed? how can i do it?
0
0
178
Jul ’25
APP ID's indentifier not updating
When implementing Sign In with Apple I created an App ID and a Service ID for my app. I didn't configure the Server-to-Server Notification URL properly there and token revocation didn't work. Later on I updated the url config and the name of the identifiers. However, when I Sign in with Apple in my app I still see the old identifier name in my iPhone Settings->Apple Account->Sign in with Apple. I would assume that if the name doesn't update, the configuration doesn't update either. I'm using automatic Xcode signing, I have deleted all the profiles locally, cleaned project, bumped versions, waited for a week, nothing worked. Token revocation for account deletion doesn't work properly I would assume because of the initial misconfiguration. I want to mention that this is working fine for my development build (another bundleID, AppID, ServiceID) What am I missing here?
0
0
134
Jun ’25
Privacy & Security Resources
General: Forums topic: Privacy & Security Privacy Resources Security Resources Share and Enjoy — Quinn “The Eskimo!” @ Developer Technical Support @ Apple let myEmail = "eskimo" + "1" + "@" + "apple.com"
0
0
580
Jul ’25
Authentication using MSAL library in offline mode
Hi. We are trying to get the access token before calling any API. The app can go in bad network areas but the token acquisition keeps happening for the network call. The devices are managed devices which means it has some policies installed. We are using MSAL lib for the authentication and we are investigating from that angle too but the below error seems to be coming from apple authentication which needs our attention. ========================================== LaunchServices: store (null) or url (null) was nil: Error Domain=NSOSStatusErrorDomain Code=-54 "process may not map database" UserInfo={NSDebugDescription=process may not map database, _LSLine=68, _LSFunction=_LSServer_GetServerStoreForConnectionWithCompletionHandler} Attempt to map database failed: permission was denied. This attempt will not be retried. Failed to initialize client context with error Error Domain=NSOSStatusErrorDomain Code=-54 "process may not map database" UserInfo={NSDebugDescription=process may not map database, _LSLine=68, _LSFunction=_LSServer_GetServerStoreForConnectionWithCompletionHandler} Failed to get application extension record: Error Domain=NSOSStatusErrorDomain Code=-54 "(null)" ASAuthorizationController credential request failed with error: Error Domain=com.apple.AuthenticationServices.AuthorizationError Code=1003 "(null)" ========================================== This happens mostly when we switches the network or keep the device in no or low network area. This comes sometimes when app goes in background too. Just trying to give as much as information I could. Any lead would be highly appreciated. Thank you
0
0
146
Apr ’25
Developing Platform SSO extension
Hi, I am developing a Platform SSO in order to have integrated with our IdP, which I am also adapting to provide the right endpoints for Platform SSO. I have a few questions about the implementation: does the client-request-id need to be present on all requests? Is it unique per request, or requests that are bound together like those requesting a nonce and those who will use that nonce should use the same client-request-id? I am not sure how the loginManager.presentRegistrationViewController works. I'd like to get the user to authenticate to my IdP before device registration. So I am not sure if I should provide my own Webview or something similar or if this method should do something for me; My idea is to request user authentication once, save the state when performing device registration, so that I avoid asking for user authentication twice when performing user registration. Is this the right way to do it? How does platform SSO handles tokens? If one application of my IdP requests the authentication on a common OIDC/OAuth2 flow, should I perform some sort of token exchange? How about SAML? Platform SSO seems to be token-centric, but how does one handle SAML flows? Is it by using WebView as well?
0
0
158
Nov ’25
Production-Grade Implementation Guidance: DCError Matrices, Retry Strategies, and Simulator Testing for App Attest APIs
Hi there, We're implementing Apple's DeviceCheck App Attest for production iOS authentication. The public documentation defines DCError cases but doesn't specify which errors are expected per API method or recommend retry/remediation strategies. We need Apple's guidance to implement robust, production-aligned error handling before rollout. 1. Error Surface per API Method Question: Can you confirm the complete, officially expected set of DCError values for each method? We understand the following errors are possible across App Attest APIs: invalidKey invalidInput featureUnsupported serverUnavailable unknownSystemFailure Specifically, please confirm which errors can occur for: DCAppAttestService.generateKey() DCAppAttestService.attestKey(_:clientData:) DCAppAttestService.generateAssertion(keyID:clientData:) Are there any additional undocumented or edge-case errors we should handle? 2. Retry Strategy & Remediation Matrix Question: For each API method and error code, please help us with proposal around which errorCode is retriable, whats the remediation pre retry, retry cap and backoff strategy: Kindly also help with errors that are not covered here: Specific sub-questions: invalidKey handling: When this error occurs: Should the app delete the key and call generateKey again? Or should it fail the entire flow? serverUnavailable handling: Should we retry immediately, or wait before retrying? Is exponential backoff recommended? What's the recommended max retry count? Backoff strategy: Which errors (if any) qualify for exponential backoff? Recommended base delay, max delay, and jitter approach? When should we give up and fail the request? unknownSystemFailure: Is this retriable or should we fail? Any known causes or mitigations? 3. Simulator Testing Questions: Simulator API behavior: Can App Attest APIs be called normally on iOS Simulator? If not, is there a way to simulate for testing. Do they complete successfully with simulated attestations, or do they fail? Thanks, Nirekshitha
0
0
230
1w
Sign in with Apple Web: invalid_client on token exchange with real authorization code, but invalid_grant with dummy code
We are integrating Sign in with Apple for our web application and have been stuck on an invalid_client error during the token exchange step. The Problem The authorization step works fine — the user authenticates on Apple's page and a valid authorization code is returned to our callback URL. However, when we exchange that code at https://appleid.apple.com/auth/token, it returns: {"error": "invalid_client"} The Puzzling Part When we send a dummy/expired authorization code with the exact same client_id and client_secret, Apple returns: {"error": "invalid_grant", "error_description": "The code has expired or has been revoked."} This confirms that our client credentials (client_id + client_secret JWT) are valid and accepted by Apple. The invalid_client error only occurs when a real, freshly-issued authorization code is used. Configuration Service ID configured with Sign in with Apple enabled Primary App ID with Sign in with Apple capability enabled Domain verified, Return URL registered Key created with Sign in with Apple enabled, linked to the correct Primary App ID Client Secret JWT Generated per Apple's documentation: Header: alg: ES256, kid set to our Key ID Claims: iss: Team ID iat: current timestamp exp: iat + 6 months (within Apple's limit) aud: https://appleid.apple.com sub: Service ID (matches the client_id used in authorization) Signed with: the .p8 private key associated with the Key Token Exchange Request POST https://appleid.apple.com/auth/tokenContent-Type: application/x-www-form-urlencodedclient_id=client_secret=code=grant_type=authorization_coderedirect_uri= What We've Tried Standalone test endpoint — built a minimal endpoint (no framework) that does the token exchange via server-side curl. Same invalid_client. Multiple Service IDs — created and tried 3 different Service IDs. All produce the same error with real codes. Multiple Keys — tried 2 different keys. Same error. Verified redirect_uri matches exactly between the authorization request and token request. Verified client_id matches exactly between the authorization URL and token request. Used client_secret_post (credentials in body, not Basic auth header). Freshness — code is used immediately upon receipt (within seconds), well before the 5-minute expiry. Filed a Developer Support case — was directed to Forums. Summary Scenario code Result Dummy/expired code abc123 invalid_grant (credentials accepted) Real fresh code from Apple callback invalid_client This pattern suggests something goes wrong specifically when Apple validates the authorization code against the client — even though the client credentials themselves are accepted in isolation. Has anyone encountered this behavior? Is there a known configuration issue that could cause invalid_client only with valid authorization codes? Any guidance would be greatly appreciated.
0
0
117
2w
Unexpectedly invalidated Biometrics in iOS 18.3.2 or later
There is a sudden surge of users in our apps with invalidated biometrics. Even though the issue is being handled correctly and the user has another way to login, some of the users forgot their passwords and they can not login. Is there any known issue with Biometrics in iOS 18.3.2 or later? There is a (possible) related discussion here: https://discussions.apple.com/thread/256011565
1
0
122
Apr ’25
Apple Sign-In: "invalid-credential" error despite correct configuration - Firebase Auth iOS
Problem Summary I'm experiencing a persistent invalid-credential error with Apple Sign-In on iOS despite having verified every aspect of the configuration over the past 6 months. The error occurs at the Firebase Authentication level after successfully receiving credentials from Apple. Error Message: Firebase auth error: invalid-credential - Invalid OAuth response from apple.com. Environment Platform: iOS (Flutter app) Firebase Auth: v5.7.0 Sign in with Apple: v6.1.2 Xcode: Latest version with capability enabled iOS Target: 13.0+ Bundle ID: com.harmonics.orakl What Actually Happens ✅ Apple Sign-In popup appears ✅ User can authenticate with Apple ID ✅ Apple returns credentials with identityToken ❌ Firebase rejects with invalid-credential error The error occurs at Firebase level, not Apple level. What I've Tried Created a brand new Apple Key (previous key was 6 months old) Tested with both App ID and Service ID in Firebase Completely reinstalled CocoaPods dependencies Verified nonce handling is correct (hashed to Apple, raw to Firebase) Activated Firebase Hosting and attempted to deploy .well-known file Checked Cloud Logging (no detailed error messages found) Disabled and re-enabled Apple Sign-In provider in Firebase Verified Return URL matches exactly Waited and retried multiple times over 6 months Questions Is the .well-known/apple-developer-domain-association.txt file required? If yes, how should it be generated? Firebase Hosting doesn't auto-generate it. Could there be a server-side caching/blacklist issue with my domain or Service ID after multiple failed attempts? Should the Apple Key be linked to the Service ID instead of the App ID? The key shows as linked to Z3NNDZVWMZ.com.harmonics.orakl (the App ID). Is there any way to get more detailed error logs from Firebase about why it's rejecting the Apple OAuth response? Could using a custom domain instead of .firebaseapp.com resolve the issue? Additional Context Google Sign-In works perfectly on the same app The configuration has been reviewed by multiple developers Error persists across different devices and iOS versions No errors in Xcode console except the Firebase rejection Any help would be greatly appreciated. I've exhausted all standard troubleshooting steps and documentation. Project Details: Bundle ID: com.harmonics.orakl Firebase Project: harmonics-app Team ID: Z3N....... code : // 1. Generate raw nonce final String rawNonce = _generateRandomNonce(); // 2. Hash with SHA-256 final String hashedNonce = _sha256Hash(rawNonce); // 3. Send HASHED nonce to Apple ✅ final appleCredential = await SignInWithApple.getAppleIDCredential( scopes: [AppleIDAuthorizationScopes.email, AppleIDAuthorizationScopes.fullName], nonce: hashedNonce, // Correct: hashed nonce to Apple ); // 4. Create Firebase credential with RAW nonce ✅ final oauthCredential = OAuthProvider("apple.com").credential( idToken: appleCredential.identityToken!, rawNonce: rawNonce, // Correct: raw nonce to Firebase ); // 5. Sign in with Firebase - ERROR OCCURS HERE ❌ await FirebaseAuth.instance.signInWithCredential(oauthCredential);
0
0
100
Oct ’25
iPhone + Safari + Passwords violates WebAuthn spec when pubKeyCredParams doesn't contain ES256
WebAuthn Level 3 § 6.3.2 Step 2 states the authenticator must : Check if at least one of the specified combinations of PublicKeyCredentialType and cryptographic parameters in credTypesAndPubKeyAlgs is supported. If not, return an error code equivalent to "NotSupportedError" and terminate the operation. On my iPhone 15 Pro Max running iOS 18.5, Safari + Passwords does not exhibit this behavior; instead an error is not reported and an ES256 credential is created when an RP passes a non-empty sequence that does not contain {"type":"public-key","alg":-7} (e.g., [{"type":"public-key","alg":-8}]). When I use Chromium 138.0.7204.92 on my laptop running Arch Linux in conjunction with the Passwords app (connected via the "hybrid" protocol), a credential is not created and instead an error is reported per the spec.
3
0
534
Jul ’25
Clarity App Attestation Errors
I'm currently reviewing the various DCError cases defined in Apple’s DeviceCheck framework (reference: https://developer.apple.com/documentation/devicecheck/dcerror-swift.struct). To better understand how to handle these in production, I’m looking for a clear breakdown of: Which specific DCError values can occur during service.generateKey, service.attestKey, and service.generateAssertion The realworld scenarios or conditions that typically cause each error for each method. If anyone has insight on how these errors arise and what conditions trigger them, I’d appreciate your input.
1
0
285
2w
Security Resources
General: Forums topic: Privacy & Security Apple Platform Security support document Developer > Security Enabling enhanced security for your app documentation article Creating enhanced security helper extensions documentation article Security Audit Thoughts forums post Cryptography: Forums tags: Security, Apple CryptoKit Security framework documentation Apple CryptoKit framework documentation Common Crypto man pages — For the full list of pages, run: % man -k 3cc For more information about man pages, see Reading UNIX Manual Pages. On Cryptographic Key Formats forums post SecItem attributes for keys forums post CryptoCompatibility sample code Keychain: Forums tags: Security Security > Keychain Items documentation TN3137 On Mac keychain APIs and implementations SecItem Fundamentals forums post SecItem Pitfalls and Best Practices forums post Investigating hard-to-reproduce keychain problems forums post App ID Prefix Change and Keychain Access forums post Smart cards and other secure tokens: Forums tag: CryptoTokenKit CryptoTokenKit framework documentation Mac-specific resources: Forums tags: Security Foundation, Security Interface Security Foundation framework documentation Security Interface framework documentation BSD Privilege Escalation on macOS Related: Networking Resources — This covers high-level network security, including HTTPS and TLS. Network Extension Resources — This covers low-level network security, including VPN and content filters. Code Signing Resources Notarisation Resources Trusted Execution Resources — This includes Gatekeeper. App Sandbox Resources Share and Enjoy — Quinn “The Eskimo!” @ Developer Technical Support @ Apple let myEmail = "eskimo" + "1" + "@" + "apple.com"
0
0
3.8k
Nov ’25
App Attest server unreachable – DNS or firewall issue suspected
Hello, We are working on integrating app integrity verification into our service application, following Apple's App Attest and DeviceCheck guide. Our server issues a challenge to the client, which then sends the challenge, attestation, and keyId in CBOR format to Apple's App Attest server for verification. However, we are unable to reach both https://attest.apple.com and https://attest.development.apple.com due to network issues. These attempts have been made from both our internal corporate network and mobile hotspot environments. Despite adjusting DNS settings and other configurations, the issue persists. Are there alternative methods or solutions to address this problem? Any recommended network configurations or guidelines to successfully connect to Apple's App Attest servers would be greatly appreciated. Thank you.
0
0
191
May ’25
External website handling and ATT
Our proposed solution to identify an app user when opening a website operated by app developer is: Apps sends a request to backed with app users auth header Backend fetches a generated authenticated url from website backend, based on users auth header App opens it in browser The browser journey is self contained within domain of the business. Would this interaction require an ATT request given that the users identity cannot be tracked back to the app user ? Thanks
0
0
123
3w
DeviceCheck.generateToken, Error: com.apple.devicecheck.error 0
Dear Apple Developer Support Team, We are experiencing a recurring issue with the DeviceCheck API where the following error is being returned: com.apple.devicecheck.error 0 Upon analyzing our logs, we have noticed that this error occurs significantly more often when users are connected to Wi-Fi networks, compared to mobile networks. This leads us to suspect that there might be a relationship between Wi-Fi configuration and the DeviceCheck service’s ability to generate or validate tokens. We would like to know: Is this error code (0) known to be caused by specific types of network behavior or misconfigurations on Wi-Fi networks (e.g., DNS filtering, firewall restrictions, proxy servers)? Are there any recommended best practices for ensuring reliable DeviceCheck API communication over Wi-Fi networks? Additionally, could you please clarify what general conditions could trigger this com.apple.devicecheck.error 0? The lack of specific documentation makes debugging this issue difficult from our side. Any guidance or internal documentation on this error code and its potential causes would be greatly appreciated. IDE: Xcode 16.3 Looking forward to your support. Best regards,
2
0
167
May ’25
"access_denied" error during Sign in with Apple user migration (TN3159)
Hello, I am currently process of migrating an app from Team A to Team B and attempting to generate transfer identifiers using the migration endpoint: POST https://appleid.apple.com/auth/usermigrationinfo. Content-Type: application/x-www-form-urlencoded However, I am consistently receiving an { "error": "access_denied" } response. [Current Configuration] Team A (Source): Primary App ID: com.example.primary Grouped App IDs: com.example.service (Services ID for Web) com.example.app (App ID for iOS - The one being transferred) All identifiers are under the same App Group. Team B (Destination): New App ID and Key created. [Steps Taken] Created a Client Secret (JWT) using Team A's Key ID and Team ID. The sub (subject) in the JWT is set to the Primary App ID of Team A. Requesting with client_id (Primary App ID), client_secret (JWT), and user_token. [Questions] 1. App Group Impact: Does the fact that the App being transferred is a Grouped App ID (not the Primary) affect the usermigrationinfo request? Should I use the Primary App ID or the specific Grouped App ID as the client_id? 2. Ungrouping Safety: If I need to ungroup the App ID from the Primary App ID to resolve this: Will existing users still be able to sign in without issues? Is there any risk of changing the sub (user identifier) that the app receives from Apple? Will this cause any immediate service interruption for the live app? Any insights on why access_denied occurs in this Primary-Grouped configuration would be greatly appreciated.
1
0
145
Mar ’26
ScreenCapture permissions disappear and don't return
On Tahoe and earlier, ScreenCapture permissions can disappear and not return. Customers are having an issue with this disappearing and when our code executes CGRequestScreenCaptureAccess() nothing happens, the prompt does not appear. I can reproduce this by using the "-" button and removing the entry in the settings, then adding it back with the "+" button. CGPreflightScreenCaptureAccess() always returns the correct value but once the entry has been removed, CGRequestScreenCaptureAccess() requires a reboot before it will work again.
3
0
329
3w
requestTrackingAuthorization stuck in .notDetermined
When developing and testing using my phone I got prompted for allowing app tracking. I later uploaded a build to TestFlight, deleted the old testing app and installed the TestFlight build. I am now stuck in an infinite loop of not getting prompted for allowing app tracking for the app. When entering the app settings the toggle for tracking never appears which leaves me not able to enter the app's content. My guess is that the prompt can only be shown once for the app bundle, but there has to be a way for me to get prompted again without changing the app bundle id. Help is appreciated since this app is scheduled to be published in a week.
Replies
0
Boosts
0
Views
168
Activity
May ’25
SecItem: Pitfalls and Best Practices
I regularly help developers with keychain problems, both here on DevForums and in various DTS cases. Over the years I’ve learnt a lot about the API, including many pitfalls and best practices. This post is my attempt to collect that experience in one place. If you have questions or comments about any of this, put them in a new thread and apply the Security tag so that I see it. Share and Enjoy — Quinn “The Eskimo!” @ Developer Technical Support @ Apple let myEmail = "eskimo" + "1" + "@" + "apple.com" SecItem: Pitfalls and Best Practices It’s just four functions, how hard can it be? The SecItem API seems very simple. After all, it only has four function calls, how hard can it be? In reality, things are not that easy. Various factors contribute to making this API much trickier than it might seem at first glance. This post explains some of the keychain’s pitfalls and then goes on to explain various best practices. Before reading this, make sure you understand the fundamentals by reading its companion post, SecItem: Fundamentals. Pitfalls Lets start with some common pitfalls. Queries and Uniqueness Constraints The relationship between query dictionaries and uniqueness constraints is a major source of problems with the keychain API. Consider code like this: var copyResult: CFTypeRef? = nil let query = [ kSecClass: kSecClassGenericPassword, kSecAttrService: "AYS", kSecAttrAccount: "mrgumby", kSecAttrGeneric: Data("SecItemHints".utf8), ] as NSMutableDictionary let err = SecItemCopyMatching(query, &copyResult) if err == errSecItemNotFound { query[kSecValueData] = Data("opendoor".utf8) let err2 = SecItemAdd(query, nil) if err2 == errSecDuplicateItem { fatalError("… can you get here? …") } } Can you get to the fatal error? At first glance this might not seem possible because you’ve run your query and it’s returned errSecItemNotFound. However, the fatal error is possible because the query contains an attribute, kSecAttrGeneric, that does not contribute to the uniqueness. If the keychain contains a generic password whose service (kSecAttrService) and account (kSecAttrAccount) attributes match those supplied but whose generic (kSecAttrGeneric) attribute does not, the SecItemCopyMatching calls will return errSecItemNotFound. However, for a generic password item, of the attributes shown here, only the service and account attributes are included in the uniqueness constraint. If you try to add an item where those attributes match an existing item, the add will fail with errSecDuplicateItem even though the value of the generic attribute is different. The take-home point is that that you should study the attributes that contribute to uniqueness and use them in a way that’s aligned with your view of uniqueness. See the Uniqueness section of SecItem: Fundamentals for a link to the relevant documentation. Erroneous Attributes Each keychain item class supports its own specific set of attributes. For information about the attributes supported by a given class, see SecItem: Fundamentals. I regularly see folks use attributes that aren’t supported by the class they’re working with. For example, the kSecAttrApplicationTag attribute is only supported for key items (kSecClassKey). Using it with a certificate item (kSecClassCertificate) will cause, at best, a runtime error and, at worst, mysterious bugs. This is an easy mistake to make because: The ‘parameter block’ nature of the SecItem API means that the compiler won’t complain if you use an erroneous attribute. On macOS, the shim that connects to the file-based keychain ignores unsupported attributes. Imagine you want to store a certificate for a particular user. You might write code like this: let err = SecItemAdd([ kSecClass: kSecClassCertificate, kSecAttrApplicationTag: Data(name.utf8), kSecValueRef: cert, ] as NSDictionary, nil) The goal is to store the user’s name in the kSecAttrApplicationTag attribute so that you can get back their certificate with code like this: let err = SecItemCopyMatching([ kSecClass: kSecClassCertificate, kSecAttrApplicationTag: Data(name.utf8), kSecReturnRef: true, ] as NSDictionary, &copyResult) On iOS, and with the data protection keychain on macOS, both calls will fail with errSecNoSuchAttr. That makes sense, because the kSecAttrApplicationTag attribute is not supported for certificate items. Unfortunately, the macOS shim that connects the SecItem API to the file-based keychain ignores extraneous attributes. This results in some very bad behaviour: SecItemAdd works, ignoring kSecAttrApplicationTag. SecItemCopyMatching ignores kSecAttrApplicationTag, returning the first certificate that it finds. If you only test with a single user, everything seems to work. But, later on, when you try your code with multiple users, you might get back the wrong result depending on the which certificate the SecItemCopyMatching call happens to discover first. Ouch! Context Matters Some properties change behaviour based on the context. The value type properties are the biggest offender here, as discussed in the Value Type Subtleties section of SecItem: Fundamentals. However, there are others. The one that’s bitten me is kSecMatchLimit: In a query and return dictionary its default value is kSecMatchLimitOne. If you don’t supply a value for kSecMatchLimit, SecItemCopyMatching returns at most one item that matches your query. In a pure query dictionary its default value is kSecMatchLimitAll. For example, if you don’t supply a value for kSecMatchLimit, SecItemDelete will delete all items that match your query. This is a lesson that, once learnt, is never forgotten! Note Although this only applies to the data protection keychain. If you’re on macOS and targeting the file-based keychain, kSecMatchLimit always defaults to kSecMatchLimitOne. This is clearly a bug, but we can’t fix it due to compatibility concerns (r. 105800863). Fun times! Digital Identities Aren’t Real A digital identity is the combination of a certificate and the private key that matches the public key within that certificate. The SecItem API has a digital identity keychain item class, namely kSecClassIdentity. However, the keychain does not store digital identities. When you add a digital identity to the keychain, the system stores its components, the certificate and the private key, separately, using kSecClassCertificate and kSecClassKey respectively. This has a number of non-obvious effects: Adding a certificate can ‘add’ a digital identity. If the new certificate happens to match a private key that’s already in the keychain, the keychain treats that pair as a digital identity. Likewise when you add a private key. Similarly, removing a certificate or private key can ‘remove’ a digital identity. Adding a digital identity will either add a private key, or a certificate, or both, depending on what’s already in the keychain. Removing a digital identity removes its certificate. It might also remove the private key, depending on whether that private key is used by a different digital identity. The system forms a digital identity by matching the kSecAttrApplicationLabel (klbl) attribute of the private key with the kSecAttrPublicKeyHash (pkhh) attribute of the certificate. If you add both items to the keychain and the system doesn’t form an identity, check the value of these attributes. For more information the key attributes, see SecItem attributes for keys. Keys Aren’t Stored in the Secure Enclave Apple platforms let you protect a key with the Secure Enclave (SE). The key is then hardware bound. It can only be used by that specific SE [1]. Earlier versions of the Protecting keys with the Secure Enclave article implied that SE-protected keys were stored in the SE itself. This is not true, and it’s caused a lot of confusion. For example, I once asked the keychain team “How much space does the SE have available to store keys?”, a question that’s complete nonsense once you understand how this works. In reality, SE-protected keys are stored in the standard keychain database alongside all your other keychain items. The difference is that the key is wrapped in such a way that only the SE can use it. So, the key is protected by the SE, not stored in the SE. A while back we updated the docs to clarify this point but the confusion persists. [1] Technically it’s that specific iteration of that specific SE. If you erase the device then the key material needed to use the key is erased and so the key becomes permanently useless. Or at least that’s my understanding of how things work (-: For details like this I defer to Apple Platform Security. Careful With that Shim, Mac Developer As explained in TN3137 On Mac keychain APIs and implementations, macOS has a shim that connects the SecItem API to either the data protection keychain or the file-based keychain depending on the nature of the request. That shim has limitations. Some of those are architectural but others are simply bugs in the shim. For some great examples, see the Investigating Complex Attributes section below. The best way to avoid problems like this is to target the data protection keychain. If you can’t do that, try to avoid exploring the outer reaches of the SecItem API. If you encounter a case that doesn’t make sense, try that same case with the data protection keychain. If it works there but fails with the file-based keychain, please do file a bug against the shim. It’ll be in good company. Here’s some known issues with the shim: It ignores unsupported attributes. See Erroneous Attributes, above, for more background on that. The shim can fan out to both the data protection and the file-based keychain. In that case it has to make a policy decision about how to handle errors. This results in some unexpected behaviour (r. 143405965). For example, if you call SecItemCopyMatching while the keychain is locked, the data protection keychain will fail with errSecInteractionNotAllowed (-25308). OTOH, it’s possible to query for the presence of items in the file-based keychain even when it’s locked. If you do that and there’s no matching item, the file-based keychain fails with errSecItemNotFound (-25300). When the shim gets these conflicting errors, it chooses to return the latter. Whether this is right or wrong depends on your perspective, but it’s certainly confusing, especially if you’re coming at this from the iOS side. If you call SecItemDelete without specifying a match limit (kSecMatchLimit), the data protection keychain deletes all matching items, whereas the file-based keychain just deletes a single match (r. 105800863). While these shim issue have all have bug numbers, there’s no guarantee that any of them will be fixed. Fixing bugs like this is tricky because of binary compatibility concerns. Add-only Attributes Some attributes can only be set when you add an item. These attributes are usually associated with the scope of the item. For example, to protect an item with the Secure Enclave, supply the kSecAttrAccessControl attribute to the SecItemAdd call. Once you do that, however, you can’t change the attribute. Calling SecItemUpdate with a new kSecAttrAccessControl won’t work. Lost Keychain Items A common complaint from developers is that a seemingly minor update to their app has caused it to lose all of its keychain items. Usually this is caused by one of two problems: Entitlement changes Query dictionary confusion Access to keychain items is mediated by various entitlements, as described in Sharing access to keychain items among a collection of apps. If the two versions of your app have different entitlements, one version may not be able to ‘see’ items created by the other. Imagine you have an app with an App ID of SKMME9E2Y8.com.example.waffle-varnisher. Version 1 of your app is signed with the keychain-access-groups entitlement set to [ SKMME9E2Y8.groupA, SKMME9E2Y8.groupB ]. That makes its keychain access group list [ SKMME9E2Y8.groupA, SKMME9E2Y8.groupB, SKMME9E2Y8.com.example.waffle-varnisher ]. If this app creates a new keychain item without specifying kSecAttrAccessGroup, the system places the item into SKMME9E2Y8.groupA. If version 2 of your app removes SKMME9E2Y8.groupA from the keychain-access-groups, it’ll no longer be able to see the keychain items created by version 1. You’ll also see this problem if you change your App ID prefix, as described in App ID Prefix Change and Keychain Access. IMPORTANT When checking for this problem, don’t rely on your .entitlements file. There are many steps between it and your app’s actual entitlements. Rather, run codesign to dump the entitlements of your built app: % codesign -d --entitlements - /path/to/your.app Lost Keychain Items, Redux Another common cause of lost keychain items is confusion about query dictionaries, something discussed in detail in this post and SecItem: Fundamentals. If SecItemCopyMatching isn’t returning the expected item, add some test code to get all the items and their attributes. For example, to dump all the generic password items, run code like this: func dumpGenericPasswords() throws { let itemDicts = try secCall { SecItemCopyMatching([ kSecClass: kSecClassGenericPassword, kSecMatchLimit: kSecMatchLimitAll, kSecReturnAttributes: true, ] as NSDictionary, $0) } as! [[String: Any]] print(itemDicts) } Then compare each item’s attributes against the attributes you’re looking for to see why there was no match. Data Protection and Background Execution Keychain items are subject to data protection. Specifically, an item may or may not be accessible depending on whether specific key material is available. For an in-depth discussion of how this works, see Apple Platform Security. Note This section focuses on iOS but you’ll see similar effects on all Apple platforms. On macOS specifically, the contents of this section only apply to the data protection keychain. The keychain supports three data protection levels: kSecAttrAccessibleWhenUnlocked kSecAttrAccessibleAfterFirstUnlock kSecAttrAccessibleAlways Note There are additional data protection levels, all with the ThisDeviceOnly suffix. Understanding those is not necessary to understanding this pitfall. Each data protection level describes the lifetime of the key material needed to work with items protected in that way. Specifically: The key material needed to work with a kSecAttrAccessibleWhenUnlocked item comes and goes as the user locks and unlocks their device. The key material needed to work with a kSecAttrAccessibleAfterFirstUnlock item becomes available when the device is first unlocked and remains available until the device restarts. The default data protection level is kSecAttrAccessibleWhenUnlocked. If you add an item to the keychain and don’t specify a data protection level, this is what you get [1]. To specify a data protection level when you add an item to the keychain, apply the kSecAttrAccessible attribute. Alternatively, embed the access level within a SecAccessControl object and apply that using the kSecAttrAccessControl attribute. IMPORTANT It’s best practice to set these attributes when you add the item and then never update them. See Add-only Attributes, above, for more on that. If you perform an operation whose data protection is incompatible with the currently available key material, that operation fails with errSecInteractionNotAllowed [2]. There are four fundamental keychain operations, discussed in the SecItem: Fundamentals, and each interacts with data protection in a different way: Copy — If you attempt to access a keychain item whose key material is unavailable, SecItemCopyMatching fails with errSecInteractionNotAllowed. This is an obvious result; the whole point of data protection is to enforce this security policy. Add — If you attempt to add a keychain item whose key material is unavailable, SecItemAdd fails with errSecInteractionNotAllowed. This is less obvious. The reason why this fails is that the system needs the key material to protect (by encryption) the keychain item, and it can’t do that if if that key material isn’t available. Update — If you attempt to update a keychain item whose key material is unavailable, SecItemUpdate fails with errSecInteractionNotAllowed. This result is an obvious consequence of the previous result. Delete — Deleting a keychain item, using SecItemDelete, doesn’t require its key material, and thus a delete will succeed when the item is otherwise unavailable. That last point is a significant pitfall. I regularly see keychain code like this: Read an item holding a critical user credential. If that works, use that credential. If it fails, delete the item and start from a ‘factory reset’ state. The problem is that, if your code ends up running in the background unexpectedly, step 1 fails with errSecInteractionNotAllowed and you turn around and delete the user’s credential. Ouch! Note Even if you didn’t write this code, you might have inherited it from a keychain wrapper library. See Think Before Wrapping, below. There are two paths forward here: If you don’t expect this code to work in the background, check for the errSecInteractionNotAllowed error and non-destructively cancel the operation in that case. If you expect this code to be running in the background, switch to a different data protection level. WARNING For the second path, the most obvious fix is to move from kSecAttrAccessibleWhenUnlocked to kSecAttrAccessibleAfterFirstUnlock. However, this is not a panacea. It’s possible that your app might end up running before first unlock [3]. So, if you choose the second path, you must also make sure to follow the advice for the first path. You can determine whether the device is unlocked using the isProtectedDataAvailable property and its associated notifications. However, it’s best not to use this property as part of your core code, because such preflighting is fundamentally racy. Rather, perform the operation and handle the error gracefully. It might make sense to use isProtectedDataAvailable property as part of debugging, logging, and diagnostic code. [1] For file data protection there’s an entitlement (com.apple.developer.default-data-protection) that controls the default data protection level. There’s no such entitlement for the keychain. That’s actually a good thing! In my experience the file data protection entitlement is an ongoing source of grief. See this thread if you’re curious. [2] This might seem like an odd error but it’s actually pretty reasonable: The operation needs some key material that’s currently unavailable. Only a user action can provide that key material. But the data protection keychain will never prompt the user to unlock their device. Thus you get an error instead. [3] iOS generally avoids running third-party code before first unlock, but there are circumstances where that can happen. The obvious legitimate example of this is a VoIP app, where the user expects their phone to ring even if they haven’t unlocked it since the last restart. There are also other less legitimate examples of this, including historical bugs that caused apps to launch in the background before first unlock. Best Practices With the pitfalls out of the way, let’s talk about best practices. Less Painful Dictionaries I look at a lot of keychain code and it’s amazing how much of it is way more painful than it needs to be. The biggest offender here is the dictionaries. Here are two tips to minimise the pain. First, don’t use CFDictionary. It’s seriously ugly. While the SecItem API is defined in terms of CFDictionary, you don’t have to work with CFDictionary directly. Rather, use NSDictionary and take advantage of the toll-free bridge. For example, consider this CFDictionary code: CFTypeRef keys[4] = { kSecClass, kSecAttrService, kSecMatchLimit, kSecReturnAttributes, }; static const int kTen = 10; CFNumberRef ten = CFNumberCreate(NULL, kCFNumberIntType, &kTen); CFAutorelease(ten); CFTypeRef values[4] = { kSecClassGenericPassword, CFSTR("AYS"), ten, kCFBooleanTrue, }; CFDictionaryRef query = CFDictionaryCreate( NULL, keys, values, 4, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks ); Note This might seem rather extreme but I’ve literally seen code like this, and worse, while helping developers. Contrast this to the equivalent NSDictionary code: NSDictionary * query = @{ (__bridge NSString *) kSecClass: (__bridge NSString *) kSecClassGenericPassword, (__bridge NSString *) kSecAttrService: @"AYS", (__bridge NSString *) kSecMatchLimit: @10, (__bridge NSString *) kSecReturnAttributes: @YES, }; Wow, that’s so much better. Second, if you’re working in Swift, take advantage of its awesome ability to create NSDictionary values from Swift dictionary literals. Here’s the equivalent code in Swift: let query = [ kSecClass: kSecClassGenericPassword, kSecAttrService: "AYS", kSecMatchLimit: 10, kSecReturnAttributes: true, ] as NSDictionary Nice! Avoid Reusing Dictionaries I regularly see folks reuse dictionaries for different SecItem calls. For example, they might have code like this: var copyResult: CFTypeRef? = nil let dict = [ kSecClass: kSecClassGenericPassword, kSecAttrService: "AYS", kSecAttrAccount: "mrgumby", kSecReturnData: true, ] as NSMutableDictionary var err = SecItemCopyMatching(dict, &copyResult) if err == errSecItemNotFound { dict[kSecValueData] = Data("opendoor".utf8) err = SecItemAdd(dict, nil) } This specific example will work, but it’s easy to spot the logic error. kSecReturnData is a return type property and it makes no sense to pass it to a SecItemAdd call whose second parameter is nil. I’m not sure why folks do this. I think it’s because they think that constructing dictionaries is expensive. Regardless, this pattern can lead to all sorts of weird problems. For example, it’s the leading cause of the issue described in the Queries and the Uniqueness Constraints section, above. My advice is that you use a new dictionary for each call. That prevents state from one call accidentally leaking into a subsequent call. For example, I’d rewrite the above as: var copyResult: CFTypeRef? = nil let query = [ kSecClass: kSecClassGenericPassword, kSecAttrService: "AYS", kSecAttrAccount: "mrgumby", kSecReturnData: true, ] as NSMutableDictionary var err = SecItemCopyMatching(query, &copyResult) if err == errSecItemNotFound { let add = [ kSecClass: kSecClassGenericPassword, kSecAttrService: "AYS", kSecAttrAccount: "mrgumby", kSecValueData: Data("opendoor".utf8), ] as NSMutableDictionary err = SecItemAdd(add, nil) } It’s a bit longer, but it’s much easier to track the flow. And if you want to eliminate the repetition, use a helper function: func makeDict() -> NSMutableDictionary { [ kSecClass: kSecClassGenericPassword, kSecAttrService: "AYS", kSecAttrAccount: "mrgumby", ] as NSMutableDictionary } var copyResult: CFTypeRef? = nil let query = makeDict() query[kSecReturnData] = true var err = SecItemCopyMatching(query, &copyResult) if err == errSecItemNotFound { let add = makeDict() query[kSecValueData] = Data("opendoor".utf8) err = SecItemAdd(add, nil) } Think Before Wrapping A lot of folks look at the SecItem API and immediately reach for a wrapper library. A keychain wrapper library might seem like a good idea but there are some serious downsides: It adds another dependency to your project. Different subsystems within your project may use different wrappers. The wrapper can obscure the underlying API. Indeed, its entire raison d’être is to obscure the underlying API. This is problematic if things go wrong. I regularly talk to folks with hard-to-debug keychain problems and the conversation goes something like this: Quinn: What attributes do you use in the query dictionary? J R Developer: What’s a query dictionary? Quinn: OK, so what error are you getting back? J R Developer: It throws WrapperKeychainFailedError. That’s not helpful )-: If you do use a wrapper, make sure it has diagnostic support that includes the values passed to and from the SecItem API. Also make sure that, when it fails, it returns an error that includes the underlying keychain error code. These benefits will be particularly useful if you encounter a keychain problem that only shows up in the field. Wrappers must choose whether to be general or specific. A general wrapper may be harder to understand than the equivalent SecItem calls, and it’ll certainly contain a lot of complex code. On the other hand, a specific wrapper may have a model of the keychain that doesn’t align with your requirements. I recommend that you think twice before using a keychain wrapper. Personally I find the SecItem API relatively easy to call, assuming that: I use the techniques shown in Less Painful Dictionaries, above, to avoid having to deal with CFDictionary. I use my secCall(…) helpers to simplify error handling. For the code, see Calling Security Framework from Swift. If you’re not prepared to take the SecItem API neat, consider writing your own wrapper, one that’s tightly focused on the requirements of your project. For example, in my VPN apps I use the wrapper from this post, which does exactly what I need in about 100 lines of code. Prefer to Update Of the four SecItem functions, SecItemUpdate is the most neglected. Rather than calling SecItemUpdate I regularly see folks delete and then re-add the item. This is a shame because SecItemUpdate has some important benefits: It preserves persistent references. If you delete and then re-add the item, you get a new item with a new persistent reference. It’s well aligned with the fundamental database nature of the keychain. It forces you to think about which attributes uniquely identify your item and which items can be updated without changing the item’s identity. For a cool example of its power, check out Transfer Items Between Keychain Access Groups, below. Understand These Key Attributes Key items have a number of attributes that are similarly named, and it’s important to keep them straight. I created a cheat sheet for this, namely, SecItem attributes for keys. You wouldn’t believe how often I consult this! Investigating Complex Attributes Some attributes have values where the format is not obvious. For example, the kSecAttrIssuer attributed is documented as: The corresponding value is of type CFData and contains the X.500 issuer name of a certificate. What exactly does that mean? If I want to search the keychain for all certificates issued by a specific certificate authority, what value should I supply? One way to figure this out is to add a certificate to the keychain, read the attributes back, and then dump the kSecAttrIssuer value. For example: let cert: SecCertificate = … let attrs = try secCall { SecItemAdd([ kSecValueRef: cert, kSecReturnAttributes: true, ] as NSDictionary, $0) } as! [String: Any] let issuer = attrs[kSecAttrIssuer as String] as! NSData print((issuer as NSData).debugDescription) // prints: <3110300e 06035504 030c074d 6f757365 4341310b 30090603 55040613 024742> Those bytes represent the contents of a X.509 Name ASN.1 structure with DER encoding. This is without the outer SEQUENCE element, so if you dump it as ASN.1 you’ll get a nice dump of the first SET and then a warning about extra stuff at the end of the file: % xxd issuer.asn1 00000000: 3110 300e 0603 5504 030c 074d 6f75 7365 1.0...U....Mouse 00000010: 4341 310b 3009 0603 5504 0613 0247 42 CA1.0...U....GB % dumpasn1 -p issuer.asn1 SET { SEQUENCE { OBJECT IDENTIFIER commonName (2 5 4 3) UTF8String 'MouseCA' } } Warning: Further data follows ASN.1 data at position 18. Note For details on the Name structure, see section 4.1.2.4 of RFC 5280. Amusingly, if you run the same test against the file-based keychain you’ll… crash. OK, that’s not amusing. It turns out that the code above doesn’t work when targeting the file-based keychain because SecItemAdd doesn’t return a dictionary but rather an array of dictionaries (r. 21111543). Once you get past that, however, you’ll see it print: <301f3110 300e0603 5504030c 074d6f75 73654341 310b3009 06035504 06130247 42> Which is different! Dumping it as ASN.1 shows that it’s the full Name structure, including the outer SEQUENCE element: % xxd issuer-file-based.asn1 00000000: 301f 3110 300e 0603 5504 030c 074d 6f75 0.1.0...U....Mou 00000010: 7365 4341 310b 3009 0603 5504 0613 0247 seCA1.0...U....G 00000020: 42 B % dumpasn1 -p issuer-file-based.asn1 SEQUENCE { SET { SEQUENCE { OBJECT IDENTIFIER commonName (2 5 4 3) UTF8String 'MouseCA' } } SET { SEQUENCE { OBJECT IDENTIFIER countryName (2 5 4 6) PrintableString 'GB' } } } This difference in behaviour between the data protection and file-based keychains is a known bug (r. 26391756) but in this case it’s handy because the file-based keychain behaviour makes it easier to understand the data protection keychain behaviour. Import, Then Add It’s possible to import data directly into the keychain. For example, you might use this code to add a certificate: let certData: Data = … try secCall { SecItemAdd([ kSecClass: kSecClassCertificate, kSecValueData: certData, ] as NSDictionary, nil) } However, it’s better to import the data and then add the resulting credential reference. For example: let certData: Data = … let cert = try secCall { SecCertificateCreateWithData(nil, certData as NSData) } try secCall { SecItemAdd([ kSecValueRef: cert, ] as NSDictionary, nil) } There are two advantages to this: If you get an error, you know whether the problem was with the import step or the add step. It ensures that the resulting keychain item has the correct attributes. This is especially important for keys. These can be packaged in a wide range of formats, so it’s vital to know whether you’re interpreting the key data correctly. I see a lot of code that adds key data directly to the keychain. That’s understandable because, back in the day, this was the only way to import a key on iOS. Fortunately, that’s not been the case since the introduction of SecKeyCreateWithData in iOS 10 and aligned releases. For more information about importing keys, see Importing Cryptographic Keys. App Groups on the Mac Sharing access to keychain items among a collection of apps explains that three entitlements determine your keychain access: keychain-access-groups application-identifier (com.apple.application-identifier on macOS) com.apple.security.application-groups In the discussion of the last item says: You can use app group names as keychain access group names, without adding them to the Keychain access groups entitlement. That’s true, but it’s also potentially misleading. This affordance works all the time on iOS and its child platforms. But on the Mac it only works if your entitlements are validated by a provisioning profile. For more on that topic, see App Groups: macOS vs iOS: Working Towards Harmony. Transfer Items Between Keychain Access Groups In some cases you might want to move a bunch of keychain items from one app group to another, for example, when preparing for an App ID prefix change. This is easier than you might first think. For example, to move all the generic password items for a particular service between oldGroup and newGroup, run this code: try secCall { SecItemUpdate([ kSecClass: kSecClassGenericPassword, kSecUseDataProtectionKeychain: true, kSecAttrAccessGroup: oldGroup, kSecAttrService: "MyService", ] as NSDictionary, [ kSecAttrAccessGroup: newGroup, ] as NSDictionary) } This snippet highlights both the power and the subtlety of the SecItem API. The first parameter to SecItemUpdate is a pure query dictionary. It selects all the generic password items for MyService that are in the old keychain access group. In contrast, the second parameter is an update dictionary, which in this case just changes a single attribute. See SecItem: Fundamentals for a deeper explanation of these concepts. This call is atomic from your perspective [1]. The call will either fail or all the selected items will move as one. IMPORTANT Bulk operations like this are risky. That’s not because the keychain item will do the wrong thing, but rather because you have to be very careful what you ask for. If, for example, your query dictionary matches more than you intended, you might end up moving items unexpectedly. Be careful when crafting this code, and test it thoroughly. [1] It may even be atomic in a wider sense, given that the keychain is currently implemented as an SQLite database. Revision History 2026-04-02 Added the Transfer Items Between Keychain Access Groups section. Updated the App Groups on the Mac section to account for recent changes to app groups on the Mac. Made other minor editorial changes. 2025-06-29 Added the Data Protection and Background Execution section. Made other minor editorial changes. 2025-02-03 Added another specific example to the Careful With that Shim, Mac Developer section. 2025-01-29 Added somes specific examples to the Careful With that Shim, Mac Developer section. 2025-01-23 Added the Import, Then Add section. 2024-08-29 Added a discussion of identity formation to the Digital Identities Aren’t Real section. 2024-04-11 Added the App Groups on the Mac section. 2023-10-25 Added the Lost Keychain Items and Lost Keychain Items, Redux sections. 2023-09-22 Made minor editorial changes. 2023-09-12 Fixed various bugs in the revision history. Added the Erroneous Attributes section. 2023-02-22 Fixed the link to the VPNKeychain post. Corrected the name of the Context Matters section. Added the Investigating Complex Attributes section. 2023-01-28 First posted.
Replies
0
Boosts
0
Views
4k
Activity
1w
why prepareInterfaceToProvideCredential does call
we develop extension "Autofill Credential Provider" function for passkey. 1.first step registe passkey 2.second step authenticate with passkey step 1 &amp; step 2 has finished and run success with provideCredentialWithoutUserInteraction. But we want to prepare our interface for use to input password and select passkey what the want. however the func prepareInterfaceToProvideCredential in ASCredentialProviderViewController does call? what i missed? how can i do it?
Replies
0
Boosts
0
Views
178
Activity
Jul ’25
APP ID's indentifier not updating
When implementing Sign In with Apple I created an App ID and a Service ID for my app. I didn't configure the Server-to-Server Notification URL properly there and token revocation didn't work. Later on I updated the url config and the name of the identifiers. However, when I Sign in with Apple in my app I still see the old identifier name in my iPhone Settings->Apple Account->Sign in with Apple. I would assume that if the name doesn't update, the configuration doesn't update either. I'm using automatic Xcode signing, I have deleted all the profiles locally, cleaned project, bumped versions, waited for a week, nothing worked. Token revocation for account deletion doesn't work properly I would assume because of the initial misconfiguration. I want to mention that this is working fine for my development build (another bundleID, AppID, ServiceID) What am I missing here?
Replies
0
Boosts
0
Views
134
Activity
Jun ’25
Privacy & Security Resources
General: Forums topic: Privacy & Security Privacy Resources Security Resources Share and Enjoy — Quinn “The Eskimo!” @ Developer Technical Support @ Apple let myEmail = "eskimo" + "1" + "@" + "apple.com"
Replies
0
Boosts
0
Views
580
Activity
Jul ’25
Authentication using MSAL library in offline mode
Hi. We are trying to get the access token before calling any API. The app can go in bad network areas but the token acquisition keeps happening for the network call. The devices are managed devices which means it has some policies installed. We are using MSAL lib for the authentication and we are investigating from that angle too but the below error seems to be coming from apple authentication which needs our attention. ========================================== LaunchServices: store (null) or url (null) was nil: Error Domain=NSOSStatusErrorDomain Code=-54 "process may not map database" UserInfo={NSDebugDescription=process may not map database, _LSLine=68, _LSFunction=_LSServer_GetServerStoreForConnectionWithCompletionHandler} Attempt to map database failed: permission was denied. This attempt will not be retried. Failed to initialize client context with error Error Domain=NSOSStatusErrorDomain Code=-54 "process may not map database" UserInfo={NSDebugDescription=process may not map database, _LSLine=68, _LSFunction=_LSServer_GetServerStoreForConnectionWithCompletionHandler} Failed to get application extension record: Error Domain=NSOSStatusErrorDomain Code=-54 "(null)" ASAuthorizationController credential request failed with error: Error Domain=com.apple.AuthenticationServices.AuthorizationError Code=1003 "(null)" ========================================== This happens mostly when we switches the network or keep the device in no or low network area. This comes sometimes when app goes in background too. Just trying to give as much as information I could. Any lead would be highly appreciated. Thank you
Replies
0
Boosts
0
Views
146
Activity
Apr ’25
Developing Platform SSO extension
Hi, I am developing a Platform SSO in order to have integrated with our IdP, which I am also adapting to provide the right endpoints for Platform SSO. I have a few questions about the implementation: does the client-request-id need to be present on all requests? Is it unique per request, or requests that are bound together like those requesting a nonce and those who will use that nonce should use the same client-request-id? I am not sure how the loginManager.presentRegistrationViewController works. I'd like to get the user to authenticate to my IdP before device registration. So I am not sure if I should provide my own Webview or something similar or if this method should do something for me; My idea is to request user authentication once, save the state when performing device registration, so that I avoid asking for user authentication twice when performing user registration. Is this the right way to do it? How does platform SSO handles tokens? If one application of my IdP requests the authentication on a common OIDC/OAuth2 flow, should I perform some sort of token exchange? How about SAML? Platform SSO seems to be token-centric, but how does one handle SAML flows? Is it by using WebView as well?
Replies
0
Boosts
0
Views
158
Activity
Nov ’25
Production-Grade Implementation Guidance: DCError Matrices, Retry Strategies, and Simulator Testing for App Attest APIs
Hi there, We're implementing Apple's DeviceCheck App Attest for production iOS authentication. The public documentation defines DCError cases but doesn't specify which errors are expected per API method or recommend retry/remediation strategies. We need Apple's guidance to implement robust, production-aligned error handling before rollout. 1. Error Surface per API Method Question: Can you confirm the complete, officially expected set of DCError values for each method? We understand the following errors are possible across App Attest APIs: invalidKey invalidInput featureUnsupported serverUnavailable unknownSystemFailure Specifically, please confirm which errors can occur for: DCAppAttestService.generateKey() DCAppAttestService.attestKey(_:clientData:) DCAppAttestService.generateAssertion(keyID:clientData:) Are there any additional undocumented or edge-case errors we should handle? 2. Retry Strategy & Remediation Matrix Question: For each API method and error code, please help us with proposal around which errorCode is retriable, whats the remediation pre retry, retry cap and backoff strategy: Kindly also help with errors that are not covered here: Specific sub-questions: invalidKey handling: When this error occurs: Should the app delete the key and call generateKey again? Or should it fail the entire flow? serverUnavailable handling: Should we retry immediately, or wait before retrying? Is exponential backoff recommended? What's the recommended max retry count? Backoff strategy: Which errors (if any) qualify for exponential backoff? Recommended base delay, max delay, and jitter approach? When should we give up and fail the request? unknownSystemFailure: Is this retriable or should we fail? Any known causes or mitigations? 3. Simulator Testing Questions: Simulator API behavior: Can App Attest APIs be called normally on iOS Simulator? If not, is there a way to simulate for testing. Do they complete successfully with simulated attestations, or do they fail? Thanks, Nirekshitha
Replies
0
Boosts
0
Views
230
Activity
1w
Auto-fill of name and email address with Sign in with Apple doesn't work in reviews
We implemented a feature to receive name and email address after Sign in with Apple, and it works perfectly in our testing. However, during the app review, they says it doesn't work, and the app is rejected. Why does it work differently?
Replies
0
Boosts
0
Views
348
Activity
Jul ’25
Sign in with Apple Web: invalid_client on token exchange with real authorization code, but invalid_grant with dummy code
We are integrating Sign in with Apple for our web application and have been stuck on an invalid_client error during the token exchange step. The Problem The authorization step works fine — the user authenticates on Apple's page and a valid authorization code is returned to our callback URL. However, when we exchange that code at https://appleid.apple.com/auth/token, it returns: {"error": "invalid_client"} The Puzzling Part When we send a dummy/expired authorization code with the exact same client_id and client_secret, Apple returns: {"error": "invalid_grant", "error_description": "The code has expired or has been revoked."} This confirms that our client credentials (client_id + client_secret JWT) are valid and accepted by Apple. The invalid_client error only occurs when a real, freshly-issued authorization code is used. Configuration Service ID configured with Sign in with Apple enabled Primary App ID with Sign in with Apple capability enabled Domain verified, Return URL registered Key created with Sign in with Apple enabled, linked to the correct Primary App ID Client Secret JWT Generated per Apple's documentation: Header: alg: ES256, kid set to our Key ID Claims: iss: Team ID iat: current timestamp exp: iat + 6 months (within Apple's limit) aud: https://appleid.apple.com sub: Service ID (matches the client_id used in authorization) Signed with: the .p8 private key associated with the Key Token Exchange Request POST https://appleid.apple.com/auth/tokenContent-Type: application/x-www-form-urlencodedclient_id=client_secret=code=grant_type=authorization_coderedirect_uri= What We've Tried Standalone test endpoint — built a minimal endpoint (no framework) that does the token exchange via server-side curl. Same invalid_client. Multiple Service IDs — created and tried 3 different Service IDs. All produce the same error with real codes. Multiple Keys — tried 2 different keys. Same error. Verified redirect_uri matches exactly between the authorization request and token request. Verified client_id matches exactly between the authorization URL and token request. Used client_secret_post (credentials in body, not Basic auth header). Freshness — code is used immediately upon receipt (within seconds), well before the 5-minute expiry. Filed a Developer Support case — was directed to Forums. Summary Scenario code Result Dummy/expired code abc123 invalid_grant (credentials accepted) Real fresh code from Apple callback invalid_client This pattern suggests something goes wrong specifically when Apple validates the authorization code against the client — even though the client credentials themselves are accepted in isolation. Has anyone encountered this behavior? Is there a known configuration issue that could cause invalid_client only with valid authorization codes? Any guidance would be greatly appreciated.
Replies
0
Boosts
0
Views
117
Activity
2w
Unexpectedly invalidated Biometrics in iOS 18.3.2 or later
There is a sudden surge of users in our apps with invalidated biometrics. Even though the issue is being handled correctly and the user has another way to login, some of the users forgot their passwords and they can not login. Is there any known issue with Biometrics in iOS 18.3.2 or later? There is a (possible) related discussion here: https://discussions.apple.com/thread/256011565
Replies
1
Boosts
0
Views
122
Activity
Apr ’25
Apple Sign-In: "invalid-credential" error despite correct configuration - Firebase Auth iOS
Problem Summary I'm experiencing a persistent invalid-credential error with Apple Sign-In on iOS despite having verified every aspect of the configuration over the past 6 months. The error occurs at the Firebase Authentication level after successfully receiving credentials from Apple. Error Message: Firebase auth error: invalid-credential - Invalid OAuth response from apple.com. Environment Platform: iOS (Flutter app) Firebase Auth: v5.7.0 Sign in with Apple: v6.1.2 Xcode: Latest version with capability enabled iOS Target: 13.0+ Bundle ID: com.harmonics.orakl What Actually Happens ✅ Apple Sign-In popup appears ✅ User can authenticate with Apple ID ✅ Apple returns credentials with identityToken ❌ Firebase rejects with invalid-credential error The error occurs at Firebase level, not Apple level. What I've Tried Created a brand new Apple Key (previous key was 6 months old) Tested with both App ID and Service ID in Firebase Completely reinstalled CocoaPods dependencies Verified nonce handling is correct (hashed to Apple, raw to Firebase) Activated Firebase Hosting and attempted to deploy .well-known file Checked Cloud Logging (no detailed error messages found) Disabled and re-enabled Apple Sign-In provider in Firebase Verified Return URL matches exactly Waited and retried multiple times over 6 months Questions Is the .well-known/apple-developer-domain-association.txt file required? If yes, how should it be generated? Firebase Hosting doesn't auto-generate it. Could there be a server-side caching/blacklist issue with my domain or Service ID after multiple failed attempts? Should the Apple Key be linked to the Service ID instead of the App ID? The key shows as linked to Z3NNDZVWMZ.com.harmonics.orakl (the App ID). Is there any way to get more detailed error logs from Firebase about why it's rejecting the Apple OAuth response? Could using a custom domain instead of .firebaseapp.com resolve the issue? Additional Context Google Sign-In works perfectly on the same app The configuration has been reviewed by multiple developers Error persists across different devices and iOS versions No errors in Xcode console except the Firebase rejection Any help would be greatly appreciated. I've exhausted all standard troubleshooting steps and documentation. Project Details: Bundle ID: com.harmonics.orakl Firebase Project: harmonics-app Team ID: Z3N....... code : // 1. Generate raw nonce final String rawNonce = _generateRandomNonce(); // 2. Hash with SHA-256 final String hashedNonce = _sha256Hash(rawNonce); // 3. Send HASHED nonce to Apple ✅ final appleCredential = await SignInWithApple.getAppleIDCredential( scopes: [AppleIDAuthorizationScopes.email, AppleIDAuthorizationScopes.fullName], nonce: hashedNonce, // Correct: hashed nonce to Apple ); // 4. Create Firebase credential with RAW nonce ✅ final oauthCredential = OAuthProvider("apple.com").credential( idToken: appleCredential.identityToken!, rawNonce: rawNonce, // Correct: raw nonce to Firebase ); // 5. Sign in with Firebase - ERROR OCCURS HERE ❌ await FirebaseAuth.instance.signInWithCredential(oauthCredential);
Replies
0
Boosts
0
Views
100
Activity
Oct ’25
iPhone + Safari + Passwords violates WebAuthn spec when pubKeyCredParams doesn't contain ES256
WebAuthn Level 3 § 6.3.2 Step 2 states the authenticator must : Check if at least one of the specified combinations of PublicKeyCredentialType and cryptographic parameters in credTypesAndPubKeyAlgs is supported. If not, return an error code equivalent to "NotSupportedError" and terminate the operation. On my iPhone 15 Pro Max running iOS 18.5, Safari + Passwords does not exhibit this behavior; instead an error is not reported and an ES256 credential is created when an RP passes a non-empty sequence that does not contain {"type":"public-key","alg":-7} (e.g., [{"type":"public-key","alg":-8}]). When I use Chromium 138.0.7204.92 on my laptop running Arch Linux in conjunction with the Passwords app (connected via the "hybrid" protocol), a credential is not created and instead an error is reported per the spec.
Replies
3
Boosts
0
Views
534
Activity
Jul ’25
Clarity App Attestation Errors
I'm currently reviewing the various DCError cases defined in Apple’s DeviceCheck framework (reference: https://developer.apple.com/documentation/devicecheck/dcerror-swift.struct). To better understand how to handle these in production, I’m looking for a clear breakdown of: Which specific DCError values can occur during service.generateKey, service.attestKey, and service.generateAssertion The realworld scenarios or conditions that typically cause each error for each method. If anyone has insight on how these errors arise and what conditions trigger them, I’d appreciate your input.
Replies
1
Boosts
0
Views
285
Activity
2w
Security Resources
General: Forums topic: Privacy & Security Apple Platform Security support document Developer > Security Enabling enhanced security for your app documentation article Creating enhanced security helper extensions documentation article Security Audit Thoughts forums post Cryptography: Forums tags: Security, Apple CryptoKit Security framework documentation Apple CryptoKit framework documentation Common Crypto man pages — For the full list of pages, run: % man -k 3cc For more information about man pages, see Reading UNIX Manual Pages. On Cryptographic Key Formats forums post SecItem attributes for keys forums post CryptoCompatibility sample code Keychain: Forums tags: Security Security > Keychain Items documentation TN3137 On Mac keychain APIs and implementations SecItem Fundamentals forums post SecItem Pitfalls and Best Practices forums post Investigating hard-to-reproduce keychain problems forums post App ID Prefix Change and Keychain Access forums post Smart cards and other secure tokens: Forums tag: CryptoTokenKit CryptoTokenKit framework documentation Mac-specific resources: Forums tags: Security Foundation, Security Interface Security Foundation framework documentation Security Interface framework documentation BSD Privilege Escalation on macOS Related: Networking Resources — This covers high-level network security, including HTTPS and TLS. Network Extension Resources — This covers low-level network security, including VPN and content filters. Code Signing Resources Notarisation Resources Trusted Execution Resources — This includes Gatekeeper. App Sandbox Resources Share and Enjoy — Quinn “The Eskimo!” @ Developer Technical Support @ Apple let myEmail = "eskimo" + "1" + "@" + "apple.com"
Replies
0
Boosts
0
Views
3.8k
Activity
Nov ’25
App Attest server unreachable – DNS or firewall issue suspected
Hello, We are working on integrating app integrity verification into our service application, following Apple's App Attest and DeviceCheck guide. Our server issues a challenge to the client, which then sends the challenge, attestation, and keyId in CBOR format to Apple's App Attest server for verification. However, we are unable to reach both https://attest.apple.com and https://attest.development.apple.com due to network issues. These attempts have been made from both our internal corporate network and mobile hotspot environments. Despite adjusting DNS settings and other configurations, the issue persists. Are there alternative methods or solutions to address this problem? Any recommended network configurations or guidelines to successfully connect to Apple's App Attest servers would be greatly appreciated. Thank you.
Replies
0
Boosts
0
Views
191
Activity
May ’25
External website handling and ATT
Our proposed solution to identify an app user when opening a website operated by app developer is: Apps sends a request to backed with app users auth header Backend fetches a generated authenticated url from website backend, based on users auth header App opens it in browser The browser journey is self contained within domain of the business. Would this interaction require an ATT request given that the users identity cannot be tracked back to the app user ? Thanks
Replies
0
Boosts
0
Views
123
Activity
3w
DeviceCheck.generateToken, Error: com.apple.devicecheck.error 0
Dear Apple Developer Support Team, We are experiencing a recurring issue with the DeviceCheck API where the following error is being returned: com.apple.devicecheck.error 0 Upon analyzing our logs, we have noticed that this error occurs significantly more often when users are connected to Wi-Fi networks, compared to mobile networks. This leads us to suspect that there might be a relationship between Wi-Fi configuration and the DeviceCheck service’s ability to generate or validate tokens. We would like to know: Is this error code (0) known to be caused by specific types of network behavior or misconfigurations on Wi-Fi networks (e.g., DNS filtering, firewall restrictions, proxy servers)? Are there any recommended best practices for ensuring reliable DeviceCheck API communication over Wi-Fi networks? Additionally, could you please clarify what general conditions could trigger this com.apple.devicecheck.error 0? The lack of specific documentation makes debugging this issue difficult from our side. Any guidance or internal documentation on this error code and its potential causes would be greatly appreciated. IDE: Xcode 16.3 Looking forward to your support. Best regards,
Replies
2
Boosts
0
Views
167
Activity
May ’25
"access_denied" error during Sign in with Apple user migration (TN3159)
Hello, I am currently process of migrating an app from Team A to Team B and attempting to generate transfer identifiers using the migration endpoint: POST https://appleid.apple.com/auth/usermigrationinfo. Content-Type: application/x-www-form-urlencoded However, I am consistently receiving an { "error": "access_denied" } response. [Current Configuration] Team A (Source): Primary App ID: com.example.primary Grouped App IDs: com.example.service (Services ID for Web) com.example.app (App ID for iOS - The one being transferred) All identifiers are under the same App Group. Team B (Destination): New App ID and Key created. [Steps Taken] Created a Client Secret (JWT) using Team A's Key ID and Team ID. The sub (subject) in the JWT is set to the Primary App ID of Team A. Requesting with client_id (Primary App ID), client_secret (JWT), and user_token. [Questions] 1. App Group Impact: Does the fact that the App being transferred is a Grouped App ID (not the Primary) affect the usermigrationinfo request? Should I use the Primary App ID or the specific Grouped App ID as the client_id? 2. Ungrouping Safety: If I need to ungroup the App ID from the Primary App ID to resolve this: Will existing users still be able to sign in without issues? Is there any risk of changing the sub (user identifier) that the app receives from Apple? Will this cause any immediate service interruption for the live app? Any insights on why access_denied occurs in this Primary-Grouped configuration would be greatly appreciated.
Replies
1
Boosts
0
Views
145
Activity
Mar ’26
ScreenCapture permissions disappear and don't return
On Tahoe and earlier, ScreenCapture permissions can disappear and not return. Customers are having an issue with this disappearing and when our code executes CGRequestScreenCaptureAccess() nothing happens, the prompt does not appear. I can reproduce this by using the "-" button and removing the entry in the settings, then adding it back with the "+" button. CGPreflightScreenCaptureAccess() always returns the correct value but once the entry has been removed, CGRequestScreenCaptureAccess() requires a reboot before it will work again.
Replies
3
Boosts
0
Views
329
Activity
3w