使用外部依赖项和在第三方仓库上发布的插件会使您的构建面临风险。特别是,您需要注意哪些二进制文件被传递性地引入以及它们是否合法。为了降低安全风险并避免在您的项目中集成受损的依赖项,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),因为它们的校验和和签名自然会不断变化。

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

依赖项验证的范围

依赖项验证配置是全局的:单个文件用于配置整个构建的验证。特别是,同一个文件用于(子)项目和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,以“Generated by 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属性值从Generated by Gradle更改为PDFBox Official site。更改origin可以给用户一种您的构建的可信度。

有趣的是,使用pdfbox将需要比这两个工件多得多的东西,因为它还会引入传递性依赖项。如果依赖项验证文件只包含您使用的主工件的校验和,则构建将失败并出现如下错误

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-armored 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而不是长ID29579f18fa8fd93b)。这最大限度地减少了碰撞攻击的可能性。

当时,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提供了使用包含所需公钥的本地密钥环文件的能力。请注意,只存储和使用公钥包和每个密钥的单个用户ID。所有其他信息(用户属性、签名等)都会从下载或导出的密钥中删除。

Gradle支持两种不同的密钥环文件格式:二进制格式(.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执行乐观验证,因此假定一个健全的构建环境。因此,它将

  • 一旦验证通过,自动添加受信任的密钥

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

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

如果由于某种原因,在生成过程中验证失败,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个。

跳过Javadocs和源代码

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

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

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

手动将密钥添加到密钥环

将密钥添加到ASCII-armored密钥环

添加的密钥必须是ASCII-armored格式,并且可以简单地添加到文件末尾。如果您已经以正确的格式下载了密钥,则可以简单地将其附加到文件中。

或者,您可以通过发出以下命令来修改现有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:虽然不完美(它不会注意到只在配置时解析的依赖项),但它会生成一个新文件,您可以将其与现有文件进行比较。

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

刷新缺失的密钥

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

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

./gradlew build --refresh-keys

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