@@ -2,8 +2,10 @@ package modulefirewall
22
33import (
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+
92177func parseForbiddenBody (body string ) []RemoteBlock {
93178 var blocks []RemoteBlock
94179 for _ , line := range strings .Split (body , "\n " ) {
0 commit comments