处理来自第三方仓库的外部依赖项和插件会将您的构建置于风险之中。特别是,您需要了解通过传递性引入了哪些二进制文件,以及它们是否合法。为了降低安全风险并避免在项目中集成受损的依赖项,Gradle 支持依赖项验证

从本质上讲,依赖项验证是一个不便使用的功能。这意味着每当您更新依赖项时,构建很可能会失败。这意味着合并分支会更加困难,因为每个分支可能具有不同的依赖项。这意味着您会很想将其关闭。

那么您为什么要费心呢?

依赖项验证关乎您获取和发布的内容的信任

如果没有依赖项验证,攻击者很容易就会破坏您的供应链。有很多现实世界的例子表明工具因添加恶意依赖项而被破坏。依赖项验证旨在通过强制您确保构建中包含的 Artifact 是您预期的 Artifact,从而保护您免受这些攻击。然而,它并非旨在阻止您包含易受攻击的依赖项。

在安全性和便利性之间找到正确的平衡点很困难,但 Gradle 会尽量让您选择适合您的“正确级别”。

依赖项验证包含两个不同且互补的操作

  • 校验和验证,用于断言依赖项的完整性

  • 签名验证,用于断言依赖项的来源

Gradle 支持开箱即用的校验和和签名验证,但默认不执行任何依赖项验证。本节将指导您根据需要正确配置依赖项验证。

此功能可用于

  • 检测受损依赖项

  • 检测受损插件

  • 检测本地依赖项缓存中被篡改的依赖项

启用依赖项验证

验证元数据文件

目前,依赖项验证元数据的唯一来源是此 XML 配置文件。Gradle 的未来版本可能包含其他来源(例如通过外部服务)。

一旦发现依赖项验证的配置文件,依赖项验证就会自动启用。此配置文件位于 $PROJECT_ROOT/gradle/verification-metadata.xml。该文件至少包含以下内容

<?xml version="1.0" encoding="UTF-8"?>
<verification-metadata xmlns="https://schema.gradle.org/dependency-verification"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="https://schema.gradle.org/dependency-verification https://schema.gradle.org/dependency-verification/dependency-verification-1.3.xsd">
   <configuration>
      <verify-metadata>true</verify-metadata>
      <verify-signatures>false</verify-signatures>
    </configuration>
</verification-metadata>

这样做,Gradle 将使用校验和验证所有 Artifact,但不会验证签名。Gradle 将验证使用其依赖项管理引擎下载的任何 Artifact,包括但不限于

  • 构建期间使用的 Artifact 文件(例如 jar 文件、zip 文件等)

  • 元数据 Artifact(POM 文件、Ivy 描述符、Gradle 模块元数据)

  • 插件(项目插件和设置插件)

  • 使用高级依赖项解析 API 解析的 Artifact

Gradle 不会验证变更依赖项(特别是 SNAPSHOT 依赖项)或本地生成的 Artifact(通常是构建过程中生成的 jar),因为它们的校验和和签名本质上总会发生变化。

使用这样一个最简配置文件,任何使用任何外部依赖项或插件的项目都会立即开始失败,因为它不包含任何用于验证的校验和。

依赖项验证的范围

依赖项验证配置是全局的:一个文件用于配置整个构建的验证。特别是,子项目和 buildSrc 都使用同一个文件。

如果使用了包含的构建

  • 使用当前构建的配置文件进行验证

  • 因此,如果包含的构建本身使用验证,则其配置将被忽略,而优先使用当前配置

  • 这意味着包含一个构建的工作方式类似于升级依赖项:它可能需要您更新当前的验证元数据

因此,一个简单的入门方法是为现有构建生成最简配置。

配置控制台输出

默认情况下,如果依赖项验证失败,Gradle 将生成一份关于验证失败的简短摘要,以及一份包含失败详细信息的 HTML 报告。如果您的环境阻止您读取此 HTML 报告文件(例如,如果您在 CI 上运行构建且不易获取远程 Artifact),Gradle 提供了一种选择启用详细控制台报告的方式。为此,您需要在 gradle.properties 文件中添加此 Gradle 属性

org.gradle.dependency.verification.console=verbose

引导依赖项验证

值得一提的是,虽然 Gradle 可以为您生成依赖项验证文件,但您应该始终检查 Gradle 为您生成的内容,因为您的构建可能已经包含受损的依赖项而您却不知道。有关更多信息,请参阅相应的校验和验证签名验证部分。

如果您计划使用签名验证,请同时阅读文档的相应部分

引导可用于从头开始创建一个文件,也可用于更新现有文件中的新信息。因此,建议在开始引导后始终使用相同的参数。

可以使用以下 CLI 指令生成依赖项验证文件

gradle --write-verification-metadata sha256 help

write-verification-metadata 标志需要指定您要生成的校验和列表或用于签名pgp

执行此命令行将导致 Gradle

  • 解析所有可解析配置,包括

    • 根项目中的配置

    • 所有子项目中的配置

    • buildSrc 中的配置

    • 包含的构建配置

    • 插件使用的配置

  • 下载解析过程中发现的所有 Artifact

  • 计算请求的校验和并根据您的要求可能验证签名

  • 在构建结束时,生成包含推断出的验证元数据的配置文件

因此,verification-metadata.xml 文件将在后续构建中用于验证依赖项。

有些依赖项是 Gradle 无法通过这种方式发现的。特别是,您会注意到上面的 CLI 使用了 help 任务。如果您不指定任何任务,Gradle 也会自动运行默认任务并在构建结束时生成配置文件。

区别在于 Gradle 可能会根据您执行的任务发现更多依赖项和 Artifact。事实上,Gradle 无法自动发现分离配置,这本质上是作为任务执行的内部实现细节解析的依赖图:它们特别是不会被声明为任务的输入,因为它们实际上依赖于任务执行时的配置。

一个好的开始方法是使用最简单的任务 help,它会尽可能多地发现依赖项,如果后续构建因验证错误而失败,您可以重新执行生成,并使用适当的任务来“发现”更多依赖项。

