iOS中的客户端证书和身份认证

23

我使用SecKeyGeneratePair函数为我的基于Swift的iOS应用程序生成了私钥和公钥。
然后,我使用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: [])

现在,我应使用DER数据创建证书,方法是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)的错误。以下代码用于获取身份。tag是我用来生成私钥/公钥的私钥标记。

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从密钥链中获取私钥、公钥和证书,并将证书添加到密钥链中,但查询SecIdentity无法工作。我的证书可能与我的密钥不匹配吗?如何检查这一点?

我以Base64格式从iOS中打印了公钥。打印结果如下:

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)从证书签名请求中提取了公钥,打印出以下响应:

-----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加密,MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A是用于RSA加密"1.2.840.113549.1.1.1"的base64格式标识符。因此,我猜公钥可能没问题?


开始思考,如果从pem转换为der的证书转换失败了。 - lipponen
你确定 -----BEGIN CERTIFICATE----------END CERTIFICATE----- 没有换行符导致问题吗?在移除它们后,你可能会得到一个新的换行符。 - Mostafa Berg
我还删除了所有的换行符(\n),否则SecCertificateCreateWithData会抛出错误。我已经将该行添加到问题中,在实际代码中该行已经存在。 - lipponen
啊哈,我明白了,所以这段代码现在与你当前失败的代码完全一致? - Mostafa Berg
你正在使用iOS-SCR,这是一个非常不安全的代码。它只是一个示例项目,而不是你应该在生产中使用的东西。我建议立即更换它。 - Antwan van Houdt
显示剩余2条评论
2个回答

8
我们不使用CSR的相同方法,但我们有一个等效的方式,我们执行以下操作:
  1. 生成密钥对
  2. 将公钥发送到远程服务器
  3. 远程服务器使用公钥生成已签名的客户端证书
  4. 将客户端证书返回到iOS设备
  5. 将客户端证书添加到钥匙串中
  6. 稍后,在NSURLSession或类似的地方使用客户端证书。
正如您所发现的那样,iOS需要这个额外的东西称为“身份”,以绑定客户端证书。
我们还发现iOS有一个奇怪的事情,你需要在将客户端证书和身份添加到钥匙串中之前从钥匙串中删除公钥,否则身份似乎无法正确地定位客户端证书。我们选择将公钥作为“通用密码”(即任意用户数据)添加回去 - 我们之所以这样做是因为iOS没有合理的API可以从证书中提取公钥,而我们需要公钥进行其他奇怪的事情。
如果你只是做TLS客户端证书认证,一旦你有了证书,你就不需要明确的公钥副本,因此你可以通过简单地删除它并跳过“添加回通用密码”部分来简化该过程。
请原谅这堆代码,加密的东西总是需要很多工作。
以下是执行上述任务的代码片段:
/// 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格式,而是采用“裁剪第一个24字节的DER”格式。我不确定它的正式名称是什么,但微软和苹果似乎都将其用作公钥的原始格式。如果您的服务器是运行.NET(桌面或Core)的微软服务器,则可能会接受公钥字节本身。如果使用Java并且需要DER格式,则可能需要生成DER头——这是一个固定的序列,您可以简单地连接它。

将客户端证书添加到密钥链中,生成标识

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
}

我们的代码选择返回一个标签(label),然后使用该标签根据需要查找身份(identity), 以下是代码示例。您也可以选择仅从上述函数中返回身份引用,而不是标签。无论如何,这是我们的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证书相关的文档非常不完善,希望这能有所帮助。


请有人救救我,向我展示如何使用Obj-c获取发行者和序列号:已经花费80小时以上在使用NSURLSession处理证书。FML @Orion Edwards - Ezos
@Ezos 我自己没有做过这个,但我猜你可以调用SecCertificateCopyNormalizedIssuerSequence来获取颁发者,调用SecCertificateCopySerialNumberData来获取颁发者。从文档上看(苹果的文档在这方面很垃圾),我猜这些函数会给你原始的ASN1序列,你需要解析它们,但希望这不是太难的事情。 - Orion Edwards
@svenyonson 付出了相当大的努力。我们的后端是C# / aspnetcore,在Linux或Windows上运行,并执行以下操作: https://gist.github.com/borland/5cf356d76904bbb7e83c156e9359dca6请记住,证书只是一个带有一些元数据钉住的公钥,然后由另一个颁发者(根)证书签名的。这是我们如何在.NET中使用BouncyCastle实现的。 - Orion Edwards
@OrionEdwards 我的事情几乎快做好了。我可以提取证书和身份,但是当我使用证书和身份创建URLCredential对象时,结果为nil。如果打印SecIdentityRef,则只有12个字节 - 这样正确吗? - svenyonson
运行得很好。似乎不再需要删除公钥了。此外,为挑战创建凭据只需要身份和凭据参数为空即可。 - user1055568
显示剩余2条评论

-1
通常生成SSL证书的方法是使用私钥生成CSR(证书签名请求)信息。实际上,您使用该密钥签名隐藏了公司、电子邮件等信息。有了CSR,您就可以签署您的证书,因此它将与您的私钥和存储在CSR中的信息相关联,而不用考虑公钥。目前我无法在IOS CSR Generation项目中看到您可以传递已生成密钥的地方:对我来说,使用IOS CSR Generation项目生成的CSR正在使用其自己生成的密钥或根本没有私钥。这将符合以下逻辑:您无法从CER或DER中提取私钥,因为它不存在。

私钥在方法中传递到CSR构建过程:-(NSData *) build:(NSData *)publicKeyBits privateKey:(SecKeyRef)privateKey。 - lipponen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接