iOS

时间:2016-10-11 10:56:18

标签: ios swift encryption certificate

我使用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: [])

现在,我应该使用let certificate = SecCertificateCreateWithData(nil, certDer)

从DER-data创建证书

我的问题如下:如何将证书与我在开头创建的私钥连接起来并获得这两者(密钥和证书)所属的身份?
也许,将证书添加到钥匙串并获取使用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不起作用。我的证书可能与我的密钥不匹配吗?怎么检查?

我以iOS64格式从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 encryption,似乎MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A是RSA加密的基本64格式标识符" 1.2.840.113549.1.1.1"。所以我猜公钥可能没问题?

2 个答案:

答案 0 :(得分:1)

我们不使用相同的CSR方法,但我们在执行以下操作时有相同的内容:

  1. 生成密钥对
  2. 将公钥发送到远程服务器
  3. 远程服务器使用公钥生成签名的客户端证书
  4. 将客户端证书发送回iOS设备
  5. 将客户端证书添加到钥匙串
  6. 稍后,在NSURLSession或类似的地方使用客户端证书。
  7. 正如您似乎已经发现的那样,iOS需要这个额外的东西称为“身份”来绑定客户端证书。

    我们还发现iOS有一个奇怪的事情,您需要在将密钥链中的公钥添加到其中之前从密钥链中删除公钥,否则身份似乎不会正确地找到客户端证书。我们选择重新添加公钥,但作为“通用密码”(即任意用户数据) - 我们只这样做,因为iOS没有合理的API用于动态提取公钥,我们需要公钥来处理我们碰巧正在做的其他奇怪的事情。

    如果您只是在进行TLS客户端证书身份验证,那么一旦获得证书,您就不需要公钥的明确副本,因此您可以通过简单地删除它来简化流程,并跳过“添加 - in-as-generic-password“bit

    请原谅大堆代码,加密东西似乎总是需要很多工作。

    以下是执行上述任务的代码:

    生成密钥对,并删除/重新保存公钥

    /// 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,前24个字节被修剪掉”格式。我不确定这是什么称为官方,但微软和苹果似乎都将它用作公钥的原始格式。如果您的服务器是运行.NET(桌面或核心)的微软服务器,那么它可能会对公钥字节按原样感到满意。如果它是Java并且期望DER你可能需要生成DER头 - 这是一个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
    }
    

    在我们的代码中,我们选择返回一个标签,然后使用标签和以下代码按需查找标识。您也可以选择仅从上面的函数而不是标签返回identity ref。这是我们的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证书的内容记录非常糟糕,希望这会有所帮助。

答案 1 :(得分:0)

生成SSL证书的常用方法是使用私钥生成CSR,证书签名请求信息。实际上,您正在使用该密钥签名隐藏公司,电子邮件等信息。然后,使用该CSR,您将签署您的证书,因此它将与您的私钥和存储在CSR中的信息相关联,从不关注公钥。我目前无法在IOS CSR Generation项目中看到您可以传递生成的密钥:在我看来,使用IOS CSR Generation项目生成的CSR正在使用它自己生成的密钥,或者根本没有私钥。那将是逻辑,因为你无法从CER或DER中提取私钥,因为它不在那里。