机器翻译,未经母语人士审校。以英文版为准。

验证与证据在GitHub上编辑来源:docs/EVIDENCE-PACKS.md

证据包:GNSS信任事件的已签名记录

证据包是技术记录,不是法律意见。它说明Kshana引擎在所述配置下,根据所述接收机日志计算出了什么,并让任何人都能核查其中没有任何内容在事后被更改。它不说明某个事件的起因,不说明由谁负责,不说明任何义务是否已履行,也不说明日志显示的就是接收机实际收到的内容。清单和摘要都写明了这一点。

证据包用于事件报告、保险公司或监管机构的提问:“在这两个时间之间,你们的系统对GNSS信号说了什么,我们能信任这份记录吗?”

生成证据包#

# once: a signing key. The private key stays with you; never commit it.
kshana evidence keygen --out signer.key        # writes signer.key (0600) and signer.key.pub

kshana receiver-trust evidence session.toml \
    --from 100 --to 260 \                       # seconds since the first epoch, or ISO-8601 UTC
    --key signer.key --out pack/ \
    --title "Berth 4, 14 June"                  # optional

对于由实验室会话生成的证据包,--lab-attestation lab.toml会把操作人员对设置的陈述(传导式还是屏蔽式、衰减或屏蔽罩、任何射频发射源是什么)记录进已签名的清单;见HARDWARE-SAFETY.md。它按所给内容记录,从不核实。

--from和--to是自日志第一个历元起算的秒数,或者当日志给出自身起始时间时,是ISO-8601 UTC时间。--out不得已有文件。--created-utc接受2026-01-02T03:04:05Z这样的真实UTC时间(其他任何内容均被拒绝),或者none,后者会省去创建时间,并使证据包逐字节可复现。创建时间就是签名者所陈述的内容;没有任何东西对照时钟核查它,而可选的时间戳令牌正是为此而设。

密钥文件保存着一个32字节的Ed25519种子,以64个十六进制数字表示;可以自备(例如保存在你的密钥管理系统中的密钥),也可以让keygen生成一个。如果该文件可被其他用户读取,kshana会发出警告。.gitignore排除了*.evidence-key;如果你的密钥放在检出的工作副本中,请这样命名它。

证据包里有什么#

证据包的结构。接收机日志(NMEA、u-blox UBX、RINEX 3或Android日志)和一个时间窗口输入到kshana receiver-trust evidence,由它写出七个文件:log-slice.bin、config.json、epochs.json、summary.html、manifest.json、manifest.sig以及可选的timestamp.tsr。清单记录了整份日志和每个文件的SHA-256,以及覆盖这些文件的哈希链;签名覆盖清单。kshana evidence verify检查签名、每个文件的哈希、哈希链,以及没有未列出的文件,验证通过时退出码为0,出现失败时为1,签名者未被固定时为3。证据包是技术记录,不是法律意见。证据包的结构。接收机日志(NMEA、u-blox UBX、RINEX 3或Android日志)和一个时间窗口输入到kshana receiver-trust evidence,由它写出七个文件:log-slice.bin、config.json、epochs.json、summary.html、manifest.json、manifest.sig以及可选的timestamp.tsr。清单记录了整份日志和每个文件的SHA-256,以及覆盖这些文件的哈希链;签名覆盖清单。kshana evidence verify检查签名、每个文件的哈希、哈希链,以及没有未列出的文件,验证通过时退出码为0,出现失败时为1,签名者未被固定时为3。证据包是技术记录,不是法律意见。
文件 内容
log-slice.bin 该窗口内的原始接收机日志字节
config.json 所运行的场景(每个监测器阈值、事件和比较规则)、所运行的监测器、校准基线以及场景哈希值;对于船舶,还有每个船舶输出都带有的咨询性声明(仅供参考的软件,不是经型式认可的导航设备);自0.36起还有geometry_model(实际使用的[geometry]表、几何监测器的权重、各项出处和类别规则)。目录名和内联载荷已被移除。
epochs.json 窗口内的每个历元及引擎的统计量、告警(及其原因)和信任状态;自0.36起还有其卫星geometry(良好、带原因的降级,或日志未给出卫星几何时的未知),以及每项推断上的class(geometry、environment、inconsistency)
summary.html 一份自包含、不含脚本的摘要:标识、哈希值、结果、原因、与推断类别分开的几何通道(按几何状态划分的历元)(0.36)、每个阈值、逐历元表格,以及该记录的局限
manifest.json 引擎版本、窗口、创建时间、日志的格式和名称、完整日志的SHA-256、切片的范围及其SHA-256、签名者的公钥和指纹、每个文件的大小和SHA-256,以及覆盖这些文件的哈希链
manifest.sig 对manifest.json的精确字节所作的Ed25519签名
timestamp.tsr 可选的RFC 3161时间戳令牌

日志切片。读取器会报告每个历元在源文件中的字节跨度,因此切片就是包含窗口内每个历元的最小字节范围(byte-range):NMEA、u-blox UBX、RINEX 3观测数据和Android日志。它按原始字节计算哈希,绝不按解码后的文本。对于实时数据流或RINEX 2,没有跨度;此时会打包整份日志,清单中写明whole-log(窗口限定报告哪些历元,而不是包含哪些字节)。摘要会说明适用哪一种。切片是该窗口字节的记录,而不是一份独立的日志:例如,RINEX切片没有文件头。

哈希链。清单按固定顺序列出文件。link₀ = SHA-256("kshana- evidence-chain/1");对每个文件,linkᵢ = SHA-256(linkᵢ₋₁ ‖ SHA-256(file) ‖ len(name) ‖ name)(长度为4字节,大端序)。最后一个链环是chain_head。重新排序、删除或替换文件都会改变哈希链;签名覆盖该哈希链。

验证证据包#

kshana evidence verify pack/ --pubkey signer.key.pub [--log session.nmea] \
    [--require-timestamp] [--allow-unpinned] [--json]

退出状态0表示已验证;1表示有失败;2表示用法错误;3表示证据包完好,但其签名者未被固定(见下文)。每项检查报告通过、失败或跳过,且每个失败都有名称:

失败代码 含义
manifest-missing, manifest-malformed, unsupported-format 无法读取清单
signature-missing, signature-malformed, signature-invalid 没有签名,不是64字节的小写十六进制签名,或者无法通过对manifest.json的验证
public-key-malformed, public-key-mismatch 密钥无效,或与你提供的不同
file-missing, file-size-mismatch, file-hash-mismatch 所列文件缺失或与清单不符;会指出该文件
unlisted-file 某个文件在证据包中,但不在清单中
chain-mismatch, chain-head-mismatch 某个链环与其输入所得结果不符;会指出该文件
artifact-list-malformed, slice-record-inconsistent, epoch-count-mismatch 清单自相矛盾,或与epochs.json矛盾
full-log-hash-mismatch, slice-not-from-full-log 使用--log:你持有的日志不是所记录的那份,或者切片不是它的该范围
timestamp-missing, timestamp-malformed, timestamp-imprint-mismatch 见下文

更改任何文件中的一个字节,验证都会以相应的原因失败:被更改的产物会被报告为该文件的哈希不匹配,而不会有其他提示;被更改的清单会导致签名失败。编辑文件后重新计算哈希和哈希链,没有签名密钥也无济于事;此时只会签名失败。

固定密钥。没有--pubkey时,证据包只能证明它对于自己所署名的密钥是完好的,而这个密钥任何人都可以生成。因此验证此时会以退出码3结束,并显示“INTACT, BUT THE SIGNER IS NOT PINNED”而不是“VERIFIED”;--allow-unpinned会在知情的情况下接受证据包自带的密钥,并打印“VERIFIED against the key the pack names itself (signer NOT pinned; pass --pubkey)”。请通过你信任的途径(而不是从证据包中)向签名者获取其公钥,并用--pubkey传入:64个十六进制数字或.pub文件。误传私钥会被识别并拒绝。指纹(密钥SHA-256的前128位,32个十六进制数字)会被打印出来供肉眼比对;固定完整密钥才是可靠的做法。

证据包格式与验证程序(规范性)#

