Certificate Pinning? 'This certificate is marked as trusted for "<IP address>"'

I'm on macOS 10.15.6. My application is 100% Swift right now, but I'm comfortable enough with Objective-C if that is needed.

When I visit a page using a self-signed certificate in Safari, I get the expected "This Connection Is Not Private" message. If I hit Show Details > visit this website > Visit Website, the certificate is added to my Keychain. It has a little blue circle with a white + in it. When I double-click the certificate, it gives me the details. One of them, just under the expiration date at the top, is:

This certificate is marked as trusted for "<IP address>"

Is there a public API to add certificates like this with my own software? Or to add this property to an existing certificate?

I'm making a tool which talks to an API over HTTPS. Some of the servers running this API use the same certificate for a web UI to manage the application. When I visit the page in Safari and trust the certificate there, my application trusts it with no further fuss. I would like the same behavior in the other direction. If a user happens to connect to the server with my application first, I would like to add the certificate to the Keychain in such a way that Safari would also be able to use it for the same server.

Accepted Reply

That got me where I needed to be. Thank you!

To save others time, here's what I ended up writing:
Code Block Swift
func addTrustForCertificate(_ serverCertificate:SecCertificate, host:String) {
let serverCertDictionary:CFDictionary = [
kSecClass:kSecClassCertificate,
kSecValueRef:serverCertificate
] as [CFString:Any] as CFDictionary
let secItemAddError:OSStatus = SecItemAdd(serverCertDictionary, nil)
switch secItemAddError {
case noErr:
break
default:
let errorMessage:String = SecCopyErrorMessageString(secItemAddError,nil)
print("addTrustForCertificate SecItemAdd error: \(String(describing: errorMessage))")
}
let secPolicyToSet:SecPolicy = SecPolicyCreateSSL(true, nil)
let SecTrustDict1:CFDictionary = [
"kSecTrustSettingsAllowedError":CSSMERR_TP_CERT_EXPIRED,
"kSecTrustSettingsPolicy":secPolicyToSet,
"kSecTrustSettingsPolicyName":"sslServer",
"kSecTrustSettingsPolicyString":host,
"kSecTrustSettingsResult":1
] as CFDictionary
let SecTrustDict2:CFDictionary = [
"kSecTrustSettingsAllowedError":-2147408896,
"kSecTrustSettingsPolicy":secPolicyToSet,
"kSecTrustSettingsPolicyName":"sslServer",
"kSecTrustSettingsPolicyString":host,
"kSecTrustSettingsResult":1
] as CFDictionary
let trustSettings:CFArray = [SecTrustDict1,SecTrustDict2] as CFArray
let trustSettingsError:OSStatus = SecTrustSettingsSetTrustSettings(serverCertificate, .user, trustSettings)
switch trustSettingsError {
case noErr:
break
default:
let errorMessage:String = SecCopyErrorMessageString(trustSettingsError,nil)
print("addTrustForCertificate SecTrustSettingsSetTrustSettings error: \(String(describing: errorMessage))")
}
}

Lines 2 through 13 import the certificate into the Keychain. Lines 15 through 38 set the trust overrides. Explicit types because I don't mess around with type inference in even vaguely-security-related code.

CSSMERR_TP_CERT_EXPIRED is error -2147409654 (cssmerr.h). I'm not sure what error -2147408896 is. Haven't found it in an error table yet. I got the two values by examining some existing self-signed certificates I trusted in Safari.

Replies

Is there a public API to add certificates like this with my own
software?

Yes, that’d be the SecTrustSettings API. See the Trust Settings group on this page.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@apple.com"
That got me where I needed to be. Thank you!

To save others time, here's what I ended up writing:
Code Block Swift
func addTrustForCertificate(_ serverCertificate:SecCertificate, host:String) {
let serverCertDictionary:CFDictionary = [
kSecClass:kSecClassCertificate,
kSecValueRef:serverCertificate
] as [CFString:Any] as CFDictionary
let secItemAddError:OSStatus = SecItemAdd(serverCertDictionary, nil)
switch secItemAddError {
case noErr:
break
default:
let errorMessage:String = SecCopyErrorMessageString(secItemAddError,nil)
print("addTrustForCertificate SecItemAdd error: \(String(describing: errorMessage))")
}
let secPolicyToSet:SecPolicy = SecPolicyCreateSSL(true, nil)
let SecTrustDict1:CFDictionary = [
"kSecTrustSettingsAllowedError":CSSMERR_TP_CERT_EXPIRED,
"kSecTrustSettingsPolicy":secPolicyToSet,
"kSecTrustSettingsPolicyName":"sslServer",
"kSecTrustSettingsPolicyString":host,
"kSecTrustSettingsResult":1
] as CFDictionary
let SecTrustDict2:CFDictionary = [
"kSecTrustSettingsAllowedError":-2147408896,
"kSecTrustSettingsPolicy":secPolicyToSet,
"kSecTrustSettingsPolicyName":"sslServer",
"kSecTrustSettingsPolicyString":host,
"kSecTrustSettingsResult":1
] as CFDictionary
let trustSettings:CFArray = [SecTrustDict1,SecTrustDict2] as CFArray
let trustSettingsError:OSStatus = SecTrustSettingsSetTrustSettings(serverCertificate, .user, trustSettings)
switch trustSettingsError {
case noErr:
break
default:
let errorMessage:String = SecCopyErrorMessageString(trustSettingsError,nil)
print("addTrustForCertificate SecTrustSettingsSetTrustSettings error: \(String(describing: errorMessage))")
}
}

Lines 2 through 13 import the certificate into the Keychain. Lines 15 through 38 set the trust overrides. Explicit types because I don't mess around with type inference in even vaguely-security-related code.

CSSMERR_TP_CERT_EXPIRED is error -2147409654 (cssmerr.h). I'm not sure what error -2147408896 is. Haven't found it in an error table yet. I got the two values by examining some existing self-signed certificates I trusted in Safari.
I accidentally marked my reply as the solution, when I meant to mark Quinn's. Now I don't see a way to undo that. Weird.

I'm not sure what error -2147408896 is. Haven't found it in an error
table yet.

Code Block
% security error -2147408896
Error: 0x80012400 -2147408896 Host name mismatch


Share and Enjoy

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