jjzjj

ios - iOS 中的客户端证书和身份

coder 2023-07-15 原文

我已经使用 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 数据创建证书

我的问题如下:如何将证书与我在开始时创建的私钥连接起来,并获得这两者( key 和证书)所属的身份?
也许,将证书添加到钥匙串(keychain)并获得使用 SecItemCopyMatching 的身份?我已按照问题中提出的程序进行操作 SecIdentityRef procedure

编辑:

将证书添加到钥匙串(keychain)时,我得到状态响应 0,我认为这意味着证书已添加到钥匙串(keychain)。

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)。以下代码用于获取身份。标签是我用来生成私钥/公钥的私钥标签。

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 从钥匙串(keychain)中获取私钥、公钥和证书,并将证书添加到钥匙串(keychain),但查询 SecIdentity 不起作用。我的证书是否可能与我的 key 不匹配?如何检查?

我以 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 生成的 key 的开头似乎有细微差别。 (MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A)。基于问题RSA encryption ,似乎 MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A 是 RSA 加密“1.2.840.113549.1.1.1”的 base64 格式标识符。所以我想公钥可能没问题?

最佳答案

我们不使用相同的 CSR 方法,但我们有一个等效的方法,我们执行以下操作:

  1. 生成 key 对
  2. 将公钥发送到远程服务器
  3. 远程服务器使用公钥生成签名的客户端证书
  4. 将客户端证书发送回 iOS 设备
  5. 将客户端证书添加到钥匙串(keychain)
  6. 稍后,在 NSURLSession 或类似的程序中使用客户端证书。

您似乎已经发现,iOS 需要这种称为“身份”的额外东西来绑定(bind)客户端证书。

我们还发现 iOS 有一个奇怪的事情,您需要在将客户端证书和身份添加到其中之前从钥匙串(keychain)中删除公钥,否则身份似乎无法正确定位客户端证书。我们选择重新添加公钥,但作为“通用密码”(即任意用户数据)——我们这样做只是因为 iOS 没有用于即时从证书中提取公钥的合理 API,我们我们碰巧正在做的其他奇怪的事情需要公钥。

如果您只是进行 TLS 客户端证书身份验证,一旦您拥有证书,您将不需要公钥的显式副本,因此您可以通过简单地删除它来简化流程,并跳过“添加回- in-as-generic-password"位

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

下面是执行上述任务的一些代码:

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

/// 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(桌面或核心)的 Microsoft 服务器,那么它可能会对原样的公钥字节感到满意。如果它是 Java 并且需要 DER,您可能需要生成 DER header - 这是一个固定的 24 字节序列,您可以直接连接。

将客户端证书添加到钥匙串(keychain),生成身份

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
}

在我们的代码中,我们选择返回标签,然后根据需要使用标签和以下代码查找标识。您还可以选择仅从上述函数返回标识引用而不是标签。无论如何,这是我们的 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 证书内容的文档非常少,希望这对您有所帮助。

关于ios - iOS 中的客户端证书和身份,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/39975716/

