使用第三方存储库上发布的外部依赖项和插件会使您的构建面临风险。特别是,您需要了解传递引入的二进制文件以及它们是否合法。为了降低安全风险并避免在项目中集成受损的依赖项,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开头“由 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 生成校验和并通过仔细检查生成的文件进行验证通常更容易。

在这个例子中,我们不仅可以检查校验和是否正确,而且还可以在官方网站上找到它,这就是为什么我们将元素origin上的 of 属性的值sha512从更改Generated by GradlePDFBox 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.

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

请参阅对依赖项验证进行故障排除,以获取有关在这种情况下该怎么做的更多见解。

验证哪些校验和?

如果依赖项验证元数据文件为依赖项声明了多个校验和,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指纹签名的,我们就信任它。

该元素的工作方式与trust-artifacttrusted-key元素类似:

  • group,可信任的工件组

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

  • version,要信任的工件版本

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

  • regex,一个布尔值,表示、 和group属性name是否需要解释为正则表达式(默认为)versionfilefalse

在全局信任密钥时应该小心。

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

  • 有效的密钥可能已用于签署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 种不同的密钥环文件格式:二进制格式 ( .gpgfile) 和纯文本格式 ( .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生成验证元数据文件并且现有验证元数据文件设置enabledfalse,则该标志将被忽略,以便下载可能丢失的密钥。

禁用验证或放宽验证

依赖项验证可能会很昂贵,或者有时验证可能会妨碍日常开发(例如,由于频繁的依赖项升级)。

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

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

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

  • lenient,即使验证失败也会运行构建。验证错误将在构建过程中显示,而不会导致构建失败。

  • off当验证被完全忽略时。

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

./gradlew --dependency-verification lenient build

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

./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,一个布尔值,表示、 和group属性name是否需要解释为正则表达式(默认为)versionfilefalse

  • 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 配置为自动信任所有 javadocs/源:

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

手动将密钥添加到密钥环

将密钥添加到 ASCII 铠装密钥环

添加的密钥必须是 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 上有效。在这个阶段,我们已经知道问题存在于镜像中,它可能已被泄露,但我们需要验证。

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

然后我们发现其意图并不是恶意的,而是不知何故构建已被较新的版本覆盖(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-keysCLI 标志运行:

./gradlew build --refresh-keys

如果 Gradle 始终无法下载密钥,请参阅此处如何手动添加密钥。