介绍
JAR文件是一种基于流行的ZIP文件格式的文件格式,用于将许多文件聚合到一个文件中。JAR文件本质上是一个包含可选META-INF目录的zip文件。可以通过命令行jar工具或使用Java平台中的java.util.jar
API来创建JAR文件。对于JAR文件的名称没有限制,可以是特定平台上的任何合法文件名。
模块化JAR文件
模块化JAR文件是具有模块描述符module-info.class
的JAR文件,该描述符位于顶级目录(或根目录)中。模块描述符是模块声明的二进制形式。(请注意关于多版本JAR文件的部分进一步细化了模块化JAR文件的定义。)
在模块路径上部署的模块化JAR文件(而不是类路径)是一个显式模块。依赖关系和服务提供者在模块描述符中声明。如果模块化JAR文件部署在类路径上,则其行为类似于非模块化JAR文件。
在模块路径上部署的非模块化JAR文件是一个自动模块。如果JAR文件具有主属性Automatic-Module-Name
(请参阅主属性),则属性的值是模块名称,否则模块名称是从JAR文件的名称派生的,如在ModuleFinder.of(Path...)
中指定。
多版本JAR文件
多版本JAR文件允许单个JAR文件支持多个Java平台主要版本。例如,多版本JAR文件可以依赖于Java 8和Java 9主要平台版本,其中一些类文件依赖于Java 8中的API,而其他类文件依赖于Java 9中的API。这使得库和框架开发人员可以将特定主要版本的Java平台发布中的API的使用与所有用户迁移到该主要版本的要求分离开来。库和框架开发人员可以逐步迁移到并支持新的Java功能,同时仍支持旧功能。
多版本JAR文件由主属性标识:
Multi-Release: true
在JAR清单的主要部分中声明。
依赖于Java平台发布的主要版本9或更高版本的类和资源文件可以位于版本化目录而不是顶级(或根)目录下。版本化目录位于META-INF目录下,格式如下:
META-INF/versions/N
其中N是Java平台发布的主要版本号的字符串表示。具体来说,N
必须符合以下规范:
N: | {1-9} {0-9} * |
---|
任何值为小于9
的N
的版本化目录将被忽略,以及不符合上述规范的N
的字符串表示。
在多版本JAR的版本化目录下的类文件,假设版本为N
,必须具有小于或等于与Java平台发布的第N
个主要版本相关联的类文件版本的类文件版本。如果类文件的类是公共的或受保护的,则该类必须主持具有相同完全限定名称和访问修饰符的类的类文件,该类文件存在于顶级目录下。逻辑上,这也适用于版本小于N
的版本化目录下的类文件的类。
如果多版本JAR文件部署在Java平台发布运行时的主要版本N
的类路径或模块路径上(作为自动模块或显式的多版本模块),则加载来自该JAR文件的类的类加载器将首先搜索第N
个版本化目录下的类文件,然后按降序搜索之前的版本化目录(如果存在),直到较低的主要版本边界为9
,最后搜索顶级目录下的类文件。
多版本JAR文件中的类导出的公共API必须在各个版本中完全相同,因此至少需要为版本化目录下的类文件的版本化公共或受保护类主持顶级目录下的类文件的类。进行广泛的API验证检查难度大且成本高昂,因为诸如jar
工具之类的工具不需要执行广泛的验证,Java运行时也不需要执行任何验证。本规范的未来版本可能会放宽完全相同API约束以支持谨慎的演进。
META-INF
目录下的资源不能进行版本化(例如服务配置)。
多版本JAR文件可以进行签名。
Java运行时的引导类加载器不支持多版本JAR文件。如果将多版本JAR文件附加到引导类路径(使用-Xbootclasspath/a
选项),则该JAR文件将被视为普通JAR文件。
模块化多版本JAR文件
模块化多版本JAR文件是具有模块描述符module-info.class
的多版本JAR文件,该描述符位于顶级目录(与模块化 JAR文件相同)或直接位于版本化目录中。
非导出包中的公共或受保护类(未在模块描述符中声明为导出的)不需要主持顶级目录下存在相同完全限定名称和访问修饰符的类。
模块描述符通常与任何其他类或资源文件没有区别对待。模块描述符可以存在于版本化区域但不存在于顶级目录下。例如,这确保了只有Java 8版本化类可以存在于顶级目录下,而Java 9版本化类(包括或仅包括模块描述符)可以存在于9
版本化目录下。
任何主持低版本化模块描述符的版本化模块描述符或者在顶级目录下的M
必须与M
相同,有两个例外:
- 主持版本化描述符可以具有不同的非
transitive
requires
子句,其中包括java.*
和jdk.*
模块;和 - 主持版本化描述符可以具有不同的
uses
子句,即使是在java.*
和jdk.*
模块之外定义的服务类型。
工具,如jar
工具,应该对版本化模块描述符执行此类验证,但Java运行时不需要执行任何验证。
META-INF目录
META-INF目录中的以下文件/目录由Java平台识别和解释,用于配置应用程序、类加载器和服务:
MANIFEST.MF
用于定义与包相关的数据的清单文件。
x.SF
JAR文件的签名文件。'x'代表基本文件名。
x.DSA
、x.RSA
或x.EC
与具有相同基本文件名的签名文件相关联的签名块文件。此文件以PKCS #7结构存储相应签名文件的数字签名。
services/
此目录存储所有JAR文件的服务提供者配置文件,这些文件部署在类路径上的JAR文件或作为模块路径上的自动模块部署的JAR文件。有关更多详细信息,请参阅服务提供者开发的规范。
versions/
此目录包含多版本JAR文件的版本化类和资源文件。
名称-值对和部分
在我们深入研究各个配置文件的内容之前,需要定义一些格式约定。在大多数情况下,清单文件和签名文件中包含的信息以所谓的“名称: 值”对表示,受RFC822标准启发。我们也称这些对为头部或属性。
名称-值对的组称为“部分”。部分与其他部分之间由空行分隔。
任何形式的二进制数据都表示为base64。对于导致行长度超过72个字节的二进制数据,需要进行延续。二进制数据的示例包括摘要和签名。
实现应支持最多65535字节的头部值。
本文档中的所有规范都使用相同的语法,其中终端符号显示为固定宽度字体,非终端符号显示为斜体。
规范:
section: | 部分: |
---|---|
nonempty-section: | 非空部分: |
newline: | CR LF | LF | CR (不跟随 LF ) |
header: | 名称 : 值 |
name: | 字母数字 *头部字符 |
value: | 空格 *其他字符 换行 *继续 |
continuation: | 空格 *其他字符 换行 |
alphanum: | {A-Z } | {a-z } | {0-9 } |
headerchar: | 字母数字 | - | _ |
otherchar: | 除 NUL, CR 和 LF 之外的任何 UTF-8 字符 |
- 注意:为防止通过纯文本电子邮件发送的文件损坏,没有标题将以四个字母“From”开头。
上述规范中定义的非终端符号将在以下规范中引用。
JAR清单
概述
JAR文件清单由一个主部分和一个由单独的JAR文件条目部分组成的列表组成,每个部分之间用换行符分隔。主部分和单独的部分都遵循上面指定的部分语法。它们各自具有自己的特定限制和规则。
-
主部分包含有关JAR文件本身以及应用程序的安全性和配置信息。它还定义了适用于每个单独清单条目的主要属性。此部分以空行终止。
-
单独的部分为此JAR文件中包含的包或文件定义各种属性。并非JAR文件中的所有文件都需要在清单中列出为条目,但所有要签名的文件必须列出。清单文件本身不得列出。每个部分必须以名称为“
Name
”的属性开头,其值必须是文件的相对路径,或引用存档之外数据的绝对URL。 -
如果同一文件条目有多个单独的部分,则这些部分中的属性将被合并。如果某个属性在不同部分中具有不同的值,则将识别最后一个值。
-
未理解的属性将被忽略。此类属性可能包括应用程序使用的实现特定信息。
清单规范:
manifest-file: | 主要部分 换行 *单独部分 |
---|---|
main-section: | 版本信息 换行 *主属性 |
version-info: | Manifest-Version : 版本号 |
version-number: | 数字+{. 数字+}* |
main-attribute: | (任何合法的主属性) 换行 |
individual-section: | Name : 值 换行 *每个条目属性 |
perentry-attribute: | (任何合法的每个条目属性) 换行 |
newline: | CR LF | LF | CR (不跟随 LF ) |
digit: | {0-9} |
在上述规范中,可以出现在主部分中的属性称为主属性,而可以出现在单独部分中的属性称为每个条目属性。某些属性可以同时出现在主部分和单独部分中,在这种情况下,每个条目属性值将覆盖指定条目的主属性值。这两种属性类型定义如下。
主属性
主属性是出现在清单主部分中的属性。它们分为以下不同组:
- 一般主属性
- Manifest-Version: 定义清单文件版本。该值是一个合法的版本号,如上述规范中所述。
- Created-By: 定义生成此清单文件的Java实现的版本和供应商。此属性由
jar
工具生成。 - Signature-Version: 定义jar文件的签名版本。该值应为有效的版本号字符串。
- Class-Path: 此属性的值指定此应用程序所需的库的相对URL。URL由一个或多个空格分隔。应用程序类加载器使用此属性的值来构建其内部搜索路径。有关详细信息,请参阅Class-Path属性部分。
- Automatic-Module-Name: 如果此JAR文件作为模块路径上的自动模块部署,则定义模块名称。有关详细信息,请参阅
automatic modules
的规范。 - Multi-Release: 此属性定义此JAR文件是否为多版本 JAR文件。如果值为“true”,则忽略大小写,则Java运行时和工具将处理此JAR文件作为多版本JAR文件。否则,如果值不是“true”,则忽略此属性。
- 为独立应用程序定义的属性:此属性由打包为可执行JAR文件的独立应用程序使用,可以通过直接运行“
java -jar x.jar
”来由Java运行时直接调用。- Main-Class: 此属性的值是启动器将在启动时加载的主应用程序类的类名。该值必须不附加
.class
扩展名到类名。 - Launcher-Agent-Class: 如果存在此属性,则其值是在调用应用程序主方法之前启动的Java代理的类名。此属性可用于将Java代理打包到与应用程序相同的可执行JAR文件中的情况。代理类在
java.lang.instrument
包摘要中指定了一个形式的公共静态方法名称agentmain
。其他属性(如Can-Retransform-Classes
)可用于指示代理所需的功能。
- Main-Class: 此属性的值是启动器将在启动时加载的主应用程序类的类名。该值必须不附加
- 包版本和封装信息定义的属性:这些属性的值适用于JAR文件中的所有包,但可以被每个条目属性覆盖。
- Implementation-Title: 值是定义扩展实现的标题的字符串。
- Implementation-Version: 值是定义扩展实现的版本的字符串。
- Implementation-Vendor: 值是定义维护扩展实现的组织的字符串。
- Specification-Title: 值是定义扩展规范的标题的字符串。
- Specification-Version: 值是定义扩展规范的版本的字符串。
- Specification-Vendor: 值是定义维护扩展规范的组织的字符串。
- Sealed: 此属性定义此JAR文件是否已封装。该值可以是“true”或“false”,忽略大小写。如果设置为“true”,则JAR文件中的所有包默认为封装,除非它们在单独定义时另有规定。另请参阅包封装部分。
每个条目属性
每个条目属性仅适用于清单条目所关联的单个JAR文件条目。如果同一属性也出现在主部分中,则每个条目属性的值将覆盖主属性的值。例如,如果JAR文件a.jar具有以下清单内容:
Manifest-Version: 1.0
Created-By: 1.8 (Oracle Inc.)
Sealed: true
Name: foo/bar/
Sealed: false
这意味着a.jar中存档的所有包都是封装的,除了包foo.bar不是。
每个条目属性分为以下组:
- 为文件内容定义的属性:
- 用于包版本和封装信息定义的属性:这些属性与上述定义的主属性相同,用于定义扩展包版本和封装信息。当用作每个条目的属性时,这些属性会覆盖主属性,但仅适用于清单条目指定的个别文件。
- 为Beans对象定义的属性:
- Java-Bean: 定义特定JAR文件条目是否为Java Beans对象。该值应为"true"或"false"中的一个,不区分大小写。
- 用于签名的属性:这些属性用于签名和验证目的。更多细节请参阅此处。
- x-Digest-y: 此属性的名称指定用于计算相应JAR文件条目的摘要值的摘要算法的名称。此属性的值存储实际的摘要值。前缀'x'指定算法名称,可选的后缀'y'指示摘要值应与哪种语言进行验证。
- Magic: 这是一个可选属性,应用程序可以使用它来指示验证程序应如何计算清单条目中包含的摘要值。此属性的值是一组逗号分隔的上下文特定字符串。详细描述请参见此处。
已签名的JAR文件
概述
可以使用命令行 jarsigner 工具或直接通过 java.security
API 对JAR文件进行签名。如果使用 jarsigner 工具对JAR文件进行签名,则包括 META-INF
目录中的非签名相关文件在内的每个文件条目都将被签名。与签名相关的文件包括:
META-INF/MANIFEST.MF
META-INF/*.SF
META-INF/*.DSA
META-INF/*.RSA
META-INF/*.EC
META-INF/SIG-*
请注意,如果这些文件位于 META-INF
子目录中,则它们不被视为与签名相关。这些文件名的不区分大小写版本也被保留,并且也不会被签名。
可以使用 java.security
API 对JAR文件的子集进行签名。已签名的JAR文件与原始JAR文件完全相同,只是其清单已更新,并且在 META-INF
目录中添加了两个附加文件:一个签名文件和一个签名块文件。当未使用 jarsigner 时,签名程序必须构建签名文件和签名块文件。
对于已签名的JAR文件中的每个文件条目,只要清单中不存在该条目,就会为其创建一个单独的清单条目。每个清单条目列出一个或多个摘要属性和一个可选的 Magic 属性。
签名文件
每个签名者由扩展名为 .SF
的签名文件表示。该文件的主要部分类似于清单文件。它包括一个主要部分,其中包含签名者提供的信息,但不特定于任何特定的JAR文件条目。除了 Signature-Version
和 Created-By
属性(请参见 主属性)之外,主要部分还可以包括以下安全属性:
- x-Digest-Manifest-Main-Attributes(其中 x 是
java.security.MessageDigest
算法的标准名称):此属性的值是清单主属性的摘要值。 - x-Digest-Manifest(其中 x 是
java.security.MessageDigest
算法的标准名称):此属性的值是整个清单的摘要值。
主要部分后面是一个列出各个条目的列表,这些条目的名称也必须存在于清单文件中。每个单独的条目必须至少包含其在清单文件中对应条目的摘要。
出现在清单文件中但不在签名文件中的路径或URL不会用于计算。
签名验证
成功的JAR文件验证发生在签名有效且在生成签名时存在于JAR文件中的文件没有发生更改的情况下。JAR文件验证涉及以下步骤:
-
在首次解析清单时验证签名文件上的签名。为了效率,此验证可以被记住。请注意,此验证仅验证签名指令本身,而不验证实际的存档文件。
-
如果签名文件中存在
x-Digest-Manifest
属性,则验证该值是否与整个清单上计算的摘要匹配。如果签名文件中存在多个x-Digest-Manifest
属性,则验证至少一个与计算的摘要值匹配。 -
如果签名文件中不存在
x-Digest-Manifest
属性或在上一步中计算的摘要值都不匹配,则执行较少优化的验证:-
如果签名文件中存在
x-Digest-Manifest-Main-Attributes
条目,则验证该值是否与清单文件中主属性的摘要匹配。如果此计算失败,则JAR文件验证失败。此决定可以被记住以提高效率。如果签名文件中不存在x-Digest-Manifest-Main-Attributes
条目,则其不存在不会影响JAR文件验证,也不会验证清单主属性。 -
将签名文件中每个源文件信息部分中的摘要值与清单文件中相应条目的摘要值进行比较。如果任何摘要值不匹配,则JAR文件验证失败。
存储在
x-Digest-Manifest
属性中的清单文件的摘要值与当前清单文件的摘要值不匹配的一个原因是,它可能包含在签名后添加的新文件的部分。例如,假设在生成签名后向JAR文件(使用jar工具)添加了一个或多个文件。如果JAR文件由不同的签名者再次签名,则清单文件会更改(jarsigner工具会为新文件添加部分),并创建一个新的签名文件,但原始签名文件不会更改。如果签名文件中非标头部分的摘要值等于清单文件中相应部分的摘要值,则对原始签名的验证仍被视为成功,只要自签名后JAR文件中的文件没有发生更改,这种情况下签名文件的摘要值等于清单文件中相应部分的摘要值。 -
-
对于清单中的每个条目,将清单文件中的摘要值与“Name:”属性中引用的实际数据上计算的摘要值进行比较,该属性指定相对文件路径或URL。如果任何摘要值不匹配,则JAR文件验证失败。
示例清单文件:
Manifest-Version: 1.0
Created-By: 1.8.0 (Oracle Inc.)
Name: common/class1.class
SHA-256-Digest: (SHA-256摘要的base64表示)
Name: common/class2.class
SHA1-Digest: (SHA1摘要的base64表示)
SHA-256-Digest: (SHA-256摘要的base64表示)
相应的签名文件将是:
Signature-Version: 1.0
SHA-256-Digest-Manifest: (SHA-256摘要的base64表示)
SHA-256-Digest-Manifest-Main-Attributes: (SHA-256摘要的base64表示)
Name: common/class1.class
SHA-256-Digest: (SHA-256摘要的base64表示)
Name: common/class2.class
SHA-256-Digest: (SHA-256摘要的base64表示)
Magic 属性
验证给定清单条目上的签名的另一个要求是验证程序理解该条目的清单条目中的 Magic 键值的值或值。
Magic 属性是可选的,但要求解析器理解正在验证该条目签名的条目的 Magic 键的值。
Magic 属性的值或值是一组逗号分隔的上下文特定字符串。逗号前后的空格将被忽略。不区分大小写。Magic 属性的确切含义是特定于应用程序的。这些值指示如何计算清单条目中包含的哈希值,因此对签名的正确验证至关重要。关键字可用于动态或嵌入式内容,多语言文档的多个哈希值等。
以下是清单文件中 Magic 属性潜在用途的两个示例:
Name: http://www.example-scripts.com/index#script1
SHA-256-Digest: (SHA-256哈希的base64表示)
Magic: JavaScript, Dynamic
Name: http://www.example-tourist.com/guide.html
SHA-256-Digest: (SHA-256哈希的base64表示)
SHA-256-Digest-French: (SHA-256哈希的base64表示)
SHA-256-Digest-German: (SHA-256哈希的base64表示)
Magic: Multilingual
在第一个示例中,这些 Magic 值可能表示 http 查询的结果是嵌入在文档中的脚本,而不是文档本身,并且脚本是动态生成的。这两个信息指示如何计算哈希值以便与清单的摘要值进行比较,从而比较有效的签名。
在第二个示例中,Magic 值指示检索到的文档可能已经为特定语言进行内容协商,并且要验证的摘要取决于检索到的文档所写的语言。
数字签名
数字签名是 .SF
签名文件的签名版本。这些是不适合人类解释的二进制文件。
数字签名文件具有与 .SF 文件相同的文件名,但具有不同的扩展名。扩展名取决于签名者私钥的算法。
.RSA
(PKCS7签名,用于RSA或RSASSA-PSS密钥).DSA
(PKCS7签名,用于DSA密钥).EC
(PKCS7签名,用于EC或EdDSA密钥)
未列出上述签名算法的数字签名文件必须位于 META-INF
目录中,并且具有前缀 "SIG-
"。相应的签名文件(.SF
文件)也必须具有相同的前缀。
对于不支持外部签名数据的格式,文件应包含.SF
文件的签名副本。因此,某些数据可能会重复,验证者应该比较这两个文件。
支持外部数据的格式要么引用.SF
文件,要么对其进行隐式引用的计算。
每个.SF
文件可以有多个数字签名,但这些签名应由同一法律实体生成。
文件名扩展名可以是1到3个字母数字字符。未识别的扩展名将被忽略。
关于清单和签名文件的注意事项
以下是适用于清单和签名文件的附加限制和规则的列表。
- 属性:
- 在所有情况下,对于所有部分,未被理解的属性将被忽略。
- 属性名称不区分大小写。生成清单和签名文件的程序应该使用本规范中显示的大小写,然而。
- 属性名称不能在一个部分内重复。
- 版本:
- Manifest-Version和Signature-Version必须首先出现,并且正好是这种情况(以便它们可以被轻松识别为魔术字符串)。除此之外,在主要部分内属性的顺序并不重要。
- 顺序:
- 单个清单条目的顺序并不重要。
- 单个签名条目的顺序并不重要,除了被签名的摘要是按照那个顺序的。
- 行长度:
- 没有一行的长度可以超过72个字节(而不是字符),以其UTF8编码形式计算。如果一个值使得初始行超过这个长度,它应该在额外的行上继续(每行以一个单独的空格开头)。
- 错误:
- 如果一个文件无法按照本规范解析,应输出警告,并且不应信任任何签名。
- 限制:
- 因为头部名称不能继续,头部名称的最大长度为70个字节(名称后必须有一个冒号和一个空格)。
- NUL、CR和LF不能嵌入在头部值中,NUL、CR、LF和“:”不能嵌入在头部名称中。
- 实现应支持65535字节(而不是字符)的头部值,以及每个文件65535个头部。它们可能会耗尽内存,但不应该在这些值以下有硬编码限制。
- 签名者:
- 从技术上讲,不同实体可能使用不同的签名算法来共享单个签名文件。这违反了标准,额外的签名可能会被忽略。
- 算法:
- 本标准不规定任何摘要算法或签名算法。然而,必须支持至少SHA-256和SHA1摘要算法中的一个。
Class-Path属性
应用程序的清单可以指定一个或多个相对URL,用于引用其所需的其他库的JAR文件和目录。这些相对URL将相对于包含应用程序加载的代码库(“上下文JAR”)进行处理。
应用程序(或更一般地说,JAR文件)通过清单属性Class-Path
指定其所需库的相对URL。该属性列出了要搜索其他库的实现的URL,如果它们在主机Java虚拟机上找不到。这些相对URL可以包括应用程序所需的任何库或资源的JAR文件和目录。不以'/'结尾的相对URL被假定是指向JAR文件。例如,
Class-Path: servlet.jar infobus.jar acme/beans.jar images/
一个JAR文件的清单中最多可以指定一个Class-Path
头。
如果以下条件为真,则Class-Path
条目有效:
-
它可以通过将其与上下文JAR的URL解析来创建一个
URL
。 -
它是相对的,而不是绝对的,即它不包含方案组件,除非上下文JAR是从文件系统加载的,这种情况下允许使用
file
方案出于兼容性原因。 -
由此条目表示的JAR文件或目录的位置位于上下文JAR的包含目录内。不允许使用“../”导航到父目录,除非上下文JAR是从文件系统加载的情况。
无效条目将被忽略。有效条目将根据上下文JAR解析。如果生成的URL无效或引用无法找到的资源,则将被忽略。重复的URL将被忽略。
生成的URL将被插入到类路径中,紧随上下文JAR的URL之后。例如,给定以下类路径:
a.jar b.jar
如果b.jar
包含以下Class-Path
清单属性:
Class-Path: lib/x.jar a.jar
那么这样一个URLClassLoader
实例的有效搜索路径将是:
a.jar b.jar lib/x.jar
当然,如果x.jar
有自己的依赖关系,那么根据相同的规则添加这些依赖关系,以及对于每个后续URL的处理。在实际实现中,JAR文件依赖关系是懒处理的,因此直到需要时才会实际打开JAR文件。
包封装
JAR文件和包可以选择性地进行封装,以便包可以在一个版本内强制执行一致性。
在JAR中封装的包指定该包中定义的所有类必须源自同一个JAR。否则,将抛出SecurityException
。
封装的JAR指定该JAR定义的所有包都是封装的,除非为特定包明确覆盖。
通过清单属性Sealed
指定封装的包,其值为true
或false
(大小写不敏感)。例如,
Name: javax/servlet/internal/
Sealed: true
指定javax.servlet.internal
包是封装的,该包中的所有类必须从同一个JAR文件加载。
如果缺少此属性,则包封装属性为包含JAR文件的属性。
通过相同的清单头Sealed
指定封装的JAR,其值再次为true
或false
。例如,
Sealed: true
指定此存档中的所有包都是封装的,除非在清单条目中明确覆盖特定包的Sealed
属性。
如果缺少此属性,则假定JAR文件未封装,以保持向后兼容性。然后系统默认检查包头以获取封装信息。
包封装对于安全性也很重要,因为它限制对仅限于同一JAR文件中源自的包中定义的成员的访问。
未命名的包不可封装,因此要封装的类必须放在自己的包中。