使用外部依赖项和第三方存储库上发布的插件会给您的构建带来风险。特别是,您需要了解哪些二进制文件是通过传递方式引入的以及它们是否合法。为了降低安全风险并避免在您的项目中集成受损的依赖项,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 模块元数据)

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

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

Gradle 不会 验证更改的依赖项(特别是 SNAPSHOT 依赖项)或本地生成的工件(通常是在构建本身期间生成的 jar),因为它们的校验和和签名本质上会不断更改。

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

依赖项验证的范围

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

如果使用了包含的构建

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

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

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

因此,开始使用的简单方法是为现有构建生成最小配置。

配置控制台输出

默认情况下,如果依赖项验证失败,Gradle 将生成一个关于验证失败的小型摘要以及包含失败全部信息的 HTML 报告。如果你的环境阻止你阅读此 HTML 报告文件(例如,如果你在 CI 上运行构建,并且无法轻松获取远程制品),Gradle 提供了一种选择详细控制台报告的方法。为此,你需要将此 Gradle 属性添加到你的 gradle.properties 文件中

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,该origin 以“由 Gradle 生成”开头,这是一个需要查看条目的良好指示器,

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

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

有了上述好处,只需再次生成文件并查看更改,即可轻松地考虑新依赖项或依赖项版本。

使用干运行模式

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

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

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

然后,将生成一个新文件,而不是生成 verification-metadata.xml 文件,名为 verification-metadata.dryrun.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 坐标是

  • org.apache.pdfbox

  • 名称 pdfbox

  • 版本 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 属性的值的原因,从 由 Gradle 生成更改为 PDFBox 官方网站。更改 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 只能导致更大的排除范围,因此,使用它是安全的。

这实际上意味着如果你使用密钥 8756c4f765c9ac3cb6b85d62379ce192d401ab61com.github.javaparser:javaparser-core:3.6.11进行签名,则你信任它。

如果没有它,则构建将失败并出现此错误

> 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 当验证被完全忽略时。

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

./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 个。

跳过 Javadoc 和源

默认情况下,Gradle 将验证所有下载的工件,其中包括 Javadoc 和源。通常这不是问题,但您可能会遇到 IDE 的问题,这些 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来接受额外的校验和

  • 依赖项已被破坏

    • 立即通知该库的维护者

    • 通知被破坏库的存储库维护者

请注意,被破坏库的一种变体通常是名称抢注,黑客会使用看起来合法但实际上只有一个字符不同的 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 上是有效的。在此阶段,我们已经知道问题出在镜像中,可能已被入侵,但我们需要验证。

一个好主意是比较 2 个工件,你可以使用类似 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:虽然不完美(它不会注意到仅在配置时才解决的依赖项),但它会生成一个新文件,你可以将其与现有文件进行比较。

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

刷新丢失的密钥

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

如果您想立即重试,可以使用 --refresh-keys CLI 标志运行

./gradlew build --refresh-keys

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