
上周同事把一个驱动包甩给我说在开发机上装得好好的到了客户那台 Windows 10 21H2 的机器上设备管理器立刻挂了个黄色感叹号属性页里写着Windows 无法验证此设备所需的驱动程序的数字签名代码 52。他的第一反应是 INF 写错了硬件 ID 匹配翻来覆去改了两个小时。我把压缩包解开看了一眼里面躺着 MyDriver.inf、MyDriver.sys唯独没有 MyDriver.cat。问题跟哪一行代码都没关系问题是这个驱动包少了一张封条。这篇内容就围绕 Windows 驱动程序里的 cat 文件展开。我会讲清楚它到底是什么、系统是凭什么信任它的、怎么用 WDK 里的工具自己造一张出来、WHQL 和测试签名这条路怎么选以及代码 52、代码 39 这类报错该从哪儿下手排查。适合已经写过或正在写 Windows 驱动的朋友也适合负责驱动打包发布、在客户现场做售后排查的同学——哪怕你暂时不写内核代码只要你的产品里有 INF 加 SYS 这种组合CAT 文件迟早会找你麻烦。1. CAT 文件在驱动包里到底扮演什么角色1.1 驱动包里的几个文件各管什么事一个能装得上的驱动包通常就四五个文件但很多同学对它们的职责划分是模糊的。INF 是说明书告诉系统这个驱动支持哪些硬件 ID、要拷贝哪些文件、注册哪些服务和注册表项SYS 是本体真正的内核态代码PNF 是系统在安装过程中根据 INF 生成的预编译版本属于安装产物而不是你打包时要带的东西而 CAT 文件是封条——它本身不含任何可执行代码只声明这个包里的这几个文件经过我认证内容就是这个样子没有被改过。这个分工带来一个很直观的后果SYS 负责能不能跑CAT 负责让不让跑。在 64 位 Windows 上一个没有有效 CAT 文件的内核驱动哪怕代码写得完美无瑕也照样加载不进去。很多人第一次遇到代码 52 的时候会本能地去怀疑 SYS 编译有问题其实系统根本没走到那一步——它在签名校验环节就把你拦下了。1.2 CAT 里装的不是代码而是一张哈希清单用makecat或直接看文件属性你会发现 CAT 文件本质上是一个 PKCS#7 签名结构里面挂着一张成员清单每个成员是一条记录记录了驱动包里某个文件的内容摘要。安装时系统会拿这张清单去和实际文件比对只要有一个字节对不上整张封条就作废。这里有个特别容易踩的坑目录里存的成员哈希是 Windows 那套目录接口CryptCATAdminCalcHashFromFileHandle这一类 API算出来的值它和你在命令行敲certutil -hashfile MyDriver.sys sha256得到的普通文件哈希在很多情况下并不是一个可以拿来直接对比的东西。我见过有同事为了验证哈希对不对把两个值贴在一起逐位比对比了半天得出不一样的结论然后开始怀疑微软的工具链有问题。想确认某个文件和目录里的记录是否匹配正确做法是用signtool verify指定CatalogFile去验让工具告诉你结论而不是自己手算。另外不同年代生成的 CAT 文件成员哈希用的算法是不一样的。老包里常见 SHA-1新工具链默认走 SHA-256。这本身不算错但如果你把两种不同算法生成的目录、二进制、签名证书混在一个包里发出去就会在新系统上碰到一些莫名其妙的拒绝。我的习惯是一个驱动包从第一天起就统一用 SHA-256不要留历史包袱。1.3 微软为什么不把签名直接塞进 SYS早期确实有把签名直接嵌在二进制文件里的做法现在很多 EXE、DLL 也还在用嵌入签名。但内核驱动选择了目录文件这条路原因有三点很实在。第一一个驱动包里往往不止一个 SYS。很多设备驱动会带一两个辅助 DLL、可能有用户态的配置程序、甚至还有几个不同架构的二进制。用一张 CAT 把整包一起签比逐个文件嵌签名要省事得多。第二签名是有成本的——不管走 WHQL 还是走微软的签名服务每提交一次都要等待、要验证。把文件清单集中到一张 CAT 上意味着你替换包里的某个小文件时只需要重新生成并重签这张 CAT流程更短。第三对系统来说整包一致比单文件可信更容易做统一策略安装时一次性校验不用边装边验。代价也来自同一个设计CAT 和文件内容是强绑定的。你只要在生成目录之后动过包里任何一个被收录的文件封条立刻失效。这个特性后面在排查环节会反复出现。2. 系统是怎么一步步信任一张 CAT 文件的2.1 校验的顺序先找成员再验签名最后看证书链理解校验链路排查时才能判断卡在哪一环。大致顺序是这样的系统拿到驱动包后先定位 CAT 文件——如果 INF 的[Version]段里写了CatalogFile就按这个名字找没写的话默认去找与 INF 同名、扩展名为 .cat 的文件。找到之后把目录信息注册进系统的目录数据库然后对包里每个受保护文件计算目录哈希在成员清单里查有没有对应记录。成员匹配上了接下来才是验签名把 CAT 文件当成一个普通的签名对象验证它的签名摘要、构建证书链、确认签发链能追到一个受信任的根同时检查证书的用途字段里有没有内核模式代码签名这个扩展密钥用法。最后还要看时间戳——这是很多人忽略的一环。如果这几步里任何一步失败报出来的错误各不相同代码 52 只是最笼统的那一种。所以现场排查的第一件事不是重装驱动而是用工具把这条链路走一遍看它断在哪。后面第 5 节我会给出具体的命令和输出解读。2.2 CatRoot 与 CatRoot2 这两个目录各自在干什么C:\Windows\System32\CatRoot下面有一个以 GUID 命名的子目录里面存的是被系统登记过的目录文件驱动安装时 CAT 会被复制到这里一份C:\Windows\System32\CatRoot2则是 CryptoAPI 用来做文件签名校验的目录数据库里面能看到数据库文件和日志。这两个目录都被系统保护直接删是删不掉的。它们和排查有什么关系关系很大。CAT 文件的校验结果在系统里是带缓存的如果一张旧封条留下的记录和你的新包发生了冲突表现就是文件明明换了报错还是老的。这种时候常见的处理手法是停掉加密服务、把 CatRoot2 重命名备份让系统重新构建这个数据库再重启。我要提醒一句做这个操作前一定确认机器上没有其他正在依赖加密服务的业务也别在业务高峰做——重建期间部分签名校验行为会短暂异常。这个操作属于清理缓存性质不是修根本问题如果根本原因是包本身有问题缓存清十遍也没用。2.3 时间戳决定签名能活多久的那一步证书是有有效期的代码签名证书一般一到三年。假设你用一张 2026 年过期的证书签了驱动不做任何额外处理那到了 2027 年系统在校验时看到的就是签名时证书已过期直接判无效。解决方案就是签名时同时打一个时间戳由时间戳服务出具一份证明说明这个签名是在某个时刻完成的那一刻证书是有效的。之后即使证书过期只要时间戳链验得过签名依然有效。signtool里和时间戳相关的参数有两组容易混淆老式的/t用 Authenticode 时间戳协议新式的/tr配/td用 RFC 3161 协议并且可以指定摘要算法。现在一律用后者摘要算法跟签名摘要保持一致。还有一个现实约束打时间戳需要联网访问时间戳服务器如果你的构建机在完全隔离的内网环境里这一步会直接失败signtool会明确告诉你时间戳服务不可达——这不是签名工具有问题是网络策略问题得提前规划好构建机的出网权限。3. 动手做一张 CAT 文件Inf2Cat 加 signtool 的完整流程3.1 工具链准备与目录结构约定需要的工具都在 WDK 里Inf2Cat.exe用来从 INF 生成目录文件signtool.exe用来签名和验证makecat.exe用来生成非驱动类的通用目录做驱动一般用不上。安装完 WDK 后它们通常在类似C:\Program Files (x86)\Windows Kits\10\bin\10.0.xxxxx.0\这样的路径下inf2cat.exe在 x86 子目录signtool.exe在 x64 子目录。我的习惯是把这两个工具路径加到一个构建脚本的变量里而不是依赖系统 PATH——因为开发机上经常并存好几个版本的 WDKPATH 里到底是哪一版很难说清。目录结构上我建议保持极简MyDriver\ MyDriver.inf MyDriver.sys MyDriver.cat (生成物随包发布) build_sign.bat (不随包发布)把构建脚本和要发布的文件分开是避免把调试脚本打进包里这类低级事故的最简单办法。3.2 INF 里必须写对的那几行CAT 能不能被自动识别取决于 INF 里有没有把这个名字说清楚。最小可用的写法是在[Version]段里加一行CatalogFile[Version] Signature $Windows NT$ Class System ClassGuid {4d36e97d-e325-11ce-bfc1-08002be10318} Provider %ProviderName% CatalogFile MyDriver.cat DriverVer 06/18/2026,1.0.0.1三个点值得强调。CatalogFile的值必须和实际文件名的大小写与拼写完全一致Windows 文件系统对大小写不敏感但工具和校验逻辑不一定这么宽容我曾经因为写了mydriver.cat而实际文件是MyDriver.cat在部分环境下静默失败过。DriverVer里的日期千万别写未来时间日期格式也要符合规范它直接参与驱动仓库里的版本排序。CatalogFile如果省略系统会去捡与 INF 同名的 .cat看似方便但一旦你的 INF 被重命名过现场很常见封条就找不到了。3.3 用 Inf2Cat 生成目录文件命令本身不复杂麻烦的是os参数C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x86\inf2cat.exe ^ /driver:C:\src\MyDriver ^ /os:10_X64,10_VB,Server10_X64 ^ /verbose这里最关键的一条经验是/os里列出的每一个目标系统版本都必须在 INF 的[Manufacturer]段里有对应的修饰节比如NTamd64.10.0...这种带版本号的节名否则 Inf2Cat 会直接报错退出提示某个目标在 INF 中找不到对应段落。反过来如果 INF 里声明了一堆目标版本而/os里没列全生成出来的目录文件覆盖范围就会比预期小现场表现是某个系统版本上装不上其他版本都正常。/os的合法取值每个 WDK 版本不完全一样最靠谱的办法是直接不带参数跑一次inf2cat.exe它会把当前版本支持的所有目标名称打印出来照着填就行不要凭记忆猜。生成成功后你会在/driver指定的目录里看到.cat文件同时verbose输出会列出被收录进目录的每一个文件——这张列表值得逐行看一眼确认该进去的都进去了尤其别漏掉那些运行时必须存在但容易被忽略的辅助 DLL。3.4 签名、验证与一个容易搞反的顺序问题生成完目录就可以签了signtool sign /v /fd sha256 /f mycert.pfx /p 密码 ^ /tr http://timestamp.digicert.com /td sha256 ^ C:\src\MyDriver\MyDriver.cat签完立刻验证别等打包之后signtool verify /v /kp /c MyDriver.cat MyDriver.sys signtool verify /v /kp MyDriver.cat第一条命令的意思是用指定的 CAT 去校验 SYS 文件的目录哈希和签名链/kp表示按内核模式代码签名策略检查。第二条是单独验 CAT 自身的签名。两条都过了才算一个合格的驱动包。顺序问题在这里如果驱动包里某个文件你还要额外做嵌入签名比如附带的用户态程序那必须在跑 Inf2Cat之前完成嵌入签名。因为嵌入签名会改变文件的字节内容进而改变它的目录哈希。先 Inf2Cat 后嵌签名等于亲手把自己的封条撕坏然后在现场对着代码 52 百思不得其解。我建议把整个流程固化成一个批处理构建 → 嵌入签名如有→ Inf2Cat → 签 CAT → 验证中间任何一步失败就中断绝不先生成再说。4. WHQL、证明签名与测试签名该怎么选4.1 新内核驱动为什么绕不开微软从 Windows 10 1607 开始新提交的内核模式驱动基本不可能再靠自己买一张代码签名证书再配交叉证书的方式获得系统信任了。现在的通行路径是提交到微软的硬件开发者门户走证明签名Attestation Signing或者完整 WHQL 认证由微软用自己的证书为你的目录文件签名。这个变化对中小团队影响很大——以前拿到证书就万事大吉现在必须注册账号、做企业身份验证、准备提交用的 CAB 包。不过老驱动是有豁免的在策略生效之前已经签过名、且带有效时间戳的驱动可以继续用。这也解释了为什么市场上很多老设备的驱动还能在 Win11 上装着走而你自己新写的一个却怎么都过不去。4.2 证明签名和 WHQL 的实际差别对比项证明签名WHQL 认证是否需要跑 HLK 测试不需要需要且要提交测试日志审核周期短通常当天到几天长取决于测试与返工轮次适用系统范围主要是 Windows 10/11 桌面版覆盖更广含服务器与部分旧版本能否上 Windows Update 分发一般不能可以适合场景内部部署、直发给客户的成品面向海量终端、OEM 预装的成品我的建议很直接给客户直发的中小规模驱动走证明签名就够了省下的时间远远超过它带来的限制如果产品要预装进别人的整机、或者要靠系统更新渠道分发那就别省这个功夫老老实实跑 WHQL。4.3 测试签名的边界能用但绝不能发出去开发调试阶段没必要每次都走在线提交本地测试签名就够了。核心是两步用自己生成的自签名证书或者团队内部 CA 签发的证书给 CAT 签名并且把这张证书装进目标测试机的受信任的根证书颁发机构和受信任的发布者两个存储区里然后用管理员权限执行bcdedit /set testsigning on重启后桌面右下角会出现测试模式的标识。这个标识本身就是一道提醒这台机器上任何签名不严格的驱动都能加载安全性是被主动削弱的。有几个边界必须记住开启测试模式要求安全启动处于关闭状态很多新机器出厂默认开着安全启动你改完还得处理这一层测试模式下的驱动拿到任何生产环境都是装不上的别抱着先让客户开测试模式凑合一下的想法这是把安全问题从签名层面转移到了客户的管理层面。如果只是临时加载一次驱动做验证也可以用高级启动菜单里的禁用驱动程序强制签名选项一次性生效重启后失效适合现场临时救急但同样不能作为交付方案。5. 代码 52、代码 39 现场排查的完整链路5.1 第一步永远是先看签名本身拿到一台报错的机器别急着动系统先把驱动包拷到本地在命令行里跑# 看 CAT 自身的签名状态 Get-AuthenticodeSignature .\MyDriver.cat | Format-List * # 用 CAT 校验 SYS 是否匹配 signtool verify /v /kp /c .\MyDriver.cat .\MyDriver.syssigntool的输出信息量很大要养成逐段读的习惯。看到Signing Certificate Chain之后重点看三件事链有没有到一条系统信任的根证书的用途是不是内核模式代码签名有没有The signature is timestamped这一行。如果链是断的它会明确写链处理成功但终止于不受信任的根如果没有签就直接是找不到签名。这些信息比设备管理器里那个笼统的错误码有用一百倍。5.2 哈希不匹配改过东西之后的典型症状如果签名本身没问题但把 SYS 放进去校验就失败提示文件不在目录中、或者哈希与目录记录不一致那基本可以断定这张 CAT 和你手上的二进制不是同一批产物。常见的三种成因我在现场都碰到过。一是改完代码只重新编译了 SYS忘了重新生成和签名 CAT。这是最普遍的。二是打包环节出了问题——有人用某个压缩工具重新压了一遍包工具顺手改了文件时间戳或者做了某种优化内容变了哈希自然就变了。所以发布包尽量只压一次压完别再动。三是构建系统做了可重现构建的调整或者链接器参数变了导致每次构建出来的二进制哈希都不同而发布流程里用的还是几天前生成的那张 CAT。我把这条总结成一句硬规矩**CAT 和二进制必须同源同批任何一个字节的变化都必须走一遍完整生成流程。**别想着只改了一个常量不用重签。5.3 证书链、吊销与时间戳这三处的坑链的问题在现场占比很高。最常见的是目标机器上没有安装中间证书只装了根证书看起来根是信的就行实际上链构不出来。解决办法是在驱动包里附带中间证书或者通过部署脚本把中间证书装进目标机的中间证书存储区。其次是吊销检查。带时间戳的签名正常情况下即使证书过期也不影响校验但如果系统在校验时必须去查吊销列表而网络又不可达也可能报出让人摸不着头脑的错误。这类问题的稳妥处理是保证签名带合规时间戳并把完整的证书链部署到目标环境而不是去关掉系统的吊销检查——关掉等于自己给自己挖安全坑。第三是自签名场景很多同学在测试机上验得好好的一换机器就报代码 52原因就是那台新机器没装你的自签证书。这也是为什么测试签名只能自己用。5.4 驱动仓库里的旧版本抢位还有一种特别隐蔽的情况报错和签名完全无关纯属装上了但装的不是你想装的。驱动仓库C:\Windows\System32\DriverStore\FileRepository里会长期保留历史上装过的每个驱动包如果新包的DriverVer没有比旧包高系统在排序时会更倾向于已有的那个表现就是明明拷贝了新文件设备属性里的驱动版本还是老的。排查手法是先枚举pnputil /enum-drivers看清楚当前有哪些第三方驱动包进来过找出可疑的那几个确认不需要后用pnputil /delete-driver oem12.inf /uninstall /force清掉再重装。这类问题的几个典型错误码对照如下错误码常见含义优先怀疑的方向代码 52无法验证数字签名CAT 缺失、哈希不匹配、证书链不全、无时间戳代码 39驱动文件损坏或缺失目录哈希与文件不符、包被二次修改过代码 31无法加载设备所需驱动服务注册项有问题、SYS 与 CAT 版本不一致代码 28未安装驱动程序INF 与硬件 ID 不匹配、目录未收录 INF 声明的文件注意代码 39 和 52 在表象上很接近都是装上了但用不了区分点是 39 更偏向文件内容层面不一致52 更偏向签名信任层面过不去。用上面那两条signtool命令一跑基本就能把方向定下来。6. 批量部署与长期维护中那些不成文的习惯6.1 用 pnputil 和 DISM 批量导入时的注意点单机装驱动点两下 INF 就够了批量场景下通常用pnputil /add-driver MyDriver.inf /install或者用 DISM 把驱动打进系统镜像。批量导入有两个细节容易忽略一是导入操作会把 CAT 一并注册进系统目录数据库所以包必须完整不能出现为了省空间只发 INF 和 SYS这种操作二是在离线镜像上集成驱动时目标系统版本的判定是按镜像版本来的如果你的 CAT 里没有收录那个版本的os目标集成会成功但首次启动时加载失败——这种问题在镜像里很难查最好在集成前就用signtool把包的覆盖范围确认清楚。另外导入成功不代表版本一定生效pnputil /add-driver之后的排序规则还是看DriverVer和硬件匹配度所以发版时版本号一定要递增别回退。6.2 把 CAT 纳入版本管理和发布检查清单我吃过的最贵的一次亏是一个紧急修复版本在凌晨发出去第二天客户反馈大面积代码 52。复盘发现那个版本只重新编译了 SYSCAT 用的还是前一天的因为时间太紧签名流程要排队。从那次之后我在每个驱动项目里都放了一张发布前的检查清单任何人发版前必须逐项打勾二进制与 CAT 来自同一次构建构建日志里能对上时间戳INF 里CatalogFile的拼写与实际文件名逐字符一致signtool verify /v /kp /c对包里每个受保护文件都通过签名带有效时间戳且时间戳摘要算法与签名摘要一致打包后没有再对包内文件做任何改动压缩包本体只生成一次在一台干净的、没装过该驱动的目标系统上做过一次完整安装验证这张清单看着啰嗦但它拦住的问题每一个都值一个通宵。6.3 签名有效不等于一定能加载最后一个容易被忽略的点即使你的签名链完整、时间戳有效、目录哈希全对驱动也可能被拒绝加载。较新版本的 Windows 内置了易受攻击的驱动程序阻止列表某些历史上有安全问题的老驱动、或者被滥用的签名驱动会被直接拦下企业环境里还可能通过应用控制策略限定只允许特定发布者的驱动加载这种情况下你签名再完美也过不了策略。所以现场遇到签名都验过了还是装不上的情况别只在签名这一层打转去看一眼系统策略和阻止列表的状态往往答案在那儿。这种情况不常见但一旦碰上方向错了能白折腾一整天。我把这几年和 CAT 文件打交道的体会压成一句它的作用就是在驱动加载的最后一公里上用哈希和证书两道锁把这个包没被动过手脚这件事说清楚。工具就那么几个命令也不长真正费时间的从来不是生成它而是在出问题的时候能顺着成员哈希、签名链、时间戳、目录缓存这条线快速判断卡在哪一环。你可以先把第 5 节那两条验证命令存成自己的快捷脚本下次再看到代码 52先跑一遍再动手比盲改 INF 快得多。