feat(macos): hy2 via a published SPKI pin for sing-box (0.0.74 blocker) - #668
Merged
Merged
Conversation
Tests only. macOS: a hysteria2 block carrying certificate-public-key-sha256 must be available and emit tls.certificate_public_key_sha256; a block with only the DER fingerprint stays unavailable. Control plane: the optional pin must be block-style standard base64 of 32 bytes on hysteria2 blocks only. Publisher: keeps the pin line verbatim. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HLfruZoE8SYQZsGmnciybx
sing-box 1.15.0-alpha.3 only pins tls.certificate_public_key_sha256 (base64
SHA-256 of the SubjectPublicKeyInfo); the catalog's hy2 fingerprint is the
leaf DER hash, so every hy2 node was unavailable on macOS.
A managed hysteria2 block may now carry certificate-public-key-sha256,
computed by the operator on the node beside the mandatory DER fingerprint.
macOS reads it, keeps only canonical base64 of 32 bytes, and emits
tls {enabled, server_name, certificate_public_key_sha256: [pin]}; never
insecure. A block without a valid pin stays unavailable with the existing
reason. Selection, the backup-channel offer and route choices already
filter on singBoxUnavailableReason, so pinned hy2 reaches them unchanged;
TCP failure still never auto-switches onto hy2.
The control plane admits the key on hysteria2 blocks only, once, as a
plain block-style line with a canonical 32-byte value; VLESS blocks must
not carry it. Windows is unchanged: RawProxy ignores unknown keys and the
mihomo runtime mapping emits only known fields.
Owner decision 2026-09-26 (DECISIONS.md); contract, ledger H21-O-F4 and
changelog.d updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HLfruZoE8SYQZsGmnciybx
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Tests only. A pin with a trailing comment and a pin nested under another key both pass admission on 4e5e65f, but macOS keeps the comment in the value and never reads a nested key, so the node silently stays unavailable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HLfruZoE8SYQZsGmnciybx
Review ce2768d6 (opus:F1, codex:F1). catalogScalar stripped a trailing comment and the key line was matched at any indentation, so a commented or nested pin passed admission while macOS kept the comment in the value or never read the nested key, silently leaving the node unavailable. Admit the pin only as a direct field of the list item (the column of the fields under `- name:`), plain or simply quoted, with nothing after it and no deeper continuation line. Everything else is INVALID_CATALOG. Suggestion opus:F2: manage-tono-hy2-node.sh apply reports certificatePublicKeySha256 beside the DER fingerprint for every new certificate, from the same openssl pipeline as the contract. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HLfruZoE8SYQZsGmnciybx
…ISIONS entries) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HLfruZoE8SYQZsGmnciybx
Contributor
|
Owner
Author
|
jev-route review: dual_cross_family (opus + codex). ce2768d6 on 4e5e65f: PASSED, 2 minors (control plane accepted pin forms macOS cannot parse) + 1 suggestion (node script should print the SPKI pin) → fixed in f1f3fba (red a902b3a failed by assertion, Services CI 36262764273). 15213203 (--pr 668 at f1f3fba): PASSED, 0 findings. 71a8562 merges origin/main (DECISIONS.md conflict only; all entries kept). CI green on exact head 71a8562 (macOS CI 36263726887, Services CI 36263728344); no Windows paths. The published production catalog r62 already carries the pins in the accepted form. No unresolved threads. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Owns: SHIP_PLAN G2 (连不上有下一手). Release 0.0.74 blocker. Owner decision 2026-09-26: hy2 must work for every user in this release, macOS included; fix the pin now (DECISIONS).
Problem
macOS 0.0.74 runs sing-box 1.15.0-alpha.3, which pins only
tls.certificate_public_key_sha256(base64 SHA-256 of the SubjectPublicKeyInfo). The catalog's hy2fingerprintis the SHA-256 of the leaf DER (mihomo semantics), and a client cannot derive one from the other. Result: every hy2 node wasTONO_SINGBOX_HY2_DER_PIN_UNSUPPORTEDon macOS, so the backup channel did not exist there.Change
certificate-public-key-sha256on hysteria2 blocks: standard base64 (with=padding) of the 32-byte SHA-256 of the leaf SubjectPublicKeyInfo DER. The operator computes it on the node from the same certificate asfingerprint, which stays mandatory. Operator command (use the file the hy2 config'stls.certpoints at; script-installed nodes use/opt/tono-hy2/current/cert.pem):manage-tono-hy2-node.sh, this equals Gox509.MarshalPKIXPublicKey+ SHA-256 +base64.StdEncoding, which is what sing-boxVerifyPublicKeySHA256compares (pinnedcommon/tls/std_client.goat93fff595).ConfigParserreads the key intoProxyNode.certificatePublicKeySHA256.validatedOwnedHysteria2keeps it only as canonical base64 of 32 bytes (else nil). The DER fingerprint is still required.singBoxUnavailableReasonmakes hy2 available only with a valid pin. The builder emitstls: {enabled: true, server_name: <sni>, certificate_public_key_sha256: [<pin>]}, neverinsecure. Selection, the "试用备用通道" offer and route choices already filter onsingBoxUnavailableReason, so a pinned hy2 node reaches them with no change there.isCityFailoverCandidateis unchanged: a TCP failure still never auto-switches onto hy2. The pin is part ofliveSessionIdentityandCodingKeys. The mihomo YAML emitter does not emit it.catalog-yaml.ts: hysteria2 blocks may carry the key once, as a plain block-style line, with a canonical 32-byte value. Anything else failscatalogProxyUsesManagedIdentity→INVALID_CATALOG(the same path as other malformed managed blocks). VLESS blocks must not carry it. Flow style is refused because macOS parsers from before this change merge an unknown flow key into the previous value and would refuse the whole catalog.publish-managed-catalog.rb: no change needed (no key allowlist). A new test shows it keeps the line verbatim.tono-coreRawProxyhas nodeny_unknown_fieldsand says unknown fields are dropped (node.rs:197-235).admit_hysteria2still requires the DERfingerprint(node.rs:384-440). The mihomo runtime is built only fromValidatedNode::to_runtime_mapping(config.rs:952), which emits name/server/port/type/password/sni/fingerprint for hy2 (node.rs:119-139), so mihomo never sees the new key.--runtime-contract-checkforbidsinsecureand file paths, notcertificate_public_key_sha256(tooling/scripts/core-helper/main.swift:314-324). CI runs it andsing-box checkon the emitted bytes, which now include a pinned hy2 outbound.changelog.d/2026-09-26-macos-hy2-spki-pin.md.Why an SPKI pin is equivalent here
hy2 nodes serve operator-generated self-signed certificates. The private key is created on the node and never leaves it, and the DER pin never relied on a CA, name or validity period. So a key pin gives the same MITM protection. It also accepts a reissued certificate over the same key, which only the key holder can produce. sing-box sets
InsecureSkipVerifyonly together with the SPKIVerifyPeerCertificate, and hysteria2 uses the default Go engine.Tests (red first)
wip/macos-hy2-spki-pin-red@277bb163(tests only):SingBoxConfigTests.swift:228 XCTAssertNil failed: "TONO_SINGBOX_HY2_DER_PIN_UNSUPPORTED";:266 XCTAssertEqual failed(the pinnedFixture Beta · hy2was also unavailable).expected INVALID_CATALOGatcatalog-yaml.test.ts:135, a 31-byte pin was admitted).testHY2WithPublishedSPKIPinGetsPinnedSingBoxOutbound, XCTesttestProductRuntimePreservesHomeDirectAndRejectsHY2WithoutSPKIPin(renamed, pinned sibling added), vitestadmits an optional block-style SPKI pin of exactly 32 bytes on hysteria2 blocks only, Rubytest_publish_keeps_the_hy2_spki_pin_beside_the_der_fingerprint(a guard; it already passes on main).vitest run43 files / 932 passed;tsc --noEmitclean; the Ruby test passes. macOS was not built locally (hosted CI only).Not in this PR
certificate-public-key-sha256to each hy2 block, macOS still has no usable hy2.HY2_CATALOG_EMAILS. When it is set, only listed accounts receive hy2 blocks (catalog.tsis unchanged).🤖 Generated with Claude Code
https://claude.ai/code/session_01HLfruZoE8SYQZsGmnciybx