证书相关知识介绍及iOS证书校验

961 阅读6分钟

一、数字证书的基础知识

1.1、一些基础名词

  • PKI = Public Key Infrastructure,是一整套安全相关标准
  • CA = Certificate authority, 是 PKI 的”核心”,即数字证书的申请及签发机关,CA 必须具备权威性的特征,它负责管理 PKI 结构下的所有用户 (包括各种应用程序) 的证书,把用户的公钥和用户的其他信息捆绑在一起,在网上验证用户的身份,CA 还要负责用户证书的黑名单登记和黑名单发布 。
  • X.509,当前使用很广泛的一套证书标准,它规范了公开密钥认证、证书吊销列表、授权证书、证书路径验证算法、证书内容及格式等(如 https 证书)。X509 V3 版本的证书基本语法如下(只列举了 CertificateTBSCertificate 这两个结构),其他的描述见 RFC5280。其中 tbsCertificate 的数据段被拿来做 Digest,并且用上级证书的私钥加密后形成签名置入 https 证书中
  • CSR = Certificate signing request,用户申请 CA 证书的签名申请

1.2、证书(Certificate)

  • 证书,联想的是驾驶证、毕业证、英语四六级证等等,都是由权威机构认证的
  • 密码学中的证书,全称叫公钥证书(Public-key Certificate,PKC),跟驾驶证类似。里面有姓名、邮箱等个人信息,以及此人的公钥,并由认证机构(Certificate Authority,CA)施加数字签名
  • CA(Certificate Authority)就是能够认定“公钥确实属于此人”并能够生成数字签名的个人或者组织:有国际性组织、政府设立的组织、有通过提供认证服务来盈利的企业、个人也可以成立认证机构

1.3、证书的注册和下载流程

图1:证书的注册和下载流程.png

1.4、证书链

我们以百度为例,在浏览器上访问 “www.baidu.com” 域名,地址连左侧有一个小锁的标志,点击就能查看百度的数字证书,如下图所示(使用的是Safari浏览器)

图2:百度证书链.png

在图片的顶部,我们看到这样一个层次关系:GlobalSign -> GlobalSign RSA OV SSL CA 2018 -> baidu.com。这个层次可以抽象为三个级别:

  • end-user:即 baidu.com,该证书包含百度的公钥,访问者就是使用该公钥将数据加密后再传输给百度,即在 HTTPS 中使用的证书
  • intermediates:即签发人 (Issuer),用来认证公钥持有者身份的证书,负责确认 HTTPS 使用的 end-user 证书确实是来源于百度。这类 intermediates 证书可以有很多级,也就是说 签发人 Issuer 可能会有有很多级
  • root:可以理解为 最高级别的签发人 Issuer,负责认证 intermediates 身份的合法性

这其实代表了一个信任链条,最终的目的就是为了保证 end-user 证书是可信的,该证书的公钥也就是可信的。

二、签发证书和验证证书

2.1、签发 X509 证书

关于 CA 证书的生成,一般在我们实际内部项目中,大都是使用自签证书,ca.key 由自己生成和保管,使用 ca.key 签发下一层级的证书。下面我们就来操作下证书生成及签发这一过程。下图,包含了 CA 的生成和 Server 证书的生成两个步骤:

图3: CA 的生成和 Server 证书的生成两个步骤.png

2.1.1、生成CA根证书

可以先在桌面的 CustomSSL 文件夹里创建个 CustomCA 文件夹,方便管理

首先要创建CA私钥,得到ca.key,这里使用-des3进行加密,4096bit,需要四位以上密码

openssl genrsa -des3 -out ca.key 4096

根据私钥ca.key生成CA证书,得到ca.crt。这里会验证密码

openssl req -new -x509 -days 365 -key ca.key -out ca.crt

图4:生成CA根证书.png

2.1.2、生成证书请求文件CSR

创建服务器私钥,得到server.key

openssl genrsa -out server.key 4096

根据server.key生成证书请求文件CSR,得到server.csr

openssl req -new -key server.key -out server.csr

图5:生成证书请求文件CSR.png

2.1.3、签发证书

根据server.csr、ca.crt、ca.key签发自签证书,得到server.crt。需要输入生成ca.key时设置的密码。

openssl ca -in server.csr -out server.crt -cert ca.crt -keyfile ca.key -days 365

可能会遇到问题,提示找不到 ./demoCA/index.txt ,这时需要创建添加相应的目录。执行以下命令创建

mkdir -p ./demoCA/newcerts
touch demoCA/index.txt
touch demoCA/serial
echo 01 > demoCA/serial

图6:文件不存在问题.png

