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

Sign in with Apple Migration
We are in the process of transferring our existing Apple account to a new account. We read the documentation and we realized that "Sign in with Apple" users needs to be migrated via API. This API seems to require "sub" which we are not currently storing in our systems. So we are planning to "collect subs" over a period before we can start transfer. I want to make sure we are collecting correct data so I am looking for confirmation from Apple engineers on this. Should we get "sub" from "user" property in ASAuthorizationAppleIDCredential or by decoding identityToken and get "sub" from the token? I think both are same but wanted to double check. Thank you!
0
0
52
2w
When to use AppTrackingTransparency in macOS
The AppTrackingTransparency documentation says: You must use the AppTrackingTransparency framework if your app collects data about end users and shares it with other companies for purposes of tracking across apps and web sites. If I create a unique customer_id value to submit to the GIPHY Actions Register endpoint to track GIFs a user has clicked or sent, does that count as "tracking across apps and web sites"?
0
0
76
2w
Security.framework returns NULL from SecPolicyCreateSSL when the directory containing the running executable no longer exists.
https://feedbackassistant.apple.com/feedback/24553094 Security.framework returns NULL from SecPolicyCreateSSL when the directory containing the running executable no longer exists. That commonly happens with "go run" after the go command deletes its temporary build directory while a child process is still running. More details on how to reproduce: https://github.com/golang/go/issues/68557 Golang team rejected my workaround https://github.com/golang/go/pull/80471/changes so we're relying on the macOS team to allow this, unless of course there is a great reason not too. This is problematic when developing applications in golang, especially daemons.
1
0
114
2w
SecKeychainGetStatus returns varying errors after SecKeychainOpen succeeds on macOS 26.6.2
Hi, so far i've been using the following API Call Sequence in order to check the keychain status prior to using one of the certificates inside the keychain for signing or verifying remote tls connection. // Step 1: Open keychain SecKeychainRef keychain = nil; OSStatus status = SecKeychainOpen(keychainPath, &keychain); // Result: errSecSuccess (0) — always succeeds, returns valid ref // Step 2: Get keychain status SecKeychainStatus keychainStatus = 0; status = SecKeychainGetStatus(keychain, &keychainStatus); // Result: FAILS : see table below I observed SecKeychainGetStatus return errSecInternalError or errSecInvalidHandle │ Process │ Keychain │ Error │ Code │ │ GUI App │ login.keychain. | errSecInternalError │ -26276 │ │ GUI App │ login.keychain | errSecInvalidHandle │ -25308 │ │ GUI App │ /Library/Keychains/System.keychain | errSecInvalidHandle │ -25308 │ │ Daemon │ /Library/Keychains/System.keychain │ errSecInvalidHandle │ -25308 │ SecKeychainOpen always returns errSecSuccess with a non-null SecKeychainRef SecKeychainGetStatus fails immediately when called on that reference The error for login.keychain varies between calls (not deterministic) The error for System.keychain is consistently errSecInvalidHandle Issue is 100% reproducible. every keychain access attempt fails Started immediately after macOS 26.6.2 upgrade Do you know if there were any changes in 26.6.2 that could have caused this behavior? If this is expected, how should I address it? Thanks
1
0
175
2w
Does SecItemDelete guarantee immediate invalidation of an already-retained Secure Enclave SecKeyRef on macOS?
I’m looking for the documented Security.framework/macOS contract for this exact same-process sequence. An app creates a P-256 private key with SecKeyCreateRandomKey using: kSecAttrKeyType = kSecAttrKeyTypeECSECPrimeRandom kSecAttrKeySizeInBits = 256 kSecAttrTokenID = kSecAttrTokenIDSecureEnclave and, within kSecPrivateKeyAttrs: kSecAttrIsPermanent = true kSecAttrApplicationTag = <a unique tag> The app keeps the SecKeyRef returned directly by SecKeyCreateRandomKey alive and copies the corresponding public key with SecKeyCopyPublicKey. The app calls SecItemDelete with a query that uniquely matches that exact keychain item, and SecItemDelete returns errSecSuccess. Immediately after SecItemDelete returns, in the same process and without releasing or reacquiring the private-key reference, the app attempts a private-key operation using that same creation-returned SecKeyRef. Does errSecSuccess contractually guarantee that the already-retained SecKeyRef can no longer perform private-key operations from the point SecItemDelete returns? More concretely, is the following outcome forbidden by the API/platform contract: SecKeyCreateSignature succeeds after deletion, and the resulting signature verifies with the public key copied before deletion—that is, the public key from the same generated key pair? Or is invalidation of an already-issued SecKeyRef unspecified or otherwise not guaranteed by Security.framework? I’m asking only about the immediate post-return usability of the creation-returned SecKeyRef in this exact sequence. A clarification from Apple staff distinguishing the supported API/platform contract from current implementation behavior would be especially helpful.
1
0
535
2w
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 constructed 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). If you call SecItemUpdate with a query dictiony that searches for a key with a specific kSecAttrLabel value, it fails to find the item. OTOH, searching for a key via kSecAttrApplicationTag works (r. 186247975). If you call SecItemUpdate to update a key’s kSecAttrApplicationTag attribute, it actually updates the kSecAttrLabel attribute (r. 186247975). 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. Let’s walk through an example of this. Imagine you have an app with an App ID of SKMME9E2Y8.com.example.waffle-varnisher. Version 1 of your app does nothing fancy with the keychain. It uses neither keychain access groups nor app groups. Thus its keychain access group list consists of just the App ID, that is, [ SKMME9E2Y8.com.example.waffle-varnisher ]. When that version of your app creates a keychain item, the kSecAttrAccessGroup value will default to the only value available, SKMME9E2Y8.com.example.waffle-varnisher. In version 2 of your app you want to use keychain access groups, so you add the Keychain Sharing capability to your project and populate it with two values, SKMME9E2Y8.groupA and SKMME9E2Y8.groupB. If you take no other action, your app’s keychain access group list will be [ SKMME9E2Y8.groupA, SKMME9E2Y8.groupB, SKMME9E2Y8.com.example.waffle-varnisher ]. This changes the default value for new items to SKMME9E2Y8.groupA. This is an obvious pitfall. Version 1 of your app created new keychain items in SKMME9E2Y8.com.example.waffle-varnisher while version 2 creates them in SKMME9E2Y8.groupA. You now have different items in different groups, depending on which version the user first launched, and that’s a recipe for chaos. There are two common ways to avoid problems here: Migrate items from SKMME9E2Y8.com.example.waffle-varnisher to SKMME9E2Y8.groupA. See Transfer Items Between Keychain Access Groups, below. Add your App ID to the front of the Keychain Sharing list. This results in a keychain access group list of [ SKMME9E2Y8.com.example.waffle-varnisher, SKMME9E2Y8.groupA, SKMME9E2Y8.groupB, SKMME9E2Y8.com.example.waffle-varnisher ], which means that the default keychain access group doesn’t change. (The second instance of SKMME9E2Y8.com.example.waffle-varnisher in this list is redundant but doesn’t cause any complications.) So far so good. Now let’s say you took the first option and shipped version 2 of your app with SKMME9E2Y8.groupA as the default keychain access group. You want to update the app again, to version 3, and you’ve decided that SKMME9E2Y8.groupA no longer makes sense and you want to remove it, relying on SKMME9E2Y8.groupB instead. Doing that isn’t safe. If version 3 of your app has no access to SKMME9E2Y8.groupA, it won’t be able to access items created by version 2, even if the only goal is to migrate those items to SKMME9E2Y8.groupB. To make this work you have to: Move SKMME9E2Y8.groupA to the end of the Keychain Sharing list, so new items get created in SKMME9E2Y8.groupB. Add a migration from SKMME9E2Y8.groupA to SKMME9E2Y8.groupB. Update the migration from SKMME9E2Y8.com.example.waffle-varnisher to target SKMME9E2Y8.groupB instead of SKMME9E2Y8.groupA. That last point is necessary because a user might install version 1, skip version 2, and instead update straight to version 3. This is just an example, but the message is clear: Any change to your keychain access group list requires careful planning and testing. You’ll also see problems like this 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]] for itemDict in itemDicts { print("item:") let sortedKeysAndValues = itemDict.sorted(by: { $0.key < $1.key }) for (key, value) in sortedKeysAndValues { print(" \(key): \(value)") } } } 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! Starting from Scratch Sometimes it’s useful to be able to start from scratch. Imagine, for example, you’ve been rapidly iterating on some keychain code and you’re not sure whether your current code is compatible with items created by your earlier code. To simplify things, use SecItemDelete to delete all the existing items: _ = SecItemDelete([ kSecClass: kSecClassGenericPassword, kSecUseDataProtectionKeychain: true, ] as NSDictionary) WARNING This code is obviously dangerous. Read the discussion below to learn more. This deletes all generic password items that your app has access to. To delete items in a different keychain item class, change the value for the kSecClass attribute. This code uses kSecUseDataProtectionKeychain. On iOS there is only one keychain, so this is a no-op. On macOS it limits the effect to the data protection keychain. Without it, the call will delete items in file-based keychains as well. This is very dangerous because those items might belong to other apps, or the system. If you want to use this technique in a Mac product that uses the file-based keychain, don’t use this code. Rather, write code that carefully targets your app’s keychain items. Alternatively, avoid this code and instead delete the items using Keychain Access or the security tool. For more about keychains on the Mac, see TN3137 On Mac keychain APIs and implementations. I often invoke this code from my app’s debug UI. For example, in a Mac app I might have a Debug menu with a Reset Keychain menu item. I typically compile that code out of the release build. However, you might choose to leave it in your final product. For example, you might have a ‘secret’ way to enable the debug UI [1] so that you can use it to help users with problems. In that case, make sure your debug UI informs the user of the potential consequences of this action. If you’re working on a big app, it might have different subsystems that user the keychain in different ways. A debug action like this might make sense for your subsystem but not for all the others. In that case, coordinate this work with the owners of any other subsystems that use the keychain. [1] If your app ships on the App Store, make sure that App Review knows about your debug UI. 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. Command-Line Tools Access to the data protection keychain is mediated by various entitlements, as described in Sharing access to keychain items among a collection of apps. Those entitlements are restricted, that is, they must be authorised by a provisioning profile. This is fine for apps, app extensions, and system extensions, which are all bundled code; they exist within an app-like bundle structure. However, it’s problematic for command-line tools on the Mac, which are non-bundled executables. There’s no obvious way for such executables to include a provisioning profile (r. 125850707). For more about provisioning profiles, see TN3125 Inside Code Signing: Provisioning Profiles. For more about bundled code, see Creating distribution-signed code for macOS. If you’re creating a non-bundled executable for the Mac, first consider its execution context. If it runs as a launchd daemon, or outside of a user login context in some other way, it can’t use the data protection keychain. See TN3137 On Mac keychain APIs and implementations for more about that. If the executable is a command-line tool that’s typically run by the user, in Terminal or over SSH, it can use the data protection keychain. However: You have to embed the tool in an app-like wrapper. For more about that, see Signing a daemon with a restricted entitlement. If the tool is run via SSH, the user’s data protection keychain might be locked. To resolve this, the user must explicitly unlock their login keychain using the security tool. Note While the login keychain is a file-based keychain, unlocking it in this way also unlocks the data protection keychain. In-memory Plug-ins An in-memory plug-in is a native plug-in that’s loaded directly into the host process as a Mach-O bundle or shared library. For example, macOS screen savers are in-memory plug-ins. Note In-memory plug-ins are quite old school. Modern plug-ins are packaged as app extensions. If you’re created a Mac app that supports plug-ins, support app extension plug-ins by adopting ExtensionKit. From the keychain perspective, an in-memory plug-in is indistinguishable from the host app. This has both pros and cons: It can access all the keychain items that the host app has access to, in either the file-based or data protection keychains. It can’t access additional keychain items. For example, you can’t grant your in-memory plug-in access to a keychain access group that’s used by other apps that you create. I’ll leave it up to you to decide which of these is a pro and which is a con (-: Revision History 2026-08-31 Added another specific example to the Careful With that Shim, Mac Developer section. 2026-07-02 Clarified the terminology in the Keys Aren’t Stored in the Secure Enclave section. 2026-05-21 Enhanced the code snippet in the Lost Keychain Items, Redux section. 2026-04-27 Added the Command-Line Tools and In-memory Plug-ins sections. 2026-04-15 Significantly expanded the example in the Lost Keychain Items section. 2026-04-14 Added the Starting from Scratch section. 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
5.9k
2w
Sign in with Apple fails on ALL apps under our developer team with "Sign Up Not Completed" / "No se ha podido completar el registro."
Team ID: 2357A78TMR Affected App ID: me.manulorenzo.crossfittracker (App Store Connect app 6760609857) Reproduces identically across every app under this team, on a real device, personal (non-managed) Apple ID with 2FA enabled, persisting for multiple days across device reboots. Symptom matches the documented pattern in developer.apple.com/forums/thread/837986 and /thread/789222: native SIWA sheet renders, biometric/password succeeds, then "Sign Up Not Completed" — no credential ever returned. This matches Apple's own described root cause of an App ID/team never being fully provisioned server-side for Sign in with Apple (AKSQLError -6003 / GSA "Invalid client" / ASAuthorizationError 1001 with empty userInfo). Client-side configuration has been independently verified correct: provisioning profile carries com.apple.developer.applesignin entitlement, App ID capability APPLE_ID_AUTH is configured with PRIMARY_APP_CONSENT, Firebase's apple.com provider has the correct bundle ID registered. Requesting the server-side team registration fix described as the resolution in the linked threads.
2
0
96
2w
Apple sign in "Sign up not complete"
Hey there, im getting Sign up not complete! even with the official template https://developer.apple.com/documentation/authenticationservices/implementing-user-authentication-with-sign-in-with-apple my developer account is recently renewed, which it has expired for a bit i am using automatic managed signing I have a feel something went wrong my apple developer account id, and also this cause me getting invalid client when sign in with app on web app.
17
9
2.5k
2w
Private Email Relay rejects our sign-in emails with 554 5.7.1 [HM08] (FB24564316)
Following the pinned post "Gathering required information for troubleshooting Private Email Relay with Sign in with Apple" (thread 794191, which is locked, so I am posting here as instructed): I have filed FB24564316 with the required information. Since 28 July, every message from our app's sending domain to @privaterelay.appleid.com addresses is rejected by iCloud Mail with 554 5.7.1 [HM08], so users who signed up with Sign in with Apple and Hide My Email cannot receive their sign-in emails. Plain-text messages with no URL bounce as well, SPF/DKIM/DMARC pass on two independent sending systems, and external reputation checks are clean, so this looks like a false positive on the sending domain. The feedback contains the App ID (6761705463), Services ID, SMTP error codes and transaction IDs. iCloud postmaster case 21382576 and Developer Support case 20000139732014 did not resolve it. Happy to provide message headers. FB24564316
0
0
309
2w
Sign in with Apple fails with "Sign Up Not Completed"
Sign in with Apple has failed for every user of our app for four weeks. It fails inside Apple's own authorization sheet, before any credential reaches our code. App ID: com.chorestoreapp.app (SJRRC8Q327) Team ID: T4KD7949GL App Apple ID: 6797335710 — TestFlight builds 7 and 8, version 1.0.1 Symptom: The authorization sheet appears and behaves normally. When the fingerprint read is complete, it immediately fails with "Sign Up Not Completed". No credential is returned. When I cancel, it returns ERR_REQUEST_CANCELED, consistent with the sheet being dismissed by the user after being unable to sign in. Sign in with Apple works on the affected device and Apple ID. Downloading an unrelated App Store app and creating a new account with Sign in with Apple succeeded immediately, on the same iPad, with the same Apple ID, minutes before and after a ChoreStore failure. Before that test, Settings → [name] → Sign in with Apple was empty, so the account had never completed the flow — it is demonstrably capable of it now, for other apps. Reproduced with: two Apple IDs, two physical devices, both "Share My Email" and "Hide My Email", Wi-Fi and cellular, and two builds on freshly regenerated credentials. Verified on our side: The signed binary contains com.apple.developer.applesignin = ["Default"] The embedded provisioning profile includes the same entitlement App ID capability, read back from the App Store Connect API: APPLE_ID_AUTH with [{"key":"APPLE_ID_AUTH_APP_CONSENT","options":[{"key":"PRIMARY_APP_CONSENT"}]}] Provisioning profile IOS_APP_STORE, state ACTIVE, expires 2027-08-20 Distribution certificate valid to 2027-08-20 Exactly one bundle ID in the account; no grouped App ID The client call is the standard one, with no nonce or custom parameters: signInAsync({ requestedScopes: [FULL_NAME, EMAIL] }) The distribution certificate and App ID survived an Individual → Organization conversion unchanged (re-verified after the conversion) Why I'm posting rather than filing a code-level request. The request form requires a focused sample Xcode project. A sample cannot demonstrate this: anyone who opens it must sign it with their own team, which changes the App ID — the one variable that is failing. The fault appears to be server-side provisioning for this specific App ID, which no sample can carry. Question: Given the client configuration verifies correct and Sign in with Apple works on the same device and Apple ID for other apps, what would cause the authorization sheet itself to fail for a single App ID, and how can that be investigated or reset? Happy to provide a sysdiagnose, timestamps of specific failed attempts, a screen recording showing the working and failing cases minutes apart, or a TestFlight build.
0
0
129
2w
Sign in with Apple: native Sign Up Not Completed + web invalid_client — FB24556583
Feedback Assistant: FB24556583 Please inspect Sign in with Apple server-side registration for Team LQR9X2NWFT / App ID com.kanca.kanca. This is not a client-code or JWT issue. It is blocking App Store review (Guideline 2.1(a)). NATIVE Bundle ID: com.kanca.kanca App Store id: 6804206744 ASAuthorizationController / Flutter sign_in_with_apple. System sheet opens (Hide My Email), Face ID succeeds, then Apple's sheet shows Sign Up Not Completed. No identity token reaches the app. The app does not appear under Settings > Sign in with Apple. Reproduced for days. App Review reproduced it on iPhone 17 Pro Max iOS 26.6 and iPad Air 11-inch M3 iPadOS 26.6. App ID has Sign in with Apple enabled as a primary App ID. Capability re-saved; no change. Same Apple ID completes Sign in with Apple in other teams' apps on the same device. WEB Services ID: com.kanca.kanca.web GET https://appleid.apple.com/auth/authorize?client_id=com.kanca.kanca.web returns invalid_client before any login form (also via curl; no JWT involved). Dummy POST to /auth/token with a valid ES256 client_secret (iss=LQR9X2NWFT, sub=com.kanca.kanca.web, kid=KKYD9GMJS6) and a fake code returns invalid_grant, so the key/JWT/client_id are accepted at the token endpoint. Authorize still reports client not found. Please repair SIWA registration for this team/App ID. Happy to attach a sysdiagnose if needed.
0
0
466
2w
Sign in with Apple web: invalid_client at /auth/authorize for every Services ID in team UGZTH42J5G
Sign in with Apple for the web returns invalid_client at the authorization request for every Services ID in our team. The failure occurs before any token exchange, so no client secret is involved. Team ID: UGZTH42J5G Primary App ID: UGZTH42J5G.com.nearbuddy.mobile (Sign in with Apple enabled; "Enable as a primary App ID" selected) Services IDs: com.nearbuddy.mobile.signin com.nearbuddy.mobile.signin2 (created solely as a control) Both Services IDs are configured with: Sign in with Apple: enabled Primary App ID: UGZTH42J5G.com.nearbuddy.mobile Domain: dkhtjkcygbpomaummdih.supabase.co Return URLs: https://dkhtjkcygbpomaummdih.supabase.co/functions/v1/auth-apple-web/callback https://dkhtjkcygbpomaummdih.supabase.co/auth/v1/callback Failing authorization request: GET https://appleid.apple.com/auth/authorize client_id=com.nearbuddy.mobile.signin redirect_uri=https://dkhtjkcygbpomaummdih.supabase.co/functions/v1/auth-apple-web/callback response_type=code id_token response_mode=form_post scope=name email state= nonce= Apple responds with: invalid_client Invalid client. The same failure is reproduced: with both Services IDs; in Safari; in Chrome on Android; via curl; and using the authorization URL generated by Supabase GoTrue. The failure occurs before authentication/consent and before any authorization code or ID token is issued. Membership is active until March 18, 2027. The Account Holder is active and there are no pending membership or agreement banners. A fresh control Services ID reproduces exactly the same failure. Feedback Assistant report: FB24549460 Please inspect the Sign in with Apple provisioning/configuration state for Team UGZTH42J5G and determine why the registered Services IDs are not being recognized at the authorization endpoint.
0
0
282
2w
Sign In with Apple - invalid_client
Hi Apple Developer Support, We are implementing Sign in with Apple for our web application hosted on example.com. In the Service ID settings, we have configured the following: Service ID (client_id): com.example.service.local Web Domain: example.com Return URL: https://2db2-121-160-153-88.ngrok-free.app/login/oauth2/code/apple We also tested login via the following URL from our web application: https://appleid.apple.com/auth/authorize?response_mode=form_post&response_type=code&client_id=com.example.service.local&scope=name%20email&state=2f9gMY1rTe12-O7Wbnb7KWe504HQ0KWBSHTKHbg9ZEY=&redirect_uri=https://2db2-121-160-153-88.ngrok-free.app/login/oauth2/code/apple However, we’re receiving an invalid_client error after submission. Our questions: Is it valid to use an ngrok URL like https://2db2-121-160-153-88.ngrok-free.app/... as the Return URL for development and testing? Does the Web Domain need to match the ngrok domain, or is it enough to register the production domain (e.g., example.com)? Is there any propagation delay or approval process after updating the Return URL in the Service ID? Is the client_id strictly required to match the Service ID exactly? We would greatly appreciate any insights or best practices to help us resolve this issue. Thank you in advance!
36
23
3.5k
2w
Sign in with Apple fails with "Sign-Up Not Completed" for all users — server-side issue for App ID online.framenet.app
Sign in with Apple is failing for every user, on every device, for our live App Store app. Each attempt fails inside Apple's native sign-in sheet with the error "Sign-Up Not Completed" — before any of our code runs (no ASAuthorizationController delegate callback is reached). Environment / identifiers: Bundle ID: online.framenet.app Team ID: 5F2256QJNT App Store app: FrameNet, App ID 6799345029 (approved and live as of Aug 24, 2026) Timeline: Last worked: Aug 8, 2026 Failing on every attempt since: Aug 14, 2026, across builds 12–16 Our sign-in code is unchanged between the working and failing period Already verified / attempted on our side: Sign in with Apple entitlement is present in the signed builds (confirmed via codesign entitlement dump) Capability is enabled on the App ID in Certificates, Identifiers & Profiles Provisioning profile is valid and includes the capability Removed and re-added the Sign in with Apple capability and regenerated the provisioning profile on Aug 17 — no change Google Sign-In in the same app works normally, so networking and the rest of the auth stack are fine Reproduced on multiple physical devices and Apple IDs Because the failure occurs inside Apple's own sign-in sheet before our code executes, this appears to be a server-side state problem with Sign in with Apple for our App ID, and nothing we change client-side has any effect. We were forced to ship v1.0 with Sign in with Apple disabled. A Developer Support case is already open for this: case 20000140460775. A screen recording and full environment details have been provided in that case. Has anyone from Apple seen this "Sign-Up Not Completed" state, and can engineering inspect/repair the server-side Sign in with Apple configuration for this App ID?
0
0
150
2w
"Sign Up Not Completed" on every attempt — first-party ASAuthorizationController implementation, entitlement/profile/capability all verified
Sign in with Apple has never completed for our app. The native sheet presents correctly (name, Share/Hide My Email), but on confirmation it shows "Sign Up Not Completed" with a red exclamation. No identity token is returned; the delegate receives ASAuthorizationError 1001 only after the sheet is dismissed. The failure happens entirely inside the system sheet, before any callback reaches app code. Environment: iPhone 15 Pro Max / iOS 26.6, TestFlight builds. The Sign In with Apple capability was newly enabled on the App ID five days ago and the flow has failed on 100% of attempts ever since. (I'm withholding the Team ID / bundle ID here since this is public — happy to share them with an Apple engineer via the open support case or a Feedback report.) Implementation - identical to Apple's "Implementing User Authentication with Sign in with Apple" sample; we even replaced the third-party wrapper we originally used with this first-party version to rule it out, and the failure is unchanged: let provider = ASAuthorizationAppleIDProvider() let request = provider.createRequest() request.requestedScopes = [.fullName, .email] let controller = ASAuthorizationController(authorizationRequests: [request]) controller.delegate = self // controller strongly retained until completion controller.presentationContextProvider = self controller.performRequests() Verified correct (not assumed — extracted/inspected directly): Signed binary entitlements (extracted from the distributed IPA): com.apple.developer.applesignin = (Default), with the correct application-identifier and team-identifier for our team embedded.mobileprovision in the bundle contains the same entitlement App ID capability read back via the App Store Connect API: APPLE_ID_AUTH with APPLE_ID_AUTH_APP_CONSENT = PRIMARY_APP_CONSENT (portal shows "Enable as a primary App ID") Exactly one App ID in the account; no wildcard, no duplicates Program License Agreement accepted; nothing pending Already tried, no change: re-saving the capability in the portal; deleting and re-creating the capability via the App Store Connect API; generating a new provisioning profile after that re-registration and shipping a new build signed with it; Share My Email and Hide My Email; device restarts (iCloud + 2FA active); waiting 5+ days for propagation. This matches threads 789588, 789418 and 789323, and the service-side configuration issue described in thread 790827 (invalid_client / "Sign Up Not Completed" for developer accounts that created or edited app configurations, resolved server-side with no developer action). A Developer Support case and a TSI are already open. Question for Apple engineers: with the client implementation identical to the official sample and every developer-side artifact verified, is there anything left on the developer side that can produce this - or can the Sign in with Apple service-side registration for our client_id be checked? This is blocking an App Store release (the app was rejected under Guideline 4.8 and Sign in with Apple is the required remedy). A sysdiagnose captured during reproduction is available, and I can provide the Team ID, bundle ID and case numbers privately on request.
0
0
153
2w
Problem with Private Access Token (PAT)
Since October 3rd, I've stopped receiving responses to the Private Access Tokens challenge. I'm using this link: https://demo-issuer.private-access-tokens.fastly.com/.well-known/token-issuer-directory. I receive tokens from Fastly and return a header to the iOS app, but then I don't receive another authentication request from iOS. The user has automatic verification enabled on their phone. The problem is global and affects all my mobile app users. Has anyone encountered a similar problem and found a solution?
17
0
2.7k
3w
prepareInterfaceToProvideCredential .oneTimeCode case is not called
Since release of 18.4. prepareInterfaceToProvideCredential .oneTimeCode case is not called and instead prepareInterfaceForUserChoosingTextToInsert() is called. That is the wrong delegate for this case and it causes confusion for the users. Also, some TOTP fields are recognised however, the key icon button is not presented above the keyboard next to TOTP suggestions. I've also tested 18.5 and it has the same issue. provideOneTimeCodeWithoutUserInteraction works just fine.
3
2
1.1k
3w
Sign in with Apple fails with "Sign-Up Not Completed" for one App ID only — other apps in the same team work on the same device
Sign in with Apple fails with "Sign-Up Not Completed" for one App ID only — other apps in the same team work on the same device I have a Sign in with Apple failure that is isolated to a single App ID, and I would appreciate an Apple engineer looking at the server-side registration state for it. Filed as FB24483427. Symptom For App ID com.laonmon.twothrones (Team LH4XJ8LL98), Sign in with Apple never completes: The native ASAuthorizationController sheet appears correctly. The user authenticates with Face ID / Touch ID. iOS shows "Sign-Up Not Completed". The delegate receives ASAuthorizationError code 1000 with no further detail. No request ever reaches our server, so the failure happens entirely inside Apple's authorization flow. Our production database shows 0 accounts created via Sign in with Apple out of 147 total accounts — it has never succeeded for any user. The control experiment This is the part I think is diagnostic. Tested in one sitting, on the same iPad, signed in with the same standard (non-managed, adult) Apple ID: Notion (third-party App Store app) — sign-up completes SwingNote (my own released app, com.swingnote.app, Team LH4XJ8LL98) — sign-up completes Two Thrones (com.laonmon.twothrones, Team LH4XJ8LL98) — "Sign-Up Not Completed" Same device, same Apple ID, same team, same capability configuration — only the App ID differs. That rules out the device, the Apple ID, the team account, and the local configuration. What I have already checked Entitlement is in the signed binary. codesign -d --entitlements - on the archive shows com.apple.developer.applesignin = ["Default"]. (I did find and fix a real build defect earlier: our archive step ran with CODE_SIGNING_ALLOWED=NO, which skips entitlement processing entirely. Before that fix the sheet did not appear at all. Worth checking if you see no sheet — but that is a different symptom from this one.) App ID capability is enabled — APPLE_ID_AUTH, APPLE_ID_AUTH_APP_CONSENT = PRIMARY_APP_CONSENT. I removed and re-added the capability and regenerated the provisioning profile. No change. All four App IDs in the team have identical Sign in with Apple settings. Settings › Sign in with Apple does not list the app on the test device, so no app-scoped identifier was ever created. The sign-up genuinely never completes rather than failing on reuse. Updated Apple Developer Program License Agreement accepted; App Store Connect banner cleared. This is not propagation delay. I have seen the thread where it started working after ~2 days. The capability has been configured since Aug 16 and has never succeeded — across three App Review sessions on different devices (iPad Air 11-inch M3 / iPadOS 26.6, iPhone 17 Pro Max / iOS 26.6) and my own testing. Device logs captured over USB show AuthenticationServicesUI and AuthKitUIService starting normally, but all authentication detail is redacted as <private>. I understand the Accounts/AuthKit logging profile is needed to read it, and I will attach a sysdiagnose to the Feedback report once I have it. Impact Three consecutive App Review rejections under Guideline 2.1(a). This is the only thing blocking our initial release. Question Given that the same device and Apple ID succeed for another app in the same team, is there a server-side Sign in with Apple registration or account-association state per App ID that can get into a bad state, and is there anything a developer can do to reset it? Anything I can check or provide would be welcome. Feedback: FB24483427 DTS case: 20000144146252 (Developer Support advised filing here — they cannot see Feedback Assistant reports themselves.)
0
0
71
3w
Sign in with Apple fails with "Sign Up Not Completed" for every app in our team
Team ID: ED9QVUCFPE (individual) Bundle IDs affected: com.lumevolab.justmorriz, com.lumevolab.memiq Broken since: 28 July 2026, immediately after the Apple Developer Program membership was renewed/reactivated. Still broken on 21 August 2026 (24 days). Apple Developer Support case: 20000131208746 (closed: "no anomaly found on the membership", referred to the forums) App Review rejection: submission f087942b, 13 Aug 2026, guideline 2.1(a), "unable to log in with Sign in with Apple", on iPad Air 11" M3 / iPadOS 26.6 Symptom The native Sign in with Apple sheet opens, Face ID succeeds, and then the sheet shows the red error "Sign Up Not Completed" ("Registrazione non completata") underneath the Continue button. The sheet stays open, so ASAuthorizationController never calls back — the app-side code never runs. The sheet's own wording is "Create an account for ", so it is the account-creation step that fails on Apple's side, not our token handling. Device log (captured with idevicesyslog during a reproduction) akd ... signing request, calling GrandSlam (gsService) akd ... server response (733 bytes) akd(AppleIDAuthSupport) setError: 2:M2 missing (bad password) AuthKitUIService ... Showing Authorization UI Alert (8 ms later) M2 missing is the SRP exchange: the server did not return its M2 proof, i.e. it refused the client's M1 for this operation. Why this is not an account or device problem Sign in with Apple works on the same iPhone with the same Apple Account in third-party apps (verified with Vinted). It also failed on the App Review team's own iPad, with their own Apple Account (rejection above). So it is not tied to one account or one device. It fails identically on two different App IDs of the same team. Tried and made no difference: device restart, changing the Apple Account password, signing out and back in to the Apple Account. Configuration — verified via the App Store Connect API, not by eye Item State App ID com.lumevolab.justmorriz (89A4UV66RF) APPLE_ID_AUTH present, setting APPLE_ID_AUTH_APP_CONSENT = PRIMARY_APP_CONSENT Provisioning profile IOS_APP_STORE, ACTIVE, includes the applesignin entitlement com.apple.developer.applesignin ['Default'] in Debug, Release and Profile entitlements files Same entitlement in the signed binary verified with codesign -d --entitlements :- build/ios/iphoneos/Runner.app Firebase project playmorriz apple.com provider enabled Program License Agreement released 18 Aug 2026, accepted 21 Aug 2026 Paid Apps / Free Apps agreements both Active Membership active, renews 4 Apr 2027 What we tried on 21 August 2026, with no change Accepted the pending Program License Agreement (it was released on 18 Aug, so it cannot explain a failure that started on 28 Jul, but we ruled it out). Removed and re-added the APPLE_ID_AUTH capability on the App ID via the App Store Connect API (DELETE then POST /v1/bundleIdCapabilities), to force Apple to re-provision the server-side Sign in with Apple record for the App ID. Device restarted afterwards. Same error. Prior art suggesting a server-side cause Forums thread 790323 — identical akd … "M2 missing (bad password)", same asymmetry (other apps fine, one team's apps broken), thorough config checks. It resolved by itself after 11 days with no developer action. Threads 789588, 789418, 789579, 803591 — same class of report, no client-side cause ever identified. June 18–23, 2025: Apple acknowledged a server-side configuration fault producing exactly "Sign Up Not Completed" + invalid_client, affecting only accounts that had recently created or modified App ID / Services ID configurations (thread 790827). Our team's capabilities were re-provisioned at renewal on 28 Jul 2026 — the same class of event. Ask Please check the server-side Sign in with Apple relying-party record for team ED9QVUCFPE. Everything under our control is correct and has been re-created from scratch; the failure is in the sign-up call to GrandSlam.
1
0
839
3w
Sign in with Apple Migration
We are in the process of transferring our existing Apple account to a new account. We read the documentation and we realized that "Sign in with Apple" users needs to be migrated via API. This API seems to require "sub" which we are not currently storing in our systems. So we are planning to "collect subs" over a period before we can start transfer. I want to make sure we are collecting correct data so I am looking for confirmation from Apple engineers on this. Should we get "sub" from "user" property in ASAuthorizationAppleIDCredential or by decoding identityToken and get "sub" from the token? I think both are same but wanted to double check. Thank you!
Replies
0
Boosts
0
Views
52
Activity
2w
When to use AppTrackingTransparency in macOS
The AppTrackingTransparency documentation says: You must use the AppTrackingTransparency framework if your app collects data about end users and shares it with other companies for purposes of tracking across apps and web sites. If I create a unique customer_id value to submit to the GIPHY Actions Register endpoint to track GIFs a user has clicked or sent, does that count as "tracking across apps and web sites"?
Replies
0
Boosts
0
Views
76
Activity
2w
Security.framework returns NULL from SecPolicyCreateSSL when the directory containing the running executable no longer exists.
https://feedbackassistant.apple.com/feedback/24553094 Security.framework returns NULL from SecPolicyCreateSSL when the directory containing the running executable no longer exists. That commonly happens with "go run" after the go command deletes its temporary build directory while a child process is still running. More details on how to reproduce: https://github.com/golang/go/issues/68557 Golang team rejected my workaround https://github.com/golang/go/pull/80471/changes so we're relying on the macOS team to allow this, unless of course there is a great reason not too. This is problematic when developing applications in golang, especially daemons.
Replies
1
Boosts
0
Views
114
Activity
2w
SecKeychainGetStatus returns varying errors after SecKeychainOpen succeeds on macOS 26.6.2
Hi, so far i've been using the following API Call Sequence in order to check the keychain status prior to using one of the certificates inside the keychain for signing or verifying remote tls connection. // Step 1: Open keychain SecKeychainRef keychain = nil; OSStatus status = SecKeychainOpen(keychainPath, &keychain); // Result: errSecSuccess (0) — always succeeds, returns valid ref // Step 2: Get keychain status SecKeychainStatus keychainStatus = 0; status = SecKeychainGetStatus(keychain, &keychainStatus); // Result: FAILS : see table below I observed SecKeychainGetStatus return errSecInternalError or errSecInvalidHandle │ Process │ Keychain │ Error │ Code │ │ GUI App │ login.keychain. | errSecInternalError │ -26276 │ │ GUI App │ login.keychain | errSecInvalidHandle │ -25308 │ │ GUI App │ /Library/Keychains/System.keychain | errSecInvalidHandle │ -25308 │ │ Daemon │ /Library/Keychains/System.keychain │ errSecInvalidHandle │ -25308 │ SecKeychainOpen always returns errSecSuccess with a non-null SecKeychainRef SecKeychainGetStatus fails immediately when called on that reference The error for login.keychain varies between calls (not deterministic) The error for System.keychain is consistently errSecInvalidHandle Issue is 100% reproducible. every keychain access attempt fails Started immediately after macOS 26.6.2 upgrade Do you know if there were any changes in 26.6.2 that could have caused this behavior? If this is expected, how should I address it? Thanks
Replies
1
Boosts
0
Views
175
Activity
2w
No ML-DSA-44 in Apple CryptoKit?
What is the rationale for not including support for ML-DSA-44 in Apple CryptoKit?
Replies
2
Boosts
0
Views
395
Activity
2w
Does SecItemDelete guarantee immediate invalidation of an already-retained Secure Enclave SecKeyRef on macOS?
I’m looking for the documented Security.framework/macOS contract for this exact same-process sequence. An app creates a P-256 private key with SecKeyCreateRandomKey using: kSecAttrKeyType = kSecAttrKeyTypeECSECPrimeRandom kSecAttrKeySizeInBits = 256 kSecAttrTokenID = kSecAttrTokenIDSecureEnclave and, within kSecPrivateKeyAttrs: kSecAttrIsPermanent = true kSecAttrApplicationTag = <a unique tag> The app keeps the SecKeyRef returned directly by SecKeyCreateRandomKey alive and copies the corresponding public key with SecKeyCopyPublicKey. The app calls SecItemDelete with a query that uniquely matches that exact keychain item, and SecItemDelete returns errSecSuccess. Immediately after SecItemDelete returns, in the same process and without releasing or reacquiring the private-key reference, the app attempts a private-key operation using that same creation-returned SecKeyRef. Does errSecSuccess contractually guarantee that the already-retained SecKeyRef can no longer perform private-key operations from the point SecItemDelete returns? More concretely, is the following outcome forbidden by the API/platform contract: SecKeyCreateSignature succeeds after deletion, and the resulting signature verifies with the public key copied before deletion—that is, the public key from the same generated key pair? Or is invalidation of an already-issued SecKeyRef unspecified or otherwise not guaranteed by Security.framework? I’m asking only about the immediate post-return usability of the creation-returned SecKeyRef in this exact sequence. A clarification from Apple staff distinguishing the supported API/platform contract from current implementation behavior would be especially helpful.
Replies
1
Boosts
0
Views
535
Activity
2w
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 constructed 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). If you call SecItemUpdate with a query dictiony that searches for a key with a specific kSecAttrLabel value, it fails to find the item. OTOH, searching for a key via kSecAttrApplicationTag works (r. 186247975). If you call SecItemUpdate to update a key’s kSecAttrApplicationTag attribute, it actually updates the kSecAttrLabel attribute (r. 186247975). 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. Let’s walk through an example of this. Imagine you have an app with an App ID of SKMME9E2Y8.com.example.waffle-varnisher. Version 1 of your app does nothing fancy with the keychain. It uses neither keychain access groups nor app groups. Thus its keychain access group list consists of just the App ID, that is, [ SKMME9E2Y8.com.example.waffle-varnisher ]. When that version of your app creates a keychain item, the kSecAttrAccessGroup value will default to the only value available, SKMME9E2Y8.com.example.waffle-varnisher. In version 2 of your app you want to use keychain access groups, so you add the Keychain Sharing capability to your project and populate it with two values, SKMME9E2Y8.groupA and SKMME9E2Y8.groupB. If you take no other action, your app’s keychain access group list will be [ SKMME9E2Y8.groupA, SKMME9E2Y8.groupB, SKMME9E2Y8.com.example.waffle-varnisher ]. This changes the default value for new items to SKMME9E2Y8.groupA. This is an obvious pitfall. Version 1 of your app created new keychain items in SKMME9E2Y8.com.example.waffle-varnisher while version 2 creates them in SKMME9E2Y8.groupA. You now have different items in different groups, depending on which version the user first launched, and that’s a recipe for chaos. There are two common ways to avoid problems here: Migrate items from SKMME9E2Y8.com.example.waffle-varnisher to SKMME9E2Y8.groupA. See Transfer Items Between Keychain Access Groups, below. Add your App ID to the front of the Keychain Sharing list. This results in a keychain access group list of [ SKMME9E2Y8.com.example.waffle-varnisher, SKMME9E2Y8.groupA, SKMME9E2Y8.groupB, SKMME9E2Y8.com.example.waffle-varnisher ], which means that the default keychain access group doesn’t change. (The second instance of SKMME9E2Y8.com.example.waffle-varnisher in this list is redundant but doesn’t cause any complications.) So far so good. Now let’s say you took the first option and shipped version 2 of your app with SKMME9E2Y8.groupA as the default keychain access group. You want to update the app again, to version 3, and you’ve decided that SKMME9E2Y8.groupA no longer makes sense and you want to remove it, relying on SKMME9E2Y8.groupB instead. Doing that isn’t safe. If version 3 of your app has no access to SKMME9E2Y8.groupA, it won’t be able to access items created by version 2, even if the only goal is to migrate those items to SKMME9E2Y8.groupB. To make this work you have to: Move SKMME9E2Y8.groupA to the end of the Keychain Sharing list, so new items get created in SKMME9E2Y8.groupB. Add a migration from SKMME9E2Y8.groupA to SKMME9E2Y8.groupB. Update the migration from SKMME9E2Y8.com.example.waffle-varnisher to target SKMME9E2Y8.groupB instead of SKMME9E2Y8.groupA. That last point is necessary because a user might install version 1, skip version 2, and instead update straight to version 3. This is just an example, but the message is clear: Any change to your keychain access group list requires careful planning and testing. You’ll also see problems like this 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]] for itemDict in itemDicts { print("item:") let sortedKeysAndValues = itemDict.sorted(by: { $0.key < $1.key }) for (key, value) in sortedKeysAndValues { print(" \(key): \(value)") } } } 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! Starting from Scratch Sometimes it’s useful to be able to start from scratch. Imagine, for example, you’ve been rapidly iterating on some keychain code and you’re not sure whether your current code is compatible with items created by your earlier code. To simplify things, use SecItemDelete to delete all the existing items: _ = SecItemDelete([ kSecClass: kSecClassGenericPassword, kSecUseDataProtectionKeychain: true, ] as NSDictionary) WARNING This code is obviously dangerous. Read the discussion below to learn more. This deletes all generic password items that your app has access to. To delete items in a different keychain item class, change the value for the kSecClass attribute. This code uses kSecUseDataProtectionKeychain. On iOS there is only one keychain, so this is a no-op. On macOS it limits the effect to the data protection keychain. Without it, the call will delete items in file-based keychains as well. This is very dangerous because those items might belong to other apps, or the system. If you want to use this technique in a Mac product that uses the file-based keychain, don’t use this code. Rather, write code that carefully targets your app’s keychain items. Alternatively, avoid this code and instead delete the items using Keychain Access or the security tool. For more about keychains on the Mac, see TN3137 On Mac keychain APIs and implementations. I often invoke this code from my app’s debug UI. For example, in a Mac app I might have a Debug menu with a Reset Keychain menu item. I typically compile that code out of the release build. However, you might choose to leave it in your final product. For example, you might have a ‘secret’ way to enable the debug UI [1] so that you can use it to help users with problems. In that case, make sure your debug UI informs the user of the potential consequences of this action. If you’re working on a big app, it might have different subsystems that user the keychain in different ways. A debug action like this might make sense for your subsystem but not for all the others. In that case, coordinate this work with the owners of any other subsystems that use the keychain. [1] If your app ships on the App Store, make sure that App Review knows about your debug UI. 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. Command-Line Tools Access to the data protection keychain is mediated by various entitlements, as described in Sharing access to keychain items among a collection of apps. Those entitlements are restricted, that is, they must be authorised by a provisioning profile. This is fine for apps, app extensions, and system extensions, which are all bundled code; they exist within an app-like bundle structure. However, it’s problematic for command-line tools on the Mac, which are non-bundled executables. There’s no obvious way for such executables to include a provisioning profile (r. 125850707). For more about provisioning profiles, see TN3125 Inside Code Signing: Provisioning Profiles. For more about bundled code, see Creating distribution-signed code for macOS. If you’re creating a non-bundled executable for the Mac, first consider its execution context. If it runs as a launchd daemon, or outside of a user login context in some other way, it can’t use the data protection keychain. See TN3137 On Mac keychain APIs and implementations for more about that. If the executable is a command-line tool that’s typically run by the user, in Terminal or over SSH, it can use the data protection keychain. However: You have to embed the tool in an app-like wrapper. For more about that, see Signing a daemon with a restricted entitlement. If the tool is run via SSH, the user’s data protection keychain might be locked. To resolve this, the user must explicitly unlock their login keychain using the security tool. Note While the login keychain is a file-based keychain, unlocking it in this way also unlocks the data protection keychain. In-memory Plug-ins An in-memory plug-in is a native plug-in that’s loaded directly into the host process as a Mach-O bundle or shared library. For example, macOS screen savers are in-memory plug-ins. Note In-memory plug-ins are quite old school. Modern plug-ins are packaged as app extensions. If you’re created a Mac app that supports plug-ins, support app extension plug-ins by adopting ExtensionKit. From the keychain perspective, an in-memory plug-in is indistinguishable from the host app. This has both pros and cons: It can access all the keychain items that the host app has access to, in either the file-based or data protection keychains. It can’t access additional keychain items. For example, you can’t grant your in-memory plug-in access to a keychain access group that’s used by other apps that you create. I’ll leave it up to you to decide which of these is a pro and which is a con (-: Revision History 2026-08-31 Added another specific example to the Careful With that Shim, Mac Developer section. 2026-07-02 Clarified the terminology in the Keys Aren’t Stored in the Secure Enclave section. 2026-05-21 Enhanced the code snippet in the Lost Keychain Items, Redux section. 2026-04-27 Added the Command-Line Tools and In-memory Plug-ins sections. 2026-04-15 Significantly expanded the example in the Lost Keychain Items section. 2026-04-14 Added the Starting from Scratch section. 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
5.9k
Activity
2w
Sign in with Apple fails on ALL apps under our developer team with "Sign Up Not Completed" / "No se ha podido completar el registro."
Team ID: 2357A78TMR Affected App ID: me.manulorenzo.crossfittracker (App Store Connect app 6760609857) Reproduces identically across every app under this team, on a real device, personal (non-managed) Apple ID with 2FA enabled, persisting for multiple days across device reboots. Symptom matches the documented pattern in developer.apple.com/forums/thread/837986 and /thread/789222: native SIWA sheet renders, biometric/password succeeds, then "Sign Up Not Completed" — no credential ever returned. This matches Apple's own described root cause of an App ID/team never being fully provisioned server-side for Sign in with Apple (AKSQLError -6003 / GSA "Invalid client" / ASAuthorizationError 1001 with empty userInfo). Client-side configuration has been independently verified correct: provisioning profile carries com.apple.developer.applesignin entitlement, App ID capability APPLE_ID_AUTH is configured with PRIMARY_APP_CONSENT, Firebase's apple.com provider has the correct bundle ID registered. Requesting the server-side team registration fix described as the resolution in the linked threads.
Replies
2
Boosts
0
Views
96
Activity
2w
Apple sign in "Sign up not complete"
Hey there, im getting Sign up not complete! even with the official template https://developer.apple.com/documentation/authenticationservices/implementing-user-authentication-with-sign-in-with-apple my developer account is recently renewed, which it has expired for a bit i am using automatic managed signing I have a feel something went wrong my apple developer account id, and also this cause me getting invalid client when sign in with app on web app.
Replies
17
Boosts
9
Views
2.5k
Activity
2w
Private Email Relay rejects our sign-in emails with 554 5.7.1 [HM08] (FB24564316)
Following the pinned post "Gathering required information for troubleshooting Private Email Relay with Sign in with Apple" (thread 794191, which is locked, so I am posting here as instructed): I have filed FB24564316 with the required information. Since 28 July, every message from our app's sending domain to @privaterelay.appleid.com addresses is rejected by iCloud Mail with 554 5.7.1 [HM08], so users who signed up with Sign in with Apple and Hide My Email cannot receive their sign-in emails. Plain-text messages with no URL bounce as well, SPF/DKIM/DMARC pass on two independent sending systems, and external reputation checks are clean, so this looks like a false positive on the sending domain. The feedback contains the App ID (6761705463), Services ID, SMTP error codes and transaction IDs. iCloud postmaster case 21382576 and Developer Support case 20000139732014 did not resolve it. Happy to provide message headers. FB24564316
Replies
0
Boosts
0
Views
309
Activity
2w
Sign in with Apple fails with "Sign Up Not Completed"
Sign in with Apple has failed for every user of our app for four weeks. It fails inside Apple's own authorization sheet, before any credential reaches our code. App ID: com.chorestoreapp.app (SJRRC8Q327) Team ID: T4KD7949GL App Apple ID: 6797335710 — TestFlight builds 7 and 8, version 1.0.1 Symptom: The authorization sheet appears and behaves normally. When the fingerprint read is complete, it immediately fails with "Sign Up Not Completed". No credential is returned. When I cancel, it returns ERR_REQUEST_CANCELED, consistent with the sheet being dismissed by the user after being unable to sign in. Sign in with Apple works on the affected device and Apple ID. Downloading an unrelated App Store app and creating a new account with Sign in with Apple succeeded immediately, on the same iPad, with the same Apple ID, minutes before and after a ChoreStore failure. Before that test, Settings → [name] → Sign in with Apple was empty, so the account had never completed the flow — it is demonstrably capable of it now, for other apps. Reproduced with: two Apple IDs, two physical devices, both "Share My Email" and "Hide My Email", Wi-Fi and cellular, and two builds on freshly regenerated credentials. Verified on our side: The signed binary contains com.apple.developer.applesignin = ["Default"] The embedded provisioning profile includes the same entitlement App ID capability, read back from the App Store Connect API: APPLE_ID_AUTH with [{"key":"APPLE_ID_AUTH_APP_CONSENT","options":[{"key":"PRIMARY_APP_CONSENT"}]}] Provisioning profile IOS_APP_STORE, state ACTIVE, expires 2027-08-20 Distribution certificate valid to 2027-08-20 Exactly one bundle ID in the account; no grouped App ID The client call is the standard one, with no nonce or custom parameters: signInAsync({ requestedScopes: [FULL_NAME, EMAIL] }) The distribution certificate and App ID survived an Individual → Organization conversion unchanged (re-verified after the conversion) Why I'm posting rather than filing a code-level request. The request form requires a focused sample Xcode project. A sample cannot demonstrate this: anyone who opens it must sign it with their own team, which changes the App ID — the one variable that is failing. The fault appears to be server-side provisioning for this specific App ID, which no sample can carry. Question: Given the client configuration verifies correct and Sign in with Apple works on the same device and Apple ID for other apps, what would cause the authorization sheet itself to fail for a single App ID, and how can that be investigated or reset? Happy to provide a sysdiagnose, timestamps of specific failed attempts, a screen recording showing the working and failing cases minutes apart, or a TestFlight build.
Replies
0
Boosts
0
Views
129
Activity
2w
Sign in with Apple: native Sign Up Not Completed + web invalid_client — FB24556583
Feedback Assistant: FB24556583 Please inspect Sign in with Apple server-side registration for Team LQR9X2NWFT / App ID com.kanca.kanca. This is not a client-code or JWT issue. It is blocking App Store review (Guideline 2.1(a)). NATIVE Bundle ID: com.kanca.kanca App Store id: 6804206744 ASAuthorizationController / Flutter sign_in_with_apple. System sheet opens (Hide My Email), Face ID succeeds, then Apple's sheet shows Sign Up Not Completed. No identity token reaches the app. The app does not appear under Settings > Sign in with Apple. Reproduced for days. App Review reproduced it on iPhone 17 Pro Max iOS 26.6 and iPad Air 11-inch M3 iPadOS 26.6. App ID has Sign in with Apple enabled as a primary App ID. Capability re-saved; no change. Same Apple ID completes Sign in with Apple in other teams' apps on the same device. WEB Services ID: com.kanca.kanca.web GET https://appleid.apple.com/auth/authorize?client_id=com.kanca.kanca.web returns invalid_client before any login form (also via curl; no JWT involved). Dummy POST to /auth/token with a valid ES256 client_secret (iss=LQR9X2NWFT, sub=com.kanca.kanca.web, kid=KKYD9GMJS6) and a fake code returns invalid_grant, so the key/JWT/client_id are accepted at the token endpoint. Authorize still reports client not found. Please repair SIWA registration for this team/App ID. Happy to attach a sysdiagnose if needed.
Replies
0
Boosts
0
Views
466
Activity
2w
Sign in with Apple web: invalid_client at /auth/authorize for every Services ID in team UGZTH42J5G
Sign in with Apple for the web returns invalid_client at the authorization request for every Services ID in our team. The failure occurs before any token exchange, so no client secret is involved. Team ID: UGZTH42J5G Primary App ID: UGZTH42J5G.com.nearbuddy.mobile (Sign in with Apple enabled; "Enable as a primary App ID" selected) Services IDs: com.nearbuddy.mobile.signin com.nearbuddy.mobile.signin2 (created solely as a control) Both Services IDs are configured with: Sign in with Apple: enabled Primary App ID: UGZTH42J5G.com.nearbuddy.mobile Domain: dkhtjkcygbpomaummdih.supabase.co Return URLs: https://dkhtjkcygbpomaummdih.supabase.co/functions/v1/auth-apple-web/callback https://dkhtjkcygbpomaummdih.supabase.co/auth/v1/callback Failing authorization request: GET https://appleid.apple.com/auth/authorize client_id=com.nearbuddy.mobile.signin redirect_uri=https://dkhtjkcygbpomaummdih.supabase.co/functions/v1/auth-apple-web/callback response_type=code id_token response_mode=form_post scope=name email state= nonce= Apple responds with: invalid_client Invalid client. The same failure is reproduced: with both Services IDs; in Safari; in Chrome on Android; via curl; and using the authorization URL generated by Supabase GoTrue. The failure occurs before authentication/consent and before any authorization code or ID token is issued. Membership is active until March 18, 2027. The Account Holder is active and there are no pending membership or agreement banners. A fresh control Services ID reproduces exactly the same failure. Feedback Assistant report: FB24549460 Please inspect the Sign in with Apple provisioning/configuration state for Team UGZTH42J5G and determine why the registered Services IDs are not being recognized at the authorization endpoint.
Replies
0
Boosts
0
Views
282
Activity
2w
Sign In with Apple - invalid_client
Hi Apple Developer Support, We are implementing Sign in with Apple for our web application hosted on example.com. In the Service ID settings, we have configured the following: Service ID (client_id): com.example.service.local Web Domain: example.com Return URL: https://2db2-121-160-153-88.ngrok-free.app/login/oauth2/code/apple We also tested login via the following URL from our web application: https://appleid.apple.com/auth/authorize?response_mode=form_post&response_type=code&client_id=com.example.service.local&scope=name%20email&state=2f9gMY1rTe12-O7Wbnb7KWe504HQ0KWBSHTKHbg9ZEY=&redirect_uri=https://2db2-121-160-153-88.ngrok-free.app/login/oauth2/code/apple However, we’re receiving an invalid_client error after submission. Our questions: Is it valid to use an ngrok URL like https://2db2-121-160-153-88.ngrok-free.app/... as the Return URL for development and testing? Does the Web Domain need to match the ngrok domain, or is it enough to register the production domain (e.g., example.com)? Is there any propagation delay or approval process after updating the Return URL in the Service ID? Is the client_id strictly required to match the Service ID exactly? We would greatly appreciate any insights or best practices to help us resolve this issue. Thank you in advance!
Replies
36
Boosts
23
Views
3.5k
Activity
2w
Sign in with Apple fails with "Sign-Up Not Completed" for all users — server-side issue for App ID online.framenet.app
Sign in with Apple is failing for every user, on every device, for our live App Store app. Each attempt fails inside Apple's native sign-in sheet with the error "Sign-Up Not Completed" — before any of our code runs (no ASAuthorizationController delegate callback is reached). Environment / identifiers: Bundle ID: online.framenet.app Team ID: 5F2256QJNT App Store app: FrameNet, App ID 6799345029 (approved and live as of Aug 24, 2026) Timeline: Last worked: Aug 8, 2026 Failing on every attempt since: Aug 14, 2026, across builds 12–16 Our sign-in code is unchanged between the working and failing period Already verified / attempted on our side: Sign in with Apple entitlement is present in the signed builds (confirmed via codesign entitlement dump) Capability is enabled on the App ID in Certificates, Identifiers & Profiles Provisioning profile is valid and includes the capability Removed and re-added the Sign in with Apple capability and regenerated the provisioning profile on Aug 17 — no change Google Sign-In in the same app works normally, so networking and the rest of the auth stack are fine Reproduced on multiple physical devices and Apple IDs Because the failure occurs inside Apple's own sign-in sheet before our code executes, this appears to be a server-side state problem with Sign in with Apple for our App ID, and nothing we change client-side has any effect. We were forced to ship v1.0 with Sign in with Apple disabled. A Developer Support case is already open for this: case 20000140460775. A screen recording and full environment details have been provided in that case. Has anyone from Apple seen this "Sign-Up Not Completed" state, and can engineering inspect/repair the server-side Sign in with Apple configuration for this App ID?
Replies
0
Boosts
0
Views
150
Activity
2w
"Sign Up Not Completed" on every attempt — first-party ASAuthorizationController implementation, entitlement/profile/capability all verified
Sign in with Apple has never completed for our app. The native sheet presents correctly (name, Share/Hide My Email), but on confirmation it shows "Sign Up Not Completed" with a red exclamation. No identity token is returned; the delegate receives ASAuthorizationError 1001 only after the sheet is dismissed. The failure happens entirely inside the system sheet, before any callback reaches app code. Environment: iPhone 15 Pro Max / iOS 26.6, TestFlight builds. The Sign In with Apple capability was newly enabled on the App ID five days ago and the flow has failed on 100% of attempts ever since. (I'm withholding the Team ID / bundle ID here since this is public — happy to share them with an Apple engineer via the open support case or a Feedback report.) Implementation - identical to Apple's "Implementing User Authentication with Sign in with Apple" sample; we even replaced the third-party wrapper we originally used with this first-party version to rule it out, and the failure is unchanged: let provider = ASAuthorizationAppleIDProvider() let request = provider.createRequest() request.requestedScopes = [.fullName, .email] let controller = ASAuthorizationController(authorizationRequests: [request]) controller.delegate = self // controller strongly retained until completion controller.presentationContextProvider = self controller.performRequests() Verified correct (not assumed — extracted/inspected directly): Signed binary entitlements (extracted from the distributed IPA): com.apple.developer.applesignin = (Default), with the correct application-identifier and team-identifier for our team embedded.mobileprovision in the bundle contains the same entitlement App ID capability read back via the App Store Connect API: APPLE_ID_AUTH with APPLE_ID_AUTH_APP_CONSENT = PRIMARY_APP_CONSENT (portal shows "Enable as a primary App ID") Exactly one App ID in the account; no wildcard, no duplicates Program License Agreement accepted; nothing pending Already tried, no change: re-saving the capability in the portal; deleting and re-creating the capability via the App Store Connect API; generating a new provisioning profile after that re-registration and shipping a new build signed with it; Share My Email and Hide My Email; device restarts (iCloud + 2FA active); waiting 5+ days for propagation. This matches threads 789588, 789418 and 789323, and the service-side configuration issue described in thread 790827 (invalid_client / "Sign Up Not Completed" for developer accounts that created or edited app configurations, resolved server-side with no developer action). A Developer Support case and a TSI are already open. Question for Apple engineers: with the client implementation identical to the official sample and every developer-side artifact verified, is there anything left on the developer side that can produce this - or can the Sign in with Apple service-side registration for our client_id be checked? This is blocking an App Store release (the app was rejected under Guideline 4.8 and Sign in with Apple is the required remedy). A sysdiagnose captured during reproduction is available, and I can provide the Team ID, bundle ID and case numbers privately on request.
Replies
0
Boosts
0
Views
153
Activity
2w
Problem with Private Access Token (PAT)
Since October 3rd, I've stopped receiving responses to the Private Access Tokens challenge. I'm using this link: https://demo-issuer.private-access-tokens.fastly.com/.well-known/token-issuer-directory. I receive tokens from Fastly and return a header to the iOS app, but then I don't receive another authentication request from iOS. The user has automatic verification enabled on their phone. The problem is global and affects all my mobile app users. Has anyone encountered a similar problem and found a solution?
Replies
17
Boosts
0
Views
2.7k
Activity
3w
prepareInterfaceToProvideCredential .oneTimeCode case is not called
Since release of 18.4. prepareInterfaceToProvideCredential .oneTimeCode case is not called and instead prepareInterfaceForUserChoosingTextToInsert() is called. That is the wrong delegate for this case and it causes confusion for the users. Also, some TOTP fields are recognised however, the key icon button is not presented above the keyboard next to TOTP suggestions. I've also tested 18.5 and it has the same issue. provideOneTimeCodeWithoutUserInteraction works just fine.
Replies
3
Boosts
2
Views
1.1k
Activity
3w
Sign in with Apple fails with "Sign-Up Not Completed" for one App ID only — other apps in the same team work on the same device
Sign in with Apple fails with "Sign-Up Not Completed" for one App ID only — other apps in the same team work on the same device I have a Sign in with Apple failure that is isolated to a single App ID, and I would appreciate an Apple engineer looking at the server-side registration state for it. Filed as FB24483427. Symptom For App ID com.laonmon.twothrones (Team LH4XJ8LL98), Sign in with Apple never completes: The native ASAuthorizationController sheet appears correctly. The user authenticates with Face ID / Touch ID. iOS shows "Sign-Up Not Completed". The delegate receives ASAuthorizationError code 1000 with no further detail. No request ever reaches our server, so the failure happens entirely inside Apple's authorization flow. Our production database shows 0 accounts created via Sign in with Apple out of 147 total accounts — it has never succeeded for any user. The control experiment This is the part I think is diagnostic. Tested in one sitting, on the same iPad, signed in with the same standard (non-managed, adult) Apple ID: Notion (third-party App Store app) — sign-up completes SwingNote (my own released app, com.swingnote.app, Team LH4XJ8LL98) — sign-up completes Two Thrones (com.laonmon.twothrones, Team LH4XJ8LL98) — "Sign-Up Not Completed" Same device, same Apple ID, same team, same capability configuration — only the App ID differs. That rules out the device, the Apple ID, the team account, and the local configuration. What I have already checked Entitlement is in the signed binary. codesign -d --entitlements - on the archive shows com.apple.developer.applesignin = ["Default"]. (I did find and fix a real build defect earlier: our archive step ran with CODE_SIGNING_ALLOWED=NO, which skips entitlement processing entirely. Before that fix the sheet did not appear at all. Worth checking if you see no sheet — but that is a different symptom from this one.) App ID capability is enabled — APPLE_ID_AUTH, APPLE_ID_AUTH_APP_CONSENT = PRIMARY_APP_CONSENT. I removed and re-added the capability and regenerated the provisioning profile. No change. All four App IDs in the team have identical Sign in with Apple settings. Settings › Sign in with Apple does not list the app on the test device, so no app-scoped identifier was ever created. The sign-up genuinely never completes rather than failing on reuse. Updated Apple Developer Program License Agreement accepted; App Store Connect banner cleared. This is not propagation delay. I have seen the thread where it started working after ~2 days. The capability has been configured since Aug 16 and has never succeeded — across three App Review sessions on different devices (iPad Air 11-inch M3 / iPadOS 26.6, iPhone 17 Pro Max / iOS 26.6) and my own testing. Device logs captured over USB show AuthenticationServicesUI and AuthKitUIService starting normally, but all authentication detail is redacted as <private>. I understand the Accounts/AuthKit logging profile is needed to read it, and I will attach a sysdiagnose to the Feedback report once I have it. Impact Three consecutive App Review rejections under Guideline 2.1(a). This is the only thing blocking our initial release. Question Given that the same device and Apple ID succeed for another app in the same team, is there a server-side Sign in with Apple registration or account-association state per App ID that can get into a bad state, and is there anything a developer can do to reset it? Anything I can check or provide would be welcome. Feedback: FB24483427 DTS case: 20000144146252 (Developer Support advised filing here — they cannot see Feedback Assistant reports themselves.)
Replies
0
Boosts
0
Views
71
Activity
3w
Sign in with Apple fails with "Sign Up Not Completed" for every app in our team
Team ID: ED9QVUCFPE (individual) Bundle IDs affected: com.lumevolab.justmorriz, com.lumevolab.memiq Broken since: 28 July 2026, immediately after the Apple Developer Program membership was renewed/reactivated. Still broken on 21 August 2026 (24 days). Apple Developer Support case: 20000131208746 (closed: "no anomaly found on the membership", referred to the forums) App Review rejection: submission f087942b, 13 Aug 2026, guideline 2.1(a), "unable to log in with Sign in with Apple", on iPad Air 11" M3 / iPadOS 26.6 Symptom The native Sign in with Apple sheet opens, Face ID succeeds, and then the sheet shows the red error "Sign Up Not Completed" ("Registrazione non completata") underneath the Continue button. The sheet stays open, so ASAuthorizationController never calls back — the app-side code never runs. The sheet's own wording is "Create an account for ", so it is the account-creation step that fails on Apple's side, not our token handling. Device log (captured with idevicesyslog during a reproduction) akd ... signing request, calling GrandSlam (gsService) akd ... server response (733 bytes) akd(AppleIDAuthSupport) setError: 2:M2 missing (bad password) AuthKitUIService ... Showing Authorization UI Alert (8 ms later) M2 missing is the SRP exchange: the server did not return its M2 proof, i.e. it refused the client's M1 for this operation. Why this is not an account or device problem Sign in with Apple works on the same iPhone with the same Apple Account in third-party apps (verified with Vinted). It also failed on the App Review team's own iPad, with their own Apple Account (rejection above). So it is not tied to one account or one device. It fails identically on two different App IDs of the same team. Tried and made no difference: device restart, changing the Apple Account password, signing out and back in to the Apple Account. Configuration — verified via the App Store Connect API, not by eye Item State App ID com.lumevolab.justmorriz (89A4UV66RF) APPLE_ID_AUTH present, setting APPLE_ID_AUTH_APP_CONSENT = PRIMARY_APP_CONSENT Provisioning profile IOS_APP_STORE, ACTIVE, includes the applesignin entitlement com.apple.developer.applesignin ['Default'] in Debug, Release and Profile entitlements files Same entitlement in the signed binary verified with codesign -d --entitlements :- build/ios/iphoneos/Runner.app Firebase project playmorriz apple.com provider enabled Program License Agreement released 18 Aug 2026, accepted 21 Aug 2026 Paid Apps / Free Apps agreements both Active Membership active, renews 4 Apr 2027 What we tried on 21 August 2026, with no change Accepted the pending Program License Agreement (it was released on 18 Aug, so it cannot explain a failure that started on 28 Jul, but we ruled it out). Removed and re-added the APPLE_ID_AUTH capability on the App ID via the App Store Connect API (DELETE then POST /v1/bundleIdCapabilities), to force Apple to re-provision the server-side Sign in with Apple record for the App ID. Device restarted afterwards. Same error. Prior art suggesting a server-side cause Forums thread 790323 — identical akd … "M2 missing (bad password)", same asymmetry (other apps fine, one team's apps broken), thorough config checks. It resolved by itself after 11 days with no developer action. Threads 789588, 789418, 789579, 803591 — same class of report, no client-side cause ever identified. June 18–23, 2025: Apple acknowledged a server-side configuration fault producing exactly "Sign Up Not Completed" + invalid_client, affecting only accounts that had recently created or modified App ID / Services ID configurations (thread 790827). Our team's capabilities were re-provisioned at renewal on 28 Jul 2026 — the same class of event. Ask Please check the server-side Sign in with Apple relying-party record for team ED9QVUCFPE. Everything under our control is correct and has been re-created from scratch; the failure is in the sign-up call to GrandSlam.
Replies
1
Boosts
0
Views
839
Activity
3w