本节是独立验证程序据以编写的规范。表示要求的用语(必须、不得、可以)具有规范性。证据包是一个目录(或任何由文件名到字节的映射),其中的文件严格按下列名称命名;验证程序对每个文件按其存储的字节进行处理,绝不重新序列化任何内容。

文件。各产物文件按此固定顺序为log-slice.bin、config.json、epochs.json、summary.html。证据包还有manifest.json和manifest.sig,并且可以有timestamp.tsr。任何其他名称都是未列出的文件。

十六进制。每个十六进制字符串只含小写的0-9a-f,不含空白;不接受大写。SHA-256值为64个数字,公钥为64个数字,签名为128个数字。

manifest.sig。对manifest.json的精确字节所作的64字节Ed25519签名(RFC 8032,纯Ed25519方案)的128位小写十六进制表示,其后恰好跟一个\n,此外没有其他内容。签名必须严格检查:S值不小于群阶L的签名无效。

manifest.json。一个UTF-8 JSON对象。其成员如下(验证程序必须按所述JSON类型找到每一个;额外的成员会被忽略):

成员 类型 含义
format 字符串 必须等于kshana-evidence/1
title, engine_version, disclaimer 字符串 描述性
created_utc 字符串或null 创建时间,描述性
window 对象{from_s, to_s} 数字,单位为秒
epochs_in_window 非负整数 epochs.json中JSON数组的条目数
log 对象 format字符串,file_name字符串,full_sha256十六进制,full_bytes非负整数,start_label字符串或null,以及slice
log.slice 对象 kind(恰好为byte-range或whole-log;任何其他值都使清单格式错误),start和end为非负整数(字节偏移,末端不含),sha256十六进制
signer 对象 algorithm字符串(ed25519),public_key十六进制(32字节),fingerprint字符串
artifacts 对象数组 每个产物文件一项,按固定顺序:name字符串,role字符串,bytes非负整数,sha256十六进制,link十六进制
chain_head 十六进制 最后一个链环
lab_attestation 对象,可选 仅出现在由实验室会话生成的证据包中(--lab-attestation):操作人员的陈述operator、organisation、date、setup(conducted或shielded)、attenuation_db或enclosure、emitter、statement,以及可选的signature,按所给内容记录。它被清单签名覆盖,且从不核实;验证程序将其报告为“由所列操作人员证明;未经Kshana核实”。其他情况下不出现,没有它的证据包不受影响

signer.fingerprint是对32字节公钥所作SHA-256的前32个十六进制数字。

哈希链。 link0是对23个ASCII字节kshana-evidence-chain/1所作的SHA-256。按顺序对每个产物,link = SHA-256( previous_link (32 bytes) ‖ SHA-256(file) (32 bytes) ‖ len(name) as 4 bytes big-endian ‖ name as UTF-8 ),其中SHA-256(file)取自清单为该产物所记录的sha256(因此哈希链是清单的属性,并且是对照清单检查的,而不是对照文件)。

