fix(dgram): align secret handling contracts
- commit
- 93af4178a88dcb4eef0078a04b1f19e90329d288
- author
- brice · · 2026-07-23 20:22 UTC
- parent
- f2ef998
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +29 -23
M internal/adapters/dgram/proxy_registry.go +6 -3
| @@ -14,13 +14,16 @@ import ( | |||
| 14 | 14 | ) | |
| 15 | 15 | ||
| 16 | 16 | type ProxyRecord struct { | |
| 17 | - | Session string `json:"session"` | |
| 18 | - | PID int `json:"pid"` | |
| 19 | - | Port int `json:"port"` | |
| 17 | + | Session string `json:"session"` | |
| 18 | + | PID int `json:"pid"` | |
| 19 | + | Port int `json:"port"` | |
| 20 | + | // KeyFingerprint is a non-secret ownership tag (SHA-256 of the AEAD key). | |
| 21 | + | // The raw key must never be persisted by vev. | |
| 20 | 22 | KeyFingerprint string `json:"key_fingerprint"` | |
| 21 | 23 | Created time.Time `json:"created"` | |
| 22 | 24 | } | |
| 23 | 25 | ||
| 26 | + | // KeyFingerprint derives the non-secret registry ownership tag. | |
| 24 | 27 | func KeyFingerprint(key []byte) string { | |
| 25 | 28 | sum := sha256.Sum256(key) | |
| 26 | 29 | return base64.StdEncoding.EncodeToString(sum[:]) | |
| @@ -14,13 +14,16 @@ import ( | |||||
| 14 | ) | 14 | ) | ||
| 15 | 15 | ||||
| 16 | type ProxyRecord struct { | 16 | type ProxyRecord struct { | ||
| 17 | - | Session string `json:"session"` | 17 | + | Session string `json:"session"` |
| 18 | - | PID int `json:"pid"` | 18 | + | PID int `json:"pid"` |
| 19 | - | Port int `json:"port"` | 19 | + | Port int `json:"port"` |
| 20 | + | // KeyFingerprint is a non-secret ownership tag (SHA-256 of the AEAD key). | |||
| 21 | + | // The raw key must never be persisted by vev. | |||
| 20 | KeyFingerprint string `json:"key_fingerprint"` | 22 | KeyFingerprint string `json:"key_fingerprint"` | ||
| 21 | Created time.Time `json:"created"` | 23 | Created time.Time `json:"created"` | ||
| 22 | } | 24 | } | ||
| 23 | 25 | ||||
| 26 | + | // KeyFingerprint derives the non-secret registry ownership tag. | |||
| 24 | func KeyFingerprint(key []byte) string { | 27 | func KeyFingerprint(key []byte) string { | ||
| 25 | sum := sha256.Sum256(key) | 28 | sum := sha256.Sum256(key) | ||
| 26 | return base64.StdEncoding.EncodeToString(sum[:]) | 29 | return base64.StdEncoding.EncodeToString(sum[:]) | ||
M internal/adapters/dgram/proxy_registry_test.go +12 -4
| @@ -2,6 +2,8 @@ package dgram | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "bytes" | |
| 5 | + | "crypto/sha256" | |
| 6 | + | "encoding/base64" | |
| 5 | 7 | "encoding/json" | |
| 6 | 8 | "os" | |
| 7 | 9 | "path/filepath" | |
| @@ -14,15 +16,21 @@ func privateDir(t *testing.T) string { | |||
| 14 | 16 | } | |
| 15 | 17 | ||
| 16 | 18 | func TestKeyFingerprint(t *testing.T) { | |
| 17 | - | const want = "K7gNU3sdo+OL0wNhqoVWhr3g6s1xYv72ol/pe/Unols=" | |
| 18 | - | if got := KeyFingerprint([]byte("secret")); got != want { | |
| 19 | + | key := bytes.Repeat([]byte{0xA5}, 32) | |
| 20 | + | sum := sha256.Sum256(key) | |
| 21 | + | want := base64.StdEncoding.EncodeToString(sum[:]) | |
| 22 | + | if got := KeyFingerprint(key); got != want { | |
| 19 | 23 | t.Fatalf("KeyFingerprint()=%q, want %q", got, want) | |
| 20 | 24 | } | |
| 25 | + | other := bytes.Repeat([]byte{0xA6}, 32) | |
| 26 | + | if KeyFingerprint(key) == KeyFingerprint(other) { | |
| 27 | + | t.Fatal("distinct keys produced identical fingerprints") | |
| 28 | + | } | |
| 21 | 29 | } | |
| 22 | 30 | ||
| 23 | 31 | func TestProxyRegistryFileHoldsNoKeyMaterial(t *testing.T) { | |
| 24 | 32 | r := NewProxyRegistry(privateDir(t)) | |
| 25 | - | key := []byte("raw-secret-key-material") | |
| 33 | + | key := bytes.Repeat([]byte{0xA5}, 32) | |
| 26 | 34 | fingerprint := KeyFingerprint(key) | |
| 27 | 35 | rec := ProxyRecord{Session: "work", PID: os.Getpid(), Port: 61000, KeyFingerprint: fingerprint} | |
| 28 | 36 | if err := r.Publish(rec); err != nil { | |
| @@ -33,7 +41,7 @@ func TestProxyRegistryFileHoldsNoKeyMaterial(t *testing.T) { | |||
| 33 | 41 | if err != nil { | |
| 34 | 42 | t.Fatal(err) | |
| 35 | 43 | } | |
| 36 | - | if bytes.Contains(data, key) { | |
| 44 | + | if bytes.Contains(data, key) || bytes.Contains(data, []byte(base64.StdEncoding.EncodeToString(key))) { | |
| 37 | 45 | t.Fatalf("registry file contains raw key material: %s", data) | |
| 38 | 46 | } | |
| 39 | 47 | var fields map[string]json.RawMessage | |
| @@ -2,6 +2,8 @@ package dgram | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "bytes" | 4 | "bytes" | ||
| 5 | + | "crypto/sha256" | |||
| 6 | + | "encoding/base64" | |||
| 5 | "encoding/json" | 7 | "encoding/json" | ||
| 6 | "os" | 8 | "os" | ||
| 7 | "path/filepath" | 9 | "path/filepath" | ||
| @@ -14,15 +16,21 @@ func privateDir(t *testing.T) string { | |||||
| 14 | } | 16 | } | ||
| 15 | 17 | ||||
| 16 | func TestKeyFingerprint(t *testing.T) { | 18 | func TestKeyFingerprint(t *testing.T) { | ||
| 17 | - | const want = "K7gNU3sdo+OL0wNhqoVWhr3g6s1xYv72ol/pe/Unols=" | 19 | + | key := bytes.Repeat([]byte{0xA5}, 32) |
| 18 | - | if got := KeyFingerprint([]byte("secret")); got != want { | 20 | + | sum := sha256.Sum256(key) |
| 21 | + | want := base64.StdEncoding.EncodeToString(sum[:]) | |||
| 22 | + | if got := KeyFingerprint(key); got != want { | |||
| 19 | t.Fatalf("KeyFingerprint()=%q, want %q", got, want) | 23 | t.Fatalf("KeyFingerprint()=%q, want %q", got, want) | ||
| 20 | } | 24 | } | ||
| 25 | + | other := bytes.Repeat([]byte{0xA6}, 32) | |||
| 26 | + | if KeyFingerprint(key) == KeyFingerprint(other) { | |||
| 27 | + | t.Fatal("distinct keys produced identical fingerprints") | |||
| 28 | + | } | |||
| 21 | } | 29 | } | ||
| 22 | 30 | ||||
| 23 | func TestProxyRegistryFileHoldsNoKeyMaterial(t *testing.T) { | 31 | func TestProxyRegistryFileHoldsNoKeyMaterial(t *testing.T) { | ||
| 24 | r := NewProxyRegistry(privateDir(t)) | 32 | r := NewProxyRegistry(privateDir(t)) | ||
| 25 | - | key := []byte("raw-secret-key-material") | 33 | + | key := bytes.Repeat([]byte{0xA5}, 32) |
| 26 | fingerprint := KeyFingerprint(key) | 34 | fingerprint := KeyFingerprint(key) | ||
| 27 | rec := ProxyRecord{Session: "work", PID: os.Getpid(), Port: 61000, KeyFingerprint: fingerprint} | 35 | rec := ProxyRecord{Session: "work", PID: os.Getpid(), Port: 61000, KeyFingerprint: fingerprint} | ||
| 28 | if err := r.Publish(rec); err != nil { | 36 | if err := r.Publish(rec); err != nil { | ||
| @@ -33,7 +41,7 @@ func TestProxyRegistryFileHoldsNoKeyMaterial(t *testing.T) { | |||||
| 33 | if err != nil { | 41 | if err != nil { | ||
| 34 | t.Fatal(err) | 42 | t.Fatal(err) | ||
| 35 | } | 43 | } | ||
| 36 | - | if bytes.Contains(data, key) { | 44 | + | if bytes.Contains(data, key) || bytes.Contains(data, []byte(base64.StdEncoding.EncodeToString(key))) { |
| 37 | t.Fatalf("registry file contains raw key material: %s", data) | 45 | t.Fatalf("registry file contains raw key material: %s", data) | ||
| 38 | } | 46 | } | ||
| 39 | var fields map[string]json.RawMessage | 47 | var fields map[string]json.RawMessage | ||
M pkg/dgram/secret_disabled.go +4 -8
| @@ -1,11 +1,7 @@ | |||
| 1 | - | //go:build !goexperiment.runtimesecret || !linux || (!amd64 && !arm64) | |
| 1 | + | //go:build !goexperiment.runtimesecret | |
| 2 | 2 | ||
| 3 | 3 | package dgram | |
| 4 | 4 | ||
| 5 | - | // SecretDo runs f synchronously. Without Go 1.26's experimental runtime secret | |
| 6 | - | // support on linux/amd64 or linux/arm64, it invokes f directly. Runtime secret | |
| 7 | - | // protection, when available, does not extend to global variables or goroutines | |
| 8 | - | // started by f. | |
| 9 | - | func SecretDo(f func()) { | |
| 10 | - | f() | |
| 11 | - | } | |
| 5 | + | // SecretDo invokes f directly when runtime/secret is not in this build. | |
| 6 | + | // Callers still explicitly clear every reachable key buffer with Erase. | |
| 7 | + | func SecretDo(f func()) { f() } | |
| @@ -1,11 +1,7 @@ | |||||
| 1 | - | //go:build !goexperiment.runtimesecret || !linux || (!amd64 && !arm64) | 1 | + | //go:build !goexperiment.runtimesecret |
| 2 | 2 | ||||
| 3 | package dgram | 3 | package dgram | ||
| 4 | 4 | ||||
| 5 | - | // SecretDo runs f synchronously. Without Go 1.26's experimental runtime secret | 5 | + | // SecretDo invokes f directly when runtime/secret is not in this build. |
| 6 | - | // support on linux/amd64 or linux/arm64, it invokes f directly. Runtime secret | 6 | + | // Callers still explicitly clear every reachable key buffer with Erase. |
| 7 | - | // protection, when available, does not extend to global variables or goroutines | 7 | + | func SecretDo(f func()) { f() } |
| 8 | - | // started by f. | |||
| 9 | - | func SecretDo(f func()) { | |||
| 10 | - | f() | |||
| 11 | - | } | |||
M pkg/dgram/secret_enabled.go +7 -8
| @@ -1,13 +1,12 @@ | |||
| 1 | - | //go:build goexperiment.runtimesecret && linux && (amd64 || arm64) | |
| 1 | + | //go:build goexperiment.runtimesecret | |
| 2 | 2 | ||
| 3 | 3 | package dgram | |
| 4 | 4 | ||
| 5 | 5 | import "runtime/secret" | |
| 6 | 6 | ||
| 7 | - | // SecretDo runs f synchronously. On supported platforms with Go 1.26's | |
| 8 | - | // experimental runtime secret support enabled, it protects temporary storage | |
| 9 | - | // used by f's full call tree. Protection does not extend to global variables or | |
| 10 | - | // goroutines started by f. | |
| 11 | - | func SecretDo(f func()) { | |
| 12 | - | secret.Do(f) | |
| 13 | - | } | |
| 7 | + | // SecretDo invokes f through runtime/secret.Do. On supported Go 1.26 | |
| 8 | + | // platforms this protects temporary stack/register storage for f's call tree | |
| 9 | + | // and marks its allocations for clearing after they become unreachable and | |
| 10 | + | // the GC notices. On unsupported platforms runtime/secret.Do invokes f | |
| 11 | + | // directly. It does not protect globals or goroutines started by f. | |
| 12 | + | func SecretDo(f func()) { secret.Do(f) } | |
| @@ -1,13 +1,12 @@ | |||||
| 1 | - | //go:build goexperiment.runtimesecret && linux && (amd64 || arm64) | 1 | + | //go:build goexperiment.runtimesecret |
| 2 | 2 | ||||
| 3 | package dgram | 3 | package dgram | ||
| 4 | 4 | ||||
| 5 | import "runtime/secret" | 5 | import "runtime/secret" | ||
| 6 | 6 | ||||
| 7 | - | // SecretDo runs f synchronously. On supported platforms with Go 1.26's | 7 | + | // SecretDo invokes f through runtime/secret.Do. On supported Go 1.26 |
| 8 | - | // experimental runtime secret support enabled, it protects temporary storage | 8 | + | // platforms this protects temporary stack/register storage for f's call tree |
| 9 | - | // used by f's full call tree. Protection does not extend to global variables or | 9 | + | // and marks its allocations for clearing after they become unreachable and |
| 10 | - | // goroutines started by f. | 10 | + | // the GC notices. On unsupported platforms runtime/secret.Do invokes f |
| 11 | - | func SecretDo(f func()) { | 11 | + | // directly. It does not protect globals or goroutines started by f. |
| 12 | - | secret.Do(f) | 12 | + | func SecretDo(f func()) { secret.Do(f) } |
| 13 | - | } | |||