SSL Pinning 的进阶策略:动态 Pinning、备份 Pinning、多域名 Pinning

静态 Pinning 虽然简单,但有个致命问题——证书换不了。我早年在一个金融项目里就吃过这个亏,证书过期那天,线上用户集体断连,紧急发版都来不及。从那以后,我对动态 Pinning 和备份 Pinning 就格外上心。

说白了,静态 Pinning 是把证书钉死在代码里。动态 Pinning 则是让钉子的位置可以远程调整。备份 Pinning 是给钉子加个保险。多域名 Pinning 嘛,就是同时钉好几个目标。

咱们一个一个聊。

动态 Pinning:让证书策略活起来

动态 Pinning 的核心思路很简单:证书指纹不写死在代码里,而是从远程服务器获取。这样证书更新时,你只需要改服务端的配置,客户端不用发版。

我个人习惯的做法是这样的:

  1. App 启动时,先请求一个配置接口,拿到当前有效的证书指纹列表
  2. 把这个指纹列表缓存到本地(SharedPreferences 或本地文件)
  3. 后续的 HTTPS 请求,用这个列表做 Pinning 校验
  4. 如果配置接口请求失败,就用上一次缓存的指纹

来看一段核心代码:

// 动态 Pinning 的核心逻辑
public class DynamicPinningInterceptor implements Interceptor {
    private static final String CONFIG_URL = "https://config.yourdomain.com/pinning.json";
    private Set<String> pinnedHashes = new HashSet<>();
    
    @Override
    public Response intercept(Chain chain) throws IOException {
        // 1. 先尝试更新 Pinning 配置
        refreshPinningConfig();
        
        // 2. 获取目标证书的 SHA256 指纹
        Request request = chain.request();
        Response response = chain.proceed(request);
        String certHash = extractCertHash(response);
        
        // 3. 校验指纹是否在白名单中
        if (!pinnedHashes.contains(certHash)) {
            throw new SSLPeerUnverifiedException("证书指纹不匹配!");
        }
        return response;
    }
    
    private void refreshPinningConfig() {
        // 从远程获取最新的证书指纹列表
        // 失败时使用本地缓存
    }
}

关键点:动态 Pinning 的配置接口本身也要走 HTTPS,而且最好也做 Pinning。不然就陷入「先有鸡还是先有蛋」的死循环了。

我的经验:配置接口的域名和业务接口的域名分开。这样即使业务域名证书出问题,配置接口还能正常工作,App 就能通过动态更新来「自救」。

备份 Pinning:给证书策略上双保险

备份 Pinning 是什么意思?就是除了主证书,你还得准备一个或多个备用证书的指纹。一旦主证书失效,备用证书能顶上。

我曾经在一个 IoT 项目里遇到过这种情况:设备端的根证书因为 CA 机构被吊销,所有设备一夜之间全部离线。幸好我们做了备份 Pinning,紧急切换到备用 CA 的证书,才没造成更大损失。

备份 Pinning 的典型做法:

  • 主证书:当前正在使用的服务器证书
  • 备用证书 1:同一 CA 签发的备用证书(提前申请好)
  • 备用证书 2:不同 CA 签发的证书(防止 CA 本身出问题)
  • 备用证书 3:自签名证书(最后的保底方案)

代码实现上,其实就是把多个指纹放到一个集合里:

// 备份 Pinning 的指纹集合
Set<String> backupPins = new HashSet<>() {{
    // 主证书指纹
    add("sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=");
    // 备用证书 1(同一 CA)
    add("sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=");
    // 备用证书 2(不同 CA)
    add("sha256/CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC=");
    // 备用证书 3(自签名)
    add("sha256/DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD=");
}};

注意:备用证书的私钥一定要妥善保管。我见过有人把备用证书的私钥直接放在 Git 仓库里,那跟没做 Pinning 有什么区别?

多域名 Pinning:一个 App 对接多个服务

现在的 App 很少只连一个后端。API 服务器、图片 CDN、日志上报、推送服务……每个域名都可能需要独立的 Pinning 策略。

多域名 Pinning 的核心是:为每个域名维护独立的证书指纹列表。你不能用一个指纹去校验所有域名,那会出大问题。

我建议的做法是维护一个域名到指纹列表的映射表:

// 多域名 Pinning 配置
Map<String, Set<String>> domainPins = new HashMap<>() {{
    // API 服务
    put("api.yourdomain.com", new HashSet<>() {{
        add("sha256/AAAA...");
        add("sha256/BBBB...");
    }});
    
    // 图片 CDN
    put("cdn.yourdomain.com", new HashSet<>() {{
        add("sha256/CCCC...");
        add("sha256/DDDD...");
    }});
    
    // 日志上报
    put("log.yourdomain.com", new HashSet<>() {{
        add("sha256/EEEE...");
        add("sha256/FFFF...");
    }});
}};

然后在拦截器里根据请求的 host 来选择对应的指纹列表:

@Override
public Response intercept(Chain chain) throws IOException {
    Request request = chain.request();
    String host = request.url().host();
    
    // 获取该域名对应的 Pinning 列表
    Set<String> pins = domainPins.get(host);
    if (pins == null) {
        // 未配置的域名,走默认策略
        return chain.proceed(request);
    }
    
    // 校验证书指纹
    String certHash = extractCertHash(chain.proceed(request));
    if (!pins.contains(certHash)) {
        throw new SSLPeerUnverifiedException(
            "域名 " + host + " 证书指纹不匹配"
        );
    }
    
    return chain.proceed(request);
}

核心原则:每个域名独立管理,互不影响。一个域名的证书更新不会影响其他域名的正常通信。

三种策略的对比与选择

策略 适用场景 优点 缺点
静态 Pinning 证书极少变更、可控制发版节奏 实现简单,无额外依赖 证书更新必须发版
动态 Pinning 证书频繁变更、需要远程控制 无需发版即可更新 依赖配置接口可用性
备份 Pinning 高可用要求、容灾场景 证书失效时有备用方案 管理多个证书较复杂
多域名 Pinning 多后端服务、微服务架构 域名间隔离,互不影响 配置量大,维护成本高

实际项目中,这三种策略往往是组合使用的。比如:动态 Pinning + 备份 Pinning,或者多域名 Pinning + 静态 Pinning。没有银弹,只有最适合你业务场景的方案。

知识体系总览

下面这张图把三种进阶策略的关系梳理清楚了:

SSL Pinning 进阶策略体系 SSL Pinning 动态 Pinning 备份 Pinning 多域名 Pinning 远程配置获取 本地缓存兜底 主证书指纹 备用证书指纹 多 CA 备份 域名-指纹映射 独立校验逻辑 组合使用:动态 + 备份 + 多域名 = 高可用 Pinning 方案 根据业务场景灵活搭配,没有银弹

避坑指南:我曾经在一个项目里同时用了动态 Pinning 和备份 Pinning,但忘了给配置接口本身做 Pinning。结果配置接口被中间人攻击,返回了错误的指纹列表,所有业务请求全部被拦截。嗯,这个坑我替你们踩过了。

最后说一句:Pinning 策略没有绝对的对错,只有合不合适。你的 App 证书一年换一次?静态 Pinning 就够了。你的 App 对接了十几个微服务?多域名 Pinning 是标配。你的 App 对可用性要求极高?动态 + 备份 Pinning 必须上。

想清楚你的业务场景,再选策略。别为了炫技而过度设计。


公众号:蓝海资料掘金营,微信deep3321