程序。验证程序按顺序执行这些步骤并收集每一个失败;结果是失败的集合,其中每项是一个code,并在有说明之处附上文件name。除有说明之处外,不得在第一个失败处停止。pin是验证程序被告知应当信任的公钥(如果有的话)。

  1. 没有manifest.json:失败manifest-missing;停止。
  2. 如果manifest.json不是一个JSON对象,或者没有以所述类型包含上述每个成员(包括带有每个成员的artifacts条目,以及log.slice):失败manifest-malformed。否则,如果format不是上述值:unsupported-format。
  3. 签名阶段。无论清单是否可读,都要执行此阶段。如果清单可读,则具名密钥为signer.public_key;如果它不是64个小写十六进制数字,则失败public-key-malformed(没有具名密钥);否则,如果signer.fingerprint不是它的指纹,则失败public-key-malformed(该密钥仍可使用)。如果同时存在pin和具名密钥且二者不同:失败public-key-mismatch。检查密钥是pin(如果给出了的话),否则是具名密钥(如果有的话);两者都没有时,就没有检查密钥(这包括清单不可读且未给出pin的所有情形)。如果没有manifest.sig:signature-missing。否则,如果该文件不是恰好128个小写十六进制数字加一个\n:signature-malformed。否则,如果存在检查密钥,但它不是有效的Ed25519公钥编码:public-key-malformed。否则,如果存在检查密钥,且签名对manifest.json的字节(无论其内容如何)严格验证不通过:signature-invalid。(没有检查密钥时,不检查签名,也不会因此产生失败。)
  4. 如果清单不可读(步骤2给出manifest-malformed):在此停止。
  5. 产物。如果artifacts的名称不是恰好按顺序排列的上述四个:artifact-list-malformed。然后对每个清单条目,按顺序:如果证据包中没有该名称的文件,则file-missing(名称);否则,如果其长度与bytes不同,则file-size-mismatch(名称);并且,另行独立判断,如果该文件的SHA-256与sha256不同,则file-hash-mismatch(名称)。
  6. 未列出的文件:对于证据包中除manifest.json、manifest.sig、timestamp.tsr以及清单artifacts中的名称之外的每个文件:unlisted-file(名称)。
  7. 哈希链。从link0起,按顺序遍历清单的artifacts。如果某条目的sha256不是64个小写十六进制数字:manifest-malformed,并放弃该哈希链(不做链头检查)。否则计算链环;如果它与该条目的link不同:chain-mismatch(名称)。遍历之后,如果哈希链未被放弃,且最后一个链环与chain_head不同:chain-head-mismatch。
  8. 切片与历元。如果出现以下任一情形,则触发slice-record-inconsistent(每个原因至多一次,且无论适用多少个原因,验证程序对该代码只报告一次):start > end;end > full_bytes;log-slice.bin条目的sha256与slice.sha256不同;该条目的bytes与end - start不同;或者kind为whole-log且出现start != 0、end != full_bytes、slice.sha256 != full_sha256中的任一项。如果epochs.json存在,且解析为长度与epochs_in_window不同的JSON数组:epoch-count-mismatch。
  9. 完整日志。仅当验证程序拿到了原始日志字节时:如果其SHA-256与log.full_sha256不同,则full-log-hash-mismatch;否则,如果范围start..end不在日志之内,或日志在该处的字节与log-slice.bin不同,则slice-not-from-full-log。
  10. 时间戳。没有timestamp.tsr时:如果验证程序被告知必须要求有时间戳,则timestamp-missing;否则什么也不做。有时间戳时:如果它无法按RFC 3161令牌或响应读取(见下文),则timestamp-malformed;否则,如果其消息摘要不是manifest.json的字节按该摘要自身算法(SHA-256、SHA-384或SHA-512)所得的哈希:timestamp-imprint-mismatch。

当且仅当失败集合为空时,证据包才算通过验证。签名者是否被固定另行报告,不改变失败集合。

时间戳令牌。令牌是一个DER ContentInfo,其内容类型为CMS SignedData(1.2.840.113549.1.7.2),其封装内容类型为id-ct-TSTInfo(1.2.840.113549.1.9.16.1.4);响应是一个DER TimeStampResp(PKIStatusInfo后跟令牌),其状态为0或1。TSTInfo的消息摘要算法必须是SHA-256、SHA-384或SHA-512,摘要长度与该算法的长度一致,其genTime必须是真实的UTC日历时间。不接受不定长度的BER。只有在给出受信任的锚点时(--tsa-ca,见下文),才检查授时机构的签名和证书链。

证据标签(0.40)#

由0.40或更高版本生成的证据包,在config.json之内带有一个evidence条目(等级,即VALIDATED、MODELLED或PARTNER,取其各行中最弱的状态;以及该证据包内容所依据的验证矩阵行的标识和需求:证据包完整性行、receiver-trust行,以及这些证据包对应的jammertest评分行或往返行),并带有config_version "1.1"。它位于config.json之内,因此清单的文件哈希、哈希链和签名都覆盖了它:改动它会导致验证失败。kshana evidence verify会报告等级和各行标识。

