Freeze catalog signature vectors (T-614)
This commit is contained in:
@@ -10,12 +10,15 @@ import (
|
|||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
)
|
)
|
||||||
|
|
||||||
var integerJSONNumber = regexp.MustCompile(`^-?(0|[1-9][0-9]*)$`)
|
var integerJSONNumber = regexp.MustCompile(`^(0|[1-9][0-9]*|-[1-9][0-9]*)$`)
|
||||||
|
|
||||||
func parseRestrictedJSON(data []byte) (any, error) {
|
func parseRestrictedJSON(data []byte) (any, error) {
|
||||||
if !utf8.Valid(data) {
|
if !utf8.Valid(data) {
|
||||||
return nil, fmt.Errorf("%w: input is not valid UTF-8", ErrInvalidDocument)
|
return nil, fmt.Errorf("%w: input is not valid UTF-8", ErrInvalidDocument)
|
||||||
}
|
}
|
||||||
|
if err := validateJSONStringSurrogates(data); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
decoder := json.NewDecoder(bytes.NewReader(data))
|
decoder := json.NewDecoder(bytes.NewReader(data))
|
||||||
decoder.UseNumber()
|
decoder.UseNumber()
|
||||||
|
|
||||||
@@ -33,6 +36,78 @@ func parseRestrictedJSON(data []byte) (any, error) {
|
|||||||
return value, nil
|
return value, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateJSONStringSurrogates(data []byte) error {
|
||||||
|
for index := 0; index < len(data); index++ {
|
||||||
|
if data[index] != '"' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
next, err := scanJSONStringSurrogates(data, index)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
index = next - 1
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func scanJSONStringSurrogates(data []byte, start int) (int, error) {
|
||||||
|
for index := start + 1; index < len(data); index++ {
|
||||||
|
switch data[index] {
|
||||||
|
case '"':
|
||||||
|
return index + 1, nil
|
||||||
|
case '\\':
|
||||||
|
if index+1 >= len(data) {
|
||||||
|
return 0, fmt.Errorf("%w: incomplete string escape", ErrInvalidDocument)
|
||||||
|
}
|
||||||
|
if data[index+1] != 'u' {
|
||||||
|
index++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
codeUnit, ok := decodeJSONHexCodeUnit(data, index+2)
|
||||||
|
if !ok {
|
||||||
|
return 0, fmt.Errorf("%w: invalid unicode escape", ErrInvalidDocument)
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case codeUnit >= 0xd800 && codeUnit <= 0xdbff:
|
||||||
|
if index+7 >= len(data) || data[index+6] != '\\' || data[index+7] != 'u' {
|
||||||
|
return 0, fmt.Errorf("%w: high surrogate is not paired", ErrInvalidDocument)
|
||||||
|
}
|
||||||
|
lowSurrogate, ok := decodeJSONHexCodeUnit(data, index+8)
|
||||||
|
if !ok || lowSurrogate < 0xdc00 || lowSurrogate > 0xdfff {
|
||||||
|
return 0, fmt.Errorf("%w: high surrogate is not followed by a low surrogate", ErrInvalidDocument)
|
||||||
|
}
|
||||||
|
index += 11
|
||||||
|
case codeUnit >= 0xdc00 && codeUnit <= 0xdfff:
|
||||||
|
return 0, fmt.Errorf("%w: low surrogate has no high surrogate", ErrInvalidDocument)
|
||||||
|
default:
|
||||||
|
index += 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("%w: unterminated string", ErrInvalidDocument)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeJSONHexCodeUnit(data []byte, start int) (uint16, bool) {
|
||||||
|
if start+4 > len(data) {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
var value uint16
|
||||||
|
for _, digit := range data[start : start+4] {
|
||||||
|
value <<= 4
|
||||||
|
switch {
|
||||||
|
case digit >= '0' && digit <= '9':
|
||||||
|
value |= uint16(digit - '0')
|
||||||
|
case digit >= 'a' && digit <= 'f':
|
||||||
|
value |= uint16(digit-'a') + 10
|
||||||
|
case digit >= 'A' && digit <= 'F':
|
||||||
|
value |= uint16(digit-'A') + 10
|
||||||
|
default:
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value, true
|
||||||
|
}
|
||||||
|
|
||||||
func decodeJSONValue(decoder *json.Decoder) (any, error) {
|
func decodeJSONValue(decoder *json.Decoder) (any, error) {
|
||||||
token, err := decoder.Token()
|
token, err := decoder.Token()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,171 @@
|
|||||||
|
package catalog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type canonicalVectorCorpus struct {
|
||||||
|
SchemaVersion int `json:"schema_version"`
|
||||||
|
PublicKeyBase64 string `json:"public_key_base64"`
|
||||||
|
Vectors []canonicalVector `json:"vectors"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type canonicalVector struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Document string `json:"document"`
|
||||||
|
SignedPayloadBase64 string `json:"signed_payload_base64"`
|
||||||
|
Signature string `json:"signature"`
|
||||||
|
WantError string `json:"want_error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVerifierCanonicalVectors(t *testing.T) {
|
||||||
|
corpus := readCanonicalVectorCorpus(t)
|
||||||
|
publicKey, err := base64.StdEncoding.DecodeString(corpus.PublicKeyBase64)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("decode corpus public key: %v", err)
|
||||||
|
}
|
||||||
|
verifier, err := NewVerifier(publicKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewVerifier() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, vector := range corpus.Vectors {
|
||||||
|
vector := vector
|
||||||
|
t.Run(vector.Name, func(t *testing.T) {
|
||||||
|
verified, err := verifier.Verify([]byte(vector.Document))
|
||||||
|
if vector.WantError != "" {
|
||||||
|
want := canonicalVectorError(t, vector.WantError)
|
||||||
|
if !errors.Is(err, want) {
|
||||||
|
t.Fatalf("Verify() error = %v, want %v", err, want)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Verify() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedPayload, err := base64.StdEncoding.DecodeString(vector.SignedPayloadBase64)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("decode static signed payload: %v", err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(verified.SignedPayload, expectedPayload) {
|
||||||
|
t.Fatalf(
|
||||||
|
"SignedPayload = %q, want static vector %q",
|
||||||
|
verified.SignedPayload,
|
||||||
|
expectedPayload,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
signature, err := base64.StdEncoding.DecodeString(vector.Signature)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("decode static signature: %v", err)
|
||||||
|
}
|
||||||
|
if !ed25519.Verify(ed25519.PublicKey(publicKey), expectedPayload, signature) {
|
||||||
|
t.Fatal("static signature does not verify the static signed payload")
|
||||||
|
}
|
||||||
|
if got := vectorDocumentSignature(t, vector.Document); got != vector.Signature {
|
||||||
|
t.Fatalf("document signature = %q, want static vector %q", got, vector.Signature)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParserRejectsNonCanonicalSignatureVectorText(t *testing.T) {
|
||||||
|
corpus := readCanonicalVectorCorpus(t)
|
||||||
|
for _, vector := range corpus.Vectors {
|
||||||
|
if vector.WantError != "signature_invalid" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
vector := vector
|
||||||
|
t.Run(vector.Name, func(t *testing.T) {
|
||||||
|
if err := validateSignature(vectorDocumentSignature(t, vector.Document)); err == nil {
|
||||||
|
t.Fatal("validateSignature() accepted a non-canonical signature text")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParserRejectsNonCanonicalPackageSignatureVectors(t *testing.T) {
|
||||||
|
corpus := readCanonicalVectorCorpus(t)
|
||||||
|
for _, vector := range corpus.Vectors {
|
||||||
|
if vector.WantError != "signature_invalid" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
vector := vector
|
||||||
|
t.Run(vector.Name, func(t *testing.T) {
|
||||||
|
manifest := validManifestForTest()
|
||||||
|
publishedPackage := manifest.Apps[0].Packages[ArchitectureAMD64]
|
||||||
|
publishedPackage.Signature = vectorDocumentSignature(t, vector.Document)
|
||||||
|
manifest.Apps[0].Packages[ArchitectureAMD64] = publishedPackage
|
||||||
|
|
||||||
|
_, err := parseSignedManifestForTest(t, manifest, ChannelModern)
|
||||||
|
if !errors.Is(err, ErrInvalidManifest) {
|
||||||
|
t.Fatalf("Parse() error = %v, want %v", err, ErrInvalidManifest)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readCanonicalVectorCorpus(t *testing.T) canonicalVectorCorpus {
|
||||||
|
t.Helper()
|
||||||
|
path := filepath.Join("..", "..", "testdata", "catalog", "canonical-vectors.json")
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open canonical vector corpus: %v", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
decoder := json.NewDecoder(file)
|
||||||
|
decoder.DisallowUnknownFields()
|
||||||
|
var corpus canonicalVectorCorpus
|
||||||
|
if err := decoder.Decode(&corpus); err != nil {
|
||||||
|
t.Fatalf("decode canonical vector corpus: %v", err)
|
||||||
|
}
|
||||||
|
if err := decoder.Decode(&struct{}{}); err != io.EOF {
|
||||||
|
t.Fatalf("canonical vector corpus has trailing data: %v", err)
|
||||||
|
}
|
||||||
|
if corpus.SchemaVersion != 1 {
|
||||||
|
t.Fatalf("corpus schema_version = %d, want 1", corpus.SchemaVersion)
|
||||||
|
}
|
||||||
|
if len(corpus.Vectors) == 0 {
|
||||||
|
t.Fatal("corpus has no vectors")
|
||||||
|
}
|
||||||
|
return corpus
|
||||||
|
}
|
||||||
|
|
||||||
|
func canonicalVectorError(t *testing.T, value string) error {
|
||||||
|
t.Helper()
|
||||||
|
switch value {
|
||||||
|
case "invalid_document":
|
||||||
|
return ErrInvalidDocument
|
||||||
|
case "unsupported_number":
|
||||||
|
return ErrUnsupportedNumber
|
||||||
|
case "signature_invalid":
|
||||||
|
return ErrSignatureInvalid
|
||||||
|
default:
|
||||||
|
t.Fatalf("unsupported corpus want_error %q", value)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func vectorDocumentSignature(t *testing.T, document string) string {
|
||||||
|
t.Helper()
|
||||||
|
var root struct {
|
||||||
|
Signature string `json:"signature"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal([]byte(document), &root); err != nil {
|
||||||
|
t.Fatalf("decode vector document signature: %v", err)
|
||||||
|
}
|
||||||
|
if root.Signature == "" {
|
||||||
|
t.Fatal("vector document has no signature")
|
||||||
|
}
|
||||||
|
return root.Signature
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@ package catalog
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/base64"
|
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
@@ -231,12 +230,9 @@ func validatePackage(architecture Architecture, publishedPackage Package) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
func validateSignature(value string) error {
|
func validateSignature(value string) error {
|
||||||
signature, err := base64.StdEncoding.Strict().DecodeString(value)
|
_, err := decodeCanonicalSignature(value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("must be strict Base64: %v", err)
|
return fmt.Errorf("must be canonical padded Base64 for 64 bytes: %v", err)
|
||||||
}
|
|
||||||
if len(signature) != 64 {
|
|
||||||
return fmt.Errorf("must decode to 64 bytes")
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-10
@@ -66,17 +66,9 @@ func (verifier Verifier) Verify(document []byte) (VerifiedDocument, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return VerifiedDocument{}, err
|
return VerifiedDocument{}, err
|
||||||
}
|
}
|
||||||
signature, err := base64.StdEncoding.Strict().DecodeString(signatureText)
|
signature, err := decodeCanonicalSignature(signatureText)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return VerifiedDocument{}, fmt.Errorf("%w: base64: %v", ErrSignatureInvalid, err)
|
return VerifiedDocument{}, fmt.Errorf("%w: %v", ErrSignatureInvalid, err)
|
||||||
}
|
|
||||||
if len(signature) != ed25519.SignatureSize {
|
|
||||||
return VerifiedDocument{}, fmt.Errorf(
|
|
||||||
"%w: got %d signature bytes, want %d",
|
|
||||||
ErrSignatureInvalid,
|
|
||||||
len(signature),
|
|
||||||
ed25519.SignatureSize,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
if !ed25519.Verify(verifier.publicKey, signedPayload, signature) {
|
if !ed25519.Verify(verifier.publicKey, signedPayload, signature) {
|
||||||
return VerifiedDocument{}, ErrSignatureInvalid
|
return VerifiedDocument{}, ErrSignatureInvalid
|
||||||
@@ -87,3 +79,21 @@ func (verifier Verifier) Verify(document []byte) (VerifiedDocument, error) {
|
|||||||
SignedPayload: append([]byte(nil), signedPayload...),
|
SignedPayload: append([]byte(nil), signedPayload...),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func decodeCanonicalSignature(value string) ([]byte, error) {
|
||||||
|
signature, err := base64.StdEncoding.Strict().DecodeString(value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid standard Base64: %w", err)
|
||||||
|
}
|
||||||
|
if base64.StdEncoding.EncodeToString(signature) != value {
|
||||||
|
return nil, errors.New("signature must use canonical padded Base64")
|
||||||
|
}
|
||||||
|
if len(signature) != ed25519.SignatureSize {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"got %d signature bytes, want %d",
|
||||||
|
len(signature),
|
||||||
|
ed25519.SignatureSize,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return signature, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ SoftBox 软件盒子是一个使用 Go + Gio 开发的 Windows 桌面客户端,
|
|||||||
|
|
||||||
## 当前阶段
|
## 当前阶段
|
||||||
|
|
||||||
当前项目已完成 Phase 0~2、T-301 与审核整改 `T-604`~`T-613`。Windows 安全路径阻断项、图标缓存资源边界、后台结果回 UI 线程的事件接线、双适配器交互契约、`VisibleItems` 快照生命周期、双端 Gio shell 职责拆分、unsafe cache 安全诊断/runbook、ZIP 中央目录/EOCD(含 ZIP64)预扫描以及安装文件/目录/journal 的代码层耐久顺序均已关闭;`T-614` 是当前唯一任务,用于冻结 Catalog canonicalization/签名跨实现向量。物理断电、文件锁与杀毒软件干扰验证仍后置到 T-302/T-601,T-302 暂后置。
|
当前项目已完成 Phase 0~2、T-301 与审核整改 `T-604`~`T-614`。Windows 安全路径阻断项、图标缓存资源边界、后台结果回 UI 线程的事件接线、双适配器交互契约、`VisibleItems` 快照生命周期、双端 Gio shell 职责拆分、unsafe cache 安全诊断/runbook、ZIP 中央目录/EOCD(含 ZIP64)预扫描、安装文件/目录/journal 的代码层耐久顺序以及 Catalog canonicalization/签名静态 corpus 均已关闭;下一步正式落成 T-302。物理断电、文件锁与杀毒软件干扰验证仍后置到 T-302/T-601。
|
||||||
|
|
||||||
优先路径:
|
优先路径:
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@ SoftBox 软件盒子是一个使用 Go + Gio 开发的 Windows 桌面客户端,
|
|||||||
2. 已完成 Phase 1:清单验签、ZIP 安全解压、原子切换回滚原型。
|
2. 已完成 Phase 1:清单验签、ZIP 安全解压、原子切换回滚原型。
|
||||||
3. 已完成 Phase 2 与 T-301:清单/列表/详情/图标缓存 + 可恢复下载队列。
|
3. 已完成 Phase 2 与 T-301:清单/列表/详情/图标缓存 + 可恢复下载队列。
|
||||||
4. 已完成 T-604:modern/Win7 workspace 与 Gio 版本解析彻底隔离。
|
4. 已完成 T-604:modern/Win7 workspace 与 Gio 版本解析彻底隔离。
|
||||||
5. 已完成 T-606~T-613:图标缓存资源边界、UI 线程事件接线、双 Gio 适配器交互契约、`VisibleItems` generation 生命周期、双端 `shell.go` 同 package 镜像职责拆分、unsafe cache 诊断/人工恢复指引、ZIP 中央目录/EOCD 预扫描和安装耐久顺序;下一步执行 T-614 的独立签名向量任务,再继续 T-302/T-303 与 Phase 4-6。T-302/T-601 仍须补真实 Windows 环境的断电/干扰注入。
|
5. 已完成 T-606~T-614:图标缓存资源边界、UI 线程事件接线、双 Gio 适配器交互契约、`VisibleItems` generation 生命周期、双端 `shell.go` 同 package 镜像职责拆分、unsafe cache 诊断/人工恢复指引、ZIP 中央目录/EOCD 预扫描、安装耐久顺序和 Catalog 静态签名向量;下一步正式落成并执行 T-302,再继续 T-303 与 Phase 4-6。T-302/T-601 仍须补真实 Windows 环境的断电/干扰注入。
|
||||||
|
|
||||||
## 领取任务规则
|
## 领取任务规则
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -42,7 +42,7 @@
|
|||||||
|
|
||||||
#### Phase 1 交叉审核加固
|
#### Phase 1 交叉审核加固
|
||||||
|
|
||||||
Phase 1 安全整改按 `docs/review/phase1-security-review.md` 的交叉复核定稿顺序串行落成。T-605、T-612 与 T-613 已关闭;T-613 建立文件、目录和 journal 的代码层耐久顺序,物理断电验证仍后置到 T-302/T-601。T-614 冻结 Catalog canonicalization/签名跨实现 testdata corpus 与客户端拒绝规则;完成后才进入 T-302。
|
Phase 1 安全整改按 `docs/review/phase1-security-review.md` 的交叉复核定稿顺序串行落成。T-605、T-612、T-613 与 T-614 已关闭;T-613 建立文件、目录和 journal 的代码层耐久顺序,T-614 冻结 Catalog canonicalization/签名静态 corpus 与客户端拒绝规则。下一步按路线图正式落成并执行 T-302;物理断电验证仍后置到 T-302/T-601。
|
||||||
|
|
||||||
| ID | 任务 | 依赖 | 验收要点 |
|
| ID | 任务 | 依赖 | 验收要点 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
|
|||||||
@@ -13,22 +13,22 @@
|
|||||||
## 当前快照
|
## 当前快照
|
||||||
|
|
||||||
- 日期:2026-07-18
|
- 日期:2026-07-18
|
||||||
- 阶段:Phase 2 已完成(T-201~T-204);Phase 3 的 T-301 可恢复下载队列已完成;审核整改 T-604~T-613 已完成,T-614 是当前唯一 TODO,T-302 继续暂后置
|
- 阶段:Phase 2 已完成(T-201~T-204);Phase 3 的 T-301 可恢复下载队列已完成;审核整改 T-604~T-614 已完成,下一步应正式落成并执行 T-302
|
||||||
- 技术栈:根 Go 1.25 workspace 只纳入 core/app-modern,`app-win7/go.work` 独立纳入 core/app-win7;版本闸门证明 modern Gio v0.10.1 与 win7 Gio v0.6.0 不交叉解析
|
- 技术栈:根 Go 1.25 workspace 只纳入 core/app-modern,`app-win7/go.work` 独立纳入 core/app-win7;版本闸门证明 modern Gio v0.10.1 与 win7 Gio v0.6.0 不交叉解析
|
||||||
- 生产代码:core 已有 Catalog/本地状态/存储、共享 Windows 安全相对路径策略、安全 ZIP 解压/回滚原型(Extractor 在同一普通文件句柄上核对 expected Catalog size 后有界预扫 EOCD/ZIP64、中央目录与条目数,每个 payload Sync/Close 后同步 staging tree 及父目录;transaction/switch/rollback/recovery 的 journal、rename、清理经统一 fail-closed 耐久栅栏,Windows 使用目录句柄 FlushFileBuffers)、发布稳定只读 generation 的无 IO 软件列表模型、按 key in-flight + 流式有界读取 + 32 MiB/256-key LRU 的可信图标缓存、图标 Load/Decode 事件发布用例、有界 application event relay,以及默认并发 2 的持久可恢复下载队列;modern/win7 主循环已接 relay/Invalidate,AppShell 已实现搜索/分类/视图、惰性列表、详情右栏、完整图标失败 identity 生命周期与仅 `unsafe_cache` 可见的安全 locator/人工恢复提示,并按 root/header/catalog/detail/style 同 package 镜像职责拆文件
|
- 生产代码:core 已有 Catalog/本地状态/存储、共享 Windows 安全相对路径策略与静态跨实现 canonicalization/Ed25519 vector corpus(拒绝非法 surrogate、`-0` 和非唯一 Base64 signature,大整数保持 token)、安全 ZIP 解压/回滚原型(Extractor 在同一普通文件句柄上核对 expected Catalog size 后有界预扫 EOCD/ZIP64、中央目录与条目数,每个 payload Sync/Close 后同步 staging tree 及父目录;transaction/switch/rollback/recovery 的 journal、rename、清理经统一 fail-closed 耐久栅栏,Windows 使用目录句柄 FlushFileBuffers)、发布稳定只读 generation 的无 IO 软件列表模型、按 key in-flight + 流式有界读取 + 32 MiB/256-key LRU 的可信图标缓存、图标 Load/Decode 事件发布用例、有界 application event relay,以及默认并发 2 的持久可恢复下载队列;modern/win7 主循环已接 relay/Invalidate,AppShell 已实现搜索/分类/视图、惰性列表、详情右栏、完整图标失败 identity 生命周期与仅 `unsafe_cache` 可见的安全 locator/人工恢复提示,并按 root/header/catalog/detail/style 同 package 镜像职责拆文件
|
||||||
- 测试:core 覆盖 Catalog、列表快照 generation/零复制、SemVer/12 状态、本地安装记录、Windows dot-space/设备名/Unicode 折叠路径攻击、ZIP destination 包含性与 EOCD/ZIP64 原始包/中央目录/条目数预扫描、payload/staging tree/journal/rename/rollback/recovery/cleanup 耐久顺序及错误注入、Windows 原生目录 `FlushFileBuffers`、图标并发/取消/读取边界/LRU、真实目录/symlink fail-closed 与 cache→`unsafe_cache` event、relay 背压与关闭、下载并发/暂停/取消/重试/Range/断连/恢复/事件失败与文件身份替换;两个 app 覆盖 Editor/视图/分类/行/恢复/关闭接线、500 项 viewport、AppID 控件与分类控件生命周期、详情上下文、空状态语义、UI drain 前后、图标失败身份生命周期与 `unsafe_cache` 详情语义;安装恢复矩阵保持通过
|
- 测试:core 覆盖 Catalog 静态 canonicalization/Ed25519 vectors、非法 surrogate/`-0`/Base64 fail-closed、列表快照 generation/零复制、SemVer/12 状态、本地安装记录、Windows dot-space/设备名/Unicode 折叠路径攻击、ZIP destination 包含性与 EOCD/ZIP64 原始包/中央目录/条目数预扫描、payload/staging tree/journal/rename/rollback/recovery/cleanup 耐久顺序及错误注入、Windows 原生目录 `FlushFileBuffers`、图标并发/取消/读取边界/LRU、真实目录/symlink fail-closed 与 cache→`unsafe_cache` event、relay 背压与关闭、下载并发/暂停/取消/重试/Range/断连/恢复/事件失败与文件身份替换;两个 app 覆盖 Editor/视图/分类/行/恢复/关闭接线、500 项 viewport、AppID 控件与分类控件生命周期、详情上下文、空状态语义、UI drain 前后、图标失败身份生命周期与 `unsafe_cache` 详情语义;安装恢复矩阵保持通过
|
||||||
- 数据:`schemas/` 已有 manifest/app.json/installed-app.json/download-task.json v1 Schema并注明 Windows 路径运行时权威规则;`testdata/catalog/` 有公开虚构清单样例;`testdata/zip/` 与 `testdata/download/` 记录运行时生成的攻击/传输矩阵
|
- 数据:`schemas/` 已有 manifest/app.json/installed-app.json/download-task.json v1 Schema并注明 Windows 路径运行时权威规则;`testdata/catalog/` 有公开虚构清单样例和 v1 静态 canonicalization/Ed25519 corpus;`testdata/zip/` 与 `testdata/download/` 记录运行时生成的攻击/传输矩阵
|
||||||
- 标准启动路径:`./init.sh` / `./init.ps1`(同步依赖、执行完整 Phase 0 闸门、打印双目标构建命令)
|
- 标准启动路径:`./init.sh` / `./init.ps1`(同步依赖、执行完整 Phase 0 闸门、打印双目标构建命令)
|
||||||
- 标准验证路径:`bash scripts/verify_phase0.sh` / `./scripts/verify_phase0.ps1`
|
- 标准验证路径:`bash scripts/verify_phase0.sh` / `./scripts/verify_phase0.ps1`
|
||||||
- 版本管理:git 已初始化,main 分支,远端 origin 为 Gitea `opc/soft_quay`;harness 文档已提交
|
- 版本管理:git 已初始化,main 分支,远端 origin 为 Gitea `opc/soft_quay`;harness 文档已提交
|
||||||
- 当前 blocker:无;T-613 已建立 payload、staging tree、journal 与目录 rename/remove 的 fail-closed 耐久顺序,并由 Windows 原生目录 `FlushFileBuffers` 用例验证。T-614 将冻结 Catalog canonicalization/签名静态跨实现向量与 surrogate/Base64/`-0` 拒绝边界;完成前 T-302 继续后置。物理断电、文件锁/杀毒软件干扰仍需 T-302/T-601 的目标 Windows VM/真机故障注入
|
- 当前 blocker:无;T-614 已以静态 corpus 冻结客户端 Catalog canonicalization/签名行为,外部 `softbox-catalog` 消费 corpus 的 CI 证据仍需跨仓库协调,但不阻止落成 T-302。物理断电、文件锁/杀毒软件干扰仍需 T-302/T-601 的目标 Windows VM/真机故障注入
|
||||||
|
|
||||||
## 当前目录要点
|
## 当前目录要点
|
||||||
|
|
||||||
| 路径 | 状态 | 说明 |
|
| 路径 | 状态 | 说明 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `docs/` | 已有 | harness coding 文档集(本次初始化完成) |
|
| `docs/` | 已有 | harness coding 文档集(本次初始化完成) |
|
||||||
| `docs/tasks/` | 已有 | Phase 0~2、T-301 与 T-604~T-613 已完成;T-614 是当前唯一 TODO 的 Phase 1 签名向量整改,T-302 暂后置 |
|
| `docs/tasks/` | 已有 | Phase 0~2、T-301 与 T-604~T-614 已完成;下一步按路线图落成 T-302 安装流程整合任务 |
|
||||||
| `scripts/` | 已有 | harness 治理、core 边界、Go 版本检查与 Phase 0 双平台验证入口 |
|
| `scripts/` | 已有 | harness 治理、core 边界、Go 版本检查与 Phase 0 双平台验证入口 |
|
||||||
| `core/` | 已建 | Go 1.20 兼容;已有正式 Catalog、本地状态/存储、共享 Windows safepath、列表模型、有界并发图标缓存、图标事件/relay、可恢复下载队列与 Phase 1 安装安全原型 |
|
| `core/` | 已建 | Go 1.20 兼容;已有正式 Catalog、本地状态/存储、共享 Windows safepath、列表模型、有界并发图标缓存、图标事件/relay、可恢复下载队列与 Phase 1 安装安全原型 |
|
||||||
| `app-modern/` | 已建 | Go 1.25.0 + Gio v0.10.1;Modern AppShell 已接入虚拟列表、详情、图标事件 drain/过期拒绝和内存 ImageOp,并拆为五类 shell 职责文件 |
|
| `app-modern/` | 已建 | Go 1.25.0 + Gio v0.10.1;Modern AppShell 已接入虚拟列表、详情、图标事件 drain/过期拒绝和内存 ImageOp,并拆为五类 shell 职责文件 |
|
||||||
@@ -40,9 +40,9 @@
|
|||||||
|
|
||||||
任务状态以 `docs/tasks/` 各任务文件 frontmatter 的 `status` 为准。本节只写项目级摘要:
|
任务状态以 `docs/tasks/` 各任务文件 frontmatter 的 `status` 为准。本节只写项目级摘要:
|
||||||
|
|
||||||
- 已完成:Phase 0 的 `T-001`~`T-004`;Phase 1 的 `T-101`、`T-102`、`T-103`;Phase 2 的 `T-201`~`T-204`;Phase 3 的 `T-301`;审核整改 `T-604`~`T-613`。
|
- 已完成:Phase 0 的 `T-001`~`T-004`;Phase 1 的 `T-101`、`T-102`、`T-103`;Phase 2 的 `T-201`~`T-204`;Phase 3 的 `T-301`;审核整改 `T-604`~`T-614`。
|
||||||
- 正在进行:无。
|
- 正在进行:无。
|
||||||
- 下一个可领取任务:`T-614` — 冻结 Catalog canonicalization/签名静态跨实现向量,收紧 surrogate、`-0` 与 Base64 文本边界;完成后才可领取 T-302。T-302/T-601 的物理断电与干扰故障注入仍保留为发布前环境验证。
|
- 下一个可领取任务:暂无;应按 Phase 3 路线图先将 T-302 安装流程整合正式落成任务文件,再领取。T-302/T-601 的物理断电与干扰故障注入仍保留为发布前环境验证。
|
||||||
|
|
||||||
## 当前可运行内容
|
## 当前可运行内容
|
||||||
|
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ ZIP mode 决定 `0o600`/`0o700` 在目标 Windows 上基本不构成安全问题
|
|||||||
1. **[阻断 T-302 · 最高 · T-605]** 修 Windows 尾部空格/点、DOS 设备名与路径别名:建逐段 Windows 安全路径校验器(各层共用)+ 提取时 destination 包含性兜底检查;补 Win7/10/11 真实文件系统用例。关闭前不得把 Extractor 描述为"无路径穿越"。
|
1. **[阻断 T-302 · 最高 · T-605]** 修 Windows 尾部空格/点、DOS 设备名与路径别名:建逐段 Windows 安全路径校验器(各层共用)+ 提取时 destination 包含性兜底检查;补 Win7/10/11 真实文件系统用例。关闭前不得把 Extractor 描述为"无路径穿越"。
|
||||||
2. **[已关闭 · T-612]** 已在构造 `zip.Reader` 前增加同句柄包大小与中央目录/EOCD(含 ZIP64)预扫描边界;Extractor 强制接收 expected Catalog `size`,核对打开文件长度并限制原始包、中央目录与声明条目数,不只依赖 `len(archive.File)`。T-302 仍须把已验签 Catalog、完成下载文件和 SHA-256 编排为真实安装调用链。
|
2. **[已关闭 · T-612]** 已在构造 `zip.Reader` 前增加同句柄包大小与中央目录/EOCD(含 ZIP64)预扫描边界;Extractor 强制接收 expected Catalog `size`,核对打开文件长度并限制原始包、中央目录与声明条目数,不只依赖 `len(archive.File)`。T-302 仍须把已验签 Catalog、完成下载文件和 SHA-256 编排为真实安装调用链。
|
||||||
3. **[代码顺序已关闭 · T-613;发布前环境验证仍必做]** payload 文件 Sync/Close → staging 子目录、根与父目录元数据栅栏 → journal 临时文件 Sync/Close 与 root 栅栏 → 每次 rename 后 root 栅栏 → committed 耐久后才删 backup/journal。Windows 已使用 Win7 可用的目录句柄 `CreateFile(FILE_FLAG_BACKUP_SEMANTICS)` + `FlushFileBuffers`,失败 fail closed;fake 覆盖 payload/tree/journal/rename/rollback/recovery/cleanup 失败,且原生 Windows 用例验证目录 fence 可执行。T-302/T-601 仍必须在 VM/真机进行真实断电、文件锁/杀毒干扰注入,不能以单元测试替代。
|
3. **[代码顺序已关闭 · T-613;发布前环境验证仍必做]** payload 文件 Sync/Close → staging 子目录、根与父目录元数据栅栏 → journal 临时文件 Sync/Close 与 root 栅栏 → 每次 rename 后 root 栅栏 → committed 耐久后才删 backup/journal。Windows 已使用 Win7 可用的目录句柄 `CreateFile(FILE_FLAG_BACKUP_SEMANTICS)` + `FlushFileBuffers`,失败 fail closed;fake 覆盖 payload/tree/journal/rename/rollback/recovery/cleanup 失败,且原生 Windows 用例验证目录 fence 可执行。T-302/T-601 仍必须在 VM/真机进行真实断电、文件锁/杀毒干扰注入,不能以单元测试替代。
|
||||||
4. **[协议冻结前 · T-614]** `softbox-catalog` 与客户端独立 canonicalization/签名测试向量:Unicode 键序、转义、`<>&`、U+2028/2029、合法/非法 surrogate 拒绝策略、`-0`/大整数/嵌套 signature、base64 padding/CR/LF、同语义不同表示同签名字节。T-614 先在本仓库冻结 corpus 和客户端拒绝规则;外部发布端消费同一 corpus 的 CI 证据仍须单独取得。
|
4. **[客户端协议已关闭 · T-614;外部发布端验证待协调]** 已冻结静态 corpus 与客户端 canonicalization/签名拒绝规则:Unicode 键序、转义、`<>&`、U+2028/2029、合法/非法 surrogate、`-0`/大整数/嵌套 signature、Base64 padding/CR/LF/空白、同语义不同表示同 signing bytes。`softbox-catalog` 外部仓库仍必须消费相同 corpus 并提供独立 CI 证据;本仓库没有其源码,不能将客户端回归测试表述为发布端集成已完成。
|
||||||
5. **[整合时复核]** 按真实包采样调解压硬上限,保留硬边界,不降级为软信号。
|
5. **[整合时复核]** 按真实包采样调解压硬上限,保留硬边界,不降级为软信号。
|
||||||
6. **[实现时重点审]** T-402/T-403 真实健康检查:防"进程短暂启动即判健康"、错误工作目录/错误二进制被探活。
|
6. **[实现时重点审]** T-402/T-403 真实健康检查:防"进程短暂启动即判健康"、错误工作目录/错误二进制被探活。
|
||||||
7. **[低优先]** 固定解压落盘 mode,可执行入口由已验证 app.json/Catalog 定义。
|
7. **[低优先]** 固定解压落盘 mode,可执行入口由已验证 app.json/Catalog 定义。
|
||||||
@@ -222,3 +222,9 @@ ZIP mode 决定 `0o600`/`0o700` 在目标 Windows 上基本不构成安全问题
|
|||||||
- transaction、Switcher、rollback、Recovery 和受控删除统一经内部耐久接口:journal 临时文件先同步内容,所有 journal/目录 rename 或删除后同步 app root,未完成栅栏不得推进测试步骤或下一 phase。清理路径同时保留 `ErrRecoveryRequired` 与底层耐久错误,以便调用方可判定恢复状态。
|
- transaction、Switcher、rollback、Recovery 和受控删除统一经内部耐久接口:journal 临时文件先同步内容,所有 journal/目录 rename 或删除后同步 app root,未完成栅栏不得推进测试步骤或下一 phase。清理路径同时保留 `ErrRecoveryRequired` 与底层耐久错误,以便调用方可判定恢复状态。
|
||||||
- Windows 原生目录测试已验证读写目录句柄 `CreateFile(FILE_FLAG_BACKUP_SEMANTICS)` + `FlushFileBuffers`;Go 1.20 标准 `syscall` 路径不引入 Win10+ API 或第三方依赖。fake 覆盖 payload/tree、journal、switch、rollback/recovery 与 committed 清理的错误传播和后续 Recover 不变式。
|
- Windows 原生目录测试已验证读写目录句柄 `CreateFile(FILE_FLAG_BACKUP_SEMANTICS)` + `FlushFileBuffers`;Go 1.20 标准 `syscall` 路径不引入 Win10+ API 或第三方依赖。fake 覆盖 payload/tree、journal、switch、rollback/recovery 与 committed 清理的错误传播和后续 Recover 不变式。
|
||||||
- 关闭的是代码中的耐久顺序与 fail-closed 策略;T-302/T-601 的 Windows VM/真机物理断电、文件锁与杀毒软件故障注入仍为发布前环境验证,不宣称当前单元测试提供硬件级保证。
|
- 关闭的是代码中的耐久顺序与 fail-closed 策略;T-302/T-601 的 Windows VM/真机物理断电、文件锁与杀毒软件故障注入仍为发布前环境验证,不宣称当前单元测试提供硬件级保证。
|
||||||
|
|
||||||
|
### T-614 完成记录(2026-07-18)
|
||||||
|
|
||||||
|
- 新增 `testdata/catalog/canonical-vectors.json` v1:公开 RFC 8032 测试公钥、固定原始 document、固定 canonical signing bytes 和 Ed25519 signature 均为静态数据。测试先比较 bytes,再用静态公钥验证静态 signature,不调用客户端 canonicalizer 或运行时私钥生成期望值。
|
||||||
|
- corpus 覆盖 Unicode 键排序/转义、`<>&`、U+2028/U+2029、合法 pair 和三类非法 surrogate、`-0`、大整数、嵌套 `signature`、标准 padding、CR/LF/space/tab、padding 缺失/额外以及两种语义相同 JSON 表示。
|
||||||
|
- 客户端现在在 JSON decoder 前拒绝孤立/不匹配 surrogate,拒绝 `-0`;Verifier 和 Parser 共享标准 padded Base64 的 decode→reencode 相等检查。关闭的是客户端协议契约;外部 `softbox-catalog` 消费 corpus 的实现/CI 证据仍需跨仓库协调。
|
||||||
|
|||||||
+6
-3
@@ -3,12 +3,12 @@ id: T-614
|
|||||||
title: 冻结 Catalog 规范化与签名跨实现测试向量
|
title: 冻结 Catalog 规范化与签名跨实现测试向量
|
||||||
phase: 1
|
phase: 1
|
||||||
deps: [T-613]
|
deps: [T-613]
|
||||||
status: TODO
|
status: DONE
|
||||||
created: 2026-07-18
|
created: 2026-07-18
|
||||||
issue: null
|
issue: null
|
||||||
context_ref: null
|
context_ref: 9e5f3f4840b03dc90342a9bde923c39d6178b029
|
||||||
claim_branch: null
|
claim_branch: null
|
||||||
work_branch: null
|
work_branch: agent/codex/T-614
|
||||||
write_paths:
|
write_paths:
|
||||||
- docs/tasks/T-614.md
|
- docs/tasks/T-614.md
|
||||||
- core/catalog/
|
- core/catalog/
|
||||||
@@ -69,3 +69,6 @@ T-101 的 Catalog verifier 已能拒绝重复字段、尾随 JSON 和非整数
|
|||||||
|
|
||||||
- 2026-07-18:根据 `docs/review/phase1-security-review.md` 最终处理顺序第 4 项落成;T-613 已完成,因此 T-614 成为下一项唯一任务。
|
- 2026-07-18:根据 `docs/review/phase1-security-review.md` 最终处理顺序第 4 项落成;T-613 已完成,因此 T-614 成为下一项唯一任务。
|
||||||
- 2026-07-18:前置检索确认现有合法签名测试由同一客户端 canonicalizer/测试私钥生成,Go JSON decoder 会将非法 surrogate 替换为 U+FFFD,且标准库 strict Base64 仍容忍 CR/LF。任务据此冻结拒绝策略和静态跨实现 corpus,不把客户端自举测试误作发布端互操作证明。
|
- 2026-07-18:前置检索确认现有合法签名测试由同一客户端 canonicalizer/测试私钥生成,Go JSON decoder 会将非法 surrogate 替换为 U+FFFD,且标准库 strict Base64 仍容忍 CR/LF。任务据此冻结拒绝策略和静态跨实现 corpus,不把客户端自举测试误作发布端互操作证明。
|
||||||
|
- 2026-07-18:新增 `testdata/catalog/canonical-vectors.json` v1 和客户端 corpus 测试。有效向量固定断言 canonical bytes、顶层签名和 RFC 8032 公开测试公钥验证结果,不调用 `canonicalJSON` 或测试期私钥生成预期;拒绝向量覆盖孤立/不匹配 surrogate、`-0`、Base64 CR/LF/space/tab 与 padding 缺失/额外。两种 key order/空白不同的 JSON 固定映射到同一 bytes/signature。
|
||||||
|
- 2026-07-18:在受限 JSON 解析器增加 surrogate 扫描,拒绝 Go decoder 原会替换的非法 pair;整数 token 只接受 `0`、正整数或负非零整数。Verifier 和 Parser 共享 Base64 decode→reencode 相等校验,因此非唯一文本不再被接受;嵌套 `signature` 仍在 signed payload。
|
||||||
|
- 2026-07-18:验证通过:`GOWORK=off go -C core vet ./catalog`、`GOWORK=off go -C core test -count=10 ./catalog`、`./scripts/verify_phase0.ps1`、`python scripts/validate_agent_context.py`、`python scripts/validate_harness_governance.py` 与 `git diff --check`;完整验证同时覆盖 Go 1.20.14 core、现代版与 Win7 版构建/测试。外部 `softbox-catalog` 的 corpus 消费和 CI 证据不在本仓库,已作为跨仓库待协调项如实保留,无当前代码 blocker。
|
||||||
|
|||||||
Vendored
+8
@@ -7,3 +7,11 @@
|
|||||||
- 恶意样例用于证明解析器和安全边界会拒绝输入,不得被发布流程消费。
|
- 恶意样例用于证明解析器和安全边界会拒绝输入,不得被发布流程消费。
|
||||||
- `catalog/manifest-valid-payload.json` 同时作为 manifest v1 强类型解析与目标过滤的公开虚构样例;包哈希与签名只保证格式合法,不对应真实下载物。
|
- `catalog/manifest-valid-payload.json` 同时作为 manifest v1 强类型解析与目标过滤的公开虚构样例;包哈希与签名只保证格式合法,不对应真实下载物。
|
||||||
- `download/`:T-301 在运行时生成 HTTPS Range、断连、并发和恢复样例,不保存真实下载包。
|
- `download/`:T-301 在运行时生成 HTTPS Range、断连、并发和恢复样例,不保存真实下载包。
|
||||||
|
|
||||||
|
## Catalog canonicalization vectors
|
||||||
|
|
||||||
|
`catalog/canonical-vectors.json` is the versioned, cross-implementation signing corpus for the Catalog signature domain. It contains only a public RFC 8032 test key and fixed synthetic documents, canonical signing bytes and Ed25519 signatures; it never contains a production private key, real Catalog URL or release signature.
|
||||||
|
|
||||||
|
Each `document` is the raw UTF-8 JSON text passed to the verifier. A successful vector provides the exact `signed_payload_base64` and top-level `signature`; a rejecting vector provides `want_error` (`invalid_document`, `unsupported_number` or `signature_invalid`). Both the client and the external `softbox-catalog` publisher must consume these static values verbatim. They must not recreate expected bytes or signatures by calling their own canonicalizer or signing helper.
|
||||||
|
|
||||||
|
The corpus freezes Unicode key sorting/escaping, U+2028/U+2029, valid and invalid surrogate behavior, `-0` and large integers, nested `signature`, canonical padded Base64 and semantic-equivalent JSON representations. Extending it is a protocol change: increment `schema_version` when its interpretation changes and update `docs/api.md` together with client and publisher tests.
|
||||||
|
|||||||
+69
@@ -0,0 +1,69 @@
|
|||||||
|
{
|
||||||
|
"schema_version": 1,
|
||||||
|
"public_key_base64": "11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo=",
|
||||||
|
"vectors": [
|
||||||
|
{
|
||||||
|
"name": "unicode-escapes-sorted-keys-and-nested-signature",
|
||||||
|
"document": "{\"z\":\"\\u003c\\u003e\\u0026\\u2028\\u2029\",\"nested\":{\"signature\":\"inner\",\"n\":9007199254740993},\"emoji\":\"\\uD83D\\uDE00\",\"\\u00e9\":\"\\u00e9\",\"a\":\"line\\u000aquote\\u0022slash\\u005c\",\"signature\":\"JS8or6xTEfGtGOoxPbephpnYbB15fcRL1VnEcoQ0IZIHDx6URuRs3SciDIubEqxVCqy+GVOuyD2lRAOLm11OBQ==\"}",
|
||||||
|
"signed_payload_base64": "eyJhIjoibGluZVxucXVvdGVcInNsYXNoXFwiLCJlbW9qaSI6IvCfmIAiLCJuZXN0ZWQiOnsibiI6OTAwNzE5OTI1NDc0MDk5Mywic2lnbmF0dXJlIjoiaW5uZXIifSwieiI6Ilx1MDAzY1x1MDAzZVx1MDAyNlx1MjAyOFx1MjAyOSIsIsOpIjoiw6kifQ==",
|
||||||
|
"signature": "JS8or6xTEfGtGOoxPbephpnYbB15fcRL1VnEcoQ0IZIHDx6URuRs3SciDIubEqxVCqy+GVOuyD2lRAOLm11OBQ=="
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "equivalent-object-order-a",
|
||||||
|
"document": "{\"b\":[true,null,\"x\"],\"signature\":\"ozK/qFxWS+WX2Ic//QJMP87iDQqm1jXNv+1BE45/eddfx92zcq20x74Xo8VjO6zAOirKTj5OilL5JL59AdULCg==\",\"a\":0}",
|
||||||
|
"signed_payload_base64": "eyJhIjowLCJiIjpbdHJ1ZSxudWxsLCJ4Il19",
|
||||||
|
"signature": "ozK/qFxWS+WX2Ic//QJMP87iDQqm1jXNv+1BE45/eddfx92zcq20x74Xo8VjO6zAOirKTj5OilL5JL59AdULCg=="
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "equivalent-object-order-b",
|
||||||
|
"document": " { \"a\" : 0 , \"b\" : [ true , null , \"x\" ] , \"signature\" : \"ozK/qFxWS+WX2Ic//QJMP87iDQqm1jXNv+1BE45/eddfx92zcq20x74Xo8VjO6zAOirKTj5OilL5JL59AdULCg==\" } ",
|
||||||
|
"signed_payload_base64": "eyJhIjowLCJiIjpbdHJ1ZSxudWxsLCJ4Il19",
|
||||||
|
"signature": "ozK/qFxWS+WX2Ic//QJMP87iDQqm1jXNv+1BE45/eddfx92zcq20x74Xo8VjO6zAOirKTj5OilL5JL59AdULCg=="
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "reject-isolated-high-surrogate",
|
||||||
|
"document": "{\"value\":\"\\uD800\",\"signature\":\"ozK/qFxWS+WX2Ic//QJMP87iDQqm1jXNv+1BE45/eddfx92zcq20x74Xo8VjO6zAOirKTj5OilL5JL59AdULCg==\"}",
|
||||||
|
"want_error": "invalid_document"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "reject-isolated-low-surrogate",
|
||||||
|
"document": "{\"value\":\"\\uDC00\",\"signature\":\"ozK/qFxWS+WX2Ic//QJMP87iDQqm1jXNv+1BE45/eddfx92zcq20x74Xo8VjO6zAOirKTj5OilL5JL59AdULCg==\"}",
|
||||||
|
"want_error": "invalid_document"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "reject-mismatched-surrogate-pair",
|
||||||
|
"document": "{\"value\":\"\\uD800\\u0041\",\"signature\":\"ozK/qFxWS+WX2Ic//QJMP87iDQqm1jXNv+1BE45/eddfx92zcq20x74Xo8VjO6zAOirKTj5OilL5JL59AdULCg==\"}",
|
||||||
|
"want_error": "invalid_document"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "reject-negative-zero",
|
||||||
|
"document": "{\"n\":-0,\"signature\":\"ozK/qFxWS+WX2Ic//QJMP87iDQqm1jXNv+1BE45/eddfx92zcq20x74Xo8VjO6zAOirKTj5OilL5JL59AdULCg==\"}",
|
||||||
|
"want_error": "unsupported_number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "reject-signature-crlf",
|
||||||
|
"document": "{\"a\":0,\"b\":[true,null,\"x\"],\"signature\":\"ozK/qFxWS+WX2Ic//QJMP87iDQqm1jXNv+1BE45/eddfx92zcq20x74Xo8VjO6zAOirKTj5OilL5JL59\\r\\nAdULCg==\"}",
|
||||||
|
"want_error": "signature_invalid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "reject-signature-space",
|
||||||
|
"document": "{\"a\":0,\"b\":[true,null,\"x\"],\"signature\":\"ozK/qFxWS+WX2Ic//QJMP87iDQqm1jXNv+1BE45/eddfx92zcq20x74Xo8VjO6zAOirKTj5OilL5JL59AdULCg== \"}",
|
||||||
|
"want_error": "signature_invalid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "reject-signature-tab",
|
||||||
|
"document": "{\"a\":0,\"b\":[true,null,\"x\"],\"signature\":\"ozK/qFxWS+WX2Ic//QJMP87iDQqm1jXNv+1BE45/eddfx92zcq20x74Xo8VjO6zAOirKTj5OilL5JL59AdULCg==\\t\"}",
|
||||||
|
"want_error": "signature_invalid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "reject-signature-missing-padding",
|
||||||
|
"document": "{\"a\":0,\"b\":[true,null,\"x\"],\"signature\":\"ozK/qFxWS+WX2Ic//QJMP87iDQqm1jXNv+1BE45/eddfx92zcq20x74Xo8VjO6zAOirKTj5OilL5JL59AdULCg\"}",
|
||||||
|
"want_error": "signature_invalid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "reject-signature-extra-padding",
|
||||||
|
"document": "{\"a\":0,\"b\":[true,null,\"x\"],\"signature\":\"ozK/qFxWS+WX2Ic//QJMP87iDQqm1jXNv+1BE45/eddfx92zcq20x74Xo8VjO6zAOirKTj5OilL5JL59AdULCg===\"}",
|
||||||
|
"want_error": "signature_invalid"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user