如有其他问题,可以自行网络搜索,一般都能搜到,按照网上说的操作即可。

成功情况下,如下:

图7:签发证书成功.png

2.1.4、查看证书及证书转换指令
2.1.4.1、查看证书详情信息

查看证书详情信息

openssl x509 -in server.crt -noout -text

图8:证书转换及查看证书.png

2.2、校验 X.509 证书

X.509 证书的校验方法(如下图),校验方法是基于证书信任链(Certificate Trusted Chain)的结构,由下层自上验证到顶层 Root 根,具体步骤为:

图9:校验证书信任链.png

  • 取上一级证书的公钥 public key,对下级证书的签名进行解密得出下级证书的摘要 Digest1
  • 对下级证书计算信息摘要(默认为 TBSCertificate),需要严格遵循 X509 的协议格式,得到 Digest2
  • 判断 Digest1=?Digest2,相等则说明下级证书校验通过
  • 依次对各个相邻级别证书实施 1--3 步骤,直到根证书(或者可信任锚点 trusted anchor

校验证书签名 signature(成功):

openssl verify -CAfile ca.crt server.crt

校验 public 的值(成功):

diff -eq < (openssl x509 -pubkey -noout -in server.crt) <(openssl rsa -pubout -in server.key)

图10:校验证书成功示例.png

2.1.4.2、openssl一些证书转换指令

pem转换为cer:openssl x509 -inform pem -in 你的证书名字.pem -outform der -out 你的新证书.cer

openssl x509 -inform pem -in server.pem -outform der -out server.cer

crt转换为pem:openssl x509 -in 你的证书名字.crt -out 你的新证书.pem

openssl x509 -in server.crt  -out server.pem

crt转换为cer:openssl x509 -in 你的证书.crt -out 你的新证书.cer -outform der

openssl x509 -in server.crt -out server.cer -outform der

生成p12文件

openssl pkcs12 -export -in server.crt -inkey server.key -out server.p12

获取cer证书公钥,data的base64字符串编码

openssl x509 -in server.cer -inform DER -noout -pubkey

p12文件中导出公钥和私钥

//1.生成1.key文件
openssl pkcs12 -in apple_payment.p12 -nocerts -nodes -out 1.key
//2.导出私钥
openssl rsa -in 1.key -out apple_pay_pri.pem
writing RSA key
//3.导出公钥
openssl rsa -in 1.key -pubout -out apple_pay_pub.pem

三、iOS证书校验及相关库的使用

可下载 Demo,相关方法存放在ZJHCertificateTool类中

3.1、Security框架介绍

Security框架用于保证应用程序所管理之数据的安全。该框架提供的接口可用于管理证书、公钥、私钥以及信任策略。它支持生成加密的安全伪随机数。同时,它也支持对证书和Keychain密钥进行保存,是用户敏感数据的安全仓库。

SecTrustRef:表示需要验证的信任对象(Trust Object),包含待验证的证书和支持的验证方法等。

SecTrustResultType:表示验证结果。其中 kSecTrustResultProceed表示serverTrust验证成功,且该验证得到了用户认可(例如在弹出的是否信任的alert框中选择always trust)。 kSecTrustResultUnspecified表示 serverTrust验证成功,此证书也被暗中信任了,但是用户并没有显示地决定信任该证书。 两者取其一就可以认为对serverTrust验证成功。

SecTrustEvaluate:函数内部递归地从叶节点证书到根证书验证。使用系统默认的验证方式验证Trust Object,根据上述证书链的验证可知,系统会根据Trust Object的验证策略,一级一级往上,验证证书链上每一级证书有效性。

3.2、iOS代码生成公私钥对

OSStatus SecKeyGeneratePair(CFDictionaryRef parameters, SecKeyRef  _Nullable *publicKey, SecKeyRef  _Nullable *privateKey);

系统提供了一系列关于RSA加解密和签名验签的方法,同时也提供了生成密钥对的方法。这样生成的密钥是SecKeyRef格式的,有时候可能会需要将生成的公钥提给其他人使用,就需要将密钥进行格式转换,最常见的就是转换成字符串的形式。幸运的是我们也可是通过一定的方式将SecKeyRef 转换为 NSData,然后再转化成base64类型的字符串。

3.3、iOS代码生成CSR文件

主要参考开源库的实现: ios-csr 。示例如下

/// 代码生成证书签名请求(.csr)文件
+ (NSData *)codeGenerateCSR {
    SecKeyRef privateKey = nil;
    SecKeyRef publicKey = nil;
    BOOL generateSucc = [self generateSecKeyPairWithKeySize:2048
                                               publicKeyRef:&publicKey
                                              privateKeyRef:&privateKey];
    if (!generateSucc) {
        return nil;
    }
    NSData *publicKeyBits = [self exportKeyDataFromSecKeyRef:publicKey];
    
    SCCSR *sccsr = [[SCCSR alloc]init];
    // 签发证书时,commonName相同的话,可能会导致失败:failed to update database TXT_DB error number 2
    // 这时换一下名称即可
    sccsr.commonName = @"www.codegenerate1.com";
    sccsr.organizationName = @"ZJH";
    sccsr.countryName = @"CN";

    NSData *certificateRequest = [sccsr build:publicKeyBits privateKey:privateKey];
    NSString *str = [certificateRequest base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength];

    NSString *strCertificateRequest = @"-----BEGIN CERTIFICATE REQUEST-----\n";
    strCertificateRequest = [strCertificateRequest stringByAppendingString:str];
    strCertificateRequest = [strCertificateRequest stringByAppendingString:@"\n-----END CERTIFICATE REQUEST-----\n"];
    NSLog(@"%@" , strCertificateRequest);
    
    NSData *csrData = [strCertificateRequest dataUsingEncoding:NSUTF8StringEncoding];
    
    NSString *path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
                                                          NSUserDomainMask,
                                                          YES) lastObject];
    NSString *filePath = [NSString stringWithFormat:@"%@/codegenerate.csr", path];
    NSLog(@"filePath : %@", filePath);
    [csrData writeToURL:[NSURL fileURLWithPath:filePath] atomically:YES];
    
    return csrData;
}