Gradle 不会验证使用其自身 HTTP 客户端的插件的校验和或签名。只有使用 Gradle 提供的基础设施来执行请求的插件,其请求才会被验证。

使用生成进行增量更新

Gradle 生成的验证文件对其所有内容都有严格的顺序。它还利用现有状态的信息将更改限制在最低限度。

这意味着生成实际上是更新验证文件的一个便捷工具

  • Gradle 生成的校验和条目将具有明确的 origin,以“由 Gradle 生成”开头,这很好地表明该条目需要进行评审,

  • 手动添加的条目将立即被记录在案,并在写入文件后出现在正确的位置,

  • 文件的头部注释将被保留,即根 XML 节点之前的注释。这允许您拥有一个许可证头或关于使用哪些任务和哪些参数来生成该文件的说明。

凭借上述优点,只需再次生成文件并评审更改,就可以非常容易地记录新的依赖项或依赖项版本。

使用模拟运行模式

默认情况下,引导是增量的,这意味着如果您多次运行它,信息会添加到文件中,特别是您可以依靠您的 VCS 来检查差异。在某些情况下,您只想查看生成的验证元数据文件是什么样子,而无需实际更改现有文件或覆盖它。

为此,您只需添加 --dry-run

gradle --write-verification-metadata sha256 help --dry-run

然后,不是生成 verification-metadata.xml 文件,而是会生成一个新文件,名为 verification-metadata.dryrun.xml

由于 --dry-run 不执行任务,这将快得多,但会错过任务执行时发生的任何解析。

禁用元数据验证

默认情况下,Gradle 不仅会验证 Artifact(jar 等),还会验证与这些 Artifact 关联的元数据(通常是 POM 文件)。验证元数据可确保最高级别的安全性:元数据文件通常会说明将包含哪些传递依赖项,因此受损的元数据文件可能会导致在图中引入不需要的依赖项。然而,由于所有 Artifact 都经过验证,通常您会很容易发现此类 Artifact,因为它们会引起校验和验证失败(验证元数据中会缺少校验和)。由于元数据验证会显著增加配置文件的大小,您可能因此希望禁用元数据验证。如果您了解这样做的风险,请在配置文件中将 <verify-metadata> 标志设置为 false

<?xml version="1.0" encoding="UTF-8"?>
<verification-metadata xmlns="https://schema.gradle.org/dependency-verification"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="https://schema.gradle.org/dependency-verification https://schema.gradle.org/dependency-verification/dependency-verification-1.3.xsd">
   <configuration>
      <verify-metadata>false</verify-metadata>
      <verify-signatures>false</verify-signatures>
    </configuration>
    <!-- the rest of this file doesn't need to declare anything about metadata files -->
</verification-metadata>

验证依赖项校验和

校验和验证可让您确保 Artifact 的完整性。这是 Gradle 能为您做的最简单的事情,以确保您使用的 Artifact 未被篡改。

Gradle 支持 MD5、SHA1、SHA-256 和 SHA-512 校验和。然而,现在只有 SHA-256 和 SHA-512 校验和被认为是安全的。

添加 Artifact 的校验和

外部组件由 GAV 坐标标识,然后每个 Artifact 由其文件名标识。要声明 Artifact 的校验和,您需要在验证元数据文件中添加相应的节。例如,要声明 Apache PDFBox 的校验和。GAV 坐标是

  • group org.apache.pdfbox

  • name pdfbox

  • version 2.0.17

使用此依赖项将触发下载 2 个不同的文件

  • pdfbox-2.0.17.jar,这是主 Artifact

  • pdfbox-2.0.17.pom,这是与此 Artifact 关联的元数据文件

