Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 或复用旧证据来制造通过。
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
6 changes: 6 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,9 @@ raco rivet package # 分发产物
## License

AGPL-3.0

## 反馈与诊断

问题和建议请使用 [GitHub Issues](https://github.com/turinglambdaai/keyward/issues),附版本、系统/架构和复现步骤。分享前先检查并脱敏诊断;不要公开 API key、授权凭据、密钥库或私有文档。安全敏感问题优先按 SECURITY.md 联系;未提供私密渠道时,先请求私下联系方式,不在公开 issue 中披露敏感细节。此入口不会自动上传数据。

深度校验按用途逐个检查所有私钥与对应公钥材料;同节的更新签名钥不会掩盖授权签名钥的问题。多钥节只有全部通过才显示通过,摘要只包含名称、指纹和状态,不包含密钥值。
140 changes: 83 additions & 57 deletions app/core/keymeta.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)]))
6 changes: 6 additions & 0 deletions site/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@
s.parentNode.insertBefore(hm, s);
})();
</script>
<style>
#product-support { display:flex; flex-wrap:wrap; justify-content:center; gap:12px 24px; padding:24px; font:inherit; border-top:1px solid var(--border, #a8a29e); }
#product-support a:focus-visible { outline:2px solid currentColor; outline-offset:4px; }
html:lang(en) #product-support .support-zh, html:lang(zh) #product-support .support-en { display:none; }
</style>
</head>
<body>
<header class="nav">
Expand Down Expand Up @@ -193,6 +198,7 @@ <h2 data-i18n="roadmap-title">路线图</h2>
<span>AGPL-3.0 · <a href="https://github.com/turinglambdaai/keyward">github.com/turinglambdaai/keyward</a></span>
<span data-i18n="footer-note">引擎零网络代码;库文件永远在你自己的同步盘里。明文同步是信任设备间的设计决策,防失窃交给各机全盘加密。</span>
</footer>
<nav id="product-support" aria-label="Support / 支持"><a href="https://github.com/turinglambdaai/keyward/issues"><span class="support-zh">反馈</span><span class="support-en" lang="en">Feedback</span></a> <a href="https://github.com/turinglambdaai/keyward/releases"><span class="support-zh">发布与更新</span><span class="support-en" lang="en">Releases</span></a> <a href="https://github.com/turinglambdaai/keyward/blob/main/LICENSE"><span class="support-zh">源码许可</span><span class="support-en" lang="en">Source license</span></a></nav>

<script>
(function () {
Expand Down
44 changes: 44 additions & 0 deletions tests/keymeta-test.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
rackunit
racket/format
racket/list
racket/string
"../app/core/keymeta.rkt")

(define (b64 btes) (bytes->string/utf-8 (base64-encode btes "")))
Expand Down Expand Up @@ -94,3 +95,46 @@
(for ([e (in-list (key-summary (list (cons "UPDATE_ED25519_PRIVATE_KEY_B64" (b64 priv-der))
(cons "UPDATE_ED25519_PUBLIC_KEY_SPKI_B64" (b64 spki)))))])
(check-true (<= (string-length (cdr e)) 64)))

;; A valid update key must not hide a second, corrupt or mismatched license
;; key. Pairing anchors are scoped to the private entry's purpose.
(define update-pair
(list (cons "UPDATE_ED25519_PRIVATE_KEY_B64" (b64 priv-der))
(cons "UPDATE_ED25519_PUBLIC_KEY_SPKI_B64" (b64 spki))))
(define (summary-status entries)
(cdr (assoc 'status (key-summary entries))))
(check-equal? (summary-status
(append update-pair (list (cons "LICENSE_PRIVATE_KEY" "invalid"))))
"key-unreadable")
(check-equal? (summary-status
(append update-pair
(list (cons "LICENSE_PRIVATE_KEY" (b64 other-priv))
(cons "LICENSE_PUBLIC_KEY" (b64 spki)))))
"pair-mismatch")

(define two-pairs
(append update-pair
(list (cons "LICENSE_PRIVATE_KEY" (b64 other-priv))
(cons "LICENSE_PUBLIC_KEY_FPR" (fingerprint-of-der other-spki)))))
(check-equal? (summary-status two-pairs) "ok")
(check-equal? (summary-status (reverse two-pairs)) "ok")
(check-equal? (length (key-summaries two-pairs)) 2)
(check-equal? (summary-status
(append update-pair (list (cons "LICENSE_PRIVATE_KEY" ""))))
"key-unreadable")
(check-equal? (summary-status
(append update-pair
(list (cons "LICENSE_PRIVATE_KEY" (b64 other-priv)))))
"missing-public")
(define safe-summary (format "~s" (key-summaries two-pairs)))
(check-false (string-contains? safe-summary (b64 priv-der)))
(check-false (string-contains? safe-summary (b64 other-priv)))
(check-equal? (summary-status
(list (cons "LICENSE_PRIVATE_KEY" (b64 priv-der))
(cons "UPDATE_ED25519_PUBLIC_KEY_SPKI_B64" (b64 spki))))
"missing-public")
(check-equal? (summary-status
(append update-pair
(list (cons "UPDATE_ED25519_PUBLIC_KEY_RAW_TAIL32_B64"
(b64 (subbytes other-spki 12))))))
"pair-mismatch")
Loading