Bump all dependencies except dev-tunnels
This commit is contained in:
parent
2b84a3d698
commit
96956da8de
501 changed files with 35317 additions and 31761 deletions
106
go.mod
106
go.mod
|
|
@ -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
372
go.sum
|
|
@ -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=
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
21
third-party/github.com/charmbracelet/x/exp/slice/LICENSE
vendored
Normal file
21
third-party/github.com/charmbracelet/x/exp/slice/LICENSE
vendored
Normal 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.
|
||||
|
|
@ -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
|
||||
25
third-party/github.com/hashicorp/go-version/.github/dependabot.yml
vendored
Normal file
25
third-party/github.com/hashicorp/go-version/.github/dependabot.yml
vendored
Normal 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
|
||||
74
third-party/github.com/hashicorp/go-version/.github/workflows/go-tests.yml
vendored
Normal file
74
third-party/github.com/hashicorp/go-version/.github/workflows/go-tests.yml
vendored
Normal 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 }}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
Copyright (c) 2014 HashiCorp, Inc.
|
||||
|
||||
Mozilla Public License, version 2.0
|
||||
|
||||
1. Definitions
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# Versioning Library for Go
|
||||
[](https://circleci.com/gh/hashicorp/go-version/tree/master)
|
||||

|
||||
[](https://godoc.org/github.com/hashicorp/go-version)
|
||||
|
||||
go-version is a library for parsing versions and version constraints,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package version
|
||||
|
||||
import (
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
53
third-party/github.com/letsencrypt/boulder/.github/workflows/check-iana-registries.yml
vendored
Normal file
53
third-party/github.com/letsencrypt/boulder/.github/workflows/check-iana-registries.yml
vendored
Normal 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
|
||||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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 }}"
|
||||
|
|
|
|||
|
|
@ -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 }}"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -33,5 +33,6 @@ extend-ignore-re = [
|
|||
"otConf" = "otConf"
|
||||
"serInt" = "serInt"
|
||||
"StratName" = "StratName"
|
||||
"typ" = "typ"
|
||||
"UPDATEs" = "UPDATEs"
|
||||
"vai" = "vai"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@
|
|||
[](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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
43
third-party/github.com/letsencrypt/boulder/allowlist/main.go
vendored
Normal file
43
third-party/github.com/letsencrypt/boulder/allowlist/main.go
vendored
Normal 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
|
||||
}
|
||||
109
third-party/github.com/letsencrypt/boulder/allowlist/main_test.go
vendored
Normal file
109
third-party/github.com/letsencrypt/boulder/allowlist/main_test.go
vendored
Normal 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])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)}}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
465
third-party/github.com/letsencrypt/boulder/ca/ca.go
vendored
465
third-party/github.com/letsencrypt/boulder/ca/ca.go
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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, ®IDs)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
allowList := &ECDSAAllowList{regIDsMap: makeRegIDsMap(regIDs)}
|
||||
return allowList, len(allowList.regIDsMap), nil
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,2 +0,0 @@
|
|||
---
|
||||
- 1337
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
---
|
||||
- 1338
|
||||
|
|
@ -1 +0,0 @@
|
|||
not yaml
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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.")
|
||||
}
|
||||
}
|
||||
|
|
@ -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{}})
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
59
third-party/github.com/letsencrypt/boulder/cmd/admin/admin_test.go
vendored
Normal file
59
third-party/github.com/letsencrypt/boulder/cmd/admin/admin_test.go
vendored
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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, ®IDs, "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
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
194
third-party/github.com/letsencrypt/boulder/cmd/admin/pause_identifier.go
vendored
Normal file
194
third-party/github.com/letsencrypt/boulder/cmd/admin/pause_identifier.go
vendored
Normal 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
|
||||
}
|
||||
195
third-party/github.com/letsencrypt/boulder/cmd/admin/pause_identifier_test.go
vendored
Normal file
195
third-party/github.com/letsencrypt/boulder/cmd/admin/pause_identifier_test.go
vendored
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
168
third-party/github.com/letsencrypt/boulder/cmd/admin/unpause_account.go
vendored
Normal file
168
third-party/github.com/letsencrypt/boulder/cmd/admin/unpause_account.go
vendored
Normal 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
|
||||
}
|
||||
134
third-party/github.com/letsencrypt/boulder/cmd/admin/unpause_account_test.go
vendored
Normal file
134
third-party/github.com/letsencrypt/boulder/cmd/admin/unpause_account_test.go
vendored
Normal 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))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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` |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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})
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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-----
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
|
@ -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{}})
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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 twenty‐four (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,
|
||||
|
|
|
|||
130
third-party/github.com/letsencrypt/boulder/cmd/email-exporter/main.go
vendored
Normal file
130
third-party/github.com/letsencrypt/boulder/cmd/email-exporter/main.go
vendored
Normal 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 12 MB
|
||||
// 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{}})
|
||||
}
|
||||
|
|
@ -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{}})
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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,
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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{}})
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue