使用外部依赖项和第三方仓库上发布的插件会使您的构建面临风险。 特别是,您需要了解传递性引入了哪些二进制文件,以及它们是否合法。 为了降低安全风险并避免在您的项目中集成受损的依赖项,Gradle 支持依赖项验证

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

那么,为什么要费心呢?

依赖项验证关乎对您获取和交付内容的信任

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

在安全性和便利性之间找到合适的平衡点很困难,但 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 将使用校验和验证所有工件,但不会验证签名。 Gradle 将验证使用其依赖管理引擎下载的任何工件,包括但不限于

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

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

  • 插件(项目插件和 settings 插件)

  • 使用高级依赖解析 API 解析的工件

Gradle 将不会验证变化的依赖项(特别是 SNAPSHOT 依赖项)或本地生成的工件(通常是在构建本身期间生成的 jar),因为它们的校验和和签名自然会始终变化。

使用如此小的配置文件,使用任何外部依赖项或插件的项目都会立即开始失败,因为它不包含任何要验证的校验和。

依赖项验证的范围

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

如果使用了包含的构建

  • 当前构建的配置文件用于验证

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

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

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

配置控制台输出

默认情况下,如果依赖项验证失败,Gradle 将生成关于验证失败的小摘要,以及包含关于失败完整信息的 HTML 报告。 如果您的环境阻止您读取此 HTML 报告文件(例如,如果您在 CI 上运行构建并且不容易获取远程工件),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 的配置

    • 包含的构建配置

    • 插件使用的配置

  • 下载在解析期间发现的所有工件

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

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

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

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

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

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

Gradle 不会验证使用自己的 HTTP 客户端的插件的校验和或签名。 只有使用 Gradle 提供的基础设施执行请求的插件才会看到它们的请求被验证。

使用生成进行增量更新

Gradle 生成的验证文件对其所有内容都有严格的排序。 它还使用来自现有状态的信息,以将更改限制在严格的最小值内。

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

  • Gradle 生成的校验和条目将具有清晰的 origin,以“Generated by Gradle”开头,这很好地表明条目需要审查,

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

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

凭借上述优势,通过简单地再次生成文件并查看更改,就可以轻松地考虑新的依赖项或依赖项版本。

使用 dry 模式

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

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

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

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

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

禁用元数据验证

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

验证依赖项校验和

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

Gradle 支持 MD5、SHA1、SHA-256 和 SHA-512 校验和。 但是,如今只有 SHA-256 和 SHA-512 校验和被认为是安全的。

为工件添加校验和

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

  • group org.apache.pdfbox

  • name pdfbox

  • version 2.0.17

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

  • pdfbox-2.0.17.jar,这是主工件

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

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

<?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>

从哪里获取校验和?

通常,校验和与工件一起发布在公共仓库上。 但是,如果依赖项在仓库中被破坏,则其校验和也可能被破坏,因此最好从其他地方获取校验和,通常是库本身的网站。

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

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

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

有趣的是,使用 pdfbox 将需要远不止这 2 个工件,因为它还会引入传递依赖项。 如果依赖项验证文件仅包含您使用的主要工件的校验和,则构建将失败,并显示如下错误

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 校验和很难但有可能,但为同一工件同时伪造两者则更难。

  2. 您可能想要将生成的校验和添加到上面的列表中

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

验证依赖项签名

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

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

签名可以像校验和一样用于评估依赖项的完整性。 签名是对工件哈希的签名,而不是工件本身。 这意味着,如果签名是在不安全的哈希(即使是 SHA1)上完成的,那么您就没有正确评估文件的完整性。 因此,如果您既关心完整性又关心来源,则需要将签名校验和都添加到您的验证元数据中。

然而

  • Gradle 仅支持验证以 ASCII 装甲 PGP 文件形式发布在远程仓库上的签名

  • 并非所有工件都发布了签名

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

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

关于过期的密钥

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

启用签名验证

由于验证签名更昂贵(无论是 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>

理解签名验证

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

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

  • 如果它存在

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

    • 使用下载的公钥验证工件

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

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

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

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

  • 如果工件通过验证,则为该工件配置的任何其他校验和也将被检查

但是,工件通过签名验证并不意味着您可以信任它:您需要信任密钥

在实践中,这意味着您需要列出您信任的每个工件的密钥,这可以通过添加 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 也不会告诉您。

全局信任密钥

签名验证的优势在于,它可以使依赖项验证的配置更容易,而无需像仅校验和验证那样显式列出所有工件。 实际上,同一个密钥通常可以用于签署多个工件。 如果是这种情况,您可以将受信任的密钥从工件级别移动到全局配置块

<?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>

上面的配置意味着,对于属于 com.github.javaparser 组的任何工件,如果它使用 8756c4f765c9ac3cb6b85d62379ce192d401ab61 指纹签名,我们就信任它。

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

  • group,要信任的工件的组

  • name,要信任的工件的名称

  • version,要信任的工件的版本

  • file,要信任的工件文件的名称

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

全局信任密钥时应谨慎。

尝试将其限制为适当的组或工件

  • 有效的密钥可能已用于签署您信任的工件 A

  • 稍后,密钥被盗并用于签署工件 B

这意味着您可以信任密钥 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 装甲格式。

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

您可以通过添加 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 装甲文件。 使用此选项选择首选格式。 您应该只为您的项目选择一个。

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

*.gpg           binary

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

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

引导和签名验证

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

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

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

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

引导时,Gradle 执行乐观验证,因此假设构建环境是健全的。因此,它将

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

  • 对于无法从公钥服务器下载的密钥,自动添加忽略的密钥。请参阅此处了解如何在需要时手动添加密钥

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

如果由于某些原因,在生成过程中验证失败,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,要信任的工件的组

  • name,要信任的工件的名称

  • version,要信任的工件的版本

  • file,要信任的工件文件的名称

  • 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 个。

跳过 Javadocs 和 sources

默认情况下,Gradle 将验证所有下载的工件,包括 Javadocs 和 sources。一般来说,这不是问题,但您可能会遇到 IDE 在导入期间自动尝试下载它们的问题:如果您没有为它们设置校验和,导入将失败。

为了避免这种情况,您可以配置 Gradle 以自动信任所有 javadocs/sources

<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来接受额外的校验和

  • 依赖项被攻破

    • 立即告知库的维护人员

    • 通知仓库维护人员被攻破的库

请注意,被攻破库的一种变体通常是抢注名称,即黑客会使用 看起来合法 但实际上只差一个字符的 GAV 坐标,或者仓库影子,即具有官方 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 持续无法下载密钥,请参阅此处了解如何手动添加密钥。