Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ services:
- .:/boulder:cached
- ./.gocache:/root/.cache/go-build:cached
- ./test/certs/.softhsm-tokens/:/var/lib/softhsm/tokens/:cached
- pkimetal-socket:/var/run/pkimetal
networks:
bouldernet:
ipv4_address: 10.77.77.77
Expand Down Expand Up @@ -144,9 +145,11 @@ services:
- bouldernet

bpkimetal:
image: ghcr.io/pkimetal/pkimetal:v1.20.0
networks:
- bouldernet
image: ghcr.io/pkimetal/pkimetal:v1.41.0
volumes:
- pkimetal-socket:/var/run/pkimetal
- ./test/pkimetal-config.yaml:/config/config.yaml:ro
network_mode: none

bvitess:
# The `letsencrypt/boulder-vtcomboserver:latest` tag is automatically built
Expand Down Expand Up @@ -181,6 +184,17 @@ services:
aliases:
- boulder-vitess

volumes:
# Shared between bpkimetal (which listens on a unix socket here) and any
# boulder container that needs to reach pkimetal. Owned by uid 1001 so
# the default pkimetal user in the container can create the socket.
pkimetal-socket:
driver: local
driver_opts:
type: tmpfs
device: tmpfs
o: "uid=1001,gid=1001,mode=0755"

