fix(dgram): redact and clear bootstrap keys promptly
- commit
- 9f8c7d27f7cdb324c751e61442f1de18e39d2e65
- author
- brice · · 2026-07-23 20:29 UTC
- parent
- 93af417
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +44 -20
M internal/adapters/dgram/ssh_dialer.go +32 -15
| @@ -158,6 +158,7 @@ func (d RemoteDialer) Dial(ctx context.Context) (ports.Transport, error) { | |||
| 158 | 158 | cleanup = false | |
| 159 | 159 | return nil, udpUnavailable("read bootstrap readiness", err, &stderr) | |
| 160 | 160 | } | |
| 161 | + | defer pdgram.Erase(ready.key) | |
| 161 | 162 | if err := waitBootstrapContext(bootstrapCtx, proc); err != nil { | |
| 162 | 163 | waited = true | |
| 163 | 164 | cleanup = false | |
| @@ -182,6 +183,7 @@ func (d RemoteDialer) Dial(ctx context.Context) (ports.Transport, error) { | |||
| 182 | 183 | return listenUDPPacket(ctx) | |
| 183 | 184 | }, | |
| 184 | 185 | }, WithRuntimeObserver(d.RuntimeObserver)) | |
| 186 | + | pdgram.Erase(ready.key) | |
| 185 | 187 | if err != nil { | |
| 186 | 188 | _ = pc.Close() | |
| 187 | 189 | return nil, udpUnavailable("create UDP transport", err, &stderr) | |
| @@ -251,22 +253,37 @@ func readUDPReady(r io.Reader) (udpReady, error) { | |||
| 251 | 253 | if err != nil && line == "" { | |
| 252 | 254 | return udpReady{}, err | |
| 253 | 255 | } | |
| 254 | - | fields := strings.Fields(line) | |
| 255 | - | if len(fields) != 3 || fields[0] != "VEV-UDP" { | |
| 256 | - | return udpReady{}, fmt.Errorf("malformed readiness line %q", strings.TrimSpace(line)) | |
| 257 | - | } | |
| 258 | - | port, err := strconv.Atoi(fields[1]) | |
| 259 | - | if err != nil || port < 1 || port > 65535 { | |
| 260 | - | return udpReady{}, fmt.Errorf("invalid UDP port %q", fields[1]) | |
| 261 | - | } | |
| 262 | - | key, err := base64.StdEncoding.DecodeString(fields[2]) | |
| 263 | - | if err != nil { | |
| 264 | - | return udpReady{}, fmt.Errorf("invalid bootstrap key: %w", err) | |
| 265 | - | } | |
| 266 | - | if len(key) != pdgram.KeySize { | |
| 267 | - | return udpReady{}, fmt.Errorf("invalid bootstrap key length %d", len(key)) | |
| 256 | + | ||
| 257 | + | ready := udpReady{key: make([]byte, pdgram.KeySize)} | |
| 258 | + | var parseErr error | |
| 259 | + | pdgram.SecretDo(func() { | |
| 260 | + | fields := strings.Fields(line) | |
| 261 | + | if len(fields) != 3 || fields[0] != "VEV-UDP" { | |
| 262 | + | parseErr = fmt.Errorf("malformed UDP readiness line") | |
| 263 | + | return | |
| 264 | + | } | |
| 265 | + | ready.port, parseErr = strconv.Atoi(fields[1]) | |
| 266 | + | if parseErr != nil || ready.port < 1 || ready.port > 65535 { | |
| 267 | + | parseErr = fmt.Errorf("invalid UDP port %q", fields[1]) | |
| 268 | + | return | |
| 269 | + | } | |
| 270 | + | key, decodeErr := base64.StdEncoding.DecodeString(fields[2]) | |
| 271 | + | defer pdgram.Erase(key) | |
| 272 | + | if decodeErr != nil { | |
| 273 | + | parseErr = fmt.Errorf("invalid bootstrap key: %w", decodeErr) | |
| 274 | + | return | |
| 275 | + | } | |
| 276 | + | if len(key) != pdgram.KeySize { | |
| 277 | + | parseErr = fmt.Errorf("invalid bootstrap key length %d", len(key)) | |
| 278 | + | return | |
| 279 | + | } | |
| 280 | + | copy(ready.key, key) | |
| 281 | + | }) | |
| 282 | + | if parseErr != nil { | |
| 283 | + | pdgram.Erase(ready.key) | |
| 284 | + | return udpReady{}, parseErr | |
| 268 | 285 | } | |
| 269 | - | return udpReady{port: port, key: key}, nil | |
| 286 | + | return ready, nil | |
| 270 | 287 | } | |
| 271 | 288 | ||
| 272 | 289 | func udpUnavailable(action string, err error, stderr fmt.Stringer) error { | |
| @@ -158,6 +158,7 @@ func (d RemoteDialer) Dial(ctx context.Context) (ports.Transport, error) { | |||||
| 158 | cleanup = false | 158 | cleanup = false | ||
| 159 | return nil, udpUnavailable("read bootstrap readiness", err, &stderr) | 159 | return nil, udpUnavailable("read bootstrap readiness", err, &stderr) | ||
| 160 | } | 160 | } | ||
| 161 | + | defer pdgram.Erase(ready.key) | |||
| 161 | if err := waitBootstrapContext(bootstrapCtx, proc); err != nil { | 162 | if err := waitBootstrapContext(bootstrapCtx, proc); err != nil { | ||
| 162 | waited = true | 163 | waited = true | ||
| 163 | cleanup = false | 164 | cleanup = false | ||
| @@ -182,6 +183,7 @@ func (d RemoteDialer) Dial(ctx context.Context) (ports.Transport, error) { | |||||
| 182 | return listenUDPPacket(ctx) | 183 | return listenUDPPacket(ctx) | ||
| 183 | }, | 184 | }, | ||
| 184 | }, WithRuntimeObserver(d.RuntimeObserver)) | 185 | }, WithRuntimeObserver(d.RuntimeObserver)) | ||
| 186 | + | pdgram.Erase(ready.key) | |||
| 185 | if err != nil { | 187 | if err != nil { | ||
| 186 | _ = pc.Close() | 188 | _ = pc.Close() | ||
| 187 | return nil, udpUnavailable("create UDP transport", err, &stderr) | 189 | return nil, udpUnavailable("create UDP transport", err, &stderr) | ||
| @@ -251,22 +253,37 @@ func readUDPReady(r io.Reader) (udpReady, error) { | |||||
| 251 | if err != nil && line == "" { | 253 | if err != nil && line == "" { | ||
| 252 | return udpReady{}, err | 254 | return udpReady{}, err | ||
| 253 | } | 255 | } | ||
| 254 | - | fields := strings.Fields(line) | 256 | + | |
| 255 | - | if len(fields) != 3 || fields[0] != "VEV-UDP" { | 257 | + | ready := udpReady{key: make([]byte, pdgram.KeySize)} |
| 256 | - | return udpReady{}, fmt.Errorf("malformed readiness line %q", strings.TrimSpace(line)) | 258 | + | var parseErr error |
| 257 | - | } | 259 | + | pdgram.SecretDo(func() { |
| 258 | - | port, err := strconv.Atoi(fields[1]) | 260 | + | fields := strings.Fields(line) |
| 259 | - | if err != nil || port < 1 || port > 65535 { | 261 | + | if len(fields) != 3 || fields[0] != "VEV-UDP" { |
| 260 | - | return udpReady{}, fmt.Errorf("invalid UDP port %q", fields[1]) | 262 | + | parseErr = fmt.Errorf("malformed UDP readiness line") |
| 261 | - | } | 263 | + | return |
| 262 | - | key, err := base64.StdEncoding.DecodeString(fields[2]) | 264 | + | } |
| 263 | - | if err != nil { | 265 | + | ready.port, parseErr = strconv.Atoi(fields[1]) |
| 264 | - | return udpReady{}, fmt.Errorf("invalid bootstrap key: %w", err) | 266 | + | if parseErr != nil || ready.port < 1 || ready.port > 65535 { |
| 265 | - | } | 267 | + | parseErr = fmt.Errorf("invalid UDP port %q", fields[1]) |
| 266 | - | if len(key) != pdgram.KeySize { | 268 | + | return |
| 267 | - | return udpReady{}, fmt.Errorf("invalid bootstrap key length %d", len(key)) | 269 | + | } |
| 270 | + | key, decodeErr := base64.StdEncoding.DecodeString(fields[2]) | |||
| 271 | + | defer pdgram.Erase(key) | |||
| 272 | + | if decodeErr != nil { | |||
| 273 | + | parseErr = fmt.Errorf("invalid bootstrap key: %w", decodeErr) | |||
| 274 | + | return | |||
| 275 | + | } | |||
| 276 | + | if len(key) != pdgram.KeySize { | |||
| 277 | + | parseErr = fmt.Errorf("invalid bootstrap key length %d", len(key)) | |||
| 278 | + | return | |||
| 279 | + | } | |||
| 280 | + | copy(ready.key, key) | |||
| 281 | + | }) | |||
| 282 | + | if parseErr != nil { | |||
| 283 | + | pdgram.Erase(ready.key) | |||
| 284 | + | return udpReady{}, parseErr | |||
| 268 | } | 285 | } | ||
| 269 | - | return udpReady{port: port, key: key}, nil | 286 | + | return ready, nil |
| 270 | } | 287 | } | ||
| 271 | 288 | ||||
| 272 | func udpUnavailable(action string, err error, stderr fmt.Stringer) error { | 289 | func udpUnavailable(action string, err error, stderr fmt.Stringer) error { | ||
M internal/adapters/dgram/ssh_dialer_test.go +12 -5
| @@ -60,7 +60,7 @@ func TestRemoteDialerUDPBootstrapFailures(t *testing.T) { | |||
| 60 | 60 | wantStdoutClosed bool | |
| 61 | 61 | }{ | |
| 62 | 62 | {name: "start failure", start: errors.New("ssh missing"), want: "remote UDP transport unavailable: start bootstrap: ssh missing", wantStdoutClosed: true}, | |
| 63 | - | {name: "malformed readiness", stdout: "hello\n", want: "malformed readiness line", killed: true, waited: true, wantStdoutClosed: true}, | |
| 63 | + | {name: "malformed readiness", stdout: "hello\n", want: "malformed UDP readiness line", killed: true, waited: true, wantStdoutClosed: true}, | |
| 64 | 64 | {name: "bootstrap wait failure", stdout: "VEV-UDP 4444 " + key + "\n", want: "wait bootstrap: exit status 255", waited: true, wantStdoutClosed: true}, | |
| 65 | 65 | {name: "packet listen failure", stdout: "VEV-UDP 4444 " + key + "\n", listen: errors.New("bind denied"), want: "listen UDP: bind denied", waited: true, wantStdoutClosed: true}, | |
| 66 | 66 | } | |
| @@ -180,18 +180,25 @@ func TestReadUDPReady(t *testing.T) { | |||
| 180 | 180 | name, line string | |
| 181 | 181 | wantPort int | |
| 182 | 182 | wantErr string | |
| 183 | + | exactErr bool | |
| 183 | 184 | }{ | |
| 184 | 185 | {name: "valid", line: "VEV-UDP 1234 " + key + "\n", wantPort: 1234}, | |
| 185 | - | {name: "bad prefix", line: "NOPE 1234 " + key + "\n", wantErr: "malformed readiness"}, | |
| 186 | - | {name: "bad port", line: "VEV-UDP 99999 " + key + "\n", wantErr: "invalid UDP port"}, | |
| 186 | + | {name: "bad prefix", line: "NOPE 1234 " + key + "\n", wantErr: "malformed UDP readiness line", exactErr: true}, | |
| 187 | + | {name: "bad port", line: "VEV-UDP 99999 " + key + "\n", wantErr: `invalid UDP port "99999"`, exactErr: true}, | |
| 187 | 188 | {name: "bad key", line: "VEV-UDP 1234 !!!\n", wantErr: "invalid bootstrap key"}, | |
| 188 | 189 | } | |
| 189 | 190 | for _, tt := range tests { | |
| 190 | 191 | t.Run(tt.name, func(t *testing.T) { | |
| 191 | 192 | got, err := readUDPReady(strings.NewReader(tt.line)) | |
| 192 | 193 | if tt.wantErr != "" { | |
| 193 | - | if err == nil || !strings.Contains(err.Error(), tt.wantErr) { | |
| 194 | - | t.Fatalf("err=%v, want %q", err, tt.wantErr) | |
| 194 | + | if err == nil { | |
| 195 | + | t.Fatalf("err=nil, want %q", tt.wantErr) | |
| 196 | + | } | |
| 197 | + | if tt.exactErr && err.Error() != tt.wantErr { | |
| 198 | + | t.Fatalf("err=%q, want exactly %q", err, tt.wantErr) | |
| 199 | + | } | |
| 200 | + | if !tt.exactErr && !strings.Contains(err.Error(), tt.wantErr) { | |
| 201 | + | t.Fatalf("err=%v, want containing %q", err, tt.wantErr) | |
| 195 | 202 | } | |
| 196 | 203 | return | |
| 197 | 204 | } | |
| @@ -60,7 +60,7 @@ func TestRemoteDialerUDPBootstrapFailures(t *testing.T) { | |||||
| 60 | wantStdoutClosed bool | 60 | wantStdoutClosed bool | ||
| 61 | }{ | 61 | }{ | ||
| 62 | {name: "start failure", start: errors.New("ssh missing"), want: "remote UDP transport unavailable: start bootstrap: ssh missing", wantStdoutClosed: true}, | 62 | {name: "start failure", start: errors.New("ssh missing"), want: "remote UDP transport unavailable: start bootstrap: ssh missing", wantStdoutClosed: true}, | ||
| 63 | - | {name: "malformed readiness", stdout: "hello\n", want: "malformed readiness line", killed: true, waited: true, wantStdoutClosed: true}, | 63 | + | {name: "malformed readiness", stdout: "hello\n", want: "malformed UDP readiness line", killed: true, waited: true, wantStdoutClosed: true}, |
| 64 | {name: "bootstrap wait failure", stdout: "VEV-UDP 4444 " + key + "\n", want: "wait bootstrap: exit status 255", waited: true, wantStdoutClosed: true}, | 64 | {name: "bootstrap wait failure", stdout: "VEV-UDP 4444 " + key + "\n", want: "wait bootstrap: exit status 255", waited: true, wantStdoutClosed: true}, | ||
| 65 | {name: "packet listen failure", stdout: "VEV-UDP 4444 " + key + "\n", listen: errors.New("bind denied"), want: "listen UDP: bind denied", waited: true, wantStdoutClosed: true}, | 65 | {name: "packet listen failure", stdout: "VEV-UDP 4444 " + key + "\n", listen: errors.New("bind denied"), want: "listen UDP: bind denied", waited: true, wantStdoutClosed: true}, | ||
| 66 | } | 66 | } | ||
| @@ -180,18 +180,25 @@ func TestReadUDPReady(t *testing.T) { | |||||
| 180 | name, line string | 180 | name, line string | ||
| 181 | wantPort int | 181 | wantPort int | ||
| 182 | wantErr string | 182 | wantErr string | ||
| 183 | + | exactErr bool | |||
| 183 | }{ | 184 | }{ | ||
| 184 | {name: "valid", line: "VEV-UDP 1234 " + key + "\n", wantPort: 1234}, | 185 | {name: "valid", line: "VEV-UDP 1234 " + key + "\n", wantPort: 1234}, | ||
| 185 | - | {name: "bad prefix", line: "NOPE 1234 " + key + "\n", wantErr: "malformed readiness"}, | 186 | + | {name: "bad prefix", line: "NOPE 1234 " + key + "\n", wantErr: "malformed UDP readiness line", exactErr: true}, |
| 186 | - | {name: "bad port", line: "VEV-UDP 99999 " + key + "\n", wantErr: "invalid UDP port"}, | 187 | + | {name: "bad port", line: "VEV-UDP 99999 " + key + "\n", wantErr: `invalid UDP port "99999"`, exactErr: true}, |
| 187 | {name: "bad key", line: "VEV-UDP 1234 !!!\n", wantErr: "invalid bootstrap key"}, | 188 | {name: "bad key", line: "VEV-UDP 1234 !!!\n", wantErr: "invalid bootstrap key"}, | ||
| 188 | } | 189 | } | ||
| 189 | for _, tt := range tests { | 190 | for _, tt := range tests { | ||
| 190 | t.Run(tt.name, func(t *testing.T) { | 191 | t.Run(tt.name, func(t *testing.T) { | ||
| 191 | got, err := readUDPReady(strings.NewReader(tt.line)) | 192 | got, err := readUDPReady(strings.NewReader(tt.line)) | ||
| 192 | if tt.wantErr != "" { | 193 | if tt.wantErr != "" { | ||
| 193 | - | if err == nil || !strings.Contains(err.Error(), tt.wantErr) { | 194 | + | if err == nil { |
| 194 | - | t.Fatalf("err=%v, want %q", err, tt.wantErr) | 195 | + | t.Fatalf("err=nil, want %q", tt.wantErr) |
| 196 | + | } | |||
| 197 | + | if tt.exactErr && err.Error() != tt.wantErr { | |||
| 198 | + | t.Fatalf("err=%q, want exactly %q", err, tt.wantErr) | |||
| 199 | + | } | |||
| 200 | + | if !tt.exactErr && !strings.Contains(err.Error(), tt.wantErr) { | |||
| 201 | + | t.Fatalf("err=%v, want containing %q", err, tt.wantErr) | |||
| 195 | } | 202 | } | ||
| 196 | return | 203 | return | ||
| 197 | } | 204 | } | ||