3.4、获取证书公钥

详见下面代码及注释:

/// 获取证书公钥
+ (NSData *)getPublicKeyRefrenceFromData:(NSData *)certificateData {
    // 获取证书信任对象
    SecTrustRef trust = [self getSecTrustWithCertificateData:certificateData];
    if (!trust) {
        return nil;
    }
    
    //  从 Trust 对象拷贝出公钥 (这一步可以先根据 Trust 对象来判断证书是否可信)
    id publicKey = (__bridge_transfer id)SecTrustCopyPublicKey(trust);
    
    if (trust) { // 释放资源
        CFRelease(trust);
    }

    // 转换成data,并返回
    NSData *publicKeyData = [self exportKeyDataFromSecKeyRef:(SecKeyRef)publicKey];
    return publicKeyData;
}

/// 获取证书信任对象
+ (SecTrustRef)getSecTrustWithCertificateData:(NSData *)certificateData {
    if (!certificateData) {
        return nil;
    }
    /* 从证书的DER表示形式创建证书对象
       allocator:您希望用于分配证书对象的CFAllocator对象。传递NULL以使用默认分配器。
       data:X.509证书的DER(杰出编码规则)表示。
       返回值:新创建的证书对象。当您完成此对象时,调用CFRelease函数以释放它。如果data的数据不是有效的DER编码的X.509证书,则返回NULL。*/
    SecCertificateRef cerRef = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData);
    if (!cerRef) { // 转换证书失败
        NSLog(@"Can not read certificate from %@", certificateData);
        return nil;
    }
    
    SecCertificateRef certificateArr[1] = {cerRef}; // 证书数组
    CFArrayRef cerCFArr = CFArrayCreate(NULL, (const void **)certificateArr, 1, NULL);
    SecPolicyRef policy = SecPolicyCreateBasicX509(); // 返回默认X.509策略的策略对象。
    SecTrustRef trust = nil;
    /* 基于证书和策略创建信任管理对象。
       certificates:要验证的证书,以及您认为可能对验证证书有用的任何其他证书。要验证的证书必须是数组中的第一个。如果您只想指定一个证书,您可以传递一个Sec对象;否则,传递一个Sec对象数组。
       policies:引用一个或多个要评估的政策。您可以传递单个Sec对象,或一个或多个Sec对象的数组。如果您通过多个策略,则所有策略都必须验证证书链才能被视为有效。您通常使用标准策略之一,如SecX509返回的策略。
       trust:返回时,指向新创建的信任管理对象。当您完成此对象时,调用CFRelease函数以释放它。*/
    OSStatus status = SecTrustCreateWithCertificates(cerCFArr, policy, &trust);
    if (status != noErr) {
        NSLog(@"SecTrustCreateWithCertificates fail. Error Code: %d", (int)status);
        CFRelease(cerRef);
        CFRelease(policy);
        CFRelease(cerCFArr);
        return nil;
    }
    
    SecTrustResultType result;
    /* 评估指定证书和策略的信任度。
       trust:要评估的信任管理对象。信任管理对象包括要验证的证书以及用于评估信任的一个或多个策略。它还可以选择包括用于验证第一个证书的其他证书。使用SecTrust函数创建信任管理对象。
       result:返回时,指向反映此评估结果的结果类型。有关可能值的描述,请参阅Sec。有关如何处理特定值的解释,请参阅下面的讨论。*/
    status = SecTrustEvaluate(trust, &result); // 建议子线程调用
    if (status != noErr) {
        NSLog(@"SecTrustEvaluate fail. Error Code: %d", (int)status);
        CFRelease(cerRef);
        CFRelease(policy);
        CFRelease(trust);
        CFRelease(cerCFArr);
        return nil;
    }
    
    return trust;
}