有关ios - iOS 中的客户端证书和身份的更多相关文章

  1. ruby - 如何从 ruby​​ 中的字符串运行任意对象方法? - 2

    总的来说,我对ruby​​还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用

  2. ruby - 其他文件中的 Rake 任务 - 2

    我试图在一个项目中使用rake,如果我把所有东西都放到Rakefile中,它会很大并且很难读取/找到东西,所以我试着将每个命名空间放在lib/rake中它自己的文件中,我添加了这个到我的rake文件的顶部:Dir['#{File.dirname(__FILE__)}/lib/rake/*.rake'].map{|f|requiref}它加载文件没问题,但没有任务。我现在只有一个.rake文件作为测试,名为“servers.rake”,它看起来像这样:namespace:serverdotask:testdoputs"test"endend所以当我运行rakeserver:testid时

  3. ruby-on-rails - Ruby net/ldap 模块中的内存泄漏 - 2

    作为我的Rails应用程序的一部分,我编写了一个小导入程序,它从我们的LDAP系统中吸取数据并将其塞入一个用户表中。不幸的是,与LDAP相关的代码在遍历我们的32K用户时泄漏了大量内存,我一直无法弄清楚如何解决这个问题。这个问题似乎在某种程度上与LDAP库有关,因为当我删除对LDAP内容的调用时,内存使用情况会很好地稳定下来。此外,不断增加的对象是Net::BER::BerIdentifiedString和Net::BER::BerIdentifiedArray,它们都是LDAP库的一部分。当我运行导入时,内存使用量最终达到超过1GB的峰值。如果问题存在,我需要找到一些方法来更正我的代

  4. ruby-on-rails - Rails 3 中的多个路由文件 - 2

    Rails2.3可以选择随时使用RouteSet#add_configuration_file添加更多路由。是否可以在Rails3项目中做同样的事情? 最佳答案 在config/application.rb中:config.paths.config.routes在Rails3.2(也可能是Rails3.1)中,使用:config.paths["config/routes"] 关于ruby-on-rails-Rails3中的多个路由文件,我们在StackOverflow上找到一个类似的问题

  5. ruby - 具有身份验证的私有(private) Ruby Gem 服务器 - 2

    我想安装一个带有一些身份验证的私有(private)Rubygem服务器。我希望能够使用公共(public)Ubuntu服务器托管内部gem。我读到了http://docs.rubygems.org/read/chapter/18.但是那个没有身份验证-如我所见。然后我读到了https://github.com/cwninja/geminabox.但是当我使用基本身份验证(他们在他们的Wiki中有)时,它会提示从我的服务器获取源。所以。如何制作带有身份验证的私有(private)Rubygem服务器?这是不可能的吗?谢谢。编辑:Geminabox问题。我尝试“捆绑”以安装新的gem..

  6. ruby-on-rails - Rails - 一个 View 中的多个模型 - 2

    我需要从一个View访问多个模型。以前,我的links_controller仅用于提供以不同方式排序的链接资源。现在我想包括一个部分(我假设)显示按分数排序的顶级用户(@users=User.all.sort_by(&:score))我知道我可以将此代码插入每个链接操作并从View访问它,但这似乎不是“ruby方式”,我将需要在不久的将来访问更多模型。这可能会变得很脏,是否有针对这种情况的任何技术?注意事项:我认为我的应用程序正朝着单一格式和动态页面内容的方向发展,本质上是一个典型的网络应用程序。我知道before_filter但考虑到我希望应用程序进入的方向,这似乎很麻烦。最终从任何

  7. ruby-on-rails - Rails 3.2.1 中 ActionMailer 中的未定义方法 'default_content_type=' - 2

    我在我的项目中添加了一个系统来重置用户密码并通过电子邮件将密码发送给他,以防他忘记密码。昨天它运行良好(当我实现它时)。当我今天尝试启动服务器时,出现以下错误。=>BootingWEBrick=>Rails3.2.1applicationstartingindevelopmentonhttp://0.0.0.0:3000=>Callwith-dtodetach=>Ctrl-CtoshutdownserverExiting/Users/vinayshenoy/.rvm/gems/ruby-1.9.3-p0/gems/actionmailer-3.2.1/lib/action_mailer

  8. ruby-on-rails - Rails 应用程序中的 Rails : How are you using application_controller. rb 是新手吗? - 2

    刚入门rails,开始慢慢理解。有人可以解释或给我一些关于在application_controller中编码的好处或时间和原因的想法吗?有哪些用例。您如何为Rails应用程序使用应用程序Controller?我不想在那里放太多代码,因为据我了解,每个请求都会调用此Controller。这是真的? 最佳答案 ApplicationController实际上是您应用程序中的每个其他Controller都将从中继承的类(尽管这不是强制性的)。我同意不要用太多代码弄乱它并保持干净整洁的态度,尽管在某些情况下ApplicationContr

  9. ruby-on-rails - form_for 中不在模型中的自定义字段 - 2

    我想向我的Controller传递一个参数,它是一个简单的复选框,但我不知道如何在模型的form_for中引入它,这是我的观点:{:id=>'go_finance'}do|f|%>Transferirde:para:Entrada:"input",:placeholder=>"Quantofoiganho?"%>Saída:"output",:placeholder=>"Quantofoigasto?"%>Nota:我想做一个额外的复选框,但我该怎么做,模型中没有一个对象,而是一个要检查的对象,以便在Controller中创建一个ifelse,如果没有检查,请帮助我,非常感谢,谢谢

  10. ruby - rspec 需要 .rspec 文件中的 spec_helper - 2

    我注意到像bundler这样的项目在每个specfile中执行requirespec_helper我还注意到rspec使用选项--require,它允许您在引导rspec时要求一个文件。您还可以将其添加到.rspec文件中,因此只要您运行不带参数的rspec就会添加它。使用上述方法有什么缺点可以解释为什么像bundler这样的项目选择在每个规范文件中都需要spec_helper吗? 最佳答案 我不在Bundler上工作,所以我不能直接谈论他们的做法。并非所有项目都checkin.rspec文件。原因是这个文件,通常按照当前的惯例,只

随机推荐