iOS 中的客户端证书和身份
Client certificates and identities in iOS
我已经使用 SecKeyGeneratePair
函数为基于 Swift 的 iOS 应用程序生成了私钥和 public 密钥。
然后,我生成了证书使用 iOS CSR generation 签名请求,我的服务器用 PEM 格式的证书链回复。
我使用以下代码将 PEM 证书转换为 DER 格式:
var modifiedCert = certJson.replacingOccurrences(of: "-----BEGIN CERTIFICATE-----", with: "")
modifiedCert = modifiedCert.replacingOccurrences(of: "-----END CERTIFICATE-----", with: "")
modifiedCert = modifiedCert.replacingOccurrences(of: "\n", with: "")
let dataDecoded = NSData(base64Encoded: modifiedCert, options: [])
现在,我应该使用 let certificate = SecCertificateCreateWithData(nil, certDer)
从 DER 数据创建证书
我的问题如下:如何将证书与我在开始时创建的私钥连接起来,并获得这两者(密钥和证书)所属的身份?
也许,将证书添加到钥匙串并使用 SecItemCopyMatching
获取身份?我已按照问题中提出的程序进行操作 SecIdentityRef procedure
编辑:
将证书添加到钥匙串时,我得到状态响应 0,我认为这意味着证书已添加到钥匙串。
let certificate: SecCertificate? = SecCertificateCreateWithData(nil, certDer)
if certificate != nil{
let params : [String: Any] = [
kSecClass as String : kSecClassCertificate,
kSecValueRef as String : certificate!
]
let status = SecItemAdd(params as CFDictionary, &certRef)
print(status)
}
现在,当我尝试获取身份时,我得到状态 -25300 (errSecItemNotFound)。以下代码用于获取身份。标签是我用来生成 private/public 密钥的私钥标签。
let query: [String: Any] = [
kSecClass as String : kSecClassIdentity,
kSecAttrApplicationTag as String : tag,
kSecReturnRef as String: true
]
var retrievedData: SecIdentity?
var extractedData: AnyObject?
let status = SecItemCopyMatching(query as NSDictionary, &extractedData)
if (status == errSecSuccess) {
retrievedData = extractedData as! SecIdentity?
}
我可以使用 SecItemCopyMatching 从钥匙串中获取私钥和 public 钥匙和证书,并将证书添加到钥匙串,但查询 SecIdentity 不起作用。我的证书是否可能与我的密钥不匹配?如何检查?
我以 base64 格式打印了来自 iOS 的 public 密钥。打印了以下内容:
MIIBCgKCAQEAo/MRST9oZpO3nTl243o+ocJfFCyKLtPgO/QiO9apb2sWq4kqexHy
58jIehBcz4uGJLyKYi6JHx/NgxdSRKE3PcjU2sopdMN35LeO6jZ34auH37gX41Sl
4HWkpMOB9v/OZvMoKrQJ9b6/qmBVZXYsrSJONbr+74/mI/m1VNtLOM2FIzewVYcL
HHsM38XOg/kjSUsHEUKET/FfJkozgp76r0r3E0khcbxwU70qc77YPgeJHglHcZKF
ZHFbvNz4E9qUy1mWJvoCmAEItWnyvuw+N9svD1Rri3t5qlaBwaIN/AtayHwJWoWA
/HF+Jg87eVvEErqeT1wARzJL2xv5V1O4ZwIDAQAB
然后从证书签名请求中,我使用 openssl (openssl req -in ios.csr -pubkey -noout) 提取了 public 密钥。打印了以下响应:
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAo/MRST9oZpO3nTl243o+
ocJfFCyKLtPgO/QiO9apb2sWq4kqexHy58jIehBcz4uGJLyKYi6JHx/NgxdSRKE3
PcjU2sopdMN35LeO6jZ34auH37gX41Sl4HWkpMOB9v/OZvMoKrQJ9b6/qmBVZXYs
rSJONbr+74/mI/m1VNtLOM2FIzewVYcLHHsM38XOg/kjSUsHEUKET/FfJkozgp76
r0r3E0khcbxwU70qc77YPgeJHglHcZKFZHFbvNz4E9qUy1mWJvoCmAEItWnyvuw+
N9svD1Rri3t5qlaBwaIN/AtayHwJWoWA/HF+Jg87eVvEErqeT1wARzJL2xv5V1O4
ZwIDAQAB
-----END PUBLIC KEY----
CSR生成的密钥开头似乎有细微差别。 (MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A)。根据问题 RSA encryption,MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A 似乎是 RSA 加密“1.2.840.113549.1.1.1”的 base64 格式标识符。所以我想 public 键可能没问题?
生成SSL证书的常用方法是使用私钥生成CSR,证书签名请求信息。事实上,您还使用该调号隐藏了公司、电子邮件等信息。然后,使用该 CSR,您签署证书,因此它将与您的私钥和存储在 CSR 中的信息相关联,更不用说 public 密钥了。我目前无法在 IOS CSR 生成项目中看到您可以在其中传递生成的密钥:在我看来,使用 IOS CSR 生成项目生成的 CSR 正在使用它自己生成的密钥,或者没有私有密钥关键。这将得到逻辑,因为您无法从 CER 或 DER 中提取私钥,因为它不存在。
我们不使用相同的 CSR 方法,但我们有一个等效的方法,我们执行以下操作:
- 生成密钥对
- 将 public 密钥发送到远程服务器
- 远程服务器使用 public 密钥生成签名的客户端证书
- 将客户端证书发送回 iOS 设备
- 将客户端证书添加到钥匙串
- 稍后,在 NSURLSession 或类似的程序中使用客户端证书。
您似乎已经发现,iOS 需要这个叫做 "identity" 的额外东西来绑定客户端证书。
我们还发现 iOS 有一个奇怪的事情,您需要在将客户端证书和身份添加到其中之前从钥匙串中删除 public 密钥,否则身份不会似乎正确地找到了客户端证书。我们选择重新添加 public 键,但作为 "generic password"(即任意用户数据)——我们这样做只是因为 iOS 没有合理的 API即时从证书中提取 public 密钥,我们需要 public 密钥来处理我们碰巧正在做的其他奇怪的事情。
如果您只是进行 TLS 客户端证书身份验证,一旦您拥有证书,您将不需要 public 密钥的显式副本,因此您可以通过简单地删除它来简化过程,然后跳过"add-back-in-as-generic-password"位
请原谅一大堆代码,加密的东西似乎总是需要很多工作。
下面是执行上述任务的一些代码:
生成密钥对,deleting/re-saving public 密钥
/// Returns the public key binary data in ASN1 format (DER encoded without the key usage header)
static func generateKeyPairWithPublicKeyAsGenericPassword(privateKeyTag: String, publicKeyAccount: String, publicKeyService: String) throws -> Data {
let tempPublicKeyTag = "TMPPUBLICKEY:\(privateKeyTag)" // we delete this public key and replace it with a generic password, but it needs a tag during the transition
let privateKeyAttr: [NSString: Any] = [
kSecAttrApplicationTag: privateKeyTag.data(using: .utf8)!,
kSecAttrAccessible: kSecAttrAccessibleAlwaysThisDeviceOnly,
kSecAttrIsPermanent: true ]
let publicKeyAttr: [NSString: Any] = [
kSecAttrApplicationTag: tempPublicKeyTag.data(using: .utf8)!,
kSecAttrAccessible: kSecAttrAccessibleAlwaysThisDeviceOnly,
kSecAttrIsPermanent: true ]
let keyPairAttr: [NSString: Any] = [
kSecAttrKeyType: kSecAttrKeyTypeRSA,
kSecAttrKeySizeInBits: 2048,
kSecPrivateKeyAttrs: privateKeyAttr,
kSecPublicKeyAttrs: publicKeyAttr ]
var publicKey: SecKey?, privateKey: SecKey?
let genKeyPairStatus = SecKeyGeneratePair(keyPairAttr as CFDictionary, &publicKey, &privateKey)
guard genKeyPairStatus == errSecSuccess else {
log.error("Generation of key pair failed. Error = \(genKeyPairStatus)")
throw KeychainError.generateKeyPairFailed(genKeyPairStatus)
}
// Would need CFRelease(publicKey and privateKey) here but swift does it for us
// we store the public key in the keychain as a "generic password" so that it doesn't interfere with retrieving certificates
// The keychain will normally only store the private key and the certificate
// As we want to keep a reference to the public key itself without having to ASN.1 parse it out of the certificate
// we can stick it in the keychain as a "generic password" for convenience
let findPubKeyArgs: [NSString: Any] = [
kSecClass: kSecClassKey,
kSecValueRef: publicKey!,
kSecAttrKeyType: kSecAttrKeyTypeRSA,
kSecReturnData: true ]
var resultRef:AnyObject?
let status = SecItemCopyMatching(findPubKeyArgs as CFDictionary, &resultRef)
guard status == errSecSuccess, let publicKeyData = resultRef as? Data else {
log.error("Public Key not found: \(status))")
throw KeychainError.publicKeyNotFound(status)
}
// now we have the public key data, add it in as a generic password
let attrs: [NSString: Any] = [
kSecClass: kSecClassGenericPassword,
kSecAttrAccessible: kSecAttrAccessibleAlwaysThisDeviceOnly,
kSecAttrService: publicKeyService,
kSecAttrAccount: publicKeyAccount,
kSecValueData: publicKeyData ]
var result: AnyObject?
let addStatus = SecItemAdd(attrs as CFDictionary, &result)
if addStatus != errSecSuccess {
log.error("Adding public key to keychain failed. Error = \(addStatus)")
throw KeychainError.cannotAddPublicKeyToKeychain(addStatus)
}
// delete the "public key" representation of the public key from the keychain or it interferes with looking up the certificate
let pkattrs: [NSString: Any] = [
kSecClass: kSecClassKey,
kSecValueRef: publicKey! ]
let deleteStatus = SecItemDelete(pkattrs as CFDictionary)
if deleteStatus != errSecSuccess {
log.error("Deletion of public key from keychain failed. Error = \(deleteStatus)")
throw KeychainError.cannotDeletePublicKeyFromKeychain(addStatus)
}
// no need to CFRelease, swift does this.
return publicKeyData
}
请注意,publicKeyData 并非严格采用 DER 格式,而是 "DER with the first 24 bytes trimmed off" 格式。我不确定这被正式称为什么,但微软和苹果似乎都将它用作 public 键的原始格式。如果您的服务器是 Microsoft 运行 .NET(桌面或核心),那么它可能会对 public 关键字节 as-is 感到满意。如果它是 Java 并且需要 DER,您可能需要生成 DER header - 这是一个固定的 24 字节序列,您可以将其连接起来。
将客户端证书添加到钥匙串,生成身份
static func addIdentity(clientCertificate: Data, label: String) throws {
log.info("Adding client certificate to keychain with label \(label)")
guard let certificateRef = SecCertificateCreateWithData(kCFAllocatorDefault, clientCertificate as CFData) else {
log.error("Could not create certificate, data was not valid DER encoded X509 cert")
throw KeychainError.invalidX509Data
}
// Add the client certificate to the keychain to create the identity
let addArgs: [NSString: Any] = [
kSecClass: kSecClassCertificate,
kSecAttrAccessible: kSecAttrAccessibleAlwaysThisDeviceOnly,
kSecAttrLabel: label,
kSecValueRef: certificateRef,
kSecReturnAttributes: true ]
var resultRef: AnyObject?
let addStatus = SecItemAdd(addArgs as CFDictionary, &resultRef)
guard addStatus == errSecSuccess, let certAttrs = resultRef as? [NSString: Any] else {
log.error("Failed to add certificate to keychain, error: \(addStatus)")
throw KeychainError.cannotAddCertificateToKeychain(addStatus)
}
// Retrieve the client certificate issuer and serial number which will be used to retrieve the identity
let issuer = certAttrs[kSecAttrIssuer] as! Data
let serialNumber = certAttrs[kSecAttrSerialNumber] as! Data
// Retrieve a persistent reference to the identity consisting of the client certificate and the pre-existing private key
let copyArgs: [NSString: Any] = [
kSecClass: kSecClassIdentity,
kSecAttrIssuer: issuer,
kSecAttrSerialNumber: serialNumber,
kSecReturnPersistentRef: true] // we need returnPersistentRef here or the keychain makes a temporary identity that doesn't stick around, even though we don't use the persistentRef
let copyStatus = SecItemCopyMatching(copyArgs as CFDictionary, &resultRef);
guard copyStatus == errSecSuccess, let _ = resultRef as? Data else {
log.error("Identity not found, error: \(copyStatus) - returned attributes were \(certAttrs)")
throw KeychainError.cannotCreateIdentityPersistentRef(addStatus)
}
// no CFRelease(identityRef) due to swift
}
在我们的代码中,我们选择 return 一个标签,然后使用该标签和以下代码查找身份 as-required。您还可以选择仅 return 来自上述函数的身份引用而不是标签。无论如何,这是我们的 getIdentity 函数
稍后获取身份
// Remember any OBJECTIVE-C code that calls this method needs to call CFRetain
static func getIdentity(label: String) -> SecIdentity? {
let copyArgs: [NSString: Any] = [
kSecClass: kSecClassIdentity,
kSecAttrLabel: label,
kSecReturnRef: true ]
var resultRef: AnyObject?
let copyStatus = SecItemCopyMatching(copyArgs as CFDictionary, &resultRef)
guard copyStatus == errSecSuccess else {
log.error("Identity not found, error: \(copyStatus)")
return nil
}
// back when this function was all ObjC we would __bridge_transfer into ARC, but swift can't do that
// It wants to manage CF types on it's own which is fine, except they release when we return them out
// back into ObjC code.
return (resultRef as! SecIdentity)
}
// Remember any OBJECTIVE-C code that calls this method needs to call CFRetain
static func getCertificate(label: String) -> SecCertificate? {
let copyArgs: [NSString: Any] = [
kSecClass: kSecClassCertificate,
kSecAttrLabel: label,
kSecReturnRef: true]
var resultRef: AnyObject?
let copyStatus = SecItemCopyMatching(copyArgs as CFDictionary, &resultRef)
guard copyStatus == errSecSuccess else {
log.error("Identity not found, error: \(copyStatus)")
return nil
}
// back when this function was all ObjC we would __bridge_transfer into ARC, but swift can't do that
// It wants to manage CF types on it's own which is fine, except they release when we return them out
// back into ObjC code.
return (resultRef as! SecCertificate)
}
最后
使用身份验证服务器
这一点在 objc 中,因为我们的应用恰好就是这样工作的,但你明白了:
SecIdentityRef _clientIdentity = [XYZ getClientIdentityWithLabel: certLabel];
if(_clientIdentity) {
CFRetain(_clientIdentity);
}
SecCertificateRef _clientCertificate = [XYZ getClientCertificateWithLabel:certLabel];
if(_clientCertificate) {
CFRetain(_clientCertificate);
}
...
- (void)URLSession:(nullable NSURLSession *)session
task:(nullable NSURLSessionTask *)task
didReceiveChallenge:(nullable NSURLAuthenticationChallenge *)challenge
completionHandler:(nullable void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {
if (challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodClientCertificate) {
// supply the appropriate client certificate
id bridgedCert = (__bridge id)_clientCertificate;
NSArray* certificates = bridgedCert ? @[bridgedCert] : @[];
NSURLCredential* credential = [NSURLCredential credentialWithIdentity:identity certificates:certificates persistence:NSURLCredentialPersistenceForSession];
completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
}
}
这段代码花了很多时间才正确。 iOS 证书内容的记录非常少,希望这会有所帮助。
我已经使用 SecKeyGeneratePair
函数为基于 Swift 的 iOS 应用程序生成了私钥和 public 密钥。
然后,我生成了证书使用 iOS CSR generation 签名请求,我的服务器用 PEM 格式的证书链回复。
我使用以下代码将 PEM 证书转换为 DER 格式:
var modifiedCert = certJson.replacingOccurrences(of: "-----BEGIN CERTIFICATE-----", with: "")
modifiedCert = modifiedCert.replacingOccurrences(of: "-----END CERTIFICATE-----", with: "")
modifiedCert = modifiedCert.replacingOccurrences(of: "\n", with: "")
let dataDecoded = NSData(base64Encoded: modifiedCert, options: [])
现在,我应该使用 let certificate = SecCertificateCreateWithData(nil, certDer)
我的问题如下:如何将证书与我在开始时创建的私钥连接起来,并获得这两者(密钥和证书)所属的身份?
也许,将证书添加到钥匙串并使用 SecItemCopyMatching
获取身份?我已按照问题中提出的程序进行操作 SecIdentityRef procedure
编辑:
将证书添加到钥匙串时,我得到状态响应 0,我认为这意味着证书已添加到钥匙串。
let certificate: SecCertificate? = SecCertificateCreateWithData(nil, certDer)
if certificate != nil{
let params : [String: Any] = [
kSecClass as String : kSecClassCertificate,
kSecValueRef as String : certificate!
]
let status = SecItemAdd(params as CFDictionary, &certRef)
print(status)
}
现在,当我尝试获取身份时,我得到状态 -25300 (errSecItemNotFound)。以下代码用于获取身份。标签是我用来生成 private/public 密钥的私钥标签。
let query: [String: Any] = [
kSecClass as String : kSecClassIdentity,
kSecAttrApplicationTag as String : tag,
kSecReturnRef as String: true
]
var retrievedData: SecIdentity?
var extractedData: AnyObject?
let status = SecItemCopyMatching(query as NSDictionary, &extractedData)
if (status == errSecSuccess) {
retrievedData = extractedData as! SecIdentity?
}
我可以使用 SecItemCopyMatching 从钥匙串中获取私钥和 public 钥匙和证书,并将证书添加到钥匙串,但查询 SecIdentity 不起作用。我的证书是否可能与我的密钥不匹配?如何检查?
我以 base64 格式打印了来自 iOS 的 public 密钥。打印了以下内容:
MIIBCgKCAQEAo/MRST9oZpO3nTl243o+ocJfFCyKLtPgO/QiO9apb2sWq4kqexHy
58jIehBcz4uGJLyKYi6JHx/NgxdSRKE3PcjU2sopdMN35LeO6jZ34auH37gX41Sl
4HWkpMOB9v/OZvMoKrQJ9b6/qmBVZXYsrSJONbr+74/mI/m1VNtLOM2FIzewVYcL
HHsM38XOg/kjSUsHEUKET/FfJkozgp76r0r3E0khcbxwU70qc77YPgeJHglHcZKF
ZHFbvNz4E9qUy1mWJvoCmAEItWnyvuw+N9svD1Rri3t5qlaBwaIN/AtayHwJWoWA
/HF+Jg87eVvEErqeT1wARzJL2xv5V1O4ZwIDAQAB
然后从证书签名请求中,我使用 openssl (openssl req -in ios.csr -pubkey -noout) 提取了 public 密钥。打印了以下响应:
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAo/MRST9oZpO3nTl243o+
ocJfFCyKLtPgO/QiO9apb2sWq4kqexHy58jIehBcz4uGJLyKYi6JHx/NgxdSRKE3
PcjU2sopdMN35LeO6jZ34auH37gX41Sl4HWkpMOB9v/OZvMoKrQJ9b6/qmBVZXYs
rSJONbr+74/mI/m1VNtLOM2FIzewVYcLHHsM38XOg/kjSUsHEUKET/FfJkozgp76
r0r3E0khcbxwU70qc77YPgeJHglHcZKFZHFbvNz4E9qUy1mWJvoCmAEItWnyvuw+
N9svD1Rri3t5qlaBwaIN/AtayHwJWoWA/HF+Jg87eVvEErqeT1wARzJL2xv5V1O4
ZwIDAQAB
-----END PUBLIC KEY----
CSR生成的密钥开头似乎有细微差别。 (MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A)。根据问题 RSA encryption,MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A 似乎是 RSA 加密“1.2.840.113549.1.1.1”的 base64 格式标识符。所以我想 public 键可能没问题?
生成SSL证书的常用方法是使用私钥生成CSR,证书签名请求信息。事实上,您还使用该调号隐藏了公司、电子邮件等信息。然后,使用该 CSR,您签署证书,因此它将与您的私钥和存储在 CSR 中的信息相关联,更不用说 public 密钥了。我目前无法在 IOS CSR 生成项目中看到您可以在其中传递生成的密钥:在我看来,使用 IOS CSR 生成项目生成的 CSR 正在使用它自己生成的密钥,或者没有私有密钥关键。这将得到逻辑,因为您无法从 CER 或 DER 中提取私钥,因为它不存在。
我们不使用相同的 CSR 方法,但我们有一个等效的方法,我们执行以下操作:
- 生成密钥对
- 将 public 密钥发送到远程服务器
- 远程服务器使用 public 密钥生成签名的客户端证书
- 将客户端证书发送回 iOS 设备
- 将客户端证书添加到钥匙串
- 稍后,在 NSURLSession 或类似的程序中使用客户端证书。
您似乎已经发现,iOS 需要这个叫做 "identity" 的额外东西来绑定客户端证书。
我们还发现 iOS 有一个奇怪的事情,您需要在将客户端证书和身份添加到其中之前从钥匙串中删除 public 密钥,否则身份不会似乎正确地找到了客户端证书。我们选择重新添加 public 键,但作为 "generic password"(即任意用户数据)——我们这样做只是因为 iOS 没有合理的 API即时从证书中提取 public 密钥,我们需要 public 密钥来处理我们碰巧正在做的其他奇怪的事情。
如果您只是进行 TLS 客户端证书身份验证,一旦您拥有证书,您将不需要 public 密钥的显式副本,因此您可以通过简单地删除它来简化过程,然后跳过"add-back-in-as-generic-password"位
请原谅一大堆代码,加密的东西似乎总是需要很多工作。
下面是执行上述任务的一些代码:
生成密钥对,deleting/re-saving public 密钥
/// Returns the public key binary data in ASN1 format (DER encoded without the key usage header)
static func generateKeyPairWithPublicKeyAsGenericPassword(privateKeyTag: String, publicKeyAccount: String, publicKeyService: String) throws -> Data {
let tempPublicKeyTag = "TMPPUBLICKEY:\(privateKeyTag)" // we delete this public key and replace it with a generic password, but it needs a tag during the transition
let privateKeyAttr: [NSString: Any] = [
kSecAttrApplicationTag: privateKeyTag.data(using: .utf8)!,
kSecAttrAccessible: kSecAttrAccessibleAlwaysThisDeviceOnly,
kSecAttrIsPermanent: true ]
let publicKeyAttr: [NSString: Any] = [
kSecAttrApplicationTag: tempPublicKeyTag.data(using: .utf8)!,
kSecAttrAccessible: kSecAttrAccessibleAlwaysThisDeviceOnly,
kSecAttrIsPermanent: true ]
let keyPairAttr: [NSString: Any] = [
kSecAttrKeyType: kSecAttrKeyTypeRSA,
kSecAttrKeySizeInBits: 2048,
kSecPrivateKeyAttrs: privateKeyAttr,
kSecPublicKeyAttrs: publicKeyAttr ]
var publicKey: SecKey?, privateKey: SecKey?
let genKeyPairStatus = SecKeyGeneratePair(keyPairAttr as CFDictionary, &publicKey, &privateKey)
guard genKeyPairStatus == errSecSuccess else {
log.error("Generation of key pair failed. Error = \(genKeyPairStatus)")
throw KeychainError.generateKeyPairFailed(genKeyPairStatus)
}
// Would need CFRelease(publicKey and privateKey) here but swift does it for us
// we store the public key in the keychain as a "generic password" so that it doesn't interfere with retrieving certificates
// The keychain will normally only store the private key and the certificate
// As we want to keep a reference to the public key itself without having to ASN.1 parse it out of the certificate
// we can stick it in the keychain as a "generic password" for convenience
let findPubKeyArgs: [NSString: Any] = [
kSecClass: kSecClassKey,
kSecValueRef: publicKey!,
kSecAttrKeyType: kSecAttrKeyTypeRSA,
kSecReturnData: true ]
var resultRef:AnyObject?
let status = SecItemCopyMatching(findPubKeyArgs as CFDictionary, &resultRef)
guard status == errSecSuccess, let publicKeyData = resultRef as? Data else {
log.error("Public Key not found: \(status))")
throw KeychainError.publicKeyNotFound(status)
}
// now we have the public key data, add it in as a generic password
let attrs: [NSString: Any] = [
kSecClass: kSecClassGenericPassword,
kSecAttrAccessible: kSecAttrAccessibleAlwaysThisDeviceOnly,
kSecAttrService: publicKeyService,
kSecAttrAccount: publicKeyAccount,
kSecValueData: publicKeyData ]
var result: AnyObject?
let addStatus = SecItemAdd(attrs as CFDictionary, &result)
if addStatus != errSecSuccess {
log.error("Adding public key to keychain failed. Error = \(addStatus)")
throw KeychainError.cannotAddPublicKeyToKeychain(addStatus)
}
// delete the "public key" representation of the public key from the keychain or it interferes with looking up the certificate
let pkattrs: [NSString: Any] = [
kSecClass: kSecClassKey,
kSecValueRef: publicKey! ]
let deleteStatus = SecItemDelete(pkattrs as CFDictionary)
if deleteStatus != errSecSuccess {
log.error("Deletion of public key from keychain failed. Error = \(deleteStatus)")
throw KeychainError.cannotDeletePublicKeyFromKeychain(addStatus)
}
// no need to CFRelease, swift does this.
return publicKeyData
}
请注意,publicKeyData 并非严格采用 DER 格式,而是 "DER with the first 24 bytes trimmed off" 格式。我不确定这被正式称为什么,但微软和苹果似乎都将它用作 public 键的原始格式。如果您的服务器是 Microsoft 运行 .NET(桌面或核心),那么它可能会对 public 关键字节 as-is 感到满意。如果它是 Java 并且需要 DER,您可能需要生成 DER header - 这是一个固定的 24 字节序列,您可以将其连接起来。
将客户端证书添加到钥匙串,生成身份
static func addIdentity(clientCertificate: Data, label: String) throws {
log.info("Adding client certificate to keychain with label \(label)")
guard let certificateRef = SecCertificateCreateWithData(kCFAllocatorDefault, clientCertificate as CFData) else {
log.error("Could not create certificate, data was not valid DER encoded X509 cert")
throw KeychainError.invalidX509Data
}
// Add the client certificate to the keychain to create the identity
let addArgs: [NSString: Any] = [
kSecClass: kSecClassCertificate,
kSecAttrAccessible: kSecAttrAccessibleAlwaysThisDeviceOnly,
kSecAttrLabel: label,
kSecValueRef: certificateRef,
kSecReturnAttributes: true ]
var resultRef: AnyObject?
let addStatus = SecItemAdd(addArgs as CFDictionary, &resultRef)
guard addStatus == errSecSuccess, let certAttrs = resultRef as? [NSString: Any] else {
log.error("Failed to add certificate to keychain, error: \(addStatus)")
throw KeychainError.cannotAddCertificateToKeychain(addStatus)
}
// Retrieve the client certificate issuer and serial number which will be used to retrieve the identity
let issuer = certAttrs[kSecAttrIssuer] as! Data
let serialNumber = certAttrs[kSecAttrSerialNumber] as! Data
// Retrieve a persistent reference to the identity consisting of the client certificate and the pre-existing private key
let copyArgs: [NSString: Any] = [
kSecClass: kSecClassIdentity,
kSecAttrIssuer: issuer,
kSecAttrSerialNumber: serialNumber,
kSecReturnPersistentRef: true] // we need returnPersistentRef here or the keychain makes a temporary identity that doesn't stick around, even though we don't use the persistentRef
let copyStatus = SecItemCopyMatching(copyArgs as CFDictionary, &resultRef);
guard copyStatus == errSecSuccess, let _ = resultRef as? Data else {
log.error("Identity not found, error: \(copyStatus) - returned attributes were \(certAttrs)")
throw KeychainError.cannotCreateIdentityPersistentRef(addStatus)
}
// no CFRelease(identityRef) due to swift
}
在我们的代码中,我们选择 return 一个标签,然后使用该标签和以下代码查找身份 as-required。您还可以选择仅 return 来自上述函数的身份引用而不是标签。无论如何,这是我们的 getIdentity 函数
稍后获取身份
// Remember any OBJECTIVE-C code that calls this method needs to call CFRetain
static func getIdentity(label: String) -> SecIdentity? {
let copyArgs: [NSString: Any] = [
kSecClass: kSecClassIdentity,
kSecAttrLabel: label,
kSecReturnRef: true ]
var resultRef: AnyObject?
let copyStatus = SecItemCopyMatching(copyArgs as CFDictionary, &resultRef)
guard copyStatus == errSecSuccess else {
log.error("Identity not found, error: \(copyStatus)")
return nil
}
// back when this function was all ObjC we would __bridge_transfer into ARC, but swift can't do that
// It wants to manage CF types on it's own which is fine, except they release when we return them out
// back into ObjC code.
return (resultRef as! SecIdentity)
}
// Remember any OBJECTIVE-C code that calls this method needs to call CFRetain
static func getCertificate(label: String) -> SecCertificate? {
let copyArgs: [NSString: Any] = [
kSecClass: kSecClassCertificate,
kSecAttrLabel: label,
kSecReturnRef: true]
var resultRef: AnyObject?
let copyStatus = SecItemCopyMatching(copyArgs as CFDictionary, &resultRef)
guard copyStatus == errSecSuccess else {
log.error("Identity not found, error: \(copyStatus)")
return nil
}
// back when this function was all ObjC we would __bridge_transfer into ARC, but swift can't do that
// It wants to manage CF types on it's own which is fine, except they release when we return them out
// back into ObjC code.
return (resultRef as! SecCertificate)
}
最后
使用身份验证服务器
这一点在 objc 中,因为我们的应用恰好就是这样工作的,但你明白了:
SecIdentityRef _clientIdentity = [XYZ getClientIdentityWithLabel: certLabel];
if(_clientIdentity) {
CFRetain(_clientIdentity);
}
SecCertificateRef _clientCertificate = [XYZ getClientCertificateWithLabel:certLabel];
if(_clientCertificate) {
CFRetain(_clientCertificate);
}
...
- (void)URLSession:(nullable NSURLSession *)session
task:(nullable NSURLSessionTask *)task
didReceiveChallenge:(nullable NSURLAuthenticationChallenge *)challenge
completionHandler:(nullable void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {
if (challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodClientCertificate) {
// supply the appropriate client certificate
id bridgedCert = (__bridge id)_clientCertificate;
NSArray* certificates = bridgedCert ? @[bridgedCert] : @[];
NSURLCredential* credential = [NSURLCredential credentialWithIdentity:identity certificates:certificates persistence:NSURLCredentialPersistenceForSession];
completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
}
}
这段代码花了很多时间才正确。 iOS 证书内容的记录非常少,希望这会有所帮助。