Current Setup:
Using Secure Enclave with userPresence access control
Foreground keychain accessibility: whenPasscodeSetThisDeviceOnly
Security Requirement:
Our security group wants us to invalidate biometrics and require a username/password if a biometric item is added (potentially by a hostile 3rd party)
Need to upgrade from userPresence to biometricCurrentSet to ensure re-authentication when biometric credentials change.
Issue:
After implementing biometricCurrentSet, authentication cancels after two failed biometric attempts instead of falling back to passcode.
Current Detection Method:
User completes initial biometric authentication
Biometric changes occur (undetectable by app)
App attempts Secure Enclave access
Access denial triggers re-authentication requirement
Cannot revoke refresh token after access is denied
Security Concern:
Current implementation allows new biometric enrollments to access existing authenticated sessions without re-verification.
Question:
What's the recommended approach to:
Implement biometricCurrentSet while maintaining passcode fallback
Properly handle refresh token invalidation when biometric credentials change
Looking for guidance on best practices for implementing these security requirements while maintaining good UX.
General
RSS for tagPrioritize user privacy and data security in your app. Discuss best practices for data handling, user consent, and security measures to protect user information.
Selecting any option will automatically load the page
Post
Replies
Boosts
Views
Activity
I regularly help developers with keychain problems, both here on DevForums and for my Day Job™ in DTS. 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, ©Result)
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, ©Result)
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 (r. 105800863). Fun times!
Digital Identities Aren’t Real
A digital identity is the combination of a certificate and the private key that matches the public key within that certificate. The SecItem API has a digital identity keychain item class, namely kSecClassIdentity. However, the keychain does not store digital identities. When you add a digital identity to the keychain, the system stores its components, the certificate and the private key, separately, using kSecClassCertificate and kSecClassKey respectively.
This has a number of non-obvious effects:
Adding a certificate can ‘add’ a digital identity. If the new certificate happens to match a private key that’s already in the keychain, the keychain treats that pair as a digital identity.
Likewise when you add a private key.
Similarly, removing a certificate or private key can ‘remove’ a digital identity.
Adding a digital identity will either add a private key, or a certificate, or both, depending on what’s already in the keychain.
Removing a digital identity removes its certificate. It might also remove the private key, depending on whether that private key is used by a different digital identity.
The system forms a digital identity by matching the kSecAttrApplicationLabel (klbl) attribute of the private key with the kSecAttrPublicKeyHash (pkhh) attribute of the certificate. If you add both items to the keychain and the system doesn’t form an identity, check the value of these attributes.
For more information the key attributes, see SecItem attributes for keys.
Keys Aren’t Stored in the Secure Enclave
Apple platforms let you protect a key with the Secure Enclave (SE). The key is then hardware bound. It can only be used by that specific SE [1].
Earlier versions of the Protecting keys with the Secure Enclave article implied that SE-protected keys were stored in the SE itself. This is not true, and it’s caused a lot of confusion. For example, I once asked the keychain team “How much space does the SE have available to store keys?”, a question that’s complete nonsense once you understand how this works.
In reality, SE-protected keys are stored in the standard keychain database alongside all your other keychain items. The difference is that the key is wrapped in such a way that only the SE can use it. So, the key is protected by the SE, not stored in the SE.
A while back we updated the docs to clarify this point but the confusion persists.
[1] Technically it’s that specific iteration of that specific SE. If you erase the device then the key material needed to use the key is erased and so the key becomes permanently useless. This is the sort of thing you’ll find explained in Apple Platform Security.
Careful With that Shim, Mac Developer
As explained in TN3137 On Mac keychain APIs and implementations, macOS has a shim that connects the SecItem API to either the data protection keychain or the file-based keychain depending on the nature of the request. That shim has limitations. Some of those are architectural but others are simply bugs in the shim. For some great examples, see the Investigating Complex Attributes section below.
The best way to avoid problems like this is to target the data protection keychain. If you can’t do that, try to avoid exploring the outer reaches of the SecItem API. If you encounter a case that doesn’t make sense, try that same case with the data protection keychain. If it works there but fails with the file-based keychain, please do file a bug against the shim. It’ll be in good company.
Here’s some known issues with the shim:
It ignores unsupported attributes. See Erroneous Attributes, above, for more background on that.
The shim can fan out to both the data protection and the file-based keychain. In that case it has to make a policy decision about how to handle errors. This results in some unexpected behaviour (r. 143405965). For example, if you call SecItemCopyMatching while the keychain is locked, the data protection keychain will fail with errSecInteractionNotAllowed (-25308). OTOH, it’s possible to query for the presence of items in the file-based keychain even when it’s locked. If you do that and there’s no matching item, the file-based keychain fails with errSecItemNotFound (-25300). When the shim gets these conflicting errors, it chooses to return the latter. Whether this is right or wrong depends on your perspective, but it’s certainly confusing, especially if you’re coming at this from the iOS side.
If you call SecItemDelete without specifying a match limit (kSecMatchLimit), the data protection keychain deletes all matching items, whereas the file-based keychain just deletes a single match (r. 105800863).
While these issue have all have bug numbers, there’s no guarantee that any of them will be fixed. Fixing bugs like this is tricky because of binary compatibility concerns.
Add-only Attributes
Some attributes can only be set when you add an item. These attributes are usually associated with the scope of the item. For example, to protect an item with the Secure Enclave, supply the kSecAttrAccessControl attribute to the SecItemAdd call. Once you do that, however, you can’t change the attribute. Calling SecItemUpdate with a new kSecAttrAccessControl won’t work.
Lost Keychain Items
A common complaint from developers is that a seemingly minor update to their app has caused it to lose all of its keychain items. Usually this is caused by one of two problems:
Entitlement changes
Query dictionary confusion
Access to keychain items is mediated by various entitlements, as described in Sharing access to keychain items among a collection of apps. If the two versions of your app have different entitlements, one version may not be able to ‘see’ items created by the other.
Imagine you have an app with an App ID of SKMME9E2Y8.com.example.waffle-varnisher. Version 1 of your app is signed with the keychain-access-groups entitlement set to [ SKMME9E2Y8.groupA, SKMME9E2Y8.groupB ]. That makes its keychain access group list [ SKMME9E2Y8.groupA, SKMME9E2Y8.groupB, SKMME9E2Y8.com.example.waffle-varnisher ]. If this app creates a new keychain item without specifying kSecAttrAccessGroup, the system places the item into SKMME9E2Y8.groupA. If version 2 of your app removes SKMME9E2Y8.groupA from the keychain-access-groups, it’ll no longer be able to see the keychain items created by version 1.
You’ll also see this problem if you change your App ID prefix, as described in App ID Prefix Change and Keychain Access.
IMPORTANT When checking for this problem, don’t rely on your .entitlements file. There are many steps between it and your app’s actual entitlements. Rather, run codesign to dump the entitlements of your built app:
% codesign -d --entitlements - /path/to/your.app
Lost Keychain Items, Redux
Another common cause of lost keychain items is confusion about query dictionaries, something discussed in detail in this post and SecItem: Fundamentals. If SecItemCopyMatching isn’t returning the expected item, add some test code to get all the items and their attributes. For example, to dump all the generic password items, run code like this:
func dumpGenericPasswords() throws {
let itemDicts = try secCall {
SecItemCopyMatching([
kSecClass: kSecClassGenericPassword,
kSecMatchLimit: kSecMatchLimitAll,
kSecReturnAttributes: true,
] as NSDictionary, $0)
} as! [[String: Any]]
print(itemDicts)
}
Then compare each item’s attributes against the attributes you’re looking for to see why there was no match.
Data Protection and Background Execution
Keychain items are subject to data protection. Specifically, an item may or may not be accessible depending on whether specific key material is available. For an in-depth discussion of how this works, see Apple Platform Security.
Note This section focuses on iOS but you’ll see similar effects on all Apple platforms. On macOS specifically, the contents of this section only apply to the data protection keychain.
The keychain supports three data protection levels:
kSecAttrAccessibleWhenUnlocked
kSecAttrAccessibleAfterFirstUnlock
kSecAttrAccessibleAlways
Note There are additional data protection levels, all with the ThisDeviceOnly suffix. Understanding those is not necessary to understanding this pitfall.
Each data protection level describes the lifetime of the key material needed to work with items protected in that way. Specifically:
The key material needed to work with a kSecAttrAccessibleWhenUnlocked item comes and goes as the user locks and unlocks their device.
The key material needed to work with a kSecAttrAccessibleAfterFirstUnlock item becomes available when the device is first unlocked and remains available until the device restarts.
The default data protection level is kSecAttrAccessibleWhenUnlocked. If you add an item to the keychain and don’t specify a data protection level, this is what you get [1].
To specify a data protection level when you add an item to the keychain, apply the kSecAttrAccessible attribute. Alternatively, embed the access level within a SecAccessControl object and apply that using the kSecAttrAccessControl attribute.
IMPORTANT It’s best practice to set these attributes when you add the item and then never update them. See Add-only Attributes, above, for more on that.
If you perform an operation whose data protection is incompatible with the currently available key material, that operation fails with errSecInteractionNotAllowed [2].
There are four fundamental keychain operations, discussed in the SecItem: Fundamentals, and each interacts with data protection in a different way:
Copy — If you attempt to access a keychain item whose key material is unavailable, SecItemCopyMatching fails with errSecInteractionNotAllowed. This is an obvious result; the whole point of data protection is to enforce this security policy.
Add — If you attempt to add a keychain item whose key material is unavailable, SecItemAdd fails with errSecInteractionNotAllowed. This is less obvious. The reason why this fails is that the system needs the key material to protect (by encryption) the keychain item, and it can’t do that if if that key material isn’t available.
Update — If you attempt to update a keychain item whose key material is unavailable, SecItemUpdate fails with errSecInteractionNotAllowed. This result is an obvious consequence of the previous result.
Delete — Deleting a keychain item, using SecItemDelete, doesn’t require its key material, and thus a delete will succeed when the item is otherwise unavailable.
That last point is a significant pitfall. I regularly see keychain code like this:
Read an item holding a critical user credential.
If that works, use that credential.
If it fails, delete the item and start from a ‘factory reset’ state.
The problem is that, if your code ends up running in the background unexpectedly, step 1 fails with errSecInteractionNotAllowed and you turn around and delete the user’s credential. Ouch!
Note Even if you didn’t write this code, you might have inherited it from a keychain wrapper library. See *Think Before Wrapping, below.
There are two paths forward here:
If you don’t expect this code to work in the background, check for the errSecInteractionNotAllowed error and non-destructively cancel the operation in that case.
If you expect this code to be running in the background, switch to a different data protection level.
WARNING For the second path, the most obvious fix is to move from kSecAttrAccessibleWhenUnlocked to kSecAttrAccessibleAfterFirstUnlock. However, this is not a panacea. It’s possible that your app might end up running before first unlock [3]. So, if you choose the second path, you must also make sure to follow the advice for the first path.
You can determine whether the device is unlocked using the isProtectedDataAvailable property and its associated notifications. However, it’s best not to use this property as part of your core code, because such preflighting is fundamentally racy. Rather, perform the operation and handle the error gracefully.
It might make sense to use isProtectedDataAvailable property as part of debugging, logging, and diagnostic code.
[1] For file data protection there’s an entitlement (com.apple.developer.default-data-protection) that controls the default data protection level. There’s no such entitlement for the keychain. That’s actually a good thing! In my experience the file data protection entitlement is an ongoing source of grief. See this thread if you’re curious.
[2] This might seem like an odd error but it’s actually pretty reasonable:
The operation needs some key material that’s currently unavailable.
Only a user action can provide that key material.
But the data protection keychain will never prompt the user to unlock their device.
Thus you get an error instead.
[3] iOS generally avoids running third-party code before first unlock, but there are circumstances where that can happen. The obvious legitimate example of this is a VoIP app, where the user expects their phone to ring even if they haven’t unlocked it since the last restart. There are also other less legitimate examples of this, including historical bugs that caused apps to launch in the background before first unlock.
Best Practices
With the pitfalls out of the way, let’s talk about best practices.
Less Painful Dictionaries
I look at a lot of keychain code and it’s amazing how much of it is way more painful than it needs to be. The biggest offender here is the dictionaries. Here are two tips to minimise the pain.
First, don’t use CFDictionary. It’s seriously ugly. While the SecItem API is defined in terms of CFDictionary, you don’t have to work with CFDictionary directly. Rather, use NSDictionary and take advantage of the toll-free bridge.
For example, consider this CFDictionary code:
CFTypeRef keys[4] = {
kSecClass,
kSecAttrService,
kSecMatchLimit,
kSecReturnAttributes,
};
static const int kTen = 10;
CFNumberRef ten = CFNumberCreate(NULL, kCFNumberIntType, &kTen);
CFAutorelease(ten);
CFTypeRef values[4] = {
kSecClassGenericPassword,
CFSTR("AYS"),
ten,
kCFBooleanTrue,
};
CFDictionaryRef query = CFDictionaryCreate(
NULL,
keys,
values,
4,
&kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks
);
Note This might seem rather extreme but I’ve literally seen code like this, and worse, while helping developers.
Contrast this to the equivalent NSDictionary code:
NSDictionary * query = @{
(__bridge NSString *) kSecClass: (__bridge NSString *) kSecClassGenericPassword,
(__bridge NSString *) kSecAttrService: @"AYS",
(__bridge NSString *) kSecMatchLimit: @10,
(__bridge NSString *) kSecReturnAttributes: @YES,
};
Wow, that’s so much better.
Second, if you’re working in Swift, take advantage of its awesome ability to create NSDictionary values from Swift dictionary literals. Here’s the equivalent code in Swift:
let query = [
kSecClass: kSecClassGenericPassword,
kSecAttrService: "AYS",
kSecMatchLimit: 10,
kSecReturnAttributes: true,
] as NSDictionary
Nice!
Avoid Reusing Dictionaries
I regularly see folks reuse dictionaries for different SecItem calls. For example, they might have code like this:
var copyResult: CFTypeRef? = nil
let dict = [
kSecClass: kSecClassGenericPassword,
kSecAttrService: "AYS",
kSecAttrAccount: "mrgumby",
kSecReturnData: true,
] as NSMutableDictionary
var err = SecItemCopyMatching(dict, ©Result)
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, ©Result)
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, ©Result)
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.
Understand These Key Attributes
Key items have a number of attributes that are similarly named, and it’s important to keep them straight. I created a cheat sheet for this, namely, SecItem attributes for keys. You wouldn’t believe how often I consult this!
Investigating Complex Attributes
Some attributes have values where the format is not obvious. For example, the kSecAttrIssuer attributed is documented as:
The corresponding value is of type CFData and contains the X.500
issuer name of a certificate.
What exactly does that mean? If I want to search the keychain for all certificates issued by a specific certificate authority, what value should I supply?
One way to figure this out is to add a certificate to the keychain, read the attributes back, and then dump the kSecAttrIssuer value. For example:
let cert: SecCertificate = …
let attrs = try secCall { SecItemAdd([
kSecValueRef: cert,
kSecReturnAttributes: true,
] as NSDictionary, $0) } as! [String: Any]
let issuer = attrs[kSecAttrIssuer as String] as! NSData
print((issuer as NSData).debugDescription)
// prints: <3110300e 06035504 030c074d 6f757365 4341310b 30090603 55040613 024742>
Those bytes represent the contents of a X.509 Name ASN.1 structure with DER encoding. This is without the outer SEQUENCE element, so if you dump it as ASN.1 you’ll get a nice dump of the first SET and then a warning about extra stuff at the end of the file:
% xxd issuer.asn1
00000000: 3110 300e 0603 5504 030c 074d 6f75 7365 1.0...U....Mouse
00000010: 4341 310b 3009 0603 5504 0613 0247 42 CA1.0...U....GB
% dumpasn1 -p issuer.asn1
SET {
SEQUENCE {
OBJECT IDENTIFIER commonName (2 5 4 3)
UTF8String 'MouseCA'
}
}
Warning: Further data follows ASN.1 data at position 18.
Note For details on the Name structure, see section 4.1.2.4 of RFC 5280.
Amusingly, if you run the same test against the file-based keychain you’ll… crash. OK, that’s not amusing. It turns out that the code above doesn’t work when targeting the file-based keychain because SecItemAdd doesn’t return a dictionary but rather an array of dictionaries (r. 21111543). Once you get past that, however, you’ll see it print:
<301f3110 300e0603 5504030c 074d6f75 73654341 310b3009 06035504 06130247 42>
Which is different! Dumping it as ASN.1 shows that it’s the full Name structure, including the outer SEQUENCE element:
% xxd issuer-file-based.asn1
00000000: 301f 3110 300e 0603 5504 030c 074d 6f75 0.1.0...U....Mou
00000010: 7365 4341 310b 3009 0603 5504 0613 0247 seCA1.0...U....G
00000020: 42 B
% dumpasn1 -p issuer-file-based.asn1
SEQUENCE {
SET {
SEQUENCE {
OBJECT IDENTIFIER commonName (2 5 4 3)
UTF8String 'MouseCA'
}
}
SET {
SEQUENCE {
OBJECT IDENTIFIER countryName (2 5 4 6)
PrintableString 'GB'
}
}
}
This difference in behaviour between the data protection and file-based keychains is a known bug (r. 26391756) but in this case it’s handy because the file-based keychain behaviour makes it easier to understand the data protection keychain behaviour.
Import, Then Add
It’s possible to import data directly into the keychain. For example, you might use this code to add a certificate:
let certData: Data = …
try secCall { SecItemAdd([
kSecClass: kSecClassCertificate,
kSecValueData: certData,
] as NSDictionary, nil)
}
However, it’s better to import the data and then add the resulting credential reference. For example:
let certData: Data = …
let cert = try secCall {
SecCertificateCreateWithData(nil, certData as NSData)
}
try secCall { SecItemAdd([
kSecValueRef: cert,
] as NSDictionary, nil)
}
There are two advantages to this:
If you get an error, you know whether the problem was with the import step or the add step.
It ensures that the resulting keychain item has the correct attributes.
This is especially important for keys. These can be packaged in a wide range of formats, so it’s vital to know whether you’re interpreting the key data correctly.
I see a lot of code that adds key data directly to the keychain. That’s understandable because, back in the day, this was the only way to import a key on iOS. Fortunately, that’s not been the case since the introduction of SecKeyCreateWithData in iOS 10 and aligned releases.
For more information about importing keys, see Importing Cryptographic Keys.
App Groups on the Mac
Sharing access to keychain items among a collection of apps explains that three entitlements determine your keychain access:
keychain-access-groups
application-identifier (com.apple.application-identifier on macOS)
com.apple.security.application-groups
In the discussion of com.apple.security.application-groups it says:
Starting in iOS 8, the array of strings given by this
entitlement also extends the list of keychain access groups.
That’s true, but it’s also potentially misleading. This affordance only works on iOS and its child platforms. It doesn’t work on macOS.
That’s because app groups work very differently on macOS than they do on iOS. For all the details, see App Groups: macOS vs iOS: Working Towards Harmony. However, the take-home point is that, when you use the data protection keychain on macOS, your keychain access group list is built from keychain-access-groups and com.apple.application-identifier.
Revision History
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.
We’ve identified an issue in our app where, upon clicking the "Call Customer Center" button, users are unexpectedly shown a logo and message option on a native pop-up window.
However, this wasn't the case before, and it should only display a phone number to dial, which was given inside our code.
This is incorrect and misleading for our users, as:
We are a Canadian-based service and have no affiliation with US messaging chat.
The messaging feature was never enabled or intended for our app.
Our app should only initiate a phone call to our customer support center — no messages or branding from third parties should appear
Topic:
Privacy & Security
SubTopic:
General
Hi everyone,
I'm encountering an issue where the background location indicator remains visible on the status bar even though I have set the location permissions to Never for my app in the system settings. Despite taking all the necessary steps to stop location tracking (including stopping updates, geofencing, and other location-related services), the indicator still appears. This seems to be a bug since everything has been turned off on my end.
Here’s what I’ve already tried:
Setting location permissions to Never in the settings.
Stopping startUpdatingLocation(), stopMonitoringSignificantLocationChanges(), and geofencing (using locationManager.stopMonitoringRegions()).
Calling locationManager.showsBackgroundLocationIndicator = false.
Ensuring that the CLLocationManager is fully invalidated.
Despite all of this, the background location indicator still remains in the status bar. I’ve tested it on real devices, as well as in the simulator, with no improvement.
Has anyone experienced something similar, or can suggest why this might be happening? Could this be related to an iOS 18+ issue?
Any insights or guidance would be greatly appreciated.
Hi,
Just follow the related post to implement this method in the app, but it gave me error, like: "An SSL error has occurred and a secure connection to the server cannot be made"
the info plist configuration like below,
NSPinnedDomains
mysite.com
NSIncludesSubdomains
NSPinnedCAIdentities
SPKI-SHA256-BASE64
r/mIkG3eEpVdm+u/ko/cwxzOMo1bk4TyHIlByibiA5E=
The pub key is right for me, since it works when I use different pub key pinning through URLSession interface.
So here, I dont know where to start the troubleshooting, any advice would be appreciated.
Topic:
Privacy & Security
SubTopic:
General
I have add my domani and email address to Configure Sign in with Apple for Email Communication (https://developer.apple.com/account/resources/services/configure)
and it pass SPF already but when it send from server that i setup is had "Error Description : Permanament error. Please do not try again, according to the information returned by the other party to confirm the specific cause of the error. Cause:550 5.1.1 : unauthorized sender"
a mail service is on Alibaba Cloud the email that i want to sending to is ending with @privaterelay.appleid.com
it that have any solve problem or i missing any thing else ?
General:
Forums topic: Privacy & Security
Apple Platform Security support document
Developer > Security
Security Audit Thoughts forums post
Cryptography:
Forums tags: Security, Apple CryptoKit
Security framework documentation
Apple CryptoKit framework documentation
Common Crypto man pages — For the full list of pages, run:
% man -k 3cc
For more information about man pages, see Reading UNIX Manual Pages.
On Cryptographic Key Formats forums post
SecItem attributes for keys forums post
CryptoCompatibility sample code
Keychain:
Forums tags: Security
Security > Keychain Items documentation
TN3137 On Mac keychain APIs and implementations
SecItem Fundamentals forums post
SecItem Pitfalls and Best Practices forums post
Investigating hard-to-reproduce keychain problems forums post
App ID Prefix Change and Keychain Access forums post
Smart cards and other secure tokens:
Forums tag: CryptoTokenKit
CryptoTokenKit framework documentation
Mac-specific resources:
Forums tags: Security Foundation, Security Interface
Security Foundation framework documentation
Security Interface framework documentation
BSD Privilege Escalation on macOS
Related:
Networking Resources — This covers high-level network security, including HTTPS and TLS.
Network Extension Resources — This covers low-level network security, including VPN and content filters.
Code Signing Resources
Notarisation Resources
Trusted Execution Resources — This includes Gatekeeper.
App Sandbox Resources
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
I am developing apps using NWJS framework, which access devices on the local network. I am doing this on Sequoia on Macos (Desktop). I have developed other apps using NWJS before, but on earlier versions of Macos.
My issue is, I am unable to give my app permission to app to access devices on local network on one of the apps.
Some background:
Other apps which I have used which access devices on the local network, on first-time launching, have given a prompt asking me if I want to allow or deny access to local device for the app.
However, on first-time launching (and many others after that), It simply says the device cannot be reached, and I never get a prompt asking me if I want to allow or deny access to local device for my app.
In its barebones proof-of-concept stage of my app, I have an iframe who's src attribute is the IP address of a device known on the network with that address. I have tried the protocol https://192.168.1.99 and http://192.168.1.99 in the src attribute. This protocol works in another app I have built where upon first-time launch, I was able to get a prompt and give it the needed permission.
If I check in System Settings > Privacy and Security > Network, the app doesn't appear where I can toggle a setting. I also am unable to explicitly add my app to the list.
**
This worked for one app, but not another:
In researching this issue, it was recommended that I add the following keys in info.plist:
com.apple.developer.networking.multicast - boolean true
NSLocalNetworkUsageDescription - string description
NSNearbyInteractionUsageDescription - string description
This worked for one of my apps, but not another, which has a nearly identical structure. In fact, other than CFBundleIdentifier, CFBundleDisplayName and CFBundleName, info.plist is identical.
Why did this work one time, and how can I get my app to prompt for permission for local network access?
Our company developed an app that relies on the collected list to display the phone's label in the list when the user's phone receives an incoming call. However, we have been rejected. The main reason for the rejection is as follows:
“ Guideline 1.1.6 - Safety - Objectionable Content
The app still allows users to unblock and reveal blocked incoming numbers to identify the individual calling or texting, which is not appropriate. Specifically, your app claims to offer the call blocking functionality, but solely identifies numbers that the user has explicitly blocked themselves.
Since users can choose to hide their caller ID in iPhone Settings, apps should not attempt to circumvent this iOS feature to reveal the caller's number. ”
But our developers clearly stated that there is no way to bypass these settings, and CallDirectory Extensionde cannot directly "unlock hidden numbers" or bypass the built-in restrictions of IOS. We don't know how to solve this problem next, and hope to get everyone's help.
Topic:
Privacy & Security
SubTopic:
General
Hi,
I have a question about UIDevice identifierForVendor.
I am distributing 3 apps using an enterprise account. All apps use the same developer account and certificates.
The bundle IDs of the apps are as follows:
com.abc.inhouse.mail
com.abc.searchent
com.abc.noteent
In the Enterprise builds, apps 1 and 2 share the same identifierForVendor (IDFV). However, app 3 has a different IDFV value.
According to Apple documentation, the IDFV is determined based on the bundle ID when distributing through Enterprise.
Why does app 3 have a different IDFV?
Are there any other factors besides the bundle ID that affect the IDFV in Enterprise builds?
Please help me figure this out.
Thank you for your time!
Topic:
Privacy & Security
SubTopic:
General
Hi I am currently developping the "Sign in with apple" feature.
We set up everything according to the documentation :
https://developer.apple.com/help/account/configure-app-capabilities/configure-private-email-relay-service
When trying to send an email from one of the registered communication emails (that is SPF and DKIM Authentication compliant) the emails are still ending up in the spam box.
If it can help the received email address (that is hidden) is a gmail.
I can not catch what is missing/wrong on our side.
AFAIU a new screen capture notification was added within the Control Center in Sequoia 15.2, see attached examples:
My question is whether there is some way to suppress this notification preferably via an MDM configuration profile.
See also https://discussions.apple.com/thread/255886645?sortBy=rank for more information.
Thanks,
Doron.
Hello, I'm having a problem with taxes calculation for US, the thing is, for some very specific addresses the taxes are coming wrong because we don't have the full address before the authorization with Adyen (our payment provider), for example, when I put the address "1 Infinite Loop, CA, US, 95014" in my wallet, I'm not receiving the value "1 Infinite Loop" (addressLines[0]) in backend until we authorize in Adyen, but I need this field to calculate the taxes and show to the customer before the authorization. My question is, is there any way to have this field before the authorization? If not, you have any idea how other ecommerces that use ApplePay and Adyen handle with this problem?
Thanks in advance!
Topic:
Privacy & Security
SubTopic:
General
I was in the webview long according to the pictures, and then select "save image" will lead to collapse, I have passed on the info plist add NSPhotoLibraryAddUsageDescription solved it.
Now I have another question, does the last item "query" in the screenshot have a similar problem? Since I couldn't click the button (I don't know why this happened), I couldn't test it on the real machine.
Hello! Few month ago i did get hacked on my pc and then my android and iphone. Did get at notice that payments couldent draw. lucky I only had 240kr on lunar card that it did draw 200kr to a gift card. Did get mail from skrill that a account whas created with one of my Gmail’s. Tryed to log them out but window did keep close. Gmail did flag like crazy and wanted me to change pw. how the fuck when I lost control of my phone?!?!??! Just lock it god Damn. let’s make it short! I shared network to pc from my phone With usb. I don’t just think it whas a attacker program as Gmail did flag. I think I did get mirror linked on my android and maybe my iphone. Had a real struggle to reset my pc and phones before it worked. My iPhone drains battery like crazy and feels laggy sometimes. A non registered number whas added to two Gmail’s that they did try to change pw multiple times. did notice I Linux pc activity on my fb and some other stuff. My iphone do reboot still sometimes and every second reboot wifi/bluet can’t be activated and mobile share change pw as the original one did look. Next reboot all work and are the same again. Iam scared that iam still hacked or havent removed him from everything. How can I make sure that Iam still not mirror linked and that he or she can’t access anything? Sorry for the long text but iam scared as fuck.
Topic:
Privacy & Security
SubTopic:
General
Hi all, I've on high alert after hearing about the security concerns with npm. Full disclosure, I'm new to computer and network architecture, however, as someone who is on high alert for aplications exfiltrating data or poisioning my on-device machine learning models — I've seen some things I can't fully explain and I'm hoping the community can help.
I ran the code odutil show all and I was wondering why certain node names are hidden in my system and when I use the directory utility, I can't use my computer login and password to authenticate to see the users? Am I being locked out of seeing my own system? I'm trying to dig to see if a root kit was installed on my device.
Does anyone know what the users and groups in the directory utility are? Who is "nobody" and who is "Unknown user"? I'll probably have a lot more questions about this suspicious files I've seen on my device. Does anyone else's device download machine learning model payloads from the internet without notifying the user (even through a firewall, no startup applications?). I've also tried deleting applications I no longer need anymore and my "system" makes them re-appear.... what?
Topic:
Privacy & Security
SubTopic:
General
If we record the user's Device Model (ie. iPhone 15), what checkbox do we need to select under Data Collection in App Privacy?
Device model is not a unique identifier, we do not use it for tracking. We use it to know in aggregate which phone models are using our app the most so we can prioritize our QA to focus on the top devices.
Please note: we DO NOT access Device ID, as we DO NOT use it.
hello,
My organization has an outlook add-in that requires auth into our platform. As Microsoft forces Auth on MacOS to use WKWebView https://learn.microsoft.com/en-us/office/dev/add-ins/concepts/browsers-used-by-office-web-add-ins, we are running into a situation that we cannot use passkeys as an auth method as we are unable to trigger WebAuthN flows.
We’ve raised this in Microsoft side but they have deferred to Apple given WKWebView is Safari based.
This is a big blocker for us to achieve a full passwordless future. Has anyone come across this situation?
Thank you.
Topic:
Privacy & Security
SubTopic:
General
Tags:
Passkeys in iCloud Keychain
Authentication Services
WebKit
Hi everyone,
I’m developing a health-related mobile app and considering using EAS Update to deliver over-the-air (OTA) updates for JavaScript code and assets. Before implementing this, I want to ensure that this approach complies with Apple App Store policies, especially given the sensitivity of health-related apps.
Here are my concerns:
Does using EAS Update (OTA) align with Apple’s guidelines regarding app updates and dynamic behavior changes?
Are there specific rules or restrictions for health apps using OTA updates that I should be cautious of?
Could this approach be flagged as violating Apple’s policies on app integrity, especially those requiring updates to go through the App Store review process?
I’d greatly appreciate any insights, advice, or references to Apple’s official documentation regarding OTA updates for apps distributed through the App Store.
Thanks in advance for your help!
Hi,
I know it's been discussed before, but I'm testing the Sign in with Apple feature, and I only get the user info on the first try.
Now, I know that you're supposed to go to the account settings, and look for the list of accounts that you used your Apple account to sign in with, and it used to work a few months back. But for the last few weeks I haven't been able to get the user info, even after deleting the entry from my Sign In With Apple app list.
Has there been a recent change to Apple security policy that prevents such a move from working ? Or am I doing something wrong ?
Thank you