因此,您需要声明它们的校验和(除非您禁用了元数据验证

<?xml version="1.0" encoding="UTF-8"?>
<verification-metadata xmlns="https://schema.gradle.org/dependency-verification"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="https://schema.gradle.org/dependency-verification https://schema.gradle.org/dependency-verification/dependency-verification-1.3.xsd">
   <configuration>
      <verify-metadata>true</verify-metadata>
      <verify-signatures>false</verify-signatures>
   </configuration>
   <components>
      <component group="org.apache.pdfbox" name="pdfbox" version="2.0.17">
         <artifact name="pdfbox-2.0.17.jar">
            <sha512 value="7e11e54a21c395d461e59552e88b0de0ebaf1bf9d9bcacadf17b240d9bbc29bf6beb8e36896c186fe405d287f5d517b02c89381aa0fcc5e0aa5814e44f0ab331" origin="PDFBox Official site (https://pdfbox.apache.org/download.cgi)"/>
         </artifact>
         <artifact name="pdfbox-2.0.17.pom">
            <sha512 value="82de436b38faf6121d8d2e71dda06e79296fc0f7bc7aba0766728c8d306fd1b0684b5379c18808ca724bf91707277eba81eb4fe19518e99e8f2a56459b79742f" origin="Generated by Gradle"/>
         </artifact>
      </component>
   </components>
</verification-metadata>

从哪里获取校验和?

通常,校验和与 Artifact 一起发布在公共仓库上。但是,如果仓库中的依赖项受到威胁,其校验和也很可能受到威胁,因此最好从不同的地方获取校验和,通常是库本身的网站。

事实上,将 Artifact 的校验和发布在与 Artifact 本身托管的服务器不同的服务器上是一个良好的安全实践:同时在仓库和官方网站上破坏一个库更困难。

在上面的示例中,JAR 的校验和发布在网站上,但 POM 文件没有。这就是为什么通常更容易让 Gradle 生成校验和并仔细检查生成的文件进行验证。

在此示例中,我们不仅可以检查校验和是否正确,还可以在官方网站上找到它,这就是为什么我们将 sha512 元素上的 origin 属性值从 Generated by Gradle 更改为 PDFBox Official site 的原因。更改 origin 让用户了解您的构建有多可信。

有趣的是,使用 pdfbox 将需要比那 2 个 Artifact 多得多的内容,因为它还会引入传递依赖项。如果依赖项验证文件仅包含您使用的主 Artifact 的校验和,则构建将失败并出现如下错误

Execution failed for task ':compileJava'.
> Dependency verification failed for configuration ':compileClasspath':
    - On artifact commons-logging-1.2.jar (commons-logging:commons-logging:1.2) in repository 'MavenRepo': checksum is missing from verification metadata.
    - On artifact commons-logging-1.2.pom (commons-logging:commons-logging:1.2) in repository 'MavenRepo': checksum is missing from verification metadata.

这表明您的构建在执行 compileJava 时需要 commons-logging,但验证文件没有足够的信息让 Gradle 验证依赖项的完整性,这意味着您需要将所需信息添加到验证元数据文件中。

有关此情况下的更多见解,请参阅依赖项验证故障排除

哪些校验和会被验证?

如果依赖项验证元数据文件为一个依赖项声明了不止一个校验和,Gradle 会验证所有这些校验和,并且如果其中任何一个失败,构建就会失败。例如,以下配置会同时检查 md5sha1 校验和

<component group="org.apache.pdfbox" name="pdfbox" version="2.0.17">
   <artifact name="pdfbox-2.0.17.jar">
      <md5 value="c713a8e252d0add65e9282b151adf6b4" origin="official site"/>
      <sha1 value="b5c8dff799bd967c70ccae75e6972327ae640d35" origin="official site" reason="Additional check for this artifact"/>
   </artifact>
</component>

您可能希望这样做的原因有很多

  1. 官方网站不发布安全校验和(SHA-256、SHA-512),但发布多个不安全的校验和(MD5、SHA1)。虽然伪造 MD5 校验和很容易,伪造 SHA1 校验和困难但可能,但同时伪造同一个 Artifact 的两个校验和则更难。

  2. 您可能希望将生成的校验和添加到上述列表中

  3. 在用更安全的校验和更新依赖项验证文件时,您不想意外擦除校验和

验证依赖项签名

除了校验和之外,Gradle 还支持签名验证。签名用于评估依赖项的来源(它说明是谁签署了 Artifact,这通常对应于谁生产了它)。

由于启用签名验证通常意味着更高级别的安全性,您可能希望用签名验证取代校验和验证。

签名可以像校验和一样用于评估依赖项的完整性。签名是对 Artifact 的哈希进行的签名,而不是 Artifact 本身。这意味着,如果签名是在不安全的哈希(甚至是 SHA1)上进行的,那么您就没有正确评估文件的完整性。因此,如果您同时关注两者,则需要将签名和校验和都添加到您的验证元数据中。

但是

  • Gradle 只支持验证发布在远程仓库上的 ASCII-armored PGP 文件签名

  • 并非所有 Artifact 都随附签名发布

  • 好的签名并不意味着签署者合法

因此,签名验证通常会与校验和验证一起使用。

关于过期密钥

使用过期密钥签署的 Artifact 非常常见。这对验证来说不是问题:密钥过期主要用于避免使用被盗密钥进行签署。如果 Artifact 在过期前签署,它仍然是有效的。

启用签名验证

由于验证签名更加昂贵(无论是 I/O 还是 CPU 方面),并且手动检查更困难,因此默认情况下不启用此功能。

启用此功能需要您更改 verification-metadata.xml 文件中的配置选项

<?xml version="1.0" encoding="UTF-8"?>
<verification-metadata xmlns="https://schema.gradle.org/dependency-verification"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="https://schema.gradle.org/dependency-verification https://schema.gradle.org/dependency-verification/dependency-verification-1.3.xsd">
   <configuration>
      <verify-signatures>true</verify-signatures>
   </configuration>
</verification-metadata>

理解签名验证

一旦启用签名验证,对于每个 Artifact,Gradle 将

  • 尝试下载相应的 .asc 文件

  • 如果存在

    • 自动下载执行签名验证所需的密钥

    • 使用下载的公钥验证 Artifact

    • 如果签名验证通过,则执行额外的请求校验和验证

  • 如果不存在,则回退到校验和验证

也就是说,如果启用签名验证,Gradle 的验证机制比仅使用校验和验证要强大得多。特别是

  • 如果一个 Artifact 使用多个密钥签署,则所有密钥都必须通过验证,否则构建将失败

  • 如果一个 Artifact 通过验证,为该 Artifact 配置的任何附加校验和也将被检查

然而,一个 Artifact 通过签名验证并不意味着您可以信任它:您需要信任这些密钥

实际上,这意味着您需要列出您信任的每个 Artifact 的密钥,这是通过添加一个 pgp 条目而不是例如 sha1 来完成的

<component group="com.github.javaparser" name="javaparser-core" version="3.6.11">
   <artifact name="javaparser-core-3.6.11.jar">
      <pgp value="8756c4f765c9ac3cb6b85d62379ce192d401ab61"/>
   </artifact>
</component>

对于 pgptrusted-key 元素,Gradle 要求使用完整的指纹 ID(例如 b801e2f8ef035068ec1139cc29579f18fa8fd93b,而不是长 ID 29579f18fa8fd93b)。这最大程度地减少了碰撞攻击的可能性。

目前,V4 密钥指纹长度为 160 位(40 个字符)。我们接受更长的密钥,以便在引入更长的密钥指纹时具有前瞻性。

ignore-key 元素中,可以使用指纹或长(64 位)ID。较短的 ID 只会导致更大的排除范围,因此使用是安全的。

这实际上意味着,如果 com.github.javaparser:javaparser-core:3.6.11 使用密钥 8756c4f765c9ac3cb6b85d62379ce192d401ab61 签署,您就可以信任它。

否则,构建将失败并出现此错误

> Dependency verification failed for configuration ':compileClasspath':
    - On artifact javaparser-core-3.6.11.jar (com.github.javaparser:javaparser-core:3.6.11) in repository 'MavenRepo': Artifact was signed with key '8756c4f765c9ac3cb6b85d62379ce192d401ab61' (Bintray (by JFrog) <****>) and passed verification but the key isn't in your trusted keys list.

Gradle 在错误消息中显示的密钥 ID 是它尝试验证的签名文件中找到的密钥 ID。这并不意味着它们一定是您应该信任的密钥。特别是,如果签名正确但由恶意实体完成,Gradle 不会告诉您。

全局信任密钥

签名验证的优点是,它不必像仅进行校验和验证那样显式列出所有 Artifact,从而可以简化依赖项验证的配置。事实上,同一个密钥常用于签署多个 Artifact。如果是这种情况,您可以将受信任的密钥从 Artifact 级别移动到全局配置块

<?xml version="1.0" encoding="UTF-8"?>
<verification-metadata xmlns="https://schema.gradle.org/dependency-verification"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="https://schema.gradle.org/dependency-verification https://schema.gradle.org/dependency-verification/dependency-verification-1.3.xsd">
   <configuration>
      <verify-metadata>true</verify-metadata>
      <verify-signatures>true</verify-signatures>
      <trusted-keys>
         <trusted-key id="8756c4f765c9ac3cb6b85d62379ce192d401ab61" group="com.github.javaparser"/>
      </trusted-keys>
   </configuration>
   <components/>
</verification-metadata>

上述配置意味着,对于属于 group com.github.javaparser 的任何 Artifact,如果它使用指纹 8756c4f765c9ac3cb6b85d62379ce192d401ab61 签署,我们就会信任它。

trusted-key 元素的工作方式类似于trusted-artifact 元素

  • group,要信任的 Artifact 的 group

  • name,要信任的 Artifact 的 name

  • version,要信任的 Artifact 的 version

  • file,要信任的 Artifact 文件的 name

  • regex,一个布尔值,表示 groupnameversionfile 属性是否需要解释为正则表达式(默认为 false

全局信任密钥时应小心。

尽量将其限制在适当的 group 或 Artifact 范围

  • 一个有效密钥可能已被用于签署您信任的 Artifact A

  • 稍后,该密钥被盗并用于签署 Artifact B

这意味着您对第一个 Artifact 可以信任密钥 A,可能仅限于密钥被盗之前的发布版本,但不能信任 B

请记住,任何人在生成 PGP 密钥时都可以随意命名,因此切勿仅凭密钥名称信任密钥。验证密钥是否在官方网站上列出。例如,Apache 项目通常会提供一个您可以信任的 KEYS.txt 文件。

指定密钥服务器和忽略密钥

Gradle 会自动下载验证签名所需的公钥。为此,它使用一个已知且受信任的密钥服务器列表(该列表可能会在 Gradle 版本之间更改,请参阅实现以确定默认使用的服务器)。

您可以通过将密钥服务器添加到配置中来显式设置要使用的密钥服务器列表

<?xml version="1.0" encoding="UTF-8"?>
<verification-metadata xmlns="https://schema.gradle.org/dependency-verification"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="https://schema.gradle.org/dependency-verification https://schema.gradle.org/dependency-verification/dependency-verification-1.3.xsd">
   <configuration>
      <verify-metadata>true</verify-metadata>
      <verify-signatures>true</verify-signatures>
      <key-servers>
         <key-server uri="hkp://my-key-server.org"/>
         <key-server uri="https://my-other-key-server.org"/>
      </key-servers>
   </configuration>
</verification-metadata>

尽管如此,密钥仍有可能不可用

  • 因为它未发布到公共密钥服务器

  • 因为它丢失了

在这种情况下,您可以在配置块中忽略某个密钥

<?xml version="1.0" encoding="UTF-8"?>
<verification-metadata xmlns="https://schema.gradle.org/dependency-verification"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="https://schema.gradle.org/dependency-verification https://schema.gradle.org/dependency-verification/dependency-verification-1.3.xsd">
   <configuration>
      <verify-metadata>true</verify-metadata>
      <verify-signatures>true</verify-signatures>
      <ignored-keys>
         <ignored-key id="abcdef1234567890" reason="Key is not available in any key server"/>
      </ignored-keys>
   </configuration>
</verification-metadata>

一旦某个密钥被忽略,即使签名文件中提到了它,也不会用于验证。但是,如果签名无法使用至少一个其他密钥进行验证,Gradle 将要求您提供校验和。

如果在引导过程中 Gradle 无法下载密钥,它会将其标记为忽略。如果您可以找到该密钥而 Gradle 找不到,您可以手动将其添加到密钥环文件中。

导出密钥以加快验证

Gradle 会自动下载所需的密钥,但这操作可能相当缓慢,并且要求每个人都下载密钥。为避免这种情况,Gradle 提供了使用包含所需公钥的本地密钥环文件的能力。请注意,密钥环文件中只存储和使用公钥数据包和每个密钥的单个 userId。所有其他信息(用户属性、签名等)会从下载或导出的密钥中剥离。

Gradle 支持 2 种不同的密钥环文件格式:二进制格式(.gpg 文件)和纯文本格式(.keys),也称为 ASCII-armored 格式。

每种格式都有优缺点:二进制格式更紧凑,可以直接通过 GPG 命令更新,但完全不透明(二进制)。相反,ASCII-armored 格式是人类可读的,可以轻松手动更新,并且由于可读的差异而更容易进行代码评审。

您可以通过添加 keyring-format 配置选项来配置使用哪种文件类型

<?xml version="1.0" encoding="UTF-8"?>
<verification-metadata xmlns="https://schema.gradle.org/dependency-verification"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="https://schema.gradle.org/dependency-verification https://schema.gradle.org/dependency-verification/dependency-verification-1.3.xsd">
   <configuration>
      <verify-metadata>true</verify-metadata>
      <verify-signatures>true</verify-signatures>
      <keyring-format>armored</keyring-format>
   </configuration>
</verification-metadata>

密钥环格式的可用选项有 armoredbinary

如果没有指定 keyring-format,如果存在 gradle/verification-keyring.gpggradle/verification-keyring.keys 文件,Gradle 将优先在这些文件中搜索密钥。如果已经存在 .gpg 文件(二进制版本优先),纯文本文件将被忽略。

在引导过程中,您可以要求 Gradle 将其用于此构建验证的所有密钥导出到密钥环

./gradlew --write-verification-metadata pgp,sha256 --export-keys

除非指定了 keyring-format,否则此命令将同时生成二进制版本和 ASCII-armored 文件。使用此选项选择首选格式。您的项目应只选择一种格式。

将此文件提交到 VCS 是个好主意(只要您信任您的 VCS)。如果您使用 git 并使用二进制版本,请确保将其视为二进制文件,方法是将此内容添加到您的 .gitattributes 文件中

*.gpg           binary

您还可以要求 Gradle 导出所有受信任的密钥,而不更新验证元数据文件

./gradlew --export-keys
此命令不会报告验证错误,只导出密钥。

引导和签名验证

签名验证引导采取了乐观的观点,认为签名验证足够。因此,如果您也关心完整性,您必须首先使用校验和验证进行引导,然后再使用签名验证。

与校验和引导类似,Gradle 提供了一种方便的方式来引导启用签名验证的配置文件。为此,只需将 pgp 选项添加到要生成的验证列表中。但是,由于可能存在验证失败、密钥丢失或签名文件丢失的情况,您必须提供一个回退校验和验证算法

./gradlew --write-verification-metadata pgp,sha256

这意味着当出现问题时,Gradle 将验证签名并回退到 SHA-256 校验和。

引导时,Gradle 执行乐观验证,因此假定构建环境是正常的。因此它会

  • 验证通过后立即自动添加受信任的密钥

  • 对于无法从公共密钥服务器下载的密钥,会自动添加为忽略密钥。如果需要手动添加密钥,请参见此处

  • 自动为没有签名或被忽略密钥的 Artifact 生成校验和

如果在生成过程中因某种原因验证失败,Gradle 会自动生成一个忽略密钥条目,但会警告您必须绝对检查发生了什么。

本节所述,这种情况很常见:一个典型的例子是依赖的 POM 文件在不同仓库之间存在差异(通常是非实质性的差异)。

此外,Gradle 会尝试自动分组密钥并生成 trusted-keys 块,从而尽可能减小配置文件大小。

强制仅使用本地密钥环

本地密钥环文件(.gpg.keys)可用于避免在验证构件需要密钥时连接到密钥服务器。但是,本地密钥环可能不包含所需的密钥,在这种情况下,Gradle 将使用密钥服务器获取缺失的密钥。如果本地密钥环文件没有通过导出密钥定期更新,那么您的 CI 构建(例如)可能会过于频繁地连接到密钥服务器(特别是如果您使用一次性容器进行构建)。

为避免这种情况,Gradle 提供了完全禁止使用密钥服务器的功能:只使用本地密钥环文件,如果该文件中缺少密钥,构建将失败。

要启用此模式,您需要在配置文件中禁用密钥服务器

<?xml version="1.0" encoding="UTF-8"?>
<verification-metadata xmlns="https://schema.gradle.org/dependency-verification"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="https://schema.gradle.org/dependency-verification https://schema.gradle.org/dependency-verification/dependency-verification-1.3.xsd">
   <configuration>
      <key-servers enabled="false"/>
      ...
   </configuration>
   ...
</verification-metadata>
如果您要求 Gradle 生成验证元数据文件,并且现有验证元数据文件将 enabled 设置为 false,那么此标志将被忽略,以便下载可能缺失的密钥。

禁用验证或使其宽松

依赖项验证可能会很耗时,或者有时验证可能会影响日常开发(例如,由于频繁升级依赖项)。

或者,您可能希望在 CI 服务器上启用验证,但在本地机器上不启用。

Gradle 实际提供了 3 种不同的验证模式

  • strict,这是默认模式。验证会尽可能早地失败,以避免在构建过程中使用受损的依赖项。

  • lenient,即使存在验证失败,构建也会继续运行。验证错误将在构建期间显示,但不会导致构建失败。

  • off,此时验证被完全忽略。

所有这些模式都可以在 CLI 上使用 --dependency-verification 标志激活,例如

./gradlew --dependency-verification lenient build

或者,您可以在 CLI 上设置 org.gradle.dependency.verification 系统属性

./gradlew -Dorg.gradle.dependency.verification=lenient build

或者在 gradle.properties 文件中设置

org.gradle.dependency.verification=lenient

仅对某些配置禁用依赖项验证

为了提供尽可能强的安全级别,依赖项验证全局启用。例如,这将确保您信任所有使用的插件。然而,插件本身可能需要解析额外的依赖项,让用户接受这些依赖项没有意义。为此,Gradle 提供了一个 API,允许在某些特定配置上禁用依赖项验证

如果您关心安全性,禁用依赖项验证不是一个好主意。此 API 主要用于检查依赖项没有意义的情况。然而,为了安全起见,Gradle 将在特定配置禁用验证时系统地打印警告。

例如,插件可能想要检查某个库是否有更新的版本可用并列出这些版本。在这种情况下,要求用户提供新版本的 POM 文件的校验和是没有意义的,因为根据定义,他们不知道这些校验和。因此,插件可能需要在独立于依赖项验证配置的情况下运行其代码。

要做到这一点,您需要调用 ResolutionStrategy#disableDependencyVerification 方法

build.gradle.kts
configurations {
    "myPluginClasspath" {
        resolutionStrategy {
            disableDependencyVerification()
        }
    }
}
build.gradle
configurations {
    myPluginClasspath {
        resolutionStrategy {
            disableDependencyVerification()
        }
    }
}

也可以在分离配置上禁用验证,如下例所示

build.gradle.kts
tasks.register("checkDetachedDependencies") {
    val detachedConf: FileCollection = configurations.detachedConfiguration(dependencies.create("org.apache.commons:commons-lang3:3.3.1")).apply {
        resolutionStrategy.disableDependencyVerification()
    }
    doLast {
        println(detachedConf.files)
    }
}
build.gradle
tasks.register("checkDetachedDependencies") {
    def detachedConf = configurations.detachedConfiguration(dependencies.create("org.apache.commons:commons-lang3:3.3.1"))
    detachedConf.resolutionStrategy.disableDependencyVerification()
    doLast {
        println(detachedConf.files)
    }
}

信任某些特定构件

您可能想比其他构件更信任某些构件。例如,认为贵公司生产且仅存在于内部仓库中的构件是安全的,而您想检查所有外部组件,这是合理的。

这是一个典型的公司策略。实际上,没有任何东西能阻止您的内部仓库被攻破,所以检查您的内部构件也是个好主意!

为此,Gradle 提供了一种自动信任某些构件的方法。您可以通过将此添加到您的配置来信任某个组中的所有构件

<?xml version="1.0" encoding="UTF-8"?>
<verification-metadata xmlns="https://schema.gradle.org/dependency-verification"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="https://schema.gradle.org/dependency-verification https://schema.gradle.org/dependency-verification/dependency-verification-1.3.xsd">
   <configuration>
      <trusted-artifacts>
         <trust group="com.mycompany" reason="We trust mycompany artifacts"/>
      </trusted-artifacts>
   </configuration>
</verification-metadata>

这意味着所有组为 com.mycompany 的组件都将自动被信任。信任意味着 Gradle 将不会执行任何验证。

trust 元素接受这些属性

  • group,要信任的 Artifact 的 group

  • name,要信任的 Artifact 的 name

  • version,要信任的 Artifact 的 version

  • file,要信任的 Artifact 文件的 name

  • regex,一个布尔值,表示 groupnameversionfile 属性是否需要解释为正则表达式(默认为 false

  • reason,一个可选原因,说明为何匹配的构件被信任

在上面的例子中,这意味着被信任的构件将是 com.mycompany 中的构件,但不包括 com.mycompany.other。要信任 com.mycompany 及其所有子组中的所有构件,您可以使用

<?xml version="1.0" encoding="UTF-8"?>
<verification-metadata xmlns="https://schema.gradle.org/dependency-verification"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="https://schema.gradle.org/dependency-verification https://schema.gradle.org/dependency-verification/dependency-verification-1.3.xsd">
   <configuration>
      <trusted-artifacts>
         <trust group="^com[.]mycompany($|([.].*))" regex="true" reason="We trust all mycompany artifacts"/>
      </trusted-artifacts>
   </configuration>
</verification-metadata>

信任构件的多个校验和

同一个构件在不同地方拥有不同的校验和是相当常见的。这怎么可能?尽管有所进步,开发者通常会将构件分别发布到 Maven Central 和其他仓库,使用不同的构建。一般来说,这不是问题,但有时这意味着元数据文件会不同(不同的时间戳,额外的空白等)。再加上您的构建可能使用多个仓库或仓库镜像,使得单个构建很可能“看到”同一组件的不同元数据文件!一般来说,这不是恶意的(但您必须验证构件实际上是正确的),所以 Gradle 允许您声明额外的构件校验和。例如

      <component group="org.apache" name="apache" version="13">
         <artifact name="apache-13.pom">
            <sha256 value="2fafa38abefe1b40283016f506ba9e844bfcf18713497284264166a5dbf4b95e">
               <also-trust value="ff513db0361fd41237bef4784968bc15aae478d4ec0a9496f811072ccaf3841d"/>
            </sha256>
         </artifact>
      </component>

您可以根据需要添加任意数量的 also-trust 条目,但通常不应超过 2 个。

跳过 Javadoc 和源代码

默认情况下,Gradle 会验证所有下载的构件,这包括 Javadoc 和源代码。通常这不是问题,但您可能会遇到 IDE 在导入时自动尝试下载它们的问题:如果您没有为它们设置校验和,导入将会失败。

为避免这种情况,您可以配置 Gradle 自动信任所有 javadoc/源代码

<trusted-artifacts>
   <trust file=".*-javadoc[.]jar" regex="true"/>
   <trust file=".*-sources[.]jar" regex="true"/>
</trusted-artifacts>

手动将密钥添加到密钥环

将密钥添加到 ASCII 封装的密钥环

添加的密钥必须是 ASCII 封装格式,并且可以直接添加到文件末尾。如果您已经下载了正确格式的密钥,可以直接将其附加到文件。

或者,您可以执行以下命令来修改现有的 KEYS 文件

$ gpg --no-default-keyring --keyring /tmp/keyring.gpg --recv-keys 8756c4f765c9ac3cb6b85d62379ce192d401ab61

gpg: keybox '/tmp/keyring.gpg' created
gpg: key 379CE192D401AB61: public key "Bintray (by JFrog) <****>" imported
gpg: Total number processed: 1
gpg:               imported: 1

# Write its ASCII-armored version
$ gpg --keyring /tmp/keyring.gpg --export --armor 8756c4f765c9ac3cb6b85d62379ce192d401ab61 > gradle/verification-keyring.keys

完成后,请确保再次运行生成命令,以便 Gradle 处理密钥。这将执行以下操作

  • 为密钥添加标准头部

  • 使用 Gradle 自己的格式重写密钥,该格式将密钥裁剪到最低限度

  • 将密钥移动到其排序位置,保持文件的可重现性

将密钥添加到二进制密钥环

您可以使用 GPG 将密钥添加到二进制版本,例如执行以下命令(语法可能取决于您使用的工具)

$ gpg --no-default-keyring --keyring gradle/verification-keyring.gpg --recv-keys 8756c4f765c9ac3cb6b85d62379ce192d401ab61

gpg: keybox 'gradle/verification-keyring.gpg' created
gpg: key 379CE192D401AB61: public key "Bintray (by JFrog) <****>" imported
gpg: Total number processed: 1
gpg:               imported: 1

$ gpg --no-default-keyring --keyring gradle/verification-keyring.gpg --recv-keys 6f538074ccebf35f28af9b066a0975f8b1127b83

gpg: key 0729A0AFF8999A87: public key "Kotlin Release <****>" imported
gpg: Total number processed: 1
gpg:               imported: 1

处理验证失败

依赖项验证可能以不同的方式失败,本节解释了如何处理各种情况。

缺少验证元数据

您可能遇到的最简单的失败是依赖项验证文件中缺少验证元数据。例如,如果您使用校验和验证,然后更新依赖项并引入依赖项的新版本(以及可能其传递依赖项),就会发生这种情况。

Gradle 会告诉您缺少哪些元数据

Execution failed for task ':compileJava'.
> Dependency verification failed for configuration ':compileClasspath':
    - On artifact commons-logging-1.2.jar (commons-logging:commons-logging:1.2) in repository 'MavenRepo': checksum is missing from verification metadata.
  • 缺失的模块组是 commons-logging,它的构件名称是 commons-logging,版本是 1.2。对应的构件是 commons-logging-1.2.jar,所以您需要在验证文件中添加以下条目

<component group="commons-logging" name="commons-logging" version="1.2">
   <artifact name="commons-logging-1.2.jar">
      <sha256 value="daddea1ea0be0f56978ab3006b8ac92834afeefbd9b7e4e6316fca57df0fa636" origin="official distribution"/>
   </artifact>
</component>

或者,您可以使用引导机制要求 Gradle 生成缺失的信息:元数据文件中的现有信息将得到保留,Gradle 只会添加缺失的验证元数据。

校验和不正确

一个更棘手的问题是实际的校验和验证失败

Execution failed for task ':compileJava'.
> Dependency verification failed for configuration ':compileClasspath':
    - On artifact commons-logging-1.2.jar (commons-logging:commons-logging:1.2) in repository 'MavenRepo': expected a 'sha256' checksum of '91f7a33096ea69bac2cbaf6d01feb934cac002c48d8c8cfa9c240b40f1ec21df' but was 'daddea1ea0be0f56978ab3006b8ac92834afeefbd9b7e4e6316fca57df0fa636'

这次,Gradle 会告诉您哪个依赖项有问题,预期的校验和是什么(您在验证元数据文件中声明的那个),以及验证期间实际计算出的校验和。

此类失败表明依赖项可能已被篡改。在此阶段,您必须手动进行验证并检查发生了什么。可能发生以下几种情况

  • 依赖项在 Gradle 的本地依赖项缓存中被篡改。这通常无害:从缓存中删除文件,Gradle 将重新下载依赖项。

  • 依赖项在多个源中可用,但二进制文件略有不同(额外的空白等)

    • 请通知库的维护者他们存在此类问题

    • 您可以使用also-trust接受额外的校验和

  • 依赖项被篡改

    • 立即通知库的维护者

    • 通知被篡改库的仓库维护者

请注意,被篡改库的一种变体通常是域名抢注(name squatting),黑客会使用看起来合法但实际只差一个字符的 GAV 坐标,或者仓库影子攻击(repository shadowing),将具有官方 GAV 坐标的依赖项发布到构建中优先级更高的恶意仓库中。

不受信任的签名

如果您启用了签名验证,Gradle 将执行签名验证,但不会自动信任它们

> Dependency verification failed for configuration ':compileClasspath':
    - On artifact javaparser-core-3.6.11.jar (com.github.javaparser:javaparser-core:3.6.11) in repository 'MavenRepo': Artifact was signed with key '379ce192d401ab61' (Bintray (by JFrog) <****>) and passed verification but the key isn't in your trusted keys list.

在这种情况下,这意味着您需要自行检查用于验证(因此也是签名)的密钥是否可以信任,然后参考文档的这一节来确定如何声明可信任的密钥。

签名验证失败

如果 Gradle 未能验证签名,您需要采取行动并手动验证构件,因为这可能表明依赖项已被篡改

如果发生这种情况,Gradle 将失败并显示

> Dependency verification failed for configuration ':compileClasspath':
    - On artifact javaparser-core-3.6.11.jar (com.github.javaparser:javaparser-core:3.6.11) in repository 'MavenRepo': Artifact was signed with key '379ce192d401ab61' (Bintray (by JFrog) <****>) but signature didn't match

有几种选择

  1. 签名本身就是错误的,这在使用不同仓库发布的依赖项时经常发生。

  2. 签名正确,但构件已被篡改(无论是在本地依赖项缓存中还是远程)

正确的做法是访问依赖项的官方网站,查看他们是否发布了构件的签名。如果发布了,验证 Gradle 下载的签名是否与发布的签名匹配。

如果您已经检查了依赖项被篡改并且“仅仅”是签名错误,您应该声明一个构件级别的密钥排除

   <components>
       <component group="com.github.javaparser" name="javaparser-core" version="3.6.11">
          <artifact name="javaparser-core-3.6.11.pom">
             <ignored-keys>
                <ignored-key id="379ce192d401ab61" reason="internal repo has corrupted POM"/>
             </ignored-keys>
          </artifact>
       </component>
   </components>

但是,如果您只这样做,Gradle 仍然会失败,因为此构件的所有密钥都将被忽略,并且您没有提供校验和

   <components>
       <component group="com.github.javaparser" name="javaparser-core" version="3.6.11">
          <artifact name="javaparser-core-3.6.11.pom">
             <ignored-keys>
                <ignored-key id="379ce192d401ab61" reason="internal repo has corrupted POM"/>
             </ignored-keys>
             <sha256 value="a2023504cfd611332177f96358b6f6db26e43d96e8ef4cff59b0f5a2bee3c1e1"/>
          </artifact>
       </component>
   </components>

手动验证依赖项

您很可能会遇到依赖项验证失败(无论是校验和验证还是签名验证),并且需要确定依赖项是否已被篡改。

在本节中,我们提供一个示例,说明如何手动检查依赖项是否被篡改。

为此,我们将以这个失败示例为例

> Dependency verification failed for configuration ':compileClasspath':
- On artifact j2objc-annotations-1.1.jar (com.google.j2objc:j2objc-annotations:1.1) in repository 'MyCompany Mirror': Artifact was signed with key '29579f18fa8fd93b' but signature didn't match

此错误消息提供了问题依赖项的 GAV 坐标,以及依赖项的获取来源。这里,依赖项来自 MyCompany Mirror,这是我们构建中声明的仓库。

因此,首先要做的是手动从镜像下载构件及其签名

$ curl https://my-company-mirror.com/repo/com/google/j2objc/j2objc-annotations/1.1/j2objc-annotations-1.1.jar --output j2objc-annotations-1.1.jar
$ curl https://my-company-mirror.com/repo/com/google/j2objc/j2objc-annotations/1.1/j2objc-annotations-1.1.jar.asc --output j2objc-annotations-1.1.jar.asc

然后,我们可以使用错误消息中提供的密钥信息在本地导入密钥

$ gpg --recv-keys B801E2F8EF035068EC1139CC29579F18FA8FD93B

并执行验证

$ gpg --verify j2objc-annotations-1.1.jar.asc
gpg: assuming signed data in 'j2objc-annotations-1.1.jar'
gpg: Signature made Thu 19 Jan 2017 12:06:51 AM CET
gpg:                using RSA key 29579F18FA8FD93B
gpg: BAD signature from "Tom Ball <****>" [unknown]

这告诉我们问题在本地机器上:仓库已经包含一个错误的签名

下一步是下载 Maven Central 上的实际内容,并执行相同的操作

$ curl https://my-company-mirror.com/repo/com/google/j2objc/j2objc-annotations/1.1/j2objc-annotations-1.1.jar  --output central-j2objc-annotations-1.1.jar
$ curl https://my-company-mirror.com/repo/com/google/j2objc/j2objc-annotations/1/1/j2objc-annotations-1.1.jar.asc  --output central-j2objc-annotations-1.1.jar.asc

现在我们可以再次检查签名

$ gpg --verify central-j2objc-annotations-1.1.jar.asc

gpg: assuming signed data in 'central-j2objc-annotations-1.1.jar'
gpg: Signature made Thu 19 Jan 2017 12:06:51 AM CET
gpg:                using RSA key 29579F18FA8FD93B
gpg: Good signature from "Tom Ball <****>" [unknown]
gpg: WARNING: This key is not certified with a trusted signature!
gpg:          There is no indication that the signature belongs to the owner.
Primary key fingerprint: B801 E2F8 EF03 5068 EC11  39CC 2957 9F18 FA8F D93B

这表明依赖项在 Maven Central 上是有效的。在此阶段,我们已经知道问题出在镜像中,它可能已被篡改,但我们需要验证。

一个好主意是比较这两个构件,您可以使用像 diffoscope 这样的工具来完成。

然后我们发现,意图并非恶意,而是由于某种原因,一个构建被新版本覆盖了(Central 中的版本比我们仓库中的版本新)。

在这种情况下,您可以决定

  • 忽略此构件的签名,并信任不同的可能校验和(包括旧构件和新版本的校验和)

  • 或清理您的镜像,使其包含与 Maven Central 中相同的版本

值得注意的是,如果您选择从仓库中删除该版本,您将需要将其从本地 Gradle 缓存中删除。

错误消息会告诉您文件所在的位置,这使得操作变得方便

> Dependency verification failed for configuration ':compileClasspath':
    - On artifact j2objc-annotations-1.1.jar (com.google.j2objc:j2objc-annotations:1.1) in repository 'MyCompany Mirror': Artifact was signed with key '29579f18fa8fd93b' but signature didn't match

  This can indicate that a dependency has been compromised. Please carefully verify the signatures and checksums.

  For your information here are the path to the files which failed verification:
    - $<<directory_layout.adoc#dir:gradle_user_home,GRADLE_USER_HOME>>/caches/modules-2/files-2.1/com.google.j2objc/j2objc-annotations/1.1/976d8d30bebc251db406f2bdb3eb01962b5685b3/j2objc-annotations-1.1.jar (signature: GRADLE_USER_HOME/caches/modules-2/files-2.1/com.google.j2objc/j2objc-annotations/1.1/82e922e14f57d522de465fd144ec26eb7da44501/j2objc-annotations-1.1.jar.asc)

  GRADLE_USER_HOME = /home/jiraya/.gradle

您可以安全地删除构件文件,因为 Gradle 会自动重新下载它

rm -rf ~/.gradle/caches/modules-2/files-2.1/com.google.j2objc/j2objc-annotations/1.1

清理验证文件

如果您什么都不做,依赖项验证元数据会随着您添加新依赖项或更改版本而随时间增长:Gradle 不会自动从此文件中删除未使用的条目。原因是 Gradle 无法事先知道依赖项是否会在构建过程中被实际使用。

因此,添加依赖项或更改依赖项版本很容易导致文件中出现更多条目,同时留下不必要的条目。

清理文件的一种方法是将现有的 verification-metadata.xml 文件移动到另一个位置,然后使用--dry-run 模式调用 Gradle:虽然不完美(它不会注意到仅在配置时解析的依赖项),但它会生成一个新文件,您可以将其与现有文件进行比较。

我们需要移动现有文件,因为引导模式和 dry-run 模式都是增量的:它们从现有元数据验证文件中复制信息(特别是可信任的密钥)。

刷新缺失的密钥

Gradle 会缓存缺失的密钥 24 小时,这意味着在失败后 24 小时内不会尝试重新下载缺失的密钥。

如果您想立即重试,可以运行带有 --refresh-keys CLI 标志的命令

./gradlew build --refresh-keys

如果 Gradle 持续无法下载密钥,请参阅此处了解如何手动添加密钥。