这一更改是增量式且仅向前兼容的。清单的format保持为kshana-evidence/1:上文的规范性文本规定验证器必须找到该值,而scripts/中的洁净室验证器确实如此。因此,次版本号由config_version承载(在0.40之前生成的证据包中缺省,表示1.0),而不是由新的format字符串承载。在0.40之前生成的证据包没有标签,验证结果不变;kshana evidence verify会显示“no evidence label (pre-0.40 pack)”。测试为tests/evidence_pack_label.rs(包括已提交的0.40之前的参考基准证据包)和tests/evidence_labels_doc_sync.rs。

RFC 3161时间戳(可选)#

来自时间戳授时机构的时间戳令牌,表明清单在该机构所证明的某个时间已经存在。令牌是针对manifest.json的,因此要在证据包生成之后再申请(创建证据包时无法提供令牌,并且create_bundle会拒绝并非针对该确切清单的令牌):

openssl ts -query -data pack/manifest.json -sha256 -cert -out req.tsq
# send req.tsq to your authority, get the response, then:
kshana evidence attach-timestamp pack/ response.tsr
openssl ts -verify -in response.tsr -data pack/manifest.json -CAfile authority-ca.pem

kshana evidence verify读取令牌(单独的令牌或完整响应),检查它已被授予,且其消息摘要等于manifest.json的哈希,并打印其所述的时间。没有受信任的锚点时,它会补充“timestamp authority signature not verified by Kshana”并报告authority_signature_verified: false;这一部分请运行上面的openssl ts -verify一行。摘要不同的令牌或无法解析的令牌属于失败。令牌的内容类型、摘要长度和生成时间会检查其是否合理。

使用--tsa-ca authority-ca.pem(一个或多个PEM证书,均受信任)时,verify还会像openssl ts -verify一样离线检查授时机构:已签名属性的消息摘要、签名(RSA PKCS#1 v1.5、RSA-PSS、ECDSA P-256或P-384)、ESS签名证书绑定、签名者的关键timeStamping扩展密钥用途,以及到自签发锚点的证书链,每个证书在令牌的genTime时都有效。失败时报告timestamp-authority-invalid并给出第一个未通过的检查,仅当全部通过时authority_signature_verified才为true。不检查吊销情况(CRL或OCSP)。该验证程序在27个自行生成的用例上与OpenSSL 3.0.13一致(tests/fixtures/evidence_tsa/PREREGISTRATION.md)。

令牌位于已签名的清单旁边,而不在其内部。签名和哈希链都不覆盖它,因此令牌可以被移除,或被另一个有效令牌替换,而签名不会失败。因此verify会在证据包没有令牌时给出提示,--require-timestamp则使缺失令牌成为失败(timestamp-missing);被替换的令牌只有在与清单不匹配时才会被发现。attach-timestamp除非传入--replace,否则拒绝覆盖已有的令牌。如果时间戳对你很重要,请将令牌和验证结果与你自己的记录一并保存,并要求提供它。

通过代码#

kshana::evidence::build_receiver_trust_pack是命令行和其他所有界面生成证据包时调用的唯一入口:输入是一个场景、日志的字节(对RINEX还有导航文件的字节)、文本形式的窗口、标题和可选的创建时间,外加签名种子;输出是文件、历元数、切片、清单哈希值和签名者指纹。它不读取文件,也不读取时钟,因此相同的输入给出相同的字节。在它之下,create_bundle和verify_bundle是纯函数:输入字节(EvidenceInput、32字节种子、由文件组成的BTreeMap<String, Vec<u8>>),输出可序列化的结果(Files、VerifyReport)。它们不使用文件、时钟或进程状态,可为wasm32构建,因此Python、WebAssembly和服务器界面可以原样调用它们。命令行只是它们的一层薄封装。

证据包不做什么#

  • 它不会重新运行评估。任何持有完整日志和config.json的人都可以重新运行kshana receiver-trust并与epochs.json比较;证据包证明的是所记录的内容,而不是引擎是否正确。
  • 它不能证明日志是真实或完整的。完整日志的哈希值把记录与一个文件绑定;该文件从何而来,须由操作人员自行确立。
  • 它不包含任何关于检测或抑制干扰的性能声明。
  • 它是技术记录,不是法律意见,也不是认证。