Skip to content

Commit 4a30567

Browse files
feat(firewall): HTTPS remote TLS pins + security policy keys
EvaluateRemote pins TrustedFirewallSigners/Thumbprint; trusted modules on policy roots. Co-authored-by: Cursor
1 parent 64ab4b0 commit 4a30567

2 files changed

Lines changed: 93 additions & 8 deletions

File tree

‎modulefirewall/remote.go‎

Lines changed: 91 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ package modulefirewall
22

33
import (
44
"bytes"
5+
"crypto/sha1"
56
"crypto/tls"
67
"crypto/x509"
8+
"encoding/hex"
79
"fmt"
810
"io"
911
"net/http"
@@ -26,11 +28,20 @@ type RemoteResult struct {
2628
ErrorMsg string
2729
}
2830

31+
// RemoteTLSOptions configures HTTPS policy client behavior.
32+
type RemoteTLSOptions struct {
33+
TimeoutSec int
34+
AllowedOrgs []string // TrustedFirewallSigners (O= match); empty = any valid CA chain
35+
AllowedThumbprints []string // TrustedFirewallThumbprint (SHA-1 leaf hex); empty = no pin
36+
}
37+
2938
// EvaluateRemote POSTs newline-delimited module names to an HTTPS endpoint.
30-
func EvaluateRemote(endpoint string, modules []PackageSpec, timeoutSec int, rootCAs *x509.CertPool, insecureSkipVerify bool) RemoteResult {
39+
func EvaluateRemote(endpoint string, modules []PackageSpec, opts RemoteTLSOptions) RemoteResult {
40+
timeoutSec := opts.TimeoutSec
3141
if timeoutSec <= 0 {
3242
timeoutSec = 3
3343
}
44+
3445
var body bytes.Buffer
3546
for _, m := range modules {
3647
line := m.Raw
@@ -44,14 +55,15 @@ func EvaluateRemote(endpoint string, modules []PackageSpec, timeoutSec int, root
4455
body.WriteByte('\n')
4556
}
4657

58+
tlsCfg := &tls.Config{
59+
MinVersion: tls.VersionTLS12,
60+
VerifyPeerCertificate: makePeerVerifier(opts.AllowedOrgs, opts.AllowedThumbprints),
61+
}
62+
4763
client := &http.Client{
4864
Timeout: time.Duration(timeoutSec) * time.Second,
4965
Transport: &http.Transport{
50-
TLSClientConfig: &tls.Config{
51-
RootCAs: rootCAs,
52-
InsecureSkipVerify: insecureSkipVerify, //nolint:gosec // only when policy explicitly allows
53-
MinVersion: tls.VersionTLS12,
54-
},
66+
TLSClientConfig: tlsCfg,
5567
},
5668
}
5769

@@ -89,6 +101,79 @@ func EvaluateRemote(endpoint string, modules []PackageSpec, timeoutSec int, root
89101
}
90102
}
91103

104+
func makePeerVerifier(orgs, thumbs []string) func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
105+
orgs = normalizePinList(orgs)
106+
thumbs = normalizeThumbList(thumbs)
107+
if len(orgs) == 0 && len(thumbs) == 0 {
108+
return nil
109+
}
110+
return func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
111+
if len(rawCerts) == 0 {
112+
return fmt.Errorf("firewall remote: empty peer certificate")
113+
}
114+
leaf, err := x509.ParseCertificate(rawCerts[0])
115+
if err != nil {
116+
return fmt.Errorf("firewall remote: parse leaf: %w", err)
117+
}
118+
if len(thumbs) > 0 {
119+
sum := sha1.Sum(leaf.Raw)
120+
got := strings.ToLower(hex.EncodeToString(sum[:]))
121+
ok := false
122+
for _, want := range thumbs {
123+
if got == want {
124+
ok = true
125+
break
126+
}
127+
}
128+
if !ok {
129+
return fmt.Errorf("firewall remote: leaf thumbprint not in TrustedFirewallThumbprint")
130+
}
131+
}
132+
if len(orgs) > 0 {
133+
ok := false
134+
for _, o := range leaf.Subject.Organization {
135+
for _, want := range orgs {
136+
if strings.EqualFold(strings.TrimSpace(o), want) {
137+
ok = true
138+
break
139+
}
140+
}
141+
if ok {
142+
break
143+
}
144+
}
145+
if !ok {
146+
return fmt.Errorf("firewall remote: leaf O= not in TrustedFirewallSigners")
147+
}
148+
}
149+
_ = verifiedChains
150+
return nil
151+
}
152+
}
153+
154+
func normalizePinList(in []string) []string {
155+
out := make([]string, 0, len(in))
156+
for _, s := range in {
157+
s = strings.TrimSpace(s)
158+
if s != "" {
159+
out = append(out, s)
160+
}
161+
}
162+
return out
163+
}
164+
165+
func normalizeThumbList(in []string) []string {
166+
out := make([]string, 0, len(in))
167+
for _, s := range in {
168+
s = strings.ToLower(strings.ReplaceAll(strings.TrimSpace(s), ":", ""))
169+
s = strings.ReplaceAll(s, " ", "")
170+
if s != "" {
171+
out = append(out, s)
172+
}
173+
}
174+
return out
175+
}
176+
92177
func parseForbiddenBody(body string) []RemoteBlock {
93178
var blocks []RemoteBlock
94179
for _, line := range strings.Split(body, "\n") {

‎settings/settings.go‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,8 +209,8 @@ func applyMachineOnlySettings() {
209209
func isSecurityPolicySetting(name string) bool {
210210
switch name {
211211
case "allowed_signers", "allow_insecure_downloads", "local_install_only", "air_gapped",
212-
"approved_modules", "approved_global_modules", "trusted_firewall_signers", "trusted_firewall_thumbprint",
213-
"firewall_http_timeout_seconds":
212+
"approved_modules", "approved_global_modules", "trusted_modules", "untrusted_module_handler_action",
213+
"trusted_firewall_signers", "trusted_firewall_thumbprint", "firewall_http_timeout_seconds":
214214
return true
215215
default:
216216
return false

0 commit comments

Comments
 (0)