// `tally-seal v1`:给摘要再盖一枚**用密钥认过**的印记。
//
// 三层各司其职,别混:
// * `tally-plan v3` 给人看:哪段保留/截断/丢弃;
// * `tally-digest v1` 给机器算:报告字节与逐段字节的 sha256(**谁都能算**);
// * `tally-seal v1` 给"谁认过":摘要字节的 sha256 + 用共享密钥算出的 HMAC。
//
// 为什么不是公钥签名:核心包零第三方依赖,做 Ed25519 是另一件事;
// 而 HMAC 已经把"文件被换过"和"没换但也没人认过"分开了。边界写进格式:
// 它证明**持有同一把密钥的人**认过这份摘要,不证明"谁写的"。
//
// 形式(5 行,行序固定,逐字节冻结):
//
// tally-seal v1
// scheme hmac-sha256
// key_id ← 多密钥轮换时用来指认是哪把
// digest
// seal ← 覆盖上面 4 行(含头部),不含本行
//
// 只签 4 行不含 `seal` 行,是因为签名不可能覆盖自己。
///|
pub(all) struct Seal {
/// 目前只有 `hmac-sha256`;写进行里是为了将来换算法时能被显式拒绝
scheme : String
/// 密钥标识;`-` 表示未声明(轮换与审计用,不参与安全假设)
key_id : String
/// 被签摘要文件的 sha256(hex)
digest_sha : String
/// HMAC-SHA256 的 hex
seal : String
} derive(Debug)
///|
pub suberror SealError {
/// 不认识的头部
BadSealHeader(String)
/// 第 `line` 行格式不合法
SealMalformedLine(Int, String)
/// 行序固定,但这一行不是期望的键
SealUnexpectedField(Int, String, String)
/// 只有 `hmac-sha256`;别的算法宁可拒绝
SealSchemeUnsupported(String)
/// 密钥为空:空密钥等于人人可签
SealEmptyKey
/// `key_id` 含空白(会破坏空格分隔的格式)
SealBadKeyId(String)
/// 被签摘要与 `digest` 行不一致
SealDigestMismatch(String, String)
/// HMAC 不一致:密钥不对,或四行里有一处被改
SealMacMismatch(String, String)
}
///|
fn has_whitespace(s : String) -> Bool {
for c in s.to_array() {
if c == ' ' || c == '\n' || c == '\r' || c == '\t' {
return true
}
}
false
}
///|
/// 规范化的 `key_id`:空串写成 `-`。
fn normalized_key_id(key_id : String) -> String raise SealError {
if key_id == "" {
"-"
} else {
if has_whitespace(key_id) {
raise SealBadKeyId(key_id)
}
key_id
}
}
///|
/// 被签的 4 行(含头部)。`seal` 行不在此列。
fn signing_text(seal : Seal) -> String {
"tally-seal v1\nscheme " +
seal.scheme +
"\nkey_id " +
seal.key_id +
"\ndigest " +
seal.digest_sha +
"\n"
}
///|
/// 渲染成规范文本(5 行,以 `\n` 结尾)。
fn Seal::render(self : Seal) -> String {
signing_text(self) + "seal " + self.seal + "\n"
}
///|
/// 给摘要盖印记。`key` 是共享密钥的 UTF-8 字节;空密钥直接拒绝。
pub fn seal_digest(
digest_text : String,
key : String,
key_id : String,
) -> String raise SealError {
if key == "" {
raise SealEmptyKey
}
let digest_sha = sha256_hex(@utf8.encode(normalize_newlines(digest_text)))
let key_id = normalized_key_id(key_id)
let unsigned : Seal = {
scheme: "hmac-sha256",
key_id,
digest_sha,
seal: "",
}
let mac = hmac_sha256_hex(@utf8.encode(key), @utf8.encode(signing_text(unsigned)))
let signed : Seal = { scheme: "hmac-sha256", key_id, digest_sha, seal: mac }
signed.render()
}
///|
/// 盖章的非异常通道:把构造器折成带位置的一句话。
///
/// 实际只有 `SealEmptyKey` / `SealBadKeyId` 可达(输入问题),其余照列是为了
/// 让 `catch` 穷尽——同时把"哪些错误存在"集中在 `SealError` 一处。
pub fn seal_digest_result(
digest_text : String,
key : String,
key_id : String,
) -> Result[String, String] {
let outcome : Result[String, SealError] = try {
Ok(seal_digest(digest_text, key, key_id))
} catch {
BadSealHeader(h) => Err(BadSealHeader(h))
SealMalformedLine(n, t) => Err(SealMalformedLine(n, t))
SealUnexpectedField(n, a, b) => Err(SealUnexpectedField(n, a, b))
SealSchemeUnsupported(s) => Err(SealSchemeUnsupported(s))
SealEmptyKey => Err(SealEmptyKey)
SealBadKeyId(k) => Err(SealBadKeyId(k))
SealDigestMismatch(a, b) => Err(SealDigestMismatch(a, b))
SealMacMismatch(a, b) => Err(SealMacMismatch(a, b))
}
match outcome {
Ok(text) => Ok(text)
Err(e) => Err(e.message())
}
}
///|
/// 解析印记文本。行序固定,键必须一一对上;不做"尽力而为"的容错。
pub fn parse_seal(text : String) -> Seal raise SealError {
let text = normalize_newlines(text)
let lines : Array[String] = []
for raw in text.split("\n") {
let line = raw.trim()
if line.length() > 0 {
lines.push(line.to_owned())
}
}
if lines.length() != 5 {
raise SealMalformedLine(
lines.length(),
"expected exactly 5 lines (header + scheme + key_id + digest + seal)",
)
}
if lines[0] != "tally-seal v1" {
raise BadSealHeader(lines[0])
}
let expected = ["scheme", "key_id", "digest", "seal"]
let mut scheme = ""
let mut key_id = ""
let mut digest_sha = ""
let mut seal = ""
for i = 1; i < 5; i = i + 1 {
let parts = split_spaces(lines[i])
if parts.length() != 2 {
raise SealMalformedLine(i + 1, lines[i])
}
if parts[0] != expected[i - 1] {
raise SealUnexpectedField(i + 1, expected[i - 1], parts[0])
}
if i == 1 {
scheme = parts[1]
} else if i == 2 {
key_id = parts[1]
} else if i == 3 {
digest_sha = parts[1]
} else {
seal = parts[1]
}
}
if scheme != "hmac-sha256" {
raise SealSchemeUnsupported(scheme)
}
if key_id == "" {
raise SealMalformedLine(3, "empty key_id (use `-`)")
}
if digest_sha.length() != 64 || seal.length() != 64 {
raise SealMalformedLine(4, "digest and seal must be 64 hex chars")
}
{ scheme, key_id, digest_sha, seal }
}
///|
/// 校验一对「摘要 + 印记」:摘要没被换、印记确实由同一把密钥对这份摘要盖出。
pub fn verify_seal_pair(
digest_text : String,
seal_text : String,
key : String,
) -> Unit raise SealError {
if key == "" {
raise SealEmptyKey
}
let seal = parse_seal(seal_text)
let normalized = normalize_newlines(digest_text)
let actual_digest_sha = sha256_hex(@utf8.encode(normalized))
if actual_digest_sha != seal.digest_sha {
raise SealDigestMismatch(seal.digest_sha, actual_digest_sha)
}
let actual_seal = hmac_sha256_hex(@utf8.encode(key), @utf8.encode(signing_text(seal)))
if actual_seal != seal.seal {
raise SealMacMismatch(seal.seal, actual_seal)
}
}
///|
/// 给 CLI / 日志用的可读消息,位置与两个 hex 都保留在字符串里。
pub fn SealError::message(self : SealError) -> String {
match self {
BadSealHeader(h) => "unrecognised seal header: " + h
SealMalformedLine(n, t) => "seal line " + n.to_string() + ": malformed: " + t
SealUnexpectedField(n, expected, actual) =>
"seal line " +
n.to_string() +
": expected `" +
expected +
"`, got `" +
actual +
"`"
SealSchemeUnsupported(s) => "unsupported seal scheme: " + s
SealEmptyKey => "seal key must not be empty"
SealBadKeyId(k) => "seal key_id must not contain whitespace: " + k
SealDigestMismatch(expected, actual) =>
"seal digest mismatch: seal says " + expected + ", file is " + actual
SealMacMismatch(expected, actual) =>
"seal mismatch: seal says " + expected + ", recomputed " + actual
}
}
///|
/// 不 `raise` 的校验通道:理由同 `verify_report`(suberror 构造器不能经门面再导出)。
pub fn verify_seal(
digest_text : String,
seal_text : String,
key : String,
) -> Result[Unit, String] {
let err : SealError? = try {
verify_seal_pair(digest_text, seal_text, key)
None
} catch {
BadSealHeader(h) => Some(BadSealHeader(h))
SealMalformedLine(n, t) => Some(SealMalformedLine(n, t))
SealUnexpectedField(n, a, b) => Some(SealUnexpectedField(n, a, b))
SealSchemeUnsupported(s) => Some(SealSchemeUnsupported(s))
SealEmptyKey => Some(SealEmptyKey)
SealBadKeyId(k) => Some(SealBadKeyId(k))
SealDigestMismatch(a, b) => Some(SealDigestMismatch(a, b))
SealMacMismatch(a, b) => Some(SealMacMismatch(a, b))
}
match err {
None => Ok(())
Some(e) => Err(e.message())
}
}