Bump all dependencies except dev-tunnels

This commit is contained in:
William Martin 2025-07-01 16:08:23 +02:00
parent 2b84a3d698
commit 96956da8de
501 changed files with 35317 additions and 31761 deletions

106
go.mod
View file

@ -1,6 +1,6 @@
module github.com/cli/cli/v2
go 1.24
go 1.24.0
toolchain go1.24.4
@ -8,29 +8,29 @@ require (
github.com/AlecAivazis/survey/v2 v2.3.7
github.com/MakeNowJust/heredoc v1.0.0
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2
github.com/briandowns/spinner v1.18.1
github.com/briandowns/spinner v1.23.2
github.com/cenkalti/backoff/v4 v4.3.0
github.com/cenkalti/backoff/v5 v5.0.2
github.com/charmbracelet/glamour v0.9.2-0.20250319212134-549f544650e3
github.com/charmbracelet/glamour v0.10.0
github.com/charmbracelet/huh v0.7.0
github.com/charmbracelet/lipgloss v1.1.1-0.20250319133953-166f707985bc
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834
github.com/cli/go-gh/v2 v2.12.1
github.com/cli/go-internal v0.0.0-20241025142207-6c48bcd5ce24
github.com/cli/oauth v1.1.1
github.com/cli/oauth v1.2.0
github.com/cli/safeexec v1.0.1
github.com/cpuguy83/go-md2man/v2 v2.0.7
github.com/creack/pty v1.1.24
github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7
github.com/digitorus/timestamp v0.0.0-20250524132541-c45532741eea
github.com/distribution/reference v0.6.0
github.com/gabriel-vasile/mimetype v1.4.9
github.com/gdamore/tcell/v2 v2.5.4
github.com/golang/snappy v0.0.4
github.com/gdamore/tcell/v2 v2.8.1
github.com/golang/snappy v1.0.0
github.com/google/go-cmp v0.7.0
github.com/google/go-containerregistry v0.20.6
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/gorilla/websocket v1.5.3
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/go-version v1.3.0
github.com/hashicorp/go-version v1.7.0
github.com/henvic/httpretty v0.1.4
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec
github.com/in-toto/attestation v1.1.2
@ -40,10 +40,10 @@ require (
github.com/mattn/go-isatty v0.0.20
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d
github.com/microsoft/dev-tunnels v0.0.25
github.com/muhammadmuzzammil1998/jsonc v0.0.0-20201229145248-615b0916ca38
github.com/muhammadmuzzammil1998/jsonc v1.0.0
github.com/opentracing/opentracing-go v1.2.0
github.com/rivo/tview v0.0.0-20221029100920-c4a7e501810d
github.com/shurcooL/githubv4 v0.0.0-20240120211514-18a1ae0e79dc
github.com/rivo/tview v0.0.0-20250625164341-a4a78f1e05cb
github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7
github.com/sigstore/protobuf-specs v0.4.3
github.com/sigstore/sigstore-go v1.0.0
github.com/spf13/cobra v1.9.1
@ -51,24 +51,24 @@ require (
github.com/stretchr/testify v1.10.0
github.com/theupdateframework/go-tuf/v2 v2.1.1
github.com/yuin/goldmark v1.7.12
github.com/zalando/go-keyring v0.2.5
github.com/zalando/go-keyring v0.2.6
golang.org/x/crypto v0.39.0
golang.org/x/sync v0.15.0
golang.org/x/term v0.32.0
golang.org/x/text v0.26.0
google.golang.org/grpc v1.72.2
google.golang.org/grpc v1.73.0
google.golang.org/protobuf v1.36.6
gopkg.in/h2non/gock.v1 v1.1.2
gopkg.in/yaml.v3 v3.0.1
)
require (
dario.cat/mergo v1.0.1 // indirect
al.essio.dev/pkg/shellescape v1.6.0 // indirect
dario.cat/mergo v1.0.2 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.3.0 // indirect
github.com/Masterminds/semver/v3 v3.4.0 // indirect
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
github.com/alecthomas/chroma/v2 v2.14.0 // indirect
github.com/alessio/shellescape v1.4.2 // indirect
github.com/alecthomas/chroma/v2 v2.19.0 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
@ -76,35 +76,36 @@ require (
github.com/blang/semver v3.5.1+incompatible // indirect
github.com/catppuccin/go v0.3.0 // indirect
github.com/charmbracelet/bubbles v0.21.0 // indirect
github.com/charmbracelet/bubbletea v1.3.4 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/x/ansi v0.8.0 // indirect
github.com/charmbracelet/bubbletea v1.3.5 // indirect
github.com/charmbracelet/colorprofile v0.3.1 // indirect
github.com/charmbracelet/x/ansi v0.9.3 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
github.com/charmbracelet/x/exp/slice v0.0.0-20250630141444-821143405392 // indirect
github.com/charmbracelet/x/exp/strings v0.0.0-20250630141444-821143405392 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/cli/browser v1.3.0 // indirect
github.com/cli/shurcooL-graphql v0.0.4 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect
github.com/cyberphone/json-canonicalization v0.0.0-20220623050100-57a0ce2678a7 // indirect
github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 // indirect
github.com/danieljoos/wincred v1.2.2 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 // indirect
github.com/dlclark/regexp2 v1.11.0 // indirect
github.com/docker/cli v28.2.2+incompatible // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/docker/cli v28.3.0+incompatible // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/docker-credential-helpers v0.9.3 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/fatih/color v1.16.0 // indirect
github.com/fsnotify/fsnotify v1.8.0 // indirect
github.com/gdamore/encoding v1.0.0 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/gdamore/encoding v1.0.1 // indirect
github.com/go-chi/chi v4.1.2+incompatible // indirect
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
github.com/go-jose/go-jose/v4 v4.1.1 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/analysis v0.23.0 // indirect
github.com/go-openapi/errors v0.22.1 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonpointer v0.21.1 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/loads v0.22.0 // indirect
github.com/go-openapi/runtime v0.28.0 // indirect
@ -112,9 +113,9 @@ require (
github.com/go-openapi/strfmt v0.23.0 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-openapi/validate v0.24.0 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/go-viper/mapstructure/v2 v2.3.0 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/google/certificate-transparency-go v1.3.1 // indirect
github.com/google/certificate-transparency-go v1.3.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect
@ -122,12 +123,12 @@ require (
github.com/huandu/xstrings v1.5.0 // indirect
github.com/in-toto/in-toto-golang v0.9.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/itchyny/gojq v0.12.15 // indirect
github.com/itchyny/timefmt-go v0.1.5 // indirect
github.com/jedisct1/go-minisign v0.0.0-20211028175153-1c139d1cc84b // indirect
github.com/itchyny/gojq v0.12.17 // indirect
github.com/itchyny/timefmt-go v0.1.6 // indirect
github.com/jedisct1/go-minisign v0.0.0-20241212093149-d2f9f49435c7 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/letsencrypt/boulder v0.0.0-20240620165639-de9c06129bec // indirect
github.com/letsencrypt/boulder v0.20250630.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
@ -145,48 +146,47 @@ require (
github.com/oklog/ulid v1.3.1 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rodaine/table v1.0.1 // indirect
github.com/rodaine/table v1.3.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sagikazarmark/locafero v0.7.0 // indirect
github.com/sagikazarmark/locafero v0.9.0 // indirect
github.com/sassoftware/relic v7.2.1+incompatible // indirect
github.com/secure-systems-lab/go-securesystemslib v0.9.0 // indirect
github.com/shibumi/go-pathspec v1.3.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 // indirect
github.com/sigstore/rekor v1.3.10 // indirect
github.com/sigstore/sigstore v1.9.4 // indirect
github.com/sigstore/timestamp-authority v1.2.7 // indirect
github.com/sigstore/sigstore v1.9.5 // indirect
github.com/sigstore/timestamp-authority v1.2.8 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.12.0 // indirect
github.com/spf13/cast v1.7.1 // indirect
github.com/spf13/afero v1.14.0 // indirect
github.com/spf13/cast v1.9.2 // indirect
github.com/spf13/viper v1.20.1 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/theupdateframework/go-tuf v0.7.0 // indirect
github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e // indirect
github.com/thlib/go-timezone-local v0.0.6 // indirect
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect
github.com/transparency-dev/merkle v0.0.2 // indirect
github.com/vbatts/tar-split v0.12.1 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yuin/goldmark-emoji v1.0.5 // indirect
go.mongodb.org/mongo-driver v1.14.0 // indirect
github.com/yuin/goldmark-emoji v1.0.6 // indirect
go.mongodb.org/mongo-driver v1.17.4 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel v1.36.0 // indirect
go.opentelemetry.io/otel/metric v1.36.0 // indirect
go.opentelemetry.io/otel/trace v1.36.0 // indirect
go.opentelemetry.io/otel v1.37.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect
go.opentelemetry.io/otel/trace v1.37.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc // indirect
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
golang.org/x/mod v0.25.0 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/tools v0.34.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250414145226-207652e42e2e // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e // indirect
k8s.io/klog/v2 v2.130.1 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
)

372
go.sum
View file

@ -1,19 +1,21 @@
al.essio.dev/pkg/shellescape v1.6.0 h1:NxFcEqzFSEVCGN2yq7Huv/9hyCEGVa/TncnOOBBeXHA=
al.essio.dev/pkg/shellescape v1.6.0/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=
cloud.google.com/go v0.120.0 h1:wc6bgG9DHyKqF5/vQvX1CiZrtHnxJjBlKUyF9nP6meA=
cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit64Q=
cloud.google.com/go/auth v0.16.0 h1:Pd8P1s9WkcrBE2n/PhAwKsdrR35V3Sg2II9B+ndM3CU=
cloud.google.com/go/auth v0.16.0/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI=
cloud.google.com/go/auth v0.16.1 h1:XrXauHMd30LhQYVRHLGvJiYeczweKQXZxsTbV9TiguU=
cloud.google.com/go/auth v0.16.1/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU=
cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo=
cloud.google.com/go/iam v1.5.0 h1:QlLcVMhbLGOjRcGe6VTGGTyQib8dRLK2B/kYNV0+2xs=
cloud.google.com/go/iam v1.5.0/go.mod h1:U+DOtKQltF/LxPEtcDLoobcsZMilSRwR7mgNL7knOpo=
cloud.google.com/go/kms v1.21.2 h1:c/PRUSMNQ8zXrc1sdAUnsenWWaNXN+PzTXfXOcSFdoE=
cloud.google.com/go/kms v1.21.2/go.mod h1:8wkMtHV/9Z8mLXEXr1GK7xPSBdi6knuLXIhqjuWcI6w=
cloud.google.com/go/longrunning v0.6.6 h1:XJNDo5MUfMM05xK3ewpbSdmt7R2Zw+aQEMbdQR65Rbw=
cloud.google.com/go/longrunning v0.6.6/go.mod h1:hyeGJUrPHcx0u2Uu1UFSoYZLn4lkMrccJig0t4FI7yw=
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8=
cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE=
cloud.google.com/go/kms v1.22.0 h1:dBRIj7+GDeeEvatJeTB19oYZNV0aj6wEqSIT/7gLqtk=
cloud.google.com/go/kms v1.22.0/go.mod h1:U7mf8Sva5jpOb4bxYZdtw/9zsbIjrklYwPcvMk34AL8=
cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE=
cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY=
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d h1:zjqpY4C7H15HjRPEenkS4SAn3Jy2eRRjkjZbGR30TOg=
@ -22,8 +24,8 @@ github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkk
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0 h1:OVoM452qUFBrX+URdH3VpR299ma4kfom0yB0URYky9g=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0/go.mod h1:kUjrAo8bgEwLeZ/CmHqNl3Z/kPm7y6FKfxxK0izYUg4=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.0 h1:j8BorDEigD8UFOSZQiSqAMOOleyQOOQPnUAwV+Ls1gA=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.0/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1 h1:Wgf5rZba3YZqeTNJPtvqZoBu1sBN/L4sry+u2U3Y75w=
@ -36,54 +38,52 @@ github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0=
github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.19.0 h1:Im+SLRgT8maArxv81mULDWN8oKxkzboH07CHesxElq4=
github.com/alecthomas/chroma/v2 v2.19.0/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/alessio/shellescape v1.4.2 h1:MHPfaU+ddJ0/bYWpgIeUnQUqKrlJ1S7BfEYPM4uEoM0=
github.com/alessio/shellescape v1.4.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aws/aws-sdk-go v1.55.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk=
github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM=
github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg=
github.com/aws/aws-sdk-go-v2/config v1.29.14 h1:f+eEi/2cKCg9pqKBoAIwRGzVb70MRKqWX4dg1BDcSJM=
github.com/aws/aws-sdk-go-v2/config v1.29.14/go.mod h1:wVPHWcIFv3WO89w0rE10gzf17ZYy+UVS1Geq8Iei34g=
github.com/aws/aws-sdk-go-v2/credentials v1.17.67 h1:9KxtdcIA/5xPNQyZRgUSpYOE6j9Bc4+D7nZua0KGYOM=
github.com/aws/aws-sdk-go-v2/credentials v1.17.67/go.mod h1:p3C44m+cfnbv763s52gCqrjaqyPikj9Sg47kUVaNZQQ=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q=
github.com/aws/aws-sdk-go-v2 v1.36.5 h1:0OF9RiEMEdDdZEMqF9MRjevyxAQcf6gY+E7vwBILFj0=
github.com/aws/aws-sdk-go-v2 v1.36.5/go.mod h1:EYrzvCCN9CMUTa5+6lf6MM4tq3Zjp8UhSGR/cBsjai0=
github.com/aws/aws-sdk-go-v2/config v1.29.17 h1:jSuiQ5jEe4SAMH6lLRMY9OVC+TqJLP5655pBGjmnjr0=
github.com/aws/aws-sdk-go-v2/config v1.29.17/go.mod h1:9P4wwACpbeXs9Pm9w1QTh6BwWwJjwYvJ1iCt5QbCXh8=
github.com/aws/aws-sdk-go-v2/credentials v1.17.70 h1:ONnH5CM16RTXRkS8Z1qg7/s2eDOhHhaXVd72mmyv4/0=
github.com/aws/aws-sdk-go-v2/credentials v1.17.70/go.mod h1:M+lWhhmomVGgtuPOhO85u4pEa3SmssPTdcYpP/5J/xc=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 h1:KAXP9JSHO1vKGCr5f4O6WmlVKLFFXgWYAGoJosorxzU=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32/go.mod h1:h4Sg6FQdexC1yYG9RDnOvLbW1a/P986++/Y/a+GyEM8=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36 h1:SsytQyTMHMDPspp+spo7XwXTP44aJZZAC7fBV2C5+5s=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36/go.mod h1:Q1lnJArKRXkenyog6+Y+zr7WDpk4e6XlR6gs20bbeNo=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36 h1:i2vNHQiXUvKhs3quBR6aqlgJaiaexz/aNvdCktW/kAM=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36/go.mod h1:UdyGa7Q91id/sdyHPwth+043HhmP6yP9MBHgbZM0xo8=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 h1:CXV68E2dNqhuynZJPB80bhPQwAKqBWVer887figW6Jc=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4/go.mod h1:/xFi9KtvBXP97ppCz1TAEvU1Uf66qvid89rbem3wCzQ=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 h1:t0E6FzREdtCsiLIoLCWsYliNsRBgyGD/MCK571qk4MI=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17/go.mod h1:ygpklyoaypuyDvOM5ujWGrYWpAK3h7ugnmKCU/76Ys4=
github.com/aws/aws-sdk-go-v2/service/kms v1.38.3 h1:RivOtUH3eEu6SWnUMFHKAW4MqDOzWn1vGQ3S38Y5QMg=
github.com/aws/aws-sdk-go-v2/service/kms v1.38.3/go.mod h1:cQn6tAF77Di6m4huxovNM7NVAozWTZLsDRp9t8Z/WYk=
github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 h1:1Gw+9ajCV1jogloEv1RRnvfRFia2cL6c9cuKV2Ps+G8=
github.com/aws/aws-sdk-go-v2/service/sso v1.25.3/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 h1:hXmVKytPfTy5axZ+fYbR5d0cFmC3JvwLm5kM83luako=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs=
github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 h1:1XuUZ8mYJw9B6lzAkXhqHlJd/XvaX32evhproijJEZY=
github.com/aws/aws-sdk-go-v2/service/sts v1.33.19/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4=
github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ=
github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
github.com/aws/aws-sdk-go-v2/service/sso v1.25.5 h1:AIRJ3lfb2w/1/8wOOSqYb9fUKGwQbtysJ2H1MofRUPg=
github.com/aws/aws-sdk-go-v2/service/sso v1.25.5/go.mod h1:b7SiVprpU+iGazDUqvRSLf5XmCdn+JtT1on7uNL6Ipc=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 h1:BpOxT3yhLwSJ77qIY3DoHAQjZsc4HEGfMCE4NGy3uFg=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3/go.mod h1:vq/GQR1gOFLquZMSrxUK/cpvKCNVYibNyJ1m7JrU88E=
github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 h1:NFOJ/NXEGV4Rq//71Hs1jC/NvPs1ezajK+yQmkwnPV0=
github.com/aws/aws-sdk-go-v2/service/sts v1.34.0/go.mod h1:7ph2tGpfQvwzgistp2+zga9f+bCjlQJPkPUmMgDSD7w=
github.com/aws/smithy-go v1.22.4 h1:uqXzVZNuNexwc/xrh6Tb56u89WDlJY6HS+KC0S4QSjw=
github.com/aws/smithy-go v1.22.4/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
@ -94,8 +94,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/briandowns/spinner v1.18.1 h1:yhQmQtM1zsqFsouh09Bk/jCjd50pC3EOGsh28gLVvwY=
github.com/briandowns/spinner v1.18.1/go.mod h1:mQak9GHqbspjC/5iUx3qMlIho8xBS/ppAL/hX5SmPJU=
github.com/briandowns/spinner v1.23.2 h1:Zc6ecUnI+YzLmJniCfDNaMbW0Wid1d5+qcTq4L2FW8w=
github.com/briandowns/spinner v1.23.2/go.mod h1:LaZeM4wm2Ywy6vO571mvhQNRcWfRUnXOs0RcKV0wYKM=
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
@ -106,18 +106,18 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI=
github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
github.com/charmbracelet/glamour v0.9.2-0.20250319212134-549f544650e3 h1:hx6E25SvI2WiZdt/gxINcYBnHD7PE2Vr9auqwg5B05g=
github.com/charmbracelet/glamour v0.9.2-0.20250319212134-549f544650e3/go.mod h1:ihVqv4/YOY5Fweu1cxajuQrwJFh3zU4Ukb4mHVNjq3s=
github.com/charmbracelet/bubbletea v1.3.5 h1:JAMNLTbqMOhSwoELIr0qyP4VidFq72/6E9j7HHmRKQc=
github.com/charmbracelet/bubbletea v1.3.5/go.mod h1:TkCnmH+aBd4LrXhXcqrKiYwRs7qyQx5rBgH5fVY3v54=
github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40=
github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0=
github.com/charmbracelet/glamour v0.10.0 h1:MtZvfwsYCx8jEPFJm3rIBFIMZUfUJ765oX8V6kXldcY=
github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk=
github.com/charmbracelet/huh v0.7.0 h1:W8S1uyGETgj9Tuda3/JdVkc3x7DBLZYPZc4c+/rnRdc=
github.com/charmbracelet/huh v0.7.0/go.mod h1:UGC3DZHlgOKHvHC07a5vHag41zzhpPFj34U92sOmyuk=
github.com/charmbracelet/lipgloss v1.1.1-0.20250319133953-166f707985bc h1:nFRtCfZu/zkltd2lsLUPlVNv3ej/Atod9hcdbRZtlys=
github.com/charmbracelet/lipgloss v1.1.1-0.20250319133953-166f707985bc/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA=
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE=
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA=
github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0=
github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U=
@ -126,8 +126,10 @@ github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4=
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
github.com/charmbracelet/x/exp/slice v0.0.0-20250630141444-821143405392 h1:VHLoEcL+kH60a4F8qMsPfOIfWjFE3ciaW4gge2YR3sA=
github.com/charmbracelet/x/exp/slice v0.0.0-20250630141444-821143405392/go.mod h1:vI5nDVMWi6veaYH+0Fmvpbe/+cv/iJfMntdh+N0+Tms=
github.com/charmbracelet/x/exp/strings v0.0.0-20250630141444-821143405392 h1:6ipGA1NEA0AZG2UEf81RQGJvEPvYLn/M18mZcdt4J8g=
github.com/charmbracelet/x/exp/strings v0.0.0-20250630141444-821143405392/go.mod h1:Rgw3/F+xlcUc5XygUtimVSxAqCOsqyvJjqF5UHRvc5k=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
@ -141,8 +143,8 @@ github.com/cli/go-gh/v2 v2.12.1 h1:SVt1/afj5FRAythyMV3WJKaUfDNsxXTIe7arZbwTWKA=
github.com/cli/go-gh/v2 v2.12.1/go.mod h1:+5aXmEOJsH9fc9mBHfincDwnS02j2AIA/DsTH0Bk5uw=
github.com/cli/go-internal v0.0.0-20241025142207-6c48bcd5ce24 h1:QDrhR4JA2n3ij9YQN0u5ZeuvRIIvsUGmf5yPlTS0w8E=
github.com/cli/go-internal v0.0.0-20241025142207-6c48bcd5ce24/go.mod h1:rr9GNING0onuVw8MnracQHn7PcchnFlP882Y0II2KZk=
github.com/cli/oauth v1.1.1 h1:459gD3hSjlKX9B1uXBuiAMdpXBUQ9QGf/NDcCpoQxPs=
github.com/cli/oauth v1.1.1/go.mod h1:qd/FX8ZBD6n1sVNQO3aIdRxeu5LGw9WhKnYhIIoC2A4=
github.com/cli/oauth v1.2.0 h1:9Bb7nWsgi92Xy5Ifa0oKfW6D1+hNAsO6OWSCx7FJdKA=
github.com/cli/oauth v1.2.0/go.mod h1:qd/FX8ZBD6n1sVNQO3aIdRxeu5LGw9WhKnYhIIoC2A4=
github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q=
github.com/cli/safeexec v1.0.1 h1:e/C79PbXF4yYTN/wauC4tviMxEV13BwljGj0N9j+N00=
github.com/cli/safeexec v1.0.1/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q=
@ -158,8 +160,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6N
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/cyberphone/json-canonicalization v0.0.0-20220623050100-57a0ce2678a7 h1:vU+EP9ZuFUCYE0NYLwTSob+3LNEJATzNfP/DC7SWGWI=
github.com/cyberphone/json-canonicalization v0.0.0-20220623050100-57a0ce2678a7/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw=
github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 h1:uX1JmpONuD549D73r6cgnxyUu18Zb7yHAy5AYU0Pm4Q=
github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw=
github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0=
github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -169,14 +171,14 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8Yc
github.com/digitorus/pkcs7 v0.0.0-20230713084857-e76b763bdc49/go.mod h1:SKVExuS+vpu2l9IoOc0RwqE7NYnb0JlcFHFnEJkVDzc=
github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 h1:ge14PCmCvPjpMQMIAH7uKg0lrtNSOdpYsRXlwk3QbaE=
github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352/go.mod h1:SKVExuS+vpu2l9IoOc0RwqE7NYnb0JlcFHFnEJkVDzc=
github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 h1:lxmTCgmHE1GUYL7P0MlNa00M67axePTq+9nBSGddR8I=
github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7/go.mod h1:GvWntX9qiTlOud0WkQ6ewFm0LPy5JUR1Xo0Ngbd1w6Y=
github.com/digitorus/timestamp v0.0.0-20250524132541-c45532741eea h1:ALRwvjsSP53QmnN3Bcj0NpR8SsFLnskny/EIMebAk1c=
github.com/digitorus/timestamp v0.0.0-20250524132541-c45532741eea/go.mod h1:GvWntX9qiTlOud0WkQ6ewFm0LPy5JUR1Xo0Ngbd1w6Y=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/docker/cli v28.2.2+incompatible h1:qzx5BNUDFqlvyq4AHzdNB7gSyVTmU4cgsyN9SdInc1A=
github.com/docker/cli v28.2.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/docker/cli v28.3.0+incompatible h1:s+ttruVLhB5ayeuf2BciwDVxYdKi+RoUlxmwNHV3Vfo=
github.com/docker/cli v28.3.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8=
@ -185,25 +187,24 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell/v2 v2.5.4 h1:TGU4tSjD3sCL788vFNeJnTdzpNKIw1H5dgLnJRQVv/k=
github.com/gdamore/tcell/v2 v2.5.4/go.mod h1:dZgRy5v4iMobMEcWNYBtREnDZAT9DYmfqIkrgEMxLyw=
github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw=
github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo=
github.com/gdamore/tcell/v2 v2.8.1 h1:KPNxyqclpWpWQlPLx6Xui1pMk8S+7+R37h3g07997NU=
github.com/gdamore/tcell/v2 v2.8.1/go.mod h1:bj8ori1BG3OYMjmb3IklZVWfZUJ1UBQt9JXrOCOhGWw=
github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec=
github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
github.com/go-jose/go-jose/v4 v4.1.1 h1:JYhSgy4mXXzAdF3nUx3ygx347LRXJRrpgyU3adRmkAI=
github.com/go-jose/go-jose/v4 v4.1.1/go.mod h1:BdsZGqgdO3b6tTc6LSE56wcDbMMLuPsw5d4ZD5f94kA=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@ -213,8 +214,8 @@ github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC0
github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo=
github.com/go-openapi/errors v0.22.1 h1:kslMRRnK7NCb/CvR1q1VWuEQCEIsBGn5GgKD9e+HYhU=
github.com/go-openapi/errors v0.22.1/go.mod h1:+n/5UdIqdVnLIJ6Q9Se8HNGUXYaY6CN8ImWzfi/Gzp0=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic=
github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk=
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
github.com/go-openapi/loads v0.22.0 h1:ECPGd4jX1U6NApCGG1We+uEozOAvXvJSF4nnwHZ8Aco=
@ -229,22 +230,23 @@ github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZ
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58=
github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ=
github.com/go-sql-driver/mysql v1.9.1 h1:FrjNGn/BsJQjVRuSa8CBrM5BWA9BWoXXat3KrtSb/iI=
github.com/go-sql-driver/mysql v1.9.1/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU=
github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk=
github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/certificate-transparency-go v1.3.1 h1:akbcTfQg0iZlANZLn0L9xOeWtyCIdeoYhKrqi5iH3Go=
github.com/google/certificate-transparency-go v1.3.1/go.mod h1:gg+UQlx6caKEDQ9EElFOujyxEQEfOiQzAt6782Bvi8k=
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/certificate-transparency-go v1.3.2 h1:9ahSNZF2o7SYMaKaXhAumVEzXB2QaayzII9C8rv7v+A=
github.com/google/certificate-transparency-go v1.3.2/go.mod h1:H5FpMUaGa5Ab2+KCYsxg6sELw3Flkl7pGZzWdBoYLXs=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-containerregistry v0.20.6 h1:cvWX87UxxLgaH76b4hIvya6Dzz9qHB31qAwjAohdSTU=
@ -255,14 +257,14 @@ github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/trillian v1.7.1 h1:+zX8jLM3524bAMPS+VxaDIDgsMv3/ty6DuLWerHXcek=
github.com/google/trillian v1.7.1/go.mod h1:E1UMAHqpZCA8AQdrKdWmHmtUfSeiD0sDWD1cv00Xa+c=
github.com/google/trillian v1.7.2 h1:EPBxc4YWY4Ak8tcuhyFleY+zYlbCDCa4Sn24e1Ka8Js=
github.com/google/trillian v1.7.2/go.mod h1:mfQJW4qRH6/ilABtPYNBerVJAJ/upxHLX81zxNQw05s=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q=
github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA=
github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0=
github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
@ -286,8 +288,8 @@ github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4=
github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc=
github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
github.com/hashicorp/go-version v1.3.0 h1:McDWVJIU/y+u1BRV06dPaLfLCaT7fUTJLp5r04x7iNw=
github.com/hashicorp/go-version v1.3.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
@ -310,22 +312,20 @@ github.com/in-toto/in-toto-golang v0.9.0 h1:tHny7ac4KgtsfrG6ybU8gVOZux2H8jN05AXJ
github.com/in-toto/in-toto-golang v0.9.0/go.mod h1:xsBVrVsHNsB61++S6Dy2vWosKhuA3lUTQd+eF9HdeMo=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/itchyny/gojq v0.12.15 h1:WC1Nxbx4Ifw5U2oQWACYz32JK8G9qxNtHzrvW4KEcqI=
github.com/itchyny/gojq v0.12.15/go.mod h1:uWAHCbCIla1jiNxmeT5/B5mOjSdfkCq6p8vxWg+BM10=
github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm6GE=
github.com/itchyny/timefmt-go v0.1.5/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8=
github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 h1:Dj0L5fhJ9F82ZJyVOmBx6msDp/kfd1t9GRfny/mfJA0=
github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds=
github.com/itchyny/gojq v0.12.17 h1:8av8eGduDb5+rvEdaOO+zQUjA04MS0m3Ps8HiD+fceg=
github.com/itchyny/gojq v0.12.17/go.mod h1:WBrEMkgAfAGO1LUcGOckBl5O726KPp+OlkKug0I/FEY=
github.com/itchyny/timefmt-go v0.1.6 h1:ia3s54iciXDdzWzwaVKXZPbiXzxxnv1SPGFfM/myJ5Q=
github.com/itchyny/timefmt-go v0.1.6/go.mod h1:RRDZYC5s9ErkjQvTvvU7keJjxUYzIISJGxm9/mAERQg=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI=
github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jedisct1/go-minisign v0.0.0-20211028175153-1c139d1cc84b h1:ZGiXF8sz7PDk6RgkP+A/SFfUD0ZR/AgG6SpRNEDKZy8=
github.com/jedisct1/go-minisign v0.0.0-20211028175153-1c139d1cc84b/go.mod h1:hQmNrgofl+IY/8L+n20H6E6PWBBTokdsv+q49j0QhsU=
github.com/jedisct1/go-minisign v0.0.0-20241212093149-d2f9f49435c7 h1:FWpSWRD8FbVkKQu8M1DM9jF5oXFLyE+XpisIYfdzbic=
github.com/jedisct1/go-minisign v0.0.0-20241212093149-d2f9f49435c7/go.mod h1:BMxO138bOokdgt4UaxZiEfypcSHX0t6SIFimVP1oRfk=
github.com/jellydator/ttlcache/v3 v3.3.0 h1:BdoC9cE81qXfrxeb9eoJi9dWrdhSuwXMAnHTbnBm4Wc=
github.com/jellydator/ttlcache/v3 v3.3.0/go.mod h1:bj2/e0l4jRnQdrnSTaGTsh4GSXvMjQcy41i7th0GVGw=
github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 h1:liMMTbpW34dhU4az1GN0pTPADwNmvoRSeoZ6PItiqnY=
@ -348,8 +348,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
github.com/letsencrypt/boulder v0.0.0-20240620165639-de9c06129bec h1:2tTW6cDth2TSgRbAhD7yjZzTQmcN25sDRPEeinR51yQ=
github.com/letsencrypt/boulder v0.0.0-20240620165639-de9c06129bec/go.mod h1:TmwEoGCwIti7BCeJ9hescZgRtatxRE+A72pCoPfmcfk=
github.com/letsencrypt/boulder v0.20250630.0 h1:dD3llgKuZWuJZwqzT6weaEcCLSMEBJkIkQ5OdLkK2OA=
github.com/letsencrypt/boulder v0.20250630.0/go.mod h1:8FCmFZoomZMKQSid72Jhke4h08xFnhoiZz8OQysKazE=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
@ -362,9 +362,7 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
@ -392,8 +390,8 @@ github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/muhammadmuzzammil1998/jsonc v0.0.0-20201229145248-615b0916ca38 h1:0FrBxrkJ0hVembTb/e4EU5Ml6vLcOusAqymmYISg5Uo=
github.com/muhammadmuzzammil1998/jsonc v0.0.0-20201229145248-615b0916ca38/go.mod h1:saF2fIVw4banK0H4+/EuqfFLpRnoy5S+ECwTOCcRcSU=
github.com/muhammadmuzzammil1998/jsonc v1.0.0 h1:8o5gBQn4ZA3NBA9DlTujCj2a4w0tqWrPVjDwhzkgTIs=
github.com/muhammadmuzzammil1998/jsonc v1.0.0/go.mod h1:saF2fIVw4banK0H4+/EuqfFLpRnoy5S+ECwTOCcRcSU=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4=
@ -406,8 +404,8 @@ github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJw
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@ -423,22 +421,23 @@ github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/rivo/tview v0.0.0-20221029100920-c4a7e501810d h1:jKIUJdMcIVGOSHi6LSqJqw9RqblyblE2ZrHvFbWR3S0=
github.com/rivo/tview v0.0.0-20221029100920-c4a7e501810d/go.mod h1:YX2wUZOcJGOIycErz2s9KvDaP0jnWwRCirQMPLPpQ+Y=
github.com/rivo/tview v0.0.0-20250625164341-a4a78f1e05cb h1:n7UJ8X9UnrTZBYXnd1kAIBc067SWyuPIrsocjketYW8=
github.com/rivo/tview v0.0.0-20250625164341-a4a78f1e05cb/go.mod h1:cSfIYfhpSGCjp3r/ECJb+GKS7cGJnqV8vfjQPwoXyfY=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rodaine/table v1.0.1 h1:U/VwCnUxlVYxw8+NJiLIuCxA/xa6jL38MY3FYysVWWQ=
github.com/rodaine/table v1.0.1/go.mod h1:UVEtfBsflpeEcD56nF4F5AocNFta0ZuolpSVdPtlmP4=
github.com/rodaine/table v1.3.0 h1:4/3S3SVkHnVZX91EHFvAMV7K42AnJ0XuymRR2C5HlGE=
github.com/rodaine/table v1.3.0/go.mod h1:47zRsHar4zw0jgxGxL9YtFfs7EGN6B/TaS+/Dmk4WxU=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k=
github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
github.com/sassoftware/relic v7.2.1+incompatible h1:Pwyh1F3I0r4clFJXkSI8bOyJINGqpgjJU3DYAZeI05A=
github.com/sassoftware/relic v7.2.1+incompatible/go.mod h1:CWfAxv73/iLZ17rbyhIEq3K9hs5w6FpNMdUT//qR+zk=
github.com/sassoftware/relic/v7 v7.6.2 h1:rS44Lbv9G9eXsukknS4mSjIAuuX+lMq/FnStgmZlUv4=
@ -451,16 +450,16 @@ github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh
github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh5tVaaMCl3jE=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/shurcooL/githubv4 v0.0.0-20240120211514-18a1ae0e79dc h1:vH0NQbIDk+mJLvBliNGfcQgUmhlniWBDXC79oRxfZA0=
github.com/shurcooL/githubv4 v0.0.0-20240120211514-18a1ae0e79dc/go.mod h1:zqMwyHmnN/eDOZOdiTohqIUKUrTFX62PNlu7IJdu0q8=
github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7 h1:cYCy18SHPKRkvclm+pWm1Lk4YrREb4IOIb/YdFO0p2M=
github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7/go.mod h1:zqMwyHmnN/eDOZOdiTohqIUKUrTFX62PNlu7IJdu0q8=
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 h1:17JxqqJY66GmZVHkmAsGEkcIu0oCe3AM420QDgGwZx0=
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466/go.mod h1:9dIRpgIY7hVhoqfe0/FcYp0bpInZaT7dc3BYOprrIUE=
github.com/sigstore/protobuf-specs v0.4.3 h1:kRgJ+ciznipH9xhrkAbAEHuuxD3GhYnGC873gZpjJT4=
github.com/sigstore/protobuf-specs v0.4.3/go.mod h1:+gXR+38nIa2oEupqDdzg4qSBT0Os+sP7oYv6alWewWc=
github.com/sigstore/rekor v1.3.10 h1:/mSvRo4MZ/59ECIlARhyykAlQlkmeAQpvBPlmJtZOCU=
github.com/sigstore/rekor v1.3.10/go.mod h1:JvryKJ40O0XA48MdzYUPu0y4fyvqt0C4iSY7ri9iu3A=
github.com/sigstore/sigstore v1.9.4 h1:64+OGed80+A4mRlNzRd055vFcgBeDghjZw24rPLZgDU=
github.com/sigstore/sigstore v1.9.4/go.mod h1:Q7tGTC3gbtK7c3jcxEmGc2MmK4rRpIRzi3bxRFWKvEY=
github.com/sigstore/sigstore v1.9.5 h1:Wm1LT9yF4LhQdEMy5A2JeGRHTrAWGjT3ubE5JUSrGVU=
github.com/sigstore/sigstore v1.9.5/go.mod h1:VtxgvGqCmEZN9X2zhFSOkfXxvKUjpy8RpUW39oCtoII=
github.com/sigstore/sigstore-go v1.0.0 h1:4N07S2zLxf09nTRwaPKyAxbKzpM8WJYUS8lWWaYxneU=
github.com/sigstore/sigstore-go v1.0.0/go.mod h1:UYsZ/XHE4eltv1o1Lu+n6poW1Z5to3f0+emvfXNxIN8=
github.com/sigstore/sigstore/pkg/signature/kms/aws v1.9.4 h1:kQqUJ1VuWdJltMkinFXAHTlJrzMRPoNgL+dy6WyJ/dA=
@ -471,16 +470,16 @@ github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.9.4 h1:C2nSyTmTxpuamUmLCWW
github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.9.4/go.mod h1:vjDahU0sEw/WMkKkygZNH72EMg86iaFNLAaJFXhItXU=
github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.9.4 h1:t9yfb6yteIDv8CNRT6OHdqgTV6TSj+CdOtZP9dVhpsQ=
github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.9.4/go.mod h1:m7sQxVJmDa+rsmS1m6biQxaLX83pzNS7ThUEyjOqkCU=
github.com/sigstore/timestamp-authority v1.2.7 h1:HP/VT4wnL4uzP0fVo3eHXlt0reuNgW3PLt78+BV0I5I=
github.com/sigstore/timestamp-authority v1.2.7/go.mod h1:te4ThQ3Q/CX1bzVsf5mMN0K7Z/cgc2OcoEGxAJiFqqI=
github.com/sigstore/timestamp-authority v1.2.8 h1:BEV3fkphwU4zBp3allFAhCqQb99HkiyCXB853RIwuEE=
github.com/sigstore/timestamp-authority v1.2.8/go.mod h1:G2/0hAZmLPnevEwT1S9IvtNHUm9Ktzvso6xuRhl94ZY=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE=
github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
@ -488,11 +487,17 @@ github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
@ -501,8 +506,8 @@ github.com/theupdateframework/go-tuf v0.7.0 h1:CqbQFrWo1ae3/I0UCblSbczevCCbS31Qv
github.com/theupdateframework/go-tuf v0.7.0/go.mod h1:uEB7WSY+7ZIugK6R1hiBMBjQftaFzn7ZCDJcp1tCUug=
github.com/theupdateframework/go-tuf/v2 v2.1.1 h1:OWcoHItwsGO+7m0wLa7FDWPR4oB1cj0zOr1kosE4G+I=
github.com/theupdateframework/go-tuf/v2 v2.1.1/go.mod h1:V675cQGhZONR0OGQ8r1feO0uwtsTBYPDWHzAAPn5rjE=
github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e h1:BuzhfgfWQbX0dWzYzT1zsORLnHRv3bcRcsaUk0VmXA8=
github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e/go.mod h1:/Tnicc6m/lsJE0irFMA0LfIwTBo4QP7A8IfyIv4zZKI=
github.com/thlib/go-timezone-local v0.0.6 h1:Ii3QJ4FhosL/+eCZl6Hsdr4DDU4tfevNoV83yAEo2tU=
github.com/thlib/go-timezone-local v0.0.6/go.mod h1:/Tnicc6m/lsJE0irFMA0LfIwTBo4QP7A8IfyIv4zZKI=
github.com/tink-crypto/tink-go-awskms/v2 v2.1.0 h1:N9UxlsOzu5mttdjhxkDLbzwtEecuXmlxZVo/ds7JKJI=
github.com/tink-crypto/tink-go-awskms/v2 v2.1.0/go.mod h1:PxSp9GlOkKL9rlybW804uspnHuO9nbD98V/fDX4uSis=
github.com/tink-crypto/tink-go-gcpkms/v2 v2.2.0 h1:3B9i6XBXNTRspfkTC0asN5W0K6GhOSgcujNiECNRNb0=
@ -520,33 +525,32 @@ github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVO
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark v1.7.12 h1:YwGP/rrea2/CnCtUHgjuolG/PnMxdQtPMO5PvaE2/nY=
github.com/yuin/goldmark v1.7.12/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk=
github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
github.com/zalando/go-keyring v0.2.5 h1:Bc2HHpjALryKD62ppdEzaFG6VxL6Bc+5v0LYpN8Lba8=
github.com/zalando/go-keyring v0.2.5/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk=
go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80=
go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c=
github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs=
github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA=
github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s=
github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI=
go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw=
go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
go.step.sm/crypto v0.63.0 h1:U1QGELQqJ85oDfeNFE2V52cow1rvy0m3MekG3wFmyXY=
go.step.sm/crypto v0.63.0/go.mod h1:aj3LETmCZeSil1DMq3BlbhDBcN86+mmKrHZtXWyc0L4=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.step.sm/crypto v0.66.0 h1:9TW6BEguOtcS9NIjja9bDQ+j8OjhenU/F6lJfHjbXNU=
go.step.sm/crypto v0.66.0/go.mod h1:anqGyvO/Px05D1mznHq4/a9wwP1I1DmMZvk+TWX5Dzo=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
@ -555,22 +559,39 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc h1:O9NuF4s+E/PvMIy+9IUZB9znFwUIXEWSstNjek6VpVg=
golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -582,18 +603,36 @@ golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
@ -601,19 +640,22 @@ golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.230.0 h1:2u1hni3E+UXAXrONrrkfWpi/V6cyKVAbfGVeGtC3OxM=
google.golang.org/api v0.230.0/go.mod h1:aqvtoMk7YkiXx+6U12arQFExiRV9D/ekvMCwCd/TksQ=
google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb h1:ITgPrl429bc6+2ZraNSzMDk3I95nmQln2fuPstKwFDE=
google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:sAo5UzpjUwgFBCzupwhcLcxHVDK7vG5IqI30YnwX2eE=
google.golang.org/genproto/googleapis/api v0.0.0-20250414145226-207652e42e2e h1:UdXH7Kzbj+Vzastr5nVfccbmFsmYNygVLSPk1pEfDoY=
google.golang.org/genproto/googleapis/api v0.0.0-20250414145226-207652e42e2e/go.mod h1:085qFyf2+XaZlRdCgKNCIZ3afY2p4HHZdoIRpId8F4A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e h1:ztQaXfzEXTmCBvbtWYRhJxW+0iJcz2qXfd38/e9l7bA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8=
google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
google.golang.org/api v0.234.0 h1:d3sAmYq3E9gdr2mpmiWGbm9pHsA/KJmyiLkwKfHBqU4=
google.golang.org/api v0.234.0/go.mod h1:QpeJkemzkFKe5VCE/PMv7GsUfn9ZF+u+q1Q7w6ckxTg=
google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 h1:1tXaIXCracvtsRxSBsYDiSBN0cuJvM7QYW+MrpIRY78=
google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:49MsLSx0oWMOZqcpB3uL8ZOkAh1+TndpJ8ONoCBWiZk=
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View file

@ -7,64 +7,65 @@ The following open source dependencies are used to build the [cli/cli][] GitHub
Some packages may only be included on certain architectures or operating systems.
- [dario.cat/mergo](https://pkg.go.dev/dario.cat/mergo) ([BSD-3-Clause](https://github.com/imdario/mergo/blob/v1.0.1/LICENSE))
- [al.essio.dev/pkg/shellescape](https://pkg.go.dev/al.essio.dev/pkg/shellescape) ([MIT](https://github.com/alessio/shellescape/blob/v1.6.0/LICENSE))
- [dario.cat/mergo](https://pkg.go.dev/dario.cat/mergo) ([BSD-3-Clause](https://github.com/imdario/mergo/blob/v1.0.2/LICENSE))
- [github.com/AlecAivazis/survey/v2](https://pkg.go.dev/github.com/AlecAivazis/survey/v2) ([MIT](https://github.com/AlecAivazis/survey/blob/v2.3.7/LICENSE))
- [github.com/AlecAivazis/survey/v2/terminal](https://pkg.go.dev/github.com/AlecAivazis/survey/v2/terminal) ([MIT](https://github.com/AlecAivazis/survey/blob/v2.3.7/terminal/LICENSE.txt))
- [github.com/MakeNowJust/heredoc](https://pkg.go.dev/github.com/MakeNowJust/heredoc) ([MIT](https://github.com/MakeNowJust/heredoc/blob/v1.0.0/LICENSE))
- [github.com/Masterminds/goutils](https://pkg.go.dev/github.com/Masterminds/goutils) ([Apache-2.0](https://github.com/Masterminds/goutils/blob/v1.1.1/LICENSE.txt))
- [github.com/Masterminds/semver/v3](https://pkg.go.dev/github.com/Masterminds/semver/v3) ([MIT](https://github.com/Masterminds/semver/blob/v3.3.0/LICENSE.txt))
- [github.com/Masterminds/semver/v3](https://pkg.go.dev/github.com/Masterminds/semver/v3) ([MIT](https://github.com/Masterminds/semver/blob/v3.4.0/LICENSE.txt))
- [github.com/Masterminds/sprig/v3](https://pkg.go.dev/github.com/Masterminds/sprig/v3) ([MIT](https://github.com/Masterminds/sprig/blob/v3.3.0/LICENSE.txt))
- [github.com/alecthomas/chroma/v2](https://pkg.go.dev/github.com/alecthomas/chroma/v2) ([MIT](https://github.com/alecthomas/chroma/blob/v2.14.0/COPYING))
- [github.com/alessio/shellescape](https://pkg.go.dev/github.com/alessio/shellescape) ([MIT](https://github.com/alessio/shellescape/blob/v1.4.2/LICENSE))
- [github.com/alecthomas/chroma/v2](https://pkg.go.dev/github.com/alecthomas/chroma/v2) ([MIT](https://github.com/alecthomas/chroma/blob/v2.19.0/COPYING))
- [github.com/asaskevich/govalidator](https://pkg.go.dev/github.com/asaskevich/govalidator) ([MIT](https://github.com/asaskevich/govalidator/blob/a9d515a09cc2/LICENSE))
- [github.com/atotto/clipboard](https://pkg.go.dev/github.com/atotto/clipboard) ([BSD-3-Clause](https://github.com/atotto/clipboard/blob/v0.1.4/LICENSE))
- [github.com/aymanbagabas/go-osc52/v2](https://pkg.go.dev/github.com/aymanbagabas/go-osc52/v2) ([MIT](https://github.com/aymanbagabas/go-osc52/blob/v2.0.1/LICENSE))
- [github.com/aymerick/douceur](https://pkg.go.dev/github.com/aymerick/douceur) ([MIT](https://github.com/aymerick/douceur/blob/v0.2.0/LICENSE))
- [github.com/blang/semver](https://pkg.go.dev/github.com/blang/semver) ([MIT](https://github.com/blang/semver/blob/v3.5.1/LICENSE))
- [github.com/briandowns/spinner](https://pkg.go.dev/github.com/briandowns/spinner) ([Apache-2.0](https://github.com/briandowns/spinner/blob/v1.18.1/LICENSE))
- [github.com/briandowns/spinner](https://pkg.go.dev/github.com/briandowns/spinner) ([Apache-2.0](https://github.com/briandowns/spinner/blob/v1.23.2/LICENSE))
- [github.com/catppuccin/go](https://pkg.go.dev/github.com/catppuccin/go) ([MIT](https://github.com/catppuccin/go/blob/v0.3.0/LICENSE))
- [github.com/cenkalti/backoff/v4](https://pkg.go.dev/github.com/cenkalti/backoff/v4) ([MIT](https://github.com/cenkalti/backoff/blob/v4.3.0/LICENSE))
- [github.com/cenkalti/backoff/v5](https://pkg.go.dev/github.com/cenkalti/backoff/v5) ([MIT](https://github.com/cenkalti/backoff/blob/v5.0.2/LICENSE))
- [github.com/charmbracelet/bubbles](https://pkg.go.dev/github.com/charmbracelet/bubbles) ([MIT](https://github.com/charmbracelet/bubbles/blob/v0.21.0/LICENSE))
- [github.com/charmbracelet/bubbletea](https://pkg.go.dev/github.com/charmbracelet/bubbletea) ([MIT](https://github.com/charmbracelet/bubbletea/blob/v1.3.4/LICENSE))
- [github.com/charmbracelet/colorprofile](https://pkg.go.dev/github.com/charmbracelet/colorprofile) ([MIT](https://github.com/charmbracelet/colorprofile/blob/f60798e515dc/LICENSE))
- [github.com/charmbracelet/glamour](https://pkg.go.dev/github.com/charmbracelet/glamour) ([MIT](https://github.com/charmbracelet/glamour/blob/549f544650e3/LICENSE))
- [github.com/charmbracelet/bubbletea](https://pkg.go.dev/github.com/charmbracelet/bubbletea) ([MIT](https://github.com/charmbracelet/bubbletea/blob/v1.3.5/LICENSE))
- [github.com/charmbracelet/colorprofile](https://pkg.go.dev/github.com/charmbracelet/colorprofile) ([MIT](https://github.com/charmbracelet/colorprofile/blob/v0.3.1/LICENSE))
- [github.com/charmbracelet/glamour](https://pkg.go.dev/github.com/charmbracelet/glamour) ([MIT](https://github.com/charmbracelet/glamour/blob/v0.10.0/LICENSE))
- [github.com/charmbracelet/huh](https://pkg.go.dev/github.com/charmbracelet/huh) ([MIT](https://github.com/charmbracelet/huh/blob/v0.7.0/LICENSE))
- [github.com/charmbracelet/lipgloss](https://pkg.go.dev/github.com/charmbracelet/lipgloss) ([MIT](https://github.com/charmbracelet/lipgloss/blob/166f707985bc/LICENSE))
- [github.com/charmbracelet/x/ansi](https://pkg.go.dev/github.com/charmbracelet/x/ansi) ([MIT](https://github.com/charmbracelet/x/blob/ansi/v0.8.0/ansi/LICENSE))
- [github.com/charmbracelet/lipgloss](https://pkg.go.dev/github.com/charmbracelet/lipgloss) ([MIT](https://github.com/charmbracelet/lipgloss/blob/76690c660834/LICENSE))
- [github.com/charmbracelet/x/ansi](https://pkg.go.dev/github.com/charmbracelet/x/ansi) ([MIT](https://github.com/charmbracelet/x/blob/ansi/v0.9.3/ansi/LICENSE))
- [github.com/charmbracelet/x/cellbuf](https://pkg.go.dev/github.com/charmbracelet/x/cellbuf) ([MIT](https://github.com/charmbracelet/x/blob/cellbuf/v0.0.13/cellbuf/LICENSE))
- [github.com/charmbracelet/x/exp/strings](https://pkg.go.dev/github.com/charmbracelet/x/exp/strings) ([MIT](https://github.com/charmbracelet/x/blob/212f7b056ed0/exp/strings/LICENSE))
- [github.com/charmbracelet/x/exp/slice](https://pkg.go.dev/github.com/charmbracelet/x/exp/slice) ([MIT](https://github.com/charmbracelet/x/blob/821143405392/exp/slice/LICENSE))
- [github.com/charmbracelet/x/exp/strings](https://pkg.go.dev/github.com/charmbracelet/x/exp/strings) ([MIT](https://github.com/charmbracelet/x/blob/821143405392/exp/strings/LICENSE))
- [github.com/charmbracelet/x/term](https://pkg.go.dev/github.com/charmbracelet/x/term) ([MIT](https://github.com/charmbracelet/x/blob/term/v0.2.1/term/LICENSE))
- [github.com/cli/browser](https://pkg.go.dev/github.com/cli/browser) ([BSD-2-Clause](https://github.com/cli/browser/blob/v1.3.0/LICENSE))
- [github.com/cli/go-gh/v2](https://pkg.go.dev/github.com/cli/go-gh/v2) ([MIT](https://github.com/cli/go-gh/blob/v2.12.1/LICENSE))
- [github.com/cli/oauth](https://pkg.go.dev/github.com/cli/oauth) ([MIT](https://github.com/cli/oauth/blob/v1.1.1/LICENSE))
- [github.com/cli/oauth](https://pkg.go.dev/github.com/cli/oauth) ([MIT](https://github.com/cli/oauth/blob/v1.2.0/LICENSE))
- [github.com/cli/safeexec](https://pkg.go.dev/github.com/cli/safeexec) ([BSD-2-Clause](https://github.com/cli/safeexec/blob/v1.0.1/LICENSE))
- [github.com/cli/shurcooL-graphql](https://pkg.go.dev/github.com/cli/shurcooL-graphql) ([MIT](https://github.com/cli/shurcooL-graphql/blob/v0.0.4/LICENSE))
- [github.com/containerd/stargz-snapshotter/estargz](https://pkg.go.dev/github.com/containerd/stargz-snapshotter/estargz) ([Apache-2.0](https://github.com/containerd/stargz-snapshotter/blob/estargz/v0.16.3/estargz/LICENSE))
- [github.com/cpuguy83/go-md2man/v2/md2man](https://pkg.go.dev/github.com/cpuguy83/go-md2man/v2/md2man) ([MIT](https://github.com/cpuguy83/go-md2man/blob/v2.0.7/LICENSE.md))
- [github.com/cyberphone/json-canonicalization/go/src/webpki.org/jsoncanonicalizer](https://pkg.go.dev/github.com/cyberphone/json-canonicalization/go/src/webpki.org/jsoncanonicalizer) ([Apache-2.0](https://github.com/cyberphone/json-canonicalization/blob/57a0ce2678a7/LICENSE))
- [github.com/cyberphone/json-canonicalization/go/src/webpki.org/jsoncanonicalizer](https://pkg.go.dev/github.com/cyberphone/json-canonicalization/go/src/webpki.org/jsoncanonicalizer) ([Apache-2.0](https://github.com/cyberphone/json-canonicalization/blob/19d51d7fe467/LICENSE))
- [github.com/davecgh/go-spew/spew](https://pkg.go.dev/github.com/davecgh/go-spew/spew) ([ISC](https://github.com/davecgh/go-spew/blob/d8f796af33cc/LICENSE))
- [github.com/digitorus/pkcs7](https://pkg.go.dev/github.com/digitorus/pkcs7) ([MIT](https://github.com/digitorus/pkcs7/blob/3a137a874352/LICENSE))
- [github.com/digitorus/timestamp](https://pkg.go.dev/github.com/digitorus/timestamp) ([BSD-2-Clause](https://github.com/digitorus/timestamp/blob/220c5c2851b7/LICENSE))
- [github.com/digitorus/timestamp](https://pkg.go.dev/github.com/digitorus/timestamp) ([BSD-2-Clause](https://github.com/digitorus/timestamp/blob/c45532741eea/LICENSE))
- [github.com/distribution/reference](https://pkg.go.dev/github.com/distribution/reference) ([Apache-2.0](https://github.com/distribution/reference/blob/v0.6.0/LICENSE))
- [github.com/dlclark/regexp2](https://pkg.go.dev/github.com/dlclark/regexp2) ([MIT](https://github.com/dlclark/regexp2/blob/v1.11.0/LICENSE))
- [github.com/docker/cli/cli/config](https://pkg.go.dev/github.com/docker/cli/cli/config) ([Apache-2.0](https://github.com/docker/cli/blob/v28.2.2/LICENSE))
- [github.com/dlclark/regexp2](https://pkg.go.dev/github.com/dlclark/regexp2) ([MIT](https://github.com/dlclark/regexp2/blob/v1.11.5/LICENSE))
- [github.com/docker/cli/cli/config](https://pkg.go.dev/github.com/docker/cli/cli/config) ([Apache-2.0](https://github.com/docker/cli/blob/v28.3.0/LICENSE))
- [github.com/docker/distribution/registry/client/auth/challenge](https://pkg.go.dev/github.com/docker/distribution/registry/client/auth/challenge) ([Apache-2.0](https://github.com/docker/distribution/blob/v2.8.3/LICENSE))
- [github.com/docker/docker-credential-helpers](https://pkg.go.dev/github.com/docker/docker-credential-helpers) ([MIT](https://github.com/docker/docker-credential-helpers/blob/v0.9.3/LICENSE))
- [github.com/dustin/go-humanize](https://pkg.go.dev/github.com/dustin/go-humanize) ([MIT](https://github.com/dustin/go-humanize/blob/v1.0.1/LICENSE))
- [github.com/fatih/color](https://pkg.go.dev/github.com/fatih/color) ([MIT](https://github.com/fatih/color/blob/v1.16.0/LICENSE.md))
- [github.com/fsnotify/fsnotify](https://pkg.go.dev/github.com/fsnotify/fsnotify) ([BSD-3-Clause](https://github.com/fsnotify/fsnotify/blob/v1.8.0/LICENSE))
- [github.com/fatih/color](https://pkg.go.dev/github.com/fatih/color) ([MIT](https://github.com/fatih/color/blob/v1.18.0/LICENSE.md))
- [github.com/fsnotify/fsnotify](https://pkg.go.dev/github.com/fsnotify/fsnotify) ([BSD-3-Clause](https://github.com/fsnotify/fsnotify/blob/v1.9.0/LICENSE))
- [github.com/gabriel-vasile/mimetype](https://pkg.go.dev/github.com/gabriel-vasile/mimetype) ([MIT](https://github.com/gabriel-vasile/mimetype/blob/v1.4.9/LICENSE))
- [github.com/gdamore/encoding](https://pkg.go.dev/github.com/gdamore/encoding) ([Apache-2.0](https://github.com/gdamore/encoding/blob/v1.0.0/LICENSE))
- [github.com/gdamore/tcell/v2](https://pkg.go.dev/github.com/gdamore/tcell/v2) ([Apache-2.0](https://github.com/gdamore/tcell/blob/v2.5.4/LICENSE))
- [github.com/gdamore/encoding](https://pkg.go.dev/github.com/gdamore/encoding) ([Apache-2.0](https://github.com/gdamore/encoding/blob/v1.0.1/LICENSE))
- [github.com/gdamore/tcell/v2](https://pkg.go.dev/github.com/gdamore/tcell/v2) ([Apache-2.0](https://github.com/gdamore/tcell/blob/v2.8.1/LICENSE))
- [github.com/go-chi/chi](https://pkg.go.dev/github.com/go-chi/chi) ([MIT](https://github.com/go-chi/chi/blob/v4.1.2/LICENSE))
- [github.com/go-jose/go-jose/v4](https://pkg.go.dev/github.com/go-jose/go-jose/v4) ([Apache-2.0](https://github.com/go-jose/go-jose/blob/v4.0.5/LICENSE))
- [github.com/go-jose/go-jose/v4/json](https://pkg.go.dev/github.com/go-jose/go-jose/v4/json) ([BSD-3-Clause](https://github.com/go-jose/go-jose/blob/v4.0.5/json/LICENSE))
- [github.com/go-jose/go-jose/v4](https://pkg.go.dev/github.com/go-jose/go-jose/v4) ([Apache-2.0](https://github.com/go-jose/go-jose/blob/v4.1.1/LICENSE))
- [github.com/go-jose/go-jose/v4/json](https://pkg.go.dev/github.com/go-jose/go-jose/v4/json) ([BSD-3-Clause](https://github.com/go-jose/go-jose/blob/v4.1.1/json/LICENSE))
- [github.com/go-logr/logr](https://pkg.go.dev/github.com/go-logr/logr) ([Apache-2.0](https://github.com/go-logr/logr/blob/v1.4.3/LICENSE))
- [github.com/go-logr/stdr](https://pkg.go.dev/github.com/go-logr/stdr) ([Apache-2.0](https://github.com/go-logr/stdr/blob/v1.2.2/LICENSE))
- [github.com/go-openapi/analysis](https://pkg.go.dev/github.com/go-openapi/analysis) ([Apache-2.0](https://github.com/go-openapi/analysis/blob/v0.23.0/LICENSE))
- [github.com/go-openapi/errors](https://pkg.go.dev/github.com/go-openapi/errors) ([Apache-2.0](https://github.com/go-openapi/errors/blob/v0.22.1/LICENSE))
- [github.com/go-openapi/jsonpointer](https://pkg.go.dev/github.com/go-openapi/jsonpointer) ([Apache-2.0](https://github.com/go-openapi/jsonpointer/blob/v0.21.0/LICENSE))
- [github.com/go-openapi/jsonpointer](https://pkg.go.dev/github.com/go-openapi/jsonpointer) ([Apache-2.0](https://github.com/go-openapi/jsonpointer/blob/v0.21.1/LICENSE))
- [github.com/go-openapi/jsonreference](https://pkg.go.dev/github.com/go-openapi/jsonreference) ([Apache-2.0](https://github.com/go-openapi/jsonreference/blob/v0.21.0/LICENSE))
- [github.com/go-openapi/loads](https://pkg.go.dev/github.com/go-openapi/loads) ([Apache-2.0](https://github.com/go-openapi/loads/blob/v0.22.0/LICENSE))
- [github.com/go-openapi/runtime](https://pkg.go.dev/github.com/go-openapi/runtime) ([Apache-2.0](https://github.com/go-openapi/runtime/blob/v0.28.0/LICENSE))
@ -73,9 +74,9 @@ Some packages may only be included on certain architectures or operating systems
- [github.com/go-openapi/strfmt](https://pkg.go.dev/github.com/go-openapi/strfmt) ([Apache-2.0](https://github.com/go-openapi/strfmt/blob/v0.23.0/LICENSE))
- [github.com/go-openapi/swag](https://pkg.go.dev/github.com/go-openapi/swag) ([Apache-2.0](https://github.com/go-openapi/swag/blob/v0.23.1/LICENSE))
- [github.com/go-openapi/validate](https://pkg.go.dev/github.com/go-openapi/validate) ([Apache-2.0](https://github.com/go-openapi/validate/blob/v0.24.0/LICENSE))
- [github.com/go-viper/mapstructure/v2](https://pkg.go.dev/github.com/go-viper/mapstructure/v2) ([MIT](https://github.com/go-viper/mapstructure/blob/v2.2.1/LICENSE))
- [github.com/golang/snappy](https://pkg.go.dev/github.com/golang/snappy) ([BSD-3-Clause](https://github.com/golang/snappy/blob/v0.0.4/LICENSE))
- [github.com/google/certificate-transparency-go](https://pkg.go.dev/github.com/google/certificate-transparency-go) ([Apache-2.0](https://github.com/google/certificate-transparency-go/blob/v1.3.1/LICENSE))
- [github.com/go-viper/mapstructure/v2](https://pkg.go.dev/github.com/go-viper/mapstructure/v2) ([MIT](https://github.com/go-viper/mapstructure/blob/v2.3.0/LICENSE))
- [github.com/golang/snappy](https://pkg.go.dev/github.com/golang/snappy) ([BSD-3-Clause](https://github.com/golang/snappy/blob/v1.0.0/LICENSE))
- [github.com/google/certificate-transparency-go](https://pkg.go.dev/github.com/google/certificate-transparency-go) ([Apache-2.0](https://github.com/google/certificate-transparency-go/blob/v1.3.2/LICENSE))
- [github.com/google/go-containerregistry](https://pkg.go.dev/github.com/google/go-containerregistry) ([Apache-2.0](https://github.com/google/go-containerregistry/blob/v0.20.6/LICENSE))
- [github.com/google/shlex](https://pkg.go.dev/github.com/google/shlex) ([Apache-2.0](https://github.com/google/shlex/blob/e7afc7fbc510/COPYING))
- [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.6.0/LICENSE))
@ -83,21 +84,21 @@ Some packages may only be included on certain architectures or operating systems
- [github.com/gorilla/websocket](https://pkg.go.dev/github.com/gorilla/websocket) ([BSD-2-Clause](https://github.com/gorilla/websocket/blob/v1.5.3/LICENSE))
- [github.com/hashicorp/errwrap](https://pkg.go.dev/github.com/hashicorp/errwrap) ([MPL-2.0](https://github.com/hashicorp/errwrap/blob/v1.1.0/LICENSE))
- [github.com/hashicorp/go-multierror](https://pkg.go.dev/github.com/hashicorp/go-multierror) ([MPL-2.0](https://github.com/hashicorp/go-multierror/blob/v1.1.1/LICENSE))
- [github.com/hashicorp/go-version](https://pkg.go.dev/github.com/hashicorp/go-version) ([MPL-2.0](https://github.com/hashicorp/go-version/blob/v1.3.0/LICENSE))
- [github.com/hashicorp/go-version](https://pkg.go.dev/github.com/hashicorp/go-version) ([MPL-2.0](https://github.com/hashicorp/go-version/blob/v1.7.0/LICENSE))
- [github.com/henvic/httpretty](https://pkg.go.dev/github.com/henvic/httpretty) ([MIT](https://github.com/henvic/httpretty/blob/v0.1.4/LICENSE.md))
- [github.com/huandu/xstrings](https://pkg.go.dev/github.com/huandu/xstrings) ([MIT](https://github.com/huandu/xstrings/blob/v1.5.0/LICENSE))
- [github.com/in-toto/attestation/go/v1](https://pkg.go.dev/github.com/in-toto/attestation/go/v1) ([Apache-2.0](https://github.com/in-toto/attestation/blob/v1.1.2/LICENSE))
- [github.com/in-toto/in-toto-golang/in_toto](https://pkg.go.dev/github.com/in-toto/in-toto-golang/in_toto) ([Apache-2.0](https://github.com/in-toto/in-toto-golang/blob/v0.9.0/LICENSE))
- [github.com/itchyny/gojq](https://pkg.go.dev/github.com/itchyny/gojq) ([MIT](https://github.com/itchyny/gojq/blob/v0.12.15/LICENSE))
- [github.com/itchyny/timefmt-go](https://pkg.go.dev/github.com/itchyny/timefmt-go) ([MIT](https://github.com/itchyny/timefmt-go/blob/v0.1.5/LICENSE))
- [github.com/jedisct1/go-minisign](https://pkg.go.dev/github.com/jedisct1/go-minisign) ([MIT](https://github.com/jedisct1/go-minisign/blob/1c139d1cc84b/LICENSE))
- [github.com/itchyny/gojq](https://pkg.go.dev/github.com/itchyny/gojq) ([MIT](https://github.com/itchyny/gojq/blob/v0.12.17/LICENSE))
- [github.com/itchyny/timefmt-go](https://pkg.go.dev/github.com/itchyny/timefmt-go) ([MIT](https://github.com/itchyny/timefmt-go/blob/v0.1.6/LICENSE))
- [github.com/jedisct1/go-minisign](https://pkg.go.dev/github.com/jedisct1/go-minisign) ([MIT](https://github.com/jedisct1/go-minisign/blob/d2f9f49435c7/LICENSE))
- [github.com/joho/godotenv](https://pkg.go.dev/github.com/joho/godotenv) ([MIT](https://github.com/joho/godotenv/blob/v1.5.1/LICENCE))
- [github.com/josharian/intern](https://pkg.go.dev/github.com/josharian/intern) ([MIT](https://github.com/josharian/intern/blob/v1.0.0/license.md))
- [github.com/kballard/go-shellquote](https://pkg.go.dev/github.com/kballard/go-shellquote) ([MIT](https://github.com/kballard/go-shellquote/blob/95032a82bc51/LICENSE))
- [github.com/klauspost/compress](https://pkg.go.dev/github.com/klauspost/compress) ([Apache-2.0](https://github.com/klauspost/compress/blob/v1.18.0/LICENSE))
- [github.com/klauspost/compress/internal/snapref](https://pkg.go.dev/github.com/klauspost/compress/internal/snapref) ([BSD-3-Clause](https://github.com/klauspost/compress/blob/v1.18.0/internal/snapref/LICENSE))
- [github.com/klauspost/compress/zstd/internal/xxhash](https://pkg.go.dev/github.com/klauspost/compress/zstd/internal/xxhash) ([MIT](https://github.com/klauspost/compress/blob/v1.18.0/zstd/internal/xxhash/LICENSE.txt))
- [github.com/letsencrypt/boulder](https://pkg.go.dev/github.com/letsencrypt/boulder) ([MPL-2.0](https://github.com/letsencrypt/boulder/blob/de9c06129bec/LICENSE.txt))
- [github.com/letsencrypt/boulder](https://pkg.go.dev/github.com/letsencrypt/boulder) ([MPL-2.0](https://github.com/letsencrypt/boulder/blob/v0.20250630.0/LICENSE.txt))
- [github.com/lucasb-eyer/go-colorful](https://pkg.go.dev/github.com/lucasb-eyer/go-colorful) ([MIT](https://github.com/lucasb-eyer/go-colorful/blob/v1.2.0/LICENSE))
- [github.com/mailru/easyjson](https://pkg.go.dev/github.com/mailru/easyjson) ([MIT](https://github.com/mailru/easyjson/blob/v0.9.0/LICENSE))
- [github.com/mattn/go-colorable](https://pkg.go.dev/github.com/mattn/go-colorable) ([MIT](https://github.com/mattn/go-colorable/blob/v0.1.14/LICENSE))
@ -115,34 +116,34 @@ Some packages may only be included on certain architectures or operating systems
- [github.com/muesli/cancelreader](https://pkg.go.dev/github.com/muesli/cancelreader) ([MIT](https://github.com/muesli/cancelreader/blob/v0.2.2/LICENSE))
- [github.com/muesli/reflow](https://pkg.go.dev/github.com/muesli/reflow) ([MIT](https://github.com/muesli/reflow/blob/v0.3.0/LICENSE))
- [github.com/muesli/termenv](https://pkg.go.dev/github.com/muesli/termenv) ([MIT](https://github.com/muesli/termenv/blob/v0.16.0/LICENSE))
- [github.com/muhammadmuzzammil1998/jsonc](https://pkg.go.dev/github.com/muhammadmuzzammil1998/jsonc) ([MIT](https://github.com/muhammadmuzzammil1998/jsonc/blob/615b0916ca38/LICENSE))
- [github.com/muhammadmuzzammil1998/jsonc](https://pkg.go.dev/github.com/muhammadmuzzammil1998/jsonc) ([MIT](https://github.com/muhammadmuzzammil1998/jsonc/blob/v1.0.0/LICENSE))
- [github.com/oklog/ulid](https://pkg.go.dev/github.com/oklog/ulid) ([Apache-2.0](https://github.com/oklog/ulid/blob/v1.3.1/LICENSE))
- [github.com/opencontainers/go-digest](https://pkg.go.dev/github.com/opencontainers/go-digest) ([Apache-2.0](https://github.com/opencontainers/go-digest/blob/v1.0.0/LICENSE))
- [github.com/opencontainers/image-spec/specs-go](https://pkg.go.dev/github.com/opencontainers/image-spec/specs-go) ([Apache-2.0](https://github.com/opencontainers/image-spec/blob/v1.1.1/LICENSE))
- [github.com/opentracing/opentracing-go](https://pkg.go.dev/github.com/opentracing/opentracing-go) ([Apache-2.0](https://github.com/opentracing/opentracing-go/blob/v1.2.0/LICENSE))
- [github.com/pelletier/go-toml/v2](https://pkg.go.dev/github.com/pelletier/go-toml/v2) ([MIT](https://github.com/pelletier/go-toml/blob/v2.2.3/LICENSE))
- [github.com/pelletier/go-toml/v2](https://pkg.go.dev/github.com/pelletier/go-toml/v2) ([MIT](https://github.com/pelletier/go-toml/blob/v2.2.4/LICENSE))
- [github.com/pkg/errors](https://pkg.go.dev/github.com/pkg/errors) ([BSD-2-Clause](https://github.com/pkg/errors/blob/v0.9.1/LICENSE))
- [github.com/pmezard/go-difflib/difflib](https://pkg.go.dev/github.com/pmezard/go-difflib/difflib) ([BSD-3-Clause](https://github.com/pmezard/go-difflib/blob/5d4384ee4fb2/LICENSE))
- [github.com/rivo/tview](https://pkg.go.dev/github.com/rivo/tview) ([MIT](https://github.com/rivo/tview/blob/c4a7e501810d/LICENSE.txt))
- [github.com/rivo/tview](https://pkg.go.dev/github.com/rivo/tview) ([MIT](https://github.com/rivo/tview/blob/a4a78f1e05cb/LICENSE.txt))
- [github.com/rivo/uniseg](https://pkg.go.dev/github.com/rivo/uniseg) ([MIT](https://github.com/rivo/uniseg/blob/v0.4.7/LICENSE.txt))
- [github.com/rodaine/table](https://pkg.go.dev/github.com/rodaine/table) ([MIT](https://github.com/rodaine/table/blob/v1.0.1/license))
- [github.com/rodaine/table](https://pkg.go.dev/github.com/rodaine/table) ([MIT](https://github.com/rodaine/table/blob/v1.3.0/license))
- [github.com/russross/blackfriday/v2](https://pkg.go.dev/github.com/russross/blackfriday/v2) ([BSD-2-Clause](https://github.com/russross/blackfriday/blob/v2.1.0/LICENSE.txt))
- [github.com/sagikazarmark/locafero](https://pkg.go.dev/github.com/sagikazarmark/locafero) ([MIT](https://github.com/sagikazarmark/locafero/blob/v0.7.0/LICENSE))
- [github.com/sagikazarmark/locafero](https://pkg.go.dev/github.com/sagikazarmark/locafero) ([MIT](https://github.com/sagikazarmark/locafero/blob/v0.9.0/LICENSE))
- [github.com/sassoftware/relic/lib](https://pkg.go.dev/github.com/sassoftware/relic/lib) ([Apache-2.0](https://github.com/sassoftware/relic/blob/v7.2.1/LICENSE))
- [github.com/secure-systems-lab/go-securesystemslib](https://pkg.go.dev/github.com/secure-systems-lab/go-securesystemslib) ([MIT](https://github.com/secure-systems-lab/go-securesystemslib/blob/v0.9.0/LICENSE))
- [github.com/shibumi/go-pathspec](https://pkg.go.dev/github.com/shibumi/go-pathspec) ([Apache-2.0](https://github.com/shibumi/go-pathspec/blob/v1.3.0/LICENSE))
- [github.com/shopspring/decimal](https://pkg.go.dev/github.com/shopspring/decimal) ([MIT](https://github.com/shopspring/decimal/blob/v1.4.0/LICENSE))
- [github.com/shurcooL/githubv4](https://pkg.go.dev/github.com/shurcooL/githubv4) ([MIT](https://github.com/shurcooL/githubv4/blob/18a1ae0e79dc/LICENSE))
- [github.com/shurcooL/githubv4](https://pkg.go.dev/github.com/shurcooL/githubv4) ([MIT](https://github.com/shurcooL/githubv4/blob/48295856cce7/LICENSE))
- [github.com/shurcooL/graphql](https://pkg.go.dev/github.com/shurcooL/graphql) ([MIT](https://github.com/shurcooL/graphql/blob/ed46e5a46466/LICENSE))
- [github.com/sigstore/protobuf-specs/gen/pb-go](https://pkg.go.dev/github.com/sigstore/protobuf-specs/gen/pb-go) ([Apache-2.0](https://github.com/sigstore/protobuf-specs/blob/v0.4.3/LICENSE))
- [github.com/sigstore/rekor/pkg](https://pkg.go.dev/github.com/sigstore/rekor/pkg) ([Apache-2.0](https://github.com/sigstore/rekor/blob/v1.3.10/LICENSE))
- [github.com/sigstore/sigstore-go/pkg](https://pkg.go.dev/github.com/sigstore/sigstore-go/pkg) ([Apache-2.0](https://github.com/sigstore/sigstore-go/blob/v1.0.0/LICENSE))
- [github.com/sigstore/sigstore/pkg](https://pkg.go.dev/github.com/sigstore/sigstore/pkg) ([Apache-2.0](https://github.com/sigstore/sigstore/blob/v1.9.4/LICENSE))
- [github.com/sigstore/timestamp-authority/pkg/verification](https://pkg.go.dev/github.com/sigstore/timestamp-authority/pkg/verification) ([Apache-2.0](https://github.com/sigstore/timestamp-authority/blob/v1.2.7/LICENSE))
- [github.com/sigstore/sigstore/pkg](https://pkg.go.dev/github.com/sigstore/sigstore/pkg) ([Apache-2.0](https://github.com/sigstore/sigstore/blob/v1.9.5/LICENSE))
- [github.com/sigstore/timestamp-authority/pkg/verification](https://pkg.go.dev/github.com/sigstore/timestamp-authority/pkg/verification) ([Apache-2.0](https://github.com/sigstore/timestamp-authority/blob/v1.2.8/LICENSE))
- [github.com/sirupsen/logrus](https://pkg.go.dev/github.com/sirupsen/logrus) ([MIT](https://github.com/sirupsen/logrus/blob/v1.9.3/LICENSE))
- [github.com/sourcegraph/conc](https://pkg.go.dev/github.com/sourcegraph/conc) ([MIT](https://github.com/sourcegraph/conc/blob/v0.3.0/LICENSE))
- [github.com/spf13/afero](https://pkg.go.dev/github.com/spf13/afero) ([Apache-2.0](https://github.com/spf13/afero/blob/v1.12.0/LICENSE.txt))
- [github.com/spf13/cast](https://pkg.go.dev/github.com/spf13/cast) ([MIT](https://github.com/spf13/cast/blob/v1.7.1/LICENSE))
- [github.com/spf13/afero](https://pkg.go.dev/github.com/spf13/afero) ([Apache-2.0](https://github.com/spf13/afero/blob/v1.14.0/LICENSE.txt))
- [github.com/spf13/cast](https://pkg.go.dev/github.com/spf13/cast) ([MIT](https://github.com/spf13/cast/blob/v1.9.2/LICENSE))
- [github.com/spf13/cobra](https://pkg.go.dev/github.com/spf13/cobra) ([Apache-2.0](https://github.com/spf13/cobra/blob/v1.9.1/LICENSE.txt))
- [github.com/spf13/pflag](https://pkg.go.dev/github.com/spf13/pflag) ([BSD-3-Clause](https://github.com/spf13/pflag/blob/v1.0.6/LICENSE))
- [github.com/spf13/viper](https://pkg.go.dev/github.com/spf13/viper) ([MIT](https://github.com/spf13/viper/blob/v1.20.1/LICENSE))
@ -151,34 +152,33 @@ Some packages may only be included on certain architectures or operating systems
- [github.com/subosito/gotenv](https://pkg.go.dev/github.com/subosito/gotenv) ([MIT](https://github.com/subosito/gotenv/blob/v1.6.0/LICENSE))
- [github.com/theupdateframework/go-tuf](https://pkg.go.dev/github.com/theupdateframework/go-tuf) ([BSD-3-Clause](https://github.com/theupdateframework/go-tuf/blob/v0.7.0/LICENSE))
- [github.com/theupdateframework/go-tuf/v2/metadata](https://pkg.go.dev/github.com/theupdateframework/go-tuf/v2/metadata) ([Apache-2.0](https://github.com/theupdateframework/go-tuf/blob/v2.1.1/LICENSE))
- [github.com/thlib/go-timezone-local/tzlocal](https://pkg.go.dev/github.com/thlib/go-timezone-local/tzlocal) ([Unlicense](https://github.com/thlib/go-timezone-local/blob/ef149e42d28e/LICENSE))
- [github.com/thlib/go-timezone-local/tzlocal](https://pkg.go.dev/github.com/thlib/go-timezone-local/tzlocal) ([Unlicense](https://github.com/thlib/go-timezone-local/blob/v0.0.6/LICENSE))
- [github.com/titanous/rocacheck](https://pkg.go.dev/github.com/titanous/rocacheck) ([MIT](https://github.com/titanous/rocacheck/blob/afe73141d399/LICENSE))
- [github.com/transparency-dev/merkle](https://pkg.go.dev/github.com/transparency-dev/merkle) ([Apache-2.0](https://github.com/transparency-dev/merkle/blob/v0.0.2/LICENSE))
- [github.com/vbatts/tar-split/archive/tar](https://pkg.go.dev/github.com/vbatts/tar-split/archive/tar) ([BSD-3-Clause](https://github.com/vbatts/tar-split/blob/v0.12.1/LICENSE))
- [github.com/xo/terminfo](https://pkg.go.dev/github.com/xo/terminfo) ([MIT](https://github.com/xo/terminfo/blob/abceb7e1c41e/LICENSE))
- [github.com/yuin/goldmark](https://pkg.go.dev/github.com/yuin/goldmark) ([MIT](https://github.com/yuin/goldmark/blob/v1.7.12/LICENSE))
- [github.com/yuin/goldmark-emoji](https://pkg.go.dev/github.com/yuin/goldmark-emoji) ([MIT](https://github.com/yuin/goldmark-emoji/blob/v1.0.5/LICENSE))
- [github.com/zalando/go-keyring](https://pkg.go.dev/github.com/zalando/go-keyring) ([MIT](https://github.com/zalando/go-keyring/blob/v0.2.5/LICENSE))
- [go.mongodb.org/mongo-driver](https://pkg.go.dev/go.mongodb.org/mongo-driver) ([Apache-2.0](https://github.com/mongodb/mongo-go-driver/blob/v1.14.0/LICENSE))
- [github.com/yuin/goldmark-emoji](https://pkg.go.dev/github.com/yuin/goldmark-emoji) ([MIT](https://github.com/yuin/goldmark-emoji/blob/v1.0.6/LICENSE))
- [github.com/zalando/go-keyring](https://pkg.go.dev/github.com/zalando/go-keyring) ([MIT](https://github.com/zalando/go-keyring/blob/v0.2.6/LICENSE))
- [go.mongodb.org/mongo-driver](https://pkg.go.dev/go.mongodb.org/mongo-driver) ([Apache-2.0](https://github.com/mongodb/mongo-go-driver/blob/v1.17.4/LICENSE))
- [go.opentelemetry.io/auto/sdk](https://pkg.go.dev/go.opentelemetry.io/auto/sdk) ([Apache-2.0](https://github.com/open-telemetry/opentelemetry-go-instrumentation/blob/sdk/v1.1.0/sdk/LICENSE))
- [go.opentelemetry.io/otel](https://pkg.go.dev/go.opentelemetry.io/otel) ([Apache-2.0](https://github.com/open-telemetry/opentelemetry-go/blob/v1.36.0/LICENSE))
- [go.opentelemetry.io/otel/metric](https://pkg.go.dev/go.opentelemetry.io/otel/metric) ([Apache-2.0](https://github.com/open-telemetry/opentelemetry-go/blob/metric/v1.36.0/metric/LICENSE))
- [go.opentelemetry.io/otel/trace](https://pkg.go.dev/go.opentelemetry.io/otel/trace) ([Apache-2.0](https://github.com/open-telemetry/opentelemetry-go/blob/trace/v1.36.0/trace/LICENSE))
- [go.opentelemetry.io/otel](https://pkg.go.dev/go.opentelemetry.io/otel) ([Apache-2.0](https://github.com/open-telemetry/opentelemetry-go/blob/v1.37.0/LICENSE))
- [go.opentelemetry.io/otel/metric](https://pkg.go.dev/go.opentelemetry.io/otel/metric) ([Apache-2.0](https://github.com/open-telemetry/opentelemetry-go/blob/metric/v1.37.0/metric/LICENSE))
- [go.opentelemetry.io/otel/trace](https://pkg.go.dev/go.opentelemetry.io/otel/trace) ([Apache-2.0](https://github.com/open-telemetry/opentelemetry-go/blob/trace/v1.37.0/trace/LICENSE))
- [go.uber.org/multierr](https://pkg.go.dev/go.uber.org/multierr) ([MIT](https://github.com/uber-go/multierr/blob/v1.11.0/LICENSE.txt))
- [go.uber.org/zap](https://pkg.go.dev/go.uber.org/zap) ([MIT](https://github.com/uber-go/zap/blob/v1.27.0/LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.39.0:LICENSE))
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/fd00a4e0:LICENSE))
- [golang.org/x/exp/slices](https://pkg.go.dev/golang.org/x/exp/slices) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/b7579e27:LICENSE))
- [golang.org/x/mod](https://pkg.go.dev/golang.org/x/mod) ([BSD-3-Clause](https://cs.opensource.google/go/x/mod/+/v0.25.0:LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.41.0:LICENSE))
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.15.0:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.33.0:LICENSE))
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.32.0:LICENSE))
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.26.0:LICENSE))
- [google.golang.org/genproto/googleapis/api](https://pkg.go.dev/google.golang.org/genproto/googleapis/api) ([Apache-2.0](https://github.com/googleapis/go-genproto/blob/207652e42e2e/googleapis/api/LICENSE))
- [google.golang.org/genproto/googleapis/rpc/status](https://pkg.go.dev/google.golang.org/genproto/googleapis/rpc/status) ([Apache-2.0](https://github.com/googleapis/go-genproto/blob/207652e42e2e/googleapis/rpc/LICENSE))
- [google.golang.org/grpc](https://pkg.go.dev/google.golang.org/grpc) ([Apache-2.0](https://github.com/grpc/grpc-go/blob/v1.72.2/LICENSE))
- [google.golang.org/genproto/googleapis/api](https://pkg.go.dev/google.golang.org/genproto/googleapis/api) ([Apache-2.0](https://github.com/googleapis/go-genproto/blob/513f23925822/googleapis/api/LICENSE))
- [google.golang.org/genproto/googleapis/rpc/status](https://pkg.go.dev/google.golang.org/genproto/googleapis/rpc/status) ([Apache-2.0](https://github.com/googleapis/go-genproto/blob/513f23925822/googleapis/rpc/LICENSE))
- [google.golang.org/grpc](https://pkg.go.dev/google.golang.org/grpc) ([Apache-2.0](https://github.com/grpc/grpc-go/blob/v1.73.0/LICENSE))
- [google.golang.org/protobuf](https://pkg.go.dev/google.golang.org/protobuf) ([BSD-3-Clause](https://github.com/protocolbuffers/protobuf-go/blob/v1.36.6/LICENSE))
- [gopkg.in/yaml.v3](https://pkg.go.dev/gopkg.in/yaml.v3) ([MIT](https://github.com/go-yaml/yaml/blob/v3.0.1/LICENSE))
- [k8s.io/klog/v2](https://pkg.go.dev/k8s.io/klog/v2) ([Apache-2.0](https://github.com/kubernetes/klog/blob/v2.130.1/LICENSE))
[cli/cli]: https://github.com/cli/cli

View file

@ -7,63 +7,64 @@ The following open source dependencies are used to build the [cli/cli][] GitHub
Some packages may only be included on certain architectures or operating systems.
- [dario.cat/mergo](https://pkg.go.dev/dario.cat/mergo) ([BSD-3-Clause](https://github.com/imdario/mergo/blob/v1.0.1/LICENSE))
- [dario.cat/mergo](https://pkg.go.dev/dario.cat/mergo) ([BSD-3-Clause](https://github.com/imdario/mergo/blob/v1.0.2/LICENSE))
- [github.com/AlecAivazis/survey/v2](https://pkg.go.dev/github.com/AlecAivazis/survey/v2) ([MIT](https://github.com/AlecAivazis/survey/blob/v2.3.7/LICENSE))
- [github.com/AlecAivazis/survey/v2/terminal](https://pkg.go.dev/github.com/AlecAivazis/survey/v2/terminal) ([MIT](https://github.com/AlecAivazis/survey/blob/v2.3.7/terminal/LICENSE.txt))
- [github.com/MakeNowJust/heredoc](https://pkg.go.dev/github.com/MakeNowJust/heredoc) ([MIT](https://github.com/MakeNowJust/heredoc/blob/v1.0.0/LICENSE))
- [github.com/Masterminds/goutils](https://pkg.go.dev/github.com/Masterminds/goutils) ([Apache-2.0](https://github.com/Masterminds/goutils/blob/v1.1.1/LICENSE.txt))
- [github.com/Masterminds/semver/v3](https://pkg.go.dev/github.com/Masterminds/semver/v3) ([MIT](https://github.com/Masterminds/semver/blob/v3.3.0/LICENSE.txt))
- [github.com/Masterminds/semver/v3](https://pkg.go.dev/github.com/Masterminds/semver/v3) ([MIT](https://github.com/Masterminds/semver/blob/v3.4.0/LICENSE.txt))
- [github.com/Masterminds/sprig/v3](https://pkg.go.dev/github.com/Masterminds/sprig/v3) ([MIT](https://github.com/Masterminds/sprig/blob/v3.3.0/LICENSE.txt))
- [github.com/alecthomas/chroma/v2](https://pkg.go.dev/github.com/alecthomas/chroma/v2) ([MIT](https://github.com/alecthomas/chroma/blob/v2.14.0/COPYING))
- [github.com/alecthomas/chroma/v2](https://pkg.go.dev/github.com/alecthomas/chroma/v2) ([MIT](https://github.com/alecthomas/chroma/blob/v2.19.0/COPYING))
- [github.com/asaskevich/govalidator](https://pkg.go.dev/github.com/asaskevich/govalidator) ([MIT](https://github.com/asaskevich/govalidator/blob/a9d515a09cc2/LICENSE))
- [github.com/atotto/clipboard](https://pkg.go.dev/github.com/atotto/clipboard) ([BSD-3-Clause](https://github.com/atotto/clipboard/blob/v0.1.4/LICENSE))
- [github.com/aymanbagabas/go-osc52/v2](https://pkg.go.dev/github.com/aymanbagabas/go-osc52/v2) ([MIT](https://github.com/aymanbagabas/go-osc52/blob/v2.0.1/LICENSE))
- [github.com/aymerick/douceur](https://pkg.go.dev/github.com/aymerick/douceur) ([MIT](https://github.com/aymerick/douceur/blob/v0.2.0/LICENSE))
- [github.com/blang/semver](https://pkg.go.dev/github.com/blang/semver) ([MIT](https://github.com/blang/semver/blob/v3.5.1/LICENSE))
- [github.com/briandowns/spinner](https://pkg.go.dev/github.com/briandowns/spinner) ([Apache-2.0](https://github.com/briandowns/spinner/blob/v1.18.1/LICENSE))
- [github.com/briandowns/spinner](https://pkg.go.dev/github.com/briandowns/spinner) ([Apache-2.0](https://github.com/briandowns/spinner/blob/v1.23.2/LICENSE))
- [github.com/catppuccin/go](https://pkg.go.dev/github.com/catppuccin/go) ([MIT](https://github.com/catppuccin/go/blob/v0.3.0/LICENSE))
- [github.com/cenkalti/backoff/v4](https://pkg.go.dev/github.com/cenkalti/backoff/v4) ([MIT](https://github.com/cenkalti/backoff/blob/v4.3.0/LICENSE))
- [github.com/cenkalti/backoff/v5](https://pkg.go.dev/github.com/cenkalti/backoff/v5) ([MIT](https://github.com/cenkalti/backoff/blob/v5.0.2/LICENSE))
- [github.com/charmbracelet/bubbles](https://pkg.go.dev/github.com/charmbracelet/bubbles) ([MIT](https://github.com/charmbracelet/bubbles/blob/v0.21.0/LICENSE))
- [github.com/charmbracelet/bubbletea](https://pkg.go.dev/github.com/charmbracelet/bubbletea) ([MIT](https://github.com/charmbracelet/bubbletea/blob/v1.3.4/LICENSE))
- [github.com/charmbracelet/colorprofile](https://pkg.go.dev/github.com/charmbracelet/colorprofile) ([MIT](https://github.com/charmbracelet/colorprofile/blob/f60798e515dc/LICENSE))
- [github.com/charmbracelet/glamour](https://pkg.go.dev/github.com/charmbracelet/glamour) ([MIT](https://github.com/charmbracelet/glamour/blob/549f544650e3/LICENSE))
- [github.com/charmbracelet/bubbletea](https://pkg.go.dev/github.com/charmbracelet/bubbletea) ([MIT](https://github.com/charmbracelet/bubbletea/blob/v1.3.5/LICENSE))
- [github.com/charmbracelet/colorprofile](https://pkg.go.dev/github.com/charmbracelet/colorprofile) ([MIT](https://github.com/charmbracelet/colorprofile/blob/v0.3.1/LICENSE))
- [github.com/charmbracelet/glamour](https://pkg.go.dev/github.com/charmbracelet/glamour) ([MIT](https://github.com/charmbracelet/glamour/blob/v0.10.0/LICENSE))
- [github.com/charmbracelet/huh](https://pkg.go.dev/github.com/charmbracelet/huh) ([MIT](https://github.com/charmbracelet/huh/blob/v0.7.0/LICENSE))
- [github.com/charmbracelet/lipgloss](https://pkg.go.dev/github.com/charmbracelet/lipgloss) ([MIT](https://github.com/charmbracelet/lipgloss/blob/166f707985bc/LICENSE))
- [github.com/charmbracelet/x/ansi](https://pkg.go.dev/github.com/charmbracelet/x/ansi) ([MIT](https://github.com/charmbracelet/x/blob/ansi/v0.8.0/ansi/LICENSE))
- [github.com/charmbracelet/lipgloss](https://pkg.go.dev/github.com/charmbracelet/lipgloss) ([MIT](https://github.com/charmbracelet/lipgloss/blob/76690c660834/LICENSE))
- [github.com/charmbracelet/x/ansi](https://pkg.go.dev/github.com/charmbracelet/x/ansi) ([MIT](https://github.com/charmbracelet/x/blob/ansi/v0.9.3/ansi/LICENSE))
- [github.com/charmbracelet/x/cellbuf](https://pkg.go.dev/github.com/charmbracelet/x/cellbuf) ([MIT](https://github.com/charmbracelet/x/blob/cellbuf/v0.0.13/cellbuf/LICENSE))
- [github.com/charmbracelet/x/exp/strings](https://pkg.go.dev/github.com/charmbracelet/x/exp/strings) ([MIT](https://github.com/charmbracelet/x/blob/212f7b056ed0/exp/strings/LICENSE))
- [github.com/charmbracelet/x/exp/slice](https://pkg.go.dev/github.com/charmbracelet/x/exp/slice) ([MIT](https://github.com/charmbracelet/x/blob/821143405392/exp/slice/LICENSE))
- [github.com/charmbracelet/x/exp/strings](https://pkg.go.dev/github.com/charmbracelet/x/exp/strings) ([MIT](https://github.com/charmbracelet/x/blob/821143405392/exp/strings/LICENSE))
- [github.com/charmbracelet/x/term](https://pkg.go.dev/github.com/charmbracelet/x/term) ([MIT](https://github.com/charmbracelet/x/blob/term/v0.2.1/term/LICENSE))
- [github.com/cli/browser](https://pkg.go.dev/github.com/cli/browser) ([BSD-2-Clause](https://github.com/cli/browser/blob/v1.3.0/LICENSE))
- [github.com/cli/go-gh/v2](https://pkg.go.dev/github.com/cli/go-gh/v2) ([MIT](https://github.com/cli/go-gh/blob/v2.12.1/LICENSE))
- [github.com/cli/oauth](https://pkg.go.dev/github.com/cli/oauth) ([MIT](https://github.com/cli/oauth/blob/v1.1.1/LICENSE))
- [github.com/cli/oauth](https://pkg.go.dev/github.com/cli/oauth) ([MIT](https://github.com/cli/oauth/blob/v1.2.0/LICENSE))
- [github.com/cli/safeexec](https://pkg.go.dev/github.com/cli/safeexec) ([BSD-2-Clause](https://github.com/cli/safeexec/blob/v1.0.1/LICENSE))
- [github.com/cli/shurcooL-graphql](https://pkg.go.dev/github.com/cli/shurcooL-graphql) ([MIT](https://github.com/cli/shurcooL-graphql/blob/v0.0.4/LICENSE))
- [github.com/containerd/stargz-snapshotter/estargz](https://pkg.go.dev/github.com/containerd/stargz-snapshotter/estargz) ([Apache-2.0](https://github.com/containerd/stargz-snapshotter/blob/estargz/v0.16.3/estargz/LICENSE))
- [github.com/cpuguy83/go-md2man/v2/md2man](https://pkg.go.dev/github.com/cpuguy83/go-md2man/v2/md2man) ([MIT](https://github.com/cpuguy83/go-md2man/blob/v2.0.7/LICENSE.md))
- [github.com/cyberphone/json-canonicalization/go/src/webpki.org/jsoncanonicalizer](https://pkg.go.dev/github.com/cyberphone/json-canonicalization/go/src/webpki.org/jsoncanonicalizer) ([Apache-2.0](https://github.com/cyberphone/json-canonicalization/blob/57a0ce2678a7/LICENSE))
- [github.com/cyberphone/json-canonicalization/go/src/webpki.org/jsoncanonicalizer](https://pkg.go.dev/github.com/cyberphone/json-canonicalization/go/src/webpki.org/jsoncanonicalizer) ([Apache-2.0](https://github.com/cyberphone/json-canonicalization/blob/19d51d7fe467/LICENSE))
- [github.com/davecgh/go-spew/spew](https://pkg.go.dev/github.com/davecgh/go-spew/spew) ([ISC](https://github.com/davecgh/go-spew/blob/d8f796af33cc/LICENSE))
- [github.com/digitorus/pkcs7](https://pkg.go.dev/github.com/digitorus/pkcs7) ([MIT](https://github.com/digitorus/pkcs7/blob/3a137a874352/LICENSE))
- [github.com/digitorus/timestamp](https://pkg.go.dev/github.com/digitorus/timestamp) ([BSD-2-Clause](https://github.com/digitorus/timestamp/blob/220c5c2851b7/LICENSE))
- [github.com/digitorus/timestamp](https://pkg.go.dev/github.com/digitorus/timestamp) ([BSD-2-Clause](https://github.com/digitorus/timestamp/blob/c45532741eea/LICENSE))
- [github.com/distribution/reference](https://pkg.go.dev/github.com/distribution/reference) ([Apache-2.0](https://github.com/distribution/reference/blob/v0.6.0/LICENSE))
- [github.com/dlclark/regexp2](https://pkg.go.dev/github.com/dlclark/regexp2) ([MIT](https://github.com/dlclark/regexp2/blob/v1.11.0/LICENSE))
- [github.com/docker/cli/cli/config](https://pkg.go.dev/github.com/docker/cli/cli/config) ([Apache-2.0](https://github.com/docker/cli/blob/v28.2.2/LICENSE))
- [github.com/dlclark/regexp2](https://pkg.go.dev/github.com/dlclark/regexp2) ([MIT](https://github.com/dlclark/regexp2/blob/v1.11.5/LICENSE))
- [github.com/docker/cli/cli/config](https://pkg.go.dev/github.com/docker/cli/cli/config) ([Apache-2.0](https://github.com/docker/cli/blob/v28.3.0/LICENSE))
- [github.com/docker/distribution/registry/client/auth/challenge](https://pkg.go.dev/github.com/docker/distribution/registry/client/auth/challenge) ([Apache-2.0](https://github.com/docker/distribution/blob/v2.8.3/LICENSE))
- [github.com/docker/docker-credential-helpers](https://pkg.go.dev/github.com/docker/docker-credential-helpers) ([MIT](https://github.com/docker/docker-credential-helpers/blob/v0.9.3/LICENSE))
- [github.com/dustin/go-humanize](https://pkg.go.dev/github.com/dustin/go-humanize) ([MIT](https://github.com/dustin/go-humanize/blob/v1.0.1/LICENSE))
- [github.com/fatih/color](https://pkg.go.dev/github.com/fatih/color) ([MIT](https://github.com/fatih/color/blob/v1.16.0/LICENSE.md))
- [github.com/fsnotify/fsnotify](https://pkg.go.dev/github.com/fsnotify/fsnotify) ([BSD-3-Clause](https://github.com/fsnotify/fsnotify/blob/v1.8.0/LICENSE))
- [github.com/fatih/color](https://pkg.go.dev/github.com/fatih/color) ([MIT](https://github.com/fatih/color/blob/v1.18.0/LICENSE.md))
- [github.com/fsnotify/fsnotify](https://pkg.go.dev/github.com/fsnotify/fsnotify) ([BSD-3-Clause](https://github.com/fsnotify/fsnotify/blob/v1.9.0/LICENSE))
- [github.com/gabriel-vasile/mimetype](https://pkg.go.dev/github.com/gabriel-vasile/mimetype) ([MIT](https://github.com/gabriel-vasile/mimetype/blob/v1.4.9/LICENSE))
- [github.com/gdamore/encoding](https://pkg.go.dev/github.com/gdamore/encoding) ([Apache-2.0](https://github.com/gdamore/encoding/blob/v1.0.0/LICENSE))
- [github.com/gdamore/tcell/v2](https://pkg.go.dev/github.com/gdamore/tcell/v2) ([Apache-2.0](https://github.com/gdamore/tcell/blob/v2.5.4/LICENSE))
- [github.com/gdamore/encoding](https://pkg.go.dev/github.com/gdamore/encoding) ([Apache-2.0](https://github.com/gdamore/encoding/blob/v1.0.1/LICENSE))
- [github.com/gdamore/tcell/v2](https://pkg.go.dev/github.com/gdamore/tcell/v2) ([Apache-2.0](https://github.com/gdamore/tcell/blob/v2.8.1/LICENSE))
- [github.com/go-chi/chi](https://pkg.go.dev/github.com/go-chi/chi) ([MIT](https://github.com/go-chi/chi/blob/v4.1.2/LICENSE))
- [github.com/go-jose/go-jose/v4](https://pkg.go.dev/github.com/go-jose/go-jose/v4) ([Apache-2.0](https://github.com/go-jose/go-jose/blob/v4.0.5/LICENSE))
- [github.com/go-jose/go-jose/v4/json](https://pkg.go.dev/github.com/go-jose/go-jose/v4/json) ([BSD-3-Clause](https://github.com/go-jose/go-jose/blob/v4.0.5/json/LICENSE))
- [github.com/go-jose/go-jose/v4](https://pkg.go.dev/github.com/go-jose/go-jose/v4) ([Apache-2.0](https://github.com/go-jose/go-jose/blob/v4.1.1/LICENSE))
- [github.com/go-jose/go-jose/v4/json](https://pkg.go.dev/github.com/go-jose/go-jose/v4/json) ([BSD-3-Clause](https://github.com/go-jose/go-jose/blob/v4.1.1/json/LICENSE))
- [github.com/go-logr/logr](https://pkg.go.dev/github.com/go-logr/logr) ([Apache-2.0](https://github.com/go-logr/logr/blob/v1.4.3/LICENSE))
- [github.com/go-logr/stdr](https://pkg.go.dev/github.com/go-logr/stdr) ([Apache-2.0](https://github.com/go-logr/stdr/blob/v1.2.2/LICENSE))
- [github.com/go-openapi/analysis](https://pkg.go.dev/github.com/go-openapi/analysis) ([Apache-2.0](https://github.com/go-openapi/analysis/blob/v0.23.0/LICENSE))
- [github.com/go-openapi/errors](https://pkg.go.dev/github.com/go-openapi/errors) ([Apache-2.0](https://github.com/go-openapi/errors/blob/v0.22.1/LICENSE))
- [github.com/go-openapi/jsonpointer](https://pkg.go.dev/github.com/go-openapi/jsonpointer) ([Apache-2.0](https://github.com/go-openapi/jsonpointer/blob/v0.21.0/LICENSE))
- [github.com/go-openapi/jsonpointer](https://pkg.go.dev/github.com/go-openapi/jsonpointer) ([Apache-2.0](https://github.com/go-openapi/jsonpointer/blob/v0.21.1/LICENSE))
- [github.com/go-openapi/jsonreference](https://pkg.go.dev/github.com/go-openapi/jsonreference) ([Apache-2.0](https://github.com/go-openapi/jsonreference/blob/v0.21.0/LICENSE))
- [github.com/go-openapi/loads](https://pkg.go.dev/github.com/go-openapi/loads) ([Apache-2.0](https://github.com/go-openapi/loads/blob/v0.22.0/LICENSE))
- [github.com/go-openapi/runtime](https://pkg.go.dev/github.com/go-openapi/runtime) ([Apache-2.0](https://github.com/go-openapi/runtime/blob/v0.28.0/LICENSE))
@ -72,10 +73,10 @@ Some packages may only be included on certain architectures or operating systems
- [github.com/go-openapi/strfmt](https://pkg.go.dev/github.com/go-openapi/strfmt) ([Apache-2.0](https://github.com/go-openapi/strfmt/blob/v0.23.0/LICENSE))
- [github.com/go-openapi/swag](https://pkg.go.dev/github.com/go-openapi/swag) ([Apache-2.0](https://github.com/go-openapi/swag/blob/v0.23.1/LICENSE))
- [github.com/go-openapi/validate](https://pkg.go.dev/github.com/go-openapi/validate) ([Apache-2.0](https://github.com/go-openapi/validate/blob/v0.24.0/LICENSE))
- [github.com/go-viper/mapstructure/v2](https://pkg.go.dev/github.com/go-viper/mapstructure/v2) ([MIT](https://github.com/go-viper/mapstructure/blob/v2.2.1/LICENSE))
- [github.com/go-viper/mapstructure/v2](https://pkg.go.dev/github.com/go-viper/mapstructure/v2) ([MIT](https://github.com/go-viper/mapstructure/blob/v2.3.0/LICENSE))
- [github.com/godbus/dbus/v5](https://pkg.go.dev/github.com/godbus/dbus/v5) ([BSD-2-Clause](https://github.com/godbus/dbus/blob/v5.1.0/LICENSE))
- [github.com/golang/snappy](https://pkg.go.dev/github.com/golang/snappy) ([BSD-3-Clause](https://github.com/golang/snappy/blob/v0.0.4/LICENSE))
- [github.com/google/certificate-transparency-go](https://pkg.go.dev/github.com/google/certificate-transparency-go) ([Apache-2.0](https://github.com/google/certificate-transparency-go/blob/v1.3.1/LICENSE))
- [github.com/golang/snappy](https://pkg.go.dev/github.com/golang/snappy) ([BSD-3-Clause](https://github.com/golang/snappy/blob/v1.0.0/LICENSE))
- [github.com/google/certificate-transparency-go](https://pkg.go.dev/github.com/google/certificate-transparency-go) ([Apache-2.0](https://github.com/google/certificate-transparency-go/blob/v1.3.2/LICENSE))
- [github.com/google/go-containerregistry](https://pkg.go.dev/github.com/google/go-containerregistry) ([Apache-2.0](https://github.com/google/go-containerregistry/blob/v0.20.6/LICENSE))
- [github.com/google/shlex](https://pkg.go.dev/github.com/google/shlex) ([Apache-2.0](https://github.com/google/shlex/blob/e7afc7fbc510/COPYING))
- [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.6.0/LICENSE))
@ -83,21 +84,21 @@ Some packages may only be included on certain architectures or operating systems
- [github.com/gorilla/websocket](https://pkg.go.dev/github.com/gorilla/websocket) ([BSD-2-Clause](https://github.com/gorilla/websocket/blob/v1.5.3/LICENSE))
- [github.com/hashicorp/errwrap](https://pkg.go.dev/github.com/hashicorp/errwrap) ([MPL-2.0](https://github.com/hashicorp/errwrap/blob/v1.1.0/LICENSE))
- [github.com/hashicorp/go-multierror](https://pkg.go.dev/github.com/hashicorp/go-multierror) ([MPL-2.0](https://github.com/hashicorp/go-multierror/blob/v1.1.1/LICENSE))
- [github.com/hashicorp/go-version](https://pkg.go.dev/github.com/hashicorp/go-version) ([MPL-2.0](https://github.com/hashicorp/go-version/blob/v1.3.0/LICENSE))
- [github.com/hashicorp/go-version](https://pkg.go.dev/github.com/hashicorp/go-version) ([MPL-2.0](https://github.com/hashicorp/go-version/blob/v1.7.0/LICENSE))
- [github.com/henvic/httpretty](https://pkg.go.dev/github.com/henvic/httpretty) ([MIT](https://github.com/henvic/httpretty/blob/v0.1.4/LICENSE.md))
- [github.com/huandu/xstrings](https://pkg.go.dev/github.com/huandu/xstrings) ([MIT](https://github.com/huandu/xstrings/blob/v1.5.0/LICENSE))
- [github.com/in-toto/attestation/go/v1](https://pkg.go.dev/github.com/in-toto/attestation/go/v1) ([Apache-2.0](https://github.com/in-toto/attestation/blob/v1.1.2/LICENSE))
- [github.com/in-toto/in-toto-golang/in_toto](https://pkg.go.dev/github.com/in-toto/in-toto-golang/in_toto) ([Apache-2.0](https://github.com/in-toto/in-toto-golang/blob/v0.9.0/LICENSE))
- [github.com/itchyny/gojq](https://pkg.go.dev/github.com/itchyny/gojq) ([MIT](https://github.com/itchyny/gojq/blob/v0.12.15/LICENSE))
- [github.com/itchyny/timefmt-go](https://pkg.go.dev/github.com/itchyny/timefmt-go) ([MIT](https://github.com/itchyny/timefmt-go/blob/v0.1.5/LICENSE))
- [github.com/jedisct1/go-minisign](https://pkg.go.dev/github.com/jedisct1/go-minisign) ([MIT](https://github.com/jedisct1/go-minisign/blob/1c139d1cc84b/LICENSE))
- [github.com/itchyny/gojq](https://pkg.go.dev/github.com/itchyny/gojq) ([MIT](https://github.com/itchyny/gojq/blob/v0.12.17/LICENSE))
- [github.com/itchyny/timefmt-go](https://pkg.go.dev/github.com/itchyny/timefmt-go) ([MIT](https://github.com/itchyny/timefmt-go/blob/v0.1.6/LICENSE))
- [github.com/jedisct1/go-minisign](https://pkg.go.dev/github.com/jedisct1/go-minisign) ([MIT](https://github.com/jedisct1/go-minisign/blob/d2f9f49435c7/LICENSE))
- [github.com/joho/godotenv](https://pkg.go.dev/github.com/joho/godotenv) ([MIT](https://github.com/joho/godotenv/blob/v1.5.1/LICENCE))
- [github.com/josharian/intern](https://pkg.go.dev/github.com/josharian/intern) ([MIT](https://github.com/josharian/intern/blob/v1.0.0/license.md))
- [github.com/kballard/go-shellquote](https://pkg.go.dev/github.com/kballard/go-shellquote) ([MIT](https://github.com/kballard/go-shellquote/blob/95032a82bc51/LICENSE))
- [github.com/klauspost/compress](https://pkg.go.dev/github.com/klauspost/compress) ([Apache-2.0](https://github.com/klauspost/compress/blob/v1.18.0/LICENSE))
- [github.com/klauspost/compress/internal/snapref](https://pkg.go.dev/github.com/klauspost/compress/internal/snapref) ([BSD-3-Clause](https://github.com/klauspost/compress/blob/v1.18.0/internal/snapref/LICENSE))
- [github.com/klauspost/compress/zstd/internal/xxhash](https://pkg.go.dev/github.com/klauspost/compress/zstd/internal/xxhash) ([MIT](https://github.com/klauspost/compress/blob/v1.18.0/zstd/internal/xxhash/LICENSE.txt))
- [github.com/letsencrypt/boulder](https://pkg.go.dev/github.com/letsencrypt/boulder) ([MPL-2.0](https://github.com/letsencrypt/boulder/blob/de9c06129bec/LICENSE.txt))
- [github.com/letsencrypt/boulder](https://pkg.go.dev/github.com/letsencrypt/boulder) ([MPL-2.0](https://github.com/letsencrypt/boulder/blob/v0.20250630.0/LICENSE.txt))
- [github.com/lucasb-eyer/go-colorful](https://pkg.go.dev/github.com/lucasb-eyer/go-colorful) ([MIT](https://github.com/lucasb-eyer/go-colorful/blob/v1.2.0/LICENSE))
- [github.com/mailru/easyjson](https://pkg.go.dev/github.com/mailru/easyjson) ([MIT](https://github.com/mailru/easyjson/blob/v0.9.0/LICENSE))
- [github.com/mattn/go-colorable](https://pkg.go.dev/github.com/mattn/go-colorable) ([MIT](https://github.com/mattn/go-colorable/blob/v0.1.14/LICENSE))
@ -115,34 +116,34 @@ Some packages may only be included on certain architectures or operating systems
- [github.com/muesli/cancelreader](https://pkg.go.dev/github.com/muesli/cancelreader) ([MIT](https://github.com/muesli/cancelreader/blob/v0.2.2/LICENSE))
- [github.com/muesli/reflow](https://pkg.go.dev/github.com/muesli/reflow) ([MIT](https://github.com/muesli/reflow/blob/v0.3.0/LICENSE))
- [github.com/muesli/termenv](https://pkg.go.dev/github.com/muesli/termenv) ([MIT](https://github.com/muesli/termenv/blob/v0.16.0/LICENSE))
- [github.com/muhammadmuzzammil1998/jsonc](https://pkg.go.dev/github.com/muhammadmuzzammil1998/jsonc) ([MIT](https://github.com/muhammadmuzzammil1998/jsonc/blob/615b0916ca38/LICENSE))
- [github.com/muhammadmuzzammil1998/jsonc](https://pkg.go.dev/github.com/muhammadmuzzammil1998/jsonc) ([MIT](https://github.com/muhammadmuzzammil1998/jsonc/blob/v1.0.0/LICENSE))
- [github.com/oklog/ulid](https://pkg.go.dev/github.com/oklog/ulid) ([Apache-2.0](https://github.com/oklog/ulid/blob/v1.3.1/LICENSE))
- [github.com/opencontainers/go-digest](https://pkg.go.dev/github.com/opencontainers/go-digest) ([Apache-2.0](https://github.com/opencontainers/go-digest/blob/v1.0.0/LICENSE))
- [github.com/opencontainers/image-spec/specs-go](https://pkg.go.dev/github.com/opencontainers/image-spec/specs-go) ([Apache-2.0](https://github.com/opencontainers/image-spec/blob/v1.1.1/LICENSE))
- [github.com/opentracing/opentracing-go](https://pkg.go.dev/github.com/opentracing/opentracing-go) ([Apache-2.0](https://github.com/opentracing/opentracing-go/blob/v1.2.0/LICENSE))
- [github.com/pelletier/go-toml/v2](https://pkg.go.dev/github.com/pelletier/go-toml/v2) ([MIT](https://github.com/pelletier/go-toml/blob/v2.2.3/LICENSE))
- [github.com/pelletier/go-toml/v2](https://pkg.go.dev/github.com/pelletier/go-toml/v2) ([MIT](https://github.com/pelletier/go-toml/blob/v2.2.4/LICENSE))
- [github.com/pkg/errors](https://pkg.go.dev/github.com/pkg/errors) ([BSD-2-Clause](https://github.com/pkg/errors/blob/v0.9.1/LICENSE))
- [github.com/pmezard/go-difflib/difflib](https://pkg.go.dev/github.com/pmezard/go-difflib/difflib) ([BSD-3-Clause](https://github.com/pmezard/go-difflib/blob/5d4384ee4fb2/LICENSE))
- [github.com/rivo/tview](https://pkg.go.dev/github.com/rivo/tview) ([MIT](https://github.com/rivo/tview/blob/c4a7e501810d/LICENSE.txt))
- [github.com/rivo/tview](https://pkg.go.dev/github.com/rivo/tview) ([MIT](https://github.com/rivo/tview/blob/a4a78f1e05cb/LICENSE.txt))
- [github.com/rivo/uniseg](https://pkg.go.dev/github.com/rivo/uniseg) ([MIT](https://github.com/rivo/uniseg/blob/v0.4.7/LICENSE.txt))
- [github.com/rodaine/table](https://pkg.go.dev/github.com/rodaine/table) ([MIT](https://github.com/rodaine/table/blob/v1.0.1/license))
- [github.com/rodaine/table](https://pkg.go.dev/github.com/rodaine/table) ([MIT](https://github.com/rodaine/table/blob/v1.3.0/license))
- [github.com/russross/blackfriday/v2](https://pkg.go.dev/github.com/russross/blackfriday/v2) ([BSD-2-Clause](https://github.com/russross/blackfriday/blob/v2.1.0/LICENSE.txt))
- [github.com/sagikazarmark/locafero](https://pkg.go.dev/github.com/sagikazarmark/locafero) ([MIT](https://github.com/sagikazarmark/locafero/blob/v0.7.0/LICENSE))
- [github.com/sagikazarmark/locafero](https://pkg.go.dev/github.com/sagikazarmark/locafero) ([MIT](https://github.com/sagikazarmark/locafero/blob/v0.9.0/LICENSE))
- [github.com/sassoftware/relic/lib](https://pkg.go.dev/github.com/sassoftware/relic/lib) ([Apache-2.0](https://github.com/sassoftware/relic/blob/v7.2.1/LICENSE))
- [github.com/secure-systems-lab/go-securesystemslib](https://pkg.go.dev/github.com/secure-systems-lab/go-securesystemslib) ([MIT](https://github.com/secure-systems-lab/go-securesystemslib/blob/v0.9.0/LICENSE))
- [github.com/shibumi/go-pathspec](https://pkg.go.dev/github.com/shibumi/go-pathspec) ([Apache-2.0](https://github.com/shibumi/go-pathspec/blob/v1.3.0/LICENSE))
- [github.com/shopspring/decimal](https://pkg.go.dev/github.com/shopspring/decimal) ([MIT](https://github.com/shopspring/decimal/blob/v1.4.0/LICENSE))
- [github.com/shurcooL/githubv4](https://pkg.go.dev/github.com/shurcooL/githubv4) ([MIT](https://github.com/shurcooL/githubv4/blob/18a1ae0e79dc/LICENSE))
- [github.com/shurcooL/githubv4](https://pkg.go.dev/github.com/shurcooL/githubv4) ([MIT](https://github.com/shurcooL/githubv4/blob/48295856cce7/LICENSE))
- [github.com/shurcooL/graphql](https://pkg.go.dev/github.com/shurcooL/graphql) ([MIT](https://github.com/shurcooL/graphql/blob/ed46e5a46466/LICENSE))
- [github.com/sigstore/protobuf-specs/gen/pb-go](https://pkg.go.dev/github.com/sigstore/protobuf-specs/gen/pb-go) ([Apache-2.0](https://github.com/sigstore/protobuf-specs/blob/v0.4.3/LICENSE))
- [github.com/sigstore/rekor/pkg](https://pkg.go.dev/github.com/sigstore/rekor/pkg) ([Apache-2.0](https://github.com/sigstore/rekor/blob/v1.3.10/LICENSE))
- [github.com/sigstore/sigstore-go/pkg](https://pkg.go.dev/github.com/sigstore/sigstore-go/pkg) ([Apache-2.0](https://github.com/sigstore/sigstore-go/blob/v1.0.0/LICENSE))
- [github.com/sigstore/sigstore/pkg](https://pkg.go.dev/github.com/sigstore/sigstore/pkg) ([Apache-2.0](https://github.com/sigstore/sigstore/blob/v1.9.4/LICENSE))
- [github.com/sigstore/timestamp-authority/pkg/verification](https://pkg.go.dev/github.com/sigstore/timestamp-authority/pkg/verification) ([Apache-2.0](https://github.com/sigstore/timestamp-authority/blob/v1.2.7/LICENSE))
- [github.com/sigstore/sigstore/pkg](https://pkg.go.dev/github.com/sigstore/sigstore/pkg) ([Apache-2.0](https://github.com/sigstore/sigstore/blob/v1.9.5/LICENSE))
- [github.com/sigstore/timestamp-authority/pkg/verification](https://pkg.go.dev/github.com/sigstore/timestamp-authority/pkg/verification) ([Apache-2.0](https://github.com/sigstore/timestamp-authority/blob/v1.2.8/LICENSE))
- [github.com/sirupsen/logrus](https://pkg.go.dev/github.com/sirupsen/logrus) ([MIT](https://github.com/sirupsen/logrus/blob/v1.9.3/LICENSE))
- [github.com/sourcegraph/conc](https://pkg.go.dev/github.com/sourcegraph/conc) ([MIT](https://github.com/sourcegraph/conc/blob/v0.3.0/LICENSE))
- [github.com/spf13/afero](https://pkg.go.dev/github.com/spf13/afero) ([Apache-2.0](https://github.com/spf13/afero/blob/v1.12.0/LICENSE.txt))
- [github.com/spf13/cast](https://pkg.go.dev/github.com/spf13/cast) ([MIT](https://github.com/spf13/cast/blob/v1.7.1/LICENSE))
- [github.com/spf13/afero](https://pkg.go.dev/github.com/spf13/afero) ([Apache-2.0](https://github.com/spf13/afero/blob/v1.14.0/LICENSE.txt))
- [github.com/spf13/cast](https://pkg.go.dev/github.com/spf13/cast) ([MIT](https://github.com/spf13/cast/blob/v1.9.2/LICENSE))
- [github.com/spf13/cobra](https://pkg.go.dev/github.com/spf13/cobra) ([Apache-2.0](https://github.com/spf13/cobra/blob/v1.9.1/LICENSE.txt))
- [github.com/spf13/pflag](https://pkg.go.dev/github.com/spf13/pflag) ([BSD-3-Clause](https://github.com/spf13/pflag/blob/v1.0.6/LICENSE))
- [github.com/spf13/viper](https://pkg.go.dev/github.com/spf13/viper) ([MIT](https://github.com/spf13/viper/blob/v1.20.1/LICENSE))
@ -151,34 +152,33 @@ Some packages may only be included on certain architectures or operating systems
- [github.com/subosito/gotenv](https://pkg.go.dev/github.com/subosito/gotenv) ([MIT](https://github.com/subosito/gotenv/blob/v1.6.0/LICENSE))
- [github.com/theupdateframework/go-tuf](https://pkg.go.dev/github.com/theupdateframework/go-tuf) ([BSD-3-Clause](https://github.com/theupdateframework/go-tuf/blob/v0.7.0/LICENSE))
- [github.com/theupdateframework/go-tuf/v2/metadata](https://pkg.go.dev/github.com/theupdateframework/go-tuf/v2/metadata) ([Apache-2.0](https://github.com/theupdateframework/go-tuf/blob/v2.1.1/LICENSE))
- [github.com/thlib/go-timezone-local/tzlocal](https://pkg.go.dev/github.com/thlib/go-timezone-local/tzlocal) ([Unlicense](https://github.com/thlib/go-timezone-local/blob/ef149e42d28e/LICENSE))
- [github.com/thlib/go-timezone-local/tzlocal](https://pkg.go.dev/github.com/thlib/go-timezone-local/tzlocal) ([Unlicense](https://github.com/thlib/go-timezone-local/blob/v0.0.6/LICENSE))
- [github.com/titanous/rocacheck](https://pkg.go.dev/github.com/titanous/rocacheck) ([MIT](https://github.com/titanous/rocacheck/blob/afe73141d399/LICENSE))
- [github.com/transparency-dev/merkle](https://pkg.go.dev/github.com/transparency-dev/merkle) ([Apache-2.0](https://github.com/transparency-dev/merkle/blob/v0.0.2/LICENSE))
- [github.com/vbatts/tar-split/archive/tar](https://pkg.go.dev/github.com/vbatts/tar-split/archive/tar) ([BSD-3-Clause](https://github.com/vbatts/tar-split/blob/v0.12.1/LICENSE))
- [github.com/xo/terminfo](https://pkg.go.dev/github.com/xo/terminfo) ([MIT](https://github.com/xo/terminfo/blob/abceb7e1c41e/LICENSE))
- [github.com/yuin/goldmark](https://pkg.go.dev/github.com/yuin/goldmark) ([MIT](https://github.com/yuin/goldmark/blob/v1.7.12/LICENSE))
- [github.com/yuin/goldmark-emoji](https://pkg.go.dev/github.com/yuin/goldmark-emoji) ([MIT](https://github.com/yuin/goldmark-emoji/blob/v1.0.5/LICENSE))
- [github.com/zalando/go-keyring](https://pkg.go.dev/github.com/zalando/go-keyring) ([MIT](https://github.com/zalando/go-keyring/blob/v0.2.5/LICENSE))
- [go.mongodb.org/mongo-driver](https://pkg.go.dev/go.mongodb.org/mongo-driver) ([Apache-2.0](https://github.com/mongodb/mongo-go-driver/blob/v1.14.0/LICENSE))
- [github.com/yuin/goldmark-emoji](https://pkg.go.dev/github.com/yuin/goldmark-emoji) ([MIT](https://github.com/yuin/goldmark-emoji/blob/v1.0.6/LICENSE))
- [github.com/zalando/go-keyring](https://pkg.go.dev/github.com/zalando/go-keyring) ([MIT](https://github.com/zalando/go-keyring/blob/v0.2.6/LICENSE))
- [go.mongodb.org/mongo-driver](https://pkg.go.dev/go.mongodb.org/mongo-driver) ([Apache-2.0](https://github.com/mongodb/mongo-go-driver/blob/v1.17.4/LICENSE))
- [go.opentelemetry.io/auto/sdk](https://pkg.go.dev/go.opentelemetry.io/auto/sdk) ([Apache-2.0](https://github.com/open-telemetry/opentelemetry-go-instrumentation/blob/sdk/v1.1.0/sdk/LICENSE))
- [go.opentelemetry.io/otel](https://pkg.go.dev/go.opentelemetry.io/otel) ([Apache-2.0](https://github.com/open-telemetry/opentelemetry-go/blob/v1.36.0/LICENSE))
- [go.opentelemetry.io/otel/metric](https://pkg.go.dev/go.opentelemetry.io/otel/metric) ([Apache-2.0](https://github.com/open-telemetry/opentelemetry-go/blob/metric/v1.36.0/metric/LICENSE))
- [go.opentelemetry.io/otel/trace](https://pkg.go.dev/go.opentelemetry.io/otel/trace) ([Apache-2.0](https://github.com/open-telemetry/opentelemetry-go/blob/trace/v1.36.0/trace/LICENSE))
- [go.opentelemetry.io/otel](https://pkg.go.dev/go.opentelemetry.io/otel) ([Apache-2.0](https://github.com/open-telemetry/opentelemetry-go/blob/v1.37.0/LICENSE))
- [go.opentelemetry.io/otel/metric](https://pkg.go.dev/go.opentelemetry.io/otel/metric) ([Apache-2.0](https://github.com/open-telemetry/opentelemetry-go/blob/metric/v1.37.0/metric/LICENSE))
- [go.opentelemetry.io/otel/trace](https://pkg.go.dev/go.opentelemetry.io/otel/trace) ([Apache-2.0](https://github.com/open-telemetry/opentelemetry-go/blob/trace/v1.37.0/trace/LICENSE))
- [go.uber.org/multierr](https://pkg.go.dev/go.uber.org/multierr) ([MIT](https://github.com/uber-go/multierr/blob/v1.11.0/LICENSE.txt))
- [go.uber.org/zap](https://pkg.go.dev/go.uber.org/zap) ([MIT](https://github.com/uber-go/zap/blob/v1.27.0/LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.39.0:LICENSE))
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/fd00a4e0:LICENSE))
- [golang.org/x/exp/slices](https://pkg.go.dev/golang.org/x/exp/slices) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/b7579e27:LICENSE))
- [golang.org/x/mod](https://pkg.go.dev/golang.org/x/mod) ([BSD-3-Clause](https://cs.opensource.google/go/x/mod/+/v0.25.0:LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.41.0:LICENSE))
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.15.0:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.33.0:LICENSE))
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.32.0:LICENSE))
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.26.0:LICENSE))
- [google.golang.org/genproto/googleapis/api](https://pkg.go.dev/google.golang.org/genproto/googleapis/api) ([Apache-2.0](https://github.com/googleapis/go-genproto/blob/207652e42e2e/googleapis/api/LICENSE))
- [google.golang.org/genproto/googleapis/rpc/status](https://pkg.go.dev/google.golang.org/genproto/googleapis/rpc/status) ([Apache-2.0](https://github.com/googleapis/go-genproto/blob/207652e42e2e/googleapis/rpc/LICENSE))
- [google.golang.org/grpc](https://pkg.go.dev/google.golang.org/grpc) ([Apache-2.0](https://github.com/grpc/grpc-go/blob/v1.72.2/LICENSE))
- [google.golang.org/genproto/googleapis/api](https://pkg.go.dev/google.golang.org/genproto/googleapis/api) ([Apache-2.0](https://github.com/googleapis/go-genproto/blob/513f23925822/googleapis/api/LICENSE))
- [google.golang.org/genproto/googleapis/rpc/status](https://pkg.go.dev/google.golang.org/genproto/googleapis/rpc/status) ([Apache-2.0](https://github.com/googleapis/go-genproto/blob/513f23925822/googleapis/rpc/LICENSE))
- [google.golang.org/grpc](https://pkg.go.dev/google.golang.org/grpc) ([Apache-2.0](https://github.com/grpc/grpc-go/blob/v1.73.0/LICENSE))
- [google.golang.org/protobuf](https://pkg.go.dev/google.golang.org/protobuf) ([BSD-3-Clause](https://github.com/protocolbuffers/protobuf-go/blob/v1.36.6/LICENSE))
- [gopkg.in/yaml.v3](https://pkg.go.dev/gopkg.in/yaml.v3) ([MIT](https://github.com/go-yaml/yaml/blob/v3.0.1/LICENSE))
- [k8s.io/klog/v2](https://pkg.go.dev/k8s.io/klog/v2) ([Apache-2.0](https://github.com/kubernetes/klog/blob/v2.130.1/LICENSE))
[cli/cli]: https://github.com/cli/cli

View file

@ -7,65 +7,66 @@ The following open source dependencies are used to build the [cli/cli][] GitHub
Some packages may only be included on certain architectures or operating systems.
- [dario.cat/mergo](https://pkg.go.dev/dario.cat/mergo) ([BSD-3-Clause](https://github.com/imdario/mergo/blob/v1.0.1/LICENSE))
- [dario.cat/mergo](https://pkg.go.dev/dario.cat/mergo) ([BSD-3-Clause](https://github.com/imdario/mergo/blob/v1.0.2/LICENSE))
- [github.com/AlecAivazis/survey/v2](https://pkg.go.dev/github.com/AlecAivazis/survey/v2) ([MIT](https://github.com/AlecAivazis/survey/blob/v2.3.7/LICENSE))
- [github.com/AlecAivazis/survey/v2/terminal](https://pkg.go.dev/github.com/AlecAivazis/survey/v2/terminal) ([MIT](https://github.com/AlecAivazis/survey/blob/v2.3.7/terminal/LICENSE.txt))
- [github.com/MakeNowJust/heredoc](https://pkg.go.dev/github.com/MakeNowJust/heredoc) ([MIT](https://github.com/MakeNowJust/heredoc/blob/v1.0.0/LICENSE))
- [github.com/Masterminds/goutils](https://pkg.go.dev/github.com/Masterminds/goutils) ([Apache-2.0](https://github.com/Masterminds/goutils/blob/v1.1.1/LICENSE.txt))
- [github.com/Masterminds/semver/v3](https://pkg.go.dev/github.com/Masterminds/semver/v3) ([MIT](https://github.com/Masterminds/semver/blob/v3.3.0/LICENSE.txt))
- [github.com/Masterminds/semver/v3](https://pkg.go.dev/github.com/Masterminds/semver/v3) ([MIT](https://github.com/Masterminds/semver/blob/v3.4.0/LICENSE.txt))
- [github.com/Masterminds/sprig/v3](https://pkg.go.dev/github.com/Masterminds/sprig/v3) ([MIT](https://github.com/Masterminds/sprig/blob/v3.3.0/LICENSE.txt))
- [github.com/alecthomas/chroma/v2](https://pkg.go.dev/github.com/alecthomas/chroma/v2) ([MIT](https://github.com/alecthomas/chroma/blob/v2.14.0/COPYING))
- [github.com/alecthomas/chroma/v2](https://pkg.go.dev/github.com/alecthomas/chroma/v2) ([MIT](https://github.com/alecthomas/chroma/blob/v2.19.0/COPYING))
- [github.com/asaskevich/govalidator](https://pkg.go.dev/github.com/asaskevich/govalidator) ([MIT](https://github.com/asaskevich/govalidator/blob/a9d515a09cc2/LICENSE))
- [github.com/atotto/clipboard](https://pkg.go.dev/github.com/atotto/clipboard) ([BSD-3-Clause](https://github.com/atotto/clipboard/blob/v0.1.4/LICENSE))
- [github.com/aymanbagabas/go-osc52/v2](https://pkg.go.dev/github.com/aymanbagabas/go-osc52/v2) ([MIT](https://github.com/aymanbagabas/go-osc52/blob/v2.0.1/LICENSE))
- [github.com/aymerick/douceur](https://pkg.go.dev/github.com/aymerick/douceur) ([MIT](https://github.com/aymerick/douceur/blob/v0.2.0/LICENSE))
- [github.com/blang/semver](https://pkg.go.dev/github.com/blang/semver) ([MIT](https://github.com/blang/semver/blob/v3.5.1/LICENSE))
- [github.com/briandowns/spinner](https://pkg.go.dev/github.com/briandowns/spinner) ([Apache-2.0](https://github.com/briandowns/spinner/blob/v1.18.1/LICENSE))
- [github.com/briandowns/spinner](https://pkg.go.dev/github.com/briandowns/spinner) ([Apache-2.0](https://github.com/briandowns/spinner/blob/v1.23.2/LICENSE))
- [github.com/catppuccin/go](https://pkg.go.dev/github.com/catppuccin/go) ([MIT](https://github.com/catppuccin/go/blob/v0.3.0/LICENSE))
- [github.com/cenkalti/backoff/v4](https://pkg.go.dev/github.com/cenkalti/backoff/v4) ([MIT](https://github.com/cenkalti/backoff/blob/v4.3.0/LICENSE))
- [github.com/cenkalti/backoff/v5](https://pkg.go.dev/github.com/cenkalti/backoff/v5) ([MIT](https://github.com/cenkalti/backoff/blob/v5.0.2/LICENSE))
- [github.com/charmbracelet/bubbles](https://pkg.go.dev/github.com/charmbracelet/bubbles) ([MIT](https://github.com/charmbracelet/bubbles/blob/v0.21.0/LICENSE))
- [github.com/charmbracelet/bubbletea](https://pkg.go.dev/github.com/charmbracelet/bubbletea) ([MIT](https://github.com/charmbracelet/bubbletea/blob/v1.3.4/LICENSE))
- [github.com/charmbracelet/colorprofile](https://pkg.go.dev/github.com/charmbracelet/colorprofile) ([MIT](https://github.com/charmbracelet/colorprofile/blob/f60798e515dc/LICENSE))
- [github.com/charmbracelet/glamour](https://pkg.go.dev/github.com/charmbracelet/glamour) ([MIT](https://github.com/charmbracelet/glamour/blob/549f544650e3/LICENSE))
- [github.com/charmbracelet/bubbletea](https://pkg.go.dev/github.com/charmbracelet/bubbletea) ([MIT](https://github.com/charmbracelet/bubbletea/blob/v1.3.5/LICENSE))
- [github.com/charmbracelet/colorprofile](https://pkg.go.dev/github.com/charmbracelet/colorprofile) ([MIT](https://github.com/charmbracelet/colorprofile/blob/v0.3.1/LICENSE))
- [github.com/charmbracelet/glamour](https://pkg.go.dev/github.com/charmbracelet/glamour) ([MIT](https://github.com/charmbracelet/glamour/blob/v0.10.0/LICENSE))
- [github.com/charmbracelet/huh](https://pkg.go.dev/github.com/charmbracelet/huh) ([MIT](https://github.com/charmbracelet/huh/blob/v0.7.0/LICENSE))
- [github.com/charmbracelet/lipgloss](https://pkg.go.dev/github.com/charmbracelet/lipgloss) ([MIT](https://github.com/charmbracelet/lipgloss/blob/166f707985bc/LICENSE))
- [github.com/charmbracelet/x/ansi](https://pkg.go.dev/github.com/charmbracelet/x/ansi) ([MIT](https://github.com/charmbracelet/x/blob/ansi/v0.8.0/ansi/LICENSE))
- [github.com/charmbracelet/lipgloss](https://pkg.go.dev/github.com/charmbracelet/lipgloss) ([MIT](https://github.com/charmbracelet/lipgloss/blob/76690c660834/LICENSE))
- [github.com/charmbracelet/x/ansi](https://pkg.go.dev/github.com/charmbracelet/x/ansi) ([MIT](https://github.com/charmbracelet/x/blob/ansi/v0.9.3/ansi/LICENSE))
- [github.com/charmbracelet/x/cellbuf](https://pkg.go.dev/github.com/charmbracelet/x/cellbuf) ([MIT](https://github.com/charmbracelet/x/blob/cellbuf/v0.0.13/cellbuf/LICENSE))
- [github.com/charmbracelet/x/exp/strings](https://pkg.go.dev/github.com/charmbracelet/x/exp/strings) ([MIT](https://github.com/charmbracelet/x/blob/212f7b056ed0/exp/strings/LICENSE))
- [github.com/charmbracelet/x/exp/slice](https://pkg.go.dev/github.com/charmbracelet/x/exp/slice) ([MIT](https://github.com/charmbracelet/x/blob/821143405392/exp/slice/LICENSE))
- [github.com/charmbracelet/x/exp/strings](https://pkg.go.dev/github.com/charmbracelet/x/exp/strings) ([MIT](https://github.com/charmbracelet/x/blob/821143405392/exp/strings/LICENSE))
- [github.com/charmbracelet/x/term](https://pkg.go.dev/github.com/charmbracelet/x/term) ([MIT](https://github.com/charmbracelet/x/blob/term/v0.2.1/term/LICENSE))
- [github.com/cli/browser](https://pkg.go.dev/github.com/cli/browser) ([BSD-2-Clause](https://github.com/cli/browser/blob/v1.3.0/LICENSE))
- [github.com/cli/go-gh/v2](https://pkg.go.dev/github.com/cli/go-gh/v2) ([MIT](https://github.com/cli/go-gh/blob/v2.12.1/LICENSE))
- [github.com/cli/oauth](https://pkg.go.dev/github.com/cli/oauth) ([MIT](https://github.com/cli/oauth/blob/v1.1.1/LICENSE))
- [github.com/cli/oauth](https://pkg.go.dev/github.com/cli/oauth) ([MIT](https://github.com/cli/oauth/blob/v1.2.0/LICENSE))
- [github.com/cli/safeexec](https://pkg.go.dev/github.com/cli/safeexec) ([BSD-2-Clause](https://github.com/cli/safeexec/blob/v1.0.1/LICENSE))
- [github.com/cli/shurcooL-graphql](https://pkg.go.dev/github.com/cli/shurcooL-graphql) ([MIT](https://github.com/cli/shurcooL-graphql/blob/v0.0.4/LICENSE))
- [github.com/containerd/stargz-snapshotter/estargz](https://pkg.go.dev/github.com/containerd/stargz-snapshotter/estargz) ([Apache-2.0](https://github.com/containerd/stargz-snapshotter/blob/estargz/v0.16.3/estargz/LICENSE))
- [github.com/cpuguy83/go-md2man/v2/md2man](https://pkg.go.dev/github.com/cpuguy83/go-md2man/v2/md2man) ([MIT](https://github.com/cpuguy83/go-md2man/blob/v2.0.7/LICENSE.md))
- [github.com/cyberphone/json-canonicalization/go/src/webpki.org/jsoncanonicalizer](https://pkg.go.dev/github.com/cyberphone/json-canonicalization/go/src/webpki.org/jsoncanonicalizer) ([Apache-2.0](https://github.com/cyberphone/json-canonicalization/blob/57a0ce2678a7/LICENSE))
- [github.com/cyberphone/json-canonicalization/go/src/webpki.org/jsoncanonicalizer](https://pkg.go.dev/github.com/cyberphone/json-canonicalization/go/src/webpki.org/jsoncanonicalizer) ([Apache-2.0](https://github.com/cyberphone/json-canonicalization/blob/19d51d7fe467/LICENSE))
- [github.com/danieljoos/wincred](https://pkg.go.dev/github.com/danieljoos/wincred) ([MIT](https://github.com/danieljoos/wincred/blob/v1.2.2/LICENSE))
- [github.com/davecgh/go-spew/spew](https://pkg.go.dev/github.com/davecgh/go-spew/spew) ([ISC](https://github.com/davecgh/go-spew/blob/d8f796af33cc/LICENSE))
- [github.com/digitorus/pkcs7](https://pkg.go.dev/github.com/digitorus/pkcs7) ([MIT](https://github.com/digitorus/pkcs7/blob/3a137a874352/LICENSE))
- [github.com/digitorus/timestamp](https://pkg.go.dev/github.com/digitorus/timestamp) ([BSD-2-Clause](https://github.com/digitorus/timestamp/blob/220c5c2851b7/LICENSE))
- [github.com/digitorus/timestamp](https://pkg.go.dev/github.com/digitorus/timestamp) ([BSD-2-Clause](https://github.com/digitorus/timestamp/blob/c45532741eea/LICENSE))
- [github.com/distribution/reference](https://pkg.go.dev/github.com/distribution/reference) ([Apache-2.0](https://github.com/distribution/reference/blob/v0.6.0/LICENSE))
- [github.com/dlclark/regexp2](https://pkg.go.dev/github.com/dlclark/regexp2) ([MIT](https://github.com/dlclark/regexp2/blob/v1.11.0/LICENSE))
- [github.com/docker/cli/cli/config](https://pkg.go.dev/github.com/docker/cli/cli/config) ([Apache-2.0](https://github.com/docker/cli/blob/v28.2.2/LICENSE))
- [github.com/dlclark/regexp2](https://pkg.go.dev/github.com/dlclark/regexp2) ([MIT](https://github.com/dlclark/regexp2/blob/v1.11.5/LICENSE))
- [github.com/docker/cli/cli/config](https://pkg.go.dev/github.com/docker/cli/cli/config) ([Apache-2.0](https://github.com/docker/cli/blob/v28.3.0/LICENSE))
- [github.com/docker/distribution/registry/client/auth/challenge](https://pkg.go.dev/github.com/docker/distribution/registry/client/auth/challenge) ([Apache-2.0](https://github.com/docker/distribution/blob/v2.8.3/LICENSE))
- [github.com/docker/docker-credential-helpers](https://pkg.go.dev/github.com/docker/docker-credential-helpers) ([MIT](https://github.com/docker/docker-credential-helpers/blob/v0.9.3/LICENSE))
- [github.com/dustin/go-humanize](https://pkg.go.dev/github.com/dustin/go-humanize) ([MIT](https://github.com/dustin/go-humanize/blob/v1.0.1/LICENSE))
- [github.com/erikgeiser/coninput](https://pkg.go.dev/github.com/erikgeiser/coninput) ([MIT](https://github.com/erikgeiser/coninput/blob/1c3628e74d0f/LICENSE))
- [github.com/fatih/color](https://pkg.go.dev/github.com/fatih/color) ([MIT](https://github.com/fatih/color/blob/v1.16.0/LICENSE.md))
- [github.com/fsnotify/fsnotify](https://pkg.go.dev/github.com/fsnotify/fsnotify) ([BSD-3-Clause](https://github.com/fsnotify/fsnotify/blob/v1.8.0/LICENSE))
- [github.com/fatih/color](https://pkg.go.dev/github.com/fatih/color) ([MIT](https://github.com/fatih/color/blob/v1.18.0/LICENSE.md))
- [github.com/fsnotify/fsnotify](https://pkg.go.dev/github.com/fsnotify/fsnotify) ([BSD-3-Clause](https://github.com/fsnotify/fsnotify/blob/v1.9.0/LICENSE))
- [github.com/gabriel-vasile/mimetype](https://pkg.go.dev/github.com/gabriel-vasile/mimetype) ([MIT](https://github.com/gabriel-vasile/mimetype/blob/v1.4.9/LICENSE))
- [github.com/gdamore/encoding](https://pkg.go.dev/github.com/gdamore/encoding) ([Apache-2.0](https://github.com/gdamore/encoding/blob/v1.0.0/LICENSE))
- [github.com/gdamore/tcell/v2](https://pkg.go.dev/github.com/gdamore/tcell/v2) ([Apache-2.0](https://github.com/gdamore/tcell/blob/v2.5.4/LICENSE))
- [github.com/gdamore/encoding](https://pkg.go.dev/github.com/gdamore/encoding) ([Apache-2.0](https://github.com/gdamore/encoding/blob/v1.0.1/LICENSE))
- [github.com/gdamore/tcell/v2](https://pkg.go.dev/github.com/gdamore/tcell/v2) ([Apache-2.0](https://github.com/gdamore/tcell/blob/v2.8.1/LICENSE))
- [github.com/go-chi/chi](https://pkg.go.dev/github.com/go-chi/chi) ([MIT](https://github.com/go-chi/chi/blob/v4.1.2/LICENSE))
- [github.com/go-jose/go-jose/v4](https://pkg.go.dev/github.com/go-jose/go-jose/v4) ([Apache-2.0](https://github.com/go-jose/go-jose/blob/v4.0.5/LICENSE))
- [github.com/go-jose/go-jose/v4/json](https://pkg.go.dev/github.com/go-jose/go-jose/v4/json) ([BSD-3-Clause](https://github.com/go-jose/go-jose/blob/v4.0.5/json/LICENSE))
- [github.com/go-jose/go-jose/v4](https://pkg.go.dev/github.com/go-jose/go-jose/v4) ([Apache-2.0](https://github.com/go-jose/go-jose/blob/v4.1.1/LICENSE))
- [github.com/go-jose/go-jose/v4/json](https://pkg.go.dev/github.com/go-jose/go-jose/v4/json) ([BSD-3-Clause](https://github.com/go-jose/go-jose/blob/v4.1.1/json/LICENSE))
- [github.com/go-logr/logr](https://pkg.go.dev/github.com/go-logr/logr) ([Apache-2.0](https://github.com/go-logr/logr/blob/v1.4.3/LICENSE))
- [github.com/go-logr/stdr](https://pkg.go.dev/github.com/go-logr/stdr) ([Apache-2.0](https://github.com/go-logr/stdr/blob/v1.2.2/LICENSE))
- [github.com/go-openapi/analysis](https://pkg.go.dev/github.com/go-openapi/analysis) ([Apache-2.0](https://github.com/go-openapi/analysis/blob/v0.23.0/LICENSE))
- [github.com/go-openapi/errors](https://pkg.go.dev/github.com/go-openapi/errors) ([Apache-2.0](https://github.com/go-openapi/errors/blob/v0.22.1/LICENSE))
- [github.com/go-openapi/jsonpointer](https://pkg.go.dev/github.com/go-openapi/jsonpointer) ([Apache-2.0](https://github.com/go-openapi/jsonpointer/blob/v0.21.0/LICENSE))
- [github.com/go-openapi/jsonpointer](https://pkg.go.dev/github.com/go-openapi/jsonpointer) ([Apache-2.0](https://github.com/go-openapi/jsonpointer/blob/v0.21.1/LICENSE))
- [github.com/go-openapi/jsonreference](https://pkg.go.dev/github.com/go-openapi/jsonreference) ([Apache-2.0](https://github.com/go-openapi/jsonreference/blob/v0.21.0/LICENSE))
- [github.com/go-openapi/loads](https://pkg.go.dev/github.com/go-openapi/loads) ([Apache-2.0](https://github.com/go-openapi/loads/blob/v0.22.0/LICENSE))
- [github.com/go-openapi/runtime](https://pkg.go.dev/github.com/go-openapi/runtime) ([Apache-2.0](https://github.com/go-openapi/runtime/blob/v0.28.0/LICENSE))
@ -74,9 +75,9 @@ Some packages may only be included on certain architectures or operating systems
- [github.com/go-openapi/strfmt](https://pkg.go.dev/github.com/go-openapi/strfmt) ([Apache-2.0](https://github.com/go-openapi/strfmt/blob/v0.23.0/LICENSE))
- [github.com/go-openapi/swag](https://pkg.go.dev/github.com/go-openapi/swag) ([Apache-2.0](https://github.com/go-openapi/swag/blob/v0.23.1/LICENSE))
- [github.com/go-openapi/validate](https://pkg.go.dev/github.com/go-openapi/validate) ([Apache-2.0](https://github.com/go-openapi/validate/blob/v0.24.0/LICENSE))
- [github.com/go-viper/mapstructure/v2](https://pkg.go.dev/github.com/go-viper/mapstructure/v2) ([MIT](https://github.com/go-viper/mapstructure/blob/v2.2.1/LICENSE))
- [github.com/golang/snappy](https://pkg.go.dev/github.com/golang/snappy) ([BSD-3-Clause](https://github.com/golang/snappy/blob/v0.0.4/LICENSE))
- [github.com/google/certificate-transparency-go](https://pkg.go.dev/github.com/google/certificate-transparency-go) ([Apache-2.0](https://github.com/google/certificate-transparency-go/blob/v1.3.1/LICENSE))
- [github.com/go-viper/mapstructure/v2](https://pkg.go.dev/github.com/go-viper/mapstructure/v2) ([MIT](https://github.com/go-viper/mapstructure/blob/v2.3.0/LICENSE))
- [github.com/golang/snappy](https://pkg.go.dev/github.com/golang/snappy) ([BSD-3-Clause](https://github.com/golang/snappy/blob/v1.0.0/LICENSE))
- [github.com/google/certificate-transparency-go](https://pkg.go.dev/github.com/google/certificate-transparency-go) ([Apache-2.0](https://github.com/google/certificate-transparency-go/blob/v1.3.2/LICENSE))
- [github.com/google/go-containerregistry](https://pkg.go.dev/github.com/google/go-containerregistry) ([Apache-2.0](https://github.com/google/go-containerregistry/blob/v0.20.6/LICENSE))
- [github.com/google/shlex](https://pkg.go.dev/github.com/google/shlex) ([Apache-2.0](https://github.com/google/shlex/blob/e7afc7fbc510/COPYING))
- [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.6.0/LICENSE))
@ -84,22 +85,22 @@ Some packages may only be included on certain architectures or operating systems
- [github.com/gorilla/websocket](https://pkg.go.dev/github.com/gorilla/websocket) ([BSD-2-Clause](https://github.com/gorilla/websocket/blob/v1.5.3/LICENSE))
- [github.com/hashicorp/errwrap](https://pkg.go.dev/github.com/hashicorp/errwrap) ([MPL-2.0](https://github.com/hashicorp/errwrap/blob/v1.1.0/LICENSE))
- [github.com/hashicorp/go-multierror](https://pkg.go.dev/github.com/hashicorp/go-multierror) ([MPL-2.0](https://github.com/hashicorp/go-multierror/blob/v1.1.1/LICENSE))
- [github.com/hashicorp/go-version](https://pkg.go.dev/github.com/hashicorp/go-version) ([MPL-2.0](https://github.com/hashicorp/go-version/blob/v1.3.0/LICENSE))
- [github.com/hashicorp/go-version](https://pkg.go.dev/github.com/hashicorp/go-version) ([MPL-2.0](https://github.com/hashicorp/go-version/blob/v1.7.0/LICENSE))
- [github.com/henvic/httpretty](https://pkg.go.dev/github.com/henvic/httpretty) ([MIT](https://github.com/henvic/httpretty/blob/v0.1.4/LICENSE.md))
- [github.com/huandu/xstrings](https://pkg.go.dev/github.com/huandu/xstrings) ([MIT](https://github.com/huandu/xstrings/blob/v1.5.0/LICENSE))
- [github.com/in-toto/attestation/go/v1](https://pkg.go.dev/github.com/in-toto/attestation/go/v1) ([Apache-2.0](https://github.com/in-toto/attestation/blob/v1.1.2/LICENSE))
- [github.com/in-toto/in-toto-golang/in_toto](https://pkg.go.dev/github.com/in-toto/in-toto-golang/in_toto) ([Apache-2.0](https://github.com/in-toto/in-toto-golang/blob/v0.9.0/LICENSE))
- [github.com/inconshreveable/mousetrap](https://pkg.go.dev/github.com/inconshreveable/mousetrap) ([Apache-2.0](https://github.com/inconshreveable/mousetrap/blob/v1.1.0/LICENSE))
- [github.com/itchyny/gojq](https://pkg.go.dev/github.com/itchyny/gojq) ([MIT](https://github.com/itchyny/gojq/blob/v0.12.15/LICENSE))
- [github.com/itchyny/timefmt-go](https://pkg.go.dev/github.com/itchyny/timefmt-go) ([MIT](https://github.com/itchyny/timefmt-go/blob/v0.1.5/LICENSE))
- [github.com/jedisct1/go-minisign](https://pkg.go.dev/github.com/jedisct1/go-minisign) ([MIT](https://github.com/jedisct1/go-minisign/blob/1c139d1cc84b/LICENSE))
- [github.com/itchyny/gojq](https://pkg.go.dev/github.com/itchyny/gojq) ([MIT](https://github.com/itchyny/gojq/blob/v0.12.17/LICENSE))
- [github.com/itchyny/timefmt-go](https://pkg.go.dev/github.com/itchyny/timefmt-go) ([MIT](https://github.com/itchyny/timefmt-go/blob/v0.1.6/LICENSE))
- [github.com/jedisct1/go-minisign](https://pkg.go.dev/github.com/jedisct1/go-minisign) ([MIT](https://github.com/jedisct1/go-minisign/blob/d2f9f49435c7/LICENSE))
- [github.com/joho/godotenv](https://pkg.go.dev/github.com/joho/godotenv) ([MIT](https://github.com/joho/godotenv/blob/v1.5.1/LICENCE))
- [github.com/josharian/intern](https://pkg.go.dev/github.com/josharian/intern) ([MIT](https://github.com/josharian/intern/blob/v1.0.0/license.md))
- [github.com/kballard/go-shellquote](https://pkg.go.dev/github.com/kballard/go-shellquote) ([MIT](https://github.com/kballard/go-shellquote/blob/95032a82bc51/LICENSE))
- [github.com/klauspost/compress](https://pkg.go.dev/github.com/klauspost/compress) ([Apache-2.0](https://github.com/klauspost/compress/blob/v1.18.0/LICENSE))
- [github.com/klauspost/compress/internal/snapref](https://pkg.go.dev/github.com/klauspost/compress/internal/snapref) ([BSD-3-Clause](https://github.com/klauspost/compress/blob/v1.18.0/internal/snapref/LICENSE))
- [github.com/klauspost/compress/zstd/internal/xxhash](https://pkg.go.dev/github.com/klauspost/compress/zstd/internal/xxhash) ([MIT](https://github.com/klauspost/compress/blob/v1.18.0/zstd/internal/xxhash/LICENSE.txt))
- [github.com/letsencrypt/boulder](https://pkg.go.dev/github.com/letsencrypt/boulder) ([MPL-2.0](https://github.com/letsencrypt/boulder/blob/de9c06129bec/LICENSE.txt))
- [github.com/letsencrypt/boulder](https://pkg.go.dev/github.com/letsencrypt/boulder) ([MPL-2.0](https://github.com/letsencrypt/boulder/blob/v0.20250630.0/LICENSE.txt))
- [github.com/lucasb-eyer/go-colorful](https://pkg.go.dev/github.com/lucasb-eyer/go-colorful) ([MIT](https://github.com/lucasb-eyer/go-colorful/blob/v1.2.0/LICENSE))
- [github.com/mailru/easyjson](https://pkg.go.dev/github.com/mailru/easyjson) ([MIT](https://github.com/mailru/easyjson/blob/v0.9.0/LICENSE))
- [github.com/mattn/go-colorable](https://pkg.go.dev/github.com/mattn/go-colorable) ([MIT](https://github.com/mattn/go-colorable/blob/v0.1.14/LICENSE))
@ -118,34 +119,34 @@ Some packages may only be included on certain architectures or operating systems
- [github.com/muesli/cancelreader](https://pkg.go.dev/github.com/muesli/cancelreader) ([MIT](https://github.com/muesli/cancelreader/blob/v0.2.2/LICENSE))
- [github.com/muesli/reflow](https://pkg.go.dev/github.com/muesli/reflow) ([MIT](https://github.com/muesli/reflow/blob/v0.3.0/LICENSE))
- [github.com/muesli/termenv](https://pkg.go.dev/github.com/muesli/termenv) ([MIT](https://github.com/muesli/termenv/blob/v0.16.0/LICENSE))
- [github.com/muhammadmuzzammil1998/jsonc](https://pkg.go.dev/github.com/muhammadmuzzammil1998/jsonc) ([MIT](https://github.com/muhammadmuzzammil1998/jsonc/blob/615b0916ca38/LICENSE))
- [github.com/muhammadmuzzammil1998/jsonc](https://pkg.go.dev/github.com/muhammadmuzzammil1998/jsonc) ([MIT](https://github.com/muhammadmuzzammil1998/jsonc/blob/v1.0.0/LICENSE))
- [github.com/oklog/ulid](https://pkg.go.dev/github.com/oklog/ulid) ([Apache-2.0](https://github.com/oklog/ulid/blob/v1.3.1/LICENSE))
- [github.com/opencontainers/go-digest](https://pkg.go.dev/github.com/opencontainers/go-digest) ([Apache-2.0](https://github.com/opencontainers/go-digest/blob/v1.0.0/LICENSE))
- [github.com/opencontainers/image-spec/specs-go](https://pkg.go.dev/github.com/opencontainers/image-spec/specs-go) ([Apache-2.0](https://github.com/opencontainers/image-spec/blob/v1.1.1/LICENSE))
- [github.com/opentracing/opentracing-go](https://pkg.go.dev/github.com/opentracing/opentracing-go) ([Apache-2.0](https://github.com/opentracing/opentracing-go/blob/v1.2.0/LICENSE))
- [github.com/pelletier/go-toml/v2](https://pkg.go.dev/github.com/pelletier/go-toml/v2) ([MIT](https://github.com/pelletier/go-toml/blob/v2.2.3/LICENSE))
- [github.com/pelletier/go-toml/v2](https://pkg.go.dev/github.com/pelletier/go-toml/v2) ([MIT](https://github.com/pelletier/go-toml/blob/v2.2.4/LICENSE))
- [github.com/pkg/errors](https://pkg.go.dev/github.com/pkg/errors) ([BSD-2-Clause](https://github.com/pkg/errors/blob/v0.9.1/LICENSE))
- [github.com/pmezard/go-difflib/difflib](https://pkg.go.dev/github.com/pmezard/go-difflib/difflib) ([BSD-3-Clause](https://github.com/pmezard/go-difflib/blob/5d4384ee4fb2/LICENSE))
- [github.com/rivo/tview](https://pkg.go.dev/github.com/rivo/tview) ([MIT](https://github.com/rivo/tview/blob/c4a7e501810d/LICENSE.txt))
- [github.com/rivo/tview](https://pkg.go.dev/github.com/rivo/tview) ([MIT](https://github.com/rivo/tview/blob/a4a78f1e05cb/LICENSE.txt))
- [github.com/rivo/uniseg](https://pkg.go.dev/github.com/rivo/uniseg) ([MIT](https://github.com/rivo/uniseg/blob/v0.4.7/LICENSE.txt))
- [github.com/rodaine/table](https://pkg.go.dev/github.com/rodaine/table) ([MIT](https://github.com/rodaine/table/blob/v1.0.1/license))
- [github.com/rodaine/table](https://pkg.go.dev/github.com/rodaine/table) ([MIT](https://github.com/rodaine/table/blob/v1.3.0/license))
- [github.com/russross/blackfriday/v2](https://pkg.go.dev/github.com/russross/blackfriday/v2) ([BSD-2-Clause](https://github.com/russross/blackfriday/blob/v2.1.0/LICENSE.txt))
- [github.com/sagikazarmark/locafero](https://pkg.go.dev/github.com/sagikazarmark/locafero) ([MIT](https://github.com/sagikazarmark/locafero/blob/v0.7.0/LICENSE))
- [github.com/sagikazarmark/locafero](https://pkg.go.dev/github.com/sagikazarmark/locafero) ([MIT](https://github.com/sagikazarmark/locafero/blob/v0.9.0/LICENSE))
- [github.com/sassoftware/relic/lib](https://pkg.go.dev/github.com/sassoftware/relic/lib) ([Apache-2.0](https://github.com/sassoftware/relic/blob/v7.2.1/LICENSE))
- [github.com/secure-systems-lab/go-securesystemslib](https://pkg.go.dev/github.com/secure-systems-lab/go-securesystemslib) ([MIT](https://github.com/secure-systems-lab/go-securesystemslib/blob/v0.9.0/LICENSE))
- [github.com/shibumi/go-pathspec](https://pkg.go.dev/github.com/shibumi/go-pathspec) ([Apache-2.0](https://github.com/shibumi/go-pathspec/blob/v1.3.0/LICENSE))
- [github.com/shopspring/decimal](https://pkg.go.dev/github.com/shopspring/decimal) ([MIT](https://github.com/shopspring/decimal/blob/v1.4.0/LICENSE))
- [github.com/shurcooL/githubv4](https://pkg.go.dev/github.com/shurcooL/githubv4) ([MIT](https://github.com/shurcooL/githubv4/blob/18a1ae0e79dc/LICENSE))
- [github.com/shurcooL/githubv4](https://pkg.go.dev/github.com/shurcooL/githubv4) ([MIT](https://github.com/shurcooL/githubv4/blob/48295856cce7/LICENSE))
- [github.com/shurcooL/graphql](https://pkg.go.dev/github.com/shurcooL/graphql) ([MIT](https://github.com/shurcooL/graphql/blob/ed46e5a46466/LICENSE))
- [github.com/sigstore/protobuf-specs/gen/pb-go](https://pkg.go.dev/github.com/sigstore/protobuf-specs/gen/pb-go) ([Apache-2.0](https://github.com/sigstore/protobuf-specs/blob/v0.4.3/LICENSE))
- [github.com/sigstore/rekor/pkg](https://pkg.go.dev/github.com/sigstore/rekor/pkg) ([Apache-2.0](https://github.com/sigstore/rekor/blob/v1.3.10/LICENSE))
- [github.com/sigstore/sigstore-go/pkg](https://pkg.go.dev/github.com/sigstore/sigstore-go/pkg) ([Apache-2.0](https://github.com/sigstore/sigstore-go/blob/v1.0.0/LICENSE))
- [github.com/sigstore/sigstore/pkg](https://pkg.go.dev/github.com/sigstore/sigstore/pkg) ([Apache-2.0](https://github.com/sigstore/sigstore/blob/v1.9.4/LICENSE))
- [github.com/sigstore/timestamp-authority/pkg/verification](https://pkg.go.dev/github.com/sigstore/timestamp-authority/pkg/verification) ([Apache-2.0](https://github.com/sigstore/timestamp-authority/blob/v1.2.7/LICENSE))
- [github.com/sigstore/sigstore/pkg](https://pkg.go.dev/github.com/sigstore/sigstore/pkg) ([Apache-2.0](https://github.com/sigstore/sigstore/blob/v1.9.5/LICENSE))
- [github.com/sigstore/timestamp-authority/pkg/verification](https://pkg.go.dev/github.com/sigstore/timestamp-authority/pkg/verification) ([Apache-2.0](https://github.com/sigstore/timestamp-authority/blob/v1.2.8/LICENSE))
- [github.com/sirupsen/logrus](https://pkg.go.dev/github.com/sirupsen/logrus) ([MIT](https://github.com/sirupsen/logrus/blob/v1.9.3/LICENSE))
- [github.com/sourcegraph/conc](https://pkg.go.dev/github.com/sourcegraph/conc) ([MIT](https://github.com/sourcegraph/conc/blob/v0.3.0/LICENSE))
- [github.com/spf13/afero](https://pkg.go.dev/github.com/spf13/afero) ([Apache-2.0](https://github.com/spf13/afero/blob/v1.12.0/LICENSE.txt))
- [github.com/spf13/cast](https://pkg.go.dev/github.com/spf13/cast) ([MIT](https://github.com/spf13/cast/blob/v1.7.1/LICENSE))
- [github.com/spf13/afero](https://pkg.go.dev/github.com/spf13/afero) ([Apache-2.0](https://github.com/spf13/afero/blob/v1.14.0/LICENSE.txt))
- [github.com/spf13/cast](https://pkg.go.dev/github.com/spf13/cast) ([MIT](https://github.com/spf13/cast/blob/v1.9.2/LICENSE))
- [github.com/spf13/cobra](https://pkg.go.dev/github.com/spf13/cobra) ([Apache-2.0](https://github.com/spf13/cobra/blob/v1.9.1/LICENSE.txt))
- [github.com/spf13/pflag](https://pkg.go.dev/github.com/spf13/pflag) ([BSD-3-Clause](https://github.com/spf13/pflag/blob/v1.0.6/LICENSE))
- [github.com/spf13/viper](https://pkg.go.dev/github.com/spf13/viper) ([MIT](https://github.com/spf13/viper/blob/v1.20.1/LICENSE))
@ -154,34 +155,33 @@ Some packages may only be included on certain architectures or operating systems
- [github.com/subosito/gotenv](https://pkg.go.dev/github.com/subosito/gotenv) ([MIT](https://github.com/subosito/gotenv/blob/v1.6.0/LICENSE))
- [github.com/theupdateframework/go-tuf](https://pkg.go.dev/github.com/theupdateframework/go-tuf) ([BSD-3-Clause](https://github.com/theupdateframework/go-tuf/blob/v0.7.0/LICENSE))
- [github.com/theupdateframework/go-tuf/v2/metadata](https://pkg.go.dev/github.com/theupdateframework/go-tuf/v2/metadata) ([Apache-2.0](https://github.com/theupdateframework/go-tuf/blob/v2.1.1/LICENSE))
- [github.com/thlib/go-timezone-local/tzlocal](https://pkg.go.dev/github.com/thlib/go-timezone-local/tzlocal) ([Unlicense](https://github.com/thlib/go-timezone-local/blob/ef149e42d28e/LICENSE))
- [github.com/thlib/go-timezone-local/tzlocal](https://pkg.go.dev/github.com/thlib/go-timezone-local/tzlocal) ([Unlicense](https://github.com/thlib/go-timezone-local/blob/v0.0.6/LICENSE))
- [github.com/titanous/rocacheck](https://pkg.go.dev/github.com/titanous/rocacheck) ([MIT](https://github.com/titanous/rocacheck/blob/afe73141d399/LICENSE))
- [github.com/transparency-dev/merkle](https://pkg.go.dev/github.com/transparency-dev/merkle) ([Apache-2.0](https://github.com/transparency-dev/merkle/blob/v0.0.2/LICENSE))
- [github.com/vbatts/tar-split/archive/tar](https://pkg.go.dev/github.com/vbatts/tar-split/archive/tar) ([BSD-3-Clause](https://github.com/vbatts/tar-split/blob/v0.12.1/LICENSE))
- [github.com/xo/terminfo](https://pkg.go.dev/github.com/xo/terminfo) ([MIT](https://github.com/xo/terminfo/blob/abceb7e1c41e/LICENSE))
- [github.com/yuin/goldmark](https://pkg.go.dev/github.com/yuin/goldmark) ([MIT](https://github.com/yuin/goldmark/blob/v1.7.12/LICENSE))
- [github.com/yuin/goldmark-emoji](https://pkg.go.dev/github.com/yuin/goldmark-emoji) ([MIT](https://github.com/yuin/goldmark-emoji/blob/v1.0.5/LICENSE))
- [github.com/zalando/go-keyring](https://pkg.go.dev/github.com/zalando/go-keyring) ([MIT](https://github.com/zalando/go-keyring/blob/v0.2.5/LICENSE))
- [go.mongodb.org/mongo-driver](https://pkg.go.dev/go.mongodb.org/mongo-driver) ([Apache-2.0](https://github.com/mongodb/mongo-go-driver/blob/v1.14.0/LICENSE))
- [github.com/yuin/goldmark-emoji](https://pkg.go.dev/github.com/yuin/goldmark-emoji) ([MIT](https://github.com/yuin/goldmark-emoji/blob/v1.0.6/LICENSE))
- [github.com/zalando/go-keyring](https://pkg.go.dev/github.com/zalando/go-keyring) ([MIT](https://github.com/zalando/go-keyring/blob/v0.2.6/LICENSE))
- [go.mongodb.org/mongo-driver](https://pkg.go.dev/go.mongodb.org/mongo-driver) ([Apache-2.0](https://github.com/mongodb/mongo-go-driver/blob/v1.17.4/LICENSE))
- [go.opentelemetry.io/auto/sdk](https://pkg.go.dev/go.opentelemetry.io/auto/sdk) ([Apache-2.0](https://github.com/open-telemetry/opentelemetry-go-instrumentation/blob/sdk/v1.1.0/sdk/LICENSE))
- [go.opentelemetry.io/otel](https://pkg.go.dev/go.opentelemetry.io/otel) ([Apache-2.0](https://github.com/open-telemetry/opentelemetry-go/blob/v1.36.0/LICENSE))
- [go.opentelemetry.io/otel/metric](https://pkg.go.dev/go.opentelemetry.io/otel/metric) ([Apache-2.0](https://github.com/open-telemetry/opentelemetry-go/blob/metric/v1.36.0/metric/LICENSE))
- [go.opentelemetry.io/otel/trace](https://pkg.go.dev/go.opentelemetry.io/otel/trace) ([Apache-2.0](https://github.com/open-telemetry/opentelemetry-go/blob/trace/v1.36.0/trace/LICENSE))
- [go.opentelemetry.io/otel](https://pkg.go.dev/go.opentelemetry.io/otel) ([Apache-2.0](https://github.com/open-telemetry/opentelemetry-go/blob/v1.37.0/LICENSE))
- [go.opentelemetry.io/otel/metric](https://pkg.go.dev/go.opentelemetry.io/otel/metric) ([Apache-2.0](https://github.com/open-telemetry/opentelemetry-go/blob/metric/v1.37.0/metric/LICENSE))
- [go.opentelemetry.io/otel/trace](https://pkg.go.dev/go.opentelemetry.io/otel/trace) ([Apache-2.0](https://github.com/open-telemetry/opentelemetry-go/blob/trace/v1.37.0/trace/LICENSE))
- [go.uber.org/multierr](https://pkg.go.dev/go.uber.org/multierr) ([MIT](https://github.com/uber-go/multierr/blob/v1.11.0/LICENSE.txt))
- [go.uber.org/zap](https://pkg.go.dev/go.uber.org/zap) ([MIT](https://github.com/uber-go/zap/blob/v1.27.0/LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.39.0:LICENSE))
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/fd00a4e0:LICENSE))
- [golang.org/x/exp/slices](https://pkg.go.dev/golang.org/x/exp/slices) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/b7579e27:LICENSE))
- [golang.org/x/mod](https://pkg.go.dev/golang.org/x/mod) ([BSD-3-Clause](https://cs.opensource.google/go/x/mod/+/v0.25.0:LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.41.0:LICENSE))
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.15.0:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.33.0:LICENSE))
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.32.0:LICENSE))
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.26.0:LICENSE))
- [google.golang.org/genproto/googleapis/api](https://pkg.go.dev/google.golang.org/genproto/googleapis/api) ([Apache-2.0](https://github.com/googleapis/go-genproto/blob/207652e42e2e/googleapis/api/LICENSE))
- [google.golang.org/genproto/googleapis/rpc/status](https://pkg.go.dev/google.golang.org/genproto/googleapis/rpc/status) ([Apache-2.0](https://github.com/googleapis/go-genproto/blob/207652e42e2e/googleapis/rpc/LICENSE))
- [google.golang.org/grpc](https://pkg.go.dev/google.golang.org/grpc) ([Apache-2.0](https://github.com/grpc/grpc-go/blob/v1.72.2/LICENSE))
- [google.golang.org/genproto/googleapis/api](https://pkg.go.dev/google.golang.org/genproto/googleapis/api) ([Apache-2.0](https://github.com/googleapis/go-genproto/blob/513f23925822/googleapis/api/LICENSE))
- [google.golang.org/genproto/googleapis/rpc/status](https://pkg.go.dev/google.golang.org/genproto/googleapis/rpc/status) ([Apache-2.0](https://github.com/googleapis/go-genproto/blob/513f23925822/googleapis/rpc/LICENSE))
- [google.golang.org/grpc](https://pkg.go.dev/google.golang.org/grpc) ([Apache-2.0](https://github.com/grpc/grpc-go/blob/v1.73.0/LICENSE))
- [google.golang.org/protobuf](https://pkg.go.dev/google.golang.org/protobuf) ([BSD-3-Clause](https://github.com/protocolbuffers/protobuf-go/blob/v1.36.6/LICENSE))
- [gopkg.in/yaml.v3](https://pkg.go.dev/gopkg.in/yaml.v3) ([MIT](https://github.com/go-yaml/yaml/blob/v3.0.1/LICENSE))
- [k8s.io/klog/v2](https://pkg.go.dev/k8s.io/klog/v2) ([Apache-2.0](https://github.com/kubernetes/klog/blob/v2.130.1/LICENSE))
[cli/cli]: https://github.com/cli/cli

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Charmbracelet, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,60 +0,0 @@
version: 2.1
references:
images:
go: &GOLANG_IMAGE docker.mirror.hashicorp.services/circleci/golang:1.15.3
environments:
tmp: &TEST_RESULTS_PATH /tmp/test-results # path to where test results are saved
# reusable 'executor' object for jobs
executors:
go:
docker:
- image: *GOLANG_IMAGE
environment:
- TEST_RESULTS: *TEST_RESULTS_PATH
jobs:
go-test:
executor: go
steps:
- checkout
- run: mkdir -p $TEST_RESULTS
- restore_cache: # restore cache from dev-build job
keys:
- go-version-modcache-v1-{{ checksum "go.mod" }}
- run: go mod download
# Save go module cache if the go.mod file has changed
- save_cache:
key: go-version-modcache-v1-{{ checksum "go.mod" }}
paths:
- "/go/pkg/mod"
# check go fmt output because it does not report non-zero when there are fmt changes
- run:
name: check go fmt
command: |
files=$(go fmt ./...)
if [ -n "$files" ]; then
echo "The following file(s) do not conform to go fmt:"
echo "$files"
exit 1
fi
# run go tests with gotestsum
- run: |
PACKAGE_NAMES=$(go list ./...)
gotestsum --format=short-verbose --junitfile $TEST_RESULTS/gotestsum-report.xml -- $PACKAGE_NAMES
- store_test_results:
path: *TEST_RESULTS_PATH
- store_artifacts:
path: *TEST_RESULTS_PATH
workflows:
version: 2
test-and-build:
jobs:
- go-test

View file

@ -0,0 +1,25 @@
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "daily"
labels: ["dependencies"]
- package-ecosystem: github-actions
directory: /
schedule:
interval: monthly
labels:
- dependencies
# only update HashiCorp actions, external actions managed by TSCCR
allow:
- dependency-name: hashicorp/*
groups:
github-actions-breaking:
update-types:
- major
github-actions-backward-compatible:
update-types:
- minor
- patch

View file

@ -0,0 +1,74 @@
name: go-tests
on: [push]
env:
TEST_RESULTS: /tmp/test-results
jobs:
go-tests:
runs-on: ubuntu-latest
strategy:
matrix:
go-version: [ 1.15.3, 1.19 ]
steps:
- name: Setup go
uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # v3.5.0
with:
go-version: ${{ matrix.go-version }}
- name: Checkout code
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
- name: Create test directory
run: |
mkdir -p ${{ env.TEST_RESULTS }}
- name: Download go modules
run: go mod download
- name: Cache / restore go modules
uses: actions/cache@69d9d449aced6a2ede0bc19182fadc3a0a42d2b0 # v3.2.6
with:
path: |
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
# Check go fmt output because it does not report non-zero when there are fmt changes
- name: Run gofmt
run: |
go fmt ./...
files=$(go fmt ./...)
if [ -n "$files" ]; then
echo "The following file(s) do not conform to go fmt:"
echo "$files"
exit 1
fi
# Install gotestsum with go get for 1.15.3; otherwise default to go install
- name: Install gotestsum
run: |
GTS="gotest.tools/gotestsum@v1.8.2"
# We use the same error message prefix in either failure case, so just define it once here.
ERROR="Failed to install $GTS"
# First try to 'go install', if that fails try 'go get'...
go install "$GTS" || go get "$GTS" || { echo "$ERROR: both 'go install' and 'go get' failed"; exit 1; }
# Check that the gotestsum command was actually installed in the path...
command -v gotestsum > /dev/null 2>&1 || { echo "$ERROR: gotestsum command not installed"; exit 1; }
echo "OK: Command 'gotestsum' installed ($GTS)"
- name: Run go tests
run: |
PACKAGE_NAMES=$(go list ./...)
gotestsum --format=short-verbose --junitfile $TEST_RESULTS/gotestsum-report.xml -- $PACKAGE_NAMES
# Save coverage report parts
- name: Upload and save artifacts
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
with:
name: Test Results
path: ${{ env.TEST_RESULTS }}

View file

@ -1,3 +1,42 @@
# 1.7.0 (May 24, 2024)
ENHANCEMENTS:
- Remove `reflect` dependency ([#91](https://github.com/hashicorp/go-version/pull/91))
- Implement the `database/sql.Scanner` and `database/sql/driver.Value` interfaces for `Version` ([#133](https://github.com/hashicorp/go-version/pull/133))
INTERNAL:
- [COMPLIANCE] Add Copyright and License Headers ([#115](https://github.com/hashicorp/go-version/pull/115))
- [COMPLIANCE] Update MPL-2.0 LICENSE ([#105](https://github.com/hashicorp/go-version/pull/105))
- Bump actions/cache from 3.0.11 to 3.2.5 ([#116](https://github.com/hashicorp/go-version/pull/116))
- Bump actions/checkout from 3.2.0 to 3.3.0 ([#111](https://github.com/hashicorp/go-version/pull/111))
- Bump actions/upload-artifact from 3.1.1 to 3.1.2 ([#112](https://github.com/hashicorp/go-version/pull/112))
- GHA Migration ([#103](https://github.com/hashicorp/go-version/pull/103))
- github: Pin external GitHub Actions to hashes ([#107](https://github.com/hashicorp/go-version/pull/107))
- SEC-090: Automated trusted workflow pinning (2023-04-05) ([#124](https://github.com/hashicorp/go-version/pull/124))
- update readme ([#104](https://github.com/hashicorp/go-version/pull/104))
# 1.6.0 (June 28, 2022)
FEATURES:
- Add `Prerelease` function to `Constraint` to return true if the version includes a prerelease field ([#100](https://github.com/hashicorp/go-version/pull/100))
# 1.5.0 (May 18, 2022)
FEATURES:
- Use `encoding` `TextMarshaler` & `TextUnmarshaler` instead of JSON equivalents ([#95](https://github.com/hashicorp/go-version/pull/95))
- Add JSON handlers to allow parsing from/to JSON ([#93](https://github.com/hashicorp/go-version/pull/93))
# 1.4.0 (January 5, 2022)
FEATURES:
- Introduce `MustConstraints()` ([#87](https://github.com/hashicorp/go-version/pull/87))
- `Constraints`: Introduce `Equals()` and `sort.Interface` methods ([#88](https://github.com/hashicorp/go-version/pull/88))
# 1.3.0 (March 31, 2021)
Please note that CHANGELOG.md does not exist in the source code prior to this release.

View file

@ -1,3 +1,5 @@
Copyright (c) 2014 HashiCorp, Inc.
Mozilla Public License, version 2.0
1. Definitions

View file

@ -1,5 +1,5 @@
# Versioning Library for Go
[![Build Status](https://circleci.com/gh/hashicorp/go-version/tree/master.svg?style=svg)](https://circleci.com/gh/hashicorp/go-version/tree/master)
![Build Status](https://github.com/hashicorp/go-version/actions/workflows/go-tests.yml/badge.svg)
[![GoDoc](https://godoc.org/github.com/hashicorp/go-version?status.svg)](https://godoc.org/github.com/hashicorp/go-version)
go-version is a library for parsing versions and version constraints,

View file

@ -1,9 +1,12 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package version
import (
"fmt"
"reflect"
"regexp"
"sort"
"strings"
)
@ -11,30 +14,40 @@ import (
// ">= 1.0".
type Constraint struct {
f constraintFunc
op operator
check *Version
original string
}
func (c *Constraint) Equals(con *Constraint) bool {
return c.op == con.op && c.check.Equal(con.check)
}
// Constraints is a slice of constraints. We make a custom type so that
// we can add methods to it.
type Constraints []*Constraint
type constraintFunc func(v, c *Version) bool
var constraintOperators map[string]constraintFunc
var constraintOperators map[string]constraintOperation
type constraintOperation struct {
op operator
f constraintFunc
}
var constraintRegexp *regexp.Regexp
func init() {
constraintOperators = map[string]constraintFunc{
"": constraintEqual,
"=": constraintEqual,
"!=": constraintNotEqual,
">": constraintGreaterThan,
"<": constraintLessThan,
">=": constraintGreaterThanEqual,
"<=": constraintLessThanEqual,
"~>": constraintPessimistic,
constraintOperators = map[string]constraintOperation{
"": {op: equal, f: constraintEqual},
"=": {op: equal, f: constraintEqual},
"!=": {op: notEqual, f: constraintNotEqual},
">": {op: greaterThan, f: constraintGreaterThan},
"<": {op: lessThan, f: constraintLessThan},
">=": {op: greaterThanEqual, f: constraintGreaterThanEqual},
"<=": {op: lessThanEqual, f: constraintLessThanEqual},
"~>": {op: pessimistic, f: constraintPessimistic},
}
ops := make([]string, 0, len(constraintOperators))
@ -66,6 +79,16 @@ func NewConstraint(v string) (Constraints, error) {
return Constraints(result), nil
}
// MustConstraints is a helper that wraps a call to a function
// returning (Constraints, error) and panics if error is non-nil.
func MustConstraints(c Constraints, err error) Constraints {
if err != nil {
panic(err)
}
return c
}
// Check tests if a version satisfies all the constraints.
func (cs Constraints) Check(v *Version) bool {
for _, c := range cs {
@ -77,6 +100,56 @@ func (cs Constraints) Check(v *Version) bool {
return true
}
// Equals compares Constraints with other Constraints
// for equality. This may not represent logical equivalence
// of compared constraints.
// e.g. even though '>0.1,>0.2' is logically equivalent
// to '>0.2' it is *NOT* treated as equal.
//
// Missing operator is treated as equal to '=', whitespaces
// are ignored and constraints are sorted before comaparison.
func (cs Constraints) Equals(c Constraints) bool {
if len(cs) != len(c) {
return false
}
// make copies to retain order of the original slices
left := make(Constraints, len(cs))
copy(left, cs)
sort.Stable(left)
right := make(Constraints, len(c))
copy(right, c)
sort.Stable(right)
// compare sorted slices
for i, con := range left {
if !con.Equals(right[i]) {
return false
}
}
return true
}
func (cs Constraints) Len() int {
return len(cs)
}
func (cs Constraints) Less(i, j int) bool {
if cs[i].op < cs[j].op {
return true
}
if cs[i].op > cs[j].op {
return false
}
return cs[i].check.LessThan(cs[j].check)
}
func (cs Constraints) Swap(i, j int) {
cs[i], cs[j] = cs[j], cs[i]
}
// Returns the string format of the constraints
func (cs Constraints) String() string {
csStr := make([]string, len(cs))
@ -92,6 +165,12 @@ func (c *Constraint) Check(v *Version) bool {
return c.f(v, c.check)
}
// Prerelease returns true if the version underlying this constraint
// contains a prerelease field.
func (c *Constraint) Prerelease() bool {
return len(c.check.Prerelease()) > 0
}
func (c *Constraint) String() string {
return c.original
}
@ -107,8 +186,11 @@ func parseSingle(v string) (*Constraint, error) {
return nil, err
}
cop := constraintOperators[matches[1]]
return &Constraint{
f: constraintOperators[matches[1]],
f: cop.f,
op: cop.op,
check: check,
original: v,
}, nil
@ -119,7 +201,7 @@ func prereleaseCheck(v, c *Version) bool {
case cPre && vPre:
// A constraint with a pre-release can only match a pre-release version
// with the same base segments.
return reflect.DeepEqual(c.Segments64(), v.Segments64())
return v.equalSegments(c)
case !cPre && vPre:
// A constraint without a pre-release can only match a version without a
@ -138,6 +220,18 @@ func prereleaseCheck(v, c *Version) bool {
// Constraint functions
//-------------------------------------------------------------------
type operator rune
const (
equal operator = '='
notEqual operator = '≠'
greaterThan operator = '>'
lessThan operator = '<'
greaterThanEqual operator = '≥'
lessThanEqual operator = '≤'
pessimistic operator = '~'
)
func constraintEqual(v, c *Version) bool {
return v.Equal(c)
}

View file

@ -1,6 +1,12 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package version
import (
"fmt"
"reflect"
"sort"
"testing"
)
@ -97,6 +103,132 @@ func TestConstraintCheck(t *testing.T) {
}
}
func TestConstraintPrerelease(t *testing.T) {
cases := []struct {
constraint string
prerelease bool
}{
{"= 1.0", false},
{"= 1.0-beta", true},
{"~> 2.1.0", false},
{"~> 2.1.0-dev", true},
{"> 2.0", false},
{">= 2.1.0-a", true},
}
for _, tc := range cases {
c, err := parseSingle(tc.constraint)
if err != nil {
t.Fatalf("err: %s", err)
}
actual := c.Prerelease()
expected := tc.prerelease
if actual != expected {
t.Fatalf("Constraint: %s\nExpected: %#v",
tc.constraint, expected)
}
}
}
func TestConstraintEqual(t *testing.T) {
cases := []struct {
leftConstraint string
rightConstraint string
expectedEqual bool
}{
{
"0.0.1",
"0.0.1",
true,
},
{ // whitespaces
" 0.0.1 ",
"0.0.1",
true,
},
{ // equal op implied
"=0.0.1 ",
"0.0.1",
true,
},
{ // version difference
"=0.0.1",
"=0.0.2",
false,
},
{ // operator difference
">0.0.1",
"=0.0.1",
false,
},
{ // different order
">0.1.0, <=1.0.0",
"<=1.0.0, >0.1.0",
true,
},
}
for _, tc := range cases {
leftCon, err := NewConstraint(tc.leftConstraint)
if err != nil {
t.Fatalf("err: %s", err)
}
rightCon, err := NewConstraint(tc.rightConstraint)
if err != nil {
t.Fatalf("err: %s", err)
}
actual := leftCon.Equals(rightCon)
if actual != tc.expectedEqual {
t.Fatalf("Constraints: %s vs %s\nExpected: %t\nActual: %t",
tc.leftConstraint, tc.rightConstraint, tc.expectedEqual, actual)
}
}
}
func TestConstraint_sort(t *testing.T) {
cases := []struct {
constraint string
expectedConstraints string
}{
{
">= 0.1.0,< 1.12",
"< 1.12,>= 0.1.0",
},
{
"< 1.12,>= 0.1.0",
"< 1.12,>= 0.1.0",
},
{
"< 1.12,>= 0.1.0,0.2.0",
"< 1.12,0.2.0,>= 0.1.0",
},
{
">1.0,>0.1.0,>0.3.0,>0.2.0",
">0.1.0,>0.2.0,>0.3.0,>1.0",
},
}
for i, tc := range cases {
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
c, err := NewConstraint(tc.constraint)
if err != nil {
t.Fatalf("err: %s", err)
}
sort.Sort(c)
actual := c.String()
if !reflect.DeepEqual(actual, tc.expectedConstraints) {
t.Fatalf("unexpected order\nexpected: %#v\nactual: %#v",
tc.expectedConstraints, actual)
}
})
}
}
func TestConstraintsString(t *testing.T) {
cases := []struct {
constraint string

View file

@ -1,9 +1,12 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package version
import (
"bytes"
"database/sql/driver"
"fmt"
"reflect"
"regexp"
"strconv"
"strings"
@ -64,7 +67,6 @@ func newVersion(v string, pattern *regexp.Regexp) (*Version, error) {
}
segmentsStr := strings.Split(matches[1], ".")
segments := make([]int64, len(segmentsStr))
si := 0
for i, str := range segmentsStr {
val, err := strconv.ParseInt(str, 10, 64)
if err != nil {
@ -72,8 +74,7 @@ func newVersion(v string, pattern *regexp.Regexp) (*Version, error) {
"Error parsing version: %s", err)
}
segments[i] = int64(val)
si++
segments[i] = val
}
// Even though we could support more than three segments, if we
@ -92,7 +93,7 @@ func newVersion(v string, pattern *regexp.Regexp) (*Version, error) {
metadata: matches[10],
pre: pre,
segments: segments,
si: si,
si: len(segmentsStr),
original: v,
}, nil
}
@ -119,11 +120,8 @@ func (v *Version) Compare(other *Version) int {
return 0
}
segmentsSelf := v.Segments64()
segmentsOther := other.Segments64()
// If the segments are the same, we must compare on prerelease info
if reflect.DeepEqual(segmentsSelf, segmentsOther) {
if v.equalSegments(other) {
preSelf := v.Prerelease()
preOther := other.Prerelease()
if preSelf == "" && preOther == "" {
@ -139,6 +137,8 @@ func (v *Version) Compare(other *Version) int {
return comparePrereleases(preSelf, preOther)
}
segmentsSelf := v.Segments64()
segmentsOther := other.Segments64()
// Get the highest specificity (hS), or if they're equal, just use segmentSelf length
lenSelf := len(segmentsSelf)
lenOther := len(segmentsOther)
@ -162,7 +162,7 @@ func (v *Version) Compare(other *Version) int {
// this means Other had the lower specificity
// Check to see if the remaining segments in Self are all zeros -
if !allZero(segmentsSelf[i:]) {
//if not, it means that Self has to be greater than Other
// if not, it means that Self has to be greater than Other
return 1
}
break
@ -182,6 +182,21 @@ func (v *Version) Compare(other *Version) int {
return 0
}
func (v *Version) equalSegments(other *Version) bool {
segmentsSelf := v.Segments64()
segmentsOther := other.Segments64()
if len(segmentsSelf) != len(segmentsOther) {
return false
}
for i, v := range segmentsSelf {
if v != segmentsOther[i] {
return false
}
}
return true
}
func allZero(segs []int64) bool {
for _, s := range segs {
if s != 0 {
@ -390,3 +405,37 @@ func (v *Version) String() string {
func (v *Version) Original() string {
return v.original
}
// UnmarshalText implements encoding.TextUnmarshaler interface.
func (v *Version) UnmarshalText(b []byte) error {
temp, err := NewVersion(string(b))
if err != nil {
return err
}
*v = *temp
return nil
}
// MarshalText implements encoding.TextMarshaler interface.
func (v *Version) MarshalText() ([]byte, error) {
return []byte(v.String()), nil
}
// Scan implements the sql.Scanner interface.
func (v *Version) Scan(src interface{}) error {
switch src := src.(type) {
case string:
return v.UnmarshalText([]byte(src))
case nil:
return nil
default:
return fmt.Errorf("cannot scan %T as Version", src)
}
}
// Value implements the driver.Valuer interface.
func (v *Version) Value() (driver.Value, error) {
return v.String(), nil
}

View file

@ -1,3 +1,6 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package version
// Collection is a type that implements the sort.Interface interface

View file

@ -1,3 +1,6 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package version
import (

View file

@ -1,6 +1,11 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package version
import (
"encoding/json"
"fmt"
"reflect"
"testing"
)
@ -21,13 +26,13 @@ func TestNewVersion(t *testing.T) {
{"1.2-beta.5", false},
{"\n1.2", true},
{"1.2.0-x.Y.0+metadata", false},
{"1.2.0-x.Y.0+metadata-width-hypen", false},
{"1.2.3-rc1-with-hypen", false},
{"1.2.0-x.Y.0+metadata-width-hyphen", false},
{"1.2.3-rc1-with-hyphen", false},
{"1.2.3.4", false},
{"1.2.0.4-x.Y.0+metadata", false},
{"1.2.0.4-x.Y.0+metadata-width-hypen", false},
{"1.2.0.4-x.Y.0+metadata-width-hyphen", false},
{"1.2.0-X-1.2.0+metadata~dist", false},
{"1.2.3.4-rc1-with-hypen", false},
{"1.2.3.4-rc1-with-hyphen", false},
{"1.2.3.4", false},
{"v1.2.3", false},
{"foo1.2.3", true},
@ -62,13 +67,13 @@ func TestNewSemver(t *testing.T) {
{"1.2-beta.5", false},
{"\n1.2", true},
{"1.2.0-x.Y.0+metadata", false},
{"1.2.0-x.Y.0+metadata-width-hypen", false},
{"1.2.3-rc1-with-hypen", false},
{"1.2.0-x.Y.0+metadata-width-hyphen", false},
{"1.2.3-rc1-with-hyphen", false},
{"1.2.3.4", false},
{"1.2.0.4-x.Y.0+metadata", false},
{"1.2.0.4-x.Y.0+metadata-width-hypen", false},
{"1.2.0.4-x.Y.0+metadata-width-hyphen", false},
{"1.2.0-X-1.2.0+metadata~dist", false},
{"1.2.3.4-rc1-with-hypen", false},
{"1.2.3.4-rc1-with-hyphen", false},
{"1.2.3.4", false},
{"v1.2.3", false},
{"foo1.2.3", true},
@ -393,6 +398,75 @@ func TestVersionSegments64(t *testing.T) {
}
}
func TestJsonMarshal(t *testing.T) {
cases := []struct {
version string
err bool
}{
{"1.2.3", false},
{"1.2.0-x.Y.0+metadata", false},
{"1.2.0-x.Y.0+metadata-width-hyphen", false},
{"1.2.3-rc1-with-hyphen", false},
{"1.2.3.4", false},
{"1.2.0.4-x.Y.0+metadata", false},
{"1.2.0.4-x.Y.0+metadata-width-hyphen", false},
{"1.2.0-X-1.2.0+metadata~dist", false},
{"1.2.3.4-rc1-with-hyphen", false},
{"1.2.3.4", false},
}
for _, tc := range cases {
v, err1 := NewVersion(tc.version)
if err1 != nil {
t.Fatalf("error for version %q: %s", tc.version, err1)
}
parsed, err2 := json.Marshal(v)
if err2 != nil {
t.Fatalf("error marshaling version %q: %s", tc.version, err2)
}
result := string(parsed)
expected := fmt.Sprintf("%q", tc.version)
if result != expected && !tc.err {
t.Fatalf("Error marshaling unexpected marshaled content: result=%q expected=%q", result, expected)
}
}
}
func TestJsonUnmarshal(t *testing.T) {
cases := []struct {
version string
err bool
}{
{"1.2.3", false},
{"1.2.0-x.Y.0+metadata", false},
{"1.2.0-x.Y.0+metadata-width-hyphen", false},
{"1.2.3-rc1-with-hyphen", false},
{"1.2.3.4", false},
{"1.2.0.4-x.Y.0+metadata", false},
{"1.2.0.4-x.Y.0+metadata-width-hyphen", false},
{"1.2.0-X-1.2.0+metadata~dist", false},
{"1.2.3.4-rc1-with-hyphen", false},
{"1.2.3.4", false},
}
for _, tc := range cases {
expected, err1 := NewVersion(tc.version)
if err1 != nil {
t.Fatalf("err: %s", err1)
}
actual := &Version{}
err2 := json.Unmarshal([]byte(fmt.Sprintf("%q", tc.version)), actual)
if err2 != nil {
t.Fatalf("error unmarshaling version: %s", err2)
}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("error unmarshaling, unexpected object content: actual=%q expected=%q", actual, expected)
}
}
}
func TestVersionString(t *testing.T) {
cases := [][]string{
{"1.2.3", "1.2.3"},

View file

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2018-2021 Frank Denis
Copyright (c) 2018-2024 Frank Denis
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View file

@ -27,7 +27,7 @@ jobs:
# tags and 5 tests there would be 10 jobs run.
b:
# The type of runner that the job will run on
runs-on: ubuntu-20.04
runs-on: ubuntu-24.04
strategy:
# When set to true, GitHub cancels all in-progress jobs if any matrix job fails. Default: true
@ -36,7 +36,7 @@ jobs:
matrix:
# Add additional docker image tags here and all tests will be run with the additional image.
BOULDER_TOOLS_TAG:
- go1.22.3_2024-05-22
- go1.24.4_2025-06-06
# Tests command definitions. Use the entire "docker compose" command you want to run.
tests:
# Run ./test.sh --help for a description of each of the flags.
@ -71,7 +71,7 @@ jobs:
- name: Docker Login
# You may pin to the exact commit or the version.
# uses: docker/login-action@f3364599c6aa293cdc2b8391b1b56d0c30e45c8a
uses: docker/login-action@v3.2.0
uses: docker/login-action@v3.4.0
with:
# Username used to log against the Docker registry
username: ${{ secrets.DOCKER_USERNAME}}
@ -95,7 +95,7 @@ jobs:
run: ${{ matrix.tests }}
govulncheck:
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
strategy:
fail-fast: false
@ -117,12 +117,12 @@ jobs:
run: go run golang.org/x/vuln/cmd/govulncheck@latest ./...
vendorcheck:
runs-on: ubuntu-20.04
runs-on: ubuntu-24.04
strategy:
# When set to true, GitHub cancels all in-progress jobs if any matrix job fails. Default: true
fail-fast: false
matrix:
go-version: [ '1.22.2' ]
go-version: [ '1.24.1' ]
steps:
# Checks out your repository under $GITHUB_WORKSPACE, so your job can access it
@ -153,7 +153,7 @@ jobs:
permissions:
contents: none
if: ${{ always() }}
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
name: Boulder CI Test Matrix
needs:
- b

View file

@ -0,0 +1,53 @@
name: Check for IANA special-purpose address registry updates
on:
schedule:
- cron: "20 16 * * *"
workflow_dispatch:
jobs:
check-iana-registries:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout iana/data from main branch
uses: actions/checkout@v4
with:
sparse-checkout: iana/data
# If the branch already exists, this will fail, which will remind us about
# the outstanding PR.
- name: Create an iana-registries-gha branch
run: |
git checkout --track origin/main -b iana-registries-gha
- name: Retrieve the IANA special-purpose address registries
run: |
IANA_IPV4="https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry-1.csv"
IANA_IPV6="https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry-1.csv"
REPO_IPV4="iana/data/iana-ipv4-special-registry-1.csv"
REPO_IPV6="iana/data/iana-ipv6-special-registry-1.csv"
curl --fail --location --show-error --silent --output "${REPO_IPV4}" "${IANA_IPV4}"
curl --fail --location --show-error --silent --output "${REPO_IPV6}" "${IANA_IPV6}"
- name: Create a commit and pull request
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell:
bash
# `git diff --exit-code` returns an error code if there are any changes.
run: |
if ! git diff --exit-code; then
git add iana/data/
git config user.name "Irwin the IANA Bot"
git commit \
--message "Update IANA special-purpose address registries"
git push origin HEAD
gh pr create --fill
fi

View file

@ -2,7 +2,7 @@ name: Check PR for configuration and SQL changes
on:
pull_request:
types: [ready_for_review, review_requested]
types: [review_requested]
paths:
- 'test/config-next/*.json'
- 'test/config-next/*.yaml'

View file

@ -0,0 +1,17 @@
# This GitHub Action runs only on pushes to main or a hotfix branch. It can
# be used by tag protection rules to ensure that tags may only be pushed if
# their corresponding commit was first pushed to one of those branches.
name: Merged to main (or hotfix)
on:
push:
branches:
- main
- release-branch-*
jobs:
merged-to-main:
name: Merged to main (or hotfix)
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false

View file

@ -15,26 +15,31 @@ jobs:
fail-fast: false
matrix:
GO_VERSION:
- "1.22.3"
runs-on: ubuntu-20.04
- "1.24.4"
runs-on: ubuntu-24.04
permissions:
contents: write
packages: write
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
fetch-depth: '0' # Needed for verify-release-ancestry.sh to see origin/main
- name: Verify release ancestry
run: ./tools/verify-release-ancestry.sh "$GITHUB_SHA"
- name: Build .deb
id: build
env:
GO_VERSION: ${{ matrix.GO_VERSION }}
run: ./tools/make-assets.sh
run: docker run -v $PWD:/boulder -e GO_VERSION=$GO_VERSION -e COMMIT_ID="$(git rev-parse --short=8 HEAD)" ubuntu:24.04 bash -c 'apt update && apt -y install gnupg2 curl sudo git gcc && cd /boulder/ && ./tools/make-assets.sh'
- name: Compute checksums
id: checksums
# The files listed on this line must be identical to the files uploaded
# in the last step.
run: sha256sum boulder*.deb boulder*.tar.gz >| checksums.txt
run: sha256sum boulder*.deb boulder*.tar.gz >| boulder-${{ matrix.GO_VERSION }}.$(date +%s)-$(git rev-parse --short=8 HEAD).checksums.txt
- name: Create release
env:
@ -47,4 +52,15 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# https://cli.github.com/manual/gh_release_upload
run: gh release upload "${GITHUB_REF_NAME}" boulder*.deb boulder*.tar.gz checksums.txt
run: gh release upload "${GITHUB_REF_NAME}" boulder*.deb boulder*.tar.gz boulder*.checksums.txt
- name: Build ct-test-srv Container
run: docker buildx build . --build-arg "GO_VERSION=${{ matrix.GO_VERSION }}" -f test/ct-test-srv/Dockerfile -t "ghcr.io/letsencrypt/ct-test-srv:${{ github.ref_name }}-go${{ matrix.GO_VERSION }}"
- name: Login to ghcr.io
run: printenv GITHUB_TOKEN | docker login ghcr.io -u "${{ github.actor }}" --password-stdin
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Push ct-test-srv Container
run: docker push "ghcr.io/letsencrypt/ct-test-srv:${{ github.ref_name }}-go${{ matrix.GO_VERSION }}"

View file

@ -8,6 +8,7 @@ on:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
jobs:
try-release:
@ -15,8 +16,8 @@ jobs:
fail-fast: false
matrix:
GO_VERSION:
- "1.22.3"
runs-on: ubuntu-20.04
- "1.24.4"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
@ -26,10 +27,21 @@ jobs:
id: build
env:
GO_VERSION: ${{ matrix.GO_VERSION }}
run: ./tools/make-assets.sh
run: docker run -v $PWD:/boulder -e GO_VERSION=$GO_VERSION -e COMMIT_ID="$(git rev-parse --short=8 HEAD)" ubuntu:24.04 bash -c 'apt update && apt -y install gnupg2 curl sudo git gcc && cd /boulder/ && ./tools/make-assets.sh'
- name: Compute checksums
id: checksums
# The files listed on this line must be identical to the files uploaded
# in the last step of the real release action.
run: sha256sum boulder*.deb boulder*.tar.gz
run: sha256sum boulder*.deb boulder*.tar.gz >| boulder-${{ matrix.GO_VERSION }}.$(date +%s)-$(git rev-parse --short=8 HEAD).checksums.txt
- name: List files
id: files
run: ls boulder*.deb boulder*.tar.gz boulder*.checksums.txt
- name: Show checksums
id: check
run: cat boulder*.checksums.txt
- name: Build ct-test-srv Container
run: docker buildx build . --build-arg "GO_VERSION=${{ matrix.GO_VERSION }}" -f test/ct-test-srv/Dockerfile -t "ghcr.io/letsencrypt/ct-test-srv:${{ github.sha }}-go${{ matrix.GO_VERSION }}"

View file

@ -1,60 +1,89 @@
version: "2"
linters:
disable-all: true
default: none
enable:
- asciicheck
- bidichk
- errcheck
- gofmt
- gosec
- gosimple
- govet
- ineffassign
- misspell
- typecheck
- nolintlint
- spancheck
- sqlclosecheck
- staticcheck
- unconvert
- unparam
- unused
# TODO(#6202): Re-enable 'wastedassign' linter
linters-settings:
errcheck:
exclude-functions:
- (net/http.ResponseWriter).Write
- (net.Conn).Write
- encoding/binary.Write
- io.Write
- net/http.Write
- os.Remove
- github.com/miekg/dns.WriteMsg
gosimple:
# S1029: Range over the string directly
checks: ["all", "-S1029"]
govet:
enable-all: true
disable:
- fieldalignment
- shadow
settings:
printf:
funcs:
- (github.com/letsencrypt/boulder/log.Logger).Errf
- (github.com/letsencrypt/boulder/log.Logger).Warningf
- (github.com/letsencrypt/boulder/log.Logger).Infof
- (github.com/letsencrypt/boulder/log.Logger).Debugf
- (github.com/letsencrypt/boulder/log.Logger).AuditInfof
- (github.com/letsencrypt/boulder/log.Logger).AuditErrf
- (github.com/letsencrypt/boulder/ocsp/responder).SampledError
- (github.com/letsencrypt/boulder/web.RequestEvent).AddError
gosec:
excludes:
# TODO: Identify, fix, and remove violations of most of these rules
- G101 # Potential hardcoded credentials
- G102 # Binds to all network interfaces
- G107 # Potential HTTP request made with variable url
- G201 # SQL string formatting
- G202 # SQL string concatenation
- G306 # Expect WriteFile permissions to be 0600 or less
- G401 # Use of weak cryptographic primitive
- G402 # TLS InsecureSkipVerify set true.
- G403 # RSA keys should be at least 2048 bits
- G404 # Use of weak random number generator (math/rand instead of crypto/rand)
- G501 # Blacklisted import `crypto/md5`: weak cryptographic primitive
- G505 # Blacklisted import `crypto/sha1`: weak cryptographic primitive
- G601 # Implicit memory aliasing in for loop (this is fixed by go1.22)
- wastedassign
settings:
errcheck:
exclude-functions:
- (net/http.ResponseWriter).Write
- (net.Conn).Write
- encoding/binary.Write
- io.Write
- net/http.Write
- os.Remove
- github.com/miekg/dns.WriteMsg
govet:
disable:
- fieldalignment
- shadow
enable-all: true
settings:
printf:
funcs:
- (github.com/letsencrypt/boulder/log.Logger).Errf
- (github.com/letsencrypt/boulder/log.Logger).Warningf
- (github.com/letsencrypt/boulder/log.Logger).Infof
- (github.com/letsencrypt/boulder/log.Logger).Debugf
- (github.com/letsencrypt/boulder/log.Logger).AuditInfof
- (github.com/letsencrypt/boulder/log.Logger).AuditErrf
- (github.com/letsencrypt/boulder/ocsp/responder).SampledError
- (github.com/letsencrypt/boulder/web.RequestEvent).AddError
gosec:
excludes:
# TODO: Identify, fix, and remove violations of most of these rules
- G101 # Potential hardcoded credentials
- G102 # Binds to all network interfaces
- G104 # Errors unhandled
- G107 # Potential HTTP request made with variable url
- G201 # SQL string formatting
- G202 # SQL string concatenation
- G204 # Subprocess launched with variable
- G302 # Expect file permissions to be 0600 or less
- G306 # Expect WriteFile permissions to be 0600 or less
- G304 # Potential file inclusion via variable
- G401 # Use of weak cryptographic primitive
- G402 # TLS InsecureSkipVerify set true.
- G403 # RSA keys should be at least 2048 bits
- G404 # Use of weak random number generator
nolintlint:
require-explanation: true
require-specific: true
allow-unused: false
staticcheck:
checks:
- all
# TODO: Identify, fix, and remove violations of most of these rules
- -S1029 # Range over the string directly
- -SA1019 # Using a deprecated function, variable, constant or field
- -SA6003 # Converting a string to a slice of runes before ranging over it
- -ST1000 # Incorrect or missing package comment
- -ST1003 # Poorly chosen identifier
- -ST1005 # Incorrectly formatted error string
- -QF1001 # Could apply De Morgan's law
- -QF1003 # Could use tagged switch
- -QF1004 # Could use strings.Split instead
- -QF1007 # Could merge conditional assignment into variable declaration
- -QF1008 # Could remove embedded field from selector
- -QF1009 # Probably want to use time.Time.Equal
- -QF1012 # Use fmt.Fprintf(...) instead of Write(fmt.Sprintf(...))
exclusions:
presets:
- std-error-handling
formatters:
enable:
- gofmt

View file

@ -33,5 +33,6 @@ extend-ignore-re = [
"otConf" = "otConf"
"serInt" = "serInt"
"StratName" = "StratName"
"typ" = "typ"
"UPDATEs" = "UPDATEs"
"vai" = "vai"

View file

@ -6,9 +6,8 @@ VERSION ?= 1.0.0
EPOCH ?= 1
MAINTAINER ?= "Community"
CMDS = $(shell find ./cmd -maxdepth 1 -mindepth 1 -type d | grep -v testdata)
CMD_BASENAMES = $(shell echo $(CMDS) | xargs -n1 basename)
CMD_BINS = $(addprefix bin/, $(CMD_BASENAMES) )
CMDS = admin boulder ceremony ct-test-srv pardot-test-srv chall-test-srv
CMD_BINS = $(addprefix bin/, $(CMDS) )
OBJECTS = $(CMD_BINS)
# Build environment variables (referencing core/util.go)
@ -25,7 +24,7 @@ BUILD_TIME_VAR = github.com/letsencrypt/boulder/core.BuildTime
GO_BUILD_FLAGS = -ldflags "-X \"$(BUILD_ID_VAR)=$(BUILD_ID)\" -X \"$(BUILD_TIME_VAR)=$(BUILD_TIME)\" -X \"$(BUILD_HOST_VAR)=$(BUILD_HOST)\""
.PHONY: all build build_cmds rpm deb tar
.PHONY: all build build_cmds deb tar
all: build
build: $(OBJECTS)
@ -38,24 +37,13 @@ $(CMD_BINS): build_cmds
build_cmds: | $(OBJDIR)
echo $(OBJECTS)
GOBIN=$(OBJDIR) GO111MODULE=on go install -mod=vendor $(GO_BUILD_FLAGS) ./...
./link.sh
# Building an RPM requires `fpm` from https://github.com/jordansissel/fpm
# Building a .deb requires `fpm` from https://github.com/jordansissel/fpm
# which you can install with `gem install fpm`.
# It is recommended that maintainers use environment overrides to specify
# Version and Epoch, such as:
#
# VERSION=0.1.9 EPOCH=52 MAINTAINER="$(whoami)" ARCHIVEDIR=/tmp make build rpm
rpm: build
fpm -f -s dir -t rpm --rpm-digest sha256 --name "boulder" \
--license "Mozilla Public License v2.0" --vendor "ISRG" \
--url "https://github.com/letsencrypt/boulder" --prefix=/opt/boulder \
--version "$(VERSION)" --iteration "$(COMMIT_ID)" --epoch "$(EPOCH)" \
--package "$(ARCHIVEDIR)/boulder-$(VERSION)-$(COMMIT_ID).x86_64.rpm" \
--description "Boulder is an ACME-compatible X.509 Certificate Authority" \
--maintainer "$(MAINTAINER)" \
test/config/ sa/db data/ $(OBJECTS)
# VERSION=0.1.9 EPOCH=52 MAINTAINER="$(whoami)" ARCHIVEDIR=/tmp make build deb
deb: build
fpm -f -s dir -t deb --name "boulder" \
--license "Mozilla Public License v2.0" --vendor "ISRG" \
@ -64,10 +52,10 @@ deb: build
--package "$(ARCHIVEDIR)/boulder-$(VERSION)-$(COMMIT_ID).x86_64.deb" \
--description "Boulder is an ACME-compatible X.509 Certificate Authority" \
--maintainer "$(MAINTAINER)" \
test/config/ sa/db data/ $(OBJECTS) bin/ct-test-srv
test/config/ sa/db data/ $(OBJECTS)
tar: build
fpm -f -s dir -t tar --name "boulder" --prefix=/opt/boulder \
--package "$(ARCHIVEDIR)/boulder-$(VERSION)-$(COMMIT_ID).amd64.tar" \
test/config/ sa/db data/ $(OBJECTS) bin/ct-test-srv
test/config/ sa/db data/ $(OBJECTS)
gzip -f "$(ARCHIVEDIR)/boulder-$(VERSION)-$(COMMIT_ID).amd64.tar"

View file

@ -3,10 +3,10 @@
[![Build Status](https://github.com/letsencrypt/boulder/actions/workflows/boulder-ci.yml/badge.svg?branch=main)](https://github.com/letsencrypt/boulder/actions/workflows/boulder-ci.yml?query=branch%3Amain)
This is an implementation of an ACME-based CA. The [ACME
protocol](https://github.com/ietf-wg-acme/acme/) allows the CA to
automatically verify that an applicant for a certificate actually controls an
identifier, and allows domain holders to issue and revoke certificates for
their domains. Boulder is the software that runs [Let's
protocol](https://github.com/ietf-wg-acme/acme/) allows the CA to automatically
verify that an applicant for a certificate actually controls an identifier, and
allows subscribers to issue and revoke certificates for the identifiers they
control. Boulder is the software that runs [Let's
Encrypt](https://letsencrypt.org).
## Contents

View file

@ -3,7 +3,7 @@ package akamai
import (
"bytes"
"crypto/hmac"
"crypto/md5"
"crypto/md5" //nolint: gosec // MD5 is required by the Akamai API.
"crypto/sha256"
"crypto/x509"
"encoding/base64"

View file

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.34.1
// protoc-gen-go v1.36.5
// protoc v3.20.1
// source: akamai.proto
@ -12,6 +12,7 @@ import (
emptypb "google.golang.org/protobuf/types/known/emptypb"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
@ -22,20 +23,17 @@ const (
)
type PurgeRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Urls []string `protobuf:"bytes,1,rep,name=urls,proto3" json:"urls,omitempty"`
unknownFields protoimpl.UnknownFields
Urls []string `protobuf:"bytes,1,rep,name=urls,proto3" json:"urls,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *PurgeRequest) Reset() {
*x = PurgeRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_akamai_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
mi := &file_akamai_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *PurgeRequest) String() string {
@ -46,7 +44,7 @@ func (*PurgeRequest) ProtoMessage() {}
func (x *PurgeRequest) ProtoReflect() protoreflect.Message {
mi := &file_akamai_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@ -70,7 +68,7 @@ func (x *PurgeRequest) GetUrls() []string {
var File_akamai_proto protoreflect.FileDescriptor
var file_akamai_proto_rawDesc = []byte{
var file_akamai_proto_rawDesc = string([]byte{
0x0a, 0x0c, 0x61, 0x6b, 0x61, 0x6d, 0x61, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x06,
0x61, 0x6b, 0x61, 0x6d, 0x61, 0x69, 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72,
@ -85,22 +83,22 @@ var file_akamai_proto_rawDesc = []byte{
0x65, 0x74, 0x73, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x2f, 0x62, 0x6f, 0x75, 0x6c, 0x64,
0x65, 0x72, 0x2f, 0x61, 0x6b, 0x61, 0x6d, 0x61, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
})
var (
file_akamai_proto_rawDescOnce sync.Once
file_akamai_proto_rawDescData = file_akamai_proto_rawDesc
file_akamai_proto_rawDescData []byte
)
func file_akamai_proto_rawDescGZIP() []byte {
file_akamai_proto_rawDescOnce.Do(func() {
file_akamai_proto_rawDescData = protoimpl.X.CompressGZIP(file_akamai_proto_rawDescData)
file_akamai_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_akamai_proto_rawDesc), len(file_akamai_proto_rawDesc)))
})
return file_akamai_proto_rawDescData
}
var file_akamai_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
var file_akamai_proto_goTypes = []interface{}{
var file_akamai_proto_goTypes = []any{
(*PurgeRequest)(nil), // 0: akamai.PurgeRequest
(*emptypb.Empty)(nil), // 1: google.protobuf.Empty
}
@ -119,25 +117,11 @@ func file_akamai_proto_init() {
if File_akamai_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_akamai_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*PurgeRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_akamai_proto_rawDesc,
RawDescriptor: unsafe.Slice(unsafe.StringData(file_akamai_proto_rawDesc), len(file_akamai_proto_rawDesc)),
NumEnums: 0,
NumMessages: 1,
NumExtensions: 0,
@ -148,7 +132,6 @@ func file_akamai_proto_init() {
MessageInfos: file_akamai_proto_msgTypes,
}.Build()
File_akamai_proto = out.File
file_akamai_proto_rawDesc = nil
file_akamai_proto_goTypes = nil
file_akamai_proto_depIdxs = nil
}

View file

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.3.0
// - protoc-gen-go-grpc v1.5.1
// - protoc v3.20.1
// source: akamai.proto
@ -50,20 +50,24 @@ func (c *akamaiPurgerClient) Purge(ctx context.Context, in *PurgeRequest, opts .
// AkamaiPurgerServer is the server API for AkamaiPurger service.
// All implementations must embed UnimplementedAkamaiPurgerServer
// for forward compatibility
// for forward compatibility.
type AkamaiPurgerServer interface {
Purge(context.Context, *PurgeRequest) (*emptypb.Empty, error)
mustEmbedUnimplementedAkamaiPurgerServer()
}
// UnimplementedAkamaiPurgerServer must be embedded to have forward compatible implementations.
type UnimplementedAkamaiPurgerServer struct {
}
// UnimplementedAkamaiPurgerServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedAkamaiPurgerServer struct{}
func (UnimplementedAkamaiPurgerServer) Purge(context.Context, *PurgeRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method Purge not implemented")
}
func (UnimplementedAkamaiPurgerServer) mustEmbedUnimplementedAkamaiPurgerServer() {}
func (UnimplementedAkamaiPurgerServer) testEmbeddedByValue() {}
// UnsafeAkamaiPurgerServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to AkamaiPurgerServer will
@ -73,6 +77,13 @@ type UnsafeAkamaiPurgerServer interface {
}
func RegisterAkamaiPurgerServer(s grpc.ServiceRegistrar, srv AkamaiPurgerServer) {
// If the following call pancis, it indicates UnimplementedAkamaiPurgerServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&AkamaiPurger_ServiceDesc, srv)
}

View file

@ -0,0 +1,43 @@
package allowlist
import (
"github.com/letsencrypt/boulder/strictyaml"
)
// List holds a unique collection of items of type T. Membership can be checked
// by calling the Contains method.
type List[T comparable] struct {
members map[T]struct{}
}
// NewList returns a *List[T] populated with the provided members of type T. All
// duplicate entries are ignored, ensuring uniqueness.
func NewList[T comparable](members []T) *List[T] {
l := &List[T]{members: make(map[T]struct{})}
for _, m := range members {
l.members[m] = struct{}{}
}
return l
}
// NewFromYAML reads a YAML sequence of values of type T and returns a *List[T]
// containing those values. If data is empty, an empty (deny all) list is
// returned. If data cannot be parsed, an error is returned.
func NewFromYAML[T comparable](data []byte) (*List[T], error) {
if len(data) == 0 {
return NewList([]T{}), nil
}
var entries []T
err := strictyaml.Unmarshal(data, &entries)
if err != nil {
return nil, err
}
return NewList(entries), nil
}
// Contains reports whether the provided entry is a member of the list.
func (l *List[T]) Contains(entry T) bool {
_, ok := l.members[entry]
return ok
}

View file

@ -0,0 +1,109 @@
package allowlist
import (
"testing"
)
func TestNewFromYAML(t *testing.T) {
t.Parallel()
tests := []struct {
name string
yamlData string
check []string
expectAnswers []bool
expectErr bool
}{
{
name: "valid YAML",
yamlData: "- oak\n- maple\n- cherry",
check: []string{"oak", "walnut", "maple", "cherry"},
expectAnswers: []bool{true, false, true, true},
expectErr: false,
},
{
name: "empty YAML",
yamlData: "",
check: []string{"oak", "walnut", "maple", "cherry"},
expectAnswers: []bool{false, false, false, false},
expectErr: false,
},
{
name: "invalid YAML",
yamlData: "{ invalid_yaml",
check: []string{},
expectAnswers: []bool{},
expectErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
list, err := NewFromYAML[string]([]byte(tt.yamlData))
if (err != nil) != tt.expectErr {
t.Fatalf("NewFromYAML() error = %v, expectErr = %v", err, tt.expectErr)
}
if err == nil {
for i, item := range tt.check {
got := list.Contains(item)
if got != tt.expectAnswers[i] {
t.Errorf("Contains(%q) got %v, want %v", item, got, tt.expectAnswers[i])
}
}
}
})
}
}
func TestNewList(t *testing.T) {
t.Parallel()
tests := []struct {
name string
members []string
check []string
expectAnswers []bool
}{
{
name: "unique members",
members: []string{"oak", "maple", "cherry"},
check: []string{"oak", "walnut", "maple", "cherry"},
expectAnswers: []bool{true, false, true, true},
},
{
name: "duplicate members",
members: []string{"oak", "maple", "cherry", "oak"},
check: []string{"oak", "walnut", "maple", "cherry"},
expectAnswers: []bool{true, false, true, true},
},
{
name: "nil list",
members: nil,
check: []string{"oak", "walnut", "maple", "cherry"},
expectAnswers: []bool{false, false, false, false},
},
{
name: "empty list",
members: []string{},
check: []string{"oak", "walnut", "maple", "cherry"},
expectAnswers: []bool{false, false, false, false},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
list := NewList[string](tt.members)
for i, item := range tt.check {
got := list.Contains(item)
if got != tt.expectAnswers[i] {
t.Errorf("Contains(%q) got %v, want %v", item, got, tt.expectAnswers[i])
}
}
})
}
}

View file

@ -9,6 +9,7 @@ import (
"io"
"net"
"net/http"
"net/netip"
"net/url"
"slices"
"strconv"
@ -20,137 +21,11 @@ import (
"github.com/miekg/dns"
"github.com/prometheus/client_golang/prometheus"
"github.com/letsencrypt/boulder/features"
"github.com/letsencrypt/boulder/iana"
blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/metrics"
)
func parseCidr(network string, comment string) net.IPNet {
_, net, err := net.ParseCIDR(network)
if err != nil {
panic(fmt.Sprintf("error parsing %s (%s): %s", network, comment, err))
}
return *net
}
var (
// Private CIDRs to ignore
privateNetworks = []net.IPNet{
// RFC1918
// 10.0.0.0/8
{
IP: []byte{10, 0, 0, 0},
Mask: []byte{255, 0, 0, 0},
},
// 172.16.0.0/12
{
IP: []byte{172, 16, 0, 0},
Mask: []byte{255, 240, 0, 0},
},
// 192.168.0.0/16
{
IP: []byte{192, 168, 0, 0},
Mask: []byte{255, 255, 0, 0},
},
// RFC5735
// 127.0.0.0/8
{
IP: []byte{127, 0, 0, 0},
Mask: []byte{255, 0, 0, 0},
},
// RFC1122 Section 3.2.1.3
// 0.0.0.0/8
{
IP: []byte{0, 0, 0, 0},
Mask: []byte{255, 0, 0, 0},
},
// RFC3927
// 169.254.0.0/16
{
IP: []byte{169, 254, 0, 0},
Mask: []byte{255, 255, 0, 0},
},
// RFC 5736
// 192.0.0.0/24
{
IP: []byte{192, 0, 0, 0},
Mask: []byte{255, 255, 255, 0},
},
// RFC 5737
// 192.0.2.0/24
{
IP: []byte{192, 0, 2, 0},
Mask: []byte{255, 255, 255, 0},
},
// 198.51.100.0/24
{
IP: []byte{198, 51, 100, 0},
Mask: []byte{255, 255, 255, 0},
},
// 203.0.113.0/24
{
IP: []byte{203, 0, 113, 0},
Mask: []byte{255, 255, 255, 0},
},
// RFC 3068
// 192.88.99.0/24
{
IP: []byte{192, 88, 99, 0},
Mask: []byte{255, 255, 255, 0},
},
// RFC 2544, Errata 423
// 198.18.0.0/15
{
IP: []byte{198, 18, 0, 0},
Mask: []byte{255, 254, 0, 0},
},
// RFC 3171
// 224.0.0.0/4
{
IP: []byte{224, 0, 0, 0},
Mask: []byte{240, 0, 0, 0},
},
// RFC 1112
// 240.0.0.0/4
{
IP: []byte{240, 0, 0, 0},
Mask: []byte{240, 0, 0, 0},
},
// RFC 919 Section 7
// 255.255.255.255/32
{
IP: []byte{255, 255, 255, 255},
Mask: []byte{255, 255, 255, 255},
},
// RFC 6598
// 100.64.0.0/10
{
IP: []byte{100, 64, 0, 0},
Mask: []byte{255, 192, 0, 0},
},
}
// Sourced from https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml
// where Global, Source, or Destination is False
privateV6Networks = []net.IPNet{
parseCidr("::/128", "RFC 4291: Unspecified Address"),
parseCidr("::1/128", "RFC 4291: Loopback Address"),
parseCidr("::ffff:0:0/96", "RFC 4291: IPv4-mapped Address"),
parseCidr("100::/64", "RFC 6666: Discard Address Block"),
parseCidr("2001::/23", "RFC 2928: IETF Protocol Assignments"),
parseCidr("2001:2::/48", "RFC 5180: Benchmarking"),
parseCidr("2001:db8::/32", "RFC 3849: Documentation"),
parseCidr("2001::/32", "RFC 4380: TEREDO"),
parseCidr("fc00::/7", "RFC 4193: Unique-Local"),
parseCidr("fe80::/10", "RFC 4291: Section 2.5.6 Link-Scoped Unicast"),
parseCidr("ff00::/8", "RFC 4291: Section 2.7"),
// We disable validations to IPs under the 6to4 anycase prefix because
// there's too much risk of a malicious actor advertising the prefix and
// answering validations for a 6to4 host they do not control.
// https://community.letsencrypt.org/t/problems-validating-ipv6-against-host-running-6to4/18312/9
parseCidr("2002::/16", "RFC 7526: 6to4 anycast prefix deprecated"),
}
)
// ResolverAddrs contains DNS resolver(s) that were chosen to perform a
// validation request or CAA recheck. A ResolverAddr will be in the form of
// host:port, A:host:port, or AAAA:host:port depending on which type of lookup
@ -160,7 +35,7 @@ type ResolverAddrs []string
// Client queries for DNS records
type Client interface {
LookupTXT(context.Context, string) (txts []string, resolver ResolverAddrs, err error)
LookupHost(context.Context, string) ([]net.IP, ResolverAddrs, error)
LookupHost(context.Context, string) ([]netip.Addr, ResolverAddrs, error)
LookupCAA(context.Context, string) ([]*dns.CAA, string, ResolverAddrs, error)
}
@ -196,33 +71,28 @@ func New(
stats prometheus.Registerer,
clk clock.Clock,
maxTries int,
userAgent string,
log blog.Logger,
tlsConfig *tls.Config,
) Client {
var client exchanger
if features.Get().DOH {
// Clone the default transport because it comes with various settings
// that we like, which are different from the zero value of an
// `http.Transport`.
transport := http.DefaultTransport.(*http.Transport).Clone()
transport.TLSClientConfig = tlsConfig
// The default transport already sets this field, but it isn't
// documented that it will always be set. Set it again to be sure,
// because Unbound will reject non-HTTP/2 DoH requests.
transport.ForceAttemptHTTP2 = true
client = &dohExchanger{
clk: clk,
hc: http.Client{
Timeout: readTimeout,
Transport: transport,
},
}
} else {
client = &dns.Client{
// Set timeout for underlying net.Conn
ReadTimeout: readTimeout,
Net: "udp",
}
// Clone the default transport because it comes with various settings
// that we like, which are different from the zero value of an
// `http.Transport`.
transport := http.DefaultTransport.(*http.Transport).Clone()
transport.TLSClientConfig = tlsConfig
// The default transport already sets this field, but it isn't
// documented that it will always be set. Set it again to be sure,
// because Unbound will reject non-HTTP/2 DoH requests.
transport.ForceAttemptHTTP2 = true
client = &dohExchanger{
clk: clk,
hc: http.Client{
Timeout: readTimeout,
Transport: transport,
},
userAgent: userAgent,
}
queryTime := prometheus.NewHistogramVec(
@ -279,10 +149,11 @@ func NewTest(
stats prometheus.Registerer,
clk clock.Clock,
maxTries int,
userAgent string,
log blog.Logger,
tlsConfig *tls.Config,
) Client {
resolver := New(readTimeout, servers, stats, clk, maxTries, log, tlsConfig)
resolver := New(readTimeout, servers, stats, clk, maxTries, userAgent, log, tlsConfig)
resolver.(*impl).allowRestrictedAddresses = true
return resolver
}
@ -402,17 +273,10 @@ func (dnsClient *impl) exchangeOne(ctx context.Context, hostname string, qtype u
case r := <-ch:
if r.err != nil {
var isRetryable bool
if features.Get().DOH {
// According to the http package documentation, retryable
// errors emitted by the http package are of type *url.Error.
var urlErr *url.Error
isRetryable = errors.As(r.err, &urlErr) && urlErr.Temporary()
} else {
// According to the net package documentation, retryable
// errors emitted by the net package are of type *net.OpError.
var opErr *net.OpError
isRetryable = errors.As(r.err, &opErr) && opErr.Temporary()
}
// According to the http package documentation, retryable
// errors emitted by the http package are of type *url.Error.
var urlErr *url.Error
isRetryable = errors.As(r.err, &urlErr) && urlErr.Temporary()
hasRetriesLeft := tries < dnsClient.maxTries
if isRetryable && hasRetriesLeft {
tries++
@ -437,7 +301,6 @@ func (dnsClient *impl) exchangeOne(ctx context.Context, hostname string, qtype u
return
}
}
}
// isTLD returns a simplified view of whether something is a TLD: does it have
@ -479,24 +342,6 @@ func (dnsClient *impl) LookupTXT(ctx context.Context, hostname string) ([]string
return txt, ResolverAddrs{resolver}, err
}
func isPrivateV4(ip net.IP) bool {
for _, net := range privateNetworks {
if net.Contains(ip) {
return true
}
}
return false
}
func isPrivateV6(ip net.IP) bool {
for _, net := range privateV6Networks {
if net.Contains(ip) {
return true
}
}
return false
}
func (dnsClient *impl) lookupIP(ctx context.Context, hostname string, ipType uint16) ([]dns.RR, string, error) {
resp, resolver, err := dnsClient.exchangeOne(ctx, hostname, ipType)
switch ipType {
@ -521,7 +366,7 @@ func (dnsClient *impl) lookupIP(ctx context.Context, hostname string, ipType uin
// chase CNAME/DNAME aliases and return relevant records. It will retry
// requests in the case of temporary network errors. It returns an error if
// both the A and AAAA lookups fail or are empty, but succeeds otherwise.
func (dnsClient *impl) LookupHost(ctx context.Context, hostname string) ([]net.IP, ResolverAddrs, error) {
func (dnsClient *impl) LookupHost(ctx context.Context, hostname string) ([]netip.Addr, ResolverAddrs, error) {
var recordsA, recordsAAAA []dns.RR
var errA, errAAAA error
var resolverA, resolverAAAA string
@ -544,13 +389,16 @@ func (dnsClient *impl) LookupHost(ctx context.Context, hostname string) ([]net.I
return a == ""
})
var addrsA []net.IP
var addrsA []netip.Addr
if errA == nil {
for _, answer := range recordsA {
if answer.Header().Rrtype == dns.TypeA {
a, ok := answer.(*dns.A)
if ok && a.A.To4() != nil && (!isPrivateV4(a.A) || dnsClient.allowRestrictedAddresses) {
addrsA = append(addrsA, a.A)
if ok && a.A.To4() != nil {
netIP, ok := netip.AddrFromSlice(a.A)
if ok && (iana.IsReservedAddr(netIP) == nil || dnsClient.allowRestrictedAddresses) {
addrsA = append(addrsA, netIP)
}
}
}
}
@ -559,13 +407,16 @@ func (dnsClient *impl) LookupHost(ctx context.Context, hostname string) ([]net.I
}
}
var addrsAAAA []net.IP
var addrsAAAA []netip.Addr
if errAAAA == nil {
for _, answer := range recordsAAAA {
if answer.Header().Rrtype == dns.TypeAAAA {
aaaa, ok := answer.(*dns.AAAA)
if ok && aaaa.AAAA.To16() != nil && (!isPrivateV6(aaaa.AAAA) || dnsClient.allowRestrictedAddresses) {
addrsAAAA = append(addrsAAAA, aaaa.AAAA)
if ok && aaaa.AAAA.To16() != nil {
netIP, ok := netip.AddrFromSlice(aaaa.AAAA)
if ok && (iana.IsReservedAddr(netIP) == nil || dnsClient.allowRestrictedAddresses) {
addrsAAAA = append(addrsAAAA, netIP)
}
}
}
}
@ -685,8 +536,9 @@ func logDNSError(
}
type dohExchanger struct {
clk clock.Clock
hc http.Client
clk clock.Clock
hc http.Client
userAgent string
}
// Exchange sends a DoH query to the provided DoH server and returns the response.
@ -704,6 +556,9 @@ func (d *dohExchanger) Exchange(query *dns.Msg, server string) (*dns.Msg, time.D
}
req.Header.Set("Content-Type", "application/dns-message")
req.Header.Set("Accept", "application/dns-message")
if len(d.userAgent) > 0 {
req.Header.Set("User-Agent", d.userAgent)
}
start := d.clk.Now()
resp, err := d.hc.Do(req)

View file

@ -2,10 +2,15 @@ package bdns
import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"io"
"log"
"net"
"net/http"
"net/netip"
"net/url"
"os"
"regexp"
@ -19,7 +24,6 @@ import (
"github.com/miekg/dns"
"github.com/prometheus/client_golang/prometheus"
"github.com/letsencrypt/boulder/features"
blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/metrics"
"github.com/letsencrypt/boulder/test"
@ -27,7 +31,30 @@ import (
const dnsLoopbackAddr = "127.0.0.1:4053"
func mockDNSQuery(w dns.ResponseWriter, r *dns.Msg) {
func mockDNSQuery(w http.ResponseWriter, httpReq *http.Request) {
if httpReq.Header.Get("Content-Type") != "application/dns-message" {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(w, "client didn't send Content-Type: application/dns-message")
}
if httpReq.Header.Get("Accept") != "application/dns-message" {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(w, "client didn't accept Content-Type: application/dns-message")
}
requestBody, err := io.ReadAll(httpReq.Body)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(w, "reading body: %s", err)
}
httpReq.Body.Close()
r := new(dns.Msg)
err = r.Unpack(requestBody)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(w, "unpacking request: %s", err)
}
m := new(dns.Msg)
m.SetReply(r)
m.Compress = false
@ -57,19 +84,19 @@ func mockDNSQuery(w dns.ResponseWriter, r *dns.Msg) {
if q.Name == "v6.letsencrypt.org." {
record := new(dns.AAAA)
record.Hdr = dns.RR_Header{Name: "v6.letsencrypt.org.", Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: 0}
record.AAAA = net.ParseIP("::1")
record.AAAA = net.ParseIP("2602:80a:6000:abad:cafe::1")
appendAnswer(record)
}
if q.Name == "dualstack.letsencrypt.org." {
record := new(dns.AAAA)
record.Hdr = dns.RR_Header{Name: "dualstack.letsencrypt.org.", Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: 0}
record.AAAA = net.ParseIP("::1")
record.AAAA = net.ParseIP("2602:80a:6000:abad:cafe::1")
appendAnswer(record)
}
if q.Name == "v4error.letsencrypt.org." {
record := new(dns.AAAA)
record.Hdr = dns.RR_Header{Name: "v4error.letsencrypt.org.", Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: 0}
record.AAAA = net.ParseIP("::1")
record.AAAA = net.ParseIP("2602:80a:6000:abad:cafe::1")
appendAnswer(record)
}
if q.Name == "v6error.letsencrypt.org." {
@ -85,19 +112,19 @@ func mockDNSQuery(w dns.ResponseWriter, r *dns.Msg) {
if q.Name == "cps.letsencrypt.org." {
record := new(dns.A)
record.Hdr = dns.RR_Header{Name: "cps.letsencrypt.org.", Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 0}
record.A = net.ParseIP("127.0.0.1")
record.A = net.ParseIP("64.112.117.1")
appendAnswer(record)
}
if q.Name == "dualstack.letsencrypt.org." {
record := new(dns.A)
record.Hdr = dns.RR_Header{Name: "dualstack.letsencrypt.org.", Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 0}
record.A = net.ParseIP("127.0.0.1")
record.A = net.ParseIP("64.112.117.1")
appendAnswer(record)
}
if q.Name == "v6error.letsencrypt.org." {
record := new(dns.A)
record.Hdr = dns.RR_Header{Name: "dualstack.letsencrypt.org.", Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 0}
record.A = net.ParseIP("127.0.0.1")
record.A = net.ParseIP("64.112.117.1")
appendAnswer(record)
}
if q.Name == "v4error.letsencrypt.org." {
@ -173,45 +200,37 @@ func mockDNSQuery(w dns.ResponseWriter, r *dns.Msg) {
}
}
err := w.WriteMsg(m)
body, err := m.Pack()
if err != nil {
fmt.Fprintf(os.Stderr, "packing reply: %s\n", err)
}
w.Header().Set("Content-Type", "application/dns-message")
_, err = w.Write(body)
if err != nil {
panic(err) // running tests, so panic is OK
}
}
func serveLoopResolver(stopChan chan bool) {
dns.HandleFunc(".", mockDNSQuery)
tcpServer := &dns.Server{
m := http.NewServeMux()
m.HandleFunc("/dns-query", mockDNSQuery)
httpServer := &http.Server{
Addr: dnsLoopbackAddr,
Net: "tcp",
ReadTimeout: time.Second,
WriteTimeout: time.Second,
}
udpServer := &dns.Server{
Addr: dnsLoopbackAddr,
Net: "udp",
Handler: m,
ReadTimeout: time.Second,
WriteTimeout: time.Second,
}
go func() {
err := tcpServer.ListenAndServe()
if err != nil {
fmt.Println(err)
}
}()
go func() {
err := udpServer.ListenAndServe()
cert := "../test/certs/ipki/localhost/cert.pem"
key := "../test/certs/ipki/localhost/key.pem"
err := httpServer.ListenAndServeTLS(cert, key)
if err != nil {
fmt.Println(err)
}
}()
go func() {
<-stopChan
err := tcpServer.Shutdown()
if err != nil {
log.Fatal(err)
}
err = udpServer.Shutdown()
err := httpServer.Shutdown(context.Background())
if err != nil {
log.Fatal(err)
}
@ -239,7 +258,21 @@ func pollServer() {
}
}
// tlsConfig is used for the TLS config of client instances that talk to the
// DoH server set up in TestMain.
var tlsConfig *tls.Config
func TestMain(m *testing.M) {
root, err := os.ReadFile("../test/certs/ipki/minica.pem")
if err != nil {
log.Fatal(err)
}
pool := x509.NewCertPool()
pool.AppendCertsFromPEM(root)
tlsConfig = &tls.Config{
RootCAs: pool,
}
stop := make(chan bool, 1)
serveLoopResolver(stop)
pollServer()
@ -252,7 +285,7 @@ func TestDNSNoServers(t *testing.T) {
staticProvider, err := NewStaticProvider([]string{})
test.AssertNotError(t, err, "Got error creating StaticProvider")
obj := NewTest(time.Hour, staticProvider, metrics.NoopRegisterer, clock.NewFake(), 1, blog.UseMock(), nil)
obj := New(time.Hour, staticProvider, metrics.NoopRegisterer, clock.NewFake(), 1, "", blog.UseMock(), tlsConfig)
_, resolvers, err := obj.LookupHost(context.Background(), "letsencrypt.org")
test.AssertEquals(t, len(resolvers), 0)
@ -269,7 +302,7 @@ func TestDNSOneServer(t *testing.T) {
staticProvider, err := NewStaticProvider([]string{dnsLoopbackAddr})
test.AssertNotError(t, err, "Got error creating StaticProvider")
obj := NewTest(time.Second*10, staticProvider, metrics.NoopRegisterer, clock.NewFake(), 1, blog.UseMock(), nil)
obj := New(time.Second*10, staticProvider, metrics.NoopRegisterer, clock.NewFake(), 1, "", blog.UseMock(), tlsConfig)
_, resolvers, err := obj.LookupHost(context.Background(), "cps.letsencrypt.org")
test.AssertEquals(t, len(resolvers), 2)
@ -282,7 +315,7 @@ func TestDNSDuplicateServers(t *testing.T) {
staticProvider, err := NewStaticProvider([]string{dnsLoopbackAddr, dnsLoopbackAddr})
test.AssertNotError(t, err, "Got error creating StaticProvider")
obj := NewTest(time.Second*10, staticProvider, metrics.NoopRegisterer, clock.NewFake(), 1, blog.UseMock(), nil)
obj := New(time.Second*10, staticProvider, metrics.NoopRegisterer, clock.NewFake(), 1, "", blog.UseMock(), tlsConfig)
_, resolvers, err := obj.LookupHost(context.Background(), "cps.letsencrypt.org")
test.AssertEquals(t, len(resolvers), 2)
@ -295,7 +328,7 @@ func TestDNSServFail(t *testing.T) {
staticProvider, err := NewStaticProvider([]string{dnsLoopbackAddr})
test.AssertNotError(t, err, "Got error creating StaticProvider")
obj := NewTest(time.Second*10, staticProvider, metrics.NoopRegisterer, clock.NewFake(), 1, blog.UseMock(), nil)
obj := New(time.Second*10, staticProvider, metrics.NoopRegisterer, clock.NewFake(), 1, "", blog.UseMock(), tlsConfig)
bad := "servfail.com"
_, _, err = obj.LookupTXT(context.Background(), bad)
@ -313,7 +346,7 @@ func TestDNSLookupTXT(t *testing.T) {
staticProvider, err := NewStaticProvider([]string{dnsLoopbackAddr})
test.AssertNotError(t, err, "Got error creating StaticProvider")
obj := NewTest(time.Second*10, staticProvider, metrics.NoopRegisterer, clock.NewFake(), 1, blog.UseMock(), nil)
obj := New(time.Second*10, staticProvider, metrics.NoopRegisterer, clock.NewFake(), 1, "", blog.UseMock(), tlsConfig)
a, _, err := obj.LookupTXT(context.Background(), "letsencrypt.org")
t.Logf("A: %v", a)
@ -326,11 +359,12 @@ func TestDNSLookupTXT(t *testing.T) {
test.AssertEquals(t, a[0], "abc")
}
// TODO(#8213): Convert this to a table test.
func TestDNSLookupHost(t *testing.T) {
staticProvider, err := NewStaticProvider([]string{dnsLoopbackAddr})
test.AssertNotError(t, err, "Got error creating StaticProvider")
obj := NewTest(time.Second*10, staticProvider, metrics.NoopRegisterer, clock.NewFake(), 1, blog.UseMock(), nil)
obj := New(time.Second*10, staticProvider, metrics.NoopRegisterer, clock.NewFake(), 1, "", blog.UseMock(), tlsConfig)
ip, resolvers, err := obj.LookupHost(context.Background(), "servfail.com")
t.Logf("servfail.com - IP: %s, Err: %s", ip, err)
@ -373,10 +407,10 @@ func TestDNSLookupHost(t *testing.T) {
t.Logf("dualstack.letsencrypt.org - IP: %s, Err: %s", ip, err)
test.AssertNotError(t, err, "Not an error to exist")
test.Assert(t, len(ip) == 2, "Should have 2 IPs")
expected := net.ParseIP("127.0.0.1")
test.Assert(t, ip[0].To4().Equal(expected), "wrong ipv4 address")
expected = net.ParseIP("::1")
test.Assert(t, ip[1].To16().Equal(expected), "wrong ipv6 address")
expected := netip.MustParseAddr("64.112.117.1")
test.Assert(t, ip[0] == expected, "wrong ipv4 address")
expected = netip.MustParseAddr("2602:80a:6000:abad:cafe::1")
test.Assert(t, ip[1] == expected, "wrong ipv6 address")
slices.Sort(resolvers)
test.AssertDeepEquals(t, resolvers, ResolverAddrs{"A:127.0.0.1:4053", "AAAA:127.0.0.1:4053"})
@ -385,8 +419,8 @@ func TestDNSLookupHost(t *testing.T) {
t.Logf("v6error.letsencrypt.org - IP: %s, Err: %s", ip, err)
test.AssertNotError(t, err, "Not an error to exist")
test.Assert(t, len(ip) == 1, "Should have 1 IP")
expected = net.ParseIP("127.0.0.1")
test.Assert(t, ip[0].To4().Equal(expected), "wrong ipv4 address")
expected = netip.MustParseAddr("64.112.117.1")
test.Assert(t, ip[0] == expected, "wrong ipv4 address")
slices.Sort(resolvers)
test.AssertDeepEquals(t, resolvers, ResolverAddrs{"A:127.0.0.1:4053", "AAAA:127.0.0.1:4053"})
@ -395,8 +429,8 @@ func TestDNSLookupHost(t *testing.T) {
t.Logf("v4error.letsencrypt.org - IP: %s, Err: %s", ip, err)
test.AssertNotError(t, err, "Not an error to exist")
test.Assert(t, len(ip) == 1, "Should have 1 IP")
expected = net.ParseIP("::1")
test.Assert(t, ip[0].To16().Equal(expected), "wrong ipv6 address")
expected = netip.MustParseAddr("2602:80a:6000:abad:cafe::1")
test.Assert(t, ip[0] == expected, "wrong ipv6 address")
slices.Sort(resolvers)
test.AssertDeepEquals(t, resolvers, ResolverAddrs{"A:127.0.0.1:4053", "AAAA:127.0.0.1:4053"})
@ -416,7 +450,7 @@ func TestDNSNXDOMAIN(t *testing.T) {
staticProvider, err := NewStaticProvider([]string{dnsLoopbackAddr})
test.AssertNotError(t, err, "Got error creating StaticProvider")
obj := NewTest(time.Second*10, staticProvider, metrics.NoopRegisterer, clock.NewFake(), 1, blog.UseMock(), nil)
obj := New(time.Second*10, staticProvider, metrics.NoopRegisterer, clock.NewFake(), 1, "", blog.UseMock(), tlsConfig)
hostname := "nxdomain.letsencrypt.org"
_, _, err = obj.LookupHost(context.Background(), hostname)
@ -432,7 +466,7 @@ func TestDNSLookupCAA(t *testing.T) {
staticProvider, err := NewStaticProvider([]string{dnsLoopbackAddr})
test.AssertNotError(t, err, "Got error creating StaticProvider")
obj := NewTest(time.Second*10, staticProvider, metrics.NoopRegisterer, clock.NewFake(), 1, blog.UseMock(), nil)
obj := New(time.Second*10, staticProvider, metrics.NoopRegisterer, clock.NewFake(), 1, "", blog.UseMock(), tlsConfig)
removeIDExp := regexp.MustCompile(" id: [[:digit:]]+")
caas, resp, resolvers, err := obj.LookupCAA(context.Background(), "bracewel.net")
@ -487,37 +521,6 @@ caa.example.com. 0 IN CAA 1 issue "letsencrypt.org"
test.AssertEquals(t, resolvers[0], "127.0.0.1:4053")
}
func TestIsPrivateIP(t *testing.T) {
test.Assert(t, isPrivateV4(net.ParseIP("127.0.0.1")), "should be private")
test.Assert(t, isPrivateV4(net.ParseIP("192.168.254.254")), "should be private")
test.Assert(t, isPrivateV4(net.ParseIP("10.255.0.3")), "should be private")
test.Assert(t, isPrivateV4(net.ParseIP("172.16.255.255")), "should be private")
test.Assert(t, isPrivateV4(net.ParseIP("172.31.255.255")), "should be private")
test.Assert(t, !isPrivateV4(net.ParseIP("128.0.0.1")), "should be private")
test.Assert(t, !isPrivateV4(net.ParseIP("192.169.255.255")), "should not be private")
test.Assert(t, !isPrivateV4(net.ParseIP("9.255.0.255")), "should not be private")
test.Assert(t, !isPrivateV4(net.ParseIP("172.32.255.255")), "should not be private")
test.Assert(t, isPrivateV6(net.ParseIP("::0")), "should be private")
test.Assert(t, isPrivateV6(net.ParseIP("::1")), "should be private")
test.Assert(t, !isPrivateV6(net.ParseIP("::2")), "should not be private")
test.Assert(t, isPrivateV6(net.ParseIP("fe80::1")), "should be private")
test.Assert(t, isPrivateV6(net.ParseIP("febf::1")), "should be private")
test.Assert(t, !isPrivateV6(net.ParseIP("fec0::1")), "should not be private")
test.Assert(t, !isPrivateV6(net.ParseIP("feff::1")), "should not be private")
test.Assert(t, isPrivateV6(net.ParseIP("ff00::1")), "should be private")
test.Assert(t, isPrivateV6(net.ParseIP("ff10::1")), "should be private")
test.Assert(t, isPrivateV6(net.ParseIP("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")), "should be private")
test.Assert(t, isPrivateV6(net.ParseIP("2002::")), "should be private")
test.Assert(t, isPrivateV6(net.ParseIP("2002:ffff:ffff:ffff:ffff:ffff:ffff:ffff")), "should be private")
test.Assert(t, isPrivateV6(net.ParseIP("0100::")), "should be private")
test.Assert(t, isPrivateV6(net.ParseIP("0100::0000:ffff:ffff:ffff:ffff")), "should be private")
test.Assert(t, !isPrivateV6(net.ParseIP("0100::0001:0000:0000:0000:0000")), "should be private")
}
type testExchanger struct {
sync.Mutex
count int
@ -542,10 +545,9 @@ func (te *testExchanger) Exchange(m *dns.Msg, a string) (*dns.Msg, time.Duration
}
func TestRetry(t *testing.T) {
isTempErr := &net.OpError{Op: "read", Err: tempError(true)}
nonTempErr := &net.OpError{Op: "read", Err: tempError(false)}
isTempErr := &url.Error{Op: "read", Err: tempError(true)}
nonTempErr := &url.Error{Op: "read", Err: tempError(false)}
servFailError := errors.New("DNS problem: server failure at resolver looking up TXT for example.com")
netError := errors.New("DNS problem: networking error looking up TXT for example.com")
type testCase struct {
name string
maxTries int
@ -596,7 +598,7 @@ func TestRetry(t *testing.T) {
isTempErr,
},
},
expected: netError,
expected: servFailError,
expectedCount: 3,
metricsAllRetries: 1,
},
@ -649,7 +651,7 @@ func TestRetry(t *testing.T) {
isTempErr,
},
},
expected: netError,
expected: servFailError,
expectedCount: 3,
metricsAllRetries: 1,
},
@ -663,7 +665,7 @@ func TestRetry(t *testing.T) {
nonTempErr,
},
},
expected: netError,
expected: servFailError,
expectedCount: 2,
},
}
@ -673,7 +675,7 @@ func TestRetry(t *testing.T) {
staticProvider, err := NewStaticProvider([]string{dnsLoopbackAddr})
test.AssertNotError(t, err, "Got error creating StaticProvider")
testClient := NewTest(time.Second*10, staticProvider, metrics.NoopRegisterer, clock.NewFake(), tc.maxTries, blog.UseMock(), nil)
testClient := New(time.Second*10, staticProvider, metrics.NoopRegisterer, clock.NewFake(), tc.maxTries, "", blog.UseMock(), tlsConfig)
dr := testClient.(*impl)
dr.dnsClient = tc.te
_, _, err = dr.LookupTXT(context.Background(), "example.com")
@ -704,7 +706,7 @@ func TestRetry(t *testing.T) {
staticProvider, err := NewStaticProvider([]string{dnsLoopbackAddr})
test.AssertNotError(t, err, "Got error creating StaticProvider")
testClient := NewTest(time.Second*10, staticProvider, metrics.NoopRegisterer, clock.NewFake(), 3, blog.UseMock(), nil)
testClient := New(time.Second*10, staticProvider, metrics.NoopRegisterer, clock.NewFake(), 3, "", blog.UseMock(), tlsConfig)
dr := testClient.(*impl)
dr.dnsClient = &testExchanger{errs: []error{isTempErr, isTempErr, nil}}
ctx, cancel := context.WithCancel(context.Background())
@ -783,7 +785,7 @@ func (e *rotateFailureExchanger) Exchange(m *dns.Msg, a string) (*dns.Msg, time.
// If its a broken server, return a retryable error
if e.brokenAddresses[a] {
isTempErr := &net.OpError{Op: "read", Err: tempError(true)}
isTempErr := &url.Error{Op: "read", Err: tempError(true)}
return nil, 2 * time.Millisecond, isTempErr
}
@ -805,10 +807,9 @@ func TestRotateServerOnErr(t *testing.T) {
// working server
staticProvider, err := NewStaticProvider(dnsServers)
test.AssertNotError(t, err, "Got error creating StaticProvider")
fmt.Println(staticProvider.servers)
maxTries := 5
client := NewTest(time.Second*10, staticProvider, metrics.NoopRegisterer, clock.NewFake(), maxTries, blog.UseMock(), nil)
client := New(time.Second*10, staticProvider, metrics.NoopRegisterer, clock.NewFake(), maxTries, "", blog.UseMock(), tlsConfig)
// Configure a mock exchanger that will always return a retryable error for
// servers A and B. This will force server "[2606:4700:4700::1111]:53" to do
@ -872,13 +873,10 @@ func (dohE *dohAlwaysRetryExchanger) Exchange(m *dns.Msg, a string) (*dns.Msg, t
}
func TestDOHMetric(t *testing.T) {
features.Set(features.Config{DOH: true})
defer features.Reset()
staticProvider, err := NewStaticProvider([]string{dnsLoopbackAddr})
test.AssertNotError(t, err, "Got error creating StaticProvider")
testClient := NewTest(time.Second*11, staticProvider, metrics.NoopRegisterer, clock.NewFake(), 0, blog.UseMock(), nil)
testClient := New(time.Second*11, staticProvider, metrics.NoopRegisterer, clock.NewFake(), 0, "", blog.UseMock(), tlsConfig)
resolver := testClient.(*impl)
resolver.dnsClient = &dohAlwaysRetryExchanger{err: &url.Error{Op: "read", Err: tempError(true)}}

View file

@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"net"
"net/netip"
"os"
"github.com/miekg/dns"
@ -67,13 +68,13 @@ func (t timeoutError) Timeout() bool {
}
// LookupHost is a mock
func (mock *MockClient) LookupHost(_ context.Context, hostname string) ([]net.IP, ResolverAddrs, error) {
func (mock *MockClient) LookupHost(_ context.Context, hostname string) ([]netip.Addr, ResolverAddrs, error) {
if hostname == "always.invalid" ||
hostname == "invalid.invalid" {
return []net.IP{}, ResolverAddrs{"MockClient"}, nil
return []netip.Addr{}, ResolverAddrs{"MockClient"}, nil
}
if hostname == "always.timeout" {
return []net.IP{}, ResolverAddrs{"MockClient"}, &Error{dns.TypeA, "always.timeout", makeTimeoutError(), -1, nil}
return []netip.Addr{}, ResolverAddrs{"MockClient"}, &Error{dns.TypeA, "always.timeout", makeTimeoutError(), -1, nil}
}
if hostname == "always.error" {
err := &net.OpError{
@ -86,7 +87,7 @@ func (mock *MockClient) LookupHost(_ context.Context, hostname string) ([]net.IP
m.AuthenticatedData = true
m.SetEdns0(4096, false)
logDNSError(mock.Log, "mock.server", hostname, m, nil, err)
return []net.IP{}, ResolverAddrs{"MockClient"}, &Error{dns.TypeA, hostname, err, -1, nil}
return []netip.Addr{}, ResolverAddrs{"MockClient"}, &Error{dns.TypeA, hostname, err, -1, nil}
}
if hostname == "id.mismatch" {
err := dns.ErrId
@ -100,22 +101,21 @@ func (mock *MockClient) LookupHost(_ context.Context, hostname string) ([]net.IP
record.A = net.ParseIP("127.0.0.1")
r.Answer = append(r.Answer, record)
logDNSError(mock.Log, "mock.server", hostname, m, r, err)
return []net.IP{}, ResolverAddrs{"MockClient"}, &Error{dns.TypeA, hostname, err, -1, nil}
return []netip.Addr{}, ResolverAddrs{"MockClient"}, &Error{dns.TypeA, hostname, err, -1, nil}
}
// dual-homed host with an IPv6 and an IPv4 address
if hostname == "ipv4.and.ipv6.localhost" {
return []net.IP{
net.ParseIP("::1"),
net.ParseIP("127.0.0.1"),
return []netip.Addr{
netip.MustParseAddr("::1"),
netip.MustParseAddr("127.0.0.1"),
}, ResolverAddrs{"MockClient"}, nil
}
if hostname == "ipv6.localhost" {
return []net.IP{
net.ParseIP("::1"),
return []netip.Addr{
netip.MustParseAddr("::1"),
}, ResolverAddrs{"MockClient"}, nil
}
ip := net.ParseIP("127.0.0.1")
return []net.IP{ip}, ResolverAddrs{"MockClient"}, nil
return []netip.Addr{netip.MustParseAddr("127.0.0.1")}, ResolverAddrs{"MockClient"}, nil
}
// LookupCAA returns mock records for use in tests.

View file

@ -2,8 +2,10 @@ package bdns
import (
"context"
"errors"
"fmt"
"net"
"net/url"
"github.com/miekg/dns"
)
@ -96,7 +98,9 @@ var extendedErrorCodeToString = map[uint16]string{
func (d Error) Error() string {
var detail, additional string
if d.underlying != nil {
if netErr, ok := d.underlying.(*net.OpError); ok {
var netErr *net.OpError
var urlErr *url.Error
if errors.As(d.underlying, &netErr) {
if netErr.Timeout() {
detail = detailDNSTimeout
} else {
@ -104,9 +108,14 @@ func (d Error) Error() string {
}
// Note: we check d.underlying here even though `Timeout()` does this because the call to `netErr.Timeout()` above only
// happens for `*net.OpError` underlying types!
} else if d.underlying == context.DeadlineExceeded {
} else if errors.As(d.underlying, &urlErr) && urlErr.Timeout() {
// For DOH queries, we can get back a `*url.Error` that wraps the unexported type
// `http.httpError`. Unfortunately `http.httpError` doesn't wrap any errors (like
// context.DeadlineExceeded), we can't check for that; instead we need to call Timeout().
detail = detailDNSTimeout
} else if d.underlying == context.Canceled {
} else if errors.Is(d.underlying, context.DeadlineExceeded) {
detail = detailDNSTimeout
} else if errors.Is(d.underlying, context.Canceled) {
detail = detailCanceled
} else {
detail = detailServerFailure

View file

@ -4,6 +4,7 @@ import (
"context"
"errors"
"net"
"net/url"
"testing"
"github.com/letsencrypt/boulder/test"
@ -51,6 +52,9 @@ func TestError(t *testing.T) {
}, {
&Error{dns.TypeA, "hostname", nil, dns.RcodeFormatError, nil},
"DNS problem: FORMERR looking up A for hostname",
}, {
&Error{dns.TypeA, "hostname", &url.Error{Op: "GET", URL: "https://example.com/", Err: dohTimeoutError{}}, -1, nil},
"DNS problem: query timed out looking up A for hostname",
},
}
for _, tc := range testCases {
@ -60,6 +64,16 @@ func TestError(t *testing.T) {
}
}
type dohTimeoutError struct{}
func (dohTimeoutError) Error() string {
return "doh no"
}
func (dohTimeoutError) Timeout() bool {
return true
}
func TestWrapErr(t *testing.T) {
err := wrapErr(dns.TypeA, "hostname", &dns.Msg{
MsgHdr: dns.MsgHdr{Rcode: dns.RcodeSuccess},

View file

@ -4,15 +4,17 @@ import (
"context"
"errors"
"fmt"
"math/rand"
"math/rand/v2"
"net"
"net/netip"
"strconv"
"sync"
"time"
"github.com/letsencrypt/boulder/cmd"
"github.com/miekg/dns"
"github.com/prometheus/client_golang/prometheus"
"github.com/letsencrypt/boulder/cmd"
)
// ServerProvider represents a type which can provide a list of addresses for
@ -60,10 +62,9 @@ func validateServerAddress(address string) error {
}
// Ensure the `host` portion of `address` is a valid FQDN or IP address.
IPv6 := net.ParseIP(host).To16()
IPv4 := net.ParseIP(host).To4()
_, err = netip.ParseAddr(host)
FQDN := dns.IsFqdn(dns.Fqdn(host))
if IPv6 == nil && IPv4 == nil && !FQDN {
if err != nil && !FQDN {
return errors.New("host is not an FQDN or IP address")
}
return nil
@ -306,7 +307,7 @@ func (dp *dynamicProvider) Addrs() ([]string, error) {
var r []string
dp.mu.RLock()
for ip, ports := range dp.addrs {
port := fmt.Sprint(ports[rand.Intn(len(ports))])
port := fmt.Sprint(ports[rand.IntN(len(ports))])
addr := net.JoinHostPort(ip, port)
r = append(r, addr)
}

View file

@ -9,13 +9,11 @@ import (
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/gob"
"encoding/hex"
"errors"
"fmt"
"math/big"
mrand "math/rand"
"strings"
mrand "math/rand/v2"
"time"
ct "github.com/google/certificate-transparency-go"
@ -23,7 +21,10 @@ import (
"github.com/jmhodges/clock"
"github.com/miekg/pkcs11"
"github.com/prometheus/client_golang/prometheus"
"github.com/zmap/zlint/v3/lint"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
"golang.org/x/crypto/cryptobyte"
cryptobyte_asn1 "golang.org/x/crypto/cryptobyte/asn1"
"golang.org/x/crypto/ocsp"
@ -31,14 +32,14 @@ import (
capb "github.com/letsencrypt/boulder/ca/proto"
"github.com/letsencrypt/boulder/core"
corepb "github.com/letsencrypt/boulder/core/proto"
csrlib "github.com/letsencrypt/boulder/csr"
berrors "github.com/letsencrypt/boulder/errors"
"github.com/letsencrypt/boulder/features"
"github.com/letsencrypt/boulder/goodkey"
"github.com/letsencrypt/boulder/identifier"
"github.com/letsencrypt/boulder/issuance"
"github.com/letsencrypt/boulder/linter"
blog "github.com/letsencrypt/boulder/log"
rapb "github.com/letsencrypt/boulder/ra/proto"
sapb "github.com/letsencrypt/boulder/sa/proto"
)
@ -49,6 +50,24 @@ const (
certType = certificateType("certificate")
)
// issuanceEvent is logged before and after issuance of precertificates and certificates.
// The `omitempty` fields are not always present.
// CSR, Precertificate, and Certificate are hex-encoded DER bytes to make it easier to
// ad-hoc search for sequences or OIDs in logs. Other data, like public key within CSR,
// is logged as base64 because it doesn't have interesting DER structure.
type issuanceEvent struct {
CSR string `json:",omitempty"`
IssuanceRequest *issuance.IssuanceRequest
Issuer string
OrderID int64
Profile string
Requester int64
Result struct {
Precertificate string `json:",omitempty"`
Certificate string `json:",omitempty"`
}
}
// Two maps of keys to Issuers. Lookup by PublicKeyAlgorithm is useful for
// determining the set of issuers which can sign a given (pre)cert, based on its
// PublicKeyAlgorithm. Lookup by NameID is useful for looking up a specific
@ -60,31 +79,17 @@ type issuerMaps struct {
type certProfileWithID struct {
// name is a human readable name used to refer to the certificate profile.
name string
// hash is SHA256 sum over every exported field of an issuance.ProfileConfig
// used to generate the embedded *issuance.Profile.
hash [32]byte
name string
profile *issuance.Profile
}
// certProfilesMaps allows looking up the human-readable name of a certificate
// profile to retrieve the actual profile. The default profile to be used is
// stored alongside the maps.
type certProfilesMaps struct {
// The name of the profile that will be selected if no explicit profile name
// is provided via gRPC.
defaultName string
profileByHash map[[32]byte]*certProfileWithID
profileByName map[string]*certProfileWithID
}
// caMetrics holds various metrics which are shared between caImpl, ocspImpl,
// and crlImpl.
type caMetrics struct {
signatureCount *prometheus.CounterVec
signErrorCount *prometheus.CounterVec
lintErrorCount prometheus.Counter
certificates *prometheus.CounterVec
}
func NewCAMetrics(stats prometheus.Registerer) *caMetrics {
@ -109,7 +114,15 @@ func NewCAMetrics(stats prometheus.Registerer) *caMetrics {
})
stats.MustRegister(lintErrorCount)
return &caMetrics{signatureCount, signErrorCount, lintErrorCount}
certificates := prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "certificates",
Help: "Number of certificates issued",
},
[]string{"profile"})
stats.MustRegister(certificates)
return &caMetrics{signatureCount, signErrorCount, lintErrorCount, certificates}
}
func (m *caMetrics) noteSignError(err error) {
@ -124,21 +137,19 @@ func (m *caMetrics) noteSignError(err error) {
type certificateAuthorityImpl struct {
capb.UnsafeCertificateAuthorityServer
sa sapb.StorageAuthorityCertificateClient
sctClient rapb.SCTProviderClient
pa core.PolicyAuthority
issuers issuerMaps
certProfiles certProfilesMaps
certProfiles map[string]*certProfileWithID
// This is temporary, and will be used for testing and slow roll-out
// of ECDSA issuance, but will then be removed.
ecdsaAllowList *ECDSAAllowList
prefix int // Prepended to the serial number
validityPeriod time.Duration
backdate time.Duration
maxNames int
keyPolicy goodkey.KeyPolicy
clk clock.Clock
log blog.Logger
metrics *caMetrics
// The prefix is prepended to the serial number.
prefix byte
maxNames int
keyPolicy goodkey.KeyPolicy
clk clock.Clock
log blog.Logger
metrics *caMetrics
tracer trace.Tracer
}
var _ capb.CertificateAuthorityServer = (*certificateAuthorityImpl)(nil)
@ -169,75 +180,27 @@ func makeIssuerMaps(issuers []*issuance.Issuer) (issuerMaps, error) {
}
// makeCertificateProfilesMap processes a set of named certificate issuance
// profile configs into a two pre-computed maps: 1) a human-readable name to the
// profile and 2) a unique hash over contents of the profile to the profile
// itself. It returns the maps or an error if a duplicate name or hash is found.
// It also associates the given lint registry with each profile.
//
// The unique hash is used in the case of
// - RA instructs CA1 to issue a precertificate
// - CA1 returns the precertificate DER bytes and profile hash to the RA
// - RA instructs CA2 to issue a final certificate, but CA2 does not contain a
// profile corresponding to that hash and an issuance is prevented.
func makeCertificateProfilesMap(defaultName string, profiles map[string]issuance.ProfileConfig, lints lint.Registry) (certProfilesMaps, error) {
// profile configs into a map from name to profile.
func makeCertificateProfilesMap(profiles map[string]*issuance.ProfileConfig) (map[string]*certProfileWithID, error) {
if len(profiles) <= 0 {
return certProfilesMaps{}, fmt.Errorf("must pass at least one certificate profile")
return nil, fmt.Errorf("must pass at least one certificate profile")
}
// Check that a profile exists with the configured default profile name.
_, ok := profiles[defaultName]
if !ok {
return certProfilesMaps{}, fmt.Errorf("defaultCertificateProfileName:\"%s\" was configured, but a profile object was not found for that name", defaultName)
}
profileByName := make(map[string]*certProfileWithID, len(profiles))
profileByHash := make(map[[32]byte]*certProfileWithID, len(profiles))
profilesByName := make(map[string]*certProfileWithID, len(profiles))
for name, profileConfig := range profiles {
profile, err := issuance.NewProfile(profileConfig, lints)
profile, err := issuance.NewProfile(profileConfig)
if err != nil {
return certProfilesMaps{}, err
return nil, err
}
// gob can only encode exported fields, of which an issuance.Profile has
// none. However, since we're already in a loop iteration having access
// to the issuance.ProfileConfig used to generate the issuance.Profile,
// we'll generate the hash from that.
var encodedProfile bytes.Buffer
enc := gob.NewEncoder(&encodedProfile)
err = enc.Encode(profileConfig)
if err != nil {
return certProfilesMaps{}, err
}
if len(encodedProfile.Bytes()) <= 0 {
return certProfilesMaps{}, fmt.Errorf("certificate profile encoding returned 0 bytes")
}
hash := sha256.Sum256(encodedProfile.Bytes())
_, ok := profileByName[name]
if !ok {
profileByName[name] = &certProfileWithID{
name: name,
hash: hash,
profile: profile,
}
} else {
return certProfilesMaps{}, fmt.Errorf("duplicate certificate profile name %s", name)
}
_, ok = profileByHash[hash]
if !ok {
profileByHash[hash] = &certProfileWithID{
name: name,
hash: hash,
profile: profile,
}
} else {
return certProfilesMaps{}, fmt.Errorf("duplicate certificate profile hash %d", hash)
profilesByName[name] = &certProfileWithID{
name: name,
profile: profile,
}
}
return certProfilesMaps{defaultName, profileByHash, profileByName}, nil
return profilesByName, nil
}
// NewCertificateAuthorityImpl creates a CA instance that can sign certificates
@ -245,15 +208,11 @@ func makeCertificateProfilesMap(defaultName string, profiles map[string]issuance
// OCSP (via delegation to an ocspImpl and its issuers).
func NewCertificateAuthorityImpl(
sa sapb.StorageAuthorityCertificateClient,
sctService rapb.SCTProviderClient,
pa core.PolicyAuthority,
boulderIssuers []*issuance.Issuer,
defaultCertProfileName string,
certificateProfiles map[string]issuance.ProfileConfig,
lints lint.Registry,
ecdsaAllowList *ECDSAAllowList,
certExpiry time.Duration,
certBackdate time.Duration,
serialPrefix int,
certificateProfiles map[string]*issuance.ProfileConfig,
serialPrefix byte,
maxNames int,
keyPolicy goodkey.KeyPolicy,
logger blog.Logger,
@ -263,15 +222,8 @@ func NewCertificateAuthorityImpl(
var ca *certificateAuthorityImpl
var err error
// TODO(briansmith): Make the backdate setting mandatory after the
// production ca.json has been updated to include it. Until then, manually
// default to 1h, which is the backdating duration we currently use.
if certBackdate == 0 {
certBackdate = time.Hour
}
if serialPrefix < 1 || serialPrefix > 127 {
err = errors.New("serial prefix must be between 1 and 127")
if serialPrefix < 0x01 || serialPrefix > 0x7f {
err = errors.New("serial prefix must be between 0x01 (1) and 0x7f (127)")
return nil, err
}
@ -279,7 +231,7 @@ func NewCertificateAuthorityImpl(
return nil, errors.New("must have at least one issuer")
}
certProfiles, err := makeCertificateProfilesMap(defaultCertProfileName, certificateProfiles, lints)
certProfiles, err := makeCertificateProfilesMap(certificateProfiles)
if err != nil {
return nil, err
}
@ -290,19 +242,18 @@ func NewCertificateAuthorityImpl(
}
ca = &certificateAuthorityImpl{
sa: sa,
pa: pa,
issuers: issuers,
certProfiles: certProfiles,
validityPeriod: certExpiry,
backdate: certBackdate,
prefix: serialPrefix,
maxNames: maxNames,
keyPolicy: keyPolicy,
log: logger,
metrics: metrics,
clk: clk,
ecdsaAllowList: ecdsaAllowList,
sa: sa,
sctClient: sctService,
pa: pa,
issuers: issuers,
certProfiles: certProfiles,
prefix: serialPrefix,
maxNames: maxNames,
keyPolicy: keyPolicy,
log: logger,
metrics: metrics,
tracer: otel.GetTracerProvider().Tracer("github.com/letsencrypt/boulder/ca"),
clk: clk,
}
return ca, nil
@ -314,42 +265,38 @@ var ocspStatusToCode = map[string]int{
"unknown": ocsp.Unknown,
}
// IssuePrecertificate is the first step in the [issuance cycle]. It allocates and stores a serial number,
// issuePrecertificate is the first step in the [issuance cycle]. It allocates and stores a serial number,
// selects a certificate profile, generates and stores a linting certificate, sets the serial's status to
// "wait", signs and stores a precertificate, updates the serial's status to "good", then returns the
// precertificate.
//
// Subsequent final issuance based on this precertificate must happen at most once, and must use the same
// certificate profile. The certificate profile is identified by a hash to ensure an exact match even if
// the configuration for a specific profile _name_ changes.
// certificate profile.
//
// Returns precertificate DER.
//
// [issuance cycle]: https://github.com/letsencrypt/boulder/blob/main/docs/ISSUANCE-CYCLE.md
func (ca *certificateAuthorityImpl) IssuePrecertificate(ctx context.Context, issueReq *capb.IssueCertificateRequest) (*capb.IssuePrecertificateResponse, error) {
// issueReq.orderID may be zero, for ACMEv1 requests.
// issueReq.CertProfileName may be empty and will be populated in
// issuePrecertificateInner if so.
if core.IsAnyNilOrZero(issueReq, issueReq.Csr, issueReq.RegistrationID) {
return nil, berrors.InternalServerError("Incomplete issue certificate request")
}
serialBigInt, validity, err := ca.generateSerialNumberAndValidity()
func (ca *certificateAuthorityImpl) issuePrecertificate(ctx context.Context, certProfile *certProfileWithID, issueReq *capb.IssueCertificateRequest) ([]byte, error) {
serialBigInt, err := ca.generateSerialNumber()
if err != nil {
return nil, err
}
notBefore, notAfter := certProfile.profile.GenerateValidity(ca.clk.Now())
serialHex := core.SerialToString(serialBigInt)
regID := issueReq.RegistrationID
_, err = ca.sa.AddSerial(ctx, &sapb.AddSerialRequest{
Serial: serialHex,
RegID: regID,
Created: timestamppb.New(ca.clk.Now()),
Expires: timestamppb.New(validity.NotAfter),
Expires: timestamppb.New(notAfter),
})
if err != nil {
return nil, err
}
precertDER, cpwid, err := ca.issuePrecertificateInner(ctx, issueReq, serialBigInt, validity)
precertDER, _, err := ca.issuePrecertificateInner(ctx, issueReq, certProfile, serialBigInt, notBefore, notAfter)
if err != nil {
return nil, err
}
@ -359,14 +306,39 @@ func (ca *certificateAuthorityImpl) IssuePrecertificate(ctx context.Context, iss
return nil, err
}
return &capb.IssuePrecertificateResponse{
DER: precertDER,
CertProfileName: cpwid.name,
CertProfileHash: cpwid.hash[:],
}, nil
return precertDER, nil
}
// IssueCertificateForPrecertificate final step in the [issuance cycle].
func (ca *certificateAuthorityImpl) IssueCertificate(ctx context.Context, issueReq *capb.IssueCertificateRequest) (*capb.IssueCertificateResponse, error) {
if core.IsAnyNilOrZero(issueReq, issueReq.Csr, issueReq.RegistrationID, issueReq.OrderID) {
return nil, berrors.InternalServerError("Incomplete issue certificate request")
}
if ca.sctClient == nil {
return nil, errors.New("IssueCertificate called with a nil SCT service")
}
// All issuance requests must come with a profile name, and the RA handles selecting the default.
certProfile, ok := ca.certProfiles[issueReq.CertProfileName]
if !ok {
return nil, fmt.Errorf("the CA is incapable of using a profile named %s", issueReq.CertProfileName)
}
precertDER, err := ca.issuePrecertificate(ctx, certProfile, issueReq)
if err != nil {
return nil, err
}
scts, err := ca.sctClient.GetSCTs(ctx, &rapb.SCTRequest{PrecertDER: precertDER})
if err != nil {
return nil, err
}
certDER, err := ca.issueCertificateForPrecertificate(ctx, certProfile, precertDER, scts.SctDER, issueReq.RegistrationID, issueReq.OrderID)
if err != nil {
return nil, err
}
return &capb.IssueCertificateResponse{DER: certDER}, nil
}
// issueCertificateForPrecertificate is final step in the [issuance cycle].
//
// Given a precertificate and a set of SCTs for that precertificate, it generates
// a linting final certificate, then signs a final certificate using a real issuer.
@ -376,12 +348,11 @@ func (ca *certificateAuthorityImpl) IssuePrecertificate(ctx context.Context, iss
//
// It's critical not to sign two different final certificates for the same
// precertificate. This can happen, for instance, if the caller provides a
// different set of SCTs on subsequent calls to IssueCertificateForPrecertificate.
// We rely on the RA not to call IssueCertificateForPrecertificate twice for the
// different set of SCTs on subsequent calls to issueCertificateForPrecertificate.
// We rely on the RA not to call issueCertificateForPrecertificate twice for the
// same serial. This is accomplished by the fact that
// IssueCertificateForPrecertificate is only ever called in a straight-through
// RPC path without retries. If there is any error, including a networking
// error, the whole certificate issuance attempt fails and any subsequent
// issueCertificateForPrecertificate is only ever called once per call to `IssueCertificate`.
// If there is any error, the whole certificate issuance attempt fails and any subsequent
// issuance will use a different serial number.
//
// We also check that the provided serial number does not already exist as a
@ -389,23 +360,17 @@ func (ca *certificateAuthorityImpl) IssuePrecertificate(ctx context.Context, iss
// there could be race conditions where two goroutines are issuing for the same
// serial number at the same time.
//
// Returns the final certificate's bytes as DER.
//
// [issuance cycle]: https://github.com/letsencrypt/boulder/blob/main/docs/ISSUANCE-CYCLE.md
func (ca *certificateAuthorityImpl) IssueCertificateForPrecertificate(ctx context.Context, req *capb.IssueCertificateForPrecertificateRequest) (*corepb.Certificate, error) {
// issueReq.orderID may be zero, for ACMEv1 requests.
if core.IsAnyNilOrZero(req, req.DER, req.SCTs, req.RegistrationID, req.CertProfileHash) {
return nil, berrors.InternalServerError("Incomplete cert for precertificate request")
}
// The certificate profile hash is checked here instead of the name because
// the hash is over the entire contents of a *ProfileConfig giving assurance
// that the certificate profile has remained unchanged during the roundtrip
// from a CA, to the RA, then back to a (potentially different) CA node.
certProfile, ok := ca.certProfiles.profileByHash[[32]byte(req.CertProfileHash)]
if !ok {
return nil, fmt.Errorf("the CA is incapable of using a profile with hash %d", req.CertProfileHash)
}
precert, err := x509.ParseCertificate(req.DER)
func (ca *certificateAuthorityImpl) issueCertificateForPrecertificate(ctx context.Context,
certProfile *certProfileWithID,
precertDER []byte,
sctBytes [][]byte,
regID int64,
orderID int64,
) ([]byte, error) {
precert, err := x509.ParseCertificate(precertDER)
if err != nil {
return nil, err
}
@ -419,9 +384,9 @@ func (ca *certificateAuthorityImpl) IssueCertificateForPrecertificate(ctx contex
return nil, fmt.Errorf("error checking for duplicate issuance of %s: %s", serialHex, err)
}
var scts []ct.SignedCertificateTimestamp
for _, sctBytes := range req.SCTs {
for _, singleSCTBytes := range sctBytes {
var sct ct.SignedCertificateTimestamp
_, err = cttls.Unmarshal(sctBytes, &sct)
_, err = cttls.Unmarshal(singleSCTBytes, &sct)
if err != nil {
return nil, err
}
@ -438,24 +403,42 @@ func (ca *certificateAuthorityImpl) IssueCertificateForPrecertificate(ctx contex
return nil, err
}
names := strings.Join(issuanceReq.DNSNames, ", ")
ca.log.AuditInfof("Signing cert: issuer=[%s] serial=[%s] regID=[%d] names=[%s] certProfileName=[%s] certProfileHash=[%x] precert=[%s]",
issuer.Name(), serialHex, req.RegistrationID, names, certProfile.name, certProfile.hash, hex.EncodeToString(precert.Raw))
lintCertBytes, issuanceToken, err := issuer.Prepare(certProfile.profile, issuanceReq)
if err != nil {
ca.log.AuditErrf("Preparing cert failed: issuer=[%s] serial=[%s] regID=[%d] names=[%s] certProfileName=[%s] certProfileHash=[%x] err=[%v]",
issuer.Name(), serialHex, req.RegistrationID, names, certProfile.name, certProfile.hash, err)
ca.log.AuditErrf("Preparing cert failed: serial=[%s] err=[%v]", serialHex, err)
return nil, berrors.InternalServerError("failed to prepare certificate signing: %s", err)
}
logEvent := issuanceEvent{
IssuanceRequest: issuanceReq,
Issuer: issuer.Name(),
OrderID: orderID,
Profile: certProfile.name,
Requester: regID,
}
ca.log.AuditObject("Signing cert", logEvent)
var ipStrings []string
for _, ip := range issuanceReq.IPAddresses {
ipStrings = append(ipStrings, ip.String())
}
_, span := ca.tracer.Start(ctx, "signing cert", trace.WithAttributes(
attribute.String("serial", serialHex),
attribute.String("issuer", issuer.Name()),
attribute.String("certProfileName", certProfile.name),
attribute.StringSlice("names", issuanceReq.DNSNames),
attribute.StringSlice("ipAddresses", ipStrings),
))
certDER, err := issuer.Issue(issuanceToken)
if err != nil {
ca.metrics.noteSignError(err)
ca.log.AuditErrf("Signing cert failed: issuer=[%s] serial=[%s] regID=[%d] names=[%s] certProfileName=[%s] certProfileHash=[%x] err=[%v]",
issuer.Name(), serialHex, req.RegistrationID, names, certProfile.name, certProfile.hash, err)
ca.log.AuditErrf("Signing cert failed: serial=[%s] err=[%v]", serialHex, err)
span.SetStatus(codes.Error, err.Error())
span.End()
return nil, berrors.InternalServerError("failed to sign certificate: %s", err)
}
span.End()
err = tbsCertIsDeterministic(lintCertBytes, certDER)
if err != nil {
@ -463,56 +446,40 @@ func (ca *certificateAuthorityImpl) IssueCertificateForPrecertificate(ctx contex
}
ca.metrics.signatureCount.With(prometheus.Labels{"purpose": string(certType), "issuer": issuer.Name()}).Inc()
ca.log.AuditInfof("Signing cert success: issuer=[%s] serial=[%s] regID=[%d] names=[%s] certificate=[%s] certProfileName=[%s] certProfileHash=[%x]",
issuer.Name(), serialHex, req.RegistrationID, names, hex.EncodeToString(certDER), certProfile.name, certProfile.hash)
ca.metrics.certificates.With(prometheus.Labels{"profile": certProfile.name}).Inc()
logEvent.Result.Certificate = hex.EncodeToString(certDER)
ca.log.AuditObject("Signing cert success", logEvent)
_, err = ca.sa.AddCertificate(ctx, &sapb.AddCertificateRequest{
Der: certDER,
RegID: req.RegistrationID,
RegID: regID,
Issued: timestamppb.New(ca.clk.Now()),
})
if err != nil {
ca.log.AuditErrf("Failed RPC to store at SA: issuer=[%s] serial=[%s] cert=[%s] regID=[%d] orderID=[%d] certProfileName=[%s] certProfileHash=[%x] err=[%v]",
issuer.Name(), serialHex, hex.EncodeToString(certDER), req.RegistrationID, req.OrderID, certProfile.name, certProfile.hash, err)
ca.log.AuditErrf("Failed RPC to store at SA: serial=[%s] err=[%v]", serialHex, hex.EncodeToString(certDER))
return nil, err
}
return &corepb.Certificate{
RegistrationID: req.RegistrationID,
Serial: core.SerialToString(precert.SerialNumber),
Der: certDER,
Digest: core.Fingerprint256(certDER),
Issued: timestamppb.New(precert.NotBefore),
Expires: timestamppb.New(precert.NotAfter),
}, nil
return certDER, nil
}
type validity struct {
NotBefore time.Time
NotAfter time.Time
}
func (ca *certificateAuthorityImpl) generateSerialNumberAndValidity() (*big.Int, validity, error) {
// generateSerialNumber produces a big.Int which has more than 64 bits of
// entropy and has the CA's configured one-byte prefix.
func (ca *certificateAuthorityImpl) generateSerialNumber() (*big.Int, error) {
// We want 136 bits of random number, plus an 8-bit instance id prefix.
const randBits = 136
serialBytes := make([]byte, randBits/8+1)
serialBytes[0] = byte(ca.prefix)
serialBytes[0] = ca.prefix
_, err := rand.Read(serialBytes[1:])
if err != nil {
err = berrors.InternalServerError("failed to generate serial: %s", err)
ca.log.AuditErrf("Serial randomness failed, err=[%v]", err)
return nil, validity{}, err
return nil, err
}
serialBigInt := big.NewInt(0)
serialBigInt = serialBigInt.SetBytes(serialBytes)
notBefore := ca.clk.Now().Add(-ca.backdate)
validity := validity{
NotBefore: notBefore,
NotAfter: notBefore.Add(ca.validityPeriod - time.Second),
}
return serialBigInt, validity, nil
return serialBigInt, nil
}
// generateSKID computes the Subject Key Identifier using one of the methods in
@ -538,20 +505,7 @@ func generateSKID(pk crypto.PublicKey) ([]byte, error) {
return skid[0:20:20], nil
}
func (ca *certificateAuthorityImpl) issuePrecertificateInner(ctx context.Context, issueReq *capb.IssueCertificateRequest, serialBigInt *big.Int, validity validity) ([]byte, *certProfileWithID, error) {
// The CA must check if it is capable of issuing for the given certificate
// profile name. The name is checked here instead of the hash because the RA
// is unaware of what certificate profiles exist. Pre-existing orders stored
// in the database may not have an associated certificate profile name and
// will take the default name stored alongside the map.
if issueReq.CertProfileName == "" {
issueReq.CertProfileName = ca.certProfiles.defaultName
}
certProfile, ok := ca.certProfiles.profileByName[issueReq.CertProfileName]
if !ok {
return nil, nil, fmt.Errorf("the CA is incapable of using a profile named %s", issueReq.CertProfileName)
}
func (ca *certificateAuthorityImpl) issuePrecertificateInner(ctx context.Context, issueReq *capb.IssueCertificateRequest, certProfile *certProfileWithID, serialBigInt *big.Int, notBefore time.Time, notAfter time.Time) ([]byte, *certProfileWithID, error) {
csr, err := x509.ParseCertificateRequest(issueReq.Csr)
if err != nil {
return nil, nil, err
@ -566,20 +520,17 @@ func (ca *certificateAuthorityImpl) issuePrecertificateInner(ctx context.Context
}
// Select which pool of issuers to use, based on the to-be-issued cert's key
// type and whether we're using the ECDSA Allow List.
// type.
alg := csr.PublicKeyAlgorithm
if alg == x509.ECDSA && !features.Get().ECDSAForAll && ca.ecdsaAllowList != nil && !ca.ecdsaAllowList.permitted(issueReq.RegistrationID) {
alg = x509.RSA
}
// Select a random issuer from among the active issuers of this key type.
issuerPool, ok := ca.issuers.byAlg[alg]
if !ok || len(issuerPool) == 0 {
return nil, nil, berrors.InternalServerError("no issuers found for public key algorithm %s", csr.PublicKeyAlgorithm)
}
issuer := issuerPool[mrand.Intn(len(issuerPool))]
issuer := issuerPool[mrand.IntN(len(issuerPool))]
if issuer.Cert.NotAfter.Before(validity.NotAfter) {
if issuer.Cert.NotAfter.Before(notAfter) {
err = berrors.InternalServerError("cannot issue a certificate that expires after the issuer certificate")
ca.log.AuditErr(err.Error())
return nil, nil, err
@ -592,32 +543,37 @@ func (ca *certificateAuthorityImpl) issuePrecertificateInner(ctx context.Context
serialHex := core.SerialToString(serialBigInt)
ca.log.AuditInfof("Signing precert: serial=[%s] regID=[%d] names=[%s] csr=[%s]",
serialHex, issueReq.RegistrationID, strings.Join(csr.DNSNames, ", "), hex.EncodeToString(csr.Raw))
dnsNames, ipAddresses, err := identifier.FromCSR(csr).ToValues()
if err != nil {
return nil, nil, err
}
names := csrlib.NamesFromCSR(csr)
req := &issuance.IssuanceRequest{
PublicKey: csr.PublicKey,
SubjectKeyId: subjectKeyId,
Serial: serialBigInt.Bytes(),
DNSNames: names.SANs,
CommonName: names.CN,
IncludeCTPoison: true,
IncludeMustStaple: issuance.ContainsMustStaple(csr.Extensions),
NotBefore: validity.NotBefore,
NotAfter: validity.NotAfter,
PublicKey: issuance.MarshalablePublicKey{PublicKey: csr.PublicKey},
SubjectKeyId: subjectKeyId,
Serial: serialBigInt.Bytes(),
DNSNames: dnsNames,
IPAddresses: ipAddresses,
CommonName: csrlib.CNFromCSR(csr),
IncludeCTPoison: true,
NotBefore: notBefore,
NotAfter: notAfter,
}
lintCertBytes, issuanceToken, err := issuer.Prepare(certProfile.profile, req)
if err != nil {
ca.log.AuditErrf("Preparing precert failed: issuer=[%s] serial=[%s] regID=[%d] names=[%s] certProfileName=[%s] certProfileHash=[%x] err=[%v]",
issuer.Name(), serialHex, issueReq.RegistrationID, strings.Join(csr.DNSNames, ", "), certProfile.name, certProfile.hash, err)
ca.log.AuditErrf("Preparing precert failed: serial=[%s] err=[%v]", serialHex, err)
if errors.Is(err, linter.ErrLinting) {
ca.metrics.lintErrorCount.Inc()
}
return nil, nil, berrors.InternalServerError("failed to prepare precertificate signing: %s", err)
}
// Note: we write the linting certificate bytes to this table, rather than the precertificate
// (which we audit log but do not put in the database). This is to ensure that even if there is
// an error immediately after signing the precertificate, we have a record in the DB of what we
// intended to sign, and can do revocations based on that. See #6807.
// The name of the SA method ("AddPrecertificate") is a historical artifact.
_, err = ca.sa.AddPrecertificate(context.Background(), &sapb.AddCertificateRequest{
Der: lintCertBytes,
RegID: issueReq.RegistrationID,
@ -629,13 +585,37 @@ func (ca *certificateAuthorityImpl) issuePrecertificateInner(ctx context.Context
return nil, nil, err
}
logEvent := issuanceEvent{
CSR: hex.EncodeToString(csr.Raw),
IssuanceRequest: req,
Issuer: issuer.Name(),
Profile: certProfile.name,
Requester: issueReq.RegistrationID,
OrderID: issueReq.OrderID,
}
ca.log.AuditObject("Signing precert", logEvent)
var ipStrings []string
for _, ip := range csr.IPAddresses {
ipStrings = append(ipStrings, ip.String())
}
_, span := ca.tracer.Start(ctx, "signing precert", trace.WithAttributes(
attribute.String("serial", serialHex),
attribute.String("issuer", issuer.Name()),
attribute.String("certProfileName", certProfile.name),
attribute.StringSlice("names", csr.DNSNames),
attribute.StringSlice("ipAddresses", ipStrings),
))
certDER, err := issuer.Issue(issuanceToken)
if err != nil {
ca.metrics.noteSignError(err)
ca.log.AuditErrf("Signing precert failed: issuer=[%s] serial=[%s] regID=[%d] names=[%s] certProfileName=[%s] certProfileHash=[%x] err=[%v]",
issuer.Name(), serialHex, issueReq.RegistrationID, strings.Join(csr.DNSNames, ", "), certProfile.name, certProfile.hash, err)
ca.log.AuditErrf("Signing precert failed: serial=[%s] err=[%v]", serialHex, err)
span.SetStatus(codes.Error, err.Error())
span.End()
return nil, nil, berrors.InternalServerError("failed to sign precertificate: %s", err)
}
span.End()
err = tbsCertIsDeterministic(lintCertBytes, certDER)
if err != nil {
@ -643,10 +623,13 @@ func (ca *certificateAuthorityImpl) issuePrecertificateInner(ctx context.Context
}
ca.metrics.signatureCount.With(prometheus.Labels{"purpose": string(precertType), "issuer": issuer.Name()}).Inc()
ca.log.AuditInfof("Signing precert success: issuer=[%s] serial=[%s] regID=[%d] names=[%s] precertificate=[%s] certProfileName=[%s] certProfileHash=[%x]",
issuer.Name(), serialHex, issueReq.RegistrationID, strings.Join(csr.DNSNames, ", "), hex.EncodeToString(certDER), certProfile.name, certProfile.hash)
return certDER, &certProfileWithID{certProfile.name, certProfile.hash, nil}, nil
logEvent.Result.Precertificate = hex.EncodeToString(certDER)
// The CSR is big and not that informative, so don't log it a second time.
logEvent.CSR = ""
ca.log.AuditObject("Signing precert success", logEvent)
return certDER, &certProfileWithID{certProfile.name, nil}, nil
}
// verifyTBSCertIsDeterministic verifies that x509.CreateCertificate signing

View file

@ -11,6 +11,7 @@ import (
"errors"
"fmt"
"math/big"
mrand "math/rand"
"os"
"strings"
"testing"
@ -22,7 +23,6 @@ import (
"github.com/jmhodges/clock"
"github.com/miekg/pkcs11"
"github.com/prometheus/client_golang/prometheus"
"github.com/zmap/zlint/v3/lint"
"google.golang.org/grpc"
"google.golang.org/protobuf/types/known/emptypb"
@ -33,12 +33,13 @@ import (
berrors "github.com/letsencrypt/boulder/errors"
"github.com/letsencrypt/boulder/features"
"github.com/letsencrypt/boulder/goodkey"
"github.com/letsencrypt/boulder/identifier"
"github.com/letsencrypt/boulder/issuance"
"github.com/letsencrypt/boulder/linter"
blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/metrics"
"github.com/letsencrypt/boulder/must"
"github.com/letsencrypt/boulder/policy"
rapb "github.com/letsencrypt/boulder/ra/proto"
sapb "github.com/letsencrypt/boulder/sa/proto"
"github.com/letsencrypt/boulder/test"
)
@ -93,28 +94,22 @@ var (
OIDExtensionSCTList = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11129, 2, 4, 2}
)
const arbitraryRegID int64 = 1001
func mustRead(path string) []byte {
return must.Do(os.ReadFile(path))
}
type testCtx struct {
pa core.PolicyAuthority
ocsp *ocspImpl
crl *crlImpl
defaultCertProfileName string
lints lint.Registry
certProfiles map[string]issuance.ProfileConfig
certExpiry time.Duration
certBackdate time.Duration
serialPrefix int
maxNames int
boulderIssuers []*issuance.Issuer
keyPolicy goodkey.KeyPolicy
fc clock.FakeClock
metrics *caMetrics
logger *blog.Mock
pa core.PolicyAuthority
ocsp *ocspImpl
crl *crlImpl
certProfiles map[string]*issuance.ProfileConfig
serialPrefix byte
maxNames int
boulderIssuers []*issuance.Issuer
keyPolicy goodkey.KeyPolicy
fc clock.FakeClock
metrics *caMetrics
logger *blog.Mock
}
type mockSA struct {
@ -153,33 +148,27 @@ func setup(t *testing.T) *testCtx {
fc := clock.NewFake()
fc.Add(1 * time.Hour)
pa, err := policy.New(nil, blog.NewMock())
pa, err := policy.New(map[identifier.IdentifierType]bool{"dns": true}, nil, blog.NewMock())
test.AssertNotError(t, err, "Couldn't create PA")
err = pa.LoadHostnamePolicyFile("../test/hostname-policy.yaml")
test.AssertNotError(t, err, "Couldn't set hostname policy")
certProfiles := make(map[string]issuance.ProfileConfig, 0)
certProfiles["defaultBoulderCertificateProfile"] = issuance.ProfileConfig{
AllowMustStaple: true,
AllowCTPoison: true,
AllowSCTList: true,
AllowCommonName: true,
Policies: []issuance.PolicyConfig{
{OID: "2.23.140.1.2.1"},
},
MaxValidityPeriod: config.Duration{Duration: time.Hour * 8760},
MaxValidityBackdate: config.Duration{Duration: time.Hour},
certProfiles := make(map[string]*issuance.ProfileConfig, 0)
certProfiles["legacy"] = &issuance.ProfileConfig{
IncludeCRLDistributionPoints: true,
MaxValidityPeriod: config.Duration{Duration: time.Hour * 24 * 90},
MaxValidityBackdate: config.Duration{Duration: time.Hour},
IgnoredLints: []string{"w_subject_common_name_included"},
}
certProfiles["longerLived"] = issuance.ProfileConfig{
AllowMustStaple: true,
AllowCTPoison: true,
AllowSCTList: true,
AllowCommonName: true,
Policies: []issuance.PolicyConfig{
{OID: "2.23.140.1.2.1"},
},
MaxValidityPeriod: config.Duration{Duration: time.Hour * 8761},
MaxValidityBackdate: config.Duration{Duration: time.Hour},
certProfiles["modern"] = &issuance.ProfileConfig{
OmitCommonName: true,
OmitKeyEncipherment: true,
OmitClientAuth: true,
OmitSKID: true,
IncludeCRLDistributionPoints: true,
MaxValidityPeriod: config.Duration{Duration: time.Hour * 24 * 6},
MaxValidityBackdate: config.Duration{Duration: time.Hour},
IgnoredLints: []string{"w_ext_subject_key_identifier_missing_sub_cert"},
}
test.AssertEquals(t, len(certProfiles), 2)
@ -190,6 +179,7 @@ func setup(t *testing.T) *testCtx {
IssuerURL: fmt.Sprintf("http://not-example.com/i/%s", name),
OCSPURL: "http://not-example.com/o",
CRLURLBase: fmt.Sprintf("http://not-example.com/c/%s/", name),
CRLShards: 10,
Location: issuance.IssuerLoc{
File: fmt.Sprintf("../test/hierarchy/%s.key.pem", name),
CertFile: fmt.Sprintf("../test/hierarchy/%s.cert.pem", name),
@ -216,10 +206,12 @@ func setup(t *testing.T) *testCtx {
Name: "lint_errors",
Help: "Number of issuances that were halted by linting errors",
})
cametrics := &caMetrics{signatureCount, signErrorCount, lintErrorCount}
lints, err := linter.NewRegistry([]string{"w_subject_common_name_included"})
test.AssertNotError(t, err, "Failed to create zlint registry")
certificatesCount := prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "certificates",
Help: "Number of certificates issued",
}, []string{"profile"})
cametrics := &caMetrics{signatureCount, signErrorCount, lintErrorCount, certificatesCount}
ocsp, err := NewOCSPImpl(
boulderIssuers,
@ -246,21 +238,17 @@ func setup(t *testing.T) *testCtx {
test.AssertNotError(t, err, "Failed to create crl impl")
return &testCtx{
pa: pa,
ocsp: ocsp,
crl: crl,
defaultCertProfileName: "defaultBoulderCertificateProfile",
lints: lints,
certProfiles: certProfiles,
certExpiry: 8760 * time.Hour,
certBackdate: time.Hour,
serialPrefix: 17,
maxNames: 2,
boulderIssuers: boulderIssuers,
keyPolicy: keyPolicy,
fc: fc,
metrics: cametrics,
logger: blog.NewMock(),
pa: pa,
ocsp: ocsp,
crl: crl,
certProfiles: certProfiles,
serialPrefix: 0x11,
maxNames: 2,
boulderIssuers: boulderIssuers,
keyPolicy: keyPolicy,
fc: fc,
metrics: cametrics,
logger: blog.NewMock(),
}
}
@ -272,13 +260,9 @@ func TestSerialPrefix(t *testing.T) {
nil,
nil,
nil,
"",
nil,
nil,
nil,
testCtx.certExpiry,
testCtx.certBackdate,
0,
0x00,
testCtx.maxNames,
testCtx.keyPolicy,
testCtx.logger,
@ -290,13 +274,9 @@ func TestSerialPrefix(t *testing.T) {
nil,
nil,
nil,
"",
nil,
nil,
nil,
testCtx.certExpiry,
testCtx.certBackdate,
128,
0x80,
testCtx.maxNames,
testCtx.keyPolicy,
testCtx.logger,
@ -334,36 +314,29 @@ func TestIssuePrecertificate(t *testing.T) {
subTest func(t *testing.T, i *TestCertificateIssuance)
}{
{"IssuePrecertificate", CNandSANCSR, issueCertificateSubTestIssuePrecertificate},
{"ValidityUsesCAClock", CNandSANCSR, issueCertificateSubTestValidityUsesCAClock},
{"ProfileSelectionRSA", CNandSANCSR, issueCertificateSubTestProfileSelectionRSA},
{"ProfileSelectionECDSA", ECDSACSR, issueCertificateSubTestProfileSelectionECDSA},
{"MustStaple", MustStapleCSR, issueCertificateSubTestMustStaple},
{"UnknownExtension", UnsupportedExtensionCSR, issueCertificateSubTestUnknownExtension},
{"CTPoisonExtension", CTPoisonExtensionCSR, issueCertificateSubTestCTPoisonExtension},
{"CTPoisonExtensionEmpty", CTPoisonExtensionEmptyCSR, issueCertificateSubTestCTPoisonExtension},
}
for _, testCase := range testCases {
// TODO(#7454) Remove this rebinding
testCase := testCase
// The loop through the issuance modes must be inside the loop through
// |testCases| because the "certificate-for-precertificate" tests use
// the precertificates previously generated from the preceding
// "precertificate" test.
for _, mode := range []string{"precertificate", "certificate-for-precertificate"} {
ca, sa := issueCertificateSubTestSetup(t, nil)
ca, sa := issueCertificateSubTestSetup(t)
t.Run(fmt.Sprintf("%s - %s", mode, testCase.name), func(t *testing.T) {
t.Parallel()
req, err := x509.ParseCertificateRequest(testCase.csr)
test.AssertNotError(t, err, "Certificate request failed to parse")
issueReq := &capb.IssueCertificateRequest{Csr: testCase.csr, RegistrationID: arbitraryRegID}
var certDER []byte
response, err := ca.IssuePrecertificate(ctx, issueReq)
issueReq := &capb.IssueCertificateRequest{Csr: testCase.csr, RegistrationID: mrand.Int63(), OrderID: mrand.Int63()}
profile := ca.certProfiles["legacy"]
certDER, err := ca.issuePrecertificate(ctx, profile, issueReq)
test.AssertNotError(t, err, "Failed to issue precertificate")
certDER = response.DER
cert, err := x509.ParseCertificate(certDER)
test.AssertNotError(t, err, "Certificate failed to parse")
@ -388,23 +361,21 @@ func TestIssuePrecertificate(t *testing.T) {
}
}
func issueCertificateSubTestSetup(t *testing.T, e *ECDSAAllowList) (*certificateAuthorityImpl, *mockSA) {
type mockSCTService struct{}
func (m mockSCTService) GetSCTs(ctx context.Context, sctRequest *rapb.SCTRequest, _ ...grpc.CallOption) (*rapb.SCTResponse, error) {
return &rapb.SCTResponse{}, nil
}
func issueCertificateSubTestSetup(t *testing.T) (*certificateAuthorityImpl, *mockSA) {
testCtx := setup(t)
ecdsaAllowList := &ECDSAAllowList{}
if e == nil {
e = ecdsaAllowList
}
sa := &mockSA{}
ca, err := NewCertificateAuthorityImpl(
sa,
mockSCTService{},
testCtx.pa,
testCtx.boulderIssuers,
testCtx.defaultCertProfileName,
testCtx.certProfiles,
testCtx.lints,
e,
testCtx.certExpiry,
testCtx.certBackdate,
testCtx.serialPrefix,
testCtx.maxNames,
testCtx.keyPolicy,
@ -433,11 +404,6 @@ func issueCertificateSubTestIssuePrecertificate(t *testing.T, i *TestCertificate
}
}
func issueCertificateSubTestValidityUsesCAClock(t *testing.T, i *TestCertificateIssuance) {
test.AssertEquals(t, i.cert.NotBefore, i.ca.clk.Now().Add(-1*i.ca.backdate))
test.AssertEquals(t, i.cert.NotAfter.Add(time.Second).Sub(i.cert.NotBefore), i.ca.validityPeriod)
}
// Test failure mode when no issuers are present.
func TestNoIssuers(t *testing.T) {
t.Parallel()
@ -445,14 +411,10 @@ func TestNoIssuers(t *testing.T) {
sa := &mockSA{}
_, err := NewCertificateAuthorityImpl(
sa,
mockSCTService{},
testCtx.pa,
nil, // No issuers
testCtx.defaultCertProfileName,
testCtx.certProfiles,
testCtx.lints,
nil,
testCtx.certExpiry,
testCtx.certBackdate,
testCtx.serialPrefix,
testCtx.maxNames,
testCtx.keyPolicy,
@ -470,14 +432,10 @@ func TestMultipleIssuers(t *testing.T) {
sa := &mockSA{}
ca, err := NewCertificateAuthorityImpl(
sa,
mockSCTService{},
testCtx.pa,
testCtx.boulderIssuers,
testCtx.defaultCertProfileName,
testCtx.certProfiles,
testCtx.lints,
nil,
testCtx.certExpiry,
testCtx.certBackdate,
testCtx.serialPrefix,
testCtx.maxNames,
testCtx.keyPolicy,
@ -486,14 +444,11 @@ func TestMultipleIssuers(t *testing.T) {
testCtx.fc)
test.AssertNotError(t, err, "Failed to remake CA")
selectedProfile := ca.certProfiles.defaultName
_, ok := ca.certProfiles.profileByName[selectedProfile]
test.Assert(t, ok, "Certificate profile was expected to exist")
// Test that an RSA CSR gets issuance from an RSA issuer.
issuedCert, err := ca.IssuePrecertificate(ctx, &capb.IssueCertificateRequest{Csr: CNandSANCSR, RegistrationID: arbitraryRegID, CertProfileName: selectedProfile})
profile := ca.certProfiles["legacy"]
issuedCertDER, err := ca.issuePrecertificate(ctx, profile, &capb.IssueCertificateRequest{Csr: CNandSANCSR, RegistrationID: mrand.Int63(), OrderID: mrand.Int63()})
test.AssertNotError(t, err, "Failed to issue certificate")
cert, err := x509.ParseCertificate(issuedCert.DER)
cert, err := x509.ParseCertificate(issuedCertDER)
test.AssertNotError(t, err, "Certificate failed to parse")
validated := false
for _, issuer := range ca.issuers.byAlg[x509.RSA] {
@ -507,9 +462,9 @@ func TestMultipleIssuers(t *testing.T) {
test.AssertMetricWithLabelsEquals(t, ca.metrics.signatureCount, prometheus.Labels{"purpose": "precertificate", "status": "success"}, 1)
// Test that an ECDSA CSR gets issuance from an ECDSA issuer.
issuedCert, err = ca.IssuePrecertificate(ctx, &capb.IssueCertificateRequest{Csr: ECDSACSR, RegistrationID: arbitraryRegID, CertProfileName: selectedProfile})
issuedCertDER, err = ca.issuePrecertificate(ctx, profile, &capb.IssueCertificateRequest{Csr: ECDSACSR, RegistrationID: mrand.Int63(), OrderID: mrand.Int63(), CertProfileName: "legacy"})
test.AssertNotError(t, err, "Failed to issue certificate")
cert, err = x509.ParseCertificate(issuedCert.DER)
cert, err = x509.ParseCertificate(issuedCertDER)
test.AssertNotError(t, err, "Certificate failed to parse")
validated = false
for _, issuer := range ca.issuers.byAlg[x509.ECDSA] {
@ -538,6 +493,7 @@ func TestUnpredictableIssuance(t *testing.T) {
IssuerURL: fmt.Sprintf("http://not-example.com/i/%s", name),
OCSPURL: "http://not-example.com/o",
CRLURLBase: fmt.Sprintf("http://not-example.com/c/%s/", name),
CRLShards: 10,
Location: issuance.IssuerLoc{
File: fmt.Sprintf("../test/hierarchy/%s.key.pem", name),
CertFile: fmt.Sprintf("../test/hierarchy/%s.cert.pem", name),
@ -548,14 +504,10 @@ func TestUnpredictableIssuance(t *testing.T) {
ca, err := NewCertificateAuthorityImpl(
sa,
mockSCTService{},
testCtx.pa,
boulderIssuers,
testCtx.defaultCertProfileName,
testCtx.certProfiles,
testCtx.lints,
nil,
testCtx.certExpiry,
testCtx.certBackdate,
testCtx.serialPrefix,
testCtx.maxNames,
testCtx.keyPolicy,
@ -576,13 +528,14 @@ func TestUnpredictableIssuance(t *testing.T) {
// trials, the probability that all 20 issuances come from the same issuer is
// 0.5 ^ 20 = 9.5e-7 ~= 1e-6 = 1 in a million, so we do not consider this test
// to be flaky.
req := &capb.IssueCertificateRequest{Csr: ECDSACSR, RegistrationID: arbitraryRegID}
req := &capb.IssueCertificateRequest{Csr: ECDSACSR, RegistrationID: mrand.Int63(), OrderID: mrand.Int63()}
seenE2 := false
seenR3 := false
profile := ca.certProfiles["legacy"]
for i := 0; i < 20; i++ {
result, err := ca.IssuePrecertificate(ctx, req)
precertDER, err := ca.issuePrecertificate(ctx, profile, req)
test.AssertNotError(t, err, "Failed to issue test certificate")
cert, err := x509.ParseCertificate(result.DER)
cert, err := x509.ParseCertificate(precertDER)
test.AssertNotError(t, err, "Failed to parse test certificate")
if strings.Contains(cert.Issuer.CommonName, "E1") {
t.Fatal("Issued certificate from inactive issuer")
@ -596,209 +549,74 @@ func TestUnpredictableIssuance(t *testing.T) {
test.Assert(t, seenR3, "Expected at least one issuance from active issuer")
}
func TestProfiles(t *testing.T) {
func TestMakeCertificateProfilesMap(t *testing.T) {
t.Parallel()
testCtx := setup(t)
test.AssertEquals(t, len(testCtx.certProfiles), 2)
sa := &mockSA{}
duplicateProfiles := make(map[string]issuance.ProfileConfig, 0)
// These profiles contain the same data which will produce an identical
// hash, even though the names are different.
duplicateProfiles["defaultBoulderCertificateProfile"] = issuance.ProfileConfig{
AllowMustStaple: false,
AllowCTPoison: false,
AllowSCTList: false,
AllowCommonName: false,
Policies: []issuance.PolicyConfig{
{OID: "2.23.140.1.2.1"},
},
MaxValidityPeriod: config.Duration{Duration: time.Hour * 8760},
MaxValidityBackdate: config.Duration{Duration: time.Hour},
}
duplicateProfiles["uhoh_ohno"] = issuance.ProfileConfig{
AllowMustStaple: false,
AllowCTPoison: false,
AllowSCTList: false,
AllowCommonName: false,
Policies: []issuance.PolicyConfig{
{OID: "2.23.140.1.2.1"},
},
MaxValidityPeriod: config.Duration{Duration: time.Hour * 8760},
MaxValidityBackdate: config.Duration{Duration: time.Hour},
}
test.AssertEquals(t, len(duplicateProfiles), 2)
jackedProfiles := make(map[string]issuance.ProfileConfig, 0)
jackedProfiles["ruhroh"] = issuance.ProfileConfig{
AllowMustStaple: false,
AllowCTPoison: false,
AllowSCTList: false,
AllowCommonName: false,
Policies: []issuance.PolicyConfig{
{OID: "2.23.140.1.2.1"},
},
MaxValidityPeriod: config.Duration{Duration: time.Hour * 9000},
MaxValidityBackdate: config.Duration{Duration: time.Hour},
}
test.AssertEquals(t, len(jackedProfiles), 1)
type nameToHash struct {
name string
hash [32]byte
}
emptyMap := make(map[string]issuance.ProfileConfig, 0)
testCases := []struct {
name string
profileConfigs map[string]issuance.ProfileConfig
defaultName string
profileConfigs map[string]*issuance.ProfileConfig
expectedErrSubstr string
expectedProfiles []nameToHash
expectedProfiles []string
}{
{
name: "no profiles",
profileConfigs: emptyMap,
expectedErrSubstr: "at least one certificate profile",
},
{
name: "nil profile map",
profileConfigs: nil,
expectedErrSubstr: "at least one certificate profile",
},
{
name: "duplicate hash",
profileConfigs: duplicateProfiles,
expectedErrSubstr: "duplicate certificate profile hash",
name: "no profiles",
profileConfigs: map[string]*issuance.ProfileConfig{},
expectedErrSubstr: "at least one certificate profile",
},
{
name: "default profiles from setup func",
profileConfigs: testCtx.certProfiles,
expectedProfiles: []nameToHash{
{
name: testCtx.defaultCertProfileName,
hash: [32]byte{205, 182, 88, 236, 32, 18, 154, 120, 148, 194, 42, 215, 117, 140, 13, 169, 127, 196, 219, 67, 82, 36, 147, 67, 254, 117, 65, 112, 202, 60, 185, 9},
},
{
name: "longerLived",
hash: [32]byte{80, 228, 198, 83, 7, 184, 187, 236, 113, 17, 103, 213, 226, 245, 172, 212, 135, 241, 125, 92, 122, 200, 34, 159, 139, 72, 191, 41, 1, 244, 86, 62},
},
name: "empty profile config",
profileConfigs: map[string]*issuance.ProfileConfig{
"empty": {},
},
expectedErrSubstr: "at least one revocation mechanism must be included",
},
{
name: "no profile matching default name",
profileConfigs: jackedProfiles,
expectedErrSubstr: "profile object was not found for that name",
},
{
name: "certificate profile hash changed mid-issuance",
profileConfigs: jackedProfiles,
defaultName: "ruhroh",
expectedProfiles: []nameToHash{
{
// We'll change the mapped hash key under the hood during
// the test.
name: "ruhroh",
hash: [32]byte{84, 131, 8, 59, 3, 244, 7, 36, 151, 161, 118, 68, 117, 183, 197, 177, 179, 232, 215, 10, 188, 48, 159, 195, 195, 140, 19, 204, 201, 182, 239, 235},
},
name: "minimal profile config",
profileConfigs: map[string]*issuance.ProfileConfig{
"empty": {IncludeCRLDistributionPoints: true},
},
expectedProfiles: []string{"empty"},
},
{
name: "default profiles from setup func",
profileConfigs: testCtx.certProfiles,
expectedProfiles: []string{"legacy", "modern"},
},
}
for _, tc := range testCases {
// TODO(#7454) Remove this rebinding
tc := tc
// This is handled by boulder-ca, not the CA package.
if tc.defaultName == "" {
tc.defaultName = testCtx.defaultCertProfileName
}
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
tCA, err := NewCertificateAuthorityImpl(
sa,
testCtx.pa,
testCtx.boulderIssuers,
tc.defaultName,
tc.profileConfigs,
testCtx.lints,
nil,
testCtx.certExpiry,
testCtx.certBackdate,
testCtx.serialPrefix,
testCtx.maxNames,
testCtx.keyPolicy,
testCtx.logger,
testCtx.metrics,
testCtx.fc,
)
profiles, err := makeCertificateProfilesMap(tc.profileConfigs)
if tc.expectedErrSubstr != "" {
test.AssertError(t, err, "profile construction should have failed")
test.AssertContains(t, err.Error(), tc.expectedErrSubstr)
test.AssertError(t, err, "No profile found during CA construction.")
} else {
test.AssertNotError(t, err, "Profiles should exist, but were not found")
test.AssertNotError(t, err, "profile construction should have succeeded")
}
if tc.expectedProfiles != nil {
test.AssertEquals(t, len(tc.expectedProfiles), len(tCA.certProfiles.profileByName))
test.AssertEquals(t, len(profiles), len(tc.expectedProfiles))
}
for _, expected := range tc.expectedProfiles {
cpwid, ok := tCA.certProfiles.profileByName[expected.name]
test.Assert(t, ok, "Profile name was not found, but should have been")
test.AssertEquals(t, expected.hash, cpwid.hash)
cpwid, ok := profiles[expected]
test.Assert(t, ok, fmt.Sprintf("expected profile %q not found", expected))
if tc.name == "certificate profile hash changed mid-issuance" {
// This is an attempt to simulate the hash changing, but the
// name remaining the same on a CA node in the duration
// between CA1 sending capb.IssuePrecerticateResponse and
// before the RA calls
// capb.IssueCertificateForPrecertificate. We expect the
// receiving CA2 to error that the hash we expect could not
// be found in the map.
originalHash := cpwid.hash
cpwid.hash = [32]byte{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 6, 6, 6}
test.AssertNotEquals(t, originalHash, cpwid.hash)
}
test.AssertEquals(t, cpwid.name, expected)
}
})
}
}
func TestECDSAAllowList(t *testing.T) {
t.Parallel()
req := &capb.IssueCertificateRequest{Csr: ECDSACSR, RegistrationID: arbitraryRegID}
// With allowlist containing arbitraryRegID, issuance should come from ECDSA issuer.
regIDMap := makeRegIDsMap([]int64{arbitraryRegID})
ca, _ := issueCertificateSubTestSetup(t, &ECDSAAllowList{regIDMap})
result, err := ca.IssuePrecertificate(ctx, req)
test.AssertNotError(t, err, "Failed to issue certificate")
cert, err := x509.ParseCertificate(result.DER)
test.AssertNotError(t, err, "Certificate failed to parse")
test.AssertEquals(t, cert.SignatureAlgorithm, x509.ECDSAWithSHA384)
// With allowlist not containing arbitraryRegID, issuance should fall back to RSA issuer.
regIDMap = makeRegIDsMap([]int64{2002})
ca, _ = issueCertificateSubTestSetup(t, &ECDSAAllowList{regIDMap})
result, err = ca.IssuePrecertificate(ctx, req)
test.AssertNotError(t, err, "Failed to issue certificate")
cert, err = x509.ParseCertificate(result.DER)
test.AssertNotError(t, err, "Certificate failed to parse")
test.AssertEquals(t, cert.SignatureAlgorithm, x509.SHA256WithRSA)
// With empty allowlist but ECDSAForAll enabled, issuance should come from ECDSA issuer.
ca, _ = issueCertificateSubTestSetup(t, nil)
features.Set(features.Config{ECDSAForAll: true})
defer features.Reset()
result, err = ca.IssuePrecertificate(ctx, req)
test.AssertNotError(t, err, "Failed to issue certificate")
cert, err = x509.ParseCertificate(result.DER)
test.AssertNotError(t, err, "Certificate failed to parse")
test.AssertEquals(t, cert.SignatureAlgorithm, x509.ECDSAWithSHA384)
}
func TestInvalidCSRs(t *testing.T) {
t.Parallel()
testCases := []struct {
@ -841,32 +659,20 @@ func TestInvalidCSRs(t *testing.T) {
// * Signature Algorithm: sha1WithRSAEncryption
{"RejectBadAlgorithm", "./testdata/bad_algorithm.der.csr", nil, "Issued a certificate based on a CSR with a bad signature algorithm.", berrors.BadCSR},
// CSR generated by Go:
// * Random RSA public key.
// * CN = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com
// * DNSNames = [none]
{"RejectLongCommonName", "./testdata/long_cn.der.csr", nil, "Issued a certificate with a CN over 64 bytes.", berrors.BadCSR},
// CSR generated by OpenSSL:
// Edited signature to become invalid.
{"RejectWrongSignature", "./testdata/invalid_signature.der.csr", nil, "Issued a certificate based on a CSR with an invalid signature.", berrors.BadCSR},
}
for _, testCase := range testCases {
// TODO(#7454) Remove this rebinding
testCase := testCase
testCtx := setup(t)
sa := &mockSA{}
ca, err := NewCertificateAuthorityImpl(
sa,
mockSCTService{},
testCtx.pa,
testCtx.boulderIssuers,
testCtx.defaultCertProfileName,
testCtx.certProfiles,
testCtx.lints,
nil,
testCtx.certExpiry,
testCtx.certBackdate,
testCtx.serialPrefix,
testCtx.maxNames,
testCtx.keyPolicy,
@ -878,8 +684,9 @@ func TestInvalidCSRs(t *testing.T) {
t.Run(testCase.name, func(t *testing.T) {
t.Parallel()
serializedCSR := mustRead(testCase.csrPath)
issueReq := &capb.IssueCertificateRequest{Csr: serializedCSR, RegistrationID: arbitraryRegID}
_, err = ca.IssuePrecertificate(ctx, issueReq)
profile := ca.certProfiles["legacy"]
issueReq := &capb.IssueCertificateRequest{Csr: serializedCSR, RegistrationID: mrand.Int63(), OrderID: mrand.Int63(), CertProfileName: "legacy"}
_, err = ca.issuePrecertificate(ctx, profile, issueReq)
test.AssertErrorIs(t, err, testCase.errorType)
test.AssertMetricWithLabelsEquals(t, ca.metrics.signatureCount, prometheus.Labels{"purpose": "cert"}, 0)
@ -895,17 +702,17 @@ func TestInvalidCSRs(t *testing.T) {
func TestRejectValidityTooLong(t *testing.T) {
t.Parallel()
testCtx := setup(t)
sa := &mockSA{}
// Jump to a time just moments before the test issuers expire.
future := testCtx.boulderIssuers[0].Cert.Certificate.NotAfter.Add(-1 * time.Hour)
testCtx.fc.Set(future)
ca, err := NewCertificateAuthorityImpl(
sa,
&mockSA{},
mockSCTService{},
testCtx.pa,
testCtx.boulderIssuers,
testCtx.defaultCertProfileName,
testCtx.certProfiles,
testCtx.lints,
nil,
testCtx.certExpiry,
testCtx.certBackdate,
testCtx.serialPrefix,
testCtx.maxNames,
testCtx.keyPolicy,
@ -914,12 +721,9 @@ func TestRejectValidityTooLong(t *testing.T) {
testCtx.fc)
test.AssertNotError(t, err, "Failed to create CA")
future, err := time.Parse(time.RFC3339, "2025-02-10T00:30:00Z")
test.AssertNotError(t, err, "Failed to parse time")
testCtx.fc.Set(future)
// Test that the CA rejects CSRs that would expire after the intermediate cert
_, err = ca.IssuePrecertificate(ctx, &capb.IssueCertificateRequest{Csr: CNandSANCSR, RegistrationID: arbitraryRegID})
profile := ca.certProfiles["legacy"]
_, err = ca.issuePrecertificate(ctx, profile, &capb.IssueCertificateRequest{Csr: CNandSANCSR, RegistrationID: mrand.Int63(), OrderID: mrand.Int63(), CertProfileName: "legacy"})
test.AssertError(t, err, "Cannot issue a certificate that expires after the intermediate certificate")
test.AssertErrorIs(t, err, berrors.InternalServer)
}
@ -938,30 +742,12 @@ func issueCertificateSubTestProfileSelectionECDSA(t *testing.T, i *TestCertifica
test.AssertEquals(t, i.cert.KeyUsage, expectedKeyUsage)
}
func countMustStaple(t *testing.T, cert *x509.Certificate) (count int) {
oidTLSFeature := asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 24}
mustStapleFeatureValue := []byte{0x30, 0x03, 0x02, 0x01, 0x05}
for _, ext := range cert.Extensions {
if ext.Id.Equal(oidTLSFeature) {
test.Assert(t, !ext.Critical, "Extension was marked critical")
test.AssertByteEquals(t, ext.Value, mustStapleFeatureValue)
count++
}
}
return count
}
func issueCertificateSubTestMustStaple(t *testing.T, i *TestCertificateIssuance) {
test.AssertMetricWithLabelsEquals(t, i.ca.metrics.signatureCount, prometheus.Labels{"purpose": "precertificate"}, 1)
test.AssertEquals(t, countMustStaple(t, i.cert), 1)
}
func issueCertificateSubTestUnknownExtension(t *testing.T, i *TestCertificateIssuance) {
test.AssertMetricWithLabelsEquals(t, i.ca.metrics.signatureCount, prometheus.Labels{"purpose": "precertificate"}, 1)
// NOTE: The hard-coded value here will have to change over time as Boulder
// adds or removes (unrequested/default) extensions in certificates.
expectedExtensionCount := 9
expectedExtensionCount := 10
test.AssertEquals(t, len(i.cert.Extensions), expectedExtensionCount)
}
@ -999,14 +785,10 @@ func TestIssueCertificateForPrecertificate(t *testing.T) {
sa := &mockSA{}
ca, err := NewCertificateAuthorityImpl(
sa,
mockSCTService{},
testCtx.pa,
testCtx.boulderIssuers,
testCtx.defaultCertProfileName,
testCtx.certProfiles,
testCtx.lints,
nil,
testCtx.certExpiry,
testCtx.certBackdate,
testCtx.serialPrefix,
testCtx.maxNames,
testCtx.keyPolicy,
@ -1015,13 +797,11 @@ func TestIssueCertificateForPrecertificate(t *testing.T) {
testCtx.fc)
test.AssertNotError(t, err, "Failed to create CA")
_, ok := ca.certProfiles.profileByName[ca.certProfiles.defaultName]
test.Assert(t, ok, "Certificate profile was expected to exist")
issueReq := capb.IssueCertificateRequest{Csr: CNandSANCSR, RegistrationID: arbitraryRegID, OrderID: 0}
precert, err := ca.IssuePrecertificate(ctx, &issueReq)
profile := ca.certProfiles["legacy"]
issueReq := capb.IssueCertificateRequest{Csr: CNandSANCSR, RegistrationID: mrand.Int63(), OrderID: mrand.Int63(), CertProfileName: "legacy"}
precertDER, err := ca.issuePrecertificate(ctx, profile, &issueReq)
test.AssertNotError(t, err, "Failed to issue precert")
parsedPrecert, err := x509.ParseCertificate(precert.DER)
parsedPrecert, err := x509.ParseCertificate(precertDER)
test.AssertNotError(t, err, "Failed to parse precert")
test.AssertMetricWithLabelsEquals(t, ca.metrics.signatureCount, prometheus.Labels{"purpose": "precertificate", "status": "success"}, 1)
test.AssertMetricWithLabelsEquals(t, ca.metrics.signatureCount, prometheus.Labels{"purpose": "certificate", "status": "success"}, 0)
@ -1038,15 +818,14 @@ func TestIssueCertificateForPrecertificate(t *testing.T) {
}
test.AssertNotError(t, err, "Failed to marshal SCT")
cert, err := ca.IssueCertificateForPrecertificate(ctx, &capb.IssueCertificateForPrecertificateRequest{
DER: precert.DER,
SCTs: sctBytes,
RegistrationID: arbitraryRegID,
OrderID: 0,
CertProfileHash: precert.CertProfileHash,
})
certDER, err := ca.issueCertificateForPrecertificate(ctx,
profile,
precertDER,
sctBytes,
mrand.Int63(),
mrand.Int63())
test.AssertNotError(t, err, "Failed to issue cert from precert")
parsedCert, err := x509.ParseCertificate(cert.Der)
parsedCert, err := x509.ParseCertificate(certDER)
test.AssertNotError(t, err, "Failed to parse cert")
test.AssertMetricWithLabelsEquals(t, ca.metrics.signatureCount, prometheus.Labels{"purpose": "certificate", "status": "success"}, 1)
@ -1068,14 +847,10 @@ func TestIssueCertificateForPrecertificateWithSpecificCertificateProfile(t *test
sa := &mockSA{}
ca, err := NewCertificateAuthorityImpl(
sa,
mockSCTService{},
testCtx.pa,
testCtx.boulderIssuers,
testCtx.defaultCertProfileName,
testCtx.certProfiles,
testCtx.lints,
nil,
testCtx.certExpiry,
testCtx.certBackdate,
testCtx.serialPrefix,
testCtx.maxNames,
testCtx.keyPolicy,
@ -1084,19 +859,19 @@ func TestIssueCertificateForPrecertificateWithSpecificCertificateProfile(t *test
testCtx.fc)
test.AssertNotError(t, err, "Failed to create CA")
selectedProfile := "longerLived"
certProfile, ok := ca.certProfiles.profileByName[selectedProfile]
selectedProfile := "modern"
certProfile, ok := ca.certProfiles[selectedProfile]
test.Assert(t, ok, "Certificate profile was expected to exist")
issueReq := capb.IssueCertificateRequest{
Csr: CNandSANCSR,
RegistrationID: arbitraryRegID,
OrderID: 0,
RegistrationID: mrand.Int63(),
OrderID: mrand.Int63(),
CertProfileName: selectedProfile,
}
precert, err := ca.IssuePrecertificate(ctx, &issueReq)
precertDER, err := ca.issuePrecertificate(ctx, certProfile, &issueReq)
test.AssertNotError(t, err, "Failed to issue precert")
parsedPrecert, err := x509.ParseCertificate(precert.DER)
parsedPrecert, err := x509.ParseCertificate(precertDER)
test.AssertNotError(t, err, "Failed to parse precert")
test.AssertMetricWithLabelsEquals(t, ca.metrics.signatureCount, prometheus.Labels{"purpose": "precertificate", "status": "success"}, 1)
test.AssertMetricWithLabelsEquals(t, ca.metrics.signatureCount, prometheus.Labels{"purpose": "certificate", "status": "success"}, 0)
@ -1113,15 +888,14 @@ func TestIssueCertificateForPrecertificateWithSpecificCertificateProfile(t *test
}
test.AssertNotError(t, err, "Failed to marshal SCT")
cert, err := ca.IssueCertificateForPrecertificate(ctx, &capb.IssueCertificateForPrecertificateRequest{
DER: precert.DER,
SCTs: sctBytes,
RegistrationID: arbitraryRegID,
OrderID: 0,
CertProfileHash: certProfile.hash[:],
})
certDER, err := ca.issueCertificateForPrecertificate(ctx,
certProfile,
precertDER,
sctBytes,
mrand.Int63(),
mrand.Int63())
test.AssertNotError(t, err, "Failed to issue cert from precert")
parsedCert, err := x509.ParseCertificate(cert.Der)
parsedCert, err := x509.ParseCertificate(certDER)
test.AssertNotError(t, err, "Failed to parse cert")
test.AssertMetricWithLabelsEquals(t, ca.metrics.signatureCount, prometheus.Labels{"purpose": "certificate", "status": "success"}, 1)
@ -1188,14 +962,10 @@ func TestIssueCertificateForPrecertificateDuplicateSerial(t *testing.T) {
sa := &dupeSA{}
ca, err := NewCertificateAuthorityImpl(
sa,
mockSCTService{},
testCtx.pa,
testCtx.boulderIssuers,
testCtx.defaultCertProfileName,
testCtx.certProfiles,
testCtx.lints,
nil,
testCtx.certExpiry,
testCtx.certBackdate,
testCtx.serialPrefix,
testCtx.maxNames,
testCtx.keyPolicy,
@ -1209,21 +979,17 @@ func TestIssueCertificateForPrecertificateDuplicateSerial(t *testing.T) {
t.Fatal(err)
}
selectedProfile := ca.certProfiles.defaultName
certProfile, ok := ca.certProfiles.profileByName[selectedProfile]
test.Assert(t, ok, "Certificate profile was expected to exist")
issueReq := capb.IssueCertificateRequest{Csr: CNandSANCSR, RegistrationID: arbitraryRegID, OrderID: 0}
precert, err := ca.IssuePrecertificate(ctx, &issueReq)
profile := ca.certProfiles["legacy"]
issueReq := capb.IssueCertificateRequest{Csr: CNandSANCSR, RegistrationID: mrand.Int63(), OrderID: mrand.Int63(), CertProfileName: "legacy"}
precertDER, err := ca.issuePrecertificate(ctx, profile, &issueReq)
test.AssertNotError(t, err, "Failed to issue precert")
test.AssertMetricWithLabelsEquals(t, ca.metrics.signatureCount, prometheus.Labels{"purpose": "precertificate", "status": "success"}, 1)
_, err = ca.IssueCertificateForPrecertificate(ctx, &capb.IssueCertificateForPrecertificateRequest{
DER: precert.DER,
SCTs: sctBytes,
RegistrationID: arbitraryRegID,
OrderID: 0,
CertProfileHash: certProfile.hash[:],
})
_, err = ca.issueCertificateForPrecertificate(ctx,
profile,
precertDER,
sctBytes,
mrand.Int63(),
mrand.Int63())
if err == nil {
t.Error("Expected error issuing duplicate serial but got none.")
}
@ -1239,14 +1005,10 @@ func TestIssueCertificateForPrecertificateDuplicateSerial(t *testing.T) {
errorsa := &getCertErrorSA{}
errorca, err := NewCertificateAuthorityImpl(
errorsa,
mockSCTService{},
testCtx.pa,
testCtx.boulderIssuers,
testCtx.defaultCertProfileName,
testCtx.certProfiles,
testCtx.lints,
nil,
testCtx.certExpiry,
testCtx.certBackdate,
testCtx.serialPrefix,
testCtx.maxNames,
testCtx.keyPolicy,
@ -1255,13 +1017,12 @@ func TestIssueCertificateForPrecertificateDuplicateSerial(t *testing.T) {
testCtx.fc)
test.AssertNotError(t, err, "Failed to create CA")
_, err = errorca.IssueCertificateForPrecertificate(ctx, &capb.IssueCertificateForPrecertificateRequest{
DER: precert.DER,
SCTs: sctBytes,
RegistrationID: arbitraryRegID,
OrderID: 0,
CertProfileHash: certProfile.hash[:],
})
_, err = errorca.issueCertificateForPrecertificate(ctx,
profile,
precertDER,
sctBytes,
mrand.Int63(),
mrand.Int63())
if err == nil {
t.Fatal("Expected error issuing duplicate serial but got none.")
}
@ -1369,8 +1130,6 @@ func TestVerifyTBSCertIsDeterministic(t *testing.T) {
}
for _, testCase := range testCases {
// TODO(#7454) Remove this rebinding
testCase := testCase
t.Run(testCase.name, func(t *testing.T) {
t.Parallel()
err := tbsCertIsDeterministic(testCase.lintCertBytes, testCase.leafCertBytes)

View file

@ -1,45 +0,0 @@
package ca
import (
"os"
"github.com/letsencrypt/boulder/strictyaml"
)
// ECDSAAllowList acts as a container for a map of Registration IDs.
type ECDSAAllowList struct {
regIDsMap map[int64]bool
}
// permitted checks if ECDSA issuance is permitted for the specified
// Registration ID.
func (e *ECDSAAllowList) permitted(regID int64) bool {
return e.regIDsMap[regID]
}
func makeRegIDsMap(regIDs []int64) map[int64]bool {
regIDsMap := make(map[int64]bool)
for _, regID := range regIDs {
regIDsMap[regID] = true
}
return regIDsMap
}
// NewECDSAAllowListFromFile is exported to allow `boulder-ca` to construct a
// new `ECDSAAllowList` object. It returns the ECDSAAllowList, the size of allow
// list after attempting to load it (for CA logging purposes so inner fields don't need to be exported), or an error.
func NewECDSAAllowListFromFile(filename string) (*ECDSAAllowList, int, error) {
configBytes, err := os.ReadFile(filename)
if err != nil {
return nil, 0, err
}
var regIDs []int64
err = strictyaml.Unmarshal(configBytes, &regIDs)
if err != nil {
return nil, 0, err
}
allowList := &ECDSAAllowList{regIDsMap: makeRegIDsMap(regIDs)}
return allowList, len(allowList.regIDsMap), nil
}

View file

@ -1,70 +0,0 @@
package ca
import (
"testing"
)
func TestNewECDSAAllowListFromFile(t *testing.T) {
t.Parallel()
type args struct {
filename string
}
tests := []struct {
name string
args args
want1337Permitted bool
wantEntries int
wantErrBool bool
}{
{
name: "one entry",
args: args{"testdata/ecdsa_allow_list.yml"},
want1337Permitted: true,
wantEntries: 1,
wantErrBool: false,
},
{
name: "one entry but it's not 1337",
args: args{"testdata/ecdsa_allow_list2.yml"},
want1337Permitted: false,
wantEntries: 1,
wantErrBool: false,
},
{
name: "should error due to no file",
args: args{"testdata/ecdsa_allow_list_no_exist.yml"},
want1337Permitted: false,
wantEntries: 0,
wantErrBool: true,
},
{
name: "should error due to malformed YAML",
args: args{"testdata/ecdsa_allow_list_malformed.yml"},
want1337Permitted: false,
wantEntries: 0,
wantErrBool: true,
},
}
for _, tt := range tests {
// TODO(Remove this >= go1.22.3) This shouldn't be necessary due to
// go1.22 changing loopvars.
// https://github.com/golang/go/issues/65612#issuecomment-1943342030
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
allowList, gotEntries, err := NewECDSAAllowListFromFile(tt.args.filename)
if (err != nil) != tt.wantErrBool {
t.Errorf("NewECDSAAllowListFromFile() error = %v, wantErr %v", err, tt.wantErrBool)
t.Error(allowList, gotEntries, err)
return
}
if allowList != nil && allowList.permitted(1337) != tt.want1337Permitted {
t.Errorf("NewECDSAAllowListFromFile() allowList = %v, want %v", allowList, tt.want1337Permitted)
}
if gotEntries != tt.wantEntries {
t.Errorf("NewECDSAAllowListFromFile() gotEntries = %v, want %v", gotEntries, tt.wantEntries)
}
})
}
}

View file

@ -4,6 +4,7 @@ import (
"context"
"crypto/x509"
"encoding/hex"
mrand "math/rand"
"testing"
"time"
@ -31,14 +32,10 @@ func TestOCSP(t *testing.T) {
testCtx := setup(t)
ca, err := NewCertificateAuthorityImpl(
&mockSA{},
mockSCTService{},
testCtx.pa,
testCtx.boulderIssuers,
testCtx.defaultCertProfileName,
testCtx.certProfiles,
testCtx.lints,
nil,
testCtx.certExpiry,
testCtx.certBackdate,
testCtx.serialPrefix,
testCtx.maxNames,
testCtx.keyPolicy,
@ -48,11 +45,12 @@ func TestOCSP(t *testing.T) {
test.AssertNotError(t, err, "Failed to create CA")
ocspi := testCtx.ocsp
profile := ca.certProfiles["legacy"]
// Issue a certificate from an RSA issuer, request OCSP from the same issuer,
// and make sure it works.
rsaCertPB, err := ca.IssuePrecertificate(ctx, &capb.IssueCertificateRequest{Csr: CNandSANCSR, RegistrationID: arbitraryRegID})
rsaCertDER, err := ca.issuePrecertificate(ctx, profile, &capb.IssueCertificateRequest{Csr: CNandSANCSR, RegistrationID: mrand.Int63(), OrderID: mrand.Int63(), CertProfileName: "legacy"})
test.AssertNotError(t, err, "Failed to issue certificate")
rsaCert, err := x509.ParseCertificate(rsaCertPB.DER)
rsaCert, err := x509.ParseCertificate(rsaCertDER)
test.AssertNotError(t, err, "Failed to parse rsaCert")
rsaIssuerID := issuance.IssuerNameID(rsaCert)
rsaOCSPPB, err := ocspi.GenerateOCSP(ctx, &capb.GenerateOCSPRequest{
@ -73,9 +71,9 @@ func TestOCSP(t *testing.T) {
// Issue a certificate from an ECDSA issuer, request OCSP from the same issuer,
// and make sure it works.
ecdsaCertPB, err := ca.IssuePrecertificate(ctx, &capb.IssueCertificateRequest{Csr: ECDSACSR, RegistrationID: arbitraryRegID})
ecdsaCertDER, err := ca.issuePrecertificate(ctx, profile, &capb.IssueCertificateRequest{Csr: ECDSACSR, RegistrationID: mrand.Int63(), OrderID: mrand.Int63(), CertProfileName: "legacy"})
test.AssertNotError(t, err, "Failed to issue certificate")
ecdsaCert, err := x509.ParseCertificate(ecdsaCertPB.DER)
ecdsaCert, err := x509.ParseCertificate(ecdsaCertDER)
test.AssertNotError(t, err, "Failed to parse ecdsaCert")
ecdsaIssuerID := issuance.IssuerNameID(ecdsaCert)
ecdsaOCSPPB, err := ocspi.GenerateOCSP(ctx, &capb.GenerateOCSPRequest{

View file

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.34.1
// protoc-gen-go v1.36.5
// protoc v3.20.1
// source: ca.proto
@ -13,6 +13,7 @@ import (
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
@ -23,10 +24,7 @@ const (
)
type IssueCertificateRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
state protoimpl.MessageState `protogen:"open.v1"`
// Next unused field number: 6
Csr []byte `protobuf:"bytes,1,opt,name=csr,proto3" json:"csr,omitempty"`
RegistrationID int64 `protobuf:"varint,2,opt,name=registrationID,proto3" json:"registrationID,omitempty"`
@ -36,15 +34,15 @@ type IssueCertificateRequest struct {
// assigned inside the CA during *Profile construction if no name is provided.
// The value of this field should not be relied upon inside the RA.
CertProfileName string `protobuf:"bytes,5,opt,name=certProfileName,proto3" json:"certProfileName,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *IssueCertificateRequest) Reset() {
*x = IssueCertificateRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_ca_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
mi := &file_ca_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *IssueCertificateRequest) String() string {
@ -55,7 +53,7 @@ func (*IssueCertificateRequest) ProtoMessage() {}
func (x *IssueCertificateRequest) ProtoReflect() protoreflect.Message {
mi := &file_ca_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@ -98,41 +96,29 @@ func (x *IssueCertificateRequest) GetCertProfileName() string {
return ""
}
type IssuePrecertificateResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
type IssueCertificateResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
DER []byte `protobuf:"bytes,1,opt,name=DER,proto3" json:"DER,omitempty"`
unknownFields protoimpl.UnknownFields
// Next unused field number: 4
DER []byte `protobuf:"bytes,1,opt,name=DER,proto3" json:"DER,omitempty"`
// certProfileHash is a hash over the exported fields of a certificate profile
// to ensure that the profile remains unchanged after multiple roundtrips
// through the RA and CA.
CertProfileHash []byte `protobuf:"bytes,2,opt,name=certProfileHash,proto3" json:"certProfileHash,omitempty"`
// certProfileName is a human readable name returned back to the RA for later
// use. If IssueCertificateRequest.certProfileName was an empty string, the
// CAs default profile name will be assigned.
CertProfileName string `protobuf:"bytes,3,opt,name=certProfileName,proto3" json:"certProfileName,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *IssuePrecertificateResponse) Reset() {
*x = IssuePrecertificateResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_ca_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *IssueCertificateResponse) Reset() {
*x = IssueCertificateResponse{}
mi := &file_ca_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *IssuePrecertificateResponse) String() string {
func (x *IssueCertificateResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*IssuePrecertificateResponse) ProtoMessage() {}
func (*IssueCertificateResponse) ProtoMessage() {}
func (x *IssuePrecertificateResponse) ProtoReflect() protoreflect.Message {
func (x *IssueCertificateResponse) ProtoReflect() protoreflect.Message {
mi := &file_ca_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@ -142,136 +128,36 @@ func (x *IssuePrecertificateResponse) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
// Deprecated: Use IssuePrecertificateResponse.ProtoReflect.Descriptor instead.
func (*IssuePrecertificateResponse) Descriptor() ([]byte, []int) {
// Deprecated: Use IssueCertificateResponse.ProtoReflect.Descriptor instead.
func (*IssueCertificateResponse) Descriptor() ([]byte, []int) {
return file_ca_proto_rawDescGZIP(), []int{1}
}
func (x *IssuePrecertificateResponse) GetDER() []byte {
func (x *IssueCertificateResponse) GetDER() []byte {
if x != nil {
return x.DER
}
return nil
}
func (x *IssuePrecertificateResponse) GetCertProfileHash() []byte {
if x != nil {
return x.CertProfileHash
}
return nil
}
func (x *IssuePrecertificateResponse) GetCertProfileName() string {
if x != nil {
return x.CertProfileName
}
return ""
}
type IssueCertificateForPrecertificateRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// Next unused field number: 6
DER []byte `protobuf:"bytes,1,opt,name=DER,proto3" json:"DER,omitempty"`
SCTs [][]byte `protobuf:"bytes,2,rep,name=SCTs,proto3" json:"SCTs,omitempty"`
RegistrationID int64 `protobuf:"varint,3,opt,name=registrationID,proto3" json:"registrationID,omitempty"`
OrderID int64 `protobuf:"varint,4,opt,name=orderID,proto3" json:"orderID,omitempty"`
// certProfileHash is a hash over the exported fields of a certificate profile
// to ensure that the profile remains unchanged after multiple roundtrips
// through the RA and CA.
CertProfileHash []byte `protobuf:"bytes,5,opt,name=certProfileHash,proto3" json:"certProfileHash,omitempty"`
}
func (x *IssueCertificateForPrecertificateRequest) Reset() {
*x = IssueCertificateForPrecertificateRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_ca_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *IssueCertificateForPrecertificateRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*IssueCertificateForPrecertificateRequest) ProtoMessage() {}
func (x *IssueCertificateForPrecertificateRequest) ProtoReflect() protoreflect.Message {
mi := &file_ca_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use IssueCertificateForPrecertificateRequest.ProtoReflect.Descriptor instead.
func (*IssueCertificateForPrecertificateRequest) Descriptor() ([]byte, []int) {
return file_ca_proto_rawDescGZIP(), []int{2}
}
func (x *IssueCertificateForPrecertificateRequest) GetDER() []byte {
if x != nil {
return x.DER
}
return nil
}
func (x *IssueCertificateForPrecertificateRequest) GetSCTs() [][]byte {
if x != nil {
return x.SCTs
}
return nil
}
func (x *IssueCertificateForPrecertificateRequest) GetRegistrationID() int64 {
if x != nil {
return x.RegistrationID
}
return 0
}
func (x *IssueCertificateForPrecertificateRequest) GetOrderID() int64 {
if x != nil {
return x.OrderID
}
return 0
}
func (x *IssueCertificateForPrecertificateRequest) GetCertProfileHash() []byte {
if x != nil {
return x.CertProfileHash
}
return nil
}
// Exactly one of certDER or [serial and issuerID] must be set.
type GenerateOCSPRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
state protoimpl.MessageState `protogen:"open.v1"`
// Next unused field number: 8
Status string `protobuf:"bytes,2,opt,name=status,proto3" json:"status,omitempty"`
Reason int32 `protobuf:"varint,3,opt,name=reason,proto3" json:"reason,omitempty"`
RevokedAt *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=revokedAt,proto3" json:"revokedAt,omitempty"`
Serial string `protobuf:"bytes,5,opt,name=serial,proto3" json:"serial,omitempty"`
IssuerID int64 `protobuf:"varint,6,opt,name=issuerID,proto3" json:"issuerID,omitempty"`
Status string `protobuf:"bytes,2,opt,name=status,proto3" json:"status,omitempty"`
Reason int32 `protobuf:"varint,3,opt,name=reason,proto3" json:"reason,omitempty"`
RevokedAt *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=revokedAt,proto3" json:"revokedAt,omitempty"`
Serial string `protobuf:"bytes,5,opt,name=serial,proto3" json:"serial,omitempty"`
IssuerID int64 `protobuf:"varint,6,opt,name=issuerID,proto3" json:"issuerID,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GenerateOCSPRequest) Reset() {
*x = GenerateOCSPRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_ca_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
mi := &file_ca_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GenerateOCSPRequest) String() string {
@ -281,8 +167,8 @@ func (x *GenerateOCSPRequest) String() string {
func (*GenerateOCSPRequest) ProtoMessage() {}
func (x *GenerateOCSPRequest) ProtoReflect() protoreflect.Message {
mi := &file_ca_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil {
mi := &file_ca_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@ -294,7 +180,7 @@ func (x *GenerateOCSPRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use GenerateOCSPRequest.ProtoReflect.Descriptor instead.
func (*GenerateOCSPRequest) Descriptor() ([]byte, []int) {
return file_ca_proto_rawDescGZIP(), []int{3}
return file_ca_proto_rawDescGZIP(), []int{2}
}
func (x *GenerateOCSPRequest) GetStatus() string {
@ -333,20 +219,17 @@ func (x *GenerateOCSPRequest) GetIssuerID() int64 {
}
type OCSPResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Response []byte `protobuf:"bytes,1,opt,name=response,proto3" json:"response,omitempty"`
unknownFields protoimpl.UnknownFields
Response []byte `protobuf:"bytes,1,opt,name=response,proto3" json:"response,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *OCSPResponse) Reset() {
*x = OCSPResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_ca_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
mi := &file_ca_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *OCSPResponse) String() string {
@ -356,8 +239,8 @@ func (x *OCSPResponse) String() string {
func (*OCSPResponse) ProtoMessage() {}
func (x *OCSPResponse) ProtoReflect() protoreflect.Message {
mi := &file_ca_proto_msgTypes[4]
if protoimpl.UnsafeEnabled && x != nil {
mi := &file_ca_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@ -369,7 +252,7 @@ func (x *OCSPResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use OCSPResponse.ProtoReflect.Descriptor instead.
func (*OCSPResponse) Descriptor() ([]byte, []int) {
return file_ca_proto_rawDescGZIP(), []int{4}
return file_ca_proto_rawDescGZIP(), []int{3}
}
func (x *OCSPResponse) GetResponse() []byte {
@ -380,24 +263,21 @@ func (x *OCSPResponse) GetResponse() []byte {
}
type GenerateCRLRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// Types that are assignable to Payload:
state protoimpl.MessageState `protogen:"open.v1"`
// Types that are valid to be assigned to Payload:
//
// *GenerateCRLRequest_Metadata
// *GenerateCRLRequest_Entry
Payload isGenerateCRLRequest_Payload `protobuf_oneof:"payload"`
Payload isGenerateCRLRequest_Payload `protobuf_oneof:"payload"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GenerateCRLRequest) Reset() {
*x = GenerateCRLRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_ca_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
mi := &file_ca_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GenerateCRLRequest) String() string {
@ -407,8 +287,8 @@ func (x *GenerateCRLRequest) String() string {
func (*GenerateCRLRequest) ProtoMessage() {}
func (x *GenerateCRLRequest) ProtoReflect() protoreflect.Message {
mi := &file_ca_proto_msgTypes[5]
if protoimpl.UnsafeEnabled && x != nil {
mi := &file_ca_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@ -420,26 +300,30 @@ func (x *GenerateCRLRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use GenerateCRLRequest.ProtoReflect.Descriptor instead.
func (*GenerateCRLRequest) Descriptor() ([]byte, []int) {
return file_ca_proto_rawDescGZIP(), []int{5}
return file_ca_proto_rawDescGZIP(), []int{4}
}
func (m *GenerateCRLRequest) GetPayload() isGenerateCRLRequest_Payload {
if m != nil {
return m.Payload
func (x *GenerateCRLRequest) GetPayload() isGenerateCRLRequest_Payload {
if x != nil {
return x.Payload
}
return nil
}
func (x *GenerateCRLRequest) GetMetadata() *CRLMetadata {
if x, ok := x.GetPayload().(*GenerateCRLRequest_Metadata); ok {
return x.Metadata
if x != nil {
if x, ok := x.Payload.(*GenerateCRLRequest_Metadata); ok {
return x.Metadata
}
}
return nil
}
func (x *GenerateCRLRequest) GetEntry() *proto.CRLEntry {
if x, ok := x.GetPayload().(*GenerateCRLRequest_Entry); ok {
return x.Entry
if x != nil {
if x, ok := x.Payload.(*GenerateCRLRequest_Entry); ok {
return x.Entry
}
}
return nil
}
@ -461,23 +345,20 @@ func (*GenerateCRLRequest_Metadata) isGenerateCRLRequest_Payload() {}
func (*GenerateCRLRequest_Entry) isGenerateCRLRequest_Payload() {}
type CRLMetadata struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
state protoimpl.MessageState `protogen:"open.v1"`
// Next unused field number: 5
IssuerNameID int64 `protobuf:"varint,1,opt,name=issuerNameID,proto3" json:"issuerNameID,omitempty"`
ThisUpdate *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=thisUpdate,proto3" json:"thisUpdate,omitempty"`
ShardIdx int64 `protobuf:"varint,3,opt,name=shardIdx,proto3" json:"shardIdx,omitempty"`
IssuerNameID int64 `protobuf:"varint,1,opt,name=issuerNameID,proto3" json:"issuerNameID,omitempty"`
ThisUpdate *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=thisUpdate,proto3" json:"thisUpdate,omitempty"`
ShardIdx int64 `protobuf:"varint,3,opt,name=shardIdx,proto3" json:"shardIdx,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *CRLMetadata) Reset() {
*x = CRLMetadata{}
if protoimpl.UnsafeEnabled {
mi := &file_ca_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
mi := &file_ca_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *CRLMetadata) String() string {
@ -487,8 +368,8 @@ func (x *CRLMetadata) String() string {
func (*CRLMetadata) ProtoMessage() {}
func (x *CRLMetadata) ProtoReflect() protoreflect.Message {
mi := &file_ca_proto_msgTypes[6]
if protoimpl.UnsafeEnabled && x != nil {
mi := &file_ca_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@ -500,7 +381,7 @@ func (x *CRLMetadata) ProtoReflect() protoreflect.Message {
// Deprecated: Use CRLMetadata.ProtoReflect.Descriptor instead.
func (*CRLMetadata) Descriptor() ([]byte, []int) {
return file_ca_proto_rawDescGZIP(), []int{6}
return file_ca_proto_rawDescGZIP(), []int{5}
}
func (x *CRLMetadata) GetIssuerNameID() int64 {
@ -525,20 +406,17 @@ func (x *CRLMetadata) GetShardIdx() int64 {
}
type GenerateCRLResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Chunk []byte `protobuf:"bytes,1,opt,name=chunk,proto3" json:"chunk,omitempty"`
unknownFields protoimpl.UnknownFields
Chunk []byte `protobuf:"bytes,1,opt,name=chunk,proto3" json:"chunk,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *GenerateCRLResponse) Reset() {
*x = GenerateCRLResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_ca_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
mi := &file_ca_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GenerateCRLResponse) String() string {
@ -548,8 +426,8 @@ func (x *GenerateCRLResponse) String() string {
func (*GenerateCRLResponse) ProtoMessage() {}
func (x *GenerateCRLResponse) ProtoReflect() protoreflect.Message {
mi := &file_ca_proto_msgTypes[7]
if protoimpl.UnsafeEnabled && x != nil {
mi := &file_ca_proto_msgTypes[6]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@ -561,7 +439,7 @@ func (x *GenerateCRLResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use GenerateCRLResponse.ProtoReflect.Descriptor instead.
func (*GenerateCRLResponse) Descriptor() ([]byte, []int) {
return file_ca_proto_rawDescGZIP(), []int{7}
return file_ca_proto_rawDescGZIP(), []int{6}
}
func (x *GenerateCRLResponse) GetChunk() []byte {
@ -573,7 +451,7 @@ func (x *GenerateCRLResponse) GetChunk() []byte {
var File_ca_proto protoreflect.FileDescriptor
var file_ca_proto_rawDesc = []byte{
var file_ca_proto_rawDesc = string([]byte{
0x0a, 0x08, 0x63, 0x61, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x02, 0x63, 0x61, 0x1a, 0x15,
0x63, 0x6f, 0x72, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x63, 0x6f, 0x72, 0x65, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72,
@ -588,134 +466,106 @@ var file_ca_proto_rawDesc = []byte{
0x72, 0x64, 0x65, 0x72, 0x49, 0x44, 0x12, 0x28, 0x0a, 0x0f, 0x63, 0x65, 0x72, 0x74, 0x50, 0x72,
0x6f, 0x66, 0x69, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52,
0x0f, 0x63, 0x65, 0x72, 0x74, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65,
0x4a, 0x04, 0x08, 0x04, 0x10, 0x05, 0x22, 0x83, 0x01, 0x0a, 0x1b, 0x49, 0x73, 0x73, 0x75, 0x65,
0x50, 0x72, 0x65, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x44, 0x45, 0x52, 0x18, 0x01, 0x20,
0x01, 0x28, 0x0c, 0x52, 0x03, 0x44, 0x45, 0x52, 0x12, 0x28, 0x0a, 0x0f, 0x63, 0x65, 0x72, 0x74,
0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x48, 0x61, 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28,
0x0c, 0x52, 0x0f, 0x63, 0x65, 0x72, 0x74, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x48, 0x61,
0x73, 0x68, 0x12, 0x28, 0x0a, 0x0f, 0x63, 0x65, 0x72, 0x74, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c,
0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x63, 0x65, 0x72,
0x74, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0xbc, 0x01, 0x0a,
0x28, 0x49, 0x73, 0x73, 0x75, 0x65, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74,
0x65, 0x46, 0x6f, 0x72, 0x50, 0x72, 0x65, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61,
0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x44, 0x45, 0x52,
0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x44, 0x45, 0x52, 0x12, 0x12, 0x0a, 0x04, 0x53,
0x43, 0x54, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x04, 0x53, 0x43, 0x54, 0x73, 0x12,
0x26, 0x0a, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49,
0x44, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72,
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x6f, 0x72, 0x64, 0x65, 0x72,
0x49, 0x44, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x49,
0x44, 0x12, 0x28, 0x0a, 0x0f, 0x63, 0x65, 0x72, 0x74, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65,
0x48, 0x61, 0x73, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0f, 0x63, 0x65, 0x72, 0x74,
0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x48, 0x61, 0x73, 0x68, 0x22, 0xb9, 0x01, 0x0a, 0x13,
0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x4f, 0x43, 0x53, 0x50, 0x52, 0x65, 0x71, 0x75,
0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20,
0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x72,
0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x72, 0x65, 0x61,
0x73, 0x6f, 0x6e, 0x12, 0x38, 0x0a, 0x09, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x41, 0x74,
0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61,
0x6d, 0x70, 0x52, 0x09, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x41, 0x74, 0x12, 0x16, 0x0a,
0x06, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73,
0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x49,
0x44, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x49,
0x44, 0x4a, 0x04, 0x08, 0x04, 0x10, 0x05, 0x22, 0x2a, 0x0a, 0x0c, 0x4f, 0x43, 0x53, 0x50, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x22, 0x76, 0x0a, 0x12, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x43,
0x52, 0x4c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2d, 0x0a, 0x08, 0x6d, 0x65, 0x74,
0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x63, 0x61,
0x2e, 0x43, 0x52, 0x4c, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x48, 0x00, 0x52, 0x08,
0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x26, 0x0a, 0x05, 0x65, 0x6e, 0x74, 0x72,
0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x43,
0x52, 0x4c, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x48, 0x00, 0x52, 0x05, 0x65, 0x6e, 0x74, 0x72, 0x79,
0x42, 0x09, 0x0a, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x22, 0x8f, 0x01, 0x0a, 0x0b,
0x43, 0x52, 0x4c, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x22, 0x0a, 0x0c, 0x69,
0x73, 0x73, 0x75, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28,
0x03, 0x52, 0x0c, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x49, 0x44, 0x12,
0x3a, 0x0a, 0x0a, 0x74, 0x68, 0x69, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x18, 0x04, 0x20,
0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52,
0x0a, 0x74, 0x68, 0x69, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x73,
0x68, 0x61, 0x72, 0x64, 0x49, 0x64, 0x78, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x73,
0x68, 0x61, 0x72, 0x64, 0x49, 0x64, 0x78, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x22, 0x2b, 0x0a,
0x13, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x43, 0x52, 0x4c, 0x52, 0x65, 0x73, 0x70,
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x68, 0x75, 0x6e, 0x6b, 0x18, 0x01, 0x20,
0x01, 0x28, 0x0c, 0x52, 0x05, 0x63, 0x68, 0x75, 0x6e, 0x6b, 0x32, 0xd5, 0x01, 0x0a, 0x14, 0x43,
0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72,
0x69, 0x74, 0x79, 0x12, 0x55, 0x0a, 0x13, 0x49, 0x73, 0x73, 0x75, 0x65, 0x50, 0x72, 0x65, 0x63,
0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x1b, 0x2e, 0x63, 0x61, 0x2e,
0x49, 0x73, 0x73, 0x75, 0x65, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x63, 0x61, 0x2e, 0x49, 0x73, 0x73,
0x75, 0x65, 0x50, 0x72, 0x65, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x66, 0x0a, 0x21, 0x49, 0x73,
0x73, 0x75, 0x65, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x46, 0x6f,
0x72, 0x50, 0x72, 0x65, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12,
0x2c, 0x2e, 0x63, 0x61, 0x2e, 0x49, 0x73, 0x73, 0x75, 0x65, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66,
0x69, 0x63, 0x61, 0x74, 0x65, 0x46, 0x6f, 0x72, 0x50, 0x72, 0x65, 0x63, 0x65, 0x72, 0x74, 0x69,
0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e,
0x63, 0x6f, 0x72, 0x65, 0x2e, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65,
0x22, 0x00, 0x32, 0x4c, 0x0a, 0x0d, 0x4f, 0x43, 0x53, 0x50, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61,
0x74, 0x6f, 0x72, 0x12, 0x3b, 0x0a, 0x0c, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x4f,
0x43, 0x53, 0x50, 0x12, 0x17, 0x2e, 0x63, 0x61, 0x2e, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74,
0x65, 0x4f, 0x43, 0x53, 0x50, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x63,
0x61, 0x2e, 0x4f, 0x43, 0x53, 0x50, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00,
0x32, 0x54, 0x0a, 0x0c, 0x43, 0x52, 0x4c, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72,
0x12, 0x44, 0x0a, 0x0b, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x43, 0x52, 0x4c, 0x12,
0x16, 0x2e, 0x63, 0x61, 0x2e, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x43, 0x52, 0x4c,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x63, 0x61, 0x2e, 0x47, 0x65, 0x6e,
0x65, 0x72, 0x61, 0x74, 0x65, 0x43, 0x52, 0x4c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x22, 0x00, 0x28, 0x01, 0x30, 0x01, 0x42, 0x29, 0x5a, 0x27, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62,
0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x65, 0x74, 0x73, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74,
0x2f, 0x62, 0x6f, 0x75, 0x6c, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x61, 0x2f, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
0x4a, 0x04, 0x08, 0x04, 0x10, 0x05, 0x22, 0x2c, 0x0a, 0x18, 0x49, 0x73, 0x73, 0x75, 0x65, 0x43,
0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x44, 0x45, 0x52, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52,
0x03, 0x44, 0x45, 0x52, 0x22, 0xb9, 0x01, 0x0a, 0x13, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74,
0x65, 0x4f, 0x43, 0x53, 0x50, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06,
0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74,
0x61, 0x74, 0x75, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x03,
0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x38, 0x0a, 0x09,
0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x41, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32,
0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x72, 0x65, 0x76,
0x6f, 0x6b, 0x65, 0x64, 0x41, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c,
0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, 0x1a,
0x0a, 0x08, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x49, 0x44, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03,
0x52, 0x08, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x49, 0x44, 0x4a, 0x04, 0x08, 0x04, 0x10, 0x05,
0x22, 0x2a, 0x0a, 0x0c, 0x4f, 0x43, 0x53, 0x50, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x01, 0x20, 0x01,
0x28, 0x0c, 0x52, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x76, 0x0a, 0x12,
0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x43, 0x52, 0x4c, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x12, 0x2d, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01,
0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x63, 0x61, 0x2e, 0x43, 0x52, 0x4c, 0x4d, 0x65, 0x74,
0x61, 0x64, 0x61, 0x74, 0x61, 0x48, 0x00, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74,
0x61, 0x12, 0x26, 0x0a, 0x05, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b,
0x32, 0x0e, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x43, 0x52, 0x4c, 0x45, 0x6e, 0x74, 0x72, 0x79,
0x48, 0x00, 0x52, 0x05, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x42, 0x09, 0x0a, 0x07, 0x70, 0x61, 0x79,
0x6c, 0x6f, 0x61, 0x64, 0x22, 0x8f, 0x01, 0x0a, 0x0b, 0x43, 0x52, 0x4c, 0x4d, 0x65, 0x74, 0x61,
0x64, 0x61, 0x74, 0x61, 0x12, 0x22, 0x0a, 0x0c, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x4e, 0x61,
0x6d, 0x65, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x69, 0x73, 0x73, 0x75,
0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x49, 0x44, 0x12, 0x3a, 0x0a, 0x0a, 0x74, 0x68, 0x69, 0x73,
0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67,
0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54,
0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x74, 0x68, 0x69, 0x73, 0x55, 0x70,
0x64, 0x61, 0x74, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x68, 0x61, 0x72, 0x64, 0x49, 0x64, 0x78,
0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x73, 0x68, 0x61, 0x72, 0x64, 0x49, 0x64, 0x78,
0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x22, 0x2b, 0x0a, 0x13, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61,
0x74, 0x65, 0x43, 0x52, 0x4c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a,
0x05, 0x63, 0x68, 0x75, 0x6e, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x63, 0x68,
0x75, 0x6e, 0x6b, 0x32, 0x67, 0x0a, 0x14, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61,
0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x12, 0x4f, 0x0a, 0x10, 0x49,
0x73, 0x73, 0x75, 0x65, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12,
0x1b, 0x2e, 0x63, 0x61, 0x2e, 0x49, 0x73, 0x73, 0x75, 0x65, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66,
0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x63,
0x61, 0x2e, 0x49, 0x73, 0x73, 0x75, 0x65, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61,
0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x32, 0x4c, 0x0a, 0x0d,
0x4f, 0x43, 0x53, 0x50, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x12, 0x3b, 0x0a,
0x0c, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x4f, 0x43, 0x53, 0x50, 0x12, 0x17, 0x2e,
0x63, 0x61, 0x2e, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x4f, 0x43, 0x53, 0x50, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x63, 0x61, 0x2e, 0x4f, 0x43, 0x53, 0x50,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x32, 0x54, 0x0a, 0x0c, 0x43, 0x52,
0x4c, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x12, 0x44, 0x0a, 0x0b, 0x47, 0x65,
0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x43, 0x52, 0x4c, 0x12, 0x16, 0x2e, 0x63, 0x61, 0x2e, 0x47,
0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x43, 0x52, 0x4c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x1a, 0x17, 0x2e, 0x63, 0x61, 0x2e, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x43,
0x52, 0x4c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x28, 0x01, 0x30, 0x01,
0x42, 0x29, 0x5a, 0x27, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c,
0x65, 0x74, 0x73, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x2f, 0x62, 0x6f, 0x75, 0x6c, 0x64,
0x65, 0x72, 0x2f, 0x63, 0x61, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x33,
})
var (
file_ca_proto_rawDescOnce sync.Once
file_ca_proto_rawDescData = file_ca_proto_rawDesc
file_ca_proto_rawDescData []byte
)
func file_ca_proto_rawDescGZIP() []byte {
file_ca_proto_rawDescOnce.Do(func() {
file_ca_proto_rawDescData = protoimpl.X.CompressGZIP(file_ca_proto_rawDescData)
file_ca_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_ca_proto_rawDesc), len(file_ca_proto_rawDesc)))
})
return file_ca_proto_rawDescData
}
var file_ca_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
var file_ca_proto_goTypes = []interface{}{
(*IssueCertificateRequest)(nil), // 0: ca.IssueCertificateRequest
(*IssuePrecertificateResponse)(nil), // 1: ca.IssuePrecertificateResponse
(*IssueCertificateForPrecertificateRequest)(nil), // 2: ca.IssueCertificateForPrecertificateRequest
(*GenerateOCSPRequest)(nil), // 3: ca.GenerateOCSPRequest
(*OCSPResponse)(nil), // 4: ca.OCSPResponse
(*GenerateCRLRequest)(nil), // 5: ca.GenerateCRLRequest
(*CRLMetadata)(nil), // 6: ca.CRLMetadata
(*GenerateCRLResponse)(nil), // 7: ca.GenerateCRLResponse
(*timestamppb.Timestamp)(nil), // 8: google.protobuf.Timestamp
(*proto.CRLEntry)(nil), // 9: core.CRLEntry
(*proto.Certificate)(nil), // 10: core.Certificate
var file_ca_proto_msgTypes = make([]protoimpl.MessageInfo, 7)
var file_ca_proto_goTypes = []any{
(*IssueCertificateRequest)(nil), // 0: ca.IssueCertificateRequest
(*IssueCertificateResponse)(nil), // 1: ca.IssueCertificateResponse
(*GenerateOCSPRequest)(nil), // 2: ca.GenerateOCSPRequest
(*OCSPResponse)(nil), // 3: ca.OCSPResponse
(*GenerateCRLRequest)(nil), // 4: ca.GenerateCRLRequest
(*CRLMetadata)(nil), // 5: ca.CRLMetadata
(*GenerateCRLResponse)(nil), // 6: ca.GenerateCRLResponse
(*timestamppb.Timestamp)(nil), // 7: google.protobuf.Timestamp
(*proto.CRLEntry)(nil), // 8: core.CRLEntry
}
var file_ca_proto_depIdxs = []int32{
8, // 0: ca.GenerateOCSPRequest.revokedAt:type_name -> google.protobuf.Timestamp
6, // 1: ca.GenerateCRLRequest.metadata:type_name -> ca.CRLMetadata
9, // 2: ca.GenerateCRLRequest.entry:type_name -> core.CRLEntry
8, // 3: ca.CRLMetadata.thisUpdate:type_name -> google.protobuf.Timestamp
0, // 4: ca.CertificateAuthority.IssuePrecertificate:input_type -> ca.IssueCertificateRequest
2, // 5: ca.CertificateAuthority.IssueCertificateForPrecertificate:input_type -> ca.IssueCertificateForPrecertificateRequest
3, // 6: ca.OCSPGenerator.GenerateOCSP:input_type -> ca.GenerateOCSPRequest
5, // 7: ca.CRLGenerator.GenerateCRL:input_type -> ca.GenerateCRLRequest
1, // 8: ca.CertificateAuthority.IssuePrecertificate:output_type -> ca.IssuePrecertificateResponse
10, // 9: ca.CertificateAuthority.IssueCertificateForPrecertificate:output_type -> core.Certificate
4, // 10: ca.OCSPGenerator.GenerateOCSP:output_type -> ca.OCSPResponse
7, // 11: ca.CRLGenerator.GenerateCRL:output_type -> ca.GenerateCRLResponse
8, // [8:12] is the sub-list for method output_type
4, // [4:8] is the sub-list for method input_type
4, // [4:4] is the sub-list for extension type_name
4, // [4:4] is the sub-list for extension extendee
0, // [0:4] is the sub-list for field type_name
7, // 0: ca.GenerateOCSPRequest.revokedAt:type_name -> google.protobuf.Timestamp
5, // 1: ca.GenerateCRLRequest.metadata:type_name -> ca.CRLMetadata
8, // 2: ca.GenerateCRLRequest.entry:type_name -> core.CRLEntry
7, // 3: ca.CRLMetadata.thisUpdate:type_name -> google.protobuf.Timestamp
0, // 4: ca.CertificateAuthority.IssueCertificate:input_type -> ca.IssueCertificateRequest
2, // 5: ca.OCSPGenerator.GenerateOCSP:input_type -> ca.GenerateOCSPRequest
4, // 6: ca.CRLGenerator.GenerateCRL:input_type -> ca.GenerateCRLRequest
1, // 7: ca.CertificateAuthority.IssueCertificate:output_type -> ca.IssueCertificateResponse
3, // 8: ca.OCSPGenerator.GenerateOCSP:output_type -> ca.OCSPResponse
6, // 9: ca.CRLGenerator.GenerateCRL:output_type -> ca.GenerateCRLResponse
7, // [7:10] is the sub-list for method output_type
4, // [4:7] is the sub-list for method input_type
4, // [4:4] is the sub-list for extension type_name
4, // [4:4] is the sub-list for extension extendee
0, // [0:4] is the sub-list for field type_name
}
func init() { file_ca_proto_init() }
@ -723,105 +573,7 @@ func file_ca_proto_init() {
if File_ca_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_ca_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*IssueCertificateRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_ca_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*IssuePrecertificateResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_ca_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*IssueCertificateForPrecertificateRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_ca_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*GenerateOCSPRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_ca_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*OCSPResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_ca_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*GenerateCRLRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_ca_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*CRLMetadata); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_ca_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*GenerateCRLResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
file_ca_proto_msgTypes[5].OneofWrappers = []interface{}{
file_ca_proto_msgTypes[4].OneofWrappers = []any{
(*GenerateCRLRequest_Metadata)(nil),
(*GenerateCRLRequest_Entry)(nil),
}
@ -829,9 +581,9 @@ func file_ca_proto_init() {
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_ca_proto_rawDesc,
RawDescriptor: unsafe.Slice(unsafe.StringData(file_ca_proto_rawDesc), len(file_ca_proto_rawDesc)),
NumEnums: 0,
NumMessages: 8,
NumMessages: 7,
NumExtensions: 0,
NumServices: 3,
},
@ -840,7 +592,6 @@ func file_ca_proto_init() {
MessageInfos: file_ca_proto_msgTypes,
}.Build()
File_ca_proto = out.File
file_ca_proto_rawDesc = nil
file_ca_proto_goTypes = nil
file_ca_proto_depIdxs = nil
}

View file

@ -8,8 +8,8 @@ import "google/protobuf/timestamp.proto";
// CertificateAuthority issues certificates.
service CertificateAuthority {
rpc IssuePrecertificate(IssueCertificateRequest) returns (IssuePrecertificateResponse) {}
rpc IssueCertificateForPrecertificate(IssueCertificateForPrecertificateRequest) returns (core.Certificate) {}
// IssueCertificate issues a precertificate, gets SCTs, issues a certificate, and returns that.
rpc IssueCertificate(IssueCertificateRequest) returns (IssueCertificateResponse) {}
}
message IssueCertificateRequest {
@ -26,32 +26,8 @@ message IssueCertificateRequest {
string certProfileName = 5;
}
message IssuePrecertificateResponse {
// Next unused field number: 4
message IssueCertificateResponse {
bytes DER = 1;
// certProfileHash is a hash over the exported fields of a certificate profile
// to ensure that the profile remains unchanged after multiple roundtrips
// through the RA and CA.
bytes certProfileHash = 2;
// certProfileName is a human readable name returned back to the RA for later
// use. If IssueCertificateRequest.certProfileName was an empty string, the
// CAs default profile name will be assigned.
string certProfileName = 3;
}
message IssueCertificateForPrecertificateRequest {
// Next unused field number: 6
bytes DER = 1;
repeated bytes SCTs = 2;
int64 registrationID = 3;
int64 orderID = 4;
// certProfileHash is a hash over the exported fields of a certificate profile
// to ensure that the profile remains unchanged after multiple roundtrips
// through the RA and CA.
bytes certProfileHash = 5;
}
// OCSPGenerator generates OCSP. We separate this out from

View file

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.3.0
// - protoc-gen-go-grpc v1.5.1
// - protoc v3.20.1
// source: ca.proto
@ -8,7 +8,6 @@ package proto
import (
context "context"
proto "github.com/letsencrypt/boulder/core/proto"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
@ -20,16 +19,17 @@ import (
const _ = grpc.SupportPackageIsVersion9
const (
CertificateAuthority_IssuePrecertificate_FullMethodName = "/ca.CertificateAuthority/IssuePrecertificate"
CertificateAuthority_IssueCertificateForPrecertificate_FullMethodName = "/ca.CertificateAuthority/IssueCertificateForPrecertificate"
CertificateAuthority_IssueCertificate_FullMethodName = "/ca.CertificateAuthority/IssueCertificate"
)
// CertificateAuthorityClient is the client API for CertificateAuthority service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
//
// CertificateAuthority issues certificates.
type CertificateAuthorityClient interface {
IssuePrecertificate(ctx context.Context, in *IssueCertificateRequest, opts ...grpc.CallOption) (*IssuePrecertificateResponse, error)
IssueCertificateForPrecertificate(ctx context.Context, in *IssueCertificateForPrecertificateRequest, opts ...grpc.CallOption) (*proto.Certificate, error)
// IssueCertificate issues a precertificate, gets SCTs, issues a certificate, and returns that.
IssueCertificate(ctx context.Context, in *IssueCertificateRequest, opts ...grpc.CallOption) (*IssueCertificateResponse, error)
}
type certificateAuthorityClient struct {
@ -40,20 +40,10 @@ func NewCertificateAuthorityClient(cc grpc.ClientConnInterface) CertificateAutho
return &certificateAuthorityClient{cc}
}
func (c *certificateAuthorityClient) IssuePrecertificate(ctx context.Context, in *IssueCertificateRequest, opts ...grpc.CallOption) (*IssuePrecertificateResponse, error) {
func (c *certificateAuthorityClient) IssueCertificate(ctx context.Context, in *IssueCertificateRequest, opts ...grpc.CallOption) (*IssueCertificateResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(IssuePrecertificateResponse)
err := c.cc.Invoke(ctx, CertificateAuthority_IssuePrecertificate_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *certificateAuthorityClient) IssueCertificateForPrecertificate(ctx context.Context, in *IssueCertificateForPrecertificateRequest, opts ...grpc.CallOption) (*proto.Certificate, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(proto.Certificate)
err := c.cc.Invoke(ctx, CertificateAuthority_IssueCertificateForPrecertificate_FullMethodName, in, out, cOpts...)
out := new(IssueCertificateResponse)
err := c.cc.Invoke(ctx, CertificateAuthority_IssueCertificate_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@ -62,24 +52,27 @@ func (c *certificateAuthorityClient) IssueCertificateForPrecertificate(ctx conte
// CertificateAuthorityServer is the server API for CertificateAuthority service.
// All implementations must embed UnimplementedCertificateAuthorityServer
// for forward compatibility
// for forward compatibility.
//
// CertificateAuthority issues certificates.
type CertificateAuthorityServer interface {
IssuePrecertificate(context.Context, *IssueCertificateRequest) (*IssuePrecertificateResponse, error)
IssueCertificateForPrecertificate(context.Context, *IssueCertificateForPrecertificateRequest) (*proto.Certificate, error)
// IssueCertificate issues a precertificate, gets SCTs, issues a certificate, and returns that.
IssueCertificate(context.Context, *IssueCertificateRequest) (*IssueCertificateResponse, error)
mustEmbedUnimplementedCertificateAuthorityServer()
}
// UnimplementedCertificateAuthorityServer must be embedded to have forward compatible implementations.
type UnimplementedCertificateAuthorityServer struct {
}
// UnimplementedCertificateAuthorityServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedCertificateAuthorityServer struct{}
func (UnimplementedCertificateAuthorityServer) IssuePrecertificate(context.Context, *IssueCertificateRequest) (*IssuePrecertificateResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method IssuePrecertificate not implemented")
}
func (UnimplementedCertificateAuthorityServer) IssueCertificateForPrecertificate(context.Context, *IssueCertificateForPrecertificateRequest) (*proto.Certificate, error) {
return nil, status.Errorf(codes.Unimplemented, "method IssueCertificateForPrecertificate not implemented")
func (UnimplementedCertificateAuthorityServer) IssueCertificate(context.Context, *IssueCertificateRequest) (*IssueCertificateResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method IssueCertificate not implemented")
}
func (UnimplementedCertificateAuthorityServer) mustEmbedUnimplementedCertificateAuthorityServer() {}
func (UnimplementedCertificateAuthorityServer) testEmbeddedByValue() {}
// UnsafeCertificateAuthorityServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to CertificateAuthorityServer will
@ -89,41 +82,30 @@ type UnsafeCertificateAuthorityServer interface {
}
func RegisterCertificateAuthorityServer(s grpc.ServiceRegistrar, srv CertificateAuthorityServer) {
// If the following call pancis, it indicates UnimplementedCertificateAuthorityServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&CertificateAuthority_ServiceDesc, srv)
}
func _CertificateAuthority_IssuePrecertificate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
func _CertificateAuthority_IssueCertificate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(IssueCertificateRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(CertificateAuthorityServer).IssuePrecertificate(ctx, in)
return srv.(CertificateAuthorityServer).IssueCertificate(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: CertificateAuthority_IssuePrecertificate_FullMethodName,
FullMethod: CertificateAuthority_IssueCertificate_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(CertificateAuthorityServer).IssuePrecertificate(ctx, req.(*IssueCertificateRequest))
}
return interceptor(ctx, in, info, handler)
}
func _CertificateAuthority_IssueCertificateForPrecertificate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(IssueCertificateForPrecertificateRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(CertificateAuthorityServer).IssueCertificateForPrecertificate(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: CertificateAuthority_IssueCertificateForPrecertificate_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(CertificateAuthorityServer).IssueCertificateForPrecertificate(ctx, req.(*IssueCertificateForPrecertificateRequest))
return srv.(CertificateAuthorityServer).IssueCertificate(ctx, req.(*IssueCertificateRequest))
}
return interceptor(ctx, in, info, handler)
}
@ -136,12 +118,8 @@ var CertificateAuthority_ServiceDesc = grpc.ServiceDesc{
HandlerType: (*CertificateAuthorityServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "IssuePrecertificate",
Handler: _CertificateAuthority_IssuePrecertificate_Handler,
},
{
MethodName: "IssueCertificateForPrecertificate",
Handler: _CertificateAuthority_IssueCertificateForPrecertificate_Handler,
MethodName: "IssueCertificate",
Handler: _CertificateAuthority_IssueCertificate_Handler,
},
},
Streams: []grpc.StreamDesc{},
@ -155,6 +133,11 @@ const (
// OCSPGeneratorClient is the client API for OCSPGenerator service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
//
// OCSPGenerator generates OCSP. We separate this out from
// CertificateAuthority so that we can restrict access to a different subset of
// hosts, so the hosts that need to request OCSP generation don't need to be
// able to request certificate issuance.
type OCSPGeneratorClient interface {
GenerateOCSP(ctx context.Context, in *GenerateOCSPRequest, opts ...grpc.CallOption) (*OCSPResponse, error)
}
@ -179,20 +162,29 @@ func (c *oCSPGeneratorClient) GenerateOCSP(ctx context.Context, in *GenerateOCSP
// OCSPGeneratorServer is the server API for OCSPGenerator service.
// All implementations must embed UnimplementedOCSPGeneratorServer
// for forward compatibility
// for forward compatibility.
//
// OCSPGenerator generates OCSP. We separate this out from
// CertificateAuthority so that we can restrict access to a different subset of
// hosts, so the hosts that need to request OCSP generation don't need to be
// able to request certificate issuance.
type OCSPGeneratorServer interface {
GenerateOCSP(context.Context, *GenerateOCSPRequest) (*OCSPResponse, error)
mustEmbedUnimplementedOCSPGeneratorServer()
}
// UnimplementedOCSPGeneratorServer must be embedded to have forward compatible implementations.
type UnimplementedOCSPGeneratorServer struct {
}
// UnimplementedOCSPGeneratorServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedOCSPGeneratorServer struct{}
func (UnimplementedOCSPGeneratorServer) GenerateOCSP(context.Context, *GenerateOCSPRequest) (*OCSPResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GenerateOCSP not implemented")
}
func (UnimplementedOCSPGeneratorServer) mustEmbedUnimplementedOCSPGeneratorServer() {}
func (UnimplementedOCSPGeneratorServer) testEmbeddedByValue() {}
// UnsafeOCSPGeneratorServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to OCSPGeneratorServer will
@ -202,6 +194,13 @@ type UnsafeOCSPGeneratorServer interface {
}
func RegisterOCSPGeneratorServer(s grpc.ServiceRegistrar, srv OCSPGeneratorServer) {
// If the following call pancis, it indicates UnimplementedOCSPGeneratorServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&OCSPGenerator_ServiceDesc, srv)
}
@ -246,6 +245,8 @@ const (
// CRLGeneratorClient is the client API for CRLGenerator service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
//
// CRLGenerator signs CRLs. It is separated for the same reason as OCSPGenerator.
type CRLGeneratorClient interface {
GenerateCRL(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[GenerateCRLRequest, GenerateCRLResponse], error)
}
@ -273,20 +274,26 @@ type CRLGenerator_GenerateCRLClient = grpc.BidiStreamingClient[GenerateCRLReques
// CRLGeneratorServer is the server API for CRLGenerator service.
// All implementations must embed UnimplementedCRLGeneratorServer
// for forward compatibility
// for forward compatibility.
//
// CRLGenerator signs CRLs. It is separated for the same reason as OCSPGenerator.
type CRLGeneratorServer interface {
GenerateCRL(grpc.BidiStreamingServer[GenerateCRLRequest, GenerateCRLResponse]) error
mustEmbedUnimplementedCRLGeneratorServer()
}
// UnimplementedCRLGeneratorServer must be embedded to have forward compatible implementations.
type UnimplementedCRLGeneratorServer struct {
}
// UnimplementedCRLGeneratorServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedCRLGeneratorServer struct{}
func (UnimplementedCRLGeneratorServer) GenerateCRL(grpc.BidiStreamingServer[GenerateCRLRequest, GenerateCRLResponse]) error {
return status.Errorf(codes.Unimplemented, "method GenerateCRL not implemented")
}
func (UnimplementedCRLGeneratorServer) mustEmbedUnimplementedCRLGeneratorServer() {}
func (UnimplementedCRLGeneratorServer) testEmbeddedByValue() {}
// UnsafeCRLGeneratorServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to CRLGeneratorServer will
@ -296,6 +303,13 @@ type UnsafeCRLGeneratorServer interface {
}
func RegisterCRLGeneratorServer(s grpc.ServiceRegistrar, srv CRLGeneratorServer) {
// If the following call pancis, it indicates UnimplementedCRLGeneratorServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&CRLGenerator_ServiceDesc, srv)
}

View file

@ -1,2 +0,0 @@
---
- 1337

View file

@ -1,16 +0,0 @@
package canceled
import (
"context"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// Is returns true if err is non-nil and is either context.Canceled, or has a
// grpc code of Canceled. This is useful because cancellations propagate through
// gRPC boundaries, and if we choose to treat in-process cancellations a certain
// way, we usually want to treat cross-process cancellations the same way.
func Is(err error) bool {
return err == context.Canceled || status.Code(err) == codes.Canceled
}

View file

@ -1,22 +0,0 @@
package canceled
import (
"context"
"errors"
"testing"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func TestCanceled(t *testing.T) {
if !Is(context.Canceled) {
t.Errorf("Expected context.Canceled to be canceled, but wasn't.")
}
if !Is(status.Errorf(codes.Canceled, "hi")) {
t.Errorf("Expected gRPC cancellation to be cancelled, but wasn't.")
}
if Is(errors.New("hi")) {
t.Errorf("Expected random error to not be cancelled, but was.")
}
}

View file

@ -1,70 +0,0 @@
package notmain
import (
"fmt"
"os"
"github.com/letsencrypt/boulder/cmd"
"github.com/letsencrypt/boulder/features"
)
type Config struct {
Revoker struct {
DB cmd.DBConfig
// Similarly, the Revoker needs a TLSConfig to set up its GRPC client
// certs, but doesn't get the TLS field from ServiceConfig, so declares
// its own.
TLS cmd.TLSConfig
RAService *cmd.GRPCClientConfig
SAService *cmd.GRPCClientConfig
Features features.Config
}
Syslog cmd.SyslogConfig
}
func main() {
if len(os.Args) == 1 {
fmt.Println("use `admin -h` to learn how to use the new admin tool")
os.Exit(1)
}
command := os.Args[1]
switch {
case command == "serial-revoke":
fmt.Println("use `admin -config path/to/cfg.json revoke-cert -serial deadbeef -reason X` instead")
case command == "batched-serial-revoke":
fmt.Println("use `admin -config path/to/cfg.json revoke-cert -serials-file path -reason X` instead")
case command == "reg-revoke":
fmt.Println("use `admin -config path/to/cfg.json revoke-cert -reg-id Y -reason X` instead")
case command == "malformed-revoke":
fmt.Println("use `admin -config path/to/cfg.json revoke-cert -serial deadbeef -reason X -malformed` instead")
case command == "list-reasons":
fmt.Println("use `admin -config path/to/cfg.json revoke-cert -h` instead")
case command == "private-key-revoke":
fmt.Println("use `admin -config path/to/cfg.json revoke-cert -private-key path -reason X` instead")
case command == "private-key-block":
fmt.Println("use `admin -config path/to/cfg.json block-key -private-key path -comment foo` instead")
case command == "incident-table-revoke":
fmt.Println("use `admin -config path/to/cfg.json revoke-cert -incident-table tablename -reason X` instead")
case command == "clear-email":
fmt.Println("use `admin -config path/to/cfg.json update-email -address foo@bar.org -clear` instead")
default:
fmt.Println("use `admin -h` to see a list of flags and subcommands for the new admin tool")
}
}
func init() {
cmd.RegisterCommand("admin-revoker", main, &cmd.ConfigValidator{Config: &Config{}})
}

View file

@ -2,6 +2,7 @@ package main
import (
"context"
"errors"
"fmt"
"github.com/jmhodges/clock"
@ -47,7 +48,7 @@ func newAdmin(configFile string, dryRun bool) (*admin, error) {
return nil, fmt.Errorf("parsing config file: %w", err)
}
scope, logger, oTelShutdown := cmd.StatsAndLogging(c.Syslog, c.OpenTelemetry, c.Admin.DebugAddr)
scope, logger, oTelShutdown := cmd.StatsAndLogging(c.Syslog, c.OpenTelemetry, "")
defer oTelShutdown(context.Background())
logger.Info(cmd.VersionString())
@ -94,3 +95,22 @@ func newAdmin(configFile string, dryRun bool) (*admin, error) {
log: logger,
}, nil
}
// findActiveInputMethodFlag returns a single key from setInputs with a value of `true`,
// if exactly one exists. Otherwise it returns an error.
func findActiveInputMethodFlag(setInputs map[string]bool) (string, error) {
var activeFlags []string
for flag, isSet := range setInputs {
if isSet {
activeFlags = append(activeFlags, flag)
}
}
if len(activeFlags) == 0 {
return "", errors.New("at least one input method flag must be specified")
} else if len(activeFlags) > 1 {
return "", fmt.Errorf("more than one input method flag specified: %v", activeFlags)
}
return activeFlags[0], nil
}

View file

@ -0,0 +1,59 @@
package main
import (
"testing"
"github.com/letsencrypt/boulder/test"
)
func Test_findActiveInputMethodFlag(t *testing.T) {
tests := []struct {
name string
setInputs map[string]bool
expected string
wantErr bool
}{
{
name: "No active flags",
setInputs: map[string]bool{
"-private-key": false,
"-spki-file": false,
"-cert-file": false,
},
expected: "",
wantErr: true,
},
{
name: "Multiple active flags",
setInputs: map[string]bool{
"-private-key": true,
"-spki-file": true,
"-cert-file": false,
},
expected: "",
wantErr: true,
},
{
name: "Single active flag",
setInputs: map[string]bool{
"-private-key": true,
"-spki-file": false,
"-cert-file": false,
},
expected: "-private-key",
wantErr: false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result, err := findActiveInputMethodFlag(tc.setInputs)
if tc.wantErr {
test.AssertError(t, err, "findActiveInputMethodFlag() should have errored")
} else {
test.AssertNotError(t, err, "findActiveInputMethodFlag() should not have errored")
test.AssertEquals(t, result, tc.expected)
}
})
}
}

View file

@ -15,7 +15,6 @@ import (
"unicode"
"golang.org/x/crypto/ocsp"
"golang.org/x/exp/maps"
core "github.com/letsencrypt/boulder/core"
berrors "github.com/letsencrypt/boulder/errors"
@ -43,8 +42,9 @@ type subcommandRevokeCert struct {
incidentTable string
serialsFile string
privKey string
regID uint
regID int64
certFile string
crlShard int64
}
var _ subcommand = (*subcommandRevokeCert)(nil)
@ -59,13 +59,14 @@ func (s *subcommandRevokeCert) Flags(flag *flag.FlagSet) {
flag.StringVar(&s.reasonStr, "reason", "unspecified", "Revocation reason (unspecified, keyCompromise, superseded, cessationOfOperation, or privilegeWithdrawn)")
flag.BoolVar(&s.skipBlock, "skip-block-key", false, "Skip blocking the key, if revoked for keyCompromise - use with extreme caution")
flag.BoolVar(&s.malformed, "malformed", false, "Indicates that the cert cannot be parsed - use with caution")
flag.Int64Var(&s.crlShard, "crl-shard", 0, "For malformed certs, the CRL shard the certificate belongs to")
// Flags specifying the input method for the certificates to be revoked.
flag.StringVar(&s.serial, "serial", "", "Revoke the certificate with this hex serial")
flag.StringVar(&s.incidentTable, "incident-table", "", "Revoke all certificates whose serials are in this table")
flag.StringVar(&s.serialsFile, "serials-file", "", "Revoke all certificates whose hex serials are in this file")
flag.StringVar(&s.privKey, "private-key", "", "Revoke all certificates whose pubkey matches this private key")
flag.UintVar(&s.regID, "reg-id", 0, "Revoke all certificates issued to this account")
flag.Int64Var(&s.regID, "reg-id", 0, "Revoke all certificates issued to this account")
flag.StringVar(&s.certFile, "cert-file", "", "Revoke the single PEM-formatted certificate in this file")
}
@ -109,16 +110,13 @@ func (s *subcommandRevokeCert) Run(ctx context.Context, a *admin) error {
"-reg-id": s.regID != 0,
"-cert-file": s.certFile != "",
}
maps.DeleteFunc(setInputs, func(_ string, v bool) bool { return !v })
if len(setInputs) == 0 {
return errors.New("at least one input method flag must be specified")
} else if len(setInputs) > 1 {
return fmt.Errorf("more than one input method flag specified: %v", maps.Keys(setInputs))
activeFlag, err := findActiveInputMethodFlag(setInputs)
if err != nil {
return err
}
var serials []string
var err error
switch maps.Keys(setInputs)[0] {
switch activeFlag {
case "-serial":
serials, err = []string{s.serial}, nil
case "-incident-table":
@ -128,7 +126,7 @@ func (s *subcommandRevokeCert) Run(ctx context.Context, a *admin) error {
case "-private-key":
serials, err = a.serialsFromPrivateKey(ctx, s.privKey)
case "-reg-id":
serials, err = a.serialsFromRegID(ctx, int64(s.regID))
serials, err = a.serialsFromRegID(ctx, s.regID)
case "-cert-file":
serials, err = a.serialsFromCertPEM(ctx, s.certFile)
default:
@ -138,12 +136,22 @@ func (s *subcommandRevokeCert) Run(ctx context.Context, a *admin) error {
return fmt.Errorf("collecting serials to revoke: %w", err)
}
serials, err = cleanSerials(serials)
if err != nil {
return err
}
if len(serials) == 0 {
return errors.New("no serials to revoke found")
}
a.log.Infof("Found %d certificates to revoke", len(serials))
err = a.revokeSerials(ctx, serials, reasonCode, s.malformed, s.skipBlock, s.parallelism)
if s.malformed {
return s.revokeMalformed(ctx, a, serials, reasonCode)
}
err = a.revokeSerials(ctx, serials, reasonCode, s.skipBlock, s.parallelism)
if err != nil {
return fmt.Errorf("revoking serials: %w", err)
}
@ -151,6 +159,31 @@ func (s *subcommandRevokeCert) Run(ctx context.Context, a *admin) error {
return nil
}
func (s *subcommandRevokeCert) revokeMalformed(ctx context.Context, a *admin, serials []string, reasonCode revocation.Reason) error {
u, err := user.Current()
if err != nil {
return fmt.Errorf("getting admin username: %w", err)
}
if s.crlShard == 0 {
return errors.New("when revoking malformed certificates, a nonzero CRL shard must be specified")
}
if len(serials) > 1 {
return errors.New("when revoking malformed certificates, only one cert at a time is allowed")
}
_, err = a.rac.AdministrativelyRevokeCertificate(
ctx,
&rapb.AdministrativelyRevokeCertificateRequest{
Serial: serials[0],
Code: int64(reasonCode),
AdminName: u.Username,
SkipBlockKey: s.skipBlock,
Malformed: true,
CrlShard: s.crlShard,
},
)
return err
}
func (a *admin) serialsFromIncidentTable(ctx context.Context, tableName string) ([]string, error) {
stream, err := a.saroc.SerialsForIncident(ctx, &sapb.SerialsForIncidentRequest{IncidentTable: tableName})
if err != nil {
@ -252,7 +285,9 @@ func (a *admin) serialsFromCertPEM(_ context.Context, filename string) ([]string
return []string{core.SerialToString(cert.SerialNumber)}, nil
}
func cleanSerial(serial string) (string, error) {
// cleanSerials removes non-alphanumeric characters from the serials and checks
// that all resulting serials are valid (hex encoded, and the correct length).
func cleanSerials(serials []string) ([]string, error) {
serialStrip := func(r rune) rune {
switch {
case unicode.IsLetter(r):
@ -262,14 +297,19 @@ func cleanSerial(serial string) (string, error) {
}
return rune(-1)
}
strippedSerial := strings.Map(serialStrip, serial)
if !core.ValidSerial(strippedSerial) {
return "", fmt.Errorf("cleaned serial %q is not valid", strippedSerial)
var ret []string
for _, s := range serials {
cleaned := strings.Map(serialStrip, s)
if !core.ValidSerial(cleaned) {
return nil, fmt.Errorf("cleaned serial %q is not valid", cleaned)
}
ret = append(ret, cleaned)
}
return strippedSerial, nil
return ret, nil
}
func (a *admin) revokeSerials(ctx context.Context, serials []string, reason revocation.Reason, malformed bool, skipBlockKey bool, parallelism uint) error {
func (a *admin) revokeSerials(ctx context.Context, serials []string, reason revocation.Reason, skipBlockKey bool, parallelism uint) error {
u, err := user.Current()
if err != nil {
return fmt.Errorf("getting admin username: %w", err)
@ -283,19 +323,17 @@ func (a *admin) revokeSerials(ctx context.Context, serials []string, reason revo
go func() {
defer wg.Done()
for serial := range work {
cleanedSerial, err := cleanSerial(serial)
if err != nil {
a.log.Errf("skipping serial %q: %s", serial, err)
continue
}
_, err = a.rac.AdministrativelyRevokeCertificate(
_, err := a.rac.AdministrativelyRevokeCertificate(
ctx,
&rapb.AdministrativelyRevokeCertificateRequest{
Serial: cleanedSerial,
Serial: serial,
Code: int64(reason),
AdminName: u.Username,
SkipBlockKey: skipBlockKey,
Malformed: malformed,
// This is a well-formed certificate so send CrlShard 0
// to let the RA figure out the right shard from the cert.
Malformed: false,
CrlShard: 0,
},
)
if err != nil {

View file

@ -10,6 +10,7 @@ import (
"errors"
"os"
"path"
"reflect"
"slices"
"strings"
"sync"
@ -198,20 +199,20 @@ func (mra *mockRARecordingRevocations) reset() {
func TestRevokeSerials(t *testing.T) {
t.Parallel()
serials := []string{
"2a:18:59:2b:7f:4b:f5:96:fb:1a:1d:f1:35:56:7a:cd:82:5a",
"03:8c:3f:63:88:af:b7:69:5d:d4:d6:bb:e3:d2:64:f1:e4:e2",
"048c3f6388afb7695dd4d6bbe3d264f1e5e5!",
"2a18592b7f4bf596fb1a1df135567acd825a",
"038c3f6388afb7695dd4d6bbe3d264f1e4e2",
"048c3f6388afb7695dd4d6bbe3d264f1e5e5",
}
mra := mockRARecordingRevocations{}
log := blog.NewMock()
a := admin{rac: &mra, log: log}
assertRequestsContain := func(reqs []*rapb.AdministrativelyRevokeCertificateRequest, code revocation.Reason, skipBlockKey bool, malformed bool) {
assertRequestsContain := func(reqs []*rapb.AdministrativelyRevokeCertificateRequest, code revocation.Reason, skipBlockKey bool) {
t.Helper()
for _, req := range reqs {
test.AssertEquals(t, len(req.Cert), 0)
test.AssertEquals(t, req.Code, int64(code))
test.AssertEquals(t, req.SkipBlockKey, skipBlockKey)
test.AssertEquals(t, req.Malformed, malformed)
}
}
@ -219,49 +220,113 @@ func TestRevokeSerials(t *testing.T) {
mra.reset()
log.Clear()
a.dryRun = false
err := a.revokeSerials(context.Background(), serials, 0, false, false, 1)
err := a.revokeSerials(context.Background(), serials, 0, false, 1)
test.AssertEquals(t, len(log.GetAllMatching("invalid serial format")), 0)
test.AssertNotError(t, err, "")
test.AssertEquals(t, len(log.GetAll()), 0)
test.AssertEquals(t, len(mra.revocationRequests), 3)
assertRequestsContain(mra.revocationRequests, 0, false, false)
assertRequestsContain(mra.revocationRequests, 0, false)
// Revoking an already-revoked serial should result in one log line.
mra.reset()
log.Clear()
mra.alreadyRevoked = []string{"048c3f6388afb7695dd4d6bbe3d264f1e5e5"}
err = a.revokeSerials(context.Background(), serials, 0, false, false, 1)
err = a.revokeSerials(context.Background(), serials, 0, false, 1)
t.Logf("error: %s", err)
t.Logf("logs: %s", strings.Join(log.GetAll(), ""))
test.AssertError(t, err, "already-revoked should result in error")
test.AssertEquals(t, len(log.GetAllMatching("not revoking")), 1)
test.AssertEquals(t, len(mra.revocationRequests), 3)
assertRequestsContain(mra.revocationRequests, 0, false, false)
assertRequestsContain(mra.revocationRequests, 0, false)
// Revoking a doomed-to-fail serial should also result in one log line.
mra.reset()
log.Clear()
mra.doomedToFail = []string{"048c3f6388afb7695dd4d6bbe3d264f1e5e5"}
err = a.revokeSerials(context.Background(), serials, 0, false, false, 1)
err = a.revokeSerials(context.Background(), serials, 0, false, 1)
test.AssertError(t, err, "gRPC error should result in error")
test.AssertEquals(t, len(log.GetAllMatching("failed to revoke")), 1)
test.AssertEquals(t, len(mra.revocationRequests), 3)
assertRequestsContain(mra.revocationRequests, 0, false, false)
assertRequestsContain(mra.revocationRequests, 0, false)
// Revoking with other parameters should get carried through.
mra.reset()
log.Clear()
err = a.revokeSerials(context.Background(), serials, 1, true, true, 3)
err = a.revokeSerials(context.Background(), serials, 1, true, 3)
test.AssertNotError(t, err, "")
test.AssertEquals(t, len(mra.revocationRequests), 3)
assertRequestsContain(mra.revocationRequests, 1, true, true)
assertRequestsContain(mra.revocationRequests, 1, true)
// Revoking in dry-run mode should result in no gRPC requests and three logs.
mra.reset()
log.Clear()
a.dryRun = true
a.rac = dryRunRAC{log: log}
err = a.revokeSerials(context.Background(), serials, 0, false, false, 1)
err = a.revokeSerials(context.Background(), serials, 0, false, 1)
test.AssertNotError(t, err, "")
test.AssertEquals(t, len(log.GetAllMatching("dry-run:")), 3)
test.AssertEquals(t, len(mra.revocationRequests), 0)
assertRequestsContain(mra.revocationRequests, 0, false, false)
assertRequestsContain(mra.revocationRequests, 0, false)
}
func TestRevokeMalformed(t *testing.T) {
t.Parallel()
mra := mockRARecordingRevocations{}
log := blog.NewMock()
a := &admin{
rac: &mra,
log: log,
dryRun: false,
}
s := subcommandRevokeCert{
crlShard: 623,
}
serial := "0379c3dfdd518be45948f2dbfa6ea3e9b209"
err := s.revokeMalformed(context.Background(), a, []string{serial}, 1)
if err != nil {
t.Errorf("revokedMalformed with crlShard 623: want success, got %s", err)
}
if len(mra.revocationRequests) != 1 {
t.Errorf("revokeMalformed: want 1 revocation request to SA, got %v", mra.revocationRequests)
}
if mra.revocationRequests[0].Serial != serial {
t.Errorf("revokeMalformed: want %s to be revoked, got %s", serial, mra.revocationRequests[0])
}
s = subcommandRevokeCert{
crlShard: 0,
}
err = s.revokeMalformed(context.Background(), a, []string{"038c3f6388afb7695dd4d6bbe3d264f1e4e2"}, 1)
if err == nil {
t.Errorf("revokedMalformed with crlShard 0: want error, got none")
}
s = subcommandRevokeCert{
crlShard: 623,
}
err = s.revokeMalformed(context.Background(), a, []string{"038c3f6388afb7695dd4d6bbe3d264f1e4e2", "28a94f966eae14e525777188512ddf5a0a3b"}, 1)
if err == nil {
t.Errorf("revokedMalformed with multiple serials: want error, got none")
}
}
func TestCleanSerials(t *testing.T) {
input := []string{
"2a:18:59:2b:7f:4b:f5:96:fb:1a:1d:f1:35:56:7a:cd:82:5a",
"03:8c:3f:63:88:af:b7:69:5d:d4:d6:bb:e3:d2:64:f1:e4:e2",
"038c3f6388afb7695dd4d6bbe3d264f1e4e2",
}
expected := []string{
"2a18592b7f4bf596fb1a1df135567acd825a",
"038c3f6388afb7695dd4d6bbe3d264f1e4e2",
"038c3f6388afb7695dd4d6bbe3d264f1e4e2",
}
output, err := cleanSerials(input)
if err != nil {
t.Errorf("cleanSerials(%s): %s, want %s", input, err, expected)
}
if !reflect.DeepEqual(output, expected) {
t.Errorf("cleanSerials(%s)=%s, want %s", input, output, expected)
}
}

View file

@ -32,10 +32,6 @@ type dryRunSAC struct {
}
func (d dryRunSAC) AddBlockedKey(_ context.Context, req *sapb.AddBlockedKeyRequest, _ ...grpc.CallOption) (*emptypb.Empty, error) {
b, err := prototext.Marshal(req)
if err != nil {
return nil, err
}
d.log.Infof("dry-run: %#v", string(b))
d.log.Infof("dry-run: Block SPKI hash %x by %s %s", req.KeyHash, req.Comment, req.Source)
return &emptypb.Empty{}, nil
}

View file

@ -1,84 +0,0 @@
package main
import (
"context"
"errors"
"flag"
"fmt"
"github.com/letsencrypt/boulder/sa"
)
// subcommandUpdateEmail encapsulates the "admin update-email" command.
//
// Note that this command may be very slow, as the initial query to find the set
// of accounts which have a matching contact email address does not use a
// database index. Therefore, when updating the found accounts, it does not exit
// on failure, preferring to continue and make as much progress as possible.
type subcommandUpdateEmail struct {
address string
clear bool
}
var _ subcommand = (*subcommandUpdateEmail)(nil)
func (s *subcommandUpdateEmail) Desc() string {
return "Change or remove an email address across all accounts"
}
func (s *subcommandUpdateEmail) Flags(flag *flag.FlagSet) {
flag.StringVar(&s.address, "address", "", "Email address to update")
flag.BoolVar(&s.clear, "clear", false, "If set, remove the address")
}
func (s *subcommandUpdateEmail) Run(ctx context.Context, a *admin) error {
if s.address == "" {
return errors.New("the -address flag is required")
}
if s.clear {
return a.clearEmail(ctx, s.address)
}
return errors.New("no action to perform on the given email was specified")
}
func (a *admin) clearEmail(ctx context.Context, address string) error {
a.log.AuditInfof("Scanning database for accounts with email addresses matching %q in order to clear the email addresses.", address)
// We use SQL `CONCAT` rather than interpolating with `+` or `%s` because we want to
// use a `?` placeholder for the email, which prevents SQL injection.
// Since this uses a substring match, it is important
// to subsequently parse the JSON list of addresses and look for exact matches.
// Because this does not use an index, it is very slow.
var regIDs []int64
_, err := a.dbMap.Select(ctx, &regIDs, "SELECT id FROM registrations WHERE contact LIKE CONCAT('%\"mailto:', ?, '\"%')", address)
if err != nil {
return fmt.Errorf("identifying matching accounts: %w", err)
}
a.log.Infof("Found %d registration IDs matching email %q.", len(regIDs), address)
failures := 0
for _, regID := range regIDs {
if a.dryRun {
a.log.Infof("dry-run: remove %q from account %d", address, regID)
continue
}
err := sa.ClearEmail(ctx, a.dbMap, regID, address)
if err != nil {
// Log, but don't fail, because it took a long time to find the relevant registration IDs
// and we don't want to have to redo that work.
a.log.AuditErrf("failed to clear email %q for registration ID %d: %s", address, regID, err)
failures++
} else {
a.log.AuditInfof("cleared email %q for registration ID %d", address, regID)
}
}
if failures > 0 {
return fmt.Errorf("failed to clear email for %d out of %d registration IDs", failures, len(regIDs))
}
return nil
}

View file

@ -3,7 +3,9 @@ package main
import (
"bufio"
"context"
"crypto/x509"
"encoding/hex"
"encoding/pem"
"errors"
"flag"
"fmt"
@ -13,7 +15,6 @@ import (
"sync"
"sync/atomic"
"golang.org/x/exp/maps"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/letsencrypt/boulder/core"
@ -26,9 +27,14 @@ import (
type subcommandBlockKey struct {
parallelism uint
comment string
privKey string
spkiFile string
certFile string
privKey string
spkiFile string
certFile string
csrFile string
csrFileExpectedCN string
checkSignature bool
}
var _ subcommand = (*subcommandBlockKey)(nil)
@ -46,6 +52,10 @@ func (s *subcommandBlockKey) Flags(flag *flag.FlagSet) {
flag.StringVar(&s.privKey, "private-key", "", "Block issuance for the pubkey corresponding to this private key")
flag.StringVar(&s.spkiFile, "spki-file", "", "Block issuance for all keys listed in this file as SHA256 hashes of SPKI, hex encoded, one per line")
flag.StringVar(&s.certFile, "cert-file", "", "Block issuance for the public key of the single PEM-formatted certificate in this file")
flag.StringVar(&s.csrFile, "csr-file", "", "Block issuance for the public key of the single PEM-formatted CSR in this file")
flag.StringVar(&s.csrFileExpectedCN, "csr-file-expected-cn", "The key that signed this CSR has been publicly disclosed. It should not be used for any purpose.", "The Subject CN of a CSR will be verified to match this before blocking")
flag.BoolVar(&s.checkSignature, "check-signature", true, "Check self-signature of CSR before revoking")
}
func (s *subcommandBlockKey) Run(ctx context.Context, a *admin) error {
@ -56,17 +66,15 @@ func (s *subcommandBlockKey) Run(ctx context.Context, a *admin) error {
"-private-key": s.privKey != "",
"-spki-file": s.spkiFile != "",
"-cert-file": s.certFile != "",
"-csr-file": s.csrFile != "",
}
maps.DeleteFunc(setInputs, func(_ string, v bool) bool { return !v })
if len(setInputs) == 0 {
return errors.New("at least one input method flag must be specified")
} else if len(setInputs) > 1 {
return fmt.Errorf("more than one input method flag specified: %v", maps.Keys(setInputs))
activeFlag, err := findActiveInputMethodFlag(setInputs)
if err != nil {
return err
}
var spkiHashes [][]byte
var err error
switch maps.Keys(setInputs)[0] {
switch activeFlag {
case "-private-key":
var spkiHash []byte
spkiHash, err = a.spkiHashFromPrivateKey(s.privKey)
@ -75,6 +83,8 @@ func (s *subcommandBlockKey) Run(ctx context.Context, a *admin) error {
spkiHashes, err = a.spkiHashesFromFile(s.spkiFile)
case "-cert-file":
spkiHashes, err = a.spkiHashesFromCertPEM(s.certFile)
case "-csr-file":
spkiHashes, err = a.spkiHashFromCSRPEM(s.csrFile, s.checkSignature, s.csrFileExpectedCN)
default:
return errors.New("no recognized input method flag set (this shouldn't happen)")
}
@ -146,6 +156,43 @@ func (a *admin) spkiHashesFromCertPEM(filename string) ([][]byte, error) {
return [][]byte{spkiHash[:]}, nil
}
func (a *admin) spkiHashFromCSRPEM(filename string, checkSignature bool, expectedCN string) ([][]byte, error) {
csrFile, err := os.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("reading CSR file %q: %w", filename, err)
}
data, _ := pem.Decode(csrFile)
if data == nil {
return nil, fmt.Errorf("no PEM data found in %q", filename)
}
a.log.AuditInfof("Parsing key to block from CSR PEM: %x", data)
csr, err := x509.ParseCertificateRequest(data.Bytes)
if err != nil {
return nil, fmt.Errorf("parsing CSR %q: %w", filename, err)
}
if checkSignature {
err = csr.CheckSignature()
if err != nil {
return nil, fmt.Errorf("checking CSR signature: %w", err)
}
}
if csr.Subject.CommonName != expectedCN {
return nil, fmt.Errorf("Got CSR CommonName %q, expected %q", csr.Subject.CommonName, expectedCN)
}
spkiHash, err := core.KeyDigest(csr.PublicKey)
if err != nil {
return nil, fmt.Errorf("computing SPKI hash: %w", err)
}
return [][]byte{spkiHash[:]}, nil
}
func (a *admin) blockSPKIHashes(ctx context.Context, spkiHashes [][]byte, comment string, parallelism uint) error {
u, err := user.Current()
if err != nil {

View file

@ -68,6 +68,53 @@ func TestSPKIHashesFromFile(t *testing.T) {
}
}
// The key is the p256 test key from RFC9500
const goodCSR = `
-----BEGIN CERTIFICATE REQUEST-----
MIG6MGICAQAwADBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABEIlSPiPt4L/teyj
dERSxyoeVY+9b3O+XkjpMjLMRcWxbEzRDEy41bihcTnpSILImSVymTQl9BQZq36Q
pCpJQnKgADAKBggqhkjOPQQDAgNIADBFAiBadw3gvL9IjUfASUTa7MvmkbC4ZCvl
21m1KMwkIx/+CQIhAKvuyfCcdZ0cWJYOXCOb1OavolWHIUzgEpNGUWul6O0s
-----END CERTIFICATE REQUEST-----
`
// TestCSR checks that we get the correct SPKI from a CSR, even if its signature is invalid
func TestCSR(t *testing.T) {
expectedSPKIHash := "b2b04340cfaee616ec9c2c62d261b208e54bb197498df52e8cadede23ac0ba5e"
goodCSRFile := path.Join(t.TempDir(), "good.csr")
err := os.WriteFile(goodCSRFile, []byte(goodCSR), 0600)
test.AssertNotError(t, err, "writing good csr")
a := admin{log: blog.NewMock()}
goodHash, err := a.spkiHashFromCSRPEM(goodCSRFile, true, "")
test.AssertNotError(t, err, "expected to read CSR")
if len(goodHash) != 1 {
t.Fatalf("expected to read 1 SPKI from CSR, read %d", len(goodHash))
}
test.AssertEquals(t, hex.EncodeToString(goodHash[0]), expectedSPKIHash)
// Flip a bit, in the signature, to make a bad CSR:
badCSR := strings.Replace(goodCSR, "Wul6", "Wul7", 1)
csrFile := path.Join(t.TempDir(), "bad.csr")
err = os.WriteFile(csrFile, []byte(badCSR), 0600)
test.AssertNotError(t, err, "writing bad csr")
_, err = a.spkiHashFromCSRPEM(csrFile, true, "")
test.AssertError(t, err, "expected invalid signature")
badHash, err := a.spkiHashFromCSRPEM(csrFile, false, "")
test.AssertNotError(t, err, "expected to read CSR with bad signature")
if len(badHash) != 1 {
t.Fatalf("expected to read 1 SPKI from CSR, read %d", len(badHash))
}
test.AssertEquals(t, hex.EncodeToString(badHash[0]), expectedSPKIHash)
}
// mockSARecordingBlocks is a mock which only implements the AddBlockedKey gRPC
// method.
type mockSARecordingBlocks struct {
@ -131,6 +178,6 @@ func TestBlockSPKIHash(t *testing.T) {
err = a.blockSPKIHash(context.Background(), keyHash[:], u, "")
test.AssertNotError(t, err, "")
test.AssertEquals(t, len(log.GetAllMatching("Found 0 unexpired certificates")), 1)
test.AssertEquals(t, len(log.GetAllMatching("dry-run:")), 1)
test.AssertEquals(t, len(log.GetAllMatching("dry-run: Block SPKI hash "+hex.EncodeToString(keyHash[:]))), 1)
test.AssertEquals(t, len(msa.blockRequests), 0)
}

View file

@ -31,8 +31,6 @@ type Config struct {
RAService *cmd.GRPCClientConfig
SAService *cmd.GRPCClientConfig
DebugAddr string
Features features.Config
}
@ -70,9 +68,10 @@ func main() {
// This is the registry of all subcommands that the admin tool can run.
subcommands := map[string]subcommand{
"revoke-cert": &subcommandRevokeCert{},
"block-key": &subcommandBlockKey{},
"update-email": &subcommandUpdateEmail{},
"revoke-cert": &subcommandRevokeCert{},
"block-key": &subcommandBlockKey{},
"pause-identifier": &subcommandPauseIdentifier{},
"unpause-account": &subcommandUnpauseAccount{},
}
defaultUsage := flag.Usage

View file

@ -0,0 +1,194 @@
package main
import (
"context"
"encoding/csv"
"errors"
"flag"
"fmt"
"io"
"os"
"strconv"
"sync"
"sync/atomic"
corepb "github.com/letsencrypt/boulder/core/proto"
"github.com/letsencrypt/boulder/identifier"
sapb "github.com/letsencrypt/boulder/sa/proto"
)
// subcommandPauseIdentifier encapsulates the "admin pause-identifiers" command.
type subcommandPauseIdentifier struct {
batchFile string
parallelism uint
}
var _ subcommand = (*subcommandPauseIdentifier)(nil)
func (p *subcommandPauseIdentifier) Desc() string {
return "Administratively pause an account preventing it from attempting certificate issuance"
}
func (p *subcommandPauseIdentifier) Flags(flag *flag.FlagSet) {
flag.StringVar(&p.batchFile, "batch-file", "", "Path to a CSV file containing (account ID, identifier type, identifier value)")
flag.UintVar(&p.parallelism, "parallelism", 10, "The maximum number of concurrent pause requests to send to the SA (default: 10)")
}
func (p *subcommandPauseIdentifier) Run(ctx context.Context, a *admin) error {
if p.batchFile == "" {
return errors.New("the -batch-file flag is required")
}
idents, err := a.readPausedAccountFile(p.batchFile)
if err != nil {
return err
}
_, err = a.pauseIdentifiers(ctx, idents, p.parallelism)
if err != nil {
return err
}
return nil
}
// pauseIdentifiers concurrently pauses identifiers for each account using up to
// `parallelism` workers. It returns all pause responses and any accumulated
// errors.
func (a *admin) pauseIdentifiers(ctx context.Context, entries []pauseCSVData, parallelism uint) ([]*sapb.PauseIdentifiersResponse, error) {
if len(entries) <= 0 {
return nil, errors.New("cannot pause identifiers because no pauseData was sent")
}
accountToIdents := make(map[int64][]*corepb.Identifier)
for _, entry := range entries {
accountToIdents[entry.accountID] = append(accountToIdents[entry.accountID], &corepb.Identifier{
Type: string(entry.identifierType),
Value: entry.identifierValue,
})
}
var errCount atomic.Uint64
respChan := make(chan *sapb.PauseIdentifiersResponse, len(accountToIdents))
work := make(chan struct {
accountID int64
idents []*corepb.Identifier
}, parallelism)
var wg sync.WaitGroup
for i := uint(0); i < parallelism; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for data := range work {
response, err := a.sac.PauseIdentifiers(ctx, &sapb.PauseRequest{
RegistrationID: data.accountID,
Identifiers: data.idents,
})
if err != nil {
errCount.Add(1)
a.log.Errf("error pausing identifier(s) %q for account %d: %v", data.idents, data.accountID, err)
} else {
respChan <- response
}
}
}()
}
for accountID, idents := range accountToIdents {
work <- struct {
accountID int64
idents []*corepb.Identifier
}{accountID, idents}
}
close(work)
wg.Wait()
close(respChan)
var responses []*sapb.PauseIdentifiersResponse
for response := range respChan {
responses = append(responses, response)
}
if errCount.Load() > 0 {
return responses, fmt.Errorf("encountered %d errors while pausing identifiers; see logs above for details", errCount.Load())
}
return responses, nil
}
// pauseCSVData contains a golang representation of the data loaded in from a
// CSV file for pausing.
type pauseCSVData struct {
accountID int64
identifierType identifier.IdentifierType
identifierValue string
}
// readPausedAccountFile parses the contents of a CSV into a slice of
// `pauseCSVData` objects and returns it or an error. It will skip malformed
// lines and continue processing until either the end of file marker is detected
// or other read error.
func (a *admin) readPausedAccountFile(filePath string) ([]pauseCSVData, error) {
fp, err := os.Open(filePath)
if err != nil {
return nil, fmt.Errorf("opening paused account data file: %w", err)
}
defer fp.Close()
reader := csv.NewReader(fp)
// identifierValue can have 1 or more entries
reader.FieldsPerRecord = -1
reader.TrimLeadingSpace = true
var parsedRecords []pauseCSVData
lineCounter := 0
// Process contents of the CSV file
for {
record, err := reader.Read()
if errors.Is(err, io.EOF) {
break
} else if err != nil {
return nil, err
}
lineCounter++
// We should have strictly 3 fields, note that just commas is considered
// a valid CSV line.
if len(record) != 3 {
a.log.Infof("skipping: malformed line %d, should contain exactly 3 fields\n", lineCounter)
continue
}
recordID := record[0]
accountID, err := strconv.ParseInt(recordID, 10, 64)
if err != nil || accountID == 0 {
a.log.Infof("skipping: malformed accountID entry on line %d\n", lineCounter)
continue
}
// Ensure that an identifier type is present, otherwise skip the line.
if len(record[1]) == 0 {
a.log.Infof("skipping: malformed identifierType entry on line %d\n", lineCounter)
continue
}
if len(record[2]) == 0 {
a.log.Infof("skipping: malformed identifierValue entry on line %d\n", lineCounter)
continue
}
parsedRecord := pauseCSVData{
accountID: accountID,
identifierType: identifier.IdentifierType(record[1]),
identifierValue: record[2],
}
parsedRecords = append(parsedRecords, parsedRecord)
}
a.log.Infof("detected %d valid record(s) from input file\n", len(parsedRecords))
return parsedRecords, nil
}

View file

@ -0,0 +1,195 @@
package main
import (
"context"
"errors"
"os"
"path"
"strings"
"testing"
blog "github.com/letsencrypt/boulder/log"
sapb "github.com/letsencrypt/boulder/sa/proto"
"github.com/letsencrypt/boulder/test"
"google.golang.org/grpc"
)
func TestReadingPauseCSV(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
data []string
expectedRecords int
}{
{
name: "No data in file",
data: nil,
},
{
name: "valid",
data: []string{"1,dns,example.com"},
expectedRecords: 1,
},
{
name: "valid with duplicates",
data: []string{"1,dns,example.com", "2,dns,example.org", "1,dns,example.com", "1,dns,example.net", "3,dns,example.gov", "3,dns,example.gov"},
expectedRecords: 6,
},
{
name: "invalid with multiple domains on the same line",
data: []string{"1,dns,example.com,example.net"},
},
{
name: "invalid just commas",
data: []string{",,,"},
},
{
name: "invalid only contains accountID",
data: []string{"1"},
},
{
name: "invalid only contains accountID and identifierType",
data: []string{"1,dns"},
},
{
name: "invalid missing identifierType",
data: []string{"1,,example.com"},
},
{
name: "invalid accountID isnt an int",
data: []string{"blorple"},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
t.Parallel()
log := blog.NewMock()
a := admin{log: log}
csvFile := path.Join(t.TempDir(), path.Base(t.Name()+".csv"))
err := os.WriteFile(csvFile, []byte(strings.Join(testCase.data, "\n")), os.ModePerm)
test.AssertNotError(t, err, "could not write temporary file")
parsedData, err := a.readPausedAccountFile(csvFile)
test.AssertNotError(t, err, "no error expected, but received one")
test.AssertEquals(t, len(parsedData), testCase.expectedRecords)
})
}
}
// mockSAPaused is a mock which always succeeds. It records the PauseRequest it
// received, and returns the number of identifiers as a
// PauseIdentifiersResponse. It does not maintain state of repaused identifiers.
type mockSAPaused struct {
sapb.StorageAuthorityClient
}
func (msa *mockSAPaused) PauseIdentifiers(ctx context.Context, in *sapb.PauseRequest, _ ...grpc.CallOption) (*sapb.PauseIdentifiersResponse, error) {
return &sapb.PauseIdentifiersResponse{Paused: int64(len(in.Identifiers))}, nil
}
// mockSAPausedBroken is a mock which always errors.
type mockSAPausedBroken struct {
sapb.StorageAuthorityClient
}
func (msa *mockSAPausedBroken) PauseIdentifiers(ctx context.Context, in *sapb.PauseRequest, _ ...grpc.CallOption) (*sapb.PauseIdentifiersResponse, error) {
return nil, errors.New("its all jacked up")
}
func TestPauseIdentifiers(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
data []pauseCSVData
saImpl sapb.StorageAuthorityClient
expectRespLen int
expectErr bool
}{
{
name: "no data",
data: nil,
expectErr: true,
},
{
name: "valid single entry",
data: []pauseCSVData{
{
accountID: 1,
identifierType: "dns",
identifierValue: "example.com",
},
},
expectRespLen: 1,
},
{
name: "valid single entry but broken SA",
expectErr: true,
saImpl: &mockSAPausedBroken{},
data: []pauseCSVData{
{
accountID: 1,
identifierType: "dns",
identifierValue: "example.com",
},
},
},
{
name: "valid multiple entries with duplicates",
data: []pauseCSVData{
{
accountID: 1,
identifierType: "dns",
identifierValue: "example.com",
},
{
accountID: 1,
identifierType: "dns",
identifierValue: "example.com",
},
{
accountID: 2,
identifierType: "dns",
identifierValue: "example.org",
},
{
accountID: 3,
identifierType: "dns",
identifierValue: "example.net",
},
{
accountID: 3,
identifierType: "dns",
identifierValue: "example.org",
},
},
expectRespLen: 3,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
t.Parallel()
log := blog.NewMock()
// Default to a working mock SA implementation
if testCase.saImpl == nil {
testCase.saImpl = &mockSAPaused{}
}
a := admin{sac: testCase.saImpl, log: log}
responses, err := a.pauseIdentifiers(context.Background(), testCase.data, 10)
if testCase.expectErr {
test.AssertError(t, err, "should have errored, but did not")
} else {
test.AssertNotError(t, err, "should not have errored")
// Batching will consolidate identifiers under the same account.
test.AssertEquals(t, len(responses), testCase.expectRespLen)
}
})
}
}

View file

@ -0,0 +1,168 @@
package main
import (
"bufio"
"context"
"errors"
"flag"
"fmt"
"os"
"slices"
"strconv"
"sync"
"sync/atomic"
sapb "github.com/letsencrypt/boulder/sa/proto"
"github.com/letsencrypt/boulder/unpause"
)
// subcommandUnpauseAccount encapsulates the "admin unpause-account" command.
type subcommandUnpauseAccount struct {
accountID int64
batchFile string
parallelism uint
}
var _ subcommand = (*subcommandUnpauseAccount)(nil)
func (u *subcommandUnpauseAccount) Desc() string {
return "Administratively unpause an account to allow certificate issuance attempts"
}
func (u *subcommandUnpauseAccount) Flags(flag *flag.FlagSet) {
flag.Int64Var(&u.accountID, "account", 0, "A single account ID to unpause")
flag.StringVar(&u.batchFile, "batch-file", "", "Path to a file containing multiple account IDs where each is separated by a newline")
flag.UintVar(&u.parallelism, "parallelism", 10, "The maximum number of concurrent unpause requests to send to the SA (default: 10)")
}
func (u *subcommandUnpauseAccount) Run(ctx context.Context, a *admin) error {
// This is a map of all input-selection flags to whether or not they were set
// to a non-default value. We use this to ensure that exactly one input
// selection flag was given on the command line.
setInputs := map[string]bool{
"-account": u.accountID != 0,
"-batch-file": u.batchFile != "",
}
activeFlag, err := findActiveInputMethodFlag(setInputs)
if err != nil {
return err
}
var regIDs []int64
switch activeFlag {
case "-account":
regIDs = []int64{u.accountID}
case "-batch-file":
regIDs, err = a.readUnpauseAccountFile(u.batchFile)
default:
return errors.New("no recognized input method flag set (this shouldn't happen)")
}
if err != nil {
return fmt.Errorf("collecting serials to revoke: %w", err)
}
_, err = a.unpauseAccounts(ctx, regIDs, u.parallelism)
if err != nil {
return err
}
return nil
}
type unpauseCount struct {
accountID int64
count int64
}
// unpauseAccount concurrently unpauses all identifiers for each account using
// up to `parallelism` workers. It returns a count of the number of identifiers
// unpaused for each account and any accumulated errors.
func (a *admin) unpauseAccounts(ctx context.Context, accountIDs []int64, parallelism uint) ([]unpauseCount, error) {
if len(accountIDs) <= 0 {
return nil, errors.New("no account IDs provided for unpausing")
}
slices.Sort(accountIDs)
accountIDs = slices.Compact(accountIDs)
countChan := make(chan unpauseCount, len(accountIDs))
work := make(chan int64)
var wg sync.WaitGroup
var errCount atomic.Uint64
for i := uint(0); i < parallelism; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for accountID := range work {
totalCount := int64(0)
for {
response, err := a.sac.UnpauseAccount(ctx, &sapb.RegistrationID{Id: accountID})
if err != nil {
errCount.Add(1)
a.log.Errf("error unpausing accountID %d: %v", accountID, err)
break
}
totalCount += response.Count
if response.Count < unpause.RequestLimit {
// All identifiers have been unpaused.
break
}
}
countChan <- unpauseCount{accountID: accountID, count: totalCount}
}
}()
}
go func() {
for _, accountID := range accountIDs {
work <- accountID
}
close(work)
}()
go func() {
wg.Wait()
close(countChan)
}()
var unpauseCounts []unpauseCount
for count := range countChan {
unpauseCounts = append(unpauseCounts, count)
}
if errCount.Load() > 0 {
return unpauseCounts, fmt.Errorf("encountered %d errors while unpausing; see logs above for details", errCount.Load())
}
return unpauseCounts, nil
}
// readUnpauseAccountFile parses the contents of a file containing one account
// ID per into a slice of int64s. It will skip malformed records and continue
// processing until the end of file marker.
func (a *admin) readUnpauseAccountFile(filePath string) ([]int64, error) {
fp, err := os.Open(filePath)
if err != nil {
return nil, fmt.Errorf("opening paused account data file: %w", err)
}
defer fp.Close()
var unpauseAccounts []int64
lineCounter := 0
scanner := bufio.NewScanner(fp)
for scanner.Scan() {
lineCounter++
regID, err := strconv.ParseInt(scanner.Text(), 10, 64)
if err != nil {
a.log.Infof("skipping: malformed account ID entry on line %d\n", lineCounter)
continue
}
unpauseAccounts = append(unpauseAccounts, regID)
}
if err := scanner.Err(); err != nil {
return nil, scanner.Err()
}
return unpauseAccounts, nil
}

View file

@ -0,0 +1,134 @@
package main
import (
"context"
"errors"
"os"
"path"
"strings"
"testing"
blog "github.com/letsencrypt/boulder/log"
sapb "github.com/letsencrypt/boulder/sa/proto"
"github.com/letsencrypt/boulder/test"
"google.golang.org/grpc"
)
func TestReadingUnpauseAccountsFile(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
data []string
expectedRegIDs int
}{
{
name: "No data in file",
data: nil,
},
{
name: "valid",
data: []string{"1"},
expectedRegIDs: 1,
},
{
name: "valid with duplicates",
data: []string{"1", "2", "1", "3", "3"},
expectedRegIDs: 5,
},
{
name: "valid with empty lines and duplicates",
data: []string{"1", "\n", "6", "6", "6"},
expectedRegIDs: 4,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
t.Parallel()
log := blog.NewMock()
a := admin{log: log}
file := path.Join(t.TempDir(), path.Base(t.Name()+".txt"))
err := os.WriteFile(file, []byte(strings.Join(testCase.data, "\n")), os.ModePerm)
test.AssertNotError(t, err, "could not write temporary file")
regIDs, err := a.readUnpauseAccountFile(file)
test.AssertNotError(t, err, "no error expected, but received one")
test.AssertEquals(t, len(regIDs), testCase.expectedRegIDs)
})
}
}
type mockSAUnpause struct {
sapb.StorageAuthorityClient
}
func (msa *mockSAUnpause) UnpauseAccount(ctx context.Context, in *sapb.RegistrationID, _ ...grpc.CallOption) (*sapb.Count, error) {
return &sapb.Count{Count: 1}, nil
}
// mockSAUnpauseBroken is a mock that always returns an error.
type mockSAUnpauseBroken struct {
sapb.StorageAuthorityClient
}
func (msa *mockSAUnpauseBroken) UnpauseAccount(ctx context.Context, in *sapb.RegistrationID, _ ...grpc.CallOption) (*sapb.Count, error) {
return nil, errors.New("oh dear")
}
func TestUnpauseAccounts(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
regIDs []int64
saImpl sapb.StorageAuthorityClient
expectErr bool
expectCounts int
}{
{
name: "no data",
regIDs: nil,
expectErr: true,
},
{
name: "valid single entry",
regIDs: []int64{1},
expectCounts: 1,
},
{
name: "valid single entry but broken SA",
expectErr: true,
saImpl: &mockSAUnpauseBroken{},
regIDs: []int64{1},
},
{
name: "valid multiple entries with duplicates",
regIDs: []int64{1, 1, 2, 3, 4},
expectCounts: 4,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
t.Parallel()
log := blog.NewMock()
// Default to a working mock SA implementation
if testCase.saImpl == nil {
testCase.saImpl = &mockSAUnpause{}
}
a := admin{sac: testCase.saImpl, log: log}
counts, err := a.unpauseAccounts(context.Background(), testCase.regIDs, 10)
if testCase.expectErr {
test.AssertError(t, err, "should have errored, but did not")
} else {
test.AssertNotError(t, err, "should not have errored")
test.AssertEquals(t, testCase.expectCounts, len(counts))
}
})
}
}

View file

@ -1,15 +1,10 @@
package notmain
import (
"bytes"
"context"
"crypto/x509"
"flag"
"fmt"
"html/template"
netmail "net/mail"
"os"
"strings"
"time"
"github.com/jmhodges/clock"
@ -24,7 +19,6 @@ import (
"github.com/letsencrypt/boulder/db"
bgrpc "github.com/letsencrypt/boulder/grpc"
blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/mail"
rapb "github.com/letsencrypt/boulder/ra/proto"
"github.com/letsencrypt/boulder/sa"
)
@ -43,10 +37,6 @@ var certsRevoked = prometheus.NewCounter(prometheus.CounterOpts{
Name: "bad_keys_certs_revoked",
Help: "A counter of certificates associated with rows in blockedKeys that have been revoked",
})
var mailErrors = prometheus.NewCounter(prometheus.CounterOpts{
Name: "bad_keys_mail_errors",
Help: "A counter of email send errors",
})
// revoker is an interface used to reduce the scope of a RA gRPC client
// to only the single method we need to use, this makes testing significantly
@ -60,9 +50,6 @@ type badKeyRevoker struct {
maxRevocations int
serialBatchSize int
raClient revoker
mailer mail.Mailer
emailSubject string
emailTemplate *template.Template
logger blog.Logger
clk clock.Clock
backoffIntervalBase time.Duration
@ -141,7 +128,7 @@ func (bkr *badKeyRevoker) findUnrevoked(ctx context.Context, unchecked unchecked
"SELECT id, certSerial FROM keyHashToSerial WHERE keyHash = ? AND id > ? AND certNotAfter > ? ORDER BY id LIMIT ?",
unchecked.KeyHash,
initialID,
bkr.clk.Now().Truncate(time.Second),
bkr.clk.Now(),
bkr.serialBatchSize,
)
if err != nil {
@ -190,109 +177,27 @@ func (bkr *badKeyRevoker) markRowChecked(ctx context.Context, unchecked unchecke
return err
}
// resolveContacts builds a map of id -> email addresses
func (bkr *badKeyRevoker) resolveContacts(ctx context.Context, ids []int64) (map[int64][]string, error) {
idToEmail := map[int64][]string{}
for _, id := range ids {
var emails struct {
Contact []string
}
err := bkr.dbMap.SelectOne(ctx, &emails, "SELECT contact FROM registrations WHERE id = ?", id)
// revokeCerts revokes all the provided certificates. It uses reason
// keyCompromise and includes note indicating that they were revoked by
// bad-key-revoker.
func (bkr *badKeyRevoker) revokeCerts(certs []unrevokedCertificate) error {
for _, cert := range certs {
_, err := bkr.raClient.AdministrativelyRevokeCertificate(context.Background(), &rapb.AdministrativelyRevokeCertificateRequest{
Cert: cert.DER,
Serial: cert.Serial,
Code: int64(ocsp.KeyCompromise),
AdminName: "bad-key-revoker",
})
if err != nil {
// ErrNoRows is not acceptable here since there should always be a
// row for the registration, even if there are no contacts
return nil, err
return err
}
if len(emails.Contact) != 0 {
for _, email := range emails.Contact {
idToEmail[id] = append(idToEmail[id], strings.TrimPrefix(email, "mailto:"))
}
} else {
// if the account has no contacts add a placeholder empty contact
// so that we don't skip any certificates
idToEmail[id] = append(idToEmail[id], "")
continue
}
}
return idToEmail, nil
}
var maxSerials = 100
// sendMessage sends a single email to the provided address with the revoked
// serials
func (bkr *badKeyRevoker) sendMessage(addr string, serials []string) error {
conn, err := bkr.mailer.Connect()
if err != nil {
return err
}
defer func() {
_ = conn.Close()
}()
mutSerials := make([]string, len(serials))
copy(mutSerials, serials)
if len(mutSerials) > maxSerials {
more := len(mutSerials) - maxSerials
mutSerials = mutSerials[:maxSerials]
mutSerials = append(mutSerials, fmt.Sprintf("and %d more certificates.", more))
}
message := bytes.NewBuffer(nil)
err = bkr.emailTemplate.Execute(message, mutSerials)
if err != nil {
return err
}
err = conn.SendMail([]string{addr}, bkr.emailSubject, message.String())
if err != nil {
return err
certsRevoked.Inc()
}
return nil
}
// revokeCerts revokes all the certificates associated with a particular key hash and sends
// emails to the users that issued the certificates. Emails are not sent to the user which
// requested revocation of the original certificate which marked the key as compromised.
func (bkr *badKeyRevoker) revokeCerts(revokerEmails []string, emailToCerts map[string][]unrevokedCertificate) error {
revokerEmailsMap := map[string]bool{}
for _, email := range revokerEmails {
revokerEmailsMap[email] = true
}
alreadyRevoked := map[int]bool{}
for email, certs := range emailToCerts {
var revokedSerials []string
for _, cert := range certs {
revokedSerials = append(revokedSerials, cert.Serial)
if alreadyRevoked[cert.ID] {
continue
}
_, err := bkr.raClient.AdministrativelyRevokeCertificate(context.Background(), &rapb.AdministrativelyRevokeCertificateRequest{
Cert: cert.DER,
Serial: cert.Serial,
Code: int64(ocsp.KeyCompromise),
AdminName: "bad-key-revoker",
})
if err != nil {
return err
}
certsRevoked.Inc()
alreadyRevoked[cert.ID] = true
}
// don't send emails to the person who revoked the certificate
if revokerEmailsMap[email] || email == "" {
continue
}
err := bkr.sendMessage(email, revokedSerials)
if err != nil {
mailErrors.Inc()
bkr.logger.Errf("failed to send message to %q: %s", email, err)
continue
}
}
return nil
}
// invoke processes a single key in the blockedKeys table and returns whether
// there were any rows to process or not.
// invoke exits early and returns true if there is no work to be done.
// Otherwise, it processes a single key in the blockedKeys table and returns false.
func (bkr *badKeyRevoker) invoke(ctx context.Context) (bool, error) {
// Gather a count of rows to be processed.
uncheckedCount, err := bkr.countUncheckedKeys(ctx)
@ -337,45 +242,14 @@ func (bkr *badKeyRevoker) invoke(ctx context.Context) (bool, error) {
return false, nil
}
// build a map of registration ID -> certificates, and collect a
// list of unique registration IDs
ownedBy := map[int64][]unrevokedCertificate{}
var ids []int64
var serials []string
for _, cert := range unrevokedCerts {
if ownedBy[cert.RegistrationID] == nil {
ids = append(ids, cert.RegistrationID)
}
ownedBy[cert.RegistrationID] = append(ownedBy[cert.RegistrationID], cert)
}
// if the account that revoked the original certificate isn't an owner of any
// extant certificates, still add them to ids so that we can resolve their
// email and avoid sending emails later. If RevokedBy == 0 it was a row
// inserted by admin-revoker with a dummy ID, since there won't be a registration
// to look up, don't bother adding it to ids.
if _, present := ownedBy[unchecked.RevokedBy]; !present && unchecked.RevokedBy != 0 {
ids = append(ids, unchecked.RevokedBy)
}
// get contact addresses for the list of IDs
idToEmails, err := bkr.resolveContacts(ctx, ids)
if err != nil {
return false, err
serials = append(serials, cert.Serial)
}
bkr.logger.AuditInfo(fmt.Sprintf("revoking serials %v for key with hash %x", serials, unchecked.KeyHash))
// build a map of email -> certificates, this de-duplicates accounts with
// the same email addresses
emailsToCerts := map[string][]unrevokedCertificate{}
for id, emails := range idToEmails {
for _, email := range emails {
emailsToCerts[email] = append(emailsToCerts[email], ownedBy[id]...)
}
}
revokerEmails := idToEmails[unchecked.RevokedBy]
bkr.logger.AuditInfo(fmt.Sprintf("revoking certs. revoked emails=%v, emailsToCerts=%s",
revokerEmails, emailsToCerts))
// revoke each certificate and send emails to their owners
err = bkr.revokeCerts(idToEmails[unchecked.RevokedBy], emailsToCerts)
// revoke each certificate
err = bkr.revokeCerts(unrevokedCerts)
if err != nil {
return false, err
}
@ -415,15 +289,14 @@ type Config struct {
// or no work to do.
BackoffIntervalMax config.Duration `validate:"-"`
// Deprecated: the bad-key-revoker no longer sends emails; we use ARI.
// TODO(#8199): Remove this config stanza entirely.
Mailer struct {
cmd.SMTPConfig
// Path to a file containing a list of trusted root certificates for use
// during the SMTP connection (as opposed to the gRPC connections).
cmd.SMTPConfig `validate:"-"`
SMTPTrustedRootFile string
From string `validate:"required"`
EmailSubject string `validate:"required"`
EmailTemplate string `validate:"required"`
From string
EmailSubject string
EmailTemplate string
}
}
@ -455,7 +328,6 @@ func main() {
scope.MustRegister(keysProcessed)
scope.MustRegister(certsRevoked)
scope.MustRegister(mailErrors)
dbMap, err := sa.InitWrappedDb(config.BadKeyRevoker.DB, scope, logger)
cmd.FailOnError(err, "While initializing dbMap")
@ -467,50 +339,11 @@ func main() {
cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to RA")
rac := rapb.NewRegistrationAuthorityClient(conn)
var smtpRoots *x509.CertPool
if config.BadKeyRevoker.Mailer.SMTPTrustedRootFile != "" {
pem, err := os.ReadFile(config.BadKeyRevoker.Mailer.SMTPTrustedRootFile)
cmd.FailOnError(err, "Loading trusted roots file")
smtpRoots = x509.NewCertPool()
if !smtpRoots.AppendCertsFromPEM(pem) {
cmd.FailOnError(nil, "Failed to parse root certs PEM")
}
}
fromAddress, err := netmail.ParseAddress(config.BadKeyRevoker.Mailer.From)
cmd.FailOnError(err, fmt.Sprintf("Could not parse from address: %s", config.BadKeyRevoker.Mailer.From))
smtpPassword, err := config.BadKeyRevoker.Mailer.PasswordConfig.Pass()
cmd.FailOnError(err, "Failed to load SMTP password")
mailClient := mail.New(
config.BadKeyRevoker.Mailer.Server,
config.BadKeyRevoker.Mailer.Port,
config.BadKeyRevoker.Mailer.Username,
smtpPassword,
smtpRoots,
*fromAddress,
logger,
scope,
1*time.Second, // reconnection base backoff
5*60*time.Second, // reconnection maximum backoff
)
if config.BadKeyRevoker.Mailer.EmailSubject == "" {
cmd.Fail("BadKeyRevoker.Mailer.EmailSubject must be populated")
}
templateBytes, err := os.ReadFile(config.BadKeyRevoker.Mailer.EmailTemplate)
cmd.FailOnError(err, fmt.Sprintf("failed to read email template %q: %s", config.BadKeyRevoker.Mailer.EmailTemplate, err))
emailTemplate, err := template.New("email").Parse(string(templateBytes))
cmd.FailOnError(err, fmt.Sprintf("failed to parse email template %q: %s", config.BadKeyRevoker.Mailer.EmailTemplate, err))
bkr := &badKeyRevoker{
dbMap: dbMap,
maxRevocations: config.BadKeyRevoker.MaximumRevocations,
serialBatchSize: config.BadKeyRevoker.FindCertificatesBatchSize,
raClient: rac,
mailer: mailClient,
emailSubject: config.BadKeyRevoker.Mailer.EmailSubject,
emailTemplate: emailTemplate,
logger: logger,
clk: clk,
backoffIntervalMax: config.BadKeyRevoker.BackoffIntervalMax.Duration,

View file

@ -4,24 +4,22 @@ import (
"context"
"crypto/rand"
"fmt"
"html/template"
"strings"
"sync"
"testing"
"time"
"github.com/jmhodges/clock"
"github.com/prometheus/client_golang/prometheus"
"google.golang.org/grpc"
"google.golang.org/protobuf/types/known/emptypb"
"github.com/letsencrypt/boulder/core"
"github.com/letsencrypt/boulder/db"
blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/mocks"
rapb "github.com/letsencrypt/boulder/ra/proto"
"github.com/letsencrypt/boulder/sa"
"github.com/letsencrypt/boulder/test"
"github.com/letsencrypt/boulder/test/vars"
"github.com/prometheus/client_golang/prometheus"
"google.golang.org/grpc"
"google.golang.org/protobuf/types/known/emptypb"
)
func randHash(t *testing.T) []byte {
@ -81,27 +79,17 @@ func TestSelectUncheckedRows(t *testing.T) {
test.AssertEquals(t, row.RevokedBy, int64(1))
}
func insertRegistration(t *testing.T, dbMap *db.WrappedMap, fc clock.Clock, addrs ...string) int64 {
func insertRegistration(t *testing.T, dbMap *db.WrappedMap, fc clock.Clock) int64 {
t.Helper()
jwkHash := make([]byte, 32)
_, err := rand.Read(jwkHash)
test.AssertNotError(t, err, "failed to read rand")
contactStr := "[]"
if len(addrs) > 0 {
contacts := []string{}
for _, addr := range addrs {
contacts = append(contacts, fmt.Sprintf(`"mailto:%s"`, addr))
}
contactStr = fmt.Sprintf("[%s]", strings.Join(contacts, ","))
}
res, err := dbMap.ExecContext(
context.Background(),
"INSERT INTO registrations (jwk, jwk_sha256, contact, agreement, initialIP, createdAt, status, LockCol) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
"INSERT INTO registrations (jwk, jwk_sha256, agreement, createdAt, status, LockCol) VALUES (?, ?, ?, ?, ?, ?)",
[]byte{},
fmt.Sprintf("%x", jwkHash),
contactStr,
"yes",
[]byte{},
fc.Now(),
string(core.StatusValid),
0,
@ -245,47 +233,6 @@ func TestFindUnrevoked(t *testing.T) {
test.AssertEquals(t, err.Error(), fmt.Sprintf("too many certificates to revoke associated with %x: got 1, max 0", hashA))
}
func TestResolveContacts(t *testing.T) {
dbMap, err := sa.DBMapForTest(vars.DBConnSAFullPerms)
test.AssertNotError(t, err, "failed setting up db client")
defer test.ResetBoulderTestDatabase(t)()
fc := clock.NewFake()
bkr := &badKeyRevoker{dbMap: dbMap, clk: fc}
regIDA := insertRegistration(t, dbMap, fc)
regIDB := insertRegistration(t, dbMap, fc, "example.com", "example-2.com")
regIDC := insertRegistration(t, dbMap, fc, "example.com")
regIDD := insertRegistration(t, dbMap, fc, "example-2.com")
idToEmail, err := bkr.resolveContacts(context.Background(), []int64{regIDA, regIDB, regIDC, regIDD})
test.AssertNotError(t, err, "resolveContacts failed")
test.AssertDeepEquals(t, idToEmail, map[int64][]string{
regIDA: {""},
regIDB: {"example.com", "example-2.com"},
regIDC: {"example.com"},
regIDD: {"example-2.com"},
})
}
var testTemplate = template.Must(template.New("testing").Parse("{{range .}}{{.}}\n{{end}}"))
func TestSendMessage(t *testing.T) {
mm := &mocks.Mailer{}
fc := clock.NewFake()
bkr := &badKeyRevoker{mailer: mm, emailSubject: "testing", emailTemplate: testTemplate, clk: fc}
maxSerials = 2
err := bkr.sendMessage("example.com", []string{"a", "b", "c"})
test.AssertNotError(t, err, "sendMessages failed")
test.AssertEquals(t, len(mm.Messages), 1)
test.AssertEquals(t, mm.Messages[0].To, "example.com")
test.AssertEquals(t, mm.Messages[0].Subject, bkr.emailSubject)
test.AssertEquals(t, mm.Messages[0].Body, "a\nb\nand 1 more certificates.\n")
}
type mockRevoker struct {
revoked int
mu sync.Mutex
@ -304,20 +251,15 @@ func TestRevokeCerts(t *testing.T) {
defer test.ResetBoulderTestDatabase(t)()
fc := clock.NewFake()
mm := &mocks.Mailer{}
mr := &mockRevoker{}
bkr := &badKeyRevoker{dbMap: dbMap, raClient: mr, mailer: mm, emailSubject: "testing", emailTemplate: testTemplate, clk: fc}
bkr := &badKeyRevoker{dbMap: dbMap, raClient: mr, clk: fc}
err = bkr.revokeCerts([]string{"revoker@example.com", "revoker-b@example.com"}, map[string][]unrevokedCertificate{
"revoker@example.com": {{ID: 0, Serial: "ff"}},
"revoker-b@example.com": {{ID: 0, Serial: "ff"}},
"other@example.com": {{ID: 1, Serial: "ee"}},
err = bkr.revokeCerts([]unrevokedCertificate{
{ID: 0, Serial: "ff"},
{ID: 1, Serial: "ee"},
})
test.AssertNotError(t, err, "revokeCerts failed")
test.AssertEquals(t, len(mm.Messages), 1)
test.AssertEquals(t, mm.Messages[0].To, "other@example.com")
test.AssertEquals(t, mm.Messages[0].Subject, bkr.emailSubject)
test.AssertEquals(t, mm.Messages[0].Body, "ee\n")
test.AssertEquals(t, mr.revoked, 2)
}
func TestCertificateAbsent(t *testing.T) {
@ -330,7 +272,7 @@ func TestCertificateAbsent(t *testing.T) {
fc := clock.NewFake()
// populate DB with all the test data
regIDA := insertRegistration(t, dbMap, fc, "example.com")
regIDA := insertRegistration(t, dbMap, fc)
hashA := randHash(t)
insertBlockedRow(t, dbMap, fc, hashA, regIDA, false)
@ -350,9 +292,6 @@ func TestCertificateAbsent(t *testing.T) {
maxRevocations: 1,
serialBatchSize: 1,
raClient: &mockRevoker{},
mailer: &mocks.Mailer{},
emailSubject: "testing",
emailTemplate: testTemplate,
logger: blog.NewMock(),
clk: fc,
}
@ -369,24 +308,20 @@ func TestInvoke(t *testing.T) {
fc := clock.NewFake()
mm := &mocks.Mailer{}
mr := &mockRevoker{}
bkr := &badKeyRevoker{
dbMap: dbMap,
maxRevocations: 10,
serialBatchSize: 1,
raClient: mr,
mailer: mm,
emailSubject: "testing",
emailTemplate: testTemplate,
logger: blog.NewMock(),
clk: fc,
}
// populate DB with all the test data
regIDA := insertRegistration(t, dbMap, fc, "example.com")
regIDB := insertRegistration(t, dbMap, fc, "example.com")
regIDC := insertRegistration(t, dbMap, fc, "other.example.com", "uno.example.com")
regIDA := insertRegistration(t, dbMap, fc)
regIDB := insertRegistration(t, dbMap, fc)
regIDC := insertRegistration(t, dbMap, fc)
regIDD := insertRegistration(t, dbMap, fc)
hashA := randHash(t)
insertBlockedRow(t, dbMap, fc, hashA, regIDC, false)
@ -399,8 +334,6 @@ func TestInvoke(t *testing.T) {
test.AssertNotError(t, err, "invoke failed")
test.AssertEquals(t, noWork, false)
test.AssertEquals(t, mr.revoked, 4)
test.AssertEquals(t, len(mm.Messages), 1)
test.AssertEquals(t, mm.Messages[0].To, "example.com")
test.AssertMetricWithLabelsEquals(t, keysToProcess, prometheus.Labels{}, 1)
var checked struct {
@ -441,23 +374,19 @@ func TestInvokeRevokerHasNoExtantCerts(t *testing.T) {
fc := clock.NewFake()
mm := &mocks.Mailer{}
mr := &mockRevoker{}
bkr := &badKeyRevoker{dbMap: dbMap,
maxRevocations: 10,
serialBatchSize: 1,
raClient: mr,
mailer: mm,
emailSubject: "testing",
emailTemplate: testTemplate,
logger: blog.NewMock(),
clk: fc,
}
// populate DB with all the test data
regIDA := insertRegistration(t, dbMap, fc, "a@example.com")
regIDB := insertRegistration(t, dbMap, fc, "a@example.com")
regIDC := insertRegistration(t, dbMap, fc, "b@example.com")
regIDA := insertRegistration(t, dbMap, fc)
regIDB := insertRegistration(t, dbMap, fc)
regIDC := insertRegistration(t, dbMap, fc)
hashA := randHash(t)
@ -472,8 +401,6 @@ func TestInvokeRevokerHasNoExtantCerts(t *testing.T) {
test.AssertNotError(t, err, "invoke failed")
test.AssertEquals(t, noWork, false)
test.AssertEquals(t, mr.revoked, 4)
test.AssertEquals(t, len(mm.Messages), 1)
test.AssertEquals(t, mm.Messages[0].To, "b@example.com")
}
func TestBackoffPolicy(t *testing.T) {

View file

@ -3,12 +3,11 @@ package notmain
import (
"context"
"flag"
"fmt"
"os"
"reflect"
"strconv"
"time"
"github.com/zmap/zlint/v3/lint"
"github.com/letsencrypt/boulder/ca"
capb "github.com/letsencrypt/boulder/ca/proto"
"github.com/letsencrypt/boulder/cmd"
@ -19,8 +18,8 @@ import (
"github.com/letsencrypt/boulder/goodkey/sagoodkey"
bgrpc "github.com/letsencrypt/boulder/grpc"
"github.com/letsencrypt/boulder/issuance"
"github.com/letsencrypt/boulder/linter"
"github.com/letsencrypt/boulder/policy"
rapb "github.com/letsencrypt/boulder/ra/proto"
sapb "github.com/letsencrypt/boulder/sa/proto"
)
@ -34,37 +33,39 @@ type Config struct {
SAService *cmd.GRPCClientConfig
SCTService *cmd.GRPCClientConfig
// Issuance contains all information necessary to load and initialize issuers.
Issuance struct {
// The name of the certificate profile to use if one wasn't provided
// by the RA during NewOrder and Finalize requests. Must match a
// configured certificate profile or boulder-ca will fail to start.
//
// Deprecated: set the defaultProfileName in the RA config instead.
DefaultCertificateProfileName string `validate:"omitempty,alphanum,min=1,max=32"`
// TODO(#7414) Remove this deprecated field.
// Deprecated: Use CertProfiles instead. Profile implicitly takes
// the internal Boulder default value of ca.DefaultCertProfileName.
Profile issuance.ProfileConfig `validate:"required_without=CertProfiles,structonly"`
// One of the profile names must match the value of
// DefaultCertificateProfileName or boulder-ca will fail to start.
CertProfiles map[string]issuance.ProfileConfig `validate:"dive,keys,alphanum,min=1,max=32,endkeys,required_without=Profile,structonly"`
// One of the profile names must match the value of ra.defaultProfileName
// or large amounts of issuance will fail.
CertProfiles map[string]*issuance.ProfileConfig `validate:"dive,keys,alphanum,min=1,max=32,endkeys,required_without=Profile,structonly"`
// TODO(#7159): Make this required once all live configs are using it.
CRLProfile issuance.CRLProfileConfig `validate:"-"`
Issuers []issuance.IssuerConfig `validate:"min=1,dive"`
LintConfig string
IgnoredLints []string
CRLProfile issuance.CRLProfileConfig `validate:"-"`
Issuers []issuance.IssuerConfig `validate:"min=1,dive"`
}
// How long issued certificates are valid for.
Expiry config.Duration
// How far back certificates should be backdated.
Backdate config.Duration
// What digits we should prepend to serials after randomly generating them.
SerialPrefix int `validate:"required,min=1,max=127"`
// Deprecated: Use SerialPrefixHex instead.
SerialPrefix int `validate:"required_without=SerialPrefixHex,omitempty,min=1,max=127"`
// SerialPrefixHex is the hex string to prepend to serials after randomly
// generating them. The minimum value is "01" to ensure that at least
// one bit in the prefix byte is set. The maximum value is "7f" to
// ensure that the first bit in the prefix byte is not set. The validate
// library cannot enforce mix/max values on strings, so that is done in
// NewCertificateAuthorityImpl.
//
// TODO(#7213): Replace `required_without` with `required` when SerialPrefix is removed.
SerialPrefixHex string `validate:"required_without=SerialPrefix,omitempty,hexadecimal,len=2"`
// MaxNames is the maximum number of subjectAltNames in a single cert.
// The value supplied MUST be greater than 0 and no more than 100. These
@ -77,12 +78,6 @@ type Config struct {
// Section 4.9.10, it MUST NOT be more than 10 days. Default 96h.
LifespanOCSP config.Duration
// LifespanCRL is how long CRLs are valid for. It should be longer than the
// `period` field of the CRL Updater. Per the BRs, Section 4.9.7, it MUST
// NOT be more than 10 days.
// Deprecated: Use Config.CA.Issuance.CRLProfile.ValidityInterval instead.
LifespanCRL config.Duration `validate:"-"`
// GoodKey is an embedded config stanza for the goodkey library.
GoodKey goodkey.Config
@ -100,10 +95,6 @@ type Config struct {
// Recommended to be around 500ms.
OCSPLogPeriod config.Duration
// Path of a YAML file containing the list of int64 RegIDs
// allowed to request ECDSA issuance
ECDSAAllowListFilename string
// CTLogListFile is the path to a JSON file on disk containing the set of
// all logs trusted by Chrome. The file must match the v3 log list schema:
// https://www.gstatic.com/ct/log_list/v3/log_list_schema.json
@ -151,6 +142,13 @@ func main() {
c.CA.DebugAddr = *debugAddr
}
serialPrefix := byte(c.CA.SerialPrefix)
if c.CA.SerialPrefixHex != "" {
parsedSerialPrefix, err := strconv.ParseUint(c.CA.SerialPrefixHex, 16, 8)
cmd.FailOnError(err, "Couldn't convert SerialPrefixHex to int")
serialPrefix = byte(parsedSerialPrefix)
}
if c.CA.MaxNames == 0 {
cmd.Fail("Error in CA config: MaxNames must not be 0")
}
@ -159,15 +157,6 @@ func main() {
c.CA.LifespanOCSP.Duration = 96 * time.Hour
}
// TODO(#7159): Remove these fallbacks once all live configs are setting the
// CRL validity interval inside the Issuance.CRLProfile Config.
if c.CA.Issuance.CRLProfile.ValidityInterval.Duration == 0 && c.CA.LifespanCRL.Duration != 0 {
c.CA.Issuance.CRLProfile.ValidityInterval = c.CA.LifespanCRL
}
if c.CA.Issuance.CRLProfile.MaxBackdate.Duration == 0 && c.CA.Backdate.Duration != 0 {
c.CA.Issuance.CRLProfile.MaxBackdate = c.CA.Backdate
}
scope, logger, oTelShutdown := cmd.StatsAndLogging(c.Syslog, c.OpenTelemetry, c.CA.DebugAddr)
defer oTelShutdown(context.Background())
logger.Info(cmd.VersionString())
@ -175,8 +164,9 @@ func main() {
metrics := ca.NewCAMetrics(scope)
cmd.FailOnError(c.PA.CheckChallenges(), "Invalid PA configuration")
cmd.FailOnError(c.PA.CheckIdentifiers(), "Invalid PA configuration")
pa, err := policy.New(c.PA.Challenges, logger)
pa, err := policy.New(c.PA.Identifiers, c.PA.Challenges, logger)
cmd.FailOnError(err, "Couldn't create PA")
if c.CA.HostnamePolicyFile == "" {
@ -192,59 +182,45 @@ func main() {
cmd.FailOnError(err, "Failed to load CT Log List")
}
clk := cmd.Clock()
var crlShards int
issuers := make([]*issuance.Issuer, 0, len(c.CA.Issuance.Issuers))
for _, issuerConfig := range c.CA.Issuance.Issuers {
issuer, err := issuance.LoadIssuer(issuerConfig, cmd.Clock())
for i, issuerConfig := range c.CA.Issuance.Issuers {
issuer, err := issuance.LoadIssuer(issuerConfig, clk)
cmd.FailOnError(err, "Loading issuer")
// All issuers should have the same number of CRL shards, because
// crl-updater assumes they all have the same number.
if issuerConfig.CRLShards != 0 && crlShards == 0 {
crlShards = issuerConfig.CRLShards
}
if issuerConfig.CRLShards != crlShards {
cmd.Fail(fmt.Sprintf("issuer %d has %d shards, want %d", i, issuerConfig.CRLShards, crlShards))
}
issuers = append(issuers, issuer)
logger.Infof("Loaded issuer: name=[%s] keytype=[%s] nameID=[%v] isActive=[%t]", issuer.Name(), issuer.KeyType(), issuer.NameID(), issuer.IsActive())
}
if c.CA.Issuance.DefaultCertificateProfileName == "" {
c.CA.Issuance.DefaultCertificateProfileName = "defaultBoulderCertificateProfile"
}
logger.Infof("Configured default certificate profile name set to: %s", c.CA.Issuance.DefaultCertificateProfileName)
// TODO(#7414) Remove this check.
if !reflect.ValueOf(c.CA.Issuance.Profile).IsZero() && len(c.CA.Issuance.CertProfiles) > 0 {
cmd.Fail("Only one of Issuance.Profile or Issuance.CertProfiles can be configured")
}
// TODO(#7414) Remove this check.
// Use the deprecated Profile as a CertProfiles
if len(c.CA.Issuance.CertProfiles) == 0 {
c.CA.Issuance.CertProfiles = make(map[string]issuance.ProfileConfig, 0)
c.CA.Issuance.CertProfiles[c.CA.Issuance.DefaultCertificateProfileName] = c.CA.Issuance.Profile
}
lints, err := linter.NewRegistry(c.CA.Issuance.IgnoredLints)
cmd.FailOnError(err, "Failed to create zlint registry")
if c.CA.Issuance.LintConfig != "" {
lintconfig, err := lint.NewConfigFromFile(c.CA.Issuance.LintConfig)
cmd.FailOnError(err, "Failed to load zlint config file")
lints.SetConfiguration(lintconfig)
cmd.Fail("At least one profile must be configured")
}
tlsConfig, err := c.CA.TLS.Load(scope)
cmd.FailOnError(err, "TLS config")
clk := cmd.Clock()
conn, err := bgrpc.ClientSetup(c.CA.SAService, tlsConfig, scope, clk)
saConn, err := bgrpc.ClientSetup(c.CA.SAService, tlsConfig, scope, clk)
cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to SA")
sa := sapb.NewStorageAuthorityClient(conn)
sa := sapb.NewStorageAuthorityClient(saConn)
var sctService rapb.SCTProviderClient
if c.CA.SCTService != nil {
sctConn, err := bgrpc.ClientSetup(c.CA.SCTService, tlsConfig, scope, clk)
cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to RA for SCTs")
sctService = rapb.NewSCTProviderClient(sctConn)
}
kp, err := sagoodkey.NewPolicy(&c.CA.GoodKey, sa.KeyBlocked)
cmd.FailOnError(err, "Unable to create key policy")
var ecdsaAllowList *ca.ECDSAAllowList
var entries int
if c.CA.ECDSAAllowListFilename != "" {
// Create an allow list object.
ecdsaAllowList, entries, err = ca.NewECDSAAllowListFromFile(c.CA.ECDSAAllowListFilename)
cmd.FailOnError(err, "Unable to load ECDSA allow list from YAML file")
logger.Infof("Loaded an ECDSA allow list with %d entries", entries)
}
srv := bgrpc.NewServer(c.CA.GRPCCA, logger)
if !c.CA.DisableOCSPService {
@ -281,15 +257,11 @@ func main() {
if !c.CA.DisableCertService {
cai, err := ca.NewCertificateAuthorityImpl(
sa,
sctService,
pa,
issuers,
c.CA.Issuance.DefaultCertificateProfileName,
c.CA.Issuance.CertProfiles,
lints,
ecdsaAllowList,
c.CA.Expiry.Duration,
c.CA.Backdate.Duration,
c.CA.SerialPrefix,
serialPrefix,
c.CA.MaxNames,
kp,
logger,

View file

@ -4,7 +4,6 @@ import (
"context"
"flag"
"os"
"time"
akamaipb "github.com/letsencrypt/boulder/akamai/proto"
capb "github.com/letsencrypt/boulder/ca/proto"
@ -25,6 +24,7 @@ import (
"github.com/letsencrypt/boulder/ratelimits"
bredis "github.com/letsencrypt/boulder/redis"
sapb "github.com/letsencrypt/boulder/sa/proto"
"github.com/letsencrypt/boulder/va"
vapb "github.com/letsencrypt/boulder/va/proto"
)
@ -33,7 +33,8 @@ type Config struct {
cmd.ServiceConfig
cmd.HostnamePolicyConfig
RateLimitPoliciesFilename string `validate:"required"`
// RateLimitPoliciesFilename is deprecated.
RateLimitPoliciesFilename string
MaxContactsPerRegistration int
@ -76,26 +77,35 @@ type Config struct {
// limits are per section 7.1 of our combined CP/CPS, under "DV-SSL
// Subscriber Certificate". The value must match the CA and WFE
// configurations.
MaxNames int `validate:"required,min=1,max=100"`
//
// Deprecated: Set ValidationProfiles[*].MaxNames instead.
MaxNames int `validate:"omitempty,min=1,max=100"`
// AuthorizationLifetimeDays defines how long authorizations will be
// considered valid for. Given a value of 300 days when used with a 90-day
// cert lifetime, this allows creation of certs that will cover a whole
// year, plus a grace period of a month.
AuthorizationLifetimeDays int `validate:"required,min=1,max=397"`
// ValidationProfiles is a map of validation profiles to their
// respective issuance allow lists. If a profile is not included in this
// mapping, it cannot be used by any account. If this field is left
// empty, all profiles are open to all accounts.
ValidationProfiles map[string]*ra.ValidationProfileConfig `validate:"required"`
// PendingAuthorizationLifetimeDays defines how long authorizations may be in
// the pending state. If you can't respond to a challenge this quickly, then
// you need to request a new challenge.
PendingAuthorizationLifetimeDays int `validate:"required,min=1,max=29"`
// DefaultProfileName sets the profile to use if one wasn't provided by the
// client in the new-order request. Must match a configured validation
// profile or the RA will fail to start. Must match a certificate profile
// configured in the CA or finalization will fail for orders using this
// default.
DefaultProfileName string `validate:"required"`
// MustStapleAllowList specified the path to a YAML file containing a
// list of account IDs permitted to request certificates with the OCSP
// Must-Staple extension.
//
// Deprecated: This field no longer has any effect, all Must-Staple requests
// are rejected.
// TODO(#8177): Remove this field.
MustStapleAllowList string `validate:"omitempty"`
// GoodKey is an embedded config stanza for the goodkey library.
GoodKey goodkey.Config
// OrderLifetime is how far in the future an Order's expiration date should
// be set when it is first created.
OrderLifetime config.Duration
// FinalizeTimeout is how long the RA is willing to wait for the Order
// finalization process to take. This config parameter only has an effect
// if the AsyncFinalization feature flag is enabled. Any systems which
@ -113,11 +123,6 @@ type Config struct {
// a `Stagger` value controlling how long we wait for one operator group
// to respond before trying a different one.
CTLogs ctconfig.CTConfig
// InformationalCTLogs are a set of CT logs we will always submit to
// but won't ever use the SCTs from. This may be because we want to
// test them or because they are not yet approved by a browser/root
// program but we still want our certs to end up there.
InformationalCTLogs []ctconfig.LogDescription
// IssuerCerts are paths to all intermediate certificates which may have
// been used to issue certificates in the last 90 days. These are used to
@ -162,8 +167,9 @@ func main() {
// Validate PA config and set defaults if needed
cmd.FailOnError(c.PA.CheckChallenges(), "Invalid PA configuration")
cmd.FailOnError(c.PA.CheckIdentifiers(), "Invalid PA configuration")
pa, err := policy.New(c.PA.Challenges, logger)
pa, err := policy.New(c.PA.Identifiers, c.PA.Challenges, logger)
cmd.FailOnError(err, "Couldn't create PA")
if c.RA.HostnamePolicyFile == "" {
@ -232,23 +238,22 @@ func main() {
ctp = ctpolicy.New(pubc, sctLogs, infoLogs, finalLogs, c.RA.CTLogs.Stagger.Duration, logger, scope)
// Baseline Requirements v1.8.1 section 4.2.1: "any reused data, document,
// or completed validation MUST be obtained no more than 398 days prior
// to issuing the Certificate". If unconfigured or the configured value is
// greater than 397 days, bail out.
if c.RA.AuthorizationLifetimeDays <= 0 || c.RA.AuthorizationLifetimeDays > 397 {
cmd.Fail("authorizationLifetimeDays value must be greater than 0 and less than 398")
if len(c.RA.ValidationProfiles) == 0 {
cmd.Fail("At least one profile must be configured")
}
authorizationLifetime := time.Duration(c.RA.AuthorizationLifetimeDays) * 24 * time.Hour
// The Baseline Requirements v1.8.1 state that validation tokens "MUST
// NOT be used for more than 30 days from its creation". If unconfigured
// or the configured value pendingAuthorizationLifetimeDays is greater
// than 29 days, bail out.
if c.RA.PendingAuthorizationLifetimeDays <= 0 || c.RA.PendingAuthorizationLifetimeDays > 29 {
cmd.Fail("pendingAuthorizationLifetimeDays value must be greater than 0 and less than 30")
// TODO(#7993): Remove this fallback and make ValidationProfile.MaxNames a
// required config field. We don't do any validation on the value of this
// top-level MaxNames because that happens inside the call to
// NewValidationProfiles below.
for _, pc := range c.RA.ValidationProfiles {
if pc.MaxNames == 0 {
pc.MaxNames = c.RA.MaxNames
}
}
pendingAuthorizationLifetime := time.Duration(c.RA.PendingAuthorizationLifetimeDays) * 24 * time.Hour
validationProfiles, err := ra.NewValidationProfiles(c.RA.DefaultProfileName, c.RA.ValidationProfiles)
cmd.FailOnError(err, "Failed to load validation profiles")
if features.Get().AsyncFinalize && c.RA.FinalizeTimeout.Duration == 0 {
cmd.Fail("finalizeTimeout must be supplied when AsyncFinalize feature is enabled")
@ -257,10 +262,6 @@ func main() {
kp, err := sagoodkey.NewPolicy(&c.RA.GoodKey, sac.KeyBlocked)
cmd.FailOnError(err, "Unable to create key policy")
if c.RA.MaxNames == 0 {
cmd.Fail("Error in RA config: MaxNames must not be 0")
}
var limiter *ratelimits.Limiter
var txnBuilder *ratelimits.TransactionBuilder
var limiterRedis *bredis.Ring
@ -272,7 +273,7 @@ func main() {
source := ratelimits.NewRedisSource(limiterRedis.Ring, clk, scope)
limiter, err = ratelimits.NewLimiter(clk, source, scope)
cmd.FailOnError(err, "Failed to create rate limiter")
txnBuilder, err = ratelimits.NewTransactionBuilder(c.RA.Limiter.Defaults, c.RA.Limiter.Overrides)
txnBuilder, err = ratelimits.NewTransactionBuilderFromFiles(c.RA.Limiter.Defaults, c.RA.Limiter.Overrides)
cmd.FailOnError(err, "Failed to create rate limits transaction builder")
}
@ -285,29 +286,29 @@ func main() {
limiter,
txnBuilder,
c.RA.MaxNames,
authorizationLifetime,
pendingAuthorizationLifetime,
validationProfiles,
pubc,
caaClient,
c.RA.OrderLifetime.Duration,
c.RA.FinalizeTimeout.Duration,
ctp,
apc,
issuerCerts,
)
defer rai.DrainFinalize()
defer rai.Drain()
policyErr := rai.LoadRateLimitPoliciesFile(c.RA.RateLimitPoliciesFilename)
cmd.FailOnError(policyErr, "Couldn't load rate limit policies file")
rai.PA = pa
rai.VA = vac
rai.VA = va.RemoteClients{
VAClient: vac,
CAAClient: caaClient,
}
rai.CA = cac
rai.OCSP = ocspc
rai.SA = sac
start, err := bgrpc.NewServer(c.RA.GRPC, logger).Add(
&rapb.RegistrationAuthority_ServiceDesc, rai).Build(tlsConfig, scope, clk)
&rapb.RegistrationAuthority_ServiceDesc, rai).Add(
&rapb.SCTProvider_ServiceDesc, rai).
Build(tlsConfig, scope, clk)
cmd.FailOnError(err, "Unable to setup RA gRPC server")
cmd.FailOnError(start(), "RA gRPC service failed")

View file

@ -10,16 +10,48 @@ import (
"github.com/letsencrypt/boulder/cmd"
"github.com/letsencrypt/boulder/features"
bgrpc "github.com/letsencrypt/boulder/grpc"
"github.com/letsencrypt/boulder/iana"
"github.com/letsencrypt/boulder/va"
vaConfig "github.com/letsencrypt/boulder/va/config"
vapb "github.com/letsencrypt/boulder/va/proto"
)
// RemoteVAGRPCClientConfig contains the information necessary to setup a gRPC
// client connection. The following GRPC client configuration field combinations
// are allowed:
//
// ServerIPAddresses, [Timeout]
// ServerAddress, DNSAuthority, [Timeout], [HostOverride]
// SRVLookup, DNSAuthority, [Timeout], [HostOverride], [SRVResolver]
// SRVLookups, DNSAuthority, [Timeout], [HostOverride], [SRVResolver]
type RemoteVAGRPCClientConfig struct {
cmd.GRPCClientConfig
// Perspective uniquely identifies the Network Perspective used to
// perform the validation, as specified in BRs Section 5.4.1,
// Requirement 2.7 ("Multi-Perspective Issuance Corroboration attempts
// from each Network Perspective"). It should uniquely identify a group
// of RVAs deployed in the same datacenter.
Perspective string `validate:"required"`
// RIR indicates the Regional Internet Registry where this RVA is
// located. This field is used to identify the RIR region from which a
// given validation was performed, as specified in the "Phased
// Implementation Timeline" in BRs Section 3.2.2.9. It must be one of
// the following values:
// - ARIN
// - RIPE
// - APNIC
// - LACNIC
// - AFRINIC
RIR string `validate:"required,oneof=ARIN RIPE APNIC LACNIC AFRINIC"`
}
type Config struct {
VA struct {
vaConfig.Common
RemoteVAs []cmd.GRPCClientConfig `validate:"omitempty,dive"`
MaxRemoteValidationFailures int `validate:"omitempty,min=0,required_with=RemoteVAs"`
RemoteVAs []RemoteVAGRPCClientConfig `validate:"omitempty,dive"`
// Deprecated and ignored
MaxRemoteValidationFailures int `validate:"omitempty,min=0,required_with=RemoteVAs"`
Features features.Config
}
@ -50,16 +82,12 @@ func main() {
clk := cmd.Clock()
var servers bdns.ServerProvider
proto := "udp"
if features.Get().DOH {
proto = "tcp"
}
if len(c.VA.DNSStaticResolvers) != 0 {
servers, err = bdns.NewStaticProvider(c.VA.DNSStaticResolvers)
cmd.FailOnError(err, "Couldn't start static DNS server resolver")
} else {
servers, err = bdns.StartDynamicProvider(c.VA.DNSProvider, 60*time.Second, proto)
servers, err = bdns.StartDynamicProvider(c.VA.DNSProvider, 60*time.Second, "tcp")
cmd.FailOnError(err, "Couldn't start dynamic DNS server resolver")
}
defer servers.Stop()
@ -75,6 +103,7 @@ func main() {
scope,
clk,
c.VA.DNSTries,
c.VA.UserAgent,
logger,
tlsConfig)
} else {
@ -84,6 +113,7 @@ func main() {
scope,
clk,
c.VA.DNSTries,
c.VA.UserAgent,
logger,
tlsConfig)
}
@ -91,7 +121,7 @@ func main() {
if len(c.VA.RemoteVAs) > 0 {
for _, rva := range c.VA.RemoteVAs {
rva := rva
vaConn, err := bgrpc.ClientSetup(&rva, tlsConfig, scope, clk)
vaConn, err := bgrpc.ClientSetup(&rva.GRPCClientConfig, tlsConfig, scope, clk)
cmd.FailOnError(err, "Unable to create remote VA client")
remotes = append(
remotes,
@ -100,7 +130,9 @@ func main() {
VAClient: vapb.NewVAClient(vaConn),
CAAClient: vapb.NewCAAClient(vaConn),
},
Address: rva.ServerAddress,
Address: rva.ServerAddress,
Perspective: rva.Perspective,
RIR: rva.RIR,
},
)
}
@ -109,13 +141,15 @@ func main() {
vai, err := va.NewValidationAuthorityImpl(
resolver,
remotes,
c.VA.MaxRemoteValidationFailures,
c.VA.UserAgent,
c.VA.IssuerDomain,
scope,
clk,
logger,
c.VA.AccountURIPrefixes)
c.VA.AccountURIPrefixes,
va.PrimaryPerspective,
"",
iana.IsReservedAddr)
cmd.FailOnError(err, "Unable to create VA server")
start, err := bgrpc.NewServer(c.VA.GRPC, logger).Add(

View file

@ -6,28 +6,26 @@ import (
"encoding/pem"
"flag"
"fmt"
"log"
"net/http"
"os"
"time"
"github.com/jmhodges/clock"
"github.com/prometheus/client_golang/prometheus"
"github.com/letsencrypt/boulder/cmd"
"github.com/letsencrypt/boulder/config"
emailpb "github.com/letsencrypt/boulder/email/proto"
"github.com/letsencrypt/boulder/features"
"github.com/letsencrypt/boulder/goodkey"
"github.com/letsencrypt/boulder/goodkey/sagoodkey"
bgrpc "github.com/letsencrypt/boulder/grpc"
"github.com/letsencrypt/boulder/grpc/noncebalancer"
"github.com/letsencrypt/boulder/issuance"
blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/nonce"
rapb "github.com/letsencrypt/boulder/ra/proto"
"github.com/letsencrypt/boulder/ratelimits"
bredis "github.com/letsencrypt/boulder/redis"
sapb "github.com/letsencrypt/boulder/sa/proto"
"github.com/letsencrypt/boulder/unpause"
"github.com/letsencrypt/boulder/web"
"github.com/letsencrypt/boulder/wfe2"
)
@ -45,22 +43,26 @@ type Config struct {
TLSListenAddress string `validate:"omitempty,hostname_port"`
// Timeout is the per-request overall timeout. This should be slightly
// lower than the upstream's timeout when making request to the WFE.
// lower than the upstream's timeout when making requests to this service.
Timeout config.Duration `validate:"-"`
// ShutdownStopTimeout determines the maximum amount of time to wait
// for extant request handlers to complete before exiting. It should be
// greater than Timeout.
ShutdownStopTimeout config.Duration
ServerCertificatePath string `validate:"required_with=TLSListenAddress"`
ServerKeyPath string `validate:"required_with=TLSListenAddress"`
AllowOrigins []string
ShutdownStopTimeout config.Duration
SubscriberAgreementURL string
TLS cmd.TLSConfig
RAService *cmd.GRPCClientConfig
SAService *cmd.GRPCClientConfig
RAService *cmd.GRPCClientConfig
SAService *cmd.GRPCClientConfig
EmailExporter *cmd.GRPCClientConfig
// GetNonceService is a gRPC config which contains a single SRV name
// used to lookup nonce-service instances used exclusively for nonce
@ -74,12 +76,13 @@ type Config struct {
// local and remote nonce-service instances.
RedeemNonceService *cmd.GRPCClientConfig `validate:"required"`
// NoncePrefixKey is a secret used for deriving the prefix of each nonce
// instance. It should contain 256 bits of random data to be suitable as
// an HMAC-SHA256 key (e.g. the output of `openssl rand -hex 32`). In a
// NonceHMACKey is a path to a file containing an HMAC key which is a
// secret used for deriving the prefix of each nonce instance. It should
// contain 256 bits (32 bytes) of random data to be suitable as an
// HMAC-SHA256 key (e.g. the output of `openssl rand -hex 32`). In a
// multi-DC deployment this value should be the same across all
// boulder-wfe and nonce-service instances.
NoncePrefixKey cmd.PasswordConfig `validate:"-"`
NonceHMACKey cmd.HMACKeyConfig `validate:"-"`
// Chains is a list of lists of certificate filenames. Each inner list is
// a chain (starting with the issuing intermediate, followed by one or
@ -116,17 +119,18 @@ type Config struct {
// StaleTimeout determines how old should data be to be accessed via Boulder-specific GET-able APIs
StaleTimeout config.Duration `validate:"-"`
// AuthorizationLifetimeDays defines how long authorizations will be
// considered valid for. The WFE uses this to find the creation date of
// authorizations by subtracing this value from the expiry. It should match
// the value configured in the RA.
AuthorizationLifetimeDays int `validate:"required,min=1,max=397"`
// AuthorizationLifetimeDays duplicates the RA's config of the same name.
// Deprecated: This field no longer has any effect.
AuthorizationLifetimeDays int `validate:"-"`
// PendingAuthorizationLifetimeDays defines how long authorizations may be in
// the pending state before expiry. The WFE uses this to find the creation
// date of pending authorizations by subtracting this value from the expiry.
// It should match the value configured in the RA.
PendingAuthorizationLifetimeDays int `validate:"required,min=1,max=29"`
// PendingAuthorizationLifetimeDays duplicates the RA's config of the same name.
// Deprecated: This field no longer has any effect.
PendingAuthorizationLifetimeDays int `validate:"-"`
// MaxContactsPerRegistration limits the number of contact addresses which
// can be provided in a single NewAccount request. Requests containing more
// contacts than this are rejected. Default: 10.
MaxContactsPerRegistration int `validate:"omitempty,min=1"`
AccountCache *CacheConfig
@ -152,18 +156,30 @@ type Config struct {
Overrides string
}
// MaxNames is the maximum number of subjectAltNames in a single cert.
// The value supplied SHOULD be greater than 0 and no more than 100,
// defaults to 100. These limits are per section 7.1 of our combined
// CP/CPS, under "DV-SSL Subscriber Certificate". The value must match
// the CA and RA configurations.
MaxNames int `validate:"min=0,max=100"`
// CertProfiles is a map of acceptable certificate profile names to
// descriptions (perhaps including URLs) of those profiles. NewOrder
// Requests with a profile name not present in this map will be rejected.
// This field is optional; if unset, no profile names are accepted.
CertProfiles map[string]string `validate:"omitempty,dive,keys,alphanum,min=1,max=32,endkeys"`
// CertificateProfileNames is the list of acceptable certificate profile
// names for newOrder requests. Requests with a profile name not in this
// list will be rejected. This field is optional; if unset, no profile
// names are accepted.
CertificateProfileNames []string `validate:"omitempty,dive,alphanum,min=1,max=32"`
Unpause struct {
// HMACKey signs outgoing JWTs for redemption at the unpause
// endpoint. This key must match the one configured for all SFEs.
// This field is required to enable the pausing feature.
HMACKey cmd.HMACKeyConfig `validate:"required_with=JWTLifetime URL,structonly"`
// JWTLifetime is the lifetime of the unpause JWTs generated by the
// WFE for redemption at the SFE. The minimum value for this field
// is 336h (14 days). This field is required to enable the pausing
// feature.
JWTLifetime config.Duration `validate:"omitempty,required_with=HMACKey URL,min=336h"`
// URL is the URL of the Self-Service Frontend (SFE). This is used
// to build URLs sent to end-users in error messages. This field
// must be a URL with a scheme of 'https://' This field is required
// to enable the pausing feature.
URL string `validate:"omitempty,required_with=HMACKey JWTLifetime,url,startswith=https://,endsnotwith=/"`
}
}
Syslog cmd.SyslogConfig
@ -199,63 +215,6 @@ func loadChain(certFiles []string) (*issuance.Certificate, []byte, error) {
return certs[0], buf.Bytes(), nil
}
func setupWFE(c Config, scope prometheus.Registerer, clk clock.Clock) (rapb.RegistrationAuthorityClient, sapb.StorageAuthorityReadOnlyClient, nonce.Getter, nonce.Redeemer, string) {
tlsConfig, err := c.WFE.TLS.Load(scope)
cmd.FailOnError(err, "TLS config")
raConn, err := bgrpc.ClientSetup(c.WFE.RAService, tlsConfig, scope, clk)
cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to RA")
rac := rapb.NewRegistrationAuthorityClient(raConn)
saConn, err := bgrpc.ClientSetup(c.WFE.SAService, tlsConfig, scope, clk)
cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to SA")
sac := sapb.NewStorageAuthorityReadOnlyClient(saConn)
if c.WFE.RedeemNonceService == nil {
cmd.Fail("'redeemNonceService' must be configured.")
}
if c.WFE.GetNonceService == nil {
cmd.Fail("'getNonceService' must be configured")
}
var rncKey string
if c.WFE.NoncePrefixKey.PasswordFile != "" {
rncKey, err = c.WFE.NoncePrefixKey.Pass()
cmd.FailOnError(err, "Failed to load noncePrefixKey")
}
getNonceConn, err := bgrpc.ClientSetup(c.WFE.GetNonceService, tlsConfig, scope, clk)
cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to get nonce service")
gnc := nonce.NewGetter(getNonceConn)
if c.WFE.RedeemNonceService.SRVResolver != noncebalancer.SRVResolverScheme {
cmd.Fail(fmt.Sprintf(
"'redeemNonceService.SRVResolver' must be set to %q", noncebalancer.SRVResolverScheme),
)
}
redeemNonceConn, err := bgrpc.ClientSetup(c.WFE.RedeemNonceService, tlsConfig, scope, clk)
cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to redeem nonce service")
rnc := nonce.NewRedeemer(redeemNonceConn)
return rac, sac, gnc, rnc, rncKey
}
type errorWriter struct {
blog.Logger
}
func (ew errorWriter) Write(p []byte) (n int, err error) {
// log.Logger will append a newline to all messages before calling
// Write. Our log checksum checker doesn't like newlines, because
// syslog will strip them out so the calculated checksums will
// differ. So that we don't hit this corner case for every line
// logged from inside net/http.Server we strip the newline before
// we get to the checksum generator.
p = bytes.TrimRight(p, "\n")
ew.Logger.Err(fmt.Sprintf("net/http.Server: %s", string(p)))
return
}
func main() {
listenAddr := flag.String("addr", "", "HTTP listen address override")
tlsAddr := flag.String("tls-addr", "", "HTTPS listen address override")
@ -282,11 +241,6 @@ func main() {
if *debugAddr != "" {
c.WFE.DebugAddr = *debugAddr
}
maxNames := c.WFE.MaxNames
if maxNames == 0 {
// Default to 100 names per cert.
maxNames = 100
}
certChains := map[issuance.NameID][][]byte{}
issuerCerts := map[issuance.NameID]*issuance.Certificate{}
@ -309,7 +263,52 @@ func main() {
clk := cmd.Clock()
rac, sac, gnc, rnc, npKey := setupWFE(c, stats, clk)
var unpauseSigner unpause.JWTSigner
if features.Get().CheckIdentifiersPaused {
unpauseSigner, err = unpause.NewJWTSigner(c.WFE.Unpause.HMACKey)
cmd.FailOnError(err, "Failed to create unpause signer from HMACKey")
}
tlsConfig, err := c.WFE.TLS.Load(stats)
cmd.FailOnError(err, "TLS config")
raConn, err := bgrpc.ClientSetup(c.WFE.RAService, tlsConfig, stats, clk)
cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to RA")
rac := rapb.NewRegistrationAuthorityClient(raConn)
saConn, err := bgrpc.ClientSetup(c.WFE.SAService, tlsConfig, stats, clk)
cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to SA")
sac := sapb.NewStorageAuthorityReadOnlyClient(saConn)
var eec emailpb.ExporterClient
if c.WFE.EmailExporter != nil {
emailExporterConn, err := bgrpc.ClientSetup(c.WFE.EmailExporter, tlsConfig, stats, clk)
cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to email-exporter")
eec = emailpb.NewExporterClient(emailExporterConn)
}
if c.WFE.RedeemNonceService == nil {
cmd.Fail("'redeemNonceService' must be configured.")
}
if c.WFE.GetNonceService == nil {
cmd.Fail("'getNonceService' must be configured")
}
noncePrefixKey, err := c.WFE.NonceHMACKey.Load()
cmd.FailOnError(err, "Failed to load nonceHMACKey file")
getNonceConn, err := bgrpc.ClientSetup(c.WFE.GetNonceService, tlsConfig, stats, clk)
cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to get nonce service")
gnc := nonce.NewGetter(getNonceConn)
if c.WFE.RedeemNonceService.SRVResolver != noncebalancer.SRVResolverScheme {
cmd.Fail(fmt.Sprintf(
"'redeemNonceService.SRVResolver' must be set to %q", noncebalancer.SRVResolverScheme),
)
}
redeemNonceConn, err := bgrpc.ClientSetup(c.WFE.RedeemNonceService, tlsConfig, stats, clk)
cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to redeem nonce service")
rnc := nonce.NewRedeemer(redeemNonceConn)
kp, err := sagoodkey.NewPolicy(&c.WFE.GoodKey, sac.KeyBlocked)
cmd.FailOnError(err, "Unable to create key policy")
@ -318,23 +317,9 @@ func main() {
c.WFE.StaleTimeout.Duration = time.Minute * 10
}
// Baseline Requirements v1.8.1 section 4.2.1: "any reused data, document,
// or completed validation MUST be obtained no more than 398 days prior
// to issuing the Certificate". If unconfigured or the configured value is
// greater than 397 days, bail out.
if c.WFE.AuthorizationLifetimeDays <= 0 || c.WFE.AuthorizationLifetimeDays > 397 {
cmd.Fail("authorizationLifetimeDays value must be greater than 0 and less than 398")
if c.WFE.MaxContactsPerRegistration == 0 {
c.WFE.MaxContactsPerRegistration = 10
}
authorizationLifetime := time.Duration(c.WFE.AuthorizationLifetimeDays) * 24 * time.Hour
// The Baseline Requirements v1.8.1 state that validation tokens "MUST
// NOT be used for more than 30 days from its creation". If unconfigured
// or the configured value pendingAuthorizationLifetimeDays is greater
// than 29 days, bail out.
if c.WFE.PendingAuthorizationLifetimeDays <= 0 || c.WFE.PendingAuthorizationLifetimeDays > 29 {
cmd.Fail("pendingAuthorizationLifetimeDays value must be greater than 0 and less than 30")
}
pendingAuthorizationLifetime := time.Duration(c.WFE.PendingAuthorizationLifetimeDays) * 24 * time.Hour
var limiter *ratelimits.Limiter
var txnBuilder *ratelimits.TransactionBuilder
@ -347,7 +332,7 @@ func main() {
source := ratelimits.NewRedisSource(limiterRedis.Ring, clk, stats)
limiter, err = ratelimits.NewLimiter(clk, source, stats)
cmd.FailOnError(err, "Failed to create rate limiter")
txnBuilder, err = ratelimits.NewTransactionBuilder(c.WFE.Limiter.Defaults, c.WFE.Limiter.Overrides)
txnBuilder, err = ratelimits.NewTransactionBuilderFromFiles(c.WFE.Limiter.Defaults, c.WFE.Limiter.Overrides)
cmd.FailOnError(err, "Failed to create rate limits transaction builder")
}
@ -370,18 +355,20 @@ func main() {
logger,
c.WFE.Timeout.Duration,
c.WFE.StaleTimeout.Duration,
authorizationLifetime,
pendingAuthorizationLifetime,
c.WFE.MaxContactsPerRegistration,
rac,
sac,
eec,
gnc,
rnc,
npKey,
noncePrefixKey,
accountGetter,
limiter,
txnBuilder,
maxNames,
c.WFE.CertificateProfileNames,
c.WFE.CertProfiles,
unpauseSigner,
c.WFE.Unpause.JWTLifetime.Duration,
c.WFE.Unpause.URL,
)
cmd.FailOnError(err, "Unable to create WFE")
@ -400,15 +387,7 @@ func main() {
logger.Infof("Server running, listening on %s....", c.WFE.ListenAddress)
handler := wfe.Handler(stats, c.OpenTelemetryHTTPConfig.Options()...)
srv := http.Server{
ReadTimeout: 30 * time.Second,
WriteTimeout: 120 * time.Second,
IdleTimeout: 120 * time.Second,
Addr: c.WFE.ListenAddress,
ErrorLog: log.New(errorWriter{logger}, "", 0),
Handler: handler,
}
srv := web.NewServer(c.WFE.ListenAddress, handler, logger)
go func() {
err := srv.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
@ -416,14 +395,7 @@ func main() {
}
}()
tlsSrv := http.Server{
ReadTimeout: 30 * time.Second,
WriteTimeout: 120 * time.Second,
IdleTimeout: 120 * time.Second,
Addr: c.WFE.TLSListenAddress,
ErrorLog: log.New(errorWriter{logger}, "", 0),
Handler: handler,
}
tlsSrv := web.NewServer(c.WFE.TLSListenAddress, handler, logger)
if tlsSrv.Addr != "" {
go func() {
logger.Infof("TLS server listening on %s", tlsSrv.Addr)

View file

@ -5,7 +5,6 @@ import (
"os"
"strings"
_ "github.com/letsencrypt/boulder/cmd/admin-revoker"
_ "github.com/letsencrypt/boulder/cmd/akamai-purger"
_ "github.com/letsencrypt/boulder/cmd/bad-key-revoker"
_ "github.com/letsencrypt/boulder/cmd/boulder-ca"
@ -16,19 +15,17 @@ import (
_ "github.com/letsencrypt/boulder/cmd/boulder-va"
_ "github.com/letsencrypt/boulder/cmd/boulder-wfe2"
_ "github.com/letsencrypt/boulder/cmd/cert-checker"
_ "github.com/letsencrypt/boulder/cmd/contact-auditor"
_ "github.com/letsencrypt/boulder/cmd/crl-checker"
_ "github.com/letsencrypt/boulder/cmd/crl-storer"
_ "github.com/letsencrypt/boulder/cmd/crl-updater"
_ "github.com/letsencrypt/boulder/cmd/expiration-mailer"
_ "github.com/letsencrypt/boulder/cmd/id-exporter"
_ "github.com/letsencrypt/boulder/cmd/email-exporter"
_ "github.com/letsencrypt/boulder/cmd/log-validator"
_ "github.com/letsencrypt/boulder/cmd/nonce-service"
_ "github.com/letsencrypt/boulder/cmd/notify-mailer"
_ "github.com/letsencrypt/boulder/cmd/ocsp-responder"
_ "github.com/letsencrypt/boulder/cmd/remoteva"
_ "github.com/letsencrypt/boulder/cmd/reversed-hostname-checker"
_ "github.com/letsencrypt/boulder/cmd/rocsp-tool"
_ "github.com/letsencrypt/boulder/cmd/sfe"
"github.com/letsencrypt/boulder/core"
"github.com/letsencrypt/boulder/cmd"
@ -84,37 +81,31 @@ var boulderUsage = fmt.Sprintf(`Usage: %s <subcommand> [flags]
func main() {
defer cmd.AuditPanic()
var command string
if core.Command() == "boulder" {
// Operator passed the boulder component as a subcommand.
if len(os.Args) <= 1 {
// No arguments passed.
fmt.Fprint(os.Stderr, boulderUsage)
return
}
if os.Args[1] == "--help" || os.Args[1] == "-help" {
// Help flag passed.
fmt.Fprint(os.Stderr, boulderUsage)
return
}
if os.Args[1] == "--list" || os.Args[1] == "-list" {
// List flag passed.
for _, c := range cmd.AvailableCommands() {
fmt.Println(c)
}
return
}
command = os.Args[1]
// Remove the subcommand from the arguments.
os.Args = os.Args[1:]
} else {
// Operator ran a boulder component using a symlink.
command = core.Command()
if len(os.Args) <= 1 {
// No arguments passed.
fmt.Fprint(os.Stderr, boulderUsage)
return
}
if os.Args[1] == "--help" || os.Args[1] == "-help" {
// Help flag passed.
fmt.Fprint(os.Stderr, boulderUsage)
return
}
if os.Args[1] == "--list" || os.Args[1] == "-list" {
// List flag passed.
for _, c := range cmd.AvailableCommands() {
fmt.Println(c)
}
return
}
// Remove the subcommand from the arguments.
command := os.Args[1]
os.Args = os.Args[1:]
config := getConfigPath()
if config != "" {
// Config flag passed.

View file

@ -40,11 +40,7 @@ func TestConfigValidation(t *testing.T) {
case "boulder-sa":
fileNames = []string{"sa.json"}
case "boulder-va":
fileNames = []string{
"va.json",
"va-remote-a.json",
"va-remote-b.json",
}
fileNames = []string{"va.json"}
case "remoteva":
fileNames = []string{
"remoteva-a.json",
@ -52,6 +48,8 @@ func TestConfigValidation(t *testing.T) {
}
case "boulder-wfe2":
fileNames = []string{"wfe2.json"}
case "sfe":
fileNames = []string{"sfe.json"}
case "nonce-service":
fileNames = []string{
"nonce-a.json",

View file

@ -123,7 +123,6 @@ certificate-profile:
policies:
- oid: 1.2.3
- oid: 4.5.6
cps-uri: "http://example.com/cps"
key-usages:
- Digital Signature
- Cert Sign
@ -420,5 +419,5 @@ The certificate profile defines a restricted set of fields that are used to gene
| `ocsp-url` | Specifies the AIA OCSP responder URL |
| `crl-url` | Specifies the cRLDistributionPoints URL |
| `issuer-url` | Specifies the AIA caIssuer URL |
| `policies` | Specifies contents of a certificatePolicies extension. Should contain a list of policies with the fields `oid`, indicating the policy OID, and a `cps-uri` field, containing the CPS URI to use, if the policy should contain a id-qt-cps qualifier. Only single CPS values are supported. |
| `policies` | Specifies contents of a certificatePolicies extension. Should contain a list of policies with the field `oid`, indicating the policy OID. |
| `key-usages` | Specifies list of key usage bits should be set, list can contain `Digital Signature`, `CRL Sign`, and `Cert Sign` |

View file

@ -17,9 +17,6 @@ import (
type policyInfoConfig struct {
OID string
// Deprecated: we do not include the id-qt-cps policy qualifier in our
// certificate policy extensions anymore.
CPSURI string `yaml:"cps-uri"`
}
// certProfile contains the information required to generate a certificate
@ -308,12 +305,11 @@ func makeTemplate(randReader io.Reader, profile *certProfile, pubKey []byte, tbc
case crlCert:
cert.IsCA = false
case requestCert, intermediateCert:
// id-kp-serverAuth and id-kp-clientAuth are included in intermediate
// certificates in order to technically constrain them. id-kp-serverAuth
// is required by 7.1.2.2.g of the CABF Baseline Requirements, but
// id-kp-clientAuth isn't. We include id-kp-clientAuth as we also include
// it in our end-entity certificates.
cert.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}
// id-kp-serverAuth is included in intermediate certificates, as required by
// Section 7.1.2.10.6 of the CA/BF Baseline Requirements.
// id-kp-clientAuth is excluded, as required by section 3.2.1 of the Chrome
// Root Program Requirements.
cert.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}
cert.MaxPathLenZero = true
case crossCert:
cert.ExtKeyUsage = tbcs.ExtKeyUsage
@ -321,11 +317,11 @@ func makeTemplate(randReader io.Reader, profile *certProfile, pubKey []byte, tbc
}
for _, policyConfig := range profile.Policies {
oid, err := parseOID(policyConfig.OID)
x509OID, err := x509.ParseOID(policyConfig.OID)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to parse %s as OID: %w", policyConfig.OID, err)
}
cert.PolicyIdentifiers = append(cert.PolicyIdentifiers, oid)
cert.Policies = append(cert.Policies, x509OID)
}
return cert, nil

View file

@ -2,8 +2,9 @@ package main
import (
"bytes"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
@ -126,15 +127,14 @@ func TestMakeTemplateRoot(t *testing.T) {
test.AssertEquals(t, len(cert.IssuingCertificateURL), 1)
test.AssertEquals(t, cert.IssuingCertificateURL[0], profile.IssuerURL)
test.AssertEquals(t, cert.KeyUsage, x509.KeyUsageDigitalSignature|x509.KeyUsageCRLSign)
test.AssertEquals(t, len(cert.PolicyIdentifiers), 2)
test.AssertEquals(t, len(cert.Policies), 2)
test.AssertEquals(t, len(cert.ExtKeyUsage), 0)
cert, err = makeTemplate(randReader, profile, pubKey, nil, intermediateCert)
test.AssertNotError(t, err, "makeTemplate failed when everything worked as expected")
test.Assert(t, cert.MaxPathLenZero, "MaxPathLenZero not set in intermediate template")
test.AssertEquals(t, len(cert.ExtKeyUsage), 2)
test.AssertEquals(t, cert.ExtKeyUsage[0], x509.ExtKeyUsageClientAuth)
test.AssertEquals(t, cert.ExtKeyUsage[1], x509.ExtKeyUsageServerAuth)
test.AssertEquals(t, len(cert.ExtKeyUsage), 1)
test.AssertEquals(t, cert.ExtKeyUsage[0], x509.ExtKeyUsageServerAuth)
}
func TestMakeTemplateRestrictedCrossCertificate(t *testing.T) {
@ -551,7 +551,7 @@ func TestGenerateCSR(t *testing.T) {
Country: "country",
}
signer, err := rsa.GenerateKey(rand.Reader, 1024)
signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
test.AssertNotError(t, err, "failed to generate test key")
csrBytes, err := generateCSR(profile, &wrappedSigner{signer})

View file

@ -34,7 +34,7 @@ var kp goodkey.KeyPolicy
func init() {
var err error
kp, err = goodkey.NewPolicy(&goodkey.Config{FermatRounds: 100}, nil)
kp, err = goodkey.NewPolicy(nil, nil)
if err != nil {
log.Fatal("Could not create goodkey.KeyPolicy")
}
@ -96,7 +96,7 @@ func postIssuanceLinting(fc *x509.Certificate, skipLints []string) error {
type keyGenConfig struct {
Type string `yaml:"type"`
RSAModLength uint `yaml:"rsa-mod-length"`
RSAModLength int `yaml:"rsa-mod-length"`
ECDSACurve string `yaml:"ecdsa-curve"`
}

View file

@ -6,8 +6,9 @@ import (
"log"
"math/big"
"github.com/letsencrypt/boulder/pkcs11helpers"
"github.com/miekg/pkcs11"
"github.com/letsencrypt/boulder/pkcs11helpers"
)
const (
@ -18,10 +19,10 @@ const (
// device and specifies which mechanism should be used. modulusLen specifies the
// length of the modulus to be generated on the device in bits and exponent
// specifies the public exponent that should be used.
func rsaArgs(label string, modulusLen, exponent uint, keyID []byte) generateArgs {
func rsaArgs(label string, modulusLen int, keyID []byte) generateArgs {
// Encode as unpadded big endian encoded byte slice
expSlice := big.NewInt(int64(exponent)).Bytes()
log.Printf("\tEncoded public exponent (%d) as: %0X\n", exponent, expSlice)
expSlice := big.NewInt(rsaExp).Bytes()
log.Printf("\tEncoded public exponent (%d) as: %0X\n", rsaExp, expSlice)
return generateArgs{
mechanism: []*pkcs11.Mechanism{
pkcs11.NewMechanism(pkcs11.CKM_RSA_PKCS_KEY_PAIR_GEN, nil),
@ -55,15 +56,15 @@ func rsaArgs(label string, modulusLen, exponent uint, keyID []byte) generateArgs
// handle, and constructs a rsa.PublicKey. It also checks that the key has the
// correct length modulus and that the public exponent is what was requested in
// the public key template.
func rsaPub(session *pkcs11helpers.Session, object pkcs11.ObjectHandle, modulusLen, exponent uint) (*rsa.PublicKey, error) {
func rsaPub(session *pkcs11helpers.Session, object pkcs11.ObjectHandle, modulusLen int) (*rsa.PublicKey, error) {
pubKey, err := session.GetRSAPublicKey(object)
if err != nil {
return nil, err
}
if pubKey.E != int(exponent) {
if pubKey.E != rsaExp {
return nil, errors.New("returned CKA_PUBLIC_EXPONENT doesn't match expected exponent")
}
if pubKey.N.BitLen() != int(modulusLen) {
if pubKey.N.BitLen() != modulusLen {
return nil, errors.New("returned CKA_MODULUS isn't of the expected bit length")
}
log.Printf("\tPublic exponent: %d\n", pubKey.E)
@ -75,21 +76,21 @@ func rsaPub(session *pkcs11helpers.Session, object pkcs11.ObjectHandle, modulusL
// specified by modulusLen and with the exponent 65537.
// It returns the public part of the generated key pair as a rsa.PublicKey
// and the random key ID that the HSM uses to identify the key pair.
func rsaGenerate(session *pkcs11helpers.Session, label string, modulusLen uint) (*rsa.PublicKey, []byte, error) {
func rsaGenerate(session *pkcs11helpers.Session, label string, modulusLen int) (*rsa.PublicKey, []byte, error) {
keyID := make([]byte, 4)
_, err := newRandReader(session).Read(keyID)
if err != nil {
return nil, nil, err
}
log.Printf("Generating RSA key with %d bit modulus and public exponent %d and ID %x\n", modulusLen, rsaExp, keyID)
args := rsaArgs(label, modulusLen, rsaExp, keyID)
args := rsaArgs(label, modulusLen, keyID)
pub, _, err := session.GenerateKeyPair(args.mechanism, args.publicAttrs, args.privateAttrs)
if err != nil {
return nil, nil, err
}
log.Println("Key generated")
log.Println("Extracting public key")
pk, err := rsaPub(session, pub, modulusLen, rsaExp)
pk, err := rsaPub(session, pub, modulusLen)
if err != nil {
return nil, nil, err
}

View file

@ -8,24 +8,15 @@ import (
"math/big"
"testing"
"github.com/miekg/pkcs11"
"github.com/letsencrypt/boulder/pkcs11helpers"
"github.com/letsencrypt/boulder/test"
"github.com/miekg/pkcs11"
)
func TestRSAPub(t *testing.T) {
s, ctx := pkcs11helpers.NewSessionWithMock()
// test we fail to construct key with non-matching exp
ctx.GetAttributeValueFunc = func(pkcs11.SessionHandle, pkcs11.ObjectHandle, []*pkcs11.Attribute) ([]*pkcs11.Attribute, error) {
return []*pkcs11.Attribute{
pkcs11.NewAttribute(pkcs11.CKA_PUBLIC_EXPONENT, []byte{1, 0, 1}),
pkcs11.NewAttribute(pkcs11.CKA_MODULUS, []byte{255}),
}, nil
}
_, err := rsaPub(s, 0, 0, 255)
test.AssertError(t, err, "rsaPub didn't fail with non-matching exp")
// test we fail to construct key with non-matching modulus
ctx.GetAttributeValueFunc = func(pkcs11.SessionHandle, pkcs11.ObjectHandle, []*pkcs11.Attribute) ([]*pkcs11.Attribute, error) {
return []*pkcs11.Attribute{
@ -33,7 +24,7 @@ func TestRSAPub(t *testing.T) {
pkcs11.NewAttribute(pkcs11.CKA_MODULUS, []byte{255}),
}, nil
}
_, err = rsaPub(s, 0, 16, 65537)
_, err := rsaPub(s, 0, 16)
test.AssertError(t, err, "rsaPub didn't fail with non-matching modulus size")
// test we don't fail with the correct attributes
@ -43,7 +34,7 @@ func TestRSAPub(t *testing.T) {
pkcs11.NewAttribute(pkcs11.CKA_MODULUS, []byte{255}),
}, nil
}
_, err = rsaPub(s, 0, 8, 65537)
_, err = rsaPub(s, 0, 8)
test.AssertNotError(t, err, "rsaPub failed with valid attributes")
}

View file

@ -8,6 +8,7 @@ import (
"encoding/json"
"flag"
"fmt"
"net/netip"
"os"
"regexp"
"slices"
@ -29,7 +30,8 @@ import (
"github.com/letsencrypt/boulder/features"
"github.com/letsencrypt/boulder/goodkey"
"github.com/letsencrypt/boulder/goodkey/sagoodkey"
_ "github.com/letsencrypt/boulder/linter"
"github.com/letsencrypt/boulder/identifier"
"github.com/letsencrypt/boulder/linter"
blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/policy"
"github.com/letsencrypt/boulder/precert"
@ -77,7 +79,7 @@ func (r *report) dump() error {
type reportEntry struct {
Valid bool `json:"valid"`
DNSNames []string `json:"dnsNames"`
SANs []string `json:"sans"`
Problems []string `json:"problems,omitempty"`
}
@ -99,12 +101,13 @@ type certChecker struct {
kp goodkey.KeyPolicy
dbMap certDB
getPrecert precertGetter
certs chan core.Certificate
certs chan *corepb.Certificate
clock clock.Clock
rMu *sync.Mutex
issuedReport report
checkPeriod time.Duration
acceptableValidityDurations map[time.Duration]bool
lints lint.Registry
logger blog.Logger
}
@ -114,6 +117,7 @@ func newChecker(saDbMap certDB,
kp goodkey.KeyPolicy,
period time.Duration,
avd map[time.Duration]bool,
lints lint.Registry,
logger blog.Logger,
) certChecker {
precertGetter := func(ctx context.Context, serial string) ([]byte, error) {
@ -121,19 +125,20 @@ func newChecker(saDbMap certDB,
if err != nil {
return nil, err
}
return precertPb.DER, nil
return precertPb.Der, nil
}
return certChecker{
pa: pa,
kp: kp,
dbMap: saDbMap,
getPrecert: precertGetter,
certs: make(chan core.Certificate, batchSize),
certs: make(chan *corepb.Certificate, batchSize),
rMu: new(sync.Mutex),
clock: clk,
issuedReport: report{Entries: make(map[string]reportEntry)},
checkPeriod: period,
acceptableValidityDurations: avd,
lints: lints,
logger: logger,
}
}
@ -210,7 +215,7 @@ func (c *certChecker) getCerts(ctx context.Context) error {
batchStartID := initialID
var retries int
for {
certs, err := sa.SelectCertificates(
certs, highestID, err := sa.SelectCertificates(
ctx,
c.dbMap,
`WHERE id > :id AND
@ -235,16 +240,16 @@ func (c *certChecker) getCerts(ctx context.Context) error {
}
retries = 0
for _, cert := range certs {
c.certs <- cert.Certificate
c.certs <- cert
}
if len(certs) == 0 {
break
}
lastCert := certs[len(certs)-1]
batchStartID = lastCert.ID
if lastCert.Issued.After(c.issuedReport.end) {
if lastCert.Issued.AsTime().After(c.issuedReport.end) {
break
}
batchStartID = highestID
}
// Close channel so range operations won't block once the channel empties out
@ -252,15 +257,15 @@ func (c *certChecker) getCerts(ctx context.Context) error {
return nil
}
func (c *certChecker) processCerts(ctx context.Context, wg *sync.WaitGroup, badResultsOnly bool, ignoredLints map[string]bool) {
func (c *certChecker) processCerts(ctx context.Context, wg *sync.WaitGroup, badResultsOnly bool) {
for cert := range c.certs {
dnsNames, problems := c.checkCert(ctx, cert, ignoredLints)
sans, problems := c.checkCert(ctx, cert)
valid := len(problems) == 0
c.rMu.Lock()
if !badResultsOnly || (badResultsOnly && !valid) {
c.issuedReport.Entries[cert.Serial] = reportEntry{
Valid: valid,
DNSNames: dnsNames,
SANs: sans,
Problems: problems,
}
}
@ -298,8 +303,8 @@ var expectedExtensionContent = map[string][]byte{
// likely valid at the time the certificate was issued. Authorizations with
// status = "deactivated" are counted for this, so long as their validatedAt
// is before the issuance and expiration is after.
func (c *certChecker) checkValidations(ctx context.Context, cert core.Certificate, dnsNames []string) error {
authzs, err := sa.SelectAuthzsMatchingIssuance(ctx, c.dbMap, cert.RegistrationID, cert.Issued, dnsNames)
func (c *certChecker) checkValidations(ctx context.Context, cert *corepb.Certificate, idents identifier.ACMEIdentifiers) error {
authzs, err := sa.SelectAuthzsMatchingIssuance(ctx, c.dbMap, cert.RegistrationID, cert.Issued.AsTime(), idents)
if err != nil {
return fmt.Errorf("error checking authzs for certificate %s: %w", cert.Serial, err)
}
@ -308,18 +313,18 @@ func (c *certChecker) checkValidations(ctx context.Context, cert core.Certificat
return fmt.Errorf("no relevant authzs found valid at %s", cert.Issued)
}
// We may get multiple authorizations for the same name, but that's okay.
// Any authorization for a given name is sufficient.
nameToAuthz := make(map[string]*corepb.Authorization)
// We may get multiple authorizations for the same identifier, but that's
// okay. Any authorization for a given identifier is sufficient.
identToAuthz := make(map[identifier.ACMEIdentifier]*corepb.Authorization)
for _, m := range authzs {
nameToAuthz[m.Identifier] = m
identToAuthz[identifier.FromProto(m.Identifier)] = m
}
var errors []error
for _, name := range dnsNames {
_, ok := nameToAuthz[name]
for _, ident := range idents {
_, ok := identToAuthz[ident]
if !ok {
errors = append(errors, fmt.Errorf("missing authz for %q", name))
errors = append(errors, fmt.Errorf("missing authz for %q", ident.Value))
continue
}
}
@ -329,155 +334,196 @@ func (c *certChecker) checkValidations(ctx context.Context, cert core.Certificat
return nil
}
// checkCert returns a list of DNS names in the certificate and a list of problems with the certificate.
func (c *certChecker) checkCert(ctx context.Context, cert core.Certificate, ignoredLints map[string]bool) ([]string, []string) {
var dnsNames []string
// checkCert returns a list of Subject Alternative Names in the certificate and a list of problems with the certificate.
func (c *certChecker) checkCert(ctx context.Context, cert *corepb.Certificate) ([]string, []string) {
var problems []string
// Check that the digests match.
if cert.Digest != core.Fingerprint256(cert.DER) {
if cert.Digest != core.Fingerprint256(cert.Der) {
problems = append(problems, "Stored digest doesn't match certificate digest")
}
// Parse the certificate.
parsedCert, err := zX509.ParseCertificate(cert.DER)
parsedCert, err := zX509.ParseCertificate(cert.Der)
if err != nil {
problems = append(problems, fmt.Sprintf("Couldn't parse stored certificate: %s", err))
// This is a fatal error, we can't do any further processing.
return nil, problems
}
// Now that it's parsed, we can extract the SANs.
sans := slices.Clone(parsedCert.DNSNames)
for _, ip := range parsedCert.IPAddresses {
sans = append(sans, ip.String())
}
// Run zlint checks.
results := zlint.LintCertificateEx(parsedCert, c.lints)
for name, res := range results.Results {
if res.Status <= lint.Pass {
continue
}
prob := fmt.Sprintf("zlint %s: %s", res.Status, name)
if res.Details != "" {
prob = fmt.Sprintf("%s %s", prob, res.Details)
}
problems = append(problems, prob)
}
// Check if stored serial is correct.
storedSerial, err := core.StringToSerial(cert.Serial)
if err != nil {
problems = append(problems, "Stored serial is invalid")
} else if parsedCert.SerialNumber.Cmp(storedSerial) != 0 {
problems = append(problems, "Stored serial doesn't match certificate serial")
}
// Check that we have the correct expiration time.
if !parsedCert.NotAfter.Equal(cert.Expires.AsTime()) {
problems = append(problems, "Stored expiration doesn't match certificate NotAfter")
}
// Check if basic constraints are set.
if !parsedCert.BasicConstraintsValid {
problems = append(problems, "Certificate doesn't have basic constraints set")
}
// Check that the cert isn't able to sign other certificates.
if parsedCert.IsCA {
problems = append(problems, "Certificate can sign other certificates")
}
// Check that the cert has a valid validity period. The validity
// period is computed inclusive of the whole final second indicated by
// notAfter.
validityDuration := parsedCert.NotAfter.Add(time.Second).Sub(parsedCert.NotBefore)
_, ok := c.acceptableValidityDurations[validityDuration]
if !ok {
problems = append(problems, "Certificate has unacceptable validity period")
}
// Check that the stored issuance time isn't too far back/forward dated.
if parsedCert.NotBefore.Before(cert.Issued.AsTime().Add(-6*time.Hour)) || parsedCert.NotBefore.After(cert.Issued.AsTime().Add(6*time.Hour)) {
problems = append(problems, "Stored issuance date is outside of 6 hour window of certificate NotBefore")
}
// Check that the cert doesn't contain any SANs of unexpected types.
if len(parsedCert.EmailAddresses) != 0 || len(parsedCert.URIs) != 0 {
problems = append(problems, "Certificate contains SAN of unacceptable type (email or URI)")
}
if parsedCert.Subject.CommonName != "" {
// Check if the CommonName is <= 64 characters.
if len(parsedCert.Subject.CommonName) > 64 {
problems = append(
problems,
fmt.Sprintf("Certificate has common name >64 characters long (%d)", len(parsedCert.Subject.CommonName)),
)
}
// Check that the CommonName is included in the SANs.
if !slices.Contains(sans, parsedCert.Subject.CommonName) {
problems = append(problems, fmt.Sprintf("Certificate Common Name does not appear in Subject Alternative Names: %q !< %v",
parsedCert.Subject.CommonName, parsedCert.DNSNames))
}
}
// Check that the PA is still willing to issue for each DNS name and IP
// address in the SANs. We do not check the CommonName here, as (if it exists)
// we already checked that it is identical to one of the DNSNames in the SAN.
for _, name := range parsedCert.DNSNames {
err = c.pa.WillingToIssue(identifier.ACMEIdentifiers{identifier.NewDNS(name)})
if err != nil {
problems = append(problems, fmt.Sprintf("Policy Authority isn't willing to issue for '%s': %s", name, err))
continue
}
// For defense-in-depth, even if the PA was willing to issue for a name
// we double check it against a list of forbidden domains. This way even
// if the hostnamePolicyFile malfunctions we will flag the forbidden
// domain matches
if forbidden, pattern := isForbiddenDomain(name); forbidden {
problems = append(problems, fmt.Sprintf(
"Policy Authority was willing to issue but domain '%s' matches "+
"forbiddenDomains entry %q", name, pattern))
}
}
for _, name := range parsedCert.IPAddresses {
ip, ok := netip.AddrFromSlice(name)
if !ok {
problems = append(problems, fmt.Sprintf("SANs contain malformed IP %q", name))
continue
}
err = c.pa.WillingToIssue(identifier.ACMEIdentifiers{identifier.NewIP(ip)})
if err != nil {
problems = append(problems, fmt.Sprintf("Policy Authority isn't willing to issue for '%s': %s", name, err))
continue
}
}
// Check the cert has the correct key usage extensions
serverAndClient := slices.Equal(parsedCert.ExtKeyUsage, []zX509.ExtKeyUsage{zX509.ExtKeyUsageServerAuth, zX509.ExtKeyUsageClientAuth})
serverOnly := slices.Equal(parsedCert.ExtKeyUsage, []zX509.ExtKeyUsage{zX509.ExtKeyUsageServerAuth})
if !(serverAndClient || serverOnly) {
problems = append(problems, "Certificate has incorrect key usage extensions")
}
for _, ext := range parsedCert.Extensions {
_, ok := allowedExtensions[ext.Id.String()]
if !ok {
problems = append(problems, fmt.Sprintf("Certificate contains an unexpected extension: %s", ext.Id))
}
expectedContent, ok := expectedExtensionContent[ext.Id.String()]
if ok {
if !bytes.Equal(ext.Value, expectedContent) {
problems = append(problems, fmt.Sprintf("Certificate extension %s contains unexpected content: has %x, expected %x", ext.Id, ext.Value, expectedContent))
}
}
}
// Check that the cert has a good key. Note that this does not perform
// checks which rely on external resources such as weak or blocked key
// lists, or the list of blocked keys in the database. This only performs
// static checks, such as against the RSA key size and the ECDSA curve.
p, err := x509.ParseCertificate(cert.Der)
if err != nil {
problems = append(problems, fmt.Sprintf("Couldn't parse stored certificate: %s", err))
} else {
dnsNames = parsedCert.DNSNames
// Run zlint checks.
results := zlint.LintCertificate(parsedCert)
for name, res := range results.Results {
if ignoredLints[name] || res.Status <= lint.Pass {
continue
}
prob := fmt.Sprintf("zlint %s: %s", res.Status, name)
if res.Details != "" {
prob = fmt.Sprintf("%s %s", prob, res.Details)
}
problems = append(problems, prob)
}
// Check if stored serial is correct.
storedSerial, err := core.StringToSerial(cert.Serial)
if err != nil {
problems = append(problems, "Stored serial is invalid")
} else if parsedCert.SerialNumber.Cmp(storedSerial) != 0 {
problems = append(problems, "Stored serial doesn't match certificate serial")
}
// Check that we have the correct expiration time.
if !parsedCert.NotAfter.Equal(cert.Expires) {
problems = append(problems, "Stored expiration doesn't match certificate NotAfter")
}
// Check if basic constraints are set.
if !parsedCert.BasicConstraintsValid {
problems = append(problems, "Certificate doesn't have basic constraints set")
}
// Check that the cert isn't able to sign other certificates.
if parsedCert.IsCA {
problems = append(problems, "Certificate can sign other certificates")
}
// Check that the cert has a valid validity period. The validity
// period is computed inclusive of the whole final second indicated by
// notAfter.
validityDuration := parsedCert.NotAfter.Add(time.Second).Sub(parsedCert.NotBefore)
_, ok := c.acceptableValidityDurations[validityDuration]
if !ok {
problems = append(problems, "Certificate has unacceptable validity period")
}
// Check that the stored issuance time isn't too far back/forward dated.
if parsedCert.NotBefore.Before(cert.Issued.Add(-6*time.Hour)) || parsedCert.NotBefore.After(cert.Issued.Add(6*time.Hour)) {
problems = append(problems, "Stored issuance date is outside of 6 hour window of certificate NotBefore")
}
if parsedCert.Subject.CommonName != "" {
// Check if the CommonName is <= 64 characters.
if len(parsedCert.Subject.CommonName) > 64 {
problems = append(
problems,
fmt.Sprintf("Certificate has common name >64 characters long (%d)", len(parsedCert.Subject.CommonName)),
)
}
// Check that the CommonName is included in the SANs.
if !slices.Contains(parsedCert.DNSNames, parsedCert.Subject.CommonName) {
problems = append(problems, fmt.Sprintf("Certificate Common Name does not appear in Subject Alternative Names: %q !< %v",
parsedCert.Subject.CommonName, parsedCert.DNSNames))
}
}
// Check that the PA is still willing to issue for each name in DNSNames.
// We do not check the CommonName here, as (if it exists) we already checked
// that it is identical to one of the DNSNames in the SAN.
for _, name := range parsedCert.DNSNames {
err = c.pa.WillingToIssue([]string{name})
if err != nil {
problems = append(problems, fmt.Sprintf("Policy Authority isn't willing to issue for '%s': %s", name, err))
} else {
// For defense-in-depth, even if the PA was willing to issue for a name
// we double check it against a list of forbidden domains. This way even
// if the hostnamePolicyFile malfunctions we will flag the forbidden
// domain matches
if forbidden, pattern := isForbiddenDomain(name); forbidden {
problems = append(problems, fmt.Sprintf(
"Policy Authority was willing to issue but domain '%s' matches "+
"forbiddenDomains entry %q", name, pattern))
}
}
}
// Check the cert has the correct key usage extensions
if !slices.Equal(parsedCert.ExtKeyUsage, []zX509.ExtKeyUsage{zX509.ExtKeyUsageServerAuth, zX509.ExtKeyUsageClientAuth}) {
problems = append(problems, "Certificate has incorrect key usage extensions")
}
for _, ext := range parsedCert.Extensions {
_, ok := allowedExtensions[ext.Id.String()]
if !ok {
problems = append(problems, fmt.Sprintf("Certificate contains an unexpected extension: %s", ext.Id))
}
expectedContent, ok := expectedExtensionContent[ext.Id.String()]
if ok {
if !bytes.Equal(ext.Value, expectedContent) {
problems = append(problems, fmt.Sprintf("Certificate extension %s contains unexpected content: has %x, expected %x", ext.Id, ext.Value, expectedContent))
}
}
}
// Check that the cert has a good key. Note that this does not perform
// checks which rely on external resources such as weak or blocked key
// lists, or the list of blocked keys in the database. This only performs
// static checks, such as against the RSA key size and the ECDSA curve.
p, err := x509.ParseCertificate(cert.DER)
if err != nil {
problems = append(problems, fmt.Sprintf("Couldn't parse stored certificate: %s", err))
}
err = c.kp.GoodKey(ctx, p.PublicKey)
if err != nil {
problems = append(problems, fmt.Sprintf("Key Policy isn't willing to issue for public key: %s", err))
}
}
precertDER, err := c.getPrecert(ctx, cert.Serial)
precertDER, err := c.getPrecert(ctx, cert.Serial)
if err != nil {
// Log and continue, since we want the problems slice to only contains
// problems with the cert itself.
c.logger.Errf("fetching linting precertificate for %s: %s", cert.Serial, err)
atomic.AddInt64(&c.issuedReport.DbErrs, 1)
} else {
err = precert.Correspond(precertDER, cert.Der)
if err != nil {
// Log and continue, since we want the problems slice to only contains
// problems with the cert itself.
c.logger.Errf("fetching linting precertificate for %s: %s", cert.Serial, err)
atomic.AddInt64(&c.issuedReport.DbErrs, 1)
} else {
err = precert.Correspond(precertDER, cert.DER)
if err != nil {
problems = append(problems,
fmt.Sprintf("Certificate does not correspond to precert for %s: %s", cert.Serial, err))
}
problems = append(problems, fmt.Sprintf("Certificate does not correspond to precert for %s: %s", cert.Serial, err))
}
}
if features.Get().CertCheckerChecksValidations {
err = c.checkValidations(ctx, cert, parsedCert.DNSNames)
if err != nil {
if features.Get().CertCheckerRequiresValidations {
problems = append(problems, err.Error())
} else {
c.logger.Errf("Certificate %s %s: %s", cert.Serial, parsedCert.DNSNames, err)
if features.Get().CertCheckerChecksValidations {
idents := identifier.FromCert(p)
err = c.checkValidations(ctx, cert, idents)
if err != nil {
if features.Get().CertCheckerRequiresValidations {
problems = append(problems, err.Error())
} else {
var identValues []string
for _, ident := range idents {
identValues = append(identValues, ident.Value)
}
c.logger.Errf("Certificate %s %s: %s", cert.Serial, identValues, err)
}
}
}
return dnsNames, problems
return sans, problems
}
type Config struct {
@ -500,6 +546,9 @@ type Config struct {
// public keys in the certs it checks.
GoodKey goodkey.Config
// LintConfig is a path to a zlint config file, which can be used to control
// the behavior of zlint's "customizable lints".
LintConfig string
// IgnoredLints is a list of zlint names. Any lint results from a lint in
// the IgnoredLists list are ignored regardless of LintStatus level.
IgnoredLints []string
@ -546,13 +595,8 @@ func main() {
// Validate PA config and set defaults if needed.
cmd.FailOnError(config.PA.CheckChallenges(), "Invalid PA configuration")
cmd.FailOnError(config.PA.CheckIdentifiers(), "Invalid PA configuration")
if config.CertChecker.GoodKey.WeakKeyFile != "" {
cmd.Fail("cert-checker does not support checking against weak key files")
}
if config.CertChecker.GoodKey.BlockedKeyFile != "" {
cmd.Fail("cert-checker does not support checking against blocked key files")
}
kp, err := sagoodkey.NewPolicy(&config.CertChecker.GoodKey, nil)
cmd.FailOnError(err, "Unable to create key policy")
@ -565,7 +609,7 @@ func main() {
})
prometheus.DefaultRegisterer.MustRegister(checkerLatency)
pa, err := policy.New(config.PA.Challenges, logger)
pa, err := policy.New(config.PA.Identifiers, config.PA.Challenges, logger)
cmd.FailOnError(err, "Failed to create PA")
err = pa.LoadHostnamePolicyFile(config.CertChecker.HostnamePolicyFile)
@ -576,6 +620,14 @@ func main() {
cmd.FailOnError(err, "Failed to load CT Log List")
}
lints, err := linter.NewRegistry(config.CertChecker.IgnoredLints)
cmd.FailOnError(err, "Failed to create zlint registry")
if config.CertChecker.LintConfig != "" {
lintconfig, err := lint.NewConfigFromFile(config.CertChecker.LintConfig)
cmd.FailOnError(err, "Failed to load zlint config file")
lints.SetConfiguration(lintconfig)
}
checker := newChecker(
saDbMap,
cmd.Clock(),
@ -583,15 +635,11 @@ func main() {
kp,
config.CertChecker.CheckPeriod.Duration,
acceptableValidityDurations,
lints,
logger,
)
fmt.Fprintf(os.Stderr, "# Getting certificates issued in the last %s\n", config.CertChecker.CheckPeriod)
ignoredLintsMap := make(map[string]bool)
for _, name := range config.CertChecker.IgnoredLints {
ignoredLintsMap[name] = true
}
// Since we grab certificates in batches we don't want this to block, when it
// is finished it will close the certificate channel which allows the range
// loops in checker.processCerts to break
@ -606,7 +654,7 @@ func main() {
wg.Add(1)
go func() {
s := checker.clock.Now()
checker.processCerts(context.TODO(), wg, config.CertChecker.BadResultsOnly, ignoredLintsMap)
checker.processCerts(context.TODO(), wg, config.CertChecker.BadResultsOnly)
checkerLatency.Observe(checker.clock.Since(s).Seconds())
}()
}

View file

@ -15,10 +15,9 @@ import (
"errors"
"log"
"math/big"
mrand "math/rand"
mrand "math/rand/v2"
"os"
"slices"
"sort"
"strings"
"sync"
"testing"
@ -28,9 +27,12 @@ import (
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/letsencrypt/boulder/core"
corepb "github.com/letsencrypt/boulder/core/proto"
"github.com/letsencrypt/boulder/ctpolicy/loglist"
"github.com/letsencrypt/boulder/goodkey"
"github.com/letsencrypt/boulder/goodkey/sagoodkey"
"github.com/letsencrypt/boulder/identifier"
"github.com/letsencrypt/boulder/linter"
blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/metrics"
"github.com/letsencrypt/boulder/policy"
@ -51,7 +53,10 @@ var (
func init() {
var err error
pa, err = policy.New(map[core.AcmeChallenge]bool{}, blog.NewMock())
pa, err = policy.New(
map[identifier.IdentifierType]bool{identifier.TypeDNS: true, identifier.TypeIP: true},
map[core.AcmeChallenge]bool{},
blog.NewMock())
if err != nil {
log.Fatal(err)
}
@ -59,15 +64,15 @@ func init() {
if err != nil {
log.Fatal(err)
}
kp, err = sagoodkey.NewPolicy(&goodkey.Config{FermatRounds: 100}, nil)
kp, err = sagoodkey.NewPolicy(nil, nil)
if err != nil {
log.Fatal(err)
}
}
func BenchmarkCheckCert(b *testing.B) {
checker := newChecker(nil, clock.New(), pa, kp, time.Hour, testValidityDurations, blog.NewMock())
testKey, _ := rsa.GenerateKey(rand.Reader, 1024)
checker := newChecker(nil, clock.New(), pa, kp, time.Hour, testValidityDurations, nil, blog.NewMock())
testKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
expiry := time.Now().AddDate(0, 0, 1)
serial := big.NewInt(1337)
rawCert := x509.Certificate{
@ -79,16 +84,16 @@ func BenchmarkCheckCert(b *testing.B) {
SerialNumber: serial,
}
certDer, _ := x509.CreateCertificate(rand.Reader, &rawCert, &rawCert, &testKey.PublicKey, testKey)
cert := core.Certificate{
cert := &corepb.Certificate{
Serial: core.SerialToString(serial),
Digest: core.Fingerprint256(certDer),
DER: certDer,
Issued: time.Now(),
Expires: expiry,
Der: certDer,
Issued: timestamppb.New(time.Now()),
Expires: timestamppb.New(expiry),
}
b.ResetTimer()
for range b.N {
checker.checkCert(context.Background(), cert, nil)
checker.checkCert(context.Background(), cert)
}
}
@ -102,7 +107,7 @@ func TestCheckWildcardCert(t *testing.T) {
testKey, _ := rsa.GenerateKey(rand.Reader, 2048)
fc := clock.NewFake()
checker := newChecker(saDbMap, fc, pa, kp, time.Hour, testValidityDurations, blog.NewMock())
checker := newChecker(saDbMap, fc, pa, kp, time.Hour, testValidityDurations, nil, blog.NewMock())
issued := checker.clock.Now().Add(-time.Minute)
goodExpiry := issued.Add(testValidityDuration - time.Second)
serial := big.NewInt(1337)
@ -125,27 +130,27 @@ func TestCheckWildcardCert(t *testing.T) {
test.AssertNotError(t, err, "Couldn't create certificate")
parsed, err := x509.ParseCertificate(wildcardCertDer)
test.AssertNotError(t, err, "Couldn't parse created certificate")
cert := core.Certificate{
cert := &corepb.Certificate{
Serial: core.SerialToString(serial),
Digest: core.Fingerprint256(wildcardCertDer),
Expires: parsed.NotAfter,
Issued: parsed.NotBefore,
DER: wildcardCertDer,
Expires: timestamppb.New(parsed.NotAfter),
Issued: timestamppb.New(parsed.NotBefore),
Der: wildcardCertDer,
}
_, problems := checker.checkCert(context.Background(), cert, nil)
_, problems := checker.checkCert(context.Background(), cert)
for _, p := range problems {
t.Errorf(p)
t.Error(p)
}
}
func TestCheckCertReturnsDNSNames(t *testing.T) {
func TestCheckCertReturnsSANs(t *testing.T) {
saDbMap, err := sa.DBMapForTest(vars.DBConnSA)
test.AssertNotError(t, err, "Couldn't connect to database")
saCleanup := test.ResetBoulderTestDatabase(t)
defer func() {
saCleanup()
}()
checker := newChecker(saDbMap, clock.NewFake(), pa, kp, time.Hour, testValidityDurations, blog.NewMock())
checker := newChecker(saDbMap, clock.NewFake(), pa, kp, time.Hour, testValidityDurations, nil, blog.NewMock())
certPEM, err := os.ReadFile("testdata/quite_invalid.pem")
if err != nil {
@ -157,16 +162,16 @@ func TestCheckCertReturnsDNSNames(t *testing.T) {
t.Fatal("failed to parse cert PEM")
}
cert := core.Certificate{
cert := &corepb.Certificate{
Serial: "00000000000",
Digest: core.Fingerprint256(block.Bytes),
Expires: time.Now().Add(time.Hour),
Issued: time.Now(),
DER: block.Bytes,
Expires: timestamppb.New(time.Now().Add(time.Hour)),
Issued: timestamppb.New(time.Now()),
Der: block.Bytes,
}
names, problems := checker.checkCert(context.Background(), cert, nil)
if !slices.Equal(names, []string{"quite_invalid.com", "al--so--wr--ong.com"}) {
names, problems := checker.checkCert(context.Background(), cert)
if !slices.Equal(names, []string{"quite_invalid.com", "al--so--wr--ong.com", "127.0.0.1"}) {
t.Errorf("didn't get expected DNS names. other problems: %s", strings.Join(problems, "\n"))
}
}
@ -212,7 +217,7 @@ func TestCheckCert(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
testKey, _ := tc.key.genKey()
checker := newChecker(saDbMap, clock.NewFake(), pa, kp, time.Hour, testValidityDurations, blog.NewMock())
checker := newChecker(saDbMap, clock.NewFake(), pa, kp, time.Hour, testValidityDurations, nil, blog.NewMock())
// Create a RFC 7633 OCSP Must Staple Extension.
// OID 1.3.6.1.5.5.7.1.24
@ -262,14 +267,14 @@ func TestCheckCert(t *testing.T) {
// Serial doesn't match
// Expiry doesn't match
// Issued doesn't match
cert := core.Certificate{
cert := &corepb.Certificate{
Serial: "8485f2687eba29ad455ae4e31c8679206fec",
DER: brokenCertDer,
Issued: issued.Add(12 * time.Hour),
Expires: goodExpiry.AddDate(0, 0, 2), // Expiration doesn't match
Der: brokenCertDer,
Issued: timestamppb.New(issued.Add(12 * time.Hour)),
Expires: timestamppb.New(goodExpiry.AddDate(0, 0, 2)), // Expiration doesn't match
}
_, problems := checker.checkCert(context.Background(), cert, nil)
_, problems := checker.checkCert(context.Background(), cert)
problemsMap := map[string]int{
"Stored digest doesn't match certificate digest": 1,
@ -291,12 +296,12 @@ func TestCheckCert(t *testing.T) {
delete(problemsMap, p)
}
for k := range problemsMap {
t.Errorf("Expected problem but didn't find it: '%s'.", k)
t.Errorf("Expected problem but didn't find '%s' in problems: %q.", k, problems)
}
// Same settings as above, but the stored serial number in the DB is invalid.
cert.Serial = "not valid"
_, problems = checker.checkCert(context.Background(), cert, nil)
_, problems = checker.checkCert(context.Background(), cert)
foundInvalidSerialProblem := false
for _, p := range problems {
if p == "Stored serial is invalid" {
@ -318,10 +323,10 @@ func TestCheckCert(t *testing.T) {
test.AssertNotError(t, err, "Couldn't parse created certificate")
cert.Serial = core.SerialToString(serial)
cert.Digest = core.Fingerprint256(goodCertDer)
cert.DER = goodCertDer
cert.Expires = parsed.NotAfter
cert.Issued = parsed.NotBefore
_, problems = checker.checkCert(context.Background(), cert, nil)
cert.Der = goodCertDer
cert.Expires = timestamppb.New(parsed.NotAfter)
cert.Issued = timestamppb.New(parsed.NotBefore)
_, problems = checker.checkCert(context.Background(), cert)
test.AssertEquals(t, len(problems), 0)
})
}
@ -333,7 +338,7 @@ func TestGetAndProcessCerts(t *testing.T) {
fc := clock.NewFake()
fc.Set(fc.Now().Add(time.Hour))
checker := newChecker(saDbMap, fc, pa, kp, time.Hour, testValidityDurations, blog.NewMock())
checker := newChecker(saDbMap, fc, pa, kp, time.Hour, testValidityDurations, nil, blog.NewMock())
sa, err := sa.NewSQLStorageAuthority(saDbMap, saDbMap, nil, 1, 0, fc, blog.NewMock(), metrics.NoopRegisterer)
test.AssertNotError(t, err, "Couldn't create SA to insert certificates")
saCleanUp := test.ResetBoulderTestDatabase(t)
@ -341,7 +346,7 @@ func TestGetAndProcessCerts(t *testing.T) {
saCleanUp()
}()
testKey, _ := rsa.GenerateKey(rand.Reader, 1024)
testKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
// Problems
// Expiry period is too long
rawCert := x509.Certificate{
@ -355,7 +360,7 @@ func TestGetAndProcessCerts(t *testing.T) {
reg := satest.CreateWorkingRegistration(t, isa.SA{Impl: sa})
test.AssertNotError(t, err, "Couldn't create registration")
for range 5 {
rawCert.SerialNumber = big.NewInt(mrand.Int63())
rawCert.SerialNumber = big.NewInt(mrand.Int64())
certDER, err := x509.CreateCertificate(rand.Reader, &rawCert, &rawCert, &testKey.PublicKey, testKey)
test.AssertNotError(t, err, "Couldn't create certificate")
_, err = sa.AddCertificate(context.Background(), &sapb.AddCertificateRequest{
@ -372,7 +377,7 @@ func TestGetAndProcessCerts(t *testing.T) {
test.AssertEquals(t, len(checker.certs), 5)
wg := new(sync.WaitGroup)
wg.Add(1)
checker.processCerts(context.Background(), wg, false, nil)
checker.processCerts(context.Background(), wg, false)
test.AssertEquals(t, checker.issuedReport.BadCerts, int64(5))
test.AssertEquals(t, len(checker.issuedReport.Entries), 5)
}
@ -396,9 +401,6 @@ func (db mismatchedCountDB) SelectNullInt(_ context.Context, _ string, _ ...inte
// `getCerts` then calls `Select` to retrieve the Certificate rows. We pull
// a dastardly switch-a-roo here and return an empty set
func (db mismatchedCountDB) Select(_ context.Context, output interface{}, _ string, _ ...interface{}) ([]interface{}, error) {
// But actually return nothing
outputPtr, _ := output.(*[]sa.CertWithID)
*outputPtr = []sa.CertWithID{}
return nil, nil
}
@ -427,7 +429,7 @@ func (db mismatchedCountDB) SelectOne(_ context.Context, _ interface{}, _ string
func TestGetCertsEmptyResults(t *testing.T) {
saDbMap, err := sa.DBMapForTest(vars.DBConnSA)
test.AssertNotError(t, err, "Couldn't connect to database")
checker := newChecker(saDbMap, clock.NewFake(), pa, kp, time.Hour, testValidityDurations, blog.NewMock())
checker := newChecker(saDbMap, clock.NewFake(), pa, kp, time.Hour, testValidityDurations, nil, blog.NewMock())
checker.dbMap = mismatchedCountDB{}
batchSize = 3
@ -453,7 +455,7 @@ func (db emptyDB) SelectNullInt(_ context.Context, _ string, _ ...interface{}) (
// expected if the DB finds no certificates to match the SELECT query and
// should return an error.
func TestGetCertsNullResults(t *testing.T) {
checker := newChecker(emptyDB{}, clock.NewFake(), pa, kp, time.Hour, testValidityDurations, blog.NewMock())
checker := newChecker(emptyDB{}, clock.NewFake(), pa, kp, time.Hour, testValidityDurations, nil, blog.NewMock())
err := checker.getCerts(context.Background())
test.AssertError(t, err, "Should have gotten error from empty DB")
@ -497,7 +499,7 @@ func TestGetCertsLate(t *testing.T) {
clk := clock.NewFake()
db := &lateDB{issuedTime: clk.Now().Add(-time.Hour)}
checkPeriod := 24 * time.Hour
checker := newChecker(db, clk, pa, kp, checkPeriod, testValidityDurations, blog.NewMock())
checker := newChecker(db, clk, pa, kp, checkPeriod, testValidityDurations, nil, blog.NewMock())
err := checker.getCerts(context.Background())
test.AssertNotError(t, err, "getting certs")
@ -582,21 +584,22 @@ func TestIgnoredLint(t *testing.T) {
err = loglist.InitLintList("../../test/ct-test-srv/log_list.json")
test.AssertNotError(t, err, "failed to load ct log list")
testKey, _ := rsa.GenerateKey(rand.Reader, 2048)
checker := newChecker(saDbMap, clock.NewFake(), pa, kp, time.Hour, testValidityDurations, blog.NewMock())
checker := newChecker(saDbMap, clock.NewFake(), pa, kp, time.Hour, testValidityDurations, nil, blog.NewMock())
serial := big.NewInt(1337)
x509OID, err := x509.OIDFromInts([]uint64{1, 2, 3})
test.AssertNotError(t, err, "failed to create x509.OID")
template := &x509.Certificate{
Subject: pkix.Name{
CommonName: "CPU's Cool CA",
},
SerialNumber: serial,
NotBefore: time.Now(),
NotAfter: time.Now().Add(testValidityDuration - time.Second),
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
PolicyIdentifiers: []asn1.ObjectIdentifier{
{1, 2, 3},
},
SerialNumber: serial,
NotBefore: time.Now(),
NotAfter: time.Now().Add(testValidityDuration - time.Second),
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
Policies: []x509.OID{x509OID},
BasicConstraintsValid: true,
IsCA: true,
IssuingCertificateURL: []string{"http://aia.example.org"},
@ -623,43 +626,46 @@ func TestIgnoredLint(t *testing.T) {
subjectCert, err := x509.ParseCertificate(subjectCertDer)
test.AssertNotError(t, err, "failed to parse EE cert")
cert := core.Certificate{
cert := &corepb.Certificate{
Serial: core.SerialToString(serial),
DER: subjectCertDer,
Der: subjectCertDer,
Digest: core.Fingerprint256(subjectCertDer),
Issued: subjectCert.NotBefore,
Expires: subjectCert.NotAfter,
Issued: timestamppb.New(subjectCert.NotBefore),
Expires: timestamppb.New(subjectCert.NotAfter),
}
// Without any ignored lints we expect one error level result due to the
// missing OCSP url in the template.
// Without any ignored lints we expect several errors and warnings about SCTs,
// the common name, and the subject key identifier extension.
expectedProblems := []string{
"zlint error: e_sub_cert_aia_does_not_contain_ocsp_url",
"zlint warn: w_subject_common_name_included",
"zlint warn: w_ext_subject_key_identifier_not_recommended_subscriber",
"zlint info: w_ct_sct_policy_count_unsatisfied Certificate had 0 embedded SCTs. Browser policy may require 2 for this certificate.",
"zlint error: e_scts_from_same_operator Certificate had too few embedded SCTs; browser policy requires 2.",
}
sort.Strings(expectedProblems)
slices.Sort(expectedProblems)
// Check the certificate with a nil ignore map. This should return the
// expected zlint problems.
_, problems := checker.checkCert(context.Background(), cert, nil)
sort.Strings(problems)
_, problems := checker.checkCert(context.Background(), cert)
slices.Sort(problems)
test.AssertDeepEquals(t, problems, expectedProblems)
// Check the certificate again with an ignore map that excludes the affected
// lints. This should return no problems.
_, problems = checker.checkCert(context.Background(), cert, map[string]bool{
"e_sub_cert_aia_does_not_contain_ocsp_url": true,
"w_subject_common_name_included": true,
"w_ct_sct_policy_count_unsatisfied": true,
"e_scts_from_same_operator": true,
lints, err := linter.NewRegistry([]string{
"w_subject_common_name_included",
"w_ext_subject_key_identifier_not_recommended_subscriber",
"w_ct_sct_policy_count_unsatisfied",
"e_scts_from_same_operator",
})
test.AssertNotError(t, err, "creating test lint registry")
checker.lints = lints
_, problems = checker.checkCert(context.Background(), cert)
test.AssertEquals(t, len(problems), 0)
}
func TestPrecertCorrespond(t *testing.T) {
checker := newChecker(nil, clock.New(), pa, kp, time.Hour, testValidityDurations, blog.NewMock())
checker := newChecker(nil, clock.New(), pa, kp, time.Hour, testValidityDurations, nil, blog.NewMock())
checker.getPrecert = func(_ context.Context, _ string) ([]byte, error) {
return []byte("hello"), nil
}
@ -675,14 +681,14 @@ func TestPrecertCorrespond(t *testing.T) {
SerialNumber: serial,
}
certDer, _ := x509.CreateCertificate(rand.Reader, &rawCert, &rawCert, &testKey.PublicKey, testKey)
cert := core.Certificate{
cert := &corepb.Certificate{
Serial: core.SerialToString(serial),
Digest: core.Fingerprint256(certDer),
DER: certDer,
Issued: time.Now(),
Expires: expiry,
Der: certDer,
Issued: timestamppb.New(time.Now()),
Expires: timestamppb.New(expiry),
}
_, problems := checker.checkCert(context.Background(), cert, nil)
_, problems := checker.checkCert(context.Background(), cert)
if len(problems) == 0 {
t.Errorf("expected precert correspondence problem")
}

View file

@ -1,5 +1,5 @@
-----BEGIN CERTIFICATE-----
MIIDUzCCAjugAwIBAgIILgLqdMwyzT4wDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE
MIIDWTCCAkGgAwIBAgIILgLqdMwyzT4wDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE
AxMVbWluaWNhIHJvb3QgY2EgOTMzZTM5MB4XDTIxMTExMTIwMjMzMloXDTIzMTIx
MTIwMjMzMlowHDEaMBgGA1UEAwwRcXVpdGVfaW52YWxpZC5jb20wggEiMA0GCSqG
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDi4jBbqMyvhMonDngNsvie9SHPB16mdpiy
@ -7,14 +7,14 @@ Y/agreU84xUz/roKK07TpVmeqvwWvDkvHTFov7ytKdnCY+z/NXKJ3hNqflWCwU7h
Uk9TmpBp0vg+5NvalYul/+bq/B4qDhEvTBzAX3k/UYzd0GQdMyAbwXtG41f5cSK6
cWTQYfJL3gGR5/KLoTz3/VemLgEgAP/CvgcUJPbQceQViiZ4opi9hFIfUqxX2NsD
49klw8cDFu/BG2LEC+XtbdT8XevD0aGIOuYVr+Pa2mxb2QCDXu4tXOsDXH9Y/Cmk
8103QbdB8Y+usOiHG/IXxK2q4J7QNPal4ER4/PGA06V0gwrjNH8BAgMBAAGjgZQw
gZEwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcD
8103QbdB8Y+usOiHG/IXxK2q4J7QNPal4ER4/PGA06V0gwrjNH8BAgMBAAGjgZow
gZcwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcD
AjAMBgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaAFNIcaCjv32YRafE065dZO57ONWuk
MDEGA1UdEQQqMCiCEXF1aXRlX2ludmFsaWQuY29tghNhbC0tc28tLXdyLS1vbmcu
Y29tMA0GCSqGSIb3DQEBCwUAA4IBAQAjSv0o5G4VuLnnwHON4P53bLvGnYqaqYju
TEafi3hSgHAfBuhOQUVgwujoYpPp1w1fm5spfcbSwNNRte79HgV97kAuZ4R4RHk1
5Xux1ITLalaHR/ilu002N0eJ7dFYawBgV2xMudULzohwmW2RjPJ5811iWwtiVf1b
A3V5SZJWSJll1BhANBs7R0pBbyTSNHR470N8TGG0jfXqgTKd0xZaH91HrwEMo+96
llbfp90Y5OfHIfym/N1sH2hVgd+ZAkhiVEiNBWZlbSyOgbZ1cCBvBXg6TuwpQMZK
9RWjlpni8yuzLGduPl8qHG1dqsUvbVqcG+WhHLbaZMNhiMfiWInL
MDcGA1UdEQQwMC6CEXF1aXRlX2ludmFsaWQuY29tghNhbC0tc28tLXdyLS1vbmcu
Y29thwR/AAABMA0GCSqGSIb3DQEBCwUAA4IBAQAjSv0o5G4VuLnnwHON4P53bLvG
nYqaqYjuTEafi3hSgHAfBuhOQUVgwujoYpPp1w1fm5spfcbSwNNRte79HgV97kAu
Z4R4RHk15Xux1ITLalaHR/ilu002N0eJ7dFYawBgV2xMudULzohwmW2RjPJ5811i
WwtiVf1bA3V5SZJWSJll1BhANBs7R0pBbyTSNHR470N8TGG0jfXqgTKd0xZaH91H
rwEMo+96llbfp90Y5OfHIfym/N1sH2hVgd+ZAkhiVEiNBWZlbSyOgbZ1cCBvBXg6
TuwpQMZK9RWjlpni8yuzLGduPl8qHG1dqsUvbVqcG+WhHLbaZMNhiMfiWInL
-----END CERTIFICATE-----

View file

@ -24,7 +24,7 @@ func Clock() clock.Clock {
cl := clock.NewFake()
cl.Set(targetTime)
blog.Get().Infof("Time was set to %v via FAKECLOCK", targetTime)
blog.Get().Debugf("Time was set to %v via FAKECLOCK", targetTime)
return cl
}

View file

@ -3,6 +3,7 @@ package cmd
import (
"crypto/tls"
"crypto/x509"
"encoding/hex"
"errors"
"fmt"
"net"
@ -15,6 +16,7 @@ import (
"github.com/letsencrypt/boulder/config"
"github.com/letsencrypt/boulder/core"
"github.com/letsencrypt/boulder/identifier"
)
// PasswordConfig contains a path to a file containing a password.
@ -87,6 +89,8 @@ func (d *DBConfig) URL() (string, error) {
return strings.TrimSpace(string(url)), err
}
// SMTPConfig is deprecated.
// TODO(#8199): Delete this when it is removed from bad-key-revoker's config.
type SMTPConfig struct {
PasswordConfig
Server string `validate:"required"`
@ -98,8 +102,9 @@ type SMTPConfig struct {
// database, what policies it should enforce, and what challenges
// it should offer.
type PAConfig struct {
DBConfig `validate:"-"`
Challenges map[core.AcmeChallenge]bool `validate:"omitempty,dive,keys,oneof=http-01 dns-01 tls-alpn-01,endkeys"`
DBConfig `validate:"-"`
Challenges map[core.AcmeChallenge]bool `validate:"omitempty,dive,keys,oneof=http-01 dns-01 tls-alpn-01,endkeys"`
Identifiers map[identifier.IdentifierType]bool `validate:"omitempty,dive,keys,oneof=dns ip,endkeys"`
}
// CheckChallenges checks whether the list of challenges in the PA config
@ -116,6 +121,17 @@ func (pc PAConfig) CheckChallenges() error {
return nil
}
// CheckIdentifiers checks whether the list of identifiers in the PA config
// actually contains valid identifier type names
func (pc PAConfig) CheckIdentifiers() error {
for i := range pc.Identifiers {
if !i.IsValid() {
return fmt.Errorf("invalid identifier type in PA config: %s", i)
}
}
return nil
}
// HostnamePolicyConfig specifies a file from which to load a policy regarding
// what hostnames to issue for.
type HostnamePolicyConfig struct {
@ -283,7 +299,7 @@ type GRPCClientConfig struct {
// If you've added the above to your Consul configuration file (and reloaded
// Consul) then you should be able to resolve the following dig query:
//
// $ dig @10.55.55.10 -t SRV _foo._tcp.service.consul +short
// $ dig @10.77.77.10 -t SRV _foo._tcp.service.consul +short
// 1 1 8080 0a585858.addr.dc1.consul.
// 1 1 8080 0a4d4d4d.addr.dc1.consul.
SRVLookup *ServiceDomain `validate:"required_without_all=SRVLookups ServerAddress ServerIPAddresses"`
@ -323,7 +339,7 @@ type GRPCClientConfig struct {
// If you've added the above to your Consul configuration file (and reloaded
// Consul) then you should be able to resolve the following dig query:
//
// $ dig A @10.55.55.10 foo.service.consul +short
// $ dig A @10.77.77.10 foo.service.consul +short
// 10.77.77.77
// 10.88.88.88
ServerAddress string `validate:"required_without_all=ServerIPAddresses SRVLookup SRVLookups,omitempty,hostname_port"`
@ -449,7 +465,7 @@ type GRPCServerConfig struct {
// These service names must match the service names advertised by gRPC itself,
// which are identical to the names set in our gRPC .proto files prefixed by
// the package names set in those files (e.g. "ca.CertificateAuthority").
Services map[string]GRPCServiceConfig `json:"services" validate:"required,dive,required"`
Services map[string]*GRPCServiceConfig `json:"services" validate:"required,dive,required"`
// MaxConnectionAge specifies how long a connection may live before the server sends a GoAway to the
// client. Because gRPC connections re-resolve DNS after a connection close,
// this controls how long it takes before a client learns about changes to its
@ -460,10 +476,10 @@ type GRPCServerConfig struct {
// GRPCServiceConfig contains the information needed to configure a gRPC service.
type GRPCServiceConfig struct {
// PerServiceClientNames is a map of gRPC service names to client certificate
// SANs. The upstream listening server will reject connections from clients
// which do not appear in this list, and the server interceptor will reject
// RPC calls for this service from clients which are not listed here.
// ClientNames is the list of accepted gRPC client certificate SANs.
// Connections from clients not in this list will be rejected by the
// upstream listener, and RPCs from unlisted clients will be denied by the
// server interceptor.
ClientNames []string `json:"clientNames" validate:"min=1,dive,hostname,required"`
}
@ -548,8 +564,38 @@ type DNSProvider struct {
// If you've added the above to your Consul configuration file (and reloaded
// Consul) then you should be able to resolve the following dig query:
//
// $ dig @10.55.55.10 -t SRV _unbound._udp.service.consul +short
// $ dig @10.77.77.10 -t SRV _unbound._udp.service.consul +short
// 1 1 8053 0a4d4d4d.addr.dc1.consul.
// 1 1 8153 0a4d4d4d.addr.dc1.consul.
SRVLookup ServiceDomain `validate:"required"`
}
// HMACKeyConfig specifies a path to a file containing a hexadecimal-encoded
// HMAC key. The key must represent exactly 256 bits (32 bytes) of random data
// to be suitable for use as a 256-bit hashing key (e.g., the output of `openssl
// rand -hex 32`).
type HMACKeyConfig struct {
KeyFile string `validate:"required"`
}
// Load reads the HMAC key from the file, decodes it from hexadecimal, ensures
// it represents exactly 256 bits (32 bytes), and returns it as a byte slice.
func (hc *HMACKeyConfig) Load() ([]byte, error) {
contents, err := os.ReadFile(hc.KeyFile)
if err != nil {
return nil, err
}
decoded, err := hex.DecodeString(strings.TrimSpace(string(contents)))
if err != nil {
return nil, fmt.Errorf("invalid hexadecimal encoding: %w", err)
}
if len(decoded) != 32 {
return nil, fmt.Errorf(
"validating HMAC key, must be exactly 256 bits (32 bytes) after decoding, got %d",
len(decoded),
)
}
return decoded, nil
}

View file

@ -136,3 +136,58 @@ func TestTLSConfigLoad(t *testing.T) {
})
}
}
func TestHMACKeyConfigLoad(t *testing.T) {
t.Parallel()
tests := []struct {
name string
content string
expectedErr bool
}{
{
name: "Valid key",
content: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
expectedErr: false,
},
{
name: "Empty file",
content: "",
expectedErr: true,
},
{
name: "Just under 256-bit",
content: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789ab",
expectedErr: true,
},
{
name: "Just over 256-bit",
content: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef01",
expectedErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
tempKeyFile, err := os.CreateTemp("", "*")
if err != nil {
t.Fatalf("failed to create temp file: %v", err)
}
defer os.Remove(tempKeyFile.Name())
_, err = tempKeyFile.WriteString(tt.content)
if err != nil {
t.Fatalf("failed to write to temp file: %v", err)
}
tempKeyFile.Close()
hmacKeyConfig := HMACKeyConfig{KeyFile: tempKeyFile.Name()}
_, err = hmacKeyConfig.Load()
if (err != nil) != tt.expectedErr {
t.Errorf("expected error: %v, got: %v", tt.expectedErr, err)
}
})
}
}

View file

@ -1,84 +0,0 @@
# Contact-Auditor
Audits subscriber registrations for e-mail addresses that
`notify-mailer` is currently configured to skip.
# Usage:
```shell
-config string
File containing a JSON config.
-to-file
Write the audit results to a file.
-to-stdout
Print the audit results to stdout.
```
## Results format:
```
<id> <createdAt> <problem type> "<contact contents or entry>" "<error msg>"
```
## Example output:
### Successful run with no violations encountered and `--to-file`:
```
I004823 contact-auditor nfWK_gM Running contact-auditor
I004823 contact-auditor qJ_zsQ4 Beginning database query
I004823 contact-auditor je7V9QM Query completed successfully
I004823 contact-auditor 7LzGvQI Audit finished successfully
I004823 contact-auditor 5Pbk_QM Audit results were written to: audit-2006-01-02T15:04.tsv
```
### Contact contains entries that violate policy and `--to-stdout`:
```
I004823 contact-auditor nfWK_gM Running contact-auditor
I004823 contact-auditor qJ_zsQ4 Beginning database query
I004823 contact-auditor je7V9QM Query completed successfully
1 2006-01-02 15:04:05 validation "<contact entry>" "<error msg>"
...
I004823 contact-auditor 2fv7-QY Audit finished successfully
```
### Contact is not valid JSON and `--to-stdout`:
```
I004823 contact-auditor nfWK_gM Running contact-auditor
I004823 contact-auditor qJ_zsQ4 Beginning database query
I004823 contact-auditor je7V9QM Query completed successfully
3 2006-01-02 15:04:05 unmarshal "<contact contents>" "<error msg>"
...
I004823 contact-auditor 2fv7-QY Audit finished successfully
```
### Audit incomplete, query ended prematurely:
```
I004823 contact-auditor nfWK_gM Running contact-auditor
I004823 contact-auditor qJ_zsQ4 Beginning database query
...
E004823 contact-auditor 8LmTgww [AUDIT] Audit was interrupted, results may be incomplete: <error msg>
exit status 1
```
# Configuration file:
The path to a database config file like the one below must be provided
following the `-config` flag.
```json
{
"contactAuditor": {
"db": {
"dbConnectFile": <string>,
"maxOpenConns": <int>,
"maxIdleConns": <int>,
"connMaxLifetime": <int>,
"connMaxIdleTime": <int>
}
}
}
```

View file

@ -1,212 +0,0 @@
package notmain
import (
"context"
"database/sql"
"encoding/json"
"errors"
"flag"
"fmt"
"os"
"strings"
"time"
"github.com/letsencrypt/boulder/cmd"
"github.com/letsencrypt/boulder/db"
blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/policy"
"github.com/letsencrypt/boulder/sa"
)
type contactAuditor struct {
db *db.WrappedMap
resultsFile *os.File
writeToStdout bool
logger blog.Logger
}
type result struct {
id int64
contacts []string
createdAt string
}
func unmarshalContact(contact []byte) ([]string, error) {
var contacts []string
err := json.Unmarshal(contact, &contacts)
if err != nil {
return nil, err
}
return contacts, nil
}
func validateContacts(id int64, createdAt string, contacts []string) error {
// Setup a buffer to store any validation problems we encounter.
var probsBuff strings.Builder
// Helper to write validation problems to our buffer.
writeProb := func(contact string, prob string) {
// Add validation problem to buffer.
fmt.Fprintf(&probsBuff, "%d\t%s\tvalidation\t%q\t%q\t%q\n", id, createdAt, contact, prob, contacts)
}
for _, contact := range contacts {
if strings.HasPrefix(contact, "mailto:") {
err := policy.ValidEmail(strings.TrimPrefix(contact, "mailto:"))
if err != nil {
writeProb(contact, err.Error())
}
} else {
writeProb(contact, "missing 'mailto:' prefix")
}
}
if probsBuff.Len() != 0 {
return errors.New(probsBuff.String())
}
return nil
}
// beginAuditQuery executes the audit query and returns a cursor used to
// stream the results.
func (c contactAuditor) beginAuditQuery(ctx context.Context) (*sql.Rows, error) {
rows, err := c.db.QueryContext(ctx, `
SELECT DISTINCT id, contact, createdAt
FROM registrations
WHERE contact NOT IN ('[]', 'null');`)
if err != nil {
return nil, err
}
return rows, nil
}
func (c contactAuditor) writeResults(result string) {
if c.writeToStdout {
_, err := fmt.Print(result)
if err != nil {
c.logger.Errf("Error while writing result to stdout: %s", err)
}
}
if c.resultsFile != nil {
_, err := c.resultsFile.WriteString(result)
if err != nil {
c.logger.Errf("Error while writing result to file: %s", err)
}
}
}
// run retrieves a cursor from `beginAuditQuery` and then audits the
// `contact` column of all returned rows for abnormalities or policy
// violations.
func (c contactAuditor) run(ctx context.Context, resChan chan *result) error {
c.logger.Infof("Beginning database query")
rows, err := c.beginAuditQuery(ctx)
if err != nil {
return err
}
for rows.Next() {
var id int64
var contact []byte
var createdAt string
err := rows.Scan(&id, &contact, &createdAt)
if err != nil {
return err
}
contacts, err := unmarshalContact(contact)
if err != nil {
c.writeResults(fmt.Sprintf("%d\t%s\tunmarshal\t%q\t%q\n", id, createdAt, contact, err))
}
err = validateContacts(id, createdAt, contacts)
if err != nil {
c.writeResults(err.Error())
}
// Only used for testing.
if resChan != nil {
resChan <- &result{id, contacts, createdAt}
}
}
// Ensure the query wasn't interrupted before it could complete.
err = rows.Close()
if err != nil {
return err
} else {
c.logger.Info("Query completed successfully")
}
// Only used for testing.
if resChan != nil {
close(resChan)
}
return nil
}
type Config struct {
ContactAuditor struct {
DB cmd.DBConfig
}
}
func main() {
configFile := flag.String("config", "", "File containing a JSON config.")
writeToStdout := flag.Bool("to-stdout", false, "Print the audit results to stdout.")
writeToFile := flag.Bool("to-file", false, "Write the audit results to a file.")
flag.Parse()
logger := cmd.NewLogger(cmd.SyslogConfig{StdoutLevel: 7})
logger.Info(cmd.VersionString())
if *configFile == "" {
flag.Usage()
os.Exit(1)
}
// Load config from JSON.
configData, err := os.ReadFile(*configFile)
cmd.FailOnError(err, fmt.Sprintf("Error reading config file: %q", *configFile))
var cfg Config
err = json.Unmarshal(configData, &cfg)
cmd.FailOnError(err, "Couldn't unmarshal config")
db, err := sa.InitWrappedDb(cfg.ContactAuditor.DB, nil, logger)
cmd.FailOnError(err, "Couldn't setup database client")
var resultsFile *os.File
if *writeToFile {
resultsFile, err = os.Create(
fmt.Sprintf("contact-audit-%s.tsv", time.Now().Format("2006-01-02T15:04")),
)
cmd.FailOnError(err, "Failed to create results file")
}
// Setup and run contact-auditor.
auditor := contactAuditor{
db: db,
resultsFile: resultsFile,
writeToStdout: *writeToStdout,
logger: logger,
}
logger.Info("Running contact-auditor")
err = auditor.run(context.TODO(), nil)
cmd.FailOnError(err, "Audit was interrupted, results may be incomplete")
logger.Info("Audit finished successfully")
if *writeToFile {
logger.Infof("Audit results were written to: %s", resultsFile.Name())
resultsFile.Close()
}
}
func init() {
cmd.RegisterCommand("contact-auditor", main, &cmd.ConfigValidator{Config: &Config{}})
}

View file

@ -1,219 +0,0 @@
package notmain
import (
"context"
"fmt"
"net"
"os"
"strings"
"testing"
"time"
"github.com/jmhodges/clock"
corepb "github.com/letsencrypt/boulder/core/proto"
"github.com/letsencrypt/boulder/db"
blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/metrics"
"github.com/letsencrypt/boulder/sa"
"github.com/letsencrypt/boulder/test"
"github.com/letsencrypt/boulder/test/vars"
)
var (
regA *corepb.Registration
regB *corepb.Registration
regC *corepb.Registration
regD *corepb.Registration
)
const (
emailARaw = "test@example.com"
emailBRaw = "example@notexample.com"
emailCRaw = "test-example@notexample.com"
telNum = "666-666-7777"
)
func TestContactAuditor(t *testing.T) {
testCtx := setup(t)
defer testCtx.cleanUp()
// Add some test registrations.
testCtx.addRegistrations(t)
resChan := make(chan *result, 10)
err := testCtx.c.run(context.Background(), resChan)
test.AssertNotError(t, err, "received error")
// We should get back A, B, C, and D
test.AssertEquals(t, len(resChan), 4)
for entry := range resChan {
err := validateContacts(entry.id, entry.createdAt, entry.contacts)
switch entry.id {
case regA.Id:
// Contact validation policy sad path.
test.AssertDeepEquals(t, entry.contacts, []string{"mailto:test@example.com"})
test.AssertError(t, err, "failed to error on a contact that violates our e-mail policy")
case regB.Id:
// Ensure grace period was respected.
test.AssertDeepEquals(t, entry.contacts, []string{"mailto:example@notexample.com"})
test.AssertNotError(t, err, "received error for a valid contact entry")
case regC.Id:
// Contact validation happy path.
test.AssertDeepEquals(t, entry.contacts, []string{"mailto:test-example@notexample.com"})
test.AssertNotError(t, err, "received error for a valid contact entry")
// Unmarshal Contact sad path.
_, err := unmarshalContact([]byte("[ mailto:test@example.com ]"))
test.AssertError(t, err, "failed to error while unmarshaling invalid Contact JSON")
// Fix our JSON and ensure that the contact field returns
// errors for our 2 additional contacts
contacts, err := unmarshalContact([]byte(`[ "mailto:test@example.com", "tel:666-666-7777" ]`))
test.AssertNotError(t, err, "received error while unmarshaling valid Contact JSON")
// Ensure Contact validation now fails.
err = validateContacts(entry.id, entry.createdAt, contacts)
test.AssertError(t, err, "failed to error on 2 invalid Contact entries")
case regD.Id:
test.AssertDeepEquals(t, entry.contacts, []string{"tel:666-666-7777"})
test.AssertError(t, err, "failed to error on an invalid contact entry")
default:
t.Errorf("ID: %d was not expected", entry.id)
}
}
// Load results file.
data, err := os.ReadFile(testCtx.c.resultsFile.Name())
if err != nil {
t.Error(err)
}
// Results file should contain 2 newlines, 1 for each result.
contentLines := strings.Split(strings.TrimRight(string(data), "\n"), "\n")
test.AssertEquals(t, len(contentLines), 2)
// Each result entry should contain six tab separated columns.
for _, line := range contentLines {
test.AssertEquals(t, len(strings.Split(line, "\t")), 6)
}
}
type testCtx struct {
c contactAuditor
dbMap *db.WrappedMap
ssa *sa.SQLStorageAuthority
cleanUp func()
}
func (tc testCtx) addRegistrations(t *testing.T) {
emailA := "mailto:" + emailARaw
emailB := "mailto:" + emailBRaw
emailC := "mailto:" + emailCRaw
tel := "tel:" + telNum
// Every registration needs a unique JOSE key
jsonKeyA := []byte(`{
"kty":"RSA",
"n":"0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw",
"e":"AQAB"
}`)
jsonKeyB := []byte(`{
"kty":"RSA",
"n":"z8bp-jPtHt4lKBqepeKF28g_QAEOuEsCIou6sZ9ndsQsEjxEOQxQ0xNOQezsKa63eogw8YS3vzjUcPP5BJuVzfPfGd5NVUdT-vSSwxk3wvk_jtNqhrpcoG0elRPQfMVsQWmxCAXCVRz3xbcFI8GTe-syynG3l-g1IzYIIZVNI6jdljCZML1HOMTTW4f7uJJ8mM-08oQCeHbr5ejK7O2yMSSYxW03zY-Tj1iVEebROeMv6IEEJNFSS4yM-hLpNAqVuQxFGetwtwjDMC1Drs1dTWrPuUAAjKGrP151z1_dE74M5evpAhZUmpKv1hY-x85DC6N0hFPgowsanmTNNiV75w",
"e":"AAEAAQ"
}`)
jsonKeyC := []byte(`{
"kty":"RSA",
"n":"rFH5kUBZrlPj73epjJjyCxzVzZuV--JjKgapoqm9pOuOt20BUTdHqVfC2oDclqM7HFhkkX9OSJMTHgZ7WaVqZv9u1X2yjdx9oVmMLuspX7EytW_ZKDZSzL-sCOFCuQAuYKkLbsdcA3eHBK_lwc4zwdeHFMKIulNvLqckkqYB9s8GpgNXBDIQ8GjR5HuJke_WUNjYHSd8jY1LU9swKWsLQe2YoQUz_ekQvBvBCoaFEtrtRaSJKNLIVDObXFr2TLIiFiM0Em90kK01-eQ7ZiruZTKomll64bRFPoNo4_uwubddg3xTqur2vdF3NyhTrYdvAgTem4uC0PFjEQ1bK_djBQ",
"e":"AQAB"
}`)
jsonKeyD := []byte(`{
"kty":"RSA",
"n":"rFH5kUBZrlPj73epjJjyCxzVzZuV--JjKgapoqm9pOuOt20BUTdHqVfC2oDclqM7HFhkkX9OSJMTHgZ7WaVqZv9u1X2yjdx9oVmMLuspX7EytW_ZKDZSzL-FCOFCuQAuYKkLbsdcA3eHBK_lwc4zwdeHFMKIulNvLqckkqYB9s8GpgNXBDIQ8GjR5HuJke_WUNjYHSd8jY1LU9swKWsLQe2YoQUz_ekQvBvBCoaFEtrtRaSJKNLIVDObXFr2TLIiFiM0Em90kK01-eQ7ZiruZTKomll64bRFPoNo4_uwubddg3xTqur2vdF3NyhTrYdvAgTem4uC0PFjEQ1bK_djBQ",
"e":"AQAB"
}`)
initialIP, err := net.ParseIP("127.0.0.1").MarshalText()
test.AssertNotError(t, err, "Couldn't create initialIP")
regA = &corepb.Registration{
Id: 1,
Contact: []string{emailA},
Key: jsonKeyA,
InitialIP: initialIP,
}
regB = &corepb.Registration{
Id: 2,
Contact: []string{emailB},
Key: jsonKeyB,
InitialIP: initialIP,
}
regC = &corepb.Registration{
Id: 3,
Contact: []string{emailC},
Key: jsonKeyC,
InitialIP: initialIP,
}
// Reg D has a `tel:` contact ACME URL
regD = &corepb.Registration{
Id: 4,
Contact: []string{tel},
Key: jsonKeyD,
InitialIP: initialIP,
}
// Add the four test registrations
ctx := context.Background()
regA, err = tc.ssa.NewRegistration(ctx, regA)
test.AssertNotError(t, err, "Couldn't store regA")
regB, err = tc.ssa.NewRegistration(ctx, regB)
test.AssertNotError(t, err, "Couldn't store regB")
regC, err = tc.ssa.NewRegistration(ctx, regC)
test.AssertNotError(t, err, "Couldn't store regC")
regD, err = tc.ssa.NewRegistration(ctx, regD)
test.AssertNotError(t, err, "Couldn't store regD")
}
func setup(t *testing.T) testCtx {
log := blog.UseMock()
// Using DBConnSAFullPerms to be able to insert registrations and
// certificates
dbMap, err := sa.DBMapForTest(vars.DBConnSAFullPerms)
if err != nil {
t.Fatalf("Couldn't connect to the database: %s", err)
}
// Make temp results file
file, err := os.CreateTemp("", fmt.Sprintf("audit-%s", time.Now().Format("2006-01-02T15:04")))
if err != nil {
t.Fatal(err)
}
cleanUp := func() {
test.ResetBoulderTestDatabase(t)
file.Close()
os.Remove(file.Name())
}
db, err := sa.DBMapForTest(vars.DBConnSAMailer)
if err != nil {
t.Fatalf("Couldn't connect to the database: %s", err)
}
ssa, err := sa.NewSQLStorageAuthority(dbMap, dbMap, nil, 1, 0, clock.New(), log, metrics.NoopRegisterer)
if err != nil {
t.Fatalf("unable to create SQLStorageAuthority: %s", err)
}
return testCtx{
c: contactAuditor{
db: db,
resultsFile: file,
logger: blog.NewMock(),
},
dbMap: dbMap,
ssa: ssa,
cleanUp: cleanUp,
}
}

View file

@ -56,33 +56,12 @@ type Config struct {
// recovering from an outage to ensure continuity of coverage.
LookbackPeriod config.Duration `validate:"-"`
// CertificateLifetime is the validity period (usually expressed in hours,
// like "2160h") of the longest-lived currently-unexpired certificate. For
// Let's Encrypt, this is usually ninety days. If the validity period of
// the issued certificates ever changes upwards, this value must be updated
// immediately; if the validity period of the issued certificates ever
// changes downwards, the value must not change until after all certificates with
// the old validity period have expired.
// Deprecated: This config value is no longer used.
// TODO(#6438): Remove this value.
CertificateLifetime config.Duration `validate:"-"`
// UpdatePeriod controls how frequently the crl-updater runs and publishes
// new versions of every CRL shard. The Baseline Requirements, Section 4.9.7
// state that this MUST NOT be more than 7 days. We believe that future
// updates may require that this not be more than 24 hours, and currently
// recommend an UpdatePeriod of 6 hours.
// new versions of every CRL shard. The Baseline Requirements, Section 4.9.7:
// "MUST update and publish a new CRL within twentyfour (24) hours after
// recording a Certificate as revoked."
UpdatePeriod config.Duration
// UpdateOffset controls the times at which crl-updater runs, to avoid
// scheduling the batch job at exactly midnight. The updater runs every
// UpdatePeriod, starting from the Unix Epoch plus UpdateOffset, and
// continuing forward into the future forever. This value must be strictly
// less than the UpdatePeriod.
// Deprecated: This config value is not relevant with continuous updating.
// TODO(#7023): Remove this value.
UpdateOffset config.Duration `validate:"-"`
// UpdateTimeout controls how long a single CRL shard is allowed to attempt
// to update before being timed out. The total CRL updating process may take
// significantly longer, since a full update cycle may consist of updating
@ -91,6 +70,19 @@ type Config struct {
// of magnitude greater than our p99 update latency.
UpdateTimeout config.Duration `validate:"-"`
// TemporallyShardedSerialPrefixes is a list of prefixes that were used to
// issue certificates with no CRLDistributionPoints extension, and which are
// therefore temporally sharded. If it's non-empty, the CRL Updater will
// require matching serials when querying by temporal shard. When querying
// by explicit shard, any prefix is allowed.
//
// This should be set to the current set of serial prefixes in production.
// When deploying explicit sharding (i.e. the CRLDistributionPoints extension),
// the CAs should be configured with a new set of serial prefixes that haven't
// been used before (and the OCSP Responder config should be updated to
// recognize the new prefixes as well as the old ones).
TemporallyShardedSerialPrefixes []string
// MaxParallelism controls how many workers may be running in parallel.
// A higher value reduces the total time necessary to update all CRL shards
// that this updater is responsible for, but also increases the memory used
@ -103,6 +95,37 @@ type Config struct {
// load of said run. The default is 1.
MaxAttempts int `validate:"omitempty,min=1"`
// ExpiresMargin adds a small increment to the CRL's HTTP Expires time.
//
// When uploading a CRL, its Expires field in S3 is set to the expected time
// the next CRL will be uploaded (by this instance). That allows our CDN
// instances to cache for that long. However, since the next update might be
// slow or delayed, we add a margin of error.
//
// Tradeoffs: A large ExpiresMargin reduces the chance that a CRL becomes
// uncacheable and floods S3 with traffic (which might result in 503s while
// S3 scales out).
//
// A small ExpiresMargin means revocations become visible sooner, including
// admin-invoked revocations that may have a time requirement.
ExpiresMargin config.Duration
// CacheControl is a string passed verbatim to the crl-storer to store on
// the S3 object.
//
// Note: if this header contains max-age, it will override
// Expires. https://www.rfc-editor.org/rfc/rfc9111.html#name-calculating-freshness-lifet
// Cache-Control: max-age has the disadvantage that it caches for a fixed
// amount of time, regardless of how close the CRL is to replacement. So
// if max-age is used, the worst-case time for a revocation to become visible
// is UpdatePeriod + the value of max age.
//
// The stale-if-error and stale-while-revalidate headers may be useful here:
// https://aws.amazon.com/about-aws/whats-new/2023/05/amazon-cloudfront-stale-while-revalidate-stale-if-error-cache-control-directives/
//
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
CacheControl string
Features features.Config
}
@ -176,6 +199,9 @@ func main() {
c.CRLUpdater.UpdateTimeout.Duration,
c.CRLUpdater.MaxParallelism,
c.CRLUpdater.MaxAttempts,
c.CRLUpdater.CacheControl,
c.CRLUpdater.ExpiresMargin.Duration,
c.CRLUpdater.TemporallyShardedSerialPrefixes,
sac,
cac,
csc,

View file

@ -0,0 +1,130 @@
package notmain
import (
"context"
"flag"
"os"
"github.com/letsencrypt/boulder/cmd"
"github.com/letsencrypt/boulder/email"
emailpb "github.com/letsencrypt/boulder/email/proto"
bgrpc "github.com/letsencrypt/boulder/grpc"
)
// Config holds the configuration for the email-exporter service.
type Config struct {
EmailExporter struct {
cmd.ServiceConfig
// PerDayLimit enforces the daily request limit imposed by the Pardot
// API. The total daily limit, which varies based on the Salesforce
// Pardot subscription tier, must be distributed among all
// email-exporter instances. For more information, see:
// https://developer.salesforce.com/docs/marketing/pardot/guide/overview.html?q=rate+limits#daily-requests-limits
PerDayLimit float64 `validate:"required,min=1"`
// MaxConcurrentRequests enforces the concurrent request limit imposed
// by the Pardot API. This limit must be distributed among all
// email-exporter instances and be proportional to each instance's
// PerDayLimit. For example, if the total daily limit is 50,000 and one
// instance is assigned 40% (20,000 requests), it should also receive
// 40% of the max concurrent requests (2 out of 5). For more
// information, see:
// https://developer.salesforce.com/docs/marketing/pardot/guide/overview.html?q=rate+limits#concurrent-requests
MaxConcurrentRequests int `validate:"required,min=1,max=5"`
// PardotBusinessUnit is the Pardot business unit to use.
PardotBusinessUnit string `validate:"required"`
// ClientId is the OAuth API client ID provided by Salesforce.
ClientId cmd.PasswordConfig
// ClientSecret is the OAuth API client secret provided by Salesforce.
ClientSecret cmd.PasswordConfig
// SalesforceBaseURL is the base URL for the Salesforce API. (e.g.,
// "https://login.salesforce.com")
SalesforceBaseURL string `validate:"required"`
// PardotBaseURL is the base URL for the Pardot API. (e.g.,
// "https://pi.pardot.com")
PardotBaseURL string `validate:"required"`
// EmailCacheSize controls how many hashed email addresses are retained
// in memory to prevent duplicates from being sent to the Pardot API.
// Each entry consumes ~120 bytes, so 100,000 entries uses around 12MB
// of memory. If left unset, no caching is performed.
EmailCacheSize int `validate:"omitempty,min=1"`
}
Syslog cmd.SyslogConfig
OpenTelemetry cmd.OpenTelemetryConfig
}
func main() {
configFile := flag.String("config", "", "Path to configuration file")
grpcAddr := flag.String("addr", "", "gRPC listen address override")
debugAddr := flag.String("debug-addr", "", "Debug server address override")
flag.Parse()
if *configFile == "" {
flag.Usage()
os.Exit(1)
}
var c Config
err := cmd.ReadConfigFile(*configFile, &c)
cmd.FailOnError(err, "Reading JSON config file into config structure")
if *grpcAddr != "" {
c.EmailExporter.ServiceConfig.GRPC.Address = *grpcAddr
}
if *debugAddr != "" {
c.EmailExporter.ServiceConfig.DebugAddr = *debugAddr
}
scope, logger, oTelShutdown := cmd.StatsAndLogging(c.Syslog, c.OpenTelemetry, c.EmailExporter.ServiceConfig.DebugAddr)
defer oTelShutdown(context.Background())
logger.Info(cmd.VersionString())
clk := cmd.Clock()
clientId, err := c.EmailExporter.ClientId.Pass()
cmd.FailOnError(err, "Loading clientId")
clientSecret, err := c.EmailExporter.ClientSecret.Pass()
cmd.FailOnError(err, "Loading clientSecret")
var cache *email.EmailCache
if c.EmailExporter.EmailCacheSize > 0 {
cache = email.NewHashedEmailCache(c.EmailExporter.EmailCacheSize, scope)
}
pardotClient, err := email.NewPardotClientImpl(
clk,
c.EmailExporter.PardotBusinessUnit,
clientId,
clientSecret,
c.EmailExporter.SalesforceBaseURL,
c.EmailExporter.PardotBaseURL,
)
cmd.FailOnError(err, "Creating Pardot API client")
exporterServer := email.NewExporterImpl(pardotClient, cache, c.EmailExporter.PerDayLimit, c.EmailExporter.MaxConcurrentRequests, scope, logger)
tlsConfig, err := c.EmailExporter.TLS.Load(scope)
cmd.FailOnError(err, "Loading email-exporter TLS config")
daemonCtx, shutdownExporterServer := context.WithCancel(context.Background())
go exporterServer.Start(daemonCtx)
start, err := bgrpc.NewServer(c.EmailExporter.GRPC, logger).Add(
&emailpb.Exporter_ServiceDesc, exporterServer).Build(tlsConfig, scope, clk)
cmd.FailOnError(err, "Configuring email-exporter gRPC server")
err = start()
shutdownExporterServer()
exporterServer.Drain()
cmd.FailOnError(err, "email-exporter gRPC service failed to start")
}
func init() {
cmd.RegisterCommand("email-exporter", main, &cmd.ConfigValidator{Config: &Config{}})
}

View file

@ -1,968 +0,0 @@
package notmain
import (
"bytes"
"context"
"crypto/x509"
"encoding/json"
"errors"
"flag"
"fmt"
"math"
netmail "net/mail"
"net/url"
"os"
"sort"
"strings"
"sync"
"text/template"
"time"
"github.com/jmhodges/clock"
"google.golang.org/grpc"
"github.com/prometheus/client_golang/prometheus"
"github.com/letsencrypt/boulder/cmd"
"github.com/letsencrypt/boulder/config"
"github.com/letsencrypt/boulder/core"
corepb "github.com/letsencrypt/boulder/core/proto"
"github.com/letsencrypt/boulder/db"
"github.com/letsencrypt/boulder/features"
bgrpc "github.com/letsencrypt/boulder/grpc"
blog "github.com/letsencrypt/boulder/log"
bmail "github.com/letsencrypt/boulder/mail"
"github.com/letsencrypt/boulder/metrics"
"github.com/letsencrypt/boulder/policy"
"github.com/letsencrypt/boulder/sa"
sapb "github.com/letsencrypt/boulder/sa/proto"
)
const (
defaultExpirationSubject = "Let's Encrypt certificate expiration notice for domain {{.ExpirationSubject}}"
)
var (
errNoValidEmail = errors.New("no usable contact address")
)
type regStore interface {
GetRegistration(ctx context.Context, req *sapb.RegistrationID, _ ...grpc.CallOption) (*corepb.Registration, error)
}
// limiter tracks how many mails we've sent to a given address in a given day.
// Note that this does not track mails across restarts of the process.
// Modifications to `counts` and `currentDay` are protected by a mutex.
type limiter struct {
sync.RWMutex
// currentDay is a day in UTC, truncated to 24 hours. When the current
// time is more than 24 hours past this date, all counts reset and this
// date is updated.
currentDay time.Time
// counts is a map from address to number of mails we have attempted to
// send during `currentDay`.
counts map[string]int
// limit is the number of sends after which we'll return an error from
// check()
limit int
clk clock.Clock
}
const oneDay = 24 * time.Hour
// maybeBumpDay updates lim.currentDay if its current value is more than 24
// hours ago, and resets the counts map. Expects limiter is locked.
func (lim *limiter) maybeBumpDay() {
today := lim.clk.Now().Truncate(oneDay)
if (today.Sub(lim.currentDay) >= oneDay && len(lim.counts) > 0) ||
lim.counts == nil {
// Throw away counts so far and switch to a new day.
// This also does the initialization of counts and currentDay the first
// time inc() is called.
lim.counts = make(map[string]int)
lim.currentDay = today
}
}
// inc increments the count for the current day, and cleans up previous days
// if needed.
func (lim *limiter) inc(address string) {
lim.Lock()
defer lim.Unlock()
lim.maybeBumpDay()
lim.counts[address] += 1
}
// check checks whether the count for the given address is at the limit,
// and returns an error if so.
func (lim *limiter) check(address string) error {
lim.RLock()
defer lim.RUnlock()
lim.maybeBumpDay()
if lim.counts[address] >= lim.limit {
return fmt.Errorf("daily mail limit exceeded for %q", address)
}
return nil
}
type mailer struct {
log blog.Logger
dbMap *db.WrappedMap
rs regStore
mailer bmail.Mailer
emailTemplate *template.Template
subjectTemplate *template.Template
nagTimes []time.Duration
parallelSends uint
certificatesPerTick int
// addressLimiter limits how many mails we'll send to a single address in
// a single day.
addressLimiter *limiter
// Maximum number of rows to update in a single SQL UPDATE statement.
updateChunkSize int
clk clock.Clock
stats mailerStats
}
type certDERWithRegID struct {
DER core.CertDER
RegID int64
}
type mailerStats struct {
sendDelay *prometheus.GaugeVec
sendDelayHistogram *prometheus.HistogramVec
nagsAtCapacity *prometheus.GaugeVec
errorCount *prometheus.CounterVec
sendLatency prometheus.Histogram
processingLatency prometheus.Histogram
certificatesExamined prometheus.Counter
certificatesAlreadyRenewed prometheus.Counter
certificatesPerAccountNeedingMail prometheus.Histogram
}
func (m *mailer) sendNags(conn bmail.Conn, contacts []string, certs []*x509.Certificate) error {
if len(certs) == 0 {
return errors.New("no certs given to send nags for")
}
emails := []string{}
for _, contact := range contacts {
parsed, err := url.Parse(contact)
if err != nil {
m.log.Errf("parsing contact email %s: %s", contact, err)
continue
}
if parsed.Scheme != "mailto" {
continue
}
address := parsed.Opaque
err = policy.ValidEmail(address)
if err != nil {
m.log.Debugf("skipping invalid email %q: %s", address, err)
continue
}
err = m.addressLimiter.check(address)
if err != nil {
m.log.Infof("not sending mail: %s", err)
continue
}
m.addressLimiter.inc(address)
emails = append(emails, parsed.Opaque)
}
if len(emails) == 0 {
return errNoValidEmail
}
expiresIn := time.Duration(math.MaxInt64)
expDate := m.clk.Now()
domains := []string{}
serials := []string{}
// Pick out the expiration date that is closest to being hit.
for _, cert := range certs {
domains = append(domains, cert.DNSNames...)
serials = append(serials, core.SerialToString(cert.SerialNumber))
possible := cert.NotAfter.Sub(m.clk.Now())
if possible < expiresIn {
expiresIn = possible
expDate = cert.NotAfter
}
}
domains = core.UniqueLowerNames(domains)
sort.Strings(domains)
const maxSerials = 100
truncatedSerials := serials
if len(truncatedSerials) > maxSerials {
truncatedSerials = serials[0:maxSerials]
}
const maxDomains = 100
truncatedDomains := domains
if len(truncatedDomains) > maxDomains {
truncatedDomains = domains[0:maxDomains]
}
// Construct the information about the expiring certificates for use in the
// subject template
expiringSubject := fmt.Sprintf("%q", domains[0])
if len(domains) > 1 {
expiringSubject += fmt.Sprintf(" (and %d more)", len(domains)-1)
}
// Execute the subjectTemplate by filling in the ExpirationSubject
subjBuf := new(bytes.Buffer)
err := m.subjectTemplate.Execute(subjBuf, struct {
ExpirationSubject string
}{
ExpirationSubject: expiringSubject,
})
if err != nil {
m.stats.errorCount.With(prometheus.Labels{"type": "SubjectTemplateFailure"}).Inc()
return err
}
email := struct {
ExpirationDate string
DaysToExpiration int
DNSNames string
TruncatedDNSNames string
NumDNSNamesOmitted int
}{
ExpirationDate: expDate.UTC().Format(time.DateOnly),
DaysToExpiration: int(expiresIn.Hours() / 24),
DNSNames: strings.Join(domains, "\n"),
TruncatedDNSNames: strings.Join(truncatedDomains, "\n"),
NumDNSNamesOmitted: len(domains) - len(truncatedDomains),
}
msgBuf := new(bytes.Buffer)
err = m.emailTemplate.Execute(msgBuf, email)
if err != nil {
m.stats.errorCount.With(prometheus.Labels{"type": "TemplateFailure"}).Inc()
return err
}
logItem := struct {
Rcpt []string
DaysToExpiration int
TruncatedDNSNames []string
TruncatedSerials []string
}{
Rcpt: emails,
DaysToExpiration: email.DaysToExpiration,
TruncatedDNSNames: truncatedDomains,
TruncatedSerials: truncatedSerials,
}
logStr, err := json.Marshal(logItem)
if err != nil {
m.log.Errf("logItem could not be serialized to JSON. Raw: %+v", logItem)
return err
}
m.log.Infof("attempting send JSON=%s", string(logStr))
startSending := m.clk.Now()
err = conn.SendMail(emails, subjBuf.String(), msgBuf.String())
if err != nil {
m.log.Errf("failed send JSON=%s err=%s", string(logStr), err)
return err
}
finishSending := m.clk.Now()
elapsed := finishSending.Sub(startSending)
m.stats.sendLatency.Observe(elapsed.Seconds())
return nil
}
// updateLastNagTimestamps updates the lastExpirationNagSent column for every cert in
// the given list. Even though it can encounter errors, it only logs them and
// does not return them, because we always prefer to simply continue.
func (m *mailer) updateLastNagTimestamps(ctx context.Context, certs []*x509.Certificate) {
for len(certs) > 0 {
size := len(certs)
if m.updateChunkSize > 0 && size > m.updateChunkSize {
size = m.updateChunkSize
}
chunk := certs[0:size]
certs = certs[size:]
m.updateLastNagTimestampsChunk(ctx, chunk)
}
}
// updateLastNagTimestampsChunk processes a single chunk (up to 65k) of certificates.
func (m *mailer) updateLastNagTimestampsChunk(ctx context.Context, certs []*x509.Certificate) {
params := make([]interface{}, len(certs)+1)
for i, cert := range certs {
params[i+1] = core.SerialToString(cert.SerialNumber)
}
query := fmt.Sprintf(
"UPDATE certificateStatus SET lastExpirationNagSent = ? WHERE serial IN (%s)",
db.QuestionMarks(len(certs)),
)
params[0] = m.clk.Now()
_, err := m.dbMap.ExecContext(ctx, query, params...)
if err != nil {
m.log.AuditErrf("Error updating certificate status for %d certs: %s", len(certs), err)
m.stats.errorCount.With(prometheus.Labels{"type": "UpdateCertificateStatus"}).Inc()
}
}
func (m *mailer) certIsRenewed(ctx context.Context, names []string, issued time.Time) (bool, error) {
namehash := core.HashNames(names)
var present bool
err := m.dbMap.SelectOne(
ctx,
&present,
`SELECT EXISTS (SELECT id FROM fqdnSets WHERE setHash = ? AND issued > ? LIMIT 1)`,
namehash,
issued,
)
return present, err
}
type work struct {
regID int64
certDERs []core.CertDER
}
func (m *mailer) processCerts(
ctx context.Context,
allCerts []certDERWithRegID,
expiresIn time.Duration,
) error {
regIDToCertDERs := make(map[int64][]core.CertDER)
for _, cert := range allCerts {
cs := regIDToCertDERs[cert.RegID]
cs = append(cs, cert.DER)
regIDToCertDERs[cert.RegID] = cs
}
parallelSends := m.parallelSends
if parallelSends == 0 {
parallelSends = 1
}
var wg sync.WaitGroup
workChan := make(chan work, len(regIDToCertDERs))
// Populate the work chan on a goroutine so work is available as soon
// as one of the sender routines starts.
go func(ch chan<- work) {
for regID, certs := range regIDToCertDERs {
ch <- work{regID, certs}
}
close(workChan)
}(workChan)
for senderNum := uint(0); senderNum < parallelSends; senderNum++ {
// For politeness' sake, don't open more than 1 new connection per
// second.
if senderNum > 0 {
time.Sleep(time.Second)
}
if ctx.Err() != nil {
return ctx.Err()
}
conn, err := m.mailer.Connect()
if err != nil {
m.log.AuditErrf("connecting parallel sender %d: %s", senderNum, err)
return err
}
wg.Add(1)
go func(conn bmail.Conn, ch <-chan work) {
defer wg.Done()
for w := range ch {
err := m.sendToOneRegID(ctx, conn, w.regID, w.certDERs, expiresIn)
if err != nil {
m.log.AuditErr(err.Error())
}
}
conn.Close()
}(conn, workChan)
}
wg.Wait()
return nil
}
func (m *mailer) sendToOneRegID(ctx context.Context, conn bmail.Conn, regID int64, certDERs []core.CertDER, expiresIn time.Duration) error {
if ctx.Err() != nil {
return ctx.Err()
}
if len(certDERs) == 0 {
return errors.New("shouldn't happen: empty certificate list in sendToOneRegID")
}
reg, err := m.rs.GetRegistration(ctx, &sapb.RegistrationID{Id: regID})
if err != nil {
m.stats.errorCount.With(prometheus.Labels{"type": "GetRegistration"}).Inc()
return fmt.Errorf("Error fetching registration %d: %s", regID, err)
}
parsedCerts := []*x509.Certificate{}
for i, certDER := range certDERs {
if ctx.Err() != nil {
return ctx.Err()
}
parsedCert, err := x509.ParseCertificate(certDER)
if err != nil {
// TODO(#1420): tell registration about this error
m.log.AuditErrf("Error parsing certificate: %s. Body: %x", err, certDER)
m.stats.errorCount.With(prometheus.Labels{"type": "ParseCertificate"}).Inc()
continue
}
// The histogram version of send delay reports the worst case send delay for
// a single regID in this cycle.
if i == 0 {
sendDelay := expiresIn - parsedCert.NotAfter.Sub(m.clk.Now())
m.stats.sendDelayHistogram.With(prometheus.Labels{"nag_group": expiresIn.String()}).Observe(
sendDelay.Truncate(time.Second).Seconds())
}
renewed, err := m.certIsRenewed(ctx, parsedCert.DNSNames, parsedCert.NotBefore)
if err != nil {
m.log.AuditErrf("expiration-mailer: error fetching renewal state: %v", err)
// assume not renewed
} else if renewed {
m.log.Debugf("Cert %s is already renewed", core.SerialToString(parsedCert.SerialNumber))
m.stats.certificatesAlreadyRenewed.Add(1)
m.updateLastNagTimestamps(ctx, []*x509.Certificate{parsedCert})
continue
}
parsedCerts = append(parsedCerts, parsedCert)
}
m.stats.certificatesPerAccountNeedingMail.Observe(float64(len(parsedCerts)))
if len(parsedCerts) == 0 {
// all certificates are renewed
return nil
}
err = m.sendNags(conn, reg.Contact, parsedCerts)
if err != nil {
// If the error was due to the address(es) being unusable or the mail being
// undeliverable, we don't want to try again later.
var badAddrErr *bmail.BadAddressSMTPError
if errors.Is(err, errNoValidEmail) || errors.As(err, &badAddrErr) {
m.updateLastNagTimestamps(ctx, parsedCerts)
// Some accounts have no email; some accounts have an invalid email.
// Treat those as non-error cases.
return nil
}
m.stats.errorCount.With(prometheus.Labels{"type": "SendNags"}).Inc()
return fmt.Errorf("sending nag emails: %s", err)
}
m.updateLastNagTimestamps(ctx, parsedCerts)
return nil
}
// findExpiringCertificates finds certificates that might need an expiration mail, filters them,
// groups by account, sends mail, and updates their status in the DB so we don't examine them again.
//
// Invariant: findExpiringCertificates should examine each certificate at most N times, where
// N is the number of reminders. For every certificate examined (barring errors), this function
// should update the lastExpirationNagSent field of certificateStatus, so it does not need to
// examine the same certificate again on the next go-round. This ensures we make forward progress
// and don't clog up the window of certificates to be examined.
func (m *mailer) findExpiringCertificates(ctx context.Context) error {
now := m.clk.Now()
// E.g. m.nagTimes = [2, 4, 8, 15] days from expiration
for i, expiresIn := range m.nagTimes {
left := now
if i > 0 {
left = left.Add(m.nagTimes[i-1])
}
right := now.Add(expiresIn)
m.log.Infof("expiration-mailer: Searching for certificates that expire between %s and %s and had last nag >%s before expiry",
left.UTC(), right.UTC(), expiresIn)
var certs []certDERWithRegID
var err error
if features.Get().ExpirationMailerUsesJoin {
certs, err = m.getCertsWithJoin(ctx, left, right, expiresIn)
} else {
certs, err = m.getCerts(ctx, left, right, expiresIn)
}
if err != nil {
return err
}
m.stats.certificatesExamined.Add(float64(len(certs)))
// If the number of rows was exactly `m.certificatesPerTick` rows we need to increment
// a stat indicating that this nag group is at capacity. If this condition
// continually occurs across mailer runs then we will not catch up,
// resulting in under-sending expiration mails. The effects of this
// were initially described in issue #2002[0].
//
// 0: https://github.com/letsencrypt/boulder/issues/2002
atCapacity := float64(0)
if len(certs) == m.certificatesPerTick {
m.log.Infof("nag group %s expiring certificates at configured capacity (select limit %d)",
expiresIn.String(), m.certificatesPerTick)
atCapacity = float64(1)
}
m.stats.nagsAtCapacity.With(prometheus.Labels{"nag_group": expiresIn.String()}).Set(atCapacity)
m.log.Infof("Found %d certificates expiring between %s and %s", len(certs),
left.Format(time.DateTime), right.Format(time.DateTime))
if len(certs) == 0 {
continue // nothing to do
}
processingStarted := m.clk.Now()
err = m.processCerts(ctx, certs, expiresIn)
if err != nil {
m.log.AuditErr(err.Error())
}
processingEnded := m.clk.Now()
elapsed := processingEnded.Sub(processingStarted)
m.stats.processingLatency.Observe(elapsed.Seconds())
}
return nil
}
func (m *mailer) getCertsWithJoin(ctx context.Context, left, right time.Time, expiresIn time.Duration) ([]certDERWithRegID, error) {
// First we do a query on the certificateStatus table to find certificates
// nearing expiry meeting our criteria for email notification. We later
// sequentially fetch the certificate details. This avoids an expensive
// JOIN.
var certs []certDERWithRegID
_, err := m.dbMap.Select(
ctx,
&certs,
`SELECT
cert.der as der, cert.registrationID as regID
FROM certificateStatus AS cs
JOIN certificates as cert
ON cs.serial = cert.serial
AND cs.notAfter > :cutoffA
AND cs.notAfter <= :cutoffB
AND cs.status != "revoked"
AND COALESCE(TIMESTAMPDIFF(SECOND, cs.lastExpirationNagSent, cs.notAfter) > :nagCutoff, 1)
ORDER BY cs.notAfter ASC
LIMIT :certificatesPerTick`,
map[string]interface{}{
"cutoffA": left,
"cutoffB": right,
"nagCutoff": expiresIn.Seconds(),
"certificatesPerTick": m.certificatesPerTick,
},
)
if err != nil {
m.log.AuditErrf("expiration-mailer: Error loading certificate serials: %s", err)
return nil, err
}
m.log.Debugf("found %d certificates", len(certs))
return certs, nil
}
func (m *mailer) getCerts(ctx context.Context, left, right time.Time, expiresIn time.Duration) ([]certDERWithRegID, error) {
// First we do a query on the certificateStatus table to find certificates
// nearing expiry meeting our criteria for email notification. We later
// sequentially fetch the certificate details. This avoids an expensive
// JOIN.
var serials []string
_, err := m.dbMap.Select(
ctx,
&serials,
`SELECT
cs.serial
FROM certificateStatus AS cs
WHERE cs.notAfter > :cutoffA
AND cs.notAfter <= :cutoffB
AND cs.status != "revoked"
AND COALESCE(TIMESTAMPDIFF(SECOND, cs.lastExpirationNagSent, cs.notAfter) > :nagCutoff, 1)
ORDER BY cs.notAfter ASC
LIMIT :certificatesPerTick`,
map[string]interface{}{
"cutoffA": left,
"cutoffB": right,
"nagCutoff": expiresIn.Seconds(),
"certificatesPerTick": m.certificatesPerTick,
},
)
if err != nil {
m.log.AuditErrf("expiration-mailer: Error loading certificate serials: %s", err)
return nil, err
}
m.log.Debugf("found %d certificates", len(serials))
// Now we can sequentially retrieve the certificate details for each of the
// certificate status rows
var certs []certDERWithRegID
for i, serial := range serials {
if ctx.Err() != nil {
return nil, ctx.Err()
}
var cert core.Certificate
cert, err := sa.SelectCertificate(ctx, m.dbMap, serial)
if err != nil {
// We can get a NoRowsErr when processing a serial number corresponding
// to a precertificate with no final certificate. Since this certificate
// is not being used by a subscriber, we don't send expiration email about
// it.
if db.IsNoRows(err) {
m.log.Infof("no rows for serial %q", serial)
continue
}
m.log.AuditErrf("expiration-mailer: Error loading cert %q: %s", cert.Serial, err)
continue
}
certs = append(certs, certDERWithRegID{
DER: cert.DER,
RegID: cert.RegistrationID,
})
if i == 0 {
// Report the send delay metric. Note: this is the worst-case send delay
// of any certificate in this batch because it's based on the first (oldest).
sendDelay := expiresIn - cert.Expires.Sub(m.clk.Now())
m.stats.sendDelay.With(prometheus.Labels{"nag_group": expiresIn.String()}).Set(
sendDelay.Truncate(time.Second).Seconds())
}
}
return certs, nil
}
type durationSlice []time.Duration
func (ds durationSlice) Len() int {
return len(ds)
}
func (ds durationSlice) Less(a, b int) bool {
return ds[a] < ds[b]
}
func (ds durationSlice) Swap(a, b int) {
ds[a], ds[b] = ds[b], ds[a]
}
type Config struct {
Mailer struct {
DebugAddr string `validate:"omitempty,hostname_port"`
DB cmd.DBConfig
cmd.SMTPConfig
// From is an RFC 5322 formatted "From" address for reminder messages,
// e.g. "Example <example@test.org>"
From string `validate:"required"`
// Subject is the Subject line of reminder messages. This is a Go
// template with a single variable: ExpirationSubject, which contains
// a list of affected hostnames, possibly truncated.
Subject string
// CertLimit is the maximum number of certificates to investigate in a
// single batch. Defaults to 100.
CertLimit int `validate:"min=0"`
// MailsPerAddressPerDay is the maximum number of emails we'll send to
// a single address in a single day. Defaults to 0 (unlimited).
// Note that this does not track sends across restarts of the process,
// so we may send more than this when we restart expiration-mailer.
// This is a best-effort limitation. Defaults to math.MaxInt.
MailsPerAddressPerDay int `validate:"min=0"`
// UpdateChunkSize is the maximum number of rows to update in a single
// SQL UPDATE statement.
UpdateChunkSize int `validate:"min=0,max=65535"`
NagTimes []string `validate:"min=1,dive,required"`
// Path to a text/template email template with a .gotmpl or .txt file
// extension.
EmailTemplate string `validate:"required"`
// How often to process a batch of certificates
Frequency config.Duration
// ParallelSends is the number of parallel goroutines used to process
// each batch of emails. Defaults to 1.
ParallelSends uint
TLS cmd.TLSConfig
SAService *cmd.GRPCClientConfig
// Path to a file containing a list of trusted root certificates for use
// during the SMTP connection (as opposed to the gRPC connections).
SMTPTrustedRootFile string
Features features.Config
}
Syslog cmd.SyslogConfig
OpenTelemetry cmd.OpenTelemetryConfig
}
func initStats(stats prometheus.Registerer) mailerStats {
sendDelay := prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "send_delay",
Help: "For the last batch of certificates, difference between the idealized send time and actual send time. Will always be nonzero, bigger numbers are worse",
},
[]string{"nag_group"})
stats.MustRegister(sendDelay)
sendDelayHistogram := prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "send_delay_histogram",
Help: "For each mail sent, difference between the idealized send time and actual send time. Will always be nonzero, bigger numbers are worse",
Buckets: prometheus.LinearBuckets(86400, 86400, 10),
},
[]string{"nag_group"})
stats.MustRegister(sendDelayHistogram)
nagsAtCapacity := prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "nags_at_capacity",
Help: "Count of nag groups at capacity",
},
[]string{"nag_group"})
stats.MustRegister(nagsAtCapacity)
errorCount := prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "errors",
Help: "Number of errors",
},
[]string{"type"})
stats.MustRegister(errorCount)
sendLatency := prometheus.NewHistogram(
prometheus.HistogramOpts{
Name: "send_latency",
Help: "Time the mailer takes sending messages in seconds",
Buckets: metrics.InternetFacingBuckets,
})
stats.MustRegister(sendLatency)
processingLatency := prometheus.NewHistogram(
prometheus.HistogramOpts{
Name: "processing_latency",
Help: "Time the mailer takes processing certificates in seconds",
Buckets: []float64{30, 60, 75, 90, 120, 600, 3600},
})
stats.MustRegister(processingLatency)
certificatesExamined := prometheus.NewCounter(
prometheus.CounterOpts{
Name: "certificates_examined",
Help: "Number of certificates looked at that are potentially due for an expiration mail",
})
stats.MustRegister(certificatesExamined)
certificatesAlreadyRenewed := prometheus.NewCounter(
prometheus.CounterOpts{
Name: "certificates_already_renewed",
Help: "Number of certificates from certificates_examined that were ignored because they were already renewed",
})
stats.MustRegister(certificatesAlreadyRenewed)
accountsNeedingMail := prometheus.NewHistogram(
prometheus.HistogramOpts{
Name: "certificates_per_account_needing_mail",
Help: "After ignoring certificates_already_renewed and grouping the remaining certificates by account, how many accounts needed to get an email; grouped by how many certificates each account needed",
Buckets: []float64{0, 1, 2, 100, 1000, 10000, 100000},
})
stats.MustRegister(accountsNeedingMail)
return mailerStats{
sendDelay: sendDelay,
sendDelayHistogram: sendDelayHistogram,
nagsAtCapacity: nagsAtCapacity,
errorCount: errorCount,
sendLatency: sendLatency,
processingLatency: processingLatency,
certificatesExamined: certificatesExamined,
certificatesAlreadyRenewed: certificatesAlreadyRenewed,
certificatesPerAccountNeedingMail: accountsNeedingMail,
}
}
func main() {
debugAddr := flag.String("debug-addr", "", "Debug server address override")
configFile := flag.String("config", "", "File path to the configuration file for this service")
certLimit := flag.Int("cert_limit", 0, "Count of certificates to process per expiration period")
reconnBase := flag.Duration("reconnectBase", 1*time.Second, "Base sleep duration between reconnect attempts")
reconnMax := flag.Duration("reconnectMax", 5*60*time.Second, "Max sleep duration between reconnect attempts after exponential backoff")
daemon := flag.Bool("daemon", false, "Run in daemon mode")
flag.Parse()
if *configFile == "" {
flag.Usage()
os.Exit(1)
}
var c Config
err := cmd.ReadConfigFile(*configFile, &c)
cmd.FailOnError(err, "Reading JSON config file into config structure")
features.Set(c.Mailer.Features)
if *debugAddr != "" {
c.Mailer.DebugAddr = *debugAddr
}
scope, logger, oTelShutdown := cmd.StatsAndLogging(c.Syslog, c.OpenTelemetry, c.Mailer.DebugAddr)
defer oTelShutdown(context.Background())
logger.Info(cmd.VersionString())
if *daemon && c.Mailer.Frequency.Duration == 0 {
fmt.Fprintln(os.Stderr, "mailer.frequency is not set in the JSON config")
os.Exit(1)
}
if *certLimit > 0 {
c.Mailer.CertLimit = *certLimit
}
// Default to 100 if no certLimit is set
if c.Mailer.CertLimit == 0 {
c.Mailer.CertLimit = 100
}
if c.Mailer.MailsPerAddressPerDay == 0 {
c.Mailer.MailsPerAddressPerDay = math.MaxInt
}
dbMap, err := sa.InitWrappedDb(c.Mailer.DB, scope, logger)
cmd.FailOnError(err, "While initializing dbMap")
tlsConfig, err := c.Mailer.TLS.Load(scope)
cmd.FailOnError(err, "TLS config")
clk := cmd.Clock()
conn, err := bgrpc.ClientSetup(c.Mailer.SAService, tlsConfig, scope, clk)
cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to SA")
sac := sapb.NewStorageAuthorityClient(conn)
var smtpRoots *x509.CertPool
if c.Mailer.SMTPTrustedRootFile != "" {
pem, err := os.ReadFile(c.Mailer.SMTPTrustedRootFile)
cmd.FailOnError(err, "Loading trusted roots file")
smtpRoots = x509.NewCertPool()
if !smtpRoots.AppendCertsFromPEM(pem) {
cmd.FailOnError(nil, "Failed to parse root certs PEM")
}
}
// Load email template
emailTmpl, err := os.ReadFile(c.Mailer.EmailTemplate)
cmd.FailOnError(err, fmt.Sprintf("Could not read email template file [%s]", c.Mailer.EmailTemplate))
tmpl, err := template.New("expiry-email").Parse(string(emailTmpl))
cmd.FailOnError(err, "Could not parse email template")
// If there is no configured subject template, use a default
if c.Mailer.Subject == "" {
c.Mailer.Subject = defaultExpirationSubject
}
// Load subject template
subjTmpl, err := template.New("expiry-email-subject").Parse(c.Mailer.Subject)
cmd.FailOnError(err, "Could not parse email subject template")
fromAddress, err := netmail.ParseAddress(c.Mailer.From)
cmd.FailOnError(err, fmt.Sprintf("Could not parse from address: %s", c.Mailer.From))
smtpPassword, err := c.Mailer.PasswordConfig.Pass()
cmd.FailOnError(err, "Failed to load SMTP password")
mailClient := bmail.New(
c.Mailer.Server,
c.Mailer.Port,
c.Mailer.Username,
smtpPassword,
smtpRoots,
*fromAddress,
logger,
scope,
*reconnBase,
*reconnMax)
var nags durationSlice
for _, nagDuration := range c.Mailer.NagTimes {
dur, err := time.ParseDuration(nagDuration)
if err != nil {
logger.AuditErrf("Failed to parse nag duration string [%s]: %s", nagDuration, err)
return
}
// Add some padding to the nag times so we send _before_ the configured
// time rather than after. See https://github.com/letsencrypt/boulder/pull/1029
adjustedInterval := dur + c.Mailer.Frequency.Duration
nags = append(nags, adjustedInterval)
}
// Make sure durations are sorted in increasing order
sort.Sort(nags)
if c.Mailer.UpdateChunkSize > 65535 {
// MariaDB limits the number of placeholders parameters to max_uint16:
// https://github.com/MariaDB/server/blob/10.5/sql/sql_prepare.cc#L2629-L2635
cmd.Fail(fmt.Sprintf("UpdateChunkSize of %d is too big", c.Mailer.UpdateChunkSize))
}
m := mailer{
log: logger,
dbMap: dbMap,
rs: sac,
mailer: mailClient,
subjectTemplate: subjTmpl,
emailTemplate: tmpl,
nagTimes: nags,
certificatesPerTick: c.Mailer.CertLimit,
addressLimiter: &limiter{clk: cmd.Clock(), limit: c.Mailer.MailsPerAddressPerDay},
updateChunkSize: c.Mailer.UpdateChunkSize,
parallelSends: c.Mailer.ParallelSends,
clk: clk,
stats: initStats(scope),
}
// Prefill this labelled stat with the possible label values, so each value is
// set to 0 on startup, rather than being missing from stats collection until
// the first mail run.
for _, expiresIn := range nags {
m.stats.nagsAtCapacity.With(prometheus.Labels{"nag_group": expiresIn.String()}).Set(0)
}
ctx, cancel := context.WithCancel(context.Background())
go cmd.CatchSignals(cancel)
if *daemon {
t := time.NewTicker(c.Mailer.Frequency.Duration)
for {
select {
case <-t.C:
err = m.findExpiringCertificates(ctx)
if err != nil && !errors.Is(err, context.Canceled) {
cmd.FailOnError(err, "expiration-mailer has failed")
}
case <-ctx.Done():
return
}
}
} else {
err = m.findExpiringCertificates(ctx)
if err != nil && !errors.Is(err, context.Canceled) {
cmd.FailOnError(err, "expiration-mailer has failed")
}
}
}
func init() {
cmd.RegisterCommand("expiration-mailer", main, &cmd.ConfigValidator{Config: &Config{}})
}

View file

@ -1,71 +0,0 @@
package notmain
import (
"crypto/x509"
"crypto/x509/pkix"
"fmt"
"math/big"
"testing"
"time"
"github.com/letsencrypt/boulder/mocks"
"github.com/letsencrypt/boulder/test"
)
var (
email1 = "mailto:one@shared-example.com"
email2 = "mailto:two@shared-example.com"
)
func TestSendEarliestCertInfo(t *testing.T) {
expiresIn := 24 * time.Hour
ctx := setup(t, []time.Duration{expiresIn})
defer ctx.cleanUp()
rawCertA := newX509Cert("happy A",
ctx.fc.Now().AddDate(0, 0, 5),
[]string{"example-A.com", "SHARED-example.com"},
serial1,
)
rawCertB := newX509Cert("happy B",
ctx.fc.Now().AddDate(0, 0, 2),
[]string{"shared-example.com", "example-b.com"},
serial2,
)
conn, err := ctx.m.mailer.Connect()
test.AssertNotError(t, err, "connecting SMTP")
err = ctx.m.sendNags(conn, []string{email1, email2}, []*x509.Certificate{rawCertA, rawCertB})
if err != nil {
t.Fatal(err)
}
if len(ctx.mc.Messages) != 2 {
t.Errorf("num of messages, want %d, got %d", 2, len(ctx.mc.Messages))
}
if len(ctx.mc.Messages) == 0 {
t.Fatalf("no message sent")
}
domains := "example-a.com\nexample-b.com\nshared-example.com"
expected := mocks.MailerMessage{
Subject: "Testing: Let's Encrypt certificate expiration notice for domain \"example-a.com\" (and 2 more)",
Body: fmt.Sprintf(`hi, cert for DNS names %s is going to expire in 2 days (%s)`,
domains,
rawCertB.NotAfter.Format(time.DateOnly)),
}
expected.To = "one@shared-example.com"
test.AssertEquals(t, expected, ctx.mc.Messages[0])
expected.To = "two@shared-example.com"
test.AssertEquals(t, expected, ctx.mc.Messages[1])
}
func newX509Cert(commonName string, notAfter time.Time, dnsNames []string, serial *big.Int) *x509.Certificate {
return &x509.Certificate{
Subject: pkix.Name{
CommonName: commonName,
},
NotAfter: notAfter,
DNSNames: dnsNames,
SerialNumber: serial,
}
}

View file

@ -1,304 +0,0 @@
package notmain
import (
"bufio"
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"os"
"strings"
"time"
"github.com/jmhodges/clock"
"github.com/letsencrypt/boulder/cmd"
"github.com/letsencrypt/boulder/db"
"github.com/letsencrypt/boulder/features"
blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/sa"
)
type idExporter struct {
log blog.Logger
dbMap *db.WrappedMap
clk clock.Clock
grace time.Duration
}
// resultEntry is a JSON marshalable exporter result entry.
type resultEntry struct {
// ID is exported to support marshaling to JSON.
ID int64 `json:"id"`
// Hostname is exported to support marshaling to JSON. Not all queries
// will fill this field, so it's JSON field tag marks at as
// omittable.
Hostname string `json:"hostname,omitempty"`
}
// reverseHostname converts (reversed) names sourced from the
// registrations table to standard hostnames.
func (r *resultEntry) reverseHostname() {
r.Hostname = sa.ReverseName(r.Hostname)
}
// idExporterResults is passed as a selectable 'holder' for the results
// of id-exporter database queries
type idExporterResults []*resultEntry
// marshalToJSON returns JSON as bytes for all elements of the inner `id`
// slice.
func (i *idExporterResults) marshalToJSON() ([]byte, error) {
data, err := json.Marshal(i)
if err != nil {
return nil, err
}
data = append(data, '\n')
return data, nil
}
// writeToFile writes the contents of the inner `ids` slice, as JSON, to
// a file
func (i *idExporterResults) writeToFile(outfile string) error {
data, err := i.marshalToJSON()
if err != nil {
return err
}
return os.WriteFile(outfile, data, 0644)
}
// findIDs gathers all registration IDs with unexpired certificates.
func (c idExporter) findIDs(ctx context.Context) (idExporterResults, error) {
var holder idExporterResults
_, err := c.dbMap.Select(
ctx,
&holder,
`SELECT DISTINCT r.id
FROM registrations AS r
INNER JOIN certificates AS c on c.registrationID = r.id
WHERE r.contact NOT IN ('[]', 'null')
AND c.expires >= :expireCutoff;`,
map[string]interface{}{
"expireCutoff": c.clk.Now().Add(-c.grace),
})
if err != nil {
c.log.AuditErrf("Error finding IDs: %s", err)
return nil, err
}
return holder, nil
}
// findIDsWithExampleHostnames gathers all registration IDs with
// unexpired certificates and a corresponding example hostname.
func (c idExporter) findIDsWithExampleHostnames(ctx context.Context) (idExporterResults, error) {
var holder idExporterResults
_, err := c.dbMap.Select(
ctx,
&holder,
`SELECT SQL_BIG_RESULT
cert.registrationID AS id,
name.reversedName AS hostname
FROM certificates AS cert
INNER JOIN issuedNames AS name ON name.serial = cert.serial
WHERE cert.expires >= :expireCutoff
GROUP BY cert.registrationID;`,
map[string]interface{}{
"expireCutoff": c.clk.Now().Add(-c.grace),
})
if err != nil {
c.log.AuditErrf("Error finding IDs and example hostnames: %s", err)
return nil, err
}
for _, result := range holder {
result.reverseHostname()
}
return holder, nil
}
// findIDsForHostnames gathers all registration IDs with unexpired
// certificates for each `hostnames` entry.
func (c idExporter) findIDsForHostnames(ctx context.Context, hostnames []string) (idExporterResults, error) {
var holder idExporterResults
for _, hostname := range hostnames {
// Pass the same list in each time, borp will happily just append to the slice
// instead of overwriting it each time
// https://github.com/letsencrypt/borp/blob/c87bd6443d59746a33aca77db34a60cfc344adb2/select.go#L349-L353
_, err := c.dbMap.Select(
ctx,
&holder,
`SELECT DISTINCT c.registrationID AS id
FROM certificates AS c
INNER JOIN issuedNames AS n ON c.serial = n.serial
WHERE c.expires >= :expireCutoff
AND n.reversedName = :reversedName;`,
map[string]interface{}{
"expireCutoff": c.clk.Now().Add(-c.grace),
"reversedName": sa.ReverseName(hostname),
},
)
if err != nil {
if db.IsNoRows(err) {
continue
}
return nil, err
}
}
return holder, nil
}
const usageIntro = `
Introduction:
The ID exporter exists to retrieve the IDs of all registered
users with currently unexpired certificates. This list of registration IDs can
then be given as input to the notification mailer to send bulk notifications.
The -grace parameter can be used to allow registrations with certificates that
have already expired to be included in the export. The argument is a Go duration
obeying the usual suffix rules (e.g. 24h).
Registration IDs are favoured over email addresses as the intermediate format in
order to ensure the most up to date contact information is used at the time of
notification. The notification mailer will resolve the ID to email(s) when the
mailing is underway, ensuring we use the correct address if a user has updated
their contact information between the time of export and the time of
notification.
By default, the ID exporter's output will be JSON of the form:
[
{ "id": 1 },
...
{ "id": n }
]
Operations that return a hostname will be JSON of the form:
[
{ "id": 1, "hostname": "example-1.com" },
...
{ "id": n, "hostname": "example-n.com" }
]
Examples:
Export all registration IDs with unexpired certificates to "regs.json":
id-exporter -config test/config/id-exporter.json -outfile regs.json
Export all registration IDs with certificates that are unexpired or expired
within the last two days to "regs.json":
id-exporter -config test/config/id-exporter.json -grace 48h -outfile
"regs.json"
Required arguments:
- config
- outfile`
// unmarshalHostnames unmarshals a hostnames file and ensures that the file
// contained at least one entry.
func unmarshalHostnames(filePath string) ([]string, error) {
file, err := os.Open(filePath)
if err != nil {
return nil, err
}
defer file.Close()
scanner := bufio.NewScanner(file)
scanner.Split(bufio.ScanLines)
var hostnames []string
for scanner.Scan() {
line := scanner.Text()
if strings.Contains(line, " ") {
return nil, fmt.Errorf(
"line: %q contains more than one entry, entries must be separated by newlines", line)
}
hostnames = append(hostnames, line)
}
if len(hostnames) == 0 {
return nil, errors.New("provided file contains 0 hostnames")
}
return hostnames, nil
}
type Config struct {
ContactExporter struct {
DB cmd.DBConfig
cmd.PasswordConfig
Features features.Config
}
}
func main() {
outFile := flag.String("outfile", "", "File to output results JSON to.")
grace := flag.Duration("grace", 2*24*time.Hour, "Include results with certificates that expired in < grace ago.")
hostnamesFile := flag.String(
"hostnames", "", "Only include results with unexpired certificates that contain hostnames\nlisted (newline separated) in this file.")
withExampleHostnames := flag.Bool(
"with-example-hostnames", false, "Include an example hostname for each registration ID with an unexpired certificate.")
configFile := flag.String("config", "", "File containing a JSON config.")
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "%s\n\n", usageIntro)
fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0])
flag.PrintDefaults()
}
// Parse flags and check required.
flag.Parse()
if *outFile == "" || *configFile == "" {
flag.Usage()
os.Exit(1)
}
log := cmd.NewLogger(cmd.SyslogConfig{StdoutLevel: 7})
log.Info(cmd.VersionString())
// Load configuration file.
configData, err := os.ReadFile(*configFile)
cmd.FailOnError(err, fmt.Sprintf("Reading %q", *configFile))
// Unmarshal JSON config file.
var cfg Config
err = json.Unmarshal(configData, &cfg)
cmd.FailOnError(err, "Unmarshaling config")
features.Set(cfg.ContactExporter.Features)
dbMap, err := sa.InitWrappedDb(cfg.ContactExporter.DB, nil, log)
cmd.FailOnError(err, "While initializing dbMap")
exporter := idExporter{
log: log,
dbMap: dbMap,
clk: cmd.Clock(),
grace: *grace,
}
var results idExporterResults
if *hostnamesFile != "" {
hostnames, err := unmarshalHostnames(*hostnamesFile)
cmd.FailOnError(err, "Problem unmarshalling hostnames")
results, err = exporter.findIDsForHostnames(context.TODO(), hostnames)
cmd.FailOnError(err, "Could not find IDs for hostnames")
} else if *withExampleHostnames {
results, err = exporter.findIDsWithExampleHostnames(context.TODO())
cmd.FailOnError(err, "Could not find IDs with hostnames")
} else {
results, err = exporter.findIDs(context.TODO())
cmd.FailOnError(err, "Could not find IDs")
}
err = results.writeToFile(*outFile)
cmd.FailOnError(err, fmt.Sprintf("Could not write result to outfile %q", *outFile))
}
func init() {
cmd.RegisterCommand("id-exporter", main, &cmd.ConfigValidator{Config: &Config{}})
}

View file

@ -1,486 +0,0 @@
package notmain
import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
"fmt"
"math/big"
"net"
"os"
"testing"
"time"
"github.com/jmhodges/clock"
"github.com/letsencrypt/boulder/core"
corepb "github.com/letsencrypt/boulder/core/proto"
blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/metrics"
"github.com/letsencrypt/boulder/sa"
sapb "github.com/letsencrypt/boulder/sa/proto"
"github.com/letsencrypt/boulder/test"
isa "github.com/letsencrypt/boulder/test/inmem/sa"
"github.com/letsencrypt/boulder/test/vars"
)
var (
regA *corepb.Registration
regB *corepb.Registration
regC *corepb.Registration
regD *corepb.Registration
)
const (
emailARaw = "test@example.com"
emailBRaw = "example@example.com"
emailCRaw = "test-example@example.com"
telNum = "666-666-7777"
)
func TestFindIDs(t *testing.T) {
ctx := context.Background()
testCtx := setup(t)
defer testCtx.cleanUp()
// Add some test registrations
testCtx.addRegistrations(t)
// Run findIDs - since no certificates have been added corresponding to
// the above registrations, no IDs should be found.
results, err := testCtx.c.findIDs(ctx)
test.AssertNotError(t, err, "findIDs() produced error")
test.AssertEquals(t, len(results), 0)
// Now add some certificates
testCtx.addCertificates(t)
// Run findIDs - since there are three registrations with unexpired certs
// we should get exactly three IDs back: RegA, RegC and RegD. RegB should
// *not* be present since their certificate has already expired. Unlike
// previous versions of this test RegD is not filtered out for having a `tel:`
// contact field anymore - this is the duty of the notify-mailer.
results, err = testCtx.c.findIDs(ctx)
test.AssertNotError(t, err, "findIDs() produced error")
test.AssertEquals(t, len(results), 3)
for _, entry := range results {
switch entry.ID {
case regA.Id:
case regC.Id:
case regD.Id:
default:
t.Errorf("ID: %d not expected", entry.ID)
}
}
// Allow a 1 year grace period
testCtx.c.grace = 360 * 24 * time.Hour
results, err = testCtx.c.findIDs(ctx)
test.AssertNotError(t, err, "findIDs() produced error")
// Now all four registration should be returned, including RegB since its
// certificate expired within the grace period
for _, entry := range results {
switch entry.ID {
case regA.Id:
case regB.Id:
case regC.Id:
case regD.Id:
default:
t.Errorf("ID: %d not expected", entry.ID)
}
}
}
func TestFindIDsWithExampleHostnames(t *testing.T) {
ctx := context.Background()
testCtx := setup(t)
defer testCtx.cleanUp()
// Add some test registrations
testCtx.addRegistrations(t)
// Run findIDsWithExampleHostnames - since no certificates have been
// added corresponding to the above registrations, no IDs should be
// found.
results, err := testCtx.c.findIDsWithExampleHostnames(ctx)
test.AssertNotError(t, err, "findIDs() produced error")
test.AssertEquals(t, len(results), 0)
// Now add some certificates
testCtx.addCertificates(t)
// Run findIDsWithExampleHostnames - since there are three
// registrations with unexpired certs we should get exactly three
// IDs back: RegA, RegC and RegD. RegB should *not* be present since
// their certificate has already expired.
results, err = testCtx.c.findIDsWithExampleHostnames(ctx)
test.AssertNotError(t, err, "findIDs() produced error")
test.AssertEquals(t, len(results), 3)
for _, entry := range results {
switch entry.ID {
case regA.Id:
test.AssertEquals(t, entry.Hostname, "example-a.com")
case regC.Id:
test.AssertEquals(t, entry.Hostname, "example-c.com")
case regD.Id:
test.AssertEquals(t, entry.Hostname, "example-d.com")
default:
t.Errorf("ID: %d not expected", entry.ID)
}
}
// Allow a 1 year grace period
testCtx.c.grace = 360 * 24 * time.Hour
results, err = testCtx.c.findIDsWithExampleHostnames(ctx)
test.AssertNotError(t, err, "findIDs() produced error")
// Now all four registrations should be returned, including RegB
// since it expired within the grace period
test.AssertEquals(t, len(results), 4)
for _, entry := range results {
switch entry.ID {
case regA.Id:
test.AssertEquals(t, entry.Hostname, "example-a.com")
case regB.Id:
test.AssertEquals(t, entry.Hostname, "example-b.com")
case regC.Id:
test.AssertEquals(t, entry.Hostname, "example-c.com")
case regD.Id:
test.AssertEquals(t, entry.Hostname, "example-d.com")
default:
t.Errorf("ID: %d not expected", entry.ID)
}
}
}
func TestFindIDsForHostnames(t *testing.T) {
ctx := context.Background()
testCtx := setup(t)
defer testCtx.cleanUp()
// Add some test registrations
testCtx.addRegistrations(t)
// Run findIDsForHostnames - since no certificates have been added corresponding to
// the above registrations, no IDs should be found.
results, err := testCtx.c.findIDsForHostnames(ctx, []string{"example-a.com", "example-b.com", "example-c.com", "example-d.com"})
test.AssertNotError(t, err, "findIDs() produced error")
test.AssertEquals(t, len(results), 0)
// Now add some certificates
testCtx.addCertificates(t)
results, err = testCtx.c.findIDsForHostnames(ctx, []string{"example-a.com", "example-b.com", "example-c.com", "example-d.com"})
test.AssertNotError(t, err, "findIDsForHostnames() failed")
test.AssertEquals(t, len(results), 3)
for _, entry := range results {
switch entry.ID {
case regA.Id:
case regC.Id:
case regD.Id:
default:
t.Errorf("ID: %d not expected", entry.ID)
}
}
}
func TestWriteToFile(t *testing.T) {
expected := `[{"id":1},{"id":2},{"id":3}]`
mockResults := idExporterResults{{ID: 1}, {ID: 2}, {ID: 3}}
dir := os.TempDir()
f, err := os.CreateTemp(dir, "ids_test")
test.AssertNotError(t, err, "os.CreateTemp produced an error")
// Writing the result to an outFile should produce the correct results
err = mockResults.writeToFile(f.Name())
test.AssertNotError(t, err, fmt.Sprintf("writeIDs produced an error writing to %s", f.Name()))
contents, err := os.ReadFile(f.Name())
test.AssertNotError(t, err, fmt.Sprintf("os.ReadFile produced an error reading from %s", f.Name()))
test.AssertEquals(t, string(contents), expected+"\n")
}
func Test_unmarshalHostnames(t *testing.T) {
testDir := os.TempDir()
testFile, err := os.CreateTemp(testDir, "ids_test")
test.AssertNotError(t, err, "os.CreateTemp produced an error")
// Non-existent hostnamesFile
_, err = unmarshalHostnames("file_does_not_exist")
test.AssertError(t, err, "expected error for non-existent file")
// Empty hostnamesFile
err = os.WriteFile(testFile.Name(), []byte(""), 0644)
test.AssertNotError(t, err, "os.WriteFile produced an error")
_, err = unmarshalHostnames(testFile.Name())
test.AssertError(t, err, "expected error for file containing 0 entries")
// One hostname present in the hostnamesFile
err = os.WriteFile(testFile.Name(), []byte("example-a.com"), 0644)
test.AssertNotError(t, err, "os.WriteFile produced an error")
results, err := unmarshalHostnames(testFile.Name())
test.AssertNotError(t, err, "error when unmarshalling hostnamesFile with a single hostname")
test.AssertEquals(t, len(results), 1)
// Two hostnames present in the hostnamesFile
err = os.WriteFile(testFile.Name(), []byte("example-a.com\nexample-b.com"), 0644)
test.AssertNotError(t, err, "os.WriteFile produced an error")
results, err = unmarshalHostnames(testFile.Name())
test.AssertNotError(t, err, "error when unmarshalling hostnamesFile with a two hostnames")
test.AssertEquals(t, len(results), 2)
// Three hostnames present in the hostnamesFile but two are separated only by a space
err = os.WriteFile(testFile.Name(), []byte("example-a.com\nexample-b.com example-c.com"), 0644)
test.AssertNotError(t, err, "os.WriteFile produced an error")
_, err = unmarshalHostnames(testFile.Name())
test.AssertError(t, err, "error when unmarshalling hostnamesFile with three space separated domains")
}
type testCtx struct {
c idExporter
ssa sapb.StorageAuthorityClient
cleanUp func()
}
func (tc testCtx) addRegistrations(t *testing.T) {
emailA := "mailto:" + emailARaw
emailB := "mailto:" + emailBRaw
emailC := "mailto:" + emailCRaw
tel := "tel:" + telNum
// Every registration needs a unique JOSE key
jsonKeyA := []byte(`{
"kty":"RSA",
"n":"0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw",
"e":"AQAB"
}`)
jsonKeyB := []byte(`{
"kty":"RSA",
"n":"z8bp-jPtHt4lKBqepeKF28g_QAEOuEsCIou6sZ9ndsQsEjxEOQxQ0xNOQezsKa63eogw8YS3vzjUcPP5BJuVzfPfGd5NVUdT-vSSwxk3wvk_jtNqhrpcoG0elRPQfMVsQWmxCAXCVRz3xbcFI8GTe-syynG3l-g1IzYIIZVNI6jdljCZML1HOMTTW4f7uJJ8mM-08oQCeHbr5ejK7O2yMSSYxW03zY-Tj1iVEebROeMv6IEEJNFSS4yM-hLpNAqVuQxFGetwtwjDMC1Drs1dTWrPuUAAjKGrP151z1_dE74M5evpAhZUmpKv1hY-x85DC6N0hFPgowsanmTNNiV75w",
"e":"AAEAAQ"
}`)
jsonKeyC := []byte(`{
"kty":"RSA",
"n":"rFH5kUBZrlPj73epjJjyCxzVzZuV--JjKgapoqm9pOuOt20BUTdHqVfC2oDclqM7HFhkkX9OSJMTHgZ7WaVqZv9u1X2yjdx9oVmMLuspX7EytW_ZKDZSzL-sCOFCuQAuYKkLbsdcA3eHBK_lwc4zwdeHFMKIulNvLqckkqYB9s8GpgNXBDIQ8GjR5HuJke_WUNjYHSd8jY1LU9swKWsLQe2YoQUz_ekQvBvBCoaFEtrtRaSJKNLIVDObXFr2TLIiFiM0Em90kK01-eQ7ZiruZTKomll64bRFPoNo4_uwubddg3xTqur2vdF3NyhTrYdvAgTem4uC0PFjEQ1bK_djBQ",
"e":"AQAB"
}`)
jsonKeyD := []byte(`{
"kty":"RSA",
"n":"rFH5kUBZrlPj73epjJjyCxzVzZuV--JjKgapoqm9pOuOt20BUTdHqVfC2oDclqM7HFhkkX9OSJMTHgZ7WaVqZv9u1X2yjdx9oVmMLuspX7EytW_ZKDZSzL-FCOFCuQAuYKkLbsdcA3eHBK_lwc4zwdeHFMKIulNvLqckkqYB9s8GpgNXBDIQ8GjR5HuJke_WUNjYHSd8jY1LU9swKWsLQe2YoQUz_ekQvBvBCoaFEtrtRaSJKNLIVDObXFr2TLIiFiM0Em90kK01-eQ7ZiruZTKomll64bRFPoNo4_uwubddg3xTqur2vdF3NyhTrYdvAgTem4uC0PFjEQ1bK_djBQ",
"e":"AQAB"
}`)
initialIP, err := net.ParseIP("127.0.0.1").MarshalText()
test.AssertNotError(t, err, "Couldn't create initialIP")
// Regs A through C have `mailto:` contact ACME URL's
regA = &corepb.Registration{
Id: 1,
Contact: []string{emailA},
Key: jsonKeyA,
InitialIP: initialIP,
}
regB = &corepb.Registration{
Id: 2,
Contact: []string{emailB},
Key: jsonKeyB,
InitialIP: initialIP,
}
regC = &corepb.Registration{
Id: 3,
Contact: []string{emailC},
Key: jsonKeyC,
InitialIP: initialIP,
}
// Reg D has a `tel:` contact ACME URL
regD = &corepb.Registration{
Id: 4,
Contact: []string{tel},
Key: jsonKeyD,
InitialIP: initialIP,
}
// Add the four test registrations
ctx := context.Background()
regA, err = tc.ssa.NewRegistration(ctx, regA)
test.AssertNotError(t, err, "Couldn't store regA")
regB, err = tc.ssa.NewRegistration(ctx, regB)
test.AssertNotError(t, err, "Couldn't store regB")
regC, err = tc.ssa.NewRegistration(ctx, regC)
test.AssertNotError(t, err, "Couldn't store regC")
regD, err = tc.ssa.NewRegistration(ctx, regD)
test.AssertNotError(t, err, "Couldn't store regD")
}
func (tc testCtx) addCertificates(t *testing.T) {
ctx := context.Background()
serial1 := big.NewInt(1336)
serial1String := core.SerialToString(serial1)
serial2 := big.NewInt(1337)
serial2String := core.SerialToString(serial2)
serial3 := big.NewInt(1338)
serial3String := core.SerialToString(serial3)
serial4 := big.NewInt(1339)
serial4String := core.SerialToString(serial4)
n := bigIntFromB64("n4EPtAOCc9AlkeQHPzHStgAbgs7bTZLwUBZdR8_KuKPEHLd4rHVTeT-O-XV2jRojdNhxJWTDvNd7nqQ0VEiZQHz_AJmSCpMaJMRBSFKrKb2wqVwGU_NsYOYL-QtiWN2lbzcEe6XC0dApr5ydQLrHqkHHig3RBordaZ6Aj-oBHqFEHYpPe7Tpe-OfVfHd1E6cS6M1FZcD1NNLYD5lFHpPI9bTwJlsde3uhGqC0ZCuEHg8lhzwOHrtIQbS0FVbb9k3-tVTU4fg_3L_vniUFAKwuCLqKnS2BYwdq_mzSnbLY7h_qixoR7jig3__kRhuaxwUkRz5iaiQkqgc5gHdrNP5zw==")
e := intFromB64("AQAB")
d := bigIntFromB64("bWUC9B-EFRIo8kpGfh0ZuyGPvMNKvYWNtB_ikiH9k20eT-O1q_I78eiZkpXxXQ0UTEs2LsNRS-8uJbvQ-A1irkwMSMkK1J3XTGgdrhCku9gRldY7sNA_AKZGh-Q661_42rINLRCe8W-nZ34ui_qOfkLnK9QWDDqpaIsA-bMwWWSDFu2MUBYwkHTMEzLYGqOe04noqeq1hExBTHBOBdkMXiuFhUq1BU6l-DqEiWxqg82sXt2h-LMnT3046AOYJoRioz75tSUQfGCshWTBnP5uDjd18kKhyv07lhfSJdrPdM5Plyl21hsFf4L_mHCuoFau7gdsPfHPxxjVOcOpBrQzwQ==")
p := bigIntFromB64("uKE2dh-cTf6ERF4k4e_jy78GfPYUIaUyoSSJuBzp3Cubk3OCqs6grT8bR_cu0Dm1MZwWmtdqDyI95HrUeq3MP15vMMON8lHTeZu2lmKvwqW7anV5UzhM1iZ7z4yMkuUwFWoBvyY898EXvRD-hdqRxHlSqAZ192zB3pVFJ0s7pFc=")
q := bigIntFromB64("uKE2dh-cTf6ERF4k4e_jy78GfPYUIaUyoSSJuBzp3Cubk3OCqs6grT8bR_cu0Dm1MZwWmtdqDyI95HrUeq3MP15vMMON8lHTeZu2lmKvwqW7anV5UzhM1iZ7z4yMkuUwFWoBvyY898EXvRD-hdqRxHlSqAZ192zB3pVFJ0s7pFc=")
testKey := rsa.PrivateKey{
PublicKey: rsa.PublicKey{N: n, E: e},
D: d,
Primes: []*big.Int{p, q},
}
fc := clock.NewFake()
// Add one cert for RegA that expires in 30 days
rawCertA := x509.Certificate{
Subject: pkix.Name{
CommonName: "happy A",
},
NotAfter: fc.Now().Add(30 * 24 * time.Hour),
DNSNames: []string{"example-a.com"},
SerialNumber: serial1,
}
certDerA, _ := x509.CreateCertificate(rand.Reader, &rawCertA, &rawCertA, &testKey.PublicKey, &testKey)
certA := &core.Certificate{
RegistrationID: regA.Id,
Serial: serial1String,
Expires: rawCertA.NotAfter,
DER: certDerA,
}
err := tc.c.dbMap.Insert(ctx, certA)
test.AssertNotError(t, err, "Couldn't add certA")
_, err = tc.c.dbMap.ExecContext(
ctx,
"INSERT INTO issuedNames (reversedName, serial, notBefore) VALUES (?,?,0)",
"com.example-a",
serial1String,
)
test.AssertNotError(t, err, "Couldn't add issued name for certA")
// Add one cert for RegB that already expired 30 days ago
rawCertB := x509.Certificate{
Subject: pkix.Name{
CommonName: "happy B",
},
NotAfter: fc.Now().Add(-30 * 24 * time.Hour),
DNSNames: []string{"example-b.com"},
SerialNumber: serial2,
}
certDerB, _ := x509.CreateCertificate(rand.Reader, &rawCertB, &rawCertB, &testKey.PublicKey, &testKey)
certB := &core.Certificate{
RegistrationID: regB.Id,
Serial: serial2String,
Expires: rawCertB.NotAfter,
DER: certDerB,
}
err = tc.c.dbMap.Insert(ctx, certB)
test.AssertNotError(t, err, "Couldn't add certB")
_, err = tc.c.dbMap.ExecContext(
ctx,
"INSERT INTO issuedNames (reversedName, serial, notBefore) VALUES (?,?,0)",
"com.example-b",
serial2String,
)
test.AssertNotError(t, err, "Couldn't add issued name for certB")
// Add one cert for RegC that expires in 30 days
rawCertC := x509.Certificate{
Subject: pkix.Name{
CommonName: "happy C",
},
NotAfter: fc.Now().Add(30 * 24 * time.Hour),
DNSNames: []string{"example-c.com"},
SerialNumber: serial3,
}
certDerC, _ := x509.CreateCertificate(rand.Reader, &rawCertC, &rawCertC, &testKey.PublicKey, &testKey)
certC := &core.Certificate{
RegistrationID: regC.Id,
Serial: serial3String,
Expires: rawCertC.NotAfter,
DER: certDerC,
}
err = tc.c.dbMap.Insert(ctx, certC)
test.AssertNotError(t, err, "Couldn't add certC")
_, err = tc.c.dbMap.ExecContext(
ctx,
"INSERT INTO issuedNames (reversedName, serial, notBefore) VALUES (?,?,0)",
"com.example-c",
serial3String,
)
test.AssertNotError(t, err, "Couldn't add issued name for certC")
// Add one cert for RegD that expires in 30 days
rawCertD := x509.Certificate{
Subject: pkix.Name{
CommonName: "happy D",
},
NotAfter: fc.Now().Add(30 * 24 * time.Hour),
DNSNames: []string{"example-d.com"},
SerialNumber: serial4,
}
certDerD, _ := x509.CreateCertificate(rand.Reader, &rawCertD, &rawCertD, &testKey.PublicKey, &testKey)
certD := &core.Certificate{
RegistrationID: regD.Id,
Serial: serial4String,
Expires: rawCertD.NotAfter,
DER: certDerD,
}
err = tc.c.dbMap.Insert(ctx, certD)
test.AssertNotError(t, err, "Couldn't add certD")
_, err = tc.c.dbMap.ExecContext(
ctx,
"INSERT INTO issuedNames (reversedName, serial, notBefore) VALUES (?,?,0)",
"com.example-d",
serial4String,
)
test.AssertNotError(t, err, "Couldn't add issued name for certD")
}
func setup(t *testing.T) testCtx {
log := blog.UseMock()
fc := clock.NewFake()
// Using DBConnSAFullPerms to be able to insert registrations and certificates
dbMap, err := sa.DBMapForTest(vars.DBConnSAFullPerms)
if err != nil {
t.Fatalf("Couldn't connect the database: %s", err)
}
cleanUp := test.ResetBoulderTestDatabase(t)
ssa, err := sa.NewSQLStorageAuthority(dbMap, dbMap, nil, 1, 0, fc, log, metrics.NoopRegisterer)
if err != nil {
t.Fatalf("unable to create SQLStorageAuthority: %s", err)
}
return testCtx{
c: idExporter{
dbMap: dbMap,
log: log,
clk: fc,
},
ssa: isa.SA{Impl: ssa},
cleanUp: cleanUp,
}
}
func bigIntFromB64(b64 string) *big.Int {
bytes, _ := base64.URLEncoding.DecodeString(b64)
x := big.NewInt(0)
x.SetBytes(bytes)
return x
}
func intFromB64(b64 string) int {
return int(bigIntFromB64(b64).Int64())
}

View file

@ -5,6 +5,7 @@ import (
"flag"
"fmt"
"net"
"net/netip"
"os"
"github.com/letsencrypt/boulder/cmd"
@ -19,28 +20,20 @@ type Config struct {
MaxUsed int
// UseDerivablePrefix indicates whether to use a nonce prefix derived
// from the gRPC listening address. If this is false, the nonce prefix
// will be the value of the NoncePrefix field. If this is true, the
// NoncePrefixKey field is required.
// TODO(#6610): Remove this.
//
// Deprecated: this value is ignored, and treated as though it is always true.
UseDerivablePrefix bool `validate:"-"`
// NoncePrefixKey is a secret used for deriving the prefix of each nonce
// instance. It should contain 256 bits (32 bytes) of random data to be
// suitable as an HMAC-SHA256 key (e.g. the output of `openssl rand -hex
// 32`). In a multi-DC deployment this value should be the same across
// all boulder-wfe and nonce-service instances.
NoncePrefixKey cmd.PasswordConfig `validate:"required"`
// NonceHMACKey is a path to a file containing an HMAC key which is a
// secret used for deriving the prefix of each nonce instance. It should
// contain 256 bits (32 bytes) of random data to be suitable as an
// HMAC-SHA256 key (e.g. the output of `openssl rand -hex 32`). In a
// multi-DC deployment this value should be the same across all
// boulder-wfe and nonce-service instances.
NonceHMACKey cmd.HMACKeyConfig `validate:"required"`
Syslog cmd.SyslogConfig
OpenTelemetry cmd.OpenTelemetryConfig
}
}
func derivePrefix(key string, grpcAddr string) (string, error) {
func derivePrefix(key []byte, grpcAddr string) (string, error) {
host, port, err := net.SplitHostPort(grpcAddr)
if err != nil {
return "", fmt.Errorf("parsing gRPC listen address: %w", err)
@ -49,8 +42,8 @@ func derivePrefix(key string, grpcAddr string) (string, error) {
return "", fmt.Errorf("nonce service gRPC address must include an IP address: got %q", grpcAddr)
}
if host != "" && port != "" {
hostIP := net.ParseIP(host)
if hostIP == nil {
hostIP, err := netip.ParseAddr(host)
if err != nil {
return "", fmt.Errorf("gRPC address host part was not an IP address")
}
if hostIP.IsUnspecified() {
@ -82,12 +75,9 @@ func main() {
c.NonceService.DebugAddr = *debugAddr
}
if c.NonceService.NoncePrefixKey.PasswordFile == "" {
cmd.Fail("NoncePrefixKey PasswordFile must be set")
}
key, err := c.NonceService.NonceHMACKey.Load()
cmd.FailOnError(err, "Failed to load nonceHMACKey file.")
key, err := c.NonceService.NoncePrefixKey.Pass()
cmd.FailOnError(err, "Failed to load 'noncePrefixKey' file.")
noncePrefix, err := derivePrefix(key, c.NonceService.GRPC.Address)
cmd.FailOnError(err, "Failed to derive nonce prefix")

View file

@ -1,619 +0,0 @@
package notmain
import (
"context"
"encoding/csv"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"net/mail"
"os"
"sort"
"strconv"
"strings"
"sync"
"text/template"
"time"
"github.com/jmhodges/clock"
"github.com/letsencrypt/boulder/cmd"
"github.com/letsencrypt/boulder/db"
blog "github.com/letsencrypt/boulder/log"
bmail "github.com/letsencrypt/boulder/mail"
"github.com/letsencrypt/boulder/metrics"
"github.com/letsencrypt/boulder/policy"
"github.com/letsencrypt/boulder/sa"
)
type mailer struct {
clk clock.Clock
log blog.Logger
dbMap dbSelector
mailer bmail.Mailer
subject string
emailTemplate *template.Template
recipients []recipient
targetRange interval
sleepInterval time.Duration
parallelSends uint
}
// interval defines a range of email addresses to send to in alphabetical order.
// The `start` field is inclusive and the `end` field is exclusive. To include
// everything, set `end` to \xFF.
type interval struct {
start string
end string
}
// contactQueryResult is a receiver for queries to the `registrations` table.
type contactQueryResult struct {
// ID is exported to receive the value of `id`.
ID int64
// Contact is exported to receive the value of `contact`.
Contact []byte
}
func (i *interval) ok() error {
if i.start > i.end {
return fmt.Errorf("interval start value (%s) is greater than end value (%s)",
i.start, i.end)
}
return nil
}
func (i *interval) includes(s string) bool {
return s >= i.start && s < i.end
}
// ok ensures that both the `targetRange` and `sleepInterval` are valid.
func (m *mailer) ok() error {
err := m.targetRange.ok()
if err != nil {
return err
}
if m.sleepInterval < 0 {
return fmt.Errorf(
"sleep interval (%d) is < 0", m.sleepInterval)
}
return nil
}
func (m *mailer) logStatus(to string, current, total int, start time.Time) {
// Should never happen.
if total <= 0 || current < 1 || current > total {
m.log.AuditErrf("Invalid current (%d) or total (%d)", current, total)
}
completion := (float32(current) / float32(total)) * 100
now := m.clk.Now()
elapsed := now.Sub(start)
m.log.Infof("Sending message (%d) of (%d) to address (%s) [%.2f%%] time elapsed (%s)",
current, total, to, completion, elapsed)
}
func sortAddresses(input addressToRecipientMap) []string {
var addresses []string
for address := range input {
addresses = append(addresses, address)
}
sort.Strings(addresses)
return addresses
}
// makeMessageBody is a helper for mailer.run() that's split out for the
// purposes of testing.
func (m *mailer) makeMessageBody(recipients []recipient) (string, error) {
var messageBody strings.Builder
err := m.emailTemplate.Execute(&messageBody, recipients)
if err != nil {
return "", err
}
if messageBody.Len() == 0 {
return "", errors.New("templating resulted in an empty message body")
}
return messageBody.String(), nil
}
func (m *mailer) run(ctx context.Context) error {
err := m.ok()
if err != nil {
return err
}
totalRecipients := len(m.recipients)
m.log.Infof("Resolving addresses for (%d) recipients", totalRecipients)
addressToRecipient, err := m.resolveAddresses(ctx)
if err != nil {
return err
}
totalAddresses := len(addressToRecipient)
if totalAddresses == 0 {
return errors.New("0 recipients remained after resolving addresses")
}
m.log.Infof("%d recipients were resolved to %d addresses", totalRecipients, totalAddresses)
var mostRecipients string
var mostRecipientsLen int
for k, v := range addressToRecipient {
if len(v) > mostRecipientsLen {
mostRecipientsLen = len(v)
mostRecipients = k
}
}
m.log.Infof("Address %q was associated with the most recipients (%d)",
mostRecipients, mostRecipientsLen)
type work struct {
index int
address string
}
var wg sync.WaitGroup
workChan := make(chan work, totalAddresses)
startTime := m.clk.Now()
sortedAddresses := sortAddresses(addressToRecipient)
if (m.targetRange.start != "" && m.targetRange.start > sortedAddresses[totalAddresses-1]) ||
(m.targetRange.end != "" && m.targetRange.end < sortedAddresses[0]) {
return errors.New("Zero found addresses fall inside target range")
}
go func(ch chan<- work) {
for i, address := range sortedAddresses {
ch <- work{i, address}
}
close(workChan)
}(workChan)
if m.parallelSends < 1 {
m.parallelSends = 1
}
for senderNum := uint(0); senderNum < m.parallelSends; senderNum++ {
// For politeness' sake, don't open more than 1 new connection per
// second.
if senderNum > 0 {
m.clk.Sleep(time.Second)
}
conn, err := m.mailer.Connect()
if err != nil {
return fmt.Errorf("connecting parallel sender %d: %w", senderNum, err)
}
wg.Add(1)
go func(conn bmail.Conn, ch <-chan work) {
defer wg.Done()
for w := range ch {
if !m.targetRange.includes(w.address) {
m.log.Debugf("Address %q is outside of target range, skipping", w.address)
continue
}
err := policy.ValidEmail(w.address)
if err != nil {
m.log.Infof("Skipping %q due to policy violation: %s", w.address, err)
continue
}
recipients := addressToRecipient[w.address]
m.logStatus(w.address, w.index+1, totalAddresses, startTime)
messageBody, err := m.makeMessageBody(recipients)
if err != nil {
m.log.Errf("Skipping %q due to templating error: %s", w.address, err)
continue
}
err = conn.SendMail([]string{w.address}, m.subject, messageBody)
if err != nil {
var badAddrErr bmail.BadAddressSMTPError
if errors.As(err, &badAddrErr) {
m.log.Errf("address %q was rejected by server: %s", w.address, err)
continue
}
m.log.AuditErrf("while sending mail (%d) of (%d) to address %q: %s",
w.index, len(sortedAddresses), w.address, err)
}
m.clk.Sleep(m.sleepInterval)
}
conn.Close()
}(conn, workChan)
}
wg.Wait()
return nil
}
// resolveAddresses creates a mapping of email addresses to (a list of)
// `recipient`s that resolve to that email address.
func (m *mailer) resolveAddresses(ctx context.Context) (addressToRecipientMap, error) {
result := make(addressToRecipientMap, len(m.recipients))
for _, recipient := range m.recipients {
addresses, err := getAddressForID(ctx, recipient.id, m.dbMap)
if err != nil {
return nil, err
}
for _, address := range addresses {
parsed, err := mail.ParseAddress(address)
if err != nil {
m.log.Errf("Unparsable address %q, skipping ID (%d)", address, recipient.id)
continue
}
result[parsed.Address] = append(result[parsed.Address], recipient)
}
}
return result, nil
}
// dbSelector abstracts over a subset of methods from `borp.DbMap` objects to
// facilitate mocking in unit tests.
type dbSelector interface {
SelectOne(ctx context.Context, holder interface{}, query string, args ...interface{}) error
}
// getAddressForID queries the database for the email address associated with
// the provided registration ID.
func getAddressForID(ctx context.Context, id int64, dbMap dbSelector) ([]string, error) {
var result contactQueryResult
err := dbMap.SelectOne(ctx, &result,
`SELECT id,
contact
FROM registrations
WHERE contact NOT IN ('[]', 'null')
AND id = :id;`,
map[string]interface{}{"id": id})
if err != nil {
if db.IsNoRows(err) {
return []string{}, nil
}
return nil, err
}
var contacts []string
err = json.Unmarshal(result.Contact, &contacts)
if err != nil {
return nil, err
}
var addresses []string
for _, contact := range contacts {
if strings.HasPrefix(contact, "mailto:") {
addresses = append(addresses, strings.TrimPrefix(contact, "mailto:"))
}
}
return addresses, nil
}
// recipient represents a single record from the recipient list file. The 'id'
// column is parsed to the 'id' field, all additional data will be parsed to a
// mapping of column name to value in the 'Data' field. Please inform SRE if you
// make any changes to the exported fields of this struct. These fields are
// referenced in operationally critical e-mail templates used to notify
// subscribers during incident response.
type recipient struct {
// id is the subscriber's ID.
id int64
// Data is a mapping of column name to value parsed from a single record in
// the provided recipient list file. It's exported so the contents can be
// accessed by the template package. Please inform SRE if you make any
// changes to this field.
Data map[string]string
}
// addressToRecipientMap maps email addresses to a list of `recipient`s that
// resolve to that email address.
type addressToRecipientMap map[string][]recipient
// readRecipientsList parses the contents of a recipient list file into a list
// of `recipient` objects.
func readRecipientsList(filename string, delimiter rune) ([]recipient, string, error) {
f, err := os.Open(filename)
if err != nil {
return nil, "", err
}
reader := csv.NewReader(f)
reader.Comma = delimiter
// Parse header.
record, err := reader.Read()
if err != nil {
return nil, "", fmt.Errorf("failed to parse header: %w", err)
}
if record[0] != "id" {
return nil, "", errors.New("header must begin with \"id\"")
}
// Collect the names of each header column after `id`.
var dataColumns []string
for _, v := range record[1:] {
dataColumns = append(dataColumns, strings.TrimSpace(v))
if len(v) == 0 {
return nil, "", errors.New("header contains an empty column")
}
}
var recordsWithEmptyColumns []int64
var recordsWithDuplicateIDs []int64
var probsBuff strings.Builder
stringProbs := func() string {
if len(recordsWithEmptyColumns) != 0 {
fmt.Fprintf(&probsBuff, "ID(s) %v contained empty columns and ",
recordsWithEmptyColumns)
}
if len(recordsWithDuplicateIDs) != 0 {
fmt.Fprintf(&probsBuff, "ID(s) %v were skipped as duplicates",
recordsWithDuplicateIDs)
}
if probsBuff.Len() == 0 {
return ""
}
return strings.TrimSuffix(probsBuff.String(), " and ")
}
// Parse records.
recipientIDs := make(map[int64]bool)
var recipients []recipient
for {
record, err := reader.Read()
if errors.Is(err, io.EOF) {
// Finished parsing the file.
if len(recipients) == 0 {
return nil, stringProbs(), errors.New("no records after header")
}
return recipients, stringProbs(), nil
} else if err != nil {
return nil, "", err
}
// Ensure the first column of each record can be parsed as a valid
// registration ID.
recordID := record[0]
id, err := strconv.ParseInt(recordID, 10, 64)
if err != nil {
return nil, "", fmt.Errorf(
"%q couldn't be parsed as a registration ID due to: %s", recordID, err)
}
// Skip records that have the same ID as those read previously.
if recipientIDs[id] {
recordsWithDuplicateIDs = append(recordsWithDuplicateIDs, id)
continue
}
recipientIDs[id] = true
// Collect the columns of data after `id` into a map.
var emptyColumn bool
data := make(map[string]string)
for i, v := range record[1:] {
if len(v) == 0 {
emptyColumn = true
}
data[dataColumns[i]] = v
}
// Only used for logging.
if emptyColumn {
recordsWithEmptyColumns = append(recordsWithEmptyColumns, id)
}
recipients = append(recipients, recipient{id, data})
}
}
const usageIntro = `
Introduction:
The notification mailer exists to send a message to the contact associated
with a list of registration IDs. The attributes of the message (from address,
subject, and message content) are provided by the command line arguments. The
message content is provided as a path to a template file via the -body argument.
Provide a list of recipient user ids in a CSV file passed with the -recipientList
flag. The CSV file must have "id" as the first column and may have additional
fields to be interpolated into the email template:
id, lastIssuance
1234, "from example.com 2018-12-01"
5678, "from example.net 2018-12-13"
The additional fields will be interpolated with Golang templating, e.g.:
Your last issuance on each account was:
{{ range . }} {{ .Data.lastIssuance }}
{{ end }}
To help the operator gain confidence in the mailing run before committing fully
three safety features are supported: dry runs, intervals and a sleep between emails.
The -dryRun=true flag will use a mock mailer that prints message content to
stdout instead of performing an SMTP transaction with a real mailserver. This
can be used when the initial parameters are being tweaked to ensure no real
emails are sent. Using -dryRun=false will send real email.
Intervals supported via the -start and -end arguments. Only email addresses that
are alphabetically between the -start and -end strings will be sent. This can be used
to break up sending into batches, or more likely to resume sending if a batch is killed,
without resending messages that have already been sent. The -start flag is inclusive and
the -end flag is exclusive.
Notify-mailer de-duplicates email addresses and groups together the resulting recipient
structs, so a person who has multiple accounts using the same address will only receive
one email.
During mailing the -sleep argument is used to space out individual messages.
This can be used to ensure that the mailing happens at a steady pace with ample
opportunity for the operator to terminate early in the event of error. The
-sleep flag honours durations with a unit suffix (e.g. 1m for 1 minute, 10s for
10 seconds, etc). Using -sleep=0 will disable the sleep and send at full speed.
Examples:
Send an email with subject "Hello!" from the email "hello@goodbye.com" with
the contents read from "test_msg_body.txt" to every email associated with the
registration IDs listed in "test_reg_recipients.json", sleeping 10 seconds
between each message:
notify-mailer -config test/config/notify-mailer.json -body
cmd/notify-mailer/testdata/test_msg_body.txt -from hello@goodbye.com
-recipientList cmd/notify-mailer/testdata/test_msg_recipients.csv -subject "Hello!"
-sleep 10s -dryRun=false
Do the same, but only to example@example.com:
notify-mailer -config test/config/notify-mailer.json
-body cmd/notify-mailer/testdata/test_msg_body.txt -from hello@goodbye.com
-recipientList cmd/notify-mailer/testdata/test_msg_recipients.csv -subject "Hello!"
-start example@example.com -end example@example.comX
Send the message starting with example@example.com and emailing every address that's
alphabetically higher:
notify-mailer -config test/config/notify-mailer.json
-body cmd/notify-mailer/testdata/test_msg_body.txt -from hello@goodbye.com
-recipientList cmd/notify-mailer/testdata/test_msg_recipients.csv -subject "Hello!"
-start example@example.com
Required arguments:
- body
- config
- from
- subject
- recipientList`
type Config struct {
NotifyMailer struct {
DB cmd.DBConfig
cmd.SMTPConfig
}
Syslog cmd.SyslogConfig
}
func main() {
from := flag.String("from", "", "From header for emails. Must be a bare email address.")
subject := flag.String("subject", "", "Subject of emails")
recipientListFile := flag.String("recipientList", "", "File containing a CSV list of registration IDs and extra info.")
parseAsTSV := flag.Bool("tsv", false, "Parse the recipient list file as a TSV.")
bodyFile := flag.String("body", "", "File containing the email body in Golang template format.")
dryRun := flag.Bool("dryRun", true, "Whether to do a dry run.")
sleep := flag.Duration("sleep", 500*time.Millisecond, "How long to sleep between emails.")
parallelSends := flag.Uint("parallelSends", 1, "How many parallel goroutines should process emails")
start := flag.String("start", "", "Alphabetically lowest email address to include.")
end := flag.String("end", "\xFF", "Alphabetically highest email address (exclusive).")
reconnBase := flag.Duration("reconnectBase", 1*time.Second, "Base sleep duration between reconnect attempts")
reconnMax := flag.Duration("reconnectMax", 5*60*time.Second, "Max sleep duration between reconnect attempts after exponential backoff")
configFile := flag.String("config", "", "File containing a JSON config.")
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "%s\n\n", usageIntro)
fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0])
flag.PrintDefaults()
}
// Validate required args.
flag.Parse()
if *from == "" || *subject == "" || *bodyFile == "" || *configFile == "" || *recipientListFile == "" {
flag.Usage()
os.Exit(1)
}
configData, err := os.ReadFile(*configFile)
cmd.FailOnError(err, "Couldn't load JSON config file")
// Parse JSON config.
var cfg Config
err = json.Unmarshal(configData, &cfg)
cmd.FailOnError(err, "Couldn't unmarshal JSON config file")
log := cmd.NewLogger(cfg.Syslog)
log.Info(cmd.VersionString())
dbMap, err := sa.InitWrappedDb(cfg.NotifyMailer.DB, nil, log)
cmd.FailOnError(err, "While initializing dbMap")
// Load and parse message body.
template, err := template.ParseFiles(*bodyFile)
cmd.FailOnError(err, "Couldn't parse message template")
// Ensure that in the event of a missing key, an informative error is
// returned.
template.Option("missingkey=error")
address, err := mail.ParseAddress(*from)
cmd.FailOnError(err, fmt.Sprintf("Couldn't parse %q to address", *from))
recipientListDelimiter := ','
if *parseAsTSV {
recipientListDelimiter = '\t'
}
recipients, probs, err := readRecipientsList(*recipientListFile, recipientListDelimiter)
cmd.FailOnError(err, "Couldn't populate recipients")
if probs != "" {
log.Infof("While reading the recipient list file %s", probs)
}
var mailClient bmail.Mailer
if *dryRun {
log.Infof("Starting %s in dry-run mode", cmd.VersionString())
mailClient = bmail.NewDryRun(*address, log)
} else {
log.Infof("Starting %s", cmd.VersionString())
smtpPassword, err := cfg.NotifyMailer.PasswordConfig.Pass()
cmd.FailOnError(err, "Couldn't load SMTP password from file")
mailClient = bmail.New(
cfg.NotifyMailer.Server,
cfg.NotifyMailer.Port,
cfg.NotifyMailer.Username,
smtpPassword,
nil,
*address,
log,
metrics.NoopRegisterer,
*reconnBase,
*reconnMax)
}
m := mailer{
clk: cmd.Clock(),
log: log,
dbMap: dbMap,
mailer: mailClient,
subject: *subject,
recipients: recipients,
emailTemplate: template,
targetRange: interval{
start: *start,
end: *end,
},
sleepInterval: *sleep,
parallelSends: *parallelSends,
}
err = m.run(context.TODO())
cmd.FailOnError(err, "Couldn't complete")
log.Info("Completed successfully")
}
func init() {
cmd.RegisterCommand("notify-mailer", main, &cmd.ConfigValidator{Config: &Config{}})
}

Some files were not shown because too many files have changed in this diff Show more