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 方法,但我们有一个等效的方法,我们执行以下操作:

  1. 生成密钥对
  2. 将 public 密钥发送到远程服务器
  3. 远程服务器使用 public 密钥生成签名的客户端证书
  4. 将客户端证书发送回 iOS 设备
  5. 将客户端证书添加到钥匙串
  6. 稍后,在 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 证书内容的记录非常少,希望这会有所帮助。