3.5、获取证书私钥

详见下面代码及注释:

/// 获取私钥
+ (NSData *)getPrivateKeyRefrenceFromData:(NSData*)p12Data password:(NSString*)password {
    SecKeyRef privateKeyRef = NULL;
    NSMutableDictionary * options = [[NSMutableDictionary alloc] init];
    [options setObject: password forKey:(__bridge id)kSecImportExportPassphrase];
    CFArrayRef items = CFArrayCreate(NULL, 0, 0, NULL);
    OSStatus securityError = SecPKCS12Import((__bridge CFDataRef) p12Data, (__bridge CFDictionaryRef)options, &items);
    if (securityError == noErr && CFArrayGetCount(items) > 0) {
        CFDictionaryRef identityDict = CFArrayGetValueAtIndex(items, 0);
        SecIdentityRef identityApp = (SecIdentityRef)CFDictionaryGetValue(identityDict, kSecImportItemIdentity);
        securityError = SecIdentityCopyPrivateKey(identityApp, &privateKeyRef);
        if (securityError != noErr) {
            privateKeyRef = NULL;
        }
    }
    CFRelease(items);
    
    // 转换成data,并返回
    NSData *privateKey = [self exportKeyDataFromSecKeyRef:(SecKeyRef)privateKeyRef];
    return privateKey;
}

3.6、证书校验

详见下面代码及注释:

/// 验证证书的合法性
+ (BOOL)validCertificate:(NSData *)rootCerData deviceCerData:(NSData *)deviceCerData {
    if (!rootCerData || rootCerData.length == 0 || !deviceCerData || deviceCerData.length== 0) {
        return NO;
    }
    
    // 获取需要验证的设备证书Trust
    SecTrustRef deviceTrust = [self getSecTrustWithCertificateData:deviceCerData];
    if (!deviceTrust) {
        return NO;
    }
        
    // 校验设备证书(nsbundle .cer)
    NSMutableArray *rootCertificates = [NSMutableArray array];
    /* 把证书data,用系统api转成 SecCertificateRef 类型的数据,SecCertificateCreateWithData函数
     对原先的pinnedCertificates做一些处理,保证返回的证书都是DER编码的X.509证书 */
    id cerObj = (__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)rootCerData);
    [rootCertificates addObject:cerObj];
    
    /* 将 rootCertificates 设置成需要参与验证的 Anchor Certificate
      ( 锚点证书,通过SecTrustSetAnchorCertificates设置了参与校验锚点证书之后,假如验证的数字证书是这个锚点证书的子节点,
        即验证的数字证书是由锚点证书对应CA或子CA签发的,或是该证书本身,则信任该证书 ),具体就是调用SecTrustEvaluate来验证。
     deviceTrust是需要被验证的证书。 */
    SecTrustSetAnchorCertificates(deviceTrust, (__bridge CFArrayRef)rootCertificates);
    
    // 验证设备证书
    BOOL isValid = NO;
    SecTrustResultType resultType;
    if (SecTrustEvaluate(deviceTrust, &resultType) == errSecSuccess) {
        isValid = (resultType == kSecTrustResultUnspecified || resultType == kSecTrustResultProceed);
    }
    
    // 释放资源
    if (deviceTrust) {
        CFRelease(deviceTrust);
    }
    
    return isValid;
}

参考链接:
苹果官方文档:https://developer.apple.com/documentation/security
证书(Certificate)的那些事:https://pandaychen.github.io/2019/07/24/auth/
iOS使用security.framework实现RSA加解密:https://blog.csdn.net/tammy_min/article/details/51720846
iOS RSA密钥的生成与转换:https://www.jianshu.com/p/5ba276c6cd87
生成 SecKeyRef 的正规方式:https://blog.csdn.net/u013712343/article/details/118900701
一次iOS客户端PKI及HTTPS双向认证的踩坑记录:https://juejin.cn/post/6953449442418622478