diff --git a/AGENTS.md b/AGENTS.md index 327686e..a99f2e9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -141,3 +141,10 @@ nearby compiler output when diagnosing a failure. - 轨道与授权:轨道 4 社区引流;授权形态:AGPL-3.0;商业归属:永久免费(安全工具开源即信任;家族发布基础设施)。 - 组合归属唯一权威:[PORTFOLIO.md](https://github.com/turinglambdaai/turinglambdaai.github.io/blob/master/PORTFOLIO.md)(turinglambdaai.github.io 仓库)。改归属/授权先改权威档,再同步本节。 - 纪律:价格只进本仓 `PRICING.md`,且收费开始时才发布;README 与产品站不做价格预告。 + +## 产品交付与下一步(2026-10-23) + +- 家族[交付标准](https://github.com/turinglambdaai/turinglambdaai.github.io/blob/master/docs/PRODUCT-STANDARD.md)与[组合策略](https://github.com/turinglambdaai/turinglambdaai.github.io/blob/master/PORTFOLIO.md)是共用政策;本仓具体行为以 `README.md` 为准,不复制五库依赖图。 +- 当前执行入口:[产品审查 issue](https://github.com/turinglambdaai/keyward/issues/3)。先保证每个私钥都有独立解析/配对结果,不能用一个正确 key 掩盖同节其他坏 key。维持明文可逃生格式和引擎零网络;用合成临时密钥验证。 +- 开工先检查干净工作树与远端,以 fast-forward-only 更新并记录 SHA;共享文件一次一个写入者,保留其他 checkout 的未提交工作。 +- 每次只完成一个可验收切片,记录 owned files、依赖 pin、命令/平台/产物证据和回滚。已实现、已发布、实机验收分别陈述;不修改 requirements 或复用旧证据来制造通过。 diff --git a/CHANGELOG.md b/CHANGELOG.md index ff18308..4add9a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to Keyward are documented here. Format follows ## [Unreleased] +### Fixed + +- Deep check now evaluates every private entry and all public anchors within its purpose. A valid update key no longer hides a corrupt or mismatched license key; multiple safe fingerprints remain labelled within the existing RPC row layout. Empty private entries are reported as unreadable. Regression coverage uses generated temporary keys only. + + ### Fixed - The summary-tile count could wrap under width pressure: "Products 15" folded into a vertical "1/5" that read as two mystery numbers. Counts diff --git a/README.md b/README.md index 3d2f213..55039f9 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,9 @@ your own synced folder and adds the discipline a plaintext store lacks: - **Deep check.** Parses each key (Ed25519 / RSA PKCS#8, base64-DER, base64-PEM or bare PEM), derives the public half, computes the fingerprint, and pairs it against the stored public material — the audit that used to be - a pile of hand-run openssl commands. + a pile of hand-run openssl commands. Every private entry is checked against + all public anchors of the same purpose; a section passes only when every + private key passes, with each fingerprint labelled in multi-key sections. - **Collect, don't scatter.** In-app keypair generation writes straight into the vault; PEM imports land as single-line base64 values. No stray key files, even momentarily. @@ -80,3 +82,11 @@ raco rivet package # distributable ## License AGPL-3.0 + +## Delivery priorities (2026-10-23) + +The current product review issue linked from AGENTS owns the next bounded slice and its acceptance scenarios. Follow the existing product specification and the [family delivery standard](https://github.com/turinglambdaai/turinglambdaai.github.io/blob/master/docs/PRODUCT-STANDARD.md). Hardware, native-platform and live-payment gates remain explicit; this documentation update does not claim they passed. + +## Feedback and diagnostics + +Report reproducible bugs and feature requests through [GitHub Issues](https://github.com/turinglambdaai/keyward/issues). Include the release, OS/architecture and steps; inspect and redact diagnostics before sharing. Do not post API keys, license files, vault contents or private documents. For sensitive security reports, follow SECURITY.md when present; otherwise request a private contact route without disclosing sensitive details. Nothing is uploaded automatically. diff --git a/README.zh-CN.md b/README.zh-CN.md index 1d7183b..c2e8c11 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -70,3 +70,9 @@ raco rivet package # 分发产物 ## License AGPL-3.0 + +## 反馈与诊断 + +问题和建议请使用 [GitHub Issues](https://github.com/turinglambdaai/keyward/issues),附版本、系统/架构和复现步骤。分享前先检查并脱敏诊断;不要公开 API key、授权凭据、密钥库或私有文档。安全敏感问题优先按 SECURITY.md 联系;未提供私密渠道时,先请求私下联系方式,不在公开 issue 中披露敏感细节。此入口不会自动上传数据。 + +深度校验按用途逐个检查所有私钥与对应公钥材料;同节的更新签名钥不会掩盖授权签名钥的问题。多钥节只有全部通过才显示通过,摘要只包含名称、指纹和状态,不包含密钥值。 diff --git a/app/core/keymeta.rkt b/app/core/keymeta.rkt index a9bf065..cf96d76 100644 --- a/app/core/keymeta.rkt +++ b/app/core/keymeta.rkt @@ -12,12 +12,14 @@ (only-in crypto/libcrypto libcrypto-factory) net/base64 racket/format + racket/list racket/match racket/string) (crypto-factories (list libcrypto-factory)) -(provide key-summary +(provide key-summaries + key-summary value->der private-key-from-der public-key-of @@ -93,65 +95,89 @@ (spki-der (public-key-of priv)))) ;; ---- summaries ---------------------------------------------------------------- -;; entries: (listof (cons NAME value)) — analyze the first readable private -;; key and report whether any stored public entry matches it. +;; Analyze every private entry, including empty/malformed values. Public anchors +;; are selected by the same purpose prefix (UPDATE_ED25519_, LICENSE_, ...). +;; A valid key from another trust domain must never make a section look paired. (define private-name-rx #px"PRIVATE") -(define known-private-names - '("UPDATE_ED25519_PRIVATE_KEY_B64" - "UPDATE_ED25519_PRIVATE_KEY" - "UPDATER_PRIVATE_KEY" - "LICENSE_PRIVATE_KEY")) +(define (summary kind fingerprint paired status) + (list (cons 'kind kind) (cons 'fingerprint fingerprint) + (cons 'paired paired) (cons 'status status))) + +(define (summary-ref value key) + (cdr (assq key value))) + +(define (private-entry-summary priv-entry entries) + (define der (value->der (cdr priv-entry))) + (define key (and der (private-key-from-der der))) + (cond + [(not key) (summary "?" "-" "-" "key-unreadable")] + [else + (define kind (or (kind-of key) "unknown")) + (define spki (spki-der (public-key-of key))) + (define fp (fingerprint-of-der spki)) + (define raw32 (and (string=? kind "ed25519") (= (bytes-length spki) 44) + (subbytes spki 12))) + (define purpose (cadr (regexp-match #px"^(.*?)PRIVATE" (car priv-entry)))) + (define pub-entries + (for/list ([e (in-list entries)] + #:when (and (string-prefix? (car e) purpose) + (string-prefix? (substring (car e) (string-length purpose)) + "PUBLIC") + (not (string=? (cdr e) "")))) + e)) + (define (matches? entry) + (define name (car entry)) + (define value (cdr entry)) + (cond + [(regexp-match? #px"_FPR$" name) + (string=? (string-downcase (string-trim value)) fp)] + [else + (define stored (value->bytes value)) + (and stored + (or (equal? stored spki) + (and raw32 (= (bytes-length stored) 32) + (equal? stored raw32))))])) + ;; Every declared anchor must agree, not merely one of several forms. + (define paired (cond [(null? pub-entries) "missing-public"] + [(andmap matches? pub-entries) "yes"] + [else "no"])) + (summary kind fp paired + (cond [(string=? paired "yes") "ok"] + [(string=? paired "missing-public") "missing-public"] + [else "pair-mismatch"]))])) + +;; Safe per-entry results for diagnostics; values never leave this module. +(define (key-summaries entries) + (for/list ([e (in-list entries)] + #:when (regexp-match? private-name-rx (car e))) + (cons (car e) (private-entry-summary e entries)))) + +;; Preserve the product's existing row layout and status vocabulary. A section +;; is green only if every private key and each of its public anchors is green. +;; Multiple fingerprints are labelled by entry name instead of showing only the +;; first one; native hosts can display the result without an RPC/schema change. (define (key-summary entries) - (define priv-entry - (or (for/first ([e (in-list entries)] - #:when (and (member (car e) known-private-names) - (not (string=? (cdr e) "")))) - e) - (for/first ([e (in-list entries)] - #:when (and (regexp-match? private-name-rx (car e)) - (not (string=? (cdr e) "")))) - e))) + (define per-key (key-summaries entries)) (cond - [(not priv-entry) (list (cons 'kind "-") (cons 'fingerprint "-") - (cons 'paired "-") (cons 'status "no-key"))] + [(null? per-key) (summary "-" "-" "-" "no-key")] + [(null? (cdr per-key)) (cdar per-key)] [else - (define der (value->der (cdr priv-entry))) - (define key (and der (private-key-from-der der))) - (cond - [(not key) (list (cons 'kind "?") (cons 'fingerprint "-") - (cons 'paired "-") (cons 'status "key-unreadable"))] - [else - (define kind (or (kind-of key) "unknown")) - (define spki (spki-der (public-key-of key))) - (define fp (fingerprint-of-der spki)) - (define raw32 (and (string=? kind "ed25519") (= (bytes-length spki) 44) - (subbytes spki 12))) - ;; public material may be stored two ways: as decodable key bytes - ;; (PUBLIC_KEY entries) or as a bare fingerprint (…_FPR entries) - (define pub-entries - (for/list ([e (in-list entries)] - #:when (and (regexp-match? #px"PUBLIC" (car e)) - (not (string=? (cdr e) "")))) - e)) - (define (matches? name value) - (cond [(regexp-match? #px"_FPR$" name) - (and (string? value) - (string=? (string-downcase (string-trim value)) fp))] - [else - (define der* (value->bytes value)) - (and der* - (or (equal? der* spki) - (and raw32 (= (bytes-length der*) 32) (equal? der* raw32))))])) - (define paired (cond [(null? pub-entries) "missing-public"] - [(ormap (lambda (e) (matches? (car e) (cdr e))) pub-entries) "yes"] - [else "no"])) - (define status - (cond [(string=? paired "yes") "ok"] - [(string=? paired "missing-public") "missing-public"] - [else "pair-mismatch"])) - (list (cons 'kind kind) - (cons 'fingerprint fp) - (cons 'paired paired) - (cons 'status status))])])) + (define states (map (lambda (entry) (summary-ref (cdr entry) 'status)) per-key)) + (define status + (or (for/first ([failure (in-list '("key-unreadable" "pair-mismatch" "missing-public"))] + #:when (member failure states)) + failure) + "ok")) + (summary + (string-join (remove-duplicates + (map (lambda (entry) (summary-ref (cdr entry) 'kind)) per-key)) ", ") + (string-join + (map (lambda (entry) + (format "~a=~a" (car entry) (summary-ref (cdr entry) 'fingerprint))) per-key) + "; ") + (cond [(string=? status "ok") "yes"] + [(string=? status "missing-public") "missing-public"] + [else "no"]) + status)])) diff --git a/site/index.html b/site/index.html index 3174551..ea69ef8 100644 --- a/site/index.html +++ b/site/index.html @@ -25,6 +25,11 @@ s.parentNode.insertBefore(hm, s); })(); +