Swift iOS客户端证书认证

14

我想要使用的网络服务需要客户端证书。我该如何将我的证书发送给它?

更进一步地,我不明白如何创建SecIdentityRef

在我的NSURLConnectiondidReceiveAuthenticationChallenge中,在ServerTrust之后,我有这个条件语句:

else if challenge?.protectionSpace.authenticationMethod == NSURLAuthenticationMethodClientCertificate
    {
        var secIdent : SecIdentityRef = ?????????
        var certCred = NSURLCredential(identity: secIdent, certificates: [getClientCertificate()], persistence: NSURLCredentialPersistence.Permanent)
        challenge?.sender.useCredential(certCred, forAuthenticationChallenge: challenge!)
    }

getClientCertificate方法:

func getClientCertificate() -> SecCertificateRef
{
    let mainBundle : NSBundle = NSBundle.mainBundle()
    var mainBund = mainBundle.pathForResource("iosClientCert", ofType: "cer") //exported the cert in der format.
    var key : NSData = NSData(contentsOfFile: mainBund!)!
    var turnToCert : SecCertificateRef = SecCertificateCreateWithData(kCFAllocatorDefault, key).takeRetainedValue()

    return turnToCert;
}

感谢 @EpicPandaForce 的慷慨奖励。 - sk1tt1sh
1
我们遇到了同样的问题,但我们仍在努力解决它...不过,我们可能需要直接移植以下解决方案https://dev59.com/eF8d5IYBdhLWcg3wYxY-#27012819,使之能够在Swift中使用。 - EpicPandaForce
我必须将应用程序转换为使用令牌等身份验证方式,以便将其进入测试阶段。我会尽快重新审视这个问题。我差点采用桥接头路线并在 obj-c 中实现 http 客户端...但我也没有时间做那件事。 - sk1tt1sh
声望来自EpicPandaForce,还有22小时结束。blah - EpicPandaForce
.cer 是一个简单的导出,没有任何密钥。在请求时如何进行附加?有人解决了这个问题吗? - Swaroop S
5个回答

10

从技术上讲,当我认识的某个人需要在Swift中实现时,他使用了以下Objective-C实现来获取NSURLCredential对象到连接;基于包含在PKCS12密钥库中的私钥和X509证书对

抱歉,我没有访问包含Swift解决方案的源代码。我知道的只是NSURLCredential被返回给Swift并直接在那里的http URL连接中使用。虽然它类似于这个,但不完全相同。

我不是iOS开发人员,所以无法帮助您完成“桥接到Swift”的部分。

- (void)getMessageWithURL:(NSString *)url {

    NSURL *URL = [NSURL URLWithString:url];

    NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
    [request setURL:URL];
    [request setHTTPMethod:@"GET"];
    NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
    [connection self];
}

- (void)postMessageWithURL:(NSString *)url withContent:(NSString *)content {

    NSData *postData = [content dataUsingEncoding:NSUTF8StringEncoding];
    NSString *postLength = [NSString stringWithFormat:@"%d", [postData length]];

    NSURL *myURL = [NSURL URLWithString:url];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:myURL cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:60];

    [request setHTTPMethod:@"POST"];
    [request setValue:postLength forHTTPHeaderField:@"Content-Length"];
    [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
    [request setHTTPBody:postData];

    NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
    [connection self];

}

- (BOOL)connection:(NSURLConnection *)connection canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace *)protectionSpace {
    return [protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust];
}

- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
    NSLog(@"didReceiveAuthenticationChallenge");
}

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
    responseData = [[NSMutableData alloc] init];
}

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    [responseData appendData:data];
}

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    NSLog(@"Unable to fetch data");
    NSLog(@"%@", error);
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
    NSLog(@"Succeeded! Received %lu bytes of data", (unsigned long)[responseData
            length]);

    NSString *responseString = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding];
    NSLog(@"%@", responseString);

    [bridge callHandler:handlerName data:responseString];

}

- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {

    /*
    Reading the certificate and creating the identity
    */
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectory = paths[0]; // Get documents directory

    NSData *p12data = [CertificateManager getP12Data]; //returns essentially a byte array containing a valid PKCS12 certificate

    if (!p12data) {
      return;
      NSAssert(p12data, @"Couldn't load p12 file...");
    }

    CFStringRef password = CFSTR("password");

    const void *keys[] = {kSecImportExportPassphrase};
    const void *values[] = {password};
    CFDictionaryRef optionsDictionary = CFDictionaryCreate(NULL, keys, values, 1, NULL, NULL);
    CFArrayRef p12Items;

    OSStatus result = SecPKCS12Import((__bridge CFDataRef) p12data, optionsDictionary, &p12Items);

    if (result == noErr) {
        CFDictionaryRef identityDict = CFArrayGetValueAtIndex(p12Items, 0);
        SecIdentityRef identityApp = (SecIdentityRef) CFDictionaryGetValue(identityDict, kSecImportItemIdentity);

        SecCertificateRef certRef;
        SecIdentityCopyCertificate(identityApp, &certRef);

        SecCertificateRef certArray[1] = {certRef};
        CFArrayRef myCerts = CFArrayCreate(NULL, (void *) certArray, 1, NULL);
        CFRelease(certRef);

        NSURLCredential *credential = [NSURLCredential credentialWithIdentity:identityApp certificates:nil persistence:NSURLCredentialPersistenceNone];
        CFRelease(myCerts);

        [[challenge sender] useCredential:credential forAuthenticationChallenge:challenge];
    }
    else {
        // Certificate is invalid or password is invalid given the certificate
        NSLog(@"Invalid certificate or password");
        NSError *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:result userInfo:nil];
        return;
    }
}

编辑:哈哈,很有趣,当奖励还在时你们没有投票,现在却投了两票。*咕哝*

无论如何,要使用上述内容,您只需要从Swift中访问它。

func connection(connection: NSURLConnection, willSendRequestForAuthenticationChallenge challenge: NSURLAuthenticationChallenge) {
    if let p12Data = UserManager.currentP12,
       let credential = CertificateManager.getCredentialsForP12(p12Data) as? NSURLCredential {
            challenge.sender.useCredential(credential, forAuthenticationChallenge: challenge)
    } else {
        UIApplication.sharedApplication().networkActivityIndicatorVisible = false
    }   
}

使用这个的方法是这样的。
+ (id)getCredentialsForP12:(NSData *)p12 {
    NSData* p12data = p12;
    const void *keys[] = {kSecImportExportPassphrase};
    const void *values[] = {CFSTR("thePassword")};
    CFDictionaryRef optionsDictionary = CFDictionaryCreate(NULL, keys, values, 1, NULL, NULL);
    CFArrayRef p12Items;
    OSStatus result = SecPKCS12Import((__bridge CFDataRef) p12data, optionsDictionary, &p12Items);
    if (result == noErr) {
        CFDictionaryRef identityDict = CFArrayGetValueAtIndex(p12Items, 0);
        SecIdentityRef identityApp = (SecIdentityRef) CFDictionaryGetValue(identityDict, kSecImportItemIdentity);
        SecCertificateRef certRef;
        SecIdentityCopyCertificate(identityApp, &certRef);
        SecCertificateRef certArray[1] = {certRef};
        CFArrayRef myCerts = CFArrayCreate(NULL, (void *) certArray, 1, NULL);
        CFRelease(certRef);

        NSURLCredential *credential = [NSURLCredential credentialWithIdentity:identityApp certificates:nil persistence:NSURLCredentialPersistenceNone];
        CFRelease(myCerts);
        return credential;

    }
    else {
        // Certificate is invalid or password is invalid given the certificate
        NSLog(@"Invalid certificate or password");

        UIAlertView* av = [[UIAlertView alloc] initWithTitle:@"Error" message:@"Invalid cert or pass" delegate:nil cancelButtonTitle:@"ok" otherButtonTitles: nil];
        [av show];
        NSError *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:result userInfo:nil];
        return nil;
    }