networks:
# This network represents the data-center internal network. It is used for
# boulder services and their infrastructure, such as consul, mariadb, and
Expand Down
31 changes: 26 additions & 5 deletions linter/lints/rfc/lint_cert_via_pkimetal.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import (
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/url"
"slices"
"strings"
"sync"
"time"

"github.com/zmap/zcrypto/x509"
Expand All @@ -20,10 +22,28 @@ import (
// PKIMetalConfig and its execute method provide a shared basis for linting
// both certs and CRLs using PKIMetal.
type PKIMetalConfig struct {
Addr string `toml:"addr" comment:"The address where a pkilint REST API can be reached."`
Socket string `toml:"socket" comment:"Path to a unix socket where a pkilint REST API is listening."`
Comment thread
mcpherrinm marked this conversation as resolved.
Outdated
Severity string `toml:"severity" comment:"The minimum severity of findings to report (meta, debug, info, notice, warning, error, bug, or fatal)."`
Timeout time.Duration `toml:"timeout" comment:"How long, in nanoseconds, to wait before giving up."`
IgnoreLints []string `toml:"ignore_lints" comment:"The unique Validator:Code IDs of lint findings which should be ignored."`
Comment thread
mcpherrinm marked this conversation as resolved.
Outdated

clientOnce sync.Once
client *http.Client
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it makes sense to store these on the Config object.

The only reason that .execute() is a method on the config object is that it is shared between both certViaPKIMetal and crlViaPKIMetal. It doesn't semantically belong there; having it be a method on the config struct was just a convenience.

I think this PR should definitely store the http.Client on the certViaPKIMetal struct (and the crlViaPKIMetal struct), rather than on the config struct. And if you want to go farther for true code-matches-semantics, it should make execute a standalone function and supply all of the config fields as arguments.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok. Yeah I was definitely just copying execute(), but can do

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I've made a refactor here to actually introduce a new linter/pkimetal package which contains just the code needed to Execute. I think this ends up being a lot neater, though it still has the somewhat unfortunate sync.Once that I don't love to get a reusable http client, but with the way Zlint's Configure() lifecycle is set up, I don't think we're going to do better without sidestepping zlint's config

}

func (pkim *PKIMetalConfig) httpClient() *http.Client {
pkim.clientOnce.Do(func() {
socket := pkim.Socket
pkim.client = &http.Client{
Transport: &http.Transport{
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
var d net.Dialer
return d.DialContext(ctx, "unix", socket)
},
},
Comment thread
mcpherrinm marked this conversation as resolved.
Outdated
}
})
return pkim.client
Comment thread
mcpherrinm marked this conversation as resolved.
Outdated
}

func (pkim *PKIMetalConfig) execute(endpoint string, der []byte) (*lint.LintResult, error) {
Expand All @@ -35,7 +55,8 @@ func (pkim *PKIMetalConfig) execute(endpoint string, der []byte) (*lint.LintResu
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()

apiURL, err := url.JoinPath(pkim.Addr, endpoint)
// Host is ignored by our unix-socket transport, so any valid base works.
apiURL, err := url.JoinPath("http://pkimetal", endpoint)
if err != nil {
return nil, fmt.Errorf("constructing pkimetal url: %w", err)
}
Expand All @@ -56,7 +77,7 @@ func (pkim *PKIMetalConfig) execute(endpoint string, der []byte) (*lint.LintResu
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
req.Header.Add("Accept", "application/json")

resp, err := http.DefaultClient.Do(req)
resp, err := pkim.httpClient().Do(req)
if err != nil {
return nil, fmt.Errorf("making POST request to pkimetal API: %s (timeout %s)", err, timeout)
}
Expand Down Expand Up @@ -141,8 +162,8 @@ func (l *certViaPKIMetal) Configure() any {

func (l *certViaPKIMetal) CheckApplies(c *x509.Certificate) bool {
// This lint applies to all certificates issued by Boulder, as long as it has
// been configured with an address to reach out to. If not, skip it.
return l.Addr != ""
// been configured with a socket to reach out to. If not, skip it.
return l.Socket != ""
}

func (l *certViaPKIMetal) Execute(c *x509.Certificate) *lint.LintResult {
Expand Down
4 changes: 2 additions & 2 deletions linter/lints/rfc/lint_crl_via_pkimetal.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ func (l *crlViaPKIMetal) Configure() any {

func (l *crlViaPKIMetal) CheckApplies(c *x509.RevocationList) bool {
// This lint applies to all CRLs issued by Boulder, as long as it has
// been configured with an address to reach out to. If not, skip it.
return l.Addr != ""
// been configured with a socket to reach out to. If not, skip it.
return l.Socket != ""
Comment thread
mcpherrinm marked this conversation as resolved.
Outdated
}

func (l *crlViaPKIMetal) Execute(c *x509.RevocationList) *lint.LintResult {
Expand Down
9 changes: 6 additions & 3 deletions test/config-next/zlint.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[e_pkimetal_lint_cabf_serverauth_cert]
addr = "http://bpkimetal:8080"
socket = "/var/run/pkimetal/pkimetal.sock"
severity = "notice"
timeout = 2000000000 # 2 seconds
ignore_lints = [
Expand All @@ -18,11 +18,14 @@ ignore_lints = [
# Some linters continue to complain about the lack of an AIA OCSP URI, even
# when a CRLDP is present.
"certlint:br_certificates_must_include_an_http_url_of_the_ocsp_responder",
"x509lint:no_ocsp_over_http"
"x509lint:no_ocsp_over_http",
# ctlint requires CCADB data to verify SCT signatures; our test issuers are
# not in CCADB so this warning fires for every issued certificate.
"ctlint:cannot_verify_sct_signature_without_issuer_spki,_which_could_not_be_found_in_the_available_ccadb_data",
]

[e_pkimetal_lint_cabf_serverauth_crl]
addr = "http://bpkimetal:8080"
socket = "/var/run/pkimetal/pkimetal.sock"
severity = "notice"
timeout = 2000000000 # 2 seconds
ignore_lints = []
7 changes: 5 additions & 2 deletions test/config/zlint.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[e_pkimetal_lint_cabf_serverauth_cert]
addr = "http://bpkimetal:8080"
socket = "/var/run/pkimetal/pkimetal.sock"
severity = "notice"
timeout = 2000000000 # 2 seconds
ignore_lints = [
Expand All @@ -15,10 +15,13 @@ ignore_lints = [
# issued under the "classic" profile, but have removed it from our "tlsserver"
# and "shortlived" profiles.
"pkilint:cabf.serverauth.subscriber_rsa_digitalsignature_and_keyencipherment_present",
# ctlint requires CCADB data to verify SCT signatures; our test issuers are
# not in CCADB so this warning fires for every issued certificate.
"ctlint:cannot_verify_sct_signature_without_issuer_spki,_which_could_not_be_found_in_the_available_ccadb_data",
]

[e_pkimetal_lint_cabf_serverauth_crl]
addr = "http://bpkimetal:8080"
socket = "/var/run/pkimetal/pkimetal.sock"
severity = "notice"
timeout = 2000000000 # 2 seconds
ignore_lints = []
4 changes: 2 additions & 2 deletions test/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ configure_database_endpoints
./test/wait-for-it.sh boulder-mariadb 3306
./test/wait-for-it.sh boulder-proxysql 6033

# make sure we can reach pkilint
./test/wait-for-it.sh bpkimetal 8080
# make sure pkimetal's unix socket is ready
./test/wait-for-socket.sh /var/run/pkimetal/pkimetal.sock

if [[ $# -eq 0 ]]; then
exec python3 ./start.py
Expand Down
5 changes: 5 additions & 0 deletions test/pkimetal-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
server:
# Disable the TCP webserver; we only listen on a unix socket so callers
# do not need any network access to reach pkimetal.
Comment thread
mcpherrinm marked this conversation as resolved.
Outdated
webserverPort: 0
webserverPath: /var/run/pkimetal/pkimetal.sock
18 changes: 18 additions & 0 deletions test/wait-for-socket.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/bin/bash
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a pretty straightforward port of wait-for-it.sh to support a socket. I think we could probably do this in docker-compose with a healthcheck and dependencies instead, but I figured that's too much changing at once


set -e -u

socket="${1}"
max_tries=40

for n in $(seq 1 "${max_tries}"); do
if [ -S "${socket}" ]; then
echo "Socket ${socket} is ready"
exit 0
fi
echo "$(date) - still waiting for socket ${socket}"
sleep 1
done

echo "timed out waiting for socket ${socket}"
exit 1
Loading