编辑:上面的内容有一个Swift版本,但它太混乱了,我们宁愿不使用它。

            var p12items : Unmanaged<CFArrayRef>?

            let index: CFIndex = 1
            let password: CFString = "password"
            let key = kSecImportExportPassphrase.takeRetainedValue() as String
            var values = [unsafeAddressOf(password)]
            var keys = [unsafeAddressOf(key)]

            var keyCallbacks = kCFTypeDictionaryKeyCallBacks
            var valueCallbacks = kCFTypeDictionaryValueCallBacks

            let length: CFIndex = p12Data.length
            let p12CfData: CFData = CFDataCreate(kCFAllocatorDefault, UnsafePointer<UInt8>(p12Data.bytes), length)

            let options = CFDictionaryCreate(kCFAllocatorDefault, &keys, &values, index, &keyCallbacks, &valueCallbacks)
            let result = SecPKCS12Import(p12CfData, options, &p12items)

            if result == noErr {

                let idIndex: CFIndex = 0
                var items = p12items?.takeRetainedValue()
                var identityDict = CFArrayGetValueAtIndex(items!, idIndex) 

                var key = kSecImportItemIdentity.takeRetainedValue() as String
                var keyAddress = unsafeAddressOf(key)
                var identityApp: SecIdentityRef = CFDictionaryGetValue(identityDict, keyAddress) 
                var certRef : Unmanaged<SecCertificateRef>?
                SecIdentityCopyCertificate(identityApp, &certRef)

                var cert: SecCertificateRef = certRef!.takeRetainedValue()
                var certArray = [unsafeAddressOf(cert)]
                var arrayCallback = kCFTypeArrayCallBacks
                var myCerts: CFArrayRef = CFArrayCreate(kCFAllocatorDefault, &certArray, index, &arrayCallback);

                let credential: NSURLCredential = NSURLCredential(identity: identityApp, certificates: [AnyObject](), persistence: NSURLCredentialPersistence.None)

您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - sk1tt1sh
我的意思是密钥必须是PKCS,这意味着它将包括证书的公钥和私钥。在其他语言中,我可以使用.cer文件,它只是一个直接导出而没有私钥,并且不需要证书密码。虽然我们有一个CA签名的证书,但这并不是什么大问题,但对我来说有点奇怪。 - sk1tt1sh
1
从技术上讲,这是在Java中完成的,我的一个朋友成功地将我使用BouncyCastle完成的整个过程映射到了iOS上使用OpenSSL。但是为了使客户端身份验证起作用,您确实需要私钥和公钥;因为您使用收件人的公钥加密内容,他们使用您的公钥加密内容;但要解密它,您需要自己的私钥,该私钥存储在密钥库中。这是通过SecPKCS12Import调用导入的,以便在SSL连接中同时拥有两者。客户端认证很神奇,但奇怪的是没有任何相关信息。 - EpicPandaForce
2
所以我已经可以访问Swift代码了,并添加了其中一部分。我不会删除我的答案,只是因为一些对客户端认证毫无头绪的人觉得闯进来表现得很聪明并进行投票。 - EpicPandaForce
我将其标记为答案,因为这已经足够使用了。有可能将其转换为100%的Swift吗?谁知道呢,但至少你尝试过了。感谢你,很抱歉你遭到了那些负评。我感到非常惊讶。 - sk1tt1sh
显示剩余2条评论

4

2
为了响应认证挑战,您需要从客户端证书中提取身份信息。
struct IdentityAndTrust {

    var identityRef:SecIdentityRef
    var trust:SecTrustRef
    var certArray:NSArray
}

func extractIdentity(certData:NSData, certPassword:String) -> IdentityAndTrust {

    var identityAndTrust:IdentityAndTrust!
    var securityError:OSStatus = errSecSuccess

    var items:Unmanaged<CFArray>?
    let certOptions:CFDictionary = [ kSecImportExportPassphrase.takeRetainedValue() as String: certPassword ];

    // import certificate to read its entries
    securityError = SecPKCS12Import(certData, certOptions, &items);

    if securityError == errSecSuccess {

        let certItems:CFArray = items?.takeUnretainedValue() as CFArray!;
        let certItemsArray:Array = certItems as Array
        let dict:AnyObject? = certItemsArray.first;

        if let certEntry:Dictionary = dict as? Dictionary<String, AnyObject> {

            // grab the identity
            let identityPointer:AnyObject? = certEntry["identity"];
            let secIdentityRef:SecIdentityRef = identityPointer as! SecIdentityRef!;

            // grab the trust
            let trustPointer:AnyObject? = certEntry["trust"];
            let trustRef:SecTrustRef = trustPointer as! SecTrustRef;

            // grab the certificate chain
            var certRef:Unmanaged<SecCertificate>?
            SecIdentityCopyCertificate(secIdentityRef, &certRef);
            let certArray:NSMutableArray = NSMutableArray();
            certArray.addObject(certRef?.takeRetainedValue() as SecCertificateRef!);

            identityAndTrust = IdentityAndTrust(identityRef: secIdentityRef, trust: trustRef, certArray: certArray);
        }
    }

    return identityAndTrust;
}

NSURLSessionDelegate中,处理身份验证挑战的方法如下:
public func URLSession(session: NSURLSession, didReceiveChallenge challenge: NSURLAuthenticationChallenge, completionHandler: (NSURLSessionAuthChallengeDisposition, NSURLCredential?) -> Void) {

    let bundle:NSBundle = NSBundle(forClass: self.dynamicType);
    let bundleCertPath:NSString = bundle.pathForResource("clientCertificateName", ofType: "p12")!;
    let certData:NSData = NSData(contentsOfFile: bundleCertPath as String)!;
    let identityAndTrust:IdentityAndTrust = self.certificateHelper.extractIdentity(certData, certPassword: "C00lp@assword");

    if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodClientCertificate {

        let urlCredential:NSURLCredential = NSURLCredential(
                identity: identityAndTrust.identityRef,
                certificates: identityAndTrust.certArray as [AnyObject],
                persistence: NSURLCredentialPersistence.ForSession);

        completionHandler(NSURLSessionAuthChallengeDisposition.UseCredential, urlCredential);


    } else {

        // nothing here but us chickens
    }
}

这非常有帮助,但我收到了一个钥匙串警报,要求用户允许访问私钥。我没有预料到这一点,我可能做错了什么吗? - Chris
谢谢,但您能否提供一个如何使用它的示例? - GameDev
什么是CertificateHelper? - Eduardo Oliveros

0

正在使用最新版本的Xcode +13的人会发现这很有帮助。

最终使用.p12文件和使用PKCS12方法从.p12中提取所有细节(如身份,证书链,信任,密钥ID(私钥和公钥)),并将这些密钥签名到一个对象中以生成签名!

下面的代码将帮助您实现客户端和服务器之间的mTLS身份验证,无需处理.pfx文件。您只需要base 64字符串证书数据即可。

struct IdentityAndTrust {
   var identityRef:SecIdentity
   var trust:SecTrust
   var certArray:NSArray
   var privateKey:SecKey?
   var publicKey:SecKey?
   var signatureData:String?
}

func extractIdentity(certData:NSData, certPassword:String, signingData:String) -> IdentityAndTrust {

  let certString = String.init(data: certData as Data, encoding: .utf8) ?? ""
  let base64 = Data(base64Encoded: certString, options: .ignoreUnknownCharacters)
  var identityAndTrust:IdentityAndTrust!
  var items:CFArray?
  var privateKey:SecKey?
  var publicKey:SecKey?
  var signatureData:String?
  var error: Unmanaged<CFError>?

  let importPasswordOption:NSDictionary = [kSecImportExportPassphrase as NSString:certPassword]

// import certificate to read its entries
  let secError:OSStatus = SecPKCS12Import((base64 ?? Data()) as CFData, importPasswordOption, &items)
  guard secError == errSecSuccess else {
      if secError == errSecAuthFailed {
          NSLog("ERROR: SecPKCS12Import returned errSecAuthFailed. Incorrect password?")
      }
      NSLog("SecPKCS12Import returned an error trying to import PKCS12 data")
      return identityAndTrust
  }

  if let certItems:CFArray = items {
      let certItemsArray:Array = certItems as Array
      let dict:AnyObject? = certItemsArray.first
    
      if let certEntry:Dictionary = dict as? Dictionary<String, AnyObject> {
        
        // grab the identity
          let identityPointer:AnyObject? = certEntry["identity"];
          let secIdentityRef:SecIdentity = identityPointer as! SecIdentity
        
        // grab the trust
          let trustPointer:AnyObject? = certEntry["trust"];
          let trustRef:SecTrust = trustPointer as! SecTrust;
        
        // grab the certificate chain
          var certRef:SecCertificate?
          SecIdentityCopyCertificate(secIdentityRef, &certRef);
          let certArray:NSMutableArray = NSMutableArray();
          certArray.add(certRef!);
        
        //To get the Private Key
          var key: SecKey?
          SecIdentityCopyPrivateKey(secIdentityRef , &key)
          if let privateKeyData = key{
              print("privatekeyData: \(privateKeyData)")
              privateKey = privateKeyData
            
            //Make sure the key supports signing with the algorithm
              guard SecKeyIsAlgorithmSupported(privateKeyData, .sign, .rsaSignatureMessagePKCS1v15SHA256) else {
                  return identityAndTrust
              }
            //Making signature
              if let  signature = SecKeyCreateSignature(privateKeyData, .rsaSignatureMessagePKCS1v15SHA256, Data(signingData.utf8) as CFData, &error) {
                  signatureData =  (signature as Data).base64EncodedString()
                  print("signature: \(signature)")
              } else {
                  NSLog("\(error!.takeUnretainedValue())")
              }
            //To get the Public Key from Private key
              if let publickey = SecKeyCopyPublicKey(privateKeyData) {
                  publicKey = publickey
                  print("publickey: \(publickey)")
              }
          }
          identityAndTrust = IdentityAndTrust(identityRef: secIdentityRef, trust: trustRef, certArray: certArray, privateKey: privateKey, publicKey:publicKey, signatureData:signatureData)
      }
  }
 
    print("identityAndTrust \(String(describing: identityAndTrust))")
    return identityAndTrust
 }

0

我正在使用最新的Xcode和Swift版本,这段代码对我有效,使用客户端证书.pfx,基于Bins Ich的答案:

 func extractIdentity(certData:NSData) -> IdentityAndTrust { 
    var identityAndTrust:IdentityAndTrust!
    var securityError:OSStatus = errSecSuccess
    var items:Unmanaged<CFArray>?
    let certOptions:CFDictionary = [ kSecImportExportPassphrase.takeRetainedValue() as String: "password" ];

    // import certificate to read its entries
    securityError = SecPKCS12Import(certData, certOptions, &items);

    if securityError == errSecSuccess {
        let certItems:CFArray = items?.takeUnretainedValue() as CFArray!;
        let certItemsArray:Array = certItems as Array
        let dict:AnyObject? = certItemsArray.first;
        if let certEntry:Dictionary = dict as? Dictionary<String, AnyObject> {

            // grab the identity
            let identityPointer:AnyObject? = certEntry["identity"];
            let secIdentityRef:SecIdentityRef = identityPointer as! SecIdentityRef!;

            // grab the trust
            let trustPointer:AnyObject? = certEntry["trust"];
            let trustRef:SecTrustRef = trustPointer as! SecTrustRef;

            // grab the cert
            let chainPointer:AnyObject? = certEntry["chain"];
            let chainRef:SecCertificateRef = chainPointer as! SecCertificateRef;
            let  certArray:CFArrayRef = chainRef as! CFArrayRef

            identityAndTrust = IdentityAndTrust(identityRef: secIdentityRef, trust: trustRef, certArray:  certArray);
        }
    }
    return identityAndTrust;
}

func connection(connection: NSURLConnection, willSendRequestForAuthenticationChallenge challenge: NSURLAuthenticationChallenge) {

    let strTemp = challenge.protectionSpace.authenticationMethod

    if(strTemp == NSURLAuthenticationMethodServerTrust) {
         challenge.sender.continueWithoutCredentialForAuthenticationChallenge(challenge)
    }

    if(strTemp == NSURLAuthenticationMethodClientCertificate) {

        let certFile = NSBundle.mainBundle().pathForResource("mycert", ofType:"pfx")

        let p12Data = NSData(contentsOfFile:certFile!)
        let identityAndTrust:IdentityAndTrust = extractIdentity(p12Data!)

        let urlCredential:NSURLCredential = NSURLCredential(
            identity: identityAndTrust.identityRef,
            certificates:identityAndTrust.certArray as [AnyObject],
            persistence: NSURLCredentialPersistence.Permanent)

        challenge.sender.useCredential(urlCredential ,forAuthenticationChallenge:challenge)
    }
}

谢谢Jose!你知道是什么改变让它工作了吗?这与我尝试让它工作时使用的一些最初的代码非常相似。 - sk1tt1sh
我使用以下代码获取证书链: let chainPointer:AnyObject? = certEntry["chain"]; let chainRef:SecCertificateRef = chainPointer as! SecCertificateRef; let certArray:CFArrayRef = chainRef as! CFArrayRef - Jose Sanchez

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