diff --git a/go.mod b/go.mod index e5d499aee..e8be99e08 100644 --- a/go.mod +++ b/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 ) diff --git a/go.sum b/go.sum index f0f2bb5ed..fd2a9c32b 100644 --- a/go.sum +++ b/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= diff --git a/third-party-licenses.darwin.md b/third-party-licenses.darwin.md index 8a6271d03..60498f974 100644 --- a/third-party-licenses.darwin.md +++ b/third-party-licenses.darwin.md @@ -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 diff --git a/third-party-licenses.linux.md b/third-party-licenses.linux.md index c11e24573..cb5d2db05 100644 --- a/third-party-licenses.linux.md +++ b/third-party-licenses.linux.md @@ -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 diff --git a/third-party-licenses.windows.md b/third-party-licenses.windows.md index f175e8641..d276a5e44 100644 --- a/third-party-licenses.windows.md +++ b/third-party-licenses.windows.md @@ -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 diff --git a/third-party/github.com/alessio/shellescape/LICENSE b/third-party/al.essio.dev/pkg/shellescape/LICENSE similarity index 100% rename from third-party/github.com/alessio/shellescape/LICENSE rename to third-party/al.essio.dev/pkg/shellescape/LICENSE diff --git a/third-party/github.com/charmbracelet/x/exp/slice/LICENSE b/third-party/github.com/charmbracelet/x/exp/slice/LICENSE new file mode 100644 index 000000000..65a5654e2 --- /dev/null +++ b/third-party/github.com/charmbracelet/x/exp/slice/LICENSE @@ -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. diff --git a/third-party/github.com/hashicorp/go-version/.circleci/config.yml b/third-party/github.com/hashicorp/go-version/.circleci/config.yml deleted file mode 100644 index 221951163..000000000 --- a/third-party/github.com/hashicorp/go-version/.circleci/config.yml +++ /dev/null @@ -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 diff --git a/third-party/github.com/hashicorp/go-version/.github/dependabot.yml b/third-party/github.com/hashicorp/go-version/.github/dependabot.yml new file mode 100644 index 000000000..f401df1ef --- /dev/null +++ b/third-party/github.com/hashicorp/go-version/.github/dependabot.yml @@ -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 diff --git a/third-party/github.com/hashicorp/go-version/.github/workflows/go-tests.yml b/third-party/github.com/hashicorp/go-version/.github/workflows/go-tests.yml new file mode 100644 index 000000000..ca6882a70 --- /dev/null +++ b/third-party/github.com/hashicorp/go-version/.github/workflows/go-tests.yml @@ -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 }} \ No newline at end of file diff --git a/third-party/github.com/hashicorp/go-version/CHANGELOG.md b/third-party/github.com/hashicorp/go-version/CHANGELOG.md index dbae7f7be..6d48174bf 100644 --- a/third-party/github.com/hashicorp/go-version/CHANGELOG.md +++ b/third-party/github.com/hashicorp/go-version/CHANGELOG.md @@ -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. diff --git a/third-party/github.com/hashicorp/go-version/LICENSE b/third-party/github.com/hashicorp/go-version/LICENSE index c33dcc7c9..1409d6ab9 100644 --- a/third-party/github.com/hashicorp/go-version/LICENSE +++ b/third-party/github.com/hashicorp/go-version/LICENSE @@ -1,3 +1,5 @@ +Copyright (c) 2014 HashiCorp, Inc. + Mozilla Public License, version 2.0 1. Definitions diff --git a/third-party/github.com/hashicorp/go-version/README.md b/third-party/github.com/hashicorp/go-version/README.md index 851a337be..4b7806cd9 100644 --- a/third-party/github.com/hashicorp/go-version/README.md +++ b/third-party/github.com/hashicorp/go-version/README.md @@ -1,5 +1,5 @@ # Versioning Library for Go -[![Build Status](https://circleci.com/gh/hashicorp/go-version/tree/master.svg?style=svg)](https://circleci.com/gh/hashicorp/go-version/tree/master) +![Build Status](https://github.com/hashicorp/go-version/actions/workflows/go-tests.yml/badge.svg) [![GoDoc](https://godoc.org/github.com/hashicorp/go-version?status.svg)](https://godoc.org/github.com/hashicorp/go-version) go-version is a library for parsing versions and version constraints, diff --git a/third-party/github.com/hashicorp/go-version/constraint.go b/third-party/github.com/hashicorp/go-version/constraint.go index d05575961..29bdc4d2b 100644 --- a/third-party/github.com/hashicorp/go-version/constraint.go +++ b/third-party/github.com/hashicorp/go-version/constraint.go @@ -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) } diff --git a/third-party/github.com/hashicorp/go-version/constraint_test.go b/third-party/github.com/hashicorp/go-version/constraint_test.go index 9c5bee312..e76d3b0d5 100644 --- a/third-party/github.com/hashicorp/go-version/constraint_test.go +++ b/third-party/github.com/hashicorp/go-version/constraint_test.go @@ -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 diff --git a/third-party/github.com/hashicorp/go-version/version.go b/third-party/github.com/hashicorp/go-version/version.go index 8068834ec..7c683c281 100644 --- a/third-party/github.com/hashicorp/go-version/version.go +++ b/third-party/github.com/hashicorp/go-version/version.go @@ -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 +} diff --git a/third-party/github.com/hashicorp/go-version/version_collection.go b/third-party/github.com/hashicorp/go-version/version_collection.go index cc888d43e..83547fe13 100644 --- a/third-party/github.com/hashicorp/go-version/version_collection.go +++ b/third-party/github.com/hashicorp/go-version/version_collection.go @@ -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 diff --git a/third-party/github.com/hashicorp/go-version/version_collection_test.go b/third-party/github.com/hashicorp/go-version/version_collection_test.go index 14783d7e7..b6298a85f 100644 --- a/third-party/github.com/hashicorp/go-version/version_collection_test.go +++ b/third-party/github.com/hashicorp/go-version/version_collection_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package version import ( diff --git a/third-party/github.com/hashicorp/go-version/version_test.go b/third-party/github.com/hashicorp/go-version/version_test.go index 9fa34f6bd..8256794f3 100644 --- a/third-party/github.com/hashicorp/go-version/version_test.go +++ b/third-party/github.com/hashicorp/go-version/version_test.go @@ -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"}, diff --git a/third-party/github.com/jedisct1/go-minisign/LICENSE b/third-party/github.com/jedisct1/go-minisign/LICENSE index 010ad6e7a..7d147b428 100644 --- a/third-party/github.com/jedisct1/go-minisign/LICENSE +++ b/third-party/github.com/jedisct1/go-minisign/LICENSE @@ -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 diff --git a/third-party/github.com/letsencrypt/boulder/.github/workflows/boulder-ci.yml b/third-party/github.com/letsencrypt/boulder/.github/workflows/boulder-ci.yml index 342b0c009..f41f9767f 100644 --- a/third-party/github.com/letsencrypt/boulder/.github/workflows/boulder-ci.yml +++ b/third-party/github.com/letsencrypt/boulder/.github/workflows/boulder-ci.yml @@ -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 diff --git a/third-party/github.com/letsencrypt/boulder/.github/workflows/check-iana-registries.yml b/third-party/github.com/letsencrypt/boulder/.github/workflows/check-iana-registries.yml new file mode 100644 index 000000000..4e7884163 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/.github/workflows/check-iana-registries.yml @@ -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 diff --git a/third-party/github.com/letsencrypt/boulder/.github/workflows/issue-for-sre-handoff.yml b/third-party/github.com/letsencrypt/boulder/.github/workflows/issue-for-sre-handoff.yml index 19cdc8b09..47325aaae 100644 --- a/third-party/github.com/letsencrypt/boulder/.github/workflows/issue-for-sre-handoff.yml +++ b/third-party/github.com/letsencrypt/boulder/.github/workflows/issue-for-sre-handoff.yml @@ -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' diff --git a/third-party/github.com/letsencrypt/boulder/.github/workflows/merged-to-main-or-release-branch.yml b/third-party/github.com/letsencrypt/boulder/.github/workflows/merged-to-main-or-release-branch.yml new file mode 100644 index 000000000..6c0dd964f --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/.github/workflows/merged-to-main-or-release-branch.yml @@ -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 diff --git a/third-party/github.com/letsencrypt/boulder/.github/workflows/release.yml b/third-party/github.com/letsencrypt/boulder/.github/workflows/release.yml index ea678fc5e..88b07e63a 100644 --- a/third-party/github.com/letsencrypt/boulder/.github/workflows/release.yml +++ b/third-party/github.com/letsencrypt/boulder/.github/workflows/release.yml @@ -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 }}" diff --git a/third-party/github.com/letsencrypt/boulder/.github/workflows/try-release.yml b/third-party/github.com/letsencrypt/boulder/.github/workflows/try-release.yml index d93d696ab..e8fd363f9 100644 --- a/third-party/github.com/letsencrypt/boulder/.github/workflows/try-release.yml +++ b/third-party/github.com/letsencrypt/boulder/.github/workflows/try-release.yml @@ -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 }}" diff --git a/third-party/github.com/letsencrypt/boulder/.golangci.yml b/third-party/github.com/letsencrypt/boulder/.golangci.yml index 7e0aed488..e03d5d449 100644 --- a/third-party/github.com/letsencrypt/boulder/.golangci.yml +++ b/third-party/github.com/letsencrypt/boulder/.golangci.yml @@ -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 diff --git a/third-party/github.com/letsencrypt/boulder/.typos.toml b/third-party/github.com/letsencrypt/boulder/.typos.toml index 3451ac76a..12320dd71 100644 --- a/third-party/github.com/letsencrypt/boulder/.typos.toml +++ b/third-party/github.com/letsencrypt/boulder/.typos.toml @@ -33,5 +33,6 @@ extend-ignore-re = [ "otConf" = "otConf" "serInt" = "serInt" "StratName" = "StratName" +"typ" = "typ" "UPDATEs" = "UPDATEs" "vai" = "vai" diff --git a/third-party/github.com/letsencrypt/boulder/Makefile b/third-party/github.com/letsencrypt/boulder/Makefile index dfe15599d..9f961d492 100644 --- a/third-party/github.com/letsencrypt/boulder/Makefile +++ b/third-party/github.com/letsencrypt/boulder/Makefile @@ -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" diff --git a/third-party/github.com/letsencrypt/boulder/README.md b/third-party/github.com/letsencrypt/boulder/README.md index c12240a18..5f3c67b8f 100644 --- a/third-party/github.com/letsencrypt/boulder/README.md +++ b/third-party/github.com/letsencrypt/boulder/README.md @@ -3,10 +3,10 @@ [![Build Status](https://github.com/letsencrypt/boulder/actions/workflows/boulder-ci.yml/badge.svg?branch=main)](https://github.com/letsencrypt/boulder/actions/workflows/boulder-ci.yml?query=branch%3Amain) This is an implementation of an ACME-based CA. The [ACME -protocol](https://github.com/ietf-wg-acme/acme/) allows the CA to -automatically verify that an applicant for a certificate actually controls an -identifier, and allows domain holders to issue and revoke certificates for -their domains. Boulder is the software that runs [Let's +protocol](https://github.com/ietf-wg-acme/acme/) allows the CA to automatically +verify that an applicant for a certificate actually controls an identifier, and +allows subscribers to issue and revoke certificates for the identifiers they +control. Boulder is the software that runs [Let's Encrypt](https://letsencrypt.org). ## Contents diff --git a/third-party/github.com/letsencrypt/boulder/akamai/cache-client.go b/third-party/github.com/letsencrypt/boulder/akamai/cache-client.go index 58b51ebd5..4e54140bf 100644 --- a/third-party/github.com/letsencrypt/boulder/akamai/cache-client.go +++ b/third-party/github.com/letsencrypt/boulder/akamai/cache-client.go @@ -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" diff --git a/third-party/github.com/letsencrypt/boulder/akamai/proto/akamai.pb.go b/third-party/github.com/letsencrypt/boulder/akamai/proto/akamai.pb.go index bdc56162f..a97b96dea 100644 --- a/third-party/github.com/letsencrypt/boulder/akamai/proto/akamai.pb.go +++ b/third-party/github.com/letsencrypt/boulder/akamai/proto/akamai.pb.go @@ -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 } diff --git a/third-party/github.com/letsencrypt/boulder/akamai/proto/akamai_grpc.pb.go b/third-party/github.com/letsencrypt/boulder/akamai/proto/akamai_grpc.pb.go index 6970a2c67..f041cb585 100644 --- a/third-party/github.com/letsencrypt/boulder/akamai/proto/akamai_grpc.pb.go +++ b/third-party/github.com/letsencrypt/boulder/akamai/proto/akamai_grpc.pb.go @@ -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) } diff --git a/third-party/github.com/letsencrypt/boulder/allowlist/main.go b/third-party/github.com/letsencrypt/boulder/allowlist/main.go new file mode 100644 index 000000000..b7a0e5c35 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/allowlist/main.go @@ -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 +} diff --git a/third-party/github.com/letsencrypt/boulder/allowlist/main_test.go b/third-party/github.com/letsencrypt/boulder/allowlist/main_test.go new file mode 100644 index 000000000..97bef54cb --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/allowlist/main_test.go @@ -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]) + } + } + }) + } +} diff --git a/third-party/github.com/letsencrypt/boulder/bdns/dns.go b/third-party/github.com/letsencrypt/boulder/bdns/dns.go index 775d99383..5d297f3ef 100644 --- a/third-party/github.com/letsencrypt/boulder/bdns/dns.go +++ b/third-party/github.com/letsencrypt/boulder/bdns/dns.go @@ -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) diff --git a/third-party/github.com/letsencrypt/boulder/bdns/dns_test.go b/third-party/github.com/letsencrypt/boulder/bdns/dns_test.go index 8014e4928..563912133 100644 --- a/third-party/github.com/letsencrypt/boulder/bdns/dns_test.go +++ b/third-party/github.com/letsencrypt/boulder/bdns/dns_test.go @@ -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)}} diff --git a/third-party/github.com/letsencrypt/boulder/bdns/mocks.go b/third-party/github.com/letsencrypt/boulder/bdns/mocks.go index 36bf2e88d..fe7d07c29 100644 --- a/third-party/github.com/letsencrypt/boulder/bdns/mocks.go +++ b/third-party/github.com/letsencrypt/boulder/bdns/mocks.go @@ -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. diff --git a/third-party/github.com/letsencrypt/boulder/bdns/problem.go b/third-party/github.com/letsencrypt/boulder/bdns/problem.go index 7e22fbedf..8783743a5 100644 --- a/third-party/github.com/letsencrypt/boulder/bdns/problem.go +++ b/third-party/github.com/letsencrypt/boulder/bdns/problem.go @@ -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 diff --git a/third-party/github.com/letsencrypt/boulder/bdns/problem_test.go b/third-party/github.com/letsencrypt/boulder/bdns/problem_test.go index f20f5bdb3..8e925d80b 100644 --- a/third-party/github.com/letsencrypt/boulder/bdns/problem_test.go +++ b/third-party/github.com/letsencrypt/boulder/bdns/problem_test.go @@ -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}, diff --git a/third-party/github.com/letsencrypt/boulder/bdns/servers.go b/third-party/github.com/letsencrypt/boulder/bdns/servers.go index dd8edee98..efd3ef581 100644 --- a/third-party/github.com/letsencrypt/boulder/bdns/servers.go +++ b/third-party/github.com/letsencrypt/boulder/bdns/servers.go @@ -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) } diff --git a/third-party/github.com/letsencrypt/boulder/ca/ca.go b/third-party/github.com/letsencrypt/boulder/ca/ca.go index 239a5a4c3..a212ace1e 100644 --- a/third-party/github.com/letsencrypt/boulder/ca/ca.go +++ b/third-party/github.com/letsencrypt/boulder/ca/ca.go @@ -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 diff --git a/third-party/github.com/letsencrypt/boulder/ca/ca_test.go b/third-party/github.com/letsencrypt/boulder/ca/ca_test.go index e016ff505..3b0a00465 100644 --- a/third-party/github.com/letsencrypt/boulder/ca/ca_test.go +++ b/third-party/github.com/letsencrypt/boulder/ca/ca_test.go @@ -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) diff --git a/third-party/github.com/letsencrypt/boulder/ca/ecdsa_allow_list.go b/third-party/github.com/letsencrypt/boulder/ca/ecdsa_allow_list.go deleted file mode 100644 index d0007ca6e..000000000 --- a/third-party/github.com/letsencrypt/boulder/ca/ecdsa_allow_list.go +++ /dev/null @@ -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 -} diff --git a/third-party/github.com/letsencrypt/boulder/ca/ecdsa_allow_list_test.go b/third-party/github.com/letsencrypt/boulder/ca/ecdsa_allow_list_test.go deleted file mode 100644 index 78aed0348..000000000 --- a/third-party/github.com/letsencrypt/boulder/ca/ecdsa_allow_list_test.go +++ /dev/null @@ -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) - } - }) - } -} diff --git a/third-party/github.com/letsencrypt/boulder/ca/ocsp_test.go b/third-party/github.com/letsencrypt/boulder/ca/ocsp_test.go index 9cea07656..d65c726d7 100644 --- a/third-party/github.com/letsencrypt/boulder/ca/ocsp_test.go +++ b/third-party/github.com/letsencrypt/boulder/ca/ocsp_test.go @@ -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{ diff --git a/third-party/github.com/letsencrypt/boulder/ca/proto/ca.pb.go b/third-party/github.com/letsencrypt/boulder/ca/proto/ca.pb.go index fec630087..2b42a01e9 100644 --- a/third-party/github.com/letsencrypt/boulder/ca/proto/ca.pb.go +++ b/third-party/github.com/letsencrypt/boulder/ca/proto/ca.pb.go @@ -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 } diff --git a/third-party/github.com/letsencrypt/boulder/ca/proto/ca.proto b/third-party/github.com/letsencrypt/boulder/ca/proto/ca.proto index bb470e26d..dbbc12f6d 100644 --- a/third-party/github.com/letsencrypt/boulder/ca/proto/ca.proto +++ b/third-party/github.com/letsencrypt/boulder/ca/proto/ca.proto @@ -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 diff --git a/third-party/github.com/letsencrypt/boulder/ca/proto/ca_grpc.pb.go b/third-party/github.com/letsencrypt/boulder/ca/proto/ca_grpc.pb.go index c2d87bc0c..dea96f3b8 100644 --- a/third-party/github.com/letsencrypt/boulder/ca/proto/ca_grpc.pb.go +++ b/third-party/github.com/letsencrypt/boulder/ca/proto/ca_grpc.pb.go @@ -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) } diff --git a/third-party/github.com/letsencrypt/boulder/ca/testdata/ecdsa_allow_list.yml b/third-party/github.com/letsencrypt/boulder/ca/testdata/ecdsa_allow_list.yml deleted file mode 100644 index a648abda3..000000000 --- a/third-party/github.com/letsencrypt/boulder/ca/testdata/ecdsa_allow_list.yml +++ /dev/null @@ -1,2 +0,0 @@ ---- -- 1337 diff --git a/third-party/github.com/letsencrypt/boulder/ca/testdata/ecdsa_allow_list2.yml b/third-party/github.com/letsencrypt/boulder/ca/testdata/ecdsa_allow_list2.yml deleted file mode 100644 index 3365f2b9c..000000000 --- a/third-party/github.com/letsencrypt/boulder/ca/testdata/ecdsa_allow_list2.yml +++ /dev/null @@ -1,2 +0,0 @@ ---- -- 1338 diff --git a/third-party/github.com/letsencrypt/boulder/ca/testdata/ecdsa_allow_list_malformed.yml b/third-party/github.com/letsencrypt/boulder/ca/testdata/ecdsa_allow_list_malformed.yml deleted file mode 100644 index 286888a0a..000000000 --- a/third-party/github.com/letsencrypt/boulder/ca/testdata/ecdsa_allow_list_malformed.yml +++ /dev/null @@ -1 +0,0 @@ -not yaml diff --git a/third-party/github.com/letsencrypt/boulder/canceled/canceled.go b/third-party/github.com/letsencrypt/boulder/canceled/canceled.go deleted file mode 100644 index 405cacd3e..000000000 --- a/third-party/github.com/letsencrypt/boulder/canceled/canceled.go +++ /dev/null @@ -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 -} diff --git a/third-party/github.com/letsencrypt/boulder/canceled/canceled_test.go b/third-party/github.com/letsencrypt/boulder/canceled/canceled_test.go deleted file mode 100644 index 251072d8e..000000000 --- a/third-party/github.com/letsencrypt/boulder/canceled/canceled_test.go +++ /dev/null @@ -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.") - } -} diff --git a/third-party/github.com/letsencrypt/boulder/cmd/admin-revoker/main.go b/third-party/github.com/letsencrypt/boulder/cmd/admin-revoker/main.go deleted file mode 100644 index 7d18bc749..000000000 --- a/third-party/github.com/letsencrypt/boulder/cmd/admin-revoker/main.go +++ /dev/null @@ -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{}}) -} diff --git a/third-party/github.com/letsencrypt/boulder/cmd/admin/admin.go b/third-party/github.com/letsencrypt/boulder/cmd/admin/admin.go index d8d3d2ba8..f776e5271 100644 --- a/third-party/github.com/letsencrypt/boulder/cmd/admin/admin.go +++ b/third-party/github.com/letsencrypt/boulder/cmd/admin/admin.go @@ -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 +} diff --git a/third-party/github.com/letsencrypt/boulder/cmd/admin/admin_test.go b/third-party/github.com/letsencrypt/boulder/cmd/admin/admin_test.go new file mode 100644 index 000000000..1e0ba3d2e --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/cmd/admin/admin_test.go @@ -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) + } + }) + } +} diff --git a/third-party/github.com/letsencrypt/boulder/cmd/admin/cert.go b/third-party/github.com/letsencrypt/boulder/cmd/admin/cert.go index dc9c48884..33c27c3af 100644 --- a/third-party/github.com/letsencrypt/boulder/cmd/admin/cert.go +++ b/third-party/github.com/letsencrypt/boulder/cmd/admin/cert.go @@ -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 { diff --git a/third-party/github.com/letsencrypt/boulder/cmd/admin/cert_test.go b/third-party/github.com/letsencrypt/boulder/cmd/admin/cert_test.go index 185d49701..788348de8 100644 --- a/third-party/github.com/letsencrypt/boulder/cmd/admin/cert_test.go +++ b/third-party/github.com/letsencrypt/boulder/cmd/admin/cert_test.go @@ -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) + } } diff --git a/third-party/github.com/letsencrypt/boulder/cmd/admin/dryrun.go b/third-party/github.com/letsencrypt/boulder/cmd/admin/dryrun.go index 77a7b1614..00b9d8fd3 100644 --- a/third-party/github.com/letsencrypt/boulder/cmd/admin/dryrun.go +++ b/third-party/github.com/letsencrypt/boulder/cmd/admin/dryrun.go @@ -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 } diff --git a/third-party/github.com/letsencrypt/boulder/cmd/admin/email.go b/third-party/github.com/letsencrypt/boulder/cmd/admin/email.go deleted file mode 100644 index c9b85e0c5..000000000 --- a/third-party/github.com/letsencrypt/boulder/cmd/admin/email.go +++ /dev/null @@ -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 -} diff --git a/third-party/github.com/letsencrypt/boulder/cmd/admin/key.go b/third-party/github.com/letsencrypt/boulder/cmd/admin/key.go index 66da63ebe..d0b0a3b25 100644 --- a/third-party/github.com/letsencrypt/boulder/cmd/admin/key.go +++ b/third-party/github.com/letsencrypt/boulder/cmd/admin/key.go @@ -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 { diff --git a/third-party/github.com/letsencrypt/boulder/cmd/admin/key_test.go b/third-party/github.com/letsencrypt/boulder/cmd/admin/key_test.go index 0bb192236..4b165df0b 100644 --- a/third-party/github.com/letsencrypt/boulder/cmd/admin/key_test.go +++ b/third-party/github.com/letsencrypt/boulder/cmd/admin/key_test.go @@ -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) } diff --git a/third-party/github.com/letsencrypt/boulder/cmd/admin/main.go b/third-party/github.com/letsencrypt/boulder/cmd/admin/main.go index 01397d209..acef4f872 100644 --- a/third-party/github.com/letsencrypt/boulder/cmd/admin/main.go +++ b/third-party/github.com/letsencrypt/boulder/cmd/admin/main.go @@ -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 diff --git a/third-party/github.com/letsencrypt/boulder/cmd/admin/pause_identifier.go b/third-party/github.com/letsencrypt/boulder/cmd/admin/pause_identifier.go new file mode 100644 index 000000000..ffeaf4805 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/cmd/admin/pause_identifier.go @@ -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 +} diff --git a/third-party/github.com/letsencrypt/boulder/cmd/admin/pause_identifier_test.go b/third-party/github.com/letsencrypt/boulder/cmd/admin/pause_identifier_test.go new file mode 100644 index 000000000..937cf1791 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/cmd/admin/pause_identifier_test.go @@ -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) + } + }) + } +} diff --git a/third-party/github.com/letsencrypt/boulder/cmd/admin/unpause_account.go b/third-party/github.com/letsencrypt/boulder/cmd/admin/unpause_account.go new file mode 100644 index 000000000..ee6db3cc6 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/cmd/admin/unpause_account.go @@ -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 +} diff --git a/third-party/github.com/letsencrypt/boulder/cmd/admin/unpause_account_test.go b/third-party/github.com/letsencrypt/boulder/cmd/admin/unpause_account_test.go new file mode 100644 index 000000000..f39b168fc --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/cmd/admin/unpause_account_test.go @@ -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)) + } + }) + } +} diff --git a/third-party/github.com/letsencrypt/boulder/cmd/bad-key-revoker/main.go b/third-party/github.com/letsencrypt/boulder/cmd/bad-key-revoker/main.go index b234987f5..8e6cfac85 100644 --- a/third-party/github.com/letsencrypt/boulder/cmd/bad-key-revoker/main.go +++ b/third-party/github.com/letsencrypt/boulder/cmd/bad-key-revoker/main.go @@ -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, diff --git a/third-party/github.com/letsencrypt/boulder/cmd/bad-key-revoker/main_test.go b/third-party/github.com/letsencrypt/boulder/cmd/bad-key-revoker/main_test.go index ab654ce32..94bbdb85e 100644 --- a/third-party/github.com/letsencrypt/boulder/cmd/bad-key-revoker/main_test.go +++ b/third-party/github.com/letsencrypt/boulder/cmd/bad-key-revoker/main_test.go @@ -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) { diff --git a/third-party/github.com/letsencrypt/boulder/cmd/boulder-ca/main.go b/third-party/github.com/letsencrypt/boulder/cmd/boulder-ca/main.go index 86be24a3e..156b4fe90 100644 --- a/third-party/github.com/letsencrypt/boulder/cmd/boulder-ca/main.go +++ b/third-party/github.com/letsencrypt/boulder/cmd/boulder-ca/main.go @@ -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, diff --git a/third-party/github.com/letsencrypt/boulder/cmd/boulder-ra/main.go b/third-party/github.com/letsencrypt/boulder/cmd/boulder-ra/main.go index c5b994e73..9aa809e42 100644 --- a/third-party/github.com/letsencrypt/boulder/cmd/boulder-ra/main.go +++ b/third-party/github.com/letsencrypt/boulder/cmd/boulder-ra/main.go @@ -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") diff --git a/third-party/github.com/letsencrypt/boulder/cmd/boulder-va/main.go b/third-party/github.com/letsencrypt/boulder/cmd/boulder-va/main.go index 032435fac..5086a3923 100644 --- a/third-party/github.com/letsencrypt/boulder/cmd/boulder-va/main.go +++ b/third-party/github.com/letsencrypt/boulder/cmd/boulder-va/main.go @@ -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( diff --git a/third-party/github.com/letsencrypt/boulder/cmd/boulder-wfe2/main.go b/third-party/github.com/letsencrypt/boulder/cmd/boulder-wfe2/main.go index 1b3b497c6..955fe406c 100644 --- a/third-party/github.com/letsencrypt/boulder/cmd/boulder-wfe2/main.go +++ b/third-party/github.com/letsencrypt/boulder/cmd/boulder-wfe2/main.go @@ -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) diff --git a/third-party/github.com/letsencrypt/boulder/cmd/boulder/main.go b/third-party/github.com/letsencrypt/boulder/cmd/boulder/main.go index c2fcfaab2..5fde04acd 100644 --- a/third-party/github.com/letsencrypt/boulder/cmd/boulder/main.go +++ b/third-party/github.com/letsencrypt/boulder/cmd/boulder/main.go @@ -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 [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. diff --git a/third-party/github.com/letsencrypt/boulder/cmd/boulder/main_test.go b/third-party/github.com/letsencrypt/boulder/cmd/boulder/main_test.go index 45cfa1d63..1dbcb25b0 100644 --- a/third-party/github.com/letsencrypt/boulder/cmd/boulder/main_test.go +++ b/third-party/github.com/letsencrypt/boulder/cmd/boulder/main_test.go @@ -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", diff --git a/third-party/github.com/letsencrypt/boulder/cmd/ceremony/README.md b/third-party/github.com/letsencrypt/boulder/cmd/ceremony/README.md index 2b5b39350..80cadeb6c 100644 --- a/third-party/github.com/letsencrypt/boulder/cmd/ceremony/README.md +++ b/third-party/github.com/letsencrypt/boulder/cmd/ceremony/README.md @@ -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` | diff --git a/third-party/github.com/letsencrypt/boulder/cmd/ceremony/cert.go b/third-party/github.com/letsencrypt/boulder/cmd/ceremony/cert.go index 6c8a5c4f5..397c3b732 100644 --- a/third-party/github.com/letsencrypt/boulder/cmd/ceremony/cert.go +++ b/third-party/github.com/letsencrypt/boulder/cmd/ceremony/cert.go @@ -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 diff --git a/third-party/github.com/letsencrypt/boulder/cmd/ceremony/cert_test.go b/third-party/github.com/letsencrypt/boulder/cmd/ceremony/cert_test.go index 95a2b3375..0549b9a92 100644 --- a/third-party/github.com/letsencrypt/boulder/cmd/ceremony/cert_test.go +++ b/third-party/github.com/letsencrypt/boulder/cmd/ceremony/cert_test.go @@ -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}) diff --git a/third-party/github.com/letsencrypt/boulder/cmd/ceremony/main.go b/third-party/github.com/letsencrypt/boulder/cmd/ceremony/main.go index a026a461a..12cc9249c 100644 --- a/third-party/github.com/letsencrypt/boulder/cmd/ceremony/main.go +++ b/third-party/github.com/letsencrypt/boulder/cmd/ceremony/main.go @@ -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"` } diff --git a/third-party/github.com/letsencrypt/boulder/cmd/ceremony/rsa.go b/third-party/github.com/letsencrypt/boulder/cmd/ceremony/rsa.go index 69e326b39..7d0eb4b30 100644 --- a/third-party/github.com/letsencrypt/boulder/cmd/ceremony/rsa.go +++ b/third-party/github.com/letsencrypt/boulder/cmd/ceremony/rsa.go @@ -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 } diff --git a/third-party/github.com/letsencrypt/boulder/cmd/ceremony/rsa_test.go b/third-party/github.com/letsencrypt/boulder/cmd/ceremony/rsa_test.go index f0dc37071..40eb9d5df 100644 --- a/third-party/github.com/letsencrypt/boulder/cmd/ceremony/rsa_test.go +++ b/third-party/github.com/letsencrypt/boulder/cmd/ceremony/rsa_test.go @@ -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") } diff --git a/third-party/github.com/letsencrypt/boulder/cmd/cert-checker/main.go b/third-party/github.com/letsencrypt/boulder/cmd/cert-checker/main.go index d432fde00..5e36e2162 100644 --- a/third-party/github.com/letsencrypt/boulder/cmd/cert-checker/main.go +++ b/third-party/github.com/letsencrypt/boulder/cmd/cert-checker/main.go @@ -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()) }() } diff --git a/third-party/github.com/letsencrypt/boulder/cmd/cert-checker/main_test.go b/third-party/github.com/letsencrypt/boulder/cmd/cert-checker/main_test.go index 3ebda1c80..615cfdbee 100644 --- a/third-party/github.com/letsencrypt/boulder/cmd/cert-checker/main_test.go +++ b/third-party/github.com/letsencrypt/boulder/cmd/cert-checker/main_test.go @@ -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") } diff --git a/third-party/github.com/letsencrypt/boulder/cmd/cert-checker/testdata/quite_invalid.pem b/third-party/github.com/letsencrypt/boulder/cmd/cert-checker/testdata/quite_invalid.pem index 632b8b67e..5a5b86c02 100644 --- a/third-party/github.com/letsencrypt/boulder/cmd/cert-checker/testdata/quite_invalid.pem +++ b/third-party/github.com/letsencrypt/boulder/cmd/cert-checker/testdata/quite_invalid.pem @@ -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----- diff --git a/third-party/github.com/letsencrypt/boulder/cmd/clock_integration.go b/third-party/github.com/letsencrypt/boulder/cmd/clock_integration.go index beb5b0103..b589a8831 100644 --- a/third-party/github.com/letsencrypt/boulder/cmd/clock_integration.go +++ b/third-party/github.com/letsencrypt/boulder/cmd/clock_integration.go @@ -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 } diff --git a/third-party/github.com/letsencrypt/boulder/cmd/config.go b/third-party/github.com/letsencrypt/boulder/cmd/config.go index 1a3edabff..13842fdf9 100644 --- a/third-party/github.com/letsencrypt/boulder/cmd/config.go +++ b/third-party/github.com/letsencrypt/boulder/cmd/config.go @@ -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 +} diff --git a/third-party/github.com/letsencrypt/boulder/cmd/config_test.go b/third-party/github.com/letsencrypt/boulder/cmd/config_test.go index b6eeb9860..2935889b5 100644 --- a/third-party/github.com/letsencrypt/boulder/cmd/config_test.go +++ b/third-party/github.com/letsencrypt/boulder/cmd/config_test.go @@ -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) + } + }) + } +} diff --git a/third-party/github.com/letsencrypt/boulder/cmd/contact-auditor/README.md b/third-party/github.com/letsencrypt/boulder/cmd/contact-auditor/README.md deleted file mode 100644 index 39083c894..000000000 --- a/third-party/github.com/letsencrypt/boulder/cmd/contact-auditor/README.md +++ /dev/null @@ -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: - -``` - "" "" -``` - -## 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 "" "" -... -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 "" "" -... -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: -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": , - "maxOpenConns": , - "maxIdleConns": , - "connMaxLifetime": , - "connMaxIdleTime": - } - } - } - -``` diff --git a/third-party/github.com/letsencrypt/boulder/cmd/contact-auditor/main.go b/third-party/github.com/letsencrypt/boulder/cmd/contact-auditor/main.go deleted file mode 100644 index d6b366b6b..000000000 --- a/third-party/github.com/letsencrypt/boulder/cmd/contact-auditor/main.go +++ /dev/null @@ -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{}}) -} diff --git a/third-party/github.com/letsencrypt/boulder/cmd/contact-auditor/main_test.go b/third-party/github.com/letsencrypt/boulder/cmd/contact-auditor/main_test.go deleted file mode 100644 index c9c2a2edf..000000000 --- a/third-party/github.com/letsencrypt/boulder/cmd/contact-auditor/main_test.go +++ /dev/null @@ -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, - } -} diff --git a/third-party/github.com/letsencrypt/boulder/cmd/crl-updater/main.go b/third-party/github.com/letsencrypt/boulder/cmd/crl-updater/main.go index 23032f130..b294bbd95 100644 --- a/third-party/github.com/letsencrypt/boulder/cmd/crl-updater/main.go +++ b/third-party/github.com/letsencrypt/boulder/cmd/crl-updater/main.go @@ -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, diff --git a/third-party/github.com/letsencrypt/boulder/cmd/email-exporter/main.go b/third-party/github.com/letsencrypt/boulder/cmd/email-exporter/main.go new file mode 100644 index 000000000..52c1a49ee --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/cmd/email-exporter/main.go @@ -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{}}) +} diff --git a/third-party/github.com/letsencrypt/boulder/cmd/expiration-mailer/main.go b/third-party/github.com/letsencrypt/boulder/cmd/expiration-mailer/main.go deleted file mode 100644 index 46fa939a6..000000000 --- a/third-party/github.com/letsencrypt/boulder/cmd/expiration-mailer/main.go +++ /dev/null @@ -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 " - 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{}}) -} diff --git a/third-party/github.com/letsencrypt/boulder/cmd/expiration-mailer/main_test.go b/third-party/github.com/letsencrypt/boulder/cmd/expiration-mailer/main_test.go deleted file mode 100644 index e5c86147e..000000000 --- a/third-party/github.com/letsencrypt/boulder/cmd/expiration-mailer/main_test.go +++ /dev/null @@ -1,1007 +0,0 @@ -package notmain - -import ( - "context" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "crypto/x509" - "errors" - "fmt" - "math/big" - "net" - "strings" - "testing" - "text/template" - "time" - - "github.com/jmhodges/clock" - "github.com/prometheus/client_golang/prometheus" - io_prometheus_client "github.com/prometheus/client_model/go" - "google.golang.org/grpc" - - "github.com/letsencrypt/boulder/core" - corepb "github.com/letsencrypt/boulder/core/proto" - "github.com/letsencrypt/boulder/db" - berrors "github.com/letsencrypt/boulder/errors" - blog "github.com/letsencrypt/boulder/log" - bmail "github.com/letsencrypt/boulder/mail" - "github.com/letsencrypt/boulder/metrics" - "github.com/letsencrypt/boulder/mocks" - "github.com/letsencrypt/boulder/sa" - sapb "github.com/letsencrypt/boulder/sa/proto" - "github.com/letsencrypt/boulder/sa/satest" - "github.com/letsencrypt/boulder/test" - isa "github.com/letsencrypt/boulder/test/inmem/sa" - "github.com/letsencrypt/boulder/test/vars" -) - -type fakeRegStore struct { - RegByID map[int64]*corepb.Registration -} - -func (f fakeRegStore) GetRegistration(ctx context.Context, req *sapb.RegistrationID, _ ...grpc.CallOption) (*corepb.Registration, error) { - r, ok := f.RegByID[req.Id] - if !ok { - return r, berrors.NotFoundError("no registration found for %q", req.Id) - } - return r, nil -} - -func newFakeRegStore() fakeRegStore { - return fakeRegStore{RegByID: make(map[int64]*corepb.Registration)} -} - -const testTmpl = `hi, cert for DNS names {{.DNSNames}} is going to expire in {{.DaysToExpiration}} days ({{.ExpirationDate}})` -const testEmailSubject = `email subject for test` -const emailARaw = "rolandshoemaker@gmail.com" -const emailBRaw = "test@gmail.com" - -var ( - emailA = "mailto:" + emailARaw - emailB = "mailto:" + emailBRaw - 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" -}`) - tmpl = template.Must(template.New("expiry-email").Parse(testTmpl)) - subjTmpl = template.Must(template.New("expiry-email-subject").Parse("Testing: " + defaultExpirationSubject)) -) - -func TestSendNagsManyCerts(t *testing.T) { - mc := mocks.Mailer{} - rs := newFakeRegStore() - fc := clock.NewFake() - - staticTmpl := template.Must(template.New("expiry-email-subject-static").Parse(testEmailSubject)) - tmpl := template.Must(template.New("expiry-email").Parse( - `cert for DNS names {{.TruncatedDNSNames}} is going to expire in {{.DaysToExpiration}} days ({{.ExpirationDate}})`)) - - m := mailer{ - log: blog.NewMock(), - mailer: &mc, - emailTemplate: tmpl, - addressLimiter: &limiter{clk: fc, limit: 4}, - // Explicitly override the default subject to use testEmailSubject - subjectTemplate: staticTmpl, - rs: rs, - clk: fc, - stats: initStats(metrics.NoopRegisterer), - } - - var certs []*x509.Certificate - for i := range 101 { - certs = append(certs, &x509.Certificate{ - SerialNumber: big.NewInt(0x0304), - NotAfter: fc.Now().AddDate(0, 0, 2), - DNSNames: []string{fmt.Sprintf("example-%d.com", i)}, - }) - } - - conn, err := m.mailer.Connect() - test.AssertNotError(t, err, "connecting SMTP") - err = m.sendNags(conn, []string{emailA}, certs) - test.AssertNotError(t, err, "sending mail") - - test.AssertEquals(t, len(mc.Messages), 1) - if len(strings.Split(mc.Messages[0].Body, "\n")) > 100 { - t.Errorf("Expected mailed message to truncate after 100 domains, got: %q", mc.Messages[0].Body) - } -} - -func TestSendNags(t *testing.T) { - mc := mocks.Mailer{} - rs := newFakeRegStore() - fc := clock.NewFake() - - staticTmpl := template.Must(template.New("expiry-email-subject-static").Parse(testEmailSubject)) - - log := blog.NewMock() - m := mailer{ - log: log, - mailer: &mc, - emailTemplate: tmpl, - addressLimiter: &limiter{clk: fc, limit: 4}, - // Explicitly override the default subject to use testEmailSubject - subjectTemplate: staticTmpl, - rs: rs, - clk: fc, - stats: initStats(metrics.NoopRegisterer), - } - - cert := &x509.Certificate{ - SerialNumber: big.NewInt(0x0304), - NotAfter: fc.Now().AddDate(0, 0, 2), - DNSNames: []string{"example.com"}, - } - - conn, err := m.mailer.Connect() - test.AssertNotError(t, err, "connecting SMTP") - err = m.sendNags(conn, []string{emailA}, []*x509.Certificate{cert}) - test.AssertNotError(t, err, "Failed to send warning messages") - test.AssertEquals(t, len(mc.Messages), 1) - test.AssertEquals(t, mc.Messages[0], mocks.MailerMessage{ - To: emailARaw, - Subject: testEmailSubject, - Body: fmt.Sprintf(`hi, cert for DNS names example.com is going to expire in 2 days (%s)`, cert.NotAfter.Format(time.DateOnly)), - }) - - mc.Clear() - conn, err = m.mailer.Connect() - test.AssertNotError(t, err, "connecting SMTP") - err = m.sendNags(conn, []string{emailA, emailB}, []*x509.Certificate{cert}) - test.AssertNotError(t, err, "Failed to send warning messages") - test.AssertEquals(t, len(mc.Messages), 2) - test.AssertEquals(t, mc.Messages[0], mocks.MailerMessage{ - To: emailARaw, - Subject: testEmailSubject, - Body: fmt.Sprintf(`hi, cert for DNS names example.com is going to expire in 2 days (%s)`, cert.NotAfter.Format(time.DateOnly)), - }) - test.AssertEquals(t, mc.Messages[1], mocks.MailerMessage{ - To: emailBRaw, - Subject: testEmailSubject, - Body: fmt.Sprintf(`hi, cert for DNS names example.com is going to expire in 2 days (%s)`, cert.NotAfter.Format(time.DateOnly)), - }) - - mc.Clear() - conn, err = m.mailer.Connect() - test.AssertNotError(t, err, "connecting SMTP") - err = m.sendNags(conn, []string{}, []*x509.Certificate{cert}) - test.AssertErrorIs(t, err, errNoValidEmail) - test.AssertEquals(t, len(mc.Messages), 0) - - sendLogs := log.GetAllMatching("INFO: attempting send JSON=.*") - if len(sendLogs) != 2 { - t.Errorf("expected 2 'attempting send' log line, got %d: %s", len(sendLogs), strings.Join(sendLogs, "\n")) - } - if !strings.Contains(sendLogs[0], `"Rcpt":["rolandshoemaker@gmail.com"]`) { - t.Errorf("expected first 'attempting send' log line to have one address, got %q", sendLogs[0]) - } - if !strings.Contains(sendLogs[0], `"TruncatedSerials":["000000000000000000000000000000000304"]`) { - t.Errorf("expected first 'attempting send' log line to have one serial, got %q", sendLogs[0]) - } - if !strings.Contains(sendLogs[0], `"DaysToExpiration":2`) { - t.Errorf("expected first 'attempting send' log line to have 2 days to expiration, got %q", sendLogs[0]) - } - if !strings.Contains(sendLogs[0], `"TruncatedDNSNames":["example.com"]`) { - t.Errorf("expected first 'attempting send' log line to have 1 domain, 'example.com', got %q", sendLogs[0]) - } -} - -func TestSendNagsAddressLimited(t *testing.T) { - mc := mocks.Mailer{} - rs := newFakeRegStore() - fc := clock.NewFake() - - staticTmpl := template.Must(template.New("expiry-email-subject-static").Parse(testEmailSubject)) - - log := blog.NewMock() - m := mailer{ - log: log, - mailer: &mc, - emailTemplate: tmpl, - addressLimiter: &limiter{clk: fc, limit: 1}, - // Explicitly override the default subject to use testEmailSubject - subjectTemplate: staticTmpl, - rs: rs, - clk: fc, - stats: initStats(metrics.NoopRegisterer), - } - - m.addressLimiter.inc(emailARaw) - - cert := &x509.Certificate{ - SerialNumber: big.NewInt(0x0304), - NotAfter: fc.Now().AddDate(0, 0, 2), - DNSNames: []string{"example.com"}, - } - - conn, err := m.mailer.Connect() - test.AssertNotError(t, err, "connecting SMTP") - - // Try sending a message to an over-the-limit address - err = m.sendNags(conn, []string{emailA}, []*x509.Certificate{cert}) - test.AssertErrorIs(t, err, errNoValidEmail) - // Expect that no messages were sent because this address was over the limit - test.AssertEquals(t, len(mc.Messages), 0) - - // Try sending a message to an over-the-limit address and an under-the-limit - // one. It should only go to the under-the-limit one. - err = m.sendNags(conn, []string{emailA, emailB}, []*x509.Certificate{cert}) - test.AssertNotError(t, err, "sending warning messages to two addresses") - test.AssertEquals(t, len(mc.Messages), 1) - test.AssertEquals(t, mc.Messages[0], mocks.MailerMessage{ - To: emailBRaw, - Subject: testEmailSubject, - Body: fmt.Sprintf(`hi, cert for DNS names example.com is going to expire in 2 days (%s)`, cert.NotAfter.Format(time.DateOnly)), - }) -} - -var serial1 = big.NewInt(0x1336) -var serial2 = big.NewInt(0x1337) -var serial3 = big.NewInt(0x1338) -var serial4 = big.NewInt(0x1339) -var serial4String = core.SerialToString(serial4) -var serial5 = big.NewInt(0x1340) -var serial5String = core.SerialToString(serial5) -var serial6 = big.NewInt(0x1341) -var serial7 = big.NewInt(0x1342) -var serial8 = big.NewInt(0x1343) -var serial9 = big.NewInt(0x1344) - -var testKey *ecdsa.PrivateKey - -func init() { - var err error - testKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - panic(err) - } -} - -func TestProcessCerts(t *testing.T) { - expiresIn := time.Hour * 24 * 7 - testCtx := setup(t, []time.Duration{expiresIn}) - - certs := addExpiringCerts(t, testCtx) - err := testCtx.m.processCerts(context.Background(), certs, expiresIn) - test.AssertNotError(t, err, "processing certs") - // Test that the lastExpirationNagSent was updated for the certificate - // corresponding to serial4, which is set up as "already renewed" by - // addExpiringCerts. - if len(testCtx.log.GetAllMatching("UPDATE certificateStatus.*000000000000000000000000000000001339")) != 1 { - t.Errorf("Expected an update to certificateStatus, got these log lines:\n%s", - strings.Join(testCtx.log.GetAll(), "\n")) - } -} - -// There's an account with an expiring certificate but no email address. We shouldn't examine -// that certificate repeatedly; we should mark it as if it had an email sent already. -func TestNoContactCertIsNotRenewed(t *testing.T) { - expiresIn := time.Hour * 24 * 7 - testCtx := setup(t, []time.Duration{expiresIn}) - - reg, err := makeRegistration(testCtx.ssa, 1, jsonKeyA, nil) - test.AssertNotError(t, err, "Couldn't store regA") - - cert, err := makeCertificate( - reg.Id, - serial1, - []string{"example-a.com"}, - 23*time.Hour, - testCtx.fc) - test.AssertNotError(t, err, "creating cert A") - - err = insertCertificate(cert, time.Time{}) - test.AssertNotError(t, err, "inserting certificate") - - err = testCtx.m.findExpiringCertificates(context.Background()) - test.AssertNotError(t, err, "finding expired certificates") - - // We should have sent no mail, because there was no contact address - test.AssertEquals(t, len(testCtx.mc.Messages), 0) - - // We should have examined exactly one certificate - certsExamined := testCtx.m.stats.certificatesExamined - test.AssertMetricWithLabelsEquals(t, certsExamined, prometheus.Labels{}, 1.0) - - certsAlreadyRenewed := testCtx.m.stats.certificatesAlreadyRenewed - test.AssertMetricWithLabelsEquals(t, certsAlreadyRenewed, prometheus.Labels{}, 0.0) - - // Run findExpiringCertificates again. The count of examined certificates - // should not increase again. - err = testCtx.m.findExpiringCertificates(context.Background()) - test.AssertNotError(t, err, "finding expired certificates") - test.AssertMetricWithLabelsEquals(t, certsExamined, prometheus.Labels{}, 1.0) - test.AssertMetricWithLabelsEquals(t, certsAlreadyRenewed, prometheus.Labels{}, 0.0) -} - -// An account with no contact info has a certificate that is expiring but has been renewed. -// We should only examine that certificate once. -func TestNoContactCertIsRenewed(t *testing.T) { - ctx := context.Background() - - testCtx := setup(t, []time.Duration{time.Hour * 24 * 7}) - - reg, err := makeRegistration(testCtx.ssa, 1, jsonKeyA, []string{}) - test.AssertNotError(t, err, "Couldn't store regA") - - names := []string{"example-a.com"} - cert, err := makeCertificate( - reg.Id, - serial1, - names, - 23*time.Hour, - testCtx.fc) - test.AssertNotError(t, err, "creating cert A") - - expires := testCtx.fc.Now().Add(23 * time.Hour) - - err = insertCertificate(cert, time.Time{}) - test.AssertNotError(t, err, "inserting certificate") - - setupDBMap, err := sa.DBMapForTest(vars.DBConnSAFullPerms) - test.AssertNotError(t, err, "setting up DB") - err = setupDBMap.Insert(ctx, &core.FQDNSet{ - SetHash: core.HashNames(names), - Serial: core.SerialToString(serial2), - Issued: testCtx.fc.Now().Add(time.Hour), - Expires: expires.Add(time.Hour), - }) - test.AssertNotError(t, err, "inserting FQDNSet for renewal") - - err = testCtx.m.findExpiringCertificates(ctx) - test.AssertNotError(t, err, "finding expired certificates") - - // We should have examined exactly one certificate - certsExamined := testCtx.m.stats.certificatesExamined - test.AssertMetricWithLabelsEquals(t, certsExamined, prometheus.Labels{}, 1.0) - - certsAlreadyRenewed := testCtx.m.stats.certificatesAlreadyRenewed - test.AssertMetricWithLabelsEquals(t, certsAlreadyRenewed, prometheus.Labels{}, 1.0) - - // Run findExpiringCertificates again. The count of examined certificates - // should not increase again. - err = testCtx.m.findExpiringCertificates(ctx) - test.AssertNotError(t, err, "finding expired certificates") - test.AssertMetricWithLabelsEquals(t, certsExamined, prometheus.Labels{}, 1.0) - test.AssertMetricWithLabelsEquals(t, certsAlreadyRenewed, prometheus.Labels{}, 1.0) -} - -func TestProcessCertsParallel(t *testing.T) { - expiresIn := time.Hour * 24 * 7 - testCtx := setup(t, []time.Duration{expiresIn}) - - testCtx.m.parallelSends = 2 - certs := addExpiringCerts(t, testCtx) - err := testCtx.m.processCerts(context.Background(), certs, expiresIn) - test.AssertNotError(t, err, "processing certs") - // Test that the lastExpirationNagSent was updated for the certificate - // corresponding to serial4, which is set up as "already renewed" by - // addExpiringCerts. - if len(testCtx.log.GetAllMatching("UPDATE certificateStatus.*000000000000000000000000000000001339")) != 1 { - t.Errorf("Expected an update to certificateStatus, got these log lines:\n%s", - strings.Join(testCtx.log.GetAll(), "\n")) - } -} - -type erroringMailClient struct{} - -func (e erroringMailClient) Connect() (bmail.Conn, error) { - return nil, errors.New("whoopsie-doo") -} - -func TestProcessCertsConnectError(t *testing.T) { - expiresIn := time.Hour * 24 * 7 - testCtx := setup(t, []time.Duration{expiresIn}) - - testCtx.m.mailer = erroringMailClient{} - certs := addExpiringCerts(t, testCtx) - // Checking that this terminates rather than deadlocks - err := testCtx.m.processCerts(context.Background(), certs, expiresIn) - test.AssertError(t, err, "processing certs") -} - -func TestFindExpiringCertificates(t *testing.T) { - testCtx := setup(t, []time.Duration{time.Hour * 24, time.Hour * 24 * 4, time.Hour * 24 * 7}) - - addExpiringCerts(t, testCtx) - - err := testCtx.m.findExpiringCertificates(context.Background()) - test.AssertNotError(t, err, "Failed on no certificates") - test.AssertEquals(t, len(testCtx.log.GetAllMatching("Searching for certificates that expire between.*")), 3) - - err = testCtx.m.findExpiringCertificates(context.Background()) - test.AssertNotError(t, err, "Failed to find expiring certs") - // Should get 001 and 003 - if len(testCtx.mc.Messages) != 2 { - builder := new(strings.Builder) - for _, m := range testCtx.mc.Messages { - fmt.Fprintf(builder, "%s\n", m) - } - t.Fatalf("Expected two messages when finding expiring certificates, got:\n%s", - builder.String()) - } - - test.AssertEquals(t, testCtx.mc.Messages[0], mocks.MailerMessage{ - To: emailARaw, - // A certificate with only one domain should have only one domain listed in - // the subject - Subject: "Testing: Let's Encrypt certificate expiration notice for domain \"example-a.com\"", - Body: "hi, cert for DNS names example-a.com is going to expire in 0 days (1970-01-01)", - }) - test.AssertEquals(t, testCtx.mc.Messages[1], mocks.MailerMessage{ - To: emailBRaw, - // A certificate with two domains should have only one domain listed and an - // additional count included - Subject: "Testing: Let's Encrypt certificate expiration notice for domain \"another.example-c.com\" (and 1 more)", - Body: "hi, cert for DNS names another.example-c.com\nexample-c.com is going to expire in 7 days (1970-01-08)", - }) - - // Check that regC's only certificate being renewed does not cause a log - test.AssertEquals(t, len(testCtx.log.GetAllMatching("no certs given to send nags for")), 0) - - // A consecutive run shouldn't find anything - testCtx.mc.Clear() - err = testCtx.m.findExpiringCertificates(context.Background()) - test.AssertNotError(t, err, "Failed to find expiring certs") - test.AssertEquals(t, len(testCtx.mc.Messages), 0) - test.AssertMetricWithLabelsEquals(t, testCtx.m.stats.sendDelay, prometheus.Labels{"nag_group": "48h0m0s"}, 90000) - test.AssertMetricWithLabelsEquals(t, testCtx.m.stats.sendDelay, prometheus.Labels{"nag_group": "192h0m0s"}, 82800) -} - -func makeRegistration(sac sapb.StorageAuthorityClient, id int64, jsonKey []byte, contacts []string) (*corepb.Registration, error) { - var ip [4]byte - _, err := rand.Reader.Read(ip[:]) - if err != nil { - return nil, err - } - ipText, err := net.IP(ip[:]).MarshalText() - if err != nil { - return nil, fmt.Errorf("formatting IP address: %s", err) - } - reg, err := sac.NewRegistration(context.Background(), &corepb.Registration{ - Id: id, - Contact: contacts, - Key: jsonKey, - InitialIP: ipText, - }) - if err != nil { - return nil, fmt.Errorf("storing registration: %s", err) - } - return reg, nil -} - -func makeCertificate(regID int64, serial *big.Int, dnsNames []string, expires time.Duration, fc clock.FakeClock) (certDERWithRegID, error) { - // Expires in <1d, last nag was the 4d nag - template := &x509.Certificate{ - NotAfter: fc.Now().Add(expires), - DNSNames: dnsNames, - SerialNumber: serial, - } - certDer, err := x509.CreateCertificate(rand.Reader, template, template, &testKey.PublicKey, testKey) - if err != nil { - return certDERWithRegID{}, err - } - return certDERWithRegID{ - RegID: regID, - DER: certDer, - }, nil -} - -func insertCertificate(cert certDERWithRegID, lastNagSent time.Time) error { - ctx := context.Background() - - parsedCert, err := x509.ParseCertificate(cert.DER) - if err != nil { - return err - } - - setupDBMap, err := sa.DBMapForTest(vars.DBConnSAFullPerms) - if err != nil { - return err - } - err = setupDBMap.Insert(ctx, &core.Certificate{ - RegistrationID: cert.RegID, - Serial: core.SerialToString(parsedCert.SerialNumber), - Issued: parsedCert.NotBefore, - Expires: parsedCert.NotAfter, - DER: cert.DER, - }) - if err != nil { - return fmt.Errorf("inserting certificate: %w", err) - } - - return setupDBMap.Insert(ctx, &core.CertificateStatus{ - Serial: core.SerialToString(parsedCert.SerialNumber), - LastExpirationNagSent: lastNagSent, - Status: core.OCSPStatusGood, - NotAfter: parsedCert.NotAfter, - OCSPLastUpdated: time.Time{}, - RevokedDate: time.Time{}, - RevokedReason: 0, - }) -} - -func addExpiringCerts(t *testing.T, ctx *testCtx) []certDERWithRegID { - // Add some expiring certificates and registrations - regA, err := makeRegistration(ctx.ssa, 1, jsonKeyA, []string{emailA}) - test.AssertNotError(t, err, "Couldn't store regA") - regB, err := makeRegistration(ctx.ssa, 2, jsonKeyB, []string{emailB}) - test.AssertNotError(t, err, "Couldn't store regB") - regC, err := makeRegistration(ctx.ssa, 3, jsonKeyC, []string{emailB}) - test.AssertNotError(t, err, "Couldn't store regC") - - // Expires in <1d, last nag was the 4d nag - certA, err := makeCertificate( - regA.Id, - serial1, - []string{"example-a.com"}, - 23*time.Hour, - ctx.fc) - test.AssertNotError(t, err, "creating cert A") - - // Expires in 3d, already sent 4d nag at 4.5d - certB, err := makeCertificate( - regA.Id, - serial2, - []string{"example-b.com"}, - 72*time.Hour, - ctx.fc) - test.AssertNotError(t, err, "creating cert B") - - // Expires in 7d and change, no nag sent at all yet - certC, err := makeCertificate( - regB.Id, - serial3, - []string{"example-c.com", "another.example-c.com"}, - (7*24+1)*time.Hour, - ctx.fc) - test.AssertNotError(t, err, "creating cert C") - - // Expires in 3d, renewed - certDNames := []string{"example-d.com"} - certD, err := makeCertificate( - regC.Id, - serial4, - certDNames, - 72*time.Hour, - ctx.fc) - test.AssertNotError(t, err, "creating cert D") - - fqdnStatusD := &core.FQDNSet{ - SetHash: core.HashNames(certDNames), - Serial: serial4String, - Issued: ctx.fc.Now().AddDate(0, 0, -87), - Expires: ctx.fc.Now().AddDate(0, 0, 3), - } - fqdnStatusDRenewed := &core.FQDNSet{ - SetHash: core.HashNames(certDNames), - Serial: serial5String, - Issued: ctx.fc.Now().AddDate(0, 0, -3), - Expires: ctx.fc.Now().AddDate(0, 0, 87), - } - - err = insertCertificate(certA, ctx.fc.Now().Add(-72*time.Hour)) - test.AssertNotError(t, err, "inserting certA") - err = insertCertificate(certB, ctx.fc.Now().Add(-36*time.Hour)) - test.AssertNotError(t, err, "inserting certB") - err = insertCertificate(certC, ctx.fc.Now().Add(-36*time.Hour)) - test.AssertNotError(t, err, "inserting certC") - err = insertCertificate(certD, ctx.fc.Now().Add(-36*time.Hour)) - test.AssertNotError(t, err, "inserting certD") - - setupDBMap, err := sa.DBMapForTest(vars.DBConnSAFullPerms) - test.AssertNotError(t, err, "setting up DB") - err = setupDBMap.Insert(context.Background(), fqdnStatusD) - test.AssertNotError(t, err, "Couldn't add fqdnStatusD") - err = setupDBMap.Insert(context.Background(), fqdnStatusDRenewed) - test.AssertNotError(t, err, "Couldn't add fqdnStatusDRenewed") - return []certDERWithRegID{certA, certB, certC, certD} -} - -func countGroupsAtCapacity(group string, counter *prometheus.GaugeVec) int { - ch := make(chan prometheus.Metric, 10) - counter.With(prometheus.Labels{"nag_group": group}).Collect(ch) - m := <-ch - var iom io_prometheus_client.Metric - _ = m.Write(&iom) - return int(iom.Gauge.GetValue()) -} - -func TestFindCertsAtCapacity(t *testing.T) { - testCtx := setup(t, []time.Duration{time.Hour * 24}) - - addExpiringCerts(t, testCtx) - - // Set the limit to 1 so we are "at capacity" with one result - testCtx.m.certificatesPerTick = 1 - - err := testCtx.m.findExpiringCertificates(context.Background()) - test.AssertNotError(t, err, "Failed to find expiring certs") - test.AssertEquals(t, len(testCtx.mc.Messages), 1) - - // The "48h0m0s" nag group should have its prometheus stat incremented once. - // Note: this is not the 24h0m0s nag as you would expect sending time.Hour - // * 24 to setup() for the nag duration. This is because all of the nags are - // offset by 24 hours in this test file's setup() function, to mimic a 24h - // setting for the "Frequency" field in the JSON config. - test.AssertEquals(t, countGroupsAtCapacity("48h0m0s", testCtx.m.stats.nagsAtCapacity), 1) - - // A consecutive run shouldn't find anything - testCtx.mc.Clear() - err = testCtx.m.findExpiringCertificates(context.Background()) - test.AssertNotError(t, err, "Failed to find expiring certs") - test.AssertEquals(t, len(testCtx.mc.Messages), 0) - - // The "48h0m0s" nag group should now be reporting that it isn't at capacity - test.AssertEquals(t, countGroupsAtCapacity("48h0m0s", testCtx.m.stats.nagsAtCapacity), 0) -} - -func TestCertIsRenewed(t *testing.T) { - testCtx := setup(t, []time.Duration{time.Hour * 24, time.Hour * 24 * 4, time.Hour * 24 * 7}) - - reg := satest.CreateWorkingRegistration(t, testCtx.ssa) - - testCerts := []*struct { - Serial *big.Int - stringSerial string - DNS []string - NotBefore time.Time - NotAfter time.Time - // this field is the test assertion - IsRenewed bool - }{ - { - Serial: serial1, - DNS: []string{"a.example.com", "a2.example.com"}, - NotBefore: testCtx.fc.Now().Add((-1 * 24) * time.Hour), - NotAfter: testCtx.fc.Now().Add((89 * 24) * time.Hour), - IsRenewed: true, - }, - { - Serial: serial2, - DNS: []string{"a.example.com", "a2.example.com"}, - NotBefore: testCtx.fc.Now().Add((0 * 24) * time.Hour), - NotAfter: testCtx.fc.Now().Add((90 * 24) * time.Hour), - IsRenewed: false, - }, - { - Serial: serial3, - DNS: []string{"b.example.net"}, - NotBefore: testCtx.fc.Now().Add((0 * 24) * time.Hour), - NotAfter: testCtx.fc.Now().Add((90 * 24) * time.Hour), - IsRenewed: false, - }, - { - Serial: serial4, - DNS: []string{"c.example.org"}, - NotBefore: testCtx.fc.Now().Add((-100 * 24) * time.Hour), - NotAfter: testCtx.fc.Now().Add((-10 * 24) * time.Hour), - IsRenewed: true, - }, - { - Serial: serial5, - DNS: []string{"c.example.org"}, - NotBefore: testCtx.fc.Now().Add((-80 * 24) * time.Hour), - NotAfter: testCtx.fc.Now().Add((10 * 24) * time.Hour), - IsRenewed: true, - }, - { - Serial: serial6, - DNS: []string{"c.example.org"}, - NotBefore: testCtx.fc.Now().Add((-75 * 24) * time.Hour), - NotAfter: testCtx.fc.Now().Add((15 * 24) * time.Hour), - IsRenewed: true, - }, - { - Serial: serial7, - DNS: []string{"c.example.org"}, - NotBefore: testCtx.fc.Now().Add((-1 * 24) * time.Hour), - NotAfter: testCtx.fc.Now().Add((89 * 24) * time.Hour), - IsRenewed: false, - }, - { - Serial: serial8, - DNS: []string{"d.example.com", "d2.example.com"}, - NotBefore: testCtx.fc.Now().Add((-1 * 24) * time.Hour), - NotAfter: testCtx.fc.Now().Add((89 * 24) * time.Hour), - IsRenewed: false, - }, - { - Serial: serial9, - DNS: []string{"d.example.com", "d2.example.com", "d3.example.com"}, - NotBefore: testCtx.fc.Now().Add((0 * 24) * time.Hour), - NotAfter: testCtx.fc.Now().Add((90 * 24) * time.Hour), - IsRenewed: false, - }, - } - - setupDBMap, err := sa.DBMapForTest(vars.DBConnSAFullPerms) - if err != nil { - t.Fatal(err) - } - - for _, testData := range testCerts { - testData.stringSerial = core.SerialToString(testData.Serial) - - rawCert := x509.Certificate{ - NotBefore: testData.NotBefore, - NotAfter: testData.NotAfter, - DNSNames: testData.DNS, - SerialNumber: testData.Serial, - } - // Can't use makeCertificate here because we also care about NotBefore - certDer, err := x509.CreateCertificate(rand.Reader, &rawCert, &rawCert, &testKey.PublicKey, testKey) - if err != nil { - t.Fatal(err) - } - fqdnStatus := &core.FQDNSet{ - SetHash: core.HashNames(testData.DNS), - Serial: testData.stringSerial, - Issued: testData.NotBefore, - Expires: testData.NotAfter, - } - - err = insertCertificate(certDERWithRegID{DER: certDer, RegID: reg.Id}, time.Time{}) - test.AssertNotError(t, err, fmt.Sprintf("Couldn't add cert %s", testData.stringSerial)) - - err = setupDBMap.Insert(context.Background(), fqdnStatus) - test.AssertNotError(t, err, fmt.Sprintf("Couldn't add fqdnStatus %s", testData.stringSerial)) - } - - for _, testData := range testCerts { - renewed, err := testCtx.m.certIsRenewed(context.Background(), testData.DNS, testData.NotBefore) - if err != nil { - t.Errorf("error checking renewal state for %s: %v", testData.stringSerial, err) - continue - } - if renewed != testData.IsRenewed { - t.Errorf("for %s: got %v, expected %v", testData.stringSerial, renewed, testData.IsRenewed) - } - } -} - -func TestLifetimeOfACert(t *testing.T) { - testCtx := setup(t, []time.Duration{time.Hour * 24, time.Hour * 24 * 4, time.Hour * 24 * 7}) - defer testCtx.cleanUp() - - regA, err := makeRegistration(testCtx.ssa, 1, jsonKeyA, []string{emailA}) - test.AssertNotError(t, err, "Couldn't store regA") - - certA, err := makeCertificate( - regA.Id, - serial1, - []string{"example-a.com"}, - 0, - testCtx.fc) - test.AssertNotError(t, err, "making certificate") - - err = insertCertificate(certA, time.Time{}) - test.AssertNotError(t, err, "unable to insert Certificate") - - type lifeTest struct { - timeLeft time.Duration - numMsgs int - context string - } - tests := []lifeTest{ - { - timeLeft: 9 * 24 * time.Hour, // 9 days before expiration - - numMsgs: 0, - context: "Expected no emails sent because we are more than 7 days out.", - }, - { - (7*24 + 12) * time.Hour, // 7.5 days before - 1, - "Sent 1 for 7 day notice.", - }, - { - 7 * 24 * time.Hour, - 1, - "The 7 day email was already sent.", - }, - { - (4*24 - 1) * time.Hour, // <4 days before, the mailer did not run yesterday - 2, - "Sent 1 for the 7 day notice, and 1 for the 4 day notice.", - }, - { - 36 * time.Hour, // within 1day + nagMargin - 3, - "Sent 1 for the 7 day notice, 1 for the 4 day notice, and 1 for the 1 day notice.", - }, - { - 12 * time.Hour, - 3, - "The 1 day before email was already sent.", - }, - { - -2 * 24 * time.Hour, // 2 days after expiration - 3, - "No expiration warning emails are sent after expiration", - }, - } - - for _, tt := range tests { - testCtx.fc.Add(-tt.timeLeft) - err = testCtx.m.findExpiringCertificates(context.Background()) - test.AssertNotError(t, err, "error calling findExpiringCertificates") - if len(testCtx.mc.Messages) != tt.numMsgs { - t.Errorf(tt.context+" number of messages: expected %d, got %d", tt.numMsgs, len(testCtx.mc.Messages)) - } - testCtx.fc.Add(tt.timeLeft) - } -} - -func TestDontFindRevokedCert(t *testing.T) { - expiresIn := 24 * time.Hour - testCtx := setup(t, []time.Duration{expiresIn}) - - regA, err := makeRegistration(testCtx.ssa, 1, jsonKeyA, []string{"mailto:one@mail.com"}) - test.AssertNotError(t, err, "Couldn't store regA") - certA, err := makeCertificate( - regA.Id, - serial1, - []string{"example-a.com"}, - expiresIn, - testCtx.fc) - test.AssertNotError(t, err, "making certificate") - - err = insertCertificate(certA, time.Time{}) - test.AssertNotError(t, err, "inserting certificate") - - ctx := context.Background() - - setupDBMap, err := sa.DBMapForTest(vars.DBConnSAFullPerms) - test.AssertNotError(t, err, "sa.NewDbMap failed") - _, err = setupDBMap.ExecContext(ctx, "UPDATE certificateStatus SET status = ? WHERE serial = ?", - string(core.OCSPStatusRevoked), core.SerialToString(serial1)) - test.AssertNotError(t, err, "revoking certificate") - - err = testCtx.m.findExpiringCertificates(ctx) - test.AssertNotError(t, err, "err from findExpiringCertificates") - - if len(testCtx.mc.Messages) != 0 { - t.Errorf("no emails should have been sent, but sent %d", len(testCtx.mc.Messages)) - } -} - -func TestDedupOnRegistration(t *testing.T) { - expiresIn := 96 * time.Hour - testCtx := setup(t, []time.Duration{expiresIn}) - - regA, err := makeRegistration(testCtx.ssa, 1, jsonKeyA, []string{emailA}) - test.AssertNotError(t, err, "Couldn't store regA") - certA, err := makeCertificate( - regA.Id, - serial1, - []string{"example-a.com", "shared-example.com"}, - 72*time.Hour, - testCtx.fc) - test.AssertNotError(t, err, "making certificate") - err = insertCertificate(certA, time.Time{}) - test.AssertNotError(t, err, "inserting certificate") - - certB, err := makeCertificate( - regA.Id, - serial2, - []string{"example-b.com", "shared-example.com"}, - 48*time.Hour, - testCtx.fc) - test.AssertNotError(t, err, "making certificate") - err = insertCertificate(certB, time.Time{}) - test.AssertNotError(t, err, "inserting certificate") - - expires := testCtx.fc.Now().Add(48 * time.Hour) - - err = testCtx.m.findExpiringCertificates(context.Background()) - test.AssertNotError(t, err, "error calling findExpiringCertificates") - if len(testCtx.mc.Messages) > 1 { - t.Errorf("num of messages, want %d, got %d", 1, len(testCtx.mc.Messages)) - } - if len(testCtx.mc.Messages) == 0 { - t.Fatalf("no messages sent") - } - domains := "example-a.com\nexample-b.com\nshared-example.com" - test.AssertEquals(t, testCtx.mc.Messages[0], mocks.MailerMessage{ - To: emailARaw, - // A certificate with three domain names should have one in the subject and - // a count of '2 more' at the end - 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, - expires.Format(time.DateOnly)), - }) -} - -type testCtx struct { - dbMap *db.WrappedMap - ssa sapb.StorageAuthorityClient - mc *mocks.Mailer - fc clock.FakeClock - m *mailer - log *blog.Mock - cleanUp func() -} - -func setup(t *testing.T, nagTimes []time.Duration) *testCtx { - log := blog.NewMock() - - // We use the test_setup user (which has full permissions to everything) - // because the SA we return is used for inserting data to set up the test. - dbMap, err := sa.DBMapForTestWithLog(vars.DBConnSAFullPerms, log) - if err != nil { - t.Fatalf("Couldn't connect the database: %s", err) - } - - fc := clock.NewFake() - ssa, err := sa.NewSQLStorageAuthority(dbMap, dbMap, nil, 1, 0, fc, log, metrics.NoopRegisterer) - if err != nil { - t.Fatalf("unable to create SQLStorageAuthority: %s", err) - } - cleanUp := test.ResetBoulderTestDatabase(t) - - mc := &mocks.Mailer{} - - offsetNags := make([]time.Duration, len(nagTimes)) - for i, t := range nagTimes { - offsetNags[i] = t + 24*time.Hour - } - - m := &mailer{ - log: log, - mailer: mc, - emailTemplate: tmpl, - subjectTemplate: subjTmpl, - dbMap: dbMap, - rs: isa.SA{Impl: ssa}, - nagTimes: offsetNags, - addressLimiter: &limiter{clk: fc, limit: 4}, - certificatesPerTick: 100, - clk: fc, - stats: initStats(metrics.NoopRegisterer), - } - return &testCtx{ - dbMap: dbMap, - ssa: isa.SA{Impl: ssa}, - mc: mc, - fc: fc, - m: m, - log: log, - cleanUp: cleanUp, - } -} - -func TestLimiter(t *testing.T) { - clk := clock.NewFake() - lim := &limiter{clk: clk, limit: 4} - fooAtExample := "foo@example.com" - lim.inc(fooAtExample) - test.AssertNotError(t, lim.check(fooAtExample), "expected no error") - lim.inc(fooAtExample) - test.AssertNotError(t, lim.check(fooAtExample), "expected no error") - lim.inc(fooAtExample) - test.AssertNotError(t, lim.check(fooAtExample), "expected no error") - lim.inc(fooAtExample) - test.AssertError(t, lim.check(fooAtExample), "expected an error") - - clk.Sleep(time.Hour) - test.AssertError(t, lim.check(fooAtExample), "expected an error") - - // Sleep long enough to reset the limit - clk.Sleep(24 * time.Hour) - test.AssertNotError(t, lim.check(fooAtExample), "expected no error") -} diff --git a/third-party/github.com/letsencrypt/boulder/cmd/expiration-mailer/send_test.go b/third-party/github.com/letsencrypt/boulder/cmd/expiration-mailer/send_test.go deleted file mode 100644 index a95816fea..000000000 --- a/third-party/github.com/letsencrypt/boulder/cmd/expiration-mailer/send_test.go +++ /dev/null @@ -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, - } - -} diff --git a/third-party/github.com/letsencrypt/boulder/cmd/id-exporter/main.go b/third-party/github.com/letsencrypt/boulder/cmd/id-exporter/main.go deleted file mode 100644 index fa09cc953..000000000 --- a/third-party/github.com/letsencrypt/boulder/cmd/id-exporter/main.go +++ /dev/null @@ -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{}}) -} diff --git a/third-party/github.com/letsencrypt/boulder/cmd/id-exporter/main_test.go b/third-party/github.com/letsencrypt/boulder/cmd/id-exporter/main_test.go deleted file mode 100644 index 20fdec760..000000000 --- a/third-party/github.com/letsencrypt/boulder/cmd/id-exporter/main_test.go +++ /dev/null @@ -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()) -} diff --git a/third-party/github.com/letsencrypt/boulder/cmd/nonce-service/main.go b/third-party/github.com/letsencrypt/boulder/cmd/nonce-service/main.go index cdc634db7..1e2a62ad2 100644 --- a/third-party/github.com/letsencrypt/boulder/cmd/nonce-service/main.go +++ b/third-party/github.com/letsencrypt/boulder/cmd/nonce-service/main.go @@ -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") diff --git a/third-party/github.com/letsencrypt/boulder/cmd/notify-mailer/main.go b/third-party/github.com/letsencrypt/boulder/cmd/notify-mailer/main.go deleted file mode 100644 index 6c01efd64..000000000 --- a/third-party/github.com/letsencrypt/boulder/cmd/notify-mailer/main.go +++ /dev/null @@ -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{}}) -} diff --git a/third-party/github.com/letsencrypt/boulder/cmd/notify-mailer/main_test.go b/third-party/github.com/letsencrypt/boulder/cmd/notify-mailer/main_test.go deleted file mode 100644 index 4f57069f8..000000000 --- a/third-party/github.com/letsencrypt/boulder/cmd/notify-mailer/main_test.go +++ /dev/null @@ -1,782 +0,0 @@ -package notmain - -import ( - "context" - "database/sql" - "errors" - "fmt" - "io" - "os" - "testing" - "text/template" - "time" - - "github.com/jmhodges/clock" - - "github.com/letsencrypt/boulder/db" - blog "github.com/letsencrypt/boulder/log" - "github.com/letsencrypt/boulder/mocks" - "github.com/letsencrypt/boulder/test" -) - -func TestIntervalOK(t *testing.T) { - // Test a number of intervals know to be OK, ensure that no error is - // produced when calling `ok()`. - okCases := []struct { - testInterval interval - }{ - {interval{}}, - {interval{start: "aa", end: "\xFF"}}, - {interval{end: "aa"}}, - {interval{start: "aa", end: "bb"}}, - } - for _, testcase := range okCases { - err := testcase.testInterval.ok() - test.AssertNotError(t, err, "valid interval produced ok() error") - } - - badInterval := interval{start: "bb", end: "aa"} - err := badInterval.ok() - test.AssertError(t, err, "bad interval was considered ok") -} - -func setupMakeRecipientList(t *testing.T, contents string) string { - entryFile, err := os.CreateTemp("", "") - test.AssertNotError(t, err, "couldn't create temp file") - - _, err = entryFile.WriteString(contents) - test.AssertNotError(t, err, "couldn't write contents to temp file") - - err = entryFile.Close() - test.AssertNotError(t, err, "couldn't close temp file") - return entryFile.Name() -} - -func TestReadRecipientList(t *testing.T) { - contents := `id, domainName, date -10,example.com,2018-11-21 -23,example.net,2018-11-22` - - entryFile := setupMakeRecipientList(t, contents) - defer os.Remove(entryFile) - - list, _, err := readRecipientsList(entryFile, ',') - test.AssertNotError(t, err, "received an error for a valid CSV file") - - expected := []recipient{ - {id: 10, Data: map[string]string{"date": "2018-11-21", "domainName": "example.com"}}, - {id: 23, Data: map[string]string{"date": "2018-11-22", "domainName": "example.net"}}, - } - test.AssertDeepEquals(t, list, expected) - - contents = `id domainName date -10 example.com 2018-11-21 -23 example.net 2018-11-22` - - entryFile = setupMakeRecipientList(t, contents) - defer os.Remove(entryFile) - - list, _, err = readRecipientsList(entryFile, '\t') - test.AssertNotError(t, err, "received an error for a valid TSV file") - test.AssertDeepEquals(t, list, expected) -} - -func TestReadRecipientListNoExtraColumns(t *testing.T) { - contents := `id -10 -23` - - entryFile := setupMakeRecipientList(t, contents) - defer os.Remove(entryFile) - - _, _, err := readRecipientsList(entryFile, ',') - test.AssertNotError(t, err, "received an error for a valid CSV file") -} - -func TestReadRecipientsListFileNoExist(t *testing.T) { - _, _, err := readRecipientsList("doesNotExist", ',') - test.AssertError(t, err, "expected error for a file that doesn't exist") -} - -func TestReadRecipientListWithEmptyColumnInHeader(t *testing.T) { - contents := `id, domainName,,date -10,example.com,2018-11-21 -23,example.net` - - entryFile := setupMakeRecipientList(t, contents) - defer os.Remove(entryFile) - - _, _, err := readRecipientsList(entryFile, ',') - test.AssertError(t, err, "failed to error on CSV file with trailing delimiter in header") - test.AssertDeepEquals(t, err, errors.New("header contains an empty column")) -} - -func TestReadRecipientListWithProblems(t *testing.T) { - contents := `id, domainName, date -10,example.com,2018-11-21 -23,example.net, -10,example.com,2018-11-22 -42,example.net, -24,example.com,2018-11-21 -24,example.com,2018-11-21 -` - - entryFile := setupMakeRecipientList(t, contents) - defer os.Remove(entryFile) - - recipients, probs, err := readRecipientsList(entryFile, ',') - test.AssertNotError(t, err, "received an error for a valid CSV file") - test.AssertEquals(t, probs, "ID(s) [23 42] contained empty columns and ID(s) [10 24] were skipped as duplicates") - test.AssertEquals(t, len(recipients), 4) - - // Ensure trailing " and " is trimmed from single problem. - contents = `id, domainName, date -23,example.net, -10,example.com,2018-11-21 -42,example.net, -` - - entryFile = setupMakeRecipientList(t, contents) - defer os.Remove(entryFile) - - _, probs, err = readRecipientsList(entryFile, ',') - test.AssertNotError(t, err, "received an error for a valid CSV file") - test.AssertEquals(t, probs, "ID(s) [23 42] contained empty columns") -} - -func TestReadRecipientListWithEmptyLine(t *testing.T) { - contents := `id, domainName, date -10,example.com,2018-11-21 - -23,example.net,2018-11-22` - - entryFile := setupMakeRecipientList(t, contents) - defer os.Remove(entryFile) - - _, _, err := readRecipientsList(entryFile, ',') - test.AssertNotError(t, err, "received an error for a valid CSV file") -} - -func TestReadRecipientListWithMismatchedColumns(t *testing.T) { - contents := `id, domainName, date -10,example.com,2018-11-21 -23,example.net` - - entryFile := setupMakeRecipientList(t, contents) - defer os.Remove(entryFile) - - _, _, err := readRecipientsList(entryFile, ',') - test.AssertError(t, err, "failed to error on CSV file with mismatched columns") -} - -func TestReadRecipientListWithDuplicateIDs(t *testing.T) { - contents := `id, domainName, date -10,example.com,2018-11-21 -10,example.net,2018-11-22` - - entryFile := setupMakeRecipientList(t, contents) - defer os.Remove(entryFile) - - _, _, err := readRecipientsList(entryFile, ',') - test.AssertNotError(t, err, "received an error for a valid CSV file") -} - -func TestReadRecipientListWithUnparsableID(t *testing.T) { - contents := `id, domainName, date -10,example.com,2018-11-21 -twenty,example.net,2018-11-22` - - entryFile := setupMakeRecipientList(t, contents) - defer os.Remove(entryFile) - - _, _, err := readRecipientsList(entryFile, ',') - test.AssertError(t, err, "expected error for CSV file that contains an unparsable registration ID") -} - -func TestReadRecipientListWithoutIDHeader(t *testing.T) { - contents := `notId, domainName, date -10,example.com,2018-11-21 -twenty,example.net,2018-11-22` - - entryFile := setupMakeRecipientList(t, contents) - defer os.Remove(entryFile) - - _, _, err := readRecipientsList(entryFile, ',') - test.AssertError(t, err, "expected error for CSV file missing header field `id`") -} - -func TestReadRecipientListWithNoRecords(t *testing.T) { - contents := `id, domainName, date -` - entryFile := setupMakeRecipientList(t, contents) - defer os.Remove(entryFile) - - _, _, err := readRecipientsList(entryFile, ',') - test.AssertError(t, err, "expected error for CSV file containing only a header") -} - -func TestReadRecipientListWithNoHeaderOrRecords(t *testing.T) { - contents := `` - entryFile := setupMakeRecipientList(t, contents) - defer os.Remove(entryFile) - - _, _, err := readRecipientsList(entryFile, ',') - test.AssertError(t, err, "expected error for CSV file containing only a header") - test.AssertErrorIs(t, err, io.EOF) -} - -func TestMakeMessageBody(t *testing.T) { - emailTemplate := `{{range . }} -{{ .Data.date }} -{{ .Data.domainName }} -{{end}}` - - m := &mailer{ - log: blog.UseMock(), - mailer: &mocks.Mailer{}, - emailTemplate: template.Must(template.New("email").Parse(emailTemplate)).Option("missingkey=error"), - sleepInterval: 0, - targetRange: interval{end: "\xFF"}, - clk: clock.NewFake(), - recipients: nil, - dbMap: mockEmailResolver{}, - } - - recipients := []recipient{ - {id: 10, Data: map[string]string{"date": "2018-11-21", "domainName": "example.com"}}, - {id: 23, Data: map[string]string{"date": "2018-11-22", "domainName": "example.net"}}, - } - - expectedMessageBody := ` -2018-11-21 -example.com - -2018-11-22 -example.net -` - - // Ensure that a very basic template with 2 recipients can be successfully - // executed. - messageBody, err := m.makeMessageBody(recipients) - test.AssertNotError(t, err, "failed to execute a valid template") - test.AssertEquals(t, messageBody, expectedMessageBody) - - // With no recipients we should get an empty body error. - recipients = []recipient{} - _, err = m.makeMessageBody(recipients) - test.AssertError(t, err, "should have errored on empty body") - - // With a missing key we should get an informative templating error. - recipients = []recipient{{id: 10, Data: map[string]string{"domainName": "example.com"}}} - _, err = m.makeMessageBody(recipients) - test.AssertEquals(t, err.Error(), "template: email:2:8: executing \"email\" at <.Data.date>: map has no entry for key \"date\"") -} - -func TestSleepInterval(t *testing.T) { - const sleepLen = 10 - mc := &mocks.Mailer{} - dbMap := mockEmailResolver{} - tmpl := template.Must(template.New("letter").Parse("an email body")) - recipients := []recipient{{id: 1}, {id: 2}, {id: 3}} - // Set up a mock mailer that sleeps for `sleepLen` seconds and only has one - // goroutine to process results - m := &mailer{ - log: blog.UseMock(), - mailer: mc, - emailTemplate: tmpl, - sleepInterval: sleepLen * time.Second, - parallelSends: 1, - targetRange: interval{start: "", end: "\xFF"}, - clk: clock.NewFake(), - recipients: recipients, - dbMap: dbMap, - } - - // Call run() - this should sleep `sleepLen` per destination address - // After it returns, we expect (sleepLen * number of destinations) seconds has - // elapsed - err := m.run(context.Background()) - test.AssertNotError(t, err, "error calling mailer run()") - expectedEnd := clock.NewFake() - expectedEnd.Add(time.Second * time.Duration(sleepLen*len(recipients))) - test.AssertEquals(t, m.clk.Now(), expectedEnd.Now()) - - // Set up a mock mailer that doesn't sleep at all - m = &mailer{ - log: blog.UseMock(), - mailer: mc, - emailTemplate: tmpl, - sleepInterval: 0, - targetRange: interval{end: "\xFF"}, - clk: clock.NewFake(), - recipients: recipients, - dbMap: dbMap, - } - - // Call run() - this should blast through all destinations without sleep - // After it returns, we expect no clock time to have elapsed on the fake clock - err = m.run(context.Background()) - test.AssertNotError(t, err, "error calling mailer run()") - expectedEnd = clock.NewFake() - test.AssertEquals(t, m.clk.Now(), expectedEnd.Now()) -} - -func TestMailIntervals(t *testing.T) { - const testSubject = "Test Subject" - dbMap := mockEmailResolver{} - - tmpl := template.Must(template.New("letter").Parse("an email body")) - recipients := []recipient{{id: 1}, {id: 2}, {id: 3}} - - mc := &mocks.Mailer{} - - // Create a mailer with a checkpoint interval larger than any of the - // destination email addresses. - m := &mailer{ - log: blog.UseMock(), - mailer: mc, - dbMap: dbMap, - subject: testSubject, - recipients: recipients, - emailTemplate: tmpl, - targetRange: interval{start: "\xFF", end: "\xFF\xFF"}, - sleepInterval: 0, - clk: clock.NewFake(), - } - - // Run the mailer. It should produce an error about the interval start - mc.Clear() - err := m.run(context.Background()) - test.AssertError(t, err, "expected error") - test.AssertEquals(t, len(mc.Messages), 0) - - // Create a mailer with a negative sleep interval - m = &mailer{ - log: blog.UseMock(), - mailer: mc, - dbMap: dbMap, - subject: testSubject, - recipients: recipients, - emailTemplate: tmpl, - targetRange: interval{}, - sleepInterval: -10, - clk: clock.NewFake(), - } - - // Run the mailer. It should produce an error about the sleep interval - mc.Clear() - err = m.run(context.Background()) - test.AssertEquals(t, len(mc.Messages), 0) - test.AssertEquals(t, err.Error(), "sleep interval (-10) is < 0") - - // Create a mailer with an interval starting with a specific email address. - // It should send email to that address and others alphabetically higher. - m = &mailer{ - log: blog.UseMock(), - mailer: mc, - dbMap: dbMap, - subject: testSubject, - recipients: []recipient{{id: 1}, {id: 2}, {id: 3}, {id: 4}}, - emailTemplate: tmpl, - targetRange: interval{start: "test-example-updated@letsencrypt.org", end: "\xFF"}, - sleepInterval: 0, - clk: clock.NewFake(), - } - - // Run the mailer. Two messages should have been produced, one to - // test-example-updated@letsencrypt.org (beginning of the range), - // and one to test-test-test@letsencrypt.org. - mc.Clear() - err = m.run(context.Background()) - test.AssertNotError(t, err, "run() produced an error") - test.AssertEquals(t, len(mc.Messages), 2) - test.AssertEquals(t, mocks.MailerMessage{ - To: "test-example-updated@letsencrypt.org", - Subject: testSubject, - Body: "an email body", - }, mc.Messages[0]) - test.AssertEquals(t, mocks.MailerMessage{ - To: "test-test-test@letsencrypt.org", - Subject: testSubject, - Body: "an email body", - }, mc.Messages[1]) - - // Create a mailer with a checkpoint interval ending before - // "test-example-updated@letsencrypt.org" - m = &mailer{ - log: blog.UseMock(), - mailer: mc, - dbMap: dbMap, - subject: testSubject, - recipients: []recipient{{id: 1}, {id: 2}, {id: 3}, {id: 4}}, - emailTemplate: tmpl, - targetRange: interval{end: "test-example-updated@letsencrypt.org"}, - sleepInterval: 0, - clk: clock.NewFake(), - } - - // Run the mailer. Two messages should have been produced, one to - // example@letsencrypt.org (ID 1), one to example-example-example@example.com (ID 2) - mc.Clear() - err = m.run(context.Background()) - test.AssertNotError(t, err, "run() produced an error") - test.AssertEquals(t, len(mc.Messages), 2) - test.AssertEquals(t, mocks.MailerMessage{ - To: "example-example-example@letsencrypt.org", - Subject: testSubject, - Body: "an email body", - }, mc.Messages[0]) - test.AssertEquals(t, mocks.MailerMessage{ - To: "example@letsencrypt.org", - Subject: testSubject, - Body: "an email body", - }, mc.Messages[1]) -} - -func TestParallelism(t *testing.T) { - const testSubject = "Test Subject" - dbMap := mockEmailResolver{} - - tmpl := template.Must(template.New("letter").Parse("an email body")) - recipients := []recipient{{id: 1}, {id: 2}, {id: 3}, {id: 4}} - - mc := &mocks.Mailer{} - - // Create a mailer with 10 parallel workers. - m := &mailer{ - log: blog.UseMock(), - mailer: mc, - dbMap: dbMap, - subject: testSubject, - recipients: recipients, - emailTemplate: tmpl, - targetRange: interval{end: "\xFF"}, - sleepInterval: 0, - parallelSends: 10, - clk: clock.NewFake(), - } - - mc.Clear() - err := m.run(context.Background()) - test.AssertNotError(t, err, "run() produced an error") - - // The fake clock should have advanced 9 seconds, one for each parallel - // goroutine after the first doing its polite 1-second sleep at startup. - expectedEnd := clock.NewFake() - expectedEnd.Add(9 * time.Second) - test.AssertEquals(t, m.clk.Now(), expectedEnd.Now()) - - // A message should have been sent to all four addresses. - test.AssertEquals(t, len(mc.Messages), 4) - expectedAddresses := []string{ - "example@letsencrypt.org", - "test-example-updated@letsencrypt.org", - "test-test-test@letsencrypt.org", - "example-example-example@letsencrypt.org", - } - for _, msg := range mc.Messages { - test.AssertSliceContains(t, expectedAddresses, msg.To) - } -} - -func TestMessageContentStatic(t *testing.T) { - // Create a mailer with fixed content - const ( - testSubject = "Test Subject" - ) - dbMap := mockEmailResolver{} - mc := &mocks.Mailer{} - m := &mailer{ - log: blog.UseMock(), - mailer: mc, - dbMap: dbMap, - subject: testSubject, - recipients: []recipient{{id: 1}}, - emailTemplate: template.Must(template.New("letter").Parse("an email body")), - targetRange: interval{end: "\xFF"}, - sleepInterval: 0, - clk: clock.NewFake(), - } - - // Run the mailer, one message should have been created with the content - // expected - err := m.run(context.Background()) - test.AssertNotError(t, err, "error calling mailer run()") - test.AssertEquals(t, len(mc.Messages), 1) - test.AssertEquals(t, mocks.MailerMessage{ - To: "example@letsencrypt.org", - Subject: testSubject, - Body: "an email body", - }, mc.Messages[0]) -} - -// Send mail with a variable interpolated. -func TestMessageContentInterpolated(t *testing.T) { - recipients := []recipient{ - { - id: 1, - Data: map[string]string{ - "validationMethod": "eyeballing it", - }, - }, - } - dbMap := mockEmailResolver{} - mc := &mocks.Mailer{} - m := &mailer{ - log: blog.UseMock(), - mailer: mc, - dbMap: dbMap, - subject: "Test Subject", - recipients: recipients, - emailTemplate: template.Must(template.New("letter").Parse( - `issued by {{range .}}{{ .Data.validationMethod }}{{end}}`)), - targetRange: interval{end: "\xFF"}, - sleepInterval: 0, - clk: clock.NewFake(), - } - - // Run the mailer, one message should have been created with the content - // expected - err := m.run(context.Background()) - test.AssertNotError(t, err, "error calling mailer run()") - test.AssertEquals(t, len(mc.Messages), 1) - test.AssertEquals(t, mocks.MailerMessage{ - To: "example@letsencrypt.org", - Subject: "Test Subject", - Body: "issued by eyeballing it", - }, mc.Messages[0]) -} - -// Send mail with a variable interpolated multiple times for accounts that share -// an email address. -func TestMessageContentInterpolatedMultiple(t *testing.T) { - recipients := []recipient{ - { - id: 200, - Data: map[string]string{ - "domain": "blog.example.com", - }, - }, - { - id: 201, - Data: map[string]string{ - "domain": "nas.example.net", - }, - }, - { - id: 202, - Data: map[string]string{ - "domain": "mail.example.org", - }, - }, - { - id: 203, - Data: map[string]string{ - "domain": "panel.example.net", - }, - }, - } - dbMap := mockEmailResolver{} - mc := &mocks.Mailer{} - m := &mailer{ - log: blog.UseMock(), - mailer: mc, - dbMap: dbMap, - subject: "Test Subject", - recipients: recipients, - emailTemplate: template.Must(template.New("letter").Parse( - `issued for: -{{range .}}{{ .Data.domain }} -{{end}}Thanks`)), - targetRange: interval{end: "\xFF"}, - sleepInterval: 0, - clk: clock.NewFake(), - } - - // Run the mailer, one message should have been created with the content - // expected - err := m.run(context.Background()) - test.AssertNotError(t, err, "error calling mailer run()") - test.AssertEquals(t, len(mc.Messages), 1) - test.AssertEquals(t, mocks.MailerMessage{ - To: "gotta.lotta.accounts@letsencrypt.org", - Subject: "Test Subject", - Body: `issued for: -blog.example.com -nas.example.net -mail.example.org -panel.example.net -Thanks`, - }, mc.Messages[0]) -} - -// the `mockEmailResolver` implements the `dbSelector` interface from -// `notify-mailer/main.go` to allow unit testing without using a backing -// database -type mockEmailResolver struct{} - -// the `mockEmailResolver` select method treats the requested reg ID as an index -// into a list of anonymous structs -func (bs mockEmailResolver) SelectOne(ctx context.Context, output interface{}, _ string, args ...interface{}) error { - // The "dbList" is just a list of contact records in memory - dbList := []contactQueryResult{ - { - ID: 1, - Contact: []byte(`["mailto:example@letsencrypt.org"]`), - }, - { - ID: 2, - Contact: []byte(`["mailto:test-example-updated@letsencrypt.org"]`), - }, - { - ID: 3, - Contact: []byte(`["mailto:test-test-test@letsencrypt.org"]`), - }, - { - ID: 4, - Contact: []byte(`["mailto:example-example-example@letsencrypt.org"]`), - }, - { - ID: 5, - Contact: []byte(`["mailto:youve.got.mail@letsencrypt.org"]`), - }, - { - ID: 6, - Contact: []byte(`["mailto:mail@letsencrypt.org"]`), - }, - { - ID: 7, - Contact: []byte(`["mailto:***********"]`), - }, - { - ID: 200, - Contact: []byte(`["mailto:gotta.lotta.accounts@letsencrypt.org"]`), - }, - { - ID: 201, - Contact: []byte(`["mailto:gotta.lotta.accounts@letsencrypt.org"]`), - }, - { - ID: 202, - Contact: []byte(`["mailto:gotta.lotta.accounts@letsencrypt.org"]`), - }, - { - ID: 203, - Contact: []byte(`["mailto:gotta.lotta.accounts@letsencrypt.org"]`), - }, - { - ID: 204, - Contact: []byte(`["mailto:gotta.lotta.accounts@letsencrypt.org"]`), - }, - } - - // Play the type cast game so that we can dig into the arguments map and get - // out an int64 `id` parameter. - argsRaw := args[0] - argsMap, ok := argsRaw.(map[string]interface{}) - if !ok { - return fmt.Errorf("incorrect args type %T", args) - } - idRaw := argsMap["id"] - id, ok := idRaw.(int64) - if !ok { - return fmt.Errorf("incorrect args ID type %T", id) - } - - // Play the type cast game to get a `*contactQueryResult` so we can write - // the result from the db list. - outputPtr, ok := output.(*contactQueryResult) - if !ok { - return fmt.Errorf("incorrect output type %T", output) - } - - for _, v := range dbList { - if v.ID == id { - *outputPtr = v - } - } - if outputPtr.ID == 0 { - return db.ErrDatabaseOp{ - Op: "select one", - Table: "registrations", - Err: sql.ErrNoRows, - } - } - return nil -} - -func TestResolveEmails(t *testing.T) { - // Start with three reg. IDs. Note: the IDs have been matched with fake - // results in the `db` slice in `mockEmailResolver`'s `SelectOne`. If you add - // more test cases here you must also add the corresponding DB result in the - // mock. - recipients := []recipient{ - { - id: 1, - }, - { - id: 2, - }, - { - id: 3, - }, - // This registration ID deliberately doesn't exist in the mock data to make - // sure this case is handled gracefully - { - id: 999, - }, - // This registration ID deliberately returns an invalid email to make sure any - // invalid contact info that slipped into the DB once upon a time will be ignored - { - id: 7, - }, - { - id: 200, - }, - { - id: 201, - }, - { - id: 202, - }, - { - id: 203, - }, - { - id: 204, - }, - } - - tmpl := template.Must(template.New("letter").Parse("an email body")) - - dbMap := mockEmailResolver{} - mc := &mocks.Mailer{} - m := &mailer{ - log: blog.UseMock(), - mailer: mc, - dbMap: dbMap, - subject: "Test", - recipients: recipients, - emailTemplate: tmpl, - targetRange: interval{end: "\xFF"}, - sleepInterval: 0, - clk: clock.NewFake(), - } - - addressesToRecipients, err := m.resolveAddresses(context.Background()) - test.AssertNotError(t, err, "failed to resolveEmailAddresses") - - expected := []string{ - "example@letsencrypt.org", - "test-example-updated@letsencrypt.org", - "test-test-test@letsencrypt.org", - "gotta.lotta.accounts@letsencrypt.org", - } - - test.AssertEquals(t, len(addressesToRecipients), len(expected)) - for _, address := range expected { - if _, ok := addressesToRecipients[address]; !ok { - t.Errorf("missing entry in addressesToRecipients: %q", address) - } - } -} diff --git a/third-party/github.com/letsencrypt/boulder/cmd/notify-mailer/testdata/test_msg_body.txt b/third-party/github.com/letsencrypt/boulder/cmd/notify-mailer/testdata/test_msg_body.txt deleted file mode 100644 index 16417d92c..000000000 --- a/third-party/github.com/letsencrypt/boulder/cmd/notify-mailer/testdata/test_msg_body.txt +++ /dev/null @@ -1,3 +0,0 @@ -This is a test message body regarding these domains: -{{ range . }} {{ .Extra.domainName }} -{{ end }} diff --git a/third-party/github.com/letsencrypt/boulder/cmd/notify-mailer/testdata/test_msg_recipients.csv b/third-party/github.com/letsencrypt/boulder/cmd/notify-mailer/testdata/test_msg_recipients.csv deleted file mode 100644 index ce3b9f86a..000000000 --- a/third-party/github.com/letsencrypt/boulder/cmd/notify-mailer/testdata/test_msg_recipients.csv +++ /dev/null @@ -1,4 +0,0 @@ -id,domainName -1,one.example.com -2,two.example.net -3,three.example.org diff --git a/third-party/github.com/letsencrypt/boulder/cmd/ocsp-responder/main.go b/third-party/github.com/letsencrypt/boulder/cmd/ocsp-responder/main.go index 4c14ead1e..ec03eb05f 100644 --- a/third-party/github.com/letsencrypt/boulder/cmd/ocsp-responder/main.go +++ b/third-party/github.com/letsencrypt/boulder/cmd/ocsp-responder/main.go @@ -51,10 +51,15 @@ type Config struct { // OCSP requests. This has a default value of ":80". ListenAddress string `validate:"omitempty,hostname_port"` - // When to timeout a request. This should be slightly lower than the - // upstream's timeout when making request to ocsp-responder. + // Timeout is the per-request overall timeout. This should be slightly + // 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 + // How often a response should be signed when using Redis/live-signing // path. This has a default value of 60h. LiveSigningPeriod config.Duration `validate:"-"` @@ -80,8 +85,6 @@ type Config struct { // 40 * 5 / 0.02 = 10,000 requests before the oldest request times out. MaxSigningWaiters int `validate:"min=0"` - ShutdownStopTimeout config.Duration - RequiredSerialPrefixes []string `validate:"omitempty,dive,hexadecimal"` Features features.Config diff --git a/third-party/github.com/letsencrypt/boulder/cmd/remoteva/main.go b/third-party/github.com/letsencrypt/boulder/cmd/remoteva/main.go index 9ea068fc0..f4c0cbe76 100644 --- a/third-party/github.com/letsencrypt/boulder/cmd/remoteva/main.go +++ b/third-party/github.com/letsencrypt/boulder/cmd/remoteva/main.go @@ -11,6 +11,7 @@ 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" @@ -20,6 +21,25 @@ type Config struct { RVA struct { vaConfig.Common + // 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 `omitempty:"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"` + // SkipGRPCClientCertVerification, when disabled as it should typically // be, will cause the remoteva server (which receives gRPCs from a // boulder-va client) to use our default RequireAndVerifyClientCert @@ -67,16 +87,12 @@ func main() { clk := cmd.Clock() var servers bdns.ServerProvider - proto := "udp" - if features.Get().DOH { - proto = "tcp" - } if len(c.RVA.DNSStaticResolvers) != 0 { servers, err = bdns.NewStaticProvider(c.RVA.DNSStaticResolvers) cmd.FailOnError(err, "Couldn't start static DNS server resolver") } else { - servers, err = bdns.StartDynamicProvider(c.RVA.DNSProvider, 60*time.Second, proto) + servers, err = bdns.StartDynamicProvider(c.RVA.DNSProvider, 60*time.Second, "tcp") cmd.FailOnError(err, "Couldn't start dynamic DNS server resolver") } defer servers.Stop() @@ -96,6 +112,7 @@ func main() { scope, clk, c.RVA.DNSTries, + c.RVA.UserAgent, logger, tlsConfig) } else { @@ -105,6 +122,7 @@ func main() { scope, clk, c.RVA.DNSTries, + c.RVA.UserAgent, logger, tlsConfig) } @@ -112,13 +130,15 @@ func main() { vai, err := va.NewValidationAuthorityImpl( resolver, nil, // Our RVAs will never have RVAs of their own. - 0, // Only the VA is concerned with max validation failures c.RVA.UserAgent, c.RVA.IssuerDomain, scope, clk, logger, - c.RVA.AccountURIPrefixes) + c.RVA.AccountURIPrefixes, + c.RVA.Perspective, + c.RVA.RIR, + iana.IsReservedAddr) cmd.FailOnError(err, "Unable to create Remote-VA server") start, err := bgrpc.NewServer(c.RVA.GRPC, logger).Add( diff --git a/third-party/github.com/letsencrypt/boulder/cmd/reversed-hostname-checker/main.go b/third-party/github.com/letsencrypt/boulder/cmd/reversed-hostname-checker/main.go index b0a354d15..530dd7ca3 100644 --- a/third-party/github.com/letsencrypt/boulder/cmd/reversed-hostname-checker/main.go +++ b/third-party/github.com/letsencrypt/boulder/cmd/reversed-hostname-checker/main.go @@ -1,5 +1,5 @@ -// Read a list of reversed hostnames, separated by newlines. Print only those -// that are rejected by the current policy. +// Read a list of reversed FQDNs and/or normal IP addresses, separated by +// newlines. Print only those that are rejected by the current policy. package notmain @@ -9,9 +9,11 @@ import ( "fmt" "io" "log" + "net/netip" "os" "github.com/letsencrypt/boulder/cmd" + "github.com/letsencrypt/boulder/identifier" "github.com/letsencrypt/boulder/policy" "github.com/letsencrypt/boulder/sa" ) @@ -39,7 +41,7 @@ func main() { scanner := bufio.NewScanner(input) logger := cmd.NewLogger(cmd.SyslogConfig{StdoutLevel: 7}) logger.Info(cmd.VersionString()) - pa, err := policy.New(nil, logger) + pa, err := policy.New(nil, nil, logger) if err != nil { log.Fatal(err) } @@ -49,8 +51,15 @@ func main() { } var errors bool for scanner.Scan() { - n := sa.ReverseName(scanner.Text()) - err := pa.WillingToIssue([]string{n}) + n := sa.EncodeIssuedName(scanner.Text()) + var ident identifier.ACMEIdentifier + ip, err := netip.ParseAddr(n) + if err == nil { + ident = identifier.NewIP(ip) + } else { + ident = identifier.NewDNS(n) + } + err = pa.WillingToIssue(identifier.ACMEIdentifiers{ident}) if err != nil { errors = true fmt.Printf("%s: %s\n", n, err) diff --git a/third-party/github.com/letsencrypt/boulder/cmd/rocsp-tool/client.go b/third-party/github.com/letsencrypt/boulder/cmd/rocsp-tool/client.go index c70fa30aa..6800f9f46 100644 --- a/third-party/github.com/letsencrypt/boulder/cmd/rocsp-tool/client.go +++ b/third-party/github.com/letsencrypt/boulder/cmd/rocsp-tool/client.go @@ -3,7 +3,7 @@ package notmain import ( "context" "fmt" - "math/rand" + "math/rand/v2" "os" "sync/atomic" "time" @@ -34,7 +34,7 @@ type client struct { // for a single certificateStatus ID. If `err` is non-nil, it indicates the // attempt failed. type processResult struct { - id uint64 + id int64 err error } @@ -104,9 +104,9 @@ func (cl *client) loadFromDB(ctx context.Context, speed ProcessingSpeed, startFr if result.err != nil { errorCount++ if errorCount < 10 || - (errorCount < 1000 && rand.Intn(1000) < 100) || - (errorCount < 100000 && rand.Intn(1000) < 10) || - (rand.Intn(1000) < 1) { + (errorCount < 1000 && rand.IntN(1000) < 100) || + (errorCount < 100000 && rand.IntN(1000) < 10) || + (rand.IntN(1000) < 1) { cl.logger.Errf("error: %s", result.err) } } else { @@ -115,9 +115,9 @@ func (cl *client) loadFromDB(ctx context.Context, speed ProcessingSpeed, startFr total := successCount + errorCount if total < 10 || - (total < 1000 && rand.Intn(1000) < 100) || - (total < 100000 && rand.Intn(1000) < 10) || - (rand.Intn(1000) < 1) { + (total < 1000 && rand.IntN(1000) < 100) || + (total < 100000 && rand.IntN(1000) < 10) || + (rand.IntN(1000) < 1) { cl.logger.Infof("stored %d responses, %d errors", successCount, errorCount) } } @@ -181,7 +181,7 @@ func (cl *client) scanFromDBOneBatch(ctx context.Context, prevID int64, frequenc return fmt.Errorf("scanning row %d (previous ID %d): %w", scanned, previousID, err) } scanned++ - inflightIDs.add(uint64(status.ID)) + inflightIDs.add(status.ID) // Emit a log line every 100000 rows. For our current ~215M rows, that // will emit about 2150 log lines. This probably strikes a good balance // between too spammy and having a reasonably frequent checkpoint. @@ -213,25 +213,25 @@ func (cl *client) signAndStoreResponses(ctx context.Context, input <-chan *sa.Ce Serial: status.Serial, IssuerID: status.IssuerID, Status: string(status.Status), - Reason: int32(status.RevokedReason), + Reason: int32(status.RevokedReason), //nolint: gosec // Revocation reasons are guaranteed to be small, no risk of overflow. RevokedAt: timestamppb.New(status.RevokedDate), } result, err := cl.ocspGenerator.GenerateOCSP(ctx, ocspReq) if err != nil { - output <- processResult{id: uint64(status.ID), err: err} + output <- processResult{id: status.ID, err: err} continue } resp, err := ocsp.ParseResponse(result.Response, nil) if err != nil { - output <- processResult{id: uint64(status.ID), err: err} + output <- processResult{id: status.ID, err: err} continue } err = cl.redis.StoreResponse(ctx, resp) if err != nil { - output <- processResult{id: uint64(status.ID), err: err} + output <- processResult{id: status.ID, err: err} } else { - output <- processResult{id: uint64(status.ID), err: nil} + output <- processResult{id: status.ID, err: nil} } } } diff --git a/third-party/github.com/letsencrypt/boulder/cmd/rocsp-tool/client_test.go b/third-party/github.com/letsencrypt/boulder/cmd/rocsp-tool/client_test.go index ddb11f015..7e04bb1d9 100644 --- a/third-party/github.com/letsencrypt/boulder/cmd/rocsp-tool/client_test.go +++ b/third-party/github.com/letsencrypt/boulder/cmd/rocsp-tool/client_test.go @@ -15,6 +15,7 @@ import ( capb "github.com/letsencrypt/boulder/ca/proto" "github.com/letsencrypt/boulder/cmd" "github.com/letsencrypt/boulder/core" + "github.com/letsencrypt/boulder/db" blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/metrics" "github.com/letsencrypt/boulder/rocsp" @@ -39,8 +40,8 @@ func makeClient() (*rocsp.RWClient, clock.Clock) { rdb := redis.NewRing(&redis.RingOptions{ Addrs: map[string]string{ - "shard1": "10.33.33.2:4218", - "shard2": "10.33.33.3:4218", + "shard1": "10.77.77.2:4218", + "shard2": "10.77.77.3:4218", }, Username: "unittest-rw", Password: "824968fa490f4ecec1e52d5e34916bdb60d45f8d", @@ -50,29 +51,34 @@ func makeClient() (*rocsp.RWClient, clock.Clock) { return rocsp.NewWritingClient(rdb, 500*time.Millisecond, clk, metrics.NoopRegisterer), clk } -func TestGetStartingID(t *testing.T) { - ctx := context.Background() +func insertCertificateStatus(t *testing.T, dbMap db.Executor, serial string, notAfter, ocspLastUpdated time.Time) int64 { + result, err := dbMap.ExecContext(context.Background(), + `INSERT INTO certificateStatus + (serial, notAfter, status, ocspLastUpdated, revokedDate, revokedReason, lastExpirationNagSent, issuerID) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, + serial, + notAfter, + core.OCSPStatusGood, + ocspLastUpdated, + time.Time{}, + 0, + time.Time{}, + 99) + test.AssertNotError(t, err, "inserting certificate status") + id, err := result.LastInsertId() + test.AssertNotError(t, err, "getting last insert ID") + return id +} +func TestGetStartingID(t *testing.T) { clk := clock.NewFake() dbMap, err := sa.DBMapForTest(vars.DBConnSAFullPerms) test.AssertNotError(t, err, "failed setting up db client") defer test.ResetBoulderTestDatabase(t)() - cs := core.CertificateStatus{ - Serial: "1337", - NotAfter: clk.Now().Add(12 * time.Hour), - } - err = dbMap.Insert(ctx, &cs) - test.AssertNotError(t, err, "inserting certificate status") - firstID := cs.ID + firstID := insertCertificateStatus(t, dbMap, "1337", clk.Now().Add(12*time.Hour), time.Time{}) + secondID := insertCertificateStatus(t, dbMap, "1338", clk.Now().Add(36*time.Hour), time.Time{}) - cs = core.CertificateStatus{ - Serial: "1338", - NotAfter: clk.Now().Add(36 * time.Hour), - } - err = dbMap.Insert(ctx, &cs) - test.AssertNotError(t, err, "inserting certificate status") - secondID := cs.ID t.Logf("first ID %d, second ID %d", firstID, secondID) clk.Sleep(48 * time.Hour) @@ -131,11 +137,7 @@ func TestLoadFromDB(t *testing.T) { defer test.ResetBoulderTestDatabase(t) for i := range 100 { - err = dbMap.Insert(context.Background(), &core.CertificateStatus{ - Serial: fmt.Sprintf("%036x", i), - NotAfter: clk.Now().Add(200 * time.Hour), - OCSPLastUpdated: clk.Now(), - }) + insertCertificateStatus(t, dbMap, fmt.Sprintf("%036x", i), clk.Now().Add(200*time.Hour), clk.Now()) if err != nil { t.Fatalf("Failed to insert certificateStatus: %s", err) } diff --git a/third-party/github.com/letsencrypt/boulder/cmd/rocsp-tool/inflight.go b/third-party/github.com/letsencrypt/boulder/cmd/rocsp-tool/inflight.go index 5a0ca5ba6..b64130537 100644 --- a/third-party/github.com/letsencrypt/boulder/cmd/rocsp-tool/inflight.go +++ b/third-party/github.com/letsencrypt/boulder/cmd/rocsp-tool/inflight.go @@ -4,22 +4,22 @@ import "sync" type inflight struct { sync.RWMutex - items map[uint64]struct{} + items map[int64]struct{} } func newInflight() *inflight { return &inflight{ - items: make(map[uint64]struct{}), + items: make(map[int64]struct{}), } } -func (i *inflight) add(n uint64) { +func (i *inflight) add(n int64) { i.Lock() defer i.Unlock() i.items[n] = struct{}{} } -func (i *inflight) remove(n uint64) { +func (i *inflight) remove(n int64) { i.Lock() defer i.Unlock() delete(i.items, n) @@ -34,13 +34,13 @@ func (i *inflight) len() int { // min returns the numerically smallest key inflight. If nothing is inflight, // it returns 0. Note: this takes O(n) time in the number of keys and should // be called rarely. -func (i *inflight) min() uint64 { +func (i *inflight) min() int64 { i.RLock() defer i.RUnlock() if len(i.items) == 0 { return 0 } - var min uint64 + var min int64 for k := range i.items { if min == 0 { min = k diff --git a/third-party/github.com/letsencrypt/boulder/cmd/rocsp-tool/inflight_test.go b/third-party/github.com/letsencrypt/boulder/cmd/rocsp-tool/inflight_test.go index 9ce52ee03..d157eb9c2 100644 --- a/third-party/github.com/letsencrypt/boulder/cmd/rocsp-tool/inflight_test.go +++ b/third-party/github.com/letsencrypt/boulder/cmd/rocsp-tool/inflight_test.go @@ -9,25 +9,25 @@ import ( func TestInflight(t *testing.T) { ifl := newInflight() test.AssertEquals(t, ifl.len(), 0) - test.AssertEquals(t, ifl.min(), uint64(0)) + test.AssertEquals(t, ifl.min(), int64(0)) ifl.add(1337) test.AssertEquals(t, ifl.len(), 1) - test.AssertEquals(t, ifl.min(), uint64(1337)) + test.AssertEquals(t, ifl.min(), int64(1337)) ifl.remove(1337) test.AssertEquals(t, ifl.len(), 0) - test.AssertEquals(t, ifl.min(), uint64(0)) + test.AssertEquals(t, ifl.min(), int64(0)) ifl.add(7341) ifl.add(3317) ifl.add(1337) test.AssertEquals(t, ifl.len(), 3) - test.AssertEquals(t, ifl.min(), uint64(1337)) + test.AssertEquals(t, ifl.min(), int64(1337)) ifl.remove(3317) ifl.remove(1337) ifl.remove(7341) test.AssertEquals(t, ifl.len(), 0) - test.AssertEquals(t, ifl.min(), uint64(0)) + test.AssertEquals(t, ifl.min(), int64(0)) } diff --git a/third-party/github.com/letsencrypt/boulder/cmd/sfe/main.go b/third-party/github.com/letsencrypt/boulder/cmd/sfe/main.go new file mode 100644 index 000000000..aeb8e8b9d --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/cmd/sfe/main.go @@ -0,0 +1,139 @@ +package notmain + +import ( + "context" + "flag" + "net/http" + "os" + + "github.com/letsencrypt/boulder/cmd" + "github.com/letsencrypt/boulder/config" + "github.com/letsencrypt/boulder/features" + bgrpc "github.com/letsencrypt/boulder/grpc" + rapb "github.com/letsencrypt/boulder/ra/proto" + sapb "github.com/letsencrypt/boulder/sa/proto" + "github.com/letsencrypt/boulder/sfe" + "github.com/letsencrypt/boulder/web" +) + +type Config struct { + SFE struct { + DebugAddr string `validate:"omitempty,hostname_port"` + + // ListenAddress is the address:port on which to listen for incoming + // HTTP requests. Defaults to ":80". + ListenAddress string `validate:"omitempty,hostname_port"` + + // Timeout is the per-request overall timeout. This should be slightly + // 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 + + TLS cmd.TLSConfig + + RAService *cmd.GRPCClientConfig + SAService *cmd.GRPCClientConfig + + // UnpauseHMACKey validates incoming JWT signatures at the unpause + // endpoint. This key must be the same as the one configured for all + // WFEs. This field is required to enable the pausing feature. + UnpauseHMACKey cmd.HMACKeyConfig + + Features features.Config + } + + Syslog cmd.SyslogConfig + OpenTelemetry cmd.OpenTelemetryConfig + + // OpenTelemetryHTTPConfig configures tracing on incoming HTTP requests + OpenTelemetryHTTPConfig cmd.OpenTelemetryHTTPConfig +} + +func main() { + listenAddr := flag.String("addr", "", "HTTP listen address override") + debugAddr := flag.String("debug-addr", "", "Debug server address override") + configFile := flag.String("config", "", "File path to the configuration file for this service") + 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.SFE.Features) + + if *listenAddr != "" { + c.SFE.ListenAddress = *listenAddr + } + if c.SFE.ListenAddress == "" { + cmd.Fail("HTTP listen address is not configured") + } + if *debugAddr != "" { + c.SFE.DebugAddr = *debugAddr + } + + stats, logger, oTelShutdown := cmd.StatsAndLogging(c.Syslog, c.OpenTelemetry, c.SFE.DebugAddr) + logger.Info(cmd.VersionString()) + + clk := cmd.Clock() + + unpauseHMACKey, err := c.SFE.UnpauseHMACKey.Load() + cmd.FailOnError(err, "Failed to load unpauseHMACKey") + + tlsConfig, err := c.SFE.TLS.Load(stats) + cmd.FailOnError(err, "TLS config") + + raConn, err := bgrpc.ClientSetup(c.SFE.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.SFE.SAService, tlsConfig, stats, clk) + cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to SA") + sac := sapb.NewStorageAuthorityReadOnlyClient(saConn) + + sfei, err := sfe.NewSelfServiceFrontEndImpl( + stats, + clk, + logger, + c.SFE.Timeout.Duration, + rac, + sac, + unpauseHMACKey, + ) + cmd.FailOnError(err, "Unable to create SFE") + + logger.Infof("Server running, listening on %s....", c.SFE.ListenAddress) + handler := sfei.Handler(stats, c.OpenTelemetryHTTPConfig.Options()...) + + srv := web.NewServer(c.SFE.ListenAddress, handler, logger) + go func() { + err := srv.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + cmd.FailOnError(err, "Running HTTP server") + } + }() + + // When main is ready to exit (because it has received a shutdown signal), + // gracefully shutdown the servers. Calling these shutdown functions causes + // ListenAndServe() and ListenAndServeTLS() to immediately return, then waits + // for any lingering connection-handling goroutines to finish their work. + defer func() { + ctx, cancel := context.WithTimeout(context.Background(), c.SFE.ShutdownStopTimeout.Duration) + defer cancel() + _ = srv.Shutdown(ctx) + oTelShutdown(ctx) + }() + + cmd.WaitForSignal() +} + +func init() { + cmd.RegisterCommand("sfe", main, &cmd.ConfigValidator{Config: &Config{}}) +} diff --git a/third-party/github.com/letsencrypt/boulder/cmd/shell.go b/third-party/github.com/letsencrypt/boulder/cmd/shell.go index 0934614a3..60732f256 100644 --- a/third-party/github.com/letsencrypt/boulder/cmd/shell.go +++ b/third-party/github.com/letsencrypt/boulder/cmd/shell.go @@ -31,9 +31,10 @@ import ( "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/sdk/resource" "go.opentelemetry.io/otel/sdk/trace" - semconv "go.opentelemetry.io/otel/semconv/v1.25.0" + semconv "go.opentelemetry.io/otel/semconv/v1.30.0" "google.golang.org/grpc/grpclog" + "github.com/letsencrypt/boulder/config" "github.com/letsencrypt/boulder/core" blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/strictyaml" @@ -221,7 +222,7 @@ func NewLogger(logConf SyslogConfig) blog.Logger { // Boulder's conception of time. go func() { for { - time.Sleep(time.Minute) + time.Sleep(time.Hour) logger.Info(fmt.Sprintf("time=%s", time.Now().Format(time.RFC3339Nano))) } }() @@ -260,6 +261,12 @@ func newVersionCollector() prometheus.Collector { func newStatsRegistry(addr string, logger blog.Logger) prometheus.Registerer { registry := prometheus.NewRegistry() + + if addr == "" { + logger.Info("No debug listen address specified") + return registry + } + registry.MustRegister(collectors.NewGoCollector()) registry.MustRegister(collectors.NewProcessCollector( collectors.ProcessCollectorOpts{})) @@ -286,10 +293,6 @@ func newStatsRegistry(addr string, logger blog.Logger) prometheus.Registerer { ErrorLog: promLogger{logger}, })) - if addr == "" { - logger.Err("Debug listen address is not configured") - os.Exit(1) - } logger.Infof("Debug server listening on %s", addr) server := http.Server{ @@ -313,20 +316,15 @@ func NewOpenTelemetry(config OpenTelemetryConfig, logger blog.Logger) func(ctx c otel.SetLogger(stdr.New(logOutput{logger})) otel.SetErrorHandler(otel.ErrorHandlerFunc(func(err error) { logger.Errf("OpenTelemetry error: %v", err) })) - r, err := resource.Merge( - resource.Default(), - resource.NewWithAttributes( - semconv.SchemaURL, - semconv.ServiceNameKey.String(core.Command()), - semconv.ServiceVersionKey.String(core.GetBuildID()), - ), + resources := resource.NewWithAttributes( + semconv.SchemaURL, + semconv.ServiceName(core.Command()), + semconv.ServiceVersion(core.GetBuildID()), + semconv.ProcessPID(os.Getpid()), ) - if err != nil { - FailOnError(err, "Could not create OpenTelemetry resource") - } opts := []trace.TracerProviderOption{ - trace.WithResource(r), + trace.WithResource(resources), // Use a ParentBased sampler to respect the sample decisions on incoming // traces, and TraceIDRatioBased to randomly sample new traces. trace.WithSampler(trace.ParentBased(trace.TraceIDRatioBased(config.SampleRatio))), @@ -455,6 +453,9 @@ func ValidateJSONConfig(cv *ConfigValidator, in io.Reader) error { } } + // Register custom types for use with existing validation tags. + validate.RegisterCustomTypeFunc(config.DurationCustomTypeFunc, config.Duration{}) + err := decodeJSONStrict(in, cv.Config) if err != nil { return err @@ -497,6 +498,9 @@ func ValidateYAMLConfig(cv *ConfigValidator, in io.Reader) error { } } + // Register custom types for use with existing validation tags. + validate.RegisterCustomTypeFunc(config.DurationCustomTypeFunc, config.Duration{}) + inBytes, err := io.ReadAll(in) if err != nil { return err diff --git a/third-party/github.com/letsencrypt/boulder/cmd/shell_test.go b/third-party/github.com/letsencrypt/boulder/cmd/shell_test.go index debafd54e..80ac0dae6 100644 --- a/third-party/github.com/letsencrypt/boulder/cmd/shell_test.go +++ b/third-party/github.com/letsencrypt/boulder/cmd/shell_test.go @@ -11,32 +11,36 @@ import ( "testing" "time" + "github.com/prometheus/client_golang/prometheus" + + "github.com/letsencrypt/boulder/config" "github.com/letsencrypt/boulder/core" blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/test" - "github.com/prometheus/client_golang/prometheus" ) var ( validPAConfig = []byte(`{ "dbConnect": "dummyDBConnect", "enforcePolicyWhitelist": false, - "challenges": { "http-01": true } + "challenges": { "http-01": true }, + "identifiers": { "dns": true, "ip": true } }`) invalidPAConfig = []byte(`{ "dbConnect": "dummyDBConnect", "enforcePolicyWhitelist": false, - "challenges": { "nonsense": true } + "challenges": { "nonsense": true }, + "identifiers": { "openpgp": true } }`) - noChallengesPAConfig = []byte(`{ + noChallengesIdentsPAConfig = []byte(`{ "dbConnect": "dummyDBConnect", "enforcePolicyWhitelist": false }`) - - emptyChallengesPAConfig = []byte(`{ + emptyChallengesIdentsPAConfig = []byte(`{ "dbConnect": "dummyDBConnect", "enforcePolicyWhitelist": false, - "challenges": {} + "challenges": {}, + "identifiers": {} }`) ) @@ -45,21 +49,25 @@ func TestPAConfigUnmarshal(t *testing.T) { err := json.Unmarshal(validPAConfig, &pc1) test.AssertNotError(t, err, "Failed to unmarshal PAConfig") test.AssertNotError(t, pc1.CheckChallenges(), "Flagged valid challenges as bad") + test.AssertNotError(t, pc1.CheckIdentifiers(), "Flagged valid identifiers as bad") var pc2 PAConfig err = json.Unmarshal(invalidPAConfig, &pc2) test.AssertNotError(t, err, "Failed to unmarshal PAConfig") test.AssertError(t, pc2.CheckChallenges(), "Considered invalid challenges as good") + test.AssertError(t, pc2.CheckIdentifiers(), "Considered invalid identifiers as good") var pc3 PAConfig - err = json.Unmarshal(noChallengesPAConfig, &pc3) + err = json.Unmarshal(noChallengesIdentsPAConfig, &pc3) test.AssertNotError(t, err, "Failed to unmarshal PAConfig") test.AssertError(t, pc3.CheckChallenges(), "Disallow empty challenges map") + test.AssertNotError(t, pc3.CheckIdentifiers(), "Disallowed empty identifiers map") var pc4 PAConfig - err = json.Unmarshal(emptyChallengesPAConfig, &pc4) + err = json.Unmarshal(emptyChallengesIdentsPAConfig, &pc4) test.AssertNotError(t, err, "Failed to unmarshal PAConfig") test.AssertError(t, pc4.CheckChallenges(), "Disallow empty challenges map") + test.AssertNotError(t, pc4.CheckIdentifiers(), "Disallowed empty identifiers map") } func TestMysqlLogger(t *testing.T) { @@ -125,16 +133,13 @@ func TestReadConfigFile(t *testing.T) { test.AssertError(t, err, "ReadConfigFile('') did not error") type config struct { - NotifyMailer struct { - DB DBConfig - SMTPConfig - } - Syslog SyslogConfig + GRPC *GRPCClientConfig + TLS *TLSConfig } var c config - err = ReadConfigFile("../test/config/notify-mailer.json", &c) - test.AssertNotError(t, err, "ReadConfigFile(../test/config/notify-mailer.json) errored") - test.AssertEquals(t, c.NotifyMailer.SMTPConfig.Server, "localhost") + err = ReadConfigFile("../test/config/health-checker.json", &c) + test.AssertNotError(t, err, "ReadConfigFile(../test/config/health-checker.json) errored") + test.AssertEquals(t, c.GRPC.Timeout.Duration, 1*time.Second) } func TestLogWriter(t *testing.T) { @@ -196,9 +201,11 @@ func loadConfigFile(t *testing.T, path string) *os.File { func TestFailedConfigValidation(t *testing.T) { type FooConfig struct { - VitalValue string `yaml:"vitalValue" validate:"required"` - VoluntarilyVoid string `yaml:"voluntarilyVoid"` - VisciouslyVetted string `yaml:"visciouslyVetted" validate:"omitempty,endswith=baz"` + VitalValue string `yaml:"vitalValue" validate:"required"` + VoluntarilyVoid string `yaml:"voluntarilyVoid"` + VisciouslyVetted string `yaml:"visciouslyVetted" validate:"omitempty,endswith=baz"` + VolatileVagary config.Duration `yaml:"volatileVagary" validate:"required,lte=120s"` + VernalVeil config.Duration `yaml:"vernalVeil" validate:"required"` } // Violates 'endswith' tag JSON. @@ -228,6 +235,34 @@ func TestFailedConfigValidation(t *testing.T) { err = ValidateYAMLConfig(&ConfigValidator{&FooConfig{}, nil}, cf) test.AssertError(t, err, "Expected validation error") test.AssertContains(t, err.Error(), "'required'") + + // Violates 'lte' tag JSON for config.Duration type. + cf = loadConfigFile(t, "testdata/3_configDuration_too_darn_big.json") + defer cf.Close() + err = ValidateJSONConfig(&ConfigValidator{&FooConfig{}, nil}, cf) + test.AssertError(t, err, "Expected validation error") + test.AssertContains(t, err.Error(), "'lte'") + + // Violates 'lte' tag JSON for config.Duration type. + cf = loadConfigFile(t, "testdata/3_configDuration_too_darn_big.json") + defer cf.Close() + err = ValidateJSONConfig(&ConfigValidator{&FooConfig{}, nil}, cf) + test.AssertError(t, err, "Expected validation error") + test.AssertContains(t, err.Error(), "'lte'") + + // Incorrect value for the config.Duration type. + cf = loadConfigFile(t, "testdata/4_incorrect_data_for_type.json") + defer cf.Close() + err = ValidateJSONConfig(&ConfigValidator{&FooConfig{}, nil}, cf) + test.AssertError(t, err, "Expected error") + test.AssertContains(t, err.Error(), "missing unit in duration") + + // Incorrect value for the config.Duration type. + cf = loadConfigFile(t, "testdata/4_incorrect_data_for_type.yaml") + defer cf.Close() + err = ValidateYAMLConfig(&ConfigValidator{&FooConfig{}, nil}, cf) + test.AssertError(t, err, "Expected error") + test.AssertContains(t, err.Error(), "missing unit in duration") } func TestFailExit(t *testing.T) { @@ -241,9 +276,6 @@ func TestFailExit(t *testing.T) { return } - // gosec points out that os.Args[0] is tainted, but we only run this as a test - // so we are not worried about it containing an untrusted value. - //nolint:gosec cmd := exec.Command(os.Args[0], "-test.run=TestFailExit") cmd.Env = append(os.Environ(), "TIME_TO_DIE=1") output, err := cmd.CombinedOutput() @@ -256,7 +288,7 @@ func TestFailExit(t *testing.T) { func testPanicStackTraceHelper() { var x *int - *x = 1 //nolint:govet + *x = 1 //nolint: govet // Purposeful nil pointer dereference to trigger a panic } func TestPanicStackTrace(t *testing.T) { @@ -270,9 +302,6 @@ func TestPanicStackTrace(t *testing.T) { return } - // gosec points out that os.Args[0] is tainted, but we only run this as a test - // so we are not worried about it containing an untrusted value. - //nolint:gosec cmd := exec.Command(os.Args[0], "-test.run=TestPanicStackTrace") cmd.Env = append(os.Environ(), "AT_THE_DISCO=1") output, err := cmd.CombinedOutput() diff --git a/third-party/github.com/letsencrypt/boulder/cmd/testdata/3_configDuration_too_darn_big.json b/third-party/github.com/letsencrypt/boulder/cmd/testdata/3_configDuration_too_darn_big.json new file mode 100644 index 000000000..0b108edb7 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/cmd/testdata/3_configDuration_too_darn_big.json @@ -0,0 +1,6 @@ +{ + "vitalValue": "Gotcha", + "voluntarilyVoid": "Not used", + "visciouslyVetted": "Whateverbaz", + "volatileVagary": "121s" +} diff --git a/third-party/github.com/letsencrypt/boulder/cmd/testdata/4_incorrect_data_for_type.json b/third-party/github.com/letsencrypt/boulder/cmd/testdata/4_incorrect_data_for_type.json new file mode 100644 index 000000000..5805d59ee --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/cmd/testdata/4_incorrect_data_for_type.json @@ -0,0 +1,7 @@ +{ + "vitalValue": "Gotcha", + "voluntarilyVoid": "Not used", + "visciouslyVetted": "Whateverbaz", + "volatileVagary": "120s", + "vernalVeil": "60" +} diff --git a/third-party/github.com/letsencrypt/boulder/cmd/testdata/4_incorrect_data_for_type.yaml b/third-party/github.com/letsencrypt/boulder/cmd/testdata/4_incorrect_data_for_type.yaml new file mode 100644 index 000000000..02093be82 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/cmd/testdata/4_incorrect_data_for_type.yaml @@ -0,0 +1,5 @@ +vitalValue: "Gotcha" +voluntarilyVoid: "Not used" +visciouslyVetted: "Whateverbaz" +volatileVagary: "120s" +vernalVeil: "60" diff --git a/third-party/github.com/letsencrypt/boulder/config/duration.go b/third-party/github.com/letsencrypt/boulder/config/duration.go index c97eeb486..90cb2277d 100644 --- a/third-party/github.com/letsencrypt/boulder/config/duration.go +++ b/third-party/github.com/letsencrypt/boulder/config/duration.go @@ -3,15 +3,27 @@ package config import ( "encoding/json" "errors" + "reflect" "time" ) -// Duration is just an alias for time.Duration that allows -// serialization to YAML as well as JSON. +// Duration is custom type embedding a time.Duration which allows defining +// methods such as serialization to YAML or JSON. type Duration struct { time.Duration `validate:"required"` } +// DurationCustomTypeFunc enables registration of our custom config.Duration +// type as a time.Duration and performing validation on the configured value +// using the standard suite of validation functions. +func DurationCustomTypeFunc(field reflect.Value) interface{} { + if c, ok := field.Interface().(Duration); ok { + return c.Duration + } + + return reflect.Invalid +} + // ErrDurationMustBeString is returned when a non-string value is // presented to be deserialized as a ConfigDuration var ErrDurationMustBeString = errors.New("cannot JSON unmarshal something other than a string into a ConfigDuration") diff --git a/third-party/github.com/letsencrypt/boulder/core/interfaces.go b/third-party/github.com/letsencrypt/boulder/core/interfaces.go index 59b55a3f4..1b3a1eedd 100644 --- a/third-party/github.com/letsencrypt/boulder/core/interfaces.go +++ b/third-party/github.com/letsencrypt/boulder/core/interfaces.go @@ -7,8 +7,8 @@ import ( // PolicyAuthority defines the public interface for the Boulder PA // TODO(#5891): Move this interface to a more appropriate location. type PolicyAuthority interface { - WillingToIssue([]string) error - ChallengesFor(identifier.ACMEIdentifier) ([]Challenge, error) + WillingToIssue(identifier.ACMEIdentifiers) error + ChallengeTypesFor(identifier.ACMEIdentifier) ([]AcmeChallenge, error) ChallengeTypeEnabled(AcmeChallenge) bool - CheckAuthz(*Authorization) error + CheckAuthzChallenges(*Authorization) error } diff --git a/third-party/github.com/letsencrypt/boulder/core/objects.go b/third-party/github.com/letsencrypt/boulder/core/objects.go index c01f551ab..474d0bcba 100644 --- a/third-party/github.com/letsencrypt/boulder/core/objects.go +++ b/third-party/github.com/letsencrypt/boulder/core/objects.go @@ -6,7 +6,7 @@ import ( "encoding/json" "fmt" "hash/fnv" - "net" + "net/netip" "strings" "time" @@ -68,7 +68,7 @@ func (c AcmeChallenge) IsValid() bool { } } -// OCSPStatus defines the state of OCSP for a domain +// OCSPStatus defines the state of OCSP for a certificate type OCSPStatus string // These status are the states of OCSP @@ -98,7 +98,7 @@ type RawCertificateRequest struct { // to account keys. type Registration struct { // Unique identifier - ID int64 `json:"id,omitempty" db:"id"` + ID int64 `json:"id,omitempty"` // Account key to which the details are attached Key *jose.JSONWebKey `json:"key"` @@ -109,9 +109,6 @@ type Registration struct { // Agreement with terms of service Agreement string `json:"agreement,omitempty"` - // InitialIP is the IP address from which the registration was created - InitialIP net.IP `json:"initialIp"` - // CreatedAt is the time the registration was created. CreatedAt *time.Time `json:"createdAt,omitempty"` @@ -125,10 +122,13 @@ type ValidationRecord struct { URL string `json:"url,omitempty"` // Shared - Hostname string `json:"hostname,omitempty"` - Port string `json:"port,omitempty"` - AddressesResolved []net.IP `json:"addressesResolved,omitempty"` - AddressUsed net.IP `json:"addressUsed,omitempty"` + // + // Hostname can hold either a DNS name or an IP address. + Hostname string `json:"hostname,omitempty"` + Port string `json:"port,omitempty"` + AddressesResolved []netip.Addr `json:"addressesResolved,omitempty"` + AddressUsed netip.Addr `json:"addressUsed,omitempty"` + // AddressesTried contains a list of addresses tried before the `AddressUsed`. // Presently this will only ever be one IP from `AddressesResolved` since the // only retry is in the case of a v6 failure with one v4 fallback. E.g. if @@ -143,18 +143,12 @@ type ValidationRecord struct { // AddressesTried: [ ::1 ], // ... // } - AddressesTried []net.IP `json:"addressesTried,omitempty"` + AddressesTried []netip.Addr `json:"addressesTried,omitempty"` + // ResolverAddrs is the host:port of the DNS resolver(s) that fulfilled the // lookup for AddressUsed. During recursive A and AAAA lookups, a record may // instead look like A:host:port or AAAA:host:port ResolverAddrs []string `json:"resolverAddrs,omitempty"` - // UsedRSAKEX is a *temporary* addition to the validation record, so we can - // see how many servers that we reach out to during HTTP-01 and TLS-ALPN-01 - // validation are only willing to negotiate RSA key exchange mechanisms. The - // field is not included in the serialized json to avoid cluttering the - // database and log lines. - // TODO(#7321): Remove this when we have collected sufficient data. - UsedRSAKEX bool `json:"-"` } // Challenge is an aggregate of all data needed for any challenges. @@ -184,14 +178,6 @@ type Challenge struct { // by all current challenges (http-01, tls-alpn-01, and dns-01). Token string `json:"token,omitempty"` - // ProvidedKeyAuthorization used to carry the expected key authorization from - // the RA to the VA. However, since this field is never presented to the user - // via the ACME API, it should not be on this type. - // - // Deprecated: use vapb.PerformValidationRequest.ExpectedKeyAuthorization instead. - // TODO(#7514): Remove this. - ProvidedKeyAuthorization string `json:"keyAuthorization,omitempty"` - // Contains information about URLs used or redirected to and IPs resolved and // used ValidationRecord []ValidationRecord `json:"validationRecord,omitempty"` @@ -215,7 +201,7 @@ func (ch Challenge) ExpectedKeyAuthorization(key *jose.JSONWebKey) (string, erro // RecordsSane checks the sanity of a ValidationRecord object before sending it // back to the RA to be stored. func (ch Challenge) RecordsSane() bool { - if ch.ValidationRecord == nil || len(ch.ValidationRecord) == 0 { + if len(ch.ValidationRecord) == 0 { return false } @@ -224,7 +210,7 @@ func (ch Challenge) RecordsSane() bool { for _, rec := range ch.ValidationRecord { // TODO(#7140): Add a check for ResolverAddress == "" only after the // core.proto change has been deployed. - if rec.URL == "" || rec.Hostname == "" || rec.Port == "" || rec.AddressUsed == nil || + if rec.URL == "" || rec.Hostname == "" || rec.Port == "" || (rec.AddressUsed == netip.Addr{}) || len(rec.AddressesResolved) == 0 { return false } @@ -239,7 +225,7 @@ func (ch Challenge) RecordsSane() bool { // TODO(#7140): Add a check for ResolverAddress == "" only after the // core.proto change has been deployed. if ch.ValidationRecord[0].Hostname == "" || ch.ValidationRecord[0].Port == "" || - ch.ValidationRecord[0].AddressUsed == nil || len(ch.ValidationRecord[0].AddressesResolved) == 0 { + (ch.ValidationRecord[0].AddressUsed == netip.Addr{}) || len(ch.ValidationRecord[0].AddressesResolved) == 0 { return false } case ChallengeTypeDNS01: @@ -285,30 +271,30 @@ func (ch Challenge) StringID() string { return base64.RawURLEncoding.EncodeToString(h.Sum(nil)[0:4]) } -// Authorization represents the authorization of an account key holder -// to act on behalf of a domain. This struct is intended to be used both -// internally and for JSON marshaling on the wire. Any fields that should be -// suppressed on the wire (e.g., ID, regID) must be made empty before marshaling. +// Authorization represents the authorization of an account key holder to act on +// behalf of an identifier. This struct is intended to be used both internally +// and for JSON marshaling on the wire. Any fields that should be suppressed on +// the wire (e.g., ID, regID) must be made empty before marshaling. type Authorization struct { // An identifier for this authorization, unique across // authorizations and certificates within this instance. - ID string `json:"id,omitempty" db:"id"` + ID string `json:"-"` // The identifier for which authorization is being given - Identifier identifier.ACMEIdentifier `json:"identifier,omitempty" db:"identifier"` + Identifier identifier.ACMEIdentifier `json:"identifier,omitempty"` // The registration ID associated with the authorization - RegistrationID int64 `json:"regId,omitempty" db:"registrationID"` + RegistrationID int64 `json:"-"` // The status of the validation of this authorization - Status AcmeStatus `json:"status,omitempty" db:"status"` + Status AcmeStatus `json:"status,omitempty"` // The date after which this authorization will be no // longer be considered valid. Note: a certificate may be issued even on the // last day of an authorization's lifetime. The last day for which someone can // hold a valid certificate based on an authorization is authorization // lifetime + certificate lifetime. - Expires *time.Time `json:"expires,omitempty" db:"expires"` + Expires *time.Time `json:"expires,omitempty"` // An array of challenges objects used to validate the // applicant's control of the identifier. For authorizations @@ -318,7 +304,7 @@ type Authorization struct { // // There should only ever be one challenge of each type in this // slice and the order of these challenges may not be predictable. - Challenges []Challenge `json:"challenges,omitempty" db:"-"` + Challenges []Challenge `json:"challenges,omitempty"` // https://datatracker.ietf.org/doc/html/rfc8555#page-29 // @@ -332,7 +318,12 @@ type Authorization struct { // the identifier stored in the database. Unlike the identifier returned // as part of the authorization, the identifier we store in the database // can contain an asterisk. - Wildcard bool `json:"wildcard,omitempty" db:"-"` + Wildcard bool `json:"wildcard,omitempty"` + + // CertificateProfileName is the name of the profile associated with the + // order that first resulted in the creation of this authorization. Omitted + // from API responses. + CertificateProfileName string `json:"-"` } // FindChallengeByStringID will look for a challenge matching the given ID inside @@ -352,14 +343,14 @@ func (authz *Authorization) FindChallengeByStringID(id string) int { // challenge is valid. func (authz *Authorization) SolvedBy() (AcmeChallenge, error) { if len(authz.Challenges) == 0 { - return "", fmt.Errorf("Authorization has no challenges") + return "", fmt.Errorf("authorization has no challenges") } for _, chal := range authz.Challenges { if chal.Status == StatusValid { return chal.Type, nil } } - return "", fmt.Errorf("Authorization not solved by any challenge") + return "", fmt.Errorf("authorization not solved by any challenge") } // JSONBuffer fields get encoded and decoded JOSE-style, in base64url encoding @@ -471,20 +462,26 @@ func (window SuggestedWindow) IsWithin(now time.Time) bool { // endpoint specified in draft-aaron-ari. type RenewalInfo struct { SuggestedWindow SuggestedWindow `json:"suggestedWindow"` + ExplanationURL string `json:"explanationURL,omitempty"` } // RenewalInfoSimple constructs a `RenewalInfo` object and suggested window // using a very simple renewal calculation: calculate a point 2/3rds of the way -// through the validity period, then give a 2-day window around that. Both the -// `issued` and `expires` timestamps are expected to be UTC. +// through the validity period (or halfway through, for short-lived certs), then +// give a 2%-of-validity wide window around that. Both the `issued` and +// `expires` timestamps are expected to be UTC. func RenewalInfoSimple(issued time.Time, expires time.Time) RenewalInfo { validity := expires.Add(time.Second).Sub(issued) renewalOffset := validity / time.Duration(3) + if validity < 10*24*time.Hour { + renewalOffset = validity / time.Duration(2) + } idealRenewal := expires.Add(-renewalOffset) + margin := validity / time.Duration(100) return RenewalInfo{ SuggestedWindow: SuggestedWindow{ - Start: idealRenewal.Add(-24 * time.Hour), - End: idealRenewal.Add(24 * time.Hour), + Start: idealRenewal.Add(-1 * margin).Truncate(time.Second), + End: idealRenewal.Add(margin).Truncate(time.Second), }, } } @@ -493,13 +490,15 @@ func RenewalInfoSimple(issued time.Time, expires time.Time) RenewalInfo { // window in the past. Per the draft-ietf-acme-ari-01 spec, clients should // attempt to renew immediately if the suggested window is in the past. The // passed `now` is assumed to be a timestamp representing the current moment in -// time. -func RenewalInfoImmediate(now time.Time) RenewalInfo { +// time. The `explanationURL` is an optional URL that the subscriber can use to +// learn more about why the renewal is suggested. +func RenewalInfoImmediate(now time.Time, explanationURL string) RenewalInfo { oneHourAgo := now.Add(-1 * time.Hour) return RenewalInfo{ SuggestedWindow: SuggestedWindow{ - Start: oneHourAgo, - End: oneHourAgo.Add(time.Minute * 30), + Start: oneHourAgo.Truncate(time.Second), + End: oneHourAgo.Add(time.Minute * 30).Truncate(time.Second), }, + ExplanationURL: explanationURL, } } diff --git a/third-party/github.com/letsencrypt/boulder/core/objects_test.go b/third-party/github.com/letsencrypt/boulder/core/objects_test.go index 9aba3b2fd..2d3194e63 100644 --- a/third-party/github.com/letsencrypt/boulder/core/objects_test.go +++ b/third-party/github.com/letsencrypt/boulder/core/objects_test.go @@ -4,7 +4,7 @@ import ( "crypto/rsa" "encoding/json" "math/big" - "net" + "net/netip" "testing" "time" @@ -39,8 +39,8 @@ func TestRecordSanityCheckOnUnsupportedChallengeType(t *testing.T) { URL: "http://localhost/test", Hostname: "localhost", Port: "80", - AddressesResolved: []net.IP{{127, 0, 0, 1}}, - AddressUsed: net.IP{127, 0, 0, 1}, + AddressesResolved: []netip.Addr{netip.MustParseAddr("127.0.0.1")}, + AddressUsed: netip.MustParseAddr("127.0.0.1"), ResolverAddrs: []string{"eastUnboundAndDown"}, }, } @@ -100,7 +100,7 @@ func TestAuthorizationSolvedBy(t *testing.T) { { Name: "No challenges", Authz: Authorization{}, - ExpectedError: "Authorization has no challenges", + ExpectedError: "authorization has no challenges", }, // An authz with all non-valid challenges should return nil { @@ -108,7 +108,7 @@ func TestAuthorizationSolvedBy(t *testing.T) { Authz: Authorization{ Challenges: []Challenge{HTTPChallenge01(""), DNSChallenge01("")}, }, - ExpectedError: "Authorization not solved by any challenge", + ExpectedError: "authorization not solved by any challenge", }, // An authz with one valid HTTP01 challenge amongst other challenges should // return the HTTP01 challenge diff --git a/third-party/github.com/letsencrypt/boulder/core/proto/core.pb.go b/third-party/github.com/letsencrypt/boulder/core/proto/core.pb.go index 1f926178e..f19e6df93 100644 --- a/third-party/github.com/letsencrypt/boulder/core/proto/core.pb.go +++ b/third-party/github.com/letsencrypt/boulder/core/proto/core.pb.go @@ -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: core.proto @@ -12,6 +12,7 @@ import ( timestamppb "google.golang.org/protobuf/types/known/timestamppb" reflect "reflect" sync "sync" + unsafe "unsafe" ) const ( @@ -21,31 +22,80 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) -type Challenge struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache +type Identifier struct { + state protoimpl.MessageState `protogen:"open.v1"` + Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` + Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} - // Next unused field number: 13 - Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` - Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"` - Status string `protobuf:"bytes,6,opt,name=status,proto3" json:"status,omitempty"` - Uri string `protobuf:"bytes,9,opt,name=uri,proto3" json:"uri,omitempty"` - Token string `protobuf:"bytes,3,opt,name=token,proto3" json:"token,omitempty"` - // TODO(#7514): Remove this. - KeyAuthorization string `protobuf:"bytes,5,opt,name=keyAuthorization,proto3" json:"keyAuthorization,omitempty"` - Validationrecords []*ValidationRecord `protobuf:"bytes,10,rep,name=validationrecords,proto3" json:"validationrecords,omitempty"` - Error *ProblemDetails `protobuf:"bytes,7,opt,name=error,proto3" json:"error,omitempty"` - Validated *timestamppb.Timestamp `protobuf:"bytes,12,opt,name=validated,proto3" json:"validated,omitempty"` +func (x *Identifier) Reset() { + *x = Identifier{} + mi := &file_core_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Identifier) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Identifier) ProtoMessage() {} + +func (x *Identifier) ProtoReflect() protoreflect.Message { + mi := &file_core_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Identifier.ProtoReflect.Descriptor instead. +func (*Identifier) Descriptor() ([]byte, []int) { + return file_core_proto_rawDescGZIP(), []int{0} +} + +func (x *Identifier) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *Identifier) GetValue() string { + if x != nil { + return x.Value + } + return "" +} + +type Challenge struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + // Fields specified by RFC 8555, Section 8. + Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"` + Url string `protobuf:"bytes,9,opt,name=url,proto3" json:"url,omitempty"` + Status string `protobuf:"bytes,6,opt,name=status,proto3" json:"status,omitempty"` + Validated *timestamppb.Timestamp `protobuf:"bytes,12,opt,name=validated,proto3" json:"validated,omitempty"` + Error *ProblemDetails `protobuf:"bytes,7,opt,name=error,proto3" json:"error,omitempty"` + // Fields specified by individual validation methods. + Token string `protobuf:"bytes,3,opt,name=token,proto3" json:"token,omitempty"` + // Additional fields for our own record keeping. + Validationrecords []*ValidationRecord `protobuf:"bytes,10,rep,name=validationrecords,proto3" json:"validationrecords,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Challenge) Reset() { *x = Challenge{} - if protoimpl.UnsafeEnabled { - mi := &file_core_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_core_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Challenge) String() string { @@ -55,8 +105,8 @@ func (x *Challenge) String() string { func (*Challenge) ProtoMessage() {} func (x *Challenge) ProtoReflect() protoreflect.Message { - mi := &file_core_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_core_proto_msgTypes[1] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -68,7 +118,7 @@ func (x *Challenge) ProtoReflect() protoreflect.Message { // Deprecated: Use Challenge.ProtoReflect.Descriptor instead. func (*Challenge) Descriptor() ([]byte, []int) { - return file_core_proto_rawDescGZIP(), []int{0} + return file_core_proto_rawDescGZIP(), []int{1} } func (x *Challenge) GetId() int64 { @@ -85,6 +135,13 @@ func (x *Challenge) GetType() string { return "" } +func (x *Challenge) GetUrl() string { + if x != nil { + return x.Url + } + return "" +} + func (x *Challenge) GetStatus() string { if x != nil { return x.Status @@ -92,30 +149,9 @@ func (x *Challenge) GetStatus() string { return "" } -func (x *Challenge) GetUri() string { +func (x *Challenge) GetValidated() *timestamppb.Timestamp { if x != nil { - return x.Uri - } - return "" -} - -func (x *Challenge) GetToken() string { - if x != nil { - return x.Token - } - return "" -} - -func (x *Challenge) GetKeyAuthorization() string { - if x != nil { - return x.KeyAuthorization - } - return "" -} - -func (x *Challenge) GetValidationrecords() []*ValidationRecord { - if x != nil { - return x.Validationrecords + return x.Validated } return nil } @@ -127,39 +163,43 @@ func (x *Challenge) GetError() *ProblemDetails { return nil } -func (x *Challenge) GetValidated() *timestamppb.Timestamp { +func (x *Challenge) GetToken() string { if x != nil { - return x.Validated + return x.Token + } + return "" +} + +func (x *Challenge) GetValidationrecords() []*ValidationRecord { + if x != nil { + return x.Validationrecords } return nil } type ValidationRecord struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // Next unused field number: 9 Hostname string `protobuf:"bytes,1,opt,name=hostname,proto3" json:"hostname,omitempty"` Port string `protobuf:"bytes,2,opt,name=port,proto3" json:"port,omitempty"` - AddressesResolved [][]byte `protobuf:"bytes,3,rep,name=addressesResolved,proto3" json:"addressesResolved,omitempty"` // net.IP.MarshalText() - AddressUsed []byte `protobuf:"bytes,4,opt,name=addressUsed,proto3" json:"addressUsed,omitempty"` // net.IP.MarshalText() + AddressesResolved [][]byte `protobuf:"bytes,3,rep,name=addressesResolved,proto3" json:"addressesResolved,omitempty"` // netip.Addr.MarshalText() + AddressUsed []byte `protobuf:"bytes,4,opt,name=addressUsed,proto3" json:"addressUsed,omitempty"` // netip.Addr.MarshalText() Authorities []string `protobuf:"bytes,5,rep,name=authorities,proto3" json:"authorities,omitempty"` Url string `protobuf:"bytes,6,opt,name=url,proto3" json:"url,omitempty"` // A list of addresses tried before the address used (see // core/objects.go and the comment on the ValidationRecord structure // definition for more information. - AddressesTried [][]byte `protobuf:"bytes,7,rep,name=addressesTried,proto3" json:"addressesTried,omitempty"` // net.IP.MarshalText() + AddressesTried [][]byte `protobuf:"bytes,7,rep,name=addressesTried,proto3" json:"addressesTried,omitempty"` // netip.Addr.MarshalText() ResolverAddrs []string `protobuf:"bytes,8,rep,name=resolverAddrs,proto3" json:"resolverAddrs,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ValidationRecord) Reset() { *x = ValidationRecord{} - if protoimpl.UnsafeEnabled { - mi := &file_core_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_core_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ValidationRecord) String() string { @@ -169,8 +209,8 @@ func (x *ValidationRecord) String() string { func (*ValidationRecord) ProtoMessage() {} func (x *ValidationRecord) ProtoReflect() protoreflect.Message { - mi := &file_core_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_core_proto_msgTypes[2] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -182,7 +222,7 @@ func (x *ValidationRecord) ProtoReflect() protoreflect.Message { // Deprecated: Use ValidationRecord.ProtoReflect.Descriptor instead. func (*ValidationRecord) Descriptor() ([]byte, []int) { - return file_core_proto_rawDescGZIP(), []int{1} + return file_core_proto_rawDescGZIP(), []int{2} } func (x *ValidationRecord) GetHostname() string { @@ -242,22 +282,19 @@ func (x *ValidationRecord) GetResolverAddrs() []string { } type ProblemDetails struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + ProblemType string `protobuf:"bytes,1,opt,name=problemType,proto3" json:"problemType,omitempty"` + Detail string `protobuf:"bytes,2,opt,name=detail,proto3" json:"detail,omitempty"` + HttpStatus int32 `protobuf:"varint,3,opt,name=httpStatus,proto3" json:"httpStatus,omitempty"` unknownFields protoimpl.UnknownFields - - ProblemType string `protobuf:"bytes,1,opt,name=problemType,proto3" json:"problemType,omitempty"` - Detail string `protobuf:"bytes,2,opt,name=detail,proto3" json:"detail,omitempty"` - HttpStatus int32 `protobuf:"varint,3,opt,name=httpStatus,proto3" json:"httpStatus,omitempty"` + sizeCache protoimpl.SizeCache } func (x *ProblemDetails) Reset() { *x = ProblemDetails{} - if protoimpl.UnsafeEnabled { - mi := &file_core_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_core_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ProblemDetails) String() string { @@ -267,8 +304,8 @@ func (x *ProblemDetails) String() string { func (*ProblemDetails) ProtoMessage() {} func (x *ProblemDetails) ProtoReflect() protoreflect.Message { - mi := &file_core_proto_msgTypes[2] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_core_proto_msgTypes[3] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -280,7 +317,7 @@ func (x *ProblemDetails) ProtoReflect() protoreflect.Message { // Deprecated: Use ProblemDetails.ProtoReflect.Descriptor instead. func (*ProblemDetails) Descriptor() ([]byte, []int) { - return file_core_proto_rawDescGZIP(), []int{2} + return file_core_proto_rawDescGZIP(), []int{3} } func (x *ProblemDetails) GetProblemType() string { @@ -305,10 +342,7 @@ func (x *ProblemDetails) GetHttpStatus() int32 { } type Certificate struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // Next unused field number: 9 RegistrationID int64 `protobuf:"varint,1,opt,name=registrationID,proto3" json:"registrationID,omitempty"` Serial string `protobuf:"bytes,2,opt,name=serial,proto3" json:"serial,omitempty"` @@ -316,15 +350,15 @@ type Certificate struct { Der []byte `protobuf:"bytes,4,opt,name=der,proto3" json:"der,omitempty"` Issued *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=issued,proto3" json:"issued,omitempty"` Expires *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=expires,proto3" json:"expires,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Certificate) Reset() { *x = Certificate{} - if protoimpl.UnsafeEnabled { - mi := &file_core_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_core_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Certificate) String() string { @@ -334,8 +368,8 @@ func (x *Certificate) String() string { func (*Certificate) ProtoMessage() {} func (x *Certificate) ProtoReflect() protoreflect.Message { - mi := &file_core_proto_msgTypes[3] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_core_proto_msgTypes[4] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -347,7 +381,7 @@ func (x *Certificate) ProtoReflect() protoreflect.Message { // Deprecated: Use Certificate.ProtoReflect.Descriptor instead. func (*Certificate) Descriptor() ([]byte, []int) { - return file_core_proto_rawDescGZIP(), []int{3} + return file_core_proto_rawDescGZIP(), []int{4} } func (x *Certificate) GetRegistrationID() int64 { @@ -393,10 +427,7 @@ func (x *Certificate) GetExpires() *timestamppb.Timestamp { } type CertificateStatus struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // Next unused field number: 16 Serial string `protobuf:"bytes,1,opt,name=serial,proto3" json:"serial,omitempty"` Status string `protobuf:"bytes,3,opt,name=status,proto3" json:"status,omitempty"` @@ -407,15 +438,15 @@ type CertificateStatus struct { NotAfter *timestamppb.Timestamp `protobuf:"bytes,14,opt,name=notAfter,proto3" json:"notAfter,omitempty"` IsExpired bool `protobuf:"varint,10,opt,name=isExpired,proto3" json:"isExpired,omitempty"` IssuerID int64 `protobuf:"varint,11,opt,name=issuerID,proto3" json:"issuerID,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *CertificateStatus) Reset() { *x = CertificateStatus{} - if protoimpl.UnsafeEnabled { - mi := &file_core_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_core_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *CertificateStatus) String() string { @@ -425,8 +456,8 @@ func (x *CertificateStatus) String() string { func (*CertificateStatus) ProtoMessage() {} func (x *CertificateStatus) ProtoReflect() protoreflect.Message { - mi := &file_core_proto_msgTypes[4] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_core_proto_msgTypes[5] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -438,7 +469,7 @@ func (x *CertificateStatus) ProtoReflect() protoreflect.Message { // Deprecated: Use CertificateStatus.ProtoReflect.Descriptor instead. func (*CertificateStatus) Descriptor() ([]byte, []int) { - return file_core_proto_rawDescGZIP(), []int{4} + return file_core_proto_rawDescGZIP(), []int{5} } func (x *CertificateStatus) GetSerial() string { @@ -505,28 +536,23 @@ func (x *CertificateStatus) GetIssuerID() int64 { } type Registration struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // Next unused field number: 10 - Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` - Key []byte `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"` - Contact []string `protobuf:"bytes,3,rep,name=contact,proto3" json:"contact,omitempty"` - ContactsPresent bool `protobuf:"varint,4,opt,name=contactsPresent,proto3" json:"contactsPresent,omitempty"` - Agreement string `protobuf:"bytes,5,opt,name=agreement,proto3" json:"agreement,omitempty"` - InitialIP []byte `protobuf:"bytes,6,opt,name=initialIP,proto3" json:"initialIP,omitempty"` - CreatedAt *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=createdAt,proto3" json:"createdAt,omitempty"` - Status string `protobuf:"bytes,8,opt,name=status,proto3" json:"status,omitempty"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + Key []byte `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"` + Contact []string `protobuf:"bytes,3,rep,name=contact,proto3" json:"contact,omitempty"` + Agreement string `protobuf:"bytes,5,opt,name=agreement,proto3" json:"agreement,omitempty"` + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=createdAt,proto3" json:"createdAt,omitempty"` + Status string `protobuf:"bytes,8,opt,name=status,proto3" json:"status,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Registration) Reset() { *x = Registration{} - if protoimpl.UnsafeEnabled { - mi := &file_core_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_core_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Registration) String() string { @@ -536,8 +562,8 @@ func (x *Registration) String() string { func (*Registration) ProtoMessage() {} func (x *Registration) ProtoReflect() protoreflect.Message { - mi := &file_core_proto_msgTypes[5] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_core_proto_msgTypes[6] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -549,7 +575,7 @@ func (x *Registration) ProtoReflect() protoreflect.Message { // Deprecated: Use Registration.ProtoReflect.Descriptor instead. func (*Registration) Descriptor() ([]byte, []int) { - return file_core_proto_rawDescGZIP(), []int{5} + return file_core_proto_rawDescGZIP(), []int{6} } func (x *Registration) GetId() int64 { @@ -573,13 +599,6 @@ func (x *Registration) GetContact() []string { return nil } -func (x *Registration) GetContactsPresent() bool { - if x != nil { - return x.ContactsPresent - } - return false -} - func (x *Registration) GetAgreement() string { if x != nil { return x.Agreement @@ -587,13 +606,6 @@ func (x *Registration) GetAgreement() string { return "" } -func (x *Registration) GetInitialIP() []byte { - if x != nil { - return x.InitialIP - } - return nil -} - func (x *Registration) GetCreatedAt() *timestamppb.Timestamp { if x != nil { return x.CreatedAt @@ -609,26 +621,23 @@ func (x *Registration) GetStatus() string { } type Authorization struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - // Next unused field number: 10 - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - Identifier string `protobuf:"bytes,2,opt,name=identifier,proto3" json:"identifier,omitempty"` - RegistrationID int64 `protobuf:"varint,3,opt,name=registrationID,proto3" json:"registrationID,omitempty"` - Status string `protobuf:"bytes,4,opt,name=status,proto3" json:"status,omitempty"` - Expires *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=expires,proto3" json:"expires,omitempty"` - Challenges []*Challenge `protobuf:"bytes,6,rep,name=challenges,proto3" json:"challenges,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + RegistrationID int64 `protobuf:"varint,3,opt,name=registrationID,proto3" json:"registrationID,omitempty"` + Identifier *Identifier `protobuf:"bytes,11,opt,name=identifier,proto3" json:"identifier,omitempty"` + Status string `protobuf:"bytes,4,opt,name=status,proto3" json:"status,omitempty"` + Expires *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=expires,proto3" json:"expires,omitempty"` + Challenges []*Challenge `protobuf:"bytes,6,rep,name=challenges,proto3" json:"challenges,omitempty"` + CertificateProfileName string `protobuf:"bytes,10,opt,name=certificateProfileName,proto3" json:"certificateProfileName,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Authorization) Reset() { *x = Authorization{} - if protoimpl.UnsafeEnabled { - mi := &file_core_proto_msgTypes[6] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_core_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Authorization) String() string { @@ -638,8 +647,8 @@ func (x *Authorization) String() string { func (*Authorization) ProtoMessage() {} func (x *Authorization) ProtoReflect() protoreflect.Message { - mi := &file_core_proto_msgTypes[6] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_core_proto_msgTypes[7] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -651,7 +660,7 @@ func (x *Authorization) ProtoReflect() protoreflect.Message { // Deprecated: Use Authorization.ProtoReflect.Descriptor instead. func (*Authorization) Descriptor() ([]byte, []int) { - return file_core_proto_rawDescGZIP(), []int{6} + return file_core_proto_rawDescGZIP(), []int{7} } func (x *Authorization) GetId() string { @@ -661,13 +670,6 @@ func (x *Authorization) GetId() string { return "" } -func (x *Authorization) GetIdentifier() string { - if x != nil { - return x.Identifier - } - return "" -} - func (x *Authorization) GetRegistrationID() int64 { if x != nil { return x.RegistrationID @@ -675,6 +677,13 @@ func (x *Authorization) GetRegistrationID() int64 { return 0 } +func (x *Authorization) GetIdentifier() *Identifier { + if x != nil { + return x.Identifier + } + return nil +} + func (x *Authorization) GetStatus() string { if x != nil { return x.Status @@ -696,32 +705,40 @@ func (x *Authorization) GetChallenges() []*Challenge { return nil } -type Order struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields +func (x *Authorization) GetCertificateProfileName() string { + if x != nil { + return x.CertificateProfileName + } + return "" +} - // Next unused field number: 15 - Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` - RegistrationID int64 `protobuf:"varint,2,opt,name=registrationID,proto3" json:"registrationID,omitempty"` - Expires *timestamppb.Timestamp `protobuf:"bytes,12,opt,name=expires,proto3" json:"expires,omitempty"` - Error *ProblemDetails `protobuf:"bytes,4,opt,name=error,proto3" json:"error,omitempty"` - CertificateSerial string `protobuf:"bytes,5,opt,name=certificateSerial,proto3" json:"certificateSerial,omitempty"` - Status string `protobuf:"bytes,7,opt,name=status,proto3" json:"status,omitempty"` - Names []string `protobuf:"bytes,8,rep,name=names,proto3" json:"names,omitempty"` - BeganProcessing bool `protobuf:"varint,9,opt,name=beganProcessing,proto3" json:"beganProcessing,omitempty"` +type Order struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + RegistrationID int64 `protobuf:"varint,2,opt,name=registrationID,proto3" json:"registrationID,omitempty"` + // Fields specified by RFC 8555, Section 7.1.3 + // Note that we do not respect notBefore and notAfter, and we infer the + // finalize and certificate URLs from the id and certificateSerial fields. + Status string `protobuf:"bytes,7,opt,name=status,proto3" json:"status,omitempty"` + Expires *timestamppb.Timestamp `protobuf:"bytes,12,opt,name=expires,proto3" json:"expires,omitempty"` + Identifiers []*Identifier `protobuf:"bytes,16,rep,name=identifiers,proto3" json:"identifiers,omitempty"` + Error *ProblemDetails `protobuf:"bytes,4,opt,name=error,proto3" json:"error,omitempty"` + V2Authorizations []int64 `protobuf:"varint,11,rep,packed,name=v2Authorizations,proto3" json:"v2Authorizations,omitempty"` + CertificateSerial string `protobuf:"bytes,5,opt,name=certificateSerial,proto3" json:"certificateSerial,omitempty"` + // Additional fields for our own record-keeping. Created *timestamppb.Timestamp `protobuf:"bytes,13,opt,name=created,proto3" json:"created,omitempty"` - V2Authorizations []int64 `protobuf:"varint,11,rep,packed,name=v2Authorizations,proto3" json:"v2Authorizations,omitempty"` CertificateProfileName string `protobuf:"bytes,14,opt,name=certificateProfileName,proto3" json:"certificateProfileName,omitempty"` + Replaces string `protobuf:"bytes,15,opt,name=replaces,proto3" json:"replaces,omitempty"` + BeganProcessing bool `protobuf:"varint,9,opt,name=beganProcessing,proto3" json:"beganProcessing,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Order) Reset() { *x = Order{} - if protoimpl.UnsafeEnabled { - mi := &file_core_proto_msgTypes[7] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_core_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Order) String() string { @@ -731,8 +748,8 @@ func (x *Order) String() string { func (*Order) ProtoMessage() {} func (x *Order) ProtoReflect() protoreflect.Message { - mi := &file_core_proto_msgTypes[7] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_core_proto_msgTypes[8] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -744,7 +761,7 @@ func (x *Order) ProtoReflect() protoreflect.Message { // Deprecated: Use Order.ProtoReflect.Descriptor instead. func (*Order) Descriptor() ([]byte, []int) { - return file_core_proto_rawDescGZIP(), []int{7} + return file_core_proto_rawDescGZIP(), []int{8} } func (x *Order) GetId() int64 { @@ -761,6 +778,13 @@ func (x *Order) GetRegistrationID() int64 { return 0 } +func (x *Order) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + func (x *Order) GetExpires() *timestamppb.Timestamp { if x != nil { return x.Expires @@ -768,6 +792,13 @@ func (x *Order) GetExpires() *timestamppb.Timestamp { return nil } +func (x *Order) GetIdentifiers() []*Identifier { + if x != nil { + return x.Identifiers + } + return nil +} + func (x *Order) GetError() *ProblemDetails { if x != nil { return x.Error @@ -775,6 +806,13 @@ func (x *Order) GetError() *ProblemDetails { return nil } +func (x *Order) GetV2Authorizations() []int64 { + if x != nil { + return x.V2Authorizations + } + return nil +} + func (x *Order) GetCertificateSerial() string { if x != nil { return x.CertificateSerial @@ -782,27 +820,6 @@ func (x *Order) GetCertificateSerial() string { return "" } -func (x *Order) GetStatus() string { - if x != nil { - return x.Status - } - return "" -} - -func (x *Order) GetNames() []string { - if x != nil { - return x.Names - } - return nil -} - -func (x *Order) GetBeganProcessing() bool { - if x != nil { - return x.BeganProcessing - } - return false -} - func (x *Order) GetCreated() *timestamppb.Timestamp { if x != nil { return x.Created @@ -810,13 +827,6 @@ func (x *Order) GetCreated() *timestamppb.Timestamp { return nil } -func (x *Order) GetV2Authorizations() []int64 { - if x != nil { - return x.V2Authorizations - } - return nil -} - func (x *Order) GetCertificateProfileName() string { if x != nil { return x.CertificateProfileName @@ -824,24 +834,35 @@ func (x *Order) GetCertificateProfileName() string { return "" } -type CRLEntry struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields +func (x *Order) GetReplaces() string { + if x != nil { + return x.Replaces + } + return "" +} +func (x *Order) GetBeganProcessing() bool { + if x != nil { + return x.BeganProcessing + } + return false +} + +type CRLEntry struct { + state protoimpl.MessageState `protogen:"open.v1"` // Next unused field number: 5 - Serial string `protobuf:"bytes,1,opt,name=serial,proto3" json:"serial,omitempty"` - Reason int32 `protobuf:"varint,2,opt,name=reason,proto3" json:"reason,omitempty"` - RevokedAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=revokedAt,proto3" json:"revokedAt,omitempty"` + Serial string `protobuf:"bytes,1,opt,name=serial,proto3" json:"serial,omitempty"` + Reason int32 `protobuf:"varint,2,opt,name=reason,proto3" json:"reason,omitempty"` + RevokedAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=revokedAt,proto3" json:"revokedAt,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *CRLEntry) Reset() { *x = CRLEntry{} - if protoimpl.UnsafeEnabled { - mi := &file_core_proto_msgTypes[8] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_core_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *CRLEntry) String() string { @@ -851,8 +872,8 @@ func (x *CRLEntry) String() string { func (*CRLEntry) ProtoMessage() {} func (x *CRLEntry) ProtoReflect() protoreflect.Message { - mi := &file_core_proto_msgTypes[8] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_core_proto_msgTypes[9] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -864,7 +885,7 @@ func (x *CRLEntry) ProtoReflect() protoreflect.Message { // Deprecated: Use CRLEntry.ProtoReflect.Descriptor instead. func (*CRLEntry) Descriptor() ([]byte, []int) { - return file_core_proto_rawDescGZIP(), []int{8} + return file_core_proto_rawDescGZIP(), []int{9} } func (x *CRLEntry) GetSerial() string { @@ -890,223 +911,232 @@ func (x *CRLEntry) GetRevokedAt() *timestamppb.Timestamp { var File_core_proto protoreflect.FileDescriptor -var file_core_proto_rawDesc = []byte{ +var file_core_proto_rawDesc = string([]byte{ 0x0a, 0x0a, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x04, 0x63, 0x6f, 0x72, 0x65, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x22, 0xd9, 0x02, 0x0a, 0x09, 0x43, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, - 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, - 0x64, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, - 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x10, 0x0a, - 0x03, 0x75, 0x72, 0x69, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x69, 0x12, - 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, - 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x2a, 0x0a, 0x10, 0x6b, 0x65, 0x79, 0x41, 0x75, 0x74, 0x68, - 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x10, 0x6b, 0x65, 0x79, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x12, 0x44, 0x0a, 0x11, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x72, - 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x63, - 0x6f, 0x72, 0x65, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, - 0x63, 0x6f, 0x72, 0x64, 0x52, 0x11, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x12, 0x2a, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, - 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x50, 0x72, - 0x6f, 0x62, 0x6c, 0x65, 0x6d, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x52, 0x05, 0x65, 0x72, - 0x72, 0x6f, 0x72, 0x12, 0x38, 0x0a, 0x09, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x64, - 0x18, 0x0c, 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, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x64, 0x4a, 0x04, 0x08, - 0x04, 0x10, 0x05, 0x4a, 0x04, 0x08, 0x08, 0x10, 0x09, 0x4a, 0x04, 0x08, 0x0b, 0x10, 0x0c, 0x22, - 0x94, 0x02, 0x0a, 0x10, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, - 0x63, 0x6f, 0x72, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, - 0x12, 0x12, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, - 0x70, 0x6f, 0x72, 0x74, 0x12, 0x2c, 0x0a, 0x11, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, - 0x73, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0c, 0x52, - 0x11, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, - 0x65, 0x64, 0x12, 0x20, 0x0a, 0x0b, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x55, 0x73, 0x65, - 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, - 0x55, 0x73, 0x65, 0x64, 0x12, 0x20, 0x0a, 0x0b, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, - 0x69, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x61, 0x75, 0x74, 0x68, 0x6f, - 0x72, 0x69, 0x74, 0x69, 0x65, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x06, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x26, 0x0a, 0x0e, 0x61, 0x64, 0x64, 0x72, - 0x65, 0x73, 0x73, 0x65, 0x73, 0x54, 0x72, 0x69, 0x65, 0x64, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0c, - 0x52, 0x0e, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x54, 0x72, 0x69, 0x65, 0x64, - 0x12, 0x24, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x72, 0x41, 0x64, 0x64, 0x72, - 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, - 0x72, 0x41, 0x64, 0x64, 0x72, 0x73, 0x22, 0x6a, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x62, 0x6c, 0x65, - 0x6d, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, 0x20, 0x0a, 0x0b, 0x70, 0x72, 0x6f, 0x62, - 0x6c, 0x65, 0x6d, 0x54, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x70, - 0x72, 0x6f, 0x62, 0x6c, 0x65, 0x6d, 0x54, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x65, - 0x74, 0x61, 0x69, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x65, 0x74, 0x61, - 0x69, 0x6c, 0x12, 0x1e, 0x0a, 0x0a, 0x68, 0x74, 0x74, 0x70, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x68, 0x74, 0x74, 0x70, 0x53, 0x74, 0x61, 0x74, - 0x75, 0x73, 0x22, 0xed, 0x01, 0x0a, 0x0b, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, - 0x74, 0x65, 0x12, 0x26, 0x0a, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x72, 0x65, 0x67, 0x69, - 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x65, - 0x72, 0x69, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x65, 0x72, 0x69, - 0x61, 0x6c, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x69, 0x67, 0x65, 0x73, 0x74, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x06, 0x64, 0x69, 0x67, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x64, 0x65, - 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x64, 0x65, 0x72, 0x12, 0x32, 0x0a, 0x06, - 0x69, 0x73, 0x73, 0x75, 0x65, 0x64, 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, 0x06, 0x69, 0x73, 0x73, 0x75, 0x65, 0x64, - 0x12, 0x34, 0x0a, 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, + 0x6f, 0x74, 0x6f, 0x22, 0x36, 0x0a, 0x0a, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, + 0x72, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0xb3, 0x02, 0x0a, 0x09, + 0x43, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x10, 0x0a, + 0x03, 0x75, 0x72, 0x6c, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, + 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x38, 0x0a, 0x09, 0x76, 0x61, 0x6c, 0x69, 0x64, + 0x61, 0x74, 0x65, 0x64, 0x18, 0x0c, 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, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, + 0x64, 0x12, 0x2a, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x14, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x50, 0x72, 0x6f, 0x62, 0x6c, 0x65, 0x6d, 0x44, + 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x14, 0x0a, + 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, + 0x6b, 0x65, 0x6e, 0x12, 0x44, 0x0a, 0x11, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, + 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x11, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x4a, 0x04, 0x08, 0x04, 0x10, 0x05, 0x4a, + 0x04, 0x08, 0x05, 0x10, 0x06, 0x4a, 0x04, 0x08, 0x08, 0x10, 0x09, 0x4a, 0x04, 0x08, 0x0b, 0x10, + 0x0c, 0x22, 0x94, 0x02, 0x0a, 0x10, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, + 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x2c, 0x0a, 0x11, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, + 0x73, 0x65, 0x73, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x18, 0x03, 0x20, 0x03, 0x28, + 0x0c, 0x52, 0x11, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x52, 0x65, 0x73, 0x6f, + 0x6c, 0x76, 0x65, 0x64, 0x12, 0x20, 0x0a, 0x0b, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x55, + 0x73, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x61, 0x64, 0x64, 0x72, 0x65, + 0x73, 0x73, 0x55, 0x73, 0x65, 0x64, 0x12, 0x20, 0x0a, 0x0b, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, + 0x69, 0x74, 0x69, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x61, 0x75, 0x74, + 0x68, 0x6f, 0x72, 0x69, 0x74, 0x69, 0x65, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, + 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x26, 0x0a, 0x0e, 0x61, 0x64, + 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x54, 0x72, 0x69, 0x65, 0x64, 0x18, 0x07, 0x20, 0x03, + 0x28, 0x0c, 0x52, 0x0e, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x54, 0x72, 0x69, + 0x65, 0x64, 0x12, 0x24, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x72, 0x41, 0x64, + 0x64, 0x72, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x6c, + 0x76, 0x65, 0x72, 0x41, 0x64, 0x64, 0x72, 0x73, 0x22, 0x6a, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x62, + 0x6c, 0x65, 0x6d, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, 0x20, 0x0a, 0x0b, 0x70, 0x72, + 0x6f, 0x62, 0x6c, 0x65, 0x6d, 0x54, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0b, 0x70, 0x72, 0x6f, 0x62, 0x6c, 0x65, 0x6d, 0x54, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, + 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x65, + 0x74, 0x61, 0x69, 0x6c, 0x12, 0x1e, 0x0a, 0x0a, 0x68, 0x74, 0x74, 0x70, 0x53, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x68, 0x74, 0x74, 0x70, 0x53, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x22, 0xed, 0x01, 0x0a, 0x0b, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, + 0x63, 0x61, 0x74, 0x65, 0x12, 0x26, 0x0a, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x72, 0x65, + 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x12, 0x16, 0x0a, 0x06, + 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x65, + 0x72, 0x69, 0x61, 0x6c, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x69, 0x67, 0x65, 0x73, 0x74, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x69, 0x67, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, + 0x64, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x64, 0x65, 0x72, 0x12, 0x32, + 0x0a, 0x06, 0x69, 0x73, 0x73, 0x75, 0x65, 0x64, 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, 0x06, 0x69, 0x73, 0x73, 0x75, + 0x65, 0x64, 0x12, 0x34, 0x0a, 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x18, 0x08, 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, + 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x4a, 0x04, 0x08, 0x05, 0x10, 0x06, 0x4a, 0x04, + 0x08, 0x06, 0x10, 0x07, 0x22, 0xd5, 0x03, 0x0a, 0x11, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, + 0x63, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x65, + 0x72, 0x69, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x65, 0x72, 0x69, + 0x61, 0x6c, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x44, 0x0a, 0x0f, 0x6f, 0x63, + 0x73, 0x70, 0x4c, 0x61, 0x73, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x18, 0x0f, 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, + 0x0f, 0x6f, 0x63, 0x73, 0x70, 0x4c, 0x61, 0x73, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, + 0x12, 0x3c, 0x0a, 0x0b, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x44, 0x61, 0x74, 0x65, 0x18, + 0x0c, 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, 0x0b, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x44, 0x61, 0x74, 0x65, 0x12, 0x24, + 0x0a, 0x0d, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, + 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x52, 0x65, + 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x50, 0x0a, 0x15, 0x6c, 0x61, 0x73, 0x74, 0x45, 0x78, 0x70, 0x69, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x67, 0x53, 0x65, 0x6e, 0x74, 0x18, 0x0d, 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, + 0x15, 0x6c, 0x61, 0x73, 0x74, 0x45, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4e, + 0x61, 0x67, 0x53, 0x65, 0x6e, 0x74, 0x12, 0x36, 0x0a, 0x08, 0x6e, 0x6f, 0x74, 0x41, 0x66, 0x74, + 0x65, 0x72, 0x18, 0x0e, 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, 0x08, 0x6e, 0x6f, 0x74, 0x41, 0x66, 0x74, 0x65, 0x72, 0x12, 0x1c, + 0x0a, 0x09, 0x69, 0x73, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x09, 0x69, 0x73, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x64, 0x12, 0x1a, 0x0a, 0x08, + 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x49, 0x44, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, + 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x49, 0x44, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x4a, 0x04, + 0x08, 0x04, 0x10, 0x05, 0x4a, 0x04, 0x08, 0x05, 0x10, 0x06, 0x4a, 0x04, 0x08, 0x07, 0x10, 0x08, + 0x4a, 0x04, 0x08, 0x08, 0x10, 0x09, 0x4a, 0x04, 0x08, 0x09, 0x10, 0x0a, 0x22, 0xcc, 0x01, 0x0a, + 0x0c, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0e, 0x0a, + 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x12, 0x10, 0x0a, + 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, + 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x63, 0x74, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, + 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x63, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x67, 0x72, + 0x65, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x67, + 0x72, 0x65, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x38, 0x0a, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x64, 0x41, 0x74, 0x18, 0x09, 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, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, + 0x74, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x4a, 0x04, 0x08, 0x04, 0x10, 0x05, 0x4a, + 0x04, 0x08, 0x06, 0x10, 0x07, 0x4a, 0x04, 0x08, 0x07, 0x10, 0x08, 0x22, 0xc8, 0x02, 0x0a, 0x0d, + 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0e, 0x0a, + 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 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, 0x30, 0x0a, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, + 0x69, 0x65, 0x72, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x6f, 0x72, 0x65, + 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x0a, 0x69, 0x64, 0x65, + 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, + 0x34, 0x0a, 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x18, 0x09, 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, 0x07, 0x65, 0x78, + 0x70, 0x69, 0x72, 0x65, 0x73, 0x12, 0x2f, 0x0a, 0x0a, 0x63, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, + 0x67, 0x65, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x63, 0x6f, 0x72, 0x65, + 0x2e, 0x43, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x52, 0x0a, 0x63, 0x68, 0x61, 0x6c, + 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x73, 0x12, 0x36, 0x0a, 0x16, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, + 0x69, 0x63, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, + 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x16, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, + 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x4a, 0x04, + 0x08, 0x05, 0x10, 0x06, 0x4a, 0x04, 0x08, 0x07, 0x10, 0x08, 0x4a, 0x04, 0x08, 0x08, 0x10, 0x09, + 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x22, 0x93, 0x04, 0x0a, 0x05, 0x4f, 0x72, 0x64, 0x65, 0x72, + 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, + 0x12, 0x26, 0x0a, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x12, 0x34, 0x0a, 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x18, 0x0c, 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, 0x07, 0x65, - 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x4a, 0x04, 0x08, 0x05, 0x10, 0x06, 0x4a, 0x04, 0x08, 0x06, - 0x10, 0x07, 0x22, 0xd5, 0x03, 0x0a, 0x11, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, - 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x65, 0x72, 0x69, + 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x12, 0x32, 0x0a, 0x0b, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, + 0x66, 0x69, 0x65, 0x72, 0x73, 0x18, 0x10, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x6f, + 0x72, 0x65, 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x0b, 0x69, + 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x12, 0x2a, 0x0a, 0x05, 0x65, 0x72, + 0x72, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x63, 0x6f, 0x72, 0x65, + 0x2e, 0x50, 0x72, 0x6f, 0x62, 0x6c, 0x65, 0x6d, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x52, + 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x2a, 0x0a, 0x10, 0x76, 0x32, 0x41, 0x75, 0x74, 0x68, + 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x03, + 0x52, 0x10, 0x76, 0x32, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x12, 0x2c, 0x0a, 0x11, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, + 0x65, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x63, + 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, + 0x12, 0x34, 0x0a, 0x07, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x18, 0x0d, 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, 0x07, 0x63, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x12, 0x36, 0x0a, 0x16, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, + 0x69, 0x63, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, + 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x16, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, + 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1a, + 0x0a, 0x08, 0x72, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x73, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x72, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x73, 0x12, 0x28, 0x0a, 0x0f, 0x62, 0x65, + 0x67, 0x61, 0x6e, 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x69, 0x6e, 0x67, 0x18, 0x09, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x0f, 0x62, 0x65, 0x67, 0x61, 0x6e, 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, + 0x73, 0x69, 0x6e, 0x67, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x4a, 0x04, 0x08, 0x06, 0x10, 0x07, + 0x4a, 0x04, 0x08, 0x0a, 0x10, 0x0b, 0x4a, 0x04, 0x08, 0x08, 0x10, 0x09, 0x22, 0x7a, 0x0a, 0x08, + 0x43, 0x52, 0x4c, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, - 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x44, 0x0a, 0x0f, 0x6f, 0x63, 0x73, 0x70, - 0x4c, 0x61, 0x73, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x18, 0x0f, 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, 0x0f, 0x6f, - 0x63, 0x73, 0x70, 0x4c, 0x61, 0x73, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x12, 0x3c, - 0x0a, 0x0b, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x44, 0x61, 0x74, 0x65, 0x18, 0x0c, 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, - 0x0b, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x44, 0x61, 0x74, 0x65, 0x12, 0x24, 0x0a, 0x0d, - 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x06, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x0d, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x52, 0x65, 0x61, 0x73, - 0x6f, 0x6e, 0x12, 0x50, 0x0a, 0x15, 0x6c, 0x61, 0x73, 0x74, 0x45, 0x78, 0x70, 0x69, 0x72, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x67, 0x53, 0x65, 0x6e, 0x74, 0x18, 0x0d, 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, 0x15, 0x6c, - 0x61, 0x73, 0x74, 0x45, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x67, - 0x53, 0x65, 0x6e, 0x74, 0x12, 0x36, 0x0a, 0x08, 0x6e, 0x6f, 0x74, 0x41, 0x66, 0x74, 0x65, 0x72, - 0x18, 0x0e, 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, 0x08, 0x6e, 0x6f, 0x74, 0x41, 0x66, 0x74, 0x65, 0x72, 0x12, 0x1c, 0x0a, 0x09, - 0x69, 0x73, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x09, 0x69, 0x73, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x69, 0x73, - 0x73, 0x75, 0x65, 0x72, 0x49, 0x44, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x69, 0x73, - 0x73, 0x75, 0x65, 0x72, 0x49, 0x44, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x4a, 0x04, 0x08, 0x04, - 0x10, 0x05, 0x4a, 0x04, 0x08, 0x05, 0x10, 0x06, 0x4a, 0x04, 0x08, 0x07, 0x10, 0x08, 0x4a, 0x04, - 0x08, 0x08, 0x10, 0x09, 0x4a, 0x04, 0x08, 0x09, 0x10, 0x0a, 0x22, 0x88, 0x02, 0x0a, 0x0c, 0x52, - 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x6b, - 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x18, 0x0a, - 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x63, 0x74, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, - 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x63, 0x74, 0x12, 0x28, 0x0a, 0x0f, 0x63, 0x6f, 0x6e, 0x74, 0x61, - 0x63, 0x74, 0x73, 0x50, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x0f, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x63, 0x74, 0x73, 0x50, 0x72, 0x65, 0x73, 0x65, 0x6e, - 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x67, 0x72, 0x65, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x05, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x67, 0x72, 0x65, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x12, - 0x1c, 0x0a, 0x09, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x49, 0x50, 0x18, 0x06, 0x20, 0x01, - 0x28, 0x0c, 0x52, 0x09, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x49, 0x50, 0x12, 0x38, 0x0a, - 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x18, 0x09, 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, 0x63, 0x72, - 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x4a, - 0x04, 0x08, 0x07, 0x10, 0x08, 0x22, 0xf8, 0x01, 0x0a, 0x0d, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, - 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, - 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x69, 0x64, 0x65, - 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 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, - 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x34, 0x0a, 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, - 0x65, 0x73, 0x18, 0x09, 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, 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x12, 0x2f, 0x0a, - 0x0a, 0x63, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x0f, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x43, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, - 0x67, 0x65, 0x52, 0x0a, 0x63, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x73, 0x4a, 0x04, - 0x08, 0x05, 0x10, 0x06, 0x4a, 0x04, 0x08, 0x07, 0x10, 0x08, 0x4a, 0x04, 0x08, 0x08, 0x10, 0x09, - 0x22, 0xd3, 0x03, 0x0a, 0x05, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x12, 0x26, 0x0a, 0x0e, 0x72, 0x65, - 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x49, 0x44, 0x12, 0x34, 0x0a, 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x18, 0x0c, 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, - 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x12, 0x2a, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, - 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x50, - 0x72, 0x6f, 0x62, 0x6c, 0x65, 0x6d, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x52, 0x05, 0x65, - 0x72, 0x72, 0x6f, 0x72, 0x12, 0x2c, 0x0a, 0x11, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, - 0x61, 0x74, 0x65, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x11, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x53, 0x65, 0x72, 0x69, - 0x61, 0x6c, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x07, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x61, - 0x6d, 0x65, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x6e, 0x61, 0x6d, 0x65, 0x73, - 0x12, 0x28, 0x0a, 0x0f, 0x62, 0x65, 0x67, 0x61, 0x6e, 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, - 0x69, 0x6e, 0x67, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x62, 0x65, 0x67, 0x61, 0x6e, - 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x69, 0x6e, 0x67, 0x12, 0x34, 0x0a, 0x07, 0x63, 0x72, - 0x65, 0x61, 0x74, 0x65, 0x64, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, + 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x02, 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, 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, 0x07, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, - 0x12, 0x2a, 0x0a, 0x10, 0x76, 0x32, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x03, 0x52, 0x10, 0x76, 0x32, 0x41, 0x75, - 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x36, 0x0a, 0x16, - 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x66, 0x69, - 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x16, 0x63, 0x65, - 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, - 0x4e, 0x61, 0x6d, 0x65, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x4a, 0x04, 0x08, 0x06, 0x10, 0x07, - 0x4a, 0x04, 0x08, 0x0a, 0x10, 0x0b, 0x22, 0x7a, 0x0a, 0x08, 0x43, 0x52, 0x4c, 0x45, 0x6e, 0x74, - 0x72, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x06, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, - 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x02, 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, - 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, 0x09, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x41, 0x74, 0x4a, 0x04, 0x08, 0x03, - 0x10, 0x04, 0x42, 0x2b, 0x5a, 0x29, 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, 0x6f, 0x72, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, -} + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, + 0x41, 0x74, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x42, 0x2b, 0x5a, 0x29, 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, 0x6f, 0x72, 0x65, 0x2f, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +}) var ( file_core_proto_rawDescOnce sync.Once - file_core_proto_rawDescData = file_core_proto_rawDesc + file_core_proto_rawDescData []byte ) func file_core_proto_rawDescGZIP() []byte { file_core_proto_rawDescOnce.Do(func() { - file_core_proto_rawDescData = protoimpl.X.CompressGZIP(file_core_proto_rawDescData) + file_core_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_core_proto_rawDesc), len(file_core_proto_rawDesc))) }) return file_core_proto_rawDescData } -var file_core_proto_msgTypes = make([]protoimpl.MessageInfo, 9) -var file_core_proto_goTypes = []interface{}{ - (*Challenge)(nil), // 0: core.Challenge - (*ValidationRecord)(nil), // 1: core.ValidationRecord - (*ProblemDetails)(nil), // 2: core.ProblemDetails - (*Certificate)(nil), // 3: core.Certificate - (*CertificateStatus)(nil), // 4: core.CertificateStatus - (*Registration)(nil), // 5: core.Registration - (*Authorization)(nil), // 6: core.Authorization - (*Order)(nil), // 7: core.Order - (*CRLEntry)(nil), // 8: core.CRLEntry - (*timestamppb.Timestamp)(nil), // 9: google.protobuf.Timestamp +var file_core_proto_msgTypes = make([]protoimpl.MessageInfo, 10) +var file_core_proto_goTypes = []any{ + (*Identifier)(nil), // 0: core.Identifier + (*Challenge)(nil), // 1: core.Challenge + (*ValidationRecord)(nil), // 2: core.ValidationRecord + (*ProblemDetails)(nil), // 3: core.ProblemDetails + (*Certificate)(nil), // 4: core.Certificate + (*CertificateStatus)(nil), // 5: core.CertificateStatus + (*Registration)(nil), // 6: core.Registration + (*Authorization)(nil), // 7: core.Authorization + (*Order)(nil), // 8: core.Order + (*CRLEntry)(nil), // 9: core.CRLEntry + (*timestamppb.Timestamp)(nil), // 10: google.protobuf.Timestamp } var file_core_proto_depIdxs = []int32{ - 1, // 0: core.Challenge.validationrecords:type_name -> core.ValidationRecord - 2, // 1: core.Challenge.error:type_name -> core.ProblemDetails - 9, // 2: core.Challenge.validated:type_name -> google.protobuf.Timestamp - 9, // 3: core.Certificate.issued:type_name -> google.protobuf.Timestamp - 9, // 4: core.Certificate.expires:type_name -> google.protobuf.Timestamp - 9, // 5: core.CertificateStatus.ocspLastUpdated:type_name -> google.protobuf.Timestamp - 9, // 6: core.CertificateStatus.revokedDate:type_name -> google.protobuf.Timestamp - 9, // 7: core.CertificateStatus.lastExpirationNagSent:type_name -> google.protobuf.Timestamp - 9, // 8: core.CertificateStatus.notAfter:type_name -> google.protobuf.Timestamp - 9, // 9: core.Registration.createdAt:type_name -> google.protobuf.Timestamp - 9, // 10: core.Authorization.expires:type_name -> google.protobuf.Timestamp - 0, // 11: core.Authorization.challenges:type_name -> core.Challenge - 9, // 12: core.Order.expires:type_name -> google.protobuf.Timestamp - 2, // 13: core.Order.error:type_name -> core.ProblemDetails - 9, // 14: core.Order.created:type_name -> google.protobuf.Timestamp - 9, // 15: core.CRLEntry.revokedAt:type_name -> google.protobuf.Timestamp - 16, // [16:16] is the sub-list for method output_type - 16, // [16:16] is the sub-list for method input_type - 16, // [16:16] is the sub-list for extension type_name - 16, // [16:16] is the sub-list for extension extendee - 0, // [0:16] is the sub-list for field type_name + 10, // 0: core.Challenge.validated:type_name -> google.protobuf.Timestamp + 3, // 1: core.Challenge.error:type_name -> core.ProblemDetails + 2, // 2: core.Challenge.validationrecords:type_name -> core.ValidationRecord + 10, // 3: core.Certificate.issued:type_name -> google.protobuf.Timestamp + 10, // 4: core.Certificate.expires:type_name -> google.protobuf.Timestamp + 10, // 5: core.CertificateStatus.ocspLastUpdated:type_name -> google.protobuf.Timestamp + 10, // 6: core.CertificateStatus.revokedDate:type_name -> google.protobuf.Timestamp + 10, // 7: core.CertificateStatus.lastExpirationNagSent:type_name -> google.protobuf.Timestamp + 10, // 8: core.CertificateStatus.notAfter:type_name -> google.protobuf.Timestamp + 10, // 9: core.Registration.createdAt:type_name -> google.protobuf.Timestamp + 0, // 10: core.Authorization.identifier:type_name -> core.Identifier + 10, // 11: core.Authorization.expires:type_name -> google.protobuf.Timestamp + 1, // 12: core.Authorization.challenges:type_name -> core.Challenge + 10, // 13: core.Order.expires:type_name -> google.protobuf.Timestamp + 0, // 14: core.Order.identifiers:type_name -> core.Identifier + 3, // 15: core.Order.error:type_name -> core.ProblemDetails + 10, // 16: core.Order.created:type_name -> google.protobuf.Timestamp + 10, // 17: core.CRLEntry.revokedAt:type_name -> google.protobuf.Timestamp + 18, // [18:18] is the sub-list for method output_type + 18, // [18:18] is the sub-list for method input_type + 18, // [18:18] is the sub-list for extension type_name + 18, // [18:18] is the sub-list for extension extendee + 0, // [0:18] is the sub-list for field type_name } func init() { file_core_proto_init() } @@ -1114,123 +1144,13 @@ func file_core_proto_init() { if File_core_proto != nil { return } - if !protoimpl.UnsafeEnabled { - file_core_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Challenge); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_core_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ValidationRecord); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_core_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ProblemDetails); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_core_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Certificate); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_core_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CertificateStatus); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_core_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Registration); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_core_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Authorization); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_core_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Order); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_core_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CRLEntry); 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_core_proto_rawDesc, + RawDescriptor: unsafe.Slice(unsafe.StringData(file_core_proto_rawDesc), len(file_core_proto_rawDesc)), NumEnums: 0, - NumMessages: 9, + NumMessages: 10, NumExtensions: 0, NumServices: 0, }, @@ -1239,7 +1159,6 @@ func file_core_proto_init() { MessageInfos: file_core_proto_msgTypes, }.Build() File_core_proto = out.File - file_core_proto_rawDesc = nil file_core_proto_goTypes = nil file_core_proto_depIdxs = nil } diff --git a/third-party/github.com/letsencrypt/boulder/core/proto/core.proto b/third-party/github.com/letsencrypt/boulder/core/proto/core.proto index 3a13afa97..22cbfa43a 100644 --- a/third-party/github.com/letsencrypt/boulder/core/proto/core.proto +++ b/third-party/github.com/letsencrypt/boulder/core/proto/core.proto @@ -5,36 +5,40 @@ option go_package = "github.com/letsencrypt/boulder/core/proto"; import "google/protobuf/timestamp.proto"; +message Identifier { + string type = 1; + string value = 2; +} + message Challenge { // Next unused field number: 13 + reserved 4, 5, 8, 11; int64 id = 1; + // Fields specified by RFC 8555, Section 8. string type = 2; + string url = 9; string status = 6; - string uri = 9; - string token = 3; - reserved 4; // Previously accountKey - // TODO(#7514): Remove this. - string keyAuthorization = 5; - repeated ValidationRecord validationrecords = 10; - ProblemDetails error = 7; - reserved 8; // Unused and accidentally skipped during initial commit. - reserved 11; // Previously validatedNS google.protobuf.Timestamp validated = 12; + ProblemDetails error = 7; + // Fields specified by individual validation methods. + string token = 3; + // Additional fields for our own record keeping. + repeated ValidationRecord validationrecords = 10; } message ValidationRecord { // Next unused field number: 9 string hostname = 1; string port = 2; - repeated bytes addressesResolved = 3; // net.IP.MarshalText() - bytes addressUsed = 4; // net.IP.MarshalText() + repeated bytes addressesResolved = 3; // netip.Addr.MarshalText() + bytes addressUsed = 4; // netip.Addr.MarshalText() repeated string authorities = 5; string url = 6; // A list of addresses tried before the address used (see // core/objects.go and the comment on the ValidationRecord structure // definition for more information. - repeated bytes addressesTried = 7; // net.IP.MarshalText() + repeated bytes addressesTried = 7; // netip.Addr.MarshalText() repeated string resolverAddrs = 8; } @@ -80,43 +84,50 @@ message Registration { int64 id = 1; bytes key = 2; repeated string contact = 3; - bool contactsPresent = 4; + reserved 4; // Previously contactsPresent string agreement = 5; - bytes initialIP = 6; + reserved 6; // Previously initialIP reserved 7; // Previously createdAtNS google.protobuf.Timestamp createdAt = 9; string status = 8; } message Authorization { - // Next unused field number: 10 + // Next unused field number: 12 + reserved 5, 7, 8; string id = 1; - string identifier = 2; int64 registrationID = 3; + // Fields specified by RFC 8555, Section 7.1.4 + reserved 2; // Previously dnsName + Identifier identifier = 11; string status = 4; - reserved 5; // Previously expiresNS google.protobuf.Timestamp expires = 9; repeated core.Challenge challenges = 6; - reserved 7; // previously ACMEv1 combinations - reserved 8; // previously v2 + string certificateProfileName = 10; + // We do not directly represent the "wildcard" field, instead inferring it + // from the identifier value. } message Order { - // Next unused field number: 15 + // Next unused field number: 17 + reserved 3, 6, 10; int64 id = 1; int64 registrationID = 2; - reserved 3; // Previously expiresNS - google.protobuf.Timestamp expires = 12; - ProblemDetails error = 4; - string certificateSerial = 5; - reserved 6; // previously authorizations, deprecated in favor of v2Authorizations + // Fields specified by RFC 8555, Section 7.1.3 + // Note that we do not respect notBefore and notAfter, and we infer the + // finalize and certificate URLs from the id and certificateSerial fields. string status = 7; - repeated string names = 8; - bool beganProcessing = 9; - reserved 10; // Previously createdNS - google.protobuf.Timestamp created = 13; + google.protobuf.Timestamp expires = 12; + reserved 8; // Previously dnsNames + repeated Identifier identifiers = 16; + ProblemDetails error = 4; repeated int64 v2Authorizations = 11; + string certificateSerial = 5; + // Additional fields for our own record-keeping. + google.protobuf.Timestamp created = 13; string certificateProfileName = 14; + string replaces = 15; + bool beganProcessing = 9; } message CRLEntry { diff --git a/third-party/github.com/letsencrypt/boulder/core/util.go b/third-party/github.com/letsencrypt/boulder/core/util.go index 641521f16..6be6e143a 100644 --- a/third-party/github.com/letsencrypt/boulder/core/util.go +++ b/third-party/github.com/letsencrypt/boulder/core/util.go @@ -1,6 +1,7 @@ package core import ( + "context" "crypto" "crypto/ecdsa" "crypto/rand" @@ -15,7 +16,7 @@ import ( "fmt" "io" "math/big" - mrand "math/rand" + mrand "math/rand/v2" "os" "path" "reflect" @@ -26,8 +27,12 @@ import ( "unicode" "github.com/go-jose/go-jose/v4" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/letsencrypt/boulder/identifier" ) const Unspecified = "Unspecified" @@ -316,11 +321,15 @@ func UniqueLowerNames(names []string) (unique []string) { return } -// HashNames returns a hash of the names requested. This is intended for use -// when interacting with the orderFqdnSets table and rate limiting. -func HashNames(names []string) []byte { - names = UniqueLowerNames(names) - hash := sha256.Sum256([]byte(strings.Join(names, ","))) +// HashIdentifiers returns a hash of the identifiers requested. This is intended +// for use when interacting with the orderFqdnSets table and rate limiting. +func HashIdentifiers(idents identifier.ACMEIdentifiers) []byte { + var values []string + for _, ident := range identifier.Normalize(idents) { + values = append(values, ident.Value) + } + + hash := sha256.Sum256([]byte(strings.Join(values, ","))) return hash[:] } @@ -378,6 +387,14 @@ func IsASCII(str string) bool { return true } +// IsCanceled 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 IsCanceled(err error) bool { + return errors.Is(err, context.Canceled) || status.Code(err) == codes.Canceled +} + func Command() string { return path.Base(os.Args[0]) } diff --git a/third-party/github.com/letsencrypt/boulder/core/util_test.go b/third-party/github.com/letsencrypt/boulder/core/util_test.go index 294f555a3..7cae9ff7b 100644 --- a/third-party/github.com/letsencrypt/boulder/core/util_test.go +++ b/third-party/github.com/letsencrypt/boulder/core/util_test.go @@ -1,21 +1,27 @@ package core import ( - "bytes" + "context" "encoding/json" + "errors" "fmt" "math" "math/big" + "net/netip" "os" + "slices" "sort" "strings" "testing" "time" "github.com/go-jose/go-jose/v4" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/timestamppb" + "github.com/letsencrypt/boulder/identifier" "github.com/letsencrypt/boulder/test" ) @@ -315,29 +321,108 @@ func TestRetryBackoff(t *testing.T) { } -func TestHashNames(t *testing.T) { - // Test that it is deterministic - h1 := HashNames([]string{"a"}) - h2 := HashNames([]string{"a"}) - test.AssertByteEquals(t, h1, h2) +func TestHashIdentifiers(t *testing.T) { + dns1 := identifier.NewDNS("example.com") + dns1_caps := identifier.NewDNS("eXaMpLe.COM") + dns2 := identifier.NewDNS("high-energy-cheese-lab.nrc-cnrc.gc.ca") + dns2_caps := identifier.NewDNS("HIGH-ENERGY-CHEESE-LAB.NRC-CNRC.GC.CA") + ipv4_1 := identifier.NewIP(netip.MustParseAddr("10.10.10.10")) + ipv4_2 := identifier.NewIP(netip.MustParseAddr("172.16.16.16")) + ipv6_1 := identifier.NewIP(netip.MustParseAddr("2001:0db8:0bad:0dab:c0ff:fee0:0007:1337")) + ipv6_2 := identifier.NewIP(netip.MustParseAddr("3fff::")) - // Test that it differentiates - h1 = HashNames([]string{"a"}) - h2 = HashNames([]string{"b"}) - test.Assert(t, !bytes.Equal(h1, h2), "Should have been different") + testCases := []struct { + Name string + Idents1 identifier.ACMEIdentifiers + Idents2 identifier.ACMEIdentifiers + ExpectedEqual bool + }{ + { + Name: "Deterministic for DNS", + Idents1: identifier.ACMEIdentifiers{dns1}, + Idents2: identifier.ACMEIdentifiers{dns1}, + ExpectedEqual: true, + }, + { + Name: "Deterministic for IPv4", + Idents1: identifier.ACMEIdentifiers{ipv4_1}, + Idents2: identifier.ACMEIdentifiers{ipv4_1}, + ExpectedEqual: true, + }, + { + Name: "Deterministic for IPv6", + Idents1: identifier.ACMEIdentifiers{ipv6_1}, + Idents2: identifier.ACMEIdentifiers{ipv6_1}, + ExpectedEqual: true, + }, + { + Name: "Differentiates for DNS", + Idents1: identifier.ACMEIdentifiers{dns1}, + Idents2: identifier.ACMEIdentifiers{dns2}, + ExpectedEqual: false, + }, + { + Name: "Differentiates for IPv4", + Idents1: identifier.ACMEIdentifiers{ipv4_1}, + Idents2: identifier.ACMEIdentifiers{ipv4_2}, + ExpectedEqual: false, + }, + { + Name: "Differentiates for IPv6", + Idents1: identifier.ACMEIdentifiers{ipv6_1}, + Idents2: identifier.ACMEIdentifiers{ipv6_2}, + ExpectedEqual: false, + }, + { + Name: "Not subject to ordering", + Idents1: identifier.ACMEIdentifiers{ + dns1, dns2, ipv4_1, ipv4_2, ipv6_1, ipv6_2, + }, + Idents2: identifier.ACMEIdentifiers{ + ipv6_1, dns2, ipv4_2, dns1, ipv4_1, ipv6_2, + }, + ExpectedEqual: true, + }, + { + Name: "Not case sensitive", + Idents1: identifier.ACMEIdentifiers{ + dns1, dns2, + }, + Idents2: identifier.ACMEIdentifiers{ + dns1_caps, dns2_caps, + }, + ExpectedEqual: true, + }, + { + Name: "Not subject to duplication", + Idents1: identifier.ACMEIdentifiers{ + dns1, dns1, + }, + Idents2: identifier.ACMEIdentifiers{dns1}, + ExpectedEqual: true, + }, + } - // Test that it is not subject to ordering - h1 = HashNames([]string{"a", "b"}) - h2 = HashNames([]string{"b", "a"}) - test.AssertByteEquals(t, h1, h2) - - // Test that it is not subject to case - h1 = HashNames([]string{"a", "b"}) - h2 = HashNames([]string{"A", "B"}) - test.AssertByteEquals(t, h1, h2) - - // Test that it is not subject to duplication - h1 = HashNames([]string{"a", "a"}) - h2 = HashNames([]string{"a"}) - test.AssertByteEquals(t, h1, h2) + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + t.Parallel() + h1 := HashIdentifiers(tc.Idents1) + h2 := HashIdentifiers(tc.Idents2) + if slices.Equal(h1, h2) != tc.ExpectedEqual { + t.Errorf("Comparing hashes of idents %#v and %#v, expected equality to be %v", tc.Idents1, tc.Idents2, tc.ExpectedEqual) + } + }) + } +} + +func TestIsCanceled(t *testing.T) { + if !IsCanceled(context.Canceled) { + t.Errorf("Expected context.Canceled to be canceled, but wasn't.") + } + if !IsCanceled(status.Errorf(codes.Canceled, "hi")) { + t.Errorf("Expected gRPC cancellation to be canceled, but wasn't.") + } + if IsCanceled(errors.New("hi")) { + t.Errorf("Expected random error to not be canceled, but was.") + } } diff --git a/third-party/github.com/letsencrypt/boulder/crl/checker/checker.go b/third-party/github.com/letsencrypt/boulder/crl/checker/checker.go index 9bceb308f..08a1add8f 100644 --- a/third-party/github.com/letsencrypt/boulder/crl/checker/checker.go +++ b/third-party/github.com/letsencrypt/boulder/crl/checker/checker.go @@ -59,11 +59,11 @@ func Diff(old, new *x509.RevocationList) (*diffResult, error) { return nil, fmt.Errorf("CRLs were not issued by same issuer") } - if !old.ThisUpdate.Before(new.ThisUpdate) { + if old.Number.Cmp(new.Number) >= 0 { return nil, fmt.Errorf("old CRL does not precede new CRL") } - if old.Number.Cmp(new.Number) >= 0 { + if new.ThisUpdate.Before(old.ThisUpdate) { return nil, fmt.Errorf("old CRL does not precede new CRL") } diff --git a/third-party/github.com/letsencrypt/boulder/crl/idp/idp.go b/third-party/github.com/letsencrypt/boulder/crl/idp/idp.go index b329d4383..2ed835dfd 100644 --- a/third-party/github.com/letsencrypt/boulder/crl/idp/idp.go +++ b/third-party/github.com/letsencrypt/boulder/crl/idp/idp.go @@ -23,7 +23,7 @@ type issuingDistributionPoint struct { // others are omitted. type distributionPointName struct { // Technically, FullName is of type GeneralNames, which is of type SEQUENCE OF - // GeneralName. But GeneralName itself is of type CHOICE, and the asn1.Marhsal + // GeneralName. But GeneralName itself is of type CHOICE, and the asn1.Marshal // function doesn't support marshalling structs to CHOICEs, so we have to use // asn1.RawValue and encode the GeneralName ourselves. FullName []asn1.RawValue `asn1:"optional,tag:0"` diff --git a/third-party/github.com/letsencrypt/boulder/crl/idp/idp_test.go b/third-party/github.com/letsencrypt/boulder/crl/idp/idp_test.go index a142a5913..904a3586f 100644 --- a/third-party/github.com/letsencrypt/boulder/crl/idp/idp_test.go +++ b/third-party/github.com/letsencrypt/boulder/crl/idp/idp_test.go @@ -27,7 +27,6 @@ func TestMakeUserCertsExt(t *testing.T) { }, } for _, tc := range tests { - tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() got, err := MakeUserCertsExt(tc.urls) diff --git a/third-party/github.com/letsencrypt/boulder/crl/storer/proto/storer.pb.go b/third-party/github.com/letsencrypt/boulder/crl/storer/proto/storer.pb.go index ba95c8ab1..7484333fc 100644 --- a/third-party/github.com/letsencrypt/boulder/crl/storer/proto/storer.pb.go +++ b/third-party/github.com/letsencrypt/boulder/crl/storer/proto/storer.pb.go @@ -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: storer.proto @@ -10,8 +10,10 @@ import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" emptypb "google.golang.org/protobuf/types/known/emptypb" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" reflect "reflect" sync "sync" + unsafe "unsafe" ) const ( @@ -22,24 +24,21 @@ const ( ) type UploadCRLRequest 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: // // *UploadCRLRequest_Metadata // *UploadCRLRequest_CrlChunk - Payload isUploadCRLRequest_Payload `protobuf_oneof:"payload"` + Payload isUploadCRLRequest_Payload `protobuf_oneof:"payload"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *UploadCRLRequest) Reset() { *x = UploadCRLRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_storer_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_storer_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *UploadCRLRequest) String() string { @@ -50,7 +49,7 @@ func (*UploadCRLRequest) ProtoMessage() {} func (x *UploadCRLRequest) ProtoReflect() protoreflect.Message { mi := &file_storer_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) @@ -65,23 +64,27 @@ func (*UploadCRLRequest) Descriptor() ([]byte, []int) { return file_storer_proto_rawDescGZIP(), []int{0} } -func (m *UploadCRLRequest) GetPayload() isUploadCRLRequest_Payload { - if m != nil { - return m.Payload +func (x *UploadCRLRequest) GetPayload() isUploadCRLRequest_Payload { + if x != nil { + return x.Payload } return nil } func (x *UploadCRLRequest) GetMetadata() *CRLMetadata { - if x, ok := x.GetPayload().(*UploadCRLRequest_Metadata); ok { - return x.Metadata + if x != nil { + if x, ok := x.Payload.(*UploadCRLRequest_Metadata); ok { + return x.Metadata + } } return nil } func (x *UploadCRLRequest) GetCrlChunk() []byte { - if x, ok := x.GetPayload().(*UploadCRLRequest_CrlChunk); ok { - return x.CrlChunk + if x != nil { + if x, ok := x.Payload.(*UploadCRLRequest_CrlChunk); ok { + return x.CrlChunk + } } return nil } @@ -103,22 +106,21 @@ func (*UploadCRLRequest_Metadata) isUploadCRLRequest_Payload() {} func (*UploadCRLRequest_CrlChunk) isUploadCRLRequest_Payload() {} type CRLMetadata struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + IssuerNameID int64 `protobuf:"varint,1,opt,name=issuerNameID,proto3" json:"issuerNameID,omitempty"` + Number int64 `protobuf:"varint,2,opt,name=number,proto3" json:"number,omitempty"` + ShardIdx int64 `protobuf:"varint,3,opt,name=shardIdx,proto3" json:"shardIdx,omitempty"` + Expires *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=expires,proto3" json:"expires,omitempty"` + CacheControl string `protobuf:"bytes,5,opt,name=cacheControl,proto3" json:"cacheControl,omitempty"` unknownFields protoimpl.UnknownFields - - IssuerNameID int64 `protobuf:"varint,1,opt,name=issuerNameID,proto3" json:"issuerNameID,omitempty"` - Number int64 `protobuf:"varint,2,opt,name=number,proto3" json:"number,omitempty"` - ShardIdx int64 `protobuf:"varint,3,opt,name=shardIdx,proto3" json:"shardIdx,omitempty"` + sizeCache protoimpl.SizeCache } func (x *CRLMetadata) Reset() { *x = CRLMetadata{} - if protoimpl.UnsafeEnabled { - mi := &file_storer_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_storer_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *CRLMetadata) String() string { @@ -129,7 +131,7 @@ func (*CRLMetadata) ProtoMessage() {} func (x *CRLMetadata) ProtoReflect() protoreflect.Message { mi := &file_storer_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) @@ -165,64 +167,88 @@ func (x *CRLMetadata) GetShardIdx() int64 { return 0 } +func (x *CRLMetadata) GetExpires() *timestamppb.Timestamp { + if x != nil { + return x.Expires + } + return nil +} + +func (x *CRLMetadata) GetCacheControl() string { + if x != nil { + return x.CacheControl + } + return "" +} + var File_storer_proto protoreflect.FileDescriptor -var file_storer_proto_rawDesc = []byte{ +var file_storer_proto_rawDesc = string([]byte{ 0x0a, 0x0c, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x06, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x72, 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, - 0x6f, 0x74, 0x6f, 0x22, 0x6e, 0x0a, 0x10, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x43, 0x52, 0x4c, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x73, 0x74, 0x6f, 0x72, - 0x65, 0x72, 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, 0x1c, 0x0a, 0x08, 0x63, 0x72, - 0x6c, 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x00, 0x52, 0x08, - 0x63, 0x72, 0x6c, 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x42, 0x09, 0x0a, 0x07, 0x70, 0x61, 0x79, 0x6c, - 0x6f, 0x61, 0x64, 0x22, 0x65, 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, 0x16, 0x0a, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 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, 0x32, 0x4e, 0x0a, 0x09, 0x43, 0x52, - 0x4c, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x72, 0x12, 0x41, 0x0a, 0x09, 0x55, 0x70, 0x6c, 0x6f, 0x61, - 0x64, 0x43, 0x52, 0x4c, 0x12, 0x18, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x72, 0x2e, 0x55, 0x70, - 0x6c, 0x6f, 0x61, 0x64, 0x43, 0x52, 0x4c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, - 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x28, 0x01, 0x42, 0x31, 0x5a, 0x2f, 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, 0x72, 0x6c, - 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x33, -} + 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x6e, 0x0a, 0x10, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x43, 0x52, + 0x4c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x73, 0x74, 0x6f, + 0x72, 0x65, 0x72, 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, 0x1c, 0x0a, 0x08, 0x63, + 0x72, 0x6c, 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x00, 0x52, + 0x08, 0x63, 0x72, 0x6c, 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x42, 0x09, 0x0a, 0x07, 0x70, 0x61, 0x79, + 0x6c, 0x6f, 0x61, 0x64, 0x22, 0xbf, 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, 0x16, 0x0a, 0x06, 0x6e, 0x75, 0x6d, 0x62, + 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, + 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, 0x12, 0x34, 0x0a, 0x07, + 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 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, 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, + 0x65, 0x73, 0x12, 0x22, 0x0a, 0x0c, 0x63, 0x61, 0x63, 0x68, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x72, + 0x6f, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x63, 0x61, 0x63, 0x68, 0x65, 0x43, + 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x32, 0x4e, 0x0a, 0x09, 0x43, 0x52, 0x4c, 0x53, 0x74, 0x6f, + 0x72, 0x65, 0x72, 0x12, 0x41, 0x0a, 0x09, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x43, 0x52, 0x4c, + 0x12, 0x18, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x72, 0x2e, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, + 0x43, 0x52, 0x4c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, + 0x74, 0x79, 0x22, 0x00, 0x28, 0x01, 0x42, 0x31, 0x5a, 0x2f, 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, 0x72, 0x6c, 0x2f, 0x73, 0x74, 0x6f, + 0x72, 0x65, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, +}) var ( file_storer_proto_rawDescOnce sync.Once - file_storer_proto_rawDescData = file_storer_proto_rawDesc + file_storer_proto_rawDescData []byte ) func file_storer_proto_rawDescGZIP() []byte { file_storer_proto_rawDescOnce.Do(func() { - file_storer_proto_rawDescData = protoimpl.X.CompressGZIP(file_storer_proto_rawDescData) + file_storer_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_storer_proto_rawDesc), len(file_storer_proto_rawDesc))) }) return file_storer_proto_rawDescData } var file_storer_proto_msgTypes = make([]protoimpl.MessageInfo, 2) -var file_storer_proto_goTypes = []interface{}{ - (*UploadCRLRequest)(nil), // 0: storer.UploadCRLRequest - (*CRLMetadata)(nil), // 1: storer.CRLMetadata - (*emptypb.Empty)(nil), // 2: google.protobuf.Empty +var file_storer_proto_goTypes = []any{ + (*UploadCRLRequest)(nil), // 0: storer.UploadCRLRequest + (*CRLMetadata)(nil), // 1: storer.CRLMetadata + (*timestamppb.Timestamp)(nil), // 2: google.protobuf.Timestamp + (*emptypb.Empty)(nil), // 3: google.protobuf.Empty } var file_storer_proto_depIdxs = []int32{ 1, // 0: storer.UploadCRLRequest.metadata:type_name -> storer.CRLMetadata - 0, // 1: storer.CRLStorer.UploadCRL:input_type -> storer.UploadCRLRequest - 2, // 2: storer.CRLStorer.UploadCRL:output_type -> google.protobuf.Empty - 2, // [2:3] is the sub-list for method output_type - 1, // [1:2] is the sub-list for method input_type - 1, // [1:1] is the sub-list for extension type_name - 1, // [1:1] is the sub-list for extension extendee - 0, // [0:1] is the sub-list for field type_name + 2, // 1: storer.CRLMetadata.expires:type_name -> google.protobuf.Timestamp + 0, // 2: storer.CRLStorer.UploadCRL:input_type -> storer.UploadCRLRequest + 3, // 3: storer.CRLStorer.UploadCRL:output_type -> google.protobuf.Empty + 3, // [3:4] is the sub-list for method output_type + 2, // [2:3] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name } func init() { file_storer_proto_init() } @@ -230,33 +256,7 @@ func file_storer_proto_init() { if File_storer_proto != nil { return } - if !protoimpl.UnsafeEnabled { - file_storer_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UploadCRLRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_storer_proto_msgTypes[1].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_storer_proto_msgTypes[0].OneofWrappers = []interface{}{ + file_storer_proto_msgTypes[0].OneofWrappers = []any{ (*UploadCRLRequest_Metadata)(nil), (*UploadCRLRequest_CrlChunk)(nil), } @@ -264,7 +264,7 @@ func file_storer_proto_init() { out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_storer_proto_rawDesc, + RawDescriptor: unsafe.Slice(unsafe.StringData(file_storer_proto_rawDesc), len(file_storer_proto_rawDesc)), NumEnums: 0, NumMessages: 2, NumExtensions: 0, @@ -275,7 +275,6 @@ func file_storer_proto_init() { MessageInfos: file_storer_proto_msgTypes, }.Build() File_storer_proto = out.File - file_storer_proto_rawDesc = nil file_storer_proto_goTypes = nil file_storer_proto_depIdxs = nil } diff --git a/third-party/github.com/letsencrypt/boulder/crl/storer/proto/storer.proto b/third-party/github.com/letsencrypt/boulder/crl/storer/proto/storer.proto index 451d61165..fa5f55c54 100644 --- a/third-party/github.com/letsencrypt/boulder/crl/storer/proto/storer.proto +++ b/third-party/github.com/letsencrypt/boulder/crl/storer/proto/storer.proto @@ -4,6 +4,7 @@ package storer; option go_package = "github.com/letsencrypt/boulder/crl/storer/proto"; import "google/protobuf/empty.proto"; +import "google/protobuf/timestamp.proto"; service CRLStorer { rpc UploadCRL(stream UploadCRLRequest) returns (google.protobuf.Empty) {} @@ -20,4 +21,6 @@ message CRLMetadata { int64 issuerNameID = 1; int64 number = 2; int64 shardIdx = 3; + google.protobuf.Timestamp expires = 4; + string cacheControl = 5; } diff --git a/third-party/github.com/letsencrypt/boulder/crl/storer/proto/storer_grpc.pb.go b/third-party/github.com/letsencrypt/boulder/crl/storer/proto/storer_grpc.pb.go index 06e8b0c7d..32c9e128e 100644 --- a/third-party/github.com/letsencrypt/boulder/crl/storer/proto/storer_grpc.pb.go +++ b/third-party/github.com/letsencrypt/boulder/crl/storer/proto/storer_grpc.pb.go @@ -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: storer.proto @@ -53,20 +53,24 @@ type CRLStorer_UploadCRLClient = grpc.ClientStreamingClient[UploadCRLRequest, em // CRLStorerServer is the server API for CRLStorer service. // All implementations must embed UnimplementedCRLStorerServer -// for forward compatibility +// for forward compatibility. type CRLStorerServer interface { UploadCRL(grpc.ClientStreamingServer[UploadCRLRequest, emptypb.Empty]) error mustEmbedUnimplementedCRLStorerServer() } -// UnimplementedCRLStorerServer must be embedded to have forward compatible implementations. -type UnimplementedCRLStorerServer struct { -} +// UnimplementedCRLStorerServer 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 UnimplementedCRLStorerServer struct{} func (UnimplementedCRLStorerServer) UploadCRL(grpc.ClientStreamingServer[UploadCRLRequest, emptypb.Empty]) error { return status.Errorf(codes.Unimplemented, "method UploadCRL not implemented") } func (UnimplementedCRLStorerServer) mustEmbedUnimplementedCRLStorerServer() {} +func (UnimplementedCRLStorerServer) testEmbeddedByValue() {} // UnsafeCRLStorerServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to CRLStorerServer will @@ -76,6 +80,13 @@ type UnsafeCRLStorerServer interface { } func RegisterCRLStorerServer(s grpc.ServiceRegistrar, srv CRLStorerServer) { + // If the following call pancis, it indicates UnimplementedCRLStorerServer 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(&CRLStorer_ServiceDesc, srv) } diff --git a/third-party/github.com/letsencrypt/boulder/crl/storer/storer.go b/third-party/github.com/letsencrypt/boulder/crl/storer/storer.go index 9b41f560f..5896da2ac 100644 --- a/third-party/github.com/letsencrypt/boulder/crl/storer/storer.go +++ b/third-party/github.com/letsencrypt/boulder/crl/storer/storer.go @@ -105,6 +105,8 @@ func (cs *crlStorer) UploadCRL(stream grpc.ClientStreamingServer[cspb.UploadCRLR var shardIdx int64 var crlNumber *big.Int crlBytes := make([]byte, 0) + var cacheControl string + var expires time.Time // Read all of the messages from the input stream. for { @@ -125,6 +127,9 @@ func (cs *crlStorer) UploadCRL(stream grpc.ClientStreamingServer[cspb.UploadCRLR return errors.New("got incomplete metadata message") } + cacheControl = payload.Metadata.CacheControl + expires = payload.Metadata.Expires.AsTime() + shardIdx = payload.Metadata.ShardIdx crlNumber = crl.Number(time.Unix(0, payload.Metadata.Number)) @@ -229,6 +234,8 @@ func (cs *crlStorer) UploadCRL(stream grpc.ClientStreamingServer[cspb.UploadCRLR ChecksumSHA256: &checksumb64, ContentType: &crlContentType, Metadata: map[string]string{"crlNumber": crlNumber.String()}, + Expires: &expires, + CacheControl: &cacheControl, }) latency := cs.clk.Now().Sub(start) diff --git a/third-party/github.com/letsencrypt/boulder/crl/updater/batch_test.go b/third-party/github.com/letsencrypt/boulder/crl/updater/batch_test.go index 26907ecc0..97b93f833 100644 --- a/third-party/github.com/letsencrypt/boulder/crl/updater/batch_test.go +++ b/third-party/github.com/letsencrypt/boulder/crl/updater/batch_test.go @@ -26,9 +26,12 @@ func TestRunOnce(t *testing.T) { []*issuance.Certificate{e1, r3}, 2, 18*time.Hour, 24*time.Hour, 6*time.Hour, time.Minute, 1, 1, - &fakeSAC{grcc: fakeGRCC{err: errors.New("db no worky")}, maxNotAfter: clk.Now().Add(90 * 24 * time.Hour)}, - &fakeCGC{gcc: fakeGCC{}}, - &fakeCSC{ucc: fakeUCC{}}, + "stale-if-error=60", + 5*time.Minute, + nil, + &fakeSAC{revokedCerts: revokedCertsStream{err: errors.New("db no worky")}, maxNotAfter: clk.Now().Add(90 * 24 * time.Hour)}, + &fakeCA{gcc: generateCRLStream{}}, + &fakeStorer{uploaderStream: &noopUploader{}}, metrics.NoopRegisterer, mockLog, clk, ) test.AssertNotError(t, err, "building test crlUpdater") diff --git a/third-party/github.com/letsencrypt/boulder/crl/updater/continuous.go b/third-party/github.com/letsencrypt/boulder/crl/updater/continuous.go index e4552f68f..4597fd60a 100644 --- a/third-party/github.com/letsencrypt/boulder/crl/updater/continuous.go +++ b/third-party/github.com/letsencrypt/boulder/crl/updater/continuous.go @@ -2,7 +2,7 @@ package updater import ( "context" - "math/rand" + "math/rand/v2" "sync" "time" @@ -21,7 +21,7 @@ func (cu *crlUpdater) Run(ctx context.Context) error { // Wait for a random number of nanoseconds less than the updatePeriod, so // that process restarts do not skip or delay shards deterministically. - waitTimer := time.NewTimer(time.Duration(rand.Int63n(cu.updatePeriod.Nanoseconds()))) + waitTimer := time.NewTimer(time.Duration(rand.Int64N(cu.updatePeriod.Nanoseconds()))) defer waitTimer.Stop() select { case <-waitTimer.C: diff --git a/third-party/github.com/letsencrypt/boulder/crl/updater/updater.go b/third-party/github.com/letsencrypt/boulder/crl/updater/updater.go index c5790b72b..600b17f22 100644 --- a/third-party/github.com/letsencrypt/boulder/crl/updater/updater.go +++ b/third-party/github.com/letsencrypt/boulder/crl/updater/updater.go @@ -7,10 +7,12 @@ import ( "fmt" "io" "math" + "slices" "time" "github.com/jmhodges/clock" "github.com/prometheus/client_golang/prometheus" + "golang.org/x/crypto/ocsp" "google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/timestamppb" @@ -34,6 +36,11 @@ type crlUpdater struct { maxParallelism int maxAttempts int + cacheControl string + expiresMargin time.Duration + + temporallyShardedPrefixes []string + sa sapb.StorageAuthorityClient ca capb.CRLGeneratorClient cs cspb.CRLStorerClient @@ -54,6 +61,9 @@ func NewUpdater( updateTimeout time.Duration, maxParallelism int, maxAttempts int, + cacheControl string, + expiresMargin time.Duration, + temporallyShardedPrefixes []string, sa sapb.StorageAuthorityClient, ca capb.CRLGeneratorClient, cs cspb.CRLStorerClient, @@ -70,8 +80,8 @@ func NewUpdater( return nil, fmt.Errorf("must have positive number of shards, got: %d", numShards) } - if updatePeriod >= 7*24*time.Hour { - return nil, fmt.Errorf("must update CRLs at least every 7 days, got: %s", updatePeriod) + if updatePeriod >= 24*time.Hour { + return nil, fmt.Errorf("must update CRLs at least every 24 hours, got: %s", updatePeriod) } if updateTimeout >= updatePeriod { @@ -112,6 +122,9 @@ func NewUpdater( updateTimeout, maxParallelism, maxAttempts, + cacheControl, + expiresMargin, + temporallyShardedPrefixes, sa, ca, cs, @@ -125,9 +138,9 @@ func NewUpdater( // updateShardWithRetry calls updateShard repeatedly (with exponential backoff // between attempts) until it succeeds or the max number of attempts is reached. func (cu *crlUpdater) updateShardWithRetry(ctx context.Context, atTime time.Time, issuerNameID issuance.NameID, shardIdx int, chunks []chunk) error { - ctx, cancel := context.WithTimeout(ctx, cu.updateTimeout) + deadline := cu.clk.Now().Add(cu.updateTimeout) + ctx, cancel := context.WithDeadline(ctx, deadline) defer cancel() - deadline, _ := ctx.Deadline() if chunks == nil { // Compute the shard map and relevant chunk boundaries, if not supplied. @@ -183,11 +196,78 @@ func (cu *crlUpdater) updateShardWithRetry(ctx context.Context, atTime time.Time return nil } +type crlStream interface { + Recv() (*proto.CRLEntry, error) +} + +// reRevoked returns the later of the two entries, only if the latter represents a valid +// re-revocation of the former (reason == KeyCompromise). +func reRevoked(a *proto.CRLEntry, b *proto.CRLEntry) (*proto.CRLEntry, error) { + first, second := a, b + if b.RevokedAt.AsTime().Before(a.RevokedAt.AsTime()) { + first, second = b, a + } + if first.Reason != ocsp.KeyCompromise && second.Reason == ocsp.KeyCompromise { + return second, nil + } + // The RA has logic to prevent re-revocation for any reason other than KeyCompromise, + // so this should be impossible. The best we can do is error out. + return nil, fmt.Errorf("certificate %s was revoked with reason %d at %s and re-revoked with invalid reason %d at %s", + first.Serial, first.Reason, first.RevokedAt.AsTime(), second.Reason, second.RevokedAt.AsTime()) +} + +// addFromStream pulls `proto.CRLEntry` objects from a stream, adding them to the crlEntries map. +// +// Consolidates duplicates and checks for internal consistency of the results. +// If allowedSerialPrefixes is non-empty, only serials with that one-byte prefix (two hex-encoded +// bytes) will be accepted. +// +// Returns the number of entries received from the stream, regardless of whether they were accepted. +func addFromStream(crlEntries map[string]*proto.CRLEntry, stream crlStream, allowedSerialPrefixes []string) (int, error) { + var count int + for { + entry, err := stream.Recv() + if err != nil { + if err == io.EOF { + break + } + return 0, fmt.Errorf("retrieving entry from SA: %w", err) + } + count++ + serialPrefix := entry.Serial[0:2] + if len(allowedSerialPrefixes) > 0 && !slices.Contains(allowedSerialPrefixes, serialPrefix) { + continue + } + previousEntry := crlEntries[entry.Serial] + if previousEntry == nil { + crlEntries[entry.Serial] = entry + continue + } + if previousEntry.Reason == entry.Reason && + previousEntry.RevokedAt.AsTime().Equal(entry.RevokedAt.AsTime()) { + continue + } + + // There's a tiny possibility a certificate was re-revoked for KeyCompromise and + // we got a different view of it from temporal sharding vs explicit sharding. + // Prefer the re-revoked CRL entry, which must be the one with KeyCompromise. + second, err := reRevoked(entry, previousEntry) + if err != nil { + return 0, err + } + crlEntries[entry.Serial] = second + } + return count, nil +} + // updateShard processes a single shard. It computes the shard's boundaries, gets // the list of revoked certs in that shard from the SA, gets the CA to sign the // resulting CRL, and gets the crl-storer to upload it. It returns an error if // any of these operations fail. func (cu *crlUpdater) updateShard(ctx context.Context, atTime time.Time, issuerNameID issuance.NameID, shardIdx int, chunks []chunk) (err error) { + if shardIdx <= 0 { + return fmt.Errorf("invalid shard %d", shardIdx) + } ctx, cancel := context.WithCancel(ctx) defer cancel() @@ -207,8 +287,10 @@ func (cu *crlUpdater) updateShard(ctx context.Context, atTime time.Time, issuerN cu.log.Infof( "Generating CRL shard: id=[%s] numChunks=[%d]", crlID, len(chunks)) - // Get the full list of CRL Entries for this shard from the SA. - var crlEntries []*proto.CRLEntry + // Deduplicate the CRL entries by serial number, since we can get the same certificate via + // both temporal sharding (GetRevokedCerts) and explicit sharding (GetRevokedCertsByShard). + crlEntries := make(map[string]*proto.CRLEntry) + for _, chunk := range chunks { saStream, err := cu.sa.GetRevokedCerts(ctx, &sapb.GetRevokedCertsRequest{ IssuerNameID: int64(issuerNameID), @@ -217,25 +299,41 @@ func (cu *crlUpdater) updateShard(ctx context.Context, atTime time.Time, issuerN RevokedBefore: timestamppb.New(atTime), }) if err != nil { - return fmt.Errorf("connecting to SA: %w", err) + return fmt.Errorf("GetRevokedCerts: %w", err) } - for { - entry, err := saStream.Recv() - if err != nil { - if err == io.EOF { - break - } - return fmt.Errorf("retrieving entry from SA: %w", err) - } - crlEntries = append(crlEntries, entry) + n, err := addFromStream(crlEntries, saStream, cu.temporallyShardedPrefixes) + if err != nil { + return fmt.Errorf("streaming GetRevokedCerts: %w", err) } cu.log.Infof( "Queried SA for CRL shard: id=[%s] expiresAfter=[%s] expiresBefore=[%s] numEntries=[%d]", - crlID, chunk.start, chunk.end, len(crlEntries)) + crlID, chunk.start, chunk.end, n) } + // Query for unexpired certificates, with padding to ensure that revoked certificates show + // up in at least one CRL, even if they expire between revocation and CRL generation. + expiresAfter := cu.clk.Now().Add(-cu.lookbackPeriod) + + saStream, err := cu.sa.GetRevokedCertsByShard(ctx, &sapb.GetRevokedCertsByShardRequest{ + IssuerNameID: int64(issuerNameID), + ShardIdx: int64(shardIdx), + ExpiresAfter: timestamppb.New(expiresAfter), + RevokedBefore: timestamppb.New(atTime), + }) + if err != nil { + return fmt.Errorf("GetRevokedCertsByShard: %w", err) + } + + n, err := addFromStream(crlEntries, saStream, nil) + if err != nil { + return fmt.Errorf("streaming GetRevokedCertsByShard: %w", err) + } + + cu.log.Infof( + "Queried SA by CRL shard number: id=[%s] shardIdx=[%d] numEntries=[%d]", crlID, shardIdx, n) + // Send the full list of CRL Entries to the CA. caStream, err := cu.ca.GenerateCRL(ctx) if err != nil { @@ -301,6 +399,8 @@ func (cu *crlUpdater) updateShard(ctx context.Context, atTime time.Time, issuerN IssuerNameID: int64(issuerNameID), Number: atTime.UnixNano(), ShardIdx: int64(shardIdx), + CacheControl: cu.cacheControl, + Expires: timestamppb.New(atTime.Add(cu.updatePeriod).Add(cu.expiresMargin)), }, }, }) diff --git a/third-party/github.com/letsencrypt/boulder/crl/updater/updater_test.go b/third-party/github.com/letsencrypt/boulder/crl/updater/updater_test.go index 9b2b16108..d3c1f9595 100644 --- a/third-party/github.com/letsencrypt/boulder/crl/updater/updater_test.go +++ b/third-party/github.com/letsencrypt/boulder/crl/updater/updater_test.go @@ -1,12 +1,17 @@ package updater import ( + "bytes" "context" + "encoding/json" "errors" + "fmt" "io" + "reflect" "testing" "time" + "golang.org/x/crypto/ocsp" "google.golang.org/grpc" "google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/timestamppb" @@ -24,17 +29,17 @@ import ( "github.com/letsencrypt/boulder/test" ) -// fakeGRCC is a fake grpc.ClientStreamingClient which can be +// revokedCertsStream is a fake grpc.ClientStreamingClient which can be // populated with some CRL entries or an error for use as the return value of // a faked GetRevokedCerts call. -type fakeGRCC struct { +type revokedCertsStream struct { grpc.ClientStream entries []*corepb.CRLEntry nextIdx int err error } -func (f *fakeGRCC) Recv() (*corepb.CRLEntry, error) { +func (f *revokedCertsStream) Recv() (*corepb.CRLEntry, error) { if f.err != nil { return nil, f.err } @@ -51,13 +56,31 @@ func (f *fakeGRCC) Recv() (*corepb.CRLEntry, error) { // fake timestamp to serve as the database's maximum notAfter value. type fakeSAC struct { sapb.StorageAuthorityClient - grcc fakeGRCC - maxNotAfter time.Time - leaseError error + revokedCerts revokedCertsStream + revokedCertsByShard revokedCertsStream + maxNotAfter time.Time + leaseError error } func (f *fakeSAC) GetRevokedCerts(ctx context.Context, _ *sapb.GetRevokedCertsRequest, _ ...grpc.CallOption) (grpc.ServerStreamingClient[corepb.CRLEntry], error) { - return &f.grcc, nil + return &f.revokedCerts, nil +} + +// Return some configured contents, but only for shard 2. +func (f *fakeSAC) GetRevokedCertsByShard(ctx context.Context, req *sapb.GetRevokedCertsByShardRequest, _ ...grpc.CallOption) (grpc.ServerStreamingClient[corepb.CRLEntry], error) { + // This time is based on the setting of `clk` in TestUpdateShard, + // minus the setting of `lookbackPeriod` in that same function (24h). + want := time.Date(2020, time.January, 17, 0, 0, 0, 0, time.UTC) + got := req.ExpiresAfter.AsTime().UTC() + if !got.Equal(want) { + return nil, fmt.Errorf("fakeSAC.GetRevokedCertsByShard called with ExpiresAfter=%s, want %s", + got, want) + } + + if req.ShardIdx == 2 { + return &f.revokedCertsByShard, nil + } + return &revokedCertsStream{}, nil } func (f *fakeSAC) GetMaxExpiration(_ context.Context, req *emptypb.Empty, _ ...grpc.CallOption) (*timestamppb.Timestamp, error) { @@ -71,10 +94,20 @@ func (f *fakeSAC) LeaseCRLShard(_ context.Context, req *sapb.LeaseCRLShardReques return &sapb.LeaseCRLShardResponse{IssuerNameID: req.IssuerNameID, ShardIdx: req.MinShardIdx}, nil } -// fakeGCC is a fake grpc.BidiStreamingClient which can be -// populated with some CRL entries or an error for use as the return value of -// a faked GenerateCRL call. -type fakeGCC struct { +// generateCRLStream implements the streaming API returned from GenerateCRL. +// +// Specifically it implements grpc.BidiStreamingClient. +// +// If it has non-nil error fields, it returns those on Send() or Recv(). +// +// When it receives a CRL entry (on Send()), it records that entry internally, JSON serialized, +// with a newline between JSON objects. +// +// When it is asked for bytes of a signed CRL (Recv()), it sends those JSON serialized contents. +// +// We use JSON instead of CRL format because we're not testing the signing and formatting done +// by the CA, just the plumbing of different components together done by the crl-updater. +type generateCRLStream struct { grpc.ClientStream chunks [][]byte nextIdx int @@ -82,15 +115,36 @@ type fakeGCC struct { recvErr error } -func (f *fakeGCC) Send(*capb.GenerateCRLRequest) error { +type crlEntry struct { + Serial string + Reason int32 + RevokedAt time.Time +} + +func (f *generateCRLStream) Send(req *capb.GenerateCRLRequest) error { + if f.sendErr != nil { + return f.sendErr + } + if t, ok := req.Payload.(*capb.GenerateCRLRequest_Entry); ok { + jsonBytes, err := json.Marshal(crlEntry{ + Serial: t.Entry.Serial, + Reason: t.Entry.Reason, + RevokedAt: t.Entry.RevokedAt.AsTime(), + }) + if err != nil { + return err + } + f.chunks = append(f.chunks, jsonBytes) + f.chunks = append(f.chunks, []byte("\n")) + } return f.sendErr } -func (f *fakeGCC) CloseSend() error { +func (f *generateCRLStream) CloseSend() error { return nil } -func (f *fakeGCC) Recv() (*capb.GenerateCRLResponse, error) { +func (f *generateCRLStream) Recv() (*capb.GenerateCRLResponse, error) { if f.recvErr != nil { return nil, f.recvErr } @@ -102,43 +156,67 @@ func (f *fakeGCC) Recv() (*capb.GenerateCRLResponse, error) { return nil, io.EOF } -// fakeCGC is a fake capb.CRLGeneratorClient which can be populated with a -// fakeGCC to be used as the return value for calls to GenerateCRL. -type fakeCGC struct { - gcc fakeGCC +// fakeCA acts as a fake CA (specifically implementing capb.CRLGeneratorClient). +// +// It always returns its field in response to `GenerateCRL`. Because this is a streaming +// RPC, that return value is responsible for most of the work. +type fakeCA struct { + gcc generateCRLStream } -func (f *fakeCGC) GenerateCRL(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[capb.GenerateCRLRequest, capb.GenerateCRLResponse], error) { +func (f *fakeCA) GenerateCRL(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[capb.GenerateCRLRequest, capb.GenerateCRLResponse], error) { return &f.gcc, nil } -// fakeUCC is a fake grpc.ClientStreamingClient which can be populated with +// recordingUploader acts as the streaming part of UploadCRL. +// +// Records all uploaded chunks in crlBody. +type recordingUploader struct { + grpc.ClientStream + + crlBody []byte +} + +func (r *recordingUploader) Send(req *cspb.UploadCRLRequest) error { + if t, ok := req.Payload.(*cspb.UploadCRLRequest_CrlChunk); ok { + r.crlBody = append(r.crlBody, t.CrlChunk...) + } + return nil +} + +func (r *recordingUploader) CloseAndRecv() (*emptypb.Empty, error) { + return &emptypb.Empty{}, nil +} + +// noopUploader is a fake grpc.ClientStreamingClient which can be populated with // an error for use as the return value of a faked UploadCRL call. -type fakeUCC struct { +// +// It does nothing with uploaded contents. +type noopUploader struct { grpc.ClientStream sendErr error recvErr error } -func (f *fakeUCC) Send(*cspb.UploadCRLRequest) error { +func (f *noopUploader) Send(*cspb.UploadCRLRequest) error { return f.sendErr } -func (f *fakeUCC) CloseAndRecv() (*emptypb.Empty, error) { +func (f *noopUploader) CloseAndRecv() (*emptypb.Empty, error) { if f.recvErr != nil { return nil, f.recvErr } return &emptypb.Empty{}, nil } -// fakeCSC is a fake cspb.CRLStorerClient which can be populated with a -// fakeUCC for use as the return value for calls to UploadCRL. -type fakeCSC struct { - ucc fakeUCC +// fakeStorer is a fake cspb.CRLStorerClient which can be populated with an +// uploader stream for use as the return value for calls to UploadCRL. +type fakeStorer struct { + uploaderStream grpc.ClientStreamingClient[cspb.UploadCRLRequest, emptypb.Empty] } -func (f *fakeCSC) UploadCRL(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[cspb.UploadCRLRequest, emptypb.Empty], error) { - return &f.ucc, nil +func (f *fakeStorer) UploadCRL(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[cspb.UploadCRLRequest, emptypb.Empty], error) { + return f.uploaderStream, nil } func TestUpdateShard(t *testing.T) { @@ -152,14 +230,24 @@ func TestUpdateShard(t *testing.T) { defer cancel() clk := clock.NewFake() - clk.Set(time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC)) + clk.Set(time.Date(2020, time.January, 18, 0, 0, 0, 0, time.UTC)) cu, err := NewUpdater( []*issuance.Certificate{e1, r3}, - 2, 18*time.Hour, 24*time.Hour, - 6*time.Hour, time.Minute, 1, 1, - &fakeSAC{grcc: fakeGRCC{}, maxNotAfter: clk.Now().Add(90 * 24 * time.Hour)}, - &fakeCGC{gcc: fakeGCC{}}, - &fakeCSC{ucc: fakeUCC{}}, + 2, + 18*time.Hour, // shardWidth + 24*time.Hour, // lookbackPeriod + 6*time.Hour, // updatePeriod + time.Minute, // updateTimeout + 1, 1, + "stale-if-error=60", + 5*time.Minute, + nil, + &fakeSAC{ + revokedCerts: revokedCertsStream{}, + maxNotAfter: clk.Now().Add(90 * 24 * time.Hour), + }, + &fakeCA{gcc: generateCRLStream{}}, + &fakeStorer{uploaderStream: &noopUploader{}}, metrics.NoopRegisterer, blog.NewMock(), clk, ) test.AssertNotError(t, err, "building test crlUpdater") @@ -169,7 +257,91 @@ func TestUpdateShard(t *testing.T) { } // Ensure that getting no results from the SA still works. - err = cu.updateShard(ctx, cu.clk.Now(), e1.NameID(), 0, testChunks) + err = cu.updateShard(ctx, cu.clk.Now(), e1.NameID(), 1, testChunks) + test.AssertNotError(t, err, "empty CRL") + test.AssertMetricWithLabelsEquals(t, cu.updatedCounter, prometheus.Labels{ + "issuer": "(TEST) Elegant Elephant E1", "result": "success", + }, 1) + + // Make a CRL with actual contents. Verify that the information makes it through + // each of the steps: + // - read from SA + // - write to CA and read the response + // - upload with CRL storer + // + // The final response should show up in the bytes recorded by our fake storer. + recordingUploader := &recordingUploader{} + now := timestamppb.Now() + cu.cs = &fakeStorer{uploaderStream: recordingUploader} + cu.sa = &fakeSAC{ + revokedCerts: revokedCertsStream{ + entries: []*corepb.CRLEntry{ + { + Serial: "0311b5d430823cfa25b0fc85d14c54ee35", + Reason: int32(ocsp.KeyCompromise), + RevokedAt: now, + }, + }, + }, + revokedCertsByShard: revokedCertsStream{ + entries: []*corepb.CRLEntry{ + { + Serial: "0311b5d430823cfa25b0fc85d14c54ee35", + Reason: int32(ocsp.KeyCompromise), + RevokedAt: now, + }, + { + Serial: "037d6a05a0f6a975380456ae605cee9889", + Reason: int32(ocsp.AffiliationChanged), + RevokedAt: now, + }, + { + Serial: "03aa617ab8ee58896ba082bfa25199c884", + Reason: int32(ocsp.Unspecified), + RevokedAt: now, + }, + }, + }, + maxNotAfter: clk.Now().Add(90 * 24 * time.Hour), + } + // We ask for shard 2 specifically because GetRevokedCertsByShard only returns our + // certificate for that shard. + err = cu.updateShard(ctx, cu.clk.Now(), e1.NameID(), 2, testChunks) + test.AssertNotError(t, err, "updateShard") + + expectedEntries := map[string]int32{ + "0311b5d430823cfa25b0fc85d14c54ee35": int32(ocsp.KeyCompromise), + "037d6a05a0f6a975380456ae605cee9889": int32(ocsp.AffiliationChanged), + "03aa617ab8ee58896ba082bfa25199c884": int32(ocsp.Unspecified), + } + for _, r := range bytes.Split(recordingUploader.crlBody, []byte("\n")) { + if len(r) == 0 { + continue + } + var entry crlEntry + err := json.Unmarshal(r, &entry) + if err != nil { + t.Fatalf("unmarshaling JSON: %s", err) + } + expectedReason, ok := expectedEntries[entry.Serial] + if !ok { + t.Errorf("CRL entry for %s was unexpected", entry.Serial) + } + if entry.Reason != expectedReason { + t.Errorf("CRL entry for %s had reason=%d, want %d", entry.Serial, entry.Reason, expectedReason) + } + delete(expectedEntries, entry.Serial) + } + // At this point the expectedEntries map should be empty; if it's not, emit an error + // for each remaining expectation. + for k, v := range expectedEntries { + t.Errorf("expected cert %s to be revoked for reason=%d, but it was not on the CRL", k, v) + } + + cu.updatedCounter.Reset() + + // Ensure that getting no results from the SA still works. + err = cu.updateShard(ctx, cu.clk.Now(), e1.NameID(), 1, testChunks) test.AssertNotError(t, err, "empty CRL") test.AssertMetricWithLabelsEquals(t, cu.updatedCounter, prometheus.Labels{ "issuer": "(TEST) Elegant Elephant E1", "result": "success", @@ -177,8 +349,8 @@ func TestUpdateShard(t *testing.T) { cu.updatedCounter.Reset() // Errors closing the Storer upload stream should bubble up. - cu.cs = &fakeCSC{ucc: fakeUCC{recvErr: sentinelErr}} - err = cu.updateShard(ctx, cu.clk.Now(), e1.NameID(), 0, testChunks) + cu.cs = &fakeStorer{uploaderStream: &noopUploader{recvErr: sentinelErr}} + err = cu.updateShard(ctx, cu.clk.Now(), e1.NameID(), 1, testChunks) test.AssertError(t, err, "storer error") test.AssertContains(t, err.Error(), "closing CRLStorer upload stream") test.AssertErrorIs(t, err, sentinelErr) @@ -188,8 +360,8 @@ func TestUpdateShard(t *testing.T) { cu.updatedCounter.Reset() // Errors sending to the Storer should bubble up sooner. - cu.cs = &fakeCSC{ucc: fakeUCC{sendErr: sentinelErr}} - err = cu.updateShard(ctx, cu.clk.Now(), e1.NameID(), 0, testChunks) + cu.cs = &fakeStorer{uploaderStream: &noopUploader{sendErr: sentinelErr}} + err = cu.updateShard(ctx, cu.clk.Now(), e1.NameID(), 1, testChunks) test.AssertError(t, err, "storer error") test.AssertContains(t, err.Error(), "sending CRLStorer metadata") test.AssertErrorIs(t, err, sentinelErr) @@ -199,8 +371,8 @@ func TestUpdateShard(t *testing.T) { cu.updatedCounter.Reset() // Errors reading from the CA should bubble up sooner. - cu.ca = &fakeCGC{gcc: fakeGCC{recvErr: sentinelErr}} - err = cu.updateShard(ctx, cu.clk.Now(), e1.NameID(), 0, testChunks) + cu.ca = &fakeCA{gcc: generateCRLStream{recvErr: sentinelErr}} + err = cu.updateShard(ctx, cu.clk.Now(), e1.NameID(), 1, testChunks) test.AssertError(t, err, "CA error") test.AssertContains(t, err.Error(), "receiving CRL bytes") test.AssertErrorIs(t, err, sentinelErr) @@ -210,8 +382,8 @@ func TestUpdateShard(t *testing.T) { cu.updatedCounter.Reset() // Errors sending to the CA should bubble up sooner. - cu.ca = &fakeCGC{gcc: fakeGCC{sendErr: sentinelErr}} - err = cu.updateShard(ctx, cu.clk.Now(), e1.NameID(), 0, testChunks) + cu.ca = &fakeCA{gcc: generateCRLStream{sendErr: sentinelErr}} + err = cu.updateShard(ctx, cu.clk.Now(), e1.NameID(), 1, testChunks) test.AssertError(t, err, "CA error") test.AssertContains(t, err.Error(), "sending CA metadata") test.AssertErrorIs(t, err, sentinelErr) @@ -221,8 +393,8 @@ func TestUpdateShard(t *testing.T) { cu.updatedCounter.Reset() // Errors reading from the SA should bubble up soonest. - cu.sa = &fakeSAC{grcc: fakeGRCC{err: sentinelErr}, maxNotAfter: clk.Now().Add(90 * 24 * time.Hour)} - err = cu.updateShard(ctx, cu.clk.Now(), e1.NameID(), 0, testChunks) + cu.sa = &fakeSAC{revokedCerts: revokedCertsStream{err: sentinelErr}, maxNotAfter: clk.Now().Add(90 * 24 * time.Hour)} + err = cu.updateShard(ctx, cu.clk.Now(), e1.NameID(), 1, testChunks) test.AssertError(t, err, "database error") test.AssertContains(t, err.Error(), "retrieving entry from SA") test.AssertErrorIs(t, err, sentinelErr) @@ -250,9 +422,12 @@ func TestUpdateShardWithRetry(t *testing.T) { []*issuance.Certificate{e1, r3}, 2, 18*time.Hour, 24*time.Hour, 6*time.Hour, time.Minute, 1, 1, - &fakeSAC{grcc: fakeGRCC{err: sentinelErr}, maxNotAfter: clk.Now().Add(90 * 24 * time.Hour)}, - &fakeCGC{gcc: fakeGCC{}}, - &fakeCSC{ucc: fakeUCC{}}, + "stale-if-error=60", + 5*time.Minute, + nil, + &fakeSAC{revokedCerts: revokedCertsStream{err: sentinelErr}, maxNotAfter: clk.Now().Add(90 * 24 * time.Hour)}, + &fakeCA{gcc: generateCRLStream{}}, + &fakeStorer{uploaderStream: &noopUploader{}}, metrics.NoopRegisterer, blog.NewMock(), clk, ) test.AssertNotError(t, err, "building test crlUpdater") @@ -264,7 +439,7 @@ func TestUpdateShardWithRetry(t *testing.T) { // Ensure that having MaxAttempts set to 1 results in the clock not moving // forward at all. startTime := cu.clk.Now() - err = cu.updateShardWithRetry(ctx, cu.clk.Now(), e1.NameID(), 0, testChunks) + err = cu.updateShardWithRetry(ctx, cu.clk.Now(), e1.NameID(), 1, testChunks) test.AssertError(t, err, "database error") test.AssertErrorIs(t, err, sentinelErr) test.AssertEquals(t, cu.clk.Now(), startTime) @@ -274,7 +449,7 @@ func TestUpdateShardWithRetry(t *testing.T) { // in, so we have to be approximate. cu.maxAttempts = 5 startTime = cu.clk.Now() - err = cu.updateShardWithRetry(ctx, cu.clk.Now(), e1.NameID(), 0, testChunks) + err = cu.updateShardWithRetry(ctx, cu.clk.Now(), e1.NameID(), 1, testChunks) test.AssertError(t, err, "database error") test.AssertErrorIs(t, err, sentinelErr) t.Logf("start: %v", startTime) @@ -396,6 +571,150 @@ func TestGetChunkAtTime(t *testing.T) { // the time twice, since the whole point of "very far in the future" is that // it isn't representable by a time.Duration. atTime = anchorTime().Add(200 * 365 * 24 * time.Hour).Add(200 * 365 * 24 * time.Hour) - c, err = GetChunkAtTime(shardWidth, numShards, atTime) + _, err = GetChunkAtTime(shardWidth, numShards, atTime) test.AssertError(t, err, "getting far-future chunk") } + +func TestAddFromStream(t *testing.T) { + now := time.Now() + yesterday := now.Add(-24 * time.Hour) + simpleEntry := &corepb.CRLEntry{ + Serial: "abcdefg", + Reason: ocsp.CessationOfOperation, + RevokedAt: timestamppb.New(yesterday), + } + + reRevokedEntry := &corepb.CRLEntry{ + Serial: "abcdefg", + Reason: ocsp.KeyCompromise, + RevokedAt: timestamppb.New(now), + } + + reRevokedEntryOld := &corepb.CRLEntry{ + Serial: "abcdefg", + Reason: ocsp.KeyCompromise, + RevokedAt: timestamppb.New(now.Add(-48 * time.Hour)), + } + + reRevokedEntryBadReason := &corepb.CRLEntry{ + Serial: "abcdefg", + Reason: ocsp.AffiliationChanged, + RevokedAt: timestamppb.New(now), + } + + type testCase struct { + name string + inputs [][]*corepb.CRLEntry + expected map[string]*corepb.CRLEntry + expectErr bool + } + + testCases := []testCase{ + { + name: "two streams with same entry", + inputs: [][]*corepb.CRLEntry{ + {simpleEntry}, + {simpleEntry}, + }, + expected: map[string]*corepb.CRLEntry{ + simpleEntry.Serial: simpleEntry, + }, + }, + { + name: "re-revoked", + inputs: [][]*corepb.CRLEntry{ + {simpleEntry}, + {simpleEntry, reRevokedEntry}, + }, + expected: map[string]*corepb.CRLEntry{ + simpleEntry.Serial: reRevokedEntry, + }, + }, + { + name: "re-revoked (newer shows up first)", + inputs: [][]*corepb.CRLEntry{ + {reRevokedEntry, simpleEntry}, + {simpleEntry}, + }, + expected: map[string]*corepb.CRLEntry{ + simpleEntry.Serial: reRevokedEntry, + }, + }, + { + name: "re-revoked (wrong date)", + inputs: [][]*corepb.CRLEntry{ + {simpleEntry}, + {simpleEntry, reRevokedEntryOld}, + }, + expectErr: true, + }, + { + name: "re-revoked (wrong reason)", + inputs: [][]*corepb.CRLEntry{ + {simpleEntry}, + {simpleEntry, reRevokedEntryBadReason}, + }, + expectErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + crlEntries := make(map[string]*corepb.CRLEntry) + var err error + for _, input := range tc.inputs { + _, err = addFromStream(crlEntries, &revokedCertsStream{entries: input}, nil) + if err != nil { + break + } + } + if tc.expectErr { + if err == nil { + t.Errorf("addFromStream=%+v, want error", crlEntries) + } + } else { + if err != nil { + t.Fatalf("addFromStream=%s, want no error", err) + } + + if !reflect.DeepEqual(crlEntries, tc.expected) { + t.Errorf("addFromStream=%+v, want %+v", crlEntries, tc.expected) + } + } + }) + } +} + +func TestAddFromStreamDisallowedSerialPrefix(t *testing.T) { + now := time.Now() + yesterday := now.Add(-24 * time.Hour) + input := []*corepb.CRLEntry{ + { + Serial: "abcdefg", + Reason: ocsp.CessationOfOperation, + RevokedAt: timestamppb.New(yesterday), + }, + { + Serial: "01020304", + Reason: ocsp.CessationOfOperation, + RevokedAt: timestamppb.New(yesterday), + }, + } + crlEntries := make(map[string]*corepb.CRLEntry) + var err error + _, err = addFromStream( + crlEntries, + &revokedCertsStream{entries: input}, + []string{"ab"}, + ) + if err != nil { + t.Fatalf("addFromStream: %s", err) + } + expected := map[string]*corepb.CRLEntry{ + "abcdefg": input[0], + } + + if !reflect.DeepEqual(crlEntries, expected) { + t.Errorf("addFromStream=%+v, want %+v", crlEntries, expected) + } +} diff --git a/third-party/github.com/letsencrypt/boulder/csr/csr.go b/third-party/github.com/letsencrypt/boulder/csr/csr.go index 1f343ba9b..730bb9a9f 100644 --- a/third-party/github.com/letsencrypt/boulder/csr/csr.go +++ b/third-party/github.com/letsencrypt/boulder/csr/csr.go @@ -5,11 +5,13 @@ import ( "crypto" "crypto/x509" "errors" + "net/netip" "strings" "github.com/letsencrypt/boulder/core" berrors "github.com/letsencrypt/boulder/errors" "github.com/letsencrypt/boulder/goodkey" + "github.com/letsencrypt/boulder/identifier" ) // maxCNLength is the maximum length allowed for the common name as specified in RFC 5280 @@ -33,13 +35,13 @@ var ( unsupportedSigAlg = berrors.BadCSRError("signature algorithm not supported") invalidSig = berrors.BadCSRError("invalid signature on CSR") invalidEmailPresent = berrors.BadCSRError("CSR contains one or more email address fields") - invalidIPPresent = berrors.BadCSRError("CSR contains one or more IP address fields") - invalidNoDNS = berrors.BadCSRError("at least one DNS name is required") + invalidURIPresent = berrors.BadCSRError("CSR contains one or more URI fields") + invalidNoIdent = berrors.BadCSRError("at least one identifier is required") ) -// VerifyCSR checks the validity of a x509.CertificateRequest. Before doing checks it normalizes -// the CSR which lowers the case of DNS names and subject CN, and hoist a DNS name into the CN -// if it is empty. +// VerifyCSR checks the validity of a x509.CertificateRequest. It uses +// identifier.FromCSR to normalize the DNS names before checking whether we'll +// issue for them. func VerifyCSR(ctx context.Context, csr *x509.CertificateRequest, maxNames int, keyPolicy *goodkey.KeyPolicy, pa core.PolicyAuthority) error { key, ok := csr.PublicKey.(crypto.PublicKey) if !ok { @@ -63,59 +65,54 @@ func VerifyCSR(ctx context.Context, csr *x509.CertificateRequest, maxNames int, if len(csr.EmailAddresses) > 0 { return invalidEmailPresent } - if len(csr.IPAddresses) > 0 { - return invalidIPPresent + if len(csr.URIs) > 0 { + return invalidURIPresent } - names := NamesFromCSR(csr) - - if len(names.SANs) == 0 && names.CN == "" { - return invalidNoDNS + // FromCSR also performs normalization, returning values that may not match + // the literal CSR contents. + idents := identifier.FromCSR(csr) + if len(idents) == 0 { + return invalidNoIdent } - if len(names.CN) > maxCNLength { - return berrors.BadCSRError("CN was longer than %d bytes", maxCNLength) - } - if len(names.SANs) > maxNames { - return berrors.BadCSRError("CSR contains more than %d DNS names", maxNames) + if len(idents) > maxNames { + return berrors.BadCSRError("CSR contains more than %d identifiers", maxNames) } - err = pa.WillingToIssue(names.SANs) + err = pa.WillingToIssue(idents) if err != nil { return err } return nil } -type names struct { - SANs []string - CN string -} - -// NamesFromCSR deduplicates and lower-cases the Subject Common Name and Subject -// Alternative Names from the CSR. If the CSR contains a CN, then it preserves -// it and guarantees that the SANs also include it. If the CSR does not contain -// a CN, then it also attempts to promote a SAN to the CN (if any is short -// enough to fit). -func NamesFromCSR(csr *x509.CertificateRequest) names { - // Produce a new "sans" slice with the same memory address as csr.DNSNames - // but force a new allocation if an append happens so that we don't - // accidentally mutate the underlying csr.DNSNames array. - sans := csr.DNSNames[0:len(csr.DNSNames):len(csr.DNSNames)] - if csr.Subject.CommonName != "" { - sans = append(sans, csr.Subject.CommonName) +// CNFromCSR returns the lower-cased Subject Common Name from the CSR, if a +// short enough CN was provided. If it was too long or appears to be an IP, +// there will be no CN. If none was provided, the CN will be the first SAN that +// is short enough, which is done only for backwards compatibility with prior +// Let's Encrypt behaviour. +func CNFromCSR(csr *x509.CertificateRequest) string { + if len(csr.Subject.CommonName) > maxCNLength { + return "" } if csr.Subject.CommonName != "" { - return names{SANs: core.UniqueLowerNames(sans), CN: strings.ToLower(csr.Subject.CommonName)} + _, err := netip.ParseAddr(csr.Subject.CommonName) + if err == nil { // inverted; we're looking for successful parsing here + return "" + } + + return strings.ToLower(csr.Subject.CommonName) } - // If there's no CN already, but we want to set one, promote the first SAN - // which is shorter than the maximum acceptable CN length (if any). - for _, name := range sans { + // If there's no CN already, but we want to set one, promote the first dnsName + // SAN which is shorter than the maximum acceptable CN length (if any). We + // will never promote an ipAddress SAN to the CN. + for _, name := range csr.DNSNames { if len(name) <= maxCNLength { - return names{SANs: core.UniqueLowerNames(sans), CN: strings.ToLower(name)} + return strings.ToLower(name) } } - return names{SANs: core.UniqueLowerNames(sans)} + return "" } diff --git a/third-party/github.com/letsencrypt/boulder/csr/csr_test.go b/third-party/github.com/letsencrypt/boulder/csr/csr_test.go index 90884906a..1aabc3cb8 100644 --- a/third-party/github.com/letsencrypt/boulder/csr/csr_test.go +++ b/third-party/github.com/letsencrypt/boulder/csr/csr_test.go @@ -9,6 +9,8 @@ import ( "encoding/asn1" "errors" "net" + "net/netip" + "net/url" "strings" "testing" @@ -22,13 +24,13 @@ import ( type mockPA struct{} -func (pa *mockPA) ChallengesFor(identifier identifier.ACMEIdentifier) (challenges []core.Challenge, err error) { - return +func (pa *mockPA) ChallengeTypesFor(ident identifier.ACMEIdentifier) ([]core.AcmeChallenge, error) { + return []core.AcmeChallenge{}, nil } -func (pa *mockPA) WillingToIssue(domains []string) error { - for _, domain := range domains { - if domain == "bad-name.com" || domain == "other-bad-name.com" { +func (pa *mockPA) WillingToIssue(idents identifier.ACMEIdentifiers) error { + for _, ident := range idents { + if ident.Value == "bad-name.com" || ident.Value == "other-bad-name.com" { return errors.New("policy forbids issuing for identifier") } } @@ -39,7 +41,7 @@ func (pa *mockPA) ChallengeTypeEnabled(t core.AcmeChallenge) bool { return true } -func (pa *mockPA) CheckAuthz(a *core.Authorization) error { +func (pa *mockPA) CheckAuthzChallenges(a *core.Authorization) error { return nil } @@ -68,6 +70,10 @@ func TestVerifyCSR(t *testing.T) { signedReqWithIPAddress := new(x509.CertificateRequest) *signedReqWithIPAddress = *signedReq signedReqWithIPAddress.IPAddresses = []net.IP{net.IPv4(1, 2, 3, 4)} + signedReqWithURI := new(x509.CertificateRequest) + *signedReqWithURI = *signedReq + testURI, _ := url.ParseRequestURI("https://example.com/") + signedReqWithURI.URIs = []*url.URL{testURI} signedReqWithAllLongSANs := new(x509.CertificateRequest) *signedReqWithAllLongSANs = *signedReq signedReqWithAllLongSANs.DNSNames = []string{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com"} @@ -103,19 +109,19 @@ func TestVerifyCSR(t *testing.T) { signedReq, 100, &mockPA{}, - invalidNoDNS, + invalidNoIdent, }, { signedReqWithLongCN, 100, &mockPA{}, - berrors.BadCSRError("CN was longer than %d bytes", maxCNLength), + nil, }, { signedReqWithHosts, 1, &mockPA{}, - berrors.BadCSRError("CSR contains more than 1 DNS names"), + berrors.BadCSRError("CSR contains more than 1 identifiers"), }, { signedReqWithBadNames, @@ -133,7 +139,13 @@ func TestVerifyCSR(t *testing.T) { signedReqWithIPAddress, 100, &mockPA{}, - invalidIPPresent, + nil, + }, + { + signedReqWithURI, + 100, + &mockPA{}, + invalidURIPresent, }, { signedReqWithAllLongSANs, @@ -149,47 +161,49 @@ func TestVerifyCSR(t *testing.T) { } } -func TestNamesFromCSR(t *testing.T) { +func TestCNFromCSR(t *testing.T) { tooLongString := strings.Repeat("a", maxCNLength+1) cases := []struct { - name string - csr *x509.CertificateRequest - expectedCN string - expectedNames []string + name string + csr *x509.CertificateRequest + expectedCN string }{ { "no explicit CN", &x509.CertificateRequest{DNSNames: []string{"a.com"}}, "a.com", - []string{"a.com"}, }, { "explicit uppercase CN", &x509.CertificateRequest{Subject: pkix.Name{CommonName: "A.com"}, DNSNames: []string{"a.com"}}, "a.com", - []string{"a.com"}, }, { "no explicit CN, uppercase SAN", &x509.CertificateRequest{DNSNames: []string{"A.com"}}, "a.com", - []string{"a.com"}, }, { "duplicate SANs", &x509.CertificateRequest{DNSNames: []string{"b.com", "b.com", "a.com", "a.com"}}, "b.com", - []string{"a.com", "b.com"}, }, { "explicit CN not found in SANs", &x509.CertificateRequest{Subject: pkix.Name{CommonName: "a.com"}, DNSNames: []string{"b.com"}}, "a.com", - []string{"a.com", "b.com"}, }, { - "no explicit CN, too long leading SANs", + "no explicit CN, all SANs too long to be the CN", + &x509.CertificateRequest{DNSNames: []string{ + tooLongString + ".a.com", + tooLongString + ".b.com", + }}, + "", + }, + { + "no explicit CN, leading SANs too long to be the CN", &x509.CertificateRequest{DNSNames: []string{ tooLongString + ".a.com", tooLongString + ".b.com", @@ -197,10 +211,9 @@ func TestNamesFromCSR(t *testing.T) { "b.com", }}, "a.com", - []string{"a.com", tooLongString + ".a.com", tooLongString + ".b.com", "b.com"}, }, { - "explicit CN, too long leading SANs", + "explicit CN, leading SANs too long to be the CN", &x509.CertificateRequest{ Subject: pkix.Name{CommonName: "A.com"}, DNSNames: []string{ @@ -210,14 +223,43 @@ func TestNamesFromCSR(t *testing.T) { "b.com", }}, "a.com", - []string{"a.com", tooLongString + ".a.com", tooLongString + ".b.com", "b.com"}, + }, + { + "explicit CN that's too long to be the CN", + &x509.CertificateRequest{ + Subject: pkix.Name{CommonName: tooLongString + ".a.com"}, + }, + "", + }, + { + "explicit CN that's too long to be the CN, with a SAN", + &x509.CertificateRequest{ + Subject: pkix.Name{CommonName: tooLongString + ".a.com"}, + DNSNames: []string{ + "b.com", + }}, + "", + }, + { + "explicit CN that's an IP", + &x509.CertificateRequest{ + Subject: pkix.Name{CommonName: "127.0.0.1"}, + }, + "", + }, + { + "no CN, only IP SANs", + &x509.CertificateRequest{ + IPAddresses: []net.IP{ + netip.MustParseAddr("127.0.0.1").AsSlice(), + }, + }, + "", }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - names := NamesFromCSR(tc.csr) - test.AssertEquals(t, names.CN, tc.expectedCN) - test.AssertDeepEquals(t, names.SANs, tc.expectedNames) + test.AssertEquals(t, CNFromCSR(tc.csr), tc.expectedCN) }) } } diff --git a/third-party/github.com/letsencrypt/boulder/ctpolicy/ctconfig/ctconfig.go b/third-party/github.com/letsencrypt/boulder/ctpolicy/ctconfig/ctconfig.go index 8adab4adb..2e936ffae 100644 --- a/third-party/github.com/letsencrypt/boulder/ctpolicy/ctconfig/ctconfig.go +++ b/third-party/github.com/letsencrypt/boulder/ctpolicy/ctconfig/ctconfig.go @@ -1,93 +1,9 @@ package ctconfig import ( - "errors" - "fmt" - "time" - "github.com/letsencrypt/boulder/config" ) -// LogShard describes a single shard of a temporally sharded -// CT log -type LogShard struct { - URI string - Key string - WindowStart time.Time - WindowEnd time.Time -} - -// TemporalSet contains a set of temporal shards of a single log -type TemporalSet struct { - Name string - Shards []LogShard -} - -// Setup initializes the TemporalSet by parsing the start and end dates -// and verifying WindowEnd > WindowStart -func (ts *TemporalSet) Setup() error { - if ts.Name == "" { - return errors.New("Name cannot be empty") - } - if len(ts.Shards) == 0 { - return errors.New("temporal set contains no shards") - } - for i := range ts.Shards { - if !ts.Shards[i].WindowEnd.After(ts.Shards[i].WindowStart) { - return errors.New("WindowStart must be before WindowEnd") - } - } - return nil -} - -// pick chooses the correct shard from a TemporalSet to use for the given -// expiration time. In the case where two shards have overlapping windows -// the earlier of the two shards will be chosen. -func (ts *TemporalSet) pick(exp time.Time) (*LogShard, error) { - for _, shard := range ts.Shards { - if exp.Before(shard.WindowStart) { - continue - } - if !exp.Before(shard.WindowEnd) { - continue - } - return &shard, nil - } - return nil, fmt.Errorf("no valid shard available for temporal set %q for expiration date %q", ts.Name, exp) -} - -// LogDescription contains the information needed to submit certificates -// to a CT log and verify returned receipts. If TemporalSet is non-nil then -// URI and Key should be empty. -type LogDescription struct { - URI string - Key string - SubmitFinalCert bool - - *TemporalSet -} - -// Info returns the URI and key of the log, either from a plain log description -// or from the earliest valid shard from a temporal log set -func (ld LogDescription) Info(exp time.Time) (string, string, error) { - if ld.TemporalSet == nil { - return ld.URI, ld.Key, nil - } - shard, err := ld.TemporalSet.pick(exp) - if err != nil { - return "", "", err - } - return shard.URI, shard.Key, nil -} - -// CTGroup represents a group of CT Logs. Although capable of holding logs -// grouped by any arbitrary feature, is today primarily used to hold logs which -// are all operated by the same legal entity. -type CTGroup struct { - Name string - Logs []LogDescription -} - // CTConfig is the top-level config object expected to be embedded in an // executable's JSON config struct. type CTConfig struct { @@ -109,13 +25,3 @@ type CTConfig struct { // and final certs to the same log. FinalLogs []string } - -// LogID holds enough information to uniquely identify a CT Log: its log_id -// (the base64-encoding of the SHA-256 hash of its public key) and its human- -// readable name/description. This is used to extract other log parameters -// (such as its URL and public key) from the Chrome Log List. -type LogID struct { - Name string - ID string - SubmitFinal bool -} diff --git a/third-party/github.com/letsencrypt/boulder/ctpolicy/ctconfig/ctconfig_test.go b/third-party/github.com/letsencrypt/boulder/ctpolicy/ctconfig/ctconfig_test.go deleted file mode 100644 index d8d710f39..000000000 --- a/third-party/github.com/letsencrypt/boulder/ctpolicy/ctconfig/ctconfig_test.go +++ /dev/null @@ -1,116 +0,0 @@ -package ctconfig - -import ( - "testing" - "time" - - "github.com/jmhodges/clock" - "github.com/letsencrypt/boulder/test" -) - -func TestTemporalSetup(t *testing.T) { - for _, tc := range []struct { - ts TemporalSet - err string - }{ - { - ts: TemporalSet{}, - err: "Name cannot be empty", - }, - { - ts: TemporalSet{ - Name: "temporal set", - }, - err: "temporal set contains no shards", - }, - { - ts: TemporalSet{ - Name: "temporal set", - Shards: []LogShard{ - { - WindowStart: time.Time{}, - WindowEnd: time.Time{}, - }, - }, - }, - err: "WindowStart must be before WindowEnd", - }, - { - ts: TemporalSet{ - Name: "temporal set", - Shards: []LogShard{ - { - WindowStart: time.Time{}.Add(time.Hour), - WindowEnd: time.Time{}, - }, - }, - }, - err: "WindowStart must be before WindowEnd", - }, - { - ts: TemporalSet{ - Name: "temporal set", - Shards: []LogShard{ - { - WindowStart: time.Time{}, - WindowEnd: time.Time{}.Add(time.Hour), - }, - }, - }, - err: "", - }, - } { - err := tc.ts.Setup() - if err != nil && tc.err != err.Error() { - t.Errorf("got error %q, wanted %q", err, tc.err) - } else if err == nil && tc.err != "" { - t.Errorf("unexpected error %q", err) - } - } -} - -func TestLogInfo(t *testing.T) { - ld := LogDescription{ - URI: "basic-uri", - Key: "basic-key", - } - uri, key, err := ld.Info(time.Time{}) - test.AssertNotError(t, err, "Info failed") - test.AssertEquals(t, uri, ld.URI) - test.AssertEquals(t, key, ld.Key) - - fc := clock.NewFake() - ld.TemporalSet = &TemporalSet{} - _, _, err = ld.Info(fc.Now()) - test.AssertError(t, err, "Info should fail with a TemporalSet with no viable shards") - ld.TemporalSet.Shards = []LogShard{{WindowStart: fc.Now().Add(time.Hour), WindowEnd: fc.Now().Add(time.Hour * 2)}} - _, _, err = ld.Info(fc.Now()) - test.AssertError(t, err, "Info should fail with a TemporalSet with no viable shards") - - fc.Add(time.Hour * 4) - now := fc.Now() - ld.TemporalSet.Shards = []LogShard{ - { - WindowStart: now.Add(time.Hour * -4), - WindowEnd: now.Add(time.Hour * -2), - URI: "a", - Key: "a", - }, - { - WindowStart: now.Add(time.Hour * -2), - WindowEnd: now.Add(time.Hour * 2), - URI: "b", - Key: "b", - }, - { - WindowStart: now.Add(time.Hour * 2), - WindowEnd: now.Add(time.Hour * 4), - URI: "c", - Key: "c", - }, - } - uri, key, err = ld.Info(now) - test.AssertNotError(t, err, "Info failed") - test.AssertEquals(t, uri, "b") - test.AssertEquals(t, key, "b") -} diff --git a/third-party/github.com/letsencrypt/boulder/ctpolicy/ctpolicy.go b/third-party/github.com/letsencrypt/boulder/ctpolicy/ctpolicy.go index de713f1e4..4b85b5b0e 100644 --- a/third-party/github.com/letsencrypt/boulder/ctpolicy/ctpolicy.go +++ b/third-party/github.com/letsencrypt/boulder/ctpolicy/ctpolicy.go @@ -2,6 +2,7 @@ package ctpolicy import ( "context" + "encoding/base64" "fmt" "strings" "time" @@ -23,15 +24,14 @@ const ( // CTPolicy is used to hold information about SCTs required from various // groupings type CTPolicy struct { - pub pubpb.PublisherClient - sctLogs loglist.List - infoLogs loglist.List - finalLogs loglist.List - stagger time.Duration - log blog.Logger - winnerCounter *prometheus.CounterVec - operatorGroupsGauge *prometheus.GaugeVec - shardExpiryGauge *prometheus.GaugeVec + pub pubpb.PublisherClient + sctLogs loglist.List + infoLogs loglist.List + finalLogs loglist.List + stagger time.Duration + log blog.Logger + winnerCounter *prometheus.CounterVec + shardExpiryGauge *prometheus.GaugeVec } // New creates a new CTPolicy struct @@ -45,15 +45,6 @@ func New(pub pubpb.PublisherClient, sctLogs loglist.List, infoLogs loglist.List, ) stats.MustRegister(winnerCounter) - operatorGroupsGauge := prometheus.NewGaugeVec( - prometheus.GaugeOpts{ - Name: "ct_operator_group_size_gauge", - Help: "Gauge for CT operators group size, by operator and log source (capable of providing SCT, informational logs, logs we submit final certs to).", - }, - []string{"operator", "source"}, - ) - stats.MustRegister(operatorGroupsGauge) - shardExpiryGauge := prometheus.NewGaugeVec( prometheus.GaugeOpts{ Name: "ct_shard_expiration_seconds", @@ -63,43 +54,30 @@ func New(pub pubpb.PublisherClient, sctLogs loglist.List, infoLogs loglist.List, ) stats.MustRegister(shardExpiryGauge) - for op, group := range sctLogs { - operatorGroupsGauge.WithLabelValues(op, "sctLogs").Set(float64(len(group))) - - for _, log := range group { - if log.EndExclusive.IsZero() { - // Handles the case for non-temporally sharded logs too. - shardExpiryGauge.WithLabelValues(op, log.Name).Set(float64(0)) - } else { - shardExpiryGauge.WithLabelValues(op, log.Name).Set(float64(log.EndExclusive.Unix())) - } + for _, log := range sctLogs { + if log.EndExclusive.IsZero() { + // Handles the case for non-temporally sharded logs too. + shardExpiryGauge.WithLabelValues(log.Operator, log.Name).Set(float64(0)) + } else { + shardExpiryGauge.WithLabelValues(log.Operator, log.Name).Set(float64(log.EndExclusive.Unix())) } } - for op, group := range infoLogs { - operatorGroupsGauge.WithLabelValues(op, "infoLogs").Set(float64(len(group))) - } - - for op, group := range finalLogs { - operatorGroupsGauge.WithLabelValues(op, "finalLogs").Set(float64(len(group))) - } - return &CTPolicy{ - pub: pub, - sctLogs: sctLogs, - infoLogs: infoLogs, - finalLogs: finalLogs, - stagger: stagger, - log: log, - winnerCounter: winnerCounter, - operatorGroupsGauge: operatorGroupsGauge, - shardExpiryGauge: shardExpiryGauge, + pub: pub, + sctLogs: sctLogs, + infoLogs: infoLogs, + finalLogs: finalLogs, + stagger: stagger, + log: log, + winnerCounter: winnerCounter, + shardExpiryGauge: shardExpiryGauge, } } type result struct { + log loglist.Log sct []byte - url string err error } @@ -115,73 +93,68 @@ func (ctp *CTPolicy) GetSCTs(ctx context.Context, cert core.CertDER, expiration subCtx, cancel := context.WithCancel(ctx) defer cancel() - // This closure will be called in parallel once for each operator group. - getOne := func(i int, g string) ([]byte, string, error) { - // Sleep a little bit to stagger our requests to the later groups. Use `i-1` - // to compute the stagger duration so that the first two groups (indices 0 + // This closure will be called in parallel once for each log. + getOne := func(i int, l loglist.Log) ([]byte, error) { + // Sleep a little bit to stagger our requests to the later logs. Use `i-1` + // to compute the stagger duration so that the first two logs (indices 0 // and 1) get negative or zero (i.e. instant) sleep durations. If the - // context gets cancelled (most likely because two logs from other operator - // groups returned SCTs already) before the sleep is complete, quit instead. + // context gets cancelled (most likely because we got enough SCTs from other + // logs already) before the sleep is complete, quit instead. select { case <-subCtx.Done(): - return nil, "", subCtx.Err() + return nil, subCtx.Err() case <-time.After(time.Duration(i-1) * ctp.stagger): } - // Pick a random log from among those in the group. In practice, very few - // operator groups have more than one log, so this loses little flexibility. - url, key, err := ctp.sctLogs.PickOne(g, expiration) - if err != nil { - return nil, "", fmt.Errorf("unable to get log info: %w", err) - } - sct, err := ctp.pub.SubmitToSingleCTWithResult(ctx, &pubpb.Request{ - LogURL: url, - LogPublicKey: key, + LogURL: l.Url, + LogPublicKey: base64.StdEncoding.EncodeToString(l.Key), Der: cert, Kind: pubpb.SubmissionType_sct, }) if err != nil { - return nil, url, fmt.Errorf("ct submission to %q (%q) failed: %w", g, url, err) + return nil, fmt.Errorf("ct submission to %q (%q) failed: %w", l.Name, l.Url, err) } - return sct.Sct, url, nil + return sct.Sct, nil } - // Ensure that this channel has a buffer equal to the number of goroutines - // we're kicking off, so that they're all guaranteed to be able to write to - // it and exit without blocking and leaking. - results := make(chan result, len(ctp.sctLogs)) + // Identify the set of candidate logs whose temporal interval includes this + // cert's expiry. Randomize the order of the logs so that we're not always + // trying to submit to the same two. + logs := ctp.sctLogs.ForTime(expiration).Permute() // Kick off a collection of goroutines to try to submit the precert to each - // log operator group. Randomize the order of the groups so that we're not - // always trying to submit to the same two operators. - for i, group := range ctp.sctLogs.Permute() { - go func(i int, g string) { - sctDER, url, err := getOne(i, g) - results <- result{sct: sctDER, url: url, err: err} - }(i, group) + // log. Ensure that the results channel has a buffer equal to the number of + // goroutines we're kicking off, so that they're all guaranteed to be able to + // write to it and exit without blocking and leaking. + resChan := make(chan result, len(logs)) + for i, log := range logs { + go func(i int, l loglist.Log) { + sctDER, err := getOne(i, l) + resChan <- result{log: l, sct: sctDER, err: err} + }(i, log) } go ctp.submitPrecertInformational(cert, expiration) // Finally, collect SCTs and/or errors from our results channel. We know that - // we will collect len(ctp.sctLogs) results from the channel because every - // goroutine is guaranteed to write one result to the channel. - scts := make(core.SCTDERs, 0) + // we can collect len(logs) results from the channel because every goroutine + // is guaranteed to write one result (either sct or error) to the channel. + results := make([]result, 0) errs := make([]string, 0) - for range len(ctp.sctLogs) { - res := <-results + for range len(logs) { + res := <-resChan if res.err != nil { errs = append(errs, res.err.Error()) - if res.url != "" { - ctp.winnerCounter.WithLabelValues(res.url, failed).Inc() - } + ctp.winnerCounter.WithLabelValues(res.log.Url, failed).Inc() continue } - scts = append(scts, res.sct) - ctp.winnerCounter.WithLabelValues(res.url, succeeded).Inc() - if len(scts) >= 2 { + results = append(results, res) + ctp.winnerCounter.WithLabelValues(res.log.Url, succeeded).Inc() + + scts := compliantSet(results) + if scts != nil { return scts, nil } } @@ -196,6 +169,36 @@ func (ctp *CTPolicy) GetSCTs(ctx context.Context, cert core.CertDER, expiration return nil, berrors.MissingSCTsError("failed to get 2 SCTs, got %d error(s): %s", len(errs), strings.Join(errs, "; ")) } +// compliantSet returns a slice of SCTs which complies with all relevant CT Log +// Policy requirements, namely that the set of SCTs: +// - contain at least two SCTs, which +// - come from logs run by at least two different operators, and +// - contain at least one RFC6962-compliant (i.e. non-static/tiled) log. +// +// If no such set of SCTs exists, returns nil. +func compliantSet(results []result) core.SCTDERs { + for _, first := range results { + if first.err != nil { + continue + } + for _, second := range results { + if second.err != nil { + continue + } + if first.log.Operator == second.log.Operator { + // The two SCTs must come from different operators. + continue + } + if first.log.Tiled && second.log.Tiled { + // At least one must come from a non-tiled log. + continue + } + return core.SCTDERs{first.sct, second.sct} + } + } + return nil +} + // submitAllBestEffort submits the given certificate or precertificate to every // log ("informational" for precerts, "final" for certs) configured in the policy. // It neither waits for these submission to complete, nor tracks their success. @@ -205,29 +208,26 @@ func (ctp *CTPolicy) submitAllBestEffort(blob core.CertDER, kind pubpb.Submissio logs = ctp.infoLogs } - for _, group := range logs { - for _, log := range group { - if log.StartInclusive.After(expiry) || log.EndExclusive.Equal(expiry) || log.EndExclusive.Before(expiry) { - continue - } - - go func(log loglist.Log) { - _, err := ctp.pub.SubmitToSingleCTWithResult( - context.Background(), - &pubpb.Request{ - LogURL: log.Url, - LogPublicKey: log.Key, - Der: blob, - Kind: kind, - }, - ) - if err != nil { - ctp.log.Warningf("ct submission of cert to log %q failed: %s", log.Url, err) - } - }(log) + for _, log := range logs { + if log.StartInclusive.After(expiry) || log.EndExclusive.Equal(expiry) || log.EndExclusive.Before(expiry) { + continue } - } + go func(log loglist.Log) { + _, err := ctp.pub.SubmitToSingleCTWithResult( + context.Background(), + &pubpb.Request{ + LogURL: log.Url, + LogPublicKey: base64.StdEncoding.EncodeToString(log.Key), + Der: blob, + Kind: kind, + }, + ) + if err != nil { + ctp.log.Warningf("ct submission of cert to log %q failed: %s", log.Url, err) + } + }(log) + } } // submitPrecertInformational submits precertificates to any configured diff --git a/third-party/github.com/letsencrypt/boulder/ctpolicy/ctpolicy_test.go b/third-party/github.com/letsencrypt/boulder/ctpolicy/ctpolicy_test.go index b7619761a..e075a030f 100644 --- a/third-party/github.com/letsencrypt/boulder/ctpolicy/ctpolicy_test.go +++ b/third-party/github.com/letsencrypt/boulder/ctpolicy/ctpolicy_test.go @@ -1,6 +1,7 @@ package ctpolicy import ( + "bytes" "context" "errors" "strings" @@ -8,6 +9,9 @@ import ( "time" "github.com/jmhodges/clock" + "github.com/prometheus/client_golang/prometheus" + "google.golang.org/grpc" + "github.com/letsencrypt/boulder/core" "github.com/letsencrypt/boulder/ctpolicy/loglist" berrors "github.com/letsencrypt/boulder/errors" @@ -15,8 +19,6 @@ import ( "github.com/letsencrypt/boulder/metrics" pubpb "github.com/letsencrypt/boulder/publisher/proto" "github.com/letsencrypt/boulder/test" - "github.com/prometheus/client_golang/prometheus" - "google.golang.org/grpc" ) type mockPub struct{} @@ -45,7 +47,7 @@ func TestGetSCTs(t *testing.T) { testCases := []struct { name string mock pubpb.PublisherClient - groups loglist.List + logs loglist.List ctx context.Context result core.SCTDERs expectErr string @@ -54,17 +56,11 @@ func TestGetSCTs(t *testing.T) { { name: "basic success case", mock: &mockPub{}, - groups: loglist.List{ - "OperA": { - "LogA1": {Url: "UrlA1", Key: "KeyA1"}, - "LogA2": {Url: "UrlA2", Key: "KeyA2"}, - }, - "OperB": { - "LogB1": {Url: "UrlB1", Key: "KeyB1"}, - }, - "OperC": { - "LogC1": {Url: "UrlC1", Key: "KeyC1"}, - }, + logs: loglist.List{ + {Name: "LogA1", Operator: "OperA", Url: "UrlA1", Key: []byte("KeyA1")}, + {Name: "LogA2", Operator: "OperA", Url: "UrlA2", Key: []byte("KeyA2")}, + {Name: "LogB1", Operator: "OperB", Url: "UrlB1", Key: []byte("KeyB1")}, + {Name: "LogC1", Operator: "OperC", Url: "UrlC1", Key: []byte("KeyC1")}, }, ctx: context.Background(), result: core.SCTDERs{[]byte{0}, []byte{0}}, @@ -72,36 +68,24 @@ func TestGetSCTs(t *testing.T) { { name: "basic failure case", mock: &mockFailPub{}, - groups: loglist.List{ - "OperA": { - "LogA1": {Url: "UrlA1", Key: "KeyA1"}, - "LogA2": {Url: "UrlA2", Key: "KeyA2"}, - }, - "OperB": { - "LogB1": {Url: "UrlB1", Key: "KeyB1"}, - }, - "OperC": { - "LogC1": {Url: "UrlC1", Key: "KeyC1"}, - }, + logs: loglist.List{ + {Name: "LogA1", Operator: "OperA", Url: "UrlA1", Key: []byte("KeyA1")}, + {Name: "LogA2", Operator: "OperA", Url: "UrlA2", Key: []byte("KeyA2")}, + {Name: "LogB1", Operator: "OperB", Url: "UrlB1", Key: []byte("KeyB1")}, + {Name: "LogC1", Operator: "OperC", Url: "UrlC1", Key: []byte("KeyC1")}, }, ctx: context.Background(), - expectErr: "failed to get 2 SCTs, got 3 error(s)", + expectErr: "failed to get 2 SCTs, got 4 error(s)", berrorType: &missingSCTErr, }, { name: "parent context timeout failure case", mock: &mockSlowPub{}, - groups: loglist.List{ - "OperA": { - "LogA1": {Url: "UrlA1", Key: "KeyA1"}, - "LogA2": {Url: "UrlA2", Key: "KeyA2"}, - }, - "OperB": { - "LogB1": {Url: "UrlB1", Key: "KeyB1"}, - }, - "OperC": { - "LogC1": {Url: "UrlC1", Key: "KeyC1"}, - }, + logs: loglist.List{ + {Name: "LogA1", Operator: "OperA", Url: "UrlA1", Key: []byte("KeyA1")}, + {Name: "LogA2", Operator: "OperA", Url: "UrlA2", Key: []byte("KeyA2")}, + {Name: "LogB1", Operator: "OperB", Url: "UrlB1", Key: []byte("KeyB1")}, + {Name: "LogC1", Operator: "OperC", Url: "UrlC1", Key: []byte("KeyC1")}, }, ctx: expired, expectErr: "failed to get 2 SCTs before ctx finished", @@ -111,7 +95,7 @@ func TestGetSCTs(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - ctp := New(tc.mock, tc.groups, nil, nil, 0, blog.NewMock(), metrics.NoopRegisterer) + ctp := New(tc.mock, tc.logs, nil, nil, 0, blog.NewMock(), metrics.NoopRegisterer) ret, err := ctp.GetSCTs(tc.ctx, []byte{0}, time.Time{}) if tc.result != nil { test.AssertDeepEquals(t, ret, tc.result) @@ -140,15 +124,9 @@ func (mp *mockFailOnePub) SubmitToSingleCTWithResult(_ context.Context, req *pub func TestGetSCTsMetrics(t *testing.T) { ctp := New(&mockFailOnePub{badURL: "UrlA1"}, loglist.List{ - "OperA": { - "LogA1": {Url: "UrlA1", Key: "KeyA1"}, - }, - "OperB": { - "LogB1": {Url: "UrlB1", Key: "KeyB1"}, - }, - "OperC": { - "LogC1": {Url: "UrlC1", Key: "KeyC1"}, - }, + {Name: "LogA1", Operator: "OperA", Url: "UrlA1", Key: []byte("KeyA1")}, + {Name: "LogB1", Operator: "OperB", Url: "UrlB1", Key: []byte("KeyB1")}, + {Name: "LogC1", Operator: "OperC", Url: "UrlC1", Key: []byte("KeyC1")}, }, nil, nil, 0, blog.NewMock(), metrics.NoopRegisterer) _, err := ctp.GetSCTs(context.Background(), []byte{0}, time.Time{}) test.AssertNotError(t, err, "GetSCTs failed") @@ -159,9 +137,7 @@ func TestGetSCTsMetrics(t *testing.T) { func TestGetSCTsFailMetrics(t *testing.T) { // Ensure the proper metrics are incremented when GetSCTs fails. ctp := New(&mockFailOnePub{badURL: "UrlA1"}, loglist.List{ - "OperA": { - "LogA1": {Url: "UrlA1", Key: "KeyA1"}, - }, + {Name: "LogA1", Operator: "OperA", Url: "UrlA1", Key: []byte("KeyA1")}, }, nil, nil, 0, blog.NewMock(), metrics.NoopRegisterer) _, err := ctp.GetSCTs(context.Background(), []byte{0}, time.Time{}) test.AssertError(t, err, "GetSCTs should have failed") @@ -173,9 +149,7 @@ func TestGetSCTsFailMetrics(t *testing.T) { defer cancel() ctp = New(&mockSlowPub{}, loglist.List{ - "OperA": { - "LogA1": {Url: "UrlA1", Key: "KeyA1"}, - }, + {Name: "LogA1", Operator: "OperA", Url: "UrlA1", Key: []byte("KeyA1")}, }, nil, nil, 0, blog.NewMock(), metrics.NoopRegisterer) _, err = ctp.GetSCTs(ctx, []byte{0}, time.Time{}) test.AssertError(t, err, "GetSCTs should have timed out") @@ -185,78 +159,96 @@ func TestGetSCTsFailMetrics(t *testing.T) { } func TestLogListMetrics(t *testing.T) { - // Multiple operator groups with configured logs. - ctp := New(&mockPub{}, loglist.List{ - "OperA": { - "LogA1": {Url: "UrlA1", Key: "KeyA1"}, - "LogA2": {Url: "UrlA2", Key: "KeyA2"}, - }, - "OperB": { - "LogB1": {Url: "UrlB1", Key: "KeyB1"}, - }, - "OperC": { - "LogC1": {Url: "UrlC1", Key: "KeyC1"}, - }, - }, nil, nil, 0, blog.NewMock(), metrics.NoopRegisterer) - test.AssertMetricWithLabelsEquals(t, ctp.operatorGroupsGauge, prometheus.Labels{"operator": "OperA", "source": "sctLogs"}, 2) - test.AssertMetricWithLabelsEquals(t, ctp.operatorGroupsGauge, prometheus.Labels{"operator": "OperB", "source": "sctLogs"}, 1) - test.AssertMetricWithLabelsEquals(t, ctp.operatorGroupsGauge, prometheus.Labels{"operator": "OperC", "source": "sctLogs"}, 1) - - // Multiple operator groups, no configured logs in one group - ctp = New(&mockPub{}, loglist.List{ - "OperA": { - "LogA1": {Url: "UrlA1", Key: "KeyA1"}, - "LogA2": {Url: "UrlA2", Key: "KeyA2"}, - }, - "OperB": { - "LogB1": {Url: "UrlB1", Key: "KeyB1"}, - }, - "OperC": {}, - }, nil, loglist.List{ - "OperA": { - "LogA1": {Url: "UrlA1", Key: "KeyA1"}, - }, - "OperB": {}, - "OperC": { - "LogC1": {Url: "UrlC1", Key: "KeyC1"}, - }, - }, 0, blog.NewMock(), metrics.NoopRegisterer) - test.AssertMetricWithLabelsEquals(t, ctp.operatorGroupsGauge, prometheus.Labels{"operator": "OperA", "source": "sctLogs"}, 2) - test.AssertMetricWithLabelsEquals(t, ctp.operatorGroupsGauge, prometheus.Labels{"operator": "OperB", "source": "sctLogs"}, 1) - test.AssertMetricWithLabelsEquals(t, ctp.operatorGroupsGauge, prometheus.Labels{"operator": "OperC", "source": "sctLogs"}, 0) - test.AssertMetricWithLabelsEquals(t, ctp.operatorGroupsGauge, prometheus.Labels{"operator": "OperA", "source": "finalLogs"}, 1) - test.AssertMetricWithLabelsEquals(t, ctp.operatorGroupsGauge, prometheus.Labels{"operator": "OperB", "source": "finalLogs"}, 0) - test.AssertMetricWithLabelsEquals(t, ctp.operatorGroupsGauge, prometheus.Labels{"operator": "OperC", "source": "finalLogs"}, 1) - - // Multiple operator groups with no configured logs. - ctp = New(&mockPub{}, loglist.List{ - "OperA": {}, - "OperB": {}, - }, nil, nil, 0, blog.NewMock(), metrics.NoopRegisterer) - test.AssertMetricWithLabelsEquals(t, ctp.operatorGroupsGauge, prometheus.Labels{"operator": "OperA", "source": "sctLogs"}, 0) - test.AssertMetricWithLabelsEquals(t, ctp.operatorGroupsGauge, prometheus.Labels{"operator": "OperB", "source": "sctLogs"}, 0) - - // Single operator group with no configured logs. - ctp = New(&mockPub{}, loglist.List{ - "OperA": {}, - }, nil, nil, 0, blog.NewMock(), metrics.NoopRegisterer) - test.AssertMetricWithLabelsEquals(t, ctp.operatorGroupsGauge, prometheus.Labels{"operator": "OperA", "source": "allLogs"}, 0) - fc := clock.NewFake() Tomorrow := fc.Now().Add(24 * time.Hour) NextWeek := fc.Now().Add(7 * 24 * time.Hour) // Multiple operator groups with configured logs. - ctp = New(&mockPub{}, loglist.List{ - "OperA": { - "LogA1": {Url: "UrlA1", Key: "KeyA1", Name: "LogA1", EndExclusive: Tomorrow}, - "LogA2": {Url: "UrlA2", Key: "KeyA2", Name: "LogA2", EndExclusive: NextWeek}, - }, - "OperB": { - "LogB1": {Url: "UrlB1", Key: "KeyB1", Name: "LogB1", EndExclusive: Tomorrow}, - }, + ctp := New(&mockPub{}, loglist.List{ + {Name: "LogA1", Operator: "OperA", Url: "UrlA1", Key: []byte("KeyA1"), EndExclusive: Tomorrow}, + {Name: "LogA2", Operator: "OperA", Url: "UrlA2", Key: []byte("KeyA2"), EndExclusive: NextWeek}, + {Name: "LogB1", Operator: "OperB", Url: "UrlB1", Key: []byte("KeyB1"), EndExclusive: Tomorrow}, }, nil, nil, 0, blog.NewMock(), metrics.NoopRegisterer) test.AssertMetricWithLabelsEquals(t, ctp.shardExpiryGauge, prometheus.Labels{"operator": "OperA", "logID": "LogA1"}, 86400) test.AssertMetricWithLabelsEquals(t, ctp.shardExpiryGauge, prometheus.Labels{"operator": "OperA", "logID": "LogA2"}, 604800) test.AssertMetricWithLabelsEquals(t, ctp.shardExpiryGauge, prometheus.Labels{"operator": "OperB", "logID": "LogB1"}, 86400) } + +func TestCompliantSet(t *testing.T) { + for _, tc := range []struct { + name string + results []result + want core.SCTDERs + }{ + { + name: "nil input", + results: nil, + want: nil, + }, + { + name: "zero length input", + results: []result{}, + want: nil, + }, + { + name: "only one result", + results: []result{ + {log: loglist.Log{Operator: "A", Tiled: false}, sct: []byte("sct1")}, + }, + want: nil, + }, + { + name: "only one good result", + results: []result{ + {log: loglist.Log{Operator: "A", Tiled: false}, sct: []byte("sct1")}, + {log: loglist.Log{Operator: "B", Tiled: false}, err: errors.New("oops")}, + }, + want: nil, + }, + { + name: "only one operator", + results: []result{ + {log: loglist.Log{Operator: "A", Tiled: false}, sct: []byte("sct1")}, + {log: loglist.Log{Operator: "A", Tiled: false}, sct: []byte("sct2")}, + }, + want: nil, + }, + { + name: "all tiled", + results: []result{ + {log: loglist.Log{Operator: "A", Tiled: true}, sct: []byte("sct1")}, + {log: loglist.Log{Operator: "B", Tiled: true}, sct: []byte("sct2")}, + }, + want: nil, + }, + { + name: "happy path", + results: []result{ + {log: loglist.Log{Operator: "A", Tiled: false}, err: errors.New("oops")}, + {log: loglist.Log{Operator: "A", Tiled: true}, sct: []byte("sct2")}, + {log: loglist.Log{Operator: "A", Tiled: false}, sct: []byte("sct3")}, + {log: loglist.Log{Operator: "B", Tiled: false}, err: errors.New("oops")}, + {log: loglist.Log{Operator: "B", Tiled: true}, sct: []byte("sct4")}, + {log: loglist.Log{Operator: "B", Tiled: false}, sct: []byte("sct6")}, + {log: loglist.Log{Operator: "C", Tiled: false}, err: errors.New("oops")}, + {log: loglist.Log{Operator: "C", Tiled: true}, sct: []byte("sct8")}, + {log: loglist.Log{Operator: "C", Tiled: false}, sct: []byte("sct9")}, + }, + // The second and sixth results should be picked, because first and fourth + // are skipped for being errors, and fifth is skipped for also being tiled. + want: core.SCTDERs{[]byte("sct2"), []byte("sct6")}, + }, + } { + t.Run(tc.name, func(t *testing.T) { + got := compliantSet(tc.results) + if len(got) != len(tc.want) { + t.Fatalf("compliantSet(%#v) returned %d SCTs, but want %d", tc.results, len(got), len(tc.want)) + } + for i, sct := range tc.want { + if !bytes.Equal(got[i], sct) { + t.Errorf("compliantSet(%#v) returned unexpected SCT at index %d", tc.results, i) + } + } + }) + } +} diff --git a/third-party/github.com/letsencrypt/boulder/ctpolicy/loglist/loglist.go b/third-party/github.com/letsencrypt/boulder/ctpolicy/loglist/loglist.go index 8722b65c8..0b49359f1 100644 --- a/third-party/github.com/letsencrypt/boulder/ctpolicy/loglist/loglist.go +++ b/third-party/github.com/letsencrypt/boulder/ctpolicy/loglist/loglist.go @@ -2,15 +2,15 @@ package loglist import ( _ "embed" - "encoding/json" + "encoding/base64" "errors" "fmt" - "math/rand" + "math/rand/v2" "os" - "strings" + "slices" "time" - "github.com/letsencrypt/boulder/ctpolicy/loglist/schema" + "github.com/google/certificate-transparency-go/loglist3" ) // purpose is the use to which a log list will be put. This type exists to allow @@ -31,74 +31,35 @@ const Informational purpose = "info" // necessarily still issuing SCTs today. const Validation purpose = "lint" -// List represents a list of logs, grouped by their operator, arranged by -// the "v3" schema as published by Chrome: -// https://www.gstatic.com/ct/log_list/v3/log_list_schema.json -// It exports no fields so that consumers don't have to deal with the terrible -// autogenerated names of the structs it wraps. -type List map[string]OperatorGroup - -// OperatorGroup represents a group of logs which are all run by the same -// operator organization. It provides constant-time lookup of logs within the -// group by their unique ID. -type OperatorGroup map[string]Log +// List represents a list of logs arranged by the "v3" schema as published by +// Chrome: https://www.gstatic.com/ct/log_list/v3/log_list_schema.json +type List []Log // Log represents a single log run by an operator. It contains just the info -// necessary to contact a log, and to determine whether that log will accept -// the submission of a certificate with a given expiration. +// necessary to determine whether we want to submit to that log, and how to +// do so. type Log struct { + Operator string Name string + Id string + Key []byte Url string - Key string StartInclusive time.Time EndExclusive time.Time - State state -} - -// State is an enum representing the various states a CT log can be in. Only -// pending, qualified, and usable logs can be submitted to. Only usable and -// readonly logs are trusted by Chrome. -type state int - -const ( - unknown state = iota - pending - qualified - usable - readonly - retired - rejected -) - -func stateFromState(s *schema.LogListSchemaJsonOperatorsElemLogsElemState) state { - if s == nil { - return unknown - } else if s.Rejected != nil { - return rejected - } else if s.Retired != nil { - return retired - } else if s.Readonly != nil { - return readonly - } else if s.Pending != nil { - return pending - } else if s.Qualified != nil { - return qualified - } else if s.Usable != nil { - return usable - } - return unknown + State loglist3.LogStatus + Tiled bool } // usableForPurpose returns true if the log state is acceptable for the given // log list purpose, and false otherwise. -func usableForPurpose(s state, p purpose) bool { +func usableForPurpose(s loglist3.LogStatus, p purpose) bool { switch p { case Issuance: - return s == usable + return s == loglist3.UsableLogStatus case Informational: - return s == usable || s == qualified || s == pending + return s == loglist3.UsableLogStatus || s == loglist3.QualifiedLogStatus || s == loglist3.PendingLogStatus case Validation: - return s == usable || s == readonly + return s == loglist3.UsableLogStatus || s == loglist3.ReadOnlyLogStatus } return false } @@ -118,46 +79,50 @@ func New(path string) (List, error) { // newHelper is a helper to allow the core logic of `New()` to be unit tested // without having to write files to disk. func newHelper(file []byte) (List, error) { - var parsed schema.LogListSchemaJson - err := json.Unmarshal(file, &parsed) + parsed, err := loglist3.NewFromJSON(file) if err != nil { return nil, fmt.Errorf("failed to parse CT Log List: %w", err) } - result := make(List) + result := make(List, 0) for _, op := range parsed.Operators { - group := make(OperatorGroup) for _, log := range op.Logs { - var name string - if log.Description != nil { - name = *log.Description - } - info := Log{ - Name: name, - Url: log.Url, - Key: log.Key, - State: stateFromState(log.State), + Operator: op.Name, + Name: log.Description, + Id: base64.StdEncoding.EncodeToString(log.LogID), + Key: log.Key, + Url: log.URL, + State: log.State.LogStatus(), + Tiled: false, } if log.TemporalInterval != nil { - startInclusive, err := time.Parse(time.RFC3339, log.TemporalInterval.StartInclusive) - if err != nil { - return nil, fmt.Errorf("failed to parse log %q start timestamp: %w", log.Url, err) - } - - endExclusive, err := time.Parse(time.RFC3339, log.TemporalInterval.EndExclusive) - if err != nil { - return nil, fmt.Errorf("failed to parse log %q end timestamp: %w", log.Url, err) - } - - info.StartInclusive = startInclusive - info.EndExclusive = endExclusive + info.StartInclusive = log.TemporalInterval.StartInclusive + info.EndExclusive = log.TemporalInterval.EndExclusive } - group[log.LogId] = info + result = append(result, info) + } + + for _, log := range op.TiledLogs { + info := Log{ + Operator: op.Name, + Name: log.Description, + Id: base64.StdEncoding.EncodeToString(log.LogID), + Key: log.Key, + Url: log.SubmissionURL, + State: log.State.LogStatus(), + Tiled: true, + } + + if log.TemporalInterval != nil { + info.StartInclusive = log.TemporalInterval.StartInclusive + info.EndExclusive = log.TemporalInterval.EndExclusive + } + + result = append(result, info) } - result[op.Name] = group } return result, nil @@ -186,45 +151,23 @@ func (ll List) SubsetForPurpose(names []string, p purpose) (List, error) { // those in the given list. It returns an error if any of the given names are // not found. func (ll List) subset(names []string) (List, error) { - remaining := make(map[string]struct{}, len(names)) + res := make(List, 0) for _, name := range names { - remaining[name] = struct{}{} - } - - newList := make(List) - for operator, group := range ll { - newGroup := make(OperatorGroup) - for id, log := range group { - if _, found := remaining[log.Name]; !found { - continue + found := false + for _, log := range ll { + if log.Name == name { + if found { + return nil, fmt.Errorf("found multiple logs matching name %q", name) + } + found = true + res = append(res, log) } - - newLog := Log{ - Name: log.Name, - Url: log.Url, - Key: log.Key, - State: log.State, - StartInclusive: log.StartInclusive, - EndExclusive: log.EndExclusive, - } - - newGroup[id] = newLog - delete(remaining, newLog.Name) } - if len(newGroup) > 0 { - newList[operator] = newGroup + if !found { + return nil, fmt.Errorf("no log found matching name %q", name) } } - - if len(remaining) > 0 { - missed := make([]string, len(remaining)) - for name := range remaining { - missed = append(missed, fmt.Sprintf("%q", name)) - } - return nil, fmt.Errorf("failed to find logs matching name(s): %s", strings.Join(missed, ", ")) - } - - return newList, nil + return res, nil } // forPurpose returns a new log list containing only those logs whose states are @@ -232,88 +175,55 @@ func (ll List) subset(names []string) (List, error) { // Issuance or Validation and the set of remaining logs is too small to satisfy // the Google "two operators" log policy. func (ll List) forPurpose(p purpose) (List, error) { - newList := make(List) - for operator, group := range ll { - newGroup := make(OperatorGroup) - for id, log := range group { - if !usableForPurpose(log.State, p) { - continue - } - - newLog := Log{ - Name: log.Name, - Url: log.Url, - Key: log.Key, - State: log.State, - StartInclusive: log.StartInclusive, - EndExclusive: log.EndExclusive, - } - - newGroup[id] = newLog - } - if len(newGroup) > 0 { - newList[operator] = newGroup - } - } - - if len(newList) < 2 && p != Informational { - return nil, errors.New("log list does not have enough groups to satisfy Chrome policy") - } - - return newList, nil -} - -// OperatorForLogID returns the Name of the Group containing the Log with the -// given ID, or an error if no such log/group can be found. -func (ll List) OperatorForLogID(logID string) (string, error) { - for op, group := range ll { - if _, found := group[logID]; found { - return op, nil - } - } - return "", fmt.Errorf("no log with ID %q found", logID) -} - -// Permute returns the list of operator group names in a randomized order. -func (ll List) Permute() []string { - keys := make([]string, 0, len(ll)) - for k := range ll { - keys = append(keys, k) - } - - result := make([]string, len(ll)) - for i, j := range rand.Perm(len(ll)) { - result[i] = keys[j] - } - return result -} - -// PickOne returns the URI and Public Key of a single randomly-selected log -// which is run by the given operator and whose temporal interval includes the -// given expiry time. It returns an error if no such log can be found. -func (ll List) PickOne(operator string, expiry time.Time) (string, string, error) { - group, ok := ll[operator] - if !ok { - return "", "", fmt.Errorf("no log operator group named %q", operator) - } - - candidates := make([]Log, 0) - for _, log := range group { - if log.StartInclusive.IsZero() || log.EndExclusive.IsZero() { - candidates = append(candidates, log) + res := make(List, 0) + operators := make(map[string]struct{}) + for _, log := range ll { + if !usableForPurpose(log.State, p) { continue } - if (log.StartInclusive.Equal(expiry) || log.StartInclusive.Before(expiry)) && log.EndExclusive.After(expiry) { - candidates = append(candidates, log) + res = append(res, log) + operators[log.Operator] = struct{}{} + } + + if len(operators) < 2 && p != Informational { + return nil, errors.New("log list does not have enough groups to satisfy Chrome policy") + } + + return res, nil +} + +// ForTime returns a new log list containing only those logs whose temporal +// intervals include the given certificate expiration timestamp. +func (ll List) ForTime(expiry time.Time) List { + res := slices.Clone(ll) + res = slices.DeleteFunc(res, func(l Log) bool { + if (l.StartInclusive.IsZero() || l.StartInclusive.Equal(expiry) || l.StartInclusive.Before(expiry)) && + (l.EndExclusive.IsZero() || l.EndExclusive.After(expiry)) { + return false + } + return true + }) + return res +} + +// Permute returns a new log list containing the exact same logs, but in a +// randomly-shuffled order. +func (ll List) Permute() List { + res := slices.Clone(ll) + rand.Shuffle(len(res), func(i int, j int) { + res[i], res[j] = res[j], res[i] + }) + return res +} + +// GetByID returns the Log matching the given ID, or an error if no such +// log can be found. +func (ll List) GetByID(logID string) (Log, error) { + for _, log := range ll { + if log.Id == logID { + return log, nil } } - - // Ensure rand.Intn below won't panic. - if len(candidates) < 1 { - return "", "", fmt.Errorf("no log found for group %q and expiry %s", operator, expiry) - } - - log := candidates[rand.Intn(len(candidates))] - return log.Url, log.Key, nil + return Log{}, fmt.Errorf("no log with ID %q found", logID) } diff --git a/third-party/github.com/letsencrypt/boulder/ctpolicy/loglist/loglist_test.go b/third-party/github.com/letsencrypt/boulder/ctpolicy/loglist/loglist_test.go index 5646809d5..7490d7895 100644 --- a/third-party/github.com/letsencrypt/boulder/ctpolicy/loglist/loglist_test.go +++ b/third-party/github.com/letsencrypt/boulder/ctpolicy/loglist/loglist_test.go @@ -4,6 +4,9 @@ import ( "testing" "time" + "github.com/google/certificate-transparency-go/loglist3" + "github.com/jmhodges/clock" + "github.com/letsencrypt/boulder/test" ) @@ -13,18 +16,12 @@ func TestNew(t *testing.T) { func TestSubset(t *testing.T) { input := List{ - "Operator A": { - "ID A1": Log{Name: "Log A1"}, - "ID A2": Log{Name: "Log A2"}, - }, - "Operator B": { - "ID B1": Log{Name: "Log B1"}, - "ID B2": Log{Name: "Log B2"}, - }, - "Operator C": { - "ID C1": Log{Name: "Log C1"}, - "ID C2": Log{Name: "Log C2"}, - }, + Log{Name: "Log A1"}, + Log{Name: "Log A2"}, + Log{Name: "Log B1"}, + Log{Name: "Log B2"}, + Log{Name: "Log C1"}, + Log{Name: "Log C2"}, } actual, err := input.subset(nil) @@ -40,13 +37,9 @@ func TestSubset(t *testing.T) { test.AssertEquals(t, len(actual), 0) expected := List{ - "Operator A": { - "ID A1": Log{Name: "Log A1"}, - "ID A2": Log{Name: "Log A2"}, - }, - "Operator B": { - "ID B1": Log{Name: "Log B1"}, - }, + Log{Name: "Log B1"}, + Log{Name: "Log A1"}, + Log{Name: "Log A2"}, } actual, err = input.subset([]string{"Log B1", "Log A1", "Log A2"}) test.AssertNotError(t, err, "normal usage should not error") @@ -55,154 +48,136 @@ func TestSubset(t *testing.T) { func TestForPurpose(t *testing.T) { input := List{ - "Operator A": { - "ID A1": Log{Name: "Log A1", State: usable}, - "ID A2": Log{Name: "Log A2", State: rejected}, - }, - "Operator B": { - "ID B1": Log{Name: "Log B1", State: usable}, - "ID B2": Log{Name: "Log B2", State: retired}, - }, - "Operator C": { - "ID C1": Log{Name: "Log C1", State: pending}, - "ID C2": Log{Name: "Log C2", State: readonly}, - }, + Log{Name: "Log A1", Operator: "A", State: loglist3.UsableLogStatus}, + Log{Name: "Log A2", Operator: "A", State: loglist3.RejectedLogStatus}, + Log{Name: "Log B1", Operator: "B", State: loglist3.UsableLogStatus}, + Log{Name: "Log B2", Operator: "B", State: loglist3.RetiredLogStatus}, + Log{Name: "Log C1", Operator: "C", State: loglist3.PendingLogStatus}, + Log{Name: "Log C2", Operator: "C", State: loglist3.ReadOnlyLogStatus}, } expected := List{ - "Operator A": { - "ID A1": Log{Name: "Log A1", State: usable}, - }, - "Operator B": { - "ID B1": Log{Name: "Log B1", State: usable}, - }, + Log{Name: "Log A1", Operator: "A", State: loglist3.UsableLogStatus}, + Log{Name: "Log B1", Operator: "B", State: loglist3.UsableLogStatus}, } actual, err := input.forPurpose(Issuance) test.AssertNotError(t, err, "should have two acceptable logs") test.AssertDeepEquals(t, actual, expected) input = List{ - "Operator A": { - "ID A1": Log{Name: "Log A1", State: usable}, - "ID A2": Log{Name: "Log A2", State: rejected}, - }, - "Operator B": { - "ID B1": Log{Name: "Log B1", State: qualified}, - "ID B2": Log{Name: "Log B2", State: retired}, - }, - "Operator C": { - "ID C1": Log{Name: "Log C1", State: pending}, - "ID C2": Log{Name: "Log C2", State: readonly}, - }, + Log{Name: "Log A1", Operator: "A", State: loglist3.UsableLogStatus}, + Log{Name: "Log A2", Operator: "A", State: loglist3.RejectedLogStatus}, + Log{Name: "Log B1", Operator: "B", State: loglist3.QualifiedLogStatus}, + Log{Name: "Log B2", Operator: "B", State: loglist3.RetiredLogStatus}, + Log{Name: "Log C1", Operator: "C", State: loglist3.PendingLogStatus}, + Log{Name: "Log C2", Operator: "C", State: loglist3.ReadOnlyLogStatus}, } _, err = input.forPurpose(Issuance) test.AssertError(t, err, "should only have one acceptable log") expected = List{ - "Operator A": { - "ID A1": Log{Name: "Log A1", State: usable}, - }, - "Operator C": { - "ID C2": Log{Name: "Log C2", State: readonly}, - }, + Log{Name: "Log A1", Operator: "A", State: loglist3.UsableLogStatus}, + Log{Name: "Log C2", Operator: "C", State: loglist3.ReadOnlyLogStatus}, } actual, err = input.forPurpose(Validation) test.AssertNotError(t, err, "should have two acceptable logs") test.AssertDeepEquals(t, actual, expected) expected = List{ - "Operator A": { - "ID A1": Log{Name: "Log A1", State: usable}, - }, - "Operator B": { - "ID B1": Log{Name: "Log B1", State: qualified}, - }, - "Operator C": { - "ID C1": Log{Name: "Log C1", State: pending}, - }, + Log{Name: "Log A1", Operator: "A", State: loglist3.UsableLogStatus}, + Log{Name: "Log B1", Operator: "B", State: loglist3.QualifiedLogStatus}, + Log{Name: "Log C1", Operator: "C", State: loglist3.PendingLogStatus}, } actual, err = input.forPurpose(Informational) test.AssertNotError(t, err, "should have three acceptable logs") test.AssertDeepEquals(t, actual, expected) } -func TestOperatorForLogID(t *testing.T) { +func TestForTime(t *testing.T) { + fc := clock.NewFake() + fc.Set(time.Now()) + input := List{ - "Operator A": { - "ID A1": Log{Name: "Log A1", State: usable}, - }, - "Operator B": { - "ID B1": Log{Name: "Log B1", State: qualified}, - }, + Log{Name: "Fully Bound", StartInclusive: fc.Now().Add(-time.Hour), EndExclusive: fc.Now().Add(time.Hour)}, + Log{Name: "Open End", StartInclusive: fc.Now().Add(-time.Hour)}, + Log{Name: "Open Start", EndExclusive: fc.Now().Add(time.Hour)}, + Log{Name: "Fully Open"}, } - actual, err := input.OperatorForLogID("ID B1") - test.AssertNotError(t, err, "should have found log") - test.AssertEquals(t, actual, "Operator B") + expected := List{ + Log{Name: "Fully Bound", StartInclusive: fc.Now().Add(-time.Hour), EndExclusive: fc.Now().Add(time.Hour)}, + Log{Name: "Open End", StartInclusive: fc.Now().Add(-time.Hour)}, + Log{Name: "Open Start", EndExclusive: fc.Now().Add(time.Hour)}, + Log{Name: "Fully Open"}, + } + actual := input.ForTime(fc.Now()) + test.AssertDeepEquals(t, actual, expected) - _, err = input.OperatorForLogID("Other ID") - test.AssertError(t, err, "should not have found log") + expected = List{ + Log{Name: "Fully Bound", StartInclusive: fc.Now().Add(-time.Hour), EndExclusive: fc.Now().Add(time.Hour)}, + Log{Name: "Open End", StartInclusive: fc.Now().Add(-time.Hour)}, + Log{Name: "Open Start", EndExclusive: fc.Now().Add(time.Hour)}, + Log{Name: "Fully Open"}, + } + actual = input.ForTime(fc.Now().Add(-time.Hour)) + test.AssertDeepEquals(t, actual, expected) + + expected = List{ + Log{Name: "Open Start", EndExclusive: fc.Now().Add(time.Hour)}, + Log{Name: "Fully Open"}, + } + actual = input.ForTime(fc.Now().Add(-2 * time.Hour)) + test.AssertDeepEquals(t, actual, expected) + + expected = List{ + Log{Name: "Open End", StartInclusive: fc.Now().Add(-time.Hour)}, + Log{Name: "Fully Open"}, + } + actual = input.ForTime(fc.Now().Add(time.Hour)) + test.AssertDeepEquals(t, actual, expected) } func TestPermute(t *testing.T) { input := List{ - "Operator A": { - "ID A1": Log{Name: "Log A1", State: usable}, - "ID A2": Log{Name: "Log A2", State: rejected}, - }, - "Operator B": { - "ID B1": Log{Name: "Log B1", State: qualified}, - "ID B2": Log{Name: "Log B2", State: retired}, - }, - "Operator C": { - "ID C1": Log{Name: "Log C1", State: pending}, - "ID C2": Log{Name: "Log C2", State: readonly}, - }, + Log{Name: "Log A1"}, + Log{Name: "Log A2"}, + Log{Name: "Log B1"}, + Log{Name: "Log B2"}, + Log{Name: "Log C1"}, + Log{Name: "Log C2"}, } - actual := input.Permute() - test.AssertEquals(t, len(actual), 3) - test.AssertSliceContains(t, actual, "Operator A") - test.AssertSliceContains(t, actual, "Operator B") - test.AssertSliceContains(t, actual, "Operator C") + foundIndices := make(map[string]map[int]int) + for _, log := range input { + foundIndices[log.Name] = make(map[int]int) + } + + for range 100 { + actual := input.Permute() + for index, log := range actual { + foundIndices[log.Name][index]++ + } + } + + for name, counts := range foundIndices { + for index, count := range counts { + if count == 0 { + t.Errorf("Log %s appeared at index %d too few times", name, index) + } + } + } } -func TestPickOne(t *testing.T) { - date0 := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC) - date1 := time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC) - date2 := time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC) - +func TestGetByID(t *testing.T) { input := List{ - "Operator A": { - "ID A1": Log{Name: "Log A1"}, - }, + Log{Name: "Log A1", Id: "ID A1"}, + Log{Name: "Log B1", Id: "ID B1"}, } - _, _, err := input.PickOne("Operator B", date0) - test.AssertError(t, err, "should have failed to find operator") - input = List{ - "Operator A": { - "ID A1": Log{Name: "Log A1", StartInclusive: date0, EndExclusive: date1}, - }, - } - _, _, err = input.PickOne("Operator A", date2) - test.AssertError(t, err, "should have failed to find log") - _, _, err = input.PickOne("Operator A", date1) - test.AssertError(t, err, "should have failed to find log") - _, _, err = input.PickOne("Operator A", date0) - test.AssertNotError(t, err, "should have found a log") - _, _, err = input.PickOne("Operator A", date0.Add(time.Hour)) - test.AssertNotError(t, err, "should have found a log") + expected := Log{Name: "Log A1", Id: "ID A1"} + actual, err := input.GetByID("ID A1") + test.AssertNotError(t, err, "should have found log") + test.AssertDeepEquals(t, actual, expected) - input = List{ - "Operator A": { - "ID A1": Log{Name: "Log A1", StartInclusive: date0, EndExclusive: date1, Key: "KA1", Url: "UA1"}, - "ID A2": Log{Name: "Log A2", StartInclusive: date1, EndExclusive: date2, Key: "KA2", Url: "UA2"}, - "ID B1": Log{Name: "Log B1", StartInclusive: date0, EndExclusive: date1, Key: "KB1", Url: "UB1"}, - "ID B2": Log{Name: "Log B2", StartInclusive: date1, EndExclusive: date2, Key: "KB2", Url: "UB2"}, - }, - } - url, key, err := input.PickOne("Operator A", date0.Add(time.Hour)) - test.AssertNotError(t, err, "should have found a log") - test.AssertSliceContains(t, []string{"UA1", "UB1"}, url) - test.AssertSliceContains(t, []string{"KA1", "KB1"}, key) + _, err = input.GetByID("Other ID") + test.AssertError(t, err, "should not have found log") } diff --git a/third-party/github.com/letsencrypt/boulder/ctpolicy/loglist/schema/log_list_schema.json b/third-party/github.com/letsencrypt/boulder/ctpolicy/loglist/schema/log_list_schema.json deleted file mode 100644 index e0dac92df..000000000 --- a/third-party/github.com/letsencrypt/boulder/ctpolicy/loglist/schema/log_list_schema.json +++ /dev/null @@ -1,280 +0,0 @@ -{ - "type": "object", - "id": "https://www.gstatic.com/ct/log_list/v3/log_list_schema.json", - "$schema": "http://json-schema.org/draft-07/schema", - "required": [ - "operators" - ], - "definitions": { - "state": { - "type": "object", - "properties": { - "timestamp": { - "description": "The time at which the log entered this state.", - "type": "string", - "format": "date-time", - "examples": [ - "2018-01-01T00:00:00Z" - ] - } - }, - "required": [ - "timestamp" - ] - } - }, - "properties": { - "version": { - "type": "string", - "title": "Version of this log list", - "description": "The version will change whenever a change is made to any part of this log list.", - "examples": [ - "1", - "1.0.0", - "1.0.0b" - ] - }, - "log_list_timestamp": { - "description": "The time at which this version of the log list was published.", - "type": "string", - "format": "date-time", - "examples": [ - "2018-01-01T00:00:00Z" - ] - }, - "operators": { - "title": "CT log operators", - "description": "People/organizations that run Certificate Transparency logs.", - "type": "array", - "items": { - "type": "object", - "required": [ - "name", - "email", - "logs" - ], - "properties": { - "name": { - "title": "Name of this log operator", - "type": "string" - }, - "email": { - "title": "CT log operator email addresses", - "description": "The log operator can be contacted using any of these email addresses.", - "type": "array", - "minItems": 1, - "uniqueItems": true, - "items": { - "type": "string", - "format": "email" - } - }, - "logs": { - "description": "Details of Certificate Transparency logs run by this operator.", - "type": "array", - "items": { - "type": "object", - "required": [ - "key", - "log_id", - "mmd", - "url" - ], - "properties": { - "description": { - "title": "Description of the CT log", - "description": "A human-readable description that can be used to identify this log.", - "type": "string" - }, - "key": { - "title": "The public key of the CT log", - "description": "The log's public key as a DER-encoded ASN.1 SubjectPublicKeyInfo structure, then encoded as base64 (https://tools.ietf.org/html/rfc5280#section-4.1.2.7).", - "type": "string" - }, - "log_id": { - "title": "The SHA-256 hash of the CT log's public key, base64-encoded", - "description": "This is the LogID found in SCTs issued by this log (https://tools.ietf.org/html/rfc6962#section-3.2).", - "type": "string", - "minLength": 44, - "maxLength": 44 - }, - "mmd": { - "title": "The Maximum Merge Delay, in seconds", - "description": "The CT log should not take longer than this to incorporate a certificate (https://tools.ietf.org/html/rfc6962#section-3).", - "type": "number", - "minimum": 1, - "default": 86400 - }, - "url": { - "title": "The base URL of the CT log's HTTP API", - "description": "The API endpoints are defined in https://tools.ietf.org/html/rfc6962#section-4.", - "type": "string", - "format": "uri", - "examples": [ - "https://ct.googleapis.com/pilot/" - ] - }, - "dns": { - "title": "The domain name of the CT log's DNS API", - "description": "The API endpoints are defined in https://github.com/google/certificate-transparency-rfcs/blob/master/dns/draft-ct-over-dns.md.", - "type": "string", - "format": "hostname", - "examples": [ - "pilot.ct.googleapis.com" - ] - }, - "temporal_interval": { - "description": "The log will only accept certificates that expire (have a NotAfter date) between these dates.", - "type": "object", - "required": [ - "start_inclusive", - "end_exclusive" - ], - "properties": { - "start_inclusive": { - "description": "All certificates must expire on this date or later.", - "type": "string", - "format": "date-time", - "examples": [ - "2018-01-01T00:00:00Z" - ] - }, - "end_exclusive": { - "description": "All certificates must expire before this date.", - "type": "string", - "format": "date-time", - "examples": [ - "2019-01-01T00:00:00Z" - ] - } - } - }, - "log_type": { - "description": "The purpose of this log, e.g. test.", - "type": "string", - "enum": [ - "prod", - "test" - ] - }, - "state": { - "title": "The state of the log from the log list distributor's perspective.", - "type": "object", - "properties": { - "pending": { - "$ref": "#/definitions/state" - }, - "qualified": { - "$ref": "#/definitions/state" - }, - "usable": { - "$ref": "#/definitions/state" - }, - "readonly": { - "allOf": [ - { - "$ref": "#/definitions/state" - }, - { - "required": [ - "final_tree_head" - ], - "properties": { - "final_tree_head": { - "description": "The tree head (tree size and root hash) at which the log was made read-only.", - "type": "object", - "required": [ - "tree_size", - "sha256_root_hash" - ], - "properties": { - "tree_size": { - "type": "number", - "minimum": 0 - }, - "sha256_root_hash": { - "type": "string", - "minLength": 44, - "maxLength": 44 - } - } - } - } - } - ] - }, - "retired": { - "$ref": "#/definitions/state" - }, - "rejected": { - "$ref": "#/definitions/state" - } - }, - "oneOf": [ - { - "required": [ - "pending" - ] - }, - { - "required": [ - "qualified" - ] - }, - { - "required": [ - "usable" - ] - }, - { - "required": [ - "readonly" - ] - }, - { - "required": [ - "retired" - ] - }, - { - "required": [ - "rejected" - ] - } - ] - }, - "previous_operators": { - "title": "Previous operators that ran this log in the past, if any.", - "description": "If the log has changed operators, this will contain a list of the previous operators, along with the timestamp when they stopped operating the log.", - "type": "array", - "uniqueItems": true, - "items": { - "type": "object", - "required": [ - "name", - "end_time" - ], - "properties": { - "name": { - "title": "Name of the log operator", - "type": "string" - }, - "end_time": { - "description": "The time at which this operator stopped operating this log.", - "type": "string", - "format": "date-time", - "examples": [ - "2018-01-01T00:00:00Z" - ] - } - } - } - } - } - } - } - } - } - } - } -} diff --git a/third-party/github.com/letsencrypt/boulder/ctpolicy/loglist/schema/schema.go b/third-party/github.com/letsencrypt/boulder/ctpolicy/loglist/schema/schema.go deleted file mode 100644 index 79a1957b0..000000000 --- a/third-party/github.com/letsencrypt/boulder/ctpolicy/loglist/schema/schema.go +++ /dev/null @@ -1,269 +0,0 @@ -// Code generated by github.com/atombender/go-jsonschema, DO NOT EDIT. - -package schema - -import "fmt" -import "encoding/json" -import "reflect" - -type LogListSchemaJson struct { - // The time at which this version of the log list was published. - LogListTimestamp *string `json:"log_list_timestamp,omitempty"` - - // People/organizations that run Certificate Transparency logs. - Operators []LogListSchemaJsonOperatorsElem `json:"operators"` - - // The version will change whenever a change is made to any part of this log list. - Version *string `json:"version,omitempty"` -} - -type LogListSchemaJsonOperatorsElem struct { - // The log operator can be contacted using any of these email addresses. - Email []string `json:"email"` - - // Details of Certificate Transparency logs run by this operator. - Logs []LogListSchemaJsonOperatorsElemLogsElem `json:"logs"` - - // Name corresponds to the JSON schema field "name". - Name string `json:"name"` -} - -type LogListSchemaJsonOperatorsElemLogsElem struct { - // A human-readable description that can be used to identify this log. - Description *string `json:"description,omitempty"` - - // The API endpoints are defined in - // https://github.com/google/certificate-transparency-rfcs/blob/master/dns/draft-ct-over-dns.md. - Dns *string `json:"dns,omitempty"` - - // The log's public key as a DER-encoded ASN.1 SubjectPublicKeyInfo structure, - // then encoded as base64 (https://tools.ietf.org/html/rfc5280#section-4.1.2.7). - Key string `json:"key"` - - // This is the LogID found in SCTs issued by this log - // (https://tools.ietf.org/html/rfc6962#section-3.2). - LogId string `json:"log_id"` - - // The purpose of this log, e.g. test. - LogType *LogListSchemaJsonOperatorsElemLogsElemLogType `json:"log_type,omitempty"` - - // The CT log should not take longer than this to incorporate a certificate - // (https://tools.ietf.org/html/rfc6962#section-3). - Mmd float64 `json:"mmd"` - - // If the log has changed operators, this will contain a list of the previous - // operators, along with the timestamp when they stopped operating the log. - PreviousOperators []LogListSchemaJsonOperatorsElemLogsElemPreviousOperatorsElem `json:"previous_operators,omitempty"` - - // State corresponds to the JSON schema field "state". - State *LogListSchemaJsonOperatorsElemLogsElemState `json:"state,omitempty"` - - // The log will only accept certificates that expire (have a NotAfter date) - // between these dates. - TemporalInterval *LogListSchemaJsonOperatorsElemLogsElemTemporalInterval `json:"temporal_interval,omitempty"` - - // The API endpoints are defined in https://tools.ietf.org/html/rfc6962#section-4. - Url string `json:"url"` -} - -type LogListSchemaJsonOperatorsElemLogsElemLogType string - -const LogListSchemaJsonOperatorsElemLogsElemLogTypeProd LogListSchemaJsonOperatorsElemLogsElemLogType = "prod" -const LogListSchemaJsonOperatorsElemLogsElemLogTypeTest LogListSchemaJsonOperatorsElemLogsElemLogType = "test" - -type LogListSchemaJsonOperatorsElemLogsElemPreviousOperatorsElem struct { - // The time at which this operator stopped operating this log. - EndTime string `json:"end_time"` - - // Name corresponds to the JSON schema field "name". - Name string `json:"name"` -} - -type LogListSchemaJsonOperatorsElemLogsElemState struct { - // Pending corresponds to the JSON schema field "pending". - Pending *State `json:"pending,omitempty"` - - // Qualified corresponds to the JSON schema field "qualified". - Qualified *State `json:"qualified,omitempty"` - - // Readonly corresponds to the JSON schema field "readonly". - Readonly interface{} `json:"readonly,omitempty"` - - // Rejected corresponds to the JSON schema field "rejected". - Rejected *State `json:"rejected,omitempty"` - - // Retired corresponds to the JSON schema field "retired". - Retired *State `json:"retired,omitempty"` - - // Usable corresponds to the JSON schema field "usable". - Usable *State `json:"usable,omitempty"` -} - -// The log will only accept certificates that expire (have a NotAfter date) between -// these dates. -type LogListSchemaJsonOperatorsElemLogsElemTemporalInterval struct { - // All certificates must expire before this date. - EndExclusive string `json:"end_exclusive"` - - // All certificates must expire on this date or later. - StartInclusive string `json:"start_inclusive"` -} - -type State struct { - // The time at which the log entered this state. - Timestamp string `json:"timestamp"` -} - -// UnmarshalJSON implements json.Unmarshaler. -func (j *LogListSchemaJsonOperatorsElemLogsElemPreviousOperatorsElem) UnmarshalJSON(b []byte) error { - var raw map[string]interface{} - if err := json.Unmarshal(b, &raw); err != nil { - return err - } - if v, ok := raw["end_time"]; !ok || v == nil { - return fmt.Errorf("field end_time: required") - } - if v, ok := raw["name"]; !ok || v == nil { - return fmt.Errorf("field name: required") - } - type Plain LogListSchemaJsonOperatorsElemLogsElemPreviousOperatorsElem - var plain Plain - if err := json.Unmarshal(b, &plain); err != nil { - return err - } - *j = LogListSchemaJsonOperatorsElemLogsElemPreviousOperatorsElem(plain) - return nil -} - -// UnmarshalJSON implements json.Unmarshaler. -func (j *LogListSchemaJsonOperatorsElemLogsElemTemporalInterval) UnmarshalJSON(b []byte) error { - var raw map[string]interface{} - if err := json.Unmarshal(b, &raw); err != nil { - return err - } - if v, ok := raw["end_exclusive"]; !ok || v == nil { - return fmt.Errorf("field end_exclusive: required") - } - if v, ok := raw["start_inclusive"]; !ok || v == nil { - return fmt.Errorf("field start_inclusive: required") - } - type Plain LogListSchemaJsonOperatorsElemLogsElemTemporalInterval - var plain Plain - if err := json.Unmarshal(b, &plain); err != nil { - return err - } - *j = LogListSchemaJsonOperatorsElemLogsElemTemporalInterval(plain) - return nil -} - -// UnmarshalJSON implements json.Unmarshaler. -func (j *LogListSchemaJsonOperatorsElemLogsElemLogType) UnmarshalJSON(b []byte) error { - var v string - if err := json.Unmarshal(b, &v); err != nil { - return err - } - var ok bool - for _, expected := range enumValues_LogListSchemaJsonOperatorsElemLogsElemLogType { - if reflect.DeepEqual(v, expected) { - ok = true - break - } - } - if !ok { - return fmt.Errorf("invalid value (expected one of %#v): %#v", enumValues_LogListSchemaJsonOperatorsElemLogsElemLogType, v) - } - *j = LogListSchemaJsonOperatorsElemLogsElemLogType(v) - return nil -} - -// UnmarshalJSON implements json.Unmarshaler. -func (j *LogListSchemaJsonOperatorsElemLogsElem) UnmarshalJSON(b []byte) error { - var raw map[string]interface{} - if err := json.Unmarshal(b, &raw); err != nil { - return err - } - if v, ok := raw["key"]; !ok || v == nil { - return fmt.Errorf("field key: required") - } - if v, ok := raw["log_id"]; !ok || v == nil { - return fmt.Errorf("field log_id: required") - } - if v, ok := raw["url"]; !ok || v == nil { - return fmt.Errorf("field url: required") - } - type Plain LogListSchemaJsonOperatorsElemLogsElem - var plain Plain - if err := json.Unmarshal(b, &plain); err != nil { - return err - } - if v, ok := raw["mmd"]; !ok || v == nil { - plain.Mmd = 86400 - } - *j = LogListSchemaJsonOperatorsElemLogsElem(plain) - return nil -} - -// UnmarshalJSON implements json.Unmarshaler. -func (j *State) UnmarshalJSON(b []byte) error { - var raw map[string]interface{} - if err := json.Unmarshal(b, &raw); err != nil { - return err - } - if v, ok := raw["timestamp"]; !ok || v == nil { - return fmt.Errorf("field timestamp: required") - } - type Plain State - var plain Plain - if err := json.Unmarshal(b, &plain); err != nil { - return err - } - *j = State(plain) - return nil -} - -// UnmarshalJSON implements json.Unmarshaler. -func (j *LogListSchemaJsonOperatorsElem) UnmarshalJSON(b []byte) error { - var raw map[string]interface{} - if err := json.Unmarshal(b, &raw); err != nil { - return err - } - if v, ok := raw["email"]; !ok || v == nil { - return fmt.Errorf("field email: required") - } - if v, ok := raw["logs"]; !ok || v == nil { - return fmt.Errorf("field logs: required") - } - if v, ok := raw["name"]; !ok || v == nil { - return fmt.Errorf("field name: required") - } - type Plain LogListSchemaJsonOperatorsElem - var plain Plain - if err := json.Unmarshal(b, &plain); err != nil { - return err - } - *j = LogListSchemaJsonOperatorsElem(plain) - return nil -} - -var enumValues_LogListSchemaJsonOperatorsElemLogsElemLogType = []interface{}{ - "prod", - "test", -} - -// UnmarshalJSON implements json.Unmarshaler. -func (j *LogListSchemaJson) UnmarshalJSON(b []byte) error { - var raw map[string]interface{} - if err := json.Unmarshal(b, &raw); err != nil { - return err - } - if v, ok := raw["operators"]; !ok || v == nil { - return fmt.Errorf("field operators: required") - } - type Plain LogListSchemaJson - var plain Plain - if err := json.Unmarshal(b, &plain); err != nil { - return err - } - *j = LogListSchemaJson(plain) - return nil -} diff --git a/third-party/github.com/letsencrypt/boulder/ctpolicy/loglist/schema/update.sh b/third-party/github.com/letsencrypt/boulder/ctpolicy/loglist/schema/update.sh deleted file mode 100644 index b5a6c8c8d..000000000 --- a/third-party/github.com/letsencrypt/boulder/ctpolicy/loglist/schema/update.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env bash - -set -e - -# This script updates the log list JSON Schema and the Go structs generated -# from that schema. - -# It is not intended to be run on a regular basis; we do not expect the JSON -# Schema to change. It is retained here for historical purposes, so that if/when -# the schema does change, or the ecosystem moves to a v4 version of the schema, -# regenerating these files will be quick and easy. - -# This script expects github.com/atombender/go-jsonschema to be installed: -if ! command -v gojsonschema -then - echo "Install gojsonschema, then re-run this script:" - echo "go install github.com/atombender/go-jsonschema/cmd/gojsonschema@latest" -fi - -this_dir=$(dirname $(readlink -f "${0}")) - -curl https://www.gstatic.com/ct/log_list/v3/log_list_schema.json >| "${this_dir}"/log_list_schema.json - -gojsonschema -p schema "${this_dir}"/log_list_schema.json >| "${this_dir}"/schema.go diff --git a/third-party/github.com/letsencrypt/boulder/db/interfaces.go b/third-party/github.com/letsencrypt/boulder/db/interfaces.go index f08e25888..99f701d4a 100644 --- a/third-party/github.com/letsencrypt/boulder/db/interfaces.go +++ b/third-party/github.com/letsencrypt/boulder/db/interfaces.go @@ -58,17 +58,9 @@ type Executor interface { OneSelector Inserter SelectExecer - Queryer Delete(context.Context, ...interface{}) (int64, error) Get(context.Context, interface{}, ...interface{}) (interface{}, error) Update(context.Context, ...interface{}) (int64, error) -} - -// Queryer offers the QueryContext method. Note that this is not read-only (i.e. not -// Selector), since a QueryContext can be `INSERT`, `UPDATE`, etc. The difference -// between QueryContext and ExecContext is that QueryContext can return rows. So for instance it is -// suitable for inserting rows and getting back ids. -type Queryer interface { QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) } diff --git a/third-party/github.com/letsencrypt/boulder/db/map.go b/third-party/github.com/letsencrypt/boulder/db/map.go index 4abd2dce5..642fdf70c 100644 --- a/third-party/github.com/letsencrypt/boulder/db/map.go +++ b/third-party/github.com/letsencrypt/boulder/db/map.go @@ -129,6 +129,18 @@ func (m *WrappedMap) BeginTx(ctx context.Context) (Transaction, error) { }, err } +func (m *WrappedMap) ColumnsForModel(model interface{}) ([]string, error) { + tbl, err := m.dbMap.TableFor(reflect.TypeOf(model), true) + if err != nil { + return nil, err + } + var columns []string + for _, col := range tbl.Columns { + columns = append(columns, col.ColumnName) + } + return columns, nil +} + // WrappedTransaction wraps a *borp.Transaction such that its major functions // wrap error results in ErrDatabaseOp instances before returning them to the // caller. diff --git a/third-party/github.com/letsencrypt/boulder/db/map_test.go b/third-party/github.com/letsencrypt/boulder/db/map_test.go index 19fdd7fe4..a65a54d08 100644 --- a/third-party/github.com/letsencrypt/boulder/db/map_test.go +++ b/third-party/github.com/letsencrypt/boulder/db/map_test.go @@ -10,6 +10,7 @@ import ( "github.com/letsencrypt/borp" "github.com/go-sql-driver/mysql" + "github.com/letsencrypt/boulder/core" "github.com/letsencrypt/boulder/test" "github.com/letsencrypt/boulder/test/vars" @@ -122,7 +123,7 @@ func TestTableFromQuery(t *testing.T) { expectedTable string }{ { - query: "SELECT id, jwk, jwk_sha256, contact, agreement, initialIP, createdAt, LockCol, status FROM registrations WHERE jwk_sha256 = ?", + query: "SELECT id, jwk, jwk_sha256, contact, agreement, createdAt, LockCol, status FROM registrations WHERE jwk_sha256 = ?", expectedTable: "registrations", }, { @@ -134,15 +135,15 @@ func TestTableFromQuery(t *testing.T) { expectedTable: "authz2", }, { - query: "insert into `registrations` (`id`,`jwk`,`jw k_sha256`,`contact`,`agreement`,`initialIp`,`createdAt`,`LockCol`,`status`) values (null,?,?,?,?,?,?,?,?);", + query: "insert into `registrations` (`id`,`jwk`,`jw k_sha256`,`contact`,`agreement`,`createdAt`,`LockCol`,`status`) values (null,?,?,?,?,?,?,?,?);", expectedTable: "`registrations`", }, { - query: "update `registrations` set `jwk`=?, `jwk_sh a256`=?, `contact`=?, `agreement`=?, `initialIp`=?, `createdAt`=?, `LockCol` =?, `status`=? where `id`=? and `LockCol`=?;", + query: "update `registrations` set `jwk`=?, `jwk_sh a256`=?, `contact`=?, `agreement`=?, `createdAt`=?, `LockCol` =?, `status`=? where `id`=? and `LockCol`=?;", expectedTable: "`registrations`", }, { - query: "SELECT COUNT(*) FROM registrations WHERE initialIP = ? AND ? < createdAt AND createdAt <= ?", + query: "SELECT COUNT(*) FROM registrations WHERE ? < createdAt AND createdAt <= ?", expectedTable: "registrations", }, { @@ -185,10 +186,6 @@ func TestTableFromQuery(t *testing.T) { query: "insert into `certificates` (`registrationID`,`serial`,`digest`,`der`,`issued`,`expires`) values (?,?,?,?,?,?);", expectedTable: "`certificates`", }, - { - query: "INSERT INTO certificatesPerName (eTLDPlusOne, time, count) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE count=count+1;", - expectedTable: "certificatesPerName", - }, { query: "insert into `fqdnSets` (`ID`,`SetHash`,`Serial`,`Issued`,`Expires`) values (null,?,?,?,?);", expectedTable: "`fqdnSets`", diff --git a/third-party/github.com/letsencrypt/boulder/db/multi.go b/third-party/github.com/letsencrypt/boulder/db/multi.go index bcb2fbe3f..04e7ecc8f 100644 --- a/third-party/github.com/letsencrypt/boulder/db/multi.go +++ b/third-party/github.com/letsencrypt/boulder/db/multi.go @@ -7,29 +7,24 @@ import ( ) // MultiInserter makes it easy to construct a -// `INSERT INTO table (...) VALUES ... RETURNING id;` +// `INSERT INTO table (...) VALUES ...;` // query which inserts multiple rows into the same table. It can also execute // the resulting query. type MultiInserter struct { // These are validated by the constructor as containing only characters // that are allowed in an unquoted identifier. // https://mariadb.com/kb/en/identifier-names/#unquoted - table string - fields []string - returningColumn string + table string + fields []string values [][]interface{} } // NewMultiInserter creates a new MultiInserter, checking for reasonable table -// name and list of fields. returningColumn is the name of a column to be used -// in a `RETURNING xyz` clause at the end. If it is empty, no `RETURNING xyz` -// clause is used. If returningColumn is present, it must refer to a column -// that can be parsed into an int64. -// Safety: `table`, `fields`, and `returningColumn` must contain only strings -// that are known at compile time. They must not contain user-controlled -// strings. -func NewMultiInserter(table string, fields []string, returningColumn string) (*MultiInserter, error) { +// name and list of fields. +// Safety: `table` and `fields` must contain only strings that are known at +// compile time. They must not contain user-controlled strings. +func NewMultiInserter(table string, fields []string) (*MultiInserter, error) { if len(table) == 0 || len(fields) == 0 { return nil, fmt.Errorf("empty table name or fields list") } @@ -44,18 +39,11 @@ func NewMultiInserter(table string, fields []string, returningColumn string) (*M return nil, err } } - if returningColumn != "" { - err := validMariaDBUnquotedIdentifier(returningColumn) - if err != nil { - return nil, err - } - } return &MultiInserter{ - table: table, - fields: fields, - returningColumn: returningColumn, - values: make([][]interface{}, 0), + table: table, + fields: fields, + values: make([][]interface{}, 0), }, nil } @@ -84,56 +72,36 @@ func (mi *MultiInserter) query() (string, []interface{}) { questions := strings.TrimRight(questionsBuf.String(), ",") - // Safety: we are interpolating `mi.returningColumn` into an SQL query. We - // know it is a valid unquoted identifier in MariaDB because we verified - // that in the constructor. - returning := "" - if mi.returningColumn != "" { - returning = fmt.Sprintf(" RETURNING %s", mi.returningColumn) - } // Safety: we are interpolating `mi.table` and `mi.fields` into an SQL // query. We know they contain, respectively, a valid unquoted identifier // and a slice of valid unquoted identifiers because we verified that in // the constructor. We know the query overall has valid syntax because we // generate it entirely within this function. - query := fmt.Sprintf("INSERT INTO %s (%s) VALUES %s%s", mi.table, strings.Join(mi.fields, ","), questions, returning) + query := fmt.Sprintf("INSERT INTO %s (%s) VALUES %s", mi.table, strings.Join(mi.fields, ","), questions) return query, queryArgs } // Insert inserts all the collected rows into the database represented by -// `queryer`. If a non-empty returningColumn was provided, then it returns -// the list of values from that column returned by the query. -func (mi *MultiInserter) Insert(ctx context.Context, queryer Queryer) ([]int64, error) { +// `queryer`. +func (mi *MultiInserter) Insert(ctx context.Context, db Execer) error { + if len(mi.values) == 0 { + return nil + } + query, queryArgs := mi.query() - rows, err := queryer.QueryContext(ctx, query, queryArgs...) + res, err := db.ExecContext(ctx, query, queryArgs...) if err != nil { - return nil, err + return err } - ids := make([]int64, 0, len(mi.values)) - if mi.returningColumn != "" { - for rows.Next() { - var id int64 - err = rows.Scan(&id) - if err != nil { - rows.Close() - return nil, err - } - ids = append(ids, id) - } + affected, err := res.RowsAffected() + if err != nil { + return err + } + if affected != int64(len(mi.values)) { + return fmt.Errorf("unexpected number of rows inserted: %d != %d", affected, len(mi.values)) } - // Hack: sometimes in unittests we make a mock Queryer that returns a nil - // `*sql.Rows`. A nil `*sql.Rows` is not actually valid— calling `Close()` - // on it will panic— but here we choose to treat it like an empty list, - // and skip calling `Close()` to avoid the panic. - if rows != nil { - err = rows.Close() - if err != nil { - return nil, err - } - } - - return ids, nil + return nil } diff --git a/third-party/github.com/letsencrypt/boulder/db/multi_test.go b/third-party/github.com/letsencrypt/boulder/db/multi_test.go index f972f4748..d866699bf 100644 --- a/third-party/github.com/letsencrypt/boulder/db/multi_test.go +++ b/third-party/github.com/letsencrypt/boulder/db/multi_test.go @@ -7,34 +7,29 @@ import ( ) func TestNewMulti(t *testing.T) { - _, err := NewMultiInserter("", []string{"colA"}, "") + _, err := NewMultiInserter("", []string{"colA"}) test.AssertError(t, err, "Empty table name should fail") - _, err = NewMultiInserter("myTable", nil, "") + _, err = NewMultiInserter("myTable", nil) test.AssertError(t, err, "Empty fields list should fail") - mi, err := NewMultiInserter("myTable", []string{"colA"}, "") + mi, err := NewMultiInserter("myTable", []string{"colA"}) test.AssertNotError(t, err, "Single-column construction should not fail") test.AssertEquals(t, len(mi.fields), 1) - mi, err = NewMultiInserter("myTable", []string{"colA", "colB", "colC"}, "") + mi, err = NewMultiInserter("myTable", []string{"colA", "colB", "colC"}) test.AssertNotError(t, err, "Multi-column construction should not fail") test.AssertEquals(t, len(mi.fields), 3) - _, err = NewMultiInserter("", []string{"colA"}, "colB") - test.AssertError(t, err, "expected error for empty table name") - _, err = NewMultiInserter("foo\"bar", []string{"colA"}, "colB") + _, err = NewMultiInserter("foo\"bar", []string{"colA"}) test.AssertError(t, err, "expected error for invalid table name") - _, err = NewMultiInserter("myTable", []string{"colA", "foo\"bar"}, "colB") + _, err = NewMultiInserter("myTable", []string{"colA", "foo\"bar"}) test.AssertError(t, err, "expected error for invalid column name") - - _, err = NewMultiInserter("myTable", []string{"colA"}, "foo\"bar") - test.AssertError(t, err, "expected error for invalid returning column name") } func TestMultiAdd(t *testing.T) { - mi, err := NewMultiInserter("table", []string{"a", "b", "c"}, "") + mi, err := NewMultiInserter("table", []string{"a", "b", "c"}) test.AssertNotError(t, err, "Failed to create test MultiInserter") err = mi.Add([]interface{}{}) @@ -57,7 +52,7 @@ func TestMultiAdd(t *testing.T) { } func TestMultiQuery(t *testing.T) { - mi, err := NewMultiInserter("table", []string{"a", "b", "c"}, "") + mi, err := NewMultiInserter("table", []string{"a", "b", "c"}) test.AssertNotError(t, err, "Failed to create test MultiInserter") err = mi.Add([]interface{}{"one", "two", "three"}) test.AssertNotError(t, err, "Failed to insert test row") @@ -67,15 +62,4 @@ func TestMultiQuery(t *testing.T) { query, queryArgs := mi.query() test.AssertEquals(t, query, "INSERT INTO table (a,b,c) VALUES (?,?,?),(?,?,?)") test.AssertDeepEquals(t, queryArgs, []interface{}{"one", "two", "three", "egy", "kettö", "három"}) - - mi, err = NewMultiInserter("table", []string{"a", "b", "c"}, "id") - test.AssertNotError(t, err, "Failed to create test MultiInserter") - err = mi.Add([]interface{}{"one", "two", "three"}) - test.AssertNotError(t, err, "Failed to insert test row") - err = mi.Add([]interface{}{"egy", "kettö", "három"}) - test.AssertNotError(t, err, "Failed to insert test row") - - query, queryArgs = mi.query() - test.AssertEquals(t, query, "INSERT INTO table (a,b,c) VALUES (?,?,?),(?,?,?) RETURNING id") - test.AssertDeepEquals(t, queryArgs, []interface{}{"one", "two", "three", "egy", "kettö", "három"}) } diff --git a/third-party/github.com/letsencrypt/boulder/docker-compose.next.yml b/third-party/github.com/letsencrypt/boulder/docker-compose.next.yml index b18fb5ee7..4a1578509 100644 --- a/third-party/github.com/letsencrypt/boulder/docker-compose.next.yml +++ b/third-party/github.com/letsencrypt/boulder/docker-compose.next.yml @@ -1,7 +1,7 @@ services: boulder: environment: - FAKE_DNS: 10.77.77.77 + FAKE_DNS: 64.112.117.122 BOULDER_CONFIG_DIR: test/config-next GOFLAGS: -mod=vendor GOCACHE: /boulder/.gocache/go-build-next diff --git a/third-party/github.com/letsencrypt/boulder/docker-compose.yml b/third-party/github.com/letsencrypt/boulder/docker-compose.yml index f25309579..8092b1522 100644 --- a/third-party/github.com/letsencrypt/boulder/docker-compose.yml +++ b/third-party/github.com/letsencrypt/boulder/docker-compose.yml @@ -8,12 +8,12 @@ services: context: test/boulder-tools/ # Should match one of the GO_CI_VERSIONS in test/boulder-tools/tag_and_upload.sh. args: - GO_VERSION: 1.22.2 + GO_VERSION: 1.24.1 environment: # To solve HTTP-01 and TLS-ALPN-01 challenges, change the IP in FAKE_DNS - # to the IP address where your ACME client's solver is listening. - # FAKE_DNS: 172.17.0.1 - FAKE_DNS: 10.77.77.77 + # to the IP address where your ACME client's solver is listening. This is + # pointing at the boulder service's "public" IP, where challtestsrv is. + FAKE_DNS: 64.112.117.122 BOULDER_CONFIG_DIR: test/config GOCACHE: /boulder/.gocache/go-build GOFLAGS: -mod=vendor @@ -24,12 +24,10 @@ services: networks: bouldernet: ipv4_address: 10.77.77.77 - integrationtestnet: - ipv4_address: 10.88.88.88 - redisnet: - ipv4_address: 10.33.33.33 - consulnet: - ipv4_address: 10.55.55.55 + publicnet: + ipv4_address: 64.112.117.122 + publicnet2: + ipv4_address: 64.112.117.134 # Use consul as a backup to Docker's embedded DNS server. If there's a name # Docker's DNS server doesn't know about, it will forward the query to this # IP (running consul). @@ -38,16 +36,21 @@ services: # are configured via the ServerAddress field of cmd.GRPCClientConfig. # TODO: Remove this when ServerAddress is deprecated in favor of SRV records # and DNSAuthority. - dns: 10.55.55.10 + dns: 10.77.77.10 extra_hosts: - # Allow the boulder container to be reached as "ca.example.org", so that - # we can put that name inside our integration test certs (e.g. as a crl + # Allow the boulder container to be reached as "ca.example.org", so we + # can put that name inside our integration test certs (e.g. as a crl # url) and have it look like a publicly-accessible name. - - "ca.example.org:10.77.77.77" + # TODO(#8215): Move s3-test-srv to a separate service. + - "ca.example.org:64.112.117.122" + # Allow the boulder container to be reached as "integration.trust", for + # similar reasons, but intended for use as a SAN rather than a CRLDP. + # TODO(#8215): Move observer's probe target to a separate service. + - "integration.trust:64.112.117.122" ports: - 4001:4001 # ACMEv2 - 4002:4002 # OCSP - - 4003:4003 # OCSP + - 4003:4003 # SFE depends_on: - bmysql - bproxysql @@ -57,7 +60,7 @@ services: - bredis_4 - bconsul - bjaeger - - bpkilint + - bpkimetal entrypoint: test/entrypoint.sh working_dir: &boulder_working_dir /boulder @@ -76,7 +79,7 @@ services: - setup bmysql: - image: mariadb:10.5 + image: mariadb:10.6.22 networks: bouldernet: aliases: @@ -91,6 +94,7 @@ services: command: mysqld --bind-address=0.0.0.0 --slow-query-log --log-output=TABLE --log-queries-not-using-indexes=ON logging: driver: none + bproxysql: image: proxysql/proxysql:2.5.4 # The --initial flag force resets the ProxySQL database on startup. By @@ -113,8 +117,12 @@ services: - ./test/:/test/:cached command: redis-server /test/redis-ocsp.config networks: - redisnet: - ipv4_address: 10.33.33.2 + bouldernet: + # TODO(#8215): Remove this static IP allocation (and similar below) when + # we tear down ocsp-responder. We only have it because ocsp-responder + # requires IPs in its "ShardAddrs" config, while ratelimit redis + # supports looking up shards via hostname and SRV record. + ipv4_address: 10.77.77.2 bredis_2: image: redis:6.2.7 @@ -122,8 +130,8 @@ services: - ./test/:/test/:cached command: redis-server /test/redis-ocsp.config networks: - redisnet: - ipv4_address: 10.33.33.3 + bouldernet: + ipv4_address: 10.77.77.3 bredis_3: image: redis:6.2.7 @@ -131,8 +139,8 @@ services: - ./test/:/test/:cached command: redis-server /test/redis-ratelimits.config networks: - redisnet: - ipv4_address: 10.33.33.4 + bouldernet: + ipv4_address: 10.77.77.4 bredis_4: image: redis:6.2.7 @@ -140,16 +148,14 @@ services: - ./test/:/test/:cached command: redis-server /test/redis-ratelimits.config networks: - redisnet: - ipv4_address: 10.33.33.5 + bouldernet: + ipv4_address: 10.77.77.5 bconsul: image: hashicorp/consul:1.15.4 volumes: - ./test/:/test/:cached networks: - consulnet: - ipv4_address: 10.55.55.10 bouldernet: ipv4_address: 10.77.77.10 command: "consul agent -dev -config-format=hcl -config-file=/test/consul/config.hcl" @@ -157,28 +163,42 @@ services: bjaeger: image: jaegertracing/all-in-one:1.50 networks: - bouldernet: - ipv4_address: 10.77.77.17 + - bouldernet - bpkilint: - image: ghcr.io/digicert/pkilint:v0.10.1 + bpkimetal: + image: ghcr.io/pkimetal/pkimetal:v1.20.0 networks: - bouldernet: - ipv4_address: 10.77.77.9 - command: "gunicorn -w 8 -k uvicorn.workers.UvicornWorker -b 0.0.0.0:80 pkilint.rest:app" + - bouldernet networks: - # This network is primarily used for boulder services. It is also used by - # challtestsrv, which is used in the integration tests. + # This network represents the data-center internal network. It is used for + # boulder services and their infrastructure, such as consul, mariadb, and + # redis. bouldernet: driver: bridge ipam: driver: default config: - subnet: 10.77.77.0/24 + # Only issue DHCP addresses in the top half of the range, to avoid + # conflict with static addresses. + ip_range: 10.77.77.128/25 + + # This network represents the public internet. It uses a real public IP space + # (that Let's Encrypt controls) so that our integration tests are happy to + # validate and issue for it. It is used by challtestsrv, which binds to + # 64.112.117.122:80 and :443 for its HTTP-01 challenge responder. + # + # TODO(#8215): Put akamai-test-srv and s3-test-srv on this network. + publicnet: + driver: bridge + ipam: + driver: default + config: + - subnet: 64.112.117.0/25 # This network is used for two things in the integration tests: - # - challtestsrv binds to 10.88.88.88:443 for its tls-alpn-01 challenge + # - challtestsrv binds to 64.112.117.134:443 for its tls-alpn-01 challenge # responder, to avoid interfering with the HTTPS port used for testing # HTTP->HTTPS redirects during http-01 challenges. Note: this could # probably be updated in the future so that challtestsrv can handle @@ -186,24 +206,13 @@ networks: # - test/v2_integration.py has some test cases that start their own HTTP # server instead of relying on challtestsrv, because they want very # specific behavior. For these cases, v2_integration.py creates a Python - # HTTP server and binds it to 10.88.88.88:80. - integrationtestnet: + # HTTP server and binds it to 64.112.117.134:80. + # + # TODO(#8215): Deprecate this network, replacing it with individual IPs within + # the existing publicnet. + publicnet2: driver: bridge ipam: driver: default config: - - subnet: 10.88.88.0/24 - - redisnet: - driver: bridge - ipam: - driver: default - config: - - subnet: 10.33.33.0/24 - - consulnet: - driver: bridge - ipam: - driver: default - config: - - subnet: 10.55.55.0/24 + - subnet: 64.112.117.128/25 diff --git a/third-party/github.com/letsencrypt/boulder/docs/CONTRIBUTING.md b/third-party/github.com/letsencrypt/boulder/docs/CONTRIBUTING.md index 7e311ae9e..1df57bf78 100644 --- a/third-party/github.com/letsencrypt/boulder/docs/CONTRIBUTING.md +++ b/third-party/github.com/letsencrypt/boulder/docs/CONTRIBUTING.md @@ -33,6 +33,19 @@ guidelines for Boulder contributions. * Are there new RPCs or config fields? Make sure the patch meets the Deployability rules below. +# Merge Requirements + +We have a bot that will comment on some PRs indicating there are: + + 1. configuration changes + 2. SQL schema changes + 3. feature flag changes + +These may require either a CP/CPS review or filing of a ticket to make matching changes +in production. It is the responsibility of the person merging the PR to make sure +the required action has been performed before merging. Usually this will be confirmed +in a comment or in the PR description. + # Patch Guidelines * Please include helpful comments. No need to gratuitously comment clear code, @@ -139,11 +152,12 @@ separate deploy-triggered problems from config-triggered problems. When adding significant new features or replacing existing RPCs the `boulder/features` package should be used to gate its usage. To add a flag, a -new `const FeatureFlag` should be added and its default value specified in -`features.features` in `features/features.go`. In order to test if the flag -is enabled elsewhere in the codebase you can use -`features.Enabled(features.ExampleFeatureName)` which returns a `bool` -indicating if the flag is enabled or not. +new field of the `features.Config` struct should be added. All flags default +to false. + +In order to test if the flag is enabled elsewhere in the codebase you can use +`features.Get().ExampleFeatureName` which gets the `bool` value from a global +config. Each service should include a `map[string]bool` named `Features` in its configuration object at the top level and call `features.Set` with that map @@ -160,13 +174,24 @@ immediately after parsing the configuration. For example to enable } ``` -Avoid negative flag names such as `"DontCancelRequest": false` because such -names are difficult to reason about. - Feature flags are meant to be used temporarily and should not be used for -permanent boolean configuration options. Once a feature has been enabled in -both staging and production the flag should be removed making the previously -gated functionality the default in future deployments. +permanent boolean configuration options. + +### Deprecating a feature flag + +Once a feature has been enabled in both staging and production, someone on the +team should deprecate it: + + - Remove any instances of `features.Get().ExampleFeatureName`, adjusting code + as needed. + - Move the field to the top of the `features.Config` struct, under a comment + saying it's deprecated. + - Remove all references to the feature flag from `test/config-next`. + - Add the feature flag to `test/config`. This serves to check that we still + tolerate parsing the flag at startup, even though it is ineffective. + - File a ticket to remove the feature flag in staging and production. + - Once the feature flag is removed in staging and production, delete it from + `test/config` and `features.Config`. ### Gating RPCs @@ -326,7 +351,7 @@ must check that timestamps are non-zero before accepting them. # Rounding time in DB -All times that we write to the database are truncated to one second's worth of +All times that we send to the database are truncated to one second's worth of precision. This reduces the size of indexes that include timestamps, and makes querying them more efficient. The Storage Authority (SA) is responsible for this truncation, and performs it for SELECT queries as well as INSERT and UPDATE. diff --git a/third-party/github.com/letsencrypt/boulder/docs/CRLS.md b/third-party/github.com/letsencrypt/boulder/docs/CRLS.md new file mode 100644 index 000000000..2faddedf3 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/docs/CRLS.md @@ -0,0 +1,89 @@ +# CRLs + +For each issuer certificate, Boulder generates several sharded CRLs. +The responsibility is shared across these components: + + - crl-updater + - sa + - ca + - crl-storer + +The crl-updater starts the process: for each shard of each issuer, +it requests revoked certificate information from the SA. It sends +that information to the CA for signing, and receives back a signed +CRL. It sends the signed CRL to the crl-storer for upload to an +S3-compatible data store. + +The crl-storer uploads the CRLs to the filename `/.crl`, +where `issuerID` is an integer that uniquely identifies the Subject of +the issuer certificate (based on hashing the Subject's encoded bytes). + +There's one more component that's not in this repository: an HTTP server +to serve objects from the S3-compatible data store. For Let's Encrypt, this +role is served by a CDN. Note that the CA must be carefully configured so +that the CRLBaseURL for each issuer matches the publicly accessible URL +where that issuer's CRLs will be served. + +## Shard assignment + +Certificates are assigned to shards one of two ways: temporally or explicitly. +Temporal shard assignment places certificates into shards based on their +notAfter. Explicit shard assignment places certificates into shards based +on the (random) low bytes of their serial numbers. + +Boulder distinguishes the two types of sharding by the one-byte (two hex +encoded bytes) prefix on the serial number, configured at the CA. +When enabling explicit sharding at the CA, operators should at the same +time change the CA's configured serial prefix. Also, the crl-updater should +be configured with `temporallyShardedPrefixes` set to the _old_ serial prefix. + +An explicitly sharded certificate will always have the CRLDistributionPoints +extension, containing a URL that points to its CRL shard. A temporally sharded +certificate will never have that extension. + +As of Jan 2025, we are planning to turn on explicit sharding for new +certificates soon. Once all temporally sharded certificates have expired, we +will remove the code for temporal sharding. + +## Storage + +When a certificate is revoked, its status in the `certificateStatus` table is +always updated. If that certificate has an explicit shard, an entry in the +`revokedCertificates` table is also added or updated. Note: the certificateStatus +table has an entry for every certificate, even unrevoked ones. The +`revokedCertificates` table only has entries for revoked certificates. + +The SA exposes the two different types of recordkeeping in two different ways: +`GetRevokedCerts` returns revoked certificates whose NotAfter dates fall +within a requested range. This is used for temporal sharding. +`GetRevokedCertsByShard` returns revoked certificates whose `shardIdx` matches +the requested shard. + +For each shard, the crl-storer queries both methods. Typically a certificate +will have a different temporal shard than its explicit shard, so for a +transition period, revoked certs may show up in two different CRL shards. +A fraction of certificates will have the same temporal shard as their explicit +shard. To avoid including the same serial twice in the same sharded CRL, the +crl-updater de-duplicates by serial number. + +## Enabling explicit sharding + +Explicit sharding is enabled at the CA by configuring each issuer with a number +of CRL shards. This number must be the same across all issuers and must match +the number of shards configured on the crl-updater. As part of the same config +deploy, the CA must be updated to issue using a new serial prefix. Note: the +ocsp-responder must also be updated to recognize the new serial prefix. + +The crl-updater must also be updated to add the `temporallyShardedPrefixes` +field, listing the _old_ serial prefixes (i.e., those that were issued by a CA +that did not include the CRLDistributionPoints extension). + +Once we've turned on explicit sharding, we can turn it back off. However, for +the certificates we've already issued, we are still committed to serving their +revocations in the CRL hosted at the URL embedded in those certificates. +Fortunately, all of the revocation and storage elements that rely on explicit +sharding are gated by the contents of the certificate being revoked (specifically, +the presence of CRLDistributionPoints). So even if we turn off explicit sharding +for new certificates, we will still do the right thing at revocation time and +CRL generation time for any already existing certificates that have a +CRLDistributionPoints extension. diff --git a/third-party/github.com/letsencrypt/boulder/docs/DESIGN.md b/third-party/github.com/letsencrypt/boulder/docs/DESIGN.md index 3fd6f8053..032c92f0c 100644 --- a/third-party/github.com/letsencrypt/boulder/docs/DESIGN.md +++ b/third-party/github.com/letsencrypt/boulder/docs/DESIGN.md @@ -236,7 +236,7 @@ order finalization and does not offer the new-cert endpoint. * 3-4: RA does the following: * Verify the PKCS#10 CSR in the certificate request object - * Verify that the CSR has a non-zero number of domain names + * Verify that the CSR has a non-zero number of identifiers * Verify that the public key in the CSR is different from the account key * For each authorization referenced in the certificate request * Retrieve the authorization from the database @@ -303,7 +303,7 @@ ACME v2: * 2-4: RA does the following: * Verify the PKCS#10 CSR in the certificate request object - * Verify that the CSR has a non-zero number of domain names + * Verify that the CSR has a non-zero number of identifiers * Verify that the public key in the CSR is different from the account key * Retrieve and verify the status and expiry of the order object * For each identifier referenced in the order request diff --git a/third-party/github.com/letsencrypt/boulder/docs/acme-divergences.md b/third-party/github.com/letsencrypt/boulder/docs/acme-divergences.md index 4a6e7a88b..60f41d4d2 100644 --- a/third-party/github.com/letsencrypt/boulder/docs/acme-divergences.md +++ b/third-party/github.com/letsencrypt/boulder/docs/acme-divergences.md @@ -9,10 +9,6 @@ Presently, Boulder diverges from the [RFC 8555] ACME spec in the following ways: Boulder supports POST-as-GET but does not mandate it for requests that simply fetch a resource (certificate, order, authorization, or challenge). -## [Section 6.6](https://tools.ietf.org/html/rfc8555#section-6.6) - -For all rate-limits, Boulder includes a `Link` header to additional documentation on rate-limiting. Only rate-limits on `duplicate certificates` and `certificates per registered domain` are accompanied by a `Retry-After` header. - ## [Section 7.1.2](https://tools.ietf.org/html/rfc8555#section-7.1.2) Boulder does not supply the `orders` field on account objects. We intend to @@ -22,7 +18,7 @@ support this non-essential feature in the future. Please follow Boulder Issue ## [Section 7.4](https://tools.ietf.org/html/rfc8555#section-7.4) Boulder does not accept the optional `notBefore` and `notAfter` fields of a -`newOrder` request paylod. +`newOrder` request payload. ## [Section 7.4.1](https://tools.ietf.org/html/rfc8555#section-7.4.1) diff --git a/third-party/github.com/letsencrypt/boulder/docs/multi-va.md b/third-party/github.com/letsencrypt/boulder/docs/multi-va.md index 4c8df880d..d1d0b044f 100644 --- a/third-party/github.com/letsencrypt/boulder/docs/multi-va.md +++ b/third-party/github.com/letsencrypt/boulder/docs/multi-va.md @@ -38,7 +38,7 @@ and as their config files. We require that almost all remote validation requests succeed; the exact number -is controlled by the VA's `maxRemoteFailures` config variable. If the number of +is controlled by the VA based on the thresholds required by MPIC. If the number of failing remote VAs exceeds that threshold, validation is terminated. If the number of successful remote VAs is high enough that it would be impossible for the outstanding remote VAs to exceed that threshold, validation immediately diff --git a/third-party/github.com/letsencrypt/boulder/docs/redis.md b/third-party/github.com/letsencrypt/boulder/docs/redis.md index 5ef6a5b93..0ce5d52c8 100644 --- a/third-party/github.com/letsencrypt/boulder/docs/redis.md +++ b/third-party/github.com/letsencrypt/boulder/docs/redis.md @@ -23,13 +23,13 @@ docker compose up boulder Then, in a different window, run the following to connect to `bredis_1`: ```shell -./test/redis-cli.sh -h 10.33.33.2 +./test/redis-cli.sh -h 10.77.77.2 ``` Similarly, to connect to `bredis_2`: ```shell -./test/redis-cli.sh -h 10.33.33.3 +./test/redis-cli.sh -h 10.77.77.3 ``` You can pass any IP address for the -h (host) parameter. The full list of IP @@ -40,7 +40,7 @@ You may want to go a level deeper and communicate with a Redis node using the Redis protocol. Here's the command to do that (run from the Boulder root): ```shell -openssl s_client -connect 10.33.33.2:4218 \ +openssl s_client -connect 10.77.77.2:4218 \ -CAfile test/certs/ipki/minica.pem \ -cert test/certs/ipki/localhost/cert.pem \ -key test/certs/ipki/localhost/key.pem diff --git a/third-party/github.com/letsencrypt/boulder/docs/release.md b/third-party/github.com/letsencrypt/boulder/docs/release.md index 8afc30e36..856b5ce73 100644 --- a/third-party/github.com/letsencrypt/boulder/docs/release.md +++ b/third-party/github.com/letsencrypt/boulder/docs/release.md @@ -80,43 +80,35 @@ release is being tagged (not the date that the release is expected to be deployed): ```sh -git tag -s -m "Boulder release $(date +%F)" -s "release-$(date +%F)" -git push origin "release-$(date +%F)" +go run github.com/letsencrypt/boulder/tools/release/tag@main ``` -### Clean Hotfix Releases +This will print the newly-created tag and instructions on how to push it after +you are satisfied that it is correct. Alternately you can run the command with +the `-push` flag to push the resulting tag automatically. -If a hotfix release is necessary, and the desired hotfix commits are the **only** commits which have landed on `main` since the initial release was cut (i.e. there are not any commits on `main` which we want to exclude from the hotfix release), then the hotfix tag can be created much like a normal release tag. +### Hotfix Releases -If it is still the same day as an already-tagged release, increment the letter suffix of the tag: +Sometimes it is necessary to create a new release which looks like a prior +release but with one or more additional commits added. This is usually the case +when we discover a critical bug in the currently-deployed version that needs to +be fixed, but we don't want to include other changes that have already been +merged to `main` since the currently-deployed release was tagged. + +In this situation, we create a new hotfix release branch starting at the point +of the previous release tag. We then use the normal GitHub PR and code-review +process to merge the necessary fix(es) to the branch. Finally we create a new release tag at the tip of the release branch instead of the tip of main. + +To create the new release branch, substitute the name of the release tag which you want to use as the starting point into this command: ```sh -git tag -s -m "Boulder hotfix release $(date +%F)a" -s "release-$(date +%F)a" -git push origin "release-$(date +%F)a" +go run github.com/letsencrypt/boulder/tools/release/branch@main v0.YYYYMMDD.0 ``` -If it is a new day, simply follow the regular release process above. - -### Dirty Hotfix Release - -If a hotfix release is necessary, but `main` already contains both commits that -we do and commits that we do not want to include in the hotfix release, then we -must go back and create a release branch for just the desired commits to be -cherry-picked to. Then, all subsequent hotfix releases will be tagged on this -branch. - -The commands below assume that it is still the same day as the original release -tag was created (hence the use of "`date +%F`"), but this may not always be the -case. The rule is that the date in the release branch name should be identical -to the date in the original release tag. Similarly, this may not be the first -hotfix release; the rule is that the letter suffix should increment (e.g. "b", -"c", etc.) for each hotfix release with the same date. +This will create a release branch named `release-branch-v0.YYYYMMDD`. When all necessary PRs have been merged into that branch, create the new tag by substituting the branch name into this command: ```sh -git checkout -b "release-branch-$(date +%F)" "release-$(date +%F)" -git cherry-pick baddecaf -git tag -s -m "Boulder hotfix release $(date +%F)a" "release-$(date +%F)a" -git push origin "release-branch-$(date +%F)" "release-$(date +%F)a" +go run github.com/letsencrypt/boulder/tools/release/tag@main release-branch-v0.YYYYMMDD ``` ## Deploying Releases diff --git a/third-party/github.com/letsencrypt/boulder/email/cache.go b/third-party/github.com/letsencrypt/boulder/email/cache.go new file mode 100644 index 000000000..74e414bb2 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/email/cache.go @@ -0,0 +1,92 @@ +package email + +import ( + "crypto/sha256" + "encoding/hex" + "sync" + + "github.com/golang/groupcache/lru" + "github.com/prometheus/client_golang/prometheus" +) + +type EmailCache struct { + sync.Mutex + cache *lru.Cache + requests *prometheus.CounterVec +} + +func NewHashedEmailCache(maxEntries int, stats prometheus.Registerer) *EmailCache { + requests := prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: "email_cache_requests", + }, []string{"status"}) + stats.MustRegister(requests) + + return &EmailCache{ + cache: lru.New(maxEntries), + requests: requests, + } +} + +func hashEmail(email string) string { + sum := sha256.Sum256([]byte(email)) + return hex.EncodeToString(sum[:]) +} + +func (c *EmailCache) Seen(email string) bool { + if c == nil { + // If the cache is nil we assume it was not configured. + return false + } + + hash := hashEmail(email) + + c.Lock() + defer c.Unlock() + + _, ok := c.cache.Get(hash) + if !ok { + c.requests.WithLabelValues("miss").Inc() + return false + } + + c.requests.WithLabelValues("hit").Inc() + return true +} + +func (c *EmailCache) Remove(email string) { + if c == nil { + // If the cache is nil we assume it was not configured. + return + } + + hash := hashEmail(email) + + c.Lock() + defer c.Unlock() + + c.cache.Remove(hash) +} + +// StoreIfAbsent stores the email in the cache if it is not already present, as +// a single atomic operation. It returns true if the email was stored and false +// if it was already in the cache. If the cache is nil, true is always returned. +func (c *EmailCache) StoreIfAbsent(email string) bool { + if c == nil { + // If the cache is nil we assume it was not configured. + return true + } + + hash := hashEmail(email) + + c.Lock() + defer c.Unlock() + + _, ok := c.cache.Get(hash) + if ok { + c.requests.WithLabelValues("hit").Inc() + return false + } + c.cache.Add(hash, nil) + c.requests.WithLabelValues("miss").Inc() + return true +} diff --git a/third-party/github.com/letsencrypt/boulder/email/exporter.go b/third-party/github.com/letsencrypt/boulder/email/exporter.go new file mode 100644 index 000000000..8bed230f5 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/email/exporter.go @@ -0,0 +1,181 @@ +package email + +import ( + "context" + "errors" + "sync" + + "github.com/prometheus/client_golang/prometheus" + "golang.org/x/time/rate" + "google.golang.org/protobuf/types/known/emptypb" + + "github.com/letsencrypt/boulder/core" + emailpb "github.com/letsencrypt/boulder/email/proto" + berrors "github.com/letsencrypt/boulder/errors" + blog "github.com/letsencrypt/boulder/log" +) + +// contactsQueueCap limits the queue size to prevent unbounded growth. This +// value is adjustable as needed. Each RFC 5321 email address, encoded in UTF-8, +// is at most 320 bytes. Storing 100,000 emails requires ~34.4 MB of memory. +const contactsQueueCap = 100000 + +var ErrQueueFull = errors.New("email-exporter queue is full") + +// ExporterImpl implements the gRPC server and processes email exports. +type ExporterImpl struct { + emailpb.UnsafeExporterServer + + sync.Mutex + drainWG sync.WaitGroup + // wake is used to signal workers when new emails are enqueued in toSend. + // The sync.Cond docs note that "For many simple use cases, users will be + // better off using channels." However, channels enforce FIFO ordering, + // while this implementation uses a LIFO queue. Making channels behave as + // LIFO would require extra complexity. Using a slice and broadcasting is + // simpler and achieves exactly what we need. + wake *sync.Cond + toSend []string + + maxConcurrentRequests int + limiter *rate.Limiter + client PardotClient + emailCache *EmailCache + emailsHandledCounter prometheus.Counter + pardotErrorCounter prometheus.Counter + log blog.Logger +} + +var _ emailpb.ExporterServer = (*ExporterImpl)(nil) + +// NewExporterImpl initializes an ExporterImpl with the given client and +// configuration. Both perDayLimit and maxConcurrentRequests should be +// distributed proportionally among instances based on their share of the daily +// request cap. 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 (e.g., 2 out of 5). For more details, see: +// https://developer.salesforce.com/docs/marketing/pardot/guide/overview.html?q=rate%20limits +func NewExporterImpl(client PardotClient, cache *EmailCache, perDayLimit float64, maxConcurrentRequests int, scope prometheus.Registerer, logger blog.Logger) *ExporterImpl { + limiter := rate.NewLimiter(rate.Limit(perDayLimit/86400.0), maxConcurrentRequests) + + emailsHandledCounter := prometheus.NewCounter(prometheus.CounterOpts{ + Name: "email_exporter_emails_handled", + Help: "Total number of emails handled by the email exporter", + }) + scope.MustRegister(emailsHandledCounter) + + pardotErrorCounter := prometheus.NewCounter(prometheus.CounterOpts{ + Name: "email_exporter_errors", + Help: "Total number of Pardot API errors encountered by the email exporter", + }) + scope.MustRegister(pardotErrorCounter) + + impl := &ExporterImpl{ + maxConcurrentRequests: maxConcurrentRequests, + limiter: limiter, + toSend: make([]string, 0, contactsQueueCap), + client: client, + emailCache: cache, + emailsHandledCounter: emailsHandledCounter, + pardotErrorCounter: pardotErrorCounter, + log: logger, + } + impl.wake = sync.NewCond(&impl.Mutex) + + queueGauge := prometheus.NewGaugeFunc(prometheus.GaugeOpts{ + Name: "email_exporter_queue_length", + Help: "Current length of the email export queue", + }, func() float64 { + impl.Lock() + defer impl.Unlock() + return float64(len(impl.toSend)) + }) + scope.MustRegister(queueGauge) + + return impl +} + +// SendContacts enqueues the provided email addresses. If the queue cannot +// accommodate the new emails, an ErrQueueFull is returned. +func (impl *ExporterImpl) SendContacts(ctx context.Context, req *emailpb.SendContactsRequest) (*emptypb.Empty, error) { + if core.IsAnyNilOrZero(req, req.Emails) { + return nil, berrors.InternalServerError("Incomplete gRPC request message") + } + + impl.Lock() + defer impl.Unlock() + + spotsLeft := contactsQueueCap - len(impl.toSend) + if spotsLeft < len(req.Emails) { + return nil, ErrQueueFull + } + impl.toSend = append(impl.toSend, req.Emails...) + // Wake waiting workers to process the new emails. + impl.wake.Broadcast() + + return &emptypb.Empty{}, nil +} + +// Start begins asynchronous processing of the email queue. When the parent +// daemonCtx is cancelled the queue will be drained and the workers will exit. +func (impl *ExporterImpl) Start(daemonCtx context.Context) { + go func() { + <-daemonCtx.Done() + // Wake waiting workers to exit. + impl.wake.Broadcast() + }() + + worker := func() { + defer impl.drainWG.Done() + for { + impl.Lock() + + for len(impl.toSend) == 0 && daemonCtx.Err() == nil { + // Wait for the queue to be updated or the daemon to exit. + impl.wake.Wait() + } + + if len(impl.toSend) == 0 && daemonCtx.Err() != nil { + // No more emails to process, exit. + impl.Unlock() + return + } + + // Dequeue and dispatch an email. + last := len(impl.toSend) - 1 + email := impl.toSend[last] + impl.toSend = impl.toSend[:last] + impl.Unlock() + + if !impl.emailCache.StoreIfAbsent(email) { + // Another worker has already processed this email. + continue + } + + err := impl.limiter.Wait(daemonCtx) + if err != nil && !errors.Is(err, context.Canceled) { + impl.log.Errf("Unexpected limiter.Wait() error: %s", err) + continue + } + + err = impl.client.SendContact(email) + if err != nil { + impl.emailCache.Remove(email) + impl.pardotErrorCounter.Inc() + impl.log.Errf("Sending Contact to Pardot: %s", err) + } else { + impl.emailsHandledCounter.Inc() + } + } + } + + for range impl.maxConcurrentRequests { + impl.drainWG.Add(1) + go worker() + } +} + +// Drain blocks until all workers have finished processing the email queue. +func (impl *ExporterImpl) Drain() { + impl.drainWG.Wait() +} diff --git a/third-party/github.com/letsencrypt/boulder/email/exporter_test.go b/third-party/github.com/letsencrypt/boulder/email/exporter_test.go new file mode 100644 index 000000000..e9beca396 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/email/exporter_test.go @@ -0,0 +1,225 @@ +package email + +import ( + "context" + "fmt" + "slices" + "sync" + "testing" + "time" + + emailpb "github.com/letsencrypt/boulder/email/proto" + blog "github.com/letsencrypt/boulder/log" + "github.com/letsencrypt/boulder/metrics" + "github.com/letsencrypt/boulder/test" + + "github.com/prometheus/client_golang/prometheus" +) + +var ctx = context.Background() + +// mockPardotClientImpl is a mock implementation of PardotClient. +type mockPardotClientImpl struct { + sync.Mutex + CreatedContacts []string +} + +// newMockPardotClientImpl returns a MockPardotClientImpl, implementing the +// PardotClient interface. Both refer to the same instance, with the interface +// for mock interaction and the struct for state inspection and modification. +func newMockPardotClientImpl() (PardotClient, *mockPardotClientImpl) { + mockImpl := &mockPardotClientImpl{ + CreatedContacts: []string{}, + } + return mockImpl, mockImpl +} + +// SendContact adds an email to CreatedContacts. +func (m *mockPardotClientImpl) SendContact(email string) error { + m.Lock() + m.CreatedContacts = append(m.CreatedContacts, email) + m.Unlock() + return nil +} + +func (m *mockPardotClientImpl) getCreatedContacts() []string { + m.Lock() + defer m.Unlock() + + // Return a copy to avoid race conditions. + return slices.Clone(m.CreatedContacts) +} + +// setup creates a new ExporterImpl, a MockPardotClientImpl, and the start and +// cleanup functions for the ExporterImpl. Call start() to begin processing the +// ExporterImpl queue and cleanup() to drain and shutdown. If start() is called, +// cleanup() must be called. +func setup() (*ExporterImpl, *mockPardotClientImpl, func(), func()) { + mockClient, clientImpl := newMockPardotClientImpl() + exporter := NewExporterImpl(mockClient, nil, 1000000, 5, metrics.NoopRegisterer, blog.NewMock()) + daemonCtx, cancel := context.WithCancel(context.Background()) + return exporter, clientImpl, + func() { exporter.Start(daemonCtx) }, + func() { + cancel() + exporter.Drain() + } +} + +func TestSendContacts(t *testing.T) { + t.Parallel() + + exporter, clientImpl, start, cleanup := setup() + start() + defer cleanup() + + wantContacts := []string{"test@example.com", "user@example.com"} + _, err := exporter.SendContacts(ctx, &emailpb.SendContactsRequest{ + Emails: wantContacts, + }) + test.AssertNotError(t, err, "Error creating contacts") + + var gotContacts []string + for range 100 { + gotContacts = clientImpl.getCreatedContacts() + if len(gotContacts) == 2 { + break + } + time.Sleep(5 * time.Millisecond) + } + test.AssertSliceContains(t, gotContacts, wantContacts[0]) + test.AssertSliceContains(t, gotContacts, wantContacts[1]) + + // Check that the error counter was not incremented. + test.AssertMetricWithLabelsEquals(t, exporter.pardotErrorCounter, prometheus.Labels{}, 0) +} + +func TestSendContactsQueueFull(t *testing.T) { + t.Parallel() + + exporter, _, start, cleanup := setup() + start() + defer cleanup() + + var err error + for range contactsQueueCap * 2 { + _, err = exporter.SendContacts(ctx, &emailpb.SendContactsRequest{ + Emails: []string{"test@example.com"}, + }) + if err != nil { + break + } + } + test.AssertErrorIs(t, err, ErrQueueFull) +} + +func TestSendContactsQueueDrains(t *testing.T) { + t.Parallel() + + exporter, clientImpl, start, cleanup := setup() + start() + + var emails []string + for i := range 100 { + emails = append(emails, fmt.Sprintf("test@%d.example.com", i)) + } + + _, err := exporter.SendContacts(ctx, &emailpb.SendContactsRequest{ + Emails: emails, + }) + test.AssertNotError(t, err, "Error creating contacts") + + // Drain the queue. + cleanup() + + test.AssertEquals(t, 100, len(clientImpl.getCreatedContacts())) +} + +type mockAlwaysFailClient struct{} + +func (m *mockAlwaysFailClient) SendContact(email string) error { + return fmt.Errorf("simulated failure") +} + +func TestSendContactsErrorMetrics(t *testing.T) { + t.Parallel() + + mockClient := &mockAlwaysFailClient{} + exporter := NewExporterImpl(mockClient, nil, 1000000, 5, metrics.NoopRegisterer, blog.NewMock()) + + daemonCtx, cancel := context.WithCancel(context.Background()) + exporter.Start(daemonCtx) + + _, err := exporter.SendContacts(ctx, &emailpb.SendContactsRequest{ + Emails: []string{"test@example.com"}, + }) + test.AssertNotError(t, err, "Error creating contacts") + + // Drain the queue. + cancel() + exporter.Drain() + + // Check that the error counter was incremented. + test.AssertMetricWithLabelsEquals(t, exporter.pardotErrorCounter, prometheus.Labels{}, 1) +} + +func TestSendContactDeduplication(t *testing.T) { + t.Parallel() + + cache := NewHashedEmailCache(1000, metrics.NoopRegisterer) + mockClient, clientImpl := newMockPardotClientImpl() + exporter := NewExporterImpl(mockClient, cache, 1000000, 5, metrics.NoopRegisterer, blog.NewMock()) + + daemonCtx, cancel := context.WithCancel(context.Background()) + exporter.Start(daemonCtx) + + _, err := exporter.SendContacts(ctx, &emailpb.SendContactsRequest{ + Emails: []string{"duplicate@example.com", "duplicate@example.com"}, + }) + test.AssertNotError(t, err, "Error enqueuing contacts") + + // Drain the queue. + cancel() + exporter.Drain() + + contacts := clientImpl.getCreatedContacts() + test.AssertEquals(t, 1, len(contacts)) + test.AssertEquals(t, "duplicate@example.com", contacts[0]) + + // Only one successful send should be recorded. + test.AssertMetricWithLabelsEquals(t, exporter.emailsHandledCounter, prometheus.Labels{}, 1) + + if !cache.Seen("duplicate@example.com") { + t.Errorf("duplicate@example.com should have been cached after send") + } +} + +func TestSendContactErrorRemovesFromCache(t *testing.T) { + t.Parallel() + + cache := NewHashedEmailCache(1000, metrics.NoopRegisterer) + fc := &mockAlwaysFailClient{} + + exporter := NewExporterImpl(fc, cache, 1000000, 1, metrics.NoopRegisterer, blog.NewMock()) + + daemonCtx, cancel := context.WithCancel(context.Background()) + exporter.Start(daemonCtx) + + _, err := exporter.SendContacts(ctx, &emailpb.SendContactsRequest{ + Emails: []string{"error@example.com"}, + }) + test.AssertNotError(t, err, "enqueue failed") + + // Drain the queue. + cancel() + exporter.Drain() + + // The email should have been evicted from the cache after send encountered + // an error. + if cache.Seen("error@example.com") { + t.Errorf("error@example.com should have been evicted from cache after send errors") + } + + // Check that the error counter was incremented. + test.AssertMetricWithLabelsEquals(t, exporter.pardotErrorCounter, prometheus.Labels{}, 1) +} diff --git a/third-party/github.com/letsencrypt/boulder/email/pardot.go b/third-party/github.com/letsencrypt/boulder/email/pardot.go new file mode 100644 index 000000000..1d1c7299a --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/email/pardot.go @@ -0,0 +1,198 @@ +package email + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "sync" + "time" + + "github.com/jmhodges/clock" + "github.com/letsencrypt/boulder/core" +) + +const ( + // tokenPath is the path to the Salesforce OAuth2 token endpoint. + tokenPath = "/services/oauth2/token" + + // contactsPath is the path to the Pardot v5 Prospects endpoint. This + // endpoint will create a new Prospect if one does not already exist with + // the same email address. + contactsPath = "/api/v5/objects/prospects" + + // maxAttempts is the maximum number of attempts to retry a request. + maxAttempts = 3 + + // retryBackoffBase is the base for exponential backoff. + retryBackoffBase = 2.0 + + // retryBackoffMax is the maximum backoff time. + retryBackoffMax = 10 * time.Second + + // retryBackoffMin is the minimum backoff time. + retryBackoffMin = 200 * time.Millisecond + + // tokenExpirationBuffer is the time before the token expires that we will + // attempt to refresh it. + tokenExpirationBuffer = 5 * time.Minute +) + +// PardotClient is an interface for interacting with Pardot. It exists to +// facilitate testing mocks. +type PardotClient interface { + SendContact(email string) error +} + +// oAuthToken holds the OAuth2 access token and its expiration. +type oAuthToken struct { + sync.Mutex + + accessToken string + expiresAt time.Time +} + +// PardotClientImpl handles authentication and sending contacts to Pardot. It +// implements the PardotClient interface. +type PardotClientImpl struct { + businessUnit string + clientId string + clientSecret string + contactsURL string + tokenURL string + token *oAuthToken + clk clock.Clock +} + +var _ PardotClient = &PardotClientImpl{} + +// NewPardotClientImpl creates a new PardotClientImpl. +func NewPardotClientImpl(clk clock.Clock, businessUnit, clientId, clientSecret, oauthbaseURL, pardotBaseURL string) (*PardotClientImpl, error) { + contactsURL, err := url.JoinPath(pardotBaseURL, contactsPath) + if err != nil { + return nil, fmt.Errorf("failed to join contacts path: %w", err) + } + tokenURL, err := url.JoinPath(oauthbaseURL, tokenPath) + if err != nil { + return nil, fmt.Errorf("failed to join token path: %w", err) + } + + return &PardotClientImpl{ + businessUnit: businessUnit, + clientId: clientId, + clientSecret: clientSecret, + contactsURL: contactsURL, + tokenURL: tokenURL, + token: &oAuthToken{}, + clk: clk, + }, nil +} + +type oauthTokenResp struct { + AccessToken string `json:"access_token"` + ExpiresIn int `json:"expires_in"` +} + +// updateToken updates the OAuth token if necessary. +func (pc *PardotClientImpl) updateToken() error { + pc.token.Lock() + defer pc.token.Unlock() + + now := pc.clk.Now() + if now.Before(pc.token.expiresAt.Add(-tokenExpirationBuffer)) && pc.token.accessToken != "" { + return nil + } + + resp, err := http.PostForm(pc.tokenURL, url.Values{ + "grant_type": {"client_credentials"}, + "client_id": {pc.clientId}, + "client_secret": {pc.clientSecret}, + }) + if err != nil { + return fmt.Errorf("failed to retrieve token: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, readErr := io.ReadAll(resp.Body) + if readErr != nil { + return fmt.Errorf("token request failed with status %d; while reading body: %w", resp.StatusCode, readErr) + } + return fmt.Errorf("token request failed with status %d: %s", resp.StatusCode, body) + } + + var respJSON oauthTokenResp + err = json.NewDecoder(resp.Body).Decode(&respJSON) + if err != nil { + return fmt.Errorf("failed to decode token response: %w", err) + } + pc.token.accessToken = respJSON.AccessToken + pc.token.expiresAt = pc.clk.Now().Add(time.Duration(respJSON.ExpiresIn) * time.Second) + + return nil +} + +// redactEmail replaces all occurrences of an email address in a response body +// with "[REDACTED]". +func redactEmail(body []byte, email string) string { + return string(bytes.ReplaceAll(body, []byte(email), []byte("[REDACTED]"))) +} + +// SendContact submits an email to the Pardot Contacts endpoint, retrying up +// to 3 times with exponential backoff. +func (pc *PardotClientImpl) SendContact(email string) error { + var err error + for attempt := range maxAttempts { + time.Sleep(core.RetryBackoff(attempt, retryBackoffMin, retryBackoffMax, retryBackoffBase)) + err = pc.updateToken() + if err != nil { + continue + } + break + } + if err != nil { + return fmt.Errorf("failed to update token: %w", err) + } + + payload, err := json.Marshal(map[string]string{"email": email}) + if err != nil { + return fmt.Errorf("failed to marshal payload: %w", err) + } + + var finalErr error + for attempt := range maxAttempts { + time.Sleep(core.RetryBackoff(attempt, retryBackoffMin, retryBackoffMax, retryBackoffBase)) + + req, err := http.NewRequest("POST", pc.contactsURL, bytes.NewReader(payload)) + if err != nil { + finalErr = fmt.Errorf("failed to create new contact request: %w", err) + continue + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+pc.token.accessToken) + req.Header.Set("Pardot-Business-Unit-Id", pc.businessUnit) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + finalErr = fmt.Errorf("create contact request failed: %w", err) + continue + } + + defer resp.Body.Close() + if resp.StatusCode >= 200 && resp.StatusCode < 300 { + return nil + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + finalErr = fmt.Errorf("create contact request returned status %d; while reading body: %w", resp.StatusCode, err) + continue + } + finalErr = fmt.Errorf("create contact request returned status %d: %s", resp.StatusCode, redactEmail(body, email)) + continue + } + + return finalErr +} diff --git a/third-party/github.com/letsencrypt/boulder/email/pardot_test.go b/third-party/github.com/letsencrypt/boulder/email/pardot_test.go new file mode 100644 index 000000000..700ed6982 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/email/pardot_test.go @@ -0,0 +1,210 @@ +package email + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/jmhodges/clock" + "github.com/letsencrypt/boulder/test" +) + +func defaultTokenHandler(w http.ResponseWriter, r *http.Request) { + err := json.NewEncoder(w).Encode(oauthTokenResp{ + AccessToken: "dummy", + ExpiresIn: 3600, + }) + if err != nil { + // This should never happen. + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("failed to encode token")) + return + } +} + +func TestSendContactSuccess(t *testing.T) { + t.Parallel() + + contactHandler := func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("Authorization") != "Bearer dummy" { + w.WriteHeader(http.StatusUnauthorized) + return + } + w.WriteHeader(http.StatusOK) + } + + tokenSrv := httptest.NewServer(http.HandlerFunc(defaultTokenHandler)) + defer tokenSrv.Close() + + contactSrv := httptest.NewServer(http.HandlerFunc(contactHandler)) + defer contactSrv.Close() + + clk := clock.NewFake() + client, err := NewPardotClientImpl(clk, "biz-unit", "cid", "csec", tokenSrv.URL, contactSrv.URL) + test.AssertNotError(t, err, "failed to create client") + + err = client.SendContact("test@example.com") + test.AssertNotError(t, err, "SendContact should succeed") +} + +func TestSendContactUpdateTokenFails(t *testing.T) { + t.Parallel() + + tokenHandlerThatAlwaysErrors := func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintln(w, "token error") + } + + contactHandler := func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + } + + tokenSrv := httptest.NewServer(http.HandlerFunc(tokenHandlerThatAlwaysErrors)) + defer tokenSrv.Close() + + contactSrv := httptest.NewServer(http.HandlerFunc(contactHandler)) + defer contactSrv.Close() + + clk := clock.NewFake() + client, err := NewPardotClientImpl(clk, "biz-unit", "cid", "csec", tokenSrv.URL, contactSrv.URL) + test.AssertNotError(t, err, "Failed to create client") + + err = client.SendContact("test@example.com") + test.AssertError(t, err, "Expected token update to fail") + test.AssertContains(t, err.Error(), "failed to update token") +} + +func TestSendContact4xx(t *testing.T) { + t.Parallel() + + contactHandler := func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusBadRequest) + _, err := io.WriteString(w, "bad request") + test.AssertNotError(t, err, "failed to write response") + } + + tokenSrv := httptest.NewServer(http.HandlerFunc(defaultTokenHandler)) + defer tokenSrv.Close() + + contactSrv := httptest.NewServer(http.HandlerFunc(contactHandler)) + defer contactSrv.Close() + + clk := clock.NewFake() + client, err := NewPardotClientImpl(clk, "biz-unit", "cid", "csec", tokenSrv.URL, contactSrv.URL) + test.AssertNotError(t, err, "Failed to create client") + + err = client.SendContact("test@example.com") + test.AssertError(t, err, "Should fail on 400") + test.AssertContains(t, err.Error(), "create contact request returned status 400") +} + +func TestSendContactTokenExpiry(t *testing.T) { + t.Parallel() + + // tokenHandler returns "old_token" on the first call and "new_token" on subsequent calls. + tokenRetrieved := false + tokenHandler := func(w http.ResponseWriter, r *http.Request) { + token := "new_token" + if !tokenRetrieved { + token = "old_token" + tokenRetrieved = true + } + err := json.NewEncoder(w).Encode(oauthTokenResp{ + AccessToken: token, + ExpiresIn: 3600, + }) + test.AssertNotError(t, err, "failed to encode token") + } + + // contactHandler expects "old_token" for the first request and "new_token" for the next. + firstRequest := true + contactHandler := func(w http.ResponseWriter, r *http.Request) { + expectedToken := "new_token" + if firstRequest { + expectedToken = "old_token" + firstRequest = false + } + if r.Header.Get("Authorization") != "Bearer "+expectedToken { + w.WriteHeader(http.StatusUnauthorized) + return + } + w.WriteHeader(http.StatusOK) + } + + tokenSrv := httptest.NewServer(http.HandlerFunc(tokenHandler)) + defer tokenSrv.Close() + + contactSrv := httptest.NewServer(http.HandlerFunc(contactHandler)) + defer contactSrv.Close() + + clk := clock.NewFake() + client, err := NewPardotClientImpl(clk, "biz-unit", "cid", "csec", tokenSrv.URL, contactSrv.URL) + test.AssertNotError(t, err, "Failed to create client") + + // First call uses the initial token ("old_token"). + err = client.SendContact("test@example.com") + test.AssertNotError(t, err, "SendContact should succeed with the initial token") + + // Advance time to force token expiry. + clk.Add(3601 * time.Second) + + // Second call should refresh the token to "new_token". + err = client.SendContact("test@example.com") + test.AssertNotError(t, err, "SendContact should succeed after refreshing the token") +} + +func TestSendContactServerErrorsAfterMaxAttempts(t *testing.T) { + t.Parallel() + + gotAttempts := 0 + contactHandler := func(w http.ResponseWriter, r *http.Request) { + gotAttempts++ + w.WriteHeader(http.StatusServiceUnavailable) + } + + tokenSrv := httptest.NewServer(http.HandlerFunc(defaultTokenHandler)) + defer tokenSrv.Close() + + contactSrv := httptest.NewServer(http.HandlerFunc(contactHandler)) + defer contactSrv.Close() + + client, _ := NewPardotClientImpl(clock.NewFake(), "biz-unit", "cid", "csec", tokenSrv.URL, contactSrv.URL) + + err := client.SendContact("test@example.com") + test.AssertError(t, err, "Should fail after retrying all attempts") + test.AssertEquals(t, maxAttempts, gotAttempts) + test.AssertContains(t, err.Error(), "create contact request returned status 503") +} + +func TestSendContactRedactsEmail(t *testing.T) { + t.Parallel() + + emailToTest := "test@example.com" + + contactHandler := func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusBadRequest) + // Intentionally include the request email in the response body. + resp := fmt.Sprintf("error: %s is invalid", emailToTest) + _, err := io.WriteString(w, resp) + test.AssertNotError(t, err, "failed to write response") + } + + tokenSrv := httptest.NewServer(http.HandlerFunc(defaultTokenHandler)) + defer tokenSrv.Close() + + contactSrv := httptest.NewServer(http.HandlerFunc(contactHandler)) + defer contactSrv.Close() + + clk := clock.NewFake() + client, err := NewPardotClientImpl(clk, "biz-unit", "cid", "csec", tokenSrv.URL, contactSrv.URL) + test.AssertNotError(t, err, "failed to create client") + + err = client.SendContact(emailToTest) + test.AssertError(t, err, "SendContact should fail") + test.AssertNotContains(t, err.Error(), emailToTest) + test.AssertContains(t, err.Error(), "[REDACTED]") +} diff --git a/third-party/github.com/letsencrypt/boulder/email/proto/exporter.pb.go b/third-party/github.com/letsencrypt/boulder/email/proto/exporter.pb.go new file mode 100644 index 000000000..41c167479 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/email/proto/exporter.pb.go @@ -0,0 +1,138 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.5 +// protoc v3.20.1 +// source: exporter.proto + +package proto + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + emptypb "google.golang.org/protobuf/types/known/emptypb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type SendContactsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Emails []string `protobuf:"bytes,1,rep,name=emails,proto3" json:"emails,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SendContactsRequest) Reset() { + *x = SendContactsRequest{} + mi := &file_exporter_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SendContactsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SendContactsRequest) ProtoMessage() {} + +func (x *SendContactsRequest) ProtoReflect() protoreflect.Message { + mi := &file_exporter_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SendContactsRequest.ProtoReflect.Descriptor instead. +func (*SendContactsRequest) Descriptor() ([]byte, []int) { + return file_exporter_proto_rawDescGZIP(), []int{0} +} + +func (x *SendContactsRequest) GetEmails() []string { + if x != nil { + return x.Emails + } + return nil +} + +var File_exporter_proto protoreflect.FileDescriptor + +var file_exporter_proto_rawDesc = string([]byte{ + 0x0a, 0x0e, 0x65, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x12, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 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, 0x6f, 0x74, 0x6f, 0x22, 0x2d, 0x0a, 0x13, 0x53, 0x65, 0x6e, 0x64, 0x43, 0x6f, 0x6e, 0x74, + 0x61, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x65, + 0x6d, 0x61, 0x69, 0x6c, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x65, 0x6d, 0x61, + 0x69, 0x6c, 0x73, 0x32, 0x4e, 0x0a, 0x08, 0x45, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x72, 0x12, + 0x42, 0x0a, 0x0c, 0x53, 0x65, 0x6e, 0x64, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x63, 0x74, 0x73, 0x12, + 0x1a, 0x2e, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x43, 0x6f, 0x6e, 0x74, + 0x61, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, + 0x70, 0x74, 0x79, 0x42, 0x2c, 0x5a, 0x2a, 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, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +}) + +var ( + file_exporter_proto_rawDescOnce sync.Once + file_exporter_proto_rawDescData []byte +) + +func file_exporter_proto_rawDescGZIP() []byte { + file_exporter_proto_rawDescOnce.Do(func() { + file_exporter_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_exporter_proto_rawDesc), len(file_exporter_proto_rawDesc))) + }) + return file_exporter_proto_rawDescData +} + +var file_exporter_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_exporter_proto_goTypes = []any{ + (*SendContactsRequest)(nil), // 0: email.SendContactsRequest + (*emptypb.Empty)(nil), // 1: google.protobuf.Empty +} +var file_exporter_proto_depIdxs = []int32{ + 0, // 0: email.Exporter.SendContacts:input_type -> email.SendContactsRequest + 1, // 1: email.Exporter.SendContacts:output_type -> google.protobuf.Empty + 1, // [1:2] is the sub-list for method output_type + 0, // [0:1] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_exporter_proto_init() } +func file_exporter_proto_init() { + if File_exporter_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_exporter_proto_rawDesc), len(file_exporter_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_exporter_proto_goTypes, + DependencyIndexes: file_exporter_proto_depIdxs, + MessageInfos: file_exporter_proto_msgTypes, + }.Build() + File_exporter_proto = out.File + file_exporter_proto_goTypes = nil + file_exporter_proto_depIdxs = nil +} diff --git a/third-party/github.com/letsencrypt/boulder/email/proto/exporter.proto b/third-party/github.com/letsencrypt/boulder/email/proto/exporter.proto new file mode 100644 index 000000000..93abcecd5 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/email/proto/exporter.proto @@ -0,0 +1,14 @@ +syntax = "proto3"; + +package email; +option go_package = "github.com/letsencrypt/boulder/email/proto"; + +import "google/protobuf/empty.proto"; + +service Exporter { + rpc SendContacts (SendContactsRequest) returns (google.protobuf.Empty); +} + +message SendContactsRequest { + repeated string emails = 1; +} diff --git a/third-party/github.com/letsencrypt/boulder/email/proto/exporter_grpc.pb.go b/third-party/github.com/letsencrypt/boulder/email/proto/exporter_grpc.pb.go new file mode 100644 index 000000000..4660d0b97 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/email/proto/exporter_grpc.pb.go @@ -0,0 +1,122 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc v3.20.1 +// source: exporter.proto + +package proto + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + emptypb "google.golang.org/protobuf/types/known/emptypb" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + Exporter_SendContacts_FullMethodName = "/email.Exporter/SendContacts" +) + +// ExporterClient is the client API for Exporter 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. +type ExporterClient interface { + SendContacts(ctx context.Context, in *SendContactsRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) +} + +type exporterClient struct { + cc grpc.ClientConnInterface +} + +func NewExporterClient(cc grpc.ClientConnInterface) ExporterClient { + return &exporterClient{cc} +} + +func (c *exporterClient) SendContacts(ctx context.Context, in *SendContactsRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, Exporter_SendContacts_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// ExporterServer is the server API for Exporter service. +// All implementations must embed UnimplementedExporterServer +// for forward compatibility. +type ExporterServer interface { + SendContacts(context.Context, *SendContactsRequest) (*emptypb.Empty, error) + mustEmbedUnimplementedExporterServer() +} + +// UnimplementedExporterServer 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 UnimplementedExporterServer struct{} + +func (UnimplementedExporterServer) SendContacts(context.Context, *SendContactsRequest) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method SendContacts not implemented") +} +func (UnimplementedExporterServer) mustEmbedUnimplementedExporterServer() {} +func (UnimplementedExporterServer) testEmbeddedByValue() {} + +// UnsafeExporterServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to ExporterServer will +// result in compilation errors. +type UnsafeExporterServer interface { + mustEmbedUnimplementedExporterServer() +} + +func RegisterExporterServer(s grpc.ServiceRegistrar, srv ExporterServer) { + // If the following call pancis, it indicates UnimplementedExporterServer 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(&Exporter_ServiceDesc, srv) +} + +func _Exporter_SendContacts_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SendContactsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ExporterServer).SendContacts(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Exporter_SendContacts_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ExporterServer).SendContacts(ctx, req.(*SendContactsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// Exporter_ServiceDesc is the grpc.ServiceDesc for Exporter service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Exporter_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "email.Exporter", + HandlerType: (*ExporterServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "SendContacts", + Handler: _Exporter_SendContacts_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "exporter.proto", +} diff --git a/third-party/github.com/letsencrypt/boulder/errors/errors.go b/third-party/github.com/letsencrypt/boulder/errors/errors.go index d7328b08d..cc1790362 100644 --- a/third-party/github.com/letsencrypt/boulder/errors/errors.go +++ b/third-party/github.com/letsencrypt/boulder/errors/errors.go @@ -1,22 +1,33 @@ -// Package errors provides internal-facing error types for use in Boulder. Many -// of these are transformed directly into Problem Details documents by the WFE. -// Some, like NotFound, may be handled internally. We avoid using Problem -// Details documents as part of our internal error system to avoid layering -// confusions. +// Package errors provide a special error type for use in Boulder. This error +// type carries additional type information with it, and has two special powers: // -// These errors are specifically for use in errors that cross RPC boundaries. -// An error type that does not need to be passed through an RPC can use a plain -// Go type locally. Our gRPC code is aware of these error types and will -// serialize and deserialize them automatically. +// 1. It is recognized by our gRPC code, and the type metadata and detail string +// will cross gRPC boundaries intact. +// +// 2. It is recognized by our frontend API "rendering" code, and will be +// automatically converted to the corresponding urn:ietf:params:acme:error:... +// ACME Problem Document. +// +// This means that a deeply-nested service (such as the SA) that wants to ensure +// that the ACME client sees a particular problem document (such as NotFound) +// can return a BoulderError and be sure that it will be propagated all the way +// to the client. +// +// Note, however, that any additional context wrapped *around* the BoulderError +// (such as by fmt.Errorf("oops: %w")) will be lost when the error is converted +// into a problem document. Similarly, any type information wrapped *by* a +// BoulderError (such as a sql.ErrNoRows) is lost at the gRPC serialization +// boundary. package errors import ( "fmt" "time" - "github.com/letsencrypt/boulder/identifier" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + + "github.com/letsencrypt/boulder/identifier" ) // ErrorType provides a coarse category for BoulderErrors. @@ -30,7 +41,7 @@ const ( // InternalServer is deprecated. Instead, pass a plain Go error. That will get // turned into a probs.InternalServerError by the WFE. InternalServer ErrorType = iota - _ + _ // Reserved, previously NotSupported Malformed Unauthorized NotFound @@ -49,11 +60,17 @@ const ( AlreadyRevoked BadRevocationReason UnsupportedContact - // The requesteed serial number does not exist in the `serials` table. + // The requested serial number does not exist in the `serials` table. UnknownSerial + Conflict + // Defined in https://datatracker.ietf.org/doc/draft-aaron-acme-profiles/00/ + InvalidProfile // The certificate being indicated for replacement already has a replacement // order. - Conflict + AlreadyReplaced + BadSignatureAlgorithm + AccountDoesNotExist + BadNonce ) func (ErrorType) Error() string { @@ -78,10 +95,15 @@ type SubBoulderError struct { Identifier identifier.ACMEIdentifier } +// Error implements the error interface, returning a string representation of +// this error. func (be *BoulderError) Error() string { return be.Detail } +// Unwrap implements the optional error-unwrapping interface. It returns the +// underlying type, all of when themselves implement the error interface, so +// that `if errors.Is(someError, berrors.Malformed)` works. func (be *BoulderError) Unwrap() error { return be.Type } @@ -147,31 +169,40 @@ func (be *BoulderError) WithSubErrors(subErrs []SubBoulderError) *BoulderError { } } -// New is a convenience function for creating a new BoulderError -func New(errType ErrorType, msg string, args ...interface{}) error { +// New is a convenience function for creating a new BoulderError. +func New(errType ErrorType, msg string) error { + return &BoulderError{ + Type: errType, + Detail: msg, + } +} + +// newf is a convenience function for creating a new BoulderError with a +// formatted message. +func newf(errType ErrorType, msg string, args ...any) error { return &BoulderError{ Type: errType, Detail: fmt.Sprintf(msg, args...), } } -func InternalServerError(msg string, args ...interface{}) error { - return New(InternalServer, msg, args...) +func InternalServerError(msg string, args ...any) error { + return newf(InternalServer, msg, args...) } -func MalformedError(msg string, args ...interface{}) error { - return New(Malformed, msg, args...) +func MalformedError(msg string, args ...any) error { + return newf(Malformed, msg, args...) } -func UnauthorizedError(msg string, args ...interface{}) error { - return New(Unauthorized, msg, args...) +func UnauthorizedError(msg string, args ...any) error { + return newf(Unauthorized, msg, args...) } -func NotFoundError(msg string, args ...interface{}) error { - return New(NotFound, msg, args...) +func NotFoundError(msg string, args ...any) error { + return newf(NotFound, msg, args...) } -func RateLimitError(retryAfter time.Duration, msg string, args ...interface{}) error { +func RateLimitError(retryAfter time.Duration, msg string, args ...any) error { return &BoulderError{ Type: RateLimit, Detail: fmt.Sprintf(msg+": see https://letsencrypt.org/docs/rate-limits/", args...), @@ -179,86 +210,126 @@ func RateLimitError(retryAfter time.Duration, msg string, args ...interface{}) e } } -func DuplicateCertificateError(retryAfter time.Duration, msg string, args ...interface{}) error { +func RegistrationsPerIPAddressError(retryAfter time.Duration, msg string, args ...any) error { return &BoulderError{ Type: RateLimit, - Detail: fmt.Sprintf(msg+": see https://letsencrypt.org/docs/duplicate-certificate-limit/", args...), + Detail: fmt.Sprintf(msg+": see https://letsencrypt.org/docs/rate-limits/#new-registrations-per-ip-address", args...), RetryAfter: retryAfter, } } -func FailedValidationError(retryAfter time.Duration, msg string, args ...interface{}) error { +func RegistrationsPerIPv6RangeError(retryAfter time.Duration, msg string, args ...any) error { return &BoulderError{ Type: RateLimit, - Detail: fmt.Sprintf(msg+": see https://letsencrypt.org/docs/failed-validation-limit/", args...), + Detail: fmt.Sprintf(msg+": see https://letsencrypt.org/docs/rate-limits/#new-registrations-per-ipv6-range", args...), RetryAfter: retryAfter, } } -func RegistrationsPerIPError(retryAfter time.Duration, msg string, args ...interface{}) error { +func NewOrdersPerAccountError(retryAfter time.Duration, msg string, args ...any) error { return &BoulderError{ Type: RateLimit, - Detail: fmt.Sprintf(msg+": see https://letsencrypt.org/docs/too-many-registrations-for-this-ip/", args...), + Detail: fmt.Sprintf(msg+": see https://letsencrypt.org/docs/rate-limits/#new-orders-per-account", args...), RetryAfter: retryAfter, } } -func RejectedIdentifierError(msg string, args ...interface{}) error { - return New(RejectedIdentifier, msg, args...) +func CertificatesPerDomainError(retryAfter time.Duration, msg string, args ...any) error { + return &BoulderError{ + Type: RateLimit, + Detail: fmt.Sprintf(msg+": see https://letsencrypt.org/docs/rate-limits/#new-certificates-per-registered-domain", args...), + RetryAfter: retryAfter, + } } -func InvalidEmailError(msg string, args ...interface{}) error { - return New(InvalidEmail, msg, args...) +func CertificatesPerFQDNSetError(retryAfter time.Duration, msg string, args ...any) error { + return &BoulderError{ + Type: RateLimit, + Detail: fmt.Sprintf(msg+": see https://letsencrypt.org/docs/rate-limits/#new-certificates-per-exact-set-of-hostnames", args...), + RetryAfter: retryAfter, + } } -func UnsupportedContactError(msg string, args ...interface{}) error { - return New(UnsupportedContact, msg, args...) +func FailedAuthorizationsPerDomainPerAccountError(retryAfter time.Duration, msg string, args ...any) error { + return &BoulderError{ + Type: RateLimit, + Detail: fmt.Sprintf(msg+": see https://letsencrypt.org/docs/rate-limits/#authorization-failures-per-hostname-per-account", args...), + RetryAfter: retryAfter, + } } -func ConnectionFailureError(msg string, args ...interface{}) error { - return New(ConnectionFailure, msg, args...) +func RejectedIdentifierError(msg string, args ...any) error { + return newf(RejectedIdentifier, msg, args...) } -func CAAError(msg string, args ...interface{}) error { - return New(CAA, msg, args...) +func InvalidEmailError(msg string, args ...any) error { + return newf(InvalidEmail, msg, args...) } -func MissingSCTsError(msg string, args ...interface{}) error { - return New(MissingSCTs, msg, args...) +func UnsupportedContactError(msg string, args ...any) error { + return newf(UnsupportedContact, msg, args...) } -func DuplicateError(msg string, args ...interface{}) error { - return New(Duplicate, msg, args...) +func ConnectionFailureError(msg string, args ...any) error { + return newf(ConnectionFailure, msg, args...) } -func OrderNotReadyError(msg string, args ...interface{}) error { - return New(OrderNotReady, msg, args...) +func CAAError(msg string, args ...any) error { + return newf(CAA, msg, args...) } -func DNSError(msg string, args ...interface{}) error { - return New(DNS, msg, args...) +func MissingSCTsError(msg string, args ...any) error { + return newf(MissingSCTs, msg, args...) } -func BadPublicKeyError(msg string, args ...interface{}) error { - return New(BadPublicKey, msg, args...) +func DuplicateError(msg string, args ...any) error { + return newf(Duplicate, msg, args...) } -func BadCSRError(msg string, args ...interface{}) error { - return New(BadCSR, msg, args...) +func OrderNotReadyError(msg string, args ...any) error { + return newf(OrderNotReady, msg, args...) } -func AlreadyRevokedError(msg string, args ...interface{}) error { - return New(AlreadyRevoked, msg, args...) +func DNSError(msg string, args ...any) error { + return newf(DNS, msg, args...) +} + +func BadPublicKeyError(msg string, args ...any) error { + return newf(BadPublicKey, msg, args...) +} + +func BadCSRError(msg string, args ...any) error { + return newf(BadCSR, msg, args...) +} + +func AlreadyReplacedError(msg string, args ...any) error { + return newf(AlreadyReplaced, msg, args...) +} + +func AlreadyRevokedError(msg string, args ...any) error { + return newf(AlreadyRevoked, msg, args...) } func BadRevocationReasonError(reason int64) error { - return New(BadRevocationReason, "disallowed revocation reason: %d", reason) + return newf(BadRevocationReason, "disallowed revocation reason: %d", reason) } func UnknownSerialError() error { - return New(UnknownSerial, "unknown serial") + return newf(UnknownSerial, "unknown serial") } -func ConflictError(msg string, args ...interface{}) error { - return New(Conflict, msg, args...) +func InvalidProfileError(msg string, args ...any) error { + return newf(InvalidProfile, msg, args...) +} + +func BadSignatureAlgorithmError(msg string, args ...any) error { + return newf(BadSignatureAlgorithm, msg, args...) +} + +func AccountDoesNotExistError(msg string, args ...any) error { + return newf(AccountDoesNotExist, msg, args...) +} + +func BadNonceError(msg string, args ...any) error { + return newf(BadNonce, msg, args...) } diff --git a/third-party/github.com/letsencrypt/boulder/errors/errors_test.go b/third-party/github.com/letsencrypt/boulder/errors/errors_test.go index 675b23597..f69abbf46 100644 --- a/third-party/github.com/letsencrypt/boulder/errors/errors_test.go +++ b/third-party/github.com/letsencrypt/boulder/errors/errors_test.go @@ -17,14 +17,14 @@ func TestWithSubErrors(t *testing.T) { subErrs := []SubBoulderError{ { - Identifier: identifier.DNSIdentifier("example.com"), + Identifier: identifier.NewDNS("example.com"), BoulderError: &BoulderError{ Type: RateLimit, Detail: "everyone uses this example domain", }, }, { - Identifier: identifier.DNSIdentifier("what about example.com"), + Identifier: identifier.NewDNS("what about example.com"), BoulderError: &BoulderError{ Type: RateLimit, Detail: "try a real identifier value next time", @@ -39,7 +39,7 @@ func TestWithSubErrors(t *testing.T) { test.AssertDeepEquals(t, outResult.SubErrors, subErrs) // Adding another suberr shouldn't squash the original sub errors anotherSubErr := SubBoulderError{ - Identifier: identifier.DNSIdentifier("another ident"), + Identifier: identifier.NewDNS("another ident"), BoulderError: &BoulderError{ Type: RateLimit, Detail: "another rate limit err", diff --git a/third-party/github.com/letsencrypt/boulder/features/features.go b/third-party/github.com/letsencrypt/boulder/features/features.go index c3d6be771..84e8df50d 100644 --- a/third-party/github.com/letsencrypt/boulder/features/features.go +++ b/third-party/github.com/letsencrypt/boulder/features/features.go @@ -15,32 +15,24 @@ import ( // then call features.Set(parsedConfig) to load the parsed struct into this // package's global Config. type Config struct { - // Deprecated features. These features have no effect. Removing them from - // configuration is safe. - // - // Once all references to them have been removed from deployed configuration, - // they can be deleted from this struct, after which Boulder will fail to - // start if they are present in configuration. - CAAAfterValidation bool - AllowNoCommonName bool - SHA256SubjectKeyIdentifier bool - EnforceMultiVA bool - MultiVAFullResults bool - CertCheckerRequiresCorrespondence bool - - // ECDSAForAll enables all accounts, regardless of their presence in the CA's - // ecdsaAllowedAccounts config value, to get issuance from ECDSA issuers. - ECDSAForAll bool + // Deprecated flags. + IncrementRateLimits bool + UseKvLimitsForNewOrder bool + DisableLegacyLimitWrites bool + MultipleCertificateProfiles bool + InsertAuthzsIndividually bool + EnforceMultiCAA bool + EnforceMPIC bool + MPICFullResults bool + UnsplitIssuance bool + ExpirationMailerUsesJoin bool + DOH bool + IgnoreAccountContacts bool // ServeRenewalInfo exposes the renewalInfo endpoint in the directory and for // GET requests. WARNING: This feature is a draft and highly unstable. ServeRenewalInfo bool - // ExpirationMailerUsesJoin enables using a JOIN query in expiration-mailer - // rather than a SELECT from certificateStatus followed by thousands of - // one-row SELECTs from certificates. - ExpirationMailerUsesJoin bool - // CertCheckerChecksValidations enables an extra query for each certificate // checked, to find the relevant authzs. Since this query might be // expensive, we gate it behind a feature flag. @@ -59,38 +51,35 @@ type Config struct { // for the cert URL to appear. AsyncFinalize bool - // DOH enables DNS-over-HTTPS queries for validation - DOH bool + // CheckIdentifiersPaused checks if any of the identifiers in the order are + // currently paused at NewOrder time. If any are paused, an error is + // returned to the Subscriber indicating that the order cannot be processed + // until the paused identifiers are unpaused and the order is resubmitted. + CheckIdentifiersPaused bool - // EnforceMultiCAA causes the VA to kick off remote CAA rechecks when true. - // When false, no remote CAA rechecks will be performed. The primary VA will - // make a valid/invalid decision with the results. The primary VA will - // return an early decision if MultiCAAFullResults is false. - EnforceMultiCAA bool + // PropagateCancels controls whether the WFE and ocsp-responder allows + // cancellation of an inbound request to cancel downstream gRPC and other + // queries. In practice, cancellation of an inbound request is achieved by + // Nginx closing the connection on which the request was happening. This may + // help shed load in overcapacity situations. However, note that in-progress + // database queries (for instance, in the SA) are not cancelled. Database + // queries waiting for an available connection may be cancelled. + PropagateCancels bool - // MultiCAAFullResults will cause the main VA to block and wait for all of - // the remote VA CAA recheck results instead of returning early if the - // number of failures is greater than the configured - // maxRemoteValidationFailures. Only used when EnforceMultiCAA is true. - MultiCAAFullResults bool + // AutomaticallyPauseZombieClients configures the RA to automatically track + // and pause issuance for each (account, hostname) pair that repeatedly + // fails validation. + AutomaticallyPauseZombieClients bool - // TrackReplacementCertificatesARI, when enabled, triggers the following - // behavior: - // - SA.NewOrderAndAuthzs: upon receiving a NewOrderRequest with a - // 'replacesSerial' value, will create a new entry in the 'replacement - // Orders' table. This will occur inside of the new order transaction. - // - SA.FinalizeOrder will update the 'replaced' column of any row with - // a 'orderID' matching the finalized order to true. This will occur - // inside of the finalize (order) transaction. - TrackReplacementCertificatesARI bool + // NoPendingAuthzReuse causes the RA to only select already-validated authzs + // to attach to a newly created order. This preserves important client-facing + // functionality (valid authz reuse) while letting us simplify our code by + // removing pending authz reuse. + NoPendingAuthzReuse bool - // MultipleCertificateProfiles, when enabled, triggers the following - // behavior: - // - SA.NewOrderAndAuthzs: upon receiving a NewOrderRequest with a - // `certificateProfileName` value, will add that value to the database's - // `orders.certificateProfileName` column. Values in this column are - // allowed to be empty. - MultipleCertificateProfiles bool + // StoreARIReplacesInOrders causes the SA to store and retrieve the optional + // ARI replaces field in the orders table. + StoreARIReplacesInOrders bool } var fMu = new(sync.RWMutex) diff --git a/third-party/github.com/letsencrypt/boulder/go.mod b/third-party/github.com/letsencrypt/boulder/go.mod index 5f668f3a2..6733d91ca 100644 --- a/third-party/github.com/letsencrypt/boulder/go.mod +++ b/third-party/github.com/letsencrypt/boulder/go.mod @@ -1,99 +1,94 @@ module github.com/letsencrypt/boulder -go 1.22.0 +go 1.24.0 require ( - github.com/aws/aws-sdk-go-v2 v1.27.2 - github.com/aws/aws-sdk-go-v2/config v1.27.18 - github.com/aws/aws-sdk-go-v2/service/s3 v1.55.1 - github.com/aws/smithy-go v1.20.2 - github.com/eggsampler/acme/v3 v3.6.0 - github.com/go-jose/go-jose/v4 v4.0.1 + github.com/aws/aws-sdk-go-v2 v1.36.5 + github.com/aws/aws-sdk-go-v2/config v1.29.17 + github.com/aws/aws-sdk-go-v2/service/s3 v1.81.0 + github.com/aws/smithy-go v1.22.4 + github.com/eggsampler/acme/v3 v3.6.2-0.20250208073118-0466a0230941 + github.com/go-jose/go-jose/v4 v4.1.0 github.com/go-logr/stdr v1.2.2 - github.com/go-sql-driver/mysql v1.5.0 + github.com/go-sql-driver/mysql v1.9.1 github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da - github.com/google/certificate-transparency-go v1.1.6 - github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 + github.com/google/certificate-transparency-go v1.3.2-0.20250507091337-0eddb39e94f8 + github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 github.com/jmhodges/clock v1.2.0 - github.com/letsencrypt/borp v0.0.0-20230707160741-6cc6ce580243 - github.com/letsencrypt/challtestsrv v1.2.1 + github.com/letsencrypt/borp v0.0.0-20240620175310-a78493c6e2bd + github.com/letsencrypt/challtestsrv v1.3.3 github.com/letsencrypt/pkcs11key/v4 v4.0.0 github.com/letsencrypt/validator/v10 v10.0.0-20230215210743-a0c7dfc17158 - github.com/miekg/dns v1.1.58 + github.com/miekg/dns v1.1.61 github.com/miekg/pkcs11 v1.1.1 github.com/nxadm/tail v1.4.11 - github.com/prometheus/client_golang v1.15.1 - github.com/prometheus/client_model v0.4.0 - github.com/redis/go-redis/v9 v9.3.0 + github.com/prometheus/client_golang v1.22.0 + github.com/prometheus/client_model v0.6.1 + github.com/redis/go-redis/extra/redisotel/v9 v9.5.3 + github.com/redis/go-redis/v9 v9.7.3 github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 - github.com/weppos/publicsuffix-go v0.30.3-0.20240510084413-5f1d03393b3d - github.com/zmap/zcrypto v0.0.0-20231219022726-a1f61fb1661c - github.com/zmap/zlint/v3 v3.6.0 - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.52.0 - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 - go.opentelemetry.io/otel v1.27.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 - go.opentelemetry.io/otel/sdk v1.27.0 - go.opentelemetry.io/otel/trace v1.27.0 - golang.org/x/crypto v0.23.0 - golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 - golang.org/x/net v0.25.0 - golang.org/x/sync v0.7.0 - golang.org/x/term v0.20.0 - golang.org/x/text v0.15.0 - google.golang.org/grpc v1.64.0 - google.golang.org/protobuf v1.34.1 + github.com/weppos/publicsuffix-go v0.40.3-0.20250307081557-c05521c3453a + github.com/zmap/zcrypto v0.0.0-20250129210703-03c45d0bae98 + github.com/zmap/zlint/v3 v3.6.6 + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 + go.opentelemetry.io/otel v1.36.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 + go.opentelemetry.io/otel/sdk v1.36.0 + go.opentelemetry.io/otel/trace v1.36.0 + golang.org/x/crypto v0.38.0 + golang.org/x/net v0.40.0 + golang.org/x/sync v0.14.0 + golang.org/x/term v0.32.0 + golang.org/x/text v0.25.0 + golang.org/x/time v0.11.0 + google.golang.org/grpc v1.72.1 + google.golang.org/protobuf v1.36.6 gopkg.in/yaml.v3 v3.0.1 ) require ( - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.17.18 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.5 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.9 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.9 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.9 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.11 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.11 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.9 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.20.11 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.5 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.28.12 // indirect + filippo.io/edwards25519 v1.1.0 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.70 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.25.5 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/cenkalti/backoff/v4 v4.3.0 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cenkalti/backoff/v5 v5.0.2 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect - github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/logr v1.4.2 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/golang/protobuf v1.5.4 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/poy/onpar v1.1.2 // indirect - github.com/prometheus/common v0.42.0 // indirect - github.com/prometheus/procfs v0.9.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 // indirect - go.opentelemetry.io/otel/metric v1.27.0 // indirect - go.opentelemetry.io/proto/otlp v1.2.0 // indirect - golang.org/x/mod v0.14.0 // indirect - golang.org/x/sys v0.20.0 // indirect - golang.org/x/tools v0.17.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240520151616-dc85e6b867a5 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240520151616-dc85e6b867a5 // indirect + github.com/prometheus/common v0.62.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/redis/go-redis/extra/rediscmd/v9 v9.5.3 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 // indirect + go.opentelemetry.io/otel/metric v1.36.0 // indirect + go.opentelemetry.io/proto/otlp v1.6.0 // indirect + golang.org/x/mod v0.22.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/tools v0.29.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect - k8s.io/klog/v2 v2.100.1 // indirect ) - -// Versions of go-sql-driver/mysql >1.5.0 introduce performance regressions for -// us, so we exclude them. - -// This version is required by parts of the honeycombio/beeline-go package -exclude github.com/go-sql-driver/mysql v1.6.0 - -// This version is required by borp -exclude github.com/go-sql-driver/mysql v1.7.1 diff --git a/third-party/github.com/letsencrypt/boulder/go.sum b/third-party/github.com/letsencrypt/boulder/go.sum index 8d476f8cb..b769040d2 100644 --- a/third-party/github.com/letsencrypt/boulder/go.sum +++ b/third-party/github.com/letsencrypt/boulder/go.sum @@ -1,48 +1,48 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g= github.com/a8m/expect v1.0.0/go.mod h1:4IwSCMumY49ScypDnjNbYEjgVeqy1/U2cEs3Lat96eA= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= -github.com/aws/aws-sdk-go-v2 v1.27.2 h1:pLsTXqX93rimAOZG2FIYraDQstZaaGVVN4tNw65v0h8= -github.com/aws/aws-sdk-go-v2 v1.27.2/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2 h1:x6xsQXGSmW6frevwDA+vi/wqhp1ct18mVXYN08/93to= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2/go.mod h1:lPprDr1e6cJdyYeGXnRaJoP4Md+cDBvi2eOj00BlGmg= -github.com/aws/aws-sdk-go-v2/config v1.27.18 h1:wFvAnwOKKe7QAyIxziwSKjmer9JBMH1vzIL6W+fYuKk= -github.com/aws/aws-sdk-go-v2/config v1.27.18/go.mod h1:0xz6cgdX55+kmppvPm2IaKzIXOheGJhAufacPJaXZ7c= -github.com/aws/aws-sdk-go-v2/credentials v1.17.18 h1:D/ALDWqK4JdY3OFgA2thcPO1c9aYTT5STS/CvnkqY1c= -github.com/aws/aws-sdk-go-v2/credentials v1.17.18/go.mod h1:JuitCWq+F5QGUrmMPsk945rop6bB57jdscu+Glozdnc= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.5 h1:dDgptDO9dxeFkXy+tEgVkzSClHZje/6JkPW5aZyEvrQ= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.5/go.mod h1:gjvE2KBUgUQhcv89jqxrIxH9GaKs1JbZzWejj/DaHGA= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.9 h1:cy8ahBJuhtM8GTTSyOkfy6WVPV1IE+SS5/wfXUYuulw= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.9/go.mod h1:CZBXGLaJnEZI6EVNcPd7a6B5IC5cA/GkRWtu9fp3S6Y= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.9 h1:A4SYk07ef04+vxZToz9LWvAXl9LW0NClpPpMsi31cz0= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.9/go.mod h1:5jJcHuwDagxN+ErjQ3PU3ocf6Ylc/p9x+BLO/+X4iXw= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.9 h1:vHyZxoLVOgrI8GqX7OMHLXp4YYoxeEsrjweXKpye+ds= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.9/go.mod h1:z9VXZsWA2BvZNH1dT0ToUYwMu/CR9Skkj/TBX+mceZw= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 h1:Ji0DY1xUsUr3I8cHps0G+XM3WWU16lP6yG8qu1GAZAs= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2/go.mod h1:5CsjAbs3NlGQyZNFACh+zztPDI7fU6eW9QsxjfnuBKg= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.11 h1:4vt9Sspk59EZyHCAEMaktHKiq0C09noRTQorXD/qV+s= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.11/go.mod h1:5jHR79Tv+Ccq6rwYh+W7Nptmw++WiFafMfR42XhwNl8= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.11 h1:o4T+fKxA3gTMcluBNZZXE9DNaMkJuUL1O3mffCUjoJo= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.11/go.mod h1:84oZdJ+VjuJKs9v1UTC9NaodRZRseOXCTgku+vQJWR8= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.9 h1:TE2i0A9ErH1YfRSvXfCr2SQwfnqsoJT9nPQ9kj0lkxM= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.9/go.mod h1:9TzXX3MehQNGPwCZ3ka4CpwQsoAMWSF48/b+De9rfVM= -github.com/aws/aws-sdk-go-v2/service/s3 v1.55.1 h1:UAxBuh0/8sFJk1qOkvOKewP5sWeWaTPDknbQz0ZkDm0= -github.com/aws/aws-sdk-go-v2/service/s3 v1.55.1/go.mod h1:hWjsYGjVuqCgfoveVcVFPXIWgz0aByzwaxKlN1StKcM= -github.com/aws/aws-sdk-go-v2/service/sso v1.20.11 h1:gEYM2GSpr4YNWc6hCd5nod4+d4kd9vWIAWrmGuLdlMw= -github.com/aws/aws-sdk-go-v2/service/sso v1.20.11/go.mod h1:gVvwPdPNYehHSP9Rs7q27U1EU+3Or2ZpXvzAYJNh63w= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.5 h1:iXjh3uaH3vsVcnyZX7MqCoCfcyxIrVE9iOQruRaWPrQ= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.5/go.mod h1:5ZXesEuy/QcO0WUnt+4sDkxhdXRHTu2yG0uCSH8B6os= -github.com/aws/aws-sdk-go-v2/service/sts v1.28.12 h1:M/1u4HBpwLuMtjlxuI2y6HoVLzF5e2mfxHCg7ZVMYmk= -github.com/aws/aws-sdk-go-v2/service/sts v1.28.12/go.mod h1:kcfd+eTdEi/40FIbLq4Hif3XMXnl5b/+t/KTfLt9xIk= -github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q= -github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= +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/aws/protocol/eventstream v1.6.11 h1:12SpdwU8Djs+YGklkinSSlcrPyj3H4VifVsKf78KbwA= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11/go.mod h1:dd+Lkp6YmMryke+qxW/VnKyhMBDTYP41Q2Bb+6gNZgY= +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/internal/v4a v1.3.36 h1:GMYy2EOWfzdP3wfVAGXBNKY5vK4K8vMET4sYOYltmqs= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36/go.mod h1:gDhdAV6wL3PmPqBhiPbnlS447GoWs8HTTOYef9/9Inw= +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/checksum v1.7.4 h1:nAP2GYbfh8dd2zGZqFRSMlq+/F6cMPBUuCsGAMkN074= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4/go.mod h1:LT10DsiGjLWh4GbjInf9LQejkYEhBgBCjLG5+lvk4EE= +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/internal/s3shared v1.18.17 h1:qcLWgdhq45sDM9na4cvXax9dyLitn8EYBRl8Ak4XtG4= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17/go.mod h1:M+jkjBFZ2J6DJrjMv2+vkBbuht6kxJYtJiwoVgX4p4U= +github.com/aws/aws-sdk-go-v2/service/s3 v1.81.0 h1:1GmCadhKR3J2sMVKs2bAYq9VnwYeCqfRyZzD4RASGlA= +github.com/aws/aws-sdk-go-v2/service/s3 v1.81.0/go.mod h1:kUklwasNoCn5YpyAqC/97r6dzTA1SRKJfKq16SXeoDU= +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/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -51,14 +51,12 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= -github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= -github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= -github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8= +github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= @@ -72,8 +70,8 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/eggsampler/acme/v3 v3.6.0 h1:TbQYoWlpl62fTdJq5i2LHBDY6h3LDU3pPAdyoUSQMOc= -github.com/eggsampler/acme/v3 v3.6.0/go.mod h1:/qh0rKC/Dh7Jj+p4So7DbWmFNzC4dpcpK53r226Fhuo= +github.com/eggsampler/acme/v3 v3.6.2-0.20250208073118-0466a0230941 h1:CnQwymLMJ3MSfjbZQ/bpaLfuXBZuM3LUgAHJ0gO/7d8= +github.com/eggsampler/acme/v3 v3.6.2-0.20250208073118-0466a0230941/go.mod h1:/qh0rKC/Dh7Jj+p4So7DbWmFNzC4dpcpK53r226Fhuo= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= @@ -81,15 +79,14 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U= -github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= +github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY= +github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= -github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= @@ -98,8 +95,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= -github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +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-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= @@ -110,29 +107,27 @@ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4er github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/certificate-transparency-go v1.1.6 h1:SW5K3sr7ptST/pIvNkSVWMiJqemRmkjJPPT0jzXdOOY= -github.com/google/certificate-transparency-go v1.1.6/go.mod h1:0OJjOsOk+wj6aYQgP7FU0ioQ0AJUmnWPFMqTjQeazPQ= +github.com/google/certificate-transparency-go v1.3.2-0.20250507091337-0eddb39e94f8 h1:1RSWsOSxq2gk4pD/63bhsPwoOXgz2yXVadxXPbwZ0ec= +github.com/google/certificate-transparency-go v1.3.2-0.20250507091337-0eddb39e94f8/go.mod h1:6Rm5w0Mlv87LyBNOCgfKYjdIBBpF42XpXGsbQvQGomQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-github/v50 v50.2.0/go.mod h1:VBY8FB6yPIjrtKhozXv4FQupxKLS6H4m6xFZlT43q8Q= -github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= +github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA= +github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 h1:pRhl55Yx1eC7BZ1N+BBWwnKaMyD8uC+34TLdndZMAKk= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0/go.mod h1:XKMd7iuf/RGPSMJ/U4HP0zS2Z9Fh8Ps9a+6X26m/tmI= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jmhodges/clock v1.2.0 h1:eq4kys+NI0PLngzaHEe7AmPT90XMGIEySD1JfV1PDIs= @@ -141,6 +136,8 @@ github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22 github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -150,26 +147,28 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/letsencrypt/borp v0.0.0-20230707160741-6cc6ce580243 h1:xS2U6PQYRURk61YN4Y5xvyLbQVyAP/8fpE6hJZdwEWs= -github.com/letsencrypt/borp v0.0.0-20230707160741-6cc6ce580243/go.mod h1:podMDq5wDu2ZO6JMKYQcjD3QdqOfNLWtP2RDSy8CHUU= -github.com/letsencrypt/challtestsrv v1.2.1 h1:Lzv4jM+wSgVMCeO5a/F/IzSanhClstFMnX6SfrAJXjI= -github.com/letsencrypt/challtestsrv v1.2.1/go.mod h1:Ur4e4FvELUXLGhkMztHOsPIsvGxD/kzSJninOrkM+zc= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/letsencrypt/borp v0.0.0-20240620175310-a78493c6e2bd h1:3c+LdlAOEcW1qmG8gtkMCyAEoslmj6XCmniB+926kMM= +github.com/letsencrypt/borp v0.0.0-20240620175310-a78493c6e2bd/go.mod h1:gMSMCNKhxox/ccR923EJsIvHeVVYfCABGbirqa0EwuM= +github.com/letsencrypt/challtestsrv v1.3.3 h1:ki02PH84fo6IOe/A+zt1/kfRBp2JrtauEaa5xwjg4/Q= +github.com/letsencrypt/challtestsrv v1.3.3/go.mod h1:Ur4e4FvELUXLGhkMztHOsPIsvGxD/kzSJninOrkM+zc= github.com/letsencrypt/pkcs11key/v4 v4.0.0 h1:qLc/OznH7xMr5ARJgkZCCWk+EomQkiNTOoOF5LAgagc= github.com/letsencrypt/pkcs11key/v4 v4.0.0/go.mod h1:EFUvBDay26dErnNb70Nd0/VW3tJiIbETBPTl9ATXQag= github.com/letsencrypt/validator/v10 v10.0.0-20230215210743-a0c7dfc17158 h1:HGFsIltYMUiB5eoFSowFzSoXkocM2k9ctmJ57QMGjys= github.com/letsencrypt/validator/v10 v10.0.0-20230215210743-a0c7dfc17158/go.mod h1:ZFNBS3H6OEsprCRjscty6GCBe5ZiX44x6qY4s7+bDX0= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= -github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= -github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mattn/go-sqlite3 v1.14.26 h1:h72fc7d3zXGhHpwjWw+fPOBxYUupuKlbhUAQi5n6t58= +github.com/mattn/go-sqlite3 v1.14.26/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= -github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= -github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4= -github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY= +github.com/miekg/dns v1.1.61 h1:nLxbwF3XxhwVSm8g9Dghm9MHPaUZuqhPiGL+675ZmEs= +github.com/miekg/dns v1.1.61/go.mod h1:mnAarhS3nWaW+NVP2wTkYVIZyHNJ098SJZUki3eykwQ= github.com/miekg/pkcs11 v1.0.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU= github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= @@ -177,6 +176,8 @@ github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrk github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8= github.com/mreiferson/go-httpclient v0.0.0-20201222173833-5e475fde3a4d/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8= +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/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nelsam/hel/v2 v2.3.2/go.mod h1:1ZTGfU2PFTOd5mx22i5O0Lc2GY933lQ2wb/ggy+rL3w= github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= @@ -194,32 +195,38 @@ github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY= github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= -github.com/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI= -github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= -github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= -github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= +github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= -github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= +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/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -github.com/redis/go-redis/v9 v9.3.0 h1:RiVDjmig62jIWp7Kk4XVLs0hzV6pI3PyTnnL0cnn0u0= -github.com/redis/go-redis/v9 v9.3.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= +github.com/redis/go-redis/extra/rediscmd/v9 v9.5.3 h1:1/BDligzCa40GTllkDnY3Y5DTHuKCONbB2JcRyIfl20= +github.com/redis/go-redis/extra/rediscmd/v9 v9.5.3/go.mod h1:3dZmcLn3Qw6FLlWASn1g4y+YO9ycEFUOM+bhBmzLVKQ= +github.com/redis/go-redis/extra/redisotel/v9 v9.5.3 h1:kuvuJL/+MZIEdvtb/kTBRiRgYaOmx1l+lYJyVdrRUOs= +github.com/redis/go-redis/extra/redisotel/v9 v9.5.3/go.mod h1:7f/FMrf5RRRVHXgfk7CzSVzXHiWeuOQUu2bsVqWoa+g= +github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= +github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= -github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +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.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= @@ -230,19 +237,25 @@ github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/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/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0= github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/weppos/publicsuffix-go v0.13.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k= -github.com/weppos/publicsuffix-go v0.30.2-0.20230730094716-a20f9abcc222/go.mod h1:s41lQh6dIsDWIC1OWh7ChWJXLH0zkJ9KHZVqA7vHyuQ= -github.com/weppos/publicsuffix-go v0.30.3-0.20240510084413-5f1d03393b3d h1:q80YKUcDWRNvvQcziH63e3ammTWARwrhohBCunHaYAg= -github.com/weppos/publicsuffix-go v0.30.3-0.20240510084413-5f1d03393b3d/go.mod h1:vLdXKydr/OJssAXmjY0XBgLXUfivBMrNRIBljgtqCnw= +github.com/weppos/publicsuffix-go v0.40.3-0.20250127173806-e489a31678ca/go.mod h1:43Dfyxu2dpmLg56at26Q4k9gwf3yWSUiwk8kGnwzULk= +github.com/weppos/publicsuffix-go v0.40.3-0.20250307081557-c05521c3453a h1:YTfQ27VVE3PLzEZnGeSrxSKXMOs0JM2lfK0u4qT3/Mk= +github.com/weppos/publicsuffix-go v0.40.3-0.20250307081557-c05521c3453a/go.mod h1:Uao6F2ZmUjG3hDVL4Bn43YHRLuLapqXWKOa9GWk9JC0= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -253,30 +266,34 @@ github.com/zmap/zcertificate v0.0.0-20180516150559-0e3d58b1bac4/go.mod h1:5iU54t github.com/zmap/zcertificate v0.0.1/go.mod h1:q0dlN54Jm4NVSSuzisusQY0hqDWvu92C+TWveAxiVWk= github.com/zmap/zcrypto v0.0.0-20201128221613-3719af1573cf/go.mod h1:aPM7r+JOkfL+9qSB4KbYjtoEzJqUK50EXkkJabeNJDQ= github.com/zmap/zcrypto v0.0.0-20201211161100-e54a5822fb7e/go.mod h1:aPM7r+JOkfL+9qSB4KbYjtoEzJqUK50EXkkJabeNJDQ= -github.com/zmap/zcrypto v0.0.0-20231219022726-a1f61fb1661c h1:U1b4THKcgOpJ+kILupuznNwPiURtwVW3e9alJvji9+s= -github.com/zmap/zcrypto v0.0.0-20231219022726-a1f61fb1661c/go.mod h1:GSDpFDD4TASObxvfZfvpZZ3OWHIUHMlhVWlkOe4ewVk= +github.com/zmap/zcrypto v0.0.0-20250129210703-03c45d0bae98 h1:Qp98bmMm9JHPPOaLi2Nb6oWoZ+1OyOMWI7PPeJrirI0= +github.com/zmap/zcrypto v0.0.0-20250129210703-03c45d0bae98/go.mod h1:YTUyN/U1oJ7RzCEY5hUweYxbVUu7X+11wB7OXZT15oE= github.com/zmap/zlint/v3 v3.0.0/go.mod h1:paGwFySdHIBEMJ61YjoqT4h7Ge+fdYG4sUQhnTb1lJ8= -github.com/zmap/zlint/v3 v3.6.0 h1:vTEaDRtYN0d/1Ax60T+ypvbLQUHwHxbvYRnUMVr35ug= -github.com/zmap/zlint/v3 v3.6.0/go.mod h1:NVgiIWssgzp0bNl8P4Gz94NHV2ep/4Jyj9V69uTmZyg= +github.com/zmap/zlint/v3 v3.6.6 h1:tH7RJM9bDmh7IonlLEkFIkIn8XDYDYjehhUPgpLVqYA= +github.com/zmap/zlint/v3 v3.6.6/go.mod h1:6yXG+CBOQBRpMCOnpIVPUUL296m5HYksZC9bj5LZkwE= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.52.0 h1:vS1Ao/R55RNV4O7TA2Qopok8yN+X0LIP6RVWLFkprck= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.52.0/go.mod h1:BMsdeOxN04K0L5FNUBfjFdvwWGNe/rkmSwH4Aelu/X0= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 h1:9l89oX4ba9kHbBol3Xin3leYJ+252h0zszDtBwyKe2A= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0/go.mod h1:XLZfZboOJWHNKUv7eH0inh0E9VV6eWDFB/9yJyTLPp0= -go.opentelemetry.io/otel v1.27.0 h1:9BZoF3yMK/O1AafMiQTVu0YDj5Ea4hPhxCs7sGva+cg= -go.opentelemetry.io/otel v1.27.0/go.mod h1:DMpAK8fzYRzs+bi3rS5REupisuqTheUlSZJ1WnZaPAQ= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 h1:R9DE4kQ4k+YtfLI2ULwX82VtNQ2J8yZmA7ZIF/D+7Mc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0/go.mod h1:OQFyQVrDlbe+R7xrEyDr/2Wr67Ol0hRUgsfA+V5A95s= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ= -go.opentelemetry.io/otel/metric v1.27.0 h1:hvj3vdEKyeCi4YaYfNjv2NUje8FqKqUY8IlF0FxV/ik= -go.opentelemetry.io/otel/metric v1.27.0/go.mod h1:mVFgmRlhljgBiuk/MP/oKylr4hs85GZAylncepAX/ak= -go.opentelemetry.io/otel/sdk v1.27.0 h1:mlk+/Y1gLPLn84U4tI8d3GNJmGT/eXe3ZuOXN9kTWmI= -go.opentelemetry.io/otel/sdk v1.27.0/go.mod h1:Ha9vbLwJE6W86YstIywK2xFfPjbWlCuwPtMkKdz/Y4A= -go.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5/Rscw= -go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4= -go.opentelemetry.io/proto/otlp v1.2.0 h1:pVeZGk7nXDC9O2hncA6nHldxEjm6LByfA2aN8IOkz94= -go.opentelemetry.io/proto/otlp v1.2.0/go.mod h1:gGpR8txAl5M03pDhMC79G6SdqNV26naRm/KDsgaHD8A= +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.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/exporters/otlp/otlptrace v1.36.0 h1:dNzwXjZKpMpE2JhmO+9HsPl42NIXFIFSUSSs0fiqra0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0/go.mod h1:90PoxvaEB5n6AOdZvi+yWJQoE95U8Dhhw2bSyRqnTD0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 h1:JgtbA0xkWHnTmYk7YusopJFX6uleBmAuZ8n05NEh8nQ= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0/go.mod h1:179AK5aar5R3eS9FucPy6rggvU0g52cvKId8pv4+v0c= +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.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.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= +go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= +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.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI= +go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= @@ -289,28 +306,30 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +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 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= -golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 h1:hNQpMuAJe5CtcUqCXaWga3FHu+kQvCqcsoVaQgSV60o= -golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 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.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +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.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= +golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= @@ -318,14 +337,16 @@ golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v 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.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= +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 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -334,8 +355,13 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/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.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +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.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -349,43 +375,50 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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.0.0-20220908164124-27713097b956/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.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +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 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.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.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +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-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 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.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= -golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +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 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +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.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +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 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= 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.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -394,29 +427,25 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200313205530-4303120df7d8/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= 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.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= -golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= +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.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE= +golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto/googleapis/api v0.0.0-20240520151616-dc85e6b867a5 h1:P8OJ/WCl/Xo4E4zoe4/bifHpSmmKwARqyqE4nW6J2GQ= -google.golang.org/genproto/googleapis/api v0.0.0-20240520151616-dc85e6b867a5/go.mod h1:RGnPtTG7r4i8sPlNyDeikXF99hMM+hN6QMm4ooG9g2g= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240520151616-dc85e6b867a5 h1:Q2RxlXqh1cgzzUgV261vBO2jI5R/3DD1J2pM0nI4NhU= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240520151616-dc85e6b867a5/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= +google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 h1:Kog3KlB4xevJlAcbbbzPfRG0+X9fdoGM+UBRKVz6Wr0= +google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237/go.mod h1:ezi0AVyMKDWy5xAncvjLWH7UcLBB5n7y2fQ8MzjJcto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:cJfm9zPbe1e873mHJzmQ1nwVEeRDU/T1wXDK2kUSU34= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= -google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= -google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= +google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= +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/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -432,5 +461,3 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg= -k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= diff --git a/third-party/github.com/letsencrypt/boulder/goodkey/blocked.go b/third-party/github.com/letsencrypt/boulder/goodkey/blocked.go deleted file mode 100644 index 198c09db4..000000000 --- a/third-party/github.com/letsencrypt/boulder/goodkey/blocked.go +++ /dev/null @@ -1,95 +0,0 @@ -package goodkey - -import ( - "crypto" - "crypto/sha256" - "encoding/base64" - "encoding/hex" - "errors" - "os" - - "github.com/letsencrypt/boulder/core" - "github.com/letsencrypt/boulder/strictyaml" -) - -// blockedKeys is a type for maintaining a map of SHA256 hashes -// of SubjectPublicKeyInfo's that should be considered blocked. -// blockedKeys are created by using loadBlockedKeysList. -type blockedKeys map[core.Sha256Digest]bool - -var ErrWrongDecodedSize = errors.New("not enough bytes decoded for sha256 hash") - -// blocked checks if the given public key is considered administratively -// blocked based on a SHA256 hash of the SubjectPublicKeyInfo. -// Important: blocked should not be called except on a blockedKeys instance -// returned from loadBlockedKeysList. -// function should not be used until after `loadBlockedKeysList` has returned. -func (b blockedKeys) blocked(key crypto.PublicKey) (bool, error) { - hash, err := core.KeyDigest(key) - if err != nil { - // the bool result should be ignored when err is != nil but to be on the - // paranoid side return true anyway so that a key we can't compute the - // digest for will always be blocked even if a caller foolishly discards the - // err result. - return true, err - } - return b[hash], nil -} - -// loadBlockedKeysList creates a blockedKeys object that can be used to check if -// a key is blocked. It creates a lookup map from a list of -// SHA256 hashes of SubjectPublicKeyInfo's in the input YAML file -// with the expected format: -// -// blocked: -// - cuwGhNNI6nfob5aqY90e7BleU6l7rfxku4X3UTJ3Z7M= -// -// - Qebc1V3SkX3izkYRGNJilm9Bcuvf0oox4U2Rn+b4JOE= -// -// If no hashes are found in the input YAML an error is returned. -func loadBlockedKeysList(filename string) (*blockedKeys, error) { - yamlBytes, err := os.ReadFile(filename) - if err != nil { - return nil, err - } - - var list struct { - BlockedHashes []string `yaml:"blocked"` - BlockedHashesHex []string `yaml:"blockedHashesHex"` - } - err = strictyaml.Unmarshal(yamlBytes, &list) - if err != nil { - return nil, err - } - - if len(list.BlockedHashes) == 0 && len(list.BlockedHashesHex) == 0 { - return nil, errors.New("no blocked hashes in YAML") - } - - blockedKeys := make(blockedKeys, len(list.BlockedHashes)+len(list.BlockedHashesHex)) - for _, b64Hash := range list.BlockedHashes { - decoded, err := base64.StdEncoding.DecodeString(b64Hash) - if err != nil { - return nil, err - } - if len(decoded) != sha256.Size { - return nil, ErrWrongDecodedSize - } - var sha256Digest core.Sha256Digest - copy(sha256Digest[:], decoded[0:sha256.Size]) - blockedKeys[sha256Digest] = true - } - for _, hexHash := range list.BlockedHashesHex { - decoded, err := hex.DecodeString(hexHash) - if err != nil { - return nil, err - } - if len(decoded) != sha256.Size { - return nil, ErrWrongDecodedSize - } - var sha256Digest core.Sha256Digest - copy(sha256Digest[:], decoded[0:sha256.Size]) - blockedKeys[sha256Digest] = true - } - return &blockedKeys, nil -} diff --git a/third-party/github.com/letsencrypt/boulder/goodkey/blocked_test.go b/third-party/github.com/letsencrypt/boulder/goodkey/blocked_test.go deleted file mode 100644 index b3c2cdfce..000000000 --- a/third-party/github.com/letsencrypt/boulder/goodkey/blocked_test.go +++ /dev/null @@ -1,100 +0,0 @@ -package goodkey - -import ( - "context" - "crypto" - "os" - "testing" - - yaml "gopkg.in/yaml.v3" - - "github.com/letsencrypt/boulder/core" - "github.com/letsencrypt/boulder/test" - "github.com/letsencrypt/boulder/web" -) - -func TestBlockedKeys(t *testing.T) { - // Start with an empty list - var inList struct { - BlockedHashes []string `yaml:"blocked"` - BlockedHashesHex []string `yaml:"blockedHashesHex"` - } - - yamlList, err := yaml.Marshal(&inList) - test.AssertNotError(t, err, "error marshaling test blockedKeys list") - - yamlListFile, err := os.CreateTemp("", "test-blocked-keys-list.*.yaml") - test.AssertNotError(t, err, "error creating test blockedKeys yaml file") - defer os.Remove(yamlListFile.Name()) - - err = os.WriteFile(yamlListFile.Name(), yamlList, 0640) - test.AssertNotError(t, err, "error writing test blockedKeys yaml file") - - // Trying to load it should error - _, err = loadBlockedKeysList(yamlListFile.Name()) - test.AssertError(t, err, "expected error loading empty blockedKeys yaml file") - - // Load some test certs/keys - see ../test/block-a-key/test/README.txt - // for more information. - testCertA, err := core.LoadCert("../test/block-a-key/test/test.rsa.cert.pem") - test.AssertNotError(t, err, "error loading test.rsa.cert.pem") - testCertB, err := core.LoadCert("../test/block-a-key/test/test.ecdsa.cert.pem") - test.AssertNotError(t, err, "error loading test.ecdsa.cert.pem") - testJWKA, err := web.LoadJWK("../test/block-a-key/test/test.rsa.jwk.json") - test.AssertNotError(t, err, "error loading test.rsa.jwk.pem") - testJWKB, err := web.LoadJWK("../test/block-a-key/test/test.ecdsa.jwk.json") - test.AssertNotError(t, err, "error loading test.ecdsa.jwk.pem") - - // All of the above should be blocked - blockedKeys := []crypto.PublicKey{ - testCertA.PublicKey, - testCertB.PublicKey, - testJWKA.Key, - testJWKB.Key, - } - - // Now use a populated list - these values match the base64 digest of the - // public keys in the test certs/JWKs - inList.BlockedHashes = []string{ - "cuwGhNNI6nfob5aqY90e7BleU6l7rfxku4X3UTJ3Z7M=", - } - inList.BlockedHashesHex = []string{ - "41e6dcd55dd2917de2ce461118d262966f4172ebdfd28a31e14d919fe6f824e1", - } - - yamlList, err = yaml.Marshal(&inList) - test.AssertNotError(t, err, "error marshaling test blockedKeys list") - - yamlListFile, err = os.CreateTemp("", "test-blocked-keys-list.*.yaml") - test.AssertNotError(t, err, "error creating test blockedKeys yaml file") - defer os.Remove(yamlListFile.Name()) - - err = os.WriteFile(yamlListFile.Name(), yamlList, 0640) - test.AssertNotError(t, err, "error writing test blockedKeys yaml file") - - // Trying to load it should not error - outList, err := loadBlockedKeysList(yamlListFile.Name()) - test.AssertNotError(t, err, "unexpected error loading empty blockedKeys yaml file") - - // Create a test policy that doesn't reference the blocked list - testingPolicy := &KeyPolicy{allowedKeys: AllowedKeys{ - RSA2048: true, RSA3072: true, RSA4096: true, ECDSAP256: true, ECDSAP384: true, - }} - - // All of the test keys should not be considered blocked - for _, k := range blockedKeys { - err := testingPolicy.GoodKey(context.Background(), k) - test.AssertNotError(t, err, "test key was blocked by key policy without block list") - } - - // Now update the key policy with the blocked list - testingPolicy.blockedList = outList - - // Now all of the test keys should be considered blocked, and with the correct - // type of error. - for _, k := range blockedKeys { - err := testingPolicy.GoodKey(context.Background(), k) - test.AssertError(t, err, "test key was not blocked by key policy with block list") - test.AssertErrorIs(t, err, ErrBadKey) - } -} diff --git a/third-party/github.com/letsencrypt/boulder/goodkey/good_key.go b/third-party/github.com/letsencrypt/boulder/goodkey/good_key.go index 04a075d35..6f479fc18 100644 --- a/third-party/github.com/letsencrypt/boulder/goodkey/good_key.go +++ b/third-party/github.com/letsencrypt/boulder/goodkey/good_key.go @@ -42,18 +42,10 @@ type Config struct { // AllowedKeys enables or disables specific key algorithms and sizes. If // nil, defaults to just those keys allowed by the Let's Encrypt CPS. AllowedKeys *AllowedKeys - // WeakKeyFile is the path to a JSON file containing truncated modulus hashes - // of known weak RSA keys. If this config value is empty, then RSA modulus - // hash checking will be disabled. - WeakKeyFile string - // BlockedKeyFile is the path to a YAML file containing base64-encoded SHA256 - // hashes of PKIX Subject Public Keys that should be blocked. If this config - // value is empty, then blocked key checking will be disabled. - BlockedKeyFile string // FermatRounds is an integer number of rounds of Fermat's factorization // method that should be performed to attempt to detect keys whose modulus can // be trivially factored because the two factors are very close to each other. - // If this config value is empty (0), no factorization will be attempted. + // If this config value is empty or 0, it will default to 110 rounds. FermatRounds int } @@ -112,17 +104,14 @@ type BlockedKeyCheckFunc func(ctx context.Context, keyHash []byte) (bool, error) // operations. type KeyPolicy struct { allowedKeys AllowedKeys - weakRSAList *WeakRSAKeys - blockedList *blockedKeys fermatRounds int blockedCheck BlockedKeyCheckFunc } // NewPolicy returns a key policy based on the given configuration, with sane // defaults. If the config's AllowedKeys is nil, the LetsEncryptCPS AllowedKeys -// is used. If the config's WeakKeyFile or BlockedKeyFile paths are empty, those -// checks are disabled. If the config's FermatRounds is 0, Fermat Factorization -// is disabled. +// is used. If the configured FermatRounds is 0, Fermat Factorization defaults to +// attempting 110 rounds. func NewPolicy(config *Config, bkc BlockedKeyCheckFunc) (KeyPolicy, error) { if config == nil { config = &Config{} @@ -135,24 +124,14 @@ func NewPolicy(config *Config, bkc BlockedKeyCheckFunc) (KeyPolicy, error) { } else { kp.allowedKeys = *config.AllowedKeys } - if config.WeakKeyFile != "" { - keyList, err := LoadWeakRSASuffixes(config.WeakKeyFile) - if err != nil { - return KeyPolicy{}, err - } - kp.weakRSAList = keyList + if config.FermatRounds == 0 { + // The BRs require 100 rounds, so give ourselves a margin above that. + kp.fermatRounds = 110 + } else if config.FermatRounds < 100 { + return KeyPolicy{}, fmt.Errorf("Fermat factorization rounds must be at least 100: %d", config.FermatRounds) + } else { + kp.fermatRounds = config.FermatRounds } - if config.BlockedKeyFile != "" { - blocked, err := loadBlockedKeysList(config.BlockedKeyFile) - if err != nil { - return KeyPolicy{}, err - } - kp.blockedList = blocked - } - if config.FermatRounds < 0 { - return KeyPolicy{}, fmt.Errorf("Fermat factorization rounds cannot be negative: %d", config.FermatRounds) - } - kp.fermatRounds = config.FermatRounds return kp, nil } @@ -169,15 +148,6 @@ func (policy *KeyPolicy) GoodKey(ctx context.Context, key crypto.PublicKey) erro default: return badKey("unsupported key type %T", t) } - // If there is a blocked list configured then check if the public key is one - // that has been administratively blocked. - if policy.blockedList != nil { - if blocked, err := policy.blockedList.blocked(key); err != nil { - return fmt.Errorf("error checking blocklist for key: %v", key) - } else if blocked { - return badKey("public key is forbidden") - } - } if policy.blockedCheck != nil { digest, err := core.KeyDigest(key) if err != nil { @@ -322,10 +292,6 @@ func (policy *KeyPolicy) goodKeyRSA(key *rsa.PublicKey) error { return err } - if policy.weakRSAList != nil && policy.weakRSAList.Known(key) { - return badKey("key is on a known weak RSA key list") - } - // Rather than support arbitrary exponents, which significantly increases // the size of the key space we allow, we restrict E to the defacto standard // RSA exponent 65537. There is no specific standards document that specifies @@ -354,12 +320,11 @@ func (policy *KeyPolicy) goodKeyRSA(key *rsa.PublicKey) error { if rocacheck.IsWeak(key) { return badKey("key generated by vulnerable Infineon-based hardware") } + // Check if the key can be easily factored via Fermat's factorization method. - if policy.fermatRounds > 0 { - err := checkPrimeFactorsTooClose(modulus, policy.fermatRounds) - if err != nil { - return badKey("key generated with factors too close together: %w", err) - } + err = checkPrimeFactorsTooClose(modulus, policy.fermatRounds) + if err != nil { + return badKey("key generated with factors too close together: %w", err) } return nil @@ -439,7 +404,7 @@ func checkPrimeFactorsTooClose(n *big.Int, rounds int) error { b2 := new(big.Int) b2.Mul(a, a).Sub(b2, n) - for range rounds { + for round := range rounds { // To see if b2 is a perfect square, we take its square root, square that, // and check to see if we got the same result back. bb.Sqrt(b2).Mul(bb, bb) @@ -449,7 +414,7 @@ func checkPrimeFactorsTooClose(n *big.Int, rounds int) error { bb.Sqrt(bb) p := new(big.Int).Add(a, bb) q := new(big.Int).Sub(a, bb) - return fmt.Errorf("public modulus n = pq factored into p: %s; q: %s", p, q) + return fmt.Errorf("public modulus n = pq factored in %d rounds into p: %s and q: %s", round+1, p, q) } // Set up the next iteration by incrementing a by one and recalculating b2. diff --git a/third-party/github.com/letsencrypt/boulder/goodkey/good_key_test.go b/third-party/github.com/letsencrypt/boulder/goodkey/good_key_test.go index e12e73c7a..a512aea7d 100644 --- a/third-party/github.com/letsencrypt/boulder/goodkey/good_key_test.go +++ b/third-party/github.com/letsencrypt/boulder/goodkey/good_key_test.go @@ -25,13 +25,6 @@ func TestUnknownKeyType(t *testing.T) { err := testingPolicy.GoodKey(context.Background(), notAKey) test.AssertError(t, err, "Should have rejected a key of unknown type") test.AssertEquals(t, err.Error(), "unsupported key type struct {}") - - // Check for early rejection and that no error is seen from blockedKeys.blocked. - testingPolicyWithBlockedKeys := *testingPolicy - testingPolicyWithBlockedKeys.blockedList = &blockedKeys{} - err = testingPolicyWithBlockedKeys.GoodKey(context.Background(), notAKey) - test.AssertError(t, err, "Should have rejected a key of unknown type") - test.AssertEquals(t, err.Error(), "unsupported key type struct {}") } func TestNilKey(t *testing.T) { @@ -300,7 +293,7 @@ func TestDefaultAllowedKeys(t *testing.T) { test.Assert(t, policy.allowedKeys.ECDSAP384, "NIST P384 should be allowed") test.Assert(t, !policy.allowedKeys.ECDSAP521, "NIST P521 should not be allowed") - policy, err = NewPolicy(&Config{FermatRounds: 100}, nil) + policy, err = NewPolicy(&Config{}, nil) test.AssertNotError(t, err, "NewPolicy with nil config.AllowedKeys failed") test.Assert(t, policy.allowedKeys.RSA2048, "RSA 2048 should be allowed") test.Assert(t, policy.allowedKeys.RSA3072, "RSA 3072 should be allowed") @@ -318,43 +311,96 @@ func TestRSAStrangeSize(t *testing.T) { } func TestCheckPrimeFactorsTooClose(t *testing.T) { - // The prime factors of 5959 are 59 and 101. The values a and b calculated - // by Fermat's method will be 80 and 21. The ceil of the square root of 5959 - // is 78. Therefore it takes 3 rounds of Fermat's method to find the factors. - n := big.NewInt(5959) - err := checkPrimeFactorsTooClose(n, 2) - test.AssertNotError(t, err, "factored n in too few iterations") - err = checkPrimeFactorsTooClose(n, 3) - test.AssertError(t, err, "failed to factor n") - test.AssertContains(t, err.Error(), "p: 101") - test.AssertContains(t, err.Error(), "q: 59") + type testCase struct { + name string + p string + q string + expectRounds int + } - // These factors differ only in their second-to-last digit. They're so close - // that a single iteration of Fermat's method is sufficient to find them. - p, ok := new(big.Int).SetString("12451309173743450529024753538187635497858772172998414407116324997634262083672423797183640278969532658774374576700091736519352600717664126766443002156788367", 10) - test.Assert(t, ok, "failed to create large prime") - q, ok := new(big.Int).SetString("12451309173743450529024753538187635497858772172998414407116324997634262083672423797183640278969532658774374576700091736519352600717664126766443002156788337", 10) - test.Assert(t, ok, "failed to create large prime") - n = n.Mul(p, q) - err = checkPrimeFactorsTooClose(n, 0) - test.AssertNotError(t, err, "factored n in too few iterations") - err = checkPrimeFactorsTooClose(n, 1) - test.AssertError(t, err, "failed to factor n") - test.AssertContains(t, err.Error(), fmt.Sprintf("p: %s", p)) - test.AssertContains(t, err.Error(), fmt.Sprintf("q: %s", q)) + testCases := []testCase{ + { + // The factors 59 and 101 multiply to 5959. The values a and b calculated + // by Fermat's method will be 80 and 21. The ceil of the square root of + // 5959 is 78. Therefore it takes 3 rounds of Fermat's method to find the + // factors. + name: "tiny", + p: "101", + q: "59", + expectRounds: 3, + }, + { + // These factors differ only in their second-to-last digit. They're so close + // that a single iteration of Fermat's method is sufficient to find them. + name: "very close", + p: "12451309173743450529024753538187635497858772172998414407116324997634262083672423797183640278969532658774374576700091736519352600717664126766443002156788367", + q: "12451309173743450529024753538187635497858772172998414407116324997634262083672423797183640278969532658774374576700091736519352600717664126766443002156788337", + expectRounds: 1, + }, + { + // These factors differ by slightly more than 2^256, which takes fourteen + // rounds to factor. + name: "still too close", + p: "11779932606551869095289494662458707049283241949932278009554252037480401854504909149712949171865707598142483830639739537075502512627849249573564209082969463", + q: "11779932606551869095289494662458707049283241949932278009554252037480401854503793357623711855670284027157475142731886267090836872063809791989556295953329083", + expectRounds: 14, + }, + { + // These factors come from a real canon printer in the wild with a broken + // key generation mechanism. + name: "canon printer (2048 bit, 1 round)", + p: "155536235030272749691472293262418471207550926406427515178205576891522284497518443889075039382254334975506248481615035474816604875321501901699955105345417152355947783063521554077194367454070647740704883461064399268622437721385112646454393005862535727615809073410746393326688230040267160616554768771412289114449", + q: "155536235030272749691472293262418471207550926406427515178205576891522284497518443889075039382254334975506248481615035474816604875321501901699955105345417152355947783063521554077194367454070647740704883461064399268622437721385112646454393005862535727615809073410746393326688230040267160616554768771412289114113", + expectRounds: 1, + }, + { + // These factors come from a real innsbruck printer in the wild with a + // broken key generation mechanism. + name: "innsbruck printer (4096 bit, 1 round)", + p: "25868808535211632564072019392873831934145242707953960515208595626279836366691068618582894100813803673421320899654654938470888358089618966238341690624345530870988951109006149164192566967552401505863871260691612081236189439839963332690997129144163260418447718577834226720411404568398865166471102885763673744513186211985402019037772108416694793355840983833695882936201196462579254234744648546792097397517107797153785052856301942321429858537224127598198913168345965493941246097657533085617002572245972336841716321849601971924830462771411171570422802773095537171762650402420866468579928479284978914972383512240254605625661", + q: "25868808535211632564072019392873831934145242707953960515208595626279836366691068618582894100813803673421320899654654938470888358089618966238341690624345530870988951109006149164192566967552401505863871260691612081236189439839963332690997129144163260418447718577834226720411404568398865166471102885763673744513186211985402019037772108416694793355840983833695882936201196462579254234744648546792097397517107797153785052856301942321429858537224127598198913168345965493941246097657533085617002572245972336841716321849601971924830462771411171570422802773095537171762650402420866468579928479284978914972383512240254605624819", + expectRounds: 1, + }, + { + // FIPS requires that |p-q| > 2^(nlen/2 - 100). For example, a 2048-bit + // RSA key must have prime factors with a difference of at least 2^924. + // These two factors have a difference of exactly 2^924 + 4, just *barely* + // FIPS-compliant. Their first different digit is in column 52 of this + // file, which makes them vastly further apart than the cases above. Their + // product cannot be factored even with 100,000,000 rounds of Fermat's + // Algorithm. + name: "barely FIPS compliant (2048 bit)", + p: "151546560166767007654995655231369126386504564489055366370313539237722892921762327477057109592614214965864835328962951695621854530739049166771701397343693962526456985866167580660948398404000483264137738772983130282095332559392185543017295488346592188097443414824871619976114874896240350402349774470198190454623", + q: "151546560166767007654995655231510939369872272987323309037144546294925352276321214430320942815891873491060949332482502812040326472743233767963240491605860423063942576391584034077877871768428333113881339606298282107984376151546711223157061364850161576363709081794948857957944390170575452970542651659150041855843", + expectRounds: -1, + }, + } - // These factors differ by slightly more than 2^256. - p, ok = p.SetString("11779932606551869095289494662458707049283241949932278009554252037480401854504909149712949171865707598142483830639739537075502512627849249573564209082969463", 10) - test.Assert(t, ok, "failed to create large prime") - q, ok = q.SetString("11779932606551869095289494662458707049283241949932278009554252037480401854503793357623711855670284027157475142731886267090836872063809791989556295953329083", 10) - test.Assert(t, ok, "failed to create large prime") - n = n.Mul(p, q) - err = checkPrimeFactorsTooClose(n, 13) - test.AssertNotError(t, err, "factored n in too few iterations") - err = checkPrimeFactorsTooClose(n, 14) - test.AssertError(t, err, "failed to factor n") - test.AssertContains(t, err.Error(), fmt.Sprintf("p: %s", p)) - test.AssertContains(t, err.Error(), fmt.Sprintf("q: %s", q)) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + p, ok := new(big.Int).SetString(tc.p, 10) + if !ok { + t.Fatalf("failed to load prime factor p (%s)", tc.p) + } + + q, ok := new(big.Int).SetString(tc.q, 10) + if !ok { + t.Fatalf("failed to load prime factor q (%s)", tc.q) + } + + n := new(big.Int).Mul(p, q) + err := checkPrimeFactorsTooClose(n, 100) + + if tc.expectRounds > 0 { + test.AssertError(t, err, "failed to factor n") + test.AssertContains(t, err.Error(), fmt.Sprintf("p: %s", tc.p)) + test.AssertContains(t, err.Error(), fmt.Sprintf("q: %s", tc.q)) + test.AssertContains(t, err.Error(), fmt.Sprintf("in %d rounds", tc.expectRounds)) + } else { + test.AssertNil(t, err, "factored the unfactorable") + } + }) + } } func benchFermat(rounds int, b *testing.B) { diff --git a/third-party/github.com/letsencrypt/boulder/goodkey/weak.go b/third-party/github.com/letsencrypt/boulder/goodkey/weak.go deleted file mode 100644 index dd7afd5e4..000000000 --- a/third-party/github.com/letsencrypt/boulder/goodkey/weak.go +++ /dev/null @@ -1,66 +0,0 @@ -package goodkey - -// This file defines a basic method for testing if a given RSA public key is on one of -// the Debian weak key lists and is therefore considered compromised. Instead of -// directly loading the hash suffixes from the individual lists we flatten them all -// into a single JSON list using cmd/weak-key-flatten for ease of use. - -import ( - "crypto/rsa" - "crypto/sha1" - "encoding/hex" - "encoding/json" - "fmt" - "os" -) - -type truncatedHash [10]byte - -type WeakRSAKeys struct { - suffixes map[truncatedHash]struct{} -} - -func LoadWeakRSASuffixes(path string) (*WeakRSAKeys, error) { - f, err := os.ReadFile(path) - if err != nil { - return nil, err - } - - var suffixList []string - err = json.Unmarshal(f, &suffixList) - if err != nil { - return nil, err - } - - wk := &WeakRSAKeys{suffixes: make(map[truncatedHash]struct{})} - for _, suffix := range suffixList { - err := wk.addSuffix(suffix) - if err != nil { - return nil, err - } - } - return wk, nil -} - -func (wk *WeakRSAKeys) addSuffix(str string) error { - var suffix truncatedHash - decoded, err := hex.DecodeString(str) - if err != nil { - return err - } - if len(decoded) != 10 { - return fmt.Errorf("unexpected suffix length of %d", len(decoded)) - } - copy(suffix[:], decoded) - wk.suffixes[suffix] = struct{}{} - return nil -} - -func (wk *WeakRSAKeys) Known(key *rsa.PublicKey) bool { - // Hash input is in the format "Modulus={upper-case hex of modulus}\n" - hash := sha1.Sum([]byte(fmt.Sprintf("Modulus=%X\n", key.N.Bytes()))) - var suffix truncatedHash - copy(suffix[:], hash[10:]) - _, present := wk.suffixes[suffix] - return present -} diff --git a/third-party/github.com/letsencrypt/boulder/goodkey/weak_test.go b/third-party/github.com/letsencrypt/boulder/goodkey/weak_test.go deleted file mode 100644 index 1f1d1db51..000000000 --- a/third-party/github.com/letsencrypt/boulder/goodkey/weak_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package goodkey - -import ( - "crypto/rsa" - "encoding/hex" - "math/big" - "os" - "path/filepath" - "testing" - - "github.com/letsencrypt/boulder/test" -) - -func TestKnown(t *testing.T) { - modBytes, err := hex.DecodeString("D673252AF6723C3F72529403EAB7C30DEF3C52F97E799825F4A70191C616ADCF1ECE1113F1625971074C492C592025FDEADBDB146A081826BDF0D77C3C913DCF1B6F0B3B78F5108D2E493AD0EEE8CA5C021711ADC13D358E61133870FCD19C8E5C22403959782AA82E72AEE53A3D491E3912CE27B27E1A85EA69C19A527D28F7934C9823B7E56FDD657DAC83FDC65BB22A98D843DF73238919781B714C81A5E2AFEC71F5C54AA2A27C590AD94C03C1062D50EFCFFAC743E3C8A3AE056846A1D756EB862BF4224169D467C35215ADE0AFCC11E85FE629AFB802C4786FF2E9C929BCCF502B3D3B8876C6A11785CC398B389F1D86BDD9CB0BD4EC13956EC3FA270D") - test.AssertNotError(t, err, "Failed to decode modulus bytes") - mod := &big.Int{} - mod.SetBytes(modBytes) - testKey := rsa.PublicKey{N: mod} - otherKey := rsa.PublicKey{N: big.NewInt(2020)} - - wk := &WeakRSAKeys{suffixes: make(map[truncatedHash]struct{})} - err = wk.addSuffix("8df20e6961a16398b85a") - // a3853d0c563765e504c18df20e6961a16398b85a - test.AssertNotError(t, err, "WeakRSAKeys.addSuffix failed") - test.Assert(t, wk.Known(&testKey), "WeakRSAKeys.Known failed to find suffix that has been added") - test.Assert(t, !wk.Known(&otherKey), "WeakRSAKeys.Known found a suffix that has not been added") -} - -func TestLoadKeys(t *testing.T) { - modBytes, err := hex.DecodeString("D673252AF6723C3F72529403EAB7C30DEF3C52F97E799825F4A70191C616ADCF1ECE1113F1625971074C492C592025FDEADBDB146A081826BDF0D77C3C913DCF1B6F0B3B78F5108D2E493AD0EEE8CA5C021711ADC13D358E61133870FCD19C8E5C22403959782AA82E72AEE53A3D491E3912CE27B27E1A85EA69C19A527D28F7934C9823B7E56FDD657DAC83FDC65BB22A98D843DF73238919781B714C81A5E2AFEC71F5C54AA2A27C590AD94C03C1062D50EFCFFAC743E3C8A3AE056846A1D756EB862BF4224169D467C35215ADE0AFCC11E85FE629AFB802C4786FF2E9C929BCCF502B3D3B8876C6A11785CC398B389F1D86BDD9CB0BD4EC13956EC3FA270D") - test.AssertNotError(t, err, "Failed to decode modulus bytes") - mod := &big.Int{} - mod.SetBytes(modBytes) - testKey := rsa.PublicKey{N: mod} - tempDir := t.TempDir() - tempPath := filepath.Join(tempDir, "a.json") - err = os.WriteFile(tempPath, []byte("[\"8df20e6961a16398b85a\"]"), os.ModePerm) - test.AssertNotError(t, err, "Failed to create temporary file") - - wk, err := LoadWeakRSASuffixes(tempPath) - test.AssertNotError(t, err, "Failed to load suffixes from directory") - test.Assert(t, wk.Known(&testKey), "WeakRSAKeys.Known failed to find suffix that has been added") -} diff --git a/third-party/github.com/letsencrypt/boulder/grpc/client.go b/third-party/github.com/letsencrypt/boulder/grpc/client.go index 6234d5e16..87ff82f79 100644 --- a/third-party/github.com/letsencrypt/boulder/grpc/client.go +++ b/third-party/github.com/letsencrypt/boulder/grpc/client.go @@ -5,21 +5,25 @@ import ( "errors" "fmt" - grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus" + grpc_prometheus "github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus" "github.com/jmhodges/clock" - "github.com/letsencrypt/boulder/cmd" - bcreds "github.com/letsencrypt/boulder/grpc/creds" "github.com/prometheus/client_golang/prometheus" "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" "google.golang.org/grpc" - // 'grpc/health' is imported for its init function, which causes clients to - // rely on the Health Service for load-balancing. + "github.com/letsencrypt/boulder/cmd" + bcreds "github.com/letsencrypt/boulder/grpc/creds" + // 'grpc/internal/resolver/dns' is imported for its init function, which // registers the SRV resolver. - _ "github.com/letsencrypt/boulder/grpc/internal/resolver/dns" "google.golang.org/grpc/balancer/roundrobin" + + // 'grpc/health' is imported for its init function, which causes clients to + // rely on the Health Service for load-balancing as long as a + // "healthCheckConfig" is specified in the gRPC service config. _ "google.golang.org/grpc/health" + + _ "github.com/letsencrypt/boulder/grpc/internal/resolver/dns" ) // ClientSetup creates a gRPC TransportCredentials that presents @@ -44,13 +48,11 @@ func ClientSetup(c *cmd.GRPCClientConfig, tlsConfig *tls.Config, statsRegistry p unaryInterceptors := []grpc.UnaryClientInterceptor{ cmi.Unary, cmi.metrics.grpcMetrics.UnaryClientInterceptor(), - otelgrpc.UnaryClientInterceptor(), } streamInterceptors := []grpc.StreamClientInterceptor{ cmi.Stream, cmi.metrics.grpcMetrics.StreamClientInterceptor(), - otelgrpc.StreamClientInterceptor(), } target, hostOverride, err := c.MakeTargetAndHostOverride() @@ -59,12 +61,27 @@ func ClientSetup(c *cmd.GRPCClientConfig, tlsConfig *tls.Config, statsRegistry p } creds := bcreds.NewClientCredentials(tlsConfig.RootCAs, tlsConfig.Certificates, hostOverride) - return grpc.Dial( + return grpc.NewClient( target, - grpc.WithDefaultServiceConfig(fmt.Sprintf(`{"loadBalancingConfig": [{"%s":{}}]}`, roundrobin.Name)), + grpc.WithDefaultServiceConfig( + fmt.Sprintf( + // By setting the service name to an empty string in + // healthCheckConfig, we're instructing the gRPC client to query + // the overall health status of each server. The grpc-go health + // server, as constructed by health.NewServer(), unconditionally + // sets the overall service (e.g. "") status to SERVING. If a + // specific service name were set, the server would need to + // explicitly transition that service to SERVING; otherwise, + // clients would receive a NOT_FOUND status and the connection + // would be marked as unhealthy (TRANSIENT_FAILURE). + `{"healthCheckConfig": {"serviceName": ""},"loadBalancingConfig": [{"%s":{}}]}`, + roundrobin.Name, + ), + ), grpc.WithTransportCredentials(creds), grpc.WithChainUnaryInterceptor(unaryInterceptors...), grpc.WithChainStreamInterceptor(streamInterceptors...), + grpc.WithStatsHandler(otelgrpc.NewClientHandler()), ) } @@ -82,8 +99,11 @@ type clientMetrics struct { // maximum of once per registry, or there will be conflicting names. func newClientMetrics(stats prometheus.Registerer) (clientMetrics, error) { // Create the grpc prometheus client metrics instance and register it - grpcMetrics := grpc_prometheus.NewClientMetrics() - grpcMetrics.EnableClientHandlingTimeHistogram() + grpcMetrics := grpc_prometheus.NewClientMetrics( + grpc_prometheus.WithClientHandlingTimeHistogram( + grpc_prometheus.WithHistogramBuckets([]float64{.01, .025, .05, .1, .5, 1, 2.5, 5, 10, 45, 90}), + ), + ) err := stats.Register(grpcMetrics) if err != nil { are := prometheus.AlreadyRegisteredError{} diff --git a/third-party/github.com/letsencrypt/boulder/grpc/creds/creds_test.go b/third-party/github.com/letsencrypt/boulder/grpc/creds/creds_test.go index e252f004f..0cbf92b61 100644 --- a/third-party/github.com/letsencrypt/boulder/grpc/creds/creds_test.go +++ b/third-party/github.com/letsencrypt/boulder/grpc/creds/creds_test.go @@ -2,8 +2,9 @@ package creds import ( "context" + "crypto/ecdsa" + "crypto/elliptic" "crypto/rand" - "crypto/rsa" "crypto/tls" "crypto/x509" "math/big" @@ -80,8 +81,8 @@ func TestServerTransportCredentials(t *testing.T) { } func TestClientTransportCredentials(t *testing.T) { - priv, err := rsa.GenerateKey(rand.Reader, 1024) - test.AssertNotError(t, err, "rsa.GenerateKey failed") + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + test.AssertNotError(t, err, "failed to generate test key") temp := &x509.Certificate{ SerialNumber: big.NewInt(1), diff --git a/third-party/github.com/letsencrypt/boulder/grpc/errors_test.go b/third-party/github.com/letsencrypt/boulder/grpc/errors_test.go index 02b4953fd..98fd1eb32 100644 --- a/third-party/github.com/letsencrypt/boulder/grpc/errors_test.go +++ b/third-party/github.com/letsencrypt/boulder/grpc/errors_test.go @@ -12,6 +12,7 @@ import ( "google.golang.org/grpc/credentials/insecure" "github.com/jmhodges/clock" + berrors "github.com/letsencrypt/boulder/errors" "github.com/letsencrypt/boulder/grpc/test_proto" "github.com/letsencrypt/boulder/identifier" @@ -96,7 +97,7 @@ func TestSubErrorWrapping(t *testing.T) { subErrors := []berrors.SubBoulderError{ { - Identifier: identifier.DNSIdentifier("chillserver.com"), + Identifier: identifier.NewDNS("chillserver.com"), BoulderError: &berrors.BoulderError{ Type: berrors.RejectedIdentifier, Detail: "2 ill 2 chill", diff --git a/third-party/github.com/letsencrypt/boulder/grpc/interceptors.go b/third-party/github.com/letsencrypt/boulder/grpc/interceptors.go index 1d87a6dcf..83ad20ab6 100644 --- a/third-party/github.com/letsencrypt/boulder/grpc/interceptors.go +++ b/third-party/github.com/letsencrypt/boulder/grpc/interceptors.go @@ -18,12 +18,14 @@ import ( "github.com/letsencrypt/boulder/cmd" berrors "github.com/letsencrypt/boulder/errors" + "github.com/letsencrypt/boulder/web" ) const ( returnOverhead = 20 * time.Millisecond meaningfulWorkOverhead = 100 * time.Millisecond clientRequestTimeKey = "client-request-time" + userAgentKey = "acme-client-user-agent" ) type serverInterceptor interface { @@ -78,13 +80,19 @@ func (smi *serverMetadataInterceptor) Unary( return nil, berrors.InternalServerError("passed nil *grpc.UnaryServerInfo") } - // Extract the grpc metadata from the context. If the context has - // a `clientRequestTimeKey` field, and it has a value, then observe the RPC - // latency with Prometheus. - if md, ok := metadata.FromIncomingContext(ctx); ok && len(md[clientRequestTimeKey]) > 0 { - err := smi.observeLatency(md[clientRequestTimeKey][0]) - if err != nil { - return nil, err + // Extract the grpc metadata from the context, and handle the client request + // timestamp embedded in it. It's okay if the timestamp is missing, since some + // clients (like nomad's health-checker) don't set it. + md, ok := metadata.FromIncomingContext(ctx) + if ok { + if len(md[clientRequestTimeKey]) > 0 { + err := smi.checkLatency(md[clientRequestTimeKey][0]) + if err != nil { + return nil, err + } + } + if len(md[userAgentKey]) > 0 { + ctx = web.WithUserAgent(ctx, md[userAgentKey][0]) } } @@ -96,6 +104,9 @@ func (smi *serverMetadataInterceptor) Unary( // opposed to "RA.NewCertificate timed out" (causing a 500). // Once we've shaved the deadline, we ensure we have we have at least another // 100ms left to do work; otherwise we abort early. + // Note that these computations use the global clock (time.Now) instead of + // the local clock (smi.clk.Now) because context.WithTimeout also uses the + // global clock. deadline, ok := ctx.Deadline() // Should never happen: there was no deadline. if !ok { @@ -137,11 +148,12 @@ func (smi *serverMetadataInterceptor) Stream( handler grpc.StreamHandler) error { ctx := ss.Context() - // Extract the grpc metadata from the context. If the context has - // a `clientRequestTimeKey` field, and it has a value, then observe the RPC - // latency with Prometheus. - if md, ok := metadata.FromIncomingContext(ctx); ok && len(md[clientRequestTimeKey]) > 0 { - err := smi.observeLatency(md[clientRequestTimeKey][0]) + // Extract the grpc metadata from the context, and handle the client request + // timestamp embedded in it. It's okay if the timestamp is missing, since some + // clients (like nomad's health-checker) don't set it. + md, ok := metadata.FromIncomingContext(ctx) + if ok && len(md[clientRequestTimeKey]) > 0 { + err := smi.checkLatency(md[clientRequestTimeKey][0]) if err != nil { return err } @@ -155,6 +167,9 @@ func (smi *serverMetadataInterceptor) Stream( // opposed to "RA.NewCertificate timed out" (causing a 500). // Once we've shaved the deadline, we ensure we have we have at least another // 100ms left to do work; otherwise we abort early. + // Note that these computations use the global clock (time.Now) instead of + // the local clock (smi.clk.Now) because context.WithTimeout also uses the + // global clock. deadline, ok := ctx.Deadline() // Should never happen: there was no deadline. if !ok { @@ -190,12 +205,13 @@ func splitMethodName(fullMethodName string) (string, string) { return "unknown", "unknown" } -// observeLatency is called with the `clientRequestTimeKey` value from +// checkLatency is called with the `clientRequestTimeKey` value from // a request's gRPC metadata. This string value is converted to a timestamp and // used to calculate the latency between send and receive time. The latency is // published to the server interceptor's rpcLag prometheus histogram. An error -// is returned if the `clientReqTime` string is not a valid timestamp. -func (smi *serverMetadataInterceptor) observeLatency(clientReqTime string) error { +// is returned if the `clientReqTime` string is not a valid timestamp, or if +// the latency is so large that it indicates dangerous levels of clock skew. +func (smi *serverMetadataInterceptor) checkLatency(clientReqTime string) error { // Convert the metadata request time into an int64 reqTimeUnixNanos, err := strconv.ParseInt(clientReqTime, 10, 64) if err != nil { @@ -205,6 +221,17 @@ func (smi *serverMetadataInterceptor) observeLatency(clientReqTime string) error // Calculate the elapsed time since the client sent the RPC reqTime := time.Unix(0, reqTimeUnixNanos) elapsed := smi.clk.Since(reqTime) + + // If the elapsed time is very large, that indicates it is probably due to + // clock skew rather than simple latency. Refuse to handle the request, since + // accurate timekeeping is critical to CA operations and large skew indicates + // something has gone very wrong. + if tooSkewed(elapsed) { + return fmt.Errorf( + "gRPC client reported a very different time: %s (client) vs %s (this server)", + reqTime, smi.clk.Now()) + } + // Publish an RPC latency observation to the histogram smi.metrics.rpcLag.Observe(elapsed.Seconds()) return nil @@ -250,8 +277,10 @@ func (cmi *clientMetadataInterceptor) Unary( // Convert the current unix nano timestamp to a string for embedding in the grpc metadata nowTS := strconv.FormatInt(cmi.clk.Now().UnixNano(), 10) // Create a grpc/metadata.Metadata instance for the request metadata. - // Initialize it with the request time. - reqMD := metadata.New(map[string]string{clientRequestTimeKey: nowTS}) + reqMD := metadata.New(map[string]string{ + clientRequestTimeKey: nowTS, + userAgentKey: web.UserAgent(ctx), + }) // Configure the localCtx with the metadata so it gets sent along in the request localCtx = metadata.NewOutgoingContext(localCtx, reqMD) @@ -360,7 +389,10 @@ func (cmi *clientMetadataInterceptor) Stream( nowTS := strconv.FormatInt(cmi.clk.Now().UnixNano(), 10) // Create a grpc/metadata.Metadata instance for the request metadata. // Initialize it with the request time. - reqMD := metadata.New(map[string]string{clientRequestTimeKey: nowTS}) + reqMD := metadata.New(map[string]string{ + clientRequestTimeKey: nowTS, + userAgentKey: web.UserAgent(ctx), + }) // Configure the localCtx with the metadata so it gets sent along in the request localCtx = metadata.NewOutgoingContext(localCtx, reqMD) diff --git a/third-party/github.com/letsencrypt/boulder/grpc/interceptors_test.go b/third-party/github.com/letsencrypt/boulder/grpc/interceptors_test.go index 5e543d497..7e24640b4 100644 --- a/third-party/github.com/letsencrypt/boulder/grpc/interceptors_test.go +++ b/third-party/github.com/letsencrypt/boulder/grpc/interceptors_test.go @@ -28,6 +28,7 @@ import ( "github.com/letsencrypt/boulder/grpc/test_proto" "github.com/letsencrypt/boulder/metrics" "github.com/letsencrypt/boulder/test" + "github.com/letsencrypt/boulder/web" ) var fc = clock.NewFake() @@ -102,7 +103,7 @@ func TestWaitForReadyTrue(t *testing.T) { clk: clock.NewFake(), waitForReady: true, } - conn, err := grpc.Dial("localhost:19876", // random, probably unused port + conn, err := grpc.NewClient("localhost:19876", // random, probably unused port grpc.WithDefaultServiceConfig(fmt.Sprintf(`{"loadBalancingConfig": [{"%s":{}}]}`, roundrobin.Name)), grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithUnaryInterceptor(ci.Unary)) @@ -134,7 +135,7 @@ func TestWaitForReadyFalse(t *testing.T) { clk: clock.NewFake(), waitForReady: false, } - conn, err := grpc.Dial("localhost:19876", // random, probably unused port + conn, err := grpc.NewClient("localhost:19876", // random, probably unused port grpc.WithDefaultServiceConfig(fmt.Sprintf(`{"loadBalancingConfig": [{"%s":{}}]}`, roundrobin.Name)), grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithUnaryInterceptor(ci.Unary)) @@ -154,14 +155,14 @@ func TestWaitForReadyFalse(t *testing.T) { } } -// testServer is used to implement TestTimeouts, and will attempt to sleep for +// testTimeoutServer is used to implement TestTimeouts, and will attempt to sleep for // the given amount of time (unless it hits a timeout or cancel). -type testServer struct { +type testTimeoutServer struct { test_proto.UnimplementedChillerServer } // Chill implements ChillerServer.Chill -func (s *testServer) Chill(ctx context.Context, in *test_proto.Time) (*test_proto.Time, error) { +func (s *testTimeoutServer) Chill(ctx context.Context, in *test_proto.Time) (*test_proto.Time, error) { start := time.Now() // Sleep for either the requested amount of time, or the context times out or // is canceled. @@ -175,42 +176,9 @@ func (s *testServer) Chill(ctx context.Context, in *test_proto.Time) (*test_prot } func TestTimeouts(t *testing.T) { - // start server - lis, err := net.Listen("tcp", ":0") - if err != nil { - log.Fatalf("failed to listen: %v", err) - } - port := lis.Addr().(*net.TCPAddr).Port - - serverMetrics, err := newServerMetrics(metrics.NoopRegisterer) - test.AssertNotError(t, err, "creating server metrics") - si := newServerMetadataInterceptor(serverMetrics, clock.NewFake()) - s := grpc.NewServer(grpc.UnaryInterceptor(si.Unary)) - test_proto.RegisterChillerServer(s, &testServer{}) - go func() { - start := time.Now() - err := s.Serve(lis) - if err != nil && !strings.HasSuffix(err.Error(), "use of closed network connection") { - t.Logf("s.Serve: %v after %s", err, time.Since(start)) - } - }() - defer s.Stop() - - // make client - clientMetrics, err := newClientMetrics(metrics.NoopRegisterer) - test.AssertNotError(t, err, "creating client metrics") - ci := &clientMetadataInterceptor{ - timeout: 30 * time.Second, - metrics: clientMetrics, - clk: clock.NewFake(), - } - conn, err := grpc.Dial(net.JoinHostPort("localhost", strconv.Itoa(port)), - grpc.WithTransportCredentials(insecure.NewCredentials()), - grpc.WithUnaryInterceptor(ci.Unary)) - if err != nil { - t.Fatalf("did not connect: %v", err) - } - c := test_proto.NewChillerClient(conn) + server := new(testTimeoutServer) + client, _, stop := setup(t, server, clock.NewFake()) + defer stop() testCases := []struct { timeout time.Duration @@ -224,7 +192,7 @@ func TestTimeouts(t *testing.T) { t.Run(tc.timeout.String(), func(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), tc.timeout) defer cancel() - _, err := c.Chill(ctx, &test_proto.Time{Duration: durationpb.New(time.Second)}) + _, err := client.Chill(ctx, &test_proto.Time{Duration: durationpb.New(time.Second)}) if err == nil { t.Fatal("Got no error, expected a timeout") } @@ -236,58 +204,69 @@ func TestTimeouts(t *testing.T) { } func TestRequestTimeTagging(t *testing.T) { - clk := clock.NewFake() - // Listen for TCP requests on a random system assigned port number - lis, err := net.Listen("tcp", ":0") - if err != nil { - log.Fatalf("failed to listen: %v", err) - } - // Retrieve the concrete port numberthe system assigned our listener - port := lis.Addr().(*net.TCPAddr).Port - - // Create a new ChillerServer + server := new(testTimeoutServer) serverMetrics, err := newServerMetrics(metrics.NoopRegisterer) test.AssertNotError(t, err, "creating server metrics") - si := newServerMetadataInterceptor(serverMetrics, clk) - s := grpc.NewServer(grpc.UnaryInterceptor(si.Unary)) - test_proto.RegisterChillerServer(s, &testServer{}) - // Chill until ill - go func() { - start := time.Now() - err := s.Serve(lis) - if err != nil && !strings.HasSuffix(err.Error(), "use of closed network connection") { - t.Logf("s.Serve: %v after %s", err, time.Since(start)) - } - }() - defer s.Stop() - - // Dial the ChillerServer - clientMetrics, err := newClientMetrics(metrics.NoopRegisterer) - test.AssertNotError(t, err, "creating client metrics") - ci := &clientMetadataInterceptor{ - timeout: 30 * time.Second, - metrics: clientMetrics, - clk: clk, - } - conn, err := grpc.Dial(net.JoinHostPort("localhost", strconv.Itoa(port)), - grpc.WithTransportCredentials(insecure.NewCredentials()), - grpc.WithUnaryInterceptor(ci.Unary)) - if err != nil { - t.Fatalf("did not connect: %v", err) - } - // Create a ChillerClient with the connection to the ChillerServer - c := test_proto.NewChillerClient(conn) + client, _, stop := setup(t, server, serverMetrics) + defer stop() // Make an RPC request with the ChillerClient with a timeout higher than the // requested ChillerServer delay so that the RPC completes normally ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - if _, err := c.Chill(ctx, &test_proto.Time{Duration: durationpb.New(time.Second * 5)}); err != nil { + if _, err := client.Chill(ctx, &test_proto.Time{Duration: durationpb.New(time.Second * 5)}); err != nil { t.Fatalf("Unexpected error calling Chill RPC: %s", err) } // There should be one histogram sample in the serverInterceptor rpcLag stat - test.AssertMetricWithLabelsEquals(t, si.metrics.rpcLag, prometheus.Labels{}, 1) + test.AssertMetricWithLabelsEquals(t, serverMetrics.rpcLag, prometheus.Labels{}, 1) +} + +func TestClockSkew(t *testing.T) { + // Create two separate clocks for the client and server + serverClk := clock.NewFake() + serverClk.Set(time.Now()) + clientClk := clock.NewFake() + clientClk.Set(time.Now()) + + _, serverPort, stop := setup(t, &testTimeoutServer{}, serverClk) + defer stop() + + clientMetrics, err := newClientMetrics(metrics.NoopRegisterer) + test.AssertNotError(t, err, "creating client metrics") + ci := &clientMetadataInterceptor{ + timeout: 30 * time.Second, + metrics: clientMetrics, + clk: clientClk, + } + conn, err := grpc.NewClient(net.JoinHostPort("localhost", strconv.Itoa(serverPort)), + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithUnaryInterceptor(ci.Unary)) + if err != nil { + t.Fatalf("did not connect: %v", err) + } + + client := test_proto.NewChillerClient(conn) + + // Create a context with plenty of timeout + ctx, cancel := context.WithDeadline(context.Background(), clientClk.Now().Add(10*time.Second)) + defer cancel() + + // Attempt a gRPC request which should succeed + _, err = client.Chill(ctx, &test_proto.Time{Duration: durationpb.New(100 * time.Millisecond)}) + test.AssertNotError(t, err, "should succeed with no skew") + + // Skew the client clock forward and the request should fail due to skew + clientClk.Add(time.Hour) + _, err = client.Chill(ctx, &test_proto.Time{Duration: durationpb.New(100 * time.Millisecond)}) + test.AssertError(t, err, "should fail with positive client skew") + test.AssertContains(t, err.Error(), "very different time") + + // Skew the server clock forward and the request should fail due to skew + serverClk.Add(2 * time.Hour) + _, err = client.Chill(ctx, &test_proto.Time{Duration: durationpb.New(100 * time.Millisecond)}) + test.AssertError(t, err, "should fail with negative client skew") + test.AssertContains(t, err.Error(), "very different time") } // blockedServer implements a ChillerServer with a Chill method that: @@ -312,18 +291,15 @@ func (s *blockedServer) Chill(_ context.Context, _ *test_proto.Time) (*test_prot } func TestInFlightRPCStat(t *testing.T) { - clk := clock.NewFake() - // Listen for TCP requests on a random system assigned port number - lis, err := net.Listen("tcp", ":0") - if err != nil { - log.Fatalf("failed to listen: %v", err) - } - // Retrieve the concrete port numberthe system assigned our listener - port := lis.Addr().(*net.TCPAddr).Port - // Create a new blockedServer to act as a ChillerServer server := &blockedServer{} + metrics, err := newClientMetrics(metrics.NoopRegisterer) + test.AssertNotError(t, err, "creating client metrics") + + client, _, stop := setup(t, server, metrics) + defer stop() + // Increment the roadblock waitgroup - this will cause all chill RPCs to // the server to block until we call Done()! server.roadblock.Add(1) @@ -334,43 +310,11 @@ func TestInFlightRPCStat(t *testing.T) { numRPCs := 5 server.received.Add(numRPCs) - serverMetrics, err := newServerMetrics(metrics.NoopRegisterer) - test.AssertNotError(t, err, "creating server metrics") - si := newServerMetadataInterceptor(serverMetrics, clk) - s := grpc.NewServer(grpc.UnaryInterceptor(si.Unary)) - test_proto.RegisterChillerServer(s, server) - // Chill until ill - go func() { - start := time.Now() - err := s.Serve(lis) - if err != nil && !strings.HasSuffix(err.Error(), "use of closed network connection") { - t.Logf("s.Serve: %v after %s", err, time.Since(start)) - } - }() - defer s.Stop() - - // Dial the ChillerServer - clientMetrics, err := newClientMetrics(metrics.NoopRegisterer) - test.AssertNotError(t, err, "creating client metrics") - ci := &clientMetadataInterceptor{ - timeout: 30 * time.Second, - metrics: clientMetrics, - clk: clk, - } - conn, err := grpc.Dial(net.JoinHostPort("localhost", strconv.Itoa(port)), - grpc.WithTransportCredentials(insecure.NewCredentials()), - grpc.WithUnaryInterceptor(ci.Unary)) - if err != nil { - t.Fatalf("did not connect: %v", err) - } - // Create a ChillerClient with the connection to the ChillerServer - c := test_proto.NewChillerClient(conn) - // Fire off a few RPCs. They will block on the blockedServer's roadblock wg for range numRPCs { go func() { // Ignore errors, just chilllll. - _, _ = c.Chill(context.Background(), &test_proto.Time{}) + _, _ = client.Chill(context.Background(), &test_proto.Time{}) }() } @@ -385,7 +329,7 @@ func TestInFlightRPCStat(t *testing.T) { } // We expect the inFlightRPCs gauge for the Chiller.Chill RPCs to be equal to numRPCs. - test.AssertMetricWithLabelsEquals(t, ci.metrics.inFlightRPCs, labels, float64(numRPCs)) + test.AssertMetricWithLabelsEquals(t, metrics.inFlightRPCs, labels, float64(numRPCs)) // Unblock the blockedServer to let all of the Chiller.Chill RPCs complete server.roadblock.Done() @@ -393,7 +337,7 @@ func TestInFlightRPCStat(t *testing.T) { time.Sleep(1 * time.Second) // Check the gauge value again - test.AssertMetricWithLabelsEquals(t, ci.metrics.inFlightRPCs, labels, 0) + test.AssertMetricWithLabelsEquals(t, metrics.inFlightRPCs, labels, 0) } func TestServiceAuthChecker(t *testing.T) { @@ -468,3 +412,86 @@ func TestServiceAuthChecker(t *testing.T) { err = ac.checkContextAuth(ctx, "/package.ServiceName/Method/") test.AssertNotError(t, err, "checking allowed cert") } + +// testUserAgentServer stores the last value it saw in the user agent field of its context. +type testUserAgentServer struct { + test_proto.UnimplementedChillerServer + + lastSeenUA string +} + +// Chill implements ChillerServer.Chill +func (s *testUserAgentServer) Chill(ctx context.Context, in *test_proto.Time) (*test_proto.Time, error) { + s.lastSeenUA = web.UserAgent(ctx) + return nil, nil +} + +func TestUserAgentMetadata(t *testing.T) { + server := new(testUserAgentServer) + client, _, stop := setup(t, server) + defer stop() + + testUA := "test UA" + ctx := web.WithUserAgent(context.Background(), testUA) + + _, err := client.Chill(ctx, &test_proto.Time{}) + if err != nil { + t.Fatalf("calling c.Chill: %s", err) + } + + if server.lastSeenUA != testUA { + t.Errorf("last seen User-Agent on server side was %q, want %q", server.lastSeenUA, testUA) + } +} + +// setup creates a server and client, returning the created client, the running server's port, and a stop function. +func setup(t *testing.T, server test_proto.ChillerServer, opts ...any) (test_proto.ChillerClient, int, func()) { + clk := clock.NewFake() + serverMetricsVal, err := newServerMetrics(metrics.NoopRegisterer) + test.AssertNotError(t, err, "creating server metrics") + clientMetricsVal, err := newClientMetrics(metrics.NoopRegisterer) + test.AssertNotError(t, err, "creating client metrics") + + for _, opt := range opts { + switch optTyped := opt.(type) { + case clock.FakeClock: + clk = optTyped + case clientMetrics: + clientMetricsVal = optTyped + case serverMetrics: + serverMetricsVal = optTyped + default: + t.Fatalf("setup called with unrecognize option %#v", t) + } + } + lis, err := net.Listen("tcp", ":0") + if err != nil { + log.Fatalf("failed to listen: %v", err) + } + port := lis.Addr().(*net.TCPAddr).Port + + si := newServerMetadataInterceptor(serverMetricsVal, clk) + s := grpc.NewServer(grpc.UnaryInterceptor(si.Unary)) + test_proto.RegisterChillerServer(s, server) + + go func() { + start := time.Now() + err := s.Serve(lis) + if err != nil && !strings.HasSuffix(err.Error(), "use of closed network connection") { + t.Logf("s.Serve: %v after %s", err, time.Since(start)) + } + }() + + ci := &clientMetadataInterceptor{ + timeout: 30 * time.Second, + metrics: clientMetricsVal, + clk: clock.NewFake(), + } + conn, err := grpc.NewClient(net.JoinHostPort("localhost", strconv.Itoa(port)), + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithUnaryInterceptor(ci.Unary)) + if err != nil { + t.Fatalf("did not connect: %v", err) + } + return test_proto.NewChillerClient(conn), port, s.Stop +} diff --git a/third-party/github.com/letsencrypt/boulder/grpc/internal/grpcrand/grpcrand.go b/third-party/github.com/letsencrypt/boulder/grpc/internal/grpcrand/grpcrand.go index 740f83c2b..f4df37293 100644 --- a/third-party/github.com/letsencrypt/boulder/grpc/internal/grpcrand/grpcrand.go +++ b/third-party/github.com/letsencrypt/boulder/grpc/internal/grpcrand/grpcrand.go @@ -21,13 +21,12 @@ package grpcrand import ( - "math/rand" + "math/rand/v2" "sync" - "time" ) var ( - r = rand.New(rand.NewSource(time.Now().UnixNano())) + r = rand.New(rand.NewPCG(rand.Uint64(), rand.Uint64())) mu sync.Mutex ) @@ -42,14 +41,14 @@ func Int() int { func Int63n(n int64) int64 { mu.Lock() defer mu.Unlock() - return r.Int63n(n) + return r.Int64N(n) } // Intn implements rand.Intn on the grpcrand global source. func Intn(n int) int { mu.Lock() defer mu.Unlock() - return r.Intn(n) + return r.IntN(n) } // Float64 implements rand.Float64 on the grpcrand global source. diff --git a/third-party/github.com/letsencrypt/boulder/grpc/internal/resolver/dns/dns_resolver.go b/third-party/github.com/letsencrypt/boulder/grpc/internal/resolver/dns/dns_resolver.go index 1f6460eff..3fb6331a2 100644 --- a/third-party/github.com/letsencrypt/boulder/grpc/internal/resolver/dns/dns_resolver.go +++ b/third-party/github.com/letsencrypt/boulder/grpc/internal/resolver/dns/dns_resolver.go @@ -27,17 +27,19 @@ import ( "errors" "fmt" "net" + "net/netip" "strconv" "strings" "sync" "time" - "github.com/letsencrypt/boulder/bdns" - "github.com/letsencrypt/boulder/grpc/internal/backoff" - "github.com/letsencrypt/boulder/grpc/noncebalancer" "google.golang.org/grpc/grpclog" "google.golang.org/grpc/resolver" "google.golang.org/grpc/serviceconfig" + + "github.com/letsencrypt/boulder/bdns" + "github.com/letsencrypt/boulder/grpc/internal/backoff" + "github.com/letsencrypt/boulder/grpc/noncebalancer" ) var logger = grpclog.Component("srv") @@ -292,11 +294,11 @@ func (d *dnsResolver) lookup() (*resolver.State, error) { // If addr is an IPv4 address, return the addr and ok = true. // If addr is an IPv6 address, return the addr enclosed in square brackets and ok = true. func formatIP(addr string) (addrIP string, ok bool) { - ip := net.ParseIP(addr) - if ip == nil { + ip, err := netip.ParseAddr(addr) + if err != nil { return "", false } - if ip.To4() != nil { + if ip.Is4() { return addr, true } return "[" + addr + "]", true diff --git a/third-party/github.com/letsencrypt/boulder/grpc/internal/resolver/dns/dns_resolver_test.go b/third-party/github.com/letsencrypt/boulder/grpc/internal/resolver/dns/dns_resolver_test.go index 891fb970e..3ec584179 100644 --- a/third-party/github.com/letsencrypt/boulder/grpc/internal/resolver/dns/dns_resolver_test.go +++ b/third-party/github.com/letsencrypt/boulder/grpc/internal/resolver/dns/dns_resolver_test.go @@ -600,7 +600,7 @@ func TestCustomAuthority(t *testing.T) { err = <-errChan if err != nil { - t.Errorf(err.Error()) + t.Error(err.Error()) } if a.expectError { diff --git a/third-party/github.com/letsencrypt/boulder/grpc/noncebalancer/noncebalancer.go b/third-party/github.com/letsencrypt/boulder/grpc/noncebalancer/noncebalancer.go index cf4e56671..0fc202602 100644 --- a/third-party/github.com/letsencrypt/boulder/grpc/noncebalancer/noncebalancer.go +++ b/third-party/github.com/letsencrypt/boulder/grpc/noncebalancer/noncebalancer.go @@ -2,6 +2,7 @@ package noncebalancer import ( "errors" + "sync" "github.com/letsencrypt/boulder/nonce" @@ -35,7 +36,7 @@ var ErrNoBackendsMatchPrefix = status.New(codes.Unavailable, "no backends match var errMissingPrefixCtxKey = errors.New("nonce.PrefixCtxKey value required in RPC context") var errMissingHMACKeyCtxKey = errors.New("nonce.HMACKeyCtxKey value required in RPC context") var errInvalidPrefixCtxKeyType = errors.New("nonce.PrefixCtxKey value in RPC context must be a string") -var errInvalidHMACKeyCtxKeyType = errors.New("nonce.HMACKeyCtxKey value in RPC context must be a string") +var errInvalidHMACKeyCtxKeyType = errors.New("nonce.HMACKeyCtxKey value in RPC context must be a byte slice") // Balancer implements the base.PickerBuilder interface. It's used to create new // balancer.Picker instances. It should only be used by nonce-service clients. @@ -61,8 +62,9 @@ func (b *Balancer) Build(buildInfo base.PickerBuildInfo) balancer.Picker { // Picker implements the balancer.Picker interface. It picks a backend (SubConn) // based on the nonce prefix contained in each request's Context. type Picker struct { - backends map[balancer.SubConn]base.SubConnInfo - prefixToBackend map[string]balancer.SubConn + backends map[balancer.SubConn]base.SubConnInfo + prefixToBackend map[string]balancer.SubConn + prefixToBackendOnce sync.Once } // Compile-time assertion that *Picker implements the balancer.Picker interface. @@ -84,13 +86,13 @@ func (p *Picker) Pick(info balancer.PickInfo) (balancer.PickResult, error) { // This should never happen. return balancer.PickResult{}, errMissingHMACKeyCtxKey } - hmacKey, ok := hmacKeyVal.(string) + hmacKey, ok := hmacKeyVal.([]byte) if !ok { // This should never happen. return balancer.PickResult{}, errInvalidHMACKeyCtxKeyType } - if p.prefixToBackend == nil { + p.prefixToBackendOnce.Do(func() { // First call to Pick with a new Picker. prefixToBackend := make(map[string]balancer.SubConn) for sc, scInfo := range p.backends { @@ -98,7 +100,7 @@ func (p *Picker) Pick(info balancer.PickInfo) (balancer.PickResult, error) { prefixToBackend[scPrefix] = sc } p.prefixToBackend = prefixToBackend - } + }) // Get the destination prefix from the RPC context. destPrefixVal := info.Ctx.Value(nonce.PrefixCtxKey{}) diff --git a/third-party/github.com/letsencrypt/boulder/grpc/noncebalancer/noncebalancer_test.go b/third-party/github.com/letsencrypt/boulder/grpc/noncebalancer/noncebalancer_test.go index ce7a05649..b8127b9d5 100644 --- a/third-party/github.com/letsencrypt/boulder/grpc/noncebalancer/noncebalancer_test.go +++ b/third-party/github.com/letsencrypt/boulder/grpc/noncebalancer/noncebalancer_test.go @@ -4,19 +4,20 @@ import ( "context" "testing" - "github.com/letsencrypt/boulder/nonce" - "github.com/letsencrypt/boulder/test" "google.golang.org/grpc/balancer" "google.golang.org/grpc/balancer/base" "google.golang.org/grpc/resolver" + + "github.com/letsencrypt/boulder/nonce" + "github.com/letsencrypt/boulder/test" ) func TestPickerPicksCorrectBackend(t *testing.T) { _, p, subConns := setupTest(false) - prefix := nonce.DerivePrefix(subConns[0].addrs[0].Addr, "Kala namak") + prefix := nonce.DerivePrefix(subConns[0].addrs[0].Addr, []byte("Kala namak")) testCtx := context.WithValue(context.Background(), nonce.PrefixCtxKey{}, "HNmOnt8w") - testCtx = context.WithValue(testCtx, nonce.HMACKeyCtxKey{}, prefix) + testCtx = context.WithValue(testCtx, nonce.HMACKeyCtxKey{}, []byte(prefix)) info := balancer.PickInfo{Ctx: testCtx} gotPick, err := p.Pick(info) @@ -26,9 +27,9 @@ func TestPickerPicksCorrectBackend(t *testing.T) { func TestPickerMissingPrefixInCtx(t *testing.T) { _, p, subConns := setupTest(false) - prefix := nonce.DerivePrefix(subConns[0].addrs[0].Addr, "Kala namak") + prefix := nonce.DerivePrefix(subConns[0].addrs[0].Addr, []byte("Kala namak")) - testCtx := context.WithValue(context.Background(), nonce.HMACKeyCtxKey{}, prefix) + testCtx := context.WithValue(context.Background(), nonce.HMACKeyCtxKey{}, []byte(prefix)) info := balancer.PickInfo{Ctx: testCtx} gotPick, err := p.Pick(info) @@ -40,7 +41,7 @@ func TestPickerInvalidPrefixInCtx(t *testing.T) { _, p, _ := setupTest(false) testCtx := context.WithValue(context.Background(), nonce.PrefixCtxKey{}, 9) - testCtx = context.WithValue(testCtx, nonce.HMACKeyCtxKey{}, "foobar") + testCtx = context.WithValue(testCtx, nonce.HMACKeyCtxKey{}, []byte("foobar")) info := balancer.PickInfo{Ctx: testCtx} gotPick, err := p.Pick(info) @@ -73,10 +74,10 @@ func TestPickerInvalidHMACKeyInCtx(t *testing.T) { func TestPickerNoMatchingSubConnAvailable(t *testing.T) { _, p, subConns := setupTest(false) - prefix := nonce.DerivePrefix(subConns[0].addrs[0].Addr, "Kala namak") + prefix := nonce.DerivePrefix(subConns[0].addrs[0].Addr, []byte("Kala namak")) testCtx := context.WithValue(context.Background(), nonce.PrefixCtxKey{}, "rUsTrUin") - testCtx = context.WithValue(testCtx, nonce.HMACKeyCtxKey{}, prefix) + testCtx = context.WithValue(testCtx, nonce.HMACKeyCtxKey{}, []byte(prefix)) info := balancer.PickInfo{Ctx: testCtx} gotPick, err := p.Pick(info) @@ -114,19 +115,12 @@ func setupTest(noSubConns bool) (*Balancer, balancer.Picker, []*subConn) { return b, p, subConns } -// subConn implements the balancer.SubConn interface. +// subConn is a test mock which implements the balancer.SubConn interface. type subConn struct { + balancer.SubConn addrs []resolver.Address } func (s *subConn) UpdateAddresses(addrs []resolver.Address) { s.addrs = addrs } - -func (s *subConn) Connect() {} - -func (s *subConn) GetOrBuildProducer(balancer.ProducerBuilder) (p balancer.Producer, close func()) { - panic("unimplemented") -} - -func (s *subConn) Shutdown() {} diff --git a/third-party/github.com/letsencrypt/boulder/grpc/pb-marshalling.go b/third-party/github.com/letsencrypt/boulder/grpc/pb-marshalling.go index 90de4a9eb..758c44db8 100644 --- a/third-party/github.com/letsencrypt/boulder/grpc/pb-marshalling.go +++ b/third-party/github.com/letsencrypt/boulder/grpc/pb-marshalling.go @@ -7,7 +7,7 @@ package grpc import ( "fmt" - "net" + "net/netip" "time" "github.com/go-jose/go-jose/v4" @@ -18,12 +18,12 @@ import ( corepb "github.com/letsencrypt/boulder/core/proto" "github.com/letsencrypt/boulder/identifier" "github.com/letsencrypt/boulder/probs" - "github.com/letsencrypt/boulder/revocation" sapb "github.com/letsencrypt/boulder/sa/proto" vapb "github.com/letsencrypt/boulder/va/proto" ) var ErrMissingParameters = CodedError(codes.FailedPrecondition, "required RPC parameter was missing") +var ErrInvalidParameters = CodedError(codes.InvalidArgument, "RPC parameter was invalid") // This file defines functions to translate between the protobuf types and the // code types. @@ -36,7 +36,7 @@ func ProblemDetailsToPB(prob *probs.ProblemDetails) (*corepb.ProblemDetails, err return &corepb.ProblemDetails{ ProblemType: string(prob.Type), Detail: prob.Detail, - HttpStatus: int32(prob.HTTPStatus), + HttpStatus: int32(prob.HTTPStatus), //nolint: gosec // HTTP status codes are guaranteed to be small, no risk of overflow. }, nil } @@ -83,7 +83,6 @@ func ChallengeToPB(challenge core.Challenge) (*corepb.Challenge, error) { Type: string(challenge.Type), Status: string(challenge.Status), Token: challenge.Token, - KeyAuthorization: challenge.ProvidedKeyAuthorization, Error: prob, Validationrecords: recordAry, Validated: validated, @@ -124,9 +123,6 @@ func PBToChallenge(in *corepb.Challenge) (challenge core.Challenge, err error) { ValidationRecord: recordAry, Validated: validated, } - if in.KeyAuthorization != "" { - ch.ProvidedKeyAuthorization = in.KeyAuthorization - } return ch, nil } @@ -135,10 +131,10 @@ func ValidationRecordToPB(record core.ValidationRecord) (*corepb.ValidationRecor addrsTried := make([][]byte, len(record.AddressesTried)) var err error for i, v := range record.AddressesResolved { - addrs[i] = []byte(v) + addrs[i] = v.AsSlice() } for i, v := range record.AddressesTried { - addrsTried[i] = []byte(v) + addrsTried[i] = v.AsSlice() } addrUsed, err := record.AddressUsed.MarshalText() if err != nil { @@ -159,15 +155,23 @@ func PBToValidationRecord(in *corepb.ValidationRecord) (record core.ValidationRe if in == nil { return core.ValidationRecord{}, ErrMissingParameters } - addrs := make([]net.IP, len(in.AddressesResolved)) + addrs := make([]netip.Addr, len(in.AddressesResolved)) for i, v := range in.AddressesResolved { - addrs[i] = net.IP(v) + netIP, ok := netip.AddrFromSlice(v) + if !ok { + return core.ValidationRecord{}, ErrInvalidParameters + } + addrs[i] = netIP } - addrsTried := make([]net.IP, len(in.AddressesTried)) + addrsTried := make([]netip.Addr, len(in.AddressesTried)) for i, v := range in.AddressesTried { - addrsTried[i] = net.IP(v) + netIP, ok := netip.AddrFromSlice(v) + if !ok { + return core.ValidationRecord{}, ErrInvalidParameters + } + addrsTried[i] = netIP } - var addrUsed net.IP + var addrUsed netip.Addr err = addrUsed.UnmarshalText(in.AddressUsed) if err != nil { return @@ -183,7 +187,7 @@ func PBToValidationRecord(in *corepb.ValidationRecord) (record core.ValidationRe }, nil } -func ValidationResultToPB(records []core.ValidationRecord, prob *probs.ProblemDetails) (*vapb.ValidationResult, error) { +func ValidationResultToPB(records []core.ValidationRecord, prob *probs.ProblemDetails, perspective, rir string) (*vapb.ValidationResult, error) { recordAry := make([]*corepb.ValidationRecord, len(records)) var err error for i, v := range records { @@ -192,13 +196,15 @@ func ValidationResultToPB(records []core.ValidationRecord, prob *probs.ProblemDe return nil, err } } - marshalledProbs, err := ProblemDetailsToPB(prob) + marshalledProb, err := ProblemDetailsToPB(prob) if err != nil { return nil, err } return &vapb.ValidationResult{ - Records: recordAry, - Problems: marshalledProbs, + Records: recordAry, + Problem: marshalledProb, + Perspective: perspective, + Rir: rir, }, nil } @@ -214,7 +220,7 @@ func pbToValidationResult(in *vapb.ValidationResult) ([]core.ValidationRecord, * return nil, nil, err } } - prob, err := PBToProblemDetails(in.Problems) + prob, err := PBToProblemDetails(in.Problem) if err != nil { return nil, nil, err } @@ -226,15 +232,7 @@ func RegistrationToPB(reg core.Registration) (*corepb.Registration, error) { if err != nil { return nil, err } - ipBytes, err := reg.InitialIP.MarshalText() - if err != nil { - return nil, err - } var contacts []string - // Since the default value of corepb.Registration.Contact is a slice - // we need a indicator as to if the value is actually important on - // the other side (pb -> reg). - contactsPresent := reg.Contact != nil if reg.Contact != nil { contacts = *reg.Contact } @@ -247,14 +245,12 @@ func RegistrationToPB(reg core.Registration) (*corepb.Registration, error) { } return &corepb.Registration{ - Id: reg.ID, - Key: keyBytes, - Contact: contacts, - ContactsPresent: contactsPresent, - Agreement: reg.Agreement, - InitialIP: ipBytes, - CreatedAt: createdAt, - Status: string(reg.Status), + Id: reg.ID, + Key: keyBytes, + Contact: contacts, + Agreement: reg.Agreement, + CreatedAt: createdAt, + Status: string(reg.Status), }, nil } @@ -264,36 +260,20 @@ func PbToRegistration(pb *corepb.Registration) (core.Registration, error) { if err != nil { return core.Registration{}, err } - var initialIP net.IP - err = initialIP.UnmarshalText(pb.InitialIP) - if err != nil { - return core.Registration{}, err - } var createdAt *time.Time if !core.IsAnyNilOrZero(pb.CreatedAt) { c := pb.CreatedAt.AsTime() createdAt = &c } var contacts *[]string - if pb.ContactsPresent { - if len(pb.Contact) != 0 { - contacts = &pb.Contact - } else { - // When gRPC creates an empty slice it is actually a nil slice. Since - // certain things boulder uses, like encoding/json, differentiate between - // these we need to de-nil these slices. Without this we are unable to - // properly do registration updates as contacts would always be removed - // as we use the difference between a nil and empty slice in ra.mergeUpdate. - empty := []string{} - contacts = &empty - } + if len(pb.Contact) != 0 { + contacts = &pb.Contact } return core.Registration{ ID: pb.Id, Key: &key, Contact: contacts, Agreement: pb.Agreement, - InitialIP: initialIP, CreatedAt: createdAt, Status: core.AcmeStatus(pb.Status), }, nil @@ -317,12 +297,13 @@ func AuthzToPB(authz core.Authorization) (*corepb.Authorization, error) { } return &corepb.Authorization{ - Id: authz.ID, - Identifier: authz.Identifier.Value, - RegistrationID: authz.RegistrationID, - Status: string(authz.Status), - Expires: expires, - Challenges: challs, + Id: authz.ID, + Identifier: authz.Identifier.ToProto(), + RegistrationID: authz.RegistrationID, + Status: string(authz.Status), + Expires: expires, + Challenges: challs, + CertificateProfileName: authz.CertificateProfileName, }, nil } @@ -341,12 +322,13 @@ func PBToAuthz(pb *corepb.Authorization) (core.Authorization, error) { expires = &c } authz := core.Authorization{ - ID: pb.Id, - Identifier: identifier.ACMEIdentifier{Type: identifier.DNS, Value: pb.Identifier}, - RegistrationID: pb.RegistrationID, - Status: core.AcmeStatus(pb.Status), - Expires: expires, - Challenges: challs, + ID: pb.Id, + Identifier: identifier.FromProto(pb.Identifier), + RegistrationID: pb.RegistrationID, + Status: core.AcmeStatus(pb.Status), + Expires: expires, + Challenges: challs, + CertificateProfileName: pb.CertificateProfileName, } return authz, nil } @@ -366,69 +348,19 @@ func orderValid(order *corepb.Order) bool { // `order.CertificateSerial` to be nil such that it can be used in places where // the order has not been finalized yet. func newOrderValid(order *corepb.Order) bool { - return !(order.RegistrationID == 0 || order.Expires == nil || len(order.Names) == 0) + return !(order.RegistrationID == 0 || order.Expires == nil || len(order.Identifiers) == 0) } -func CertToPB(cert core.Certificate) *corepb.Certificate { - return &corepb.Certificate{ - RegistrationID: cert.RegistrationID, - Serial: cert.Serial, - Digest: cert.Digest, - Der: cert.DER, - Issued: timestamppb.New(cert.Issued), - Expires: timestamppb.New(cert.Expires), - } -} - -func PBToCert(pb *corepb.Certificate) core.Certificate { - return core.Certificate{ - RegistrationID: pb.RegistrationID, - Serial: pb.Serial, - Digest: pb.Digest, - DER: pb.Der, - Issued: pb.Issued.AsTime(), - Expires: pb.Expires.AsTime(), - } -} - -func CertStatusToPB(certStatus core.CertificateStatus) *corepb.CertificateStatus { - return &corepb.CertificateStatus{ - Serial: certStatus.Serial, - Status: string(certStatus.Status), - OcspLastUpdated: timestamppb.New(certStatus.OCSPLastUpdated), - RevokedDate: timestamppb.New(certStatus.RevokedDate), - RevokedReason: int64(certStatus.RevokedReason), - LastExpirationNagSent: timestamppb.New(certStatus.LastExpirationNagSent), - NotAfter: timestamppb.New(certStatus.NotAfter), - IsExpired: certStatus.IsExpired, - IssuerID: certStatus.IssuerNameID, - } -} - -func PBToCertStatus(pb *corepb.CertificateStatus) core.CertificateStatus { - return core.CertificateStatus{ - Serial: pb.Serial, - Status: core.OCSPStatus(pb.Status), - OCSPLastUpdated: pb.OcspLastUpdated.AsTime(), - RevokedDate: pb.RevokedDate.AsTime(), - RevokedReason: revocation.Reason(pb.RevokedReason), - LastExpirationNagSent: pb.LastExpirationNagSent.AsTime(), - NotAfter: pb.NotAfter.AsTime(), - IsExpired: pb.IsExpired, - IssuerNameID: pb.IssuerID, - } -} - -// PBToAuthzMap converts a protobuf map of domains mapped to protobuf authorizations to a -// golang map[string]*core.Authorization. -func PBToAuthzMap(pb *sapb.Authorizations) (map[string]*core.Authorization, error) { - m := make(map[string]*core.Authorization, len(pb.Authz)) - for _, v := range pb.Authz { - authz, err := PBToAuthz(v.Authz) +// PBToAuthzMap converts a protobuf map of identifiers mapped to protobuf +// authorizations to a golang map[string]*core.Authorization. +func PBToAuthzMap(pb *sapb.Authorizations) (map[identifier.ACMEIdentifier]*core.Authorization, error) { + m := make(map[identifier.ACMEIdentifier]*core.Authorization, len(pb.Authzs)) + for _, v := range pb.Authzs { + authz, err := PBToAuthz(v) if err != nil { return nil, err } - m[v.Domain] = &authz + m[authz.Identifier] = &authz } return m, nil } diff --git a/third-party/github.com/letsencrypt/boulder/grpc/pb-marshalling_test.go b/third-party/github.com/letsencrypt/boulder/grpc/pb-marshalling_test.go index 2973703bf..1b76ae831 100644 --- a/third-party/github.com/letsencrypt/boulder/grpc/pb-marshalling_test.go +++ b/third-party/github.com/letsencrypt/boulder/grpc/pb-marshalling_test.go @@ -2,7 +2,7 @@ package grpc import ( "encoding/json" - "net" + "net/netip" "testing" "time" @@ -55,11 +55,10 @@ func TestChallenge(t *testing.T) { test.AssertNotError(t, err, "Failed to unmarshal test key") validated := time.Now().Round(0).UTC() chall := core.Challenge{ - Type: core.ChallengeTypeDNS01, - Status: core.StatusValid, - Token: "asd", - ProvidedKeyAuthorization: "keyauth", - Validated: &validated, + Type: core.ChallengeTypeDNS01, + Status: core.StatusValid, + Token: "asd", + Validated: &validated, } pb, err := ChallengeToPB(chall) @@ -70,15 +69,15 @@ func TestChallenge(t *testing.T) { test.AssertNotError(t, err, "PBToChallenge failed") test.AssertDeepEquals(t, recon, chall) - ip := net.ParseIP("1.1.1.1") + ip := netip.MustParseAddr("1.1.1.1") chall.ValidationRecord = []core.ValidationRecord{ { Hostname: "example.com", Port: "2020", - AddressesResolved: []net.IP{ip}, + AddressesResolved: []netip.Addr{ip}, AddressUsed: ip, URL: "https://example.com:2020", - AddressesTried: []net.IP{ip}, + AddressesTried: []netip.Addr{ip}, }, } chall.Error = &probs.ProblemDetails{Type: probs.TLSProblem, Detail: "asd", HTTPStatus: 200} @@ -98,11 +97,10 @@ func TestChallenge(t *testing.T) { test.AssertEquals(t, err, ErrMissingParameters) challNilValidation := core.Challenge{ - Type: core.ChallengeTypeDNS01, - Status: core.StatusValid, - Token: "asd", - ProvidedKeyAuthorization: "keyauth", - Validated: nil, + Type: core.ChallengeTypeDNS01, + Status: core.StatusValid, + Token: "asd", + Validated: nil, } pb, err = ChallengeToPB(challNilValidation) test.AssertNotError(t, err, "ChallengeToPB failed") @@ -113,14 +111,14 @@ func TestChallenge(t *testing.T) { } func TestValidationRecord(t *testing.T) { - ip := net.ParseIP("1.1.1.1") + ip := netip.MustParseAddr("1.1.1.1") vr := core.ValidationRecord{ Hostname: "exampleA.com", Port: "80", - AddressesResolved: []net.IP{ip}, + AddressesResolved: []netip.Addr{ip}, AddressUsed: ip, URL: "http://exampleA.com", - AddressesTried: []net.IP{ip}, + AddressesTried: []netip.Addr{ip}, ResolverAddrs: []string{"resolver:5353"}, } @@ -134,31 +132,33 @@ func TestValidationRecord(t *testing.T) { } func TestValidationResult(t *testing.T) { - ip := net.ParseIP("1.1.1.1") + ip := netip.MustParseAddr("1.1.1.1") vrA := core.ValidationRecord{ Hostname: "exampleA.com", Port: "443", - AddressesResolved: []net.IP{ip}, + AddressesResolved: []netip.Addr{ip}, AddressUsed: ip, URL: "https://exampleA.com", - AddressesTried: []net.IP{ip}, + AddressesTried: []netip.Addr{ip}, ResolverAddrs: []string{"resolver:5353"}, } vrB := core.ValidationRecord{ Hostname: "exampleB.com", Port: "443", - AddressesResolved: []net.IP{ip}, + AddressesResolved: []netip.Addr{ip}, AddressUsed: ip, URL: "https://exampleB.com", - AddressesTried: []net.IP{ip}, + AddressesTried: []netip.Addr{ip}, ResolverAddrs: []string{"resolver:5353"}, } result := []core.ValidationRecord{vrA, vrB} prob := &probs.ProblemDetails{Type: probs.TLSProblem, Detail: "asd", HTTPStatus: 200} - pb, err := ValidationResultToPB(result, prob) + pb, err := ValidationResultToPB(result, prob, "surreal", "ARIN") test.AssertNotError(t, err, "ValidationResultToPB failed") test.Assert(t, pb != nil, "Returned vapb.ValidationResult is nil") + test.AssertEquals(t, pb.Perspective, "surreal") + test.AssertEquals(t, pb.Rir, "ARIN") reconResult, reconProb, err := pbToValidationResult(pb) test.AssertNotError(t, err, "pbToValidationResult failed") @@ -183,7 +183,6 @@ func TestRegistration(t *testing.T) { Key: &key, Contact: &contacts, Agreement: "yup", - InitialIP: net.ParseIP("1.1.1.1"), CreatedAt: &createdAt, Status: core.StatusValid, } @@ -207,14 +206,15 @@ func TestRegistration(t *testing.T) { test.AssertNotError(t, err, "registrationToPB failed") outReg, err = PbToRegistration(pbReg) test.AssertNotError(t, err, "PbToRegistration failed") - test.Assert(t, *outReg.Contact != nil, "Empty slice was converted to a nil slice") + if outReg.Contact != nil { + t.Errorf("Empty contacts should be a nil slice") + } inRegNilCreatedAt := core.Registration{ ID: 1, Key: &key, Contact: &contacts, Agreement: "yup", - InitialIP: net.ParseIP("1.1.1.1"), CreatedAt: nil, Status: core.StatusValid, } @@ -227,22 +227,20 @@ func TestRegistration(t *testing.T) { func TestAuthz(t *testing.T) { exp := time.Now().AddDate(0, 0, 1).UTC() - identifier := identifier.ACMEIdentifier{Type: identifier.DNS, Value: "example.com"} + ident := identifier.NewDNS("example.com") challA := core.Challenge{ - Type: core.ChallengeTypeDNS01, - Status: core.StatusPending, - Token: "asd", - ProvidedKeyAuthorization: "keyauth", + Type: core.ChallengeTypeDNS01, + Status: core.StatusPending, + Token: "asd", } challB := core.Challenge{ - Type: core.ChallengeTypeDNS01, - Status: core.StatusPending, - Token: "asd2", - ProvidedKeyAuthorization: "keyauth4", + Type: core.ChallengeTypeDNS01, + Status: core.StatusPending, + Token: "asd2", } inAuthz := core.Authorization{ ID: "1", - Identifier: identifier, + Identifier: ident, RegistrationID: 5, Status: core.StatusPending, Expires: &exp, @@ -256,7 +254,7 @@ func TestAuthz(t *testing.T) { inAuthzNilExpires := core.Authorization{ ID: "1", - Identifier: identifier, + Identifier: ident, RegistrationID: 5, Status: core.StatusPending, Expires: nil, @@ -269,23 +267,6 @@ func TestAuthz(t *testing.T) { test.AssertDeepEquals(t, inAuthzNilExpires, outAuthz2) } -func TestCert(t *testing.T) { - now := time.Now().Round(0).UTC() - cert := core.Certificate{ - RegistrationID: 1, - Serial: "serial", - Digest: "digest", - DER: []byte{255}, - Issued: now, - Expires: now.Add(time.Hour), - } - - certPB := CertToPB(cert) - outCert := PBToCert(certPB) - - test.AssertDeepEquals(t, cert, outCert) -} - func TestOrderValid(t *testing.T) { created := time.Now() expires := created.Add(1 * time.Hour) @@ -302,7 +283,7 @@ func TestOrderValid(t *testing.T) { Expires: timestamppb.New(expires), CertificateSerial: "", V2Authorizations: []int64{}, - Names: []string{"example.com"}, + Identifiers: []*corepb.Identifier{identifier.NewDNS("example.com").ToProto()}, BeganProcessing: false, Created: timestamppb.New(created), }, @@ -315,7 +296,7 @@ func TestOrderValid(t *testing.T) { RegistrationID: 1, Expires: timestamppb.New(expires), V2Authorizations: []int64{}, - Names: []string{"example.com"}, + Identifiers: []*corepb.Identifier{identifier.NewDNS("example.com").ToProto()}, BeganProcessing: false, Created: timestamppb.New(created), }, @@ -333,7 +314,7 @@ func TestOrderValid(t *testing.T) { Expires: timestamppb.New(expires), CertificateSerial: "", V2Authorizations: []int64{}, - Names: []string{"example.com"}, + Identifiers: []*corepb.Identifier{identifier.NewDNS("example.com").ToProto()}, BeganProcessing: false, }, }, @@ -345,7 +326,7 @@ func TestOrderValid(t *testing.T) { Expires: timestamppb.New(expires), CertificateSerial: "", V2Authorizations: []int64{}, - Names: []string{"example.com"}, + Identifiers: []*corepb.Identifier{identifier.NewDNS("example.com").ToProto()}, BeganProcessing: false, }, }, @@ -357,7 +338,7 @@ func TestOrderValid(t *testing.T) { Expires: nil, CertificateSerial: "", V2Authorizations: []int64{}, - Names: []string{"example.com"}, + Identifiers: []*corepb.Identifier{identifier.NewDNS("example.com").ToProto()}, BeganProcessing: false, }, }, @@ -369,7 +350,7 @@ func TestOrderValid(t *testing.T) { Expires: timestamppb.New(expires), CertificateSerial: "", V2Authorizations: []int64{}, - Names: []string{}, + Identifiers: []*corepb.Identifier{}, BeganProcessing: false, }, }, diff --git a/third-party/github.com/letsencrypt/boulder/grpc/resolver.go b/third-party/github.com/letsencrypt/boulder/grpc/resolver.go index ea26baefe..4fb8df9c6 100644 --- a/third-party/github.com/letsencrypt/boulder/grpc/resolver.go +++ b/third-party/github.com/letsencrypt/boulder/grpc/resolver.go @@ -3,6 +3,7 @@ package grpc import ( "fmt" "net" + "net/netip" "strings" "google.golang.org/grpc/resolver" @@ -91,7 +92,8 @@ func parseResolverIPAddress(addr string) (*resolver.Address, error) { // empty (e.g. :80), the local system is assumed. host = "127.0.0.1" } - if net.ParseIP(host) == nil { + _, err = netip.ParseAddr(host) + if err != nil { // Host is a DNS name or an IPv6 address without brackets. return nil, fmt.Errorf("address %q is not an IP address", addr) } diff --git a/third-party/github.com/letsencrypt/boulder/grpc/server.go b/third-party/github.com/letsencrypt/boulder/grpc/server.go index b3313d46b..2fb09f7f0 100644 --- a/third-party/github.com/letsencrypt/boulder/grpc/server.go +++ b/third-party/github.com/letsencrypt/boulder/grpc/server.go @@ -6,10 +6,11 @@ import ( "errors" "fmt" "net" + "slices" "strings" "time" - grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus" + grpc_prometheus "github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus" "github.com/jmhodges/clock" "github.com/prometheus/client_golang/prometheus" "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" @@ -123,12 +124,21 @@ func (sb *serverBuilder) Build(tlsConfig *tls.Config, statsRegistry prometheus.R // This is the names which are allowlisted at the server level, plus the union // of all names which are allowlisted for any individual service. acceptedSANs := make(map[string]struct{}) + var acceptedSANsSlice []string for _, service := range sb.cfg.Services { for _, name := range service.ClientNames { acceptedSANs[name] = struct{}{} + if !slices.Contains(acceptedSANsSlice, name) { + acceptedSANsSlice = append(acceptedSANsSlice, name) + } } } + // Ensure that the health service has the same ClientNames as the other + // services, so that health checks can be performed by clients which are + // allowed to connect to the server. + sb.cfg.Services[healthpb.Health_ServiceDesc.ServiceName].ClientNames = acceptedSANsSlice + creds, err := bcreds.NewServerCredentials(tlsConfig, acceptedSANs) if err != nil { return nil, err @@ -224,8 +234,12 @@ func (sb *serverBuilder) Build(tlsConfig *tls.Config, statsRegistry prometheus.R // initLongRunningCheck initializes a goroutine which will periodically check // the health of the provided service and update the health server accordingly. +// +// TODO(#8255): Remove the service parameter and instead rely on transitioning +// the overall health of the server (e.g. "") instead of individual services. func (sb *serverBuilder) initLongRunningCheck(shutdownCtx context.Context, service string, checkImpl func(context.Context) error) { // Set the initial health status for the service. + sb.healthSrv.SetServingStatus("", healthpb.HealthCheckResponse_NOT_SERVING) sb.healthSrv.SetServingStatus(service, healthpb.HealthCheckResponse_NOT_SERVING) // check is a helper function that checks the health of the service and, if @@ -249,10 +263,13 @@ func (sb *serverBuilder) initLongRunningCheck(shutdownCtx context.Context, servi } if next != healthpb.HealthCheckResponse_SERVING { + sb.logger.Errf("transitioning overall health from %q to %q, due to: %s", last, next, err) sb.logger.Errf("transitioning health of %q from %q to %q, due to: %s", service, last, next, err) } else { + sb.logger.Infof("transitioning overall health from %q to %q", last, next) sb.logger.Infof("transitioning health of %q from %q to %q", service, last, next) } + sb.healthSrv.SetServingStatus("", next) sb.healthSrv.SetServingStatus(service, next) return next } @@ -291,8 +308,11 @@ type serverMetrics struct { // single registry, it will gracefully avoid registering duplicate metrics. func newServerMetrics(stats prometheus.Registerer) (serverMetrics, error) { // Create the grpc prometheus server metrics instance and register it - grpcMetrics := grpc_prometheus.NewServerMetrics() - grpcMetrics.EnableHandlingTimeHistogram() + grpcMetrics := grpc_prometheus.NewServerMetrics( + grpc_prometheus.WithServerHandlingTimeHistogram( + grpc_prometheus.WithHistogramBuckets([]float64{.01, .025, .05, .1, .5, 1, 2.5, 5, 10, 45, 90}), + ), + ) err := stats.Register(grpcMetrics) if err != nil { are := prometheus.AlreadyRegisteredError{} diff --git a/third-party/github.com/letsencrypt/boulder/grpc/server_test.go b/third-party/github.com/letsencrypt/boulder/grpc/server_test.go index 7553e24c7..16c2e86a4 100644 --- a/third-party/github.com/letsencrypt/boulder/grpc/server_test.go +++ b/third-party/github.com/letsencrypt/boulder/grpc/server_test.go @@ -11,7 +11,7 @@ import ( "google.golang.org/grpc/health" ) -func Test_serverBuilder_initLongRunningCheck(t *testing.T) { +func TestServerBuilderInitLongRunningCheck(t *testing.T) { t.Parallel() hs := health.NewServer() mockLogger := blog.NewMock() @@ -41,8 +41,8 @@ func Test_serverBuilder_initLongRunningCheck(t *testing.T) { // - ~100ms 3rd check failed, SERVING to NOT_SERVING serving := mockLogger.GetAllMatching(".*\"NOT_SERVING\" to \"SERVING\"") notServing := mockLogger.GetAllMatching((".*\"SERVING\" to \"NOT_SERVING\"")) - test.Assert(t, len(serving) == 1, "expected one serving log line") - test.Assert(t, len(notServing) == 1, "expected one not serving log line") + test.Assert(t, len(serving) == 2, "expected two serving log lines") + test.Assert(t, len(notServing) == 2, "expected two not serving log lines") mockLogger.Clear() @@ -67,6 +67,6 @@ func Test_serverBuilder_initLongRunningCheck(t *testing.T) { // - ~100ms 3rd check passed, NOT_SERVING to SERVING serving = mockLogger.GetAllMatching(".*\"NOT_SERVING\" to \"SERVING\"") notServing = mockLogger.GetAllMatching((".*\"SERVING\" to \"NOT_SERVING\"")) - test.Assert(t, len(serving) == 2, "expected two serving log lines") - test.Assert(t, len(notServing) == 1, "expected one not serving log line") + test.Assert(t, len(serving) == 4, "expected four serving log lines") + test.Assert(t, len(notServing) == 2, "expected two not serving log lines") } diff --git a/third-party/github.com/letsencrypt/boulder/grpc/skew.go b/third-party/github.com/letsencrypt/boulder/grpc/skew.go new file mode 100644 index 000000000..653a9ccef --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/grpc/skew.go @@ -0,0 +1,13 @@ +//go:build !integration + +package grpc + +import "time" + +// tooSkewed returns true if the absolute value of the input duration is more +// than ten minutes. We break this out into a separate function so that it can +// be disabled in the integration tests, which make extensive use of fake +// clocks. +func tooSkewed(skew time.Duration) bool { + return skew > 10*time.Minute || skew < -10*time.Minute +} diff --git a/third-party/github.com/letsencrypt/boulder/grpc/skew_integration.go b/third-party/github.com/letsencrypt/boulder/grpc/skew_integration.go new file mode 100644 index 000000000..5bb946be2 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/grpc/skew_integration.go @@ -0,0 +1,12 @@ +//go:build integration + +package grpc + +import "time" + +// tooSkewed always returns false, but is only built when the integration build +// flag is set. We use this to replace the real tooSkewed function in the +// integration tests, which make extensive use of fake clocks. +func tooSkewed(_ time.Duration) bool { + return false +} diff --git a/third-party/github.com/letsencrypt/boulder/grpc/test_proto/interceptors_test.pb.go b/third-party/github.com/letsencrypt/boulder/grpc/test_proto/interceptors_test.pb.go index 09ffb40ad..eb2f680dd 100644 --- a/third-party/github.com/letsencrypt/boulder/grpc/test_proto/interceptors_test.pb.go +++ b/third-party/github.com/letsencrypt/boulder/grpc/test_proto/interceptors_test.pb.go @@ -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: interceptors_test.proto @@ -12,6 +12,7 @@ import ( durationpb "google.golang.org/protobuf/types/known/durationpb" reflect "reflect" sync "sync" + unsafe "unsafe" ) const ( @@ -22,20 +23,17 @@ const ( ) type Time struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Duration *durationpb.Duration `protobuf:"bytes,2,opt,name=duration,proto3" json:"duration,omitempty"` unknownFields protoimpl.UnknownFields - - Duration *durationpb.Duration `protobuf:"bytes,2,opt,name=duration,proto3" json:"duration,omitempty"` + sizeCache protoimpl.SizeCache } func (x *Time) Reset() { *x = Time{} - if protoimpl.UnsafeEnabled { - mi := &file_interceptors_test_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_interceptors_test_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Time) String() string { @@ -46,7 +44,7 @@ func (*Time) ProtoMessage() {} func (x *Time) ProtoReflect() protoreflect.Message { mi := &file_interceptors_test_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 *Time) GetDuration() *durationpb.Duration { var File_interceptors_test_proto protoreflect.FileDescriptor -var file_interceptors_test_proto_rawDesc = []byte{ +var file_interceptors_test_proto_rawDesc = string([]byte{ 0x0a, 0x17, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x6f, 0x72, 0x73, 0x5f, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x75, 0x72, 0x61, 0x74, @@ -85,22 +83,22 @@ var file_interceptors_test_proto_rawDesc = []byte{ 0x2f, 0x6c, 0x65, 0x74, 0x73, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x2f, 0x62, 0x6f, 0x75, 0x6c, 0x64, 0x65, 0x72, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2f, 0x74, 0x65, 0x73, 0x74, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, -} +}) var ( file_interceptors_test_proto_rawDescOnce sync.Once - file_interceptors_test_proto_rawDescData = file_interceptors_test_proto_rawDesc + file_interceptors_test_proto_rawDescData []byte ) func file_interceptors_test_proto_rawDescGZIP() []byte { file_interceptors_test_proto_rawDescOnce.Do(func() { - file_interceptors_test_proto_rawDescData = protoimpl.X.CompressGZIP(file_interceptors_test_proto_rawDescData) + file_interceptors_test_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_interceptors_test_proto_rawDesc), len(file_interceptors_test_proto_rawDesc))) }) return file_interceptors_test_proto_rawDescData } var file_interceptors_test_proto_msgTypes = make([]protoimpl.MessageInfo, 1) -var file_interceptors_test_proto_goTypes = []interface{}{ +var file_interceptors_test_proto_goTypes = []any{ (*Time)(nil), // 0: Time (*durationpb.Duration)(nil), // 1: google.protobuf.Duration } @@ -120,25 +118,11 @@ func file_interceptors_test_proto_init() { if File_interceptors_test_proto != nil { return } - if !protoimpl.UnsafeEnabled { - file_interceptors_test_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Time); 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_interceptors_test_proto_rawDesc, + RawDescriptor: unsafe.Slice(unsafe.StringData(file_interceptors_test_proto_rawDesc), len(file_interceptors_test_proto_rawDesc)), NumEnums: 0, NumMessages: 1, NumExtensions: 0, @@ -149,7 +133,6 @@ func file_interceptors_test_proto_init() { MessageInfos: file_interceptors_test_proto_msgTypes, }.Build() File_interceptors_test_proto = out.File - file_interceptors_test_proto_rawDesc = nil file_interceptors_test_proto_goTypes = nil file_interceptors_test_proto_depIdxs = nil } diff --git a/third-party/github.com/letsencrypt/boulder/grpc/test_proto/interceptors_test_grpc.pb.go b/third-party/github.com/letsencrypt/boulder/grpc/test_proto/interceptors_test_grpc.pb.go index 01d660b64..d44529e5a 100644 --- a/third-party/github.com/letsencrypt/boulder/grpc/test_proto/interceptors_test_grpc.pb.go +++ b/third-party/github.com/letsencrypt/boulder/grpc/test_proto/interceptors_test_grpc.pb.go @@ -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: interceptors_test.proto @@ -15,8 +15,8 @@ import ( // This is a compile-time assertion to ensure that this generated file // is compatible with the grpc package it is being compiled against. -// Requires gRPC-Go v1.62.0 or later. -const _ = grpc.SupportPackageIsVersion8 +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 const ( Chiller_Chill_FullMethodName = "/Chiller/Chill" @@ -50,21 +50,25 @@ func (c *chillerClient) Chill(ctx context.Context, in *Time, opts ...grpc.CallOp // ChillerServer is the server API for Chiller service. // All implementations must embed UnimplementedChillerServer -// for forward compatibility +// for forward compatibility. type ChillerServer interface { // Sleep for the given amount of time, and return the amount of time slept. Chill(context.Context, *Time) (*Time, error) mustEmbedUnimplementedChillerServer() } -// UnimplementedChillerServer must be embedded to have forward compatible implementations. -type UnimplementedChillerServer struct { -} +// UnimplementedChillerServer 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 UnimplementedChillerServer struct{} func (UnimplementedChillerServer) Chill(context.Context, *Time) (*Time, error) { return nil, status.Errorf(codes.Unimplemented, "method Chill not implemented") } func (UnimplementedChillerServer) mustEmbedUnimplementedChillerServer() {} +func (UnimplementedChillerServer) testEmbeddedByValue() {} // UnsafeChillerServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to ChillerServer will @@ -74,6 +78,13 @@ type UnsafeChillerServer interface { } func RegisterChillerServer(s grpc.ServiceRegistrar, srv ChillerServer) { + // If the following call pancis, it indicates UnimplementedChillerServer 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(&Chiller_ServiceDesc, srv) } diff --git a/third-party/github.com/letsencrypt/boulder/iana/data/iana-ipv4-special-registry-1.csv b/third-party/github.com/letsencrypt/boulder/iana/data/iana-ipv4-special-registry-1.csv new file mode 100644 index 000000000..99458ca36 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/iana/data/iana-ipv4-special-registry-1.csv @@ -0,0 +1,27 @@ +Address Block,Name,RFC,Allocation Date,Termination Date,Source,Destination,Forwardable,Globally Reachable,Reserved-by-Protocol +0.0.0.0/8,"""This network""","[RFC791], Section 3.2",1981-09,N/A,True,False,False,False,True +0.0.0.0/32,"""This host on this network""","[RFC1122], Section 3.2.1.3",1981-09,N/A,True,False,False,False,True +10.0.0.0/8,Private-Use,[RFC1918],1996-02,N/A,True,True,True,False,False +100.64.0.0/10,Shared Address Space,[RFC6598],2012-04,N/A,True,True,True,False,False +127.0.0.0/8,Loopback,"[RFC1122], Section 3.2.1.3",1981-09,N/A,False [1],False [1],False [1],False [1],True +169.254.0.0/16,Link Local,[RFC3927],2005-05,N/A,True,True,False,False,True +172.16.0.0/12,Private-Use,[RFC1918],1996-02,N/A,True,True,True,False,False +192.0.0.0/24 [2],IETF Protocol Assignments,"[RFC6890], Section 2.1",2010-01,N/A,False,False,False,False,False +192.0.0.0/29,IPv4 Service Continuity Prefix,[RFC7335],2011-06,N/A,True,True,True,False,False +192.0.0.8/32,IPv4 dummy address,[RFC7600],2015-03,N/A,True,False,False,False,False +192.0.0.9/32,Port Control Protocol Anycast,[RFC7723],2015-10,N/A,True,True,True,True,False +192.0.0.10/32,Traversal Using Relays around NAT Anycast,[RFC8155],2017-02,N/A,True,True,True,True,False +"192.0.0.170/32, 192.0.0.171/32",NAT64/DNS64 Discovery,"[RFC8880][RFC7050], Section 2.2",2013-02,N/A,False,False,False,False,True +192.0.2.0/24,Documentation (TEST-NET-1),[RFC5737],2010-01,N/A,False,False,False,False,False +192.31.196.0/24,AS112-v4,[RFC7535],2014-12,N/A,True,True,True,True,False +192.52.193.0/24,AMT,[RFC7450],2014-12,N/A,True,True,True,True,False +192.88.99.0/24,Deprecated (6to4 Relay Anycast),[RFC7526],2001-06,2015-03,,,,, +192.88.99.2/32,6a44-relay anycast address,[RFC6751],2012-10,N/A,True,True,True,False,False +192.168.0.0/16,Private-Use,[RFC1918],1996-02,N/A,True,True,True,False,False +192.175.48.0/24,Direct Delegation AS112 Service,[RFC7534],1996-01,N/A,True,True,True,True,False +198.18.0.0/15,Benchmarking,[RFC2544],1999-03,N/A,True,True,True,False,False +198.51.100.0/24,Documentation (TEST-NET-2),[RFC5737],2010-01,N/A,False,False,False,False,False +203.0.113.0/24,Documentation (TEST-NET-3),[RFC5737],2010-01,N/A,False,False,False,False,False +240.0.0.0/4,Reserved,"[RFC1112], Section 4",1989-08,N/A,False,False,False,False,True +255.255.255.255/32,Limited Broadcast,"[RFC8190] + [RFC919], Section 7",1984-10,N/A,False,True,False,False,True diff --git a/third-party/github.com/letsencrypt/boulder/iana/data/iana-ipv6-special-registry-1.csv b/third-party/github.com/letsencrypt/boulder/iana/data/iana-ipv6-special-registry-1.csv new file mode 100644 index 000000000..f5bf9c073 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/iana/data/iana-ipv6-special-registry-1.csv @@ -0,0 +1,28 @@ +Address Block,Name,RFC,Allocation Date,Termination Date,Source,Destination,Forwardable,Globally Reachable,Reserved-by-Protocol +::1/128,Loopback Address,[RFC4291],2006-02,N/A,False,False,False,False,True +::/128,Unspecified Address,[RFC4291],2006-02,N/A,True,False,False,False,True +::ffff:0:0/96,IPv4-mapped Address,[RFC4291],2006-02,N/A,False,False,False,False,True +64:ff9b::/96,IPv4-IPv6 Translat.,[RFC6052],2010-10,N/A,True,True,True,True,False +64:ff9b:1::/48,IPv4-IPv6 Translat.,[RFC8215],2017-06,N/A,True,True,True,False,False +100::/64,Discard-Only Address Block,[RFC6666],2012-06,N/A,True,True,True,False,False +100:0:0:1::/64,Dummy IPv6 Prefix,[RFC9780],2025-04,N/A,True,False,False,False,False +2001::/23,IETF Protocol Assignments,[RFC2928],2000-09,N/A,False [1],False [1],False [1],False [1],False +2001::/32,TEREDO,"[RFC4380] + [RFC8190]",2006-01,N/A,True,True,True,N/A [2],False +2001:1::1/128,Port Control Protocol Anycast,[RFC7723],2015-10,N/A,True,True,True,True,False +2001:1::2/128,Traversal Using Relays around NAT Anycast,[RFC8155],2017-02,N/A,True,True,True,True,False +2001:1::3/128,DNS-SD Service Registration Protocol Anycast,[RFC9665],2024-04,N/A,True,True,True,True,False +2001:2::/48,Benchmarking,[RFC5180][RFC Errata 1752],2008-04,N/A,True,True,True,False,False +2001:3::/32,AMT,[RFC7450],2014-12,N/A,True,True,True,True,False +2001:4:112::/48,AS112-v6,[RFC7535],2014-12,N/A,True,True,True,True,False +2001:10::/28,Deprecated (previously ORCHID),[RFC4843],2007-03,2014-03,,,,, +2001:20::/28,ORCHIDv2,[RFC7343],2014-07,N/A,True,True,True,True,False +2001:30::/28,Drone Remote ID Protocol Entity Tags (DETs) Prefix,[RFC9374],2022-12,N/A,True,True,True,True,False +2001:db8::/32,Documentation,[RFC3849],2004-07,N/A,False,False,False,False,False +2002::/16 [3],6to4,[RFC3056],2001-02,N/A,True,True,True,N/A [3],False +2620:4f:8000::/48,Direct Delegation AS112 Service,[RFC7534],2011-05,N/A,True,True,True,True,False +3fff::/20,Documentation,[RFC9637],2024-07,N/A,False,False,False,False,False +5f00::/16,Segment Routing (SRv6) SIDs,[RFC9602],2024-04,N/A,True,True,True,False,False +fc00::/7,Unique-Local,"[RFC4193] + [RFC8190]",2005-10,N/A,True,True,True,False [4],False +fe80::/10,Link-Local Unicast,[RFC4291],2006-02,N/A,True,True,False,False,True diff --git a/third-party/github.com/letsencrypt/boulder/iana/ip.go b/third-party/github.com/letsencrypt/boulder/iana/ip.go new file mode 100644 index 000000000..6f5ed3bb7 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/iana/ip.go @@ -0,0 +1,179 @@ +package iana + +import ( + "bytes" + "encoding/csv" + "errors" + "fmt" + "io" + "net/netip" + "regexp" + "slices" + "strings" + + _ "embed" +) + +type reservedPrefix struct { + // addressFamily is "IPv4" or "IPv6". + addressFamily string + // The other fields are defined in: + // https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml + // https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml + addressBlock netip.Prefix + name string + rfc string + // The BRs' requirement that we not issue for Reserved IP Addresses only + // cares about presence in one of these registries, not any of the other + // metadata fields tracked by the registries. Therefore, we ignore the + // Allocation Date, Termination Date, Source, Destination, Forwardable, + // Globally Reachable, and Reserved By Protocol columns. +} + +var ( + reservedPrefixes []reservedPrefix + + // https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml + //go:embed data/iana-ipv4-special-registry-1.csv + ipv4Registry []byte + // https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml + //go:embed data/iana-ipv6-special-registry-1.csv + ipv6Registry []byte +) + +// init parses and loads the embedded IANA special-purpose address registry CSV +// files for all address families, panicking if any one fails. +func init() { + ipv4Prefixes, err := parseReservedPrefixFile(ipv4Registry, "IPv4") + if err != nil { + panic(err) + } + + ipv6Prefixes, err := parseReservedPrefixFile(ipv6Registry, "IPv6") + if err != nil { + panic(err) + } + + // Add multicast addresses, which aren't in the IANA registries. + // + // TODO(#8237): Move these entries to IP address blocklists once they're + // implemented. + additionalPrefixes := []reservedPrefix{ + { + addressFamily: "IPv4", + addressBlock: netip.MustParsePrefix("224.0.0.0/4"), + name: "Multicast Addresses", + rfc: "[RFC3171]", + }, + { + addressFamily: "IPv6", + addressBlock: netip.MustParsePrefix("ff00::/8"), + name: "Multicast Addresses", + rfc: "[RFC4291]", + }, + } + + reservedPrefixes = slices.Concat(ipv4Prefixes, ipv6Prefixes, additionalPrefixes) + + // Sort the list of reserved prefixes in descending order of prefix size, so + // that checks will match the most-specific reserved prefix first. + slices.SortFunc(reservedPrefixes, func(a, b reservedPrefix) int { + if a.addressBlock.Bits() == b.addressBlock.Bits() { + return 0 + } + if a.addressBlock.Bits() > b.addressBlock.Bits() { + return -1 + } + return 1 + }) +} + +// Define regexps we'll use to clean up poorly formatted registry entries. +var ( + // 2+ sequential whitespace characters. The csv package takes care of + // newlines automatically. + ianaWhitespacesRE = regexp.MustCompile(`\s{2,}`) + // Footnotes at the end, like `[2]`. + ianaFootnotesRE = regexp.MustCompile(`\[\d+\]$`) +) + +// parseReservedPrefixFile parses and returns the IANA special-purpose address +// registry CSV data for a single address family, or returns an error if parsing +// fails. +func parseReservedPrefixFile(registryData []byte, addressFamily string) ([]reservedPrefix, error) { + if addressFamily != "IPv4" && addressFamily != "IPv6" { + return nil, fmt.Errorf("failed to parse reserved address registry: invalid address family %q", addressFamily) + } + if registryData == nil { + return nil, fmt.Errorf("failed to parse reserved %s address registry: empty", addressFamily) + } + + reader := csv.NewReader(bytes.NewReader(registryData)) + + // Parse the header row. + record, err := reader.Read() + if err != nil { + return nil, fmt.Errorf("failed to parse reserved %s address registry header: %w", addressFamily, err) + } + if record[0] != "Address Block" || record[1] != "Name" || record[2] != "RFC" { + return nil, fmt.Errorf("failed to parse reserved %s address registry header: must begin with \"Address Block\", \"Name\" and \"RFC\"", addressFamily) + } + + // Parse the records. + var prefixes []reservedPrefix + for { + row, err := reader.Read() + if errors.Is(err, io.EOF) { + // Finished parsing the file. + if len(prefixes) < 1 { + return nil, fmt.Errorf("failed to parse reserved %s address registry: no rows after header", addressFamily) + } + break + } else if err != nil { + return nil, err + } else if len(row) < 3 { + return nil, fmt.Errorf("failed to parse reserved %s address registry: incomplete row", addressFamily) + } + + // Remove any footnotes, then handle each comma-separated prefix. + for _, prefixStr := range strings.Split(ianaFootnotesRE.ReplaceAllLiteralString(row[0], ""), ",") { + prefix, err := netip.ParsePrefix(strings.TrimSpace(prefixStr)) + if err != nil { + return nil, fmt.Errorf("failed to parse reserved %s address registry: couldn't parse entry %q as an IP address prefix: %s", addressFamily, prefixStr, err) + } + + prefixes = append(prefixes, reservedPrefix{ + addressFamily: addressFamily, + addressBlock: prefix, + name: row[1], + // Replace any whitespace sequences with a single space. + rfc: ianaWhitespacesRE.ReplaceAllLiteralString(row[2], " "), + }) + } + } + + return prefixes, nil +} + +// IsReservedAddr returns an error if an IP address is part of a reserved range. +func IsReservedAddr(ip netip.Addr) error { + for _, rpx := range reservedPrefixes { + if rpx.addressBlock.Contains(ip) { + return fmt.Errorf("IP address is in a reserved address block: %s: %s", rpx.rfc, rpx.name) + } + } + + return nil +} + +// IsReservedPrefix returns an error if an IP address prefix overlaps with a +// reserved range. +func IsReservedPrefix(prefix netip.Prefix) error { + for _, rpx := range reservedPrefixes { + if rpx.addressBlock.Overlaps(prefix) { + return fmt.Errorf("IP address is in a reserved address block: %s: %s", rpx.rfc, rpx.name) + } + } + + return nil +} diff --git a/third-party/github.com/letsencrypt/boulder/iana/ip_test.go b/third-party/github.com/letsencrypt/boulder/iana/ip_test.go new file mode 100644 index 000000000..e4db17b64 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/iana/ip_test.go @@ -0,0 +1,96 @@ +package iana + +import ( + "net/netip" + "strings" + "testing" +) + +func TestIsReservedAddr(t *testing.T) { + t.Parallel() + + cases := []struct { + ip string + want string + }{ + {"127.0.0.1", "Loopback"}, // second-lowest IP in a reserved /8, common mistaken request + {"128.0.0.1", ""}, // second-lowest IP just above a reserved /8 + {"192.168.254.254", "Private-Use"}, // highest IP in a reserved /16 + {"192.169.255.255", ""}, // highest IP in the /16 above a reserved /16 + + {"::", "Unspecified Address"}, // lowest possible IPv6 address, reserved, possible parsing edge case + {"::1", "Loopback Address"}, // reserved, common mistaken request + {"::2", ""}, // surprisingly unreserved + + {"fe80::1", "Link-Local Unicast"}, // second-lowest IP in a reserved /10 + {"febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff", "Link-Local Unicast"}, // highest IP in a reserved /10 + {"fec0::1", ""}, // second-lowest IP just above a reserved /10 + + {"192.0.0.170", "NAT64/DNS64 Discovery"}, // first of two reserved IPs that are comma-split in IANA's CSV; also a more-specific of a larger reserved block that comes first + {"192.0.0.171", "NAT64/DNS64 Discovery"}, // second of two reserved IPs that are comma-split in IANA's CSV; also a more-specific of a larger reserved block that comes first + {"2001:1::1", "Port Control Protocol Anycast"}, // reserved IP that comes after a line with a line break in IANA's CSV; also a more-specific of a larger reserved block that comes first + {"2002::", "6to4"}, // lowest IP in a reserved /16 that has a footnote in IANA's CSV + {"2002:ffff:ffff:ffff:ffff:ffff:ffff:ffff", "6to4"}, // highest IP in a reserved /16 that has a footnote in IANA's CSV + + {"0100::", "Discard-Only Address Block"}, // part of a reserved block in a non-canonical IPv6 format + {"0100::0000:ffff:ffff:ffff:ffff", "Discard-Only Address Block"}, // part of a reserved block in a non-canonical IPv6 format + {"0100::0002:0000:0000:0000:0000", ""}, // non-reserved but in a non-canonical IPv6 format + + // TODO(#8237): Move these entries to IP address blocklists once they're + // implemented. + {"ff00::1", "Multicast Addresses"}, // second-lowest IP in a reserved /8 we hardcode + {"ff10::1", "Multicast Addresses"}, // in the middle of a reserved /8 we hardcode + {"ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", "Multicast Addresses"}, // highest IP in a reserved /8 we hardcode + } + + for _, tc := range cases { + t.Run(tc.ip, func(t *testing.T) { + t.Parallel() + err := IsReservedAddr(netip.MustParseAddr(tc.ip)) + if err == nil && tc.want != "" { + t.Errorf("Got success, wanted error for %#v", tc.ip) + } + if err != nil && !strings.Contains(err.Error(), tc.want) { + t.Errorf("%#v: got %q, want %q", tc.ip, err.Error(), tc.want) + } + }) + } +} + +func TestIsReservedPrefix(t *testing.T) { + t.Parallel() + + cases := []struct { + cidr string + want bool + }{ + {"172.16.0.0/12", true}, + {"172.16.0.0/32", true}, + {"172.16.0.1/32", true}, + {"172.31.255.0/24", true}, + {"172.31.255.255/24", true}, + {"172.31.255.255/32", true}, + {"172.32.0.0/24", false}, + {"172.32.0.1/32", false}, + + {"100::/64", true}, + {"100::/128", true}, + {"100::1/128", true}, + {"100::1:ffff:ffff:ffff:ffff/128", true}, + {"100:0:0:2::/64", false}, + {"100:0:0:2::1/128", false}, + } + + for _, tc := range cases { + t.Run(tc.cidr, func(t *testing.T) { + t.Parallel() + err := IsReservedPrefix(netip.MustParsePrefix(tc.cidr)) + if err != nil && !tc.want { + t.Error(err) + } + if err == nil && tc.want { + t.Errorf("Wanted error for %#v, got success", tc.cidr) + } + }) + } +} diff --git a/third-party/github.com/letsencrypt/boulder/identifier/identifier.go b/third-party/github.com/letsencrypt/boulder/identifier/identifier.go index cbf228f86..9054ea273 100644 --- a/third-party/github.com/letsencrypt/boulder/identifier/identifier.go +++ b/third-party/github.com/letsencrypt/boulder/identifier/identifier.go @@ -1,15 +1,45 @@ // The identifier package defines types for RFC 8555 ACME identifiers. +// +// It exists as a separate package to prevent an import loop between the core +// and probs packages. +// +// Function naming conventions: +// - "New" creates a new instance from one or more simple base type inputs. +// - "From" and "To" extract information from, or compose, a more complex object. package identifier +import ( + "crypto/x509" + "fmt" + "net" + "net/netip" + "slices" + "strings" + + corepb "github.com/letsencrypt/boulder/core/proto" +) + // IdentifierType is a named string type for registered ACME identifier types. // See https://tools.ietf.org/html/rfc8555#section-9.7.7 type IdentifierType string const ( - // DNS is specified in RFC 8555 for DNS type identifiers. - DNS = IdentifierType("dns") + // TypeDNS is specified in RFC 8555 for TypeDNS type identifiers. + TypeDNS = IdentifierType("dns") + // TypeIP is specified in RFC 8738 + TypeIP = IdentifierType("ip") ) +// IsValid tests whether the identifier type is known +func (i IdentifierType) IsValid() bool { + switch i { + case TypeDNS, TypeIP: + return true + default: + return false + } +} + // ACMEIdentifier is a struct encoding an identifier that can be validated. The // protocol allows for different types of identifier to be supported (DNS // names, IP addresses, etc.), but currently we only support RFC 8555 DNS type @@ -22,11 +52,163 @@ type ACMEIdentifier struct { Value string `json:"value"` } -// DNSIdentifier is a convenience function for creating an ACMEIdentifier with -// Type DNS for a given domain name. -func DNSIdentifier(domain string) ACMEIdentifier { +// ACMEIdentifiers is a named type for a slice of ACME identifiers, so that +// methods can be applied to these slices. +type ACMEIdentifiers []ACMEIdentifier + +func (i ACMEIdentifier) ToProto() *corepb.Identifier { + return &corepb.Identifier{ + Type: string(i.Type), + Value: i.Value, + } +} + +func FromProto(ident *corepb.Identifier) ACMEIdentifier { return ACMEIdentifier{ - Type: DNS, + Type: IdentifierType(ident.Type), + Value: ident.Value, + } +} + +// ToProtoSlice is a convenience function for converting a slice of +// ACMEIdentifier into a slice of *corepb.Identifier, to use for RPCs. +func (idents ACMEIdentifiers) ToProtoSlice() []*corepb.Identifier { + var pbIdents []*corepb.Identifier + for _, ident := range idents { + pbIdents = append(pbIdents, ident.ToProto()) + } + return pbIdents +} + +// FromProtoSlice is a convenience function for converting a slice of +// *corepb.Identifier from RPCs into a slice of ACMEIdentifier. +func FromProtoSlice(pbIdents []*corepb.Identifier) ACMEIdentifiers { + var idents ACMEIdentifiers + + for _, pbIdent := range pbIdents { + idents = append(idents, FromProto(pbIdent)) + } + return idents +} + +// NewDNS is a convenience function for creating an ACMEIdentifier with Type +// "dns" for a given domain name. +func NewDNS(domain string) ACMEIdentifier { + return ACMEIdentifier{ + Type: TypeDNS, Value: domain, } } + +// NewDNSSlice is a convenience function for creating a slice of ACMEIdentifier +// with Type "dns" for a given slice of domain names. +func NewDNSSlice(input []string) ACMEIdentifiers { + var out ACMEIdentifiers + for _, in := range input { + out = append(out, NewDNS(in)) + } + return out +} + +// NewIP is a convenience function for creating an ACMEIdentifier with Type "ip" +// for a given IP address. +func NewIP(ip netip.Addr) ACMEIdentifier { + return ACMEIdentifier{ + Type: TypeIP, + // RFC 8738, Sec. 3: The identifier value MUST contain the textual form + // of the address as defined in RFC 1123, Sec. 2.1 for IPv4 and in RFC + // 5952, Sec. 4 for IPv6. + Value: ip.String(), + } +} + +// fromX509 extracts the Subject Alternative Names from a certificate or CSR's fields, and +// returns a slice of ACMEIdentifiers. +func fromX509(commonName string, dnsNames []string, ipAddresses []net.IP) ACMEIdentifiers { + var sans ACMEIdentifiers + for _, name := range dnsNames { + sans = append(sans, NewDNS(name)) + } + if commonName != "" { + // Boulder won't generate certificates with a CN that's not also present + // in the SANs, but such a certificate is possible. If appended, this is + // deduplicated later with Normalize(). We assume the CN is a DNSName, + // because CNs are untyped strings without metadata, and we will never + // configure a Boulder profile to issue a certificate that contains both + // an IP address identifier and a CN. + sans = append(sans, NewDNS(commonName)) + } + + for _, ip := range ipAddresses { + sans = append(sans, ACMEIdentifier{ + Type: TypeIP, + Value: ip.String(), + }) + } + + return Normalize(sans) +} + +// FromCert extracts the Subject Common Name and Subject Alternative Names from +// a certificate, and returns a slice of ACMEIdentifiers. +func FromCert(cert *x509.Certificate) ACMEIdentifiers { + return fromX509(cert.Subject.CommonName, cert.DNSNames, cert.IPAddresses) +} + +// FromCSR extracts the Subject Common Name and Subject Alternative Names from a +// CSR, and returns a slice of ACMEIdentifiers. +func FromCSR(csr *x509.CertificateRequest) ACMEIdentifiers { + return fromX509(csr.Subject.CommonName, csr.DNSNames, csr.IPAddresses) +} + +// Normalize returns the set of all unique ACME identifiers in the input after +// all of them are lowercased. The returned identifier values will be in their +// lowercased form and sorted alphabetically by value. DNS identifiers will +// precede IP address identifiers. +func Normalize(idents ACMEIdentifiers) ACMEIdentifiers { + for i := range idents { + idents[i].Value = strings.ToLower(idents[i].Value) + } + + slices.SortFunc(idents, func(a, b ACMEIdentifier) int { + if a.Type == b.Type { + if a.Value == b.Value { + return 0 + } + if a.Value < b.Value { + return -1 + } + return 1 + } + if a.Type == "dns" && b.Type == "ip" { + return -1 + } + return 1 + }) + + return slices.Compact(idents) +} + +// ToValues returns a slice of DNS names and a slice of IP addresses in the +// input. If an identifier type or IP address is invalid, it returns an error. +func (idents ACMEIdentifiers) ToValues() ([]string, []net.IP, error) { + var dnsNames []string + var ipAddresses []net.IP + + for _, ident := range idents { + switch ident.Type { + case TypeDNS: + dnsNames = append(dnsNames, ident.Value) + case TypeIP: + ip := net.ParseIP(ident.Value) + if ip == nil { + return nil, nil, fmt.Errorf("parsing IP address: %s", ident.Value) + } + ipAddresses = append(ipAddresses, ip) + default: + return nil, nil, fmt.Errorf("evaluating identifier type: %s for %s", ident.Type, ident.Value) + } + } + + return dnsNames, ipAddresses, nil +} diff --git a/third-party/github.com/letsencrypt/boulder/identifier/identifier_test.go b/third-party/github.com/letsencrypt/boulder/identifier/identifier_test.go new file mode 100644 index 000000000..7cfb4f371 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/identifier/identifier_test.go @@ -0,0 +1,230 @@ +package identifier + +import ( + "crypto/x509" + "crypto/x509/pkix" + "net" + "net/netip" + "reflect" + "slices" + "testing" +) + +// TestFromX509 tests FromCert and FromCSR, which are fromX509's public +// wrappers. +func TestFromX509(t *testing.T) { + cases := []struct { + name string + subject pkix.Name + dnsNames []string + ipAddresses []net.IP + want ACMEIdentifiers + }{ + { + name: "no explicit CN", + dnsNames: []string{"a.com"}, + want: ACMEIdentifiers{NewDNS("a.com")}, + }, + { + name: "explicit uppercase CN", + subject: pkix.Name{CommonName: "A.com"}, + dnsNames: []string{"a.com"}, + want: ACMEIdentifiers{NewDNS("a.com")}, + }, + { + name: "no explicit CN, uppercase SAN", + dnsNames: []string{"A.com"}, + want: ACMEIdentifiers{NewDNS("a.com")}, + }, + { + name: "duplicate SANs", + dnsNames: []string{"b.com", "b.com", "a.com", "a.com"}, + want: ACMEIdentifiers{NewDNS("a.com"), NewDNS("b.com")}, + }, + { + name: "explicit CN not found in SANs", + subject: pkix.Name{CommonName: "a.com"}, + dnsNames: []string{"b.com"}, + want: ACMEIdentifiers{NewDNS("a.com"), NewDNS("b.com")}, + }, + { + name: "mix of DNSNames and IPAddresses", + dnsNames: []string{"a.com"}, + ipAddresses: []net.IP{{192, 168, 1, 1}}, + want: ACMEIdentifiers{NewDNS("a.com"), NewIP(netip.MustParseAddr("192.168.1.1"))}, + }, + } + for _, tc := range cases { + t.Run("cert/"+tc.name, func(t *testing.T) { + t.Parallel() + got := FromCert(&x509.Certificate{Subject: tc.subject, DNSNames: tc.dnsNames, IPAddresses: tc.ipAddresses}) + if !slices.Equal(got, tc.want) { + t.Errorf("FromCert() got %#v, but want %#v", got, tc.want) + } + }) + t.Run("csr/"+tc.name, func(t *testing.T) { + t.Parallel() + got := FromCSR(&x509.CertificateRequest{Subject: tc.subject, DNSNames: tc.dnsNames, IPAddresses: tc.ipAddresses}) + if !slices.Equal(got, tc.want) { + t.Errorf("FromCSR() got %#v, but want %#v", got, tc.want) + } + }) + } +} + +func TestNormalize(t *testing.T) { + cases := []struct { + name string + idents ACMEIdentifiers + want ACMEIdentifiers + }{ + { + name: "convert to lowercase", + idents: ACMEIdentifiers{ + {Type: TypeDNS, Value: "AlPha.example.coM"}, + {Type: TypeIP, Value: "fe80::CAFE"}, + }, + want: ACMEIdentifiers{ + {Type: TypeDNS, Value: "alpha.example.com"}, + {Type: TypeIP, Value: "fe80::cafe"}, + }, + }, + { + name: "sort", + idents: ACMEIdentifiers{ + {Type: TypeDNS, Value: "foobar.com"}, + {Type: TypeDNS, Value: "bar.com"}, + {Type: TypeDNS, Value: "baz.com"}, + {Type: TypeDNS, Value: "a.com"}, + {Type: TypeIP, Value: "fe80::cafe"}, + {Type: TypeIP, Value: "2001:db8::1dea"}, + {Type: TypeIP, Value: "192.168.1.1"}, + }, + want: ACMEIdentifiers{ + {Type: TypeDNS, Value: "a.com"}, + {Type: TypeDNS, Value: "bar.com"}, + {Type: TypeDNS, Value: "baz.com"}, + {Type: TypeDNS, Value: "foobar.com"}, + {Type: TypeIP, Value: "192.168.1.1"}, + {Type: TypeIP, Value: "2001:db8::1dea"}, + {Type: TypeIP, Value: "fe80::cafe"}, + }, + }, + { + name: "de-duplicate", + idents: ACMEIdentifiers{ + {Type: TypeDNS, Value: "AlPha.example.coM"}, + {Type: TypeIP, Value: "fe80::CAFE"}, + {Type: TypeDNS, Value: "alpha.example.com"}, + {Type: TypeIP, Value: "fe80::cafe"}, + NewIP(netip.MustParseAddr("fe80:0000:0000:0000:0000:0000:0000:cafe")), + }, + want: ACMEIdentifiers{ + {Type: TypeDNS, Value: "alpha.example.com"}, + {Type: TypeIP, Value: "fe80::cafe"}, + }, + }, + { + name: "DNS before IP", + idents: ACMEIdentifiers{ + {Type: TypeIP, Value: "fe80::cafe"}, + {Type: TypeDNS, Value: "alpha.example.com"}, + }, + want: ACMEIdentifiers{ + {Type: TypeDNS, Value: "alpha.example.com"}, + {Type: TypeIP, Value: "fe80::cafe"}, + }, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + got := Normalize(tc.idents) + if !slices.Equal(got, tc.want) { + t.Errorf("Got %#v, but want %#v", got, tc.want) + } + }) + } +} + +func TestToValues(t *testing.T) { + cases := []struct { + name string + idents ACMEIdentifiers + wantErr string + wantDnsNames []string + wantIpAddresses []net.IP + }{ + { + name: "DNS names and IP addresses", + // These are deliberately out of alphabetical and type order, to + // ensure ToValues doesn't do normalization, which ought to be done + // explicitly. + idents: ACMEIdentifiers{ + {Type: TypeDNS, Value: "beta.example.com"}, + {Type: TypeIP, Value: "fe80::cafe"}, + {Type: TypeDNS, Value: "alpha.example.com"}, + {Type: TypeIP, Value: "127.0.0.1"}, + }, + wantErr: "", + wantDnsNames: []string{"beta.example.com", "alpha.example.com"}, + wantIpAddresses: []net.IP{net.ParseIP("fe80::cafe"), net.ParseIP("127.0.0.1")}, + }, + { + name: "DNS names only", + idents: ACMEIdentifiers{ + {Type: TypeDNS, Value: "alpha.example.com"}, + {Type: TypeDNS, Value: "beta.example.com"}, + }, + wantErr: "", + wantDnsNames: []string{"alpha.example.com", "beta.example.com"}, + wantIpAddresses: nil, + }, + { + name: "IP addresses only", + idents: ACMEIdentifiers{ + {Type: TypeIP, Value: "127.0.0.1"}, + {Type: TypeIP, Value: "fe80::cafe"}, + }, + wantErr: "", + wantDnsNames: nil, + wantIpAddresses: []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("fe80::cafe")}, + }, + { + name: "invalid IP address", + idents: ACMEIdentifiers{ + {Type: TypeIP, Value: "fe80::c0ffee"}, + }, + wantErr: "parsing IP address: fe80::c0ffee", + wantDnsNames: nil, + wantIpAddresses: nil, + }, + { + name: "invalid identifier type", + idents: ACMEIdentifiers{ + {Type: "fnord", Value: "panic.example.com"}, + }, + wantErr: "evaluating identifier type: fnord for panic.example.com", + wantDnsNames: nil, + wantIpAddresses: nil, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + gotDnsNames, gotIpAddresses, gotErr := tc.idents.ToValues() + if !slices.Equal(gotDnsNames, tc.wantDnsNames) { + t.Errorf("Got DNS names %#v, but want %#v", gotDnsNames, tc.wantDnsNames) + } + if !reflect.DeepEqual(gotIpAddresses, tc.wantIpAddresses) { + t.Errorf("Got IP addresses %#v, but want %#v", gotIpAddresses, tc.wantIpAddresses) + } + if tc.wantErr != "" && (gotErr.Error() != tc.wantErr) { + t.Errorf("Got error %#v, but want %#v", gotErr.Error(), tc.wantErr) + } + if tc.wantErr == "" && gotErr != nil { + t.Errorf("Got error %#v, but didn't want one", gotErr.Error()) + } + }) + } +} diff --git a/third-party/github.com/letsencrypt/boulder/issuance/cert.go b/third-party/github.com/letsencrypt/boulder/issuance/cert.go index 6b8734b7c..fdcf5d6af 100644 --- a/third-party/github.com/letsencrypt/boulder/issuance/cert.go +++ b/third-party/github.com/letsencrypt/boulder/issuance/cert.go @@ -9,9 +9,11 @@ import ( "crypto/x509" "crypto/x509/pkix" "encoding/asn1" + "encoding/json" "errors" "fmt" "math/big" + "net" "sync" "time" @@ -21,22 +23,53 @@ import ( "github.com/jmhodges/clock" "github.com/zmap/zlint/v3/lint" + "github.com/letsencrypt/boulder/cmd" "github.com/letsencrypt/boulder/config" + "github.com/letsencrypt/boulder/linter" "github.com/letsencrypt/boulder/precert" ) // ProfileConfig describes the certificate issuance constraints for all issuers. type ProfileConfig struct { + // AllowMustStaple, when false, causes all IssuanceRequests which specify the + // OCSP Must Staple extension to be rejected. + // + // Deprecated: This has no effect, Must Staple is always omitted. + // TODO(#8177): Remove this. AllowMustStaple bool - AllowCTPoison bool - AllowSCTList bool - AllowCommonName bool + + // OmitCommonName causes the CN field to be excluded from the resulting + // certificate, regardless of its inclusion in the IssuanceRequest. + OmitCommonName bool + // OmitKeyEncipherment causes the keyEncipherment bit to be omitted from the + // Key Usage field of all certificates (instead of only from ECDSA certs). + OmitKeyEncipherment bool + // OmitClientAuth causes the id-kp-clientAuth OID (TLS Client Authentication) + // to be omitted from the EKU extension. + OmitClientAuth bool + // OmitSKID causes the Subject Key Identifier extension to be omitted. + OmitSKID bool + // OmitOCSP causes the OCSP URI field to be omitted from the Authority + // Information Access extension. This cannot be true unless + // IncludeCRLDistributionPoints is also true, to ensure that every + // certificate has at least one revocation mechanism included. + // + // Deprecated: This has no effect; OCSP is always omitted. + // TODO(#8177): Remove this. + OmitOCSP bool + // IncludeCRLDistributionPoints causes the CRLDistributionPoints extension to + // be added to all certificates issued by this profile. + IncludeCRLDistributionPoints bool MaxValidityPeriod config.Duration MaxValidityBackdate config.Duration - // Deprecated: we do not respect this field. - Policies []PolicyConfig `validate:"-"` + // 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 lint names that we know will fail for this + // profile, and which we know it is safe to ignore. + IgnoredLints []string } // PolicyConfig describes a policy @@ -46,10 +79,12 @@ type PolicyConfig struct { // Profile is the validated structure created by reading in ProfileConfigs and IssuerConfigs type Profile struct { - allowMustStaple bool - allowCTPoison bool - allowSCTList bool - allowCommonName bool + omitCommonName bool + omitKeyEncipherment bool + omitClientAuth bool + omitSKID bool + + includeCRLDistributionPoints bool maxBackdate time.Duration maxValidity time.Duration @@ -57,25 +92,67 @@ type Profile struct { lints lint.Registry } -// NewProfile converts the profile config and lint registry into a usable profile. -func NewProfile(profileConfig ProfileConfig, lints lint.Registry) (*Profile, error) { +// NewProfile converts the profile config into a usable profile. +func NewProfile(profileConfig *ProfileConfig) (*Profile, error) { + // The Baseline Requirements, Section 7.1.2.7, says that the notBefore time + // must be "within 48 hours of the time of signing". We can be even stricter. + if profileConfig.MaxValidityBackdate.Duration >= 24*time.Hour { + return nil, fmt.Errorf("backdate %q is too large", profileConfig.MaxValidityBackdate.Duration) + } + + // Our CP/CPS, Section 7.1, says that our Subscriber Certificates have a + // validity period of "up to 100 days". + if profileConfig.MaxValidityPeriod.Duration >= 100*24*time.Hour { + return nil, fmt.Errorf("validity period %q is too large", profileConfig.MaxValidityPeriod.Duration) + } + + // Although the Baseline Requirements say that revocation information may be + // omitted entirely *for short-lived certs*, the Microsoft root program still + // requires that at least one revocation mechanism be included in all certs. + // TODO(#7673): Remove this restriction. + if !profileConfig.IncludeCRLDistributionPoints { + return nil, fmt.Errorf("at least one revocation mechanism must be included") + } + + lints, err := linter.NewRegistry(profileConfig.IgnoredLints) + cmd.FailOnError(err, "Failed to create zlint registry") + if profileConfig.LintConfig != "" { + lintconfig, err := lint.NewConfigFromFile(profileConfig.LintConfig) + cmd.FailOnError(err, "Failed to load zlint config file") + lints.SetConfiguration(lintconfig) + } + sp := &Profile{ - allowMustStaple: profileConfig.AllowMustStaple, - allowCTPoison: profileConfig.AllowCTPoison, - allowSCTList: profileConfig.AllowSCTList, - allowCommonName: profileConfig.AllowCommonName, - maxBackdate: profileConfig.MaxValidityBackdate.Duration, - maxValidity: profileConfig.MaxValidityPeriod.Duration, - lints: lints, + omitCommonName: profileConfig.OmitCommonName, + omitKeyEncipherment: profileConfig.OmitKeyEncipherment, + omitClientAuth: profileConfig.OmitClientAuth, + omitSKID: profileConfig.OmitSKID, + includeCRLDistributionPoints: profileConfig.IncludeCRLDistributionPoints, + maxBackdate: profileConfig.MaxValidityBackdate.Duration, + maxValidity: profileConfig.MaxValidityPeriod.Duration, + lints: lints, } return sp, nil } +// GenerateValidity returns a notBefore/notAfter pair bracketing the input time, +// based on the profile's configured backdate and validity. +func (p *Profile) GenerateValidity(now time.Time) (time.Time, time.Time) { + // Don't use the full maxBackdate, to ensure that the actual backdate remains + // acceptable throughout the rest of the issuance process. + backdate := time.Duration(float64(p.maxBackdate.Nanoseconds()) * 0.9) + notBefore := now.Add(-1 * backdate) + // Subtract one second, because certificate validity periods are *inclusive* + // of their final second (Baseline Requirements, Section 1.6.1). + notAfter := notBefore.Add(p.maxValidity).Add(-1 * time.Second) + return notBefore, notAfter +} + // requestValid verifies the passed IssuanceRequest against the profile. If the // request doesn't match the signing profile an error is returned. func (i *Issuer) requestValid(clk clock.Clock, prof *Profile, req *IssuanceRequest) error { - switch req.PublicKey.(type) { + switch req.PublicKey.PublicKey.(type) { case *rsa.PublicKey, *ecdsa.PublicKey: default: return errors.New("unsupported public key type") @@ -85,30 +162,14 @@ func (i *Issuer) requestValid(clk clock.Clock, prof *Profile, req *IssuanceReque return errors.New("inactive issuer cannot issue precert") } - if len(req.SubjectKeyId) != 20 { + if len(req.SubjectKeyId) != 0 && len(req.SubjectKeyId) != 20 { return errors.New("unexpected subject key ID length") } - if !prof.allowMustStaple && req.IncludeMustStaple { - return errors.New("must-staple extension cannot be included") - } - - if !prof.allowCTPoison && req.IncludeCTPoison { - return errors.New("ct poison extension cannot be included") - } - - if !prof.allowSCTList && req.sctList != nil { - return errors.New("sct list extension cannot be included") - } - if req.IncludeCTPoison && req.sctList != nil { return errors.New("cannot include both ct poison and sct list extensions") } - if !prof.allowCommonName && req.CommonName != "" { - return errors.New("common name cannot be included") - } - // The validity period is calculated inclusive of the whole second represented // by the notAfter timestamp. validity := req.NotAfter.Add(time.Second).Sub(req.NotBefore) @@ -136,23 +197,25 @@ func (i *Issuer) requestValid(clk clock.Clock, prof *Profile, req *IssuanceReque return nil } +// Baseline Requirements, Section 7.1.6.1: domain-validated +var domainValidatedOID = func() x509.OID { + x509OID, err := x509.OIDFromInts([]uint64{2, 23, 140, 1, 2, 1}) + if err != nil { + // This should never happen, as the OID is hardcoded. + panic(fmt.Errorf("failed to create OID using ints %v: %s", x509OID, err)) + } + return x509OID +}() + func (i *Issuer) generateTemplate() *x509.Certificate { template := &x509.Certificate{ - SignatureAlgorithm: i.sigAlg, - ExtKeyUsage: []x509.ExtKeyUsage{ - x509.ExtKeyUsageServerAuth, - x509.ExtKeyUsageClientAuth, - }, - OCSPServer: []string{i.ocspURL}, + SignatureAlgorithm: i.sigAlg, IssuingCertificateURL: []string{i.issuerURL}, BasicConstraintsValid: true, // Baseline Requirements, Section 7.1.6.1: domain-validated - PolicyIdentifiers: []asn1.ObjectIdentifier{{2, 23, 140, 1, 2, 1}}, + Policies: []x509.OID{domainValidatedOID}, } - // TODO(#7294): Use i.crlURLBase and a shard calculation to create a - // crlDistributionPoint. - return template } @@ -189,31 +252,45 @@ func generateSCTListExt(scts []ct.SignedCertificateTimestamp) (pkix.Extension, e }, nil } -var mustStapleExt = pkix.Extension{ - // RFC 7633: id-pe-tlsfeature OBJECT IDENTIFIER ::= { id-pe 24 } - Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 24}, - // ASN.1 encoding of: - // SEQUENCE - // INTEGER 5 - // where "5" is the status_request feature (RFC 6066) - Value: []byte{0x30, 0x03, 0x02, 0x01, 0x05}, +// MarshalablePublicKey is a wrapper for crypto.PublicKey with a custom JSON +// marshaller that encodes the public key as a DER-encoded SubjectPublicKeyInfo. +type MarshalablePublicKey struct { + crypto.PublicKey +} + +func (pk MarshalablePublicKey) MarshalJSON() ([]byte, error) { + keyDER, err := x509.MarshalPKIXPublicKey(pk.PublicKey) + if err != nil { + return nil, err + } + return json.Marshal(keyDER) +} + +type HexMarshalableBytes []byte + +func (h HexMarshalableBytes) MarshalJSON() ([]byte, error) { + return json.Marshal(fmt.Sprintf("%x", h)) } // IssuanceRequest describes a certificate issuance request +// +// It can be marshaled as JSON for logging purposes, though note that sctList and precertDER +// will be omitted from the marshaled output because they are unexported. type IssuanceRequest struct { - PublicKey crypto.PublicKey - SubjectKeyId []byte + // PublicKey is of type MarshalablePublicKey so we can log an IssuanceRequest as a JSON object. + PublicKey MarshalablePublicKey + SubjectKeyId HexMarshalableBytes - Serial []byte + Serial HexMarshalableBytes NotBefore time.Time NotAfter time.Time - CommonName string - DNSNames []string + CommonName string + DNSNames []string + IPAddresses []net.IP - IncludeMustStaple bool - IncludeCTPoison bool + IncludeCTPoison bool // sctList is a list of SCTs to include in a final certificate. // If it is non-empty, PrecertDER must also be non-empty. @@ -232,7 +309,7 @@ type IssuanceRequest struct { type issuanceToken struct { mu sync.Mutex template *x509.Certificate - pubKey any + pubKey MarshalablePublicKey // A pointer to the issuer that created this token. This token may only // be redeemed by the same issuer. issuer *Issuer @@ -254,22 +331,40 @@ func (i *Issuer) Prepare(prof *Profile, req *IssuanceRequest) ([]byte, *issuance // generate template from the issuer's data template := i.generateTemplate() + ekus := []x509.ExtKeyUsage{ + x509.ExtKeyUsageServerAuth, + x509.ExtKeyUsageClientAuth, + } + if prof.omitClientAuth { + ekus = []x509.ExtKeyUsage{ + x509.ExtKeyUsageServerAuth, + } + } + template.ExtKeyUsage = ekus + // populate template from the issuance request template.NotBefore, template.NotAfter = req.NotBefore, req.NotAfter template.SerialNumber = big.NewInt(0).SetBytes(req.Serial) - if req.CommonName != "" { + if req.CommonName != "" && !prof.omitCommonName { template.Subject.CommonName = req.CommonName } template.DNSNames = req.DNSNames + template.IPAddresses = req.IPAddresses - switch req.PublicKey.(type) { + switch req.PublicKey.PublicKey.(type) { case *rsa.PublicKey: - template.KeyUsage = x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment + if prof.omitKeyEncipherment { + template.KeyUsage = x509.KeyUsageDigitalSignature + } else { + template.KeyUsage = x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment + } case *ecdsa.PublicKey: template.KeyUsage = x509.KeyUsageDigitalSignature } - template.SubjectKeyId = req.SubjectKeyId + if !prof.omitSKID { + template.SubjectKeyId = req.SubjectKeyId + } if req.IncludeCTPoison { template.ExtraExtensions = append(template.ExtraExtensions, ctPoisonExt) @@ -286,13 +381,22 @@ func (i *Issuer) Prepare(prof *Profile, req *IssuanceRequest) ([]byte, *issuance return nil, nil, errors.New("invalid request contains neither sctList nor precertDER") } - if req.IncludeMustStaple { - template.ExtraExtensions = append(template.ExtraExtensions, mustStapleExt) + // If explicit CRL sharding is enabled, pick a shard based on the serial number + // modulus the number of shards. This gives us random distribution that is + // nonetheless consistent between precert and cert. + if prof.includeCRLDistributionPoints { + if i.crlShards <= 0 { + return nil, nil, errors.New("IncludeCRLDistributionPoints was set but CRLShards was not set") + } + shardZeroBased := big.NewInt(0).Mod(template.SerialNumber, big.NewInt(int64(i.crlShards))) + shard := int(shardZeroBased.Int64()) + 1 + url := i.crlURL(shard) + template.CRLDistributionPoints = []string{url} } // check that the tbsCertificate is properly formed by signing it // with a throwaway key and then linting it using zlint - lintCertBytes, err := i.Linter.Check(template, req.PublicKey, prof.lints) + lintCertBytes, err := i.Linter.Check(template, req.PublicKey.PublicKey, prof.lints) if err != nil { return nil, nil, fmt.Errorf("tbsCertificate linting failed: %w", err) } @@ -327,19 +431,7 @@ func (i *Issuer) Issue(token *issuanceToken) ([]byte, error) { return nil, errors.New("tried to redeem issuance token with the wrong issuer") } - return x509.CreateCertificate(rand.Reader, template, i.Cert.Certificate, token.pubKey, i.Signer) -} - -// ContainsMustStaple returns true if the provided set of extensions includes -// an entry whose OID and value both match the expected values for the OCSP -// Must-Staple (a.k.a. id-pe-tlsFeature) extension. -func ContainsMustStaple(extensions []pkix.Extension) bool { - for _, ext := range extensions { - if ext.Id.Equal(mustStapleExt.Id) && bytes.Equal(ext.Value, mustStapleExt.Value) { - return true - } - } - return false + return x509.CreateCertificate(rand.Reader, template, i.Cert.Certificate, token.pubKey.PublicKey, i.Signer) } // containsCTPoison returns true if the provided set of extensions includes @@ -362,15 +454,15 @@ func RequestFromPrecert(precert *x509.Certificate, scts []ct.SignedCertificateTi return nil, errors.New("provided certificate doesn't contain the CT poison extension") } return &IssuanceRequest{ - PublicKey: precert.PublicKey, - SubjectKeyId: precert.SubjectKeyId, - Serial: precert.SerialNumber.Bytes(), - NotBefore: precert.NotBefore, - NotAfter: precert.NotAfter, - CommonName: precert.Subject.CommonName, - DNSNames: precert.DNSNames, - IncludeMustStaple: ContainsMustStaple(precert.Extensions), - sctList: scts, - precertDER: precert.Raw, + PublicKey: MarshalablePublicKey{precert.PublicKey}, + SubjectKeyId: precert.SubjectKeyId, + Serial: precert.SerialNumber.Bytes(), + NotBefore: precert.NotBefore, + NotAfter: precert.NotAfter, + CommonName: precert.Subject.CommonName, + DNSNames: precert.DNSNames, + IPAddresses: precert.IPAddresses, + sctList: scts, + precertDER: precert.Raw, }, nil } diff --git a/third-party/github.com/letsencrypt/boulder/issuance/cert_test.go b/third-party/github.com/letsencrypt/boulder/issuance/cert_test.go index 87704745d..db3cb63cb 100644 --- a/third-party/github.com/letsencrypt/boulder/issuance/cert_test.go +++ b/third-party/github.com/letsencrypt/boulder/issuance/cert_test.go @@ -9,14 +9,17 @@ import ( "crypto/rsa" "crypto/x509" "crypto/x509/pkix" - "encoding/asn1" "encoding/base64" + "net" + "reflect" + "strings" "testing" "time" ct "github.com/google/certificate-transparency-go" "github.com/jmhodges/clock" + "github.com/letsencrypt/boulder/config" "github.com/letsencrypt/boulder/ctpolicy/loglist" "github.com/letsencrypt/boulder/linter" "github.com/letsencrypt/boulder/test" @@ -27,14 +30,59 @@ var ( ) func defaultProfile() *Profile { - lints, _ := linter.NewRegistry([]string{ - "w_ct_sct_policy_count_unsatisfied", - "e_scts_from_same_operator", - }) - p, _ := NewProfile(defaultProfileConfig(), lints) + p, _ := NewProfile(defaultProfileConfig()) return p } +func TestGenerateValidity(t *testing.T) { + fc := clock.NewFake() + fc.Set(time.Date(2015, time.June, 04, 11, 04, 38, 0, time.UTC)) + + tests := []struct { + name string + backdate time.Duration + validity time.Duration + notBefore time.Time + notAfter time.Time + }{ + { + name: "normal usage", + backdate: time.Hour, // 90% of one hour is 54 minutes + validity: 7 * 24 * time.Hour, + notBefore: time.Date(2015, time.June, 04, 10, 10, 38, 0, time.UTC), + notAfter: time.Date(2015, time.June, 11, 10, 10, 37, 0, time.UTC), + }, + { + name: "zero backdate", + backdate: 0, + validity: 7 * 24 * time.Hour, + notBefore: time.Date(2015, time.June, 04, 11, 04, 38, 0, time.UTC), + notAfter: time.Date(2015, time.June, 11, 11, 04, 37, 0, time.UTC), + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + p := Profile{maxBackdate: tc.backdate, maxValidity: tc.validity} + notBefore, notAfter := p.GenerateValidity(fc.Now()) + test.AssertEquals(t, notBefore, tc.notBefore) + test.AssertEquals(t, notAfter, tc.notAfter) + }) + } +} + +func TestCRLURL(t *testing.T) { + issuer, err := newIssuer(defaultIssuerConfig(), issuerCert, issuerSigner, clock.NewFake()) + if err != nil { + t.Fatalf("newIssuer: %s", err) + } + url := issuer.crlURL(4928) + want := "http://crl-url.example.org/4928.crl" + if url != want { + t.Errorf("crlURL(4928)=%s, want %s", url, want) + } +} + func TestRequestValid(t *testing.T) { fc := clock.NewFake() fc.Add(time.Hour * 24) @@ -50,21 +98,21 @@ func TestRequestValid(t *testing.T) { name: "unsupported key type", issuer: &Issuer{}, profile: &Profile{}, - request: &IssuanceRequest{PublicKey: &dsa.PublicKey{}}, + request: &IssuanceRequest{PublicKey: MarshalablePublicKey{&dsa.PublicKey{}}}, expectedError: "unsupported public key type", }, { name: "inactive (rsa)", issuer: &Issuer{}, profile: &Profile{}, - request: &IssuanceRequest{PublicKey: &rsa.PublicKey{}}, + request: &IssuanceRequest{PublicKey: MarshalablePublicKey{&rsa.PublicKey{}}}, expectedError: "inactive issuer cannot issue precert", }, { name: "inactive (ecdsa)", issuer: &Issuer{}, profile: &Profile{}, - request: &IssuanceRequest{PublicKey: &ecdsa.PublicKey{}}, + request: &IssuanceRequest{PublicKey: MarshalablePublicKey{&ecdsa.PublicKey{}}}, expectedError: "inactive issuer cannot issue precert", }, { @@ -74,80 +122,25 @@ func TestRequestValid(t *testing.T) { }, profile: &Profile{}, request: &IssuanceRequest{ - PublicKey: &ecdsa.PublicKey{}, + PublicKey: MarshalablePublicKey{&ecdsa.PublicKey{}}, SubjectKeyId: []byte{0, 1, 2, 3, 4}, }, expectedError: "unexpected subject key ID length", }, { - name: "must staple not allowed", + name: "both sct list and ct poison provided", issuer: &Issuer{ active: true, }, profile: &Profile{}, request: &IssuanceRequest{ - PublicKey: &ecdsa.PublicKey{}, - SubjectKeyId: goodSKID, - IncludeMustStaple: true, - }, - expectedError: "must-staple extension cannot be included", - }, - { - name: "ct poison not allowed", - issuer: &Issuer{ - active: true, - }, - profile: &Profile{}, - request: &IssuanceRequest{ - PublicKey: &ecdsa.PublicKey{}, - SubjectKeyId: goodSKID, - IncludeCTPoison: true, - }, - expectedError: "ct poison extension cannot be included", - }, - { - name: "sct list not allowed", - issuer: &Issuer{ - active: true, - }, - profile: &Profile{}, - request: &IssuanceRequest{ - PublicKey: &ecdsa.PublicKey{}, - SubjectKeyId: goodSKID, - sctList: []ct.SignedCertificateTimestamp{}, - }, - expectedError: "sct list extension cannot be included", - }, - { - name: "sct list and ct poison not allowed", - issuer: &Issuer{ - active: true, - }, - profile: &Profile{ - allowCTPoison: true, - allowSCTList: true, - }, - request: &IssuanceRequest{ - PublicKey: &ecdsa.PublicKey{}, + PublicKey: MarshalablePublicKey{&ecdsa.PublicKey{}}, SubjectKeyId: goodSKID, IncludeCTPoison: true, sctList: []ct.SignedCertificateTimestamp{}, }, expectedError: "cannot include both ct poison and sct list extensions", }, - { - name: "common name not allowed", - issuer: &Issuer{ - active: true, - }, - profile: &Profile{}, - request: &IssuanceRequest{ - PublicKey: &ecdsa.PublicKey{}, - SubjectKeyId: goodSKID, - CommonName: "cn", - }, - expectedError: "common name cannot be included", - }, { name: "negative validity", issuer: &Issuer{ @@ -155,7 +148,7 @@ func TestRequestValid(t *testing.T) { }, profile: &Profile{}, request: &IssuanceRequest{ - PublicKey: &ecdsa.PublicKey{}, + PublicKey: MarshalablePublicKey{&ecdsa.PublicKey{}}, SubjectKeyId: goodSKID, NotBefore: fc.Now().Add(time.Hour), NotAfter: fc.Now(), @@ -171,7 +164,7 @@ func TestRequestValid(t *testing.T) { maxValidity: time.Minute, }, request: &IssuanceRequest{ - PublicKey: &ecdsa.PublicKey{}, + PublicKey: MarshalablePublicKey{&ecdsa.PublicKey{}}, SubjectKeyId: goodSKID, NotBefore: fc.Now(), NotAfter: fc.Now().Add(time.Hour - time.Second), @@ -187,7 +180,7 @@ func TestRequestValid(t *testing.T) { maxValidity: time.Hour, }, request: &IssuanceRequest{ - PublicKey: &ecdsa.PublicKey{}, + PublicKey: MarshalablePublicKey{&ecdsa.PublicKey{}}, SubjectKeyId: goodSKID, NotBefore: fc.Now(), NotAfter: fc.Now().Add(time.Hour), @@ -204,7 +197,7 @@ func TestRequestValid(t *testing.T) { maxBackdate: time.Hour, }, request: &IssuanceRequest{ - PublicKey: &ecdsa.PublicKey{}, + PublicKey: MarshalablePublicKey{&ecdsa.PublicKey{}}, SubjectKeyId: goodSKID, NotBefore: fc.Now().Add(-time.Hour * 2), NotAfter: fc.Now().Add(-time.Hour), @@ -221,7 +214,7 @@ func TestRequestValid(t *testing.T) { maxBackdate: time.Hour, }, request: &IssuanceRequest{ - PublicKey: &ecdsa.PublicKey{}, + PublicKey: MarshalablePublicKey{&ecdsa.PublicKey{}}, SubjectKeyId: goodSKID, NotBefore: fc.Now().Add(time.Hour), NotAfter: fc.Now().Add(time.Hour * 2), @@ -237,7 +230,7 @@ func TestRequestValid(t *testing.T) { maxValidity: time.Hour * 2, }, request: &IssuanceRequest{ - PublicKey: &ecdsa.PublicKey{}, + PublicKey: MarshalablePublicKey{&ecdsa.PublicKey{}}, SubjectKeyId: goodSKID, NotBefore: fc.Now(), NotAfter: fc.Now().Add(time.Hour), @@ -254,7 +247,7 @@ func TestRequestValid(t *testing.T) { maxValidity: time.Hour * 2, }, request: &IssuanceRequest{ - PublicKey: &ecdsa.PublicKey{}, + PublicKey: MarshalablePublicKey{&ecdsa.PublicKey{}}, SubjectKeyId: goodSKID, NotBefore: fc.Now(), NotAfter: fc.Now().Add(time.Hour), @@ -263,7 +256,7 @@ func TestRequestValid(t *testing.T) { expectedError: "serial must be between 9 and 19 bytes", }, { - name: "good", + name: "good with poison", issuer: &Issuer{ active: true, }, @@ -271,11 +264,29 @@ func TestRequestValid(t *testing.T) { maxValidity: time.Hour * 2, }, request: &IssuanceRequest{ - PublicKey: &ecdsa.PublicKey{}, + PublicKey: MarshalablePublicKey{&ecdsa.PublicKey{}}, + SubjectKeyId: goodSKID, + NotBefore: fc.Now(), + NotAfter: fc.Now().Add(time.Hour), + Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9}, + IncludeCTPoison: true, + }, + }, + { + name: "good with scts", + issuer: &Issuer{ + active: true, + }, + profile: &Profile{ + maxValidity: time.Hour * 2, + }, + request: &IssuanceRequest{ + PublicKey: MarshalablePublicKey{&ecdsa.PublicKey{}}, SubjectKeyId: goodSKID, NotBefore: fc.Now(), NotAfter: fc.Now().Add(time.Hour), Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9}, + sctList: []ct.SignedCertificateTimestamp{}, }, }, } @@ -298,7 +309,6 @@ func TestRequestValid(t *testing.T) { func TestGenerateTemplate(t *testing.T) { issuer := &Issuer{ - ocspURL: "http://ocsp", issuerURL: "http://issuer", crlURLBase: "http://crl/", sigAlg: x509.SHA256WithRSA, @@ -309,14 +319,11 @@ func TestGenerateTemplate(t *testing.T) { expected := &x509.Certificate{ BasicConstraintsValid: true, SignatureAlgorithm: x509.SHA256WithRSA, - ExtKeyUsage: []x509.ExtKeyUsage{ - x509.ExtKeyUsageServerAuth, - x509.ExtKeyUsageClientAuth, - }, IssuingCertificateURL: []string{"http://issuer"}, - OCSPServer: []string{"http://ocsp"}, + Policies: []x509.OID{domainValidatedOID}, + // These fields are only included if specified in the profile. + OCSPServer: nil, CRLDistributionPoints: nil, - PolicyIdentifiers: []asn1.ObjectIdentifier{{2, 23, 140, 1, 2, 1}}, } test.AssertDeepEquals(t, actual, expected) @@ -351,10 +358,11 @@ func TestIssue(t *testing.T) { pk, err := tc.generateFunc() test.AssertNotError(t, err, "failed to generate test key") lintCertBytes, issuanceToken, err := signer.Prepare(defaultProfile(), &IssuanceRequest{ - PublicKey: pk.Public(), + PublicKey: MarshalablePublicKey{pk.Public()}, SubjectKeyId: goodSKID, Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9}, DNSNames: []string{"example.com"}, + IPAddresses: []net.IP{net.ParseIP("128.101.101.101"), net.ParseIP("3fff:aaa:a:c0ff:ee:a:bad:deed")}, NotBefore: fc.Now(), NotAfter: fc.Now().Add(time.Hour - time.Second), IncludeCTPoison: true, @@ -369,41 +377,169 @@ func TestIssue(t *testing.T) { err = cert.CheckSignatureFrom(issuerCert.Certificate) test.AssertNotError(t, err, "signature validation failed") test.AssertDeepEquals(t, cert.DNSNames, []string{"example.com"}) + // net.ParseIP always returns a 16-byte address; IPv4 addresses are + // returned in IPv4-mapped IPv6 form. But RFC 5280, Sec. 4.2.1.6 + // requires that IPv4 addresses be encoded as 4 bytes. + // + // The issuance pipeline calls x509.marshalSANs, which reduces IPv4 + // addresses back to 4 bytes. Adding .To4() both allows this test to + // succeed, and covers this requirement. + test.AssertDeepEquals(t, cert.IPAddresses, []net.IP{net.ParseIP("128.101.101.101").To4(), net.ParseIP("3fff:aaa:a:c0ff:ee:a:bad:deed")}) test.AssertByteEquals(t, cert.SerialNumber.Bytes(), []byte{1, 2, 3, 4, 5, 6, 7, 8, 9}) test.AssertDeepEquals(t, cert.PublicKey, pk.Public()) - test.AssertEquals(t, len(cert.Extensions), 9) // Constraints, KU, EKU, SKID, AKID, AIA, SAN, Policies, Poison + test.AssertEquals(t, len(cert.Extensions), 10) // Constraints, KU, EKU, SKID, AKID, AIA, CRLDP, SAN, Policies, Poison test.AssertEquals(t, cert.KeyUsage, tc.ku) + if len(cert.CRLDistributionPoints) != 1 || !strings.HasPrefix(cert.CRLDistributionPoints[0], "http://crl-url.example.org/") { + t.Errorf("want CRLDistributionPoints=[http://crl-url.example.org/x.crl], got %v", cert.CRLDistributionPoints) + } }) } } +func TestIssueDNSNamesOnly(t *testing.T) { + fc := clock.NewFake() + signer, err := newIssuer(defaultIssuerConfig(), issuerCert, issuerSigner, fc) + if err != nil { + t.Fatalf("newIssuer: %s", err) + } + pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatalf("ecdsa.GenerateKey: %s", err) + } + _, issuanceToken, err := signer.Prepare(defaultProfile(), &IssuanceRequest{ + PublicKey: MarshalablePublicKey{pk.Public()}, + SubjectKeyId: goodSKID, + Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9}, + DNSNames: []string{"example.com"}, + NotBefore: fc.Now(), + NotAfter: fc.Now().Add(time.Hour - time.Second), + IncludeCTPoison: true, + }) + if err != nil { + t.Fatalf("signer.Prepare: %s", err) + } + certBytes, err := signer.Issue(issuanceToken) + if err != nil { + t.Fatalf("signer.Issue: %s", err) + } + cert, err := x509.ParseCertificate(certBytes) + if err != nil { + t.Fatalf("x509.ParseCertificate: %s", err) + } + if !reflect.DeepEqual(cert.DNSNames, []string{"example.com"}) { + t.Errorf("got DNSNames %s, wanted example.com", cert.DNSNames) + } + // BRs 7.1.2.7.12 requires iPAddress, if present, to contain an entry. + if cert.IPAddresses != nil { + t.Errorf("got IPAddresses %s, wanted nil", cert.IPAddresses) + } +} + +func TestIssueIPAddressesOnly(t *testing.T) { + fc := clock.NewFake() + signer, err := newIssuer(defaultIssuerConfig(), issuerCert, issuerSigner, fc) + if err != nil { + t.Fatalf("newIssuer: %s", err) + } + pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatalf("ecdsa.GenerateKey: %s", err) + } + _, issuanceToken, err := signer.Prepare(defaultProfile(), &IssuanceRequest{ + PublicKey: MarshalablePublicKey{pk.Public()}, + SubjectKeyId: goodSKID, + Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9}, + IPAddresses: []net.IP{net.ParseIP("128.101.101.101"), net.ParseIP("3fff:aaa:a:c0ff:ee:a:bad:deed")}, + NotBefore: fc.Now(), + NotAfter: fc.Now().Add(time.Hour - time.Second), + IncludeCTPoison: true, + }) + if err != nil { + t.Fatalf("signer.Prepare: %s", err) + } + certBytes, err := signer.Issue(issuanceToken) + if err != nil { + t.Fatalf("signer.Issue: %s", err) + } + cert, err := x509.ParseCertificate(certBytes) + if err != nil { + t.Fatalf("x509.ParseCertificate: %s", err) + } + // BRs 7.1.2.7.12 requires dNSName, if present, to contain an entry. + if cert.DNSNames != nil { + t.Errorf("got DNSNames %s, wanted nil", cert.DNSNames) + } + if !reflect.DeepEqual(cert.IPAddresses, []net.IP{net.ParseIP("128.101.101.101").To4(), net.ParseIP("3fff:aaa:a:c0ff:ee:a:bad:deed")}) { + t.Errorf("got IPAddresses %s, wanted 128.101.101.101 (4-byte) & 3fff:aaa:a:c0ff:ee:a:bad:deed (16-byte)", cert.IPAddresses) + } +} + +func TestIssueWithCRLDP(t *testing.T) { + fc := clock.NewFake() + issuerConfig := defaultIssuerConfig() + issuerConfig.CRLURLBase = "http://crls.example.net/" + issuerConfig.CRLShards = 999 + signer, err := newIssuer(issuerConfig, issuerCert, issuerSigner, fc) + if err != nil { + t.Fatalf("newIssuer: %s", err) + } + pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatalf("ecdsa.GenerateKey: %s", err) + } + profile := defaultProfile() + profile.includeCRLDistributionPoints = true + _, issuanceToken, err := signer.Prepare(profile, &IssuanceRequest{ + PublicKey: MarshalablePublicKey{pk.Public()}, + SubjectKeyId: goodSKID, + Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9}, + DNSNames: []string{"example.com"}, + NotBefore: fc.Now(), + NotAfter: fc.Now().Add(time.Hour - time.Second), + IncludeCTPoison: true, + }) + if err != nil { + t.Fatalf("signer.Prepare: %s", err) + } + certBytes, err := signer.Issue(issuanceToken) + if err != nil { + t.Fatalf("signer.Issue: %s", err) + } + cert, err := x509.ParseCertificate(certBytes) + if err != nil { + t.Fatalf("x509.ParseCertificate: %s", err) + } + // Because CRL shard is calculated deterministically from serial, we know which shard will be chosen. + expectedCRLDP := []string{"http://crls.example.net/919.crl"} + if !reflect.DeepEqual(cert.CRLDistributionPoints, expectedCRLDP) { + t.Errorf("CRLDP=%+v, want %+v", cert.CRLDistributionPoints, expectedCRLDP) + } +} + func TestIssueCommonName(t *testing.T) { fc := clock.NewFake() fc.Set(time.Now()) - lints, err := linter.NewRegistry([]string{ - "w_subject_common_name_included", - "w_ct_sct_policy_count_unsatisfied", - "e_scts_from_same_operator", - }) - test.AssertNotError(t, err, "building test lint registry") - cnProfile, err := NewProfile(defaultProfileConfig(), lints) + prof := defaultProfileConfig() + prof.IgnoredLints = append(prof.IgnoredLints, "w_subject_common_name_included") + cnProfile, err := NewProfile(prof) test.AssertNotError(t, err, "NewProfile failed") signer, err := newIssuer(defaultIssuerConfig(), issuerCert, issuerSigner, fc) test.AssertNotError(t, err, "NewIssuer failed") pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) test.AssertNotError(t, err, "failed to generate test key") ir := &IssuanceRequest{ - PublicKey: pk.Public(), + PublicKey: MarshalablePublicKey{pk.Public()}, SubjectKeyId: goodSKID, Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9}, - CommonName: "example.com", DNSNames: []string{"example.com", "www.example.com"}, NotBefore: fc.Now(), NotAfter: fc.Now().Add(time.Hour - time.Second), IncludeCTPoison: true, } + // In the default profile, the common name is allowed if requested. + ir.CommonName = "example.com" _, issuanceToken, err := signer.Prepare(cnProfile, ir) test.AssertNotError(t, err, "Prepare failed") certBytes, err := signer.Issue(issuanceToken) @@ -412,10 +548,7 @@ func TestIssueCommonName(t *testing.T) { test.AssertNotError(t, err, "failed to parse certificate") test.AssertEquals(t, cert.Subject.CommonName, "example.com") - cnProfile.allowCommonName = false - _, _, err = signer.Prepare(cnProfile, ir) - test.AssertError(t, err, "Prepare should have failed") - + // But not including the common name should be acceptable as well. ir.CommonName = "" _, issuanceToken, err = signer.Prepare(cnProfile, ir) test.AssertNotError(t, err, "Prepare failed") @@ -424,7 +557,64 @@ func TestIssueCommonName(t *testing.T) { cert, err = x509.ParseCertificate(certBytes) test.AssertNotError(t, err, "failed to parse certificate") test.AssertEquals(t, cert.Subject.CommonName, "") - test.AssertDeepEquals(t, cert.DNSNames, []string{"example.com", "www.example.com"}) + + // And the common name should be omitted if the profile is so configured. + ir.CommonName = "example.com" + cnProfile.omitCommonName = true + _, issuanceToken, err = signer.Prepare(cnProfile, ir) + test.AssertNotError(t, err, "Prepare failed") + certBytes, err = signer.Issue(issuanceToken) + test.AssertNotError(t, err, "Issue failed") + cert, err = x509.ParseCertificate(certBytes) + test.AssertNotError(t, err, "failed to parse certificate") + test.AssertEquals(t, cert.Subject.CommonName, "") +} + +func TestIssueOmissions(t *testing.T) { + fc := clock.NewFake() + fc.Set(time.Now()) + + pc := defaultProfileConfig() + pc.OmitCommonName = true + pc.OmitKeyEncipherment = true + pc.OmitClientAuth = true + pc.OmitSKID = true + pc.IgnoredLints = []string{ + // Reduce the lint ignores to just the minimal (SCT-related) set. + "w_ct_sct_policy_count_unsatisfied", + "e_scts_from_same_operator", + // Ignore the warning about *not* including the SubjectKeyIdentifier extension: + // zlint has both lints (one enforcing RFC5280, the other the BRs). + "w_ext_subject_key_identifier_missing_sub_cert", + } + prof, err := NewProfile(pc) + test.AssertNotError(t, err, "building test profile") + + signer, err := newIssuer(defaultIssuerConfig(), issuerCert, issuerSigner, fc) + test.AssertNotError(t, err, "NewIssuer failed") + + pk, err := rsa.GenerateKey(rand.Reader, 2048) + test.AssertNotError(t, err, "failed to generate test key") + _, issuanceToken, err := signer.Prepare(prof, &IssuanceRequest{ + PublicKey: MarshalablePublicKey{pk.Public()}, + SubjectKeyId: goodSKID, + Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9}, + DNSNames: []string{"example.com"}, + CommonName: "example.com", + IncludeCTPoison: true, + NotBefore: fc.Now(), + NotAfter: fc.Now().Add(time.Hour - time.Second), + }) + test.AssertNotError(t, err, "Prepare failed") + certBytes, err := signer.Issue(issuanceToken) + test.AssertNotError(t, err, "Issue failed") + cert, err := x509.ParseCertificate(certBytes) + test.AssertNotError(t, err, "failed to parse certificate") + + test.AssertEquals(t, cert.Subject.CommonName, "") + test.AssertEquals(t, cert.KeyUsage, x509.KeyUsageDigitalSignature) + test.AssertDeepEquals(t, cert.ExtKeyUsage, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}) + test.AssertEquals(t, len(cert.SubjectKeyId), 0) } func TestIssueCTPoison(t *testing.T) { @@ -432,11 +622,10 @@ func TestIssueCTPoison(t *testing.T) { fc.Set(time.Now()) signer, err := newIssuer(defaultIssuerConfig(), issuerCert, issuerSigner, fc) test.AssertNotError(t, err, "NewIssuer failed") - test.AssertNotError(t, err, "NewIssuer failed") pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) test.AssertNotError(t, err, "failed to generate test key") _, issuanceToken, err := signer.Prepare(defaultProfile(), &IssuanceRequest{ - PublicKey: pk.Public(), + PublicKey: MarshalablePublicKey{pk.Public()}, SubjectKeyId: goodSKID, Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9}, DNSNames: []string{"example.com"}, @@ -453,8 +642,8 @@ func TestIssueCTPoison(t *testing.T) { test.AssertNotError(t, err, "signature validation failed") test.AssertByteEquals(t, cert.SerialNumber.Bytes(), []byte{1, 2, 3, 4, 5, 6, 7, 8, 9}) test.AssertDeepEquals(t, cert.PublicKey, pk.Public()) - test.AssertEquals(t, len(cert.Extensions), 9) // Constraints, KU, EKU, SKID, AKID, AIA, SAN, Policies, CT Poison - test.AssertDeepEquals(t, cert.Extensions[8], ctPoisonExt) + test.AssertEquals(t, len(cert.Extensions), 10) // Constraints, KU, EKU, SKID, AKID, AIA, CRLDP, SAN, Policies, Poison + test.AssertDeepEquals(t, cert.Extensions[9], ctPoisonExt) } func mustDecodeB64(b string) []byte { @@ -472,16 +661,19 @@ func TestIssueSCTList(t *testing.T) { err := loglist.InitLintList("../test/ct-test-srv/log_list.json") test.AssertNotError(t, err, "failed to load log list") - lints, err := linter.NewRegistry([]string{}) - test.AssertNotError(t, err, "building test lint registry") - enforceSCTsProfile, err := NewProfile(defaultProfileConfig(), lints) + pc := defaultProfileConfig() + pc.IgnoredLints = []string{ + // Only ignore the SKID lint, i.e., don't ignore the "missing SCT" lints. + "w_ext_subject_key_identifier_not_recommended_subscriber", + } + enforceSCTsProfile, err := NewProfile(pc) test.AssertNotError(t, err, "NewProfile failed") signer, err := newIssuer(defaultIssuerConfig(), issuerCert, issuerSigner, fc) test.AssertNotError(t, err, "NewIssuer failed") pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) test.AssertNotError(t, err, "failed to generate test key") _, issuanceToken, err := signer.Prepare(enforceSCTsProfile, &IssuanceRequest{ - PublicKey: pk.Public(), + PublicKey: MarshalablePublicKey{pk.Public()}, SubjectKeyId: goodSKID, Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9}, DNSNames: []string{"example.com"}, @@ -522,8 +714,8 @@ func TestIssueSCTList(t *testing.T) { test.AssertNotError(t, err, "signature validation failed") test.AssertByteEquals(t, finalCert.SerialNumber.Bytes(), []byte{1, 2, 3, 4, 5, 6, 7, 8, 9}) test.AssertDeepEquals(t, finalCert.PublicKey, pk.Public()) - test.AssertEquals(t, len(finalCert.Extensions), 9) // Constraints, KU, EKU, SKID, AKID, AIA, SAN, Policies, SCT list - test.AssertDeepEquals(t, finalCert.Extensions[8], pkix.Extension{ + test.AssertEquals(t, len(finalCert.Extensions), 10) // Constraints, KU, EKU, SKID, AKID, AIA, CRLDP, SAN, Policies, Poison + test.AssertDeepEquals(t, finalCert.Extensions[9], pkix.Extension{ Id: sctListOID, Value: []byte{ 4, 100, 0, 98, 0, 47, 0, 56, 152, 140, 148, 208, 53, 152, 195, 147, 45, @@ -536,51 +728,20 @@ func TestIssueSCTList(t *testing.T) { }) } -func TestIssueMustStaple(t *testing.T) { - fc := clock.NewFake() - fc.Set(time.Now()) - - signer, err := newIssuer(defaultIssuerConfig(), issuerCert, issuerSigner, fc) - test.AssertNotError(t, err, "NewIssuer failed") - pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - test.AssertNotError(t, err, "failed to generate test key") - _, issuanceToken, err := signer.Prepare(defaultProfile(), &IssuanceRequest{ - PublicKey: pk.Public(), - SubjectKeyId: goodSKID, - Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9}, - DNSNames: []string{"example.com"}, - IncludeMustStaple: true, - NotBefore: fc.Now(), - NotAfter: fc.Now().Add(time.Hour - time.Second), - IncludeCTPoison: true, - }) - test.AssertNotError(t, err, "Prepare failed") - certBytes, err := signer.Issue(issuanceToken) - test.AssertNotError(t, err, "Issue failed") - cert, err := x509.ParseCertificate(certBytes) - test.AssertNotError(t, err, "failed to parse certificate") - err = cert.CheckSignatureFrom(issuerCert.Certificate) - test.AssertNotError(t, err, "signature validation failed") - test.AssertByteEquals(t, cert.SerialNumber.Bytes(), []byte{1, 2, 3, 4, 5, 6, 7, 8, 9}) - test.AssertDeepEquals(t, cert.PublicKey, pk.Public()) - test.AssertEquals(t, len(cert.Extensions), 10) // Constraints, KU, EKU, SKID, AKID, AIA, SAN, Policies, Must-Staple, Poison - test.AssertDeepEquals(t, cert.Extensions[9], mustStapleExt) -} - func TestIssueBadLint(t *testing.T) { fc := clock.NewFake() fc.Set(time.Now()) - lints, err := linter.NewRegistry([]string{}) - test.AssertNotError(t, err, "building test lint registry") - noSkipLintsProfile, err := NewProfile(defaultProfileConfig(), lints) + pc := defaultProfileConfig() + pc.IgnoredLints = []string{} + noSkipLintsProfile, err := NewProfile(pc) test.AssertNotError(t, err, "NewProfile failed") signer, err := newIssuer(defaultIssuerConfig(), issuerCert, issuerSigner, fc) test.AssertNotError(t, err, "NewIssuer failed") pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) test.AssertNotError(t, err, "failed to generate test key") _, _, err = signer.Prepare(noSkipLintsProfile, &IssuanceRequest{ - PublicKey: pk.Public(), + PublicKey: MarshalablePublicKey{pk.Public()}, SubjectKeyId: goodSKID, Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9}, DNSNames: []string{"example-com"}, @@ -609,7 +770,7 @@ func TestIssuanceToken(t *testing.T) { pk, err := rsa.GenerateKey(rand.Reader, 2048) test.AssertNotError(t, err, "failed to generate test key") _, issuanceToken, err := signer.Prepare(defaultProfile(), &IssuanceRequest{ - PublicKey: pk.Public(), + PublicKey: MarshalablePublicKey{pk.Public()}, SubjectKeyId: goodSKID, Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9}, DNSNames: []string{"example.com"}, @@ -626,7 +787,7 @@ func TestIssuanceToken(t *testing.T) { test.AssertContains(t, err.Error(), "issuance token already redeemed") _, issuanceToken, err = signer.Prepare(defaultProfile(), &IssuanceRequest{ - PublicKey: pk.Public(), + PublicKey: MarshalablePublicKey{pk.Public()}, SubjectKeyId: goodSKID, Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9}, DNSNames: []string{"example.com"}, @@ -656,7 +817,7 @@ func TestInvalidProfile(t *testing.T) { pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) test.AssertNotError(t, err, "failed to generate test key") _, _, err = signer.Prepare(defaultProfile(), &IssuanceRequest{ - PublicKey: pk.Public(), + PublicKey: MarshalablePublicKey{pk.Public()}, SubjectKeyId: goodSKID, Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9}, DNSNames: []string{"example.com"}, @@ -668,7 +829,7 @@ func TestInvalidProfile(t *testing.T) { test.AssertError(t, err, "Invalid IssuanceRequest") _, _, err = signer.Prepare(defaultProfile(), &IssuanceRequest{ - PublicKey: pk.Public(), + PublicKey: MarshalablePublicKey{pk.Public()}, SubjectKeyId: goodSKID, Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9}, DNSNames: []string{"example.com"}, @@ -697,19 +858,15 @@ func TestMismatchedProfiles(t *testing.T) { issuer1, err := newIssuer(defaultIssuerConfig(), issuerCert, issuerSigner, fc) test.AssertNotError(t, err, "NewIssuer failed") - lints, err := linter.NewRegistry([]string{ - "w_subject_common_name_included", - "w_ct_sct_policy_count_unsatisfied", - "e_scts_from_same_operator", - }) - test.AssertNotError(t, err, "building test lint registry") - cnProfile, err := NewProfile(defaultProfileConfig(), lints) + pc := defaultProfileConfig() + pc.IgnoredLints = append(pc.IgnoredLints, "w_subject_common_name_included") + cnProfile, err := NewProfile(pc) test.AssertNotError(t, err, "NewProfile failed") pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) test.AssertNotError(t, err, "failed to generate test key") _, issuanceToken, err := issuer1.Prepare(cnProfile, &IssuanceRequest{ - PublicKey: pk.Public(), + PublicKey: MarshalablePublicKey{pk.Public()}, SubjectKeyId: goodSKID, Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9}, CommonName: "example.com", @@ -724,14 +881,10 @@ func TestMismatchedProfiles(t *testing.T) { test.AssertNotError(t, err, "signing precert") // Create a new profile that differs slightly (no common name) - profileConfig := defaultProfileConfig() - profileConfig.AllowCommonName = false - lints, err = linter.NewRegistry([]string{ - "w_ct_sct_policy_count_unsatisfied", - "e_scts_from_same_operator", - }) + pc = defaultProfileConfig() + pc.OmitCommonName = false test.AssertNotError(t, err, "building test lint registry") - noCNProfile, err := NewProfile(profileConfig, lints) + noCNProfile, err := NewProfile(pc) test.AssertNotError(t, err, "NewProfile failed") issuer2, err := newIssuer(defaultIssuerConfig(), issuerCert, issuerSigner, fc) @@ -759,3 +912,61 @@ func TestMismatchedProfiles(t *testing.T) { test.AssertError(t, err, "preparing final cert issuance") test.AssertContains(t, err.Error(), "precert does not correspond to linted final cert") } + +func TestNewProfile(t *testing.T) { + for _, tc := range []struct { + name string + config ProfileConfig + wantErr string + }{ + { + name: "happy path", + config: ProfileConfig{ + MaxValidityBackdate: config.Duration{Duration: 1 * time.Hour}, + MaxValidityPeriod: config.Duration{Duration: 90 * 24 * time.Hour}, + IncludeCRLDistributionPoints: true, + }, + }, + { + name: "large backdate", + config: ProfileConfig{ + MaxValidityBackdate: config.Duration{Duration: 24 * time.Hour}, + MaxValidityPeriod: config.Duration{Duration: 90 * 24 * time.Hour}, + }, + wantErr: "backdate \"24h0m0s\" is too large", + }, + { + name: "large validity", + config: ProfileConfig{ + MaxValidityBackdate: config.Duration{Duration: 1 * time.Hour}, + MaxValidityPeriod: config.Duration{Duration: 397 * 24 * time.Hour}, + }, + wantErr: "validity period \"9528h0m0s\" is too large", + }, + { + name: "no revocation info", + config: ProfileConfig{ + MaxValidityBackdate: config.Duration{Duration: 1 * time.Hour}, + MaxValidityPeriod: config.Duration{Duration: 90 * 24 * time.Hour}, + IncludeCRLDistributionPoints: false, + }, + wantErr: "revocation mechanism must be included", + }, + } { + t.Run(tc.name, func(t *testing.T) { + gotProfile, gotErr := NewProfile(&tc.config) + if tc.wantErr != "" { + if gotErr == nil { + t.Errorf("NewProfile(%#v) = %#v, but want err %q", tc.config, gotProfile, tc.wantErr) + } + if !strings.Contains(gotErr.Error(), tc.wantErr) { + t.Errorf("NewProfile(%#v) = %q, but want %q", tc.config, gotErr, tc.wantErr) + } + } else { + if gotErr != nil { + t.Errorf("NewProfile(%#v) = %q, but want no error", tc.config, gotErr) + } + } + }) + } +} diff --git a/third-party/github.com/letsencrypt/boulder/issuance/crl.go b/third-party/github.com/letsencrypt/boulder/issuance/crl.go index 48fc54e3f..f33af1883 100644 --- a/third-party/github.com/letsencrypt/boulder/issuance/crl.go +++ b/third-party/github.com/letsencrypt/boulder/issuance/crl.go @@ -17,6 +17,13 @@ import ( type CRLProfileConfig struct { ValidityInterval config.Duration MaxBackdate config.Duration + + // 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 lint names that we know will fail for this + // profile, and which we know it is safe to ignore. + IgnoredLints []string } type CRLProfile struct { @@ -38,10 +45,17 @@ func NewCRLProfile(config CRLProfileConfig) (*CRLProfile, error) { return nil, fmt.Errorf("crl max backdate must be non-negative, got %q", config.MaxBackdate) } - reg, err := linter.NewRegistry(nil) + reg, err := linter.NewRegistry(config.IgnoredLints) if err != nil { return nil, fmt.Errorf("creating lint registry: %w", err) } + if config.LintConfig != "" { + lintconfig, err := lint.NewConfigFromFile(config.LintConfig) + if err != nil { + return nil, fmt.Errorf("loading zlint config file: %w", err) + } + reg.SetConfiguration(lintconfig) + } return &CRLProfile{ validityInterval: config.ValidityInterval.Duration, @@ -59,6 +73,11 @@ type CRLRequest struct { Entries []x509.RevocationListEntry } +// crlURL combines the CRL URL base with a shard, and adds a suffix. +func (i *Issuer) crlURL(shard int) string { + return fmt.Sprintf("%s%d.crl", i.crlURLBase, shard) +} + func (i *Issuer) IssueCRL(prof *CRLProfile, req *CRLRequest) ([]byte, error) { backdatedBy := i.clk.Now().Sub(req.ThisUpdate) if backdatedBy > prof.maxBackdate { @@ -82,7 +101,7 @@ func (i *Issuer) IssueCRL(prof *CRLProfile, req *CRLRequest) ([]byte, error) { // Concat the base with the shard directly, since we require that the base // end with a single trailing slash. idp, err := idp.MakeUserCertsExt([]string{ - fmt.Sprintf("%s%d.crl", i.crlURLBase, req.Shard), + i.crlURL(int(req.Shard)), }) if err != nil { return nil, fmt.Errorf("creating IDP extension: %w", err) diff --git a/third-party/github.com/letsencrypt/boulder/issuance/crl_test.go b/third-party/github.com/letsencrypt/boulder/issuance/crl_test.go index 38b822c3f..df30bd1af 100644 --- a/third-party/github.com/letsencrypt/boulder/issuance/crl_test.go +++ b/third-party/github.com/letsencrypt/boulder/issuance/crl_test.go @@ -60,7 +60,6 @@ func TestNewCRLProfile(t *testing.T) { }, } for _, tc := range tests { - tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() actual, err := NewCRLProfile(tc.config) diff --git a/third-party/github.com/letsencrypt/boulder/issuance/issuer.go b/third-party/github.com/letsencrypt/boulder/issuance/issuer.go index 4206b65c6..95d2f03a7 100644 --- a/third-party/github.com/letsencrypt/boulder/issuance/issuer.go +++ b/third-party/github.com/letsencrypt/boulder/issuance/issuer.go @@ -157,13 +157,19 @@ type IssuerConfig struct { // (for which an issuance token is presented), OCSP responses, and CRLs. // All Active issuers of a given key type (RSA or ECDSA) are part of a pool // and each precertificate will be issued randomly from a selected pool. - // The selection of which pool depends on the precertificate's key algorithm, - // the ECDSAForAll feature flag, and the ECDSAAllowListFilename config field. + // The selection of which pool depends on the precertificate's key algorithm. Active bool IssuerURL string `validate:"required,url"` - OCSPURL string `validate:"required,url"` - CRLURLBase string `validate:"omitempty,url,startswith=http://,endswith=/"` + CRLURLBase string `validate:"required,url,startswith=http://,endswith=/"` + + // TODO(#8177): Remove this. + OCSPURL string `validate:"omitempty,url"` + + // Number of CRL shards. + // This must be nonzero if adding CRLDistributionPoints to certificates + // (that is, if profile.IncludeCRLDistributionPoints is true). + CRLShards int Location IssuerLoc } @@ -201,13 +207,12 @@ type Issuer struct { // Used to set the Authority Information Access caIssuers URL in issued // certificates. issuerURL string - // Used to set the Authority Information Access ocsp URL in issued - // certificates. - ocspURL string // Used to set the Issuing Distribution Point extension in issued CRLs - // *and* (eventually) the CRL Distribution Point extension in issued certs. + // and the CRL Distribution Point extension in issued certs. crlURLBase string + crlShards int + clk clock.Clock } @@ -237,9 +242,6 @@ func newIssuer(config IssuerConfig, cert *Certificate, signer crypto.Signer, clk if config.IssuerURL == "" { return nil, errors.New("Issuer URL is required") } - if config.OCSPURL == "" { - return nil, errors.New("OCSP URL is required") - } if config.CRLURLBase == "" { return nil, errors.New("CRL URL base is required") } @@ -275,8 +277,8 @@ func newIssuer(config IssuerConfig, cert *Certificate, signer crypto.Signer, clk sigAlg: sigAlg, active: config.Active, issuerURL: config.IssuerURL, - ocspURL: config.OCSPURL, crlURLBase: config.CRLURLBase, + crlShards: config.CRLShards, clk: clk, } return i, nil diff --git a/third-party/github.com/letsencrypt/boulder/issuance/issuer_test.go b/third-party/github.com/letsencrypt/boulder/issuance/issuer_test.go index 4e96145a1..fa55d030a 100644 --- a/third-party/github.com/letsencrypt/boulder/issuance/issuer_test.go +++ b/third-party/github.com/letsencrypt/boulder/issuance/issuer_test.go @@ -22,14 +22,20 @@ import ( "github.com/letsencrypt/boulder/test" ) -func defaultProfileConfig() ProfileConfig { - return ProfileConfig{ - AllowCommonName: true, - AllowCTPoison: true, - AllowSCTList: true, - AllowMustStaple: true, - MaxValidityPeriod: config.Duration{Duration: time.Hour}, - MaxValidityBackdate: config.Duration{Duration: time.Hour}, +func defaultProfileConfig() *ProfileConfig { + return &ProfileConfig{ + AllowMustStaple: true, + IncludeCRLDistributionPoints: true, + MaxValidityPeriod: config.Duration{Duration: time.Hour}, + MaxValidityBackdate: config.Duration{Duration: time.Hour}, + IgnoredLints: []string{ + // Ignore the two SCT lints because these tests don't get SCTs. + "w_ct_sct_policy_count_unsatisfied", + "e_scts_from_same_operator", + // Ignore the warning about including the SubjectKeyIdentifier extension: + // we include it on purpose, but plan to remove it soon. + "w_ext_subject_key_identifier_not_recommended_subscriber", + }, } } @@ -37,8 +43,8 @@ func defaultIssuerConfig() IssuerConfig { return IssuerConfig{ Active: true, IssuerURL: "http://issuer-url.example.org", - OCSPURL: "http://ocsp-url.example.org", CRLURLBase: "http://crl-url.example.org/", + CRLShards: 10, } } @@ -78,7 +84,6 @@ func TestLoadCertificate(t *testing.T) { {"happy path", "../test/hierarchy/int-e1.cert.pem", ""}, } for _, tc := range tests { - tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() _, err := LoadCertificate(tc.path) @@ -115,14 +120,13 @@ func TestLoadSigner(t *testing.T) { {"invalid key file", IssuerLoc{File: "../test/hierarchy/int-e1.crl.pem"}, "unable to parse"}, {"ECDSA key file", IssuerLoc{File: "../test/hierarchy/int-e1.key.pem"}, ""}, {"RSA key file", IssuerLoc{File: "../test/hierarchy/int-r3.key.pem"}, ""}, - {"invalid config file", IssuerLoc{ConfigFile: "../test/example-weak-keys.json"}, "json: cannot unmarshal"}, + {"invalid config file", IssuerLoc{ConfigFile: "../test/hostname-policy.yaml"}, "invalid character"}, // Note that we don't have a test for "valid config file" because it would // always fail -- in CI, the softhsm hasn't been initialized, so there's no // key to look up; locally even if the softhsm has been initialized, the // keys in it don't match the fakeKey we generated above. } for _, tc := range tests { - tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() _, err := loadSigner(tc.loc, fakeKey.Public()) @@ -180,7 +184,6 @@ func TestNewIssuerKeyUsage(t *testing.T) { {"all three", x509.KeyUsageCertSign | x509.KeyUsageCRLSign | x509.KeyUsageDigitalSignature, ""}, } for _, tc := range tests { - tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() _, err := newIssuer( diff --git a/third-party/github.com/letsencrypt/boulder/link.sh b/third-party/github.com/letsencrypt/boulder/link.sh deleted file mode 100644 index 77344d224..000000000 --- a/third-party/github.com/letsencrypt/boulder/link.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash -# -# Symlink the various boulder subcommands into place. -# -BINDIR="$PWD/bin" -for n in `"${BINDIR}/boulder" --list` ; do - ln -sf boulder "${BINDIR}/$n" -done diff --git a/third-party/github.com/letsencrypt/boulder/linter/linter.go b/third-party/github.com/letsencrypt/boulder/linter/linter.go index e9bf33b85..522dd5ee5 100644 --- a/third-party/github.com/letsencrypt/boulder/linter/linter.go +++ b/third-party/github.com/letsencrypt/boulder/linter/linter.go @@ -194,7 +194,7 @@ func makeIssuer(realIssuer *x509.Certificate, lintSigner crypto.Signer) (*x509.C PermittedEmailAddresses: realIssuer.PermittedEmailAddresses, PermittedIPRanges: realIssuer.PermittedIPRanges, PermittedURIDomains: realIssuer.PermittedURIDomains, - PolicyIdentifiers: realIssuer.PolicyIdentifiers, + Policies: realIssuer.Policies, SerialNumber: realIssuer.SerialNumber, Subject: realIssuer.Subject, SubjectKeyId: realIssuer.SubjectKeyId, diff --git a/third-party/github.com/letsencrypt/boulder/linter/linter_test.go b/third-party/github.com/letsencrypt/boulder/linter/linter_test.go index 5b2c06eb9..7f759629a 100644 --- a/third-party/github.com/letsencrypt/boulder/linter/linter_test.go +++ b/third-party/github.com/letsencrypt/boulder/linter/linter_test.go @@ -6,13 +6,14 @@ import ( "crypto/elliptic" "crypto/rsa" "math/big" + "strings" "testing" "github.com/letsencrypt/boulder/test" ) func TestMakeSigner_RSA(t *testing.T) { - rsaMod, ok := big.NewInt(0).SetString("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 16) + rsaMod, ok := big.NewInt(0).SetString(strings.Repeat("ff", 128), 16) test.Assert(t, ok, "failed to set RSA mod") realSigner := &rsa.PrivateKey{ PublicKey: rsa.PublicKey{ diff --git a/third-party/github.com/letsencrypt/boulder/linter/lints/chrome/e_scts_from_same_operator.go b/third-party/github.com/letsencrypt/boulder/linter/lints/chrome/e_scts_from_same_operator.go index eb50e43c8..2b39baa43 100644 --- a/third-party/github.com/letsencrypt/boulder/linter/lints/chrome/e_scts_from_same_operator.go +++ b/third-party/github.com/letsencrypt/boulder/linter/lints/chrome/e_scts_from_same_operator.go @@ -64,15 +64,26 @@ func (l *sctsFromSameOperator) Execute(c *x509.Certificate) *lint.LintResult { } } + rfc6962Compliant := false operatorNames := make(map[string]struct{}) for logID := range logIDs { - operator, err := l.logList.OperatorForLogID(logID.Base64String()) + log, err := l.logList.GetByID(logID.Base64String()) if err != nil { // This certificate *may* have more than 2 SCTs, so missing one now isn't // a problem. continue } - operatorNames[operator] = struct{}{} + if !log.Tiled { + rfc6962Compliant = true + } + operatorNames[log.Operator] = struct{}{} + } + + if !rfc6962Compliant { + return &lint.LintResult{ + Status: lint.Error, + Details: "At least one certificate SCT must be from an RFC6962-compliant log.", + } } if len(operatorNames) < 2 { diff --git a/third-party/github.com/letsencrypt/boulder/linter/lints/common_test.go b/third-party/github.com/letsencrypt/boulder/linter/lints/common_test.go index a09e3ff69..f9a6757bd 100644 --- a/third-party/github.com/letsencrypt/boulder/linter/lints/common_test.go +++ b/third-party/github.com/letsencrypt/boulder/linter/lints/common_test.go @@ -3,9 +3,10 @@ package lints import ( "testing" - "github.com/letsencrypt/boulder/test" "golang.org/x/crypto/cryptobyte" "golang.org/x/crypto/cryptobyte/asn1" + + "github.com/letsencrypt/boulder/test" ) var onlyContainsUserCertsTag = asn1.Tag(1).ContextSpecific() @@ -78,7 +79,6 @@ func TestReadOptionalASN1BooleanWithTag(t *testing.T) { } for _, tc := range testCases { - tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() diff --git a/third-party/github.com/letsencrypt/boulder/linter/lints/rfc/lint_cert_via_pkilint.go b/third-party/github.com/letsencrypt/boulder/linter/lints/rfc/lint_cert_via_pkilint.go deleted file mode 100644 index 6a0dbd3d5..000000000 --- a/third-party/github.com/letsencrypt/boulder/linter/lints/rfc/lint_cert_via_pkilint.go +++ /dev/null @@ -1,156 +0,0 @@ -package rfc - -import ( - "bytes" - "context" - "encoding/base64" - "encoding/json" - "fmt" - "io" - "net/http" - "slices" - "strings" - "time" - - "github.com/zmap/zcrypto/x509" - "github.com/zmap/zlint/v3/lint" - "github.com/zmap/zlint/v3/util" -) - -type certViaPKILint struct { - PKILintAddr string `toml:"pkilint_addr" comment:"The address where a pkilint REST API can be reached."` - PKILintTimeout time.Duration `toml:"pkilint_timeout" comment:"How long, in nanoseconds, to wait before giving up."` - IgnoreLints []string `toml:"ignore_lints" comment:"The unique Validator:Code IDs of lint findings which should be ignored."` -} - -func init() { - lint.RegisterCertificateLint(&lint.CertificateLint{ - LintMetadata: lint.LintMetadata{ - Name: "e_pkilint_lint_cabf_serverauth_cert", - Description: "Runs pkilint's suite of cabf serverauth certificate lints", - Citation: "https://github.com/digicert/pkilint", - Source: lint.Community, - EffectiveDate: util.CABEffectiveDate, - }, - Lint: NewCertValidityNotRound, - }) -} - -func NewCertValidityNotRound() lint.CertificateLintInterface { - return &certViaPKILint{} -} - -func (l *certViaPKILint) Configure() interface{} { - return l -} - -func (l *certViaPKILint) CheckApplies(c *x509.Certificate) bool { - // This lint applies to all certificates issued by Boulder, as long as it has - // been configured with an address to reach out to. If not, skip it. - return l.PKILintAddr != "" -} - -type PKILintResponse struct { - Results []struct { - Validator string `json:"validator"` - NodePath string `json:"node_path"` - FindingDescriptions []struct { - Severity string `json:"severity"` - Code string `json:"code"` - Message string `json:"message,omitempty"` - } `json:"finding_descriptions"` - } `json:"results"` - Linter struct { - Name string `json:"name"` - } `json:"linter"` -} - -func (l *certViaPKILint) Execute(c *x509.Certificate) *lint.LintResult { - timeout := l.PKILintTimeout - if timeout == 0 { - timeout = 100 * time.Millisecond - } - - ctx, cancel := context.WithTimeout(context.Background(), timeout) - defer cancel() - - reqJSON, err := json.Marshal(struct { - B64 string `json:"b64"` - }{ - B64: base64.StdEncoding.EncodeToString(c.Raw), - }) - if err != nil { - return &lint.LintResult{ - Status: lint.Error, - Details: fmt.Sprintf("marshalling pkilint request: %s", err), - } - } - - url := fmt.Sprintf("%s/certificate/cabf-serverauth", l.PKILintAddr) - req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(reqJSON)) - if err != nil { - return &lint.LintResult{ - Status: lint.Error, - Details: fmt.Sprintf("creating pkilint request: %s", err), - } - } - - resp, err := http.DefaultClient.Do(req) - if err != nil { - return &lint.LintResult{ - Status: lint.Error, - Details: fmt.Sprintf("making POST request to pkilint API: %s", err), - } - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return &lint.LintResult{ - Status: lint.Error, - Details: fmt.Sprintf("got status %d (%s) from pkilint API", resp.StatusCode, resp.Status), - } - } - - res, err := io.ReadAll(resp.Body) - if err != nil { - return &lint.LintResult{ - Status: lint.Error, - Details: fmt.Sprintf("reading response from pkilint API: %s", err), - } - } - - var jsonResult PKILintResponse - err = json.Unmarshal(res, &jsonResult) - if err != nil { - return &lint.LintResult{ - Status: lint.Error, - Details: fmt.Sprintf("parsing response from pkilint API: %s", err), - } - } - - var findings []string - for _, validator := range jsonResult.Results { - for _, finding := range validator.FindingDescriptions { - id := fmt.Sprintf("%s:%s", validator.Validator, finding.Code) - if slices.Contains(l.IgnoreLints, id) { - continue - } - desc := fmt.Sprintf("%s from %s at %s", finding.Severity, id, validator.NodePath) - if finding.Message != "" { - desc = fmt.Sprintf("%s: %s", desc, finding.Message) - } - findings = append(findings, desc) - } - } - - if len(findings) != 0 { - // Group the findings by severity, for human readers. - slices.Sort(findings) - return &lint.LintResult{ - Status: lint.Error, - Details: fmt.Sprintf("got %d lint findings from pkilint API: %s", len(findings), strings.Join(findings, "; ")), - } - } - - return &lint.LintResult{Status: lint.Pass} -} diff --git a/third-party/github.com/letsencrypt/boulder/linter/lints/rfc/lint_cert_via_pkimetal.go b/third-party/github.com/letsencrypt/boulder/linter/lints/rfc/lint_cert_via_pkimetal.go new file mode 100644 index 000000000..31fc08d81 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/linter/lints/rfc/lint_cert_via_pkimetal.go @@ -0,0 +1,158 @@ +package rfc + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "slices" + "strings" + "time" + + "github.com/zmap/zcrypto/x509" + "github.com/zmap/zlint/v3/lint" + "github.com/zmap/zlint/v3/util" +) + +// PKIMetalConfig and its execute method provide a shared basis for linting +// both certs and CRLs using PKIMetal. +type PKIMetalConfig struct { + Addr string `toml:"addr" comment:"The address where a pkilint REST API can be reached."` + Severity string `toml:"severity" comment:"The minimum severity of findings to report (meta, debug, info, notice, warning, error, bug, or fatal)."` + Timeout time.Duration `toml:"timeout" comment:"How long, in nanoseconds, to wait before giving up."` + IgnoreLints []string `toml:"ignore_lints" comment:"The unique Validator:Code IDs of lint findings which should be ignored."` +} + +func (pkim *PKIMetalConfig) execute(endpoint string, der []byte) (*lint.LintResult, error) { + timeout := pkim.Timeout + if timeout == 0 { + timeout = 100 * time.Millisecond + } + + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + apiURL, err := url.JoinPath(pkim.Addr, endpoint) + if err != nil { + return nil, fmt.Errorf("constructing pkimetal url: %w", err) + } + + // reqForm matches PKIMetal's documented form-urlencoded request format. It + // does not include the "profile" field, as its default value ("autodetect") + // is good for our purposes. + // https://github.com/pkimetal/pkimetal/blob/578ac224a7ca3775af51b47fce16c95753d9ac8d/doc/openapi.yaml#L179-L194 + reqForm := url.Values{} + reqForm.Set("b64input", base64.StdEncoding.EncodeToString(der)) + reqForm.Set("severity", pkim.Severity) + reqForm.Set("format", "json") + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, apiURL, strings.NewReader(reqForm.Encode())) + if err != nil { + return nil, fmt.Errorf("creating pkimetal request: %w", err) + } + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + req.Header.Add("Accept", "application/json") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, fmt.Errorf("making POST request to pkimetal API: %s (timeout %s)", err, timeout) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("got status %d (%s) from pkimetal API", resp.StatusCode, resp.Status) + } + + resJSON, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("reading response from pkimetal API: %s", err) + } + + // finding matches the repeated portion of PKIMetal's documented JSON response. + // https://github.com/pkimetal/pkimetal/blob/578ac224a7ca3775af51b47fce16c95753d9ac8d/doc/openapi.yaml#L201-L221 + type finding struct { + Linter string `json:"linter"` + Finding string `json:"finding"` + Severity string `json:"severity"` + Code string `json:"code"` + Field string `json:"field"` + } + + var res []finding + err = json.Unmarshal(resJSON, &res) + if err != nil { + return nil, fmt.Errorf("parsing response from pkimetal API: %s", err) + } + + var findings []string + for _, finding := range res { + var id string + if finding.Code != "" { + id = fmt.Sprintf("%s:%s", finding.Linter, finding.Code) + } else { + id = fmt.Sprintf("%s:%s", finding.Linter, strings.ReplaceAll(strings.ToLower(finding.Finding), " ", "_")) + } + if slices.Contains(pkim.IgnoreLints, id) { + continue + } + desc := fmt.Sprintf("%s from %s: %s", finding.Severity, id, finding.Finding) + findings = append(findings, desc) + } + + if len(findings) != 0 { + // Group the findings by severity, for human readers. + slices.Sort(findings) + return &lint.LintResult{ + Status: lint.Error, + Details: fmt.Sprintf("got %d lint findings from pkimetal API: %s", len(findings), strings.Join(findings, "; ")), + }, nil + } + + return &lint.LintResult{Status: lint.Pass}, nil +} + +type certViaPKIMetal struct { + PKIMetalConfig +} + +func init() { + lint.RegisterCertificateLint(&lint.CertificateLint{ + LintMetadata: lint.LintMetadata{ + Name: "e_pkimetal_lint_cabf_serverauth_cert", + Description: "Runs pkimetal's suite of cabf serverauth certificate lints", + Citation: "https://github.com/pkimetal/pkimetal", + Source: lint.Community, + EffectiveDate: util.CABEffectiveDate, + }, + Lint: NewCertViaPKIMetal, + }) +} + +func NewCertViaPKIMetal() lint.CertificateLintInterface { + return &certViaPKIMetal{} +} + +func (l *certViaPKIMetal) Configure() any { + return l +} + +func (l *certViaPKIMetal) CheckApplies(c *x509.Certificate) bool { + // This lint applies to all certificates issued by Boulder, as long as it has + // been configured with an address to reach out to. If not, skip it. + return l.Addr != "" +} + +func (l *certViaPKIMetal) Execute(c *x509.Certificate) *lint.LintResult { + res, err := l.execute("lintcert", c.Raw) + if err != nil { + return &lint.LintResult{ + Status: lint.Error, + Details: err.Error(), + } + } + + return res +} diff --git a/third-party/github.com/letsencrypt/boulder/linter/lints/rfc/lint_crl_via_pkimetal.go b/third-party/github.com/letsencrypt/boulder/linter/lints/rfc/lint_crl_via_pkimetal.go new file mode 100644 index 000000000..c927eebe5 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/linter/lints/rfc/lint_crl_via_pkimetal.go @@ -0,0 +1,50 @@ +package rfc + +import ( + "github.com/zmap/zcrypto/x509" + "github.com/zmap/zlint/v3/lint" + "github.com/zmap/zlint/v3/util" +) + +type crlViaPKIMetal struct { + PKIMetalConfig +} + +func init() { + lint.RegisterRevocationListLint(&lint.RevocationListLint{ + LintMetadata: lint.LintMetadata{ + Name: "e_pkimetal_lint_cabf_serverauth_crl", + Description: "Runs pkimetal's suite of cabf serverauth CRL lints", + Citation: "https://github.com/pkimetal/pkimetal", + Source: lint.Community, + EffectiveDate: util.CABEffectiveDate, + }, + Lint: NewCrlViaPKIMetal, + }) +} + +func NewCrlViaPKIMetal() lint.RevocationListLintInterface { + return &crlViaPKIMetal{} +} + +func (l *crlViaPKIMetal) Configure() any { + return l +} + +func (l *crlViaPKIMetal) CheckApplies(c *x509.RevocationList) bool { + // This lint applies to all CRLs issued by Boulder, as long as it has + // been configured with an address to reach out to. If not, skip it. + return l.Addr != "" +} + +func (l *crlViaPKIMetal) Execute(c *x509.RevocationList) *lint.LintResult { + res, err := l.execute("lintcrl", c.Raw) + if err != nil { + return &lint.LintResult{ + Status: lint.Error, + Details: err.Error(), + } + } + + return res +} diff --git a/third-party/github.com/letsencrypt/boulder/mail/mailer.go b/third-party/github.com/letsencrypt/boulder/mail/mailer.go deleted file mode 100644 index 31ebd40b1..000000000 --- a/third-party/github.com/letsencrypt/boulder/mail/mailer.go +++ /dev/null @@ -1,430 +0,0 @@ -package mail - -import ( - "bytes" - "crypto/rand" - "crypto/tls" - "crypto/x509" - "errors" - "fmt" - "io" - "math" - "math/big" - "mime/quotedprintable" - "net" - "net/mail" - "net/smtp" - "net/textproto" - "strconv" - "strings" - "syscall" - "time" - - "github.com/jmhodges/clock" - "github.com/prometheus/client_golang/prometheus" - - "github.com/letsencrypt/boulder/core" - blog "github.com/letsencrypt/boulder/log" -) - -type idGenerator interface { - generate() *big.Int -} - -var maxBigInt = big.NewInt(math.MaxInt64) - -type realSource struct{} - -func (s realSource) generate() *big.Int { - randInt, err := rand.Int(rand.Reader, maxBigInt) - if err != nil { - panic(err) - } - return randInt -} - -// Mailer is an interface that allows creating Conns. Implementations must -// be safe for concurrent use. -type Mailer interface { - Connect() (Conn, error) -} - -// Conn is an interface that allows sending mail. When you are done with a -// Conn, call Close(). Implementations are not required to be safe for -// concurrent use. -type Conn interface { - SendMail([]string, string, string) error - Close() error -} - -// connImpl represents a single connection to a mail server. It is not safe -// for concurrent use. -type connImpl struct { - config - client smtpClient -} - -// mailerImpl defines a mail transfer agent to use for sending mail. It is -// safe for concurrent us. -type mailerImpl struct { - config -} - -type config struct { - log blog.Logger - dialer dialer - from mail.Address - clk clock.Clock - csprgSource idGenerator - reconnectBase time.Duration - reconnectMax time.Duration - sendMailAttempts *prometheus.CounterVec -} - -type dialer interface { - Dial() (smtpClient, error) -} - -type smtpClient interface { - Mail(string) error - Rcpt(string) error - Data() (io.WriteCloser, error) - Reset() error - Close() error -} - -type dryRunClient struct { - log blog.Logger -} - -func (d dryRunClient) Dial() (smtpClient, error) { - return d, nil -} - -func (d dryRunClient) Mail(from string) error { - d.log.Debugf("MAIL FROM:<%s>", from) - return nil -} - -func (d dryRunClient) Rcpt(to string) error { - d.log.Debugf("RCPT TO:<%s>", to) - return nil -} - -func (d dryRunClient) Close() error { - return nil -} - -func (d dryRunClient) Data() (io.WriteCloser, error) { - return d, nil -} - -func (d dryRunClient) Write(p []byte) (n int, err error) { - for _, line := range strings.Split(string(p), "\n") { - d.log.Debugf("data: %s", line) - } - return len(p), nil -} - -func (d dryRunClient) Reset() (err error) { - d.log.Debugf("RESET") - return nil -} - -// New constructs a Mailer to represent an account on a particular mail -// transfer agent. -func New( - server, - port, - username, - password string, - rootCAs *x509.CertPool, - from mail.Address, - logger blog.Logger, - stats prometheus.Registerer, - reconnectBase time.Duration, - reconnectMax time.Duration) *mailerImpl { - - sendMailAttempts := prometheus.NewCounterVec(prometheus.CounterOpts{ - Name: "send_mail_attempts", - Help: "A counter of send mail attempts labelled by result", - }, []string{"result", "error"}) - stats.MustRegister(sendMailAttempts) - - return &mailerImpl{ - config: config{ - dialer: &dialerImpl{ - username: username, - password: password, - server: server, - port: port, - rootCAs: rootCAs, - }, - log: logger, - from: from, - clk: clock.New(), - csprgSource: realSource{}, - reconnectBase: reconnectBase, - reconnectMax: reconnectMax, - sendMailAttempts: sendMailAttempts, - }, - } -} - -// NewDryRun constructs a Mailer suitable for doing a dry run. It simply logs -// each command that would have been run, at debug level. -func NewDryRun(from mail.Address, logger blog.Logger) *mailerImpl { - return &mailerImpl{ - config: config{ - dialer: dryRunClient{logger}, - from: from, - clk: clock.New(), - csprgSource: realSource{}, - sendMailAttempts: prometheus.NewCounterVec(prometheus.CounterOpts{ - Name: "send_mail_attempts", - Help: "A counter of send mail attempts labelled by result", - }, []string{"result", "error"}), - }, - } -} - -func (c config) generateMessage(to []string, subject, body string) ([]byte, error) { - mid := c.csprgSource.generate() - now := c.clk.Now().UTC() - addrs := []string{} - for _, a := range to { - if !core.IsASCII(a) { - return nil, fmt.Errorf("Non-ASCII email address") - } - addrs = append(addrs, strconv.Quote(a)) - } - headers := []string{ - fmt.Sprintf("To: %s", strings.Join(addrs, ", ")), - fmt.Sprintf("From: %s", c.from.String()), - fmt.Sprintf("Subject: %s", subject), - fmt.Sprintf("Date: %s", now.Format(time.RFC822)), - fmt.Sprintf("Message-Id: <%s.%s.%s>", now.Format("20060102T150405"), mid.String(), c.from.Address), - "MIME-Version: 1.0", - "Content-Type: text/plain; charset=UTF-8", - "Content-Transfer-Encoding: quoted-printable", - } - for i := range headers[1:] { - // strip LFs - headers[i] = strings.Replace(headers[i], "\n", "", -1) - } - bodyBuf := new(bytes.Buffer) - mimeWriter := quotedprintable.NewWriter(bodyBuf) - _, err := mimeWriter.Write([]byte(body)) - if err != nil { - return nil, err - } - err = mimeWriter.Close() - if err != nil { - return nil, err - } - return []byte(fmt.Sprintf( - "%s\r\n\r\n%s\r\n", - strings.Join(headers, "\r\n"), - bodyBuf.String(), - )), nil -} - -func (c *connImpl) reconnect() { - for i := 0; ; i++ { - sleepDuration := core.RetryBackoff(i, c.reconnectBase, c.reconnectMax, 2) - c.log.Infof("sleeping for %s before reconnecting mailer", sleepDuration) - c.clk.Sleep(sleepDuration) - c.log.Info("attempting to reconnect mailer") - client, err := c.dialer.Dial() - if err != nil { - c.log.Warningf("reconnect error: %s", err) - continue - } - c.client = client - break - } - c.log.Info("reconnected successfully") -} - -// Connect opens a connection to the specified mail server. It must be called -// before SendMail. -func (m *mailerImpl) Connect() (Conn, error) { - client, err := m.dialer.Dial() - if err != nil { - return nil, err - } - return &connImpl{m.config, client}, nil -} - -type dialerImpl struct { - username, password, server, port string - rootCAs *x509.CertPool -} - -func (di *dialerImpl) Dial() (smtpClient, error) { - hostport := net.JoinHostPort(di.server, di.port) - var conn net.Conn - var err error - conn, err = tls.Dial("tcp", hostport, &tls.Config{ - RootCAs: di.rootCAs, - }) - if err != nil { - return nil, err - } - client, err := smtp.NewClient(conn, di.server) - if err != nil { - return nil, err - } - auth := smtp.PlainAuth("", di.username, di.password, di.server) - if err = client.Auth(auth); err != nil { - return nil, err - } - return client, nil -} - -// resetAndError resets the current mail transaction and then returns its -// argument as an error. If the reset command also errors, it combines both -// errors and returns them. Without this we would get `nested MAIL command`. -// https://github.com/letsencrypt/boulder/issues/3191 -func (c *connImpl) resetAndError(err error) error { - if err == io.EOF { - return err - } - if err2 := c.client.Reset(); err2 != nil { - return fmt.Errorf("%s (also, on sending RSET: %s)", err, err2) - } - return err -} - -func (c *connImpl) sendOne(to []string, subject, msg string) error { - if c.client == nil { - return errors.New("call Connect before SendMail") - } - body, err := c.generateMessage(to, subject, msg) - if err != nil { - return err - } - if err = c.client.Mail(c.from.String()); err != nil { - return err - } - for _, t := range to { - if err = c.client.Rcpt(t); err != nil { - return c.resetAndError(err) - } - } - w, err := c.client.Data() - if err != nil { - return c.resetAndError(err) - } - _, err = w.Write(body) - if err != nil { - return c.resetAndError(err) - } - err = w.Close() - if err != nil { - return c.resetAndError(err) - } - return nil -} - -// BadAddressSMTPError is returned by SendMail when the server rejects a message -// but for a reason that doesn't prevent us from continuing to send mail. The -// error message contains the error code and the error message returned from the -// server. -type BadAddressSMTPError struct { - Message string -} - -func (e BadAddressSMTPError) Error() string { - return e.Message -} - -// Based on reading of various SMTP documents these are a handful -// of errors we are likely to be able to continue sending mail after -// receiving. The majority of these errors boil down to 'bad address'. -var badAddressErrorCodes = map[int]bool{ - 401: true, // Invalid recipient - 422: true, // Recipient mailbox is full - 441: true, // Recipient server is not responding - 450: true, // User's mailbox is not available - 501: true, // Bad recipient address syntax - 510: true, // Invalid recipient - 511: true, // Invalid recipient - 513: true, // Address type invalid - 541: true, // Recipient rejected message - 550: true, // Non-existent address - 553: true, // Non-existent address -} - -// SendMail sends an email to the provided list of recipients. The email body -// is simple text. -func (c *connImpl) SendMail(to []string, subject, msg string) error { - var protoErr *textproto.Error - for { - err := c.sendOne(to, subject, msg) - if err == nil { - // If the error is nil, we sent the mail without issue. nice! - break - } else if err == io.EOF { - c.sendMailAttempts.WithLabelValues("failure", "EOF").Inc() - // If the error is an EOF, we should try to reconnect on a backoff - // schedule, sleeping between attempts. - c.reconnect() - // After reconnecting, loop around and try `sendOne` again. - continue - } else if errors.Is(err, syscall.ECONNRESET) { - c.sendMailAttempts.WithLabelValues("failure", "TCP RST").Inc() - // If the error is `syscall.ECONNRESET`, we should try to reconnect on a backoff - // schedule, sleeping between attempts. - c.reconnect() - // After reconnecting, loop around and try `sendOne` again. - continue - } else if errors.Is(err, syscall.EPIPE) { - // EPIPE also seems to be a common way to signal TCP RST. - c.sendMailAttempts.WithLabelValues("failure", "EPIPE").Inc() - c.reconnect() - continue - } else if errors.As(err, &protoErr) && protoErr.Code == 421 { - c.sendMailAttempts.WithLabelValues("failure", "SMTP 421").Inc() - /* - * If the error is an instance of `textproto.Error` with a SMTP error code, - * and that error code is 421 then treat this as a reconnect-able event. - * - * The SMTP RFC defines this error code as: - * 421 Service not available, closing transmission channel - * (This may be a reply to any command if the service knows it - * must shut down) - * - * In practice we see this code being used by our production SMTP server - * when the connection has gone idle for too long. For more information - * see issue #2249[0]. - * - * [0] - https://github.com/letsencrypt/boulder/issues/2249 - */ - c.reconnect() - // After reconnecting, loop around and try `sendOne` again. - continue - } else if errors.As(err, &protoErr) && badAddressErrorCodes[protoErr.Code] { - c.sendMailAttempts.WithLabelValues("failure", fmt.Sprintf("SMTP %d", protoErr.Code)).Inc() - return BadAddressSMTPError{fmt.Sprintf("%d: %s", protoErr.Code, protoErr.Msg)} - } else { - // If it wasn't an EOF error or a recoverable SMTP error it is unexpected and we - // return from SendMail() with the error - c.sendMailAttempts.WithLabelValues("failure", "unexpected").Inc() - return err - } - } - - c.sendMailAttempts.WithLabelValues("success", "").Inc() - return nil -} - -// Close closes the connection. -func (c *connImpl) Close() error { - err := c.client.Close() - if err != nil { - return err - } - c.client = nil - return nil -} diff --git a/third-party/github.com/letsencrypt/boulder/mail/mailer_test.go b/third-party/github.com/letsencrypt/boulder/mail/mailer_test.go deleted file mode 100644 index 241412051..000000000 --- a/third-party/github.com/letsencrypt/boulder/mail/mailer_test.go +++ /dev/null @@ -1,545 +0,0 @@ -package mail - -import ( - "bufio" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "crypto/tls" - "crypto/x509" - "fmt" - "math/big" - "net" - "net/mail" - "net/textproto" - "strings" - "testing" - "time" - - "github.com/jmhodges/clock" - - blog "github.com/letsencrypt/boulder/log" - "github.com/letsencrypt/boulder/metrics" - "github.com/letsencrypt/boulder/test" -) - -var ( - // These variables are populated by init(), and then referenced by setup() and - // listenForever(). smtpCert is the TLS certificate which will be served by - // the fake SMTP server, and smtpRoot is the issuer of that certificate which - // will be trusted by the SMTP client under test. - smtpRoot *x509.CertPool - smtpCert *tls.Certificate -) - -func init() { - // Populate the global smtpRoot and smtpCert variables. We use a single self - // signed cert for both, for ease of generation. It has to assert the name - // localhost to appease the mailer, which is connecting to localhost. - key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - fmt.Println(err) - template := x509.Certificate{ - DNSNames: []string{"localhost"}, - SerialNumber: big.NewInt(123), - NotBefore: time.Now().Add(-24 * time.Hour), - NotAfter: time.Now().Add(24 * time.Hour), - } - certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, key.Public(), key) - fmt.Println(err) - cert, err := x509.ParseCertificate(certDER) - fmt.Println(err) - - smtpRoot = x509.NewCertPool() - smtpRoot.AddCert(cert) - - smtpCert = &tls.Certificate{ - Certificate: [][]byte{certDER}, - PrivateKey: key, - Leaf: cert, - } -} - -type fakeSource struct{} - -func (f fakeSource) generate() *big.Int { - return big.NewInt(1991) -} - -func TestGenerateMessage(t *testing.T) { - fc := clock.NewFake() - fromAddress, _ := mail.ParseAddress("happy sender ") - log := blog.UseMock() - m := New("", "", "", "", nil, *fromAddress, log, metrics.NoopRegisterer, 0, 0) - m.clk = fc - m.csprgSource = fakeSource{} - messageBytes, err := m.generateMessage([]string{"recv@email.com"}, "test subject", "this is the body\n") - test.AssertNotError(t, err, "Failed to generate email body") - message := string(messageBytes) - fields := strings.Split(message, "\r\n") - test.AssertEquals(t, len(fields), 12) - fmt.Println(message) - test.AssertEquals(t, fields[0], "To: \"recv@email.com\"") - test.AssertEquals(t, fields[1], "From: \"happy sender\" ") - test.AssertEquals(t, fields[2], "Subject: test subject") - test.AssertEquals(t, fields[3], "Date: 01 Jan 70 00:00 UTC") - test.AssertEquals(t, fields[4], "Message-Id: <19700101T000000.1991.send@email.com>") - test.AssertEquals(t, fields[5], "MIME-Version: 1.0") - test.AssertEquals(t, fields[6], "Content-Type: text/plain; charset=UTF-8") - test.AssertEquals(t, fields[7], "Content-Transfer-Encoding: quoted-printable") - test.AssertEquals(t, fields[8], "") - test.AssertEquals(t, fields[9], "this is the body") -} - -func TestFailNonASCIIAddress(t *testing.T) { - log := blog.UseMock() - fromAddress, _ := mail.ParseAddress("send@email.com") - m := New("", "", "", "", nil, *fromAddress, log, metrics.NoopRegisterer, 0, 0) - _, err := m.generateMessage([]string{"遗憾@email.com"}, "test subject", "this is the body\n") - test.AssertError(t, err, "Allowed a non-ASCII to address incorrectly") -} - -func expect(t *testing.T, buf *bufio.Reader, expected string) error { - line, _, err := buf.ReadLine() - if err != nil { - t.Errorf("readline: %s expected: %s\n", err, expected) - return err - } - if string(line) != expected { - t.Errorf("Expected %s, got %s", expected, line) - return fmt.Errorf("Expected %s, got %s", expected, line) - } - return nil -} - -type connHandler func(int, *testing.T, net.Conn, *net.TCPConn) - -func listenForever(l *net.TCPListener, t *testing.T, handler connHandler) { - tlsConf := &tls.Config{ - Certificates: []tls.Certificate{*smtpCert}, - } - connID := 0 - for { - tcpConn, err := l.AcceptTCP() - if err != nil { - return - } - - tlsConn := tls.Server(tcpConn, tlsConf) - connID++ - go handler(connID, t, tlsConn, tcpConn) - } -} - -func authenticateClient(t *testing.T, conn net.Conn) { - buf := bufio.NewReader(conn) - // we can ignore write errors because any - // failures will be caught on the connecting - // side - _, _ = conn.Write([]byte("220 smtp.example.com ESMTP\n")) - err := expect(t, buf, "EHLO localhost") - if err != nil { - return - } - - _, _ = conn.Write([]byte("250-PIPELINING\n")) - _, _ = conn.Write([]byte("250-AUTH PLAIN LOGIN\n")) - _, _ = conn.Write([]byte("250 8BITMIME\n")) - // Base64 encoding of "\0user@example.com\0passwd" - err = expect(t, buf, "AUTH PLAIN AHVzZXJAZXhhbXBsZS5jb20AcGFzc3dk") - if err != nil { - return - } - _, _ = conn.Write([]byte("235 2.7.0 Authentication successful\n")) -} - -// The normal handler authenticates the client and then disconnects without -// further command processing. It is sufficient for TestConnect() -func normalHandler(connID int, t *testing.T, tlsConn net.Conn, tcpConn *net.TCPConn) { - defer func() { - err := tlsConn.Close() - if err != nil { - t.Errorf("conn.Close: %s", err) - } - }() - authenticateClient(t, tlsConn) -} - -// The disconnectHandler authenticates the client like the normalHandler but -// additionally processes an email flow (e.g. MAIL, RCPT and DATA commands). -// When the `connID` is <= `closeFirst` the connection is closed immediately -// after the MAIL command is received and prior to issuing a 250 response. If -// a `goodbyeMsg` is provided, it is written to the client immediately before -// closing. In this way the first `closeFirst` connections will not complete -// normally and can be tested for reconnection logic. -func disconnectHandler(closeFirst int, goodbyeMsg string) connHandler { - return func(connID int, t *testing.T, conn net.Conn, _ *net.TCPConn) { - defer func() { - err := conn.Close() - if err != nil { - t.Errorf("conn.Close: %s", err) - } - }() - authenticateClient(t, conn) - - buf := bufio.NewReader(conn) - err := expect(t, buf, "MAIL FROM:<> BODY=8BITMIME") - if err != nil { - return - } - - if connID <= closeFirst { - // If there was a `goodbyeMsg` specified, write it to the client before - // closing the connection. This is a good way to deliver a SMTP error - // before closing - if goodbyeMsg != "" { - _, _ = fmt.Fprintf(conn, "%s\r\n", goodbyeMsg) - t.Logf("Wrote goodbye msg: %s", goodbyeMsg) - } - t.Log("Cutting off client early") - return - } - _, _ = conn.Write([]byte("250 Sure. Go on. \r\n")) - - err = expect(t, buf, "RCPT TO:") - if err != nil { - return - } - _, _ = conn.Write([]byte("250 Tell Me More \r\n")) - - err = expect(t, buf, "DATA") - if err != nil { - return - } - _, _ = conn.Write([]byte("354 Cool Data\r\n")) - _, _ = conn.Write([]byte("250 Peace Out\r\n")) - } -} - -func badEmailHandler(messagesToProcess int) connHandler { - return func(_ int, t *testing.T, conn net.Conn, _ *net.TCPConn) { - defer func() { - err := conn.Close() - if err != nil { - t.Errorf("conn.Close: %s", err) - } - }() - authenticateClient(t, conn) - - buf := bufio.NewReader(conn) - err := expect(t, buf, "MAIL FROM:<> BODY=8BITMIME") - if err != nil { - return - } - - _, _ = conn.Write([]byte("250 Sure. Go on. \r\n")) - - err = expect(t, buf, "RCPT TO:") - if err != nil { - return - } - _, _ = conn.Write([]byte("401 4.1.3 Bad recipient address syntax\r\n")) - err = expect(t, buf, "RSET") - if err != nil { - return - } - _, _ = conn.Write([]byte("250 Ok yr rset now\r\n")) - } -} - -// The rstHandler authenticates the client like the normalHandler but -// additionally processes an email flow (e.g. MAIL, RCPT and DATA -// commands). When the `connID` is <= `rstFirst` the socket of the -// listening connection is set to abruptively close (sends TCP RST but -// no FIN). The listening connection is closed immediately after the -// MAIL command is received and prior to issuing a 250 response. In this -// way the first `rstFirst` connections will not complete normally and -// can be tested for reconnection logic. -func rstHandler(rstFirst int) connHandler { - return func(connID int, t *testing.T, tlsConn net.Conn, tcpConn *net.TCPConn) { - defer func() { - err := tcpConn.Close() - if err != nil { - t.Errorf("conn.Close: %s", err) - } - }() - authenticateClient(t, tlsConn) - - buf := bufio.NewReader(tlsConn) - err := expect(t, buf, "MAIL FROM:<> BODY=8BITMIME") - if err != nil { - return - } - // Set the socket of the listening connection to abruptively - // close. - if connID <= rstFirst { - err := tcpConn.SetLinger(0) - if err != nil { - t.Error(err) - return - } - t.Log("Socket set for abruptive close. Cutting off client early") - return - } - _, _ = tlsConn.Write([]byte("250 Sure. Go on. \r\n")) - - err = expect(t, buf, "RCPT TO:") - if err != nil { - return - } - _, _ = tlsConn.Write([]byte("250 Tell Me More \r\n")) - - err = expect(t, buf, "DATA") - if err != nil { - return - } - _, _ = tlsConn.Write([]byte("354 Cool Data\r\n")) - _, _ = tlsConn.Write([]byte("250 Peace Out\r\n")) - } -} - -func setup(t *testing.T) (*mailerImpl, *net.TCPListener, func()) { - fromAddress, _ := mail.ParseAddress("you-are-a-winner@example.com") - log := blog.UseMock() - - // Listen on port 0 to get any free available port - tcpAddr, err := net.ResolveTCPAddr("tcp", ":0") - if err != nil { - t.Fatalf("resolving tcp addr: %s", err) - } - tcpl, err := net.ListenTCP("tcp", tcpAddr) - if err != nil { - t.Fatalf("listen: %s", err) - } - - cleanUp := func() { - err := tcpl.Close() - if err != nil { - t.Errorf("listen.Close: %s", err) - } - } - - // We can look at the listener Addr() to figure out which free port was - // assigned by the operating system - - _, port, err := net.SplitHostPort(tcpl.Addr().String()) - if err != nil { - t.Fatal("failed parsing port from tcp listen") - } - - m := New( - "localhost", - port, - "user@example.com", - "passwd", - smtpRoot, - *fromAddress, - log, - metrics.NoopRegisterer, - time.Second*2, time.Second*10) - - return m, tcpl, cleanUp -} - -func TestConnect(t *testing.T) { - m, l, cleanUp := setup(t) - defer cleanUp() - - go listenForever(l, t, normalHandler) - conn, err := m.Connect() - if err != nil { - t.Errorf("Failed to connect: %s", err) - } - err = conn.Close() - if err != nil { - t.Errorf("Failed to clean up: %s", err) - } -} - -func TestReconnectSuccess(t *testing.T) { - m, l, cleanUp := setup(t) - defer cleanUp() - const closedConns = 5 - - // Configure a test server that will disconnect the first `closedConns` - // connections after the MAIL cmd - go listenForever(l, t, disconnectHandler(closedConns, "")) - - // With a mailer client that has a max attempt > `closedConns` we expect no - // error. The message should be delivered after `closedConns` reconnect - // attempts. - conn, err := m.Connect() - if err != nil { - t.Errorf("Failed to connect: %s", err) - } - err = conn.SendMail([]string{"hi@bye.com"}, "You are already a winner!", "Just kidding") - if err != nil { - t.Errorf("Expected SendMail() to not fail. Got err: %s", err) - } -} - -func TestBadEmailError(t *testing.T) { - m, l, cleanUp := setup(t) - defer cleanUp() - const messages = 3 - - go listenForever(l, t, badEmailHandler(messages)) - - conn, err := m.Connect() - if err != nil { - t.Errorf("Failed to connect: %s", err) - } - - err = conn.SendMail([]string{"hi@bye.com"}, "You are already a winner!", "Just kidding") - // We expect there to be an error - if err == nil { - t.Errorf("Expected SendMail() to return an BadAddressSMTPError, got nil") - } - expected := "401: 4.1.3 Bad recipient address syntax" - var badAddrErr BadAddressSMTPError - test.AssertErrorWraps(t, err, &badAddrErr) - test.AssertEquals(t, badAddrErr.Message, expected) -} - -func TestReconnectSMTP421(t *testing.T) { - m, l, cleanUp := setup(t) - defer cleanUp() - const closedConns = 5 - - // A SMTP 421 can be generated when the server times out an idle connection. - // For more information see https://github.com/letsencrypt/boulder/issues/2249 - smtp421 := "421 1.2.3 green.eggs.and.spam Error: timeout exceeded" - - // Configure a test server that will disconnect the first `closedConns` - // connections after the MAIL cmd with a SMTP 421 error - go listenForever(l, t, disconnectHandler(closedConns, smtp421)) - - // With a mailer client that has a max attempt > `closedConns` we expect no - // error. The message should be delivered after `closedConns` reconnect - // attempts. - conn, err := m.Connect() - if err != nil { - t.Errorf("Failed to connect: %s", err) - } - err = conn.SendMail([]string{"hi@bye.com"}, "You are already a winner!", "Just kidding") - if err != nil { - t.Errorf("Expected SendMail() to not fail. Got err: %s", err) - } -} - -func TestOtherError(t *testing.T) { - m, l, cleanUp := setup(t) - defer cleanUp() - - go listenForever(l, t, func(_ int, t *testing.T, conn net.Conn, _ *net.TCPConn) { - defer func() { - err := conn.Close() - if err != nil { - t.Errorf("conn.Close: %s", err) - } - }() - authenticateClient(t, conn) - - buf := bufio.NewReader(conn) - err := expect(t, buf, "MAIL FROM:<> BODY=8BITMIME") - if err != nil { - return - } - - _, _ = conn.Write([]byte("250 Sure. Go on. \r\n")) - - err = expect(t, buf, "RCPT TO:") - if err != nil { - return - } - - _, _ = conn.Write([]byte("999 1.1.1 This would probably be bad?\r\n")) - - err = expect(t, buf, "RSET") - if err != nil { - return - } - - _, _ = conn.Write([]byte("250 Ok yr rset now\r\n")) - }) - - conn, err := m.Connect() - if err != nil { - t.Errorf("Failed to connect: %s", err) - } - - err = conn.SendMail([]string{"hi@bye.com"}, "You are already a winner!", "Just kidding") - // We expect there to be an error - if err == nil { - t.Errorf("Expected SendMail() to return an error, got nil") - } - expected := "999 1.1.1 This would probably be bad?" - var rcptErr *textproto.Error - test.AssertErrorWraps(t, err, &rcptErr) - test.AssertEquals(t, rcptErr.Error(), expected) - - m, l, cleanUp = setup(t) - defer cleanUp() - - go listenForever(l, t, func(_ int, t *testing.T, conn net.Conn, _ *net.TCPConn) { - defer func() { - err := conn.Close() - if err != nil { - t.Errorf("conn.Close: %s", err) - } - }() - authenticateClient(t, conn) - - buf := bufio.NewReader(conn) - err := expect(t, buf, "MAIL FROM:<> BODY=8BITMIME") - if err != nil { - return - } - - _, _ = conn.Write([]byte("250 Sure. Go on. \r\n")) - - err = expect(t, buf, "RCPT TO:") - if err != nil { - return - } - - _, _ = conn.Write([]byte("999 1.1.1 This would probably be bad?\r\n")) - - err = expect(t, buf, "RSET") - if err != nil { - return - } - - _, _ = conn.Write([]byte("nop\r\n")) - }) - conn, err = m.Connect() - if err != nil { - t.Errorf("Failed to connect: %s", err) - } - - err = conn.SendMail([]string{"hi@bye.com"}, "You are already a winner!", "Just kidding") - // We expect there to be an error - test.AssertError(t, err, "SendMail didn't fail as expected") - test.AssertEquals(t, err.Error(), "999 1.1.1 This would probably be bad? (also, on sending RSET: short response: nop)") -} - -func TestReconnectAfterRST(t *testing.T) { - m, l, cleanUp := setup(t) - defer cleanUp() - const rstConns = 5 - - // Configure a test server that will RST and disconnect the first - // `closedConns` connections - go listenForever(l, t, rstHandler(rstConns)) - - // With a mailer client that has a max attempt > `closedConns` we expect no - // error. The message should be delivered after `closedConns` reconnect - // attempts. - conn, err := m.Connect() - if err != nil { - t.Errorf("Failed to connect: %s", err) - } - err = conn.SendMail([]string{"hi@bye.com"}, "You are already a winner!", "Just kidding") - if err != nil { - t.Errorf("Expected SendMail() to not fail. Got err: %s", err) - } -} diff --git a/third-party/github.com/letsencrypt/boulder/metrics/measured_http/http.go b/third-party/github.com/letsencrypt/boulder/metrics/measured_http/http.go index ecd50b284..5367747b7 100644 --- a/third-party/github.com/letsencrypt/boulder/metrics/measured_http/http.go +++ b/third-party/github.com/letsencrypt/boulder/metrics/measured_http/http.go @@ -45,6 +45,9 @@ type MeasuredHandler struct { clk clock.Clock // Normally this is always responseTime, but we override it for testing. stat *prometheus.HistogramVec + // inFlightRequestsGauge is a gauge that tracks the number of requests + // currently in flight, labeled by endpoint. + inFlightRequestsGauge *prometheus.GaugeVec } func New(m serveMux, clk clock.Clock, stats prometheus.Registerer, opts ...otelhttp.Option) http.Handler { @@ -55,10 +58,21 @@ func New(m serveMux, clk clock.Clock, stats prometheus.Registerer, opts ...otelh }, []string{"endpoint", "method", "code"}) stats.MustRegister(responseTime) + + inFlightRequestsGauge := prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "in_flight_requests", + Help: "Tracks the number of WFE requests currently in flight, labeled by endpoint.", + }, + []string{"endpoint"}, + ) + stats.MustRegister(inFlightRequestsGauge) + return otelhttp.NewHandler(&MeasuredHandler{ - serveMux: m, - clk: clk, - stat: responseTime, + serveMux: m, + clk: clk, + stat: responseTime, + inFlightRequestsGauge: inFlightRequestsGauge, }, "server", opts...) } @@ -66,6 +80,10 @@ func (h *MeasuredHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { begin := h.clk.Now() rwws := &responseWriterWithStatus{w, 0} + subHandler, pattern := h.Handler(r) + h.inFlightRequestsGauge.WithLabelValues(pattern).Inc() + defer h.inFlightRequestsGauge.WithLabelValues(pattern).Dec() + // Use the method string only if it's a recognized HTTP method. This avoids // ballooning timeseries with invalid methods from public input. var method string @@ -78,7 +96,6 @@ func (h *MeasuredHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { method = "unknown" } - subHandler, pattern := h.Handler(r) defer func() { h.stat.With(prometheus.Labels{ "endpoint": pattern, diff --git a/third-party/github.com/letsencrypt/boulder/metrics/measured_http/http_test.go b/third-party/github.com/letsencrypt/boulder/metrics/measured_http/http_test.go index ee435c353..6f836250c 100644 --- a/third-party/github.com/letsencrypt/boulder/metrics/measured_http/http_test.go +++ b/third-party/github.com/letsencrypt/boulder/metrics/measured_http/http_test.go @@ -42,12 +42,21 @@ func TestMeasuring(t *testing.T) { }, []string{"endpoint", "method", "code"}) + inFlightRequestsGauge := prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "in_flight_requests", + Help: "Tracks the number of WFE requests currently in flight, labeled by endpoint.", + }, + []string{"endpoint"}, + ) + mux := http.NewServeMux() mux.Handle("/foo", sleepyHandler{clk}) mh := MeasuredHandler{ - serveMux: mux, - clk: clk, - stat: stat, + serveMux: mux, + clk: clk, + stat: stat, + inFlightRequestsGauge: inFlightRequestsGauge, } mh.ServeHTTP(httptest.NewRecorder(), &http.Request{ URL: &url.URL{Path: "/foo"}, @@ -95,13 +104,21 @@ func TestUnknownMethod(t *testing.T) { Help: "fake", }, []string{"endpoint", "method", "code"}) + inFlightRequestsGauge := prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "in_flight_requests", + Help: "Tracks the number of WFE requests currently in flight, labeled by endpoint.", + }, + []string{"endpoint"}, + ) mux := http.NewServeMux() mux.Handle("/foo", sleepyHandler{clk}) mh := MeasuredHandler{ - serveMux: mux, - clk: clk, - stat: stat, + serveMux: mux, + clk: clk, + stat: stat, + inFlightRequestsGauge: inFlightRequestsGauge, } mh.ServeHTTP(httptest.NewRecorder(), &http.Request{ URL: &url.URL{Path: "/foo"}, @@ -140,14 +157,22 @@ func TestWrite(t *testing.T) { }, []string{"endpoint", "method", "code"}) + inFlightRequestsGauge := prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "in_flight_requests", + Help: "Tracks the number of WFE requests currently in flight, labeled by endpoint.", + }, + []string{"endpoint"}) + mux := http.NewServeMux() mux.HandleFunc("/foo", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte{}) }) mh := MeasuredHandler{ - serveMux: mux, - clk: clk, - stat: stat, + serveMux: mux, + clk: clk, + stat: stat, + inFlightRequestsGauge: inFlightRequestsGauge, } mh.ServeHTTP(httptest.NewRecorder(), &http.Request{ URL: &url.URL{Path: "/foo"}, @@ -162,6 +187,7 @@ func TestWrite(t *testing.T) { }, []string{"endpoint", "method", "code"}) mh.stat = stat + mh.inFlightRequestsGauge = inFlightRequestsGauge expectedLabels := map[string]string{ "endpoint": "/foo", "method": "GET", diff --git a/third-party/github.com/letsencrypt/boulder/mocks/ca.go b/third-party/github.com/letsencrypt/boulder/mocks/ca.go index 929c204e7..6494d09fb 100644 --- a/third-party/github.com/letsencrypt/boulder/mocks/ca.go +++ b/third-party/github.com/letsencrypt/boulder/mocks/ca.go @@ -2,17 +2,13 @@ package mocks import ( "context" - "crypto/sha256" "crypto/x509" "encoding/pem" "fmt" - "time" "google.golang.org/grpc" - "google.golang.org/protobuf/types/known/timestamppb" capb "github.com/letsencrypt/boulder/ca/proto" - corepb "github.com/letsencrypt/boulder/core/proto" ) // MockCA is a mock of a CA that always returns the cert from PEM in response to @@ -21,37 +17,17 @@ type MockCA struct { PEM []byte } -// IssuePrecertificate is a mock -func (ca *MockCA) IssuePrecertificate(ctx context.Context, req *capb.IssueCertificateRequest, _ ...grpc.CallOption) (*capb.IssuePrecertificateResponse, error) { +// IssueCertificate is a mock +func (ca *MockCA) IssueCertificate(ctx context.Context, req *capb.IssueCertificateRequest, _ ...grpc.CallOption) (*capb.IssueCertificateResponse, error) { if ca.PEM == nil { return nil, fmt.Errorf("MockCA's PEM field must be set before calling IssueCertificate") } block, _ := pem.Decode(ca.PEM) - cert, err := x509.ParseCertificate(block.Bytes) + sampleDER, err := x509.ParseCertificate(block.Bytes) if err != nil { return nil, err } - profHash := sha256.Sum256([]byte(req.CertProfileName)) - return &capb.IssuePrecertificateResponse{ - DER: cert.Raw, - CertProfileHash: profHash[:8], - CertProfileName: req.CertProfileName, - }, nil -} - -// IssueCertificateForPrecertificate is a mock -func (ca *MockCA) IssueCertificateForPrecertificate(ctx context.Context, req *capb.IssueCertificateForPrecertificateRequest, _ ...grpc.CallOption) (*corepb.Certificate, error) { - now := time.Now() - expires := now.Add(1 * time.Hour) - - return &corepb.Certificate{ - Der: req.DER, - RegistrationID: 1, - Serial: "mock", - Digest: "mock", - Issued: timestamppb.New(now), - Expires: timestamppb.New(expires), - }, nil + return &capb.IssueCertificateResponse{DER: sampleDER.Raw}, nil } type MockOCSPGenerator struct{} diff --git a/third-party/github.com/letsencrypt/boulder/mocks/emailexporter.go b/third-party/github.com/letsencrypt/boulder/mocks/emailexporter.go new file mode 100644 index 000000000..070eff7c4 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/mocks/emailexporter.go @@ -0,0 +1,70 @@ +package mocks + +import ( + "context" + "sync" + + "google.golang.org/grpc" + "google.golang.org/protobuf/types/known/emptypb" + + "github.com/letsencrypt/boulder/email" + emailpb "github.com/letsencrypt/boulder/email/proto" +) + +// MockPardotClientImpl is a mock implementation of PardotClient. +type MockPardotClientImpl struct { + sync.Mutex + CreatedContacts []string +} + +// NewMockPardotClientImpl returns a emailPardotClient and a +// MockPardotClientImpl. Both refer to the same instance, with the interface for +// mock interaction and the struct for state inspection and modification. +func NewMockPardotClientImpl() (email.PardotClient, *MockPardotClientImpl) { + mockImpl := &MockPardotClientImpl{ + CreatedContacts: []string{}, + } + return mockImpl, mockImpl +} + +// SendContact adds an email to CreatedContacts. +func (m *MockPardotClientImpl) SendContact(email string) error { + m.Lock() + defer m.Unlock() + + m.CreatedContacts = append(m.CreatedContacts, email) + return nil +} + +// GetCreatedContacts is used for testing to retrieve the list of created +// contacts in a thread-safe manner. +func (m *MockPardotClientImpl) GetCreatedContacts() []string { + m.Lock() + defer m.Unlock() + // Return a copy to avoid race conditions. + return append([]string{}, m.CreatedContacts...) +} + +// MockExporterClientImpl is a mock implementation of ExporterClient. +type MockExporterClientImpl struct { + PardotClient email.PardotClient +} + +// NewMockExporterImpl returns a MockExporterClientImpl as an ExporterClient. +func NewMockExporterImpl(pardotClient email.PardotClient) emailpb.ExporterClient { + return &MockExporterClientImpl{ + PardotClient: pardotClient, + } +} + +// SendContacts submits emails to the inner PardotClient, returning an error if +// any fail. +func (m *MockExporterClientImpl) SendContacts(ctx context.Context, req *emailpb.SendContactsRequest, _ ...grpc.CallOption) (*emptypb.Empty, error) { + for _, e := range req.Emails { + err := m.PardotClient.SendContact(e) + if err != nil { + return nil, err + } + } + return &emptypb.Empty{}, nil +} diff --git a/third-party/github.com/letsencrypt/boulder/mocks/mailer.go b/third-party/github.com/letsencrypt/boulder/mocks/mailer.go deleted file mode 100644 index a6081aebb..000000000 --- a/third-party/github.com/letsencrypt/boulder/mocks/mailer.go +++ /dev/null @@ -1,60 +0,0 @@ -package mocks - -import ( - "sync" - - "github.com/letsencrypt/boulder/mail" -) - -// Mailer is a mock -type Mailer struct { - sync.Mutex - Messages []MailerMessage -} - -var _ mail.Mailer = &Mailer{} - -// mockMailerConn is a mock that satisfies the mail.Conn interface -type mockMailerConn struct { - parent *Mailer -} - -var _ mail.Conn = &mockMailerConn{} - -// MailerMessage holds the captured emails from SendMail() -type MailerMessage struct { - To string - Subject string - Body string -} - -// Clear removes any previously recorded messages -func (m *Mailer) Clear() { - m.Lock() - defer m.Unlock() - m.Messages = nil -} - -// SendMail is a mock -func (m *mockMailerConn) SendMail(to []string, subject, msg string) error { - m.parent.Lock() - defer m.parent.Unlock() - for _, rcpt := range to { - m.parent.Messages = append(m.parent.Messages, MailerMessage{ - To: rcpt, - Subject: subject, - Body: msg, - }) - } - return nil -} - -// Close is a mock -func (m *mockMailerConn) Close() error { - return nil -} - -// Connect is a mock -func (m *Mailer) Connect() (mail.Conn, error) { - return &mockMailerConn{parent: m}, nil -} diff --git a/third-party/github.com/letsencrypt/boulder/mocks/sa.go b/third-party/github.com/letsencrypt/boulder/mocks/sa.go index 032378d78..a982a8047 100644 --- a/third-party/github.com/letsencrypt/boulder/mocks/sa.go +++ b/third-party/github.com/letsencrypt/boulder/mocks/sa.go @@ -5,9 +5,7 @@ import ( "context" "crypto/x509" "errors" - "fmt" - "math/rand" - "net" + "math/rand/v2" "os" "time" @@ -76,12 +74,11 @@ func (sa *StorageAuthorityReadOnly) GetRegistration(_ context.Context, req *sapb } goodReg := &corepb.Registration{ - Id: req.Id, - Key: []byte(test1KeyPublicJSON), - Agreement: agreementURL, - Contact: []string{"mailto:person@mail.com"}, - ContactsPresent: true, - Status: string(core.StatusValid), + Id: req.Id, + Key: []byte(test1KeyPublicJSON), + Agreement: agreementURL, + Contact: []string{"mailto:person@mail.com"}, + Status: string(core.StatusValid), } // Return a populated registration with contacts for ID == 1 or ID == 5 @@ -114,7 +111,6 @@ func (sa *StorageAuthorityReadOnly) GetRegistration(_ context.Context, req *sapb return goodReg, nil } - goodReg.InitialIP, _ = net.ParseIP("5.6.7.8").MarshalText() goodReg.CreatedAt = timestamppb.New(time.Date(2003, 9, 27, 0, 0, 0, 0, time.UTC)) return goodReg, nil } @@ -139,12 +135,11 @@ func (sa *StorageAuthorityReadOnly) GetRegistrationByKey(_ context.Context, req if bytes.Equal(req.Jwk, []byte(test1KeyPublicJSON)) { return &corepb.Registration{ - Id: 1, - Key: req.Jwk, - Agreement: agreementURL, - Contact: contacts, - ContactsPresent: true, - Status: string(core.StatusValid), + Id: 1, + Key: req.Jwk, + Agreement: agreementURL, + Contact: contacts, + Status: string(core.StatusValid), }, nil } @@ -174,12 +169,11 @@ func (sa *StorageAuthorityReadOnly) GetRegistrationByKey(_ context.Context, req if bytes.Equal(req.Jwk, []byte(test3KeyPublicJSON)) { // deactivated registration return &corepb.Registration{ - Id: 2, - Key: req.Jwk, - Agreement: agreementURL, - Contact: contacts, - ContactsPresent: true, - Status: string(core.StatusDeactivated), + Id: 2, + Key: req.Jwk, + Agreement: agreementURL, + Contact: contacts, + Status: string(core.StatusDeactivated), }, nil } @@ -226,7 +220,6 @@ func (sa *StorageAuthorityReadOnly) GetCertificateStatus(_ context.Context, req func (sa *StorageAuthorityReadOnly) SetCertificateStatusReady(ctx context.Context, req *sapb.Serial, _ ...grpc.CallOption) (*emptypb.Empty, error) { return nil, status.Error(codes.Unimplemented, "unimplemented mock") - } // GetRevocationStatus is a mock @@ -274,11 +267,41 @@ func (sa *StorageAuthority) GetRevokedCerts(ctx context.Context, _ *sapb.GetRevo return &ServerStreamClient[corepb.CRLEntry]{}, nil } +// GetRevokedCertsByShard is a mock +func (sa *StorageAuthorityReadOnly) GetRevokedCertsByShard(ctx context.Context, _ *sapb.GetRevokedCertsByShardRequest, _ ...grpc.CallOption) (grpc.ServerStreamingClient[corepb.CRLEntry], error) { + return &ServerStreamClient[corepb.CRLEntry]{}, nil +} + // GetMaxExpiration is a mock func (sa *StorageAuthorityReadOnly) GetMaxExpiration(_ context.Context, req *emptypb.Empty, _ ...grpc.CallOption) (*timestamppb.Timestamp, error) { return nil, nil } +// AddRateLimitOverride is a mock +func (sa *StorageAuthority) AddRateLimitOverride(_ context.Context, req *sapb.AddRateLimitOverrideRequest, _ ...grpc.CallOption) (*sapb.AddRateLimitOverrideResponse, error) { + return nil, nil +} + +// DisableRateLimitOverride is a mock +func (sa *StorageAuthority) DisableRateLimitOverride(ctx context.Context, req *sapb.DisableRateLimitOverrideRequest) (*emptypb.Empty, error) { + return nil, nil +} + +// EnableRateLimitOverride is a mock +func (sa *StorageAuthority) EnableRateLimitOverride(ctx context.Context, req *sapb.EnableRateLimitOverrideRequest) (*emptypb.Empty, error) { + return nil, nil +} + +// GetRateLimitOverride is a mock +func (sa *StorageAuthorityReadOnly) GetRateLimitOverride(_ context.Context, req *sapb.GetRateLimitOverrideRequest, _ ...grpc.CallOption) (*sapb.RateLimitOverrideResponse, error) { + return nil, nil +} + +// GetEnabledRateLimitOverrides is a mock +func (sa *StorageAuthorityReadOnly) GetEnabledRateLimitOverrides(_ context.Context, _ *emptypb.Empty, _ ...grpc.CallOption) (sapb.StorageAuthorityReadOnly_GetEnabledRateLimitOverridesClient, error) { + return nil, nil +} + // AddPrecertificate is a mock func (sa *StorageAuthority) AddPrecertificate(ctx context.Context, req *sapb.AddCertificateRequest, _ ...grpc.CallOption) (*emptypb.Empty, error) { return nil, nil @@ -304,11 +327,6 @@ func (sa *StorageAuthority) UpdateRegistration(_ context.Context, _ *corepb.Regi return &emptypb.Empty{}, nil } -// CountFQDNSets is a mock -func (sa *StorageAuthorityReadOnly) CountFQDNSets(_ context.Context, _ *sapb.CountFQDNSetsRequest, _ ...grpc.CallOption) (*sapb.Count, error) { - return &sapb.Count{}, nil -} - // FQDNSetTimestampsForWindow is a mock func (sa *StorageAuthorityReadOnly) FQDNSetTimestampsForWindow(_ context.Context, _ *sapb.CountFQDNSetsRequest, _ ...grpc.CallOption) (*sapb.Timestamps, error) { return &sapb.Timestamps{}, nil @@ -319,26 +337,6 @@ func (sa *StorageAuthorityReadOnly) FQDNSetExists(_ context.Context, _ *sapb.FQD return &sapb.Exists{Exists: false}, nil } -// CountCertificatesByNames is a mock -func (sa *StorageAuthorityReadOnly) CountCertificatesByNames(_ context.Context, _ *sapb.CountCertificatesByNamesRequest, _ ...grpc.CallOption) (*sapb.CountByNames, error) { - return &sapb.CountByNames{}, nil -} - -// CountRegistrationsByIP is a mock -func (sa *StorageAuthorityReadOnly) CountRegistrationsByIP(_ context.Context, _ *sapb.CountRegistrationsByIPRequest, _ ...grpc.CallOption) (*sapb.Count, error) { - return &sapb.Count{}, nil -} - -// CountRegistrationsByIPRange is a mock -func (sa *StorageAuthorityReadOnly) CountRegistrationsByIPRange(_ context.Context, _ *sapb.CountRegistrationsByIPRequest, _ ...grpc.CallOption) (*sapb.Count, error) { - return &sapb.Count{}, nil -} - -// CountOrders is a mock -func (sa *StorageAuthorityReadOnly) CountOrders(_ context.Context, _ *sapb.CountOrdersRequest, _ ...grpc.CallOption) (*sapb.Count, error) { - return &sapb.Count{}, nil -} - // DeactivateRegistration is a mock func (sa *StorageAuthority) DeactivateRegistration(_ context.Context, _ *sapb.RegistrationID, _ ...grpc.CallOption) (*emptypb.Empty, error) { return &emptypb.Empty{}, nil @@ -350,10 +348,10 @@ func (sa *StorageAuthority) NewOrderAndAuthzs(_ context.Context, req *sapb.NewOr // Fields from the input new order request. RegistrationID: req.NewOrder.RegistrationID, Expires: req.NewOrder.Expires, - Names: req.NewOrder.Names, + Identifiers: req.NewOrder.Identifiers, V2Authorizations: req.NewOrder.V2Authorizations, // Mock new fields generated by the database transaction. - Id: rand.Int63(), + Id: rand.Int64(), Created: timestamppb.Now(), // A new order is never processing because it can't have been finalized yet. BeganProcessing: false, @@ -394,12 +392,12 @@ func (sa *StorageAuthorityReadOnly) GetOrder(_ context.Context, req *sapb.OrderR RegistrationID: 1, Created: timestamppb.New(created), Expires: timestamppb.New(exp), - Names: []string{"example.com"}, + Identifiers: []*corepb.Identifier{identifier.NewDNS("example.com").ToProto()}, Status: string(core.StatusValid), V2Authorizations: []int64{1}, CertificateSerial: "serial", Error: nil, - CertificateProfileName: "defaultBoulderCertificateProfile", + CertificateProfileName: "default", } // Order ID doesn't have a certificate serial yet @@ -468,34 +466,28 @@ func (sa *StorageAuthorityReadOnly) GetValidAuthorizations2(ctx context.Context, if req.RegistrationID != 1 && req.RegistrationID != 5 && req.RegistrationID != 4 { return &sapb.Authorizations{}, nil } - now := req.Now.AsTime() + expiryCutoff := req.ValidUntil.AsTime() auths := &sapb.Authorizations{} - for _, name := range req.Domains { - exp := now.AddDate(100, 0, 0) + for _, ident := range req.Identifiers { + exp := expiryCutoff.AddDate(100, 0, 0) authzPB, err := bgrpc.AuthzToPB(core.Authorization{ Status: core.StatusValid, RegistrationID: req.RegistrationID, Expires: &exp, - Identifier: identifier.ACMEIdentifier{ - Type: identifier.DNS, - Value: name, - }, + Identifier: identifier.FromProto(ident), Challenges: []core.Challenge{ { Status: core.StatusValid, Type: core.ChallengeTypeDNS01, Token: "exampleToken", - Validated: &now, + Validated: &expiryCutoff, }, }, }) if err != nil { return nil, err } - auths.Authz = append(auths.Authz, &sapb.Authorizations_MapElement{ - Domain: name, - Authz: authzPB, - }) + auths.Authzs = append(auths.Authzs, authzPB) } return auths, nil } @@ -504,61 +496,9 @@ func (sa *StorageAuthorityReadOnly) GetAuthorizations2(ctx context.Context, req return &sapb.Authorizations{}, nil } -func (sa *StorageAuthorityReadOnly) GetPendingAuthorization2(ctx context.Context, req *sapb.GetPendingAuthorizationRequest, _ ...grpc.CallOption) (*corepb.Authorization, error) { - return nil, nil -} - -var ( - authzIdValid = int64(1) - authzIdPending = int64(2) - authzIdExpired = int64(3) - authzIdErrorResult = int64(4) - authzIdDiffAccount = int64(5) -) - // GetAuthorization2 is a mock func (sa *StorageAuthorityReadOnly) GetAuthorization2(ctx context.Context, id *sapb.AuthorizationID2, _ ...grpc.CallOption) (*corepb.Authorization, error) { - authz := core.Authorization{ - Status: core.StatusValid, - RegistrationID: 1, - Identifier: identifier.DNSIdentifier("not-an-example.com"), - Challenges: []core.Challenge{ - { - Status: "pending", - Token: "token", - Type: "dns", - }, - }, - } - - switch id.Id { - case authzIdValid: - exp := sa.clk.Now().AddDate(100, 0, 0) - authz.Expires = &exp - authz.ID = fmt.Sprintf("%d", authzIdValid) - return bgrpc.AuthzToPB(authz) - case authzIdPending: - exp := sa.clk.Now().AddDate(100, 0, 0) - authz.Expires = &exp - authz.ID = fmt.Sprintf("%d", authzIdPending) - authz.Status = core.StatusPending - return bgrpc.AuthzToPB(authz) - case authzIdExpired: - exp := sa.clk.Now().AddDate(0, -1, 0) - authz.Expires = &exp - authz.ID = fmt.Sprintf("%d", authzIdExpired) - return bgrpc.AuthzToPB(authz) - case authzIdErrorResult: - return nil, fmt.Errorf("unspecified database error") - case authzIdDiffAccount: - exp := sa.clk.Now().AddDate(100, 0, 0) - authz.RegistrationID = 2 - authz.Expires = &exp - authz.ID = fmt.Sprintf("%d", authzIdDiffAccount) - return bgrpc.AuthzToPB(authz) - } - - return nil, berrors.NotFoundError("no authorization found with id %q", id) + return &corepb.Authorization{}, nil } // GetSerialsByKey is a mock diff --git a/third-party/github.com/letsencrypt/boulder/nonce/nonce.go b/third-party/github.com/letsencrypt/boulder/nonce/nonce.go index 388ab62d0..dae37ba3e 100644 --- a/third-party/github.com/letsencrypt/boulder/nonce/nonce.go +++ b/third-party/github.com/letsencrypt/boulder/nonce/nonce.go @@ -55,8 +55,8 @@ type HMACKeyCtxKey struct{} // DerivePrefix derives a nonce prefix from the provided listening address and // key. The prefix is derived by take the first 8 characters of the base64url // encoded HMAC-SHA256 hash of the listening address using the provided key. -func DerivePrefix(grpcAddr, key string) string { - h := hmac.New(sha256.New, []byte(key)) +func DerivePrefix(grpcAddr string, key []byte) string { + h := hmac.New(sha256.New, key) h.Write([]byte(grpcAddr)) return base64.RawURLEncoding.EncodeToString(h.Sum(nil))[:PrefixLen] } diff --git a/third-party/github.com/letsencrypt/boulder/nonce/nonce_test.go b/third-party/github.com/letsencrypt/boulder/nonce/nonce_test.go index db515d2a3..42b436491 100644 --- a/third-party/github.com/letsencrypt/boulder/nonce/nonce_test.go +++ b/third-party/github.com/letsencrypt/boulder/nonce/nonce_test.go @@ -147,6 +147,6 @@ func TestNoncePrefixValidation(t *testing.T) { } func TestDerivePrefix(t *testing.T) { - prefix := DerivePrefix("192.168.1.1:8080", "3b8c758dd85e113ea340ce0b3a99f389d40a308548af94d1730a7692c1874f1f") + prefix := DerivePrefix("192.168.1.1:8080", []byte("3b8c758dd85e113ea340ce0b3a99f389d40a308548af94d1730a7692c1874f1f")) test.AssertEquals(t, prefix, "P9qQaK4o") } diff --git a/third-party/github.com/letsencrypt/boulder/nonce/proto/nonce.pb.go b/third-party/github.com/letsencrypt/boulder/nonce/proto/nonce.pb.go index b500162f7..3ae86bd12 100644 --- a/third-party/github.com/letsencrypt/boulder/nonce/proto/nonce.pb.go +++ b/third-party/github.com/letsencrypt/boulder/nonce/proto/nonce.pb.go @@ -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: nonce.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 NonceMessage struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Nonce string `protobuf:"bytes,1,opt,name=nonce,proto3" json:"nonce,omitempty"` unknownFields protoimpl.UnknownFields - - Nonce string `protobuf:"bytes,1,opt,name=nonce,proto3" json:"nonce,omitempty"` + sizeCache protoimpl.SizeCache } func (x *NonceMessage) Reset() { *x = NonceMessage{} - if protoimpl.UnsafeEnabled { - mi := &file_nonce_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_nonce_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *NonceMessage) String() string { @@ -46,7 +44,7 @@ func (*NonceMessage) ProtoMessage() {} func (x *NonceMessage) ProtoReflect() protoreflect.Message { mi := &file_nonce_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) @@ -69,20 +67,17 @@ func (x *NonceMessage) GetNonce() string { } type ValidMessage struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Valid bool `protobuf:"varint,1,opt,name=valid,proto3" json:"valid,omitempty"` unknownFields protoimpl.UnknownFields - - Valid bool `protobuf:"varint,1,opt,name=valid,proto3" json:"valid,omitempty"` + sizeCache protoimpl.SizeCache } func (x *ValidMessage) Reset() { *x = ValidMessage{} - if protoimpl.UnsafeEnabled { - mi := &file_nonce_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_nonce_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ValidMessage) String() string { @@ -93,7 +88,7 @@ func (*ValidMessage) ProtoMessage() {} func (x *ValidMessage) ProtoReflect() protoreflect.Message { mi := &file_nonce_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) @@ -117,7 +112,7 @@ func (x *ValidMessage) GetValid() bool { var File_nonce_proto protoreflect.FileDescriptor -var file_nonce_proto_rawDesc = []byte{ +var file_nonce_proto_rawDesc = string([]byte{ 0x0a, 0x0b, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 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, 0x6f, 0x74, @@ -138,22 +133,22 @@ var file_nonce_proto_rawDesc = []byte{ 0x63, 0x72, 0x79, 0x70, 0x74, 0x2f, 0x62, 0x6f, 0x75, 0x6c, 0x64, 0x65, 0x72, 0x2f, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, -} +}) var ( file_nonce_proto_rawDescOnce sync.Once - file_nonce_proto_rawDescData = file_nonce_proto_rawDesc + file_nonce_proto_rawDescData []byte ) func file_nonce_proto_rawDescGZIP() []byte { file_nonce_proto_rawDescOnce.Do(func() { - file_nonce_proto_rawDescData = protoimpl.X.CompressGZIP(file_nonce_proto_rawDescData) + file_nonce_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_nonce_proto_rawDesc), len(file_nonce_proto_rawDesc))) }) return file_nonce_proto_rawDescData } var file_nonce_proto_msgTypes = make([]protoimpl.MessageInfo, 2) -var file_nonce_proto_goTypes = []interface{}{ +var file_nonce_proto_goTypes = []any{ (*NonceMessage)(nil), // 0: nonce.NonceMessage (*ValidMessage)(nil), // 1: nonce.ValidMessage (*emptypb.Empty)(nil), // 2: google.protobuf.Empty @@ -175,37 +170,11 @@ func file_nonce_proto_init() { if File_nonce_proto != nil { return } - if !protoimpl.UnsafeEnabled { - file_nonce_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*NonceMessage); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_nonce_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ValidMessage); 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_nonce_proto_rawDesc, + RawDescriptor: unsafe.Slice(unsafe.StringData(file_nonce_proto_rawDesc), len(file_nonce_proto_rawDesc)), NumEnums: 0, NumMessages: 2, NumExtensions: 0, @@ -216,7 +185,6 @@ func file_nonce_proto_init() { MessageInfos: file_nonce_proto_msgTypes, }.Build() File_nonce_proto = out.File - file_nonce_proto_rawDesc = nil file_nonce_proto_goTypes = nil file_nonce_proto_depIdxs = nil } diff --git a/third-party/github.com/letsencrypt/boulder/nonce/proto/nonce_grpc.pb.go b/third-party/github.com/letsencrypt/boulder/nonce/proto/nonce_grpc.pb.go index e3cb5412f..d0525e879 100644 --- a/third-party/github.com/letsencrypt/boulder/nonce/proto/nonce_grpc.pb.go +++ b/third-party/github.com/letsencrypt/boulder/nonce/proto/nonce_grpc.pb.go @@ -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: nonce.proto @@ -62,16 +62,19 @@ func (c *nonceServiceClient) Redeem(ctx context.Context, in *NonceMessage, opts // NonceServiceServer is the server API for NonceService service. // All implementations must embed UnimplementedNonceServiceServer -// for forward compatibility +// for forward compatibility. type NonceServiceServer interface { Nonce(context.Context, *emptypb.Empty) (*NonceMessage, error) Redeem(context.Context, *NonceMessage) (*ValidMessage, error) mustEmbedUnimplementedNonceServiceServer() } -// UnimplementedNonceServiceServer must be embedded to have forward compatible implementations. -type UnimplementedNonceServiceServer struct { -} +// UnimplementedNonceServiceServer 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 UnimplementedNonceServiceServer struct{} func (UnimplementedNonceServiceServer) Nonce(context.Context, *emptypb.Empty) (*NonceMessage, error) { return nil, status.Errorf(codes.Unimplemented, "method Nonce not implemented") @@ -80,6 +83,7 @@ func (UnimplementedNonceServiceServer) Redeem(context.Context, *NonceMessage) (* return nil, status.Errorf(codes.Unimplemented, "method Redeem not implemented") } func (UnimplementedNonceServiceServer) mustEmbedUnimplementedNonceServiceServer() {} +func (UnimplementedNonceServiceServer) testEmbeddedByValue() {} // UnsafeNonceServiceServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to NonceServiceServer will @@ -89,6 +93,13 @@ type UnsafeNonceServiceServer interface { } func RegisterNonceServiceServer(s grpc.ServiceRegistrar, srv NonceServiceServer) { + // If the following call pancis, it indicates UnimplementedNonceServiceServer 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(&NonceService_ServiceDesc, srv) } diff --git a/third-party/github.com/letsencrypt/boulder/observer/probers/crl/crl.go b/third-party/github.com/letsencrypt/boulder/observer/probers/crl/crl.go index 66f463038..2f3c2de10 100644 --- a/third-party/github.com/letsencrypt/boulder/observer/probers/crl/crl.go +++ b/third-party/github.com/letsencrypt/boulder/observer/probers/crl/crl.go @@ -4,15 +4,19 @@ import ( "crypto/x509" "io" "net/http" + "slices" "time" "github.com/prometheus/client_golang/prometheus" + + "github.com/letsencrypt/boulder/crl/idp" ) // CRLProbe is the exported 'Prober' object for monitors configured to // monitor CRL availability & characteristics. type CRLProbe struct { url string + partitioned bool cNextUpdate *prometheus.GaugeVec cThisUpdate *prometheus.GaugeVec cCertCount *prometheus.GaugeVec @@ -47,6 +51,19 @@ func (p CRLProbe) Probe(timeout time.Duration) (bool, time.Duration) { return false, dur } + // Partitioned CRLs MUST contain an issuingDistributionPoint extension, which + // MUST contain the URL from which they were fetched, to prevent substitution + // attacks. + if p.partitioned { + idps, err := idp.GetIDPURIs(crl.Extensions) + if err != nil { + return false, dur + } + if !slices.Contains(idps, p.url) { + return false, dur + } + } + // Report metrics for this CRL p.cThisUpdate.WithLabelValues(p.url).Set(float64(crl.ThisUpdate.Unix())) p.cNextUpdate.WithLabelValues(p.url).Set(float64(crl.NextUpdate.Unix())) diff --git a/third-party/github.com/letsencrypt/boulder/observer/probers/crl/crl_conf.go b/third-party/github.com/letsencrypt/boulder/observer/probers/crl/crl_conf.go index 991a4328c..b414d3072 100644 --- a/third-party/github.com/letsencrypt/boulder/observer/probers/crl/crl_conf.go +++ b/third-party/github.com/letsencrypt/boulder/observer/probers/crl/crl_conf.go @@ -4,9 +4,10 @@ import ( "fmt" "net/url" + "github.com/prometheus/client_golang/prometheus" + "github.com/letsencrypt/boulder/observer/probers" "github.com/letsencrypt/boulder/strictyaml" - "github.com/prometheus/client_golang/prometheus" ) const ( @@ -17,7 +18,8 @@ const ( // CRLConf is exported to receive YAML configuration type CRLConf struct { - URL string `yaml:"url"` + URL string `yaml:"url"` + Partitioned bool `yaml:"partitioned"` } // Kind returns a name that uniquely identifies the `Kind` of `Configurer`. @@ -87,7 +89,7 @@ func (c CRLConf) MakeProber(collectors map[string]prometheus.Collector) (probers return nil, fmt.Errorf("crl prober received collector %q of wrong type, got: %T, expected *prometheus.GaugeVec", certCountName, coll) } - return CRLProbe{c.URL, nextUpdateColl, thisUpdateColl, certCountColl}, nil + return CRLProbe{c.URL, c.Partitioned, nextUpdateColl, thisUpdateColl, certCountColl}, nil } // Instrument constructs any `prometheus.Collector` objects the `CRLProbe` will diff --git a/third-party/github.com/letsencrypt/boulder/observer/probers/crl/crl_conf_test.go b/third-party/github.com/letsencrypt/boulder/observer/probers/crl/crl_conf_test.go index bb99aecaf..f3a619ede 100644 --- a/third-party/github.com/letsencrypt/boulder/observer/probers/crl/crl_conf_test.go +++ b/third-party/github.com/letsencrypt/boulder/observer/probers/crl/crl_conf_test.go @@ -3,10 +3,11 @@ package probers import ( "testing" - "github.com/letsencrypt/boulder/observer/probers" - "github.com/letsencrypt/boulder/test" "github.com/prometheus/client_golang/prometheus" "gopkg.in/yaml.v3" + + "github.com/letsencrypt/boulder/observer/probers" + "github.com/letsencrypt/boulder/test" ) func TestCRLConf_MakeProber(t *testing.T) { @@ -70,25 +71,20 @@ func TestCRLConf_MakeProber(t *testing.T) { } func TestCRLConf_UnmarshalSettings(t *testing.T) { - type fields struct { - url interface{} - } tests := []struct { name string - fields fields + fields probers.Settings want probers.Configurer wantErr bool }{ - {"valid", fields{"google.com"}, CRLConf{"google.com"}, false}, - {"invalid (map)", fields{make(map[string]interface{})}, nil, true}, - {"invalid (list)", fields{make([]string, 0)}, nil, true}, + {"valid", probers.Settings{"url": "google.com"}, CRLConf{"google.com", false}, false}, + {"valid with partitioned", probers.Settings{"url": "google.com", "partitioned": true}, CRLConf{"google.com", true}, false}, + {"invalid (map)", probers.Settings{"url": make(map[string]interface{})}, nil, true}, + {"invalid (list)", probers.Settings{"url": make([]string, 0)}, nil, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - settings := probers.Settings{ - "url": tt.fields.url, - } - settingsBytes, _ := yaml.Marshal(settings) + settingsBytes, _ := yaml.Marshal(tt.fields) t.Log(string(settingsBytes)) c := CRLConf{} got, err := c.UnmarshalSettings(settingsBytes) diff --git a/third-party/github.com/letsencrypt/boulder/observer/probers/dns/dns_conf.go b/third-party/github.com/letsencrypt/boulder/observer/probers/dns/dns_conf.go index ecd92fb2d..3827ebf28 100644 --- a/third-party/github.com/letsencrypt/boulder/observer/probers/dns/dns_conf.go +++ b/third-party/github.com/letsencrypt/boulder/observer/probers/dns/dns_conf.go @@ -3,13 +3,15 @@ package probers import ( "fmt" "net" + "net/netip" "strconv" "strings" - "github.com/letsencrypt/boulder/observer/probers" - "github.com/letsencrypt/boulder/strictyaml" "github.com/miekg/dns" "github.com/prometheus/client_golang/prometheus" + + "github.com/letsencrypt/boulder/observer/probers" + "github.com/letsencrypt/boulder/strictyaml" ) var ( @@ -58,13 +60,12 @@ func (c DNSConf) validateServer() error { return fmt.Errorf( "invalid `server`, %q, port number must be one in [1-65535]", c.Server) } - // Ensure `server` is a valid FQDN or IPv4 / IPv6 address. - IPv6 := net.ParseIP(host).To16() - IPv4 := net.ParseIP(host).To4() + // Ensure `server` is a valid FQDN or IP address. + _, err = netip.ParseAddr(host) FQDN := dns.IsFqdn(dns.Fqdn(host)) - if IPv6 == nil && IPv4 == nil && !FQDN { + if err != nil && !FQDN { return fmt.Errorf( - "invalid `server`, %q, is not an FQDN or IPv4 / IPv6 address", c.Server) + "invalid `server`, %q, is not an FQDN or IP address", c.Server) } return nil } diff --git a/third-party/github.com/letsencrypt/boulder/observer/probers/tls/tls.go b/third-party/github.com/letsencrypt/boulder/observer/probers/tls/tls.go index d7d088aa0..070eceadf 100644 --- a/third-party/github.com/letsencrypt/boulder/observer/probers/tls/tls.go +++ b/third-party/github.com/letsencrypt/boulder/observer/probers/tls/tls.go @@ -5,15 +5,17 @@ import ( "crypto/tls" "crypto/x509" "encoding/base64" + "errors" "fmt" "io" "net" "net/http" "time" - "github.com/letsencrypt/boulder/observer/obsdialer" "github.com/prometheus/client_golang/prometheus" "golang.org/x/crypto/ocsp" + + "github.com/letsencrypt/boulder/observer/obsdialer" ) type reason int @@ -21,17 +23,17 @@ type reason int const ( none reason = iota internalError - ocspError + revocationStatusError rootDidNotMatch - responseDidNotMatch + statusDidNotMatch ) var reasonToString = map[reason]string{ - none: "nil", - internalError: "internalError", - ocspError: "ocspError", - rootDidNotMatch: "rootDidNotMatch", - responseDidNotMatch: "responseDidNotMatch", + none: "nil", + internalError: "internalError", + revocationStatusError: "revocationStatusError", + rootDidNotMatch: "rootDidNotMatch", + statusDidNotMatch: "statusDidNotMatch", } func getReasons() []string { @@ -65,14 +67,19 @@ func (p TLSProbe) Kind() string { } // Get OCSP status (good, revoked or unknown) of certificate -func checkOCSP(cert, issuer *x509.Certificate, want int) (bool, error) { +func checkOCSP(ctx context.Context, cert, issuer *x509.Certificate, want int) (bool, error) { req, err := ocsp.CreateRequest(cert, issuer, nil) if err != nil { return false, err } url := fmt.Sprintf("%s/%s", cert.OCSPServer[0], base64.StdEncoding.EncodeToString(req)) - res, err := http.Get(url) + r, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return false, err + } + + res, err := http.DefaultClient.Do(r) if err != nil { return false, err } @@ -90,6 +97,45 @@ func checkOCSP(cert, issuer *x509.Certificate, want int) (bool, error) { return ocspRes.Status == want, nil } +func checkCRL(ctx context.Context, cert, issuer *x509.Certificate, want int) (bool, error) { + if len(cert.CRLDistributionPoints) != 1 { + return false, errors.New("cert does not contain CRLDP URI") + } + + req, err := http.NewRequestWithContext(ctx, "GET", cert.CRLDistributionPoints[0], nil) + if err != nil { + return false, fmt.Errorf("creating HTTP request: %w", err) + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return false, fmt.Errorf("downloading CRL: %w", err) + } + defer resp.Body.Close() + + der, err := io.ReadAll(resp.Body) + if err != nil { + return false, fmt.Errorf("reading CRL: %w", err) + } + + crl, err := x509.ParseRevocationList(der) + if err != nil { + return false, fmt.Errorf("parsing CRL: %w", err) + } + + err = crl.CheckSignatureFrom(issuer) + if err != nil { + return false, fmt.Errorf("validating CRL: %w", err) + } + + for _, entry := range crl.RevokedCertificateEntries { + if entry.SerialNumber.Cmp(cert.SerialNumber) == 0 { + return want == ocsp.Revoked, nil + } + } + return want == ocsp.Good, nil +} + // Return an error if the root settings are nonempty and do not match the // expected root. func (p TLSProbe) checkRoot(rootOrg, rootCN string) error { @@ -109,29 +155,44 @@ func (p TLSProbe) exportMetrics(cert *x509.Certificate, reason reason) { } func (p TLSProbe) probeExpired(timeout time.Duration) bool { - config := &tls.Config{ - // Set InsecureSkipVerify to skip the default validation we are - // replacing. This will not disable VerifyConnection. - InsecureSkipVerify: true, - VerifyConnection: func(cs tls.ConnectionState) error { - opts := x509.VerifyOptions{ - CurrentTime: cs.PeerCertificates[0].NotAfter, - Intermediates: x509.NewCertPool(), - } - for _, cert := range cs.PeerCertificates[1:] { - opts.Intermediates.AddCert(cert) - } - _, err := cs.PeerCertificates[0].Verify(opts) - return err - }, + addr := p.hostname + _, _, err := net.SplitHostPort(addr) + if err != nil { + addr = net.JoinHostPort(addr, "443") } - ctx, cancel := context.WithTimeout(context.Background(), timeout) - defer cancel() + tlsDialer := tls.Dialer{ NetDialer: &obsdialer.Dialer, - Config: config, + Config: &tls.Config{ + // Set InsecureSkipVerify to skip the default validation we are + // replacing. This will not disable VerifyConnection. + InsecureSkipVerify: true, + VerifyConnection: func(cs tls.ConnectionState) error { + issuers := x509.NewCertPool() + for _, cert := range cs.PeerCertificates[1:] { + issuers.AddCert(cert) + } + opts := x509.VerifyOptions{ + // We set the current time to be the cert's expiration date so that + // the validation routine doesn't complain that the cert is expired. + CurrentTime: cs.PeerCertificates[0].NotAfter, + // By settings roots and intermediates to be whatever was presented + // in the handshake, we're saying that we don't care about the cert + // chaining up to the system trust store. This is safe because we + // check the root ourselves in checkRoot(). + Intermediates: issuers, + Roots: issuers, + } + _, err := cs.PeerCertificates[0].Verify(opts) + return err + }, + }, } - conn, err := tlsDialer.DialContext(ctx, "tcp", p.hostname+":443") + + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + conn, err := tlsDialer.DialContext(ctx, "tcp", addr) if err != nil { p.exportMetrics(nil, internalError) return false @@ -139,10 +200,9 @@ func (p TLSProbe) probeExpired(timeout time.Duration) bool { defer conn.Close() // tls.Dialer.DialContext is documented to always return *tls.Conn - tlsConn := conn.(*tls.Conn) - peers := tlsConn.ConnectionState().PeerCertificates + peers := conn.(*tls.Conn).ConnectionState().PeerCertificates if time.Until(peers[0].NotAfter) > 0 { - p.exportMetrics(peers[0], responseDidNotMatch) + p.exportMetrics(peers[0], statusDidNotMatch) return false } @@ -158,14 +218,49 @@ func (p TLSProbe) probeExpired(timeout time.Duration) bool { } func (p TLSProbe) probeUnexpired(timeout time.Duration) bool { - conn, err := tls.DialWithDialer(&net.Dialer{Timeout: timeout}, "tcp", p.hostname+":443", &tls.Config{}) + addr := p.hostname + _, _, err := net.SplitHostPort(addr) + if err != nil { + addr = net.JoinHostPort(addr, "443") + } + + tlsDialer := tls.Dialer{ + NetDialer: &obsdialer.Dialer, + Config: &tls.Config{ + // Set InsecureSkipVerify to skip the default validation we are + // replacing. This will not disable VerifyConnection. + InsecureSkipVerify: true, + VerifyConnection: func(cs tls.ConnectionState) error { + issuers := x509.NewCertPool() + for _, cert := range cs.PeerCertificates[1:] { + issuers.AddCert(cert) + } + opts := x509.VerifyOptions{ + // By settings roots and intermediates to be whatever was presented + // in the handshake, we're saying that we don't care about the cert + // chaining up to the system trust store. This is safe because we + // check the root ourselves in checkRoot(). + Intermediates: issuers, + Roots: issuers, + } + _, err := cs.PeerCertificates[0].Verify(opts) + return err + }, + }, + } + + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + conn, err := tlsDialer.DialContext(ctx, "tcp", addr) if err != nil { p.exportMetrics(nil, internalError) return false } - defer conn.Close() - peers := conn.ConnectionState().PeerCertificates + + // tls.Dialer.DialContext is documented to always return *tls.Conn + peers := conn.(*tls.Conn).ConnectionState().PeerCertificates root := peers[len(peers)-1].Issuer err = p.checkRoot(root.Organization[0], root.CommonName) if err != nil { @@ -173,20 +268,27 @@ func (p TLSProbe) probeUnexpired(timeout time.Duration) bool { return false } - var ocspStatus bool + var wantStatus int switch p.response { case "valid": - ocspStatus, err = checkOCSP(peers[0], peers[1], ocsp.Good) + wantStatus = ocsp.Good case "revoked": - ocspStatus, err = checkOCSP(peers[0], peers[1], ocsp.Revoked) + wantStatus = ocsp.Revoked + } + + var statusMatch bool + if len(peers[0].OCSPServer) != 0 { + statusMatch, err = checkOCSP(ctx, peers[0], peers[1], wantStatus) + } else { + statusMatch, err = checkCRL(ctx, peers[0], peers[1], wantStatus) } if err != nil { - p.exportMetrics(peers[0], ocspError) + p.exportMetrics(peers[0], revocationStatusError) return false } - if !ocspStatus { - p.exportMetrics(peers[0], responseDidNotMatch) + if !statusMatch { + p.exportMetrics(peers[0], statusDidNotMatch) return false } diff --git a/third-party/github.com/letsencrypt/boulder/observer/probers/tls/tls_conf.go b/third-party/github.com/letsencrypt/boulder/observer/probers/tls/tls_conf.go index 461ff9169..530c4458d 100644 --- a/third-party/github.com/letsencrypt/boulder/observer/probers/tls/tls_conf.go +++ b/third-party/github.com/letsencrypt/boulder/observer/probers/tls/tls_conf.go @@ -2,12 +2,15 @@ package probers import ( "fmt" + "net" "net/url" + "strconv" "strings" + "github.com/prometheus/client_golang/prometheus" + "github.com/letsencrypt/boulder/observer/probers" "github.com/letsencrypt/boulder/strictyaml" - "github.com/prometheus/client_golang/prometheus" ) const ( @@ -42,15 +45,28 @@ func (c TLSConf) UnmarshalSettings(settings []byte) (probers.Configurer, error) } func (c TLSConf) validateHostname() error { - url, err := url.Parse(c.Hostname) + hostname := c.Hostname + + if strings.Contains(c.Hostname, ":") { + host, port, err := net.SplitHostPort(c.Hostname) + if err != nil { + return fmt.Errorf("invalid 'hostname', got %q, expected a valid hostport: %s", c.Hostname, err) + } + + _, err = strconv.Atoi(port) + if err != nil { + return fmt.Errorf("invalid 'hostname', got %q, expected a valid hostport: %s", c.Hostname, err) + } + hostname = host + } + + url, err := url.Parse(hostname) if err != nil { - return fmt.Errorf( - "invalid 'hostname', got %q, expected a valid hostname: %s", c.Hostname, err) + return fmt.Errorf("invalid 'hostname', got %q, expected a valid hostname: %s", c.Hostname, err) } if url.Scheme != "" { - return fmt.Errorf( - "invalid 'hostname', got: %q, should not include scheme", c.Hostname) + return fmt.Errorf("invalid 'hostname', got: %q, should not include scheme", c.Hostname) } return nil diff --git a/third-party/github.com/letsencrypt/boulder/observer/probers/tls/tls_conf_test.go b/third-party/github.com/letsencrypt/boulder/observer/probers/tls/tls_conf_test.go index 1bf3355cf..5da13f11c 100644 --- a/third-party/github.com/letsencrypt/boulder/observer/probers/tls/tls_conf_test.go +++ b/third-party/github.com/letsencrypt/boulder/observer/probers/tls/tls_conf_test.go @@ -4,9 +4,10 @@ import ( "reflect" "testing" - "github.com/letsencrypt/boulder/observer/probers" "github.com/prometheus/client_golang/prometheus" "gopkg.in/yaml.v3" + + "github.com/letsencrypt/boulder/observer/probers" ) func TestTLSConf_MakeProber(t *testing.T) { @@ -33,10 +34,12 @@ func TestTLSConf_MakeProber(t *testing.T) { // valid {"valid hostname", fields{"example.com", goodRootCN, "valid"}, colls, false}, {"valid hostname with path", fields{"example.com/foo/bar", "ISRG Root X2", "Revoked"}, colls, false}, + {"valid hostname with port", fields{"example.com:8080", goodRootCN, "expired"}, colls, false}, // invalid hostname {"bad hostname", fields{":::::", goodRootCN, goodResponse}, colls, true}, {"included scheme", fields{"https://example.com", goodRootCN, goodResponse}, colls, true}, + {"included scheme and port", fields{"https://example.com:443", goodRootCN, goodResponse}, colls, true}, // invalid response {"empty response", fields{goodHostname, goodRootCN, ""}, colls, true}, diff --git a/third-party/github.com/letsencrypt/boulder/ocsp/responder/filter_source.go b/third-party/github.com/letsencrypt/boulder/ocsp/responder/filter_source.go index d97ba80d4..e523d7678 100644 --- a/third-party/github.com/letsencrypt/boulder/ocsp/responder/filter_source.go +++ b/third-party/github.com/letsencrypt/boulder/ocsp/responder/filter_source.go @@ -4,7 +4,7 @@ import ( "bytes" "context" "crypto" - "crypto/sha1" + "crypto/sha1" //nolint: gosec // SHA1 is required by the RFC 5019 Lightweight OCSP Profile "crypto/x509/pkix" "encoding/asn1" "encoding/hex" diff --git a/third-party/github.com/letsencrypt/boulder/ocsp/responder/responder.go b/third-party/github.com/letsencrypt/boulder/ocsp/responder/responder.go index 5fc273644..d985e92ef 100644 --- a/third-party/github.com/letsencrypt/boulder/ocsp/responder/responder.go +++ b/third-party/github.com/letsencrypt/boulder/ocsp/responder/responder.go @@ -40,7 +40,7 @@ import ( "errors" "fmt" "io" - "math/rand" + "math/rand/v2" "net/http" "net/url" "time" @@ -153,7 +153,7 @@ var hashToString = map[crypto.Hash]string{ } func SampledError(log blog.Logger, sampleRate int, format string, a ...interface{}) { - if sampleRate > 0 && rand.Intn(sampleRate) == 0 { + if sampleRate > 0 && rand.IntN(sampleRate) == 0 { log.Errf(format, a...) } } diff --git a/third-party/github.com/letsencrypt/boulder/pkcs11helpers/helpers.go b/third-party/github.com/letsencrypt/boulder/pkcs11helpers/helpers.go index 173123e17..4c02146d8 100644 --- a/third-party/github.com/letsencrypt/boulder/pkcs11helpers/helpers.go +++ b/third-party/github.com/letsencrypt/boulder/pkcs11helpers/helpers.go @@ -235,7 +235,7 @@ const ( // Hash identifiers required for PKCS#11 RSA signing. Only support SHA-256, SHA-384, // and SHA-512 -var hashIdentifiers = map[crypto.Hash][]byte{ +var hashIdents = map[crypto.Hash][]byte{ crypto.SHA256: {0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20}, crypto.SHA384: {0x30, 0x41, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x02, 0x05, 0x00, 0x04, 0x30}, crypto.SHA512: {0x30, 0x51, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03, 0x05, 0x00, 0x04, 0x40}, @@ -250,7 +250,7 @@ func (s *Session) Sign(object pkcs11.ObjectHandle, keyType keyType, digest []byt switch keyType { case RSAKey: mech[0] = pkcs11.NewMechanism(pkcs11.CKM_RSA_PKCS, nil) - prefix, ok := hashIdentifiers[hash] + prefix, ok := hashIdents[hash] if !ok { return nil, errors.New("unsupported hash function") } diff --git a/third-party/github.com/letsencrypt/boulder/policy/pa.go b/third-party/github.com/letsencrypt/boulder/policy/pa.go index ce7857a7d..b1acfb885 100644 --- a/third-party/github.com/letsencrypt/boulder/policy/pa.go +++ b/third-party/github.com/letsencrypt/boulder/policy/pa.go @@ -5,9 +5,8 @@ import ( "encoding/hex" "errors" "fmt" - "math/rand" - "net" "net/mail" + "net/netip" "os" "regexp" "slices" @@ -34,22 +33,17 @@ type AuthorityImpl struct { wildcardExactBlocklist map[string]bool blocklistMu sync.RWMutex - enabledChallenges map[core.AcmeChallenge]bool - pseudoRNG *rand.Rand - rngMu sync.Mutex + enabledChallenges map[core.AcmeChallenge]bool + enabledIdentifiers map[identifier.IdentifierType]bool } // New constructs a Policy Authority. -func New(challengeTypes map[core.AcmeChallenge]bool, log blog.Logger) (*AuthorityImpl, error) { - - pa := AuthorityImpl{ - log: log, - enabledChallenges: challengeTypes, - // We don't need real randomness for this. - pseudoRNG: rand.New(rand.NewSource(99)), - } - - return &pa, nil +func New(identifierTypes map[identifier.IdentifierType]bool, challengeTypes map[core.AcmeChallenge]bool, log blog.Logger) (*AuthorityImpl, error) { + return &AuthorityImpl{ + log: log, + enabledChallenges: challengeTypes, + enabledIdentifiers: identifierTypes, + }, nil } // blockedNamesPolicy is a struct holding lists of blocked domain names. One for @@ -175,9 +169,10 @@ var ( errPolicyForbidden = berrors.RejectedIdentifierError("The ACME server refuses to issue a certificate for this domain name, because it is forbidden by policy") errInvalidDNSCharacter = berrors.MalformedError("Domain name contains an invalid character") errNameTooLong = berrors.MalformedError("Domain name is longer than 253 bytes") - errIPAddress = berrors.MalformedError("The ACME server can not issue a certificate for an IP address") + errIPAddressInDNS = berrors.MalformedError("Identifier type is DNS but value is an IP address") + errIPInvalid = berrors.MalformedError("IP address is invalid") errTooManyLabels = berrors.MalformedError("Domain name has more than 10 labels (parts)") - errEmptyName = berrors.MalformedError("Domain name is empty") + errEmptyIdentifier = berrors.MalformedError("Identifier value (name) is empty") errNameEndsInDot = berrors.MalformedError("Domain name ends in a dot") errTooFewLabels = berrors.MalformedError("Domain name needs at least one dot") errLabelTooShort = berrors.MalformedError("Domain name can not have two dots in a row") @@ -188,6 +183,7 @@ var ( errMalformedWildcard = berrors.MalformedError("Domain name contains an invalid wildcard. A wildcard is only permitted before the first dot in a domain name") errICANNTLDWildcard = berrors.MalformedError("Domain name is a wildcard for an ICANN TLD") errWildcardNotSupported = berrors.MalformedError("Wildcard domain names are not supported") + errUnsupportedIdent = berrors.MalformedError("Invalid identifier type") ) // validNonWildcardDomain checks that a domain isn't: @@ -205,7 +201,7 @@ var ( // It does NOT ensure that the domain is absent from any PA blocked lists. func validNonWildcardDomain(domain string) error { if domain == "" { - return errEmptyName + return errEmptyIdentifier } if strings.HasPrefix(domain, "*.") { @@ -222,8 +218,9 @@ func validNonWildcardDomain(domain string) error { return errNameTooLong } - if ip := net.ParseIP(domain); ip != nil { - return errIPAddress + _, err := netip.ParseAddr(domain) + if err == nil { + return errIPAddressInDNS } if strings.HasSuffix(domain, ".") { @@ -326,6 +323,30 @@ func ValidDomain(domain string) error { return validNonWildcardDomain(baseDomain) } +// ValidIP checks that an IP address: +// - isn't empty +// - is an IPv4 or IPv6 address +// - isn't in an IANA special-purpose address registry +// +// It does NOT ensure that the IP address is absent from any PA blocked lists. +func ValidIP(ip string) error { + if ip == "" { + return errEmptyIdentifier + } + + // Check the output of netip.Addr.String(), to ensure the input complied + // with RFC 8738, Sec. 3. ("The identifier value MUST contain the textual + // form of the address as defined in RFC 1123, Sec. 2.1 for IPv4 and in RFC + // 5952, Sec. 4 for IPv6.") ParseAddr() will accept a non-compliant but + // otherwise valid string; String() will output a compliant string. + parsedIP, err := netip.ParseAddr(ip) + if err != nil || parsedIP.String() != ip { + return errIPInvalid + } + + return iana.IsReservedAddr(parsedIP) +} + // forbiddenMailDomains is a map of domain names we do not allow after the // @ symbol in contact mailto addresses. These are frequently used when // copy-pasting example configurations and would not result in expiration @@ -344,38 +365,33 @@ var forbiddenMailDomains = map[string]bool{ func ValidEmail(address string) error { email, err := mail.ParseAddress(address) if err != nil { - if len(address) > 254 { - address = address[:254] + "..." - } - return berrors.InvalidEmailError("%q is not a valid e-mail address", address) + return berrors.InvalidEmailError("unable to parse email address") } splitEmail := strings.SplitN(email.Address, "@", -1) domain := strings.ToLower(splitEmail[len(splitEmail)-1]) err = validNonWildcardDomain(domain) if err != nil { - return berrors.InvalidEmailError( - "contact email %q has invalid domain : %s", - email.Address, err) + return berrors.InvalidEmailError("contact email has invalid domain: %s", err) } if forbiddenMailDomains[domain] { - return berrors.InvalidEmailError( - "invalid contact domain. Contact emails @%s are forbidden", - domain) + // We're okay including the domain in the error message here because this + // case occurs only for a small block-list of domains listed above. + return berrors.InvalidEmailError("contact email has forbidden domain %q", domain) } return nil } // subError returns an appropriately typed error based on the input error -func subError(name string, err error) berrors.SubBoulderError { +func subError(ident identifier.ACMEIdentifier, err error) berrors.SubBoulderError { var bErr *berrors.BoulderError if errors.As(err, &bErr) { return berrors.SubBoulderError{ - Identifier: identifier.DNSIdentifier(name), + Identifier: ident, BoulderError: bErr, } } else { return berrors.SubBoulderError{ - Identifier: identifier.DNSIdentifier(name), + Identifier: ident, BoulderError: &berrors.BoulderError{ Type: berrors.RejectedIdentifier, Detail: err.Error(), @@ -385,53 +401,67 @@ func subError(name string, err error) berrors.SubBoulderError { } // WillingToIssue determines whether the CA is willing to issue for the provided -// domain names. +// identifiers. // -// It checks the criteria checked by `WellFormedDomainNames`, and additionally checks -// whether any domain is on a blocklist. +// It checks the criteria checked by `WellFormedIdentifiers`, and additionally +// checks whether any identifier is on a blocklist. // -// If multiple domains are invalid, the error will contain suberrors specific to -// each domain. +// If multiple identifiers are invalid, the error will contain suberrors +// specific to each identifier. // -// Precondition: all input domain names must be in lowercase. -func (pa *AuthorityImpl) WillingToIssue(domains []string) error { - err := WellFormedDomainNames(domains) +// Precondition: all input identifier values must be in lowercase. +func (pa *AuthorityImpl) WillingToIssue(idents identifier.ACMEIdentifiers) error { + err := WellFormedIdentifiers(idents) if err != nil { return err } var subErrors []berrors.SubBoulderError - for _, domain := range domains { - if strings.Count(domain, "*") > 0 { - // The base domain is the wildcard request with the `*.` prefix removed - baseDomain := strings.TrimPrefix(domain, "*.") - - // The base domain can't be in the wildcard exact blocklist - err = pa.checkWildcardHostList(baseDomain) - if err != nil { - subErrors = append(subErrors, subError(domain, err)) - continue - } + for _, ident := range idents { + if !pa.IdentifierTypeEnabled(ident.Type) { + subErrors = append(subErrors, subError(ident, berrors.RejectedIdentifierError("The ACME server has disabled this identifier type"))) + continue } - // For both wildcard and non-wildcard domains, check whether any parent domain - // name is on the regular blocklist. - err := pa.checkHostLists(domain) - if err != nil { - subErrors = append(subErrors, subError(domain, err)) - continue + // Only DNS identifiers are subject to wildcard and blocklist checks. + // Unsupported identifier types will have been caught by + // WellFormedIdentifiers(). + // + // TODO(#8237): We may want to implement IP address blocklists too. + if ident.Type == identifier.TypeDNS { + if strings.Count(ident.Value, "*") > 0 { + // The base domain is the wildcard request with the `*.` prefix removed + baseDomain := strings.TrimPrefix(ident.Value, "*.") + + // The base domain can't be in the wildcard exact blocklist + err = pa.checkWildcardHostList(baseDomain) + if err != nil { + subErrors = append(subErrors, subError(ident, err)) + continue + } + } + + // For both wildcard and non-wildcard domains, check whether any parent domain + // name is on the regular blocklist. + err := pa.checkHostLists(ident.Value) + if err != nil { + subErrors = append(subErrors, subError(ident, err)) + continue + } } } return combineSubErrors(subErrors) } -// WellFormedDomainNames returns an error if any of the provided domains do not meet these criteria: +// WellFormedIdentifiers returns an error if any of the provided identifiers do +// not meet these criteria: // +// For DNS identifiers: // - MUST contains only lowercase characters, numbers, hyphens, and dots // - MUST NOT have more than maxLabels labels // - MUST follow the DNS hostname syntax rules in RFC 1035 and RFC 2181 // -// In particular, it: +// In particular, DNS identifiers: // - MUST NOT contain underscores // - MUST NOT match the syntax of an IP address // - MUST end in a public suffix @@ -439,20 +469,34 @@ func (pa *AuthorityImpl) WillingToIssue(domains []string) error { // - MUST NOT be a label-wise suffix match for a name on the block list, // where comparison is case-independent (normalized to lower case) // -// If a domain contains a *, we additionally require: +// If a DNS identifier contains a *, we additionally require: // - There is at most one `*` wildcard character // - That the wildcard character is the leftmost label // - That the wildcard label is not immediately adjacent to a top level ICANN // TLD // -// If multiple domains are invalid, the error will contain suberrors specific to -// each domain. -func WellFormedDomainNames(domains []string) error { +// For IP identifiers: +// - MUST match the syntax of an IP address +// - MUST NOT be in an IANA special-purpose address registry +// +// If multiple identifiers are invalid, the error will contain suberrors +// specific to each identifier. +func WellFormedIdentifiers(idents identifier.ACMEIdentifiers) error { var subErrors []berrors.SubBoulderError - for _, domain := range domains { - err := ValidDomain(domain) - if err != nil { - subErrors = append(subErrors, subError(domain, err)) + for _, ident := range idents { + switch ident.Type { + case identifier.TypeDNS: + err := ValidDomain(ident.Value) + if err != nil { + subErrors = append(subErrors, subError(ident, err)) + } + case identifier.TypeIP: + err := ValidIP(ident.Value) + if err != nil { + subErrors = append(subErrors, subError(ident, err)) + } + default: + subErrors = append(subErrors, subError(ident, errUnsupportedIdent)) } } return combineSubErrors(subErrors) @@ -524,75 +568,40 @@ func (pa *AuthorityImpl) checkHostLists(domain string) error { return nil } -// challengeTypesFor determines which challenge types are acceptable for the -// given identifier. -func (pa *AuthorityImpl) challengeTypesFor(identifier identifier.ACMEIdentifier) ([]core.AcmeChallenge, error) { - var challenges []core.AcmeChallenge - - // If the identifier is for a DNS wildcard name we only - // provide a DNS-01 challenge as a matter of CA policy. - if strings.HasPrefix(identifier.Value, "*.") { - // We must have the DNS-01 challenge type enabled to create challenges for - // a wildcard identifier per LE policy. - if !pa.ChallengeTypeEnabled(core.ChallengeTypeDNS01) { - return nil, fmt.Errorf( - "Challenges requested for wildcard identifier but DNS-01 " + - "challenge type is not enabled") - } - // Only provide a DNS-01-Wildcard challenge - challenges = []core.AcmeChallenge{core.ChallengeTypeDNS01} - } else { - // Otherwise we collect up challenges based on what is enabled. - if pa.ChallengeTypeEnabled(core.ChallengeTypeHTTP01) { - challenges = append(challenges, core.ChallengeTypeHTTP01) +// ChallengeTypesFor determines which challenge types are acceptable for the +// given identifier. This determination is made purely based on the identifier, +// and not based on which challenge types are enabled, so that challenge type +// filtering can happen dynamically at request rather than being set in stone +// at creation time. +func (pa *AuthorityImpl) ChallengeTypesFor(ident identifier.ACMEIdentifier) ([]core.AcmeChallenge, error) { + switch ident.Type { + case identifier.TypeDNS: + // If the identifier is for a DNS wildcard name we only provide a DNS-01 + // challenge, to comply with the BRs Sections 3.2.2.4.19 and 3.2.2.4.20 + // stating that ACME HTTP-01 and TLS-ALPN-01 are not suitable for validating + // Wildcard Domains. + if strings.HasPrefix(ident.Value, "*.") { + return []core.AcmeChallenge{core.ChallengeTypeDNS01}, nil } - if pa.ChallengeTypeEnabled(core.ChallengeTypeTLSALPN01) { - challenges = append(challenges, core.ChallengeTypeTLSALPN01) - } - - if pa.ChallengeTypeEnabled(core.ChallengeTypeDNS01) { - challenges = append(challenges, core.ChallengeTypeDNS01) - } + // Return all challenge types we support for non-wildcard DNS identifiers. + return []core.AcmeChallenge{ + core.ChallengeTypeHTTP01, + core.ChallengeTypeDNS01, + core.ChallengeTypeTLSALPN01, + }, nil + case identifier.TypeIP: + // Only HTTP-01 and TLS-ALPN-01 are suitable for IP address identifiers + // per RFC 8738, Sec. 4. + return []core.AcmeChallenge{ + core.ChallengeTypeHTTP01, + core.ChallengeTypeTLSALPN01, + }, nil + default: + // Otherwise return an error because we don't support any challenges for this + // identifier type. + return nil, fmt.Errorf("unrecognized identifier type %q", ident.Type) } - - return challenges, nil -} - -// ChallengesFor determines which challenge types are acceptable for the given -// identifier, and constructs new challenge objects for those challenge types. -// The resulting challenge objects all share a single challenge token and are -// returned in a random order. -func (pa *AuthorityImpl) ChallengesFor(identifier identifier.ACMEIdentifier) ([]core.Challenge, error) { - challTypes, err := pa.challengeTypesFor(identifier) - if err != nil { - return nil, err - } - - challenges := make([]core.Challenge, len(challTypes)) - - token := core.NewToken() - - for i, t := range challTypes { - c, err := core.NewChallenge(t, token) - if err != nil { - return nil, err - } - - challenges[i] = c - } - - // We shuffle the challenges to prevent ACME clients from relying on the - // specific order that boulder returns them in. - shuffled := make([]core.Challenge, len(challenges)) - - pa.rngMu.Lock() - defer pa.rngMu.Unlock() - for i, challIdx := range pa.pseudoRNG.Perm(len(challenges)) { - shuffled[i] = challenges[challIdx] - } - - return shuffled, nil } // ChallengeTypeEnabled returns whether the specified challenge type is enabled @@ -602,22 +611,34 @@ func (pa *AuthorityImpl) ChallengeTypeEnabled(t core.AcmeChallenge) bool { return pa.enabledChallenges[t] } -// CheckAuthz determines that an authorization was fulfilled by a challenge -// that was appropriate for the kind of identifier in the authorization. -func (pa *AuthorityImpl) CheckAuthz(authz *core.Authorization) error { +// CheckAuthzChallenges determines that an authorization was fulfilled by a +// challenge that is currently enabled and was appropriate for the kind of +// identifier in the authorization. +func (pa *AuthorityImpl) CheckAuthzChallenges(authz *core.Authorization) error { chall, err := authz.SolvedBy() if err != nil { return err } - challTypes, err := pa.challengeTypesFor(authz.Identifier) + if !pa.ChallengeTypeEnabled(chall) { + return errors.New("authorization fulfilled by disabled challenge type") + } + + challTypes, err := pa.ChallengeTypesFor(authz.Identifier) if err != nil { return err } if !slices.Contains(challTypes, chall) { - return errors.New("authorization fulfilled by invalid challenge") + return errors.New("authorization fulfilled by inapplicable challenge type") } return nil } + +// IdentifierTypeEnabled returns whether the specified identifier type is enabled +func (pa *AuthorityImpl) IdentifierTypeEnabled(t identifier.IdentifierType) bool { + pa.blocklistMu.RLock() + defer pa.blocklistMu.RUnlock() + return pa.enabledIdentifiers[t] +} diff --git a/third-party/github.com/letsencrypt/boulder/policy/pa_test.go b/third-party/github.com/letsencrypt/boulder/policy/pa_test.go index e2f4fdc9d..5eda7e2bd 100644 --- a/third-party/github.com/letsencrypt/boulder/policy/pa_test.go +++ b/third-party/github.com/letsencrypt/boulder/policy/pa_test.go @@ -2,117 +2,167 @@ package policy import ( "fmt" + "net/netip" "os" + "strings" "testing" + "gopkg.in/yaml.v3" + "github.com/letsencrypt/boulder/core" berrors "github.com/letsencrypt/boulder/errors" "github.com/letsencrypt/boulder/features" "github.com/letsencrypt/boulder/identifier" blog "github.com/letsencrypt/boulder/log" - "github.com/letsencrypt/boulder/must" "github.com/letsencrypt/boulder/test" - "gopkg.in/yaml.v3" ) -var enabledChallenges = map[core.AcmeChallenge]bool{ - core.ChallengeTypeHTTP01: true, - core.ChallengeTypeDNS01: true, -} - func paImpl(t *testing.T) *AuthorityImpl { - pa, err := New(enabledChallenges, blog.NewMock()) + enabledChallenges := map[core.AcmeChallenge]bool{ + core.ChallengeTypeHTTP01: true, + core.ChallengeTypeDNS01: true, + core.ChallengeTypeTLSALPN01: true, + } + + enabledIdentifiers := map[identifier.IdentifierType]bool{ + identifier.TypeDNS: true, + identifier.TypeIP: true, + } + + pa, err := New(enabledIdentifiers, enabledChallenges, blog.NewMock()) if err != nil { t.Fatalf("Couldn't create policy implementation: %s", err) } return pa } -func TestWellFormedDomainNames(t *testing.T) { +func TestWellFormedIdentifiers(t *testing.T) { testCases := []struct { - domain string - err error + ident identifier.ACMEIdentifier + err error }{ - {``, errEmptyName}, // Empty name - {`zomb!.com`, errInvalidDNSCharacter}, // ASCII character out of range - {`emailaddress@myseriously.present.com`, errInvalidDNSCharacter}, - {`user:pass@myseriously.present.com`, errInvalidDNSCharacter}, - {`zömbo.com`, errInvalidDNSCharacter}, // non-ASCII character - {`127.0.0.1`, errIPAddress}, // IPv4 address - {`fe80::1:1`, errInvalidDNSCharacter}, // IPv6 addresses - {`[2001:db8:85a3:8d3:1319:8a2e:370:7348]`, errInvalidDNSCharacter}, // unexpected IPv6 variants - {`[2001:db8:85a3:8d3:1319:8a2e:370:7348]:443`, errInvalidDNSCharacter}, - {`2001:db8::/32`, errInvalidDNSCharacter}, - {`a.b.c.d.e.f.g.h.i.j.k`, errTooManyLabels}, // Too many labels (>10) + // Invalid identifier types + {identifier.ACMEIdentifier{}, errUnsupportedIdent}, // Empty identifier type + {identifier.ACMEIdentifier{Type: "fnord", Value: "uh-oh, Spaghetti-Os[tm]"}, errUnsupportedIdent}, - {`www.0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef012345.com`, errNameTooLong}, // Too long (254 characters) + // Empty identifier values + {identifier.NewDNS(``), errEmptyIdentifier}, // Empty DNS identifier + {identifier.ACMEIdentifier{Type: "ip"}, errEmptyIdentifier}, // Empty IP identifier - {`www.ef0123456789abcdef013456789abcdef012345.789abcdef012345679abcdef0123456789abcdef01234.6789abcdef0123456789abcdef0.23456789abcdef0123456789a.cdef0123456789abcdef0123456789ab.def0123456789abcdef0123456789.bcdef0123456789abcdef012345.com`, nil}, // OK, not too long (240 characters) + // DNS follies - {`www.abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz.com`, errLabelTooLong}, // Label too long (>63 characters) + {identifier.NewDNS(`zomb!.com`), errInvalidDNSCharacter}, // ASCII character out of range + {identifier.NewDNS(`emailaddress@myseriously.present.com`), errInvalidDNSCharacter}, + {identifier.NewDNS(`user:pass@myseriously.present.com`), errInvalidDNSCharacter}, + {identifier.NewDNS(`zömbo.com`), errInvalidDNSCharacter}, // non-ASCII character + {identifier.NewDNS(`127.0.0.1`), errIPAddressInDNS}, // IPv4 address + {identifier.NewDNS(`fe80::1:1`), errInvalidDNSCharacter}, // IPv6 address + {identifier.NewDNS(`[2001:db8:85a3:8d3:1319:8a2e:370:7348]`), errInvalidDNSCharacter}, // unexpected IPv6 variants + {identifier.NewDNS(`[2001:db8:85a3:8d3:1319:8a2e:370:7348]:443`), errInvalidDNSCharacter}, + {identifier.NewDNS(`2001:db8::/32`), errInvalidDNSCharacter}, + {identifier.NewDNS(`a.b.c.d.e.f.g.h.i.j.k`), errTooManyLabels}, // Too many labels (>10) - {`www.-ombo.com`, errInvalidDNSCharacter}, // Label starts with '-' - {`www.zomb-.com`, errInvalidDNSCharacter}, // Label ends with '-' - {`xn--.net`, errInvalidDNSCharacter}, // Label ends with '-' - {`-0b.net`, errInvalidDNSCharacter}, // First label begins with '-' - {`-0.net`, errInvalidDNSCharacter}, // First label begins with '-' - {`-.net`, errInvalidDNSCharacter}, // First label is only '-' - {`---.net`, errInvalidDNSCharacter}, // First label is only hyphens - {`0`, errTooFewLabels}, - {`1`, errTooFewLabels}, - {`*`, errMalformedWildcard}, - {`**`, errTooManyWildcards}, - {`*.*`, errTooManyWildcards}, - {`zombo*com`, errMalformedWildcard}, - {`*.com`, errICANNTLDWildcard}, - {`..a`, errLabelTooShort}, - {`a..a`, errLabelTooShort}, - {`.a..a`, errLabelTooShort}, - {`..foo.com`, errLabelTooShort}, - {`.`, errNameEndsInDot}, - {`..`, errNameEndsInDot}, - {`a..`, errNameEndsInDot}, - {`.....`, errNameEndsInDot}, - {`.a.`, errNameEndsInDot}, - {`www.zombo.com.`, errNameEndsInDot}, - {`www.zombo_com.com`, errInvalidDNSCharacter}, - {`\uFEFF`, errInvalidDNSCharacter}, // Byte order mark - {`\uFEFFwww.zombo.com`, errInvalidDNSCharacter}, - {`www.zom\u202Ebo.com`, errInvalidDNSCharacter}, // Right-to-Left Override - {`\u202Ewww.zombo.com`, errInvalidDNSCharacter}, - {`www.zom\u200Fbo.com`, errInvalidDNSCharacter}, // Right-to-Left Mark - {`\u200Fwww.zombo.com`, errInvalidDNSCharacter}, + {identifier.NewDNS(`www.0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef012345.com`), errNameTooLong}, // Too long (254 characters) + + {identifier.NewDNS(`www.ef0123456789abcdef013456789abcdef012345.789abcdef012345679abcdef0123456789abcdef01234.6789abcdef0123456789abcdef0.23456789abcdef0123456789a.cdef0123456789abcdef0123456789ab.def0123456789abcdef0123456789.bcdef0123456789abcdef012345.com`), nil}, // OK, not too long (240 characters) + + {identifier.NewDNS(`www.abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz.com`), errLabelTooLong}, // Label too long (>63 characters) + + {identifier.NewDNS(`www.-ombo.com`), errInvalidDNSCharacter}, // Label starts with '-' + {identifier.NewDNS(`www.zomb-.com`), errInvalidDNSCharacter}, // Label ends with '-' + {identifier.NewDNS(`xn--.net`), errInvalidDNSCharacter}, // Label ends with '-' + {identifier.NewDNS(`-0b.net`), errInvalidDNSCharacter}, // First label begins with '-' + {identifier.NewDNS(`-0.net`), errInvalidDNSCharacter}, // First label begins with '-' + {identifier.NewDNS(`-.net`), errInvalidDNSCharacter}, // First label is only '-' + {identifier.NewDNS(`---.net`), errInvalidDNSCharacter}, // First label is only hyphens + {identifier.NewDNS(`0`), errTooFewLabels}, + {identifier.NewDNS(`1`), errTooFewLabels}, + {identifier.NewDNS(`*`), errMalformedWildcard}, + {identifier.NewDNS(`**`), errTooManyWildcards}, + {identifier.NewDNS(`*.*`), errTooManyWildcards}, + {identifier.NewDNS(`zombo*com`), errMalformedWildcard}, + {identifier.NewDNS(`*.com`), errICANNTLDWildcard}, + {identifier.NewDNS(`..a`), errLabelTooShort}, + {identifier.NewDNS(`a..a`), errLabelTooShort}, + {identifier.NewDNS(`.a..a`), errLabelTooShort}, + {identifier.NewDNS(`..foo.com`), errLabelTooShort}, + {identifier.NewDNS(`.`), errNameEndsInDot}, + {identifier.NewDNS(`..`), errNameEndsInDot}, + {identifier.NewDNS(`a..`), errNameEndsInDot}, + {identifier.NewDNS(`.....`), errNameEndsInDot}, + {identifier.NewDNS(`.a.`), errNameEndsInDot}, + {identifier.NewDNS(`www.zombo.com.`), errNameEndsInDot}, + {identifier.NewDNS(`www.zombo_com.com`), errInvalidDNSCharacter}, + {identifier.NewDNS(`\uFEFF`), errInvalidDNSCharacter}, // Byte order mark + {identifier.NewDNS(`\uFEFFwww.zombo.com`), errInvalidDNSCharacter}, + {identifier.NewDNS(`www.zom\u202Ebo.com`), errInvalidDNSCharacter}, // Right-to-Left Override + {identifier.NewDNS(`\u202Ewww.zombo.com`), errInvalidDNSCharacter}, + {identifier.NewDNS(`www.zom\u200Fbo.com`), errInvalidDNSCharacter}, // Right-to-Left Mark + {identifier.NewDNS(`\u200Fwww.zombo.com`), errInvalidDNSCharacter}, // Underscores are technically disallowed in DNS. Some DNS // implementations accept them but we will be conservative. - {`www.zom_bo.com`, errInvalidDNSCharacter}, - {`zombocom`, errTooFewLabels}, - {`localhost`, errTooFewLabels}, - {`mail`, errTooFewLabels}, + {identifier.NewDNS(`www.zom_bo.com`), errInvalidDNSCharacter}, + {identifier.NewDNS(`zombocom`), errTooFewLabels}, + {identifier.NewDNS(`localhost`), errTooFewLabels}, + {identifier.NewDNS(`mail`), errTooFewLabels}, // disallow capitalized letters for #927 - {`CapitalizedLetters.com`, errInvalidDNSCharacter}, + {identifier.NewDNS(`CapitalizedLetters.com`), errInvalidDNSCharacter}, - {`example.acting`, errNonPublic}, - {`example.internal`, errNonPublic}, + {identifier.NewDNS(`example.acting`), errNonPublic}, + {identifier.NewDNS(`example.internal`), errNonPublic}, // All-numeric final label not okay. - {`www.zombo.163`, errNonPublic}, - {`xn--109-3veba6djs1bfxlfmx6c9g.xn--f1awi.xn--p1ai`, errMalformedIDN}, // Not in Unicode NFC - {`bq--abwhky3f6fxq.jakacomo.com`, errInvalidRLDH}, + {identifier.NewDNS(`www.zombo.163`), errNonPublic}, + {identifier.NewDNS(`xn--109-3veba6djs1bfxlfmx6c9g.xn--f1awi.xn--p1ai`), errMalformedIDN}, // Not in Unicode NFC + {identifier.NewDNS(`bq--abwhky3f6fxq.jakacomo.com`), errInvalidRLDH}, // Three hyphens starting at third second char of first label. - {`bq---abwhky3f6fxq.jakacomo.com`, errInvalidRLDH}, + {identifier.NewDNS(`bq---abwhky3f6fxq.jakacomo.com`), errInvalidRLDH}, // Three hyphens starting at second char of first label. - {`h---test.hk2yz.org`, errInvalidRLDH}, - {`co.uk`, errICANNTLD}, - {`foo.bd`, errICANNTLD}, + {identifier.NewDNS(`h---test.hk2yz.org`), errInvalidRLDH}, + {identifier.NewDNS(`co.uk`), errICANNTLD}, + {identifier.NewDNS(`foo.bd`), errICANNTLD}, + + // IP oopsies + + {identifier.ACMEIdentifier{Type: "ip", Value: `zombo.com`}, errIPInvalid}, // That's DNS! + + // Unexpected IPv4 variants + {identifier.ACMEIdentifier{Type: "ip", Value: `192.168.1.1.1`}, errIPInvalid}, // extra octet + {identifier.ACMEIdentifier{Type: "ip", Value: `192.168.1.256`}, errIPInvalid}, // octet out of range + {identifier.ACMEIdentifier{Type: "ip", Value: `192.168.1.a1`}, errIPInvalid}, // character out of range + {identifier.ACMEIdentifier{Type: "ip", Value: `192.168.1.0/24`}, errIPInvalid}, // with CIDR + {identifier.ACMEIdentifier{Type: "ip", Value: `192.168.1.1:443`}, errIPInvalid}, // with port + {identifier.ACMEIdentifier{Type: "ip", Value: `0xc0a80101`}, errIPInvalid}, // as hex + {identifier.ACMEIdentifier{Type: "ip", Value: `1.1.168.192.in-addr.arpa`}, errIPInvalid}, // reverse DNS + + // Unexpected IPv6 variants + {identifier.ACMEIdentifier{Type: "ip", Value: `3fff:aaa:a:c0ff:ee:a:bad:deed:ffff`}, errIPInvalid}, // extra octet + {identifier.ACMEIdentifier{Type: "ip", Value: `3fff:aaa:a:c0ff:ee:a:bad:mead`}, errIPInvalid}, // character out of range + {identifier.ACMEIdentifier{Type: "ip", Value: `2001:db8::/32`}, errIPInvalid}, // with CIDR + {identifier.ACMEIdentifier{Type: "ip", Value: `[3fff:aaa:a:c0ff:ee:a:bad:deed]`}, errIPInvalid}, // in brackets + {identifier.ACMEIdentifier{Type: "ip", Value: `[3fff:aaa:a:c0ff:ee:a:bad:deed]:443`}, errIPInvalid}, // in brackets, with port + {identifier.ACMEIdentifier{Type: "ip", Value: `0x3fff0aaa000ac0ff00ee000a0baddeed`}, errIPInvalid}, // as hex + {identifier.ACMEIdentifier{Type: "ip", Value: `d.e.e.d.d.a.b.0.a.0.0.0.e.e.0.0.f.f.0.c.a.0.0.0.a.a.a.0.f.f.f.3.ip6.arpa`}, errIPInvalid}, // reverse DNS + {identifier.ACMEIdentifier{Type: "ip", Value: `3fff:0aaa:a:c0ff:ee:a:bad:deed`}, errIPInvalid}, // leading 0 in 2nd octet (RFC 5952, Sec. 4.1) + {identifier.ACMEIdentifier{Type: "ip", Value: `3fff:aaa:0:0:0:a:bad:deed`}, errIPInvalid}, // lone 0s in 3rd-5th octets, :: not used (RFC 5952, Sec. 4.2.1) + {identifier.ACMEIdentifier{Type: "ip", Value: `3fff:aaa::c0ff:ee:a:bad:deed`}, errIPInvalid}, // :: used for just one empty octet (RFC 5952, Sec. 4.2.2) + {identifier.ACMEIdentifier{Type: "ip", Value: `3fff:aaa::ee:0:0:0`}, errIPInvalid}, // :: used for the shorter of two possible collapses (RFC 5952, Sec. 4.2.3) + {identifier.ACMEIdentifier{Type: "ip", Value: `fe80:0:0:0:a::`}, errIPInvalid}, // :: used for the last of two possible equal-length collapses (RFC 5952, Sec. 4.2.3) + {identifier.ACMEIdentifier{Type: "ip", Value: `3fff:aaa:a:C0FF:EE:a:bad:deed`}, errIPInvalid}, // alpha characters capitalized (RFC 5952, Sec. 4.3) + {identifier.ACMEIdentifier{Type: "ip", Value: `::ffff:192.168.1.1`}, berrors.MalformedError("IP address is in a reserved address block")}, // IPv6-encapsulated IPv4 + + // IANA special-purpose address blocks + {identifier.NewIP(netip.MustParseAddr("192.0.2.129")), berrors.MalformedError("IP address is in a reserved address block")}, // Documentation (TEST-NET-1) + {identifier.NewIP(netip.MustParseAddr("2001:db8:eee:eeee:eeee:eeee:d01:f1")), berrors.MalformedError("IP address is in a reserved address block")}, // Documentation } // Test syntax errors for _, tc := range testCases { - err := WellFormedDomainNames([]string{tc.domain}) + err := WellFormedIdentifiers(identifier.ACMEIdentifiers{tc.ident}) if tc.err == nil { - test.AssertNil(t, err, fmt.Sprintf("Unexpected error for domain %q, got %s", tc.domain, err)) + test.AssertNil(t, err, fmt.Sprintf("Unexpected error for %q identifier %q, got %s", tc.ident.Type, tc.ident.Value, err)) } else { - test.AssertError(t, err, fmt.Sprintf("Expected error for domain %q, but got none", tc.domain)) + test.AssertError(t, err, fmt.Sprintf("Expected error for %q identifier %q, but got none", tc.ident.Type, tc.ident.Value)) var berr *berrors.BoulderError test.AssertErrorWraps(t, err, &berr) test.AssertContains(t, berr.Error(), tc.err.Error()) @@ -121,13 +171,13 @@ func TestWellFormedDomainNames(t *testing.T) { } func TestWillingToIssue(t *testing.T) { - shouldBeBlocked := []string{ - `highvalue.website1.org`, - `website2.co.uk`, - `www.website3.com`, - `lots.of.labels.website4.com`, - `banned.in.dc.com`, - `bad.brains.banned.in.dc.com`, + shouldBeBlocked := identifier.ACMEIdentifiers{ + identifier.NewDNS(`highvalue.website1.org`), + identifier.NewDNS(`website2.co.uk`), + identifier.NewDNS(`www.website3.com`), + identifier.NewDNS(`lots.of.labels.website4.com`), + identifier.NewDNS(`banned.in.dc.com`), + identifier.NewDNS(`bad.brains.banned.in.dc.com`), } blocklistContents := []string{ `website2.com`, @@ -145,15 +195,17 @@ func TestWillingToIssue(t *testing.T) { `banned.in.dc.com`, } - shouldBeAccepted := []string{ - `lowvalue.website1.org`, - `website4.sucks`, - "www.unrelated.com", - "unrelated.com", - "www.8675309.com", - "8675309.com", - "web5ite2.com", - "www.web-site2.com", + shouldBeAccepted := identifier.ACMEIdentifiers{ + identifier.NewDNS(`lowvalue.website1.org`), + identifier.NewDNS(`website4.sucks`), + identifier.NewDNS(`www.unrelated.com`), + identifier.NewDNS(`unrelated.com`), + identifier.NewDNS(`www.8675309.com`), + identifier.NewDNS(`8675309.com`), + identifier.NewDNS(`web5ite2.com`), + identifier.NewDNS(`www.web-site2.com`), + identifier.NewIP(netip.MustParseAddr(`9.9.9.9`)), + identifier.NewIP(netip.MustParseAddr(`2620:fe::fe`)), } policy := blockedNamesPolicy{ @@ -175,29 +227,32 @@ func TestWillingToIssue(t *testing.T) { test.AssertNotError(t, err, "Couldn't load rules") // Invalid encoding - err = pa.WillingToIssue([]string{"www.xn--m.com"}) + err = pa.WillingToIssue(identifier.ACMEIdentifiers{identifier.NewDNS("www.xn--m.com")}) test.AssertError(t, err, "WillingToIssue didn't fail on a malformed IDN") + // Invalid identifier type + err = pa.WillingToIssue(identifier.ACMEIdentifiers{identifier.ACMEIdentifier{Type: "fnord", Value: "uh-oh, Spaghetti-Os[tm]"}}) + test.AssertError(t, err, "WillingToIssue didn't fail on an invalid identifier type") // Valid encoding - err = pa.WillingToIssue([]string{"www.xn--mnich-kva.com"}) + err = pa.WillingToIssue(identifier.ACMEIdentifiers{identifier.NewDNS("www.xn--mnich-kva.com")}) test.AssertNotError(t, err, "WillingToIssue failed on a properly formed IDN") // IDN TLD - err = pa.WillingToIssue([]string{"xn--example--3bhk5a.xn--p1ai"}) + err = pa.WillingToIssue(identifier.ACMEIdentifiers{identifier.NewDNS("xn--example--3bhk5a.xn--p1ai")}) test.AssertNotError(t, err, "WillingToIssue failed on a properly formed domain with IDN TLD") features.Reset() // Test expected blocked domains - for _, domain := range shouldBeBlocked { - err := pa.WillingToIssue([]string{domain}) - test.AssertError(t, err, "domain was not correctly forbidden") + for _, ident := range shouldBeBlocked { + err := pa.WillingToIssue(identifier.ACMEIdentifiers{ident}) + test.AssertError(t, err, "identifier was not correctly forbidden") var berr *berrors.BoulderError test.AssertErrorWraps(t, err, &berr) test.AssertContains(t, berr.Detail, errPolicyForbidden.Error()) } // Test acceptance of good names - for _, domain := range shouldBeAccepted { - err := pa.WillingToIssue([]string{domain}) - test.AssertNotError(t, err, "domain was incorrectly forbidden") + for _, ident := range shouldBeAccepted { + err := pa.WillingToIssue(identifier.ACMEIdentifiers{ident}) + test.AssertNotError(t, err, "identifier was incorrectly forbidden") } } @@ -282,7 +337,7 @@ func TestWillingToIssue_Wildcards(t *testing.T) { for _, tc := range testCases { t.Run(tc.Name, func(t *testing.T) { - err := pa.WillingToIssue([]string{tc.Domain}) + err := pa.WillingToIssue(identifier.ACMEIdentifiers{identifier.NewDNS(tc.Domain)}) if tc.ExpectedErr == nil { test.AssertNil(t, err, fmt.Sprintf("Unexpected error for domain %q, got %s", tc.Domain, err)) } else { @@ -317,12 +372,12 @@ func TestWillingToIssue_SubErrors(t *testing.T) { test.AssertNotError(t, err, "Couldn't load policy contents from file") // Test multiple malformed domains and one banned domain; only the malformed ones will generate errors - err = pa.WillingToIssue([]string{ - "perfectly-fine.com", // fine - "letsdecrypt_org", // malformed - "example.comm", // malformed - "letsdecrypt.org", // banned - "also-perfectly-fine.com", // fine + err = pa.WillingToIssue(identifier.ACMEIdentifiers{ + identifier.NewDNS("perfectly-fine.com"), // fine + identifier.NewDNS("letsdecrypt_org"), // malformed + identifier.NewDNS("example.comm"), // malformed + identifier.NewDNS("letsdecrypt.org"), // banned + identifier.NewDNS("also-perfectly-fine.com"), // fine }) test.AssertDeepEquals(t, err, &berrors.BoulderError{ @@ -334,24 +389,24 @@ func TestWillingToIssue_SubErrors(t *testing.T) { Type: berrors.Malformed, Detail: "Domain name contains an invalid character", }, - Identifier: identifier.ACMEIdentifier{Type: identifier.DNS, Value: "letsdecrypt_org"}, + Identifier: identifier.NewDNS("letsdecrypt_org"), }, { BoulderError: &berrors.BoulderError{ Type: berrors.Malformed, Detail: "Domain name does not end with a valid public suffix (TLD)", }, - Identifier: identifier.ACMEIdentifier{Type: identifier.DNS, Value: "example.comm"}, + Identifier: identifier.NewDNS("example.comm"), }, }, }) // Test multiple banned domains. - err = pa.WillingToIssue([]string{ - "perfectly-fine.com", // fine - "letsdecrypt.org", // banned - "example.com", // banned - "also-perfectly-fine.com", // fine + err = pa.WillingToIssue(identifier.ACMEIdentifiers{ + identifier.NewDNS("perfectly-fine.com"), // fine + identifier.NewDNS("letsdecrypt.org"), // banned + identifier.NewDNS("example.com"), // banned + identifier.NewDNS("also-perfectly-fine.com"), // fine }) test.AssertError(t, err, "Expected err from WillingToIssueWildcards") @@ -365,20 +420,20 @@ func TestWillingToIssue_SubErrors(t *testing.T) { Type: berrors.RejectedIdentifier, Detail: "The ACME server refuses to issue a certificate for this domain name, because it is forbidden by policy", }, - Identifier: identifier.ACMEIdentifier{Type: identifier.DNS, Value: "letsdecrypt.org"}, + Identifier: identifier.NewDNS("letsdecrypt.org"), }, { BoulderError: &berrors.BoulderError{ Type: berrors.RejectedIdentifier, Detail: "The ACME server refuses to issue a certificate for this domain name, because it is forbidden by policy", }, - Identifier: identifier.ACMEIdentifier{Type: identifier.DNS, Value: "example.com"}, + Identifier: identifier.NewDNS("example.com"), }, }, }) // Test willing to issue with only *one* bad identifier. - err = pa.WillingToIssue([]string{"letsdecrypt.org"}) + err = pa.WillingToIssue(identifier.ACMEIdentifiers{identifier.NewDNS("letsdecrypt.org")}) test.AssertDeepEquals(t, err, &berrors.BoulderError{ Type: berrors.RejectedIdentifier, @@ -386,54 +441,60 @@ func TestWillingToIssue_SubErrors(t *testing.T) { }) } -func TestChallengesFor(t *testing.T) { +func TestChallengeTypesFor(t *testing.T) { + t.Parallel() pa := paImpl(t) - challenges, err := pa.ChallengesFor(identifier.ACMEIdentifier{}) - test.AssertNotError(t, err, "ChallengesFor failed") - - test.Assert(t, len(challenges) == len(enabledChallenges), "Wrong number of challenges returned") - - seenChalls := make(map[core.AcmeChallenge]bool) - for _, challenge := range challenges { - test.Assert(t, !seenChalls[challenge.Type], "should not already have seen this type") - seenChalls[challenge.Type] = true - - test.Assert(t, enabledChallenges[challenge.Type], "Unsupported challenge returned") - } - test.AssertEquals(t, len(seenChalls), len(enabledChallenges)) - -} - -func TestChallengesForWildcard(t *testing.T) { - // wildcardIdent is an identifier for a wildcard domain name - wildcardIdent := identifier.ACMEIdentifier{ - Type: identifier.DNS, - Value: "*.zombo.com", + testCases := []struct { + name string + ident identifier.ACMEIdentifier + wantChalls []core.AcmeChallenge + wantErr string + }{ + { + name: "dns", + ident: identifier.NewDNS("example.com"), + wantChalls: []core.AcmeChallenge{ + core.ChallengeTypeHTTP01, core.ChallengeTypeDNS01, core.ChallengeTypeTLSALPN01, + }, + }, + { + name: "dns wildcard", + ident: identifier.NewDNS("*.example.com"), + wantChalls: []core.AcmeChallenge{ + core.ChallengeTypeDNS01, + }, + }, + { + name: "ip", + ident: identifier.NewIP(netip.MustParseAddr("1.2.3.4")), + wantChalls: []core.AcmeChallenge{ + core.ChallengeTypeHTTP01, core.ChallengeTypeTLSALPN01, + }, + }, + { + name: "invalid", + ident: identifier.ACMEIdentifier{Type: "fnord", Value: "uh-oh, Spaghetti-Os[tm]"}, + wantErr: "unrecognized identifier type", + }, } - // First try to get a challenge for the wildcard ident without the - // DNS-01 challenge type enabled. This should produce an error - var enabledChallenges = map[core.AcmeChallenge]bool{ - core.ChallengeTypeHTTP01: true, - core.ChallengeTypeDNS01: false, - } - pa := must.Do(New(enabledChallenges, blog.NewMock())) - _, err := pa.ChallengesFor(wildcardIdent) - test.AssertError(t, err, "ChallengesFor did not error for a wildcard ident "+ - "when DNS-01 was disabled") - test.AssertEquals(t, err.Error(), "Challenges requested for wildcard "+ - "identifier but DNS-01 challenge type is not enabled") + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + challs, err := pa.ChallengeTypesFor(tc.ident) - // Try again with DNS-01 enabled. It should not error and - // should return only one DNS-01 type challenge - enabledChallenges[core.ChallengeTypeDNS01] = true - pa = must.Do(New(enabledChallenges, blog.NewMock())) - challenges, err := pa.ChallengesFor(wildcardIdent) - test.AssertNotError(t, err, "ChallengesFor errored for a wildcard ident "+ - "unexpectedly") - test.AssertEquals(t, len(challenges), 1) - test.AssertEquals(t, challenges[0].Type, core.ChallengeTypeDNS01) + if len(tc.wantChalls) != 0 { + test.AssertNotError(t, err, "should have succeeded") + test.AssertDeepEquals(t, challs, tc.wantChalls) + } + + if tc.wantErr != "" { + test.AssertError(t, err, "should have errored") + test.AssertContains(t, err.Error(), tc.wantErr) + } + }) + } } // TestMalformedExactBlocklist tests that loading a YAML policy file with an @@ -472,14 +533,200 @@ func TestMalformedExactBlocklist(t *testing.T) { func TestValidEmailError(t *testing.T) { err := ValidEmail("(๑•́ ω •̀๑)") - test.AssertEquals(t, err.Error(), "\"(๑•́ ω •̀๑)\" is not a valid e-mail address") + test.AssertEquals(t, err.Error(), "unable to parse email address") err = ValidEmail("john.smith@gmail.com #replace with real email") - test.AssertEquals(t, err.Error(), "\"john.smith@gmail.com #replace with real email\" is not a valid e-mail address") + test.AssertEquals(t, err.Error(), "unable to parse email address") err = ValidEmail("example@example.com") - test.AssertEquals(t, err.Error(), "invalid contact domain. Contact emails @example.com are forbidden") + test.AssertEquals(t, err.Error(), "contact email has forbidden domain \"example.com\"") err = ValidEmail("example@-foobar.com") - test.AssertEquals(t, err.Error(), "contact email \"example@-foobar.com\" has invalid domain : Domain name contains an invalid character") + test.AssertEquals(t, err.Error(), "contact email has invalid domain: Domain name contains an invalid character") +} + +func TestCheckAuthzChallenges(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + authz core.Authorization + enabled map[core.AcmeChallenge]bool + wantErr string + }{ + { + name: "unrecognized identifier", + authz: core.Authorization{ + Identifier: identifier.ACMEIdentifier{Type: "oops", Value: "example.com"}, + Challenges: []core.Challenge{{Type: core.ChallengeTypeDNS01, Status: core.StatusValid}}, + }, + wantErr: "unrecognized identifier type", + }, + { + name: "no challenges", + authz: core.Authorization{ + Identifier: identifier.NewDNS("example.com"), + Challenges: []core.Challenge{}, + }, + wantErr: "has no challenges", + }, + { + name: "no valid challenges", + authz: core.Authorization{ + Identifier: identifier.NewDNS("example.com"), + Challenges: []core.Challenge{{Type: core.ChallengeTypeDNS01, Status: core.StatusPending}}, + }, + wantErr: "not solved by any challenge", + }, + { + name: "solved by disabled challenge", + authz: core.Authorization{ + Identifier: identifier.NewDNS("example.com"), + Challenges: []core.Challenge{{Type: core.ChallengeTypeDNS01, Status: core.StatusValid}}, + }, + enabled: map[core.AcmeChallenge]bool{core.ChallengeTypeHTTP01: true}, + wantErr: "disabled challenge type", + }, + { + name: "solved by wrong kind of challenge", + authz: core.Authorization{ + Identifier: identifier.NewDNS("*.example.com"), + Challenges: []core.Challenge{{Type: core.ChallengeTypeHTTP01, Status: core.StatusValid}}, + }, + wantErr: "inapplicable challenge type", + }, + { + name: "valid authz", + authz: core.Authorization{ + Identifier: identifier.NewDNS("example.com"), + Challenges: []core.Challenge{{Type: core.ChallengeTypeTLSALPN01, Status: core.StatusValid}}, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + pa := paImpl(t) + + if tc.enabled != nil { + pa.enabledChallenges = tc.enabled + } + + err := pa.CheckAuthzChallenges(&tc.authz) + + if tc.wantErr == "" { + test.AssertNotError(t, err, "should have succeeded") + } else { + test.AssertError(t, err, "should have errored") + test.AssertContains(t, err.Error(), tc.wantErr) + } + }) + } +} + +func TestWillingToIssue_IdentifierType(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + ident identifier.ACMEIdentifier + enabled map[identifier.IdentifierType]bool + wantErr string + }{ + { + name: "DNS identifier, none enabled", + ident: identifier.NewDNS("example.com"), + enabled: nil, + wantErr: "The ACME server has disabled this identifier type", + }, + { + name: "DNS identifier, DNS enabled", + ident: identifier.NewDNS("example.com"), + enabled: map[identifier.IdentifierType]bool{identifier.TypeDNS: true}, + wantErr: "", + }, + { + name: "DNS identifier, DNS & IP enabled", + ident: identifier.NewDNS("example.com"), + enabled: map[identifier.IdentifierType]bool{identifier.TypeDNS: true, identifier.TypeIP: true}, + wantErr: "", + }, + { + name: "DNS identifier, IP enabled", + ident: identifier.NewDNS("example.com"), + enabled: map[identifier.IdentifierType]bool{identifier.TypeIP: true}, + wantErr: "The ACME server has disabled this identifier type", + }, + { + name: "IP identifier, none enabled", + ident: identifier.NewIP(netip.MustParseAddr("9.9.9.9")), + enabled: nil, + wantErr: "The ACME server has disabled this identifier type", + }, + { + name: "IP identifier, DNS enabled", + ident: identifier.NewIP(netip.MustParseAddr("9.9.9.9")), + enabled: map[identifier.IdentifierType]bool{identifier.TypeDNS: true}, + wantErr: "The ACME server has disabled this identifier type", + }, + { + name: "IP identifier, DNS & IP enabled", + ident: identifier.NewIP(netip.MustParseAddr("9.9.9.9")), + enabled: map[identifier.IdentifierType]bool{identifier.TypeDNS: true, identifier.TypeIP: true}, + wantErr: "", + }, + { + name: "IP identifier, IP enabled", + ident: identifier.NewIP(netip.MustParseAddr("9.9.9.9")), + enabled: map[identifier.IdentifierType]bool{identifier.TypeIP: true}, + wantErr: "", + }, + { + name: "invalid identifier type", + ident: identifier.ACMEIdentifier{Type: "drywall", Value: "oh yeah!"}, + enabled: map[identifier.IdentifierType]bool{"drywall": true}, + wantErr: "Invalid identifier type", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + policy := blockedNamesPolicy{ + HighRiskBlockedNames: []string{"zombo.gov.us"}, + ExactBlockedNames: []string{`highvalue.website1.org`}, + AdminBlockedNames: []string{`banned.in.dc.com`}, + } + + yamlPolicyBytes, err := yaml.Marshal(policy) + test.AssertNotError(t, err, "Couldn't YAML serialize blocklist") + yamlPolicyFile, _ := os.CreateTemp("", "test-blocklist.*.yaml") + defer os.Remove(yamlPolicyFile.Name()) + err = os.WriteFile(yamlPolicyFile.Name(), yamlPolicyBytes, 0640) + test.AssertNotError(t, err, "Couldn't write YAML blocklist") + + pa := paImpl(t) + + err = pa.LoadHostnamePolicyFile(yamlPolicyFile.Name()) + test.AssertNotError(t, err, "Couldn't load rules") + + pa.enabledIdentifiers = tc.enabled + + err = pa.WillingToIssue(identifier.ACMEIdentifiers{tc.ident}) + + if tc.wantErr == "" { + if err != nil { + t.Errorf("should have succeeded, but got error: %s", err.Error()) + } + } else { + if err == nil { + t.Errorf("should have failed") + } else if !strings.Contains(err.Error(), tc.wantErr) { + t.Errorf("wrong error; wanted '%s', but got '%s'", tc.wantErr, err.Error()) + } + } + }) + } } diff --git a/third-party/github.com/letsencrypt/boulder/probs/probs.go b/third-party/github.com/letsencrypt/boulder/probs/probs.go index ec6c272ae..7ff35ca61 100644 --- a/third-party/github.com/letsencrypt/boulder/probs/probs.go +++ b/third-party/github.com/letsencrypt/boulder/probs/probs.go @@ -4,6 +4,8 @@ import ( "fmt" "net/http" + "github.com/go-jose/go-jose/v4" + "github.com/letsencrypt/boulder/identifier" ) @@ -12,7 +14,11 @@ const ( // same order as they are defined in RFC8555 Section 6.7. We do not implement // the `compound`, `externalAccountRequired`, or `userActionRequired` errors, // because we have no path that would return them. - AccountDoesNotExistProblem = ProblemType("accountDoesNotExist") + AccountDoesNotExistProblem = ProblemType("accountDoesNotExist") + // AlreadyReplacedProblem is a problem type that is defined in Section 7.4 + // of draft-ietf-acme-ari-08, for more information see: + // https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-08#section-7.4 + AlreadyReplacedProblem = ProblemType("alreadyReplaced") AlreadyRevokedProblem = ProblemType("alreadyRevoked") BadCSRProblem = ProblemType("badCSR") BadNonceProblem = ProblemType("badNonce") @@ -27,6 +33,7 @@ const ( InvalidContactProblem = ProblemType("invalidContact") MalformedProblem = ProblemType("malformed") OrderNotReadyProblem = ProblemType("orderNotReady") + PausedProblem = ProblemType("rateLimited") RateLimitedProblem = ProblemType("rateLimited") RejectedIdentifierProblem = ProblemType("rejectedIdentifier") ServerInternalProblem = ProblemType("serverInternal") @@ -35,6 +42,9 @@ const ( UnsupportedContactProblem = ProblemType("unsupportedContact") UnsupportedIdentifierProblem = ProblemType("unsupportedIdentifier") + // Defined in https://datatracker.ietf.org/doc/draft-aaron-acme-profiles/ + InvalidProfileProblem = ProblemType("invalidProfile") + ErrorNS = "urn:ietf:params:acme:error:" ) @@ -52,6 +62,10 @@ type ProblemDetails struct { // SubProblems are optional additional per-identifier problems. See // RFC 8555 Section 6.7.1: https://tools.ietf.org/html/rfc8555#section-6.7.1 SubProblems []SubProblemDetails `json:"subproblems,omitempty"` + // Algorithms is an extension field defined only for problem documents of type + // badSignatureAlgorithm. See RFC 8555, Section 6.2: + // https://datatracker.ietf.org/doc/html/rfc8555#section-6.2 + Algorithms []jose.SignatureAlgorithm `json:"algorithms,omitempty"` } // SubProblemDetails represents sub-problems specific to an identifier that are @@ -62,7 +76,7 @@ type SubProblemDetails struct { Identifier identifier.ACMEIdentifier `json:"identifier"` } -func (pd *ProblemDetails) Error() string { +func (pd *ProblemDetails) String() string { return fmt.Sprintf("%s :: %s", pd.Type, pd.Detail) } @@ -90,21 +104,31 @@ func AccountDoesNotExist(detail string) *ProblemDetails { } } +// AlreadyReplaced returns a ProblemDetails with a AlreadyReplacedProblem and a +// 409 Conflict status code. +func AlreadyReplaced(detail string) *ProblemDetails { + return &ProblemDetails{ + Type: AlreadyReplacedProblem, + Detail: detail, + HTTPStatus: http.StatusConflict, + } +} + // AlreadyRevoked returns a ProblemDetails with a AlreadyRevokedProblem and a 400 Bad // Request status code. -func AlreadyRevoked(detail string, a ...any) *ProblemDetails { +func AlreadyRevoked(detail string) *ProblemDetails { return &ProblemDetails{ Type: AlreadyRevokedProblem, - Detail: fmt.Sprintf(detail, a...), + Detail: detail, HTTPStatus: http.StatusBadRequest, } } // BadCSR returns a ProblemDetails representing a BadCSRProblem. -func BadCSR(detail string, a ...any) *ProblemDetails { +func BadCSR(detail string) *ProblemDetails { return &ProblemDetails{ Type: BadCSRProblem, - Detail: fmt.Sprintf(detail, a...), + Detail: detail, HTTPStatus: http.StatusBadRequest, } } @@ -121,30 +145,30 @@ func BadNonce(detail string) *ProblemDetails { // BadPublicKey returns a ProblemDetails with a BadPublicKeyProblem and a 400 Bad // Request status code. -func BadPublicKey(detail string, a ...any) *ProblemDetails { +func BadPublicKey(detail string) *ProblemDetails { return &ProblemDetails{ Type: BadPublicKeyProblem, - Detail: fmt.Sprintf(detail, a...), + Detail: detail, HTTPStatus: http.StatusBadRequest, } } // BadRevocationReason returns a ProblemDetails representing // a BadRevocationReasonProblem -func BadRevocationReason(detail string, a ...any) *ProblemDetails { +func BadRevocationReason(detail string) *ProblemDetails { return &ProblemDetails{ Type: BadRevocationReasonProblem, - Detail: fmt.Sprintf(detail, a...), + Detail: detail, HTTPStatus: http.StatusBadRequest, } } // BadSignatureAlgorithm returns a ProblemDetails with a BadSignatureAlgorithmProblem // and a 400 Bad Request status code. -func BadSignatureAlgorithm(detail string, a ...any) *ProblemDetails { +func BadSignatureAlgorithm(detail string) *ProblemDetails { return &ProblemDetails{ Type: BadSignatureAlgorithmProblem, - Detail: fmt.Sprintf(detail, a...), + Detail: detail, HTTPStatus: http.StatusBadRequest, } } @@ -200,10 +224,10 @@ func Malformed(detail string, a ...any) *ProblemDetails { } // OrderNotReady returns a ProblemDetails representing a OrderNotReadyProblem -func OrderNotReady(detail string, a ...any) *ProblemDetails { +func OrderNotReady(detail string) *ProblemDetails { return &ProblemDetails{ Type: OrderNotReadyProblem, - Detail: fmt.Sprintf(detail, a...), + Detail: detail, HTTPStatus: http.StatusForbidden, } } @@ -217,6 +241,15 @@ func RateLimited(detail string) *ProblemDetails { } } +// Paused returns a ProblemDetails representing a RateLimitedProblem error +func Paused(detail string) *ProblemDetails { + return &ProblemDetails{ + Type: PausedProblem, + Detail: detail, + HTTPStatus: http.StatusTooManyRequests, + } +} + // RejectedIdentifier returns a ProblemDetails with a RejectedIdentifierProblem and a 400 Bad // Request status code. func RejectedIdentifier(detail string) *ProblemDetails { @@ -302,26 +335,6 @@ func Conflict(detail string) *ProblemDetails { } } -// ContentLengthRequired returns a ProblemDetails representing a missing -// Content-Length header error -func ContentLengthRequired() *ProblemDetails { - return &ProblemDetails{ - Type: MalformedProblem, - Detail: "missing Content-Length header", - HTTPStatus: http.StatusLengthRequired, - } -} - -// InvalidContentType returns a ProblemDetails suitable for a missing -// ContentType header, or an incorrect ContentType header -func InvalidContentType(detail string) *ProblemDetails { - return &ProblemDetails{ - Type: MalformedProblem, - Detail: detail, - HTTPStatus: http.StatusUnsupportedMediaType, - } -} - // MethodNotAllowed returns a ProblemDetails representing a disallowed HTTP // method error. func MethodNotAllowed() *ProblemDetails { @@ -341,3 +354,13 @@ func NotFound(detail string) *ProblemDetails { HTTPStatus: http.StatusNotFound, } } + +// InvalidProfile returns a ProblemDetails with type InvalidProfile, specified +// in https://datatracker.ietf.org/doc/draft-aaron-acme-profiles/. +func InvalidProfile(detail string) *ProblemDetails { + return &ProblemDetails{ + Type: InvalidProfileProblem, + Detail: detail, + HTTPStatus: http.StatusBadRequest, + } +} diff --git a/third-party/github.com/letsencrypt/boulder/probs/probs_test.go b/third-party/github.com/letsencrypt/boulder/probs/probs_test.go index af00e899f..ceefdfc64 100644 --- a/third-party/github.com/letsencrypt/boulder/probs/probs_test.go +++ b/third-party/github.com/letsencrypt/boulder/probs/probs_test.go @@ -15,7 +15,7 @@ func TestProblemDetails(t *testing.T) { Detail: "Wat? o.O", HTTPStatus: 403, } - test.AssertEquals(t, pd.Error(), "malformed :: Wat? o.O") + test.AssertEquals(t, pd.String(), "malformed :: Wat? o.O") } func TestProblemDetailsConvenience(t *testing.T) { @@ -67,7 +67,7 @@ func TestWithSubProblems(t *testing.T) { } subProbs := []SubProblemDetails{ { - Identifier: identifier.DNSIdentifier("example.com"), + Identifier: identifier.NewDNS("example.com"), ProblemDetails: ProblemDetails{ Type: RateLimitedProblem, Detail: "don't you think you have enough certificates already?", @@ -75,7 +75,7 @@ func TestWithSubProblems(t *testing.T) { }, }, { - Identifier: identifier.DNSIdentifier("what about example.com"), + Identifier: identifier.NewDNS("what about example.com"), ProblemDetails: ProblemDetails{ Type: MalformedProblem, Detail: "try a real identifier value next time", @@ -92,7 +92,7 @@ func TestWithSubProblems(t *testing.T) { test.AssertDeepEquals(t, outResult.SubProblems, subProbs) // Adding another sub problem shouldn't squash the original sub problems anotherSubProb := SubProblemDetails{ - Identifier: identifier.DNSIdentifier("another ident"), + Identifier: identifier.NewDNS("another ident"), ProblemDetails: ProblemDetails{ Type: RateLimitedProblem, Detail: "yet another rate limit err", diff --git a/third-party/github.com/letsencrypt/boulder/publisher/proto/publisher.pb.go b/third-party/github.com/letsencrypt/boulder/publisher/proto/publisher.pb.go index 9705dea9a..50574d436 100644 --- a/third-party/github.com/letsencrypt/boulder/publisher/proto/publisher.pb.go +++ b/third-party/github.com/letsencrypt/boulder/publisher/proto/publisher.pb.go @@ -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: publisher.proto @@ -11,6 +11,7 @@ import ( protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" + unsafe "unsafe" ) const ( @@ -73,23 +74,20 @@ func (SubmissionType) EnumDescriptor() ([]byte, []int) { } type Request struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Der []byte `protobuf:"bytes,1,opt,name=der,proto3" json:"der,omitempty"` + LogURL string `protobuf:"bytes,2,opt,name=LogURL,proto3" json:"LogURL,omitempty"` + LogPublicKey string `protobuf:"bytes,3,opt,name=LogPublicKey,proto3" json:"LogPublicKey,omitempty"` + Kind SubmissionType `protobuf:"varint,5,opt,name=kind,proto3,enum=SubmissionType" json:"kind,omitempty"` unknownFields protoimpl.UnknownFields - - Der []byte `protobuf:"bytes,1,opt,name=der,proto3" json:"der,omitempty"` - LogURL string `protobuf:"bytes,2,opt,name=LogURL,proto3" json:"LogURL,omitempty"` - LogPublicKey string `protobuf:"bytes,3,opt,name=LogPublicKey,proto3" json:"LogPublicKey,omitempty"` - Kind SubmissionType `protobuf:"varint,5,opt,name=kind,proto3,enum=SubmissionType" json:"kind,omitempty"` + sizeCache protoimpl.SizeCache } func (x *Request) Reset() { *x = Request{} - if protoimpl.UnsafeEnabled { - mi := &file_publisher_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_publisher_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Request) String() string { @@ -100,7 +98,7 @@ func (*Request) ProtoMessage() {} func (x *Request) ProtoReflect() protoreflect.Message { mi := &file_publisher_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) @@ -144,20 +142,17 @@ func (x *Request) GetKind() SubmissionType { } type Result struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Sct []byte `protobuf:"bytes,1,opt,name=sct,proto3" json:"sct,omitempty"` unknownFields protoimpl.UnknownFields - - Sct []byte `protobuf:"bytes,1,opt,name=sct,proto3" json:"sct,omitempty"` + sizeCache protoimpl.SizeCache } func (x *Result) Reset() { *x = Result{} - if protoimpl.UnsafeEnabled { - mi := &file_publisher_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_publisher_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Result) String() string { @@ -168,7 +163,7 @@ func (*Result) ProtoMessage() {} func (x *Result) ProtoReflect() protoreflect.Message { mi := &file_publisher_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) @@ -192,7 +187,7 @@ func (x *Result) GetSct() []byte { var File_publisher_proto protoreflect.FileDescriptor -var file_publisher_proto_rawDesc = []byte{ +var file_publisher_proto_rawDesc = string([]byte{ 0x0a, 0x0f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x82, 0x01, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x64, 0x65, 0x72, 0x12, @@ -216,23 +211,23 @@ var file_publisher_proto_rawDesc = []byte{ 0x74, 0x73, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x2f, 0x62, 0x6f, 0x75, 0x6c, 0x64, 0x65, 0x72, 0x2f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x65, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, -} +}) var ( file_publisher_proto_rawDescOnce sync.Once - file_publisher_proto_rawDescData = file_publisher_proto_rawDesc + file_publisher_proto_rawDescData []byte ) func file_publisher_proto_rawDescGZIP() []byte { file_publisher_proto_rawDescOnce.Do(func() { - file_publisher_proto_rawDescData = protoimpl.X.CompressGZIP(file_publisher_proto_rawDescData) + file_publisher_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_publisher_proto_rawDesc), len(file_publisher_proto_rawDesc))) }) return file_publisher_proto_rawDescData } var file_publisher_proto_enumTypes = make([]protoimpl.EnumInfo, 1) var file_publisher_proto_msgTypes = make([]protoimpl.MessageInfo, 2) -var file_publisher_proto_goTypes = []interface{}{ +var file_publisher_proto_goTypes = []any{ (SubmissionType)(0), // 0: SubmissionType (*Request)(nil), // 1: Request (*Result)(nil), // 2: Result @@ -253,37 +248,11 @@ func file_publisher_proto_init() { if File_publisher_proto != nil { return } - if !protoimpl.UnsafeEnabled { - file_publisher_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Request); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_publisher_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Result); 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_publisher_proto_rawDesc, + RawDescriptor: unsafe.Slice(unsafe.StringData(file_publisher_proto_rawDesc), len(file_publisher_proto_rawDesc)), NumEnums: 1, NumMessages: 2, NumExtensions: 0, @@ -295,7 +264,6 @@ func file_publisher_proto_init() { MessageInfos: file_publisher_proto_msgTypes, }.Build() File_publisher_proto = out.File - file_publisher_proto_rawDesc = nil file_publisher_proto_goTypes = nil file_publisher_proto_depIdxs = nil } diff --git a/third-party/github.com/letsencrypt/boulder/publisher/proto/publisher_grpc.pb.go b/third-party/github.com/letsencrypt/boulder/publisher/proto/publisher_grpc.pb.go index 0c91e6fb5..852b6bc2b 100644 --- a/third-party/github.com/letsencrypt/boulder/publisher/proto/publisher_grpc.pb.go +++ b/third-party/github.com/letsencrypt/boulder/publisher/proto/publisher_grpc.pb.go @@ -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: publisher.proto @@ -49,20 +49,24 @@ func (c *publisherClient) SubmitToSingleCTWithResult(ctx context.Context, in *Re // PublisherServer is the server API for Publisher service. // All implementations must embed UnimplementedPublisherServer -// for forward compatibility +// for forward compatibility. type PublisherServer interface { SubmitToSingleCTWithResult(context.Context, *Request) (*Result, error) mustEmbedUnimplementedPublisherServer() } -// UnimplementedPublisherServer must be embedded to have forward compatible implementations. -type UnimplementedPublisherServer struct { -} +// UnimplementedPublisherServer 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 UnimplementedPublisherServer struct{} func (UnimplementedPublisherServer) SubmitToSingleCTWithResult(context.Context, *Request) (*Result, error) { return nil, status.Errorf(codes.Unimplemented, "method SubmitToSingleCTWithResult not implemented") } func (UnimplementedPublisherServer) mustEmbedUnimplementedPublisherServer() {} +func (UnimplementedPublisherServer) testEmbeddedByValue() {} // UnsafePublisherServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to PublisherServer will @@ -72,6 +76,13 @@ type UnsafePublisherServer interface { } func RegisterPublisherServer(s grpc.ServiceRegistrar, srv PublisherServer) { + // If the following call pancis, it indicates UnimplementedPublisherServer 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(&Publisher_ServiceDesc, srv) } diff --git a/third-party/github.com/letsencrypt/boulder/publisher/publisher.go b/third-party/github.com/letsencrypt/boulder/publisher/publisher.go index 7e43a56f6..b213054ed 100644 --- a/third-party/github.com/letsencrypt/boulder/publisher/publisher.go +++ b/third-party/github.com/letsencrypt/boulder/publisher/publisher.go @@ -25,7 +25,6 @@ import ( cttls "github.com/google/certificate-transparency-go/tls" "github.com/prometheus/client_golang/prometheus" - "github.com/letsencrypt/boulder/canceled" "github.com/letsencrypt/boulder/core" "github.com/letsencrypt/boulder/issuance" blog "github.com/letsencrypt/boulder/log" @@ -40,11 +39,20 @@ type Log struct { client *ctClient.LogClient } +// cacheKey is a comparable type for use as a key within a logCache. It holds +// both the log URI and its log_id (base64 encoding of its pubkey), so that +// the cache won't interfere if the RA decides that a log's URI or pubkey has +// changed. +type cacheKey struct { + uri string + pubkey string +} + // logCache contains a cache of *Log's that are constructed as required by // `SubmitToSingleCT` type logCache struct { sync.RWMutex - logs map[string]*Log + logs map[cacheKey]*Log } // AddLog adds a *Log to the cache by constructing the statName, client and @@ -52,7 +60,7 @@ type logCache struct { func (c *logCache) AddLog(uri, b64PK, userAgent string, logger blog.Logger) (*Log, error) { // Lock the mutex for reading to check the cache c.RLock() - log, present := c.logs[b64PK] + log, present := c.logs[cacheKey{uri, b64PK}] c.RUnlock() // If we have already added this log, give it back @@ -69,7 +77,7 @@ func (c *logCache) AddLog(uri, b64PK, userAgent string, logger blog.Logger) (*Lo if err != nil { return nil, err } - c.logs[b64PK] = log + c.logs[cacheKey{uri, b64PK}] = log return log, nil } @@ -219,7 +227,7 @@ func New( issuerBundles: bundles, userAgent: userAgent, ctLogsCache: logCache{ - logs: make(map[string]*Log), + logs: make(map[cacheKey]*Log), }, log: logger, metrics: initMetrics(stats), @@ -261,7 +269,7 @@ func (pub *Impl) SubmitToSingleCTWithResult(ctx context.Context, req *pubpb.Requ sct, err := pub.singleLogSubmit(ctx, chain, req.Kind, ctLog) if err != nil { - if canceled.Is(err) { + if core.IsCanceled(err) { return nil, err } var body string @@ -297,7 +305,7 @@ func (pub *Impl) singleLogSubmit( took := time.Since(start).Seconds() if err != nil { status := "error" - if canceled.Is(err) { + if core.IsCanceled(err) { status = "canceled" } httpStatus := "" @@ -324,15 +332,16 @@ func (pub *Impl) singleLogSubmit( "http_status": "", }).Observe(took) - timestamp := time.Unix(int64(sct.Timestamp)/1000, 0) - if time.Until(timestamp) > time.Minute { - return nil, fmt.Errorf("SCT Timestamp was too far in the future (%s)", timestamp) + threshold := uint64(time.Now().Add(time.Minute).UnixMilli()) //nolint: gosec // Current-ish timestamp is guaranteed to fit in a uint64 + if sct.Timestamp > threshold { + return nil, fmt.Errorf("SCT Timestamp was too far in the future (%d > %d)", sct.Timestamp, threshold) } // For regular certificates, we could get an old SCT, but that shouldn't // happen for precertificates. - if kind != pubpb.SubmissionType_final && time.Until(timestamp) < -10*time.Minute { - return nil, fmt.Errorf("SCT Timestamp was too far in the past (%s)", timestamp) + threshold = uint64(time.Now().Add(-10 * time.Minute).UnixMilli()) //nolint: gosec // Current-ish timestamp is guaranteed to fit in a uint64 + if kind != pubpb.SubmissionType_final && sct.Timestamp < threshold { + return nil, fmt.Errorf("SCT Timestamp was too far in the past (%d < %d)", sct.Timestamp, threshold) } return sct, nil @@ -363,7 +372,7 @@ func CreateTestingSignedSCT(req []string, k *ecdsa.PrivateKey, precert bool, tim // Sign the SCT rawKey, _ := x509.MarshalPKIXPublicKey(&k.PublicKey) logID := sha256.Sum256(rawKey) - timestampMillis := uint64(timestamp.UnixNano()) / 1e6 + timestampMillis := uint64(timestamp.UnixMilli()) //nolint: gosec // Current-ish timestamp is guaranteed to fit in a uint64 serialized, _ := ct.SerializeSCTSignatureInput(ct.SignedCertificateTimestamp{ SCTVersion: ct.V1, LogID: ct.LogID{KeyID: logID}, diff --git a/third-party/github.com/letsencrypt/boulder/publisher/publisher_test.go b/third-party/github.com/letsencrypt/boulder/publisher/publisher_test.go index 3ed5007fc..ea02d1cda 100644 --- a/third-party/github.com/letsencrypt/boulder/publisher/publisher_test.go +++ b/third-party/github.com/letsencrypt/boulder/publisher/publisher_test.go @@ -269,7 +269,7 @@ func TestTimestampVerificationPast(t *testing.T) { func TestLogCache(t *testing.T) { cache := logCache{ - logs: make(map[string]*Log), + logs: make(map[cacheKey]*Log), } // Adding a log with an invalid base64 public key should error diff --git a/third-party/github.com/letsencrypt/boulder/ra/proto/ra.pb.go b/third-party/github.com/letsencrypt/boulder/ra/proto/ra.pb.go index 34c6b7305..6617b0724 100644 --- a/third-party/github.com/letsencrypt/boulder/ra/proto/ra.pb.go +++ b/third-party/github.com/letsencrypt/boulder/ra/proto/ra.pb.go @@ -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: ra.proto @@ -11,9 +11,11 @@ import ( proto "github.com/letsencrypt/boulder/core/proto" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + durationpb "google.golang.org/protobuf/types/known/durationpb" emptypb "google.golang.org/protobuf/types/known/emptypb" reflect "reflect" sync "sync" + unsafe "unsafe" ) const ( @@ -23,21 +25,106 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) -type GenerateOCSPRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache +type SCTRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + PrecertDER []byte `protobuf:"bytes,1,opt,name=precertDER,proto3" json:"precertDER,omitempty"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} - Serial string `protobuf:"bytes,1,opt,name=serial,proto3" json:"serial,omitempty"` +func (x *SCTRequest) Reset() { + *x = SCTRequest{} + mi := &file_ra_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SCTRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SCTRequest) ProtoMessage() {} + +func (x *SCTRequest) ProtoReflect() protoreflect.Message { + mi := &file_ra_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SCTRequest.ProtoReflect.Descriptor instead. +func (*SCTRequest) Descriptor() ([]byte, []int) { + return file_ra_proto_rawDescGZIP(), []int{0} +} + +func (x *SCTRequest) GetPrecertDER() []byte { + if x != nil { + return x.PrecertDER + } + return nil +} + +type SCTResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + SctDER [][]byte `protobuf:"bytes,1,rep,name=sctDER,proto3" json:"sctDER,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SCTResponse) Reset() { + *x = SCTResponse{} + mi := &file_ra_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SCTResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SCTResponse) ProtoMessage() {} + +func (x *SCTResponse) ProtoReflect() protoreflect.Message { + mi := &file_ra_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SCTResponse.ProtoReflect.Descriptor instead. +func (*SCTResponse) Descriptor() ([]byte, []int) { + return file_ra_proto_rawDescGZIP(), []int{1} +} + +func (x *SCTResponse) GetSctDER() [][]byte { + if x != nil { + return x.SctDER + } + return nil +} + +type GenerateOCSPRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Serial string `protobuf:"bytes,1,opt,name=serial,proto3" json:"serial,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GenerateOCSPRequest) Reset() { *x = GenerateOCSPRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_ra_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_ra_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *GenerateOCSPRequest) String() string { @@ -47,8 +134,8 @@ func (x *GenerateOCSPRequest) String() string { func (*GenerateOCSPRequest) ProtoMessage() {} func (x *GenerateOCSPRequest) ProtoReflect() protoreflect.Message { - mi := &file_ra_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_ra_proto_msgTypes[2] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -60,7 +147,7 @@ func (x *GenerateOCSPRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GenerateOCSPRequest.ProtoReflect.Descriptor instead. func (*GenerateOCSPRequest) Descriptor() ([]byte, []int) { - return file_ra_proto_rawDescGZIP(), []int{0} + return file_ra_proto_rawDescGZIP(), []int{2} } func (x *GenerateOCSPRequest) GetSerial() string { @@ -70,33 +157,30 @@ func (x *GenerateOCSPRequest) GetSerial() string { return "" } -type UpdateRegistrationRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Base *proto.Registration `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"` - Update *proto.Registration `protobuf:"bytes,2,opt,name=update,proto3" json:"update,omitempty"` +type UpdateRegistrationContactRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + RegistrationID int64 `protobuf:"varint,1,opt,name=registrationID,proto3" json:"registrationID,omitempty"` + Contacts []string `protobuf:"bytes,2,rep,name=contacts,proto3" json:"contacts,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *UpdateRegistrationRequest) Reset() { - *x = UpdateRegistrationRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_ra_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } +func (x *UpdateRegistrationContactRequest) Reset() { + *x = UpdateRegistrationContactRequest{} + mi := &file_ra_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } -func (x *UpdateRegistrationRequest) String() string { +func (x *UpdateRegistrationContactRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*UpdateRegistrationRequest) ProtoMessage() {} +func (*UpdateRegistrationContactRequest) ProtoMessage() {} -func (x *UpdateRegistrationRequest) ProtoReflect() protoreflect.Message { - mi := &file_ra_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { +func (x *UpdateRegistrationContactRequest) ProtoReflect() protoreflect.Message { + mi := &file_ra_proto_msgTypes[3] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -106,42 +190,135 @@ func (x *UpdateRegistrationRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use UpdateRegistrationRequest.ProtoReflect.Descriptor instead. -func (*UpdateRegistrationRequest) Descriptor() ([]byte, []int) { - return file_ra_proto_rawDescGZIP(), []int{1} +// Deprecated: Use UpdateRegistrationContactRequest.ProtoReflect.Descriptor instead. +func (*UpdateRegistrationContactRequest) Descriptor() ([]byte, []int) { + return file_ra_proto_rawDescGZIP(), []int{3} } -func (x *UpdateRegistrationRequest) GetBase() *proto.Registration { +func (x *UpdateRegistrationContactRequest) GetRegistrationID() int64 { if x != nil { - return x.Base + return x.RegistrationID + } + return 0 +} + +func (x *UpdateRegistrationContactRequest) GetContacts() []string { + if x != nil { + return x.Contacts } return nil } -func (x *UpdateRegistrationRequest) GetUpdate() *proto.Registration { +type UpdateRegistrationKeyRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + RegistrationID int64 `protobuf:"varint,1,opt,name=registrationID,proto3" json:"registrationID,omitempty"` + Jwk []byte `protobuf:"bytes,2,opt,name=jwk,proto3" json:"jwk,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateRegistrationKeyRequest) Reset() { + *x = UpdateRegistrationKeyRequest{} + mi := &file_ra_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateRegistrationKeyRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateRegistrationKeyRequest) ProtoMessage() {} + +func (x *UpdateRegistrationKeyRequest) ProtoReflect() protoreflect.Message { + mi := &file_ra_proto_msgTypes[4] if x != nil { - return x.Update + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateRegistrationKeyRequest.ProtoReflect.Descriptor instead. +func (*UpdateRegistrationKeyRequest) Descriptor() ([]byte, []int) { + return file_ra_proto_rawDescGZIP(), []int{4} +} + +func (x *UpdateRegistrationKeyRequest) GetRegistrationID() int64 { + if x != nil { + return x.RegistrationID + } + return 0 +} + +func (x *UpdateRegistrationKeyRequest) GetJwk() []byte { + if x != nil { + return x.Jwk } return nil } +type DeactivateRegistrationRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + RegistrationID int64 `protobuf:"varint,1,opt,name=registrationID,proto3" json:"registrationID,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeactivateRegistrationRequest) Reset() { + *x = DeactivateRegistrationRequest{} + mi := &file_ra_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeactivateRegistrationRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeactivateRegistrationRequest) ProtoMessage() {} + +func (x *DeactivateRegistrationRequest) ProtoReflect() protoreflect.Message { + mi := &file_ra_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeactivateRegistrationRequest.ProtoReflect.Descriptor instead. +func (*DeactivateRegistrationRequest) Descriptor() ([]byte, []int) { + return file_ra_proto_rawDescGZIP(), []int{5} +} + +func (x *DeactivateRegistrationRequest) GetRegistrationID() int64 { + if x != nil { + return x.RegistrationID + } + return 0 +} + type UpdateAuthorizationRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Authz *proto.Authorization `protobuf:"bytes,1,opt,name=authz,proto3" json:"authz,omitempty"` - ChallengeIndex int64 `protobuf:"varint,2,opt,name=challengeIndex,proto3" json:"challengeIndex,omitempty"` - Response *proto.Challenge `protobuf:"bytes,3,opt,name=response,proto3" json:"response,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Authz *proto.Authorization `protobuf:"bytes,1,opt,name=authz,proto3" json:"authz,omitempty"` + ChallengeIndex int64 `protobuf:"varint,2,opt,name=challengeIndex,proto3" json:"challengeIndex,omitempty"` + Response *proto.Challenge `protobuf:"bytes,3,opt,name=response,proto3" json:"response,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *UpdateAuthorizationRequest) Reset() { *x = UpdateAuthorizationRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_ra_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_ra_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *UpdateAuthorizationRequest) String() string { @@ -151,8 +328,8 @@ func (x *UpdateAuthorizationRequest) String() string { func (*UpdateAuthorizationRequest) ProtoMessage() {} func (x *UpdateAuthorizationRequest) ProtoReflect() protoreflect.Message { - mi := &file_ra_proto_msgTypes[2] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_ra_proto_msgTypes[6] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -164,7 +341,7 @@ func (x *UpdateAuthorizationRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateAuthorizationRequest.ProtoReflect.Descriptor instead. func (*UpdateAuthorizationRequest) Descriptor() ([]byte, []int) { - return file_ra_proto_rawDescGZIP(), []int{2} + return file_ra_proto_rawDescGZIP(), []int{6} } func (x *UpdateAuthorizationRequest) GetAuthz() *proto.Authorization { @@ -189,21 +366,18 @@ func (x *UpdateAuthorizationRequest) GetResponse() *proto.Challenge { } type PerformValidationRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Authz *proto.Authorization `protobuf:"bytes,1,opt,name=authz,proto3" json:"authz,omitempty"` - ChallengeIndex int64 `protobuf:"varint,2,opt,name=challengeIndex,proto3" json:"challengeIndex,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Authz *proto.Authorization `protobuf:"bytes,1,opt,name=authz,proto3" json:"authz,omitempty"` + ChallengeIndex int64 `protobuf:"varint,2,opt,name=challengeIndex,proto3" json:"challengeIndex,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *PerformValidationRequest) Reset() { *x = PerformValidationRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_ra_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_ra_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *PerformValidationRequest) String() string { @@ -213,8 +387,8 @@ func (x *PerformValidationRequest) String() string { func (*PerformValidationRequest) ProtoMessage() {} func (x *PerformValidationRequest) ProtoReflect() protoreflect.Message { - mi := &file_ra_proto_msgTypes[3] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_ra_proto_msgTypes[7] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -226,7 +400,7 @@ func (x *PerformValidationRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PerformValidationRequest.ProtoReflect.Descriptor instead. func (*PerformValidationRequest) Descriptor() ([]byte, []int) { - return file_ra_proto_rawDescGZIP(), []int{3} + return file_ra_proto_rawDescGZIP(), []int{7} } func (x *PerformValidationRequest) GetAuthz() *proto.Authorization { @@ -244,22 +418,19 @@ func (x *PerformValidationRequest) GetChallengeIndex() int64 { } type RevokeCertByApplicantRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Cert []byte `protobuf:"bytes,1,opt,name=cert,proto3" json:"cert,omitempty"` + Code int64 `protobuf:"varint,2,opt,name=code,proto3" json:"code,omitempty"` + RegID int64 `protobuf:"varint,3,opt,name=regID,proto3" json:"regID,omitempty"` unknownFields protoimpl.UnknownFields - - Cert []byte `protobuf:"bytes,1,opt,name=cert,proto3" json:"cert,omitempty"` - Code int64 `protobuf:"varint,2,opt,name=code,proto3" json:"code,omitempty"` - RegID int64 `protobuf:"varint,3,opt,name=regID,proto3" json:"regID,omitempty"` + sizeCache protoimpl.SizeCache } func (x *RevokeCertByApplicantRequest) Reset() { *x = RevokeCertByApplicantRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_ra_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_ra_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *RevokeCertByApplicantRequest) String() string { @@ -269,8 +440,8 @@ func (x *RevokeCertByApplicantRequest) String() string { func (*RevokeCertByApplicantRequest) ProtoMessage() {} func (x *RevokeCertByApplicantRequest) ProtoReflect() protoreflect.Message { - mi := &file_ra_proto_msgTypes[4] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_ra_proto_msgTypes[8] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -282,7 +453,7 @@ func (x *RevokeCertByApplicantRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RevokeCertByApplicantRequest.ProtoReflect.Descriptor instead. func (*RevokeCertByApplicantRequest) Descriptor() ([]byte, []int) { - return file_ra_proto_rawDescGZIP(), []int{4} + return file_ra_proto_rawDescGZIP(), []int{8} } func (x *RevokeCertByApplicantRequest) GetCert() []byte { @@ -307,20 +478,17 @@ func (x *RevokeCertByApplicantRequest) GetRegID() int64 { } type RevokeCertByKeyRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Cert []byte `protobuf:"bytes,1,opt,name=cert,proto3" json:"cert,omitempty"` unknownFields protoimpl.UnknownFields - - Cert []byte `protobuf:"bytes,1,opt,name=cert,proto3" json:"cert,omitempty"` + sizeCache protoimpl.SizeCache } func (x *RevokeCertByKeyRequest) Reset() { *x = RevokeCertByKeyRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_ra_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_ra_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *RevokeCertByKeyRequest) String() string { @@ -330,8 +498,8 @@ func (x *RevokeCertByKeyRequest) String() string { func (*RevokeCertByKeyRequest) ProtoMessage() {} func (x *RevokeCertByKeyRequest) ProtoReflect() protoreflect.Message { - mi := &file_ra_proto_msgTypes[5] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_ra_proto_msgTypes[9] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -343,7 +511,7 @@ func (x *RevokeCertByKeyRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RevokeCertByKeyRequest.ProtoReflect.Descriptor instead. func (*RevokeCertByKeyRequest) Descriptor() ([]byte, []int) { - return file_ra_proto_rawDescGZIP(), []int{5} + return file_ra_proto_rawDescGZIP(), []int{9} } func (x *RevokeCertByKeyRequest) GetCert() []byte { @@ -354,10 +522,7 @@ func (x *RevokeCertByKeyRequest) GetCert() []byte { } type AdministrativelyRevokeCertificateRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // Deprecated: this field is ignored. Cert []byte `protobuf:"bytes,1,opt,name=cert,proto3" json:"cert,omitempty"` // The `serial` field is required. @@ -369,15 +534,23 @@ type AdministrativelyRevokeCertificateRequest struct { // certificate in question. In this case, the keyCompromise reason cannot be // specified, because the key cannot be blocked. Malformed bool `protobuf:"varint,6,opt,name=malformed,proto3" json:"malformed,omitempty"` + // The CRL shard to store the revocation in. + // + // This is used when revoking malformed certificates, to allow human judgement + // in setting the CRL shard instead of automatically determining it by parsing + // the certificate. + // + // Passing a nonzero crlShard with malformed=false returns error. + CrlShard int64 `protobuf:"varint,7,opt,name=crlShard,proto3" json:"crlShard,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *AdministrativelyRevokeCertificateRequest) Reset() { *x = AdministrativelyRevokeCertificateRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_ra_proto_msgTypes[6] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_ra_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *AdministrativelyRevokeCertificateRequest) String() string { @@ -387,8 +560,8 @@ func (x *AdministrativelyRevokeCertificateRequest) String() string { func (*AdministrativelyRevokeCertificateRequest) ProtoMessage() {} func (x *AdministrativelyRevokeCertificateRequest) ProtoReflect() protoreflect.Message { - mi := &file_ra_proto_msgTypes[6] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_ra_proto_msgTypes[10] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -400,7 +573,7 @@ func (x *AdministrativelyRevokeCertificateRequest) ProtoReflect() protoreflect.M // Deprecated: Use AdministrativelyRevokeCertificateRequest.ProtoReflect.Descriptor instead. func (*AdministrativelyRevokeCertificateRequest) Descriptor() ([]byte, []int) { - return file_ra_proto_rawDescGZIP(), []int{6} + return file_ra_proto_rawDescGZIP(), []int{10} } func (x *AdministrativelyRevokeCertificateRequest) GetCert() []byte { @@ -445,26 +618,32 @@ func (x *AdministrativelyRevokeCertificateRequest) GetMalformed() bool { return false } -type NewOrderRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields +func (x *AdministrativelyRevokeCertificateRequest) GetCrlShard() int64 { + if x != nil { + return x.CrlShard + } + return 0 +} - // Next unused field number: 6 - RegistrationID int64 `protobuf:"varint,1,opt,name=registrationID,proto3" json:"registrationID,omitempty"` - Names []string `protobuf:"bytes,2,rep,name=names,proto3" json:"names,omitempty"` - ReplacesSerial string `protobuf:"bytes,3,opt,name=replacesSerial,proto3" json:"replacesSerial,omitempty"` - LimitsExempt bool `protobuf:"varint,4,opt,name=limitsExempt,proto3" json:"limitsExempt,omitempty"` - CertificateProfileName string `protobuf:"bytes,5,opt,name=certificateProfileName,proto3" json:"certificateProfileName,omitempty"` +type NewOrderRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Next unused field number: 9 + RegistrationID int64 `protobuf:"varint,1,opt,name=registrationID,proto3" json:"registrationID,omitempty"` + Identifiers []*proto.Identifier `protobuf:"bytes,8,rep,name=identifiers,proto3" json:"identifiers,omitempty"` + CertificateProfileName string `protobuf:"bytes,5,opt,name=certificateProfileName,proto3" json:"certificateProfileName,omitempty"` + // Replaces is the ARI certificate Id that this order replaces. + Replaces string `protobuf:"bytes,7,opt,name=replaces,proto3" json:"replaces,omitempty"` + // ReplacesSerial is the serial number of the certificate that this order replaces. + ReplacesSerial string `protobuf:"bytes,3,opt,name=replacesSerial,proto3" json:"replacesSerial,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *NewOrderRequest) Reset() { *x = NewOrderRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_ra_proto_msgTypes[7] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_ra_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *NewOrderRequest) String() string { @@ -474,8 +653,8 @@ func (x *NewOrderRequest) String() string { func (*NewOrderRequest) ProtoMessage() {} func (x *NewOrderRequest) ProtoReflect() protoreflect.Message { - mi := &file_ra_proto_msgTypes[7] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_ra_proto_msgTypes[11] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -487,7 +666,7 @@ func (x *NewOrderRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use NewOrderRequest.ProtoReflect.Descriptor instead. func (*NewOrderRequest) Descriptor() ([]byte, []int) { - return file_ra_proto_rawDescGZIP(), []int{7} + return file_ra_proto_rawDescGZIP(), []int{11} } func (x *NewOrderRequest) GetRegistrationID() int64 { @@ -497,27 +676,13 @@ func (x *NewOrderRequest) GetRegistrationID() int64 { return 0 } -func (x *NewOrderRequest) GetNames() []string { +func (x *NewOrderRequest) GetIdentifiers() []*proto.Identifier { if x != nil { - return x.Names + return x.Identifiers } return nil } -func (x *NewOrderRequest) GetReplacesSerial() string { - if x != nil { - return x.ReplacesSerial - } - return "" -} - -func (x *NewOrderRequest) GetLimitsExempt() bool { - if x != nil { - return x.LimitsExempt - } - return false -} - func (x *NewOrderRequest) GetCertificateProfileName() string { if x != nil { return x.CertificateProfileName @@ -525,22 +690,77 @@ func (x *NewOrderRequest) GetCertificateProfileName() string { return "" } -type FinalizeOrderRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields +func (x *NewOrderRequest) GetReplaces() string { + if x != nil { + return x.Replaces + } + return "" +} - Order *proto.Order `protobuf:"bytes,1,opt,name=order,proto3" json:"order,omitempty"` - Csr []byte `protobuf:"bytes,2,opt,name=csr,proto3" json:"csr,omitempty"` +func (x *NewOrderRequest) GetReplacesSerial() string { + if x != nil { + return x.ReplacesSerial + } + return "" +} + +type GetAuthorizationRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetAuthorizationRequest) Reset() { + *x = GetAuthorizationRequest{} + mi := &file_ra_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetAuthorizationRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetAuthorizationRequest) ProtoMessage() {} + +func (x *GetAuthorizationRequest) ProtoReflect() protoreflect.Message { + mi := &file_ra_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetAuthorizationRequest.ProtoReflect.Descriptor instead. +func (*GetAuthorizationRequest) Descriptor() ([]byte, []int) { + return file_ra_proto_rawDescGZIP(), []int{12} +} + +func (x *GetAuthorizationRequest) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +type FinalizeOrderRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Order *proto.Order `protobuf:"bytes,1,opt,name=order,proto3" json:"order,omitempty"` + Csr []byte `protobuf:"bytes,2,opt,name=csr,proto3" json:"csr,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *FinalizeOrderRequest) Reset() { *x = FinalizeOrderRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_ra_proto_msgTypes[8] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_ra_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *FinalizeOrderRequest) String() string { @@ -550,8 +770,8 @@ func (x *FinalizeOrderRequest) String() string { func (*FinalizeOrderRequest) ProtoMessage() {} func (x *FinalizeOrderRequest) ProtoReflect() protoreflect.Message { - mi := &file_ra_proto_msgTypes[8] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_ra_proto_msgTypes[13] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -563,7 +783,7 @@ func (x *FinalizeOrderRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use FinalizeOrderRequest.ProtoReflect.Descriptor instead. func (*FinalizeOrderRequest) Descriptor() ([]byte, []int) { - return file_ra_proto_rawDescGZIP(), []int{8} + return file_ra_proto_rawDescGZIP(), []int{13} } func (x *FinalizeOrderRequest) GetOrder() *proto.Order { @@ -581,21 +801,18 @@ func (x *FinalizeOrderRequest) GetCsr() []byte { } type UnpauseAccountRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // The registrationID to be unpaused so issuance can be resumed. RegistrationID int64 `protobuf:"varint,1,opt,name=registrationID,proto3" json:"registrationID,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *UnpauseAccountRequest) Reset() { *x = UnpauseAccountRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_ra_proto_msgTypes[9] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_ra_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *UnpauseAccountRequest) String() string { @@ -605,8 +822,8 @@ func (x *UnpauseAccountRequest) String() string { func (*UnpauseAccountRequest) ProtoMessage() {} func (x *UnpauseAccountRequest) ProtoReflect() protoreflect.Message { - mi := &file_ra_proto_msgTypes[9] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_ra_proto_msgTypes[14] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -618,7 +835,7 @@ func (x *UnpauseAccountRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UnpauseAccountRequest.ProtoReflect.Descriptor instead. func (*UnpauseAccountRequest) Descriptor() ([]byte, []int) { - return file_ra_proto_rawDescGZIP(), []int{9} + return file_ra_proto_rawDescGZIP(), []int{14} } func (x *UnpauseAccountRequest) GetRegistrationID() int64 { @@ -628,210 +845,476 @@ func (x *UnpauseAccountRequest) GetRegistrationID() int64 { return 0 } +type UnpauseAccountResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Count is the number of identifiers which were unpaused for the input regid. + Count int64 `protobuf:"varint,1,opt,name=count,proto3" json:"count,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UnpauseAccountResponse) Reset() { + *x = UnpauseAccountResponse{} + mi := &file_ra_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UnpauseAccountResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UnpauseAccountResponse) ProtoMessage() {} + +func (x *UnpauseAccountResponse) ProtoReflect() protoreflect.Message { + mi := &file_ra_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UnpauseAccountResponse.ProtoReflect.Descriptor instead. +func (*UnpauseAccountResponse) Descriptor() ([]byte, []int) { + return file_ra_proto_rawDescGZIP(), []int{15} +} + +func (x *UnpauseAccountResponse) GetCount() int64 { + if x != nil { + return x.Count + } + return 0 +} + +type AddRateLimitOverrideRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + LimitEnum int64 `protobuf:"varint,1,opt,name=limitEnum,proto3" json:"limitEnum,omitempty"` + BucketKey string `protobuf:"bytes,2,opt,name=bucketKey,proto3" json:"bucketKey,omitempty"` + Comment string `protobuf:"bytes,3,opt,name=comment,proto3" json:"comment,omitempty"` + Period *durationpb.Duration `protobuf:"bytes,4,opt,name=period,proto3" json:"period,omitempty"` + Count int64 `protobuf:"varint,5,opt,name=count,proto3" json:"count,omitempty"` + Burst int64 `protobuf:"varint,6,opt,name=burst,proto3" json:"burst,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AddRateLimitOverrideRequest) Reset() { + *x = AddRateLimitOverrideRequest{} + mi := &file_ra_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AddRateLimitOverrideRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddRateLimitOverrideRequest) ProtoMessage() {} + +func (x *AddRateLimitOverrideRequest) ProtoReflect() protoreflect.Message { + mi := &file_ra_proto_msgTypes[16] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AddRateLimitOverrideRequest.ProtoReflect.Descriptor instead. +func (*AddRateLimitOverrideRequest) Descriptor() ([]byte, []int) { + return file_ra_proto_rawDescGZIP(), []int{16} +} + +func (x *AddRateLimitOverrideRequest) GetLimitEnum() int64 { + if x != nil { + return x.LimitEnum + } + return 0 +} + +func (x *AddRateLimitOverrideRequest) GetBucketKey() string { + if x != nil { + return x.BucketKey + } + return "" +} + +func (x *AddRateLimitOverrideRequest) GetComment() string { + if x != nil { + return x.Comment + } + return "" +} + +func (x *AddRateLimitOverrideRequest) GetPeriod() *durationpb.Duration { + if x != nil { + return x.Period + } + return nil +} + +func (x *AddRateLimitOverrideRequest) GetCount() int64 { + if x != nil { + return x.Count + } + return 0 +} + +func (x *AddRateLimitOverrideRequest) GetBurst() int64 { + if x != nil { + return x.Burst + } + return 0 +} + +type AddRateLimitOverrideResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Inserted bool `protobuf:"varint,1,opt,name=inserted,proto3" json:"inserted,omitempty"` + Enabled bool `protobuf:"varint,2,opt,name=enabled,proto3" json:"enabled,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AddRateLimitOverrideResponse) Reset() { + *x = AddRateLimitOverrideResponse{} + mi := &file_ra_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AddRateLimitOverrideResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddRateLimitOverrideResponse) ProtoMessage() {} + +func (x *AddRateLimitOverrideResponse) ProtoReflect() protoreflect.Message { + mi := &file_ra_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AddRateLimitOverrideResponse.ProtoReflect.Descriptor instead. +func (*AddRateLimitOverrideResponse) Descriptor() ([]byte, []int) { + return file_ra_proto_rawDescGZIP(), []int{17} +} + +func (x *AddRateLimitOverrideResponse) GetInserted() bool { + if x != nil { + return x.Inserted + } + return false +} + +func (x *AddRateLimitOverrideResponse) GetEnabled() bool { + if x != nil { + return x.Enabled + } + return false +} + var File_ra_proto protoreflect.FileDescriptor -var file_ra_proto_rawDesc = []byte{ +var file_ra_proto_rawDesc = string([]byte{ 0x0a, 0x08, 0x72, 0x61, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x02, 0x72, 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, 0x11, 0x63, 0x61, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x63, 0x61, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 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, 0x6f, 0x74, 0x6f, 0x22, 0x2d, 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, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x65, - 0x72, 0x69, 0x61, 0x6c, 0x22, 0x6f, 0x0a, 0x19, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, - 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x26, 0x0a, 0x04, 0x62, 0x61, 0x73, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x12, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x52, 0x04, 0x62, 0x61, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x06, 0x75, 0x70, 0x64, - 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x63, 0x6f, 0x72, 0x65, - 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x75, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x22, 0x9c, 0x01, 0x0a, 0x1a, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x29, 0x0a, 0x05, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, - 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x05, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x12, - 0x26, 0x0a, 0x0e, 0x63, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x49, 0x6e, 0x64, 0x65, - 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x63, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, - 0x67, 0x65, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x2b, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x63, 0x6f, 0x72, 0x65, - 0x2e, 0x43, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x52, 0x08, 0x72, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x6d, 0x0a, 0x18, 0x50, 0x65, 0x72, 0x66, 0x6f, 0x72, 0x6d, 0x56, - 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x29, 0x0a, 0x05, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x13, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x05, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x12, 0x26, 0x0a, 0x0e, 0x63, - 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x0e, 0x63, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x49, 0x6e, - 0x64, 0x65, 0x78, 0x22, 0x5c, 0x0a, 0x1c, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x43, 0x65, 0x72, - 0x74, 0x42, 0x79, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x65, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0c, 0x52, 0x04, 0x63, 0x65, 0x72, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x72, - 0x65, 0x67, 0x49, 0x44, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x72, 0x65, 0x67, 0x49, - 0x44, 0x22, 0x32, 0x0a, 0x16, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x43, 0x65, 0x72, 0x74, 0x42, - 0x79, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x63, - 0x65, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x63, 0x65, 0x72, 0x74, 0x4a, - 0x04, 0x08, 0x02, 0x10, 0x03, 0x22, 0xca, 0x01, 0x0a, 0x28, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x69, - 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x76, 0x65, 0x6c, 0x79, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, - 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x2c, 0x0a, 0x0a, 0x53, 0x43, 0x54, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x70, 0x72, 0x65, 0x63, 0x65, 0x72, 0x74, 0x44, 0x45, + 0x52, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x70, 0x72, 0x65, 0x63, 0x65, 0x72, 0x74, + 0x44, 0x45, 0x52, 0x22, 0x25, 0x0a, 0x0b, 0x53, 0x43, 0x54, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x63, 0x74, 0x44, 0x45, 0x52, 0x18, 0x01, 0x20, 0x03, + 0x28, 0x0c, 0x52, 0x06, 0x73, 0x63, 0x74, 0x44, 0x45, 0x52, 0x22, 0x2d, 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, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x06, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x22, 0x66, 0x0a, 0x20, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, + 0x6f, 0x6e, 0x74, 0x61, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, + 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x12, 0x1a, 0x0a, 0x08, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x63, 0x74, + 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x63, 0x74, + 0x73, 0x22, 0x58, 0x0a, 0x1c, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, + 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x26, 0x0a, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, + 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x12, 0x10, 0x0a, 0x03, 0x6a, 0x77, 0x6b, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x6a, 0x77, 0x6b, 0x22, 0x47, 0x0a, 0x1d, 0x44, + 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0e, + 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x49, 0x44, 0x22, 0x9c, 0x01, 0x0a, 0x1a, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, + 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x29, 0x0a, 0x05, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, + 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x05, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x12, 0x26, + 0x0a, 0x0e, 0x63, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x49, 0x6e, 0x64, 0x65, 0x78, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x63, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, + 0x65, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x2b, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, + 0x43, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x52, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x6d, 0x0a, 0x18, 0x50, 0x65, 0x72, 0x66, 0x6f, 0x72, 0x6d, 0x56, 0x61, + 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x29, 0x0a, 0x05, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, + 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x52, 0x05, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x12, 0x26, 0x0a, 0x0e, 0x63, 0x68, + 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x0e, 0x63, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x49, 0x6e, 0x64, + 0x65, 0x78, 0x22, 0x5c, 0x0a, 0x1c, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x43, 0x65, 0x72, 0x74, + 0x42, 0x79, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x65, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, - 0x52, 0x04, 0x63, 0x65, 0x72, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, 0x12, - 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x63, 0x6f, - 0x64, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x4e, 0x61, 0x6d, 0x65, - 0x12, 0x22, 0x0a, 0x0c, 0x73, 0x6b, 0x69, 0x70, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x4b, 0x65, 0x79, - 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x73, 0x6b, 0x69, 0x70, 0x42, 0x6c, 0x6f, 0x63, - 0x6b, 0x4b, 0x65, 0x79, 0x12, 0x1c, 0x0a, 0x09, 0x6d, 0x61, 0x6c, 0x66, 0x6f, 0x72, 0x6d, 0x65, - 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x6d, 0x61, 0x6c, 0x66, 0x6f, 0x72, 0x6d, - 0x65, 0x64, 0x22, 0xd3, 0x01, 0x0a, 0x0f, 0x4e, 0x65, 0x77, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, - 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, - 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x12, 0x14, - 0x0a, 0x05, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x6e, - 0x61, 0x6d, 0x65, 0x73, 0x12, 0x26, 0x0a, 0x0e, 0x72, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x73, - 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x72, 0x65, - 0x70, 0x6c, 0x61, 0x63, 0x65, 0x73, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, 0x22, 0x0a, 0x0c, - 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x73, 0x45, 0x78, 0x65, 0x6d, 0x70, 0x74, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x0c, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x73, 0x45, 0x78, 0x65, 0x6d, 0x70, 0x74, - 0x12, 0x36, 0x0a, 0x16, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x50, - 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x16, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, - 0x66, 0x69, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x4b, 0x0a, 0x14, 0x46, 0x69, 0x6e, 0x61, - 0x6c, 0x69, 0x7a, 0x65, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x21, 0x0a, 0x05, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x0b, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x05, 0x6f, 0x72, - 0x64, 0x65, 0x72, 0x12, 0x10, 0x0a, 0x03, 0x63, 0x73, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, - 0x52, 0x03, 0x63, 0x73, 0x72, 0x22, 0x3f, 0x0a, 0x15, 0x55, 0x6e, 0x70, 0x61, 0x75, 0x73, 0x65, - 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, - 0x0a, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x32, 0xf4, 0x06, 0x0a, 0x15, 0x52, 0x65, 0x67, 0x69, 0x73, - 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, - 0x12, 0x3b, 0x0a, 0x0f, 0x4e, 0x65, 0x77, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, - 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x1a, 0x12, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x52, - 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x00, 0x12, 0x49, 0x0a, - 0x12, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x12, 0x1d, 0x2e, 0x72, 0x61, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, - 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, - 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x11, 0x50, 0x65, 0x72, 0x66, - 0x6f, 0x72, 0x6d, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x2e, - 0x72, 0x61, 0x2e, 0x50, 0x65, 0x72, 0x66, 0x6f, 0x72, 0x6d, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x63, 0x6f, - 0x72, 0x65, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x22, 0x00, 0x12, 0x46, 0x0a, 0x16, 0x44, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, + 0x52, 0x04, 0x63, 0x65, 0x72, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x65, + 0x67, 0x49, 0x44, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x72, 0x65, 0x67, 0x49, 0x44, + 0x22, 0x32, 0x0a, 0x16, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x43, 0x65, 0x72, 0x74, 0x42, 0x79, + 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x65, + 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x63, 0x65, 0x72, 0x74, 0x4a, 0x04, + 0x08, 0x02, 0x10, 0x03, 0x22, 0xe6, 0x01, 0x0a, 0x28, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x69, 0x73, + 0x74, 0x72, 0x61, 0x74, 0x69, 0x76, 0x65, 0x6c, 0x79, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x43, + 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x65, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x04, 0x63, 0x65, 0x72, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, 0x12, 0x0a, + 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x63, 0x6f, 0x64, + 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x12, + 0x22, 0x0a, 0x0c, 0x73, 0x6b, 0x69, 0x70, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x4b, 0x65, 0x79, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x73, 0x6b, 0x69, 0x70, 0x42, 0x6c, 0x6f, 0x63, 0x6b, + 0x4b, 0x65, 0x79, 0x12, 0x1c, 0x0a, 0x09, 0x6d, 0x61, 0x6c, 0x66, 0x6f, 0x72, 0x6d, 0x65, 0x64, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x6d, 0x61, 0x6c, 0x66, 0x6f, 0x72, 0x6d, 0x65, + 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x63, 0x72, 0x6c, 0x53, 0x68, 0x61, 0x72, 0x64, 0x18, 0x07, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x08, 0x63, 0x72, 0x6c, 0x53, 0x68, 0x61, 0x72, 0x64, 0x22, 0xfb, 0x01, + 0x0a, 0x0f, 0x4e, 0x65, 0x77, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x26, 0x0a, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, + 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x12, 0x32, 0x0a, 0x0b, 0x69, 0x64, 0x65, + 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, + 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, + 0x52, 0x0b, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x12, 0x36, 0x0a, + 0x16, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x66, + 0x69, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x16, 0x63, + 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, + 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, + 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, + 0x73, 0x12, 0x26, 0x0a, 0x0e, 0x72, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x73, 0x53, 0x65, 0x72, + 0x69, 0x61, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x72, 0x65, 0x70, 0x6c, 0x61, + 0x63, 0x65, 0x73, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x4a, + 0x04, 0x08, 0x04, 0x10, 0x05, 0x4a, 0x04, 0x08, 0x06, 0x10, 0x07, 0x22, 0x29, 0x0a, 0x17, 0x47, + 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x22, 0x4b, 0x0a, 0x14, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, + 0x7a, 0x65, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x21, + 0x0a, 0x05, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0b, 0x2e, + 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x05, 0x6f, 0x72, 0x64, 0x65, + 0x72, 0x12, 0x10, 0x0a, 0x03, 0x63, 0x73, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, + 0x63, 0x73, 0x72, 0x22, 0x3f, 0x0a, 0x15, 0x55, 0x6e, 0x70, 0x61, 0x75, 0x73, 0x65, 0x41, 0x63, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0e, + 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x49, 0x44, 0x22, 0x2e, 0x0a, 0x16, 0x55, 0x6e, 0x70, 0x61, 0x75, 0x73, 0x65, 0x41, + 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, + 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x22, 0xd2, 0x01, 0x0a, 0x1b, 0x41, 0x64, 0x64, 0x52, 0x61, 0x74, 0x65, + 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x45, 0x6e, 0x75, + 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x45, 0x6e, + 0x75, 0x6d, 0x12, 0x1c, 0x0a, 0x09, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x4b, 0x65, 0x79, + 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x31, 0x0a, 0x06, 0x70, 0x65, + 0x72, 0x69, 0x6f, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x70, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x12, 0x14, 0x0a, + 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, + 0x75, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x62, 0x75, 0x72, 0x73, 0x74, 0x18, 0x06, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x05, 0x62, 0x75, 0x72, 0x73, 0x74, 0x22, 0x54, 0x0a, 0x1c, 0x41, 0x64, 0x64, + 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, + 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x69, 0x6e, 0x73, + 0x65, 0x72, 0x74, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x69, 0x6e, 0x73, + 0x65, 0x72, 0x74, 0x65, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x32, + 0x87, 0x09, 0x0a, 0x15, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x12, 0x3b, 0x0a, 0x0f, 0x4e, 0x65, 0x77, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x17, 0x44, 0x65, - 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x13, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x41, 0x75, 0x74, - 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, - 0x74, 0x79, 0x22, 0x00, 0x12, 0x53, 0x0a, 0x15, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x43, 0x65, - 0x72, 0x74, 0x42, 0x79, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x6e, 0x74, 0x12, 0x20, 0x2e, - 0x72, 0x61, 0x2e, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x43, 0x65, 0x72, 0x74, 0x42, 0x79, 0x41, - 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x47, 0x0a, 0x0f, 0x52, 0x65, 0x76, - 0x6f, 0x6b, 0x65, 0x43, 0x65, 0x72, 0x74, 0x42, 0x79, 0x4b, 0x65, 0x79, 0x12, 0x1a, 0x2e, 0x72, - 0x61, 0x2e, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x43, 0x65, 0x72, 0x74, 0x42, 0x79, 0x4b, 0x65, - 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x1a, 0x12, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x00, 0x12, 0x57, 0x0a, 0x19, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, + 0x61, 0x63, 0x74, 0x12, 0x24, 0x2e, 0x72, 0x61, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, + 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x61, + 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x63, 0x6f, 0x72, 0x65, + 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x00, 0x12, + 0x4f, 0x0a, 0x15, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4b, 0x65, 0x79, 0x12, 0x20, 0x2e, 0x72, 0x61, 0x2e, 0x55, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x63, 0x6f, 0x72, + 0x65, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x00, + 0x12, 0x51, 0x0a, 0x16, 0x44, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x52, 0x65, + 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x21, 0x2e, 0x72, 0x61, 0x2e, + 0x44, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, + 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x11, 0x50, 0x65, 0x72, 0x66, 0x6f, 0x72, 0x6d, 0x56, 0x61, + 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x2e, 0x72, 0x61, 0x2e, 0x50, 0x65, + 0x72, 0x66, 0x6f, 0x72, 0x6d, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x41, 0x75, + 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x00, 0x12, 0x48, 0x0a, + 0x17, 0x44, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, + 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x13, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, + 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x1a, 0x16, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x53, 0x0a, 0x15, 0x52, 0x65, 0x76, 0x6f, 0x6b, + 0x65, 0x43, 0x65, 0x72, 0x74, 0x42, 0x79, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x6e, 0x74, + 0x12, 0x20, 0x2e, 0x72, 0x61, 0x2e, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x43, 0x65, 0x72, 0x74, + 0x42, 0x79, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x47, 0x0a, 0x0f, + 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x43, 0x65, 0x72, 0x74, 0x42, 0x79, 0x4b, 0x65, 0x79, 0x12, + 0x1a, 0x2e, 0x72, 0x61, 0x2e, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x43, 0x65, 0x72, 0x74, 0x42, + 0x79, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, + 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x6b, 0x0a, 0x21, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x69, 0x73, + 0x74, 0x72, 0x61, 0x74, 0x69, 0x76, 0x65, 0x6c, 0x79, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x43, + 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x2c, 0x2e, 0x72, 0x61, 0x2e, + 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x76, 0x65, 0x6c, 0x79, + 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, + 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, - 0x22, 0x00, 0x12, 0x6b, 0x0a, 0x21, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x69, 0x73, 0x74, 0x72, 0x61, - 0x74, 0x69, 0x76, 0x65, 0x6c, 0x79, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x43, 0x65, 0x72, 0x74, - 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x2c, 0x2e, 0x72, 0x61, 0x2e, 0x41, 0x64, 0x6d, - 0x69, 0x6e, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x76, 0x65, 0x6c, 0x79, 0x52, 0x65, 0x76, - 0x6f, 0x6b, 0x65, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, - 0x2e, 0x0a, 0x08, 0x4e, 0x65, 0x77, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x13, 0x2e, 0x72, 0x61, - 0x2e, 0x4e, 0x65, 0x77, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x0b, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x22, 0x00, 0x12, - 0x38, 0x0a, 0x0d, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x4f, 0x72, 0x64, 0x65, 0x72, - 0x12, 0x18, 0x2e, 0x72, 0x61, 0x2e, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x4f, 0x72, - 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0b, 0x2e, 0x63, 0x6f, 0x72, - 0x65, 0x2e, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x22, 0x00, 0x12, 0x3b, 0x0a, 0x0c, 0x47, 0x65, 0x6e, - 0x65, 0x72, 0x61, 0x74, 0x65, 0x4f, 0x43, 0x53, 0x50, 0x12, 0x17, 0x2e, 0x72, 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, 0x12, 0x45, 0x0a, 0x0e, 0x55, 0x6e, 0x70, 0x61, 0x75, 0x73, - 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x19, 0x2e, 0x72, 0x61, 0x2e, 0x55, 0x6e, - 0x70, 0x61, 0x75, 0x73, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 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, - 0x72, 0x61, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, -} + 0x22, 0x00, 0x12, 0x2e, 0x0a, 0x08, 0x4e, 0x65, 0x77, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x13, + 0x2e, 0x72, 0x61, 0x2e, 0x4e, 0x65, 0x77, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x0b, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x4f, 0x72, 0x64, 0x65, 0x72, + 0x22, 0x00, 0x12, 0x46, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, + 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1b, 0x2e, 0x72, 0x61, 0x2e, 0x47, 0x65, 0x74, 0x41, + 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, + 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x00, 0x12, 0x38, 0x0a, 0x0d, 0x46, 0x69, + 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x18, 0x2e, 0x72, 0x61, + 0x2e, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0b, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x4f, 0x72, 0x64, + 0x65, 0x72, 0x22, 0x00, 0x12, 0x3b, 0x0a, 0x0c, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, + 0x4f, 0x43, 0x53, 0x50, 0x12, 0x17, 0x2e, 0x72, 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, 0x12, 0x49, 0x0a, 0x0e, 0x55, 0x6e, 0x70, 0x61, 0x75, 0x73, 0x65, 0x41, 0x63, 0x63, 0x6f, + 0x75, 0x6e, 0x74, 0x12, 0x19, 0x2e, 0x72, 0x61, 0x2e, 0x55, 0x6e, 0x70, 0x61, 0x75, 0x73, 0x65, + 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, + 0x2e, 0x72, 0x61, 0x2e, 0x55, 0x6e, 0x70, 0x61, 0x75, 0x73, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x5b, 0x0a, 0x14, + 0x41, 0x64, 0x64, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, + 0x72, 0x69, 0x64, 0x65, 0x12, 0x1f, 0x2e, 0x72, 0x61, 0x2e, 0x41, 0x64, 0x64, 0x52, 0x61, 0x74, + 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x72, 0x61, 0x2e, 0x41, 0x64, 0x64, 0x52, 0x61, + 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x32, 0x3b, 0x0a, 0x0b, 0x53, 0x43, 0x54, + 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x2c, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x53, + 0x43, 0x54, 0x73, 0x12, 0x0e, 0x2e, 0x72, 0x61, 0x2e, 0x53, 0x43, 0x54, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x0f, 0x2e, 0x72, 0x61, 0x2e, 0x53, 0x43, 0x54, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 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, 0x72, 0x61, 0x2f, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +}) var ( file_ra_proto_rawDescOnce sync.Once - file_ra_proto_rawDescData = file_ra_proto_rawDesc + file_ra_proto_rawDescData []byte ) func file_ra_proto_rawDescGZIP() []byte { file_ra_proto_rawDescOnce.Do(func() { - file_ra_proto_rawDescData = protoimpl.X.CompressGZIP(file_ra_proto_rawDescData) + file_ra_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_ra_proto_rawDesc), len(file_ra_proto_rawDesc))) }) return file_ra_proto_rawDescData } -var file_ra_proto_msgTypes = make([]protoimpl.MessageInfo, 10) -var file_ra_proto_goTypes = []interface{}{ - (*GenerateOCSPRequest)(nil), // 0: ra.GenerateOCSPRequest - (*UpdateRegistrationRequest)(nil), // 1: ra.UpdateRegistrationRequest - (*UpdateAuthorizationRequest)(nil), // 2: ra.UpdateAuthorizationRequest - (*PerformValidationRequest)(nil), // 3: ra.PerformValidationRequest - (*RevokeCertByApplicantRequest)(nil), // 4: ra.RevokeCertByApplicantRequest - (*RevokeCertByKeyRequest)(nil), // 5: ra.RevokeCertByKeyRequest - (*AdministrativelyRevokeCertificateRequest)(nil), // 6: ra.AdministrativelyRevokeCertificateRequest - (*NewOrderRequest)(nil), // 7: ra.NewOrderRequest - (*FinalizeOrderRequest)(nil), // 8: ra.FinalizeOrderRequest - (*UnpauseAccountRequest)(nil), // 9: ra.UnpauseAccountRequest - (*proto.Registration)(nil), // 10: core.Registration - (*proto.Authorization)(nil), // 11: core.Authorization - (*proto.Challenge)(nil), // 12: core.Challenge - (*proto.Order)(nil), // 13: core.Order - (*emptypb.Empty)(nil), // 14: google.protobuf.Empty - (*proto1.OCSPResponse)(nil), // 15: ca.OCSPResponse +var file_ra_proto_msgTypes = make([]protoimpl.MessageInfo, 18) +var file_ra_proto_goTypes = []any{ + (*SCTRequest)(nil), // 0: ra.SCTRequest + (*SCTResponse)(nil), // 1: ra.SCTResponse + (*GenerateOCSPRequest)(nil), // 2: ra.GenerateOCSPRequest + (*UpdateRegistrationContactRequest)(nil), // 3: ra.UpdateRegistrationContactRequest + (*UpdateRegistrationKeyRequest)(nil), // 4: ra.UpdateRegistrationKeyRequest + (*DeactivateRegistrationRequest)(nil), // 5: ra.DeactivateRegistrationRequest + (*UpdateAuthorizationRequest)(nil), // 6: ra.UpdateAuthorizationRequest + (*PerformValidationRequest)(nil), // 7: ra.PerformValidationRequest + (*RevokeCertByApplicantRequest)(nil), // 8: ra.RevokeCertByApplicantRequest + (*RevokeCertByKeyRequest)(nil), // 9: ra.RevokeCertByKeyRequest + (*AdministrativelyRevokeCertificateRequest)(nil), // 10: ra.AdministrativelyRevokeCertificateRequest + (*NewOrderRequest)(nil), // 11: ra.NewOrderRequest + (*GetAuthorizationRequest)(nil), // 12: ra.GetAuthorizationRequest + (*FinalizeOrderRequest)(nil), // 13: ra.FinalizeOrderRequest + (*UnpauseAccountRequest)(nil), // 14: ra.UnpauseAccountRequest + (*UnpauseAccountResponse)(nil), // 15: ra.UnpauseAccountResponse + (*AddRateLimitOverrideRequest)(nil), // 16: ra.AddRateLimitOverrideRequest + (*AddRateLimitOverrideResponse)(nil), // 17: ra.AddRateLimitOverrideResponse + (*proto.Authorization)(nil), // 18: core.Authorization + (*proto.Challenge)(nil), // 19: core.Challenge + (*proto.Identifier)(nil), // 20: core.Identifier + (*proto.Order)(nil), // 21: core.Order + (*durationpb.Duration)(nil), // 22: google.protobuf.Duration + (*proto.Registration)(nil), // 23: core.Registration + (*emptypb.Empty)(nil), // 24: google.protobuf.Empty + (*proto1.OCSPResponse)(nil), // 25: ca.OCSPResponse } var file_ra_proto_depIdxs = []int32{ - 10, // 0: ra.UpdateRegistrationRequest.base:type_name -> core.Registration - 10, // 1: ra.UpdateRegistrationRequest.update:type_name -> core.Registration - 11, // 2: ra.UpdateAuthorizationRequest.authz:type_name -> core.Authorization - 12, // 3: ra.UpdateAuthorizationRequest.response:type_name -> core.Challenge - 11, // 4: ra.PerformValidationRequest.authz:type_name -> core.Authorization - 13, // 5: ra.FinalizeOrderRequest.order:type_name -> core.Order - 10, // 6: ra.RegistrationAuthority.NewRegistration:input_type -> core.Registration - 1, // 7: ra.RegistrationAuthority.UpdateRegistration:input_type -> ra.UpdateRegistrationRequest - 3, // 8: ra.RegistrationAuthority.PerformValidation:input_type -> ra.PerformValidationRequest - 10, // 9: ra.RegistrationAuthority.DeactivateRegistration:input_type -> core.Registration - 11, // 10: ra.RegistrationAuthority.DeactivateAuthorization:input_type -> core.Authorization - 4, // 11: ra.RegistrationAuthority.RevokeCertByApplicant:input_type -> ra.RevokeCertByApplicantRequest - 5, // 12: ra.RegistrationAuthority.RevokeCertByKey:input_type -> ra.RevokeCertByKeyRequest - 6, // 13: ra.RegistrationAuthority.AdministrativelyRevokeCertificate:input_type -> ra.AdministrativelyRevokeCertificateRequest - 7, // 14: ra.RegistrationAuthority.NewOrder:input_type -> ra.NewOrderRequest - 8, // 15: ra.RegistrationAuthority.FinalizeOrder:input_type -> ra.FinalizeOrderRequest - 0, // 16: ra.RegistrationAuthority.GenerateOCSP:input_type -> ra.GenerateOCSPRequest - 9, // 17: ra.RegistrationAuthority.UnpauseAccount:input_type -> ra.UnpauseAccountRequest - 10, // 18: ra.RegistrationAuthority.NewRegistration:output_type -> core.Registration - 10, // 19: ra.RegistrationAuthority.UpdateRegistration:output_type -> core.Registration - 11, // 20: ra.RegistrationAuthority.PerformValidation:output_type -> core.Authorization - 14, // 21: ra.RegistrationAuthority.DeactivateRegistration:output_type -> google.protobuf.Empty - 14, // 22: ra.RegistrationAuthority.DeactivateAuthorization:output_type -> google.protobuf.Empty - 14, // 23: ra.RegistrationAuthority.RevokeCertByApplicant:output_type -> google.protobuf.Empty - 14, // 24: ra.RegistrationAuthority.RevokeCertByKey:output_type -> google.protobuf.Empty - 14, // 25: ra.RegistrationAuthority.AdministrativelyRevokeCertificate:output_type -> google.protobuf.Empty - 13, // 26: ra.RegistrationAuthority.NewOrder:output_type -> core.Order - 13, // 27: ra.RegistrationAuthority.FinalizeOrder:output_type -> core.Order - 15, // 28: ra.RegistrationAuthority.GenerateOCSP:output_type -> ca.OCSPResponse - 14, // 29: ra.RegistrationAuthority.UnpauseAccount:output_type -> google.protobuf.Empty - 18, // [18:30] is the sub-list for method output_type - 6, // [6:18] is the sub-list for method input_type + 18, // 0: ra.UpdateAuthorizationRequest.authz:type_name -> core.Authorization + 19, // 1: ra.UpdateAuthorizationRequest.response:type_name -> core.Challenge + 18, // 2: ra.PerformValidationRequest.authz:type_name -> core.Authorization + 20, // 3: ra.NewOrderRequest.identifiers:type_name -> core.Identifier + 21, // 4: ra.FinalizeOrderRequest.order:type_name -> core.Order + 22, // 5: ra.AddRateLimitOverrideRequest.period:type_name -> google.protobuf.Duration + 23, // 6: ra.RegistrationAuthority.NewRegistration:input_type -> core.Registration + 3, // 7: ra.RegistrationAuthority.UpdateRegistrationContact:input_type -> ra.UpdateRegistrationContactRequest + 4, // 8: ra.RegistrationAuthority.UpdateRegistrationKey:input_type -> ra.UpdateRegistrationKeyRequest + 5, // 9: ra.RegistrationAuthority.DeactivateRegistration:input_type -> ra.DeactivateRegistrationRequest + 7, // 10: ra.RegistrationAuthority.PerformValidation:input_type -> ra.PerformValidationRequest + 18, // 11: ra.RegistrationAuthority.DeactivateAuthorization:input_type -> core.Authorization + 8, // 12: ra.RegistrationAuthority.RevokeCertByApplicant:input_type -> ra.RevokeCertByApplicantRequest + 9, // 13: ra.RegistrationAuthority.RevokeCertByKey:input_type -> ra.RevokeCertByKeyRequest + 10, // 14: ra.RegistrationAuthority.AdministrativelyRevokeCertificate:input_type -> ra.AdministrativelyRevokeCertificateRequest + 11, // 15: ra.RegistrationAuthority.NewOrder:input_type -> ra.NewOrderRequest + 12, // 16: ra.RegistrationAuthority.GetAuthorization:input_type -> ra.GetAuthorizationRequest + 13, // 17: ra.RegistrationAuthority.FinalizeOrder:input_type -> ra.FinalizeOrderRequest + 2, // 18: ra.RegistrationAuthority.GenerateOCSP:input_type -> ra.GenerateOCSPRequest + 14, // 19: ra.RegistrationAuthority.UnpauseAccount:input_type -> ra.UnpauseAccountRequest + 16, // 20: ra.RegistrationAuthority.AddRateLimitOverride:input_type -> ra.AddRateLimitOverrideRequest + 0, // 21: ra.SCTProvider.GetSCTs:input_type -> ra.SCTRequest + 23, // 22: ra.RegistrationAuthority.NewRegistration:output_type -> core.Registration + 23, // 23: ra.RegistrationAuthority.UpdateRegistrationContact:output_type -> core.Registration + 23, // 24: ra.RegistrationAuthority.UpdateRegistrationKey:output_type -> core.Registration + 23, // 25: ra.RegistrationAuthority.DeactivateRegistration:output_type -> core.Registration + 18, // 26: ra.RegistrationAuthority.PerformValidation:output_type -> core.Authorization + 24, // 27: ra.RegistrationAuthority.DeactivateAuthorization:output_type -> google.protobuf.Empty + 24, // 28: ra.RegistrationAuthority.RevokeCertByApplicant:output_type -> google.protobuf.Empty + 24, // 29: ra.RegistrationAuthority.RevokeCertByKey:output_type -> google.protobuf.Empty + 24, // 30: ra.RegistrationAuthority.AdministrativelyRevokeCertificate:output_type -> google.protobuf.Empty + 21, // 31: ra.RegistrationAuthority.NewOrder:output_type -> core.Order + 18, // 32: ra.RegistrationAuthority.GetAuthorization:output_type -> core.Authorization + 21, // 33: ra.RegistrationAuthority.FinalizeOrder:output_type -> core.Order + 25, // 34: ra.RegistrationAuthority.GenerateOCSP:output_type -> ca.OCSPResponse + 15, // 35: ra.RegistrationAuthority.UnpauseAccount:output_type -> ra.UnpauseAccountResponse + 17, // 36: ra.RegistrationAuthority.AddRateLimitOverride:output_type -> ra.AddRateLimitOverrideResponse + 1, // 37: ra.SCTProvider.GetSCTs:output_type -> ra.SCTResponse + 22, // [22:38] is the sub-list for method output_type + 6, // [6:22] is the sub-list for method input_type 6, // [6:6] is the sub-list for extension type_name 6, // [6:6] is the sub-list for extension extendee 0, // [0:6] is the sub-list for field type_name @@ -842,144 +1325,21 @@ func file_ra_proto_init() { if File_ra_proto != nil { return } - if !protoimpl.UnsafeEnabled { - file_ra_proto_msgTypes[0].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_ra_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UpdateRegistrationRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_ra_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UpdateAuthorizationRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_ra_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PerformValidationRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_ra_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RevokeCertByApplicantRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_ra_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RevokeCertByKeyRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_ra_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*AdministrativelyRevokeCertificateRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_ra_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*NewOrderRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_ra_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*FinalizeOrderRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_ra_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UnpauseAccountRequest); 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_ra_proto_rawDesc, + RawDescriptor: unsafe.Slice(unsafe.StringData(file_ra_proto_rawDesc), len(file_ra_proto_rawDesc)), NumEnums: 0, - NumMessages: 10, + NumMessages: 18, NumExtensions: 0, - NumServices: 1, + NumServices: 2, }, GoTypes: file_ra_proto_goTypes, DependencyIndexes: file_ra_proto_depIdxs, MessageInfos: file_ra_proto_msgTypes, }.Build() File_ra_proto = out.File - file_ra_proto_rawDesc = nil file_ra_proto_goTypes = nil file_ra_proto_depIdxs = nil } diff --git a/third-party/github.com/letsencrypt/boulder/ra/proto/ra.proto b/third-party/github.com/letsencrypt/boulder/ra/proto/ra.proto index bc8d0bfcc..069721e60 100644 --- a/third-party/github.com/letsencrypt/boulder/ra/proto/ra.proto +++ b/third-party/github.com/letsencrypt/boulder/ra/proto/ra.proto @@ -6,30 +6,55 @@ option go_package = "github.com/letsencrypt/boulder/ra/proto"; import "core/proto/core.proto"; import "ca/proto/ca.proto"; import "google/protobuf/empty.proto"; +import "google/protobuf/duration.proto"; service RegistrationAuthority { rpc NewRegistration(core.Registration) returns (core.Registration) {} - rpc UpdateRegistration(UpdateRegistrationRequest) returns (core.Registration) {} + rpc UpdateRegistrationContact(UpdateRegistrationContactRequest) returns (core.Registration) {} + rpc UpdateRegistrationKey(UpdateRegistrationKeyRequest) returns (core.Registration) {} + rpc DeactivateRegistration(DeactivateRegistrationRequest) returns (core.Registration) {} rpc PerformValidation(PerformValidationRequest) returns (core.Authorization) {} - rpc DeactivateRegistration(core.Registration) returns (google.protobuf.Empty) {} rpc DeactivateAuthorization(core.Authorization) returns (google.protobuf.Empty) {} rpc RevokeCertByApplicant(RevokeCertByApplicantRequest) returns (google.protobuf.Empty) {} rpc RevokeCertByKey(RevokeCertByKeyRequest) returns (google.protobuf.Empty) {} rpc AdministrativelyRevokeCertificate(AdministrativelyRevokeCertificateRequest) returns (google.protobuf.Empty) {} rpc NewOrder(NewOrderRequest) returns (core.Order) {} + rpc GetAuthorization(GetAuthorizationRequest) returns (core.Authorization) {} rpc FinalizeOrder(FinalizeOrderRequest) returns (core.Order) {} // Generate an OCSP response based on the DB's current status and reason code. rpc GenerateOCSP(GenerateOCSPRequest) returns (ca.OCSPResponse) {} - rpc UnpauseAccount(UnpauseAccountRequest) returns (google.protobuf.Empty) {} + rpc UnpauseAccount(UnpauseAccountRequest) returns (UnpauseAccountResponse) {} + rpc AddRateLimitOverride(AddRateLimitOverrideRequest) returns (AddRateLimitOverrideResponse) {} +} + +service SCTProvider { + rpc GetSCTs(SCTRequest) returns (SCTResponse) {} +} + +message SCTRequest { + bytes precertDER = 1; +} + +message SCTResponse { + repeated bytes sctDER = 1; } message GenerateOCSPRequest { string serial = 1; } -message UpdateRegistrationRequest { - core.Registration base = 1; - core.Registration update = 2; +message UpdateRegistrationContactRequest { + int64 registrationID = 1; + repeated string contacts = 2; +} + +message UpdateRegistrationKeyRequest { + int64 registrationID = 1; + bytes jwk = 2; +} + +message DeactivateRegistrationRequest { + int64 registrationID = 1; } message UpdateAuthorizationRequest { @@ -66,15 +91,32 @@ message AdministrativelyRevokeCertificateRequest { // certificate in question. In this case, the keyCompromise reason cannot be // specified, because the key cannot be blocked. bool malformed = 6; + // The CRL shard to store the revocation in. + // + // This is used when revoking malformed certificates, to allow human judgement + // in setting the CRL shard instead of automatically determining it by parsing + // the certificate. + // + // Passing a nonzero crlShard with malformed=false returns error. + int64 crlShard = 7; } message NewOrderRequest { - // Next unused field number: 6 + // Next unused field number: 9 int64 registrationID = 1; - repeated string names = 2; - string replacesSerial = 3; - bool limitsExempt = 4; + reserved 2; // previously dnsNames + repeated core.Identifier identifiers = 8; string certificateProfileName = 5; + // Replaces is the ARI certificate Id that this order replaces. + string replaces = 7; + // ReplacesSerial is the serial number of the certificate that this order replaces. + string replacesSerial = 3; + reserved 4; // previously isARIRenewal + reserved 6; // previously isRenewal +} + +message GetAuthorizationRequest { + int64 id = 1; } message FinalizeOrderRequest { @@ -88,3 +130,24 @@ message UnpauseAccountRequest { // The registrationID to be unpaused so issuance can be resumed. int64 registrationID = 1; } + +message UnpauseAccountResponse { + // Next unused field number: 2 + + // Count is the number of identifiers which were unpaused for the input regid. + int64 count = 1; +} + +message AddRateLimitOverrideRequest { + int64 limitEnum = 1; + string bucketKey = 2; + string comment = 3; + google.protobuf.Duration period = 4; + int64 count = 5; + int64 burst = 6; +} + +message AddRateLimitOverrideResponse { + bool inserted = 1; + bool enabled = 2; +} diff --git a/third-party/github.com/letsencrypt/boulder/ra/proto/ra_grpc.pb.go b/third-party/github.com/letsencrypt/boulder/ra/proto/ra_grpc.pb.go index d4fcdbab8..15c3ea287 100644 --- a/third-party/github.com/letsencrypt/boulder/ra/proto/ra_grpc.pb.go +++ b/third-party/github.com/letsencrypt/boulder/ra/proto/ra_grpc.pb.go @@ -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: ra.proto @@ -23,17 +23,20 @@ const _ = grpc.SupportPackageIsVersion9 const ( RegistrationAuthority_NewRegistration_FullMethodName = "/ra.RegistrationAuthority/NewRegistration" - RegistrationAuthority_UpdateRegistration_FullMethodName = "/ra.RegistrationAuthority/UpdateRegistration" - RegistrationAuthority_PerformValidation_FullMethodName = "/ra.RegistrationAuthority/PerformValidation" + RegistrationAuthority_UpdateRegistrationContact_FullMethodName = "/ra.RegistrationAuthority/UpdateRegistrationContact" + RegistrationAuthority_UpdateRegistrationKey_FullMethodName = "/ra.RegistrationAuthority/UpdateRegistrationKey" RegistrationAuthority_DeactivateRegistration_FullMethodName = "/ra.RegistrationAuthority/DeactivateRegistration" + RegistrationAuthority_PerformValidation_FullMethodName = "/ra.RegistrationAuthority/PerformValidation" RegistrationAuthority_DeactivateAuthorization_FullMethodName = "/ra.RegistrationAuthority/DeactivateAuthorization" RegistrationAuthority_RevokeCertByApplicant_FullMethodName = "/ra.RegistrationAuthority/RevokeCertByApplicant" RegistrationAuthority_RevokeCertByKey_FullMethodName = "/ra.RegistrationAuthority/RevokeCertByKey" RegistrationAuthority_AdministrativelyRevokeCertificate_FullMethodName = "/ra.RegistrationAuthority/AdministrativelyRevokeCertificate" RegistrationAuthority_NewOrder_FullMethodName = "/ra.RegistrationAuthority/NewOrder" + RegistrationAuthority_GetAuthorization_FullMethodName = "/ra.RegistrationAuthority/GetAuthorization" RegistrationAuthority_FinalizeOrder_FullMethodName = "/ra.RegistrationAuthority/FinalizeOrder" RegistrationAuthority_GenerateOCSP_FullMethodName = "/ra.RegistrationAuthority/GenerateOCSP" RegistrationAuthority_UnpauseAccount_FullMethodName = "/ra.RegistrationAuthority/UnpauseAccount" + RegistrationAuthority_AddRateLimitOverride_FullMethodName = "/ra.RegistrationAuthority/AddRateLimitOverride" ) // RegistrationAuthorityClient is the client API for RegistrationAuthority service. @@ -41,18 +44,21 @@ const ( // 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. type RegistrationAuthorityClient interface { NewRegistration(ctx context.Context, in *proto.Registration, opts ...grpc.CallOption) (*proto.Registration, error) - UpdateRegistration(ctx context.Context, in *UpdateRegistrationRequest, opts ...grpc.CallOption) (*proto.Registration, error) + UpdateRegistrationContact(ctx context.Context, in *UpdateRegistrationContactRequest, opts ...grpc.CallOption) (*proto.Registration, error) + UpdateRegistrationKey(ctx context.Context, in *UpdateRegistrationKeyRequest, opts ...grpc.CallOption) (*proto.Registration, error) + DeactivateRegistration(ctx context.Context, in *DeactivateRegistrationRequest, opts ...grpc.CallOption) (*proto.Registration, error) PerformValidation(ctx context.Context, in *PerformValidationRequest, opts ...grpc.CallOption) (*proto.Authorization, error) - DeactivateRegistration(ctx context.Context, in *proto.Registration, opts ...grpc.CallOption) (*emptypb.Empty, error) DeactivateAuthorization(ctx context.Context, in *proto.Authorization, opts ...grpc.CallOption) (*emptypb.Empty, error) RevokeCertByApplicant(ctx context.Context, in *RevokeCertByApplicantRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) RevokeCertByKey(ctx context.Context, in *RevokeCertByKeyRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) AdministrativelyRevokeCertificate(ctx context.Context, in *AdministrativelyRevokeCertificateRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) NewOrder(ctx context.Context, in *NewOrderRequest, opts ...grpc.CallOption) (*proto.Order, error) + GetAuthorization(ctx context.Context, in *GetAuthorizationRequest, opts ...grpc.CallOption) (*proto.Authorization, error) FinalizeOrder(ctx context.Context, in *FinalizeOrderRequest, opts ...grpc.CallOption) (*proto.Order, error) // Generate an OCSP response based on the DB's current status and reason code. GenerateOCSP(ctx context.Context, in *GenerateOCSPRequest, opts ...grpc.CallOption) (*proto1.OCSPResponse, error) - UnpauseAccount(ctx context.Context, in *UnpauseAccountRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + UnpauseAccount(ctx context.Context, in *UnpauseAccountRequest, opts ...grpc.CallOption) (*UnpauseAccountResponse, error) + AddRateLimitOverride(ctx context.Context, in *AddRateLimitOverrideRequest, opts ...grpc.CallOption) (*AddRateLimitOverrideResponse, error) } type registrationAuthorityClient struct { @@ -73,10 +79,30 @@ func (c *registrationAuthorityClient) NewRegistration(ctx context.Context, in *p return out, nil } -func (c *registrationAuthorityClient) UpdateRegistration(ctx context.Context, in *UpdateRegistrationRequest, opts ...grpc.CallOption) (*proto.Registration, error) { +func (c *registrationAuthorityClient) UpdateRegistrationContact(ctx context.Context, in *UpdateRegistrationContactRequest, opts ...grpc.CallOption) (*proto.Registration, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(proto.Registration) - err := c.cc.Invoke(ctx, RegistrationAuthority_UpdateRegistration_FullMethodName, in, out, cOpts...) + err := c.cc.Invoke(ctx, RegistrationAuthority_UpdateRegistrationContact_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *registrationAuthorityClient) UpdateRegistrationKey(ctx context.Context, in *UpdateRegistrationKeyRequest, opts ...grpc.CallOption) (*proto.Registration, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(proto.Registration) + err := c.cc.Invoke(ctx, RegistrationAuthority_UpdateRegistrationKey_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *registrationAuthorityClient) DeactivateRegistration(ctx context.Context, in *DeactivateRegistrationRequest, opts ...grpc.CallOption) (*proto.Registration, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(proto.Registration) + err := c.cc.Invoke(ctx, RegistrationAuthority_DeactivateRegistration_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -93,16 +119,6 @@ func (c *registrationAuthorityClient) PerformValidation(ctx context.Context, in return out, nil } -func (c *registrationAuthorityClient) DeactivateRegistration(ctx context.Context, in *proto.Registration, opts ...grpc.CallOption) (*emptypb.Empty, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(emptypb.Empty) - err := c.cc.Invoke(ctx, RegistrationAuthority_DeactivateRegistration_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - func (c *registrationAuthorityClient) DeactivateAuthorization(ctx context.Context, in *proto.Authorization, opts ...grpc.CallOption) (*emptypb.Empty, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(emptypb.Empty) @@ -153,6 +169,16 @@ func (c *registrationAuthorityClient) NewOrder(ctx context.Context, in *NewOrder return out, nil } +func (c *registrationAuthorityClient) GetAuthorization(ctx context.Context, in *GetAuthorizationRequest, opts ...grpc.CallOption) (*proto.Authorization, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(proto.Authorization) + err := c.cc.Invoke(ctx, RegistrationAuthority_GetAuthorization_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *registrationAuthorityClient) FinalizeOrder(ctx context.Context, in *FinalizeOrderRequest, opts ...grpc.CallOption) (*proto.Order, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(proto.Order) @@ -173,9 +199,9 @@ func (c *registrationAuthorityClient) GenerateOCSP(ctx context.Context, in *Gene return out, nil } -func (c *registrationAuthorityClient) UnpauseAccount(ctx context.Context, in *UnpauseAccountRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { +func (c *registrationAuthorityClient) UnpauseAccount(ctx context.Context, in *UnpauseAccountRequest, opts ...grpc.CallOption) (*UnpauseAccountResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(emptypb.Empty) + out := new(UnpauseAccountResponse) err := c.cc.Invoke(ctx, RegistrationAuthority_UnpauseAccount_FullMethodName, in, out, cOpts...) if err != nil { return nil, err @@ -183,42 +209,61 @@ func (c *registrationAuthorityClient) UnpauseAccount(ctx context.Context, in *Un return out, nil } +func (c *registrationAuthorityClient) AddRateLimitOverride(ctx context.Context, in *AddRateLimitOverrideRequest, opts ...grpc.CallOption) (*AddRateLimitOverrideResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(AddRateLimitOverrideResponse) + err := c.cc.Invoke(ctx, RegistrationAuthority_AddRateLimitOverride_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // RegistrationAuthorityServer is the server API for RegistrationAuthority service. // All implementations must embed UnimplementedRegistrationAuthorityServer -// for forward compatibility +// for forward compatibility. type RegistrationAuthorityServer interface { NewRegistration(context.Context, *proto.Registration) (*proto.Registration, error) - UpdateRegistration(context.Context, *UpdateRegistrationRequest) (*proto.Registration, error) + UpdateRegistrationContact(context.Context, *UpdateRegistrationContactRequest) (*proto.Registration, error) + UpdateRegistrationKey(context.Context, *UpdateRegistrationKeyRequest) (*proto.Registration, error) + DeactivateRegistration(context.Context, *DeactivateRegistrationRequest) (*proto.Registration, error) PerformValidation(context.Context, *PerformValidationRequest) (*proto.Authorization, error) - DeactivateRegistration(context.Context, *proto.Registration) (*emptypb.Empty, error) DeactivateAuthorization(context.Context, *proto.Authorization) (*emptypb.Empty, error) RevokeCertByApplicant(context.Context, *RevokeCertByApplicantRequest) (*emptypb.Empty, error) RevokeCertByKey(context.Context, *RevokeCertByKeyRequest) (*emptypb.Empty, error) AdministrativelyRevokeCertificate(context.Context, *AdministrativelyRevokeCertificateRequest) (*emptypb.Empty, error) NewOrder(context.Context, *NewOrderRequest) (*proto.Order, error) + GetAuthorization(context.Context, *GetAuthorizationRequest) (*proto.Authorization, error) FinalizeOrder(context.Context, *FinalizeOrderRequest) (*proto.Order, error) // Generate an OCSP response based on the DB's current status and reason code. GenerateOCSP(context.Context, *GenerateOCSPRequest) (*proto1.OCSPResponse, error) - UnpauseAccount(context.Context, *UnpauseAccountRequest) (*emptypb.Empty, error) + UnpauseAccount(context.Context, *UnpauseAccountRequest) (*UnpauseAccountResponse, error) + AddRateLimitOverride(context.Context, *AddRateLimitOverrideRequest) (*AddRateLimitOverrideResponse, error) mustEmbedUnimplementedRegistrationAuthorityServer() } -// UnimplementedRegistrationAuthorityServer must be embedded to have forward compatible implementations. -type UnimplementedRegistrationAuthorityServer struct { -} +// UnimplementedRegistrationAuthorityServer 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 UnimplementedRegistrationAuthorityServer struct{} func (UnimplementedRegistrationAuthorityServer) NewRegistration(context.Context, *proto.Registration) (*proto.Registration, error) { return nil, status.Errorf(codes.Unimplemented, "method NewRegistration not implemented") } -func (UnimplementedRegistrationAuthorityServer) UpdateRegistration(context.Context, *UpdateRegistrationRequest) (*proto.Registration, error) { - return nil, status.Errorf(codes.Unimplemented, "method UpdateRegistration not implemented") +func (UnimplementedRegistrationAuthorityServer) UpdateRegistrationContact(context.Context, *UpdateRegistrationContactRequest) (*proto.Registration, error) { + return nil, status.Errorf(codes.Unimplemented, "method UpdateRegistrationContact not implemented") +} +func (UnimplementedRegistrationAuthorityServer) UpdateRegistrationKey(context.Context, *UpdateRegistrationKeyRequest) (*proto.Registration, error) { + return nil, status.Errorf(codes.Unimplemented, "method UpdateRegistrationKey not implemented") +} +func (UnimplementedRegistrationAuthorityServer) DeactivateRegistration(context.Context, *DeactivateRegistrationRequest) (*proto.Registration, error) { + return nil, status.Errorf(codes.Unimplemented, "method DeactivateRegistration not implemented") } func (UnimplementedRegistrationAuthorityServer) PerformValidation(context.Context, *PerformValidationRequest) (*proto.Authorization, error) { return nil, status.Errorf(codes.Unimplemented, "method PerformValidation not implemented") } -func (UnimplementedRegistrationAuthorityServer) DeactivateRegistration(context.Context, *proto.Registration) (*emptypb.Empty, error) { - return nil, status.Errorf(codes.Unimplemented, "method DeactivateRegistration not implemented") -} func (UnimplementedRegistrationAuthorityServer) DeactivateAuthorization(context.Context, *proto.Authorization) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method DeactivateAuthorization not implemented") } @@ -234,16 +279,23 @@ func (UnimplementedRegistrationAuthorityServer) AdministrativelyRevokeCertificat func (UnimplementedRegistrationAuthorityServer) NewOrder(context.Context, *NewOrderRequest) (*proto.Order, error) { return nil, status.Errorf(codes.Unimplemented, "method NewOrder not implemented") } +func (UnimplementedRegistrationAuthorityServer) GetAuthorization(context.Context, *GetAuthorizationRequest) (*proto.Authorization, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetAuthorization not implemented") +} func (UnimplementedRegistrationAuthorityServer) FinalizeOrder(context.Context, *FinalizeOrderRequest) (*proto.Order, error) { return nil, status.Errorf(codes.Unimplemented, "method FinalizeOrder not implemented") } func (UnimplementedRegistrationAuthorityServer) GenerateOCSP(context.Context, *GenerateOCSPRequest) (*proto1.OCSPResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method GenerateOCSP not implemented") } -func (UnimplementedRegistrationAuthorityServer) UnpauseAccount(context.Context, *UnpauseAccountRequest) (*emptypb.Empty, error) { +func (UnimplementedRegistrationAuthorityServer) UnpauseAccount(context.Context, *UnpauseAccountRequest) (*UnpauseAccountResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method UnpauseAccount not implemented") } +func (UnimplementedRegistrationAuthorityServer) AddRateLimitOverride(context.Context, *AddRateLimitOverrideRequest) (*AddRateLimitOverrideResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method AddRateLimitOverride not implemented") +} func (UnimplementedRegistrationAuthorityServer) mustEmbedUnimplementedRegistrationAuthorityServer() {} +func (UnimplementedRegistrationAuthorityServer) testEmbeddedByValue() {} // UnsafeRegistrationAuthorityServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to RegistrationAuthorityServer will @@ -253,6 +305,13 @@ type UnsafeRegistrationAuthorityServer interface { } func RegisterRegistrationAuthorityServer(s grpc.ServiceRegistrar, srv RegistrationAuthorityServer) { + // If the following call pancis, it indicates UnimplementedRegistrationAuthorityServer 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(&RegistrationAuthority_ServiceDesc, srv) } @@ -274,20 +333,56 @@ func _RegistrationAuthority_NewRegistration_Handler(srv interface{}, ctx context return interceptor(ctx, in, info, handler) } -func _RegistrationAuthority_UpdateRegistration_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(UpdateRegistrationRequest) +func _RegistrationAuthority_UpdateRegistrationContact_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateRegistrationContactRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { - return srv.(RegistrationAuthorityServer).UpdateRegistration(ctx, in) + return srv.(RegistrationAuthorityServer).UpdateRegistrationContact(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: RegistrationAuthority_UpdateRegistration_FullMethodName, + FullMethod: RegistrationAuthority_UpdateRegistrationContact_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(RegistrationAuthorityServer).UpdateRegistration(ctx, req.(*UpdateRegistrationRequest)) + return srv.(RegistrationAuthorityServer).UpdateRegistrationContact(ctx, req.(*UpdateRegistrationContactRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _RegistrationAuthority_UpdateRegistrationKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateRegistrationKeyRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RegistrationAuthorityServer).UpdateRegistrationKey(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RegistrationAuthority_UpdateRegistrationKey_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RegistrationAuthorityServer).UpdateRegistrationKey(ctx, req.(*UpdateRegistrationKeyRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _RegistrationAuthority_DeactivateRegistration_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeactivateRegistrationRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RegistrationAuthorityServer).DeactivateRegistration(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RegistrationAuthority_DeactivateRegistration_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RegistrationAuthorityServer).DeactivateRegistration(ctx, req.(*DeactivateRegistrationRequest)) } return interceptor(ctx, in, info, handler) } @@ -310,24 +405,6 @@ func _RegistrationAuthority_PerformValidation_Handler(srv interface{}, ctx conte return interceptor(ctx, in, info, handler) } -func _RegistrationAuthority_DeactivateRegistration_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(proto.Registration) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(RegistrationAuthorityServer).DeactivateRegistration(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: RegistrationAuthority_DeactivateRegistration_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(RegistrationAuthorityServer).DeactivateRegistration(ctx, req.(*proto.Registration)) - } - return interceptor(ctx, in, info, handler) -} - func _RegistrationAuthority_DeactivateAuthorization_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(proto.Authorization) if err := dec(in); err != nil { @@ -418,6 +495,24 @@ func _RegistrationAuthority_NewOrder_Handler(srv interface{}, ctx context.Contex return interceptor(ctx, in, info, handler) } +func _RegistrationAuthority_GetAuthorization_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetAuthorizationRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RegistrationAuthorityServer).GetAuthorization(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RegistrationAuthority_GetAuthorization_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RegistrationAuthorityServer).GetAuthorization(ctx, req.(*GetAuthorizationRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _RegistrationAuthority_FinalizeOrder_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(FinalizeOrderRequest) if err := dec(in); err != nil { @@ -472,6 +567,24 @@ func _RegistrationAuthority_UnpauseAccount_Handler(srv interface{}, ctx context. return interceptor(ctx, in, info, handler) } +func _RegistrationAuthority_AddRateLimitOverride_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AddRateLimitOverrideRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RegistrationAuthorityServer).AddRateLimitOverride(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RegistrationAuthority_AddRateLimitOverride_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RegistrationAuthorityServer).AddRateLimitOverride(ctx, req.(*AddRateLimitOverrideRequest)) + } + return interceptor(ctx, in, info, handler) +} + // RegistrationAuthority_ServiceDesc is the grpc.ServiceDesc for RegistrationAuthority service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -484,17 +597,21 @@ var RegistrationAuthority_ServiceDesc = grpc.ServiceDesc{ Handler: _RegistrationAuthority_NewRegistration_Handler, }, { - MethodName: "UpdateRegistration", - Handler: _RegistrationAuthority_UpdateRegistration_Handler, + MethodName: "UpdateRegistrationContact", + Handler: _RegistrationAuthority_UpdateRegistrationContact_Handler, }, { - MethodName: "PerformValidation", - Handler: _RegistrationAuthority_PerformValidation_Handler, + MethodName: "UpdateRegistrationKey", + Handler: _RegistrationAuthority_UpdateRegistrationKey_Handler, }, { MethodName: "DeactivateRegistration", Handler: _RegistrationAuthority_DeactivateRegistration_Handler, }, + { + MethodName: "PerformValidation", + Handler: _RegistrationAuthority_PerformValidation_Handler, + }, { MethodName: "DeactivateAuthorization", Handler: _RegistrationAuthority_DeactivateAuthorization_Handler, @@ -515,6 +632,10 @@ var RegistrationAuthority_ServiceDesc = grpc.ServiceDesc{ MethodName: "NewOrder", Handler: _RegistrationAuthority_NewOrder_Handler, }, + { + MethodName: "GetAuthorization", + Handler: _RegistrationAuthority_GetAuthorization_Handler, + }, { MethodName: "FinalizeOrder", Handler: _RegistrationAuthority_FinalizeOrder_Handler, @@ -527,6 +648,112 @@ var RegistrationAuthority_ServiceDesc = grpc.ServiceDesc{ MethodName: "UnpauseAccount", Handler: _RegistrationAuthority_UnpauseAccount_Handler, }, + { + MethodName: "AddRateLimitOverride", + Handler: _RegistrationAuthority_AddRateLimitOverride_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "ra.proto", +} + +const ( + SCTProvider_GetSCTs_FullMethodName = "/ra.SCTProvider/GetSCTs" +) + +// SCTProviderClient is the client API for SCTProvider 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. +type SCTProviderClient interface { + GetSCTs(ctx context.Context, in *SCTRequest, opts ...grpc.CallOption) (*SCTResponse, error) +} + +type sCTProviderClient struct { + cc grpc.ClientConnInterface +} + +func NewSCTProviderClient(cc grpc.ClientConnInterface) SCTProviderClient { + return &sCTProviderClient{cc} +} + +func (c *sCTProviderClient) GetSCTs(ctx context.Context, in *SCTRequest, opts ...grpc.CallOption) (*SCTResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SCTResponse) + err := c.cc.Invoke(ctx, SCTProvider_GetSCTs_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// SCTProviderServer is the server API for SCTProvider service. +// All implementations must embed UnimplementedSCTProviderServer +// for forward compatibility. +type SCTProviderServer interface { + GetSCTs(context.Context, *SCTRequest) (*SCTResponse, error) + mustEmbedUnimplementedSCTProviderServer() +} + +// UnimplementedSCTProviderServer 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 UnimplementedSCTProviderServer struct{} + +func (UnimplementedSCTProviderServer) GetSCTs(context.Context, *SCTRequest) (*SCTResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetSCTs not implemented") +} +func (UnimplementedSCTProviderServer) mustEmbedUnimplementedSCTProviderServer() {} +func (UnimplementedSCTProviderServer) testEmbeddedByValue() {} + +// UnsafeSCTProviderServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to SCTProviderServer will +// result in compilation errors. +type UnsafeSCTProviderServer interface { + mustEmbedUnimplementedSCTProviderServer() +} + +func RegisterSCTProviderServer(s grpc.ServiceRegistrar, srv SCTProviderServer) { + // If the following call pancis, it indicates UnimplementedSCTProviderServer 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(&SCTProvider_ServiceDesc, srv) +} + +func _SCTProvider_GetSCTs_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SCTRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SCTProviderServer).GetSCTs(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: SCTProvider_GetSCTs_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SCTProviderServer).GetSCTs(ctx, req.(*SCTRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// SCTProvider_ServiceDesc is the grpc.ServiceDesc for SCTProvider service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var SCTProvider_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "ra.SCTProvider", + HandlerType: (*SCTProviderServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "GetSCTs", + Handler: _SCTProvider_GetSCTs_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "ra.proto", diff --git a/third-party/github.com/letsencrypt/boulder/ra/ra.go b/third-party/github.com/letsencrypt/boulder/ra/ra.go index a873276f5..91c58f1f2 100644 --- a/third-party/github.com/letsencrypt/boulder/ra/ra.go +++ b/third-party/github.com/letsencrypt/boulder/ra/ra.go @@ -1,19 +1,18 @@ package ra import ( + "bytes" "context" "crypto" "crypto/x509" - "encoding/hex" + "crypto/x509/pkix" + "encoding/asn1" "encoding/json" "errors" "fmt" - "math/big" - "net" "net/url" "os" "slices" - "sort" "strconv" "strings" "sync" @@ -23,9 +22,6 @@ import ( "github.com/jmhodges/clock" "github.com/prometheus/client_golang/prometheus" "golang.org/x/crypto/ocsp" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/emptypb" @@ -33,7 +29,9 @@ import ( "github.com/letsencrypt/boulder/akamai" akamaipb "github.com/letsencrypt/boulder/akamai/proto" + "github.com/letsencrypt/boulder/allowlist" capb "github.com/letsencrypt/boulder/ca/proto" + "github.com/letsencrypt/boulder/config" "github.com/letsencrypt/boulder/core" corepb "github.com/letsencrypt/boulder/core/proto" csrlib "github.com/letsencrypt/boulder/csr" @@ -50,10 +48,10 @@ import ( "github.com/letsencrypt/boulder/probs" pubpb "github.com/letsencrypt/boulder/publisher/proto" rapb "github.com/letsencrypt/boulder/ra/proto" - "github.com/letsencrypt/boulder/ratelimit" "github.com/letsencrypt/boulder/ratelimits" "github.com/letsencrypt/boulder/revocation" sapb "github.com/letsencrypt/boulder/sa/proto" + "github.com/letsencrypt/boulder/va" vapb "github.com/letsencrypt/boulder/va/proto" "github.com/letsencrypt/boulder/web" @@ -70,61 +68,52 @@ var ( caaRecheckDuration = -7 * time.Hour ) -type caaChecker interface { - IsCAAValid( - ctx context.Context, - in *vapb.IsCAAValidRequest, - opts ...grpc.CallOption, - ) (*vapb.IsCAAValidResponse, error) -} - // RegistrationAuthorityImpl defines an RA. // // NOTE: All of the fields in RegistrationAuthorityImpl need to be // populated, or there is a risk of panic. type RegistrationAuthorityImpl struct { rapb.UnsafeRegistrationAuthorityServer + rapb.UnsafeSCTProviderServer CA capb.CertificateAuthorityClient OCSP capb.OCSPGeneratorClient - VA vapb.VAClient + VA va.RemoteClients SA sapb.StorageAuthorityClient PA core.PolicyAuthority publisher pubpb.PublisherClient - caa caaChecker - clk clock.Clock - log blog.Logger - keyPolicy goodkey.KeyPolicy - // How long before a newly created authorization expires. - authorizationLifetime time.Duration - pendingAuthorizationLifetime time.Duration - rlPolicies ratelimit.Limits - maxContactsPerReg int - limiter *ratelimits.Limiter - txnBuilder *ratelimits.TransactionBuilder - maxNames int - orderLifetime time.Duration - finalizeTimeout time.Duration - finalizeWG sync.WaitGroup + clk clock.Clock + log blog.Logger + keyPolicy goodkey.KeyPolicy + profiles *validationProfiles + maxContactsPerReg int + limiter *ratelimits.Limiter + txnBuilder *ratelimits.TransactionBuilder + finalizeTimeout time.Duration + drainWG sync.WaitGroup issuersByNameID map[issuance.NameID]*issuance.Certificate purger akamaipb.AkamaiPurgerClient ctpolicy *ctpolicy.CTPolicy - ctpolicyResults *prometheus.HistogramVec - revocationReasonCounter *prometheus.CounterVec - namesPerCert *prometheus.HistogramVec - rlCheckLatency *prometheus.HistogramVec - rlOverrideUsageGauge *prometheus.GaugeVec - newRegCounter prometheus.Counter - recheckCAACounter prometheus.Counter - newCertCounter *prometheus.CounterVec - recheckCAAUsedAuthzLifetime prometheus.Counter - authzAges *prometheus.HistogramVec - orderAges *prometheus.HistogramVec - inflightFinalizes prometheus.Gauge - certCSRMismatch prometheus.Counter + ctpolicyResults *prometheus.HistogramVec + revocationReasonCounter *prometheus.CounterVec + namesPerCert *prometheus.HistogramVec + newRegCounter prometheus.Counter + recheckCAACounter prometheus.Counter + newCertCounter prometheus.Counter + authzAges *prometheus.HistogramVec + orderAges *prometheus.HistogramVec + inflightFinalizes prometheus.Gauge + certCSRMismatch prometheus.Counter + pauseCounter *prometheus.CounterVec + // TODO(#8177): Remove once the rate of requests failing to finalize due to + // requesting Must-Staple has diminished. + mustStapleRequestsCounter *prometheus.CounterVec + // TODO(#7966): Remove once the rate of registrations with contacts has been + // determined. + newOrUpdatedContactCounter *prometheus.CounterVec } var _ rapb.RegistrationAuthorityServer = (*RegistrationAuthorityImpl)(nil) @@ -139,11 +128,8 @@ func NewRegistrationAuthorityImpl( limiter *ratelimits.Limiter, txnBuilder *ratelimits.TransactionBuilder, maxNames int, - authorizationLifetime time.Duration, - pendingAuthorizationLifetime time.Duration, + profiles *validationProfiles, pubc pubpb.PublisherClient, - caaClient caaChecker, - orderLifetime time.Duration, finalizeTimeout time.Duration, ctp *ctpolicy.CTPolicy, purger akamaipb.AkamaiPurgerClient, @@ -172,18 +158,6 @@ func NewRegistrationAuthorityImpl( ) stats.MustRegister(namesPerCert) - rlCheckLatency := prometheus.NewHistogramVec(prometheus.HistogramOpts{ - Name: "ratelimitsv1_check_latency_seconds", - Help: fmt.Sprintf("Latency of ratelimit checks labeled by limit=[name] and decision=[%s|%s], in seconds", ratelimits.Allowed, ratelimits.Denied), - }, []string{"limit", "decision"}) - stats.MustRegister(rlCheckLatency) - - overrideUsageGauge := prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Name: "ratelimitsv1_override_usage", - Help: "Proportion of override limit used, by limit name and client identifier.", - }, []string{"limit", "override_key"}) - stats.MustRegister(overrideUsageGauge) - newRegCounter := prometheus.NewCounter(prometheus.CounterOpts{ Name: "new_registrations", Help: "A counter of new registrations", @@ -196,16 +170,10 @@ func NewRegistrationAuthorityImpl( }) stats.MustRegister(recheckCAACounter) - recheckCAAUsedAuthzLifetime := prometheus.NewCounter(prometheus.CounterOpts{ - Name: "recheck_caa_used_authz_lifetime", - Help: "A counter times the old codepath was used for CAA recheck time", - }) - stats.MustRegister(recheckCAAUsedAuthzLifetime) - - newCertCounter := prometheus.NewCounterVec(prometheus.CounterOpts{ + newCertCounter := prometheus.NewCounter(prometheus.CounterOpts{ Name: "new_certificates", - Help: "A counter of new certificates including the certificate profile name and hexadecimal certificate profile hash", - }, []string{"profileName", "profileHash"}) + Help: "A counter of issued certificates", + }) stats.MustRegister(newCertCounter) revocationReasonCounter := prometheus.NewCounterVec(prometheus.CounterOpts{ @@ -250,57 +218,202 @@ func NewRegistrationAuthorityImpl( }) stats.MustRegister(certCSRMismatch) + pauseCounter := prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: "paused_pairs", + Help: "Number of times a pause operation is performed, labeled by paused=[bool], repaused=[bool], grace=[bool]", + }, []string{"paused", "repaused", "grace"}) + stats.MustRegister(pauseCounter) + + mustStapleRequestsCounter := prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: "must_staple_requests", + Help: "Number of times a must-staple request is made, labeled by allowlist=[allowed|denied]", + }, []string{"allowlist"}) + stats.MustRegister(mustStapleRequestsCounter) + + // TODO(#7966): Remove once the rate of registrations with contacts has been + // determined. + newOrUpdatedContactCounter := prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: "new_or_updated_contact", + Help: "A counter of new or updated contacts, labeled by new=[bool]", + }, []string{"new"}) + stats.MustRegister(newOrUpdatedContactCounter) + issuersByNameID := make(map[issuance.NameID]*issuance.Certificate) for _, issuer := range issuers { issuersByNameID[issuer.NameID()] = issuer } ra := &RegistrationAuthorityImpl{ - clk: clk, - log: logger, - authorizationLifetime: authorizationLifetime, - pendingAuthorizationLifetime: pendingAuthorizationLifetime, - rlPolicies: ratelimit.New(), - maxContactsPerReg: maxContactsPerReg, - keyPolicy: keyPolicy, - limiter: limiter, - txnBuilder: txnBuilder, - maxNames: maxNames, - publisher: pubc, - caa: caaClient, - orderLifetime: orderLifetime, - finalizeTimeout: finalizeTimeout, - ctpolicy: ctp, - ctpolicyResults: ctpolicyResults, - purger: purger, - issuersByNameID: issuersByNameID, - namesPerCert: namesPerCert, - rlCheckLatency: rlCheckLatency, - rlOverrideUsageGauge: overrideUsageGauge, - newRegCounter: newRegCounter, - recheckCAACounter: recheckCAACounter, - newCertCounter: newCertCounter, - revocationReasonCounter: revocationReasonCounter, - recheckCAAUsedAuthzLifetime: recheckCAAUsedAuthzLifetime, - authzAges: authzAges, - orderAges: orderAges, - inflightFinalizes: inflightFinalizes, - certCSRMismatch: certCSRMismatch, + clk: clk, + log: logger, + profiles: profiles, + maxContactsPerReg: maxContactsPerReg, + keyPolicy: keyPolicy, + limiter: limiter, + txnBuilder: txnBuilder, + publisher: pubc, + finalizeTimeout: finalizeTimeout, + ctpolicy: ctp, + ctpolicyResults: ctpolicyResults, + purger: purger, + issuersByNameID: issuersByNameID, + namesPerCert: namesPerCert, + newRegCounter: newRegCounter, + recheckCAACounter: recheckCAACounter, + newCertCounter: newCertCounter, + revocationReasonCounter: revocationReasonCounter, + authzAges: authzAges, + orderAges: orderAges, + inflightFinalizes: inflightFinalizes, + certCSRMismatch: certCSRMismatch, + pauseCounter: pauseCounter, + mustStapleRequestsCounter: mustStapleRequestsCounter, + newOrUpdatedContactCounter: newOrUpdatedContactCounter, } return ra } -func (ra *RegistrationAuthorityImpl) LoadRateLimitPoliciesFile(filename string) error { - configBytes, err := os.ReadFile(filename) - if err != nil { - return err - } - err = ra.rlPolicies.LoadPolicies(configBytes) - if err != nil { - return err +// ValidationProfileConfig is a config struct which can be used to create a +// ValidationProfile. +type ValidationProfileConfig struct { + // PendingAuthzLifetime defines how far in the future an authorization's + // "expires" timestamp is set when it is first created, i.e. how much + // time the applicant has to attempt the challenge. + PendingAuthzLifetime config.Duration `validate:"required"` + // ValidAuthzLifetime defines how far in the future an authorization's + // "expires" timestamp is set when one of its challenges is fulfilled, + // i.e. how long a validated authorization may be reused. + ValidAuthzLifetime config.Duration `validate:"required"` + // OrderLifetime defines how far in the future an order's "expires" + // timestamp is set when it is first created, i.e. how much time the + // applicant has to fulfill all challenges and finalize the order. This is + // a maximum time: if the order reuses an authorization and that authz + // expires earlier than this OrderLifetime would otherwise set, then the + // order's expiration is brought in to match that authorization. + OrderLifetime config.Duration `validate:"required"` + // 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 + // limits are per section 7.1 of our combined CP/CPS, under "DV-SSL + // Subscriber Certificate". The value must be less than or equal to the + // global (i.e. not per-profile) value configured in the CA. + MaxNames int `validate:"omitempty,min=1,max=100"` + // AllowList specifies the path to a YAML file containing a list of + // account IDs permitted to use this profile. If no path is + // specified, the profile is open to all accounts. If the file + // exists but is empty, the profile is closed to all accounts. + AllowList string `validate:"omitempty"` + // IdentifierTypes is a list of identifier types that may be issued under + // this profile. + IdentifierTypes []identifier.IdentifierType `validate:"required,dive,oneof=dns ip"` +} + +// validationProfile holds the attributes of a given validation profile. +type validationProfile struct { + // pendingAuthzLifetime defines how far in the future an authorization's + // "expires" timestamp is set when it is first created, i.e. how much + // time the applicant has to attempt the challenge. + pendingAuthzLifetime time.Duration + // validAuthzLifetime defines how far in the future an authorization's + // "expires" timestamp is set when one of its challenges is fulfilled, + // i.e. how long a validated authorization may be reused. + validAuthzLifetime time.Duration + // orderLifetime defines how far in the future an order's "expires" + // timestamp is set when it is first created, i.e. how much time the + // applicant has to fulfill all challenges and finalize the order. This is + // a maximum time: if the order reuses an authorization and that authz + // expires earlier than this OrderLifetime would otherwise set, then the + // order's expiration is brought in to match that authorization. + orderLifetime time.Duration + // maxNames is the maximum number of subjectAltNames in a single cert. + maxNames int + // allowList holds the set of account IDs allowed to use this profile. If + // nil, the profile is open to all accounts (everyone is allowed). + allowList *allowlist.List[int64] + // identifierTypes is a list of identifier types that may be issued under + // this profile. + identifierTypes []identifier.IdentifierType +} + +// validationProfiles provides access to the set of configured profiles, +// including the default profile for orders/authzs which do not specify one. +type validationProfiles struct { + defaultName string + byName map[string]*validationProfile +} + +// NewValidationProfiles builds a new validationProfiles struct from the given +// configs and default name. It enforces that the given authorization lifetimes +// are within the bounds mandated by the Baseline Requirements. +func NewValidationProfiles(defaultName string, configs map[string]*ValidationProfileConfig) (*validationProfiles, error) { + if defaultName == "" { + return nil, errors.New("default profile name must be configured") } - return nil + profiles := make(map[string]*validationProfile, len(configs)) + + for name, config := range configs { + // 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 config.PendingAuthzLifetime.Duration <= 0 || config.PendingAuthzLifetime.Duration > 29*(24*time.Hour) { + return nil, fmt.Errorf("PendingAuthzLifetime value must be greater than 0 and less than 30d, but got %q", config.PendingAuthzLifetime.Duration) + } + + // 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 config.ValidAuthzLifetime.Duration <= 0 || config.ValidAuthzLifetime.Duration > 397*(24*time.Hour) { + return nil, fmt.Errorf("ValidAuthzLifetime value must be greater than 0 and less than 398d, but got %q", config.ValidAuthzLifetime.Duration) + } + + if config.MaxNames <= 0 || config.MaxNames > 100 { + return nil, fmt.Errorf("MaxNames must be greater than 0 and at most 100") + } + + var allowList *allowlist.List[int64] + if config.AllowList != "" { + data, err := os.ReadFile(config.AllowList) + if err != nil { + return nil, fmt.Errorf("reading allowlist: %w", err) + } + allowList, err = allowlist.NewFromYAML[int64](data) + if err != nil { + return nil, fmt.Errorf("parsing allowlist: %w", err) + } + } + + profiles[name] = &validationProfile{ + pendingAuthzLifetime: config.PendingAuthzLifetime.Duration, + validAuthzLifetime: config.ValidAuthzLifetime.Duration, + orderLifetime: config.OrderLifetime.Duration, + maxNames: config.MaxNames, + allowList: allowList, + identifierTypes: config.IdentifierTypes, + } + } + + _, ok := profiles[defaultName] + if !ok { + return nil, fmt.Errorf("no profile configured matching default profile name %q", defaultName) + } + + return &validationProfiles{ + defaultName: defaultName, + byName: profiles, + }, nil +} + +func (vp *validationProfiles) get(name string) (*validationProfile, error) { + if name == "" { + name = vp.defaultName + } + profile, ok := vp.byName[name] + if !ok { + return nil, berrors.InvalidProfileError("unrecognized profile name %q", name) + } + return profile, nil } // certificateRequestAuthz is a struct for holding information about a valid @@ -329,8 +442,8 @@ type certificateRequestEvent struct { VerifiedFields []string `json:",omitempty"` // CommonName is the subject common name from the issued cert CommonName string `json:",omitempty"` - // Names are the DNS SAN entries from the issued cert - Names []string `json:",omitempty"` + // Identifiers are the identifiers from the issued cert + Identifiers identifier.ACMEIdentifiers `json:",omitempty"` // NotBefore is the starting timestamp of the issued cert's validity period NotBefore time.Time `json:",omitempty"` // NotAfter is the ending timestamp of the issued cert's validity period @@ -350,6 +463,13 @@ type certificateRequestEvent struct { // CertProfileHash is SHA256 sum over every exported field of an // issuance.ProfileConfig, represented here as a hexadecimal string. CertProfileHash string `json:",omitempty"` + // PreviousCertificateIssued is present when this certificate uses the same set + // of FQDNs as a previous certificate (from any account) and contains the + // notBefore of the most recent such certificate. + PreviousCertificateIssued time.Time `json:",omitempty"` + // UserAgent is the User-Agent header from the ACME client (provided to the + // RA via gRPC metadata). + UserAgent string } // certificateRevocationEvent is a struct for holding information that is logged @@ -360,13 +480,14 @@ type certificateRevocationEvent struct { // serial number. SerialNumber string `json:",omitempty"` // Reason is the integer representing the revocation reason used. - Reason int64 `json:",omitempty"` + Reason int64 `json:"reason"` // Method is the way in which revocation was requested. // It will be one of the strings: "applicant", "subscriber", "control", "key", or "admin". Method string `json:",omitempty"` // RequesterID is the account ID of the requester. // Will be zero for admin revocations. RequesterID int64 `json:",omitempty"` + CRLShard int64 // AdminName is the name of the admin requester. // Will be zero for subscriber revocations. AdminName string `json:",omitempty"` @@ -388,99 +509,10 @@ type finalizationCAACheckEvent struct { Rechecked int `json:",omitempty"` } -// noRegistrationID is used for the regID parameter to GetThreshold when no -// registration-based overrides are necessary. -const noRegistrationID = -1 - -// registrationCounter is a type to abstract the use of `CountRegistrationsByIP` -// or `CountRegistrationsByIPRange` SA methods. -type registrationCounter func(context.Context, *sapb.CountRegistrationsByIPRequest, ...grpc.CallOption) (*sapb.Count, error) - -// checkRegistrationIPLimit checks a specific registraton limit by using the -// provided registrationCounter function to determine if the limit has been -// exceeded for a given IP or IP range -func (ra *RegistrationAuthorityImpl) checkRegistrationIPLimit(ctx context.Context, limit ratelimit.RateLimitPolicy, ip net.IP, counter registrationCounter) error { - now := ra.clk.Now() - count, err := counter(ctx, &sapb.CountRegistrationsByIPRequest{ - Ip: ip, - Range: &sapb.Range{ - Earliest: timestamppb.New(limit.WindowBegin(now)), - Latest: timestamppb.New(now), - }, - }) - if err != nil { - return err - } - - threshold, overrideKey := limit.GetThreshold(ip.String(), noRegistrationID) - if count.Count >= threshold { - return berrors.RegistrationsPerIPError(0, "too many registrations for this IP") - } - if overrideKey != "" { - // We do not support overrides for the NewRegistrationsPerIPRange limit. - utilization := float64(count.Count+1) / float64(threshold) - ra.rlOverrideUsageGauge.WithLabelValues(ratelimit.RegistrationsPerIP, overrideKey).Set(utilization) - } - - return nil -} - -// checkRegistrationLimits enforces the RegistrationsPerIP and -// RegistrationsPerIPRange limits -func (ra *RegistrationAuthorityImpl) checkRegistrationLimits(ctx context.Context, ip net.IP) error { - // Check the registrations per IP limit using the CountRegistrationsByIP SA - // function that matches IP addresses exactly - exactRegLimit := ra.rlPolicies.RegistrationsPerIP() - if exactRegLimit.Enabled() { - started := ra.clk.Now() - err := ra.checkRegistrationIPLimit(ctx, exactRegLimit, ip, ra.SA.CountRegistrationsByIP) - elapsed := ra.clk.Since(started) - if err != nil { - if errors.Is(err, berrors.RateLimit) { - ra.rlCheckLatency.WithLabelValues(ratelimit.RegistrationsPerIP, ratelimits.Denied).Observe(elapsed.Seconds()) - ra.log.Infof("Rate limit exceeded, RegistrationsPerIP, by IP: %q", ip) - } - return err - } - ra.rlCheckLatency.WithLabelValues(ratelimit.RegistrationsPerIP, ratelimits.Allowed).Observe(elapsed.Seconds()) - } - - // We only apply the fuzzy reg limit to IPv6 addresses. - // Per https://golang.org/pkg/net/#IP.To4 "If ip is not an IPv4 address, To4 - // returns nil" - if ip.To4() != nil { - return nil - } - - // Check the registrations per IP range limit using the - // CountRegistrationsByIPRange SA function that fuzzy-matches IPv6 addresses - // within a larger address range - fuzzyRegLimit := ra.rlPolicies.RegistrationsPerIPRange() - if fuzzyRegLimit.Enabled() { - started := ra.clk.Now() - err := ra.checkRegistrationIPLimit(ctx, fuzzyRegLimit, ip, ra.SA.CountRegistrationsByIPRange) - elapsed := ra.clk.Since(started) - if err != nil { - if errors.Is(err, berrors.RateLimit) { - ra.rlCheckLatency.WithLabelValues(ratelimit.RegistrationsPerIPRange, ratelimits.Denied).Observe(elapsed.Seconds()) - ra.log.Infof("Rate limit exceeded, RegistrationsByIPRange, IP: %q", ip) - - // For the fuzzyRegLimit we use a new error message that specifically - // mentions that the limit being exceeded is applied to a *range* of IPs - return berrors.RateLimitError(0, "too many registrations for this IP range") - } - return err - } - ra.rlCheckLatency.WithLabelValues(ratelimit.RegistrationsPerIPRange, ratelimits.Allowed).Observe(elapsed.Seconds()) - } - - return nil -} - // NewRegistration constructs a new Registration from a request. func (ra *RegistrationAuthorityImpl) NewRegistration(ctx context.Context, request *corepb.Registration) (*corepb.Registration, error) { // Error if the request is nil, there is no account key or IP address - if request == nil || len(request.Key) == 0 || len(request.InitialIP) == 0 { + if request == nil || len(request.Key) == 0 { return nil, errIncompleteGRPCRequest } @@ -495,22 +527,8 @@ func (ra *RegistrationAuthorityImpl) NewRegistration(ctx context.Context, reques return nil, berrors.MalformedError("invalid public key: %s", err.Error()) } - // Check IP address rate limits. - var ipAddr net.IP - err = ipAddr.UnmarshalText(request.InitialIP) - if err != nil { - return nil, berrors.InternalServerError("failed to unmarshal ip address: %s", err.Error()) - } - err = ra.checkRegistrationLimits(ctx, ipAddr) - if err != nil { - return nil, err - } - // Check that contacts conform to our expectations. - err = validateContactsPresent(request.Contact, request.ContactsPresent) - if err != nil { - return nil, err - } + // TODO(#8199): Remove this when no contacts are included in any requests. err = ra.validateContacts(request.Contact) if err != nil { return nil, err @@ -518,12 +536,10 @@ func (ra *RegistrationAuthorityImpl) NewRegistration(ctx context.Context, reques // Don't populate ID or CreatedAt because those will be set by the SA. req := &corepb.Registration{ - Key: request.Key, - Contact: request.Contact, - ContactsPresent: request.ContactsPresent, - Agreement: request.Agreement, - InitialIP: request.InitialIP, - Status: string(core.StatusValid), + Key: request.Key, + Contact: request.Contact, + Agreement: request.Agreement, + Status: string(core.StatusValid), } // Store the registration object, then return the version that got stored. @@ -532,6 +548,12 @@ func (ra *RegistrationAuthorityImpl) NewRegistration(ctx context.Context, reques return nil, err } + // TODO(#7966): Remove once the rate of registrations with contacts has been + // determined. + for range request.Contact { + ra.newOrUpdatedContactCounter.With(prometheus.Labels{"new": "true"}).Inc() + } + ra.newRegCounter.Inc() return res, nil } @@ -564,22 +586,19 @@ func (ra *RegistrationAuthorityImpl) validateContacts(contacts []string) error { } parsed, err := url.Parse(contact) if err != nil { - return berrors.InvalidEmailError("invalid contact") + return berrors.InvalidEmailError("unparsable contact") } if parsed.Scheme != "mailto" { - return berrors.UnsupportedContactError("contact method %q is not supported", parsed.Scheme) + return berrors.UnsupportedContactError("only contact scheme 'mailto:' is supported") } if parsed.RawQuery != "" || contact[len(contact)-1] == '?' { - return berrors.InvalidEmailError("contact email %q contains a question mark", contact) + return berrors.InvalidEmailError("contact email contains a question mark") } if parsed.Fragment != "" || contact[len(contact)-1] == '#' { - return berrors.InvalidEmailError("contact email %q contains a '#'", contact) + return berrors.InvalidEmailError("contact email contains a '#'") } if !core.IsASCII(contact) { - return berrors.InvalidEmailError( - "contact email [%q] contains non-ASCII characters", - contact, - ) + return berrors.InvalidEmailError("contact email contains non-ASCII characters") } err = policy.ValidEmail(parsed.Opaque) if err != nil { @@ -593,10 +612,7 @@ func (ra *RegistrationAuthorityImpl) validateContacts(contacts []string) error { // That means the largest marshalled JSON value we can store is 191 bytes. const maxContactBytes = 191 if jsonBytes, err := json.Marshal(contacts); err != nil { - // This shouldn't happen with a simple []string but if it does we want the - // error to be logged internally but served as a 500 to the user so we - // return a bare error and not a berror here. - return fmt.Errorf("failed to marshal reg.Contact to JSON: %#v", contacts) + return fmt.Errorf("failed to marshal reg.Contact to JSON: %w", err) } else if len(jsonBytes) >= maxContactBytes { return berrors.InvalidEmailError( "too many/too long contact(s). Please use shorter or fewer email addresses") @@ -605,121 +621,8 @@ func (ra *RegistrationAuthorityImpl) validateContacts(contacts []string) error { return nil } -func (ra *RegistrationAuthorityImpl) checkPendingAuthorizationLimit(ctx context.Context, regID int64, limit ratelimit.RateLimitPolicy) error { - // This rate limit's threshold can only be overridden on a per-regID basis, - // not based on any other key. - threshold, overrideKey := limit.GetThreshold("", regID) - if threshold == -1 { - return nil - } - countPB, err := ra.SA.CountPendingAuthorizations2(ctx, &sapb.RegistrationID{ - Id: regID, - }) - if err != nil { - return err - } - if countPB.Count >= threshold { - ra.log.Infof("Rate limit exceeded, PendingAuthorizationsByRegID, regID: %d", regID) - return berrors.RateLimitError(0, "too many currently pending authorizations: %d", countPB.Count) - } - if overrideKey != "" { - utilization := float64(countPB.Count) / float64(threshold) - ra.rlOverrideUsageGauge.WithLabelValues(ratelimit.PendingAuthorizationsPerAccount, overrideKey).Set(utilization) - } - return nil -} - -// checkInvalidAuthorizationLimits checks the failed validation limit for each -// of the provided hostnames. It returns the first error. -func (ra *RegistrationAuthorityImpl) checkInvalidAuthorizationLimits(ctx context.Context, regID int64, hostnames []string, limits ratelimit.RateLimitPolicy) error { - results := make(chan error, len(hostnames)) - for _, hostname := range hostnames { - go func(hostname string) { - results <- ra.checkInvalidAuthorizationLimit(ctx, regID, hostname, limits) - }(hostname) - } - // We don't have to wait for all of the goroutines to finish because there's - // enough capacity in the chan for them all to write their result even if - // nothing is reading off the chan anymore. - for range len(hostnames) { - err := <-results - if err != nil { - return err - } - } - return nil -} - -func (ra *RegistrationAuthorityImpl) checkInvalidAuthorizationLimit(ctx context.Context, regID int64, hostname string, limit ratelimit.RateLimitPolicy) error { - latest := ra.clk.Now().Add(ra.pendingAuthorizationLifetime) - earliest := latest.Add(-limit.Window.Duration) - req := &sapb.CountInvalidAuthorizationsRequest{ - RegistrationID: regID, - Hostname: hostname, - Range: &sapb.Range{ - Earliest: timestamppb.New(earliest), - Latest: timestamppb.New(latest), - }, - } - count, err := ra.SA.CountInvalidAuthorizations2(ctx, req) - if err != nil { - return err - } - // Most rate limits have a key for overrides, but there is no meaningful key - // here. - noKey := "" - threshold, overrideKey := limit.GetThreshold(noKey, regID) - if count.Count >= threshold { - ra.log.Infof("Rate limit exceeded, InvalidAuthorizationsByRegID, regID: %d", regID) - return berrors.FailedValidationError(0, "too many failed authorizations recently") - } - if overrideKey != "" { - utilization := float64(count.Count) / float64(threshold) - ra.rlOverrideUsageGauge.WithLabelValues(ratelimit.InvalidAuthorizationsPerAccount, overrideKey).Set(utilization) - } - return nil -} - -// checkNewOrdersPerAccountLimit enforces the rlPolicies `NewOrdersPerAccount` -// rate limit. This rate limit ensures a client can not create more than the -// specified threshold of new orders within the specified time window. -func (ra *RegistrationAuthorityImpl) checkNewOrdersPerAccountLimit(ctx context.Context, acctID int64, names []string, limit ratelimit.RateLimitPolicy) error { - // Check if there is already an existing certificate for the exact name set we - // are issuing for. If so bypass the newOrders limit. - exists, err := ra.SA.FQDNSetExists(ctx, &sapb.FQDNSetExistsRequest{Domains: names}) - if err != nil { - return fmt.Errorf("checking renewal exemption for %q: %s", names, err) - } - if exists.Exists { - return nil - } - - now := ra.clk.Now() - count, err := ra.SA.CountOrders(ctx, &sapb.CountOrdersRequest{ - AccountID: acctID, - Range: &sapb.Range{ - Earliest: timestamppb.New(now.Add(-limit.Window.Duration)), - Latest: timestamppb.New(now), - }, - }) - if err != nil { - return err - } - // There is no meaningful override key to use for this rate limit - noKey := "" - threshold, overrideKey := limit.GetThreshold(noKey, acctID) - if count.Count >= threshold { - return berrors.RateLimitError(0, "too many new orders recently") - } - if overrideKey != "" { - utilization := float64(count.Count+1) / float64(threshold) - ra.rlOverrideUsageGauge.WithLabelValues(ratelimit.NewOrdersPerAccount, overrideKey).Set(utilization) - } - return nil -} - // matchesCSR tests the contents of a generated certificate to make sure -// that the PublicKey, CommonName, and DNSNames match those provided in +// that the PublicKey, CommonName, and identifiers match those provided in // the CSR that was used to generate the certificate. It also checks the // following fields for: // - notBefore is not more than 24 hours ago @@ -732,29 +635,29 @@ func (ra *RegistrationAuthorityImpl) matchesCSR(parsedCertificate *x509.Certific return berrors.InternalServerError("generated certificate public key doesn't match CSR public key") } - csrNames := csrlib.NamesFromCSR(csr) + csrIdents := identifier.FromCSR(csr) if parsedCertificate.Subject.CommonName != "" { // Only check that the issued common name matches one of the SANs if there // is an issued CN at all: this allows flexibility on whether we include // the CN. - if !slices.Contains(csrNames.SANs, parsedCertificate.Subject.CommonName) { + if !slices.Contains(csrIdents, identifier.NewDNS(parsedCertificate.Subject.CommonName)) { return berrors.InternalServerError("generated certificate CommonName doesn't match any CSR name") } } - parsedNames := parsedCertificate.DNSNames - sort.Strings(parsedNames) - if !slices.Equal(parsedNames, csrNames.SANs) { - return berrors.InternalServerError("generated certificate DNSNames don't match CSR DNSNames") + parsedIdents := identifier.FromCert(parsedCertificate) + if !slices.Equal(csrIdents, parsedIdents) { + return berrors.InternalServerError("generated certificate identifiers don't match CSR identifiers") } - if !slices.EqualFunc(parsedCertificate.IPAddresses, csr.IPAddresses, func(l, r net.IP) bool { return l.Equal(r) }) { - return berrors.InternalServerError("generated certificate IPAddresses don't match CSR IPAddresses") - } if !slices.Equal(parsedCertificate.EmailAddresses, csr.EmailAddresses) { return berrors.InternalServerError("generated certificate EmailAddresses don't match CSR EmailAddresses") } + if !slices.Equal(parsedCertificate.URIs, csr.URIs) { + return berrors.InternalServerError("generated certificate URIs don't match CSR URIs") + } + if len(parsedCertificate.Subject.Country) > 0 || len(parsedCertificate.Subject.Organization) > 0 || len(parsedCertificate.Subject.OrganizationalUnit) > 0 || len(parsedCertificate.Subject.Locality) > 0 || len(parsedCertificate.Subject.Province) > 0 || len(parsedCertificate.Subject.StreetAddress) > 0 || @@ -771,8 +674,13 @@ func (ra *RegistrationAuthorityImpl) matchesCSR(parsedCertificate *x509.Certific if parsedCertificate.IsCA { return berrors.InternalServerError("generated certificate can sign other certificates") } - if !slices.Equal(parsedCertificate.ExtKeyUsage, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}) { - return berrors.InternalServerError("generated certificate doesn't have correct key usage extensions") + for _, eku := range parsedCertificate.ExtKeyUsage { + if eku != x509.ExtKeyUsageServerAuth && eku != x509.ExtKeyUsageClientAuth { + return berrors.InternalServerError("generated certificate has unacceptable EKU") + } + } + if !slices.Contains(parsedCertificate.ExtKeyUsage, x509.ExtKeyUsageServerAuth) { + return berrors.InternalServerError("generated certificate doesn't have serverAuth EKU") } return nil @@ -785,9 +693,10 @@ func (ra *RegistrationAuthorityImpl) matchesCSR(parsedCertificate *x509.Certific // will be of type BoulderError. func (ra *RegistrationAuthorityImpl) checkOrderAuthorizations( ctx context.Context, - names []string, + orderID orderID, acctID accountID, - orderID orderID) (map[string]*core.Authorization, error) { + idents identifier.ACMEIdentifiers, + now time.Time) (map[identifier.ACMEIdentifier]*core.Authorization, error) { // Get all of the valid authorizations for this account/order req := &sapb.GetValidOrderAuthorizationsRequest{ Id: int64(orderID), @@ -802,21 +711,62 @@ func (ra *RegistrationAuthorityImpl) checkOrderAuthorizations( return nil, err } - // Ensure the names from the CSR are free of duplicates & lowercased. - names = core.UniqueLowerNames(names) - - // Check the authorizations to ensure validity for the names required. - err = ra.checkAuthorizationsCAA(ctx, int64(acctID), names, authzs, ra.clk.Now()) - if err != nil { - return nil, err + // Ensure that every identifier has a matching authz, and vice-versa. + var missing []string + var invalid []string + var expired []string + for _, ident := range idents { + authz, ok := authzs[ident] + if !ok || authz == nil { + missing = append(missing, ident.Value) + continue + } + if authz.Status != core.StatusValid { + invalid = append(invalid, ident.Value) + continue + } + if authz.Expires.Before(now) { + expired = append(expired, ident.Value) + continue + } + err = ra.PA.CheckAuthzChallenges(authz) + if err != nil { + invalid = append(invalid, ident.Value) + continue + } } - // Check the challenges themselves too. - for _, authz := range authzs { - err = ra.PA.CheckAuthz(authz) - if err != nil { - return nil, err - } + if len(missing) > 0 { + return nil, berrors.UnauthorizedError( + "authorizations for these identifiers not found: %s", + strings.Join(missing, ", "), + ) + } + + if len(invalid) > 0 { + return nil, berrors.UnauthorizedError( + "authorizations for these identifiers not valid: %s", + strings.Join(invalid, ", "), + ) + } + if len(expired) > 0 { + return nil, berrors.UnauthorizedError( + "authorizations for these identifiers expired: %s", + strings.Join(expired, ", "), + ) + } + + // Even though this check is cheap, we do it after the more specific checks + // so that we can return more specific error messages. + if len(idents) != len(authzs) { + return nil, berrors.UnauthorizedError("incorrect number of identifiers requested for finalization") + } + + // Check that the authzs either don't need CAA rechecking, or do the + // necessary CAA rechecks right now. + err = ra.checkAuthorizationsCAA(ctx, int64(acctID), authzs, now) + if err != nil { + return nil, err } return authzs, nil @@ -827,27 +777,23 @@ func (ra *RegistrationAuthorityImpl) checkOrderAuthorizations( func validatedBefore(authz *core.Authorization, caaRecheckTime time.Time) (bool, error) { numChallenges := len(authz.Challenges) if numChallenges != 1 { - return false, fmt.Errorf("authorization has incorrect number of challenges. 1 expected, %d found for: id %s", numChallenges, authz.ID) + return false, berrors.InternalServerError("authorization has incorrect number of challenges. 1 expected, %d found for: id %s", numChallenges, authz.ID) } if authz.Challenges[0].Validated == nil { - return false, fmt.Errorf("authorization's challenge has no validated timestamp for: id %s", authz.ID) + return false, berrors.InternalServerError("authorization's challenge has no validated timestamp for: id %s", authz.ID) } return authz.Challenges[0].Validated.Before(caaRecheckTime), nil } -// checkAuthorizationsCAA implements the common logic of validating a set of -// authorizations against a set of names that is used by both -// `checkAuthorizations` and `checkOrderAuthorizations`. If required CAA will be -// rechecked for authorizations that are too old. -// If it returns an error, it will be of type BoulderError. +// checkAuthorizationsCAA ensures that we have sufficiently-recent CAA checks +// for every input identifier/authz. If any authz was validated too long ago, it +// kicks off a CAA recheck for that identifier If it returns an error, it will +// be of type BoulderError. func (ra *RegistrationAuthorityImpl) checkAuthorizationsCAA( ctx context.Context, acctID int64, - names []string, - authzs map[string]*core.Authorization, + authzs map[identifier.ACMEIdentifier]*core.Authorization, now time.Time) error { - // badNames contains the names that were unauthorized - var badNames []string // recheckAuthzs is a list of authorizations that must have their CAA records rechecked var recheckAuthzs []*core.Authorization @@ -860,33 +806,18 @@ func (ra *RegistrationAuthorityImpl) checkAuthorizationsCAA( // Set the recheck time to 7 hours ago. caaRecheckAfter := now.Add(caaRecheckDuration) - // Set a CAA recheck time based on the assumption of a 30 day authz - // lifetime. This has been deprecated in favor of a new check based - // off the Validated time stored in the database, but we want to check - // both for a time and increment a stat if this code path is hit for - // compliance safety. - caaRecheckTime := now.Add(ra.authorizationLifetime).Add(caaRecheckDuration) - - for _, name := range names { - authz := authzs[name] - if authz == nil { - badNames = append(badNames, name) - } else if authz.Expires == nil { - return berrors.InternalServerError("found an authorization with a nil Expires field: id %s", authz.ID) - } else if authz.Expires.Before(now) { - badNames = append(badNames, name) - } else if staleCAA, err := validatedBefore(authz, caaRecheckAfter); err != nil { - return berrors.InternalServerError(err.Error()) + for _, authz := range authzs { + if staleCAA, err := validatedBefore(authz, caaRecheckAfter); err != nil { + return err } else if staleCAA { - // Ensure that CAA is rechecked for this name - recheckAuthzs = append(recheckAuthzs, authz) - } else if authz.Expires.Before(caaRecheckTime) { - // Ensure that CAA is rechecked for this name - recheckAuthzs = append(recheckAuthzs, authz) - // This codepath should not be used, but is here as a safety - // net until the new codepath is proven. Increment metric if - // it is used. - ra.recheckCAAUsedAuthzLifetime.Add(1) + switch authz.Identifier.Type { + case identifier.TypeDNS: + // Ensure that CAA is rechecked for this name + recheckAuthzs = append(recheckAuthzs, authz) + case identifier.TypeIP: + default: + return berrors.MalformedError("invalid identifier type: %s", authz.Identifier.Type) + } } } @@ -897,13 +828,6 @@ func (ra *RegistrationAuthorityImpl) checkAuthorizationsCAA( } } - if len(badNames) > 0 { - return berrors.UnauthorizedError( - "authorizations for these names not found or expired: %s", - strings.Join(badNames, ", "), - ) - } - caaEvent := &finalizationCAACheckEvent{ Requester: acctID, Reused: len(authzs) - len(recheckAuthzs), @@ -928,8 +852,6 @@ func (ra *RegistrationAuthorityImpl) recheckCAA(ctx context.Context, authzs []*c ch := make(chan authzCAAResult, len(authzs)) for _, authz := range authzs { go func(authz *core.Authorization) { - name := authz.Identifier.Value - // If an authorization has multiple valid challenges, // the type of the first valid challenge is used for // the purposes of CAA rechecking. @@ -945,13 +867,14 @@ func (ra *RegistrationAuthorityImpl) recheckCAA(ctx context.Context, authzs []*c authz: authz, err: berrors.InternalServerError( "Internal error determining validation method for authorization ID %v (%v)", - authz.ID, name), + authz.ID, authz.Identifier.Value), } return } - - resp, err := ra.caa.IsCAAValid(ctx, &vapb.IsCAAValidRequest{ - Domain: name, + var resp *vapb.IsCAAValidResponse + var err error + resp, err = ra.VA.DoCAA(ctx, &vapb.IsCAAValidRequest{ + Identifier: authz.Identifier.ToProto(), ValidationMethod: method, AccountURIID: authz.RegistrationID, }) @@ -959,10 +882,10 @@ func (ra *RegistrationAuthorityImpl) recheckCAA(ctx context.Context, authzs []*c ra.log.AuditErrf("Rechecking CAA: %s", err) err = berrors.InternalServerError( "Internal error rechecking CAA for authorization ID %v (%v)", - authz.ID, name, + authz.ID, authz.Identifier.Value, ) } else if resp.Problem != nil { - err = berrors.CAAError(resp.Problem.Detail) + err = berrors.CAAError("rechecking caa: %s", resp.Problem.Detail) } ch <- authzCAAResult{ authz: authz, @@ -1065,6 +988,7 @@ func (ra *RegistrationAuthorityImpl) FinalizeOrder(ctx context.Context, req *rap OrderID: req.Order.Id, Requester: req.Order.RegistrationID, RequestTime: ra.clk.Now(), + UserAgent: web.UserAgent(ctx), } csr, err := ra.validateFinalizeRequest(ctx, req, &logEvent) if err != nil { @@ -1110,15 +1034,19 @@ func (ra *RegistrationAuthorityImpl) FinalizeOrder(ctx context.Context, req *rap // // We track this goroutine's lifetime in a waitgroup global to this RA, so // that it can wait for all goroutines to drain during shutdown. - ra.finalizeWG.Add(1) + ra.drainWG.Add(1) go func() { + // The original context will be canceled in the RPC layer when FinalizeOrder returns, + // so split off a context that won't be canceled (and has its own timeout). + ctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), ra.finalizeTimeout) + defer cancel() _, err := ra.issueCertificateOuter(ctx, proto.Clone(order).(*corepb.Order), csr, logEvent) if err != nil { // We only log here, because this is in a background goroutine with // no parent goroutine waiting for it to receive the error. ra.log.AuditErrf("Asynchronous finalization failed: %s", err.Error()) } - ra.finalizeWG.Done() + ra.drainWG.Done() }() return order, nil } else { @@ -1126,6 +1054,26 @@ func (ra *RegistrationAuthorityImpl) FinalizeOrder(ctx context.Context, req *rap } } +// containsMustStaple returns true if the provided set of extensions includes +// an entry whose OID and value both match the expected values for the OCSP +// Must-Staple (a.k.a. id-pe-tlsFeature) extension. +func containsMustStaple(extensions []pkix.Extension) bool { + // RFC 7633: id-pe-tlsfeature OBJECT IDENTIFIER ::= { id-pe 24 } + var mustStapleExtId = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 24} + // ASN.1 encoding of: + // SEQUENCE + // INTEGER 5 + // where "5" is the status_request feature (RFC 6066) + var mustStapleExtValue = []byte{0x30, 0x03, 0x02, 0x01, 0x05} + + for _, ext := range extensions { + if ext.Id.Equal(mustStapleExtId) && bytes.Equal(ext.Value, mustStapleExtValue) { + return true + } + } + return false +} + // validateFinalizeRequest checks that a FinalizeOrder request is fully correct // and ready for issuance. func (ra *RegistrationAuthorityImpl) validateFinalizeRequest( @@ -1146,11 +1094,18 @@ func (ra *RegistrationAuthorityImpl) validateFinalizeRequest( req.Order.Status) } - // There should never be an order with 0 names at the stage, but we check to + profile, err := ra.profiles.get(req.Order.CertificateProfileName) + if err != nil { + return nil, err + } + + orderIdents := identifier.Normalize(identifier.FromProtoSlice(req.Order.Identifiers)) + + // There should never be an order with 0 identifiers at the stage, but we check to // be on the safe side, throwing an internal server error if this assumption // is ever violated. - if len(req.Order.Names) == 0 { - return nil, berrors.InternalServerError("Order has no associated names") + if len(orderIdents) == 0 { + return nil, berrors.InternalServerError("Order has no associated identifiers") } // Parse the CSR from the request @@ -1159,7 +1114,14 @@ func (ra *RegistrationAuthorityImpl) validateFinalizeRequest( return nil, berrors.BadCSRError("unable to parse CSR: %s", err.Error()) } - err = csrlib.VerifyCSR(ctx, csr, ra.maxNames, &ra.keyPolicy, ra.PA) + if containsMustStaple(csr.Extensions) { + ra.mustStapleRequestsCounter.WithLabelValues("denied").Inc() + return nil, berrors.UnauthorizedError( + "OCSP must-staple extension is no longer available: see https://letsencrypt.org/2024/12/05/ending-ocsp", + ) + } + + err = csrlib.VerifyCSR(ctx, csr, profile.maxNames, &ra.keyPolicy, ra.PA) if err != nil { // VerifyCSR returns berror instances that can be passed through as-is // without wrapping. @@ -1168,19 +1130,10 @@ func (ra *RegistrationAuthorityImpl) validateFinalizeRequest( // Dedupe, lowercase and sort both the names from the CSR and the names in the // order. - csrNames := csrlib.NamesFromCSR(csr).SANs - orderNames := core.UniqueLowerNames(req.Order.Names) - - // Immediately reject the request if the number of names differ - if len(orderNames) != len(csrNames) { - return nil, berrors.UnauthorizedError("Order includes different number of names than CSR specifies") - } - + csrIdents := identifier.FromCSR(csr) // Check that the order names and the CSR names are an exact match - for i, name := range orderNames { - if name != csrNames[i] { - return nil, berrors.UnauthorizedError("CSR is missing Order domain %q", name) - } + if !slices.Equal(csrIdents, orderIdents) { + return nil, berrors.UnauthorizedError("CSR does not specify same identifiers as Order") } // Get the originating account for use in the next check. @@ -1199,9 +1152,10 @@ func (ra *RegistrationAuthorityImpl) validateFinalizeRequest( return nil, berrors.MalformedError("certificate public key must be different than account key") } - // Double-check that all authorizations on this order are also associated with - // the same account as the order itself. - authzs, err := ra.checkOrderAuthorizations(ctx, csrNames, accountID(req.Order.RegistrationID), orderID(req.Order.Id)) + // Double-check that all authorizations on this order are valid, are also + // associated with the same account as the order itself, and have recent CAA. + authzs, err := ra.checkOrderAuthorizations( + ctx, orderID(req.Order.Id), accountID(req.Order.RegistrationID), csrIdents, ra.clk.Now()) if err != nil { // Pass through the error without wrapping it because the called functions // return BoulderError and we don't want to lose the type. @@ -1210,16 +1164,16 @@ func (ra *RegistrationAuthorityImpl) validateFinalizeRequest( // Collect up a certificateRequestAuthz that stores the ID and challenge type // of each of the valid authorizations we used for this issuance. - logEventAuthzs := make(map[string]certificateRequestAuthz, len(csrNames)) - for name, authz := range authzs { + logEventAuthzs := make(map[string]certificateRequestAuthz, len(csrIdents)) + for _, authz := range authzs { // No need to check for error here because we know this same call just // succeeded inside ra.checkOrderAuthorizations solvedByChallengeType, _ := authz.SolvedBy() - logEventAuthzs[name] = certificateRequestAuthz{ + logEventAuthzs[authz.Identifier.Value] = certificateRequestAuthz{ ID: authz.ID, ChallengeType: solvedByChallengeType, } - authzAge := (ra.authorizationLifetime - authz.Expires.Sub(ra.clk.Now())).Seconds() + authzAge := (profile.validAuthzLifetime - authz.Expires.Sub(ra.clk.Now())).Seconds() ra.authzAges.WithLabelValues("FinalizeOrder", string(authz.Status)).Observe(authzAge) } logEvent.Authorizations = logEventAuthzs @@ -1230,6 +1184,16 @@ func (ra *RegistrationAuthorityImpl) validateFinalizeRequest( return csr, nil } +func (ra *RegistrationAuthorityImpl) GetSCTs(ctx context.Context, sctRequest *rapb.SCTRequest) (*rapb.SCTResponse, error) { + scts, err := ra.getSCTs(ctx, sctRequest.PrecertDER) + if err != nil { + return nil, err + } + return &rapb.SCTResponse{ + SctDER: scts, + }, nil +} + // issueCertificateOuter exists solely to ensure that all calls to // issueCertificateInner have their result handled uniformly, no matter what // return path that inner function takes. It takes ownership of the logEvent, @@ -1243,9 +1207,30 @@ func (ra *RegistrationAuthorityImpl) issueCertificateOuter( ra.inflightFinalizes.Inc() defer ra.inflightFinalizes.Dec() + idents := identifier.FromProtoSlice(order.Identifiers) + + isRenewal := false + timestamps, err := ra.SA.FQDNSetTimestampsForWindow(ctx, &sapb.CountFQDNSetsRequest{ + Identifiers: idents.ToProtoSlice(), + Window: durationpb.New(120 * 24 * time.Hour), + Limit: 1, + }) + if err != nil { + return nil, fmt.Errorf("checking if certificate is a renewal: %w", err) + } + if len(timestamps.Timestamps) > 0 { + isRenewal = true + logEvent.PreviousCertificateIssued = timestamps.Timestamps[0].AsTime() + } + + profileName := order.CertificateProfileName + if profileName == "" { + profileName = ra.profiles.defaultName + } + // Step 3: Issue the Certificate - cert, cpId, err := ra.issueCertificateInner( - ctx, csr, order.CertificateProfileName, accountID(order.RegistrationID), orderID(order.Id)) + cert, err := ra.issueCertificateInner( + ctx, csr, isRenewal, profileName, accountID(order.RegistrationID), orderID(order.Id)) // Step 4: Fail the order if necessary, and update metrics and log fields var result string @@ -1267,21 +1252,16 @@ func (ra *RegistrationAuthorityImpl) issueCertificateOuter( ra.namesPerCert.With( prometheus.Labels{"type": "issued"}, - ).Observe(float64(len(order.Names))) + ).Observe(float64(len(idents))) - ra.newCertCounter.With( - prometheus.Labels{ - "profileName": cpId.name, - "profileHash": hex.EncodeToString(cpId.hash), - }).Inc() + ra.newCertCounter.Inc() logEvent.SerialNumber = core.SerialToString(cert.SerialNumber) logEvent.CommonName = cert.Subject.CommonName - logEvent.Names = cert.DNSNames + logEvent.Identifiers = identifier.FromCert(cert) logEvent.NotBefore = cert.NotBefore logEvent.NotAfter = cert.NotAfter - logEvent.CertProfileName = cpId.name - logEvent.CertProfileHash = hex.EncodeToString(cpId.hash) + logEvent.CertProfileName = profileName result = "successful" } @@ -1292,11 +1272,33 @@ func (ra *RegistrationAuthorityImpl) issueCertificateOuter( return order, err } -// certProfileID contains the name and hash of a certificate profile returned by -// a CA. -type certProfileID struct { - name string - hash []byte +// countCertificateIssued increments the certificates (per domain and per +// account) and duplicate certificate rate limits. There is no reason to surface +// errors from this function to the Subscriber, spends against these limit are +// best effort. +func (ra *RegistrationAuthorityImpl) countCertificateIssued(ctx context.Context, regId int64, orderIdents identifier.ACMEIdentifiers, isRenewal bool) { + var transactions []ratelimits.Transaction + if !isRenewal { + txns, err := ra.txnBuilder.CertificatesPerDomainSpendOnlyTransactions(regId, orderIdents) + if err != nil { + ra.log.Warningf("building rate limit transactions at finalize: %s", err) + } + transactions = append(transactions, txns...) + } + + txn, err := ra.txnBuilder.CertificatesPerFQDNSetSpendOnlyTransaction(orderIdents) + if err != nil { + ra.log.Warningf("building rate limit transaction at finalize: %s", err) + } + transactions = append(transactions, txn) + + _, err = ra.limiter.BatchSpend(ctx, transactions) + if err != nil { + if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + return + } + ra.log.Warningf("spending against rate limits at finalize: %s", err) + } } // issueCertificateInner is part of the [issuance cycle]. @@ -1319,16 +1321,10 @@ type certProfileID struct { func (ra *RegistrationAuthorityImpl) issueCertificateInner( ctx context.Context, csr *x509.CertificateRequest, + isRenewal bool, profileName string, acctID accountID, - oID orderID) (*x509.Certificate, *certProfileID, error) { - if features.Get().AsyncFinalize { - // If we're in async mode, use a context with a much longer timeout. - var cancel func() - ctx, cancel = context.WithTimeout(context.WithoutCancel(ctx), ra.finalizeTimeout) - defer cancel() - } - + oID orderID) (*x509.Certificate, error) { // wrapError adds a prefix to an error. If the error is a boulder error then // the problem detail is updated with the prefix. Otherwise a new error is // returned with the message prefixed using `fmt.Errorf` @@ -1346,47 +1342,26 @@ func (ra *RegistrationAuthorityImpl) issueCertificateInner( OrderID: int64(oID), CertProfileName: profileName, } - // Once we get a precert from IssuePrecertificate, we must attempt issuing - // a final certificate at most once. We achieve that by bailing on any error - // between here and IssueCertificateForPrecertificate. - precert, err := ra.CA.IssuePrecertificate(ctx, issueReq) + + resp, err := ra.CA.IssueCertificate(ctx, issueReq) if err != nil { - return nil, nil, wrapError(err, "issuing precertificate") + return nil, err } - parsedPrecert, err := x509.ParseCertificate(precert.DER) + parsedCertificate, err := x509.ParseCertificate(resp.DER) if err != nil { - return nil, nil, wrapError(err, "parsing precertificate") + return nil, wrapError(err, "parsing final certificate") } - scts, err := ra.getSCTs(ctx, precert.DER, parsedPrecert.NotAfter) - if err != nil { - return nil, nil, wrapError(err, "getting SCTs") - } - - cert, err := ra.CA.IssueCertificateForPrecertificate(ctx, &capb.IssueCertificateForPrecertificateRequest{ - DER: precert.DER, - SCTs: scts, - RegistrationID: int64(acctID), - OrderID: int64(oID), - CertProfileHash: precert.CertProfileHash, - }) - if err != nil { - return nil, nil, wrapError(err, "issuing certificate for precertificate") - } - - parsedCertificate, err := x509.ParseCertificate(cert.Der) - if err != nil { - return nil, nil, wrapError(err, "parsing final certificate") - } + ra.countCertificateIssued(ctx, int64(acctID), identifier.FromCert(parsedCertificate), isRenewal) // Asynchronously submit the final certificate to any configured logs - go ra.ctpolicy.SubmitFinalCert(cert.Der, parsedCertificate.NotAfter) + go ra.ctpolicy.SubmitFinalCert(resp.DER, parsedCertificate.NotAfter) err = ra.matchesCSR(parsedCertificate, csr) if err != nil { ra.certCSRMismatch.Inc() - return nil, nil, err + return nil, err } _, err = ra.SA.FinalizeOrder(ctx, &sapb.FinalizeOrderRequest{ @@ -1394,26 +1369,28 @@ func (ra *RegistrationAuthorityImpl) issueCertificateInner( CertificateSerial: core.SerialToString(parsedCertificate.SerialNumber), }) if err != nil { - return nil, nil, wrapError(err, "persisting finalized order") + return nil, wrapError(err, "persisting finalized order") } - return parsedCertificate, &certProfileID{name: precert.CertProfileName, hash: precert.CertProfileHash}, nil + return parsedCertificate, nil } -func (ra *RegistrationAuthorityImpl) getSCTs(ctx context.Context, cert []byte, expiration time.Time) (core.SCTDERs, error) { +func (ra *RegistrationAuthorityImpl) getSCTs(ctx context.Context, precertDER []byte) (core.SCTDERs, error) { started := ra.clk.Now() - scts, err := ra.ctpolicy.GetSCTs(ctx, cert, expiration) + precert, err := x509.ParseCertificate(precertDER) + if err != nil { + return nil, fmt.Errorf("parsing precertificate: %w", err) + } + + scts, err := ra.ctpolicy.GetSCTs(ctx, precertDER, precert.NotAfter) took := ra.clk.Since(started) - // The final cert has already been issued so actually return it to the - // user even if this fails since we aren't actually doing anything with - // the SCTs yet. if err != nil { state := "failure" if err == context.DeadlineExceeded { state = "deadlineExceeded" // Convert the error to a missingSCTsError to communicate the timeout, // otherwise it will be a generic serverInternalError - err = berrors.MissingSCTsError(err.Error()) + err = berrors.MissingSCTsError("failed to get SCTs: %s", err.Error()) } ra.log.Warningf("ctpolicy.GetSCTs failed: %s", err) ra.ctpolicyResults.With(prometheus.Labels{"result": state}).Observe(took.Seconds()) @@ -1423,369 +1400,63 @@ func (ra *RegistrationAuthorityImpl) getSCTs(ctx context.Context, cert []byte, e return scts, nil } -// enforceNameCounts uses the provided count RPC to find a count of certificates -// for each of the names. If the count for any of the names exceeds the limit -// for the given registration then the names out of policy are returned to be -// used for a rate limit error. -func (ra *RegistrationAuthorityImpl) enforceNameCounts(ctx context.Context, names []string, limit ratelimit.RateLimitPolicy, regID int64) ([]string, time.Time, error) { - now := ra.clk.Now() - req := &sapb.CountCertificatesByNamesRequest{ - Names: names, - Range: &sapb.Range{ - Earliest: timestamppb.New(limit.WindowBegin(now)), - Latest: timestamppb.New(now), - }, - } - - response, err := ra.SA.CountCertificatesByNames(ctx, req) - if err != nil { - return nil, time.Time{}, err - } - - if len(response.Counts) == 0 { - return nil, time.Time{}, errIncompleteGRPCResponse - } - - var badNames []string - var metricsData []struct { - overrideKey string - utilization float64 - } - - // Find the names that have counts at or over the threshold. Range - // over the names slice input to ensure the order of badNames will - // return the badNames in the same order they were input. - for _, name := range names { - threshold, overrideKey := limit.GetThreshold(name, regID) - if response.Counts[name] >= threshold { - badNames = append(badNames, name) - } - if overrideKey != "" { - // Name is under threshold due to an override. - utilization := float64(response.Counts[name]+1) / float64(threshold) - metricsData = append(metricsData, struct { - overrideKey string - utilization float64 - }{overrideKey, utilization}) - } - } - - if len(badNames) == 0 { - // All names were under the threshold, emit override utilization metrics. - for _, data := range metricsData { - ra.rlOverrideUsageGauge.WithLabelValues(ratelimit.CertificatesPerName, data.overrideKey).Set(data.utilization) - } - } - return badNames, response.Earliest.AsTime(), nil -} - -func (ra *RegistrationAuthorityImpl) checkCertificatesPerNameLimit(ctx context.Context, names []string, limit ratelimit.RateLimitPolicy, regID int64) error { - // check if there is already an existing certificate for - // the exact name set we are issuing for. If so bypass the - // the certificatesPerName limit. - exists, err := ra.SA.FQDNSetExists(ctx, &sapb.FQDNSetExistsRequest{Domains: names}) - if err != nil { - return fmt.Errorf("checking renewal exemption for %q: %s", names, err) - } - if exists.Exists { - return nil - } - - tldNames := ratelimits.DomainsForRateLimiting(names) - namesOutOfLimit, earliest, err := ra.enforceNameCounts(ctx, tldNames, limit, regID) - if err != nil { - return fmt.Errorf("checking certificates per name limit for %q: %s", - names, err) - } - - if len(namesOutOfLimit) > 0 { - // Determine the amount of time until the earliest event would fall out - // of the window. - retryAfter := earliest.Add(limit.Window.Duration).Sub(ra.clk.Now()) - retryString := earliest.Add(limit.Window.Duration).Format(time.RFC3339) - - ra.log.Infof("Rate limit exceeded, CertificatesForDomain, regID: %d, domains: %s", regID, strings.Join(namesOutOfLimit, ", ")) - if len(namesOutOfLimit) > 1 { - var subErrors []berrors.SubBoulderError - for _, name := range namesOutOfLimit { - subErrors = append(subErrors, berrors.SubBoulderError{ - Identifier: identifier.DNSIdentifier(name), - BoulderError: berrors.RateLimitError(retryAfter, "too many certificates already issued. Retry after %s", retryString).(*berrors.BoulderError), - }) - } - return berrors.RateLimitError(retryAfter, "too many certificates already issued for multiple names (%q and %d others). Retry after %s", namesOutOfLimit[0], len(namesOutOfLimit), retryString).(*berrors.BoulderError).WithSubErrors(subErrors) - } - return berrors.RateLimitError(retryAfter, "too many certificates already issued for %q. Retry after %s", namesOutOfLimit[0], retryString) - } - - return nil -} - -func (ra *RegistrationAuthorityImpl) checkCertificatesPerFQDNSetLimit(ctx context.Context, names []string, limit ratelimit.RateLimitPolicy, regID int64) error { - names = core.UniqueLowerNames(names) - threshold, overrideKey := limit.GetThreshold(strings.Join(names, ","), regID) - if threshold <= 0 { - // No limit configured. - return nil - } - - prevIssuances, err := ra.SA.FQDNSetTimestampsForWindow(ctx, &sapb.CountFQDNSetsRequest{ - Domains: names, - Window: durationpb.New(limit.Window.Duration), - }) - if err != nil { - return fmt.Errorf("checking duplicate certificate limit for %q: %s", names, err) - } - - if overrideKey != "" { - utilization := float64(len(prevIssuances.Timestamps)) / float64(threshold) - ra.rlOverrideUsageGauge.WithLabelValues(ratelimit.CertificatesPerFQDNSet, overrideKey).Set(utilization) - } - - issuanceCount := int64(len(prevIssuances.Timestamps)) - if issuanceCount < threshold { - // Issuance in window is below the threshold, no need to limit. - if overrideKey != "" { - utilization := float64(issuanceCount+1) / float64(threshold) - ra.rlOverrideUsageGauge.WithLabelValues(ratelimit.CertificatesPerFQDNSet, overrideKey).Set(utilization) - } - return nil - } else { - // Evaluate the rate limit using a leaky bucket algorithm. The bucket - // has a capacity of threshold and is refilled at a rate of 1 token per - // limit.Window/threshold from the time of each issuance timestamp. The - // timestamps start from the most recent issuance and go back in time. - now := ra.clk.Now() - nsPerToken := limit.Window.Nanoseconds() / threshold - for i, timestamp := range prevIssuances.Timestamps { - tokensGeneratedSince := now.Add(-time.Duration(int64(i+1) * nsPerToken)) - if timestamp.AsTime().Before(tokensGeneratedSince) { - // We know `i+1` tokens were generated since `tokenGeneratedSince`, - // and only `i` certificates were issued, so there's room to allow - // for an additional issuance. - if overrideKey != "" { - utilization := float64(issuanceCount) / float64(threshold) - ra.rlOverrideUsageGauge.WithLabelValues(ratelimit.CertificatesPerFQDNSet, overrideKey).Set(utilization) - } - return nil - } - } - retryTime := prevIssuances.Timestamps[0].AsTime().Add(time.Duration(nsPerToken)) - retryAfter := retryTime.Sub(now) - return berrors.DuplicateCertificateError( - retryAfter, - "too many certificates (%d) already issued for this exact set of domains in the last %.0f hours: %s, retry after %s", - threshold, limit.Window.Duration.Hours(), strings.Join(names, ","), retryTime.Format(time.RFC3339), - ) - } -} - -func (ra *RegistrationAuthorityImpl) checkNewOrderLimits(ctx context.Context, names []string, regID int64) error { - newOrdersPerAccountLimits := ra.rlPolicies.NewOrdersPerAccount() - if newOrdersPerAccountLimits.Enabled() { - started := ra.clk.Now() - err := ra.checkNewOrdersPerAccountLimit(ctx, regID, names, newOrdersPerAccountLimits) - elapsed := ra.clk.Since(started) - if err != nil { - if errors.Is(err, berrors.RateLimit) { - ra.rlCheckLatency.WithLabelValues(ratelimit.NewOrdersPerAccount, ratelimits.Denied).Observe(elapsed.Seconds()) - } - return err - } - ra.rlCheckLatency.WithLabelValues(ratelimit.NewOrdersPerAccount, ratelimits.Allowed).Observe(elapsed.Seconds()) - } - - certNameLimits := ra.rlPolicies.CertificatesPerName() - if certNameLimits.Enabled() { - started := ra.clk.Now() - err := ra.checkCertificatesPerNameLimit(ctx, names, certNameLimits, regID) - elapsed := ra.clk.Since(started) - if err != nil { - if errors.Is(err, berrors.RateLimit) { - ra.rlCheckLatency.WithLabelValues(ratelimit.CertificatesPerName, ratelimits.Denied).Observe(elapsed.Seconds()) - } - return err - } - ra.rlCheckLatency.WithLabelValues(ratelimit.CertificatesPerName, ratelimits.Allowed).Observe(elapsed.Seconds()) - } - - fqdnLimitsFast := ra.rlPolicies.CertificatesPerFQDNSetFast() - if fqdnLimitsFast.Enabled() { - started := ra.clk.Now() - err := ra.checkCertificatesPerFQDNSetLimit(ctx, names, fqdnLimitsFast, regID) - elapsed := ra.clk.Since(started) - if err != nil { - if errors.Is(err, berrors.RateLimit) { - ra.rlCheckLatency.WithLabelValues(ratelimit.CertificatesPerFQDNSetFast, ratelimits.Denied).Observe(elapsed.Seconds()) - } - return err - } - ra.rlCheckLatency.WithLabelValues(ratelimit.CertificatesPerFQDNSetFast, ratelimits.Allowed).Observe(elapsed.Seconds()) - } - - fqdnLimits := ra.rlPolicies.CertificatesPerFQDNSet() - if fqdnLimits.Enabled() { - started := ra.clk.Now() - err := ra.checkCertificatesPerFQDNSetLimit(ctx, names, fqdnLimits, regID) - elapsed := ra.clk.Since(started) - if err != nil { - if errors.Is(err, berrors.RateLimit) { - ra.rlCheckLatency.WithLabelValues(ratelimit.CertificatesPerFQDNSet, ratelimits.Denied).Observe(elapsed.Seconds()) - } - return err - } - ra.rlCheckLatency.WithLabelValues(ratelimit.CertificatesPerFQDNSet, ratelimits.Allowed).Observe(elapsed.Seconds()) - } - - invalidAuthzPerAccountLimits := ra.rlPolicies.InvalidAuthorizationsPerAccount() - if invalidAuthzPerAccountLimits.Enabled() { - started := ra.clk.Now() - err := ra.checkInvalidAuthorizationLimits(ctx, regID, names, invalidAuthzPerAccountLimits) - elapsed := ra.clk.Since(started) - if err != nil { - if errors.Is(err, berrors.RateLimit) { - ra.rlCheckLatency.WithLabelValues(ratelimit.InvalidAuthorizationsPerAccount, ratelimits.Denied).Observe(elapsed.Seconds()) - } - return err - } - ra.rlCheckLatency.WithLabelValues(ratelimit.InvalidAuthorizationsPerAccount, ratelimits.Allowed).Observe(elapsed.Seconds()) - } - - return nil -} - -// UpdateRegistration updates an existing Registration with new values. Caller -// is responsible for making sure that update.Key is only different from base.Key -// if it is being called from the WFE key change endpoint. -// TODO(#5554): Split this into separate methods for updating Contacts vs Key. -func (ra *RegistrationAuthorityImpl) UpdateRegistration(ctx context.Context, req *rapb.UpdateRegistrationRequest) (*corepb.Registration, error) { - // Error if the request is nil, there is no account key or IP address - if req.Base == nil || len(req.Base.Key) == 0 || len(req.Base.InitialIP) == 0 || req.Base.Id == 0 { +// UpdateRegistrationContact updates an existing Registration's contact. The +// updated contacts field may be empty. +// +// Deprecated: This method has no callers. See +// https://github.com/letsencrypt/boulder/issues/8199 for removal. +func (ra *RegistrationAuthorityImpl) UpdateRegistrationContact(ctx context.Context, req *rapb.UpdateRegistrationContactRequest) (*corepb.Registration, error) { + if core.IsAnyNilOrZero(req.RegistrationID) { return nil, errIncompleteGRPCRequest } - err := validateContactsPresent(req.Base.Contact, req.Base.ContactsPresent) + err := ra.validateContacts(req.Contacts) if err != nil { - return nil, err - } - err = validateContactsPresent(req.Update.Contact, req.Update.ContactsPresent) - if err != nil { - return nil, err - } - err = ra.validateContacts(req.Update.Contact) - if err != nil { - return nil, err + return nil, fmt.Errorf("invalid contact: %w", err) } - update, changed := mergeUpdate(req.Base, req.Update) - if !changed { - // If merging the update didn't actually change the base then our work is - // done, we can return before calling ra.SA.UpdateRegistration since there's - // nothing for the SA to do - return req.Base, nil + update, err := ra.SA.UpdateRegistrationContact(ctx, &sapb.UpdateRegistrationContactRequest{ + RegistrationID: req.RegistrationID, + Contacts: req.Contacts, + }) + if err != nil { + return nil, fmt.Errorf("failed to update registration contact: %w", err) } - _, err = ra.SA.UpdateRegistration(ctx, update) - if err != nil { - // berrors.InternalServerError since the user-data was validated before being - // passed to the SA. - err = berrors.InternalServerError("Could not update registration: %s", err) - return nil, err + // TODO(#7966): Remove once the rate of registrations with contacts has + // been determined. + for range req.Contacts { + ra.newOrUpdatedContactCounter.With(prometheus.Labels{"new": "false"}).Inc() } return update, nil } -func contactsEqual(a []string, b []string) bool { - if len(a) != len(b) { - return false +// UpdateRegistrationKey updates an existing Registration's key. +func (ra *RegistrationAuthorityImpl) UpdateRegistrationKey(ctx context.Context, req *rapb.UpdateRegistrationKeyRequest) (*corepb.Registration, error) { + if core.IsAnyNilOrZero(req.RegistrationID, req.Jwk) { + return nil, errIncompleteGRPCRequest } - // If there is an existing contact slice and it has the same length as the - // new contact slice we need to look at each contact to determine if there - // is a change being made. Use `sort.Strings` here to ensure a consistent - // comparison - sort.Strings(a) - sort.Strings(b) - for i := range len(b) { - // If the contact's string representation differs at any index they aren't - // equal - if a[i] != b[i] { - return false - } + update, err := ra.SA.UpdateRegistrationKey(ctx, &sapb.UpdateRegistrationKeyRequest{ + RegistrationID: req.RegistrationID, + Jwk: req.Jwk, + }) + if err != nil { + return nil, fmt.Errorf("failed to update registration key: %w", err) } - // They are equal! - return true -} - -// MergeUpdate returns a new corepb.Registration with the majority of its fields -// copies from the base Registration, and a subset (Contact, Agreement, and Key) -// copied from the update Registration. It also returns a boolean indicating -// whether or not this operation resulted in a Registration which differs from -// the base. -func mergeUpdate(base *corepb.Registration, update *corepb.Registration) (*corepb.Registration, bool) { - var changed bool - - // Start by copying all of the fields. - res := &corepb.Registration{ - Id: base.Id, - Key: base.Key, - Contact: base.Contact, - ContactsPresent: base.ContactsPresent, - Agreement: base.Agreement, - InitialIP: base.InitialIP, - CreatedAt: base.CreatedAt, - Status: base.Status, - } - - // Note: we allow update.Contact to overwrite base.Contact even if the former - // is empty in order to allow users to remove the contact associated with - // a registration. If the update has ContactsPresent set to false, then we - // know it is not attempting to update the contacts field. - if update.ContactsPresent && !contactsEqual(base.Contact, update.Contact) { - res.Contact = update.Contact - res.ContactsPresent = update.ContactsPresent - changed = true - } - - if len(update.Agreement) > 0 && update.Agreement != base.Agreement { - res.Agreement = update.Agreement - changed = true - } - - if len(update.Key) > 0 { - if len(update.Key) != len(base.Key) { - res.Key = update.Key - changed = true - } else { - for i := range len(base.Key) { - if update.Key[i] != base.Key[i] { - res.Key = update.Key - changed = true - break - } - } - } - } - - return res, changed + return update, nil } // recordValidation records an authorization validation event, // it should only be used on v2 style authorizations. -func (ra *RegistrationAuthorityImpl) recordValidation(ctx context.Context, authID string, authExpires *time.Time, challenge *core.Challenge) error { +func (ra *RegistrationAuthorityImpl) recordValidation(ctx context.Context, authID string, authExpires time.Time, challenge *core.Challenge) error { authzID, err := strconv.ParseInt(authID, 10, 64) if err != nil { return err } - var expires time.Time - if challenge.Status == core.StatusInvalid { - expires = *authExpires - } else { - expires = ra.clk.Now().Add(ra.authorizationLifetime) - } - vr, err := bgrpc.ValidationResultToPB(challenge.ValidationRecord, challenge.Error) + vr, err := bgrpc.ValidationResultToPB(challenge.ValidationRecord, challenge.Error, "", "") if err != nil { return err } @@ -1796,32 +1467,90 @@ func (ra *RegistrationAuthorityImpl) recordValidation(ctx context.Context, authI _, err = ra.SA.FinalizeAuthorization2(ctx, &sapb.FinalizeAuthorizationRequest{ Id: authzID, Status: string(challenge.Status), - Expires: timestamppb.New(expires), + Expires: timestamppb.New(authExpires), Attempted: string(challenge.Type), AttemptedAt: validated, ValidationRecords: vr.Records, - ValidationError: vr.Problems, + ValidationError: vr.Problem, }) return err } -func (ra *RegistrationAuthorityImpl) countFailedValidation(ctx context.Context, regId int64, name string) { - if ra.limiter == nil || ra.txnBuilder == nil { - // Limiter is disabled. - return - } - - txn, err := ra.txnBuilder.FailedAuthorizationsPerDomainPerAccountSpendOnlyTransaction(regId, name) +// countFailedValidations increments the FailedAuthorizationsPerDomainPerAccount limit. +// and the FailedAuthorizationsForPausingPerDomainPerAccountTransaction limit. +func (ra *RegistrationAuthorityImpl) countFailedValidations(ctx context.Context, regId int64, ident identifier.ACMEIdentifier) error { + txn, err := ra.txnBuilder.FailedAuthorizationsPerDomainPerAccountSpendOnlyTransaction(regId, ident) if err != nil { - ra.log.Errf("constructing rate limit transaction for the %s rate limit: %s", ratelimits.FailedAuthorizationsPerDomainPerAccount, err) + return fmt.Errorf("building rate limit transaction for the %s rate limit: %w", ratelimits.FailedAuthorizationsPerDomainPerAccount, err) } _, err = ra.limiter.Spend(ctx, txn) if err != nil { - if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { - return + return fmt.Errorf("spending against the %s rate limit: %w", ratelimits.FailedAuthorizationsPerDomainPerAccount, err) + } + + if features.Get().AutomaticallyPauseZombieClients { + txn, err = ra.txnBuilder.FailedAuthorizationsForPausingPerDomainPerAccountTransaction(regId, ident) + if err != nil { + return fmt.Errorf("building rate limit transaction for the %s rate limit: %w", ratelimits.FailedAuthorizationsForPausingPerDomainPerAccount, err) } - ra.log.Errf("checking the %s rate limit: %s", ratelimits.FailedAuthorizationsPerDomainPerAccount, err) + + decision, err := ra.limiter.Spend(ctx, txn) + if err != nil { + return fmt.Errorf("spending against the %s rate limit: %s", ratelimits.FailedAuthorizationsForPausingPerDomainPerAccount, err) + } + + if decision.Result(ra.clk.Now()) != nil { + resp, err := ra.SA.PauseIdentifiers(ctx, &sapb.PauseRequest{ + RegistrationID: regId, + Identifiers: []*corepb.Identifier{ident.ToProto()}, + }) + if err != nil { + return fmt.Errorf("failed to pause %d/%q: %w", regId, ident.Value, err) + } + ra.pauseCounter.With(prometheus.Labels{ + "paused": strconv.FormatBool(resp.Paused > 0), + "repaused": strconv.FormatBool(resp.Repaused > 0), + "grace": strconv.FormatBool(resp.Paused <= 0 && resp.Repaused <= 0), + }).Inc() + } + } + return nil +} + +// resetAccountPausingLimit resets bucket to maximum capacity for given account. +// There is no reason to surface errors from this function to the Subscriber. +func (ra *RegistrationAuthorityImpl) resetAccountPausingLimit(ctx context.Context, regId int64, ident identifier.ACMEIdentifier) { + bucketKey := ratelimits.NewRegIdIdentValueBucketKey(ratelimits.FailedAuthorizationsForPausingPerDomainPerAccount, regId, ident.Value) + err := ra.limiter.Reset(ctx, bucketKey) + if err != nil { + ra.log.Warningf("resetting bucket for regID=[%d] identifier=[%s]: %s", regId, ident.Value, err) + } +} + +// doDCVAndCAA performs DCV and CAA checks sequentially: DCV is performed first +// and CAA is only checked if DCV is successful. Validation records from the DCV +// check are returned even if the CAA check fails. +func (ra *RegistrationAuthorityImpl) checkDCVAndCAA(ctx context.Context, dcvReq *vapb.PerformValidationRequest, caaReq *vapb.IsCAAValidRequest) (*corepb.ProblemDetails, []*corepb.ValidationRecord, error) { + doDCVRes, err := ra.VA.DoDCV(ctx, dcvReq) + if err != nil { + return nil, nil, err + } + if doDCVRes.Problem != nil { + return doDCVRes.Problem, doDCVRes.Records, nil + } + + switch identifier.FromProto(dcvReq.Identifier).Type { + case identifier.TypeDNS: + doCAAResp, err := ra.VA.DoCAA(ctx, caaReq) + if err != nil { + return nil, nil, err + } + return doCAAResp.Problem, doDCVRes.Records, nil + case identifier.TypeIP: + return nil, doDCVRes.Records, nil + default: + return nil, nil, berrors.MalformedError("invalid identifier type: %s", dcvReq.Identifier.Type) } } @@ -1835,8 +1564,7 @@ func (ra *RegistrationAuthorityImpl) PerformValidation( // Clock for start of PerformValidation. vStart := ra.clk.Now() - // TODO(#7153): Check each value via core.IsAnyNilOrZero - if req.Authz == nil || req.Authz.Id == "" || req.Authz.Identifier == "" || req.Authz.Status == "" || core.IsAnyNilOrZero(req.Authz.Expires) { + if core.IsAnyNilOrZero(req.Authz, req.Authz.Id, req.Authz.Identifier, req.Authz.Status, req.Authz.Expires) { return nil, errIncompleteGRPCRequest } @@ -1850,6 +1578,11 @@ func (ra *RegistrationAuthorityImpl) PerformValidation( return nil, berrors.MalformedError("expired authorization") } + profile, err := ra.profiles.get(authz.CertificateProfileName) + if err != nil { + return nil, err + } + challIndex := int(req.ChallengeIndex) if challIndex >= len(authz.Challenges) { return nil, @@ -1878,11 +1611,11 @@ func (ra *RegistrationAuthorityImpl) PerformValidation( // Look up the account key for this authorization regPB, err := ra.SA.GetRegistration(ctx, &sapb.RegistrationID{Id: authz.RegistrationID}) if err != nil { - return nil, berrors.InternalServerError(err.Error()) + return nil, berrors.InternalServerError("getting acct for authorization: %s", err.Error()) } reg, err := bgrpc.PbToRegistration(regPB) if err != nil { - return nil, berrors.InternalServerError(err.Error()) + return nil, berrors.InternalServerError("getting acct for authorization: %s", err.Error()) } // Compute the key authorization field based on the registration key @@ -1891,16 +1624,17 @@ func (ra *RegistrationAuthorityImpl) PerformValidation( return nil, berrors.InternalServerError("could not compute expected key authorization value") } - ch.ProvidedKeyAuthorization = expectedKeyAuthorization - // Double check before sending to VA if cErr := ch.CheckPending(); cErr != nil { - return nil, berrors.MalformedError(cErr.Error()) + return nil, berrors.MalformedError("cannot validate challenge: %s", cErr.Error()) } // Dispatch to the VA for service + ra.drainWG.Add(1) vaCtx := context.Background() go func(authz core.Authorization) { + defer ra.drainWG.Done() + // We will mutate challenges later in this goroutine to change status and // add error, but we also return a copy of authz immediately. To avoid a // data race, make a copy of the challenges slice here for mutation. @@ -1908,32 +1642,37 @@ func (ra *RegistrationAuthorityImpl) PerformValidation( copy(challenges, authz.Challenges) authz.Challenges = challenges chall, _ := bgrpc.ChallengeToPB(authz.Challenges[challIndex]) - req := vapb.PerformValidationRequest{ - Domain: authz.Identifier.Value, - Challenge: chall, - Authz: &vapb.AuthzMeta{ - Id: authz.ID, - RegID: authz.RegistrationID, + checkProb, checkRecords, err := ra.checkDCVAndCAA( + vaCtx, + &vapb.PerformValidationRequest{ + Identifier: authz.Identifier.ToProto(), + Challenge: chall, + Authz: &vapb.AuthzMeta{Id: authz.ID, RegID: authz.RegistrationID}, + ExpectedKeyAuthorization: expectedKeyAuthorization, }, - ExpectedKeyAuthorization: expectedKeyAuthorization, - } - res, err := ra.VA.PerformValidation(vaCtx, &req) + &vapb.IsCAAValidRequest{ + Identifier: authz.Identifier.ToProto(), + ValidationMethod: chall.Type, + AccountURIID: authz.RegistrationID, + AuthzID: authz.ID, + }, + ) challenge := &authz.Challenges[challIndex] var prob *probs.ProblemDetails if err != nil { prob = probs.ServerInternal("Could not communicate with VA") ra.log.AuditErrf("Could not communicate with VA: %s", err) } else { - if res.Problems != nil { - prob, err = bgrpc.PBToProblemDetails(res.Problems) + if checkProb != nil { + prob, err = bgrpc.PBToProblemDetails(checkProb) if err != nil { prob = probs.ServerInternal("Could not communicate with VA") ra.log.AuditErrf("Could not communicate with VA: %s", err) } } // Save the updated records - records := make([]core.ValidationRecord, len(res.Records)) - for i, r := range res.Records { + records := make([]core.ValidationRecord, len(checkRecords)) + for i, r := range checkRecords { records[i], err = bgrpc.PBToValidationRecord(r) if err != nil { prob = probs.ServerInternal("Records for validation corrupt") @@ -1945,26 +1684,32 @@ func (ra *RegistrationAuthorityImpl) PerformValidation( prob = probs.ServerInternal("Records for validation failed sanity check") } + expires := *authz.Expires if prob != nil { challenge.Status = core.StatusInvalid challenge.Error = prob - - // TODO(#5545): Spending can be async until key-value rate limits - // are authoritative. This saves us from adding latency to each - // request. Goroutines spun out below will respect a context - // deadline set by the ratelimits package and cannot be prematurely - // canceled by the requester. - go ra.countFailedValidation(vaCtx, authz.RegistrationID, authz.Identifier.Value) + err := ra.countFailedValidations(vaCtx, authz.RegistrationID, authz.Identifier) + if err != nil { + ra.log.Warningf("incrementing failed validations: %s", err) + } } else { challenge.Status = core.StatusValid + expires = ra.clk.Now().Add(profile.validAuthzLifetime) + if features.Get().AutomaticallyPauseZombieClients { + ra.resetAccountPausingLimit(vaCtx, authz.RegistrationID, authz.Identifier) + } } challenge.Validated = &vStart authz.Challenges[challIndex] = *challenge - err = ra.recordValidation(vaCtx, authz.ID, authz.Expires, challenge) + err = ra.recordValidation(vaCtx, authz.ID, expires, challenge) if err != nil { - if errors.Is(err, berrors.AlreadyRevoked) { - ra.log.Infof("Didn't record already-finalized validation: regID=[%d] authzID=[%s] err=[%s]", + if errors.Is(err, berrors.NotFound) { + // We log NotFound at a lower level because this is largely due to a + // parallel-validation race: a different validation attempt has already + // updated this authz, so we failed to find a *pending* authz with the + // given ID to update. + ra.log.Infof("Failed to record validation (likely parallel validation race): regID=[%d] authzID=[%s] err=[%s]", authz.RegistrationID, authz.ID, err) } else { ra.log.AuditErrf("Failed to record validation: regID=[%d] authzID=[%s] err=[%s]", @@ -1977,14 +1722,20 @@ func (ra *RegistrationAuthorityImpl) PerformValidation( // revokeCertificate updates the database to mark the certificate as revoked, // with the given reason and current timestamp. -func (ra *RegistrationAuthorityImpl) revokeCertificate(ctx context.Context, serial *big.Int, issuerID issuance.NameID, reason revocation.Reason) error { - serialString := core.SerialToString(serial) +func (ra *RegistrationAuthorityImpl) revokeCertificate(ctx context.Context, cert *x509.Certificate, reason revocation.Reason) error { + serialString := core.SerialToString(cert.SerialNumber) + issuerID := issuance.IssuerNameID(cert) + shardIdx, err := crlShard(cert) + if err != nil { + return err + } - _, err := ra.SA.RevokeCertificate(ctx, &sapb.RevokeCertificateRequest{ + _, err = ra.SA.RevokeCertificate(ctx, &sapb.RevokeCertificateRequest{ Serial: serialString, Reason: int64(reason), Date: timestamppb.New(ra.clk.Now()), IssuerID: int64(issuerID), + ShardIdx: shardIdx, }) if err != nil { return err @@ -1998,9 +1749,7 @@ func (ra *RegistrationAuthorityImpl) revokeCertificate(ctx context.Context, seri // as revoked, with the given reason and current timestamp. This only works for // certificates that were previously revoked for a reason other than // keyCompromise, and which are now being updated to keyCompromise instead. -func (ra *RegistrationAuthorityImpl) updateRevocationForKeyCompromise(ctx context.Context, serial *big.Int, issuerID issuance.NameID) error { - serialString := core.SerialToString(serial) - +func (ra *RegistrationAuthorityImpl) updateRevocationForKeyCompromise(ctx context.Context, serialString string, issuerID issuance.NameID) error { status, err := ra.SA.GetCertificateStatus(ctx, &sapb.Serial{Serial: serialString}) if err != nil { return berrors.NotFoundError("unable to confirm that serial %q was ever issued: %s", serialString, err) @@ -2015,12 +1764,27 @@ func (ra *RegistrationAuthorityImpl) updateRevocationForKeyCompromise(ctx contex return berrors.AlreadyRevokedError("unable to re-revoke serial %q which is already revoked for keyCompromise", serialString) } + cert, err := ra.SA.GetCertificate(ctx, &sapb.Serial{Serial: serialString}) + if err != nil { + return berrors.NotFoundError("unable to confirm that serial %q was ever issued: %s", serialString, err) + } + x509Cert, err := x509.ParseCertificate(cert.Der) + if err != nil { + return err + } + + shardIdx, err := crlShard(x509Cert) + if err != nil { + return err + } + _, err = ra.SA.UpdateRevokedCertificate(ctx, &sapb.RevokeCertificateRequest{ Serial: serialString, Reason: int64(ocsp.KeyCompromise), Date: timestamppb.New(ra.clk.Now()), Backdate: status.RevokedDate, IssuerID: int64(issuerID), + ShardIdx: shardIdx, }) if err != nil { return err @@ -2033,6 +1797,12 @@ func (ra *RegistrationAuthorityImpl) updateRevocationForKeyCompromise(ctx contex // purgeOCSPCache makes a request to akamai-purger to purge the cache entries // for the given certificate. func (ra *RegistrationAuthorityImpl) purgeOCSPCache(ctx context.Context, cert *x509.Certificate, issuerID issuance.NameID) error { + if len(cert.OCSPServer) == 0 { + // We can't purge the cache (and there should be no responses in the cache) + // for certs that have no AIA OCSP URI. + return nil + } + issuer, ok := ra.issuersByNameID[issuerID] if !ok { return fmt.Errorf("unable to identify issuer of cert with serial %q", core.SerialToString(cert.SerialNumber)) @@ -2110,23 +1880,26 @@ func (ra *RegistrationAuthorityImpl) RevokeCertByApplicant(ctx context.Context, // authorizations for all names in the cert. logEvent.Method = "control" - var authzMapPB *sapb.Authorizations - authzMapPB, err = ra.SA.GetValidAuthorizations2(ctx, &sapb.GetValidAuthorizationsRequest{ + idents := identifier.FromCert(cert) + var authzPB *sapb.Authorizations + authzPB, err = ra.SA.GetValidAuthorizations2(ctx, &sapb.GetValidAuthorizationsRequest{ RegistrationID: req.RegID, - Domains: cert.DNSNames, - Now: timestamppb.New(ra.clk.Now()), + Identifiers: idents.ToProtoSlice(), + ValidUntil: timestamppb.New(ra.clk.Now()), }) if err != nil { return nil, err } - m := make(map[string]struct{}) - for _, authz := range authzMapPB.Authz { - m[authz.Domain] = struct{}{} + var authzMap map[identifier.ACMEIdentifier]*core.Authorization + authzMap, err = bgrpc.PBToAuthzMap(authzPB) + if err != nil { + return nil, err } - for _, name := range cert.DNSNames { - if _, present := m[name]; !present { - return nil, berrors.UnauthorizedError("requester does not control all names in cert with serial %q", serialString) + + for _, ident := range idents { + if _, present := authzMap[ident]; !present { + return nil, berrors.UnauthorizedError("requester does not control all identifiers in cert with serial %q", serialString) } } @@ -2138,11 +1911,9 @@ func (ra *RegistrationAuthorityImpl) RevokeCertByApplicant(ctx context.Context, logEvent.Reason = req.Code } - issuerID := issuance.IssuerNameID(cert) err = ra.revokeCertificate( ctx, - cert.SerialNumber, - issuerID, + cert, revocation.Reason(req.Code), ) if err != nil { @@ -2150,11 +1921,50 @@ func (ra *RegistrationAuthorityImpl) RevokeCertByApplicant(ctx context.Context, } // Don't propagate purger errors to the client. + issuerID := issuance.IssuerNameID(cert) _ = ra.purgeOCSPCache(ctx, cert, issuerID) return &emptypb.Empty{}, nil } +// crlShard extracts the CRL shard from a certificate's CRLDistributionPoint. +// +// If there is no CRLDistributionPoint, returns 0. +// +// If there is more than one CRLDistributionPoint, returns an error. +// +// Assumes the shard number is represented in the URL as an integer that +// occurs in the last path component, optionally followed by ".crl". +// +// Note: This assumes (a) the CA is generating well-formed, correct +// CRLDistributionPoints and (b) an earlier component has verified the signature +// on this certificate comes from one of our issuers. +func crlShard(cert *x509.Certificate) (int64, error) { + if len(cert.CRLDistributionPoints) == 0 { + return 0, nil + } + if len(cert.CRLDistributionPoints) > 1 { + return 0, errors.New("too many crlDistributionPoints in certificate") + } + + url := strings.TrimSuffix(cert.CRLDistributionPoints[0], ".crl") + lastIndex := strings.LastIndex(url, "/") + if lastIndex == -1 { + return 0, fmt.Errorf("malformed CRLDistributionPoint %q", url) + } + shardStr := url[lastIndex+1:] + shardIdx, err := strconv.Atoi(shardStr) + if err != nil { + return 0, fmt.Errorf("parsing CRLDistributionPoint: %s", err) + } + + if shardIdx <= 0 { + return 0, fmt.Errorf("invalid shard in CRLDistributionPoint: %d", shardIdx) + } + + return int64(shardIdx), nil +} + // addToBlockedKeys initiates a GRPC call to have the Base64-encoded SHA256 // digest of a provided public key added to the blockedKeys table. func (ra *RegistrationAuthorityImpl) addToBlockedKeys(ctx context.Context, key crypto.PublicKey, src string, comment string) error { @@ -2194,8 +2004,6 @@ func (ra *RegistrationAuthorityImpl) RevokeCertByKey(ctx context.Context, req *r return nil, err } - issuerID := issuance.IssuerNameID(cert) - logEvent := certificateRevocationEvent{ ID: core.NewToken(), SerialNumber: core.SerialToString(cert.SerialNumber), @@ -2220,8 +2028,7 @@ func (ra *RegistrationAuthorityImpl) RevokeCertByKey(ctx context.Context, req *r // since that addition needs to happen no matter what. revokeErr := ra.revokeCertificate( ctx, - cert.SerialNumber, - issuerID, + cert, revocation.Reason(ocsp.KeyCompromise), ) @@ -2233,6 +2040,8 @@ func (ra *RegistrationAuthorityImpl) RevokeCertByKey(ctx context.Context, req *r return nil, err } + issuerID := issuance.IssuerNameID(cert) + // Check the error returned from revokeCertificate itself. err = revokeErr if err == nil { @@ -2244,7 +2053,7 @@ func (ra *RegistrationAuthorityImpl) RevokeCertByKey(ctx context.Context, req *r } else if errors.Is(err, berrors.AlreadyRevoked) { // If it was an AlreadyRevoked error, try to re-revoke the cert in case // it was revoked for a reason other than keyCompromise. - err = ra.updateRevocationForKeyCompromise(ctx, cert.SerialNumber, issuerID) + err = ra.updateRevocationForKeyCompromise(ctx, core.SerialToString(cert.SerialNumber), issuerID) // Perform an Akamai cache purge to handle occurrences of a client // previously successfully revoking a certificate, but the cache purge had @@ -2263,7 +2072,7 @@ func (ra *RegistrationAuthorityImpl) RevokeCertByKey(ctx context.Context, req *r // AdministrativelyRevokeCertificate terminates trust in the certificate // provided and does not require the registration ID of the requester since this -// method is only called from the admin-revoker tool. It trusts that the admin +// method is only called from the `admin` tool. It trusts that the admin // is doing the right thing, so if the requested reason is keyCompromise, it // blocks the key from future issuance even though compromise has not been // demonstrated here. It purges the certificate from the Akamai cache, and @@ -2276,6 +2085,9 @@ func (ra *RegistrationAuthorityImpl) AdministrativelyRevokeCertificate(ctx conte if req.Serial == "" { return nil, errIncompleteGRPCRequest } + if req.CrlShard != 0 && !req.Malformed { + return nil, errors.New("non-zero CRLShard is only allowed for malformed certificates (shard is automatic for well formed certificates)") + } reasonCode := revocation.Reason(req.Code) if _, present := revocation.AdminAllowedReasons[reasonCode]; !present { @@ -2292,6 +2104,7 @@ func (ra *RegistrationAuthorityImpl) AdministrativelyRevokeCertificate(ctx conte ID: core.NewToken(), SerialNumber: req.Serial, Reason: req.Code, + CRLShard: req.CrlShard, Method: "admin", AdminName: req.AdminName, } @@ -2309,6 +2122,7 @@ func (ra *RegistrationAuthorityImpl) AdministrativelyRevokeCertificate(ctx conte var cert *x509.Certificate var issuerID issuance.NameID + var shard int64 if req.Cert != nil { // If the incoming request includes a certificate body, just use that and // avoid doing any database queries. This code path is deprecated and will @@ -2318,6 +2132,10 @@ func (ra *RegistrationAuthorityImpl) AdministrativelyRevokeCertificate(ctx conte return nil, err } issuerID = issuance.IssuerNameID(cert) + shard, err = crlShard(cert) + if err != nil { + return nil, err + } } else if !req.Malformed { // As long as we don't believe the cert will be malformed, we should // get the precertificate so we can block its pubkey if necessary and purge @@ -2335,6 +2153,10 @@ func (ra *RegistrationAuthorityImpl) AdministrativelyRevokeCertificate(ctx conte return nil, err } issuerID = issuance.IssuerNameID(cert) + shard, err = crlShard(cert) + if err != nil { + return nil, err + } } else { // But if the cert is malformed, we at least still need its IssuerID. var status *corepb.CertificateStatus @@ -2343,29 +2165,30 @@ func (ra *RegistrationAuthorityImpl) AdministrativelyRevokeCertificate(ctx conte return nil, fmt.Errorf("unable to confirm that serial %q was ever issued: %w", req.Serial, err) } issuerID = issuance.NameID(status.IssuerID) + shard = req.CrlShard } - var serialInt *big.Int - serialInt, err = core.StringToSerial(req.Serial) - if err != nil { - return nil, err - } - - err = ra.revokeCertificate(ctx, serialInt, issuerID, revocation.Reason(req.Code)) + _, err = ra.SA.RevokeCertificate(ctx, &sapb.RevokeCertificateRequest{ + Serial: req.Serial, + Reason: req.Code, + Date: timestamppb.New(ra.clk.Now()), + IssuerID: int64(issuerID), + ShardIdx: shard, + }) // Perform an Akamai cache purge to handle occurrences of a client // successfully revoking a certificate, but the initial cache purge failing. if errors.Is(err, berrors.AlreadyRevoked) { if cert != nil { err = ra.purgeOCSPCache(ctx, cert, issuerID) if err != nil { - err = fmt.Errorf("OCSP cache purge for already revoked serial %v failed: %w", serialInt, err) + err = fmt.Errorf("OCSP cache purge for already revoked serial %v failed: %w", req.Serial, err) return nil, err } } } if err != nil { if req.Code == ocsp.KeyCompromise && errors.Is(err, berrors.AlreadyRevoked) { - err = ra.updateRevocationForKeyCompromise(ctx, serialInt, issuerID) + err = ra.updateRevocationForKeyCompromise(ctx, req.Serial, issuerID) if err != nil { return nil, err } @@ -2386,7 +2209,7 @@ func (ra *RegistrationAuthorityImpl) AdministrativelyRevokeCertificate(ctx conte if cert != nil { err = ra.purgeOCSPCache(ctx, cert, issuerID) if err != nil { - err = fmt.Errorf("OCSP cache purge for serial %v failed: %w", serialInt, err) + err = fmt.Errorf("OCSP cache purge for serial %v failed: %w", req.Serial, err) return nil, err } } @@ -2395,23 +2218,24 @@ func (ra *RegistrationAuthorityImpl) AdministrativelyRevokeCertificate(ctx conte } // DeactivateRegistration deactivates a valid registration -func (ra *RegistrationAuthorityImpl) DeactivateRegistration(ctx context.Context, reg *corepb.Registration) (*emptypb.Empty, error) { - if reg == nil || reg.Id == 0 { +func (ra *RegistrationAuthorityImpl) DeactivateRegistration(ctx context.Context, req *rapb.DeactivateRegistrationRequest) (*corepb.Registration, error) { + if req == nil || req.RegistrationID == 0 { return nil, errIncompleteGRPCRequest } - if reg.Status != string(core.StatusValid) { - return nil, berrors.MalformedError("only valid registrations can be deactivated") - } - _, err := ra.SA.DeactivateRegistration(ctx, &sapb.RegistrationID{Id: reg.Id}) + + updatedAcct, err := ra.SA.DeactivateRegistration(ctx, &sapb.RegistrationID{Id: req.RegistrationID}) if err != nil { - return nil, berrors.InternalServerError(err.Error()) + return nil, err } - return &emptypb.Empty{}, nil + + return updatedAcct, nil } // DeactivateAuthorization deactivates a currently valid authorization func (ra *RegistrationAuthorityImpl) DeactivateAuthorization(ctx context.Context, req *corepb.Authorization) (*emptypb.Empty, error) { - if req == nil || req.Id == "" || req.Status == "" { + ident := identifier.FromProto(req.Identifier) + + if core.IsAnyNilOrZero(req, req.Id, ident, req.Status, req.RegistrationID) { return nil, errIncompleteGRPCRequest } authzID, err := strconv.ParseInt(req.Id, 10, 64) @@ -2421,6 +2245,17 @@ func (ra *RegistrationAuthorityImpl) DeactivateAuthorization(ctx context.Context if _, err := ra.SA.DeactivateAuthorization2(ctx, &sapb.AuthorizationID2{Id: authzID}); err != nil { return nil, err } + if req.Status == string(core.StatusPending) { + // Some clients deactivate pending authorizations without attempting them. + // We're not sure exactly when this happens but it's most likely due to + // internal errors in the client. From our perspective this uses storage + // resources similar to how failed authorizations do, so we increment the + // failed authorizations limit. + err = ra.countFailedValidations(ctx, req.RegistrationID, ident) + if err != nil { + return nil, fmt.Errorf("failed to update rate limits: %w", err) + } + } return &emptypb.Empty{}, nil } @@ -2457,7 +2292,7 @@ func (ra *RegistrationAuthorityImpl) GenerateOCSP(ctx context.Context, req *rapb return ra.OCSP.GenerateOCSP(ctx, &capb.GenerateOCSPRequest{ Serial: req.Serial, Status: status.Status, - Reason: int32(status.RevokedReason), + Reason: int32(status.RevokedReason), //nolint: gosec // Revocation reasons are guaranteed to be small, no risk of overflow. RevokedAt: status.RevokedDate, IssuerID: status.IssuerID, }) @@ -2469,24 +2304,39 @@ func (ra *RegistrationAuthorityImpl) NewOrder(ctx context.Context, req *rapb.New return nil, errIncompleteGRPCRequest } - newOrder := &sapb.NewOrderRequest{ - RegistrationID: req.RegistrationID, - Names: core.UniqueLowerNames(req.Names), - ReplacesSerial: req.ReplacesSerial, - } + idents := identifier.Normalize(identifier.FromProtoSlice(req.Identifiers)) - if len(newOrder.Names) > ra.maxNames { - return nil, berrors.MalformedError( - "Order cannot contain more than %d DNS names", ra.maxNames) - } - - // Validate that our policy allows issuing for each of the names in the order - err := ra.PA.WillingToIssue(newOrder.Names) + profile, err := ra.profiles.get(req.CertificateProfileName) if err != nil { return nil, err } - err = wildcardOverlap(newOrder.Names) + if profile.allowList != nil && !profile.allowList.Contains(req.RegistrationID) { + return nil, berrors.UnauthorizedError("account ID %d is not permitted to use certificate profile %q", + req.RegistrationID, + req.CertificateProfileName, + ) + } + + if len(idents) > profile.maxNames { + return nil, berrors.MalformedError( + "Order cannot contain more than %d identifiers", profile.maxNames) + } + + for _, ident := range idents { + if !slices.Contains(profile.identifierTypes, ident.Type) { + return nil, berrors.RejectedIdentifierError("Profile %q does not permit %s type identifiers", req.CertificateProfileName, ident.Type) + } + } + + // Validate that our policy allows issuing for each of the identifiers in + // the order + err = ra.PA.WillingToIssue(idents) + if err != nil { + return nil, err + } + + err = wildcardOverlap(idents) if err != nil { return nil, err } @@ -2494,8 +2344,8 @@ func (ra *RegistrationAuthorityImpl) NewOrder(ctx context.Context, req *rapb.New // See if there is an existing unexpired pending (or ready) order that can be reused // for this account existingOrder, err := ra.SA.GetOrderForNames(ctx, &sapb.GetOrderForNamesRequest{ - AcctID: newOrder.RegistrationID, - Names: newOrder.Names, + AcctID: req.RegistrationID, + Identifiers: idents.ToProtoSlice(), }) // If there was an error and it wasn't an acceptable "NotFound" error, return // immediately @@ -2507,22 +2357,16 @@ func (ra *RegistrationAuthorityImpl) NewOrder(ctx context.Context, req *rapb.New // Error if an incomplete order is returned. if existingOrder != nil { // Check to see if the expected fields of the existing order are set. - // TODO(#7153): Check each value via core.IsAnyNilOrZero - if existingOrder.Id == 0 || existingOrder.Status == "" || existingOrder.RegistrationID == 0 || len(existingOrder.Names) == 0 || core.IsAnyNilOrZero(existingOrder.Created, existingOrder.Expires) { + if core.IsAnyNilOrZero(existingOrder.Id, existingOrder.Status, existingOrder.RegistrationID, existingOrder.Identifiers, existingOrder.Created, existingOrder.Expires) { return nil, errIncompleteGRPCResponse } - // Track how often we reuse an existing order and how old that order is. - ra.orderAges.WithLabelValues("NewOrder").Observe(ra.clk.Since(existingOrder.Created.AsTime()).Seconds()) - return existingOrder, nil - } - // Renewal orders, indicated by ARI, are exempt from NewOrder rate limits. - if !req.LimitsExempt { - - // Check if there is rate limit space for issuing a certificate. - err = ra.checkNewOrderLimits(ctx, newOrder.Names, newOrder.RegistrationID) - if err != nil { - return nil, err + // Only re-use the order if the profile (even if it is just the empty + // string, leaving us to choose a default profile) matches. + if existingOrder.CertificateProfileName == req.CertificateProfileName { + // Track how often we reuse an existing order and how old that order is. + ra.orderAges.WithLabelValues("NewOrder").Observe(ra.clk.Since(existingOrder.Created.AsTime()).Seconds()) + return existingOrder, nil } } @@ -2531,137 +2375,156 @@ func (ra *RegistrationAuthorityImpl) NewOrder(ctx context.Context, req *rapb.New // `sa.GetAuthorizations` returned an authorization that was very close to // expiry. The resulting pending order that references it would itself end up // expiring very soon. - // To prevent this we only return authorizations that are at least 1 day away - // from expiring. - authzExpiryCutoff := ra.clk.Now().AddDate(0, 0, 1) - - getAuthReq := &sapb.GetAuthorizationsRequest{ - RegistrationID: newOrder.RegistrationID, - Now: timestamppb.New(authzExpiryCutoff), - Domains: newOrder.Names, + // What is considered "very soon" scales with the associated order's lifetime, + // up to a point. + minTimeToExpiry := profile.orderLifetime / 8 + if minTimeToExpiry < time.Hour { + minTimeToExpiry = time.Hour + } else if minTimeToExpiry > 24*time.Hour { + minTimeToExpiry = 24 * time.Hour + } + authzExpiryCutoff := ra.clk.Now().Add(minTimeToExpiry) + + var existingAuthz *sapb.Authorizations + if features.Get().NoPendingAuthzReuse { + getAuthReq := &sapb.GetValidAuthorizationsRequest{ + RegistrationID: req.RegistrationID, + ValidUntil: timestamppb.New(authzExpiryCutoff), + Identifiers: idents.ToProtoSlice(), + Profile: req.CertificateProfileName, + } + existingAuthz, err = ra.SA.GetValidAuthorizations2(ctx, getAuthReq) + } else { + getAuthReq := &sapb.GetAuthorizationsRequest{ + RegistrationID: req.RegistrationID, + ValidUntil: timestamppb.New(authzExpiryCutoff), + Identifiers: idents.ToProtoSlice(), + Profile: req.CertificateProfileName, + } + existingAuthz, err = ra.SA.GetAuthorizations2(ctx, getAuthReq) } - existingAuthz, err := ra.SA.GetAuthorizations2(ctx, getAuthReq) if err != nil { return nil, err } - // Collect up the authorizations we found into a map keyed by the domains the - // authorizations correspond to - nameToExistingAuthz := make(map[string]*corepb.Authorization, len(newOrder.Names)) - for _, v := range existingAuthz.Authz { - nameToExistingAuthz[v.Domain] = v.Authz + identToExistingAuthz, err := bgrpc.PBToAuthzMap(existingAuthz) + if err != nil { + return nil, err } - // For each of the names in the order, if there is an acceptable - // existing authz, append it to the order to reuse it. Otherwise track - // that there is a missing authz for that name. - var missingAuthzNames []string - for _, name := range newOrder.Names { + // For each of the identifiers in the order, if there is an acceptable + // existing authz, append it to the order to reuse it. Otherwise track that + // there is a missing authz for that identifier. + var newOrderAuthzs []int64 + var missingAuthzIdents identifier.ACMEIdentifiers + for _, ident := range idents { // If there isn't an existing authz, note that its missing and continue - if _, exists := nameToExistingAuthz[name]; !exists { - missingAuthzNames = append(missingAuthzNames, name) - continue - } - authz := nameToExistingAuthz[name] - authzAge := (ra.authorizationLifetime - authz.Expires.AsTime().Sub(ra.clk.Now())).Seconds() - // If the identifier is a wildcard and the existing authz only has one - // DNS-01 type challenge we can reuse it. In theory we will - // never get back an authorization for a domain with a wildcard prefix - // that doesn't meet this criteria from SA.GetAuthorizations but we verify - // again to be safe. - if strings.HasPrefix(name, "*.") && - len(authz.Challenges) == 1 && core.AcmeChallenge(authz.Challenges[0].Type) == core.ChallengeTypeDNS01 { - authzID, err := strconv.ParseInt(authz.Id, 10, 64) - if err != nil { - return nil, err - } - newOrder.V2Authorizations = append(newOrder.V2Authorizations, authzID) - ra.authzAges.WithLabelValues("NewOrder", authz.Status).Observe(authzAge) - continue - } else if !strings.HasPrefix(name, "*.") { - // If the identifier isn't a wildcard, we can reuse any authz - authzID, err := strconv.ParseInt(authz.Id, 10, 64) - if err != nil { - return nil, err - } - newOrder.V2Authorizations = append(newOrder.V2Authorizations, authzID) - ra.authzAges.WithLabelValues("NewOrder", authz.Status).Observe(authzAge) + authz, exists := identToExistingAuthz[ident] + if !exists { + // The existing authz was not acceptable for reuse, and we need to + // mark the name as requiring a new pending authz. + missingAuthzIdents = append(missingAuthzIdents, ident) continue } - // Delete the authz from the nameToExistingAuthz map since we are not reusing it. - delete(nameToExistingAuthz, name) - // If we reached this point then the existing authz was not acceptable for - // reuse and we need to mark the name as requiring a new pending authz - missingAuthzNames = append(missingAuthzNames, name) - } - - // Renewal orders, indicated by ARI, are exempt from NewOrder rate limits. - if len(missingAuthzNames) > 0 && !req.LimitsExempt { - pendingAuthzLimits := ra.rlPolicies.PendingAuthorizationsPerAccount() - if pendingAuthzLimits.Enabled() { - // The order isn't fully authorized we need to check that the client - // has rate limit room for more pending authorizations. - started := ra.clk.Now() - err := ra.checkPendingAuthorizationLimit(ctx, newOrder.RegistrationID, pendingAuthzLimits) - elapsed := ra.clk.Since(started) - if err != nil { - if errors.Is(err, berrors.RateLimit) { - ra.rlCheckLatency.WithLabelValues(ratelimit.PendingAuthorizationsPerAccount, ratelimits.Denied).Observe(elapsed.Seconds()) - } - return nil, err - } - ra.rlCheckLatency.WithLabelValues(ratelimit.PendingAuthorizationsPerAccount, ratelimits.Allowed).Observe(elapsed.Seconds()) + // If the authz is associated with the wrong profile, don't reuse it. + if authz.CertificateProfileName != req.CertificateProfileName { + missingAuthzIdents = append(missingAuthzIdents, ident) + // Delete the authz from the identToExistingAuthz map since we are not reusing it. + delete(identToExistingAuthz, ident) + continue } - } - // Loop through each of the names missing authzs and create a new pending - // authorization for each. - var newAuthzs []*corepb.Authorization - for _, name := range missingAuthzNames { - pb, err := ra.createPendingAuthz(newOrder.RegistrationID, identifier.ACMEIdentifier{ - Type: identifier.DNS, - Value: name, - }) + // This is only used for our metrics. + authzAge := (profile.validAuthzLifetime - authz.Expires.Sub(ra.clk.Now())).Seconds() + if authz.Status == core.StatusPending { + authzAge = (profile.pendingAuthzLifetime - authz.Expires.Sub(ra.clk.Now())).Seconds() + } + + // If the identifier is a wildcard DNS name, it must have exactly one + // DNS-01 type challenge. The PA guarantees this at order creation time, + // but we verify again to be safe. + if ident.Type == identifier.TypeDNS && strings.HasPrefix(ident.Value, "*.") && + (len(authz.Challenges) != 1 || authz.Challenges[0].Type != core.ChallengeTypeDNS01) { + return nil, berrors.InternalServerError( + "SA.GetAuthorizations returned a DNS wildcard authz (%s) with invalid challenge(s)", + authz.ID) + } + + // If we reached this point then the existing authz was acceptable for + // reuse. + authzID, err := strconv.ParseInt(authz.ID, 10, 64) if err != nil { return nil, err } - newAuthzs = append(newAuthzs, pb) - ra.authzAges.WithLabelValues("NewOrder", pb.Status).Observe(0) + newOrderAuthzs = append(newOrderAuthzs, authzID) + ra.authzAges.WithLabelValues("NewOrder", string(authz.Status)).Observe(authzAge) + } + + // Loop through each of the identifiers missing authzs and create a new + // pending authorization for each. + var newAuthzs []*sapb.NewAuthzRequest + for _, ident := range missingAuthzIdents { + challTypes, err := ra.PA.ChallengeTypesFor(ident) + if err != nil { + return nil, err + } + + var challStrs []string + for _, t := range challTypes { + challStrs = append(challStrs, string(t)) + } + + newAuthzs = append(newAuthzs, &sapb.NewAuthzRequest{ + Identifier: ident.ToProto(), + RegistrationID: req.RegistrationID, + Expires: timestamppb.New(ra.clk.Now().Add(profile.pendingAuthzLifetime).Truncate(time.Second)), + ChallengeTypes: challStrs, + Token: core.NewToken(), + }) + + ra.authzAges.WithLabelValues("NewOrder", string(core.StatusPending)).Observe(0) } // Start with the order's own expiry as the minExpiry. We only care // about authz expiries that are sooner than the order's expiry - minExpiry := ra.clk.Now().Add(ra.orderLifetime) + minExpiry := ra.clk.Now().Add(profile.orderLifetime) // Check the reused authorizations to see if any have an expiry before the // minExpiry (the order's lifetime) - for _, authz := range nameToExistingAuthz { + for _, authz := range identToExistingAuthz { // An authz without an expiry is an unexpected internal server event if core.IsAnyNilOrZero(authz.Expires) { return nil, berrors.InternalServerError( "SA.GetAuthorizations returned an authz (%s) with zero expiry", - authz.Id) + authz.ID) } // If the reused authorization expires before the minExpiry, it's expiry // is the new minExpiry. - authzExpiry := authz.Expires.AsTime() - if authzExpiry.Before(minExpiry) { - minExpiry = authzExpiry + if authz.Expires.Before(minExpiry) { + minExpiry = *authz.Expires } } // If the newly created pending authz's have an expiry closer than the // minExpiry the minExpiry is the pending authz expiry. if len(newAuthzs) > 0 { - newPendingAuthzExpires := ra.clk.Now().Add(ra.pendingAuthorizationLifetime) + newPendingAuthzExpires := ra.clk.Now().Add(profile.pendingAuthzLifetime) if newPendingAuthzExpires.Before(minExpiry) { minExpiry = newPendingAuthzExpires } } - // Set the order's expiry to the minimum expiry. The db doesn't store - // sub-second values, so truncate here. - newOrder.Expires = timestamppb.New(minExpiry.Truncate(time.Second)) + newOrder := &sapb.NewOrderRequest{ + RegistrationID: req.RegistrationID, + Identifiers: idents.ToProtoSlice(), + CertificateProfileName: req.CertificateProfileName, + Replaces: req.Replaces, + ReplacesSerial: req.ReplacesSerial, + // Set the order's expiry to the minimum expiry. The db doesn't store + // sub-second values, so truncate here. + Expires: timestamppb.New(minExpiry.Truncate(time.Second)), + V2Authorizations: newOrderAuthzs, + } newOrderAndAuthzsReq := &sapb.NewOrderAndAuthzsRequest{ NewOrder: newOrder, NewAuthzs: newAuthzs, @@ -2671,60 +2534,25 @@ func (ra *RegistrationAuthorityImpl) NewOrder(ctx context.Context, req *rapb.New return nil, err } - if core.IsAnyNilOrZero(storedOrder.Id, storedOrder.Status, storedOrder.RegistrationID, storedOrder.Names, storedOrder.Created, storedOrder.Expires) { + if core.IsAnyNilOrZero(storedOrder.Id, storedOrder.Status, storedOrder.RegistrationID, storedOrder.Identifiers, storedOrder.Created, storedOrder.Expires) { return nil, errIncompleteGRPCResponse } ra.orderAges.WithLabelValues("NewOrder").Observe(0) - // Note how many names are being requested in this certificate order. - ra.namesPerCert.With(prometheus.Labels{"type": "requested"}).Observe(float64(len(storedOrder.Names))) + // Note how many identifiers are being requested in this certificate order. + ra.namesPerCert.With(prometheus.Labels{"type": "requested"}).Observe(float64(len(storedOrder.Identifiers))) return storedOrder, nil } -// createPendingAuthz checks that a name is allowed for issuance and creates the -// necessary challenges for it and puts this and all of the relevant information -// into a corepb.Authorization for transmission to the SA to be stored -func (ra *RegistrationAuthorityImpl) createPendingAuthz(reg int64, identifier identifier.ACMEIdentifier) (*corepb.Authorization, error) { - authz := &corepb.Authorization{ - Identifier: identifier.Value, - RegistrationID: reg, - Status: string(core.StatusPending), - Expires: timestamppb.New(ra.clk.Now().Add(ra.pendingAuthorizationLifetime).Truncate(time.Second)), - } - - // Create challenges. The WFE will update them with URIs before sending them out. - challenges, err := ra.PA.ChallengesFor(identifier) - if err != nil { - // The only time ChallengesFor errors it is a fatal configuration error - // where challenges required by policy for an identifier are not enabled. We - // want to treat this as an internal server error. - return nil, berrors.InternalServerError(err.Error()) - } - // Check each challenge for sanity. - for _, challenge := range challenges { - err := challenge.CheckPending() - if err != nil { - // berrors.InternalServerError because we generated these challenges, they should - // be OK. - err = berrors.InternalServerError("challenge didn't pass sanity check: %+v", challenge) - return nil, err - } - challPB, err := bgrpc.ChallengeToPB(challenge) - if err != nil { - return nil, err - } - authz.Challenges = append(authz.Challenges, challPB) - } - return authz, nil -} - -// wildcardOverlap takes a slice of domain names and returns an error if any of +// wildcardOverlap takes a slice of identifiers and returns an error if any of // them is a non-wildcard FQDN that overlaps with a wildcard domain in the map. -func wildcardOverlap(dnsNames []string) error { - nameMap := make(map[string]bool, len(dnsNames)) - for _, v := range dnsNames { - nameMap[v] = true +func wildcardOverlap(idents identifier.ACMEIdentifiers) error { + nameMap := make(map[string]bool, len(idents)) + for _, v := range idents { + if v.Type == identifier.TypeDNS { + nameMap[v.Value] = true + } } for name := range nameMap { if name[0] == '*' { @@ -2740,31 +2568,85 @@ func wildcardOverlap(dnsNames []string) error { return nil } -// validateContactsPresent will return an error if the contacts []string -// len is greater than zero and the contactsPresent bool is false. We -// don't care about any other cases. If the length of the contacts is zero -// and contactsPresent is true, it seems like a mismatch but we have to -// assume that the client is requesting to update the contacts field with -// by removing the existing contacts value so we don't want to return an -// error here. -func validateContactsPresent(contacts []string, contactsPresent bool) error { - if len(contacts) > 0 && !contactsPresent { - return berrors.InternalServerError("account contacts present but contactsPresent false") - } - return nil -} - -func (ra *RegistrationAuthorityImpl) DrainFinalize() { - ra.finalizeWG.Wait() -} - // UnpauseAccount receives a validated account unpause request from the SFE and // instructs the SA to unpause that account. If the account cannot be unpaused, // an error is returned. -func (ra *RegistrationAuthorityImpl) UnpauseAccount(ctx context.Context, request *rapb.UnpauseAccountRequest) (*emptypb.Empty, error) { +func (ra *RegistrationAuthorityImpl) UnpauseAccount(ctx context.Context, request *rapb.UnpauseAccountRequest) (*rapb.UnpauseAccountResponse, error) { if core.IsAnyNilOrZero(request.RegistrationID) { return nil, errIncompleteGRPCRequest } - return nil, status.Errorf(codes.Unimplemented, "method UnpauseAccount not implemented") + count, err := ra.SA.UnpauseAccount(ctx, &sapb.RegistrationID{ + Id: request.RegistrationID, + }) + if err != nil { + return nil, berrors.InternalServerError("failed to unpause account ID %d", request.RegistrationID) + } + + return &rapb.UnpauseAccountResponse{Count: count.Count}, nil +} + +func (ra *RegistrationAuthorityImpl) GetAuthorization(ctx context.Context, req *rapb.GetAuthorizationRequest) (*corepb.Authorization, error) { + if core.IsAnyNilOrZero(req, req.Id) { + return nil, errIncompleteGRPCRequest + } + + authz, err := ra.SA.GetAuthorization2(ctx, &sapb.AuthorizationID2{Id: req.Id}) + if err != nil { + return nil, fmt.Errorf("getting authz from SA: %w", err) + } + + // Filter out any challenges which are currently disabled, so that the client + // doesn't attempt them. + challs := []*corepb.Challenge{} + for _, chall := range authz.Challenges { + if ra.PA.ChallengeTypeEnabled(core.AcmeChallenge(chall.Type)) { + challs = append(challs, chall) + } + } + + authz.Challenges = challs + return authz, nil +} + +// AddRateLimitOverride dispatches an SA RPC to add a rate limit override to the +// database. If the override already exists, it will be updated. If the override +// does not exist, it will be inserted and enabled. If the override exists but +// has been disabled, it will be updated but not be re-enabled. The status of +// the override is returned in Enabled field of the response. To re-enable an +// override, use sa.EnableRateLimitOverride. +func (ra *RegistrationAuthorityImpl) AddRateLimitOverride(ctx context.Context, req *rapb.AddRateLimitOverrideRequest) (*rapb.AddRateLimitOverrideResponse, error) { + if core.IsAnyNilOrZero(req, req.LimitEnum, req.BucketKey, req.Count, req.Burst, req.Period, req.Comment) { + return nil, errIncompleteGRPCRequest + } + + resp, err := ra.SA.AddRateLimitOverride(ctx, &sapb.AddRateLimitOverrideRequest{ + Override: &sapb.RateLimitOverride{ + LimitEnum: req.LimitEnum, + BucketKey: req.BucketKey, + Comment: req.Comment, + Period: req.Period, + Count: req.Count, + Burst: req.Burst, + }, + }) + if err != nil { + return nil, fmt.Errorf("adding rate limit override: %w", err) + } + + return &rapb.AddRateLimitOverrideResponse{ + Inserted: resp.Inserted, + Enabled: resp.Enabled, + }, nil +} + +// Drain blocks until all detached goroutines are done. +// +// The RA runs detached goroutines for challenge validation and finalization, +// so that ACME responses can be returned to the user promptly while work continues. +// +// The main goroutine should call this before exiting to avoid canceling the work +// being done in detached goroutines. +func (ra *RegistrationAuthorityImpl) Drain() { + ra.drainWG.Wait() } diff --git a/third-party/github.com/letsencrypt/boulder/ra/ra_test.go b/third-party/github.com/letsencrypt/boulder/ra/ra_test.go index ee69e54bd..1bcb2706e 100644 --- a/third-party/github.com/letsencrypt/boulder/ra/ra_test.go +++ b/third-party/github.com/letsencrypt/boulder/ra/ra_test.go @@ -7,17 +7,18 @@ import ( "crypto/elliptic" "crypto/rand" "crypto/rsa" - "crypto/sha256" "crypto/x509" "crypto/x509/pkix" + "encoding/asn1" + "encoding/hex" "encoding/json" "encoding/pem" "errors" "fmt" + "math" "math/big" - mrand "math/rand" - "net" - "os" + mrand "math/rand/v2" + "net/netip" "regexp" "strconv" "strings" @@ -26,28 +27,26 @@ import ( "time" "github.com/go-jose/go-jose/v4" - ctasn1 "github.com/google/certificate-transparency-go/asn1" - ctx509 "github.com/google/certificate-transparency-go/x509" - ctpkix "github.com/google/certificate-transparency-go/x509/pkix" "github.com/jmhodges/clock" "github.com/prometheus/client_golang/prometheus" - "github.com/weppos/publicsuffix-go/publicsuffix" "golang.org/x/crypto/ocsp" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/timestamppb" akamaipb "github.com/letsencrypt/boulder/akamai/proto" + "github.com/letsencrypt/boulder/allowlist" capb "github.com/letsencrypt/boulder/ca/proto" - "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/ctpolicy" "github.com/letsencrypt/boulder/ctpolicy/loglist" berrors "github.com/letsencrypt/boulder/errors" + "github.com/letsencrypt/boulder/features" "github.com/letsencrypt/boulder/goodkey" bgrpc "github.com/letsencrypt/boulder/grpc" "github.com/letsencrypt/boulder/identifier" @@ -58,62 +57,82 @@ import ( "github.com/letsencrypt/boulder/policy" pubpb "github.com/letsencrypt/boulder/publisher/proto" rapb "github.com/letsencrypt/boulder/ra/proto" - "github.com/letsencrypt/boulder/ratelimit" "github.com/letsencrypt/boulder/ratelimits" - bredis "github.com/letsencrypt/boulder/redis" "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" + "github.com/letsencrypt/boulder/va" vapb "github.com/letsencrypt/boulder/va/proto" ) -func createPendingAuthorization(t *testing.T, sa sapb.StorageAuthorityClient, domain string, exp time.Time) *corepb.Authorization { +// randomDomain creates a random domain name for testing. +// +// panics if crypto/rand.Rand.Read fails. +func randomDomain() string { + var bytes [4]byte + _, err := rand.Read(bytes[:]) + if err != nil { + panic(err) + } + return fmt.Sprintf("%x.example.com", bytes[:]) +} + +// randomIPv6 creates a random IPv6 netip.Addr for testing. It uses a real IPv6 +// address range, not a test/documentation range. +// +// panics if crypto/rand.Rand.Read or netip.AddrFromSlice fails. +func randomIPv6() netip.Addr { + var ipBytes [10]byte + _, err := rand.Read(ipBytes[:]) + if err != nil { + panic(err) + } + ipPrefix, err := hex.DecodeString("2602080a600f") + if err != nil { + panic(err) + } + ip, ok := netip.AddrFromSlice(bytes.Join([][]byte{ipPrefix, ipBytes[:]}, nil)) + if !ok { + panic("Couldn't parse random IP to netip.Addr") + } + return ip +} + +func createPendingAuthorization(t *testing.T, sa sapb.StorageAuthorityClient, ident identifier.ACMEIdentifier, exp time.Time) *corepb.Authorization { t.Helper() - authz := core.Authorization{ - Identifier: identifier.DNSIdentifier(domain), - RegistrationID: Registration.Id, - Status: "pending", - Expires: &exp, - Challenges: []core.Challenge{ - { - Token: core.NewToken(), - Type: core.ChallengeTypeHTTP01, - Status: core.StatusPending, + res, err := sa.NewOrderAndAuthzs( + context.Background(), + &sapb.NewOrderAndAuthzsRequest{ + NewOrder: &sapb.NewOrderRequest{ + RegistrationID: Registration.Id, + Expires: timestamppb.New(exp), + Identifiers: []*corepb.Identifier{ident.ToProto()}, }, - { - Token: core.NewToken(), - Type: core.ChallengeTypeDNS01, - Status: core.StatusPending, - }, - { - Token: core.NewToken(), - Type: core.ChallengeTypeTLSALPN01, - Status: core.StatusPending, + NewAuthzs: []*sapb.NewAuthzRequest{ + { + Identifier: ident.ToProto(), + RegistrationID: Registration.Id, + Expires: timestamppb.New(exp), + ChallengeTypes: []string{ + string(core.ChallengeTypeHTTP01), + string(core.ChallengeTypeDNS01), + string(core.ChallengeTypeTLSALPN01)}, + Token: core.NewToken(), + }, }, }, - } - authzPB, err := bgrpc.AuthzToPB(authz) - test.AssertNotError(t, err, "AuthzToPB failed") - - res, err := sa.NewOrderAndAuthzs(context.Background(), &sapb.NewOrderAndAuthzsRequest{ - NewOrder: &sapb.NewOrderRequest{ - RegistrationID: Registration.Id, - Expires: timestamppb.New(exp), - Names: []string{domain}, - }, - NewAuthzs: []*corepb.Authorization{authzPB}, - }) + ) test.AssertNotError(t, err, "sa.NewOrderAndAuthzs failed") return getAuthorization(t, fmt.Sprint(res.V2Authorizations[0]), sa) } -func createFinalizedAuthorization(t *testing.T, sa sapb.StorageAuthorityClient, domain string, exp time.Time, chall core.AcmeChallenge, attemptedAt time.Time) int64 { +func createFinalizedAuthorization(t *testing.T, sa sapb.StorageAuthorityClient, ident identifier.ACMEIdentifier, exp time.Time, chall core.AcmeChallenge, attemptedAt time.Time) int64 { t.Helper() - pending := createPendingAuthorization(t, sa, domain, exp) + pending := createPendingAuthorization(t, sa, ident, exp) pendingID, err := strconv.ParseInt(pending.Id, 10, 64) test.AssertNotError(t, err, "strconv.ParseInt failed") _, err = sa.FinalizeAuthorization2(context.Background(), &sapb.FinalizeAuthorizationRequest{ @@ -157,15 +176,58 @@ func numAuthorizations(o *corepb.Order) int { return len(o.V2Authorizations) } +// def is a test-only helper that returns the default validation profile +// and is guaranteed to succeed because the validationProfile constructor +// ensures that the default name has a corresponding profile. +func (vp *validationProfiles) def() *validationProfile { + return vp.byName[vp.defaultName] +} + type DummyValidationAuthority struct { - performValidationRequest chan *vapb.PerformValidationRequest - PerformValidationRequestResultError error - PerformValidationRequestResultReturn *vapb.ValidationResult + doDCVRequest chan *vapb.PerformValidationRequest + doDCVError error + doDCVResult *vapb.ValidationResult + + doCAARequest chan *vapb.IsCAAValidRequest + doCAAError error + doCAAResponse *vapb.IsCAAValidResponse } func (dva *DummyValidationAuthority) PerformValidation(ctx context.Context, req *vapb.PerformValidationRequest, _ ...grpc.CallOption) (*vapb.ValidationResult, error) { - dva.performValidationRequest <- req - return dva.PerformValidationRequestResultReturn, dva.PerformValidationRequestResultError + dcvRes, err := dva.DoDCV(ctx, req) + if err != nil { + return nil, err + } + if dcvRes.Problem != nil { + return dcvRes, nil + } + caaResp, err := dva.DoCAA(ctx, &vapb.IsCAAValidRequest{ + Identifier: req.Identifier, + ValidationMethod: req.Challenge.Type, + AccountURIID: req.Authz.RegID, + AuthzID: req.Authz.Id, + }) + if err != nil { + return nil, err + } + return &vapb.ValidationResult{ + Records: dcvRes.Records, + Problem: caaResp.Problem, + }, nil +} + +func (dva *DummyValidationAuthority) IsCAAValid(ctx context.Context, req *vapb.IsCAAValidRequest, _ ...grpc.CallOption) (*vapb.IsCAAValidResponse, error) { + return nil, status.Error(codes.Unimplemented, "IsCAAValid not implemented") +} + +func (dva *DummyValidationAuthority) DoDCV(ctx context.Context, req *vapb.PerformValidationRequest, _ ...grpc.CallOption) (*vapb.ValidationResult, error) { + dva.doDCVRequest <- req + return dva.doDCVResult, dva.doDCVError +} + +func (dva *DummyValidationAuthority) DoCAA(ctx context.Context, req *vapb.IsCAAValidRequest, _ ...grpc.CallOption) (*vapb.IsCAAValidResponse, error) { + dva.doCAARequest <- req + return dva.doCAAResponse, dva.doCAAError } var ( @@ -226,70 +288,7 @@ var ( var ctx = context.Background() -// dummyRateLimitConfig satisfies the rl.RateLimitConfig interface while -// allowing easy mocking of the individual RateLimitPolicy's -type dummyRateLimitConfig struct { - CertificatesPerNamePolicy ratelimit.RateLimitPolicy - RegistrationsPerIPPolicy ratelimit.RateLimitPolicy - RegistrationsPerIPRangePolicy ratelimit.RateLimitPolicy - PendingAuthorizationsPerAccountPolicy ratelimit.RateLimitPolicy - NewOrdersPerAccountPolicy ratelimit.RateLimitPolicy - InvalidAuthorizationsPerAccountPolicy ratelimit.RateLimitPolicy - CertificatesPerFQDNSetPolicy ratelimit.RateLimitPolicy - CertificatesPerFQDNSetFastPolicy ratelimit.RateLimitPolicy -} - -func (r *dummyRateLimitConfig) CertificatesPerName() ratelimit.RateLimitPolicy { - return r.CertificatesPerNamePolicy -} - -func (r *dummyRateLimitConfig) RegistrationsPerIP() ratelimit.RateLimitPolicy { - return r.RegistrationsPerIPPolicy -} - -func (r *dummyRateLimitConfig) RegistrationsPerIPRange() ratelimit.RateLimitPolicy { - return r.RegistrationsPerIPRangePolicy -} - -func (r *dummyRateLimitConfig) PendingAuthorizationsPerAccount() ratelimit.RateLimitPolicy { - return r.PendingAuthorizationsPerAccountPolicy -} - -func (r *dummyRateLimitConfig) NewOrdersPerAccount() ratelimit.RateLimitPolicy { - return r.NewOrdersPerAccountPolicy -} - -func (r *dummyRateLimitConfig) InvalidAuthorizationsPerAccount() ratelimit.RateLimitPolicy { - return r.InvalidAuthorizationsPerAccountPolicy -} - -func (r *dummyRateLimitConfig) CertificatesPerFQDNSet() ratelimit.RateLimitPolicy { - return r.CertificatesPerFQDNSetPolicy -} - -func (r *dummyRateLimitConfig) CertificatesPerFQDNSetFast() ratelimit.RateLimitPolicy { - return r.CertificatesPerFQDNSetFastPolicy -} - -func (r *dummyRateLimitConfig) LoadPolicies(contents []byte) error { - return nil // NOP - unrequired behaviour for this mock -} - -func parseAndMarshalIP(t *testing.T, ip string) []byte { - ipBytes, err := net.ParseIP(ip).MarshalText() - test.AssertNotError(t, err, "failed to marshal ip") - return ipBytes -} - -func newAcctKey(t *testing.T) []byte { - key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - jwk := &jose.JSONWebKey{Key: key.Public()} - acctKey, err := jwk.MarshalJSON() - test.AssertNotError(t, err, "failed to marshal account key") - return acctKey -} - -func initAuthorities(t *testing.T) (*DummyValidationAuthority, sapb.StorageAuthorityClient, *RegistrationAuthorityImpl, clock.FakeClock, func()) { +func initAuthorities(t *testing.T) (*DummyValidationAuthority, sapb.StorageAuthorityClient, *RegistrationAuthorityImpl, ratelimits.Source, clock.FakeClock, func()) { err := json.Unmarshal(AccountKeyJSONA, &AccountKeyA) test.AssertNotError(t, err, "Failed to unmarshal public JWK") err = json.Unmarshal(AccountKeyJSONB, &AccountKeyB) @@ -319,14 +318,22 @@ func initAuthorities(t *testing.T) (*DummyValidationAuthority, sapb.StorageAutho saDBCleanUp := test.ResetBoulderTestDatabase(t) - va := &DummyValidationAuthority{ - performValidationRequest: make(chan *vapb.PerformValidationRequest, 1), + dummyVA := &DummyValidationAuthority{ + doDCVRequest: make(chan *vapb.PerformValidationRequest, 1), + doCAARequest: make(chan *vapb.IsCAAValidRequest, 1), } + va := va.RemoteClients{VAClient: dummyVA, CAAClient: dummyVA} - pa, err := policy.New(map[core.AcmeChallenge]bool{ - core.ChallengeTypeHTTP01: true, - core.ChallengeTypeDNS01: true, - }, blog.NewMock()) + pa, err := policy.New( + map[identifier.IdentifierType]bool{ + identifier.TypeDNS: true, + identifier.TypeIP: true, + }, + map[core.AcmeChallenge]bool{ + core.ChallengeTypeHTTP01: true, + core.ChallengeTypeDNS01: true, + }, + 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") @@ -343,74 +350,51 @@ func initAuthorities(t *testing.T) (*DummyValidationAuthority, sapb.StorageAutho block, _ := pem.Decode(CSRPEM) ExampleCSR, _ = x509.ParseCertificateRequest(block.Bytes) - initialIP, err := net.ParseIP("3.2.3.3").MarshalText() test.AssertNotError(t, err, "Couldn't create initial IP") Registration, _ = ssa.NewRegistration(ctx, &corepb.Registration{ - Key: AccountKeyJSONA, - InitialIP: initialIP, - Status: string(core.StatusValid), + Key: AccountKeyJSONA, + Status: string(core.StatusValid), }) ctp := ctpolicy.New(&mocks.PublisherClient{}, loglist.List{ - "OperA": { - "LogA1": {Url: "UrlA1", Key: "KeyA1"}, - }, - "OperB": { - "LogB1": {Url: "UrlB1", Key: "KeyB1"}, - }, + {Name: "LogA1", Operator: "OperA", Url: "UrlA1", Key: []byte("KeyA1")}, + {Name: "LogB1", Operator: "OperB", Url: "UrlB1", Key: []byte("KeyB1")}, }, nil, nil, 0, log, metrics.NoopRegisterer) - var limiter *ratelimits.Limiter - var txnBuilder *ratelimits.TransactionBuilder - if strings.Contains(os.Getenv("BOULDER_CONFIG_DIR"), "test/config-next") { - rc := bredis.Config{ - Username: "unittest-rw", - TLS: cmd.TLSConfig{ - CACertFile: "../test/certs/ipki/minica.pem", - CertFile: "../test/certs/ipki/localhost/cert.pem", - KeyFile: "../test/certs/ipki/localhost/key.pem", - }, - Lookups: []cmd.ServiceDomain{ - { - Service: "redisratelimits", - Domain: "service.consul", - }, - }, - LookupDNSAuthority: "consul.service.consul", - } - rc.PasswordConfig = cmd.PasswordConfig{ - PasswordFile: "../test/secrets/ratelimits_redis_password", - } - ring, err := bredis.NewRingFromConfig(rc, stats, log) - test.AssertNotError(t, err, "making redis ring client") - source := ratelimits.NewRedisSource(ring.Ring, fc, stats) - test.AssertNotNil(t, source, "source should not be nil") - limiter, err = ratelimits.NewLimiter(fc, source, stats) - test.AssertNotError(t, err, "making limiter") - txnBuilder, err = ratelimits.NewTransactionBuilder("../test/config-next/wfe2-ratelimit-defaults.yml", "") - test.AssertNotError(t, err, "making transaction composer") - } + rlSource := ratelimits.NewInmemSource() + limiter, err := ratelimits.NewLimiter(fc, rlSource, stats) + test.AssertNotError(t, err, "making limiter") + txnBuilder, err := ratelimits.NewTransactionBuilderFromFiles("../test/config-next/wfe2-ratelimit-defaults.yml", "") + test.AssertNotError(t, err, "making transaction composer") testKeyPolicy, err := goodkey.NewPolicy(nil, nil) test.AssertNotError(t, err, "making keypolicy") + profiles := &validationProfiles{ + defaultName: "test", + byName: map[string]*validationProfile{"test": { + pendingAuthzLifetime: 7 * 24 * time.Hour, + validAuthzLifetime: 300 * 24 * time.Hour, + orderLifetime: 7 * 24 * time.Hour, + maxNames: 100, + identifierTypes: []identifier.IdentifierType{identifier.TypeDNS}, + }}, + } + ra := NewRegistrationAuthorityImpl( fc, log, stats, 1, testKeyPolicy, limiter, txnBuilder, 100, - 300*24*time.Hour, 7*24*time.Hour, - nil, noopCAA{}, - 0, 5*time.Minute, - ctp, nil, nil) + profiles, nil, 5*time.Minute, ctp, nil, nil) ra.SA = sa ra.VA = va ra.CA = ca ra.OCSP = &mocks.MockOCSPGenerator{} ra.PA = pa - return va, sa, ra, fc, cleanUp + return dummyVA, sa, ra, rlSource, fc, cleanUp } func TestValidateContacts(t *testing.T) { - _, _, ra, _, cleanUp := initAuthorities(t) + _, _, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() ansible := "ansible:earth.sol.milkyway.laniakea/letsencrypt" @@ -487,16 +471,14 @@ func TestValidateContacts(t *testing.T) { } func TestNewRegistration(t *testing.T) { - _, sa, ra, _, cleanUp := initAuthorities(t) + _, sa, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() mailto := "mailto:foo@letsencrypt.org" acctKeyB, err := AccountKeyB.MarshalJSON() test.AssertNotError(t, err, "failed to marshal account key") input := &corepb.Registration{ - Contact: []string{mailto}, - ContactsPresent: true, - Key: acctKeyB, - InitialIP: parseAndMarshalIP(t, "7.6.6.5"), + Contact: []string{mailto}, + Key: acctKeyB, } result, err := ra.NewRegistration(ctx, input) @@ -504,8 +486,7 @@ func TestNewRegistration(t *testing.T) { t.Fatalf("could not create new registration: %s", err) } test.AssertByteEquals(t, result.Key, acctKeyB) - test.Assert(t, len(result.Contact) == 1, "Wrong number of contacts") - test.Assert(t, mailto == (result.Contact)[0], "Contact didn't match") + test.Assert(t, len(result.Contact) == 0, "Wrong number of contacts") test.Assert(t, result.Agreement == "", "Agreement didn't default empty") reg, err := sa.GetRegistration(ctx, &sapb.RegistrationID{Id: result.Id}) @@ -513,70 +494,6 @@ func TestNewRegistration(t *testing.T) { test.AssertByteEquals(t, reg.Key, acctKeyB) } -func TestNewRegistrationContactsPresent(t *testing.T) { - _, _, ra, _, cleanUp := initAuthorities(t) - defer cleanUp() - testCases := []struct { - Name string - Reg *corepb.Registration - ExpectedErr error - }{ - { - Name: "No contacts provided by client ContactsPresent false", - Reg: &corepb.Registration{ - Key: newAcctKey(t), - InitialIP: parseAndMarshalIP(t, "7.6.6.5"), - }, - ExpectedErr: nil, - }, - { - Name: "Empty contact provided by client ContactsPresent true", - Reg: &corepb.Registration{ - Contact: []string{}, - ContactsPresent: true, - Key: newAcctKey(t), - InitialIP: parseAndMarshalIP(t, "7.6.6.4"), - }, - ExpectedErr: nil, - }, - { - Name: "Valid contact provided by client ContactsPresent true", - Reg: &corepb.Registration{ - Contact: []string{"mailto:foo@letsencrypt.org"}, - ContactsPresent: true, - Key: newAcctKey(t), - InitialIP: parseAndMarshalIP(t, "7.6.4.3"), - }, - ExpectedErr: nil, - }, - { - Name: "Valid contact provided by client ContactsPresent false", - Reg: &corepb.Registration{ - Contact: []string{"mailto:foo@letsencrypt.org"}, - ContactsPresent: false, - Key: newAcctKey(t), - InitialIP: parseAndMarshalIP(t, "7.6.6.2"), - }, - ExpectedErr: fmt.Errorf("account contacts present but contactsPresent false"), - }, - } - // For each test case we check that the NewRegistration works as - // intended with variations of Contact and ContactsPresent fields - for _, tc := range testCases { - t.Run(tc.Name, func(t *testing.T) { - // Create new registration - _, err := ra.NewRegistration(ctx, tc.Reg) - // Check error output - if tc.ExpectedErr == nil { - test.AssertNotError(t, err, "expected no error for NewRegistration") - } else { - test.AssertError(t, err, "expected error for NewRegistration") - test.AssertEquals(t, err.Error(), tc.ExpectedErr.Error()) - } - }) - } -} - type mockSAFailsNewRegistration struct { sapb.StorageAuthorityClient } @@ -586,16 +503,14 @@ func (sa *mockSAFailsNewRegistration) NewRegistration(_ context.Context, _ *core } func TestNewRegistrationSAFailure(t *testing.T) { - _, _, ra, _, cleanUp := initAuthorities(t) + _, _, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() ra.SA = &mockSAFailsNewRegistration{} acctKeyB, err := AccountKeyB.MarshalJSON() test.AssertNotError(t, err, "failed to marshal account key") input := corepb.Registration{ - Contact: []string{"mailto:test@example.com"}, - ContactsPresent: true, - Key: acctKeyB, - InitialIP: parseAndMarshalIP(t, "7.6.6.5"), + Contact: []string{"mailto:test@example.com"}, + Key: acctKeyB, } result, err := ra.NewRegistration(ctx, &input) if err == nil { @@ -604,18 +519,16 @@ func TestNewRegistrationSAFailure(t *testing.T) { } func TestNewRegistrationNoFieldOverwrite(t *testing.T) { - _, _, ra, _, cleanUp := initAuthorities(t) + _, _, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() mailto := "mailto:foo@letsencrypt.org" acctKeyC, err := AccountKeyC.MarshalJSON() test.AssertNotError(t, err, "failed to marshal account key") input := &corepb.Registration{ - Id: 23, - Key: acctKeyC, - Contact: []string{mailto}, - ContactsPresent: true, - Agreement: "I agreed", - InitialIP: parseAndMarshalIP(t, "5.0.5.0"), + Id: 23, + Key: acctKeyC, + Contact: []string{mailto}, + Agreement: "I agreed", } result, err := ra.NewRegistration(ctx, input) @@ -626,193 +539,24 @@ func TestNewRegistrationNoFieldOverwrite(t *testing.T) { } func TestNewRegistrationBadKey(t *testing.T) { - _, _, ra, _, cleanUp := initAuthorities(t) + _, _, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() mailto := "mailto:foo@letsencrypt.org" shortKey, err := ShortKey.MarshalJSON() test.AssertNotError(t, err, "failed to marshal account key") input := &corepb.Registration{ - Contact: []string{mailto}, - ContactsPresent: true, - Key: shortKey, + Contact: []string{mailto}, + Key: shortKey, } _, err = ra.NewRegistration(ctx, input) test.AssertError(t, err, "Should have rejected authorization with short key") } -func TestNewRegistrationRateLimit(t *testing.T) { - _, _, ra, _, cleanUp := initAuthorities(t) - defer cleanUp() - - // Specify a dummy rate limit policy that allows 1 registration per exact IP - // match, and 2 per range. - ra.rlPolicies = &dummyRateLimitConfig{ - RegistrationsPerIPPolicy: ratelimit.RateLimitPolicy{ - Threshold: 1, - Window: config.Duration{Duration: 24 * 90 * time.Hour}, - }, - RegistrationsPerIPRangePolicy: ratelimit.RateLimitPolicy{ - Threshold: 2, - Window: config.Duration{Duration: 24 * 90 * time.Hour}, - }, - } - - // Create one registration for an IPv4 address - mailto := "mailto:foo@letsencrypt.org" - reg := &corepb.Registration{ - Contact: []string{mailto}, - ContactsPresent: true, - Key: newAcctKey(t), - InitialIP: parseAndMarshalIP(t, "7.6.6.5"), - } - // There should be no errors - it is within the RegistrationsPerIP rate limit - _, err := ra.NewRegistration(ctx, reg) - test.AssertNotError(t, err, "Unexpected error adding new IPv4 registration") - test.AssertMetricWithLabelsEquals(t, ra.rlCheckLatency, prometheus.Labels{"limit": ratelimit.RegistrationsPerIP, "decision": ratelimits.Allowed}, 1) - // There are no overrides for this IP, so the override usage gauge should - // contain 0 entries with labels matching it. - test.AssertMetricWithLabelsEquals(t, ra.rlOverrideUsageGauge, prometheus.Labels{"limit": ratelimit.RegistrationsPerIP, "override_key": "7.6.6.5"}, 0) - - // Create another registration for the same IPv4 address by changing the key - reg.Key = newAcctKey(t) - - // There should be an error since a 2nd registration will exceed the - // RegistrationsPerIP rate limit - _, err = ra.NewRegistration(ctx, reg) - test.AssertError(t, err, "No error adding duplicate IPv4 registration") - test.AssertEquals(t, err.Error(), "too many registrations for this IP: see https://letsencrypt.org/docs/too-many-registrations-for-this-ip/") - test.AssertMetricWithLabelsEquals(t, ra.rlCheckLatency, prometheus.Labels{"limit": ratelimit.RegistrationsPerIP, "decision": ratelimits.Denied}, 1) - - // Create a registration for an IPv6 address - reg.Key = newAcctKey(t) - reg.InitialIP = parseAndMarshalIP(t, "2001:cdba:1234:5678:9101:1121:3257:9652") - - // There should be no errors - it is within the RegistrationsPerIP rate limit - _, err = ra.NewRegistration(ctx, reg) - test.AssertNotError(t, err, "Unexpected error adding a new IPv6 registration") - test.AssertMetricWithLabelsEquals(t, ra.rlCheckLatency, prometheus.Labels{"limit": ratelimit.RegistrationsPerIP, "decision": ratelimits.Allowed}, 2) - - // Create a 2nd registration for the IPv6 address by changing the key - reg.Key = newAcctKey(t) - - // There should be an error since a 2nd reg for the same IPv6 address will - // exceed the RegistrationsPerIP rate limit - _, err = ra.NewRegistration(ctx, reg) - test.AssertError(t, err, "No error adding duplicate IPv6 registration") - test.AssertEquals(t, err.Error(), "too many registrations for this IP: see https://letsencrypt.org/docs/too-many-registrations-for-this-ip/") - test.AssertMetricWithLabelsEquals(t, ra.rlCheckLatency, prometheus.Labels{"limit": ratelimit.RegistrationsPerIP, "decision": ratelimits.Denied}, 2) - - // Create a registration for an IPv6 address in the same /48 - reg.Key = newAcctKey(t) - reg.InitialIP = parseAndMarshalIP(t, "2001:cdba:1234:5678:9101:1121:3257:9653") - - // There should be no errors since two IPv6 addresses in the same /48 is - // within the RegistrationsPerIPRange limit - _, err = ra.NewRegistration(ctx, reg) - test.AssertNotError(t, err, "Unexpected error adding second IPv6 registration in the same /48") - test.AssertMetricWithLabelsEquals(t, ra.rlCheckLatency, prometheus.Labels{"limit": ratelimit.RegistrationsPerIPRange, "decision": ratelimits.Allowed}, 2) - - // Create a registration for yet another IPv6 address in the same /48 - reg.Key = newAcctKey(t) - reg.InitialIP = parseAndMarshalIP(t, "2001:cdba:1234:5678:9101:1121:3257:9654") - - // There should be an error since three registrations within the same IPv6 - // /48 is outside of the RegistrationsPerIPRange limit - _, err = ra.NewRegistration(ctx, reg) - test.AssertError(t, err, "No error adding a third IPv6 registration in the same /48") - test.AssertEquals(t, err.Error(), "too many registrations for this IP range: see https://letsencrypt.org/docs/rate-limits/") - test.AssertMetricWithLabelsEquals(t, ra.rlCheckLatency, prometheus.Labels{"limit": ratelimit.RegistrationsPerIPRange, "decision": ratelimits.Denied}, 1) -} - -func TestRegistrationsPerIPOverrideUsage(t *testing.T) { - _, _, ra, _, cleanUp := initAuthorities(t) - defer cleanUp() - - regIP := net.ParseIP("4.5.6.7") - rlp := ratelimit.RateLimitPolicy{ - Threshold: 2, - Window: config.Duration{Duration: 23 * time.Hour}, - Overrides: map[string]int64{ - regIP.String(): 3, - }, - } - - mockCounterAlwaysTwo := func(context.Context, *sapb.CountRegistrationsByIPRequest, ...grpc.CallOption) (*sapb.Count, error) { - return &sapb.Count{Count: 2}, nil - } - - // No error expected, the count of existing registrations for "4.5.6.7" - // should be 1 below the override threshold. - err := ra.checkRegistrationIPLimit(ctx, rlp, regIP, mockCounterAlwaysTwo) - test.AssertNotError(t, err, "Unexpected error checking RegistrationsPerIPRange limit") - - // Accounting for the anticipated issuance, we expect "4.5.6.7" to be at - // 100% of their override threshold. - test.AssertMetricWithLabelsEquals(t, ra.rlOverrideUsageGauge, prometheus.Labels{"limit": ratelimit.RegistrationsPerIP, "override_key": regIP.String()}, 1) - - mockCounterAlwaysThree := func(context.Context, *sapb.CountRegistrationsByIPRequest, ...grpc.CallOption) (*sapb.Count, error) { - return &sapb.Count{Count: 3}, nil - } - - // Error expected, the count of existing registrations for "4.5.6.7" should - // be exactly at the threshold. - err = ra.checkRegistrationIPLimit(ctx, rlp, regIP, mockCounterAlwaysThree) - test.AssertError(t, err, "Expected error checking RegistrationsPerIPRange limit") - - // Expecting 100% of the override for "4.5.6.7" to be utilized. - test.AssertMetricWithLabelsEquals(t, ra.rlOverrideUsageGauge, prometheus.Labels{"limit": ratelimit.RegistrationsPerIP, "override_key": regIP.String()}, 1) -} - -type NoUpdateSA struct { - sapb.StorageAuthorityClient -} - -func (sa NoUpdateSA) UpdateRegistration(_ context.Context, _ *corepb.Registration, _ ...grpc.CallOption) (*emptypb.Empty, error) { - return nil, fmt.Errorf("UpdateRegistration() is mocked to always error") -} - -func TestUpdateRegistrationSame(t *testing.T) { - _, _, ra, _, cleanUp := initAuthorities(t) - defer cleanUp() - mailto := "mailto:foo@letsencrypt.org" - - // Make a new registration with AccountKeyC and a Contact - acctKeyC, err := AccountKeyC.MarshalJSON() - test.AssertNotError(t, err, "failed to marshal account key") - reg := &corepb.Registration{ - Key: acctKeyC, - Contact: []string{mailto}, - ContactsPresent: true, - Agreement: "I agreed", - InitialIP: parseAndMarshalIP(t, "5.0.5.0"), - } - result, err := ra.NewRegistration(ctx, reg) - test.AssertNotError(t, err, "Could not create new registration") - - // Switch to a mock SA that will always error if UpdateRegistration() is called - ra.SA = &NoUpdateSA{} - - // Make an update to the registration with the same Contact & Agreement values. - updateSame := &corepb.Registration{ - Id: result.Id, - Key: acctKeyC, - Contact: []string{mailto}, - ContactsPresent: true, - Agreement: "I agreed", - } - - // The update operation should *not* error, even with the NoUpdateSA because - // UpdateRegistration() should not be called when the update content doesn't - // actually differ from the existing content - _, err = ra.UpdateRegistration(ctx, &rapb.UpdateRegistrationRequest{Base: result, Update: updateSame}) - test.AssertNotError(t, err, "Error updating registration") -} - func TestPerformValidationExpired(t *testing.T) { - _, sa, ra, fc, cleanUp := initAuthorities(t) + _, sa, ra, _, fc, cleanUp := initAuthorities(t) defer cleanUp() - authz := createPendingAuthorization(t, sa, Identifier, fc.Now().Add(-2*time.Hour)) + authz := createPendingAuthorization(t, sa, identifier.NewDNS("example.com"), fc.Now().Add(-2*time.Hour)) _, err := ra.PerformValidation(ctx, &rapb.PerformValidationRequest{ Authz: authz, @@ -822,14 +566,14 @@ func TestPerformValidationExpired(t *testing.T) { } func TestPerformValidationAlreadyValid(t *testing.T) { - va, _, ra, _, cleanUp := initAuthorities(t) + va, _, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() // Create a finalized authorization exp := ra.clk.Now().Add(365 * 24 * time.Hour) authz := core.Authorization{ ID: "1337", - Identifier: identifier.DNSIdentifier("not-example.com"), + Identifier: identifier.NewDNS("not-example.com"), RegistrationID: 1, Status: "valid", Expires: &exp, @@ -844,7 +588,7 @@ func TestPerformValidationAlreadyValid(t *testing.T) { authzPB, err := bgrpc.AuthzToPB(authz) test.AssertNotError(t, err, "bgrpc.AuthzToPB failed") - va.PerformValidationRequestResultReturn = &vapb.ValidationResult{ + va.doDCVResult = &vapb.ValidationResult{ Records: []*corepb.ValidationRecord{ { AddressUsed: []byte("192.168.0.1"), @@ -853,8 +597,9 @@ func TestPerformValidationAlreadyValid(t *testing.T) { Url: "http://example.com/", }, }, - Problems: nil, + Problem: nil, } + va.doCAAResponse = &vapb.IsCAAValidResponse{Problem: nil} // A subsequent call to perform validation should return nil due // to being short-circuited because of valid authz reuse. @@ -867,108 +612,243 @@ func TestPerformValidationAlreadyValid(t *testing.T) { } func TestPerformValidationSuccess(t *testing.T) { - va, sa, ra, fc, cleanUp := initAuthorities(t) + va, sa, ra, _, fc, cleanUp := initAuthorities(t) defer cleanUp() - // We know this is OK because of TestNewAuthorization - authzPB := createPendingAuthorization(t, sa, Identifier, fc.Now().Add(12*time.Hour)) + idents := identifier.ACMEIdentifiers{ + identifier.NewDNS("example.com"), + identifier.NewIP(netip.MustParseAddr("192.168.0.1")), + } - va.PerformValidationRequestResultReturn = &vapb.ValidationResult{ - Records: []*corepb.ValidationRecord{ - { - AddressUsed: []byte("192.168.0.1"), - Hostname: "example.com", - Port: "8080", - Url: "http://example.com/", - ResolverAddrs: []string{"rebound"}, + for _, ident := range idents { + // We know this is OK because of TestNewAuthorization + authzPB := createPendingAuthorization(t, sa, ident, fc.Now().Add(12*time.Hour)) + + va.doDCVResult = &vapb.ValidationResult{ + Records: []*corepb.ValidationRecord{ + { + AddressUsed: []byte("192.168.0.1"), + Hostname: "example.com", + Port: "8080", + Url: "http://example.com/", + ResolverAddrs: []string{"rebound"}, + }, }, - }, - Problems: nil, - } + Problem: nil, + } + va.doCAAResponse = &vapb.IsCAAValidResponse{Problem: nil} - var remainingFailedValidations int64 - var rlTxns []ratelimits.Transaction - if strings.Contains(os.Getenv("BOULDER_CONFIG_DIR"), "test/config-next") { - // Gather a baseline for the rate limit. - var err error - rlTxns, err = ra.txnBuilder.FailedAuthorizationsPerDomainPerAccountCheckOnlyTransactions(authzPB.RegistrationID, []string{Identifier}, 100) - test.AssertNotError(t, err, "FailedAuthorizationsPerDomainPerAccountCheckOnlyTransactions failed") + now := fc.Now() + challIdx := dnsChallIdx(t, authzPB.Challenges) + authzPB, err := ra.PerformValidation(ctx, &rapb.PerformValidationRequest{ + Authz: authzPB, + ChallengeIndex: challIdx, + }) + test.AssertNotError(t, err, "PerformValidation failed") - d, err := ra.limiter.BatchSpend(ctx, rlTxns) - test.AssertNotError(t, err, "BatchSpend failed") - remainingFailedValidations = d.Remaining - } + var vaRequest *vapb.PerformValidationRequest + select { + case r := <-va.doDCVRequest: + vaRequest = r + case <-time.After(time.Second): + t.Fatal("Timed out waiting for DummyValidationAuthority.PerformValidation to complete") + } - now := fc.Now() - challIdx := dnsChallIdx(t, authzPB.Challenges) - authzPB, err := ra.PerformValidation(ctx, &rapb.PerformValidationRequest{ - Authz: authzPB, - ChallengeIndex: challIdx, - }) - test.AssertNotError(t, err, "PerformValidation failed") + // Verify that the VA got the request, and it's the same as the others + test.AssertEquals(t, authzPB.Challenges[challIdx].Type, vaRequest.Challenge.Type) + test.AssertEquals(t, authzPB.Challenges[challIdx].Token, vaRequest.Challenge.Token) - var vaRequest *vapb.PerformValidationRequest - select { - case r := <-va.performValidationRequest: - vaRequest = r - case <-time.After(time.Second): - t.Fatal("Timed out waiting for DummyValidationAuthority.PerformValidation to complete") - } + // Sleep so the RA has a chance to write to the SA + time.Sleep(100 * time.Millisecond) - // Verify that the VA got the request, and it's the same as the others - test.AssertEquals(t, authzPB.Challenges[challIdx].Type, vaRequest.Challenge.Type) - test.AssertEquals(t, authzPB.Challenges[challIdx].Token, vaRequest.Challenge.Token) + dbAuthzPB := getAuthorization(t, authzPB.Id, sa) + t.Log("dbAuthz:", dbAuthzPB) - // Sleep so the RA has a chance to write to the SA - time.Sleep(100 * time.Millisecond) + // Verify that the responses are reflected + challIdx = dnsChallIdx(t, dbAuthzPB.Challenges) + challenge, err := bgrpc.PBToChallenge(dbAuthzPB.Challenges[challIdx]) + test.AssertNotError(t, err, "Failed to marshall corepb.Challenge to core.Challenge.") - dbAuthzPB := getAuthorization(t, authzPB.Id, sa) - t.Log("dbAuthz:", dbAuthzPB) + test.AssertNotNil(t, vaRequest.Challenge, "Request passed to VA has no challenge") + test.Assert(t, challenge.Status == core.StatusValid, "challenge was not marked as valid") - // Verify that the responses are reflected - challIdx = dnsChallIdx(t, dbAuthzPB.Challenges) - challenge, err := bgrpc.PBToChallenge(dbAuthzPB.Challenges[challIdx]) - test.AssertNotError(t, err, "Failed to marshall corepb.Challenge to core.Challenge.") + // The DB authz's expiry should be equal to the current time plus the + // configured authorization lifetime + test.AssertEquals(t, dbAuthzPB.Expires.AsTime(), now.Add(ra.profiles.def().validAuthzLifetime)) - test.AssertNotNil(t, vaRequest.Challenge, "Request passed to VA has no challenge") - test.Assert(t, challenge.Status == core.StatusValid, "challenge was not marked as valid") - - // The DB authz's expiry should be equal to the current time plus the - // configured authorization lifetime - test.AssertEquals(t, dbAuthzPB.Expires.AsTime(), now.Add(ra.authorizationLifetime)) - - // Check that validated timestamp was recorded, stored, and retrieved - expectedValidated := fc.Now() - test.Assert(t, *challenge.Validated == expectedValidated, "Validated timestamp incorrect or missing") - - if strings.Contains(os.Getenv("BOULDER_CONFIG_DIR"), "test/config-next") { - // The failed validations bucket should be identical to the baseline. - d, err := ra.limiter.BatchSpend(ctx, rlTxns) - test.AssertNotError(t, err, "BatchSpend failed") - test.AssertEquals(t, d.Remaining, remainingFailedValidations) + // Check that validated timestamp was recorded, stored, and retrieved + expectedValidated := fc.Now() + test.Assert(t, *challenge.Validated == expectedValidated, "Validated timestamp incorrect or missing") } } -func TestPerformValidationVAError(t *testing.T) { - va, sa, ra, fc, cleanUp := initAuthorities(t) +// mockSAWithSyncPause is a mock sapb.StorageAuthorityClient that forwards all +// method calls to an inner SA, but also performs a blocking write to a channel +// when PauseIdentifiers is called to allow the tests to synchronize. +type mockSAWithSyncPause struct { + sapb.StorageAuthorityClient + out chan<- *sapb.PauseRequest +} + +func (msa mockSAWithSyncPause) PauseIdentifiers(ctx context.Context, req *sapb.PauseRequest, _ ...grpc.CallOption) (*sapb.PauseIdentifiersResponse, error) { + res, err := msa.StorageAuthorityClient.PauseIdentifiers(ctx, req) + msa.out <- req + return res, err +} + +func TestPerformValidation_FailedValidationsTriggerPauseIdentifiersRatelimit(t *testing.T) { + va, sa, ra, rl, fc, cleanUp := initAuthorities(t) defer cleanUp() - authzPB := createPendingAuthorization(t, sa, Identifier, fc.Now().Add(12*time.Hour)) + features.Set(features.Config{AutomaticallyPauseZombieClients: true}) + defer features.Reset() - var remainingFailedValidations int64 - var rlTxns []ratelimits.Transaction - if strings.Contains(os.Getenv("BOULDER_CONFIG_DIR"), "test/config-next") { - // Gather a baseline for the rate limit. - var err error - rlTxns, err = ra.txnBuilder.FailedAuthorizationsPerDomainPerAccountCheckOnlyTransactions(authzPB.RegistrationID, []string{Identifier}, 100) - test.AssertNotError(t, err, "FailedAuthorizationsPerDomainPerAccountCheckOnlyTransactions failed") - - d, err := ra.limiter.BatchSpend(ctx, rlTxns) - test.AssertNotError(t, err, "BatchSpend failed") - remainingFailedValidations = d.Remaining + // Replace the SA with one that will block when PauseIdentifiers is called. + pauseChan := make(chan *sapb.PauseRequest) + defer close(pauseChan) + ra.SA = mockSAWithSyncPause{ + StorageAuthorityClient: ra.SA, + out: pauseChan, } - va.PerformValidationRequestResultError = fmt.Errorf("Something went wrong") + // Set the default ratelimits to only allow one failed validation per 24 + // hours before pausing. + txnBuilder, err := ratelimits.NewTransactionBuilder(ratelimits.LimitConfigs{ + ratelimits.FailedAuthorizationsForPausingPerDomainPerAccount.String(): &ratelimits.LimitConfig{ + Burst: 1, + Count: 1, + Period: config.Duration{Duration: time.Hour * 24}}, + }) + test.AssertNotError(t, err, "making transaction composer") + ra.txnBuilder = txnBuilder + + // Set up a fake domain, authz, and bucket key to care about. + domain := randomDomain() + ident := identifier.NewDNS(domain) + authzPB := createPendingAuthorization(t, sa, ident, fc.Now().Add(12*time.Hour)) + bucketKey := ratelimits.NewRegIdIdentValueBucketKey(ratelimits.FailedAuthorizationsForPausingPerDomainPerAccount, authzPB.RegistrationID, ident.Value) + + // Set the stored TAT to indicate that this bucket has exhausted its quota. + err = rl.BatchSet(context.Background(), map[string]time.Time{ + bucketKey: fc.Now().Add(25 * time.Hour), + }) + test.AssertNotError(t, err, "updating rate limit bucket") + + // Now a failed validation should result in the identifier being paused + // due to the strict ratelimit. + va.doDCVResult = &vapb.ValidationResult{ + Records: []*corepb.ValidationRecord{ + { + AddressUsed: []byte("192.168.0.1"), + Hostname: domain, + Port: "8080", + Url: fmt.Sprintf("http://%s/", domain), + ResolverAddrs: []string{"rebound"}, + }, + }, + Problem: nil, + } + va.doCAAResponse = &vapb.IsCAAValidResponse{ + Problem: &corepb.ProblemDetails{ + Detail: fmt.Sprintf("CAA invalid for %s", domain), + }, + } + + _, err = ra.PerformValidation(ctx, &rapb.PerformValidationRequest{ + Authz: authzPB, + ChallengeIndex: dnsChallIdx(t, authzPB.Challenges), + }) + test.AssertNotError(t, err, "PerformValidation failed") + + // Wait for the RA to finish processing the validation, and ensure that the paused + // account+identifier is what we expect. + paused := <-pauseChan + test.AssertEquals(t, len(paused.Identifiers), 1) + test.AssertEquals(t, paused.Identifiers[0].Value, domain) +} + +// mockRLSourceWithSyncDelete is a mock ratelimits.Source that forwards all +// method calls to an inner Source, but also performs a blocking write to a +// channel when Delete is called to allow the tests to synchronize. +type mockRLSourceWithSyncDelete struct { + ratelimits.Source + out chan<- string +} + +func (rl mockRLSourceWithSyncDelete) Delete(ctx context.Context, bucketKey string) error { + err := rl.Source.Delete(ctx, bucketKey) + rl.out <- bucketKey + return err +} + +func TestPerformValidation_FailedThenSuccessfulValidationResetsPauseIdentifiersRatelimit(t *testing.T) { + va, sa, ra, rl, fc, cleanUp := initAuthorities(t) + defer cleanUp() + + features.Set(features.Config{AutomaticallyPauseZombieClients: true}) + defer features.Reset() + + // Replace the rate limit source with one that will block when Delete is called. + keyChan := make(chan string) + defer close(keyChan) + limiter, err := ratelimits.NewLimiter(fc, mockRLSourceWithSyncDelete{ + Source: rl, + out: keyChan, + }, metrics.NoopRegisterer) + test.AssertNotError(t, err, "creating mock limiter") + ra.limiter = limiter + + // Set up a fake domain, authz, and bucket key to care about. + domain := randomDomain() + ident := identifier.NewDNS(domain) + authzPB := createPendingAuthorization(t, sa, ident, fc.Now().Add(12*time.Hour)) + bucketKey := ratelimits.NewRegIdIdentValueBucketKey(ratelimits.FailedAuthorizationsForPausingPerDomainPerAccount, authzPB.RegistrationID, ident.Value) + + // Set a stored TAT so that we can tell when it's been reset. + err = rl.BatchSet(context.Background(), map[string]time.Time{ + bucketKey: fc.Now().Add(25 * time.Hour), + }) + test.AssertNotError(t, err, "updating rate limit bucket") + + va.doDCVResult = &vapb.ValidationResult{ + Records: []*corepb.ValidationRecord{ + { + AddressUsed: []byte("192.168.0.1"), + Hostname: domain, + Port: "8080", + Url: fmt.Sprintf("http://%s/", domain), + ResolverAddrs: []string{"rebound"}, + }, + }, + Problem: nil, + } + va.doCAAResponse = &vapb.IsCAAValidResponse{Problem: nil} + + _, err = ra.PerformValidation(ctx, &rapb.PerformValidationRequest{ + Authz: authzPB, + ChallengeIndex: dnsChallIdx(t, authzPB.Challenges), + }) + test.AssertNotError(t, err, "PerformValidation failed") + + // Wait for the RA to finish processesing the validation, and ensure that + // the reset bucket key is what we expect. + reset := <-keyChan + test.AssertEquals(t, reset, bucketKey) + + // Verify that the bucket no longer exists (because the limiter reset has + // deleted it). This indicates the accountID:identifier bucket has regained + // capacity avoiding being inadvertently paused. + _, err = rl.Get(ctx, bucketKey) + test.AssertErrorIs(t, err, ratelimits.ErrBucketNotFound) +} + +func TestPerformValidationVAError(t *testing.T) { + va, sa, ra, _, fc, cleanUp := initAuthorities(t) + defer cleanUp() + + authzPB := createPendingAuthorization(t, sa, identifier.NewDNS("example.com"), fc.Now().Add(12*time.Hour)) + + va.doDCVError = fmt.Errorf("Something went wrong") challIdx := dnsChallIdx(t, authzPB.Challenges) authzPB, err := ra.PerformValidation(ctx, &rapb.PerformValidationRequest{ @@ -980,7 +860,7 @@ func TestPerformValidationVAError(t *testing.T) { var vaRequest *vapb.PerformValidationRequest select { - case r := <-va.performValidationRequest: + case r := <-va.doDCVRequest: vaRequest = r case <-time.After(time.Second): t.Fatal("Timed out waiting for DummyValidationAuthority.PerformValidation to complete") @@ -1001,34 +881,27 @@ func TestPerformValidationVAError(t *testing.T) { challenge, err := bgrpc.PBToChallenge(dbAuthzPB.Challenges[challIdx]) test.AssertNotError(t, err, "Failed to marshall corepb.Challenge to core.Challenge.") test.Assert(t, challenge.Status == core.StatusInvalid, "challenge was not marked as invalid") - test.AssertContains(t, challenge.Error.Error(), "Could not communicate with VA") + test.AssertContains(t, challenge.Error.String(), "Could not communicate with VA") test.Assert(t, challenge.ValidationRecord == nil, "challenge had a ValidationRecord") // Check that validated timestamp was recorded, stored, and retrieved expectedValidated := fc.Now() test.Assert(t, *challenge.Validated == expectedValidated, "Validated timestamp incorrect or missing") - - if strings.Contains(os.Getenv("BOULDER_CONFIG_DIR"), "test/config-next") { - // The failed validations bucket should have been decremented by 1. - d, err := ra.limiter.BatchSpend(ctx, rlTxns) - test.AssertNotError(t, err, "BatchSpend failed") - test.AssertEquals(t, d.Remaining, remainingFailedValidations-1) - } } func TestCertificateKeyNotEqualAccountKey(t *testing.T) { - _, sa, ra, _, cleanUp := initAuthorities(t) + _, sa, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() exp := ra.clk.Now().Add(365 * 24 * time.Hour) - authzID := createFinalizedAuthorization(t, sa, "www.example.com", exp, core.ChallengeTypeHTTP01, ra.clk.Now()) + authzID := createFinalizedAuthorization(t, sa, identifier.NewDNS("www.example.com"), exp, core.ChallengeTypeHTTP01, ra.clk.Now()) order, err := sa.NewOrderAndAuthzs(context.Background(), &sapb.NewOrderAndAuthzsRequest{ NewOrder: &sapb.NewOrderRequest{ RegistrationID: Registration.Id, Expires: timestamppb.New(exp), - Names: []string{"www.example.com"}, + Identifiers: []*corepb.Identifier{identifier.NewDNS("www.example.com").ToProto()}, V2Authorizations: []int64{authzID}, }, }) @@ -1045,7 +918,7 @@ func TestCertificateKeyNotEqualAccountKey(t *testing.T) { _, err = ra.FinalizeOrder(ctx, &rapb.FinalizeOrderRequest{ Order: &corepb.Order{ Status: string(core.StatusReady), - Names: []string{"www.example.com"}, + Identifiers: []*corepb.Identifier{identifier.NewDNS("www.example.com").ToProto()}, Id: order.Id, RegistrationID: Registration.Id, }, @@ -1055,644 +928,12 @@ func TestCertificateKeyNotEqualAccountKey(t *testing.T) { test.AssertEquals(t, err.Error(), "certificate public key must be different than account key") } -func TestNewOrderRateLimiting(t *testing.T) { - _, sa, ra, fc, cleanUp := initAuthorities(t) - defer cleanUp() - - ra.orderLifetime = 5 * 24 * time.Hour - - // Create a dummy rate limit config that sets a NewOrdersPerAccount rate - // limit with a very low threshold/short window - rateLimitDuration := 5 * time.Minute - ra.rlPolicies = &dummyRateLimitConfig{ - NewOrdersPerAccountPolicy: ratelimit.RateLimitPolicy{ - Threshold: 1, - Window: config.Duration{Duration: rateLimitDuration}, - }, - } - - orderOne := &rapb.NewOrderRequest{ - RegistrationID: Registration.Id, - Names: []string{"first.example.com"}, - } - orderTwo := &rapb.NewOrderRequest{ - RegistrationID: Registration.Id, - Names: []string{"second.example.com"}, - } - - // To start, it should be possible to create a new order - _, err := ra.NewOrder(ctx, orderOne) - test.AssertNotError(t, err, "NewOrder for orderOne failed") - - // Advance the clock 1s to separate the orders in time - fc.Add(time.Second) - - // Creating an order immediately after the first with different names - // should fail - _, err = ra.NewOrder(ctx, orderTwo) - test.AssertError(t, err, "NewOrder for orderTwo succeeded, should have been ratelimited") - - // Creating the first order again should succeed because of order reuse, no - // new pending order is produced. - _, err = ra.NewOrder(ctx, orderOne) - test.AssertNotError(t, err, "Reuse of orderOne failed") - - // Insert a specific certificate into the database, then create an order for - // the same set of names. This order should succeed because it's a renewal. - testKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - test.AssertNotError(t, err, "generating test key") - fakeCert := &x509.Certificate{ - SerialNumber: big.NewInt(1), - DNSNames: []string{"renewing.example.com"}, - NotBefore: fc.Now().Add(-time.Hour), - NotAfter: fc.Now().Add(time.Hour), - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, - } - certDER, err := x509.CreateCertificate(rand.Reader, fakeCert, fakeCert, testKey.Public(), testKey) - test.AssertNotError(t, err, "generating test certificate") - _, err = sa.AddCertificate(ctx, &sapb.AddCertificateRequest{ - Der: certDER, - RegID: Registration.Id, - Issued: timestamppb.New(fc.Now().Add(-time.Hour)), - IssuerNameID: 1, - }) - test.AssertNotError(t, err, "Adding test certificate") - - _, err = ra.NewOrder(ctx, &rapb.NewOrderRequest{ - RegistrationID: Registration.Id, - Names: []string{"renewing.example.com"}, - }) - test.AssertNotError(t, err, "Renewal of orderRenewal failed") - - // Advancing the clock by 2 * the rate limit duration should allow orderTwo to - // succeed - fc.Add(2 * rateLimitDuration) - _, err = ra.NewOrder(ctx, orderTwo) - test.AssertNotError(t, err, "NewOrder for orderTwo failed after advancing clock") -} - -// TestEarlyOrderRateLimiting tests that NewOrder applies the certificates per -// name/per FQDN rate limits against the order names. -func TestEarlyOrderRateLimiting(t *testing.T) { - _, _, ra, _, cleanUp := initAuthorities(t) - defer cleanUp() - ra.orderLifetime = 5 * 24 * time.Hour - - rateLimitDuration := 5 * time.Minute - - domain := "early-ratelimit-example.com" - - // Set a mock RL policy with a CertificatesPerName threshold for the domain - // name so low if it were enforced it would prevent a new order for any names. - ra.rlPolicies = &dummyRateLimitConfig{ - CertificatesPerNamePolicy: ratelimit.RateLimitPolicy{ - Threshold: 10, - Window: config.Duration{Duration: rateLimitDuration}, - // Setting the Threshold to 0 skips applying the rate limit. Setting an - // override to 0 does the trick. - Overrides: map[string]int64{ - domain: 0, - }, - }, - NewOrdersPerAccountPolicy: ratelimit.RateLimitPolicy{ - Threshold: 10, - Window: config.Duration{Duration: rateLimitDuration}, - }, - } - - // Request an order for the test domain - newOrder := &rapb.NewOrderRequest{ - RegistrationID: Registration.Id, - Names: []string{domain}, - } - - // With the feature flag enabled the NewOrder request should fail because of - // the CertificatesPerNamePolicy. - _, err := ra.NewOrder(ctx, newOrder) - test.AssertError(t, err, "NewOrder did not apply cert rate limits with feature flag enabled") - - var bErr *berrors.BoulderError - test.Assert(t, errors.As(err, &bErr), "NewOrder did not return a boulder error") - test.AssertEquals(t, bErr.RetryAfter, rateLimitDuration) - - // The err should be the expected rate limit error - expected := "too many certificates already issued for \"early-ratelimit-example.com\". Retry after 2020-03-04T05:05:00Z: see https://letsencrypt.org/docs/rate-limits/" - test.AssertEquals(t, bErr.Error(), expected) -} - -// mockInvalidAuthorizationsAuthority is a mock which claims that the given -// domain has one invalid authorization. -type mockInvalidAuthorizationsAuthority struct { - sapb.StorageAuthorityClient - domainWithFailures string -} - -func (sa *mockInvalidAuthorizationsAuthority) CountInvalidAuthorizations2(ctx context.Context, req *sapb.CountInvalidAuthorizationsRequest, _ ...grpc.CallOption) (*sapb.Count, error) { - if req.Hostname == sa.domainWithFailures { - return &sapb.Count{Count: 1}, nil - } else { - return &sapb.Count{}, nil - } -} - -func TestAuthzFailedRateLimitingNewOrder(t *testing.T) { - _, _, ra, _, cleanUp := initAuthorities(t) - defer cleanUp() - - ra.rlPolicies = &dummyRateLimitConfig{ - InvalidAuthorizationsPerAccountPolicy: ratelimit.RateLimitPolicy{ - Threshold: 1, - Window: config.Duration{Duration: 1 * time.Hour}, - }, - } - - limit := ra.rlPolicies.InvalidAuthorizationsPerAccount() - ra.SA = &mockInvalidAuthorizationsAuthority{domainWithFailures: "all.i.do.is.lose.com"} - err := ra.checkInvalidAuthorizationLimits(ctx, Registration.Id, - []string{"charlie.brown.com", "all.i.do.is.lose.com"}, limit) - test.AssertError(t, err, "checkInvalidAuthorizationLimits did not encounter expected rate limit error") - test.AssertEquals(t, err.Error(), "too many failed authorizations recently: see https://letsencrypt.org/docs/failed-validation-limit/") -} - -type mockSAWithNameCounts struct { - sapb.StorageAuthorityClient - nameCounts *sapb.CountByNames - t *testing.T - clk clock.FakeClock -} - -func (m *mockSAWithNameCounts) CountCertificatesByNames(ctx context.Context, req *sapb.CountCertificatesByNamesRequest, _ ...grpc.CallOption) (*sapb.CountByNames, error) { - expectedLatest := m.clk.Now() - if req.Range.Latest.AsTime() != expectedLatest { - m.t.Errorf("incorrect latest: got '%v', expected '%v'", req.Range.Latest.AsTime(), expectedLatest) - } - expectedEarliest := m.clk.Now().Add(-23 * time.Hour) - if req.Range.Earliest.AsTime() != expectedEarliest { - m.t.Errorf("incorrect earliest: got '%v', expected '%v'", req.Range.Earliest.AsTime(), expectedEarliest) - } - counts := make(map[string]int64) - for _, name := range req.Names { - if count, ok := m.nameCounts.Counts[name]; ok { - counts[name] = count - } - } - return &sapb.CountByNames{Counts: counts}, nil -} - -// FQDNSetExists is a mock which always returns false, so the test requests -// aren't considered to be renewals. -func (m *mockSAWithNameCounts) FQDNSetExists(ctx context.Context, req *sapb.FQDNSetExistsRequest, _ ...grpc.CallOption) (*sapb.Exists, error) { - return &sapb.Exists{Exists: false}, nil -} - -func TestCheckCertificatesPerNameLimit(t *testing.T) { - _, _, ra, fc, cleanUp := initAuthorities(t) - defer cleanUp() - - rlp := ratelimit.RateLimitPolicy{ - Threshold: 3, - Window: config.Duration{Duration: 23 * time.Hour}, - Overrides: map[string]int64{ - "bigissuer.com": 100, - "smallissuer.co.uk": 1, - }, - } - - mockSA := &mockSAWithNameCounts{ - nameCounts: &sapb.CountByNames{Counts: map[string]int64{"example.com": 1}}, - clk: fc, - t: t, - } - - ra.SA = mockSA - - // One base domain, below threshold - err := ra.checkCertificatesPerNameLimit(ctx, []string{"www.example.com", "example.com"}, rlp, 99) - test.AssertNotError(t, err, "rate limited example.com incorrectly") - - // Two base domains, one above threshold, one below - mockSA.nameCounts.Counts["example.com"] = 10 - mockSA.nameCounts.Counts["good-example.com"] = 1 - err = ra.checkCertificatesPerNameLimit(ctx, []string{"www.example.com", "example.com", "good-example.com"}, rlp, 99) - test.AssertError(t, err, "incorrectly failed to rate limit example.com") - test.AssertErrorIs(t, err, berrors.RateLimit) - // There are no overrides for "example.com", so the override usage gauge - // should contain 0 entries with labels matching it. - test.AssertMetricWithLabelsEquals(t, ra.rlOverrideUsageGauge, prometheus.Labels{"limit": ratelimit.CertificatesPerName, "override_key": "example.com"}, 0) - // Verify it has no sub errors as there is only one bad name - test.AssertEquals(t, err.Error(), "too many certificates already issued for \"example.com\". Retry after 1970-01-01T23:00:00Z: see https://letsencrypt.org/docs/rate-limits/") - var bErr *berrors.BoulderError - test.AssertErrorWraps(t, err, &bErr) - test.AssertEquals(t, len(bErr.SubErrors), 0) - - // Three base domains, two above threshold, one below - mockSA.nameCounts.Counts["example.com"] = 10 - mockSA.nameCounts.Counts["other-example.com"] = 10 - mockSA.nameCounts.Counts["good-example.com"] = 1 - err = ra.checkCertificatesPerNameLimit(ctx, []string{"example.com", "other-example.com", "good-example.com"}, rlp, 99) - test.AssertError(t, err, "incorrectly failed to rate limit example.com, other-example.com") - test.AssertErrorIs(t, err, berrors.RateLimit) - // Verify it has two sub errors as there are two bad names - test.AssertEquals(t, err.Error(), "too many certificates already issued for multiple names (\"example.com\" and 2 others). Retry after 1970-01-01T23:00:00Z: see https://letsencrypt.org/docs/rate-limits/") - test.AssertErrorWraps(t, err, &bErr) - test.AssertEquals(t, len(bErr.SubErrors), 2) - - // SA misbehaved and didn't send back a count for every input name - err = ra.checkCertificatesPerNameLimit(ctx, []string{"zombo.com", "www.example.com", "example.com"}, rlp, 99) - test.AssertError(t, err, "incorrectly failed to error on misbehaving SA") - - // Two base domains, one above threshold but with an override. - mockSA.nameCounts.Counts["example.com"] = 0 - mockSA.nameCounts.Counts["bigissuer.com"] = 50 - ra.rlOverrideUsageGauge.WithLabelValues(ratelimit.CertificatesPerName, "bigissuer.com").Set(.5) - err = ra.checkCertificatesPerNameLimit(ctx, []string{"www.example.com", "subdomain.bigissuer.com"}, rlp, 99) - test.AssertNotError(t, err, "incorrectly rate limited bigissuer") - // "bigissuer.com" has an override of 100 and they've issued 50. Accounting - // for the anticipated issuance, we expect to see 51% utilization. - test.AssertMetricWithLabelsEquals(t, ra.rlOverrideUsageGauge, prometheus.Labels{"limit": ratelimit.CertificatesPerName, "override_key": "bigissuer.com"}, .51) - - // Two base domains, one above its override - mockSA.nameCounts.Counts["example.com"] = 10 - mockSA.nameCounts.Counts["bigissuer.com"] = 100 - ra.rlOverrideUsageGauge.WithLabelValues(ratelimit.CertificatesPerName, "bigissuer.com").Set(1) - err = ra.checkCertificatesPerNameLimit(ctx, []string{"www.example.com", "subdomain.bigissuer.com"}, rlp, 99) - test.AssertError(t, err, "incorrectly failed to rate limit bigissuer") - test.AssertErrorIs(t, err, berrors.RateLimit) - // "bigissuer.com" has an override of 100 and they've issued 100. They're - // already at 100% utilization, so we expect to see 100% utilization. - test.AssertMetricWithLabelsEquals(t, ra.rlOverrideUsageGauge, prometheus.Labels{"limit": ratelimit.CertificatesPerName, "override_key": "bigissuer.com"}, 1) - - // One base domain, above its override (which is below threshold) - mockSA.nameCounts.Counts["smallissuer.co.uk"] = 1 - ra.rlOverrideUsageGauge.WithLabelValues(ratelimit.CertificatesPerName, "smallissuer.co.uk").Set(1) - err = ra.checkCertificatesPerNameLimit(ctx, []string{"www.smallissuer.co.uk"}, rlp, 99) - test.AssertError(t, err, "incorrectly failed to rate limit smallissuer") - test.AssertErrorIs(t, err, berrors.RateLimit) - // "smallissuer.co.uk" has an override of 1 and they've issued 1. They're - // already at 100% utilization, so we expect to see 100% utilization. - test.AssertMetricWithLabelsEquals(t, ra.rlOverrideUsageGauge, prometheus.Labels{"limit": ratelimit.CertificatesPerName, "override_key": "smallissuer.co.uk"}, 1) -} - -// TestCheckExactCertificateLimit tests that the duplicate certificate limit -// applied to FQDN sets is respected. -func TestCheckExactCertificateLimit(t *testing.T) { - _, _, ra, _, cleanUp := initAuthorities(t) - defer cleanUp() - - // Create a rate limit with a small threshold - const dupeCertLimit = 3 - rlp := ratelimit.RateLimitPolicy{ - Threshold: dupeCertLimit, - Window: config.Duration{Duration: 24 * time.Hour}, - } - - // Create a mock SA that has a count of already issued certificates for some - // test names - firstIssuanceTimestamp := ra.clk.Now().Add(-rlp.Window.Duration) - fITS2 := firstIssuanceTimestamp.Add(time.Hour * 23) - fITS3 := firstIssuanceTimestamp.Add(time.Hour * 16) - fITS4 := firstIssuanceTimestamp.Add(time.Hour * 8) - issuanceTimestampsNS := []int64{ - fITS2.UnixNano(), - fITS3.UnixNano(), - fITS4.UnixNano(), - firstIssuanceTimestamp.UnixNano(), - } - issuanceTimestamps := []*timestamppb.Timestamp{ - timestamppb.New(fITS2), - timestamppb.New(fITS3), - timestamppb.New(fITS4), - timestamppb.New(firstIssuanceTimestamp), - } - // Our window is 24 hours and our threshold is 3 issuance. If our most - // recent issuance was 1 hour ago, we expect the next token to be available - // 8 hours from issuance time or 7 hours from now. - expectRetryAfterNS := time.Unix(0, issuanceTimestampsNS[0]).Add(time.Hour * 8).Format(time.RFC3339) - expectRetryAfter := issuanceTimestamps[0].AsTime().Add(time.Hour * 8).Format(time.RFC3339) - test.AssertEquals(t, expectRetryAfterNS, expectRetryAfter) - ra.SA = &mockSAWithFQDNSet{ - issuanceTimestamps: map[string]*sapb.Timestamps{ - "none.example.com": {Timestamps: []*timestamppb.Timestamp{}}, - "under.example.com": {Timestamps: issuanceTimestamps[3:3]}, - "equalbutvalid.example.com": {Timestamps: issuanceTimestamps[1:3]}, - "over.example.com": {Timestamps: issuanceTimestamps[0:3]}, - }, - t: t, - } - - testCases := []struct { - Name string - Domain string - ExpectedErr error - }{ - { - Name: "FQDN set issuances none", - Domain: "none.example.com", - ExpectedErr: nil, - }, - { - Name: "FQDN set issuances less than limit", - Domain: "under.example.com", - ExpectedErr: nil, - }, - { - Name: "FQDN set issuances equal to limit", - Domain: "equalbutvalid.example.com", - ExpectedErr: nil, - }, - { - Name: "FQDN set issuances above limit NS", - Domain: "over.example.com", - ExpectedErr: fmt.Errorf( - "too many certificates (3) already issued for this exact set of domains in the last 24 hours: over.example.com, retry after %s: see https://letsencrypt.org/docs/duplicate-certificate-limit/", - expectRetryAfterNS, - ), - }, - { - Name: "FQDN set issuances above limit", - Domain: "over.example.com", - ExpectedErr: fmt.Errorf( - "too many certificates (3) already issued for this exact set of domains in the last 24 hours: over.example.com, retry after %s: see https://letsencrypt.org/docs/duplicate-certificate-limit/", - expectRetryAfter, - ), - }, - } - - // For each test case we check that the certificatesPerFQDNSetLimit is applied - // as we expect - for _, tc := range testCases { - t.Run(tc.Name, func(t *testing.T) { - result := ra.checkCertificatesPerFQDNSetLimit(ctx, []string{tc.Domain}, rlp, 0) - if tc.ExpectedErr == nil { - test.AssertNotError(t, result, fmt.Sprintf("Expected no error for %q", tc.Domain)) - } else { - test.AssertError(t, result, fmt.Sprintf("Expected error for %q", tc.Domain)) - test.AssertEquals(t, result.Error(), tc.ExpectedErr.Error()) - } - }) - } -} - -func TestRegistrationUpdate(t *testing.T) { - oldURL := "http://old.invalid" - newURL := "http://new.invalid" - base := &corepb.Registration{ - Id: 1, - Contact: []string{oldURL}, - Agreement: "", - } - update := &corepb.Registration{ - Contact: []string{newURL}, - ContactsPresent: true, - Agreement: "totally!", - } - - res, changed := mergeUpdate(base, update) - test.AssertEquals(t, changed, true) - test.AssertEquals(t, res.Contact[0], update.Contact[0]) - test.AssertEquals(t, res.Agreement, update.Agreement) - - // Make sure that a `MergeUpdate` call with an empty string doesn't produce an - // error and results in a change to the base reg. - emptyUpdate := &corepb.Registration{ - Contact: []string{""}, - ContactsPresent: true, - Agreement: "totally!", - } - _, changed = mergeUpdate(res, emptyUpdate) - test.AssertEquals(t, changed, true) -} - -func TestRegistrationContactUpdate(t *testing.T) { - contactURL := "mailto://example@example.com" - - // Test that a registration contact can be removed by updating with an empty - // Contact slice. - base := &corepb.Registration{ - Id: 1, - Contact: []string{contactURL}, - Agreement: "totally!", - } - update := &corepb.Registration{ - Id: 1, - Contact: []string{}, - ContactsPresent: true, - Agreement: "totally!", - } - res, changed := mergeUpdate(base, update) - test.AssertEquals(t, changed, true) - test.Assert(t, len(res.Contact) == 0, "Contact was not deleted in update") - - // Test that a registration contact isn't changed when an update is performed - // with no Contact field - base = &corepb.Registration{ - Id: 1, - Contact: []string{contactURL}, - Agreement: "totally!", - } - update = &corepb.Registration{ - Id: 1, - Agreement: "totally!", - } - res, changed = mergeUpdate(base, update) - test.AssertEquals(t, changed, false) - test.Assert(t, len(res.Contact) == 1, "len(Contact) was updated unexpectedly") - test.Assert(t, (res.Contact)[0] == contactURL, "Contact was changed unexpectedly") -} - -func TestRegistrationKeyUpdate(t *testing.T) { - oldKey, err := rsa.GenerateKey(rand.Reader, 512) - test.AssertNotError(t, err, "rsa.GenerateKey() for oldKey failed") - oldKeyJSON, err := jose.JSONWebKey{Key: oldKey}.MarshalJSON() - test.AssertNotError(t, err, "MarshalJSON for oldKey failed") - - base := &corepb.Registration{Key: oldKeyJSON} - update := &corepb.Registration{} - _, changed := mergeUpdate(base, update) - test.Assert(t, !changed, "mergeUpdate changed the key with empty update") - - newKey, err := rsa.GenerateKey(rand.Reader, 1024) - test.AssertNotError(t, err, "rsa.GenerateKey() for newKey failed") - newKeyJSON, err := jose.JSONWebKey{Key: newKey}.MarshalJSON() - test.AssertNotError(t, err, "MarshalJSON for newKey failed") - - update = &corepb.Registration{Key: newKeyJSON} - res, changed := mergeUpdate(base, update) - test.Assert(t, changed, "mergeUpdate didn't change the key with non-empty update") - test.AssertByteEquals(t, res.Key, update.Key) -} - -// A mockSAWithFQDNSet is a mock StorageAuthority that supports -// CountCertificatesByName as well as FQDNSetExists. This allows testing -// checkCertificatesPerNameRateLimit's FQDN exemption logic. -type mockSAWithFQDNSet struct { - sapb.StorageAuthorityClient - fqdnSet map[string]bool - issuanceTimestamps map[string]*sapb.Timestamps - - t *testing.T -} - -// Construct the FQDN Set key the same way as the SA (by using -// `core.UniqueLowerNames`, joining the names with a `,` and hashing them) -// but return a string so it can be used as a key in m.fqdnSet. -func (m mockSAWithFQDNSet) hashNames(names []string) string { - names = core.UniqueLowerNames(names) - hash := sha256.Sum256([]byte(strings.Join(names, ","))) - return string(hash[:]) -} - -// Add a set of domain names to the FQDN set -func (m mockSAWithFQDNSet) addFQDNSet(names []string) { - hash := m.hashNames(names) - m.fqdnSet[hash] = true -} - -// Search for a set of domain names in the FQDN set map -func (m mockSAWithFQDNSet) FQDNSetExists(_ context.Context, req *sapb.FQDNSetExistsRequest, _ ...grpc.CallOption) (*sapb.Exists, error) { - hash := m.hashNames(req.Domains) - if _, exists := m.fqdnSet[hash]; exists { - return &sapb.Exists{Exists: true}, nil - } - return &sapb.Exists{Exists: false}, nil -} - -// Return a map of domain -> certificate count. -func (m mockSAWithFQDNSet) CountCertificatesByNames(ctx context.Context, req *sapb.CountCertificatesByNamesRequest, _ ...grpc.CallOption) (*sapb.CountByNames, error) { - counts := make(map[string]int64) - for _, name := range req.Names { - entry, ok := m.issuanceTimestamps[name] - if ok { - counts[name] = int64(len(entry.Timestamps)) - } - } - return &sapb.CountByNames{Counts: counts}, nil -} - -func (m mockSAWithFQDNSet) CountFQDNSets(_ context.Context, req *sapb.CountFQDNSetsRequest, _ ...grpc.CallOption) (*sapb.Count, error) { - var total int64 - for _, name := range req.Domains { - entry, ok := m.issuanceTimestamps[name] - if ok { - total += int64(len(entry.Timestamps)) - } - } - return &sapb.Count{Count: total}, nil -} - -func (m mockSAWithFQDNSet) FQDNSetTimestampsForWindow(_ context.Context, req *sapb.CountFQDNSetsRequest, _ ...grpc.CallOption) (*sapb.Timestamps, error) { - if len(req.Domains) == 1 { - return m.issuanceTimestamps[req.Domains[0]], nil - } else { - return nil, fmt.Errorf("FQDNSetTimestampsForWindow mock only supports a single domain") - } -} - -// Tests for boulder issue 1925[0] - that the `checkCertificatesPerNameLimit` -// properly honours the FQDNSet exemption. E.g. that if a set of domains has -// reached the certificates per name rate limit policy threshold but the exact -// same set of FQDN's was previously issued, then it should not be considered -// over the certificates per name limit. -// -// [0] https://github.com/letsencrypt/boulder/issues/1925 -func TestCheckFQDNSetRateLimitOverride(t *testing.T) { - _, _, ra, _, cleanUp := initAuthorities(t) - defer cleanUp() - - // Simple policy that only allows 1 certificate per name. - certsPerNamePolicy := ratelimit.RateLimitPolicy{ - Threshold: 1, - Window: config.Duration{Duration: 24 * time.Hour}, - } - - // Create a mock SA that has both name counts and an FQDN set - ts := timestamppb.New(ra.clk.Now()) - mockSA := &mockSAWithFQDNSet{ - issuanceTimestamps: map[string]*sapb.Timestamps{ - "example.com": {Timestamps: []*timestamppb.Timestamp{ts, ts}}, - "zombo.com": {Timestamps: []*timestamppb.Timestamp{ts, ts}}, - }, - fqdnSet: map[string]bool{}, - t: t, - } - ra.SA = mockSA - - // First check that without a pre-existing FQDN set that the provided set of - // names is rate limited due to being over the certificates per name limit for - // "example.com" and "zombo.com" - err := ra.checkCertificatesPerNameLimit(ctx, []string{"www.example.com", "example.com", "www.zombo.com"}, certsPerNamePolicy, 99) - test.AssertError(t, err, "certificate per name rate limit not applied correctly") - - // Now add a FQDN set entry for these domains - mockSA.addFQDNSet([]string{"www.example.com", "example.com", "www.zombo.com"}) - - // A subsequent check against the certificates per name limit should now be OK - // - there exists a FQDN set and so the exemption to this particular limit - // comes into effect. - err = ra.checkCertificatesPerNameLimit(ctx, []string{"www.example.com", "example.com", "www.zombo.com"}, certsPerNamePolicy, 99) - test.AssertNotError(t, err, "FQDN set certificate per name exemption not applied correctly") -} - -// TestExactPublicSuffixCertLimit tests the behaviour of issue #2681 with and -// without the feature flag for the fix enabled. -// See https://github.com/letsencrypt/boulder/issues/2681 -func TestExactPublicSuffixCertLimit(t *testing.T) { - _, _, ra, fc, cleanUp := initAuthorities(t) - defer cleanUp() - - // Simple policy that only allows 2 certificates per name. - certsPerNamePolicy := ratelimit.RateLimitPolicy{ - Threshold: 2, - Window: config.Duration{Duration: 23 * time.Hour}, - } - - // We use "dedyn.io" and "dynv6.net" domains for the test on the implicit - // assumption that both domains are present on the public suffix list. - // Quickly verify that this is true before continuing with the rest of the test. - _, err := publicsuffix.Domain("dedyn.io") - test.AssertError(t, err, "dedyn.io was not on the public suffix list, invaliding the test") - _, err = publicsuffix.Domain("dynv6.net") - test.AssertError(t, err, "dynv6.net was not on the public suffix list, invaliding the test") - - // Back the mock SA with counts as if so far we have issued the following - // certificates for the following domains: - // - test.dedyn.io (once) - // - test2.dedyn.io (once) - // - dynv6.net (twice) - mockSA := &mockSAWithNameCounts{ - nameCounts: &sapb.CountByNames{ - Counts: map[string]int64{ - "test.dedyn.io": 1, - "test2.dedyn.io": 1, - "test3.dedyn.io": 0, - "dedyn.io": 0, - "dynv6.net": 2, - }, - }, - clk: fc, - t: t, - } - ra.SA = mockSA - - // Trying to issue for "test3.dedyn.io" and "dedyn.io" should succeed because - // test3.dedyn.io has no certificates and "dedyn.io" is an exact public suffix - // match with no certificates issued for it. - err = ra.checkCertificatesPerNameLimit(ctx, []string{"test3.dedyn.io", "dedyn.io"}, certsPerNamePolicy, 99) - test.AssertNotError(t, err, "certificate per name rate limit not applied correctly") - - // Trying to issue for "test3.dedyn.io" and "dynv6.net" should fail because - // "dynv6.net" is an exact public suffix match with 2 certificates issued for - // it. - err = ra.checkCertificatesPerNameLimit(ctx, []string{"test3.dedyn.io", "dynv6.net"}, certsPerNamePolicy, 99) - test.AssertError(t, err, "certificate per name rate limit not applied correctly") -} - func TestDeactivateAuthorization(t *testing.T) { - _, sa, ra, _, cleanUp := initAuthorities(t) + _, sa, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() exp := ra.clk.Now().Add(365 * 24 * time.Hour) - authzID := createFinalizedAuthorization(t, sa, "not-example.com", exp, core.ChallengeTypeHTTP01, ra.clk.Now()) + authzID := createFinalizedAuthorization(t, sa, identifier.NewDNS("not-example.com"), exp, core.ChallengeTypeHTTP01, ra.clk.Now()) dbAuthzPB := getAuthorization(t, fmt.Sprint(authzID), sa) _, err := ra.DeactivateAuthorization(ctx, dbAuthzPB) test.AssertNotError(t, err, "Could not deactivate authorization") @@ -1701,23 +942,92 @@ func TestDeactivateAuthorization(t *testing.T) { test.AssertEquals(t, deact.Status, string(core.StatusDeactivated)) } +type mockSARecordingPauses struct { + sapb.StorageAuthorityClient + recv *sapb.PauseRequest +} + +func (sa *mockSARecordingPauses) PauseIdentifiers(ctx context.Context, req *sapb.PauseRequest, _ ...grpc.CallOption) (*sapb.PauseIdentifiersResponse, error) { + sa.recv = req + return &sapb.PauseIdentifiersResponse{Paused: int64(len(req.Identifiers))}, nil +} + +func (sa *mockSARecordingPauses) DeactivateAuthorization2(_ context.Context, _ *sapb.AuthorizationID2, _ ...grpc.CallOption) (*emptypb.Empty, error) { + return nil, nil +} + +func TestDeactivateAuthorization_Pausing(t *testing.T) { + _, _, ra, _, _, cleanUp := initAuthorities(t) + defer cleanUp() + + if ra.limiter == nil { + t.Skip("no redis limiter configured") + } + + msa := mockSARecordingPauses{} + ra.SA = &msa + + features.Set(features.Config{AutomaticallyPauseZombieClients: true}) + defer features.Reset() + + // Set the default ratelimits to only allow one failed validation per 24 + // hours before pausing. + txnBuilder, err := ratelimits.NewTransactionBuilder(ratelimits.LimitConfigs{ + ratelimits.FailedAuthorizationsForPausingPerDomainPerAccount.String(): &ratelimits.LimitConfig{ + Burst: 1, + Count: 1, + Period: config.Duration{Duration: time.Hour * 24}}, + }) + test.AssertNotError(t, err, "making transaction composer") + ra.txnBuilder = txnBuilder + + // The first deactivation of a pending authz should work and nothing should + // get paused. + _, err = ra.DeactivateAuthorization(ctx, &corepb.Authorization{ + Id: "1", + RegistrationID: 1, + Identifier: identifier.NewDNS("example.com").ToProto(), + Status: string(core.StatusPending), + }) + test.AssertNotError(t, err, "mock deactivation should work") + test.AssertBoxedNil(t, msa.recv, "shouldn't be a pause request yet") + + // Deactivating a valid authz shouldn't increment any limits or pause anything. + _, err = ra.DeactivateAuthorization(ctx, &corepb.Authorization{ + Id: "2", + RegistrationID: 1, + Identifier: identifier.NewDNS("example.com").ToProto(), + Status: string(core.StatusValid), + }) + test.AssertNotError(t, err, "mock deactivation should work") + test.AssertBoxedNil(t, msa.recv, "deactivating valid authz should never pause") + + // Deactivating a second pending authz should surpass the limit and result + // in a pause request. + _, err = ra.DeactivateAuthorization(ctx, &corepb.Authorization{ + Id: "3", + RegistrationID: 1, + Identifier: identifier.NewDNS("example.com").ToProto(), + Status: string(core.StatusPending), + }) + test.AssertNotError(t, err, "mock deactivation should work") + test.AssertNotNil(t, msa.recv, "should have recorded a pause request") + test.AssertEquals(t, msa.recv.RegistrationID, int64(1)) + test.AssertEquals(t, msa.recv.Identifiers[0].Value, "example.com") +} + func TestDeactivateRegistration(t *testing.T) { - _, _, ra, _, cleanUp := initAuthorities(t) + _, _, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() // Deactivate failure because incomplete registration provided - _, err := ra.DeactivateRegistration(context.Background(), &corepb.Registration{}) + _, err := ra.DeactivateRegistration(context.Background(), &rapb.DeactivateRegistrationRequest{}) test.AssertDeepEquals(t, err, fmt.Errorf("incomplete gRPC request message")) - // Deactivate failure because registration status already deactivated - _, err = ra.DeactivateRegistration(context.Background(), - &corepb.Registration{Id: 1, Status: string(core.StatusDeactivated)}) - test.AssertError(t, err, "DeactivateRegistration failed with a non-valid registration") - // Deactivate success with valid registration - _, err = ra.DeactivateRegistration(context.Background(), - &corepb.Registration{Id: 1, Status: string(core.StatusValid)}) + got, err := ra.DeactivateRegistration(context.Background(), &rapb.DeactivateRegistrationRequest{RegistrationID: 1}) test.AssertNotError(t, err, "DeactivateRegistration failed") + test.AssertEquals(t, got.Status, string(core.StatusDeactivated)) // Check db to make sure account is deactivated dbReg, err := ra.SA.GetRegistration(context.Background(), &sapb.RegistrationID{Id: 1}) @@ -1725,7 +1035,7 @@ func TestDeactivateRegistration(t *testing.T) { test.AssertEquals(t, dbReg.Status, string(core.StatusDeactivated)) } -// noopCAA implements caaChecker, always returning nil +// noopCAA implements vapb.CAAClient, always returning nil type noopCAA struct{} func (cr noopCAA) IsCAAValid( @@ -1736,8 +1046,16 @@ func (cr noopCAA) IsCAAValid( return &vapb.IsCAAValidResponse{}, nil } -// caaRecorder implements caaChecker, always returning nil, but recording the -// names it was called for. +func (cr noopCAA) DoCAA( + ctx context.Context, + in *vapb.IsCAAValidRequest, + opts ...grpc.CallOption, +) (*vapb.IsCAAValidResponse, error) { + return &vapb.IsCAAValidResponse{}, nil +} + +// caaRecorder implements vapb.CAAClient, always returning nil, but recording +// the names it was called for. type caaRecorder struct { sync.Mutex names map[string]bool @@ -1750,33 +1068,38 @@ func (cr *caaRecorder) IsCAAValid( ) (*vapb.IsCAAValidResponse, error) { cr.Lock() defer cr.Unlock() - cr.names[in.Domain] = true + cr.names[in.Identifier.Value] = true + return &vapb.IsCAAValidResponse{}, nil +} + +func (cr *caaRecorder) DoCAA( + ctx context.Context, + in *vapb.IsCAAValidRequest, + opts ...grpc.CallOption, +) (*vapb.IsCAAValidResponse, error) { + cr.Lock() + defer cr.Unlock() + cr.names[in.Identifier.Value] = true return &vapb.IsCAAValidResponse{}, nil } // Test that the right set of domain names have their CAA rechecked, based on // their `Validated` (attemptedAt in the database) timestamp. func TestRecheckCAADates(t *testing.T) { - _, _, ra, fc, cleanUp := initAuthorities(t) + _, _, ra, _, fc, cleanUp := initAuthorities(t) defer cleanUp() recorder := &caaRecorder{names: make(map[string]bool)} - ra.caa = recorder - ra.authorizationLifetime = 15 * time.Hour + ra.VA = va.RemoteClients{CAAClient: recorder} + ra.profiles.def().validAuthzLifetime = 15 * time.Hour recentValidated := fc.Now().Add(-1 * time.Hour) recentExpires := fc.Now().Add(15 * time.Hour) olderValidated := fc.Now().Add(-8 * time.Hour) olderExpires := fc.Now().Add(5 * time.Hour) - makeIdentifier := func(name string) identifier.ACMEIdentifier { - return identifier.ACMEIdentifier{ - Type: identifier.DNS, - Value: name, - } - } - authzs := map[string]*core.Authorization{ - "recent.com": { - Identifier: makeIdentifier("recent.com"), + authzs := map[identifier.ACMEIdentifier]*core.Authorization{ + identifier.NewDNS("recent.com"): { + Identifier: identifier.NewDNS("recent.com"), Expires: &recentExpires, Challenges: []core.Challenge{ { @@ -1787,8 +1110,8 @@ func TestRecheckCAADates(t *testing.T) { }, }, }, - "older.com": { - Identifier: makeIdentifier("older.com"), + identifier.NewDNS("older.com"): { + Identifier: identifier.NewDNS("older.com"), Expires: &olderExpires, Challenges: []core.Challenge{ { @@ -1799,8 +1122,8 @@ func TestRecheckCAADates(t *testing.T) { }, }, }, - "older2.com": { - Identifier: makeIdentifier("older2.com"), + identifier.NewDNS("older2.com"): { + Identifier: identifier.NewDNS("older2.com"), Expires: &olderExpires, Challenges: []core.Challenge{ { @@ -1811,8 +1134,8 @@ func TestRecheckCAADates(t *testing.T) { }, }, }, - "wildcard.com": { - Identifier: makeIdentifier("wildcard.com"), + identifier.NewDNS("wildcard.com"): { + Identifier: identifier.NewDNS("wildcard.com"), Expires: &olderExpires, Challenges: []core.Challenge{ { @@ -1823,8 +1146,8 @@ func TestRecheckCAADates(t *testing.T) { }, }, }, - "*.wildcard.com": { - Identifier: makeIdentifier("*.wildcard.com"), + identifier.NewDNS("*.wildcard.com"): { + Identifier: identifier.NewDNS("*.wildcard.com"), Expires: &olderExpires, Challenges: []core.Challenge{ { @@ -1835,9 +1158,11 @@ func TestRecheckCAADates(t *testing.T) { }, }, }, - "twochallenges.com": { + } + twoChallenges := map[identifier.ACMEIdentifier]*core.Authorization{ + identifier.NewDNS("twochallenges.com"): { ID: "twochal", - Identifier: makeIdentifier("twochallenges.com"), + Identifier: identifier.NewDNS("twochallenges.com"), Expires: &recentExpires, Challenges: []core.Challenge{ { @@ -1854,15 +1179,19 @@ func TestRecheckCAADates(t *testing.T) { }, }, }, - "nochallenges.com": { + } + noChallenges := map[identifier.ACMEIdentifier]*core.Authorization{ + identifier.NewDNS("nochallenges.com"): { ID: "nochal", - Identifier: makeIdentifier("nochallenges.com"), + Identifier: identifier.NewDNS("nochallenges.com"), Expires: &recentExpires, Challenges: []core.Challenge{}, }, - "novalidationtime.com": { + } + noValidationTime := map[identifier.ACMEIdentifier]*core.Authorization{ + identifier.NewDNS("novalidationtime.com"): { ID: "noval", - Identifier: makeIdentifier("novalidationtime.com"), + Identifier: identifier.NewDNS("novalidationtime.com"), Expires: &recentExpires, Challenges: []core.Challenge{ { @@ -1877,29 +1206,24 @@ func TestRecheckCAADates(t *testing.T) { // NOTE: The names provided here correspond to authorizations in the // `mockSAWithRecentAndOlder` - names := []string{"recent.com", "older.com", "older2.com", "wildcard.com", "*.wildcard.com"} - err := ra.checkAuthorizationsCAA(context.Background(), Registration.Id, names, authzs, fc.Now()) + err := ra.checkAuthorizationsCAA(context.Background(), Registration.Id, authzs, fc.Now()) // We expect that there is no error rechecking authorizations for these names if err != nil { t.Errorf("expected nil err, got %s", err) } // Should error if a authorization has `!= 1` challenge - err = ra.checkAuthorizationsCAA(context.Background(), Registration.Id, []string{"twochallenges.com"}, authzs, fc.Now()) + err = ra.checkAuthorizationsCAA(context.Background(), Registration.Id, twoChallenges, fc.Now()) test.AssertEquals(t, err.Error(), "authorization has incorrect number of challenges. 1 expected, 2 found for: id twochal") // Should error if a authorization has `!= 1` challenge - err = ra.checkAuthorizationsCAA(context.Background(), Registration.Id, []string{"nochallenges.com"}, authzs, fc.Now()) + err = ra.checkAuthorizationsCAA(context.Background(), Registration.Id, noChallenges, fc.Now()) test.AssertEquals(t, err.Error(), "authorization has incorrect number of challenges. 1 expected, 0 found for: id nochal") // Should error if authorization's challenge has no validated timestamp - err = ra.checkAuthorizationsCAA(context.Background(), Registration.Id, []string{"novalidationtime.com"}, authzs, fc.Now()) + err = ra.checkAuthorizationsCAA(context.Background(), Registration.Id, noValidationTime, fc.Now()) test.AssertEquals(t, err.Error(), "authorization's challenge has no validated timestamp for: id noval") - // Test to make sure the authorization lifetime codepath was not used - // to determine if CAA needed recheck. - test.AssertMetricWithLabelsEquals(t, ra.recheckCAAUsedAuthzLifetime, prometheus.Labels{}, 0) - // We expect that "recent.com" is not checked because its mock authorization // isn't expired if _, present := recorder.names["recent.com"]; present { @@ -1936,55 +1260,83 @@ func (cf *caaFailer) IsCAAValid( opts ...grpc.CallOption, ) (*vapb.IsCAAValidResponse, error) { cvrpb := &vapb.IsCAAValidResponse{} - switch in.Domain { + switch in.Identifier.Value { case "a.com": cvrpb.Problem = &corepb.ProblemDetails{ Detail: "CAA invalid for a.com", } + case "b.com": case "c.com": cvrpb.Problem = &corepb.ProblemDetails{ Detail: "CAA invalid for c.com", } case "d.com": return nil, fmt.Errorf("Error checking CAA for d.com") + default: + return nil, fmt.Errorf("Unexpected test case") + } + return cvrpb, nil +} + +func (cf *caaFailer) DoCAA( + ctx context.Context, + in *vapb.IsCAAValidRequest, + opts ...grpc.CallOption, +) (*vapb.IsCAAValidResponse, error) { + cvrpb := &vapb.IsCAAValidResponse{} + switch in.Identifier.Value { + case "a.com": + cvrpb.Problem = &corepb.ProblemDetails{ + Detail: "CAA invalid for a.com", + } + case "b.com": + case "c.com": + cvrpb.Problem = &corepb.ProblemDetails{ + Detail: "CAA invalid for c.com", + } + case "d.com": + return nil, fmt.Errorf("Error checking CAA for d.com") + default: + return nil, fmt.Errorf("Unexpected test case") } return cvrpb, nil } func TestRecheckCAAEmpty(t *testing.T) { - _, _, ra, _, cleanUp := initAuthorities(t) + _, _, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() err := ra.recheckCAA(context.Background(), nil) test.AssertNotError(t, err, "expected nil") } -func makeHTTP01Authorization(domain string) *core.Authorization { +func makeHTTP01Authorization(ident identifier.ACMEIdentifier) *core.Authorization { return &core.Authorization{ - Identifier: identifier.ACMEIdentifier{Type: identifier.DNS, Value: domain}, + Identifier: ident, Challenges: []core.Challenge{{Status: core.StatusValid, Type: core.ChallengeTypeHTTP01}}, } } func TestRecheckCAASuccess(t *testing.T) { - _, _, ra, _, cleanUp := initAuthorities(t) + _, _, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() + ra.VA = va.RemoteClients{CAAClient: &noopCAA{}} authzs := []*core.Authorization{ - makeHTTP01Authorization("a.com"), - makeHTTP01Authorization("b.com"), - makeHTTP01Authorization("c.com"), + makeHTTP01Authorization(identifier.NewDNS("a.com")), + makeHTTP01Authorization(identifier.NewDNS("b.com")), + makeHTTP01Authorization(identifier.NewDNS("c.com")), } err := ra.recheckCAA(context.Background(), authzs) test.AssertNotError(t, err, "expected nil") } func TestRecheckCAAFail(t *testing.T) { - _, _, ra, _, cleanUp := initAuthorities(t) + _, _, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() - ra.caa = &caaFailer{} + ra.VA = va.RemoteClients{CAAClient: &caaFailer{}} authzs := []*core.Authorization{ - makeHTTP01Authorization("a.com"), - makeHTTP01Authorization("b.com"), - makeHTTP01Authorization("c.com"), + makeHTTP01Authorization(identifier.NewDNS("a.com")), + makeHTTP01Authorization(identifier.NewDNS("b.com")), + makeHTTP01Authorization(identifier.NewDNS("c.com")), } err := ra.recheckCAA(context.Background(), authzs) @@ -2017,7 +1369,7 @@ func TestRecheckCAAFail(t *testing.T) { // Recheck CAA with just one bad authz authzs = []*core.Authorization{ - makeHTTP01Authorization("a.com"), + makeHTTP01Authorization(identifier.NewDNS("a.com")), } err = ra.recheckCAA(context.Background(), authzs) // It should error @@ -2029,338 +1381,645 @@ func TestRecheckCAAFail(t *testing.T) { } func TestRecheckCAAInternalServerError(t *testing.T) { - _, _, ra, _, cleanUp := initAuthorities(t) + _, _, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() - ra.caa = &caaFailer{} + ra.VA = va.RemoteClients{CAAClient: &caaFailer{}} authzs := []*core.Authorization{ - makeHTTP01Authorization("a.com"), - makeHTTP01Authorization("b.com"), - makeHTTP01Authorization("d.com"), + makeHTTP01Authorization(identifier.NewDNS("a.com")), + makeHTTP01Authorization(identifier.NewDNS("b.com")), + makeHTTP01Authorization(identifier.NewDNS("d.com")), } err := ra.recheckCAA(context.Background(), authzs) test.AssertError(t, err, "expected err, got nil") test.AssertErrorIs(t, err, berrors.InternalServer) } -func TestNewOrder(t *testing.T) { - _, _, ra, fc, cleanUp := initAuthorities(t) +func TestRecheckSkipIPAddress(t *testing.T) { + _, _, ra, _, fc, cleanUp := initAuthorities(t) + defer cleanUp() + ra.VA = va.RemoteClients{CAAClient: &caaFailer{}} + ident := identifier.NewIP(netip.MustParseAddr("127.0.0.1")) + olderValidated := fc.Now().Add(-8 * time.Hour) + olderExpires := fc.Now().Add(5 * time.Hour) + authzs := map[identifier.ACMEIdentifier]*core.Authorization{ + ident: { + Identifier: ident, + Expires: &olderExpires, + Challenges: []core.Challenge{ + { + Status: core.StatusValid, + Type: core.ChallengeTypeHTTP01, + Token: "exampleToken", + Validated: &olderValidated, + }, + }, + }, + } + err := ra.checkAuthorizationsCAA(context.Background(), 1, authzs, fc.Now()) + test.AssertNotError(t, err, "rechecking CAA for IP address, should have skipped") +} + +func TestRecheckInvalidIdentifierType(t *testing.T) { + _, _, ra, _, fc, cleanUp := initAuthorities(t) + defer cleanUp() + ident := identifier.ACMEIdentifier{ + Type: "fnord", + Value: "well this certainly shouldn't have happened", + } + olderValidated := fc.Now().Add(-8 * time.Hour) + olderExpires := fc.Now().Add(5 * time.Hour) + authzs := map[identifier.ACMEIdentifier]*core.Authorization{ + ident: { + Identifier: ident, + Expires: &olderExpires, + Challenges: []core.Challenge{ + { + Status: core.StatusValid, + Type: core.ChallengeTypeHTTP01, + Token: "exampleToken", + Validated: &olderValidated, + }, + }, + }, + } + err := ra.checkAuthorizationsCAA(context.Background(), 1, authzs, fc.Now()) + test.AssertError(t, err, "expected err, got nil") + test.AssertErrorIs(t, err, berrors.Malformed) + test.AssertContains(t, err.Error(), "invalid identifier type") +} + +func TestNewOrder(t *testing.T) { + _, _, ra, _, fc, cleanUp := initAuthorities(t) defer cleanUp() - ra.orderLifetime = time.Hour now := fc.Now() orderA, err := ra.NewOrder(context.Background(), &rapb.NewOrderRequest{ - RegistrationID: Registration.Id, - Names: []string{"b.com", "a.com", "a.com", "C.COM"}, + RegistrationID: Registration.Id, + CertificateProfileName: "test", + Identifiers: []*corepb.Identifier{ + identifier.NewDNS("b.com").ToProto(), + identifier.NewDNS("a.com").ToProto(), + identifier.NewDNS("a.com").ToProto(), + identifier.NewDNS("C.COM").ToProto(), + }, }) test.AssertNotError(t, err, "ra.NewOrder failed") test.AssertEquals(t, orderA.RegistrationID, int64(1)) - test.AssertEquals(t, orderA.Expires.AsTime(), now.Add(time.Hour)) - test.AssertEquals(t, len(orderA.Names), 3) - // We expect the order names to have been sorted, deduped, and lowercased - test.AssertDeepEquals(t, orderA.Names, []string{"a.com", "b.com", "c.com"}) + test.AssertEquals(t, orderA.Expires.AsTime(), now.Add(ra.profiles.def().orderLifetime)) + test.AssertEquals(t, len(orderA.Identifiers), 3) + test.AssertEquals(t, orderA.CertificateProfileName, "test") + // We expect the order's identifier values to have been sorted, + // deduplicated, and lowercased. + test.AssertDeepEquals(t, orderA.Identifiers, []*corepb.Identifier{ + identifier.NewDNS("a.com").ToProto(), + identifier.NewDNS("b.com").ToProto(), + identifier.NewDNS("c.com").ToProto(), + }) + test.AssertEquals(t, orderA.Id, int64(1)) test.AssertEquals(t, numAuthorizations(orderA), 3) - // Reuse all existing authorizations - now = fc.Now() - orderB, err := ra.NewOrder(context.Background(), &rapb.NewOrderRequest{ - RegistrationID: Registration.Id, - Names: []string{"b.com", "a.com", "C.COM"}, - }) - test.AssertNotError(t, err, "ra.NewOrder failed") - test.AssertEquals(t, orderB.RegistrationID, int64(1)) - test.AssertEquals(t, orderB.Expires.AsTime(), now.Add(time.Hour)) - // We expect orderB's ID to match orderA's because of pending order reuse - test.AssertEquals(t, orderB.Id, orderA.Id) - test.AssertEquals(t, len(orderB.Names), 3) - test.AssertDeepEquals(t, orderB.Names, []string{"a.com", "b.com", "c.com"}) - test.AssertEquals(t, numAuthorizations(orderB), 3) - test.AssertDeepEquals(t, orderB.V2Authorizations, orderA.V2Authorizations) - - // Reuse all of the existing authorizations from the previous order and - // add a new one - orderA.Names = append(orderA.Names, "d.com") - now = fc.Now() - orderC, err := ra.NewOrder(context.Background(), &rapb.NewOrderRequest{ - RegistrationID: Registration.Id, - Names: orderA.Names, - }) - test.AssertNotError(t, err, "ra.NewOrder failed") - test.AssertEquals(t, orderC.RegistrationID, int64(1)) - test.AssertEquals(t, orderC.Expires.AsTime(), now.Add(time.Hour)) - test.AssertEquals(t, len(orderC.Names), 4) - test.AssertDeepEquals(t, orderC.Names, []string{"a.com", "b.com", "c.com", "d.com"}) - // We expect orderC's ID to not match orderA/orderB's because it is for - // a different set of names - test.AssertNotEquals(t, orderC.Id, orderA.Id) - test.AssertEquals(t, numAuthorizations(orderC), 4) - // Abuse the order of the queries used to extract the reused authorizations - existing := orderC.V2Authorizations[:3] - test.AssertDeepEquals(t, existing, orderA.V2Authorizations) - _, err = ra.NewOrder(context.Background(), &rapb.NewOrderRequest{ RegistrationID: Registration.Id, - Names: []string{"a"}, + Identifiers: []*corepb.Identifier{identifier.NewDNS("a").ToProto()}, }) test.AssertError(t, err, "NewOrder with invalid names did not error") test.AssertEquals(t, err.Error(), "Cannot issue for \"a\": Domain name needs at least one dot") } -// TestNewOrderReuse tests that subsequent requests by an ACME account to create +// TestNewOrder_OrderReuse tests that subsequent requests by an ACME account to create // an identical order results in only one order being created & subsequently // reused. -func TestNewOrderReuse(t *testing.T) { - _, _, ra, fc, cleanUp := initAuthorities(t) +func TestNewOrder_OrderReuse(t *testing.T) { + _, _, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() - ctx := context.Background() - names := []string{"zombo.com", "welcome.to.zombo.com"} - - // Configure the RA to use a short order lifetime - ra.orderLifetime = time.Hour - // Create a var with two times the order lifetime to reference later - doubleLifetime := ra.orderLifetime * 2 - - // Create an initial request with regA and names - orderReq := &rapb.NewOrderRequest{ - RegistrationID: Registration.Id, - Names: names, + // Create an initial order with regA and names + idents := identifier.ACMEIdentifiers{ + identifier.NewDNS("zombo.com"), + identifier.NewDNS("welcome.to.zombo.com"), } + orderReq := &rapb.NewOrderRequest{ + RegistrationID: Registration.Id, + Identifiers: idents.ToProtoSlice(), + CertificateProfileName: "test", + } + firstOrder, err := ra.NewOrder(context.Background(), orderReq) + test.AssertNotError(t, err, "Adding an initial order for regA failed") + // Create a second registration to reference acctKeyB, err := AccountKeyB.MarshalJSON() test.AssertNotError(t, err, "failed to marshal account key") - input := &corepb.Registration{ - Key: acctKeyB, - InitialIP: parseAndMarshalIP(t, "42.42.42.42"), - } - secondReg, err := ra.NewRegistration(ctx, input) + input := &corepb.Registration{Key: acctKeyB} + secondReg, err := ra.NewRegistration(context.Background(), input) test.AssertNotError(t, err, "Error creating a second test registration") - // First, add an order with `names` for regA - firstOrder, err := ra.NewOrder(context.Background(), orderReq) - // It shouldn't fail - test.AssertNotError(t, err, "Adding an initial order for regA failed") - // It should have an ID - test.AssertNotNil(t, firstOrder.Id, "Initial order had a nil ID") + + // Insert a second (albeit identical) profile to reference + ra.profiles.byName["different"] = ra.profiles.def() testCases := []struct { - Name string - OrderReq *rapb.NewOrderRequest - ExpectReuse bool - AdvanceClock *time.Duration + Name string + RegistrationID int64 + Identifiers identifier.ACMEIdentifiers + Profile string + ExpectReuse bool }{ { - Name: "Duplicate order, same regID", - OrderReq: orderReq, + Name: "Duplicate order, same regID", + RegistrationID: Registration.Id, + Identifiers: idents, + Profile: "test", // We expect reuse since the order matches firstOrder ExpectReuse: true, }, { - Name: "Subset of order names, same regID", - OrderReq: &rapb.NewOrderRequest{ - RegistrationID: Registration.Id, - Names: []string{names[1]}, - }, + Name: "Subset of order names, same regID", + RegistrationID: Registration.Id, + Identifiers: idents[:1], + Profile: "test", // We do not expect reuse because the order names don't match firstOrder ExpectReuse: false, }, { - Name: "Duplicate order, different regID", - OrderReq: &rapb.NewOrderRequest{ - RegistrationID: secondReg.Id, - Names: names, - }, - // We do not expect reuse because the order regID differs from firstOrder + Name: "Superset of order names, same regID", + RegistrationID: Registration.Id, + Identifiers: append(idents, identifier.NewDNS("blog.zombo.com")), + Profile: "test", + // We do not expect reuse because the order names don't match firstOrder ExpectReuse: false, }, { - Name: "Duplicate order, same regID, first expired", - OrderReq: orderReq, - AdvanceClock: &doubleLifetime, - // We do not expect reuse because firstOrder has expired - ExpectReuse: true, + Name: "Missing profile, same regID", + RegistrationID: Registration.Id, + Identifiers: append(idents, identifier.NewDNS("blog.zombo.com")), + // We do not expect reuse because the profile is missing + ExpectReuse: false, }, + { + Name: "Missing profile, same regID", + RegistrationID: Registration.Id, + Identifiers: append(idents, identifier.NewDNS("blog.zombo.com")), + Profile: "different", + // We do not expect reuse because a different profile is specified + ExpectReuse: false, + }, + { + Name: "Duplicate order, different regID", + RegistrationID: secondReg.Id, + Identifiers: idents, + Profile: "test", + // We do not expect reuse because the order regID differs from firstOrder + ExpectReuse: false, + }, + // TODO(#7324): Integrate certificate profile variance into this test. } for _, tc := range testCases { t.Run(tc.Name, func(t *testing.T) { - // If the testcase specifies, advance the clock before adding the order - if tc.AdvanceClock != nil { - _ = fc.Now().Add(*tc.AdvanceClock) - } // Add the order for the test request - order, err := ra.NewOrder(ctx, tc.OrderReq) - // It shouldn't fail + order, err := ra.NewOrder(context.Background(), &rapb.NewOrderRequest{ + RegistrationID: tc.RegistrationID, + Identifiers: tc.Identifiers.ToProtoSlice(), + CertificateProfileName: tc.Profile, + }) test.AssertNotError(t, err, "NewOrder returned an unexpected error") - // The order should not have a nil ID test.AssertNotNil(t, order.Id, "NewOrder returned an order with a nil Id") if tc.ExpectReuse { // If we expected order reuse for this testcase assert that the order // has the same ID as the firstOrder - test.AssertEquals(t, firstOrder.Id, order.Id) + test.AssertEquals(t, order.Id, firstOrder.Id) } else { // Otherwise assert that the order doesn't have the same ID as the // firstOrder - test.AssertNotEquals(t, firstOrder.Id, order.Id) + test.AssertNotEquals(t, order.Id, firstOrder.Id) } }) } } -func TestNewOrderReuseInvalidAuthz(t *testing.T) { - _, _, ra, _, cleanUp := initAuthorities(t) +// TestNewOrder_OrderReuse_Expired tests that expired orders are not reused. +// This is not simply a test case in TestNewOrder_OrderReuse because it has +// side effects. +func TestNewOrder_OrderReuse_Expired(t *testing.T) { + _, _, ra, _, fc, cleanUp := initAuthorities(t) defer cleanUp() - ctx := context.Background() - names := []string{"zombo.com"} + // Set the order lifetime to something short and known. + ra.profiles.def().orderLifetime = time.Hour - // Create an initial request with regA and names - orderReq := &rapb.NewOrderRequest{ + // Create an initial order. + extant, err := ra.NewOrder(context.Background(), &rapb.NewOrderRequest{ RegistrationID: Registration.Id, - Names: names, + Identifiers: []*corepb.Identifier{ + identifier.NewDNS("a.com").ToProto(), + identifier.NewDNS("b.com").ToProto(), + }, + }) + test.AssertNotError(t, err, "creating test order") + + // Transition the original order to status invalid by jumping forward in time + // to when it has expired. + fc.Set(extant.Expires.AsTime().Add(2 * time.Hour)) + + // Now a new order for the same names should not reuse the first one. + new, err := ra.NewOrder(context.Background(), &rapb.NewOrderRequest{ + RegistrationID: Registration.Id, + Identifiers: []*corepb.Identifier{ + identifier.NewDNS("a.com").ToProto(), + identifier.NewDNS("b.com").ToProto(), + }, + }) + test.AssertNotError(t, err, "creating test order") + test.AssertNotEquals(t, new.Id, extant.Id) +} + +// TestNewOrder_OrderReuse_Invalid tests that invalid orders are not reused. +// This is not simply a test case in TestNewOrder_OrderReuse because it has +// side effects. +func TestNewOrder_OrderReuse_Invalid(t *testing.T) { + _, sa, ra, _, _, cleanUp := initAuthorities(t) + defer cleanUp() + + // Create an initial order. + extant, err := ra.NewOrder(context.Background(), &rapb.NewOrderRequest{ + RegistrationID: Registration.Id, + Identifiers: []*corepb.Identifier{ + identifier.NewDNS("a.com").ToProto(), + identifier.NewDNS("b.com").ToProto(), + }, + }) + test.AssertNotError(t, err, "creating test order") + + // Transition the original order to status invalid by invalidating one of its + // authorizations. + _, err = sa.DeactivateAuthorization2(context.Background(), &sapb.AuthorizationID2{ + Id: extant.V2Authorizations[0], + }) + test.AssertNotError(t, err, "deactivating test authorization") + + // Now a new order for the same names should not reuse the first one. + new, err := ra.NewOrder(context.Background(), &rapb.NewOrderRequest{ + RegistrationID: Registration.Id, + Identifiers: []*corepb.Identifier{ + identifier.NewDNS("a.com").ToProto(), + identifier.NewDNS("b.com").ToProto(), + }, + }) + test.AssertNotError(t, err, "creating test order") + test.AssertNotEquals(t, new.Id, extant.Id) +} + +func TestNewOrder_AuthzReuse(t *testing.T) { + _, sa, ra, _, fc, cleanUp := initAuthorities(t) + defer cleanUp() + + // Create three initial authzs by creating an initial order, then updating + // the individual authz statuses. + const ( + pending = "a-pending.com" + valid = "b-valid.com" + invalid = "c-invalid.com" + ) + extant, err := ra.NewOrder(context.Background(), &rapb.NewOrderRequest{ + RegistrationID: Registration.Id, + Identifiers: []*corepb.Identifier{ + identifier.NewDNS(pending).ToProto(), + identifier.NewDNS(valid).ToProto(), + identifier.NewDNS(invalid).ToProto(), + }, + }) + test.AssertNotError(t, err, "creating test order") + extantAuthzs := map[string]int64{ + // Take advantage of the fact that authz IDs are returned in the same order + // as the lexicographically-sorted identifiers. + pending: extant.V2Authorizations[0], + valid: extant.V2Authorizations[1], + invalid: extant.V2Authorizations[2], + } + _, err = sa.FinalizeAuthorization2(context.Background(), &sapb.FinalizeAuthorizationRequest{ + Id: extantAuthzs[valid], + Status: string(core.StatusValid), + Attempted: "hello", + Expires: timestamppb.New(fc.Now().Add(48 * time.Hour)), + }) + test.AssertNotError(t, err, "marking test authz as valid") + _, err = sa.DeactivateAuthorization2(context.Background(), &sapb.AuthorizationID2{ + Id: extantAuthzs[invalid], + }) + test.AssertNotError(t, err, "marking test authz as invalid") + + // Create a second registration to reference later. + acctKeyB, err := AccountKeyB.MarshalJSON() + test.AssertNotError(t, err, "failed to marshal account key") + input := &corepb.Registration{Key: acctKeyB} + secondReg, err := ra.NewRegistration(context.Background(), input) + test.AssertNotError(t, err, "Error creating a second test registration") + + testCases := []struct { + Name string + RegistrationID int64 + Identifier identifier.ACMEIdentifier + Profile string + ExpectReuse bool + }{ + { + Name: "Reuse pending authz", + RegistrationID: Registration.Id, + Identifier: identifier.NewDNS(pending), + ExpectReuse: true, // TODO(#7715): Invert this. + }, + { + Name: "Reuse valid authz", + RegistrationID: Registration.Id, + Identifier: identifier.NewDNS(valid), + ExpectReuse: true, + }, + { + Name: "Don't reuse invalid authz", + RegistrationID: Registration.Id, + Identifier: identifier.NewDNS(invalid), + ExpectReuse: false, + }, + { + Name: "Don't reuse valid authz with wrong profile", + RegistrationID: Registration.Id, + Identifier: identifier.NewDNS(valid), + Profile: "test", + ExpectReuse: false, + }, + { + Name: "Don't reuse valid authz from other acct", + RegistrationID: secondReg.Id, + Identifier: identifier.NewDNS(valid), + ExpectReuse: false, + }, } - // First, add an order with `names` for regA - order, err := ra.NewOrder(ctx, orderReq) - // It shouldn't fail - test.AssertNotError(t, err, "Adding an initial order for regA failed") - // It should have an ID - test.AssertNotNil(t, order.Id, "Initial order had a nil ID") - // It should have one authorization - test.AssertEquals(t, numAuthorizations(order), 1) + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + new, err := ra.NewOrder(context.Background(), &rapb.NewOrderRequest{ + RegistrationID: tc.RegistrationID, + Identifiers: []*corepb.Identifier{tc.Identifier.ToProto()}, + CertificateProfileName: tc.Profile, + }) + test.AssertNotError(t, err, "creating test order") + test.AssertNotEquals(t, new.Id, extant.Id) - _, err = ra.SA.FinalizeAuthorization2(ctx, &sapb.FinalizeAuthorizationRequest{ - Id: order.V2Authorizations[0], - Status: string(core.StatusInvalid), - Expires: order.Expires, - Attempted: string(core.ChallengeTypeDNS01), - AttemptedAt: timestamppb.New(ra.clk.Now()), - }) - test.AssertNotError(t, err, "FinalizeAuthorization2 failed") - - // The order associated with the authz should now be invalid - updatedOrder, err := ra.SA.GetOrder(ctx, &sapb.OrderRequest{Id: order.Id}) - test.AssertNotError(t, err, "Error getting order to check status") - test.AssertEquals(t, updatedOrder.Status, "invalid") - - // Create a second order for the same names/regID - secondOrder, err := ra.NewOrder(ctx, orderReq) - // It shouldn't fail - test.AssertNotError(t, err, "Adding an initial order for regA failed") - // It should have a different ID than the first now-invalid order - test.AssertNotEquals(t, secondOrder.Id, order.Id) - // It should be status pending - test.AssertEquals(t, secondOrder.Status, "pending") - test.AssertEquals(t, numAuthorizations(secondOrder), 1) - // It should have a different authorization than the first order's now-invalid authorization - test.AssertNotEquals(t, secondOrder.V2Authorizations[0], order.V2Authorizations[0]) + if tc.ExpectReuse { + test.AssertEquals(t, new.V2Authorizations[0], extantAuthzs[tc.Identifier.Value]) + } else { + test.AssertNotEquals(t, new.V2Authorizations[0], extantAuthzs[tc.Identifier.Value]) + } + }) + } } -// mockSACountPendingFails has a CountPendingAuthorizations2 implementation -// that always returns error -type mockSACountPendingFails struct { - sapb.StorageAuthorityClient -} - -func (mock *mockSACountPendingFails) CountPendingAuthorizations2(ctx context.Context, req *sapb.RegistrationID, _ ...grpc.CallOption) (*sapb.Count, error) { - return nil, errors.New("counting is slow and boring") -} - -// Ensure that we don't bother to call the SA to count pending authorizations -// when an "unlimited" limit is set. -func TestPendingAuthorizationsUnlimited(t *testing.T) { - _, _, ra, _, cleanUp := initAuthorities(t) +// TestNewOrder_AuthzReuse_NoPending tests that authz reuse doesn't reuse +// pending authzs when a feature flag is set. +// This is not simply a test case in TestNewOrder_OrderReuse because it relies +// on feature-flag gated behavior. It should be unified with that function when +// the feature flag is removed. +func TestNewOrder_AuthzReuse_NoPending(t *testing.T) { + // TODO(#7715): Integrate these cases into TestNewOrder_AuthzReuse. + _, _, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() - ra.rlPolicies = &dummyRateLimitConfig{ - PendingAuthorizationsPerAccountPolicy: ratelimit.RateLimitPolicy{ - Threshold: 1, - Window: config.Duration{Duration: 24 * time.Hour}, - RegistrationOverrides: map[int64]int64{ - 13: -1, + features.Set(features.Config{NoPendingAuthzReuse: true}) + defer features.Reset() + + // Create an initial order and two pending authzs. + extant, err := ra.NewOrder(context.Background(), &rapb.NewOrderRequest{ + RegistrationID: Registration.Id, + Identifiers: []*corepb.Identifier{ + identifier.NewDNS("a.com").ToProto(), + identifier.NewDNS("b.com").ToProto(), + }, + }) + test.AssertNotError(t, err, "creating test order") + + // With the feature flag enabled, creating a new order for one of these names + // should not reuse the existing pending authz. + new, err := ra.NewOrder(context.Background(), &rapb.NewOrderRequest{ + RegistrationID: Registration.Id, + Identifiers: []*corepb.Identifier{identifier.NewDNS("a.com").ToProto()}, + }) + test.AssertNotError(t, err, "creating test order") + test.AssertNotEquals(t, new.Id, extant.Id) + test.AssertNotEquals(t, new.V2Authorizations[0], extant.V2Authorizations[0]) +} + +func TestNewOrder_ValidationProfiles(t *testing.T) { + _, _, ra, _, _, cleanUp := initAuthorities(t) + defer cleanUp() + + ra.profiles = &validationProfiles{ + defaultName: "one", + byName: map[string]*validationProfile{ + "one": { + pendingAuthzLifetime: 1 * 24 * time.Hour, + validAuthzLifetime: 1 * 24 * time.Hour, + orderLifetime: 1 * 24 * time.Hour, + maxNames: 10, + identifierTypes: []identifier.IdentifierType{identifier.TypeDNS}, + }, + "two": { + pendingAuthzLifetime: 2 * 24 * time.Hour, + validAuthzLifetime: 2 * 24 * time.Hour, + orderLifetime: 2 * 24 * time.Hour, + maxNames: 10, + identifierTypes: []identifier.IdentifierType{identifier.TypeDNS}, }, }, } - ra.SA = &mockSACountPendingFails{} + for _, tc := range []struct { + name string + profile string + wantExpires time.Time + }{ + { + // A request with no profile should get an order and authzs with one-day lifetimes. + name: "no profile specified", + profile: "", + wantExpires: ra.clk.Now().Add(1 * 24 * time.Hour), + }, + { + // A request for profile one should get an order and authzs with one-day lifetimes. + name: "profile one", + profile: "one", + wantExpires: ra.clk.Now().Add(1 * 24 * time.Hour), + }, + { + // A request for profile two should get an order and authzs with one-day lifetimes. + name: "profile two", + profile: "two", + wantExpires: ra.clk.Now().Add(2 * 24 * time.Hour), + }, + } { + t.Run(tc.name, func(t *testing.T) { + order, err := ra.NewOrder(context.Background(), &rapb.NewOrderRequest{ + RegistrationID: Registration.Id, + Identifiers: []*corepb.Identifier{identifier.NewDNS(randomDomain()).ToProto()}, + CertificateProfileName: tc.profile, + }) + if err != nil { + t.Fatalf("creating order: %s", err) + } + gotExpires := order.Expires.AsTime() + if gotExpires != tc.wantExpires { + t.Errorf("NewOrder(profile: %q).Expires = %s, expected %s", tc.profile, gotExpires, tc.wantExpires) + } - limit := ra.rlPolicies.PendingAuthorizationsPerAccount() - err := ra.checkPendingAuthorizationLimit(context.Background(), 13, limit) - test.AssertNotError(t, err, "checking pending authorization limit") -} - -// An authority that returns nonzero failures for CountInvalidAuthorizations2, -// and also returns existing authzs for the same domain from GetAuthorizations2 -type mockInvalidPlusValidAuthzAuthority struct { - mockSAWithAuthzs - domainWithFailures string -} - -func (sa *mockInvalidPlusValidAuthzAuthority) CountInvalidAuthorizations2(ctx context.Context, req *sapb.CountInvalidAuthorizationsRequest, _ ...grpc.CallOption) (*sapb.Count, error) { - if req.Hostname == sa.domainWithFailures { - return &sapb.Count{Count: 1}, nil - } else { - return &sapb.Count{}, nil + authz, err := ra.GetAuthorization(context.Background(), &rapb.GetAuthorizationRequest{ + Id: order.V2Authorizations[0], + }) + if err != nil { + t.Fatalf("fetching test authz: %s", err) + } + gotExpires = authz.Expires.AsTime() + if gotExpires != tc.wantExpires { + t.Errorf("GetAuthorization(profile: %q).Expires = %s, expected %s", tc.profile, gotExpires, tc.wantExpires) + } + }) } } -// Test that the failed authorizations limit is checked before authz reuse. -func TestNewOrderCheckFailedAuthorizationsFirst(t *testing.T) { - _, _, ra, clk, cleanUp := initAuthorities(t) +func TestNewOrder_ProfileSelectionAllowList(t *testing.T) { + _, _, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() - // Create an order (and thus a pending authz) for example.com - ctx := context.Background() - order, err := ra.NewOrder(ctx, &rapb.NewOrderRequest{ - RegistrationID: Registration.Id, - Names: []string{"example.com"}, - }) - test.AssertNotError(t, err, "adding an initial order for regA") - test.AssertNotNil(t, order.Id, "initial order had a nil ID") - test.AssertEquals(t, numAuthorizations(order), 1) - - // Now treat example.com as if it had a recent failure, but also a valid authz. - expires := clk.Now().Add(24 * time.Hour) - ra.SA = &mockInvalidPlusValidAuthzAuthority{ - mockSAWithAuthzs: mockSAWithAuthzs{ - authzs: map[string]*core.Authorization{ - "example.com": { - ID: "1", - Identifier: identifier.DNSIdentifier("example.com"), - RegistrationID: Registration.Id, - Expires: &expires, - Status: "valid", - Challenges: []core.Challenge{ - { - Type: core.ChallengeTypeHTTP01, - Status: core.StatusValid, - }, - }, - }, - }, + testCases := []struct { + name string + profile validationProfile + expectErr bool + expectErrContains string + }{ + { + name: "Allow all account IDs", + profile: validationProfile{allowList: nil}, + expectErr: false, }, - domainWithFailures: "example.com", - } - - // Set a very restrictive police for invalid authorizations - one failure - // and you're done for a day. - ra.rlPolicies = &dummyRateLimitConfig{ - InvalidAuthorizationsPerAccountPolicy: ratelimit.RateLimitPolicy{ - Threshold: 1, - Window: config.Duration{Duration: 24 * time.Hour}, + { + name: "Deny all but account Id 1337", + profile: validationProfile{allowList: allowlist.NewList([]int64{1337})}, + expectErr: true, + expectErrContains: "not permitted to use certificate profile", + }, + { + name: "Deny all", + profile: validationProfile{allowList: allowlist.NewList([]int64{})}, + expectErr: true, + expectErrContains: "not permitted to use certificate profile", + }, + { + name: "Allow Registration.Id", + profile: validationProfile{allowList: allowlist.NewList([]int64{Registration.Id})}, + expectErr: false, }, } - // Creating an order for example.com should error with the "too many failed - // authorizations recently" error. - _, err = ra.NewOrder(ctx, &rapb.NewOrderRequest{ - RegistrationID: Registration.Id, - Names: []string{"example.com"}, - }) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tc.profile.maxNames = 1 + tc.profile.identifierTypes = []identifier.IdentifierType{identifier.TypeDNS} + ra.profiles.byName = map[string]*validationProfile{ + "test": &tc.profile, + } - test.AssertError(t, err, "expected error for domain with too many failures") - test.AssertEquals(t, err.Error(), "too many failed authorizations recently: see https://letsencrypt.org/docs/failed-validation-limit/") + orderReq := &rapb.NewOrderRequest{ + RegistrationID: Registration.Id, + Identifiers: []*corepb.Identifier{identifier.NewDNS(randomDomain()).ToProto()}, + CertificateProfileName: "test", + } + _, err := ra.NewOrder(context.Background(), orderReq) + + if tc.expectErrContains != "" { + test.AssertErrorIs(t, err, berrors.Unauthorized) + test.AssertContains(t, err.Error(), tc.expectErrContains) + } else { + test.AssertNotError(t, err, "NewOrder failed") + } + }) + } +} + +func TestNewOrder_ProfileIdentifierTypes(t *testing.T) { + _, _, ra, _, _, cleanUp := initAuthorities(t) + defer cleanUp() + + testCases := []struct { + name string + identTypes []identifier.IdentifierType + idents []*corepb.Identifier + expectErr string + }{ + { + name: "Permit DNS, provide DNS names", + identTypes: []identifier.IdentifierType{identifier.TypeDNS}, + idents: []*corepb.Identifier{identifier.NewDNS(randomDomain()).ToProto(), identifier.NewDNS(randomDomain()).ToProto()}, + }, + { + name: "Permit IP, provide IPs", + identTypes: []identifier.IdentifierType{identifier.TypeIP}, + idents: []*corepb.Identifier{identifier.NewIP(randomIPv6()).ToProto(), identifier.NewIP(randomIPv6()).ToProto()}, + }, + { + name: "Permit DNS & IP, provide DNS & IP", + identTypes: []identifier.IdentifierType{identifier.TypeDNS, identifier.TypeIP}, + idents: []*corepb.Identifier{identifier.NewIP(randomIPv6()).ToProto(), identifier.NewDNS(randomDomain()).ToProto()}, + }, + { + name: "Permit DNS, provide IP", + identTypes: []identifier.IdentifierType{identifier.TypeDNS}, + idents: []*corepb.Identifier{identifier.NewIP(randomIPv6()).ToProto()}, + expectErr: "Profile \"test\" does not permit ip type identifiers", + }, + { + name: "Permit DNS, provide DNS & IP", + identTypes: []identifier.IdentifierType{identifier.TypeDNS}, + idents: []*corepb.Identifier{identifier.NewDNS(randomDomain()).ToProto(), identifier.NewIP(randomIPv6()).ToProto()}, + expectErr: "Profile \"test\" does not permit ip type identifiers", + }, + { + name: "Permit IP, provide DNS", + identTypes: []identifier.IdentifierType{identifier.TypeIP}, + idents: []*corepb.Identifier{identifier.NewDNS(randomDomain()).ToProto()}, + expectErr: "Profile \"test\" does not permit dns type identifiers", + }, + { + name: "Permit IP, provide DNS & IP", + identTypes: []identifier.IdentifierType{identifier.TypeIP}, + idents: []*corepb.Identifier{identifier.NewIP(randomIPv6()).ToProto(), identifier.NewDNS(randomDomain()).ToProto()}, + expectErr: "Profile \"test\" does not permit dns type identifiers", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var profile validationProfile + profile.maxNames = 2 + profile.identifierTypes = tc.identTypes + ra.profiles.byName = map[string]*validationProfile{ + "test": &profile, + } + + orderReq := &rapb.NewOrderRequest{ + RegistrationID: Registration.Id, + Identifiers: tc.idents, + CertificateProfileName: "test", + } + _, err := ra.NewOrder(context.Background(), orderReq) + + if tc.expectErr != "" { + test.AssertErrorIs(t, err, berrors.RejectedIdentifier) + test.AssertContains(t, err.Error(), tc.expectErr) + } else { + test.AssertNotError(t, err, "NewOrder failed") + } + }) + } } // mockSAWithAuthzs has a GetAuthorizations2 method that returns the protobuf @@ -2369,7 +2028,7 @@ func TestNewOrderCheckFailedAuthorizationsFirst(t *testing.T) { // facilitate the full execution of RA.NewOrder. type mockSAWithAuthzs struct { sapb.StorageAuthorityClient - authzs map[string]*core.Authorization + authzs []*core.Authorization } // GetOrderForNames is a mock which always returns NotFound so that NewOrder @@ -2378,38 +2037,55 @@ func (msa *mockSAWithAuthzs) GetOrderForNames(ctx context.Context, req *sapb.Get return nil, berrors.NotFoundError("no such order") } -// GetAuthorizations2 returns a _bizarre_ authorization for "*.zombo.com" that +// GetValidAuthorizations2 returns a _bizarre_ authorization for "*.zombo.com" that // was validated by HTTP-01. This should never happen in real life since the // name is a wildcard. We use this mock to test that we reject this bizarre // situation correctly. -func (msa *mockSAWithAuthzs) GetAuthorizations2(ctx context.Context, req *sapb.GetAuthorizationsRequest, _ ...grpc.CallOption) (*sapb.Authorizations, error) { +func (msa *mockSAWithAuthzs) GetValidAuthorizations2(ctx context.Context, req *sapb.GetValidAuthorizationsRequest, _ ...grpc.CallOption) (*sapb.Authorizations, error) { resp := &sapb.Authorizations{} - for k, v := range msa.authzs { + for _, v := range msa.authzs { authzPB, err := bgrpc.AuthzToPB(*v) if err != nil { return nil, err } - resp.Authz = append(resp.Authz, &sapb.Authorizations_MapElement{Domain: k, Authz: authzPB}) + resp.Authzs = append(resp.Authzs, authzPB) } return resp, nil } +func (msa *mockSAWithAuthzs) GetAuthorizations2(ctx context.Context, req *sapb.GetAuthorizationsRequest, _ ...grpc.CallOption) (*sapb.Authorizations, error) { + return msa.GetValidAuthorizations2(ctx, &sapb.GetValidAuthorizationsRequest{ + RegistrationID: req.RegistrationID, + Identifiers: req.Identifiers, + ValidUntil: req.ValidUntil, + }) +} + +func (msa *mockSAWithAuthzs) GetAuthorization2(ctx context.Context, req *sapb.AuthorizationID2, _ ...grpc.CallOption) (*corepb.Authorization, error) { + for _, authz := range msa.authzs { + if authz.ID == fmt.Sprintf("%d", req.Id) { + return bgrpc.AuthzToPB(*authz) + } + } + return nil, berrors.NotFoundError("no such authz") +} + // NewOrderAndAuthzs is a mock which just reflects the incoming request back, // pretending to have created new db rows for the requested newAuthzs. func (msa *mockSAWithAuthzs) NewOrderAndAuthzs(ctx context.Context, req *sapb.NewOrderAndAuthzsRequest, _ ...grpc.CallOption) (*corepb.Order, error) { authzIDs := req.NewOrder.V2Authorizations for range req.NewAuthzs { - authzIDs = append(authzIDs, mrand.Int63()) + authzIDs = append(authzIDs, mrand.Int64()) } return &corepb.Order{ // Fields from the input new order request. RegistrationID: req.NewOrder.RegistrationID, Expires: req.NewOrder.Expires, - Names: req.NewOrder.Names, + Identifiers: req.NewOrder.Identifiers, V2Authorizations: authzIDs, CertificateProfileName: req.NewOrder.CertificateProfileName, // Mock new fields generated by the database transaction. - Id: mrand.Int63(), + Id: mrand.Int64(), Created: timestamppb.Now(), // A new order is never processing because it can't have been finalized yet. BeganProcessing: false, @@ -2424,21 +2100,21 @@ func (msa *mockSAWithAuthzs) NewOrderAndAuthzs(ctx context.Context, req *sapb.Ne // for background - this safety check was previously broken! // https://github.com/letsencrypt/boulder/issues/3420 func TestNewOrderAuthzReuseSafety(t *testing.T) { - _, _, ra, _, cleanUp := initAuthorities(t) + _, _, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() ctx := context.Background() - names := []string{"*.zombo.com"} + idents := identifier.ACMEIdentifiers{identifier.NewDNS("*.zombo.com")} // Use a mock SA that always returns a valid HTTP-01 authz for the name // "zombo.com" expires := time.Now() ra.SA = &mockSAWithAuthzs{ - authzs: map[string]*core.Authorization{ - "*.zombo.com": { + authzs: []*core.Authorization{ + { // A static fake ID we can check for in a unit test ID: "1", - Identifier: identifier.DNSIdentifier("*.zombo.com"), + Identifier: identifier.NewDNS("*.zombo.com"), RegistrationID: Registration.Id, // Authz is valid Status: "valid", @@ -2448,18 +2124,20 @@ func TestNewOrderAuthzReuseSafety(t *testing.T) { { Type: core.ChallengeTypeHTTP01, // The dreaded HTTP-01! X__X Status: core.StatusValid, + Token: core.NewToken(), }, // DNS-01 challenge is pending { Type: core.ChallengeTypeDNS01, Status: core.StatusPending, + Token: core.NewToken(), }, }, }, - "zombo.com": { + { // A static fake ID we can check for in a unit test ID: "2", - Identifier: identifier.DNSIdentifier("zombo.com"), + Identifier: identifier.NewDNS("zombo.com"), RegistrationID: Registration.Id, // Authz is valid Status: "valid", @@ -2469,11 +2147,13 @@ func TestNewOrderAuthzReuseSafety(t *testing.T) { { Type: core.ChallengeTypeHTTP01, Status: core.StatusValid, + Token: core.NewToken(), }, // DNS-01 challenge is pending { Type: core.ChallengeTypeDNS01, Status: core.StatusPending, + Token: core.NewToken(), }, }, }, @@ -2483,27 +2163,27 @@ func TestNewOrderAuthzReuseSafety(t *testing.T) { // Create an initial request with regA and names orderReq := &rapb.NewOrderRequest{ RegistrationID: Registration.Id, - Names: names, + Identifiers: idents.ToProtoSlice(), } // Create an order for that request - order, err := ra.NewOrder(ctx, orderReq) - // It shouldn't fail - test.AssertNotError(t, err, "Adding an initial order for regA failed") - test.AssertEquals(t, numAuthorizations(order), 1) - // It should *not* be the bad authorization! - test.AssertNotEquals(t, order.V2Authorizations[0], int64(1)) + _, err := ra.NewOrder(ctx, orderReq) + // It should fail + test.AssertError(t, err, "Added an initial order for regA with invalid challenge(s)") + test.AssertContains(t, err.Error(), "SA.GetAuthorizations returned a DNS wildcard authz (1) with invalid challenge(s)") } func TestNewOrderWildcard(t *testing.T) { - _, _, ra, _, cleanUp := initAuthorities(t) + _, _, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() - ra.orderLifetime = time.Hour - orderNames := []string{"example.com", "*.welcome.zombo.com"} + orderIdents := identifier.ACMEIdentifiers{ + identifier.NewDNS("example.com"), + identifier.NewDNS("*.welcome.zombo.com"), + } wildcardOrderRequest := &rapb.NewOrderRequest{ RegistrationID: Registration.Id, - Names: orderNames, + Identifiers: orderIdents.ToProtoSlice(), } order, err := ra.NewOrder(context.Background(), wildcardOrderRequest) @@ -2511,12 +2191,13 @@ func TestNewOrderWildcard(t *testing.T) { // We expect the order to be pending test.AssertEquals(t, order.Status, string(core.StatusPending)) - // We expect the order to have two names - test.AssertEquals(t, len(order.Names), 2) - // We expect the order to have the names we requested + // We expect the order to have two identifiers + test.AssertEquals(t, len(order.Identifiers), 2) + + // We expect the order to have the identifiers we requested test.AssertDeepEquals(t, - core.UniqueLowerNames(order.Names), - core.UniqueLowerNames(orderNames)) + identifier.Normalize(identifier.FromProtoSlice(order.Identifiers)), + identifier.Normalize(orderIdents)) test.AssertEquals(t, numAuthorizations(order), 2) // Check each of the authz IDs in the order @@ -2541,7 +2222,7 @@ func TestNewOrderWildcard(t *testing.T) { test.AssertEquals(t, authz.Challenges[0].Type, core.ChallengeTypeDNS01) case "example.com": // If the authz is for example.com, we expect it has normal challenges - test.AssertEquals(t, len(authz.Challenges), 2) + test.AssertEquals(t, len(authz.Challenges), 3) default: t.Fatalf("Received an authorization for a name not requested: %q", name) } @@ -2550,22 +2231,25 @@ func TestNewOrderWildcard(t *testing.T) { // An order for a base domain and a wildcard for the same base domain should // return just 2 authz's, one for the wildcard with a DNS-01 // challenge and one for the base domain with the normal challenges. - orderNames = []string{"zombo.com", "*.zombo.com"} + orderIdents = identifier.ACMEIdentifiers{ + identifier.NewDNS("zombo.com"), + identifier.NewDNS("*.zombo.com"), + } wildcardOrderRequest = &rapb.NewOrderRequest{ RegistrationID: Registration.Id, - Names: orderNames, + Identifiers: orderIdents.ToProtoSlice(), } order, err = ra.NewOrder(context.Background(), wildcardOrderRequest) test.AssertNotError(t, err, "NewOrder failed for a wildcard order request") // We expect the order to be pending test.AssertEquals(t, order.Status, string(core.StatusPending)) - // We expect the order to have two names - test.AssertEquals(t, len(order.Names), 2) - // We expect the order to have the names we requested + // We expect the order to have two identifiers + test.AssertEquals(t, len(order.Identifiers), 2) + // We expect the order to have the identifiers we requested test.AssertDeepEquals(t, - core.UniqueLowerNames(order.Names), - core.UniqueLowerNames(orderNames)) + identifier.Normalize(identifier.FromProtoSlice(order.Identifiers)), + identifier.Normalize(orderIdents)) test.AssertEquals(t, numAuthorizations(order), 2) for _, authzID := range order.V2Authorizations { @@ -2581,7 +2265,7 @@ func TestNewOrderWildcard(t *testing.T) { case "zombo.com": // We expect that the base domain identifier auth has the normal number of // challenges - test.AssertEquals(t, len(authz.Challenges), 2) + test.AssertEquals(t, len(authz.Challenges), 3) case "*.zombo.com": // We expect that the wildcard identifier auth has only a pending // DNS-01 type challenge @@ -2597,7 +2281,7 @@ func TestNewOrderWildcard(t *testing.T) { // pending authz for the domain normalOrderReq := &rapb.NewOrderRequest{ RegistrationID: Registration.Id, - Names: []string{"everything.is.possible.zombo.com"}, + Identifiers: []*corepb.Identifier{identifier.NewDNS("everything.is.possible.zombo.com").ToProto()}, } normalOrder, err := ra.NewOrder(context.Background(), normalOrderReq) test.AssertNotError(t, err, "NewOrder failed for a normal non-wildcard order") @@ -2615,15 +2299,15 @@ func TestNewOrderWildcard(t *testing.T) { // We expect the authz is for the identifier the correct domain test.AssertEquals(t, authz.Identifier.Value, "everything.is.possible.zombo.com") // We expect the authz has the normal # of challenges - test.AssertEquals(t, len(authz.Challenges), 2) + test.AssertEquals(t, len(authz.Challenges), 3) // Now submit an order request for a wildcard of the domain we just created an // order for. We should **NOT** reuse the authorization from the previous // order since we now require a DNS-01 challenge for the `*.` prefixed name. - orderNames = []string{"*.everything.is.possible.zombo.com"} + orderIdents = identifier.ACMEIdentifiers{identifier.NewDNS("*.everything.is.possible.zombo.com")} wildcardOrderRequest = &rapb.NewOrderRequest{ RegistrationID: Registration.Id, - Names: orderNames, + Identifiers: orderIdents.ToProtoSlice(), } order, err = ra.NewOrder(context.Background(), wildcardOrderRequest) test.AssertNotError(t, err, "NewOrder failed for a wildcard order request") @@ -2661,14 +2345,14 @@ func TestNewOrderWildcard(t *testing.T) { } func TestNewOrderExpiry(t *testing.T) { - _, _, ra, clk, cleanUp := initAuthorities(t) + _, _, ra, _, clk, cleanUp := initAuthorities(t) defer cleanUp() ctx := context.Background() - names := []string{"zombo.com"} + idents := identifier.ACMEIdentifiers{identifier.NewDNS("zombo.com")} // Set the order lifetime to 48 hours. - ra.orderLifetime = 48 * time.Hour + ra.profiles.def().orderLifetime = 48 * time.Hour // Use an expiry that is sooner than the configured order expiry but greater // than 24 hours away. @@ -2677,11 +2361,11 @@ func TestNewOrderExpiry(t *testing.T) { // Use a mock SA that always returns a soon-to-be-expired valid authz for // "zombo.com". ra.SA = &mockSAWithAuthzs{ - authzs: map[string]*core.Authorization{ - "zombo.com": { + authzs: []*core.Authorization{ + { // A static fake ID we can check for in a unit test ID: "1", - Identifier: identifier.DNSIdentifier("zombo.com"), + Identifier: identifier.NewDNS("zombo.com"), RegistrationID: Registration.Id, Expires: &fakeAuthzExpires, Status: "valid", @@ -2689,6 +2373,7 @@ func TestNewOrderExpiry(t *testing.T) { { Type: core.ChallengeTypeHTTP01, Status: core.StatusValid, + Token: core.NewToken(), }, }, }, @@ -2698,7 +2383,7 @@ func TestNewOrderExpiry(t *testing.T) { // Create an initial request with regA and names orderReq := &rapb.NewOrderRequest{ RegistrationID: Registration.Id, - Names: names, + Identifiers: idents.ToProtoSlice(), } // Create an order for that request @@ -2713,8 +2398,8 @@ func TestNewOrderExpiry(t *testing.T) { test.AssertEquals(t, order.Expires.AsTime(), fakeAuthzExpires) // Set the order lifetime to be lower than the fakeAuthzLifetime - ra.orderLifetime = 12 * time.Hour - expectedOrderExpiry := clk.Now().Add(ra.orderLifetime) + ra.profiles.def().orderLifetime = 12 * time.Hour + expectedOrderExpiry := clk.Now().Add(12 * time.Hour) // Create the order again order, err = ra.NewOrder(ctx, orderReq) // It shouldn't fail @@ -2728,16 +2413,15 @@ func TestNewOrderExpiry(t *testing.T) { } func TestFinalizeOrder(t *testing.T) { - _, sa, ra, fc, cleanUp := initAuthorities(t) + _, sa, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() - ra.orderLifetime = time.Hour // Create one finalized authorization for not-example.com and one finalized // authorization for www.not-example.org now := ra.clk.Now() exp := now.Add(365 * 24 * time.Hour) - authzIDA := createFinalizedAuthorization(t, sa, "not-example.com", exp, core.ChallengeTypeHTTP01, ra.clk.Now()) - authzIDB := createFinalizedAuthorization(t, sa, "www.not-example.com", exp, core.ChallengeTypeHTTP01, ra.clk.Now()) + authzIDA := createFinalizedAuthorization(t, sa, identifier.NewDNS("not-example.com"), exp, core.ChallengeTypeHTTP01, ra.clk.Now()) + authzIDB := createFinalizedAuthorization(t, sa, identifier.NewDNS("www.not-example.com"), exp, core.ChallengeTypeHTTP01, ra.clk.Now()) testKey, err := rsa.GenerateKey(rand.Reader, 2048) test.AssertNotError(t, err, "error generating test key") @@ -2775,7 +2459,7 @@ func TestFinalizeOrder(t *testing.T) { Subject: pkix.Name{CommonName: "not-example.com"}, DNSNames: []string{"not-example.com", "www.not-example.com"}, PublicKey: testKey.Public(), - NotBefore: fc.Now(), + NotBefore: now, BasicConstraintsValid: true, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, } @@ -2793,21 +2477,24 @@ func TestFinalizeOrder(t *testing.T) { // Add a new order for the fake reg ID fakeRegOrder, err := ra.NewOrder(context.Background(), &rapb.NewOrderRequest{ RegistrationID: Registration.Id, - Names: []string{"001.example.com"}, + Identifiers: []*corepb.Identifier{identifier.NewDNS("001.example.com").ToProto()}, }) test.AssertNotError(t, err, "Could not add test order for fake reg ID order ID") missingAuthzOrder, err := ra.NewOrder(context.Background(), &rapb.NewOrderRequest{ RegistrationID: Registration.Id, - Names: []string{"002.example.com"}, + Identifiers: []*corepb.Identifier{identifier.NewDNS("002.example.com").ToProto()}, }) test.AssertNotError(t, err, "Could not add test order for missing authz order ID") validatedOrder, err := sa.NewOrderAndAuthzs(context.Background(), &sapb.NewOrderAndAuthzsRequest{ NewOrder: &sapb.NewOrderRequest{ - RegistrationID: Registration.Id, - Expires: timestamppb.New(exp), - Names: []string{"not-example.com", "www.not-example.com"}, + RegistrationID: Registration.Id, + Expires: timestamppb.New(exp), + Identifiers: []*corepb.Identifier{ + identifier.NewDNS("not-example.com").ToProto(), + identifier.NewDNS("www.not-example.com").ToProto(), + }, V2Authorizations: []int64{authzIDA, authzIDB}, }, }) @@ -2844,11 +2531,11 @@ func TestFinalizeOrder(t *testing.T) { Id: 1, RegistrationID: 1, Status: string(core.StatusReady), - Names: []string{}, + Identifiers: []*corepb.Identifier{}, }, Csr: oneDomainCSR, }, - ExpectedErrMsg: "Order has no associated names", + ExpectedErrMsg: "Order has no associated identifiers", }, { Name: "Wrong order state (valid)", @@ -2857,7 +2544,7 @@ func TestFinalizeOrder(t *testing.T) { Id: 1, RegistrationID: 1, Status: string(core.StatusValid), - Names: []string{"a.com"}, + Identifiers: []*corepb.Identifier{identifier.NewDNS("a.com").ToProto()}, }, Csr: oneDomainCSR, }, @@ -2870,7 +2557,7 @@ func TestFinalizeOrder(t *testing.T) { Id: 1, RegistrationID: 1, Status: string(core.StatusPending), - Names: []string{"a.com"}, + Identifiers: []*corepb.Identifier{identifier.NewDNS("a.com").ToProto()}, }, Csr: oneDomainCSR, }, @@ -2884,7 +2571,7 @@ func TestFinalizeOrder(t *testing.T) { Id: 1, RegistrationID: 1, Status: string(core.StatusReady), - Names: []string{"a.com"}, + Identifiers: []*corepb.Identifier{identifier.NewDNS("a.com").ToProto()}, }, Csr: []byte{0xC0, 0xFF, 0xEE}, }, @@ -2897,11 +2584,14 @@ func TestFinalizeOrder(t *testing.T) { Id: 1, RegistrationID: 1, Status: string(core.StatusReady), - Names: []string{"a.com", "b.com"}, + Identifiers: []*corepb.Identifier{ + identifier.NewDNS("a.com").ToProto(), + identifier.NewDNS("b.com").ToProto(), + }, }, Csr: oneDomainCSR, }, - ExpectedErrMsg: "Order includes different number of names than CSR specifies", + ExpectedErrMsg: "CSR does not specify same identifiers as Order", }, { Name: "CSR and Order with diff number of names (other way)", @@ -2910,11 +2600,11 @@ func TestFinalizeOrder(t *testing.T) { Id: 1, RegistrationID: 1, Status: string(core.StatusReady), - Names: []string{"a.com"}, + Identifiers: []*corepb.Identifier{identifier.NewDNS("a.com").ToProto()}, }, Csr: twoDomainCSR, }, - ExpectedErrMsg: "Order includes different number of names than CSR specifies", + ExpectedErrMsg: "CSR does not specify same identifiers as Order", }, { Name: "CSR missing an order name", @@ -2923,11 +2613,11 @@ func TestFinalizeOrder(t *testing.T) { Id: 1, RegistrationID: 1, Status: string(core.StatusReady), - Names: []string{"foobar.com"}, + Identifiers: []*corepb.Identifier{identifier.NewDNS("foobar.com").ToProto()}, }, Csr: oneDomainCSR, }, - ExpectedErrMsg: "CSR is missing Order domain \"foobar.com\"", + ExpectedErrMsg: "CSR does not specify same identifiers as Order", }, { Name: "CSR with policy forbidden name", @@ -2936,7 +2626,7 @@ func TestFinalizeOrder(t *testing.T) { Id: 1, RegistrationID: 1, Status: string(core.StatusReady), - Names: []string{"example.org"}, + Identifiers: []*corepb.Identifier{identifier.NewDNS("example.org").ToProto()}, Expires: timestamppb.New(exp), CertificateSerial: "", BeganProcessing: false, @@ -2950,7 +2640,7 @@ func TestFinalizeOrder(t *testing.T) { OrderReq: &rapb.FinalizeOrderRequest{ Order: &corepb.Order{ Status: string(core.StatusReady), - Names: []string{"a.com"}, + Identifiers: []*corepb.Identifier{identifier.NewDNS("a.com").ToProto()}, Id: fakeRegOrder.Id, RegistrationID: fakeRegID, Expires: timestamppb.New(exp), @@ -2966,8 +2656,11 @@ func TestFinalizeOrder(t *testing.T) { Name: "Order with missing authorizations", OrderReq: &rapb.FinalizeOrderRequest{ Order: &corepb.Order{ - Status: string(core.StatusReady), - Names: []string{"a.com", "b.com"}, + Status: string(core.StatusReady), + Identifiers: []*corepb.Identifier{ + identifier.NewDNS("a.com").ToProto(), + identifier.NewDNS("b.com").ToProto(), + }, Id: missingAuthzOrder.Id, RegistrationID: Registration.Id, Expires: timestamppb.New(exp), @@ -2977,7 +2670,7 @@ func TestFinalizeOrder(t *testing.T) { }, Csr: twoDomainCSR, }, - ExpectedErrMsg: "authorizations for these names not found or expired: a.com, b.com", + ExpectedErrMsg: "authorizations for these identifiers not found: a.com, b.com", }, { Name: "Order with correct authorizations, ready status", @@ -3014,9 +2707,8 @@ func TestFinalizeOrder(t *testing.T) { } func TestFinalizeOrderWithMixedSANAndCN(t *testing.T) { - _, sa, ra, _, cleanUp := initAuthorities(t) + _, sa, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() - ra.orderLifetime = time.Hour // Pick an expiry in the future now := ra.clk.Now() @@ -3024,15 +2716,18 @@ func TestFinalizeOrderWithMixedSANAndCN(t *testing.T) { // Create one finalized authorization for Registration.Id for not-example.com and // one finalized authorization for Registration.Id for www.not-example.org - authzIDA := createFinalizedAuthorization(t, sa, "not-example.com", exp, core.ChallengeTypeHTTP01, ra.clk.Now()) - authzIDB := createFinalizedAuthorization(t, sa, "www.not-example.com", exp, core.ChallengeTypeHTTP01, ra.clk.Now()) + authzIDA := createFinalizedAuthorization(t, sa, identifier.NewDNS("not-example.com"), exp, core.ChallengeTypeHTTP01, ra.clk.Now()) + authzIDB := createFinalizedAuthorization(t, sa, identifier.NewDNS("www.not-example.com"), exp, core.ChallengeTypeHTTP01, ra.clk.Now()) // Create a new order to finalize with names in SAN and CN mixedOrder, err := sa.NewOrderAndAuthzs(context.Background(), &sapb.NewOrderAndAuthzsRequest{ NewOrder: &sapb.NewOrderRequest{ - RegistrationID: Registration.Id, - Expires: timestamppb.New(exp), - Names: []string{"not-example.com", "www.not-example.com"}, + RegistrationID: Registration.Id, + Expires: timestamppb.New(exp), + Identifiers: []*corepb.Identifier{ + identifier.NewDNS("not-example.com").ToProto(), + identifier.NewDNS("www.not-example.com").ToProto(), + }, V2Authorizations: []int64{authzIDA, authzIDB}, }, }) @@ -3076,7 +2771,7 @@ func TestFinalizeOrderWithMixedSANAndCN(t *testing.T) { } func TestFinalizeOrderWildcard(t *testing.T) { - _, sa, ra, _, cleanUp := initAuthorities(t) + _, sa, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() // Pick an expiry in the future @@ -3118,16 +2813,17 @@ func TestFinalizeOrderWildcard(t *testing.T) { ra.CA = ca // Create a new order for a wildcard domain - orderNames := []string{"*.zombo.com"} + orderIdents := identifier.ACMEIdentifiers{identifier.NewDNS("*.zombo.com")} + test.AssertNotError(t, err, "Converting identifiers to DNS names") wildcardOrderRequest := &rapb.NewOrderRequest{ RegistrationID: Registration.Id, - Names: orderNames, + Identifiers: orderIdents.ToProtoSlice(), } order, err := ra.NewOrder(context.Background(), wildcardOrderRequest) test.AssertNotError(t, err, "NewOrder failed for wildcard domain order") // Create one standard finalized authorization for Registration.Id for zombo.com - _ = createFinalizedAuthorization(t, sa, "zombo.com", exp, core.ChallengeTypeHTTP01, ra.clk.Now()) + _ = createFinalizedAuthorization(t, sa, identifier.NewDNS("zombo.com"), exp, core.ChallengeTypeHTTP01, ra.clk.Now()) // Finalizing the order should *not* work since the existing validated authz // is not a special DNS-01-Wildcard challenge authz, so the order will be @@ -3177,20 +2873,147 @@ func TestFinalizeOrderWildcard(t *testing.T) { "wildcard order") } -func TestIssueCertificateAuditLog(t *testing.T) { - _, sa, ra, _, cleanUp := initAuthorities(t) +func TestFinalizeOrderDisabledChallenge(t *testing.T) { + _, sa, ra, _, fc, cleanUp := initAuthorities(t) defer cleanUp() - // Set up order and authz expiries - ra.orderLifetime = 24 * time.Hour - exp := ra.clk.Now().Add(24 * time.Hour) + domain := randomDomain() + ident := identifier.NewDNS(domain) + + // Create a finalized authorization for that domain + authzID := createFinalizedAuthorization( + t, sa, ident, fc.Now().Add(24*time.Hour), core.ChallengeTypeHTTP01, fc.Now().Add(-1*time.Hour)) + + // Create an order that reuses that authorization + order, err := ra.NewOrder(context.Background(), &rapb.NewOrderRequest{ + RegistrationID: Registration.Id, + Identifiers: []*corepb.Identifier{ident.ToProto()}, + }) + test.AssertNotError(t, err, "creating test order") + test.AssertEquals(t, order.V2Authorizations[0], authzID) + + // Create a CSR for this order + testKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + test.AssertNotError(t, err, "generating test key") + csr, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{ + PublicKey: testKey.PublicKey, + DNSNames: []string{domain}, + }, testKey) + test.AssertNotError(t, err, "Error creating policy forbid CSR") + + // Replace the Policy Authority with one which has this challenge type disabled + pa, err := policy.New( + map[identifier.IdentifierType]bool{ + identifier.TypeDNS: true, + identifier.TypeIP: true, + }, + map[core.AcmeChallenge]bool{ + core.ChallengeTypeDNS01: true, + core.ChallengeTypeTLSALPN01: true, + }, + ra.log) + test.AssertNotError(t, err, "creating test PA") + err = pa.LoadHostnamePolicyFile("../test/hostname-policy.yaml") + test.AssertNotError(t, err, "loading test hostname policy") + ra.PA = pa + + // Now finalizing this order should fail + _, err = ra.FinalizeOrder(context.Background(), &rapb.FinalizeOrderRequest{ + Order: order, + Csr: csr, + }) + test.AssertError(t, err, "finalization should fail") + + // Unfortunately we can't test for the PA's "which is now disabled" error + // message directly, because the RA discards it and collects all invalid names + // into a single more generic error message. But it does at least distinguish + // between missing, expired, and invalid, so we can test for "invalid". + test.AssertContains(t, err.Error(), "authorizations for these identifiers not valid") +} + +func TestFinalizeWithMustStaple(t *testing.T) { + _, sa, ra, _, fc, cleanUp := initAuthorities(t) + defer cleanUp() + + ocspMustStapleExt := pkix.Extension{ + // RFC 7633: id-pe-tlsfeature OBJECT IDENTIFIER ::= { id-pe 24 } + Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 24}, + // ASN.1 encoding of: + // SEQUENCE + // INTEGER 5 + // where "5" is the status_request feature (RFC 6066) + Value: []byte{0x30, 0x03, 0x02, 0x01, 0x05}, + } + + domain := randomDomain() + + authzID := createFinalizedAuthorization( + t, sa, identifier.NewDNS(domain), fc.Now().Add(24*time.Hour), core.ChallengeTypeHTTP01, fc.Now().Add(-1*time.Hour)) + + order, err := ra.NewOrder(context.Background(), &rapb.NewOrderRequest{ + RegistrationID: Registration.Id, + Identifiers: []*corepb.Identifier{identifier.NewDNS(domain).ToProto()}, + }) + test.AssertNotError(t, err, "creating test order") + test.AssertEquals(t, order.V2Authorizations[0], authzID) + + testKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + test.AssertNotError(t, err, "generating test key") + + csr, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{ + PublicKey: testKey.Public(), + DNSNames: []string{domain}, + ExtraExtensions: []pkix.Extension{ocspMustStapleExt}, + }, testKey) + test.AssertNotError(t, err, "creating must-staple CSR") + + serial, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64)) + test.AssertNotError(t, err, "generating random serial number") + template := &x509.Certificate{ + SerialNumber: serial, + Subject: pkix.Name{CommonName: domain}, + DNSNames: []string{domain}, + NotBefore: fc.Now(), + NotAfter: fc.Now().Add(365 * 24 * time.Hour), + BasicConstraintsValid: true, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, + ExtraExtensions: []pkix.Extension{ocspMustStapleExt}, + } + cert, err := x509.CreateCertificate(rand.Reader, template, template, testKey.Public(), testKey) + test.AssertNotError(t, err, "creating certificate") + ra.CA = &mocks.MockCA{ + PEM: pem.EncodeToMemory(&pem.Block{ + Bytes: cert, + Type: "CERTIFICATE", + }), + } + + _, err = ra.FinalizeOrder(context.Background(), &rapb.FinalizeOrderRequest{ + Order: order, + Csr: csr, + }) + test.AssertError(t, err, "finalization should fail") + test.AssertContains(t, err.Error(), "no longer available") + test.AssertMetricWithLabelsEquals(t, ra.mustStapleRequestsCounter, prometheus.Labels{"allowlist": "denied"}, 1) +} + +func TestIssueCertificateAuditLog(t *testing.T) { + _, sa, ra, _, _, cleanUp := initAuthorities(t) + defer cleanUp() // Make some valid authorizations for some names using different challenge types names := []string{"not-example.com", "www.not-example.com", "still.not-example.com", "definitely.not-example.com"} + idents := identifier.ACMEIdentifiers{ + identifier.NewDNS("not-example.com"), + identifier.NewDNS("www.not-example.com"), + identifier.NewDNS("still.not-example.com"), + identifier.NewDNS("definitely.not-example.com"), + } + exp := ra.clk.Now().Add(ra.profiles.def().orderLifetime) challs := []core.AcmeChallenge{core.ChallengeTypeHTTP01, core.ChallengeTypeDNS01, core.ChallengeTypeHTTP01, core.ChallengeTypeDNS01} var authzIDs []int64 - for i, name := range names { - authzIDs = append(authzIDs, createFinalizedAuthorization(t, sa, name, exp, challs[i], ra.clk.Now())) + for i, ident := range idents { + authzIDs = append(authzIDs, createFinalizedAuthorization(t, sa, ident, exp, challs[i], ra.clk.Now())) } // Create a pending order for all of the names @@ -3198,7 +3021,7 @@ func TestIssueCertificateAuditLog(t *testing.T) { NewOrder: &sapb.NewOrderRequest{ RegistrationID: Registration.Id, Expires: timestamppb.New(exp), - Names: names, + Identifiers: idents.ToProtoSlice(), V2Authorizations: authzIDs, }, }) @@ -3281,8 +3104,8 @@ func TestIssueCertificateAuditLog(t *testing.T) { test.AssertDeepEquals(t, event.VerifiedFields, []string{"subject.commonName", "subjectAltName"}) // The event CommonName should match the expected common name test.AssertEquals(t, event.CommonName, "not-example.com") - // The event names should match the order names - test.AssertDeepEquals(t, core.UniqueLowerNames(event.Names), core.UniqueLowerNames(order.Names)) + // The event identifiers should match the order identifiers + test.AssertDeepEquals(t, identifier.Normalize(event.Identifiers), identifier.Normalize(identifier.FromProtoSlice(order.Identifiers))) // The event's NotBefore and NotAfter should match the cert's test.AssertEquals(t, event.NotBefore, parsedCert.NotBefore) test.AssertEquals(t, event.NotAfter, parsedCert.NotAfter) @@ -3301,12 +3124,9 @@ func TestIssueCertificateAuditLog(t *testing.T) { } func TestIssueCertificateCAACheckLog(t *testing.T) { - _, sa, ra, fc, cleanUp := initAuthorities(t) + _, sa, ra, _, fc, cleanUp := initAuthorities(t) defer cleanUp() - - // Set up order and authz expiries. - ra.orderLifetime = 24 * time.Hour - ra.authorizationLifetime = 15 * time.Hour + ra.VA = va.RemoteClients{CAAClient: &noopCAA{}} exp := fc.Now().Add(24 * time.Hour) recent := fc.Now().Add(-1 * time.Hour) @@ -3314,14 +3134,20 @@ func TestIssueCertificateCAACheckLog(t *testing.T) { // Make some valid authzs for four names. Half of them were validated // recently and half were validated in excess of our CAA recheck time. - names := []string{"not-example.com", "www.not-example.com", "still.not-example.com", "definitely.not-example.com"} + names := []string{ + "not-example.com", + "www.not-example.com", + "still.not-example.com", + "definitely.not-example.com", + } + idents := identifier.NewDNSSlice(names) var authzIDs []int64 - for i, name := range names { + for i, ident := range idents { attemptedAt := older if i%2 == 0 { attemptedAt = recent } - authzIDs = append(authzIDs, createFinalizedAuthorization(t, sa, name, exp, core.ChallengeTypeHTTP01, attemptedAt)) + authzIDs = append(authzIDs, createFinalizedAuthorization(t, sa, ident, exp, core.ChallengeTypeHTTP01, attemptedAt)) } // Create a pending order for all of the names. @@ -3329,7 +3155,7 @@ func TestIssueCertificateCAACheckLog(t *testing.T) { NewOrder: &sapb.NewOrderRequest{ RegistrationID: Registration.Id, Expires: timestamppb.New(exp), - Names: names, + Identifiers: idents.ToProtoSlice(), V2Authorizations: authzIDs, }, }) @@ -3412,36 +3238,36 @@ func TestIssueCertificateCAACheckLog(t *testing.T) { // // See https://github.com/letsencrypt/boulder/issues/3201 func TestUpdateMissingAuthorization(t *testing.T) { - _, sa, ra, fc, cleanUp := initAuthorities(t) + _, sa, ra, _, fc, cleanUp := initAuthorities(t) defer cleanUp() ctx := context.Background() - authzPB := createPendingAuthorization(t, sa, Identifier, fc.Now().Add(12*time.Hour)) + authzPB := createPendingAuthorization(t, sa, identifier.NewDNS("example.com"), fc.Now().Add(12*time.Hour)) authz, err := bgrpc.PBToAuthz(authzPB) test.AssertNotError(t, err, "failed to deserialize authz") // Twiddle the authz to pretend its been validated by the VA authz.Challenges[0].Status = "valid" - err = ra.recordValidation(ctx, authz.ID, authz.Expires, &authz.Challenges[0]) + err = ra.recordValidation(ctx, authz.ID, fc.Now().Add(24*time.Hour), &authz.Challenges[0]) test.AssertNotError(t, err, "ra.recordValidation failed") // Try to record the same validation a second time. - err = ra.recordValidation(ctx, authz.ID, authz.Expires, &authz.Challenges[0]) + err = ra.recordValidation(ctx, authz.ID, fc.Now().Add(25*time.Hour), &authz.Challenges[0]) test.AssertError(t, err, "ra.recordValidation didn't fail") test.AssertErrorIs(t, err, berrors.NotFound) } func TestPerformValidationBadChallengeType(t *testing.T) { - _, _, ra, fc, cleanUp := initAuthorities(t) + _, _, ra, _, fc, cleanUp := initAuthorities(t) defer cleanUp() - pa, err := policy.New(map[core.AcmeChallenge]bool{}, blog.NewMock()) + pa, err := policy.New(map[identifier.IdentifierType]bool{}, map[core.AcmeChallenge]bool{}, blog.NewMock()) test.AssertNotError(t, err, "Couldn't create PA") ra.PA = pa exp := fc.Now().Add(10 * time.Hour) authz := core.Authorization{ ID: "1337", - Identifier: identifier.DNSIdentifier("not-example.com"), + Identifier: identifier.NewDNS("not-example.com"), RegistrationID: 1, Status: "valid", Challenges: []core.Challenge{ @@ -3472,257 +3298,59 @@ func (mp *timeoutPub) SubmitToSingleCTWithResult(_ context.Context, _ *pubpb.Req } func TestCTPolicyMeasurements(t *testing.T) { - _, ssa, ra, _, cleanup := initAuthorities(t) + _, _, ra, _, _, cleanup := initAuthorities(t) defer cleanup() ra.ctpolicy = ctpolicy.New(&timeoutPub{}, loglist.List{ - "OperA": { - "LogA1": {Url: "UrlA1", Key: "KeyA1"}, - }, - "OperB": { - "LogB1": {Url: "UrlB1", Key: "KeyB1"}, - }, + {Name: "LogA1", Operator: "OperA", Url: "UrlA1", Key: []byte("KeyA1")}, + {Name: "LogB1", Operator: "OperB", Url: "UrlB1", Key: []byte("KeyB1")}, }, nil, nil, 0, log, metrics.NoopRegisterer) - // Create valid authorizations for not-example.com and www.not-example.com - exp := ra.clk.Now().Add(365 * 24 * time.Hour) - authzIDA := createFinalizedAuthorization(t, ssa, "not-example.com", exp, core.ChallengeTypeHTTP01, ra.clk.Now()) - authzIDB := createFinalizedAuthorization(t, ssa, "www.not-example.com", exp, core.ChallengeTypeHTTP01, ra.clk.Now()) - - order, err := ra.SA.NewOrderAndAuthzs(context.Background(), &sapb.NewOrderAndAuthzsRequest{ - NewOrder: &sapb.NewOrderRequest{ - RegistrationID: Registration.Id, - Expires: timestamppb.New(exp), - Names: []string{"not-example.com", "www.not-example.com"}, - V2Authorizations: []int64{authzIDA, authzIDB}, - }, + _, cert := test.ThrowAwayCert(t, clock.NewFake()) + _, err := ra.GetSCTs(context.Background(), &rapb.SCTRequest{ + PrecertDER: cert.Raw, }) - test.AssertNotError(t, err, "error generating test order") - - testKey, err := rsa.GenerateKey(rand.Reader, 2048) - test.AssertNotError(t, err, "error generating test key") - - csr, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{ - PublicKey: testKey.Public(), - SignatureAlgorithm: x509.SHA256WithRSA, - DNSNames: []string{"not-example.com", "www.not-example.com"}, - }, testKey) - test.AssertNotError(t, err, "error generating test CSR") - - _, err = ra.FinalizeOrder(context.Background(), &rapb.FinalizeOrderRequest{ - Order: order, - Csr: csr, - }) - test.AssertError(t, err, "FinalizeOrder should have failed when SCTs timed out") - test.AssertContains(t, err.Error(), "getting SCTs") + test.AssertError(t, err, "GetSCTs should have failed when SCTs timed out") + test.AssertContains(t, err.Error(), "failed to get 2 SCTs") test.AssertMetricWithLabelsEquals(t, ra.ctpolicyResults, prometheus.Labels{"result": "failure"}, 1) } func TestWildcardOverlap(t *testing.T) { - err := wildcardOverlap([]string{ - "*.example.com", - "*.example.net", + err := wildcardOverlap(identifier.ACMEIdentifiers{ + identifier.NewDNS("*.example.com"), + identifier.NewDNS("*.example.net"), }) if err != nil { t.Errorf("Got error %q, expected none", err) } - err = wildcardOverlap([]string{ - "*.example.com", - "*.example.net", - "www.example.com", + err = wildcardOverlap(identifier.ACMEIdentifiers{ + identifier.NewDNS("*.example.com"), + identifier.NewDNS("*.example.net"), + identifier.NewDNS("www.example.com"), }) if err == nil { t.Errorf("Got no error, expected one") } test.AssertErrorIs(t, err, berrors.Malformed) - err = wildcardOverlap([]string{ - "*.foo.example.com", - "*.example.net", - "www.example.com", + err = wildcardOverlap(identifier.ACMEIdentifiers{ + identifier.NewDNS("*.foo.example.com"), + identifier.NewDNS("*.example.net"), + identifier.NewDNS("www.example.com"), }) if err != nil { t.Errorf("Got error %q, expected none", err) } } -// mockCAFailPrecert is a mock CA that always returns an error from `IssuePrecertificate` -type mockCAFailPrecert struct { - mocks.MockCA - err error -} - -func (ca *mockCAFailPrecert) IssuePrecertificate( - context.Context, - *capb.IssueCertificateRequest, - ...grpc.CallOption) (*capb.IssuePrecertificateResponse, error) { - return nil, ca.err -} - -// mockCAFailCertForPrecert is a mock CA that always returns an error from -// `IssueCertificateForPrecertificate` -type mockCAFailCertForPrecert struct { - mocks.MockCA - err error -} - -// IssuePrecertificate needs to be mocked for mockCAFailCertForPrecert's `IssueCertificateForPrecertificate` to get called. -func (ca *mockCAFailCertForPrecert) IssuePrecertificate( - context.Context, - *capb.IssueCertificateRequest, - ...grpc.CallOption) (*capb.IssuePrecertificateResponse, error) { - k, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - return nil, err - } - tmpl := &ctx509.Certificate{ - SerialNumber: big.NewInt(1), - ExtraExtensions: []ctpkix.Extension{ - { - Id: ctx509.OIDExtensionCTPoison, - Critical: true, - Value: ctasn1.NullBytes, - }, - }, - } - precert, err := ctx509.CreateCertificate(rand.Reader, tmpl, tmpl, k.Public(), k) - if err != nil { - return nil, err - } - return &capb.IssuePrecertificateResponse{ - DER: precert, - }, nil -} - -func (ca *mockCAFailCertForPrecert) IssueCertificateForPrecertificate( - context.Context, - *capb.IssueCertificateForPrecertificateRequest, - ...grpc.CallOption) (*corepb.Certificate, error) { - return &corepb.Certificate{}, ca.err -} - -// TestIssueCertificateInnerErrs tests that errors from the CA caught during -// `ra.issueCertificateInner` are propagated correctly, with the part of the -// issuance process that failed prefixed on the error message. -func TestIssueCertificateInnerErrs(t *testing.T) { - _, sa, ra, _, cleanUp := initAuthorities(t) - defer cleanUp() - - ra.orderLifetime = 24 * time.Hour - exp := ra.clk.Now().Add(24 * time.Hour) - - // Make some valid authorizations for some names - names := []string{"not-example.com", "www.not-example.com", "still.not-example.com", "definitely.not-example.com"} - var authzIDs []int64 - for _, name := range names { - authzIDs = append(authzIDs, createFinalizedAuthorization(t, sa, name, exp, core.ChallengeTypeHTTP01, ra.clk.Now())) - } - - // Create a pending order for all of the names - order, err := sa.NewOrderAndAuthzs(context.Background(), &sapb.NewOrderAndAuthzsRequest{ - NewOrder: &sapb.NewOrderRequest{ - RegistrationID: Registration.Id, - Expires: timestamppb.New(exp), - Names: names, - V2Authorizations: authzIDs, - }, - }) - test.AssertNotError(t, err, "Could not add test order with finalized authz IDs") - - // Generate a CSR covering the order names with a random RSA key - testKey, err := rsa.GenerateKey(rand.Reader, 2048) - test.AssertNotError(t, err, "error generating test key") - csr, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{ - PublicKey: testKey.PublicKey, - SignatureAlgorithm: x509.SHA256WithRSA, - Subject: pkix.Name{CommonName: "not-example.com"}, - DNSNames: names, - }, testKey) - test.AssertNotError(t, err, "Could not create test order CSR") - - csrOb, err := x509.ParseCertificateRequest(csr) - test.AssertNotError(t, err, "Error pasring generated CSR") - - testCases := []struct { - Name string - Mock capb.CertificateAuthorityClient - ExpectedErr error - ExpectedProb *berrors.BoulderError - }{ - { - Name: "vanilla error during IssuePrecertificate", - Mock: &mockCAFailPrecert{ - err: fmt.Errorf("bad bad not good"), - }, - ExpectedErr: fmt.Errorf("issuing precertificate: bad bad not good"), - }, - { - Name: "malformed problem during IssuePrecertificate", - Mock: &mockCAFailPrecert{ - err: berrors.MalformedError("detected 1x whack attack"), - }, - ExpectedProb: &berrors.BoulderError{ - Detail: "issuing precertificate: detected 1x whack attack", - Type: berrors.Malformed, - }, - }, - { - Name: "vanilla error during IssueCertificateForPrecertificate", - Mock: &mockCAFailCertForPrecert{ - err: fmt.Errorf("aaaaaaaaaaaaaaaaaaaa!!"), - }, - ExpectedErr: fmt.Errorf("issuing certificate for precertificate: aaaaaaaaaaaaaaaaaaaa!!"), - }, - { - Name: "malformed problem during IssueCertificateForPrecertificate", - Mock: &mockCAFailCertForPrecert{ - err: berrors.MalformedError("provided DER is DERanged"), - }, - ExpectedProb: &berrors.BoulderError{ - Detail: "issuing certificate for precertificate: provided DER is DERanged", - Type: berrors.Malformed, - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.Name, func(t *testing.T) { - // Mock the CA - ra.CA = tc.Mock - // Attempt issuance - _, _, err = ra.issueCertificateInner(ctx, csrOb, order.CertificateProfileName, accountID(Registration.Id), orderID(order.Id)) - // We expect all of the testcases to fail because all use mocked CAs that deliberately error - test.AssertError(t, err, "issueCertificateInner with failing mock CA did not fail") - // If there is an expected `error` then match the error message - if tc.ExpectedErr != nil { - test.AssertEquals(t, err.Error(), tc.ExpectedErr.Error()) - } else if tc.ExpectedProb != nil { - // If there is an expected `berrors.BoulderError` then we expect the - // `issueCertificateInner` error to be a `berrors.BoulderError` - var berr *berrors.BoulderError - test.AssertErrorWraps(t, err, &berr) - // Match the expected berror Type and Detail to the observed - test.AssertErrorIs(t, berr, tc.ExpectedProb.Type) - test.AssertEquals(t, berr.Detail, tc.ExpectedProb.Detail) - } - }) - } -} - type MockCARecordingProfile struct { inner *mocks.MockCA profileName string - profileHash []byte } -func (ca *MockCARecordingProfile) IssuePrecertificate(ctx context.Context, req *capb.IssueCertificateRequest, _ ...grpc.CallOption) (*capb.IssuePrecertificateResponse, error) { +func (ca *MockCARecordingProfile) IssueCertificate(ctx context.Context, req *capb.IssueCertificateRequest, _ ...grpc.CallOption) (*capb.IssueCertificateResponse, error) { ca.profileName = req.CertProfileName - return ca.inner.IssuePrecertificate(ctx, req) -} - -func (ca *MockCARecordingProfile) IssueCertificateForPrecertificate(ctx context.Context, req *capb.IssueCertificateForPrecertificateRequest, _ ...grpc.CallOption) (*corepb.Certificate, error) { - ca.profileHash = req.CertProfileHash - return ca.inner.IssueCertificateForPrecertificate(ctx, req) + return ca.inner.IssueCertificate(ctx, req) } type mockSAWithFinalize struct { @@ -3733,68 +3361,20 @@ func (sa *mockSAWithFinalize) FinalizeOrder(ctx context.Context, req *sapb.Final return &emptypb.Empty{}, nil } -func TestIssueCertificateInnerWithProfile(t *testing.T) { - _, _, ra, fc, cleanup := initAuthorities(t) - defer cleanup() - - // Generate a reasonable-looking CSR and cert to pass the matchesCSR check. - testKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - test.AssertNotError(t, err, "generating test key") - csrDER, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{DNSNames: []string{"example.com"}}, testKey) - test.AssertNotError(t, err, "creating test csr") - csr, err := x509.ParseCertificateRequest(csrDER) - test.AssertNotError(t, err, "parsing test csr") - certDER, err := x509.CreateCertificate(rand.Reader, &x509.Certificate{ - SerialNumber: big.NewInt(1), - DNSNames: []string{"example.com"}, - NotBefore: fc.Now(), - BasicConstraintsValid: true, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, - }, &x509.Certificate{}, testKey.Public(), testKey) - test.AssertNotError(t, err, "creating test cert") - certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) - - // Use a mock CA that will record the profile name and profile hash included - // in the RA's request messages. Populate it with the cert generated above. - mockCA := MockCARecordingProfile{inner: &mocks.MockCA{PEM: certPEM}} - ra.CA = &mockCA - - ra.SA = &mockSAWithFinalize{} - - // Call issueCertificateInner with the CSR generated above and the profile - // name "default", which will cause the mockCA to return a specific hash. - _, cpId, err := ra.issueCertificateInner(context.Background(), csr, "default", 1, 1) - test.AssertNotError(t, err, "issuing cert with profile name") - test.AssertEquals(t, mockCA.profileName, cpId.name) - test.AssertByteEquals(t, mockCA.profileHash, cpId.hash) +func (sa *mockSAWithFinalize) FQDNSetTimestampsForWindow(ctx context.Context, in *sapb.CountFQDNSetsRequest, opts ...grpc.CallOption) (*sapb.Timestamps, error) { + return &sapb.Timestamps{ + Timestamps: []*timestamppb.Timestamp{ + timestamppb.Now(), + }, + }, nil } func TestIssueCertificateOuter(t *testing.T) { - _, sa, ra, fc, cleanup := initAuthorities(t) + _, _, ra, _, fc, cleanup := initAuthorities(t) defer cleanup() + ra.SA = &mockSAWithFinalize{} - ra.orderLifetime = 24 * time.Hour - exp := ra.clk.Now().Add(24 * time.Hour) - - // Make some valid authorizations for some names - names := []string{"not-example.com", "www.not-example.com", "still.not-example.com", "definitely.not-example.com"} - var authzIDs []int64 - for _, name := range names { - authzIDs = append(authzIDs, createFinalizedAuthorization(t, sa, name, exp, core.ChallengeTypeHTTP01, ra.clk.Now())) - } - - // Create a pending order for all of the names - order, err := sa.NewOrderAndAuthzs(context.Background(), &sapb.NewOrderAndAuthzsRequest{ - NewOrder: &sapb.NewOrderRequest{ - RegistrationID: Registration.Id, - Expires: timestamppb.New(exp), - Names: names, - V2Authorizations: authzIDs, - CertificateProfileName: "philsProfile", - }, - }) - test.AssertNotError(t, err, "Could not add test order with finalized authz IDs") - + // Create a CSR to submit and a certificate for the fake CA to return. testKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) test.AssertNotError(t, err, "generating test key") csrDER, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{DNSNames: []string{"example.com"}}, testKey) @@ -3811,33 +3391,72 @@ func TestIssueCertificateOuter(t *testing.T) { test.AssertNotError(t, err, "creating test cert") certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) - // Use a mock CA that will record the profile name and profile hash included - // in the RA's request messages. Populate it with the cert generated above. - mockCA := MockCARecordingProfile{inner: &mocks.MockCA{PEM: certPEM}} - ra.CA = &mockCA + for _, tc := range []struct { + name string + profile string + wantProfile string + }{ + { + name: "select default profile when none specified", + wantProfile: "test", // matches ra.defaultProfileName + }, + { + name: "default profile specified", + profile: "test", + wantProfile: "test", + }, + { + name: "other profile specified", + profile: "other", + wantProfile: "other", + }, + } { + t.Run(tc.name, func(t *testing.T) { + // Use a mock CA that will record the profile name and profile hash included + // in the RA's request messages. Populate it with the cert generated above. + mockCA := MockCARecordingProfile{inner: &mocks.MockCA{PEM: certPEM}} + ra.CA = &mockCA - ra.SA = &mockSAWithFinalize{} + order := &corepb.Order{ + RegistrationID: Registration.Id, + Expires: timestamppb.New(fc.Now().Add(24 * time.Hour)), + Identifiers: []*corepb.Identifier{identifier.NewDNS("example.com").ToProto()}, + CertificateProfileName: tc.profile, + } - _, err = ra.issueCertificateOuter(context.Background(), order, csr, certificateRequestEvent{}) - test.AssertNotError(t, err, "Could not issue certificate") - test.AssertMetricWithLabelsEquals(t, ra.newCertCounter, prometheus.Labels{"profileName": mockCA.profileName, "profileHash": fmt.Sprintf("%x", mockCA.profileHash)}, 1) + order, err = ra.issueCertificateOuter(context.Background(), order, csr, certificateRequestEvent{}) + + // The resulting order should have new fields populated + if order.Status != string(core.StatusValid) { + t.Errorf("order.Status = %+v, want %+v", order.Status, core.StatusValid) + } + if order.CertificateSerial != core.SerialToString(big.NewInt(1)) { + t.Errorf("CertificateSerial = %+v, want %+v", order.CertificateSerial, 1) + } + + // The recorded profile and profile hash should match what we expect. + if mockCA.profileName != tc.wantProfile { + t.Errorf("recorded profileName = %+v, want %+v", mockCA.profileName, tc.wantProfile) + } + }) + } } func TestNewOrderMaxNames(t *testing.T) { - _, _, ra, _, cleanUp := initAuthorities(t) + _, _, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() - ra.maxNames = 2 + ra.profiles.def().maxNames = 2 _, err := ra.NewOrder(context.Background(), &rapb.NewOrderRequest{ RegistrationID: 1, - Names: []string{ - "a", - "b", - "c", + Identifiers: []*corepb.Identifier{ + identifier.NewDNS("a").ToProto(), + identifier.NewDNS("b").ToProto(), + identifier.NewDNS("c").ToProto(), }, }) test.AssertError(t, err, "NewOrder didn't fail with too many names in request") - test.AssertEquals(t, err.Error(), "Order cannot contain more than 2 DNS names") + test.AssertEquals(t, err.Error(), "Order cannot contain more than 2 identifiers") test.AssertErrorIs(t, err, berrors.Malformed) } @@ -3958,6 +3577,35 @@ func (msar *mockSARevocation) GetCertificateStatus(_ context.Context, req *sapb. return nil, berrors.UnknownSerialError() } +func (msar *mockSARevocation) GetCertificate(_ context.Context, req *sapb.Serial, _ ...grpc.CallOption) (*corepb.Certificate, error) { + var serialBytes [16]byte + _, _ = rand.Read(serialBytes[:]) + serial := big.NewInt(0).SetBytes(serialBytes[:]) + + key, err := ecdsa.GenerateKey(elliptic.P224(), rand.Reader) + if err != nil { + return nil, err + } + + template := &x509.Certificate{ + SerialNumber: serial, + DNSNames: []string{"revokememaybe.example.com"}, + NotBefore: time.Now(), + NotAfter: time.Now().Add(6 * 24 * time.Hour), + IssuingCertificateURL: []string{"http://localhost:4001/acme/issuer-cert/1234"}, + CRLDistributionPoints: []string{"http://example.com/123.crl"}, + } + + testCertDER, err := x509.CreateCertificate(rand.Reader, template, template, key.Public(), key) + if err != nil { + return nil, err + } + + return &corepb.Certificate{ + Der: testCertDER, + }, nil +} + func (msar *mockSARevocation) RevokeCertificate(_ context.Context, req *sapb.RevokeCertificateRequest, _ ...grpc.CallOption) (*emptypb.Empty, error) { if _, present := msar.revoked[req.Serial]; present { return nil, berrors.AlreadyRevokedError("already revoked") @@ -4019,7 +3667,7 @@ func (msgo *mockSAGenerateOCSP) GetCertificateStatus(_ context.Context, req *sap } func TestGenerateOCSP(t *testing.T) { - _, _, ra, clk, cleanUp := initAuthorities(t) + _, _, ra, _, clk, cleanUp := initAuthorities(t) defer cleanUp() ra.OCSP = &mockOCSPA{} @@ -4059,7 +3707,7 @@ func (msgo *mockSALongExpiredSerial) GetSerialMetadata(_ context.Context, req *s } func TestGenerateOCSPLongExpiredSerial(t *testing.T) { - _, _, ra, _, cleanUp := initAuthorities(t) + _, _, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() ra.OCSP = &mockOCSPA{} @@ -4087,7 +3735,7 @@ func (msgo *mockSAUnknownSerial) GetSerialMetadata(_ context.Context, req *sapb. } func TestGenerateOCSPUnknownSerial(t *testing.T) { - _, _, ra, _, cleanUp := initAuthorities(t) + _, _, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() ra.OCSP = &mockOCSPA{} @@ -4105,7 +3753,7 @@ func TestGenerateOCSPUnknownSerial(t *testing.T) { } func TestRevokeCertByApplicant_Subscriber(t *testing.T) { - _, _, ra, clk, cleanUp := initAuthorities(t) + _, _, ra, _, clk, cleanUp := initAuthorities(t) defer cleanUp() ra.OCSP = &mockOCSPA{} @@ -4172,20 +3820,15 @@ func (msa *mockSARevocationWithAuthzs) GetValidAuthorizations2(ctx context.Conte return authzs, nil } - for _, name := range req.Domains { - authzs.Authz = append(authzs.Authz, &sapb.Authorizations_MapElement{ - Domain: name, - Authz: &corepb.Authorization{ - Identifier: name, - }, - }) + for _, ident := range req.Identifiers { + authzs.Authzs = append(authzs.Authzs, &corepb.Authorization{Identifier: ident}) } return authzs, nil } func TestRevokeCertByApplicant_Controller(t *testing.T) { - _, _, ra, clk, cleanUp := initAuthorities(t) + _, _, ra, _, clk, cleanUp := initAuthorities(t) defer cleanUp() ra.OCSP = &mockOCSPA{} @@ -4211,7 +3854,7 @@ func TestRevokeCertByApplicant_Controller(t *testing.T) { RegID: 2, }) test.AssertError(t, err, "should have failed with wrong RegID") - test.AssertContains(t, err.Error(), "requester does not control all names") + test.AssertContains(t, err.Error(), "requester does not control all identifiers") // Revoking when the account does have valid authzs for the name should succeed, // but override the revocation reason to cessationOfOperation. @@ -4226,7 +3869,7 @@ func TestRevokeCertByApplicant_Controller(t *testing.T) { } func TestRevokeCertByKey(t *testing.T) { - _, _, ra, clk, cleanUp := initAuthorities(t) + _, _, ra, _, clk, cleanUp := initAuthorities(t) defer cleanUp() ra.OCSP = &mockOCSPA{} @@ -4278,7 +3921,7 @@ func TestRevokeCertByKey(t *testing.T) { } func TestAdministrativelyRevokeCertificate(t *testing.T) { - _, _, ra, clk, cleanUp := initAuthorities(t) + _, _, ra, _, clk, cleanUp := initAuthorities(t) defer cleanUp() ra.OCSP = &mockOCSPA{} @@ -4336,8 +3979,6 @@ func TestAdministrativelyRevokeCertificate(t *testing.T) { }) test.AssertNotError(t, err, "AdministrativelyRevokeCertificate failed") test.AssertEquals(t, len(mockSA.blocked), 0) - test.AssertMetricWithLabelsEquals( - t, ra.revocationReasonCounter, prometheus.Labels{"reason": "unspecified"}, 1) // Revoking a serial for an unspecified reason should work but not block the key. mockSA.reset() @@ -4348,8 +3989,6 @@ func TestAdministrativelyRevokeCertificate(t *testing.T) { }) test.AssertNotError(t, err, "AdministrativelyRevokeCertificate failed") test.AssertEquals(t, len(mockSA.blocked), 0) - test.AssertMetricWithLabelsEquals( - t, ra.revocationReasonCounter, prometheus.Labels{"reason": "unspecified"}, 2) // Duplicate administrative revocation of a serial for an unspecified reason // should succeed because the akamai cache purge succeeds. @@ -4361,8 +4000,6 @@ func TestAdministrativelyRevokeCertificate(t *testing.T) { }) test.AssertNotError(t, err, "AdministrativelyRevokeCertificate failed") test.AssertEquals(t, len(mockSA.blocked), 0) - test.AssertMetricWithLabelsEquals( - t, ra.revocationReasonCounter, prometheus.Labels{"reason": "unspecified"}, 2) // Duplicate administrative revocation of a serial for a *malformed* cert for // an unspecified reason should fail because we can't attempt an akamai cache @@ -4377,8 +4014,6 @@ func TestAdministrativelyRevokeCertificate(t *testing.T) { test.AssertError(t, err, "Should be revoked") test.AssertContains(t, err.Error(), "already revoked") test.AssertEquals(t, len(mockSA.blocked), 0) - test.AssertMetricWithLabelsEquals( - t, ra.revocationReasonCounter, prometheus.Labels{"reason": "unspecified"}, 2) // Revoking a cert for key compromise with skipBlockKey set should work but // not block the key. @@ -4391,8 +4026,6 @@ func TestAdministrativelyRevokeCertificate(t *testing.T) { }) test.AssertNotError(t, err, "AdministrativelyRevokeCertificate failed") test.AssertEquals(t, len(mockSA.blocked), 0) - test.AssertMetricWithLabelsEquals( - t, ra.revocationReasonCounter, prometheus.Labels{"reason": "keyCompromise"}, 1) // Revoking a cert for key compromise should work and block the key. mockSA.reset() @@ -4407,8 +4040,6 @@ func TestAdministrativelyRevokeCertificate(t *testing.T) { test.AssertEquals(t, mockSA.blocked[0].Source, "admin-revoker") test.AssertEquals(t, mockSA.blocked[0].Comment, "revoked by root") test.AssertEquals(t, mockSA.blocked[0].Added.AsTime(), clk.Now()) - test.AssertMetricWithLabelsEquals( - t, ra.revocationReasonCounter, prometheus.Labels{"reason": "keyCompromise"}, 2) // Revoking a malformed cert for key compromise should fail because we don't // have the pubkey to block. @@ -4422,85 +4053,6 @@ func TestAdministrativelyRevokeCertificate(t *testing.T) { test.AssertError(t, err, "AdministrativelyRevokeCertificate should have failed with just serial for keyCompromise") } -func TestNewOrderRateLimitingExempt(t *testing.T) { - _, _, ra, _, cleanUp := initAuthorities(t) - defer cleanUp() - - ra.orderLifetime = 5 * 24 * time.Hour - - // Set up a rate limit policy that allows 1 order every 5 minutes. - rateLimitDuration := 5 * time.Minute - ra.rlPolicies = &dummyRateLimitConfig{ - NewOrdersPerAccountPolicy: ratelimit.RateLimitPolicy{ - Threshold: 1, - Window: config.Duration{Duration: rateLimitDuration}, - }, - } - - exampleOrderOne := &rapb.NewOrderRequest{ - RegistrationID: Registration.Id, - Names: []string{"first.example.com", "second.example.com"}, - } - exampleOrderTwo := &rapb.NewOrderRequest{ - RegistrationID: Registration.Id, - Names: []string{"first.example.com", "third.example.com"}, - } - - // Create an order immediately. - _, err := ra.NewOrder(ctx, exampleOrderOne) - test.AssertNotError(t, err, "orderOne should have succeeded") - - // Create another order immediately. This should fail. - _, err = ra.NewOrder(ctx, exampleOrderTwo) - test.AssertError(t, err, "orderTwo should have failed") - - // Exempt orderTwo from rate limiting. - exampleOrderTwo.LimitsExempt = true - _, err = ra.NewOrder(ctx, exampleOrderTwo) - test.AssertNotError(t, err, "orderTwo should have succeeded") -} - -func TestNewOrderFailedAuthzRateLimitingExempt(t *testing.T) { - _, _, ra, _, cleanUp := initAuthorities(t) - defer cleanUp() - - exampleOrder := &rapb.NewOrderRequest{ - RegistrationID: Registration.Id, - Names: []string{"example.com"}, - } - - // Create an order, and thus a pending authz, for "example.com". - ctx := context.Background() - order, err := ra.NewOrder(ctx, exampleOrder) - test.AssertNotError(t, err, "adding an initial order for regA") - test.AssertNotNil(t, order.Id, "initial order had a nil ID") - test.AssertEquals(t, numAuthorizations(order), 1) - - // Mock SA that has a failed authorization for "example.com". - ra.SA = &mockInvalidPlusValidAuthzAuthority{ - mockSAWithAuthzs{authzs: map[string]*core.Authorization{}}, - "example.com", - } - - // Set up a rate limit policy that allows 1 order every 24 hours. - ra.rlPolicies = &dummyRateLimitConfig{ - InvalidAuthorizationsPerAccountPolicy: ratelimit.RateLimitPolicy{ - Threshold: 1, - Window: config.Duration{Duration: 24 * time.Hour}, - }, - } - - // Requesting a new order for "example.com" should fail due to too many - // failed authorizations. - _, err = ra.NewOrder(ctx, exampleOrder) - test.AssertError(t, err, "expected error for domain with too many failures") - - // Exempt the order from rate limiting. - exampleOrder.LimitsExempt = true - _, err = ra.NewOrder(ctx, exampleOrder) - test.AssertNotError(t, err, "limit exempt order should have succeeded") -} - // An authority that returns an error from NewOrderAndAuthzs if the // "ReplacesSerial" field of the request is empty. type mockNewOrderMustBeReplacementAuthority struct { @@ -4517,17 +4069,17 @@ func (sa *mockNewOrderMustBeReplacementAuthority) NewOrderAndAuthzs(ctx context. Expires: req.NewOrder.Expires, Status: string(core.StatusPending), Created: timestamppb.New(time.Now()), - Names: req.NewOrder.Names, + Identifiers: req.NewOrder.Identifiers, }, nil } func TestNewOrderReplacesSerialCarriesThroughToSA(t *testing.T) { - _, _, ra, _, cleanUp := initAuthorities(t) + _, _, ra, _, _, cleanUp := initAuthorities(t) defer cleanUp() exampleOrder := &rapb.NewOrderRequest{ RegistrationID: Registration.Id, - Names: []string{"example.com"}, + Identifiers: []*corepb.Identifier{identifier.NewDNS("example.com").ToProto()}, ReplacesSerial: "1234", } @@ -4538,3 +4090,337 @@ func TestNewOrderReplacesSerialCarriesThroughToSA(t *testing.T) { _, err := ra.NewOrder(ctx, exampleOrder) test.AssertNotError(t, err, "order with ReplacesSerial should have succeeded") } + +// newMockSAUnpauseAccount is a fake which includes all of the SA methods called +// in the course of an account unpause. Its behavior can be customized by +// providing the number of unpaused account identifiers to allow testing of +// various scenarios. +type mockSAUnpauseAccount struct { + sapb.StorageAuthorityClient + identsToUnpause int64 + receivedRegID int64 +} + +func (sa *mockSAUnpauseAccount) UnpauseAccount(_ context.Context, req *sapb.RegistrationID, _ ...grpc.CallOption) (*sapb.Count, error) { + sa.receivedRegID = req.Id + return &sapb.Count{Count: sa.identsToUnpause}, nil +} + +// TestUnpauseAccount tests that the RA's UnpauseAccount method correctly passes +// the requested RegID to the SA, and correctly passes the SA's count back to +// the caller. +func TestUnpauseAccount(t *testing.T) { + _, _, ra, _, _, cleanUp := initAuthorities(t) + defer cleanUp() + + mockSA := mockSAUnpauseAccount{identsToUnpause: 0} + ra.SA = &mockSA + + res, err := ra.UnpauseAccount(context.Background(), &rapb.UnpauseAccountRequest{ + RegistrationID: 1, + }) + test.AssertNotError(t, err, "Should have been able to unpause account") + test.AssertEquals(t, res.Count, int64(0)) + test.AssertEquals(t, mockSA.receivedRegID, int64(1)) + + mockSA.identsToUnpause = 50001 + res, err = ra.UnpauseAccount(context.Background(), &rapb.UnpauseAccountRequest{ + RegistrationID: 1, + }) + test.AssertNotError(t, err, "Should have been able to unpause account") + test.AssertEquals(t, res.Count, int64(50001)) +} + +func TestGetAuthorization(t *testing.T) { + _, _, ra, _, _, cleanup := initAuthorities(t) + defer cleanup() + + ra.SA = &mockSAWithAuthzs{ + authzs: []*core.Authorization{ + { + ID: "1", + Identifier: identifier.NewDNS("example.com"), + Status: "valid", + Challenges: []core.Challenge{ + { + Type: core.ChallengeTypeHTTP01, + Status: core.StatusValid, + }, + }, + }, + }, + } + + // With HTTP01 enabled, GetAuthorization should pass the mock challenge through. + pa, err := policy.New( + map[identifier.IdentifierType]bool{ + identifier.TypeDNS: true, + identifier.TypeIP: true, + }, + map[core.AcmeChallenge]bool{ + core.ChallengeTypeHTTP01: true, + core.ChallengeTypeDNS01: true, + }, + blog.NewMock()) + test.AssertNotError(t, err, "Couldn't create PA") + ra.PA = pa + authz, err := ra.GetAuthorization(context.Background(), &rapb.GetAuthorizationRequest{Id: 1}) + test.AssertNotError(t, err, "should not fail") + test.AssertEquals(t, len(authz.Challenges), 1) + test.AssertEquals(t, authz.Challenges[0].Type, string(core.ChallengeTypeHTTP01)) + + // With HTTP01 disabled, GetAuthorization should filter out the mock challenge. + pa, err = policy.New( + map[identifier.IdentifierType]bool{ + identifier.TypeDNS: true, + identifier.TypeIP: true, + }, + map[core.AcmeChallenge]bool{ + core.ChallengeTypeDNS01: true, + }, + blog.NewMock()) + test.AssertNotError(t, err, "Couldn't create PA") + ra.PA = pa + authz, err = ra.GetAuthorization(context.Background(), &rapb.GetAuthorizationRequest{Id: 1}) + test.AssertNotError(t, err, "should not fail") + test.AssertEquals(t, len(authz.Challenges), 0) +} + +type NoUpdateSA struct { + sapb.StorageAuthorityClient +} + +func (sa *NoUpdateSA) UpdateRegistrationContact(_ context.Context, _ *sapb.UpdateRegistrationContactRequest, _ ...grpc.CallOption) (*corepb.Registration, error) { + return nil, fmt.Errorf("UpdateRegistrationContact() is mocked to always error") +} + +func (sa *NoUpdateSA) UpdateRegistrationKey(_ context.Context, _ *sapb.UpdateRegistrationKeyRequest, _ ...grpc.CallOption) (*corepb.Registration, error) { + return nil, fmt.Errorf("UpdateRegistrationKey() is mocked to always error") +} + +// mockSARecordingRegistration tests UpdateRegistrationContact and UpdateRegistrationKey. +type mockSARecordingRegistration struct { + sapb.StorageAuthorityClient + providedRegistrationID int64 + providedContacts []string + providedJwk []byte +} + +// UpdateRegistrationContact records the registration ID and updated contacts +// (optional) provided. +func (sa *mockSARecordingRegistration) UpdateRegistrationContact(ctx context.Context, req *sapb.UpdateRegistrationContactRequest, _ ...grpc.CallOption) (*corepb.Registration, error) { + sa.providedRegistrationID = req.RegistrationID + sa.providedContacts = req.Contacts + + return &corepb.Registration{ + Id: req.RegistrationID, + Contact: req.Contacts, + }, nil +} + +// UpdateRegistrationKey records the registration ID and updated key provided. +func (sa *mockSARecordingRegistration) UpdateRegistrationKey(ctx context.Context, req *sapb.UpdateRegistrationKeyRequest, _ ...grpc.CallOption) (*corepb.Registration, error) { + sa.providedRegistrationID = req.RegistrationID + sa.providedJwk = req.Jwk + + return &corepb.Registration{ + Id: req.RegistrationID, + Key: req.Jwk, + }, nil +} + +// TestUpdateRegistrationContact tests that the RA's UpdateRegistrationContact +// method correctly: requires a registration ID; validates the contact provided; +// does not require a contact; passes the requested registration ID and contact +// to the SA; passes the updated Registration back to the caller; and can return +// an error. +func TestUpdateRegistrationContact(t *testing.T) { + _, _, ra, _, _, cleanUp := initAuthorities(t) + defer cleanUp() + + expectRegID := int64(1) + expectContacts := []string{"mailto:test@contoso.com"} + mockSA := mockSARecordingRegistration{} + ra.SA = &mockSA + + _, err := ra.UpdateRegistrationContact(context.Background(), &rapb.UpdateRegistrationContactRequest{}) + test.AssertError(t, err, "should not have been able to update registration contact without a registration ID") + test.AssertContains(t, err.Error(), "incomplete gRPC request message") + + _, err = ra.UpdateRegistrationContact(context.Background(), &rapb.UpdateRegistrationContactRequest{ + RegistrationID: expectRegID, + Contacts: []string{"tel:+44123"}, + }) + test.AssertError(t, err, "should not have been able to update registration contact to an invalid contact") + test.AssertContains(t, err.Error(), "invalid contact") + + res, err := ra.UpdateRegistrationContact(context.Background(), &rapb.UpdateRegistrationContactRequest{ + RegistrationID: expectRegID, + }) + test.AssertNotError(t, err, "should have been able to update registration with a blank contact") + test.AssertEquals(t, res.Id, expectRegID) + test.AssertEquals(t, mockSA.providedRegistrationID, expectRegID) + test.AssertDeepEquals(t, res.Contact, []string(nil)) + test.AssertDeepEquals(t, mockSA.providedContacts, []string(nil)) + + res, err = ra.UpdateRegistrationContact(context.Background(), &rapb.UpdateRegistrationContactRequest{ + RegistrationID: expectRegID, + Contacts: expectContacts, + }) + test.AssertNotError(t, err, "should have been able to update registration with a populated contact") + test.AssertEquals(t, res.Id, expectRegID) + test.AssertEquals(t, mockSA.providedRegistrationID, expectRegID) + test.AssertDeepEquals(t, res.Contact, expectContacts) + test.AssertDeepEquals(t, mockSA.providedContacts, expectContacts) + + // Switch to a mock SA that will always error if UpdateRegistrationContact() + // is called. + ra.SA = &NoUpdateSA{} + _, err = ra.UpdateRegistrationContact(context.Background(), &rapb.UpdateRegistrationContactRequest{ + RegistrationID: expectRegID, + Contacts: expectContacts, + }) + test.AssertError(t, err, "should have received an error from the SA") + test.AssertContains(t, err.Error(), "failed to update registration contact") + test.AssertContains(t, err.Error(), "mocked to always error") +} + +// TestUpdateRegistrationKey tests that the RA's UpdateRegistrationKey method +// correctly requires a registration ID and key, passes them to the SA, and +// passes the updated Registration back to the caller. +func TestUpdateRegistrationKey(t *testing.T) { + _, _, ra, _, _, cleanUp := initAuthorities(t) + defer cleanUp() + + expectRegID := int64(1) + expectJwk := AccountKeyJSONA + mockSA := mockSARecordingRegistration{} + ra.SA = &mockSA + + _, err := ra.UpdateRegistrationKey(context.Background(), &rapb.UpdateRegistrationKeyRequest{}) + test.AssertError(t, err, "should not have been able to update registration key without a registration ID or key") + test.AssertContains(t, err.Error(), "incomplete gRPC request message") + + _, err = ra.UpdateRegistrationKey(context.Background(), &rapb.UpdateRegistrationKeyRequest{RegistrationID: expectRegID}) + test.AssertError(t, err, "should not have been able to update registration key without a key") + test.AssertContains(t, err.Error(), "incomplete gRPC request message") + + _, err = ra.UpdateRegistrationKey(context.Background(), &rapb.UpdateRegistrationKeyRequest{Jwk: expectJwk}) + test.AssertError(t, err, "should not have been able to update registration key without a registration ID") + test.AssertContains(t, err.Error(), "incomplete gRPC request message") + + res, err := ra.UpdateRegistrationKey(context.Background(), &rapb.UpdateRegistrationKeyRequest{ + RegistrationID: expectRegID, + Jwk: expectJwk, + }) + test.AssertNotError(t, err, "should have been able to update registration key") + test.AssertEquals(t, res.Id, expectRegID) + test.AssertEquals(t, mockSA.providedRegistrationID, expectRegID) + test.AssertDeepEquals(t, res.Key, expectJwk) + test.AssertDeepEquals(t, mockSA.providedJwk, expectJwk) + + // Switch to a mock SA that will always error if UpdateRegistrationKey() is + // called. + ra.SA = &NoUpdateSA{} + _, err = ra.UpdateRegistrationKey(context.Background(), &rapb.UpdateRegistrationKeyRequest{ + RegistrationID: expectRegID, + Jwk: expectJwk, + }) + test.AssertError(t, err, "should have received an error from the SA") + test.AssertContains(t, err.Error(), "failed to update registration key") + test.AssertContains(t, err.Error(), "mocked to always error") +} + +func TestCRLShard(t *testing.T) { + var cdp []string + n, err := crlShard(&x509.Certificate{CRLDistributionPoints: cdp}) + if err != nil || n != 0 { + t.Errorf("crlShard(%+v) = %d, %s, want 0, nil", cdp, n, err) + } + + cdp = []string{ + "https://example.com/123.crl", + "https://example.net/123.crl", + } + n, err = crlShard(&x509.Certificate{CRLDistributionPoints: cdp}) + if err == nil { + t.Errorf("crlShard(%+v) = %d, %s, want 0, some error", cdp, n, err) + } + + cdp = []string{ + "https://example.com/abc", + } + n, err = crlShard(&x509.Certificate{CRLDistributionPoints: cdp}) + if err == nil { + t.Errorf("crlShard(%+v) = %d, %s, want 0, some error", cdp, n, err) + } + + cdp = []string{ + "example", + } + n, err = crlShard(&x509.Certificate{CRLDistributionPoints: cdp}) + if err == nil { + t.Errorf("crlShard(%+v) = %d, %s, want 0, some error", cdp, n, err) + } + + cdp = []string{ + "https://example.com/abc/-77.crl", + } + n, err = crlShard(&x509.Certificate{CRLDistributionPoints: cdp}) + if err == nil { + t.Errorf("crlShard(%+v) = %d, %s, want 0, some error", cdp, n, err) + } + + cdp = []string{ + "https://example.com/abc/123", + } + n, err = crlShard(&x509.Certificate{CRLDistributionPoints: cdp}) + if err != nil || n != 123 { + t.Errorf("crlShard(%+v) = %d, %s, want 123, nil", cdp, n, err) + } + + cdp = []string{ + "https://example.com/abc/123.crl", + } + n, err = crlShard(&x509.Certificate{CRLDistributionPoints: cdp}) + if err != nil || n != 123 { + t.Errorf("crlShard(%+v) = %d, %s, want 123, nil", cdp, n, err) + } +} + +type mockSAWithOverrides struct { + sapb.StorageAuthorityClient + inserted *sapb.AddRateLimitOverrideRequest +} + +func (sa *mockSAWithOverrides) AddRateLimitOverride(ctx context.Context, req *sapb.AddRateLimitOverrideRequest, _ ...grpc.CallOption) (*sapb.AddRateLimitOverrideResponse, error) { + sa.inserted = req + return &sapb.AddRateLimitOverrideResponse{}, nil +} + +func TestAddRateLimitOverride(t *testing.T) { + _, _, ra, _, _, cleanUp := initAuthorities(t) + defer cleanUp() + + mockSA := mockSAWithOverrides{} + ra.SA = &mockSA + + expectBucketKey := core.RandomString(10) + ov := rapb.AddRateLimitOverrideRequest{ + LimitEnum: 1, + BucketKey: expectBucketKey, + Comment: "insert", + Period: durationpb.New(time.Hour), + Count: 100, + Burst: 100, + } + + _, err := ra.AddRateLimitOverride(ctx, &ov) + test.AssertNotError(t, err, "expected successful insert, got error") + test.AssertEquals(t, mockSA.inserted.Override.LimitEnum, ov.LimitEnum) + test.AssertEquals(t, mockSA.inserted.Override.BucketKey, expectBucketKey) + test.AssertEquals(t, mockSA.inserted.Override.Comment, ov.Comment) + test.AssertEquals(t, mockSA.inserted.Override.Period.AsDuration(), ov.Period.AsDuration()) + test.AssertEquals(t, mockSA.inserted.Override.Count, ov.Count) + test.AssertEquals(t, mockSA.inserted.Override.Burst, ov.Burst) +} diff --git a/third-party/github.com/letsencrypt/boulder/ratelimit/rate-limits.go b/third-party/github.com/letsencrypt/boulder/ratelimit/rate-limits.go deleted file mode 100644 index 812b723b2..000000000 --- a/third-party/github.com/letsencrypt/boulder/ratelimit/rate-limits.go +++ /dev/null @@ -1,237 +0,0 @@ -package ratelimit - -import ( - "strconv" - "time" - - "github.com/letsencrypt/boulder/config" - "github.com/letsencrypt/boulder/strictyaml" -) - -const ( - // CertificatesPerName is the name of the CertificatesPerName rate limit - // when referenced in metric labels. - CertificatesPerName = "certificates_per_domain" - - // RegistrationsPerIP is the name of the RegistrationsPerIP rate limit when - // referenced in metric labels. - RegistrationsPerIP = "registrations_per_ip" - - // RegistrationsPerIPRange is the name of the RegistrationsPerIPRange rate - // limit when referenced in metric labels. - RegistrationsPerIPRange = "registrations_per_ipv6_range" - - // PendingAuthorizationsPerAccount is the name of the - // PendingAuthorizationsPerAccount rate limit when referenced in metric - // labels. - PendingAuthorizationsPerAccount = "pending_authorizations_per_account" - - // InvalidAuthorizationsPerAccount is the name of the - // InvalidAuthorizationsPerAccount rate limit when referenced in metric - // labels. - InvalidAuthorizationsPerAccount = "failed_authorizations_per_account" - - // CertificatesPerFQDNSet is the name of the CertificatesPerFQDNSet rate - // limit when referenced in metric labels. - CertificatesPerFQDNSet = "certificates_per_fqdn_set" - - // CertificatesPerFQDNSetFast is the name of the CertificatesPerFQDNSetFast - // rate limit when referenced in metric labels. - CertificatesPerFQDNSetFast = "certificates_per_fqdn_set_fast" - - // NewOrdersPerAccount is the name of the NewOrdersPerAccount rate limit - // when referenced in metric labels. - NewOrdersPerAccount = "new_orders_per_account" -) - -// Limits is defined to allow mock implementations be provided during unit -// testing -type Limits interface { - CertificatesPerName() RateLimitPolicy - RegistrationsPerIP() RateLimitPolicy - RegistrationsPerIPRange() RateLimitPolicy - PendingAuthorizationsPerAccount() RateLimitPolicy - InvalidAuthorizationsPerAccount() RateLimitPolicy - CertificatesPerFQDNSet() RateLimitPolicy - CertificatesPerFQDNSetFast() RateLimitPolicy - NewOrdersPerAccount() RateLimitPolicy - LoadPolicies(contents []byte) error -} - -// limitsImpl is an unexported implementation of the Limits interface. It acts -// as a container for a rateLimitConfig. -type limitsImpl struct { - rlPolicy *rateLimitConfig -} - -func (r *limitsImpl) CertificatesPerName() RateLimitPolicy { - if r.rlPolicy == nil { - return RateLimitPolicy{} - } - return r.rlPolicy.CertificatesPerName -} - -func (r *limitsImpl) RegistrationsPerIP() RateLimitPolicy { - if r.rlPolicy == nil { - return RateLimitPolicy{} - } - return r.rlPolicy.RegistrationsPerIP -} - -func (r *limitsImpl) RegistrationsPerIPRange() RateLimitPolicy { - if r.rlPolicy == nil { - return RateLimitPolicy{} - } - return r.rlPolicy.RegistrationsPerIPRange -} - -func (r *limitsImpl) PendingAuthorizationsPerAccount() RateLimitPolicy { - if r.rlPolicy == nil { - return RateLimitPolicy{} - } - return r.rlPolicy.PendingAuthorizationsPerAccount -} - -func (r *limitsImpl) InvalidAuthorizationsPerAccount() RateLimitPolicy { - if r.rlPolicy == nil { - return RateLimitPolicy{} - } - return r.rlPolicy.InvalidAuthorizationsPerAccount -} - -func (r *limitsImpl) CertificatesPerFQDNSet() RateLimitPolicy { - if r.rlPolicy == nil { - return RateLimitPolicy{} - } - return r.rlPolicy.CertificatesPerFQDNSet -} - -func (r *limitsImpl) CertificatesPerFQDNSetFast() RateLimitPolicy { - if r.rlPolicy == nil { - return RateLimitPolicy{} - } - return r.rlPolicy.CertificatesPerFQDNSetFast -} - -func (r *limitsImpl) NewOrdersPerAccount() RateLimitPolicy { - if r.rlPolicy == nil { - return RateLimitPolicy{} - } - return r.rlPolicy.NewOrdersPerAccount -} - -// LoadPolicies loads various rate limiting policies from a byte array of -// YAML configuration. -func (r *limitsImpl) LoadPolicies(contents []byte) error { - var newPolicy rateLimitConfig - err := strictyaml.Unmarshal(contents, &newPolicy) - if err != nil { - return err - } - r.rlPolicy = &newPolicy - return nil -} - -func New() Limits { - return &limitsImpl{} -} - -// rateLimitConfig contains all application layer rate limiting policies. It is -// unexported and clients are expected to use the exported container struct -type rateLimitConfig struct { - // Number of certificates that can be extant containing any given name. - // These are counted by "base domain" aka eTLD+1, so any entries in the - // overrides section must be an eTLD+1 according to the publicsuffix package. - CertificatesPerName RateLimitPolicy `yaml:"certificatesPerName"` - // Number of registrations that can be created per IP. - // Note: Since this is checked before a registration is created, setting a - // RegistrationOverride on it has no effect. - RegistrationsPerIP RateLimitPolicy `yaml:"registrationsPerIP"` - // Number of registrations that can be created per fuzzy IP range. Unlike - // RegistrationsPerIP this will apply to a /48 for IPv6 addresses to help curb - // abuse from easily obtained IPv6 ranges. - // Note: Like RegistrationsPerIP, setting a RegistrationOverride has no - // effect here. - RegistrationsPerIPRange RateLimitPolicy `yaml:"registrationsPerIPRange"` - // Number of pending authorizations that can exist per account. Overrides by - // key are not applied, but overrides by registration are. - PendingAuthorizationsPerAccount RateLimitPolicy `yaml:"pendingAuthorizationsPerAccount"` - // Number of invalid authorizations that can be failed per account within the - // given window. Overrides by key are not applied, but overrides by registration are. - // Note that this limit is actually "per account, per hostname," but that - // is too long for the variable name. - InvalidAuthorizationsPerAccount RateLimitPolicy `yaml:"invalidAuthorizationsPerAccount"` - // Number of new orders that can be created per account within the given - // window. Overrides by key are not applied, but overrides by registration are. - NewOrdersPerAccount RateLimitPolicy `yaml:"newOrdersPerAccount"` - // Number of certificates that can be extant containing a specific set - // of DNS names. - CertificatesPerFQDNSet RateLimitPolicy `yaml:"certificatesPerFQDNSet"` - // Same as above, but intended to both trigger and reset faster (i.e. a - // lower threshold and smaller window), so that clients don't have to wait - // a long time after a small burst of accidental duplicate issuance. - CertificatesPerFQDNSetFast RateLimitPolicy `yaml:"certificatesPerFQDNSetFast"` -} - -// RateLimitPolicy describes a general limiting policy -type RateLimitPolicy struct { - // How long to count items for - Window config.Duration `yaml:"window"` - // The max number of items that can be present before triggering the rate - // limit. Zero means "no limit." - Threshold int64 `yaml:"threshold"` - // A per-key override setting different limits than the default (higher or lower). - // The key is defined on a per-limit basis and should match the key it counts on. - // For instance, a rate limit on the number of certificates per name uses name as - // a key, while a rate limit on the number of registrations per IP subnet would - // use subnet as a key. Note that a zero entry in the overrides map does not - // mean "no limit," it means a limit of zero. An entry of -1 means - // "no limit", only for the pending authorizations rate limit. - Overrides map[string]int64 `yaml:"overrides"` - // A per-registration override setting. This can be used, e.g. if there are - // hosting providers that we would like to grant a higher rate of issuance - // than the default. If both key-based and registration-based overrides are - // available, whichever is larger takes priority. Note that a zero entry in - // the overrides map does not mean "no limit", it means a limit of zero. - RegistrationOverrides map[int64]int64 `yaml:"registrationOverrides"` -} - -// Enabled returns true iff the RateLimitPolicy is enabled. -func (rlp *RateLimitPolicy) Enabled() bool { - return rlp.Threshold != 0 -} - -// GetThreshold returns the threshold for this rate limit and the override -// Id/Key if that threshold is the result of an override for the default limit, -// empty-string otherwise. The threshold returned takes into account any -// overrides for `key` or `regID`. If both `key` and `regID` have an override -// the largest of the two will be used. -func (rlp *RateLimitPolicy) GetThreshold(key string, regID int64) (int64, string) { - regOverride, regOverrideExists := rlp.RegistrationOverrides[regID] - keyOverride, keyOverrideExists := rlp.Overrides[key] - - if regOverrideExists && !keyOverrideExists { - // If there is a regOverride and no keyOverride use the regOverride - return regOverride, strconv.FormatInt(regID, 10) - } else if !regOverrideExists && keyOverrideExists { - // If there is a keyOverride and no regOverride use the keyOverride - return keyOverride, key - } else if regOverrideExists && keyOverrideExists { - // If there is both a regOverride and a keyOverride use whichever is larger. - if regOverride > keyOverride { - return regOverride, strconv.FormatInt(regID, 10) - } else { - return keyOverride, key - } - } - - // Otherwise there was no regOverride and no keyOverride, use the base - // Threshold - return rlp.Threshold, "" -} - -// WindowBegin returns the time that a RateLimitPolicy's window begins, given a -// particular end time (typically the current time). -func (rlp *RateLimitPolicy) WindowBegin(windowEnd time.Time) time.Time { - return windowEnd.Add(-1 * rlp.Window.Duration) -} diff --git a/third-party/github.com/letsencrypt/boulder/ratelimit/rate-limits_test.go b/third-party/github.com/letsencrypt/boulder/ratelimit/rate-limits_test.go deleted file mode 100644 index d264e1428..000000000 --- a/third-party/github.com/letsencrypt/boulder/ratelimit/rate-limits_test.go +++ /dev/null @@ -1,187 +0,0 @@ -package ratelimit - -import ( - "os" - "testing" - "time" - - "github.com/letsencrypt/boulder/config" - "github.com/letsencrypt/boulder/test" -) - -func TestEnabled(t *testing.T) { - policy := RateLimitPolicy{ - Threshold: 10, - } - if !policy.Enabled() { - t.Errorf("Policy should have been enabled.") - } -} - -func TestNotEnabled(t *testing.T) { - policy := RateLimitPolicy{ - Threshold: 0, - } - if policy.Enabled() { - t.Errorf("Policy should not have been enabled.") - } -} - -func TestGetThreshold(t *testing.T) { - policy := RateLimitPolicy{ - Threshold: 1, - Overrides: map[string]int64{ - "key": 2, - "baz": 99, - }, - RegistrationOverrides: map[int64]int64{ - 101: 3, - }, - } - - testCases := []struct { - Name string - Key string - RegID int64 - Expected int64 - }{ - - { - Name: "No key or reg overrides", - Key: "foo", - RegID: 11, - Expected: 1, - }, - { - Name: "Key override, no reg override", - Key: "key", - RegID: 11, - Expected: 2, - }, - { - Name: "No key override, reg override", - Key: "foo", - RegID: 101, - Expected: 3, - }, - { - Name: "Key override, larger reg override", - Key: "foo", - RegID: 101, - Expected: 3, - }, - { - Name: "Key override, smaller reg override", - Key: "baz", - RegID: 101, - Expected: 99, - }, - } - - for _, tc := range testCases { - t.Run(tc.Name, func(t *testing.T) { - threshold, _ := policy.GetThreshold(tc.Key, tc.RegID) - test.AssertEquals(t, - threshold, - tc.Expected) - }) - } -} - -func TestWindowBegin(t *testing.T) { - policy := RateLimitPolicy{ - Window: config.Duration{Duration: 24 * time.Hour}, - } - now := time.Date(2015, 9, 22, 0, 0, 0, 0, time.UTC) - expected := time.Date(2015, 9, 21, 0, 0, 0, 0, time.UTC) - actual := policy.WindowBegin(now) - if actual != expected { - t.Errorf("Incorrect WindowBegin: %s, expected %s", actual, expected) - } -} - -func TestLoadPolicies(t *testing.T) { - policy := New() - - policyContent, readErr := os.ReadFile("../test/rate-limit-policies.yml") - test.AssertNotError(t, readErr, "Failed to load rate-limit-policies.yml") - - // Test that loading a good policy from YAML doesn't error - err := policy.LoadPolicies(policyContent) - test.AssertNotError(t, err, "Failed to parse rate-limit-policies.yml") - - // Test that the CertificatesPerName section parsed correctly - certsPerName := policy.CertificatesPerName() - test.AssertEquals(t, certsPerName.Threshold, int64(2)) - test.AssertDeepEquals(t, certsPerName.Overrides, map[string]int64{ - "ratelimit.me": 1, - "lim.it": 0, - "le.wtf": 10000, - "le1.wtf": 10000, - "le2.wtf": 10000, - "le3.wtf": 10000, - "nginx.wtf": 10000, - "good-caa-reserved.com": 10000, - "bad-caa-reserved.com": 10000, - "ecdsa.le.wtf": 10000, - "must-staple.le.wtf": 10000, - }) - test.AssertDeepEquals(t, certsPerName.RegistrationOverrides, map[int64]int64{ - 101: 1000, - }) - - // Test that the RegistrationsPerIP section parsed correctly - regsPerIP := policy.RegistrationsPerIP() - test.AssertEquals(t, regsPerIP.Threshold, int64(10000)) - test.AssertDeepEquals(t, regsPerIP.Overrides, map[string]int64{ - "127.0.0.1": 1000000, - }) - test.AssertEquals(t, len(regsPerIP.RegistrationOverrides), 0) - - // Test that the PendingAuthorizationsPerAccount section parsed correctly - pendingAuthsPerAcct := policy.PendingAuthorizationsPerAccount() - test.AssertEquals(t, pendingAuthsPerAcct.Threshold, int64(150)) - test.AssertEquals(t, len(pendingAuthsPerAcct.Overrides), 0) - test.AssertEquals(t, len(pendingAuthsPerAcct.RegistrationOverrides), 0) - - // Test that the CertificatesPerFQDN section parsed correctly - certsPerFQDN := policy.CertificatesPerFQDNSet() - test.AssertEquals(t, certsPerFQDN.Threshold, int64(6)) - test.AssertDeepEquals(t, certsPerFQDN.Overrides, map[string]int64{ - "le.wtf": 10000, - "le1.wtf": 10000, - "le2.wtf": 10000, - "le3.wtf": 10000, - "le.wtf,le1.wtf": 10000, - "good-caa-reserved.com": 10000, - "nginx.wtf": 10000, - "ecdsa.le.wtf": 10000, - "must-staple.le.wtf": 10000, - }) - test.AssertEquals(t, len(certsPerFQDN.RegistrationOverrides), 0) - certsPerFQDNFast := policy.CertificatesPerFQDNSetFast() - test.AssertEquals(t, certsPerFQDNFast.Threshold, int64(2)) - test.AssertDeepEquals(t, certsPerFQDNFast.Overrides, map[string]int64{ - "le.wtf": 100, - }) - test.AssertEquals(t, len(certsPerFQDNFast.RegistrationOverrides), 0) - - // Test that loading invalid YAML generates an error - err = policy.LoadPolicies([]byte("err")) - test.AssertError(t, err, "Failed to generate error loading invalid yaml policy file") - // Re-check a field of policy to make sure a LoadPolicies error doesn't - // corrupt the existing policies - test.AssertDeepEquals(t, policy.RegistrationsPerIP().Overrides, map[string]int64{ - "127.0.0.1": 1000000, - }) - - // Test that the RateLimitConfig accessors do not panic when there has been no - // `LoadPolicy` call, and instead return empty RateLimitPolicy objects with default - // values. - emptyPolicy := New() - test.AssertEquals(t, emptyPolicy.CertificatesPerName().Threshold, int64(0)) - test.AssertEquals(t, emptyPolicy.RegistrationsPerIP().Threshold, int64(0)) - test.AssertEquals(t, emptyPolicy.RegistrationsPerIP().Threshold, int64(0)) - test.AssertEquals(t, emptyPolicy.PendingAuthorizationsPerAccount().Threshold, int64(0)) - test.AssertEquals(t, emptyPolicy.CertificatesPerFQDNSet().Threshold, int64(0)) -} diff --git a/third-party/github.com/letsencrypt/boulder/ratelimits/README.md b/third-party/github.com/letsencrypt/boulder/ratelimits/README.md index adf8afc06..a16427d0a 100644 --- a/third-party/github.com/letsencrypt/boulder/ratelimits/README.md +++ b/third-party/github.com/letsencrypt/boulder/ratelimits/README.md @@ -91,17 +91,31 @@ An ACME account registration ID. Example: `12345678` -#### domain +#### identValue -A valid eTLD+1 domain name. +A valid ACME identifier value, i.e. an FQDN or IP address. -Example: `example.com` +Examples: + - `www.example.com` + - `192.168.1.1` + - `2001:db8:eeee::1` + +#### domainOrCIDR + +A valid eTLD+1 domain name, or an IP address. IPv6 addresses must be the lowest +address in their /64, i.e. their last 64 bits must be zero; the override will +apply to the entire /64. Do not include the CIDR mask. + +Examples: + - `example.com` + - `192.168.1.0` + - `2001:db8:eeee:eeee::` #### fqdnSet -A comma-separated list of domain names. +A comma-separated list of identifier values. -Example: `example.com,example.org` +Example: `192.168.1.1,example.com,example.org` ## Bucket Key Definitions diff --git a/third-party/github.com/letsencrypt/boulder/ratelimits/bucket.go b/third-party/github.com/letsencrypt/boulder/ratelimits/bucket.go deleted file mode 100644 index ba555c2db..000000000 --- a/third-party/github.com/letsencrypt/boulder/ratelimits/bucket.go +++ /dev/null @@ -1,414 +0,0 @@ -package ratelimits - -import ( - "errors" - "fmt" - "net" - "strconv" - "strings" - - "github.com/letsencrypt/boulder/core" -) - -// ErrInvalidCost indicates that the cost specified was < 0. -var ErrInvalidCost = fmt.Errorf("invalid cost, must be >= 0") - -// ErrInvalidCostOverLimit indicates that the cost specified was > limit.Burst. -var ErrInvalidCostOverLimit = fmt.Errorf("invalid cost, must be <= limit.Burst") - -// newIPAddressBucketKey validates and returns a bucketKey for limits that use -// the 'enum:ipAddress' bucket key format. -func newIPAddressBucketKey(name Name, ip net.IP) (string, error) { //nolint: unparam - id := ip.String() - err := validateIdForName(name, id) - if err != nil { - return "", err - } - return joinWithColon(name.EnumString(), id), nil -} - -// newIPv6RangeCIDRBucketKey validates and returns a bucketKey for limits that -// use the 'enum:ipv6RangeCIDR' bucket key format. -func newIPv6RangeCIDRBucketKey(name Name, ip net.IP) (string, error) { - if ip.To4() != nil { - return "", fmt.Errorf("invalid IPv6 address, %q must be an IPv6 address", ip.String()) - } - ipMask := net.CIDRMask(48, 128) - ipNet := &net.IPNet{IP: ip.Mask(ipMask), Mask: ipMask} - id := ipNet.String() - err := validateIdForName(name, id) - if err != nil { - return "", err - } - return joinWithColon(name.EnumString(), id), nil -} - -// newRegIdBucketKey validates and returns a bucketKey for limits that use the -// 'enum:regId' bucket key format. -func newRegIdBucketKey(name Name, regId int64) (string, error) { - id := strconv.FormatInt(regId, 10) - err := validateIdForName(name, id) - if err != nil { - return "", err - } - return joinWithColon(name.EnumString(), id), nil -} - -// newDomainBucketKey validates and returns a bucketKey for limits that use the -// 'enum:domain' bucket key format. -func newDomainBucketKey(name Name, orderName string) (string, error) { - err := validateIdForName(name, orderName) - if err != nil { - return "", err - } - return joinWithColon(name.EnumString(), orderName), nil -} - -// newRegIdDomainBucketKey validates and returns a bucketKey for limits that use -// the 'enum:regId:domain' bucket key format. -func newRegIdDomainBucketKey(name Name, regId int64, orderName string) (string, error) { - regIdStr := strconv.FormatInt(regId, 10) - err := validateIdForName(name, joinWithColon(regIdStr, orderName)) - if err != nil { - return "", err - } - return joinWithColon(name.EnumString(), regIdStr, orderName), nil -} - -// newFQDNSetBucketKey validates and returns a bucketKey for limits that use the -// 'enum:fqdnSet' bucket key format. -func newFQDNSetBucketKey(name Name, orderNames []string) (string, error) { //nolint: unparam - err := validateIdForName(name, strings.Join(orderNames, ",")) - if err != nil { - return "", err - } - id := fmt.Sprintf("%x", core.HashNames(orderNames)) - return joinWithColon(name.EnumString(), id), nil -} - -// Transaction represents a single rate limit operation. It includes a -// bucketKey, which combines the specific rate limit enum with a unique -// identifier to form the key where the state of the "bucket" can be referenced -// or stored by the Limiter, the rate limit being enforced, a cost which MUST be -// >= 0, and check/spend fields, which indicate how the Transaction should be -// processed. The following are acceptable combinations of check/spend: -// - check-and-spend: when check and spend are both true, the cost will be -// checked against the bucket's capacity and spent/refunded, when possible. -// - check-only: when only check is true, the cost will be checked against the -// bucket's capacity, but will never be spent/refunded. -// - spend-only: when only spend is true, spending is best-effort. Regardless -// of the bucket's capacity, the transaction will be considered "allowed". -// - allow-only: when neither check nor spend are true, the transaction will -// be considered "allowed" regardless of the bucket's capacity. This is -// useful for limits that are disabled. -type Transaction struct { - bucketKey string - limit limit - cost int64 - check bool - spend bool -} - -func (txn Transaction) checkOnly() bool { - return txn.check && !txn.spend -} - -func (txn Transaction) spendOnly() bool { - return txn.spend && !txn.check -} - -func (txn Transaction) allowOnly() bool { - return !txn.check && !txn.spend -} - -func validateTransaction(txn Transaction) (Transaction, error) { - if txn.cost < 0 { - return Transaction{}, ErrInvalidCost - } - if txn.cost > txn.limit.Burst { - return Transaction{}, ErrInvalidCostOverLimit - } - return txn, nil -} - -func newTransaction(limit limit, bucketKey string, cost int64) (Transaction, error) { - return validateTransaction(Transaction{ - bucketKey: bucketKey, - limit: limit, - cost: cost, - check: true, - spend: true, - }) -} - -func newCheckOnlyTransaction(limit limit, bucketKey string, cost int64) (Transaction, error) { - return validateTransaction(Transaction{ - bucketKey: bucketKey, - limit: limit, - cost: cost, - check: true, - }) -} - -func newSpendOnlyTransaction(limit limit, bucketKey string, cost int64) (Transaction, error) { - return validateTransaction(Transaction{ - bucketKey: bucketKey, - limit: limit, - cost: cost, - spend: true, - }) -} - -func newAllowOnlyTransaction() (Transaction, error) { - // Zero values are sufficient. - return validateTransaction(Transaction{}) -} - -// TransactionBuilder is used to build Transactions for various rate limits. -// Each rate limit has a corresponding method that returns a Transaction for -// that limit. Call NewTransactionBuilder to create a new *TransactionBuilder. -type TransactionBuilder struct { - *limitRegistry -} - -// NewTransactionBuilder returns a new *TransactionBuilder. The provided -// defaults and overrides paths are expected to be paths to YAML files that -// contain the default and override limits, respectively. Overrides is optional, -// defaults is required. -func NewTransactionBuilder(defaults, overrides string) (*TransactionBuilder, error) { - registry, err := newLimitRegistry(defaults, overrides) - if err != nil { - return nil, err - } - return &TransactionBuilder{registry}, nil -} - -// RegistrationsPerIPAddressTransaction returns a Transaction for the -// NewRegistrationsPerIPAddress limit for the provided IP address. -func (builder *TransactionBuilder) RegistrationsPerIPAddressTransaction(ip net.IP) (Transaction, error) { - bucketKey, err := newIPAddressBucketKey(NewRegistrationsPerIPAddress, ip) - if err != nil { - return Transaction{}, err - } - limit, err := builder.getLimit(NewRegistrationsPerIPAddress, bucketKey) - if err != nil { - if errors.Is(err, errLimitDisabled) { - return newAllowOnlyTransaction() - } - return Transaction{}, err - } - return newTransaction(limit, bucketKey, 1) -} - -// RegistrationsPerIPv6RangeTransaction returns a Transaction for the -// NewRegistrationsPerIPv6Range limit for the /48 IPv6 range which contains the -// provided IPv6 address. -func (builder *TransactionBuilder) RegistrationsPerIPv6RangeTransaction(ip net.IP) (Transaction, error) { - bucketKey, err := newIPv6RangeCIDRBucketKey(NewRegistrationsPerIPv6Range, ip) - if err != nil { - return Transaction{}, err - } - limit, err := builder.getLimit(NewRegistrationsPerIPv6Range, bucketKey) - if err != nil { - if errors.Is(err, errLimitDisabled) { - return newAllowOnlyTransaction() - } - return Transaction{}, err - } - return newTransaction(limit, bucketKey, 1) -} - -// OrdersPerAccountTransaction returns a Transaction for the NewOrdersPerAccount -// limit for the provided ACME registration Id. -func (builder *TransactionBuilder) OrdersPerAccountTransaction(regId int64) (Transaction, error) { - bucketKey, err := newRegIdBucketKey(NewOrdersPerAccount, regId) - if err != nil { - return Transaction{}, err - } - limit, err := builder.getLimit(NewOrdersPerAccount, bucketKey) - if err != nil { - if errors.Is(err, errLimitDisabled) { - return newAllowOnlyTransaction() - } - return Transaction{}, err - } - return newTransaction(limit, bucketKey, 1) -} - -// FailedAuthorizationsPerDomainPerAccountCheckOnlyTransactions returns a slice -// of Transactions for the provided order domain names. An error is returned if -// any of the order domain names are invalid. This method should be used for -// checking capacity, before allowing more authorizations to be created. -// -// Precondition: orderDomains must all pass policy.WellFormedDomainNames. -// Precondition: len(orderDomains) < maxNames. -func (builder *TransactionBuilder) FailedAuthorizationsPerDomainPerAccountCheckOnlyTransactions(regId int64, orderDomains []string, maxNames int) ([]Transaction, error) { - if len(orderDomains) > maxNames { - return nil, fmt.Errorf("order contains more than %d DNS names", maxNames) - } - - // FailedAuthorizationsPerDomainPerAccount limit uses the 'enum:regId' - // bucket key format for overrides. - perAccountBucketKey, err := newRegIdBucketKey(FailedAuthorizationsPerDomainPerAccount, regId) - if err != nil { - return nil, err - } - limit, err := builder.getLimit(FailedAuthorizationsPerDomainPerAccount, perAccountBucketKey) - if err != nil && !errors.Is(err, errLimitDisabled) { - return nil, err - } - - var txns []Transaction - for _, name := range DomainsForRateLimiting(orderDomains) { - // FailedAuthorizationsPerDomainPerAccount limit uses the - // 'enum:regId:domain' bucket key format for transactions. - perDomainPerAccountBucketKey, err := newRegIdDomainBucketKey(FailedAuthorizationsPerDomainPerAccount, regId, name) - if err != nil { - return nil, err - } - - // Add a check-only transaction for each per domain per account bucket. - // The cost is 0, as we are only checking that the account and domain - // pair aren't already over the limit. - txn, err := newCheckOnlyTransaction(limit, perDomainPerAccountBucketKey, 0) - if err != nil { - return nil, err - } - txns = append(txns, txn) - } - return txns, nil -} - -// FailedAuthorizationsPerDomainPerAccountSpendOnlyTransaction returns a spend- -// only Transaction for the provided order domain name. An error is returned if -// the order domain name is invalid. This method should be used for spending -// capacity, as a result of a failed authorization. -func (builder *TransactionBuilder) FailedAuthorizationsPerDomainPerAccountSpendOnlyTransaction(regId int64, orderDomain string) (Transaction, error) { - // FailedAuthorizationsPerDomainPerAccount limit uses the 'enum:regId' - // bucket key format for overrides. - perAccountBucketKey, err := newRegIdBucketKey(FailedAuthorizationsPerDomainPerAccount, regId) - if err != nil { - return Transaction{}, err - } - limit, err := builder.getLimit(FailedAuthorizationsPerDomainPerAccount, perAccountBucketKey) - if err != nil && !errors.Is(err, errLimitDisabled) { - return Transaction{}, err - } - - // FailedAuthorizationsPerDomainPerAccount limit uses the - // 'enum:regId:domain' bucket key format for transactions. - perDomainPerAccountBucketKey, err := newRegIdDomainBucketKey(FailedAuthorizationsPerDomainPerAccount, regId, orderDomain) - if err != nil { - return Transaction{}, err - } - txn, err := newSpendOnlyTransaction(limit, perDomainPerAccountBucketKey, 1) - if err != nil { - return Transaction{}, err - } - - return txn, nil -} - -// CertificatesPerDomainTransactions returns a slice of Transactions for the -// provided order domain names. An error is returned if any of the order domain -// names are invalid. When a CertificatesPerDomainPerAccount override is -// configured, two types of Transactions are returned: -// - A spend-only Transaction for each per domain bucket. Spend-only transactions -// will not be denied if the bucket lacks the capacity to satisfy the cost. -// - A check-and-spend Transaction for each per account per domain bucket. Check- -// and-spend transactions will be denied if the bucket lacks the capacity to -// satisfy the cost. -// -// When a CertificatesPerDomainPerAccount override is not configured, a check- -// and-spend Transaction is returned for each per domain bucket. -// -// Precondition: orderDomains must all pass policy.WellFormedDomainNames. -// Precondition: len(orderDomains) < maxNames. -func (builder *TransactionBuilder) CertificatesPerDomainTransactions(regId int64, orderDomains []string, maxNames int) ([]Transaction, error) { - if len(orderDomains) > maxNames { - return nil, fmt.Errorf("order contains more than %d DNS names", maxNames) - } - - perAccountLimitBucketKey, err := newRegIdBucketKey(CertificatesPerDomainPerAccount, regId) - if err != nil { - return nil, err - } - perAccountLimit, err := builder.getLimit(CertificatesPerDomainPerAccount, perAccountLimitBucketKey) - if err != nil && !errors.Is(err, errLimitDisabled) { - return nil, err - } - - var txns []Transaction - for _, name := range DomainsForRateLimiting(orderDomains) { - perDomainBucketKey, err := newDomainBucketKey(CertificatesPerDomain, name) - if err != nil { - return nil, err - } - if perAccountLimit.isOverride() { - // An override is configured for the CertificatesPerDomainPerAccount - // limit. - perAccountPerDomainKey, err := newRegIdDomainBucketKey(CertificatesPerDomainPerAccount, regId, name) - if err != nil { - return nil, err - } - // Add a check-and-spend transaction for each per account per domain - // bucket. - txn, err := newTransaction(perAccountLimit, perAccountPerDomainKey, 1) - if err != nil { - return nil, err - } - txns = append(txns, txn) - - perDomainLimit, err := builder.getLimit(CertificatesPerDomain, perDomainBucketKey) - if errors.Is(err, errLimitDisabled) { - // Skip disabled limit. - continue - } - if err != nil { - return nil, err - } - - // Add a spend-only transaction for each per domain bucket. - txn, err = newSpendOnlyTransaction(perDomainLimit, perDomainBucketKey, 1) - if err != nil { - return nil, err - } - txns = append(txns, txn) - } else { - // Use the per domain bucket key when no per account per domain override - // is configured. - perDomainLimit, err := builder.getLimit(CertificatesPerDomain, perDomainBucketKey) - if errors.Is(err, errLimitDisabled) { - // Skip disabled limit. - continue - } - if err != nil { - return nil, err - } - // Add a check-and-spend transaction for each per domain bucket. - txn, err := newTransaction(perDomainLimit, perDomainBucketKey, 1) - if err != nil { - return nil, err - } - txns = append(txns, txn) - } - } - return txns, nil -} - -// CertificatesPerFQDNSetTransaction returns a Transaction for the provided -// order domain names. -func (builder *TransactionBuilder) CertificatesPerFQDNSetTransaction(orderNames []string) (Transaction, error) { - bucketKey, err := newFQDNSetBucketKey(CertificatesPerFQDNSet, orderNames) - if err != nil { - return Transaction{}, err - } - limit, err := builder.getLimit(CertificatesPerFQDNSet, bucketKey) - if err != nil { - if errors.Is(err, errLimitDisabled) { - return newAllowOnlyTransaction() - } - return Transaction{}, err - } - return newTransaction(limit, bucketKey, 1) -} diff --git a/third-party/github.com/letsencrypt/boulder/ratelimits/bucket_test.go b/third-party/github.com/letsencrypt/boulder/ratelimits/bucket_test.go deleted file mode 100644 index 575577caf..000000000 --- a/third-party/github.com/letsencrypt/boulder/ratelimits/bucket_test.go +++ /dev/null @@ -1,16 +0,0 @@ -package ratelimits - -import ( - "testing" - - "github.com/letsencrypt/boulder/test" -) - -func TestNewTransactionBuilder_WithBadLimitsPath(t *testing.T) { - t.Parallel() - _, err := NewTransactionBuilder("testdata/does-not-exist.yml", "") - test.AssertError(t, err, "should error") - - _, err = NewTransactionBuilder("testdata/defaults.yml", "testdata/does-not-exist.yml") - test.AssertError(t, err, "should error") -} diff --git a/third-party/github.com/letsencrypt/boulder/ratelimits/gcra.go b/third-party/github.com/letsencrypt/boulder/ratelimits/gcra.go index a712dfb98..24ae21859 100644 --- a/third-party/github.com/letsencrypt/boulder/ratelimits/gcra.go +++ b/third-party/github.com/letsencrypt/boulder/ratelimits/gcra.go @@ -9,8 +9,8 @@ import ( // maybeSpend uses the GCRA algorithm to decide whether to allow a request. It // returns a Decision struct with the result of the decision and the updated // TAT. The cost must be 0 or greater and <= the burst capacity of the limit. -func maybeSpend(clk clock.Clock, rl limit, tat time.Time, cost int64) *Decision { - if cost < 0 || cost > rl.Burst { +func maybeSpend(clk clock.Clock, txn Transaction, tat time.Time) *Decision { + if txn.cost < 0 || txn.cost > txn.limit.burst { // The condition above is the union of the conditions checked in Check // and Spend methods of Limiter. If this panic is reached, it means that // the caller has introduced a bug. @@ -27,36 +27,38 @@ func maybeSpend(clk clock.Clock, rl limit, tat time.Time, cost int64) *Decision } // Compute the cost increment. - costIncrement := rl.emissionInterval * cost + costIncrement := txn.limit.emissionInterval * txn.cost // Deduct the cost to find the new TAT and residual capacity. newTAT := tatUnix + costIncrement - difference := nowUnix - (newTAT - rl.burstOffset) + difference := nowUnix - (newTAT - txn.limit.burstOffset) if difference < 0 { // Too little capacity to satisfy the cost, deny the request. - residual := (nowUnix - (tatUnix - rl.burstOffset)) / rl.emissionInterval + residual := (nowUnix - (tatUnix - txn.limit.burstOffset)) / txn.limit.emissionInterval return &Decision{ - Allowed: false, - Remaining: residual, - RetryIn: -time.Duration(difference), - ResetIn: time.Duration(tatUnix - nowUnix), - newTAT: time.Unix(0, tatUnix).UTC(), + allowed: false, + remaining: residual, + retryIn: -time.Duration(difference), + resetIn: time.Duration(tatUnix - nowUnix), + newTAT: time.Unix(0, tatUnix).UTC(), + transaction: txn, } } // There is enough capacity to satisfy the cost, allow the request. var retryIn time.Duration - residual := difference / rl.emissionInterval + residual := difference / txn.limit.emissionInterval if difference < costIncrement { retryIn = time.Duration(costIncrement - difference) } return &Decision{ - Allowed: true, - Remaining: residual, - RetryIn: retryIn, - ResetIn: time.Duration(newTAT - nowUnix), - newTAT: time.Unix(0, newTAT).UTC(), + allowed: true, + remaining: residual, + retryIn: retryIn, + resetIn: time.Duration(newTAT - nowUnix), + newTAT: time.Unix(0, newTAT).UTC(), + transaction: txn, } } @@ -64,8 +66,8 @@ func maybeSpend(clk clock.Clock, rl limit, tat time.Time, cost int64) *Decision // the cost of a request which was previously spent. The refund cost must be 0 // or greater. A cost will only be refunded up to the burst capacity of the // limit. A partial refund is still considered successful. -func maybeRefund(clk clock.Clock, rl limit, tat time.Time, cost int64) *Decision { - if cost < 0 || cost > rl.Burst { +func maybeRefund(clk clock.Clock, txn Transaction, tat time.Time) *Decision { + if txn.cost < 0 || txn.cost > txn.limit.burst { // The condition above is checked in the Refund method of Limiter. If // this panic is reached, it means that the caller has introduced a bug. panic("invalid cost for maybeRefund") @@ -77,16 +79,17 @@ func maybeRefund(clk clock.Clock, rl limit, tat time.Time, cost int64) *Decision if nowUnix > tatUnix { // The TAT is in the past, therefore the bucket is full. return &Decision{ - Allowed: false, - Remaining: rl.Burst, - RetryIn: time.Duration(0), - ResetIn: time.Duration(0), - newTAT: tat, + allowed: false, + remaining: txn.limit.burst, + retryIn: time.Duration(0), + resetIn: time.Duration(0), + newTAT: tat, + transaction: txn, } } // Compute the refund increment. - refundIncrement := rl.emissionInterval * cost + refundIncrement := txn.limit.emissionInterval * txn.cost // Subtract the refund increment from the TAT to find the new TAT. newTAT := tatUnix - refundIncrement @@ -97,14 +100,15 @@ func maybeRefund(clk clock.Clock, rl limit, tat time.Time, cost int64) *Decision } // Calculate the new capacity. - difference := nowUnix - (newTAT - rl.burstOffset) - residual := difference / rl.emissionInterval + difference := nowUnix - (newTAT - txn.limit.burstOffset) + residual := difference / txn.limit.emissionInterval return &Decision{ - Allowed: (newTAT != tatUnix), - Remaining: residual, - RetryIn: time.Duration(0), - ResetIn: time.Duration(newTAT - nowUnix), - newTAT: time.Unix(0, newTAT).UTC(), + allowed: (newTAT != tatUnix), + remaining: residual, + retryIn: time.Duration(0), + resetIn: time.Duration(newTAT - nowUnix), + newTAT: time.Unix(0, newTAT).UTC(), + transaction: txn, } } diff --git a/third-party/github.com/letsencrypt/boulder/ratelimits/gcra_test.go b/third-party/github.com/letsencrypt/boulder/ratelimits/gcra_test.go index c1ebcf53c..7f9fb2ca3 100644 --- a/third-party/github.com/letsencrypt/boulder/ratelimits/gcra_test.go +++ b/third-party/github.com/letsencrypt/boulder/ratelimits/gcra_test.go @@ -5,221 +5,232 @@ import ( "time" "github.com/jmhodges/clock" + "github.com/letsencrypt/boulder/config" "github.com/letsencrypt/boulder/test" ) func TestDecide(t *testing.T) { clk := clock.NewFake() - limit := limit{Burst: 10, Count: 1, Period: config.Duration{Duration: time.Second}} + limit := &limit{burst: 10, count: 1, period: config.Duration{Duration: time.Second}} limit.precompute() // Begin by using 1 of our 10 requests. - d := maybeSpend(clk, limit, clk.Now(), 1) - test.Assert(t, d.Allowed, "should be allowed") - test.AssertEquals(t, d.Remaining, int64(9)) - test.AssertEquals(t, d.RetryIn, time.Duration(0)) - test.AssertEquals(t, d.ResetIn, time.Second) + d := maybeSpend(clk, Transaction{"test", limit, 1, true, true}, clk.Now()) + test.Assert(t, d.allowed, "should be allowed") + test.AssertEquals(t, d.remaining, int64(9)) + test.AssertEquals(t, d.retryIn, time.Duration(0)) + test.AssertEquals(t, d.resetIn, time.Second) + // Transaction is set when we're allowed. + test.AssertEquals(t, d.transaction, Transaction{"test", limit, 1, true, true}) // Immediately use another 9 of our remaining requests. - d = maybeSpend(clk, limit, d.newTAT, 9) - test.Assert(t, d.Allowed, "should be allowed") - test.AssertEquals(t, d.Remaining, int64(0)) + d = maybeSpend(clk, Transaction{"test", limit, 9, true, true}, d.newTAT) + test.Assert(t, d.allowed, "should be allowed") + test.AssertEquals(t, d.remaining, int64(0)) // We should have to wait 1 second before we can use another request but we // used 9 so we should have to wait 9 seconds to make an identical request. - test.AssertEquals(t, d.RetryIn, time.Second*9) - test.AssertEquals(t, d.ResetIn, time.Second*10) + test.AssertEquals(t, d.retryIn, time.Second*9) + test.AssertEquals(t, d.resetIn, time.Second*10) // Our new TAT should be 10 seconds (limit.Burst) in the future. test.AssertEquals(t, d.newTAT, clk.Now().Add(time.Second*10)) // Let's try using just 1 more request without waiting. - d = maybeSpend(clk, limit, d.newTAT, 1) - test.Assert(t, !d.Allowed, "should not be allowed") - test.AssertEquals(t, d.Remaining, int64(0)) - test.AssertEquals(t, d.RetryIn, time.Second) - test.AssertEquals(t, d.ResetIn, time.Second*10) + d = maybeSpend(clk, Transaction{"test", limit, 1, true, true}, d.newTAT) + test.Assert(t, !d.allowed, "should not be allowed") + test.AssertEquals(t, d.remaining, int64(0)) + test.AssertEquals(t, d.retryIn, time.Second) + test.AssertEquals(t, d.resetIn, time.Second*10) + // Transaction is set when we're denied. + test.AssertEquals(t, d.transaction, Transaction{"test", limit, 1, true, true}) // Let's try being exactly as patient as we're told to be. - clk.Add(d.RetryIn) - d = maybeSpend(clk, limit, d.newTAT, 0) - test.AssertEquals(t, d.Remaining, int64(1)) + clk.Add(d.retryIn) + d = maybeSpend(clk, Transaction{"test", limit, 0, true, true}, d.newTAT) + test.AssertEquals(t, d.remaining, int64(1)) // We are 1 second in the future, we should have 1 new request. - d = maybeSpend(clk, limit, d.newTAT, 1) - test.Assert(t, d.Allowed, "should be allowed") - test.AssertEquals(t, d.Remaining, int64(0)) - test.AssertEquals(t, d.RetryIn, time.Second) - test.AssertEquals(t, d.ResetIn, time.Second*10) + d = maybeSpend(clk, Transaction{"test", limit, 1, true, true}, d.newTAT) + test.Assert(t, d.allowed, "should be allowed") + test.AssertEquals(t, d.remaining, int64(0)) + test.AssertEquals(t, d.retryIn, time.Second) + test.AssertEquals(t, d.resetIn, time.Second*10) // Let's try waiting (10 seconds) for our whole bucket to refill. - clk.Add(d.ResetIn) + clk.Add(d.resetIn) // We should have 10 new requests. If we use 1 we should have 9 remaining. - d = maybeSpend(clk, limit, d.newTAT, 1) - test.Assert(t, d.Allowed, "should be allowed") - test.AssertEquals(t, d.Remaining, int64(9)) - test.AssertEquals(t, d.RetryIn, time.Duration(0)) - test.AssertEquals(t, d.ResetIn, time.Second) + d = maybeSpend(clk, Transaction{"test", limit, 1, true, true}, d.newTAT) + test.Assert(t, d.allowed, "should be allowed") + test.AssertEquals(t, d.remaining, int64(9)) + test.AssertEquals(t, d.retryIn, time.Duration(0)) + test.AssertEquals(t, d.resetIn, time.Second) // Wait just shy of how long we're told to wait for refilling. - clk.Add(d.ResetIn - time.Millisecond) + clk.Add(d.resetIn - time.Millisecond) // We should still have 9 remaining because we're still 1ms shy of the // refill time. - d = maybeSpend(clk, limit, d.newTAT, 0) - test.Assert(t, d.Allowed, "should be allowed") - test.AssertEquals(t, d.Remaining, int64(9)) - test.AssertEquals(t, d.RetryIn, time.Duration(0)) - test.AssertEquals(t, d.ResetIn, time.Millisecond) + d = maybeSpend(clk, Transaction{"test", limit, 0, true, true}, d.newTAT) + test.Assert(t, d.allowed, "should be allowed") + test.AssertEquals(t, d.remaining, int64(9)) + test.AssertEquals(t, d.retryIn, time.Duration(0)) + test.AssertEquals(t, d.resetIn, time.Millisecond) // Spending 0 simply informed us that we still have 9 remaining, let's see // what we have after waiting 20 hours. clk.Add(20 * time.Hour) // C'mon, big money, no whammies, no whammies, STOP! - d = maybeSpend(clk, limit, d.newTAT, 0) - test.Assert(t, d.Allowed, "should be allowed") - test.AssertEquals(t, d.Remaining, int64(10)) - test.AssertEquals(t, d.RetryIn, time.Duration(0)) - test.AssertEquals(t, d.ResetIn, time.Duration(0)) + d = maybeSpend(clk, Transaction{"test", limit, 0, true, true}, d.newTAT) + test.Assert(t, d.allowed, "should be allowed") + test.AssertEquals(t, d.remaining, int64(10)) + test.AssertEquals(t, d.retryIn, time.Duration(0)) + test.AssertEquals(t, d.resetIn, time.Duration(0)) // Turns out that the most we can accrue is 10 (limit.Burst). Let's empty // this bucket out so we can try something else. - d = maybeSpend(clk, limit, d.newTAT, 10) - test.Assert(t, d.Allowed, "should be allowed") - test.AssertEquals(t, d.Remaining, int64(0)) + d = maybeSpend(clk, Transaction{"test", limit, 10, true, true}, d.newTAT) + test.Assert(t, d.allowed, "should be allowed") + test.AssertEquals(t, d.remaining, int64(0)) // We should have to wait 1 second before we can use another request but we // used 10 so we should have to wait 10 seconds to make an identical // request. - test.AssertEquals(t, d.RetryIn, time.Second*10) - test.AssertEquals(t, d.ResetIn, time.Second*10) + test.AssertEquals(t, d.retryIn, time.Second*10) + test.AssertEquals(t, d.resetIn, time.Second*10) // If you spend 0 while you have 0 you should get 0. - d = maybeSpend(clk, limit, d.newTAT, 0) - test.Assert(t, d.Allowed, "should be allowed") - test.AssertEquals(t, d.Remaining, int64(0)) - test.AssertEquals(t, d.RetryIn, time.Duration(0)) - test.AssertEquals(t, d.ResetIn, time.Second*10) + d = maybeSpend(clk, Transaction{"test", limit, 0, true, true}, d.newTAT) + test.Assert(t, d.allowed, "should be allowed") + test.AssertEquals(t, d.remaining, int64(0)) + test.AssertEquals(t, d.retryIn, time.Duration(0)) + test.AssertEquals(t, d.resetIn, time.Second*10) // We don't play by the rules, we spend 1 when we have 0. - d = maybeSpend(clk, limit, d.newTAT, 1) - test.Assert(t, !d.Allowed, "should not be allowed") - test.AssertEquals(t, d.Remaining, int64(0)) - test.AssertEquals(t, d.RetryIn, time.Second) - test.AssertEquals(t, d.ResetIn, time.Second*10) + d = maybeSpend(clk, Transaction{"test", limit, 1, true, true}, d.newTAT) + test.Assert(t, !d.allowed, "should not be allowed") + test.AssertEquals(t, d.remaining, int64(0)) + test.AssertEquals(t, d.retryIn, time.Second) + test.AssertEquals(t, d.resetIn, time.Second*10) // Okay, maybe we should play by the rules if we want to get anywhere. - clk.Add(d.RetryIn) + clk.Add(d.retryIn) // Our patience pays off, we should have 1 new request. Let's use it. - d = maybeSpend(clk, limit, d.newTAT, 1) - test.Assert(t, d.Allowed, "should be allowed") - test.AssertEquals(t, d.Remaining, int64(0)) - test.AssertEquals(t, d.RetryIn, time.Second) - test.AssertEquals(t, d.ResetIn, time.Second*10) + d = maybeSpend(clk, Transaction{"test", limit, 1, true, true}, d.newTAT) + test.Assert(t, d.allowed, "should be allowed") + test.AssertEquals(t, d.remaining, int64(0)) + test.AssertEquals(t, d.retryIn, time.Second) + test.AssertEquals(t, d.resetIn, time.Second*10) // Refill from empty to 5. - clk.Add(d.ResetIn / 2) + clk.Add(d.resetIn / 2) // Attempt to spend 7 when we only have 5. We should be denied but the // decision should reflect a retry of 2 seconds, the time it would take to // refill from 5 to 7. - d = maybeSpend(clk, limit, d.newTAT, 7) - test.Assert(t, !d.Allowed, "should not be allowed") - test.AssertEquals(t, d.Remaining, int64(5)) - test.AssertEquals(t, d.RetryIn, time.Second*2) - test.AssertEquals(t, d.ResetIn, time.Second*5) + d = maybeSpend(clk, Transaction{"test", limit, 7, true, true}, d.newTAT) + test.Assert(t, !d.allowed, "should not be allowed") + test.AssertEquals(t, d.remaining, int64(5)) + test.AssertEquals(t, d.retryIn, time.Second*2) + test.AssertEquals(t, d.resetIn, time.Second*5) } func TestMaybeRefund(t *testing.T) { clk := clock.NewFake() - limit := limit{Burst: 10, Count: 1, Period: config.Duration{Duration: time.Second}} + limit := &limit{burst: 10, count: 1, period: config.Duration{Duration: time.Second}} limit.precompute() // Begin by using 1 of our 10 requests. - d := maybeSpend(clk, limit, clk.Now(), 1) - test.Assert(t, d.Allowed, "should be allowed") - test.AssertEquals(t, d.Remaining, int64(9)) - test.AssertEquals(t, d.RetryIn, time.Duration(0)) - test.AssertEquals(t, d.ResetIn, time.Second) + d := maybeSpend(clk, Transaction{"test", limit, 1, true, true}, clk.Now()) + test.Assert(t, d.allowed, "should be allowed") + test.AssertEquals(t, d.remaining, int64(9)) + test.AssertEquals(t, d.retryIn, time.Duration(0)) + test.AssertEquals(t, d.resetIn, time.Second) + // Transaction is set when we're refunding. + test.AssertEquals(t, d.transaction, Transaction{"test", limit, 1, true, true}) // Refund back to 10. - d = maybeRefund(clk, limit, d.newTAT, 1) - test.AssertEquals(t, d.Remaining, int64(10)) - test.AssertEquals(t, d.RetryIn, time.Duration(0)) - test.AssertEquals(t, d.ResetIn, time.Duration(0)) + d = maybeRefund(clk, Transaction{"test", limit, 1, true, true}, d.newTAT) + test.AssertEquals(t, d.remaining, int64(10)) + test.AssertEquals(t, d.retryIn, time.Duration(0)) + test.AssertEquals(t, d.resetIn, time.Duration(0)) // Refund 0, we should still have 10. - d = maybeRefund(clk, limit, d.newTAT, 0) - test.AssertEquals(t, d.Remaining, int64(10)) - test.AssertEquals(t, d.RetryIn, time.Duration(0)) - test.AssertEquals(t, d.ResetIn, time.Duration(0)) + d = maybeRefund(clk, Transaction{"test", limit, 0, true, true}, d.newTAT) + test.AssertEquals(t, d.remaining, int64(10)) + test.AssertEquals(t, d.retryIn, time.Duration(0)) + test.AssertEquals(t, d.resetIn, time.Duration(0)) // Spend 1 more of our 10 requests. - d = maybeSpend(clk, limit, d.newTAT, 1) - test.Assert(t, d.Allowed, "should be allowed") - test.AssertEquals(t, d.Remaining, int64(9)) - test.AssertEquals(t, d.RetryIn, time.Duration(0)) - test.AssertEquals(t, d.ResetIn, time.Second) + d = maybeSpend(clk, Transaction{"test", limit, 1, true, true}, d.newTAT) + test.Assert(t, d.allowed, "should be allowed") + test.AssertEquals(t, d.remaining, int64(9)) + test.AssertEquals(t, d.retryIn, time.Duration(0)) + test.AssertEquals(t, d.resetIn, time.Second) // Wait for our bucket to refill. - clk.Add(d.ResetIn) + clk.Add(d.resetIn) // Attempt to refund from 10 to 11. - d = maybeRefund(clk, limit, d.newTAT, 1) - test.Assert(t, !d.Allowed, "should not be allowed") - test.AssertEquals(t, d.Remaining, int64(10)) - test.AssertEquals(t, d.RetryIn, time.Duration(0)) - test.AssertEquals(t, d.ResetIn, time.Duration(0)) + d = maybeRefund(clk, Transaction{"test", limit, 1, true, true}, d.newTAT) + test.Assert(t, !d.allowed, "should not be allowed") + test.AssertEquals(t, d.remaining, int64(10)) + test.AssertEquals(t, d.retryIn, time.Duration(0)) + test.AssertEquals(t, d.resetIn, time.Duration(0)) + // Transaction is set when our bucket is full. + test.AssertEquals(t, d.transaction, Transaction{"test", limit, 1, true, true}) // Spend 10 all 10 of our requests. - d = maybeSpend(clk, limit, d.newTAT, 10) - test.Assert(t, d.Allowed, "should be allowed") - test.AssertEquals(t, d.Remaining, int64(0)) + d = maybeSpend(clk, Transaction{"test", limit, 10, true, true}, d.newTAT) + test.Assert(t, d.allowed, "should be allowed") + test.AssertEquals(t, d.remaining, int64(0)) // We should have to wait 1 second before we can use another request but we // used 10 so we should have to wait 10 seconds to make an identical // request. - test.AssertEquals(t, d.RetryIn, time.Second*10) - test.AssertEquals(t, d.ResetIn, time.Second*10) + test.AssertEquals(t, d.retryIn, time.Second*10) + test.AssertEquals(t, d.resetIn, time.Second*10) // Attempt a refund of 10. - d = maybeRefund(clk, limit, d.newTAT, 10) - test.AssertEquals(t, d.Remaining, int64(10)) - test.AssertEquals(t, d.RetryIn, time.Duration(0)) - test.AssertEquals(t, d.ResetIn, time.Duration(0)) + d = maybeRefund(clk, Transaction{"test", limit, 10, true, true}, d.newTAT) + test.AssertEquals(t, d.remaining, int64(10)) + test.AssertEquals(t, d.retryIn, time.Duration(0)) + test.AssertEquals(t, d.resetIn, time.Duration(0)) // Wait 11 seconds to catching up to TAT. clk.Add(11 * time.Second) // Attempt to refund to 11, then ensure it's still 10. - d = maybeRefund(clk, limit, d.newTAT, 1) - test.Assert(t, !d.Allowed, "should be allowed") - test.AssertEquals(t, d.Remaining, int64(10)) - test.AssertEquals(t, d.RetryIn, time.Duration(0)) - test.AssertEquals(t, d.ResetIn, time.Duration(0)) + d = maybeRefund(clk, Transaction{"test", limit, 1, true, true}, d.newTAT) + test.Assert(t, !d.allowed, "should be allowed") + test.AssertEquals(t, d.remaining, int64(10)) + test.AssertEquals(t, d.retryIn, time.Duration(0)) + test.AssertEquals(t, d.resetIn, time.Duration(0)) + // Transaction is set when our TAT is in the past. + test.AssertEquals(t, d.transaction, Transaction{"test", limit, 1, true, true}) // Spend 5 of our 10 requests, then refund 1. - d = maybeSpend(clk, limit, d.newTAT, 5) - d = maybeRefund(clk, limit, d.newTAT, 1) - test.Assert(t, d.Allowed, "should be allowed") - test.AssertEquals(t, d.Remaining, int64(6)) - test.AssertEquals(t, d.RetryIn, time.Duration(0)) + d = maybeSpend(clk, Transaction{"test", limit, 5, true, true}, d.newTAT) + d = maybeRefund(clk, Transaction{"test", limit, 1, true, true}, d.newTAT) + test.Assert(t, d.allowed, "should be allowed") + test.AssertEquals(t, d.remaining, int64(6)) + test.AssertEquals(t, d.retryIn, time.Duration(0)) // Wait, a 2.5 seconds to refill to 8.5 requests. clk.Add(time.Millisecond * 2500) // Ensure we have 8.5 requests. - d = maybeSpend(clk, limit, d.newTAT, 0) - test.Assert(t, d.Allowed, "should be allowed") - test.AssertEquals(t, d.Remaining, int64(8)) - test.AssertEquals(t, d.RetryIn, time.Duration(0)) + d = maybeSpend(clk, Transaction{"test", limit, 0, true, true}, d.newTAT) + test.Assert(t, d.allowed, "should be allowed") + test.AssertEquals(t, d.remaining, int64(8)) + test.AssertEquals(t, d.retryIn, time.Duration(0)) // Check that ResetIn represents the fractional earned request. - test.AssertEquals(t, d.ResetIn, time.Millisecond*1500) + test.AssertEquals(t, d.resetIn, time.Millisecond*1500) // Refund 2 requests, we should only have 10, not 10.5. - d = maybeRefund(clk, limit, d.newTAT, 2) - test.AssertEquals(t, d.Remaining, int64(10)) - test.AssertEquals(t, d.RetryIn, time.Duration(0)) - test.AssertEquals(t, d.ResetIn, time.Duration(0)) + d = maybeRefund(clk, Transaction{"test", limit, 2, true, true}, d.newTAT) + test.AssertEquals(t, d.remaining, int64(10)) + test.AssertEquals(t, d.retryIn, time.Duration(0)) + test.AssertEquals(t, d.resetIn, time.Duration(0)) } diff --git a/third-party/github.com/letsencrypt/boulder/ratelimits/limit.go b/third-party/github.com/letsencrypt/boulder/ratelimits/limit.go index df2cd268c..5919844e0 100644 --- a/third-party/github.com/letsencrypt/boulder/ratelimits/limit.go +++ b/third-party/github.com/letsencrypt/boulder/ratelimits/limit.go @@ -3,11 +3,13 @@ package ratelimits import ( "errors" "fmt" + "net/netip" "os" "strings" "github.com/letsencrypt/boulder/config" "github.com/letsencrypt/boulder/core" + "github.com/letsencrypt/boulder/identifier" "github.com/letsencrypt/boulder/strictyaml" ) @@ -15,7 +17,12 @@ import ( // currently configured. var errLimitDisabled = errors.New("limit disabled") -type limit struct { +// LimitConfig defines the exportable configuration for a rate limit or a rate +// limit override, without a `limit`'s internal fields. +// +// The zero value of this struct is invalid, because some of the fields must be +// greater than zero. +type LimitConfig struct { // Burst specifies maximum concurrent allowed requests at any given time. It // must be greater than zero. Burst int64 @@ -27,6 +34,26 @@ type limit struct { // Period is the duration of time in which the count (of requests) is // allowed. It must be greater than zero. Period config.Duration +} + +type LimitConfigs map[string]*LimitConfig + +// limit defines the configuration for a rate limit or a rate limit override. +// +// The zero value of this struct is invalid, because some of the fields must +// be greater than zero. +type limit struct { + // burst specifies maximum concurrent allowed requests at any given time. It + // must be greater than zero. + burst int64 + + // count is the number of requests allowed per period. It must be greater + // than zero. + count int64 + + // period is the duration of time in which the count (of requests) is + // allowed. It must be greater than zero. + period config.Duration // name is the name of the limit. It must be one of the Name enums defined // in this package. @@ -44,39 +71,34 @@ type limit struct { // precomputed to avoid doing the same calculation on every request. burstOffset int64 - // overrideKey is the key used to look up this limit in the overrides map. - overrideKey string -} - -// isOverride returns true if the limit is an override. -func (l *limit) isOverride() bool { - return l.overrideKey != "" + // isOverride is true if the limit is an override. + isOverride bool } // precompute calculates the emissionInterval and burstOffset for the limit. func (l *limit) precompute() { - l.emissionInterval = l.Period.Nanoseconds() / l.Count - l.burstOffset = l.emissionInterval * l.Burst + l.emissionInterval = l.period.Nanoseconds() / l.count + l.burstOffset = l.emissionInterval * l.burst } -func validateLimit(l limit) error { - if l.Burst <= 0 { - return fmt.Errorf("invalid burst '%d', must be > 0", l.Burst) +func validateLimit(l *limit) error { + if l.burst <= 0 { + return fmt.Errorf("invalid burst '%d', must be > 0", l.burst) } - if l.Count <= 0 { - return fmt.Errorf("invalid count '%d', must be > 0", l.Count) + if l.count <= 0 { + return fmt.Errorf("invalid count '%d', must be > 0", l.count) } - if l.Period.Duration <= 0 { - return fmt.Errorf("invalid period '%s', must be > 0", l.Period) + if l.period.Duration <= 0 { + return fmt.Errorf("invalid period '%s', must be > 0", l.period) } return nil } -type limits map[string]limit +type limits map[string]*limit // loadDefaults marshals the defaults YAML file at path into a map of limits. -func loadDefaults(path string) (limits, error) { - lm := make(limits) +func loadDefaults(path string) (LimitConfigs, error) { + lm := make(LimitConfigs) data, err := os.ReadFile(path) if err != nil { return nil, err @@ -89,7 +111,7 @@ func loadDefaults(path string) (limits, error) { } type overrideYAML struct { - limit `yaml:",inline"` + LimitConfig `yaml:",inline"` // Ids is a list of ids that this override applies to. Ids []struct { Id string `yaml:"id"` @@ -138,74 +160,103 @@ func parseOverrideNameId(key string) (Name, string, error) { return name, id, nil } -// loadAndParseOverrideLimits loads override limits from YAML. The YAML file -// must be formatted as a list of maps, where each map has a single key -// representing the limit name and a value that is a map containing the limit -// fields and an additional 'ids' field that is a list of ids that this override -// applies to. -func loadAndParseOverrideLimits(path string) (limits, error) { - fromFile, err := loadOverrides(path) - if err != nil { - return nil, err - } +// parseOverrideLimits validates a YAML list of override limits. It must be +// formatted as a list of maps, where each map has a single key representing the +// limit name and a value that is a map containing the limit fields and an +// additional 'ids' field that is a list of ids that this override applies to. +func parseOverrideLimits(newOverridesYAML overridesYAML) (limits, error) { parsed := make(limits) - for _, ov := range fromFile { + for _, ov := range newOverridesYAML { for k, v := range ov { - err = validateLimit(v.limit) - if err != nil { - return nil, fmt.Errorf("validating override limit %q: %w", k, err) - } name, ok := stringToName[k] if !ok { return nil, fmt.Errorf("unrecognized name %q in override limit, must be one of %v", k, limitNames) } - v.limit.name = name + + lim := &limit{ + burst: v.Burst, + count: v.Count, + period: v.Period, + name: name, + isOverride: true, + } + lim.precompute() + + err := validateLimit(lim) + if err != nil { + return nil, fmt.Errorf("validating override limit %q: %w", k, err) + } for _, entry := range v.Ids { - limit := v.limit id := entry.Id err = validateIdForName(name, id) if err != nil { return nil, fmt.Errorf( "validating name %s and id %q for override limit %q: %w", name, id, k, err) } - limit.overrideKey = joinWithColon(name.EnumString(), id) - if name == CertificatesPerFQDNSet { - // FQDNSet hashes are not a nice thing to ask for in a - // config file, so we allow the user to specify a - // comma-separated list of FQDNs and compute the hash here. - id = fmt.Sprintf("%x", core.HashNames(strings.Split(id, ","))) + + // We interpret and compute the override values for two rate + // limits, since they're not nice to ask for in a config file. + switch name { + case CertificatesPerDomain: + // Convert IP addresses to their covering /32 (IPv4) or /64 + // (IPv6) prefixes in CIDR notation. + ip, err := netip.ParseAddr(id) + if err == nil { + prefix, err := coveringPrefix(ip) + if err != nil { + return nil, fmt.Errorf( + "computing prefix for IP address %q: %w", id, err) + } + id = prefix.String() + } + case CertificatesPerFQDNSet: + // Compute the hash of a comma-separated list of identifier + // values. + var idents identifier.ACMEIdentifiers + for _, value := range strings.Split(id, ",") { + ip, err := netip.ParseAddr(value) + if err == nil { + idents = append(idents, identifier.NewIP(ip)) + } else { + idents = append(idents, identifier.NewDNS(value)) + } + } + id = fmt.Sprintf("%x", core.HashIdentifiers(idents)) } - limit.precompute() - parsed[joinWithColon(name.EnumString(), id)] = limit + + parsed[joinWithColon(name.EnumString(), id)] = lim } } } return parsed, nil } -// loadAndParseDefaultLimits loads default limits from YAML, validates them, and -// parses them into a map of limits keyed by 'Name'. -func loadAndParseDefaultLimits(path string) (limits, error) { - fromFile, err := loadDefaults(path) - if err != nil { - return nil, err - } - parsed := make(limits, len(fromFile)) +// parseDefaultLimits validates a map of default limits and rekeys it by 'Name'. +func parseDefaultLimits(newDefaultLimits LimitConfigs) (limits, error) { + parsed := make(limits) - for k, v := range fromFile { - err := validateLimit(v) - if err != nil { - return nil, fmt.Errorf("parsing default limit %q: %w", k, err) - } + for k, v := range newDefaultLimits { name, ok := stringToName[k] if !ok { return nil, fmt.Errorf("unrecognized name %q in default limit, must be one of %v", k, limitNames) } - v.name = name - v.precompute() - parsed[name.EnumString()] = v + + lim := &limit{ + burst: v.Burst, + count: v.Count, + period: v.Period, + name: name, + } + + err := validateLimit(lim) + if err != nil { + return nil, fmt.Errorf("parsing default limit %q: %w", k, err) + } + + lim.precompute() + parsed[name.EnumString()] = lim } return parsed, nil } @@ -218,37 +269,50 @@ type limitRegistry struct { overrides limits } -func newLimitRegistry(defaults, overrides string) (*limitRegistry, error) { - var err error - registry := &limitRegistry{} - registry.defaults, err = loadAndParseDefaultLimits(defaults) +func newLimitRegistryFromFiles(defaults, overrides string) (*limitRegistry, error) { + defaultsData, err := loadDefaults(defaults) if err != nil { return nil, err } if overrides == "" { - // No overrides specified, initialize an empty map. - registry.overrides = make(limits) - return registry, nil + return newLimitRegistry(defaultsData, nil) } - registry.overrides, err = loadAndParseOverrideLimits(overrides) + overridesData, err := loadOverrides(overrides) if err != nil { return nil, err } - return registry, nil + return newLimitRegistry(defaultsData, overridesData) +} + +func newLimitRegistry(defaults LimitConfigs, overrides overridesYAML) (*limitRegistry, error) { + regDefaults, err := parseDefaultLimits(defaults) + if err != nil { + return nil, err + } + + regOverrides, err := parseOverrideLimits(overrides) + if err != nil { + return nil, err + } + + return &limitRegistry{ + defaults: regDefaults, + overrides: regOverrides, + }, nil } // getLimit returns the limit for the specified by name and bucketKey, name is // required, bucketKey is optional. If bucketkey is empty, the default for the // limit specified by name is returned. If no default limit exists for the // specified name, errLimitDisabled is returned. -func (l *limitRegistry) getLimit(name Name, bucketKey string) (limit, error) { +func (l *limitRegistry) getLimit(name Name, bucketKey string) (*limit, error) { if !name.isValid() { // This should never happen. Callers should only be specifying the limit // Name enums defined in this package. - return limit{}, fmt.Errorf("specified name enum %q, is invalid", name) + return nil, fmt.Errorf("specified name enum %q, is invalid", name) } if bucketKey != "" { // Check for override. @@ -261,5 +325,5 @@ func (l *limitRegistry) getLimit(name Name, bucketKey string) (limit, error) { if ok { return dl, nil } - return limit{}, errLimitDisabled + return nil, errLimitDisabled } diff --git a/third-party/github.com/letsencrypt/boulder/ratelimits/limit_test.go b/third-party/github.com/letsencrypt/boulder/ratelimits/limit_test.go index a783e8ce6..593c811aa 100644 --- a/third-party/github.com/letsencrypt/boulder/ratelimits/limit_test.go +++ b/third-party/github.com/letsencrypt/boulder/ratelimits/limit_test.go @@ -1,14 +1,42 @@ package ratelimits import ( + "net/netip" "os" "testing" "time" "github.com/letsencrypt/boulder/config" + "github.com/letsencrypt/boulder/identifier" "github.com/letsencrypt/boulder/test" ) +// loadAndParseDefaultLimits is a helper that calls both loadDefaults and +// parseDefaultLimits to handle a YAML file. +// +// TODO(#7901): Update the tests to test these functions individually. +func loadAndParseDefaultLimits(path string) (limits, error) { + fromFile, err := loadDefaults(path) + if err != nil { + return nil, err + } + + return parseDefaultLimits(fromFile) +} + +// loadAndParseOverrideLimits is a helper that calls both loadOverrides and +// parseOverrideLimits to handle a YAML file. +// +// TODO(#7901): Update the tests to test these functions individually. +func loadAndParseOverrideLimits(path string) (limits, error) { + fromFile, err := loadOverrides(path) + if err != nil { + return nil, err + } + + return parseOverrideLimits(fromFile) +} + func TestParseOverrideNameId(t *testing.T) { // 'enum:ipv4' // Valid IPv4 address. @@ -19,10 +47,10 @@ func TestParseOverrideNameId(t *testing.T) { // 'enum:ipv6range' // Valid IPv6 address range. - name, id, err = parseOverrideNameId(NewRegistrationsPerIPv6Range.String() + ":2001:0db8:0000::/48") + name, id, err = parseOverrideNameId(NewRegistrationsPerIPv6Range.String() + ":2602:80a:6000::/48") test.AssertNotError(t, err, "should not error") test.AssertEquals(t, name, NewRegistrationsPerIPv6Range) - test.AssertEquals(t, id, "2001:0db8:0000::/48") + test.AssertEquals(t, id, "2602:80a:6000::/48") // Missing colon (this should never happen but we should avoid panicking). _, _, err = parseOverrideNameId(NewRegistrationsPerIPAddress.String() + "10.0.0.1") @@ -42,14 +70,14 @@ func TestParseOverrideNameId(t *testing.T) { } func TestValidateLimit(t *testing.T) { - err := validateLimit(limit{Burst: 1, Count: 1, Period: config.Duration{Duration: time.Second}}) + err := validateLimit(&limit{burst: 1, count: 1, period: config.Duration{Duration: time.Second}}) test.AssertNotError(t, err, "valid limit") // All of the following are invalid. - for _, l := range []limit{ - {Burst: 0, Count: 1, Period: config.Duration{Duration: time.Second}}, - {Burst: 1, Count: 0, Period: config.Duration{Duration: time.Second}}, - {Burst: 1, Count: 1, Period: config.Duration{Duration: 0}}, + for _, l := range []*limit{ + {burst: 0, count: 1, period: config.Duration{Duration: time.Second}}, + {burst: 1, count: 0, period: config.Duration{Duration: time.Second}}, + {burst: 1, count: 1, period: config.Duration{Duration: 0}}, } { err = validateLimit(l) test.AssertError(t, err, "limit should be invalid") @@ -60,52 +88,58 @@ func TestLoadAndParseOverrideLimits(t *testing.T) { // Load a single valid override limit with Id formatted as 'enum:RegId'. l, err := loadAndParseOverrideLimits("testdata/working_override.yml") test.AssertNotError(t, err, "valid single override limit") - expectKey := joinWithColon(NewRegistrationsPerIPAddress.EnumString(), "10.0.0.2") - test.AssertEquals(t, l[expectKey].Burst, int64(40)) - test.AssertEquals(t, l[expectKey].Count, int64(40)) - test.AssertEquals(t, l[expectKey].Period.Duration, time.Second) + expectKey := joinWithColon(NewRegistrationsPerIPAddress.EnumString(), "64.112.117.1") + test.AssertEquals(t, l[expectKey].burst, int64(40)) + test.AssertEquals(t, l[expectKey].count, int64(40)) + test.AssertEquals(t, l[expectKey].period.Duration, time.Second) - // Load single valid override limit with a 'domain' Id. - l, err = loadAndParseOverrideLimits("testdata/working_override_regid_domain.yml") - test.AssertNotError(t, err, "valid single override limit with Id of regId:domain") + // Load single valid override limit with a 'domainOrCIDR' Id. + l, err = loadAndParseOverrideLimits("testdata/working_override_regid_domainorcidr.yml") + test.AssertNotError(t, err, "valid single override limit with Id of regId:domainOrCIDR") expectKey = joinWithColon(CertificatesPerDomain.EnumString(), "example.com") - test.AssertEquals(t, l[expectKey].Burst, int64(40)) - test.AssertEquals(t, l[expectKey].Count, int64(40)) - test.AssertEquals(t, l[expectKey].Period.Duration, time.Second) + test.AssertEquals(t, l[expectKey].burst, int64(40)) + test.AssertEquals(t, l[expectKey].count, int64(40)) + test.AssertEquals(t, l[expectKey].period.Duration, time.Second) // Load multiple valid override limits with 'regId' Ids. l, err = loadAndParseOverrideLimits("testdata/working_overrides.yml") test.AssertNotError(t, err, "multiple valid override limits") - expectKey1 := joinWithColon(NewRegistrationsPerIPAddress.EnumString(), "10.0.0.2") - test.AssertEquals(t, l[expectKey1].Burst, int64(40)) - test.AssertEquals(t, l[expectKey1].Count, int64(40)) - test.AssertEquals(t, l[expectKey1].Period.Duration, time.Second) - expectKey2 := joinWithColon(NewRegistrationsPerIPv6Range.EnumString(), "2001:0db8:0000::/48") - test.AssertEquals(t, l[expectKey2].Burst, int64(50)) - test.AssertEquals(t, l[expectKey2].Count, int64(50)) - test.AssertEquals(t, l[expectKey2].Period.Duration, time.Second*2) + expectKey1 := joinWithColon(NewRegistrationsPerIPAddress.EnumString(), "64.112.117.1") + test.AssertEquals(t, l[expectKey1].burst, int64(40)) + test.AssertEquals(t, l[expectKey1].count, int64(40)) + test.AssertEquals(t, l[expectKey1].period.Duration, time.Second) + expectKey2 := joinWithColon(NewRegistrationsPerIPv6Range.EnumString(), "2602:80a:6000::/48") + test.AssertEquals(t, l[expectKey2].burst, int64(50)) + test.AssertEquals(t, l[expectKey2].count, int64(50)) + test.AssertEquals(t, l[expectKey2].period.Duration, time.Second*2) // Load multiple valid override limits with 'fqdnSet' Ids, as follows: // - CertificatesPerFQDNSet:example.com // - CertificatesPerFQDNSet:example.com,example.net // - CertificatesPerFQDNSet:example.com,example.net,example.org - firstEntryKey, err := newFQDNSetBucketKey(CertificatesPerFQDNSet, []string{"example.com"}) - test.AssertNotError(t, err, "valid fqdnSet with one domain should not fail") - secondEntryKey, err := newFQDNSetBucketKey(CertificatesPerFQDNSet, []string{"example.com", "example.net"}) - test.AssertNotError(t, err, "valid fqdnSet with two domains should not fail") - thirdEntryKey, err := newFQDNSetBucketKey(CertificatesPerFQDNSet, []string{"example.com", "example.net", "example.org"}) - test.AssertNotError(t, err, "valid fqdnSet with three domains should not fail") + entryKey1 := newFQDNSetBucketKey(CertificatesPerFQDNSet, identifier.NewDNSSlice([]string{"example.com"})) + entryKey2 := newFQDNSetBucketKey(CertificatesPerFQDNSet, identifier.NewDNSSlice([]string{"example.com", "example.net"})) + entryKey3 := newFQDNSetBucketKey(CertificatesPerFQDNSet, identifier.NewDNSSlice([]string{"example.com", "example.net", "example.org"})) + entryKey4 := newFQDNSetBucketKey(CertificatesPerFQDNSet, identifier.ACMEIdentifiers{ + identifier.NewIP(netip.MustParseAddr("2602:80a:6000::1")), + identifier.NewIP(netip.MustParseAddr("9.9.9.9")), + identifier.NewDNS("example.com"), + }) + l, err = loadAndParseOverrideLimits("testdata/working_overrides_regid_fqdnset.yml") test.AssertNotError(t, err, "multiple valid override limits with 'fqdnSet' Ids") - test.AssertEquals(t, l[firstEntryKey].Burst, int64(40)) - test.AssertEquals(t, l[firstEntryKey].Count, int64(40)) - test.AssertEquals(t, l[firstEntryKey].Period.Duration, time.Second) - test.AssertEquals(t, l[secondEntryKey].Burst, int64(50)) - test.AssertEquals(t, l[secondEntryKey].Count, int64(50)) - test.AssertEquals(t, l[secondEntryKey].Period.Duration, time.Second*2) - test.AssertEquals(t, l[thirdEntryKey].Burst, int64(60)) - test.AssertEquals(t, l[thirdEntryKey].Count, int64(60)) - test.AssertEquals(t, l[thirdEntryKey].Period.Duration, time.Second*3) + test.AssertEquals(t, l[entryKey1].burst, int64(40)) + test.AssertEquals(t, l[entryKey1].count, int64(40)) + test.AssertEquals(t, l[entryKey1].period.Duration, time.Second) + test.AssertEquals(t, l[entryKey2].burst, int64(50)) + test.AssertEquals(t, l[entryKey2].count, int64(50)) + test.AssertEquals(t, l[entryKey2].period.Duration, time.Second*2) + test.AssertEquals(t, l[entryKey3].burst, int64(60)) + test.AssertEquals(t, l[entryKey3].count, int64(60)) + test.AssertEquals(t, l[entryKey3].period.Duration, time.Second*3) + test.AssertEquals(t, l[entryKey4].burst, int64(60)) + test.AssertEquals(t, l[entryKey4].count, int64(60)) + test.AssertEquals(t, l[entryKey4].period.Duration, time.Second*4) // Path is empty string. _, err = loadAndParseOverrideLimits("") @@ -120,7 +154,7 @@ func TestLoadAndParseOverrideLimits(t *testing.T) { // Burst cannot be 0. _, err = loadAndParseOverrideLimits("testdata/busted_override_burst_0.yml") test.AssertError(t, err, "single override limit with burst=0") - test.Assert(t, !os.IsNotExist(err), "test file should exist") + test.AssertContains(t, err.Error(), "invalid burst") // Id cannot be empty. _, err = loadAndParseOverrideLimits("testdata/busted_override_empty_id.yml") @@ -152,19 +186,19 @@ func TestLoadAndParseDefaultLimits(t *testing.T) { // Load a single valid default limit. l, err := loadAndParseDefaultLimits("testdata/working_default.yml") test.AssertNotError(t, err, "valid single default limit") - test.AssertEquals(t, l[NewRegistrationsPerIPAddress.EnumString()].Burst, int64(20)) - test.AssertEquals(t, l[NewRegistrationsPerIPAddress.EnumString()].Count, int64(20)) - test.AssertEquals(t, l[NewRegistrationsPerIPAddress.EnumString()].Period.Duration, time.Second) + test.AssertEquals(t, l[NewRegistrationsPerIPAddress.EnumString()].burst, int64(20)) + test.AssertEquals(t, l[NewRegistrationsPerIPAddress.EnumString()].count, int64(20)) + test.AssertEquals(t, l[NewRegistrationsPerIPAddress.EnumString()].period.Duration, time.Second) // Load multiple valid default limits. l, err = loadAndParseDefaultLimits("testdata/working_defaults.yml") test.AssertNotError(t, err, "multiple valid default limits") - test.AssertEquals(t, l[NewRegistrationsPerIPAddress.EnumString()].Burst, int64(20)) - test.AssertEquals(t, l[NewRegistrationsPerIPAddress.EnumString()].Count, int64(20)) - test.AssertEquals(t, l[NewRegistrationsPerIPAddress.EnumString()].Period.Duration, time.Second) - test.AssertEquals(t, l[NewRegistrationsPerIPv6Range.EnumString()].Burst, int64(30)) - test.AssertEquals(t, l[NewRegistrationsPerIPv6Range.EnumString()].Count, int64(30)) - test.AssertEquals(t, l[NewRegistrationsPerIPv6Range.EnumString()].Period.Duration, time.Second*2) + test.AssertEquals(t, l[NewRegistrationsPerIPAddress.EnumString()].burst, int64(20)) + test.AssertEquals(t, l[NewRegistrationsPerIPAddress.EnumString()].count, int64(20)) + test.AssertEquals(t, l[NewRegistrationsPerIPAddress.EnumString()].period.Duration, time.Second) + test.AssertEquals(t, l[NewRegistrationsPerIPv6Range.EnumString()].burst, int64(30)) + test.AssertEquals(t, l[NewRegistrationsPerIPv6Range.EnumString()].count, int64(30)) + test.AssertEquals(t, l[NewRegistrationsPerIPv6Range.EnumString()].period.Duration, time.Second*2) // Path is empty string. _, err = loadAndParseDefaultLimits("") @@ -179,7 +213,7 @@ func TestLoadAndParseDefaultLimits(t *testing.T) { // Burst cannot be 0. _, err = loadAndParseDefaultLimits("testdata/busted_default_burst_0.yml") test.AssertError(t, err, "single default limit with burst=0") - test.Assert(t, !os.IsNotExist(err), "test file should exist") + test.AssertContains(t, err.Error(), "invalid burst") // Name cannot be empty. _, err = loadAndParseDefaultLimits("testdata/busted_default_empty_name.yml") diff --git a/third-party/github.com/letsencrypt/boulder/ratelimits/limiter.go b/third-party/github.com/letsencrypt/boulder/ratelimits/limiter.go index 557a83304..b7a195028 100644 --- a/third-party/github.com/letsencrypt/boulder/ratelimits/limiter.go +++ b/third-party/github.com/letsencrypt/boulder/ratelimits/limiter.go @@ -5,11 +5,15 @@ import ( "errors" "fmt" "math" + "math/rand/v2" "slices" + "strings" "time" "github.com/jmhodges/clock" "github.com/prometheus/client_golang/prometheus" + + berrors "github.com/letsencrypt/boulder/errors" ) const ( @@ -24,61 +28,153 @@ const ( // allowedDecision is an "allowed" *Decision that should be returned when a // checked limit is found to be disabled. -var allowedDecision = &Decision{Allowed: true, Remaining: math.MaxInt64} +var allowedDecision = &Decision{allowed: true, remaining: math.MaxInt64} // Limiter provides a high-level interface for rate limiting requests by -// utilizing a leaky bucket-style approach. +// utilizing a token bucket-style approach. type Limiter struct { // source is used to store buckets. It must be safe for concurrent use. - source source + source Source clk clock.Clock - spendLatency *prometheus.HistogramVec - overrideUsageGauge *prometheus.GaugeVec + spendLatency *prometheus.HistogramVec } // NewLimiter returns a new *Limiter. The provided source must be safe for // concurrent use. -func NewLimiter(clk clock.Clock, source source, stats prometheus.Registerer) (*Limiter, error) { - limiter := &Limiter{source: source, clk: clk} - limiter.spendLatency = prometheus.NewHistogramVec(prometheus.HistogramOpts{ +func NewLimiter(clk clock.Clock, source Source, stats prometheus.Registerer) (*Limiter, error) { + spendLatency := prometheus.NewHistogramVec(prometheus.HistogramOpts{ Name: "ratelimits_spend_latency", Help: fmt.Sprintf("Latency of ratelimit checks labeled by limit=[name] and decision=[%s|%s], in seconds", Allowed, Denied), // Exponential buckets ranging from 0.0005s to 3s. Buckets: prometheus.ExponentialBuckets(0.0005, 3, 8), }, []string{"limit", "decision"}) - stats.MustRegister(limiter.spendLatency) + stats.MustRegister(spendLatency) - limiter.overrideUsageGauge = prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Name: "ratelimits_override_usage", - Help: "Proportion of override limit used, by limit name and bucket key.", - }, []string{"limit", "bucket_key"}) - stats.MustRegister(limiter.overrideUsageGauge) - - return limiter, nil + return &Limiter{ + source: source, + clk: clk, + spendLatency: spendLatency, + }, nil } +// Decision represents the result of a rate limit check or spend operation. To +// check the result of a *Decision, call the Result() method. type Decision struct { - // Allowed is true if the bucket possessed enough capacity to allow the + // allowed is true if the bucket possessed enough capacity to allow the // request given the cost. - Allowed bool + allowed bool - // Remaining is the number of requests the client is allowed to make before + // remaining is the number of requests the client is allowed to make before // they're rate limited. - Remaining int64 + remaining int64 - // RetryIn is the duration the client MUST wait before they're allowed to + // retryIn is the duration the client MUST wait before they're allowed to // make a request. - RetryIn time.Duration + retryIn time.Duration - // ResetIn is the duration the bucket will take to refill to its maximum + // resetIn is the duration the bucket will take to refill to its maximum // capacity, assuming no further requests are made. - ResetIn time.Duration + resetIn time.Duration // newTAT indicates the time at which the bucket will be full. It is the // theoretical arrival time (TAT) of next request. It must be no more than // (burst * (period / count)) in the future at any single point in time. newTAT time.Time + + // transaction is the Transaction that resulted in this Decision. It is + // included for the production of verbose Subscriber-facing errors. It is + // set by the Limiter before returning the Decision. + transaction Transaction +} + +// Result translates a denied *Decision into a berrors.RateLimitError for the +// Subscriber, or returns nil if the *Decision allows the request. The error +// message includes a human-readable description of the exceeded rate limit and +// a retry-after timestamp. +func (d *Decision) Result(now time.Time) error { + if d.allowed { + return nil + } + + // Add 0-3% jitter to the RetryIn duration to prevent thundering herd. + jitter := time.Duration(float64(d.retryIn) * 0.03 * rand.Float64()) + retryAfter := d.retryIn + jitter + retryAfterTs := now.UTC().Add(retryAfter).Format("2006-01-02 15:04:05 MST") + + // There is no case for FailedAuthorizationsForPausingPerDomainPerAccount + // because the RA will pause clients who exceed that ratelimit. + switch d.transaction.limit.name { + case NewRegistrationsPerIPAddress: + return berrors.RegistrationsPerIPAddressError( + retryAfter, + "too many new registrations (%d) from this IP address in the last %s, retry after %s", + d.transaction.limit.burst, + d.transaction.limit.period.Duration, + retryAfterTs, + ) + + case NewRegistrationsPerIPv6Range: + return berrors.RegistrationsPerIPv6RangeError( + retryAfter, + "too many new registrations (%d) from this /48 subnet of IPv6 addresses in the last %s, retry after %s", + d.transaction.limit.burst, + d.transaction.limit.period.Duration, + retryAfterTs, + ) + case NewOrdersPerAccount: + return berrors.NewOrdersPerAccountError( + retryAfter, + "too many new orders (%d) from this account in the last %s, retry after %s", + d.transaction.limit.burst, + d.transaction.limit.period.Duration, + retryAfterTs, + ) + + case FailedAuthorizationsPerDomainPerAccount: + // Uses bucket key 'enum:regId:identValue'. + idx := strings.LastIndex(d.transaction.bucketKey, ":") + if idx == -1 { + return berrors.InternalServerError("unrecognized bucket key while generating error") + } + identValue := d.transaction.bucketKey[idx+1:] + return berrors.FailedAuthorizationsPerDomainPerAccountError( + retryAfter, + "too many failed authorizations (%d) for %q in the last %s, retry after %s", + d.transaction.limit.burst, + identValue, + d.transaction.limit.period.Duration, + retryAfterTs, + ) + + case CertificatesPerDomain, CertificatesPerDomainPerAccount: + // Uses bucket key 'enum:domainOrCIDR' or 'enum:regId:domainOrCIDR' respectively. + idx := strings.LastIndex(d.transaction.bucketKey, ":") + if idx == -1 { + return berrors.InternalServerError("unrecognized bucket key while generating error") + } + domainOrCIDR := d.transaction.bucketKey[idx+1:] + return berrors.CertificatesPerDomainError( + retryAfter, + "too many certificates (%d) already issued for %q in the last %s, retry after %s", + d.transaction.limit.burst, + domainOrCIDR, + d.transaction.limit.period.Duration, + retryAfterTs, + ) + + case CertificatesPerFQDNSet: + return berrors.CertificatesPerFQDNSetError( + retryAfter, + "too many certificates (%d) already issued for this exact set of identifiers in the last %s, retry after %s", + d.transaction.limit.burst, + d.transaction.limit.period.Duration, + retryAfterTs, + ) + + default: + return berrors.InternalServerError("cannot generate error for unknown rate limit") + } } // Check DOES NOT deduct the cost of the request from the provided bucket's @@ -101,9 +197,9 @@ func (l *Limiter) Check(ctx context.Context, txn Transaction) (*Decision, error) // First request from this client. No need to initialize the bucket // because this is a check, not a spend. A TAT of "now" is equivalent to // a full bucket. - return maybeSpend(l.clk, txn.limit, l.clk.Now(), txn.cost), nil + return maybeSpend(l.clk, txn, l.clk.Now()), nil } - return maybeSpend(l.clk, txn.limit, tat, txn.cost), nil + return maybeSpend(l.clk, txn, tat), nil } // Spend attempts to deduct the cost from the provided bucket's capacity. The @@ -133,39 +229,27 @@ func prepareBatch(txns []Transaction) ([]Transaction, []string, error) { return transactions, bucketKeys, nil } -type batchDecision struct { - *Decision -} - -func newBatchDecision() *batchDecision { - return &batchDecision{ - Decision: &Decision{ - Allowed: true, - Remaining: math.MaxInt64, - }, +func stricter(existing *Decision, incoming *Decision) *Decision { + if existing.retryIn == incoming.retryIn { + if existing.remaining < incoming.remaining { + return existing + } + return incoming } -} - -func (d *batchDecision) merge(in *Decision) { - d.Allowed = d.Allowed && in.Allowed - d.Remaining = min(d.Remaining, in.Remaining) - d.RetryIn = max(d.RetryIn, in.RetryIn) - d.ResetIn = max(d.ResetIn, in.ResetIn) - if in.newTAT.After(d.newTAT) { - d.newTAT = in.newTAT + if existing.retryIn > incoming.retryIn { + return existing } + return incoming } // BatchSpend attempts to deduct the costs from the provided buckets' // capacities. If applicable, new bucket states are persisted to the underlying // datastore before returning. Non-existent buckets will be initialized WITH the -// cost factored into the initial state. The following rules are applied to -// merge the Decisions for each Transaction into a single batch Decision: -// - Allowed is true if all Transactions where check is true were allowed, -// - RetryIn and ResetIn are the largest values of each across all Decisions, -// - Remaining is the smallest value of each across all Decisions, and -// - Decisions resulting from spend-only Transactions are never merged. +// cost factored into the initial state. The returned *Decision represents the +// strictest of all *Decisions reached in the batch. func (l *Limiter) BatchSpend(ctx context.Context, txns []Transaction) (*Decision, error) { + start := l.clk.Now() + batch, bucketKeys, err := prepareBatch(txns) if err != nil { return nil, err @@ -180,47 +264,91 @@ func (l *Limiter) BatchSpend(ctx context.Context, txns []Transaction) (*Decision ctx = context.WithoutCancel(ctx) tats, err := l.source.BatchGet(ctx, bucketKeys) if err != nil { - return nil, err + return nil, fmt.Errorf("batch get for %d keys: %w", len(bucketKeys), err) } - - start := l.clk.Now() - batchDecision := newBatchDecision() - newTATs := make(map[string]time.Time) + batchDecision := allowedDecision + newBuckets := make(map[string]time.Time) + incrBuckets := make(map[string]increment) + staleBuckets := make(map[string]time.Time) + txnOutcomes := make(map[Transaction]string) for _, txn := range batch { - tat, exists := tats[txn.bucketKey] - if !exists { - // First request from this client. - tat = l.clk.Now() - } + storedTAT, bucketExists := tats[txn.bucketKey] + d := maybeSpend(l.clk, txn, storedTAT) - d := maybeSpend(l.clk, txn.limit, tat, txn.cost) - - if txn.limit.isOverride() { - utilization := float64(txn.limit.Burst-d.Remaining) / float64(txn.limit.Burst) - l.overrideUsageGauge.WithLabelValues(txn.limit.name.String(), txn.limit.overrideKey).Set(utilization) - } - - if d.Allowed && (tat != d.newTAT) && txn.spend { - // New bucket state should be persisted. - newTATs[txn.bucketKey] = d.newTAT + if d.allowed && (storedTAT != d.newTAT) && txn.spend { + if !bucketExists { + newBuckets[txn.bucketKey] = d.newTAT + } else if storedTAT.After(l.clk.Now()) { + incrBuckets[txn.bucketKey] = increment{ + cost: time.Duration(txn.cost * txn.limit.emissionInterval), + ttl: time.Duration(txn.limit.burstOffset), + } + } else { + staleBuckets[txn.bucketKey] = d.newTAT + } } if !txn.spendOnly() { - batchDecision.merge(d) + // Spend-only Transactions are best-effort and do not contribute to + // the batchDecision. + batchDecision = stricter(batchDecision, d) + } + + txnOutcomes[txn] = Denied + if d.allowed { + txnOutcomes[txn] = Allowed } } - if batchDecision.Allowed { - err = l.source.BatchSet(ctx, newTATs) - if err != nil { - return nil, err + if batchDecision.allowed { + if len(newBuckets) > 0 { + // Use BatchSetNotExisting to create new buckets so that we detect + // if concurrent requests have created this bucket at the same time, + // which would result in overwriting if we used a plain "SET" + // command. If that happens, fall back to incrementing. + alreadyExists, err := l.source.BatchSetNotExisting(ctx, newBuckets) + if err != nil { + return nil, fmt.Errorf("batch set for %d keys: %w", len(newBuckets), err) + } + // Find the original transaction in order to compute the increment + // and set the TTL. + for _, txn := range batch { + if alreadyExists[txn.bucketKey] { + incrBuckets[txn.bucketKey] = increment{ + cost: time.Duration(txn.cost * txn.limit.emissionInterval), + ttl: time.Duration(txn.limit.burstOffset), + } + } + } + } + + if len(incrBuckets) > 0 { + err = l.source.BatchIncrement(ctx, incrBuckets) + if err != nil { + return nil, fmt.Errorf("batch increment for %d keys: %w", len(incrBuckets), err) + } + } + + if len(staleBuckets) > 0 { + // Incrementing a TAT in the past grants unintended burst capacity. + // So instead we overwrite it with a TAT of now + increment. This + // approach may cause a race condition where only the last spend is + // saved, but it's preferable to the alternative. + err = l.source.BatchSet(ctx, staleBuckets) + if err != nil { + return nil, fmt.Errorf("batch set for %d keys: %w", len(staleBuckets), err) + } } - l.spendLatency.WithLabelValues("batch", Allowed).Observe(l.clk.Since(start).Seconds()) - } else { - l.spendLatency.WithLabelValues("batch", Denied).Observe(l.clk.Since(start).Seconds()) } - return batchDecision.Decision, nil + + // Observe latency equally across all transactions in the batch. + totalLatency := l.clk.Since(start) + perTxnLatency := totalLatency / time.Duration(len(txnOutcomes)) + for txn, outcome := range txnOutcomes { + l.spendLatency.WithLabelValues(txn.limit.name.String(), outcome).Observe(perTxnLatency.Seconds()) + } + return batchDecision, nil } // Refund attempts to refund all of the cost to the capacity of the specified @@ -243,12 +371,8 @@ func (l *Limiter) Refund(ctx context.Context, txn Transaction) (*Decision, error // buckets' capacities. Non-existent buckets will NOT be initialized. The new // bucket state is persisted to the underlying datastore, if applicable, before // returning. Spend-only Transactions are assumed to be refundable. Check-only -// Transactions are never refunded. The following rules are applied to merge the -// Decisions for each Transaction into a single batch Decision: -// - Allowed is true if all Transactions where check is true were allowed, -// - RetryIn and ResetIn are the largest values of each across all Decisions, -// - Remaining is the smallest value of each across all Decisions, and -// - Decisions resulting from spend-only Transactions are never merged. +// Transactions are never refunded. The returned *Decision represents the +// strictest of all *Decisions reached in the batch. func (l *Limiter) BatchRefund(ctx context.Context, txns []Transaction) (*Decision, error) { batch, bucketKeys, err := prepareBatch(txns) if err != nil { @@ -264,38 +388,41 @@ func (l *Limiter) BatchRefund(ctx context.Context, txns []Transaction) (*Decisio ctx = context.WithoutCancel(ctx) tats, err := l.source.BatchGet(ctx, bucketKeys) if err != nil { - return nil, err + return nil, fmt.Errorf("batch get for %d keys: %w", len(bucketKeys), err) } - batchDecision := newBatchDecision() - newTATs := make(map[string]time.Time) + batchDecision := allowedDecision + incrBuckets := make(map[string]increment) for _, txn := range batch { - tat, exists := tats[txn.bucketKey] - if !exists { + tat, bucketExists := tats[txn.bucketKey] + if !bucketExists { // Ignore non-existent bucket. continue } - var cost int64 - if !txn.checkOnly() { - cost = txn.cost + if txn.checkOnly() { + // The cost of check-only transactions are never refunded. + txn.cost = 0 } - d := maybeRefund(l.clk, txn.limit, tat, cost) - batchDecision.merge(d) - if d.Allowed && tat != d.newTAT { + d := maybeRefund(l.clk, txn, tat) + batchDecision = stricter(batchDecision, d) + if d.allowed && tat != d.newTAT { // New bucket state should be persisted. - newTATs[txn.bucketKey] = d.newTAT + incrBuckets[txn.bucketKey] = increment{ + cost: time.Duration(-txn.cost * txn.limit.emissionInterval), + ttl: time.Duration(txn.limit.burstOffset), + } } } - if len(newTATs) > 0 { - err = l.source.BatchSet(ctx, newTATs) + if len(incrBuckets) > 0 { + err = l.source.BatchIncrement(ctx, incrBuckets) if err != nil { - return nil, err + return nil, fmt.Errorf("batch increment for %d keys: %w", len(incrBuckets), err) } } - return batchDecision.Decision, nil + return batchDecision, nil } // Reset resets the specified bucket to its maximum capacity. The new bucket diff --git a/third-party/github.com/letsencrypt/boulder/ratelimits/limiter_test.go b/third-party/github.com/letsencrypt/boulder/ratelimits/limiter_test.go index efec45432..eb6f938b6 100644 --- a/third-party/github.com/letsencrypt/boulder/ratelimits/limiter_test.go +++ b/third-party/github.com/letsencrypt/boulder/ratelimits/limiter_test.go @@ -2,24 +2,26 @@ package ratelimits import ( "context" - "math/rand" + "math/rand/v2" "net" + "net/netip" "testing" "time" "github.com/jmhodges/clock" - "github.com/prometheus/client_golang/prometheus" + "github.com/letsencrypt/boulder/config" + berrors "github.com/letsencrypt/boulder/errors" "github.com/letsencrypt/boulder/metrics" "github.com/letsencrypt/boulder/test" ) -// tenZeroZeroTwo is overridden in 'testdata/working_override.yml' to have -// higher burst and count values. -const tenZeroZeroTwo = "10.0.0.2" +// overriddenIP is overridden in 'testdata/working_override.yml' to have higher +// burst and count values. +const overriddenIP = "64.112.117.1" // newTestLimiter constructs a new limiter. -func newTestLimiter(t *testing.T, s source, clk clock.FakeClock) *Limiter { +func newTestLimiter(t *testing.T, s Source, clk clock.FakeClock) *Limiter { l, err := NewLimiter(clk, s, metrics.NoopRegisterer) test.AssertNotError(t, err, "should not error") return l @@ -28,9 +30,9 @@ func newTestLimiter(t *testing.T, s source, clk clock.FakeClock) *Limiter { // newTestTransactionBuilder constructs a new *TransactionBuilder with the // following configuration: // - 'NewRegistrationsPerIPAddress' burst: 20 count: 20 period: 1s -// - 'NewRegistrationsPerIPAddress:10.0.0.2' burst: 40 count: 40 period: 1s +// - 'NewRegistrationsPerIPAddress:64.112.117.1' burst: 40 count: 40 period: 1s func newTestTransactionBuilder(t *testing.T) *TransactionBuilder { - c, err := NewTransactionBuilder("testdata/working_default.yml", "testdata/working_override.yml") + c, err := NewTransactionBuilderFromFiles("testdata/working_default.yml", "testdata/working_override.yml") test.AssertNotError(t, err, "should not error") return c } @@ -43,7 +45,7 @@ func setup(t *testing.T) (context.Context, map[string]*Limiter, *TransactionBuil // runs. randIP := make(net.IP, 4) for i := range 4 { - randIP[i] = byte(rand.Intn(256)) + randIP[i] = byte(rand.IntN(256)) } // Construct a limiter for each source. @@ -58,14 +60,7 @@ func TestLimiter_CheckWithLimitOverrides(t *testing.T) { testCtx, limiters, txnBuilder, clk, testIP := setup(t) for name, l := range limiters { t.Run(name, func(t *testing.T) { - // Verify our overrideUsageGauge is being set correctly. 0.0 == 0% - // of the bucket has been consumed. - test.AssertMetricWithLabelsEquals(t, l.overrideUsageGauge, prometheus.Labels{ - "limit": NewRegistrationsPerIPAddress.String(), - "bucket_key": joinWithColon(NewRegistrationsPerIPAddress.EnumString(), tenZeroZeroTwo)}, 0) - - overriddenBucketKey, err := newIPAddressBucketKey(NewRegistrationsPerIPAddress, net.ParseIP(tenZeroZeroTwo)) - test.AssertNotError(t, err, "should not error") + overriddenBucketKey := newIPAddressBucketKey(NewRegistrationsPerIPAddress, netip.MustParseAddr(overriddenIP)) overriddenLimit, err := txnBuilder.getLimit(NewRegistrationsPerIPAddress, overriddenBucketKey) test.AssertNotError(t, err, "should not error") @@ -74,61 +69,53 @@ func TestLimiter_CheckWithLimitOverrides(t *testing.T) { test.AssertNotError(t, err, "txn should be valid") d, err := l.Spend(testCtx, overriddenTxn40) test.AssertNotError(t, err, "should not error") - test.Assert(t, d.Allowed, "should be allowed") + test.Assert(t, d.allowed, "should be allowed") // Attempting to spend 1 more, this should fail. overriddenTxn1, err := newTransaction(overriddenLimit, overriddenBucketKey, 1) test.AssertNotError(t, err, "txn should be valid") d, err = l.Spend(testCtx, overriddenTxn1) test.AssertNotError(t, err, "should not error") - test.Assert(t, !d.Allowed, "should not be allowed") - test.AssertEquals(t, d.Remaining, int64(0)) - test.AssertEquals(t, d.ResetIn, time.Second) - - // Verify our overrideUsageGauge is being set correctly. 1.0 == 100% - // of the bucket has been consumed. - test.AssertMetricWithLabelsEquals(t, l.overrideUsageGauge, prometheus.Labels{ - "limit_name": NewRegistrationsPerIPAddress.String(), - "bucket_key": joinWithColon(NewRegistrationsPerIPAddress.EnumString(), tenZeroZeroTwo)}, 1.0) + test.Assert(t, !d.allowed, "should not be allowed") + test.AssertEquals(t, d.remaining, int64(0)) + test.AssertEquals(t, d.resetIn, time.Second) // Verify our RetryIn is correct. 1 second == 1000 milliseconds and // 1000/40 = 25 milliseconds per request. - test.AssertEquals(t, d.RetryIn, time.Millisecond*25) + test.AssertEquals(t, d.retryIn, time.Millisecond*25) // Wait 50 milliseconds and try again. - clk.Add(d.RetryIn) + clk.Add(d.retryIn) // We should be allowed to spend 1 more request. d, err = l.Spend(testCtx, overriddenTxn1) test.AssertNotError(t, err, "should not error") - test.Assert(t, d.Allowed, "should be allowed") - test.AssertEquals(t, d.Remaining, int64(0)) - test.AssertEquals(t, d.ResetIn, time.Second) + test.Assert(t, d.allowed, "should be allowed") + test.AssertEquals(t, d.remaining, int64(0)) + test.AssertEquals(t, d.resetIn, time.Second) // Wait 1 second for a full bucket reset. - clk.Add(d.ResetIn) + clk.Add(d.resetIn) // Quickly spend 40 requests in a row. for i := range 40 { d, err = l.Spend(testCtx, overriddenTxn1) test.AssertNotError(t, err, "should not error") - test.Assert(t, d.Allowed, "should be allowed") - test.AssertEquals(t, d.Remaining, int64(39-i)) + test.Assert(t, d.allowed, "should be allowed") + test.AssertEquals(t, d.remaining, int64(39-i)) } // Attempting to spend 1 more, this should fail. d, err = l.Spend(testCtx, overriddenTxn1) test.AssertNotError(t, err, "should not error") - test.Assert(t, !d.Allowed, "should not be allowed") - test.AssertEquals(t, d.Remaining, int64(0)) - test.AssertEquals(t, d.ResetIn, time.Second) + test.Assert(t, !d.allowed, "should not be allowed") + test.AssertEquals(t, d.remaining, int64(0)) + test.AssertEquals(t, d.resetIn, time.Second) // Wait 1 second for a full bucket reset. - clk.Add(d.ResetIn) + clk.Add(d.resetIn) - testIP := net.ParseIP(testIP) - normalBucketKey, err := newIPAddressBucketKey(NewRegistrationsPerIPAddress, testIP) - test.AssertNotError(t, err, "should not error") + normalBucketKey := newIPAddressBucketKey(NewRegistrationsPerIPAddress, netip.MustParseAddr(testIP)) normalLimit, err := txnBuilder.getLimit(NewRegistrationsPerIPAddress, normalBucketKey) test.AssertNotError(t, err, "should not error") @@ -139,27 +126,27 @@ func TestLimiter_CheckWithLimitOverrides(t *testing.T) { test.AssertNotError(t, err, "txn should be valid") d, err = l.BatchSpend(testCtx, []Transaction{overriddenTxn1, defaultTxn1}) test.AssertNotError(t, err, "should not error") - test.Assert(t, d.Allowed, "should be allowed") - test.AssertEquals(t, d.Remaining, int64(19)) - test.AssertEquals(t, d.RetryIn, time.Duration(0)) - test.AssertEquals(t, d.ResetIn, time.Millisecond*50) + test.Assert(t, d.allowed, "should be allowed") + test.AssertEquals(t, d.remaining, int64(19)) + test.AssertEquals(t, d.retryIn, time.Duration(0)) + test.AssertEquals(t, d.resetIn, time.Millisecond*50) // Refund quota to both buckets. This should succeed, but the // decision should reflect that of the default bucket. d, err = l.BatchRefund(testCtx, []Transaction{overriddenTxn1, defaultTxn1}) test.AssertNotError(t, err, "should not error") - test.Assert(t, d.Allowed, "should be allowed") - test.AssertEquals(t, d.Remaining, int64(20)) - test.AssertEquals(t, d.RetryIn, time.Duration(0)) - test.AssertEquals(t, d.ResetIn, time.Duration(0)) + test.Assert(t, d.allowed, "should be allowed") + test.AssertEquals(t, d.remaining, int64(20)) + test.AssertEquals(t, d.retryIn, time.Duration(0)) + test.AssertEquals(t, d.resetIn, time.Duration(0)) // Once more. d, err = l.BatchSpend(testCtx, []Transaction{overriddenTxn1, defaultTxn1}) test.AssertNotError(t, err, "should not error") - test.Assert(t, d.Allowed, "should be allowed") - test.AssertEquals(t, d.Remaining, int64(19)) - test.AssertEquals(t, d.RetryIn, time.Duration(0)) - test.AssertEquals(t, d.ResetIn, time.Millisecond*50) + test.Assert(t, d.allowed, "should be allowed") + test.AssertEquals(t, d.remaining, int64(19)) + test.AssertEquals(t, d.retryIn, time.Duration(0)) + test.AssertEquals(t, d.resetIn, time.Millisecond*50) // Reset between tests. err = l.Reset(testCtx, overriddenBucketKey) @@ -174,27 +161,27 @@ func TestLimiter_CheckWithLimitOverrides(t *testing.T) { test.AssertNotError(t, err, "txn should be valid") d, err = l.BatchSpend(testCtx, []Transaction{overriddenTxn1, defaultCheckOnlyTxn1}) test.AssertNotError(t, err, "should not error") - test.AssertEquals(t, d.Remaining, int64(19)) - test.AssertEquals(t, d.RetryIn, time.Duration(0)) - test.AssertEquals(t, d.ResetIn, time.Millisecond*50) + test.AssertEquals(t, d.remaining, int64(19)) + test.AssertEquals(t, d.retryIn, time.Duration(0)) + test.AssertEquals(t, d.resetIn, time.Millisecond*50) // Check the remaining quota of the overridden bucket. overriddenCheckOnlyTxn0, err := newCheckOnlyTransaction(overriddenLimit, overriddenBucketKey, 0) test.AssertNotError(t, err, "txn should be valid") d, err = l.Check(testCtx, overriddenCheckOnlyTxn0) test.AssertNotError(t, err, "should not error") - test.AssertEquals(t, d.Remaining, int64(39)) - test.AssertEquals(t, d.RetryIn, time.Duration(0)) - test.AssertEquals(t, d.ResetIn, time.Millisecond*25) + test.AssertEquals(t, d.remaining, int64(39)) + test.AssertEquals(t, d.retryIn, time.Duration(0)) + test.AssertEquals(t, d.resetIn, time.Millisecond*25) // Check the remaining quota of the default bucket. defaultTxn0, err := newTransaction(normalLimit, normalBucketKey, 0) test.AssertNotError(t, err, "txn should be valid") d, err = l.Check(testCtx, defaultTxn0) test.AssertNotError(t, err, "should not error") - test.AssertEquals(t, d.Remaining, int64(20)) - test.AssertEquals(t, d.RetryIn, time.Duration(0)) - test.AssertEquals(t, d.ResetIn, time.Duration(0)) + test.AssertEquals(t, d.remaining, int64(20)) + test.AssertEquals(t, d.retryIn, time.Duration(0)) + test.AssertEquals(t, d.resetIn, time.Duration(0)) // Spend the same bucket but in a batch with a Transaction that is // spend-only. This should succeed, but the decision should reflect @@ -203,23 +190,23 @@ func TestLimiter_CheckWithLimitOverrides(t *testing.T) { test.AssertNotError(t, err, "txn should be valid") d, err = l.BatchSpend(testCtx, []Transaction{overriddenTxn1, defaultSpendOnlyTxn1}) test.AssertNotError(t, err, "should not error") - test.AssertEquals(t, d.Remaining, int64(38)) - test.AssertEquals(t, d.RetryIn, time.Duration(0)) - test.AssertEquals(t, d.ResetIn, time.Millisecond*50) + test.AssertEquals(t, d.remaining, int64(38)) + test.AssertEquals(t, d.retryIn, time.Duration(0)) + test.AssertEquals(t, d.resetIn, time.Millisecond*50) // Check the remaining quota of the overridden bucket. d, err = l.Check(testCtx, overriddenCheckOnlyTxn0) test.AssertNotError(t, err, "should not error") - test.AssertEquals(t, d.Remaining, int64(38)) - test.AssertEquals(t, d.RetryIn, time.Duration(0)) - test.AssertEquals(t, d.ResetIn, time.Millisecond*50) + test.AssertEquals(t, d.remaining, int64(38)) + test.AssertEquals(t, d.retryIn, time.Duration(0)) + test.AssertEquals(t, d.resetIn, time.Millisecond*50) // Check the remaining quota of the default bucket. d, err = l.Check(testCtx, defaultTxn0) test.AssertNotError(t, err, "should not error") - test.AssertEquals(t, d.Remaining, int64(19)) - test.AssertEquals(t, d.RetryIn, time.Duration(0)) - test.AssertEquals(t, d.ResetIn, time.Millisecond*50) + test.AssertEquals(t, d.remaining, int64(19)) + test.AssertEquals(t, d.retryIn, time.Duration(0)) + test.AssertEquals(t, d.resetIn, time.Millisecond*50) // Once more, but in now the spend-only Transaction will attempt to // spend 20 requests. The spend-only Transaction should fail, but @@ -228,23 +215,23 @@ func TestLimiter_CheckWithLimitOverrides(t *testing.T) { test.AssertNotError(t, err, "txn should be valid") d, err = l.BatchSpend(testCtx, []Transaction{overriddenTxn1, defaultSpendOnlyTxn20}) test.AssertNotError(t, err, "should not error") - test.AssertEquals(t, d.Remaining, int64(37)) - test.AssertEquals(t, d.RetryIn, time.Duration(0)) - test.AssertEquals(t, d.ResetIn, time.Millisecond*75) + test.AssertEquals(t, d.remaining, int64(37)) + test.AssertEquals(t, d.retryIn, time.Duration(0)) + test.AssertEquals(t, d.resetIn, time.Millisecond*75) // Check the remaining quota of the overridden bucket. d, err = l.Check(testCtx, overriddenCheckOnlyTxn0) test.AssertNotError(t, err, "should not error") - test.AssertEquals(t, d.Remaining, int64(37)) - test.AssertEquals(t, d.RetryIn, time.Duration(0)) - test.AssertEquals(t, d.ResetIn, time.Millisecond*75) + test.AssertEquals(t, d.remaining, int64(37)) + test.AssertEquals(t, d.retryIn, time.Duration(0)) + test.AssertEquals(t, d.resetIn, time.Millisecond*75) // Check the remaining quota of the default bucket. d, err = l.Check(testCtx, defaultTxn0) test.AssertNotError(t, err, "should not error") - test.AssertEquals(t, d.Remaining, int64(19)) - test.AssertEquals(t, d.RetryIn, time.Duration(0)) - test.AssertEquals(t, d.ResetIn, time.Millisecond*50) + test.AssertEquals(t, d.remaining, int64(19)) + test.AssertEquals(t, d.retryIn, time.Duration(0)) + test.AssertEquals(t, d.resetIn, time.Millisecond*50) // Reset between tests. err = l.Reset(testCtx, overriddenBucketKey) @@ -258,8 +245,7 @@ func TestLimiter_InitializationViaCheckAndSpend(t *testing.T) { testCtx, limiters, txnBuilder, _, testIP := setup(t) for name, l := range limiters { t.Run(name, func(t *testing.T) { - bucketKey, err := newIPAddressBucketKey(NewRegistrationsPerIPAddress, net.ParseIP(testIP)) - test.AssertNotError(t, err, "should not error") + bucketKey := newIPAddressBucketKey(NewRegistrationsPerIPAddress, netip.MustParseAddr(testIP)) limit, err := txnBuilder.getLimit(NewRegistrationsPerIPAddress, bucketKey) test.AssertNotError(t, err, "should not error") @@ -269,12 +255,12 @@ func TestLimiter_InitializationViaCheckAndSpend(t *testing.T) { test.AssertNotError(t, err, "txn should be valid") d, err := l.Check(testCtx, txn1) test.AssertNotError(t, err, "should not error") - test.Assert(t, d.Allowed, "should be allowed") - test.AssertEquals(t, d.Remaining, int64(19)) + test.Assert(t, d.allowed, "should be allowed") + test.AssertEquals(t, d.remaining, int64(19)) // Verify our ResetIn timing is correct. 1 second == 1000 // milliseconds and 1000/20 = 50 milliseconds per request. - test.AssertEquals(t, d.ResetIn, time.Millisecond*50) - test.AssertEquals(t, d.RetryIn, time.Duration(0)) + test.AssertEquals(t, d.resetIn, time.Millisecond*50) + test.AssertEquals(t, d.retryIn, time.Duration(0)) // However, that cost should not be spent yet, a 0 cost check should // tell us that we actually have 20 remaining. @@ -282,10 +268,10 @@ func TestLimiter_InitializationViaCheckAndSpend(t *testing.T) { test.AssertNotError(t, err, "txn should be valid") d, err = l.Check(testCtx, txn0) test.AssertNotError(t, err, "should not error") - test.Assert(t, d.Allowed, "should be allowed") - test.AssertEquals(t, d.Remaining, int64(20)) - test.AssertEquals(t, d.ResetIn, time.Duration(0)) - test.AssertEquals(t, d.RetryIn, time.Duration(0)) + test.Assert(t, d.allowed, "should be allowed") + test.AssertEquals(t, d.remaining, int64(20)) + test.AssertEquals(t, d.resetIn, time.Duration(0)) + test.AssertEquals(t, d.retryIn, time.Duration(0)) // Reset our bucket. err = l.Reset(testCtx, bucketKey) @@ -295,23 +281,23 @@ func TestLimiter_InitializationViaCheckAndSpend(t *testing.T) { // the bucket. Spend should return the same result as Check. d, err = l.Spend(testCtx, txn1) test.AssertNotError(t, err, "should not error") - test.Assert(t, d.Allowed, "should be allowed") - test.AssertEquals(t, d.Remaining, int64(19)) + test.Assert(t, d.allowed, "should be allowed") + test.AssertEquals(t, d.remaining, int64(19)) // Verify our ResetIn timing is correct. 1 second == 1000 // milliseconds and 1000/20 = 50 milliseconds per request. - test.AssertEquals(t, d.ResetIn, time.Millisecond*50) - test.AssertEquals(t, d.RetryIn, time.Duration(0)) + test.AssertEquals(t, d.resetIn, time.Millisecond*50) + test.AssertEquals(t, d.retryIn, time.Duration(0)) - // However, that cost should not be spent yet, a 0 cost check should - // tell us that we actually have 19 remaining. + // And that cost should have been spent; a 0 cost check should still + // tell us that we have 19 remaining. d, err = l.Check(testCtx, txn0) test.AssertNotError(t, err, "should not error") - test.Assert(t, d.Allowed, "should be allowed") - test.AssertEquals(t, d.Remaining, int64(19)) + test.Assert(t, d.allowed, "should be allowed") + test.AssertEquals(t, d.remaining, int64(19)) // Verify our ResetIn is correct. 1 second == 1000 milliseconds and // 1000/20 = 50 milliseconds per request. - test.AssertEquals(t, d.ResetIn, time.Millisecond*50) - test.AssertEquals(t, d.RetryIn, time.Duration(0)) + test.AssertEquals(t, d.resetIn, time.Millisecond*50) + test.AssertEquals(t, d.retryIn, time.Duration(0)) }) } } @@ -321,8 +307,7 @@ func TestLimiter_DefaultLimits(t *testing.T) { testCtx, limiters, txnBuilder, clk, testIP := setup(t) for name, l := range limiters { t.Run(name, func(t *testing.T) { - bucketKey, err := newIPAddressBucketKey(NewRegistrationsPerIPAddress, net.ParseIP(testIP)) - test.AssertNotError(t, err, "should not error") + bucketKey := newIPAddressBucketKey(NewRegistrationsPerIPAddress, netip.MustParseAddr(testIP)) limit, err := txnBuilder.getLimit(NewRegistrationsPerIPAddress, bucketKey) test.AssertNotError(t, err, "should not error") @@ -331,50 +316,50 @@ func TestLimiter_DefaultLimits(t *testing.T) { test.AssertNotError(t, err, "txn should be valid") d, err := l.Spend(testCtx, txn20) test.AssertNotError(t, err, "should not error") - test.Assert(t, d.Allowed, "should be allowed") - test.AssertEquals(t, d.Remaining, int64(0)) - test.AssertEquals(t, d.ResetIn, time.Second) + test.Assert(t, d.allowed, "should be allowed") + test.AssertEquals(t, d.remaining, int64(0)) + test.AssertEquals(t, d.resetIn, time.Second) // Attempting to spend 1 more, this should fail. txn1, err := newTransaction(limit, bucketKey, 1) test.AssertNotError(t, err, "txn should be valid") d, err = l.Spend(testCtx, txn1) test.AssertNotError(t, err, "should not error") - test.Assert(t, !d.Allowed, "should not be allowed") - test.AssertEquals(t, d.Remaining, int64(0)) - test.AssertEquals(t, d.ResetIn, time.Second) + test.Assert(t, !d.allowed, "should not be allowed") + test.AssertEquals(t, d.remaining, int64(0)) + test.AssertEquals(t, d.resetIn, time.Second) // Verify our ResetIn is correct. 1 second == 1000 milliseconds and // 1000/20 = 50 milliseconds per request. - test.AssertEquals(t, d.RetryIn, time.Millisecond*50) + test.AssertEquals(t, d.retryIn, time.Millisecond*50) // Wait 50 milliseconds and try again. - clk.Add(d.RetryIn) + clk.Add(d.retryIn) // We should be allowed to spend 1 more request. d, err = l.Spend(testCtx, txn1) test.AssertNotError(t, err, "should not error") - test.Assert(t, d.Allowed, "should be allowed") - test.AssertEquals(t, d.Remaining, int64(0)) - test.AssertEquals(t, d.ResetIn, time.Second) + test.Assert(t, d.allowed, "should be allowed") + test.AssertEquals(t, d.remaining, int64(0)) + test.AssertEquals(t, d.resetIn, time.Second) // Wait 1 second for a full bucket reset. - clk.Add(d.ResetIn) + clk.Add(d.resetIn) // Quickly spend 20 requests in a row. for i := range 20 { d, err = l.Spend(testCtx, txn1) test.AssertNotError(t, err, "should not error") - test.Assert(t, d.Allowed, "should be allowed") - test.AssertEquals(t, d.Remaining, int64(19-i)) + test.Assert(t, d.allowed, "should be allowed") + test.AssertEquals(t, d.remaining, int64(19-i)) } // Attempting to spend 1 more, this should fail. d, err = l.Spend(testCtx, txn1) test.AssertNotError(t, err, "should not error") - test.Assert(t, !d.Allowed, "should not be allowed") - test.AssertEquals(t, d.Remaining, int64(0)) - test.AssertEquals(t, d.ResetIn, time.Second) + test.Assert(t, !d.allowed, "should not be allowed") + test.AssertEquals(t, d.remaining, int64(0)) + test.AssertEquals(t, d.resetIn, time.Second) }) } } @@ -384,8 +369,7 @@ func TestLimiter_RefundAndReset(t *testing.T) { testCtx, limiters, txnBuilder, clk, testIP := setup(t) for name, l := range limiters { t.Run(name, func(t *testing.T) { - bucketKey, err := newIPAddressBucketKey(NewRegistrationsPerIPAddress, net.ParseIP(testIP)) - test.AssertNotError(t, err, "should not error") + bucketKey := newIPAddressBucketKey(NewRegistrationsPerIPAddress, netip.MustParseAddr(testIP)) limit, err := txnBuilder.getLimit(NewRegistrationsPerIPAddress, bucketKey) test.AssertNotError(t, err, "should not error") @@ -394,23 +378,23 @@ func TestLimiter_RefundAndReset(t *testing.T) { test.AssertNotError(t, err, "txn should be valid") d, err := l.Spend(testCtx, txn20) test.AssertNotError(t, err, "should not error") - test.Assert(t, d.Allowed, "should be allowed") - test.AssertEquals(t, d.Remaining, int64(0)) - test.AssertEquals(t, d.ResetIn, time.Second) + test.Assert(t, d.allowed, "should be allowed") + test.AssertEquals(t, d.remaining, int64(0)) + test.AssertEquals(t, d.resetIn, time.Second) // Refund 10 requests. txn10, err := newTransaction(limit, bucketKey, 10) test.AssertNotError(t, err, "txn should be valid") d, err = l.Refund(testCtx, txn10) test.AssertNotError(t, err, "should not error") - test.AssertEquals(t, d.Remaining, int64(10)) + test.AssertEquals(t, d.remaining, int64(10)) // Spend 10 requests, this should succeed. d, err = l.Spend(testCtx, txn10) test.AssertNotError(t, err, "should not error") - test.Assert(t, d.Allowed, "should be allowed") - test.AssertEquals(t, d.Remaining, int64(0)) - test.AssertEquals(t, d.ResetIn, time.Second) + test.Assert(t, d.allowed, "should be allowed") + test.AssertEquals(t, d.remaining, int64(0)) + test.AssertEquals(t, d.resetIn, time.Second) err = l.Reset(testCtx, bucketKey) test.AssertNotError(t, err, "should not error") @@ -418,20 +402,20 @@ func TestLimiter_RefundAndReset(t *testing.T) { // Attempt to spend 20 more requests, this should succeed. d, err = l.Spend(testCtx, txn20) test.AssertNotError(t, err, "should not error") - test.Assert(t, d.Allowed, "should be allowed") - test.AssertEquals(t, d.Remaining, int64(0)) - test.AssertEquals(t, d.ResetIn, time.Second) + test.Assert(t, d.allowed, "should be allowed") + test.AssertEquals(t, d.remaining, int64(0)) + test.AssertEquals(t, d.resetIn, time.Second) // Reset to full. - clk.Add(d.ResetIn) + clk.Add(d.resetIn) // Refund 1 requests above our limit, this should fail. txn1, err := newTransaction(limit, bucketKey, 1) test.AssertNotError(t, err, "txn should be valid") d, err = l.Refund(testCtx, txn1) test.AssertNotError(t, err, "should not error") - test.Assert(t, !d.Allowed, "should not be allowed") - test.AssertEquals(t, d.Remaining, int64(20)) + test.Assert(t, !d.allowed, "should not be allowed") + test.AssertEquals(t, d.remaining, int64(20)) // Spend so we can refund. _, err = l.Spend(testCtx, txn1) @@ -457,3 +441,149 @@ func TestLimiter_RefundAndReset(t *testing.T) { }) } } + +func TestRateLimitError(t *testing.T) { + t.Parallel() + now := clock.NewFake().Now() + + testCases := []struct { + name string + decision *Decision + expectedErr string + expectedErrType berrors.ErrorType + }{ + { + name: "Allowed decision", + decision: &Decision{ + allowed: true, + }, + }, + { + name: "RegistrationsPerIP limit reached", + decision: &Decision{ + allowed: false, + retryIn: 5 * time.Second, + transaction: Transaction{ + limit: &limit{ + name: NewRegistrationsPerIPAddress, + burst: 10, + period: config.Duration{Duration: time.Hour}, + }, + }, + }, + expectedErr: "too many new registrations (10) from this IP address in the last 1h0m0s, retry after 1970-01-01 00:00:05 UTC: see https://letsencrypt.org/docs/rate-limits/#new-registrations-per-ip-address", + expectedErrType: berrors.RateLimit, + }, + { + name: "RegistrationsPerIPv6Range limit reached", + decision: &Decision{ + allowed: false, + retryIn: 10 * time.Second, + transaction: Transaction{ + limit: &limit{ + name: NewRegistrationsPerIPv6Range, + burst: 5, + period: config.Duration{Duration: time.Hour}, + }, + }, + }, + expectedErr: "too many new registrations (5) from this /48 subnet of IPv6 addresses in the last 1h0m0s, retry after 1970-01-01 00:00:10 UTC: see https://letsencrypt.org/docs/rate-limits/#new-registrations-per-ipv6-range", + expectedErrType: berrors.RateLimit, + }, + { + name: "NewOrdersPerAccount limit reached", + decision: &Decision{ + allowed: false, + retryIn: 10 * time.Second, + transaction: Transaction{ + limit: &limit{ + name: NewOrdersPerAccount, + burst: 2, + period: config.Duration{Duration: time.Hour}, + }, + }, + }, + expectedErr: "too many new orders (2) from this account in the last 1h0m0s, retry after 1970-01-01 00:00:10 UTC: see https://letsencrypt.org/docs/rate-limits/#new-orders-per-account", + expectedErrType: berrors.RateLimit, + }, + { + name: "FailedAuthorizationsPerDomainPerAccount limit reached", + decision: &Decision{ + allowed: false, + retryIn: 15 * time.Second, + transaction: Transaction{ + limit: &limit{ + name: FailedAuthorizationsPerDomainPerAccount, + burst: 7, + period: config.Duration{Duration: time.Hour}, + }, + bucketKey: "4:12345:example.com", + }, + }, + expectedErr: "too many failed authorizations (7) for \"example.com\" in the last 1h0m0s, retry after 1970-01-01 00:00:15 UTC: see https://letsencrypt.org/docs/rate-limits/#authorization-failures-per-hostname-per-account", + expectedErrType: berrors.RateLimit, + }, + { + name: "CertificatesPerDomain limit reached", + decision: &Decision{ + allowed: false, + retryIn: 20 * time.Second, + transaction: Transaction{ + limit: &limit{ + name: CertificatesPerDomain, + burst: 3, + period: config.Duration{Duration: time.Hour}, + }, + bucketKey: "5:example.org", + }, + }, + expectedErr: "too many certificates (3) already issued for \"example.org\" in the last 1h0m0s, retry after 1970-01-01 00:00:20 UTC: see https://letsencrypt.org/docs/rate-limits/#new-certificates-per-registered-domain", + expectedErrType: berrors.RateLimit, + }, + { + name: "CertificatesPerDomainPerAccount limit reached", + decision: &Decision{ + allowed: false, + retryIn: 20 * time.Second, + transaction: Transaction{ + limit: &limit{ + name: CertificatesPerDomainPerAccount, + burst: 3, + period: config.Duration{Duration: time.Hour}, + }, + bucketKey: "6:12345678:example.net", + }, + }, + expectedErr: "too many certificates (3) already issued for \"example.net\" in the last 1h0m0s, retry after 1970-01-01 00:00:20 UTC: see https://letsencrypt.org/docs/rate-limits/#new-certificates-per-registered-domain", + expectedErrType: berrors.RateLimit, + }, + { + name: "Unknown rate limit name", + decision: &Decision{ + allowed: false, + retryIn: 30 * time.Second, + transaction: Transaction{ + limit: &limit{ + name: 9999999, + }, + }, + }, + expectedErr: "cannot generate error for unknown rate limit", + expectedErrType: berrors.InternalServer, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + err := tc.decision.Result(now) + if tc.expectedErr == "" { + test.AssertNotError(t, err, "expected no error") + } else { + test.AssertError(t, err, "expected an error") + test.AssertEquals(t, err.Error(), tc.expectedErr) + test.AssertErrorIs(t, err, tc.expectedErrType) + } + }) + } +} diff --git a/third-party/github.com/letsencrypt/boulder/ratelimits/names.go b/third-party/github.com/letsencrypt/boulder/ratelimits/names.go index fdfd8e81e..e23c03c6d 100644 --- a/third-party/github.com/letsencrypt/boulder/ratelimits/names.go +++ b/third-party/github.com/letsencrypt/boulder/ratelimits/names.go @@ -2,10 +2,11 @@ package ratelimits import ( "fmt" - "net" + "net/netip" "strconv" "strings" + "github.com/letsencrypt/boulder/iana" "github.com/letsencrypt/boulder/policy" ) @@ -13,10 +14,11 @@ import ( // limit names as strings and to provide a type-safe way to refer to rate // limits. // -// IMPORTANT: If you add a new limit Name, you MUST add: -// - it to the nameToString mapping, -// - an entry for it in the validateIdForName(), and -// - provide the appropriate constructors in bucket.go. +// IMPORTANT: If you add or remove a limit Name, you MUST update: +// - the string representation of the Name in nameToString, +// - the validators for that name in validateIdForName(), +// - the transaction constructors for that name in bucket.go, and +// - the Subscriber facing error message in ErrForDecision(). type Name int const ( @@ -44,13 +46,20 @@ const ( // depending on the context: // - When referenced in an overrides file: uses bucket key 'enum:regId', // where regId is the ACME registration Id of the account. - // - When referenced in a transaction: uses bucket key 'enum:regId:domain', - // where regId is the ACME registration Id of the account and domain is a - // domain name in the certificate. + // - When referenced in a transaction: uses bucket key + // 'enum:regId:identValue', where regId is the ACME registration Id of + // the account and identValue is the value of an identifier in the + // certificate. FailedAuthorizationsPerDomainPerAccount - // CertificatesPerDomain uses bucket key 'enum:domain', where domain is a - // domain name in the certificate. + // CertificatesPerDomain uses bucket key 'enum:domainOrCIDR', where + // domainOrCIDR is a domain name or IP address in the certificate. It uses + // two different IP address formats depending on the context: + // - When referenced in an overrides file: uses a single IP address. + // - When referenced in a transaction: uses an IP address prefix in CIDR + // notation. IPv4 prefixes must be /32, and IPv6 prefixes must be /64. + // In both cases, IPv6 addresses must be the lowest address in their /64; + // i.e. their last 64 bits must be zero. CertificatesPerDomain // CertificatesPerDomainPerAccount is only used for per-account overrides to @@ -59,9 +68,11 @@ const ( // keys depending on the context: // - When referenced in an overrides file: uses bucket key 'enum:regId', // where regId is the ACME registration Id of the account. - // - When referenced in a transaction: uses bucket key 'enum:regId:domain', - // where regId is the ACME registration Id of the account and domain is a - // domain name in the certificate. + // - When referenced in a transaction: uses bucket key + // 'enum:regId:domainOrCIDR', where regId is the ACME registration Id of + // the account and domainOrCIDR is either a domain name in the + // certificate or an IP prefix in CIDR notation. + // - IP address formats vary by context, as for CertificatesPerDomain. // // When overrides to the CertificatesPerDomainPerAccount are configured for a // subscriber, the cost: @@ -70,13 +81,37 @@ const ( CertificatesPerDomainPerAccount // CertificatesPerFQDNSet uses bucket key 'enum:fqdnSet', where fqdnSet is a - // hashed set of unique eTLD+1 domain names in the certificate. + // hashed set of unique identifier values in the certificate. // // Note: When this is referenced in an overrides file, the fqdnSet MUST be - // passed as a comma-separated list of domain names. + // passed as a comma-separated list of identifier values. CertificatesPerFQDNSet + + // FailedAuthorizationsForPausingPerDomainPerAccount is similar to + // FailedAuthorizationsPerDomainPerAccount in that it uses two different + // bucket keys depending on the context: + // - When referenced in an overrides file: uses bucket key 'enum:regId', + // where regId is the ACME registration Id of the account. + // - When referenced in a transaction: uses bucket key + // 'enum:regId:identValue', where regId is the ACME registration Id of + // the account and identValue is the value of an identifier in the + // certificate. + FailedAuthorizationsForPausingPerDomainPerAccount ) +// nameToString is a map of Name values to string names. +var nameToString = map[Name]string{ + Unknown: "Unknown", + NewRegistrationsPerIPAddress: "NewRegistrationsPerIPAddress", + NewRegistrationsPerIPv6Range: "NewRegistrationsPerIPv6Range", + NewOrdersPerAccount: "NewOrdersPerAccount", + FailedAuthorizationsPerDomainPerAccount: "FailedAuthorizationsPerDomainPerAccount", + CertificatesPerDomain: "CertificatesPerDomain", + CertificatesPerDomainPerAccount: "CertificatesPerDomainPerAccount", + CertificatesPerFQDNSet: "CertificatesPerFQDNSet", + FailedAuthorizationsForPausingPerDomainPerAccount: "FailedAuthorizationsForPausingPerDomainPerAccount", +} + // isValid returns true if the Name is a valid rate limit name. func (n Name) isValid() bool { return n > Unknown && n < Name(len(nameToString)) @@ -99,43 +134,40 @@ func (n Name) EnumString() string { return strconv.Itoa(int(n)) } -// nameToString is a map of Name values to string names. -var nameToString = map[Name]string{ - Unknown: "Unknown", - NewRegistrationsPerIPAddress: "NewRegistrationsPerIPAddress", - NewRegistrationsPerIPv6Range: "NewRegistrationsPerIPv6Range", - NewOrdersPerAccount: "NewOrdersPerAccount", - FailedAuthorizationsPerDomainPerAccount: "FailedAuthorizationsPerDomainPerAccount", - CertificatesPerDomain: "CertificatesPerDomain", - CertificatesPerDomainPerAccount: "CertificatesPerDomainPerAccount", - CertificatesPerFQDNSet: "CertificatesPerFQDNSet", -} - // validIPAddress validates that the provided string is a valid IP address. func validIPAddress(id string) error { - ip := net.ParseIP(id) - if ip == nil { + ip, err := netip.ParseAddr(id) + if err != nil { return fmt.Errorf("invalid IP address, %q must be an IP address", id) } - return nil + canon := ip.String() + if canon != id { + return fmt.Errorf( + "invalid IP address, %q must be in canonical form (%q)", id, canon) + } + return iana.IsReservedAddr(ip) } -// validIPv6RangeCIDR validates that the provided string is formatted is an IPv6 -// CIDR range with a /48 mask. +// validIPv6RangeCIDR validates that the provided string is formatted as an IPv6 +// prefix in CIDR notation, with a /48 mask. func validIPv6RangeCIDR(id string) error { - _, ipNet, err := net.ParseCIDR(id) + prefix, err := netip.ParsePrefix(id) if err != nil { return fmt.Errorf( "invalid CIDR, %q must be an IPv6 CIDR range", id) } - ones, _ := ipNet.Mask.Size() - if ones != 48 { + if prefix.Bits() != 48 { // This also catches the case where the range is an IPv4 CIDR, since an // IPv4 CIDR can't have a /48 subnet mask - the maximum is /32. return fmt.Errorf( "invalid CIDR, %q must be /48", id) } - return nil + canon := prefix.Masked().String() + if canon != id { + return fmt.Errorf( + "invalid CIDR, %q must be in canonical form (%q)", id, canon) + } + return iana.IsReservedPrefix(prefix) } // validateRegId validates that the provided string is a valid ACME regId. @@ -147,47 +179,100 @@ func validateRegId(id string) error { return nil } -// validateDomain validates that the provided string is formatted 'domain', -// where domain is a domain name. -func validateDomain(id string) error { - err := policy.ValidDomain(id) +// validateRegIdIdentValue validates that the provided string is formatted +// 'regId:identValue', where regId is an ACME registration Id and identValue is +// a valid identifier value. +func validateRegIdIdentValue(id string) error { + regIdIdentValue := strings.Split(id, ":") + if len(regIdIdentValue) != 2 { + return fmt.Errorf( + "invalid regId:identValue, %q must be formatted 'regId:identValue'", id) + } + err := validateRegId(regIdIdentValue[0]) if err != nil { - return fmt.Errorf("invalid domain, %q must be formatted 'domain': %w", id, err) + return fmt.Errorf( + "invalid regId, %q must be formatted 'regId:identValue'", id) + } + domainErr := policy.ValidDomain(regIdIdentValue[1]) + if domainErr != nil { + ipErr := policy.ValidIP(regIdIdentValue[1]) + if ipErr != nil { + return fmt.Errorf("invalid identValue, %q must be formatted 'regId:identValue': %w as domain, %w as IP", id, domainErr, ipErr) + } } return nil } -// validateRegIdDomain validates that the provided string is formatted -// 'regId:domain', where regId is an ACME registration Id and domain is a domain -// name. -func validateRegIdDomain(id string) error { - regIdDomain := strings.Split(id, ":") - if len(regIdDomain) != 2 { - return fmt.Errorf( - "invalid regId:domain, %q must be formatted 'regId:domain'", id) +// validateDomainOrCIDR validates that the provided string is either a domain +// name or an IP address. IPv6 addresses must be the lowest address in their +// /64, i.e. their last 64 bits must be zero. +func validateDomainOrCIDR(id string) error { + domainErr := policy.ValidDomain(id) + if domainErr == nil { + // This is a valid domain. + return nil } - err := validateRegId(regIdDomain[0]) + + ip, ipErr := netip.ParseAddr(id) + if ipErr != nil { + return fmt.Errorf("%q is neither a domain (%w) nor an IP address (%w)", id, domainErr, ipErr) + } + + if ip.String() != id { + return fmt.Errorf("invalid IP address %q, must be in canonical form (%q)", id, ip.String()) + } + + prefix, prefixErr := coveringPrefix(ip) + if prefixErr != nil { + return fmt.Errorf("invalid IP address %q, couldn't determine prefix: %w", id, prefixErr) + } + if prefix.Addr() != ip { + return fmt.Errorf("invalid IP address %q, must be the lowest address in its prefix (%q)", id, prefix.Addr().String()) + } + + return iana.IsReservedPrefix(prefix) +} + +// validateRegIdDomainOrCIDR validates that the provided string is formatted +// 'regId:domainOrCIDR', where domainOrCIDR is either a domain name or an IP +// address. IPv6 addresses must be the lowest address in their /64, i.e. their +// last 64 bits must be zero. +func validateRegIdDomainOrCIDR(id string) error { + regIdDomainOrCIDR := strings.Split(id, ":") + if len(regIdDomainOrCIDR) != 2 { + return fmt.Errorf( + "invalid regId:domainOrCIDR, %q must be formatted 'regId:domainOrCIDR'", id) + } + err := validateRegId(regIdDomainOrCIDR[0]) if err != nil { return fmt.Errorf( - "invalid regId, %q must be formatted 'regId:domain'", id) + "invalid regId, %q must be formatted 'regId:domainOrCIDR'", id) } - err = policy.ValidDomain(regIdDomain[1]) + err = validateDomainOrCIDR(regIdDomainOrCIDR[1]) if err != nil { - return fmt.Errorf( - "invalid domain, %q must be formatted 'regId:domain': %w", id, err) + return fmt.Errorf("invalid domainOrCIDR, %q must be formatted 'regId:domainOrCIDR': %w", id, err) } return nil } // validateFQDNSet validates that the provided string is formatted 'fqdnSet', -// where fqdnSet is a comma-separated list of domain names. +// where fqdnSet is a comma-separated list of identifier values. func validateFQDNSet(id string) error { - domains := strings.Split(id, ",") - if len(domains) == 0 { + values := strings.Split(id, ",") + if len(values) == 0 { return fmt.Errorf( "invalid fqdnSet, %q must be formatted 'fqdnSet'", id) } - return policy.WellFormedDomainNames(domains) + for _, value := range values { + domainErr := policy.ValidDomain(value) + if domainErr != nil { + ipErr := policy.ValidIP(value) + if ipErr != nil { + return fmt.Errorf("invalid fqdnSet member %q: %w as domain, %w as IP", id, domainErr, ipErr) + } + } + } + return nil } func validateIdForName(name Name, id string) error { @@ -206,8 +291,8 @@ func validateIdForName(name Name, id string) error { case FailedAuthorizationsPerDomainPerAccount: if strings.Contains(id, ":") { - // 'enum:regId:domain' for transaction - return validateRegIdDomain(id) + // 'enum:regId:identValue' for transaction + return validateRegIdIdentValue(id) } else { // 'enum:regId' for overrides return validateRegId(id) @@ -215,21 +300,30 @@ func validateIdForName(name Name, id string) error { case CertificatesPerDomainPerAccount: if strings.Contains(id, ":") { - // 'enum:regId:domain' for transaction - return validateRegIdDomain(id) + // 'enum:regId:domainOrCIDR' for transaction + return validateRegIdDomainOrCIDR(id) } else { // 'enum:regId' for overrides return validateRegId(id) } case CertificatesPerDomain: - // 'enum:domain' - return validateDomain(id) + // 'enum:domainOrCIDR' + return validateDomainOrCIDR(id) case CertificatesPerFQDNSet: // 'enum:fqdnSet' return validateFQDNSet(id) + case FailedAuthorizationsForPausingPerDomainPerAccount: + if strings.Contains(id, ":") { + // 'enum:regId:identValue' for transaction + return validateRegIdIdentValue(id) + } else { + // 'enum:regId' for overrides + return validateRegId(id) + } + case Unknown: fallthrough @@ -250,7 +344,7 @@ var stringToName = func() map[string]Name { // limitNames is a slice of all rate limit names. var limitNames = func() []string { - names := make([]string, len(nameToString)) + names := make([]string, 0, len(nameToString)) for _, v := range nameToString { names = append(names, v) } diff --git a/third-party/github.com/letsencrypt/boulder/ratelimits/names_test.go b/third-party/github.com/letsencrypt/boulder/ratelimits/names_test.go index a12b069e2..93e710643 100644 --- a/third-party/github.com/letsencrypt/boulder/ratelimits/names_test.go +++ b/third-party/github.com/letsencrypt/boulder/ratelimits/names_test.go @@ -41,12 +41,24 @@ func TestValidateIdForName(t *testing.T) { { limit: NewRegistrationsPerIPAddress, desc: "valid IPv4 address", + id: "64.112.117.1", + }, + { + limit: NewRegistrationsPerIPAddress, + desc: "reserved IPv4 address", id: "10.0.0.1", + err: "in a reserved address block", }, { limit: NewRegistrationsPerIPAddress, desc: "valid IPv6 address", + id: "2602:80a:6000::42:42", + }, + { + limit: NewRegistrationsPerIPAddress, + desc: "IPv6 address in non-canonical form", id: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + err: "must be in canonical form", }, { limit: NewRegistrationsPerIPAddress, @@ -75,7 +87,19 @@ func TestValidateIdForName(t *testing.T) { { limit: NewRegistrationsPerIPv6Range, desc: "valid IPv6 address range", - id: "2001:0db8:0000::/48", + id: "2602:80a:6000::/48", + }, + { + limit: NewRegistrationsPerIPv6Range, + desc: "IPv6 address range in non-canonical form", + id: "2602:080a:6000::/48", + err: "must be in canonical form", + }, + { + limit: NewRegistrationsPerIPv6Range, + desc: "IPv6 address range with low bits set", + id: "2602:080a:6000::1/48", + err: "must be in canonical form", }, { limit: NewRegistrationsPerIPv6Range, @@ -95,6 +119,12 @@ func TestValidateIdForName(t *testing.T) { id: "10.0.0.0/16", err: "must be /48", }, + { + limit: NewRegistrationsPerIPv6Range, + desc: "IPv4 CIDR with invalid long mask", + id: "10.0.0.0/48", + err: "must be an IPv6 CIDR range", + }, { limit: NewOrdersPerAccount, desc: "valid regId", @@ -134,6 +164,34 @@ func TestValidateIdForName(t *testing.T) { id: "12ea5", err: "invalid regId", }, + { + limit: FailedAuthorizationsForPausingPerDomainPerAccount, + desc: "transaction: valid regId and domain", + id: "12345:example.com", + }, + { + limit: FailedAuthorizationsForPausingPerDomainPerAccount, + desc: "transaction: invalid regId", + id: "12ea5:example.com", + err: "invalid regId", + }, + { + limit: FailedAuthorizationsForPausingPerDomainPerAccount, + desc: "transaction: invalid domain", + id: "12345:examplecom", + err: "name needs at least one dot", + }, + { + limit: FailedAuthorizationsForPausingPerDomainPerAccount, + desc: "override: valid regId", + id: "12345", + }, + { + limit: FailedAuthorizationsForPausingPerDomainPerAccount, + desc: "override: invalid regId", + id: "12ea5", + err: "invalid regId", + }, { limit: CertificatesPerDomainPerAccount, desc: "transaction: valid regId and domain", @@ -167,6 +225,22 @@ func TestValidateIdForName(t *testing.T) { desc: "valid domain", id: "example.com", }, + { + limit: CertificatesPerDomain, + desc: "valid IPv4 address", + id: "64.112.117.1", + }, + { + limit: CertificatesPerDomain, + desc: "valid IPv6 address", + id: "2602:80a:6000::", + }, + { + limit: CertificatesPerDomain, + desc: "IPv6 address with subnet", + id: "2602:80a:6000::/64", + err: "nor an IP address", + }, { limit: CertificatesPerDomain, desc: "malformed domain", @@ -177,22 +251,36 @@ func TestValidateIdForName(t *testing.T) { limit: CertificatesPerDomain, desc: "empty domain", id: "", - err: "name is empty", + err: "Identifier value (name) is empty", }, { limit: CertificatesPerFQDNSet, desc: "valid fqdnSet containing a single domain", id: "example.com", }, + { + limit: CertificatesPerFQDNSet, + desc: "valid fqdnSet containing a single IPv4 address", + id: "64.112.117.1", + }, + { + limit: CertificatesPerFQDNSet, + desc: "valid fqdnSet containing a single IPv6 address", + id: "2602:80a:6000::1", + }, { limit: CertificatesPerFQDNSet, desc: "valid fqdnSet containing multiple domains", id: "example.com,example.org", }, + { + limit: CertificatesPerFQDNSet, + desc: "valid fqdnSet containing multiple domains and IPs", + id: "2602:80a:6000::1,64.112.117.1,example.com,example.org", + }, } for _, tc := range testCases { - tc := tc t.Run(fmt.Sprintf("%s/%s", tc.limit, tc.desc), func(t *testing.T) { t.Parallel() err := validateIdForName(tc.limit, tc.id) diff --git a/third-party/github.com/letsencrypt/boulder/ratelimits/source.go b/third-party/github.com/letsencrypt/boulder/ratelimits/source.go index 77f43b739..74f3ae6b2 100644 --- a/third-party/github.com/letsencrypt/boulder/ratelimits/source.go +++ b/third-party/github.com/letsencrypt/boulder/ratelimits/source.go @@ -10,8 +10,8 @@ import ( // ErrBucketNotFound indicates that the bucket was not found. var ErrBucketNotFound = fmt.Errorf("bucket not found") -// source is an interface for creating and modifying TATs. -type source interface { +// Source is an interface for creating and modifying TATs. +type Source interface { // BatchSet stores the TATs at the specified bucketKeys (formatted as // 'name:id'). Implementations MUST ensure non-blocking operations by // either: @@ -20,6 +20,18 @@ type source interface { // the underlying storage client implementation). BatchSet(ctx context.Context, bucketKeys map[string]time.Time) error + // BatchSetNotExisting attempts to set TATs for the specified bucketKeys if + // they do not already exist. Returns a map indicating which keys already + // exist. + BatchSetNotExisting(ctx context.Context, buckets map[string]time.Time) (map[string]bool, error) + + // BatchIncrement updates the TATs for the specified bucketKeys, similar to + // BatchSet. Implementations MUST ensure non-blocking operations by either: + // a) applying a deadline or timeout to the context WITHIN the method, or + // b) guaranteeing the operation will not block indefinitely (e.g. via + // the underlying storage client implementation). + BatchIncrement(ctx context.Context, buckets map[string]increment) error + // Get retrieves the TAT associated with the specified bucketKey (formatted // as 'name:id'). Implementations MUST ensure non-blocking operations by // either: @@ -45,6 +57,11 @@ type source interface { Delete(ctx context.Context, bucketKey string) error } +type increment struct { + cost time.Duration + ttl time.Duration +} + // inmem is an in-memory implementation of the source interface used for // testing. type inmem struct { @@ -52,7 +69,9 @@ type inmem struct { m map[string]time.Time } -func newInmem() *inmem { +var _ Source = (*inmem)(nil) + +func NewInmemSource() *inmem { return &inmem{m: make(map[string]time.Time)} } @@ -65,6 +84,30 @@ func (in *inmem) BatchSet(_ context.Context, bucketKeys map[string]time.Time) er return nil } +func (in *inmem) BatchSetNotExisting(_ context.Context, bucketKeys map[string]time.Time) (map[string]bool, error) { + in.Lock() + defer in.Unlock() + alreadyExists := make(map[string]bool, len(bucketKeys)) + for k, v := range bucketKeys { + _, ok := in.m[k] + if ok { + alreadyExists[k] = true + } else { + in.m[k] = v + } + } + return alreadyExists, nil +} + +func (in *inmem) BatchIncrement(_ context.Context, bucketKeys map[string]increment) error { + in.Lock() + defer in.Unlock() + for k, v := range bucketKeys { + in.m[k] = in.m[k].Add(v.cost) + } + return nil +} + func (in *inmem) Get(_ context.Context, bucketKey string) (time.Time, error) { in.RLock() defer in.RUnlock() @@ -82,7 +125,7 @@ func (in *inmem) BatchGet(_ context.Context, bucketKeys []string) (map[string]ti for _, k := range bucketKeys { tat, ok := in.m[k] if !ok { - tats[k] = time.Time{} + continue } tats[k] = tat } diff --git a/third-party/github.com/letsencrypt/boulder/ratelimits/source_redis.go b/third-party/github.com/letsencrypt/boulder/ratelimits/source_redis.go index 2c807c9d4..ff32931ef 100644 --- a/third-party/github.com/letsencrypt/boulder/ratelimits/source_redis.go +++ b/third-party/github.com/letsencrypt/boulder/ratelimits/source_redis.go @@ -12,7 +12,7 @@ import ( ) // Compile-time check that RedisSource implements the source interface. -var _ source = (*RedisSource)(nil) +var _ Source = (*RedisSource)(nil) // RedisSource is a ratelimits source backed by sharded Redis. type RedisSource struct { @@ -42,10 +42,15 @@ func NewRedisSource(client *redis.Ring, clk clock.Clock, stats prometheus.Regist } } +var errMixedSuccess = errors.New("some keys not found") + // resultForError returns a string representing the result of the operation // based on the provided error. func resultForError(err error) string { - if errors.Is(redis.Nil, err) { + if errors.Is(errMixedSuccess, err) { + // Indicates that some of the keys in a batchset operation were not found. + return "mixedSuccess" + } else if errors.Is(redis.Nil, err) { // Bucket key does not exist. return "notFound" } else if errors.Is(err, context.DeadlineExceeded) { @@ -68,29 +73,95 @@ func resultForError(err error) string { return "failed" } +func (r *RedisSource) observeLatency(call string, latency time.Duration, err error) { + result := "success" + if err != nil { + result = resultForError(err) + } + r.latency.With(prometheus.Labels{"call": call, "result": result}).Observe(latency.Seconds()) +} + // BatchSet stores TATs at the specified bucketKeys using a pipelined Redis // Transaction in order to reduce the number of round-trips to each Redis shard. -// An error is returned if the operation failed and nil otherwise. func (r *RedisSource) BatchSet(ctx context.Context, buckets map[string]time.Time) error { start := r.clk.Now() pipeline := r.client.Pipeline() for bucketKey, tat := range buckets { - pipeline.Set(ctx, bucketKey, tat.UTC().UnixNano(), 0) + // Set a TTL of TAT + 10 minutes to account for clock skew. + ttl := tat.UTC().Sub(r.clk.Now()) + 10*time.Minute + pipeline.Set(ctx, bucketKey, tat.UTC().UnixNano(), ttl) } _, err := pipeline.Exec(ctx) if err != nil { - r.latency.With(prometheus.Labels{"call": "batchset", "result": resultForError(err)}).Observe(time.Since(start).Seconds()) + r.observeLatency("batchset", r.clk.Since(start), err) return err } - r.latency.With(prometheus.Labels{"call": "batchset", "result": "success"}).Observe(time.Since(start).Seconds()) + totalLatency := r.clk.Since(start) + + r.observeLatency("batchset", totalLatency, nil) return nil } -// Get retrieves the TAT at the specified bucketKey. An error is returned if the -// operation failed and nil otherwise. If the bucketKey does not exist, -// ErrBucketNotFound is returned. +// BatchSetNotExisting attempts to set TATs for the specified bucketKeys if they +// do not already exist. Returns a map indicating which keys already existed. +func (r *RedisSource) BatchSetNotExisting(ctx context.Context, buckets map[string]time.Time) (map[string]bool, error) { + start := r.clk.Now() + + pipeline := r.client.Pipeline() + cmds := make(map[string]*redis.BoolCmd, len(buckets)) + for bucketKey, tat := range buckets { + // Set a TTL of TAT + 10 minutes to account for clock skew. + ttl := tat.UTC().Sub(r.clk.Now()) + 10*time.Minute + cmds[bucketKey] = pipeline.SetNX(ctx, bucketKey, tat.UTC().UnixNano(), ttl) + } + _, err := pipeline.Exec(ctx) + if err != nil { + r.observeLatency("batchsetnotexisting", r.clk.Since(start), err) + return nil, err + } + + alreadyExists := make(map[string]bool, len(buckets)) + totalLatency := r.clk.Since(start) + for bucketKey, cmd := range cmds { + success, err := cmd.Result() + if err != nil { + return nil, err + } + if !success { + alreadyExists[bucketKey] = true + } + } + + r.observeLatency("batchsetnotexisting", totalLatency, nil) + return alreadyExists, nil +} + +// BatchIncrement updates TATs for the specified bucketKeys using a pipelined +// Redis Transaction in order to reduce the number of round-trips to each Redis +// shard. +func (r *RedisSource) BatchIncrement(ctx context.Context, buckets map[string]increment) error { + start := r.clk.Now() + + pipeline := r.client.Pipeline() + for bucketKey, incr := range buckets { + pipeline.IncrBy(ctx, bucketKey, incr.cost.Nanoseconds()) + pipeline.Expire(ctx, bucketKey, incr.ttl) + } + _, err := pipeline.Exec(ctx) + if err != nil { + r.observeLatency("batchincrby", r.clk.Since(start), err) + return err + } + + totalLatency := r.clk.Since(start) + r.observeLatency("batchincrby", totalLatency, nil) + return nil +} + +// Get retrieves the TAT at the specified bucketKey. If the bucketKey does not +// exist, ErrBucketNotFound is returned. func (r *RedisSource) Get(ctx context.Context, bucketKey string) (time.Time, error) { start := r.clk.Now() @@ -98,21 +169,22 @@ func (r *RedisSource) Get(ctx context.Context, bucketKey string) (time.Time, err if err != nil { if errors.Is(err, redis.Nil) { // Bucket key does not exist. - r.latency.With(prometheus.Labels{"call": "get", "result": "notFound"}).Observe(time.Since(start).Seconds()) + r.observeLatency("get", r.clk.Since(start), err) return time.Time{}, ErrBucketNotFound } - r.latency.With(prometheus.Labels{"call": "get", "result": resultForError(err)}).Observe(time.Since(start).Seconds()) + // An error occurred while retrieving the TAT. + r.observeLatency("get", r.clk.Since(start), err) return time.Time{}, err } - r.latency.With(prometheus.Labels{"call": "get", "result": "success"}).Observe(time.Since(start).Seconds()) + r.observeLatency("get", r.clk.Since(start), nil) return time.Unix(0, tatNano).UTC(), nil } // BatchGet retrieves the TATs at the specified bucketKeys using a pipelined // Redis Transaction in order to reduce the number of round-trips to each Redis -// shard. An error is returned if the operation failed and nil otherwise. If a -// bucketKey does not exist, it WILL NOT be included in the returned map. +// shard. If a bucketKey does not exist, it WILL NOT be included in the returned +// map. func (r *RedisSource) BatchGet(ctx context.Context, bucketKeys []string) (map[string]time.Time, error) { start := r.clk.Now() @@ -121,49 +193,60 @@ func (r *RedisSource) BatchGet(ctx context.Context, bucketKeys []string) (map[st pipeline.Get(ctx, bucketKey) } results, err := pipeline.Exec(ctx) - if err != nil { - r.latency.With(prometheus.Labels{"call": "batchget", "result": resultForError(err)}).Observe(time.Since(start).Seconds()) - if !errors.Is(err, redis.Nil) { - return nil, err - } + if err != nil && !errors.Is(err, redis.Nil) { + r.observeLatency("batchget", r.clk.Since(start), err) + return nil, err } + totalLatency := r.clk.Since(start) + tats := make(map[string]time.Time, len(bucketKeys)) + notFoundCount := 0 for i, result := range results { tatNano, err := result.(*redis.StringCmd).Int64() if err != nil { - if errors.Is(err, redis.Nil) { - // Bucket key does not exist. - continue + if !errors.Is(err, redis.Nil) { + // This should never happen as any errors should have been + // caught after the pipeline.Exec() call. + r.observeLatency("batchget", r.clk.Since(start), err) + return nil, err } - r.latency.With(prometheus.Labels{"call": "batchget", "result": resultForError(err)}).Observe(time.Since(start).Seconds()) - return nil, err + notFoundCount++ + continue } tats[bucketKeys[i]] = time.Unix(0, tatNano).UTC() } - r.latency.With(prometheus.Labels{"call": "batchget", "result": "success"}).Observe(time.Since(start).Seconds()) + var batchErr error + if notFoundCount < len(results) { + // Some keys were not found. + batchErr = errMixedSuccess + } else if notFoundCount == len(results) { + // All keys were not found. + batchErr = redis.Nil + } + + r.observeLatency("batchget", totalLatency, batchErr) return tats, nil } -// Delete deletes the TAT at the specified bucketKey ('name:id'). It returns an -// error if the operation failed and nil otherwise. A nil return value does not -// indicate that the bucketKey existed. +// Delete deletes the TAT at the specified bucketKey ('name:id'). A nil return +// value does not indicate that the bucketKey existed. func (r *RedisSource) Delete(ctx context.Context, bucketKey string) error { start := r.clk.Now() err := r.client.Del(ctx, bucketKey).Err() if err != nil { - r.latency.With(prometheus.Labels{"call": "delete", "result": resultForError(err)}).Observe(time.Since(start).Seconds()) + r.observeLatency("delete", r.clk.Since(start), err) return err } - r.latency.With(prometheus.Labels{"call": "delete", "result": "success"}).Observe(time.Since(start).Seconds()) + r.observeLatency("delete", r.clk.Since(start), nil) return nil } // Ping checks that each shard of the *redis.Ring is reachable using the PING -// command. It returns an error if any shard is unreachable and nil otherwise. +// command. func (r *RedisSource) Ping(ctx context.Context) error { start := r.clk.Now() @@ -171,9 +254,10 @@ func (r *RedisSource) Ping(ctx context.Context) error { return shard.Ping(ctx).Err() }) if err != nil { - r.latency.With(prometheus.Labels{"call": "ping", "result": resultForError(err)}).Observe(time.Since(start).Seconds()) + r.observeLatency("ping", r.clk.Since(start), err) return err } - r.latency.With(prometheus.Labels{"call": "ping", "result": "success"}).Observe(time.Since(start).Seconds()) + + r.observeLatency("ping", r.clk.Since(start), nil) return nil } diff --git a/third-party/github.com/letsencrypt/boulder/ratelimits/source_redis_test.go b/third-party/github.com/letsencrypt/boulder/ratelimits/source_redis_test.go index 11ed27158..3763dcf99 100644 --- a/third-party/github.com/letsencrypt/boulder/ratelimits/source_redis_test.go +++ b/third-party/github.com/letsencrypt/boulder/ratelimits/source_redis_test.go @@ -38,32 +38,32 @@ func newTestRedisSource(clk clock.FakeClock, addrs map[string]string) *RedisSour func newRedisTestLimiter(t *testing.T, clk clock.FakeClock) *Limiter { return newTestLimiter(t, newTestRedisSource(clk, map[string]string{ - "shard1": "10.33.33.4:4218", - "shard2": "10.33.33.5:4218", + "shard1": "10.77.77.4:4218", + "shard2": "10.77.77.5:4218", }), clk) } func TestRedisSource_Ping(t *testing.T) { clk := clock.NewFake() workingSource := newTestRedisSource(clk, map[string]string{ - "shard1": "10.33.33.4:4218", - "shard2": "10.33.33.5:4218", + "shard1": "10.77.77.4:4218", + "shard2": "10.77.77.5:4218", }) err := workingSource.Ping(context.Background()) test.AssertNotError(t, err, "Ping should not error") missingFirstShardSource := newTestRedisSource(clk, map[string]string{ - "shard1": "10.33.33.4:1337", - "shard2": "10.33.33.5:4218", + "shard1": "10.77.77.4:1337", + "shard2": "10.77.77.5:4218", }) err = missingFirstShardSource.Ping(context.Background()) test.AssertError(t, err, "Ping should not error") missingSecondShardSource := newTestRedisSource(clk, map[string]string{ - "shard1": "10.33.33.4:4218", - "shard2": "10.33.33.5:1337", + "shard1": "10.77.77.4:4218", + "shard2": "10.77.77.5:1337", }) err = missingSecondShardSource.Ping(context.Background()) @@ -73,19 +73,20 @@ func TestRedisSource_Ping(t *testing.T) { func TestRedisSource_BatchSetAndGet(t *testing.T) { clk := clock.NewFake() s := newTestRedisSource(clk, map[string]string{ - "shard1": "10.33.33.4:4218", - "shard2": "10.33.33.5:4218", + "shard1": "10.77.77.4:4218", + "shard2": "10.77.77.5:4218", }) - now := clk.Now() - val1 := now.Add(time.Second) - val2 := now.Add(time.Second * 2) - val3 := now.Add(time.Second * 3) - set := map[string]time.Time{ - "test1": val1, - "test2": val2, - "test3": val3, + "test1": clk.Now().Add(time.Second), + "test2": clk.Now().Add(time.Second * 2), + "test3": clk.Now().Add(time.Second * 3), + } + + incr := map[string]increment{ + "test1": {time.Second, time.Minute}, + "test2": {time.Second * 2, time.Minute}, + "test3": {time.Second * 3, time.Minute}, } err := s.BatchSet(context.Background(), set) @@ -95,7 +96,17 @@ func TestRedisSource_BatchSetAndGet(t *testing.T) { test.AssertNotError(t, err, "BatchGet() should not error") for k, v := range set { - test.Assert(t, got[k].Equal(v), "BatchGet() should return the values set by BatchSet()") + test.AssertEquals(t, got[k], v) + } + + err = s.BatchIncrement(context.Background(), incr) + test.AssertNotError(t, err, "BatchIncrement() should not error") + + got, err = s.BatchGet(context.Background(), []string{"test1", "test2", "test3"}) + test.AssertNotError(t, err, "BatchGet() should not error") + + for k := range set { + test.AssertEquals(t, got[k], set[k].Add(incr[k].cost)) } // Test that BatchGet() returns a zero time for a key that does not exist. diff --git a/third-party/github.com/letsencrypt/boulder/ratelimits/source_test.go b/third-party/github.com/letsencrypt/boulder/ratelimits/source_test.go index a4f55ba87..a2347c8bc 100644 --- a/third-party/github.com/letsencrypt/boulder/ratelimits/source_test.go +++ b/third-party/github.com/letsencrypt/boulder/ratelimits/source_test.go @@ -7,5 +7,5 @@ import ( ) func newInmemTestLimiter(t *testing.T, clk clock.FakeClock) *Limiter { - return newTestLimiter(t, newInmem(), clk) + return newTestLimiter(t, NewInmemSource(), clk) } diff --git a/third-party/github.com/letsencrypt/boulder/ratelimits/testdata/working_override.yml b/third-party/github.com/letsencrypt/boulder/ratelimits/testdata/working_override.yml index bd5dc80fd..447658d9a 100644 --- a/third-party/github.com/letsencrypt/boulder/ratelimits/testdata/working_override.yml +++ b/third-party/github.com/letsencrypt/boulder/ratelimits/testdata/working_override.yml @@ -3,5 +3,5 @@ count: 40 period: 1s ids: - - id: 10.0.0.2 + - id: 64.112.117.1 comment: Foo diff --git a/third-party/github.com/letsencrypt/boulder/ratelimits/testdata/working_override_13371338.yml b/third-party/github.com/letsencrypt/boulder/ratelimits/testdata/working_override_13371338.yml new file mode 100644 index 000000000..97327e510 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/ratelimits/testdata/working_override_13371338.yml @@ -0,0 +1,21 @@ +- CertificatesPerDomainPerAccount: + burst: 1337 + count: 1337 + period: 2160h + ids: + - id: 13371338 + comment: Used to test the TransactionBuilder +- FailedAuthorizationsPerDomainPerAccount: + burst: 1337 + count: 1337 + period: 5m + ids: + - id: 13371338 + comment: Used to test the TransactionBuilder +- FailedAuthorizationsForPausingPerDomainPerAccount: + burst: 1337 + count: 1 + period: 24h + ids: + - id: 13371338 + comment: Used to test the TransactionBuilder diff --git a/third-party/github.com/letsencrypt/boulder/ratelimits/testdata/working_override_regid_domain.yml b/third-party/github.com/letsencrypt/boulder/ratelimits/testdata/working_override_regid_domainorcidr.yml similarity index 100% rename from third-party/github.com/letsencrypt/boulder/ratelimits/testdata/working_override_regid_domain.yml rename to third-party/github.com/letsencrypt/boulder/ratelimits/testdata/working_override_regid_domainorcidr.yml diff --git a/third-party/github.com/letsencrypt/boulder/ratelimits/testdata/working_overrides.yml b/third-party/github.com/letsencrypt/boulder/ratelimits/testdata/working_overrides.yml index 584676e87..be1479f12 100644 --- a/third-party/github.com/letsencrypt/boulder/ratelimits/testdata/working_overrides.yml +++ b/third-party/github.com/letsencrypt/boulder/ratelimits/testdata/working_overrides.yml @@ -3,14 +3,14 @@ count: 40 period: 1s ids: - - id: 10.0.0.2 + - id: 64.112.117.1 comment: Foo - NewRegistrationsPerIPv6Range: burst: 50 count: 50 period: 2s ids: - - id: 2001:0db8:0000::/48 + - id: 2602:80a:6000::/48 comment: Foo - FailedAuthorizationsPerDomainPerAccount: burst: 60 @@ -22,3 +22,12 @@ - id: 5678 comment: Foo +- FailedAuthorizationsForPausingPerDomainPerAccount: + burst: 60 + count: 60 + period: 3s + ids: + - id: 1234 + comment: Foo + - id: 5678 + comment: Foo diff --git a/third-party/github.com/letsencrypt/boulder/ratelimits/testdata/working_overrides_regid_fqdnset.yml b/third-party/github.com/letsencrypt/boulder/ratelimits/testdata/working_overrides_regid_fqdnset.yml index 60e337fb1..ef98663fb 100644 --- a/third-party/github.com/letsencrypt/boulder/ratelimits/testdata/working_overrides_regid_fqdnset.yml +++ b/third-party/github.com/letsencrypt/boulder/ratelimits/testdata/working_overrides_regid_fqdnset.yml @@ -19,3 +19,10 @@ ids: - id: "example.com,example.net,example.org" comment: Foo +- CertificatesPerFQDNSet: + burst: 60 + count: 60 + period: 4s + ids: + - id: "2602:80a:6000::1,9.9.9.9,example.com" + comment: Foo diff --git a/third-party/github.com/letsencrypt/boulder/ratelimits/transaction.go b/third-party/github.com/letsencrypt/boulder/ratelimits/transaction.go new file mode 100644 index 000000000..adbed90c7 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/ratelimits/transaction.go @@ -0,0 +1,579 @@ +package ratelimits + +import ( + "errors" + "fmt" + "net/netip" + "strconv" + + "github.com/letsencrypt/boulder/core" + "github.com/letsencrypt/boulder/identifier" +) + +// ErrInvalidCost indicates that the cost specified was < 0. +var ErrInvalidCost = fmt.Errorf("invalid cost, must be >= 0") + +// ErrInvalidCostOverLimit indicates that the cost specified was > limit.Burst. +var ErrInvalidCostOverLimit = fmt.Errorf("invalid cost, must be <= limit.Burst") + +// newIPAddressBucketKey validates and returns a bucketKey for limits that use +// the 'enum:ipAddress' bucket key format. +func newIPAddressBucketKey(name Name, ip netip.Addr) string { //nolint:unparam // Only one named rate limit uses this helper + return joinWithColon(name.EnumString(), ip.String()) +} + +// newIPv6RangeCIDRBucketKey validates and returns a bucketKey for limits that +// use the 'enum:ipv6RangeCIDR' bucket key format. +func newIPv6RangeCIDRBucketKey(name Name, ip netip.Addr) (string, error) { + if ip.Is4() { + return "", fmt.Errorf("invalid IPv6 address, %q must be an IPv6 address", ip.String()) + } + prefix, err := ip.Prefix(48) + if err != nil { + return "", fmt.Errorf("invalid IPv6 address, can't calculate prefix of %q: %s", ip.String(), err) + } + return joinWithColon(name.EnumString(), prefix.String()), nil +} + +// newRegIdBucketKey validates and returns a bucketKey for limits that use the +// 'enum:regId' bucket key format. +func newRegIdBucketKey(name Name, regId int64) string { + return joinWithColon(name.EnumString(), strconv.FormatInt(regId, 10)) +} + +// newDomainOrCIDRBucketKey validates and returns a bucketKey for limits that use +// the 'enum:domainOrCIDR' bucket key formats. +func newDomainOrCIDRBucketKey(name Name, domainOrCIDR string) string { + return joinWithColon(name.EnumString(), domainOrCIDR) +} + +// NewRegIdIdentValueBucketKey returns a bucketKey for limits that use the +// 'enum:regId:identValue' bucket key format. This function is exported for use +// by the RA when resetting the account pausing limit. +func NewRegIdIdentValueBucketKey(name Name, regId int64, orderIdent string) string { + return joinWithColon(name.EnumString(), strconv.FormatInt(regId, 10), orderIdent) +} + +// newFQDNSetBucketKey validates and returns a bucketKey for limits that use the +// 'enum:fqdnSet' bucket key format. +func newFQDNSetBucketKey(name Name, orderIdents identifier.ACMEIdentifiers) string { //nolint: unparam // Only one named rate limit uses this helper + return joinWithColon(name.EnumString(), fmt.Sprintf("%x", core.HashIdentifiers(orderIdents))) +} + +// Transaction represents a single rate limit operation. It includes a +// bucketKey, which combines the specific rate limit enum with a unique +// identifier to form the key where the state of the "bucket" can be referenced +// or stored by the Limiter, the rate limit being enforced, a cost which MUST be +// >= 0, and check/spend fields, which indicate how the Transaction should be +// processed. The following are acceptable combinations of check/spend: +// - check-and-spend: when check and spend are both true, the cost will be +// checked against the bucket's capacity and spent/refunded, when possible. +// - check-only: when only check is true, the cost will be checked against the +// bucket's capacity, but will never be spent/refunded. +// - spend-only: when only spend is true, spending is best-effort. Regardless +// of the bucket's capacity, the transaction will be considered "allowed". +// - allow-only: when neither check nor spend are true, the transaction will +// be considered "allowed" regardless of the bucket's capacity. This is +// useful for limits that are disabled. +// +// The zero value of Transaction is an allow-only transaction and is valid even if +// it would fail validateTransaction (for instance because cost and burst are zero). +type Transaction struct { + bucketKey string + limit *limit + cost int64 + check bool + spend bool +} + +func (txn Transaction) checkOnly() bool { + return txn.check && !txn.spend +} + +func (txn Transaction) spendOnly() bool { + return txn.spend && !txn.check +} + +func (txn Transaction) allowOnly() bool { + return !txn.check && !txn.spend +} + +func validateTransaction(txn Transaction) (Transaction, error) { + if txn.cost < 0 { + return Transaction{}, ErrInvalidCost + } + if txn.limit.burst == 0 { + // This should never happen. If the limit was loaded from a file, + // Burst was validated then. If this is a zero-valued Transaction + // (that is, an allow-only transaction), then validateTransaction + // shouldn't be called because zero-valued transactions are automatically + // valid. + return Transaction{}, fmt.Errorf("invalid limit, burst must be > 0") + } + if txn.cost > txn.limit.burst { + return Transaction{}, ErrInvalidCostOverLimit + } + return txn, nil +} + +func newTransaction(limit *limit, bucketKey string, cost int64) (Transaction, error) { + return validateTransaction(Transaction{ + bucketKey: bucketKey, + limit: limit, + cost: cost, + check: true, + spend: true, + }) +} + +func newCheckOnlyTransaction(limit *limit, bucketKey string, cost int64) (Transaction, error) { + return validateTransaction(Transaction{ + bucketKey: bucketKey, + limit: limit, + cost: cost, + check: true, + }) +} + +func newSpendOnlyTransaction(limit *limit, bucketKey string, cost int64) (Transaction, error) { + return validateTransaction(Transaction{ + bucketKey: bucketKey, + limit: limit, + cost: cost, + spend: true, + }) +} + +func newAllowOnlyTransaction() Transaction { + // Zero values are sufficient. + return Transaction{} +} + +// TransactionBuilder is used to build Transactions for various rate limits. +// Each rate limit has a corresponding method that returns a Transaction for +// that limit. Call NewTransactionBuilder to create a new *TransactionBuilder. +type TransactionBuilder struct { + *limitRegistry +} + +// NewTransactionBuilderFromFiles returns a new *TransactionBuilder. The +// provided defaults and overrides paths are expected to be paths to YAML files +// that contain the default and override limits, respectively. Overrides is +// optional, defaults is required. +func NewTransactionBuilderFromFiles(defaults, overrides string) (*TransactionBuilder, error) { + registry, err := newLimitRegistryFromFiles(defaults, overrides) + if err != nil { + return nil, err + } + return &TransactionBuilder{registry}, nil +} + +// NewTransactionBuilder returns a new *TransactionBuilder. The provided +// defaults map is expected to contain default limit data. Overrides are not +// supported. Defaults is required. +func NewTransactionBuilder(defaults LimitConfigs) (*TransactionBuilder, error) { + registry, err := newLimitRegistry(defaults, nil) + if err != nil { + return nil, err + } + return &TransactionBuilder{registry}, nil +} + +// registrationsPerIPAddressTransaction returns a Transaction for the +// NewRegistrationsPerIPAddress limit for the provided IP address. +func (builder *TransactionBuilder) registrationsPerIPAddressTransaction(ip netip.Addr) (Transaction, error) { + bucketKey := newIPAddressBucketKey(NewRegistrationsPerIPAddress, ip) + limit, err := builder.getLimit(NewRegistrationsPerIPAddress, bucketKey) + if err != nil { + if errors.Is(err, errLimitDisabled) { + return newAllowOnlyTransaction(), nil + } + return Transaction{}, err + } + return newTransaction(limit, bucketKey, 1) +} + +// registrationsPerIPv6RangeTransaction returns a Transaction for the +// NewRegistrationsPerIPv6Range limit for the /48 IPv6 range which contains the +// provided IPv6 address. +func (builder *TransactionBuilder) registrationsPerIPv6RangeTransaction(ip netip.Addr) (Transaction, error) { + bucketKey, err := newIPv6RangeCIDRBucketKey(NewRegistrationsPerIPv6Range, ip) + if err != nil { + return Transaction{}, err + } + limit, err := builder.getLimit(NewRegistrationsPerIPv6Range, bucketKey) + if err != nil { + if errors.Is(err, errLimitDisabled) { + return newAllowOnlyTransaction(), nil + } + return Transaction{}, err + } + return newTransaction(limit, bucketKey, 1) +} + +// ordersPerAccountTransaction returns a Transaction for the NewOrdersPerAccount +// limit for the provided ACME registration Id. +func (builder *TransactionBuilder) ordersPerAccountTransaction(regId int64) (Transaction, error) { + bucketKey := newRegIdBucketKey(NewOrdersPerAccount, regId) + limit, err := builder.getLimit(NewOrdersPerAccount, bucketKey) + if err != nil { + if errors.Is(err, errLimitDisabled) { + return newAllowOnlyTransaction(), nil + } + return Transaction{}, err + } + return newTransaction(limit, bucketKey, 1) +} + +// FailedAuthorizationsPerDomainPerAccountCheckOnlyTransactions returns a slice +// of Transactions for the provided order identifiers. An error is returned if +// any of the order identifiers' values are invalid. This method should be used +// for checking capacity, before allowing more authorizations to be created. +// +// Precondition: len(orderIdents) < maxNames. +func (builder *TransactionBuilder) FailedAuthorizationsPerDomainPerAccountCheckOnlyTransactions(regId int64, orderIdents identifier.ACMEIdentifiers) ([]Transaction, error) { + // FailedAuthorizationsPerDomainPerAccount limit uses the 'enum:regId' + // bucket key format for overrides. + perAccountBucketKey := newRegIdBucketKey(FailedAuthorizationsPerDomainPerAccount, regId) + limit, err := builder.getLimit(FailedAuthorizationsPerDomainPerAccount, perAccountBucketKey) + if err != nil { + if errors.Is(err, errLimitDisabled) { + return []Transaction{newAllowOnlyTransaction()}, nil + } + return nil, err + } + + var txns []Transaction + for _, ident := range orderIdents { + // FailedAuthorizationsPerDomainPerAccount limit uses the + // 'enum:regId:identValue' bucket key format for transactions. + perIdentValuePerAccountBucketKey := NewRegIdIdentValueBucketKey(FailedAuthorizationsPerDomainPerAccount, regId, ident.Value) + + // Add a check-only transaction for each per identValue per account + // bucket. + txn, err := newCheckOnlyTransaction(limit, perIdentValuePerAccountBucketKey, 1) + if err != nil { + return nil, err + } + txns = append(txns, txn) + } + return txns, nil +} + +// FailedAuthorizationsPerDomainPerAccountSpendOnlyTransaction returns a spend- +// only Transaction for the provided order identifier. An error is returned if +// the order identifier's value is invalid. This method should be used for +// spending capacity, as a result of a failed authorization. +func (builder *TransactionBuilder) FailedAuthorizationsPerDomainPerAccountSpendOnlyTransaction(regId int64, orderIdent identifier.ACMEIdentifier) (Transaction, error) { + // FailedAuthorizationsPerDomainPerAccount limit uses the 'enum:regId' + // bucket key format for overrides. + perAccountBucketKey := newRegIdBucketKey(FailedAuthorizationsPerDomainPerAccount, regId) + limit, err := builder.getLimit(FailedAuthorizationsPerDomainPerAccount, perAccountBucketKey) + if err != nil { + if errors.Is(err, errLimitDisabled) { + return newAllowOnlyTransaction(), nil + } + return Transaction{}, err + } + + // FailedAuthorizationsPerDomainPerAccount limit uses the + // 'enum:regId:identValue' bucket key format for transactions. + perIdentValuePerAccountBucketKey := NewRegIdIdentValueBucketKey(FailedAuthorizationsPerDomainPerAccount, regId, orderIdent.Value) + txn, err := newSpendOnlyTransaction(limit, perIdentValuePerAccountBucketKey, 1) + if err != nil { + return Transaction{}, err + } + + return txn, nil +} + +// FailedAuthorizationsForPausingPerDomainPerAccountTransaction returns a +// Transaction for the provided order identifier. An error is returned if the +// order identifier's value is invalid. This method should be used for spending +// capacity, as a result of a failed authorization. +func (builder *TransactionBuilder) FailedAuthorizationsForPausingPerDomainPerAccountTransaction(regId int64, orderIdent identifier.ACMEIdentifier) (Transaction, error) { + // FailedAuthorizationsForPausingPerDomainPerAccount limit uses the 'enum:regId' + // bucket key format for overrides. + perAccountBucketKey := newRegIdBucketKey(FailedAuthorizationsForPausingPerDomainPerAccount, regId) + limit, err := builder.getLimit(FailedAuthorizationsForPausingPerDomainPerAccount, perAccountBucketKey) + if err != nil { + if errors.Is(err, errLimitDisabled) { + return newAllowOnlyTransaction(), nil + } + return Transaction{}, err + } + + // FailedAuthorizationsForPausingPerDomainPerAccount limit uses the + // 'enum:regId:identValue' bucket key format for transactions. + perIdentValuePerAccountBucketKey := NewRegIdIdentValueBucketKey(FailedAuthorizationsForPausingPerDomainPerAccount, regId, orderIdent.Value) + txn, err := newTransaction(limit, perIdentValuePerAccountBucketKey, 1) + if err != nil { + return Transaction{}, err + } + + return txn, nil +} + +// certificatesPerDomainCheckOnlyTransactions returns a slice of Transactions +// for the provided order identifiers. It returns an error if any of the order +// identifiers' values are invalid. This method should be used for checking +// capacity, before allowing more orders to be created. If a +// CertificatesPerDomainPerAccount override is active, a check-only Transaction +// is created for each per account per domainOrCIDR bucket. Otherwise, a +// check-only Transaction is generated for each global per domainOrCIDR bucket. +// This method should be used for checking capacity, before allowing more orders +// to be created. +// +// Precondition: All orderIdents must comply with policy.WellFormedIdentifiers. +func (builder *TransactionBuilder) certificatesPerDomainCheckOnlyTransactions(regId int64, orderIdents identifier.ACMEIdentifiers) ([]Transaction, error) { + if len(orderIdents) > 100 { + return nil, fmt.Errorf("unwilling to process more than 100 rate limit transactions, got %d", len(orderIdents)) + } + + perAccountLimitBucketKey := newRegIdBucketKey(CertificatesPerDomainPerAccount, regId) + accountOverride := true + perAccountLimit, err := builder.getLimit(CertificatesPerDomainPerAccount, perAccountLimitBucketKey) + if err != nil { + // The CertificatesPerDomainPerAccount limit never has a default. If there is an override for it, + // the above call will return the override. But if there is none, it will return errLimitDisabled. + // In that case we want to continue, but make sure we don't reference `perAccountLimit` because it + // is not a valid limit. + if errors.Is(err, errLimitDisabled) { + accountOverride = false + } else { + return nil, err + } + } + + coveringIdents, err := coveringIdentifiers(orderIdents) + if err != nil { + return nil, err + } + + var txns []Transaction + for _, ident := range coveringIdents { + perDomainOrCIDRBucketKey := newDomainOrCIDRBucketKey(CertificatesPerDomain, ident) + if accountOverride { + if !perAccountLimit.isOverride { + return nil, fmt.Errorf("shouldn't happen: CertificatesPerDomainPerAccount limit is not an override") + } + perAccountPerDomainOrCIDRBucketKey := NewRegIdIdentValueBucketKey(CertificatesPerDomainPerAccount, regId, ident) + // Add a check-only transaction for each per account per identValue + // bucket. + txn, err := newCheckOnlyTransaction(perAccountLimit, perAccountPerDomainOrCIDRBucketKey, 1) + if err != nil { + if errors.Is(err, errLimitDisabled) { + continue + } + return nil, err + } + txns = append(txns, txn) + } else { + // Use the per domainOrCIDR bucket key when no per account per + // domainOrCIDR override is configured. + perDomainOrCIDRLimit, err := builder.getLimit(CertificatesPerDomain, perDomainOrCIDRBucketKey) + if err != nil { + if errors.Is(err, errLimitDisabled) { + continue + } + return nil, err + } + // Add a check-only transaction for each per domainOrCIDR bucket. + txn, err := newCheckOnlyTransaction(perDomainOrCIDRLimit, perDomainOrCIDRBucketKey, 1) + if err != nil { + return nil, err + } + txns = append(txns, txn) + } + } + return txns, nil +} + +// CertificatesPerDomainSpendOnlyTransactions returns a slice of Transactions +// for the provided order identifiers. It returns an error if any of the order +// identifiers' values are invalid. If a CertificatesPerDomainPerAccount +// override is configured, it generates two types of Transactions: +// - A spend-only Transaction for each per-account, per-domainOrCIDR bucket, +// which enforces the limit on certificates issued per domainOrCIDR for +// each account. +// - A spend-only Transaction for each per-domainOrCIDR bucket, which +// enforces the global limit on certificates issued per domainOrCIDR. +// +// If no CertificatesPerDomainPerAccount override is present, it returns a +// spend-only Transaction for each global per-domainOrCIDR bucket. This method +// should be used for spending capacity, when a certificate is issued. +// +// Precondition: orderIdents must all pass policy.WellFormedIdentifiers. +func (builder *TransactionBuilder) CertificatesPerDomainSpendOnlyTransactions(regId int64, orderIdents identifier.ACMEIdentifiers) ([]Transaction, error) { + if len(orderIdents) > 100 { + return nil, fmt.Errorf("unwilling to process more than 100 rate limit transactions, got %d", len(orderIdents)) + } + + perAccountLimitBucketKey := newRegIdBucketKey(CertificatesPerDomainPerAccount, regId) + accountOverride := true + perAccountLimit, err := builder.getLimit(CertificatesPerDomainPerAccount, perAccountLimitBucketKey) + if err != nil { + // The CertificatesPerDomainPerAccount limit never has a default. If there is an override for it, + // the above call will return the override. But if there is none, it will return errLimitDisabled. + // In that case we want to continue, but make sure we don't reference `perAccountLimit` because it + // is not a valid limit. + if errors.Is(err, errLimitDisabled) { + accountOverride = false + } else { + return nil, err + } + } + + coveringIdents, err := coveringIdentifiers(orderIdents) + if err != nil { + return nil, err + } + + var txns []Transaction + for _, ident := range coveringIdents { + perDomainOrCIDRBucketKey := newDomainOrCIDRBucketKey(CertificatesPerDomain, ident) + if accountOverride { + if !perAccountLimit.isOverride { + return nil, fmt.Errorf("shouldn't happen: CertificatesPerDomainPerAccount limit is not an override") + } + perAccountPerDomainOrCIDRBucketKey := NewRegIdIdentValueBucketKey(CertificatesPerDomainPerAccount, regId, ident) + // Add a spend-only transaction for each per account per + // domainOrCIDR bucket. + txn, err := newSpendOnlyTransaction(perAccountLimit, perAccountPerDomainOrCIDRBucketKey, 1) + if err != nil { + return nil, err + } + txns = append(txns, txn) + + perDomainOrCIDRLimit, err := builder.getLimit(CertificatesPerDomain, perDomainOrCIDRBucketKey) + if err != nil { + if errors.Is(err, errLimitDisabled) { + continue + } + return nil, err + } + + // Add a spend-only transaction for each per domainOrCIDR bucket. + txn, err = newSpendOnlyTransaction(perDomainOrCIDRLimit, perDomainOrCIDRBucketKey, 1) + if err != nil { + return nil, err + } + txns = append(txns, txn) + } else { + // Use the per domainOrCIDR bucket key when no per account per + // domainOrCIDR override is configured. + perDomainOrCIDRLimit, err := builder.getLimit(CertificatesPerDomain, perDomainOrCIDRBucketKey) + if err != nil { + if errors.Is(err, errLimitDisabled) { + continue + } + return nil, err + } + // Add a spend-only transaction for each per domainOrCIDR bucket. + txn, err := newSpendOnlyTransaction(perDomainOrCIDRLimit, perDomainOrCIDRBucketKey, 1) + if err != nil { + return nil, err + } + txns = append(txns, txn) + } + } + return txns, nil +} + +// certificatesPerFQDNSetCheckOnlyTransaction returns a check-only Transaction +// for the provided order identifiers. This method should only be used for +// checking capacity, before allowing more orders to be created. +func (builder *TransactionBuilder) certificatesPerFQDNSetCheckOnlyTransaction(orderIdents identifier.ACMEIdentifiers) (Transaction, error) { + bucketKey := newFQDNSetBucketKey(CertificatesPerFQDNSet, orderIdents) + limit, err := builder.getLimit(CertificatesPerFQDNSet, bucketKey) + if err != nil { + if errors.Is(err, errLimitDisabled) { + return newAllowOnlyTransaction(), nil + } + return Transaction{}, err + } + return newCheckOnlyTransaction(limit, bucketKey, 1) +} + +// CertificatesPerFQDNSetSpendOnlyTransaction returns a spend-only Transaction +// for the provided order identifiers. This method should only be used for +// spending capacity, when a certificate is issued. +func (builder *TransactionBuilder) CertificatesPerFQDNSetSpendOnlyTransaction(orderIdents identifier.ACMEIdentifiers) (Transaction, error) { + bucketKey := newFQDNSetBucketKey(CertificatesPerFQDNSet, orderIdents) + limit, err := builder.getLimit(CertificatesPerFQDNSet, bucketKey) + if err != nil { + if errors.Is(err, errLimitDisabled) { + return newAllowOnlyTransaction(), nil + } + return Transaction{}, err + } + return newSpendOnlyTransaction(limit, bucketKey, 1) +} + +// NewOrderLimitTransactions takes in values from a new-order request and +// returns the set of rate limit transactions that should be evaluated before +// allowing the request to proceed. +// +// Precondition: idents must be a list of identifiers that all pass +// policy.WellFormedIdentifiers. +func (builder *TransactionBuilder) NewOrderLimitTransactions(regId int64, idents identifier.ACMEIdentifiers, isRenewal bool) ([]Transaction, error) { + makeTxnError := func(err error, limit Name) error { + return fmt.Errorf("error constructing rate limit transaction for %s rate limit: %w", limit, err) + } + + var transactions []Transaction + if !isRenewal { + txn, err := builder.ordersPerAccountTransaction(regId) + if err != nil { + return nil, makeTxnError(err, NewOrdersPerAccount) + } + transactions = append(transactions, txn) + } + + txns, err := builder.FailedAuthorizationsPerDomainPerAccountCheckOnlyTransactions(regId, idents) + if err != nil { + return nil, makeTxnError(err, FailedAuthorizationsPerDomainPerAccount) + } + transactions = append(transactions, txns...) + + if !isRenewal { + txns, err := builder.certificatesPerDomainCheckOnlyTransactions(regId, idents) + if err != nil { + return nil, makeTxnError(err, CertificatesPerDomain) + } + transactions = append(transactions, txns...) + } + + txn, err := builder.certificatesPerFQDNSetCheckOnlyTransaction(idents) + if err != nil { + return nil, makeTxnError(err, CertificatesPerFQDNSet) + } + return append(transactions, txn), nil +} + +// NewAccountLimitTransactions takes in an IP address from a new-account request +// and returns the set of rate limit transactions that should be evaluated +// before allowing the request to proceed. +func (builder *TransactionBuilder) NewAccountLimitTransactions(ip netip.Addr) ([]Transaction, error) { + makeTxnError := func(err error, limit Name) error { + return fmt.Errorf("error constructing rate limit transaction for %s rate limit: %w", limit, err) + } + + var transactions []Transaction + txn, err := builder.registrationsPerIPAddressTransaction(ip) + if err != nil { + return nil, makeTxnError(err, NewRegistrationsPerIPAddress) + } + transactions = append(transactions, txn) + + if ip.Is4() { + // This request was made from an IPv4 address. + return transactions, nil + } + + txn, err = builder.registrationsPerIPv6RangeTransaction(ip) + if err != nil { + return nil, makeTxnError(err, NewRegistrationsPerIPv6Range) + } + return append(transactions, txn), nil +} diff --git a/third-party/github.com/letsencrypt/boulder/ratelimits/transaction_test.go b/third-party/github.com/letsencrypt/boulder/ratelimits/transaction_test.go new file mode 100644 index 000000000..e1e37bf8f --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/ratelimits/transaction_test.go @@ -0,0 +1,229 @@ +package ratelimits + +import ( + "fmt" + "net/netip" + "sort" + "testing" + "time" + + "github.com/letsencrypt/boulder/config" + "github.com/letsencrypt/boulder/core" + "github.com/letsencrypt/boulder/identifier" + "github.com/letsencrypt/boulder/test" +) + +func TestNewTransactionBuilderFromFiles_WithBadLimitsPath(t *testing.T) { + t.Parallel() + _, err := NewTransactionBuilderFromFiles("testdata/does-not-exist.yml", "") + test.AssertError(t, err, "should error") + + _, err = NewTransactionBuilderFromFiles("testdata/defaults.yml", "testdata/does-not-exist.yml") + test.AssertError(t, err, "should error") +} + +func sortTransactions(txns []Transaction) []Transaction { + sort.Slice(txns, func(i, j int) bool { + return txns[i].bucketKey < txns[j].bucketKey + }) + return txns +} + +func TestNewRegistrationsPerIPAddressTransactions(t *testing.T) { + t.Parallel() + + tb, err := NewTransactionBuilderFromFiles("../test/config-next/wfe2-ratelimit-defaults.yml", "") + test.AssertNotError(t, err, "creating TransactionBuilder") + + // A check-and-spend transaction for the global limit. + txn, err := tb.registrationsPerIPAddressTransaction(netip.MustParseAddr("1.2.3.4")) + test.AssertNotError(t, err, "creating transaction") + test.AssertEquals(t, txn.bucketKey, "1:1.2.3.4") + test.Assert(t, txn.check && txn.spend, "should be check-and-spend") +} + +func TestNewRegistrationsPerIPv6AddressTransactions(t *testing.T) { + t.Parallel() + + tb, err := NewTransactionBuilderFromFiles("../test/config-next/wfe2-ratelimit-defaults.yml", "") + test.AssertNotError(t, err, "creating TransactionBuilder") + + // A check-and-spend transaction for the global limit. + txn, err := tb.registrationsPerIPv6RangeTransaction(netip.MustParseAddr("2001:db8::1")) + test.AssertNotError(t, err, "creating transaction") + test.AssertEquals(t, txn.bucketKey, "2:2001:db8::/48") + test.Assert(t, txn.check && txn.spend, "should be check-and-spend") +} + +func TestNewOrdersPerAccountTransactions(t *testing.T) { + t.Parallel() + + tb, err := NewTransactionBuilderFromFiles("../test/config-next/wfe2-ratelimit-defaults.yml", "") + test.AssertNotError(t, err, "creating TransactionBuilder") + + // A check-and-spend transaction for the global limit. + txn, err := tb.ordersPerAccountTransaction(123456789) + test.AssertNotError(t, err, "creating transaction") + test.AssertEquals(t, txn.bucketKey, "3:123456789") + test.Assert(t, txn.check && txn.spend, "should be check-and-spend") +} + +func TestFailedAuthorizationsPerDomainPerAccountTransactions(t *testing.T) { + t.Parallel() + + tb, err := NewTransactionBuilderFromFiles("../test/config-next/wfe2-ratelimit-defaults.yml", "testdata/working_override_13371338.yml") + test.AssertNotError(t, err, "creating TransactionBuilder") + + // A check-only transaction for the default per-account limit. + txns, err := tb.FailedAuthorizationsPerDomainPerAccountCheckOnlyTransactions(123456789, identifier.NewDNSSlice([]string{"so.many.labels.here.example.com"})) + test.AssertNotError(t, err, "creating transactions") + test.AssertEquals(t, len(txns), 1) + test.AssertEquals(t, txns[0].bucketKey, "4:123456789:so.many.labels.here.example.com") + test.Assert(t, txns[0].checkOnly(), "should be check-only") + test.Assert(t, !txns[0].limit.isOverride, "should not be an override") + + // A spend-only transaction for the default per-account limit. + txn, err := tb.FailedAuthorizationsPerDomainPerAccountSpendOnlyTransaction(123456789, identifier.NewDNS("so.many.labels.here.example.com")) + test.AssertNotError(t, err, "creating transaction") + test.AssertEquals(t, txn.bucketKey, "4:123456789:so.many.labels.here.example.com") + test.Assert(t, txn.spendOnly(), "should be spend-only") + test.Assert(t, !txn.limit.isOverride, "should not be an override") + + // A check-only transaction for the per-account limit override. + txns, err = tb.FailedAuthorizationsPerDomainPerAccountCheckOnlyTransactions(13371338, identifier.NewDNSSlice([]string{"so.many.labels.here.example.com"})) + test.AssertNotError(t, err, "creating transactions") + test.AssertEquals(t, len(txns), 1) + test.AssertEquals(t, txns[0].bucketKey, "4:13371338:so.many.labels.here.example.com") + test.Assert(t, txns[0].checkOnly(), "should be check-only") + test.Assert(t, txns[0].limit.isOverride, "should be an override") + + // A spend-only transaction for the per-account limit override. + txn, err = tb.FailedAuthorizationsPerDomainPerAccountSpendOnlyTransaction(13371338, identifier.NewDNS("so.many.labels.here.example.com")) + test.AssertNotError(t, err, "creating transaction") + test.AssertEquals(t, txn.bucketKey, "4:13371338:so.many.labels.here.example.com") + test.Assert(t, txn.spendOnly(), "should be spend-only") + test.Assert(t, txn.limit.isOverride, "should be an override") +} + +func TestFailedAuthorizationsForPausingPerDomainPerAccountTransactions(t *testing.T) { + t.Parallel() + + tb, err := NewTransactionBuilderFromFiles("../test/config-next/wfe2-ratelimit-defaults.yml", "testdata/working_override_13371338.yml") + test.AssertNotError(t, err, "creating TransactionBuilder") + + // A transaction for the per-account limit override. + txn, err := tb.FailedAuthorizationsForPausingPerDomainPerAccountTransaction(13371338, identifier.NewDNS("so.many.labels.here.example.com")) + test.AssertNotError(t, err, "creating transaction") + test.AssertEquals(t, txn.bucketKey, "8:13371338:so.many.labels.here.example.com") + test.Assert(t, txn.check && txn.spend, "should be check and spend") + test.Assert(t, txn.limit.isOverride, "should be an override") +} + +func TestCertificatesPerDomainTransactions(t *testing.T) { + t.Parallel() + + tb, err := NewTransactionBuilderFromFiles("../test/config-next/wfe2-ratelimit-defaults.yml", "") + test.AssertNotError(t, err, "creating TransactionBuilder") + + // One check-only transaction for the global limit. + txns, err := tb.certificatesPerDomainCheckOnlyTransactions(123456789, identifier.NewDNSSlice([]string{"so.many.labels.here.example.com"})) + test.AssertNotError(t, err, "creating transactions") + test.AssertEquals(t, len(txns), 1) + test.AssertEquals(t, txns[0].bucketKey, "5:example.com") + test.Assert(t, txns[0].checkOnly(), "should be check-only") + + // One spend-only transaction for the global limit. + txns, err = tb.CertificatesPerDomainSpendOnlyTransactions(123456789, identifier.NewDNSSlice([]string{"so.many.labels.here.example.com"})) + test.AssertNotError(t, err, "creating transactions") + test.AssertEquals(t, len(txns), 1) + test.AssertEquals(t, txns[0].bucketKey, "5:example.com") + test.Assert(t, txns[0].spendOnly(), "should be spend-only") +} + +func TestCertificatesPerDomainPerAccountTransactions(t *testing.T) { + t.Parallel() + + tb, err := NewTransactionBuilderFromFiles("../test/config-next/wfe2-ratelimit-defaults.yml", "testdata/working_override_13371338.yml") + test.AssertNotError(t, err, "creating TransactionBuilder") + + // We only expect a single check-only transaction for the per-account limit + // override. We can safely ignore the global limit when an override is + // present. + txns, err := tb.certificatesPerDomainCheckOnlyTransactions(13371338, identifier.NewDNSSlice([]string{"so.many.labels.here.example.com"})) + test.AssertNotError(t, err, "creating transactions") + test.AssertEquals(t, len(txns), 1) + test.AssertEquals(t, txns[0].bucketKey, "6:13371338:example.com") + test.Assert(t, txns[0].checkOnly(), "should be check-only") + test.Assert(t, txns[0].limit.isOverride, "should be an override") + + // Same as above, but with multiple example.com domains. + txns, err = tb.certificatesPerDomainCheckOnlyTransactions(13371338, identifier.NewDNSSlice([]string{"so.many.labels.here.example.com", "z.example.com"})) + test.AssertNotError(t, err, "creating transactions") + test.AssertEquals(t, len(txns), 1) + test.AssertEquals(t, txns[0].bucketKey, "6:13371338:example.com") + test.Assert(t, txns[0].checkOnly(), "should be check-only") + test.Assert(t, txns[0].limit.isOverride, "should be an override") + + // Same as above, but with different domains. + txns, err = tb.certificatesPerDomainCheckOnlyTransactions(13371338, identifier.NewDNSSlice([]string{"so.many.labels.here.example.com", "z.example.net"})) + test.AssertNotError(t, err, "creating transactions") + txns = sortTransactions(txns) + test.AssertEquals(t, len(txns), 2) + test.AssertEquals(t, txns[0].bucketKey, "6:13371338:example.com") + test.Assert(t, txns[0].checkOnly(), "should be check-only") + test.Assert(t, txns[0].limit.isOverride, "should be an override") + test.AssertEquals(t, txns[1].bucketKey, "6:13371338:example.net") + test.Assert(t, txns[1].checkOnly(), "should be check-only") + test.Assert(t, txns[1].limit.isOverride, "should be an override") + + // Two spend-only transactions, one for the global limit and one for the + // per-account limit override. + txns, err = tb.CertificatesPerDomainSpendOnlyTransactions(13371338, identifier.NewDNSSlice([]string{"so.many.labels.here.example.com"})) + test.AssertNotError(t, err, "creating TransactionBuilder") + test.AssertEquals(t, len(txns), 2) + txns = sortTransactions(txns) + test.AssertEquals(t, txns[0].bucketKey, "5:example.com") + test.Assert(t, txns[0].spendOnly(), "should be spend-only") + test.Assert(t, !txns[0].limit.isOverride, "should not be an override") + + test.AssertEquals(t, txns[1].bucketKey, "6:13371338:example.com") + test.Assert(t, txns[1].spendOnly(), "should be spend-only") + test.Assert(t, txns[1].limit.isOverride, "should be an override") +} + +func TestCertificatesPerFQDNSetTransactions(t *testing.T) { + t.Parallel() + + tb, err := NewTransactionBuilderFromFiles("../test/config-next/wfe2-ratelimit-defaults.yml", "") + test.AssertNotError(t, err, "creating TransactionBuilder") + + // A single check-only transaction for the global limit. + txn, err := tb.certificatesPerFQDNSetCheckOnlyTransaction(identifier.NewDNSSlice([]string{"example.com", "example.net", "example.org"})) + test.AssertNotError(t, err, "creating transaction") + namesHash := fmt.Sprintf("%x", core.HashIdentifiers(identifier.NewDNSSlice([]string{"example.com", "example.net", "example.org"}))) + test.AssertEquals(t, txn.bucketKey, "7:"+namesHash) + test.Assert(t, txn.checkOnly(), "should be check-only") + test.Assert(t, !txn.limit.isOverride, "should not be an override") +} + +func TestNewTransactionBuilder(t *testing.T) { + t.Parallel() + + expectedBurst := int64(10000) + expectedCount := int64(10000) + expectedPeriod := config.Duration{Duration: time.Hour * 168} + + tb, err := NewTransactionBuilder(LimitConfigs{ + NewRegistrationsPerIPAddress.String(): &LimitConfig{ + Burst: expectedBurst, + Count: expectedCount, + Period: expectedPeriod}, + }) + test.AssertNotError(t, err, "creating TransactionBuilder") + + newRegDefault, ok := tb.limitRegistry.defaults[NewRegistrationsPerIPAddress.EnumString()] + test.Assert(t, ok, "NewRegistrationsPerIPAddress was not populated in registry") + test.AssertEquals(t, newRegDefault.burst, expectedBurst) + test.AssertEquals(t, newRegDefault.count, expectedCount) + test.AssertEquals(t, newRegDefault.period, expectedPeriod) +} diff --git a/third-party/github.com/letsencrypt/boulder/ratelimits/utilities.go b/third-party/github.com/letsencrypt/boulder/ratelimits/utilities.go index dd5a1167e..7999b80d0 100644 --- a/third-party/github.com/letsencrypt/boulder/ratelimits/utilities.go +++ b/third-party/github.com/letsencrypt/boulder/ratelimits/utilities.go @@ -1,10 +1,14 @@ package ratelimits import ( + "fmt" + "net/netip" "strings" - "github.com/letsencrypt/boulder/core" "github.com/weppos/publicsuffix-go/publicsuffix" + + "github.com/letsencrypt/boulder/core" + "github.com/letsencrypt/boulder/identifier" ) // joinWithColon joins the provided args with a colon. @@ -12,22 +16,57 @@ func joinWithColon(args ...string) string { return strings.Join(args, ":") } -// DomainsForRateLimiting transforms a list of FQDNs into a list of eTLD+1's -// for the purpose of rate limiting. It also de-duplicates the output -// domains. Exact public suffix matches are included. -func DomainsForRateLimiting(names []string) []string { - var domains []string - for _, name := range names { - domain, err := publicsuffix.Domain(name) - if err != nil { - // The only possible errors are: - // (1) publicsuffix.Domain is giving garbage values - // (2) the public suffix is the domain itself - // We assume 2 and include the original name in the result. - domains = append(domains, name) - } else { - domains = append(domains, domain) +// coveringIdentifiers transforms a slice of ACMEIdentifiers into strings of +// their "covering" identifiers, for the CertificatesPerDomain limit. It also +// de-duplicates the output. For DNS identifiers, this is eTLD+1's; exact public +// suffix matches are included. For IP address identifiers, this is the address +// (/32) for IPv4, or the /64 prefix for IPv6, in CIDR notation. +func coveringIdentifiers(idents identifier.ACMEIdentifiers) ([]string, error) { + var covers []string + for _, ident := range idents { + switch ident.Type { + case identifier.TypeDNS: + domain, err := publicsuffix.Domain(ident.Value) + if err != nil { + if err.Error() == fmt.Sprintf("%s is a suffix", ident.Value) { + // If the public suffix is the domain itself, that's fine. + // Include the original name in the result. + covers = append(covers, ident.Value) + continue + } else { + return nil, err + } + } + covers = append(covers, domain) + case identifier.TypeIP: + ip, err := netip.ParseAddr(ident.Value) + if err != nil { + return nil, err + } + prefix, err := coveringPrefix(ip) + if err != nil { + return nil, err + } + covers = append(covers, prefix.String()) } } - return core.UniqueLowerNames(domains) + return core.UniqueLowerNames(covers), nil +} + +// coveringPrefix transforms a netip.Addr into its "covering" prefix, for the +// CertificatesPerDomain limit. For IPv4, this is the IP address (/32). For +// IPv6, this is the /64 that contains the address. +func coveringPrefix(addr netip.Addr) (netip.Prefix, error) { + var bits int + if addr.Is4() { + bits = 32 + } else { + bits = 64 + } + prefix, err := addr.Prefix(bits) + if err != nil { + // This should be impossible because bits is hardcoded. + return netip.Prefix{}, err + } + return prefix, nil } diff --git a/third-party/github.com/letsencrypt/boulder/ratelimits/utilities_test.go b/third-party/github.com/letsencrypt/boulder/ratelimits/utilities_test.go index 9c68d3a6e..28c6f037a 100644 --- a/third-party/github.com/letsencrypt/boulder/ratelimits/utilities_test.go +++ b/third-party/github.com/letsencrypt/boulder/ratelimits/utilities_test.go @@ -1,27 +1,93 @@ package ratelimits import ( + "net/netip" + "slices" "testing" - "github.com/letsencrypt/boulder/test" + "github.com/letsencrypt/boulder/identifier" ) -func TestDomainsForRateLimiting(t *testing.T) { - domains := DomainsForRateLimiting([]string{}) - test.AssertEquals(t, len(domains), 0) +func TestCoveringIdentifiers(t *testing.T) { + cases := []struct { + name string + idents identifier.ACMEIdentifiers + wantErr string + want []string + }{ + { + name: "empty string", + idents: identifier.ACMEIdentifiers{ + identifier.NewDNS(""), + }, + wantErr: "name is blank", + want: nil, + }, + { + name: "two subdomains of same domain", + idents: identifier.NewDNSSlice([]string{"www.example.com", "example.com"}), + want: []string{"example.com"}, + }, + { + name: "three subdomains across two domains", + idents: identifier.NewDNSSlice([]string{"www.example.com", "example.com", "www.example.co.uk"}), + want: []string{"example.co.uk", "example.com"}, + }, + { + name: "three subdomains across two domains, plus a bare TLD", + idents: identifier.NewDNSSlice([]string{"www.example.com", "example.com", "www.example.co.uk", "co.uk"}), + want: []string{"co.uk", "example.co.uk", "example.com"}, + }, + { + name: "two subdomains of same domain, one of them long", + idents: identifier.NewDNSSlice([]string{"foo.bar.baz.www.example.com", "baz.example.com"}), + want: []string{"example.com"}, + }, + { + name: "a domain and two of its subdomains", + idents: identifier.NewDNSSlice([]string{"github.io", "foo.github.io", "bar.github.io"}), + want: []string{"bar.github.io", "foo.github.io", "github.io"}, + }, + { + name: "a domain and an IPv4 address", + idents: identifier.ACMEIdentifiers{ + identifier.NewDNS("example.com"), + identifier.NewIP(netip.MustParseAddr("127.0.0.1")), + }, + want: []string{"127.0.0.1/32", "example.com"}, + }, + { + name: "an IPv6 address", + idents: identifier.ACMEIdentifiers{ + identifier.NewIP(netip.MustParseAddr("3fff:aaa:aaaa:aaaa:abad:0ff1:cec0:ffee")), + }, + want: []string{"3fff:aaa:aaaa:aaaa::/64"}, + }, + { + name: "four IP addresses in three prefixes", + idents: identifier.ACMEIdentifiers{ + identifier.NewIP(netip.MustParseAddr("127.0.0.1")), + identifier.NewIP(netip.MustParseAddr("127.0.0.254")), + identifier.NewIP(netip.MustParseAddr("3fff:aaa:aaaa:aaaa:abad:0ff1:cec0:ffee")), + identifier.NewIP(netip.MustParseAddr("3fff:aaa:aaaa:ffff:abad:0ff1:cec0:ffee")), + }, + want: []string{"127.0.0.1/32", "127.0.0.254/32", "3fff:aaa:aaaa:aaaa::/64", "3fff:aaa:aaaa:ffff::/64"}, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() - domains = DomainsForRateLimiting([]string{"www.example.com", "example.com"}) - test.AssertDeepEquals(t, domains, []string{"example.com"}) - - domains = DomainsForRateLimiting([]string{"www.example.com", "example.com", "www.example.co.uk"}) - test.AssertDeepEquals(t, domains, []string{"example.co.uk", "example.com"}) - - domains = DomainsForRateLimiting([]string{"www.example.com", "example.com", "www.example.co.uk", "co.uk"}) - test.AssertDeepEquals(t, domains, []string{"co.uk", "example.co.uk", "example.com"}) - - domains = DomainsForRateLimiting([]string{"foo.bar.baz.www.example.com", "baz.example.com"}) - test.AssertDeepEquals(t, domains, []string{"example.com"}) - - domains = DomainsForRateLimiting([]string{"github.io", "foo.github.io", "bar.github.io"}) - test.AssertDeepEquals(t, domains, []string{"bar.github.io", "foo.github.io", "github.io"}) + got, err := coveringIdentifiers(tc.idents) + if err != nil && err.Error() != tc.wantErr { + t.Errorf("Got unwanted error %#v", err.Error()) + } + if err == nil && tc.wantErr != "" { + t.Errorf("Got no error, wanted %#v", tc.wantErr) + } + if !slices.Equal(got, tc.want) { + t.Errorf("Got %#v, but want %#v", got, tc.want) + } + }) + } } diff --git a/third-party/github.com/letsencrypt/boulder/redis/config.go b/third-party/github.com/letsencrypt/boulder/redis/config.go index 997969373..c858a4beb 100644 --- a/third-party/github.com/letsencrypt/boulder/redis/config.go +++ b/third-party/github.com/letsencrypt/boulder/redis/config.go @@ -3,11 +3,13 @@ package redis import ( "fmt" + "github.com/prometheus/client_golang/prometheus" + "github.com/redis/go-redis/extra/redisotel/v9" + "github.com/redis/go-redis/v9" + "github.com/letsencrypt/boulder/cmd" "github.com/letsencrypt/boulder/config" blog "github.com/letsencrypt/boulder/log" - "github.com/prometheus/client_golang/prometheus" - "github.com/redis/go-redis/v9" ) // Config contains the configuration needed to act as a Redis client. @@ -163,6 +165,11 @@ func NewRingFromConfig(c Config, stats prometheus.Registerer, log blog.Logger) ( lookup.start() } + err = redisotel.InstrumentTracing(inner) + if err != nil { + return nil, err + } + return &Ring{ Ring: inner, lookup: lookup, diff --git a/third-party/github.com/letsencrypt/boulder/redis/metrics_test.go b/third-party/github.com/letsencrypt/boulder/redis/metrics_test.go index 9da3bb613..b67237ec9 100644 --- a/third-party/github.com/letsencrypt/boulder/redis/metrics_test.go +++ b/third-party/github.com/letsencrypt/boulder/redis/metrics_test.go @@ -40,16 +40,17 @@ func TestMetrics(t *testing.T) { results := make(map[string]bool) for range expectedMetrics { metric := <-outChan + t.Log(metric.Desc().String()) results[metric.Desc().String()] = true } expected := strings.Split( - `Desc{fqName: "redis_connection_pool_lookups", help: "Number of lookups for a connection in the pool, labeled by hit/miss", constLabels: {foo="bar"}, variableLabels: [{result }]} -Desc{fqName: "redis_connection_pool_lookups", help: "Number of lookups for a connection in the pool, labeled by hit/miss", constLabels: {foo="bar"}, variableLabels: [{result }]} -Desc{fqName: "redis_connection_pool_lookups", help: "Number of lookups for a connection in the pool, labeled by hit/miss", constLabels: {foo="bar"}, variableLabels: [{result }]} -Desc{fqName: "redis_connection_pool_total_conns", help: "Number of total connections in the pool.", constLabels: {foo="bar"}, variableLabels: []} -Desc{fqName: "redis_connection_pool_idle_conns", help: "Number of idle connections in the pool.", constLabels: {foo="bar"}, variableLabels: []} -Desc{fqName: "redis_connection_pool_stale_conns", help: "Number of stale connections removed from the pool.", constLabels: {foo="bar"}, variableLabels: []}`, + `Desc{fqName: "redis_connection_pool_lookups", help: "Number of lookups for a connection in the pool, labeled by hit/miss", constLabels: {foo="bar"}, variableLabels: {result}} +Desc{fqName: "redis_connection_pool_lookups", help: "Number of lookups for a connection in the pool, labeled by hit/miss", constLabels: {foo="bar"}, variableLabels: {result}} +Desc{fqName: "redis_connection_pool_lookups", help: "Number of lookups for a connection in the pool, labeled by hit/miss", constLabels: {foo="bar"}, variableLabels: {result}} +Desc{fqName: "redis_connection_pool_total_conns", help: "Number of total connections in the pool.", constLabels: {foo="bar"}, variableLabels: {}} +Desc{fqName: "redis_connection_pool_idle_conns", help: "Number of idle connections in the pool.", constLabels: {foo="bar"}, variableLabels: {}} +Desc{fqName: "redis_connection_pool_stale_conns", help: "Number of stale connections removed from the pool.", constLabels: {foo="bar"}, variableLabels: {}}`, "\n") for _, e := range expected { diff --git a/third-party/github.com/letsencrypt/boulder/rocsp/rocsp_test.go b/third-party/github.com/letsencrypt/boulder/rocsp/rocsp_test.go index 51bbc903d..499b4eb27 100644 --- a/third-party/github.com/letsencrypt/boulder/rocsp/rocsp_test.go +++ b/third-party/github.com/letsencrypt/boulder/rocsp/rocsp_test.go @@ -32,8 +32,8 @@ func makeClient() (*RWClient, clock.Clock) { rdb := redis.NewRing(&redis.RingOptions{ Addrs: map[string]string{ - "shard1": "10.33.33.2:4218", - "shard2": "10.33.33.3:4218", + "shard1": "10.77.77.2:4218", + "shard2": "10.77.77.3:4218", }, Username: "unittest-rw", Password: "824968fa490f4ecec1e52d5e34916bdb60d45f8d", diff --git a/third-party/github.com/letsencrypt/boulder/sa/database.go b/third-party/github.com/letsencrypt/boulder/sa/database.go index ba3b73003..34447d7da 100644 --- a/third-party/github.com/letsencrypt/boulder/sa/database.go +++ b/third-party/github.com/letsencrypt/boulder/sa/database.go @@ -266,24 +266,23 @@ func (log *SQLLogger) Printf(format string, v ...interface{}) { func initTables(dbMap *borp.DbMap) { regTable := dbMap.AddTableWithName(regModel{}, "registrations").SetKeys(true, "ID") - regTable.SetVersionCol("LockCol") regTable.ColMap("Key").SetNotNull(true) regTable.ColMap("KeySHA256").SetNotNull(true).SetUnique(true) dbMap.AddTableWithName(issuedNameModel{}, "issuedNames").SetKeys(true, "ID") dbMap.AddTableWithName(core.Certificate{}, "certificates").SetKeys(true, "ID") - dbMap.AddTableWithName(core.CertificateStatus{}, "certificateStatus").SetKeys(true, "ID") + dbMap.AddTableWithName(certificateStatusModel{}, "certificateStatus").SetKeys(true, "ID") dbMap.AddTableWithName(core.FQDNSet{}, "fqdnSets").SetKeys(true, "ID") - if features.Get().MultipleCertificateProfiles { - dbMap.AddTableWithName(orderModelv2{}, "orders").SetKeys(true, "ID") - } else { - dbMap.AddTableWithName(orderModelv1{}, "orders").SetKeys(true, "ID") + tableMap := dbMap.AddTableWithName(orderModel{}, "orders").SetKeys(true, "ID") + if !features.Get().StoreARIReplacesInOrders { + tableMap.ColMap("Replaces").SetTransient(true) } + dbMap.AddTableWithName(orderToAuthzModel{}, "orderToAuthz").SetKeys(false, "OrderID", "AuthzID") dbMap.AddTableWithName(orderFQDNSet{}, "orderFqdnSets").SetKeys(true, "ID") dbMap.AddTableWithName(authzModel{}, "authz2").SetKeys(true, "ID") dbMap.AddTableWithName(orderToAuthzModel{}, "orderToAuthz2").SetKeys(false, "OrderID", "AuthzID") dbMap.AddTableWithName(recordedSerialModel{}, "serials").SetKeys(true, "ID") - dbMap.AddTableWithName(precertificateModel{}, "precertificates").SetKeys(true, "ID") + dbMap.AddTableWithName(lintingCertModel{}, "precertificates").SetKeys(true, "ID") dbMap.AddTableWithName(keyHashModel{}, "keyHashToSerial").SetKeys(true, "ID") dbMap.AddTableWithName(incidentModel{}, "incidents").SetKeys(true, "ID") dbMap.AddTable(incidentSerialModel{}) @@ -291,6 +290,7 @@ func initTables(dbMap *borp.DbMap) { dbMap.AddTableWithName(revokedCertModel{}, "revokedCertificates").SetKeys(true, "ID") dbMap.AddTableWithName(replacementOrderModel{}, "replacementOrders").SetKeys(true, "ID") dbMap.AddTableWithName(pausedModel{}, "paused") + dbMap.AddTableWithName(overrideModel{}, "overrides").SetKeys(false, "limitEnum", "bucketKey") // Read-only maps used for selecting subsets of columns. dbMap.AddTableWithName(CertStatusMetadata{}, "certificateStatus") diff --git a/third-party/github.com/letsencrypt/boulder/sa/db-next/boulder_sa/20241218000000_RemoveOldRateLimitTables.sql b/third-party/github.com/letsencrypt/boulder/sa/db-next/boulder_sa/20241218000000_RemoveOldRateLimitTables.sql new file mode 100644 index 000000000..efd9cc961 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/sa/db-next/boulder_sa/20241218000000_RemoveOldRateLimitTables.sql @@ -0,0 +1,27 @@ +-- +migrate Up + +DROP TABLE certificatesPerName; +DROP TABLE newOrdersRL; + +-- +migrate Down + +DROP TABLE certificatesPerName; +DROP TABLE newOrdersRL; + +CREATE TABLE `certificatesPerName` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `eTLDPlusOne` varchar(255) NOT NULL, + `time` datetime NOT NULL, + `count` int(11) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `eTLDPlusOne_time_idx` (`eTLDPlusOne`,`time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE `newOrdersRL` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `regID` bigint(20) NOT NULL, + `time` datetime NOT NULL, + `count` int(11) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `regID_time_idx` (`regID`,`time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/third-party/github.com/letsencrypt/boulder/sa/db-next/boulder_sa/20250110000000_NullRegistrationsLockCol.sql b/third-party/github.com/letsencrypt/boulder/sa/db-next/boulder_sa/20250110000000_NullRegistrationsLockCol.sql new file mode 100644 index 000000000..af0170406 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/sa/db-next/boulder_sa/20250110000000_NullRegistrationsLockCol.sql @@ -0,0 +1,10 @@ + +-- +migrate Up +-- SQL in section 'Up' is executed when this migration is applied + +ALTER TABLE `registrations` ALTER COLUMN `LockCol` SET DEFAULT 0; + +-- +migrate Down +-- SQL section 'Down' is executed when this migration is rolled back + +ALTER TABLE `registrations` ALTER COLUMN `LockCol` DROP DEFAULT; diff --git a/third-party/github.com/letsencrypt/boulder/sa/db-next/boulder_sa/20250113000000_DropRegistrationsInitialIP.sql b/third-party/github.com/letsencrypt/boulder/sa/db-next/boulder_sa/20250113000000_DropRegistrationsInitialIP.sql new file mode 100644 index 000000000..43c218918 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/sa/db-next/boulder_sa/20250113000000_DropRegistrationsInitialIP.sql @@ -0,0 +1,13 @@ +-- +migrate Up +-- SQL in section 'Up' is executed when this migration is applied + +ALTER TABLE `registrations` +DROP COLUMN `initialIP`, +DROP KEY `initialIP_createdAt`; + +-- +migrate Down +-- SQL section 'Down' is executed when this migration is rolled back + +ALTER TABLE `registrations` +ADD COLUMN `initialIP` binary(16) NOT NULL DEFAULT '\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0', +ADD KEY `initialIP_createdAt` (`initialIP`, `createdAt`); diff --git a/third-party/github.com/letsencrypt/boulder/sa/db-next/boulder_sa/20250304000000_OrdersReplaces.sql b/third-party/github.com/letsencrypt/boulder/sa/db-next/boulder_sa/20250304000000_OrdersReplaces.sql new file mode 100644 index 000000000..b63f12c1c --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/sa/db-next/boulder_sa/20250304000000_OrdersReplaces.sql @@ -0,0 +1,9 @@ +-- +migrate Up +-- SQL in section 'Up' is executed when this migration is applied + +ALTER TABLE `orders` ADD COLUMN `replaces` varchar(255) DEFAULT NULL; + +-- +migrate Down +-- SQL section 'Down' is executed when this migration is rolled back + +ALTER TABLE `orders` DROP COLUMN `replaces`; diff --git a/third-party/github.com/letsencrypt/boulder/sa/db-next/boulder_sa/20250417000000_RateLimitOverrides.sql b/third-party/github.com/letsencrypt/boulder/sa/db-next/boulder_sa/20250417000000_RateLimitOverrides.sql new file mode 100644 index 000000000..791fa6570 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/sa/db-next/boulder_sa/20250417000000_RateLimitOverrides.sql @@ -0,0 +1,20 @@ +-- +migrate Up +-- SQL in section 'Up' is executed when this migration is applied + +CREATE TABLE overrides ( + `limitEnum` tinyint(4) UNSIGNED NOT NULL, + `bucketKey` varchar(255) NOT NULL, + `comment` varchar(255) NOT NULL, + `periodNS` bigint(20) UNSIGNED NOT NULL, + `count` int UNSIGNED NOT NULL, + `burst` int UNSIGNED NOT NULL, + `updatedAt` datetime NOT NULL, + `enabled` boolean NOT NULL DEFAULT false, + UNIQUE KEY `limitEnum_bucketKey` (`limitEnum`, `bucketKey`), + INDEX idx_enabled (enabled) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- +migrate Down +-- SQL section 'Down' is executed when this migration is rolled back + +DROP TABLE IF EXISTS overrides; diff --git a/third-party/github.com/letsencrypt/boulder/sa/db-next/boulder_sa/20250520000000_DropRegistrationsContact.sql b/third-party/github.com/letsencrypt/boulder/sa/db-next/boulder_sa/20250520000000_DropRegistrationsContact.sql new file mode 100644 index 000000000..e0373bf8a --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/sa/db-next/boulder_sa/20250520000000_DropRegistrationsContact.sql @@ -0,0 +1,9 @@ +-- +migrate Up +-- SQL in section 'Up' is executed when this migration is applied + +ALTER TABLE `registrations` DROP COLUMN `contact`; + +-- +migrate Down +-- SQL section 'Down' is executed when this migration is rolled back + +ALTER TABLE `registrations` ADD COLUMN `contact` varchar(191) CHARACTER SET utf8mb4 DEFAULT '[]'; diff --git a/third-party/github.com/letsencrypt/boulder/sa/db-users/boulder_sa.sql b/third-party/github.com/letsencrypt/boulder/sa/db-users/boulder_sa.sql index 544f52620..d51df4c6e 100644 --- a/third-party/github.com/letsencrypt/boulder/sa/db-users/boulder_sa.sql +++ b/third-party/github.com/letsencrypt/boulder/sa/db-users/boulder_sa.sql @@ -18,7 +18,6 @@ CREATE USER IF NOT EXISTS 'proxysql'@'localhost'; GRANT SELECT,INSERT ON certificates TO 'sa'@'localhost'; GRANT SELECT,INSERT,UPDATE ON certificateStatus TO 'sa'@'localhost'; GRANT SELECT,INSERT ON issuedNames TO 'sa'@'localhost'; -GRANT SELECT,INSERT,UPDATE ON certificatesPerName TO 'sa'@'localhost'; GRANT SELECT,INSERT,UPDATE ON registrations TO 'sa'@'localhost'; GRANT SELECT,INSERT on fqdnSets TO 'sa'@'localhost'; GRANT SELECT,INSERT,UPDATE ON orders TO 'sa'@'localhost'; @@ -29,18 +28,17 @@ GRANT INSERT,SELECT ON serials TO 'sa'@'localhost'; GRANT SELECT,INSERT ON precertificates TO 'sa'@'localhost'; GRANT SELECT,INSERT ON keyHashToSerial TO 'sa'@'localhost'; GRANT SELECT,INSERT ON blockedKeys TO 'sa'@'localhost'; -GRANT SELECT,INSERT,UPDATE ON newOrdersRL TO 'sa'@'localhost'; GRANT SELECT ON incidents TO 'sa'@'localhost'; GRANT SELECT,INSERT,UPDATE ON crlShards TO 'sa'@'localhost'; GRANT SELECT,INSERT,UPDATE ON revokedCertificates TO 'sa'@'localhost'; GRANT SELECT,INSERT,UPDATE ON replacementOrders TO 'sa'@'localhost'; +GRANT SELECT,INSERT,UPDATE ON overrides TO 'sa'@'localhost'; -- Tests need to be able to TRUNCATE this table, so DROP is necessary. GRANT SELECT,INSERT,UPDATE,DROP ON paused TO 'sa'@'localhost'; GRANT SELECT ON certificates TO 'sa_ro'@'localhost'; GRANT SELECT ON certificateStatus TO 'sa_ro'@'localhost'; GRANT SELECT ON issuedNames TO 'sa_ro'@'localhost'; -GRANT SELECT ON certificatesPerName TO 'sa_ro'@'localhost'; GRANT SELECT ON registrations TO 'sa_ro'@'localhost'; GRANT SELECT on fqdnSets TO 'sa_ro'@'localhost'; GRANT SELECT ON orders TO 'sa_ro'@'localhost'; @@ -51,12 +49,12 @@ GRANT SELECT ON serials TO 'sa_ro'@'localhost'; GRANT SELECT ON precertificates TO 'sa_ro'@'localhost'; GRANT SELECT ON keyHashToSerial TO 'sa_ro'@'localhost'; GRANT SELECT ON blockedKeys TO 'sa_ro'@'localhost'; -GRANT SELECT ON newOrdersRL TO 'sa_ro'@'localhost'; GRANT SELECT ON incidents TO 'sa_ro'@'localhost'; GRANT SELECT ON crlShards TO 'sa_ro'@'localhost'; GRANT SELECT ON revokedCertificates TO 'sa_ro'@'localhost'; GRANT SELECT ON replacementOrders TO 'sa_ro'@'localhost'; GRANT SELECT ON paused TO 'sa_ro'@'localhost'; +GRANT SELECT ON overrides TO 'sa_ro'@'localhost'; -- OCSP Responder GRANT SELECT ON certificateStatus TO 'ocsp_resp'@'localhost'; diff --git a/third-party/github.com/letsencrypt/boulder/sa/db/boulder_sa/20230419000000_CombinedSchema.sql b/third-party/github.com/letsencrypt/boulder/sa/db/boulder_sa/20230419000000_CombinedSchema.sql index 34d6f151c..42c489be9 100644 --- a/third-party/github.com/letsencrypt/boulder/sa/db/boulder_sa/20230419000000_CombinedSchema.sql +++ b/third-party/github.com/letsencrypt/boulder/sa/db/boulder_sa/20230419000000_CombinedSchema.sql @@ -86,6 +86,8 @@ CREATE TABLE `fqdnSets` ( `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT, `setHash` binary(32) NOT NULL, `serial` varchar(255) NOT NULL, + -- Note: This should actually be called "notBefore" since it is set + -- based on the certificate's notBefore field, not the issuance time. `issued` datetime NOT NULL, `expires` datetime NOT NULL, PRIMARY KEY (`id`), @@ -173,6 +175,9 @@ CREATE TABLE `orders` ( PARTITION BY RANGE(id) (PARTITION p_start VALUES LESS THAN (MAXVALUE)); +-- Note: This table's name is a historical artifact and it is now +-- used to store linting certificates, not precertificates. +-- See #6807. CREATE TABLE `precertificates` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `registrationID` bigint(20) NOT NULL, diff --git a/third-party/github.com/letsencrypt/boulder/sa/db-next/boulder_sa/20230919000000_RevokedCertificates.sql b/third-party/github.com/letsencrypt/boulder/sa/db/boulder_sa/20230919000000_RevokedCertificates.sql similarity index 100% rename from third-party/github.com/letsencrypt/boulder/sa/db-next/boulder_sa/20230919000000_RevokedCertificates.sql rename to third-party/github.com/letsencrypt/boulder/sa/db/boulder_sa/20230919000000_RevokedCertificates.sql diff --git a/third-party/github.com/letsencrypt/boulder/sa/db-next/boulder_sa/20240119000000_ReplacementOrders.sql b/third-party/github.com/letsencrypt/boulder/sa/db/boulder_sa/20240119000000_ReplacementOrders.sql similarity index 100% rename from third-party/github.com/letsencrypt/boulder/sa/db-next/boulder_sa/20240119000000_ReplacementOrders.sql rename to third-party/github.com/letsencrypt/boulder/sa/db/boulder_sa/20240119000000_ReplacementOrders.sql diff --git a/third-party/github.com/letsencrypt/boulder/sa/db-next/boulder_sa/20240304000000_CertificateProfiles.sql b/third-party/github.com/letsencrypt/boulder/sa/db/boulder_sa/20240304000000_CertificateProfiles.sql similarity index 100% rename from third-party/github.com/letsencrypt/boulder/sa/db-next/boulder_sa/20240304000000_CertificateProfiles.sql rename to third-party/github.com/letsencrypt/boulder/sa/db/boulder_sa/20240304000000_CertificateProfiles.sql diff --git a/third-party/github.com/letsencrypt/boulder/sa/db-next/boulder_sa/20240514000000_Paused.sql b/third-party/github.com/letsencrypt/boulder/sa/db/boulder_sa/20240514000000_Paused.sql similarity index 82% rename from third-party/github.com/letsencrypt/boulder/sa/db-next/boulder_sa/20240514000000_Paused.sql rename to third-party/github.com/letsencrypt/boulder/sa/db/boulder_sa/20240514000000_Paused.sql index e59c693eb..9f5890cad 100644 --- a/third-party/github.com/letsencrypt/boulder/sa/db-next/boulder_sa/20240514000000_Paused.sql +++ b/third-party/github.com/letsencrypt/boulder/sa/db/boulder_sa/20240514000000_Paused.sql @@ -6,12 +6,12 @@ -- rate of ~18% per year. CREATE TABLE `paused` ( - `registrationID` bigint(20) NOT NULL, + `registrationID` bigint(20) UNSIGNED NOT NULL, `identifierType` tinyint(4) NOT NULL, `identifierValue` varchar(255) NOT NULL, `pausedAt` datetime NOT NULL, `unpausedAt` datetime DEFAULT NULL, - PRIMARY KEY (`registrationID`, `identifierType`, `identifierValue`) + PRIMARY KEY (`registrationID`, `identifierValue`, `identifierType`) ); -- +migrate Down diff --git a/third-party/github.com/letsencrypt/boulder/sa/db/boulder_sa/20250115000000_AuthzProfiles.sql b/third-party/github.com/letsencrypt/boulder/sa/db/boulder_sa/20250115000000_AuthzProfiles.sql new file mode 100644 index 000000000..9795a0a76 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/sa/db/boulder_sa/20250115000000_AuthzProfiles.sql @@ -0,0 +1,9 @@ +-- +migrate Up +-- SQL in section 'Up' is executed when this migration is applied + +ALTER TABLE `authz2` ADD COLUMN `certificateProfileName` varchar(32) DEFAULT NULL; + +-- +migrate Down +-- SQL section 'Down' is executed when this migration is rolled back + +ALTER TABLE `authz2` DROP COLUMN `certificateProfileName`; diff --git a/third-party/github.com/letsencrypt/boulder/sa/db/boulder_sa/20250519000000_NullRegistrationsContact.sql b/third-party/github.com/letsencrypt/boulder/sa/db/boulder_sa/20250519000000_NullRegistrationsContact.sql new file mode 100644 index 000000000..92151c224 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/sa/db/boulder_sa/20250519000000_NullRegistrationsContact.sql @@ -0,0 +1,9 @@ +-- +migrate Up +-- SQL in section 'Up' is executed when this migration is applied + +ALTER TABLE `registrations` ALTER COLUMN `contact` SET DEFAULT '[]'; + +-- +migrate Down +-- SQL section 'Down' is executed when this migration is rolled back + +ALTER TABLE `registrations` ALTER COLUMN `contact` DROP DEFAULT; diff --git a/third-party/github.com/letsencrypt/boulder/sa/ip_range_test.go b/third-party/github.com/letsencrypt/boulder/sa/ip_range_test.go deleted file mode 100644 index a92fc7b92..000000000 --- a/third-party/github.com/letsencrypt/boulder/sa/ip_range_test.go +++ /dev/null @@ -1,54 +0,0 @@ -package sa - -import ( - "net" - "testing" -) - -func TestIncrementIP(t *testing.T) { - testCases := []struct { - ip string - index int - expected string - }{ - {"0.0.0.0", 128, "0.0.0.1"}, - {"0.0.0.255", 128, "0.0.1.0"}, - {"127.0.0.1", 128, "127.0.0.2"}, - {"1.2.3.4", 120, "1.2.4.4"}, - {"::1", 128, "::2"}, - {"2002:1001:4008::", 128, "2002:1001:4008::1"}, - {"2002:1001:4008::", 48, "2002:1001:4009::"}, - {"2002:1001:ffff::", 48, "2002:1002::"}, - {"ffff:ffff:ffff::", 48, "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"}, - } - for _, tc := range testCases { - ip := net.ParseIP(tc.ip).To16() - actual := incrementIP(ip, tc.index) - expectedIP := net.ParseIP(tc.expected) - if !actual.Equal(expectedIP) { - t.Errorf("Expected incrementIP(%s, %d) to be %s, instead got %s", - tc.ip, tc.index, expectedIP, actual.String()) - } - } -} - -func TestIPRange(t *testing.T) { - testCases := []struct { - ip string - expectedBegin string - expectedEnd string - }{ - {"28.45.45.28", "28.45.45.28", "28.45.45.29"}, - {"2002:1001:4008::", "2002:1001:4008::", "2002:1001:4009::"}, - } - for _, tc := range testCases { - ip := net.ParseIP(tc.ip) - expectedBegin := net.ParseIP(tc.expectedBegin) - expectedEnd := net.ParseIP(tc.expectedEnd) - actualBegin, actualEnd := ipRange(ip) - if !expectedBegin.Equal(actualBegin) || !expectedEnd.Equal(actualEnd) { - t.Errorf("Expected ipRange(%s) to be (%s, %s), got (%s, %s)", - tc.ip, tc.expectedBegin, tc.expectedEnd, actualBegin, actualEnd) - } - } -} diff --git a/third-party/github.com/letsencrypt/boulder/sa/model.go b/third-party/github.com/letsencrypt/boulder/sa/model.go index 19b6f569d..1fd481e9a 100644 --- a/third-party/github.com/letsencrypt/boulder/sa/model.go +++ b/third-party/github.com/letsencrypt/boulder/sa/model.go @@ -10,13 +10,15 @@ import ( "errors" "fmt" "math" - "net" + "net/netip" "net/url" "slices" "strconv" + "strings" "time" "github.com/go-jose/go-jose/v4" + "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/timestamppb" "github.com/letsencrypt/boulder/core" @@ -59,7 +61,7 @@ func badJSONError(msg string, jsonData []byte, err error) error { } } -const regFields = "id, jwk, jwk_sha256, contact, agreement, initialIP, createdAt, LockCol, status" +const regFields = "id, jwk, jwk_sha256, agreement, createdAt, LockCol, status" // ClearEmail removes the provided email address from one specified registration. If // there are multiple email addresses present, it does not modify other ones. If the email @@ -88,13 +90,33 @@ func ClearEmail(ctx context.Context, dbMap db.DatabaseMap, regID int64, email st return nil, nil } - currPb.Contact = newContacts - newModel, err := registrationPbToModel(currPb) + // We don't want to write literal JSON "null" strings into the database if the + // list of contact addresses is empty. Replace any possibly-`nil` slice with + // an empty JSON array. We don't need to check reg.ContactPresent, because + // we're going to write the whole object to the database anyway. + jsonContact := []byte("[]") + if len(newContacts) != 0 { + jsonContact, err = json.Marshal(newContacts) + if err != nil { + return nil, err + } + } + + // UPDATE the row with a direct database query, in order to avoid LockCol issues. + result, err := tx.ExecContext(ctx, + "UPDATE registrations SET contact = ? WHERE id = ? LIMIT 1", + jsonContact, + regID, + ) if err != nil { return nil, err } + rowsAffected, err := result.RowsAffected() + if err != nil || rowsAffected != 1 { + return nil, berrors.InternalServerError("no registration updated with new contact field") + } - return tx.Update(ctx, newModel) + return nil, nil }) if overallError != nil { return overallError @@ -119,65 +141,59 @@ func selectRegistration(ctx context.Context, s db.OneSelector, whereCol string, return &model, err } -const certFields = "registrationID, serial, digest, der, issued, expires" +const certFields = "id, registrationID, serial, digest, der, issued, expires" // SelectCertificate selects all fields of one certificate object identified by // a serial. If more than one row contains the same serial only the first is // returned. -func SelectCertificate(ctx context.Context, s db.OneSelector, serial string) (core.Certificate, error) { - var model core.Certificate +func SelectCertificate(ctx context.Context, s db.OneSelector, serial string) (*corepb.Certificate, error) { + var model certificateModel err := s.SelectOne( ctx, &model, "SELECT "+certFields+" FROM certificates WHERE serial = ? LIMIT 1", serial, ) - return model, err + return model.toPb(), err } const precertFields = "registrationID, serial, der, issued, expires" // SelectPrecertificate selects all fields of one precertificate object // identified by serial. -func SelectPrecertificate(ctx context.Context, s db.OneSelector, serial string) (core.Certificate, error) { - var model precertificateModel +func SelectPrecertificate(ctx context.Context, s db.OneSelector, serial string) (*corepb.Certificate, error) { + var model lintingCertModel err := s.SelectOne( ctx, &model, "SELECT "+precertFields+" FROM precertificates WHERE serial = ? LIMIT 1", serial) - return core.Certificate{ - RegistrationID: model.RegistrationID, - Serial: model.Serial, - DER: model.DER, - Issued: model.Issued, - Expires: model.Expires, - }, err -} - -type CertWithID struct { - ID int64 - core.Certificate + if err != nil { + return nil, err + } + return model.toPb(), nil } // SelectCertificates selects all fields of multiple certificate objects -func SelectCertificates(ctx context.Context, s db.Selector, q string, args map[string]interface{}) ([]CertWithID, error) { - var models []CertWithID +// +// Returns a slice of *corepb.Certificate along with the highest ID field seen +// (which can be used as input to a subsequent query when iterating in primary +// key order). +func SelectCertificates(ctx context.Context, s db.Selector, q string, args map[string]interface{}) ([]*corepb.Certificate, int64, error) { + var models []certificateModel _, err := s.Select( ctx, &models, - "SELECT id, "+certFields+" FROM certificates "+q, args) - return models, err -} - -// SelectPrecertificates selects all fields of multiple precertificate objects. -func SelectPrecertificates(ctx context.Context, s db.Selector, q string, args map[string]interface{}) ([]CertWithID, error) { - var models []CertWithID - _, err := s.Select( - ctx, - &models, - "SELECT id, "+precertFields+" FROM precertificates "+q, args) - return models, err + "SELECT "+certFields+" FROM certificates "+q, args) + var pbs []*corepb.Certificate + var highestID int64 + for _, m := range models { + pbs = append(pbs, m.toPb()) + if m.ID > highestID { + highestID = m.ID + } + } + return pbs, highestID, err } type CertStatusMetadata struct { @@ -197,15 +213,15 @@ const certStatusFields = "id, serial, status, ocspLastUpdated, revokedDate, revo // SelectCertificateStatus selects all fields of one certificate status model // identified by serial -func SelectCertificateStatus(ctx context.Context, s db.OneSelector, serial string) (core.CertificateStatus, error) { - var model core.CertificateStatus +func SelectCertificateStatus(ctx context.Context, s db.OneSelector, serial string) (*corepb.CertificateStatus, error) { + var model certificateStatusModel err := s.SelectOne( ctx, &model, "SELECT "+certStatusFields+" FROM certificateStatus WHERE serial = ? LIMIT 1", serial, ) - return model, err + return model.toPb(), err } // RevocationStatusModel represents a small subset of the columns in the @@ -254,14 +270,10 @@ type issuedNameModel struct { // regModel is the description of a core.Registration in the database before type regModel struct { - ID int64 `db:"id"` - Key []byte `db:"jwk"` - KeySHA256 string `db:"jwk_sha256"` - Contact string `db:"contact"` - Agreement string `db:"agreement"` - // InitialIP is stored as sixteen binary bytes, regardless of whether it - // represents a v4 or v6 IP address. - InitialIP []byte `db:"initialIp"` + ID int64 `db:"id"` + Key []byte `db:"jwk"` + KeySHA256 string `db:"jwk_sha256"` + Agreement string `db:"agreement"` CreatedAt time.Time `db:"createdAt"` LockCol int64 Status string `db:"status"` @@ -281,27 +293,6 @@ func registrationPbToModel(reg *corepb.Registration) (*regModel, error) { return nil, err } - // We don't want to write literal JSON "null" strings into the database if the - // list of contact addresses is empty. Replace any possibly-`nil` slice with - // an empty JSON array. We don't need to check reg.ContactPresent, because - // we're going to write the whole object to the database anyway. - jsonContact := []byte("[]") - if len(reg.Contact) != 0 { - jsonContact, err = json.Marshal(reg.Contact) - if err != nil { - return nil, err - } - } - - // For some reason we use different serialization formats for InitialIP - // in database models and in protobufs, despite the fact that both formats - // are just []byte. - var initialIP net.IP - err = initialIP.UnmarshalText(reg.InitialIP) - if err != nil { - return nil, err - } - var createdAt time.Time if !core.IsAnyNilOrZero(reg.CreatedAt) { createdAt = reg.CreatedAt.AsTime() @@ -311,48 +302,23 @@ func registrationPbToModel(reg *corepb.Registration) (*regModel, error) { ID: reg.Id, Key: reg.Key, KeySHA256: sha, - Contact: string(jsonContact), Agreement: reg.Agreement, - InitialIP: []byte(initialIP.To16()), CreatedAt: createdAt, Status: reg.Status, }, nil } func registrationModelToPb(reg *regModel) (*corepb.Registration, error) { - if reg.ID == 0 || len(reg.Key) == 0 || len(reg.InitialIP) == 0 { + if reg.ID == 0 || len(reg.Key) == 0 { return nil, errors.New("incomplete Registration retrieved from DB") } - contact := []string{} - contactsPresent := false - if len(reg.Contact) > 0 { - err := json.Unmarshal([]byte(reg.Contact), &contact) - if err != nil { - return nil, err - } - if len(contact) > 0 { - contactsPresent = true - } - } - - // For some reason we use different serialization formats for InitialIP - // in database models and in protobufs, despite the fact that both formats - // are just []byte. - ipBytes, err := net.IP(reg.InitialIP).MarshalText() - if err != nil { - return nil, err - } - return &corepb.Registration{ - Id: reg.ID, - Key: reg.Key, - Contact: contact, - ContactsPresent: contactsPresent, - Agreement: reg.Agreement, - InitialIP: ipBytes, - CreatedAt: timestamppb.New(reg.CreatedAt.UTC()), - Status: reg.Status, + Id: reg.ID, + Key: reg.Key, + Agreement: reg.Agreement, + CreatedAt: timestamppb.New(reg.CreatedAt.UTC()), + Status: reg.Status, }, nil } @@ -364,7 +330,7 @@ type recordedSerialModel struct { Expires time.Time } -type precertificateModel struct { +type lintingCertModel struct { ID int64 Serial string RegistrationID int64 @@ -373,18 +339,68 @@ type precertificateModel struct { Expires time.Time } -// TODO(#7324) orderModelv1 is deprecated, use orderModelv2 moving forward. -type orderModelv1 struct { - ID int64 - RegistrationID int64 - Expires time.Time - Created time.Time - Error []byte - CertificateSerial string - BeganProcessing bool +func (model lintingCertModel) toPb() *corepb.Certificate { + return &corepb.Certificate{ + RegistrationID: model.RegistrationID, + Serial: model.Serial, + Digest: "", + Der: model.DER, + Issued: timestamppb.New(model.Issued), + Expires: timestamppb.New(model.Expires), + } } -type orderModelv2 struct { +type certificateModel struct { + ID int64 `db:"id"` + RegistrationID int64 `db:"registrationID"` + Serial string `db:"serial"` + Digest string `db:"digest"` + DER []byte `db:"der"` + Issued time.Time `db:"issued"` + Expires time.Time `db:"expires"` +} + +func (model certificateModel) toPb() *corepb.Certificate { + return &corepb.Certificate{ + RegistrationID: model.RegistrationID, + Serial: model.Serial, + Digest: model.Digest, + Der: model.DER, + Issued: timestamppb.New(model.Issued), + Expires: timestamppb.New(model.Expires), + } +} + +type certificateStatusModel struct { + ID int64 `db:"id"` + Serial string `db:"serial"` + Status core.OCSPStatus `db:"status"` + OCSPLastUpdated time.Time `db:"ocspLastUpdated"` + RevokedDate time.Time `db:"revokedDate"` + RevokedReason revocation.Reason `db:"revokedReason"` + LastExpirationNagSent time.Time `db:"lastExpirationNagSent"` + NotAfter time.Time `db:"notAfter"` + IsExpired bool `db:"isExpired"` + IssuerID int64 `db:"issuerID"` +} + +func (model certificateStatusModel) toPb() *corepb.CertificateStatus { + return &corepb.CertificateStatus{ + Serial: model.Serial, + Status: string(model.Status), + OcspLastUpdated: timestamppb.New(model.OCSPLastUpdated), + RevokedDate: timestamppb.New(model.RevokedDate), + RevokedReason: int64(model.RevokedReason), + LastExpirationNagSent: timestamppb.New(model.LastExpirationNagSent), + NotAfter: timestamppb.New(model.NotAfter), + IsExpired: model.IsExpired, + IssuerID: model.IssuerID, + } +} + +// orderModel represents one row in the orders table. The CertificateProfileName +// column is a pointer because the column is NULL-able. +type orderModel struct { ID int64 RegistrationID int64 Expires time.Time @@ -392,7 +408,8 @@ type orderModelv2 struct { Error []byte CertificateSerial string BeganProcessing bool - CertificateProfileName string + CertificateProfileName *string + Replaces *string } type orderToAuthzModel struct { @@ -400,63 +417,20 @@ type orderToAuthzModel struct { AuthzID int64 } -// TODO(#7324) orderToModelv1 is deprecated, use orderModelv2 moving forward. -func orderToModelv1(order *corepb.Order) (*orderModelv1, error) { - om := &orderModelv1{ - ID: order.Id, - RegistrationID: order.RegistrationID, - Expires: order.Expires.AsTime(), - Created: order.Created.AsTime(), - BeganProcessing: order.BeganProcessing, - CertificateSerial: order.CertificateSerial, - } +func orderToModel(order *corepb.Order) (*orderModel, error) { + // Make a local copy so we can take a reference to it below. + profile := order.CertificateProfileName + replaces := order.Replaces - if order.Error != nil { - errJSON, err := json.Marshal(order.Error) - if err != nil { - return nil, err - } - if len(errJSON) > mediumBlobSize { - return nil, fmt.Errorf("Error object is too large to store in the database") - } - om.Error = errJSON - } - return om, nil -} - -// TODO(#7324) modelToOrderv1 is deprecated, use orderModelv2 moving forward. -func modelToOrderv1(om *orderModelv1) (*corepb.Order, error) { - order := &corepb.Order{ - Id: om.ID, - RegistrationID: om.RegistrationID, - Expires: timestamppb.New(om.Expires), - Created: timestamppb.New(om.Created), - CertificateSerial: om.CertificateSerial, - BeganProcessing: om.BeganProcessing, - } - if len(om.Error) > 0 { - var problem corepb.ProblemDetails - err := json.Unmarshal(om.Error, &problem) - if err != nil { - return &corepb.Order{}, badJSONError( - "failed to unmarshal order model's error", - om.Error, - err) - } - order.Error = &problem - } - return order, nil -} - -func orderToModelv2(order *corepb.Order) (*orderModelv2, error) { - om := &orderModelv2{ + om := &orderModel{ ID: order.Id, RegistrationID: order.RegistrationID, Expires: order.Expires.AsTime(), Created: order.Created.AsTime(), BeganProcessing: order.BeganProcessing, CertificateSerial: order.CertificateSerial, - CertificateProfileName: order.CertificateProfileName, + CertificateProfileName: &profile, + Replaces: &replaces, } if order.Error != nil { @@ -472,7 +446,15 @@ func orderToModelv2(order *corepb.Order) (*orderModelv2, error) { return om, nil } -func modelToOrderv2(om *orderModelv2) (*corepb.Order, error) { +func modelToOrder(om *orderModel) (*corepb.Order, error) { + profile := "" + if om.CertificateProfileName != nil { + profile = *om.CertificateProfileName + } + replaces := "" + if om.Replaces != nil { + replaces = *om.Replaces + } order := &corepb.Order{ Id: om.ID, RegistrationID: om.RegistrationID, @@ -480,7 +462,8 @@ func modelToOrderv2(om *orderModelv2) (*corepb.Order, error) { Created: timestamppb.New(om.Created), CertificateSerial: om.CertificateSerial, BeganProcessing: om.BeganProcessing, - CertificateProfileName: om.CertificateProfileName, + CertificateProfileName: profile, + Replaces: replaces, } if len(om.Error) > 0 { var problem corepb.ProblemDetails @@ -510,10 +493,12 @@ var uintToChallType = map[uint8]string{ var identifierTypeToUint = map[string]uint8{ "dns": 0, + "ip": 1, } -var uintToIdentifierType = map[uint8]string{ +var uintToIdentifierType = map[uint8]identifier.IdentifierType{ 0: "dns", + 1: "ip", } var statusToUint = map[core.AcmeStatus]uint8{ @@ -538,21 +523,24 @@ func statusUint(status core.AcmeStatus) uint8 { // authzFields is used in a variety of places in sa.go, and modifications to // it must be carried through to every use in sa.go -const authzFields = "id, identifierType, identifierValue, registrationID, status, expires, challenges, attempted, attemptedAt, token, validationError, validationRecord" +const authzFields = "id, identifierType, identifierValue, registrationID, certificateProfileName, status, expires, challenges, attempted, attemptedAt, token, validationError, validationRecord" +// authzModel represents one row in the authz2 table. The CertificateProfileName +// column is a pointer because the column is NULL-able. type authzModel struct { - ID int64 `db:"id"` - IdentifierType uint8 `db:"identifierType"` - IdentifierValue string `db:"identifierValue"` - RegistrationID int64 `db:"registrationID"` - Status uint8 `db:"status"` - Expires time.Time `db:"expires"` - Challenges uint8 `db:"challenges"` - Attempted *uint8 `db:"attempted"` - AttemptedAt *time.Time `db:"attemptedAt"` - Token []byte `db:"token"` - ValidationError []byte `db:"validationError"` - ValidationRecord []byte `db:"validationRecord"` + ID int64 `db:"id"` + IdentifierType uint8 `db:"identifierType"` + IdentifierValue string `db:"identifierValue"` + RegistrationID int64 `db:"registrationID"` + CertificateProfileName *string `db:"certificateProfileName"` + Status uint8 `db:"status"` + Expires time.Time `db:"expires"` + Challenges uint8 `db:"challenges"` + Attempted *uint8 `db:"attempted"` + AttemptedAt *time.Time `db:"attemptedAt"` + Token []byte `db:"token"` + ValidationError []byte `db:"validationError"` + ValidationRecord []byte `db:"validationRecord"` } // rehydrateHostPort mutates a validation record. If the URL in the validation @@ -624,29 +612,28 @@ func SelectAuthzsMatchingIssuance( s db.Selector, regID int64, issued time.Time, - dnsNames []string, + idents identifier.ACMEIdentifiers, ) ([]*corepb.Authorization, error) { + // The WHERE clause returned by this function does not contain any + // user-controlled strings; all user-controlled input ends up in the + // returned placeholder args. + identConditions, identArgs := buildIdentifierQueryConditions(idents) query := fmt.Sprintf(`SELECT %s FROM authz2 WHERE registrationID = ? AND status IN (?, ?) AND expires >= ? AND attemptedAt <= ? AND - identifierType = ? AND - identifierValue IN (%s)`, + (%s)`, authzFields, - db.QuestionMarks(len(dnsNames))) + identConditions) var args []any args = append(args, regID, - statusToUint[core.StatusValid], - statusToUint[core.StatusDeactivated], + statusToUint[core.StatusValid], statusToUint[core.StatusDeactivated], issued.Add(-1*time.Second), // leeway for clock skew issued.Add(1*time.Second), // leeway for clock skew - identifierTypeToUint[string(identifier.DNS)], ) - for _, name := range dnsNames { - args = append(args, name) - } + args = append(args, identArgs...) var authzModels []authzModel _, err := s.Select(ctx, &authzModels, query, args...) @@ -682,15 +669,54 @@ func hasMultipleNonPendingChallenges(challenges []*corepb.Challenge) bool { return false } +// newAuthzReqToModel converts an sapb.NewAuthzRequest to the authzModel storage +// representation. It hardcodes the status to "pending" because it should be +// impossible to create an authz in any other state. +func newAuthzReqToModel(authz *sapb.NewAuthzRequest, profile string) (*authzModel, error) { + am := &authzModel{ + IdentifierType: identifierTypeToUint[authz.Identifier.Type], + IdentifierValue: authz.Identifier.Value, + RegistrationID: authz.RegistrationID, + Status: statusToUint[core.StatusPending], + Expires: authz.Expires.AsTime(), + } + + if profile != "" { + am.CertificateProfileName = &profile + } + + for _, challType := range authz.ChallengeTypes { + // Set the challenge type bit in the bitmap + am.Challenges |= 1 << challTypeToUint[challType] + } + + token, err := base64.RawURLEncoding.DecodeString(authz.Token) + if err != nil { + return nil, err + } + am.Token = token + + return am, nil +} + // authzPBToModel converts a protobuf authorization representation to the // authzModel storage representation. +// Deprecated: this function is only used as part of test setup, do not +// introduce any new uses in production code. func authzPBToModel(authz *corepb.Authorization) (*authzModel, error) { + ident := identifier.FromProto(authz.Identifier) + am := &authzModel{ - IdentifierValue: authz.Identifier, + IdentifierType: identifierTypeToUint[ident.ToProto().Type], + IdentifierValue: ident.Value, RegistrationID: authz.RegistrationID, Status: statusToUint[core.AcmeStatus(authz.Status)], Expires: authz.Expires.AsTime(), } + if authz.CertificateProfileName != "" { + profile := authz.CertificateProfileName + am.CertificateProfileName = &profile + } if authz.Id != "" { // The v1 internal authorization objects use a string for the ID, the v2 // storage format uses a integer ID. In order to maintain compatibility we @@ -827,12 +853,23 @@ func populateAttemptedFields(am authzModel, challenge *corepb.Challenge) error { } func modelToAuthzPB(am authzModel) (*corepb.Authorization, error) { + identType, ok := uintToIdentifierType[am.IdentifierType] + if !ok { + return nil, fmt.Errorf("unrecognized identifier type encoding %d", am.IdentifierType) + } + + profile := "" + if am.CertificateProfileName != nil { + profile = *am.CertificateProfileName + } + pb := &corepb.Authorization{ - Id: fmt.Sprintf("%d", am.ID), - Status: string(uintToStatus[am.Status]), - Identifier: am.IdentifierValue, - RegistrationID: am.RegistrationID, - Expires: timestamppb.New(am.Expires), + Id: fmt.Sprintf("%d", am.ID), + Status: string(uintToStatus[am.Status]), + Identifier: identifier.ACMEIdentifier{Type: identType, Value: am.IdentifierValue}.ToProto(), + RegistrationID: am.RegistrationID, + Expires: timestamppb.New(am.Expires), + CertificateProfileName: profile, } // Populate authorization challenge array. We do this by iterating through // the challenge type bitmap and creating a challenge of each type if its @@ -938,9 +975,9 @@ type orderFQDNSet struct { Expires time.Time } -func addFQDNSet(ctx context.Context, db db.Inserter, names []string, serial string, issued time.Time, expires time.Time) error { +func addFQDNSet(ctx context.Context, db db.Inserter, idents identifier.ACMEIdentifiers, serial string, issued time.Time, expires time.Time) error { return db.Insert(ctx, &core.FQDNSet{ - SetHash: core.HashNames(names), + SetHash: core.HashIdentifiers(idents), Serial: serial, Issued: issued, Expires: expires, @@ -954,12 +991,12 @@ func addFQDNSet(ctx context.Context, db db.Inserter, names []string, serial stri func addOrderFQDNSet( ctx context.Context, db db.Inserter, - names []string, + idents identifier.ACMEIdentifiers, orderID int64, regID int64, expires time.Time) error { return db.Insert(ctx, &orderFQDNSet{ - SetHash: core.HashNames(names), + SetHash: core.HashIdentifiers(idents), OrderID: orderID, RegistrationID: regID, Expires: expires, @@ -995,28 +1032,64 @@ func deleteOrderFQDNSet( return nil } -func addIssuedNames(ctx context.Context, queryer db.Queryer, cert *x509.Certificate, isRenewal bool) error { - if len(cert.DNSNames) == 0 { - return berrors.InternalServerError("certificate has no DNSNames") +func addIssuedNames(ctx context.Context, queryer db.Execer, cert *x509.Certificate, isRenewal bool) error { + if len(cert.DNSNames) == 0 && len(cert.IPAddresses) == 0 { + return berrors.InternalServerError("certificate has no DNSNames or IPAddresses") } - multiInserter, err := db.NewMultiInserter("issuedNames", []string{"reversedName", "serial", "notBefore", "renewal"}, "") + multiInserter, err := db.NewMultiInserter("issuedNames", []string{"reversedName", "serial", "notBefore", "renewal"}) if err != nil { return err } for _, name := range cert.DNSNames { err = multiInserter.Add([]interface{}{ - ReverseName(name), + reverseFQDN(name), core.SerialToString(cert.SerialNumber), - cert.NotBefore, + cert.NotBefore.Truncate(24 * time.Hour), isRenewal, }) if err != nil { return err } } - _, err = multiInserter.Insert(ctx, queryer) - return err + for _, ip := range cert.IPAddresses { + err = multiInserter.Add([]interface{}{ + ip.String(), + core.SerialToString(cert.SerialNumber), + cert.NotBefore.Truncate(24 * time.Hour), + isRenewal, + }) + if err != nil { + return err + } + } + return multiInserter.Insert(ctx, queryer) +} + +// EncodeIssuedName translates a FQDN to/from the issuedNames table by reversing +// its dot-separated elements, and translates an IP address by returning its +// normal string form. +// +// This is for strings of ambiguous identifier values. If you know your string +// is a FQDN, use reverseFQDN(). If you have an IP address, use +// netip.Addr.String() or net.IP.String(). +func EncodeIssuedName(name string) string { + netIP, err := netip.ParseAddr(name) + if err == nil { + return netIP.String() + } + return reverseFQDN(name) +} + +// reverseFQDN reverses the elements of a dot-separated FQDN. +// +// If your string might be an IP address, use EncodeIssuedName() instead. +func reverseFQDN(fqdn string) string { + labels := strings.Split(fqdn, ".") + for i, j := 0, len(labels)-1; i < j; i, j = i+1, j-1 { + labels[i], labels[j] = labels[j], labels[i] + } + return strings.Join(labels, ".") } func addKeyHash(ctx context.Context, db db.Inserter, cert *x509.Certificate) error { @@ -1115,8 +1188,8 @@ func statusForOrder(order *corepb.Order, authzValidityInfo []authzValidity, now } // An order is fully authorized if it has valid authzs for each of the order - // names - fullyAuthorized := len(order.Names) == validAuthzs + // identifiers + fullyAuthorized := len(order.Identifiers) == validAuthzs // If the order isn't fully authorized we've encountered an internal error: // Above we checked for any invalid or pending authzs and should have returned @@ -1300,7 +1373,7 @@ type identifierModel struct { Value string `db:"identifierValue"` } -func newIdentifierModelFromPB(pb *sapb.Identifier) (identifierModel, error) { +func newIdentifierModelFromPB(pb *corepb.Identifier) (identifierModel, error) { idType, ok := identifierTypeToUint[pb.Type] if !ok { return identifierModel{}, fmt.Errorf("unsupported identifier type %q", pb.Type) @@ -1312,19 +1385,19 @@ func newIdentifierModelFromPB(pb *sapb.Identifier) (identifierModel, error) { }, nil } -func newPBFromIdentifierModel(id identifierModel) (*sapb.Identifier, error) { +func newPBFromIdentifierModel(id identifierModel) (*corepb.Identifier, error) { idType, ok := uintToIdentifierType[id.Type] if !ok { return nil, fmt.Errorf("unsupported identifier type %d", id.Type) } - return &sapb.Identifier{ - Type: idType, + return &corepb.Identifier{ + Type: string(idType), Value: id.Value, }, nil } -func newIdentifierModelsFromPB(pbs []*sapb.Identifier) ([]identifierModel, error) { +func newIdentifierModelsFromPB(pbs []*corepb.Identifier) ([]identifierModel, error) { ids := make([]identifierModel, 0, len(pbs)) for _, pb := range pbs { id, err := newIdentifierModelFromPB(pb) @@ -1337,7 +1410,7 @@ func newIdentifierModelsFromPB(pbs []*sapb.Identifier) ([]identifierModel, error } func newPBFromIdentifierModels(ids []identifierModel) (*sapb.Identifiers, error) { - pbs := make([]*sapb.Identifier, 0, len(ids)) + pbs := make([]*corepb.Identifier, 0, len(ids)) for _, id := range ids { pb, err := newPBFromIdentifierModel(id) if err != nil { @@ -1348,6 +1421,42 @@ func newPBFromIdentifierModels(ids []identifierModel) (*sapb.Identifiers, error) return &sapb.Identifiers{Identifiers: pbs}, nil } +// buildIdentifierQueryConditions takes a slice of identifiers and returns a +// string (conditions to use within the prepared statement) and a slice of anys +// (arguments for the prepared statement), both to use within a WHERE clause for +// queries against the authz2 table. +// +// Although this function takes user-controlled input, it does not include any +// of that input directly in the returned SQL string. The resulting string +// contains only column names, boolean operators, and questionmark placeholders. +func buildIdentifierQueryConditions(idents identifier.ACMEIdentifiers) (string, []any) { + if len(idents) == 0 { + // No identifier values to check. + return "FALSE", []any{} + } + + identsByType := map[identifier.IdentifierType][]string{} + for _, id := range idents { + identsByType[id.Type] = append(identsByType[id.Type], id.Value) + } + + var conditions []string + var args []any + for idType, idValues := range identsByType { + conditions = append(conditions, + fmt.Sprintf("identifierType = ? AND identifierValue IN (%s)", + db.QuestionMarks(len(idValues)), + ), + ) + args = append(args, identifierTypeToUint[string(idType)]) + for _, idValue := range idValues { + args = append(args, idValue) + } + } + + return strings.Join(conditions, " OR "), args +} + // pausedModel represents a row in the paused table. It contains the // registrationID of the paused account, the time the (account, identifier) pair // was paused, and the time the pair was unpaused. The UnpausedAt field is @@ -1360,3 +1469,38 @@ type pausedModel struct { PausedAt time.Time `db:"pausedAt"` UnpausedAt *time.Time `db:"unpausedAt"` } + +type overrideModel struct { + LimitEnum int64 `db:"limitEnum"` + BucketKey string `db:"bucketKey"` + Comment string `db:"comment"` + PeriodNS int64 `db:"periodNS"` + Count int64 `db:"count"` + Burst int64 `db:"burst"` + UpdatedAt time.Time `db:"updatedAt"` + Enabled bool `db:"enabled"` +} + +func overrideModelForPB(pb *sapb.RateLimitOverride, updatedAt time.Time, enabled bool) overrideModel { + return overrideModel{ + LimitEnum: pb.LimitEnum, + BucketKey: pb.BucketKey, + Comment: pb.Comment, + PeriodNS: pb.Period.AsDuration().Nanoseconds(), + Count: pb.Count, + Burst: pb.Burst, + UpdatedAt: updatedAt, + Enabled: enabled, + } +} + +func newPBFromOverrideModel(m *overrideModel) *sapb.RateLimitOverride { + return &sapb.RateLimitOverride{ + LimitEnum: m.LimitEnum, + BucketKey: m.BucketKey, + Comment: m.Comment, + Period: durationpb.New(time.Duration(m.PeriodNS)), + Count: m.Count, + Burst: m.Burst, + } +} diff --git a/third-party/github.com/letsencrypt/boulder/sa/model_test.go b/third-party/github.com/letsencrypt/boulder/sa/model_test.go index 23f4e3754..f5a1fe49a 100644 --- a/third-party/github.com/letsencrypt/boulder/sa/model_test.go +++ b/third-party/github.com/letsencrypt/boulder/sa/model_test.go @@ -2,16 +2,15 @@ package sa import ( "context" + "crypto/ecdsa" + "crypto/elliptic" "crypto/rand" - "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "database/sql" - "encoding/base64" "fmt" "math/big" - "net" - "os" + "net/netip" "testing" "time" @@ -19,8 +18,8 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" "github.com/letsencrypt/boulder/db" - "github.com/letsencrypt/boulder/features" "github.com/letsencrypt/boulder/grpc" + "github.com/letsencrypt/boulder/identifier" "github.com/letsencrypt/boulder/probs" "github.com/letsencrypt/boulder/test/vars" @@ -36,19 +35,11 @@ func TestRegistrationModelToPb(t *testing.T) { }{ { name: "No ID", - input: regModel{ID: 0, Key: []byte("foo"), InitialIP: []byte("foo")}, + input: regModel{ID: 0, Key: []byte("foo")}, }, { name: "No Key", - input: regModel{ID: 1, Key: nil, InitialIP: []byte("foo")}, - }, - { - name: "No IP", - input: regModel{ID: 1, Key: []byte("foo"), InitialIP: nil}, - }, - { - name: "Bad IP", - input: regModel{ID: 1, Key: []byte("foo"), InitialIP: []byte("foo")}, + input: regModel{ID: 1, Key: nil}, }, } for _, tc := range badCases { @@ -58,44 +49,48 @@ func TestRegistrationModelToPb(t *testing.T) { }) } - _, err := registrationModelToPb(®Model{ - ID: 1, Key: []byte("foo"), InitialIP: net.ParseIP("1.2.3.4"), - }) + _, err := registrationModelToPb(®Model{ID: 1, Key: []byte("foo")}) test.AssertNotError(t, err, "Should pass") } -func TestRegistrationPbToModel(t *testing.T) {} - func TestAuthzModel(t *testing.T) { - clk := clock.New() - now := clk.Now() - expires := now.Add(24 * time.Hour) - authzPB := &corepb.Authorization{ - Id: "1", - Identifier: "example.com", - RegistrationID: 1, - Status: string(core.StatusValid), - Expires: timestamppb.New(expires), - Challenges: []*corepb.Challenge{ - { - Type: string(core.ChallengeTypeHTTP01), - Status: string(core.StatusValid), - Token: "MTIz", - Validated: timestamppb.New(now), - Validationrecords: []*corepb.ValidationRecord{ - { - AddressUsed: []byte("1.2.3.4"), - Url: "https://example.com", - Hostname: "example.com", - Port: "443", - AddressesResolved: [][]byte{{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4}}, - AddressesTried: [][]byte{{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4}}, + // newTestAuthzPB returns a new *corepb.Authorization for `example.com` that + // is valid, and contains a single valid HTTP-01 challenge. These are the + // most common authorization attributes used in tests. Some tests will + // customize them after calling this. + newTestAuthzPB := func(validated time.Time) *corepb.Authorization { + return &corepb.Authorization{ + Id: "1", + Identifier: identifier.NewDNS("example.com").ToProto(), + RegistrationID: 1, + Status: string(core.StatusValid), + Expires: timestamppb.New(validated.Add(24 * time.Hour)), + Challenges: []*corepb.Challenge{ + { + Type: string(core.ChallengeTypeHTTP01), + Status: string(core.StatusValid), + Token: "MTIz", + Validated: timestamppb.New(validated), + Validationrecords: []*corepb.ValidationRecord{ + { + AddressUsed: []byte("1.2.3.4"), + Url: "https://example.com", + Hostname: "example.com", + Port: "443", + AddressesResolved: [][]byte{{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4}}, + AddressesTried: [][]byte{{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4}}, + }, }, }, }, - }, + } } + clk := clock.New() + + authzPB := newTestAuthzPB(clk.Now()) + authzPB.CertificateProfileName = "test" + model, err := authzPBToModel(authzPB) test.AssertNotError(t, err, "authzPBToModel failed") @@ -107,40 +102,15 @@ func TestAuthzModel(t *testing.T) { if authzPB.Challenges[0].Validationrecords[0].Port != "" { test.Assert(t, false, fmt.Sprintf("rehydrated http-01 validation record expected port field to be missing, but found %v", authzPB.Challenges[0].Validationrecords[0].Port)) } - // Shoving the Hostname and Port backinto the validation record should - // succeed because authzPB validation record will should match the retrieved + // Shoving the Hostname and Port back into the validation record should + // succeed because authzPB validation record should match the retrieved // model from the database with the rehydrated Hostname and Port. authzPB.Challenges[0].Validationrecords[0].Hostname = "example.com" authzPB.Challenges[0].Validationrecords[0].Port = "443" test.AssertDeepEquals(t, authzPB.Challenges, authzPBOut.Challenges) + test.AssertEquals(t, authzPBOut.CertificateProfileName, authzPB.CertificateProfileName) - now = clk.Now() - expires = now.Add(24 * time.Hour) - authzPB = &corepb.Authorization{ - Id: "1", - Identifier: "example.com", - RegistrationID: 1, - Status: string(core.StatusValid), - Expires: timestamppb.New(expires), - Challenges: []*corepb.Challenge{ - { - Type: string(core.ChallengeTypeHTTP01), - Status: string(core.StatusValid), - Token: "MTIz", - Validated: timestamppb.New(now), - Validationrecords: []*corepb.ValidationRecord{ - { - AddressUsed: []byte("1.2.3.4"), - Url: "https://example.com", - Hostname: "example.com", - Port: "443", - AddressesResolved: [][]byte{{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4}}, - AddressesTried: [][]byte{{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4}}, - }, - }, - }, - }, - } + authzPB = newTestAuthzPB(clk.Now()) validationErr := probs.Connection("weewoo") @@ -159,45 +129,38 @@ func TestAuthzModel(t *testing.T) { test.Assert(t, false, fmt.Sprintf("rehydrated http-01 validation record expected port field to be missing, but found %v", authzPB.Challenges[0].Validationrecords[0].Port)) } // Shoving the Hostname and Port back into the validation record should - // succeed because authzPB validation record will should match the retrieved + // succeed because authzPB validation record should match the retrieved // model from the database with the rehydrated Hostname and Port. authzPB.Challenges[0].Validationrecords[0].Hostname = "example.com" authzPB.Challenges[0].Validationrecords[0].Port = "443" test.AssertDeepEquals(t, authzPB.Challenges, authzPBOut.Challenges) - now = clk.Now() - expires = now.Add(24 * time.Hour) - authzPB = &corepb.Authorization{ - Id: "1", - Identifier: "example.com", - RegistrationID: 1, - Status: string(core.StatusInvalid), - Expires: timestamppb.New(expires), - Challenges: []*corepb.Challenge{ - { - Type: string(core.ChallengeTypeHTTP01), - Status: string(core.StatusInvalid), - Token: "MTIz", - Validationrecords: []*corepb.ValidationRecord{ - { - AddressUsed: []byte("1.2.3.4"), - Url: "url", - AddressesResolved: [][]byte{{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4}}, - AddressesTried: [][]byte{{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4}}, - }, + authzPB = newTestAuthzPB(clk.Now()) + authzPB.Status = string(core.StatusInvalid) + authzPB.Challenges = []*corepb.Challenge{ + { + Type: string(core.ChallengeTypeHTTP01), + Status: string(core.StatusInvalid), + Token: "MTIz", + Validationrecords: []*corepb.ValidationRecord{ + { + AddressUsed: []byte("1.2.3.4"), + Url: "url", + AddressesResolved: [][]byte{{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4}}, + AddressesTried: [][]byte{{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4}}, }, }, - { - Type: string(core.ChallengeTypeDNS01), - Status: string(core.StatusInvalid), - Token: "MTIz", - Validationrecords: []*corepb.ValidationRecord{ - { - AddressUsed: []byte("1.2.3.4"), - Url: "url", - AddressesResolved: [][]byte{{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4}}, - AddressesTried: [][]byte{{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4}}, - }, + }, + { + Type: string(core.ChallengeTypeDNS01), + Status: string(core.StatusInvalid), + Token: "MTIz", + Validationrecords: []*corepb.ValidationRecord{ + { + AddressUsed: []byte("1.2.3.4"), + Url: "url", + AddressesResolved: [][]byte{{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4}}, + AddressesTried: [][]byte{{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4}}, }, }, }, @@ -205,32 +168,9 @@ func TestAuthzModel(t *testing.T) { _, err = authzPBToModel(authzPB) test.AssertError(t, err, "authzPBToModel didn't fail with multiple non-pending challenges") - // Test that the caller Hostname and Port rehydration returns the expected data in the expected fields. - now = clk.Now() - expires = now.Add(24 * time.Hour) - authzPB = &corepb.Authorization{ - Id: "1", - Identifier: "example.com", - RegistrationID: 1, - Status: string(core.StatusValid), - Expires: timestamppb.New(expires), - Challenges: []*corepb.Challenge{ - { - Type: string(core.ChallengeTypeHTTP01), - Status: string(core.StatusValid), - Token: "MTIz", - Validated: timestamppb.New(now), - Validationrecords: []*corepb.ValidationRecord{ - { - AddressUsed: []byte("1.2.3.4"), - Url: "https://example.com", - AddressesResolved: [][]byte{{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4}}, - AddressesTried: [][]byte{{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4}}, - }, - }, - }, - }, - } + // Test that the caller Hostname and Port rehydration returns the expected + // data in the expected fields. + authzPB = newTestAuthzPB(clk.Now()) model, err = authzPBToModel(authzPB) test.AssertNotError(t, err, "authzPBToModel failed") @@ -243,13 +183,38 @@ func TestAuthzModel(t *testing.T) { if authzPBOut.Challenges[0].Validationrecords[0].Port != "443" { test.Assert(t, false, fmt.Sprintf("rehydrated http-01 validation record expected port 443 but found %v", authzPBOut.Challenges[0].Validationrecords[0].Port)) } + + authzPB = newTestAuthzPB(clk.Now()) + authzPB.Identifier = identifier.NewIP(netip.MustParseAddr("1.2.3.4")).ToProto() + authzPB.Challenges[0].Validationrecords[0].Url = "https://1.2.3.4" + authzPB.Challenges[0].Validationrecords[0].Hostname = "1.2.3.4" + + model, err = authzPBToModel(authzPB) + test.AssertNotError(t, err, "authzPBToModel failed") + authzPBOut, err = modelToAuthzPB(*model) + test.AssertNotError(t, err, "modelToAuthzPB failed") + + identOut := identifier.FromProto(authzPBOut.Identifier) + if identOut.Type != identifier.TypeIP { + test.Assert(t, false, fmt.Sprintf("expected identifier type ip but found %s", identOut.Type)) + } + if identOut.Value != "1.2.3.4" { + test.Assert(t, false, fmt.Sprintf("expected identifier value 1.2.3.4 but found %s", identOut.Value)) + } + + if authzPBOut.Challenges[0].Validationrecords[0].Hostname != "1.2.3.4" { + test.Assert(t, false, fmt.Sprintf("rehydrated http-01 validation record expected hostname 1.2.3.4 but found %v", authzPBOut.Challenges[0].Validationrecords[0].Hostname)) + } + if authzPBOut.Challenges[0].Validationrecords[0].Port != "443" { + test.Assert(t, false, fmt.Sprintf("rehydrated http-01 validation record expected port 443 but found %v", authzPBOut.Challenges[0].Validationrecords[0].Port)) + } } // TestModelToOrderBADJSON tests that converting an order model with an invalid // validation error JSON field to an Order produces the expected bad JSON error. func TestModelToOrderBadJSON(t *testing.T) { badJSON := []byte(`{`) - _, err := modelToOrderv2(&orderModelv2{ + _, err := modelToOrder(&orderModel{ Error: badJSON, }) test.AssertError(t, err, "expected error from modelToOrderv2") @@ -262,21 +227,6 @@ func TestOrderModelThereAndBackAgain(t *testing.T) { clk := clock.New() now := clk.Now() order := &corepb.Order{ - Id: 0, - RegistrationID: 2016, - Expires: timestamppb.New(now.Add(24 * time.Hour)), - Created: timestamppb.New(now), - Error: nil, - CertificateSerial: "1", - BeganProcessing: true, - } - model1, err := orderToModelv1(order) - test.AssertNotError(t, err, "orderToModelv1 should not have errored") - returnOrder, err := modelToOrderv1(model1) - test.AssertNotError(t, err, "modelToOrderv1 should not have errored") - test.AssertDeepEquals(t, order, returnOrder) - - anotherOrder := &corepb.Order{ Id: 1, RegistrationID: 2024, Expires: timestamppb.New(now.Add(24 * time.Hour)), @@ -286,11 +236,11 @@ func TestOrderModelThereAndBackAgain(t *testing.T) { BeganProcessing: true, CertificateProfileName: "phljny", } - model2, err := orderToModelv2(anotherOrder) + model, err := orderToModel(order) test.AssertNotError(t, err, "orderToModelv2 should not have errored") - returnOrder, err = modelToOrderv2(model2) + returnOrder, err := modelToOrder(model) test.AssertNotError(t, err, "modelToOrderv2 should not have errored") - test.AssertDeepEquals(t, anotherOrder, returnOrder) + test.AssertDeepEquals(t, order, returnOrder) } // TestPopulateAttemptedFieldsBadJSON tests that populating a challenge from an @@ -353,7 +303,7 @@ func TestCertificatesTableContainsDuplicateSerials(t *testing.T) { test.AssertNotError(t, err, "received an error for a valid query") // Ensure that `certA` and `certB` are the same. - test.AssertByteEquals(t, certA.DER, certB.DER) + test.AssertByteEquals(t, certA.Der, certB.Der) } func insertCertificate(ctx context.Context, dbMap *db.WrappedMap, fc clock.FakeClock, hostname, cn string, serial, regID int64) error { @@ -369,37 +319,27 @@ func insertCertificate(ctx context.Context, dbMap *db.WrappedMap, fc clock.FakeC SerialNumber: serialBigInt, } - testKey := makeKey() - certDer, _ := x509.CreateCertificate(rand.Reader, &template, &template, &testKey.PublicKey, &testKey) + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return fmt.Errorf("generating test key: %w", err) + } + certDer, err := x509.CreateCertificate(rand.Reader, &template, &template, key.Public(), key) + if err != nil { + return fmt.Errorf("generating test cert: %w", err) + } cert := &core.Certificate{ RegistrationID: regID, Serial: serialString, Expires: template.NotAfter, DER: certDer, } - err := dbMap.Insert(ctx, cert) + err = dbMap.Insert(ctx, cert) if err != nil { return err } return nil } -func bigIntFromB64(b64 string) *big.Int { - bytes, _ := base64.URLEncoding.DecodeString(b64) - x := big.NewInt(0) - x.SetBytes(bytes) - return x -} - -func makeKey() rsa.PrivateKey { - n := bigIntFromB64("n4EPtAOCc9AlkeQHPzHStgAbgs7bTZLwUBZdR8_KuKPEHLd4rHVTeT-O-XV2jRojdNhxJWTDvNd7nqQ0VEiZQHz_AJmSCpMaJMRBSFKrKb2wqVwGU_NsYOYL-QtiWN2lbzcEe6XC0dApr5ydQLrHqkHHig3RBordaZ6Aj-oBHqFEHYpPe7Tpe-OfVfHd1E6cS6M1FZcD1NNLYD5lFHpPI9bTwJlsde3uhGqC0ZCuEHg8lhzwOHrtIQbS0FVbb9k3-tVTU4fg_3L_vniUFAKwuCLqKnS2BYwdq_mzSnbLY7h_qixoR7jig3__kRhuaxwUkRz5iaiQkqgc5gHdrNP5zw==") - e := int(bigIntFromB64("AQAB").Int64()) - 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=") - return rsa.PrivateKey{PublicKey: rsa.PublicKey{N: n, E: e}, D: d, Primes: []*big.Int{p, q}} -} - func TestIncidentSerialModel(t *testing.T) { ctx := context.Background() @@ -454,16 +394,9 @@ func TestIncidentSerialModel(t *testing.T) { } func TestAddReplacementOrder(t *testing.T) { - if os.Getenv("BOULDER_CONFIG_DIR") != "test/config-next" { - t.Skip("Test requires replacementOrders database table") - } - sa, _, cleanUp := initSA(t) defer cleanUp() - features.Set(features.Config{TrackReplacementCertificatesARI: true}) - defer features.Reset() - oldCertSerial := "1234567890" orderId := int64(1337) orderExpires := time.Now().Add(24 * time.Hour).UTC().Truncate(time.Second) @@ -506,16 +439,9 @@ func TestAddReplacementOrder(t *testing.T) { } func TestSetReplacementOrderFinalized(t *testing.T) { - if os.Getenv("BOULDER_CONFIG_DIR") != "test/config-next" { - t.Skip("Test requires replacementOrders database table") - } - sa, _, cleanUp := initSA(t) defer cleanUp() - features.Set(features.Config{TrackReplacementCertificatesARI: true}) - defer features.Reset() - oldCertSerial := "1234567890" orderId := int64(1337) orderExpires := time.Now().Add(24 * time.Hour).UTC().Truncate(time.Second) diff --git a/third-party/github.com/letsencrypt/boulder/sa/proto/sa.pb.go b/third-party/github.com/letsencrypt/boulder/sa/proto/sa.pb.go index e938545de..8fa5f9b27 100644 --- a/third-party/github.com/letsencrypt/boulder/sa/proto/sa.pb.go +++ b/third-party/github.com/letsencrypt/boulder/sa/proto/sa.pb.go @@ -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: sa.proto @@ -15,6 +15,7 @@ import ( timestamppb "google.golang.org/protobuf/types/known/timestamppb" reflect "reflect" sync "sync" + unsafe "unsafe" ) const ( @@ -25,20 +26,17 @@ const ( ) type RegistrationID struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` unknownFields protoimpl.UnknownFields - - Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + sizeCache protoimpl.SizeCache } func (x *RegistrationID) Reset() { *x = RegistrationID{} - if protoimpl.UnsafeEnabled { - mi := &file_sa_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_sa_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *RegistrationID) String() string { @@ -49,7 +47,7 @@ func (*RegistrationID) ProtoMessage() {} func (x *RegistrationID) ProtoReflect() protoreflect.Message { mi := &file_sa_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) @@ -72,20 +70,17 @@ func (x *RegistrationID) GetId() int64 { } type JSONWebKey struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Jwk []byte `protobuf:"bytes,1,opt,name=jwk,proto3" json:"jwk,omitempty"` unknownFields protoimpl.UnknownFields - - Jwk []byte `protobuf:"bytes,1,opt,name=jwk,proto3" json:"jwk,omitempty"` + sizeCache protoimpl.SizeCache } func (x *JSONWebKey) Reset() { *x = JSONWebKey{} - if protoimpl.UnsafeEnabled { - mi := &file_sa_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_sa_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *JSONWebKey) String() string { @@ -96,7 +91,7 @@ func (*JSONWebKey) ProtoMessage() {} func (x *JSONWebKey) ProtoReflect() protoreflect.Message { mi := &file_sa_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) @@ -119,20 +114,17 @@ func (x *JSONWebKey) GetJwk() []byte { } type AuthorizationID struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` unknownFields protoimpl.UnknownFields - - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + sizeCache protoimpl.SizeCache } func (x *AuthorizationID) Reset() { *x = AuthorizationID{} - if protoimpl.UnsafeEnabled { - mi := &file_sa_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_sa_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *AuthorizationID) String() string { @@ -143,7 +135,7 @@ func (*AuthorizationID) ProtoMessage() {} func (x *AuthorizationID) ProtoReflect() protoreflect.Message { mi := &file_sa_proto_msgTypes[2] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -165,96 +157,22 @@ func (x *AuthorizationID) GetId() string { return "" } -type GetPendingAuthorizationRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - // Next unused field number: 6 - RegistrationID int64 `protobuf:"varint,1,opt,name=registrationID,proto3" json:"registrationID,omitempty"` - IdentifierType string `protobuf:"bytes,2,opt,name=identifierType,proto3" json:"identifierType,omitempty"` - IdentifierValue string `protobuf:"bytes,3,opt,name=identifierValue,proto3" json:"identifierValue,omitempty"` - ValidUntil *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=validUntil,proto3" json:"validUntil,omitempty"` // Result must be valid until at least this timestamp -} - -func (x *GetPendingAuthorizationRequest) Reset() { - *x = GetPendingAuthorizationRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_sa_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *GetPendingAuthorizationRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetPendingAuthorizationRequest) ProtoMessage() {} - -func (x *GetPendingAuthorizationRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[3] - 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 GetPendingAuthorizationRequest.ProtoReflect.Descriptor instead. -func (*GetPendingAuthorizationRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{3} -} - -func (x *GetPendingAuthorizationRequest) GetRegistrationID() int64 { - if x != nil { - return x.RegistrationID - } - return 0 -} - -func (x *GetPendingAuthorizationRequest) GetIdentifierType() string { - if x != nil { - return x.IdentifierType - } - return "" -} - -func (x *GetPendingAuthorizationRequest) GetIdentifierValue() string { - if x != nil { - return x.IdentifierValue - } - return "" -} - -func (x *GetPendingAuthorizationRequest) GetValidUntil() *timestamppb.Timestamp { - if x != nil { - return x.ValidUntil - } - return nil -} - type GetValidAuthorizationsRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - // Next unused field number: 5 + state protoimpl.MessageState `protogen:"open.v1"` + // Next unused field number: 7 RegistrationID int64 `protobuf:"varint,1,opt,name=registrationID,proto3" json:"registrationID,omitempty"` - Domains []string `protobuf:"bytes,2,rep,name=domains,proto3" json:"domains,omitempty"` - Now *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=now,proto3" json:"now,omitempty"` + Identifiers []*proto.Identifier `protobuf:"bytes,6,rep,name=identifiers,proto3" json:"identifiers,omitempty"` + ValidUntil *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=validUntil,proto3" json:"validUntil,omitempty"` + Profile string `protobuf:"bytes,5,opt,name=profile,proto3" json:"profile,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GetValidAuthorizationsRequest) Reset() { *x = GetValidAuthorizationsRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_sa_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_sa_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *GetValidAuthorizationsRequest) String() string { @@ -264,8 +182,8 @@ func (x *GetValidAuthorizationsRequest) String() string { func (*GetValidAuthorizationsRequest) ProtoMessage() {} func (x *GetValidAuthorizationsRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[4] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_sa_proto_msgTypes[3] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -277,7 +195,7 @@ func (x *GetValidAuthorizationsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetValidAuthorizationsRequest.ProtoReflect.Descriptor instead. func (*GetValidAuthorizationsRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{4} + return file_sa_proto_rawDescGZIP(), []int{3} } func (x *GetValidAuthorizationsRequest) GetRegistrationID() int64 { @@ -287,82 +205,39 @@ func (x *GetValidAuthorizationsRequest) GetRegistrationID() int64 { return 0 } -func (x *GetValidAuthorizationsRequest) GetDomains() []string { +func (x *GetValidAuthorizationsRequest) GetIdentifiers() []*proto.Identifier { if x != nil { - return x.Domains + return x.Identifiers } return nil } -func (x *GetValidAuthorizationsRequest) GetNow() *timestamppb.Timestamp { +func (x *GetValidAuthorizationsRequest) GetValidUntil() *timestamppb.Timestamp { if x != nil { - return x.Now + return x.ValidUntil } return nil } -type ValidAuthorizations struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Valid []*ValidAuthorizations_MapElement `protobuf:"bytes,1,rep,name=valid,proto3" json:"valid,omitempty"` -} - -func (x *ValidAuthorizations) Reset() { - *x = ValidAuthorizations{} - if protoimpl.UnsafeEnabled { - mi := &file_sa_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *ValidAuthorizations) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ValidAuthorizations) ProtoMessage() {} - -func (x *ValidAuthorizations) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[5] - 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 ValidAuthorizations.ProtoReflect.Descriptor instead. -func (*ValidAuthorizations) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{5} -} - -func (x *ValidAuthorizations) GetValid() []*ValidAuthorizations_MapElement { +func (x *GetValidAuthorizationsRequest) GetProfile() string { if x != nil { - return x.Valid + return x.Profile } - return nil + return "" } type Serial struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Serial string `protobuf:"bytes,1,opt,name=serial,proto3" json:"serial,omitempty"` unknownFields protoimpl.UnknownFields - - Serial string `protobuf:"bytes,1,opt,name=serial,proto3" json:"serial,omitempty"` + sizeCache protoimpl.SizeCache } func (x *Serial) Reset() { *x = Serial{} - if protoimpl.UnsafeEnabled { - mi := &file_sa_proto_msgTypes[6] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_sa_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Serial) String() string { @@ -372,8 +247,8 @@ func (x *Serial) String() string { func (*Serial) ProtoMessage() {} func (x *Serial) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[6] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_sa_proto_msgTypes[4] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -385,7 +260,7 @@ func (x *Serial) ProtoReflect() protoreflect.Message { // Deprecated: Use Serial.ProtoReflect.Descriptor instead. func (*Serial) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{6} + return file_sa_proto_rawDescGZIP(), []int{4} } func (x *Serial) GetSerial() string { @@ -396,24 +271,21 @@ func (x *Serial) GetSerial() string { } type SerialMetadata struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // Next unused field number: 7 Serial string `protobuf:"bytes,1,opt,name=serial,proto3" json:"serial,omitempty"` RegistrationID int64 `protobuf:"varint,2,opt,name=registrationID,proto3" json:"registrationID,omitempty"` Created *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=created,proto3" json:"created,omitempty"` Expires *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=expires,proto3" json:"expires,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *SerialMetadata) Reset() { *x = SerialMetadata{} - if protoimpl.UnsafeEnabled { - mi := &file_sa_proto_msgTypes[7] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_sa_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *SerialMetadata) String() string { @@ -423,8 +295,8 @@ func (x *SerialMetadata) String() string { func (*SerialMetadata) ProtoMessage() {} func (x *SerialMetadata) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[7] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_sa_proto_msgTypes[5] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -436,7 +308,7 @@ func (x *SerialMetadata) ProtoReflect() protoreflect.Message { // Deprecated: Use SerialMetadata.ProtoReflect.Descriptor instead. func (*SerialMetadata) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{7} + return file_sa_proto_rawDescGZIP(), []int{5} } func (x *SerialMetadata) GetSerial() string { @@ -468,21 +340,18 @@ func (x *SerialMetadata) GetExpires() *timestamppb.Timestamp { } type Range struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Earliest *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=earliest,proto3" json:"earliest,omitempty"` + Latest *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=latest,proto3" json:"latest,omitempty"` unknownFields protoimpl.UnknownFields - - Earliest *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=earliest,proto3" json:"earliest,omitempty"` - Latest *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=latest,proto3" json:"latest,omitempty"` + sizeCache protoimpl.SizeCache } func (x *Range) Reset() { *x = Range{} - if protoimpl.UnsafeEnabled { - mi := &file_sa_proto_msgTypes[8] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_sa_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Range) String() string { @@ -492,8 +361,8 @@ func (x *Range) String() string { func (*Range) ProtoMessage() {} func (x *Range) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[8] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_sa_proto_msgTypes[6] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -505,7 +374,7 @@ func (x *Range) ProtoReflect() protoreflect.Message { // Deprecated: Use Range.ProtoReflect.Descriptor instead. func (*Range) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{8} + return file_sa_proto_rawDescGZIP(), []int{6} } func (x *Range) GetEarliest() *timestamppb.Timestamp { @@ -523,20 +392,17 @@ func (x *Range) GetLatest() *timestamppb.Timestamp { } type Count struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Count int64 `protobuf:"varint,1,opt,name=count,proto3" json:"count,omitempty"` unknownFields protoimpl.UnknownFields - - Count int64 `protobuf:"varint,1,opt,name=count,proto3" json:"count,omitempty"` + sizeCache protoimpl.SizeCache } func (x *Count) Reset() { *x = Count{} - if protoimpl.UnsafeEnabled { - mi := &file_sa_proto_msgTypes[9] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_sa_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Count) String() string { @@ -546,8 +412,8 @@ func (x *Count) String() string { func (*Count) ProtoMessage() {} func (x *Count) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[9] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_sa_proto_msgTypes[7] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -559,7 +425,7 @@ func (x *Count) ProtoReflect() protoreflect.Message { // Deprecated: Use Count.ProtoReflect.Descriptor instead. func (*Count) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{9} + return file_sa_proto_rawDescGZIP(), []int{7} } func (x *Count) GetCount() int64 { @@ -570,20 +436,17 @@ func (x *Count) GetCount() int64 { } type Timestamps struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Timestamps []*timestamppb.Timestamp `protobuf:"bytes,2,rep,name=timestamps,proto3" json:"timestamps,omitempty"` unknownFields protoimpl.UnknownFields - - Timestamps []*timestamppb.Timestamp `protobuf:"bytes,2,rep,name=timestamps,proto3" json:"timestamps,omitempty"` + sizeCache protoimpl.SizeCache } func (x *Timestamps) Reset() { *x = Timestamps{} - if protoimpl.UnsafeEnabled { - mi := &file_sa_proto_msgTypes[10] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_sa_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Timestamps) String() string { @@ -593,8 +456,8 @@ func (x *Timestamps) String() string { func (*Timestamps) ProtoMessage() {} func (x *Timestamps) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[10] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_sa_proto_msgTypes[8] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -606,7 +469,7 @@ func (x *Timestamps) ProtoReflect() protoreflect.Message { // Deprecated: Use Timestamps.ProtoReflect.Descriptor instead. func (*Timestamps) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{10} + return file_sa_proto_rawDescGZIP(), []int{8} } func (x *Timestamps) GetTimestamps() []*timestamppb.Timestamp { @@ -616,189 +479,22 @@ func (x *Timestamps) GetTimestamps() []*timestamppb.Timestamp { return nil } -type CountCertificatesByNamesRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Range *Range `protobuf:"bytes,1,opt,name=range,proto3" json:"range,omitempty"` - Names []string `protobuf:"bytes,2,rep,name=names,proto3" json:"names,omitempty"` -} - -func (x *CountCertificatesByNamesRequest) Reset() { - *x = CountCertificatesByNamesRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_sa_proto_msgTypes[11] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *CountCertificatesByNamesRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*CountCertificatesByNamesRequest) ProtoMessage() {} - -func (x *CountCertificatesByNamesRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[11] - 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 CountCertificatesByNamesRequest.ProtoReflect.Descriptor instead. -func (*CountCertificatesByNamesRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{11} -} - -func (x *CountCertificatesByNamesRequest) GetRange() *Range { - if x != nil { - return x.Range - } - return nil -} - -func (x *CountCertificatesByNamesRequest) GetNames() []string { - if x != nil { - return x.Names - } - return nil -} - -type CountByNames struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Counts map[string]int64 `protobuf:"bytes,1,rep,name=counts,proto3" json:"counts,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"varint,2,opt,name=value,proto3"` - Earliest *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=earliest,proto3" json:"earliest,omitempty"` // Unix timestamp (nanoseconds) -} - -func (x *CountByNames) Reset() { - *x = CountByNames{} - if protoimpl.UnsafeEnabled { - mi := &file_sa_proto_msgTypes[12] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *CountByNames) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*CountByNames) ProtoMessage() {} - -func (x *CountByNames) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[12] - 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 CountByNames.ProtoReflect.Descriptor instead. -func (*CountByNames) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{12} -} - -func (x *CountByNames) GetCounts() map[string]int64 { - if x != nil { - return x.Counts - } - return nil -} - -func (x *CountByNames) GetEarliest() *timestamppb.Timestamp { - if x != nil { - return x.Earliest - } - return nil -} - -type CountRegistrationsByIPRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Ip []byte `protobuf:"bytes,1,opt,name=ip,proto3" json:"ip,omitempty"` - Range *Range `protobuf:"bytes,2,opt,name=range,proto3" json:"range,omitempty"` -} - -func (x *CountRegistrationsByIPRequest) Reset() { - *x = CountRegistrationsByIPRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_sa_proto_msgTypes[13] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *CountRegistrationsByIPRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*CountRegistrationsByIPRequest) ProtoMessage() {} - -func (x *CountRegistrationsByIPRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[13] - 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 CountRegistrationsByIPRequest.ProtoReflect.Descriptor instead. -func (*CountRegistrationsByIPRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{13} -} - -func (x *CountRegistrationsByIPRequest) GetIp() []byte { - if x != nil { - return x.Ip - } - return nil -} - -func (x *CountRegistrationsByIPRequest) GetRange() *Range { - if x != nil { - return x.Range - } - return nil -} - type CountInvalidAuthorizationsRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - RegistrationID int64 `protobuf:"varint,1,opt,name=registrationID,proto3" json:"registrationID,omitempty"` - Hostname string `protobuf:"bytes,2,opt,name=hostname,proto3" json:"hostname,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + // Next unused field number: 5 + RegistrationID int64 `protobuf:"varint,1,opt,name=registrationID,proto3" json:"registrationID,omitempty"` + Identifier *proto.Identifier `protobuf:"bytes,4,opt,name=identifier,proto3" json:"identifier,omitempty"` // Count authorizations that expire in this range. - Range *Range `protobuf:"bytes,3,opt,name=range,proto3" json:"range,omitempty"` + Range *Range `protobuf:"bytes,3,opt,name=range,proto3" json:"range,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *CountInvalidAuthorizationsRequest) Reset() { *x = CountInvalidAuthorizationsRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_sa_proto_msgTypes[14] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_sa_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *CountInvalidAuthorizationsRequest) String() string { @@ -808,8 +504,8 @@ func (x *CountInvalidAuthorizationsRequest) String() string { func (*CountInvalidAuthorizationsRequest) ProtoMessage() {} func (x *CountInvalidAuthorizationsRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[14] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_sa_proto_msgTypes[9] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -821,7 +517,7 @@ func (x *CountInvalidAuthorizationsRequest) ProtoReflect() protoreflect.Message // Deprecated: Use CountInvalidAuthorizationsRequest.ProtoReflect.Descriptor instead. func (*CountInvalidAuthorizationsRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{14} + return file_sa_proto_rawDescGZIP(), []int{9} } func (x *CountInvalidAuthorizationsRequest) GetRegistrationID() int64 { @@ -831,11 +527,11 @@ func (x *CountInvalidAuthorizationsRequest) GetRegistrationID() int64 { return 0 } -func (x *CountInvalidAuthorizationsRequest) GetHostname() string { +func (x *CountInvalidAuthorizationsRequest) GetIdentifier() *proto.Identifier { if x != nil { - return x.Hostname + return x.Identifier } - return "" + return nil } func (x *CountInvalidAuthorizationsRequest) GetRange() *Range { @@ -845,77 +541,20 @@ func (x *CountInvalidAuthorizationsRequest) GetRange() *Range { return nil } -type CountOrdersRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - AccountID int64 `protobuf:"varint,1,opt,name=accountID,proto3" json:"accountID,omitempty"` - Range *Range `protobuf:"bytes,2,opt,name=range,proto3" json:"range,omitempty"` -} - -func (x *CountOrdersRequest) Reset() { - *x = CountOrdersRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_sa_proto_msgTypes[15] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *CountOrdersRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*CountOrdersRequest) ProtoMessage() {} - -func (x *CountOrdersRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[15] - 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 CountOrdersRequest.ProtoReflect.Descriptor instead. -func (*CountOrdersRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{15} -} - -func (x *CountOrdersRequest) GetAccountID() int64 { - if x != nil { - return x.AccountID - } - return 0 -} - -func (x *CountOrdersRequest) GetRange() *Range { - if x != nil { - return x.Range - } - return nil -} - type CountFQDNSetsRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Identifiers []*proto.Identifier `protobuf:"bytes,5,rep,name=identifiers,proto3" json:"identifiers,omitempty"` + Window *durationpb.Duration `protobuf:"bytes,3,opt,name=window,proto3" json:"window,omitempty"` + Limit int64 `protobuf:"varint,4,opt,name=limit,proto3" json:"limit,omitempty"` unknownFields protoimpl.UnknownFields - - Domains []string `protobuf:"bytes,2,rep,name=domains,proto3" json:"domains,omitempty"` - Window *durationpb.Duration `protobuf:"bytes,3,opt,name=window,proto3" json:"window,omitempty"` + sizeCache protoimpl.SizeCache } func (x *CountFQDNSetsRequest) Reset() { *x = CountFQDNSetsRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_sa_proto_msgTypes[16] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_sa_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *CountFQDNSetsRequest) String() string { @@ -925,8 +564,8 @@ func (x *CountFQDNSetsRequest) String() string { func (*CountFQDNSetsRequest) ProtoMessage() {} func (x *CountFQDNSetsRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[16] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_sa_proto_msgTypes[10] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -938,12 +577,12 @@ func (x *CountFQDNSetsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CountFQDNSetsRequest.ProtoReflect.Descriptor instead. func (*CountFQDNSetsRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{16} + return file_sa_proto_rawDescGZIP(), []int{10} } -func (x *CountFQDNSetsRequest) GetDomains() []string { +func (x *CountFQDNSetsRequest) GetIdentifiers() []*proto.Identifier { if x != nil { - return x.Domains + return x.Identifiers } return nil } @@ -955,21 +594,25 @@ func (x *CountFQDNSetsRequest) GetWindow() *durationpb.Duration { return nil } -type FQDNSetExistsRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields +func (x *CountFQDNSetsRequest) GetLimit() int64 { + if x != nil { + return x.Limit + } + return 0 +} - Domains []string `protobuf:"bytes,1,rep,name=domains,proto3" json:"domains,omitempty"` +type FQDNSetExistsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Identifiers []*proto.Identifier `protobuf:"bytes,2,rep,name=identifiers,proto3" json:"identifiers,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *FQDNSetExistsRequest) Reset() { *x = FQDNSetExistsRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_sa_proto_msgTypes[17] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_sa_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *FQDNSetExistsRequest) String() string { @@ -979,8 +622,8 @@ func (x *FQDNSetExistsRequest) String() string { func (*FQDNSetExistsRequest) ProtoMessage() {} func (x *FQDNSetExistsRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[17] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_sa_proto_msgTypes[11] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -992,31 +635,28 @@ func (x *FQDNSetExistsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use FQDNSetExistsRequest.ProtoReflect.Descriptor instead. func (*FQDNSetExistsRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{17} + return file_sa_proto_rawDescGZIP(), []int{11} } -func (x *FQDNSetExistsRequest) GetDomains() []string { +func (x *FQDNSetExistsRequest) GetIdentifiers() []*proto.Identifier { if x != nil { - return x.Domains + return x.Identifiers } return nil } type Exists struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Exists bool `protobuf:"varint,1,opt,name=exists,proto3" json:"exists,omitempty"` unknownFields protoimpl.UnknownFields - - Exists bool `protobuf:"varint,1,opt,name=exists,proto3" json:"exists,omitempty"` + sizeCache protoimpl.SizeCache } func (x *Exists) Reset() { *x = Exists{} - if protoimpl.UnsafeEnabled { - mi := &file_sa_proto_msgTypes[18] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_sa_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Exists) String() string { @@ -1026,8 +666,8 @@ func (x *Exists) String() string { func (*Exists) ProtoMessage() {} func (x *Exists) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[18] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_sa_proto_msgTypes[12] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1039,7 +679,7 @@ func (x *Exists) ProtoReflect() protoreflect.Message { // Deprecated: Use Exists.ProtoReflect.Descriptor instead. func (*Exists) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{18} + return file_sa_proto_rawDescGZIP(), []int{12} } func (x *Exists) GetExists() bool { @@ -1050,24 +690,21 @@ func (x *Exists) GetExists() bool { } type AddSerialRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // Next unused field number: 7 - RegID int64 `protobuf:"varint,1,opt,name=regID,proto3" json:"regID,omitempty"` - Serial string `protobuf:"bytes,2,opt,name=serial,proto3" json:"serial,omitempty"` - Created *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=created,proto3" json:"created,omitempty"` - Expires *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=expires,proto3" json:"expires,omitempty"` + RegID int64 `protobuf:"varint,1,opt,name=regID,proto3" json:"regID,omitempty"` + Serial string `protobuf:"bytes,2,opt,name=serial,proto3" json:"serial,omitempty"` + Created *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=created,proto3" json:"created,omitempty"` + Expires *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=expires,proto3" json:"expires,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *AddSerialRequest) Reset() { *x = AddSerialRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_sa_proto_msgTypes[19] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_sa_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *AddSerialRequest) String() string { @@ -1077,8 +714,8 @@ func (x *AddSerialRequest) String() string { func (*AddSerialRequest) ProtoMessage() {} func (x *AddSerialRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[19] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_sa_proto_msgTypes[13] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1090,7 +727,7 @@ func (x *AddSerialRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use AddSerialRequest.ProtoReflect.Descriptor instead. func (*AddSerialRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{19} + return file_sa_proto_rawDescGZIP(), []int{13} } func (x *AddSerialRequest) GetRegID() int64 { @@ -1122,10 +759,7 @@ func (x *AddSerialRequest) GetExpires() *timestamppb.Timestamp { } type AddCertificateRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // Next unused field number: 8 Der []byte `protobuf:"bytes,1,opt,name=der,proto3" json:"der,omitempty"` RegID int64 `protobuf:"varint,2,opt,name=regID,proto3" json:"regID,omitempty"` @@ -1144,16 +778,16 @@ type AddCertificateRequest struct { // a linting certificate to the precertificates table, we want to make sure // we never give a "good" response for that serial until the precertificate // is actually issued. - OcspNotReady bool `protobuf:"varint,6,opt,name=ocspNotReady,proto3" json:"ocspNotReady,omitempty"` + OcspNotReady bool `protobuf:"varint,6,opt,name=ocspNotReady,proto3" json:"ocspNotReady,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *AddCertificateRequest) Reset() { *x = AddCertificateRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_sa_proto_msgTypes[20] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_sa_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *AddCertificateRequest) String() string { @@ -1163,8 +797,8 @@ func (x *AddCertificateRequest) String() string { func (*AddCertificateRequest) ProtoMessage() {} func (x *AddCertificateRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[20] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_sa_proto_msgTypes[14] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1176,7 +810,7 @@ func (x *AddCertificateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use AddCertificateRequest.ProtoReflect.Descriptor instead. func (*AddCertificateRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{20} + return file_sa_proto_rawDescGZIP(), []int{14} } func (x *AddCertificateRequest) GetDer() []byte { @@ -1215,20 +849,17 @@ func (x *AddCertificateRequest) GetOcspNotReady() bool { } type OrderRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` unknownFields protoimpl.UnknownFields - - Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + sizeCache protoimpl.SizeCache } func (x *OrderRequest) Reset() { *x = OrderRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_sa_proto_msgTypes[21] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_sa_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *OrderRequest) String() string { @@ -1238,8 +869,8 @@ func (x *OrderRequest) String() string { func (*OrderRequest) ProtoMessage() {} func (x *OrderRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[21] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_sa_proto_msgTypes[15] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1251,7 +882,7 @@ func (x *OrderRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use OrderRequest.ProtoReflect.Descriptor instead. func (*OrderRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{21} + return file_sa_proto_rawDescGZIP(), []int{15} } func (x *OrderRequest) GetId() int64 { @@ -1262,26 +893,27 @@ func (x *OrderRequest) GetId() int64 { } type NewOrderRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - // Next unused field number: 8 + state protoimpl.MessageState `protogen:"open.v1"` + // Next unused field number: 10 RegistrationID int64 `protobuf:"varint,1,opt,name=registrationID,proto3" json:"registrationID,omitempty"` Expires *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=expires,proto3" json:"expires,omitempty"` - Names []string `protobuf:"bytes,3,rep,name=names,proto3" json:"names,omitempty"` + Identifiers []*proto.Identifier `protobuf:"bytes,9,rep,name=identifiers,proto3" json:"identifiers,omitempty"` V2Authorizations []int64 `protobuf:"varint,4,rep,packed,name=v2Authorizations,proto3" json:"v2Authorizations,omitempty"` - ReplacesSerial string `protobuf:"bytes,6,opt,name=replacesSerial,proto3" json:"replacesSerial,omitempty"` CertificateProfileName string `protobuf:"bytes,7,opt,name=certificateProfileName,proto3" json:"certificateProfileName,omitempty"` + // Replaces is the ARI certificate Id that this order replaces. + Replaces string `protobuf:"bytes,8,opt,name=replaces,proto3" json:"replaces,omitempty"` + // ReplacesSerial is the serial number of the certificate that this order + // replaces. + ReplacesSerial string `protobuf:"bytes,6,opt,name=replacesSerial,proto3" json:"replacesSerial,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *NewOrderRequest) Reset() { *x = NewOrderRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_sa_proto_msgTypes[22] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_sa_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *NewOrderRequest) String() string { @@ -1291,8 +923,8 @@ func (x *NewOrderRequest) String() string { func (*NewOrderRequest) ProtoMessage() {} func (x *NewOrderRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[22] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_sa_proto_msgTypes[16] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1304,7 +936,7 @@ func (x *NewOrderRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use NewOrderRequest.ProtoReflect.Descriptor instead. func (*NewOrderRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{22} + return file_sa_proto_rawDescGZIP(), []int{16} } func (x *NewOrderRequest) GetRegistrationID() int64 { @@ -1321,9 +953,9 @@ func (x *NewOrderRequest) GetExpires() *timestamppb.Timestamp { return nil } -func (x *NewOrderRequest) GetNames() []string { +func (x *NewOrderRequest) GetIdentifiers() []*proto.Identifier { if x != nil { - return x.Names + return x.Identifiers } return nil } @@ -1335,13 +967,6 @@ func (x *NewOrderRequest) GetV2Authorizations() []int64 { return nil } -func (x *NewOrderRequest) GetReplacesSerial() string { - if x != nil { - return x.ReplacesSerial - } - return "" -} - func (x *NewOrderRequest) GetCertificateProfileName() string { if x != nil { return x.CertificateProfileName @@ -1349,22 +974,112 @@ func (x *NewOrderRequest) GetCertificateProfileName() string { return "" } -type NewOrderAndAuthzsRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields +func (x *NewOrderRequest) GetReplaces() string { + if x != nil { + return x.Replaces + } + return "" +} - NewOrder *NewOrderRequest `protobuf:"bytes,1,opt,name=newOrder,proto3" json:"newOrder,omitempty"` - NewAuthzs []*proto.Authorization `protobuf:"bytes,2,rep,name=newAuthzs,proto3" json:"newAuthzs,omitempty"` +func (x *NewOrderRequest) GetReplacesSerial() string { + if x != nil { + return x.ReplacesSerial + } + return "" +} + +// NewAuthzRequest starts with all the same fields as corepb.Authorization, +// because it is replacing that type in NewOrderAndAuthzsRequest, and then +// improves from there. +type NewAuthzRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Identifier *proto.Identifier `protobuf:"bytes,12,opt,name=identifier,proto3" json:"identifier,omitempty"` + RegistrationID int64 `protobuf:"varint,3,opt,name=registrationID,proto3" json:"registrationID,omitempty"` + Expires *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=expires,proto3" json:"expires,omitempty"` + ChallengeTypes []string `protobuf:"bytes,10,rep,name=challengeTypes,proto3" json:"challengeTypes,omitempty"` + Token string `protobuf:"bytes,11,opt,name=token,proto3" json:"token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NewAuthzRequest) Reset() { + *x = NewAuthzRequest{} + mi := &file_sa_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NewAuthzRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NewAuthzRequest) ProtoMessage() {} + +func (x *NewAuthzRequest) ProtoReflect() protoreflect.Message { + mi := &file_sa_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NewAuthzRequest.ProtoReflect.Descriptor instead. +func (*NewAuthzRequest) Descriptor() ([]byte, []int) { + return file_sa_proto_rawDescGZIP(), []int{17} +} + +func (x *NewAuthzRequest) GetIdentifier() *proto.Identifier { + if x != nil { + return x.Identifier + } + return nil +} + +func (x *NewAuthzRequest) GetRegistrationID() int64 { + if x != nil { + return x.RegistrationID + } + return 0 +} + +func (x *NewAuthzRequest) GetExpires() *timestamppb.Timestamp { + if x != nil { + return x.Expires + } + return nil +} + +func (x *NewAuthzRequest) GetChallengeTypes() []string { + if x != nil { + return x.ChallengeTypes + } + return nil +} + +func (x *NewAuthzRequest) GetToken() string { + if x != nil { + return x.Token + } + return "" +} + +type NewOrderAndAuthzsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + NewOrder *NewOrderRequest `protobuf:"bytes,1,opt,name=newOrder,proto3" json:"newOrder,omitempty"` + NewAuthzs []*NewAuthzRequest `protobuf:"bytes,2,rep,name=newAuthzs,proto3" json:"newAuthzs,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *NewOrderAndAuthzsRequest) Reset() { *x = NewOrderAndAuthzsRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_sa_proto_msgTypes[23] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_sa_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *NewOrderAndAuthzsRequest) String() string { @@ -1374,8 +1089,8 @@ func (x *NewOrderAndAuthzsRequest) String() string { func (*NewOrderAndAuthzsRequest) ProtoMessage() {} func (x *NewOrderAndAuthzsRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[23] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_sa_proto_msgTypes[18] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1387,7 +1102,7 @@ func (x *NewOrderAndAuthzsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use NewOrderAndAuthzsRequest.ProtoReflect.Descriptor instead. func (*NewOrderAndAuthzsRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{23} + return file_sa_proto_rawDescGZIP(), []int{18} } func (x *NewOrderAndAuthzsRequest) GetNewOrder() *NewOrderRequest { @@ -1397,7 +1112,7 @@ func (x *NewOrderAndAuthzsRequest) GetNewOrder() *NewOrderRequest { return nil } -func (x *NewOrderAndAuthzsRequest) GetNewAuthzs() []*proto.Authorization { +func (x *NewOrderAndAuthzsRequest) GetNewAuthzs() []*NewAuthzRequest { if x != nil { return x.NewAuthzs } @@ -1405,21 +1120,18 @@ func (x *NewOrderAndAuthzsRequest) GetNewAuthzs() []*proto.Authorization { } type SetOrderErrorRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + Error *proto.ProblemDetails `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"` unknownFields protoimpl.UnknownFields - - Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` - Error *proto.ProblemDetails `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"` + sizeCache protoimpl.SizeCache } func (x *SetOrderErrorRequest) Reset() { *x = SetOrderErrorRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_sa_proto_msgTypes[24] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_sa_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *SetOrderErrorRequest) String() string { @@ -1429,8 +1141,8 @@ func (x *SetOrderErrorRequest) String() string { func (*SetOrderErrorRequest) ProtoMessage() {} func (x *SetOrderErrorRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[24] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_sa_proto_msgTypes[19] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1442,7 +1154,7 @@ func (x *SetOrderErrorRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SetOrderErrorRequest.ProtoReflect.Descriptor instead. func (*SetOrderErrorRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{24} + return file_sa_proto_rawDescGZIP(), []int{19} } func (x *SetOrderErrorRequest) GetId() int64 { @@ -1460,21 +1172,18 @@ func (x *SetOrderErrorRequest) GetError() *proto.ProblemDetails { } type GetValidOrderAuthorizationsRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + AcctID int64 `protobuf:"varint,2,opt,name=acctID,proto3" json:"acctID,omitempty"` unknownFields protoimpl.UnknownFields - - Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` - AcctID int64 `protobuf:"varint,2,opt,name=acctID,proto3" json:"acctID,omitempty"` + sizeCache protoimpl.SizeCache } func (x *GetValidOrderAuthorizationsRequest) Reset() { *x = GetValidOrderAuthorizationsRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_sa_proto_msgTypes[25] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_sa_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *GetValidOrderAuthorizationsRequest) String() string { @@ -1484,8 +1193,8 @@ func (x *GetValidOrderAuthorizationsRequest) String() string { func (*GetValidOrderAuthorizationsRequest) ProtoMessage() {} func (x *GetValidOrderAuthorizationsRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[25] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_sa_proto_msgTypes[20] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1497,7 +1206,7 @@ func (x *GetValidOrderAuthorizationsRequest) ProtoReflect() protoreflect.Message // Deprecated: Use GetValidOrderAuthorizationsRequest.ProtoReflect.Descriptor instead. func (*GetValidOrderAuthorizationsRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{25} + return file_sa_proto_rawDescGZIP(), []int{20} } func (x *GetValidOrderAuthorizationsRequest) GetId() int64 { @@ -1515,21 +1224,19 @@ func (x *GetValidOrderAuthorizationsRequest) GetAcctID() int64 { } type GetOrderForNamesRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + // Next unused field number: 4 + AcctID int64 `protobuf:"varint,1,opt,name=acctID,proto3" json:"acctID,omitempty"` + Identifiers []*proto.Identifier `protobuf:"bytes,3,rep,name=identifiers,proto3" json:"identifiers,omitempty"` unknownFields protoimpl.UnknownFields - - AcctID int64 `protobuf:"varint,1,opt,name=acctID,proto3" json:"acctID,omitempty"` - Names []string `protobuf:"bytes,2,rep,name=names,proto3" json:"names,omitempty"` + sizeCache protoimpl.SizeCache } func (x *GetOrderForNamesRequest) Reset() { *x = GetOrderForNamesRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_sa_proto_msgTypes[26] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_sa_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *GetOrderForNamesRequest) String() string { @@ -1539,8 +1246,8 @@ func (x *GetOrderForNamesRequest) String() string { func (*GetOrderForNamesRequest) ProtoMessage() {} func (x *GetOrderForNamesRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[26] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_sa_proto_msgTypes[21] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1552,7 +1259,7 @@ func (x *GetOrderForNamesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetOrderForNamesRequest.ProtoReflect.Descriptor instead. func (*GetOrderForNamesRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{26} + return file_sa_proto_rawDescGZIP(), []int{21} } func (x *GetOrderForNamesRequest) GetAcctID() int64 { @@ -1562,29 +1269,26 @@ func (x *GetOrderForNamesRequest) GetAcctID() int64 { return 0 } -func (x *GetOrderForNamesRequest) GetNames() []string { +func (x *GetOrderForNamesRequest) GetIdentifiers() []*proto.Identifier { if x != nil { - return x.Names + return x.Identifiers } return nil } type FinalizeOrderRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` - CertificateSerial string `protobuf:"bytes,2,opt,name=certificateSerial,proto3" json:"certificateSerial,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + CertificateSerial string `protobuf:"bytes,2,opt,name=certificateSerial,proto3" json:"certificateSerial,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *FinalizeOrderRequest) Reset() { *x = FinalizeOrderRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_sa_proto_msgTypes[27] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_sa_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *FinalizeOrderRequest) String() string { @@ -1594,8 +1298,8 @@ func (x *FinalizeOrderRequest) String() string { func (*FinalizeOrderRequest) ProtoMessage() {} func (x *FinalizeOrderRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[27] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_sa_proto_msgTypes[22] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1607,7 +1311,7 @@ func (x *FinalizeOrderRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use FinalizeOrderRequest.ProtoReflect.Descriptor instead. func (*FinalizeOrderRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{27} + return file_sa_proto_rawDescGZIP(), []int{22} } func (x *FinalizeOrderRequest) GetId() int64 { @@ -1625,23 +1329,21 @@ func (x *FinalizeOrderRequest) GetCertificateSerial() string { } type GetAuthorizationsRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - // Next unused field number: 5 + state protoimpl.MessageState `protogen:"open.v1"` + // Next unused field number: 7 RegistrationID int64 `protobuf:"varint,1,opt,name=registrationID,proto3" json:"registrationID,omitempty"` - Domains []string `protobuf:"bytes,2,rep,name=domains,proto3" json:"domains,omitempty"` - Now *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=now,proto3" json:"now,omitempty"` + Identifiers []*proto.Identifier `protobuf:"bytes,6,rep,name=identifiers,proto3" json:"identifiers,omitempty"` + ValidUntil *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=validUntil,proto3" json:"validUntil,omitempty"` + Profile string `protobuf:"bytes,5,opt,name=profile,proto3" json:"profile,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GetAuthorizationsRequest) Reset() { *x = GetAuthorizationsRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_sa_proto_msgTypes[28] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_sa_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *GetAuthorizationsRequest) String() string { @@ -1651,8 +1353,8 @@ func (x *GetAuthorizationsRequest) String() string { func (*GetAuthorizationsRequest) ProtoMessage() {} func (x *GetAuthorizationsRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[28] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_sa_proto_msgTypes[23] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1664,7 +1366,7 @@ func (x *GetAuthorizationsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetAuthorizationsRequest.ProtoReflect.Descriptor instead. func (*GetAuthorizationsRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{28} + return file_sa_proto_rawDescGZIP(), []int{23} } func (x *GetAuthorizationsRequest) GetRegistrationID() int64 { @@ -1674,35 +1376,39 @@ func (x *GetAuthorizationsRequest) GetRegistrationID() int64 { return 0 } -func (x *GetAuthorizationsRequest) GetDomains() []string { +func (x *GetAuthorizationsRequest) GetIdentifiers() []*proto.Identifier { if x != nil { - return x.Domains + return x.Identifiers } return nil } -func (x *GetAuthorizationsRequest) GetNow() *timestamppb.Timestamp { +func (x *GetAuthorizationsRequest) GetValidUntil() *timestamppb.Timestamp { if x != nil { - return x.Now + return x.ValidUntil } return nil } +func (x *GetAuthorizationsRequest) GetProfile() string { + if x != nil { + return x.Profile + } + return "" +} + type Authorizations struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Authzs []*proto.Authorization `protobuf:"bytes,2,rep,name=authzs,proto3" json:"authzs,omitempty"` unknownFields protoimpl.UnknownFields - - Authz []*Authorizations_MapElement `protobuf:"bytes,1,rep,name=authz,proto3" json:"authz,omitempty"` + sizeCache protoimpl.SizeCache } func (x *Authorizations) Reset() { *x = Authorizations{} - if protoimpl.UnsafeEnabled { - mi := &file_sa_proto_msgTypes[29] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_sa_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Authorizations) String() string { @@ -1712,8 +1418,8 @@ func (x *Authorizations) String() string { func (*Authorizations) ProtoMessage() {} func (x *Authorizations) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[29] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_sa_proto_msgTypes[24] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1725,31 +1431,28 @@ func (x *Authorizations) ProtoReflect() protoreflect.Message { // Deprecated: Use Authorizations.ProtoReflect.Descriptor instead. func (*Authorizations) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{29} + return file_sa_proto_rawDescGZIP(), []int{24} } -func (x *Authorizations) GetAuthz() []*Authorizations_MapElement { +func (x *Authorizations) GetAuthzs() []*proto.Authorization { if x != nil { - return x.Authz + return x.Authzs } return nil } type AuthorizationIDs struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Ids []string `protobuf:"bytes,1,rep,name=ids,proto3" json:"ids,omitempty"` unknownFields protoimpl.UnknownFields - - Ids []string `protobuf:"bytes,1,rep,name=ids,proto3" json:"ids,omitempty"` + sizeCache protoimpl.SizeCache } func (x *AuthorizationIDs) Reset() { *x = AuthorizationIDs{} - if protoimpl.UnsafeEnabled { - mi := &file_sa_proto_msgTypes[30] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_sa_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *AuthorizationIDs) String() string { @@ -1759,8 +1462,8 @@ func (x *AuthorizationIDs) String() string { func (*AuthorizationIDs) ProtoMessage() {} func (x *AuthorizationIDs) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[30] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_sa_proto_msgTypes[25] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1772,7 +1475,7 @@ func (x *AuthorizationIDs) ProtoReflect() protoreflect.Message { // Deprecated: Use AuthorizationIDs.ProtoReflect.Descriptor instead. func (*AuthorizationIDs) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{30} + return file_sa_proto_rawDescGZIP(), []int{25} } func (x *AuthorizationIDs) GetIds() []string { @@ -1783,20 +1486,17 @@ func (x *AuthorizationIDs) GetIds() []string { } type AuthorizationID2 struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` unknownFields protoimpl.UnknownFields - - Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + sizeCache protoimpl.SizeCache } func (x *AuthorizationID2) Reset() { *x = AuthorizationID2{} - if protoimpl.UnsafeEnabled { - mi := &file_sa_proto_msgTypes[31] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_sa_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *AuthorizationID2) String() string { @@ -1806,8 +1506,8 @@ func (x *AuthorizationID2) String() string { func (*AuthorizationID2) ProtoMessage() {} func (x *AuthorizationID2) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[31] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_sa_proto_msgTypes[26] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1819,7 +1519,7 @@ func (x *AuthorizationID2) ProtoReflect() protoreflect.Message { // Deprecated: Use AuthorizationID2.ProtoReflect.Descriptor instead. func (*AuthorizationID2) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{31} + return file_sa_proto_rawDescGZIP(), []int{26} } func (x *AuthorizationID2) GetId() int64 { @@ -1830,27 +1530,24 @@ func (x *AuthorizationID2) GetId() int64 { } type RevokeCertificateRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // Next unused field number: 10 - Serial string `protobuf:"bytes,1,opt,name=serial,proto3" json:"serial,omitempty"` - Reason int64 `protobuf:"varint,2,opt,name=reason,proto3" json:"reason,omitempty"` - Date *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=date,proto3" json:"date,omitempty"` - Backdate *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=backdate,proto3" json:"backdate,omitempty"` - Response []byte `protobuf:"bytes,4,opt,name=response,proto3" json:"response,omitempty"` - IssuerID int64 `protobuf:"varint,6,opt,name=issuerID,proto3" json:"issuerID,omitempty"` - ShardIdx int64 `protobuf:"varint,7,opt,name=shardIdx,proto3" json:"shardIdx,omitempty"` + Serial string `protobuf:"bytes,1,opt,name=serial,proto3" json:"serial,omitempty"` + Reason int64 `protobuf:"varint,2,opt,name=reason,proto3" json:"reason,omitempty"` + Date *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=date,proto3" json:"date,omitempty"` + Backdate *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=backdate,proto3" json:"backdate,omitempty"` + Response []byte `protobuf:"bytes,4,opt,name=response,proto3" json:"response,omitempty"` + IssuerID int64 `protobuf:"varint,6,opt,name=issuerID,proto3" json:"issuerID,omitempty"` + ShardIdx int64 `protobuf:"varint,7,opt,name=shardIdx,proto3" json:"shardIdx,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *RevokeCertificateRequest) Reset() { *x = RevokeCertificateRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_sa_proto_msgTypes[32] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_sa_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *RevokeCertificateRequest) String() string { @@ -1860,8 +1557,8 @@ func (x *RevokeCertificateRequest) String() string { func (*RevokeCertificateRequest) ProtoMessage() {} func (x *RevokeCertificateRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[32] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_sa_proto_msgTypes[27] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1873,7 +1570,7 @@ func (x *RevokeCertificateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RevokeCertificateRequest.ProtoReflect.Descriptor instead. func (*RevokeCertificateRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{32} + return file_sa_proto_rawDescGZIP(), []int{27} } func (x *RevokeCertificateRequest) GetSerial() string { @@ -1926,10 +1623,7 @@ func (x *RevokeCertificateRequest) GetShardIdx() int64 { } type FinalizeAuthorizationRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // Next unused field number: 10 Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` Status string `protobuf:"bytes,2,opt,name=status,proto3" json:"status,omitempty"` @@ -1938,15 +1632,15 @@ type FinalizeAuthorizationRequest struct { ValidationRecords []*proto.ValidationRecord `protobuf:"bytes,5,rep,name=validationRecords,proto3" json:"validationRecords,omitempty"` ValidationError *proto.ProblemDetails `protobuf:"bytes,6,opt,name=validationError,proto3" json:"validationError,omitempty"` AttemptedAt *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=attemptedAt,proto3" json:"attemptedAt,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *FinalizeAuthorizationRequest) Reset() { *x = FinalizeAuthorizationRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_sa_proto_msgTypes[33] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_sa_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *FinalizeAuthorizationRequest) String() string { @@ -1956,8 +1650,8 @@ func (x *FinalizeAuthorizationRequest) String() string { func (*FinalizeAuthorizationRequest) ProtoMessage() {} func (x *FinalizeAuthorizationRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[33] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_sa_proto_msgTypes[28] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1969,7 +1663,7 @@ func (x *FinalizeAuthorizationRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use FinalizeAuthorizationRequest.ProtoReflect.Descriptor instead. func (*FinalizeAuthorizationRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{33} + return file_sa_proto_rawDescGZIP(), []int{28} } func (x *FinalizeAuthorizationRequest) GetId() int64 { @@ -2022,25 +1716,22 @@ func (x *FinalizeAuthorizationRequest) GetAttemptedAt() *timestamppb.Timestamp { } type AddBlockedKeyRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // Next unused field number: 7 - KeyHash []byte `protobuf:"bytes,1,opt,name=keyHash,proto3" json:"keyHash,omitempty"` - Added *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=added,proto3" json:"added,omitempty"` - Source string `protobuf:"bytes,3,opt,name=source,proto3" json:"source,omitempty"` - Comment string `protobuf:"bytes,4,opt,name=comment,proto3" json:"comment,omitempty"` - RevokedBy int64 `protobuf:"varint,5,opt,name=revokedBy,proto3" json:"revokedBy,omitempty"` + KeyHash []byte `protobuf:"bytes,1,opt,name=keyHash,proto3" json:"keyHash,omitempty"` + Added *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=added,proto3" json:"added,omitempty"` + Source string `protobuf:"bytes,3,opt,name=source,proto3" json:"source,omitempty"` + Comment string `protobuf:"bytes,4,opt,name=comment,proto3" json:"comment,omitempty"` + RevokedBy int64 `protobuf:"varint,5,opt,name=revokedBy,proto3" json:"revokedBy,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *AddBlockedKeyRequest) Reset() { *x = AddBlockedKeyRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_sa_proto_msgTypes[34] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_sa_proto_msgTypes[29] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *AddBlockedKeyRequest) String() string { @@ -2050,8 +1741,8 @@ func (x *AddBlockedKeyRequest) String() string { func (*AddBlockedKeyRequest) ProtoMessage() {} func (x *AddBlockedKeyRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[34] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_sa_proto_msgTypes[29] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2063,7 +1754,7 @@ func (x *AddBlockedKeyRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use AddBlockedKeyRequest.ProtoReflect.Descriptor instead. func (*AddBlockedKeyRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{34} + return file_sa_proto_rawDescGZIP(), []int{29} } func (x *AddBlockedKeyRequest) GetKeyHash() []byte { @@ -2102,20 +1793,17 @@ func (x *AddBlockedKeyRequest) GetRevokedBy() int64 { } type SPKIHash struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + KeyHash []byte `protobuf:"bytes,1,opt,name=keyHash,proto3" json:"keyHash,omitempty"` unknownFields protoimpl.UnknownFields - - KeyHash []byte `protobuf:"bytes,1,opt,name=keyHash,proto3" json:"keyHash,omitempty"` + sizeCache protoimpl.SizeCache } func (x *SPKIHash) Reset() { *x = SPKIHash{} - if protoimpl.UnsafeEnabled { - mi := &file_sa_proto_msgTypes[35] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_sa_proto_msgTypes[30] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *SPKIHash) String() string { @@ -2125,8 +1813,8 @@ func (x *SPKIHash) String() string { func (*SPKIHash) ProtoMessage() {} func (x *SPKIHash) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[35] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_sa_proto_msgTypes[30] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2138,7 +1826,7 @@ func (x *SPKIHash) ProtoReflect() protoreflect.Message { // Deprecated: Use SPKIHash.ProtoReflect.Descriptor instead. func (*SPKIHash) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{35} + return file_sa_proto_rawDescGZIP(), []int{30} } func (x *SPKIHash) GetKeyHash() []byte { @@ -2149,25 +1837,22 @@ func (x *SPKIHash) GetKeyHash() []byte { } type Incident struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // Next unused field number: 7 - Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` - SerialTable string `protobuf:"bytes,2,opt,name=serialTable,proto3" json:"serialTable,omitempty"` - Url string `protobuf:"bytes,3,opt,name=url,proto3" json:"url,omitempty"` - RenewBy *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=renewBy,proto3" json:"renewBy,omitempty"` - Enabled bool `protobuf:"varint,5,opt,name=enabled,proto3" json:"enabled,omitempty"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + SerialTable string `protobuf:"bytes,2,opt,name=serialTable,proto3" json:"serialTable,omitempty"` + Url string `protobuf:"bytes,3,opt,name=url,proto3" json:"url,omitempty"` + RenewBy *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=renewBy,proto3" json:"renewBy,omitempty"` + Enabled bool `protobuf:"varint,5,opt,name=enabled,proto3" json:"enabled,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Incident) Reset() { *x = Incident{} - if protoimpl.UnsafeEnabled { - mi := &file_sa_proto_msgTypes[36] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_sa_proto_msgTypes[31] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Incident) String() string { @@ -2177,8 +1862,8 @@ func (x *Incident) String() string { func (*Incident) ProtoMessage() {} func (x *Incident) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[36] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_sa_proto_msgTypes[31] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2190,7 +1875,7 @@ func (x *Incident) ProtoReflect() protoreflect.Message { // Deprecated: Use Incident.ProtoReflect.Descriptor instead. func (*Incident) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{36} + return file_sa_proto_rawDescGZIP(), []int{31} } func (x *Incident) GetId() int64 { @@ -2229,20 +1914,17 @@ func (x *Incident) GetEnabled() bool { } type Incidents struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Incidents []*Incident `protobuf:"bytes,1,rep,name=incidents,proto3" json:"incidents,omitempty"` unknownFields protoimpl.UnknownFields - - Incidents []*Incident `protobuf:"bytes,1,rep,name=incidents,proto3" json:"incidents,omitempty"` + sizeCache protoimpl.SizeCache } func (x *Incidents) Reset() { *x = Incidents{} - if protoimpl.UnsafeEnabled { - mi := &file_sa_proto_msgTypes[37] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_sa_proto_msgTypes[32] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Incidents) String() string { @@ -2252,8 +1934,8 @@ func (x *Incidents) String() string { func (*Incidents) ProtoMessage() {} func (x *Incidents) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[37] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_sa_proto_msgTypes[32] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2265,7 +1947,7 @@ func (x *Incidents) ProtoReflect() protoreflect.Message { // Deprecated: Use Incidents.ProtoReflect.Descriptor instead. func (*Incidents) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{37} + return file_sa_proto_rawDescGZIP(), []int{32} } func (x *Incidents) GetIncidents() []*Incident { @@ -2276,20 +1958,17 @@ func (x *Incidents) GetIncidents() []*Incident { } type SerialsForIncidentRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + IncidentTable string `protobuf:"bytes,1,opt,name=incidentTable,proto3" json:"incidentTable,omitempty"` unknownFields protoimpl.UnknownFields - - IncidentTable string `protobuf:"bytes,1,opt,name=incidentTable,proto3" json:"incidentTable,omitempty"` + sizeCache protoimpl.SizeCache } func (x *SerialsForIncidentRequest) Reset() { *x = SerialsForIncidentRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_sa_proto_msgTypes[38] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_sa_proto_msgTypes[33] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *SerialsForIncidentRequest) String() string { @@ -2299,8 +1978,8 @@ func (x *SerialsForIncidentRequest) String() string { func (*SerialsForIncidentRequest) ProtoMessage() {} func (x *SerialsForIncidentRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[38] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_sa_proto_msgTypes[33] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2312,7 +1991,7 @@ func (x *SerialsForIncidentRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SerialsForIncidentRequest.ProtoReflect.Descriptor instead. func (*SerialsForIncidentRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{38} + return file_sa_proto_rawDescGZIP(), []int{33} } func (x *SerialsForIncidentRequest) GetIncidentTable() string { @@ -2323,24 +2002,21 @@ func (x *SerialsForIncidentRequest) GetIncidentTable() string { } type IncidentSerial struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // Next unused field number: 6 Serial string `protobuf:"bytes,1,opt,name=serial,proto3" json:"serial,omitempty"` RegistrationID int64 `protobuf:"varint,2,opt,name=registrationID,proto3" json:"registrationID,omitempty"` // May be 0 (NULL) OrderID int64 `protobuf:"varint,3,opt,name=orderID,proto3" json:"orderID,omitempty"` // May be 0 (NULL) LastNoticeSent *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=lastNoticeSent,proto3" json:"lastNoticeSent,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *IncidentSerial) Reset() { *x = IncidentSerial{} - if protoimpl.UnsafeEnabled { - mi := &file_sa_proto_msgTypes[39] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_sa_proto_msgTypes[34] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *IncidentSerial) String() string { @@ -2350,8 +2026,8 @@ func (x *IncidentSerial) String() string { func (*IncidentSerial) ProtoMessage() {} func (x *IncidentSerial) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[39] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_sa_proto_msgTypes[34] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2363,7 +2039,7 @@ func (x *IncidentSerial) ProtoReflect() protoreflect.Message { // Deprecated: Use IncidentSerial.ProtoReflect.Descriptor instead. func (*IncidentSerial) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{39} + return file_sa_proto_rawDescGZIP(), []int{34} } func (x *IncidentSerial) GetSerial() string { @@ -2394,26 +2070,90 @@ func (x *IncidentSerial) GetLastNoticeSent() *timestamppb.Timestamp { return nil } -type GetRevokedCertsRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache +type GetRevokedCertsByShardRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + IssuerNameID int64 `protobuf:"varint,1,opt,name=issuerNameID,proto3" json:"issuerNameID,omitempty"` + RevokedBefore *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=revokedBefore,proto3" json:"revokedBefore,omitempty"` + ExpiresAfter *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=expiresAfter,proto3" json:"expiresAfter,omitempty"` + ShardIdx int64 `protobuf:"varint,4,opt,name=shardIdx,proto3" json:"shardIdx,omitempty"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} +func (x *GetRevokedCertsByShardRequest) Reset() { + *x = GetRevokedCertsByShardRequest{} + mi := &file_sa_proto_msgTypes[35] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetRevokedCertsByShardRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetRevokedCertsByShardRequest) ProtoMessage() {} + +func (x *GetRevokedCertsByShardRequest) ProtoReflect() protoreflect.Message { + mi := &file_sa_proto_msgTypes[35] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetRevokedCertsByShardRequest.ProtoReflect.Descriptor instead. +func (*GetRevokedCertsByShardRequest) Descriptor() ([]byte, []int) { + return file_sa_proto_rawDescGZIP(), []int{35} +} + +func (x *GetRevokedCertsByShardRequest) GetIssuerNameID() int64 { + if x != nil { + return x.IssuerNameID + } + return 0 +} + +func (x *GetRevokedCertsByShardRequest) GetRevokedBefore() *timestamppb.Timestamp { + if x != nil { + return x.RevokedBefore + } + return nil +} + +func (x *GetRevokedCertsByShardRequest) GetExpiresAfter() *timestamppb.Timestamp { + if x != nil { + return x.ExpiresAfter + } + return nil +} + +func (x *GetRevokedCertsByShardRequest) GetShardIdx() int64 { + if x != nil { + return x.ShardIdx + } + return 0 +} + +type GetRevokedCertsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` // Next unused field number: 9 IssuerNameID int64 `protobuf:"varint,1,opt,name=issuerNameID,proto3" json:"issuerNameID,omitempty"` ExpiresAfter *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=expiresAfter,proto3" json:"expiresAfter,omitempty"` // inclusive ExpiresBefore *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=expiresBefore,proto3" json:"expiresBefore,omitempty"` // exclusive RevokedBefore *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=revokedBefore,proto3" json:"revokedBefore,omitempty"` - ShardIdx int64 `protobuf:"varint,5,opt,name=shardIdx,proto3" json:"shardIdx,omitempty"` // Must not be set until the revokedCertificates table has 90+ days of entries. + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GetRevokedCertsRequest) Reset() { *x = GetRevokedCertsRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_sa_proto_msgTypes[40] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_sa_proto_msgTypes[36] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *GetRevokedCertsRequest) String() string { @@ -2423,8 +2163,8 @@ func (x *GetRevokedCertsRequest) String() string { func (*GetRevokedCertsRequest) ProtoMessage() {} func (x *GetRevokedCertsRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[40] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_sa_proto_msgTypes[36] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2436,7 +2176,7 @@ func (x *GetRevokedCertsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetRevokedCertsRequest.ProtoReflect.Descriptor instead. func (*GetRevokedCertsRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{40} + return file_sa_proto_rawDescGZIP(), []int{36} } func (x *GetRevokedCertsRequest) GetIssuerNameID() int64 { @@ -2467,30 +2207,20 @@ func (x *GetRevokedCertsRequest) GetRevokedBefore() *timestamppb.Timestamp { return nil } -func (x *GetRevokedCertsRequest) GetShardIdx() int64 { - if x != nil { - return x.ShardIdx - } - return 0 -} - type RevocationStatus struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` Status int64 `protobuf:"varint,1,opt,name=status,proto3" json:"status,omitempty"` RevokedReason int64 `protobuf:"varint,2,opt,name=revokedReason,proto3" json:"revokedReason,omitempty"` RevokedDate *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=revokedDate,proto3" json:"revokedDate,omitempty"` // Unix timestamp (nanoseconds) + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *RevocationStatus) Reset() { *x = RevocationStatus{} - if protoimpl.UnsafeEnabled { - mi := &file_sa_proto_msgTypes[41] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_sa_proto_msgTypes[37] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *RevocationStatus) String() string { @@ -2500,8 +2230,8 @@ func (x *RevocationStatus) String() string { func (*RevocationStatus) ProtoMessage() {} func (x *RevocationStatus) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[41] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_sa_proto_msgTypes[37] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2513,7 +2243,7 @@ func (x *RevocationStatus) ProtoReflect() protoreflect.Message { // Deprecated: Use RevocationStatus.ProtoReflect.Descriptor instead. func (*RevocationStatus) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{41} + return file_sa_proto_rawDescGZIP(), []int{37} } func (x *RevocationStatus) GetStatus() int64 { @@ -2538,23 +2268,20 @@ func (x *RevocationStatus) GetRevokedDate() *timestamppb.Timestamp { } type LeaseCRLShardRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + IssuerNameID int64 `protobuf:"varint,1,opt,name=issuerNameID,proto3" json:"issuerNameID,omitempty"` + MinShardIdx int64 `protobuf:"varint,2,opt,name=minShardIdx,proto3" json:"minShardIdx,omitempty"` + MaxShardIdx int64 `protobuf:"varint,3,opt,name=maxShardIdx,proto3" json:"maxShardIdx,omitempty"` + Until *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=until,proto3" json:"until,omitempty"` unknownFields protoimpl.UnknownFields - - IssuerNameID int64 `protobuf:"varint,1,opt,name=issuerNameID,proto3" json:"issuerNameID,omitempty"` - MinShardIdx int64 `protobuf:"varint,2,opt,name=minShardIdx,proto3" json:"minShardIdx,omitempty"` - MaxShardIdx int64 `protobuf:"varint,3,opt,name=maxShardIdx,proto3" json:"maxShardIdx,omitempty"` - Until *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=until,proto3" json:"until,omitempty"` + sizeCache protoimpl.SizeCache } func (x *LeaseCRLShardRequest) Reset() { *x = LeaseCRLShardRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_sa_proto_msgTypes[42] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_sa_proto_msgTypes[38] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *LeaseCRLShardRequest) String() string { @@ -2564,8 +2291,8 @@ func (x *LeaseCRLShardRequest) String() string { func (*LeaseCRLShardRequest) ProtoMessage() {} func (x *LeaseCRLShardRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[42] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_sa_proto_msgTypes[38] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2577,7 +2304,7 @@ func (x *LeaseCRLShardRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use LeaseCRLShardRequest.ProtoReflect.Descriptor instead. func (*LeaseCRLShardRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{42} + return file_sa_proto_rawDescGZIP(), []int{38} } func (x *LeaseCRLShardRequest) GetIssuerNameID() int64 { @@ -2609,21 +2336,18 @@ func (x *LeaseCRLShardRequest) GetUntil() *timestamppb.Timestamp { } type LeaseCRLShardResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + IssuerNameID int64 `protobuf:"varint,1,opt,name=issuerNameID,proto3" json:"issuerNameID,omitempty"` + ShardIdx int64 `protobuf:"varint,2,opt,name=shardIdx,proto3" json:"shardIdx,omitempty"` unknownFields protoimpl.UnknownFields - - IssuerNameID int64 `protobuf:"varint,1,opt,name=issuerNameID,proto3" json:"issuerNameID,omitempty"` - ShardIdx int64 `protobuf:"varint,2,opt,name=shardIdx,proto3" json:"shardIdx,omitempty"` + sizeCache protoimpl.SizeCache } func (x *LeaseCRLShardResponse) Reset() { *x = LeaseCRLShardResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_sa_proto_msgTypes[43] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_sa_proto_msgTypes[39] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *LeaseCRLShardResponse) String() string { @@ -2633,8 +2357,8 @@ func (x *LeaseCRLShardResponse) String() string { func (*LeaseCRLShardResponse) ProtoMessage() {} func (x *LeaseCRLShardResponse) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[43] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_sa_proto_msgTypes[39] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2646,7 +2370,7 @@ func (x *LeaseCRLShardResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use LeaseCRLShardResponse.ProtoReflect.Descriptor instead. func (*LeaseCRLShardResponse) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{43} + return file_sa_proto_rawDescGZIP(), []int{39} } func (x *LeaseCRLShardResponse) GetIssuerNameID() int64 { @@ -2664,23 +2388,20 @@ func (x *LeaseCRLShardResponse) GetShardIdx() int64 { } type UpdateCRLShardRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + IssuerNameID int64 `protobuf:"varint,1,opt,name=issuerNameID,proto3" json:"issuerNameID,omitempty"` + ShardIdx int64 `protobuf:"varint,2,opt,name=shardIdx,proto3" json:"shardIdx,omitempty"` + ThisUpdate *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=thisUpdate,proto3" json:"thisUpdate,omitempty"` + NextUpdate *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=nextUpdate,proto3" json:"nextUpdate,omitempty"` unknownFields protoimpl.UnknownFields - - IssuerNameID int64 `protobuf:"varint,1,opt,name=issuerNameID,proto3" json:"issuerNameID,omitempty"` - ShardIdx int64 `protobuf:"varint,2,opt,name=shardIdx,proto3" json:"shardIdx,omitempty"` - ThisUpdate *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=thisUpdate,proto3" json:"thisUpdate,omitempty"` - NextUpdate *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=nextUpdate,proto3" json:"nextUpdate,omitempty"` + sizeCache protoimpl.SizeCache } func (x *UpdateCRLShardRequest) Reset() { *x = UpdateCRLShardRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_sa_proto_msgTypes[44] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_sa_proto_msgTypes[40] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *UpdateCRLShardRequest) String() string { @@ -2690,8 +2411,8 @@ func (x *UpdateCRLShardRequest) String() string { func (*UpdateCRLShardRequest) ProtoMessage() {} func (x *UpdateCRLShardRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[44] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_sa_proto_msgTypes[40] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2703,7 +2424,7 @@ func (x *UpdateCRLShardRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateCRLShardRequest.ProtoReflect.Descriptor instead. func (*UpdateCRLShardRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{44} + return file_sa_proto_rawDescGZIP(), []int{40} } func (x *UpdateCRLShardRequest) GetIssuerNameID() int64 { @@ -2734,76 +2455,18 @@ func (x *UpdateCRLShardRequest) GetNextUpdate() *timestamppb.Timestamp { return nil } -type Identifier struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` - Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` -} - -func (x *Identifier) Reset() { - *x = Identifier{} - if protoimpl.UnsafeEnabled { - mi := &file_sa_proto_msgTypes[45] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *Identifier) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Identifier) ProtoMessage() {} - -func (x *Identifier) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[45] - 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 Identifier.ProtoReflect.Descriptor instead. -func (*Identifier) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{45} -} - -func (x *Identifier) GetType() string { - if x != nil { - return x.Type - } - return "" -} - -func (x *Identifier) GetValue() string { - if x != nil { - return x.Value - } - return "" -} - type Identifiers struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Identifiers []*proto.Identifier `protobuf:"bytes,1,rep,name=identifiers,proto3" json:"identifiers,omitempty"` unknownFields protoimpl.UnknownFields - - Identifiers []*Identifier `protobuf:"bytes,1,rep,name=identifiers,proto3" json:"identifiers,omitempty"` + sizeCache protoimpl.SizeCache } func (x *Identifiers) Reset() { *x = Identifiers{} - if protoimpl.UnsafeEnabled { - mi := &file_sa_proto_msgTypes[46] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_sa_proto_msgTypes[41] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Identifiers) String() string { @@ -2813,8 +2476,8 @@ func (x *Identifiers) String() string { func (*Identifiers) ProtoMessage() {} func (x *Identifiers) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[46] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_sa_proto_msgTypes[41] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2826,10 +2489,10 @@ func (x *Identifiers) ProtoReflect() protoreflect.Message { // Deprecated: Use Identifiers.ProtoReflect.Descriptor instead. func (*Identifiers) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{46} + return file_sa_proto_rawDescGZIP(), []int{41} } -func (x *Identifiers) GetIdentifiers() []*Identifier { +func (x *Identifiers) GetIdentifiers() []*proto.Identifier { if x != nil { return x.Identifiers } @@ -2837,21 +2500,18 @@ func (x *Identifiers) GetIdentifiers() []*Identifier { } type PauseRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - RegistrationID int64 `protobuf:"varint,1,opt,name=registrationID,proto3" json:"registrationID,omitempty"` - Identifiers []*Identifier `protobuf:"bytes,2,rep,name=identifiers,proto3" json:"identifiers,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + RegistrationID int64 `protobuf:"varint,1,opt,name=registrationID,proto3" json:"registrationID,omitempty"` + Identifiers []*proto.Identifier `protobuf:"bytes,2,rep,name=identifiers,proto3" json:"identifiers,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *PauseRequest) Reset() { *x = PauseRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_sa_proto_msgTypes[47] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_sa_proto_msgTypes[42] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *PauseRequest) String() string { @@ -2861,8 +2521,8 @@ func (x *PauseRequest) String() string { func (*PauseRequest) ProtoMessage() {} func (x *PauseRequest) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[47] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_sa_proto_msgTypes[42] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2874,7 +2534,7 @@ func (x *PauseRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PauseRequest.ProtoReflect.Descriptor instead. func (*PauseRequest) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{47} + return file_sa_proto_rawDescGZIP(), []int{42} } func (x *PauseRequest) GetRegistrationID() int64 { @@ -2884,7 +2544,7 @@ func (x *PauseRequest) GetRegistrationID() int64 { return 0 } -func (x *PauseRequest) GetIdentifiers() []*Identifier { +func (x *PauseRequest) GetIdentifiers() []*proto.Identifier { if x != nil { return x.Identifiers } @@ -2892,21 +2552,18 @@ func (x *PauseRequest) GetIdentifiers() []*Identifier { } type PauseIdentifiersResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Paused int64 `protobuf:"varint,1,opt,name=paused,proto3" json:"paused,omitempty"` + Repaused int64 `protobuf:"varint,2,opt,name=repaused,proto3" json:"repaused,omitempty"` unknownFields protoimpl.UnknownFields - - Paused int64 `protobuf:"varint,1,opt,name=paused,proto3" json:"paused,omitempty"` - Repaused int64 `protobuf:"varint,2,opt,name=repaused,proto3" json:"repaused,omitempty"` + sizeCache protoimpl.SizeCache } func (x *PauseIdentifiersResponse) Reset() { *x = PauseIdentifiersResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_sa_proto_msgTypes[48] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_sa_proto_msgTypes[43] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *PauseIdentifiersResponse) String() string { @@ -2916,8 +2573,8 @@ func (x *PauseIdentifiersResponse) String() string { func (*PauseIdentifiersResponse) ProtoMessage() {} func (x *PauseIdentifiersResponse) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[48] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_sa_proto_msgTypes[43] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2929,7 +2586,7 @@ func (x *PauseIdentifiersResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use PauseIdentifiersResponse.ProtoReflect.Descriptor instead. func (*PauseIdentifiersResponse) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{48} + return file_sa_proto_rawDescGZIP(), []int{43} } func (x *PauseIdentifiersResponse) GetPaused() int64 { @@ -2946,33 +2603,30 @@ func (x *PauseIdentifiersResponse) GetRepaused() int64 { return 0 } -type ValidAuthorizations_MapElement struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Domain string `protobuf:"bytes,1,opt,name=domain,proto3" json:"domain,omitempty"` - Authz *proto.Authorization `protobuf:"bytes,2,opt,name=authz,proto3" json:"authz,omitempty"` +type UpdateRegistrationContactRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + RegistrationID int64 `protobuf:"varint,1,opt,name=registrationID,proto3" json:"registrationID,omitempty"` + Contacts []string `protobuf:"bytes,2,rep,name=contacts,proto3" json:"contacts,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *ValidAuthorizations_MapElement) Reset() { - *x = ValidAuthorizations_MapElement{} - if protoimpl.UnsafeEnabled { - mi := &file_sa_proto_msgTypes[49] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } +func (x *UpdateRegistrationContactRequest) Reset() { + *x = UpdateRegistrationContactRequest{} + mi := &file_sa_proto_msgTypes[44] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } -func (x *ValidAuthorizations_MapElement) String() string { +func (x *UpdateRegistrationContactRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ValidAuthorizations_MapElement) ProtoMessage() {} +func (*UpdateRegistrationContactRequest) ProtoMessage() {} -func (x *ValidAuthorizations_MapElement) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[49] - if protoimpl.UnsafeEnabled && x != nil { +func (x *UpdateRegistrationContactRequest) ProtoReflect() protoreflect.Message { + mi := &file_sa_proto_msgTypes[44] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2982,52 +2636,49 @@ func (x *ValidAuthorizations_MapElement) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ValidAuthorizations_MapElement.ProtoReflect.Descriptor instead. -func (*ValidAuthorizations_MapElement) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{5, 0} +// Deprecated: Use UpdateRegistrationContactRequest.ProtoReflect.Descriptor instead. +func (*UpdateRegistrationContactRequest) Descriptor() ([]byte, []int) { + return file_sa_proto_rawDescGZIP(), []int{44} } -func (x *ValidAuthorizations_MapElement) GetDomain() string { +func (x *UpdateRegistrationContactRequest) GetRegistrationID() int64 { if x != nil { - return x.Domain + return x.RegistrationID } - return "" + return 0 } -func (x *ValidAuthorizations_MapElement) GetAuthz() *proto.Authorization { +func (x *UpdateRegistrationContactRequest) GetContacts() []string { if x != nil { - return x.Authz + return x.Contacts } return nil } -type Authorizations_MapElement struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Domain string `protobuf:"bytes,1,opt,name=domain,proto3" json:"domain,omitempty"` - Authz *proto.Authorization `protobuf:"bytes,2,opt,name=authz,proto3" json:"authz,omitempty"` +type UpdateRegistrationKeyRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + RegistrationID int64 `protobuf:"varint,1,opt,name=registrationID,proto3" json:"registrationID,omitempty"` + Jwk []byte `protobuf:"bytes,2,opt,name=jwk,proto3" json:"jwk,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *Authorizations_MapElement) Reset() { - *x = Authorizations_MapElement{} - if protoimpl.UnsafeEnabled { - mi := &file_sa_proto_msgTypes[51] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } +func (x *UpdateRegistrationKeyRequest) Reset() { + *x = UpdateRegistrationKeyRequest{} + mi := &file_sa_proto_msgTypes[45] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } -func (x *Authorizations_MapElement) String() string { +func (x *UpdateRegistrationKeyRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*Authorizations_MapElement) ProtoMessage() {} +func (*UpdateRegistrationKeyRequest) ProtoMessage() {} -func (x *Authorizations_MapElement) ProtoReflect() protoreflect.Message { - mi := &file_sa_proto_msgTypes[51] - if protoimpl.UnsafeEnabled && x != nil { +func (x *UpdateRegistrationKeyRequest) ProtoReflect() protoreflect.Message { + mi := &file_sa_proto_msgTypes[45] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -3037,28 +2688,424 @@ func (x *Authorizations_MapElement) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use Authorizations_MapElement.ProtoReflect.Descriptor instead. -func (*Authorizations_MapElement) Descriptor() ([]byte, []int) { - return file_sa_proto_rawDescGZIP(), []int{29, 0} +// Deprecated: Use UpdateRegistrationKeyRequest.ProtoReflect.Descriptor instead. +func (*UpdateRegistrationKeyRequest) Descriptor() ([]byte, []int) { + return file_sa_proto_rawDescGZIP(), []int{45} } -func (x *Authorizations_MapElement) GetDomain() string { +func (x *UpdateRegistrationKeyRequest) GetRegistrationID() int64 { if x != nil { - return x.Domain + return x.RegistrationID + } + return 0 +} + +func (x *UpdateRegistrationKeyRequest) GetJwk() []byte { + if x != nil { + return x.Jwk + } + return nil +} + +type RateLimitOverride struct { + state protoimpl.MessageState `protogen:"open.v1"` + LimitEnum int64 `protobuf:"varint,1,opt,name=limitEnum,proto3" json:"limitEnum,omitempty"` + BucketKey string `protobuf:"bytes,2,opt,name=bucketKey,proto3" json:"bucketKey,omitempty"` + Comment string `protobuf:"bytes,3,opt,name=comment,proto3" json:"comment,omitempty"` + Period *durationpb.Duration `protobuf:"bytes,4,opt,name=period,proto3" json:"period,omitempty"` + Count int64 `protobuf:"varint,5,opt,name=count,proto3" json:"count,omitempty"` + Burst int64 `protobuf:"varint,6,opt,name=burst,proto3" json:"burst,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RateLimitOverride) Reset() { + *x = RateLimitOverride{} + mi := &file_sa_proto_msgTypes[46] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RateLimitOverride) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RateLimitOverride) ProtoMessage() {} + +func (x *RateLimitOverride) ProtoReflect() protoreflect.Message { + mi := &file_sa_proto_msgTypes[46] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RateLimitOverride.ProtoReflect.Descriptor instead. +func (*RateLimitOverride) Descriptor() ([]byte, []int) { + return file_sa_proto_rawDescGZIP(), []int{46} +} + +func (x *RateLimitOverride) GetLimitEnum() int64 { + if x != nil { + return x.LimitEnum + } + return 0 +} + +func (x *RateLimitOverride) GetBucketKey() string { + if x != nil { + return x.BucketKey } return "" } -func (x *Authorizations_MapElement) GetAuthz() *proto.Authorization { +func (x *RateLimitOverride) GetComment() string { if x != nil { - return x.Authz + return x.Comment + } + return "" +} + +func (x *RateLimitOverride) GetPeriod() *durationpb.Duration { + if x != nil { + return x.Period + } + return nil +} + +func (x *RateLimitOverride) GetCount() int64 { + if x != nil { + return x.Count + } + return 0 +} + +func (x *RateLimitOverride) GetBurst() int64 { + if x != nil { + return x.Burst + } + return 0 +} + +type AddRateLimitOverrideRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Override *RateLimitOverride `protobuf:"bytes,1,opt,name=override,proto3" json:"override,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AddRateLimitOverrideRequest) Reset() { + *x = AddRateLimitOverrideRequest{} + mi := &file_sa_proto_msgTypes[47] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AddRateLimitOverrideRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddRateLimitOverrideRequest) ProtoMessage() {} + +func (x *AddRateLimitOverrideRequest) ProtoReflect() protoreflect.Message { + mi := &file_sa_proto_msgTypes[47] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AddRateLimitOverrideRequest.ProtoReflect.Descriptor instead. +func (*AddRateLimitOverrideRequest) Descriptor() ([]byte, []int) { + return file_sa_proto_rawDescGZIP(), []int{47} +} + +func (x *AddRateLimitOverrideRequest) GetOverride() *RateLimitOverride { + if x != nil { + return x.Override + } + return nil +} + +type AddRateLimitOverrideResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Inserted bool `protobuf:"varint,1,opt,name=inserted,proto3" json:"inserted,omitempty"` + Enabled bool `protobuf:"varint,2,opt,name=enabled,proto3" json:"enabled,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AddRateLimitOverrideResponse) Reset() { + *x = AddRateLimitOverrideResponse{} + mi := &file_sa_proto_msgTypes[48] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AddRateLimitOverrideResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddRateLimitOverrideResponse) ProtoMessage() {} + +func (x *AddRateLimitOverrideResponse) ProtoReflect() protoreflect.Message { + mi := &file_sa_proto_msgTypes[48] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AddRateLimitOverrideResponse.ProtoReflect.Descriptor instead. +func (*AddRateLimitOverrideResponse) Descriptor() ([]byte, []int) { + return file_sa_proto_rawDescGZIP(), []int{48} +} + +func (x *AddRateLimitOverrideResponse) GetInserted() bool { + if x != nil { + return x.Inserted + } + return false +} + +func (x *AddRateLimitOverrideResponse) GetEnabled() bool { + if x != nil { + return x.Enabled + } + return false +} + +type EnableRateLimitOverrideRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + LimitEnum int64 `protobuf:"varint,1,opt,name=limitEnum,proto3" json:"limitEnum,omitempty"` + BucketKey string `protobuf:"bytes,2,opt,name=bucketKey,proto3" json:"bucketKey,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EnableRateLimitOverrideRequest) Reset() { + *x = EnableRateLimitOverrideRequest{} + mi := &file_sa_proto_msgTypes[49] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EnableRateLimitOverrideRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EnableRateLimitOverrideRequest) ProtoMessage() {} + +func (x *EnableRateLimitOverrideRequest) ProtoReflect() protoreflect.Message { + mi := &file_sa_proto_msgTypes[49] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EnableRateLimitOverrideRequest.ProtoReflect.Descriptor instead. +func (*EnableRateLimitOverrideRequest) Descriptor() ([]byte, []int) { + return file_sa_proto_rawDescGZIP(), []int{49} +} + +func (x *EnableRateLimitOverrideRequest) GetLimitEnum() int64 { + if x != nil { + return x.LimitEnum + } + return 0 +} + +func (x *EnableRateLimitOverrideRequest) GetBucketKey() string { + if x != nil { + return x.BucketKey + } + return "" +} + +type DisableRateLimitOverrideRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + LimitEnum int64 `protobuf:"varint,1,opt,name=limitEnum,proto3" json:"limitEnum,omitempty"` + BucketKey string `protobuf:"bytes,2,opt,name=bucketKey,proto3" json:"bucketKey,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DisableRateLimitOverrideRequest) Reset() { + *x = DisableRateLimitOverrideRequest{} + mi := &file_sa_proto_msgTypes[50] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DisableRateLimitOverrideRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DisableRateLimitOverrideRequest) ProtoMessage() {} + +func (x *DisableRateLimitOverrideRequest) ProtoReflect() protoreflect.Message { + mi := &file_sa_proto_msgTypes[50] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DisableRateLimitOverrideRequest.ProtoReflect.Descriptor instead. +func (*DisableRateLimitOverrideRequest) Descriptor() ([]byte, []int) { + return file_sa_proto_rawDescGZIP(), []int{50} +} + +func (x *DisableRateLimitOverrideRequest) GetLimitEnum() int64 { + if x != nil { + return x.LimitEnum + } + return 0 +} + +func (x *DisableRateLimitOverrideRequest) GetBucketKey() string { + if x != nil { + return x.BucketKey + } + return "" +} + +type GetRateLimitOverrideRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + LimitEnum int64 `protobuf:"varint,1,opt,name=limitEnum,proto3" json:"limitEnum,omitempty"` + BucketKey string `protobuf:"bytes,2,opt,name=bucketKey,proto3" json:"bucketKey,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetRateLimitOverrideRequest) Reset() { + *x = GetRateLimitOverrideRequest{} + mi := &file_sa_proto_msgTypes[51] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetRateLimitOverrideRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetRateLimitOverrideRequest) ProtoMessage() {} + +func (x *GetRateLimitOverrideRequest) ProtoReflect() protoreflect.Message { + mi := &file_sa_proto_msgTypes[51] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetRateLimitOverrideRequest.ProtoReflect.Descriptor instead. +func (*GetRateLimitOverrideRequest) Descriptor() ([]byte, []int) { + return file_sa_proto_rawDescGZIP(), []int{51} +} + +func (x *GetRateLimitOverrideRequest) GetLimitEnum() int64 { + if x != nil { + return x.LimitEnum + } + return 0 +} + +func (x *GetRateLimitOverrideRequest) GetBucketKey() string { + if x != nil { + return x.BucketKey + } + return "" +} + +type RateLimitOverrideResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Override *RateLimitOverride `protobuf:"bytes,1,opt,name=override,proto3" json:"override,omitempty"` + Enabled bool `protobuf:"varint,2,opt,name=enabled,proto3" json:"enabled,omitempty"` + UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=updatedAt,proto3" json:"updatedAt,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RateLimitOverrideResponse) Reset() { + *x = RateLimitOverrideResponse{} + mi := &file_sa_proto_msgTypes[52] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RateLimitOverrideResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RateLimitOverrideResponse) ProtoMessage() {} + +func (x *RateLimitOverrideResponse) ProtoReflect() protoreflect.Message { + mi := &file_sa_proto_msgTypes[52] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RateLimitOverrideResponse.ProtoReflect.Descriptor instead. +func (*RateLimitOverrideResponse) Descriptor() ([]byte, []int) { + return file_sa_proto_rawDescGZIP(), []int{52} +} + +func (x *RateLimitOverrideResponse) GetOverride() *RateLimitOverride { + if x != nil { + return x.Override + } + return nil +} + +func (x *RateLimitOverrideResponse) GetEnabled() bool { + if x != nil { + return x.Enabled + } + return false +} + +func (x *RateLimitOverrideResponse) GetUpdatedAt() *timestamppb.Timestamp { + if x != nil { + return x.UpdatedAt } return nil } var File_sa_proto protoreflect.FileDescriptor -var file_sa_proto_rawDesc = []byte{ +var file_sa_proto_rawDesc = string([]byte{ 0x0a, 0x08, 0x73, 0x61, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x02, 0x73, 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, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, @@ -3073,298 +3120,294 @@ var file_sa_proto_rawDesc = []byte{ 0x4b, 0x65, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6a, 0x77, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x6a, 0x77, 0x6b, 0x22, 0x21, 0x0a, 0x0f, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0xdc, 0x01, 0x0a, 0x1e, 0x47, 0x65, 0x74, - 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0e, 0x72, - 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x49, 0x44, 0x12, 0x26, 0x0a, 0x0e, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, - 0x72, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x69, 0x64, 0x65, - 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x54, 0x79, 0x70, 0x65, 0x12, 0x28, 0x0a, 0x0f, 0x69, - 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, - 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x3a, 0x0a, 0x0a, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x55, 0x6e, - 0x74, 0x69, 0x6c, 0x18, 0x05, 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, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x55, 0x6e, 0x74, 0x69, - 0x6c, 0x4a, 0x04, 0x08, 0x04, 0x10, 0x05, 0x22, 0x95, 0x01, 0x0a, 0x1d, 0x47, 0x65, 0x74, 0x56, - 0x61, 0x6c, 0x69, 0x64, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0e, 0x72, 0x65, 0x67, - 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, - 0x44, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, - 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x2c, 0x0a, 0x03, 0x6e, - 0x6f, 0x77, 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, 0x03, 0x6e, 0x6f, 0x77, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x22, - 0xa0, 0x01, 0x0a, 0x13, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, - 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x38, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x69, 0x64, - 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x73, 0x61, 0x2e, 0x56, 0x61, 0x6c, 0x69, - 0x64, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, - 0x4d, 0x61, 0x70, 0x45, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x69, - 0x64, 0x1a, 0x4f, 0x0a, 0x0a, 0x4d, 0x61, 0x70, 0x45, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x12, - 0x16, 0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x29, 0x0a, 0x05, 0x61, 0x75, 0x74, 0x68, 0x7a, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x41, 0x75, - 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x05, 0x61, 0x75, 0x74, - 0x68, 0x7a, 0x22, 0x20, 0x0a, 0x06, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, 0x16, 0x0a, 0x06, - 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x65, - 0x72, 0x69, 0x61, 0x6c, 0x22, 0xc8, 0x01, 0x0a, 0x0e, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x4d, - 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x65, 0x72, 0x69, 0x61, - 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, - 0x26, 0x0a, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, - 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x12, 0x34, 0x0a, 0x07, 0x63, 0x72, 0x65, 0x61, 0x74, - 0x65, 0x64, 0x18, 0x05, 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, 0x07, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x12, 0x34, 0x0a, - 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x18, 0x06, 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, 0x07, 0x65, 0x78, 0x70, 0x69, - 0x72, 0x65, 0x73, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x4a, 0x04, 0x08, 0x04, 0x10, 0x05, 0x22, - 0x7f, 0x0a, 0x05, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x36, 0x0a, 0x08, 0x65, 0x61, 0x72, 0x6c, - 0x69, 0x65, 0x73, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0xdd, 0x01, 0x0a, 0x1d, 0x47, 0x65, 0x74, + 0x56, 0x61, 0x6c, 0x69, 0x64, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0e, 0x72, 0x65, + 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x49, 0x44, 0x12, 0x32, 0x0a, 0x0b, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, + 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x49, + 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x0b, 0x69, 0x64, 0x65, 0x6e, 0x74, + 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x12, 0x3a, 0x0a, 0x0a, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x55, + 0x6e, 0x74, 0x69, 0x6c, 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, 0x08, 0x65, 0x61, 0x72, 0x6c, 0x69, 0x65, 0x73, 0x74, - 0x12, 0x32, 0x0a, 0x06, 0x6c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x55, 0x6e, 0x74, + 0x69, 0x6c, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x07, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x4a, 0x04, 0x08, 0x02, + 0x10, 0x03, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x22, 0x20, 0x0a, 0x06, 0x53, 0x65, 0x72, 0x69, + 0x61, 0x6c, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x06, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x22, 0xc8, 0x01, 0x0a, 0x0e, 0x53, + 0x65, 0x72, 0x69, 0x61, 0x6c, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x16, 0x0a, + 0x06, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, + 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, 0x26, 0x0a, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x72, + 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x12, 0x34, 0x0a, + 0x07, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x18, 0x05, 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, 0x07, 0x63, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x64, 0x12, 0x34, 0x0a, 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x18, 0x06, + 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, 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x4a, + 0x04, 0x08, 0x04, 0x10, 0x05, 0x22, 0x7f, 0x0a, 0x05, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x36, + 0x0a, 0x08, 0x65, 0x61, 0x72, 0x6c, 0x69, 0x65, 0x73, 0x74, 0x18, 0x03, 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, 0x06, 0x6c, 0x61, - 0x74, 0x65, 0x73, 0x74, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, - 0x22, 0x1d, 0x0a, 0x05, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, - 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22, - 0x4e, 0x0a, 0x0a, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x73, 0x12, 0x3a, 0x0a, - 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, 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, - 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x73, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x22, - 0x58, 0x0a, 0x1f, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, - 0x61, 0x74, 0x65, 0x73, 0x42, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x1f, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x09, 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x05, 0x72, 0x61, - 0x6e, 0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, - 0x28, 0x09, 0x52, 0x05, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x22, 0xb7, 0x01, 0x0a, 0x0c, 0x43, 0x6f, - 0x75, 0x6e, 0x74, 0x42, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x12, 0x34, 0x0a, 0x06, 0x63, 0x6f, - 0x75, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x73, 0x61, 0x2e, - 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x42, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x2e, 0x43, 0x6f, 0x75, - 0x6e, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, - 0x12, 0x36, 0x0a, 0x08, 0x65, 0x61, 0x72, 0x6c, 0x69, 0x65, 0x73, 0x74, 0x18, 0x02, 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, 0x08, - 0x65, 0x61, 0x72, 0x6c, 0x69, 0x65, 0x73, 0x74, 0x1a, 0x39, 0x0a, 0x0b, 0x43, 0x6f, 0x75, 0x6e, - 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, - 0x02, 0x38, 0x01, 0x22, 0x50, 0x0a, 0x1d, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x67, 0x69, - 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x79, 0x49, 0x50, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, - 0x52, 0x02, 0x69, 0x70, 0x12, 0x1f, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x09, 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x05, - 0x72, 0x61, 0x6e, 0x67, 0x65, 0x22, 0x88, 0x01, 0x0a, 0x21, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x49, + 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x08, 0x65, 0x61, + 0x72, 0x6c, 0x69, 0x65, 0x73, 0x74, 0x12, 0x32, 0x0a, 0x06, 0x6c, 0x61, 0x74, 0x65, 0x73, 0x74, + 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, 0x06, 0x6c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, + 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x22, 0x1d, 0x0a, 0x05, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, + 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x4e, 0x0a, 0x0a, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, + 0x6d, 0x70, 0x73, 0x12, 0x3a, 0x0a, 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x73, 0x18, 0x02, 0x20, 0x03, 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, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x73, 0x4a, + 0x04, 0x08, 0x01, 0x10, 0x02, 0x22, 0xa4, 0x01, 0x0a, 0x21, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x49, 0x44, 0x12, 0x1a, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, - 0x1f, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x09, - 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, - 0x22, 0x53, 0x0a, 0x12, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, - 0x74, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x61, 0x63, 0x63, 0x6f, 0x75, - 0x6e, 0x74, 0x49, 0x44, 0x12, 0x1f, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x09, 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x05, - 0x72, 0x61, 0x6e, 0x67, 0x65, 0x22, 0x69, 0x0a, 0x14, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x46, 0x51, - 0x44, 0x4e, 0x53, 0x65, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, - 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, - 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x31, 0x0a, 0x06, 0x77, 0x69, 0x6e, 0x64, 0x6f, - 0x77, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x52, 0x06, 0x77, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, - 0x22, 0x30, 0x0a, 0x14, 0x46, 0x51, 0x44, 0x4e, 0x53, 0x65, 0x74, 0x45, 0x78, 0x69, 0x73, 0x74, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, - 0x69, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, - 0x6e, 0x73, 0x22, 0x20, 0x0a, 0x06, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x12, 0x16, 0x0a, 0x06, - 0x65, 0x78, 0x69, 0x73, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x65, 0x78, - 0x69, 0x73, 0x74, 0x73, 0x22, 0xb8, 0x01, 0x0a, 0x10, 0x41, 0x64, 0x64, 0x53, 0x65, 0x72, 0x69, - 0x61, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x65, 0x67, - 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x72, 0x65, 0x67, 0x49, 0x44, 0x12, - 0x16, 0x0a, 0x06, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x06, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, 0x34, 0x0a, 0x07, 0x63, 0x72, 0x65, 0x61, 0x74, - 0x65, 0x64, 0x18, 0x05, 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, 0x07, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x12, 0x34, 0x0a, - 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x18, 0x06, 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, 0x07, 0x65, 0x78, 0x70, 0x69, - 0x72, 0x65, 0x73, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x4a, 0x04, 0x08, 0x04, 0x10, 0x05, 0x22, - 0xc7, 0x01, 0x0a, 0x15, 0x41, 0x64, 0x64, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, - 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x64, 0x65, 0x72, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x64, 0x65, 0x72, 0x12, 0x14, 0x0a, 0x05, 0x72, - 0x65, 0x67, 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x72, 0x65, 0x67, 0x49, - 0x44, 0x12, 0x32, 0x0a, 0x06, 0x69, 0x73, 0x73, 0x75, 0x65, 0x64, 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, 0x06, 0x69, - 0x73, 0x73, 0x75, 0x65, 0x64, 0x12, 0x22, 0x0a, 0x0c, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x4e, - 0x61, 0x6d, 0x65, 0x49, 0x44, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x69, 0x73, 0x73, - 0x75, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x49, 0x44, 0x12, 0x22, 0x0a, 0x0c, 0x6f, 0x63, 0x73, - 0x70, 0x4e, 0x6f, 0x74, 0x52, 0x65, 0x61, 0x64, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x0c, 0x6f, 0x63, 0x73, 0x70, 0x4e, 0x6f, 0x74, 0x52, 0x65, 0x61, 0x64, 0x79, 0x4a, 0x04, 0x08, - 0x03, 0x10, 0x04, 0x4a, 0x04, 0x08, 0x04, 0x10, 0x05, 0x22, 0x1e, 0x0a, 0x0c, 0x4f, 0x72, 0x64, - 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x22, 0x97, 0x02, 0x0a, 0x0f, 0x4e, 0x65, - 0x77, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, - 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x12, 0x34, 0x0a, 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, + 0x6e, 0x49, 0x44, 0x12, 0x30, 0x0a, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, + 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x49, + 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, + 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x1f, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x09, 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, + 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x22, 0x9f, 0x01, 0x0a, + 0x14, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x46, 0x51, 0x44, 0x4e, 0x53, 0x65, 0x74, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x32, 0x0a, 0x0b, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, + 0x69, 0x65, 0x72, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x6f, 0x72, + 0x65, 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x0b, 0x69, 0x64, + 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x12, 0x31, 0x0a, 0x06, 0x77, 0x69, 0x6e, + 0x64, 0x6f, 0x77, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x77, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x12, 0x14, 0x0a, 0x05, + 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, + 0x69, 0x74, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x22, 0x50, + 0x0a, 0x14, 0x46, 0x51, 0x44, 0x4e, 0x53, 0x65, 0x74, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x32, 0x0a, 0x0b, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, + 0x66, 0x69, 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x6f, + 0x72, 0x65, 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x0b, 0x69, + 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, + 0x22, 0x20, 0x0a, 0x06, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x65, 0x78, + 0x69, 0x73, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x65, 0x78, 0x69, 0x73, + 0x74, 0x73, 0x22, 0xb8, 0x01, 0x0a, 0x10, 0x41, 0x64, 0x64, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x65, 0x67, 0x49, 0x44, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x72, 0x65, 0x67, 0x49, 0x44, 0x12, 0x16, 0x0a, + 0x06, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, + 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, 0x34, 0x0a, 0x07, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x18, 0x05, 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, 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6e, - 0x61, 0x6d, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x6e, 0x61, 0x6d, 0x65, - 0x73, 0x12, 0x2a, 0x0a, 0x10, 0x76, 0x32, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x03, 0x52, 0x10, 0x76, 0x32, 0x41, - 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x26, 0x0a, + 0x6d, 0x70, 0x52, 0x07, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x12, 0x34, 0x0a, 0x07, 0x65, + 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x18, 0x06, 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, 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, + 0x73, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x4a, 0x04, 0x08, 0x04, 0x10, 0x05, 0x22, 0xc7, 0x01, + 0x0a, 0x15, 0x41, 0x64, 0x64, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x64, 0x65, 0x72, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x64, 0x65, 0x72, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x65, 0x67, + 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x72, 0x65, 0x67, 0x49, 0x44, 0x12, + 0x32, 0x0a, 0x06, 0x69, 0x73, 0x73, 0x75, 0x65, 0x64, 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, 0x06, 0x69, 0x73, 0x73, + 0x75, 0x65, 0x64, 0x12, 0x22, 0x0a, 0x0c, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x4e, 0x61, 0x6d, + 0x65, 0x49, 0x44, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x69, 0x73, 0x73, 0x75, 0x65, + 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x49, 0x44, 0x12, 0x22, 0x0a, 0x0c, 0x6f, 0x63, 0x73, 0x70, 0x4e, + 0x6f, 0x74, 0x52, 0x65, 0x61, 0x64, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x6f, + 0x63, 0x73, 0x70, 0x4e, 0x6f, 0x74, 0x52, 0x65, 0x61, 0x64, 0x79, 0x4a, 0x04, 0x08, 0x03, 0x10, + 0x04, 0x4a, 0x04, 0x08, 0x04, 0x10, 0x05, 0x22, 0x1e, 0x0a, 0x0c, 0x4f, 0x72, 0x64, 0x65, 0x72, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x22, 0xd7, 0x02, 0x0a, 0x0f, 0x4e, 0x65, 0x77, 0x4f, + 0x72, 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0e, 0x72, + 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x49, 0x44, 0x12, 0x34, 0x0a, 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x18, 0x05, + 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, 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x12, 0x32, 0x0a, 0x0b, 0x69, 0x64, 0x65, + 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, + 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, + 0x52, 0x0b, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x12, 0x2a, 0x0a, + 0x10, 0x76, 0x32, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x03, 0x52, 0x10, 0x76, 0x32, 0x41, 0x75, 0x74, 0x68, 0x6f, + 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x36, 0x0a, 0x16, 0x63, 0x65, 0x72, + 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x4e, + 0x61, 0x6d, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x16, 0x63, 0x65, 0x72, 0x74, 0x69, + 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x4e, 0x61, 0x6d, + 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x73, 0x18, 0x08, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x73, 0x12, 0x26, 0x0a, 0x0e, 0x72, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x73, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x72, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x73, 0x53, - 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, 0x36, 0x0a, 0x16, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, - 0x63, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18, - 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x16, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, - 0x74, 0x65, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x4a, 0x04, 0x08, - 0x02, 0x10, 0x03, 0x22, 0x7e, 0x0a, 0x18, 0x4e, 0x65, 0x77, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x41, - 0x6e, 0x64, 0x41, 0x75, 0x74, 0x68, 0x7a, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x2f, 0x0a, 0x08, 0x6e, 0x65, 0x77, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x13, 0x2e, 0x73, 0x61, 0x2e, 0x4e, 0x65, 0x77, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x08, 0x6e, 0x65, 0x77, 0x4f, 0x72, 0x64, 0x65, 0x72, - 0x12, 0x31, 0x0a, 0x09, 0x6e, 0x65, 0x77, 0x41, 0x75, 0x74, 0x68, 0x7a, 0x73, 0x18, 0x02, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, - 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x6e, 0x65, 0x77, 0x41, 0x75, 0x74, - 0x68, 0x7a, 0x73, 0x22, 0x52, 0x0a, 0x14, 0x53, 0x65, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x45, - 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x12, 0x2a, 0x0a, 0x05, 0x65, - 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x63, 0x6f, 0x72, - 0x65, 0x2e, 0x50, 0x72, 0x6f, 0x62, 0x6c, 0x65, 0x6d, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, - 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x4c, 0x0a, 0x22, 0x47, 0x65, 0x74, 0x56, 0x61, - 0x6c, 0x69, 0x64, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, - 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x12, 0x16, 0x0a, - 0x06, 0x61, 0x63, 0x63, 0x74, 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x61, - 0x63, 0x63, 0x74, 0x49, 0x44, 0x22, 0x47, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x4f, 0x72, 0x64, 0x65, - 0x72, 0x46, 0x6f, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x16, 0x0a, 0x06, 0x61, 0x63, 0x63, 0x74, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, - 0x52, 0x06, 0x61, 0x63, 0x63, 0x74, 0x49, 0x44, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x61, 0x6d, 0x65, - 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x22, 0x54, - 0x0a, 0x14, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x12, 0x2c, 0x0a, 0x11, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, - 0x69, 0x63, 0x61, 0x74, 0x65, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x11, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x53, 0x65, - 0x72, 0x69, 0x61, 0x6c, 0x22, 0x90, 0x01, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, - 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x26, 0x0a, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, - 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, - 0x61, 0x69, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, - 0x69, 0x6e, 0x73, 0x12, 0x2c, 0x0a, 0x03, 0x6e, 0x6f, 0x77, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, + 0x65, 0x72, 0x69, 0x61, 0x6c, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x4a, 0x04, 0x08, 0x03, 0x10, + 0x04, 0x22, 0x89, 0x02, 0x0a, 0x0f, 0x4e, 0x65, 0x77, 0x41, 0x75, 0x74, 0x68, 0x7a, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x30, 0x0a, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, + 0x69, 0x65, 0x72, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x6f, 0x72, 0x65, + 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x0a, 0x69, 0x64, 0x65, + 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 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, + 0x34, 0x0a, 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x18, 0x09, 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, 0x03, 0x6e, 0x6f, - 0x77, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x22, 0x96, 0x01, 0x0a, 0x0e, 0x41, 0x75, 0x74, 0x68, - 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x33, 0x0a, 0x05, 0x61, 0x75, - 0x74, 0x68, 0x7a, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x73, 0x61, 0x2e, 0x41, - 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x4d, 0x61, - 0x70, 0x45, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x05, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x1a, - 0x4f, 0x0a, 0x0a, 0x4d, 0x61, 0x70, 0x45, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x16, 0x0a, - 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, - 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x29, 0x0a, 0x05, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x41, 0x75, 0x74, 0x68, - 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x05, 0x61, 0x75, 0x74, 0x68, 0x7a, - 0x22, 0x24, 0x0a, 0x10, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x49, 0x44, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, - 0x09, 0x52, 0x03, 0x69, 0x64, 0x73, 0x22, 0x22, 0x0a, 0x10, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, - 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x32, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x22, 0x92, 0x02, 0x0a, 0x18, 0x52, - 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x65, 0x72, 0x69, 0x61, - 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, - 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x2e, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x65, 0x18, + 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, 0x65, 0x78, + 0x70, 0x69, 0x72, 0x65, 0x73, 0x12, 0x26, 0x0a, 0x0e, 0x63, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, + 0x67, 0x65, 0x54, 0x79, 0x70, 0x65, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0e, 0x63, + 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x54, 0x79, 0x70, 0x65, 0x73, 0x12, 0x14, 0x0a, + 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, + 0x6b, 0x65, 0x6e, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x4a, + 0x04, 0x08, 0x04, 0x10, 0x05, 0x4a, 0x04, 0x08, 0x05, 0x10, 0x06, 0x4a, 0x04, 0x08, 0x06, 0x10, + 0x07, 0x4a, 0x04, 0x08, 0x07, 0x10, 0x08, 0x4a, 0x04, 0x08, 0x08, 0x10, 0x09, 0x22, 0x7e, 0x0a, + 0x18, 0x4e, 0x65, 0x77, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x41, 0x6e, 0x64, 0x41, 0x75, 0x74, 0x68, + 0x7a, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2f, 0x0a, 0x08, 0x6e, 0x65, 0x77, + 0x4f, 0x72, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x73, 0x61, + 0x2e, 0x4e, 0x65, 0x77, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x52, 0x08, 0x6e, 0x65, 0x77, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x31, 0x0a, 0x09, 0x6e, 0x65, + 0x77, 0x41, 0x75, 0x74, 0x68, 0x7a, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, + 0x73, 0x61, 0x2e, 0x4e, 0x65, 0x77, 0x41, 0x75, 0x74, 0x68, 0x7a, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x52, 0x09, 0x6e, 0x65, 0x77, 0x41, 0x75, 0x74, 0x68, 0x7a, 0x73, 0x22, 0x52, 0x0a, + 0x14, 0x53, 0x65, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x02, 0x69, 0x64, 0x12, 0x2a, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x50, 0x72, 0x6f, 0x62, + 0x6c, 0x65, 0x6d, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, + 0x72, 0x22, 0x4c, 0x0a, 0x22, 0x47, 0x65, 0x74, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x4f, 0x72, 0x64, + 0x65, 0x72, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x63, 0x63, 0x74, 0x49, + 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x61, 0x63, 0x63, 0x74, 0x49, 0x44, 0x22, + 0x6b, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x46, 0x6f, 0x72, 0x4e, 0x61, + 0x6d, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x63, + 0x63, 0x74, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x61, 0x63, 0x63, 0x74, + 0x49, 0x44, 0x12, 0x32, 0x0a, 0x0b, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, + 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x49, + 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x0b, 0x69, 0x64, 0x65, 0x6e, 0x74, + 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x22, 0x54, 0x0a, 0x14, + 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x02, 0x69, 0x64, 0x12, 0x2c, 0x0a, 0x11, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, + 0x61, 0x74, 0x65, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x11, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x53, 0x65, 0x72, 0x69, + 0x61, 0x6c, 0x22, 0xd8, 0x01, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, + 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x26, 0x0a, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, + 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x12, 0x32, 0x0a, 0x0b, 0x69, 0x64, 0x65, 0x6e, 0x74, + 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, + 0x6f, 0x72, 0x65, 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x0b, + 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x12, 0x3a, 0x0a, 0x0a, 0x76, + 0x61, 0x6c, 0x69, 0x64, 0x55, 0x6e, 0x74, 0x69, 0x6c, 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, 0x76, 0x61, 0x6c, + 0x69, 0x64, 0x55, 0x6e, 0x74, 0x69, 0x6c, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x6f, 0x66, 0x69, + 0x6c, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, + 0x65, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x22, 0x3d, 0x0a, + 0x0e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, + 0x2b, 0x0a, 0x06, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x13, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x73, 0x22, 0x24, 0x0a, 0x10, + 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x73, + 0x12, 0x10, 0x0a, 0x03, 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x03, 0x69, + 0x64, 0x73, 0x22, 0x22, 0x0a, 0x10, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x32, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x22, 0x92, 0x02, 0x0a, 0x18, 0x52, 0x65, 0x76, 0x6f, 0x6b, + 0x65, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, 0x16, 0x0a, 0x06, 0x72, + 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x72, 0x65, 0x61, + 0x73, 0x6f, 0x6e, 0x12, 0x2e, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x65, 0x18, 0x08, 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, 0x04, 0x64, + 0x61, 0x74, 0x65, 0x12, 0x36, 0x0a, 0x08, 0x62, 0x61, 0x63, 0x6b, 0x64, 0x61, 0x74, 0x65, 0x18, + 0x09, 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, 0x08, 0x62, 0x61, 0x63, 0x6b, 0x64, 0x61, 0x74, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x72, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x72, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 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, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x68, 0x61, 0x72, 0x64, 0x49, 0x64, 0x78, 0x18, + 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x73, 0x68, 0x61, 0x72, 0x64, 0x49, 0x64, 0x78, 0x4a, + 0x04, 0x08, 0x03, 0x10, 0x04, 0x4a, 0x04, 0x08, 0x05, 0x10, 0x06, 0x22, 0xea, 0x02, 0x0a, 0x1c, + 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 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, 0x34, 0x0a, 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x18, 0x08, 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, 0x04, 0x64, 0x61, 0x74, 0x65, 0x12, 0x36, 0x0a, 0x08, 0x62, 0x61, 0x63, 0x6b, 0x64, - 0x61, 0x74, 0x65, 0x18, 0x09, 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, 0x08, 0x62, 0x61, 0x63, 0x6b, 0x64, 0x61, 0x74, 0x65, 0x12, - 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x0c, 0x52, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 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, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x68, 0x61, 0x72, 0x64, - 0x49, 0x64, 0x78, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x73, 0x68, 0x61, 0x72, 0x64, - 0x49, 0x64, 0x78, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x4a, 0x04, 0x08, 0x05, 0x10, 0x06, 0x22, - 0xea, 0x02, 0x0a, 0x1c, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x41, 0x75, 0x74, 0x68, - 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, - 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, 0x34, 0x0a, 0x07, 0x65, 0x78, 0x70, 0x69, - 0x72, 0x65, 0x73, 0x18, 0x08, 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, 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x12, 0x1c, - 0x0a, 0x09, 0x61, 0x74, 0x74, 0x65, 0x6d, 0x70, 0x74, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x09, 0x61, 0x74, 0x74, 0x65, 0x6d, 0x70, 0x74, 0x65, 0x64, 0x12, 0x44, 0x0a, 0x11, - 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, - 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x56, - 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, - 0x11, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, - 0x64, 0x73, 0x12, 0x3e, 0x0a, 0x0f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x45, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x63, 0x6f, - 0x72, 0x65, 0x2e, 0x50, 0x72, 0x6f, 0x62, 0x6c, 0x65, 0x6d, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, - 0x73, 0x52, 0x0f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x72, 0x72, - 0x6f, 0x72, 0x12, 0x3c, 0x0a, 0x0b, 0x61, 0x74, 0x74, 0x65, 0x6d, 0x70, 0x74, 0x65, 0x64, 0x41, - 0x74, 0x18, 0x09, 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, 0x0b, 0x61, 0x74, 0x74, 0x65, 0x6d, 0x70, 0x74, 0x65, 0x64, 0x41, 0x74, - 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x4a, 0x04, 0x08, 0x07, 0x10, 0x08, 0x22, 0xb8, 0x01, 0x0a, - 0x14, 0x41, 0x64, 0x64, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x6b, 0x65, 0x79, 0x48, 0x61, 0x73, 0x68, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6b, 0x65, 0x79, 0x48, 0x61, 0x73, 0x68, 0x12, - 0x30, 0x0a, 0x05, 0x61, 0x64, 0x64, 0x65, 0x64, 0x18, 0x06, 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, 0x05, 0x61, 0x64, 0x64, 0x65, - 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d, - 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, - 0x65, 0x6e, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x42, 0x79, - 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x42, - 0x79, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x22, 0x24, 0x0a, 0x08, 0x53, 0x50, 0x4b, 0x49, 0x48, - 0x61, 0x73, 0x68, 0x12, 0x18, 0x0a, 0x07, 0x6b, 0x65, 0x79, 0x48, 0x61, 0x73, 0x68, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6b, 0x65, 0x79, 0x48, 0x61, 0x73, 0x68, 0x22, 0xa4, 0x01, - 0x0a, 0x08, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x12, 0x20, 0x0a, 0x0b, 0x73, 0x65, - 0x72, 0x69, 0x61, 0x6c, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0b, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x10, 0x0a, 0x03, - 0x75, 0x72, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x34, - 0x0a, 0x07, 0x72, 0x65, 0x6e, 0x65, 0x77, 0x42, 0x79, 0x18, 0x06, 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, 0x07, 0x72, 0x65, 0x6e, - 0x65, 0x77, 0x42, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x4a, 0x04, - 0x08, 0x04, 0x10, 0x05, 0x22, 0x37, 0x0a, 0x09, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, - 0x73, 0x12, 0x2a, 0x0a, 0x09, 0x69, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x73, 0x61, 0x2e, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, - 0x6e, 0x74, 0x52, 0x09, 0x69, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x73, 0x22, 0x41, 0x0a, - 0x19, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x73, 0x46, 0x6f, 0x72, 0x49, 0x6e, 0x63, 0x69, 0x64, - 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x24, 0x0a, 0x0d, 0x69, 0x6e, - 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0d, 0x69, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x54, 0x61, 0x62, 0x6c, 0x65, - 0x22, 0xb4, 0x01, 0x0a, 0x0e, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, - 0x69, 0x61, 0x6c, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, 0x26, 0x0a, 0x0e, 0x72, - 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x02, 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, 0x03, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x49, 0x44, 0x12, 0x42, 0x0a, - 0x0e, 0x6c, 0x61, 0x73, 0x74, 0x4e, 0x6f, 0x74, 0x69, 0x63, 0x65, 0x53, 0x65, 0x6e, 0x74, 0x18, - 0x05, 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, 0x0e, 0x6c, 0x61, 0x73, 0x74, 0x4e, 0x6f, 0x74, 0x69, 0x63, 0x65, 0x53, 0x65, 0x6e, - 0x74, 0x4a, 0x04, 0x08, 0x04, 0x10, 0x05, 0x22, 0xae, 0x02, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x52, + 0x70, 0x52, 0x07, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x74, + 0x74, 0x65, 0x6d, 0x70, 0x74, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, + 0x74, 0x74, 0x65, 0x6d, 0x70, 0x74, 0x65, 0x64, 0x12, 0x44, 0x0a, 0x11, 0x76, 0x61, 0x6c, 0x69, + 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x05, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x11, 0x76, 0x61, 0x6c, + 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x12, 0x3e, + 0x0a, 0x0f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x72, 0x72, 0x6f, + 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x50, + 0x72, 0x6f, 0x62, 0x6c, 0x65, 0x6d, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x52, 0x0f, 0x76, + 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x3c, + 0x0a, 0x0b, 0x61, 0x74, 0x74, 0x65, 0x6d, 0x70, 0x74, 0x65, 0x64, 0x41, 0x74, 0x18, 0x09, 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, + 0x0b, 0x61, 0x74, 0x74, 0x65, 0x6d, 0x70, 0x74, 0x65, 0x64, 0x41, 0x74, 0x4a, 0x04, 0x08, 0x03, + 0x10, 0x04, 0x4a, 0x04, 0x08, 0x07, 0x10, 0x08, 0x22, 0xb8, 0x01, 0x0a, 0x14, 0x41, 0x64, 0x64, + 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x18, 0x0a, 0x07, 0x6b, 0x65, 0x79, 0x48, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x07, 0x6b, 0x65, 0x79, 0x48, 0x61, 0x73, 0x68, 0x12, 0x30, 0x0a, 0x05, 0x61, + 0x64, 0x64, 0x65, 0x64, 0x18, 0x06, 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, 0x05, 0x61, 0x64, 0x64, 0x65, 0x64, 0x12, 0x16, 0x0a, + 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x12, + 0x1c, 0x0a, 0x09, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x42, 0x79, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x09, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x42, 0x79, 0x4a, 0x04, 0x08, + 0x02, 0x10, 0x03, 0x22, 0x24, 0x0a, 0x08, 0x53, 0x50, 0x4b, 0x49, 0x48, 0x61, 0x73, 0x68, 0x12, + 0x18, 0x0a, 0x07, 0x6b, 0x65, 0x79, 0x48, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, + 0x52, 0x07, 0x6b, 0x65, 0x79, 0x48, 0x61, 0x73, 0x68, 0x22, 0xa4, 0x01, 0x0a, 0x08, 0x49, 0x6e, + 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x12, 0x20, 0x0a, 0x0b, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, + 0x54, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x65, 0x72, + 0x69, 0x61, 0x6c, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x34, 0x0a, 0x07, 0x72, 0x65, + 0x6e, 0x65, 0x77, 0x42, 0x79, 0x18, 0x06, 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, 0x07, 0x72, 0x65, 0x6e, 0x65, 0x77, 0x42, 0x79, + 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x4a, 0x04, 0x08, 0x04, 0x10, 0x05, + 0x22, 0x37, 0x0a, 0x09, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x2a, 0x0a, + 0x09, 0x69, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x0c, 0x2e, 0x73, 0x61, 0x2e, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x52, 0x09, + 0x69, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x73, 0x22, 0x41, 0x0a, 0x19, 0x53, 0x65, 0x72, + 0x69, 0x61, 0x6c, 0x73, 0x46, 0x6f, 0x72, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x24, 0x0a, 0x0d, 0x69, 0x6e, 0x63, 0x69, 0x64, 0x65, + 0x6e, 0x74, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x69, + 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x22, 0xb4, 0x01, 0x0a, + 0x0e, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, + 0x16, 0x0a, 0x06, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x06, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, 0x26, 0x0a, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, + 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x02, 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, 0x03, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x07, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x49, 0x44, 0x12, 0x42, 0x0a, 0x0e, 0x6c, 0x61, 0x73, + 0x74, 0x4e, 0x6f, 0x74, 0x69, 0x63, 0x65, 0x53, 0x65, 0x6e, 0x74, 0x18, 0x05, 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, 0x0e, 0x6c, + 0x61, 0x73, 0x74, 0x4e, 0x6f, 0x74, 0x69, 0x63, 0x65, 0x53, 0x65, 0x6e, 0x74, 0x4a, 0x04, 0x08, + 0x04, 0x10, 0x05, 0x22, 0xe1, 0x01, 0x0a, 0x1d, 0x47, 0x65, 0x74, 0x52, 0x65, 0x76, 0x6f, 0x6b, + 0x65, 0x64, 0x43, 0x65, 0x72, 0x74, 0x73, 0x42, 0x79, 0x53, 0x68, 0x61, 0x72, 0x64, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 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, 0x40, 0x0a, 0x0d, 0x72, 0x65, 0x76, + 0x6f, 0x6b, 0x65, 0x64, 0x42, 0x65, 0x66, 0x6f, 0x72, 0x65, 0x18, 0x02, 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, 0x0d, 0x72, 0x65, + 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x42, 0x65, 0x66, 0x6f, 0x72, 0x65, 0x12, 0x3e, 0x0a, 0x0c, 0x65, + 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x66, 0x74, 0x65, 0x72, 0x18, 0x03, 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, 0x0c, 0x65, + 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x66, 0x74, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x73, + 0x68, 0x61, 0x72, 0x64, 0x49, 0x64, 0x78, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x73, + 0x68, 0x61, 0x72, 0x64, 0x49, 0x64, 0x78, 0x22, 0x98, 0x02, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x43, 0x65, 0x72, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 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, @@ -3380,100 +3423,261 @@ var file_sa_proto_rawDesc = []byte{ 0x6b, 0x65, 0x64, 0x42, 0x65, 0x66, 0x6f, 0x72, 0x65, 0x18, 0x08, 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, 0x0d, 0x72, 0x65, 0x76, - 0x6f, 0x6b, 0x65, 0x64, 0x42, 0x65, 0x66, 0x6f, 0x72, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x68, - 0x61, 0x72, 0x64, 0x49, 0x64, 0x78, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x73, 0x68, - 0x61, 0x72, 0x64, 0x49, 0x64, 0x78, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x4a, 0x04, 0x08, 0x03, - 0x10, 0x04, 0x4a, 0x04, 0x08, 0x04, 0x10, 0x05, 0x22, 0x8e, 0x01, 0x0a, 0x10, 0x52, 0x65, 0x76, - 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x16, 0x0a, - 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x73, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x24, 0x0a, 0x0d, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, - 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x72, 0x65, - 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x3c, 0x0a, 0x0b, 0x72, - 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x44, 0x61, 0x74, 0x65, 0x18, 0x03, 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, 0x0b, 0x72, 0x65, - 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x44, 0x61, 0x74, 0x65, 0x22, 0xb0, 0x01, 0x0a, 0x14, 0x4c, 0x65, - 0x61, 0x73, 0x65, 0x43, 0x52, 0x4c, 0x53, 0x68, 0x61, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 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, 0x20, 0x0a, 0x0b, 0x6d, 0x69, 0x6e, 0x53, 0x68, 0x61, - 0x72, 0x64, 0x49, 0x64, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x6d, 0x69, 0x6e, - 0x53, 0x68, 0x61, 0x72, 0x64, 0x49, 0x64, 0x78, 0x12, 0x20, 0x0a, 0x0b, 0x6d, 0x61, 0x78, 0x53, - 0x68, 0x61, 0x72, 0x64, 0x49, 0x64, 0x78, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x6d, - 0x61, 0x78, 0x53, 0x68, 0x61, 0x72, 0x64, 0x49, 0x64, 0x78, 0x12, 0x30, 0x0a, 0x05, 0x75, 0x6e, - 0x74, 0x69, 0x6c, 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, 0x05, 0x75, 0x6e, 0x74, 0x69, 0x6c, 0x22, 0x57, 0x0a, 0x15, - 0x4c, 0x65, 0x61, 0x73, 0x65, 0x43, 0x52, 0x4c, 0x53, 0x68, 0x61, 0x72, 0x64, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 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, 0x1a, 0x0a, 0x08, 0x73, 0x68, 0x61, - 0x72, 0x64, 0x49, 0x64, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x73, 0x68, 0x61, - 0x72, 0x64, 0x49, 0x64, 0x78, 0x22, 0xcf, 0x01, 0x0a, 0x15, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x43, 0x52, 0x4c, 0x53, 0x68, 0x61, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x6f, 0x6b, 0x65, 0x64, 0x42, 0x65, 0x66, 0x6f, 0x72, 0x65, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, + 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x4a, 0x04, 0x08, 0x04, 0x10, 0x05, 0x4a, 0x04, 0x08, 0x05, + 0x10, 0x06, 0x22, 0x8e, 0x01, 0x0a, 0x10, 0x52, 0x65, 0x76, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, + 0x24, 0x0a, 0x0d, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x52, + 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x3c, 0x0a, 0x0b, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, + 0x44, 0x61, 0x74, 0x65, 0x18, 0x03, 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, 0x0b, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x44, + 0x61, 0x74, 0x65, 0x22, 0xb0, 0x01, 0x0a, 0x14, 0x4c, 0x65, 0x61, 0x73, 0x65, 0x43, 0x52, 0x4c, + 0x53, 0x68, 0x61, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 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, 0x20, 0x0a, 0x0b, 0x6d, 0x69, 0x6e, 0x53, 0x68, 0x61, 0x72, 0x64, 0x49, 0x64, 0x78, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x6d, 0x69, 0x6e, 0x53, 0x68, 0x61, 0x72, 0x64, 0x49, + 0x64, 0x78, 0x12, 0x20, 0x0a, 0x0b, 0x6d, 0x61, 0x78, 0x53, 0x68, 0x61, 0x72, 0x64, 0x49, 0x64, + 0x78, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x6d, 0x61, 0x78, 0x53, 0x68, 0x61, 0x72, + 0x64, 0x49, 0x64, 0x78, 0x12, 0x30, 0x0a, 0x05, 0x75, 0x6e, 0x74, 0x69, 0x6c, 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, + 0x05, 0x75, 0x6e, 0x74, 0x69, 0x6c, 0x22, 0x57, 0x0a, 0x15, 0x4c, 0x65, 0x61, 0x73, 0x65, 0x43, + 0x52, 0x4c, 0x53, 0x68, 0x61, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 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, 0x1a, 0x0a, 0x08, 0x73, 0x68, 0x61, 0x72, 0x64, 0x49, 0x64, 0x78, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x73, 0x68, 0x61, 0x72, 0x64, 0x49, 0x64, 0x78, 0x12, - 0x3a, 0x0a, 0x0a, 0x74, 0x68, 0x69, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x18, 0x03, 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, 0x3a, 0x0a, 0x0a, 0x6e, - 0x65, 0x78, 0x74, 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, 0x6e, 0x65, 0x78, - 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x22, 0x36, 0x0a, 0x0a, 0x49, 0x64, 0x65, 0x6e, 0x74, - 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, - 0x3f, 0x0a, 0x0b, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x12, 0x30, - 0x0a, 0x0b, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x73, 0x61, 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, + 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x73, 0x68, 0x61, 0x72, 0x64, 0x49, 0x64, 0x78, 0x22, + 0xcf, 0x01, 0x0a, 0x15, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x52, 0x4c, 0x53, 0x68, 0x61, + 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 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, 0x1a, 0x0a, + 0x08, 0x73, 0x68, 0x61, 0x72, 0x64, 0x49, 0x64, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x08, 0x73, 0x68, 0x61, 0x72, 0x64, 0x49, 0x64, 0x78, 0x12, 0x3a, 0x0a, 0x0a, 0x74, 0x68, 0x69, + 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x18, 0x03, 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, 0x3a, 0x0a, 0x0a, 0x6e, 0x65, 0x78, 0x74, 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, 0x6e, 0x65, 0x78, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x22, 0x41, 0x0a, 0x0b, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, + 0x12, 0x32, 0x0a, 0x0b, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x49, 0x64, 0x65, + 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x0b, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, + 0x69, 0x65, 0x72, 0x73, 0x22, 0x6a, 0x0a, 0x0c, 0x50, 0x61, 0x75, 0x73, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x72, 0x65, + 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x12, 0x32, 0x0a, 0x0b, + 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x0b, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, - 0x22, 0x68, 0x0a, 0x0c, 0x50, 0x61, 0x75, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x26, 0x0a, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, - 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x12, 0x30, 0x0a, 0x0b, 0x69, 0x64, 0x65, 0x6e, - 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, - 0x73, 0x61, 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x0b, 0x69, - 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x22, 0x4e, 0x0a, 0x18, 0x50, 0x61, - 0x75, 0x73, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x61, 0x75, 0x73, 0x65, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x70, 0x61, 0x75, 0x73, 0x65, 0x64, 0x12, 0x1a, - 0x0a, 0x08, 0x72, 0x65, 0x70, 0x61, 0x75, 0x73, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, - 0x52, 0x08, 0x72, 0x65, 0x70, 0x61, 0x75, 0x73, 0x65, 0x64, 0x32, 0xfa, 0x10, 0x0a, 0x18, 0x53, - 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x52, - 0x65, 0x61, 0x64, 0x4f, 0x6e, 0x6c, 0x79, 0x12, 0x53, 0x0a, 0x18, 0x43, 0x6f, 0x75, 0x6e, 0x74, - 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x42, 0x79, 0x4e, 0x61, - 0x6d, 0x65, 0x73, 0x12, 0x23, 0x2e, 0x73, 0x61, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x43, 0x65, - 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x42, 0x79, 0x4e, 0x61, 0x6d, 0x65, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x73, 0x61, 0x2e, 0x43, 0x6f, - 0x75, 0x6e, 0x74, 0x42, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x22, 0x00, 0x12, 0x36, 0x0a, 0x0d, - 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x46, 0x51, 0x44, 0x4e, 0x53, 0x65, 0x74, 0x73, 0x12, 0x18, 0x2e, - 0x73, 0x61, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x46, 0x51, 0x44, 0x4e, 0x53, 0x65, 0x74, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x09, 0x2e, 0x73, 0x61, 0x2e, 0x43, 0x6f, 0x75, - 0x6e, 0x74, 0x22, 0x00, 0x12, 0x51, 0x0a, 0x1b, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x6e, 0x76, - 0x61, 0x6c, 0x69, 0x64, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x73, 0x32, 0x12, 0x25, 0x2e, 0x73, 0x61, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x6e, - 0x76, 0x61, 0x6c, 0x69, 0x64, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x09, 0x2e, 0x73, 0x61, 0x2e, - 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x00, 0x12, 0x32, 0x0a, 0x0b, 0x43, 0x6f, 0x75, 0x6e, 0x74, - 0x4f, 0x72, 0x64, 0x65, 0x72, 0x73, 0x12, 0x16, 0x2e, 0x73, 0x61, 0x2e, 0x43, 0x6f, 0x75, 0x6e, - 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x09, - 0x2e, 0x73, 0x61, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x00, 0x12, 0x3e, 0x0a, 0x1b, 0x43, - 0x6f, 0x75, 0x6e, 0x74, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x41, 0x75, 0x74, 0x68, 0x6f, - 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x32, 0x12, 0x12, 0x2e, 0x73, 0x61, 0x2e, - 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x1a, 0x09, - 0x2e, 0x73, 0x61, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x16, 0x43, - 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x73, 0x42, 0x79, 0x49, 0x50, 0x12, 0x21, 0x2e, 0x73, 0x61, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, - 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x79, 0x49, - 0x50, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x09, 0x2e, 0x73, 0x61, 0x2e, 0x43, 0x6f, - 0x75, 0x6e, 0x74, 0x22, 0x00, 0x12, 0x4d, 0x0a, 0x1b, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, - 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x79, 0x49, 0x50, 0x52, - 0x61, 0x6e, 0x67, 0x65, 0x12, 0x21, 0x2e, 0x73, 0x61, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x52, - 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x79, 0x49, 0x50, + 0x22, 0x4e, 0x0a, 0x18, 0x50, 0x61, 0x75, 0x73, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, + 0x69, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, + 0x70, 0x61, 0x75, 0x73, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x70, 0x61, + 0x75, 0x73, 0x65, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x70, 0x61, 0x75, 0x73, 0x65, 0x64, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x72, 0x65, 0x70, 0x61, 0x75, 0x73, 0x65, 0x64, + 0x22, 0x66, 0x0a, 0x20, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x63, 0x74, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x72, 0x65, + 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x12, 0x1a, 0x0a, 0x08, + 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x63, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, + 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x63, 0x74, 0x73, 0x22, 0x58, 0x0a, 0x1c, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4b, 0x65, + 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0e, 0x72, 0x65, 0x67, 0x69, + 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, + 0x12, 0x10, 0x0a, 0x03, 0x6a, 0x77, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x6a, + 0x77, 0x6b, 0x22, 0xc8, 0x01, 0x0a, 0x11, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, + 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x6c, 0x69, 0x6d, 0x69, + 0x74, 0x45, 0x6e, 0x75, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x6c, 0x69, 0x6d, + 0x69, 0x74, 0x45, 0x6e, 0x75, 0x6d, 0x12, 0x1c, 0x0a, 0x09, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, + 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x62, 0x75, 0x63, 0x6b, 0x65, + 0x74, 0x4b, 0x65, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x31, + 0x0a, 0x06, 0x70, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, + 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x70, 0x65, 0x72, 0x69, 0x6f, + 0x64, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x62, 0x75, 0x72, 0x73, 0x74, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x62, 0x75, 0x72, 0x73, 0x74, 0x22, 0x50, 0x0a, + 0x1b, 0x41, 0x64, 0x64, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, 0x76, 0x65, + 0x72, 0x72, 0x69, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x08, + 0x6f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, + 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, 0x76, 0x65, + 0x72, 0x72, 0x69, 0x64, 0x65, 0x52, 0x08, 0x6f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x22, + 0x54, 0x0a, 0x1c, 0x41, 0x64, 0x64, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, + 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x1a, 0x0a, 0x08, 0x69, 0x6e, 0x73, 0x65, 0x72, 0x74, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x08, 0x69, 0x6e, 0x73, 0x65, 0x72, 0x74, 0x65, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x65, + 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, + 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x5c, 0x0a, 0x1e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x52, + 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6c, 0x69, 0x6d, 0x69, 0x74, + 0x45, 0x6e, 0x75, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x6c, 0x69, 0x6d, 0x69, + 0x74, 0x45, 0x6e, 0x75, 0x6d, 0x12, 0x1c, 0x0a, 0x09, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x4b, + 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, + 0x4b, 0x65, 0x79, 0x22, 0x5d, 0x0a, 0x1f, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x61, + 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x45, + 0x6e, 0x75, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x6c, 0x69, 0x6d, 0x69, 0x74, + 0x45, 0x6e, 0x75, 0x6d, 0x12, 0x1c, 0x0a, 0x09, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x4b, 0x65, + 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x4b, + 0x65, 0x79, 0x22, 0x59, 0x0a, 0x1b, 0x47, 0x65, 0x74, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, + 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x45, 0x6e, 0x75, 0x6d, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x45, 0x6e, 0x75, 0x6d, 0x12, + 0x1c, 0x0a, 0x09, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x09, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x22, 0xa2, 0x01, + 0x0a, 0x19, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, 0x72, + 0x69, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x31, 0x0a, 0x08, 0x6f, + 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, + 0x73, 0x61, 0x2e, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, + 0x72, 0x69, 0x64, 0x65, 0x52, 0x08, 0x6f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x12, 0x18, + 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x38, 0x0a, 0x09, 0x75, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x64, 0x41, 0x74, 0x18, 0x03, 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, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, + 0x41, 0x74, 0x32, 0xc7, 0x0f, 0x0a, 0x18, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x41, 0x75, + 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x52, 0x65, 0x61, 0x64, 0x4f, 0x6e, 0x6c, 0x79, 0x12, + 0x51, 0x0a, 0x1b, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x41, + 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x32, 0x12, 0x25, + 0x2e, 0x73, 0x61, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, + 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x09, 0x2e, 0x73, 0x61, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, + 0x22, 0x00, 0x12, 0x3e, 0x0a, 0x1b, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x50, 0x65, 0x6e, 0x64, 0x69, + 0x6e, 0x67, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x32, 0x12, 0x12, 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x1a, 0x09, 0x2e, 0x73, 0x61, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, + 0x22, 0x00, 0x12, 0x37, 0x0a, 0x0d, 0x46, 0x51, 0x44, 0x4e, 0x53, 0x65, 0x74, 0x45, 0x78, 0x69, + 0x73, 0x74, 0x73, 0x12, 0x18, 0x2e, 0x73, 0x61, 0x2e, 0x46, 0x51, 0x44, 0x4e, 0x53, 0x65, 0x74, + 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0a, 0x2e, + 0x73, 0x61, 0x2e, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x1a, 0x46, + 0x51, 0x44, 0x4e, 0x53, 0x65, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x73, + 0x46, 0x6f, 0x72, 0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x12, 0x18, 0x2e, 0x73, 0x61, 0x2e, 0x43, + 0x6f, 0x75, 0x6e, 0x74, 0x46, 0x51, 0x44, 0x4e, 0x53, 0x65, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x0e, 0x2e, 0x73, 0x61, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, + 0x6d, 0x70, 0x73, 0x22, 0x00, 0x12, 0x40, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, + 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x32, 0x12, 0x14, 0x2e, 0x73, 0x61, 0x2e, + 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x32, + 0x1a, 0x13, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x41, 0x75, + 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x32, 0x12, 0x1c, 0x2e, + 0x73, 0x61, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x73, 0x61, + 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, + 0x00, 0x12, 0x31, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, + 0x61, 0x74, 0x65, 0x12, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x1a, + 0x11, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, + 0x74, 0x65, 0x22, 0x00, 0x12, 0x38, 0x0a, 0x15, 0x47, 0x65, 0x74, 0x4c, 0x69, 0x6e, 0x74, 0x50, + 0x72, 0x65, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x0a, 0x2e, + 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x1a, 0x11, 0x2e, 0x63, 0x6f, 0x72, 0x65, + 0x2e, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x22, 0x00, 0x12, 0x3d, + 0x0a, 0x14, 0x47, 0x65, 0x74, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, + 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, 0x69, + 0x61, 0x6c, 0x1a, 0x17, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, + 0x69, 0x63, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x00, 0x12, 0x48, 0x0a, + 0x10, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x78, 0x45, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 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, 0x22, 0x00, 0x12, 0x2b, 0x0a, 0x08, 0x47, 0x65, 0x74, 0x4f, 0x72, + 0x64, 0x65, 0x72, 0x12, 0x10, 0x2e, 0x73, 0x61, 0x2e, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0b, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x4f, 0x72, 0x64, + 0x65, 0x72, 0x22, 0x00, 0x12, 0x3e, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, + 0x46, 0x6f, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x12, 0x1b, 0x2e, 0x73, 0x61, 0x2e, 0x47, 0x65, + 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x46, 0x6f, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0b, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x4f, 0x72, 0x64, + 0x65, 0x72, 0x22, 0x00, 0x12, 0x3b, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, + 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x65, 0x67, + 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x1a, 0x12, 0x2e, 0x63, 0x6f, + 0x72, 0x65, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, + 0x00, 0x12, 0x3c, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x79, 0x4b, 0x65, 0x79, 0x12, 0x0e, 0x2e, 0x73, 0x61, 0x2e, 0x4a, + 0x53, 0x4f, 0x4e, 0x57, 0x65, 0x62, 0x4b, 0x65, 0x79, 0x1a, 0x12, 0x2e, 0x63, 0x6f, 0x72, 0x65, + 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x00, 0x12, + 0x39, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x52, 0x65, 0x76, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, 0x69, + 0x61, 0x6c, 0x1a, 0x14, 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x65, 0x76, 0x6f, 0x63, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x00, 0x12, 0x41, 0x0a, 0x0f, 0x47, 0x65, + 0x74, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x43, 0x65, 0x72, 0x74, 0x73, 0x12, 0x1a, 0x2e, + 0x73, 0x61, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x43, 0x65, 0x72, + 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0e, 0x2e, 0x63, 0x6f, 0x72, 0x65, + 0x2e, 0x43, 0x52, 0x4c, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x22, 0x00, 0x30, 0x01, 0x12, 0x4f, 0x0a, + 0x16, 0x47, 0x65, 0x74, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x43, 0x65, 0x72, 0x74, 0x73, + 0x42, 0x79, 0x53, 0x68, 0x61, 0x72, 0x64, 0x12, 0x21, 0x2e, 0x73, 0x61, 0x2e, 0x47, 0x65, 0x74, + 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x43, 0x65, 0x72, 0x74, 0x73, 0x42, 0x79, 0x53, 0x68, + 0x61, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0e, 0x2e, 0x63, 0x6f, 0x72, + 0x65, 0x2e, 0x43, 0x52, 0x4c, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x22, 0x00, 0x30, 0x01, 0x12, 0x35, + 0x0a, 0x11, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x4d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0x12, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x1a, + 0x12, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x4d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0x22, 0x00, 0x12, 0x39, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x69, + 0x61, 0x6c, 0x73, 0x42, 0x79, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x12, 0x2e, 0x73, + 0x61, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, + 0x1a, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x22, 0x00, 0x30, 0x01, + 0x12, 0x2f, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x73, 0x42, 0x79, + 0x4b, 0x65, 0x79, 0x12, 0x0c, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x50, 0x4b, 0x49, 0x48, 0x61, 0x73, + 0x68, 0x1a, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x22, 0x00, 0x30, + 0x01, 0x12, 0x52, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x41, 0x75, 0x74, + 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x32, 0x12, 0x21, 0x2e, 0x73, + 0x61, 0x2e, 0x47, 0x65, 0x74, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, + 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x12, 0x2e, 0x73, 0x61, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x22, 0x00, 0x12, 0x5c, 0x0a, 0x1c, 0x47, 0x65, 0x74, 0x56, 0x61, 0x6c, 0x69, + 0x64, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x32, 0x12, 0x26, 0x2e, 0x73, 0x61, 0x2e, 0x47, 0x65, 0x74, 0x56, 0x61, + 0x6c, 0x69, 0x64, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, + 0x73, 0x61, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x22, 0x00, 0x12, 0x31, 0x0a, 0x12, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x73, + 0x46, 0x6f, 0x72, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x53, + 0x65, 0x72, 0x69, 0x61, 0x6c, 0x1a, 0x0d, 0x2e, 0x73, 0x61, 0x2e, 0x49, 0x6e, 0x63, 0x69, 0x64, + 0x65, 0x6e, 0x74, 0x73, 0x22, 0x00, 0x12, 0x28, 0x0a, 0x0a, 0x4b, 0x65, 0x79, 0x42, 0x6c, 0x6f, + 0x63, 0x6b, 0x65, 0x64, 0x12, 0x0c, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x50, 0x4b, 0x49, 0x48, 0x61, + 0x73, 0x68, 0x1a, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x22, 0x00, + 0x12, 0x32, 0x0a, 0x16, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x4f, + 0x72, 0x64, 0x65, 0x72, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x12, 0x0a, 0x2e, 0x73, 0x61, 0x2e, + 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x1a, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x45, 0x78, 0x69, 0x73, + 0x74, 0x73, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x12, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x73, 0x46, + 0x6f, 0x72, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x12, 0x1d, 0x2e, 0x73, 0x61, 0x2e, + 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x73, 0x46, 0x6f, 0x72, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, + 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x73, 0x61, 0x2e, 0x49, + 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x22, 0x00, 0x30, + 0x01, 0x12, 0x3d, 0x0a, 0x16, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, + 0x66, 0x69, 0x65, 0x72, 0x73, 0x50, 0x61, 0x75, 0x73, 0x65, 0x64, 0x12, 0x10, 0x2e, 0x73, 0x61, + 0x2e, 0x50, 0x61, 0x75, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0f, 0x2e, + 0x73, 0x61, 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x22, 0x00, + 0x12, 0x3d, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x50, 0x61, 0x75, 0x73, 0x65, 0x64, 0x49, 0x64, 0x65, + 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x12, 0x12, 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x65, + 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x1a, 0x0f, 0x2e, 0x73, + 0x61, 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x22, 0x00, 0x12, + 0x58, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, + 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x12, 0x1f, 0x2e, 0x73, 0x61, 0x2e, 0x47, 0x65, 0x74, + 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, + 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x61, + 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x51, 0x0a, 0x1c, 0x47, 0x65, 0x74, + 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, + 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, + 0x79, 0x1a, 0x15, 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, + 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x22, 0x00, 0x30, 0x01, 0x32, 0xaa, 0x1d, 0x0a, + 0x10, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, + 0x79, 0x12, 0x51, 0x0a, 0x1b, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, + 0x64, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x32, + 0x12, 0x25, 0x2e, 0x73, 0x61, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x6e, 0x76, 0x61, 0x6c, + 0x69, 0x64, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x09, 0x2e, 0x73, 0x61, 0x2e, 0x43, 0x6f, 0x75, + 0x6e, 0x74, 0x22, 0x00, 0x12, 0x3e, 0x0a, 0x1b, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x50, 0x65, 0x6e, + 0x64, 0x69, 0x6e, 0x67, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x32, 0x12, 0x12, 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x1a, 0x09, 0x2e, 0x73, 0x61, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x00, 0x12, 0x37, 0x0a, 0x0d, 0x46, 0x51, 0x44, 0x4e, 0x53, 0x65, 0x74, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x12, 0x18, 0x2e, 0x73, 0x61, 0x2e, 0x46, 0x51, 0x44, 0x4e, 0x53, 0x65, 0x74, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, @@ -3513,601 +3717,502 @@ var file_sa_proto_rawDesc = []byte{ 0x65, 0x72, 0x46, 0x6f, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x12, 0x1b, 0x2e, 0x73, 0x61, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x46, 0x6f, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0b, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x4f, - 0x72, 0x64, 0x65, 0x72, 0x22, 0x00, 0x12, 0x55, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x50, 0x65, 0x6e, - 0x64, 0x69, 0x6e, 0x67, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x32, 0x12, 0x22, 0x2e, 0x73, 0x61, 0x2e, 0x47, 0x65, 0x74, 0x50, 0x65, 0x6e, 0x64, 0x69, - 0x6e, 0x67, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x41, 0x75, - 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x00, 0x12, 0x3b, 0x0a, - 0x0f, 0x47, 0x65, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x12, 0x12, 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x49, 0x44, 0x1a, 0x12, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x52, 0x65, 0x67, 0x69, - 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x00, 0x12, 0x3c, 0x0a, 0x14, 0x47, 0x65, - 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x79, 0x4b, - 0x65, 0x79, 0x12, 0x0e, 0x2e, 0x73, 0x61, 0x2e, 0x4a, 0x53, 0x4f, 0x4e, 0x57, 0x65, 0x62, 0x4b, - 0x65, 0x79, 0x1a, 0x12, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, - 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x00, 0x12, 0x39, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x52, - 0x65, 0x76, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, - 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x1a, 0x14, 0x2e, 0x73, 0x61, - 0x2e, 0x52, 0x65, 0x76, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x22, 0x00, 0x12, 0x41, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, - 0x64, 0x43, 0x65, 0x72, 0x74, 0x73, 0x12, 0x1a, 0x2e, 0x73, 0x61, 0x2e, 0x47, 0x65, 0x74, 0x52, - 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x43, 0x65, 0x72, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x0e, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x43, 0x52, 0x4c, 0x45, 0x6e, 0x74, - 0x72, 0x79, 0x22, 0x00, 0x30, 0x01, 0x12, 0x35, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, - 0x69, 0x61, 0x6c, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x0a, 0x2e, 0x73, 0x61, - 0x2e, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x1a, 0x12, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, - 0x69, 0x61, 0x6c, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x00, 0x12, 0x39, 0x0a, - 0x13, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x73, 0x42, 0x79, 0x41, 0x63, 0x63, - 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x12, 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, - 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x1a, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, - 0x72, 0x69, 0x61, 0x6c, 0x22, 0x00, 0x30, 0x01, 0x12, 0x2f, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x53, - 0x65, 0x72, 0x69, 0x61, 0x6c, 0x73, 0x42, 0x79, 0x4b, 0x65, 0x79, 0x12, 0x0c, 0x2e, 0x73, 0x61, - 0x2e, 0x53, 0x50, 0x4b, 0x49, 0x48, 0x61, 0x73, 0x68, 0x1a, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x53, - 0x65, 0x72, 0x69, 0x61, 0x6c, 0x22, 0x00, 0x30, 0x01, 0x12, 0x52, 0x0a, 0x17, 0x47, 0x65, 0x74, - 0x56, 0x61, 0x6c, 0x69, 0x64, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x73, 0x32, 0x12, 0x21, 0x2e, 0x73, 0x61, 0x2e, 0x47, 0x65, 0x74, 0x56, 0x61, 0x6c, - 0x69, 0x64, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x73, 0x61, 0x2e, 0x41, 0x75, 0x74, - 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x00, 0x12, 0x5c, 0x0a, - 0x1c, 0x47, 0x65, 0x74, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x41, 0x75, - 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x32, 0x12, 0x26, 0x2e, - 0x73, 0x61, 0x2e, 0x47, 0x65, 0x74, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x4f, 0x72, 0x64, 0x65, 0x72, - 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x73, 0x61, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, - 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x00, 0x12, 0x31, 0x0a, 0x12, 0x49, - 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x73, 0x46, 0x6f, 0x72, 0x53, 0x65, 0x72, 0x69, 0x61, - 0x6c, 0x12, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x1a, 0x0d, 0x2e, - 0x73, 0x61, 0x2e, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x73, 0x22, 0x00, 0x12, 0x28, - 0x0a, 0x0a, 0x4b, 0x65, 0x79, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x12, 0x0c, 0x2e, 0x73, - 0x61, 0x2e, 0x53, 0x50, 0x4b, 0x49, 0x48, 0x61, 0x73, 0x68, 0x1a, 0x0a, 0x2e, 0x73, 0x61, 0x2e, - 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x22, 0x00, 0x12, 0x32, 0x0a, 0x16, 0x52, 0x65, 0x70, 0x6c, - 0x61, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x45, 0x78, 0x69, 0x73, - 0x74, 0x73, 0x12, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x1a, 0x0a, - 0x2e, 0x73, 0x61, 0x2e, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x12, - 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x73, 0x46, 0x6f, 0x72, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, - 0x6e, 0x74, 0x12, 0x1d, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x73, 0x46, - 0x6f, 0x72, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x12, 0x2e, 0x73, 0x61, 0x2e, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x53, - 0x65, 0x72, 0x69, 0x61, 0x6c, 0x22, 0x00, 0x30, 0x01, 0x12, 0x3d, 0x0a, 0x16, 0x43, 0x68, 0x65, - 0x63, 0x6b, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x50, 0x61, 0x75, - 0x73, 0x65, 0x64, 0x12, 0x10, 0x2e, 0x73, 0x61, 0x2e, 0x50, 0x61, 0x75, 0x73, 0x65, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0f, 0x2e, 0x73, 0x61, 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, - 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x22, 0x00, 0x12, 0x3d, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x50, - 0x61, 0x75, 0x73, 0x65, 0x64, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, - 0x12, 0x12, 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x49, 0x44, 0x1a, 0x0f, 0x2e, 0x73, 0x61, 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, - 0x66, 0x69, 0x65, 0x72, 0x73, 0x22, 0x00, 0x32, 0xf7, 0x1b, 0x0a, 0x10, 0x53, 0x74, 0x6f, 0x72, - 0x61, 0x67, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x12, 0x53, 0x0a, 0x18, - 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, - 0x73, 0x42, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x12, 0x23, 0x2e, 0x73, 0x61, 0x2e, 0x43, 0x6f, - 0x75, 0x6e, 0x74, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x42, - 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, - 0x73, 0x61, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x42, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x22, - 0x00, 0x12, 0x36, 0x0a, 0x0d, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x46, 0x51, 0x44, 0x4e, 0x53, 0x65, - 0x74, 0x73, 0x12, 0x18, 0x2e, 0x73, 0x61, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x46, 0x51, 0x44, - 0x4e, 0x53, 0x65, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x09, 0x2e, 0x73, - 0x61, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x00, 0x12, 0x51, 0x0a, 0x1b, 0x43, 0x6f, 0x75, - 0x6e, 0x74, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, - 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x32, 0x12, 0x25, 0x2e, 0x73, 0x61, 0x2e, 0x43, 0x6f, - 0x75, 0x6e, 0x74, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, + 0x72, 0x64, 0x65, 0x72, 0x22, 0x00, 0x12, 0x3b, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x52, 0x65, 0x67, + 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x2e, 0x73, 0x61, 0x2e, 0x52, + 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x1a, 0x12, 0x2e, + 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x22, 0x00, 0x12, 0x3c, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x79, 0x4b, 0x65, 0x79, 0x12, 0x0e, 0x2e, 0x73, 0x61, + 0x2e, 0x4a, 0x53, 0x4f, 0x4e, 0x57, 0x65, 0x62, 0x4b, 0x65, 0x79, 0x1a, 0x12, 0x2e, 0x63, 0x6f, + 0x72, 0x65, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, + 0x00, 0x12, 0x39, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x52, 0x65, 0x76, 0x6f, 0x63, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, + 0x72, 0x69, 0x61, 0x6c, 0x1a, 0x14, 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x65, 0x76, 0x6f, 0x63, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x00, 0x12, 0x41, 0x0a, 0x0f, + 0x47, 0x65, 0x74, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x43, 0x65, 0x72, 0x74, 0x73, 0x12, + 0x1a, 0x2e, 0x73, 0x61, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x43, + 0x65, 0x72, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0e, 0x2e, 0x63, 0x6f, + 0x72, 0x65, 0x2e, 0x43, 0x52, 0x4c, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x22, 0x00, 0x30, 0x01, 0x12, + 0x4f, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x43, 0x65, 0x72, + 0x74, 0x73, 0x42, 0x79, 0x53, 0x68, 0x61, 0x72, 0x64, 0x12, 0x21, 0x2e, 0x73, 0x61, 0x2e, 0x47, + 0x65, 0x74, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x43, 0x65, 0x72, 0x74, 0x73, 0x42, 0x79, + 0x53, 0x68, 0x61, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0e, 0x2e, 0x63, + 0x6f, 0x72, 0x65, 0x2e, 0x43, 0x52, 0x4c, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x22, 0x00, 0x30, 0x01, + 0x12, 0x35, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x4d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, 0x69, 0x61, + 0x6c, 0x1a, 0x12, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x4d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x00, 0x12, 0x39, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x53, 0x65, + 0x72, 0x69, 0x61, 0x6c, 0x73, 0x42, 0x79, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x12, + 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x49, 0x44, 0x1a, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x22, 0x00, + 0x30, 0x01, 0x12, 0x2f, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x73, + 0x42, 0x79, 0x4b, 0x65, 0x79, 0x12, 0x0c, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x50, 0x4b, 0x49, 0x48, + 0x61, 0x73, 0x68, 0x1a, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x22, + 0x00, 0x30, 0x01, 0x12, 0x52, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x41, + 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x32, 0x12, 0x21, + 0x2e, 0x73, 0x61, 0x2e, 0x47, 0x65, 0x74, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x41, 0x75, 0x74, 0x68, + 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x12, 0x2e, 0x73, 0x61, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x00, 0x12, 0x5c, 0x0a, 0x1c, 0x47, 0x65, 0x74, 0x56, 0x61, + 0x6c, 0x69, 0x64, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x32, 0x12, 0x26, 0x2e, 0x73, 0x61, 0x2e, 0x47, 0x65, 0x74, + 0x56, 0x61, 0x6c, 0x69, 0x64, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x09, 0x2e, 0x73, 0x61, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x00, 0x12, 0x32, 0x0a, 0x0b, - 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x73, 0x12, 0x16, 0x2e, 0x73, 0x61, - 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x09, 0x2e, 0x73, 0x61, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x00, - 0x12, 0x3e, 0x0a, 0x1b, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, - 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x32, 0x12, - 0x12, 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x49, 0x44, 0x1a, 0x09, 0x2e, 0x73, 0x61, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x00, - 0x12, 0x48, 0x0a, 0x16, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x79, 0x49, 0x50, 0x12, 0x21, 0x2e, 0x73, 0x61, 0x2e, - 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x73, 0x42, 0x79, 0x49, 0x50, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x09, 0x2e, - 0x73, 0x61, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x00, 0x12, 0x4d, 0x0a, 0x1b, 0x43, 0x6f, - 0x75, 0x6e, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, - 0x42, 0x79, 0x49, 0x50, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x21, 0x2e, 0x73, 0x61, 0x2e, 0x43, - 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x73, 0x42, 0x79, 0x49, 0x50, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x09, 0x2e, 0x73, - 0x61, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x00, 0x12, 0x37, 0x0a, 0x0d, 0x46, 0x51, 0x44, - 0x4e, 0x53, 0x65, 0x74, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x12, 0x18, 0x2e, 0x73, 0x61, 0x2e, - 0x46, 0x51, 0x44, 0x4e, 0x53, 0x65, 0x74, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, - 0x22, 0x00, 0x12, 0x48, 0x0a, 0x1a, 0x46, 0x51, 0x44, 0x4e, 0x53, 0x65, 0x74, 0x54, 0x69, 0x6d, - 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x73, 0x46, 0x6f, 0x72, 0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77, - 0x12, 0x18, 0x2e, 0x73, 0x61, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x46, 0x51, 0x44, 0x4e, 0x53, - 0x65, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0e, 0x2e, 0x73, 0x61, 0x2e, - 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x73, 0x22, 0x00, 0x12, 0x40, 0x0a, 0x11, - 0x47, 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x32, 0x12, 0x14, 0x2e, 0x73, 0x61, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x32, 0x1a, 0x13, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x41, - 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x00, 0x12, 0x48, - 0x0a, 0x12, 0x47, 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x73, 0x32, 0x12, 0x1c, 0x2e, 0x73, 0x61, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x75, 0x74, - 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x73, 0x61, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x00, 0x12, 0x31, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x43, - 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x0a, 0x2e, 0x73, 0x61, 0x2e, - 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x1a, 0x11, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x43, 0x65, - 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x22, 0x00, 0x12, 0x38, 0x0a, 0x15, 0x47, - 0x65, 0x74, 0x4c, 0x69, 0x6e, 0x74, 0x50, 0x72, 0x65, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, - 0x63, 0x61, 0x74, 0x65, 0x12, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, - 0x1a, 0x11, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, - 0x61, 0x74, 0x65, 0x22, 0x00, 0x12, 0x3d, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x43, 0x65, 0x72, 0x74, - 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0a, 0x2e, - 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x1a, 0x17, 0x2e, 0x63, 0x6f, 0x72, 0x65, - 0x2e, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, - 0x75, 0x73, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x78, 0x45, 0x78, - 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, - 0x1a, 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, 0x22, 0x00, 0x12, 0x2b, - 0x0a, 0x08, 0x47, 0x65, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x10, 0x2e, 0x73, 0x61, 0x2e, - 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0b, 0x2e, 0x63, - 0x6f, 0x72, 0x65, 0x2e, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x22, 0x00, 0x12, 0x3e, 0x0a, 0x10, 0x47, - 0x65, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x46, 0x6f, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x12, - 0x1b, 0x2e, 0x73, 0x61, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x46, 0x6f, 0x72, - 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0b, 0x2e, 0x63, - 0x6f, 0x72, 0x65, 0x2e, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x22, 0x00, 0x12, 0x55, 0x0a, 0x18, 0x47, - 0x65, 0x74, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, - 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x32, 0x12, 0x22, 0x2e, 0x73, 0x61, 0x2e, 0x47, 0x65, 0x74, - 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x63, 0x6f, - 0x72, 0x65, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x22, 0x00, 0x12, 0x3b, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, - 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x1a, 0x12, 0x2e, 0x63, 0x6f, 0x72, 0x65, - 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x00, 0x12, - 0x3c, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x42, 0x79, 0x4b, 0x65, 0x79, 0x12, 0x0e, 0x2e, 0x73, 0x61, 0x2e, 0x4a, 0x53, 0x4f, - 0x4e, 0x57, 0x65, 0x62, 0x4b, 0x65, 0x79, 0x1a, 0x12, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x52, - 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x00, 0x12, 0x39, 0x0a, - 0x13, 0x47, 0x65, 0x74, 0x52, 0x65, 0x76, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x12, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, - 0x1a, 0x14, 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x65, 0x76, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x00, 0x12, 0x41, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x52, - 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x43, 0x65, 0x72, 0x74, 0x73, 0x12, 0x1a, 0x2e, 0x73, 0x61, - 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x43, 0x65, 0x72, 0x74, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0e, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x43, - 0x52, 0x4c, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x22, 0x00, 0x30, 0x01, 0x12, 0x35, 0x0a, 0x11, 0x47, - 0x65, 0x74, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, - 0x12, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x1a, 0x12, 0x2e, 0x73, - 0x61, 0x2e, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, - 0x22, 0x00, 0x12, 0x39, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x73, - 0x42, 0x79, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x12, 0x2e, 0x73, 0x61, 0x2e, 0x52, - 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x1a, 0x0a, 0x2e, - 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x22, 0x00, 0x30, 0x01, 0x12, 0x2f, 0x0a, - 0x0f, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x73, 0x42, 0x79, 0x4b, 0x65, 0x79, - 0x12, 0x0c, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x50, 0x4b, 0x49, 0x48, 0x61, 0x73, 0x68, 0x1a, 0x0a, - 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x22, 0x00, 0x30, 0x01, 0x12, 0x52, - 0x0a, 0x17, 0x47, 0x65, 0x74, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, - 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x32, 0x12, 0x21, 0x2e, 0x73, 0x61, 0x2e, 0x47, - 0x65, 0x74, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x73, - 0x61, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, - 0x22, 0x00, 0x12, 0x5c, 0x0a, 0x1c, 0x47, 0x65, 0x74, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x4f, 0x72, - 0x64, 0x65, 0x72, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x73, 0x32, 0x12, 0x26, 0x2e, 0x73, 0x61, 0x2e, 0x47, 0x65, 0x74, 0x56, 0x61, 0x6c, 0x69, 0x64, - 0x4f, 0x72, 0x64, 0x65, 0x72, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x73, 0x61, 0x2e, - 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x00, - 0x12, 0x31, 0x0a, 0x12, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x73, 0x46, 0x6f, 0x72, - 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, 0x69, - 0x61, 0x6c, 0x1a, 0x0d, 0x2e, 0x73, 0x61, 0x2e, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, - 0x73, 0x22, 0x00, 0x12, 0x28, 0x0a, 0x0a, 0x4b, 0x65, 0x79, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x65, - 0x64, 0x12, 0x0c, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x50, 0x4b, 0x49, 0x48, 0x61, 0x73, 0x68, 0x1a, - 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x22, 0x00, 0x12, 0x32, 0x0a, - 0x16, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x4f, 0x72, 0x64, 0x65, - 0x72, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x12, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, - 0x69, 0x61, 0x6c, 0x1a, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x22, - 0x00, 0x12, 0x4b, 0x0a, 0x12, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x73, 0x46, 0x6f, 0x72, 0x49, - 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x12, 0x1d, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, - 0x69, 0x61, 0x6c, 0x73, 0x46, 0x6f, 0x72, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x73, 0x61, 0x2e, 0x49, 0x6e, 0x63, 0x69, - 0x64, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x22, 0x00, 0x30, 0x01, 0x12, 0x3d, - 0x0a, 0x16, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, - 0x72, 0x73, 0x50, 0x61, 0x75, 0x73, 0x65, 0x64, 0x12, 0x10, 0x2e, 0x73, 0x61, 0x2e, 0x50, 0x61, - 0x75, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0f, 0x2e, 0x73, 0x61, 0x2e, - 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x22, 0x00, 0x12, 0x3d, 0x0a, - 0x14, 0x47, 0x65, 0x74, 0x50, 0x61, 0x75, 0x73, 0x65, 0x64, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, - 0x66, 0x69, 0x65, 0x72, 0x73, 0x12, 0x12, 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, - 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x1a, 0x0f, 0x2e, 0x73, 0x61, 0x2e, 0x49, - 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x22, 0x00, 0x12, 0x43, 0x0a, 0x0d, - 0x41, 0x64, 0x64, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x18, 0x2e, - 0x73, 0x61, 0x2e, 0x41, 0x64, 0x64, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x4b, 0x65, 0x79, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, - 0x00, 0x12, 0x45, 0x0a, 0x0e, 0x41, 0x64, 0x64, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, - 0x61, 0x74, 0x65, 0x12, 0x19, 0x2e, 0x73, 0x61, 0x2e, 0x41, 0x64, 0x64, 0x43, 0x65, 0x72, 0x74, - 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, - 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x11, 0x41, 0x64, 0x64, 0x50, - 0x72, 0x65, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x19, 0x2e, - 0x73, 0x61, 0x2e, 0x41, 0x64, 0x64, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, - 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, - 0x22, 0x00, 0x12, 0x41, 0x0a, 0x19, 0x53, 0x65, 0x74, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, - 0x63, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x61, 0x64, 0x79, 0x12, - 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x1a, 0x16, 0x2e, 0x67, 0x6f, + 0x12, 0x2e, 0x73, 0x61, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x22, 0x00, 0x12, 0x31, 0x0a, 0x12, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, + 0x74, 0x73, 0x46, 0x6f, 0x72, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, 0x0a, 0x2e, 0x73, 0x61, + 0x2e, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x1a, 0x0d, 0x2e, 0x73, 0x61, 0x2e, 0x49, 0x6e, 0x63, + 0x69, 0x64, 0x65, 0x6e, 0x74, 0x73, 0x22, 0x00, 0x12, 0x28, 0x0a, 0x0a, 0x4b, 0x65, 0x79, 0x42, + 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x12, 0x0c, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x50, 0x4b, 0x49, + 0x48, 0x61, 0x73, 0x68, 0x1a, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, + 0x22, 0x00, 0x12, 0x32, 0x0a, 0x16, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x6d, 0x65, 0x6e, + 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x45, 0x78, 0x69, 0x73, 0x74, 0x73, 0x12, 0x0a, 0x2e, 0x73, + 0x61, 0x2e, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x1a, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x45, 0x78, + 0x69, 0x73, 0x74, 0x73, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x12, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, + 0x73, 0x46, 0x6f, 0x72, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x12, 0x1d, 0x2e, 0x73, + 0x61, 0x2e, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x73, 0x46, 0x6f, 0x72, 0x49, 0x6e, 0x63, 0x69, + 0x64, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x73, 0x61, + 0x2e, 0x49, 0x6e, 0x63, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x22, + 0x00, 0x30, 0x01, 0x12, 0x3d, 0x0a, 0x16, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x49, 0x64, 0x65, 0x6e, + 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x50, 0x61, 0x75, 0x73, 0x65, 0x64, 0x12, 0x10, 0x2e, + 0x73, 0x61, 0x2e, 0x50, 0x61, 0x75, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x0f, 0x2e, 0x73, 0x61, 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, + 0x22, 0x00, 0x12, 0x3d, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x50, 0x61, 0x75, 0x73, 0x65, 0x64, 0x49, + 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x12, 0x12, 0x2e, 0x73, 0x61, 0x2e, + 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x1a, 0x0f, + 0x2e, 0x73, 0x61, 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x22, + 0x00, 0x12, 0x58, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, + 0x74, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x12, 0x1f, 0x2e, 0x73, 0x61, 0x2e, 0x47, + 0x65, 0x74, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, 0x72, + 0x69, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x73, 0x61, 0x2e, + 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, + 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x51, 0x0a, 0x1c, 0x47, + 0x65, 0x74, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, + 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, - 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x3b, 0x0a, 0x09, 0x41, 0x64, 0x64, 0x53, 0x65, 0x72, 0x69, - 0x61, 0x6c, 0x12, 0x14, 0x2e, 0x73, 0x61, 0x2e, 0x41, 0x64, 0x64, 0x53, 0x65, 0x72, 0x69, 0x61, - 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, - 0x22, 0x00, 0x12, 0x4a, 0x0a, 0x18, 0x44, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, - 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x32, 0x12, 0x14, - 0x2e, 0x73, 0x61, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x49, 0x44, 0x32, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x46, - 0x0a, 0x16, 0x44, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, - 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x65, - 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x1a, 0x16, 0x2e, 0x67, - 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, - 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x54, 0x0a, 0x16, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, - 0x7a, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x32, - 0x12, 0x20, 0x2e, 0x73, 0x61, 0x2e, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x41, 0x75, - 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x43, 0x0a, 0x0d, - 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x18, 0x2e, - 0x73, 0x61, 0x2e, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x4f, 0x72, 0x64, 0x65, 0x72, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, - 0x00, 0x12, 0x40, 0x0a, 0x11, 0x4e, 0x65, 0x77, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x41, 0x6e, 0x64, - 0x41, 0x75, 0x74, 0x68, 0x7a, 0x73, 0x12, 0x1c, 0x2e, 0x73, 0x61, 0x2e, 0x4e, 0x65, 0x77, 0x4f, - 0x72, 0x64, 0x65, 0x72, 0x41, 0x6e, 0x64, 0x41, 0x75, 0x74, 0x68, 0x7a, 0x73, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0b, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x4f, 0x72, 0x64, 0x65, - 0x72, 0x22, 0x00, 0x12, 0x3b, 0x0a, 0x0f, 0x4e, 0x65, 0x77, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, - 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x52, 0x65, - 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x1a, 0x12, 0x2e, 0x63, 0x6f, 0x72, - 0x65, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x00, - 0x12, 0x4b, 0x0a, 0x11, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, - 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x1c, 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x65, 0x76, 0x6f, 0x6b, - 0x65, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x43, 0x0a, - 0x0d, 0x53, 0x65, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x18, - 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x45, 0x72, 0x72, 0x6f, + 0x70, 0x74, 0x79, 0x1a, 0x15, 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, + 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x43, + 0x0a, 0x0d, 0x41, 0x64, 0x64, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, + 0x18, 0x2e, 0x73, 0x61, 0x2e, 0x41, 0x64, 0x64, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x4b, + 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, + 0x79, 0x22, 0x00, 0x12, 0x45, 0x0a, 0x0e, 0x41, 0x64, 0x64, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, + 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x19, 0x2e, 0x73, 0x61, 0x2e, 0x41, 0x64, 0x64, 0x43, 0x65, + 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x11, 0x41, 0x64, + 0x64, 0x50, 0x72, 0x65, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, + 0x19, 0x2e, 0x73, 0x61, 0x2e, 0x41, 0x64, 0x64, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, + 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, + 0x74, 0x79, 0x22, 0x00, 0x12, 0x41, 0x0a, 0x19, 0x53, 0x65, 0x74, 0x43, 0x65, 0x72, 0x74, 0x69, + 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x61, 0x64, + 0x79, 0x12, 0x0a, 0x2e, 0x73, 0x61, 0x2e, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x1a, 0x16, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x3b, 0x0a, 0x09, 0x41, 0x64, 0x64, 0x53, 0x65, + 0x72, 0x69, 0x61, 0x6c, 0x12, 0x14, 0x2e, 0x73, 0x61, 0x2e, 0x41, 0x64, 0x64, 0x53, 0x65, 0x72, + 0x69, 0x61, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, + 0x74, 0x79, 0x22, 0x00, 0x12, 0x4a, 0x0a, 0x18, 0x44, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, + 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x32, + 0x12, 0x14, 0x2e, 0x73, 0x61, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x32, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, + 0x12, 0x42, 0x0a, 0x16, 0x44, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x52, 0x65, + 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x2e, 0x73, 0x61, 0x2e, + 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x1a, 0x12, + 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x22, 0x00, 0x12, 0x54, 0x0a, 0x16, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, + 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x32, 0x12, 0x20, + 0x2e, 0x73, 0x61, 0x2e, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x41, 0x75, 0x74, 0x68, + 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x43, 0x0a, 0x0d, 0x46, 0x69, + 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x18, 0x2e, 0x73, 0x61, + 0x2e, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, + 0x40, 0x0a, 0x11, 0x4e, 0x65, 0x77, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x41, 0x6e, 0x64, 0x41, 0x75, + 0x74, 0x68, 0x7a, 0x73, 0x12, 0x1c, 0x2e, 0x73, 0x61, 0x2e, 0x4e, 0x65, 0x77, 0x4f, 0x72, 0x64, + 0x65, 0x72, 0x41, 0x6e, 0x64, 0x41, 0x75, 0x74, 0x68, 0x7a, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x0b, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x22, + 0x00, 0x12, 0x3b, 0x0a, 0x0f, 0x4e, 0x65, 0x77, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x52, 0x65, 0x67, 0x69, + 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x1a, 0x12, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, + 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x00, 0x12, 0x4b, + 0x0a, 0x11, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, + 0x61, 0x74, 0x65, 0x12, 0x1c, 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x43, + 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x43, 0x0a, 0x0d, 0x53, + 0x65, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x18, 0x2e, 0x73, + 0x61, 0x2e, 0x53, 0x65, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, + 0x12, 0x40, 0x0a, 0x12, 0x53, 0x65, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x63, + 0x65, 0x73, 0x73, 0x69, 0x6e, 0x67, 0x12, 0x10, 0x2e, 0x73, 0x61, 0x2e, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, - 0x22, 0x00, 0x12, 0x40, 0x0a, 0x12, 0x53, 0x65, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x50, 0x72, - 0x6f, 0x63, 0x65, 0x73, 0x73, 0x69, 0x6e, 0x67, 0x12, 0x10, 0x2e, 0x73, 0x61, 0x2e, 0x4f, 0x72, - 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, - 0x74, 0x79, 0x22, 0x00, 0x12, 0x42, 0x0a, 0x12, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, - 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x2e, 0x63, 0x6f, 0x72, - 0x65, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x1a, 0x16, - 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x52, 0x0a, 0x18, 0x55, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, - 0x63, 0x61, 0x74, 0x65, 0x12, 0x1c, 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, - 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x46, 0x0a, 0x0d, - 0x4c, 0x65, 0x61, 0x73, 0x65, 0x43, 0x52, 0x4c, 0x53, 0x68, 0x61, 0x72, 0x64, 0x12, 0x18, 0x2e, - 0x73, 0x61, 0x2e, 0x4c, 0x65, 0x61, 0x73, 0x65, 0x43, 0x52, 0x4c, 0x53, 0x68, 0x61, 0x72, 0x64, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x73, 0x61, 0x2e, 0x4c, 0x65, 0x61, - 0x73, 0x65, 0x43, 0x52, 0x4c, 0x53, 0x68, 0x61, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x00, 0x12, 0x45, 0x0a, 0x0e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x52, - 0x4c, 0x53, 0x68, 0x61, 0x72, 0x64, 0x12, 0x19, 0x2e, 0x73, 0x61, 0x2e, 0x55, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x43, 0x52, 0x4c, 0x53, 0x68, 0x61, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x44, 0x0a, 0x10, 0x50, - 0x61, 0x75, 0x73, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x12, - 0x10, 0x2e, 0x73, 0x61, 0x2e, 0x50, 0x61, 0x75, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x1c, 0x2e, 0x73, 0x61, 0x2e, 0x50, 0x61, 0x75, 0x73, 0x65, 0x49, 0x64, 0x65, 0x6e, - 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x00, 0x12, 0x3e, 0x0a, 0x0e, 0x55, 0x6e, 0x70, 0x61, 0x75, 0x73, 0x65, 0x41, 0x63, 0x63, 0x6f, - 0x75, 0x6e, 0x74, 0x12, 0x12, 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, - 0x00, 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, 0x73, 0x61, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x33, -} + 0x22, 0x00, 0x12, 0x57, 0x0a, 0x19, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, + 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x63, 0x74, 0x12, + 0x24, 0x2e, 0x73, 0x61, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, + 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x63, 0x74, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x52, 0x65, 0x67, + 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x00, 0x12, 0x4f, 0x0a, 0x15, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x4b, 0x65, 0x79, 0x12, 0x20, 0x2e, 0x73, 0x61, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4b, 0x65, 0x79, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x52, 0x65, + 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x00, 0x12, 0x52, 0x0a, 0x18, + 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x43, 0x65, 0x72, + 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x1c, 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x65, + 0x76, 0x6f, 0x6b, 0x65, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, + 0x12, 0x46, 0x0a, 0x0d, 0x4c, 0x65, 0x61, 0x73, 0x65, 0x43, 0x52, 0x4c, 0x53, 0x68, 0x61, 0x72, + 0x64, 0x12, 0x18, 0x2e, 0x73, 0x61, 0x2e, 0x4c, 0x65, 0x61, 0x73, 0x65, 0x43, 0x52, 0x4c, 0x53, + 0x68, 0x61, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x73, 0x61, + 0x2e, 0x4c, 0x65, 0x61, 0x73, 0x65, 0x43, 0x52, 0x4c, 0x53, 0x68, 0x61, 0x72, 0x64, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x45, 0x0a, 0x0e, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x43, 0x52, 0x4c, 0x53, 0x68, 0x61, 0x72, 0x64, 0x12, 0x19, 0x2e, 0x73, 0x61, 0x2e, + 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x52, 0x4c, 0x53, 0x68, 0x61, 0x72, 0x64, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, + 0x44, 0x0a, 0x10, 0x50, 0x61, 0x75, 0x73, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, + 0x65, 0x72, 0x73, 0x12, 0x10, 0x2e, 0x73, 0x61, 0x2e, 0x50, 0x61, 0x75, 0x73, 0x65, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x73, 0x61, 0x2e, 0x50, 0x61, 0x75, 0x73, 0x65, + 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x31, 0x0a, 0x0e, 0x55, 0x6e, 0x70, 0x61, 0x75, 0x73, 0x65, + 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x12, 0x2e, 0x73, 0x61, 0x2e, 0x52, 0x65, 0x67, + 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x1a, 0x09, 0x2e, 0x73, 0x61, + 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x00, 0x12, 0x5b, 0x0a, 0x14, 0x41, 0x64, 0x64, 0x52, + 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, + 0x12, 0x1f, 0x2e, 0x73, 0x61, 0x2e, 0x41, 0x64, 0x64, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, + 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x20, 0x2e, 0x73, 0x61, 0x2e, 0x41, 0x64, 0x64, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, + 0x6d, 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x59, 0x0a, 0x18, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, + 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, + 0x65, 0x12, 0x23, 0x2e, 0x73, 0x61, 0x2e, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x61, + 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, + 0x12, 0x57, 0x0a, 0x17, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, + 0x6d, 0x69, 0x74, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x12, 0x22, 0x2e, 0x73, 0x61, + 0x2e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, + 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 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, 0x73, 0x61, 0x2f, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +}) var ( file_sa_proto_rawDescOnce sync.Once - file_sa_proto_rawDescData = file_sa_proto_rawDesc + file_sa_proto_rawDescData []byte ) func file_sa_proto_rawDescGZIP() []byte { file_sa_proto_rawDescOnce.Do(func() { - file_sa_proto_rawDescData = protoimpl.X.CompressGZIP(file_sa_proto_rawDescData) + file_sa_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_sa_proto_rawDesc), len(file_sa_proto_rawDesc))) }) return file_sa_proto_rawDescData } -var file_sa_proto_msgTypes = make([]protoimpl.MessageInfo, 52) -var file_sa_proto_goTypes = []interface{}{ +var file_sa_proto_msgTypes = make([]protoimpl.MessageInfo, 53) +var file_sa_proto_goTypes = []any{ (*RegistrationID)(nil), // 0: sa.RegistrationID (*JSONWebKey)(nil), // 1: sa.JSONWebKey (*AuthorizationID)(nil), // 2: sa.AuthorizationID - (*GetPendingAuthorizationRequest)(nil), // 3: sa.GetPendingAuthorizationRequest - (*GetValidAuthorizationsRequest)(nil), // 4: sa.GetValidAuthorizationsRequest - (*ValidAuthorizations)(nil), // 5: sa.ValidAuthorizations - (*Serial)(nil), // 6: sa.Serial - (*SerialMetadata)(nil), // 7: sa.SerialMetadata - (*Range)(nil), // 8: sa.Range - (*Count)(nil), // 9: sa.Count - (*Timestamps)(nil), // 10: sa.Timestamps - (*CountCertificatesByNamesRequest)(nil), // 11: sa.CountCertificatesByNamesRequest - (*CountByNames)(nil), // 12: sa.CountByNames - (*CountRegistrationsByIPRequest)(nil), // 13: sa.CountRegistrationsByIPRequest - (*CountInvalidAuthorizationsRequest)(nil), // 14: sa.CountInvalidAuthorizationsRequest - (*CountOrdersRequest)(nil), // 15: sa.CountOrdersRequest - (*CountFQDNSetsRequest)(nil), // 16: sa.CountFQDNSetsRequest - (*FQDNSetExistsRequest)(nil), // 17: sa.FQDNSetExistsRequest - (*Exists)(nil), // 18: sa.Exists - (*AddSerialRequest)(nil), // 19: sa.AddSerialRequest - (*AddCertificateRequest)(nil), // 20: sa.AddCertificateRequest - (*OrderRequest)(nil), // 21: sa.OrderRequest - (*NewOrderRequest)(nil), // 22: sa.NewOrderRequest - (*NewOrderAndAuthzsRequest)(nil), // 23: sa.NewOrderAndAuthzsRequest - (*SetOrderErrorRequest)(nil), // 24: sa.SetOrderErrorRequest - (*GetValidOrderAuthorizationsRequest)(nil), // 25: sa.GetValidOrderAuthorizationsRequest - (*GetOrderForNamesRequest)(nil), // 26: sa.GetOrderForNamesRequest - (*FinalizeOrderRequest)(nil), // 27: sa.FinalizeOrderRequest - (*GetAuthorizationsRequest)(nil), // 28: sa.GetAuthorizationsRequest - (*Authorizations)(nil), // 29: sa.Authorizations - (*AuthorizationIDs)(nil), // 30: sa.AuthorizationIDs - (*AuthorizationID2)(nil), // 31: sa.AuthorizationID2 - (*RevokeCertificateRequest)(nil), // 32: sa.RevokeCertificateRequest - (*FinalizeAuthorizationRequest)(nil), // 33: sa.FinalizeAuthorizationRequest - (*AddBlockedKeyRequest)(nil), // 34: sa.AddBlockedKeyRequest - (*SPKIHash)(nil), // 35: sa.SPKIHash - (*Incident)(nil), // 36: sa.Incident - (*Incidents)(nil), // 37: sa.Incidents - (*SerialsForIncidentRequest)(nil), // 38: sa.SerialsForIncidentRequest - (*IncidentSerial)(nil), // 39: sa.IncidentSerial - (*GetRevokedCertsRequest)(nil), // 40: sa.GetRevokedCertsRequest - (*RevocationStatus)(nil), // 41: sa.RevocationStatus - (*LeaseCRLShardRequest)(nil), // 42: sa.LeaseCRLShardRequest - (*LeaseCRLShardResponse)(nil), // 43: sa.LeaseCRLShardResponse - (*UpdateCRLShardRequest)(nil), // 44: sa.UpdateCRLShardRequest - (*Identifier)(nil), // 45: sa.Identifier - (*Identifiers)(nil), // 46: sa.Identifiers - (*PauseRequest)(nil), // 47: sa.PauseRequest - (*PauseIdentifiersResponse)(nil), // 48: sa.PauseIdentifiersResponse - (*ValidAuthorizations_MapElement)(nil), // 49: sa.ValidAuthorizations.MapElement - nil, // 50: sa.CountByNames.CountsEntry - (*Authorizations_MapElement)(nil), // 51: sa.Authorizations.MapElement - (*timestamppb.Timestamp)(nil), // 52: google.protobuf.Timestamp - (*durationpb.Duration)(nil), // 53: google.protobuf.Duration - (*proto.Authorization)(nil), // 54: core.Authorization - (*proto.ProblemDetails)(nil), // 55: core.ProblemDetails - (*proto.ValidationRecord)(nil), // 56: core.ValidationRecord - (*emptypb.Empty)(nil), // 57: google.protobuf.Empty - (*proto.Registration)(nil), // 58: core.Registration - (*proto.Certificate)(nil), // 59: core.Certificate - (*proto.CertificateStatus)(nil), // 60: core.CertificateStatus - (*proto.Order)(nil), // 61: core.Order - (*proto.CRLEntry)(nil), // 62: core.CRLEntry + (*GetValidAuthorizationsRequest)(nil), // 3: sa.GetValidAuthorizationsRequest + (*Serial)(nil), // 4: sa.Serial + (*SerialMetadata)(nil), // 5: sa.SerialMetadata + (*Range)(nil), // 6: sa.Range + (*Count)(nil), // 7: sa.Count + (*Timestamps)(nil), // 8: sa.Timestamps + (*CountInvalidAuthorizationsRequest)(nil), // 9: sa.CountInvalidAuthorizationsRequest + (*CountFQDNSetsRequest)(nil), // 10: sa.CountFQDNSetsRequest + (*FQDNSetExistsRequest)(nil), // 11: sa.FQDNSetExistsRequest + (*Exists)(nil), // 12: sa.Exists + (*AddSerialRequest)(nil), // 13: sa.AddSerialRequest + (*AddCertificateRequest)(nil), // 14: sa.AddCertificateRequest + (*OrderRequest)(nil), // 15: sa.OrderRequest + (*NewOrderRequest)(nil), // 16: sa.NewOrderRequest + (*NewAuthzRequest)(nil), // 17: sa.NewAuthzRequest + (*NewOrderAndAuthzsRequest)(nil), // 18: sa.NewOrderAndAuthzsRequest + (*SetOrderErrorRequest)(nil), // 19: sa.SetOrderErrorRequest + (*GetValidOrderAuthorizationsRequest)(nil), // 20: sa.GetValidOrderAuthorizationsRequest + (*GetOrderForNamesRequest)(nil), // 21: sa.GetOrderForNamesRequest + (*FinalizeOrderRequest)(nil), // 22: sa.FinalizeOrderRequest + (*GetAuthorizationsRequest)(nil), // 23: sa.GetAuthorizationsRequest + (*Authorizations)(nil), // 24: sa.Authorizations + (*AuthorizationIDs)(nil), // 25: sa.AuthorizationIDs + (*AuthorizationID2)(nil), // 26: sa.AuthorizationID2 + (*RevokeCertificateRequest)(nil), // 27: sa.RevokeCertificateRequest + (*FinalizeAuthorizationRequest)(nil), // 28: sa.FinalizeAuthorizationRequest + (*AddBlockedKeyRequest)(nil), // 29: sa.AddBlockedKeyRequest + (*SPKIHash)(nil), // 30: sa.SPKIHash + (*Incident)(nil), // 31: sa.Incident + (*Incidents)(nil), // 32: sa.Incidents + (*SerialsForIncidentRequest)(nil), // 33: sa.SerialsForIncidentRequest + (*IncidentSerial)(nil), // 34: sa.IncidentSerial + (*GetRevokedCertsByShardRequest)(nil), // 35: sa.GetRevokedCertsByShardRequest + (*GetRevokedCertsRequest)(nil), // 36: sa.GetRevokedCertsRequest + (*RevocationStatus)(nil), // 37: sa.RevocationStatus + (*LeaseCRLShardRequest)(nil), // 38: sa.LeaseCRLShardRequest + (*LeaseCRLShardResponse)(nil), // 39: sa.LeaseCRLShardResponse + (*UpdateCRLShardRequest)(nil), // 40: sa.UpdateCRLShardRequest + (*Identifiers)(nil), // 41: sa.Identifiers + (*PauseRequest)(nil), // 42: sa.PauseRequest + (*PauseIdentifiersResponse)(nil), // 43: sa.PauseIdentifiersResponse + (*UpdateRegistrationContactRequest)(nil), // 44: sa.UpdateRegistrationContactRequest + (*UpdateRegistrationKeyRequest)(nil), // 45: sa.UpdateRegistrationKeyRequest + (*RateLimitOverride)(nil), // 46: sa.RateLimitOverride + (*AddRateLimitOverrideRequest)(nil), // 47: sa.AddRateLimitOverrideRequest + (*AddRateLimitOverrideResponse)(nil), // 48: sa.AddRateLimitOverrideResponse + (*EnableRateLimitOverrideRequest)(nil), // 49: sa.EnableRateLimitOverrideRequest + (*DisableRateLimitOverrideRequest)(nil), // 50: sa.DisableRateLimitOverrideRequest + (*GetRateLimitOverrideRequest)(nil), // 51: sa.GetRateLimitOverrideRequest + (*RateLimitOverrideResponse)(nil), // 52: sa.RateLimitOverrideResponse + (*proto.Identifier)(nil), // 53: core.Identifier + (*timestamppb.Timestamp)(nil), // 54: google.protobuf.Timestamp + (*durationpb.Duration)(nil), // 55: google.protobuf.Duration + (*proto.ProblemDetails)(nil), // 56: core.ProblemDetails + (*proto.Authorization)(nil), // 57: core.Authorization + (*proto.ValidationRecord)(nil), // 58: core.ValidationRecord + (*emptypb.Empty)(nil), // 59: google.protobuf.Empty + (*proto.Registration)(nil), // 60: core.Registration + (*proto.Certificate)(nil), // 61: core.Certificate + (*proto.CertificateStatus)(nil), // 62: core.CertificateStatus + (*proto.Order)(nil), // 63: core.Order + (*proto.CRLEntry)(nil), // 64: core.CRLEntry } var file_sa_proto_depIdxs = []int32{ - 52, // 0: sa.GetPendingAuthorizationRequest.validUntil:type_name -> google.protobuf.Timestamp - 52, // 1: sa.GetValidAuthorizationsRequest.now:type_name -> google.protobuf.Timestamp - 49, // 2: sa.ValidAuthorizations.valid:type_name -> sa.ValidAuthorizations.MapElement - 52, // 3: sa.SerialMetadata.created:type_name -> google.protobuf.Timestamp - 52, // 4: sa.SerialMetadata.expires:type_name -> google.protobuf.Timestamp - 52, // 5: sa.Range.earliest:type_name -> google.protobuf.Timestamp - 52, // 6: sa.Range.latest:type_name -> google.protobuf.Timestamp - 52, // 7: sa.Timestamps.timestamps:type_name -> google.protobuf.Timestamp - 8, // 8: sa.CountCertificatesByNamesRequest.range:type_name -> sa.Range - 50, // 9: sa.CountByNames.counts:type_name -> sa.CountByNames.CountsEntry - 52, // 10: sa.CountByNames.earliest:type_name -> google.protobuf.Timestamp - 8, // 11: sa.CountRegistrationsByIPRequest.range:type_name -> sa.Range - 8, // 12: sa.CountInvalidAuthorizationsRequest.range:type_name -> sa.Range - 8, // 13: sa.CountOrdersRequest.range:type_name -> sa.Range - 53, // 14: sa.CountFQDNSetsRequest.window:type_name -> google.protobuf.Duration - 52, // 15: sa.AddSerialRequest.created:type_name -> google.protobuf.Timestamp - 52, // 16: sa.AddSerialRequest.expires:type_name -> google.protobuf.Timestamp - 52, // 17: sa.AddCertificateRequest.issued:type_name -> google.protobuf.Timestamp - 52, // 18: sa.NewOrderRequest.expires:type_name -> google.protobuf.Timestamp - 22, // 19: sa.NewOrderAndAuthzsRequest.newOrder:type_name -> sa.NewOrderRequest - 54, // 20: sa.NewOrderAndAuthzsRequest.newAuthzs:type_name -> core.Authorization - 55, // 21: sa.SetOrderErrorRequest.error:type_name -> core.ProblemDetails - 52, // 22: sa.GetAuthorizationsRequest.now:type_name -> google.protobuf.Timestamp - 51, // 23: sa.Authorizations.authz:type_name -> sa.Authorizations.MapElement - 52, // 24: sa.RevokeCertificateRequest.date:type_name -> google.protobuf.Timestamp - 52, // 25: sa.RevokeCertificateRequest.backdate:type_name -> google.protobuf.Timestamp - 52, // 26: sa.FinalizeAuthorizationRequest.expires:type_name -> google.protobuf.Timestamp - 56, // 27: sa.FinalizeAuthorizationRequest.validationRecords:type_name -> core.ValidationRecord - 55, // 28: sa.FinalizeAuthorizationRequest.validationError:type_name -> core.ProblemDetails - 52, // 29: sa.FinalizeAuthorizationRequest.attemptedAt:type_name -> google.protobuf.Timestamp - 52, // 30: sa.AddBlockedKeyRequest.added:type_name -> google.protobuf.Timestamp - 52, // 31: sa.Incident.renewBy:type_name -> google.protobuf.Timestamp - 36, // 32: sa.Incidents.incidents:type_name -> sa.Incident - 52, // 33: sa.IncidentSerial.lastNoticeSent:type_name -> google.protobuf.Timestamp - 52, // 34: sa.GetRevokedCertsRequest.expiresAfter:type_name -> google.protobuf.Timestamp - 52, // 35: sa.GetRevokedCertsRequest.expiresBefore:type_name -> google.protobuf.Timestamp - 52, // 36: sa.GetRevokedCertsRequest.revokedBefore:type_name -> google.protobuf.Timestamp - 52, // 37: sa.RevocationStatus.revokedDate:type_name -> google.protobuf.Timestamp - 52, // 38: sa.LeaseCRLShardRequest.until:type_name -> google.protobuf.Timestamp - 52, // 39: sa.UpdateCRLShardRequest.thisUpdate:type_name -> google.protobuf.Timestamp - 52, // 40: sa.UpdateCRLShardRequest.nextUpdate:type_name -> google.protobuf.Timestamp - 45, // 41: sa.Identifiers.identifiers:type_name -> sa.Identifier - 45, // 42: sa.PauseRequest.identifiers:type_name -> sa.Identifier - 54, // 43: sa.ValidAuthorizations.MapElement.authz:type_name -> core.Authorization - 54, // 44: sa.Authorizations.MapElement.authz:type_name -> core.Authorization - 11, // 45: sa.StorageAuthorityReadOnly.CountCertificatesByNames:input_type -> sa.CountCertificatesByNamesRequest - 16, // 46: sa.StorageAuthorityReadOnly.CountFQDNSets:input_type -> sa.CountFQDNSetsRequest - 14, // 47: sa.StorageAuthorityReadOnly.CountInvalidAuthorizations2:input_type -> sa.CountInvalidAuthorizationsRequest - 15, // 48: sa.StorageAuthorityReadOnly.CountOrders:input_type -> sa.CountOrdersRequest - 0, // 49: sa.StorageAuthorityReadOnly.CountPendingAuthorizations2:input_type -> sa.RegistrationID - 13, // 50: sa.StorageAuthorityReadOnly.CountRegistrationsByIP:input_type -> sa.CountRegistrationsByIPRequest - 13, // 51: sa.StorageAuthorityReadOnly.CountRegistrationsByIPRange:input_type -> sa.CountRegistrationsByIPRequest - 17, // 52: sa.StorageAuthorityReadOnly.FQDNSetExists:input_type -> sa.FQDNSetExistsRequest - 16, // 53: sa.StorageAuthorityReadOnly.FQDNSetTimestampsForWindow:input_type -> sa.CountFQDNSetsRequest - 31, // 54: sa.StorageAuthorityReadOnly.GetAuthorization2:input_type -> sa.AuthorizationID2 - 28, // 55: sa.StorageAuthorityReadOnly.GetAuthorizations2:input_type -> sa.GetAuthorizationsRequest - 6, // 56: sa.StorageAuthorityReadOnly.GetCertificate:input_type -> sa.Serial - 6, // 57: sa.StorageAuthorityReadOnly.GetLintPrecertificate:input_type -> sa.Serial - 6, // 58: sa.StorageAuthorityReadOnly.GetCertificateStatus:input_type -> sa.Serial - 57, // 59: sa.StorageAuthorityReadOnly.GetMaxExpiration:input_type -> google.protobuf.Empty - 21, // 60: sa.StorageAuthorityReadOnly.GetOrder:input_type -> sa.OrderRequest - 26, // 61: sa.StorageAuthorityReadOnly.GetOrderForNames:input_type -> sa.GetOrderForNamesRequest - 3, // 62: sa.StorageAuthorityReadOnly.GetPendingAuthorization2:input_type -> sa.GetPendingAuthorizationRequest + 53, // 0: sa.GetValidAuthorizationsRequest.identifiers:type_name -> core.Identifier + 54, // 1: sa.GetValidAuthorizationsRequest.validUntil:type_name -> google.protobuf.Timestamp + 54, // 2: sa.SerialMetadata.created:type_name -> google.protobuf.Timestamp + 54, // 3: sa.SerialMetadata.expires:type_name -> google.protobuf.Timestamp + 54, // 4: sa.Range.earliest:type_name -> google.protobuf.Timestamp + 54, // 5: sa.Range.latest:type_name -> google.protobuf.Timestamp + 54, // 6: sa.Timestamps.timestamps:type_name -> google.protobuf.Timestamp + 53, // 7: sa.CountInvalidAuthorizationsRequest.identifier:type_name -> core.Identifier + 6, // 8: sa.CountInvalidAuthorizationsRequest.range:type_name -> sa.Range + 53, // 9: sa.CountFQDNSetsRequest.identifiers:type_name -> core.Identifier + 55, // 10: sa.CountFQDNSetsRequest.window:type_name -> google.protobuf.Duration + 53, // 11: sa.FQDNSetExistsRequest.identifiers:type_name -> core.Identifier + 54, // 12: sa.AddSerialRequest.created:type_name -> google.protobuf.Timestamp + 54, // 13: sa.AddSerialRequest.expires:type_name -> google.protobuf.Timestamp + 54, // 14: sa.AddCertificateRequest.issued:type_name -> google.protobuf.Timestamp + 54, // 15: sa.NewOrderRequest.expires:type_name -> google.protobuf.Timestamp + 53, // 16: sa.NewOrderRequest.identifiers:type_name -> core.Identifier + 53, // 17: sa.NewAuthzRequest.identifier:type_name -> core.Identifier + 54, // 18: sa.NewAuthzRequest.expires:type_name -> google.protobuf.Timestamp + 16, // 19: sa.NewOrderAndAuthzsRequest.newOrder:type_name -> sa.NewOrderRequest + 17, // 20: sa.NewOrderAndAuthzsRequest.newAuthzs:type_name -> sa.NewAuthzRequest + 56, // 21: sa.SetOrderErrorRequest.error:type_name -> core.ProblemDetails + 53, // 22: sa.GetOrderForNamesRequest.identifiers:type_name -> core.Identifier + 53, // 23: sa.GetAuthorizationsRequest.identifiers:type_name -> core.Identifier + 54, // 24: sa.GetAuthorizationsRequest.validUntil:type_name -> google.protobuf.Timestamp + 57, // 25: sa.Authorizations.authzs:type_name -> core.Authorization + 54, // 26: sa.RevokeCertificateRequest.date:type_name -> google.protobuf.Timestamp + 54, // 27: sa.RevokeCertificateRequest.backdate:type_name -> google.protobuf.Timestamp + 54, // 28: sa.FinalizeAuthorizationRequest.expires:type_name -> google.protobuf.Timestamp + 58, // 29: sa.FinalizeAuthorizationRequest.validationRecords:type_name -> core.ValidationRecord + 56, // 30: sa.FinalizeAuthorizationRequest.validationError:type_name -> core.ProblemDetails + 54, // 31: sa.FinalizeAuthorizationRequest.attemptedAt:type_name -> google.protobuf.Timestamp + 54, // 32: sa.AddBlockedKeyRequest.added:type_name -> google.protobuf.Timestamp + 54, // 33: sa.Incident.renewBy:type_name -> google.protobuf.Timestamp + 31, // 34: sa.Incidents.incidents:type_name -> sa.Incident + 54, // 35: sa.IncidentSerial.lastNoticeSent:type_name -> google.protobuf.Timestamp + 54, // 36: sa.GetRevokedCertsByShardRequest.revokedBefore:type_name -> google.protobuf.Timestamp + 54, // 37: sa.GetRevokedCertsByShardRequest.expiresAfter:type_name -> google.protobuf.Timestamp + 54, // 38: sa.GetRevokedCertsRequest.expiresAfter:type_name -> google.protobuf.Timestamp + 54, // 39: sa.GetRevokedCertsRequest.expiresBefore:type_name -> google.protobuf.Timestamp + 54, // 40: sa.GetRevokedCertsRequest.revokedBefore:type_name -> google.protobuf.Timestamp + 54, // 41: sa.RevocationStatus.revokedDate:type_name -> google.protobuf.Timestamp + 54, // 42: sa.LeaseCRLShardRequest.until:type_name -> google.protobuf.Timestamp + 54, // 43: sa.UpdateCRLShardRequest.thisUpdate:type_name -> google.protobuf.Timestamp + 54, // 44: sa.UpdateCRLShardRequest.nextUpdate:type_name -> google.protobuf.Timestamp + 53, // 45: sa.Identifiers.identifiers:type_name -> core.Identifier + 53, // 46: sa.PauseRequest.identifiers:type_name -> core.Identifier + 55, // 47: sa.RateLimitOverride.period:type_name -> google.protobuf.Duration + 46, // 48: sa.AddRateLimitOverrideRequest.override:type_name -> sa.RateLimitOverride + 46, // 49: sa.RateLimitOverrideResponse.override:type_name -> sa.RateLimitOverride + 54, // 50: sa.RateLimitOverrideResponse.updatedAt:type_name -> google.protobuf.Timestamp + 9, // 51: sa.StorageAuthorityReadOnly.CountInvalidAuthorizations2:input_type -> sa.CountInvalidAuthorizationsRequest + 0, // 52: sa.StorageAuthorityReadOnly.CountPendingAuthorizations2:input_type -> sa.RegistrationID + 11, // 53: sa.StorageAuthorityReadOnly.FQDNSetExists:input_type -> sa.FQDNSetExistsRequest + 10, // 54: sa.StorageAuthorityReadOnly.FQDNSetTimestampsForWindow:input_type -> sa.CountFQDNSetsRequest + 26, // 55: sa.StorageAuthorityReadOnly.GetAuthorization2:input_type -> sa.AuthorizationID2 + 23, // 56: sa.StorageAuthorityReadOnly.GetAuthorizations2:input_type -> sa.GetAuthorizationsRequest + 4, // 57: sa.StorageAuthorityReadOnly.GetCertificate:input_type -> sa.Serial + 4, // 58: sa.StorageAuthorityReadOnly.GetLintPrecertificate:input_type -> sa.Serial + 4, // 59: sa.StorageAuthorityReadOnly.GetCertificateStatus:input_type -> sa.Serial + 59, // 60: sa.StorageAuthorityReadOnly.GetMaxExpiration:input_type -> google.protobuf.Empty + 15, // 61: sa.StorageAuthorityReadOnly.GetOrder:input_type -> sa.OrderRequest + 21, // 62: sa.StorageAuthorityReadOnly.GetOrderForNames:input_type -> sa.GetOrderForNamesRequest 0, // 63: sa.StorageAuthorityReadOnly.GetRegistration:input_type -> sa.RegistrationID 1, // 64: sa.StorageAuthorityReadOnly.GetRegistrationByKey:input_type -> sa.JSONWebKey - 6, // 65: sa.StorageAuthorityReadOnly.GetRevocationStatus:input_type -> sa.Serial - 40, // 66: sa.StorageAuthorityReadOnly.GetRevokedCerts:input_type -> sa.GetRevokedCertsRequest - 6, // 67: sa.StorageAuthorityReadOnly.GetSerialMetadata:input_type -> sa.Serial - 0, // 68: sa.StorageAuthorityReadOnly.GetSerialsByAccount:input_type -> sa.RegistrationID - 35, // 69: sa.StorageAuthorityReadOnly.GetSerialsByKey:input_type -> sa.SPKIHash - 4, // 70: sa.StorageAuthorityReadOnly.GetValidAuthorizations2:input_type -> sa.GetValidAuthorizationsRequest - 25, // 71: sa.StorageAuthorityReadOnly.GetValidOrderAuthorizations2:input_type -> sa.GetValidOrderAuthorizationsRequest - 6, // 72: sa.StorageAuthorityReadOnly.IncidentsForSerial:input_type -> sa.Serial - 35, // 73: sa.StorageAuthorityReadOnly.KeyBlocked:input_type -> sa.SPKIHash - 6, // 74: sa.StorageAuthorityReadOnly.ReplacementOrderExists:input_type -> sa.Serial - 38, // 75: sa.StorageAuthorityReadOnly.SerialsForIncident:input_type -> sa.SerialsForIncidentRequest - 47, // 76: sa.StorageAuthorityReadOnly.CheckIdentifiersPaused:input_type -> sa.PauseRequest - 0, // 77: sa.StorageAuthorityReadOnly.GetPausedIdentifiers:input_type -> sa.RegistrationID - 11, // 78: sa.StorageAuthority.CountCertificatesByNames:input_type -> sa.CountCertificatesByNamesRequest - 16, // 79: sa.StorageAuthority.CountFQDNSets:input_type -> sa.CountFQDNSetsRequest - 14, // 80: sa.StorageAuthority.CountInvalidAuthorizations2:input_type -> sa.CountInvalidAuthorizationsRequest - 15, // 81: sa.StorageAuthority.CountOrders:input_type -> sa.CountOrdersRequest + 4, // 65: sa.StorageAuthorityReadOnly.GetRevocationStatus:input_type -> sa.Serial + 36, // 66: sa.StorageAuthorityReadOnly.GetRevokedCerts:input_type -> sa.GetRevokedCertsRequest + 35, // 67: sa.StorageAuthorityReadOnly.GetRevokedCertsByShard:input_type -> sa.GetRevokedCertsByShardRequest + 4, // 68: sa.StorageAuthorityReadOnly.GetSerialMetadata:input_type -> sa.Serial + 0, // 69: sa.StorageAuthorityReadOnly.GetSerialsByAccount:input_type -> sa.RegistrationID + 30, // 70: sa.StorageAuthorityReadOnly.GetSerialsByKey:input_type -> sa.SPKIHash + 3, // 71: sa.StorageAuthorityReadOnly.GetValidAuthorizations2:input_type -> sa.GetValidAuthorizationsRequest + 20, // 72: sa.StorageAuthorityReadOnly.GetValidOrderAuthorizations2:input_type -> sa.GetValidOrderAuthorizationsRequest + 4, // 73: sa.StorageAuthorityReadOnly.IncidentsForSerial:input_type -> sa.Serial + 30, // 74: sa.StorageAuthorityReadOnly.KeyBlocked:input_type -> sa.SPKIHash + 4, // 75: sa.StorageAuthorityReadOnly.ReplacementOrderExists:input_type -> sa.Serial + 33, // 76: sa.StorageAuthorityReadOnly.SerialsForIncident:input_type -> sa.SerialsForIncidentRequest + 42, // 77: sa.StorageAuthorityReadOnly.CheckIdentifiersPaused:input_type -> sa.PauseRequest + 0, // 78: sa.StorageAuthorityReadOnly.GetPausedIdentifiers:input_type -> sa.RegistrationID + 51, // 79: sa.StorageAuthorityReadOnly.GetRateLimitOverride:input_type -> sa.GetRateLimitOverrideRequest + 59, // 80: sa.StorageAuthorityReadOnly.GetEnabledRateLimitOverrides:input_type -> google.protobuf.Empty + 9, // 81: sa.StorageAuthority.CountInvalidAuthorizations2:input_type -> sa.CountInvalidAuthorizationsRequest 0, // 82: sa.StorageAuthority.CountPendingAuthorizations2:input_type -> sa.RegistrationID - 13, // 83: sa.StorageAuthority.CountRegistrationsByIP:input_type -> sa.CountRegistrationsByIPRequest - 13, // 84: sa.StorageAuthority.CountRegistrationsByIPRange:input_type -> sa.CountRegistrationsByIPRequest - 17, // 85: sa.StorageAuthority.FQDNSetExists:input_type -> sa.FQDNSetExistsRequest - 16, // 86: sa.StorageAuthority.FQDNSetTimestampsForWindow:input_type -> sa.CountFQDNSetsRequest - 31, // 87: sa.StorageAuthority.GetAuthorization2:input_type -> sa.AuthorizationID2 - 28, // 88: sa.StorageAuthority.GetAuthorizations2:input_type -> sa.GetAuthorizationsRequest - 6, // 89: sa.StorageAuthority.GetCertificate:input_type -> sa.Serial - 6, // 90: sa.StorageAuthority.GetLintPrecertificate:input_type -> sa.Serial - 6, // 91: sa.StorageAuthority.GetCertificateStatus:input_type -> sa.Serial - 57, // 92: sa.StorageAuthority.GetMaxExpiration:input_type -> google.protobuf.Empty - 21, // 93: sa.StorageAuthority.GetOrder:input_type -> sa.OrderRequest - 26, // 94: sa.StorageAuthority.GetOrderForNames:input_type -> sa.GetOrderForNamesRequest - 3, // 95: sa.StorageAuthority.GetPendingAuthorization2:input_type -> sa.GetPendingAuthorizationRequest - 0, // 96: sa.StorageAuthority.GetRegistration:input_type -> sa.RegistrationID - 1, // 97: sa.StorageAuthority.GetRegistrationByKey:input_type -> sa.JSONWebKey - 6, // 98: sa.StorageAuthority.GetRevocationStatus:input_type -> sa.Serial - 40, // 99: sa.StorageAuthority.GetRevokedCerts:input_type -> sa.GetRevokedCertsRequest - 6, // 100: sa.StorageAuthority.GetSerialMetadata:input_type -> sa.Serial - 0, // 101: sa.StorageAuthority.GetSerialsByAccount:input_type -> sa.RegistrationID - 35, // 102: sa.StorageAuthority.GetSerialsByKey:input_type -> sa.SPKIHash - 4, // 103: sa.StorageAuthority.GetValidAuthorizations2:input_type -> sa.GetValidAuthorizationsRequest - 25, // 104: sa.StorageAuthority.GetValidOrderAuthorizations2:input_type -> sa.GetValidOrderAuthorizationsRequest - 6, // 105: sa.StorageAuthority.IncidentsForSerial:input_type -> sa.Serial - 35, // 106: sa.StorageAuthority.KeyBlocked:input_type -> sa.SPKIHash - 6, // 107: sa.StorageAuthority.ReplacementOrderExists:input_type -> sa.Serial - 38, // 108: sa.StorageAuthority.SerialsForIncident:input_type -> sa.SerialsForIncidentRequest - 47, // 109: sa.StorageAuthority.CheckIdentifiersPaused:input_type -> sa.PauseRequest - 0, // 110: sa.StorageAuthority.GetPausedIdentifiers:input_type -> sa.RegistrationID - 34, // 111: sa.StorageAuthority.AddBlockedKey:input_type -> sa.AddBlockedKeyRequest - 20, // 112: sa.StorageAuthority.AddCertificate:input_type -> sa.AddCertificateRequest - 20, // 113: sa.StorageAuthority.AddPrecertificate:input_type -> sa.AddCertificateRequest - 6, // 114: sa.StorageAuthority.SetCertificateStatusReady:input_type -> sa.Serial - 19, // 115: sa.StorageAuthority.AddSerial:input_type -> sa.AddSerialRequest - 31, // 116: sa.StorageAuthority.DeactivateAuthorization2:input_type -> sa.AuthorizationID2 + 11, // 83: sa.StorageAuthority.FQDNSetExists:input_type -> sa.FQDNSetExistsRequest + 10, // 84: sa.StorageAuthority.FQDNSetTimestampsForWindow:input_type -> sa.CountFQDNSetsRequest + 26, // 85: sa.StorageAuthority.GetAuthorization2:input_type -> sa.AuthorizationID2 + 23, // 86: sa.StorageAuthority.GetAuthorizations2:input_type -> sa.GetAuthorizationsRequest + 4, // 87: sa.StorageAuthority.GetCertificate:input_type -> sa.Serial + 4, // 88: sa.StorageAuthority.GetLintPrecertificate:input_type -> sa.Serial + 4, // 89: sa.StorageAuthority.GetCertificateStatus:input_type -> sa.Serial + 59, // 90: sa.StorageAuthority.GetMaxExpiration:input_type -> google.protobuf.Empty + 15, // 91: sa.StorageAuthority.GetOrder:input_type -> sa.OrderRequest + 21, // 92: sa.StorageAuthority.GetOrderForNames:input_type -> sa.GetOrderForNamesRequest + 0, // 93: sa.StorageAuthority.GetRegistration:input_type -> sa.RegistrationID + 1, // 94: sa.StorageAuthority.GetRegistrationByKey:input_type -> sa.JSONWebKey + 4, // 95: sa.StorageAuthority.GetRevocationStatus:input_type -> sa.Serial + 36, // 96: sa.StorageAuthority.GetRevokedCerts:input_type -> sa.GetRevokedCertsRequest + 35, // 97: sa.StorageAuthority.GetRevokedCertsByShard:input_type -> sa.GetRevokedCertsByShardRequest + 4, // 98: sa.StorageAuthority.GetSerialMetadata:input_type -> sa.Serial + 0, // 99: sa.StorageAuthority.GetSerialsByAccount:input_type -> sa.RegistrationID + 30, // 100: sa.StorageAuthority.GetSerialsByKey:input_type -> sa.SPKIHash + 3, // 101: sa.StorageAuthority.GetValidAuthorizations2:input_type -> sa.GetValidAuthorizationsRequest + 20, // 102: sa.StorageAuthority.GetValidOrderAuthorizations2:input_type -> sa.GetValidOrderAuthorizationsRequest + 4, // 103: sa.StorageAuthority.IncidentsForSerial:input_type -> sa.Serial + 30, // 104: sa.StorageAuthority.KeyBlocked:input_type -> sa.SPKIHash + 4, // 105: sa.StorageAuthority.ReplacementOrderExists:input_type -> sa.Serial + 33, // 106: sa.StorageAuthority.SerialsForIncident:input_type -> sa.SerialsForIncidentRequest + 42, // 107: sa.StorageAuthority.CheckIdentifiersPaused:input_type -> sa.PauseRequest + 0, // 108: sa.StorageAuthority.GetPausedIdentifiers:input_type -> sa.RegistrationID + 51, // 109: sa.StorageAuthority.GetRateLimitOverride:input_type -> sa.GetRateLimitOverrideRequest + 59, // 110: sa.StorageAuthority.GetEnabledRateLimitOverrides:input_type -> google.protobuf.Empty + 29, // 111: sa.StorageAuthority.AddBlockedKey:input_type -> sa.AddBlockedKeyRequest + 14, // 112: sa.StorageAuthority.AddCertificate:input_type -> sa.AddCertificateRequest + 14, // 113: sa.StorageAuthority.AddPrecertificate:input_type -> sa.AddCertificateRequest + 4, // 114: sa.StorageAuthority.SetCertificateStatusReady:input_type -> sa.Serial + 13, // 115: sa.StorageAuthority.AddSerial:input_type -> sa.AddSerialRequest + 26, // 116: sa.StorageAuthority.DeactivateAuthorization2:input_type -> sa.AuthorizationID2 0, // 117: sa.StorageAuthority.DeactivateRegistration:input_type -> sa.RegistrationID - 33, // 118: sa.StorageAuthority.FinalizeAuthorization2:input_type -> sa.FinalizeAuthorizationRequest - 27, // 119: sa.StorageAuthority.FinalizeOrder:input_type -> sa.FinalizeOrderRequest - 23, // 120: sa.StorageAuthority.NewOrderAndAuthzs:input_type -> sa.NewOrderAndAuthzsRequest - 58, // 121: sa.StorageAuthority.NewRegistration:input_type -> core.Registration - 32, // 122: sa.StorageAuthority.RevokeCertificate:input_type -> sa.RevokeCertificateRequest - 24, // 123: sa.StorageAuthority.SetOrderError:input_type -> sa.SetOrderErrorRequest - 21, // 124: sa.StorageAuthority.SetOrderProcessing:input_type -> sa.OrderRequest - 58, // 125: sa.StorageAuthority.UpdateRegistration:input_type -> core.Registration - 32, // 126: sa.StorageAuthority.UpdateRevokedCertificate:input_type -> sa.RevokeCertificateRequest - 42, // 127: sa.StorageAuthority.LeaseCRLShard:input_type -> sa.LeaseCRLShardRequest - 44, // 128: sa.StorageAuthority.UpdateCRLShard:input_type -> sa.UpdateCRLShardRequest - 47, // 129: sa.StorageAuthority.PauseIdentifiers:input_type -> sa.PauseRequest - 0, // 130: sa.StorageAuthority.UnpauseAccount:input_type -> sa.RegistrationID - 12, // 131: sa.StorageAuthorityReadOnly.CountCertificatesByNames:output_type -> sa.CountByNames - 9, // 132: sa.StorageAuthorityReadOnly.CountFQDNSets:output_type -> sa.Count - 9, // 133: sa.StorageAuthorityReadOnly.CountInvalidAuthorizations2:output_type -> sa.Count - 9, // 134: sa.StorageAuthorityReadOnly.CountOrders:output_type -> sa.Count - 9, // 135: sa.StorageAuthorityReadOnly.CountPendingAuthorizations2:output_type -> sa.Count - 9, // 136: sa.StorageAuthorityReadOnly.CountRegistrationsByIP:output_type -> sa.Count - 9, // 137: sa.StorageAuthorityReadOnly.CountRegistrationsByIPRange:output_type -> sa.Count - 18, // 138: sa.StorageAuthorityReadOnly.FQDNSetExists:output_type -> sa.Exists - 10, // 139: sa.StorageAuthorityReadOnly.FQDNSetTimestampsForWindow:output_type -> sa.Timestamps - 54, // 140: sa.StorageAuthorityReadOnly.GetAuthorization2:output_type -> core.Authorization - 29, // 141: sa.StorageAuthorityReadOnly.GetAuthorizations2:output_type -> sa.Authorizations - 59, // 142: sa.StorageAuthorityReadOnly.GetCertificate:output_type -> core.Certificate - 59, // 143: sa.StorageAuthorityReadOnly.GetLintPrecertificate:output_type -> core.Certificate - 60, // 144: sa.StorageAuthorityReadOnly.GetCertificateStatus:output_type -> core.CertificateStatus - 52, // 145: sa.StorageAuthorityReadOnly.GetMaxExpiration:output_type -> google.protobuf.Timestamp - 61, // 146: sa.StorageAuthorityReadOnly.GetOrder:output_type -> core.Order - 61, // 147: sa.StorageAuthorityReadOnly.GetOrderForNames:output_type -> core.Order - 54, // 148: sa.StorageAuthorityReadOnly.GetPendingAuthorization2:output_type -> core.Authorization - 58, // 149: sa.StorageAuthorityReadOnly.GetRegistration:output_type -> core.Registration - 58, // 150: sa.StorageAuthorityReadOnly.GetRegistrationByKey:output_type -> core.Registration - 41, // 151: sa.StorageAuthorityReadOnly.GetRevocationStatus:output_type -> sa.RevocationStatus - 62, // 152: sa.StorageAuthorityReadOnly.GetRevokedCerts:output_type -> core.CRLEntry - 7, // 153: sa.StorageAuthorityReadOnly.GetSerialMetadata:output_type -> sa.SerialMetadata - 6, // 154: sa.StorageAuthorityReadOnly.GetSerialsByAccount:output_type -> sa.Serial - 6, // 155: sa.StorageAuthorityReadOnly.GetSerialsByKey:output_type -> sa.Serial - 29, // 156: sa.StorageAuthorityReadOnly.GetValidAuthorizations2:output_type -> sa.Authorizations - 29, // 157: sa.StorageAuthorityReadOnly.GetValidOrderAuthorizations2:output_type -> sa.Authorizations - 37, // 158: sa.StorageAuthorityReadOnly.IncidentsForSerial:output_type -> sa.Incidents - 18, // 159: sa.StorageAuthorityReadOnly.KeyBlocked:output_type -> sa.Exists - 18, // 160: sa.StorageAuthorityReadOnly.ReplacementOrderExists:output_type -> sa.Exists - 39, // 161: sa.StorageAuthorityReadOnly.SerialsForIncident:output_type -> sa.IncidentSerial - 46, // 162: sa.StorageAuthorityReadOnly.CheckIdentifiersPaused:output_type -> sa.Identifiers - 46, // 163: sa.StorageAuthorityReadOnly.GetPausedIdentifiers:output_type -> sa.Identifiers - 12, // 164: sa.StorageAuthority.CountCertificatesByNames:output_type -> sa.CountByNames - 9, // 165: sa.StorageAuthority.CountFQDNSets:output_type -> sa.Count - 9, // 166: sa.StorageAuthority.CountInvalidAuthorizations2:output_type -> sa.Count - 9, // 167: sa.StorageAuthority.CountOrders:output_type -> sa.Count - 9, // 168: sa.StorageAuthority.CountPendingAuthorizations2:output_type -> sa.Count - 9, // 169: sa.StorageAuthority.CountRegistrationsByIP:output_type -> sa.Count - 9, // 170: sa.StorageAuthority.CountRegistrationsByIPRange:output_type -> sa.Count - 18, // 171: sa.StorageAuthority.FQDNSetExists:output_type -> sa.Exists - 10, // 172: sa.StorageAuthority.FQDNSetTimestampsForWindow:output_type -> sa.Timestamps - 54, // 173: sa.StorageAuthority.GetAuthorization2:output_type -> core.Authorization - 29, // 174: sa.StorageAuthority.GetAuthorizations2:output_type -> sa.Authorizations - 59, // 175: sa.StorageAuthority.GetCertificate:output_type -> core.Certificate - 59, // 176: sa.StorageAuthority.GetLintPrecertificate:output_type -> core.Certificate - 60, // 177: sa.StorageAuthority.GetCertificateStatus:output_type -> core.CertificateStatus - 52, // 178: sa.StorageAuthority.GetMaxExpiration:output_type -> google.protobuf.Timestamp - 61, // 179: sa.StorageAuthority.GetOrder:output_type -> core.Order - 61, // 180: sa.StorageAuthority.GetOrderForNames:output_type -> core.Order - 54, // 181: sa.StorageAuthority.GetPendingAuthorization2:output_type -> core.Authorization - 58, // 182: sa.StorageAuthority.GetRegistration:output_type -> core.Registration - 58, // 183: sa.StorageAuthority.GetRegistrationByKey:output_type -> core.Registration - 41, // 184: sa.StorageAuthority.GetRevocationStatus:output_type -> sa.RevocationStatus - 62, // 185: sa.StorageAuthority.GetRevokedCerts:output_type -> core.CRLEntry - 7, // 186: sa.StorageAuthority.GetSerialMetadata:output_type -> sa.SerialMetadata - 6, // 187: sa.StorageAuthority.GetSerialsByAccount:output_type -> sa.Serial - 6, // 188: sa.StorageAuthority.GetSerialsByKey:output_type -> sa.Serial - 29, // 189: sa.StorageAuthority.GetValidAuthorizations2:output_type -> sa.Authorizations - 29, // 190: sa.StorageAuthority.GetValidOrderAuthorizations2:output_type -> sa.Authorizations - 37, // 191: sa.StorageAuthority.IncidentsForSerial:output_type -> sa.Incidents - 18, // 192: sa.StorageAuthority.KeyBlocked:output_type -> sa.Exists - 18, // 193: sa.StorageAuthority.ReplacementOrderExists:output_type -> sa.Exists - 39, // 194: sa.StorageAuthority.SerialsForIncident:output_type -> sa.IncidentSerial - 46, // 195: sa.StorageAuthority.CheckIdentifiersPaused:output_type -> sa.Identifiers - 46, // 196: sa.StorageAuthority.GetPausedIdentifiers:output_type -> sa.Identifiers - 57, // 197: sa.StorageAuthority.AddBlockedKey:output_type -> google.protobuf.Empty - 57, // 198: sa.StorageAuthority.AddCertificate:output_type -> google.protobuf.Empty - 57, // 199: sa.StorageAuthority.AddPrecertificate:output_type -> google.protobuf.Empty - 57, // 200: sa.StorageAuthority.SetCertificateStatusReady:output_type -> google.protobuf.Empty - 57, // 201: sa.StorageAuthority.AddSerial:output_type -> google.protobuf.Empty - 57, // 202: sa.StorageAuthority.DeactivateAuthorization2:output_type -> google.protobuf.Empty - 57, // 203: sa.StorageAuthority.DeactivateRegistration:output_type -> google.protobuf.Empty - 57, // 204: sa.StorageAuthority.FinalizeAuthorization2:output_type -> google.protobuf.Empty - 57, // 205: sa.StorageAuthority.FinalizeOrder:output_type -> google.protobuf.Empty - 61, // 206: sa.StorageAuthority.NewOrderAndAuthzs:output_type -> core.Order - 58, // 207: sa.StorageAuthority.NewRegistration:output_type -> core.Registration - 57, // 208: sa.StorageAuthority.RevokeCertificate:output_type -> google.protobuf.Empty - 57, // 209: sa.StorageAuthority.SetOrderError:output_type -> google.protobuf.Empty - 57, // 210: sa.StorageAuthority.SetOrderProcessing:output_type -> google.protobuf.Empty - 57, // 211: sa.StorageAuthority.UpdateRegistration:output_type -> google.protobuf.Empty - 57, // 212: sa.StorageAuthority.UpdateRevokedCertificate:output_type -> google.protobuf.Empty - 43, // 213: sa.StorageAuthority.LeaseCRLShard:output_type -> sa.LeaseCRLShardResponse - 57, // 214: sa.StorageAuthority.UpdateCRLShard:output_type -> google.protobuf.Empty - 48, // 215: sa.StorageAuthority.PauseIdentifiers:output_type -> sa.PauseIdentifiersResponse - 57, // 216: sa.StorageAuthority.UnpauseAccount:output_type -> google.protobuf.Empty - 131, // [131:217] is the sub-list for method output_type - 45, // [45:131] is the sub-list for method input_type - 45, // [45:45] is the sub-list for extension type_name - 45, // [45:45] is the sub-list for extension extendee - 0, // [0:45] is the sub-list for field type_name + 28, // 118: sa.StorageAuthority.FinalizeAuthorization2:input_type -> sa.FinalizeAuthorizationRequest + 22, // 119: sa.StorageAuthority.FinalizeOrder:input_type -> sa.FinalizeOrderRequest + 18, // 120: sa.StorageAuthority.NewOrderAndAuthzs:input_type -> sa.NewOrderAndAuthzsRequest + 60, // 121: sa.StorageAuthority.NewRegistration:input_type -> core.Registration + 27, // 122: sa.StorageAuthority.RevokeCertificate:input_type -> sa.RevokeCertificateRequest + 19, // 123: sa.StorageAuthority.SetOrderError:input_type -> sa.SetOrderErrorRequest + 15, // 124: sa.StorageAuthority.SetOrderProcessing:input_type -> sa.OrderRequest + 44, // 125: sa.StorageAuthority.UpdateRegistrationContact:input_type -> sa.UpdateRegistrationContactRequest + 45, // 126: sa.StorageAuthority.UpdateRegistrationKey:input_type -> sa.UpdateRegistrationKeyRequest + 27, // 127: sa.StorageAuthority.UpdateRevokedCertificate:input_type -> sa.RevokeCertificateRequest + 38, // 128: sa.StorageAuthority.LeaseCRLShard:input_type -> sa.LeaseCRLShardRequest + 40, // 129: sa.StorageAuthority.UpdateCRLShard:input_type -> sa.UpdateCRLShardRequest + 42, // 130: sa.StorageAuthority.PauseIdentifiers:input_type -> sa.PauseRequest + 0, // 131: sa.StorageAuthority.UnpauseAccount:input_type -> sa.RegistrationID + 47, // 132: sa.StorageAuthority.AddRateLimitOverride:input_type -> sa.AddRateLimitOverrideRequest + 50, // 133: sa.StorageAuthority.DisableRateLimitOverride:input_type -> sa.DisableRateLimitOverrideRequest + 49, // 134: sa.StorageAuthority.EnableRateLimitOverride:input_type -> sa.EnableRateLimitOverrideRequest + 7, // 135: sa.StorageAuthorityReadOnly.CountInvalidAuthorizations2:output_type -> sa.Count + 7, // 136: sa.StorageAuthorityReadOnly.CountPendingAuthorizations2:output_type -> sa.Count + 12, // 137: sa.StorageAuthorityReadOnly.FQDNSetExists:output_type -> sa.Exists + 8, // 138: sa.StorageAuthorityReadOnly.FQDNSetTimestampsForWindow:output_type -> sa.Timestamps + 57, // 139: sa.StorageAuthorityReadOnly.GetAuthorization2:output_type -> core.Authorization + 24, // 140: sa.StorageAuthorityReadOnly.GetAuthorizations2:output_type -> sa.Authorizations + 61, // 141: sa.StorageAuthorityReadOnly.GetCertificate:output_type -> core.Certificate + 61, // 142: sa.StorageAuthorityReadOnly.GetLintPrecertificate:output_type -> core.Certificate + 62, // 143: sa.StorageAuthorityReadOnly.GetCertificateStatus:output_type -> core.CertificateStatus + 54, // 144: sa.StorageAuthorityReadOnly.GetMaxExpiration:output_type -> google.protobuf.Timestamp + 63, // 145: sa.StorageAuthorityReadOnly.GetOrder:output_type -> core.Order + 63, // 146: sa.StorageAuthorityReadOnly.GetOrderForNames:output_type -> core.Order + 60, // 147: sa.StorageAuthorityReadOnly.GetRegistration:output_type -> core.Registration + 60, // 148: sa.StorageAuthorityReadOnly.GetRegistrationByKey:output_type -> core.Registration + 37, // 149: sa.StorageAuthorityReadOnly.GetRevocationStatus:output_type -> sa.RevocationStatus + 64, // 150: sa.StorageAuthorityReadOnly.GetRevokedCerts:output_type -> core.CRLEntry + 64, // 151: sa.StorageAuthorityReadOnly.GetRevokedCertsByShard:output_type -> core.CRLEntry + 5, // 152: sa.StorageAuthorityReadOnly.GetSerialMetadata:output_type -> sa.SerialMetadata + 4, // 153: sa.StorageAuthorityReadOnly.GetSerialsByAccount:output_type -> sa.Serial + 4, // 154: sa.StorageAuthorityReadOnly.GetSerialsByKey:output_type -> sa.Serial + 24, // 155: sa.StorageAuthorityReadOnly.GetValidAuthorizations2:output_type -> sa.Authorizations + 24, // 156: sa.StorageAuthorityReadOnly.GetValidOrderAuthorizations2:output_type -> sa.Authorizations + 32, // 157: sa.StorageAuthorityReadOnly.IncidentsForSerial:output_type -> sa.Incidents + 12, // 158: sa.StorageAuthorityReadOnly.KeyBlocked:output_type -> sa.Exists + 12, // 159: sa.StorageAuthorityReadOnly.ReplacementOrderExists:output_type -> sa.Exists + 34, // 160: sa.StorageAuthorityReadOnly.SerialsForIncident:output_type -> sa.IncidentSerial + 41, // 161: sa.StorageAuthorityReadOnly.CheckIdentifiersPaused:output_type -> sa.Identifiers + 41, // 162: sa.StorageAuthorityReadOnly.GetPausedIdentifiers:output_type -> sa.Identifiers + 52, // 163: sa.StorageAuthorityReadOnly.GetRateLimitOverride:output_type -> sa.RateLimitOverrideResponse + 46, // 164: sa.StorageAuthorityReadOnly.GetEnabledRateLimitOverrides:output_type -> sa.RateLimitOverride + 7, // 165: sa.StorageAuthority.CountInvalidAuthorizations2:output_type -> sa.Count + 7, // 166: sa.StorageAuthority.CountPendingAuthorizations2:output_type -> sa.Count + 12, // 167: sa.StorageAuthority.FQDNSetExists:output_type -> sa.Exists + 8, // 168: sa.StorageAuthority.FQDNSetTimestampsForWindow:output_type -> sa.Timestamps + 57, // 169: sa.StorageAuthority.GetAuthorization2:output_type -> core.Authorization + 24, // 170: sa.StorageAuthority.GetAuthorizations2:output_type -> sa.Authorizations + 61, // 171: sa.StorageAuthority.GetCertificate:output_type -> core.Certificate + 61, // 172: sa.StorageAuthority.GetLintPrecertificate:output_type -> core.Certificate + 62, // 173: sa.StorageAuthority.GetCertificateStatus:output_type -> core.CertificateStatus + 54, // 174: sa.StorageAuthority.GetMaxExpiration:output_type -> google.protobuf.Timestamp + 63, // 175: sa.StorageAuthority.GetOrder:output_type -> core.Order + 63, // 176: sa.StorageAuthority.GetOrderForNames:output_type -> core.Order + 60, // 177: sa.StorageAuthority.GetRegistration:output_type -> core.Registration + 60, // 178: sa.StorageAuthority.GetRegistrationByKey:output_type -> core.Registration + 37, // 179: sa.StorageAuthority.GetRevocationStatus:output_type -> sa.RevocationStatus + 64, // 180: sa.StorageAuthority.GetRevokedCerts:output_type -> core.CRLEntry + 64, // 181: sa.StorageAuthority.GetRevokedCertsByShard:output_type -> core.CRLEntry + 5, // 182: sa.StorageAuthority.GetSerialMetadata:output_type -> sa.SerialMetadata + 4, // 183: sa.StorageAuthority.GetSerialsByAccount:output_type -> sa.Serial + 4, // 184: sa.StorageAuthority.GetSerialsByKey:output_type -> sa.Serial + 24, // 185: sa.StorageAuthority.GetValidAuthorizations2:output_type -> sa.Authorizations + 24, // 186: sa.StorageAuthority.GetValidOrderAuthorizations2:output_type -> sa.Authorizations + 32, // 187: sa.StorageAuthority.IncidentsForSerial:output_type -> sa.Incidents + 12, // 188: sa.StorageAuthority.KeyBlocked:output_type -> sa.Exists + 12, // 189: sa.StorageAuthority.ReplacementOrderExists:output_type -> sa.Exists + 34, // 190: sa.StorageAuthority.SerialsForIncident:output_type -> sa.IncidentSerial + 41, // 191: sa.StorageAuthority.CheckIdentifiersPaused:output_type -> sa.Identifiers + 41, // 192: sa.StorageAuthority.GetPausedIdentifiers:output_type -> sa.Identifiers + 52, // 193: sa.StorageAuthority.GetRateLimitOverride:output_type -> sa.RateLimitOverrideResponse + 46, // 194: sa.StorageAuthority.GetEnabledRateLimitOverrides:output_type -> sa.RateLimitOverride + 59, // 195: sa.StorageAuthority.AddBlockedKey:output_type -> google.protobuf.Empty + 59, // 196: sa.StorageAuthority.AddCertificate:output_type -> google.protobuf.Empty + 59, // 197: sa.StorageAuthority.AddPrecertificate:output_type -> google.protobuf.Empty + 59, // 198: sa.StorageAuthority.SetCertificateStatusReady:output_type -> google.protobuf.Empty + 59, // 199: sa.StorageAuthority.AddSerial:output_type -> google.protobuf.Empty + 59, // 200: sa.StorageAuthority.DeactivateAuthorization2:output_type -> google.protobuf.Empty + 60, // 201: sa.StorageAuthority.DeactivateRegistration:output_type -> core.Registration + 59, // 202: sa.StorageAuthority.FinalizeAuthorization2:output_type -> google.protobuf.Empty + 59, // 203: sa.StorageAuthority.FinalizeOrder:output_type -> google.protobuf.Empty + 63, // 204: sa.StorageAuthority.NewOrderAndAuthzs:output_type -> core.Order + 60, // 205: sa.StorageAuthority.NewRegistration:output_type -> core.Registration + 59, // 206: sa.StorageAuthority.RevokeCertificate:output_type -> google.protobuf.Empty + 59, // 207: sa.StorageAuthority.SetOrderError:output_type -> google.protobuf.Empty + 59, // 208: sa.StorageAuthority.SetOrderProcessing:output_type -> google.protobuf.Empty + 60, // 209: sa.StorageAuthority.UpdateRegistrationContact:output_type -> core.Registration + 60, // 210: sa.StorageAuthority.UpdateRegistrationKey:output_type -> core.Registration + 59, // 211: sa.StorageAuthority.UpdateRevokedCertificate:output_type -> google.protobuf.Empty + 39, // 212: sa.StorageAuthority.LeaseCRLShard:output_type -> sa.LeaseCRLShardResponse + 59, // 213: sa.StorageAuthority.UpdateCRLShard:output_type -> google.protobuf.Empty + 43, // 214: sa.StorageAuthority.PauseIdentifiers:output_type -> sa.PauseIdentifiersResponse + 7, // 215: sa.StorageAuthority.UnpauseAccount:output_type -> sa.Count + 48, // 216: sa.StorageAuthority.AddRateLimitOverride:output_type -> sa.AddRateLimitOverrideResponse + 59, // 217: sa.StorageAuthority.DisableRateLimitOverride:output_type -> google.protobuf.Empty + 59, // 218: sa.StorageAuthority.EnableRateLimitOverride:output_type -> google.protobuf.Empty + 135, // [135:219] is the sub-list for method output_type + 51, // [51:135] is the sub-list for method input_type + 51, // [51:51] is the sub-list for extension type_name + 51, // [51:51] is the sub-list for extension extendee + 0, // [0:51] is the sub-list for field type_name } func init() { file_sa_proto_init() } @@ -4115,627 +4220,13 @@ func file_sa_proto_init() { if File_sa_proto != nil { return } - if !protoimpl.UnsafeEnabled { - file_sa_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RegistrationID); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_sa_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*JSONWebKey); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_sa_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*AuthorizationID); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_sa_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetPendingAuthorizationRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_sa_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetValidAuthorizationsRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_sa_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ValidAuthorizations); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_sa_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Serial); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_sa_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SerialMetadata); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_sa_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Range); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_sa_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Count); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_sa_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Timestamps); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_sa_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CountCertificatesByNamesRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_sa_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CountByNames); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_sa_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CountRegistrationsByIPRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_sa_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CountInvalidAuthorizationsRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_sa_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CountOrdersRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_sa_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CountFQDNSetsRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_sa_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*FQDNSetExistsRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_sa_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Exists); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_sa_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*AddSerialRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_sa_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*AddCertificateRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_sa_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*OrderRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_sa_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*NewOrderRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_sa_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*NewOrderAndAuthzsRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_sa_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SetOrderErrorRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_sa_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetValidOrderAuthorizationsRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_sa_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetOrderForNamesRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_sa_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*FinalizeOrderRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_sa_proto_msgTypes[28].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetAuthorizationsRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_sa_proto_msgTypes[29].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Authorizations); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_sa_proto_msgTypes[30].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*AuthorizationIDs); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_sa_proto_msgTypes[31].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*AuthorizationID2); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_sa_proto_msgTypes[32].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RevokeCertificateRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_sa_proto_msgTypes[33].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*FinalizeAuthorizationRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_sa_proto_msgTypes[34].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*AddBlockedKeyRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_sa_proto_msgTypes[35].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SPKIHash); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_sa_proto_msgTypes[36].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Incident); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_sa_proto_msgTypes[37].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Incidents); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_sa_proto_msgTypes[38].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SerialsForIncidentRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_sa_proto_msgTypes[39].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*IncidentSerial); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_sa_proto_msgTypes[40].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetRevokedCertsRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_sa_proto_msgTypes[41].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RevocationStatus); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_sa_proto_msgTypes[42].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*LeaseCRLShardRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_sa_proto_msgTypes[43].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*LeaseCRLShardResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_sa_proto_msgTypes[44].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UpdateCRLShardRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_sa_proto_msgTypes[45].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Identifier); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_sa_proto_msgTypes[46].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Identifiers); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_sa_proto_msgTypes[47].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PauseRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_sa_proto_msgTypes[48].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PauseIdentifiersResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_sa_proto_msgTypes[49].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ValidAuthorizations_MapElement); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_sa_proto_msgTypes[51].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Authorizations_MapElement); 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_sa_proto_rawDesc, + RawDescriptor: unsafe.Slice(unsafe.StringData(file_sa_proto_rawDesc), len(file_sa_proto_rawDesc)), NumEnums: 0, - NumMessages: 52, + NumMessages: 53, NumExtensions: 0, NumServices: 2, }, @@ -4744,7 +4235,6 @@ func file_sa_proto_init() { MessageInfos: file_sa_proto_msgTypes, }.Build() File_sa_proto = out.File - file_sa_proto_rawDesc = nil file_sa_proto_goTypes = nil file_sa_proto_depIdxs = nil } diff --git a/third-party/github.com/letsencrypt/boulder/sa/proto/sa.proto b/third-party/github.com/letsencrypt/boulder/sa/proto/sa.proto index ec63feafa..b4e494c93 100644 --- a/third-party/github.com/letsencrypt/boulder/sa/proto/sa.proto +++ b/third-party/github.com/letsencrypt/boulder/sa/proto/sa.proto @@ -10,13 +10,8 @@ import "google/protobuf/duration.proto"; // StorageAuthorityReadOnly exposes only those SA methods which are read-only. service StorageAuthorityReadOnly { - rpc CountCertificatesByNames(CountCertificatesByNamesRequest) returns (CountByNames) {} - rpc CountFQDNSets(CountFQDNSetsRequest) returns (Count) {} rpc CountInvalidAuthorizations2(CountInvalidAuthorizationsRequest) returns (Count) {} - rpc CountOrders(CountOrdersRequest) returns (Count) {} rpc CountPendingAuthorizations2(RegistrationID) returns (Count) {} - rpc CountRegistrationsByIP(CountRegistrationsByIPRequest) returns (Count) {} - rpc CountRegistrationsByIPRange(CountRegistrationsByIPRequest) returns (Count) {} rpc FQDNSetExists(FQDNSetExistsRequest) returns (Exists) {} rpc FQDNSetTimestampsForWindow(CountFQDNSetsRequest) returns (Timestamps) {} rpc GetAuthorization2(AuthorizationID2) returns (core.Authorization) {} @@ -27,11 +22,11 @@ service StorageAuthorityReadOnly { rpc GetMaxExpiration(google.protobuf.Empty) returns (google.protobuf.Timestamp) {} rpc GetOrder(OrderRequest) returns (core.Order) {} rpc GetOrderForNames(GetOrderForNamesRequest) returns (core.Order) {} - rpc GetPendingAuthorization2(GetPendingAuthorizationRequest) returns (core.Authorization) {} rpc GetRegistration(RegistrationID) returns (core.Registration) {} rpc GetRegistrationByKey(JSONWebKey) returns (core.Registration) {} rpc GetRevocationStatus(Serial) returns (RevocationStatus) {} rpc GetRevokedCerts(GetRevokedCertsRequest) returns (stream core.CRLEntry) {} + rpc GetRevokedCertsByShard(GetRevokedCertsByShardRequest) returns (stream core.CRLEntry) {} rpc GetSerialMetadata(Serial) returns (SerialMetadata) {} rpc GetSerialsByAccount(RegistrationID) returns (stream Serial) {} rpc GetSerialsByKey(SPKIHash) returns (stream Serial) {} @@ -43,18 +38,15 @@ service StorageAuthorityReadOnly { rpc SerialsForIncident (SerialsForIncidentRequest) returns (stream IncidentSerial) {} rpc CheckIdentifiersPaused (PauseRequest) returns (Identifiers) {} rpc GetPausedIdentifiers (RegistrationID) returns (Identifiers) {} + rpc GetRateLimitOverride(GetRateLimitOverrideRequest) returns (RateLimitOverrideResponse) {} + rpc GetEnabledRateLimitOverrides(google.protobuf.Empty) returns (stream RateLimitOverride) {} } // StorageAuthority provides full read/write access to the database. service StorageAuthority { // Getters: this list must be identical to the StorageAuthorityReadOnly rpcs. - rpc CountCertificatesByNames(CountCertificatesByNamesRequest) returns (CountByNames) {} - rpc CountFQDNSets(CountFQDNSetsRequest) returns (Count) {} rpc CountInvalidAuthorizations2(CountInvalidAuthorizationsRequest) returns (Count) {} - rpc CountOrders(CountOrdersRequest) returns (Count) {} rpc CountPendingAuthorizations2(RegistrationID) returns (Count) {} - rpc CountRegistrationsByIP(CountRegistrationsByIPRequest) returns (Count) {} - rpc CountRegistrationsByIPRange(CountRegistrationsByIPRequest) returns (Count) {} rpc FQDNSetExists(FQDNSetExistsRequest) returns (Exists) {} rpc FQDNSetTimestampsForWindow(CountFQDNSetsRequest) returns (Timestamps) {} rpc GetAuthorization2(AuthorizationID2) returns (core.Authorization) {} @@ -65,11 +57,11 @@ service StorageAuthority { rpc GetMaxExpiration(google.protobuf.Empty) returns (google.protobuf.Timestamp) {} rpc GetOrder(OrderRequest) returns (core.Order) {} rpc GetOrderForNames(GetOrderForNamesRequest) returns (core.Order) {} - rpc GetPendingAuthorization2(GetPendingAuthorizationRequest) returns (core.Authorization) {} rpc GetRegistration(RegistrationID) returns (core.Registration) {} rpc GetRegistrationByKey(JSONWebKey) returns (core.Registration) {} rpc GetRevocationStatus(Serial) returns (RevocationStatus) {} rpc GetRevokedCerts(GetRevokedCertsRequest) returns (stream core.CRLEntry) {} + rpc GetRevokedCertsByShard(GetRevokedCertsByShardRequest) returns (stream core.CRLEntry) {} rpc GetSerialMetadata(Serial) returns (SerialMetadata) {} rpc GetSerialsByAccount(RegistrationID) returns (stream Serial) {} rpc GetSerialsByKey(SPKIHash) returns (stream Serial) {} @@ -81,6 +73,9 @@ service StorageAuthority { rpc SerialsForIncident (SerialsForIncidentRequest) returns (stream IncidentSerial) {} rpc CheckIdentifiersPaused (PauseRequest) returns (Identifiers) {} rpc GetPausedIdentifiers (RegistrationID) returns (Identifiers) {} + rpc GetRateLimitOverride(GetRateLimitOverrideRequest) returns (RateLimitOverrideResponse) {} + rpc GetEnabledRateLimitOverrides(google.protobuf.Empty) returns (stream RateLimitOverride) {} + // Adders rpc AddBlockedKey(AddBlockedKeyRequest) returns (google.protobuf.Empty) {} rpc AddCertificate(AddCertificateRequest) returns (google.protobuf.Empty) {} @@ -88,7 +83,7 @@ service StorageAuthority { rpc SetCertificateStatusReady(Serial) returns (google.protobuf.Empty) {} rpc AddSerial(AddSerialRequest) returns (google.protobuf.Empty) {} rpc DeactivateAuthorization2(AuthorizationID2) returns (google.protobuf.Empty) {} - rpc DeactivateRegistration(RegistrationID) returns (google.protobuf.Empty) {} + rpc DeactivateRegistration(RegistrationID) returns (core.Registration) {} rpc FinalizeAuthorization2(FinalizeAuthorizationRequest) returns (google.protobuf.Empty) {} rpc FinalizeOrder(FinalizeOrderRequest) returns (google.protobuf.Empty) {} rpc NewOrderAndAuthzs(NewOrderAndAuthzsRequest) returns (core.Order) {} @@ -96,12 +91,16 @@ service StorageAuthority { rpc RevokeCertificate(RevokeCertificateRequest) returns (google.protobuf.Empty) {} rpc SetOrderError(SetOrderErrorRequest) returns (google.protobuf.Empty) {} rpc SetOrderProcessing(OrderRequest) returns (google.protobuf.Empty) {} - rpc UpdateRegistration(core.Registration) returns (google.protobuf.Empty) {} + rpc UpdateRegistrationContact(UpdateRegistrationContactRequest) returns (core.Registration) {} + rpc UpdateRegistrationKey(UpdateRegistrationKeyRequest) returns (core.Registration) {} rpc UpdateRevokedCertificate(RevokeCertificateRequest) returns (google.protobuf.Empty) {} rpc LeaseCRLShard(LeaseCRLShardRequest) returns (LeaseCRLShardResponse) {} rpc UpdateCRLShard(UpdateCRLShardRequest) returns (google.protobuf.Empty) {} rpc PauseIdentifiers(PauseRequest) returns (PauseIdentifiersResponse) {} - rpc UnpauseAccount(RegistrationID) returns (google.protobuf.Empty) {} + rpc UnpauseAccount(RegistrationID) returns (Count) {} + rpc AddRateLimitOverride(AddRateLimitOverrideRequest) returns (AddRateLimitOverrideResponse) {} + rpc DisableRateLimitOverride(DisableRateLimitOverrideRequest) returns (google.protobuf.Empty) {} + rpc EnableRateLimitOverride(EnableRateLimitOverrideRequest) returns (google.protobuf.Empty) {} } message RegistrationID { @@ -116,30 +115,14 @@ message AuthorizationID { string id = 1; } -message GetPendingAuthorizationRequest { - // Next unused field number: 6 - int64 registrationID = 1; - string identifierType = 2; - string identifierValue = 3; - // Result must be valid until at least this Unix timestamp (nanos) - reserved 4; // Previously validUntilNS - google.protobuf.Timestamp validUntil = 5; // Result must be valid until at least this timestamp -} - message GetValidAuthorizationsRequest { - // Next unused field number: 5 + // Next unused field number: 7 int64 registrationID = 1; - repeated string domains = 2; + reserved 2; // Previously dnsNames + repeated core.Identifier identifiers = 6; reserved 3; // Previously nowNS - google.protobuf.Timestamp now = 4; -} - -message ValidAuthorizations { - message MapElement { - string domain = 1; - core.Authorization authz = 2; - } - repeated MapElement valid = 1; + google.protobuf.Timestamp validUntil = 4; + string profile = 5; } message Serial { @@ -174,42 +157,28 @@ message Timestamps { repeated google.protobuf.Timestamp timestamps = 2; } -message CountCertificatesByNamesRequest { - Range range = 1; - repeated string names = 2; -} - -message CountByNames { - map counts = 1; - google.protobuf.Timestamp earliest = 2; // Unix timestamp (nanoseconds) -} - -message CountRegistrationsByIPRequest { - bytes ip = 1; - Range range = 2; -} - message CountInvalidAuthorizationsRequest { + // Next unused field number: 5 int64 registrationID = 1; - string hostname = 2; + reserved 2; // Previously dnsName + core.Identifier identifier = 4; // Count authorizations that expire in this range. Range range = 3; } -message CountOrdersRequest { - int64 accountID = 1; - Range range = 2; -} - message CountFQDNSetsRequest { - // Next unused field number: 4 + // Next unused field number: 6 reserved 1; // Previously windowNS - repeated string domains = 2; + reserved 2; // Previously dnsNames + repeated core.Identifier identifiers = 5; google.protobuf.Duration window = 3; + int64 limit = 4; } message FQDNSetExistsRequest { - repeated string domains = 1; + // Next unused field number: 3 + reserved 1; // Previously dnsNames + repeated core.Identifier identifiers = 2; } message Exists { @@ -258,19 +227,44 @@ message OrderRequest { } message NewOrderRequest { - // Next unused field number: 8 + // Next unused field number: 10 int64 registrationID = 1; reserved 2; // Previously expiresNS google.protobuf.Timestamp expires = 5; - repeated string names = 3; + reserved 3; // Previously dnsNames + repeated core.Identifier identifiers = 9; repeated int64 v2Authorizations = 4; - string replacesSerial = 6; string certificateProfileName = 7; + // Replaces is the ARI certificate Id that this order replaces. + string replaces = 8; + // ReplacesSerial is the serial number of the certificate that this order + // replaces. + string replacesSerial = 6; + +} + +// NewAuthzRequest starts with all the same fields as corepb.Authorization, +// because it is replacing that type in NewOrderAndAuthzsRequest, and then +// improves from there. +message NewAuthzRequest { + // Next unused field number: 13 + reserved 1; // previously id + reserved 2; // previously dnsName + core.Identifier identifier = 12; + int64 registrationID = 3; + reserved 4; // previously status + reserved 5; // previously expiresNS + google.protobuf.Timestamp expires = 9; + reserved 6; // previously challenges + reserved 7; // previously ACMEv1 combinations + reserved 8; // previously v2 + repeated string challengeTypes = 10; + string token = 11; } message NewOrderAndAuthzsRequest { NewOrderRequest newOrder = 1; - repeated core.Authorization newAuthzs = 2; + repeated NewAuthzRequest newAuthzs = 2; } message SetOrderErrorRequest { @@ -284,8 +278,10 @@ message GetValidOrderAuthorizationsRequest { } message GetOrderForNamesRequest { + // Next unused field number: 4 int64 acctID = 1; - repeated string names = 2; + reserved 2; // Previously dnsNames + repeated core.Identifier identifiers = 3; } message FinalizeOrderRequest { @@ -294,19 +290,17 @@ message FinalizeOrderRequest { } message GetAuthorizationsRequest { - // Next unused field number: 5 + // Next unused field number: 7 int64 registrationID = 1; - repeated string domains = 2; + reserved 2; // Previously dnsNames + repeated core.Identifier identifiers = 6; reserved 3; // Previously nowNS - google.protobuf.Timestamp now = 4; + google.protobuf.Timestamp validUntil = 4; + string profile = 5; } message Authorizations { - message MapElement { - string domain = 1; - core.Authorization authz = 2; - } - repeated MapElement authz = 1; + repeated core.Authorization authzs = 2; } message AuthorizationIDs { @@ -384,6 +378,13 @@ message IncidentSerial { google.protobuf.Timestamp lastNoticeSent = 5; } +message GetRevokedCertsByShardRequest { + int64 issuerNameID = 1; + google.protobuf.Timestamp revokedBefore = 2; + google.protobuf.Timestamp expiresAfter = 3; + int64 shardIdx = 4; +} + message GetRevokedCertsRequest { // Next unused field number: 9 int64 issuerNameID = 1; @@ -393,7 +394,7 @@ message GetRevokedCertsRequest { google.protobuf.Timestamp expiresBefore = 7; // exclusive reserved 4; // Previously revokedBeforeNS google.protobuf.Timestamp revokedBefore = 8; - int64 shardIdx = 5; // Must not be set until the revokedCertificates table has 90+ days of entries. + reserved 5; } message RevocationStatus { @@ -421,21 +422,65 @@ message UpdateCRLShardRequest { google.protobuf.Timestamp nextUpdate = 4; } -message Identifier { - string type = 1; - string value = 2; -} - message Identifiers { - repeated Identifier identifiers = 1; + repeated core.Identifier identifiers = 1; } message PauseRequest { int64 registrationID = 1; - repeated Identifier identifiers = 2; + repeated core.Identifier identifiers = 2; } message PauseIdentifiersResponse { int64 paused = 1; int64 repaused = 2; } + +message UpdateRegistrationContactRequest { + int64 registrationID = 1; + repeated string contacts = 2; +} + +message UpdateRegistrationKeyRequest { + int64 registrationID = 1; + bytes jwk = 2; +} + +message RateLimitOverride { + int64 limitEnum = 1; + string bucketKey = 2; + string comment = 3; + google.protobuf.Duration period = 4; + int64 count = 5; + int64 burst = 6; +} + +message AddRateLimitOverrideRequest { + RateLimitOverride override = 1; +} + +message AddRateLimitOverrideResponse { + bool inserted = 1; + bool enabled = 2; +} + +message EnableRateLimitOverrideRequest { + int64 limitEnum = 1; + string bucketKey = 2; +} + +message DisableRateLimitOverrideRequest { + int64 limitEnum = 1; + string bucketKey = 2; +} + +message GetRateLimitOverrideRequest { + int64 limitEnum = 1; + string bucketKey = 2; +} + +message RateLimitOverrideResponse { + RateLimitOverride override = 1; + bool enabled = 2; + google.protobuf.Timestamp updatedAt = 3; +} diff --git a/third-party/github.com/letsencrypt/boulder/sa/proto/sa_grpc.pb.go b/third-party/github.com/letsencrypt/boulder/sa/proto/sa_grpc.pb.go index 4736f8fd5..228fd822a 100644 --- a/third-party/github.com/letsencrypt/boulder/sa/proto/sa_grpc.pb.go +++ b/third-party/github.com/letsencrypt/boulder/sa/proto/sa_grpc.pb.go @@ -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: sa.proto @@ -22,13 +22,8 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - StorageAuthorityReadOnly_CountCertificatesByNames_FullMethodName = "/sa.StorageAuthorityReadOnly/CountCertificatesByNames" - StorageAuthorityReadOnly_CountFQDNSets_FullMethodName = "/sa.StorageAuthorityReadOnly/CountFQDNSets" StorageAuthorityReadOnly_CountInvalidAuthorizations2_FullMethodName = "/sa.StorageAuthorityReadOnly/CountInvalidAuthorizations2" - StorageAuthorityReadOnly_CountOrders_FullMethodName = "/sa.StorageAuthorityReadOnly/CountOrders" StorageAuthorityReadOnly_CountPendingAuthorizations2_FullMethodName = "/sa.StorageAuthorityReadOnly/CountPendingAuthorizations2" - StorageAuthorityReadOnly_CountRegistrationsByIP_FullMethodName = "/sa.StorageAuthorityReadOnly/CountRegistrationsByIP" - StorageAuthorityReadOnly_CountRegistrationsByIPRange_FullMethodName = "/sa.StorageAuthorityReadOnly/CountRegistrationsByIPRange" StorageAuthorityReadOnly_FQDNSetExists_FullMethodName = "/sa.StorageAuthorityReadOnly/FQDNSetExists" StorageAuthorityReadOnly_FQDNSetTimestampsForWindow_FullMethodName = "/sa.StorageAuthorityReadOnly/FQDNSetTimestampsForWindow" StorageAuthorityReadOnly_GetAuthorization2_FullMethodName = "/sa.StorageAuthorityReadOnly/GetAuthorization2" @@ -39,11 +34,11 @@ const ( StorageAuthorityReadOnly_GetMaxExpiration_FullMethodName = "/sa.StorageAuthorityReadOnly/GetMaxExpiration" StorageAuthorityReadOnly_GetOrder_FullMethodName = "/sa.StorageAuthorityReadOnly/GetOrder" StorageAuthorityReadOnly_GetOrderForNames_FullMethodName = "/sa.StorageAuthorityReadOnly/GetOrderForNames" - StorageAuthorityReadOnly_GetPendingAuthorization2_FullMethodName = "/sa.StorageAuthorityReadOnly/GetPendingAuthorization2" StorageAuthorityReadOnly_GetRegistration_FullMethodName = "/sa.StorageAuthorityReadOnly/GetRegistration" StorageAuthorityReadOnly_GetRegistrationByKey_FullMethodName = "/sa.StorageAuthorityReadOnly/GetRegistrationByKey" StorageAuthorityReadOnly_GetRevocationStatus_FullMethodName = "/sa.StorageAuthorityReadOnly/GetRevocationStatus" StorageAuthorityReadOnly_GetRevokedCerts_FullMethodName = "/sa.StorageAuthorityReadOnly/GetRevokedCerts" + StorageAuthorityReadOnly_GetRevokedCertsByShard_FullMethodName = "/sa.StorageAuthorityReadOnly/GetRevokedCertsByShard" StorageAuthorityReadOnly_GetSerialMetadata_FullMethodName = "/sa.StorageAuthorityReadOnly/GetSerialMetadata" StorageAuthorityReadOnly_GetSerialsByAccount_FullMethodName = "/sa.StorageAuthorityReadOnly/GetSerialsByAccount" StorageAuthorityReadOnly_GetSerialsByKey_FullMethodName = "/sa.StorageAuthorityReadOnly/GetSerialsByKey" @@ -55,19 +50,18 @@ const ( StorageAuthorityReadOnly_SerialsForIncident_FullMethodName = "/sa.StorageAuthorityReadOnly/SerialsForIncident" StorageAuthorityReadOnly_CheckIdentifiersPaused_FullMethodName = "/sa.StorageAuthorityReadOnly/CheckIdentifiersPaused" StorageAuthorityReadOnly_GetPausedIdentifiers_FullMethodName = "/sa.StorageAuthorityReadOnly/GetPausedIdentifiers" + StorageAuthorityReadOnly_GetRateLimitOverride_FullMethodName = "/sa.StorageAuthorityReadOnly/GetRateLimitOverride" + StorageAuthorityReadOnly_GetEnabledRateLimitOverrides_FullMethodName = "/sa.StorageAuthorityReadOnly/GetEnabledRateLimitOverrides" ) // StorageAuthorityReadOnlyClient is the client API for StorageAuthorityReadOnly 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. +// +// StorageAuthorityReadOnly exposes only those SA methods which are read-only. type StorageAuthorityReadOnlyClient interface { - CountCertificatesByNames(ctx context.Context, in *CountCertificatesByNamesRequest, opts ...grpc.CallOption) (*CountByNames, error) - CountFQDNSets(ctx context.Context, in *CountFQDNSetsRequest, opts ...grpc.CallOption) (*Count, error) CountInvalidAuthorizations2(ctx context.Context, in *CountInvalidAuthorizationsRequest, opts ...grpc.CallOption) (*Count, error) - CountOrders(ctx context.Context, in *CountOrdersRequest, opts ...grpc.CallOption) (*Count, error) CountPendingAuthorizations2(ctx context.Context, in *RegistrationID, opts ...grpc.CallOption) (*Count, error) - CountRegistrationsByIP(ctx context.Context, in *CountRegistrationsByIPRequest, opts ...grpc.CallOption) (*Count, error) - CountRegistrationsByIPRange(ctx context.Context, in *CountRegistrationsByIPRequest, opts ...grpc.CallOption) (*Count, error) FQDNSetExists(ctx context.Context, in *FQDNSetExistsRequest, opts ...grpc.CallOption) (*Exists, error) FQDNSetTimestampsForWindow(ctx context.Context, in *CountFQDNSetsRequest, opts ...grpc.CallOption) (*Timestamps, error) GetAuthorization2(ctx context.Context, in *AuthorizationID2, opts ...grpc.CallOption) (*proto.Authorization, error) @@ -78,11 +72,11 @@ type StorageAuthorityReadOnlyClient interface { GetMaxExpiration(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*timestamppb.Timestamp, error) GetOrder(ctx context.Context, in *OrderRequest, opts ...grpc.CallOption) (*proto.Order, error) GetOrderForNames(ctx context.Context, in *GetOrderForNamesRequest, opts ...grpc.CallOption) (*proto.Order, error) - GetPendingAuthorization2(ctx context.Context, in *GetPendingAuthorizationRequest, opts ...grpc.CallOption) (*proto.Authorization, error) GetRegistration(ctx context.Context, in *RegistrationID, opts ...grpc.CallOption) (*proto.Registration, error) GetRegistrationByKey(ctx context.Context, in *JSONWebKey, opts ...grpc.CallOption) (*proto.Registration, error) GetRevocationStatus(ctx context.Context, in *Serial, opts ...grpc.CallOption) (*RevocationStatus, error) GetRevokedCerts(ctx context.Context, in *GetRevokedCertsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[proto.CRLEntry], error) + GetRevokedCertsByShard(ctx context.Context, in *GetRevokedCertsByShardRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[proto.CRLEntry], error) GetSerialMetadata(ctx context.Context, in *Serial, opts ...grpc.CallOption) (*SerialMetadata, error) GetSerialsByAccount(ctx context.Context, in *RegistrationID, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Serial], error) GetSerialsByKey(ctx context.Context, in *SPKIHash, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Serial], error) @@ -94,6 +88,8 @@ type StorageAuthorityReadOnlyClient interface { SerialsForIncident(ctx context.Context, in *SerialsForIncidentRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[IncidentSerial], error) CheckIdentifiersPaused(ctx context.Context, in *PauseRequest, opts ...grpc.CallOption) (*Identifiers, error) GetPausedIdentifiers(ctx context.Context, in *RegistrationID, opts ...grpc.CallOption) (*Identifiers, error) + GetRateLimitOverride(ctx context.Context, in *GetRateLimitOverrideRequest, opts ...grpc.CallOption) (*RateLimitOverrideResponse, error) + GetEnabledRateLimitOverrides(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[RateLimitOverride], error) } type storageAuthorityReadOnlyClient struct { @@ -104,26 +100,6 @@ func NewStorageAuthorityReadOnlyClient(cc grpc.ClientConnInterface) StorageAutho return &storageAuthorityReadOnlyClient{cc} } -func (c *storageAuthorityReadOnlyClient) CountCertificatesByNames(ctx context.Context, in *CountCertificatesByNamesRequest, opts ...grpc.CallOption) (*CountByNames, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(CountByNames) - err := c.cc.Invoke(ctx, StorageAuthorityReadOnly_CountCertificatesByNames_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *storageAuthorityReadOnlyClient) CountFQDNSets(ctx context.Context, in *CountFQDNSetsRequest, opts ...grpc.CallOption) (*Count, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(Count) - err := c.cc.Invoke(ctx, StorageAuthorityReadOnly_CountFQDNSets_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - func (c *storageAuthorityReadOnlyClient) CountInvalidAuthorizations2(ctx context.Context, in *CountInvalidAuthorizationsRequest, opts ...grpc.CallOption) (*Count, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(Count) @@ -134,16 +110,6 @@ func (c *storageAuthorityReadOnlyClient) CountInvalidAuthorizations2(ctx context return out, nil } -func (c *storageAuthorityReadOnlyClient) CountOrders(ctx context.Context, in *CountOrdersRequest, opts ...grpc.CallOption) (*Count, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(Count) - err := c.cc.Invoke(ctx, StorageAuthorityReadOnly_CountOrders_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - func (c *storageAuthorityReadOnlyClient) CountPendingAuthorizations2(ctx context.Context, in *RegistrationID, opts ...grpc.CallOption) (*Count, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(Count) @@ -154,26 +120,6 @@ func (c *storageAuthorityReadOnlyClient) CountPendingAuthorizations2(ctx context return out, nil } -func (c *storageAuthorityReadOnlyClient) CountRegistrationsByIP(ctx context.Context, in *CountRegistrationsByIPRequest, opts ...grpc.CallOption) (*Count, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(Count) - err := c.cc.Invoke(ctx, StorageAuthorityReadOnly_CountRegistrationsByIP_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *storageAuthorityReadOnlyClient) CountRegistrationsByIPRange(ctx context.Context, in *CountRegistrationsByIPRequest, opts ...grpc.CallOption) (*Count, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(Count) - err := c.cc.Invoke(ctx, StorageAuthorityReadOnly_CountRegistrationsByIPRange_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - func (c *storageAuthorityReadOnlyClient) FQDNSetExists(ctx context.Context, in *FQDNSetExistsRequest, opts ...grpc.CallOption) (*Exists, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(Exists) @@ -274,16 +220,6 @@ func (c *storageAuthorityReadOnlyClient) GetOrderForNames(ctx context.Context, i return out, nil } -func (c *storageAuthorityReadOnlyClient) GetPendingAuthorization2(ctx context.Context, in *GetPendingAuthorizationRequest, opts ...grpc.CallOption) (*proto.Authorization, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(proto.Authorization) - err := c.cc.Invoke(ctx, StorageAuthorityReadOnly_GetPendingAuthorization2_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - func (c *storageAuthorityReadOnlyClient) GetRegistration(ctx context.Context, in *RegistrationID, opts ...grpc.CallOption) (*proto.Registration, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(proto.Registration) @@ -333,6 +269,25 @@ func (c *storageAuthorityReadOnlyClient) GetRevokedCerts(ctx context.Context, in // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. type StorageAuthorityReadOnly_GetRevokedCertsClient = grpc.ServerStreamingClient[proto.CRLEntry] +func (c *storageAuthorityReadOnlyClient) GetRevokedCertsByShard(ctx context.Context, in *GetRevokedCertsByShardRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[proto.CRLEntry], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &StorageAuthorityReadOnly_ServiceDesc.Streams[1], StorageAuthorityReadOnly_GetRevokedCertsByShard_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[GetRevokedCertsByShardRequest, proto.CRLEntry]{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type StorageAuthorityReadOnly_GetRevokedCertsByShardClient = grpc.ServerStreamingClient[proto.CRLEntry] + func (c *storageAuthorityReadOnlyClient) GetSerialMetadata(ctx context.Context, in *Serial, opts ...grpc.CallOption) (*SerialMetadata, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(SerialMetadata) @@ -345,7 +300,7 @@ func (c *storageAuthorityReadOnlyClient) GetSerialMetadata(ctx context.Context, func (c *storageAuthorityReadOnlyClient) GetSerialsByAccount(ctx context.Context, in *RegistrationID, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Serial], error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - stream, err := c.cc.NewStream(ctx, &StorageAuthorityReadOnly_ServiceDesc.Streams[1], StorageAuthorityReadOnly_GetSerialsByAccount_FullMethodName, cOpts...) + stream, err := c.cc.NewStream(ctx, &StorageAuthorityReadOnly_ServiceDesc.Streams[2], StorageAuthorityReadOnly_GetSerialsByAccount_FullMethodName, cOpts...) if err != nil { return nil, err } @@ -364,7 +319,7 @@ type StorageAuthorityReadOnly_GetSerialsByAccountClient = grpc.ServerStreamingCl func (c *storageAuthorityReadOnlyClient) GetSerialsByKey(ctx context.Context, in *SPKIHash, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Serial], error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - stream, err := c.cc.NewStream(ctx, &StorageAuthorityReadOnly_ServiceDesc.Streams[2], StorageAuthorityReadOnly_GetSerialsByKey_FullMethodName, cOpts...) + stream, err := c.cc.NewStream(ctx, &StorageAuthorityReadOnly_ServiceDesc.Streams[3], StorageAuthorityReadOnly_GetSerialsByKey_FullMethodName, cOpts...) if err != nil { return nil, err } @@ -433,7 +388,7 @@ func (c *storageAuthorityReadOnlyClient) ReplacementOrderExists(ctx context.Cont func (c *storageAuthorityReadOnlyClient) SerialsForIncident(ctx context.Context, in *SerialsForIncidentRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[IncidentSerial], error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - stream, err := c.cc.NewStream(ctx, &StorageAuthorityReadOnly_ServiceDesc.Streams[3], StorageAuthorityReadOnly_SerialsForIncident_FullMethodName, cOpts...) + stream, err := c.cc.NewStream(ctx, &StorageAuthorityReadOnly_ServiceDesc.Streams[4], StorageAuthorityReadOnly_SerialsForIncident_FullMethodName, cOpts...) if err != nil { return nil, err } @@ -470,17 +425,43 @@ func (c *storageAuthorityReadOnlyClient) GetPausedIdentifiers(ctx context.Contex return out, nil } +func (c *storageAuthorityReadOnlyClient) GetRateLimitOverride(ctx context.Context, in *GetRateLimitOverrideRequest, opts ...grpc.CallOption) (*RateLimitOverrideResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RateLimitOverrideResponse) + err := c.cc.Invoke(ctx, StorageAuthorityReadOnly_GetRateLimitOverride_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *storageAuthorityReadOnlyClient) GetEnabledRateLimitOverrides(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[RateLimitOverride], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &StorageAuthorityReadOnly_ServiceDesc.Streams[5], StorageAuthorityReadOnly_GetEnabledRateLimitOverrides_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[emptypb.Empty, RateLimitOverride]{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type StorageAuthorityReadOnly_GetEnabledRateLimitOverridesClient = grpc.ServerStreamingClient[RateLimitOverride] + // StorageAuthorityReadOnlyServer is the server API for StorageAuthorityReadOnly service. // All implementations must embed UnimplementedStorageAuthorityReadOnlyServer -// for forward compatibility +// for forward compatibility. +// +// StorageAuthorityReadOnly exposes only those SA methods which are read-only. type StorageAuthorityReadOnlyServer interface { - CountCertificatesByNames(context.Context, *CountCertificatesByNamesRequest) (*CountByNames, error) - CountFQDNSets(context.Context, *CountFQDNSetsRequest) (*Count, error) CountInvalidAuthorizations2(context.Context, *CountInvalidAuthorizationsRequest) (*Count, error) - CountOrders(context.Context, *CountOrdersRequest) (*Count, error) CountPendingAuthorizations2(context.Context, *RegistrationID) (*Count, error) - CountRegistrationsByIP(context.Context, *CountRegistrationsByIPRequest) (*Count, error) - CountRegistrationsByIPRange(context.Context, *CountRegistrationsByIPRequest) (*Count, error) FQDNSetExists(context.Context, *FQDNSetExistsRequest) (*Exists, error) FQDNSetTimestampsForWindow(context.Context, *CountFQDNSetsRequest) (*Timestamps, error) GetAuthorization2(context.Context, *AuthorizationID2) (*proto.Authorization, error) @@ -491,11 +472,11 @@ type StorageAuthorityReadOnlyServer interface { GetMaxExpiration(context.Context, *emptypb.Empty) (*timestamppb.Timestamp, error) GetOrder(context.Context, *OrderRequest) (*proto.Order, error) GetOrderForNames(context.Context, *GetOrderForNamesRequest) (*proto.Order, error) - GetPendingAuthorization2(context.Context, *GetPendingAuthorizationRequest) (*proto.Authorization, error) GetRegistration(context.Context, *RegistrationID) (*proto.Registration, error) GetRegistrationByKey(context.Context, *JSONWebKey) (*proto.Registration, error) GetRevocationStatus(context.Context, *Serial) (*RevocationStatus, error) GetRevokedCerts(*GetRevokedCertsRequest, grpc.ServerStreamingServer[proto.CRLEntry]) error + GetRevokedCertsByShard(*GetRevokedCertsByShardRequest, grpc.ServerStreamingServer[proto.CRLEntry]) error GetSerialMetadata(context.Context, *Serial) (*SerialMetadata, error) GetSerialsByAccount(*RegistrationID, grpc.ServerStreamingServer[Serial]) error GetSerialsByKey(*SPKIHash, grpc.ServerStreamingServer[Serial]) error @@ -507,34 +488,24 @@ type StorageAuthorityReadOnlyServer interface { SerialsForIncident(*SerialsForIncidentRequest, grpc.ServerStreamingServer[IncidentSerial]) error CheckIdentifiersPaused(context.Context, *PauseRequest) (*Identifiers, error) GetPausedIdentifiers(context.Context, *RegistrationID) (*Identifiers, error) + GetRateLimitOverride(context.Context, *GetRateLimitOverrideRequest) (*RateLimitOverrideResponse, error) + GetEnabledRateLimitOverrides(*emptypb.Empty, grpc.ServerStreamingServer[RateLimitOverride]) error mustEmbedUnimplementedStorageAuthorityReadOnlyServer() } -// UnimplementedStorageAuthorityReadOnlyServer must be embedded to have forward compatible implementations. -type UnimplementedStorageAuthorityReadOnlyServer struct { -} +// UnimplementedStorageAuthorityReadOnlyServer 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 UnimplementedStorageAuthorityReadOnlyServer struct{} -func (UnimplementedStorageAuthorityReadOnlyServer) CountCertificatesByNames(context.Context, *CountCertificatesByNamesRequest) (*CountByNames, error) { - return nil, status.Errorf(codes.Unimplemented, "method CountCertificatesByNames not implemented") -} -func (UnimplementedStorageAuthorityReadOnlyServer) CountFQDNSets(context.Context, *CountFQDNSetsRequest) (*Count, error) { - return nil, status.Errorf(codes.Unimplemented, "method CountFQDNSets not implemented") -} func (UnimplementedStorageAuthorityReadOnlyServer) CountInvalidAuthorizations2(context.Context, *CountInvalidAuthorizationsRequest) (*Count, error) { return nil, status.Errorf(codes.Unimplemented, "method CountInvalidAuthorizations2 not implemented") } -func (UnimplementedStorageAuthorityReadOnlyServer) CountOrders(context.Context, *CountOrdersRequest) (*Count, error) { - return nil, status.Errorf(codes.Unimplemented, "method CountOrders not implemented") -} func (UnimplementedStorageAuthorityReadOnlyServer) CountPendingAuthorizations2(context.Context, *RegistrationID) (*Count, error) { return nil, status.Errorf(codes.Unimplemented, "method CountPendingAuthorizations2 not implemented") } -func (UnimplementedStorageAuthorityReadOnlyServer) CountRegistrationsByIP(context.Context, *CountRegistrationsByIPRequest) (*Count, error) { - return nil, status.Errorf(codes.Unimplemented, "method CountRegistrationsByIP not implemented") -} -func (UnimplementedStorageAuthorityReadOnlyServer) CountRegistrationsByIPRange(context.Context, *CountRegistrationsByIPRequest) (*Count, error) { - return nil, status.Errorf(codes.Unimplemented, "method CountRegistrationsByIPRange not implemented") -} func (UnimplementedStorageAuthorityReadOnlyServer) FQDNSetExists(context.Context, *FQDNSetExistsRequest) (*Exists, error) { return nil, status.Errorf(codes.Unimplemented, "method FQDNSetExists not implemented") } @@ -565,9 +536,6 @@ func (UnimplementedStorageAuthorityReadOnlyServer) GetOrder(context.Context, *Or func (UnimplementedStorageAuthorityReadOnlyServer) GetOrderForNames(context.Context, *GetOrderForNamesRequest) (*proto.Order, error) { return nil, status.Errorf(codes.Unimplemented, "method GetOrderForNames not implemented") } -func (UnimplementedStorageAuthorityReadOnlyServer) GetPendingAuthorization2(context.Context, *GetPendingAuthorizationRequest) (*proto.Authorization, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetPendingAuthorization2 not implemented") -} func (UnimplementedStorageAuthorityReadOnlyServer) GetRegistration(context.Context, *RegistrationID) (*proto.Registration, error) { return nil, status.Errorf(codes.Unimplemented, "method GetRegistration not implemented") } @@ -580,6 +548,9 @@ func (UnimplementedStorageAuthorityReadOnlyServer) GetRevocationStatus(context.C func (UnimplementedStorageAuthorityReadOnlyServer) GetRevokedCerts(*GetRevokedCertsRequest, grpc.ServerStreamingServer[proto.CRLEntry]) error { return status.Errorf(codes.Unimplemented, "method GetRevokedCerts not implemented") } +func (UnimplementedStorageAuthorityReadOnlyServer) GetRevokedCertsByShard(*GetRevokedCertsByShardRequest, grpc.ServerStreamingServer[proto.CRLEntry]) error { + return status.Errorf(codes.Unimplemented, "method GetRevokedCertsByShard not implemented") +} func (UnimplementedStorageAuthorityReadOnlyServer) GetSerialMetadata(context.Context, *Serial) (*SerialMetadata, error) { return nil, status.Errorf(codes.Unimplemented, "method GetSerialMetadata not implemented") } @@ -613,8 +584,15 @@ func (UnimplementedStorageAuthorityReadOnlyServer) CheckIdentifiersPaused(contex func (UnimplementedStorageAuthorityReadOnlyServer) GetPausedIdentifiers(context.Context, *RegistrationID) (*Identifiers, error) { return nil, status.Errorf(codes.Unimplemented, "method GetPausedIdentifiers not implemented") } +func (UnimplementedStorageAuthorityReadOnlyServer) GetRateLimitOverride(context.Context, *GetRateLimitOverrideRequest) (*RateLimitOverrideResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetRateLimitOverride not implemented") +} +func (UnimplementedStorageAuthorityReadOnlyServer) GetEnabledRateLimitOverrides(*emptypb.Empty, grpc.ServerStreamingServer[RateLimitOverride]) error { + return status.Errorf(codes.Unimplemented, "method GetEnabledRateLimitOverrides not implemented") +} func (UnimplementedStorageAuthorityReadOnlyServer) mustEmbedUnimplementedStorageAuthorityReadOnlyServer() { } +func (UnimplementedStorageAuthorityReadOnlyServer) testEmbeddedByValue() {} // UnsafeStorageAuthorityReadOnlyServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to StorageAuthorityReadOnlyServer will @@ -624,45 +602,16 @@ type UnsafeStorageAuthorityReadOnlyServer interface { } func RegisterStorageAuthorityReadOnlyServer(s grpc.ServiceRegistrar, srv StorageAuthorityReadOnlyServer) { + // If the following call pancis, it indicates UnimplementedStorageAuthorityReadOnlyServer 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(&StorageAuthorityReadOnly_ServiceDesc, srv) } -func _StorageAuthorityReadOnly_CountCertificatesByNames_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(CountCertificatesByNamesRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(StorageAuthorityReadOnlyServer).CountCertificatesByNames(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: StorageAuthorityReadOnly_CountCertificatesByNames_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(StorageAuthorityReadOnlyServer).CountCertificatesByNames(ctx, req.(*CountCertificatesByNamesRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _StorageAuthorityReadOnly_CountFQDNSets_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(CountFQDNSetsRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(StorageAuthorityReadOnlyServer).CountFQDNSets(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: StorageAuthorityReadOnly_CountFQDNSets_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(StorageAuthorityReadOnlyServer).CountFQDNSets(ctx, req.(*CountFQDNSetsRequest)) - } - return interceptor(ctx, in, info, handler) -} - func _StorageAuthorityReadOnly_CountInvalidAuthorizations2_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(CountInvalidAuthorizationsRequest) if err := dec(in); err != nil { @@ -681,24 +630,6 @@ func _StorageAuthorityReadOnly_CountInvalidAuthorizations2_Handler(srv interface return interceptor(ctx, in, info, handler) } -func _StorageAuthorityReadOnly_CountOrders_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(CountOrdersRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(StorageAuthorityReadOnlyServer).CountOrders(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: StorageAuthorityReadOnly_CountOrders_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(StorageAuthorityReadOnlyServer).CountOrders(ctx, req.(*CountOrdersRequest)) - } - return interceptor(ctx, in, info, handler) -} - func _StorageAuthorityReadOnly_CountPendingAuthorizations2_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(RegistrationID) if err := dec(in); err != nil { @@ -717,42 +648,6 @@ func _StorageAuthorityReadOnly_CountPendingAuthorizations2_Handler(srv interface return interceptor(ctx, in, info, handler) } -func _StorageAuthorityReadOnly_CountRegistrationsByIP_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(CountRegistrationsByIPRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(StorageAuthorityReadOnlyServer).CountRegistrationsByIP(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: StorageAuthorityReadOnly_CountRegistrationsByIP_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(StorageAuthorityReadOnlyServer).CountRegistrationsByIP(ctx, req.(*CountRegistrationsByIPRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _StorageAuthorityReadOnly_CountRegistrationsByIPRange_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(CountRegistrationsByIPRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(StorageAuthorityReadOnlyServer).CountRegistrationsByIPRange(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: StorageAuthorityReadOnly_CountRegistrationsByIPRange_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(StorageAuthorityReadOnlyServer).CountRegistrationsByIPRange(ctx, req.(*CountRegistrationsByIPRequest)) - } - return interceptor(ctx, in, info, handler) -} - func _StorageAuthorityReadOnly_FQDNSetExists_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(FQDNSetExistsRequest) if err := dec(in); err != nil { @@ -933,24 +828,6 @@ func _StorageAuthorityReadOnly_GetOrderForNames_Handler(srv interface{}, ctx con return interceptor(ctx, in, info, handler) } -func _StorageAuthorityReadOnly_GetPendingAuthorization2_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(GetPendingAuthorizationRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(StorageAuthorityReadOnlyServer).GetPendingAuthorization2(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: StorageAuthorityReadOnly_GetPendingAuthorization2_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(StorageAuthorityReadOnlyServer).GetPendingAuthorization2(ctx, req.(*GetPendingAuthorizationRequest)) - } - return interceptor(ctx, in, info, handler) -} - func _StorageAuthorityReadOnly_GetRegistration_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(RegistrationID) if err := dec(in); err != nil { @@ -1016,6 +893,17 @@ func _StorageAuthorityReadOnly_GetRevokedCerts_Handler(srv interface{}, stream g // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. type StorageAuthorityReadOnly_GetRevokedCertsServer = grpc.ServerStreamingServer[proto.CRLEntry] +func _StorageAuthorityReadOnly_GetRevokedCertsByShard_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(GetRevokedCertsByShardRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(StorageAuthorityReadOnlyServer).GetRevokedCertsByShard(m, &grpc.GenericServerStream[GetRevokedCertsByShardRequest, proto.CRLEntry]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type StorageAuthorityReadOnly_GetRevokedCertsByShardServer = grpc.ServerStreamingServer[proto.CRLEntry] + func _StorageAuthorityReadOnly_GetSerialMetadata_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(Serial) if err := dec(in); err != nil { @@ -1193,6 +1081,35 @@ func _StorageAuthorityReadOnly_GetPausedIdentifiers_Handler(srv interface{}, ctx return interceptor(ctx, in, info, handler) } +func _StorageAuthorityReadOnly_GetRateLimitOverride_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetRateLimitOverrideRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StorageAuthorityReadOnlyServer).GetRateLimitOverride(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StorageAuthorityReadOnly_GetRateLimitOverride_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StorageAuthorityReadOnlyServer).GetRateLimitOverride(ctx, req.(*GetRateLimitOverrideRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _StorageAuthorityReadOnly_GetEnabledRateLimitOverrides_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(emptypb.Empty) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(StorageAuthorityReadOnlyServer).GetEnabledRateLimitOverrides(m, &grpc.GenericServerStream[emptypb.Empty, RateLimitOverride]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type StorageAuthorityReadOnly_GetEnabledRateLimitOverridesServer = grpc.ServerStreamingServer[RateLimitOverride] + // StorageAuthorityReadOnly_ServiceDesc is the grpc.ServiceDesc for StorageAuthorityReadOnly service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -1200,34 +1117,14 @@ var StorageAuthorityReadOnly_ServiceDesc = grpc.ServiceDesc{ ServiceName: "sa.StorageAuthorityReadOnly", HandlerType: (*StorageAuthorityReadOnlyServer)(nil), Methods: []grpc.MethodDesc{ - { - MethodName: "CountCertificatesByNames", - Handler: _StorageAuthorityReadOnly_CountCertificatesByNames_Handler, - }, - { - MethodName: "CountFQDNSets", - Handler: _StorageAuthorityReadOnly_CountFQDNSets_Handler, - }, { MethodName: "CountInvalidAuthorizations2", Handler: _StorageAuthorityReadOnly_CountInvalidAuthorizations2_Handler, }, - { - MethodName: "CountOrders", - Handler: _StorageAuthorityReadOnly_CountOrders_Handler, - }, { MethodName: "CountPendingAuthorizations2", Handler: _StorageAuthorityReadOnly_CountPendingAuthorizations2_Handler, }, - { - MethodName: "CountRegistrationsByIP", - Handler: _StorageAuthorityReadOnly_CountRegistrationsByIP_Handler, - }, - { - MethodName: "CountRegistrationsByIPRange", - Handler: _StorageAuthorityReadOnly_CountRegistrationsByIPRange_Handler, - }, { MethodName: "FQDNSetExists", Handler: _StorageAuthorityReadOnly_FQDNSetExists_Handler, @@ -1268,10 +1165,6 @@ var StorageAuthorityReadOnly_ServiceDesc = grpc.ServiceDesc{ MethodName: "GetOrderForNames", Handler: _StorageAuthorityReadOnly_GetOrderForNames_Handler, }, - { - MethodName: "GetPendingAuthorization2", - Handler: _StorageAuthorityReadOnly_GetPendingAuthorization2_Handler, - }, { MethodName: "GetRegistration", Handler: _StorageAuthorityReadOnly_GetRegistration_Handler, @@ -1316,6 +1209,10 @@ var StorageAuthorityReadOnly_ServiceDesc = grpc.ServiceDesc{ MethodName: "GetPausedIdentifiers", Handler: _StorageAuthorityReadOnly_GetPausedIdentifiers_Handler, }, + { + MethodName: "GetRateLimitOverride", + Handler: _StorageAuthorityReadOnly_GetRateLimitOverride_Handler, + }, }, Streams: []grpc.StreamDesc{ { @@ -1323,6 +1220,11 @@ var StorageAuthorityReadOnly_ServiceDesc = grpc.ServiceDesc{ Handler: _StorageAuthorityReadOnly_GetRevokedCerts_Handler, ServerStreams: true, }, + { + StreamName: "GetRevokedCertsByShard", + Handler: _StorageAuthorityReadOnly_GetRevokedCertsByShard_Handler, + ServerStreams: true, + }, { StreamName: "GetSerialsByAccount", Handler: _StorageAuthorityReadOnly_GetSerialsByAccount_Handler, @@ -1338,18 +1240,18 @@ var StorageAuthorityReadOnly_ServiceDesc = grpc.ServiceDesc{ Handler: _StorageAuthorityReadOnly_SerialsForIncident_Handler, ServerStreams: true, }, + { + StreamName: "GetEnabledRateLimitOverrides", + Handler: _StorageAuthorityReadOnly_GetEnabledRateLimitOverrides_Handler, + ServerStreams: true, + }, }, Metadata: "sa.proto", } const ( - StorageAuthority_CountCertificatesByNames_FullMethodName = "/sa.StorageAuthority/CountCertificatesByNames" - StorageAuthority_CountFQDNSets_FullMethodName = "/sa.StorageAuthority/CountFQDNSets" StorageAuthority_CountInvalidAuthorizations2_FullMethodName = "/sa.StorageAuthority/CountInvalidAuthorizations2" - StorageAuthority_CountOrders_FullMethodName = "/sa.StorageAuthority/CountOrders" StorageAuthority_CountPendingAuthorizations2_FullMethodName = "/sa.StorageAuthority/CountPendingAuthorizations2" - StorageAuthority_CountRegistrationsByIP_FullMethodName = "/sa.StorageAuthority/CountRegistrationsByIP" - StorageAuthority_CountRegistrationsByIPRange_FullMethodName = "/sa.StorageAuthority/CountRegistrationsByIPRange" StorageAuthority_FQDNSetExists_FullMethodName = "/sa.StorageAuthority/FQDNSetExists" StorageAuthority_FQDNSetTimestampsForWindow_FullMethodName = "/sa.StorageAuthority/FQDNSetTimestampsForWindow" StorageAuthority_GetAuthorization2_FullMethodName = "/sa.StorageAuthority/GetAuthorization2" @@ -1360,11 +1262,11 @@ const ( StorageAuthority_GetMaxExpiration_FullMethodName = "/sa.StorageAuthority/GetMaxExpiration" StorageAuthority_GetOrder_FullMethodName = "/sa.StorageAuthority/GetOrder" StorageAuthority_GetOrderForNames_FullMethodName = "/sa.StorageAuthority/GetOrderForNames" - StorageAuthority_GetPendingAuthorization2_FullMethodName = "/sa.StorageAuthority/GetPendingAuthorization2" StorageAuthority_GetRegistration_FullMethodName = "/sa.StorageAuthority/GetRegistration" StorageAuthority_GetRegistrationByKey_FullMethodName = "/sa.StorageAuthority/GetRegistrationByKey" StorageAuthority_GetRevocationStatus_FullMethodName = "/sa.StorageAuthority/GetRevocationStatus" StorageAuthority_GetRevokedCerts_FullMethodName = "/sa.StorageAuthority/GetRevokedCerts" + StorageAuthority_GetRevokedCertsByShard_FullMethodName = "/sa.StorageAuthority/GetRevokedCertsByShard" StorageAuthority_GetSerialMetadata_FullMethodName = "/sa.StorageAuthority/GetSerialMetadata" StorageAuthority_GetSerialsByAccount_FullMethodName = "/sa.StorageAuthority/GetSerialsByAccount" StorageAuthority_GetSerialsByKey_FullMethodName = "/sa.StorageAuthority/GetSerialsByKey" @@ -1376,6 +1278,8 @@ const ( StorageAuthority_SerialsForIncident_FullMethodName = "/sa.StorageAuthority/SerialsForIncident" StorageAuthority_CheckIdentifiersPaused_FullMethodName = "/sa.StorageAuthority/CheckIdentifiersPaused" StorageAuthority_GetPausedIdentifiers_FullMethodName = "/sa.StorageAuthority/GetPausedIdentifiers" + StorageAuthority_GetRateLimitOverride_FullMethodName = "/sa.StorageAuthority/GetRateLimitOverride" + StorageAuthority_GetEnabledRateLimitOverrides_FullMethodName = "/sa.StorageAuthority/GetEnabledRateLimitOverrides" StorageAuthority_AddBlockedKey_FullMethodName = "/sa.StorageAuthority/AddBlockedKey" StorageAuthority_AddCertificate_FullMethodName = "/sa.StorageAuthority/AddCertificate" StorageAuthority_AddPrecertificate_FullMethodName = "/sa.StorageAuthority/AddPrecertificate" @@ -1390,26 +1294,27 @@ const ( StorageAuthority_RevokeCertificate_FullMethodName = "/sa.StorageAuthority/RevokeCertificate" StorageAuthority_SetOrderError_FullMethodName = "/sa.StorageAuthority/SetOrderError" StorageAuthority_SetOrderProcessing_FullMethodName = "/sa.StorageAuthority/SetOrderProcessing" - StorageAuthority_UpdateRegistration_FullMethodName = "/sa.StorageAuthority/UpdateRegistration" + StorageAuthority_UpdateRegistrationContact_FullMethodName = "/sa.StorageAuthority/UpdateRegistrationContact" + StorageAuthority_UpdateRegistrationKey_FullMethodName = "/sa.StorageAuthority/UpdateRegistrationKey" StorageAuthority_UpdateRevokedCertificate_FullMethodName = "/sa.StorageAuthority/UpdateRevokedCertificate" StorageAuthority_LeaseCRLShard_FullMethodName = "/sa.StorageAuthority/LeaseCRLShard" StorageAuthority_UpdateCRLShard_FullMethodName = "/sa.StorageAuthority/UpdateCRLShard" StorageAuthority_PauseIdentifiers_FullMethodName = "/sa.StorageAuthority/PauseIdentifiers" StorageAuthority_UnpauseAccount_FullMethodName = "/sa.StorageAuthority/UnpauseAccount" + StorageAuthority_AddRateLimitOverride_FullMethodName = "/sa.StorageAuthority/AddRateLimitOverride" + StorageAuthority_DisableRateLimitOverride_FullMethodName = "/sa.StorageAuthority/DisableRateLimitOverride" + StorageAuthority_EnableRateLimitOverride_FullMethodName = "/sa.StorageAuthority/EnableRateLimitOverride" ) // StorageAuthorityClient is the client API for StorageAuthority 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. +// +// StorageAuthority provides full read/write access to the database. type StorageAuthorityClient interface { // Getters: this list must be identical to the StorageAuthorityReadOnly rpcs. - CountCertificatesByNames(ctx context.Context, in *CountCertificatesByNamesRequest, opts ...grpc.CallOption) (*CountByNames, error) - CountFQDNSets(ctx context.Context, in *CountFQDNSetsRequest, opts ...grpc.CallOption) (*Count, error) CountInvalidAuthorizations2(ctx context.Context, in *CountInvalidAuthorizationsRequest, opts ...grpc.CallOption) (*Count, error) - CountOrders(ctx context.Context, in *CountOrdersRequest, opts ...grpc.CallOption) (*Count, error) CountPendingAuthorizations2(ctx context.Context, in *RegistrationID, opts ...grpc.CallOption) (*Count, error) - CountRegistrationsByIP(ctx context.Context, in *CountRegistrationsByIPRequest, opts ...grpc.CallOption) (*Count, error) - CountRegistrationsByIPRange(ctx context.Context, in *CountRegistrationsByIPRequest, opts ...grpc.CallOption) (*Count, error) FQDNSetExists(ctx context.Context, in *FQDNSetExistsRequest, opts ...grpc.CallOption) (*Exists, error) FQDNSetTimestampsForWindow(ctx context.Context, in *CountFQDNSetsRequest, opts ...grpc.CallOption) (*Timestamps, error) GetAuthorization2(ctx context.Context, in *AuthorizationID2, opts ...grpc.CallOption) (*proto.Authorization, error) @@ -1420,11 +1325,11 @@ type StorageAuthorityClient interface { GetMaxExpiration(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*timestamppb.Timestamp, error) GetOrder(ctx context.Context, in *OrderRequest, opts ...grpc.CallOption) (*proto.Order, error) GetOrderForNames(ctx context.Context, in *GetOrderForNamesRequest, opts ...grpc.CallOption) (*proto.Order, error) - GetPendingAuthorization2(ctx context.Context, in *GetPendingAuthorizationRequest, opts ...grpc.CallOption) (*proto.Authorization, error) GetRegistration(ctx context.Context, in *RegistrationID, opts ...grpc.CallOption) (*proto.Registration, error) GetRegistrationByKey(ctx context.Context, in *JSONWebKey, opts ...grpc.CallOption) (*proto.Registration, error) GetRevocationStatus(ctx context.Context, in *Serial, opts ...grpc.CallOption) (*RevocationStatus, error) GetRevokedCerts(ctx context.Context, in *GetRevokedCertsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[proto.CRLEntry], error) + GetRevokedCertsByShard(ctx context.Context, in *GetRevokedCertsByShardRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[proto.CRLEntry], error) GetSerialMetadata(ctx context.Context, in *Serial, opts ...grpc.CallOption) (*SerialMetadata, error) GetSerialsByAccount(ctx context.Context, in *RegistrationID, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Serial], error) GetSerialsByKey(ctx context.Context, in *SPKIHash, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Serial], error) @@ -1436,6 +1341,8 @@ type StorageAuthorityClient interface { SerialsForIncident(ctx context.Context, in *SerialsForIncidentRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[IncidentSerial], error) CheckIdentifiersPaused(ctx context.Context, in *PauseRequest, opts ...grpc.CallOption) (*Identifiers, error) GetPausedIdentifiers(ctx context.Context, in *RegistrationID, opts ...grpc.CallOption) (*Identifiers, error) + GetRateLimitOverride(ctx context.Context, in *GetRateLimitOverrideRequest, opts ...grpc.CallOption) (*RateLimitOverrideResponse, error) + GetEnabledRateLimitOverrides(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[RateLimitOverride], error) // Adders AddBlockedKey(ctx context.Context, in *AddBlockedKeyRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) AddCertificate(ctx context.Context, in *AddCertificateRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) @@ -1443,7 +1350,7 @@ type StorageAuthorityClient interface { SetCertificateStatusReady(ctx context.Context, in *Serial, opts ...grpc.CallOption) (*emptypb.Empty, error) AddSerial(ctx context.Context, in *AddSerialRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) DeactivateAuthorization2(ctx context.Context, in *AuthorizationID2, opts ...grpc.CallOption) (*emptypb.Empty, error) - DeactivateRegistration(ctx context.Context, in *RegistrationID, opts ...grpc.CallOption) (*emptypb.Empty, error) + DeactivateRegistration(ctx context.Context, in *RegistrationID, opts ...grpc.CallOption) (*proto.Registration, error) FinalizeAuthorization2(ctx context.Context, in *FinalizeAuthorizationRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) FinalizeOrder(ctx context.Context, in *FinalizeOrderRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) NewOrderAndAuthzs(ctx context.Context, in *NewOrderAndAuthzsRequest, opts ...grpc.CallOption) (*proto.Order, error) @@ -1451,12 +1358,16 @@ type StorageAuthorityClient interface { RevokeCertificate(ctx context.Context, in *RevokeCertificateRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) SetOrderError(ctx context.Context, in *SetOrderErrorRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) SetOrderProcessing(ctx context.Context, in *OrderRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) - UpdateRegistration(ctx context.Context, in *proto.Registration, opts ...grpc.CallOption) (*emptypb.Empty, error) + UpdateRegistrationContact(ctx context.Context, in *UpdateRegistrationContactRequest, opts ...grpc.CallOption) (*proto.Registration, error) + UpdateRegistrationKey(ctx context.Context, in *UpdateRegistrationKeyRequest, opts ...grpc.CallOption) (*proto.Registration, error) UpdateRevokedCertificate(ctx context.Context, in *RevokeCertificateRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) LeaseCRLShard(ctx context.Context, in *LeaseCRLShardRequest, opts ...grpc.CallOption) (*LeaseCRLShardResponse, error) UpdateCRLShard(ctx context.Context, in *UpdateCRLShardRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) PauseIdentifiers(ctx context.Context, in *PauseRequest, opts ...grpc.CallOption) (*PauseIdentifiersResponse, error) - UnpauseAccount(ctx context.Context, in *RegistrationID, opts ...grpc.CallOption) (*emptypb.Empty, error) + UnpauseAccount(ctx context.Context, in *RegistrationID, opts ...grpc.CallOption) (*Count, error) + AddRateLimitOverride(ctx context.Context, in *AddRateLimitOverrideRequest, opts ...grpc.CallOption) (*AddRateLimitOverrideResponse, error) + DisableRateLimitOverride(ctx context.Context, in *DisableRateLimitOverrideRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + EnableRateLimitOverride(ctx context.Context, in *EnableRateLimitOverrideRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) } type storageAuthorityClient struct { @@ -1467,26 +1378,6 @@ func NewStorageAuthorityClient(cc grpc.ClientConnInterface) StorageAuthorityClie return &storageAuthorityClient{cc} } -func (c *storageAuthorityClient) CountCertificatesByNames(ctx context.Context, in *CountCertificatesByNamesRequest, opts ...grpc.CallOption) (*CountByNames, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(CountByNames) - err := c.cc.Invoke(ctx, StorageAuthority_CountCertificatesByNames_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *storageAuthorityClient) CountFQDNSets(ctx context.Context, in *CountFQDNSetsRequest, opts ...grpc.CallOption) (*Count, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(Count) - err := c.cc.Invoke(ctx, StorageAuthority_CountFQDNSets_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - func (c *storageAuthorityClient) CountInvalidAuthorizations2(ctx context.Context, in *CountInvalidAuthorizationsRequest, opts ...grpc.CallOption) (*Count, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(Count) @@ -1497,16 +1388,6 @@ func (c *storageAuthorityClient) CountInvalidAuthorizations2(ctx context.Context return out, nil } -func (c *storageAuthorityClient) CountOrders(ctx context.Context, in *CountOrdersRequest, opts ...grpc.CallOption) (*Count, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(Count) - err := c.cc.Invoke(ctx, StorageAuthority_CountOrders_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - func (c *storageAuthorityClient) CountPendingAuthorizations2(ctx context.Context, in *RegistrationID, opts ...grpc.CallOption) (*Count, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(Count) @@ -1517,26 +1398,6 @@ func (c *storageAuthorityClient) CountPendingAuthorizations2(ctx context.Context return out, nil } -func (c *storageAuthorityClient) CountRegistrationsByIP(ctx context.Context, in *CountRegistrationsByIPRequest, opts ...grpc.CallOption) (*Count, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(Count) - err := c.cc.Invoke(ctx, StorageAuthority_CountRegistrationsByIP_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *storageAuthorityClient) CountRegistrationsByIPRange(ctx context.Context, in *CountRegistrationsByIPRequest, opts ...grpc.CallOption) (*Count, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(Count) - err := c.cc.Invoke(ctx, StorageAuthority_CountRegistrationsByIPRange_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - func (c *storageAuthorityClient) FQDNSetExists(ctx context.Context, in *FQDNSetExistsRequest, opts ...grpc.CallOption) (*Exists, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(Exists) @@ -1637,16 +1498,6 @@ func (c *storageAuthorityClient) GetOrderForNames(ctx context.Context, in *GetOr return out, nil } -func (c *storageAuthorityClient) GetPendingAuthorization2(ctx context.Context, in *GetPendingAuthorizationRequest, opts ...grpc.CallOption) (*proto.Authorization, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(proto.Authorization) - err := c.cc.Invoke(ctx, StorageAuthority_GetPendingAuthorization2_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - func (c *storageAuthorityClient) GetRegistration(ctx context.Context, in *RegistrationID, opts ...grpc.CallOption) (*proto.Registration, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(proto.Registration) @@ -1696,6 +1547,25 @@ func (c *storageAuthorityClient) GetRevokedCerts(ctx context.Context, in *GetRev // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. type StorageAuthority_GetRevokedCertsClient = grpc.ServerStreamingClient[proto.CRLEntry] +func (c *storageAuthorityClient) GetRevokedCertsByShard(ctx context.Context, in *GetRevokedCertsByShardRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[proto.CRLEntry], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &StorageAuthority_ServiceDesc.Streams[1], StorageAuthority_GetRevokedCertsByShard_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[GetRevokedCertsByShardRequest, proto.CRLEntry]{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type StorageAuthority_GetRevokedCertsByShardClient = grpc.ServerStreamingClient[proto.CRLEntry] + func (c *storageAuthorityClient) GetSerialMetadata(ctx context.Context, in *Serial, opts ...grpc.CallOption) (*SerialMetadata, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(SerialMetadata) @@ -1708,7 +1578,7 @@ func (c *storageAuthorityClient) GetSerialMetadata(ctx context.Context, in *Seri func (c *storageAuthorityClient) GetSerialsByAccount(ctx context.Context, in *RegistrationID, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Serial], error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - stream, err := c.cc.NewStream(ctx, &StorageAuthority_ServiceDesc.Streams[1], StorageAuthority_GetSerialsByAccount_FullMethodName, cOpts...) + stream, err := c.cc.NewStream(ctx, &StorageAuthority_ServiceDesc.Streams[2], StorageAuthority_GetSerialsByAccount_FullMethodName, cOpts...) if err != nil { return nil, err } @@ -1727,7 +1597,7 @@ type StorageAuthority_GetSerialsByAccountClient = grpc.ServerStreamingClient[Ser func (c *storageAuthorityClient) GetSerialsByKey(ctx context.Context, in *SPKIHash, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Serial], error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - stream, err := c.cc.NewStream(ctx, &StorageAuthority_ServiceDesc.Streams[2], StorageAuthority_GetSerialsByKey_FullMethodName, cOpts...) + stream, err := c.cc.NewStream(ctx, &StorageAuthority_ServiceDesc.Streams[3], StorageAuthority_GetSerialsByKey_FullMethodName, cOpts...) if err != nil { return nil, err } @@ -1796,7 +1666,7 @@ func (c *storageAuthorityClient) ReplacementOrderExists(ctx context.Context, in func (c *storageAuthorityClient) SerialsForIncident(ctx context.Context, in *SerialsForIncidentRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[IncidentSerial], error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - stream, err := c.cc.NewStream(ctx, &StorageAuthority_ServiceDesc.Streams[3], StorageAuthority_SerialsForIncident_FullMethodName, cOpts...) + stream, err := c.cc.NewStream(ctx, &StorageAuthority_ServiceDesc.Streams[4], StorageAuthority_SerialsForIncident_FullMethodName, cOpts...) if err != nil { return nil, err } @@ -1833,6 +1703,35 @@ func (c *storageAuthorityClient) GetPausedIdentifiers(ctx context.Context, in *R return out, nil } +func (c *storageAuthorityClient) GetRateLimitOverride(ctx context.Context, in *GetRateLimitOverrideRequest, opts ...grpc.CallOption) (*RateLimitOverrideResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RateLimitOverrideResponse) + err := c.cc.Invoke(ctx, StorageAuthority_GetRateLimitOverride_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *storageAuthorityClient) GetEnabledRateLimitOverrides(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[RateLimitOverride], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &StorageAuthority_ServiceDesc.Streams[5], StorageAuthority_GetEnabledRateLimitOverrides_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[emptypb.Empty, RateLimitOverride]{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type StorageAuthority_GetEnabledRateLimitOverridesClient = grpc.ServerStreamingClient[RateLimitOverride] + func (c *storageAuthorityClient) AddBlockedKey(ctx context.Context, in *AddBlockedKeyRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(emptypb.Empty) @@ -1893,9 +1792,9 @@ func (c *storageAuthorityClient) DeactivateAuthorization2(ctx context.Context, i return out, nil } -func (c *storageAuthorityClient) DeactivateRegistration(ctx context.Context, in *RegistrationID, opts ...grpc.CallOption) (*emptypb.Empty, error) { +func (c *storageAuthorityClient) DeactivateRegistration(ctx context.Context, in *RegistrationID, opts ...grpc.CallOption) (*proto.Registration, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(emptypb.Empty) + out := new(proto.Registration) err := c.cc.Invoke(ctx, StorageAuthority_DeactivateRegistration_FullMethodName, in, out, cOpts...) if err != nil { return nil, err @@ -1973,10 +1872,20 @@ func (c *storageAuthorityClient) SetOrderProcessing(ctx context.Context, in *Ord return out, nil } -func (c *storageAuthorityClient) UpdateRegistration(ctx context.Context, in *proto.Registration, opts ...grpc.CallOption) (*emptypb.Empty, error) { +func (c *storageAuthorityClient) UpdateRegistrationContact(ctx context.Context, in *UpdateRegistrationContactRequest, opts ...grpc.CallOption) (*proto.Registration, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(emptypb.Empty) - err := c.cc.Invoke(ctx, StorageAuthority_UpdateRegistration_FullMethodName, in, out, cOpts...) + out := new(proto.Registration) + err := c.cc.Invoke(ctx, StorageAuthority_UpdateRegistrationContact_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *storageAuthorityClient) UpdateRegistrationKey(ctx context.Context, in *UpdateRegistrationKeyRequest, opts ...grpc.CallOption) (*proto.Registration, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(proto.Registration) + err := c.cc.Invoke(ctx, StorageAuthority_UpdateRegistrationKey_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -2023,10 +1932,40 @@ func (c *storageAuthorityClient) PauseIdentifiers(ctx context.Context, in *Pause return out, nil } -func (c *storageAuthorityClient) UnpauseAccount(ctx context.Context, in *RegistrationID, opts ...grpc.CallOption) (*emptypb.Empty, error) { +func (c *storageAuthorityClient) UnpauseAccount(ctx context.Context, in *RegistrationID, opts ...grpc.CallOption) (*Count, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Count) + err := c.cc.Invoke(ctx, StorageAuthority_UnpauseAccount_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *storageAuthorityClient) AddRateLimitOverride(ctx context.Context, in *AddRateLimitOverrideRequest, opts ...grpc.CallOption) (*AddRateLimitOverrideResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(AddRateLimitOverrideResponse) + err := c.cc.Invoke(ctx, StorageAuthority_AddRateLimitOverride_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *storageAuthorityClient) DisableRateLimitOverride(ctx context.Context, in *DisableRateLimitOverrideRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(emptypb.Empty) - err := c.cc.Invoke(ctx, StorageAuthority_UnpauseAccount_FullMethodName, in, out, cOpts...) + err := c.cc.Invoke(ctx, StorageAuthority_DisableRateLimitOverride_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *storageAuthorityClient) EnableRateLimitOverride(ctx context.Context, in *EnableRateLimitOverrideRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, StorageAuthority_EnableRateLimitOverride_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -2035,16 +1974,13 @@ func (c *storageAuthorityClient) UnpauseAccount(ctx context.Context, in *Registr // StorageAuthorityServer is the server API for StorageAuthority service. // All implementations must embed UnimplementedStorageAuthorityServer -// for forward compatibility +// for forward compatibility. +// +// StorageAuthority provides full read/write access to the database. type StorageAuthorityServer interface { // Getters: this list must be identical to the StorageAuthorityReadOnly rpcs. - CountCertificatesByNames(context.Context, *CountCertificatesByNamesRequest) (*CountByNames, error) - CountFQDNSets(context.Context, *CountFQDNSetsRequest) (*Count, error) CountInvalidAuthorizations2(context.Context, *CountInvalidAuthorizationsRequest) (*Count, error) - CountOrders(context.Context, *CountOrdersRequest) (*Count, error) CountPendingAuthorizations2(context.Context, *RegistrationID) (*Count, error) - CountRegistrationsByIP(context.Context, *CountRegistrationsByIPRequest) (*Count, error) - CountRegistrationsByIPRange(context.Context, *CountRegistrationsByIPRequest) (*Count, error) FQDNSetExists(context.Context, *FQDNSetExistsRequest) (*Exists, error) FQDNSetTimestampsForWindow(context.Context, *CountFQDNSetsRequest) (*Timestamps, error) GetAuthorization2(context.Context, *AuthorizationID2) (*proto.Authorization, error) @@ -2055,11 +1991,11 @@ type StorageAuthorityServer interface { GetMaxExpiration(context.Context, *emptypb.Empty) (*timestamppb.Timestamp, error) GetOrder(context.Context, *OrderRequest) (*proto.Order, error) GetOrderForNames(context.Context, *GetOrderForNamesRequest) (*proto.Order, error) - GetPendingAuthorization2(context.Context, *GetPendingAuthorizationRequest) (*proto.Authorization, error) GetRegistration(context.Context, *RegistrationID) (*proto.Registration, error) GetRegistrationByKey(context.Context, *JSONWebKey) (*proto.Registration, error) GetRevocationStatus(context.Context, *Serial) (*RevocationStatus, error) GetRevokedCerts(*GetRevokedCertsRequest, grpc.ServerStreamingServer[proto.CRLEntry]) error + GetRevokedCertsByShard(*GetRevokedCertsByShardRequest, grpc.ServerStreamingServer[proto.CRLEntry]) error GetSerialMetadata(context.Context, *Serial) (*SerialMetadata, error) GetSerialsByAccount(*RegistrationID, grpc.ServerStreamingServer[Serial]) error GetSerialsByKey(*SPKIHash, grpc.ServerStreamingServer[Serial]) error @@ -2071,6 +2007,8 @@ type StorageAuthorityServer interface { SerialsForIncident(*SerialsForIncidentRequest, grpc.ServerStreamingServer[IncidentSerial]) error CheckIdentifiersPaused(context.Context, *PauseRequest) (*Identifiers, error) GetPausedIdentifiers(context.Context, *RegistrationID) (*Identifiers, error) + GetRateLimitOverride(context.Context, *GetRateLimitOverrideRequest) (*RateLimitOverrideResponse, error) + GetEnabledRateLimitOverrides(*emptypb.Empty, grpc.ServerStreamingServer[RateLimitOverride]) error // Adders AddBlockedKey(context.Context, *AddBlockedKeyRequest) (*emptypb.Empty, error) AddCertificate(context.Context, *AddCertificateRequest) (*emptypb.Empty, error) @@ -2078,7 +2016,7 @@ type StorageAuthorityServer interface { SetCertificateStatusReady(context.Context, *Serial) (*emptypb.Empty, error) AddSerial(context.Context, *AddSerialRequest) (*emptypb.Empty, error) DeactivateAuthorization2(context.Context, *AuthorizationID2) (*emptypb.Empty, error) - DeactivateRegistration(context.Context, *RegistrationID) (*emptypb.Empty, error) + DeactivateRegistration(context.Context, *RegistrationID) (*proto.Registration, error) FinalizeAuthorization2(context.Context, *FinalizeAuthorizationRequest) (*emptypb.Empty, error) FinalizeOrder(context.Context, *FinalizeOrderRequest) (*emptypb.Empty, error) NewOrderAndAuthzs(context.Context, *NewOrderAndAuthzsRequest) (*proto.Order, error) @@ -2086,40 +2024,32 @@ type StorageAuthorityServer interface { RevokeCertificate(context.Context, *RevokeCertificateRequest) (*emptypb.Empty, error) SetOrderError(context.Context, *SetOrderErrorRequest) (*emptypb.Empty, error) SetOrderProcessing(context.Context, *OrderRequest) (*emptypb.Empty, error) - UpdateRegistration(context.Context, *proto.Registration) (*emptypb.Empty, error) + UpdateRegistrationContact(context.Context, *UpdateRegistrationContactRequest) (*proto.Registration, error) + UpdateRegistrationKey(context.Context, *UpdateRegistrationKeyRequest) (*proto.Registration, error) UpdateRevokedCertificate(context.Context, *RevokeCertificateRequest) (*emptypb.Empty, error) LeaseCRLShard(context.Context, *LeaseCRLShardRequest) (*LeaseCRLShardResponse, error) UpdateCRLShard(context.Context, *UpdateCRLShardRequest) (*emptypb.Empty, error) PauseIdentifiers(context.Context, *PauseRequest) (*PauseIdentifiersResponse, error) - UnpauseAccount(context.Context, *RegistrationID) (*emptypb.Empty, error) + UnpauseAccount(context.Context, *RegistrationID) (*Count, error) + AddRateLimitOverride(context.Context, *AddRateLimitOverrideRequest) (*AddRateLimitOverrideResponse, error) + DisableRateLimitOverride(context.Context, *DisableRateLimitOverrideRequest) (*emptypb.Empty, error) + EnableRateLimitOverride(context.Context, *EnableRateLimitOverrideRequest) (*emptypb.Empty, error) mustEmbedUnimplementedStorageAuthorityServer() } -// UnimplementedStorageAuthorityServer must be embedded to have forward compatible implementations. -type UnimplementedStorageAuthorityServer struct { -} +// UnimplementedStorageAuthorityServer 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 UnimplementedStorageAuthorityServer struct{} -func (UnimplementedStorageAuthorityServer) CountCertificatesByNames(context.Context, *CountCertificatesByNamesRequest) (*CountByNames, error) { - return nil, status.Errorf(codes.Unimplemented, "method CountCertificatesByNames not implemented") -} -func (UnimplementedStorageAuthorityServer) CountFQDNSets(context.Context, *CountFQDNSetsRequest) (*Count, error) { - return nil, status.Errorf(codes.Unimplemented, "method CountFQDNSets not implemented") -} func (UnimplementedStorageAuthorityServer) CountInvalidAuthorizations2(context.Context, *CountInvalidAuthorizationsRequest) (*Count, error) { return nil, status.Errorf(codes.Unimplemented, "method CountInvalidAuthorizations2 not implemented") } -func (UnimplementedStorageAuthorityServer) CountOrders(context.Context, *CountOrdersRequest) (*Count, error) { - return nil, status.Errorf(codes.Unimplemented, "method CountOrders not implemented") -} func (UnimplementedStorageAuthorityServer) CountPendingAuthorizations2(context.Context, *RegistrationID) (*Count, error) { return nil, status.Errorf(codes.Unimplemented, "method CountPendingAuthorizations2 not implemented") } -func (UnimplementedStorageAuthorityServer) CountRegistrationsByIP(context.Context, *CountRegistrationsByIPRequest) (*Count, error) { - return nil, status.Errorf(codes.Unimplemented, "method CountRegistrationsByIP not implemented") -} -func (UnimplementedStorageAuthorityServer) CountRegistrationsByIPRange(context.Context, *CountRegistrationsByIPRequest) (*Count, error) { - return nil, status.Errorf(codes.Unimplemented, "method CountRegistrationsByIPRange not implemented") -} func (UnimplementedStorageAuthorityServer) FQDNSetExists(context.Context, *FQDNSetExistsRequest) (*Exists, error) { return nil, status.Errorf(codes.Unimplemented, "method FQDNSetExists not implemented") } @@ -2150,9 +2080,6 @@ func (UnimplementedStorageAuthorityServer) GetOrder(context.Context, *OrderReque func (UnimplementedStorageAuthorityServer) GetOrderForNames(context.Context, *GetOrderForNamesRequest) (*proto.Order, error) { return nil, status.Errorf(codes.Unimplemented, "method GetOrderForNames not implemented") } -func (UnimplementedStorageAuthorityServer) GetPendingAuthorization2(context.Context, *GetPendingAuthorizationRequest) (*proto.Authorization, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetPendingAuthorization2 not implemented") -} func (UnimplementedStorageAuthorityServer) GetRegistration(context.Context, *RegistrationID) (*proto.Registration, error) { return nil, status.Errorf(codes.Unimplemented, "method GetRegistration not implemented") } @@ -2165,6 +2092,9 @@ func (UnimplementedStorageAuthorityServer) GetRevocationStatus(context.Context, func (UnimplementedStorageAuthorityServer) GetRevokedCerts(*GetRevokedCertsRequest, grpc.ServerStreamingServer[proto.CRLEntry]) error { return status.Errorf(codes.Unimplemented, "method GetRevokedCerts not implemented") } +func (UnimplementedStorageAuthorityServer) GetRevokedCertsByShard(*GetRevokedCertsByShardRequest, grpc.ServerStreamingServer[proto.CRLEntry]) error { + return status.Errorf(codes.Unimplemented, "method GetRevokedCertsByShard not implemented") +} func (UnimplementedStorageAuthorityServer) GetSerialMetadata(context.Context, *Serial) (*SerialMetadata, error) { return nil, status.Errorf(codes.Unimplemented, "method GetSerialMetadata not implemented") } @@ -2198,6 +2128,12 @@ func (UnimplementedStorageAuthorityServer) CheckIdentifiersPaused(context.Contex func (UnimplementedStorageAuthorityServer) GetPausedIdentifiers(context.Context, *RegistrationID) (*Identifiers, error) { return nil, status.Errorf(codes.Unimplemented, "method GetPausedIdentifiers not implemented") } +func (UnimplementedStorageAuthorityServer) GetRateLimitOverride(context.Context, *GetRateLimitOverrideRequest) (*RateLimitOverrideResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetRateLimitOverride not implemented") +} +func (UnimplementedStorageAuthorityServer) GetEnabledRateLimitOverrides(*emptypb.Empty, grpc.ServerStreamingServer[RateLimitOverride]) error { + return status.Errorf(codes.Unimplemented, "method GetEnabledRateLimitOverrides not implemented") +} func (UnimplementedStorageAuthorityServer) AddBlockedKey(context.Context, *AddBlockedKeyRequest) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method AddBlockedKey not implemented") } @@ -2216,7 +2152,7 @@ func (UnimplementedStorageAuthorityServer) AddSerial(context.Context, *AddSerial func (UnimplementedStorageAuthorityServer) DeactivateAuthorization2(context.Context, *AuthorizationID2) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method DeactivateAuthorization2 not implemented") } -func (UnimplementedStorageAuthorityServer) DeactivateRegistration(context.Context, *RegistrationID) (*emptypb.Empty, error) { +func (UnimplementedStorageAuthorityServer) DeactivateRegistration(context.Context, *RegistrationID) (*proto.Registration, error) { return nil, status.Errorf(codes.Unimplemented, "method DeactivateRegistration not implemented") } func (UnimplementedStorageAuthorityServer) FinalizeAuthorization2(context.Context, *FinalizeAuthorizationRequest) (*emptypb.Empty, error) { @@ -2240,8 +2176,11 @@ func (UnimplementedStorageAuthorityServer) SetOrderError(context.Context, *SetOr func (UnimplementedStorageAuthorityServer) SetOrderProcessing(context.Context, *OrderRequest) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method SetOrderProcessing not implemented") } -func (UnimplementedStorageAuthorityServer) UpdateRegistration(context.Context, *proto.Registration) (*emptypb.Empty, error) { - return nil, status.Errorf(codes.Unimplemented, "method UpdateRegistration not implemented") +func (UnimplementedStorageAuthorityServer) UpdateRegistrationContact(context.Context, *UpdateRegistrationContactRequest) (*proto.Registration, error) { + return nil, status.Errorf(codes.Unimplemented, "method UpdateRegistrationContact not implemented") +} +func (UnimplementedStorageAuthorityServer) UpdateRegistrationKey(context.Context, *UpdateRegistrationKeyRequest) (*proto.Registration, error) { + return nil, status.Errorf(codes.Unimplemented, "method UpdateRegistrationKey not implemented") } func (UnimplementedStorageAuthorityServer) UpdateRevokedCertificate(context.Context, *RevokeCertificateRequest) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method UpdateRevokedCertificate not implemented") @@ -2255,10 +2194,20 @@ func (UnimplementedStorageAuthorityServer) UpdateCRLShard(context.Context, *Upda func (UnimplementedStorageAuthorityServer) PauseIdentifiers(context.Context, *PauseRequest) (*PauseIdentifiersResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method PauseIdentifiers not implemented") } -func (UnimplementedStorageAuthorityServer) UnpauseAccount(context.Context, *RegistrationID) (*emptypb.Empty, error) { +func (UnimplementedStorageAuthorityServer) UnpauseAccount(context.Context, *RegistrationID) (*Count, error) { return nil, status.Errorf(codes.Unimplemented, "method UnpauseAccount not implemented") } +func (UnimplementedStorageAuthorityServer) AddRateLimitOverride(context.Context, *AddRateLimitOverrideRequest) (*AddRateLimitOverrideResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method AddRateLimitOverride not implemented") +} +func (UnimplementedStorageAuthorityServer) DisableRateLimitOverride(context.Context, *DisableRateLimitOverrideRequest) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method DisableRateLimitOverride not implemented") +} +func (UnimplementedStorageAuthorityServer) EnableRateLimitOverride(context.Context, *EnableRateLimitOverrideRequest) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method EnableRateLimitOverride not implemented") +} func (UnimplementedStorageAuthorityServer) mustEmbedUnimplementedStorageAuthorityServer() {} +func (UnimplementedStorageAuthorityServer) testEmbeddedByValue() {} // UnsafeStorageAuthorityServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to StorageAuthorityServer will @@ -2268,45 +2217,16 @@ type UnsafeStorageAuthorityServer interface { } func RegisterStorageAuthorityServer(s grpc.ServiceRegistrar, srv StorageAuthorityServer) { + // If the following call pancis, it indicates UnimplementedStorageAuthorityServer 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(&StorageAuthority_ServiceDesc, srv) } -func _StorageAuthority_CountCertificatesByNames_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(CountCertificatesByNamesRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(StorageAuthorityServer).CountCertificatesByNames(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: StorageAuthority_CountCertificatesByNames_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(StorageAuthorityServer).CountCertificatesByNames(ctx, req.(*CountCertificatesByNamesRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _StorageAuthority_CountFQDNSets_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(CountFQDNSetsRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(StorageAuthorityServer).CountFQDNSets(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: StorageAuthority_CountFQDNSets_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(StorageAuthorityServer).CountFQDNSets(ctx, req.(*CountFQDNSetsRequest)) - } - return interceptor(ctx, in, info, handler) -} - func _StorageAuthority_CountInvalidAuthorizations2_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(CountInvalidAuthorizationsRequest) if err := dec(in); err != nil { @@ -2325,24 +2245,6 @@ func _StorageAuthority_CountInvalidAuthorizations2_Handler(srv interface{}, ctx return interceptor(ctx, in, info, handler) } -func _StorageAuthority_CountOrders_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(CountOrdersRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(StorageAuthorityServer).CountOrders(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: StorageAuthority_CountOrders_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(StorageAuthorityServer).CountOrders(ctx, req.(*CountOrdersRequest)) - } - return interceptor(ctx, in, info, handler) -} - func _StorageAuthority_CountPendingAuthorizations2_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(RegistrationID) if err := dec(in); err != nil { @@ -2361,42 +2263,6 @@ func _StorageAuthority_CountPendingAuthorizations2_Handler(srv interface{}, ctx return interceptor(ctx, in, info, handler) } -func _StorageAuthority_CountRegistrationsByIP_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(CountRegistrationsByIPRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(StorageAuthorityServer).CountRegistrationsByIP(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: StorageAuthority_CountRegistrationsByIP_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(StorageAuthorityServer).CountRegistrationsByIP(ctx, req.(*CountRegistrationsByIPRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _StorageAuthority_CountRegistrationsByIPRange_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(CountRegistrationsByIPRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(StorageAuthorityServer).CountRegistrationsByIPRange(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: StorageAuthority_CountRegistrationsByIPRange_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(StorageAuthorityServer).CountRegistrationsByIPRange(ctx, req.(*CountRegistrationsByIPRequest)) - } - return interceptor(ctx, in, info, handler) -} - func _StorageAuthority_FQDNSetExists_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(FQDNSetExistsRequest) if err := dec(in); err != nil { @@ -2577,24 +2443,6 @@ func _StorageAuthority_GetOrderForNames_Handler(srv interface{}, ctx context.Con return interceptor(ctx, in, info, handler) } -func _StorageAuthority_GetPendingAuthorization2_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(GetPendingAuthorizationRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(StorageAuthorityServer).GetPendingAuthorization2(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: StorageAuthority_GetPendingAuthorization2_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(StorageAuthorityServer).GetPendingAuthorization2(ctx, req.(*GetPendingAuthorizationRequest)) - } - return interceptor(ctx, in, info, handler) -} - func _StorageAuthority_GetRegistration_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(RegistrationID) if err := dec(in); err != nil { @@ -2660,6 +2508,17 @@ func _StorageAuthority_GetRevokedCerts_Handler(srv interface{}, stream grpc.Serv // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. type StorageAuthority_GetRevokedCertsServer = grpc.ServerStreamingServer[proto.CRLEntry] +func _StorageAuthority_GetRevokedCertsByShard_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(GetRevokedCertsByShardRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(StorageAuthorityServer).GetRevokedCertsByShard(m, &grpc.GenericServerStream[GetRevokedCertsByShardRequest, proto.CRLEntry]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type StorageAuthority_GetRevokedCertsByShardServer = grpc.ServerStreamingServer[proto.CRLEntry] + func _StorageAuthority_GetSerialMetadata_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(Serial) if err := dec(in); err != nil { @@ -2837,6 +2696,35 @@ func _StorageAuthority_GetPausedIdentifiers_Handler(srv interface{}, ctx context return interceptor(ctx, in, info, handler) } +func _StorageAuthority_GetRateLimitOverride_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetRateLimitOverrideRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StorageAuthorityServer).GetRateLimitOverride(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StorageAuthority_GetRateLimitOverride_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StorageAuthorityServer).GetRateLimitOverride(ctx, req.(*GetRateLimitOverrideRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _StorageAuthority_GetEnabledRateLimitOverrides_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(emptypb.Empty) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(StorageAuthorityServer).GetEnabledRateLimitOverrides(m, &grpc.GenericServerStream[emptypb.Empty, RateLimitOverride]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type StorageAuthority_GetEnabledRateLimitOverridesServer = grpc.ServerStreamingServer[RateLimitOverride] + func _StorageAuthority_AddBlockedKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(AddBlockedKeyRequest) if err := dec(in); err != nil { @@ -3089,20 +2977,38 @@ func _StorageAuthority_SetOrderProcessing_Handler(srv interface{}, ctx context.C return interceptor(ctx, in, info, handler) } -func _StorageAuthority_UpdateRegistration_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(proto.Registration) +func _StorageAuthority_UpdateRegistrationContact_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateRegistrationContactRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { - return srv.(StorageAuthorityServer).UpdateRegistration(ctx, in) + return srv.(StorageAuthorityServer).UpdateRegistrationContact(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: StorageAuthority_UpdateRegistration_FullMethodName, + FullMethod: StorageAuthority_UpdateRegistrationContact_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(StorageAuthorityServer).UpdateRegistration(ctx, req.(*proto.Registration)) + return srv.(StorageAuthorityServer).UpdateRegistrationContact(ctx, req.(*UpdateRegistrationContactRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _StorageAuthority_UpdateRegistrationKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateRegistrationKeyRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StorageAuthorityServer).UpdateRegistrationKey(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StorageAuthority_UpdateRegistrationKey_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StorageAuthorityServer).UpdateRegistrationKey(ctx, req.(*UpdateRegistrationKeyRequest)) } return interceptor(ctx, in, info, handler) } @@ -3197,6 +3103,60 @@ func _StorageAuthority_UnpauseAccount_Handler(srv interface{}, ctx context.Conte return interceptor(ctx, in, info, handler) } +func _StorageAuthority_AddRateLimitOverride_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AddRateLimitOverrideRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StorageAuthorityServer).AddRateLimitOverride(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StorageAuthority_AddRateLimitOverride_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StorageAuthorityServer).AddRateLimitOverride(ctx, req.(*AddRateLimitOverrideRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _StorageAuthority_DisableRateLimitOverride_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DisableRateLimitOverrideRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StorageAuthorityServer).DisableRateLimitOverride(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StorageAuthority_DisableRateLimitOverride_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StorageAuthorityServer).DisableRateLimitOverride(ctx, req.(*DisableRateLimitOverrideRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _StorageAuthority_EnableRateLimitOverride_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(EnableRateLimitOverrideRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StorageAuthorityServer).EnableRateLimitOverride(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StorageAuthority_EnableRateLimitOverride_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StorageAuthorityServer).EnableRateLimitOverride(ctx, req.(*EnableRateLimitOverrideRequest)) + } + return interceptor(ctx, in, info, handler) +} + // StorageAuthority_ServiceDesc is the grpc.ServiceDesc for StorageAuthority service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -3204,34 +3164,14 @@ var StorageAuthority_ServiceDesc = grpc.ServiceDesc{ ServiceName: "sa.StorageAuthority", HandlerType: (*StorageAuthorityServer)(nil), Methods: []grpc.MethodDesc{ - { - MethodName: "CountCertificatesByNames", - Handler: _StorageAuthority_CountCertificatesByNames_Handler, - }, - { - MethodName: "CountFQDNSets", - Handler: _StorageAuthority_CountFQDNSets_Handler, - }, { MethodName: "CountInvalidAuthorizations2", Handler: _StorageAuthority_CountInvalidAuthorizations2_Handler, }, - { - MethodName: "CountOrders", - Handler: _StorageAuthority_CountOrders_Handler, - }, { MethodName: "CountPendingAuthorizations2", Handler: _StorageAuthority_CountPendingAuthorizations2_Handler, }, - { - MethodName: "CountRegistrationsByIP", - Handler: _StorageAuthority_CountRegistrationsByIP_Handler, - }, - { - MethodName: "CountRegistrationsByIPRange", - Handler: _StorageAuthority_CountRegistrationsByIPRange_Handler, - }, { MethodName: "FQDNSetExists", Handler: _StorageAuthority_FQDNSetExists_Handler, @@ -3272,10 +3212,6 @@ var StorageAuthority_ServiceDesc = grpc.ServiceDesc{ MethodName: "GetOrderForNames", Handler: _StorageAuthority_GetOrderForNames_Handler, }, - { - MethodName: "GetPendingAuthorization2", - Handler: _StorageAuthority_GetPendingAuthorization2_Handler, - }, { MethodName: "GetRegistration", Handler: _StorageAuthority_GetRegistration_Handler, @@ -3320,6 +3256,10 @@ var StorageAuthority_ServiceDesc = grpc.ServiceDesc{ MethodName: "GetPausedIdentifiers", Handler: _StorageAuthority_GetPausedIdentifiers_Handler, }, + { + MethodName: "GetRateLimitOverride", + Handler: _StorageAuthority_GetRateLimitOverride_Handler, + }, { MethodName: "AddBlockedKey", Handler: _StorageAuthority_AddBlockedKey_Handler, @@ -3377,8 +3317,12 @@ var StorageAuthority_ServiceDesc = grpc.ServiceDesc{ Handler: _StorageAuthority_SetOrderProcessing_Handler, }, { - MethodName: "UpdateRegistration", - Handler: _StorageAuthority_UpdateRegistration_Handler, + MethodName: "UpdateRegistrationContact", + Handler: _StorageAuthority_UpdateRegistrationContact_Handler, + }, + { + MethodName: "UpdateRegistrationKey", + Handler: _StorageAuthority_UpdateRegistrationKey_Handler, }, { MethodName: "UpdateRevokedCertificate", @@ -3400,6 +3344,18 @@ var StorageAuthority_ServiceDesc = grpc.ServiceDesc{ MethodName: "UnpauseAccount", Handler: _StorageAuthority_UnpauseAccount_Handler, }, + { + MethodName: "AddRateLimitOverride", + Handler: _StorageAuthority_AddRateLimitOverride_Handler, + }, + { + MethodName: "DisableRateLimitOverride", + Handler: _StorageAuthority_DisableRateLimitOverride_Handler, + }, + { + MethodName: "EnableRateLimitOverride", + Handler: _StorageAuthority_EnableRateLimitOverride_Handler, + }, }, Streams: []grpc.StreamDesc{ { @@ -3407,6 +3363,11 @@ var StorageAuthority_ServiceDesc = grpc.ServiceDesc{ Handler: _StorageAuthority_GetRevokedCerts_Handler, ServerStreams: true, }, + { + StreamName: "GetRevokedCertsByShard", + Handler: _StorageAuthority_GetRevokedCertsByShard_Handler, + ServerStreams: true, + }, { StreamName: "GetSerialsByAccount", Handler: _StorageAuthority_GetSerialsByAccount_Handler, @@ -3422,6 +3383,11 @@ var StorageAuthority_ServiceDesc = grpc.ServiceDesc{ Handler: _StorageAuthority_SerialsForIncident_Handler, ServerStreams: true, }, + { + StreamName: "GetEnabledRateLimitOverrides", + Handler: _StorageAuthority_GetEnabledRateLimitOverrides_Handler, + ServerStreams: true, + }, }, Metadata: "sa.proto", } diff --git a/third-party/github.com/letsencrypt/boulder/sa/rate_limits.go b/third-party/github.com/letsencrypt/boulder/sa/rate_limits.go deleted file mode 100644 index 7fb3fa9b5..000000000 --- a/third-party/github.com/letsencrypt/boulder/sa/rate_limits.go +++ /dev/null @@ -1,146 +0,0 @@ -package sa - -import ( - "context" - "strings" - "time" - - "github.com/letsencrypt/boulder/db" - sapb "github.com/letsencrypt/boulder/sa/proto" - "github.com/weppos/publicsuffix-go/publicsuffix" -) - -// baseDomain returns the eTLD+1 of a domain name for the purpose of rate -// limiting. For a domain name that is itself an eTLD, it returns its input. -func baseDomain(name string) string { - eTLDPlusOne, err := publicsuffix.Domain(name) - if err != nil { - // publicsuffix.Domain will return an error if the input name is itself a - // public suffix. In that case we use the input name as the key for rate - // limiting. Since all of its subdomains will have separate keys for rate - // limiting (e.g. "foo.bar.publicsuffix.com" will have - // "bar.publicsuffix.com", this means that domains exactly equal to a - // public suffix get their own rate limit bucket. This is important - // because otherwise they might be perpetually unable to issue, assuming - // the rate of issuance from their subdomains was high enough. - return name - } - return eTLDPlusOne -} - -// addCertificatesPerName adds 1 to the rate limit count for the provided -// domains, in a specific time bucket. It must be executed in a transaction, and -// the input timeToTheHour must be a time rounded to an hour. -func (ssa *SQLStorageAuthority) addCertificatesPerName(ctx context.Context, db db.SelectExecer, names []string, timeToTheHour time.Time) error { - // De-duplicate the base domains. - baseDomainsMap := make(map[string]bool) - var qmarks []string - var values []interface{} - for _, name := range names { - base := baseDomain(name) - if !baseDomainsMap[base] { - baseDomainsMap[base] = true - values = append(values, base, timeToTheHour, 1) - qmarks = append(qmarks, "(?, ?, ?)") - } - } - - _, err := db.ExecContext(ctx, `INSERT INTO certificatesPerName (eTLDPlusOne, time, count) VALUES `+ - strings.Join(qmarks, ", ")+` ON DUPLICATE KEY UPDATE count=count+1;`, - values...) - if err != nil { - return err - } - - return nil -} - -// countCertificates returns the count of certificates issued for a domain's -// eTLD+1 (aka base domain), during a given time range. -func (ssa *SQLStorageAuthorityRO) countCertificates(ctx context.Context, dbMap db.Selector, domain string, timeRange *sapb.Range) (int64, time.Time, error) { - latest := timeRange.Latest.AsTime() - var results []struct { - Count int64 - Time time.Time - } - _, err := dbMap.Select( - ctx, - &results, - `SELECT count, time FROM certificatesPerName - WHERE eTLDPlusOne = :baseDomain AND - time > :earliest AND - time <= :latest`, - map[string]interface{}{ - "baseDomain": baseDomain(domain), - "earliest": timeRange.Earliest.AsTime(), - "latest": latest, - }) - if err != nil { - if db.IsNoRows(err) { - return 0, time.Time{}, nil - } - return 0, time.Time{}, err - } - // Set earliest to the latest possible time, so that we can find the - // earliest certificate in the results. - var earliest = latest - var total int64 - for _, r := range results { - total += r.Count - if r.Time.Before(earliest) { - earliest = r.Time - } - } - if total <= 0 && earliest == latest { - // If we didn't find any certificates, return a zero time. - return total, time.Time{}, nil - } - return total, earliest, nil -} - -// addNewOrdersRateLimit adds 1 to the rate limit count for the provided ID, in -// a specific time bucket. It must be executed in a transaction, and the input -// timeToTheMinute must be a time rounded to a minute. -func addNewOrdersRateLimit(ctx context.Context, dbMap db.SelectExecer, regID int64, timeToTheMinute time.Time) error { - _, err := dbMap.ExecContext(ctx, `INSERT INTO newOrdersRL - (regID, time, count) - VALUES (?, ?, 1) - ON DUPLICATE KEY UPDATE count=count+1;`, - regID, - timeToTheMinute, - ) - if err != nil { - return err - } - return nil -} - -// countNewOrders returns the count of orders created in the given time range -// for the given registration ID. -func countNewOrders(ctx context.Context, dbMap db.Selector, req *sapb.CountOrdersRequest) (*sapb.Count, error) { - var counts []int64 - _, err := dbMap.Select( - ctx, - &counts, - `SELECT count FROM newOrdersRL - WHERE regID = :regID AND - time > :earliest AND - time <= :latest`, - map[string]interface{}{ - "regID": req.AccountID, - "earliest": req.Range.Earliest.AsTime(), - "latest": req.Range.Latest.AsTime(), - }, - ) - if err != nil { - if db.IsNoRows(err) { - return &sapb.Count{Count: 0}, nil - } - return nil, err - } - var total int64 - for _, count := range counts { - total += count - } - return &sapb.Count{Count: total}, nil -} diff --git a/third-party/github.com/letsencrypt/boulder/sa/rate_limits_test.go b/third-party/github.com/letsencrypt/boulder/sa/rate_limits_test.go deleted file mode 100644 index 1fed4f3f4..000000000 --- a/third-party/github.com/letsencrypt/boulder/sa/rate_limits_test.go +++ /dev/null @@ -1,141 +0,0 @@ -package sa - -import ( - "context" - "fmt" - "testing" - "time" - - "google.golang.org/protobuf/types/known/timestamppb" - - sapb "github.com/letsencrypt/boulder/sa/proto" - "github.com/letsencrypt/boulder/test" -) - -func TestCertsPerNameRateLimitTable(t *testing.T) { - ctx := context.Background() - - sa, _, cleanUp := initSA(t) - defer cleanUp() - - aprilFirst, err := time.Parse(time.RFC3339, "2019-04-01T00:00:00Z") - if err != nil { - t.Fatal(err) - } - - type inputCase struct { - time time.Time - names []string - } - inputs := []inputCase{ - {aprilFirst, []string{"example.com"}}, - {aprilFirst, []string{"example.com", "www.example.com"}}, - {aprilFirst, []string{"example.com", "other.example.com"}}, - {aprilFirst, []string{"dyndns.org"}}, - {aprilFirst, []string{"mydomain.dyndns.org"}}, - {aprilFirst, []string{"mydomain.dyndns.org"}}, - {aprilFirst, []string{"otherdomain.dyndns.org"}}, - } - - // For each hour in a week, add an entry for a certificate that has - // progressively more names. - var manyNames []string - for i := range 7 * 24 { - manyNames = append(manyNames, fmt.Sprintf("%d.manynames.example.net", i)) - inputs = append(inputs, inputCase{aprilFirst.Add(time.Duration(i) * time.Hour), manyNames}) - } - - for _, input := range inputs { - tx, err := sa.dbMap.BeginTx(ctx) - if err != nil { - t.Fatal(err) - } - err = sa.addCertificatesPerName(ctx, tx, input.names, input.time) - if err != nil { - t.Fatal(err) - } - err = tx.Commit() - if err != nil { - t.Fatal(err) - } - } - - const aWeek = time.Duration(7*24) * time.Hour - - testCases := []struct { - caseName string - domainName string - expected int64 - }{ - {"name doesn't exist", "non.example.org", 0}, - {"base name gets dinged for all certs including it", "example.com", 3}, - {"subdomain gets dinged for neighbors", "www.example.com", 3}, - {"other subdomain", "other.example.com", 3}, - {"many subdomains", "1.manynames.example.net", 168}, - {"public suffix gets its own bucket", "dyndns.org", 1}, - {"subdomain of public suffix gets its own bucket", "mydomain.dyndns.org", 2}, - {"subdomain of public suffix gets its own bucket 2", "otherdomain.dyndns.org", 1}, - } - - for _, tc := range testCases { - t.Run(tc.caseName, func(t *testing.T) { - timeRange := &sapb.Range{ - Earliest: timestamppb.New(aprilFirst.Add(-1 * time.Second)), - Latest: timestamppb.New(aprilFirst.Add(aWeek)), - } - count, earliest, err := sa.countCertificatesByName(ctx, sa.dbMap, tc.domainName, timeRange) - if err != nil { - t.Fatal(err) - } - if count != tc.expected { - t.Errorf("Expected count of %d for %q, got %d", tc.expected, tc.domainName, count) - } - if earliest.IsZero() { - // The count should always be zero if earliest is nil. - test.AssertEquals(t, count, int64(0)) - } else { - test.AssertEquals(t, earliest, aprilFirst) - } - }) - } -} - -func TestNewOrdersRateLimitTable(t *testing.T) { - sa, _, cleanUp := initSA(t) - defer cleanUp() - - manyCountRegID := int64(2) - start := time.Now().Truncate(time.Minute) - req := &sapb.CountOrdersRequest{ - AccountID: 1, - Range: &sapb.Range{ - Earliest: timestamppb.New(start), - Latest: timestamppb.New(start.Add(time.Minute * 10)), - }, - } - - for i := 0; i <= 10; i++ { - tx, err := sa.dbMap.BeginTx(ctx) - test.AssertNotError(t, err, "failed to open tx") - for j := 0; j < i+1; j++ { - err = addNewOrdersRateLimit(ctx, tx, manyCountRegID, start.Add(time.Minute*time.Duration(i))) - } - test.AssertNotError(t, err, "addNewOrdersRateLimit failed") - test.AssertNotError(t, tx.Commit(), "failed to commit tx") - } - - count, err := countNewOrders(ctx, sa.dbMap, req) - test.AssertNotError(t, err, "countNewOrders failed") - test.AssertEquals(t, count.Count, int64(0)) - - req.AccountID = manyCountRegID - count, err = countNewOrders(ctx, sa.dbMap, req) - test.AssertNotError(t, err, "countNewOrders failed") - test.AssertEquals(t, count.Count, int64(65)) - - req.Range.Earliest = timestamppb.New(start.Add(time.Minute * 5)) - req.Range.Latest = timestamppb.New(start.Add(time.Minute * 10)) - count, err = countNewOrders(ctx, sa.dbMap, req) - test.AssertNotError(t, err, "countNewOrders failed") - test.AssertEquals(t, count.Count, int64(45)) -} diff --git a/third-party/github.com/letsencrypt/boulder/sa/sa.go b/third-party/github.com/letsencrypt/boulder/sa/sa.go index 1aa1d6066..98e80b774 100644 --- a/third-party/github.com/letsencrypt/boulder/sa/sa.go +++ b/third-party/github.com/letsencrypt/boulder/sa/sa.go @@ -10,6 +10,7 @@ import ( "strings" "time" + "github.com/go-jose/go-jose/v4" "github.com/jmhodges/clock" "github.com/prometheus/client_golang/prometheus" "golang.org/x/crypto/ocsp" @@ -20,11 +21,12 @@ import ( corepb "github.com/letsencrypt/boulder/core/proto" "github.com/letsencrypt/boulder/db" berrors "github.com/letsencrypt/boulder/errors" - "github.com/letsencrypt/boulder/features" bgrpc "github.com/letsencrypt/boulder/grpc" + "github.com/letsencrypt/boulder/identifier" blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/revocation" sapb "github.com/letsencrypt/boulder/sa/proto" + "github.com/letsencrypt/boulder/unpause" ) var ( @@ -100,7 +102,7 @@ func NewSQLStorageAuthority( // NewRegistration stores a new Registration func (ssa *SQLStorageAuthority) NewRegistration(ctx context.Context, req *corepb.Registration) (*corepb.Registration, error) { - if len(req.Key) == 0 || len(req.InitialIP) == 0 { + if len(req.Key) == 0 { return nil, errIncompleteRequest } @@ -109,7 +111,7 @@ func (ssa *SQLStorageAuthority) NewRegistration(ctx context.Context, req *corepb return nil, err } - reg.CreatedAt = ssa.clk.Now().Truncate(time.Second) + reg.CreatedAt = ssa.clk.Now() err = ssa.dbMap.Insert(ctx, reg) if err != nil { @@ -123,59 +125,84 @@ func (ssa *SQLStorageAuthority) NewRegistration(ctx context.Context, req *corepb return registrationModelToPb(reg) } -// UpdateRegistration stores an updated Registration -func (ssa *SQLStorageAuthority) UpdateRegistration(ctx context.Context, req *corepb.Registration) (*emptypb.Empty, error) { - if req == nil || req.Id == 0 || len(req.Key) == 0 || len(req.InitialIP) == 0 { +// UpdateRegistrationContact makes no changes, and simply returns the account +// as it exists in the database. +// +// Deprecated: See https://github.com/letsencrypt/boulder/issues/8199 for removal. +func (ssa *SQLStorageAuthority) UpdateRegistrationContact(ctx context.Context, req *sapb.UpdateRegistrationContactRequest) (*corepb.Registration, error) { + return ssa.GetRegistration(ctx, &sapb.RegistrationID{Id: req.RegistrationID}) +} + +// UpdateRegistrationKey stores an updated key in a Registration. +func (ssa *SQLStorageAuthority) UpdateRegistrationKey(ctx context.Context, req *sapb.UpdateRegistrationKeyRequest) (*corepb.Registration, error) { + if core.IsAnyNilOrZero(req.RegistrationID, req.Jwk) { return nil, errIncompleteRequest } - curr, err := selectRegistration(ctx, ssa.dbMap, "id", req.Id) + // Even though we don't need to convert from JSON to an in-memory JSONWebKey + // for the sake of the `Key` field, we do need to do the conversion in order + // to compute the SHA256 key digest. + var jwk jose.JSONWebKey + err := jwk.UnmarshalJSON(req.Jwk) if err != nil { - if db.IsNoRows(err) { - return nil, berrors.NotFoundError("registration with ID '%d' not found", req.Id) + return nil, fmt.Errorf("parsing JWK: %w", err) + } + sha, err := core.KeyDigestB64(jwk.Key) + if err != nil { + return nil, fmt.Errorf("computing key digest: %w", err) + } + + result, overallError := db.WithTransaction(ctx, ssa.dbMap, func(tx db.Executor) (interface{}, error) { + result, err := tx.ExecContext(ctx, + "UPDATE registrations SET jwk = ?, jwk_sha256 = ? WHERE id = ? LIMIT 1", + req.Jwk, + sha, + req.RegistrationID, + ) + if err != nil { + if db.IsDuplicate(err) { + // duplicate entry error can only happen when jwk_sha256 collides, indicate + // to caller that the provided key is already in use + return nil, berrors.DuplicateError("key is already in use for a different account") + } + return nil, err } - return nil, err - } - - update, err := registrationPbToModel(req) - if err != nil { - return nil, err - } - - // The CreatedAt field shouldn't change from the original, so we copy it straight through. - // This also ensures that it's already truncated to second (which happened on creation). - update.CreatedAt = curr.CreatedAt - - // Copy the existing registration model's LockCol to the new updated - // registration model's LockCol - update.LockCol = curr.LockCol - n, err := ssa.dbMap.Update(ctx, update) - if err != nil { - if db.IsDuplicate(err) { - // duplicate entry error can only happen when jwk_sha256 collides, indicate - // to caller that the provided key is already in use - return nil, berrors.DuplicateError("key is already in use for a different account") + rowsAffected, err := result.RowsAffected() + if err != nil || rowsAffected != 1 { + return nil, berrors.InternalServerError("no registration ID '%d' updated with new jwk", req.RegistrationID) } - return nil, err - } - if n == 0 { - return nil, berrors.NotFoundError("registration with ID '%d' not found", req.Id) + + updatedRegistrationModel, err := selectRegistration(ctx, tx, "id", req.RegistrationID) + if err != nil { + if db.IsNoRows(err) { + return nil, berrors.NotFoundError("registration with ID '%d' not found", req.RegistrationID) + } + return nil, err + } + updatedRegistration, err := registrationModelToPb(updatedRegistrationModel) + if err != nil { + return nil, err + } + + return updatedRegistration, nil + }) + if overallError != nil { + return nil, overallError } - return &emptypb.Empty{}, nil + return result.(*corepb.Registration), nil } // AddSerial writes a record of a serial number generation to the DB. func (ssa *SQLStorageAuthority) AddSerial(ctx context.Context, req *sapb.AddSerialRequest) (*emptypb.Empty, error) { - // TODO(#7153): Check each value via core.IsAnyNilOrZero - if req.Serial == "" || req.RegID == 0 || core.IsAnyNilOrZero(req.Created, req.Expires) { + if core.IsAnyNilOrZero(req.Serial, req.RegID, req.Created, req.Expires) { return nil, errIncompleteRequest } err := ssa.dbMap.Insert(ctx, &recordedSerialModel{ Serial: req.Serial, RegistrationID: req.RegID, - Created: req.Created.AsTime().Truncate(time.Second), - Expires: req.Expires.AsTime().Truncate(time.Second), + Created: req.Created.AsTime(), + Expires: req.Expires.AsTime(), }) if err != nil { return nil, err @@ -209,13 +236,16 @@ func (ssa *SQLStorageAuthority) SetCertificateStatusReady(ctx context.Context, r return &emptypb.Empty{}, nil } -// AddPrecertificate writes a record of a precertificate generation to the DB. +// AddPrecertificate writes a record of a linting certificate to the database. +// +// Note: The name "AddPrecertificate" is a historical artifact, and this is now +// always called with a linting certificate. See #6807. +// // Note: this is not idempotent: it does not protect against inserting the same // certificate multiple times. Calling code needs to first insert the cert's // serial into the Serials table to ensure uniqueness. func (ssa *SQLStorageAuthority) AddPrecertificate(ctx context.Context, req *sapb.AddCertificateRequest) (*emptypb.Empty, error) { - // TODO(#7153): Check each value via core.IsAnyNilOrZero - if len(req.Der) == 0 || req.RegID == 0 || req.IssuerNameID == 0 || core.IsAnyNilOrZero(req.Issued) { + if core.IsAnyNilOrZero(req.Der, req.RegID, req.IssuerNameID, req.Issued) { return nil, errIncompleteRequest } parsed, err := x509.ParseCertificate(req.Der) @@ -224,11 +254,11 @@ func (ssa *SQLStorageAuthority) AddPrecertificate(ctx context.Context, req *sapb } serialHex := core.SerialToString(parsed.SerialNumber) - preCertModel := &precertificateModel{ + preCertModel := &lintingCertModel{ Serial: serialHex, RegistrationID: req.RegID, DER: req.Der, - Issued: req.Issued.AsTime().Truncate(time.Second), + Issued: req.Issued.AsTime(), Expires: parsed.NotAfter, } @@ -254,34 +284,28 @@ func (ssa *SQLStorageAuthority) AddPrecertificate(ctx context.Context, req *sapb if req.OcspNotReady { status = core.OCSPStatusNotReady } - cs := &core.CertificateStatus{ + cs := &certificateStatusModel{ Serial: serialHex, Status: status, - OCSPLastUpdated: ssa.clk.Now().Truncate(time.Second), + OCSPLastUpdated: ssa.clk.Now(), RevokedDate: time.Time{}, RevokedReason: 0, LastExpirationNagSent: time.Time{}, - // No need to truncate because it's already truncated to encode - // per https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.5.1 - NotAfter: parsed.NotAfter, - IsExpired: false, - IssuerNameID: req.IssuerNameID, + NotAfter: parsed.NotAfter, + IsExpired: false, + IssuerID: req.IssuerNameID, } err = ssa.dbMap.Insert(ctx, cs) if err != nil { return nil, err } - // NOTE(@cpu): When we collect up names to check if an FQDN set exists (e.g. - // that it is a renewal) we use just the DNSNames from the certificate and - // ignore the Subject Common Name (if any). This is a safe assumption because - // if a certificate we issued were to have a Subj. CN not present as a SAN it - // would be a misissuance and miscalculating whether the cert is a renewal or - // not for the purpose of rate limiting is the least of our troubles. + idents := identifier.FromCert(parsed) + isRenewal, err := ssa.checkFQDNSetExists( ctx, tx.SelectOne, - parsed.DNSNames) + idents) if err != nil { return nil, err } @@ -308,8 +332,7 @@ func (ssa *SQLStorageAuthority) AddPrecertificate(ctx context.Context, req *sapb // AddCertificate stores an issued certificate, returning an error if it is a // duplicate or if any other failure occurs. func (ssa *SQLStorageAuthority) AddCertificate(ctx context.Context, req *sapb.AddCertificateRequest) (*emptypb.Empty, error) { - // TODO(#7153): Check each value via core.IsAnyNilOrZero - if len(req.Der) == 0 || req.RegID == 0 || core.IsAnyNilOrZero(req.Issued) { + if core.IsAnyNilOrZero(req.Der, req.RegID, req.Issued) { return nil, errIncompleteRequest } parsedCertificate, err := x509.ParseCertificate(req.Der) @@ -324,11 +347,11 @@ func (ssa *SQLStorageAuthority) AddCertificate(ctx context.Context, req *sapb.Ad Serial: serial, Digest: digest, DER: req.Der, - Issued: req.Issued.AsTime().Truncate(time.Second), + Issued: req.Issued.AsTime(), Expires: parsedCertificate.NotAfter, } - isRenewalRaw, overallError := db.WithTransaction(ctx, ssa.dbMap, func(tx db.Executor) (interface{}, error) { + _, overallError := db.WithTransaction(ctx, ssa.dbMap, func(tx db.Executor) (interface{}, error) { // Select to see if cert exists var row struct { Count int64 @@ -347,58 +370,23 @@ func (ssa *SQLStorageAuthority) AddCertificate(ctx context.Context, req *sapb.Ad return nil, err } - // NOTE(@cpu): When we collect up names to check if an FQDN set exists (e.g. - // that it is a renewal) we use just the DNSNames from the certificate and - // ignore the Subject Common Name (if any). This is a safe assumption because - // if a certificate we issued were to have a Subj. CN not present as a SAN it - // would be a misissuance and miscalculating whether the cert is a renewal or - // not for the purpose of rate limiting is the least of our troubles. - isRenewal, err := ssa.checkFQDNSetExists( - ctx, - tx.SelectOne, - parsedCertificate.DNSNames) - if err != nil { - return nil, err - } - - return isRenewal, err + return nil, err }) if overallError != nil { return nil, overallError } - // Recast the interface{} return from db.WithTransaction as a bool, returning - // an error if we can't. - var isRenewal bool - if boolVal, ok := isRenewalRaw.(bool); !ok { - return nil, fmt.Errorf( - "AddCertificate db.WithTransaction returned %T out var, expected bool", - isRenewalRaw) - } else { - isRenewal = boolVal - } - - // In a separate transaction perform the work required to update tables used - // for rate limits. Since the effects of failing these writes is slight - // miscalculation of rate limits we choose to not fail the AddCertificate - // operation if the rate limit update transaction fails. - _, rlTransactionErr := db.WithTransaction(ctx, ssa.dbMap, func(tx db.Executor) (interface{}, error) { - // Add to the rate limit table, but only for new certificates. Renewals - // don't count against the certificatesPerName limit. - if !isRenewal { - timeToTheHour := parsedCertificate.NotBefore.Round(time.Hour) - err := ssa.addCertificatesPerName(ctx, tx, parsedCertificate.DNSNames, timeToTheHour) - if err != nil { - return nil, err - } - } - - // Update the FQDN sets now that there is a final certificate to ensure rate - // limits are calculated correctly. + // In a separate transaction, perform the work required to update the table + // used for order reuse. Since the effect of failing the write is just a + // missed opportunity to reuse an order, we choose to not fail the + // AddCertificate operation if this update transaction fails. + _, fqdnTransactionErr := db.WithTransaction(ctx, ssa.dbMap, func(tx db.Executor) (interface{}, error) { + // Update the FQDN sets now that there is a final certificate to ensure + // reuse is determined correctly. err = addFQDNSet( ctx, tx, - parsedCertificate.DNSNames, + identifier.FromCert(parsedCertificate), core.SerialToString(parsedCertificate.SerialNumber), parsedCertificate.NotBefore, parsedCertificate.NotAfter, @@ -409,31 +397,67 @@ func (ssa *SQLStorageAuthority) AddCertificate(ctx context.Context, req *sapb.Ad return nil, nil }) - // If the ratelimit transaction failed increment a stat and log a warning + // If the FQDN sets transaction failed, increment a stat and log a warning // but don't return an error from AddCertificate. - if rlTransactionErr != nil { + if fqdnTransactionErr != nil { ssa.rateLimitWriteErrors.Inc() - ssa.log.AuditErrf("failed AddCertificate ratelimit update transaction: %v", rlTransactionErr) + ssa.log.AuditErrf("failed AddCertificate FQDN sets insert transaction: %v", fqdnTransactionErr) } return &emptypb.Empty{}, nil } -// DeactivateRegistration deactivates a currently valid registration -func (ssa *SQLStorageAuthority) DeactivateRegistration(ctx context.Context, req *sapb.RegistrationID) (*emptypb.Empty, error) { +// DeactivateRegistration deactivates a currently valid registration and removes its contact field +func (ssa *SQLStorageAuthority) DeactivateRegistration(ctx context.Context, req *sapb.RegistrationID) (*corepb.Registration, error) { if req == nil || req.Id == 0 { return nil, errIncompleteRequest } - _, err := ssa.dbMap.ExecContext(ctx, - "UPDATE registrations SET status = ? WHERE status = ? AND id = ?", - string(core.StatusDeactivated), - string(core.StatusValid), - req.Id, - ) - if err != nil { - return nil, err + + result, overallError := db.WithTransaction(ctx, ssa.dbMap, func(tx db.Executor) (any, error) { + result, err := tx.ExecContext(ctx, + "UPDATE registrations SET status = ? WHERE status = ? AND id = ? LIMIT 1", + string(core.StatusDeactivated), + string(core.StatusValid), + req.Id, + ) + if err != nil { + return nil, fmt.Errorf("deactivating account %d: %w", req.Id, err) + } + rowsAffected, err := result.RowsAffected() + if err != nil { + return nil, fmt.Errorf("deactivating account %d: %w", req.Id, err) + } + if rowsAffected == 0 { + return nil, berrors.NotFoundError("no active account with id %d", req.Id) + } else if rowsAffected > 1 { + return nil, berrors.InternalServerError("unexpectedly deactivated multiple accounts with id %d", req.Id) + } + + updatedRegistrationModel, err := selectRegistration(ctx, tx, "id", req.Id) + if err != nil { + if db.IsNoRows(err) { + return nil, berrors.NotFoundError("fetching account %d: no rows found", req.Id) + } + return nil, fmt.Errorf("fetching account %d: %w", req.Id, err) + } + + updatedRegistration, err := registrationModelToPb(updatedRegistrationModel) + if err != nil { + return nil, err + } + + return updatedRegistration, nil + }) + if overallError != nil { + return nil, overallError } - return &emptypb.Empty{}, nil + + res, ok := result.(*corepb.Registration) + if !ok { + return nil, fmt.Errorf("unexpected casting failure in DeactivateRegistration") + } + + return res, nil } // DeactivateAuthorization2 deactivates a currently valid or pending authorization. @@ -467,149 +491,105 @@ func (ssa *SQLStorageAuthority) NewOrderAndAuthzs(ctx context.Context, req *sapb return nil, errIncompleteRequest } + for _, authz := range req.NewAuthzs { + if authz.RegistrationID != req.NewOrder.RegistrationID { + // This is a belt-and-suspenders check. These were just created by the RA, + // so their RegIDs should match. But if they don't, the consequences would + // be very bad, so we do an extra check here. + return nil, errors.New("new order and authzs must all be associated with same account") + } + } + output, err := db.WithTransaction(ctx, ssa.dbMap, func(tx db.Executor) (interface{}, error) { // First, insert all of the new authorizations and record their IDs. - newAuthzIDs := make([]int64, 0) - if len(req.NewAuthzs) != 0 { - inserter, err := db.NewMultiInserter("authz2", strings.Split(authzFields, ", "), "id") + newAuthzIDs := make([]int64, 0, len(req.NewAuthzs)) + for _, authz := range req.NewAuthzs { + am, err := newAuthzReqToModel(authz, req.NewOrder.CertificateProfileName) if err != nil { return nil, err } - for _, authz := range req.NewAuthzs { - if authz.Status != string(core.StatusPending) { - return nil, berrors.InternalServerError("authorization must be pending") - } - am, err := authzPBToModel(authz) - if err != nil { - return nil, err - } - // These parameters correspond to the fields listed in `authzFields`, as used in the - // `db.NewMultiInserter` call above, and occur in the same order. - err = inserter.Add([]interface{}{ - am.ID, - am.IdentifierType, - am.IdentifierValue, - am.RegistrationID, - statusToUint[core.StatusPending], - am.Expires.Truncate(time.Second), - am.Challenges, - nil, - nil, - am.Token, - nil, - nil, - }) - if err != nil { - return nil, err - } - } - newAuthzIDs, err = inserter.Insert(ctx, tx) + err = tx.Insert(ctx, am) if err != nil { return nil, err } + newAuthzIDs = append(newAuthzIDs, am.ID) } // Second, insert the new order. - var orderID int64 - var err error - created := ssa.clk.Now().Truncate(time.Second) - expires := req.NewOrder.Expires.AsTime().Truncate(time.Second) - if features.Get().MultipleCertificateProfiles { - omv2 := orderModelv2{ - RegistrationID: req.NewOrder.RegistrationID, - Expires: expires, - Created: created, - CertificateProfileName: req.NewOrder.CertificateProfileName, - } - err = tx.Insert(ctx, &omv2) - orderID = omv2.ID - } else { - omv1 := orderModelv1{ - RegistrationID: req.NewOrder.RegistrationID, - Expires: expires, - Created: created, - } - err = tx.Insert(ctx, &omv1) - orderID = omv1.ID + created := ssa.clk.Now() + om := orderModel{ + RegistrationID: req.NewOrder.RegistrationID, + Expires: req.NewOrder.Expires.AsTime(), + Created: created, + CertificateProfileName: &req.NewOrder.CertificateProfileName, + Replaces: &req.NewOrder.Replaces, } + err := tx.Insert(ctx, &om) if err != nil { return nil, err } + orderID := om.ID // Third, insert all of the orderToAuthz relations. - inserter, err := db.NewMultiInserter("orderToAuthz2", []string{"orderID", "authzID"}, "") + // Have to combine the already-associated and newly-created authzs. + allAuthzIds := append(req.NewOrder.V2Authorizations, newAuthzIDs...) + inserter, err := db.NewMultiInserter("orderToAuthz2", []string{"orderID", "authzID"}) if err != nil { return nil, err } - for _, id := range req.NewOrder.V2Authorizations { + for _, id := range allAuthzIds { err := inserter.Add([]interface{}{orderID, id}) if err != nil { return nil, err } } - for _, id := range newAuthzIDs { - err := inserter.Add([]interface{}{orderID, id}) - if err != nil { - return nil, err - } - } - _, err = inserter.Insert(ctx, tx) + err = inserter.Insert(ctx, tx) if err != nil { return nil, err } // Fourth, insert the FQDNSet entry for the order. - err = addOrderFQDNSet(ctx, - tx, - req.NewOrder.Names, - orderID, - req.NewOrder.RegistrationID, - expires, - ) + err = addOrderFQDNSet(ctx, tx, identifier.FromProtoSlice(req.NewOrder.Identifiers), orderID, req.NewOrder.RegistrationID, req.NewOrder.Expires.AsTime()) if err != nil { return nil, err } - // Finally, build the overall Order PB to return. - res := &corepb.Order{ - // ID and Created were auto-populated on the order model when it was inserted. - Id: orderID, - Created: timestamppb.New(created), - // These are carried over from the original request unchanged. - RegistrationID: req.NewOrder.RegistrationID, - Expires: timestamppb.New(expires), - Names: req.NewOrder.Names, - // Have to combine the already-associated and newly-reacted authzs. - V2Authorizations: append(req.NewOrder.V2Authorizations, newAuthzIDs...), - // A new order is never processing because it can't be finalized yet. - BeganProcessing: false, - // An empty string is allowed. When the RA retrieves the order and - // transmits it to the CA, the empty string will take the value of - // DefaultCertProfileName from the //issuance package. - CertificateProfileName: req.NewOrder.CertificateProfileName, - } - if req.NewOrder.ReplacesSerial != "" { // Update the replacementOrders table to indicate that this order // replaces the provided certificate serial. - err := addReplacementOrder(ctx, - tx, - req.NewOrder.ReplacesSerial, - orderID, - req.NewOrder.Expires.AsTime().Truncate(time.Second), - ) + err := addReplacementOrder(ctx, tx, req.NewOrder.ReplacesSerial, orderID, req.NewOrder.Expires.AsTime()) if err != nil { return nil, err } } // Get the partial Authorization objects for the order - authzValidityInfo, err := getAuthorizationStatuses(ctx, tx, res.V2Authorizations) + authzValidityInfo, err := getAuthorizationStatuses(ctx, tx, allAuthzIds) // If there was an error getting the authorizations, return it immediately if err != nil { return nil, err } + // Finally, build the overall Order PB. + res := &corepb.Order{ + // ID and Created were auto-populated on the order model when it was inserted. + Id: orderID, + Created: timestamppb.New(created), + // These are carried over from the original request unchanged. + RegistrationID: req.NewOrder.RegistrationID, + Expires: req.NewOrder.Expires, + Identifiers: req.NewOrder.Identifiers, + // This includes both reused and newly created authz IDs. + V2Authorizations: allAuthzIds, + // A new order is never processing because it can't be finalized yet. + BeganProcessing: false, + // An empty string is allowed. When the RA retrieves the order and + // transmits it to the CA, the empty string will take the value of + // DefaultCertProfileName from the //issuance package. + CertificateProfileName: req.NewOrder.CertificateProfileName, + Replaces: req.NewOrder.Replaces, + } + // Calculate the order status before returning it. Since it may have reused // all valid authorizations the order may be "born" in a ready status. status, err := statusForOrder(res, authzValidityInfo, ssa.clk.Now()) @@ -629,12 +609,6 @@ func (ssa *SQLStorageAuthority) NewOrderAndAuthzs(ctx context.Context, req *sapb return nil, fmt.Errorf("casting error in NewOrderAndAuthzs") } - // Increment the order creation count - err = addNewOrdersRateLimit(ctx, ssa.dbMap, req.NewOrder.RegistrationID, ssa.clk.Now().Truncate(time.Minute)) - if err != nil { - return nil, err - } - return order, nil } @@ -677,7 +651,7 @@ func (ssa *SQLStorageAuthority) SetOrderError(ctx context.Context, req *sapb.Set return nil, errIncompleteRequest } _, overallError := db.WithTransaction(ctx, ssa.dbMap, func(tx db.Executor) (interface{}, error) { - om, err := orderToModelv2(&corepb.Order{ + om, err := orderToModel(&corepb.Order{ Id: req.Id, Error: req.Error, }) @@ -740,11 +714,9 @@ func (ssa *SQLStorageAuthority) FinalizeOrder(ctx context.Context, req *sapb.Fin return nil, err } - if features.Get().TrackReplacementCertificatesARI { - err = setReplacementOrderFinalized(ctx, tx, req.Id) - if err != nil { - return nil, err - } + err = setReplacementOrderFinalized(ctx, tx, req.Id) + if err != nil { + return nil, err } return nil, nil @@ -759,8 +731,7 @@ func (ssa *SQLStorageAuthority) FinalizeOrder(ctx context.Context, req *sapb.Fin // the authorization is being moved to invalid the validationError field must be set. If the // authorization is being moved to valid the validationRecord and expires fields must be set. func (ssa *SQLStorageAuthority) FinalizeAuthorization2(ctx context.Context, req *sapb.FinalizeAuthorizationRequest) (*emptypb.Empty, error) { - // TODO(#7153): Check each value via core.IsAnyNilOrZero - if req.Status == "" || req.Attempted == "" || req.Id == 0 || core.IsAnyNilOrZero(req.Expires) { + if core.IsAnyNilOrZero(req.Status, req.Attempted, req.Id, req.Expires) { return nil, errIncompleteRequest } @@ -810,7 +781,7 @@ func (ssa *SQLStorageAuthority) FinalizeAuthorization2(ctx context.Context, req // database attemptedAt field Null instead of 1970-01-01 00:00:00. var attemptedTime *time.Time if !core.IsAnyNilOrZero(req.AttemptedAt) { - val := req.AttemptedAt.AsTime().Truncate(time.Second) + val := req.AttemptedAt.AsTime() attemptedTime = &val } params := map[string]interface{}{ @@ -820,7 +791,7 @@ func (ssa *SQLStorageAuthority) FinalizeAuthorization2(ctx context.Context, req "validationRecord": vrJSON, "id": req.Id, "pending": statusUint(core.StatusPending), - "expires": req.Expires.AsTime().Truncate(time.Second), + "expires": req.Expires.AsTime(), // if req.ValidationError is nil veJSON should also be nil // which should result in a NULL field "validationError": veJSON, @@ -881,14 +852,16 @@ func addRevokedCertificate(ctx context.Context, tx db.Executor, req *sapb.Revoke // RevokeCertificate stores revocation information about a certificate. It will only store this // information if the certificate is not already marked as revoked. +// +// If ShardIdx is non-zero, RevokeCertificate also writes an entry for this certificate to +// the revokedCertificates table, with the provided shard number. func (ssa *SQLStorageAuthority) RevokeCertificate(ctx context.Context, req *sapb.RevokeCertificateRequest) (*emptypb.Empty, error) { - // TODO(#7153): Check each value via core.IsAnyNilOrZero - if req.Serial == "" || req.IssuerID == 0 || core.IsAnyNilOrZero(req.Date) { + if core.IsAnyNilOrZero(req.Serial, req.IssuerID, req.Date) { return nil, errIncompleteRequest } _, overallError := db.WithTransaction(ctx, ssa.dbMap, func(tx db.Executor) (interface{}, error) { - revokedDate := req.Date.AsTime().Truncate(time.Second) + revokedDate := req.Date.AsTime() res, err := tx.ExecContext(ctx, `UPDATE certificateStatus SET @@ -936,8 +909,7 @@ func (ssa *SQLStorageAuthority) RevokeCertificate(ctx context.Context, req *sapb // cert is already revoked, if the new revocation reason is `KeyCompromise`, // and if the revokedDate is identical to the current revokedDate. func (ssa *SQLStorageAuthority) UpdateRevokedCertificate(ctx context.Context, req *sapb.RevokeCertificateRequest) (*emptypb.Empty, error) { - // TODO(#7153): Check each value via core.IsAnyNilOrZero - if req.Serial == "" || req.IssuerID == 0 || core.IsAnyNilOrZero(req.Date, req.Backdate) { + if core.IsAnyNilOrZero(req.Serial, req.IssuerID, req.Date, req.Backdate) { return nil, errIncompleteRequest } if req.Reason != ocsp.KeyCompromise { @@ -945,8 +917,8 @@ func (ssa *SQLStorageAuthority) UpdateRevokedCertificate(ctx context.Context, re } _, overallError := db.WithTransaction(ctx, ssa.dbMap, func(tx db.Executor) (interface{}, error) { - thisUpdate := req.Date.AsTime().Truncate(time.Second) - revokedDate := req.Backdate.AsTime().Truncate(time.Second) + thisUpdate := req.Date.AsTime() + revokedDate := req.Backdate.AsTime() res, err := tx.ExecContext(ctx, `UPDATE certificateStatus SET @@ -982,7 +954,7 @@ func (ssa *SQLStorageAuthority) UpdateRevokedCertificate(ctx context.Context, re // the "UPDATE certificateStatus SET revokedReason..." above if this // query ever becomes the first or only query in this transaction. We are // currently relying on the query above to exit early if the certificate - // does not have an appropriate status. + // does not have an appropriate status and revocation reason. err = tx.SelectOne( ctx, &rcm, `SELECT * FROM revokedCertificates WHERE serial = ?`, req.Serial) if db.IsNoRows(err) { @@ -1028,7 +1000,7 @@ func (ssa *SQLStorageAuthority) AddBlockedKey(ctx context.Context, req *sapb.Add cols, qs := blockedKeysColumns, "?, ?, ?, ?" vals := []interface{}{ req.KeyHash, - req.Added.AsTime().Truncate(time.Second), + req.Added.AsTime(), sourceInt, req.Comment, } @@ -1107,7 +1079,6 @@ func (ssa *SQLStorageAuthority) leaseOldestCRLShard(ctx context.Context, req *sa // Determine which shard index we want to lease. var shardIdx int - var needToInsert bool if len(shards) < (int(req.MaxShardIdx + 1 - req.MinShardIdx)) { // Some expected shards are missing (i.e. never-before-produced), so we // pick one at random. @@ -1123,7 +1094,17 @@ func (ssa *SQLStorageAuthority) leaseOldestCRLShard(ctx context.Context, req *sa shardIdx = idx break } - needToInsert = true + + _, err = tx.ExecContext(ctx, + `INSERT INTO crlShards (issuerID, idx, leasedUntil) + VALUES (?, ?, ?)`, + req.IssuerNameID, + shardIdx, + req.Until.AsTime(), + ) + if err != nil { + return -1, fmt.Errorf("inserting selected shard: %w", err) + } } else { // We got all the shards we expect, so we pick the oldest unleased shard. var oldest *crlShardModel @@ -1141,34 +1122,29 @@ func (ssa *SQLStorageAuthority) leaseOldestCRLShard(ctx context.Context, req *sa return -1, fmt.Errorf("issuer %d has no unleased shards in range %d-%d", req.IssuerNameID, req.MinShardIdx, req.MaxShardIdx) } shardIdx = oldest.Idx - needToInsert = false - } - if needToInsert { - _, err = tx.ExecContext(ctx, - `INSERT INTO crlShards (issuerID, idx, leasedUntil) - VALUES (?, ?, ?)`, - req.IssuerNameID, - shardIdx, - req.Until.AsTime(), - ) - if err != nil { - return -1, fmt.Errorf("inserting selected shard: %w", err) - } - } else { - _, err = tx.ExecContext(ctx, + res, err := tx.ExecContext(ctx, `UPDATE crlShards SET leasedUntil = ? WHERE issuerID = ? AND idx = ? + AND leasedUntil = ? LIMIT 1`, req.Until.AsTime(), req.IssuerNameID, shardIdx, + oldest.LeasedUntil, ) if err != nil { return -1, fmt.Errorf("updating selected shard: %w", err) } + rowsAffected, err := res.RowsAffected() + if err != nil { + return -1, fmt.Errorf("confirming update of selected shard: %w", err) + } + if rowsAffected != 1 { + return -1, errors.New("failed to lease shard") + } } return shardIdx, err @@ -1224,19 +1200,28 @@ func (ssa *SQLStorageAuthority) leaseSpecificCRLShard(ctx context.Context, req * return nil, fmt.Errorf("inserting selected shard: %w", err) } } else { - _, err = tx.ExecContext(ctx, + res, err := tx.ExecContext(ctx, `UPDATE crlShards SET leasedUntil = ? WHERE issuerID = ? AND idx = ? + AND leasedUntil = ? LIMIT 1`, req.Until.AsTime(), req.IssuerNameID, req.MinShardIdx, + shardModel.LeasedUntil, ) if err != nil { return nil, fmt.Errorf("updating selected shard: %w", err) } + rowsAffected, err := res.RowsAffected() + if err != nil { + return -1, fmt.Errorf("confirming update of selected shard: %w", err) + } + if rowsAffected != 1 { + return -1, errors.New("failed to lease shard") + } } return nil, nil @@ -1271,12 +1256,11 @@ func (ssa *SQLStorageAuthority) UpdateCRLShard(ctx context.Context, req *sapb.Up // Only set the nextUpdate if it's actually present in the request message. var nextUpdate *time.Time if req.NextUpdate != nil { - nut := req.NextUpdate.AsTime().Truncate(time.Second) + nut := req.NextUpdate.AsTime() nextUpdate = &nut } _, err := db.WithTransaction(ctx, ssa.dbMap, func(tx db.Executor) (interface{}, error) { - thisUpdate := req.ThisUpdate.AsTime().Truncate(time.Second) res, err := tx.ExecContext(ctx, `UPDATE crlShards SET thisUpdate = ?, nextUpdate = ?, leasedUntil = ? @@ -1284,12 +1268,12 @@ func (ssa *SQLStorageAuthority) UpdateCRLShard(ctx context.Context, req *sapb.Up AND idx = ? AND (thisUpdate is NULL OR thisUpdate <= ?) LIMIT 1`, - thisUpdate, + req.ThisUpdate.AsTime(), nextUpdate, - thisUpdate, + req.ThisUpdate.AsTime(), req.IssuerNameID, req.ShardIdx, - thisUpdate, + req.ThisUpdate.AsTime(), ) if err != nil { return nil, err @@ -1316,25 +1300,27 @@ func (ssa *SQLStorageAuthority) UpdateCRLShard(ctx context.Context, req *sapb.Up // PauseIdentifiers pauses a set of identifiers for the provided account. If an // identifier is currently paused, this is a no-op. If an identifier was -// previously paused and unpaused, it will be repaused. All work is accomplished -// in a transaction to limit possible race conditions. +// previously paused and unpaused, it will be repaused unless it was unpaused +// less than two weeks ago. The response will indicate how many identifiers were +// paused and how many were repaused. All work is accomplished in a transaction +// to limit possible race conditions. func (ssa *SQLStorageAuthority) PauseIdentifiers(ctx context.Context, req *sapb.PauseRequest) (*sapb.PauseIdentifiersResponse, error) { if core.IsAnyNilOrZero(req.RegistrationID, req.Identifiers) { return nil, errIncompleteRequest } // Marshal the identifier now that we've crossed the RPC boundary. - identifiers, err := newIdentifierModelsFromPB(req.Identifiers) + idents, err := newIdentifierModelsFromPB(req.Identifiers) if err != nil { return nil, err } response := &sapb.PauseIdentifiersResponse{} _, err = db.WithTransaction(ctx, ssa.dbMap, func(tx db.Executor) (interface{}, error) { - for _, identifier := range identifiers { + for _, ident := range idents { pauseError := func(op string, err error) error { return fmt.Errorf("while %s identifier %s for registration ID %d: %w", - op, identifier.Value, req.RegistrationID, err, + op, ident.Value, req.RegistrationID, err, ) } @@ -1342,13 +1328,13 @@ func (ssa *SQLStorageAuthority) PauseIdentifiers(ctx context.Context, req *sapb. err := tx.SelectOne(ctx, &entry, ` SELECT pausedAt, unpausedAt FROM paused - WHERE - registrationID = ? AND - identifierType = ? AND + WHERE + registrationID = ? AND + identifierType = ? AND identifierValue = ?`, req.RegistrationID, - identifier.Type, - identifier.Value, + ident.Type, + ident.Value, ) switch { @@ -1362,8 +1348,8 @@ func (ssa *SQLStorageAuthority) PauseIdentifiers(ctx context.Context, req *sapb. RegistrationID: req.RegistrationID, PausedAt: ssa.clk.Now().Truncate(time.Second), identifierModel: identifierModel{ - Type: identifier.Type, - Value: identifier.Value, + Type: ident.Type, + Value: ident.Value, }, }) if err != nil && !db.IsDuplicate(err) { @@ -1378,21 +1364,25 @@ func (ssa *SQLStorageAuthority) PauseIdentifiers(ctx context.Context, req *sapb. // Identifier is already paused. continue + case entry.UnpausedAt.After(ssa.clk.Now().Add(-14 * 24 * time.Hour)): + // Previously unpaused less than two weeks ago, skip this identifier. + continue + case entry.UnpausedAt.After(entry.PausedAt): // Previously paused (and unpaused), repause the identifier. _, err := tx.ExecContext(ctx, ` UPDATE paused SET pausedAt = ?, unpausedAt = NULL - WHERE - registrationID = ? AND - identifierType = ? AND + WHERE + registrationID = ? AND + identifierType = ? AND identifierValue = ? AND unpausedAt IS NOT NULL`, ssa.clk.Now().Truncate(time.Second), req.RegistrationID, - identifier.Type, - identifier.Value, + ident.Type, + ident.Value, ) if err != nil { return nil, pauseError("repausing", err) @@ -1405,7 +1395,7 @@ func (ssa *SQLStorageAuthority) PauseIdentifiers(ctx context.Context, req *sapb. default: // This indicates a database state which should never occur. return nil, fmt.Errorf("impossible database state encountered while pausing identifier %s", - identifier.Value, + ident.Value, ) } } @@ -1418,25 +1408,200 @@ func (ssa *SQLStorageAuthority) PauseIdentifiers(ctx context.Context, req *sapb. return response, nil } -// UnpauseAccount will unpause all paused identifiers for the provided account. -// If no identifiers are currently paused, this is a no-op. -func (ssa *SQLStorageAuthority) UnpauseAccount(ctx context.Context, req *sapb.RegistrationID) (*emptypb.Empty, error) { +// UnpauseAccount uses up to 5 iterations of UPDATE queries each with a LIMIT of +// 10,000 to unpause up to 50,000 identifiers and returns a count of identifiers +// unpaused. If the returned count is 50,000 there may be more paused identifiers. +func (ssa *SQLStorageAuthority) UnpauseAccount(ctx context.Context, req *sapb.RegistrationID) (*sapb.Count, error) { if core.IsAnyNilOrZero(req.Id) { return nil, errIncompleteRequest } - _, err := ssa.dbMap.ExecContext(ctx, ` - UPDATE paused - SET unpausedAt = ? - WHERE - registrationID = ? AND - unpausedAt IS NULL`, - ssa.clk.Now().Truncate(time.Second), - req.Id, - ) + total := &sapb.Count{} + + for i := 0; i < unpause.MaxBatches; i++ { + result, err := ssa.dbMap.ExecContext(ctx, ` + UPDATE paused + SET unpausedAt = ? + WHERE + registrationID = ? AND + unpausedAt IS NULL + LIMIT ?`, + ssa.clk.Now(), + req.Id, + unpause.BatchSize, + ) + if err != nil { + return nil, err + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return nil, err + } + + total.Count += rowsAffected + if rowsAffected < unpause.BatchSize { + // Fewer than batchSize rows were updated, so we're done. + break + } + } + + return total, nil +} + +// AddRateLimitOverride adds a rate limit override to the database. If the +// override already exists, it will be updated. If the override does not exist, +// it will be inserted and enabled. If the override exists but has been +// disabled, it will be updated but not be re-enabled. The status of the +// override is returned in Enabled field of the response. To re-enable an +// override, use the EnableRateLimitOverride method. +func (ssa *SQLStorageAuthority) AddRateLimitOverride(ctx context.Context, req *sapb.AddRateLimitOverrideRequest) (*sapb.AddRateLimitOverrideResponse, error) { + if core.IsAnyNilOrZero(req, req.Override, req.Override.LimitEnum, req.Override.BucketKey, req.Override.Count, req.Override.Burst, req.Override.Period, req.Override.Comment) { + return nil, errIncompleteRequest + } + + var inserted bool + var enabled bool + now := ssa.clk.Now() + + _, err := db.WithTransaction(ctx, ssa.dbMap, func(tx db.Executor) (any, error) { + var alreadyEnabled bool + err := tx.SelectOne(ctx, &alreadyEnabled, ` + SELECT enabled + FROM overrides + WHERE limitEnum = ? AND + bucketKey = ?`, + req.Override.LimitEnum, + req.Override.BucketKey, + ) + + switch { + case err != nil && !db.IsNoRows(err): + // Error querying the database. + return nil, fmt.Errorf("querying override for rate limit %d and bucket key %s: %w", + req.Override.LimitEnum, + req.Override.BucketKey, + err, + ) + + case db.IsNoRows(err): + // Insert a new overrides row. + new := overrideModelForPB(req.Override, now, true) + err = tx.Insert(ctx, &new) + if err != nil { + return nil, fmt.Errorf("inserting override for rate limit %d and bucket key %s: %w", + req.Override.LimitEnum, + req.Override.BucketKey, + err, + ) + } + inserted = true + enabled = true + + default: + // Update the existing overrides row. + updated := overrideModelForPB(req.Override, now, alreadyEnabled) + _, err = tx.Update(ctx, &updated) + if err != nil { + return nil, fmt.Errorf("updating override for rate limit %d and bucket key %s override: %w", + req.Override.LimitEnum, + req.Override.BucketKey, + err, + ) + } + inserted = false + enabled = alreadyEnabled + } + return nil, nil + }) + if err != nil { + // Error occurred during transaction. + return nil, err + } + return &sapb.AddRateLimitOverrideResponse{Inserted: inserted, Enabled: enabled}, nil +} + +// setRateLimitOverride sets the enabled field of a rate limit override to the +// provided value and updates the updatedAt column. If the override does not +// exist, a NotFoundError is returned. If the override exists but is already in +// the requested state, this is a no-op. +func (ssa *SQLStorageAuthority) setRateLimitOverride(ctx context.Context, limitEnum int64, bucketKey string, enabled bool) (*emptypb.Empty, error) { + overrideColumnsList, err := ssa.dbMap.ColumnsForModel(overrideModel{}) + if err != nil { + // This should never happen, the model is registered at init time. + return nil, fmt.Errorf("getting columns for override model: %w", err) + } + overrideColumns := strings.Join(overrideColumnsList, ", ") + _, err = db.WithTransaction(ctx, ssa.dbMap, func(tx db.Executor) (any, error) { + var existing overrideModel + err := tx.SelectOne(ctx, &existing, + // Use SELECT FOR UPDATE to both verify the row exists and lock it + // for the duration of the transaction. + `SELECT `+overrideColumns+` FROM overrides + WHERE limitEnum = ? AND + bucketKey = ? + FOR UPDATE`, + limitEnum, + bucketKey, + ) + if err != nil { + if db.IsNoRows(err) { + return nil, berrors.NotFoundError( + "no rate limit override found for limit %d and bucket key %s", + limitEnum, + bucketKey, + ) + } + return nil, fmt.Errorf("querying status of override for rate limit %d and bucket key %s: %w", + limitEnum, + bucketKey, + err, + ) + } + + if existing.Enabled == enabled { + // No-op + return nil, nil + } + + // Update the existing overrides row. + updated := existing + updated.Enabled = enabled + updated.UpdatedAt = ssa.clk.Now() + + _, err = tx.Update(ctx, &updated) + if err != nil { + return nil, fmt.Errorf("updating status of override for rate limit %d and bucket key %s to %t: %w", + limitEnum, + bucketKey, + enabled, + err, + ) + } + return nil, nil + }) if err != nil { return nil, err } - - return nil, nil + return &emptypb.Empty{}, nil +} + +// DisableRateLimitOverride disables a rate limit override. If the override does +// not exist, a NotFoundError is returned. If the override exists but is already +// disabled, this is a no-op. +func (ssa *SQLStorageAuthority) DisableRateLimitOverride(ctx context.Context, req *sapb.DisableRateLimitOverrideRequest) (*emptypb.Empty, error) { + if core.IsAnyNilOrZero(req, req.LimitEnum, req.BucketKey) { + return nil, errIncompleteRequest + } + return ssa.setRateLimitOverride(ctx, req.LimitEnum, req.BucketKey, false) +} + +// EnableRateLimitOverride enables a rate limit override. If the override does +// not exist, a NotFoundError is returned. If the override exists but is already +// enabled, this is a no-op. +func (ssa *SQLStorageAuthority) EnableRateLimitOverride(ctx context.Context, req *sapb.EnableRateLimitOverrideRequest) (*emptypb.Empty, error) { + if core.IsAnyNilOrZero(req, req.LimitEnum, req.BucketKey) { + return nil, errIncompleteRequest + } + return ssa.setRateLimitOverride(ctx, req.LimitEnum, req.BucketKey, true) } diff --git a/third-party/github.com/letsencrypt/boulder/sa/sa_test.go b/third-party/github.com/letsencrypt/boulder/sa/sa_test.go index 74f244c98..570712d5b 100644 --- a/third-party/github.com/letsencrypt/boulder/sa/sa_test.go +++ b/third-party/github.com/letsencrypt/boulder/sa/sa_test.go @@ -3,8 +3,9 @@ package sa import ( "bytes" "context" + "crypto/ecdsa" + "crypto/elliptic" "crypto/rand" - "crypto/rsa" "crypto/sha256" "crypto/x509" "database/sql" @@ -12,15 +13,16 @@ import ( "encoding/json" "errors" "fmt" + "io" "math/big" "math/bits" - mrand "math/rand" - "net" + mrand "math/rand/v2" + "net/netip" "os" "reflect" "slices" + "strconv" "strings" - "sync" "testing" "time" @@ -61,6 +63,18 @@ var ( }` ) +func mustTime(s string) time.Time { + t, err := time.Parse("2006-01-02 15:04", s) + if err != nil { + panic(fmt.Sprintf("parsing %q: %s", s, err)) + } + return t.UTC() +} + +func mustTimestamp(s string) *timestamppb.Timestamp { + return timestamppb.New(mustTime(s)) +} + type fakeServerStream[T any] struct { grpc.ServerStream output chan<- *T @@ -77,7 +91,7 @@ func (s *fakeServerStream[T]) Context() context.Context { // initSA constructs a SQLStorageAuthority and a clean up function that should // be defer'ed to the end of the test. -func initSA(t *testing.T) (*SQLStorageAuthority, clock.FakeClock, func()) { +func initSA(t testing.TB) (*SQLStorageAuthority, clock.FakeClock, func()) { t.Helper() features.Reset() @@ -92,7 +106,7 @@ func initSA(t *testing.T) (*SQLStorageAuthority, clock.FakeClock, func()) { } fc := clock.NewFake() - fc.Set(time.Date(2015, 3, 4, 5, 0, 0, 0, time.UTC)) + fc.Set(mustTime("2015-03-04 05:00")) saro, err := NewSQLStorageAuthorityRO(dbMap, dbIncidentsMap, metrics.NoopRegisterer, 1, 0, fc, log) if err != nil { @@ -109,13 +123,11 @@ func initSA(t *testing.T) (*SQLStorageAuthority, clock.FakeClock, func()) { // CreateWorkingTestRegistration inserts a new, correct Registration into the // given SA. -func createWorkingRegistration(t *testing.T, sa *SQLStorageAuthority) *corepb.Registration { - initialIP, _ := net.ParseIP("88.77.66.11").MarshalText() +func createWorkingRegistration(t testing.TB, sa *SQLStorageAuthority) *corepb.Registration { reg, err := sa.NewRegistration(context.Background(), &corepb.Registration{ Key: []byte(theKey), Contact: []string{"mailto:foo@example.com"}, - InitialIP: initialIP, - CreatedAt: timestamppb.New(time.Date(2003, 5, 10, 0, 0, 0, 0, time.UTC)), + CreatedAt: mustTimestamp("2003-05-10 00:00"), Status: string(core.StatusValid), }) if err != nil { @@ -124,7 +136,7 @@ func createWorkingRegistration(t *testing.T, sa *SQLStorageAuthority) *corepb.Re return reg } -func createPendingAuthorization(t *testing.T, sa *SQLStorageAuthority, domain string, exp time.Time) int64 { +func createPendingAuthorization(t *testing.T, sa *SQLStorageAuthority, ident identifier.ACMEIdentifier, exp time.Time) int64 { t.Helper() tokenStr := core.NewToken() @@ -132,8 +144,8 @@ func createPendingAuthorization(t *testing.T, sa *SQLStorageAuthority, domain st test.AssertNotError(t, err, "computing test authorization challenge token") am := authzModel{ - IdentifierType: 0, // dnsName - IdentifierValue: domain, + IdentifierType: identifierTypeToUint[string(ident.Type)], + IdentifierValue: ident.Value, RegistrationID: 1, Status: statusToUint[core.StatusPending], Expires: exp, @@ -147,10 +159,10 @@ func createPendingAuthorization(t *testing.T, sa *SQLStorageAuthority, domain st return am.ID } -func createFinalizedAuthorization(t *testing.T, sa *SQLStorageAuthority, domain string, exp time.Time, +func createFinalizedAuthorization(t *testing.T, sa *SQLStorageAuthority, ident identifier.ACMEIdentifier, exp time.Time, status string, attemptedAt time.Time) int64 { t.Helper() - pendingID := createPendingAuthorization(t, sa, domain, exp) + pendingID := createPendingAuthorization(t, sa, ident, exp) attempted := string(core.ChallengeTypeHTTP01) _, err := sa.FinalizeAuthorization2(context.Background(), &sapb.FinalizeAuthorizationRequest{ Id: pendingID, @@ -176,25 +188,18 @@ func TestAddRegistration(t *testing.T) { sa, clk, cleanUp := initSA(t) defer cleanUp() - jwk := goodTestJWK() - jwkJSON, _ := jwk.MarshalJSON() - - contacts := []string{"mailto:foo@example.com"} - initialIP, _ := net.ParseIP("43.34.43.34").MarshalText() + jwkJSON, _ := goodTestJWK().MarshalJSON() reg, err := sa.NewRegistration(ctx, &corepb.Registration{ - Key: jwkJSON, - Contact: contacts, - InitialIP: initialIP, + Key: jwkJSON, + Contact: []string{"mailto:foo@example.com"}, }) if err != nil { t.Fatalf("Couldn't create new registration: %s", err) } test.Assert(t, reg.Id != 0, "ID shouldn't be 0") - test.AssertDeepEquals(t, reg.Contact, contacts) - - _, err = sa.GetRegistration(ctx, &sapb.RegistrationID{Id: 0}) - test.AssertError(t, err, "Registration object for ID 0 was returned") + test.AssertEquals(t, len(reg.Contact), 0) + // Confirm that the registration can be retrieved by ID. dbReg, err := sa.GetRegistration(ctx, &sapb.RegistrationID{Id: reg.Id}) test.AssertNotError(t, err, fmt.Sprintf("Couldn't get registration with ID %v", reg.Id)) @@ -202,29 +207,22 @@ func TestAddRegistration(t *testing.T) { test.AssertEquals(t, dbReg.Id, reg.Id) test.AssertByteEquals(t, dbReg.Key, jwkJSON) test.AssertDeepEquals(t, dbReg.CreatedAt.AsTime(), createdAt) + test.AssertEquals(t, len(dbReg.Contact), 0) - initialIP, _ = net.ParseIP("72.72.72.72").MarshalText() - newReg := &corepb.Registration{ - Id: reg.Id, - Key: jwkJSON, - Contact: []string{"test.com"}, - InitialIP: initialIP, - Agreement: "yes", - } - _, err = sa.UpdateRegistration(ctx, newReg) - test.AssertNotError(t, err, fmt.Sprintf("Couldn't get registration with ID %v", reg.Id)) + _, err = sa.GetRegistration(ctx, &sapb.RegistrationID{Id: 0}) + test.AssertError(t, err, "Registration object for ID 0 was returned") + + // Confirm that the registration can be retrieved by key. dbReg, err = sa.GetRegistrationByKey(ctx, &sapb.JSONWebKey{Jwk: jwkJSON}) test.AssertNotError(t, err, "Couldn't get registration by key") - - test.AssertEquals(t, dbReg.Id, newReg.Id) - test.AssertEquals(t, dbReg.Agreement, newReg.Agreement) + test.AssertEquals(t, dbReg.Id, dbReg.Id) + test.AssertEquals(t, dbReg.Agreement, dbReg.Agreement) anotherKey := `{ "kty":"RSA", "n": "vd7rZIoTLEe-z1_8G1FcXSw9CQFEJgV4g9V277sER7yx5Qjz_Pkf2YVth6wwwFJEmzc0hoKY-MMYFNwBE4hQHw", "e":"AQAB" }` - _, err = sa.GetRegistrationByKey(ctx, &sapb.JSONWebKey{Jwk: []byte(anotherKey)}) test.AssertError(t, err, "Registration object for invalid key was returned") } @@ -242,8 +240,8 @@ func TestNoSuchRegistrationErrors(t *testing.T) { _, err = sa.GetRegistrationByKey(ctx, &sapb.JSONWebKey{Jwk: jwkJSON}) test.AssertErrorIs(t, err, berrors.NotFound) - _, err = sa.UpdateRegistration(ctx, &corepb.Registration{Id: 100, Key: jwkJSON, InitialIP: []byte("foo")}) - test.AssertErrorIs(t, err, berrors.NotFound) + _, err = sa.UpdateRegistrationKey(ctx, &sapb.UpdateRegistrationKeyRequest{RegistrationID: 100, Jwk: jwkJSON}) + test.AssertErrorIs(t, err, berrors.InternalServer) } func TestSelectRegistration(t *testing.T) { @@ -255,11 +253,9 @@ func TestSelectRegistration(t *testing.T) { sha, err := core.KeyDigestB64(jwk.Key) test.AssertNotError(t, err, "couldn't parse jwk.Key") - initialIP, _ := net.ParseIP("43.34.43.34").MarshalText() reg, err := sa.NewRegistration(ctx, &corepb.Registration{ - Key: jwkJSON, - Contact: []string{"mailto:foo@example.com"}, - InitialIP: initialIP, + Key: jwkJSON, + Contact: []string{"mailto:foo@example.com"}, }) test.AssertNotError(t, err, fmt.Sprintf("couldn't create new registration: %s", err)) test.Assert(t, reg.Id != 0, "ID shouldn't be 0") @@ -268,8 +264,6 @@ func TestSelectRegistration(t *testing.T) { test.AssertNotError(t, err, "selecting by id should work") _, err = selectRegistration(ctx, sa.dbMap, "jwk_sha256", sha) test.AssertNotError(t, err, "selecting by jwk_sha256 should work") - _, err = selectRegistration(ctx, sa.dbMap, "initialIP", reg.Id) - test.AssertError(t, err, "selecting by any other column should not work") } func TestReplicationLagRetries(t *testing.T) { @@ -316,7 +310,7 @@ func TestReplicationLagRetries(t *testing.T) { // findIssuedName is a small helper test function to directly query the // issuedNames table for a given name to find a serial (or return an err). -func findIssuedName(ctx context.Context, dbMap db.OneSelector, name string) (string, error) { +func findIssuedName(ctx context.Context, dbMap db.OneSelector, issuedName string) (string, error) { var issuedNamesSerial string err := dbMap.SelectOne( ctx, @@ -325,7 +319,7 @@ func findIssuedName(ctx context.Context, dbMap db.OneSelector, name string) (str WHERE reversedName = ? ORDER BY notBefore DESC LIMIT 1`, - ReverseName(name)) + issuedName) return issuedNamesSerial, err } @@ -415,11 +409,11 @@ func TestAddPrecertificate(t *testing.T) { // Add the cert as a precertificate regID := reg.Id - issuedTime := time.Date(2018, 4, 1, 7, 0, 0, 0, time.UTC) + issuedTime := mustTimestamp("2018-04-01 07:00") _, err := sa.AddPrecertificate(ctx, &sapb.AddCertificateRequest{ Der: testCert.Raw, RegID: regID, - Issued: timestamppb.New(issuedTime), + Issued: issuedTime, IssuerNameID: 1, }) test.AssertNotError(t, err, "Couldn't add test cert") @@ -432,7 +426,7 @@ func TestAddPrecertificate(t *testing.T) { test.AssertEquals(t, now, certStatus.OcspLastUpdated.AsTime()) // It should show up in the issued names table - issuedNamesSerial, err := findIssuedName(ctx, sa.dbMap, testCert.DNSNames[0]) + issuedNamesSerial, err := findIssuedName(ctx, sa.dbMap, reverseFQDN(testCert.DNSNames[0])) test.AssertNotError(t, err, "expected no err querying issuedNames for precert") test.AssertEquals(t, issuedNamesSerial, serial) @@ -442,7 +436,7 @@ func TestAddPrecertificate(t *testing.T) { _, err = sa.AddCertificate(ctx, &sapb.AddCertificateRequest{ Der: testCert.Raw, RegID: regID, - Issued: timestamppb.New(issuedTime), + Issued: issuedTime, }) test.AssertNotError(t, err, "unexpected err adding final cert after precert") } @@ -455,11 +449,11 @@ func TestAddPrecertificateNoOCSP(t *testing.T) { _, testCert := test.ThrowAwayCert(t, clk) regID := reg.Id - issuedTime := time.Date(2018, 4, 1, 7, 0, 0, 0, time.UTC) + issuedTime := mustTimestamp("2018-04-01 07:00") _, err := sa.AddPrecertificate(ctx, &sapb.AddCertificateRequest{ Der: testCert.Raw, RegID: regID, - Issued: timestamppb.New(issuedTime), + Issued: issuedTime, IssuerNameID: 1, }) test.AssertNotError(t, err, "Couldn't add test cert") @@ -503,11 +497,10 @@ func TestAddPrecertificateIncomplete(t *testing.T) { // Add the cert as a precertificate regID := reg.Id - issuedTime := time.Date(2018, 4, 1, 7, 0, 0, 0, time.UTC) _, err := sa.AddPrecertificate(ctx, &sapb.AddCertificateRequest{ Der: testCert.Raw, RegID: regID, - Issued: timestamppb.New(issuedTime), + Issued: mustTimestamp("2018-04-01 07:00"), // Leaving out IssuerNameID }) @@ -612,358 +605,6 @@ func TestAddCertificateDuplicate(t *testing.T) { } -func TestCountCertificatesByNamesTimeRange(t *testing.T) { - sa, clk, cleanUp := initSA(t) - defer cleanUp() - - reg := createWorkingRegistration(t, sa) - _, testCert := test.ThrowAwayCert(t, clk) - _, err := sa.AddCertificate(ctx, &sapb.AddCertificateRequest{ - Der: testCert.Raw, - RegID: reg.Id, - Issued: timestamppb.New(testCert.NotBefore), - }) - test.AssertNotError(t, err, "Couldn't add test cert") - name := testCert.DNSNames[0] - - // Move time forward, so the cert was issued slightly in the past. - clk.Add(time.Hour) - now := clk.Now() - yesterday := clk.Now().Add(-24 * time.Hour) - twoDaysAgo := clk.Now().Add(-48 * time.Hour) - tomorrow := clk.Now().Add(24 * time.Hour) - - // Count for a name that doesn't have any certs - counts, err := sa.CountCertificatesByNames(ctx, &sapb.CountCertificatesByNamesRequest{ - Names: []string{"does.not.exist"}, - Range: &sapb.Range{ - Earliest: timestamppb.New(yesterday), - Latest: timestamppb.New(now), - }, - }) - test.AssertNotError(t, err, "Error counting certs.") - test.AssertEquals(t, len(counts.Counts), 1) - test.AssertEquals(t, counts.Counts["does.not.exist"], int64(0)) - - // Time range including now should find the cert. - counts, err = sa.CountCertificatesByNames(ctx, &sapb.CountCertificatesByNamesRequest{ - Names: testCert.DNSNames, - Range: &sapb.Range{ - Earliest: timestamppb.New(yesterday), - Latest: timestamppb.New(now), - }, - }) - test.AssertNotError(t, err, "sa.CountCertificatesByName failed") - test.AssertEquals(t, len(counts.Counts), 1) - test.AssertEquals(t, counts.Counts[name], int64(1)) - - // Time range between two days ago and yesterday should not find the cert. - counts, err = sa.CountCertificatesByNames(ctx, &sapb.CountCertificatesByNamesRequest{ - Names: testCert.DNSNames, - Range: &sapb.Range{ - Earliest: timestamppb.New(twoDaysAgo), - Latest: timestamppb.New(yesterday), - }, - }) - test.AssertNotError(t, err, "Error counting certs.") - test.AssertEquals(t, len(counts.Counts), 1) - test.AssertEquals(t, counts.Counts[name], int64(0)) - - // Time range between now and tomorrow also should not (time ranges are - // inclusive at the tail end, but not the beginning end). - counts, err = sa.CountCertificatesByNames(ctx, &sapb.CountCertificatesByNamesRequest{ - Names: testCert.DNSNames, - Range: &sapb.Range{ - Earliest: timestamppb.New(now), - Latest: timestamppb.New(tomorrow), - }, - }) - test.AssertNotError(t, err, "Error counting certs.") - test.AssertEquals(t, len(counts.Counts), 1) - test.AssertEquals(t, counts.Counts[name], int64(0)) -} - -func TestCountCertificatesByNamesParallel(t *testing.T) { - sa, clk, cleanUp := initSA(t) - defer cleanUp() - - // Create two certs with different names and add them both to the database. - reg := createWorkingRegistration(t, sa) - - _, testCert := test.ThrowAwayCert(t, clk) - _, err := sa.AddCertificate(ctx, &sapb.AddCertificateRequest{ - Der: testCert.Raw, - RegID: reg.Id, - Issued: timestamppb.New(testCert.NotBefore), - }) - test.AssertNotError(t, err, "Couldn't add test cert") - - _, testCert2 := test.ThrowAwayCert(t, clk) - _, err = sa.AddCertificate(ctx, &sapb.AddCertificateRequest{ - Der: testCert2.Raw, - RegID: reg.Id, - Issued: timestamppb.New(testCert2.NotBefore), - }) - test.AssertNotError(t, err, "Couldn't add test cert") - - // Override countCertificatesByName with an implementation of certCountFunc - // that will block forever if it's called in serial, but will succeed if - // called in parallel. - names := []string{"does.not.exist", testCert.DNSNames[0], testCert2.DNSNames[0]} - - var interlocker sync.WaitGroup - interlocker.Add(len(names)) - sa.parallelismPerRPC = len(names) - oldCertCountFunc := sa.countCertificatesByName - sa.countCertificatesByName = func(ctx context.Context, sel db.Selector, domain string, timeRange *sapb.Range) (int64, time.Time, error) { - interlocker.Done() - interlocker.Wait() - return oldCertCountFunc(ctx, sel, domain, timeRange) - } - - counts, err := sa.CountCertificatesByNames(ctx, &sapb.CountCertificatesByNamesRequest{ - Names: names, - Range: &sapb.Range{ - Earliest: timestamppb.New(clk.Now().Add(-time.Hour)), - Latest: timestamppb.New(clk.Now().Add(time.Hour)), - }, - }) - test.AssertNotError(t, err, "Error counting certs.") - test.AssertEquals(t, len(counts.Counts), 3) - - // We expect there to be two of each of the names that do exist, because - // test.ThrowAwayCert creates certs for subdomains of example.com, and - // CountCertificatesByNames counts all certs under the same registered domain. - expected := map[string]int64{ - "does.not.exist": 0, - testCert.DNSNames[0]: 2, - testCert2.DNSNames[0]: 2, - } - for name, count := range expected { - test.AssertEquals(t, count, counts.Counts[name]) - } -} - -func TestCountRegistrationsByIP(t *testing.T) { - sa, fc, cleanUp := initSA(t) - defer cleanUp() - - contact := []string{"mailto:foo@example.com"} - - // Create one IPv4 registration - key, _ := jose.JSONWebKey{Key: &rsa.PublicKey{N: big.NewInt(1), E: 1}}.MarshalJSON() - initialIP, _ := net.ParseIP("43.34.43.34").MarshalText() - _, err := sa.NewRegistration(ctx, &corepb.Registration{ - Key: key, - InitialIP: initialIP, - Contact: contact, - }) - // Create two IPv6 registrations, both within the same /48 - key, _ = jose.JSONWebKey{Key: &rsa.PublicKey{N: big.NewInt(2), E: 1}}.MarshalJSON() - initialIP, _ = net.ParseIP("2001:cdba:1234:5678:9101:1121:3257:9652").MarshalText() - test.AssertNotError(t, err, "Couldn't insert registration") - _, err = sa.NewRegistration(ctx, &corepb.Registration{ - Key: key, - InitialIP: initialIP, - Contact: contact, - }) - test.AssertNotError(t, err, "Couldn't insert registration") - key, _ = jose.JSONWebKey{Key: &rsa.PublicKey{N: big.NewInt(3), E: 1}}.MarshalJSON() - initialIP, _ = net.ParseIP("2001:cdba:1234:5678:9101:1121:3257:9653").MarshalText() - _, err = sa.NewRegistration(ctx, &corepb.Registration{ - Key: key, - InitialIP: initialIP, - Contact: contact, - }) - test.AssertNotError(t, err, "Couldn't insert registration") - - latest := fc.Now() - earliest := latest.Add(-time.Hour * 24) - req := &sapb.CountRegistrationsByIPRequest{ - Ip: net.ParseIP("1.1.1.1"), - Range: &sapb.Range{ - Earliest: timestamppb.New(earliest), - Latest: timestamppb.New(latest), - }, - } - - // There should be 0 registrations for an IPv4 address we didn't add - // a registration for - count, err := sa.CountRegistrationsByIP(ctx, req) - test.AssertNotError(t, err, "Failed to count registrations") - test.AssertEquals(t, count.Count, int64(0)) - // There should be 1 registration for the IPv4 address we did add - // a registration for. - req.Ip = net.ParseIP("43.34.43.34") - count, err = sa.CountRegistrationsByIP(ctx, req) - test.AssertNotError(t, err, "Failed to count registrations") - test.AssertEquals(t, count.Count, int64(1)) - // There should be 1 registration for the first IPv6 address we added - // a registration for - req.Ip = net.ParseIP("2001:cdba:1234:5678:9101:1121:3257:9652") - count, err = sa.CountRegistrationsByIP(ctx, req) - test.AssertNotError(t, err, "Failed to count registrations") - test.AssertEquals(t, count.Count, int64(1)) - // There should be 1 registration for the second IPv6 address we added - // a registration for as well - req.Ip = net.ParseIP("2001:cdba:1234:5678:9101:1121:3257:9653") - count, err = sa.CountRegistrationsByIP(ctx, req) - test.AssertNotError(t, err, "Failed to count registrations") - test.AssertEquals(t, count.Count, int64(1)) - // There should be 0 registrations for an IPv6 address in the same /48 as the - // two IPv6 addresses with registrations - req.Ip = net.ParseIP("2001:cdba:1234:0000:0000:0000:0000:0000") - count, err = sa.CountRegistrationsByIP(ctx, req) - test.AssertNotError(t, err, "Failed to count registrations") - test.AssertEquals(t, count.Count, int64(0)) -} - -func TestCountRegistrationsByIPRange(t *testing.T) { - sa, fc, cleanUp := initSA(t) - defer cleanUp() - - contact := []string{"mailto:foo@example.com"} - - // Create one IPv4 registration - key, _ := jose.JSONWebKey{Key: &rsa.PublicKey{N: big.NewInt(1), E: 1}}.MarshalJSON() - initialIP, _ := net.ParseIP("43.34.43.34").MarshalText() - _, err := sa.NewRegistration(ctx, &corepb.Registration{ - Key: key, - InitialIP: initialIP, - Contact: contact, - }) - // Create two IPv6 registrations, both within the same /48 - key, _ = jose.JSONWebKey{Key: &rsa.PublicKey{N: big.NewInt(2), E: 1}}.MarshalJSON() - initialIP, _ = net.ParseIP("2001:cdba:1234:5678:9101:1121:3257:9652").MarshalText() - test.AssertNotError(t, err, "Couldn't insert registration") - _, err = sa.NewRegistration(ctx, &corepb.Registration{ - Key: key, - InitialIP: initialIP, - Contact: contact, - }) - test.AssertNotError(t, err, "Couldn't insert registration") - key, _ = jose.JSONWebKey{Key: &rsa.PublicKey{N: big.NewInt(3), E: 1}}.MarshalJSON() - initialIP, _ = net.ParseIP("2001:cdba:1234:5678:9101:1121:3257:9653").MarshalText() - _, err = sa.NewRegistration(ctx, &corepb.Registration{ - Key: key, - InitialIP: initialIP, - Contact: contact, - }) - test.AssertNotError(t, err, "Couldn't insert registration") - - latest := fc.Now() - earliest := latest.Add(-time.Hour * 24) - req := &sapb.CountRegistrationsByIPRequest{ - Ip: net.ParseIP("1.1.1.1"), - Range: &sapb.Range{ - Earliest: timestamppb.New(earliest), - Latest: timestamppb.New(latest), - }, - } - - // There should be 0 registrations in the range for an IPv4 address we didn't - // add a registration for - req.Ip = net.ParseIP("1.1.1.1") - count, err := sa.CountRegistrationsByIPRange(ctx, req) - test.AssertNotError(t, err, "Failed to count registrations") - test.AssertEquals(t, count.Count, int64(0)) - // There should be 1 registration in the range for the IPv4 address we did - // add a registration for - req.Ip = net.ParseIP("43.34.43.34") - count, err = sa.CountRegistrationsByIPRange(ctx, req) - test.AssertNotError(t, err, "Failed to count registrations") - test.AssertEquals(t, count.Count, int64(1)) - // There should be 2 registrations in the range for the first IPv6 address we added - // a registration for because it's in the same /48 - req.Ip = net.ParseIP("2001:cdba:1234:5678:9101:1121:3257:9652") - count, err = sa.CountRegistrationsByIPRange(ctx, req) - test.AssertNotError(t, err, "Failed to count registrations") - test.AssertEquals(t, count.Count, int64(2)) - // There should be 2 registrations in the range for the second IPv6 address - // we added a registration for as well, because it too is in the same /48 - req.Ip = net.ParseIP("2001:cdba:1234:5678:9101:1121:3257:9653") - count, err = sa.CountRegistrationsByIPRange(ctx, req) - test.AssertNotError(t, err, "Failed to count registrations") - test.AssertEquals(t, count.Count, int64(2)) - // There should also be 2 registrations in the range for an arbitrary IPv6 address in - // the same /48 as the registrations we added - req.Ip = net.ParseIP("2001:cdba:1234:0000:0000:0000:0000:0000") - count, err = sa.CountRegistrationsByIPRange(ctx, req) - test.AssertNotError(t, err, "Failed to count registrations") - test.AssertEquals(t, count.Count, int64(2)) -} - -func TestFQDNSets(t *testing.T) { - ctx := context.Background() - sa, fc, cleanUp := initSA(t) - defer cleanUp() - - tx, err := sa.dbMap.BeginTx(ctx) - test.AssertNotError(t, err, "Failed to open transaction") - names := []string{"a.example.com", "B.example.com"} - expires := fc.Now().Add(time.Hour * 2).UTC() - issued := fc.Now() - err = addFQDNSet(ctx, tx, names, "serial", issued, expires) - test.AssertNotError(t, err, "Failed to add name set") - test.AssertNotError(t, tx.Commit(), "Failed to commit transaction") - - // Invalid Window - req := &sapb.CountFQDNSetsRequest{ - Domains: names, - Window: nil, - } - _, err = sa.CountFQDNSets(ctx, req) - test.AssertErrorIs(t, err, errIncompleteRequest) - - threeHours := time.Hour * 3 - req = &sapb.CountFQDNSetsRequest{ - Domains: names, - Window: durationpb.New(threeHours), - } - // only one valid - count, err := sa.CountFQDNSets(ctx, req) - test.AssertNotError(t, err, "Failed to count name sets") - test.AssertEquals(t, count.Count, int64(1)) - - // check hash isn't affected by changing name order/casing - req.Domains = []string{"b.example.com", "A.example.COM"} - count, err = sa.CountFQDNSets(ctx, req) - test.AssertNotError(t, err, "Failed to count name sets") - test.AssertEquals(t, count.Count, int64(1)) - - // add another valid set - tx, err = sa.dbMap.BeginTx(ctx) - test.AssertNotError(t, err, "Failed to open transaction") - err = addFQDNSet(ctx, tx, names, "anotherSerial", issued, expires) - test.AssertNotError(t, err, "Failed to add name set") - test.AssertNotError(t, tx.Commit(), "Failed to commit transaction") - - // only two valid - req.Domains = names - count, err = sa.CountFQDNSets(ctx, req) - test.AssertNotError(t, err, "Failed to count name sets") - test.AssertEquals(t, count.Count, int64(2)) - - // add an expired set - tx, err = sa.dbMap.BeginTx(ctx) - test.AssertNotError(t, err, "Failed to open transaction") - err = addFQDNSet( - ctx, - tx, - names, - "yetAnotherSerial", - issued.Add(-threeHours), - expires.Add(-threeHours), - ) - test.AssertNotError(t, err, "Failed to add name set") - test.AssertNotError(t, tx.Commit(), "Failed to commit transaction") - - // only two valid - count, err = sa.CountFQDNSets(ctx, req) - test.AssertNotError(t, err, "Failed to count name sets") - test.AssertEquals(t, count.Count, int64(2)) -} - func TestFQDNSetTimestampsForWindow(t *testing.T) { sa, fc, cleanUp := initSA(t) defer cleanUp() @@ -971,20 +612,23 @@ func TestFQDNSetTimestampsForWindow(t *testing.T) { tx, err := sa.dbMap.BeginTx(ctx) test.AssertNotError(t, err, "Failed to open transaction") - names := []string{"a.example.com", "B.example.com"} + idents := identifier.ACMEIdentifiers{ + identifier.NewDNS("a.example.com"), + identifier.NewDNS("B.example.com"), + } // Invalid Window req := &sapb.CountFQDNSetsRequest{ - Domains: names, - Window: nil, + Identifiers: idents.ToProtoSlice(), + Window: nil, } _, err = sa.FQDNSetTimestampsForWindow(ctx, req) test.AssertErrorIs(t, err, errIncompleteRequest) window := time.Hour * 3 req = &sapb.CountFQDNSetsRequest{ - Domains: names, - Window: durationpb.New(window), + Identifiers: idents.ToProtoSlice(), + Window: durationpb.New(window), } // Ensure zero issuance has occurred for names. @@ -995,7 +639,7 @@ func TestFQDNSetTimestampsForWindow(t *testing.T) { // Add an issuance for names inside the window. expires := fc.Now().Add(time.Hour * 2).UTC() firstIssued := fc.Now() - err = addFQDNSet(ctx, tx, names, "serial", firstIssued, expires) + err = addFQDNSet(ctx, tx, idents, "serial", firstIssued, expires) test.AssertNotError(t, err, "Failed to add name set") test.AssertNotError(t, tx.Commit(), "Failed to commit transaction") @@ -1006,7 +650,10 @@ func TestFQDNSetTimestampsForWindow(t *testing.T) { test.AssertEquals(t, firstIssued, resp.Timestamps[len(resp.Timestamps)-1].AsTime()) // Ensure that the hash isn't affected by changing name order/casing. - req.Domains = []string{"b.example.com", "A.example.COM"} + req.Identifiers = []*corepb.Identifier{ + identifier.NewDNS("b.example.com").ToProto(), + identifier.NewDNS("A.example.COM").ToProto(), + } resp, err = sa.FQDNSetTimestampsForWindow(ctx, req) test.AssertNotError(t, err, "Failed to count name sets") test.AssertEquals(t, len(resp.Timestamps), 1) @@ -1015,12 +662,12 @@ func TestFQDNSetTimestampsForWindow(t *testing.T) { // Add another issuance for names inside the window. tx, err = sa.dbMap.BeginTx(ctx) test.AssertNotError(t, err, "Failed to open transaction") - err = addFQDNSet(ctx, tx, names, "anotherSerial", firstIssued, expires) + err = addFQDNSet(ctx, tx, idents, "anotherSerial", firstIssued, expires) test.AssertNotError(t, err, "Failed to add name set") test.AssertNotError(t, tx.Commit(), "Failed to commit transaction") // Ensure there are two issuance timestamps for names inside the window. - req.Domains = names + req.Identifiers = idents.ToProtoSlice() resp, err = sa.FQDNSetTimestampsForWindow(ctx, req) test.AssertNotError(t, err, "Failed to count name sets") test.AssertEquals(t, len(resp.Timestamps), 2) @@ -1029,7 +676,7 @@ func TestFQDNSetTimestampsForWindow(t *testing.T) { // Add another issuance for names but just outside the window. tx, err = sa.dbMap.BeginTx(ctx) test.AssertNotError(t, err, "Failed to open transaction") - err = addFQDNSet(ctx, tx, names, "yetAnotherSerial", firstIssued.Add(-window), expires) + err = addFQDNSet(ctx, tx, idents, "yetAnotherSerial", firstIssued.Add(-window), expires) test.AssertNotError(t, err, "Failed to add name set") test.AssertNotError(t, tx.Commit(), "Failed to commit transaction") @@ -1038,14 +685,27 @@ func TestFQDNSetTimestampsForWindow(t *testing.T) { test.AssertNotError(t, err, "Failed to count name sets") test.AssertEquals(t, len(resp.Timestamps), 2) test.AssertEquals(t, firstIssued, resp.Timestamps[len(resp.Timestamps)-1].AsTime()) + + resp, err = sa.FQDNSetTimestampsForWindow(ctx, &sapb.CountFQDNSetsRequest{ + Identifiers: idents.ToProtoSlice(), + Window: durationpb.New(window), + Limit: 1, + }) + test.AssertNotError(t, err, "Failed to count name sets") + test.AssertEquals(t, len(resp.Timestamps), 1) + test.AssertEquals(t, firstIssued, resp.Timestamps[len(resp.Timestamps)-1].AsTime()) } -func TestFQDNSetsExists(t *testing.T) { +func TestFQDNSetExists(t *testing.T) { sa, fc, cleanUp := initSA(t) defer cleanUp() - names := []string{"a.example.com", "B.example.com"} - exists, err := sa.FQDNSetExists(ctx, &sapb.FQDNSetExistsRequest{Domains: names}) + idents := identifier.ACMEIdentifiers{ + identifier.NewDNS("a.example.com"), + identifier.NewDNS("B.example.com"), + } + + exists, err := sa.FQDNSetExists(ctx, &sapb.FQDNSetExistsRequest{Identifiers: idents.ToProtoSlice()}) test.AssertNotError(t, err, "Failed to check FQDN set existence") test.Assert(t, !exists.Exists, "FQDN set shouldn't exist") @@ -1053,30 +713,44 @@ func TestFQDNSetsExists(t *testing.T) { test.AssertNotError(t, err, "Failed to open transaction") expires := fc.Now().Add(time.Hour * 2).UTC() issued := fc.Now() - err = addFQDNSet(ctx, tx, names, "serial", issued, expires) + err = addFQDNSet(ctx, tx, idents, "serial", issued, expires) test.AssertNotError(t, err, "Failed to add name set") test.AssertNotError(t, tx.Commit(), "Failed to commit transaction") - exists, err = sa.FQDNSetExists(ctx, &sapb.FQDNSetExistsRequest{Domains: names}) + exists, err = sa.FQDNSetExists(ctx, &sapb.FQDNSetExistsRequest{Identifiers: idents.ToProtoSlice()}) test.AssertNotError(t, err, "Failed to check FQDN set existence") test.Assert(t, exists.Exists, "FQDN set does exist") } -type queryRecorder struct { - query string - args []interface{} +type execRecorder struct { + valuesPerRow int + query string + args []interface{} } -func (e *queryRecorder) QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) { +func (e *execRecorder) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) { e.query = query e.args = args - return nil, nil + return rowsResult{int64(len(args) / e.valuesPerRow)}, nil +} + +type rowsResult struct { + rowsAffected int64 +} + +func (r rowsResult) LastInsertId() (int64, error) { + return r.rowsAffected, nil +} + +func (r rowsResult) RowsAffected() (int64, error) { + return r.rowsAffected, nil } func TestAddIssuedNames(t *testing.T) { serial := big.NewInt(1) expectedSerial := "000000000000000000000000000000000001" - notBefore := time.Date(2018, 2, 14, 12, 0, 0, 0, time.UTC) + notBefore := mustTime("2018-02-14 12:00") + expectedNotBefore := notBefore.Truncate(24 * time.Hour) placeholdersPerName := "(?,?,?,?)" baseQuery := "INSERT INTO issuedNames (reversedName,serial,notBefore,renewal) VALUES" @@ -1097,7 +771,7 @@ func TestAddIssuedNames(t *testing.T) { ExpectedArgs: []interface{}{ "uk.co.example", expectedSerial, - notBefore, + expectedNotBefore, false, }, }, @@ -1110,11 +784,11 @@ func TestAddIssuedNames(t *testing.T) { ExpectedArgs: []interface{}{ "uk.co.example", expectedSerial, - notBefore, + expectedNotBefore, false, "xyz.example", expectedSerial, - notBefore, + expectedNotBefore, false, }, }, @@ -1127,7 +801,7 @@ func TestAddIssuedNames(t *testing.T) { ExpectedArgs: []interface{}{ "uk.co.example", expectedSerial, - notBefore, + expectedNotBefore, true, }, }, @@ -1140,11 +814,11 @@ func TestAddIssuedNames(t *testing.T) { ExpectedArgs: []interface{}{ "uk.co.example", expectedSerial, - notBefore, + expectedNotBefore, true, "xyz.example", expectedSerial, - notBefore, + expectedNotBefore, true, }, }, @@ -1152,7 +826,7 @@ func TestAddIssuedNames(t *testing.T) { for _, tc := range testCases { t.Run(tc.Name, func(t *testing.T) { - var e queryRecorder + e := execRecorder{valuesPerRow: 4} err := addIssuedNames( ctx, &e, @@ -1183,12 +857,12 @@ func TestDeactivateAuthorization2(t *testing.T) { // deactivate a pending authorization expires := fc.Now().Add(time.Hour).UTC() attemptedAt := fc.Now() - authzID := createPendingAuthorization(t, sa, "example.com", expires) + authzID := createPendingAuthorization(t, sa, identifier.NewDNS("example.com"), expires) _, err := sa.DeactivateAuthorization2(context.Background(), &sapb.AuthorizationID2{Id: authzID}) test.AssertNotError(t, err, "sa.DeactivateAuthorization2 failed") - // deactivate a valid authorization" - authzID = createFinalizedAuthorization(t, sa, "example.com", expires, "valid", attemptedAt) + // deactivate a valid authorization + authzID = createFinalizedAuthorization(t, sa, identifier.NewDNS("example.com"), expires, "valid", attemptedAt) _, err = sa.DeactivateAuthorization2(context.Background(), &sapb.AuthorizationID2{Id: authzID}) test.AssertNotError(t, err, "sa.DeactivateAuthorization2 failed") } @@ -1199,18 +873,37 @@ func TestDeactivateAccount(t *testing.T) { reg := createWorkingRegistration(t, sa) - _, err := sa.DeactivateRegistration(context.Background(), &sapb.RegistrationID{Id: reg.Id}) - test.AssertNotError(t, err, "DeactivateRegistration failed") + // An incomplete request should be rejected. + _, err := sa.DeactivateRegistration(context.Background(), &sapb.RegistrationID{}) + test.AssertError(t, err, "Incomplete request should fail") + test.AssertContains(t, err.Error(), "incomplete") - dbReg, err := sa.GetRegistration(context.Background(), &sapb.RegistrationID{Id: reg.Id}) + // Deactivating should work, and return the same account but with updated + // status and cleared contacts. + got, err := sa.DeactivateRegistration(context.Background(), &sapb.RegistrationID{Id: reg.Id}) + test.AssertNotError(t, err, "DeactivateRegistration failed") + test.AssertEquals(t, got.Id, reg.Id) + test.AssertEquals(t, core.AcmeStatus(got.Status), core.StatusDeactivated) + test.AssertEquals(t, len(got.Contact), 0) + + // Double-check that the DeactivateRegistration method returned the right + // thing, by fetching the same account ourselves. + got, err = sa.GetRegistration(context.Background(), &sapb.RegistrationID{Id: reg.Id}) test.AssertNotError(t, err, "GetRegistration failed") - test.AssertEquals(t, core.AcmeStatus(dbReg.Status), core.StatusDeactivated) + test.AssertEquals(t, got.Id, reg.Id) + test.AssertEquals(t, core.AcmeStatus(got.Status), core.StatusDeactivated) + test.AssertEquals(t, len(got.Contact), 0) + + // Attempting to deactivate it a second time should fail, since it is already + // deactivated. + _, err = sa.DeactivateRegistration(context.Background(), &sapb.RegistrationID{Id: reg.Id}) + test.AssertError(t, err, "Deactivating an already-deactivated account should fail") } -func TestReverseName(t *testing.T) { +func TestReverseFQDN(t *testing.T) { testCases := []struct { - inputDomain string - inputReversed string + fqdn string + reversed string }{ {"", ""}, {"...", "..."}, @@ -1221,8 +914,46 @@ func TestReverseName(t *testing.T) { } for _, tc := range testCases { - output := ReverseName(tc.inputDomain) - test.AssertEquals(t, output, tc.inputReversed) + output := reverseFQDN(tc.fqdn) + test.AssertEquals(t, output, tc.reversed) + + output = reverseFQDN(tc.reversed) + test.AssertEquals(t, output, tc.fqdn) + } +} + +func TestEncodeIssuedName(t *testing.T) { + testCases := []struct { + issuedName string + reversed string + oneWay bool + }{ + // Empty strings and bare separators/TLDs should be unchanged. + {"", "", false}, + {"...", "...", false}, + {"com", "com", false}, + // FQDNs should be reversed. + {"example.com", "com.example", false}, + {"www.example.com", "com.example.www", false}, + {"world.wide.web.example.com", "com.example.web.wide.world", false}, + // IP addresses should stay the same. + {"1.2.3.4", "1.2.3.4", false}, + {"2602:ff3a:1:abad:c0f:fee:abad:cafe", "2602:ff3a:1:abad:c0f:fee:abad:cafe", false}, + // Tricksy FQDNs that look like IPv6 addresses should be parsed as FQDNs. + {"2602.ff3a.1.abad.c0f.fee.abad.cafe", "cafe.abad.fee.c0f.abad.1.ff3a.2602", false}, + {"2602.ff3a.0001.abad.0c0f.0fee.abad.cafe", "cafe.abad.0fee.0c0f.abad.0001.ff3a.2602", false}, + // IPv6 addresses should be returned in RFC 5952 format. + {"2602:ff3a:0001:abad:0c0f:0fee:abad:cafe", "2602:ff3a:1:abad:c0f:fee:abad:cafe", true}, + } + + for _, tc := range testCases { + output := EncodeIssuedName(tc.issuedName) + test.AssertEquals(t, output, tc.reversed) + + if !tc.oneWay { + output = EncodeIssuedName(tc.reversed) + test.AssertEquals(t, output, tc.issuedName) + } } } @@ -1230,50 +961,49 @@ func TestNewOrderAndAuthzs(t *testing.T) { sa, _, cleanup := initSA(t) defer cleanup() - // Create a test registration to reference - key, _ := jose.JSONWebKey{Key: &rsa.PublicKey{N: big.NewInt(1), E: 1}}.MarshalJSON() - initialIP, _ := net.ParseIP("42.42.42.42").MarshalText() - reg, err := sa.NewRegistration(ctx, &corepb.Registration{ - Key: key, - InitialIP: initialIP, - }) - test.AssertNotError(t, err, "Couldn't create test registration") + reg := createWorkingRegistration(t, sa) // Insert two pre-existing authorizations to reference - idA := createPendingAuthorization(t, sa, "a.com", sa.clk.Now().Add(time.Hour)) - idB := createPendingAuthorization(t, sa, "b.com", sa.clk.Now().Add(time.Hour)) + idA := createPendingAuthorization(t, sa, identifier.NewDNS("a.com"), sa.clk.Now().Add(time.Hour)) + idB := createPendingAuthorization(t, sa, identifier.NewDNS("b.com"), sa.clk.Now().Add(time.Hour)) test.AssertEquals(t, idA, int64(1)) test.AssertEquals(t, idB, int64(2)) nowC := sa.clk.Now().Add(time.Hour) nowD := sa.clk.Now().Add(time.Hour) expires := sa.clk.Now().Add(2 * time.Hour) - order, err := sa.NewOrderAndAuthzs(context.Background(), &sapb.NewOrderAndAuthzsRequest{ + req := &sapb.NewOrderAndAuthzsRequest{ // Insert an order for four names, two of which already have authzs NewOrder: &sapb.NewOrderRequest{ - RegistrationID: reg.Id, - Expires: timestamppb.New(expires), - Names: []string{"a.com", "b.com", "c.com", "d.com"}, + RegistrationID: reg.Id, + Expires: timestamppb.New(expires), + Identifiers: []*corepb.Identifier{ + identifier.NewDNS("a.com").ToProto(), + identifier.NewDNS("b.com").ToProto(), + identifier.NewDNS("c.com").ToProto(), + identifier.NewDNS("d.com").ToProto(), + }, V2Authorizations: []int64{1, 2}, }, // And add new authorizations for the other two names. - NewAuthzs: []*corepb.Authorization{ + NewAuthzs: []*sapb.NewAuthzRequest{ { - Identifier: "c.com", + Identifier: &corepb.Identifier{Type: "dns", Value: "c.com"}, RegistrationID: reg.Id, Expires: timestamppb.New(nowC), - Status: "pending", - Challenges: []*corepb.Challenge{{Token: core.NewToken()}}, + ChallengeTypes: []string{string(core.ChallengeTypeHTTP01)}, + Token: core.NewToken(), }, { - Identifier: "d.com", + Identifier: &corepb.Identifier{Type: "dns", Value: "d.com"}, RegistrationID: reg.Id, Expires: timestamppb.New(nowD), - Status: "pending", - Challenges: []*corepb.Challenge{{Token: core.NewToken()}}, + ChallengeTypes: []string{string(core.ChallengeTypeHTTP01)}, + Token: core.NewToken(), }, }, - }) + } + order, err := sa.NewOrderAndAuthzs(context.Background(), req) test.AssertNotError(t, err, "sa.NewOrderAndAuthzs failed") test.AssertEquals(t, order.Id, int64(1)) test.AssertDeepEquals(t, order.V2Authorizations, []int64{1, 2, 3, 4}) @@ -1291,66 +1021,65 @@ func TestNewOrderAndAuthzs_NonNilInnerOrder(t *testing.T) { sa, fc, cleanup := initSA(t) defer cleanup() - key, _ := jose.JSONWebKey{Key: &rsa.PublicKey{N: big.NewInt(1), E: 1}}.MarshalJSON() - initialIP, _ := net.ParseIP("17.17.17.17").MarshalText() - reg, err := sa.NewRegistration(ctx, &corepb.Registration{ - Key: key, - InitialIP: initialIP, - }) - test.AssertNotError(t, err, "Couldn't create test registration") + reg := createWorkingRegistration(t, sa) expires := fc.Now().Add(2 * time.Hour) - _, err = sa.NewOrderAndAuthzs(context.Background(), &sapb.NewOrderAndAuthzsRequest{ - NewAuthzs: []*corepb.Authorization{ + _, err := sa.NewOrderAndAuthzs(context.Background(), &sapb.NewOrderAndAuthzsRequest{ + NewAuthzs: []*sapb.NewAuthzRequest{ { - Identifier: "a.com", + Identifier: &corepb.Identifier{Type: "dns", Value: "c.com"}, RegistrationID: reg.Id, Expires: timestamppb.New(expires), - Status: "pending", - Challenges: []*corepb.Challenge{{Token: core.NewToken()}}, + ChallengeTypes: []string{string(core.ChallengeTypeDNS01)}, + Token: core.NewToken(), }, }, }) test.AssertErrorIs(t, err, errIncompleteRequest) } +func TestNewOrderAndAuthzs_MismatchedRegID(t *testing.T) { + sa, _, cleanup := initSA(t) + defer cleanup() + + _, err := sa.NewOrderAndAuthzs(context.Background(), &sapb.NewOrderAndAuthzsRequest{ + NewOrder: &sapb.NewOrderRequest{ + RegistrationID: 1, + }, + NewAuthzs: []*sapb.NewAuthzRequest{ + { + RegistrationID: 2, + }, + }, + }) + test.AssertError(t, err, "mismatched regIDs should fail") + test.AssertContains(t, err.Error(), "same account") +} + func TestNewOrderAndAuthzs_NewAuthzExpectedFields(t *testing.T) { sa, fc, cleanup := initSA(t) defer cleanup() - // Create a test registration to reference. - key, _ := jose.JSONWebKey{Key: &rsa.PublicKey{N: big.NewInt(1), E: 1}}.MarshalJSON() - initialIP, _ := net.ParseIP("17.17.17.17").MarshalText() - reg, err := sa.NewRegistration(ctx, &corepb.Registration{ - Key: key, - InitialIP: initialIP, - }) - test.AssertNotError(t, err, "Couldn't create test registration") - + reg := createWorkingRegistration(t, sa) expires := fc.Now().Add(time.Hour) domain := "a.com" // Create an authz that does not yet exist in the database with some invalid // data smuggled in. order, err := sa.NewOrderAndAuthzs(context.Background(), &sapb.NewOrderAndAuthzsRequest{ - NewAuthzs: []*corepb.Authorization{ + NewAuthzs: []*sapb.NewAuthzRequest{ { - Identifier: domain, + Identifier: &corepb.Identifier{Type: "dns", Value: domain}, RegistrationID: reg.Id, Expires: timestamppb.New(expires), - Status: string(core.StatusPending), - Challenges: []*corepb.Challenge{ - { - Status: "real fake garbage data", - Token: core.NewToken(), - }, - }, + ChallengeTypes: []string{string(core.ChallengeTypeHTTP01)}, + Token: core.NewToken(), }, }, NewOrder: &sapb.NewOrderRequest{ RegistrationID: reg.Id, Expires: timestamppb.New(expires), - Names: []string{domain}, + Identifiers: []*corepb.Identifier{identifier.NewDNS(domain).ToProto()}, }, }) test.AssertNotError(t, err, "sa.NewOrderAndAuthzs failed") @@ -1379,23 +1108,65 @@ func TestNewOrderAndAuthzs_NewAuthzExpectedFields(t *testing.T) { test.AssertBoxedNil(t, am.ValidationRecord, "am.ValidationRecord should be nil") } +func TestNewOrderAndAuthzs_Profile(t *testing.T) { + sa, fc, cleanup := initSA(t) + defer cleanup() + + reg := createWorkingRegistration(t, sa) + expires := fc.Now().Add(time.Hour) + + // Create and order and authz while specifying a profile. + order, err := sa.NewOrderAndAuthzs(context.Background(), &sapb.NewOrderAndAuthzsRequest{ + NewOrder: &sapb.NewOrderRequest{ + RegistrationID: reg.Id, + Expires: timestamppb.New(expires), + Identifiers: []*corepb.Identifier{identifier.NewDNS("example.com").ToProto()}, + CertificateProfileName: "test", + }, + NewAuthzs: []*sapb.NewAuthzRequest{ + { + Identifier: &corepb.Identifier{Type: "dns", Value: "example.com"}, + RegistrationID: reg.Id, + Expires: timestamppb.New(expires), + ChallengeTypes: []string{string(core.ChallengeTypeHTTP01)}, + Token: core.NewToken(), + }, + }, + }) + if err != nil { + t.Fatalf("inserting order and authzs: %s", err) + } + + // Retrieve the order and check that the profile is correct. + gotOrder, err := sa.GetOrder(context.Background(), &sapb.OrderRequest{Id: order.Id}) + if err != nil { + t.Fatalf("retrieving inserted order: %s", err) + } + if gotOrder.CertificateProfileName != "test" { + t.Errorf("order.CertificateProfileName = %v, want %v", gotOrder.CertificateProfileName, "test") + } + + // Retrieve the authz and check that the profile is correct. + // Safely get the authz for the order we created above. + gotAuthz, err := sa.GetAuthorization2(context.Background(), &sapb.AuthorizationID2{Id: order.V2Authorizations[0]}) + if err != nil { + t.Fatalf("retrieving inserted authz: %s", err) + } + if gotAuthz.CertificateProfileName != "test" { + t.Errorf("authz.CertificateProfileName = %v, want %v", gotAuthz.CertificateProfileName, "test") + } +} + func TestSetOrderProcessing(t *testing.T) { sa, fc, cleanup := initSA(t) defer cleanup() - // Create a test registration to reference - key, _ := jose.JSONWebKey{Key: &rsa.PublicKey{N: big.NewInt(1), E: 1}}.MarshalJSON() - initialIP, _ := net.ParseIP("42.42.42.42").MarshalText() - reg, err := sa.NewRegistration(ctx, &corepb.Registration{ - Key: key, - InitialIP: initialIP, - }) - test.AssertNotError(t, err, "Couldn't create test registration") + reg := createWorkingRegistration(t, sa) // Add one valid authz expires := fc.Now().Add(time.Hour) attemptedAt := fc.Now() - authzID := createFinalizedAuthorization(t, sa, "example.com", expires, "valid", attemptedAt) + authzID := createFinalizedAuthorization(t, sa, identifier.NewDNS("example.com"), expires, "valid", attemptedAt) // Add a new order in pending status with no certificate serial expires1Year := sa.clk.Now().Add(365 * 24 * time.Hour) @@ -1403,7 +1174,7 @@ func TestSetOrderProcessing(t *testing.T) { NewOrder: &sapb.NewOrderRequest{ RegistrationID: reg.Id, Expires: timestamppb.New(expires1Year), - Names: []string{"example.com"}, + Identifiers: []*corepb.Identifier{identifier.NewDNS("example.com").ToProto()}, V2Authorizations: []int64{authzID}, }, }) @@ -1432,19 +1203,10 @@ func TestFinalizeOrder(t *testing.T) { sa, fc, cleanup := initSA(t) defer cleanup() - // Create a test registration to reference - key, _ := jose.JSONWebKey{Key: &rsa.PublicKey{N: big.NewInt(1), E: 1}}.MarshalJSON() - initialIP, _ := net.ParseIP("42.42.42.42").MarshalText() - reg, err := sa.NewRegistration(ctx, &corepb.Registration{ - Key: key, - InitialIP: initialIP, - }) - test.AssertNotError(t, err, "Couldn't create test registration") - - // Add one valid authz + reg := createWorkingRegistration(t, sa) expires := fc.Now().Add(time.Hour) attemptedAt := fc.Now() - authzID := createFinalizedAuthorization(t, sa, "example.com", expires, "valid", attemptedAt) + authzID := createFinalizedAuthorization(t, sa, identifier.NewDNS("example.com"), expires, "valid", attemptedAt) // Add a new order in pending status with no certificate serial expires1Year := sa.clk.Now().Add(365 * 24 * time.Hour) @@ -1452,7 +1214,7 @@ func TestFinalizeOrder(t *testing.T) { NewOrder: &sapb.NewOrderRequest{ RegistrationID: reg.Id, Expires: timestamppb.New(expires1Year), - Names: []string{"example.com"}, + Identifiers: []*corepb.Identifier{identifier.NewDNS("example.com").ToProto()}, V2Authorizations: []int64{authzID}, }, }) @@ -1477,21 +1239,16 @@ func TestFinalizeOrder(t *testing.T) { test.AssertEquals(t, updatedOrder.Status, string(core.StatusValid)) } -func TestOrderWithOrderModelv1(t *testing.T) { +// TestGetOrder tests that round-tripping a simple order through +// NewOrderAndAuthzs and GetOrder has the expected result. +func TestGetOrder(t *testing.T) { sa, fc, cleanup := initSA(t) defer cleanup() - // Create a test registration to reference - key, _ := jose.JSONWebKey{Key: &rsa.PublicKey{N: big.NewInt(1), E: 1}}.MarshalJSON() - initialIP, _ := net.ParseIP("42.42.42.42").MarshalText() - reg, err := sa.NewRegistration(ctx, &corepb.Registration{ - Key: key, - InitialIP: initialIP, - }) - test.AssertNotError(t, err, "Couldn't create test registration") - + reg := createWorkingRegistration(t, sa) + ident := identifier.NewDNS("example.com") authzExpires := fc.Now().Add(time.Hour) - authzID := createPendingAuthorization(t, sa, "example.com", authzExpires) + authzID := createPendingAuthorization(t, sa, ident, authzExpires) // Set the order to expire in two hours expires := fc.Now().Add(2 * time.Hour) @@ -1499,7 +1256,7 @@ func TestOrderWithOrderModelv1(t *testing.T) { inputOrder := &corepb.Order{ RegistrationID: reg.Id, Expires: timestamppb.New(expires), - Names: []string{"example.com"}, + Identifiers: []*corepb.Identifier{ident.ToProto()}, V2Authorizations: []int64{authzID}, } @@ -1508,7 +1265,7 @@ func TestOrderWithOrderModelv1(t *testing.T) { NewOrder: &sapb.NewOrderRequest{ RegistrationID: inputOrder.RegistrationID, Expires: inputOrder.Expires, - Names: inputOrder.Names, + Identifiers: inputOrder.Identifiers, V2Authorizations: inputOrder.V2Authorizations, }, }) @@ -1517,11 +1274,11 @@ func TestOrderWithOrderModelv1(t *testing.T) { // The Order from GetOrder should match the following expected order created := sa.clk.Now() expectedOrder := &corepb.Order{ - // The registration ID, authorizations, expiry, and names should match the + // The registration ID, authorizations, expiry, and identifiers should match the // input to NewOrderAndAuthzs RegistrationID: inputOrder.RegistrationID, V2Authorizations: inputOrder.V2Authorizations, - Names: inputOrder.Names, + Identifiers: inputOrder.Identifiers, Expires: inputOrder.Expires, // The ID should have been set to 1 by the SA Id: 1, @@ -1541,40 +1298,16 @@ func TestOrderWithOrderModelv1(t *testing.T) { test.AssertDeepEquals(t, storedOrder, expectedOrder) } -func TestOrderWithOrderModelv2(t *testing.T) { - if !strings.Contains(os.Getenv("BOULDER_CONFIG_DIR"), "test/config-next") { - t.Skip() - } - - // The feature must be set before the SA is constructed because of a - // conditional on this feature in //sa/database.go. - features.Set(features.Config{MultipleCertificateProfiles: true}) - defer features.Reset() - - fc := clock.NewFake() - fc.Set(time.Date(2015, 3, 4, 5, 0, 0, 0, time.UTC)) - - dbMap, err := DBMapForTest(vars.DBConnSA) - test.AssertNotError(t, err, "Couldn't create dbMap") - - saro, err := NewSQLStorageAuthorityRO(dbMap, nil, metrics.NoopRegisterer, 1, 0, fc, log) - test.AssertNotError(t, err, "Couldn't create SARO") - - sa, err := NewSQLStorageAuthorityWrapping(saro, dbMap, metrics.NoopRegisterer) - test.AssertNotError(t, err, "Couldn't create SA") - defer test.ResetBoulderTestDatabase(t) - - // Create a test registration to reference - key, _ := jose.JSONWebKey{Key: &rsa.PublicKey{N: big.NewInt(1), E: 1}}.MarshalJSON() - initialIP, _ := net.ParseIP("42.42.42.42").MarshalText() - reg, err := sa.NewRegistration(ctx, &corepb.Registration{ - Key: key, - InitialIP: initialIP, - }) - test.AssertNotError(t, err, "Couldn't create test registration") +// TestGetOrderWithProfile tests that round-tripping a simple order through +// NewOrderAndAuthzs and GetOrder has the expected result. +func TestGetOrderWithProfile(t *testing.T) { + sa, fc, cleanup := initSA(t) + defer cleanup() + reg := createWorkingRegistration(t, sa) + ident := identifier.NewDNS("example.com") authzExpires := fc.Now().Add(time.Hour) - authzID := createPendingAuthorization(t, sa, "example.com", authzExpires) + authzID := createPendingAuthorization(t, sa, ident, authzExpires) // Set the order to expire in two hours expires := fc.Now().Add(2 * time.Hour) @@ -1582,7 +1315,7 @@ func TestOrderWithOrderModelv2(t *testing.T) { inputOrder := &corepb.Order{ RegistrationID: reg.Id, Expires: timestamppb.New(expires), - Names: []string{"example.com"}, + Identifiers: []*corepb.Identifier{ident.ToProto()}, V2Authorizations: []int64{authzID}, CertificateProfileName: "tbiapb", } @@ -1592,7 +1325,7 @@ func TestOrderWithOrderModelv2(t *testing.T) { NewOrder: &sapb.NewOrderRequest{ RegistrationID: inputOrder.RegistrationID, Expires: inputOrder.Expires, - Names: inputOrder.Names, + Identifiers: inputOrder.Identifiers, V2Authorizations: inputOrder.V2Authorizations, CertificateProfileName: inputOrder.CertificateProfileName, }, @@ -1606,7 +1339,7 @@ func TestOrderWithOrderModelv2(t *testing.T) { // input to NewOrderAndAuthzs RegistrationID: inputOrder.RegistrationID, V2Authorizations: inputOrder.V2Authorizations, - Names: inputOrder.Names, + Identifiers: inputOrder.Identifiers, Expires: inputOrder.Expires, // The ID should have been set to 1 by the SA Id: 1, @@ -1625,65 +1358,6 @@ func TestOrderWithOrderModelv2(t *testing.T) { storedOrder, err := sa.GetOrder(context.Background(), &sapb.OrderRequest{Id: order.Id}) test.AssertNotError(t, err, "sa.GetOrder failed") test.AssertDeepEquals(t, storedOrder, expectedOrder) - - // - // Test that an order without a certificate profile name, but with the - // MultipleCertificateProfiles feature flag enabled works as expected. - // - - // Create a test registration to reference - key2, _ := jose.JSONWebKey{Key: &rsa.PublicKey{N: big.NewInt(2), E: 2}}.MarshalJSON() - initialIP2, _ := net.ParseIP("44.44.44.44").MarshalText() - reg2, err := sa.NewRegistration(ctx, &corepb.Registration{ - Key: key2, - InitialIP: initialIP2, - }) - test.AssertNotError(t, err, "Couldn't create test registration") - - inputOrderNoName := &corepb.Order{ - RegistrationID: reg2.Id, - Expires: timestamppb.New(expires), - Names: []string{"example.com"}, - V2Authorizations: []int64{authzID}, - } - - // Create the order - orderNoName, err := sa.NewOrderAndAuthzs(context.Background(), &sapb.NewOrderAndAuthzsRequest{ - NewOrder: &sapb.NewOrderRequest{ - RegistrationID: inputOrderNoName.RegistrationID, - Expires: inputOrderNoName.Expires, - Names: inputOrderNoName.Names, - V2Authorizations: inputOrderNoName.V2Authorizations, - CertificateProfileName: inputOrderNoName.CertificateProfileName, - }, - }) - test.AssertNotError(t, err, "sa.NewOrderAndAuthzs failed") - - // The Order from GetOrder should match the following expected order - created = sa.clk.Now() - expectedOrderNoName := &corepb.Order{ - // The registration ID, authorizations, expiry, and names should match the - // input to NewOrderAndAuthzs - RegistrationID: inputOrderNoName.RegistrationID, - V2Authorizations: inputOrderNoName.V2Authorizations, - Names: inputOrderNoName.Names, - Expires: inputOrderNoName.Expires, - // The ID should have been set to 2 by the SA - Id: 2, - // The status should be pending - Status: string(core.StatusPending), - // The serial should be empty since this is a pending order - CertificateSerial: "", - // We should not be processing it - BeganProcessing: false, - // The created timestamp should have been set to the current time - Created: timestamppb.New(created), - } - - // Fetch the order by its ID and make sure it matches the expected - storedOrderNoName, err := sa.GetOrder(context.Background(), &sapb.OrderRequest{Id: orderNoName.Id}) - test.AssertNotError(t, err, "sa.GetOrder failed") - test.AssertDeepEquals(t, storedOrderNoName, expectedOrderNoName) } // TestGetAuthorization2NoRows ensures that the GetAuthorization2 function returns @@ -1707,138 +1381,68 @@ func TestGetAuthorizations2(t *testing.T) { exp := fc.Now().AddDate(0, 0, 10).UTC() attemptedAt := fc.Now() - identA := "aaa" - identB := "bbb" - identC := "ccc" - identD := "ddd" - idents := []string{identA, identB, identC} + identA := identifier.NewDNS("aaa") + identB := identifier.NewDNS("bbb") + identC := identifier.NewDNS("ccc") + identD := identifier.NewIP(netip.MustParseAddr("10.10.10.10")) + idents := identifier.ACMEIdentifiers{identA, identB, identC, identD} + identE := identifier.NewDNS("ddd") - authzIDA := createFinalizedAuthorization(t, sa, "aaa", exp, "valid", attemptedAt) - authzIDB := createPendingAuthorization(t, sa, "bbb", exp) + createFinalizedAuthorization(t, sa, identA, exp, "valid", attemptedAt) + createPendingAuthorization(t, sa, identB, exp) nearbyExpires := fc.Now().UTC().Add(time.Hour) - authzIDC := createPendingAuthorization(t, sa, "ccc", nearbyExpires) - - // Associate authorizations with an order so that GetAuthorizations2 thinks - // they are WFE2 authorizations. - err := sa.dbMap.Insert(ctx, &orderToAuthzModel{ - OrderID: 1, - AuthzID: authzIDA, - }) - test.AssertNotError(t, err, "sa.dbMap.Insert failed") - err = sa.dbMap.Insert(ctx, &orderToAuthzModel{ - OrderID: 1, - AuthzID: authzIDB, - }) - test.AssertNotError(t, err, "sa.dbMap.Insert failed") - err = sa.dbMap.Insert(ctx, &orderToAuthzModel{ - OrderID: 1, - AuthzID: authzIDC, - }) - test.AssertNotError(t, err, "sa.dbMap.Insert failed") + createPendingAuthorization(t, sa, identC, nearbyExpires) + createFinalizedAuthorization(t, sa, identD, exp, "valid", attemptedAt) // Set an expiry cut off of 1 day in the future similar to `RA.NewOrderAndAuthzs`. This // should exclude pending authorization C based on its nearbyExpires expiry // value. expiryCutoff := fc.Now().AddDate(0, 0, 1) - // Get authorizations for the names used above. + // Get authorizations for the identifiers used above. authz, err := sa.GetAuthorizations2(context.Background(), &sapb.GetAuthorizationsRequest{ RegistrationID: reg.Id, - Domains: idents, - Now: timestamppb.New(expiryCutoff), + Identifiers: idents.ToProtoSlice(), + ValidUntil: timestamppb.New(expiryCutoff), }) // It should not fail test.AssertNotError(t, err, "sa.GetAuthorizations2 failed") - // We should get back two authorizations since one of the three authorizations - // created above expires too soon. - test.AssertEquals(t, len(authz.Authz), 2) + // We should get back three authorizations since one of the four + // authorizations created above expires too soon. + test.AssertEquals(t, len(authz.Authzs), 3) - // Get authorizations for the names used above, and one name that doesn't exist + // Get authorizations for the identifiers used above, and one that doesn't exist authz, err = sa.GetAuthorizations2(context.Background(), &sapb.GetAuthorizationsRequest{ RegistrationID: reg.Id, - Domains: append(idents, identD), - Now: timestamppb.New(expiryCutoff), + Identifiers: append(idents.ToProtoSlice(), identE.ToProto()), + ValidUntil: timestamppb.New(expiryCutoff), }) // It should not fail test.AssertNotError(t, err, "sa.GetAuthorizations2 failed") - // It should still return only two authorizations - test.AssertEquals(t, len(authz.Authz), 2) -} - -func TestCountOrders(t *testing.T) { - sa, _, cleanUp := initSA(t) - defer cleanUp() - - reg := createWorkingRegistration(t, sa) - now := sa.clk.Now() - expires := now.Add(24 * time.Hour) - - req := &sapb.CountOrdersRequest{ - AccountID: 12345, - Range: &sapb.Range{ - Earliest: timestamppb.New(now.Add(-time.Hour)), - Latest: timestamppb.New(now.Add(time.Second)), - }, - } - - // Counting new orders for a reg ID that doesn't exist should return 0 - count, err := sa.CountOrders(ctx, req) - test.AssertNotError(t, err, "Couldn't count new orders for fake reg ID") - test.AssertEquals(t, count.Count, int64(0)) - - // Add a pending authorization - authzID := createPendingAuthorization(t, sa, "example.com", expires) - - // Add one pending order - order, err := sa.NewOrderAndAuthzs(ctx, &sapb.NewOrderAndAuthzsRequest{ - NewOrder: &sapb.NewOrderRequest{ - RegistrationID: reg.Id, - Expires: timestamppb.New(expires), - Names: []string{"example.com"}, - V2Authorizations: []int64{authzID}, - }, - }) - test.AssertNotError(t, err, "Couldn't create new pending order") - - // Counting new orders for the reg ID should now yield 1 - req.AccountID = reg.Id - count, err = sa.CountOrders(ctx, req) - test.AssertNotError(t, err, "Couldn't count new orders for reg ID") - test.AssertEquals(t, count.Count, int64(1)) - - // Moving the count window to after the order was created should return the - // count to 0 - earliest := order.Created.AsTime().Add(time.Minute) - latest := earliest.Add(time.Hour) - req.Range.Earliest = timestamppb.New(earliest) - req.Range.Latest = timestamppb.New(latest) - count, err = sa.CountOrders(ctx, req) - test.AssertNotError(t, err, "Couldn't count new orders for reg ID") - test.AssertEquals(t, count.Count, int64(0)) + // It should still return only three authorizations + test.AssertEquals(t, len(authz.Authzs), 3) } func TestFasterGetOrderForNames(t *testing.T) { sa, fc, cleanUp := initSA(t) defer cleanUp() - domain := "example.com" + ident := identifier.NewDNS("example.com") expires := fc.Now().Add(time.Hour) key, _ := goodTestJWK().MarshalJSON() - initialIP, _ := net.ParseIP("42.42.42.42").MarshalText() reg, err := sa.NewRegistration(ctx, &corepb.Registration{ - Key: key, - InitialIP: initialIP, + Key: key, }) test.AssertNotError(t, err, "Couldn't create test registration") - authzIDs := createPendingAuthorization(t, sa, domain, expires) + authzIDs := createPendingAuthorization(t, sa, ident, expires) _, err = sa.NewOrderAndAuthzs(ctx, &sapb.NewOrderAndAuthzsRequest{ NewOrder: &sapb.NewOrderRequest{ RegistrationID: reg.Id, Expires: timestamppb.New(expires), V2Authorizations: []int64{authzIDs}, - Names: []string{domain}, + Identifiers: []*corepb.Identifier{ident.ToProto()}, }, }) test.AssertNotError(t, err, "sa.NewOrderAndAuthzs failed") @@ -1848,14 +1452,14 @@ func TestFasterGetOrderForNames(t *testing.T) { RegistrationID: reg.Id, Expires: timestamppb.New(expires), V2Authorizations: []int64{authzIDs}, - Names: []string{domain}, + Identifiers: []*corepb.Identifier{ident.ToProto()}, }, }) test.AssertNotError(t, err, "sa.NewOrderAndAuthzs failed") _, err = sa.GetOrderForNames(ctx, &sapb.GetOrderForNamesRequest{ - AcctID: reg.Id, - Names: []string{domain}, + AcctID: reg.Id, + Identifiers: []*corepb.Identifier{ident.ToProto()}, }) test.AssertNotError(t, err, "sa.GetOrderForNames failed") } @@ -1870,27 +1474,28 @@ func TestGetOrderForNames(t *testing.T) { // Create two test registrations to associate with orders key, _ := goodTestJWK().MarshalJSON() - initialIP, _ := net.ParseIP("42.42.42.42").MarshalText() regA, err := sa.NewRegistration(ctx, &corepb.Registration{ - Key: key, - InitialIP: initialIP, + Key: key, }) test.AssertNotError(t, err, "Couldn't create test registration") // Add one pending authz for the first name for regA and one // pending authz for the second name for regA authzExpires := fc.Now().Add(time.Hour) - authzIDA := createPendingAuthorization(t, sa, "example.com", authzExpires) - authzIDB := createPendingAuthorization(t, sa, "just.another.example.com", authzExpires) + authzIDA := createPendingAuthorization(t, sa, identifier.NewDNS("example.com"), authzExpires) + authzIDB := createPendingAuthorization(t, sa, identifier.NewDNS("just.another.example.com"), authzExpires) ctx := context.Background() - names := []string{"example.com", "just.another.example.com"} + idents := identifier.ACMEIdentifiers{ + identifier.NewDNS("example.com"), + identifier.NewDNS("just.another.example.com"), + } // Call GetOrderForNames for a set of names we haven't created an order for // yet result, err := sa.GetOrderForNames(ctx, &sapb.GetOrderForNamesRequest{ - AcctID: regA.Id, - Names: names, + AcctID: regA.Id, + Identifiers: idents.ToProtoSlice(), }) // We expect the result to return an error test.AssertError(t, err, "sa.GetOrderForNames did not return an error for an empty result") @@ -1905,7 +1510,7 @@ func TestGetOrderForNames(t *testing.T) { RegistrationID: regA.Id, Expires: timestamppb.New(expires), V2Authorizations: []int64{authzIDA, authzIDB}, - Names: names, + Identifiers: idents.ToProtoSlice(), }, }) // It shouldn't error @@ -1916,8 +1521,8 @@ func TestGetOrderForNames(t *testing.T) { // Call GetOrderForNames with the same account ID and set of names as the // above NewOrderAndAuthzs call result, err = sa.GetOrderForNames(ctx, &sapb.GetOrderForNamesRequest{ - AcctID: regA.Id, - Names: names, + AcctID: regA.Id, + Identifiers: idents.ToProtoSlice(), }) // It shouldn't error test.AssertNotError(t, err, "sa.GetOrderForNames failed") @@ -1928,8 +1533,8 @@ func TestGetOrderForNames(t *testing.T) { // Call GetOrderForNames with a different account ID from the NewOrderAndAuthzs call regB := int64(1337) result, err = sa.GetOrderForNames(ctx, &sapb.GetOrderForNamesRequest{ - AcctID: regB, - Names: names, + AcctID: regB, + Identifiers: idents.ToProtoSlice(), }) // It should error test.AssertError(t, err, "sa.GetOrderForNames did not return an error for an empty result") @@ -1944,8 +1549,8 @@ func TestGetOrderForNames(t *testing.T) { // Call GetOrderForNames again with the same account ID and set of names as // the initial NewOrderAndAuthzs call result, err = sa.GetOrderForNames(ctx, &sapb.GetOrderForNamesRequest{ - AcctID: regA.Id, - Names: names, + AcctID: regA.Id, + Identifiers: idents.ToProtoSlice(), }) // It should error since there is no result test.AssertError(t, err, "sa.GetOrderForNames did not return an error for an empty result") @@ -1958,18 +1563,17 @@ func TestGetOrderForNames(t *testing.T) { // Create two valid authorizations authzExpires = fc.Now().Add(time.Hour) attemptedAt := fc.Now() - authzIDC := createFinalizedAuthorization(t, sa, "zombo.com", authzExpires, "valid", attemptedAt) - authzIDD := createFinalizedAuthorization(t, sa, "welcome.to.zombo.com", authzExpires, "valid", attemptedAt) + authzIDC := createFinalizedAuthorization(t, sa, identifier.NewDNS("zombo.com"), authzExpires, "valid", attemptedAt) + authzIDD := createFinalizedAuthorization(t, sa, identifier.NewDNS("welcome.to.zombo.com"), authzExpires, "valid", attemptedAt) // Add a fresh order that uses the authorizations created above - names = []string{"zombo.com", "welcome.to.zombo.com"} expires = fc.Now().Add(orderLifetime) order, err = sa.NewOrderAndAuthzs(ctx, &sapb.NewOrderAndAuthzsRequest{ NewOrder: &sapb.NewOrderRequest{ RegistrationID: regA.Id, Expires: timestamppb.New(expires), V2Authorizations: []int64{authzIDC, authzIDD}, - Names: names, + Identifiers: idents.ToProtoSlice(), }, }) // It shouldn't error @@ -1980,8 +1584,8 @@ func TestGetOrderForNames(t *testing.T) { // Call GetOrderForNames with the same account ID and set of names as // the earlier NewOrderAndAuthzs call result, err = sa.GetOrderForNames(ctx, &sapb.GetOrderForNamesRequest{ - AcctID: regA.Id, - Names: names, + AcctID: regA.Id, + Identifiers: idents.ToProtoSlice(), }) // It should not error since a ready order can be reused. test.AssertNotError(t, err, "sa.GetOrderForNames returned an unexpected error for ready order reuse") @@ -2001,8 +1605,8 @@ func TestGetOrderForNames(t *testing.T) { // Call GetOrderForNames with the same account ID and set of names as // the earlier NewOrderAndAuthzs call result, err = sa.GetOrderForNames(ctx, &sapb.GetOrderForNamesRequest{ - AcctID: regA.Id, - Names: names, + AcctID: regA.Id, + Identifiers: idents.ToProtoSlice(), }) // It should error since a valid order should not be reused. test.AssertError(t, err, "sa.GetOrderForNames did not return an error for an empty result") @@ -2027,76 +1631,93 @@ func TestStatusForOrder(t *testing.T) { // Create a pending authz, an expired authz, an invalid authz, a deactivated authz, // and a valid authz - pendingID := createPendingAuthorization(t, sa, "pending.your.order.is.up", expires) - expiredID := createPendingAuthorization(t, sa, "expired.your.order.is.up", alreadyExpired) - invalidID := createFinalizedAuthorization(t, sa, "invalid.your.order.is.up", expires, "invalid", attemptedAt) - validID := createFinalizedAuthorization(t, sa, "valid.your.order.is.up", expires, "valid", attemptedAt) - deactivatedID := createPendingAuthorization(t, sa, "deactivated.your.order.is.up", expires) + pendingID := createPendingAuthorization(t, sa, identifier.NewDNS("pending.your.order.is.up"), expires) + expiredID := createPendingAuthorization(t, sa, identifier.NewDNS("expired.your.order.is.up"), alreadyExpired) + invalidID := createFinalizedAuthorization(t, sa, identifier.NewDNS("invalid.your.order.is.up"), expires, "invalid", attemptedAt) + validID := createFinalizedAuthorization(t, sa, identifier.NewDNS("valid.your.order.is.up"), expires, "valid", attemptedAt) + deactivatedID := createPendingAuthorization(t, sa, identifier.NewDNS("deactivated.your.order.is.up"), expires) _, err := sa.DeactivateAuthorization2(context.Background(), &sapb.AuthorizationID2{Id: deactivatedID}) test.AssertNotError(t, err, "sa.DeactivateAuthorization2 failed") testCases := []struct { Name string AuthorizationIDs []int64 - OrderNames []string + OrderIdents identifier.ACMEIdentifiers OrderExpires *timestamppb.Timestamp ExpectedStatus string SetProcessing bool Finalize bool }{ { - Name: "Order with an invalid authz", - OrderNames: []string{"pending.your.order.is.up", "invalid.your.order.is.up", "deactivated.your.order.is.up", "valid.your.order.is.up"}, + Name: "Order with an invalid authz", + OrderIdents: identifier.ACMEIdentifiers{ + identifier.NewDNS("pending.your.order.is.up"), + identifier.NewDNS("invalid.your.order.is.up"), + identifier.NewDNS("deactivated.your.order.is.up"), + identifier.NewDNS("valid.your.order.is.up"), + }, AuthorizationIDs: []int64{pendingID, invalidID, deactivatedID, validID}, ExpectedStatus: string(core.StatusInvalid), }, { - Name: "Order with an expired authz", - OrderNames: []string{"pending.your.order.is.up", "expired.your.order.is.up", "deactivated.your.order.is.up", "valid.your.order.is.up"}, + Name: "Order with an expired authz", + OrderIdents: identifier.ACMEIdentifiers{ + identifier.NewDNS("pending.your.order.is.up"), + identifier.NewDNS("expired.your.order.is.up"), + identifier.NewDNS("deactivated.your.order.is.up"), + identifier.NewDNS("valid.your.order.is.up"), + }, AuthorizationIDs: []int64{pendingID, expiredID, deactivatedID, validID}, ExpectedStatus: string(core.StatusInvalid), }, { - Name: "Order with a deactivated authz", - OrderNames: []string{"pending.your.order.is.up", "deactivated.your.order.is.up", "valid.your.order.is.up"}, + Name: "Order with a deactivated authz", + OrderIdents: identifier.ACMEIdentifiers{ + identifier.NewDNS("pending.your.order.is.up"), + identifier.NewDNS("deactivated.your.order.is.up"), + identifier.NewDNS("valid.your.order.is.up"), + }, AuthorizationIDs: []int64{pendingID, deactivatedID, validID}, ExpectedStatus: string(core.StatusInvalid), }, { - Name: "Order with a pending authz", - OrderNames: []string{"valid.your.order.is.up", "pending.your.order.is.up"}, + Name: "Order with a pending authz", + OrderIdents: identifier.ACMEIdentifiers{ + identifier.NewDNS("valid.your.order.is.up"), + identifier.NewDNS("pending.your.order.is.up"), + }, AuthorizationIDs: []int64{validID, pendingID}, ExpectedStatus: string(core.StatusPending), }, { Name: "Order with only valid authzs, not yet processed or finalized", - OrderNames: []string{"valid.your.order.is.up"}, + OrderIdents: identifier.ACMEIdentifiers{identifier.NewDNS("valid.your.order.is.up")}, AuthorizationIDs: []int64{validID}, ExpectedStatus: string(core.StatusReady), }, { Name: "Order with only valid authzs, set processing", - OrderNames: []string{"valid.your.order.is.up"}, + OrderIdents: identifier.ACMEIdentifiers{identifier.NewDNS("valid.your.order.is.up")}, AuthorizationIDs: []int64{validID}, SetProcessing: true, ExpectedStatus: string(core.StatusProcessing), }, { Name: "Order with only valid authzs, not yet processed or finalized, OrderReadyStatus feature flag", - OrderNames: []string{"valid.your.order.is.up"}, + OrderIdents: identifier.ACMEIdentifiers{identifier.NewDNS("valid.your.order.is.up")}, AuthorizationIDs: []int64{validID}, ExpectedStatus: string(core.StatusReady), }, { Name: "Order with only valid authzs, set processing", - OrderNames: []string{"valid.your.order.is.up"}, + OrderIdents: identifier.ACMEIdentifiers{identifier.NewDNS("valid.your.order.is.up")}, AuthorizationIDs: []int64{validID}, SetProcessing: true, ExpectedStatus: string(core.StatusProcessing), }, { Name: "Order with only valid authzs, set processing and finalized", - OrderNames: []string{"valid.your.order.is.up"}, + OrderIdents: identifier.ACMEIdentifiers{identifier.NewDNS("valid.your.order.is.up")}, AuthorizationIDs: []int64{validID}, SetProcessing: true, Finalize: true, @@ -2118,7 +1739,7 @@ func TestStatusForOrder(t *testing.T) { RegistrationID: reg.Id, Expires: orderExpiry, V2Authorizations: tc.AuthorizationIDs, - Names: tc.OrderNames, + Identifiers: tc.OrderIdents.ToProtoSlice(), }, }) test.AssertNotError(t, err, "NewOrderAndAuthzs errored unexpectedly") @@ -2154,7 +1775,7 @@ func TestUpdateChallengesDeleteUnused(t *testing.T) { attemptedAt := fc.Now() // Create a valid authz - authzID := createFinalizedAuthorization(t, sa, "example.com", expires, "valid", attemptedAt) + authzID := createFinalizedAuthorization(t, sa, identifier.NewDNS("example.com"), expires, "valid", attemptedAt) result, err := sa.GetAuthorization2(ctx, &sapb.AuthorizationID2{Id: authzID}) test.AssertNotError(t, err, "sa.GetAuthorization2 failed") @@ -2220,10 +1841,6 @@ func TestRevokeCertificate(t *testing.T) { } func TestRevokeCertificateWithShard(t *testing.T) { - if os.Getenv("BOULDER_CONFIG_DIR") != "test/config-next" { - t.Skip("Test requires revokedCertificates database table") - } - sa, fc, cleanUp := initSA(t) defer cleanUp() @@ -2385,10 +2002,6 @@ func TestUpdateRevokedCertificate(t *testing.T) { } func TestUpdateRevokedCertificateWithShard(t *testing.T) { - if os.Getenv("BOULDER_CONFIG_DIR") != "test/config-next" { - t.Skip("Test requires revokedCertificates database table") - } - sa, fc, cleanUp := initSA(t) defer cleanUp() @@ -2446,10 +2059,6 @@ func TestUpdateRevokedCertificateWithShard(t *testing.T) { } func TestUpdateRevokedCertificateWithShardInterim(t *testing.T) { - if os.Getenv("BOULDER_CONFIG_DIR") != "test/config-next" { - t.Skip("Test requires revokedCertificates database table") - } - sa, fc, cleanUp := initSA(t) defer cleanUp() @@ -2523,7 +2132,7 @@ func TestAddCertificateRenewalBit(t *testing.T) { reg := createWorkingRegistration(t, sa) - assertIsRenewal := func(t *testing.T, name string, expected bool) { + assertIsRenewal := func(t *testing.T, issuedName string, expected bool) { t.Helper() var count int err := sa.dbMap.SelectOne( @@ -2532,14 +2141,14 @@ func TestAddCertificateRenewalBit(t *testing.T) { `SELECT COUNT(*) FROM issuedNames WHERE reversedName = ? AND renewal = ?`, - ReverseName(name), + issuedName, expected, ) test.AssertNotError(t, err, "Unexpected error from SelectOne on issuedNames") test.AssertEquals(t, count, 1) } - // Add a certificate with a never-before-seen name. + // Add a certificate with never-before-seen identifiers. _, testCert := test.ThrowAwayCert(t, fc) _, err := sa.AddPrecertificate(ctx, &sapb.AddCertificateRequest{ Der: testCert.Raw, @@ -2555,16 +2164,19 @@ func TestAddCertificateRenewalBit(t *testing.T) { }) test.AssertNotError(t, err, "Failed to add certificate") - // None of the names should have a issuedNames row marking it as a renewal. + // No identifier should have an issuedNames row marking it as a renewal. for _, name := range testCert.DNSNames { - assertIsRenewal(t, name, false) + assertIsRenewal(t, reverseFQDN(name), false) + } + for _, ip := range testCert.IPAddresses { + assertIsRenewal(t, ip.String(), false) } // Make a new cert and add its FQDN set to the db so it will be considered a // renewal serial, testCert := test.ThrowAwayCert(t, fc) - err = addFQDNSet(ctx, sa.dbMap, testCert.DNSNames, serial, testCert.NotBefore, testCert.NotAfter) - test.AssertNotError(t, err, "Failed to add name set") + err = addFQDNSet(ctx, sa.dbMap, identifier.FromCert(testCert), serial, testCert.NotBefore, testCert.NotAfter) + test.AssertNotError(t, err, "Failed to add identifier set") _, err = sa.AddPrecertificate(ctx, &sapb.AddCertificateRequest{ Der: testCert.Raw, Issued: timestamppb.New(testCert.NotBefore), @@ -2579,120 +2191,25 @@ func TestAddCertificateRenewalBit(t *testing.T) { }) test.AssertNotError(t, err, "Failed to add certificate") - // All of the names should have a issuedNames row marking it as a renewal. + // Each identifier should have an issuedNames row marking it as a renewal. for _, name := range testCert.DNSNames { - assertIsRenewal(t, name, true) + assertIsRenewal(t, reverseFQDN(name), true) } -} - -func TestCountCertificatesRenewalBit(t *testing.T) { - sa, fc, cleanUp := initSA(t) - defer cleanUp() - - // Create a test registration - reg := createWorkingRegistration(t, sa) - - // Create a small throw away key for the test certificates. - testKey, err := rsa.GenerateKey(rand.Reader, 512) - test.AssertNotError(t, err, "error generating test key") - - // Create an initial test certificate for a set of domain names, issued an - // hour ago. - template := &x509.Certificate{ - SerialNumber: big.NewInt(1337), - DNSNames: []string{"www.not-example.com", "not-example.com", "admin.not-example.com"}, - NotBefore: fc.Now().Add(-time.Hour), - BasicConstraintsValid: true, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, + for _, ip := range testCert.IPAddresses { + assertIsRenewal(t, ip.String(), true) } - certADER, err := x509.CreateCertificate(rand.Reader, template, template, testKey.Public(), testKey) - test.AssertNotError(t, err, "Failed to create test cert A") - certA, _ := x509.ParseCertificate(certADER) - - // Update the template with a new serial number and a not before of now and - // create a second test cert for the same names. This will be a renewal. - template.SerialNumber = big.NewInt(7331) - template.NotBefore = fc.Now() - certBDER, err := x509.CreateCertificate(rand.Reader, template, template, testKey.Public(), testKey) - test.AssertNotError(t, err, "Failed to create test cert B") - certB, _ := x509.ParseCertificate(certBDER) - - // Update the template with a third serial number and a partially overlapping - // set of names. This will not be a renewal but will help test the exact name - // counts. - template.SerialNumber = big.NewInt(0xC0FFEE) - template.DNSNames = []string{"www.not-example.com"} - certCDER, err := x509.CreateCertificate(rand.Reader, template, template, testKey.Public(), testKey) - test.AssertNotError(t, err, "Failed to create test cert C") - - countName := func(t *testing.T, expectedName string) int64 { - earliest := fc.Now().Add(-5 * time.Hour) - latest := fc.Now().Add(5 * time.Hour) - req := &sapb.CountCertificatesByNamesRequest{ - Names: []string{expectedName}, - Range: &sapb.Range{ - Earliest: timestamppb.New(earliest), - Latest: timestamppb.New(latest), - }, - } - counts, err := sa.CountCertificatesByNames(context.Background(), req) - test.AssertNotError(t, err, "Unexpected err from CountCertificatesByNames") - for name, count := range counts.Counts { - if name == expectedName { - return count - } - } - return 0 - } - - // Add the first certificate - it won't be considered a renewal. - issued := certA.NotBefore - _, err = sa.AddCertificate(ctx, &sapb.AddCertificateRequest{ - Der: certADER, - RegID: reg.Id, - Issued: timestamppb.New(issued), - }) - test.AssertNotError(t, err, "Failed to add CertA test certificate") - - // The count for the base domain should be 1 - just certA has been added. - test.AssertEquals(t, countName(t, "not-example.com"), int64(1)) - - // Add the second certificate - it should be considered a renewal - issued = certB.NotBefore - _, err = sa.AddCertificate(ctx, &sapb.AddCertificateRequest{ - Der: certBDER, - RegID: reg.Id, - Issued: timestamppb.New(issued), - }) - test.AssertNotError(t, err, "Failed to add CertB test certificate") - - // The count for the base domain should still be 1, just certA. CertB should - // be ignored. - test.AssertEquals(t, countName(t, "not-example.com"), int64(1)) - - // Add the third certificate - it should not be considered a renewal - _, err = sa.AddCertificate(ctx, &sapb.AddCertificateRequest{ - Der: certCDER, - RegID: reg.Id, - Issued: timestamppb.New(issued), - }) - test.AssertNotError(t, err, "Failed to add CertC test certificate") - - // The count for the base domain should be 2 now: certA and certC. - // CertB should be ignored. - test.AssertEquals(t, countName(t, "not-example.com"), int64(2)) } func TestFinalizeAuthorization2(t *testing.T) { sa, fc, cleanUp := initSA(t) defer cleanUp() - fc.Set(time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)) + fc.Set(mustTime("2021-01-01 00:00")) - authzID := createPendingAuthorization(t, sa, "aaa", fc.Now().Add(time.Hour)) + authzID := createPendingAuthorization(t, sa, identifier.NewDNS("aaa"), fc.Now().Add(time.Hour)) expires := fc.Now().Add(time.Hour * 2).UTC() attemptedAt := fc.Now() - ip, _ := net.ParseIP("1.1.1.1").MarshalText() + ip, _ := netip.MustParseAddr("1.1.1.1").MarshalText() _, err := sa.FinalizeAuthorization2(context.Background(), &sapb.FinalizeAuthorizationRequest{ Id: authzID, @@ -2723,7 +2240,7 @@ func TestFinalizeAuthorization2(t *testing.T) { test.AssertEquals(t, dbVer.Challenges[0].Validationrecords[0].ResolverAddrs[0], "resolver:5353") test.AssertEquals(t, dbVer.Challenges[0].Validated.AsTime(), attemptedAt) - authzID = createPendingAuthorization(t, sa, "aaa", fc.Now().Add(time.Hour)) + authzID = createPendingAuthorization(t, sa, identifier.NewDNS("aaa"), fc.Now().Add(time.Hour)) prob, _ := bgrpc.ProblemDetailsToPB(probs.Connection("it went bad captain")) _, err = sa.FinalizeAuthorization2(context.Background(), &sapb.FinalizeAuthorizationRequest{ @@ -2759,14 +2276,14 @@ func TestRehydrateHostPort(t *testing.T) { sa, fc, cleanUp := initSA(t) defer cleanUp() - fc.Set(time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)) + fc.Set(mustTime("2021-01-01 00:00")) expires := fc.Now().Add(time.Hour * 2).UTC() attemptedAt := fc.Now() - ip, _ := net.ParseIP("1.1.1.1").MarshalText() + ip, _ := netip.MustParseAddr("1.1.1.1").MarshalText() // Implicit good port with good scheme - authzID := createPendingAuthorization(t, sa, "aaa", fc.Now().Add(time.Hour)) + authzID := createPendingAuthorization(t, sa, identifier.NewDNS("aaa"), fc.Now().Add(time.Hour)) _, err := sa.FinalizeAuthorization2(context.Background(), &sapb.FinalizeAuthorizationRequest{ Id: authzID, ValidationRecords: []*corepb.ValidationRecord{ @@ -2787,7 +2304,7 @@ func TestRehydrateHostPort(t *testing.T) { test.AssertNotError(t, err, "rehydration failed in some fun and interesting way") // Explicit good port with good scheme - authzID = createPendingAuthorization(t, sa, "aaa", fc.Now().Add(time.Hour)) + authzID = createPendingAuthorization(t, sa, identifier.NewDNS("aaa"), fc.Now().Add(time.Hour)) _, err = sa.FinalizeAuthorization2(context.Background(), &sapb.FinalizeAuthorizationRequest{ Id: authzID, ValidationRecords: []*corepb.ValidationRecord{ @@ -2808,7 +2325,7 @@ func TestRehydrateHostPort(t *testing.T) { test.AssertNotError(t, err, "rehydration failed in some fun and interesting way") // Explicit bad port with good scheme - authzID = createPendingAuthorization(t, sa, "aaa", fc.Now().Add(time.Hour)) + authzID = createPendingAuthorization(t, sa, identifier.NewDNS("aaa"), fc.Now().Add(time.Hour)) _, err = sa.FinalizeAuthorization2(context.Background(), &sapb.FinalizeAuthorizationRequest{ Id: authzID, ValidationRecords: []*corepb.ValidationRecord{ @@ -2829,7 +2346,7 @@ func TestRehydrateHostPort(t *testing.T) { test.AssertError(t, err, "only ports 80/tcp and 443/tcp are allowed in URL \"http://example.com:444\"") // Explicit bad port with bad scheme - authzID = createPendingAuthorization(t, sa, "aaa", fc.Now().Add(time.Hour)) + authzID = createPendingAuthorization(t, sa, identifier.NewDNS("aaa"), fc.Now().Add(time.Hour)) _, err = sa.FinalizeAuthorization2(context.Background(), &sapb.FinalizeAuthorizationRequest{ Id: authzID, ValidationRecords: []*corepb.ValidationRecord{ @@ -2850,7 +2367,7 @@ func TestRehydrateHostPort(t *testing.T) { test.AssertError(t, err, "unknown scheme \"httpx\" in URL \"httpx://example.com\"") // Missing URL field - authzID = createPendingAuthorization(t, sa, "aaa", fc.Now().Add(time.Hour)) + authzID = createPendingAuthorization(t, sa, identifier.NewDNS("aaa"), fc.Now().Add(time.Hour)) _, err = sa.FinalizeAuthorization2(context.Background(), &sapb.FinalizeAuthorizationRequest{ Id: authzID, ValidationRecords: []*corepb.ValidationRecord{ @@ -2870,44 +2387,14 @@ func TestRehydrateHostPort(t *testing.T) { test.AssertError(t, err, "URL field cannot be empty") } -func TestGetPendingAuthorization2(t *testing.T) { - sa, fc, cleanUp := initSA(t) - defer cleanUp() - - domain := "example.com" - expiresA := fc.Now().Add(time.Hour).UTC() - expiresB := fc.Now().Add(time.Hour * 3).UTC() - authzIDA := createPendingAuthorization(t, sa, domain, expiresA) - authzIDB := createPendingAuthorization(t, sa, domain, expiresB) - - regID := int64(1) - validUntil := fc.Now().Add(time.Hour * 2).UTC() - dbVer, err := sa.GetPendingAuthorization2(context.Background(), &sapb.GetPendingAuthorizationRequest{ - RegistrationID: regID, - IdentifierValue: domain, - ValidUntil: timestamppb.New(validUntil), - }) - test.AssertNotError(t, err, "sa.GetPendingAuthorization2 failed") - test.AssertEquals(t, fmt.Sprintf("%d", authzIDB), dbVer.Id) - - validUntil = fc.Now().UTC() - dbVer, err = sa.GetPendingAuthorization2(context.Background(), &sapb.GetPendingAuthorizationRequest{ - RegistrationID: regID, - IdentifierValue: domain, - ValidUntil: timestamppb.New(validUntil), - }) - test.AssertNotError(t, err, "sa.GetPendingAuthorization2 failed") - test.AssertEquals(t, fmt.Sprintf("%d", authzIDA), dbVer.Id) -} - func TestCountPendingAuthorizations2(t *testing.T) { sa, fc, cleanUp := initSA(t) defer cleanUp() expiresA := fc.Now().Add(time.Hour).UTC() expiresB := fc.Now().Add(time.Hour * 3).UTC() - _ = createPendingAuthorization(t, sa, "example.com", expiresA) - _ = createPendingAuthorization(t, sa, "example.com", expiresB) + _ = createPendingAuthorization(t, sa, identifier.NewDNS("example.com"), expiresA) + _ = createPendingAuthorization(t, sa, identifier.NewDNS("example.com"), expiresB) // Registration has two new style pending authorizations regID := int64(1) @@ -2936,8 +2423,8 @@ func TestCountPendingAuthorizations2(t *testing.T) { func TestAuthzModelMapToPB(t *testing.T) { baseExpires := time.Now() - input := map[string]authzModel{ - "example.com": { + input := map[identifier.ACMEIdentifier]authzModel{ + identifier.NewDNS("example.com"): { ID: 123, IdentifierType: 0, IdentifierValue: "example.com", @@ -2946,7 +2433,7 @@ func TestAuthzModelMapToPB(t *testing.T) { Expires: baseExpires, Challenges: 4, }, - "www.example.com": { + identifier.NewDNS("www.example.com"): { ID: 124, IdentifierType: 0, IdentifierValue: "www.example.com", @@ -2955,7 +2442,7 @@ func TestAuthzModelMapToPB(t *testing.T) { Expires: baseExpires, Challenges: 1, }, - "other.example.net": { + identifier.NewDNS("other.example.net"): { ID: 125, IdentifierType: 0, IdentifierValue: "other.example.net", @@ -2964,6 +2451,15 @@ func TestAuthzModelMapToPB(t *testing.T) { Expires: baseExpires, Challenges: 3, }, + identifier.NewIP(netip.MustParseAddr("10.10.10.10")): { + ID: 126, + IdentifierType: 1, + IdentifierValue: "10.10.10.10", + RegistrationID: 77, + Status: 1, + Expires: baseExpires, + Challenges: 5, + }, } out, err := authzModelMapToPB(input) @@ -2971,35 +2467,35 @@ func TestAuthzModelMapToPB(t *testing.T) { t.Fatal(err) } - for _, el := range out.Authz { - model, ok := input[el.Domain] + for _, authzPB := range out.Authzs { + model, ok := input[identifier.FromProto(authzPB.Identifier)] if !ok { - t.Errorf("output had element for %q, a hostname not present in input", el.Domain) + t.Errorf("output had element for %q, an identifier not present in input", authzPB.Identifier.Value) } - authzPB := el.Authz test.AssertEquals(t, authzPB.Id, fmt.Sprintf("%d", model.ID)) - test.AssertEquals(t, authzPB.Identifier, model.IdentifierValue) + test.AssertEquals(t, authzPB.Identifier.Type, string(uintToIdentifierType[model.IdentifierType])) + test.AssertEquals(t, authzPB.Identifier.Value, model.IdentifierValue) test.AssertEquals(t, authzPB.RegistrationID, model.RegistrationID) test.AssertEquals(t, authzPB.Status, string(uintToStatus[model.Status])) gotTime := authzPB.Expires.AsTime() if !model.Expires.Equal(gotTime) { t.Errorf("Times didn't match. Got %s, expected %s (%s)", gotTime, model.Expires, authzPB.Expires.AsTime()) } - if len(el.Authz.Challenges) != bits.OnesCount(uint(model.Challenges)) { - t.Errorf("wrong number of challenges for %q: got %d, expected %d", el.Domain, - len(el.Authz.Challenges), bits.OnesCount(uint(model.Challenges))) + if len(authzPB.Challenges) != bits.OnesCount(uint(model.Challenges)) { + t.Errorf("wrong number of challenges for %q: got %d, expected %d", authzPB.Identifier.Value, + len(authzPB.Challenges), bits.OnesCount(uint(model.Challenges))) } switch model.Challenges { case 1: - test.AssertEquals(t, el.Authz.Challenges[0].Type, "http-01") + test.AssertEquals(t, authzPB.Challenges[0].Type, "http-01") case 3: - test.AssertEquals(t, el.Authz.Challenges[0].Type, "http-01") - test.AssertEquals(t, el.Authz.Challenges[1].Type, "dns-01") + test.AssertEquals(t, authzPB.Challenges[0].Type, "http-01") + test.AssertEquals(t, authzPB.Challenges[1].Type, "dns-01") case 4: - test.AssertEquals(t, el.Authz.Challenges[0].Type, "tls-alpn-01") + test.AssertEquals(t, authzPB.Challenges[0].Type, "tls-alpn-01") } - delete(input, el.Domain) + delete(input, identifier.FromProto(authzPB.Identifier)) } for k := range input { @@ -3011,122 +2507,222 @@ func TestGetValidOrderAuthorizations2(t *testing.T) { sa, fc, cleanup := initSA(t) defer cleanup() - // Create two new valid authorizations + // Create three new valid authorizations reg := createWorkingRegistration(t, sa) - identA := "a.example.com" - identB := "b.example.com" + identA := identifier.NewDNS("a.example.com") + identB := identifier.NewDNS("b.example.com") + identC := identifier.NewIP(netip.MustParseAddr("3fff:aaa:aaaa:aaaa:abad:0ff1:cec0:ffee")) expires := fc.Now().Add(time.Hour * 24 * 7).UTC() attemptedAt := fc.Now() authzIDA := createFinalizedAuthorization(t, sa, identA, expires, "valid", attemptedAt) authzIDB := createFinalizedAuthorization(t, sa, identB, expires, "valid", attemptedAt) + authzIDC := createFinalizedAuthorization(t, sa, identC, expires, "valid", attemptedAt) orderExpr := fc.Now().Truncate(time.Second) order, err := sa.NewOrderAndAuthzs(context.Background(), &sapb.NewOrderAndAuthzsRequest{ NewOrder: &sapb.NewOrderRequest{ - RegistrationID: reg.Id, - Expires: timestamppb.New(orderExpr), - Names: []string{"a.example.com", "b.example.com"}, - V2Authorizations: []int64{authzIDA, authzIDB}, + RegistrationID: reg.Id, + Expires: timestamppb.New(orderExpr), + Identifiers: []*corepb.Identifier{ + identifier.NewDNS("a.example.com").ToProto(), + identifier.NewDNS("b.example.com").ToProto(), + identifier.NewIP(netip.MustParseAddr("3fff:aaa:aaaa:aaaa:abad:0ff1:cec0:ffee")).ToProto(), + }, + V2Authorizations: []int64{authzIDA, authzIDB, authzIDC}, }, }) test.AssertNotError(t, err, "AddOrder failed") - authzMap, err := sa.GetValidOrderAuthorizations2( + authzPBs, err := sa.GetValidOrderAuthorizations2( context.Background(), &sapb.GetValidOrderAuthorizationsRequest{ Id: order.Id, AcctID: reg.Id, }) test.AssertNotError(t, err, "sa.GetValidOrderAuthorizations failed") - test.AssertNotNil(t, authzMap, "sa.GetValidOrderAuthorizations result was nil") - test.AssertEquals(t, len(authzMap.Authz), 2) + test.AssertNotNil(t, authzPBs, "sa.GetValidOrderAuthorizations result was nil") + test.AssertEquals(t, len(authzPBs.Authzs), 3) - namesToCheck := map[string]int64{"a.example.com": authzIDA, "b.example.com": authzIDB} - for _, a := range authzMap.Authz { - if fmt.Sprintf("%d", namesToCheck[a.Authz.Identifier]) != a.Authz.Id { - t.Fatalf("incorrect identifier %q with id %s", a.Authz.Identifier, a.Authz.Id) + identsToCheck := map[identifier.ACMEIdentifier]int64{ + identifier.NewDNS("a.example.com"): authzIDA, + identifier.NewDNS("b.example.com"): authzIDB, + identifier.NewIP(netip.MustParseAddr("3fff:aaa:aaaa:aaaa:abad:0ff1:cec0:ffee")): authzIDC, + } + for _, a := range authzPBs.Authzs { + ident := identifier.ACMEIdentifier{Type: identifier.IdentifierType(a.Identifier.Type), Value: a.Identifier.Value} + if fmt.Sprintf("%d", identsToCheck[ident]) != a.Id { + t.Fatalf("incorrect identifier %q with id %s", a.Identifier.Value, a.Id) } - test.AssertEquals(t, a.Authz.Expires.AsTime(), expires) - delete(namesToCheck, a.Authz.Identifier) + test.AssertEquals(t, a.Expires.AsTime(), expires) + delete(identsToCheck, ident) } // Getting the order authorizations for an order that doesn't exist should return nothing missingID := int64(0xC0FFEEEEEEE) - authzMap, err = sa.GetValidOrderAuthorizations2( + authzPBs, err = sa.GetValidOrderAuthorizations2( context.Background(), &sapb.GetValidOrderAuthorizationsRequest{ Id: missingID, AcctID: reg.Id, }) test.AssertNotError(t, err, "sa.GetValidOrderAuthorizations failed") - test.AssertEquals(t, len(authzMap.Authz), 0) - - // Getting the order authorizations for an order that does exist, but for the - // wrong acct ID should return nothing - wrongAcctID := int64(0xDEADDA7ABA5E) - authzMap, err = sa.GetValidOrderAuthorizations2( - context.Background(), - &sapb.GetValidOrderAuthorizationsRequest{ - Id: order.Id, - AcctID: wrongAcctID, - }) - test.AssertNotError(t, err, "sa.GetValidOrderAuthorizations failed") - test.AssertEquals(t, len(authzMap.Authz), 0) + test.AssertEquals(t, len(authzPBs.Authzs), 0) } func TestCountInvalidAuthorizations2(t *testing.T) { sa, fc, cleanUp := initSA(t) defer cleanUp() - // Create two authorizations, one pending, one invalid fc.Add(time.Hour) reg := createWorkingRegistration(t, sa) - ident := "aaa" - expiresA := fc.Now().Add(time.Hour).UTC() - expiresB := fc.Now().Add(time.Hour * 3).UTC() - attemptedAt := fc.Now() - _ = createFinalizedAuthorization(t, sa, ident, expiresA, "invalid", attemptedAt) - _ = createPendingAuthorization(t, sa, ident, expiresB) + idents := identifier.ACMEIdentifiers{ + identifier.NewDNS("aaa"), + identifier.NewIP(netip.MustParseAddr("10.10.10.10")), + } + for _, ident := range idents { + // Create two authorizations, one pending, one invalid + expiresA := fc.Now().Add(time.Hour).UTC() + expiresB := fc.Now().Add(time.Hour * 3).UTC() + attemptedAt := fc.Now() + _ = createFinalizedAuthorization(t, sa, ident, expiresA, "invalid", attemptedAt) + _ = createPendingAuthorization(t, sa, ident, expiresB) - earliest := fc.Now().Add(-time.Hour).UTC() - latest := fc.Now().Add(time.Hour * 5).UTC() - count, err := sa.CountInvalidAuthorizations2(context.Background(), &sapb.CountInvalidAuthorizationsRequest{ - RegistrationID: reg.Id, - Hostname: ident, - Range: &sapb.Range{ - Earliest: timestamppb.New(earliest), - Latest: timestamppb.New(latest), - }, - }) - test.AssertNotError(t, err, "sa.CountInvalidAuthorizations2 failed") - test.AssertEquals(t, count.Count, int64(1)) + earliest := fc.Now().Add(-time.Hour).UTC() + latest := fc.Now().Add(time.Hour * 5).UTC() + count, err := sa.CountInvalidAuthorizations2(context.Background(), &sapb.CountInvalidAuthorizationsRequest{ + RegistrationID: reg.Id, + Identifier: ident.ToProto(), + Range: &sapb.Range{ + Earliest: timestamppb.New(earliest), + Latest: timestamppb.New(latest), + }, + }) + test.AssertNotError(t, err, "sa.CountInvalidAuthorizations2 failed") + test.AssertEquals(t, count.Count, int64(1)) + } } func TestGetValidAuthorizations2(t *testing.T) { sa, fc, cleanUp := initSA(t) defer cleanUp() - // Create a valid authorization - ident := "aaa" - expires := fc.Now().Add(time.Hour).UTC() - attemptedAt := fc.Now() - authzID := createFinalizedAuthorization(t, sa, ident, expires, "valid", attemptedAt) + var aaa int64 + { + tokenStr := core.NewToken() + token, err := base64.RawURLEncoding.DecodeString(tokenStr) + test.AssertNotError(t, err, "computing test authorization challenge token") - now := fc.Now().UTC() - regID := int64(1) - authzs, err := sa.GetValidAuthorizations2(context.Background(), &sapb.GetValidAuthorizationsRequest{ - Domains: []string{ - "aaa", - "bbb", + profile := "test" + attempted := challTypeToUint[string(core.ChallengeTypeHTTP01)] + attemptedAt := fc.Now() + vr, _ := json.Marshal([]core.ValidationRecord{}) + + am := authzModel{ + IdentifierType: identifierTypeToUint[string(identifier.TypeDNS)], + IdentifierValue: "aaa", + RegistrationID: 1, + CertificateProfileName: &profile, + Status: statusToUint[core.StatusValid], + Expires: fc.Now().Add(24 * time.Hour), + Challenges: 1 << challTypeToUint[string(core.ChallengeTypeHTTP01)], + Attempted: &attempted, + AttemptedAt: &attemptedAt, + Token: token, + ValidationError: nil, + ValidationRecord: vr, + } + + err = sa.dbMap.Insert(context.Background(), &am) + test.AssertNotError(t, err, "failed to insert valid authz") + + aaa = am.ID + } + + for _, tc := range []struct { + name string + regID int64 + identifiers []*corepb.Identifier + profile string + validUntil time.Time + wantIDs []int64 + }{ + { + name: "happy path, DNS identifier", + regID: 1, + identifiers: []*corepb.Identifier{identifier.NewDNS("aaa").ToProto()}, + profile: "test", + validUntil: fc.Now().Add(time.Hour), + wantIDs: []int64{aaa}, }, - RegistrationID: regID, - Now: timestamppb.New(now), - }) - test.AssertNotError(t, err, "sa.GetValidAuthorizations2 failed") - test.AssertEquals(t, len(authzs.Authz), 1) - test.AssertEquals(t, authzs.Authz[0].Domain, ident) - test.AssertEquals(t, authzs.Authz[0].Authz.Id, fmt.Sprintf("%d", authzID)) + { + name: "different identifier type", + regID: 1, + identifiers: []*corepb.Identifier{identifier.NewIP(netip.MustParseAddr("10.10.10.10")).ToProto()}, + profile: "test", + validUntil: fc.Now().Add(time.Hour), + wantIDs: []int64{}, + }, + { + name: "different regID", + regID: 2, + identifiers: []*corepb.Identifier{identifier.NewDNS("aaa").ToProto()}, + profile: "test", + validUntil: fc.Now().Add(time.Hour), + wantIDs: []int64{}, + }, + { + name: "different DNS identifier", + regID: 1, + identifiers: []*corepb.Identifier{identifier.NewDNS("bbb").ToProto()}, + profile: "test", + validUntil: fc.Now().Add(time.Hour), + wantIDs: []int64{}, + }, + { + name: "different profile", + regID: 1, + identifiers: []*corepb.Identifier{identifier.NewDNS("aaa").ToProto()}, + profile: "other", + validUntil: fc.Now().Add(time.Hour), + wantIDs: []int64{}, + }, + { + name: "too-far-out validUntil", + regID: 2, + identifiers: []*corepb.Identifier{identifier.NewDNS("aaa").ToProto()}, + profile: "test", + validUntil: fc.Now().Add(25 * time.Hour), + wantIDs: []int64{}, + }, + } { + t.Run(tc.name, func(t *testing.T) { + got, err := sa.GetValidAuthorizations2(context.Background(), &sapb.GetValidAuthorizationsRequest{ + RegistrationID: tc.regID, + Identifiers: tc.identifiers, + Profile: tc.profile, + ValidUntil: timestamppb.New(tc.validUntil), + }) + if err != nil { + t.Fatalf("GetValidAuthorizations2 got error %q, want success", err) + } + + var gotIDs []int64 + for _, authz := range got.Authzs { + id, err := strconv.Atoi(authz.Id) + if err != nil { + t.Fatalf("parsing authz id: %s", err) + } + gotIDs = append(gotIDs, int64(id)) + } + + slices.Sort(gotIDs) + slices.Sort(tc.wantIDs) + if !slices.Equal(gotIDs, tc.wantIDs) { + t.Errorf("GetValidAuthorizations2() = %+v, want %+v", gotIDs, tc.wantIDs) + } + }) + } } func TestGetOrderExpired(t *testing.T) { @@ -3139,7 +2735,7 @@ func TestGetOrderExpired(t *testing.T) { NewOrder: &sapb.NewOrderRequest{ RegistrationID: reg.Id, Expires: timestamppb.New(now.Add(-time.Hour)), - Names: []string{"example.com"}, + Identifiers: []*corepb.Identifier{identifier.NewDNS("example.com").ToProto()}, V2Authorizations: []int64{666}, }, }) @@ -3397,7 +2993,7 @@ func TestSerialsForIncident(t *testing.T) { "1335": true, "1336": true, "1337": true, "1338": true, } for i := range expectedSerials { - randInt := func() int64 { return mrand.Int63() } + randInt := func() int64 { return mrand.Int64() } _, err := testIncidentsDbMap.ExecContext(ctx, fmt.Sprintf("INSERT INTO incident_foo (%s) VALUES ('%s', %d, %d, '%s')", "serial, registrationID, orderID, lastNoticeSent", @@ -3486,83 +3082,65 @@ func TestGetRevokedCerts(t *testing.T) { return entriesReceived, err } - // Asking for revoked certs now should return no results. - expiresAfter := time.Date(2023, time.March, 1, 0, 0, 0, 0, time.UTC) - expiresBefore := time.Date(2023, time.April, 1, 0, 0, 0, 0, time.UTC) - revokedBefore := time.Date(2023, time.April, 1, 0, 0, 0, 0, time.UTC) - count, err := countRevokedCerts(&sapb.GetRevokedCertsRequest{ + // The basic request covers a time range that should include this certificate. + basicRequest := &sapb.GetRevokedCertsRequest{ IssuerNameID: 1, - ExpiresAfter: timestamppb.New(expiresAfter), - ExpiresBefore: timestamppb.New(expiresBefore), - RevokedBefore: timestamppb.New(revokedBefore), - }) + ExpiresAfter: mustTimestamp("2023-03-01 00:00"), + ExpiresBefore: mustTimestamp("2023-04-01 00:00"), + RevokedBefore: mustTimestamp("2023-04-01 00:00"), + } + count, err := countRevokedCerts(basicRequest) test.AssertNotError(t, err, "zero rows shouldn't result in error") test.AssertEquals(t, count, 0) // Revoke the certificate. - date := time.Date(2023, time.January, 1, 0, 0, 0, 0, time.UTC) _, err = sa.RevokeCertificate(context.Background(), &sapb.RevokeCertificateRequest{ IssuerID: 1, Serial: core.SerialToString(eeCert.SerialNumber), - Date: timestamppb.New(date), + Date: mustTimestamp("2023-01-01 00:00"), Reason: 1, Response: []byte{1, 2, 3}, }) test.AssertNotError(t, err, "failed to revoke test cert") // Asking for revoked certs now should return one result. - count, err = countRevokedCerts(&sapb.GetRevokedCertsRequest{ - IssuerNameID: 1, - ExpiresAfter: timestamppb.New(expiresAfter), - ExpiresBefore: timestamppb.New(expiresBefore), - RevokedBefore: timestamppb.New(revokedBefore), - }) + count, err = countRevokedCerts(basicRequest) test.AssertNotError(t, err, "normal usage shouldn't result in error") test.AssertEquals(t, count, 1) // Asking for revoked certs with an old RevokedBefore should return no results. - expiresAfter = time.Date(2023, time.March, 1, 0, 0, 0, 0, time.UTC) - expiresBefore = time.Date(2023, time.April, 1, 0, 0, 0, 0, time.UTC) - revokedBefore = time.Date(2020, time.March, 1, 0, 0, 0, 0, time.UTC) count, err = countRevokedCerts(&sapb.GetRevokedCertsRequest{ IssuerNameID: 1, - ExpiresAfter: timestamppb.New(expiresAfter), - ExpiresBefore: timestamppb.New(expiresBefore), - RevokedBefore: timestamppb.New(revokedBefore), + ExpiresAfter: basicRequest.ExpiresAfter, + ExpiresBefore: basicRequest.ExpiresBefore, + RevokedBefore: mustTimestamp("2020-03-01 00:00"), }) test.AssertNotError(t, err, "zero rows shouldn't result in error") test.AssertEquals(t, count, 0) // Asking for revoked certs in a time period that does not cover this cert's // notAfter timestamp should return zero results. - expiresAfter = time.Date(2022, time.March, 1, 0, 0, 0, 0, time.UTC) - expiresBefore = time.Date(2022, time.April, 1, 0, 0, 0, 0, time.UTC) - revokedBefore = time.Date(2023, time.April, 1, 0, 0, 0, 0, time.UTC) count, err = countRevokedCerts(&sapb.GetRevokedCertsRequest{ IssuerNameID: 1, - ExpiresAfter: timestamppb.New(expiresAfter), - ExpiresBefore: timestamppb.New(expiresBefore), - RevokedBefore: timestamppb.New(revokedBefore), + ExpiresAfter: mustTimestamp("2022-03-01 00:00"), + ExpiresBefore: mustTimestamp("2022-04-01 00:00"), + RevokedBefore: mustTimestamp("2023-04-01 00:00"), }) test.AssertNotError(t, err, "zero rows shouldn't result in error") test.AssertEquals(t, count, 0) // Asking for revoked certs from a different issuer should return zero results. count, err = countRevokedCerts(&sapb.GetRevokedCertsRequest{ - IssuerNameID: 1, - ExpiresAfter: timestamppb.New(time.Date(2022, time.March, 1, 0, 0, 0, 0, time.UTC)), - ExpiresBefore: timestamppb.New(time.Date(2022, time.April, 1, 0, 0, 0, 0, time.UTC)), - RevokedBefore: timestamppb.New(time.Date(2023, time.April, 1, 0, 0, 0, 0, time.UTC)), + IssuerNameID: 5678, + ExpiresAfter: basicRequest.ExpiresAfter, + ExpiresBefore: basicRequest.ExpiresBefore, + RevokedBefore: basicRequest.RevokedBefore, }) test.AssertNotError(t, err, "zero rows shouldn't result in error") test.AssertEquals(t, count, 0) } func TestGetRevokedCertsByShard(t *testing.T) { - if os.Getenv("BOULDER_CONFIG_DIR") != "test/config-next" { - t.Skip("Test requires revokedCertificates database table") - } - sa, _, cleanUp := initSA(t) defer cleanUp() @@ -3593,14 +3171,14 @@ func TestGetRevokedCertsByShard(t *testing.T) { test.AssertNotError(t, err, "GetCertificateStatus failed") test.AssertEquals(t, core.OCSPStatus(status.Status), core.OCSPStatusGood) - // Here's a little helper func we'll use to call GetRevokedCerts and count + // Here's a little helper func we'll use to call GetRevokedCertsByShard and count // how many results it returned. - countRevokedCerts := func(req *sapb.GetRevokedCertsRequest) (int, error) { + countRevokedCerts := func(req *sapb.GetRevokedCertsByShardRequest) (int, error) { stream := make(chan *corepb.CRLEntry) mockServerStream := &fakeServerStream[corepb.CRLEntry]{output: stream} var err error go func() { - err = sa.GetRevokedCerts(req, mockServerStream) + err = sa.GetRevokedCertsByShard(req, mockServerStream) close(stream) }() entriesReceived := 0 @@ -3610,25 +3188,25 @@ func TestGetRevokedCertsByShard(t *testing.T) { return entriesReceived, err } - // Asking for revoked certs now should return no results. - expiresAfter := time.Date(2023, time.March, 1, 0, 0, 0, 0, time.UTC) - revokedBefore := time.Date(2023, time.April, 1, 0, 0, 0, 0, time.UTC) - count, err := countRevokedCerts(&sapb.GetRevokedCertsRequest{ + // The basic request covers a time range and shard that should include this certificate. + basicRequest := &sapb.GetRevokedCertsByShardRequest{ IssuerNameID: 1, ShardIdx: 9, - ExpiresAfter: timestamppb.New(expiresAfter), - RevokedBefore: timestamppb.New(revokedBefore), - }) + ExpiresAfter: mustTimestamp("2023-03-01 00:00"), + RevokedBefore: mustTimestamp("2023-04-01 00:00"), + } + + // Nothing's been revoked yet. Count should be zero. + count, err := countRevokedCerts(basicRequest) test.AssertNotError(t, err, "zero rows shouldn't result in error") test.AssertEquals(t, count, 0) // Revoke the certificate, providing the ShardIdx so it gets written into // both the certificateStatus and revokedCertificates tables. - date := time.Date(2023, time.January, 1, 0, 0, 0, 0, time.UTC) _, err = sa.RevokeCertificate(context.Background(), &sapb.RevokeCertificateRequest{ IssuerID: 1, Serial: core.SerialToString(eeCert.SerialNumber), - Date: timestamppb.New(date), + Date: mustTimestamp("2023-01-01 00:00"), Reason: 1, Response: []byte{1, 2, 3}, ShardIdx: 9, @@ -3643,49 +3221,36 @@ func TestGetRevokedCertsByShard(t *testing.T) { test.AssertEquals(t, c.Int64, int64(1)) // Asking for revoked certs now should return one result. - expiresAfter = time.Date(2023, time.March, 1, 0, 0, 0, 0, time.UTC) - revokedBefore = time.Date(2023, time.April, 1, 0, 0, 0, 0, time.UTC) - count, err = countRevokedCerts(&sapb.GetRevokedCertsRequest{ - IssuerNameID: 1, - ShardIdx: 9, - ExpiresAfter: timestamppb.New(expiresAfter), - RevokedBefore: timestamppb.New(revokedBefore), - }) + count, err = countRevokedCerts(basicRequest) test.AssertNotError(t, err, "normal usage shouldn't result in error") test.AssertEquals(t, count, 1) // Asking for revoked certs from a different issuer should return zero results. - expiresAfter = time.Date(2023, time.March, 1, 0, 0, 0, 0, time.UTC) - revokedBefore = time.Date(2023, time.April, 1, 0, 0, 0, 0, time.UTC) - count, err = countRevokedCerts(&sapb.GetRevokedCertsRequest{ - IssuerNameID: 2, - ShardIdx: 9, - ExpiresAfter: timestamppb.New(expiresAfter), - RevokedBefore: timestamppb.New(revokedBefore), + count, err = countRevokedCerts(&sapb.GetRevokedCertsByShardRequest{ + IssuerNameID: 5678, + ShardIdx: basicRequest.ShardIdx, + ExpiresAfter: basicRequest.ExpiresAfter, + RevokedBefore: basicRequest.RevokedBefore, }) test.AssertNotError(t, err, "zero rows shouldn't result in error") test.AssertEquals(t, count, 0) // Asking for revoked certs from a different shard should return zero results. - expiresAfter = time.Date(2023, time.March, 1, 0, 0, 0, 0, time.UTC) - revokedBefore = time.Date(2023, time.April, 1, 0, 0, 0, 0, time.UTC) - count, err = countRevokedCerts(&sapb.GetRevokedCertsRequest{ - IssuerNameID: 1, + count, err = countRevokedCerts(&sapb.GetRevokedCertsByShardRequest{ + IssuerNameID: basicRequest.IssuerNameID, ShardIdx: 8, - ExpiresAfter: timestamppb.New(expiresAfter), - RevokedBefore: timestamppb.New(revokedBefore), + ExpiresAfter: basicRequest.ExpiresAfter, + RevokedBefore: basicRequest.RevokedBefore, }) test.AssertNotError(t, err, "zero rows shouldn't result in error") test.AssertEquals(t, count, 0) // Asking for revoked certs with an old RevokedBefore should return no results. - expiresAfter = time.Date(2023, time.March, 1, 0, 0, 0, 0, time.UTC) - revokedBefore = time.Date(2020, time.March, 1, 0, 0, 0, 0, time.UTC) - count, err = countRevokedCerts(&sapb.GetRevokedCertsRequest{ - IssuerNameID: 1, - ShardIdx: 9, - ExpiresAfter: timestamppb.New(expiresAfter), - RevokedBefore: timestamppb.New(revokedBefore), + count, err = countRevokedCerts(&sapb.GetRevokedCertsByShardRequest{ + IssuerNameID: basicRequest.IssuerNameID, + ShardIdx: basicRequest.ShardIdx, + ExpiresAfter: basicRequest.ExpiresAfter, + RevokedBefore: mustTimestamp("2020-03-01 00:00"), }) test.AssertNotError(t, err, "zero rows shouldn't result in error") test.AssertEquals(t, count, 0) @@ -4013,7 +3578,7 @@ func TestUpdateCRLShard(t *testing.T) { `SELECT thisUpdate FROM crlShards WHERE issuerID = 1 AND idx = 0 LIMIT 1`, ) test.AssertNotError(t, err, "getting updated thisUpdate timestamp") - test.AssertEquals(t, *crlModel.ThisUpdate, thisUpdate) + test.Assert(t, crlModel.ThisUpdate.Equal(thisUpdate), "checking updated thisUpdate timestamp") // Updating an unleased shard should work. _, err = sa.UpdateCRLShard( @@ -4080,16 +3645,9 @@ func TestUpdateCRLShard(t *testing.T) { } func TestReplacementOrderExists(t *testing.T) { - if os.Getenv("BOULDER_CONFIG_DIR") != "test/config-next" { - t.Skip("Test requires replacementOrders database table") - } - sa, fc, cleanUp := initSA(t) defer cleanUp() - features.Set(features.Config{TrackReplacementCertificatesARI: true}) - defer features.Reset() - oldCertSerial := "1234567890" // Check that a non-existent replacement order does not exist. @@ -4103,7 +3661,7 @@ func TestReplacementOrderExists(t *testing.T) { // Add one valid authz. expires := fc.Now().Add(time.Hour) attemptedAt := fc.Now() - authzID := createFinalizedAuthorization(t, sa, "example.com", expires, "valid", attemptedAt) + authzID := createFinalizedAuthorization(t, sa, identifier.NewDNS("example.com"), expires, "valid", attemptedAt) // Add a new order in pending status with no certificate serial. expires1Year := sa.clk.Now().Add(365 * 24 * time.Hour) @@ -4111,7 +3669,7 @@ func TestReplacementOrderExists(t *testing.T) { NewOrder: &sapb.NewOrderRequest{ RegistrationID: reg.Id, Expires: timestamppb.New(expires1Year), - Names: []string{"example.com"}, + Identifiers: []*corepb.Identifier{identifier.NewDNS("example.com").ToProto()}, V2Authorizations: []int64{authzID}, }, }) @@ -4131,7 +3689,7 @@ func TestReplacementOrderExists(t *testing.T) { NewOrder: &sapb.NewOrderRequest{ RegistrationID: reg.Id, Expires: timestamppb.New(expires1Year), - Names: []string{"example.com"}, + Identifiers: []*corepb.Identifier{identifier.NewDNS("example.com").ToProto()}, V2Authorizations: []int64{authzID}, ReplacesSerial: oldCertSerial, }, @@ -4168,7 +3726,7 @@ func TestReplacementOrderExists(t *testing.T) { NewOrder: &sapb.NewOrderRequest{ RegistrationID: reg.Id, Expires: timestamppb.New(expires1Year), - Names: []string{"example.com"}, + Identifiers: []*corepb.Identifier{identifier.NewDNS("example.com").ToProto()}, V2Authorizations: []int64{authzID}, ReplacesSerial: oldCertSerial, }, @@ -4310,9 +3868,6 @@ func TestGetSerialsByAccount(t *testing.T) { } func TestUnpauseAccount(t *testing.T) { - if os.Getenv("BOULDER_CONFIG_DIR") != "test/config-next" { - t.Skip("Test requires paused database table") - } sa, _, cleanUp := initSA(t) defer cleanUp() @@ -4332,7 +3887,7 @@ func TestUnpauseAccount(t *testing.T) { { RegistrationID: 1, identifierModel: identifierModel{ - Type: identifierTypeToUint[string(identifier.DNS)], + Type: identifierTypeToUint[string(identifier.TypeDNS)], Value: "example.com", }, PausedAt: sa.clk.Now().Add(-time.Hour), @@ -4346,7 +3901,7 @@ func TestUnpauseAccount(t *testing.T) { { RegistrationID: 1, identifierModel: identifierModel{ - Type: identifierTypeToUint[string(identifier.DNS)], + Type: identifierTypeToUint[string(identifier.TypeDNS)], Value: "example.com", }, PausedAt: sa.clk.Now().Add(-time.Hour), @@ -4354,7 +3909,7 @@ func TestUnpauseAccount(t *testing.T) { { RegistrationID: 1, identifierModel: identifierModel{ - Type: identifierTypeToUint[string(identifier.DNS)], + Type: identifierTypeToUint[string(identifier.TypeDNS)], Value: "example.net", }, PausedAt: sa.clk.Now().Add(-time.Hour), @@ -4362,7 +3917,7 @@ func TestUnpauseAccount(t *testing.T) { { RegistrationID: 1, identifierModel: identifierModel{ - Type: identifierTypeToUint[string(identifier.DNS)], + Type: identifierTypeToUint[string(identifier.TypeDNS)], Value: "example.org", }, PausedAt: sa.clk.Now().Add(-time.Hour), @@ -4402,10 +3957,67 @@ func TestUnpauseAccount(t *testing.T) { } } -func TestPauseIdentifiers(t *testing.T) { - if os.Getenv("BOULDER_CONFIG_DIR") != "test/config-next" { - t.Skip("Test requires paused database table") +func bulkInsertPausedIdentifiers(ctx context.Context, sa *SQLStorageAuthority, count int) error { + const batchSize = 1000 + + values := make([]interface{}, 0, batchSize*4) + now := sa.clk.Now().Add(-time.Hour) + batches := (count + batchSize - 1) / batchSize + + for batch := 0; batch < batches; batch++ { + query := ` + INSERT INTO paused (registrationID, identifierType, identifierValue, pausedAt) + VALUES` + + start := batch * batchSize + end := start + batchSize + if end > count { + end = count + } + + for i := start; i < end; i++ { + if i > start { + query += "," + } + query += "(?, ?, ?, ?)" + values = append(values, 1, identifierTypeToUint[string(identifier.TypeDNS)], fmt.Sprintf("example%d.com", i), now) + } + + _, err := sa.dbMap.ExecContext(ctx, query, values...) + if err != nil { + return fmt.Errorf("bulk inserting paused identifiers: %w", err) + } + values = values[:0] } + + return nil +} + +func TestUnpauseAccountWithTwoLoops(t *testing.T) { + sa, _, cleanUp := initSA(t) + defer cleanUp() + + err := bulkInsertPausedIdentifiers(ctx, sa, 12000) + test.AssertNotError(t, err, "bulk inserting paused identifiers") + + result, err := sa.UnpauseAccount(ctx, &sapb.RegistrationID{Id: 1}) + test.AssertNotError(t, err, "Unexpected error for UnpauseAccount()") + test.AssertEquals(t, result.Count, int64(12000)) +} + +func TestUnpauseAccountWithMaxLoops(t *testing.T) { + sa, _, cleanUp := initSA(t) + defer cleanUp() + + err := bulkInsertPausedIdentifiers(ctx, sa, 50001) + test.AssertNotError(t, err, "bulk inserting paused identifiers") + + result, err := sa.UnpauseAccount(ctx, &sapb.RegistrationID{Id: 1}) + test.AssertNotError(t, err, "Unexpected error for UnpauseAccount()") + test.AssertEquals(t, result.Count, int64(50000)) +} + +func TestPauseIdentifiers(t *testing.T) { sa, _, cleanUp := initSA(t) defer cleanUp() @@ -4413,6 +4025,9 @@ func TestPauseIdentifiers(t *testing.T) { return &t } + fourWeeksAgo := sa.clk.Now().Add(-4 * 7 * 24 * time.Hour) + threeWeeksAgo := sa.clk.Now().Add(-3 * 7 * 24 * time.Hour) + tests := []struct { name string state []pausedModel @@ -4424,9 +4039,9 @@ func TestPauseIdentifiers(t *testing.T) { state: nil, req: &sapb.PauseRequest{ RegistrationID: 1, - Identifiers: []*sapb.Identifier{ + Identifiers: []*corepb.Identifier{ { - Type: string(identifier.DNS), + Type: string(identifier.TypeDNS), Value: "example.com", }, }, @@ -4442,18 +4057,18 @@ func TestPauseIdentifiers(t *testing.T) { { RegistrationID: 1, identifierModel: identifierModel{ - Type: identifierTypeToUint[string(identifier.DNS)], + Type: identifierTypeToUint[string(identifier.TypeDNS)], Value: "example.com", }, - PausedAt: sa.clk.Now().Add(-time.Hour), - UnpausedAt: ptrTime(sa.clk.Now().Add(-time.Minute)), + PausedAt: fourWeeksAgo, + UnpausedAt: ptrTime(threeWeeksAgo), }, }, req: &sapb.PauseRequest{ RegistrationID: 1, - Identifiers: []*sapb.Identifier{ + Identifiers: []*corepb.Identifier{ { - Type: string(identifier.DNS), + Type: string(identifier.TypeDNS), Value: "example.com", }, }, @@ -4463,23 +4078,50 @@ func TestPauseIdentifiers(t *testing.T) { Repaused: 1, }, }, + { + name: "One unpaused entry which was previously paused and unpaused less than 2 weeks ago", + state: []pausedModel{ + { + RegistrationID: 1, + identifierModel: identifierModel{ + Type: identifierTypeToUint[string(identifier.TypeDNS)], + Value: "example.com", + }, + PausedAt: fourWeeksAgo, + UnpausedAt: ptrTime(sa.clk.Now().Add(-13 * 24 * time.Hour)), + }, + }, + req: &sapb.PauseRequest{ + RegistrationID: 1, + Identifiers: []*corepb.Identifier{ + { + Type: string(identifier.TypeDNS), + Value: "example.com", + }, + }, + }, + want: &sapb.PauseIdentifiersResponse{ + Paused: 0, + Repaused: 0, + }, + }, { name: "An identifier which is currently paused", state: []pausedModel{ { RegistrationID: 1, identifierModel: identifierModel{ - Type: identifierTypeToUint[string(identifier.DNS)], + Type: identifierTypeToUint[string(identifier.TypeDNS)], Value: "example.com", }, - PausedAt: sa.clk.Now().Add(-time.Hour), + PausedAt: fourWeeksAgo, }, }, req: &sapb.PauseRequest{ RegistrationID: 1, - Identifiers: []*sapb.Identifier{ + Identifiers: []*corepb.Identifier{ { - Type: string(identifier.DNS), + Type: string(identifier.TypeDNS), Value: "example.com", }, }, @@ -4495,35 +4137,35 @@ func TestPauseIdentifiers(t *testing.T) { { RegistrationID: 1, identifierModel: identifierModel{ - Type: identifierTypeToUint[string(identifier.DNS)], + Type: identifierTypeToUint[string(identifier.TypeDNS)], Value: "example.com", }, - PausedAt: sa.clk.Now().Add(-time.Hour), - UnpausedAt: ptrTime(sa.clk.Now().Add(-time.Minute)), + PausedAt: fourWeeksAgo, + UnpausedAt: ptrTime(threeWeeksAgo), }, { RegistrationID: 1, identifierModel: identifierModel{ - Type: identifierTypeToUint[string(identifier.DNS)], + Type: identifierTypeToUint[string(identifier.TypeDNS)], Value: "example.net", }, - PausedAt: sa.clk.Now().Add(-time.Hour), - UnpausedAt: ptrTime(sa.clk.Now().Add(-time.Minute)), + PausedAt: fourWeeksAgo, + UnpausedAt: ptrTime(threeWeeksAgo), }, }, req: &sapb.PauseRequest{ RegistrationID: 1, - Identifiers: []*sapb.Identifier{ + Identifiers: []*corepb.Identifier{ { - Type: string(identifier.DNS), + Type: string(identifier.TypeDNS), Value: "example.com", }, { - Type: string(identifier.DNS), + Type: string(identifier.TypeDNS), Value: "example.net", }, { - Type: string(identifier.DNS), + Type: string(identifier.TypeDNS), Value: "example.org", }, }, @@ -4557,9 +4199,6 @@ func TestPauseIdentifiers(t *testing.T) { } func TestCheckIdentifiersPaused(t *testing.T) { - if os.Getenv("BOULDER_CONFIG_DIR") != "test/config-next" { - t.Skip("Test requires paused database table") - } sa, _, cleanUp := initSA(t) defer cleanUp() @@ -4578,15 +4217,15 @@ func TestCheckIdentifiersPaused(t *testing.T) { state: nil, req: &sapb.PauseRequest{ RegistrationID: 1, - Identifiers: []*sapb.Identifier{ + Identifiers: []*corepb.Identifier{ { - Type: string(identifier.DNS), + Type: string(identifier.TypeDNS), Value: "example.com", }, }, }, want: &sapb.Identifiers{ - Identifiers: []*sapb.Identifier{}, + Identifiers: []*corepb.Identifier{}, }, }, { @@ -4595,7 +4234,7 @@ func TestCheckIdentifiersPaused(t *testing.T) { { RegistrationID: 1, identifierModel: identifierModel{ - Type: identifierTypeToUint[string(identifier.DNS)], + Type: identifierTypeToUint[string(identifier.TypeDNS)], Value: "example.com", }, PausedAt: sa.clk.Now().Add(-time.Hour), @@ -4603,17 +4242,17 @@ func TestCheckIdentifiersPaused(t *testing.T) { }, req: &sapb.PauseRequest{ RegistrationID: 1, - Identifiers: []*sapb.Identifier{ + Identifiers: []*corepb.Identifier{ { - Type: string(identifier.DNS), + Type: string(identifier.TypeDNS), Value: "example.com", }, }, }, want: &sapb.Identifiers{ - Identifiers: []*sapb.Identifier{ + Identifiers: []*corepb.Identifier{ { - Type: string(identifier.DNS), + Type: string(identifier.TypeDNS), Value: "example.com", }, }, @@ -4625,7 +4264,7 @@ func TestCheckIdentifiersPaused(t *testing.T) { { RegistrationID: 1, identifierModel: identifierModel{ - Type: identifierTypeToUint[string(identifier.DNS)], + Type: identifierTypeToUint[string(identifier.TypeDNS)], Value: "example.com", }, PausedAt: sa.clk.Now().Add(-time.Hour), @@ -4633,7 +4272,7 @@ func TestCheckIdentifiersPaused(t *testing.T) { { RegistrationID: 1, identifierModel: identifierModel{ - Type: identifierTypeToUint[string(identifier.DNS)], + Type: identifierTypeToUint[string(identifier.TypeDNS)], Value: "example.net", }, PausedAt: sa.clk.Now().Add(-time.Hour), @@ -4641,7 +4280,7 @@ func TestCheckIdentifiersPaused(t *testing.T) { { RegistrationID: 1, identifierModel: identifierModel{ - Type: identifierTypeToUint[string(identifier.DNS)], + Type: identifierTypeToUint[string(identifier.TypeDNS)], Value: "example.org", }, PausedAt: sa.clk.Now().Add(-time.Hour), @@ -4650,29 +4289,29 @@ func TestCheckIdentifiersPaused(t *testing.T) { }, req: &sapb.PauseRequest{ RegistrationID: 1, - Identifiers: []*sapb.Identifier{ + Identifiers: []*corepb.Identifier{ { - Type: string(identifier.DNS), + Type: string(identifier.TypeDNS), Value: "example.com", }, { - Type: string(identifier.DNS), + Type: string(identifier.TypeDNS), Value: "example.net", }, { - Type: string(identifier.DNS), + Type: string(identifier.TypeDNS), Value: "example.org", }, }, }, want: &sapb.Identifiers{ - Identifiers: []*sapb.Identifier{ + Identifiers: []*corepb.Identifier{ { - Type: string(identifier.DNS), + Type: string(identifier.TypeDNS), Value: "example.com", }, { - Type: string(identifier.DNS), + Type: string(identifier.TypeDNS), Value: "example.net", }, }, @@ -4701,9 +4340,6 @@ func TestCheckIdentifiersPaused(t *testing.T) { } func TestGetPausedIdentifiers(t *testing.T) { - if os.Getenv("BOULDER_CONFIG_DIR") != "test/config-next" { - t.Skip("Test requires paused database table") - } sa, _, cleanUp := initSA(t) defer cleanUp() @@ -4722,7 +4358,7 @@ func TestGetPausedIdentifiers(t *testing.T) { state: nil, req: &sapb.RegistrationID{Id: 1}, want: &sapb.Identifiers{ - Identifiers: []*sapb.Identifier{}, + Identifiers: []*corepb.Identifier{}, }, }, { @@ -4731,7 +4367,7 @@ func TestGetPausedIdentifiers(t *testing.T) { { RegistrationID: 1, identifierModel: identifierModel{ - Type: identifierTypeToUint[string(identifier.DNS)], + Type: identifierTypeToUint[string(identifier.TypeDNS)], Value: "example.com", }, PausedAt: sa.clk.Now().Add(-time.Hour), @@ -4739,9 +4375,9 @@ func TestGetPausedIdentifiers(t *testing.T) { }, req: &sapb.RegistrationID{Id: 1}, want: &sapb.Identifiers{ - Identifiers: []*sapb.Identifier{ + Identifiers: []*corepb.Identifier{ { - Type: string(identifier.DNS), + Type: string(identifier.TypeDNS), Value: "example.com", }, }, @@ -4753,7 +4389,7 @@ func TestGetPausedIdentifiers(t *testing.T) { { RegistrationID: 1, identifierModel: identifierModel{ - Type: identifierTypeToUint[string(identifier.DNS)], + Type: identifierTypeToUint[string(identifier.TypeDNS)], Value: "example.com", }, PausedAt: sa.clk.Now().Add(-time.Hour), @@ -4761,7 +4397,7 @@ func TestGetPausedIdentifiers(t *testing.T) { { RegistrationID: 1, identifierModel: identifierModel{ - Type: identifierTypeToUint[string(identifier.DNS)], + Type: identifierTypeToUint[string(identifier.TypeDNS)], Value: "example.net", }, PausedAt: sa.clk.Now().Add(-time.Hour), @@ -4769,7 +4405,7 @@ func TestGetPausedIdentifiers(t *testing.T) { { RegistrationID: 1, identifierModel: identifierModel{ - Type: identifierTypeToUint[string(identifier.DNS)], + Type: identifierTypeToUint[string(identifier.TypeDNS)], Value: "example.org", }, PausedAt: sa.clk.Now().Add(-time.Hour), @@ -4778,13 +4414,13 @@ func TestGetPausedIdentifiers(t *testing.T) { }, req: &sapb.RegistrationID{Id: 1}, want: &sapb.Identifiers{ - Identifiers: []*sapb.Identifier{ + Identifiers: []*corepb.Identifier{ { - Type: string(identifier.DNS), + Type: string(identifier.TypeDNS), Value: "example.com", }, { - Type: string(identifier.DNS), + Type: string(identifier.TypeDNS), Value: "example.net", }, }, @@ -4813,9 +4449,6 @@ func TestGetPausedIdentifiers(t *testing.T) { } func TestGetPausedIdentifiersOnlyUnpausesOneAccount(t *testing.T) { - if os.Getenv("BOULDER_CONFIG_DIR") != "test/config-next" { - t.Skip("Test requires paused database table") - } sa, _, cleanUp := initSA(t) defer cleanUp() @@ -4823,7 +4456,7 @@ func TestGetPausedIdentifiersOnlyUnpausesOneAccount(t *testing.T) { err := sa.dbMap.Insert(ctx, &pausedModel{ RegistrationID: 1, identifierModel: identifierModel{ - Type: identifierTypeToUint[string(identifier.DNS)], + Type: identifierTypeToUint[string(identifier.TypeDNS)], Value: "example.com", }, PausedAt: sa.clk.Now().Add(-time.Hour), @@ -4833,7 +4466,7 @@ func TestGetPausedIdentifiersOnlyUnpausesOneAccount(t *testing.T) { err = sa.dbMap.Insert(ctx, &pausedModel{ RegistrationID: 2, identifierModel: identifierModel{ - Type: identifierTypeToUint[string(identifier.DNS)], + Type: identifierTypeToUint[string(identifier.TypeDNS)], Value: "example.net", }, PausedAt: sa.clk.Now().Add(-time.Hour), @@ -4845,8 +4478,294 @@ func TestGetPausedIdentifiersOnlyUnpausesOneAccount(t *testing.T) { test.AssertNotError(t, err, "UnpauseAccount failed") // Check that the second account's identifier is still paused. - identifiers, err := sa.GetPausedIdentifiers(ctx, &sapb.RegistrationID{Id: 2}) + idents, err := sa.GetPausedIdentifiers(ctx, &sapb.RegistrationID{Id: 2}) test.AssertNotError(t, err, "GetPausedIdentifiers failed") - test.AssertEquals(t, len(identifiers.Identifiers), 1) - test.AssertEquals(t, identifiers.Identifiers[0].Value, "example.net") + test.AssertEquals(t, len(idents.Identifiers), 1) + test.AssertEquals(t, idents.Identifiers[0].Value, "example.net") +} + +func newAcctKey(t *testing.T) []byte { + key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + jwk := &jose.JSONWebKey{Key: key.Public()} + acctKey, err := jwk.MarshalJSON() + test.AssertNotError(t, err, "failed to marshal account key") + return acctKey +} + +func TestUpdateRegistrationContact(t *testing.T) { + // TODO(#8199): Delete this. + sa, _, cleanUp := initSA(t) + defer cleanUp() + + noContact, _ := json.Marshal("") + exampleContact, _ := json.Marshal("test@example.com") + twoExampleContacts, _ := json.Marshal([]string{"test1@example.com", "test2@example.com"}) + + _, err := sa.UpdateRegistrationContact(ctx, &sapb.UpdateRegistrationContactRequest{}) + test.AssertError(t, err, "should not have been able to update registration contact without a registration ID") + test.AssertContains(t, err.Error(), "incomplete gRPC request message") + + tests := []struct { + name string + oldContactsJSON []string + newContacts []string + }{ + { + name: "update a valid registration from no contacts to one email address", + oldContactsJSON: []string{string(noContact)}, + newContacts: []string{"mailto:test@example.com"}, + }, + { + name: "update a valid registration from no contacts to two email addresses", + oldContactsJSON: []string{string(noContact)}, + newContacts: []string{"mailto:test1@example.com", "mailto:test2@example.com"}, + }, + { + name: "update a valid registration from one email address to no contacts", + oldContactsJSON: []string{string(exampleContact)}, + newContacts: []string{}, + }, + { + name: "update a valid registration from one email address to two email addresses", + oldContactsJSON: []string{string(exampleContact)}, + newContacts: []string{"mailto:test1@example.com", "mailto:test2@example.com"}, + }, + { + name: "update a valid registration from two email addresses to no contacts", + oldContactsJSON: []string{string(twoExampleContacts)}, + newContacts: []string{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + reg, err := sa.NewRegistration(ctx, &corepb.Registration{ + Contact: tt.oldContactsJSON, + Key: newAcctKey(t), + }) + test.AssertNotError(t, err, "creating new registration") + + updatedReg, err := sa.UpdateRegistrationContact(ctx, &sapb.UpdateRegistrationContactRequest{ + RegistrationID: reg.Id, + Contacts: tt.newContacts, + }) + test.AssertNotError(t, err, "unexpected error for UpdateRegistrationContact()") + test.AssertEquals(t, updatedReg.Id, reg.Id) + test.AssertEquals(t, len(updatedReg.Contact), 0) + + refetchedReg, err := sa.GetRegistration(ctx, &sapb.RegistrationID{Id: reg.Id}) + test.AssertNotError(t, err, "retrieving registration") + test.AssertEquals(t, refetchedReg.Id, reg.Id) + test.AssertEquals(t, len(refetchedReg.Contact), 0) + }) + } +} + +func TestUpdateRegistrationKey(t *testing.T) { + sa, _, cleanUp := initSA(t) + defer cleanUp() + + _, err := sa.UpdateRegistrationKey(ctx, &sapb.UpdateRegistrationKeyRequest{}) + test.AssertError(t, err, "should not have been able to update registration key without a registration ID") + test.AssertContains(t, err.Error(), "incomplete gRPC request message") + + existingReg, err := sa.NewRegistration(ctx, &corepb.Registration{ + Key: newAcctKey(t), + }) + test.AssertNotError(t, err, "creating new registration") + + tests := []struct { + name string + newJwk []byte + expectedError string + }{ + { + name: "update a valid registration with a new account key", + newJwk: newAcctKey(t), + }, + { + name: "update a valid registration with a duplicate account key", + newJwk: existingReg.Key, + expectedError: "key is already in use for a different account", + }, + { + name: "update a valid registration with a malformed account key", + newJwk: []byte("Eat at Joe's"), + expectedError: "parsing JWK", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + reg, err := sa.NewRegistration(ctx, &corepb.Registration{ + Key: newAcctKey(t), + }) + test.AssertNotError(t, err, "creating new registration") + + updatedReg, err := sa.UpdateRegistrationKey(ctx, &sapb.UpdateRegistrationKeyRequest{ + RegistrationID: reg.Id, + Jwk: tt.newJwk, + }) + if tt.expectedError != "" { + test.AssertError(t, err, "should have errored") + test.AssertContains(t, err.Error(), tt.expectedError) + } else { + test.AssertNotError(t, err, "unexpected error for UpdateRegistrationKey()") + test.AssertEquals(t, updatedReg.Id, reg.Id) + test.AssertDeepEquals(t, updatedReg.Key, tt.newJwk) + + refetchedReg, err := sa.GetRegistration(ctx, &sapb.RegistrationID{ + Id: reg.Id, + }) + test.AssertNotError(t, err, "retrieving registration") + test.AssertDeepEquals(t, refetchedReg.Key, tt.newJwk) + } + }) + } +} + +type mockRLOStream struct { + grpc.ServerStream + sent []*sapb.RateLimitOverride + ctx context.Context +} + +func newMockRLOStream() *mockRLOStream { + return &mockRLOStream{ctx: ctx} +} +func (m *mockRLOStream) Context() context.Context { return m.ctx } +func (m *mockRLOStream) RecvMsg(any) error { return io.EOF } +func (m *mockRLOStream) Send(ov *sapb.RateLimitOverride) error { + m.sent = append(m.sent, ov) + return nil +} + +func TestAddRateLimitOverrideInsertThenUpdate(t *testing.T) { + if os.Getenv("BOULDER_CONFIG_DIR") != "test/config-next" { + // TODO(#8147): Remove this skip. + t.Skip("skipping, this overrides table must exist for this test to run") + } + + sa, _, cleanup := initSA(t) + defer cleanup() + + expectBucketKey := core.RandomString(10) + ov := &sapb.RateLimitOverride{ + LimitEnum: 1, + BucketKey: expectBucketKey, + Comment: "insert", + Period: durationpb.New(time.Hour), + Count: 100, + Burst: 100, + } + + // Insert + resp, err := sa.AddRateLimitOverride(ctx, &sapb.AddRateLimitOverrideRequest{Override: ov}) + test.AssertNotError(t, err, "expected successful insert, got error") + test.Assert(t, resp.Inserted && resp.Enabled, fmt.Sprintf("expected (Inserted=true, Enabled=true) for initial insert, got (%v,%v)", resp.Inserted, resp.Enabled)) + + // Update (change comment) + ov.Comment = "updated" + resp, err = sa.AddRateLimitOverride(ctx, &sapb.AddRateLimitOverrideRequest{Override: ov}) + test.AssertNotError(t, err, "expected successful update, got error") + test.Assert(t, !resp.Inserted && resp.Enabled, fmt.Sprintf("expected (Inserted=false, Enabled=true) for update, got (%v, %v)", resp.Inserted, resp.Enabled)) + + got, err := sa.GetRateLimitOverride(ctx, &sapb.GetRateLimitOverrideRequest{LimitEnum: 1, BucketKey: expectBucketKey}) + test.AssertNotError(t, err, "expected GetRateLimitOverride to succeed, got error") + test.AssertEquals(t, got.Override.Comment, "updated") + + // Disable + _, err = sa.DisableRateLimitOverride(ctx, &sapb.DisableRateLimitOverrideRequest{LimitEnum: 1, BucketKey: expectBucketKey}) + test.AssertNotError(t, err, "expected DisableRateLimitOverride to succeed, got error") + + // Update and check that it's still disabled. + got, err = sa.GetRateLimitOverride(ctx, &sapb.GetRateLimitOverrideRequest{LimitEnum: 1, BucketKey: expectBucketKey}) + test.AssertNotError(t, err, "expected GetRateLimitOverride to succeed, got error") + test.Assert(t, !got.Enabled, fmt.Sprintf("expected Enabled=false after disable, got Enabled=%v", got.Enabled)) + + // Update (change period, count, and burst) + ov.Period = durationpb.New(2 * time.Hour) + ov.Count = 200 + ov.Burst = 200 + _, err = sa.AddRateLimitOverride(ctx, &sapb.AddRateLimitOverrideRequest{Override: ov}) + test.AssertNotError(t, err, "expected successful update, got error") + + got, err = sa.GetRateLimitOverride(ctx, &sapb.GetRateLimitOverrideRequest{LimitEnum: 1, BucketKey: expectBucketKey}) + test.AssertNotError(t, err, "expected GetRateLimitOverride to succeed, got error") + test.AssertEquals(t, got.Override.Period.AsDuration(), 2*time.Hour) + test.AssertEquals(t, got.Override.Count, int64(200)) + test.AssertEquals(t, got.Override.Burst, int64(200)) +} + +func TestDisableEnableRateLimitOverride(t *testing.T) { + if os.Getenv("BOULDER_CONFIG_DIR") != "test/config-next" { + // TODO(#8147): Remove this skip. + t.Skip("skipping, this overrides table must exist for this test to run") + } + + sa, _, cleanup := initSA(t) + defer cleanup() + + expectBucketKey := core.RandomString(10) + ov := &sapb.RateLimitOverride{ + LimitEnum: 2, + BucketKey: expectBucketKey, + Period: durationpb.New(time.Hour), + Count: 1, + Burst: 1, + Comment: "test", + } + _, _ = sa.AddRateLimitOverride(ctx, &sapb.AddRateLimitOverrideRequest{Override: ov}) + + // Disable + _, err := sa.DisableRateLimitOverride(ctx, + &sapb.DisableRateLimitOverrideRequest{LimitEnum: 2, BucketKey: expectBucketKey}) + test.AssertNotError(t, err, "expected DisableRateLimitOverride to succeed, got error") + + st, _ := sa.GetRateLimitOverride(ctx, + &sapb.GetRateLimitOverrideRequest{LimitEnum: 2, BucketKey: expectBucketKey}) + test.Assert(t, !st.Enabled, + fmt.Sprintf("expected Enabled=false after disable, got Enabled=%v", st.Enabled)) + + // Enable + _, err = sa.EnableRateLimitOverride(ctx, + &sapb.EnableRateLimitOverrideRequest{LimitEnum: 2, BucketKey: expectBucketKey}) + test.AssertNotError(t, err, "expected EnableRateLimitOverride to succeed, got error") + + st, _ = sa.GetRateLimitOverride(ctx, + &sapb.GetRateLimitOverrideRequest{LimitEnum: 2, BucketKey: expectBucketKey}) + test.Assert(t, st.Enabled, + fmt.Sprintf("expected Enabled=true after enable, got Enabled=%v", st.Enabled)) +} + +func TestGetEnabledRateLimitOverrides(t *testing.T) { + if os.Getenv("BOULDER_CONFIG_DIR") != "test/config-next" { + // TODO(#8147): Remove this skip. + t.Skip("skipping, this overrides table must exist for this test to run") + } + + sa, _, cleanup := initSA(t) + defer cleanup() + + // Enabled + ov1 := &sapb.RateLimitOverride{ + LimitEnum: 10, BucketKey: "on", Period: durationpb.New(time.Second), Count: 1, Burst: 1, Comment: "on", + } + // Disabled + ov2 := &sapb.RateLimitOverride{ + LimitEnum: 11, BucketKey: "off", Period: durationpb.New(time.Second), Count: 1, Burst: 1, Comment: "off", + } + + _, err := sa.AddRateLimitOverride(ctx, &sapb.AddRateLimitOverrideRequest{Override: ov1}) + test.AssertNotError(t, err, "expected successful insert of ov1, got error") + _, err = sa.AddRateLimitOverride(ctx, &sapb.AddRateLimitOverrideRequest{Override: ov2}) + test.AssertNotError(t, err, "expected successful insert of ov2, got error") + _, err = sa.DisableRateLimitOverride(ctx, &sapb.DisableRateLimitOverrideRequest{LimitEnum: 11, BucketKey: "off"}) + test.AssertNotError(t, err, "expected DisableRateLimitOverride of ov2 to succeed, got error") + _, err = sa.EnableRateLimitOverride(ctx, &sapb.EnableRateLimitOverrideRequest{LimitEnum: 10, BucketKey: "on"}) + test.AssertNotError(t, err, "expected EnableRateLimitOverride of ov1 to succeed, got error") + + stream := newMockRLOStream() + err = sa.GetEnabledRateLimitOverrides(&emptypb.Empty{}, stream) + test.AssertNotError(t, err, "expected streaming enabled overrides to succeed, got error") + test.AssertEquals(t, len(stream.sent), 1) + test.AssertEquals(t, stream.sent[0].BucketKey, "on") } diff --git a/third-party/github.com/letsencrypt/boulder/sa/saro.go b/third-party/github.com/letsencrypt/boulder/sa/saro.go index debc6b212..fe18d69e8 100644 --- a/third-party/github.com/letsencrypt/boulder/sa/saro.go +++ b/third-party/github.com/letsencrypt/boulder/sa/saro.go @@ -4,11 +4,9 @@ import ( "context" "errors" "fmt" - "math/big" - "net" + "math" "regexp" "strings" - "sync" "time" "github.com/go-jose/go-jose/v4" @@ -22,8 +20,6 @@ import ( corepb "github.com/letsencrypt/boulder/core/proto" "github.com/letsencrypt/boulder/db" berrors "github.com/letsencrypt/boulder/errors" - "github.com/letsencrypt/boulder/features" - bgrpc "github.com/letsencrypt/boulder/grpc" "github.com/letsencrypt/boulder/identifier" blog "github.com/letsencrypt/boulder/log" sapb "github.com/letsencrypt/boulder/sa/proto" @@ -33,8 +29,6 @@ var ( validIncidentTableRegexp = regexp.MustCompile(`^incident_[0-9a-zA-Z_]{1,100}$`) ) -type certCountFunc func(ctx context.Context, db db.Selector, domain string, timeRange *sapb.Range) (int64, time.Time, error) - // SQLStorageAuthorityRO defines a read-only subset of a Storage Authority type SQLStorageAuthorityRO struct { sapb.UnsafeStorageAuthorityReadOnlyServer @@ -56,10 +50,6 @@ type SQLStorageAuthorityRO struct { // as, the observed database replication lag. lagFactor time.Duration - // We use function types here so we can mock out this internal function in - // unittests. - countCertificatesByName certCountFunc - clk clock.Clock log blog.Logger @@ -100,8 +90,6 @@ func NewSQLStorageAuthorityRO( lagFactorCounter: lagFactorCounter, } - ssaro.countCertificatesByName = ssaro.countCertificates - return ssaro, nil } @@ -165,203 +153,6 @@ func (ssa *SQLStorageAuthorityRO) GetRegistrationByKey(ctx context.Context, req return registrationModelToPb(model) } -// incrementIP returns a copy of `ip` incremented at a bit index `index`, -// or in other words the first IP of the next highest subnet given a mask of -// length `index`. -// In order to easily account for overflow, we treat ip as a big.Int and add to -// it. If the increment overflows the max size of a net.IP, return the highest -// possible net.IP. -func incrementIP(ip net.IP, index int) net.IP { - bigInt := new(big.Int) - bigInt.SetBytes([]byte(ip)) - incr := new(big.Int).Lsh(big.NewInt(1), 128-uint(index)) - bigInt.Add(bigInt, incr) - // bigInt.Bytes can be shorter than 16 bytes, so stick it into a - // full-sized net.IP. - resultBytes := bigInt.Bytes() - if len(resultBytes) > 16 { - return net.ParseIP("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff") - } - result := make(net.IP, 16) - copy(result[16-len(resultBytes):], resultBytes) - return result -} - -// ipRange returns a range of IP addresses suitable for querying MySQL for the -// purpose of rate limiting using a range that is inclusive on the lower end and -// exclusive at the higher end. If ip is an IPv4 address, it returns that address, -// plus the one immediately higher than it. If ip is an IPv6 address, it applies -// a /48 mask to it and returns the lowest IP in the resulting network, and the -// first IP outside of the resulting network. -func ipRange(ip net.IP) (net.IP, net.IP) { - ip = ip.To16() - // For IPv6, match on a certain subnet range, since one person can commonly - // have an entire /48 to themselves. - maskLength := 48 - // For IPv4 addresses, do a match on exact address, so begin = ip and end = - // next higher IP. - if ip.To4() != nil { - maskLength = 128 - } - - mask := net.CIDRMask(maskLength, 128) - begin := ip.Mask(mask) - end := incrementIP(begin, maskLength) - - return begin, end -} - -// CountRegistrationsByIP returns the number of registrations created in the -// time range for a single IP address. -func (ssa *SQLStorageAuthorityRO) CountRegistrationsByIP(ctx context.Context, req *sapb.CountRegistrationsByIPRequest) (*sapb.Count, error) { - // TODO(#7153): Check each value via core.IsAnyNilOrZero - if len(req.Ip) == 0 || core.IsAnyNilOrZero(req.Range.Earliest, req.Range.Latest) { - return nil, errIncompleteRequest - } - - var count int64 - err := ssa.dbReadOnlyMap.SelectOne( - ctx, - &count, - `SELECT COUNT(*) FROM registrations - WHERE - initialIP = :ip AND - :earliest < createdAt AND - createdAt <= :latest`, - map[string]interface{}{ - "ip": req.Ip, - "earliest": req.Range.Earliest.AsTime().Truncate(time.Second), - "latest": req.Range.Latest.AsTime().Truncate(time.Second), - }) - if err != nil { - return nil, err - } - return &sapb.Count{Count: count}, nil -} - -// CountRegistrationsByIPRange returns the number of registrations created in -// the time range in an IP range. For IPv4 addresses, that range is limited to -// the single IP. For IPv6 addresses, that range is a /48, since it's not -// uncommon for one person to have a /48 to themselves. -func (ssa *SQLStorageAuthorityRO) CountRegistrationsByIPRange(ctx context.Context, req *sapb.CountRegistrationsByIPRequest) (*sapb.Count, error) { - // TODO(#7153): Check each value via core.IsAnyNilOrZero - if len(req.Ip) == 0 || core.IsAnyNilOrZero(req.Range.Earliest, req.Range.Latest) { - return nil, errIncompleteRequest - } - - var count int64 - beginIP, endIP := ipRange(req.Ip) - err := ssa.dbReadOnlyMap.SelectOne( - ctx, - &count, - `SELECT COUNT(*) FROM registrations - WHERE - :beginIP <= initialIP AND - initialIP < :endIP AND - :earliest < createdAt AND - createdAt <= :latest`, - map[string]interface{}{ - "earliest": req.Range.Earliest.AsTime().Truncate(time.Second), - "latest": req.Range.Latest.AsTime().Truncate(time.Second), - "beginIP": beginIP, - "endIP": endIP, - }) - if err != nil { - return nil, err - } - return &sapb.Count{Count: count}, nil -} - -// CountCertificatesByNames counts, for each input domain, the number of -// certificates issued in the given time range for that domain and its -// subdomains. It returns a map from domains to counts and a timestamp. The map -// of domains to counts is guaranteed to contain an entry for each input domain, -// so long as err is nil. The timestamp is the earliest time a certificate was -// issued for any of the domains during the provided range of time. Queries will -// be run in parallel. If any of them error, only one error will be returned. -func (ssa *SQLStorageAuthorityRO) CountCertificatesByNames(ctx context.Context, req *sapb.CountCertificatesByNamesRequest) (*sapb.CountByNames, error) { - // TODO(#7153): Check each value via core.IsAnyNilOrZero - if len(req.Names) == 0 || core.IsAnyNilOrZero(req.Range.Earliest, req.Range.Latest) { - return nil, errIncompleteRequest - } - - work := make(chan string, len(req.Names)) - type result struct { - err error - count int64 - earliest time.Time - domain string - } - results := make(chan result, len(req.Names)) - for _, domain := range req.Names { - work <- domain - } - close(work) - var wg sync.WaitGroup - ctx, cancel := context.WithCancel(ctx) - defer cancel() - // We may perform up to 100 queries, depending on what's in the certificate - // request. Parallelize them so we don't hit our timeout, but limit the - // parallelism so we don't consume too many threads on the database. - for range ssa.parallelismPerRPC { - wg.Add(1) - go func() { - defer wg.Done() - for domain := range work { - select { - case <-ctx.Done(): - results <- result{err: ctx.Err()} - return - default: - } - count, earliest, err := ssa.countCertificatesByName(ctx, ssa.dbReadOnlyMap, domain, req.Range) - if err != nil { - results <- result{err: err} - // Skip any further work - cancel() - return - } - results <- result{ - count: count, - earliest: earliest, - domain: domain, - } - } - }() - } - wg.Wait() - close(results) - - // Set earliest to the latest possible time, so that we can find the - // earliest certificate in the results. - earliest := req.Range.Latest - counts := make(map[string]int64) - for r := range results { - if r.err != nil { - return nil, r.err - } - counts[r.domain] = r.count - if !r.earliest.IsZero() && r.earliest.Before(earliest.AsTime()) { - earliest = timestamppb.New(r.earliest) - } - } - - // If we didn't find any certificates in the range, earliest should be set - // to a zero value. - if len(counts) == 0 { - earliest = ×tamppb.Timestamp{} - } - return &sapb.CountByNames{Counts: counts, Earliest: earliest}, nil -} - -func ReverseName(domain string) string { - labels := strings.Split(domain, ".") - for i, j := 0, len(labels)-1; i < j; i, j = i+1, j-1 { - labels[i], labels[j] = labels[j], labels[i] - } - return strings.Join(labels, ".") -} - // GetSerialMetadata returns metadata stored alongside the serial number, // such as the RegID whose certificate request created that serial, and when // the certificate with that serial will expire. @@ -413,7 +204,7 @@ func (ssa *SQLStorageAuthorityRO) GetCertificate(ctx context.Context, req *sapb. if err != nil { return nil, err } - return bgrpc.CertToPB(cert), nil + return cert, nil } // GetLintPrecertificate takes a serial number and returns the corresponding @@ -435,7 +226,7 @@ func (ssa *SQLStorageAuthorityRO) GetLintPrecertificate(ctx context.Context, req if err != nil { return nil, err } - return bgrpc.CertToPB(cert), nil + return cert, nil } // GetCertificateStatus takes a hexadecimal string representing the full 128-bit serial @@ -458,7 +249,7 @@ func (ssa *SQLStorageAuthorityRO) GetCertificateStatus(ctx context.Context, req return nil, err } - return bgrpc.CertStatusToPB(certStatus), nil + return certStatus, nil } // GetRevocationStatus takes a hexadecimal string representing the full serial @@ -483,42 +274,21 @@ func (ssa *SQLStorageAuthorityRO) GetRevocationStatus(ctx context.Context, req * return status, nil } -func (ssa *SQLStorageAuthorityRO) CountOrders(ctx context.Context, req *sapb.CountOrdersRequest) (*sapb.Count, error) { - // TODO(#7153): Check each value via core.IsAnyNilOrZero - if req.AccountID == 0 || core.IsAnyNilOrZero(req.Range.Earliest, req.Range.Latest) { - return nil, errIncompleteRequest - } - - return countNewOrders(ctx, ssa.dbReadOnlyMap, req) -} - -// CountFQDNSets counts the total number of issuances, for a set of domains, -// that occurred during a given window of time. -func (ssa *SQLStorageAuthorityRO) CountFQDNSets(ctx context.Context, req *sapb.CountFQDNSetsRequest) (*sapb.Count, error) { - if core.IsAnyNilOrZero(req.Window) || len(req.Domains) == 0 { - return nil, errIncompleteRequest - } - - var count int64 - err := ssa.dbReadOnlyMap.SelectOne( - ctx, - &count, - `SELECT COUNT(*) FROM fqdnSets - WHERE setHash = ? - AND issued > ?`, - core.HashNames(req.Domains), - ssa.clk.Now().Add(-req.Window.AsDuration()).Truncate(time.Second), - ) - return &sapb.Count{Count: count}, err -} - // FQDNSetTimestampsForWindow returns the issuance timestamps for each -// certificate, issued for a set of domains, during a given window of time, +// certificate, issued for a set of identifiers, during a given window of time, // starting from the most recent issuance. +// +// If req.Limit is nonzero, it returns only the most recent `Limit` results func (ssa *SQLStorageAuthorityRO) FQDNSetTimestampsForWindow(ctx context.Context, req *sapb.CountFQDNSetsRequest) (*sapb.Timestamps, error) { - if core.IsAnyNilOrZero(req.Window) || len(req.Domains) == 0 { + idents := identifier.FromProtoSlice(req.Identifiers) + + if core.IsAnyNilOrZero(req.Window) || len(idents) == 0 { return nil, errIncompleteRequest } + limit := req.Limit + if limit == 0 { + limit = math.MaxInt64 + } type row struct { Issued time.Time } @@ -526,12 +296,14 @@ func (ssa *SQLStorageAuthorityRO) FQDNSetTimestampsForWindow(ctx context.Context _, err := ssa.dbReadOnlyMap.Select( ctx, &rows, - `SELECT issued FROM fqdnSets + `SELECT issued FROM fqdnSets WHERE setHash = ? AND issued > ? - ORDER BY issued DESC`, - core.HashNames(req.Domains), - ssa.clk.Now().Add(-req.Window.AsDuration()).Truncate(time.Second), + ORDER BY issued DESC + LIMIT ?`, + core.HashIdentifiers(idents), + ssa.clk.Now().Add(-req.Window.AsDuration()), + limit, ) if err != nil { return nil, err @@ -547,10 +319,11 @@ func (ssa *SQLStorageAuthorityRO) FQDNSetTimestampsForWindow(ctx context.Context // FQDNSetExists returns a bool indicating if one or more FQDN sets |names| // exists in the database func (ssa *SQLStorageAuthorityRO) FQDNSetExists(ctx context.Context, req *sapb.FQDNSetExistsRequest) (*sapb.Exists, error) { - if len(req.Domains) == 0 { + idents := identifier.FromProtoSlice(req.Identifiers) + if len(idents) == 0 { return nil, errIncompleteRequest } - exists, err := ssa.checkFQDNSetExists(ctx, ssa.dbReadOnlyMap.SelectOne, req.Domains) + exists, err := ssa.checkFQDNSetExists(ctx, ssa.dbReadOnlyMap.SelectOne, idents) if err != nil { return nil, err } @@ -563,8 +336,8 @@ type oneSelectorFunc func(ctx context.Context, holder interface{}, query string, // checkFQDNSetExists uses the given oneSelectorFunc to check whether an fqdnSet // for the given names exists. -func (ssa *SQLStorageAuthorityRO) checkFQDNSetExists(ctx context.Context, selector oneSelectorFunc, names []string) (bool, error) { - namehash := core.HashNames(names) +func (ssa *SQLStorageAuthorityRO) checkFQDNSetExists(ctx context.Context, selector oneSelectorFunc, idents identifier.ACMEIdentifiers) (bool, error) { + namehash := core.HashIdentifiers(idents) var exists bool err := selector( ctx, @@ -582,13 +355,7 @@ func (ssa *SQLStorageAuthorityRO) GetOrder(ctx context.Context, req *sapb.OrderR } txn := func(tx db.Executor) (interface{}, error) { - var omObj interface{} - var err error - if features.Get().MultipleCertificateProfiles { - omObj, err = tx.Get(ctx, orderModelv2{}, req.Id) - } else { - omObj, err = tx.Get(ctx, orderModelv1{}, req.Id) - } + omObj, err := tx.Get(ctx, orderModel{}, req.Id) if err != nil { if db.IsNoRows(err) { return nil, berrors.NotFoundError("no order found for ID %d", req.Id) @@ -599,12 +366,7 @@ func (ssa *SQLStorageAuthorityRO) GetOrder(ctx context.Context, req *sapb.OrderR return nil, berrors.NotFoundError("no order found for ID %d", req.Id) } - var order *corepb.Order - if features.Get().MultipleCertificateProfiles { - order, err = modelToOrderv2(omObj.(*orderModelv2)) - } else { - order, err = modelToOrderv1(omObj.(*orderModelv1)) - } + order, err := modelToOrder(omObj.(*orderModel)) if err != nil { return nil, err } @@ -627,11 +389,11 @@ func (ssa *SQLStorageAuthorityRO) GetOrder(ctx context.Context, req *sapb.OrderR return nil, err } - names := make([]string, 0, len(authzValidityInfo)) + var idents identifier.ACMEIdentifiers for _, a := range authzValidityInfo { - names = append(names, a.IdentifierValue) + idents = append(idents, identifier.ACMEIdentifier{Type: uintToIdentifierType[a.IdentifierType], Value: a.IdentifierValue}) } - order.Names = names + order.Identifiers = idents.ToProtoSlice() // Calculate the status for the order status, err := statusForOrder(order, authzValidityInfo, ssa.clk.Now()) @@ -677,12 +439,14 @@ func (ssa *SQLStorageAuthorityRO) GetOrder(ctx context.Context, req *sapb.OrderR // unexpired orders are considered. If no order meeting these requirements is // found a nil corepb.Order pointer is returned. func (ssa *SQLStorageAuthorityRO) GetOrderForNames(ctx context.Context, req *sapb.GetOrderForNamesRequest) (*corepb.Order, error) { - if req.AcctID == 0 || len(req.Names) == 0 { + idents := identifier.FromProtoSlice(req.Identifiers) + + if req.AcctID == 0 || len(idents) == 0 { return nil, errIncompleteRequest } // Hash the names requested for lookup in the orderFqdnSets table - fqdnHash := core.HashNames(req.Names) + fqdnHash := core.HashIdentifiers(idents) // Find a possibly-suitable order. We don't include the account ID or order // status in this query because there's no index that includes those, so @@ -708,8 +472,7 @@ func (ssa *SQLStorageAuthorityRO) GetOrderForNames(ctx context.Context, req *sap AND expires > ? ORDER BY expires ASC LIMIT 1`, - fqdnHash, - ssa.clk.Now().Truncate(time.Second)) + fqdnHash, ssa.clk.Now()) if db.IsNoRows(err) { return nil, berrors.NotFoundError("no order matching request found") @@ -766,52 +529,55 @@ func (ssa *SQLStorageAuthorityRO) GetAuthorization2(ctx context.Context, req *sa return modelToAuthzPB(*(obj.(*authzModel))) } -// authzModelMapToPB converts a mapping of domain name to authzModels into a +// authzModelMapToPB converts a mapping of identifiers to authzModels into a // protobuf authorizations map -func authzModelMapToPB(m map[string]authzModel) (*sapb.Authorizations, error) { +func authzModelMapToPB(m map[identifier.ACMEIdentifier]authzModel) (*sapb.Authorizations, error) { resp := &sapb.Authorizations{} - for k, v := range m { + for _, v := range m { authzPB, err := modelToAuthzPB(v) if err != nil { return nil, err } - resp.Authz = append(resp.Authz, &sapb.Authorizations_MapElement{Domain: k, Authz: authzPB}) + resp.Authzs = append(resp.Authzs, authzPB) } return resp, nil } -// GetAuthorizations2 returns any valid or pending authorizations that exist for the list of domains -// provided. If both a valid and pending authorization exist only the valid one will be returned. +// GetAuthorizations2 returns a single pending or valid authorization owned by +// the given account for all given identifiers. If both a valid and pending +// authorization exist only the valid one will be returned. +// +// Deprecated: Use GetValidAuthorizations2, as we stop pending authz reuse. func (ssa *SQLStorageAuthorityRO) GetAuthorizations2(ctx context.Context, req *sapb.GetAuthorizationsRequest) (*sapb.Authorizations, error) { - // TODO(#7153): Check each value via core.IsAnyNilOrZero - if len(req.Domains) == 0 || req.RegistrationID == 0 || core.IsAnyNilOrZero(req.Now) { + idents := identifier.FromProtoSlice(req.Identifiers) + + if core.IsAnyNilOrZero(req, req.RegistrationID, idents, req.ValidUntil) { return nil, errIncompleteRequest } - var authzModels []authzModel - params := []interface{}{ - req.RegistrationID, - statusUint(core.StatusValid), - statusUint(core.StatusPending), - req.Now.AsTime().Truncate(time.Second), - identifierTypeToUint[string(identifier.DNS)], - } - - for _, name := range req.Domains { - params = append(params, name) - } + // The WHERE clause returned by this function does not contain any + // user-controlled strings; all user-controlled input ends up in the + // returned placeholder args. + identConditions, identArgs := buildIdentifierQueryConditions(idents) query := fmt.Sprintf( `SELECT %s FROM authz2 USE INDEX (regID_identifier_status_expires_idx) WHERE registrationID = ? AND status IN (?,?) AND expires > ? AND - identifierType = ? AND - identifierValue IN (%s)`, + (%s)`, authzFields, - db.QuestionMarks(len(req.Domains)), + identConditions, ) + params := []interface{}{ + req.RegistrationID, + statusUint(core.StatusValid), statusUint(core.StatusPending), + req.ValidUntil.AsTime(), + } + params = append(params, identArgs...) + + var authzModels []authzModel _, err := ssa.dbReadOnlyMap.Select( ctx, &authzModels, @@ -826,54 +592,31 @@ func (ssa *SQLStorageAuthorityRO) GetAuthorizations2(ctx context.Context, req *s return &sapb.Authorizations{}, nil } - authzModelMap := make(map[string]authzModel) + // TODO(#8111): Consider reducing the volume of data in this map. + authzModelMap := make(map[identifier.ACMEIdentifier]authzModel, len(authzModels)) for _, am := range authzModels { - existing, present := authzModelMap[am.IdentifierValue] + if req.Profile != "" { + // Don't return authzs whose profile doesn't match that requested. + if am.CertificateProfileName == nil || *am.CertificateProfileName != req.Profile { + continue + } + } + // If there is an existing authorization in the map, only replace it with + // one which has a "better" validation state (valid instead of pending). + identType, ok := uintToIdentifierType[am.IdentifierType] + if !ok { + return nil, fmt.Errorf("unrecognized identifier type encoding %d on authz id %d", am.IdentifierType, am.ID) + } + ident := identifier.ACMEIdentifier{Type: identType, Value: am.IdentifierValue} + existing, present := authzModelMap[ident] if !present || uintToStatus[existing.Status] == core.StatusPending && uintToStatus[am.Status] == core.StatusValid { - authzModelMap[am.IdentifierValue] = am + authzModelMap[ident] = am } } return authzModelMapToPB(authzModelMap) } -// GetPendingAuthorization2 returns the most recent Pending authorization with -// the given identifier, if available. This method only supports DNS identifier types. -// TODO(#5816): Consider removing this method, as it has no callers. -func (ssa *SQLStorageAuthorityRO) GetPendingAuthorization2(ctx context.Context, req *sapb.GetPendingAuthorizationRequest) (*corepb.Authorization, error) { - // TODO(#7153): Check each value via core.IsAnyNilOrZero - if req.RegistrationID == 0 || req.IdentifierValue == "" || core.IsAnyNilOrZero(req.ValidUntil) { - return nil, errIncompleteRequest - } - var am authzModel - err := ssa.dbReadOnlyMap.SelectOne( - ctx, - &am, - fmt.Sprintf(`SELECT %s FROM authz2 WHERE - registrationID = :regID AND - status = :status AND - expires > :validUntil AND - identifierType = :dnsType AND - identifierValue = :ident - ORDER BY expires ASC - LIMIT 1 `, authzFields), - map[string]interface{}{ - "regID": req.RegistrationID, - "status": statusUint(core.StatusPending), - "validUntil": req.ValidUntil.AsTime().Truncate(time.Second), - "dnsType": identifierTypeToUint[string(identifier.DNS)], - "ident": req.IdentifierValue, - }, - ) - if err != nil { - if db.IsNoRows(err) { - return nil, berrors.NotFoundError("pending authz not found") - } - return nil, err - } - return modelToAuthzPB(am) -} - // CountPendingAuthorizations2 returns the number of pending, unexpired authorizations // for the given registration. func (ssa *SQLStorageAuthorityRO) CountPendingAuthorizations2(ctx context.Context, req *sapb.RegistrationID) (*sapb.Count, error) { @@ -889,7 +632,7 @@ func (ssa *SQLStorageAuthorityRO) CountPendingAuthorizations2(ctx context.Contex status = :status`, map[string]interface{}{ "regID": req.Id, - "expires": ssa.clk.Now().Truncate(time.Second), + "expires": ssa.clk.Now(), "status": statusUint(core.StatusPending), }, ) @@ -899,10 +642,17 @@ func (ssa *SQLStorageAuthorityRO) CountPendingAuthorizations2(ctx context.Contex return &sapb.Count{Count: count}, nil } -// GetValidOrderAuthorizations2 is used to find the valid, unexpired authorizations -// associated with a specific order and account ID. +// GetValidOrderAuthorizations2 is used to get all authorizations +// associated with the given Order ID. +// NOTE: The name is outdated. It does *not* filter out invalid or expired +// authorizations; that it left to the caller. It also ignores the RegID field +// of the input: ensuring that the returned authorizations match the same RegID +// as the Order is also left to the caller. This is because the caller is +// generally in a better position to provide insightful error messages, whereas +// simply omitting an authz from this method's response would leave the caller +// wondering why that authz was omitted. func (ssa *SQLStorageAuthorityRO) GetValidOrderAuthorizations2(ctx context.Context, req *sapb.GetValidOrderAuthorizationsRequest) (*sapb.Authorizations, error) { - if req.AcctID == 0 || req.Id == 0 { + if core.IsAnyNilOrZero(req.Id) { return nil, errIncompleteRequest } @@ -922,16 +672,10 @@ func (ssa *SQLStorageAuthorityRO) GetValidOrderAuthorizations2(ctx context.Conte &ams, fmt.Sprintf(`SELECT %s FROM authz2 LEFT JOIN orderToAuthz2 ON authz2.ID = orderToAuthz2.authzID - WHERE authz2.registrationID = :regID AND - authz2.expires > :expires AND - authz2.status = :status AND - orderToAuthz2.orderID = :orderID`, + WHERE orderToAuthz2.orderID = :orderID`, strings.Join(qualifiedAuthzFields, " "), ), map[string]interface{}{ - "regID": req.AcctID, - "expires": ssa.clk.Now().Truncate(time.Second), - "status": statusUint(core.StatusValid), "orderID": req.Id, }, ) @@ -939,28 +683,38 @@ func (ssa *SQLStorageAuthorityRO) GetValidOrderAuthorizations2(ctx context.Conte return nil, err } - byName := make(map[string]authzModel) + // TODO(#8111): Consider reducing the volume of data in this map. + byIdent := make(map[identifier.ACMEIdentifier]authzModel) for _, am := range ams { - if uintToIdentifierType[am.IdentifierType] != string(identifier.DNS) { - return nil, fmt.Errorf("unknown identifier type: %q on authz id %d", am.IdentifierType, am.ID) + identType, ok := uintToIdentifierType[am.IdentifierType] + if !ok { + return nil, fmt.Errorf("unrecognized identifier type encoding %d on authz id %d", am.IdentifierType, am.ID) } - existing, present := byName[am.IdentifierValue] - if !present || am.Expires.After(existing.Expires) { - byName[am.IdentifierValue] = am + ident := identifier.ACMEIdentifier{Type: identType, Value: am.IdentifierValue} + _, present := byIdent[ident] + if present { + return nil, fmt.Errorf("identifier %q appears twice in authzs for order %d", am.IdentifierValue, req.Id) } + byIdent[ident] = am } - return authzModelMapToPB(byName) + return authzModelMapToPB(byIdent) } // CountInvalidAuthorizations2 counts invalid authorizations for a user expiring -// in a given time range. This method only supports DNS identifier types. +// in a given time range. func (ssa *SQLStorageAuthorityRO) CountInvalidAuthorizations2(ctx context.Context, req *sapb.CountInvalidAuthorizationsRequest) (*sapb.Count, error) { - // TODO(#7153): Check each value via core.IsAnyNilOrZero - if req.RegistrationID == 0 || req.Hostname == "" || core.IsAnyNilOrZero(req.Range.Earliest, req.Range.Latest) { + ident := identifier.FromProto(req.Identifier) + + if core.IsAnyNilOrZero(req.RegistrationID, ident, req.Range.Earliest, req.Range.Latest) { return nil, errIncompleteRequest } + idType, ok := identifierTypeToUint[ident.ToProto().Type] + if !ok { + return nil, fmt.Errorf("unsupported identifier type %q", ident.ToProto().Type) + } + var count int64 err := ssa.dbReadOnlyMap.SelectOne( ctx, @@ -970,14 +724,14 @@ func (ssa *SQLStorageAuthorityRO) CountInvalidAuthorizations2(ctx context.Contex status = :status AND expires > :expiresEarliest AND expires <= :expiresLatest AND - identifierType = :dnsType AND - identifierValue = :ident`, + identifierType = :identType AND + identifierValue = :identValue`, map[string]interface{}{ "regID": req.RegistrationID, - "dnsType": identifierTypeToUint[string(identifier.DNS)], - "ident": req.Hostname, - "expiresEarliest": req.Range.Earliest.AsTime().Truncate(time.Second), - "expiresLatest": req.Range.Latest.AsTime().Truncate(time.Second), + "identType": idType, + "identValue": ident.Value, + "expiresEarliest": req.Range.Earliest.AsTime(), + "expiresLatest": req.Range.Latest.AsTime(), "status": statusUint(core.StatusInvalid), }, ) @@ -987,35 +741,37 @@ func (ssa *SQLStorageAuthorityRO) CountInvalidAuthorizations2(ctx context.Contex return &sapb.Count{Count: count}, nil } -// GetValidAuthorizations2 returns the latest authorization for all -// domain names that the account has authorizations for. This method -// only supports DNS identifier types. +// GetValidAuthorizations2 returns a single valid authorization owned by the +// given account for all given identifiers. If more than one valid authorization +// exists, only the one with the latest expiry will be returned. func (ssa *SQLStorageAuthorityRO) GetValidAuthorizations2(ctx context.Context, req *sapb.GetValidAuthorizationsRequest) (*sapb.Authorizations, error) { - // TODO(#7153): Check each value via core.IsAnyNilOrZero - if len(req.Domains) == 0 || req.RegistrationID == 0 || core.IsAnyNilOrZero(req.Now) { + idents := identifier.FromProtoSlice(req.Identifiers) + + if core.IsAnyNilOrZero(req, req.RegistrationID, idents, req.ValidUntil) { return nil, errIncompleteRequest } + // The WHERE clause returned by this function does not contain any + // user-controlled strings; all user-controlled input ends up in the + // returned placeholder args. + identConditions, identArgs := buildIdentifierQueryConditions(idents) query := fmt.Sprintf( - `SELECT %s FROM authz2 WHERE - registrationID = ? AND + `SELECT %s FROM authz2 + USE INDEX (regID_identifier_status_expires_idx) + WHERE registrationID = ? AND status = ? AND expires > ? AND - identifierType = ? AND - identifierValue IN (%s)`, + (%s)`, authzFields, - db.QuestionMarks(len(req.Domains)), + identConditions, ) params := []interface{}{ req.RegistrationID, statusUint(core.StatusValid), - req.Now.AsTime().Truncate(time.Second), - identifierTypeToUint[string(identifier.DNS)], - } - for _, domain := range req.Domains { - params = append(params, domain) + req.ValidUntil.AsTime(), } + params = append(params, identArgs...) var authzModels []authzModel _, err := ssa.dbReadOnlyMap.Select( @@ -1028,19 +784,33 @@ func (ssa *SQLStorageAuthorityRO) GetValidAuthorizations2(ctx context.Context, r return nil, err } - authzMap := make(map[string]authzModel, len(authzModels)) + if len(authzModels) == 0 { + return &sapb.Authorizations{}, nil + } + + // TODO(#8111): Consider reducing the volume of data in this map. + authzMap := make(map[identifier.ACMEIdentifier]authzModel, len(authzModels)) for _, am := range authzModels { - // Only allow DNS identifiers - if uintToIdentifierType[am.IdentifierType] != string(identifier.DNS) { - continue + if req.Profile != "" { + // Don't return authzs whose profile doesn't match that requested. + if am.CertificateProfileName == nil || *am.CertificateProfileName != req.Profile { + continue + } } // If there is an existing authorization in the map only replace it with one // which has a later expiry. - if existing, present := authzMap[am.IdentifierValue]; present && am.Expires.Before(existing.Expires) { + identType, ok := uintToIdentifierType[am.IdentifierType] + if !ok { + return nil, fmt.Errorf("unrecognized identifier type encoding %d on authz id %d", am.IdentifierType, am.ID) + } + ident := identifier.ACMEIdentifier{Type: identType, Value: am.IdentifierValue} + existing, present := authzMap[ident] + if present && am.Expires.Before(existing.Expires) { continue } - authzMap[am.IdentifierValue] = am + authzMap[ident] = am } + return authzModelMapToPB(authzMap) } @@ -1152,26 +922,13 @@ func (ssa *SQLStorageAuthorityRO) SerialsForIncident(req *sapb.SerialsForInciden }) } -// GetRevokedCerts gets a request specifying an issuer and a period of time, -// and writes to the output stream the set of all certificates issued by that -// issuer which expire during that period of time and which have been revoked. -// The starting timestamp is treated as inclusive (certs with exactly that -// notAfter date are included), but the ending timestamp is exclusive (certs -// with exactly that notAfter date are *not* included). -func (ssa *SQLStorageAuthorityRO) GetRevokedCerts(req *sapb.GetRevokedCertsRequest, stream grpc.ServerStreamingServer[corepb.CRLEntry]) error { - if req.ShardIdx != 0 { - return ssa.getRevokedCertsFromRevokedCertificatesTable(req, stream) - } else { - return ssa.getRevokedCertsFromCertificateStatusTable(req, stream) - } -} - -// getRevokedCertsFromRevokedCertificatesTable uses the new revokedCertificates -// table to implement GetRevokedCerts. It must only be called when the request -// contains a non-zero ShardIdx. -func (ssa *SQLStorageAuthorityRO) getRevokedCertsFromRevokedCertificatesTable(req *sapb.GetRevokedCertsRequest, stream grpc.ServerStreamingServer[corepb.CRLEntry]) error { - if req.ShardIdx == 0 { - return errors.New("can't select shard 0 from revokedCertificates table") +// GetRevokedCertsByShard returns revoked certificates by explicit sharding. +// +// It returns all unexpired certificates from the revokedCertificates table with the given +// shardIdx. It limits the results those revoked before req.RevokedBefore. +func (ssa *SQLStorageAuthorityRO) GetRevokedCertsByShard(req *sapb.GetRevokedCertsByShardRequest, stream grpc.ServerStreamingServer[corepb.CRLEntry]) error { + if core.IsAnyNilOrZero(req.ShardIdx, req.IssuerNameID, req.RevokedBefore, req.ExpiresAfter) { + return errIncompleteRequest } atTime := req.RevokedBefore.AsTime() @@ -1209,15 +966,24 @@ func (ssa *SQLStorageAuthorityRO) getRevokedCertsFromRevokedCertificatesTable(re return stream.Send(&corepb.CRLEntry{ Serial: row.Serial, - Reason: int32(row.RevokedReason), + Reason: int32(row.RevokedReason), //nolint: gosec // Revocation reasons are guaranteed to be small, no risk of overflow. RevokedAt: timestamppb.New(row.RevokedDate), }) }) } -// getRevokedCertsFromCertificateStatusTable uses the old certificateStatus -// table to implement GetRevokedCerts. -func (ssa *SQLStorageAuthorityRO) getRevokedCertsFromCertificateStatusTable(req *sapb.GetRevokedCertsRequest, stream grpc.ServerStreamingServer[corepb.CRLEntry]) error { +// GetRevokedCerts returns revoked certificates based on temporal sharding. +// +// Based on a request specifying an issuer and a period of time, +// it writes to the output stream the set of all certificates issued by that +// issuer which expire during that period of time and which have been revoked. +// The starting timestamp is treated as inclusive (certs with exactly that +// notAfter date are included), but the ending timestamp is exclusive (certs +// with exactly that notAfter date are *not* included). +func (ssa *SQLStorageAuthorityRO) GetRevokedCerts(req *sapb.GetRevokedCertsRequest, stream grpc.ServerStreamingServer[corepb.CRLEntry]) error { + if core.IsAnyNilOrZero(req.IssuerNameID, req.RevokedBefore, req.ExpiresAfter, req.ExpiresBefore) { + return errIncompleteRequest + } atTime := req.RevokedBefore.AsTime() clauses := ` @@ -1226,8 +992,8 @@ func (ssa *SQLStorageAuthorityRO) getRevokedCertsFromCertificateStatusTable(req AND issuerID = ? AND status = ?` params := []interface{}{ - req.ExpiresAfter.AsTime().Truncate(time.Second), - req.ExpiresBefore.AsTime().Truncate(time.Second), + req.ExpiresAfter.AsTime(), + req.ExpiresBefore.AsTime(), req.IssuerNameID, core.OCSPStatusRevoked, } @@ -1253,7 +1019,7 @@ func (ssa *SQLStorageAuthorityRO) getRevokedCertsFromCertificateStatusTable(req return stream.Send(&corepb.CRLEntry{ Serial: row.Serial, - Reason: int32(row.RevokedReason), + Reason: int32(row.RevokedReason), //nolint: gosec // Revocation reasons are guaranteed to be small, no risk of overflow. RevokedAt: timestamppb.New(row.RevokedDate), }) }) @@ -1358,7 +1124,7 @@ func (ssa *SQLStorageAuthorityRO) GetSerialsByKey(req *sapb.SPKIHash, stream grp AND certNotAfter > ?` params := []interface{}{ req.KeyHash, - ssa.clk.Now().Truncate(time.Second), + ssa.clk.Now(), } selector, err := db.NewMappedSelector[keyHashModel](ssa.dbReadOnlyMap) @@ -1385,7 +1151,7 @@ func (ssa *SQLStorageAuthorityRO) GetSerialsByAccount(req *sapb.RegistrationID, AND expires > ?` params := []interface{}{ req.Id, - ssa.clk.Now().Truncate(time.Second), + ssa.clk.Now(), } selector, err := db.NewMappedSelector[recordedSerialModel](ssa.dbReadOnlyMap) @@ -1411,19 +1177,19 @@ func (ssa *SQLStorageAuthorityRO) CheckIdentifiersPaused(ctx context.Context, re return nil, errIncompleteRequest } - identifiers, err := newIdentifierModelsFromPB(req.Identifiers) + idents, err := newIdentifierModelsFromPB(req.Identifiers) if err != nil { return nil, err } - if len(identifiers) == 0 { + if len(idents) == 0 { // No identifier values to check. return nil, nil } - identifiersByType := map[uint8][]string{} - for _, id := range identifiers { - identifiersByType[id.Type] = append(identifiersByType[id.Type], id.Value) + identsByType := map[uint8][]string{} + for _, id := range idents { + identsByType[id.Type] = append(identsByType[id.Type], id.Value) } // Build a query to retrieve up to 15 paused identifiers using OR clauses @@ -1443,7 +1209,7 @@ func (ssa *SQLStorageAuthorityRO) CheckIdentifiersPaused(ctx context.Context, re var conditions []string args := []interface{}{req.RegistrationID} - for idType, values := range identifiersByType { + for idType, values := range identsByType { conditions = append(conditions, fmt.Sprintf("identifierType = ? AND identifierValue IN (%s)", db.QuestionMarks(len(values)), @@ -1483,7 +1249,7 @@ func (ssa *SQLStorageAuthorityRO) GetPausedIdentifiers(ctx context.Context, req _, err := ssa.dbReadOnlyMap.Select(ctx, &matches, ` SELECT identifierType, identifierValue FROM paused - WHERE + WHERE registrationID = ? AND unpausedAt IS NULL LIMIT 15`, @@ -1495,3 +1261,49 @@ func (ssa *SQLStorageAuthorityRO) GetPausedIdentifiers(ctx context.Context, req return newPBFromIdentifierModels(matches) } + +// GetRateLimitOverride retrieves a rate limit override for the given bucket key +// and limit. If no override is found, a NotFound error is returned. +func (ssa *SQLStorageAuthorityRO) GetRateLimitOverride(ctx context.Context, req *sapb.GetRateLimitOverrideRequest) (*sapb.RateLimitOverrideResponse, error) { + if core.IsAnyNilOrZero(req, req.LimitEnum, req.BucketKey) { + return nil, errIncompleteRequest + } + + obj, err := ssa.dbReadOnlyMap.Get(ctx, overrideModel{}, req.LimitEnum, req.BucketKey) + if db.IsNoRows(err) { + return nil, berrors.NotFoundError( + "no rate limit override found for limit %d and bucket key %s", + req.LimitEnum, + req.BucketKey, + ) + } + if err != nil { + return nil, err + } + row := obj.(*overrideModel) + + return &sapb.RateLimitOverrideResponse{ + Override: newPBFromOverrideModel(row), + Enabled: row.Enabled, + UpdatedAt: timestamppb.New(row.UpdatedAt), + }, nil +} + +// GetEnabledRateLimitOverrides retrieves all enabled rate limit overrides from +// the database. The results are returned as a stream. If no enabled overrides +// are found, an empty stream is returned. +func (ssa *SQLStorageAuthorityRO) GetEnabledRateLimitOverrides(_ *emptypb.Empty, stream sapb.StorageAuthorityReadOnly_GetEnabledRateLimitOverridesServer) error { + selector, err := db.NewMappedSelector[overrideModel](ssa.dbReadOnlyMap) + if err != nil { + return fmt.Errorf("initializing selector: %w", err) + } + + rows, err := selector.QueryContext(stream.Context(), "WHERE enabled = true") + if err != nil { + return fmt.Errorf("querying enabled overrides: %w", err) + } + + return rows.ForEach(func(m *overrideModel) error { + return stream.Send(newPBFromOverrideModel(m)) + }) +} diff --git a/third-party/github.com/letsencrypt/boulder/sa/satest/satest.go b/third-party/github.com/letsencrypt/boulder/sa/satest/satest.go index be4795fee..cb1b18839 100644 --- a/third-party/github.com/letsencrypt/boulder/sa/satest/satest.go +++ b/third-party/github.com/letsencrypt/boulder/sa/satest/satest.go @@ -2,7 +2,6 @@ package satest import ( "context" - "net" "testing" "time" @@ -16,7 +15,6 @@ import ( // SA using GoodKey under the hood. This is used by various non-SA tests // to initialize the a registration for the test to reference. func CreateWorkingRegistration(t *testing.T, sa sapb.StorageAuthorityClient) *corepb.Registration { - initialIP, _ := net.ParseIP("88.77.66.11").MarshalText() reg, err := sa.NewRegistration(context.Background(), &corepb.Registration{ Key: []byte(`{ "kty": "RSA", @@ -24,7 +22,6 @@ func CreateWorkingRegistration(t *testing.T, sa sapb.StorageAuthorityClient) *co "e": "AQAB" }`), Contact: []string{"mailto:foo@example.com"}, - InitialIP: initialIP, CreatedAt: timestamppb.New(time.Date(2003, 5, 10, 0, 0, 0, 0, time.UTC)), Status: string(core.StatusValid), }) diff --git a/third-party/github.com/letsencrypt/boulder/sa/type-converter.go b/third-party/github.com/letsencrypt/boulder/sa/type-converter.go index 2ffb5bc1b..d7d92eb79 100644 --- a/third-party/github.com/letsencrypt/boulder/sa/type-converter.go +++ b/third-party/github.com/letsencrypt/boulder/sa/type-converter.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "time" "github.com/go-jose/go-jose/v4" @@ -35,6 +36,18 @@ func (tc BoulderTypeConverter) ToDb(val interface{}) (interface{}, error) { return string(t), nil case core.OCSPStatus: return string(t), nil + // Time types get truncated to the nearest second. Given our DB schema, + // only seconds are stored anyhow. Avoiding sending queries with sub-second + // precision may help the query planner avoid pathological cases when + // querying against indexes on time fields (#5437). + case time.Time: + return t.Truncate(time.Second), nil + case *time.Time: + if t == nil { + return nil, nil + } + newT := t.Truncate(time.Second) + return &newT, nil default: return val, nil } diff --git a/third-party/github.com/letsencrypt/boulder/sa/type-converter_test.go b/third-party/github.com/letsencrypt/boulder/sa/type-converter_test.go index c0849e759..8ca7d35d1 100644 --- a/third-party/github.com/letsencrypt/boulder/sa/type-converter_test.go +++ b/third-party/github.com/letsencrypt/boulder/sa/type-converter_test.go @@ -3,6 +3,7 @@ package sa import ( "encoding/json" "testing" + "time" "github.com/letsencrypt/boulder/core" "github.com/letsencrypt/boulder/identifier" @@ -151,3 +152,26 @@ func TestStringSlice(t *testing.T) { test.AssertNotError(t, err, "failed to scanner.Binder") test.AssertMarshaledEquals(t, au, out) } + +func TestTimeTruncate(t *testing.T) { + tc := BoulderTypeConverter{} + preciseTime := time.Date(2024, 06, 20, 00, 00, 00, 999999999, time.UTC) + dbTime, err := tc.ToDb(preciseTime) + test.AssertNotError(t, err, "Could not ToDb") + dbTimeT, ok := dbTime.(time.Time) + test.Assert(t, ok, "Could not convert dbTime to time.Time") + test.Assert(t, dbTimeT.Nanosecond() == 0, "Nanosecond not truncated") + + dbTimePtr, err := tc.ToDb(&preciseTime) + test.AssertNotError(t, err, "Could not ToDb") + dbTimePtrT, ok := dbTimePtr.(*time.Time) + test.Assert(t, ok, "Could not convert dbTimePtr to *time.Time") + test.Assert(t, dbTimePtrT.Nanosecond() == 0, "Nanosecond not truncated") + + var dbTimePtrNil *time.Time + shouldBeNil, err := tc.ToDb(dbTimePtrNil) + test.AssertNotError(t, err, "Could not ToDb") + if shouldBeNil != nil { + t.Errorf("Expected nil, got %v", shouldBeNil) + } +} diff --git a/third-party/github.com/letsencrypt/boulder/semaphore/semaphore_test.go b/third-party/github.com/letsencrypt/boulder/semaphore/semaphore_test.go index 71a5d2340..976491b73 100644 --- a/third-party/github.com/letsencrypt/boulder/semaphore/semaphore_test.go +++ b/third-party/github.com/letsencrypt/boulder/semaphore/semaphore_test.go @@ -6,14 +6,15 @@ package semaphore_test import ( "context" - "math/rand" + "math/rand/v2" "runtime" "sync" "testing" "time" - "github.com/letsencrypt/boulder/semaphore" "golang.org/x/sync/errgroup" + + "github.com/letsencrypt/boulder/semaphore" ) const maxSleep = 1 * time.Millisecond @@ -21,7 +22,7 @@ const maxSleep = 1 * time.Millisecond func HammerWeighted(sem *semaphore.Weighted, n int64, loops int) { for i := 0; i < loops; i++ { _ = sem.Acquire(context.Background(), n) - time.Sleep(time.Duration(rand.Int63n(int64(maxSleep/time.Nanosecond))) * time.Nanosecond) + time.Sleep(time.Duration(rand.Int64N(int64(maxSleep/time.Nanosecond))) * time.Nanosecond) sem.Release(n) } } diff --git a/third-party/github.com/letsencrypt/boulder/sfe/pages/index.html b/third-party/github.com/letsencrypt/boulder/sfe/pages/index.html new file mode 100644 index 000000000..fe2cf096f --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/sfe/pages/index.html @@ -0,0 +1,16 @@ +{{ template "header" }} + +
+

Invalid Portal URL

+

+ If you got here by visiting a URL found in your ACME client logs, please + carefully check that you copied the URL correctly. +

+

+ If you continue to encounter difficulties, or if you need more help, our + community support forum + is a great resource for troubleshooting and advice. +

+
+ +{{template "footer"}} diff --git a/third-party/github.com/letsencrypt/boulder/sfe/pages/unpause-expired.html b/third-party/github.com/letsencrypt/boulder/sfe/pages/unpause-expired.html new file mode 100644 index 000000000..69b8b81f8 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/sfe/pages/unpause-expired.html @@ -0,0 +1,19 @@ +{{ template "header" }} + +
+

Expired unpause URL

+

+ If you got here by visiting a URL found in your ACME client logs, please + try an unpause URL from a more recent log entry. Each unpause URL is + only valid for a short period of time. If you cannot find a valid + unpause URL, you may need to re-run your ACME client to generate a new + one. +

+

+ If you continue to encounter difficulties, or if you need more help, our + community support forum + is a great resource for troubleshooting and advice. +

+
+ +{{template "footer"}} diff --git a/third-party/github.com/letsencrypt/boulder/sfe/pages/unpause-form.html b/third-party/github.com/letsencrypt/boulder/sfe/pages/unpause-form.html new file mode 100644 index 000000000..2554844a1 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/sfe/pages/unpause-form.html @@ -0,0 +1,73 @@ +{{ template "header" }} + +
+

Action required to unpause your account

+

+ You have been directed to this page because your ACME account (ID: {{ + .AccountID }}) is temporarily restricted from requesting new + certificates for certain identifiers including, but not limited to, the + following: +

+
    + {{ range $identifier := .Idents }}
  • {{ $identifier}}
  • {{ end }} +
+

+ These identifiers were paused after consistently failing validation + attempts without any successes over an extended period. +

+
+ +
+

Why did this happen?

+

+ This often happens when domain names expire, point to new hosts, or if + there are issues with the DNS configuration or web server settings. + These problems prevent your ACME client from successfully + validating control over + the domain, which is necessary for issuing TLS certificates. +

+
+ +
+

What can you do?

+

+ Please check the DNS configuration and web server settings for the + affected identifiers. Ensure they are properly set up to respond to ACME + challenges. This could include: +

    +
  • updating DNS records,
  • +
  • renewing domain registrations, or
  • +
  • adjusting web server configurations.
  • +
+ + If you use a hosting provider or third-party service for domain management, + you may need to coordinate with them. If you believe you've fixed the + underlying issue, consider attempting issuance against our staging + environment to verify your fix. +

+
+ +
+

Ready to unpause?

+

+ If you believe these issues have been addressed, click the button below + to remove the pause on your account. This action will allow you to + resume requesting certificates for all affected identifiers associated + with your account, not just those listed above. +

+
+ +
+
+ +
+

+ Note: If you encounter difficulties unpausing your account, or + you need more help, our community support forum is + a great resource for troubleshooting and advice. +

+
+ +{{template "footer"}} diff --git a/third-party/github.com/letsencrypt/boulder/sfe/pages/unpause-invalid-request.html b/third-party/github.com/letsencrypt/boulder/sfe/pages/unpause-invalid-request.html new file mode 100644 index 000000000..6bb45eeac --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/sfe/pages/unpause-invalid-request.html @@ -0,0 +1,16 @@ +{{ template "header" }} + +
+

Invalid unpause URL

+

+ If you got here by visiting a URL found in your ACME client logs, please + carefully check that you copied the URL correctly. +

+

+ If you continue to encounter difficulties, or if you need more help, our + community support forum + is a great resource for troubleshooting and advice. +

+
+ +{{template "footer"}} diff --git a/third-party/github.com/letsencrypt/boulder/sfe/pages/unpause-status.html b/third-party/github.com/letsencrypt/boulder/sfe/pages/unpause-status.html new file mode 100644 index 000000000..3f1c7b5b6 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/sfe/pages/unpause-status.html @@ -0,0 +1,47 @@ +{{ template "header" }} + +
+ + {{ if and .Successful (gt .Count 0) (lt .Count .Limit) }} +

Successfully unpaused all {{ .Count }} identifier(s)

+

+ To obtain a new certificate, re-attempt issuance with your ACME client. + Future repeated validation failures with no successes will result in + identifiers being paused again. +

+ + {{ else if and .Successful (eq .Count .Limit)}} +

Some identifiers were unpaused

+

+ We can only unpause a limited number of identifiers for each request ({{ + .Limit }}). There are potentially more identifiers paused for your + account. +

+

+ To attempt to unpause more identifiers, visit the unpause URL from + your logs again and click the "Please Unpause My Account" button. +

+ + {{ else if and .Successful (eq .Count 0) }} +

Account already unpaused

+

+ There were no identifiers to unpause for your account. If you face + continued difficulties, please visit our community support forum + for troubleshooting and advice. +

+ + {{ else }} +

An error occurred while unpausing your account

+

+ Please try again later. If you face continued difficulties, please visit + our community support + forum + for troubleshooting and advice. +

+ + {{ end }} + +
+ +{{ template "footer" }} diff --git a/third-party/github.com/letsencrypt/boulder/sfe/sfe.go b/third-party/github.com/letsencrypt/boulder/sfe/sfe.go new file mode 100644 index 000000000..063d706b2 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/sfe/sfe.go @@ -0,0 +1,293 @@ +package sfe + +import ( + "embed" + "errors" + "fmt" + "html/template" + "io/fs" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "github.com/go-jose/go-jose/v4/jwt" + "github.com/jmhodges/clock" + "github.com/prometheus/client_golang/prometheus" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + + "github.com/letsencrypt/boulder/core" + blog "github.com/letsencrypt/boulder/log" + "github.com/letsencrypt/boulder/metrics/measured_http" + rapb "github.com/letsencrypt/boulder/ra/proto" + sapb "github.com/letsencrypt/boulder/sa/proto" + "github.com/letsencrypt/boulder/unpause" +) + +const ( + unpausePostForm = unpause.APIPrefix + "/do-unpause" + unpauseStatus = unpause.APIPrefix + "/unpause-status" +) + +var ( + //go:embed all:static + staticFS embed.FS + + //go:embed all:templates all:pages all:static + dynamicFS embed.FS +) + +// SelfServiceFrontEndImpl provides all the logic for Boulder's selfservice +// frontend web-facing interface, i.e., a portal where a subscriber can unpause +// their account. Its methods are primarily handlers for HTTPS requests for the +// various non-ACME functions. +type SelfServiceFrontEndImpl struct { + ra rapb.RegistrationAuthorityClient + sa sapb.StorageAuthorityReadOnlyClient + + log blog.Logger + clk clock.Clock + + // requestTimeout is the per-request overall timeout. + requestTimeout time.Duration + + unpauseHMACKey []byte + templatePages *template.Template +} + +// NewSelfServiceFrontEndImpl constructs a web service for Boulder +func NewSelfServiceFrontEndImpl( + stats prometheus.Registerer, + clk clock.Clock, + logger blog.Logger, + requestTimeout time.Duration, + rac rapb.RegistrationAuthorityClient, + sac sapb.StorageAuthorityReadOnlyClient, + unpauseHMACKey []byte, +) (SelfServiceFrontEndImpl, error) { + + // Parse the files once at startup to avoid each request causing the server + // to JIT parse. The pages are stored in an in-memory embed.FS to prevent + // unnecessary filesystem I/O on a physical HDD. + tmplPages := template.Must(template.New("pages").ParseFS(dynamicFS, "templates/layout.html", "pages/*")) + + sfe := SelfServiceFrontEndImpl{ + log: logger, + clk: clk, + requestTimeout: requestTimeout, + ra: rac, + sa: sac, + unpauseHMACKey: unpauseHMACKey, + templatePages: tmplPages, + } + + return sfe, nil +} + +// handleWithTimeout registers a handler with a timeout using an +// http.TimeoutHandler. +func (sfe *SelfServiceFrontEndImpl) handleWithTimeout(mux *http.ServeMux, path string, handler http.HandlerFunc) { + timeout := sfe.requestTimeout + if timeout <= 0 { + // Default to 5 minutes if no timeout is set. + timeout = 5 * time.Minute + } + timeoutHandler := http.TimeoutHandler(handler, timeout, "Request timed out") + mux.Handle(path, timeoutHandler) +} + +// Handler returns an http.Handler that uses various functions for various +// non-ACME-specified paths. Each endpoint should have a corresponding HTML +// page that shares the same name as the endpoint. +func (sfe *SelfServiceFrontEndImpl) Handler(stats prometheus.Registerer, oTelHTTPOptions ...otelhttp.Option) http.Handler { + mux := http.NewServeMux() + + sfs, _ := fs.Sub(staticFS, "static") + staticAssetsHandler := http.StripPrefix("/static/", http.FileServerFS(sfs)) + mux.Handle("GET /static/", staticAssetsHandler) + + sfe.handleWithTimeout(mux, "/", sfe.Index) + sfe.handleWithTimeout(mux, "GET /build", sfe.BuildID) + sfe.handleWithTimeout(mux, "GET "+unpause.GetForm, sfe.UnpauseForm) + sfe.handleWithTimeout(mux, "POST "+unpausePostForm, sfe.UnpauseSubmit) + sfe.handleWithTimeout(mux, "GET "+unpauseStatus, sfe.UnpauseStatus) + + return measured_http.New(mux, sfe.clk, stats, oTelHTTPOptions...) +} + +// renderTemplate takes the name of an HTML template and optional dynamicData +// which are rendered and served back to the client via the response writer. +func (sfe *SelfServiceFrontEndImpl) renderTemplate(w http.ResponseWriter, filename string, dynamicData any) { + if len(filename) == 0 { + http.Error(w, "Template page does not exist", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + err := sfe.templatePages.ExecuteTemplate(w, filename, dynamicData) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +// Index is the homepage of the SFE +func (sfe *SelfServiceFrontEndImpl) Index(response http.ResponseWriter, request *http.Request) { + sfe.renderTemplate(response, "index.html", nil) +} + +// BuildID tells the requester what boulder build version is running. +func (sfe *SelfServiceFrontEndImpl) BuildID(response http.ResponseWriter, request *http.Request) { + response.Header().Set("Content-Type", "text/plain") + response.WriteHeader(http.StatusOK) + detailsString := fmt.Sprintf("Boulder=(%s %s)", core.GetBuildID(), core.GetBuildTime()) + if _, err := fmt.Fprintln(response, detailsString); err != nil { + sfe.log.Warningf("Could not write response: %s", err) + } +} + +// UnpauseForm allows a requester to unpause their account via a form present on +// the page. The Subscriber's client will receive a log line emitted by the WFE +// which contains a URL pre-filled with a JWT that will populate a hidden field +// in this form. +func (sfe *SelfServiceFrontEndImpl) UnpauseForm(response http.ResponseWriter, request *http.Request) { + incomingJWT := request.URL.Query().Get("jwt") + + accountID, idents, err := sfe.parseUnpauseJWT(incomingJWT) + if err != nil { + if errors.Is(err, jwt.ErrExpired) { + // JWT expired before the Subscriber visited the unpause page. + sfe.unpauseTokenExpired(response) + return + } + if errors.Is(err, unpause.ErrMalformedJWT) { + // JWT is malformed. This could happen if the Subscriber failed to + // copy the entire URL from their logs. + sfe.unpauseRequestMalformed(response) + return + } + sfe.unpauseFailed(response) + return + } + + // If any of these values change, ensure any relevant pages in //sfe/pages/ + // are also updated. + type tmplData struct { + PostPath string + JWT string + AccountID int64 + Idents []string + } + + // Present the unpause form to the Subscriber. + sfe.renderTemplate(response, "unpause-form.html", tmplData{unpausePostForm, incomingJWT, accountID, idents}) +} + +// UnpauseSubmit serves a page showing the result of the unpause form submission. +// CSRF is not addressed because a third party causing submission of an unpause +// form is not harmful. +func (sfe *SelfServiceFrontEndImpl) UnpauseSubmit(response http.ResponseWriter, request *http.Request) { + incomingJWT := request.URL.Query().Get("jwt") + + accountID, _, err := sfe.parseUnpauseJWT(incomingJWT) + if err != nil { + if errors.Is(err, jwt.ErrExpired) { + // JWT expired before the Subscriber could click the unpause button. + sfe.unpauseTokenExpired(response) + return + } + if errors.Is(err, unpause.ErrMalformedJWT) { + // JWT is malformed. This should never happen if the request came + // from our form. + sfe.unpauseRequestMalformed(response) + return + } + sfe.unpauseFailed(response) + return + } + + unpaused, err := sfe.ra.UnpauseAccount(request.Context(), &rapb.UnpauseAccountRequest{ + RegistrationID: accountID, + }) + if err != nil { + sfe.unpauseFailed(response) + return + } + + // Redirect to the unpause status page with the count of unpaused + // identifiers. + params := url.Values{} + params.Add("count", fmt.Sprintf("%d", unpaused.Count)) + http.Redirect(response, request, unpauseStatus+"?"+params.Encode(), http.StatusFound) +} + +func (sfe *SelfServiceFrontEndImpl) unpauseRequestMalformed(response http.ResponseWriter) { + sfe.renderTemplate(response, "unpause-invalid-request.html", nil) +} + +func (sfe *SelfServiceFrontEndImpl) unpauseTokenExpired(response http.ResponseWriter) { + sfe.renderTemplate(response, "unpause-expired.html", nil) +} + +type unpauseStatusTemplate struct { + Successful bool + Limit int64 + Count int64 +} + +func (sfe *SelfServiceFrontEndImpl) unpauseFailed(response http.ResponseWriter) { + sfe.renderTemplate(response, "unpause-status.html", unpauseStatusTemplate{Successful: false}) +} + +func (sfe *SelfServiceFrontEndImpl) unpauseSuccessful(response http.ResponseWriter, count int64) { + sfe.renderTemplate(response, "unpause-status.html", unpauseStatusTemplate{ + Successful: true, + Limit: unpause.RequestLimit, + Count: count}, + ) +} + +// UnpauseStatus displays a success message to the Subscriber indicating that +// their account has been unpaused. +func (sfe *SelfServiceFrontEndImpl) UnpauseStatus(response http.ResponseWriter, request *http.Request) { + if request.Method != http.MethodHead && request.Method != http.MethodGet { + response.Header().Set("Access-Control-Allow-Methods", "GET, HEAD") + response.WriteHeader(http.StatusMethodNotAllowed) + return + } + + count, err := strconv.ParseInt(request.URL.Query().Get("count"), 10, 64) + if err != nil || count < 0 { + sfe.unpauseFailed(response) + return + } + + sfe.unpauseSuccessful(response, count) +} + +// parseUnpauseJWT extracts and returns the subscriber's registration ID and a +// slice of paused identifiers from the claims. If the JWT cannot be parsed or +// is otherwise invalid, an error is returned. If the JWT is missing or +// malformed, unpause.ErrMalformedJWT is returned. +func (sfe *SelfServiceFrontEndImpl) parseUnpauseJWT(incomingJWT string) (int64, []string, error) { + if incomingJWT == "" || len(strings.Split(incomingJWT, ".")) != 3 { + // JWT is missing or malformed. This could happen if the Subscriber + // failed to copy the entire URL from their logs. This should never + // happen if the request came from our form. + return 0, nil, unpause.ErrMalformedJWT + } + + claims, err := unpause.RedeemJWT(incomingJWT, sfe.unpauseHMACKey, unpause.APIVersion, sfe.clk) + if err != nil { + return 0, nil, err + } + + account, convErr := strconv.ParseInt(claims.Subject, 10, 64) + if convErr != nil { + // This should never happen as this was just validated by the call to + // unpause.RedeemJWT(). + return 0, nil, errors.New("failed to parse account ID from JWT") + } + + return account, strings.Split(claims.I, ","), nil +} diff --git a/third-party/github.com/letsencrypt/boulder/sfe/sfe_test.go b/third-party/github.com/letsencrypt/boulder/sfe/sfe_test.go new file mode 100644 index 000000000..b8f41a913 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/sfe/sfe_test.go @@ -0,0 +1,230 @@ +package sfe + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "testing" + "time" + + "github.com/jmhodges/clock" + "google.golang.org/grpc" + + "github.com/letsencrypt/boulder/cmd" + "github.com/letsencrypt/boulder/features" + blog "github.com/letsencrypt/boulder/log" + "github.com/letsencrypt/boulder/metrics" + "github.com/letsencrypt/boulder/mocks" + "github.com/letsencrypt/boulder/must" + "github.com/letsencrypt/boulder/test" + "github.com/letsencrypt/boulder/unpause" + + rapb "github.com/letsencrypt/boulder/ra/proto" +) + +type MockRegistrationAuthority struct { + rapb.RegistrationAuthorityClient +} + +func (ra *MockRegistrationAuthority) UnpauseAccount(context.Context, *rapb.UnpauseAccountRequest, ...grpc.CallOption) (*rapb.UnpauseAccountResponse, error) { + return &rapb.UnpauseAccountResponse{}, nil +} + +func mustParseURL(s string) *url.URL { + return must.Do(url.Parse(s)) +} + +func setupSFE(t *testing.T) (SelfServiceFrontEndImpl, clock.FakeClock) { + features.Reset() + + fc := clock.NewFake() + // Set to some non-zero time. + fc.Set(time.Date(2020, 10, 10, 0, 0, 0, 0, time.UTC)) + + stats := metrics.NoopRegisterer + + mockSA := mocks.NewStorageAuthorityReadOnly(fc) + + hmacKey := cmd.HMACKeyConfig{KeyFile: "../test/secrets/sfe_unpause_key"} + key, err := hmacKey.Load() + test.AssertNotError(t, err, "Unable to load HMAC key") + + sfe, err := NewSelfServiceFrontEndImpl( + stats, + fc, + blog.NewMock(), + 10*time.Second, + &MockRegistrationAuthority{}, + mockSA, + key, + ) + test.AssertNotError(t, err, "Unable to create SFE") + + return sfe, fc +} + +func TestIndexPath(t *testing.T) { + t.Parallel() + sfe, _ := setupSFE(t) + responseWriter := httptest.NewRecorder() + sfe.Index(responseWriter, &http.Request{ + Method: "GET", + URL: mustParseURL("/"), + }) + + test.AssertEquals(t, responseWriter.Code, http.StatusOK) + test.AssertContains(t, responseWriter.Body.String(), "Let's Encrypt - Portal") +} + +func TestBuildIDPath(t *testing.T) { + t.Parallel() + sfe, _ := setupSFE(t) + responseWriter := httptest.NewRecorder() + sfe.BuildID(responseWriter, &http.Request{ + Method: "GET", + URL: mustParseURL("/build"), + }) + + test.AssertEquals(t, responseWriter.Code, http.StatusOK) + test.AssertContains(t, responseWriter.Body.String(), "Boulder=(") +} + +func TestUnpausePaths(t *testing.T) { + t.Parallel() + sfe, fc := setupSFE(t) + unpauseSigner, err := unpause.NewJWTSigner(cmd.HMACKeyConfig{KeyFile: "../test/secrets/sfe_unpause_key"}) + test.AssertNotError(t, err, "Should have been able to create JWT signer, but could not") + + // GET with no JWT + responseWriter := httptest.NewRecorder() + sfe.UnpauseForm(responseWriter, &http.Request{ + Method: "GET", + URL: mustParseURL(unpause.GetForm), + }) + test.AssertEquals(t, responseWriter.Code, http.StatusOK) + test.AssertContains(t, responseWriter.Body.String(), "Invalid unpause URL") + + // GET with an invalid JWT + responseWriter = httptest.NewRecorder() + sfe.UnpauseForm(responseWriter, &http.Request{ + Method: "GET", + URL: mustParseURL(fmt.Sprintf(unpause.GetForm + "?jwt=x")), + }) + test.AssertEquals(t, responseWriter.Code, http.StatusOK) + test.AssertContains(t, responseWriter.Body.String(), "Invalid unpause URL") + + // GET with an expired JWT + expiredJWT, err := unpause.GenerateJWT(unpauseSigner, 1234567890, []string{"example.net"}, time.Hour, fc) + test.AssertNotError(t, err, "Should have been able to create JWT, but could not") + responseWriter = httptest.NewRecorder() + // Advance the clock by 337 hours to make the JWT expired. + fc.Add(time.Hour * 337) + sfe.UnpauseForm(responseWriter, &http.Request{ + Method: "GET", + URL: mustParseURL(unpause.GetForm + "?jwt=" + expiredJWT), + }) + test.AssertEquals(t, responseWriter.Code, http.StatusOK) + test.AssertContains(t, responseWriter.Body.String(), "Expired unpause URL") + + // GET with a valid JWT and a single identifier + validJWT, err := unpause.GenerateJWT(unpauseSigner, 1234567890, []string{"example.com"}, time.Hour, fc) + test.AssertNotError(t, err, "Should have been able to create JWT, but could not") + responseWriter = httptest.NewRecorder() + sfe.UnpauseForm(responseWriter, &http.Request{ + Method: "GET", + URL: mustParseURL(unpause.GetForm + "?jwt=" + validJWT), + }) + test.AssertEquals(t, responseWriter.Code, http.StatusOK) + test.AssertContains(t, responseWriter.Body.String(), "Action required to unpause your account") + test.AssertContains(t, responseWriter.Body.String(), "example.com") + + // GET with a valid JWT and multiple identifiers + validJWT, err = unpause.GenerateJWT(unpauseSigner, 1234567890, []string{"example.com", "example.net", "example.org"}, time.Hour, fc) + test.AssertNotError(t, err, "Should have been able to create JWT, but could not") + responseWriter = httptest.NewRecorder() + sfe.UnpauseForm(responseWriter, &http.Request{ + Method: "GET", + URL: mustParseURL(unpause.GetForm + "?jwt=" + validJWT), + }) + test.AssertEquals(t, responseWriter.Code, http.StatusOK) + test.AssertContains(t, responseWriter.Body.String(), "Action required to unpause your account") + test.AssertContains(t, responseWriter.Body.String(), "example.com") + test.AssertContains(t, responseWriter.Body.String(), "example.net") + test.AssertContains(t, responseWriter.Body.String(), "example.org") + + // POST with an expired JWT + responseWriter = httptest.NewRecorder() + sfe.UnpauseSubmit(responseWriter, &http.Request{ + Method: "POST", + URL: mustParseURL(unpausePostForm + "?jwt=" + expiredJWT), + }) + test.AssertEquals(t, responseWriter.Code, http.StatusOK) + test.AssertContains(t, responseWriter.Body.String(), "Expired unpause URL") + + // POST with no JWT + responseWriter = httptest.NewRecorder() + sfe.UnpauseSubmit(responseWriter, &http.Request{ + Method: "POST", + URL: mustParseURL(unpausePostForm), + }) + test.AssertEquals(t, responseWriter.Code, http.StatusOK) + test.AssertContains(t, responseWriter.Body.String(), "Invalid unpause URL") + + // POST with an invalid JWT, missing one of the three parts + responseWriter = httptest.NewRecorder() + sfe.UnpauseSubmit(responseWriter, &http.Request{ + Method: "POST", + URL: mustParseURL(unpausePostForm + "?jwt=x.x"), + }) + test.AssertEquals(t, responseWriter.Code, http.StatusOK) + test.AssertContains(t, responseWriter.Body.String(), "Invalid unpause URL") + + // POST with an invalid JWT, all parts present but missing some characters + responseWriter = httptest.NewRecorder() + sfe.UnpauseSubmit(responseWriter, &http.Request{ + Method: "POST", + URL: mustParseURL(unpausePostForm + "?jwt=x.x.x"), + }) + test.AssertEquals(t, responseWriter.Code, http.StatusOK) + test.AssertContains(t, responseWriter.Body.String(), "Invalid unpause URL") + + // POST with a valid JWT redirects to a success page + responseWriter = httptest.NewRecorder() + sfe.UnpauseSubmit(responseWriter, &http.Request{ + Method: "POST", + URL: mustParseURL(unpausePostForm + "?jwt=" + validJWT), + }) + test.AssertEquals(t, responseWriter.Code, http.StatusFound) + test.AssertEquals(t, unpauseStatus+"?count=0", responseWriter.Result().Header.Get("Location")) + + // Redirecting after a successful unpause POST displays the success page. + responseWriter = httptest.NewRecorder() + sfe.UnpauseStatus(responseWriter, &http.Request{ + Method: "GET", + URL: mustParseURL(unpauseStatus + "?count=1"), + }) + test.AssertEquals(t, responseWriter.Code, http.StatusOK) + test.AssertContains(t, responseWriter.Body.String(), "Successfully unpaused all 1 identifier(s)") + + // Redirecting after a successful unpause POST with a count of 0 displays + // the already unpaused page. + responseWriter = httptest.NewRecorder() + sfe.UnpauseStatus(responseWriter, &http.Request{ + Method: "GET", + URL: mustParseURL(unpauseStatus + "?count=0"), + }) + test.AssertEquals(t, responseWriter.Code, http.StatusOK) + test.AssertContains(t, responseWriter.Body.String(), "Account already unpaused") + + // Redirecting after a successful unpause POST with a count equal to the + // maximum number of identifiers displays the success with caveat page. + responseWriter = httptest.NewRecorder() + sfe.UnpauseStatus(responseWriter, &http.Request{ + Method: "GET", + URL: mustParseURL(unpauseStatus + "?count=" + fmt.Sprintf("%d", unpause.RequestLimit)), + }) + test.AssertEquals(t, responseWriter.Code, http.StatusOK) + test.AssertContains(t, responseWriter.Body.String(), "Some identifiers were unpaused") +} diff --git a/third-party/github.com/letsencrypt/boulder/sfe/static/favicon.ico b/third-party/github.com/letsencrypt/boulder/sfe/static/favicon.ico new file mode 100644 index 000000000..9196d22db Binary files /dev/null and b/third-party/github.com/letsencrypt/boulder/sfe/static/favicon.ico differ diff --git a/third-party/github.com/letsencrypt/boulder/sfe/static/logo.svg b/third-party/github.com/letsencrypt/boulder/sfe/static/logo.svg new file mode 100644 index 000000000..4a09441b9 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/sfe/static/logo.svg @@ -0,0 +1,38 @@ + + + + + Layer 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/third-party/github.com/letsencrypt/boulder/sfe/templates/layout.html b/third-party/github.com/letsencrypt/boulder/sfe/templates/layout.html new file mode 100644 index 000000000..15d5e88d9 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/sfe/templates/layout.html @@ -0,0 +1,117 @@ +{{define "header"}} + + + + + + Let's Encrypt - Portal + + + + +
+
+ Let's Encrypt +
+
+{{ end }} + +{{ define "footer" }} + + + +{{ end }} diff --git a/third-party/github.com/letsencrypt/boulder/staticcheck.conf b/third-party/github.com/letsencrypt/boulder/staticcheck.conf deleted file mode 100644 index 00370524d..000000000 --- a/third-party/github.com/letsencrypt/boulder/staticcheck.conf +++ /dev/null @@ -1,8 +0,0 @@ -# Ignores the following: -# 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 - -checks = ["all", "-SA1019", "-SA6003", "-ST1000", "-ST1003", "-ST1005"] diff --git a/third-party/github.com/letsencrypt/boulder/t.sh b/third-party/github.com/letsencrypt/boulder/t.sh index 08f181f59..b49a916ee 100644 --- a/third-party/github.com/letsencrypt/boulder/t.sh +++ b/third-party/github.com/letsencrypt/boulder/t.sh @@ -10,9 +10,6 @@ if type realpath >/dev/null 2>&1 ; then fi # Generate the test keys and certs necessary for the integration tests. -docker compose run bsetup +docker compose run --rm bsetup -# Use a predictable name for the container so we can grab the logs later -# for use when testing logs analysis tools. -docker rm boulder_tests || true -exec docker compose run --name boulder_tests boulder ./test.sh "$@" +exec docker compose run --rm --name boulder_tests boulder ./test.sh "$@" diff --git a/third-party/github.com/letsencrypt/boulder/test.sh b/third-party/github.com/letsencrypt/boulder/test.sh index 6f8bedd76..e54504076 100644 --- a/third-party/github.com/letsencrypt/boulder/test.sh +++ b/third-party/github.com/letsencrypt/boulder/test.sh @@ -17,8 +17,17 @@ STATUS="FAILURE" RUN=() UNIT_PACKAGES=() UNIT_FLAGS=() +INTEGRATION_FLAGS=() FILTER=() +# +# Cleanup Functions +# + +function flush_redis() { + go run ./test/boulder-tools/flushredis/main.go +} + # # Print Functions # @@ -31,11 +40,6 @@ function print_outcome() { fi } -function print_list_of_integration_tests() { - go test -tags integration -list=. ./test/integration/... | grep '^Test' - exit 0 -} - function exit_msg() { # complain to STDERR and exit with error echo "$*" >&2 @@ -93,7 +97,7 @@ With no options passed, runs standard battery of tests (lint, unit, and integrat -l, --lints Adds lint to the list of tests to run -u, --unit Adds unit to the list of tests to run - -v, --unit-verbose Enables verbose output for unit tests + -v, --verbose Enables verbose output for unit and integration tests -w, --unit-without-cache Disables go test caching for unit tests -p , --unit-test-package= Run unit tests for specific go package(s) -e, --enable-race-detection Enables race detection for unit and integration tests @@ -101,7 +105,6 @@ With no options passed, runs standard battery of tests (lint, unit, and integrat -i, --integration Adds integration to the list of tests to run -s, --start-py Adds start to the list of tests to run -g, --generate Adds generate to the list of tests to run - -o, --list-integration-tests Outputs a list of the available integration tests -f , --filter= Run only those tests matching the regular expression Note: @@ -117,7 +120,7 @@ With no options passed, runs standard battery of tests (lint, unit, and integrat EOM )" -while getopts luvweciosmgnhp:f:-: OPT; do +while getopts luvwecismgnhp:f:-: OPT; do if [ "$OPT" = - ]; then # long option: reformulate OPT and OPTARG OPT="${OPTARG%%=*}" # extract long option name OPTARG="${OPTARG#$OPT}" # extract long option argument (may be empty) @@ -126,12 +129,11 @@ while getopts luvweciosmgnhp:f:-: OPT; do case "$OPT" in l | lints ) RUN+=("lints") ;; u | unit ) RUN+=("unit") ;; - v | unit-verbose ) UNIT_FLAGS+=("-v") ;; + v | verbose ) UNIT_FLAGS+=("-v"); INTEGRATION_FLAGS+=("-v") ;; w | unit-without-cache ) UNIT_FLAGS+=("-count=1") ;; p | unit-test-package ) check_arg; UNIT_PACKAGES+=("${OPTARG}") ;; e | enable-race-detection ) RACE="true"; UNIT_FLAGS+=("-race") ;; i | integration ) RUN+=("integration") ;; - o | list-integration-tests ) print_list_of_integration_tests ;; f | filter ) check_arg; FILTER+=("${OPTARG}") ;; s | start-py ) RUN+=("start") ;; g | generate ) RUN+=("generate") ;; @@ -209,8 +211,6 @@ STAGE="lints" if [[ "${RUN[@]}" =~ "$STAGE" ]] ; then print_heading "Running Lints" golangci-lint run --timeout 9m ./... - # Implicitly loads staticcheck.conf from the root of the boulder repository - staticcheck ./... python3 test/grafana/lint.py # Check for common spelling errors using typos. # Update .typos.toml if you find false positives @@ -225,6 +225,7 @@ fi STAGE="unit" if [[ "${RUN[@]}" =~ "$STAGE" ]] ; then print_heading "Running Unit Tests" + flush_redis run_unit_tests fi @@ -234,7 +235,12 @@ fi STAGE="integration" if [[ "${RUN[@]}" =~ "$STAGE" ]] ; then print_heading "Running Integration Tests" - python3 test/integration-test.py --chisel --gotest "${FILTER[@]}" + flush_redis + if [[ "${INTEGRATION_FLAGS[@]}" =~ "-v" ]] ; then + python3 test/integration-test.py --chisel --gotestverbose "${FILTER[@]}" + else + python3 test/integration-test.py --chisel --gotest "${FILTER[@]}" + fi fi # Test that just ./start.py works, which is a proxy for testing that diff --git a/third-party/github.com/letsencrypt/boulder/test/asserts.go b/third-party/github.com/letsencrypt/boulder/test/asserts.go index 73377423f..d0dbf29bb 100644 --- a/third-party/github.com/letsencrypt/boulder/test/asserts.go +++ b/third-party/github.com/letsencrypt/boulder/test/asserts.go @@ -147,7 +147,7 @@ func AssertUnmarshaledEquals(t *testing.T, got, expected string) { err = json.Unmarshal([]byte(expected), &expectedMap) AssertNotError(t, err, "Could not unmarshal 'expected'") if len(gotMap) != len(expectedMap) { - t.Errorf("Expected had %d keys, got had %d", len(gotMap), len(expectedMap)) + t.Errorf("Expected %d keys, but got %d", len(expectedMap), len(gotMap)) } for k, v := range expectedMap { if !reflect.DeepEqual(v, gotMap[k]) { @@ -247,5 +247,7 @@ loop: total += float64(iom.Histogram.GetSampleCount()) } } - AssertEquals(t, total, expected) + if total != expected { + t.Errorf("metric with labels %+v: got %g, want %g", l, total, expected) + } } diff --git a/third-party/github.com/letsencrypt/boulder/test/block-a-key/main.go b/third-party/github.com/letsencrypt/boulder/test/block-a-key/main.go deleted file mode 100644 index 0d027712a..000000000 --- a/third-party/github.com/letsencrypt/boulder/test/block-a-key/main.go +++ /dev/null @@ -1,108 +0,0 @@ -// block-a-key is a small utility for creating key blocklist entries. -package main - -import ( - "crypto" - "errors" - "flag" - "fmt" - "log" - "os" - - "github.com/letsencrypt/boulder/core" - "github.com/letsencrypt/boulder/web" -) - -const usageHelp = ` -block-a-key is utility tool for generating a SHA256 hash of the SubjectPublicKeyInfo -from a certificate or a synthetic SubjectPublicKeyInfo generated from a JWK public key. -It outputs the Base64 encoding of that hash. - -The produced encoded digest can be used with Boulder's key blocklist to block -any ACME account creation or certificate requests that use the same public -key. - -If you already have an SPKI hash, and it's a SHA256 hash, you can add it directly -to the key blocklist. If it's in hex form you'll need to convert it to base64 first. - -installation: - go install github.com/letsencrypt/boulder/test/block-a-key/... - -usage: - block-a-key -cert - block-a-key -jwk - -output format: - # - - "" - -examples: - $> block-a-key -jwk ./test/block-a-key/test/test.ecdsa.jwk.json - ./test/block-a-key/test/test.ecdsa.jwk.json cuwGhNNI6nfob5aqY90e7BleU6l7rfxku4X3UTJ3Z7M= - $> block-a-key -cert ./test/block-a-key/test/test.rsa.cert.pem - ./test/block-a-key/test/test.rsa.cert.pem Qebc1V3SkX3izkYRGNJilm9Bcuvf0oox4U2Rn+b4JOE= -` - -// keyFromCert returns the public key from a PEM encoded certificate located in -// pemFile or returns an error. -func keyFromCert(pemFile string) (crypto.PublicKey, error) { - c, err := core.LoadCert(pemFile) - if err != nil { - return nil, err - } - return c.PublicKey, nil -} - -// keyFromJWK returns the public key from a JSON encoded JOSE JWK located in -// jsonFile or returns an error. -func keyFromJWK(jsonFile string) (crypto.PublicKey, error) { - jwk, err := web.LoadJWK(jsonFile) - if err != nil { - return nil, err - } - return jwk.Key, nil -} - -func main() { - certFileArg := flag.String("cert", "", "path to a PEM encoded X509 certificate file") - jwkFileArg := flag.String("jwk", "", "path to a JSON encoded JWK file") - - flag.Usage = func() { - fmt.Fprintf(os.Stderr, "%s\n\n", usageHelp) - fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) - flag.PrintDefaults() - } - - flag.Parse() - - if *certFileArg == "" && *jwkFileArg == "" { - log.Fatalf("error: a -cert or -jwk argument must be provided") - } - - if *certFileArg != "" && *jwkFileArg != "" { - log.Fatalf("error: -cert and -jwk arguments are mutually exclusive") - } - - var file string - var key crypto.PublicKey - var err error - - if *certFileArg != "" { - file = *certFileArg - key, err = keyFromCert(file) - } else if *jwkFileArg != "" { - file = *jwkFileArg - key, err = keyFromJWK(file) - } else { - err = errors.New("unexpected command line state") - } - if err != nil { - log.Fatalf("error loading public key: %v", err) - } - - spkiHash, err := core.KeyDigestB64(key) - if err != nil { - log.Fatalf("error computing spki hash: %v", err) - } - fmt.Printf(" # %s\n - %s\n", file, spkiHash) -} diff --git a/third-party/github.com/letsencrypt/boulder/test/block-a-key/main_test.go b/third-party/github.com/letsencrypt/boulder/test/block-a-key/main_test.go deleted file mode 100644 index 6dbe265e0..000000000 --- a/third-party/github.com/letsencrypt/boulder/test/block-a-key/main_test.go +++ /dev/null @@ -1,59 +0,0 @@ -package main - -import ( - "crypto" - "testing" - - "github.com/letsencrypt/boulder/core" - "github.com/letsencrypt/boulder/test" -) - -func TestKeyBlocking(t *testing.T) { - testCases := []struct { - name string - certPath string - jwkPath string - expected string - }{ - // NOTE(@cpu): The JWKs and certificates were generated with the same - // keypair within an algorithm/parameter family. E.g. the RSA JWK public key - // matches the RSA certificate public key. The ECDSA JWK public key matches - // the ECDSA certificate public key. - { - name: "P-256 ECDSA JWK", - jwkPath: "test/test.ecdsa.jwk.json", - expected: "cuwGhNNI6nfob5aqY90e7BleU6l7rfxku4X3UTJ3Z7M=", - }, - { - name: "2048 RSA JWK", - jwkPath: "test/test.rsa.jwk.json", - expected: "Qebc1V3SkX3izkYRGNJilm9Bcuvf0oox4U2Rn+b4JOE=", - }, - { - name: "P-256 ECDSA Certificate", - certPath: "test/test.ecdsa.cert.pem", - expected: "cuwGhNNI6nfob5aqY90e7BleU6l7rfxku4X3UTJ3Z7M=", - }, - { - name: "2048 RSA Certificate", - certPath: "test/test.rsa.cert.pem", - expected: "Qebc1V3SkX3izkYRGNJilm9Bcuvf0oox4U2Rn+b4JOE=", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - var key crypto.PublicKey - var err error - if tc.jwkPath != "" { - key, err = keyFromJWK(tc.jwkPath) - } else { - key, err = keyFromCert(tc.certPath) - } - test.AssertNotError(t, err, "error getting key from input file") - spkiHash, err := core.KeyDigestB64(key) - test.AssertNotError(t, err, "error computing spki hash") - test.AssertEquals(t, spkiHash, tc.expected) - }) - } -} diff --git a/third-party/github.com/letsencrypt/boulder/test/block-a-key/test/README.txt b/third-party/github.com/letsencrypt/boulder/test/block-a-key/test/README.txt deleted file mode 100644 index 9035a4a56..000000000 --- a/third-party/github.com/letsencrypt/boulder/test/block-a-key/test/README.txt +++ /dev/null @@ -1,7 +0,0 @@ -The test files in this directory can be recreated with the following small program: - - https://gist.github.com/cpu/df50564a473b3e8556917eb80d99ea56 - -Crucially the public keys in the generated JWKs/Certs are shared within -algorithm/parameters. E.g. the ECDSA JWK has the same public key as the ECDSA -Cert. diff --git a/third-party/github.com/letsencrypt/boulder/test/block-a-key/test/test.ecdsa.cert.pem b/third-party/github.com/letsencrypt/boulder/test/block-a-key/test/test.ecdsa.cert.pem deleted file mode 100644 index 09bc304f1..000000000 --- a/third-party/github.com/letsencrypt/boulder/test/block-a-key/test/test.ecdsa.cert.pem +++ /dev/null @@ -1,8 +0,0 @@ ------BEGIN CERTIFICATE----- -MIH1MIGboAMCAQICAQEwCgYIKoZIzj0EAwIwADAiGA8wMDAxMDEwMTAwMDAwMFoY -DzAwMDEwMTAxMDAwMDAwWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE4LqG -kzIYWSgmyTS+B9Eet1xx1wpCKiSklMPnHfFp8eSHr1uNk6ilWv/s4AoKHSvMNAb/ -1uPfxjlijEIjK2bOQKMCMAAwCgYIKoZIzj0EAwIDSQAwRgIhAJBK1/C1BYDnzSCu -cR2pE40d8dyrRuHKj8htO/fzRgCgAiEA0UG0Vda8w0Tp84AMlJpZHOx9QUbwExSl -oFEDADJ9WQM= ------END CERTIFICATE----- diff --git a/third-party/github.com/letsencrypt/boulder/test/block-a-key/test/test.ecdsa.jwk.json b/third-party/github.com/letsencrypt/boulder/test/block-a-key/test/test.ecdsa.jwk.json deleted file mode 100644 index 364a666d2..000000000 --- a/third-party/github.com/letsencrypt/boulder/test/block-a-key/test/test.ecdsa.jwk.json +++ /dev/null @@ -1 +0,0 @@ -{"kty":"EC","crv":"P-256","alg":"ECDSA","x":"4LqGkzIYWSgmyTS-B9Eet1xx1wpCKiSklMPnHfFp8eQ","y":"h69bjZOopVr_7OAKCh0rzDQG_9bj38Y5YoxCIytmzkA"} diff --git a/third-party/github.com/letsencrypt/boulder/test/block-a-key/test/test.rsa.cert.pem b/third-party/github.com/letsencrypt/boulder/test/block-a-key/test/test.rsa.cert.pem deleted file mode 100644 index 502f94f99..000000000 --- a/third-party/github.com/letsencrypt/boulder/test/block-a-key/test/test.rsa.cert.pem +++ /dev/null @@ -1,16 +0,0 @@ ------BEGIN CERTIFICATE----- -MIICgTCCAWmgAwIBAgIBATANBgkqhkiG9w0BAQsFADAAMCIYDzAwMDEwMTAxMDAw -MDAwWhgPMDAwMTAxMDEwMDAwMDBaMAAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw -ggEKAoIBAQC+epImi+GdM4ypmQ7LeWSYbbX0AHeZJvRScp5+JvkVQNTIDjQGnYxw -7omOW1dkn0qGkQckFmvUmCHXuK6oF0GYOvRzEdOwb6KeTb+ONYQHGLirKU2bt+um -JxiB/9PMaV5yPwpyNVi0XV5Rr+BpHdV1i9lm542+4zwfWiYRKT1+tjpvicmyK0av -T/60U0kfeeSdAU0TcSFR4RDEw1fudXIRk7FPgd2GHjeJeAeMmLL4Vabr+uSecGpp -THdkbnPDV51WVPHcyoOV6rdicSEoqE9aoeMjQXZ6SntXGjY4pqlyuwjqocLZStEK -ztxp3D7eyeHub9nrCgp+UsxaWns1DtP3AgMBAAGjAjAAMA0GCSqGSIb3DQEBCwUA -A4IBAQA9sazSAm6umbleFWDrh3oyGaFBzYvRfeOAEquJky36qREjBWvrS2Yi66eX -L9Uoavr/CIk+U9qRPl81cHi5qsFBuDi+OKZzG32Uq7Rw8h+7f/9HVEUyVVy1p7v8 -iqZvygU70NeT0cT91eSl6LV88BdjhbjI6Hk1+AVF6UPAmzkgJIFAwwUWa2HUT+Ni -nMxzRThuLyPbYt4clz6bGzk26LIdoByJH4pYabXh05OwalBJjMVR/4ek9blrVMAg -b4a7Eq/WXq+CVwWnb3oholDOJo3l/KwNuG6HD90JU0Vu4fipFqmsXhBHYVNVu94y -wJWm+dAtEeAcp8KfOv/IBMCjDkyt ------END CERTIFICATE----- diff --git a/third-party/github.com/letsencrypt/boulder/test/block-a-key/test/test.rsa.jwk.json b/third-party/github.com/letsencrypt/boulder/test/block-a-key/test/test.rsa.jwk.json deleted file mode 100644 index 958a78ba3..000000000 --- a/third-party/github.com/letsencrypt/boulder/test/block-a-key/test/test.rsa.jwk.json +++ /dev/null @@ -1 +0,0 @@ -{"kty":"RSA","alg":"RS256","n":"vnqSJovhnTOMqZkOy3lkmG219AB3mSb0UnKefib5FUDUyA40Bp2McO6JjltXZJ9KhpEHJBZr1Jgh17iuqBdBmDr0cxHTsG-ink2_jjWEBxi4qylNm7frpicYgf_TzGlecj8KcjVYtF1eUa_gaR3VdYvZZueNvuM8H1omESk9frY6b4nJsitGr0_-tFNJH3nknQFNE3EhUeEQxMNX7nVyEZOxT4Hdhh43iXgHjJiy-FWm6_rknnBqaUx3ZG5zw1edVlTx3MqDleq3YnEhKKhPWqHjI0F2ekp7Vxo2OKapcrsI6qHC2UrRCs7cadw-3snh7m_Z6woKflLMWlp7NQ7T9w","e":"AQAB"} diff --git a/third-party/github.com/letsencrypt/boulder/test/boulder-tools/Dockerfile b/third-party/github.com/letsencrypt/boulder/test/boulder-tools/Dockerfile index 3e3680b55..569fbf58c 100644 --- a/third-party/github.com/letsencrypt/boulder/test/boulder-tools/Dockerfile +++ b/third-party/github.com/letsencrypt/boulder/test/boulder-tools/Dockerfile @@ -1,23 +1,22 @@ -FROM buildpack-deps:focal-scm as godeps +# syntax=docker/dockerfile:1 +FROM buildpack-deps:noble-scm AS godeps ARG GO_VERSION # Provided automatically by docker build. ARG TARGETPLATFORM ARG BUILDPLATFORM ENV TARGETPLATFORM=${TARGETPLATFORM:-$BUILDPLATFORM} ENV GO_VERSION=$GO_VERSION -ENV PATH /usr/local/go/bin:/usr/local/protoc/bin:$PATH -ENV GOBIN /usr/local/bin/ +ENV PATH=/usr/local/go/bin:/usr/local/protoc/bin:$PATH +ENV GOBIN=/usr/local/bin/ RUN curl "https://dl.google.com/go/go${GO_VERSION}.$(echo $TARGETPLATFORM | sed 's|\/|-|').tar.gz" |\ tar -C /usr/local -xz RUN go install github.com/rubenv/sql-migrate/sql-migrate@v1.1.2 -RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.34.1 -RUN go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@bb9882e6ae58f0a80a6390b50a5ec3bd63e46a3c -RUN go install github.com/letsencrypt/pebble/v2/cmd/pebble-challtestsrv@66511d8 -RUN go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.57.2 -RUN go install honnef.co/go/tools/cmd/staticcheck@2023.1.7 +RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.36.5 +RUN go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.5.1 +RUN go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.1.6 RUN go install github.com/jsha/minica@v1.1.0 -FROM rust:bullseye as rustdeps +FROM rust:latest AS rustdeps # Provided automatically by docker build. ARG TARGETPLATFORM ARG BUILDPLATFORM @@ -28,10 +27,10 @@ RUN /tmp/build-rust-deps.sh # When the version of Ubuntu (focal, jammy, etc) changes, ensure that the # version of libc6 is compatible with the rustdeps container above. See # https://github.com/letsencrypt/boulder/pull/7248#issuecomment-1896612920 for -# more information. +# more information. # # Run this command in each container: dpkg -l libc6 -FROM buildpack-deps:focal-scm +FROM buildpack-deps:noble-scm # Provided automatically by docker build. ARG TARGETPLATFORM ARG BUILDPLATFORM @@ -49,4 +48,4 @@ COPY --from=godeps /usr/local/bin/* /usr/local/bin/ COPY --from=godeps /usr/local/go/ /usr/local/go/ COPY --from=rustdeps /usr/local/cargo/bin/typos /usr/local/bin/typos -ENV PATH /usr/local/go/bin:/usr/local/protoc/bin:$PATH +ENV PATH=/usr/local/go/bin:/usr/local/protoc/bin:$PATH diff --git a/third-party/github.com/letsencrypt/boulder/test/boulder-tools/build.sh b/third-party/github.com/letsencrypt/boulder/test/boulder-tools/build.sh index bfa5cebd6..8e6ba8f49 100644 --- a/third-party/github.com/letsencrypt/boulder/test/boulder-tools/build.sh +++ b/third-party/github.com/letsencrypt/boulder/test/boulder-tools/build.sh @@ -4,7 +4,7 @@ apt-get update # Install system deps apt-get install -y --no-install-recommends \ - mariadb-client-core-10.3 \ + mariadb-client-core \ rsyslog \ build-essential \ opensc \ @@ -23,7 +23,7 @@ fi curl -L https://github.com/google/protobuf/releases/download/v3.20.1/protoc-3.20.1-linux-"${PROTO_ARCH}".zip -o /tmp/protoc.zip unzip /tmp/protoc.zip -d /usr/local/protoc -pip3 install -r /tmp/requirements.txt +pip3 install --break-system-packages -r /tmp/requirements.txt apt-get clean -y diff --git a/third-party/github.com/letsencrypt/boulder/test/boulder-tools/flushredis/main.go b/third-party/github.com/letsencrypt/boulder/test/boulder-tools/flushredis/main.go new file mode 100644 index 000000000..de09aebcd --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/test/boulder-tools/flushredis/main.go @@ -0,0 +1,56 @@ +package main + +import ( + "context" + "fmt" + "os" + + "github.com/letsencrypt/boulder/cmd" + blog "github.com/letsencrypt/boulder/log" + "github.com/letsencrypt/boulder/metrics" + bredis "github.com/letsencrypt/boulder/redis" + + "github.com/redis/go-redis/v9" +) + +func main() { + rc := bredis.Config{ + Username: "unittest-rw", + TLS: cmd.TLSConfig{ + CACertFile: "test/certs/ipki/minica.pem", + CertFile: "test/certs/ipki/localhost/cert.pem", + KeyFile: "test/certs/ipki/localhost/key.pem", + }, + Lookups: []cmd.ServiceDomain{ + { + Service: "redisratelimits", + Domain: "service.consul", + }, + }, + LookupDNSAuthority: "consul.service.consul", + } + rc.PasswordConfig = cmd.PasswordConfig{ + PasswordFile: "test/secrets/ratelimits_redis_password", + } + + stats := metrics.NoopRegisterer + log := blog.NewMock() + ring, err := bredis.NewRingFromConfig(rc, stats, log) + if err != nil { + fmt.Printf("while constructing ring client: %v\n", err) + os.Exit(1) + } + + err = ring.ForEachShard(context.Background(), func(ctx context.Context, shard *redis.Client) error { + cmd := shard.FlushAll(ctx) + _, err := cmd.Result() + if err != nil { + return err + } + return nil + }) + if err != nil { + fmt.Printf("while flushing redis shards: %v\n", err) + os.Exit(1) + } +} diff --git a/third-party/github.com/letsencrypt/boulder/test/boulder-tools/tag_and_upload.sh b/third-party/github.com/letsencrypt/boulder/test/boulder-tools/tag_and_upload.sh index 991b23fa5..21b24997a 100644 --- a/third-party/github.com/letsencrypt/boulder/test/boulder-tools/tag_and_upload.sh +++ b/third-party/github.com/letsencrypt/boulder/test/boulder-tools/tag_and_upload.sh @@ -12,7 +12,7 @@ DOCKER_REPO="letsencrypt/boulder-tools" # .github/workflows/release.yml, # .github/workflows/try-release.yml if appropriate, # and .github/workflows/boulder-ci.yml with the new container tag. -GO_CI_VERSIONS=( "1.22.3" ) +GO_CI_VERSIONS=( "1.24.4" ) echo "Please login to allow push to DockerHub" docker login diff --git a/third-party/github.com/letsencrypt/boulder/test/certs.go b/third-party/github.com/letsencrypt/boulder/test/certs.go index 6dd1ce5a2..25c136d89 100644 --- a/third-party/github.com/letsencrypt/boulder/test/certs.go +++ b/third-party/github.com/letsencrypt/boulder/test/certs.go @@ -1,6 +1,7 @@ package test import ( + "bytes" "crypto" "crypto/ecdsa" "crypto/elliptic" @@ -12,6 +13,7 @@ import ( "errors" "fmt" "math/big" + "net" "os" "testing" "time" @@ -71,6 +73,13 @@ func ThrowAwayCert(t *testing.T, clk clock.Clock) (string, *x509.Certificate) { _, _ = rand.Read(nameBytes[:]) name := fmt.Sprintf("%s.example.com", hex.EncodeToString(nameBytes[:])) + // Generate a random IPv6 address under the RFC 3849 space. + // https://www.rfc-editor.org/rfc/rfc3849.txt + var ipBytes [12]byte + _, _ = rand.Read(ipBytes[:]) + ipPrefix, _ := hex.DecodeString("20010db8") + ip := net.IP(bytes.Join([][]byte{ipPrefix, ipBytes[:]}, nil)) + var serialBytes [16]byte _, _ = rand.Read(serialBytes[:]) serial := big.NewInt(0).SetBytes(serialBytes[:]) @@ -81,6 +90,7 @@ func ThrowAwayCert(t *testing.T, clk clock.Clock) (string, *x509.Certificate) { template := &x509.Certificate{ SerialNumber: serial, DNSNames: []string{name}, + IPAddresses: []net.IP{ip}, NotBefore: clk.Now(), NotAfter: clk.Now().Add(6 * 24 * time.Hour), IssuingCertificateURL: []string{"http://localhost:4001/acme/issuer-cert/1234"}, diff --git a/third-party/github.com/letsencrypt/boulder/test/certs/README.md b/third-party/github.com/letsencrypt/boulder/test/certs/README.md index 8d0f8a411..8d8be7117 100644 --- a/third-party/github.com/letsencrypt/boulder/test/certs/README.md +++ b/third-party/github.com/letsencrypt/boulder/test/certs/README.md @@ -52,7 +52,6 @@ role of internal authentication between Let's Encrypt components: - The IP-address certificate used by challtestsrv (which acts as the integration test environment's recursive resolver) for DoH handshakes. -- The certificate presented by mail-test-srv's SMTP endpoint. - The certificate presented by the test redis cluster. - The certificate presented by the WFE's API TLS handler (which is usually behind some other load-balancer like nginx). diff --git a/third-party/github.com/letsencrypt/boulder/test/certs/generate.sh b/third-party/github.com/letsencrypt/boulder/test/certs/generate.sh index 0b33f8c18..f6ef272d3 100644 --- a/third-party/github.com/letsencrypt/boulder/test/certs/generate.sh +++ b/third-party/github.com/letsencrypt/boulder/test/certs/generate.sh @@ -17,11 +17,11 @@ ipki() ( mkdir ipki cd ipki - # Create a generic cert which can be used by our test-only services (like - # mail-test-srv) that aren't sophisticated enough to present a different name. - # This first invocation also creates the issuer key, so the loops below can - # run in the background without racing to create it. - minica -domains localhost + # Create a generic cert which can be used by our test-only services that + # aren't sophisticated enough to present a different name. This first + # invocation also creates the issuer key, so the loops below can run in the + # background without racing to create it. + minica -domains localhost --ip-addresses 127.0.0.1 # Used by challtestsrv to negotiate DoH handshakes. Even though we think of # challtestsrv as being external to our infrastructure (because it hosts the @@ -37,12 +37,12 @@ ipki() ( # Presented by the test redis cluster. Contains IP addresses because Boulder # components find individual redis servers via SRV records. - minica -domains redis -ip-addresses 10.33.33.2,10.33.33.3,10.33.33.4,10.33.33.5,10.33.33.6,10.33.33.7,10.33.33.8,10.33.33.9 + minica -domains redis -ip-addresses 10.77.77.2,10.77.77.3,10.77.77.4,10.77.77.5 # Used by Boulder gRPC services as both server and client mTLS certificates. - for SERVICE in admin-revoker expiration-mailer ocsp-responder consul \ + for SERVICE in admin ocsp-responder consul \ wfe akamai-purger bad-key-revoker crl-updater crl-storer \ - health-checker rocsp-tool; do + health-checker rocsp-tool sfe email-exporter; do minica -domains "${SERVICE}.boulder" & done @@ -63,6 +63,7 @@ webpki() ( # This function executes in a subshell, so this cd does not affect the parent # script. cd ../.. + make build mkdir ./test/certs/webpki go run ./test/certs/webpki.go ) diff --git a/third-party/github.com/letsencrypt/boulder/test/certs/webpki.go b/third-party/github.com/letsencrypt/boulder/test/certs/webpki.go index 759c11694..a0d6c7f30 100644 --- a/third-party/github.com/letsencrypt/boulder/test/certs/webpki.go +++ b/third-party/github.com/letsencrypt/boulder/test/certs/webpki.go @@ -81,10 +81,6 @@ func main() { _ = blog.Set(blog.StdoutLogger(6)) defer cmd.AuditPanic() - // Compile the ceremony binary for easy re-use. - _, err := exec.Command("make", "build").CombinedOutput() - cmd.FailOnError(err, "compiling ceremony tool") - // Create SoftHSM slots for the root signing keys rsaRootKeySlot, err := createSlot("Root RSA") cmd.FailOnError(err, "failed creating softhsm2 slot for RSA root key") diff --git a/third-party/github.com/letsencrypt/boulder/test/chall-test-srv-client/client.go b/third-party/github.com/letsencrypt/boulder/test/chall-test-srv-client/client.go new file mode 100644 index 000000000..84a327570 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/test/chall-test-srv-client/client.go @@ -0,0 +1,519 @@ +package challtestsrvclient + +import ( + "bytes" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" +) + +// Client is an HTTP client for https://github.com/letsencrypt/challtestsrv's +// management interface (test/chall-test-srv). +type Client struct { + baseURL string +} + +// NewClient creates a new Client using the provided baseURL, or defaults to +// http://10.77.77.77:8055 if none is provided. +func NewClient(baseURL string) *Client { + if baseURL == "" { + baseURL = "http://10.77.77.77:8055" + } + return &Client{baseURL: baseURL} +} + +const ( + setIPv4 = "set-default-ipv4" + setIPv6 = "set-default-ipv6" + delHistory = "clear-request-history" + getHTTPHistory = "http-request-history" + getDNSHistory = "dns-request-history" + getALPNHistory = "tlsalpn01-request-history" + addA = "add-a" + delA = "clear-a" + addAAAA = "add-aaaa" + delAAAA = "clear-aaaa" + addCAA = "add-caa" + delCAA = "clear-caa" + addRedirect = "add-redirect" + delRedirect = "del-redirect" + addHTTP = "add-http01" + delHTTP = "del-http01" + addTXT = "set-txt" + delTXT = "clear-txt" + addALPN = "add-tlsalpn01" + delALPN = "del-tlsalpn01" + addServfail = "set-servfail" + delServfail = "clear-servfail" +) + +func (c *Client) postURL(path string, body interface{}) ([]byte, error) { + endpoint, err := url.JoinPath(c.baseURL, path) + if err != nil { + return nil, fmt.Errorf("joining URL %q with path %q: %w", c.baseURL, path, err) + } + + payload, err := json.Marshal(body) + if err != nil { + return nil, fmt.Errorf("marshalling payload for %s: %w", endpoint, err) + } + + resp, err := http.Post(endpoint, "application/json", bytes.NewBuffer(payload)) + if err != nil { + return nil, fmt.Errorf("sending POST to %s: %w", endpoint, err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code %d from %s", resp.StatusCode, endpoint) + } + respBytes, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("reading response from %s: %w", endpoint, err) + } + return respBytes, nil +} + +// SetDefaultIPv4 sets the challenge server's default IPv4 address used to +// respond to A queries when there are no specific mock A addresses for the +// hostname being queried. Provide an empty string as the default address to +// disable answering A queries except for hosts that have mock A addresses +// added. Any failure returns an error that includes both the relevant operation +// and the payload. +func (c *Client) SetDefaultIPv4(addr string) ([]byte, error) { + payload := map[string]string{"ip": addr} + resp, err := c.postURL(setIPv4, payload) + if err != nil { + return nil, fmt.Errorf( + "while setting default IPv4 to %q (payload: %v): %w", + addr, payload, err, + ) + } + return resp, nil +} + +// SetDefaultIPv6 sets the challenge server's default IPv6 address used to +// respond to AAAA queries when there are no specific mock AAAA addresses for +// the hostname being queried. Provide an empty string as the default address to +// disable answering AAAA queries except for hosts that have mock AAAA addresses +// added. Any failure returns an error that includes both the relevant operation +// and the payload. +func (c *Client) SetDefaultIPv6(addr string) ([]byte, error) { + payload := map[string]string{"ip": addr} + resp, err := c.postURL(setIPv6, payload) + if err != nil { + return nil, fmt.Errorf( + "while setting default IPv6 to %q (payload: %v): %w", + addr, payload, err, + ) + } + return resp, nil +} + +// AddARecord adds a mock A response to the challenge server's DNS interface for +// the given host and IPv4 addresses. Any failure returns an error that includes +// both the relevant operation and the payload. +func (c *Client) AddARecord(host string, addresses []string) ([]byte, error) { + payload := map[string]interface{}{ + "host": host, + "addresses": addresses, + } + resp, err := c.postURL(addA, payload) + if err != nil { + return nil, fmt.Errorf( + "while adding A record for host %q (payload: %v): %w", + host, payload, err, + ) + } + return resp, nil +} + +// RemoveARecord removes a mock A response from the challenge server's DNS +// interface for the given host. Any failure returns an error that includes both +// the relevant operation and the payload. +func (c *Client) RemoveARecord(host string) ([]byte, error) { + payload := map[string]string{"host": host} + resp, err := c.postURL(delA, payload) + if err != nil { + return nil, fmt.Errorf( + "while removing A record for host %q (payload: %v): %w", + host, payload, err, + ) + } + return resp, nil +} + +// AddAAAARecord adds a mock AAAA response to the challenge server's DNS +// interface for the given host and IPv6 addresses. Any failure returns an error +// that includes both the relevant operation and the payload. +func (c *Client) AddAAAARecord(host string, addresses []string) ([]byte, error) { + payload := map[string]interface{}{ + "host": host, + "addresses": addresses, + } + resp, err := c.postURL(addAAAA, payload) + if err != nil { + return nil, fmt.Errorf( + "while adding AAAA record for host %q (payload: %v): %w", + host, payload, err, + ) + } + return resp, nil +} + +// RemoveAAAARecord removes mock AAAA response from the challenge server's DNS +// interface for the given host. Any failure returns an error that includes both +// the relevant operation and the payload. +func (c *Client) RemoveAAAARecord(host string) ([]byte, error) { + payload := map[string]string{"host": host} + resp, err := c.postURL(delAAAA, payload) + if err != nil { + return nil, fmt.Errorf( + "while removing AAAA record for host %q (payload: %v): %w", + host, payload, err, + ) + } + return resp, nil +} + +// AddCAAIssue adds a mock CAA response to the challenge server's DNS interface. +// The mock CAA response will contain one policy with an "issue" tag specifying +// the provided value. Any failure returns an error that includes both the +// relevant operation and the payload. +func (c *Client) AddCAAIssue(host, value string) ([]byte, error) { + payload := map[string]interface{}{ + "host": host, + "policies": []map[string]string{ + {"tag": "issue", "value": value}, + }, + } + resp, err := c.postURL(addCAA, payload) + if err != nil { + return nil, fmt.Errorf( + "while adding CAA issue for host %q, val %q (payload: %v): %w", + host, value, payload, err, + ) + } + return resp, nil +} + +// RemoveCAAIssue removes a mock CAA response from the challenge server's DNS +// interface for the given host. Any failure returns an error that includes both +// the relevant operation and the payload. +func (c *Client) RemoveCAAIssue(host string) ([]byte, error) { + payload := map[string]string{"host": host} + resp, err := c.postURL(delCAA, payload) + if err != nil { + return nil, fmt.Errorf( + "while removing CAA issue for host %q (payload: %v): %w", + host, payload, err, + ) + } + return resp, nil +} + +// HTTPRequest is a single HTTP request in the request history. +type HTTPRequest struct { + URL string `json:"URL"` + Host string `json:"Host"` + HTTPS bool `json:"HTTPS"` + ServerName string `json:"ServerName"` + UserAgent string `json:"UserAgent"` +} + +// HTTPRequestHistory fetches the challenge server's HTTP request history for +// the given host. +func (c *Client) HTTPRequestHistory(host string) ([]HTTPRequest, error) { + payload := map[string]string{"host": host} + raw, err := c.postURL(getHTTPHistory, payload) + if err != nil { + return nil, fmt.Errorf( + "while fetching HTTP request history for host %q (payload: %v): %w", + host, payload, err, + ) + } + var data []HTTPRequest + err = json.Unmarshal(raw, &data) + if err != nil { + return nil, fmt.Errorf("unmarshalling HTTP request history: %w", err) + } + return data, nil +} + +func (c *Client) clearRequestHistory(host, typ string) ([]byte, error) { + return c.postURL(delHistory, map[string]string{"host": host, "type": typ}) +} + +// ClearHTTPRequestHistory clears the challenge server's HTTP request history +// for the given host. Any failure returns an error that includes both the +// relevant operation and the payload. +func (c *Client) ClearHTTPRequestHistory(host string) ([]byte, error) { + resp, err := c.clearRequestHistory(host, "http") + if err != nil { + return nil, fmt.Errorf( + "while clearing HTTP request history for host %q: %w", host, err, + ) + } + return resp, nil +} + +// AddHTTPRedirect adds a redirect to the challenge server's HTTP interfaces for +// HTTP requests to the given path directing the client to the targetURL. +// Redirects are not served for HTTPS requests. Any failure returns an error +// that includes both the relevant operation and the payload. +func (c *Client) AddHTTPRedirect(path, targetURL string) ([]byte, error) { + payload := map[string]string{"path": path, "targetURL": targetURL} + resp, err := c.postURL(addRedirect, payload) + if err != nil { + return nil, fmt.Errorf( + "while adding HTTP redirect for path %q -> %q (payload: %v): %w", + path, targetURL, payload, err, + ) + } + return resp, nil +} + +// RemoveHTTPRedirect removes a redirect from the challenge server's HTTP +// interfaces for the given path. Any failure returns an error that includes +// both the relevant operation and the payload. +func (c *Client) RemoveHTTPRedirect(path string) ([]byte, error) { + payload := map[string]string{"path": path} + resp, err := c.postURL(delRedirect, payload) + if err != nil { + return nil, fmt.Errorf( + "while removing HTTP redirect for path %q (payload: %v): %w", + path, payload, err, + ) + } + return resp, nil +} + +// AddHTTP01Response adds an ACME HTTP-01 challenge response for the provided +// token under the /.well-known/acme-challenge/ path of the challenge test +// server's HTTP interfaces. The given keyauth will be returned as the HTTP +// response body for requests to the challenge token. Any failure returns an +// error that includes both the relevant operation and the payload. +func (c *Client) AddHTTP01Response(token, keyauth string) ([]byte, error) { + payload := map[string]string{"token": token, "content": keyauth} + resp, err := c.postURL(addHTTP, payload) + if err != nil { + return nil, fmt.Errorf( + "while adding HTTP-01 challenge response for token %q (payload: %v): %w", + token, payload, err, + ) + } + return resp, nil +} + +// RemoveHTTP01Response removes an ACME HTTP-01 challenge response for the +// provided token from the challenge test server. Any failure returns an error +// that includes both the relevant operation and the payload. +func (c *Client) RemoveHTTP01Response(token string) ([]byte, error) { + payload := map[string]string{"token": token} + resp, err := c.postURL(delHTTP, payload) + if err != nil { + return nil, fmt.Errorf( + "while removing HTTP-01 challenge response for token %q (payload: %v): %w", + token, payload, err, + ) + } + return resp, nil +} + +// AddServfailResponse configures the challenge test server to return SERVFAIL +// for all queries made for the provided host. This will override any other +// mocks for the host until removed with remove_servfail_response. Any failure +// returns an error that includes both the relevant operation and the payload. +func (c *Client) AddServfailResponse(host string) ([]byte, error) { + payload := map[string]string{"host": host} + resp, err := c.postURL(addServfail, payload) + if err != nil { + return nil, fmt.Errorf( + "while adding SERVFAIL response for host %q (payload: %v): %w", + host, payload, err, + ) + } + return resp, nil +} + +// RemoveServfailResponse undoes the work of AddServfailResponse, removing the +// SERVFAIL configuration for the given host. Any failure returns an error that +// includes both the relevant operation and the payload. +func (c *Client) RemoveServfailResponse(host string) ([]byte, error) { + payload := map[string]string{"host": host} + resp, err := c.postURL(delServfail, payload) + if err != nil { + return nil, fmt.Errorf( + "while removing SERVFAIL response for host %q (payload: %v): %w", + host, payload, err, + ) + } + return resp, nil +} + +// AddDNS01Response adds an ACME DNS-01 challenge response for the provided host +// to the challenge test server's DNS interfaces. The value is hashed and +// base64-encoded using RawURLEncoding, and served for TXT queries to +// _acme-challenge.. Any failure returns an error that includes both the +// relevant operation and the payload. +func (c *Client) AddDNS01Response(host, value string) ([]byte, error) { + host = "_acme-challenge." + host + if !strings.HasSuffix(host, ".") { + host += "." + } + h := sha256.Sum256([]byte(value)) + value = base64.RawURLEncoding.EncodeToString(h[:]) + payload := map[string]string{"host": host, "value": value} + resp, err := c.postURL(addTXT, payload) + if err != nil { + return nil, fmt.Errorf( + "while adding DNS-01 response for host %q, val %q (payload: %v): %w", + host, value, payload, err, + ) + } + return resp, nil +} + +// RemoveDNS01Response removes an ACME DNS-01 challenge response for the +// provided host from the challenge test server's DNS interfaces. Any failure +// returns an error that includes both the relevant operation and the payload. +func (c *Client) RemoveDNS01Response(host string) ([]byte, error) { + if !strings.HasPrefix(host, "_acme-challenge.") { + host = "_acme-challenge." + host + } + if !strings.HasSuffix(host, ".") { + host += "." + } + payload := map[string]string{"host": host} + resp, err := c.postURL(delTXT, payload) + if err != nil { + return nil, fmt.Errorf( + "while removing DNS-01 response for host %q (payload: %v): %w", + host, payload, err, + ) + } + return resp, nil +} + +// DNSRequest is a single DNS request in the request history. +type DNSRequest struct { + Question struct { + Name string `json:"Name"` + Qtype uint16 `json:"Qtype"` + Qclass uint16 `json:"Qclass"` + } `json:"Question"` + UserAgent string `json:"UserAgent"` +} + +// DNSRequestHistory returns the history of DNS requests made to the challenge +// test server's DNS interfaces for the given host. Any failure returns an error +// that includes both the relevant operation and the payload. +func (c *Client) DNSRequestHistory(host string) ([]DNSRequest, error) { + payload := map[string]string{"host": host} + raw, err := c.postURL(getDNSHistory, payload) + if err != nil { + return nil, fmt.Errorf( + "while fetching DNS request history for host %q (payload: %v): %w", + host, payload, err, + ) + } + var data []DNSRequest + err = json.Unmarshal(raw, &data) + if err != nil { + return nil, fmt.Errorf("unmarshalling DNS request history: %w", err) + } + return data, nil +} + +// ClearDNSRequestHistory clears the history of DNS requests made to the +// challenge test server's DNS interfaces for the given host. Any failure +// returns an error that includes both the relevant operation and the payload. +func (c *Client) ClearDNSRequestHistory(host string) ([]byte, error) { + resp, err := c.clearRequestHistory(host, "dns") + if err != nil { + return nil, fmt.Errorf( + "while clearing DNS request history for host %q: %w", host, err, + ) + } + return resp, nil +} + +// TLSALPN01Request is a single TLS-ALPN-01 request in the request history. +type TLSALPN01Request struct { + ServerName string `json:"ServerName"` + SupportedProtos []string `json:"SupportedProtos"` +} + +// AddTLSALPN01Response adds an ACME TLS-ALPN-01 challenge response certificate +// to the challenge test server's TLS-ALPN-01 interface for the given host. The +// provided key authorization value will be embedded in the response certificate +// served to clients that initiate a TLS-ALPN-01 challenge validation with the +// challenge test server for the provided host. Any failure returns an error +// that includes both the relevant operation and the payload. +func (c *Client) AddTLSALPN01Response(host, value string) ([]byte, error) { + payload := map[string]string{"host": host, "content": value} + resp, err := c.postURL(addALPN, payload) + if err != nil { + return nil, fmt.Errorf( + "while adding TLS-ALPN-01 response for host %q, val %q (payload: %v): %w", + host, value, payload, err, + ) + } + return resp, nil +} + +// RemoveTLSALPN01Response removes an ACME TLS-ALPN-01 challenge response +// certificate from the challenge test server's TLS-ALPN-01 interface for the +// given host. Any failure returns an error that includes both the relevant +// operation and the payload. +func (c *Client) RemoveTLSALPN01Response(host string) ([]byte, error) { + payload := map[string]string{"host": host} + resp, err := c.postURL(delALPN, payload) + if err != nil { + return nil, fmt.Errorf( + "while removing TLS-ALPN-01 response for host %q (payload: %v): %w", + host, payload, err, + ) + } + return resp, nil +} + +// TLSALPN01RequestHistory returns the history of TLS-ALPN-01 requests made to +// the challenge test server's TLS-ALPN-01 interface for the given host. Any +// failure returns an error that includes both the relevant operation and the +// payload. +func (c *Client) TLSALPN01RequestHistory(host string) ([]TLSALPN01Request, error) { + payload := map[string]string{"host": host} + raw, err := c.postURL(getALPNHistory, payload) + if err != nil { + return nil, fmt.Errorf( + "while fetching TLS-ALPN-01 request history for host %q (payload: %v): %w", + host, payload, err, + ) + } + var data []TLSALPN01Request + err = json.Unmarshal(raw, &data) + if err != nil { + return nil, fmt.Errorf("unmarshalling TLS-ALPN-01 request history: %w", err) + } + return data, nil +} + +// ClearTLSALPN01RequestHistory clears the history of TLS-ALPN-01 requests made +// to the challenge test server's TLS-ALPN-01 interface for the given host. Any +// failure returns an error that includes both the relevant operation and the +// payload. +func (c *Client) ClearTLSALPN01RequestHistory(host string) ([]byte, error) { + resp, err := c.clearRequestHistory(host, "tlsalpn") + if err != nil { + return nil, fmt.Errorf( + "while clearing TLS-ALPN-01 request history for host %q: %w", host, err, + ) + } + return resp, nil +} diff --git a/third-party/github.com/letsencrypt/boulder/test/chall-test-srv/README.md b/third-party/github.com/letsencrypt/boulder/test/chall-test-srv/README.md new file mode 100644 index 000000000..3d137ffbe --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/test/chall-test-srv/README.md @@ -0,0 +1,237 @@ +# Boulder Challenge Test Server + +**Important note: The `chall-test-srv` command is for TEST USAGE ONLY. It +is trivially insecure, offering no authentication. Only use +`chall-test-srv` in a controlled test environment.** + +The standalone `chall-test-srv` binary lets you run HTTP-01, HTTPS HTTP-01, +DNS-01, and TLS-ALPN-01 challenge servers that external programs can add/remove +challenge responses to using a HTTP management API. + +For example this is used by the Boulder integration tests to easily add/remove +TXT records for DNS-01 challenges for the `chisel.py` ACME client, and to test +redirect behaviour for HTTP-01 challenge validation. + +### Usage + +``` +Usage of chall-test-srv: + -defaultIPv4 string + Default IPv4 address for mock DNS responses to A queries (default "127.0.0.1") + -defaultIPv6 string + Default IPv6 address for mock DNS responses to AAAA queries (default "::1") + -dns01 string + Comma separated bind addresses/ports for DNS-01 challenges and fake DNS data. Set empty to disable. (default ":8053") + -http01 string + Comma separated bind addresses/ports for HTTP-01 challenges. Set empty to disable. (default ":5002") + -https01 string + Comma separated bind addresses/ports for HTTPS HTTP-01 challenges. Set empty to disable. (default ":5003") + -management string + Bind address/port for management HTTP interface (default ":8055") + -tlsalpn01 string + Comma separated bind addresses/ports for TLS-ALPN-01 and HTTPS HTTP-01 challenges. Set empty to disable. (default ":5001") +``` + +To disable a challenge type, set the bind address to `""`. E.g.: + +* To run HTTP-01 only: `chall-test-srv -https01 "" -dns01 "" -tlsalpn01 ""` +* To run HTTPS-01 only: `chall-test-srv -http01 "" -dns01 "" -tlsalpn01 ""` +* To run DNS-01 only: `chall-test-srv -http01 "" -https01 "" -tlsalpn01 ""` +* To run TLS-ALPN-01 only: `chall-test-srv -http01 "" -https01 "" -dns01 ""` + +### Management Interface + +_Note: These examples assume the default `-management` interface address, `:8055`._ + +#### Mock DNS + +##### Default A/AAAA Responses + +You can set the default IPv4 and IPv6 addresses used for `A` and `AAAA` query +responses using the `-defaultIPv4` and `-defaultIPv6` command line flags. + +To change the default IPv4 address used for responses to `A` queries that do not +match explicit mocks at runtime run: + + curl -d '{"ip":"10.10.10.2"}' http://localhost:8055/set-default-ipv4 + +Similarly to change the default IPv6 address used for responses to `AAAA` queries +that do not match explicit mocks run: + + curl -d '{"ip":"::1"}' http://localhost:8055/set-default-ipv6 + +To clear the default IPv4 or IPv6 address POST the same endpoints with an empty +(`""`) IP. + +##### Mocked A/AAAA Responses + +To add IPv4 addresses to be returned for `A` queries for +`test-host.letsencrypt.org` run: + + curl -d '{"host":"test-host.letsencrypt.org", "addresses":["12.12.12.12", "13.13.13.13"]}' http://localhost:8055/add-a + +The mocked `A` responses can be removed by running: + + curl -d '{"host":"test-host.letsencrypt.org"}' http://localhost:8055/clear-a + +To add IPv6 addresses to be returned for `AAAA` queries for +`test-host.letsencrypt.org` run: + + curl -d '{"host":"test-host.letsencrypt.org", "addresses":["2001:4860:4860::8888", "2001:4860:4860::8844"]}' http://localhost:8055/add-aaaa + +The mocked `AAAA` responses can be removed by running: + + curl -d '{"host":"test-host.letsencrypt.org"}' http://localhost:8055/clear-aaaa + +##### Mocked CAA Responses + +To add a mocked CAA policy for `test-host.letsencrypt.org` that allows issuance +by `letsencrypt.org` run: + + curl -d '{"host":"test-host.letsencrypt.org", "policies":[{"tag":"issue","value":"letsencrypt.org"}]}' http://localhost:8055/add-caa + +To remove the mocked CAA policy for `test-host.letsencrypt.org` run: + + curl -d '{"host":"test-host.letsencrypt.org"}' http://localhost:8055/clear-caa + +##### Mocked CNAME Responses + +To add a mocked CNAME record for `_acme-challenge.test-host.letsencrypt.org` run: + + curl -d '{"host":"_acme-challenge.test-host.letsencrypt.org", "target": "challenges.letsencrypt.org"}' http://localhost:8055/set-cname + +To remove a mocked CNAME record for `_acme-challenge.test-host.letsencrypt.org` run: + + curl -d '{"host":"_acme-challenge.test-host.letsencrypt.org", "target": "challenges.letsencrypt.org"}' http://localhost:8055/clear-cname + +##### Mocked SERVFAIL Responses + +To configure the DNS server to return SERVFAIL for all queries for `test-host.letsencrypt.org` run: + + curl -d '{"host":"test-host.letsencrypt.org"}' http://localhost:8055/set-servfail + +Subsequently any query types (A, AAAA, TXT) for the name will return a SERVFAIL response, overriding any A/AAAA/TXT/CNAME mocks that may also be configured. + +To remove the SERVFAIL configuration for `test-host.letsencrypt.org` run: + + curl -d '{"host":"test-host.letsencrypt.org"}' http://localhost:8055/clear-servfail + +#### HTTP-01 + +To add an HTTP-01 challenge response for the token `"aaaa"` with the content `"bbbb"` run: + + curl -d '{"token":"aaaa", "content":"bbbb"}' http://localhost:8055/add-http01 + +Afterwards the challenge response will be available over HTTP at +`http://localhost:5002/.well-known/acme-challenge/aaaa`, and HTTPS at +`https://localhost:5002/.well-known/acme-challenge/aaaa`. + +The HTTP-01 challenge response for the `"aaaa"` token can be deleted by running: + + curl -d '{"token":"aaaa"}' http://localhost:8055/del-http01 + +##### Redirects + +To add a redirect from `/.well-known/acme-challenge/whatever` to +`https://localhost:5003/ok` run: + + curl -d '{"path":"/.well-known/whatever", "targetURL": "https://localhost:5003/ok"}' http://localhost:8055/add-redirect + +Afterwards HTTP requests to `http://localhost:5002/.well-known/whatever/` will +be redirected to `https://localhost:5003/ok`. HTTPS requests that match the +path will not be served a redirect to prevent loops when redirecting the same +path from HTTP to HTTPS. + +To remove the redirect run: + + curl -d '{"path":"/.well-known/whatever"}' http://localhost:8055/del-redirect + +#### DNS-01 + +To add a DNS-01 challenge response for `_acme-challenge.test-host.letsencrypt.org` with +the value `"foo"` run: + + curl -d '{"host":"_acme-challenge.test-host.letsencrypt.org.", "value": "foo"}' http://localhost:8055/set-txt + +To remove the mocked DNS-01 challenge response run: + + curl -d '{"host":"_acme-challenge.test-host.letsencrypt.org."}' http://localhost:8055/clear-txt + +Note that a period character is required at the end of the host name here. + +#### TLS-ALPN-01 + +To add a TLS-ALPN-01 challenge response certificate for the host +`test-host.letsencrypt.org` with the key authorization `"foo"` run: + + curl -d '{"host":"test-host.letsencrypt.org", "content":"foo"}' http://localhost:8055/add-tlsalpn01 + +To remove the mocked TLS-ALPN-01 challenge response run: + + curl -d '{"host":"test-host.letsencrypt.org"}' http://localhost:8055/del-tlsalpn01 + +#### Request History + +`chall-test-srv` keeps track of the requests processed by each of the +challenge servers and exposes this information via JSON. + +To get the history of HTTP requests to `example.com` run: + + curl -d '{"host":"example.com"}' http://localhost:8055/http-request-history + +Each HTTP request event is an object of the form: +``` + { + "URL": "/test-whatever/dude?token=blah", + "Host": "example.com", + "HTTPS": true, + "ServerName": "example-sni.com" + } +``` +If the HTTP request was over the HTTPS interface then HTTPS will be true and the +ServerName field will be populated with the SNI value sent by the client in the +initial TLS hello. + +To get the history of DNS requests for `example.com` run: + + curl -d '{"host":"example.com"}' http://localhost:8055/dns-request-history + +Each DNS request event is an object of the form: +``` + { + "Question": { + "Name": "example.com.", + "Qtype": 257, + "Qclass": 1 + } + } +``` + +To get the history of TLS-ALPN-01 requests for the SNI host `example.com` run: + + curl -d '{"host":"example.com"}' http://localhost:8055/tlsalpn01-request-history + +Each TLS-ALPN-01 request event is an object of the form: +``` + { + "ServerName": "example.com", + "SupportedProtos": [ + "dogzrule" + ] + } +``` +The ServerName field is populated with the SNI value sent by the client in the +initial TLS hello. The SupportedProtos field is set with the advertised +supported next protocols from the initial TLS hello. + +To clear HTTP request history for `example.com` run: + + curl -d '{"host":"example.com", "type":"http"}' http://localhost:8055/clear-request-history + +Similarly, to clear DNS request history for `example.com` run: + + curl -d '{"host":"example.com", "type":"dns"}' http://localhost:8055/clear-request-history + +And to clear TLS-ALPN-01 request history for `example.com` run: + + curl -d '{"host":"example.com", "type":"tlsalpn"}' http://localhost:8055/clear-request-history diff --git a/third-party/github.com/letsencrypt/boulder/test/chall-test-srv/dnsone.go b/third-party/github.com/letsencrypt/boulder/test/chall-test-srv/dnsone.go new file mode 100644 index 000000000..fa077cbb2 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/test/chall-test-srv/dnsone.go @@ -0,0 +1,65 @@ +package main + +import "net/http" + +// addDNS01 handles an HTTP POST request to add a new DNS-01 challenge TXT +// record for a given host/value. +// +// The POST body is expected to have two non-empty parameters: +// "host" - the hostname to add the mock TXT response under. +// "value" - the key authorization value to return in the TXT response. +// +// A successful POST will write http.StatusOK to the client. +func (srv *managementServer) addDNS01(w http.ResponseWriter, r *http.Request) { + // Unmarshal the request body JSON as a request object + var request struct { + Host string + Value string + } + if err := mustParsePOST(&request, r); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // If the request has an empty host or value it's a bad request + if request.Host == "" || request.Value == "" { + w.WriteHeader(http.StatusBadRequest) + return + } + + // Add the DNS-01 challenge response TXT to the challenge server + srv.challSrv.AddDNSOneChallenge(request.Host, request.Value) + srv.log.Printf("Added DNS-01 TXT challenge for Host %q - Value %q\n", + request.Host, request.Value) + w.WriteHeader(http.StatusOK) +} + +// delDNS01 handles an HTTP POST request to delete an existing DNS-01 challenge +// TXT record for a given host. +// +// The POST body is expected to have one non-empty parameter: +// "host" - the hostname to remove the mock TXT response for. +// +// A successful POST will write http.StatusOK to the client. +func (srv *managementServer) delDNS01(w http.ResponseWriter, r *http.Request) { + // Unmarshal the request body JSON as a request object + var request struct { + Host string + } + if err := mustParsePOST(&request, r); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // If the request has an empty host value it's a bad request + if request.Host == "" { + w.WriteHeader(http.StatusBadRequest) + return + } + + // Delete the DNS-01 challenge response TXT for the given host from the + // challenge server + srv.challSrv.DeleteDNSOneChallenge(request.Host) + srv.log.Printf("Removed DNS-01 TXT challenge for Host %q\n", request.Host) + w.WriteHeader(http.StatusOK) +} diff --git a/third-party/github.com/letsencrypt/boulder/test/chall-test-srv/history.go b/third-party/github.com/letsencrypt/boulder/test/chall-test-srv/history.go new file mode 100644 index 000000000..b03f9f524 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/test/chall-test-srv/history.go @@ -0,0 +1,122 @@ +package main + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + + "github.com/letsencrypt/challtestsrv" +) + +// clearHistory handles an HTTP POST request to clear the challenge server +// request history for a specific hostname and type of event. +// +// The POST body is expected to have two parameters: +// "host" - the hostname to clear history for. +// "type" - the type of event to clear. May be "http", "dns", or "tlsalpn". +// +// A successful POST will write http.StatusOK to the client. +func (srv *managementServer) clearHistory(w http.ResponseWriter, r *http.Request) { + var request struct { + Host string + Type string `json:"type"` + } + if err := mustParsePOST(&request, r); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + typeMap := map[string]challtestsrv.RequestEventType{ + "http": challtestsrv.HTTPRequestEventType, + "dns": challtestsrv.DNSRequestEventType, + "tlsalpn": challtestsrv.TLSALPNRequestEventType, + } + if request.Host == "" { + http.Error(w, "host parameter must not be empty", http.StatusBadRequest) + return + } + if code, ok := typeMap[request.Type]; ok { + srv.challSrv.ClearRequestHistory(request.Host, code) + srv.log.Printf("Cleared challenge server request history for %q %q events\n", + request.Host, request.Type) + w.WriteHeader(http.StatusOK) + return + } + + http.Error(w, fmt.Sprintf("%q event type unknown", request.Type), http.StatusBadRequest) +} + +// getHTTPHistory returns only the HTTPRequestEvents for the given hostname +// from the challenge server's request history in JSON form. +func (srv *managementServer) getHTTPHistory(w http.ResponseWriter, r *http.Request) { + host, err := requestHost(r) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + srv.writeHistory( + srv.challSrv.RequestHistory(host, challtestsrv.HTTPRequestEventType), + w) +} + +// getDNSHistory returns only the DNSRequestEvents from the challenge +// server's request history in JSON form. +func (srv *managementServer) getDNSHistory(w http.ResponseWriter, r *http.Request) { + host, err := requestHost(r) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + srv.writeHistory( + srv.challSrv.RequestHistory(host, challtestsrv.DNSRequestEventType), + w) +} + +// getTLSALPNHistory returns only the TLSALPNRequestEvents from the challenge +// server's request history in JSON form. +func (srv *managementServer) getTLSALPNHistory(w http.ResponseWriter, r *http.Request) { + host, err := requestHost(r) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + srv.writeHistory( + srv.challSrv.RequestHistory(host, challtestsrv.TLSALPNRequestEventType), + w) +} + +// requestHost extracts the Host parameter of a JSON POST body in the provided +// request, or returns an error. +func requestHost(r *http.Request) (string, error) { + var request struct { + Host string + } + if err := mustParsePOST(&request, r); err != nil { + return "", err + } + if request.Host == "" { + return "", errors.New("host parameter of POST body must not be empty") + } + return request.Host, nil +} + +// writeHistory writes the provided list of challtestsrv.RequestEvents to the +// provided http.ResponseWriter in JSON form. +func (srv *managementServer) writeHistory( + history []challtestsrv.RequestEvent, w http.ResponseWriter, +) { + // Always write an empty JSON list instead of `null` + if history == nil { + history = []challtestsrv.RequestEvent{} + } + jsonHistory, err := json.MarshalIndent(history, "", " ") + if err != nil { + srv.log.Printf("Error marshaling history: %v\n", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(http.StatusOK) + _, _ = w.Write(jsonHistory) +} diff --git a/third-party/github.com/letsencrypt/boulder/test/chall-test-srv/http.go b/third-party/github.com/letsencrypt/boulder/test/chall-test-srv/http.go new file mode 100644 index 000000000..0bd28bfd3 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/test/chall-test-srv/http.go @@ -0,0 +1,24 @@ +package main + +import ( + "encoding/json" + "errors" + "io" + "net/http" +) + +// mustParsePOST will attempt to read a JSON POST body from the provided request +// and unmarshal it into the provided ob. If an error occurs at any point it +// will be returned. +func mustParsePOST(ob interface{}, request *http.Request) error { + jsonBody, err := io.ReadAll(request.Body) + if err != nil { + return err + } + + if string(jsonBody) == "" { + return errors.New("Expected JSON POST body, was empty") + } + + return json.Unmarshal(jsonBody, ob) +} diff --git a/third-party/github.com/letsencrypt/boulder/test/chall-test-srv/httpone.go b/third-party/github.com/letsencrypt/boulder/test/chall-test-srv/httpone.go new file mode 100644 index 000000000..924e2b08b --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/test/chall-test-srv/httpone.go @@ -0,0 +1,128 @@ +package main + +import "net/http" + +// addHTTP01 handles an HTTP POST request to add a new HTTP-01 challenge +// response for a given token. +// +// The POST body is expected to have two non-empty parameters: +// "token" - the HTTP-01 challenge token to add the mock HTTP-01 response under +// in the `/.well-known/acme-challenge/` path. +// +// "content" - the key authorization value to return in the HTTP response. +// +// A successful POST will write http.StatusOK to the client. +func (srv *managementServer) addHTTP01(w http.ResponseWriter, r *http.Request) { + // Unmarshal the request body JSON as a request object + var request struct { + Token string + Content string + } + if err := mustParsePOST(&request, r); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // If the request has an empty token or content it's a bad request + if request.Token == "" || request.Content == "" { + w.WriteHeader(http.StatusBadRequest) + return + } + + // Add the HTTP-01 challenge to the challenge server + srv.challSrv.AddHTTPOneChallenge(request.Token, request.Content) + srv.log.Printf("Added HTTP-01 challenge for token %q - key auth %q\n", + request.Token, request.Content) + w.WriteHeader(http.StatusOK) +} + +// delHTTP01 handles an HTTP POST request to delete an existing HTTP-01 +// challenge response for a given token. +// +// The POST body is expected to have one non-empty parameter: +// "token" - the HTTP-01 challenge token to remove the mock HTTP-01 response +// from. +// +// A successful POST will write http.StatusOK to the client. +func (srv *managementServer) delHTTP01(w http.ResponseWriter, r *http.Request) { + // Unmarshal the request body JSON as a request object + var request struct { + Token string + } + if err := mustParsePOST(&request, r); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // If the request has an empty token it's a bad request + if request.Token == "" { + w.WriteHeader(http.StatusBadRequest) + return + } + + // Delete the HTTP-01 challenge for the given token from the challenge server + srv.challSrv.DeleteHTTPOneChallenge(request.Token) + srv.log.Printf("Removed HTTP-01 challenge for token %q\n", request.Token) + w.WriteHeader(http.StatusOK) +} + +// addHTTPRedirect handles an HTTP POST request to add a new 301 redirect to be +// served for the given path to the given target URL. +// +// The POST body is expected to have two non-empty parameters: +// "path" - the path that when matched in an HTTP request will return the +// redirect. +// +// "targetURL" - the URL that the client will be redirected to when making HTTP +// requests for the redirected path. +// +// A successful POST will write http.StatusOK to the client. +func (srv *managementServer) addHTTPRedirect(w http.ResponseWriter, r *http.Request) { + // Unmarshal the request body JSON as a request object + var request struct { + Path string + TargetURL string + } + if err := mustParsePOST(&request, r); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // If the request has an empty path or target URL it's a bad request + if request.Path == "" || request.TargetURL == "" { + w.WriteHeader(http.StatusBadRequest) + return + } + // Add the HTTP redirect to the challenge server + srv.challSrv.AddHTTPRedirect(request.Path, request.TargetURL) + srv.log.Printf("Added HTTP redirect for path %q to %q\n", + request.Path, request.TargetURL) + w.WriteHeader(http.StatusOK) +} + +// delHTTPRedirect handles an HTTP POST request to delete an existing HTTP +// redirect for a given path. +// +// The POST body is expected to have one non-empty parameter: +// "path" - the path to remove a redirect for. +// +// A successful POST will write http.StatusOK to the client. +func (srv *managementServer) delHTTPRedirect(w http.ResponseWriter, r *http.Request) { + // Unmarshal the request body JSON as a request object + var request struct { + Path string + } + if err := mustParsePOST(&request, r); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + if request.Path == "" { + w.WriteHeader(http.StatusBadRequest) + return + } + // Delete the HTTP redirect for the given path from the challenge server + srv.challSrv.DeleteHTTPRedirect(request.Path) + srv.log.Printf("Removed HTTP redirect for path %q\n", request.Path) + w.WriteHeader(http.StatusOK) +} diff --git a/third-party/github.com/letsencrypt/boulder/test/chall-test-srv/main.go b/third-party/github.com/letsencrypt/boulder/test/chall-test-srv/main.go new file mode 100644 index 000000000..41241be52 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/test/chall-test-srv/main.go @@ -0,0 +1,171 @@ +package main + +import ( + "context" + "flag" + "fmt" + "log" + "net/http" + "os" + "strings" + "time" + + "github.com/letsencrypt/challtestsrv" + + "github.com/letsencrypt/boulder/cmd" +) + +// managementServer is a small HTTP server that can control a challenge server, +// adding and deleting challenge responses as required +type managementServer struct { + // A managementServer is a http.Server + *http.Server + log *log.Logger + // The challenge server that is under control by the management server + challSrv *challtestsrv.ChallSrv +} + +func (srv *managementServer) Run() { + srv.log.Printf("Starting management server on %s", srv.Server.Addr) + // Start the HTTP server in its own dedicated Go routine + go func() { + err := srv.ListenAndServe() + if err != nil && !strings.Contains(err.Error(), "Server closed") { + srv.log.Print(err) + } + }() +} + +func (srv *managementServer) Shutdown() { + if err := srv.Server.Shutdown(context.Background()); err != nil { + srv.log.Printf("Err shutting down management server") + } +} + +func filterEmpty(input []string) []string { + var output []string + for _, val := range input { + trimmed := strings.TrimSpace(val) + if trimmed != "" { + output = append(output, trimmed) + } + } + return output +} + +func main() { + httpOneBind := flag.String("http01", ":5002", + "Comma separated bind addresses/ports for HTTP-01 challenges. Set empty to disable.") + httpsOneBind := flag.String("https01", ":5003", + "Comma separated bind addresses/ports for HTTPS HTTP-01 challenges. Set empty to disable.") + dohBind := flag.String("doh", ":8443", + "Comma separated bind addresses/ports for DoH queries. Set empty to disable.") + dohCert := flag.String("doh-cert", "", "Path to certificate file for DoH server.") + dohCertKey := flag.String("doh-cert-key", "", "Path to certificate key file for DoH server.") + dnsOneBind := flag.String("dns01", ":8053", + "Comma separated bind addresses/ports for DNS-01 challenges and fake DNS data. Set empty to disable.") + tlsAlpnOneBind := flag.String("tlsalpn01", ":5001", + "Comma separated bind addresses/ports for TLS-ALPN-01 and HTTPS HTTP-01 challenges. Set empty to disable.") + managementBind := flag.String("management", ":8055", + "Bind address/port for management HTTP interface") + defaultIPv4 := flag.String("defaultIPv4", "127.0.0.1", + "Default IPv4 address for mock DNS responses to A queries") + defaultIPv6 := flag.String("defaultIPv6", "::1", + "Default IPv6 address for mock DNS responses to AAAA queries") + + flag.Parse() + + if len(flag.Args()) > 0 { + fmt.Printf("invalid command line arguments: %s\n", strings.Join(flag.Args(), " ")) + flag.Usage() + os.Exit(1) + } + + httpOneAddresses := filterEmpty(strings.Split(*httpOneBind, ",")) + httpsOneAddresses := filterEmpty(strings.Split(*httpsOneBind, ",")) + dohAddresses := filterEmpty(strings.Split(*dohBind, ",")) + dnsOneAddresses := filterEmpty(strings.Split(*dnsOneBind, ",")) + tlsAlpnOneAddresses := filterEmpty(strings.Split(*tlsAlpnOneBind, ",")) + + logger := log.New(os.Stdout, "chall-test-srv - ", log.Ldate|log.Ltime) + + // Create a new challenge server with the provided config + srv, err := challtestsrv.New(challtestsrv.Config{ + HTTPOneAddrs: httpOneAddresses, + HTTPSOneAddrs: httpsOneAddresses, + DOHAddrs: dohAddresses, + DOHCert: *dohCert, + DOHCertKey: *dohCertKey, + DNSOneAddrs: dnsOneAddresses, + TLSALPNOneAddrs: tlsAlpnOneAddresses, + Log: logger, + }) + cmd.FailOnError(err, "Unable to construct challenge server") + + // Create a new management server with the provided config + oobSrv := managementServer{ + Server: &http.Server{ + Addr: *managementBind, + ReadTimeout: 30 * time.Second, + }, + challSrv: srv, + log: logger, + } + // Register handlers on the management server for adding challenge responses + // for the configured challenges. + if *httpOneBind != "" || *httpsOneBind != "" { + http.HandleFunc("/add-http01", oobSrv.addHTTP01) + http.HandleFunc("/del-http01", oobSrv.delHTTP01) + http.HandleFunc("/add-redirect", oobSrv.addHTTPRedirect) + http.HandleFunc("/del-redirect", oobSrv.delHTTPRedirect) + } + if *dnsOneBind != "" { + http.HandleFunc("/set-default-ipv4", oobSrv.setDefaultDNSIPv4) + http.HandleFunc("/set-default-ipv6", oobSrv.setDefaultDNSIPv6) + // TODO(@cpu): It might make sense to revisit this API in the future to have + // one endpoint that accepts the mock type required (A, AAAA, CNAME, etc) + // instead of having separate endpoints per type. + http.HandleFunc("/set-txt", oobSrv.addDNS01) + http.HandleFunc("/clear-txt", oobSrv.delDNS01) + http.HandleFunc("/add-a", oobSrv.addDNSARecord) + http.HandleFunc("/clear-a", oobSrv.delDNSARecord) + http.HandleFunc("/add-aaaa", oobSrv.addDNSAAAARecord) + http.HandleFunc("/clear-aaaa", oobSrv.delDNSAAAARecord) + http.HandleFunc("/add-caa", oobSrv.addDNSCAARecord) + http.HandleFunc("/clear-caa", oobSrv.delDNSCAARecord) + http.HandleFunc("/set-cname", oobSrv.addDNSCNAMERecord) + http.HandleFunc("/clear-cname", oobSrv.delDNSCNAMERecord) + http.HandleFunc("/set-servfail", oobSrv.addDNSServFailRecord) + http.HandleFunc("/clear-servfail", oobSrv.delDNSServFailRecord) + + srv.SetDefaultDNSIPv4(*defaultIPv4) + srv.SetDefaultDNSIPv6(*defaultIPv6) + if *defaultIPv4 != "" { + logger.Printf("Answering A queries with %s by default", + *defaultIPv4) + } + if *defaultIPv6 != "" { + logger.Printf("Answering AAAA queries with %s by default", + *defaultIPv6) + } + } + if *tlsAlpnOneBind != "" { + http.HandleFunc("/add-tlsalpn01", oobSrv.addTLSALPN01) + http.HandleFunc("/del-tlsalpn01", oobSrv.delTLSALPN01) + } + + http.HandleFunc("/clear-request-history", oobSrv.clearHistory) + http.HandleFunc("/http-request-history", oobSrv.getHTTPHistory) + http.HandleFunc("/dns-request-history", oobSrv.getDNSHistory) + http.HandleFunc("/tlsalpn01-request-history", oobSrv.getTLSALPNHistory) + + // Start all of the sub-servers in their own Go routines so that the main Go + // routine can spin forever looking for signals to catch. + go srv.Run() + go oobSrv.Run() + + cmd.CatchSignals(func() { + srv.Shutdown() + oobSrv.Shutdown() + }) +} diff --git a/third-party/github.com/letsencrypt/boulder/test/chall-test-srv/mockdns.go b/third-party/github.com/letsencrypt/boulder/test/chall-test-srv/mockdns.go new file mode 100644 index 000000000..5b3151f1b --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/test/chall-test-srv/mockdns.go @@ -0,0 +1,351 @@ +// addDNS01 handles an HTTP POST request to add a new DNS-01 challenge TXT +package main + +import ( + "net/http" + "strings" + + "github.com/letsencrypt/challtestsrv" +) + +// setDefaultDNSIPv4 handles an HTTP POST request to set the default IPv4 +// address used for all A query responses that do not match more-specific mocked +// responses. +// +// The POST body is expected to have one parameter: +// "ip" - the string representation of an IPv4 address to use for all A queries +// that do not match more specific mocks. +// +// Providing an empty string as the IP value will disable the default +// A responses. +// +// A successful POST will write http.StatusOK to the client. +func (srv *managementServer) setDefaultDNSIPv4(w http.ResponseWriter, r *http.Request) { + var request struct { + IP string + } + if err := mustParsePOST(&request, r); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // Set the challenge server's default IPv4 address - we allow request.IP to be + // the empty string so that the default can be cleared using the same + // method. + srv.challSrv.SetDefaultDNSIPv4(request.IP) + srv.log.Printf("Set default IPv4 address for DNS A queries to %q\n", request.IP) + w.WriteHeader(http.StatusOK) +} + +// setDefaultDNSIPv6 handles an HTTP POST request to set the default IPv6 +// address used for all AAAA query responses that do not match more-specific +// mocked responses. +// +// The POST body is expected to have one parameter: +// "ip" - the string representation of an IPv6 address to use for all AAAA +// queries that do not match more specific mocks. +// +// Providing an empty string as the IP value will disable the default +// A responses. +// +// A successful POST will write http.StatusOK to the client. +func (srv *managementServer) setDefaultDNSIPv6(w http.ResponseWriter, r *http.Request) { + var request struct { + IP string + } + if err := mustParsePOST(&request, r); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // Set the challenge server's default IPv6 address - we allow request.IP to be + // the empty string so that the default can be cleared using the same + // method. + srv.challSrv.SetDefaultDNSIPv6(request.IP) + srv.log.Printf("Set default IPv6 address for DNS AAAA queries to %q\n", request.IP) + w.WriteHeader(http.StatusOK) +} + +// addDNSARecord handles an HTTP POST request to add a mock A query response record +// for a host. +// +// The POST body is expected to have two non-empty parameters: +// "host" - the hostname that when queried should return the mocked A record. +// "addresses" - an array of IPv4 addresses in string representation that should +// be used for the A records returned for the query. +// +// A successful POST will write http.StatusOK to the client. +func (srv *managementServer) addDNSARecord(w http.ResponseWriter, r *http.Request) { + var request struct { + Host string + Addresses []string + } + if err := mustParsePOST(&request, r); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // If the request has no addresses or an empty host it's a bad request + if len(request.Addresses) == 0 || request.Host == "" { + w.WriteHeader(http.StatusBadRequest) + return + } + + srv.challSrv.AddDNSARecord(request.Host, request.Addresses) + srv.log.Printf("Added response for DNS A queries to %q : %s\n", + request.Host, strings.Join(request.Addresses, ", ")) + w.WriteHeader(http.StatusOK) +} + +// delDNSARecord handles an HTTP POST request to delete an existing mock A +// policy record for a host. +// +// The POST body is expected to have one non-empty parameter: +// "host" - the hostname to remove the mock A record for. +// +// A successful POST will write http.StatusOK to the client. +func (srv *managementServer) delDNSARecord(w http.ResponseWriter, r *http.Request) { + var request struct { + Host string + } + if err := mustParsePOST(&request, r); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // If the request has an empty host it's a bad request + if request.Host == "" { + w.WriteHeader(http.StatusBadRequest) + return + } + + srv.challSrv.DeleteDNSARecord(request.Host) + srv.log.Printf("Removed response for DNS A queries to %q", request.Host) + w.WriteHeader(http.StatusOK) +} + +// addDNSAAAARecord handles an HTTP POST request to add a mock AAAA query +// response record for a host. +// +// The POST body is expected to have two non-empty parameters: +// "host" - the hostname that when queried should return the mocked A record. +// "addresses" - an array of IPv6 addresses in string representation that should +// be used for the AAAA records returned for the query. +// +// A successful POST will write http.StatusOK to the client. +func (srv *managementServer) addDNSAAAARecord(w http.ResponseWriter, r *http.Request) { + var request struct { + Host string + Addresses []string + } + if err := mustParsePOST(&request, r); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // If the request has no addresses or an empty host it's a bad request + if len(request.Addresses) == 0 || request.Host == "" { + w.WriteHeader(http.StatusBadRequest) + return + } + + srv.challSrv.AddDNSAAAARecord(request.Host, request.Addresses) + srv.log.Printf("Added response for DNS AAAA queries to %q : %s\n", + request.Host, strings.Join(request.Addresses, ", ")) + w.WriteHeader(http.StatusOK) +} + +// delDNSAAAARecord handles an HTTP POST request to delete an existing mock AAAA +// policy record for a host. +// +// The POST body is expected to have one non-empty parameter: +// "host" - the hostname to remove the mock AAAA record for. +// +// A successful POST will write http.StatusOK to the client. +func (srv *managementServer) delDNSAAAARecord(w http.ResponseWriter, r *http.Request) { + var request struct { + Host string + } + if err := mustParsePOST(&request, r); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // If the request has an empty host it's a bad request + if request.Host == "" { + w.WriteHeader(http.StatusBadRequest) + return + } + + srv.challSrv.DeleteDNSAAAARecord(request.Host) + srv.log.Printf("Removed response for DNS AAAA queries to %q", request.Host) + w.WriteHeader(http.StatusOK) +} + +// addDNSCAARecord handles an HTTP POST request to add a mock CAA query +// response record for a host. +// +// The POST body is expected to have two non-empty parameters: +// "host" - the hostname that when queried should return the mocked CAA record. +// "policies" - an array of CAA policy objects. Each policy object is expected +// to have two non-empty keys, "tag" and "value". +// +// A successful POST will write http.StatusOK to the client. +func (srv *managementServer) addDNSCAARecord(w http.ResponseWriter, r *http.Request) { + var request struct { + Host string + Policies []challtestsrv.MockCAAPolicy + } + if err := mustParsePOST(&request, r); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // If the request has no host or no caa policies it's a bad request + if request.Host == "" || len(request.Policies) == 0 { + w.WriteHeader(http.StatusBadRequest) + return + } + + srv.challSrv.AddDNSCAARecord(request.Host, request.Policies) + srv.log.Printf("Added response for DNS CAA queries to %q", request.Host) + w.WriteHeader(http.StatusOK) +} + +// delDNSCAARecord handles an HTTP POST request to delete an existing mock CAA +// policy record for a host. +// +// The POST body is expected to have one non-empty parameter: +// "host" - the hostname to remove the mock CAA policy for. +// +// A successful POST will write http.StatusOK to the client. +func (srv *managementServer) delDNSCAARecord(w http.ResponseWriter, r *http.Request) { + var request struct { + Host string + } + if err := mustParsePOST(&request, r); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // If the request has an empty host it's a bad request + if request.Host == "" { + w.WriteHeader(http.StatusBadRequest) + return + } + + srv.challSrv.DeleteDNSCAARecord(request.Host) + srv.log.Printf("Removed response for DNS CAA queries to %q", request.Host) + w.WriteHeader(http.StatusOK) +} + +// addDNSCNAMERecord handles an HTTP POST request to add a mock CNAME query +// response record and alias for a host. +// +// The POST body is expected to have two non-empty parameters: +// "host" - the hostname that should be treated as an alias to the target +// "target" - the hostname whose mocked DNS records should be returned +// +// A successful POST will write http.StatusOK to the client. +func (srv *managementServer) addDNSCNAMERecord(w http.ResponseWriter, r *http.Request) { + var request struct { + Host string + Target string + } + if err := mustParsePOST(&request, r); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // If the request has no host or no caa policies it's a bad request + if request.Host == "" || request.Target == "" { + w.WriteHeader(http.StatusBadRequest) + return + } + + srv.challSrv.AddDNSCNAMERecord(request.Host, request.Target) + srv.log.Printf("Added response for DNS CNAME queries to %q targeting %q", request.Host, request.Target) + w.WriteHeader(http.StatusOK) +} + +// delDNSCNAMERecord handles an HTTP POST request to delete an existing mock +// CNAME record for a host. +// +// The POST body is expected to have one non-empty parameters: +// "host" - the hostname to remove the mock CNAME alias for. +// +// A successful POST will write http.StatusOK to the client. +func (srv *managementServer) delDNSCNAMERecord(w http.ResponseWriter, r *http.Request) { + var request struct { + Host string + } + if err := mustParsePOST(&request, r); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // If the request has an empty host it's a bad request + if request.Host == "" { + w.WriteHeader(http.StatusBadRequest) + return + } + + srv.challSrv.DeleteDNSCNAMERecord(request.Host) + srv.log.Printf("Removed response for DNS CNAME queries to %q", request.Host) + w.WriteHeader(http.StatusOK) +} + +// addDNSServFailRecord handles an HTTP POST request to add a mock SERVFAIL +// response record for a host. All queries for that host will subsequently +// result in SERVFAIL responses, overriding any other mocks. +// +// The POST body is expected to have one non-empty parameter: +// "host" - the hostname that should return SERVFAIL responses. +// +// A successful POST will write http.StatusOK to the client. +func (srv *managementServer) addDNSServFailRecord(w http.ResponseWriter, r *http.Request) { + var request struct { + Host string + } + if err := mustParsePOST(&request, r); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // If the request has no host it's a bad request + if request.Host == "" { + w.WriteHeader(http.StatusBadRequest) + return + } + + srv.challSrv.AddDNSServFailRecord(request.Host) + srv.log.Printf("Added SERVFAIL response for DNS queries to %q", request.Host) + w.WriteHeader(http.StatusOK) +} + +// delDNSServFailRecord handles an HTTP POST request to delete an existing mock +// SERVFAIL record for a host. +// +// The POST body is expected to have one non-empty parameters: +// "host" - the hostname to remove the mock SERVFAIL response from. +// +// A successful POST will write http.StatusOK to the client. +func (srv *managementServer) delDNSServFailRecord(w http.ResponseWriter, r *http.Request) { + var request struct { + Host string + } + if err := mustParsePOST(&request, r); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // If the request has an empty host it's a bad request + if request.Host == "" { + w.WriteHeader(http.StatusBadRequest) + return + } + + srv.challSrv.DeleteDNSServFailRecord(request.Host) + srv.log.Printf("Removed SERVFAIL response for DNS queries to %q", request.Host) + w.WriteHeader(http.StatusOK) +} diff --git a/third-party/github.com/letsencrypt/boulder/test/chall-test-srv/tlsalpnone.go b/third-party/github.com/letsencrypt/boulder/test/chall-test-srv/tlsalpnone.go new file mode 100644 index 000000000..52cb21bf6 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/test/chall-test-srv/tlsalpnone.go @@ -0,0 +1,65 @@ +package main + +import "net/http" + +// addTLSALPN01 handles an HTTP POST request to add a new TLS-ALPN-01 challenge +// response certificate for a given host. +// +// The POST body is expected to have two non-empty parameters: +// "host" - the hostname to add the challenge response certificate for. +// "content" - the key authorization value to use to construct the TLS-ALPN-01 +// challenge response certificate. +// +// A successful POST will write http.StatusOK to the client. +func (srv *managementServer) addTLSALPN01(w http.ResponseWriter, r *http.Request) { + // Unmarshal the request body JSON as a request object + var request struct { + Host string + Content string + } + if err := mustParsePOST(&request, r); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // If the request has an empty host or content it's a bad request + if request.Host == "" || request.Content == "" { + w.WriteHeader(http.StatusBadRequest) + return + } + + // Add the TLS-ALPN-01 challenge to the challenge server + srv.challSrv.AddTLSALPNChallenge(request.Host, request.Content) + srv.log.Printf("Added TLS-ALPN-01 challenge for host %q - key auth %q\n", + request.Host, request.Content) + w.WriteHeader(http.StatusOK) +} + +// delTLSALPN01 handles an HTTP POST request to delete an existing TLS-ALPN-01 +// challenge response for a given host. +// +// The POST body is expected to have one non-empty parameter: +// "host" - the hostname to remove the TLS-ALPN-01 challenge response for. +// +// A successful POST will write http.StatusOK to the client. +func (srv *managementServer) delTLSALPN01(w http.ResponseWriter, r *http.Request) { + // Unmarshal the request body JSON as a request object + var request struct { + Host string + } + if err := mustParsePOST(&request, r); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // If the request has an empty host it's a bad request + if request.Host == "" { + w.WriteHeader(http.StatusBadRequest) + return + } + + // Delete the TLS-ALPN-01 challenge for the given host from the challenge server + srv.challSrv.DeleteTLSALPNChallenge(request.Host) + srv.log.Printf("Removed TLS-ALPN-01 challenge for host %q\n", request.Host) + w.WriteHeader(http.StatusOK) +} diff --git a/third-party/github.com/letsencrypt/boulder/test/challtestsrv.py b/third-party/github.com/letsencrypt/boulder/test/challtestsrv.py index 56e589207..0c50a2b13 100644 --- a/third-party/github.com/letsencrypt/boulder/test/challtestsrv.py +++ b/third-party/github.com/letsencrypt/boulder/test/challtestsrv.py @@ -3,8 +3,8 @@ import requests class ChallTestServer: """ - ChallTestServer is a wrapper around pebble-challtestsrv's HTTP management - API. If the pebble-challtestsrv process you want to interact with is using + ChallTestServer is a wrapper around chall-test-srv's HTTP management + API. If the chall-test-srv process you want to interact with is using a -management argument other than the default ('http://10.77.77.77:8055') you can instantiate the ChallTestServer using the -management address in use. If no custom address is provided the default is assumed. diff --git a/third-party/github.com/letsencrypt/boulder/test/config-next/admin.json b/third-party/github.com/letsencrypt/boulder/test/config-next/admin.json index 09dfe167d..c07753442 100644 --- a/third-party/github.com/letsencrypt/boulder/test/config-next/admin.json +++ b/third-party/github.com/letsencrypt/boulder/test/config-next/admin.json @@ -4,11 +4,10 @@ "dbConnectFile": "test/secrets/revoker_dburl", "maxOpenConns": 1 }, - "debugAddr": ":8014", "tls": { "caCertFile": "test/certs/ipki/minica.pem", - "certFile": "test/certs/ipki/admin-revoker.boulder/cert.pem", - "keyFile": "test/certs/ipki/admin-revoker.boulder/key.pem" + "certFile": "test/certs/ipki/admin.boulder/cert.pem", + "keyFile": "test/certs/ipki/admin.boulder/key.pem" }, "raService": { "dnsAuthority": "consul.service.consul", diff --git a/third-party/github.com/letsencrypt/boulder/test/config-next/bad-key-revoker.json b/third-party/github.com/letsencrypt/boulder/test/config-next/bad-key-revoker.json index cc98591c6..110f37ee9 100644 --- a/third-party/github.com/letsencrypt/boulder/test/config-next/bad-key-revoker.json +++ b/third-party/github.com/letsencrypt/boulder/test/config-next/bad-key-revoker.json @@ -19,16 +19,6 @@ "noWaitForReady": true, "timeout": "15s" }, - "mailer": { - "server": "localhost", - "port": "9380", - "username": "cert-manager@example.com", - "from": "bad key revoker ", - "passwordFile": "test/secrets/smtp_password", - "SMTPTrustedRootFile": "test/certs/ipki/minica.pem", - "emailSubject": "Certificates you've issued have been revoked due to key compromise", - "emailTemplate": "test/example-bad-key-revoker-template" - }, "maximumRevocations": 15, "findCertificatesBatchSize": 10, "interval": "50ms", diff --git a/third-party/github.com/letsencrypt/boulder/test/config-next/ca.json b/third-party/github.com/letsencrypt/boulder/test/config-next/ca.json index 58c335d9f..e72b9df94 100644 --- a/third-party/github.com/letsencrypt/boulder/test/config-next/ca.json +++ b/third-party/github.com/letsencrypt/boulder/test/config-next/ca.json @@ -31,6 +31,16 @@ } } }, + "sctService": { + "dnsAuthority": "consul.service.consul", + "srvLookup": { + "service": "ra-sct-provider", + "domain": "service.consul" + }, + "timeout": "15s", + "noWaitForReady": true, + "hostOverride": "ra.boulder" + }, "saService": { "dnsAuthority": "consul.service.consul", "srvLookup": { @@ -42,32 +52,55 @@ "hostOverride": "sa.boulder" }, "issuance": { - "defaultCertificateProfileName": "defaultBoulderCertificateProfile", "certProfiles": { - "defaultBoulderCertificateProfile": { - "allowMustStaple": true, - "allowCTPoison": true, - "allowSCTList": true, - "allowCommonName": true, - "policies": [ - { - "oid": "2.23.140.1.2.1" - } - ], + "legacy": { + "includeCRLDistributionPoints": true, "maxValidityPeriod": "7776000s", - "maxValidityBackdate": "1h5m" + "maxValidityBackdate": "1h5m", + "lintConfig": "test/config-next/zlint.toml", + "ignoredLints": [ + "w_subject_common_name_included", + "w_ext_subject_key_identifier_not_recommended_subscriber" + ] + }, + "modern": { + "omitCommonName": true, + "omitKeyEncipherment": true, + "omitClientAuth": true, + "omitSKID": true, + "includeCRLDistributionPoints": true, + "maxValidityPeriod": "583200s", + "maxValidityBackdate": "1h5m", + "lintConfig": "test/config-next/zlint.toml", + "ignoredLints": [ + "w_ext_subject_key_identifier_missing_sub_cert" + ] + }, + "shortlived": { + "omitCommonName": true, + "omitKeyEncipherment": true, + "omitClientAuth": true, + "omitSKID": true, + "includeCRLDistributionPoints": true, + "maxValidityPeriod": "160h", + "maxValidityBackdate": "1h5m", + "lintConfig": "test/config-next/zlint.toml", + "ignoredLints": [ + "w_ext_subject_key_identifier_missing_sub_cert" + ] } }, "crlProfile": { "validityInterval": "216h", - "maxBackdate": "1h5m" + "maxBackdate": "1h5m", + "lintConfig": "test/config-next/zlint.toml" }, "issuers": [ { "active": true, + "crlShards": 10, "issuerURL": "http://ca.example.org:4502/int-ecdsa-a", - "ocspURL": "http://ca.example.org:4002/", - "crlURLBase": "http://ca.example.org:4501/ecdsa-a/", + "crlURLBase": "http://ca.example.org:4501/lets-encrypt-crls/43104258997432926/", "location": { "configFile": "test/certs/webpki/int-ecdsa-a.pkcs11.json", "certFile": "test/certs/webpki/int-ecdsa-a.cert.pem", @@ -76,9 +109,9 @@ }, { "active": true, + "crlShards": 10, "issuerURL": "http://ca.example.org:4502/int-ecdsa-b", - "ocspURL": "http://ca.example.org:4002/", - "crlURLBase": "http://ca.example.org:4501/ecdsa-b/", + "crlURLBase": "http://ca.example.org:4501/lets-encrypt-crls/17302365692836921/", "location": { "configFile": "test/certs/webpki/int-ecdsa-b.pkcs11.json", "certFile": "test/certs/webpki/int-ecdsa-b.cert.pem", @@ -87,9 +120,9 @@ }, { "active": false, + "crlShards": 10, "issuerURL": "http://ca.example.org:4502/int-ecdsa-c", - "ocspURL": "http://ca.example.org:4002/", - "crlURLBase": "http://ca.example.org:4501/ecdsa-c/", + "crlURLBase": "http://ca.example.org:4501/lets-encrypt-crls/56560759852043581/", "location": { "configFile": "test/certs/webpki/int-ecdsa-c.pkcs11.json", "certFile": "test/certs/webpki/int-ecdsa-c.cert.pem", @@ -98,9 +131,9 @@ }, { "active": true, + "crlShards": 10, "issuerURL": "http://ca.example.org:4502/int-rsa-a", - "ocspURL": "http://ca.example.org:4002/", - "crlURLBase": "http://ca.example.org:4501/rsa-a/", + "crlURLBase": "http://ca.example.org:4501/lets-encrypt-crls/29947985078257530/", "location": { "configFile": "test/certs/webpki/int-rsa-a.pkcs11.json", "certFile": "test/certs/webpki/int-rsa-a.cert.pem", @@ -109,9 +142,9 @@ }, { "active": true, + "crlShards": 10, "issuerURL": "http://ca.example.org:4502/int-rsa-b", - "ocspURL": "http://ca.example.org:4002/", - "crlURLBase": "http://ca.example.org:4501/rsa-b/", + "crlURLBase": "http://ca.example.org:4501/lets-encrypt-crls/6762885421992935/", "location": { "configFile": "test/certs/webpki/int-rsa-b.pkcs11.json", "certFile": "test/certs/webpki/int-rsa-b.cert.pem", @@ -120,44 +153,35 @@ }, { "active": false, + "crlShards": 10, "issuerURL": "http://ca.example.org:4502/int-rsa-c", - "ocspURL": "http://ca.example.org:4002/", - "crlURLBase": "http://ca.example.org:4501/rsa-c/", + "crlURLBase": "http://ca.example.org:4501/lets-encrypt-crls/56183656833365902/", "location": { "configFile": "test/certs/webpki/int-rsa-c.pkcs11.json", "certFile": "test/certs/webpki/int-rsa-c.cert.pem", "numSessions": 2 } } - ], - "lintConfig": "test/config-next/zlint.toml", - "ignoredLints": [ - "w_subject_common_name_included", - "w_sub_cert_aia_contains_internal_names" ] }, - "expiry": "7776000s", - "backdate": "1h", - "serialPrefix": 127, + "serialPrefixHex": "6e", "maxNames": 100, "lifespanOCSP": "96h", - "goodkey": { - "weakKeyFile": "test/example-weak-keys.json", - "blockedKeyFile": "test/example-blocked-keys.yaml", - "fermatRounds": 100 - }, + "goodkey": {}, "ocspLogMaxLength": 4000, "ocspLogPeriod": "500ms", "ctLogListFile": "test/ct-test-srv/log_list.json", - "features": { - "ECDSAForAll": true - } + "features": {} }, "pa": { "challenges": { "http-01": true, "dns-01": true, "tls-alpn-01": true + }, + "identifiers": { + "dns": true, + "ip": true } }, "syslog": { diff --git a/third-party/github.com/letsencrypt/boulder/test/config-next/cert-checker.json b/third-party/github.com/letsencrypt/boulder/test/config-next/cert-checker.json index a4e7d2179..47ab4d6de 100644 --- a/third-party/github.com/letsencrypt/boulder/test/config-next/cert-checker.json +++ b/third-party/github.com/letsencrypt/boulder/test/config-next/cert-checker.json @@ -5,9 +5,6 @@ "maxOpenConns": 10 }, "hostnamePolicyFile": "test/hostname-policy.yaml", - "goodkey": { - "fermatRounds": 100 - }, "workers": 16, "unexpiredOnly": true, "badResultsOnly": true, @@ -15,13 +12,14 @@ "acceptableValidityDurations": [ "7776000s" ], + "lintConfig": "test/config-next/zlint.toml", "ignoredLints": [ "w_subject_common_name_included", - "w_sub_cert_aia_contains_internal_names" + "w_ext_subject_key_identifier_missing_sub_cert", + "w_ext_subject_key_identifier_not_recommended_subscriber" ], "ctLogListFile": "test/ct-test-srv/log_list.json", "features": { - "CertCheckerRequiresCorrespondence": true, "CertCheckerChecksValidations": true, "CertCheckerRequiresValidations": true } @@ -31,6 +29,10 @@ "http-01": true, "dns-01": true, "tls-alpn-01": true + }, + "identifiers": { + "dns": true, + "ip": true } }, "syslog": { diff --git a/third-party/github.com/letsencrypt/boulder/test/config-next/contact-auditor.json b/third-party/github.com/letsencrypt/boulder/test/config-next/contact-auditor.json deleted file mode 100644 index 23287c4a0..000000000 --- a/third-party/github.com/letsencrypt/boulder/test/config-next/contact-auditor.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "contactAuditor": { - "db": { - "dbConnectFile": "test/secrets/mailer_dburl", - "maxOpenConns": 10 - } - } -} diff --git a/third-party/github.com/letsencrypt/boulder/test/config-next/crl-updater.json b/third-party/github.com/letsencrypt/boulder/test/config-next/crl-updater.json index 86f7e601d..07e9900a9 100644 --- a/third-party/github.com/letsencrypt/boulder/test/config-next/crl-updater.json +++ b/third-party/github.com/letsencrypt/boulder/test/config-next/crl-updater.json @@ -48,12 +48,17 @@ "lookbackPeriod": "24h", "updatePeriod": "10m", "updateTimeout": "1m", + "expiresMargin": "5m", + "cacheControl": "stale-if-error=60", + "temporallyShardedSerialPrefixes": [ + "7f" + ], "maxParallelism": 10, "maxAttempts": 2, "features": {} }, "syslog": { - "stdoutlevel": 6, + "stdoutlevel": 4, "sysloglevel": -1 }, "openTelemetry": { diff --git a/third-party/github.com/letsencrypt/boulder/test/config-next/ecdsaAllowList.yml b/third-party/github.com/letsencrypt/boulder/test/config-next/ecdsaAllowList.yml deleted file mode 100644 index a648abda3..000000000 --- a/third-party/github.com/letsencrypt/boulder/test/config-next/ecdsaAllowList.yml +++ /dev/null @@ -1,2 +0,0 @@ ---- -- 1337 diff --git a/third-party/github.com/letsencrypt/boulder/test/config-next/email-exporter.json b/third-party/github.com/letsencrypt/boulder/test/config-next/email-exporter.json new file mode 100644 index 000000000..5652e0c1c --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/test/config-next/email-exporter.json @@ -0,0 +1,42 @@ +{ + "emailExporter": { + "debugAddr": ":8114", + "grpc": { + "maxConnectionAge": "30s", + "address": ":9603", + "services": { + "email.Exporter": { + "clientNames": [ + "wfe.boulder" + ] + }, + "grpc.health.v1.Health": { + "clientNames": [ + "health-checker.boulder" + ] + } + } + }, + "tls": { + "caCertFile": "test/certs/ipki/minica.pem", + "certFile": "test/certs/ipki/email-exporter.boulder/cert.pem", + "keyFile": "test/certs/ipki/email-exporter.boulder/key.pem" + }, + "perDayLimit": 999999, + "maxConcurrentRequests": 5, + "pardotBusinessUnit": "test-business-unit", + "clientId": { + "passwordFile": "test/secrets/salesforce_client_id" + }, + "clientSecret": { + "passwordFile": "test/secrets/salesforce_client_secret" + }, + "salesforceBaseURL": "http://localhost:9601", + "pardotBaseURL": "http://localhost:9602", + "emailCacheSize": 100000 + }, + "syslog": { + "stdoutlevel": 6, + "sysloglevel": -1 + } +} diff --git a/third-party/github.com/letsencrypt/boulder/test/config-next/expiration-mailer.json b/third-party/github.com/letsencrypt/boulder/test/config-next/expiration-mailer.json deleted file mode 100644 index 5289be50d..000000000 --- a/third-party/github.com/letsencrypt/boulder/test/config-next/expiration-mailer.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "mailer": { - "server": "localhost", - "port": "9380", - "username": "cert-manager@example.com", - "from": "Expiry bot ", - "passwordFile": "test/secrets/smtp_password", - "db": { - "dbConnectFile": "test/secrets/mailer_dburl", - "maxOpenConns": 10 - }, - "certLimit": 100000, - "mailsPerAddressPerDay": 4, - "updateChunkSize": 1000, - "nagTimes": [ - "480h", - "240h" - ], - "emailTemplate": "test/config-next/expiration-mailer.gotmpl", - "parallelSends": 10, - "tls": { - "caCertFile": "test/certs/ipki/minica.pem", - "certFile": "test/certs/ipki/expiration-mailer.boulder/cert.pem", - "keyFile": "test/certs/ipki/expiration-mailer.boulder/key.pem" - }, - "saService": { - "dnsAuthority": "consul.service.consul", - "srvLookup": { - "service": "sa", - "domain": "service.consul" - }, - "timeout": "15s", - "noWaitForReady": true, - "hostOverride": "sa.boulder" - }, - "SMTPTrustedRootFile": "test/certs/ipki/minica.pem", - "frequency": "1h", - "features": { - "ExpirationMailerUsesJoin": true - } - }, - "syslog": { - "stdoutlevel": 6, - "sysloglevel": -1 - }, - "openTelemetry": { - "endpoint": "bjaeger:4317", - "sampleratio": 1 - } -} diff --git a/third-party/github.com/letsencrypt/boulder/test/config-next/id-exporter.json b/third-party/github.com/letsencrypt/boulder/test/config-next/id-exporter.json deleted file mode 100644 index 526da6251..000000000 --- a/third-party/github.com/letsencrypt/boulder/test/config-next/id-exporter.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "contactExporter": { - "passwordFile": "test/secrets/smtp_password", - "db": { - "dbConnectFile": "test/secrets/mailer_dburl", - "maxOpenConns": 10 - } - } -} diff --git a/third-party/github.com/letsencrypt/boulder/test/config-next/nonce-a.json b/third-party/github.com/letsencrypt/boulder/test/config-next/nonce-a.json index 75df81b6e..d14b44063 100644 --- a/third-party/github.com/letsencrypt/boulder/test/config-next/nonce-a.json +++ b/third-party/github.com/letsencrypt/boulder/test/config-next/nonce-a.json @@ -1,8 +1,8 @@ { "NonceService": { "maxUsed": 131072, - "noncePrefixKey": { - "passwordFile": "test/secrets/nonce_prefix_key" + "nonceHMACKey": { + "keyFile": "test/secrets/nonce_prefix_key" }, "syslog": { "stdoutLevel": 6, diff --git a/third-party/github.com/letsencrypt/boulder/test/config-next/nonce-b.json b/third-party/github.com/letsencrypt/boulder/test/config-next/nonce-b.json index 75df81b6e..d14b44063 100644 --- a/third-party/github.com/letsencrypt/boulder/test/config-next/nonce-b.json +++ b/third-party/github.com/letsencrypt/boulder/test/config-next/nonce-b.json @@ -1,8 +1,8 @@ { "NonceService": { "maxUsed": 131072, - "noncePrefixKey": { - "passwordFile": "test/secrets/nonce_prefix_key" + "nonceHMACKey": { + "keyFile": "test/secrets/nonce_prefix_key" }, "syslog": { "stdoutLevel": 6, diff --git a/third-party/github.com/letsencrypt/boulder/test/config-next/notify-mailer.json b/third-party/github.com/letsencrypt/boulder/test/config-next/notify-mailer.json deleted file mode 100644 index 5aadfc4e9..000000000 --- a/third-party/github.com/letsencrypt/boulder/test/config-next/notify-mailer.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "notifyMailer": { - "server": "localhost", - "port": "9380", - "username": "cert-manager@example.com", - "passwordFile": "test/secrets/smtp_password", - "db": { - "dbConnectFile": "test/secrets/mailer_dburl", - "maxOpenConns": 10 - } - }, - "syslog": { - "stdoutLevel": 7, - "syslogLevel": -1 - } -} diff --git a/third-party/github.com/letsencrypt/boulder/test/config-next/observer.yml b/third-party/github.com/letsencrypt/boulder/test/config-next/observer.yml index d4cbc54fa..e8b86f12c 100644 --- a/third-party/github.com/letsencrypt/boulder/test/config-next/observer.yml +++ b/third-party/github.com/letsencrypt/boulder/test/config-next/observer.yml @@ -1,4 +1,4 @@ ---- +--- buckets: [.001, .002, .005, .01, .02, .05, .1, .2, .5, 1, 2, 5, 10] syslog: stdoutlevel: 6 @@ -31,10 +31,10 @@ monitors: recurse: true query_name: google.com query_type: A - - + - period: 2s kind: HTTP - settings: + settings: url: https://letsencrypt.org rcodes: [200] useragent: "letsencrypt/boulder-observer-http-client" @@ -83,10 +83,15 @@ monitors: recurse: true query_name: google.com query_type: A - - + - period: 2s kind: HTTP - settings: + settings: url: http://letsencrypt.org/foo rcodes: [200, 404] useragent: "letsencrypt/boulder-observer-http-client" + - + period: 10s + kind: TCP + settings: + hostport: acme-v02.api.letsencrypt.org:443 diff --git a/third-party/github.com/letsencrypt/boulder/test/config-next/ocsp-responder.json b/third-party/github.com/letsencrypt/boulder/test/config-next/ocsp-responder.json index bae653044..f1787d5e4 100644 --- a/third-party/github.com/letsencrypt/boulder/test/config-next/ocsp-responder.json +++ b/third-party/github.com/letsencrypt/boulder/test/config-next/ocsp-responder.json @@ -4,8 +4,8 @@ "username": "ocsp-responder", "passwordFile": "test/secrets/ocsp_responder_redis_password", "shardAddrs": { - "shard1": "10.33.33.2:4218", - "shard2": "10.33.33.3:4218" + "shard1": "10.77.77.2:4218", + "shard2": "10.77.77.3:4218" }, "timeout": "5s", "poolSize": 100, @@ -53,11 +53,12 @@ ], "liveSigningPeriod": "60h", "timeout": "4.9s", - "maxInflightSignings": 2, - "maxSigningWaiters": 1, "shutdownStopTimeout": "10s", + "maxInflightSignings": 20, + "maxSigningWaiters": 100, "requiredSerialPrefixes": [ - "7f" + "7f", + "6e" ], "features": {} }, diff --git a/third-party/github.com/letsencrypt/boulder/test/config-next/pardot-test-srv.json b/third-party/github.com/letsencrypt/boulder/test/config-next/pardot-test-srv.json new file mode 100644 index 000000000..ee5c035fb --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/test/config-next/pardot-test-srv.json @@ -0,0 +1,6 @@ +{ + "oauthAddr": ":9601", + "pardotAddr": ":9602", + "expectedClientId": "test-client-id", + "expectedClientSecret": "you-shall-not-pass" +} diff --git a/third-party/github.com/letsencrypt/boulder/test/config-next/ra.json b/third-party/github.com/letsencrypt/boulder/test/config-next/ra.json index 6ead49561..7229bae42 100644 --- a/third-party/github.com/letsencrypt/boulder/test/config-next/ra.json +++ b/third-party/github.com/letsencrypt/boulder/test/config-next/ra.json @@ -1,17 +1,32 @@ { "ra": { - "rateLimitPoliciesFilename": "test/rate-limit-policies.yml", + "limiter": { + "redis": { + "username": "boulder-wfe", + "passwordFile": "test/secrets/wfe_ratelimits_redis_password", + "lookups": [ + { + "Service": "redisratelimits", + "Domain": "service.consul" + } + ], + "lookupDNSAuthority": "consul.service.consul", + "readTimeout": "250ms", + "writeTimeout": "250ms", + "poolSize": 100, + "routeRandomly": true, + "tls": { + "caCertFile": "test/certs/ipki/minica.pem", + "certFile": "test/certs/ipki/wfe.boulder/cert.pem", + "keyFile": "test/certs/ipki/wfe.boulder/key.pem" + } + }, + "Defaults": "test/config-next/wfe2-ratelimit-defaults.yml", + "Overrides": "test/config-next/wfe2-ratelimit-overrides.yml" + }, "maxContactsPerRegistration": 3, "hostnamePolicyFile": "test/hostname-policy.yaml", - "maxNames": 100, - "authorizationLifetimeDays": 30, - "pendingAuthorizationLifetimeDays": 7, - "goodkey": { - "weakKeyFile": "test/example-weak-keys.json", - "blockedKeyFile": "test/example-blocked-keys.yaml", - "fermatRounds": 100 - }, - "orderLifetime": "168h", + "goodkey": {}, "finalizeTimeout": "30s", "issuerCerts": [ "test/certs/webpki/int-rsa-a.cert.pem", @@ -21,6 +36,37 @@ "test/certs/webpki/int-ecdsa-b.cert.pem", "test/certs/webpki/int-ecdsa-c.cert.pem" ], + "validationProfiles": { + "legacy": { + "pendingAuthzLifetime": "168h", + "validAuthzLifetime": "720h", + "orderLifetime": "168h", + "maxNames": 100, + "identifierTypes": [ + "dns" + ] + }, + "modern": { + "pendingAuthzLifetime": "7h", + "validAuthzLifetime": "7h", + "orderLifetime": "7h", + "maxNames": 10, + "identifierTypes": [ + "dns" + ] + }, + "shortlived": { + "pendingAuthzLifetime": "7h", + "validAuthzLifetime": "7h", + "orderLifetime": "7h", + "maxNames": 10, + "identifierTypes": [ + "dns", + "ip" + ] + } + }, + "defaultProfileName": "legacy", "tls": { "caCertFile": "test/certs/ipki/minica.pem", "certFile": "test/certs/ipki/ra.boulder/cert.pem", @@ -91,10 +137,16 @@ "services": { "ra.RegistrationAuthority": { "clientNames": [ - "admin-revoker.boulder", + "admin.boulder", "bad-key-revoker.boulder", "ocsp-responder.boulder", - "wfe.boulder" + "wfe.boulder", + "sfe.boulder" + ] + }, + "ra.SCTProvider": { + "clientNames": [ + "ca.boulder" ] }, "grpc.health.v1.Health": { @@ -105,7 +157,9 @@ } }, "features": { - "AsyncFinalize": true + "AsyncFinalize": true, + "AutomaticallyPauseZombieClients": true, + "NoPendingAuthzReuse": true }, "ctLogs": { "stagger": "500ms", @@ -137,6 +191,10 @@ "http-01": true, "dns-01": true, "tls-alpn-01": true + }, + "identifiers": { + "dns": true, + "ip": true } }, "syslog": { diff --git a/third-party/github.com/letsencrypt/boulder/test/config-next/remoteva-a.json b/third-party/github.com/letsencrypt/boulder/test/config-next/remoteva-a.json index 4085a6e14..43f22840c 100644 --- a/third-party/github.com/letsencrypt/boulder/test/config-next/remoteva-a.json +++ b/third-party/github.com/letsencrypt/boulder/test/config-next/remoteva-a.json @@ -7,7 +7,6 @@ "10.77.77.77:8443" ], "dnsTimeout": "1s", - "dnsAllowLoopbackAddresses": true, "issuerDomain": "happy-hacker-ca.invalid", "tls": { "caCertfile": "test/certs/ipki/minica.pem", @@ -23,6 +22,11 @@ "va.boulder" ] }, + "va.CAA": { + "clientNames": [ + "va.boulder" + ] + }, "grpc.health.v1.Health": { "clientNames": [ "health-checker.boulder" @@ -30,13 +34,12 @@ } } }, - "features": { - "DOH": true - }, "accountURIPrefixes": [ "http://boulder.service.consul:4000/acme/reg/", "http://boulder.service.consul:4001/acme/acct/" - ] + ], + "perspective": "dadaist", + "rir": "ARIN" }, "syslog": { "stdoutlevel": 4, diff --git a/third-party/github.com/letsencrypt/boulder/test/config-next/remoteva-b.json b/third-party/github.com/letsencrypt/boulder/test/config-next/remoteva-b.json index 8e9a44e84..7595a8b4e 100644 --- a/third-party/github.com/letsencrypt/boulder/test/config-next/remoteva-b.json +++ b/third-party/github.com/letsencrypt/boulder/test/config-next/remoteva-b.json @@ -7,7 +7,6 @@ "10.77.77.77:8443" ], "dnsTimeout": "1s", - "dnsAllowLoopbackAddresses": true, "issuerDomain": "happy-hacker-ca.invalid", "tls": { "caCertfile": "test/certs/ipki/minica.pem", @@ -23,6 +22,11 @@ "va.boulder" ] }, + "va.CAA": { + "clientNames": [ + "va.boulder" + ] + }, "grpc.health.v1.Health": { "clientNames": [ "health-checker.boulder" @@ -30,13 +34,12 @@ } } }, - "features": { - "DOH": true - }, "accountURIPrefixes": [ "http://boulder.service.consul:4000/acme/reg/", "http://boulder.service.consul:4001/acme/acct/" - ] + ], + "perspective": "surrealist", + "rir": "RIPE" }, "syslog": { "stdoutlevel": 4, diff --git a/third-party/github.com/letsencrypt/boulder/test/config-next/va-remote-b.json b/third-party/github.com/letsencrypt/boulder/test/config-next/remoteva-c.json similarity index 80% rename from third-party/github.com/letsencrypt/boulder/test/config-next/va-remote-b.json rename to third-party/github.com/letsencrypt/boulder/test/config-next/remoteva-c.json index e7fd187a5..a5ca7ffa5 100644 --- a/third-party/github.com/letsencrypt/boulder/test/config-next/va-remote-b.json +++ b/third-party/github.com/letsencrypt/boulder/test/config-next/remoteva-c.json @@ -1,19 +1,19 @@ { - "va": { - "userAgent": "boulder-remoteva-b", + "rva": { + "userAgent": "remoteva-c", "dnsTries": 3, "dnsStaticResolvers": [ "10.77.77.77:8343", "10.77.77.77:8443" ], "dnsTimeout": "1s", - "dnsAllowLoopbackAddresses": true, "issuerDomain": "happy-hacker-ca.invalid", "tls": { "caCertfile": "test/certs/ipki/minica.pem", "certFile": "test/certs/ipki/rva.boulder/cert.pem", "keyFile": "test/certs/ipki/rva.boulder/key.pem" }, + "skipGRPCClientCertVerification": true, "grpc": { "maxConnectionAge": "30s", "services": { @@ -22,6 +22,11 @@ "va.boulder" ] }, + "va.CAA": { + "clientNames": [ + "va.boulder" + ] + }, "grpc.health.v1.Health": { "clientNames": [ "health-checker.boulder" @@ -29,13 +34,12 @@ } } }, - "features": { - "DOH": true - }, "accountURIPrefixes": [ "http://boulder.service.consul:4000/acme/reg/", "http://boulder.service.consul:4001/acme/acct/" - ] + ], + "perspective": "cubist", + "rir": "ARIN" }, "syslog": { "stdoutlevel": 4, diff --git a/third-party/github.com/letsencrypt/boulder/test/config-next/rocsp-tool.json b/third-party/github.com/letsencrypt/boulder/test/config-next/rocsp-tool.json index a3a1d400c..dda6a73a2 100644 --- a/third-party/github.com/letsencrypt/boulder/test/config-next/rocsp-tool.json +++ b/third-party/github.com/letsencrypt/boulder/test/config-next/rocsp-tool.json @@ -4,8 +4,8 @@ "username": "rocsp-tool", "passwordFile": "test/secrets/rocsp_tool_password", "shardAddrs": { - "shard1": "10.33.33.2:4218", - "shard2": "10.33.33.3:4218" + "shard1": "10.77.77.2:4218", + "shard2": "10.77.77.3:4218" }, "timeout": "5s", "tls": { diff --git a/third-party/github.com/letsencrypt/boulder/test/config-next/sa.json b/third-party/github.com/letsencrypt/boulder/test/config-next/sa.json index c11cc9b43..1b9ff4687 100644 --- a/third-party/github.com/letsencrypt/boulder/test/config-next/sa.json +++ b/third-party/github.com/letsencrypt/boulder/test/config-next/sa.json @@ -24,18 +24,18 @@ "services": { "sa.StorageAuthority": { "clientNames": [ - "admin-revoker.boulder", + "admin.boulder", "ca.boulder", "crl-updater.boulder", - "expiration-mailer.boulder", "ra.boulder" ] }, "sa.StorageAuthorityReadOnly": { "clientNames": [ - "admin-revoker.boulder", + "admin.boulder", "ocsp-responder.boulder", - "wfe.boulder" + "wfe.boulder", + "sfe.boulder" ] }, "grpc.health.v1.Health": { @@ -48,8 +48,7 @@ }, "healthCheckInterval": "4s", "features": { - "MultipleCertificateProfiles": true, - "TrackReplacementCertificatesARI": true + "StoreARIReplacesInOrders": true } }, "syslog": { diff --git a/third-party/github.com/letsencrypt/boulder/test/config/admin-revoker.json b/third-party/github.com/letsencrypt/boulder/test/config-next/sfe.json similarity index 50% rename from third-party/github.com/letsencrypt/boulder/test/config/admin-revoker.json rename to third-party/github.com/letsencrypt/boulder/test/config-next/sfe.json index c450e0087..f15f58000 100644 --- a/third-party/github.com/letsencrypt/boulder/test/config/admin-revoker.json +++ b/third-party/github.com/letsencrypt/boulder/test/config-next/sfe.json @@ -1,13 +1,12 @@ { - "revoker": { - "db": { - "dbConnectFile": "test/secrets/revoker_dburl", - "maxOpenConns": 1 - }, + "sfe": { + "listenAddress": "0.0.0.0:4003", + "timeout": "30s", + "shutdownStopTimeout": "10s", "tls": { "caCertFile": "test/certs/ipki/minica.pem", - "certFile": "test/certs/ipki/admin-revoker.boulder/cert.pem", - "keyFile": "test/certs/ipki/admin-revoker.boulder/key.pem" + "certFile": "test/certs/ipki/sfe.boulder/cert.pem", + "keyFile": "test/certs/ipki/sfe.boulder/key.pem" }, "raService": { "dnsAuthority": "consul.service.consul", @@ -15,9 +14,9 @@ "service": "ra", "domain": "service.consul" }, - "hostOverride": "ra.boulder", + "timeout": "15s", "noWaitForReady": true, - "timeout": "15s" + "hostOverride": "ra.boulder" }, "saService": { "dnsAuthority": "consul.service.consul", @@ -29,10 +28,20 @@ "noWaitForReady": true, "hostOverride": "sa.boulder" }, + "unpauseHMACKey": { + "keyFile": "test/secrets/sfe_unpause_key" + }, "features": {} }, "syslog": { - "stdoutlevel": 6, - "sysloglevel": 6 + "stdoutlevel": 4, + "sysloglevel": -1 + }, + "openTelemetry": { + "endpoint": "bjaeger:4317", + "sampleratio": 1 + }, + "openTelemetryHttpConfig": { + "trustIncomingSpans": true } } diff --git a/third-party/github.com/letsencrypt/boulder/test/config-next/va-remote-a.json b/third-party/github.com/letsencrypt/boulder/test/config-next/va-remote-a.json deleted file mode 100644 index 15cac91de..000000000 --- a/third-party/github.com/letsencrypt/boulder/test/config-next/va-remote-a.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "va": { - "userAgent": "boulder-remoteva-a", - "dnsTries": 3, - "dnsStaticResolvers": [ - "10.77.77.77:8343", - "10.77.77.77:8443" - ], - "dnsTimeout": "1s", - "dnsAllowLoopbackAddresses": true, - "issuerDomain": "happy-hacker-ca.invalid", - "tls": { - "caCertfile": "test/certs/ipki/minica.pem", - "certFile": "test/certs/ipki/rva.boulder/cert.pem", - "keyFile": "test/certs/ipki/rva.boulder/key.pem" - }, - "grpc": { - "maxConnectionAge": "30s", - "services": { - "va.VA": { - "clientNames": [ - "va.boulder" - ] - }, - "grpc.health.v1.Health": { - "clientNames": [ - "health-checker.boulder" - ] - } - } - }, - "features": { - "DOH": true - }, - "accountURIPrefixes": [ - "http://boulder.service.consul:4000/acme/reg/", - "http://boulder.service.consul:4001/acme/acct/" - ] - }, - "syslog": { - "stdoutlevel": 4, - "sysloglevel": -1 - }, - "openTelemetry": { - "endpoint": "bjaeger:4317", - "sampleratio": 1 - } -} diff --git a/third-party/github.com/letsencrypt/boulder/test/config-next/va.json b/third-party/github.com/letsencrypt/boulder/test/config-next/va.json index 12efd33bc..a0bef772e 100644 --- a/third-party/github.com/letsencrypt/boulder/test/config-next/va.json +++ b/third-party/github.com/letsencrypt/boulder/test/config-next/va.json @@ -10,7 +10,6 @@ } }, "dnsTimeout": "1s", - "dnsAllowLoopbackAddresses": true, "issuerDomain": "happy-hacker-ca.invalid", "tls": { "caCertfile": "test/certs/ipki/minica.pem", @@ -37,34 +36,29 @@ } } }, - "features": { - "EnforceMultiCAA": true, - "MultiCAAFullResults": true, - "DOH": true - }, "remoteVAs": [ { "serverAddress": "rva1.service.consul:9397", "timeout": "15s", - "hostOverride": "rva1.boulder" + "hostOverride": "rva1.boulder", + "perspective": "dadaist", + "rir": "ARIN" }, { "serverAddress": "rva1.service.consul:9498", "timeout": "15s", - "hostOverride": "rva1.boulder" + "hostOverride": "rva1.boulder", + "perspective": "surrealist", + "rir": "RIPE" }, { - "serverAddress": "rva2.service.consul:9897", + "serverAddress": "rva1.service.consul:9499", "timeout": "15s", - "hostOverride": "rva2.boulder" - }, - { - "serverAddress": "rva2.service.consul:9998", - "timeout": "15s", - "hostOverride": "rva2.boulder" + "hostOverride": "rva1.boulder", + "perspective": "cubist", + "rir": "ARIN" } ], - "maxRemoteValidationFailures": 1, "accountURIPrefixes": [ "http://boulder.service.consul:4000/acme/reg/", "http://boulder.service.consul:4001/acme/acct/" diff --git a/third-party/github.com/letsencrypt/boulder/test/config-next/wfe2-ratelimit-defaults.yml b/third-party/github.com/letsencrypt/boulder/test/config-next/wfe2-ratelimit-defaults.yml index 0192c4bb3..d934b508c 100644 --- a/third-party/github.com/letsencrypt/boulder/test/config-next/wfe2-ratelimit-defaults.yml +++ b/third-party/github.com/letsencrypt/boulder/test/config-next/wfe2-ratelimit-defaults.yml @@ -14,11 +14,23 @@ FailedAuthorizationsPerDomainPerAccount: count: 3 burst: 3 period: 5m +# The burst represents failing 40 times per day for 90 days. The count and +# period grant one "freebie" failure per day. In combination, these parameters +# mean that: +# - Failing 120 times per day results in being paused after 30.25 days +# - Failing 40 times per day results in being paused after 92.3 days +# - Failing 20 times per day results in being paused after 6.2 months +# - Failing 4 times per day results in being paused after 3.3 years +# - Failing once per day results in never being paused +FailedAuthorizationsForPausingPerDomainPerAccount: + count: 1 + burst: 3600 + period: 24h NewOrdersPerAccount: count: 1500 burst: 1500 period: 3h CertificatesPerFQDNSet: - count: 6 - burst: 6 - period: 168h + count: 2 + burst: 2 + period: 3h diff --git a/third-party/github.com/letsencrypt/boulder/test/config-next/wfe2-ratelimit-overrides.yml b/third-party/github.com/letsencrypt/boulder/test/config-next/wfe2-ratelimit-overrides.yml index 95303173d..2bfd73980 100644 --- a/third-party/github.com/letsencrypt/boulder/test/config-next/wfe2-ratelimit-overrides.yml +++ b/third-party/github.com/letsencrypt/boulder/test/config-next/wfe2-ratelimit-overrides.yml @@ -3,8 +3,8 @@ count: 1000000 period: 168h ids: - - id: 127.0.0.1 - comment: localhost + - id: 64.112.117.1 + comment: test - CertificatesPerDomain: burst: 1 count: 1 diff --git a/third-party/github.com/letsencrypt/boulder/test/config-next/wfe2.json b/third-party/github.com/letsencrypt/boulder/test/config-next/wfe2.json index 15d480cb6..e68249aa9 100644 --- a/third-party/github.com/letsencrypt/boulder/test/config-next/wfe2.json +++ b/third-party/github.com/letsencrypt/boulder/test/config-next/wfe2.json @@ -11,9 +11,8 @@ "directoryCAAIdentity": "happy-hacker-ca.invalid", "directoryWebsite": "https://github.com/letsencrypt/boulder", "legacyKeyIDPrefix": "http://boulder.service.consul:4000/reg/", - "goodkey": { - "blockedKeyFile": "test/example-blocked-keys.yaml" - }, + "goodkey": {}, + "maxContactsPerRegistration": 3, "tls": { "caCertFile": "test/certs/ipki/minica.pem", "certFile": "test/certs/ipki/wfe.boulder/cert.pem", @@ -39,6 +38,16 @@ "noWaitForReady": true, "hostOverride": "sa.boulder" }, + "emailExporter": { + "dnsAuthority": "consul.service.consul", + "srvLookup": { + "service": "email-exporter", + "domain": "service.consul" + }, + "timeout": "15s", + "noWaitForReady": true, + "hostOverride": "email-exporter.boulder" + }, "accountCache": { "size": 9000, "ttl": "5s" @@ -70,8 +79,8 @@ "noWaitForReady": true, "hostOverride": "nonce.boulder" }, - "noncePrefixKey": { - "passwordFile": "test/secrets/nonce_prefix_key" + "nonceHMACKey": { + "keyFile": "test/secrets/nonce_prefix_key" }, "chains": [ [ @@ -100,8 +109,6 @@ ] ], "staleTimeout": "5m", - "authorizationLifetimeDays": 30, - "pendingAuthorizationLifetimeDays": 7, "limiter": { "redis": { "username": "boulder-wfe", @@ -127,15 +134,25 @@ "Overrides": "test/config-next/wfe2-ratelimit-overrides.yml" }, "features": { + "PropagateCancels": true, "ServeRenewalInfo": true, - "TrackReplacementCertificatesARI": true + "CheckIdentifiersPaused": true }, - "certificateProfileNames": [ - "defaultBoulderCertificateProfile" - ] + "certProfiles": { + "legacy": "The normal profile you know and love", + "modern": "Profile 2: Electric Boogaloo", + "shortlived": "Like modern, but smaller" + }, + "unpause": { + "hmacKey": { + "keyFile": "test/secrets/sfe_unpause_key" + }, + "jwtLifetime": "336h", + "url": "https://boulder.service.consul:4003" + } }, "syslog": { - "stdoutlevel": 4, + "stdoutlevel": 7, "sysloglevel": -1 }, "openTelemetry": { diff --git a/third-party/github.com/letsencrypt/boulder/test/config-next/zlint.toml b/third-party/github.com/letsencrypt/boulder/test/config-next/zlint.toml index 1ce7c7d9f..b80dad078 100644 --- a/third-party/github.com/letsencrypt/boulder/test/config-next/zlint.toml +++ b/third-party/github.com/letsencrypt/boulder/test/config-next/zlint.toml @@ -1,18 +1,28 @@ -[e_pkilint_lint_cabf_serverauth_cert] -pkilint_addr = "http://10.77.77.9" -pkilint_timeout = 200000000 # 200 milliseconds +[e_pkimetal_lint_cabf_serverauth_cert] +addr = "http://bpkimetal:8080" +severity = "notice" +timeout = 2000000000 # 2 seconds ignore_lints = [ - # We include the CN in (almost) all of our certificates, on purpose. - # See https://github.com/letsencrypt/boulder/issues/5112 for details. - "DvSubcriberAttributeAllowanceValidator:cabf.serverauth.dv.common_name_attribute_present", - # We include the SKID in all of our certs, on purpose. - # See https://github.com/letsencrypt/boulder/issues/7446 for details. - "SubscriberExtensionAllowanceValidator:cabf.serverauth.subscriber.subject_key_identifier_extension_present", - # We compute the skid using RFC7093 Method 1, on purpose. - # See https://github.com/letsencrypt/boulder/pull/7179 for details. - "SubjectKeyIdentifierValidator:pkix.subject_key_identifier_rfc7093_method_1_identified", - # We include the keyEncipherment key usage in RSA certs, on purpose. - # It is only necessary for old versions of TLS, and is included for backwards - # compatibility. We intend to remove this in the short-lived profile. - "SubscriberKeyUsageValidator:cabf.serverauth.subscriber_rsa_digitalsignature_and_keyencipherment_present", + # We continue to include the Common Name in our "classic" profile, but have + # removed it from our "tlsserver" and "shortlived" profiles. + "pkilint:cabf.serverauth.dv.common_name_attribute_present", + "zlint:w_subject_common_name_included", + # We continue to include the SKID extension in our "classic" profile, but have + # removed it from our "tlsserver" and "shortlived" profiles. + "pkilint:cabf.serverauth.subscriber.subject_key_identifier_extension_present", + "zlint:w_ext_subject_key_identifier_not_recommended_subscriber", + # We continue to include the Key Encipherment Key Usage for RSA certificates + # issued under the "classic" profile, but have removed it from our "tlsserver" + # and "shortlived" profiles. + "pkilint:cabf.serverauth.subscriber_rsa_digitalsignature_and_keyencipherment_present", + # Some linters continue to complain about the lack of an AIA OCSP URI, even + # when a CRLDP is present. + "certlint:br_certificates_must_include_an_http_url_of_the_ocsp_responder", + "x509lint:no_ocsp_over_http" ] + +[e_pkimetal_lint_cabf_serverauth_crl] +addr = "http://bpkimetal:8080" +severity = "notice" +timeout = 2000000000 # 2 seconds +ignore_lints = [] diff --git a/third-party/github.com/letsencrypt/boulder/test/config/admin.json b/third-party/github.com/letsencrypt/boulder/test/config/admin.json index 44ff407af..2567464d2 100644 --- a/third-party/github.com/letsencrypt/boulder/test/config/admin.json +++ b/third-party/github.com/letsencrypt/boulder/test/config/admin.json @@ -4,11 +4,10 @@ "dbConnectFile": "test/secrets/revoker_dburl", "maxOpenConns": 1 }, - "debugAddr": ":8014", "tls": { "caCertFile": "test/certs/ipki/minica.pem", - "certFile": "test/certs/ipki/admin-revoker.boulder/cert.pem", - "keyFile": "test/certs/ipki/admin-revoker.boulder/key.pem" + "certFile": "test/certs/ipki/admin.boulder/cert.pem", + "keyFile": "test/certs/ipki/admin.boulder/key.pem" }, "raService": { "dnsAuthority": "consul.service.consul", diff --git a/third-party/github.com/letsencrypt/boulder/test/config/ca.json b/third-party/github.com/letsencrypt/boulder/test/config/ca.json index cc4728363..e9a866ee6 100644 --- a/third-party/github.com/letsencrypt/boulder/test/config/ca.json +++ b/third-party/github.com/letsencrypt/boulder/test/config/ca.json @@ -1,6 +1,5 @@ { "ca": { - "debugAddr": ":8001", "tls": { "caCertFile": "test/certs/ipki/minica.pem", "certFile": "test/certs/ipki/ca.boulder/cert.pem", @@ -33,6 +32,16 @@ } } }, + "sctService": { + "dnsAuthority": "consul.service.consul", + "srvLookup": { + "service": "ra-sct-provider", + "domain": "service.consul" + }, + "timeout": "15s", + "noWaitForReady": true, + "hostOverride": "ra.boulder" + }, "saService": { "dnsAuthority": "consul.service.consul", "srvLookup": { @@ -44,25 +53,62 @@ "hostOverride": "sa.boulder" }, "issuance": { - "profile": { - "allowMustStaple": true, - "allowCTPoison": true, - "allowSCTList": true, - "allowCommonName": true, - "policies": [ - { - "oid": "2.23.140.1.2.1" - } - ], - "maxValidityPeriod": "7776000s", - "maxValidityBackdate": "1h5m" + "certProfiles": { + "legacy": { + "allowMustStaple": true, + "omitOCSP": true, + "includeCRLDistributionPoints": true, + "maxValidityPeriod": "7776000s", + "maxValidityBackdate": "1h5m", + "lintConfig": "test/config-next/zlint.toml", + "ignoredLints": [ + "w_subject_common_name_included", + "w_ext_subject_key_identifier_not_recommended_subscriber" + ] + }, + "modern": { + "allowMustStaple": true, + "omitCommonName": true, + "omitKeyEncipherment": true, + "omitClientAuth": true, + "omitSKID": true, + "omitOCSP": true, + "includeCRLDistributionPoints": true, + "maxValidityPeriod": "583200s", + "maxValidityBackdate": "1h5m", + "lintConfig": "test/config-next/zlint.toml", + "ignoredLints": [ + "w_ext_subject_key_identifier_missing_sub_cert" + ] + }, + "shortlived": { + "allowMustStaple": true, + "omitCommonName": true, + "omitKeyEncipherment": true, + "omitClientAuth": true, + "omitSKID": true, + "omitOCSP": true, + "includeCRLDistributionPoints": true, + "maxValidityPeriod": "160h", + "maxValidityBackdate": "1h5m", + "lintConfig": "test/config-next/zlint.toml", + "ignoredLints": [ + "w_ext_subject_key_identifier_missing_sub_cert" + ] + } + }, + "crlProfile": { + "validityInterval": "216h", + "maxBackdate": "1h5m", + "lintConfig": "test/config/zlint.toml" }, "issuers": [ { "active": true, + "crlShards": 10, "issuerURL": "http://ca.example.org:4502/int-ecdsa-a", "ocspURL": "http://ca.example.org:4002/", - "crlURLBase": "http://ca.example.org:4501/ecdsa-a/", + "crlURLBase": "http://ca.example.org:4501/lets-encrypt-crls/43104258997432926/", "location": { "configFile": "test/certs/webpki/int-ecdsa-a.pkcs11.json", "certFile": "test/certs/webpki/int-ecdsa-a.cert.pem", @@ -71,9 +117,10 @@ }, { "active": true, + "crlShards": 10, "issuerURL": "http://ca.example.org:4502/int-ecdsa-b", "ocspURL": "http://ca.example.org:4002/", - "crlURLBase": "http://ca.example.org:4501/ecdsa-b/", + "crlURLBase": "http://ca.example.org:4501/lets-encrypt-crls/17302365692836921/", "location": { "configFile": "test/certs/webpki/int-ecdsa-b.pkcs11.json", "certFile": "test/certs/webpki/int-ecdsa-b.cert.pem", @@ -82,9 +129,10 @@ }, { "active": false, + "crlShards": 10, "issuerURL": "http://ca.example.org:4502/int-ecdsa-c", "ocspURL": "http://ca.example.org:4002/", - "crlURLBase": "http://ca.example.org:4501/ecdsa-c/", + "crlURLBase": "http://ca.example.org:4501/lets-encrypt-crls/56560759852043581/", "location": { "configFile": "test/certs/webpki/int-ecdsa-c.pkcs11.json", "certFile": "test/certs/webpki/int-ecdsa-c.cert.pem", @@ -93,9 +141,10 @@ }, { "active": true, + "crlShards": 10, "issuerURL": "http://ca.example.org:4502/int-rsa-a", "ocspURL": "http://ca.example.org:4002/", - "crlURLBase": "http://ca.example.org:4501/rsa-a/", + "crlURLBase": "http://ca.example.org:4501/lets-encrypt-crls/29947985078257530/", "location": { "configFile": "test/certs/webpki/int-rsa-a.pkcs11.json", "certFile": "test/certs/webpki/int-rsa-a.cert.pem", @@ -104,9 +153,10 @@ }, { "active": true, + "crlShards": 10, "issuerURL": "http://ca.example.org:4502/int-rsa-b", "ocspURL": "http://ca.example.org:4002/", - "crlURLBase": "http://ca.example.org:4501/rsa-b/", + "crlURLBase": "http://ca.example.org:4501/lets-encrypt-crls/6762885421992935/", "location": { "configFile": "test/certs/webpki/int-rsa-b.pkcs11.json", "certFile": "test/certs/webpki/int-rsa-b.cert.pem", @@ -115,36 +165,25 @@ }, { "active": false, + "crlShards": 10, "issuerURL": "http://ca.example.org:4502/int-rsa-c", "ocspURL": "http://ca.example.org:4002/", - "crlURLBase": "http://ca.example.org:4501/rsa-c/", + "crlURLBase": "http://ca.example.org:4501/lets-encrypt-crls/56183656833365902/", "location": { "configFile": "test/certs/webpki/int-rsa-c.pkcs11.json", "certFile": "test/certs/webpki/int-rsa-c.cert.pem", "numSessions": 2 } } - ], - "lintConfig": "test/config/zlint.toml", - "ignoredLints": [ - "w_subject_common_name_included", - "w_sub_cert_aia_contains_internal_names" ] }, - "expiry": "7776000s", - "backdate": "1h", - "serialPrefix": 127, + "serialPrefixHex": "6e", "maxNames": 100, "lifespanOCSP": "96h", - "lifespanCRL": "216h", - "goodkey": { - "weakKeyFile": "test/example-weak-keys.json", - "blockedKeyFile": "test/example-blocked-keys.yaml", - "fermatRounds": 100 - }, + "goodkey": {}, "ocspLogMaxLength": 4000, "ocspLogPeriod": "500ms", - "ecdsaAllowListFilename": "test/config/ecdsaAllowList.yml", + "ctLogListFile": "test/ct-test-srv/log_list.json", "features": {} }, "pa": { @@ -152,6 +191,9 @@ "http-01": true, "dns-01": true, "tls-alpn-01": true + }, + "identifiers": { + "dns": true } }, "syslog": { diff --git a/third-party/github.com/letsencrypt/boulder/test/config/cert-checker.json b/third-party/github.com/letsencrypt/boulder/test/config/cert-checker.json index eb3d73cab..b4ba7e0b5 100644 --- a/third-party/github.com/letsencrypt/boulder/test/config/cert-checker.json +++ b/third-party/github.com/letsencrypt/boulder/test/config/cert-checker.json @@ -5,9 +5,6 @@ "maxOpenConns": 10 }, "hostnamePolicyFile": "test/hostname-policy.yaml", - "goodkey": { - "fermatRounds": 100 - }, "workers": 16, "unexpiredOnly": true, "badResultsOnly": true, @@ -17,14 +14,19 @@ ], "ignoredLints": [ "w_subject_common_name_included", - "w_sub_cert_aia_contains_internal_names" - ] + "w_ext_subject_key_identifier_missing_sub_cert", + "w_ext_subject_key_identifier_not_recommended_subscriber" + ], + "ctLogListFile": "test/ct-test-srv/log_list.json" }, "pa": { "challenges": { "http-01": true, "dns-01": true, "tls-alpn-01": true + }, + "identifiers": { + "dns": true } }, "syslog": { diff --git a/third-party/github.com/letsencrypt/boulder/test/config/contact-auditor.json b/third-party/github.com/letsencrypt/boulder/test/config/contact-auditor.json deleted file mode 100644 index 23287c4a0..000000000 --- a/third-party/github.com/letsencrypt/boulder/test/config/contact-auditor.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "contactAuditor": { - "db": { - "dbConnectFile": "test/secrets/mailer_dburl", - "maxOpenConns": 10 - } - } -} diff --git a/third-party/github.com/letsencrypt/boulder/test/config/crl-storer.json b/third-party/github.com/letsencrypt/boulder/test/config/crl-storer.json index ee3285d0a..3ab267b0f 100644 --- a/third-party/github.com/letsencrypt/boulder/test/config/crl-storer.json +++ b/third-party/github.com/letsencrypt/boulder/test/config/crl-storer.json @@ -25,7 +25,10 @@ "issuerCerts": [ "test/certs/webpki/int-rsa-a.cert.pem", "test/certs/webpki/int-rsa-b.cert.pem", - "test/certs/webpki/int-ecdsa-a.cert.pem" + "test/certs/webpki/int-rsa-c.cert.pem", + "test/certs/webpki/int-ecdsa-a.cert.pem", + "test/certs/webpki/int-ecdsa-b.cert.pem", + "test/certs/webpki/int-ecdsa-c.cert.pem" ], "s3Endpoint": "http://localhost:4501", "s3Bucket": "lets-encrypt-crls", diff --git a/third-party/github.com/letsencrypt/boulder/test/config/crl-updater.json b/third-party/github.com/letsencrypt/boulder/test/config/crl-updater.json index aabfad987..adb2b01e5 100644 --- a/third-party/github.com/letsencrypt/boulder/test/config/crl-updater.json +++ b/third-party/github.com/letsencrypt/boulder/test/config/crl-updater.json @@ -38,19 +38,27 @@ "issuerCerts": [ "test/certs/webpki/int-rsa-a.cert.pem", "test/certs/webpki/int-rsa-b.cert.pem", - "test/certs/webpki/int-ecdsa-a.cert.pem" + "test/certs/webpki/int-rsa-c.cert.pem", + "test/certs/webpki/int-ecdsa-a.cert.pem", + "test/certs/webpki/int-ecdsa-b.cert.pem", + "test/certs/webpki/int-ecdsa-c.cert.pem" ], "numShards": 10, "shardWidth": "240h", "lookbackPeriod": "24h", - "updatePeriod": "6h", - "updateOffset": "9120s", + "updatePeriod": "10m", + "updateTimeout": "1m", + "expiresMargin": "5m", + "cacheControl": "stale-if-error=60", + "temporallyShardedSerialPrefixes": [ + "7f" + ], "maxParallelism": 10, - "maxAttempts": 5, + "maxAttempts": 2, "features": {} }, "syslog": { - "stdoutlevel": 6, - "sysloglevel": 6 + "stdoutlevel": 4, + "sysloglevel": 4 } } diff --git a/third-party/github.com/letsencrypt/boulder/test/config/ecdsaAllowList.yml b/third-party/github.com/letsencrypt/boulder/test/config/ecdsaAllowList.yml deleted file mode 100644 index a648abda3..000000000 --- a/third-party/github.com/letsencrypt/boulder/test/config/ecdsaAllowList.yml +++ /dev/null @@ -1,2 +0,0 @@ ---- -- 1337 diff --git a/third-party/github.com/letsencrypt/boulder/test/config/email-exporter.json b/third-party/github.com/letsencrypt/boulder/test/config/email-exporter.json new file mode 100644 index 000000000..8505cc453 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/test/config/email-exporter.json @@ -0,0 +1,41 @@ +{ + "emailExporter": { + "debugAddr": ":8114", + "grpc": { + "maxConnectionAge": "30s", + "address": ":9603", + "services": { + "email.Exporter": { + "clientNames": [ + "wfe.boulder" + ] + }, + "grpc.health.v1.Health": { + "clientNames": [ + "health-checker.boulder" + ] + } + } + }, + "tls": { + "caCertFile": "test/certs/ipki/minica.pem", + "certFile": "test/certs/ipki/email-exporter.boulder/cert.pem", + "keyFile": "test/certs/ipki/email-exporter.boulder/key.pem" + }, + "perDayLimit": 999999, + "maxConcurrentRequests": 5, + "pardotBusinessUnit": "test-business-unit", + "clientId": { + "passwordFile": "test/secrets/salesforce_client_id" + }, + "clientSecret": { + "passwordFile": "test/secrets/salesforce_client_secret" + }, + "salesforceBaseURL": "http://localhost:9601", + "pardotBaseURL": "http://localhost:9602" + }, + "syslog": { + "stdoutlevel": 6, + "sysloglevel": -1 + } +} diff --git a/third-party/github.com/letsencrypt/boulder/test/config/expiration-mailer.json b/third-party/github.com/letsencrypt/boulder/test/config/expiration-mailer.json deleted file mode 100644 index 6f43bf25e..000000000 --- a/third-party/github.com/letsencrypt/boulder/test/config/expiration-mailer.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "mailer": { - "server": "localhost", - "port": "9380", - "username": "cert-manager@example.com", - "from": "Expiry bot ", - "passwordFile": "test/secrets/smtp_password", - "db": { - "dbConnectFile": "test/secrets/mailer_dburl", - "maxOpenConns": 10 - }, - "certLimit": 100000, - "nagTimes": [ - "480h", - "240h" - ], - "emailTemplate": "test/config/expiration-mailer.gotmpl", - "debugAddr": ":8008", - "tls": { - "caCertFile": "test/certs/ipki/minica.pem", - "certFile": "test/certs/ipki/expiration-mailer.boulder/cert.pem", - "keyFile": "test/certs/ipki/expiration-mailer.boulder/key.pem" - }, - "saService": { - "dnsAuthority": "consul.service.consul", - "srvLookup": { - "service": "sa", - "domain": "service.consul" - }, - "timeout": "15s", - "noWaitForReady": true, - "hostOverride": "sa.boulder" - }, - "SMTPTrustedRootFile": "test/certs/ipki/minica.pem", - "frequency": "1h" - }, - "syslog": { - "stdoutlevel": 6, - "sysloglevel": 6 - } -} diff --git a/third-party/github.com/letsencrypt/boulder/test/config/id-exporter.json b/third-party/github.com/letsencrypt/boulder/test/config/id-exporter.json deleted file mode 100644 index 526da6251..000000000 --- a/third-party/github.com/letsencrypt/boulder/test/config/id-exporter.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "contactExporter": { - "passwordFile": "test/secrets/smtp_password", - "db": { - "dbConnectFile": "test/secrets/mailer_dburl", - "maxOpenConns": 10 - } - } -} diff --git a/third-party/github.com/letsencrypt/boulder/test/config/log-validator.json b/third-party/github.com/letsencrypt/boulder/test/config/log-validator.json index bff0ca1f7..40dc121ca 100644 --- a/third-party/github.com/letsencrypt/boulder/test/config/log-validator.json +++ b/third-party/github.com/letsencrypt/boulder/test/config/log-validator.json @@ -2,20 +2,15 @@ "syslog": { "stdoutLevel": 7 }, - "debugAddr": ":8016", + "openTelemetry": { + "endpoint": "bjaeger:4317", + "sampleratio": 1 + }, "files": [ "/var/log/akamai-purger.log", "/var/log/bad-key-revoker.log", - "/var/log/boulder-ca.log", - "/var/log/boulder-observer.log", - "/var/log/boulder-publisher.log", - "/var/log/boulder-ra.log", - "/var/log/boulder-remoteva.log", - "/var/log/boulder-sa.log", - "/var/log/boulder-va.log", - "/var/log/boulder-wfe2.log", - "/var/log/crl-storer.log", - "/var/log/crl-updater.log", + "/var/log/boulder-*.log", + "/var/log/crl-*.log", "/var/log/nonce-service.log", "/var/log/ocsp-responder.log" ] diff --git a/third-party/github.com/letsencrypt/boulder/test/config/nonce-a.json b/third-party/github.com/letsencrypt/boulder/test/config/nonce-a.json index c2dd9765c..e549c30ba 100644 --- a/third-party/github.com/letsencrypt/boulder/test/config/nonce-a.json +++ b/third-party/github.com/letsencrypt/boulder/test/config/nonce-a.json @@ -1,9 +1,8 @@ { "NonceService": { "maxUsed": 131072, - "useDerivablePrefix": true, - "noncePrefixKey": { - "passwordFile": "test/secrets/nonce_prefix_key" + "nonceHMACKey": { + "keyFile": "test/secrets/nonce_prefix_key" }, "syslog": { "stdoutLevel": 6, diff --git a/third-party/github.com/letsencrypt/boulder/test/config/nonce-b.json b/third-party/github.com/letsencrypt/boulder/test/config/nonce-b.json index c2dd9765c..e549c30ba 100644 --- a/third-party/github.com/letsencrypt/boulder/test/config/nonce-b.json +++ b/third-party/github.com/letsencrypt/boulder/test/config/nonce-b.json @@ -1,9 +1,8 @@ { "NonceService": { "maxUsed": 131072, - "useDerivablePrefix": true, - "noncePrefixKey": { - "passwordFile": "test/secrets/nonce_prefix_key" + "nonceHMACKey": { + "keyFile": "test/secrets/nonce_prefix_key" }, "syslog": { "stdoutLevel": 6, diff --git a/third-party/github.com/letsencrypt/boulder/test/config/notify-mailer.json b/third-party/github.com/letsencrypt/boulder/test/config/notify-mailer.json deleted file mode 100644 index f6813a696..000000000 --- a/third-party/github.com/letsencrypt/boulder/test/config/notify-mailer.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "notifyMailer": { - "server": "localhost", - "port": "9380", - "username": "cert-manager@example.com", - "passwordFile": "test/secrets/smtp_password", - "db": { - "dbConnectFile": "test/secrets/mailer_dburl", - "maxOpenConns": 10 - } - }, - "syslog": { - "stdoutLevel": 7, - "syslogLevel": 7 - } -} diff --git a/third-party/github.com/letsencrypt/boulder/test/config/observer.yml b/third-party/github.com/letsencrypt/boulder/test/config/observer.yml index 150a76112..031f69eb6 100644 --- a/third-party/github.com/letsencrypt/boulder/test/config/observer.yml +++ b/third-party/github.com/letsencrypt/boulder/test/config/observer.yml @@ -1,5 +1,4 @@ --- -debugaddr: :8040 buckets: [.001, .002, .005, .01, .02, .05, .1, .2, .5, 1, 2, 5, 10] syslog: stdoutlevel: 6 @@ -38,6 +37,7 @@ monitors: settings: url: https://letsencrypt.org rcodes: [200] + useragent: "letsencrypt/boulder-observer-http-client" - period: 5s kind: DNS @@ -83,12 +83,13 @@ monitors: recurse: true query_name: google.com query_type: A - - + - period: 2s kind: HTTP - settings: + settings: url: http://letsencrypt.org/foo rcodes: [200, 404] + useragent: "letsencrypt/boulder-observer-http-client" - period: 10s kind: TCP diff --git a/third-party/github.com/letsencrypt/boulder/test/config/ocsp-responder.json b/third-party/github.com/letsencrypt/boulder/test/config/ocsp-responder.json index 80e155bce..1e5d4cb70 100644 --- a/third-party/github.com/letsencrypt/boulder/test/config/ocsp-responder.json +++ b/third-party/github.com/letsencrypt/boulder/test/config/ocsp-responder.json @@ -8,8 +8,8 @@ "username": "ocsp-responder", "passwordFile": "test/secrets/ocsp_responder_redis_password", "shardAddrs": { - "shard1": "10.33.33.2:4218", - "shard2": "10.33.33.3:4218" + "shard1": "10.77.77.2:4218", + "shard2": "10.77.77.3:4218" }, "timeout": "5s", "poolSize": 100, @@ -51,14 +51,19 @@ "issuerCerts": [ "test/certs/webpki/int-rsa-a.cert.pem", "test/certs/webpki/int-rsa-b.cert.pem", - "test/certs/webpki/int-ecdsa-a.cert.pem" + "test/certs/webpki/int-rsa-c.cert.pem", + "test/certs/webpki/int-ecdsa-a.cert.pem", + "test/certs/webpki/int-ecdsa-b.cert.pem", + "test/certs/webpki/int-ecdsa-c.cert.pem" ], "liveSigningPeriod": "60h", "timeout": "4.9s", "shutdownStopTimeout": "10s", + "maxInflightSignings": 20, "debugAddr": ":8005", "requiredSerialPrefixes": [ - "7f" + "7f", + "6e" ], "features": {} }, diff --git a/third-party/github.com/letsencrypt/boulder/test/config/pardot-test-srv.json b/third-party/github.com/letsencrypt/boulder/test/config/pardot-test-srv.json new file mode 100644 index 000000000..ee5c035fb --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/test/config/pardot-test-srv.json @@ -0,0 +1,6 @@ +{ + "oauthAddr": ":9601", + "pardotAddr": ":9602", + "expectedClientId": "test-client-id", + "expectedClientSecret": "you-shall-not-pass" +} diff --git a/third-party/github.com/letsencrypt/boulder/test/config/publisher.json b/third-party/github.com/letsencrypt/boulder/test/config/publisher.json index 8b67b0bc7..1909a6f60 100644 --- a/third-party/github.com/letsencrypt/boulder/test/config/publisher.json +++ b/third-party/github.com/letsencrypt/boulder/test/config/publisher.json @@ -20,10 +20,8 @@ "test/certs/webpki/root-ecdsa.cert.pem" ] ], - "debugAddr": ":8009", "grpc": { "maxConnectionAge": "30s", - "address": ":9091", "services": { "Publisher": { "clientNames": [ diff --git a/third-party/github.com/letsencrypt/boulder/test/config/ra.json b/third-party/github.com/letsencrypt/boulder/test/config/ra.json index add1779ab..613c5e1a1 100644 --- a/third-party/github.com/letsencrypt/boulder/test/config/ra.json +++ b/third-party/github.com/letsencrypt/boulder/test/config/ra.json @@ -1,23 +1,71 @@ { "ra": { - "rateLimitPoliciesFilename": "test/rate-limit-policies.yml", + "limiter": { + "redis": { + "username": "boulder-wfe", + "passwordFile": "test/secrets/wfe_ratelimits_redis_password", + "lookups": [ + { + "Service": "redisratelimits", + "Domain": "service.consul" + } + ], + "lookupDNSAuthority": "consul.service.consul", + "readTimeout": "250ms", + "writeTimeout": "250ms", + "poolSize": 100, + "routeRandomly": true, + "tls": { + "caCertFile": "test/certs/ipki/minica.pem", + "certFile": "test/certs/ipki/wfe.boulder/cert.pem", + "keyFile": "test/certs/ipki/wfe.boulder/key.pem" + } + }, + "Defaults": "test/config/wfe2-ratelimit-defaults.yml", + "Overrides": "test/config/wfe2-ratelimit-overrides.yml" + }, "maxContactsPerRegistration": 3, "debugAddr": ":8002", "hostnamePolicyFile": "test/hostname-policy.yaml", - "maxNames": 100, - "authorizationLifetimeDays": 30, - "pendingAuthorizationLifetimeDays": 7, - "goodkey": { - "weakKeyFile": "test/example-weak-keys.json", - "blockedKeyFile": "test/example-blocked-keys.yaml", - "fermatRounds": 100 - }, - "orderLifetime": "168h", + "goodkey": {}, "issuerCerts": [ "test/certs/webpki/int-rsa-a.cert.pem", "test/certs/webpki/int-rsa-b.cert.pem", - "test/certs/webpki/int-ecdsa-a.cert.pem" + "test/certs/webpki/int-rsa-c.cert.pem", + "test/certs/webpki/int-ecdsa-a.cert.pem", + "test/certs/webpki/int-ecdsa-b.cert.pem", + "test/certs/webpki/int-ecdsa-c.cert.pem" ], + "validationProfiles": { + "legacy": { + "pendingAuthzLifetime": "168h", + "validAuthzLifetime": "720h", + "orderLifetime": "168h", + "maxNames": 100, + "identifierTypes": [ + "dns" + ] + }, + "modern": { + "pendingAuthzLifetime": "7h", + "validAuthzLifetime": "7h", + "orderLifetime": "7h", + "maxNames": 10, + "identifierTypes": [ + "dns" + ] + }, + "shortlived": { + "pendingAuthzLifetime": "7h", + "validAuthzLifetime": "7h", + "orderLifetime": "7h", + "maxNames": 10, + "identifierTypes": [ + "dns" + ] + } + }, + "defaultProfileName": "legacy", "tls": { "caCertFile": "test/certs/ipki/minica.pem", "certFile": "test/certs/ipki/ra.boulder/cert.pem", @@ -85,14 +133,19 @@ }, "grpc": { "maxConnectionAge": "30s", - "address": ":9094", "services": { "ra.RegistrationAuthority": { "clientNames": [ - "admin-revoker.boulder", + "admin.boulder", "bad-key-revoker.boulder", "ocsp-responder.boulder", - "wfe.boulder" + "wfe.boulder", + "sfe.boulder" + ] + }, + "ra.SCTProvider": { + "clientNames": [ + "ca.boulder" ] }, "grpc.health.v1.Health": { @@ -102,7 +155,12 @@ } } }, - "features": {}, + "features": { + "AutomaticallyPauseZombieClients": true, + "NoPendingAuthzReuse": true, + "EnforceMPIC": true, + "UnsplitIssuance": true + }, "ctLogs": { "stagger": "500ms", "logListFile": "test/ct-test-srv/log_list.json", @@ -133,6 +191,9 @@ "http-01": true, "dns-01": true, "tls-alpn-01": true + }, + "identifiers": { + "dns": true } }, "syslog": { diff --git a/third-party/github.com/letsencrypt/boulder/test/config/remoteva-a.json b/third-party/github.com/letsencrypt/boulder/test/config/remoteva-a.json index ca21d7c89..2ace42df4 100644 --- a/third-party/github.com/letsencrypt/boulder/test/config/remoteva-a.json +++ b/third-party/github.com/letsencrypt/boulder/test/config/remoteva-a.json @@ -6,12 +6,11 @@ "dnsProvider": { "dnsAuthority": "consul.service.consul", "srvLookup": { - "service": "dns", + "service": "doh", "domain": "service.consul" } }, "dnsTimeout": "1s", - "dnsAllowLoopbackAddresses": true, "issuerDomain": "happy-hacker-ca.invalid", "tls": { "caCertfile": "test/certs/ipki/minica.pem", @@ -27,6 +26,11 @@ "va.boulder" ] }, + "va.CAA": { + "clientNames": [ + "va.boulder" + ] + }, "grpc.health.v1.Health": { "clientNames": [ "health-checker.boulder" @@ -34,11 +38,15 @@ } } }, - "features": {}, + "features": { + "DOH": true + }, "accountURIPrefixes": [ "http://boulder.service.consul:4000/acme/reg/", "http://boulder.service.consul:4001/acme/acct/" - ] + ], + "perspective": "dadaist", + "rir": "ARIN" }, "syslog": { "stdoutlevel": 4, diff --git a/third-party/github.com/letsencrypt/boulder/test/config/remoteva-b.json b/third-party/github.com/letsencrypt/boulder/test/config/remoteva-b.json index f49cd16c1..171b8534a 100644 --- a/third-party/github.com/letsencrypt/boulder/test/config/remoteva-b.json +++ b/third-party/github.com/letsencrypt/boulder/test/config/remoteva-b.json @@ -6,12 +6,11 @@ "dnsProvider": { "dnsAuthority": "consul.service.consul", "srvLookup": { - "service": "dns", + "service": "doh", "domain": "service.consul" } }, "dnsTimeout": "1s", - "dnsAllowLoopbackAddresses": true, "issuerDomain": "happy-hacker-ca.invalid", "tls": { "caCertfile": "test/certs/ipki/minica.pem", @@ -27,6 +26,11 @@ "va.boulder" ] }, + "va.CAA": { + "clientNames": [ + "va.boulder" + ] + }, "grpc.health.v1.Health": { "clientNames": [ "health-checker.boulder" @@ -34,11 +38,15 @@ } } }, - "features": {}, + "features": { + "DOH": true + }, "accountURIPrefixes": [ "http://boulder.service.consul:4000/acme/reg/", "http://boulder.service.consul:4001/acme/acct/" - ] + ], + "perspective": "surrealist", + "rir": "RIPE" }, "syslog": { "stdoutlevel": 4, diff --git a/third-party/github.com/letsencrypt/boulder/test/config/va-remote-a.json b/third-party/github.com/letsencrypt/boulder/test/config/remoteva-c.json similarity index 75% rename from third-party/github.com/letsencrypt/boulder/test/config/va-remote-a.json rename to third-party/github.com/letsencrypt/boulder/test/config/remoteva-c.json index c9571b5c4..22c168b66 100644 --- a/third-party/github.com/letsencrypt/boulder/test/config/va-remote-a.json +++ b/third-party/github.com/letsencrypt/boulder/test/config/remoteva-c.json @@ -1,17 +1,16 @@ { - "va": { - "userAgent": "boulder-remoteva-a", - "debugAddr": ":8011", + "rva": { + "userAgent": "remoteva-c", + "debugAddr": ":8213", "dnsTries": 3, "dnsProvider": { "dnsAuthority": "consul.service.consul", "srvLookup": { - "service": "dns", + "service": "doh", "domain": "service.consul" } }, "dnsTimeout": "1s", - "dnsAllowLoopbackAddresses": true, "issuerDomain": "happy-hacker-ca.invalid", "tls": { "caCertfile": "test/certs/ipki/minica.pem", @@ -20,13 +19,18 @@ }, "grpc": { "maxConnectionAge": "30s", - "address": ":9397", + "address": ":9899", "services": { "va.VA": { "clientNames": [ "va.boulder" ] }, + "va.CAA": { + "clientNames": [ + "va.boulder" + ] + }, "grpc.health.v1.Health": { "clientNames": [ "health-checker.boulder" @@ -34,11 +38,15 @@ } } }, - "features": {}, + "features": { + "DOH": true + }, "accountURIPrefixes": [ "http://boulder.service.consul:4000/acme/reg/", "http://boulder.service.consul:4001/acme/acct/" - ] + ], + "perspective": "cubist", + "rir": "ARIN" }, "syslog": { "stdoutlevel": 4, diff --git a/third-party/github.com/letsencrypt/boulder/test/config/rocsp-tool.json b/third-party/github.com/letsencrypt/boulder/test/config/rocsp-tool.json index 3f6170358..ae3d034f9 100644 --- a/third-party/github.com/letsencrypt/boulder/test/config/rocsp-tool.json +++ b/third-party/github.com/letsencrypt/boulder/test/config/rocsp-tool.json @@ -5,8 +5,8 @@ "username": "rocsp-tool", "passwordFile": "test/secrets/rocsp_tool_password", "shardAddrs": { - "shard1": "10.33.33.2:4218", - "shard2": "10.33.33.3:4218" + "shard1": "10.77.77.2:4218", + "shard2": "10.77.77.3:4218" }, "timeout": "5s", "tls": { diff --git a/third-party/github.com/letsencrypt/boulder/test/config/sa.json b/third-party/github.com/letsencrypt/boulder/test/config/sa.json index 24f635628..ec46b82df 100644 --- a/third-party/github.com/letsencrypt/boulder/test/config/sa.json +++ b/third-party/github.com/letsencrypt/boulder/test/config/sa.json @@ -8,8 +8,13 @@ "dbConnectFile": "test/secrets/sa_ro_dburl", "maxOpenConns": 100 }, + "incidentsDB": { + "dbConnectFile": "test/secrets/incidents_dburl", + "maxOpenConns": 100 + }, "ParallelismPerRPC": 20, "debugAddr": ":8003", + "lagFactor": "200ms", "tls": { "caCertFile": "test/certs/ipki/minica.pem", "certFile": "test/certs/ipki/sa.boulder/cert.pem", @@ -21,21 +26,18 @@ "services": { "sa.StorageAuthority": { "clientNames": [ - "admin-revoker.boulder", + "admin.boulder", "ca.boulder", "crl-updater.boulder", - "expiration-mailer.boulder", - "ocsp-responder.boulder", - "ra.boulder", - "wfe.boulder" + "ra.boulder" ] }, "sa.StorageAuthorityReadOnly": { "clientNames": [ - "admin-revoker.boulder", - "crl-updater.boulder", + "admin.boulder", "ocsp-responder.boulder", - "wfe.boulder" + "wfe.boulder", + "sfe.boulder" ] }, "grpc.health.v1.Health": { @@ -46,7 +48,11 @@ } } }, - "features": {} + "features": { + "MultipleCertificateProfiles": true, + "InsertAuthzsIndividually": true, + "IgnoreAccountContacts": true + } }, "syslog": { "stdoutlevel": 6, diff --git a/third-party/github.com/letsencrypt/boulder/test/config-next/admin-revoker.json b/third-party/github.com/letsencrypt/boulder/test/config/sfe.json similarity index 52% rename from third-party/github.com/letsencrypt/boulder/test/config-next/admin-revoker.json rename to third-party/github.com/letsencrypt/boulder/test/config/sfe.json index 389fc0080..73aa1f58e 100644 --- a/third-party/github.com/letsencrypt/boulder/test/config-next/admin-revoker.json +++ b/third-party/github.com/letsencrypt/boulder/test/config/sfe.json @@ -1,13 +1,13 @@ { - "revoker": { - "db": { - "dbConnectFile": "test/secrets/revoker_dburl", - "maxOpenConns": 1 - }, + "sfe": { + "listenAddress": "0.0.0.0:4003", + "debugAddr": ":8015", + "timeout": "30s", + "shutdownStopTimeout": "10s", "tls": { "caCertFile": "test/certs/ipki/minica.pem", - "certFile": "test/certs/ipki/admin-revoker.boulder/cert.pem", - "keyFile": "test/certs/ipki/admin-revoker.boulder/key.pem" + "certFile": "test/certs/ipki/sfe.boulder/cert.pem", + "keyFile": "test/certs/ipki/sfe.boulder/key.pem" }, "raService": { "dnsAuthority": "consul.service.consul", @@ -15,9 +15,9 @@ "service": "ra", "domain": "service.consul" }, - "hostOverride": "ra.boulder", + "timeout": "15s", "noWaitForReady": true, - "timeout": "15s" + "hostOverride": "ra.boulder" }, "saService": { "dnsAuthority": "consul.service.consul", @@ -29,10 +29,20 @@ "noWaitForReady": true, "hostOverride": "sa.boulder" }, + "unpauseHMACKey": { + "keyFile": "test/secrets/sfe_unpause_key" + }, "features": {} }, "syslog": { "stdoutlevel": 6, "sysloglevel": -1 + }, + "openTelemetry": { + "endpoint": "bjaeger:4317", + "sampleratio": 1 + }, + "openTelemetryHttpConfig": { + "trustIncomingSpans": true } } diff --git a/third-party/github.com/letsencrypt/boulder/test/config/va-remote-b.json b/third-party/github.com/letsencrypt/boulder/test/config/va-remote-b.json deleted file mode 100644 index c853f0cd9..000000000 --- a/third-party/github.com/letsencrypt/boulder/test/config/va-remote-b.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "va": { - "userAgent": "boulder-remoteva-b", - "debugAddr": ":8012", - "dnsTries": 3, - "dnsProvider": { - "dnsAuthority": "consul.service.consul", - "srvLookup": { - "service": "dns", - "domain": "service.consul" - } - }, - "dnsTimeout": "1s", - "dnsAllowLoopbackAddresses": true, - "issuerDomain": "happy-hacker-ca.invalid", - "tls": { - "caCertfile": "test/certs/ipki/minica.pem", - "certFile": "test/certs/ipki/rva.boulder/cert.pem", - "keyFile": "test/certs/ipki/rva.boulder/key.pem" - }, - "grpc": { - "maxConnectionAge": "30s", - "address": ":9498", - "services": { - "va.VA": { - "clientNames": [ - "va.boulder" - ] - }, - "grpc.health.v1.Health": { - "clientNames": [ - "health-checker.boulder" - ] - } - } - }, - "features": {}, - "accountURIPrefixes": [ - "http://boulder.service.consul:4000/acme/reg/", - "http://boulder.service.consul:4001/acme/acct/" - ] - }, - "syslog": { - "stdoutlevel": 4, - "sysloglevel": 4 - } -} diff --git a/third-party/github.com/letsencrypt/boulder/test/config/va.json b/third-party/github.com/letsencrypt/boulder/test/config/va.json index a04a35380..1172ad9de 100644 --- a/third-party/github.com/letsencrypt/boulder/test/config/va.json +++ b/third-party/github.com/letsencrypt/boulder/test/config/va.json @@ -6,12 +6,11 @@ "dnsProvider": { "dnsAuthority": "consul.service.consul", "srvLookup": { - "service": "dns", + "service": "doh", "domain": "service.consul" } }, "dnsTimeout": "1s", - "dnsAllowLoopbackAddresses": true, "issuerDomain": "happy-hacker-ca.invalid", "tls": { "caCertfile": "test/certs/ipki/minica.pem", @@ -38,30 +37,32 @@ } } }, - "features": {}, + "features": { + "DOH": true + }, "remoteVAs": [ { "serverAddress": "rva1.service.consul:9397", "timeout": "15s", - "hostOverride": "rva1.boulder" + "hostOverride": "rva1.boulder", + "perspective": "dadaist", + "rir": "ARIN" }, { "serverAddress": "rva1.service.consul:9498", "timeout": "15s", - "hostOverride": "rva1.boulder" + "hostOverride": "rva1.boulder", + "perspective": "surrealist", + "rir": "RIPE" }, { - "serverAddress": "rva2.service.consul:9897", + "serverAddress": "rva1.service.consul:9499", "timeout": "15s", - "hostOverride": "rva2.boulder" - }, - { - "serverAddress": "rva2.service.consul:9998", - "timeout": "15s", - "hostOverride": "rva2.boulder" + "hostOverride": "rva1.boulder", + "perspective": "cubist", + "rir": "ARIN" } ], - "maxRemoteValidationFailures": 1, "accountURIPrefixes": [ "http://boulder.service.consul:4000/acme/reg/", "http://boulder.service.consul:4001/acme/acct/" diff --git a/third-party/github.com/letsencrypt/boulder/test/config/wfe2-ratelimit-defaults.yml b/third-party/github.com/letsencrypt/boulder/test/config/wfe2-ratelimit-defaults.yml new file mode 100644 index 000000000..d934b508c --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/test/config/wfe2-ratelimit-defaults.yml @@ -0,0 +1,36 @@ +NewRegistrationsPerIPAddress: + count: 10000 + burst: 10000 + period: 168h +NewRegistrationsPerIPv6Range: + count: 99999 + burst: 99999 + period: 168h +CertificatesPerDomain: + count: 2 + burst: 2 + period: 2160h +FailedAuthorizationsPerDomainPerAccount: + count: 3 + burst: 3 + period: 5m +# The burst represents failing 40 times per day for 90 days. The count and +# period grant one "freebie" failure per day. In combination, these parameters +# mean that: +# - Failing 120 times per day results in being paused after 30.25 days +# - Failing 40 times per day results in being paused after 92.3 days +# - Failing 20 times per day results in being paused after 6.2 months +# - Failing 4 times per day results in being paused after 3.3 years +# - Failing once per day results in never being paused +FailedAuthorizationsForPausingPerDomainPerAccount: + count: 1 + burst: 3600 + period: 24h +NewOrdersPerAccount: + count: 1500 + burst: 1500 + period: 3h +CertificatesPerFQDNSet: + count: 2 + burst: 2 + period: 3h diff --git a/third-party/github.com/letsencrypt/boulder/test/config/wfe2-ratelimit-overrides.yml b/third-party/github.com/letsencrypt/boulder/test/config/wfe2-ratelimit-overrides.yml new file mode 100644 index 000000000..2bfd73980 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/test/config/wfe2-ratelimit-overrides.yml @@ -0,0 +1,60 @@ +- NewRegistrationsPerIPAddress: + burst: 1000000 + count: 1000000 + period: 168h + ids: + - id: 64.112.117.1 + comment: test +- CertificatesPerDomain: + burst: 1 + count: 1 + period: 2160h + ids: + - id: ratelimit.me + comment: Rate Limit Test Domain +- CertificatesPerDomain: + burst: 10000 + count: 10000 + period: 2160h + ids: + - id: le.wtf + comment: Let's Encrypt Test Domain + - id: le1.wtf + comment: Let's Encrypt Test Domain 1 + - id: le2.wtf + comment: Let's Encrypt Test Domain 2 + - id: le3.wtf + comment: Let's Encrypt Test Domain 3 + - id: nginx.wtf + comment: Nginx Test Domain + - id: good-caa-reserved.com + comment: Good CAA Reserved Domain + - id: bad-caa-reserved.com + comment: Bad CAA Reserved Domain + - id: ecdsa.le.wtf + comment: ECDSA Let's Encrypt Test Domain + - id: must-staple.le.wtf + comment: Must-Staple Let's Encrypt Test Domain +- CertificatesPerFQDNSet: + burst: 10000 + count: 10000 + period: 168h + ids: + - id: le.wtf + comment: Let's Encrypt Test Domain + - id: le1.wtf + comment: Let's Encrypt Test Domain 1 + - id: le2.wtf + comment: Let's Encrypt Test Domain 2 + - id: le3.wtf + comment: Let's Encrypt Test Domain 3 + - id: le.wtf,le1.wtf + comment: Let's Encrypt Test Domain, Let's Encrypt Test Domain 1 + - id: good-caa-reserved.com + comment: Good CAA Reserved Domain + - id: nginx.wtf + comment: Nginx Test Domain + - id: ecdsa.le.wtf + comment: ECDSA Let's Encrypt Test Domain + - id: must-staple.le.wtf + comment: Must-Staple Let's Encrypt Test Domain diff --git a/third-party/github.com/letsencrypt/boulder/test/config/wfe2.json b/third-party/github.com/letsencrypt/boulder/test/config/wfe2.json index 05d46fe95..51c7aa8ef 100644 --- a/third-party/github.com/letsencrypt/boulder/test/config/wfe2.json +++ b/third-party/github.com/letsencrypt/boulder/test/config/wfe2.json @@ -1,5 +1,6 @@ { "wfe": { + "timeout": "30s", "listenAddress": "0.0.0.0:4001", "TLSListenAddress": "0.0.0.0:4431", "serverCertificatePath": "test/certs/ipki/boulder/cert.pem", @@ -13,9 +14,7 @@ "directoryCAAIdentity": "happy-hacker-ca.invalid", "directoryWebsite": "https://github.com/letsencrypt/boulder", "legacyKeyIDPrefix": "http://boulder.service.consul:4000/reg/", - "goodkey": { - "blockedKeyFile": "test/example-blocked-keys.yaml" - }, + "goodkey": {}, "tls": { "caCertFile": "test/certs/ipki/minica.pem", "certFile": "test/certs/ipki/wfe.boulder/cert.pem", @@ -72,8 +71,8 @@ "noWaitForReady": true, "hostOverride": "nonce.boulder" }, - "noncePrefixKey": { - "passwordFile": "test/secrets/nonce_prefix_key" + "nonceHMACKey": { + "keyFile": "test/secrets/nonce_prefix_key" }, "chains": [ [ @@ -102,14 +101,49 @@ ] ], "staleTimeout": "5m", - "authorizationLifetimeDays": 30, - "pendingAuthorizationLifetimeDays": 7, + "limiter": { + "redis": { + "username": "boulder-wfe", + "passwordFile": "test/secrets/wfe_ratelimits_redis_password", + "lookups": [ + { + "Service": "redisratelimits", + "Domain": "service.consul" + } + ], + "lookupDNSAuthority": "consul.service.consul", + "readTimeout": "250ms", + "writeTimeout": "250ms", + "poolSize": 100, + "routeRandomly": true, + "tls": { + "caCertFile": "test/certs/ipki/minica.pem", + "certFile": "test/certs/ipki/wfe.boulder/cert.pem", + "keyFile": "test/certs/ipki/wfe.boulder/key.pem" + } + }, + "Defaults": "test/config/wfe2-ratelimit-defaults.yml", + "Overrides": "test/config/wfe2-ratelimit-overrides.yml" + }, "features": { - "ServeRenewalInfo": true + "ServeRenewalInfo": true, + "CheckIdentifiersPaused": true + }, + "certProfiles": { + "legacy": "The normal profile you know and love", + "modern": "Profile 2: Electric Boogaloo", + "shortlived": "Like modern, but smaller" + }, + "unpause": { + "hmacKey": { + "keyFile": "test/secrets/sfe_unpause_key" + }, + "jwtLifetime": "336h", + "url": "https://boulder.service.consul:4003" } }, "syslog": { - "stdoutlevel": 4, + "stdoutlevel": 7, "sysloglevel": 6 } } diff --git a/third-party/github.com/letsencrypt/boulder/test/config/zlint.toml b/third-party/github.com/letsencrypt/boulder/test/config/zlint.toml index 1ce7c7d9f..b044d1d34 100644 --- a/third-party/github.com/letsencrypt/boulder/test/config/zlint.toml +++ b/third-party/github.com/letsencrypt/boulder/test/config/zlint.toml @@ -1,18 +1,24 @@ -[e_pkilint_lint_cabf_serverauth_cert] -pkilint_addr = "http://10.77.77.9" -pkilint_timeout = 200000000 # 200 milliseconds +[e_pkimetal_lint_cabf_serverauth_cert] +addr = "http://bpkimetal:8080" +severity = "notice" +timeout = 2000000000 # 2 seconds ignore_lints = [ - # We include the CN in (almost) all of our certificates, on purpose. - # See https://github.com/letsencrypt/boulder/issues/5112 for details. - "DvSubcriberAttributeAllowanceValidator:cabf.serverauth.dv.common_name_attribute_present", - # We include the SKID in all of our certs, on purpose. - # See https://github.com/letsencrypt/boulder/issues/7446 for details. - "SubscriberExtensionAllowanceValidator:cabf.serverauth.subscriber.subject_key_identifier_extension_present", - # We compute the skid using RFC7093 Method 1, on purpose. - # See https://github.com/letsencrypt/boulder/pull/7179 for details. - "SubjectKeyIdentifierValidator:pkix.subject_key_identifier_rfc7093_method_1_identified", - # We include the keyEncipherment key usage in RSA certs, on purpose. - # It is only necessary for old versions of TLS, and is included for backwards - # compatibility. We intend to remove this in the short-lived profile. - "SubscriberKeyUsageValidator:cabf.serverauth.subscriber_rsa_digitalsignature_and_keyencipherment_present", + # We continue to include the Common Name in our "classic" profile, but have + # removed it from our "tlsserver" and "shortlived" profiles. + "pkilint:cabf.serverauth.dv.common_name_attribute_present", + "zlint:w_subject_common_name_included", + # We continue to include the SKID extension in our "classic" profile, but have + # removed it from our "tlsserver" and "shortlived" profiles. + "pkilint:cabf.serverauth.subscriber.subject_key_identifier_extension_present", + "zlint:w_ext_subject_key_identifier_not_recommended_subscriber", + # We continue to include the Key Encipherment Key Usage for RSA certificates + # issued under the "classic" profile, but have removed it from our "tlsserver" + # and "shortlived" profiles. + "pkilint:cabf.serverauth.subscriber_rsa_digitalsignature_and_keyencipherment_present", ] + +[e_pkimetal_lint_cabf_serverauth_crl] +addr = "http://bpkimetal:8080" +severity = "notice" +timeout = 2000000000 # 2 seconds +ignore_lints = [] diff --git a/third-party/github.com/letsencrypt/boulder/test/consul/README.md b/third-party/github.com/letsencrypt/boulder/test/consul/README.md index 0fb228957..a66276fe0 100644 --- a/third-party/github.com/letsencrypt/boulder/test/consul/README.md +++ b/third-party/github.com/letsencrypt/boulder/test/consul/README.md @@ -66,7 +66,7 @@ in-memory server and client with persistence disabled for ease of use. ### Linux -Consul should be accessible at http://10.55.55.10:8500. +Consul should be accessible at http://10.77.77.10:8500. ### Mac @@ -76,14 +76,14 @@ to add the following port lines (temporarily) to `docker-compose.yml`: ```yaml bconsul: ports: - - 8500:8500 # forwards 127.0.0.1:8500 -> 10.55.55.10:8500 + - 8500:8500 # forwards 127.0.0.1:8500 -> 10.77.77.10:8500 ``` For testing DNS resolution locally using `dig` you'll need to add the following: ```yaml bconsul: ports: - - 53:53/udp # forwards 127.0.0.1:53 -> 10.55.55.10:53 + - 53:53/udp # forwards 127.0.0.1:53 -> 10.77.77.10:53 ``` The next time you bring the container up you should be able to access the web UI diff --git a/third-party/github.com/letsencrypt/boulder/test/consul/config.hcl b/third-party/github.com/letsencrypt/boulder/test/consul/config.hcl index 08e3c2d1d..a296e1549 100644 --- a/third-party/github.com/letsencrypt/boulder/test/consul/config.hcl +++ b/third-party/github.com/letsencrypt/boulder/test/consul/config.hcl @@ -1,7 +1,7 @@ # Keep this file in sync with the ports bound in test/startservers.py client_addr = "0.0.0.0" -bind_addr = "10.55.55.10" +bind_addr = "10.77.77.10" log_level = "ERROR" // When set, uses a subset of the agent's TLS configuration (key_file, // cert_file, ca_file, ca_path, and server_name) to set up the client for HTTP @@ -33,6 +33,14 @@ services { tags = ["tcp"] // Required for SRV RR support in gRPC DNS resolution. } +services { + id = "email-exporter-a" + name = "email-exporter" + address = "10.77.77.77" + port = 9603 + tags = ["tcp"] // Required for SRV RR support in gRPC DNS resolution. +} + services { id = "boulder-a" name = "boulder" @@ -144,6 +152,22 @@ services { tags = ["tcp"] // Required for SRV RR support in gRPC DNS resolution. } +services { + id = "ra-sct-provider-a" + name = "ra-sct-provider" + address = "10.77.77.77" + port = 9594 + tags = ["tcp"] // Required for SRV RR support in gRPC DNS resolution. +} + +services { + id = "ra-sct-provider-b" + name = "ra-sct-provider" + address = "10.77.77.77" + port = 9694 + tags = ["tcp"] // Required for SRV RR support in gRPC DNS resolution. +} + services { id = "ra-a" name = "ra" @@ -176,6 +200,14 @@ services { tags = ["tcp"] // Required for SRV RR support in gRPC DNS resolution. } +services { + id = "rva1-c" + name = "rva1" + address = "10.77.77.77" + port = 9499 + tags = ["tcp"] // Required for SRV RR support in gRPC DNS resolution. +} + # TODO(#5294) Remove rva2-a/b in favor of rva1-a/b services { id = "rva2-a" @@ -286,7 +318,7 @@ services { services { id = "bredis3" name = "redisratelimits" - address = "10.33.33.4" + address = "10.77.77.4" port = 4218 tags = ["tcp"] // Required for SRV RR support in DNS resolution. } @@ -294,7 +326,7 @@ services { services { id = "bredis4" name = "redisratelimits" - address = "10.33.33.5" + address = "10.77.77.5" port = 4218 tags = ["tcp"] // Required for SRV RR support in DNS resolution. } diff --git a/third-party/github.com/letsencrypt/boulder/test/ct-test-srv/Dockerfile b/third-party/github.com/letsencrypt/boulder/test/ct-test-srv/Dockerfile new file mode 100644 index 000000000..c336e13e6 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/test/ct-test-srv/Dockerfile @@ -0,0 +1,26 @@ +# syntax=docker/dockerfile:1 +ARG GO_VERSION + +FROM golang:${GO_VERSION} AS build + +WORKDIR /app + +COPY go.mod go.sum vendor ./ + +COPY . . + +RUN go build -o /bin/ct-test-srv ./test/ct-test-srv/main.go + +FROM ubuntu:24.04 + +RUN useradd -r -u 10001 cttest + +COPY --from=build /bin/ct-test-srv /bin/ct-test-srv + +COPY test/ct-test-srv/ct-test-srv.json /etc/ct-test-srv.json + +ENTRYPOINT ["/bin/ct-test-srv"] + +USER cttest + +CMD ["-config", "/etc/ct-test-srv.json"] diff --git a/third-party/github.com/letsencrypt/boulder/test/ct-test-srv/log_list.json b/third-party/github.com/letsencrypt/boulder/test/ct-test-srv/log_list.json index 5a8af2d76..085bf53a5 100644 --- a/third-party/github.com/letsencrypt/boulder/test/ct-test-srv/log_list.json +++ b/third-party/github.com/letsencrypt/boulder/test/ct-test-srv/log_list.json @@ -17,7 +17,7 @@ }, "state": { "usable": { - "timestamp": "2000-00-00T00:00:00Z" + "timestamp": "2000-01-01T00:00:00Z" } } }, @@ -32,7 +32,7 @@ }, "state": { "usable": { - "timestamp": "2000-00-00T00:00:00Z" + "timestamp": "2000-01-01T00:00:00Z" } } }, @@ -47,7 +47,7 @@ }, "state": { "usable": { - "timestamp": "2000-00-00T00:00:00Z" + "timestamp": "2000-01-01T00:00:00Z" } } }, @@ -62,7 +62,7 @@ }, "state": { "usable": { - "timestamp": "2000-00-00T00:00:00Z" + "timestamp": "2000-01-01T00:00:00Z" } } } @@ -83,7 +83,7 @@ }, "state": { "usable": { - "timestamp": "2000-00-00T00:00:00Z" + "timestamp": "2000-01-01T00:00:00Z" } } }, @@ -98,7 +98,7 @@ }, "state": { "usable": { - "timestamp": "2000-00-00T00:00:00Z" + "timestamp": "2000-01-01T00:00:00Z" } } } @@ -115,7 +115,7 @@ "url": "http://boulder.service.consul:4606", "state": { "usable": { - "timestamp": "2000-00-00T00:00:00Z" + "timestamp": "2000-01-01T00:00:00Z" } } } @@ -136,7 +136,7 @@ }, "state": { "usable": { - "timestamp": "2000-00-00T00:00:00Z" + "timestamp": "2000-01-01T00:00:00Z" } } } @@ -186,8 +186,8 @@ "logs": [ { "description": "This Log Has Every Field To Ensure We Can Parse It", - "log_id": "BaseSixtyFourEncodingOfSHA256HashOfPublicKey=", - "key": "BaseSixtyFourEncodingOfDEREncodingOfPublicKey=", + "log_id": "ZqBFtFIQLFnYQOwJfVnZRn4To/NPZJTlOf/TLBuzXxg=", + "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEMVjHUOxzh2flagPhuEYy/AhAlpD9qqACg4fGcCxOhLU35r21CQXzKDdCHMu69QDFd6EAe8iGFsybg+Yn4/njtA==", "url": "https://example.com/ct/", "mmd": 86400, "state": { @@ -206,8 +206,8 @@ }, { "description": "This Log Is Missing State To Ensure We Can Handle It", - "log_id": "SomeOtherFakeLogID=", - "key": "SomeOtherFakeKey=", + "log_id": "gw0pzEo2G0THdJlm0i80NqV+qn0i9GnbcaBvhQOFxNc=", + "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEMVjHUOxzh2flaFPhuEYy/AhAlpD9qqzHg4fGcCxOhLU39r21CQXzKDdCHMu69QDFd6EAe8iGFsybg+Yn4/njtA==", "url": "https://example.net/ct/", "mmd": 86400, "temporal_interval": { diff --git a/third-party/github.com/letsencrypt/boulder/test/ct-test-srv/main.go b/third-party/github.com/letsencrypt/boulder/test/ct-test-srv/main.go index 564ad85f7..df1408e91 100644 --- a/third-party/github.com/letsencrypt/boulder/test/ct-test-srv/main.go +++ b/third-party/github.com/letsencrypt/boulder/test/ct-test-srv/main.go @@ -13,7 +13,7 @@ import ( "fmt" "io" "log" - "math/rand" + "math/rand/v2" "net/http" "os" "strings" @@ -161,7 +161,7 @@ func (is *integrationSrv) addChainOrPre(w http.ResponseWriter, r *http.Request, is.submissions[hostnames]++ is.Unlock() - if is.flakinessRate != 0 && rand.Intn(100) < is.flakinessRate { + if is.flakinessRate != 0 && rand.IntN(100) < is.flakinessRate { time.Sleep(10 * time.Second) } @@ -228,16 +228,13 @@ func runPersonality(p Personality) { m.HandleFunc("/ct/v1/add-chain", is.addChain) m.HandleFunc("/add-reject-host", is.addRejectHost) m.HandleFunc("/get-rejections", is.getRejections) - // The gosec linter complains that ReadHeaderTimeout is not set. That's fine, - // because this is test-only code. - ////nolint:gosec - srv := &http.Server{ + srv := &http.Server{ //nolint: gosec // No ReadHeaderTimeout is fine for test-only code. Addr: p.Addr, Handler: m, } logID := sha256.Sum256(pubKeyBytes) - log.Printf("ct-test-srv on %s with pubkey %s and log ID %s", p.Addr, - base64.StdEncoding.EncodeToString(pubKeyBytes), base64.StdEncoding.EncodeToString(logID[:])) + log.Printf("ct-test-srv on %s with pubkey: %s, log ID: %s, flakiness: %d%%", p.Addr, + base64.StdEncoding.EncodeToString(pubKeyBytes), base64.StdEncoding.EncodeToString(logID[:]), p.FlakinessRate) log.Fatal(srv.ListenAndServe()) } diff --git a/third-party/github.com/letsencrypt/boulder/test/db.go b/third-party/github.com/letsencrypt/boulder/test/db.go index 26212133f..bd778a793 100644 --- a/third-party/github.com/letsencrypt/boulder/test/db.go +++ b/third-party/github.com/letsencrypt/boulder/test/db.go @@ -113,6 +113,7 @@ func allTableNamesInDB(ctx context.Context, db CleanUpDB) ([]string, error) { if err != nil { return nil, err } + defer r.Close() var ts []string for r.Next() { tableName := "" diff --git a/third-party/github.com/letsencrypt/boulder/test/entrypoint.sh b/third-party/github.com/letsencrypt/boulder/test/entrypoint.sh index 12d0397c4..1d8c363c5 100644 --- a/third-party/github.com/letsencrypt/boulder/test/entrypoint.sh +++ b/third-party/github.com/letsencrypt/boulder/test/entrypoint.sh @@ -8,7 +8,7 @@ DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" # already present, which prevents the whole container from starting. We remove # it just in case it's there. rm -f /var/run/rsyslogd.pid -service rsyslog start +rsyslogd # make sure we can reach the mysqldb. ./test/wait-for-it.sh boulder-mysql 3306 @@ -16,6 +16,9 @@ service rsyslog start # make sure we can reach the proxysql. ./test/wait-for-it.sh bproxysql 6032 +# make sure we can reach pkilint +./test/wait-for-it.sh bpkimetal 8080 + # create the database MYSQL_CONTAINER=1 $DIR/create_db.sh diff --git a/third-party/github.com/letsencrypt/boulder/test/example-blocked-keys.yaml b/third-party/github.com/letsencrypt/boulder/test/example-blocked-keys.yaml deleted file mode 100644 index 2c0c3a47e..000000000 --- a/third-party/github.com/letsencrypt/boulder/test/example-blocked-keys.yaml +++ /dev/null @@ -1,26 +0,0 @@ -# -# List of blocked keys -# -# Each blocked entry is a Base64 encoded SHA256 hash of a SubjectPublicKeyInfo. -# -# Use the test/block-a-key utility to generate new additions. -# -# NOTE: This list is loaded all-at-once in-memory by Boulder and is intended -# to be used infrequently. Alternative mechanisms should be explored if -# large scale blocks are required. -# -blocked: - # test/block-a-key/test/test.ecdsa.cert.pem - - cuwGhNNI6nfob5aqY90e7BleU6l7rfxku4X3UTJ3Z7M= - # test/block-a-key/test/test.rsa.cert.pem - - Qebc1V3SkX3izkYRGNJilm9Bcuvf0oox4U2Rn+b4JOE= - # test/block-a-key/test/test.ecdsa.jwk.json - - cuwGhNNI6nfob5aqY90e7BleU6l7rfxku4X3UTJ3Z7M= - # test/block-a-key/test/test.rsa.jwk.json - - Qebc1V3SkX3izkYRGNJilm9Bcuvf0oox4U2Rn+b4JOE= - # test/hierarchy/int-r4.cert.pem - - +//lPMatuGvtf7yesXNv6FSf0UovKbP3BKdQZ23L4BY= -blockedHashesHex: - - 41e6dcd55dd2917de2ce461118d262966f4172ebdfd28a31e14d919fe6f824e1 - - diff --git a/third-party/github.com/letsencrypt/boulder/test/example-weak-keys.json b/third-party/github.com/letsencrypt/boulder/test/example-weak-keys.json deleted file mode 100644 index bf6548988..000000000 --- a/third-party/github.com/letsencrypt/boulder/test/example-weak-keys.json +++ /dev/null @@ -1,16 +0,0 @@ -[ - "0002a4226a4043426396", - "0002beb9288f6c0140cf", - "00006aa0ce2cd60e6660", - "00015b6662ff95aefa3f", - "00015e77627966ce16e7", - "000220bb2bcbc060b8da", - "00024ac71844e42b0fa6", - "00026532237f74a48943", - "00029956ea9997f257e1", - "0002a4ba3cf408927759", - "00008be7025d9f1a9088", - "0001313db46d8945bba0", - "000169a60c9eb82a558b", - "00008f7e6a29aea0b430" -] \ No newline at end of file diff --git a/third-party/github.com/letsencrypt/boulder/test/hostname-policy.yaml b/third-party/github.com/letsencrypt/boulder/test/hostname-policy.yaml index 88730260f..d7bfce22d 100644 --- a/third-party/github.com/letsencrypt/boulder/test/hostname-policy.yaml +++ b/third-party/github.com/letsencrypt/boulder/test/hostname-policy.yaml @@ -14,7 +14,7 @@ ExactBlockedNames: # all subdomains/wildcards. HighRiskBlockedNames: # See RFC 3152 - - "ipv6.arpa" + - "ip6.arpa" # See RFC 2317 - "in-addr.arpa" # Etc etc etc diff --git a/third-party/github.com/letsencrypt/boulder/test/inmem/sa/sa.go b/third-party/github.com/letsencrypt/boulder/test/inmem/sa/sa.go index 4df3017b9..a558aa671 100644 --- a/third-party/github.com/letsencrypt/boulder/test/inmem/sa/sa.go +++ b/third-party/github.com/letsencrypt/boulder/test/inmem/sa/sa.go @@ -29,15 +29,7 @@ func (sa SA) GetRegistration(ctx context.Context, req *sapb.RegistrationID, _ .. return sa.Impl.GetRegistration(ctx, req) } -func (sa SA) CountRegistrationsByIP(ctx context.Context, req *sapb.CountRegistrationsByIPRequest, _ ...grpc.CallOption) (*sapb.Count, error) { - return sa.Impl.CountRegistrationsByIP(ctx, req) -} - -func (sa SA) CountRegistrationsByIPRange(ctx context.Context, req *sapb.CountRegistrationsByIPRequest, _ ...grpc.CallOption) (*sapb.Count, error) { - return sa.Impl.CountRegistrationsByIPRange(ctx, req) -} - -func (sa SA) DeactivateRegistration(ctx context.Context, req *sapb.RegistrationID, _ ...grpc.CallOption) (*emptypb.Empty, error) { +func (sa SA) DeactivateRegistration(ctx context.Context, req *sapb.RegistrationID, _ ...grpc.CallOption) (*corepb.Registration, error) { return sa.Impl.DeactivateRegistration(ctx, req) } @@ -49,10 +41,6 @@ func (sa SA) GetAuthorizations2(ctx context.Context, req *sapb.GetAuthorizations return sa.Impl.GetAuthorizations2(ctx, req) } -func (sa SA) GetPendingAuthorization2(ctx context.Context, req *sapb.GetPendingAuthorizationRequest, _ ...grpc.CallOption) (*corepb.Authorization, error) { - return sa.Impl.GetPendingAuthorization2(ctx, req) -} - func (sa SA) GetValidAuthorizations2(ctx context.Context, req *sapb.GetValidAuthorizationsRequest, _ ...grpc.CallOption) (*sapb.Authorizations, error) { return sa.Impl.GetValidAuthorizations2(ctx, req) } @@ -85,10 +73,6 @@ func (sa SA) GetOrderForNames(ctx context.Context, req *sapb.GetOrderForNamesReq return sa.Impl.GetOrderForNames(ctx, req) } -func (sa SA) CountOrders(ctx context.Context, req *sapb.CountOrdersRequest, _ ...grpc.CallOption) (*sapb.Count, error) { - return sa.Impl.CountOrders(ctx, req) -} - func (sa SA) SetOrderError(ctx context.Context, req *sapb.SetOrderErrorRequest, _ ...grpc.CallOption) (*emptypb.Empty, error) { return sa.Impl.SetOrderError(ctx, req) } @@ -109,10 +93,6 @@ func (sa SA) AddCertificate(ctx context.Context, req *sapb.AddCertificateRequest return sa.Impl.AddCertificate(ctx, req) } -func (sa SA) CountCertificatesByNames(ctx context.Context, req *sapb.CountCertificatesByNamesRequest, _ ...grpc.CallOption) (*sapb.CountByNames, error) { - return sa.Impl.CountCertificatesByNames(ctx, req) -} - func (sa SA) RevokeCertificate(ctx context.Context, req *sapb.RevokeCertificateRequest, _ ...grpc.CallOption) (*emptypb.Empty, error) { return sa.Impl.RevokeCertificate(ctx, req) } @@ -133,6 +113,14 @@ func (sa SA) FQDNSetExists(ctx context.Context, req *sapb.FQDNSetExistsRequest, return sa.Impl.FQDNSetExists(ctx, req) } +func (sa SA) FQDNSetTimestampsForWindow(ctx context.Context, req *sapb.CountFQDNSetsRequest, _ ...grpc.CallOption) (*sapb.Timestamps, error) { + return sa.Impl.FQDNSetTimestampsForWindow(ctx, req) +} + +func (sa SA) PauseIdentifiers(ctx context.Context, req *sapb.PauseRequest, _ ...grpc.CallOption) (*sapb.PauseIdentifiersResponse, error) { + return sa.Impl.PauseIdentifiers(ctx, req) +} + type mockStreamResult[T any] struct { val T err error diff --git a/third-party/github.com/letsencrypt/boulder/test/integration-test.py b/third-party/github.com/letsencrypt/boulder/test/integration-test.py index af4aa3860..18b0452e9 100644 --- a/third-party/github.com/letsencrypt/boulder/test/integration-test.py +++ b/third-party/github.com/letsencrypt/boulder/test/integration-test.py @@ -34,7 +34,7 @@ race_detection = True if os.environ.get('RACE', 'true') != 'true': race_detection = False -def run_go_tests(filterPattern=None): +def run_go_tests(filterPattern=None,verbose=False): """ run_go_tests launches the Go integration tests. The go test command must return zero or an exception will be raised. If the filterPattern is provided @@ -43,7 +43,10 @@ def run_go_tests(filterPattern=None): cmdLine = ["go", "test"] if filterPattern is not None and filterPattern != "": cmdLine = cmdLine + ["--test.run", filterPattern] - cmdLine = cmdLine + ["-tags", "integration", "-count=1", "-race", "./test/integration"] + cmdLine = cmdLine + ["-tags", "integration", "-count=1", "-race"] + if verbose: + cmdLine = cmdLine + ["-v"] + cmdLine = cmdLine + ["./test/integration"] subprocess.check_call(cmdLine, stderr=subprocess.STDOUT) exit_status = 1 @@ -54,6 +57,8 @@ def main(): help="run integration tests using chisel") parser.add_argument('--gotest', dest="run_go", action="store_true", help="run Go integration tests") + parser.add_argument('--gotestverbose', dest="run_go_verbose", action="store_true", + help="run Go integration tests with verbose output") parser.add_argument('--filter', dest="test_case_filter", action="store", help="Regex filter for test cases") # allow any ACME client to run custom command for integration @@ -90,7 +95,10 @@ def main(): run_chisel(args.test_case_filter) if args.run_go: - run_go_tests(args.test_case_filter) + run_go_tests(args.test_case_filter, False) + + if args.run_go_verbose: + run_go_tests(args.test_case_filter, True) if args.custom: run(args.custom.split()) diff --git a/third-party/github.com/letsencrypt/boulder/test/integration/account_test.go b/third-party/github.com/letsencrypt/boulder/test/integration/account_test.go new file mode 100644 index 000000000..cf92764fc --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/test/integration/account_test.go @@ -0,0 +1,170 @@ +//go:build integration + +package integration + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "strings" + "testing" + + "github.com/eggsampler/acme/v3" + + "github.com/letsencrypt/boulder/core" +) + +// TestNewAccount tests that various new-account requests are handled correctly. +// It does not test malformed account contacts, as we no longer care about +// how well-formed the contact string is, since we no longer store them. +func TestNewAccount(t *testing.T) { + t.Parallel() + + c, err := acme.NewClient("http://boulder.service.consul:4001/directory") + if err != nil { + t.Fatalf("failed to connect to acme directory: %s", err) + } + + for _, tc := range []struct { + name string + tos bool + contact []string + wantErr string + }{ + { + name: "No TOS agreement", + tos: false, + contact: nil, + wantErr: "must agree to terms of service", + }, + { + name: "No contacts", + tos: true, + contact: nil, + }, + { + name: "One contact", + tos: true, + contact: []string{"mailto:single@chisel.com"}, + }, + { + name: "Many contacts", + tos: true, + contact: []string{"mailto:one@chisel.com", "mailto:two@chisel.com", "mailto:three@chisel.com"}, + }, + } { + t.Run(tc.name, func(t *testing.T) { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatalf("failed to generate account key: %s", err) + } + + acct, err := c.NewAccount(key, false, tc.tos, tc.contact...) + + if tc.wantErr == "" { + if err != nil { + t.Fatalf("NewAccount(tos: %t, contact: %#v) = %s, but want no err", tc.tos, tc.contact, err) + } + + if len(acct.Contact) != 0 { + t.Errorf("NewAccount(tos: %t, contact: %#v) = %#v, but want empty contacts", tc.tos, tc.contact, acct) + } + } else if tc.wantErr != "" { + if err == nil { + t.Fatalf("NewAccount(tos: %t, contact: %#v) = %#v, but want error %q", tc.tos, tc.contact, acct, tc.wantErr) + } + + if !strings.Contains(err.Error(), tc.wantErr) { + t.Errorf("NewAccount(tos: %t, contact: %#v) = %q, but want error %q", tc.tos, tc.contact, err, tc.wantErr) + } + } + }) + } +} + +func TestNewAccount_DuplicateKey(t *testing.T) { + t.Parallel() + + c, err := acme.NewClient("http://boulder.service.consul:4001/directory") + if err != nil { + t.Fatalf("failed to connect to acme directory: %s", err) + } + + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatalf("failed to generate account key: %s", err) + } + + // OnlyReturnExisting: true with a never-before-used key should result in an error. + acct, err := c.NewAccount(key, true, true) + if err == nil { + t.Fatalf("NewAccount(key: 1, ore: true) = %#v, but want error notFound", acct) + } + + // Create an account. + acct, err = c.NewAccount(key, false, true) + if err != nil { + t.Fatalf("NewAccount(key: 1, ore: false) = %#v, but want success", err) + } + + // A duplicate request should just return the same account. + acct, err = c.NewAccount(key, false, true) + if err != nil { + t.Fatalf("NewAccount(key: 1, ore: false) = %#v, but want success", err) + } + + // Specifying OnlyReturnExisting should do the same. + acct, err = c.NewAccount(key, true, true) + if err != nil { + t.Fatalf("NewAccount(key: 1, ore: true) = %#v, but want success", err) + } + + // Deactivate the account. + acct, err = c.DeactivateAccount(acct) + if err != nil { + t.Fatalf("DeactivateAccount(acct: 1) = %#v, but want success", err) + } + + // Now a new account request should return an error. + acct, err = c.NewAccount(key, false, true) + if err == nil { + t.Fatalf("NewAccount(key: 1, ore: false) = %#v, but want error deactivated", acct) + } + + // Specifying OnlyReturnExisting should do the same. + acct, err = c.NewAccount(key, true, true) + if err == nil { + t.Fatalf("NewAccount(key: 1, ore: true) = %#v, but want error deactivated", acct) + } +} + +// TestAccountDeactivate tests that account deactivation works. It does not test +// that we reject requests for other account statuses, because eggsampler/acme +// wisely does not allow us to construct such malformed requests. +func TestAccountDeactivate(t *testing.T) { + t.Parallel() + + c, err := acme.NewClient("http://boulder.service.consul:4001/directory") + if err != nil { + t.Fatalf("failed to connect to acme directory: %s", err) + } + + acctKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatalf("failed to generate account key: %s", err) + } + + account, err := c.NewAccount(acctKey, false, true, "mailto:hello@blackhole.net") + if err != nil { + t.Fatalf("failed to create initial account: %s", err) + } + + got, err := c.DeactivateAccount(account) + if err != nil { + t.Errorf("unexpected error while deactivating account: %s", err) + } + + if got.Status != string(core.StatusDeactivated) { + t.Errorf("account deactivation should have set status to %q, instead got %q", core.StatusDeactivated, got.Status) + } +} diff --git a/third-party/github.com/letsencrypt/boulder/test/integration/admin_test.go b/third-party/github.com/letsencrypt/boulder/test/integration/admin_test.go deleted file mode 100644 index 9313f8197..000000000 --- a/third-party/github.com/letsencrypt/boulder/test/integration/admin_test.go +++ /dev/null @@ -1,60 +0,0 @@ -//go:build integration - -package integration - -import ( - "fmt" - "os" - "os/exec" - "testing" - - "github.com/eggsampler/acme/v3" - _ "github.com/go-sql-driver/mysql" - - "github.com/letsencrypt/boulder/test" -) - -func TestAdminClearEmail(t *testing.T) { - t.Parallel() - os.Setenv("DIRECTORY", "http://boulder.service.consul:4001/directory") - - // Note that `example@mail.example.letsencrypt.org` is a substring of `long-example@mail.example.letsencrypt.org`. - // We specifically want to test that the superstring does not get removed, even though we use substring matching - // as an initial filter. - client1, err := makeClient("mailto:example@mail.example.letsencrypt.org", "mailto:long-example@mail.example.letsencrypt.org", "mailto:third-example@mail.example.letsencrypt.org") - test.AssertNotError(t, err, "creating first acme client") - - client2, err := makeClient("mailto:example@mail.example.letsencrypt.org") - test.AssertNotError(t, err, "creating second acme client") - - client3, err := makeClient("mailto:other@mail.example.letsencrypt.org") - test.AssertNotError(t, err, "creating second acme client") - - deleteMe := "example@mail.example.letsencrypt.org" - config := fmt.Sprintf("%s/%s", os.Getenv("BOULDER_CONFIG_DIR"), "admin.json") - cmd := exec.Command( - "./bin/admin", - "-config", config, - "-dry-run=false", - "update-email", - "-address", deleteMe, - "-clear") - output, err := cmd.CombinedOutput() - test.AssertNotError(t, err, fmt.Sprintf("clearing email via admin tool (%s): %s", cmd, string(output))) - t.Logf("clear-email output: %s\n", string(output)) - - updatedAccount1, err := client1.NewAccountOptions(client1.PrivateKey, acme.NewAcctOptOnlyReturnExisting()) - test.AssertNotError(t, err, "fetching updated account for first client") - - t.Log(updatedAccount1.Contact) - test.AssertDeepEquals(t, updatedAccount1.Contact, - []string{"mailto:long-example@mail.example.letsencrypt.org", "mailto:third-example@mail.example.letsencrypt.org"}) - - updatedAccount2, err := client2.NewAccountOptions(client2.PrivateKey, acme.NewAcctOptOnlyReturnExisting()) - test.AssertNotError(t, err, "fetching updated account for second client") - test.AssertDeepEquals(t, updatedAccount2.Contact, []string(nil)) - - updatedAccount3, err := client3.NewAccountOptions(client3.PrivateKey, acme.NewAcctOptOnlyReturnExisting()) - test.AssertNotError(t, err, "fetching updated account for third client") - test.AssertDeepEquals(t, updatedAccount3.Contact, []string{"mailto:other@mail.example.letsencrypt.org"}) -} diff --git a/third-party/github.com/letsencrypt/boulder/test/integration/ari_test.go b/third-party/github.com/letsencrypt/boulder/test/integration/ari_test.go index 70fb1c4a0..202b38b69 100644 --- a/third-party/github.com/letsencrypt/boulder/test/integration/ari_test.go +++ b/third-party/github.com/letsencrypt/boulder/test/integration/ari_test.go @@ -25,14 +25,12 @@ type certID struct { SerialNumber *big.Int } -func TestARI(t *testing.T) { +func TestARIAndReplacement(t *testing.T) { t.Parallel() - // Create an account. + // Setup client, err := makeClient("mailto:example@letsencrypt.org") test.AssertNotError(t, err, "creating acme client") - - // Create a private key. key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) test.AssertNotError(t, err, "creating random cert key") @@ -40,62 +38,112 @@ func TestARI(t *testing.T) { // the retry-after header are approximately the right amount of time in the // future. name := random_domain() - ir, err := authAndIssue(client, key, []string{name}, true) + ir, err := authAndIssue(client, key, []acme.Identifier{{Type: "dns", Value: name}}, true, "") test.AssertNotError(t, err, "failed to issue test cert") cert := ir.certs[0] ari, err := client.GetRenewalInfo(cert) test.AssertNotError(t, err, "ARI request should have succeeded") - test.AssertEquals(t, ari.SuggestedWindow.Start.Sub(time.Now()).Round(time.Hour), 1415*time.Hour) - test.AssertEquals(t, ari.SuggestedWindow.End.Sub(time.Now()).Round(time.Hour), 1463*time.Hour) + test.AssertEquals(t, ari.SuggestedWindow.Start.Sub(time.Now()).Round(time.Hour), 1418*time.Hour) + test.AssertEquals(t, ari.SuggestedWindow.End.Sub(time.Now()).Round(time.Hour), 1461*time.Hour) test.AssertEquals(t, ari.RetryAfter.Sub(time.Now()).Round(time.Hour), 6*time.Hour) - // TODO(@pgporada): Clean this up when 'test/config/{sa,wfe2}.json' sets - // TrackReplacementCertificatesARI=true. - if os.Getenv("BOULDER_CONFIG_DIR") == "test/config-next" { - // Make a new order which indicates that it replaces the cert issued above. - _, order, err := makeClientAndOrder(client, key, []string{name}, true, cert) - test.AssertNotError(t, err, "failed to issue test cert") - replaceID, err := acme.GenerateARICertID(cert) - test.AssertNotError(t, err, "failed to generate ARI certID") - test.AssertEquals(t, order.Replaces, replaceID) - test.AssertNotEquals(t, order.Replaces, "") + // Make a new order which indicates that it replaces the cert issued above, + // and verify that the replacement order succeeds. + _, order, err := makeClientAndOrder(client, key, []acme.Identifier{{Type: "dns", Value: name}}, true, "", cert) + test.AssertNotError(t, err, "failed to issue test cert") + replaceID, err := acme.GenerateARICertID(cert) + test.AssertNotError(t, err, "failed to generate ARI certID") + test.AssertEquals(t, order.Replaces, replaceID) + test.AssertNotEquals(t, order.Replaces, "") - // Try it again and verify it fails - _, order, err = makeClientAndOrder(client, key, []string{name}, true, cert) - test.AssertError(t, err, "subsequent ARI replacements for a replaced cert should fail, but didn't") + // Retrieve the order and verify that it has the correct replaces field. + resp, err := client.FetchOrder(client.Account, order.URL) + test.AssertNotError(t, err, "failed to fetch order") + if os.Getenv("BOULDER_CONFIG_DIR") == "test/config-next" { + test.AssertEquals(t, resp.Replaces, order.Replaces) } else { - // ARI is disabled so we only use the client to POST the replacement - // order, but we never finalize it. - replacementOrder, err := client.ReplacementOrder(client.Account, cert, []acme.Identifier{{Type: "dns", Value: name}}) - test.AssertNotError(t, err, "ARI replacement request should have succeeded") - test.AssertNotEquals(t, replacementOrder.Replaces, "") + test.AssertEquals(t, resp.Replaces, "") } - // Revoke the cert and re-request ARI. The renewal window should now be in - // the past indicating to the client that a renewal should happen - // immediately. + // Try another replacement order and verify that it fails. + _, order, err = makeClientAndOrder(client, key, []acme.Identifier{{Type: "dns", Value: name}}, true, "", cert) + test.AssertError(t, err, "subsequent ARI replacements for a replaced cert should fail, but didn't") + test.AssertContains(t, err.Error(), "urn:ietf:params:acme:error:alreadyReplaced") + test.AssertContains(t, err.Error(), "already has a replacement order") + test.AssertContains(t, err.Error(), "error code 409") +} + +func TestARIShortLived(t *testing.T) { + t.Parallel() + + // Setup + client, err := makeClient("mailto:example@letsencrypt.org") + test.AssertNotError(t, err, "creating acme client") + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + test.AssertNotError(t, err, "creating random cert key") + + // Issue a short-lived cert, request ARI, and check that both the suggested + // window and the retry-after header are approximately the right amount of + // time in the future. + ir, err := authAndIssue(client, key, []acme.Identifier{{Type: "dns", Value: random_domain()}}, true, "shortlived") + test.AssertNotError(t, err, "failed to issue test cert") + + cert := ir.certs[0] + ari, err := client.GetRenewalInfo(cert) + test.AssertNotError(t, err, "ARI request should have succeeded") + test.AssertEquals(t, ari.SuggestedWindow.Start.Sub(time.Now()).Round(time.Hour), 78*time.Hour) + test.AssertEquals(t, ari.SuggestedWindow.End.Sub(time.Now()).Round(time.Hour), 81*time.Hour) + test.AssertEquals(t, ari.RetryAfter.Sub(time.Now()).Round(time.Hour), 6*time.Hour) +} + +func TestARIRevoked(t *testing.T) { + t.Parallel() + + // Setup + client, err := makeClient("mailto:example@letsencrypt.org") + test.AssertNotError(t, err, "creating acme client") + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + test.AssertNotError(t, err, "creating random cert key") + + // Issue a cert, revoke it, request ARI, and check that the suggested window + // is in the past, indicating that a renewal should happen immediately. + ir, err := authAndIssue(client, key, []acme.Identifier{{Type: "dns", Value: random_domain()}}, true, "") + test.AssertNotError(t, err, "failed to issue test cert") + + cert := ir.certs[0] err = client.RevokeCertificate(client.Account, cert, client.PrivateKey, 0) test.AssertNotError(t, err, "failed to revoke cert") - ari, err = client.GetRenewalInfo(cert) + ari, err := client.GetRenewalInfo(cert) test.AssertNotError(t, err, "ARI request should have succeeded") test.Assert(t, ari.SuggestedWindow.End.Before(time.Now()), "suggested window should end in the past") test.Assert(t, ari.SuggestedWindow.Start.Before(ari.SuggestedWindow.End), "suggested window should start before it ends") +} + +func TestARIForPrecert(t *testing.T) { + t.Parallel() + + // Setup + client, err := makeClient("mailto:example@letsencrypt.org") + test.AssertNotError(t, err, "creating acme client") + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + test.AssertNotError(t, err, "creating random cert key") // Try to make a new cert for a new domain, but sabotage the CT logs so - // issuance fails. Recover the precert from CT, then request ARI and check - // that it fails, because we don't serve ARI for non-issued certs. - name = random_domain() + // issuance fails. + name := random_domain() err = ctAddRejectHost(name) test.AssertNotError(t, err, "failed to add ct-test-srv reject host") - _, err = authAndIssue(client, key, []string{name}, true) + _, err = authAndIssue(client, key, []acme.Identifier{{Type: "dns", Value: name}}, true, "") test.AssertError(t, err, "expected error from authAndIssue, was nil") - cert, err = ctFindRejection([]string{name}) + // Recover the precert from CT, then request ARI and check + // that it fails, because we don't serve ARI for non-issued certs. + cert, err := ctFindRejection([]string{name}) test.AssertNotError(t, err, "failed to find rejected precert") - ari, err = client.GetRenewalInfo(cert) + _, err = client.GetRenewalInfo(cert) test.AssertError(t, err, "ARI request should have failed") test.AssertEquals(t, err.(acme.Problem).Status, 404) } diff --git a/third-party/github.com/letsencrypt/boulder/test/integration/authz_test.go b/third-party/github.com/letsencrypt/boulder/test/integration/authz_test.go index b8783b83a..1520c9d95 100644 --- a/third-party/github.com/letsencrypt/boulder/test/integration/authz_test.go +++ b/third-party/github.com/letsencrypt/boulder/test/integration/authz_test.go @@ -6,6 +6,8 @@ import ( "testing" "time" + "github.com/eggsampler/acme/v3" + "github.com/letsencrypt/boulder/test" ) @@ -24,8 +26,8 @@ func TestValidAuthzExpires(t *testing.T) { test.AssertNotError(t, err, "makeClient failed") // Issue for a random domain - domains := []string{random_domain()} - result, err := authAndIssue(c, nil, domains, true) + idents := []acme.Identifier{{Type: "dns", Value: random_domain()}} + result, err := authAndIssue(c, nil, idents, true, "") // There should be no error test.AssertNotError(t, err, "authAndIssue failed") // The order should be valid @@ -40,7 +42,8 @@ func TestValidAuthzExpires(t *testing.T) { // The authz should be valid and for the correct identifier test.AssertEquals(t, authzOb.Status, "valid") - test.AssertEquals(t, authzOb.Identifier.Value, domains[0]) + test.AssertEquals(t, authzOb.Identifier.Type, idents[0].Type) + test.AssertEquals(t, authzOb.Identifier.Value, idents[0].Value) // The authz should have the expected expiry date, plus or minus a minute expectedExpiresMin := time.Now().AddDate(0, 0, validAuthorizationLifetime).Add(-time.Minute) diff --git a/third-party/github.com/letsencrypt/boulder/test/integration/bad_key_test.go b/third-party/github.com/letsencrypt/boulder/test/integration/bad_key_test.go index 482c04dee..e6d132c24 100644 --- a/third-party/github.com/letsencrypt/boulder/test/integration/bad_key_test.go +++ b/third-party/github.com/letsencrypt/boulder/test/integration/bad_key_test.go @@ -3,11 +3,9 @@ package integration import ( - "crypto/rand" - "crypto/rsa" "crypto/x509" - "crypto/x509/pkix" - "math/big" + "encoding/pem" + "os" "testing" "github.com/eggsampler/acme/v3" @@ -20,102 +18,52 @@ import ( func TestFermat(t *testing.T) { t.Parallel() - type testCase struct { - name string - p string - q string + // Create a client and complete an HTTP-01 challenge for a fake domain. + c, err := makeClient() + test.AssertNotError(t, err, "creating acme client") + + domain := random_domain() + + order, err := c.Client.NewOrder( + c.Account, []acme.Identifier{{Type: "dns", Value: domain}}) + test.AssertNotError(t, err, "creating new order") + test.AssertEquals(t, len(order.Authorizations), 1) + + authUrl := order.Authorizations[0] + + auth, err := c.Client.FetchAuthorization(c.Account, authUrl) + test.AssertNotError(t, err, "fetching authorization") + + chal, ok := auth.ChallengeMap[acme.ChallengeTypeHTTP01] + test.Assert(t, ok, "getting HTTP-01 challenge") + + _, err = testSrvClient.AddHTTP01Response(chal.Token, chal.KeyAuthorization) + test.AssertNotError(t, err, "") + defer func() { + _, err = testSrvClient.RemoveHTTP01Response(chal.Token) + test.AssertNotError(t, err, "") + }() + + chal, err = c.Client.UpdateChallenge(c.Account, chal) + test.AssertNotError(t, err, "updating HTTP-01 challenge") + + // Load the Fermat-weak CSR that we'll submit for finalize. This CSR was + // generated using test/integration/testdata/fermat_csr.go, has prime factors + // that differ by only 2^516 + 254, and can be factored in 42 rounds. + csrPem, err := os.ReadFile("test/integration/testdata/fermat_csr.pem") + test.AssertNotError(t, err, "reading CSR PEM from disk") + + csrDer, _ := pem.Decode(csrPem) + if csrDer == nil { + t.Fatal("failed to decode CSR PEM") } - testCases := []testCase{ - { - name: "canon printer (2048 bit, 1 round)", - p: "155536235030272749691472293262418471207550926406427515178205576891522284497518443889075039382254334975506248481615035474816604875321501901699955105345417152355947783063521554077194367454070647740704883461064399268622437721385112646454393005862535727615809073410746393326688230040267160616554768771412289114449", - q: "155536235030272749691472293262418471207550926406427515178205576891522284497518443889075039382254334975506248481615035474816604875321501901699955105345417152355947783063521554077194367454070647740704883461064399268622437721385112646454393005862535727615809073410746393326688230040267160616554768771412289114113", - }, - { - name: "innsbruck printer (4096 bit, 1 round)", - p: "25868808535211632564072019392873831934145242707953960515208595626279836366691068618582894100813803673421320899654654938470888358089618966238341690624345530870988951109006149164192566967552401505863871260691612081236189439839963332690997129144163260418447718577834226720411404568398865166471102885763673744513186211985402019037772108416694793355840983833695882936201196462579254234744648546792097397517107797153785052856301942321429858537224127598198913168345965493941246097657533085617002572245972336841716321849601971924830462771411171570422802773095537171762650402420866468579928479284978914972383512240254605625661", - q: "25868808535211632564072019392873831934145242707953960515208595626279836366691068618582894100813803673421320899654654938470888358089618966238341690624345530870988951109006149164192566967552401505863871260691612081236189439839963332690997129144163260418447718577834226720411404568398865166471102885763673744513186211985402019037772108416694793355840983833695882936201196462579254234744648546792097397517107797153785052856301942321429858537224127598198913168345965493941246097657533085617002572245972336841716321849601971924830462771411171570422802773095537171762650402420866468579928479284978914972383512240254605624819", - }, - // Ideally we'd have a 2408-bit, nearly-100-rounds test case, but it turns - // out purposefully generating keys that require 1 < N < 100 rounds to be - // factored is surprisingly tricky. - } + csr, err := x509.ParseCertificateRequest(csrDer.Bytes) + test.AssertNotError(t, err, "parsing CSR") - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - // Create a client and complete an HTTP-01 challenge for a fake domain. - c, err := makeClient() - test.AssertNotError(t, err, "creating acme client") - - domain := random_domain() - - order, err := c.Client.NewOrder( - c.Account, []acme.Identifier{{Type: "dns", Value: domain}}) - test.AssertNotError(t, err, "creating new order") - test.AssertEquals(t, len(order.Authorizations), 1) - - authUrl := order.Authorizations[0] - - auth, err := c.Client.FetchAuthorization(c.Account, authUrl) - test.AssertNotError(t, err, "fetching authorization") - - chal, ok := auth.ChallengeMap[acme.ChallengeTypeHTTP01] - test.Assert(t, ok, "getting HTTP-01 challenge") - - err = addHTTP01Response(chal.Token, chal.KeyAuthorization) - defer delHTTP01Response(chal.Token) - test.AssertNotError(t, err, "adding HTTP-01 response") - - chal, err = c.Client.UpdateChallenge(c.Account, chal) - test.AssertNotError(t, err, "updating HTTP-01 challenge") - - // Reconstruct the public modulus N from the test case's prime factors. - p, ok := new(big.Int).SetString(tc.p, 10) - test.Assert(t, ok, "failed to create large prime") - q, ok := new(big.Int).SetString(tc.q, 10) - test.Assert(t, ok, "failed to create large prime") - n := new(big.Int).Mul(p, q) - - // Reconstruct the private exponent D from the test case's prime factors. - p_1 := new(big.Int).Sub(p, big.NewInt(1)) - q_1 := new(big.Int).Sub(q, big.NewInt(1)) - field := new(big.Int).Mul(p_1, q_1) - d := new(big.Int).ModInverse(big.NewInt(65537), field) - - // Create a CSR containing the reconstructed pubkey and signed with the - // reconstructed private key. - pubkey := rsa.PublicKey{ - N: n, - E: 65537, - } - - privkey := rsa.PrivateKey{ - PublicKey: pubkey, - D: d, - Primes: []*big.Int{p, q}, - } - - csrDer, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{ - SignatureAlgorithm: x509.SHA256WithRSA, - PublicKeyAlgorithm: x509.RSA, - PublicKey: &pubkey, - Subject: pkix.Name{CommonName: domain}, - DNSNames: []string{domain}, - }, &privkey) - test.AssertNotError(t, err, "creating CSR") - - csr, err := x509.ParseCertificateRequest(csrDer) - test.AssertNotError(t, err, "parsing CSR") - - // Finalizing the order should fail as we reject the public key. - _, err = c.Client.FinalizeOrder(c.Account, order, csr) - test.AssertError(t, err, "finalizing order") - test.AssertContains(t, err.Error(), "urn:ietf:params:acme:error:badCSR") - test.AssertContains(t, err.Error(), "key generated with factors too close together") - }) - } + // Finalizing the order should fail as we reject the public key. + _, err = c.Client.FinalizeOrder(c.Account, order, csr) + test.AssertError(t, err, "finalizing order") + test.AssertContains(t, err.Error(), "urn:ietf:params:acme:error:badCSR") + test.AssertContains(t, err.Error(), "key generated with factors too close together") } diff --git a/third-party/github.com/letsencrypt/boulder/test/integration/cert_storage_failed_test.go b/third-party/github.com/letsencrypt/boulder/test/integration/cert_storage_failed_test.go index 207b15039..f79902ca1 100644 --- a/third-party/github.com/letsencrypt/boulder/test/integration/cert_storage_failed_test.go +++ b/third-party/github.com/letsencrypt/boulder/test/integration/cert_storage_failed_test.go @@ -13,10 +13,12 @@ import ( "fmt" "os" "os/exec" + "path" "strings" "testing" "time" + "github.com/eggsampler/acme/v3" _ "github.com/go-sql-driver/mysql" "golang.org/x/crypto/ocsp" @@ -29,8 +31,8 @@ import ( // getPrecertByName finds and parses a precertificate using the given hostname. // It returns the most recent one. -func getPrecertByName(db *sql.DB, name string) (*x509.Certificate, error) { - name = sa.ReverseName(name) +func getPrecertByName(db *sql.DB, reversedName string) (*x509.Certificate, error) { + reversedName = sa.EncodeIssuedName(reversedName) // Find the certificate from the precertificates table. We don't know the serial so // we have to look it up by name. var der []byte @@ -41,7 +43,7 @@ func getPrecertByName(db *sql.DB, name string) (*x509.Certificate, error) { WHERE reversedName = ? ORDER BY issuedNames.id DESC LIMIT 1 - `, name) + `, reversedName) for rows.Next() { err = rows.Scan(&der) if err != nil { @@ -49,7 +51,7 @@ func getPrecertByName(db *sql.DB, name string) (*x509.Certificate, error) { } } if der == nil { - return nil, fmt.Errorf("no precertificate found for %q", name) + return nil, fmt.Errorf("no precertificate found for %q", reversedName) } cert, err := x509.ParseCertificate(der) @@ -62,7 +64,7 @@ func getPrecertByName(db *sql.DB, name string) (*x509.Certificate, error) { // expectOCSP500 queries OCSP for the given certificate and expects a 500 error. func expectOCSP500(cert *x509.Certificate) error { - _, err := ocsp_helper.Req(cert, ocsp_helper.DefaultConfig) + _, err := ocsp_helper.Req(cert, ocspConf()) if err == nil { return errors.New("Expected error getting OCSP for certificate that failed status storage") } @@ -91,17 +93,10 @@ func expectOCSP500(cert *x509.Certificate) error { // that a final certificate exists for any precertificate, though it is // similar in spirit). func TestIssuanceCertStorageFailed(t *testing.T) { - t.Parallel() os.Setenv("DIRECTORY", "http://boulder.service.consul:4001/directory") ctx := context.Background() - // This test is gated on the StoreLintingCertificateInsteadOfPrecertificate - // feature flag. - if os.Getenv("BOULDER_CONFIG_DIR") != "test/config-next" { - t.Skip("Skipping test because it requires the StoreLintingCertificateInsteadOfPrecertificate feature flag") - } - db, err := sql.Open("mysql", vars.DBConnSAIntegrationFullPerms) test.AssertNotError(t, err, "failed to open db connection") @@ -143,7 +138,7 @@ func TestIssuanceCertStorageFailed(t *testing.T) { // ---- Test revocation by serial ---- revokeMeDomain := "revokeme.wantserror.com" // This should fail because the trigger prevented setting the certificate status to "ready" - _, err = authAndIssue(nil, certKey, []string{revokeMeDomain}, true) + _, err = authAndIssue(nil, certKey, []acme.Identifier{{Type: "dns", Value: revokeMeDomain}}, true, "") test.AssertError(t, err, "expected authAndIssue to fail") cert, err := getPrecertByName(db, revokeMeDomain) @@ -170,7 +165,7 @@ func TestIssuanceCertStorageFailed(t *testing.T) { // ---- Test revocation by key ---- blockMyKeyDomain := "blockmykey.wantserror.com" // This should fail because the trigger prevented setting the certificate status to "ready" - _, err = authAndIssue(nil, certKey, []string{blockMyKeyDomain}, true) + _, err = authAndIssue(nil, certKey, []acme.Identifier{{Type: "dns", Value: blockMyKeyDomain}}, true, "") test.AssertError(t, err, "expected authAndIssue to fail") cert, err = getPrecertByName(db, blockMyKeyDomain) @@ -183,10 +178,11 @@ func TestIssuanceCertStorageFailed(t *testing.T) { // with the same key, then revoking that certificate for keyCompromise. revokeClient, err := makeClient() test.AssertNotError(t, err, "creating second acme client") - res, err := authAndIssue(nil, certKey, []string{random_domain()}, true) + res, err := authAndIssue(nil, certKey, []acme.Identifier{{Type: "dns", Value: random_domain()}}, true, "") test.AssertNotError(t, err, "issuing second cert") successfulCert := res.certs[0] + successfulCertIssuer := res.certs[1] err = revokeClient.RevokeCertificate( revokeClient.Account, successfulCert, @@ -195,9 +191,12 @@ func TestIssuanceCertStorageFailed(t *testing.T) { ) test.AssertNotError(t, err, "revoking second certificate") + runUpdater(t, path.Join(os.Getenv("BOULDER_CONFIG_DIR"), "crl-updater.json")) + fetchAndCheckRevoked(t, successfulCert, successfulCertIssuer, ocsp.KeyCompromise) + for range 300 { _, err = ocsp_helper.Req(successfulCert, - ocsp_helper.DefaultConfig.WithExpectStatus(ocsp.Revoked).WithExpectReason(ocsp.KeyCompromise)) + ocspConf().WithExpectStatus(ocsp.Revoked).WithExpectReason(ocsp.KeyCompromise)) if err == nil { break } @@ -206,7 +205,7 @@ func TestIssuanceCertStorageFailed(t *testing.T) { test.AssertNotError(t, err, "expected status to eventually become revoked") // Try to issue again with the same key, expecting an error because of the key is blocked. - _, err = authAndIssue(nil, certKey, []string{"123.example.com"}, true) + _, err = authAndIssue(nil, certKey, []acme.Identifier{{Type: "dns", Value: "123.example.com"}}, true, "") test.AssertError(t, err, "expected authAndIssue to fail") if !strings.Contains(err.Error(), "public key is forbidden") { t.Errorf("expected issuance to be rejected with a bad pubkey") diff --git a/third-party/github.com/letsencrypt/boulder/test/integration/common_test.go b/third-party/github.com/letsencrypt/boulder/test/integration/common_test.go index 8b78a9fbf..557bc8f90 100644 --- a/third-party/github.com/letsencrypt/boulder/test/integration/common_test.go +++ b/third-party/github.com/letsencrypt/boulder/test/integration/common_test.go @@ -3,7 +3,6 @@ package integration import ( - "bytes" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" @@ -12,12 +11,16 @@ import ( "encoding/asn1" "encoding/hex" "fmt" - "net/http" + "net" "os" + challTestSrvClient "github.com/letsencrypt/boulder/test/chall-test-srv-client" + "github.com/eggsampler/acme/v3" ) +var testSrvClient = challTestSrvClient.NewClient("") + func init() { // Go tests get run in the directory their source code lives in. For these // test cases, that would be "test/integration." However, it's easier to @@ -57,39 +60,7 @@ func makeClient(contacts ...string) (*client, error) { return &client{account, c}, nil } -func addHTTP01Response(token, keyAuthorization string) error { - resp, err := http.Post("http://boulder.service.consul:8055/add-http01", "", - bytes.NewBufferString(fmt.Sprintf(`{ - "token": "%s", - "content": "%s" - }`, token, keyAuthorization))) - if err != nil { - return fmt.Errorf("adding http-01 response: %s", err) - } - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("adding http-01 response: status %d", resp.StatusCode) - } - resp.Body.Close() - return nil -} - -func delHTTP01Response(token string) error { - resp, err := http.Post("http://boulder.service.consul:8055/del-http01", "", - bytes.NewBufferString(fmt.Sprintf(`{ - "token": "%s" - }`, token))) - if err != nil { - return fmt.Errorf("deleting http-01 response: %s", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("deleting http-01 response: status %d", resp.StatusCode) - } - return nil -} - -func makeClientAndOrder(c *client, csrKey *ecdsa.PrivateKey, domains []string, cn bool, certToReplace *x509.Certificate) (*client, *acme.Order, error) { +func makeClientAndOrder(c *client, csrKey *ecdsa.PrivateKey, idents []acme.Identifier, cn bool, profile string, certToReplace *x509.Certificate) (*client, *acme.Order, error) { var err error if c == nil { c, err = makeClient() @@ -98,15 +69,11 @@ func makeClientAndOrder(c *client, csrKey *ecdsa.PrivateKey, domains []string, c } } - var ids []acme.Identifier - for _, domain := range domains { - ids = append(ids, acme.Identifier{Type: "dns", Value: domain}) - } var order acme.Order if certToReplace != nil { - order, err = c.Client.ReplacementOrder(c.Account, certToReplace, ids) + order, err = c.Client.ReplacementOrderExtension(c.Account, certToReplace, idents, acme.OrderExtension{Profile: profile}) } else { - order, err = c.Client.NewOrder(c.Account, ids) + order, err = c.Client.NewOrderExtension(c.Account, idents, acme.OrderExtension{Profile: profile}) } if err != nil { return nil, nil, err @@ -123,19 +90,22 @@ func makeClientAndOrder(c *client, csrKey *ecdsa.PrivateKey, domains []string, c return nil, nil, fmt.Errorf("no HTTP challenge at %s", authUrl) } - err = addHTTP01Response(chal.Token, chal.KeyAuthorization) + _, err = testSrvClient.AddHTTP01Response(chal.Token, chal.KeyAuthorization) if err != nil { - return nil, nil, fmt.Errorf("adding HTTP-01 response: %s", err) + return nil, nil, err } chal, err = c.Client.UpdateChallenge(c.Account, chal) if err != nil { - delHTTP01Response(chal.Token) - return nil, nil, fmt.Errorf("updating challenge: %s", err) + testSrvClient.RemoveHTTP01Response(chal.Token) + return nil, nil, err + } + _, err = testSrvClient.RemoveHTTP01Response(chal.Token) + if err != nil { + return nil, nil, err } - delHTTP01Response(chal.Token) } - csr, err := makeCSR(csrKey, domains, cn) + csr, err := makeCSR(csrKey, idents, cn) if err != nil { return nil, nil, err } @@ -153,10 +123,10 @@ type issuanceResult struct { certs []*x509.Certificate } -func authAndIssue(c *client, csrKey *ecdsa.PrivateKey, domains []string, cn bool) (*issuanceResult, error) { +func authAndIssue(c *client, csrKey *ecdsa.PrivateKey, idents []acme.Identifier, cn bool, profile string) (*issuanceResult, error) { var err error - c, order, err := makeClientAndOrder(c, csrKey, domains, cn, nil) + c, order, err := makeClientAndOrder(c, csrKey, idents, cn, profile, nil) if err != nil { return nil, err } @@ -173,8 +143,8 @@ type issuanceResultAllChains struct { certs map[string][]*x509.Certificate } -func authAndIssueFetchAllChains(c *client, csrKey *ecdsa.PrivateKey, domains []string, cn bool) (*issuanceResultAllChains, error) { - c, order, err := makeClientAndOrder(c, csrKey, domains, cn, nil) +func authAndIssueFetchAllChains(c *client, csrKey *ecdsa.PrivateKey, idents []acme.Identifier, cn bool) (*issuanceResultAllChains, error) { + c, order, err := makeClientAndOrder(c, csrKey, idents, cn, "", nil) if err != nil { return nil, err } @@ -188,7 +158,7 @@ func authAndIssueFetchAllChains(c *client, csrKey *ecdsa.PrivateKey, domains []s return &issuanceResultAllChains{*order, certs}, nil } -func makeCSR(k *ecdsa.PrivateKey, domains []string, cn bool) (*x509.CertificateRequest, error) { +func makeCSR(k *ecdsa.PrivateKey, idents []acme.Identifier, cn bool) (*x509.CertificateRequest, error) { var err error if k == nil { k, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) @@ -197,14 +167,28 @@ func makeCSR(k *ecdsa.PrivateKey, domains []string, cn bool) (*x509.CertificateR } } + var names []string + var ips []net.IP + for _, ident := range idents { + switch ident.Type { + case "dns": + names = append(names, ident.Value) + case "ip": + ips = append(ips, net.ParseIP(ident.Value)) + default: + return nil, fmt.Errorf("unrecognized identifier type %q", ident.Type) + } + } + tmpl := &x509.CertificateRequest{ SignatureAlgorithm: x509.ECDSAWithSHA256, PublicKeyAlgorithm: x509.ECDSA, PublicKey: k.Public(), - DNSNames: domains, + DNSNames: names, + IPAddresses: ips, } - if cn { - tmpl.Subject = pkix.Name{CommonName: domains[0]} + if cn && len(names) > 0 { + tmpl.Subject = pkix.Name{CommonName: names[0]} } csrDer, err := x509.CreateCertificateRequest(rand.Reader, tmpl, k) diff --git a/third-party/github.com/letsencrypt/boulder/test/integration/crl_test.go b/third-party/github.com/letsencrypt/boulder/test/integration/crl_test.go index fc7cc28a0..8e0c35a40 100644 --- a/third-party/github.com/letsencrypt/boulder/test/integration/crl_test.go +++ b/third-party/github.com/letsencrypt/boulder/test/integration/crl_test.go @@ -3,17 +3,28 @@ package integration import ( + "bytes" + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" "database/sql" + "errors" + "fmt" "io" + "net" "net/http" "os" "os/exec" "path" "path/filepath" "strings" + "sync" + "syscall" "testing" "time" + "github.com/eggsampler/acme/v3" "github.com/jmhodges/clock" "github.com/letsencrypt/boulder/core" @@ -21,10 +32,30 @@ import ( "github.com/letsencrypt/boulder/test/vars" ) +// crlUpdaterMu controls access to `runUpdater`, because two crl-updaters running +// at once will result in errors trying to lease shards that are already leased. +var crlUpdaterMu sync.Mutex + // runUpdater executes the crl-updater binary with the -runOnce flag, and // returns when it completes. func runUpdater(t *testing.T, configFile string) { t.Helper() + crlUpdaterMu.Lock() + defer crlUpdaterMu.Unlock() + + // Reset the s3-test-srv so that it only knows about serials contained in + // this new batch of CRLs. + resp, err := http.Post("http://localhost:4501/reset", "", bytes.NewReader([]byte{})) + test.AssertNotError(t, err, "opening database connection") + test.AssertEquals(t, resp.StatusCode, http.StatusOK) + + // Reset the "leasedUntil" column so this can be done alongside other + // updater runs without worrying about unclean state. + fc := clock.NewFake() + db, err := sql.Open("mysql", vars.DBConnSAIntegrationFullPerms) + test.AssertNotError(t, err, "opening database connection") + _, err = db.Exec(`UPDATE crlShards SET leasedUntil = ?`, fc.Now().Add(-time.Minute)) + test.AssertNotError(t, err, "resetting leasedUntil column") binPath, err := filepath.Abs("bin/boulder") test.AssertNotError(t, err, "computing boulder binary path") @@ -38,27 +69,80 @@ func runUpdater(t *testing.T, configFile string) { test.AssertNotError(t, err, "crl-updater failed") } +// TestCRLUpdaterStartup ensures that the crl-updater can start in daemon mode. +// We do this here instead of in startservers so that we can shut it down after +// we've confirmed it is running. It's important that it not be running while +// other CRL integration tests are running, because otherwise they fight over +// database leases, leading to flaky test failures. +func TestCRLUpdaterStartup(t *testing.T) { + t.Parallel() + + crlUpdaterMu.Lock() + defer crlUpdaterMu.Unlock() + + ctx, cancel := context.WithCancel(context.Background()) + + binPath, err := filepath.Abs("bin/boulder") + test.AssertNotError(t, err, "computing boulder binary path") + + configDir, ok := os.LookupEnv("BOULDER_CONFIG_DIR") + test.Assert(t, ok, "failed to look up test config directory") + configFile := path.Join(configDir, "crl-updater.json") + + c := exec.CommandContext(ctx, binPath, "crl-updater", "-config", configFile, "-debug-addr", ":8021") + + var wg sync.WaitGroup + wg.Add(1) + go func() { + out, err := c.CombinedOutput() + // Log the output and error, but only if the main goroutine couldn't connect + // and declared the test failed. + for _, line := range strings.Split(string(out), "\n") { + t.Log(line) + } + t.Log(err) + wg.Done() + }() + + for attempt := range 10 { + time.Sleep(core.RetryBackoff(attempt, 10*time.Millisecond, 1*time.Second, 2)) + + conn, err := net.DialTimeout("tcp", "localhost:8021", 100*time.Millisecond) + if errors.Is(err, syscall.ECONNREFUSED) { + t.Logf("Connection attempt %d failed: %s", attempt, err) + continue + } + if err != nil { + t.Logf("Connection attempt %d failed unrecoverably: %s", attempt, err) + t.Fail() + break + } + t.Logf("Connection attempt %d succeeded", attempt) + defer conn.Close() + break + } + + cancel() + wg.Wait() +} + // TestCRLPipeline runs an end-to-end test of the crl issuance process, ensuring // that the correct number of properly-formed and validly-signed CRLs are sent // to our fake S3 service. func TestCRLPipeline(t *testing.T) { // Basic setup. - fc := clock.NewFake() configDir, ok := os.LookupEnv("BOULDER_CONFIG_DIR") test.Assert(t, ok, "failed to look up test config directory") configFile := path.Join(configDir, "crl-updater.json") - // Reset the "leasedUntil" column so that this test isn't dependent on state - // like priors runs of this test. + // Create a database connection so we can pretend to jump forward in time. db, err := sql.Open("mysql", vars.DBConnSAIntegrationFullPerms) - test.AssertNotError(t, err, "opening database connection") - _, err = db.Exec(`UPDATE crlShards SET leasedUntil = ?`, fc.Now().Add(-time.Minute)) - test.AssertNotError(t, err, "resetting leasedUntil column") + test.AssertNotError(t, err, "creating database connection") // Issue a test certificate and save its serial number. client, err := makeClient() test.AssertNotError(t, err, "creating acme client") - res, err := authAndIssue(client, nil, []string{random_domain()}, true) + res, err := authAndIssue(client, nil, []acme.Identifier{{Type: "dns", Value: random_domain()}}, true, "") test.AssertNotError(t, err, "failed to create test certificate") cert := res.certs[0] serial := core.SerialToString(cert.SerialNumber) @@ -74,19 +158,133 @@ func TestCRLPipeline(t *testing.T) { err = client.RevokeCertificate(client.Account, cert, client.PrivateKey, 5) test.AssertNotError(t, err, "failed to revoke test certificate") - // Reset the "leasedUntil" column to prepare for another round of CRLs. - _, err = db.Exec(`UPDATE crlShards SET leasedUntil = ?`, fc.Now().Add(-time.Minute)) - test.AssertNotError(t, err, "resetting leasedUntil column") - - // Confirm that the cert now *does* show up in the CRLs. + // Confirm that the cert now *does* show up in the CRLs, with the right reason. runUpdater(t, configFile) resp, err = http.Get("http://localhost:4501/query?serial=" + serial) test.AssertNotError(t, err, "s3-test-srv GET /query failed") test.AssertEquals(t, resp.StatusCode, 200) - - // Confirm that the revoked certificate entry has the correct reason. reason, err := io.ReadAll(resp.Body) test.AssertNotError(t, err, "reading revocation reason") test.AssertEquals(t, string(reason), "5") resp.Body.Close() + + // Manipulate the database so it appears that the certificate is going to + // expire very soon. The cert should still appear on the CRL. + _, err = db.Exec("UPDATE revokedCertificates SET notAfterHour = ? WHERE serial = ?", time.Now().Add(time.Hour).Truncate(time.Hour).Format(time.DateTime), serial) + test.AssertNotError(t, err, "updating expiry to near future") + runUpdater(t, configFile) + resp, err = http.Get("http://localhost:4501/query?serial=" + serial) + test.AssertNotError(t, err, "s3-test-srv GET /query failed") + test.AssertEquals(t, resp.StatusCode, 200) + reason, err = io.ReadAll(resp.Body) + test.AssertNotError(t, err, "reading revocation reason") + test.AssertEquals(t, string(reason), "5") + resp.Body.Close() + + // Again update the database so that the certificate has expired in the + // very recent past. The cert should still appear on the CRL. + _, err = db.Exec("UPDATE revokedCertificates SET notAfterHour = ? WHERE serial = ?", time.Now().Add(-time.Hour).Truncate(time.Hour).Format(time.DateTime), serial) + test.AssertNotError(t, err, "updating expiry to recent past") + runUpdater(t, configFile) + resp, err = http.Get("http://localhost:4501/query?serial=" + serial) + test.AssertNotError(t, err, "s3-test-srv GET /query failed") + test.AssertEquals(t, resp.StatusCode, 200) + reason, err = io.ReadAll(resp.Body) + test.AssertNotError(t, err, "reading revocation reason") + test.AssertEquals(t, string(reason), "5") + resp.Body.Close() + + // Finally update the database so that the certificate expired several CRL + // update cycles ago. The cert should now vanish from the CRL. + _, err = db.Exec("UPDATE revokedCertificates SET notAfterHour = ? WHERE serial = ?", time.Now().Add(-48*time.Hour).Truncate(time.Hour).Format(time.DateTime), serial) + test.AssertNotError(t, err, "updating expiry to far past") + runUpdater(t, configFile) + resp, err = http.Get("http://localhost:4501/query?serial=" + serial) + test.AssertNotError(t, err, "s3-test-srv GET /query failed") + test.AssertEquals(t, resp.StatusCode, 404) + resp.Body.Close() +} + +func TestCRLTemporalAndExplicitShardingCoexist(t *testing.T) { + db, err := sql.Open("mysql", vars.DBConnSAIntegrationFullPerms) + if err != nil { + t.Fatalf("sql.Open: %s", err) + } + // Insert an old, revoked certificate in the certificateStatus table. Importantly this + // serial has the 7f prefix, which is in test/config-next/crl-updater.json in the + // `temporallyShardedPrefixes` list. + // Random serial that is unique to this test. + oldSerial := "7faa39be44fc95f3d19befe3cb715848e601" + // This is hardcoded to match one of the issuer names in our integration test environment's + // ca.json. + issuerID := 43104258997432926 + _, err = db.Exec(`DELETE FROM certificateStatus WHERE serial = ?`, oldSerial) + if err != nil { + t.Fatalf("deleting old certificateStatus row: %s", err) + } + _, err = db.Exec(` + INSERT INTO certificateStatus (serial, issuerID, notAfter, status, ocspLastUpdated, revokedDate, revokedReason, lastExpirationNagSent) + VALUES (?, ?, ?, "revoked", NOW(), NOW(), 0, 0);`, + oldSerial, issuerID, time.Now().Add(24*time.Hour).Format("2006-01-02 15:04:05")) + if err != nil { + t.Fatalf("inserting old certificateStatus row: %s", err) + } + + client, err := makeClient() + if err != nil { + t.Fatalf("creating acme client: %s", err) + } + + certKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatalf("creating cert key: %s", err) + } + + // Issue and revoke a certificate. In the config-next world, this will be an explicitly + // sharded certificate. In the config world, this will be a temporally sharded certificate + // (until we move `config` to explicit sharding). This means that in the config world, + // this test only handles temporal sharding, but we don't config-gate it because it passes + // in both worlds. + result, err := authAndIssue(client, certKey, []acme.Identifier{{Type: "dns", Value: random_domain()}}, true, "") + if err != nil { + t.Fatalf("authAndIssue: %s", err) + } + + cert := result.certs[0] + err = client.RevokeCertificate( + client.Account, + cert, + client.PrivateKey, + 0, + ) + if err != nil { + t.Fatalf("revoking: %s", err) + } + + runUpdater(t, path.Join(os.Getenv("BOULDER_CONFIG_DIR"), "crl-updater.json")) + + allCRLs := getAllCRLs(t) + seen := make(map[string]bool) + // Range over CRLs from all issuers, because the "old" certificate (7faa...) has a + // different issuer than the "new" certificate issued by `authAndIssue`, which + // has a random issuer. + for _, crls := range allCRLs { + for _, crl := range crls { + for _, entry := range crl.RevokedCertificateEntries { + serial := fmt.Sprintf("%x", entry.SerialNumber) + if seen[serial] { + t.Errorf("revoked certificate %s seen on multiple CRLs", serial) + } + seen[serial] = true + } + } + } + + newSerial := fmt.Sprintf("%x", cert.SerialNumber) + if !seen[newSerial] { + t.Errorf("revoked certificate %s not seen on any CRL", newSerial) + } + if !seen[oldSerial] { + t.Errorf("revoked certificate %s not seen on any CRL", oldSerial) + } } diff --git a/third-party/github.com/letsencrypt/boulder/test/integration/email_exporter_test.go b/third-party/github.com/letsencrypt/boulder/test/integration/email_exporter_test.go new file mode 100644 index 000000000..eb68b4828 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/test/integration/email_exporter_test.go @@ -0,0 +1,167 @@ +//go:build integration + +package integration + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "encoding/json" + "fmt" + "net/http" + "net/url" + "os" + "slices" + "strings" + "testing" + "time" + + "github.com/eggsampler/acme/v3" + + "github.com/letsencrypt/boulder/test" +) + +// randomDomain creates a random domain name for testing. +func randomDomain(t *testing.T) string { + t.Helper() + + var bytes [4]byte + _, err := rand.Read(bytes[:]) + if err != nil { + test.AssertNotError(t, err, "Failed to generate random domain") + } + return fmt.Sprintf("%x.mail.com", bytes[:]) +} + +// getOAuthToken queries the pardot-test-srv for the current OAuth token. +func getOAuthToken(t *testing.T) string { + t.Helper() + + data, err := os.ReadFile("test/secrets/salesforce_client_id") + test.AssertNotError(t, err, "Failed to read Salesforce client ID") + clientId := string(data) + + data, err = os.ReadFile("test/secrets/salesforce_client_secret") + test.AssertNotError(t, err, "Failed to read Salesforce client secret") + clientSecret := string(data) + + httpClient := http.DefaultClient + resp, err := httpClient.PostForm("http://localhost:9601/services/oauth2/token", url.Values{ + "grant_type": {"client_credentials"}, + "client_id": {strings.TrimSpace(clientId)}, + "client_secret": {strings.TrimSpace(clientSecret)}, + }) + test.AssertNotError(t, err, "Failed to fetch OAuth token") + test.AssertEquals(t, resp.StatusCode, http.StatusOK) + defer resp.Body.Close() + + var response struct { + AccessToken string `json:"access_token"` + } + decoder := json.NewDecoder(resp.Body) + err = decoder.Decode(&response) + test.AssertNotError(t, err, "Failed to decode OAuth token") + return response.AccessToken +} + +// getCreatedContacts queries the pardot-test-srv for the list of created +// contacts. +func getCreatedContacts(t *testing.T, token string) []string { + t.Helper() + + httpClient := http.DefaultClient + req, err := http.NewRequest("GET", "http://localhost:9602/contacts", nil) + test.AssertNotError(t, err, "Failed to create request") + req.Header.Set("Authorization", "Bearer "+token) + + resp, err := httpClient.Do(req) + test.AssertNotError(t, err, "Failed to query contacts") + test.AssertEquals(t, resp.StatusCode, http.StatusOK) + defer resp.Body.Close() + + var got struct { + Contacts []string `json:"contacts"` + } + decoder := json.NewDecoder(resp.Body) + err = decoder.Decode(&got) + test.AssertNotError(t, err, "Failed to decode contacts") + return got.Contacts +} + +// assertAllContactsReceived waits for the expected contacts to be received by +// pardot-test-srv. Retries every 50ms for up to 2 seconds and fails if the +// expected contacts are not received. +func assertAllContactsReceived(t *testing.T, token string, expect []string) { + t.Helper() + + for attempt := range 20 { + if attempt > 0 { + time.Sleep(50 * time.Millisecond) + } + got := getCreatedContacts(t, token) + + allFound := true + for _, e := range expect { + if !slices.Contains(got, e) { + allFound = false + break + } + } + if allFound { + break + } + if attempt >= 19 { + t.Fatalf("Expected contacts=%v to be received by pardot-test-srv, got contacts=%v", expect, got) + } + } +} + +// TestContactsSentForNewAccount tests that contacts are dispatched to +// pardot-test-srv by the email-exporter when a new account is created. +func TestContactsSentForNewAccount(t *testing.T) { + t.Parallel() + + if os.Getenv("BOULDER_CONFIG_DIR") != "test/config-next" { + t.Skip("Test requires WFE to be configured to use email-exporter") + } + + token := getOAuthToken(t) + domain := randomDomain(t) + + tests := []struct { + name string + contacts []string + expectContacts []string + }{ + { + name: "Single email", + contacts: []string{"mailto:example@" + domain}, + expectContacts: []string{"example@" + domain}, + }, + { + name: "Multiple emails", + contacts: []string{"mailto:example1@" + domain, "mailto:example2@" + domain}, + expectContacts: []string{"example1@" + domain, "example2@" + domain}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + c, err := acme.NewClient("http://boulder.service.consul:4001/directory") + if err != nil { + t.Fatalf("failed to connect to acme directory: %s", err) + } + + acctKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatalf("failed to generate account key: %s", err) + } + + _, err = c.NewAccount(acctKey, false, true, tt.contacts...) + test.AssertNotError(t, err, "Failed to create initial account with contacts") + assertAllContactsReceived(t, token, tt.expectContacts) + }) + } +} diff --git a/third-party/github.com/letsencrypt/boulder/test/integration/errors_test.go b/third-party/github.com/letsencrypt/boulder/test/integration/errors_test.go index 0c71bdb72..ad03f0d7b 100644 --- a/third-party/github.com/letsencrypt/boulder/test/integration/errors_test.go +++ b/third-party/github.com/letsencrypt/boulder/test/integration/errors_test.go @@ -3,32 +3,44 @@ package integration import ( + "bytes" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "encoding/base64" + "encoding/json" + "errors" "fmt" + "io" + "net/http" + "slices" "strings" "testing" "github.com/eggsampler/acme/v3" + "github.com/go-jose/go-jose/v4" "github.com/letsencrypt/boulder/test" ) -// TestTooBigOrderError tests that submitting an order with more than 100 names -// produces the expected problem result. +// TestTooBigOrderError tests that submitting an order with more than 100 +// identifiers produces the expected problem result. func TestTooBigOrderError(t *testing.T) { t.Parallel() - var domains []string + var idents []acme.Identifier for i := range 101 { - domains = append(domains, fmt.Sprintf("%d.example.com", i)) + idents = append(idents, acme.Identifier{Type: "dns", Value: fmt.Sprintf("%d.example.com", i)}) } - _, err := authAndIssue(nil, nil, domains, true) + _, err := authAndIssue(nil, nil, idents, true, "") test.AssertError(t, err, "authAndIssue failed") var prob acme.Problem test.AssertErrorWraps(t, err, &prob) test.AssertEquals(t, prob.Type, "urn:ietf:params:acme:error:malformed") - test.AssertEquals(t, prob.Detail, "Order cannot contain more than 100 DNS names") + test.AssertContains(t, prob.Detail, "Order cannot contain more than 100 identifiers") } // TestAccountEmailError tests that registering a new account, or updating an @@ -37,19 +49,6 @@ func TestTooBigOrderError(t *testing.T) { func TestAccountEmailError(t *testing.T) { t.Parallel() - // The registrations.contact field is VARCHAR(191). 175 'a' characters plus - // the prefix "mailto:" and the suffix "@a.com" makes exactly 191 bytes of - // encoded JSON. The correct size to hit our maximum DB field length. - var longStringBuf strings.Builder - longStringBuf.WriteString("mailto:") - for range 175 { - longStringBuf.WriteRune('a') - } - longStringBuf.WriteString("@a.com") - - createErrorPrefix := "Error creating new account :: " - updateErrorPrefix := "Unable to update account :: " - testCases := []struct { name string contacts []string @@ -66,87 +65,65 @@ func TestAccountEmailError(t *testing.T) { name: "empty proto", contacts: []string{"mailto:valid@valid.com", " "}, expectedProbType: "urn:ietf:params:acme:error:unsupportedContact", - expectedProbDetail: `contact method "" is not supported`, + expectedProbDetail: `only contact scheme 'mailto:' is supported`, }, { name: "empty mailto", contacts: []string{"mailto:valid@valid.com", "mailto:"}, expectedProbType: "urn:ietf:params:acme:error:invalidContact", - expectedProbDetail: `"" is not a valid e-mail address`, + expectedProbDetail: `unable to parse email address`, }, { name: "non-ascii mailto", contacts: []string{"mailto:valid@valid.com", "mailto:cpu@l̴etsencrypt.org"}, expectedProbType: "urn:ietf:params:acme:error:invalidContact", - expectedProbDetail: `contact email ["mailto:cpu@l̴etsencrypt.org"] contains non-ASCII characters`, + expectedProbDetail: `contact email contains non-ASCII characters`, }, { name: "too many contacts", - contacts: []string{"a", "b", "c", "d"}, + contacts: slices.Repeat([]string{"mailto:lots@valid.com"}, 11), expectedProbType: "urn:ietf:params:acme:error:malformed", - expectedProbDetail: `too many contacts provided: 4 > 3`, + expectedProbDetail: `too many contacts provided`, }, { name: "invalid contact", contacts: []string{"mailto:valid@valid.com", "mailto:a@"}, expectedProbType: "urn:ietf:params:acme:error:invalidContact", - expectedProbDetail: `"a@" is not a valid e-mail address`, + expectedProbDetail: `unable to parse email address`, }, { name: "forbidden contact domain", contacts: []string{"mailto:valid@valid.com", "mailto:a@example.com"}, expectedProbType: "urn:ietf:params:acme:error:invalidContact", - expectedProbDetail: "invalid contact domain. Contact emails @example.com are forbidden", + expectedProbDetail: "contact email has forbidden domain \"example.com\"", }, { name: "contact domain invalid TLD", contacts: []string{"mailto:valid@valid.com", "mailto:a@example.cpu"}, expectedProbType: "urn:ietf:params:acme:error:invalidContact", - expectedProbDetail: `contact email "a@example.cpu" has invalid domain : Domain name does not end with a valid public suffix (TLD)`, + expectedProbDetail: `contact email has invalid domain: Domain name does not end with a valid public suffix (TLD)`, }, { name: "contact domain invalid", contacts: []string{"mailto:valid@valid.com", "mailto:a@example./.com"}, expectedProbType: "urn:ietf:params:acme:error:invalidContact", - expectedProbDetail: "contact email \"a@example./.com\" has invalid domain : Domain name contains an invalid character", - }, - { - name: "too long contact", - contacts: []string{ - longStringBuf.String(), - }, - expectedProbType: "urn:ietf:params:acme:error:invalidContact", - expectedProbDetail: `too many/too long contact(s). Please use shorter or fewer email addresses`, + expectedProbDetail: "contact email has invalid domain: Domain name contains an invalid character", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - // First try registering a new account and ensuring the expected problem occurs var prob acme.Problem _, err := makeClient(tc.contacts...) if err != nil { test.AssertErrorWraps(t, err, &prob) test.AssertEquals(t, prob.Type, tc.expectedProbType) - test.AssertEquals(t, prob.Detail, createErrorPrefix+tc.expectedProbDetail) + test.AssertContains(t, prob.Detail, "Error validating contact(s)") + test.AssertContains(t, prob.Detail, tc.expectedProbDetail) } else { t.Errorf("expected %s type problem for %q, got nil", tc.expectedProbType, strings.Join(tc.contacts, ",")) } - - // Next try making a client with a good contact and updating with the test - // case contact info. The same problem should occur. - c, err := makeClient("mailto:valid@valid.com") - test.AssertNotError(t, err, "failed to create account with valid contact") - _, err = c.UpdateAccount(c.Account, tc.contacts...) - if err != nil { - test.AssertErrorWraps(t, err, &prob) - test.AssertEquals(t, prob.Type, tc.expectedProbType) - test.AssertEquals(t, prob.Detail, updateErrorPrefix+tc.expectedProbDetail) - } else { - t.Errorf("expected %s type problem after updating account to %q, got nil", - tc.expectedProbType, strings.Join(tc.contacts, ",")) - } }) } } @@ -155,10 +132,10 @@ func TestRejectedIdentifier(t *testing.T) { t.Parallel() // When a single malformed name is provided, we correctly reject it. - domains := []string{ - "яџ–Х6яяdь}", + idents := []acme.Identifier{ + {Type: "dns", Value: "яџ–Х6яяdь}"}, } - _, err := authAndIssue(nil, nil, domains, true) + _, err := authAndIssue(nil, nil, idents, true, "") test.AssertError(t, err, "issuance should fail for one malformed name") var prob acme.Problem test.AssertErrorWraps(t, err, &prob) @@ -169,17 +146,145 @@ func TestRejectedIdentifier(t *testing.T) { // them and reflect this in suberrors. This test ensures that the way we // encode these errors across the gRPC boundary is resilient to non-ascii // characters. - domains = []string{ - "˜o-", - "ш№Ў", - "р±y", - "яџ–Х6яя", - "яџ–Х6яя`ь", + idents = []acme.Identifier{ + {Type: "dns", Value: "˜o-"}, + {Type: "dns", Value: "ш№Ў"}, + {Type: "dns", Value: "р±y"}, + {Type: "dns", Value: "яџ–Х6яя"}, + {Type: "dns", Value: "яџ–Х6яя`ь"}, } - _, err = authAndIssue(nil, nil, domains, true) + _, err = authAndIssue(nil, nil, idents, true, "") test.AssertError(t, err, "issuance should fail for multiple malformed names") test.AssertErrorWraps(t, err, &prob) test.AssertEquals(t, prob.Type, "urn:ietf:params:acme:error:rejectedIdentifier") test.AssertContains(t, prob.Detail, "Domain name contains an invalid character") test.AssertContains(t, prob.Detail, "and 4 more problems") } + +// TestBadSignatureAlgorithm tests that supplying an unacceptable value for the +// "alg" field of the JWS Protected Header results in a problem document with +// the set of acceptable "alg" values listed in a custom extension field named +// "algorithms". Creating a request with an unacceptable "alg" field requires +// us to do some shenanigans. +func TestBadSignatureAlgorithm(t *testing.T) { + t.Parallel() + + client, err := makeClient() + if err != nil { + t.Fatal("creating test client") + } + + header, err := json.Marshal(&struct { + Alg string `json:"alg"` + KID string `json:"kid"` + Nonce string `json:"nonce"` + URL string `json:"url"` + }{ + Alg: string(jose.RS512), // This is the important bit; RS512 is unacceptable. + KID: client.Account.URL, + Nonce: "deadbeef", // This nonce would fail, but that check comes after the alg check. + URL: client.Directory().NewAccount, + }) + if err != nil { + t.Fatalf("creating JWS protected header: %s", err) + } + protected := base64.RawURLEncoding.EncodeToString(header) + + payload := base64.RawURLEncoding.EncodeToString([]byte(`{"onlyReturnExisting": true}`)) + hash := crypto.SHA512.New() + hash.Write([]byte(protected + "." + payload)) + sig, err := client.Account.PrivateKey.Sign(rand.Reader, hash.Sum(nil), crypto.SHA512) + if err != nil { + t.Fatalf("creating fake signature: %s", err) + } + + data, err := json.Marshal(&struct { + Protected string `json:"protected"` + Payload string `json:"payload"` + Signature string `json:"signature"` + }{ + Protected: protected, + Payload: payload, + Signature: base64.RawURLEncoding.EncodeToString(sig), + }) + + req, err := http.NewRequest(http.MethodPost, client.Directory().NewAccount, bytes.NewReader(data)) + if err != nil { + t.Fatalf("creating HTTP request: %s", err) + } + req.Header.Set("Content-Type", "application/jose+json") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatalf("making HTTP request: %s", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatalf("reading HTTP response: %s", err) + } + + var prob struct { + Type string `json:"type"` + Detail string `json:"detail"` + Status int `json:"status"` + Algorithms []jose.SignatureAlgorithm `json:"algorithms"` + } + err = json.Unmarshal(body, &prob) + if err != nil { + t.Fatalf("parsing HTTP response: %s", err) + } + + if prob.Type != "urn:ietf:params:acme:error:badSignatureAlgorithm" { + t.Errorf("problem document has wrong type: want badSignatureAlgorithm, got %s", prob.Type) + } + if prob.Status != http.StatusBadRequest { + t.Errorf("problem document has wrong status: want 400, got %d", prob.Status) + } + if len(prob.Algorithms) == 0 { + t.Error("problem document MUST contain acceptable algorithms, got none") + } +} + +// TestOrderFinalizeEarly tests that finalizing an order before it is fully +// authorized results in an orderNotReady error. +func TestOrderFinalizeEarly(t *testing.T) { + t.Parallel() + + client, err := makeClient() + if err != nil { + t.Fatalf("creating acme client: %s", err) + } + + idents := []acme.Identifier{{Type: "dns", Value: randomDomain(t)}} + + order, err := client.Client.NewOrder(client.Account, idents) + if err != nil { + t.Fatalf("creating order: %s", err) + } + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatalf("generating key: %s", err) + } + csr, err := makeCSR(key, idents, false) + if err != nil { + t.Fatalf("generating CSR: %s", err) + } + + order, err = client.Client.FinalizeOrder(client.Account, order, csr) + if err == nil { + t.Fatal("expected finalize to fail, but got success") + } + var prob acme.Problem + ok := errors.As(err, &prob) + if !ok { + t.Fatalf("expected error to be of type acme.Problem, got: %T", err) + } + if prob.Type != "urn:ietf:params:acme:error:orderNotReady" { + t.Errorf("expected problem type 'urn:ietf:params:acme:error:orderNotReady', got: %s", prob.Type) + } + if order.Status != "pending" { + t.Errorf("expected order status to be pending, got: %s", order.Status) + } +} diff --git a/third-party/github.com/letsencrypt/boulder/test/integration/issuance_test.go b/third-party/github.com/letsencrypt/boulder/test/integration/issuance_test.go index 4eb93d7e1..619e75551 100644 --- a/third-party/github.com/letsencrypt/boulder/test/integration/issuance_test.go +++ b/third-party/github.com/letsencrypt/boulder/test/integration/issuance_test.go @@ -6,9 +6,14 @@ import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" + "crypto/x509" "fmt" + "os" + "strings" "testing" + "github.com/eggsampler/acme/v3" + "github.com/letsencrypt/boulder/test" ) @@ -29,9 +34,14 @@ func TestCommonNameInCSR(t *testing.T) { cn := random_domain() san1 := random_domain() san2 := random_domain() + idents := []acme.Identifier{ + {Type: "dns", Value: cn}, + {Type: "dns", Value: san1}, + {Type: "dns", Value: san2}, + } // Issue a cert. authAndIssue includes the 0th name as the CN by default. - ir, err := authAndIssue(client, key, []string{cn, san1, san2}, true) + ir, err := authAndIssue(client, key, idents, true, "") test.AssertNotError(t, err, "failed to issue test cert") cert := ir.certs[0] @@ -60,9 +70,13 @@ func TestFirstCSRSANHoistedToCN(t *testing.T) { // Create some names that we can sort. san1 := "a" + random_domain() san2 := "b" + random_domain() + idents := []acme.Identifier{ + {Type: "dns", Value: san2}, + {Type: "dns", Value: san1}, + } // Issue a cert using a CSR with no CN set, and the SANs in *non*-alpha order. - ir, err := authAndIssue(client, key, []string{san2, san1}, false) + ir, err := authAndIssue(client, key, idents, false, "") test.AssertNotError(t, err, "failed to issue test cert") cert := ir.certs[0] @@ -75,8 +89,7 @@ func TestFirstCSRSANHoistedToCN(t *testing.T) { } // TestCommonNameSANsTooLong tests that, when the names in an order and CSR are -// too long to be hoisted into the CN, the correct behavior results (depending -// on the state of the AllowNoCommonName feature flag). +// too long to be hoisted into the CN, the correct behavior results. func TestCommonNameSANsTooLong(t *testing.T) { t.Parallel() @@ -91,9 +104,13 @@ func TestCommonNameSANsTooLong(t *testing.T) { // Put together some names. san1 := fmt.Sprintf("thisdomainnameis.morethan64characterslong.forthesakeoftesting.%s", random_domain()) san2 := fmt.Sprintf("thisdomainnameis.morethan64characterslong.forthesakeoftesting.%s", random_domain()) + idents := []acme.Identifier{ + {Type: "dns", Value: san1}, + {Type: "dns", Value: san2}, + } // Issue a cert using a CSR with no CN set. - ir, err := authAndIssue(client, key, []string{san1, san2}, false) + ir, err := authAndIssue(client, key, idents, false, "") test.AssertNotError(t, err, "failed to issue test cert") cert := ir.certs[0] @@ -104,3 +121,113 @@ func TestCommonNameSANsTooLong(t *testing.T) { // Ensure that the CN is empty. test.AssertEquals(t, cert.Subject.CommonName, "") } + +// TestIssuanceProfiles verifies that profile selection works, and results in +// measurable differences between certificates issued under different profiles. +// It does not test the omission of the keyEncipherment KU, because all of our +// integration test framework assumes ECDSA pubkeys for the sake of speed, +// and ECDSA certs don't get the keyEncipherment KU in either profile. +func TestIssuanceProfiles(t *testing.T) { + t.Parallel() + + // Create an account. + client, err := makeClient("mailto:example@letsencrypt.org") + test.AssertNotError(t, err, "creating acme client") + + profiles := client.Directory().Meta.Profiles + if len(profiles) < 2 { + t.Fatal("ACME server not advertising multiple profiles") + } + + // Create a private key. + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + test.AssertNotError(t, err, "creating random cert key") + + // Create a set of identifiers to request. + idents := []acme.Identifier{ + {Type: "dns", Value: random_domain()}, + } + + // Get one cert for each profile that we know the test server advertises. + res, err := authAndIssue(client, key, idents, true, "legacy") + test.AssertNotError(t, err, "failed to issue under legacy profile") + test.AssertEquals(t, res.Order.Profile, "legacy") + legacy := res.certs[0] + + res, err = authAndIssue(client, key, idents, true, "modern") + test.AssertNotError(t, err, "failed to issue under modern profile") + test.AssertEquals(t, res.Order.Profile, "modern") + modern := res.certs[0] + + // Check that each profile worked as expected. + test.AssertEquals(t, legacy.Subject.CommonName, idents[0].Value) + test.AssertEquals(t, modern.Subject.CommonName, "") + + test.AssertDeepEquals(t, legacy.ExtKeyUsage, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}) + test.AssertDeepEquals(t, modern.ExtKeyUsage, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}) + + test.AssertEquals(t, len(legacy.SubjectKeyId), 20) + test.AssertEquals(t, len(modern.SubjectKeyId), 0) +} + +// TestIPShortLived verifies that we will allow IP address identifiers only in +// orders that use the shortlived profile. +func TestIPShortLived(t *testing.T) { + t.Parallel() + + // Create an account. + client, err := makeClient("mailto:example@letsencrypt.org") + if err != nil { + t.Fatalf("creating acme client: %s", err) + } + + // Create a private key. + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatalf("creating random cert key: %s", err) + } + + // Create an IP address identifier to request. + ip := "64.112.117.122" + idents := []acme.Identifier{ + {Type: "ip", Value: ip}, + } + + // Ensure we fail under each other profile that we know the test server advertises. + _, err = authAndIssue(client, key, idents, false, "legacy") + if err == nil { + t.Error("issued for IP address identifier under legacy profile") + } + if !strings.Contains(err.Error(), "Profile \"legacy\" does not permit ip type identifiers") { + t.Fatalf("issuing under legacy profile failed for the wrong reason: %s", err) + } + + _, err = authAndIssue(client, key, idents, false, "modern") + if err == nil { + t.Error("issued for IP address identifier under modern profile") + } + if !strings.Contains(err.Error(), "Profile \"modern\" does not permit ip type identifiers") { + t.Fatalf("issuing under legacy profile failed for the wrong reason: %s", err) + } + + // Get one cert for the shortlived profile. + res, err := authAndIssue(client, key, idents, false, "shortlived") + if os.Getenv("BOULDER_CONFIG_DIR") == "test/config-next" { + if err != nil { + t.Errorf("issuing under shortlived profile: %s", err) + } + if res.Order.Profile != "shortlived" { + t.Errorf("got '%s' profile, wanted 'shortlived'", res.Order.Profile) + } + cert := res.certs[0] + + // Check that the shortlived profile worked as expected. + if cert.IPAddresses[0].String() != ip { + t.Errorf("got cert with first IP SAN '%s', wanted '%s'", cert.IPAddresses[0], ip) + } + } else { + if !strings.Contains(err.Error(), "Profile \"shortlived\" does not permit ip type identifiers") { + t.Errorf("issuing under shortlived profile failed for the wrong reason: %s", err) + } + } +} diff --git a/third-party/github.com/letsencrypt/boulder/test/integration/nonce_test.go b/third-party/github.com/letsencrypt/boulder/test/integration/nonce_test.go index 58a576f58..8475463af 100644 --- a/third-party/github.com/letsencrypt/boulder/test/integration/nonce_test.go +++ b/third-party/github.com/letsencrypt/boulder/test/integration/nonce_test.go @@ -4,11 +4,10 @@ package integration import ( "context" - "os" - "strings" "testing" "github.com/jmhodges/clock" + "google.golang.org/grpc/status" "github.com/letsencrypt/boulder/cmd" bgrpc "github.com/letsencrypt/boulder/grpc" @@ -17,7 +16,6 @@ import ( "github.com/letsencrypt/boulder/nonce" noncepb "github.com/letsencrypt/boulder/nonce/proto" "github.com/letsencrypt/boulder/test" - "google.golang.org/grpc/status" ) type nonceBalancerTestConfig struct { @@ -25,17 +23,13 @@ type nonceBalancerTestConfig struct { TLS cmd.TLSConfig GetNonceService *cmd.GRPCClientConfig RedeemNonceService *cmd.GRPCClientConfig - NoncePrefixKey cmd.PasswordConfig + NonceHMACKey cmd.HMACKeyConfig } } func TestNonceBalancer_NoBackendMatchingPrefix(t *testing.T) { t.Parallel() - if !strings.Contains(os.Getenv("BOULDER_CONFIG_DIR"), "test/config-next") { - t.Skip("Derived nonce prefixes are only configured in config-next") - } - // We're going to use a minimal nonce service client called "notwfe" which // masquerades as a wfe for the purpose of redeeming nonces. @@ -47,8 +41,8 @@ func TestNonceBalancer_NoBackendMatchingPrefix(t *testing.T) { tlsConfig, err := c.NotWFE.TLS.Load(metrics.NoopRegisterer) test.AssertNotError(t, err, "Could not load TLS config") - rncKey, err := c.NotWFE.NoncePrefixKey.Pass() - test.AssertNotError(t, err, "Failed to load noncePrefixKey") + rncKey, err := c.NotWFE.NonceHMACKey.Load() + test.AssertNotError(t, err, "Failed to load nonceHMACKey") clk := clock.New() diff --git a/third-party/github.com/letsencrypt/boulder/test/integration/observer_test.go b/third-party/github.com/letsencrypt/boulder/test/integration/observer_test.go new file mode 100644 index 000000000..bd99e17d9 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/test/integration/observer_test.go @@ -0,0 +1,176 @@ +//go:build integration + +package integration + +import ( + "bufio" + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "encoding/pem" + "fmt" + "net/http" + "os" + "os/exec" + "path" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/eggsampler/acme/v3" +) + +func streamOutput(t *testing.T, c *exec.Cmd) (<-chan string, func()) { + t.Helper() + outChan := make(chan string) + + stdout, err := c.StdoutPipe() + if err != nil { + t.Fatalf("getting stdout handle: %s", err) + } + + outScanner := bufio.NewScanner(stdout) + go func() { + for outScanner.Scan() { + outChan <- outScanner.Text() + } + }() + + stderr, err := c.StderrPipe() + if err != nil { + t.Fatalf("getting stderr handle: %s", err) + } + + errScanner := bufio.NewScanner(stderr) + go func() { + for errScanner.Scan() { + outChan <- errScanner.Text() + } + }() + + err = c.Start() + if err != nil { + t.Fatalf("starting cmd: %s", err) + } + + return outChan, func() { + c.Cancel() + c.Wait() + } +} + +func TestTLSProbe(t *testing.T) { + t.Parallel() + + // We can't use random_domain(), because the observer needs to be able to + // resolve this hostname within the docker-compose environment. + hostname := "integration.trust" + tempdir := t.TempDir() + + // Create the certificate that the prober will inspect. + client, err := makeClient() + if err != nil { + t.Fatalf("creating test acme client: %s", err) + } + + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatalf("generating test key: %s", err) + } + + res, err := authAndIssue(client, key, []acme.Identifier{{Type: "dns", Value: hostname}}, true, "") + if err != nil { + t.Fatalf("issuing test cert: %s", err) + } + + // Set up the HTTP server that the prober will be pointed at. + certFile, err := os.Create(path.Join(tempdir, "fullchain.pem")) + if err != nil { + t.Fatalf("creating cert file: %s", err) + } + + err = pem.Encode(certFile, &pem.Block{Type: "CERTIFICATE", Bytes: res.certs[0].Raw}) + if err != nil { + t.Fatalf("writing test cert to file: %s", err) + } + + err = pem.Encode(certFile, &pem.Block{Type: "CERTIFICATE", Bytes: res.certs[1].Raw}) + if err != nil { + t.Fatalf("writing test issuer cert to file: %s", err) + } + + err = certFile.Close() + if err != nil { + t.Errorf("closing cert file: %s", err) + } + + keyFile, err := os.Create(path.Join(tempdir, "privkey.pem")) + if err != nil { + t.Fatalf("creating key file: %s", err) + } + + keyDER, err := x509.MarshalECPrivateKey(key) + if err != nil { + t.Fatalf("marshalling test key: %s", err) + } + + err = pem.Encode(keyFile, &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER}) + if err != nil { + t.Fatalf("writing test key to file: %s", err) + } + + err = keyFile.Close() + if err != nil { + t.Errorf("closing key file: %s", err) + } + + go http.ListenAndServeTLS(":8675", certFile.Name(), keyFile.Name(), http.DefaultServeMux) + + // Kick off the prober, pointed at the server presenting our test cert. + configFile, err := os.Create(path.Join(tempdir, "observer.yml")) + if err != nil { + t.Fatalf("creating config file: %s", err) + } + + _, err = configFile.WriteString(fmt.Sprintf(`--- +buckets: [.001, .002, .005, .01, .02, .05, .1, .2, .5, 1, 2, 5, 10] +syslog: + stdoutlevel: 6 + sysloglevel: 0 +monitors: + - + period: 1s + kind: TLS + settings: + response: valid + hostname: "%s:8675"`, hostname)) + if err != nil { + t.Fatalf("writing test config: %s", err) + } + + binPath, err := filepath.Abs("bin/boulder") + if err != nil { + t.Fatalf("computing boulder binary path: %s", err) + } + + c := exec.CommandContext(context.Background(), binPath, "boulder-observer", "-config", configFile.Name(), "-debug-addr", ":8024") + output, cancel := streamOutput(t, c) + defer cancel() + + timeout := time.NewTimer(5 * time.Second) + + for { + select { + case <-timeout.C: + t.Fatalf("timed out before getting desired log line from boulder-observer") + case line := <-output: + t.Log(line) + if strings.Contains(line, "name=[integration.trust:8675]") && strings.Contains(line, "success=[true]") { + return + } + } + } +} diff --git a/third-party/github.com/letsencrypt/boulder/test/integration/ocsp_test.go b/third-party/github.com/letsencrypt/boulder/test/integration/ocsp_test.go index 8da548b30..140dee022 100644 --- a/third-party/github.com/letsencrypt/boulder/test/integration/ocsp_test.go +++ b/third-party/github.com/letsencrypt/boulder/test/integration/ocsp_test.go @@ -8,39 +8,30 @@ import ( "golang.org/x/crypto/ocsp" + "github.com/eggsampler/acme/v3" + "github.com/letsencrypt/boulder/core" ocsp_helper "github.com/letsencrypt/boulder/test/ocsp/helper" ) -// TODO(#5172): Fill out these test stubs. -func TestOCSPBadRequestMethod(t *testing.T) { - return -} - -func TestOCSPBadGetUrl(t *testing.T) { - return -} - -func TestOCSPBadGetBody(t *testing.T) { - return -} - -func TestOCSPBadPostBody(t *testing.T) { - return -} - -func TestOCSPBadHashAlgorithm(t *testing.T) { - return -} - -func TestOCSPBadIssuerCert(t *testing.T) { - return +func TestOCSPHappyPath(t *testing.T) { + t.Parallel() + cert, err := authAndIssue(nil, nil, []acme.Identifier{{Type: "dns", Value: random_domain()}}, true, "") + if err != nil || len(cert.certs) < 1 { + t.Fatal("failed to issue cert for OCSP testing") + } + resp, err := ocsp_helper.Req(cert.certs[0], ocspConf()) + if err != nil { + t.Fatalf("want ocsp response, but got error: %s", err) + } + if resp.Status != ocsp.Good { + t.Errorf("want ocsp status %#v, got %#v", ocsp.Good, resp.Status) + } } func TestOCSPBadSerialPrefix(t *testing.T) { t.Parallel() - domain := random_domain() - res, err := authAndIssue(nil, nil, []string{domain}, true) + res, err := authAndIssue(nil, nil, []acme.Identifier{{Type: "dns", Value: random_domain()}}, true, "") if err != nil || len(res.certs) < 1 { t.Fatal("Failed to issue dummy cert for OCSP testing") } @@ -51,20 +42,12 @@ func TestOCSPBadSerialPrefix(t *testing.T) { serialStr := []byte(core.SerialToString(cert.SerialNumber)) serialStr[0] = serialStr[0] + 1 cert.SerialNumber.SetString(string(serialStr), 16) - _, err = ocsp_helper.Req(cert, ocsp_helper.DefaultConfig) + _, err = ocsp_helper.Req(cert, ocspConf()) if err == nil { t.Fatal("Expected error getting OCSP for request with invalid serial") } } -func TestOCSPNonexistentSerial(t *testing.T) { - return -} - -func TestOCSPExpiredCert(t *testing.T) { - return -} - func TestOCSPRejectedPrecertificate(t *testing.T) { t.Parallel() domain := random_domain() @@ -73,7 +56,7 @@ func TestOCSPRejectedPrecertificate(t *testing.T) { t.Fatalf("adding ct-test-srv reject host: %s", err) } - _, err = authAndIssue(nil, nil, []string{domain}, true) + _, err = authAndIssue(nil, nil, []acme.Identifier{{Type: "dns", Value: domain}}, true, "") if err != nil { if !strings.Contains(err.Error(), "urn:ietf:params:acme:error:serverInternal") || !strings.Contains(err.Error(), "SCT embedding") { @@ -91,7 +74,7 @@ func TestOCSPRejectedPrecertificate(t *testing.T) { t.Fatalf("couldn't find rejected precert for %q", domain) } - ocspConfig := ocsp_helper.DefaultConfig.WithExpectStatus(ocsp.Good) + ocspConfig := ocspConf().WithExpectStatus(ocsp.Good) _, err = ocsp_helper.ReqDER(cert.Raw, ocspConfig) if err != nil { t.Errorf("requesting OCSP for rejected precertificate: %s", err) diff --git a/third-party/github.com/letsencrypt/boulder/test/integration/otel_test.go b/third-party/github.com/letsencrypt/boulder/test/integration/otel_test.go index b0d020c59..b3d3ce486 100644 --- a/third-party/github.com/letsencrypt/boulder/test/integration/otel_test.go +++ b/third-party/github.com/letsencrypt/boulder/test/integration/otel_test.go @@ -188,6 +188,14 @@ func httpSpan(endpoint string, children ...expectedSpans) expectedSpans { } } +func redisPipelineSpan(op, service string, children ...expectedSpans) expectedSpans { + return expectedSpans{ + Operation: "redis.pipeline " + op, + Service: service, + Children: children, + } +} + // TestTraces tests that all the expected spans are present and properly connected func TestTraces(t *testing.T) { t.Parallel() @@ -198,10 +206,12 @@ func TestTraces(t *testing.T) { traceID := traceIssuingTestCert(t) wfe := "boulder-wfe2" - sa := "boulder-sa" ra := "boulder-ra" ca := "boulder-ca" + // A very stripped-down version of the expected call graph of a full issuance + // flow: just enough to ensure that our otel tracing is working without + // asserting too much about the exact set of RPCs we use under the hood. expectedSpans := expectedSpans{ Operation: "TraceTest", Service: "integration.test", @@ -210,37 +220,13 @@ func TestTraces(t *testing.T) { {Operation: "/acme/new-nonce", Service: wfe, Children: []expectedSpans{ rpcSpan("nonce.NonceService/Nonce", wfe, "nonce-service")}}, httpSpan("/acme/new-acct", - rpcSpan("sa.StorageAuthorityReadOnly/KeyBlocked", wfe, sa), - rpcSpan("sa.StorageAuthorityReadOnly/GetRegistrationByKey", wfe, sa), - rpcSpan("ra.RegistrationAuthority/NewRegistration", wfe, ra, - rpcSpan("sa.StorageAuthority/KeyBlocked", ra, sa), - rpcSpan("sa.StorageAuthority/CountRegistrationsByIP", ra, sa), - rpcSpan("sa.StorageAuthority/NewRegistration", ra, sa))), - httpSpan("/acme/new-order", - rpcSpan("sa.StorageAuthorityReadOnly/GetRegistration", wfe, sa), - rpcSpan("ra.RegistrationAuthority/NewOrder", wfe, ra, - rpcSpan("sa.StorageAuthority/GetOrderForNames", ra, sa), - // 8 ra -> sa rate limit spans omitted here - rpcSpan("sa.StorageAuthority/NewOrderAndAuthzs", ra, sa))), - httpSpan("/acme/authz-v3/", - rpcSpan("sa.StorageAuthorityReadOnly/GetAuthorization2", wfe, sa)), - httpSpan("/acme/chall-v3/", - rpcSpan("sa.StorageAuthorityReadOnly/GetAuthorization2", wfe, sa), - rpcSpan("ra.RegistrationAuthority/PerformValidation", wfe, ra, - rpcSpan("sa.StorageAuthority/GetRegistration", ra, sa))), + redisPipelineSpan("get", wfe)), + httpSpan("/acme/new-order"), + httpSpan("/acme/authz/"), + httpSpan("/acme/chall/"), httpSpan("/acme/finalize/", - rpcSpan("sa.StorageAuthorityReadOnly/GetOrder", wfe, sa), rpcSpan("ra.RegistrationAuthority/FinalizeOrder", wfe, ra, - rpcSpan("sa.StorageAuthority/KeyBlocked", ra, sa), - rpcSpan("sa.StorageAuthority/GetRegistration", ra, sa), - rpcSpan("sa.StorageAuthority/GetValidOrderAuthorizations2", ra, sa), - rpcSpan("sa.StorageAuthority/SetOrderProcessing", ra, sa), - rpcSpan("ca.CertificateAuthority/IssuePrecertificate", ra, ca), - rpcSpan("Publisher/SubmitToSingleCTWithResult", ra, "boulder-publisher"), - rpcSpan("ca.CertificateAuthority/IssueCertificateForPrecertificate", ra, ca), - rpcSpan("sa.StorageAuthority/FinalizeOrder", ra, sa))), - httpSpan("/acme/order/", rpcSpan("sa.StorageAuthorityReadOnly/GetOrder", wfe, sa)), - httpSpan("/acme/cert/", rpcSpan("sa.StorageAuthorityReadOnly/GetCertificate", wfe, sa)), + rpcSpan("ca.CertificateAuthority/IssueCertificate", ra, ca))), }, } @@ -273,8 +259,6 @@ func TestTraces(t *testing.T) { } func traceIssuingTestCert(t *testing.T) trace.TraceID { - domains := []string{random_domain()} - // Configure this integration test to trace to jaeger:4317 like Boulder will shutdown := cmd.NewOpenTelemetry(cmd.OpenTelemetryConfig{ Endpoint: "bjaeger:4317", @@ -302,7 +286,7 @@ func traceIssuingTestCert(t *testing.T) trace.TraceID { account, err := c.NewAccount(privKey, false, true) test.AssertNotError(t, err, "newAccount failed") - _, err = authAndIssue(&client{account, c}, nil, domains, true) + _, err = authAndIssue(&client{account, c}, nil, []acme.Identifier{{Type: "dns", Value: random_domain()}}, true, "") test.AssertNotError(t, err, "authAndIssue failed") return span.SpanContext().TraceID() diff --git a/third-party/github.com/letsencrypt/boulder/test/integration/pausing_test.go b/third-party/github.com/letsencrypt/boulder/test/integration/pausing_test.go new file mode 100644 index 000000000..1247454a6 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/test/integration/pausing_test.go @@ -0,0 +1,78 @@ +//go:build integration + +package integration + +import ( + "context" + "strconv" + "strings" + "testing" + "time" + + "github.com/eggsampler/acme/v3" + "github.com/jmhodges/clock" + + "github.com/letsencrypt/boulder/cmd" + "github.com/letsencrypt/boulder/config" + bgrpc "github.com/letsencrypt/boulder/grpc" + "github.com/letsencrypt/boulder/identifier" + "github.com/letsencrypt/boulder/metrics" + sapb "github.com/letsencrypt/boulder/sa/proto" + "github.com/letsencrypt/boulder/test" +) + +func TestIdentifiersPausedForAccount(t *testing.T) { + t.Parallel() + + tlsCerts := &cmd.TLSConfig{ + CACertFile: "test/certs/ipki/minica.pem", + CertFile: "test/certs/ipki/ra.boulder/cert.pem", + KeyFile: "test/certs/ipki/ra.boulder/key.pem", + } + tlsConf, err := tlsCerts.Load(metrics.NoopRegisterer) + test.AssertNotError(t, err, "Failed to load TLS config") + saConn, err := bgrpc.ClientSetup( + &cmd.GRPCClientConfig{ + DNSAuthority: "consul.service.consul", + SRVLookup: &cmd.ServiceDomain{ + Service: "sa", + Domain: "service.consul", + }, + + Timeout: config.Duration{Duration: 5 * time.Second}, + NoWaitForReady: true, + HostOverride: "sa.boulder", + }, + tlsConf, + metrics.NoopRegisterer, + clock.NewFake(), + ) + cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to SA") + saClient := sapb.NewStorageAuthorityClient(saConn) + + c, err := makeClient() + parts := strings.SplitAfter(c.URL, "/") + regID, err := strconv.ParseInt(parts[len(parts)-1], 10, 64) + domain := random_domain() + serverIdents := identifier.ACMEIdentifiers{identifier.NewDNS(domain)} + clientIdents := []acme.Identifier{{Type: "dns", Value: domain}} + + _, err = saClient.PauseIdentifiers(context.Background(), &sapb.PauseRequest{ + RegistrationID: regID, + Identifiers: serverIdents.ToProtoSlice(), + }) + test.AssertNotError(t, err, "Failed to pause domain") + + _, err = authAndIssue(c, nil, clientIdents, true, "") + test.AssertError(t, err, "Should not be able to issue a certificate for a paused domain") + test.AssertContains(t, err.Error(), "Your account is temporarily prevented from requesting certificates for") + test.AssertContains(t, err.Error(), "https://boulder.service.consul:4003/sfe/v1/unpause?jwt=") + + _, err = saClient.UnpauseAccount(context.Background(), &sapb.RegistrationID{ + Id: regID, + }) + test.AssertNotError(t, err, "Failed to unpause domain") + + _, err = authAndIssue(c, nil, clientIdents, true, "") + test.AssertNotError(t, err, "Should be able to issue a certificate for an unpaused domain") +} diff --git a/third-party/github.com/letsencrypt/boulder/test/integration/ratelimit_test.go b/third-party/github.com/letsencrypt/boulder/test/integration/ratelimit_test.go index 88050b6b2..e1d4855e2 100644 --- a/third-party/github.com/letsencrypt/boulder/test/integration/ratelimit_test.go +++ b/third-party/github.com/letsencrypt/boulder/test/integration/ratelimit_test.go @@ -3,72 +3,64 @@ package integration import ( - "context" + "crypto/rand" + "encoding/hex" + "fmt" "os" - "strings" "testing" - "github.com/jmhodges/clock" + "github.com/eggsampler/acme/v3" - "github.com/letsencrypt/boulder/cmd" - blog "github.com/letsencrypt/boulder/log" - "github.com/letsencrypt/boulder/metrics" - "github.com/letsencrypt/boulder/ratelimits" - bredis "github.com/letsencrypt/boulder/redis" "github.com/letsencrypt/boulder/test" ) func TestDuplicateFQDNRateLimit(t *testing.T) { t.Parallel() - domain := random_domain() + idents := []acme.Identifier{{Type: "dns", Value: random_domain()}} - _, err := authAndIssue(nil, nil, []string{domain}, true) + // TODO(#8235): Remove this conditional once IP address identifiers are + // enabled in test/config. + if os.Getenv("BOULDER_CONFIG_DIR") == "test/config-next" { + idents = append(idents, acme.Identifier{Type: "ip", Value: "64.112.117.122"}) + } + + // The global rate limit for a duplicate certificates is 2 per 3 hours. + _, err := authAndIssue(nil, nil, idents, true, "shortlived") test.AssertNotError(t, err, "Failed to issue first certificate") - _, err = authAndIssue(nil, nil, []string{domain}, true) + _, err = authAndIssue(nil, nil, idents, true, "shortlived") test.AssertNotError(t, err, "Failed to issue second certificate") - _, err = authAndIssue(nil, nil, []string{domain}, true) + _, err = authAndIssue(nil, nil, idents, true, "shortlived") test.AssertError(t, err, "Somehow managed to issue third certificate") - if strings.Contains(os.Getenv("BOULDER_CONFIG_DIR"), "test/config-next") { - // Setup rate limiting. - rc := bredis.Config{ - Username: "unittest-rw", - TLS: cmd.TLSConfig{ - CACertFile: "test/certs/ipki/minica.pem", - CertFile: "test/certs/ipki/localhost/cert.pem", - KeyFile: "test/certs/ipki/localhost/key.pem", - }, - Lookups: []cmd.ServiceDomain{ - { - Service: "redisratelimits", - Domain: "service.consul", - }, - }, - LookupDNSAuthority: "consul.service.consul", - } - rc.PasswordConfig = cmd.PasswordConfig{ - PasswordFile: "test/secrets/ratelimits_redis_password", - } - - fc := clock.NewFake() - stats := metrics.NoopRegisterer - log := blog.NewMock() - ring, err := bredis.NewRingFromConfig(rc, stats, log) - test.AssertNotError(t, err, "making redis ring client") - source := ratelimits.NewRedisSource(ring.Ring, fc, stats) - test.AssertNotNil(t, source, "source should not be nil") - limiter, err := ratelimits.NewLimiter(fc, source, stats) - test.AssertNotError(t, err, "making limiter") - txnBuilder, err := ratelimits.NewTransactionBuilder("test/config-next/wfe2-ratelimit-defaults.yml", "") - test.AssertNotError(t, err, "making transaction composer") - - // Check that the CertificatesPerFQDNSet limit is reached. - txn, err := txnBuilder.CertificatesPerFQDNSetTransaction([]string{domain}) - test.AssertNotError(t, err, "making transaction") - result, err := limiter.Check(context.Background(), txn) - test.AssertNotError(t, err, "checking transaction") - test.Assert(t, !result.Allowed, "should not be allowed") - } + test.AssertContains(t, err.Error(), "too many certificates (2) already issued for this exact set of identifiers in the last 3h0m0s") +} + +func TestCertificatesPerDomain(t *testing.T) { + t.Parallel() + + randomDomain := random_domain() + randomSubDomain := func() string { + var bytes [3]byte + rand.Read(bytes[:]) + return fmt.Sprintf("%s.%s", hex.EncodeToString(bytes[:]), randomDomain) + } + + firstSubDomain := randomSubDomain() + _, err := authAndIssue(nil, nil, []acme.Identifier{{Type: "dns", Value: firstSubDomain}}, true, "") + test.AssertNotError(t, err, "Failed to issue first certificate") + + _, err = authAndIssue(nil, nil, []acme.Identifier{{Type: "dns", Value: randomSubDomain()}}, true, "") + test.AssertNotError(t, err, "Failed to issue second certificate") + + _, err = authAndIssue(nil, nil, []acme.Identifier{{Type: "dns", Value: randomSubDomain()}}, true, "") + test.AssertError(t, err, "Somehow managed to issue third certificate") + + test.AssertContains(t, err.Error(), fmt.Sprintf("too many certificates (2) already issued for %q in the last 2160h0m0s", randomDomain)) + + // Issue a certificate for the first subdomain, which should succeed because + // it's a renewal. + _, err = authAndIssue(nil, nil, []acme.Identifier{{Type: "dns", Value: firstSubDomain}}, true, "") + test.AssertNotError(t, err, "Failed to issue renewal certificate") } diff --git a/third-party/github.com/letsencrypt/boulder/test/integration/revocation_test.go b/third-party/github.com/letsencrypt/boulder/test/integration/revocation_test.go index c6ae66d73..2f03581ac 100644 --- a/third-party/github.com/letsencrypt/boulder/test/integration/revocation_test.go +++ b/third-party/github.com/letsencrypt/boulder/test/integration/revocation_test.go @@ -8,16 +8,26 @@ import ( "crypto/elliptic" "crypto/rand" "crypto/x509" + "encoding/hex" + "encoding/json" + "encoding/pem" "fmt" "io" "net/http" + "os" + "os/exec" + "path" "strings" + "sync" "testing" "time" "github.com/eggsampler/acme/v3" "golang.org/x/crypto/ocsp" + "github.com/letsencrypt/boulder/core" + "github.com/letsencrypt/boulder/crl/idp" + "github.com/letsencrypt/boulder/revocation" "github.com/letsencrypt/boulder/test" ocsp_helper "github.com/letsencrypt/boulder/test/ocsp/helper" ) @@ -33,18 +43,192 @@ func isPrecert(cert *x509.Certificate) bool { return false } +// ocspConf returns an OCSP helper config with a fallback URL that matches what is +// configured for our CA / OCSP responder. If an OCSP URL is present in a certificate, +// ocsp_helper will use that; otherwise it will use the URLFallback. This allows +// continuing to test OCSP service even after we stop including OCSP URLs in certificates. +func ocspConf() ocsp_helper.Config { + return ocsp_helper.DefaultConfig.WithURLFallback("http://ca.example.org:4002/") +} + +// getALLCRLs fetches and parses each certificate for each configured CA. +// Returns a map from issuer SKID (hex) to a list of that issuer's CRLs. +func getAllCRLs(t *testing.T) map[string][]*x509.RevocationList { + t.Helper() + b, err := os.ReadFile(path.Join(os.Getenv("BOULDER_CONFIG_DIR"), "ca.json")) + if err != nil { + t.Fatalf("reading CA config: %s", err) + } + + var conf struct { + CA struct { + Issuance struct { + Issuers []struct { + CRLURLBase string + Location struct { + CertFile string + } + } + } + } + } + + err = json.Unmarshal(b, &conf) + if err != nil { + t.Fatalf("unmarshaling CA config: %s", err) + } + + ret := make(map[string][]*x509.RevocationList) + + for _, issuer := range conf.CA.Issuance.Issuers { + issuerPEMBytes, err := os.ReadFile(issuer.Location.CertFile) + if err != nil { + t.Fatalf("reading CRL issuer: %s", err) + } + + block, _ := pem.Decode(issuerPEMBytes) + issuerCert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + t.Fatalf("parsing CRL issuer: %s", err) + } + + issuerSKID := hex.EncodeToString(issuerCert.SubjectKeyId) + + // 10 is the number of shards configured in test/config*/crl-updater.json + for i := range 10 { + crlURL := fmt.Sprintf("%s%d.crl", issuer.CRLURLBase, i+1) + list := getCRL(t, crlURL, issuerCert) + + ret[issuerSKID] = append(ret[issuerSKID], list) + } + } + return ret +} + +// getCRL fetches a CRL, parses it, and checks the signature. +func getCRL(t *testing.T, crlURL string, issuerCert *x509.Certificate) *x509.RevocationList { + t.Helper() + resp, err := http.Get(crlURL) + if err != nil { + t.Fatalf("getting CRL from %s: %s", crlURL, err) + } + if resp.StatusCode != http.StatusOK { + t.Fatalf("fetching %s: status code %d", crlURL, resp.StatusCode) + } + body, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatalf("reading CRL from %s: %s", crlURL, err) + } + resp.Body.Close() + + list, err := x509.ParseRevocationList(body) + if err != nil { + t.Fatalf("parsing CRL from %s: %s (bytes: %x)", crlURL, err, body) + } + + err = list.CheckSignatureFrom(issuerCert) + if err != nil { + t.Errorf("checking CRL signature on %s from %s: %s", + crlURL, issuerCert.Subject, err) + } + + idpURIs, err := idp.GetIDPURIs(list.Extensions) + if err != nil { + t.Fatalf("getting IDP URIs: %s", err) + } + if len(idpURIs) != 1 { + t.Errorf("CRL at %s: expected 1 IDP URI, got %s", crlURL, idpURIs) + } + if idpURIs[0] != crlURL { + t.Errorf("fetched CRL from %s, got IDP of %s (should be same)", crlURL, idpURIs[0]) + } + return list +} + +func fetchAndCheckRevoked(t *testing.T, cert, issuer *x509.Certificate, expectedReason int) { + t.Helper() + if len(cert.CRLDistributionPoints) != 1 { + t.Errorf("expected certificate to have one CRLDistributionPoints field") + } + crlURL := cert.CRLDistributionPoints[0] + list := getCRL(t, crlURL, issuer) + for _, entry := range list.RevokedCertificateEntries { + if entry.SerialNumber.Cmp(cert.SerialNumber) == 0 { + if entry.ReasonCode != expectedReason { + t.Errorf("serial %x found on CRL %s with reason %d, want %d", entry.SerialNumber, crlURL, entry.ReasonCode, expectedReason) + } + return + } + } + t.Errorf("serial %x not found on CRL %s, expected it to be revoked with reason %d", cert.SerialNumber, crlURL, expectedReason) +} + +func checkUnrevoked(t *testing.T, revocations map[string][]*x509.RevocationList, cert *x509.Certificate) { + for _, singleIssuerCRLs := range revocations { + for _, crl := range singleIssuerCRLs { + for _, entry := range crl.RevokedCertificateEntries { + if entry.SerialNumber == cert.SerialNumber { + t.Errorf("expected %x to be unrevoked, but found it on a CRL", cert.SerialNumber) + } + } + } + } +} + +func checkRevoked(t *testing.T, revocations map[string][]*x509.RevocationList, cert *x509.Certificate, expectedReason int) { + t.Helper() + akid := hex.EncodeToString(cert.AuthorityKeyId) + if len(revocations[akid]) == 0 { + t.Errorf("no CRLs found for authorityKeyID %s", akid) + } + var matchingCRLs []string + var count int + for _, list := range revocations[akid] { + for _, entry := range list.RevokedCertificateEntries { + count++ + if entry.SerialNumber.Cmp(cert.SerialNumber) == 0 { + idpURIs, err := idp.GetIDPURIs(list.Extensions) + if err != nil { + t.Errorf("getting IDP URIs: %s", err) + } + idpURI := idpURIs[0] + if entry.ReasonCode != expectedReason { + t.Errorf("revoked certificate %x in CRL %s: revocation reason %d, want %d", cert.SerialNumber, idpURI, entry.ReasonCode, expectedReason) + } + matchingCRLs = append(matchingCRLs, idpURI) + } + } + } + if len(matchingCRLs) == 0 { + t.Errorf("searching for %x in CRLs: no entry on combined CRLs of length %d", cert.SerialNumber, count) + } + + // If the cert has a CRLDP, it must be listed on the CRL served at that URL. + if len(cert.CRLDistributionPoints) > 0 { + expectedCRLDP := cert.CRLDistributionPoints[0] + found := false + for _, crl := range matchingCRLs { + if crl == expectedCRLDP { + found = true + } + } + if !found { + t.Errorf("revoked certificate %x: seen on CRLs %s, want to see on CRL %s", cert.SerialNumber, matchingCRLs, expectedCRLDP) + } + } +} + // TestRevocation tests that a certificate can be revoked using all of the // RFC 8555 revocation authentication mechanisms. It does so for both certs and // precerts (with no corresponding final cert), and for both the Unspecified and // keyCompromise revocation reasons. func TestRevocation(t *testing.T) { - t.Parallel() - type authMethod string var ( byAccount authMethod = "byAccount" byAuth authMethod = "byAuth" byKey authMethod = "byKey" + byAdmin authMethod = "byAdmin" ) type certKind string @@ -59,135 +243,184 @@ func TestRevocation(t *testing.T) { kind certKind } - var testCases []testCase + issueAndRevoke := func(tc testCase) *x509.Certificate { + issueClient, err := makeClient() + test.AssertNotError(t, err, "creating acme client") + + certKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + test.AssertNotError(t, err, "creating random cert key") + + domain := random_domain() + + // Try to issue a certificate for the name. + var cert *x509.Certificate + switch tc.kind { + case finalcert: + res, err := authAndIssue(issueClient, certKey, []acme.Identifier{{Type: "dns", Value: domain}}, true, "") + test.AssertNotError(t, err, "authAndIssue failed") + cert = res.certs[0] + + case precert: + // Make sure the ct-test-srv will reject generating SCTs for the domain, + // so we only get a precert and no final cert. + err := ctAddRejectHost(domain) + test.AssertNotError(t, err, "adding ct-test-srv reject host") + + _, err = authAndIssue(issueClient, certKey, []acme.Identifier{{Type: "dns", Value: domain}}, true, "") + test.AssertError(t, err, "expected error from authAndIssue, was nil") + if !strings.Contains(err.Error(), "urn:ietf:params:acme:error:serverInternal") || + !strings.Contains(err.Error(), "SCT embedding") { + t.Fatal(err) + } + + // Instead recover the precertificate from CT. + cert, err = ctFindRejection([]string{domain}) + if err != nil || cert == nil { + t.Fatalf("couldn't find rejected precert for %q", domain) + } + // And make sure the cert we found is in fact a precert. + if !isPrecert(cert) { + t.Fatal("precert was missing poison extension") + } + + default: + t.Fatalf("unrecognized cert kind %q", tc.kind) + } + + // Initially, the cert should have a Good OCSP response (only via OCSP; the CRL is unchanged by issuance). + ocspConfig := ocspConf().WithExpectStatus(ocsp.Good) + _, err = ocsp_helper.ReqDER(cert.Raw, ocspConfig) + test.AssertNotError(t, err, "requesting OCSP for precert") + + // Set up the account and key that we'll use to revoke the cert. + switch tc.method { + case byAccount: + // When revoking by account, use the same client and key as were used + // for the original issuance. + err = issueClient.RevokeCertificate( + issueClient.Account, + cert, + issueClient.PrivateKey, + tc.reason, + ) + test.AssertNotError(t, err, "revocation should have succeeded") + + case byAuth: + // When revoking by auth, create a brand new client, authorize it for + // the same domain, and use that account and key for revocation. Ignore + // errors from authAndIssue because all we need is the auth, not the + // issuance. + newClient, err := makeClient() + test.AssertNotError(t, err, "creating second acme client") + _, _ = authAndIssue(newClient, certKey, []acme.Identifier{{Type: "dns", Value: domain}}, true, "") + + err = newClient.RevokeCertificate( + newClient.Account, + cert, + newClient.PrivateKey, + tc.reason, + ) + test.AssertNotError(t, err, "revocation should have succeeded") + + case byKey: + // When revoking by key, create a brand new client and use it with + // the cert's key for revocation. + newClient, err := makeClient() + test.AssertNotError(t, err, "creating second acme client") + err = newClient.RevokeCertificate( + newClient.Account, + cert, + certKey, + tc.reason, + ) + test.AssertNotError(t, err, "revocation should have succeeded") + + case byAdmin: + // Invoke the admin tool to perform the revocation via gRPC, rather than + // using the external-facing ACME API. + config := fmt.Sprintf("%s/%s", os.Getenv("BOULDER_CONFIG_DIR"), "admin.json") + cmd := exec.Command( + "./bin/admin", + "-config", config, + "-dry-run=false", + "revoke-cert", + "-serial", core.SerialToString(cert.SerialNumber), + "-reason", revocation.ReasonToString[revocation.Reason(tc.reason)]) + output, err := cmd.CombinedOutput() + t.Logf("admin revoke-cert output: %s\n", string(output)) + test.AssertNotError(t, err, "revocation should have succeeded") + + default: + t.Fatalf("unrecognized revocation method %q", tc.method) + } + + return cert + } + + // revocationCheck represents a deferred that a specific certificate is revoked. + // + // We defer these checks for performance reasons: we want to run crl-updater once, + // after all certificates have been revoked. + type revocationCheck func(t *testing.T, allCRLs map[string][]*x509.RevocationList) + var revocationChecks []revocationCheck + var rcMu sync.Mutex + var wg sync.WaitGroup + for _, kind := range []certKind{precert, finalcert} { - for _, reason := range []int{ocsp.Unspecified, ocsp.KeyCompromise} { - for _, method := range []authMethod{byAccount, byAuth, byKey} { - testCases = append(testCases, testCase{ - method: method, - reason: reason, - kind: kind, - // We do not expect any of these revocation requests to error. - // The ones done byAccount will succeed as requested, but will not - // result in the key being blocked for future issuance. - // The ones done byAuth will succeed, but will be overwritten to have - // reason code 5 (cessationOfOperation). - // The ones done byKey will succeed, but will be overwritten to have - // reason code 1 (keyCompromise), and will block the key. - }) + for _, reason := range []int{ocsp.Unspecified, ocsp.KeyCompromise, ocsp.Superseded} { + for _, method := range []authMethod{byAccount, byAuth, byKey, byAdmin} { + wg.Add(1) + go func() { + defer wg.Done() + cert := issueAndRevoke(testCase{ + method: method, + reason: reason, + kind: kind, + // We do not expect any of these revocation requests to error. + // The ones done byAccount will succeed as requested, but will not + // result in the key being blocked for future issuance. + // The ones done byAuth will succeed, but will be overwritten to have + // reason code 5 (cessationOfOperation). + // The ones done byKey will succeed, but will be overwritten to have + // reason code 1 (keyCompromise), and will block the key. + }) + + // If the request was made by demonstrating control over the + // names, the reason should be overwritten to CessationOfOperation (5), + // and if the request was made by key, then the reason should be set to + // KeyCompromise (1). + expectedReason := reason + switch method { + case byAuth: + expectedReason = ocsp.CessationOfOperation + case byKey: + expectedReason = ocsp.KeyCompromise + default: + } + + check := func(t *testing.T, allCRLs map[string][]*x509.RevocationList) { + ocspConfig := ocspConf().WithExpectStatus(ocsp.Revoked).WithExpectReason(expectedReason) + _, err := ocsp_helper.ReqDER(cert.Raw, ocspConfig) + test.AssertNotError(t, err, "requesting OCSP for revoked cert") + + checkRevoked(t, allCRLs, cert, expectedReason) + } + + rcMu.Lock() + revocationChecks = append(revocationChecks, check) + rcMu.Unlock() + }() } } } - for _, tc := range testCases { - name := fmt.Sprintf("%s_%d_%s", tc.kind, tc.reason, tc.method) - t.Run(name, func(t *testing.T) { - issueClient, err := makeClient() - test.AssertNotError(t, err, "creating acme client") + wg.Wait() - certKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - test.AssertNotError(t, err, "creating random cert key") + runUpdater(t, path.Join(os.Getenv("BOULDER_CONFIG_DIR"), "crl-updater.json")) + allCRLs := getAllCRLs(t) - domain := random_domain() - - // Try to issue a certificate for the name. - var cert *x509.Certificate - switch tc.kind { - case finalcert: - res, err := authAndIssue(issueClient, certKey, []string{domain}, true) - test.AssertNotError(t, err, "authAndIssue failed") - cert = res.certs[0] - - case precert: - // Make sure the ct-test-srv will reject generating SCTs for the domain, - // so we only get a precert and no final cert. - err := ctAddRejectHost(domain) - test.AssertNotError(t, err, "adding ct-test-srv reject host") - - _, err = authAndIssue(issueClient, certKey, []string{domain}, true) - test.AssertError(t, err, "expected error from authAndIssue, was nil") - if !strings.Contains(err.Error(), "urn:ietf:params:acme:error:serverInternal") || - !strings.Contains(err.Error(), "SCT embedding") { - t.Fatal(err) - } - - // Instead recover the precertificate from CT. - cert, err = ctFindRejection([]string{domain}) - if err != nil || cert == nil { - t.Fatalf("couldn't find rejected precert for %q", domain) - } - // And make sure the cert we found is in fact a precert. - if !isPrecert(cert) { - t.Fatal("precert was missing poison extension") - } - - default: - t.Fatalf("unrecognized cert kind %q", tc.kind) - } - - // Initially, the cert should have a Good OCSP response. - ocspConfig := ocsp_helper.DefaultConfig.WithExpectStatus(ocsp.Good) - _, err = ocsp_helper.ReqDER(cert.Raw, ocspConfig) - test.AssertNotError(t, err, "requesting OCSP for precert") - - // Set up the account and key that we'll use to revoke the cert. - var revokeClient *client - var revokeKey crypto.Signer - switch tc.method { - case byAccount: - // When revoking by account, use the same client and key as were used - // for the original issuance. - revokeClient = issueClient - revokeKey = revokeClient.PrivateKey - - case byAuth: - // When revoking by auth, create a brand new client, authorize it for - // the same domain, and use that account and key for revocation. Ignore - // errors from authAndIssue because all we need is the auth, not the - // issuance. - revokeClient, err = makeClient() - test.AssertNotError(t, err, "creating second acme client") - _, _ = authAndIssue(revokeClient, certKey, []string{domain}, true) - revokeKey = revokeClient.PrivateKey - - case byKey: - // When revoking by key, create a brand new client and use it with - // the cert's key for revocation. - revokeClient, err = makeClient() - test.AssertNotError(t, err, "creating second acme client") - revokeKey = certKey - - default: - t.Fatalf("unrecognized revocation method %q", tc.method) - } - - // Revoke the cert using the specified key and client. - err = revokeClient.RevokeCertificate( - revokeClient.Account, - cert, - revokeKey, - tc.reason, - ) - - test.AssertNotError(t, err, "revocation should have succeeded") - - // Check the OCSP response for the certificate again. It should now be - // revoked. If the request was made by demonstrating control over the - // names, the reason should be overwritten to CessationOfOperation (5), - // and if the request was made by key, then the reason should be set to - // KeyCompromise (1). - ocspConfig = ocsp_helper.DefaultConfig.WithExpectStatus(ocsp.Revoked) - switch tc.method { - case byAuth: - ocspConfig = ocspConfig.WithExpectReason(ocsp.CessationOfOperation) - case byKey: - ocspConfig = ocspConfig.WithExpectReason(ocsp.KeyCompromise) - default: - ocspConfig = ocspConfig.WithExpectReason(tc.reason) - } - _, err = ocsp_helper.ReqDER(cert.Raw, ocspConfig) - test.AssertNotError(t, err, "requesting OCSP for revoked cert") - }) + for _, check := range revocationChecks { + check(t, allCRLs) } } @@ -198,8 +431,6 @@ func TestRevocation(t *testing.T) { // In which case the revocation reason (but not revocation date) will be // updated to be keyCompromise. func TestReRevocation(t *testing.T) { - t.Parallel() - type authMethod string var ( byAccount authMethod = "byAccount" @@ -231,13 +462,13 @@ func TestReRevocation(t *testing.T) { test.AssertNotError(t, err, "creating random cert key") // Try to issue a certificate for the name. - domain := random_domain() - res, err := authAndIssue(issueClient, certKey, []string{domain}, true) + res, err := authAndIssue(issueClient, certKey, []acme.Identifier{{Type: "dns", Value: random_domain()}}, true, "") test.AssertNotError(t, err, "authAndIssue failed") cert := res.certs[0] + issuer := res.certs[1] - // Initially, the cert should have a Good OCSP response. - ocspConfig := ocsp_helper.DefaultConfig.WithExpectStatus(ocsp.Good) + // Initially, the cert should have a Good OCSP response (only via OCSP; the CRL is unchanged by issuance). + ocspConfig := ocspConf().WithExpectStatus(ocsp.Good) _, err = ocsp_helper.ReqDER(cert.Raw, ocspConfig) test.AssertNotError(t, err, "requesting OCSP for precert") @@ -271,11 +502,10 @@ func TestReRevocation(t *testing.T) { ) test.AssertNotError(t, err, "initial revocation should have succeeded") - // Check the OCSP response for the certificate again. It should now be + // Check the CRL for the certificate again. It should now be // revoked. - ocspConfig = ocsp_helper.DefaultConfig.WithExpectStatus(ocsp.Revoked).WithExpectReason(tc.reason1) - _, err = ocsp_helper.ReqDER(cert.Raw, ocspConfig) - test.AssertNotError(t, err, "requesting OCSP for revoked cert") + runUpdater(t, path.Join(os.Getenv("BOULDER_CONFIG_DIR"), "crl-updater.json")) + fetchAndCheckRevoked(t, cert, issuer, tc.reason1) // Set up the account and key that we'll use to *re*-revoke the cert. switch tc.method2 { @@ -310,26 +540,30 @@ func TestReRevocation(t *testing.T) { // Check the OCSP response for the certificate again. It should still be // revoked, with the same reason. - ocspConfig = ocsp_helper.DefaultConfig.WithExpectStatus(ocsp.Revoked).WithExpectReason(tc.reason1) + ocspConfig := ocspConf().WithExpectStatus(ocsp.Revoked).WithExpectReason(tc.reason1) _, err = ocsp_helper.ReqDER(cert.Raw, ocspConfig) - test.AssertNotError(t, err, "requesting OCSP for revoked cert") + // Check the CRL for the certificate again. It should still be + // revoked, with the same reason. + runUpdater(t, path.Join(os.Getenv("BOULDER_CONFIG_DIR"), "crl-updater.json")) + fetchAndCheckRevoked(t, cert, issuer, tc.reason1) case false: test.AssertNotError(t, err, "second revocation should have succeeded") // Check the OCSP response for the certificate again. It should now be // revoked with reason keyCompromise. - ocspConfig = ocsp_helper.DefaultConfig.WithExpectStatus(ocsp.Revoked).WithExpectStatus(tc.reason2) + ocspConfig := ocspConf().WithExpectStatus(ocsp.Revoked).WithExpectReason(tc.reason2) _, err = ocsp_helper.ReqDER(cert.Raw, ocspConfig) - test.AssertNotError(t, err, "requesting OCSP for revoked cert") + // Check the CRL for the certificate again. It should now be + // revoked with reason keyCompromise. + runUpdater(t, path.Join(os.Getenv("BOULDER_CONFIG_DIR"), "crl-updater.json")) + fetchAndCheckRevoked(t, cert, issuer, tc.reason2) } }) } } func TestRevokeWithKeyCompromiseBlocksKey(t *testing.T) { - t.Parallel() - type authMethod string var ( byAccount authMethod = "byAccount" @@ -346,9 +580,14 @@ func TestRevokeWithKeyCompromiseBlocksKey(t *testing.T) { certKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) test.AssertNotError(t, err, "failed to generate cert key") - res, err := authAndIssue(c, certKey, []string{random_domain()}, true) + res, err := authAndIssue(c, certKey, []acme.Identifier{{Type: "dns", Value: random_domain()}}, true, "") test.AssertNotError(t, err, "authAndIssue failed") cert := res.certs[0] + issuer := res.certs[1] + + ocspConfig := ocspConf().WithExpectStatus(ocsp.Good) + _, err = ocsp_helper.ReqDER(cert.Raw, ocspConfig) + test.AssertNotError(t, err, "requesting OCSP for not yet revoked cert") // Revoke the cert with reason keyCompromise, either authenticated via the // issuing account, or via the certificate key itself. @@ -361,10 +600,13 @@ func TestRevokeWithKeyCompromiseBlocksKey(t *testing.T) { test.AssertNotError(t, err, "failed to revoke certificate") // Check the OCSP response. It should be revoked with reason = 1 (keyCompromise). - ocspConfig := ocsp_helper.DefaultConfig.WithExpectStatus(ocsp.Revoked).WithExpectReason(ocsp.KeyCompromise) + ocspConfig = ocspConf().WithExpectStatus(ocsp.Revoked).WithExpectReason(ocsp.KeyCompromise) _, err = ocsp_helper.ReqDER(cert.Raw, ocspConfig) test.AssertNotError(t, err, "requesting OCSP for revoked cert") + runUpdater(t, path.Join(os.Getenv("BOULDER_CONFIG_DIR"), "crl-updater.json")) + fetchAndCheckRevoked(t, cert, issuer, ocsp.KeyCompromise) + // Attempt to create a new account using the compromised key. This should // work when the key was just *reported* as compromised, but fail when // the compromise was demonstrated/proven. @@ -374,38 +616,31 @@ func TestRevokeWithKeyCompromiseBlocksKey(t *testing.T) { test.AssertNotError(t, err, "NewAccount failed with a non-blocklisted key") case byKey: test.AssertError(t, err, "NewAccount didn't fail with a blocklisted key") - test.AssertEquals(t, err.Error(), `acme: error code 400 "urn:ietf:params:acme:error:badPublicKey": public key is forbidden`) + test.AssertEquals(t, err.Error(), `acme: error code 400 "urn:ietf:params:acme:error:badPublicKey": Unable to validate JWS :: invalid request signing key: public key is forbidden`) } } } func TestBadKeyRevoker(t *testing.T) { - // Both accounts have two email addresses, one of which is shared between - // them. All three addresses should receive mail, because the revocation - // request is signed by the certificate key, not an account key, so we don't - // know who requested the revocation. Finally, a third account with no address - // to ensure the bad-key-revoker handles that gracefully. - revokerClient, err := makeClient("mailto:revoker@letsencrypt.org", "mailto:shared@letsencrypt.org") + revokerClient, err := makeClient() test.AssertNotError(t, err, "creating acme client") - revokeeClient, err := makeClient("mailto:shared@letsencrypt.org", "mailto:revokee@letsencrypt.org") - test.AssertNotError(t, err, "creating acme client") - noContactClient, err := makeClient() + revokeeClient, err := makeClient() test.AssertNotError(t, err, "creating acme client") certKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) test.AssertNotError(t, err, "failed to generate cert key") - res, err := authAndIssue(revokerClient, certKey, []string{random_domain()}, true) + res, err := authAndIssue(revokerClient, certKey, []acme.Identifier{{Type: "dns", Value: random_domain()}}, true, "") test.AssertNotError(t, err, "authAndIssue failed") badCert := res.certs[0] t.Logf("Generated to-be-revoked cert with serial %x", badCert.SerialNumber) - certs := []*x509.Certificate{} - for _, c := range []*client{revokerClient, revokeeClient, noContactClient} { - cert, err := authAndIssue(c, certKey, []string{random_domain()}, true) - t.Logf("TestBadKeyRevoker: Issued cert with serial %x", cert.certs[0].SerialNumber) + bundles := []*issuanceResult{} + for _, c := range []*client{revokerClient, revokeeClient} { + bundle, err := authAndIssue(c, certKey, []acme.Identifier{{Type: "dns", Value: random_domain()}}, true, "") + t.Logf("TestBadKeyRevoker: Issued cert with serial %x", bundle.certs[0].SerialNumber) test.AssertNotError(t, err, "authAndIssue failed") - certs = append(certs, cert.certs[0]) + bundles = append(bundles, bundle) } err = revokerClient.RevokeCertificate( @@ -416,11 +651,12 @@ func TestBadKeyRevoker(t *testing.T) { ) test.AssertNotError(t, err, "failed to revoke certificate") - ocspConfig := ocsp_helper.DefaultConfig.WithExpectStatus(ocsp.Revoked).WithExpectReason(ocsp.KeyCompromise) + ocspConfig := ocspConf().WithExpectStatus(ocsp.Revoked).WithExpectReason(ocsp.KeyCompromise) _, err = ocsp_helper.ReqDER(badCert.Raw, ocspConfig) test.AssertNotError(t, err, "ReqDER failed") - for _, cert := range certs { + for _, bundle := range bundles { + cert := bundle.certs[0] for i := range 5 { t.Logf("TestBadKeyRevoker: Requesting OCSP for cert with serial %x (attempt %d)", cert.SerialNumber, i) _, err := ocsp_helper.ReqDER(cert.Raw, ocspConfig) @@ -436,54 +672,34 @@ func TestBadKeyRevoker(t *testing.T) { } } - revokeeCount, err := http.Get("http://boulder.service.consul:9381/count?to=revokee@letsencrypt.org&from=bad-key-revoker@test.org") - test.AssertNotError(t, err, "mail-test-srv GET /count failed") - defer func() { _ = revokeeCount.Body.Close() }() - body, err := io.ReadAll(revokeeCount.Body) - test.AssertNotError(t, err, "failed to read body") - test.AssertEquals(t, string(body), "1\n") - - revokerCount, err := http.Get("http://boulder.service.consul:9381/count?to=revoker@letsencrypt.org&from=bad-key-revoker@test.org") - test.AssertNotError(t, err, "mail-test-srv GET /count failed") - defer func() { _ = revokerCount.Body.Close() }() - body, err = io.ReadAll(revokerCount.Body) - test.AssertNotError(t, err, "failed to read body") - test.AssertEquals(t, string(body), "1\n") - - sharedCount, err := http.Get("http://boulder.service.consul:9381/count?to=shared@letsencrypt.org&from=bad-key-revoker@test.org") - test.AssertNotError(t, err, "mail-test-srv GET /count failed") - defer func() { _ = sharedCount.Body.Close() }() - body, err = io.ReadAll(sharedCount.Body) - test.AssertNotError(t, err, "failed to read body") - test.AssertEquals(t, string(body), "1\n") + runUpdater(t, path.Join(os.Getenv("BOULDER_CONFIG_DIR"), "crl-updater.json")) + for _, bundle := range bundles { + cert := bundle.certs[0] + issuer := bundle.certs[1] + fetchAndCheckRevoked(t, cert, issuer, ocsp.KeyCompromise) + } } func TestBadKeyRevokerByAccount(t *testing.T) { - // Both accounts have two email addresses, one of which is shared between - // them. No accounts should receive any mail, because the revocation request - // is signed by the account key (not the cert key) and so will not be - // propagated to other certs sharing the same key. - revokerClient, err := makeClient("mailto:revoker-moz@letsencrypt.org", "mailto:shared-moz@letsencrypt.org") + revokerClient, err := makeClient() test.AssertNotError(t, err, "creating acme client") - revokeeClient, err := makeClient("mailto:shared-moz@letsencrypt.org", "mailto:revokee-moz@letsencrypt.org") - test.AssertNotError(t, err, "creating acme client") - noContactClient, err := makeClient() + revokeeClient, err := makeClient() test.AssertNotError(t, err, "creating acme client") certKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) test.AssertNotError(t, err, "failed to generate cert key") - res, err := authAndIssue(revokerClient, certKey, []string{random_domain()}, true) + res, err := authAndIssue(revokerClient, certKey, []acme.Identifier{{Type: "dns", Value: random_domain()}}, true, "") test.AssertNotError(t, err, "authAndIssue failed") badCert := res.certs[0] t.Logf("Generated to-be-revoked cert with serial %x", badCert.SerialNumber) - certs := []*x509.Certificate{} - for _, c := range []*client{revokerClient, revokeeClient, noContactClient} { - cert, err := authAndIssue(c, certKey, []string{random_domain()}, true) - t.Logf("TestBadKeyRevokerByAccount: Issued cert with serial %x", cert.certs[0].SerialNumber) + bundles := []*issuanceResult{} + for _, c := range []*client{revokerClient, revokeeClient} { + bundle, err := authAndIssue(c, certKey, []acme.Identifier{{Type: "dns", Value: random_domain()}}, true, "") + t.Logf("TestBadKeyRevokerByAccount: Issued cert with serial %x", bundle.certs[0].SerialNumber) test.AssertNotError(t, err, "authAndIssue failed") - certs = append(certs, cert.certs[0]) + bundles = append(bundles, bundle) } err = revokerClient.RevokeCertificate( @@ -494,45 +710,25 @@ func TestBadKeyRevokerByAccount(t *testing.T) { ) test.AssertNotError(t, err, "failed to revoke certificate") - ocspConfig := ocsp_helper.DefaultConfig.WithExpectStatus(ocsp.Revoked).WithExpectReason(ocsp.KeyCompromise) + ocspConfig := ocspConf().WithExpectStatus(ocsp.Revoked).WithExpectReason(ocsp.KeyCompromise) _, err = ocsp_helper.ReqDER(badCert.Raw, ocspConfig) test.AssertNotError(t, err, "ReqDER failed") - ocspConfig = ocsp_helper.DefaultConfig.WithExpectStatus(ocsp.Good) - for _, cert := range certs { - for i := range 5 { - t.Logf("TestBadKeyRevoker: Requesting OCSP for cert with serial %x (attempt %d)", cert.SerialNumber, i) - _, err := ocsp_helper.ReqDER(cert.Raw, ocspConfig) - if err != nil { - t.Logf("TestBadKeyRevoker: Got bad response: %s", err.Error()) - if i >= 4 { - t.Fatal("timed out waiting for correct OCSP status") - } - time.Sleep(time.Second) - continue - } - break + // Note: this test is inherently racy because we don't have a signal + // for when the bad-key-revoker has completed a run after the revocation. However, + // the bad-key-revoker's configured interval is 50ms, so sleeping 1s should be good enough. + time.Sleep(time.Second) + + runUpdater(t, path.Join(os.Getenv("BOULDER_CONFIG_DIR"), "crl-updater.json")) + allCRLs := getAllCRLs(t) + ocspConfig = ocspConf().WithExpectStatus(ocsp.Good) + for _, bundle := range bundles { + cert := bundle.certs[0] + t.Logf("TestBadKeyRevoker: Requesting OCSP for cert with serial %x", cert.SerialNumber) + _, err := ocsp_helper.ReqDER(cert.Raw, ocspConfig) + if err != nil { + t.Error(err) } + checkUnrevoked(t, allCRLs, cert) } - - revokeeCount, err := http.Get("http://boulder.service.consul:9381/count?to=revokee-moz@letsencrypt.org&from=bad-key-revoker@test.org") - test.AssertNotError(t, err, "mail-test-srv GET /count failed") - defer func() { _ = revokeeCount.Body.Close() }() - body, err := io.ReadAll(revokeeCount.Body) - test.AssertNotError(t, err, "failed to read body") - test.AssertEquals(t, string(body), "0\n") - - revokerCount, err := http.Get("http://boulder.service.consul:9381/count?to=revoker-moz@letsencrypt.org&from=bad-key-revoker@test.org") - test.AssertNotError(t, err, "mail-test-srv GET /count failed") - defer func() { _ = revokerCount.Body.Close() }() - body, err = io.ReadAll(revokerCount.Body) - test.AssertNotError(t, err, "failed to read body") - test.AssertEquals(t, string(body), "0\n") - - sharedCount, err := http.Get("http://boulder.service.consul:9381/count?to=shared-moz@letsencrypt.org&from=bad-key-revoker@test.org") - test.AssertNotError(t, err, "mail-test-srv GET /count failed") - defer func() { _ = sharedCount.Body.Close() }() - body, err = io.ReadAll(sharedCount.Body) - test.AssertNotError(t, err, "failed to read body") - test.AssertEquals(t, string(body), "0\n") } diff --git a/third-party/github.com/letsencrypt/boulder/test/integration/srv_resolver_test.go b/third-party/github.com/letsencrypt/boulder/test/integration/srv_resolver_test.go index c92575bfb..5ec88465a 100644 --- a/third-party/github.com/letsencrypt/boulder/test/integration/srv_resolver_test.go +++ b/third-party/github.com/letsencrypt/boulder/test/integration/srv_resolver_test.go @@ -96,7 +96,7 @@ func TestSRVResolver_CaseThree(t *testing.T) { gnc := nonce.NewGetter(getNonceConn) _, err = gnc.Nonce(context.Background(), &emptypb.Empty{}) test.AssertError(t, err, "Expected error getting nonce") - test.AssertContains(t, err.Error(), "last resolver error: produced zero addresses") + test.AssertContains(t, err.Error(), "no children to pick from") } func TestSRVResolver_CaseFour(t *testing.T) { @@ -117,5 +117,5 @@ func TestSRVResolver_CaseFour(t *testing.T) { gnc := nonce.NewGetter(getNonceConn4) _, err = gnc.Nonce(context.Background(), &emptypb.Empty{}) test.AssertError(t, err, "Expected error getting nonce") - test.AssertContains(t, err.Error(), "last resolver error: produced zero addresses") + test.AssertContains(t, err.Error(), "no children to pick from") } diff --git a/third-party/github.com/letsencrypt/boulder/test/integration/subordinate_ca_chains_test.go b/third-party/github.com/letsencrypt/boulder/test/integration/subordinate_ca_chains_test.go index 0aceb6a3e..f54069c4f 100644 --- a/third-party/github.com/letsencrypt/boulder/test/integration/subordinate_ca_chains_test.go +++ b/third-party/github.com/letsencrypt/boulder/test/integration/subordinate_ca_chains_test.go @@ -6,28 +6,24 @@ import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" - "os" "strings" "testing" + "github.com/eggsampler/acme/v3" + "github.com/letsencrypt/boulder/test" ) func TestSubordinateCAChainsServedByWFE(t *testing.T) { t.Parallel() - if os.Getenv("BOULDER_CONFIG_DIR") != "test/config-next" { - t.Skip("Skipping test in config") - } - client, err := makeClient("mailto:example@letsencrypt.org") test.AssertNotError(t, err, "creating acme client") key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) test.AssertNotError(t, err, "creating random cert key") - name := random_domain() - chains, err := authAndIssueFetchAllChains(client, key, []string{name}, true) + chains, err := authAndIssueFetchAllChains(client, key, []acme.Identifier{{Type: "dns", Value: random_domain()}}, true) test.AssertNotError(t, err, "failed to issue test cert") // An ECDSA intermediate signed by an ECDSA root, and an ECDSA cross-signed by an RSA root. diff --git a/third-party/github.com/letsencrypt/boulder/test/integration/testdata/fermat_csr.go b/third-party/github.com/letsencrypt/boulder/test/integration/testdata/fermat_csr.go new file mode 100644 index 000000000..d9a68bd19 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/test/integration/testdata/fermat_csr.go @@ -0,0 +1,99 @@ +package main + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "log" + "math/big" + "os" +) + +const ( + // bits is the size of the resulting RSA key, also known as "nlen" or "Length + // of the modulus N". Usually 1024, 2048, or 4096. + bits = 2048 + // gap is the exponent of the different between the prime factors of the RSA + // key, i.e. |p-q| ~= 2^gap. For FIPS compliance, set this to (bits/2 - 100). + gap = 516 +) + +func main() { + // Generate q, which will be the smaller of the two factors. We set its length + // so that the product of two similarly-sized factors will be the desired + // bit length. + q, err := rand.Prime(rand.Reader, (bits+1)/2) + if err != nil { + log.Fatalln(err) + } + + // Our starting point for p is q + 2^gap. + p := new(big.Int).Add(q, new(big.Int).Exp(big.NewInt(2), big.NewInt(gap), nil)) + + // Now we just keep incrementing P until we find a prime. You might think + // this would take a while, but it won't: there are a lot of primes. + attempts := 0 + for { + // Using 34 rounds of Miller-Rabin primality testing is enough for the go + // stdlib, so it's enough for us. + if p.ProbablyPrime(34) { + break + } + + // We know P is odd because it started as a prime (odd) plus a power of two + // (even), so we can increment by 2 to remain odd. + p.Add(p, big.NewInt(2)) + attempts++ + } + + fmt.Println("p:", p.String()) + fmt.Println("q:", q.String()) + fmt.Println("Differ by", fmt.Sprintf("2^%d + %d", gap, 2*attempts)) + + // Construct the public modulus N from the prime factors. + n := new(big.Int).Mul(p, q) + + // Construct the public key from the modulus and (fixed) public exponent. + pubkey := rsa.PublicKey{ + N: n, + E: 65537, + } + + // Construct the private exponent D from the prime factors. + p_1 := new(big.Int).Sub(p, big.NewInt(1)) + q_1 := new(big.Int).Sub(q, big.NewInt(1)) + field := new(big.Int).Mul(p_1, q_1) + d := new(big.Int).ModInverse(big.NewInt(65537), field) + + // Construct the private key from the factors and private exponent. + privkey := rsa.PrivateKey{ + PublicKey: pubkey, + D: d, + Primes: []*big.Int{p, q}, + } + privkey.Precompute() + + // Sign a CSR using this key, so we can use it in integration tests. + // Note that this step *only works on go1.23 and earlier*. Later versions of + // go detect that the prime factors are too close together and refuse to + // produce a signature. + csrDER, err := x509.CreateCertificateRequest( + rand.Reader, + &x509.CertificateRequest{ + Subject: pkix.Name{CommonName: "example.com"}, + PublicKey: &pubkey, + }, + &privkey) + if err != nil { + log.Fatalln(err) + } + + csrPEM := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE REQUEST", + Bytes: csrDER, + }) + fmt.Fprint(os.Stdout, string(csrPEM)) +} diff --git a/third-party/github.com/letsencrypt/boulder/test/integration/testdata/fermat_csr.pem b/third-party/github.com/letsencrypt/boulder/test/integration/testdata/fermat_csr.pem new file mode 100644 index 000000000..39966cf60 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/test/integration/testdata/fermat_csr.pem @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICWzCCAUMCAQAwFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQCVIY5cKFJU+qqXCtls7VA+oAwcDnsIk3W8+4ZO +y5vKEk3Ye9rWglKPqHSDvr4UdEv5cP6RaByWaL7PUswIPwQD8HFywR84V82+3pl+ +sEVo88M3HK1ZwI19FcmsaZn3Zh0gVymEYi4VJof2toYUK8M2DRjJGvVrnpG2P6y0 +VKpq7jBTR6G8PXr4q2JjGJaBci1Bzw2sWMUcyfOdIpdKpe185e7WSl9N0YT4pg7t +lHMoGHWYPQ6Pd7TR6EmGzKs+MThsWhREx91ViA9UmYe4n607lGevm2nHV2PJ09PR +tn+136BIE30E4uVgPVuHp5y36PKylfA5NHA9M0TMgpn0AK0/AgMBAAGgADANBgkq +hkiG9w0BAQsFAAOCAQEAk3xNRIahAtVzlygRwh57gRBqEi6uJXh651rNSSdvk1YA +MR4bhkA9IXSwrOlb8euRWGdRMnxSqx+16OqZ0MDGrTMg3RaQoSkmFo28zbMNtHgd +4243lzDF0KrZCSyQHh9bSmcMuPjbCRPZJObg70ALw1K2pdrUamTh7EjKWPbGA3hg +lrfl9RsMzC/6UDUoMUyCHRJx6pT6t6PwDl8g+tesQemnVxKNEY8WZOyf/1uEEhNb +1PmpgfnV+NQp3sOXSLsxlDpl0zRlbWq6QGnvW2O6FalxoVSZ3WIXX/FyT2rxePWg +LDaCwR0qj4byFL2On7FsbU4Wfx6bD70cplaxfv8uQQ== +-----END CERTIFICATE REQUEST----- diff --git a/third-party/github.com/letsencrypt/boulder/test/integration/testdata/nonce-client.json b/third-party/github.com/letsencrypt/boulder/test/integration/testdata/nonce-client.json index 90e84706b..a66077e26 100644 --- a/third-party/github.com/letsencrypt/boulder/test/integration/testdata/nonce-client.json +++ b/third-party/github.com/letsencrypt/boulder/test/integration/testdata/nonce-client.json @@ -32,8 +32,8 @@ "noWaitForReady": true, "hostOverride": "nonce.boulder" }, - "noncePrefixKey": { - "passwordFile": "test/secrets/nonce_prefix_key" + "nonceHMACKey": { + "keyFile": "test/secrets/nonce_prefix_key" } } } diff --git a/third-party/github.com/letsencrypt/boulder/test/integration/validation_test.go b/third-party/github.com/letsencrypt/boulder/test/integration/validation_test.go new file mode 100644 index 000000000..cd7ac413d --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/test/integration/validation_test.go @@ -0,0 +1,345 @@ +//go:build integration + +package integration + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "database/sql" + "slices" + "strings" + "testing" + "time" + + "github.com/eggsampler/acme/v3" + "github.com/miekg/dns" + + challtestsrvclient "github.com/letsencrypt/boulder/test/chall-test-srv-client" + "github.com/letsencrypt/boulder/test/vars" +) + +var expectedUserAgents = []string{"boulder", "remoteva-a", "remoteva-b", "remoteva-c"} + +func collectUserAgentsFromDNSRequests(requests []challtestsrvclient.DNSRequest) []string { + userAgents := make([]string, len(requests)) + for i, request := range requests { + userAgents[i] = request.UserAgent + } + return userAgents +} + +func assertUserAgentsLength(t *testing.T, got []string, checkType string) { + t.Helper() + + if len(got) != 4 { + t.Errorf("During %s, expected 4 User-Agents, got %d", checkType, len(got)) + } +} + +func assertExpectedUserAgents(t *testing.T, got []string, checkType string) { + t.Helper() + + for _, ua := range expectedUserAgents { + if !slices.Contains(got, ua) { + t.Errorf("During %s, expected User-Agent %q in %s (got %v)", checkType, ua, expectedUserAgents, got) + } + } +} + +func TestMPICTLSALPN01(t *testing.T) { + t.Parallel() + + client, err := makeClient() + if err != nil { + t.Fatalf("creating acme client: %s", err) + } + + domain := randomDomain(t) + + order, err := client.Client.NewOrder(client.Account, []acme.Identifier{{Type: "dns", Value: domain}}) + if err != nil { + t.Fatalf("creating order: %s", err) + } + + authz, err := client.Client.FetchAuthorization(client.Account, order.Authorizations[0]) + if err != nil { + t.Fatalf("fetching authorization: %s", err) + } + + chal, ok := authz.ChallengeMap[acme.ChallengeTypeTLSALPN01] + if !ok { + t.Fatalf("no TLS-ALPN-01 challenge found in %#v", authz) + } + + _, err = testSrvClient.AddARecord(domain, []string{"64.112.117.134"}) + if err != nil { + t.Fatalf("adding A record: %s", err) + } + defer func() { + testSrvClient.RemoveARecord(domain) + }() + + _, err = testSrvClient.AddTLSALPN01Response(domain, chal.KeyAuthorization) + if err != nil { + t.Fatal(err) + } + defer func() { + _, err = testSrvClient.RemoveTLSALPN01Response(domain) + if err != nil { + t.Fatal(err) + } + }() + + chal, err = client.Client.UpdateChallenge(client.Account, chal) + if err != nil { + t.Fatalf("completing TLS-ALPN-01 validation: %s", err) + } + + validationEvents, err := testSrvClient.TLSALPN01RequestHistory(domain) + if err != nil { + t.Fatal(err) + } + if len(validationEvents) != 4 { + t.Errorf("expected 4 validation events got %d", len(validationEvents)) + } + + dnsEvents, err := testSrvClient.DNSRequestHistory(domain) + if err != nil { + t.Fatal(err) + } + + var caaEvents []challtestsrvclient.DNSRequest + for _, event := range dnsEvents { + if event.Question.Qtype == dns.TypeCAA { + caaEvents = append(caaEvents, event) + } + } + assertUserAgentsLength(t, collectUserAgentsFromDNSRequests(caaEvents), "CAA check") + assertExpectedUserAgents(t, collectUserAgentsFromDNSRequests(caaEvents), "CAA check") +} + +func TestMPICDNS01(t *testing.T) { + t.Parallel() + + client, err := makeClient() + if err != nil { + t.Fatalf("creating acme client: %s", err) + } + + domain := randomDomain(t) + + order, err := client.Client.NewOrder(client.Account, []acme.Identifier{{Type: "dns", Value: domain}}) + if err != nil { + t.Fatalf("creating order: %s", err) + } + + authz, err := client.Client.FetchAuthorization(client.Account, order.Authorizations[0]) + if err != nil { + t.Fatalf("fetching authorization: %s", err) + } + + chal, ok := authz.ChallengeMap[acme.ChallengeTypeDNS01] + if !ok { + t.Fatalf("no DNS challenge found in %#v", authz) + } + + _, err = testSrvClient.AddDNS01Response(domain, chal.KeyAuthorization) + if err != nil { + t.Fatal(err) + } + defer func() { + _, err = testSrvClient.RemoveDNS01Response(domain) + if err != nil { + t.Fatal(err) + } + }() + + chal, err = client.Client.UpdateChallenge(client.Account, chal) + if err != nil { + t.Fatalf("completing DNS-01 validation: %s", err) + } + + challDomainDNSEvents, err := testSrvClient.DNSRequestHistory("_acme-challenge." + domain) + if err != nil { + t.Fatal(err) + } + + var validationEvents []challtestsrvclient.DNSRequest + for _, event := range challDomainDNSEvents { + if event.Question.Qtype == dns.TypeTXT && event.Question.Name == "_acme-challenge."+domain+"." { + validationEvents = append(validationEvents, event) + } + } + assertUserAgentsLength(t, collectUserAgentsFromDNSRequests(validationEvents), "DNS-01 validation") + assertExpectedUserAgents(t, collectUserAgentsFromDNSRequests(validationEvents), "DNS-01 validation") + + domainDNSEvents, err := testSrvClient.DNSRequestHistory(domain) + if err != nil { + t.Fatal(err) + } + + var caaEvents []challtestsrvclient.DNSRequest + for _, event := range domainDNSEvents { + if event.Question.Qtype == dns.TypeCAA { + caaEvents = append(caaEvents, event) + } + } + assertUserAgentsLength(t, collectUserAgentsFromDNSRequests(caaEvents), "CAA check") + assertExpectedUserAgents(t, collectUserAgentsFromDNSRequests(caaEvents), "CAA check") +} + +func TestMPICHTTP01(t *testing.T) { + t.Parallel() + + client, err := makeClient() + if err != nil { + t.Fatalf("creating acme client: %s", err) + } + + domain := randomDomain(t) + + order, err := client.Client.NewOrder(client.Account, []acme.Identifier{{Type: "dns", Value: domain}}) + if err != nil { + t.Fatalf("creating order: %s", err) + } + + authz, err := client.Client.FetchAuthorization(client.Account, order.Authorizations[0]) + if err != nil { + t.Fatalf("fetching authorization: %s", err) + } + + chal, ok := authz.ChallengeMap[acme.ChallengeTypeHTTP01] + if !ok { + t.Fatalf("no HTTP challenge found in %#v", authz) + } + + _, err = testSrvClient.AddHTTP01Response(chal.Token, chal.KeyAuthorization) + if err != nil { + t.Fatal(err) + } + defer func() { + _, err = testSrvClient.RemoveHTTP01Response(chal.Token) + if err != nil { + t.Fatal(err) + } + }() + + chal, err = client.Client.UpdateChallenge(client.Account, chal) + if err != nil { + t.Fatalf("completing HTTP-01 validation: %s", err) + } + + validationEvents, err := testSrvClient.HTTPRequestHistory(domain) + if err != nil { + t.Fatal(err) + } + + var validationUAs []string + for _, event := range validationEvents { + if event.URL == "/.well-known/acme-challenge/"+chal.Token { + validationUAs = append(validationUAs, event.UserAgent) + } + } + assertUserAgentsLength(t, validationUAs, "HTTP-01 validation") + assertExpectedUserAgents(t, validationUAs, "HTTP-01 validation") + + dnsEvents, err := testSrvClient.DNSRequestHistory(domain) + if err != nil { + t.Fatal(err) + } + + var caaEvents []challtestsrvclient.DNSRequest + for _, event := range dnsEvents { + if event.Question.Qtype == dns.TypeCAA { + caaEvents = append(caaEvents, event) + } + } + + assertUserAgentsLength(t, collectUserAgentsFromDNSRequests(caaEvents), "CAA check") + assertExpectedUserAgents(t, collectUserAgentsFromDNSRequests(caaEvents), "CAA check") +} + +func TestCAARechecking(t *testing.T) { + t.Parallel() + + domain := randomDomain(t) + idents := []acme.Identifier{{Type: "dns", Value: domain}} + + // Create an order and authorization, and fulfill the associated challenge. + // This should put the authz into the "valid" state, since CAA checks passed. + client, err := makeClient() + if err != nil { + t.Fatalf("creating acme client: %s", err) + } + + order, err := client.Client.NewOrder(client.Account, idents) + if err != nil { + t.Fatalf("creating order: %s", err) + } + + authz, err := client.Client.FetchAuthorization(client.Account, order.Authorizations[0]) + if err != nil { + t.Fatalf("fetching authorization: %s", err) + } + + chal, ok := authz.ChallengeMap[acme.ChallengeTypeHTTP01] + if !ok { + t.Fatalf("no HTTP challenge found in %#v", authz) + } + + _, err = testSrvClient.AddHTTP01Response(chal.Token, chal.KeyAuthorization) + if err != nil { + t.Fatal(err) + } + defer func() { + _, err = testSrvClient.RemoveHTTP01Response(chal.Token) + if err != nil { + t.Fatal(err) + } + }() + + chal, err = client.Client.UpdateChallenge(client.Account, chal) + if err != nil { + t.Fatalf("completing HTTP-01 validation: %s", err) + } + + // Manipulate the database so that it looks like the authz was validated + // more than 8 hours ago. + db, err := sql.Open("mysql", vars.DBConnSAIntegrationFullPerms) + if err != nil { + t.Fatalf("sql.Open: %s", err) + } + + _, err = db.Exec(`UPDATE authz2 SET attemptedAt = ? WHERE identifierValue = ?`, time.Now().Add(-24*time.Hour).Format(time.DateTime), domain) + if err != nil { + t.Fatalf("updating authz attemptedAt timestamp: %s", err) + } + + // Change the CAA record to now forbid issuance. + _, err = testSrvClient.AddCAAIssue(domain, ";") + if err != nil { + t.Fatal(err) + } + + // Try to finalize the order created above. Due to our db manipulation, this + // should trigger a CAA recheck. And due to our challtestsrv manipulation, + // that CAA recheck should fail. Therefore the whole finalize should fail. + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatalf("generating cert key: %s", err) + } + + csr, err := makeCSR(key, idents, false) + if err != nil { + t.Fatalf("generating finalize csr: %s", err) + } + + _, err = client.Client.FinalizeOrder(client.Account, order, csr) + if err == nil { + t.Errorf("expected finalize to fail, but got success") + } + if !strings.Contains(err.Error(), "CAA") { + t.Errorf("expected finalize to fail due to CAA, but got: %s", err) + } +} diff --git a/third-party/github.com/letsencrypt/boulder/test/load-generator/acme/challenge.go b/third-party/github.com/letsencrypt/boulder/test/load-generator/acme/challenge.go index 47e8d861d..12aeb9aa2 100644 --- a/third-party/github.com/letsencrypt/boulder/test/load-generator/acme/challenge.go +++ b/third-party/github.com/letsencrypt/boulder/test/load-generator/acme/challenge.go @@ -3,7 +3,7 @@ package acme import ( "errors" "fmt" - mrand "math/rand" + mrand "math/rand/v2" "strings" "github.com/letsencrypt/boulder/core" @@ -67,7 +67,7 @@ func (strategy randomChallengeStrategy) PickChallenge(authz *core.Authorization) if len(authz.Challenges) == 0 { return nil, ErrPickChallengeAuthzMissingChallenges } - return &authz.Challenges[mrand.Intn(len(authz.Challenges))], nil + return &authz.Challenges[mrand.IntN(len(authz.Challenges))], nil } // preferredTypeChallengeStrategy is a ChallengeStrategy implementation that diff --git a/third-party/github.com/letsencrypt/boulder/test/load-generator/boulder-calls.go b/third-party/github.com/letsencrypt/boulder/test/load-generator/boulder-calls.go index 8f98cade3..02e5ad88c 100644 --- a/third-party/github.com/letsencrypt/boulder/test/load-generator/boulder-calls.go +++ b/third-party/github.com/letsencrypt/boulder/test/load-generator/boulder-calls.go @@ -1,22 +1,20 @@ package main import ( - "bytes" "crypto" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" - "crypto/sha1" "crypto/sha256" "crypto/x509" "encoding/base64" - "encoding/binary" + "encoding/hex" "encoding/json" "encoding/pem" "errors" "fmt" "io" - mrand "math/rand" + mrand "math/rand/v2" "net/http" "time" @@ -51,13 +49,13 @@ type OrderJSON struct { // The URL field isn't returned by the API, we populate it manually with the // `Location` header. URL string - Status core.AcmeStatus `json:"status"` - Expires time.Time `json:"expires"` - Identifiers []identifier.ACMEIdentifier `json:"identifiers"` - Authorizations []string `json:"authorizations"` - Finalize string `json:"finalize"` - Certificate string `json:"certificate,omitempty"` - Error *probs.ProblemDetails `json:"error,omitempty"` + Status core.AcmeStatus `json:"status"` + Expires time.Time `json:"expires"` + Identifiers identifier.ACMEIdentifiers `json:"identifiers"` + Authorizations []string `json:"authorizations"` + Finalize string `json:"finalize"` + Certificate string `json:"certificate,omitempty"` + Error *probs.ProblemDetails `json:"error,omitempty"` } // getAccount takes a randomly selected v2 account from `state.accts` and puts it @@ -72,7 +70,7 @@ func getAccount(s *State, c *acmeCache) error { } // Select a random account from the state and put it into the context - c.acct = s.accts[mrand.Intn(len(s.accts))] + c.acct = s.accts[mrand.IntN(len(s.accts))] c.ns = &nonceSource{s: s} return nil } @@ -153,10 +151,9 @@ func newAccount(s *State, c *acmeCache) error { func randDomain(base string) string { // This approach will cause some repeat domains but not enough to make rate // limits annoying! - n := time.Now().UnixNano() - b := new(bytes.Buffer) - binary.Write(b, binary.LittleEndian, n) - return fmt.Sprintf("%x.%s", sha1.Sum(b.Bytes()), base) + var bytes [3]byte + _, _ = rand.Read(bytes[:]) + return hex.EncodeToString(bytes[:]) + base } // newOrder creates a new pending order object for a random set of domains using @@ -164,20 +161,17 @@ func randDomain(base string) string { func newOrder(s *State, c *acmeCache) error { // Pick a random number of names within the constraints of the maxNamesPerCert // parameter - orderSize := 1 + mrand.Intn(s.maxNamesPerCert-1) + orderSize := 1 + mrand.IntN(s.maxNamesPerCert-1) // Generate that many random domain names. There may be some duplicates, we // don't care. The ACME server will collapse those down for us, how handy! - dnsNames := []identifier.ACMEIdentifier{} + dnsNames := identifier.ACMEIdentifiers{} for range orderSize { - dnsNames = append(dnsNames, identifier.ACMEIdentifier{ - Type: identifier.DNS, - Value: randDomain(s.domainBase), - }) + dnsNames = append(dnsNames, identifier.NewDNS(randDomain(s.domainBase))) } // create the new order request object initOrder := struct { - Identifiers []identifier.ACMEIdentifier + Identifiers identifier.ACMEIdentifiers }{ Identifiers: dnsNames, } @@ -231,7 +225,7 @@ func newOrder(s *State, c *acmeCache) error { // popPendingOrder *removes* a random pendingOrder from the context, returning // it. func popPendingOrder(c *acmeCache) *OrderJSON { - orderIndex := mrand.Intn(len(c.pendingOrders)) + orderIndex := mrand.IntN(len(c.pendingOrders)) order := c.pendingOrders[orderIndex] c.pendingOrders = append(c.pendingOrders[:orderIndex], c.pendingOrders[orderIndex+1:]...) return order @@ -465,7 +459,7 @@ func pollOrderForCert(order *OrderJSON, s *State, c *acmeCache) (*OrderJSON, err // popFulfilledOrder **removes** a fulfilled order from the context, returning // it. Fulfilled orders have all of their authorizations satisfied. func popFulfilledOrder(c *acmeCache) string { - orderIndex := mrand.Intn(len(c.fulfilledOrders)) + orderIndex := mrand.IntN(len(c.fulfilledOrders)) order := c.fulfilledOrders[orderIndex] c.fulfilledOrders = append(c.fulfilledOrders[:orderIndex], c.fulfilledOrders[orderIndex+1:]...) return order @@ -580,7 +574,7 @@ func postAsGet(s *State, c *acmeCache, url string, latencyTag string) (*http.Res } func popCertificate(c *acmeCache) string { - certIndex := mrand.Intn(len(c.certs)) + certIndex := mrand.IntN(len(c.certs)) certURL := c.certs[certIndex] c.certs = append(c.certs[:certIndex], c.certs[certIndex+1:]...) return certURL diff --git a/third-party/github.com/letsencrypt/boulder/test/mail-test-srv/main.go b/third-party/github.com/letsencrypt/boulder/test/mail-test-srv/main.go index 3d13532a5..a7b5adf80 100644 --- a/third-party/github.com/letsencrypt/boulder/test/mail-test-srv/main.go +++ b/third-party/github.com/letsencrypt/boulder/test/mail-test-srv/main.go @@ -233,10 +233,7 @@ func main() { srv.setupHTTP(http.DefaultServeMux) go func() { - // The gosec linter complains that timeouts cannot be set here. That's fine, - // because this is test-only code. - ////nolint:gosec - err := http.ListenAndServe(*listenAPI, http.DefaultServeMux) + err := http.ListenAndServe(*listenAPI, http.DefaultServeMux) //nolint: gosec // No request timeout is fine for test-only code. if err != nil { log.Fatalln("Couldn't start HTTP server", err) } diff --git a/third-party/github.com/letsencrypt/boulder/test/ocsp/checkocsp/checkocsp.go b/third-party/github.com/letsencrypt/boulder/test/ocsp/checkocsp/checkocsp.go index 52a52f9b4..4a42659ff 100644 --- a/third-party/github.com/letsencrypt/boulder/test/ocsp/checkocsp/checkocsp.go +++ b/third-party/github.com/letsencrypt/boulder/test/ocsp/checkocsp/checkocsp.go @@ -48,7 +48,6 @@ stale. } serialNumber := big.NewInt(0).SetBytes(bytes) _, err = helper.ReqSerial(serialNumber, config) - } else { _, err = helper.ReqFile(a, config) } diff --git a/third-party/github.com/letsencrypt/boulder/test/ocsp/helper/helper.go b/third-party/github.com/letsencrypt/boulder/test/ocsp/helper/helper.go index a223f5fa6..469c8cec1 100644 --- a/third-party/github.com/letsencrypt/boulder/test/ocsp/helper/helper.go +++ b/third-party/github.com/letsencrypt/boulder/test/ocsp/helper/helper.go @@ -36,8 +36,11 @@ var ( // Config contains fields which control various behaviors of the // checker's behavior. type Config struct { - method string - urlOverride string + method string + // This URL will always be used in place of the URL in a certificate. + urlOverride string + // This URL will be used if no urlOverride is present and no OCSP URL is in the certificate. + urlFallback string hostOverride string tooSoon int ignoreExpiredCerts bool @@ -52,6 +55,7 @@ type Config struct { var DefaultConfig = Config{ method: "GET", urlOverride: "", + urlFallback: "", hostOverride: "", tooSoon: 76, ignoreExpiredCerts: false, @@ -115,6 +119,12 @@ func (template Config) WithExpectReason(reason int) Config { return ret } +func (template Config) WithURLFallback(url string) Config { + ret := template + ret.urlFallback = url + return ret +} + // WithOutput returns a new Config with the given output, // and all other fields the same as the receiver. func (template Config) WithOutput(w io.Writer) Config { @@ -268,7 +278,7 @@ func Req(cert *x509.Certificate, config Config) (*ocsp.Response, error) { return nil, fmt.Errorf("creating OCSP request: %s", err) } - ocspURL, err := getOCSPURL(cert, config.urlOverride) + ocspURL, err := getOCSPURL(cert, config.urlOverride, config.urlFallback) if err != nil { return nil, err } @@ -341,12 +351,14 @@ func sendHTTPRequest( return client.Do(httpRequest) } -func getOCSPURL(cert *x509.Certificate, urlOverride string) (*url.URL, error) { +func getOCSPURL(cert *x509.Certificate, urlOverride, urlFallback string) (*url.URL, error) { var ocspServer string if urlOverride != "" { ocspServer = urlOverride } else if len(cert.OCSPServer) > 0 { ocspServer = cert.OCSPServer[0] + } else if len(urlFallback) > 0 { + ocspServer = urlFallback } else { return nil, fmt.Errorf("no ocsp servers in cert") } diff --git a/third-party/github.com/letsencrypt/boulder/test/ocsp/ocsp_forever/main.go b/third-party/github.com/letsencrypt/boulder/test/ocsp/ocsp_forever/main.go index 25d3a5873..ddf5ed599 100644 --- a/third-party/github.com/letsencrypt/boulder/test/ocsp/ocsp_forever/main.go +++ b/third-party/github.com/letsencrypt/boulder/test/ocsp/ocsp_forever/main.go @@ -9,9 +9,10 @@ import ( "path/filepath" "time" - "github.com/letsencrypt/boulder/test/ocsp/helper" prom "github.com/prometheus/client_golang/prometheus" promhttp "github.com/prometheus/client_golang/prometheus/promhttp" + + "github.com/letsencrypt/boulder/test/ocsp/helper" ) var listenAddress = flag.String("listen", ":8080", "Port to listen on") @@ -86,10 +87,7 @@ func main() { } http.Handle("/metrics", promhttp.Handler()) go func() { - // The gosec linter complains that timeouts cannot be set here. That's fine, - // because this is test-only code. - ////nolint:gosec - err := http.ListenAndServe(*listenAddress, nil) + err := http.ListenAndServe(*listenAddress, nil) //nolint: gosec // No request timeout is fine for test-only code. if err != nil && err != http.ErrServerClosed { log.Fatal(err) } diff --git a/third-party/github.com/letsencrypt/boulder/test/pardot-test-srv/main.go b/third-party/github.com/letsencrypt/boulder/test/pardot-test-srv/main.go new file mode 100644 index 000000000..e247ff345 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/test/pardot-test-srv/main.go @@ -0,0 +1,218 @@ +package main + +import ( + "crypto/rand" + "encoding/json" + "flag" + "fmt" + "io" + "log" + "net/http" + "os" + "slices" + "sync" + "time" + + "github.com/letsencrypt/boulder/cmd" +) + +var contactsCap = 20 + +type config struct { + // OAuthAddr is the address (e.g. IP:port) on which the OAuth server will + // listen. + OAuthAddr string + + // PardotAddr is the address (e.g. IP:port) on which the Pardot server will + // listen. + PardotAddr string + + // ExpectedClientID is the client ID that the server expects to receive in + // requests to the /services/oauth2/token endpoint. + ExpectedClientID string `validate:"required"` + + // ExpectedClientSecret is the client secret that the server expects to + // receive in requests to the /services/oauth2/token endpoint. + ExpectedClientSecret string `validate:"required"` +} + +type contacts struct { + sync.Mutex + created []string +} + +type testServer struct { + expectedClientID string + expectedClientSecret string + token string + contacts contacts +} + +func (ts *testServer) getTokenHandler(w http.ResponseWriter, r *http.Request) { + err := r.ParseForm() + if err != nil { + http.Error(w, "Invalid request", http.StatusBadRequest) + return + } + + clientID := r.FormValue("client_id") + clientSecret := r.FormValue("client_secret") + + if clientID != ts.expectedClientID || clientSecret != ts.expectedClientSecret { + http.Error(w, "Invalid credentials", http.StatusUnauthorized) + return + } + + response := map[string]interface{}{ + "access_token": ts.token, + "token_type": "Bearer", + "expires_in": 3600, + } + + w.Header().Set("Content-Type", "application/json") + err = json.NewEncoder(w).Encode(response) + if err != nil { + log.Printf("Failed to encode token response: %v", err) + http.Error(w, "Failed to encode token response", http.StatusInternalServerError) + } +} + +func (ts *testServer) checkToken(w http.ResponseWriter, r *http.Request) { + token := r.Header.Get("Authorization") + if token != "Bearer "+ts.token { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } +} + +func (ts *testServer) createContactsHandler(w http.ResponseWriter, r *http.Request) { + ts.checkToken(w, r) + + businessUnitId := r.Header.Get("Pardot-Business-Unit-Id") + if businessUnitId == "" { + http.Error(w, "Missing 'Pardot-Business-Unit-Id' header", http.StatusBadRequest) + return + } + + body, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "Failed to read request body", http.StatusInternalServerError) + return + } + + type contactData struct { + Email string `json:"email"` + } + + var contact contactData + err = json.Unmarshal(body, &contact) + if err != nil { + http.Error(w, "Failed to parse request body", http.StatusBadRequest) + return + } + + if contact.Email == "" { + http.Error(w, "Missing 'email' field in request body", http.StatusBadRequest) + return + } + + ts.contacts.Lock() + if len(ts.contacts.created) >= contactsCap { + // Copying the slice in memory is inefficient, but this is a test server + // with a small number of contacts, so it's fine. + ts.contacts.created = ts.contacts.created[1:] + } + ts.contacts.created = append(ts.contacts.created, contact.Email) + ts.contacts.Unlock() + + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"status": "success"}`)) +} + +func (ts *testServer) queryContactsHandler(w http.ResponseWriter, r *http.Request) { + ts.checkToken(w, r) + + ts.contacts.Lock() + respContacts := slices.Clone(ts.contacts.created) + ts.contacts.Unlock() + + w.Header().Set("Content-Type", "application/json") + err := json.NewEncoder(w).Encode(map[string]interface{}{"contacts": respContacts}) + if err != nil { + log.Printf("Failed to encode contacts query response: %v", err) + http.Error(w, "Failed to encode contacts query response", http.StatusInternalServerError) + } +} + +func main() { + oauthAddr := flag.String("oauth-addr", "", "OAuth server listen address override") + pardotAddr := flag.String("pardot-addr", "", "Pardot server listen address override") + configFile := flag.String("config", "", "Path to configuration file") + 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 *oauthAddr != "" { + c.OAuthAddr = *oauthAddr + } + if *pardotAddr != "" { + c.PardotAddr = *pardotAddr + } + + tokenBytes := make([]byte, 32) + _, err = rand.Read(tokenBytes) + if err != nil { + log.Fatalf("Failed to generate token: %v", err) + } + + ts := &testServer{ + expectedClientID: c.ExpectedClientID, + expectedClientSecret: c.ExpectedClientSecret, + token: fmt.Sprintf("%x", tokenBytes), + contacts: contacts{created: make([]string, 0, contactsCap)}, + } + + // OAuth Server + oauthMux := http.NewServeMux() + oauthMux.HandleFunc("/services/oauth2/token", ts.getTokenHandler) + oauthServer := &http.Server{ + Addr: c.OAuthAddr, + Handler: oauthMux, + ReadTimeout: 30 * time.Second, + } + + log.Printf("pardot-test-srv OAuth server listening at %s", c.OAuthAddr) + go func() { + err := oauthServer.ListenAndServe() + if err != nil { + log.Fatalf("Failed to start OAuth server: %s", err) + } + }() + + // Pardot API Server + pardotMux := http.NewServeMux() + pardotMux.HandleFunc("/api/v5/objects/prospects", ts.createContactsHandler) + pardotMux.HandleFunc("/contacts", ts.queryContactsHandler) + + pardotServer := &http.Server{ + Addr: c.PardotAddr, + Handler: pardotMux, + ReadTimeout: 30 * time.Second, + } + log.Printf("pardot-test-srv Pardot API server listening at %s", c.PardotAddr) + go func() { + err := pardotServer.ListenAndServe() + if err != nil { + log.Fatalf("Failed to start Pardot API server: %s", err) + } + }() + + cmd.WaitForSignal() +} diff --git a/third-party/github.com/letsencrypt/boulder/test/redis-ratelimits.config b/third-party/github.com/letsencrypt/boulder/test/redis-ratelimits.config index 667ae9e34..a4d1eaf02 100644 --- a/third-party/github.com/letsencrypt/boulder/test/redis-ratelimits.config +++ b/third-party/github.com/letsencrypt/boulder/test/redis-ratelimits.config @@ -9,7 +9,6 @@ rename-command BGREWRITEAOF "" rename-command BGSAVE "" rename-command CONFIG "" rename-command DEBUG "" -rename-command FLUSHALL "" rename-command FLUSHDB "" rename-command KEYS "" rename-command PEXPIRE "" diff --git a/third-party/github.com/letsencrypt/boulder/test/s3-test-srv/main.go b/third-party/github.com/letsencrypt/boulder/test/s3-test-srv/main.go index 963b21f32..70336192e 100644 --- a/third-party/github.com/letsencrypt/boulder/test/s3-test-srv/main.go +++ b/third-party/github.com/letsencrypt/boulder/test/s3-test-srv/main.go @@ -27,21 +27,21 @@ func (srv *s3TestSrv) handleS3(w http.ResponseWriter, r *http.Request) { } else if r.Method == "GET" { srv.handleDownload(w, r) } else { - w.WriteHeader(405) + w.WriteHeader(http.StatusMethodNotAllowed) } } func (srv *s3TestSrv) handleUpload(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) if err != nil { - w.WriteHeader(500) + w.WriteHeader(http.StatusInternalServerError) w.Write([]byte("failed to read request body")) return } crl, err := x509.ParseRevocationList(body) if err != nil { - w.WriteHeader(500) + w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(fmt.Sprintf("failed to parse body: %s", err))) return } @@ -53,7 +53,7 @@ func (srv *s3TestSrv) handleUpload(w http.ResponseWriter, r *http.Request) { srv.allSerials[core.SerialToString(rc.SerialNumber)] = revocation.Reason(rc.ReasonCode) } - w.WriteHeader(200) + w.WriteHeader(http.StatusOK) w.Write([]byte("{}")) } @@ -62,22 +62,22 @@ func (srv *s3TestSrv) handleDownload(w http.ResponseWriter, r *http.Request) { defer srv.RUnlock() body, ok := srv.allShards[r.URL.Path] if !ok { - w.WriteHeader(404) + w.WriteHeader(http.StatusNotFound) return } - w.WriteHeader(200) + w.WriteHeader(http.StatusOK) w.Write(body) } func (srv *s3TestSrv) handleQuery(w http.ResponseWriter, r *http.Request) { if r.Method != "GET" { - w.WriteHeader(405) + w.WriteHeader(http.StatusMethodNotAllowed) return } serial := r.URL.Query().Get("serial") if serial == "" { - w.WriteHeader(400) + w.WriteHeader(http.StatusBadRequest) return } @@ -85,14 +85,28 @@ func (srv *s3TestSrv) handleQuery(w http.ResponseWriter, r *http.Request) { defer srv.RUnlock() reason, ok := srv.allSerials[serial] if !ok { - w.WriteHeader(404) + w.WriteHeader(http.StatusNotFound) return } - w.WriteHeader(200) + w.WriteHeader(http.StatusOK) w.Write([]byte(fmt.Sprintf("%d", reason))) } +func (srv *s3TestSrv) handleReset(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + srv.Lock() + defer srv.Unlock() + srv.allSerials = make(map[string]revocation.Reason) + srv.allShards = make(map[string][]byte) + + w.WriteHeader(http.StatusOK) +} + func main() { listenAddr := flag.String("listen", "0.0.0.0:4501", "Address to listen on") flag.Parse() @@ -104,6 +118,7 @@ func main() { http.HandleFunc("/", srv.handleS3) http.HandleFunc("/query", srv.handleQuery) + http.HandleFunc("/reset", srv.handleReset) s := http.Server{ ReadTimeout: 30 * time.Second, diff --git a/third-party/github.com/letsencrypt/boulder/test/secrets/nonce_prefix_key b/third-party/github.com/letsencrypt/boulder/test/secrets/nonce_prefix_key index d65802423..fb9e4fcda 100644 --- a/third-party/github.com/letsencrypt/boulder/test/secrets/nonce_prefix_key +++ b/third-party/github.com/letsencrypt/boulder/test/secrets/nonce_prefix_key @@ -1 +1 @@ -3b8c758dd85e113ea340ce0b3a99f389d40a308548af94d1730a7692c1874f1f +b91cf7d66bb88a0c50893eff1ce61555d548d6cf614925082352714efe881e30 diff --git a/third-party/github.com/letsencrypt/boulder/test/secrets/salesforce_client_id b/third-party/github.com/letsencrypt/boulder/test/secrets/salesforce_client_id new file mode 100644 index 000000000..0020d21da --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/test/secrets/salesforce_client_id @@ -0,0 +1 @@ +test-client-id diff --git a/third-party/github.com/letsencrypt/boulder/test/secrets/salesforce_client_secret b/third-party/github.com/letsencrypt/boulder/test/secrets/salesforce_client_secret new file mode 100644 index 000000000..dec23d701 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/test/secrets/salesforce_client_secret @@ -0,0 +1 @@ +you-shall-not-pass diff --git a/third-party/github.com/letsencrypt/boulder/test/secrets/sfe_unpause_key b/third-party/github.com/letsencrypt/boulder/test/secrets/sfe_unpause_key new file mode 100644 index 000000000..0e4fa9049 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/test/secrets/sfe_unpause_key @@ -0,0 +1 @@ +b18bd0dcf7113ef660e25457dbfc2162b1b3b17c0113abdc759af4752d8a90b5 diff --git a/third-party/github.com/letsencrypt/boulder/test/startservers.py b/third-party/github.com/letsencrypt/boulder/test/startservers.py index 4098375a5..4f4b508ba 100644 --- a/third-party/github.com/letsencrypt/boulder/test/startservers.py +++ b/third-party/github.com/letsencrypt/boulder/test/startservers.py @@ -16,21 +16,17 @@ Service = collections.namedtuple('Service', ('name', 'debug_port', 'grpc_port', # Keep these ports in sync with consul/config.hcl SERVICES = ( - Service('boulder-remoteva-a', - 8011, 9397, 'rva.boulder', - ('./bin/boulder', 'boulder-va', '--config', os.path.join(config_dir, 'va-remote-a.json'), '--addr', ':9397', '--debug-addr', ':8011'), - None), - Service('boulder-remoteva-b', - 8012, 9498, 'rva.boulder', - ('./bin/boulder', 'boulder-va', '--config', os.path.join(config_dir, 'va-remote-b.json'), '--addr', ':9498', '--debug-addr', ':8012'), - None), Service('remoteva-a', - 8211, 9897, 'rva.boulder', - ('./bin/boulder', 'remoteva', '--config', os.path.join(config_dir, 'remoteva-a.json'), '--addr', ':9897', '--debug-addr', ':8211'), + 8011, 9397, 'rva.boulder', + ('./bin/boulder', 'remoteva', '--config', os.path.join(config_dir, 'remoteva-a.json'), '--addr', ':9397', '--debug-addr', ':8011'), None), Service('remoteva-b', - 8212, 9998, 'rva.boulder', - ('./bin/boulder', 'remoteva', '--config', os.path.join(config_dir, 'remoteva-b.json'), '--addr', ':9998', '--debug-addr', ':8212'), + 8012, 9498, 'rva.boulder', + ('./bin/boulder', 'remoteva', '--config', os.path.join(config_dir, 'remoteva-b.json'), '--addr', ':9498', '--debug-addr', ':8012'), + None), + Service('remoteva-c', + 8023, 9499, 'rva.boulder', + ('./bin/boulder', 'remoteva', '--config', os.path.join(config_dir, 'remoteva-c.json'), '--addr', ':9499', '--debug-addr', ':8023'), None), Service('boulder-sa-1', 8003, 9395, 'sa.boulder', @@ -65,19 +61,19 @@ SERVICES = ( Service('boulder-va-1', 8004, 9392, 'va.boulder', ('./bin/boulder', 'boulder-va', '--config', os.path.join(config_dir, 'va.json'), '--addr', ':9392', '--debug-addr', ':8004'), - ('boulder-remoteva-a', 'boulder-remoteva-b', 'remoteva-a', 'remoteva-b')), + ('remoteva-a', 'remoteva-b')), Service('boulder-va-2', 8104, 9492, 'va.boulder', ('./bin/boulder', 'boulder-va', '--config', os.path.join(config_dir, 'va.json'), '--addr', ':9492', '--debug-addr', ':8104'), - ('boulder-remoteva-a', 'boulder-remoteva-b', 'remoteva-a', 'remoteva-b')), + ('remoteva-a', 'remoteva-b')), Service('boulder-ca-1', 8001, 9393, 'ca.boulder', ('./bin/boulder', 'boulder-ca', '--config', os.path.join(config_dir, 'ca.json'), '--addr', ':9393', '--debug-addr', ':8001'), - ('boulder-sa-1', 'boulder-sa-2')), + ('boulder-sa-1', 'boulder-sa-2', 'boulder-ra-sct-provider-1', 'boulder-ra-sct-provider-2')), Service('boulder-ca-2', 8101, 9493, 'ca.boulder', ('./bin/boulder', 'boulder-ca', '--config', os.path.join(config_dir, 'ca.json'), '--addr', ':9493', '--debug-addr', ':8101'), - ('boulder-sa-1', 'boulder-sa-2')), + ('boulder-sa-1', 'boulder-sa-2', 'boulder-ra-sct-provider-1', 'boulder-ra-sct-provider-2')), Service('akamai-test-srv', 6789, None, None, ('./bin/akamai-test-srv', '--listen', 'localhost:6789', '--secret', 'its-a-secret'), @@ -88,16 +84,12 @@ SERVICES = ( ('akamai-test-srv',)), Service('s3-test-srv', 4501, None, None, - ('./bin/s3-test-srv', '--listen', 'localhost:4501'), + ('./bin/s3-test-srv', '--listen', ':4501'), None), Service('crl-storer', 9667, None, None, ('./bin/boulder', 'crl-storer', '--config', os.path.join(config_dir, 'crl-storer.json'), '--addr', ':9309', '--debug-addr', ':9667'), ('s3-test-srv',)), - Service('crl-updater', - 8021, None, None, - ('./bin/boulder', 'crl-updater', '--config', os.path.join(config_dir, 'crl-updater.json'), '--debug-addr', ':8021'), - ('boulder-ca-1', 'boulder-ca-2', 'boulder-sa-1', 'boulder-sa-2', 'crl-storer')), Service('boulder-ra-1', 8002, 9394, 'ra.boulder', ('./bin/boulder', 'boulder-ra', '--config', os.path.join(config_dir, 'ra.json'), '--addr', ':9394', '--debug-addr', ':8002'), @@ -106,6 +98,21 @@ SERVICES = ( 8102, 9494, 'ra.boulder', ('./bin/boulder', 'boulder-ra', '--config', os.path.join(config_dir, 'ra.json'), '--addr', ':9494', '--debug-addr', ':8102'), ('boulder-sa-1', 'boulder-sa-2', 'boulder-ca-1', 'boulder-ca-2', 'boulder-va-1', 'boulder-va-2', 'akamai-purger', 'boulder-publisher-1', 'boulder-publisher-2')), + # We run a separate instance of the RA for use as the SCTProvider service called by the CA. + # This solves a small problem of startup order: if a client (the CA in this case) starts + # up before its backends, gRPC will try to connect immediately (due to health checks), + # get a connection refused, and enter a backoff state. That backoff state can cause + # subsequent requests to fail. This issue only exists for the CA-RA pair because they + # have a circular relationship - the RA calls CA.IssueCertificate, and the CA calls + # SCTProvider.GetSCTs (offered by the RA). + Service('boulder-ra-sct-provider-1', + 8118, 9594, 'ra.boulder', + ('./bin/boulder', 'boulder-ra', '--config', os.path.join(config_dir, 'ra.json'), '--addr', ':9594', '--debug-addr', ':8118'), + ('boulder-publisher-1', 'boulder-publisher-2')), + Service('boulder-ra-sct-provider-2', + 8119, 9694, 'ra.boulder', + ('./bin/boulder', 'boulder-ra', '--config', os.path.join(config_dir, 'ra.json'), '--addr', ':9694', '--debug-addr', ':8119'), + ('boulder-publisher-1', 'boulder-publisher-2')), Service('bad-key-revoker', 8020, None, None, ('./bin/boulder', 'bad-key-revoker', '--config', os.path.join(config_dir, 'bad-key-revoker.json'), '--debug-addr', ':8020'), @@ -129,10 +136,24 @@ SERVICES = ( 8112, None, None, ('./bin/boulder', 'nonce-service', '--config', os.path.join(config_dir, 'nonce-b.json'), '--addr', '10.77.77.77:9401', '--debug-addr', ':8112',), None), + Service('pardot-test-srv', + # Uses port 9601 to mock Salesforce OAuth2 token API and 9602 to mock + # the Pardot API. + 9601, None, None, + ('./bin/pardot-test-srv', '--config', os.path.join(config_dir, 'pardot-test-srv.json'),), + None), + Service('email-exporter', + 8114, None, None, + ('./bin/boulder', 'email-exporter', '--config', os.path.join(config_dir, 'email-exporter.json'), '--addr', ':9603', '--debug-addr', ':8114'), + ('pardot-test-srv',)), Service('boulder-wfe2', 4001, None, None, ('./bin/boulder', 'boulder-wfe2', '--config', os.path.join(config_dir, 'wfe2.json'), '--addr', ':4001', '--tls-addr', ':4431', '--debug-addr', ':8013'), - ('boulder-ra-1', 'boulder-ra-2', 'boulder-sa-1', 'boulder-sa-2', 'nonce-service-taro-1', 'nonce-service-taro-2', 'nonce-service-zinc-1')), + ('boulder-ra-1', 'boulder-ra-2', 'boulder-sa-1', 'boulder-sa-2', 'nonce-service-taro-1', 'nonce-service-taro-2', 'nonce-service-zinc-1', 'email-exporter')), + Service('sfe', + 4003, None, None, + ('./bin/boulder', 'sfe', '--config', os.path.join(config_dir, 'sfe.json'), '--addr', ':4003', '--debug-addr', ':8015'), + ('boulder-ra-1', 'boulder-ra-2', 'boulder-sa-1', 'boulder-sa-2',)), Service('log-validator', 8016, None, None, ('./bin/boulder', 'log-validator', '--config', os.path.join(config_dir, 'log-validator.json'), '--debug-addr', ':8016'), @@ -205,7 +226,7 @@ def start(fakeclock): print("Error querying DNS. Is consul running? `docker compose ps bconsul`. %s" % (e)) return False - # Start the pebble-challtestsrv first so it can be used to resolve DNS for + # Start the chall-test-srv first so it can be used to resolve DNS for # gRPC. startChallSrv() @@ -233,7 +254,7 @@ def start(fakeclock): def check(): """Return true if all started processes are still alive. - Log about anything that died. The pebble-challtestsrv is not considered when + Log about anything that died. The chall-test-srv is not considered when checking processes. """ global processes @@ -253,7 +274,7 @@ def check(): def startChallSrv(): """ - Start the pebble-challtestsrv and wait for it to become available. See also + Start the chall-test-srv and wait for it to become available. See also stopChallSrv. """ global challSrvProcess @@ -266,7 +287,7 @@ def startChallSrv(): # which is used is controlled by mock DNS data added by the relevant # integration tests. challSrvProcess = run([ - 'pebble-challtestsrv', + './bin/chall-test-srv', '--defaultIPv4', os.environ.get("FAKE_DNS"), '-defaultIPv6', '', '--dns01', ':8053,:8054', @@ -274,17 +295,17 @@ def startChallSrv(): '--doh-cert', 'test/certs/ipki/10.77.77.77/cert.pem', '--doh-cert-key', 'test/certs/ipki/10.77.77.77/key.pem', '--management', ':8055', - '--http01', '10.77.77.77:80', - '-https01', '10.77.77.77:443', - '--tlsalpn01', '10.88.88.88:443'], + '--http01', '64.112.117.122:80', + '-https01', '64.112.117.122:443', + '--tlsalpn01', '64.112.117.134:443'], None) - # Wait for the pebble-challtestsrv management port. + # Wait for the chall-test-srv management port. if not waitport(8055, ' '.join(challSrvProcess.args)): return False def stopChallSrv(): """ - Stop the running pebble-challtestsrv (if any) and wait for it to terminate. + Stop the running chall-test-srv (if any) and wait for it to terminate. See also startChallSrv. """ global challSrvProcess diff --git a/third-party/github.com/letsencrypt/boulder/test/v2_integration.py b/third-party/github.com/letsencrypt/boulder/test/v2_integration.py index 2889b3fcd..39eebb641 100644 --- a/third-party/github.com/letsencrypt/boulder/test/v2_integration.py +++ b/third-party/github.com/letsencrypt/boulder/test/v2_integration.py @@ -10,8 +10,6 @@ import os import json import re -import OpenSSL - from cryptography import x509 from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.asymmetric import rsa @@ -159,7 +157,7 @@ def test_http_challenge_broken_redirect(): redirect) # Expect the specialized error message - expectedError = "10.77.77.77: Fetching {0}: Invalid host in redirect target \"{1}.well-known\". Check webserver config for missing '/' in redirect target.".format(redirect, d) + expectedError = "64.112.117.122: Fetching {0}: Invalid host in redirect target \"{1}.well-known\". Check webserver config for missing '/' in redirect target.".format(redirect, d) # NOTE(@cpu): Can't use chisel2.expect_problem here because it doesn't let # us interrogate the detail message easily. @@ -180,7 +178,7 @@ def test_failed_validation_limit(): """ Fail a challenge repeatedly for the same domain, with the same account. Once we reach the rate limit we should get a rateLimitedError. Note that this - depends on the specific threshold configured in rate-limit-policies.yml. + depends on the specific threshold configured. This also incidentally tests a fix for https://github.com/letsencrypt/boulder/issues/4329. We expect to get @@ -365,7 +363,7 @@ def test_http_challenge_https_redirect(): # Also add an A record for the domain pointing to the interface that the # HTTPS HTTP-01 challtestsrv is bound. - challSrv.add_a_record(d, ["10.77.77.77"]) + challSrv.add_a_record(d, ["64.112.117.122"]) try: chisel2.auth_and_issue([d], client=client, chall_type="http-01") @@ -447,10 +445,10 @@ def test_http_challenge_timeout(): to a slow HTTP server appropriately. """ # Start a simple python HTTP server on port 80 in its own thread. - # NOTE(@cpu): The pebble-challtestsrv binds 10.77.77.77:80 for HTTP-01 - # challenges so we must use the 10.88.88.88 address for the throw away + # NOTE(@cpu): The chall-test-srv binds 64.112.117.122:80 for HTTP-01 + # challenges so we must use the 64.112.117.134 address for the throw away # server for this test and add a mock DNS entry that directs the VA to it. - httpd = SlowHTTPServer(("10.88.88.88", 80), SlowHTTPRequestHandler) + httpd = SlowHTTPServer(("64.112.117.134", 80), SlowHTTPRequestHandler) thread = threading.Thread(target = httpd.serve_forever) thread.daemon = False thread.start() @@ -460,7 +458,7 @@ def test_http_challenge_timeout(): # Add A record for the domains to ensure the VA's requests are directed # to the interface that we bound the HTTPServer to. - challSrv.add_a_record(hostname, ["10.88.88.88"]) + challSrv.add_a_record(hostname, ["64.112.117.134"]) start = datetime.datetime.utcnow() end = 0 @@ -492,7 +490,7 @@ def test_tls_alpn_challenge(): # to the interface that the challtestsrv has bound for TLS-ALPN-01 challenge # responses for host in domains: - challSrv.add_a_record(host, ["10.88.88.88"]) + challSrv.add_a_record(host, ["64.112.117.134"]) chisel2.auth_and_issue(domains, chall_type="tls-alpn-01") for host in domains: @@ -649,215 +647,6 @@ def test_order_reuse_failed_authz(): finally: cleanup() -def test_order_finalize_early(): - """ - Test that finalizing an order before its fully authorized results in the - order having an error set and the status being invalid. - """ - # Create a client - client = chisel2.make_client(None) - - # Create a random domain and a csr - domains = [ random_domain() ] - csr_pem = chisel2.make_csr(domains) - - # Create an order for the domain - order = client.new_order(csr_pem) - - deadline = datetime.datetime.now() + datetime.timedelta(seconds=5) - - # Finalizing an order early should generate an orderNotReady error. - chisel2.expect_problem("urn:ietf:params:acme:error:orderNotReady", - lambda: client.finalize_order(order, deadline)) - -def test_revoke_by_account_unspecified(): - client = chisel2.make_client() - cert_file = temppath('test_revoke_by_account_0.pem') - order = chisel2.auth_and_issue([random_domain()], client=client, cert_output=cert_file.name) - cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, order.fullchain_pem) - - reset_akamai_purges() - client.revoke(josepy.ComparableX509(cert), 0) - - verify_ocsp(cert_file.name, "test/certs/webpki/int-rsa-*.cert.pem", "http://localhost:4002", "revoked") - verify_akamai_purge() - -def test_revoke_by_account_with_reason(): - client = chisel2.make_client(None) - cert_file = temppath('test_revoke_by_account_1.pem') - order = chisel2.auth_and_issue([random_domain()], client=client, cert_output=cert_file.name) - cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, order.fullchain_pem) - - reset_akamai_purges() - - # Requesting revocation for keyCompromise should work, but not block the - # key. - client.revoke(josepy.ComparableX509(cert), 1) - verify_ocsp(cert_file.name, "test/certs/webpki/int-rsa-*.cert.pem", "http://localhost:4002", "revoked", "keyCompromise") - - verify_akamai_purge() - -def test_revoke_by_authz(): - domains = [random_domain()] - cert_file = temppath('test_revoke_by_authz.pem') - order = chisel2.auth_and_issue(domains, cert_output=cert_file.name) - cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, order.fullchain_pem) - - # create a new client and re-authz - client = chisel2.make_client(None) - chisel2.auth_and_issue(domains, client=client) - - reset_akamai_purges() - - # Even though we requested reason 1 ("keyCompromise"), the result should be - # 5 ("cessationOfOperation") due to the authorization method. - client.revoke(josepy.ComparableX509(cert), 1) - verify_ocsp(cert_file.name, "test/certs/webpki/int-rsa-*.cert.pem", "http://localhost:4002", "revoked", "cessationOfOperation") - - verify_akamai_purge() - -def test_revoke_by_privkey(): - domains = [random_domain()] - - # We have to make our own CSR so that we can hold on to the private key - # for revocation later. - key = rsa.generate_private_key(65537, 2048, default_backend()) - key_pem = key.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=serialization.NoEncryption() - ) - csr_pem = acme_crypto_util.make_csr(key_pem, domains, False) - - # We have to do our own issuance because we made our own CSR. - issue_client = chisel2.make_client(None) - order = issue_client.new_order(csr_pem) - cleanup = chisel2.do_http_challenges(issue_client, order.authorizations) - try: - order = issue_client.poll_and_finalize(order) - finally: - cleanup() - cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, order.fullchain_pem) - - cert_file = tempfile.NamedTemporaryFile( - dir=tempdir, suffix='.test_revoke_by_privkey.pem', - mode='w+', delete=False) - cert_file.write(OpenSSL.crypto.dump_certificate( - OpenSSL.crypto.FILETYPE_PEM, cert).decode()) - cert_file.close() - - # Create a new client with the cert key as the account key. We don't - # register a server-side account with this client, as we don't need one. - revoke_client = chisel2.uninitialized_client(key=josepy.JWKRSA(key=key)) - - reset_akamai_purges() - - # Even though we requested reason 0 ("unspecified"), the result should be - # 1 ("keyCompromise") due to the authorization method. - revoke_client.revoke(josepy.ComparableX509(cert), 0) - verify_ocsp(cert_file.name, "test/certs/webpki/int-rsa-*.cert.pem", "http://localhost:4002", "revoked", "keyCompromise") - - verify_akamai_purge() - -def test_double_revocation(): - domains = [random_domain()] - - # We have to make our own CSR so that we can hold on to the private key - # for revocation later. - key = rsa.generate_private_key(65537, 2048, default_backend()) - key_pem = key.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=serialization.NoEncryption() - ) - csr_pem = acme_crypto_util.make_csr(key_pem, domains, False) - - # We have to do our own issuance because we made our own CSR. - sub_client = chisel2.make_client(None) - order = sub_client.new_order(csr_pem) - cleanup = chisel2.do_http_challenges(sub_client, order.authorizations) - try: - order = sub_client.poll_and_finalize(order) - finally: - cleanup() - cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, order.fullchain_pem) - - cert_file = tempfile.NamedTemporaryFile( - dir=tempdir, suffix='.test_double_revoke.pem', - mode='w+', delete=False) - cert_file.write(OpenSSL.crypto.dump_certificate( - OpenSSL.crypto.FILETYPE_PEM, cert).decode()) - cert_file.close() - - # Create a new client with the cert key as the account key. We don't - # register a server-side account with this client, as we don't need one. - cert_client = chisel2.uninitialized_client(key=josepy.JWKRSA(key=key)) - - reset_akamai_purges() - - # First revoke for any reason. - sub_client.revoke(josepy.ComparableX509(cert), 0) - verify_ocsp(cert_file.name, "test/certs/webpki/int-rsa-*.cert.pem", "http://localhost:4002", "revoked") - verify_akamai_purge() - - # Re-revocation for anything other than keyCompromise should fail. - try: - sub_client.revoke(josepy.ComparableX509(cert), 3) - except messages.Error: - pass - else: - raise(Exception("Re-revoked for a bad reason")) - - # Re-revocation for keyCompromise should work, as long as it is done - # via the cert key to demonstrate said compromise. - reset_akamai_purges() - cert_client.revoke(josepy.ComparableX509(cert), 1) - verify_ocsp(cert_file.name, "test/certs/webpki/int-rsa-*.cert.pem", "http://localhost:4002", "revoked", "keyCompromise") - verify_akamai_purge() - - # A subsequent attempt should fail, because the cert is already revoked - # for keyCompromise. - try: - cert_client.revoke(josepy.ComparableX509(cert), 1) - except messages.Error: - pass - else: - raise(Exception("Re-revoked already keyCompromise'd cert")) - - # The same is true even when using the cert key. - try: - cert_client.revoke(josepy.ComparableX509(cert), 1) - except messages.Error: - pass - else: - raise(Exception("Re-revoked already keyCompromise'd cert")) - -def test_sct_embedding(): - order = chisel2.auth_and_issue([random_domain()]) - print(order.fullchain_pem.encode()) - cert = parse_cert(order) - - # make sure there is no poison extension - try: - cert.extensions.get_extension_for_oid(x509.ObjectIdentifier("1.3.6.1.4.1.11129.2.4.3")) - raise(Exception("certificate contains CT poison extension")) - except x509.ExtensionNotFound: - # do nothing - pass - - # make sure there is a SCT list extension - try: - sctList = cert.extensions.get_extension_for_oid(x509.ObjectIdentifier("1.3.6.1.4.1.11129.2.4.2")) - except x509.ExtensionNotFound: - raise(Exception("certificate doesn't contain SCT list extension")) - if len(sctList.value) != 2: - raise(Exception("SCT list contains wrong number of SCTs")) - for sct in sctList.value: - if sct.version != x509.certificate_transparency.Version.v1: - raise(Exception("SCT contains wrong version")) - if sct.entry_type != x509.certificate_transparency.LogEntryType.PRE_CERTIFICATE: - raise(Exception("SCT contains wrong entry type")) - def test_only_return_existing_reg(): client = chisel2.uninitialized_client() email = "test@not-example.com" @@ -970,20 +759,20 @@ def multiva_setup(client, guestlist): # Add an A record for the domains to ensure the VA's requests are directed # to the interface that we bound the HTTPServer to. - challSrv.add_a_record(hostname, ["10.88.88.88"]) + challSrv.add_a_record(hostname, ["64.112.117.134"]) # Add an A record for the redirect target that sends it to the real chall # test srv for a valid HTTP-01 response. - redirHostname = "pebble-challtestsrv.example.com" - challSrv.add_a_record(redirHostname, ["10.77.77.77"]) + redirHostname = "chall-test-srv.example.com" + challSrv.add_a_record(redirHostname, ["64.112.117.122"]) # Start a simple python HTTP server on port 80 in its own thread. - # NOTE(@cpu): The pebble-challtestsrv binds 10.77.77.77:80 for HTTP-01 - # challenges so we must use the 10.88.88.88 address for the throw away + # NOTE(@cpu): The chall-test-srv binds 64.112.117.122:80 for HTTP-01 + # challenges so we must use the 64.112.117.134 address for the throw away # server for this test and add a mock DNS entry that directs the VA to it. redirect = "http://{0}/.well-known/acme-challenge/{1}".format( redirHostname, token) - httpd = HTTPServer(("10.88.88.88", 80), BouncerHTTPRequestHandler(redirect, guestlist)) + httpd = HTTPServer(("64.112.117.134", 80), BouncerHTTPRequestHandler(redirect, guestlist)) thread = threading.Thread(target = httpd.serve_forever) thread.daemon = False thread.start() @@ -1005,7 +794,8 @@ def test_http_multiva_threshold_pass(): # Configure a guestlist that will pass the multiVA threshold test by # allowing the primary VA at some, but not all, remotes. - guestlist = {"boulder": 1, "boulder-remoteva-a": 1, "boulder-remoteva-b": 1, "remoteva-a": 1} + # In particular, remoteva-c is missing. + guestlist = {"boulder": 1, "remoteva-a": 1, "remoteva-b": 1} hostname, cleanup = multiva_setup(client, guestlist) @@ -1021,7 +811,7 @@ def test_http_multiva_primary_fail_remote_pass(): # Configure a guestlist that will fail the primary VA check but allow all of # the remote VAs. - guestlist = {"boulder": 0, "boulder-remoteva-a": 1, "boulder-remoteva-b": 1, "remoteva-a": 1, "remoteva-b": 1} + guestlist = {"boulder": 0, "remoteva-a": 1, "remoteva-b": 1} hostname, cleanup = multiva_setup(client, guestlist) @@ -1124,7 +914,7 @@ def test_http2_http01_challenge(): # Add an A record for the test server to ensure the VA's requests are directed # to the interface that we bind the FakeH2ServerHandler to. - challSrv.add_a_record(hostname, ["10.88.88.88"]) + challSrv.add_a_record(hostname, ["64.112.117.134"]) # Allow socket address reuse on the base TCPServer class. Failing to do this # causes subsequent integration tests to fail with "Address in use" errors even @@ -1134,11 +924,11 @@ def test_http2_http01_challenge(): # the problem. socketserver.TCPServer.allow_reuse_address = True # Create, start, and wait for a fake HTTP/2 server. - server = socketserver.TCPServer(("10.88.88.88", 80), FakeH2ServerHandler) + server = socketserver.TCPServer(("64.112.117.134", 80), FakeH2ServerHandler) thread = threading.Thread(target = server.serve_forever) thread.daemon = False thread.start() - wait_for_tcp_server("10.88.88.88", 80) + wait_for_tcp_server("64.112.117.134", 80) # Issuing an HTTP-01 challenge for this hostname should produce a connection # problem with an error specific to the HTTP/2 misconfiguration. @@ -1226,11 +1016,6 @@ def test_auth_deactivation_v2(): if resp.body.status is not messages.STATUS_DEACTIVATED: raise(Exception("unexpected authorization status")) -def test_ocsp(): - cert_file = temppath('test_ocsp.pem') - chisel2.auth_and_issue([random_domain()], cert_output=cert_file.name) - verify_ocsp(cert_file.name, "test/certs/webpki/int-rsa-*.cert.pem", "http://localhost:4002", "good") - def test_ct_submission(): hostname = random_domain() @@ -1340,128 +1125,6 @@ def test_ocsp_exp_unauth(): else: raise(Exception("timed out waiting for unauthorized OCSP response for expired certificate. Last error: {}".format(last_error))) -def test_blocked_key_account(): - # Only config-next has a blocked keys file configured. - if not CONFIG_NEXT: - return - - with open("test/hierarchy/int-r4.key.pem", "rb") as key_file: - key = serialization.load_pem_private_key(key_file.read(), password=None, backend=default_backend()) - - # Create a client with the JWK set to a blocked private key - jwk = josepy.JWKRSA(key=key) - client = chisel2.uninitialized_client(jwk) - email = "test@not-example.com" - - # Try to create an account - testPass = False - try: - client.new_account(messages.NewRegistration.from_data(email=email, - terms_of_service_agreed=True)) - except acme_errors.Error as e: - if e.typ != "urn:ietf:params:acme:error:badPublicKey": - raise(Exception("problem did not have correct error type, had {0}".format(e.typ))) - if e.detail != "public key is forbidden": - raise(Exception("problem did not have correct error detail, had {0}".format(e.detail))) - testPass = True - - if testPass is False: - raise(Exception("expected account creation to fail with Error when using blocked key")) - -def test_blocked_key_cert(): - # Only config-next has a blocked keys file configured. - if not CONFIG_NEXT: - return - - with open("test/hierarchy/int-r4.key.pem", "r") as f: - pemBytes = f.read() - - domains = [random_domain(), random_domain()] - csr = acme_crypto_util.make_csr(pemBytes, domains, False) - - client = chisel2.make_client(None) - order = client.new_order(csr) - authzs = order.authorizations - - testPass = False - cleanup = chisel2.do_http_challenges(client, authzs) - try: - order = client.poll_and_finalize(order) - except acme_errors.Error as e: - if e.typ != "urn:ietf:params:acme:error:badCSR": - raise(Exception("problem did not have correct error type, had {0}".format(e.typ))) - if e.detail != "Error finalizing order :: invalid public key in CSR: public key is forbidden": - raise(Exception("problem did not have correct error detail, had {0}".format(e.detail))) - testPass = True - - if testPass is False: - raise(Exception("expected cert creation to fail with Error when using blocked key")) - -def test_expiration_mailer(): - email_addr = "integration.%x@letsencrypt.org" % random.randrange(2**16) - order = chisel2.auth_and_issue([random_domain()], email=email_addr) - cert = parse_cert(order) - # Check that the expiration mailer sends a reminder - expiry = cert.not_valid_after - no_reminder = expiry + datetime.timedelta(days=-31) - first_reminder = expiry + datetime.timedelta(days=-13) - last_reminder = expiry + datetime.timedelta(days=-2) - - requests.post("http://localhost:9381/clear", data='') - for time in (no_reminder, first_reminder, last_reminder): - print(get_future_output( - ["./bin/boulder", "expiration-mailer", - "--config", "%s/expiration-mailer.json" % config_dir, - "--debug-addr", ":8008"], - time)) - resp = requests.get("http://localhost:9381/count?to=%s" % email_addr) - mailcount = int(resp.text) - if mailcount != 2: - raise(Exception("\nExpiry mailer failed: expected 2 emails, got %d" % mailcount)) - -caa_recheck_setup_data = {} -@register_twenty_days_ago -def caa_recheck_setup(): - client = chisel2.make_client() - # Issue a certificate with the clock set back, and save the authzs to check - # later that they are valid (200). They should however require rechecking for - # CAA purposes. - numNames = 10 - # Generate numNames subdomains of a random domain - base_domain = random_domain() - domains = [ "{0}.{1}".format(str(n),base_domain) for n in range(numNames) ] - order = chisel2.auth_and_issue(domains, client=client) - - global caa_recheck_setup_data - caa_recheck_setup_data = { - 'client': client, - 'authzs': order.authorizations, - } - -def test_recheck_caa(): - """Request issuance for a domain where we have a old cached authz from when CAA - was good. We'll set a new CAA record forbidding issuance; the CAA should - recheck CAA and reject the request. - """ - if 'authzs' not in caa_recheck_setup_data: - raise(Exception("CAA authzs not prepared for test_caa")) - domains = [] - for a in caa_recheck_setup_data['authzs']: - response = caa_recheck_setup_data['client']._post(a.uri, None) - if response.status_code != 200: - raise(Exception("Unexpected response for CAA authz: ", - response.status_code)) - domain = a.body.identifier.value - domains.append(domain) - - # Set a forbidding CAA record on just one domain - challSrv.add_caa_issue(domains[3], ";") - - # Request issuance for the previously-issued domain name, which should - # now be denied due to CAA. - chisel2.expect_problem("urn:ietf:params:acme:error:caa", - lambda: chisel2.auth_and_issue(domains, client=caa_recheck_setup_data['client'])) - def test_caa_good(): domain = random_domain() challSrv.add_caa_issue(domain, "happy-hacker-ca.invalid") @@ -1503,38 +1166,6 @@ def test_caa_extensions(): chisel2.expect_problem("urn:ietf:params:acme:error:caa", lambda: chisel2.auth_and_issue(["accounturi.good-caa-reserved.com"])) chisel2.auth_and_issue(["accounturi.good-caa-reserved.com"], client=client) -def test_new_account(): - """ - Test creating new accounts with no email, empty email, one email, and a - tuple of multiple emails. - """ - for contact in (None, (), ("mailto:single@chisel.com",), ("mailto:one@chisel.com", "mailto:two@chisel.com")): - # We don't use `chisel2.make_client` or `messages.NewRegistration.from_data` - # here because they do too much client-side processing to make the - # contact addresses look "nice". - client = chisel2.uninitialized_client() - result = client.new_account(messages.NewRegistration(contact=contact, terms_of_service_agreed=True)) - actual = result.body.contact - if contact is not None and contact != actual: - raise(Exception("New Account failed: expected contact %s, got %s" % (contact, actual))) - -def test_account_update(): - """ - Create a new ACME client/account with one contact email. Then update the - account to a different contact emails. - """ - for contact in (None, (), ("mailto:single@chisel.com",), ("mailto:one@chisel.com", "mailto:two@chisel.com")): - # We don't use `chisel2.update_email` or `messages.NewRegistration.from_data` - # here because they do too much client-side processing to make the - # contact addresses look "nice". - print() - client = chisel2.make_client() - update = client.net.account.update(body=client.net.account.body.update(contact=contact)) - result = client.update_registration(update) - actual = result.body.contact - if contact is not None and contact != actual: - raise(Exception("New Account failed: expected contact %s, got %s" % (contact, actual))) - def test_renewal_exemption(): """ Under a single domain, issue two certificates for different subdomains of @@ -1559,15 +1190,6 @@ def test_renewal_exemption(): chisel2.expect_problem("urn:ietf:params:acme:error:rateLimited", lambda: chisel2.auth_and_issue(["mail." + base_domain])) -# TODO(#5545) -# - Phase 2: Once the new rate limits are authoritative in config-next, ensure -# that this test only runs in config. -# - Phase 3: Once the new rate limits are authoritative in config, remove this -# test entirely. -def test_certificates_per_name(): - chisel2.expect_problem("urn:ietf:params:acme:error:rateLimited", - lambda: chisel2.auth_and_issue([random_domain() + ".lim.it"])) - def test_oversized_csr(): # Number of names is chosen to be one greater than the configured RA/CA maxNames numNames = 101 @@ -1582,48 +1204,6 @@ def test_oversized_csr(): def parse_cert(order): return x509.load_pem_x509_certificate(order.fullchain_pem.encode(), default_backend()) -def test_admin_revoker_cert(): - cert_file = temppath('test_admin_revoker_cert.pem') - order = chisel2.auth_and_issue([random_domain()], cert_output=cert_file.name) - parsed_cert = parse_cert(order) - - # Revoke certificate by serial - reset_akamai_purges() - run(["./bin/admin", - "-config", "%s/admin.json" % config_dir, - "-dry-run=false", - "revoke-cert", - "-serial", '%x' % parsed_cert.serial_number, - "-reason", "keyCompromise"]) - - # Wait for OCSP response to indicate revocation took place - verify_ocsp(cert_file.name, "test/certs/webpki/int-rsa-*.cert.pem", "http://localhost:4002", "revoked", "keyCompromise") - verify_akamai_purge() - -def test_admin_revoker_batched(): - serialFile = tempfile.NamedTemporaryFile( - dir=tempdir, suffix='.test_admin_revoker_batched.serials.hex', - mode='w+', delete=False) - cert_files = [ - temppath('test_admin_revoker_batched.%d.pem' % x) for x in range(3) - ] - - for cert_file in cert_files: - order = chisel2.auth_and_issue([random_domain()], cert_output=cert_file.name) - serialFile.write("%x\n" % parse_cert(order).serial_number) - serialFile.close() - - run(["./bin/admin", - "-config", "%s/admin.json" % config_dir, - "-dry-run=false", - "revoke-cert", - "-serials-file", serialFile.name, - "-reason", "unspecified", - "-parallelism", "2"]) - - for cert_file in cert_files: - verify_ocsp(cert_file.name, "test/certs/webpki/int-rsa-*.cert.pem", "http://localhost:4002", "revoked", "unspecified") - def test_sct_embedding(): order = chisel2.auth_and_issue([random_domain()]) cert = parse_cert(order) @@ -1671,7 +1251,7 @@ def test_auth_deactivation(): def get_ocsp_response_and_reason(cert_file, issuer_glob, url): """Returns the ocsp response output and revocation reason.""" output = verify_ocsp(cert_file, issuer_glob, url, None) - m = re.search('Reason: (\w+)', output) + m = re.search(r'Reason: (\w+)', output) reason = m.group(1) if m is not None else "" return output, reason @@ -1688,10 +1268,9 @@ def ocsp_resigning_setup(): cert_file = temppath('ocsp_resigning_setup.pem') order = chisel2.auth_and_issue([random_domain()], client=client, cert_output=cert_file.name) - cert = OpenSSL.crypto.load_certificate( - OpenSSL.crypto.FILETYPE_PEM, order.fullchain_pem) + cert = x509.load_pem_x509_certificate(order.fullchain_pem.encode(), default_backend()) # Revoke for reason 5: cessationOfOperation - client.revoke(josepy.ComparableX509(cert), 5) + client.revoke(cert, 5) ocsp_response, reason = get_ocsp_response_and_reason( cert_file.name, "test/certs/webpki/int-rsa-*.cert.pem", "http://localhost:4002") diff --git a/third-party/github.com/letsencrypt/boulder/tn.sh b/third-party/github.com/letsencrypt/boulder/tn.sh index a3cda0822..665e3a59b 100644 --- a/third-party/github.com/letsencrypt/boulder/tn.sh +++ b/third-party/github.com/letsencrypt/boulder/tn.sh @@ -10,9 +10,6 @@ if type realpath >/dev/null 2>&1 ; then fi # Generate the test keys and certs necessary for the integration tests. -docker compose run bsetup +docker compose run --rm bsetup -# Use a predictable name for the container so we can grab the logs later -# for use when testing logs analysis tools. -docker rm boulder_tests || true -exec docker compose -f docker-compose.yml -f docker-compose.next.yml run boulder ./test.sh "$@" +exec docker compose -f docker-compose.yml -f docker-compose.next.yml run --rm --name boulder_tests boulder ./test.sh "$@" diff --git a/third-party/github.com/letsencrypt/boulder/tools/make-assets.sh b/third-party/github.com/letsencrypt/boulder/tools/make-assets.sh index 812f56a3d..f57c5e477 100644 --- a/third-party/github.com/letsencrypt/boulder/tools/make-assets.sh +++ b/third-party/github.com/letsencrypt/boulder/tools/make-assets.sh @@ -1,22 +1,19 @@ #!/usr/bin/env bash # -# This script expects to run on Ubuntu. It installs the dependencies necessary -# to build Boulder and produce a Debian Package. The actual build and packaging -# is handled by a call to Make. +# Build Boulder and produce a .deb and a .tar.gz. +# +# This script expects to run on Ubuntu, as configured on GitHub Actions runners +# (with curl, make, and git installed). # - # -e Stops execution in the instance of a command or pipeline error. # -u Treat unset variables as an error and exit immediately. set -eu -# -# Setup Dependencies -# - -sudo apt-get install -y --no-install-recommends \ - ruby \ - ruby-dev \ - gcc +ARCH="$(uname -m)" +if [ "${ARCH}" != "x86_64" && "${ARCH}" != "amd64" ]; then + echo "Expected ARCH=x86_64 or amd64, got ${ARCH}" + exit 1 +fi # Download and unpack our production go version. Ensure that $GO_VERSION is # already set in the environment (e.g. by the github actions release workflow). @@ -24,19 +21,45 @@ $(dirname -- "${0}")/fetch-and-verify-go.sh "${GO_VERSION}" sudo tar -C /usr/local -xzf go.tar.gz export PATH=/usr/local/go/bin:$PATH -# Install fpm, this is used in our Makefile to package Boulder as a deb or rpm. -sudo gem install --no-document -v 1.14.0 fpm - # # Build # - -# Set $ARCHIVEDIR to our current directory. If left unset our Makefile will set -# it to /tmp. -export ARCHIVEDIR="${PWD}" +LDFLAGS="-X \"github.com/letsencrypt/boulder/core.BuildID=${COMMIT_ID}\" -X \"github.com/letsencrypt/boulder/core.BuildTime=$(date -u)\" -X \"github.com/letsencrypt/boulder/core.BuildHost=$(whoami)@$(hostname)\"" +GOBIN=$PWD/bin/ GO111MODULE=on go install -mod=vendor -buildvcs=false -ldflags "${LDFLAGS}" ./... # Set $VERSION to be a simulacrum of what is set in other build environments. -export VERSION="${GO_VERSION}.$(date +%s)" +VERSION="${GO_VERSION}.$(date +%s)" -# Build Boulder and produce an RPM, a .deb, and a tar.gz file in $PWD. -make rpm deb tar +BOULDER="${PWD}" +BUILD="$(mktemp -d)" +TARGET="${BUILD}/opt/boulder" + +mkdir -p "${TARGET}/bin" +for NAME in admin boulder ceremony ct-test-srv pardot-test-srv chall-test-srv ; do + cp -a "bin/${NAME}" "${TARGET}/bin/" +done + +mkdir -p "${TARGET}/test" +cp -a "${BOULDER}/test/config/" "${TARGET}/test/config/" + +mkdir -p "${TARGET}/sa" +cp -a "${BOULDER}/sa/db/" "${TARGET}/sa/db/" + +cp -a "${BOULDER}/data/" "${TARGET}/data/" + +mkdir "${BUILD}/DEBIAN" +cat > "${BUILD}/DEBIAN/control" <<-EOF +Package: boulder +Version: 1:${VERSION} +License: Mozilla Public License v2.0 +Vendor: ISRG +Architecture: amd64 +Maintainer: Community +Section: default +Priority: extra +Homepage: https://github.com/letsencrypt/boulder +Description: Boulder is an ACME-compatible X.509 Certificate Authority +EOF + +dpkg-deb -Zgzip -b "${BUILD}" "./boulder-${VERSION}-${COMMIT_ID}.x86_64.deb" +tar -C "${TARGET}" -cpzf "./boulder-${VERSION}-${COMMIT_ID}.amd64.tar.gz" . diff --git a/third-party/github.com/letsencrypt/boulder/tools/nameid/README.md b/third-party/github.com/letsencrypt/boulder/tools/nameid/README.md new file mode 100644 index 000000000..99a40508c --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/tools/nameid/README.md @@ -0,0 +1,24 @@ +# Overview + +The `nameid` tool displays a statistically-unique small ID which can be computed +from both CA and end-entity certs to link them together into a validation chain. +It is computed as a truncated hash over the issuer Subject Name bytes. It should +only be used on issuer certificates e.g. [when the CA boolean is +asserted](https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.9) which in the +`//crypto/x509` `Certificate` struct is `IsCA: true`. + +For implementation details, please see the `//issuance` package +[here](https://github.com/letsencrypt/boulder/blob/30c6e592f7f6825c2782b6a7d5da566979445674/issuance/issuer.go#L79-L83). + +# Usage + +``` +# Display help +go run ./tools/nameid/nameid.go -h + +# Output the certificate path and nameid, one per line +go run ./tools/nameid/nameid.go /path/to/cert1.pem /path/to/cert2.pem ... + +# Output just the nameid, one per line +go run ./tools/nameid/nameid.go -s /path/to/cert1.pem /path/to/cert2.pem ... +``` diff --git a/third-party/github.com/letsencrypt/boulder/tools/nameid/nameid.go b/third-party/github.com/letsencrypt/boulder/tools/nameid/nameid.go new file mode 100644 index 000000000..d15b20b80 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/tools/nameid/nameid.go @@ -0,0 +1,37 @@ +package main + +import ( + "flag" + "fmt" + "os" + + "github.com/letsencrypt/boulder/issuance" +) + +func usage() { + fmt.Printf("Usage: %s [OPTIONS] [ISSUER CERTIFICATE(S)]\n", os.Args[0]) +} + +func main() { + var shorthandFlag = flag.Bool("s", false, "Display only the nameid for each given issuer certificate") + flag.Parse() + + if len(os.Args) <= 1 { + usage() + os.Exit(1) + } + + for _, certFile := range flag.Args() { + issuer, err := issuance.LoadCertificate(certFile) + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + os.Exit(1) + } + + if *shorthandFlag { + fmt.Println(issuer.NameID()) + } else { + fmt.Printf("%s: %d\n", certFile, issuer.NameID()) + } + } +} diff --git a/third-party/github.com/letsencrypt/boulder/tools/release/branch/main.go b/third-party/github.com/letsencrypt/boulder/tools/release/branch/main.go new file mode 100644 index 000000000..c93edb0b6 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/tools/release/branch/main.go @@ -0,0 +1,156 @@ +/* +Branch Release creates a new Boulder hotfix release branch and pushes it to +GitHub. It ensures that the release branch has a standard name, and starts at +a previously-tagged mainline release. + +The expectation is that this branch will then be the target of one or more PRs +copying (cherry-picking) commits from main to the release branch, and then a +hotfix release will be tagged on the branch using the related Tag Release tool. + +Usage: + + go run github.com/letsencrypt/boulder/tools/release/tag@main [-push] tagname + +The provided tagname must be a pre-existing release tag which is reachable from +the "main" branch. + +If the -push flag is not provided, it will simply print the details of the new +branch and then exit. If it is provided, it will initiate a push to the remote. + +In all cases, it assumes that the upstream remote is named "origin". +*/ +package main + +import ( + "errors" + "flag" + "fmt" + "os" + "os/exec" + "strings" + "time" +) + +type cmdError struct { + error + output string +} + +func (e cmdError) Unwrap() error { + return e.error +} + +func git(args ...string) (string, error) { + cmd := exec.Command("git", args...) + fmt.Println("Running:", cmd.String()) + out, err := cmd.CombinedOutput() + if err != nil { + return string(out), cmdError{ + error: fmt.Errorf("running %q: %w", cmd.String(), err), + output: string(out), + } + } + return string(out), nil +} + +func show(output string) { + for line := range strings.SplitSeq(strings.TrimSpace(output), "\n") { + fmt.Println(" ", line) + } +} + +func main() { + err := branch(os.Args[1:]) + if err != nil { + var cmdErr cmdError + if errors.As(err, &cmdErr) { + show(cmdErr.output) + } + fmt.Println(err.Error()) + os.Exit(1) + } +} + +func branch(args []string) error { + fs := flag.NewFlagSet("branch", flag.ContinueOnError) + var push bool + fs.BoolVar(&push, "push", false, "If set, push the resulting hotfix release branch to GitHub.") + err := fs.Parse(args) + if err != nil { + return fmt.Errorf("invalid flags: %w", err) + } + + if len(fs.Args()) != 1 { + return fmt.Errorf("must supply exactly one argument, got %d: %#v", len(fs.Args()), fs.Args()) + } + + tag := fs.Arg(0) + + // Confirm the reasonableness of the given tag name by inspecting each of its + // components. + parts := strings.SplitN(tag, ".", 3) + if len(parts) != 3 { + return fmt.Errorf("failed to parse patch version from release tag %q", tag) + } + + major := parts[0] + if major != "v0" { + return fmt.Errorf("expected major portion of release tag to be 'v0', got %q", major) + } + + minor := parts[1] + t, err := time.Parse("20060102", minor) + if err != nil { + return fmt.Errorf("expected minor portion of release tag to be a ") + } + if t.Year() < 2015 { + return fmt.Errorf("minor portion of release tag appears to be an unrealistic date: %q", t.String()) + } + + patch := parts[2] + if patch != "0" { + return fmt.Errorf("expected patch portion of release tag to be '0', got %q", patch) + } + + // Fetch all of the latest refs from origin, so that we can get the most + // complete view of this tag and its relationship to main. + _, err = git("fetch", "origin") + if err != nil { + return err + } + + _, err = git("merge-base", "--is-ancestor", tag, "origin/main") + if err != nil { + return fmt.Errorf("tag %q is not reachable from origin/main, may not have been created properly: %w", tag, err) + } + + // Create the branch. We could skip this and instead push the tag directly + // to the desired ref name on the remote, but that wouldn't give the operator + // a chance to inspect it locally. + branch := fmt.Sprintf("release-branch-%s.%s", major, minor) + _, err = git("branch", branch, tag) + if err != nil { + return err + } + + // Show the HEAD of the new branch, not including its diff. + out, err := git("show", "-s", branch) + if err != nil { + return err + } + show(out) + + refspec := fmt.Sprintf("%s:%s", branch, branch) + + if push { + _, err = git("push", "origin", refspec) + if err != nil { + return err + } + } else { + fmt.Println() + fmt.Println("Please inspect the branch above, then run:") + fmt.Printf(" git push origin %s\n", refspec) + } + return nil +} diff --git a/third-party/github.com/letsencrypt/boulder/tools/release/tag/main.go b/third-party/github.com/letsencrypt/boulder/tools/release/tag/main.go new file mode 100644 index 000000000..1ebb92cb2 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/tools/release/tag/main.go @@ -0,0 +1,147 @@ +/* +Tag Release creates a new Boulder release tag and pushes it to GitHub. It +ensures that the release tag points to the correct commit, has standardized +formatting of both the tag itself and its message, and is GPG-signed. + +It always produces Semantic Versioning tags of the form v0.YYYYMMDD.N, where: + - the major version of 0 indicates that we are not committing to any + backwards-compatibility guarantees; + - the minor version of the current date provides a human-readable date for the + release, and ensures that minor versions will be monotonically increasing; + and + - the patch version is always 0 for mainline releases, and a monotonically + increasing number for hotfix releases. + +Usage: + + go run github.com/letsencrypt/boulder/tools/release/tag@main [-push] [branchname] + +If the "branchname" argument is not provided, it assumes "main". If it is +provided, it must be either "main" or a properly-formatted release branch name. + +If the -push flag is not provided, it will simply print the details of the new +tag and then exit. If it is provided, it will initiate a push to the remote. + +In all cases, it assumes that the upstream remote is named "origin". +*/ +package main + +import ( + "errors" + "flag" + "fmt" + "os" + "os/exec" + "strings" + "time" +) + +type cmdError struct { + error + output string +} + +func (e cmdError) Unwrap() error { + return e.error +} + +func git(args ...string) (string, error) { + cmd := exec.Command("git", args...) + fmt.Println("Running:", cmd.String()) + out, err := cmd.CombinedOutput() + if err != nil { + return string(out), cmdError{ + error: fmt.Errorf("running %q: %w", cmd.String(), err), + output: string(out), + } + } + return string(out), nil +} + +func show(output string) { + for line := range strings.SplitSeq(strings.TrimSpace(output), "\n") { + fmt.Println(" ", line) + } +} + +func main() { + err := tag(os.Args[1:]) + if err != nil { + var cmdErr cmdError + if errors.As(err, &cmdErr) { + show(cmdErr.output) + } + fmt.Println(err.Error()) + os.Exit(1) + } +} + +func tag(args []string) error { + fs := flag.NewFlagSet("tag", flag.ContinueOnError) + var push bool + fs.BoolVar(&push, "push", false, "If set, push the resulting release tag to GitHub.") + err := fs.Parse(args) + if err != nil { + return fmt.Errorf("invalid flags: %w", err) + } + + if len(fs.Args()) > 1 { + return fmt.Errorf("too many args: %#v", fs.Args()) + } + + branch := "main" + if len(fs.Args()) == 1 { + branch = fs.Arg(0) + } + + switch { + case branch == "main": + break + case strings.HasPrefix(branch, "release-branch-"): + return fmt.Errorf("sorry, tagging hotfix release branches is not yet supported") + default: + return fmt.Errorf("branch must be 'main' or 'release-branch-...', got %q", branch) + } + + // Fetch all of the latest commits on this ref from origin, so that we can + // ensure we're tagging the tip of the upstream branch. + _, err = git("fetch", "origin", branch) + if err != nil { + return err + } + + // We use semver's vMajor.Minor.Patch format, where the Major version is + // always 0 (no backwards compatibility guarantees), the Minor version is + // the date of the release, and the Patch number is zero for normal releases + // and only non-zero for hotfix releases. + minor := time.Now().Format("20060102") + version := fmt.Sprintf("v0.%s.0", minor) + message := fmt.Sprintf("Release %s", version) + + // Produce the tag, using -s to PGP sign it. This will fail if a tag with + // that name already exists. + _, err = git("tag", "-s", "-m", message, version, "origin/"+branch) + if err != nil { + return err + } + + // Show the result of the tagging operation, including the tag message and + // signature, and the commit hash and message, but not the diff. + out, err := git("show", "-s", version) + if err != nil { + return err + } + show(out) + + if push { + _, err = git("push", "origin", version) + if err != nil { + return err + } + } else { + fmt.Println() + fmt.Println("Please inspect the tag above, then run:") + fmt.Printf(" git push origin %s\n", version) + } + return nil +} diff --git a/third-party/github.com/letsencrypt/boulder/tools/verify-release-ancestry.sh b/third-party/github.com/letsencrypt/boulder/tools/verify-release-ancestry.sh new file mode 100644 index 000000000..b40830f3c --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/tools/verify-release-ancestry.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# +# Usage: verify-release-ancestry.sh +# +# Exits zero if the provided commit is either an ancestor of main or equal to a +# hotfix branch (release-branch-*). Exits 1 otherwise. +# +set -u + +if git merge-base --is-ancestor "$1" origin/main ; then + echo "'$1' is an ancestor of main" + exit 0 +elif git for-each-ref --points-at="$1" "refs/remotes/origin/release-branch-*" | grep -q "^$1.commit.refs/remotes/origin/release-branch-" ; then + echo "'$1' is equal to the tip of a hotfix branch (release-branch-*)" + exit 0 +else + echo + echo "Commit '$1' is neither an ancestor of main nor equal to a hotfix branch (release-branch-*)" + echo + exit 1 +fi diff --git a/third-party/github.com/letsencrypt/boulder/unpause/unpause.go b/third-party/github.com/letsencrypt/boulder/unpause/unpause.go new file mode 100644 index 000000000..72cde8a15 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/unpause/unpause.go @@ -0,0 +1,160 @@ +package unpause + +import ( + "errors" + "fmt" + "strconv" + "strings" + "time" + + "github.com/go-jose/go-jose/v4" + "github.com/go-jose/go-jose/v4/jwt" + "github.com/jmhodges/clock" + + "github.com/letsencrypt/boulder/cmd" +) + +const ( + // API + + // Changing this value will invalidate all existing JWTs. + APIVersion = "v1" + APIPrefix = "/sfe/" + APIVersion + GetForm = APIPrefix + "/unpause" + + // BatchSize is the maximum number of identifiers that the SA will unpause + // in a single batch. + BatchSize = 10000 + + // MaxBatches is the maximum number of batches that the SA will unpause in a + // single request. + MaxBatches = 5 + + // RequestLimit is the maximum number of identifiers that the SA will + // unpause in a single request. This is used by the SFE to infer whether + // there are more identifiers to unpause. + RequestLimit = BatchSize * MaxBatches + + // JWT + defaultIssuer = "WFE" + defaultAudience = "SFE Unpause" +) + +// JWTSigner is a type alias for jose.Signer. To create a JWTSigner instance, +// use the NewJWTSigner function provided in this package. +type JWTSigner = jose.Signer + +// NewJWTSigner loads the HMAC key from the provided configuration and returns a +// new JWT signer. +func NewJWTSigner(hmacKey cmd.HMACKeyConfig) (JWTSigner, error) { + key, err := hmacKey.Load() + if err != nil { + return nil, err + } + return jose.NewSigner(jose.SigningKey{Algorithm: jose.HS256, Key: key}, nil) +} + +// JWTClaims represents the claims of a JWT token issued by the WFE for +// redemption by the SFE. The following claims required for unpausing: +// - Subject: the account ID of the Subscriber +// - V: the API version this JWT was created for +// - I: a set of ACME identifier values. Identifier types are omitted +// since DNS and IP string representations do not overlap. +type JWTClaims struct { + jwt.Claims + + // V is the API version this JWT was created for. + V string `json:"version"` + + // I is set of comma separated ACME identifiers. + I string `json:"identifiers"` +} + +// GenerateJWT generates a serialized unpause JWT with the provided claims. +func GenerateJWT(signer JWTSigner, regID int64, idents []string, lifetime time.Duration, clk clock.Clock) (string, error) { + claims := JWTClaims{ + Claims: jwt.Claims{ + Issuer: defaultIssuer, + Subject: fmt.Sprintf("%d", regID), + Audience: jwt.Audience{defaultAudience}, + // IssuedAt is necessary for metrics. + IssuedAt: jwt.NewNumericDate(clk.Now()), + Expiry: jwt.NewNumericDate(clk.Now().Add(lifetime)), + }, + V: APIVersion, + I: strings.Join(idents, ","), + } + + serialized, err := jwt.Signed(signer).Claims(&claims).Serialize() + if err != nil { + return "", fmt.Errorf("serializing JWT: %s", err) + } + + return serialized, nil +} + +// ErrMalformedJWT is returned when the JWT is malformed. +var ErrMalformedJWT = errors.New("malformed JWT") + +// RedeemJWT deserializes an unpause JWT and returns the validated claims. The +// key is used to validate the signature of the JWT. The version is the expected +// API version of the JWT. This function validates that the JWT is: +// - well-formed, +// - valid for the current time (+/- 1 minute leeway), +// - issued by the WFE, +// - intended for the SFE, +// - contains an Account ID as the 'Subject', +// - subject can be parsed as a 64-bit integer, +// - contains a set of paused identifiers as 'Identifiers', and +// - contains the API the expected version as 'Version'. +// +// If the JWT is malformed or invalid in any way, ErrMalformedJWT is returned. +func RedeemJWT(token string, key []byte, version string, clk clock.Clock) (JWTClaims, error) { + parsedToken, err := jwt.ParseSigned(token, []jose.SignatureAlgorithm{jose.HS256}) + if err != nil { + return JWTClaims{}, errors.Join(ErrMalformedJWT, err) + } + + claims := JWTClaims{} + err = parsedToken.Claims(key, &claims) + if err != nil { + return JWTClaims{}, errors.Join(ErrMalformedJWT, err) + } + + err = claims.Validate(jwt.Expected{ + Issuer: defaultIssuer, + AnyAudience: jwt.Audience{defaultAudience}, + + // By default, the go-jose library validates the NotBefore and Expiry + // fields with a default leeway of 1 minute. + Time: clk.Now(), + }) + if err != nil { + return JWTClaims{}, fmt.Errorf("validating JWT: %w", err) + } + + if len(claims.Subject) == 0 { + return JWTClaims{}, errors.New("no account ID specified in the JWT") + } + account, err := strconv.ParseInt(claims.Subject, 10, 64) + if err != nil { + return JWTClaims{}, errors.New("invalid account ID specified in the JWT") + } + if account == 0 { + return JWTClaims{}, errors.New("no account ID specified in the JWT") + } + + if claims.V == "" { + return JWTClaims{}, errors.New("no API version specified in the JWT") + } + + if claims.V != version { + return JWTClaims{}, fmt.Errorf("unexpected API version in the JWT: %s", claims.V) + } + + if claims.I == "" { + return JWTClaims{}, errors.New("no identifiers specified in the JWT") + } + + return claims, nil +} diff --git a/third-party/github.com/letsencrypt/boulder/unpause/unpause_test.go b/third-party/github.com/letsencrypt/boulder/unpause/unpause_test.go new file mode 100644 index 000000000..eeffd5529 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/unpause/unpause_test.go @@ -0,0 +1,156 @@ +package unpause + +import ( + "testing" + "time" + + "github.com/go-jose/go-jose/v4/jwt" + "github.com/jmhodges/clock" + + "github.com/letsencrypt/boulder/cmd" + "github.com/letsencrypt/boulder/test" +) + +func TestUnpauseJWT(t *testing.T) { + fc := clock.NewFake() + + signer, err := NewJWTSigner(cmd.HMACKeyConfig{KeyFile: "../test/secrets/sfe_unpause_key"}) + test.AssertNotError(t, err, "unexpected error from NewJWTSigner()") + + config := cmd.HMACKeyConfig{KeyFile: "../test/secrets/sfe_unpause_key"} + hmacKey, err := config.Load() + test.AssertNotError(t, err, "unexpected error from Load()") + + type args struct { + key []byte + version string + account int64 + idents []string + lifetime time.Duration + clk clock.Clock + } + + tests := []struct { + name string + args args + want JWTClaims + wantGenerateJWTErr bool + wantRedeemJWTErr bool + }{ + { + name: "valid one identifier", + args: args{ + key: hmacKey, + version: APIVersion, + account: 1234567890, + idents: []string{"example.com"}, + lifetime: time.Hour, + clk: fc, + }, + want: JWTClaims{ + Claims: jwt.Claims{ + Issuer: defaultIssuer, + Subject: "1234567890", + Audience: jwt.Audience{defaultAudience}, + Expiry: jwt.NewNumericDate(fc.Now().Add(time.Hour)), + }, + V: APIVersion, + I: "example.com", + }, + wantGenerateJWTErr: false, + wantRedeemJWTErr: false, + }, + { + name: "valid multiple identifiers", + args: args{ + key: hmacKey, + version: APIVersion, + account: 1234567890, + idents: []string{"example.com", "example.org", "example.net"}, + lifetime: time.Hour, + clk: fc, + }, + want: JWTClaims{ + Claims: jwt.Claims{ + Issuer: defaultIssuer, + Subject: "1234567890", + Audience: jwt.Audience{defaultAudience}, + Expiry: jwt.NewNumericDate(fc.Now().Add(time.Hour)), + }, + V: APIVersion, + I: "example.com,example.org,example.net", + }, + wantGenerateJWTErr: false, + wantRedeemJWTErr: false, + }, + { + name: "invalid no account", + args: args{ + key: hmacKey, + version: APIVersion, + account: 0, + idents: []string{"example.com"}, + lifetime: time.Hour, + clk: fc, + }, + want: JWTClaims{}, + wantGenerateJWTErr: false, + wantRedeemJWTErr: true, + }, + { + // This test is only testing the "key too small" case for RedeemJWT + // because the "key too small" case for GenerateJWT is handled when + // the key is loaded to initialize a signer. + name: "invalid key too small", + args: args{ + key: []byte("key"), + version: APIVersion, + account: 1234567890, + idents: []string{"example.com"}, + lifetime: time.Hour, + clk: fc, + }, + want: JWTClaims{}, + wantGenerateJWTErr: false, + wantRedeemJWTErr: true, + }, + { + name: "invalid no identifiers", + args: args{ + key: hmacKey, + version: APIVersion, + account: 1234567890, + idents: nil, + lifetime: time.Hour, + clk: fc, + }, + want: JWTClaims{}, + wantGenerateJWTErr: false, + wantRedeemJWTErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + token, err := GenerateJWT(signer, tt.args.account, tt.args.idents, tt.args.lifetime, tt.args.clk) + if tt.wantGenerateJWTErr { + test.AssertError(t, err, "expected error from GenerateJWT()") + return + } + test.AssertNotError(t, err, "unexpected error from GenerateJWT()") + + got, err := RedeemJWT(token, tt.args.key, tt.args.version, tt.args.clk) + if tt.wantRedeemJWTErr { + test.AssertError(t, err, "expected error from RedeemJWT()") + return + } + test.AssertNotError(t, err, "unexpected error from RedeemJWT()") + test.AssertEquals(t, got.Issuer, tt.want.Issuer) + test.AssertEquals(t, got.Subject, tt.want.Subject) + test.AssertDeepEquals(t, got.Audience, tt.want.Audience) + test.Assert(t, got.Expiry.Time().Equal(tt.want.Expiry.Time()), "expected Expiry time to be equal") + test.AssertEquals(t, got.V, tt.want.V) + test.AssertEquals(t, got.I, tt.want.I) + }) + } +} diff --git a/third-party/github.com/letsencrypt/boulder/va/caa.go b/third-party/github.com/letsencrypt/boulder/va/caa.go index 8d9d67639..0ed15d269 100644 --- a/third-party/github.com/letsencrypt/boulder/va/caa.go +++ b/third-party/github.com/letsencrypt/boulder/va/caa.go @@ -2,8 +2,8 @@ package va import ( "context" + "errors" "fmt" - "math/rand" "net/url" "regexp" "strings" @@ -11,15 +11,12 @@ import ( "time" "github.com/miekg/dns" - "github.com/prometheus/client_golang/prometheus" + "google.golang.org/protobuf/proto" "github.com/letsencrypt/boulder/bdns" - "github.com/letsencrypt/boulder/canceled" "github.com/letsencrypt/boulder/core" corepb "github.com/letsencrypt/boulder/core/proto" berrors "github.com/letsencrypt/boulder/errors" - "github.com/letsencrypt/boulder/features" - bgrpc "github.com/letsencrypt/boulder/grpc" "github.com/letsencrypt/boulder/identifier" "github.com/letsencrypt/boulder/probs" vapb "github.com/letsencrypt/boulder/va/proto" @@ -30,250 +27,120 @@ type caaParams struct { validationMethod core.AcmeChallenge } -// IsCAAValid checks requested CAA records from a VA, and recursively any RVAs -// configured in the VA. It returns a response or an error. -func (va *ValidationAuthorityImpl) IsCAAValid(ctx context.Context, req *vapb.IsCAAValidRequest) (*vapb.IsCAAValidResponse, error) { - if core.IsAnyNilOrZero(req.Domain, req.ValidationMethod, req.AccountURIID) { +// DoCAA conducts a CAA check for the specified dnsName. When invoked on the +// primary Validation Authority (VA) and the local check succeeds, it also +// performs CAA checks using the configured remote VAs. Failed checks are +// indicated by a non-nil Problems in the returned ValidationResult. DoCAA +// returns error only for internal logic errors (and the client may receive +// errors from gRPC in the event of a communication problem). This method +// implements the CAA portion of Multi-Perspective Issuance Corroboration as +// defined in BRs Sections 3.2.2.9 and 5.4.1. +func (va *ValidationAuthorityImpl) DoCAA(ctx context.Context, req *vapb.IsCAAValidRequest) (*vapb.IsCAAValidResponse, error) { + if core.IsAnyNilOrZero(req.Identifier, req.ValidationMethod, req.AccountURIID) { return nil, berrors.InternalServerError("incomplete IsCAAValid request") } - logEvent := verificationRequestEvent{ - // TODO(#7061) Plumb req.Authz.Id as "ID:" through from the RA to - // correlate which authz triggered this request. - Requester: req.AccountURIID, - Hostname: req.Domain, - } - checkStartTime := va.clk.Now() - validationMethod := core.AcmeChallenge(req.ValidationMethod) - if !validationMethod.IsValid() { + ident := identifier.FromProto(req.Identifier) + if ident.Type != identifier.TypeDNS { + return nil, berrors.MalformedError("Identifier type for CAA check was not DNS") + } + + logEvent := validationLogEvent{ + AuthzID: req.AuthzID, + Requester: req.AccountURIID, + Identifier: ident, + } + + challType := core.AcmeChallenge(req.ValidationMethod) + if !challType.IsValid() { return nil, berrors.InternalServerError("unrecognized validation method %q", req.ValidationMethod) } - acmeID := identifier.ACMEIdentifier{ - Type: identifier.DNS, - Value: req.Domain, - } params := &caaParams{ accountURIID: req.AccountURIID, - validationMethod: validationMethod, + validationMethod: challType, } - var remoteCAAResults chan *remoteVAResult - if features.Get().EnforceMultiCAA { - if remoteVACount := len(va.remoteVAs); remoteVACount > 0 { - remoteCAAResults = make(chan *remoteVAResult, remoteVACount) - go va.performRemoteCAACheck(ctx, req, remoteCAAResults) - } - } - - checkResult := "success" - err := va.checkCAA(ctx, acmeID, params) - localCheckLatency := time.Since(checkStartTime) + // Initialize variables and a deferred function to handle check latency + // metrics, log check errors, and log an MPIC summary. Avoid using := to + // redeclare `prob`, `localLatency`, or `summary` below this point. var prob *probs.ProblemDetails - if err != nil { - prob = detailedError(err) - logEvent.Error = prob.Error() - logEvent.InternalError = err.Error() - prob.Detail = fmt.Sprintf("While processing CAA for %s: %s", req.Domain, prob.Detail) - checkResult = "failure" - } else if remoteCAAResults != nil { - if !features.Get().EnforceMultiCAA && features.Get().MultiCAAFullResults { - // If we're not going to enforce multi CAA but we are logging the - // differentials then collect and log the remote results in a separate go - // routine to avoid blocking the primary VA. - go func() { - _ = va.processRemoteCAAResults( - req.Domain, - req.AccountURIID, - string(validationMethod), - remoteCAAResults) - }() - } else if features.Get().EnforceMultiCAA { - remoteProb := va.processRemoteCAAResults( - req.Domain, - req.AccountURIID, - string(validationMethod), - remoteCAAResults) + var summary *mpicSummary + var internalErr error + var localLatency time.Duration + start := va.clk.Now() - // If the remote result was a non-nil problem then fail the CAA check - if remoteProb != nil { - prob = remoteProb - // We only set .Error here, not InternalError, because the remote VA doesn't send - // us the internal error. But that's okay, because it got logged at the remote VA. - logEvent.Error = remoteProb.Error() - checkResult = "failure" - va.log.Infof("CAA check failed due to remote failures: identifier=%v err=%s", - req.Domain, remoteProb) - va.metrics.remoteCAACheckFailures.Inc() + defer func() { + probType := "" + outcome := fail + if prob != nil { + // CAA check failed. + probType = string(prob.Type) + logEvent.Error = prob.String() + } else { + // CAA check passed. + outcome = pass + } + // Observe local check latency (primary|remote). + va.observeLatency(opCAA, va.perspective, string(challType), probType, outcome, localLatency) + if va.isPrimaryVA() { + // Observe total check latency (primary+remote). + va.observeLatency(opCAA, allPerspectives, string(challType), probType, outcome, va.clk.Since(start)) + logEvent.Summary = summary + } + // Log the total check latency. + logEvent.Latency = va.clk.Since(start).Round(time.Millisecond).Seconds() + + va.log.AuditObject("CAA check result", logEvent) + }() + + internalErr = va.checkCAA(ctx, ident, params) + + // Stop the clock for local check latency. + localLatency = va.clk.Since(start) + + if internalErr != nil { + logEvent.InternalError = internalErr.Error() + prob = detailedError(internalErr) + prob.Detail = fmt.Sprintf("While processing CAA for %s: %s", ident.Value, prob.Detail) + } + + if va.isPrimaryVA() { + op := func(ctx context.Context, remoteva RemoteVA, req proto.Message) (remoteResult, error) { + checkRequest, ok := req.(*vapb.IsCAAValidRequest) + if !ok { + return nil, fmt.Errorf("got type %T, want *vapb.IsCAAValidRequest", req) } + return remoteva.DoCAA(ctx, checkRequest) + } + var remoteProb *probs.ProblemDetails + summary, remoteProb = va.doRemoteOperation(ctx, op, req) + // If the remote result was a non-nil problem then fail the CAA check + if remoteProb != nil { + prob = remoteProb + va.log.Infof("CAA check failed due to remote failures: identifier=%v err=%s", + ident.Value, remoteProb) } } - checkLatency := time.Since(checkStartTime) - logEvent.ValidationLatency = checkLatency.Round(time.Millisecond).Seconds() - - va.metrics.localCAACheckTime.With(prometheus.Labels{ - "result": checkResult, - }).Observe(localCheckLatency.Seconds()) - va.metrics.caaCheckTime.With(prometheus.Labels{ - "result": checkResult, - }).Observe(checkLatency.Seconds()) - - va.log.AuditObject("CAA check result", logEvent) if prob != nil { // The ProblemDetails will be serialized through gRPC, which requires UTF-8. // It will also later be serialized in JSON, which defaults to UTF-8. Make // sure it is UTF-8 clean now. prob = filterProblemDetails(prob) - return &vapb.IsCAAValidResponse{Problem: &corepb.ProblemDetails{ - ProblemType: string(prob.Type), - Detail: replaceInvalidUTF8([]byte(prob.Detail)), - }}, nil + return &vapb.IsCAAValidResponse{ + Problem: &corepb.ProblemDetails{ + ProblemType: string(prob.Type), + Detail: replaceInvalidUTF8([]byte(prob.Detail)), + }, + Perspective: va.perspective, + Rir: va.rir, + }, nil } else { - return &vapb.IsCAAValidResponse{}, nil - } -} - -// processRemoteCAAResults evaluates a primary VA result, and a channel of -// remote VA problems to produce a single overall validation result based on -// configured feature flags. The overall result is calculated based on the VA's -// configured `maxRemoteFailures` value. -// -// If the `MultiCAAFullResults` feature is enabled then -// `processRemoteCAAResults` will expect to read a result from the -// `remoteResultsChan` channel for each VA and will not produce an overall -// result until all remote VAs have responded. In this case -// `logRemoteDifferentials` will also be called to describe the differential -// between the primary and all of the remote VAs. -// -// If the `MultiCAAFullResults` feature flag is not enabled then -// `processRemoteCAAResults` will potentially return before all remote VAs have -// had a chance to respond. This happens if the success or failure threshold is -// met. This doesn't allow for logging the differential between the primary and -// remote VAs but is more performant. -func (va *ValidationAuthorityImpl) processRemoteCAAResults( - domain string, - acctID int64, - challengeType string, - remoteResultsChan <-chan *remoteVAResult) *probs.ProblemDetails { - - state := "failure" - start := va.clk.Now() - - defer func() { - va.metrics.remoteCAACheckTime.With(prometheus.Labels{ - "result": state, - }).Observe(va.clk.Since(start).Seconds()) - }() - - required := len(va.remoteVAs) - va.maxRemoteFailures - good := 0 - bad := 0 - - var remoteResults []*remoteVAResult - var firstProb *probs.ProblemDetails - // Due to channel behavior this could block indefinitely and we rely on gRPC - // honoring the context deadline used in client calls to prevent that from - // happening. - for result := range remoteResultsChan { - // Add the result to the slice - remoteResults = append(remoteResults, result) - if result.Problem == nil { - good++ - } else { - bad++ - // Store the first non-nil problem to return later (if `MultiCAAFullResults` - // is enabled). - if firstProb == nil { - firstProb = result.Problem - } - } - - // If MultiCAAFullResults isn't enabled then return early whenever the - // success or failure threshold is met. - if !features.Get().MultiCAAFullResults { - if good >= required { - state = "success" - return nil - } else if bad > va.maxRemoteFailures { - modifiedProblem := *result.Problem - modifiedProblem.Detail = "During secondary CAA checking: " + firstProb.Detail - return &modifiedProblem - } - } - - // If we haven't returned early because of MultiCAAFullResults being - // enabled we need to break the loop once all of the VAs have returned a - // result. - if len(remoteResults) == len(va.remoteVAs) { - break - } - } - // If we are using `features.MultiCAAFullResults` then we haven't returned - // early and can now log the differential between what the primary VA saw and - // what all of the remote VAs saw. - va.logRemoteResults( - domain, - acctID, - challengeType, - remoteResults) - - // Based on the threshold of good/bad return nil or a problem. - if good >= required { - state = "success" - return nil - } else if bad > va.maxRemoteFailures { - modifiedProblem := *firstProb - modifiedProblem.Detail = "During secondary CAA checking: " + firstProb.Detail - va.metrics.prospectiveRemoteCAACheckFailures.Inc() - return &modifiedProblem - } - - // This condition should not occur - it indicates the good/bad counts didn't - // meet either the required threshold or the maxRemoteFailures threshold. - return probs.ServerInternal("Too few remote IsCAAValid RPC results") -} - -// performRemoteCAACheck calls `isCAAValid` for each of the configured remoteVAs -// in a random order. The provided `results` chan should have an equal size to -// the number of remote VAs. The CAA checks will be performed in separate -// go-routines. If the result `error` from a remote `isCAAValid` RPC is nil or a -// nil `ProblemDetails` instance it is written directly to the `results` chan. -// If the err is a cancelled error it is treated as a nil error. Otherwise the -// error/problem is written to the results channel as-is. -func (va *ValidationAuthorityImpl) performRemoteCAACheck( - ctx context.Context, - req *vapb.IsCAAValidRequest, - results chan<- *remoteVAResult) { - for _, i := range rand.Perm(len(va.remoteVAs)) { - remoteVA := va.remoteVAs[i] - go func(rva RemoteVA) { - result := &remoteVAResult{ - VAHostname: rva.Address, - } - res, err := rva.IsCAAValid(ctx, req) - if err != nil { - if canceled.Is(err) { - // Handle the cancellation error. - result.Problem = probs.ServerInternal("Remote VA IsCAAValid RPC cancelled") - } else { - // Handle validation error. - va.log.Errf("Remote VA %q.IsCAAValid failed: %s", rva.Address, err) - result.Problem = probs.ServerInternal("Remote VA IsCAAValid RPC failed") - } - } else if res.Problem != nil { - prob, err := bgrpc.PBToProblemDetails(res.Problem) - if err != nil { - va.log.Infof("Remote VA %q.IsCAAValid returned malformed problem: %s", rva.Address, err) - result.Problem = probs.ServerInternal( - fmt.Sprintf("Remote VA IsCAAValid RPC returned malformed result: %s", err)) - } else { - va.log.Infof("Remote VA %q.IsCAAValid returned problem: %s", rva.Address, prob) - result.Problem = prob - } - } - results <- result - }(remoteVA) + return &vapb.IsCAAValidResponse{ + Perspective: va.perspective, + Rir: va.rir, + }, nil } } @@ -281,19 +148,19 @@ func (va *ValidationAuthorityImpl) performRemoteCAACheck( // the CAA lookup & validation fail a problem is returned. func (va *ValidationAuthorityImpl) checkCAA( ctx context.Context, - identifier identifier.ACMEIdentifier, + ident identifier.ACMEIdentifier, params *caaParams) error { if core.IsAnyNilOrZero(params, params.validationMethod, params.accountURIID) { - return probs.ServerInternal("expected validationMethod or accountURIID not provided to checkCAA") + return errors.New("expected validationMethod or accountURIID not provided to checkCAA") } - foundAt, valid, response, err := va.checkCAARecords(ctx, identifier, params) + foundAt, valid, response, err := va.checkCAARecords(ctx, ident, params) if err != nil { return berrors.DNSError("%s", err) } va.log.AuditInfof("Checked CAA records for %s, [Present: %t, Account ID: %d, Challenge: %s, Valid for issuance: %t, Found at: %q] Response=%q", - identifier.Value, foundAt != "", params.accountURIID, params.validationMethod, valid, foundAt, response) + ident.Value, foundAt != "", params.accountURIID, params.validationMethod, valid, foundAt, response) if !valid { return berrors.CAAError("CAA record for %s prevents issuance", foundAt) } @@ -437,13 +304,13 @@ func (va *ValidationAuthorityImpl) getCAA(ctx context.Context, hostname string) // value (or nil). func (va *ValidationAuthorityImpl) checkCAARecords( ctx context.Context, - identifier identifier.ACMEIdentifier, + ident identifier.ACMEIdentifier, params *caaParams) (string, bool, string, error) { - hostname := strings.ToLower(identifier.Value) + hostname := strings.ToLower(ident.Value) // If this is a wildcard name, remove the prefix var wildcard bool if strings.HasPrefix(hostname, `*.`) { - hostname = strings.TrimPrefix(identifier.Value, `*.`) + hostname = strings.TrimPrefix(ident.Value, `*.`) wildcard = true } caaSet, err := va.getCAA(ctx, hostname) @@ -477,15 +344,6 @@ func (va *ValidationAuthorityImpl) validateCAA(caaSet *caaResult, wildcard bool, return false, caaSet.name } - if len(caaSet.issue) == 0 && !wildcard { - // Although CAA records exist, none of them pertain to issuance in this case. - // (e.g. there is only an issuewild directive, but we are checking for a - // non-wildcard identifier, or there is only an iodef or non-critical unknown - // directive.) - va.metrics.caaCounter.WithLabelValues("no relevant records").Inc() - return true, caaSet.name - } - // Per RFC 8659 Section 5.3: // - "Each issuewild Property MUST be ignored when processing a request for // an FQDN that is not a Wildcard Domain Name."; and @@ -500,6 +358,15 @@ func (va *ValidationAuthorityImpl) validateCAA(caaSet *caaResult, wildcard bool, records = caaSet.issuewild } + if len(records) == 0 { + // Although CAA records exist, none of them pertain to issuance in this case. + // (e.g. there is only an issuewild directive, but we are checking for a + // non-wildcard identifier, or there is only an iodef or non-critical unknown + // directive.) + va.metrics.caaCounter.WithLabelValues("no relevant records").Inc() + return true, caaSet.name + } + // There are CAA records pertaining to issuance in our case. Note that this // includes the case of the unsatisfiable CAA record value ";", used to // prevent issuance by any CA under any circumstance. @@ -532,13 +399,19 @@ func (va *ValidationAuthorityImpl) validateCAA(caaSet *caaResult, wildcard bool, return false, caaSet.name } +// caaParameter is a key-value pair parsed from a single CAA RR. +type caaParameter struct { + tag string + val string +} + // parseCAARecord extracts the domain and parameters (if any) from a // issue/issuewild CAA record. This follows RFC 8659 Section 4.2 and Section 4.3 // (https://www.rfc-editor.org/rfc/rfc8659.html#section-4). It returns the // domain name (which may be the empty string if the record forbids issuance) -// and a tag-value map of CAA parameters, or a descriptive error if the record -// is malformed. -func parseCAARecord(caa *dns.CAA) (string, map[string]string, error) { +// and a slice of CAA parameters, or a descriptive error if the record is +// malformed. +func parseCAARecord(caa *dns.CAA) (string, []caaParameter, error) { isWSP := func(r rune) bool { return r == '\t' || r == ' ' } @@ -546,16 +419,21 @@ func parseCAARecord(caa *dns.CAA) (string, map[string]string, error) { // Semi-colons (ASCII 0x3B) are prohibited from being specified in the // parameter tag or value, hence we can simply split on semi-colons. parts := strings.Split(caa.Value, ";") - domain := strings.TrimFunc(parts[0], isWSP) + + // See https://www.rfc-editor.org/rfc/rfc8659.html#section-4.2 + // + // issuer-domain-name = label *("." label) + // label = (ALPHA / DIGIT) *( *("-") (ALPHA / DIGIT)) + issuerDomainName := strings.TrimFunc(parts[0], isWSP) paramList := parts[1:] - parameters := make(map[string]string) // Handle the case where a semi-colon is specified following the domain // but no parameters are given. if len(paramList) == 1 && strings.TrimFunc(paramList[0], isWSP) == "" { - return domain, parameters, nil + return issuerDomainName, nil, nil } + var caaParameters []caaParameter for _, parameter := range paramList { // A parameter tag cannot include equal signs (ASCII 0x3D), // however they are permitted in the value itself. @@ -584,10 +462,13 @@ func parseCAARecord(caa *dns.CAA) (string, map[string]string, error) { } } - parameters[tag] = value + caaParameters = append(caaParameters, caaParameter{ + tag: tag, + val: value, + }) } - return domain, parameters, nil + return issuerDomainName, caaParameters, nil } // caaDomainMatches checks that the issuer domain name listed in the parsed @@ -599,10 +480,26 @@ func caaDomainMatches(caaDomain string, issuerDomain string) bool { // caaAccountURIMatches checks that the accounturi CAA parameter, if present, // matches one of the specific account URIs we expect. We support multiple // account URI prefixes to handle accounts which were registered under ACMEv1. +// We accept only a single "accounturi" parameter and will fail if multiple are +// found in the CAA RR. // See RFC 8657 Section 3: https://www.rfc-editor.org/rfc/rfc8657.html#section-3 -func caaAccountURIMatches(caaParams map[string]string, accountURIPrefixes []string, accountID int64) bool { - accountURI, ok := caaParams["accounturi"] - if !ok { +func caaAccountURIMatches(caaParams []caaParameter, accountURIPrefixes []string, accountID int64) bool { + var found bool + var accountURI string + for _, c := range caaParams { + if c.tag == "accounturi" { + if found { + // A Property with multiple "accounturi" parameters is + // unsatisfiable. + return false + } + accountURI = c.val + found = true + } + } + + if !found { + // A Property without an "accounturi" parameter matches any account. return true } @@ -624,17 +521,39 @@ var validationMethodRegexp = regexp.MustCompile(`^[[:alnum:]-]+$`) // caaValidationMethodMatches checks that the validationmethods CAA parameter, // if present, contains the exact name of the ACME validation method used to -// validate this domain. -// See RFC 8657 Section 4: https://www.rfc-editor.org/rfc/rfc8657.html#section-4 -func caaValidationMethodMatches(caaParams map[string]string, method core.AcmeChallenge) bool { - commaSeparatedMethods, ok := caaParams["validationmethods"] - if !ok { +// validate this domain. We accept only a single "validationmethods" parameter +// and will fail if multiple are found in the CAA RR, even if all tag-value +// pairs would be valid. See RFC 8657 Section 4: +// https://www.rfc-editor.org/rfc/rfc8657.html#section-4. +func caaValidationMethodMatches(caaParams []caaParameter, method core.AcmeChallenge) bool { + var validationMethods string + var found bool + for _, param := range caaParams { + if param.tag == "validationmethods" { + if found { + // RFC 8657 does not define what behavior to take when multiple + // "validationmethods" parameters exist, but we make the + // conscious choice to fail validation similar to how multiple + // "accounturi" parameters are "unsatisfiable". Subscribers + // should be aware of RFC 8657 Section 5.8: + // https://www.rfc-editor.org/rfc/rfc8657.html#section-5.8 + return false + } + validationMethods = param.val + found = true + } + } + + if !found { return true } - for _, m := range strings.Split(commaSeparatedMethods, ",") { - // If any listed method does not match the ABNF 1*(ALPHA / DIGIT / "-"), - // immediately reject the whole record. + for _, m := range strings.Split(validationMethods, ",") { + // The value of the "validationmethods" parameter MUST comply with the + // following ABNF [RFC5234]: + // + // value = [*(label ",") label] + // label = 1*(ALPHA / DIGIT / "-") if !validationMethodRegexp.MatchString(m) { return false } @@ -643,10 +562,10 @@ func caaValidationMethodMatches(caaParams map[string]string, method core.AcmeCha if !caaMethod.IsValid() { continue } - if caaMethod == method { return true } } + return false } diff --git a/third-party/github.com/letsencrypt/boulder/va/caa_test.go b/third-party/github.com/letsencrypt/boulder/va/caa_test.go index c6f00b0b7..92a862474 100644 --- a/third-party/github.com/letsencrypt/boulder/va/caa_test.go +++ b/third-party/github.com/letsencrypt/boulder/va/caa_test.go @@ -2,13 +2,17 @@ package va import ( "context" + "encoding/json" "errors" "fmt" - "net" + "net/netip" + "regexp" + "slices" "strings" "testing" "github.com/miekg/dns" + "github.com/prometheus/client_golang/prometheus" "github.com/letsencrypt/boulder/bdns" "github.com/letsencrypt/boulder/core" @@ -30,9 +34,8 @@ func (mock caaMockDNS) LookupTXT(_ context.Context, hostname string) ([]string, return nil, bdns.ResolverAddrs{"caaMockDNS"}, nil } -func (mock caaMockDNS) LookupHost(_ context.Context, hostname string) ([]net.IP, bdns.ResolverAddrs, error) { - ip := net.ParseIP("127.0.0.1") - return []net.IP{ip}, bdns.ResolverAddrs{"caaMockDNS"}, nil +func (mock caaMockDNS) LookupHost(_ context.Context, hostname string) ([]netip.Addr, bdns.ResolverAddrs, error) { + return []netip.Addr{netip.MustParseAddr("127.0.0.1")}, bdns.ResolverAddrs{"caaMockDNS"}, nil } func (mock caaMockDNS) LookupCAA(_ context.Context, domain string) ([]*dns.CAA, string, bdns.ResolverAddrs, error) { @@ -190,14 +193,14 @@ func (mock caaMockDNS) LookupCAA(_ context.Context, domain string) ([]*dns.CAA, } func TestCAATimeout(t *testing.T) { - va, _ := setup(nil, 0, "", nil, caaMockDNS{}) + va, _ := setup(nil, "", nil, caaMockDNS{}) params := &caaParams{ accountURIID: 12345, validationMethod: core.ChallengeTypeHTTP01, } - err := va.checkCAA(ctx, identifier.DNSIdentifier("caa-timeout.com"), params) + err := va.checkCAA(ctx, identifier.NewDNS("caa-timeout.com"), params) test.AssertErrorIs(t, err, berrors.DNS) test.AssertContains(t, err.Error(), "error") } @@ -282,11 +285,17 @@ func TestCAAChecking(t *testing.T) { Valid: false, }, { - Name: "Good (unknown non-critical, no issue/issuewild)", + Name: "Good (unknown non-critical, no issue)", Domain: "unknown-noncritical.com", FoundAt: "unknown-noncritical.com", Valid: true, }, + { + Name: "Good (unknown non-critical, no issuewild)", + Domain: "*.unknown-noncritical.com", + FoundAt: "unknown-noncritical.com", + Valid: true, + }, { Name: "Good (issue rec with unknown params)", Domain: "present-with-parameter.com", @@ -407,14 +416,14 @@ func TestCAAChecking(t *testing.T) { method := core.ChallengeTypeHTTP01 params := &caaParams{accountURIID: accountURIID, validationMethod: method} - va, _ := setup(nil, 0, "", nil, caaMockDNS{}) + va, _ := setup(nil, "", nil, caaMockDNS{}) va.accountURIPrefixes = []string{"https://letsencrypt.org/acct/reg/"} for _, caaTest := range testCases { mockLog := va.log.(*blog.Mock) defer mockLog.Clear() t.Run(caaTest.Name, func(t *testing.T) { - ident := identifier.DNSIdentifier(caaTest.Domain) + ident := identifier.NewDNS(caaTest.Domain) foundAt, valid, _, err := va.checkCAARecords(ctx, ident, params) if err != nil { t.Errorf("checkCAARecords error for %s: %s", caaTest.Domain, err) @@ -430,7 +439,7 @@ func TestCAAChecking(t *testing.T) { } func TestCAALogging(t *testing.T) { - va, _ := setup(nil, 0, "", nil, caaMockDNS{}) + va, _ := setup(nil, "", nil, caaMockDNS{}) testCases := []struct { Name string @@ -504,7 +513,7 @@ func TestCAALogging(t *testing.T) { accountURIID: tc.AccountURIID, validationMethod: tc.ChallengeType, } - _ = va.checkCAA(ctx, identifier.ACMEIdentifier{Type: identifier.DNS, Value: tc.Domain}, params) + _ = va.checkCAA(ctx, identifier.NewDNS(tc.Domain), params) caaLogLines := mockLog.GetAllMatching(`Checked CAA records for`) if len(caaLogLines) != 1 { @@ -517,16 +526,17 @@ func TestCAALogging(t *testing.T) { } } -// TestIsCAAValidErrMessage tests that an error result from `va.IsCAAValid` +// TestDoCAAErrMessage tests that an error result from `va.IsCAAValid` // includes the domain name that was being checked in the failure detail. -func TestIsCAAValidErrMessage(t *testing.T) { - va, _ := setup(nil, 0, "", nil, caaMockDNS{}) +func TestDoCAAErrMessage(t *testing.T) { + t.Parallel() + va, _ := setup(nil, "", nil, caaMockDNS{}) - // Call IsCAAValid with a domain we know fails with a generic error from the + // Call the operation with a domain we know fails with a generic error from the // caaMockDNS. domain := "caa-timeout.com" - resp, err := va.IsCAAValid(ctx, &vapb.IsCAAValidRequest{ - Domain: domain, + resp, err := va.DoCAA(ctx, &vapb.IsCAAValidRequest{ + Identifier: identifier.NewDNS(domain).ToProto(), ValidationMethod: string(core.ChallengeTypeHTTP01), AccountURIID: 12345, }) @@ -541,33 +551,42 @@ func TestIsCAAValidErrMessage(t *testing.T) { test.AssertEquals(t, resp.Problem.Detail, fmt.Sprintf("While processing CAA for %s: error", domain)) } -// TestIsCAAValidParams tests that the IsCAAValid method rejects any requests +// TestDoCAAParams tests that the IsCAAValid method rejects any requests // which do not have the necessary parameters to do CAA Account and Method // Binding checks. -func TestIsCAAValidParams(t *testing.T) { - va, _ := setup(nil, 0, "", nil, caaMockDNS{}) +func TestDoCAAParams(t *testing.T) { + t.Parallel() + va, _ := setup(nil, "", nil, caaMockDNS{}) // Calling IsCAAValid without a ValidationMethod should fail. - _, err := va.IsCAAValid(ctx, &vapb.IsCAAValidRequest{ - Domain: "present.com", + _, err := va.DoCAA(ctx, &vapb.IsCAAValidRequest{ + Identifier: identifier.NewDNS("present.com").ToProto(), AccountURIID: 12345, }) test.AssertError(t, err, "calling IsCAAValid without a ValidationMethod") // Calling IsCAAValid with an invalid ValidationMethod should fail. - _, err = va.IsCAAValid(ctx, &vapb.IsCAAValidRequest{ - Domain: "present.com", + _, err = va.DoCAA(ctx, &vapb.IsCAAValidRequest{ + Identifier: identifier.NewDNS("present.com").ToProto(), ValidationMethod: "tls-sni-01", AccountURIID: 12345, }) test.AssertError(t, err, "calling IsCAAValid with a bad ValidationMethod") // Calling IsCAAValid without an AccountURIID should fail. - _, err = va.IsCAAValid(ctx, &vapb.IsCAAValidRequest{ - Domain: "present.com", + _, err = va.DoCAA(ctx, &vapb.IsCAAValidRequest{ + Identifier: identifier.NewDNS("present.com").ToProto(), ValidationMethod: string(core.ChallengeTypeHTTP01), }) test.AssertError(t, err, "calling IsCAAValid without an AccountURIID") + + // Calling IsCAAValid with a non-DNS identifier type should fail. + _, err = va.DoCAA(ctx, &vapb.IsCAAValidRequest{ + Identifier: identifier.NewIP(netip.MustParseAddr("127.0.0.1")).ToProto(), + ValidationMethod: string(core.ChallengeTypeHTTP01), + AccountURIID: 12345, + }) + test.AssertError(t, err, "calling IsCAAValid with a non-DNS identifier type") } var errCAABrokenDNSClient = errors.New("dnsClient is broken") @@ -580,7 +599,7 @@ func (b caaBrokenDNS) LookupTXT(_ context.Context, hostname string) ([]string, b return nil, bdns.ResolverAddrs{"caaBrokenDNS"}, errCAABrokenDNSClient } -func (b caaBrokenDNS) LookupHost(_ context.Context, hostname string) ([]net.IP, bdns.ResolverAddrs, error) { +func (b caaBrokenDNS) LookupHost(_ context.Context, hostname string) ([]netip.Addr, bdns.ResolverAddrs, error) { return nil, bdns.ResolverAddrs{"caaBrokenDNS"}, errCAABrokenDNSClient } @@ -588,30 +607,6 @@ func (b caaBrokenDNS) LookupCAA(_ context.Context, domain string) ([]*dns.CAA, s return nil, "", bdns.ResolverAddrs{"caaBrokenDNS"}, errCAABrokenDNSClient } -func TestDisabledMultiCAARechecking(t *testing.T) { - brokenRVA := setupRemote(nil, "broken", caaBrokenDNS{}) - remoteVAs := []RemoteVA{{brokenRVA, "broken"}} - va, _ := setup(nil, 0, "local", remoteVAs, nil) - - features.Set(features.Config{ - EnforceMultiCAA: false, - MultiCAAFullResults: false, - }) - defer features.Reset() - - isValidRes, err := va.IsCAAValid(context.TODO(), &vapb.IsCAAValidRequest{ - Domain: "present.com", - ValidationMethod: string(core.ChallengeTypeDNS01), - AccountURIID: 1, - }) - test.AssertNotError(t, err, "Error during IsCAAValid") - // The primary VA can successfully recheck the CAA record and is allowed to - // issue for this domain. If `EnforceMultiCAA`` was enabled, the configured - // remote VA with broken dns.Client would fail the check and return a - // Problem, but that code path could never trigger. - test.AssertBoxedNil(t, isValidRes.Problem, "IsCAAValid returned a problem, but should not have") -} - // caaHijackedDNS implements the `dns.DNSClient` interface with a set of useful // test answers for CAA queries. It returns alternate CAA records than what // caaMockDNS returns simulating either a BGP hijack or DNS records that have @@ -622,9 +617,8 @@ func (h caaHijackedDNS) LookupTXT(_ context.Context, hostname string) ([]string, return nil, bdns.ResolverAddrs{"caaHijackedDNS"}, nil } -func (h caaHijackedDNS) LookupHost(_ context.Context, hostname string) ([]net.IP, bdns.ResolverAddrs, error) { - ip := net.ParseIP("127.0.0.1") - return []net.IP{ip}, bdns.ResolverAddrs{"caaHijackedDNS"}, nil +func (h caaHijackedDNS) LookupHost(_ context.Context, hostname string) ([]netip.Addr, bdns.ResolverAddrs, error) { + return []netip.Addr{netip.MustParseAddr("127.0.0.1")}, bdns.ResolverAddrs{"caaHijackedDNS"}, nil } func (h caaHijackedDNS) LookupCAA(_ context.Context, domain string) ([]*dns.CAA, string, bdns.ResolverAddrs, error) { // These records are altered from their caaMockDNS counterparts. Use this to @@ -654,6 +648,25 @@ func (h caaHijackedDNS) LookupCAA(_ context.Context, domain string) ([]*dns.CAA, return results, response, bdns.ResolverAddrs{"caaHijackedDNS"}, nil } +// parseValidationLogEvent extracts ... from JSON={ ... } in a ValidateChallenge +// audit log and returns it as a validationLogEvent struct. +func parseValidationLogEvent(t *testing.T, log []string) validationLogEvent { + re := regexp.MustCompile(`JSON=\{.*\}`) + var audit validationLogEvent + for _, line := range log { + match := re.FindString(line) + if match != "" { + jsonStr := match[len(`JSON=`):] + if err := json.Unmarshal([]byte(jsonStr), &audit); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + return audit + } + } + t.Fatal("JSON not found in log") + return audit +} + func TestMultiCAARechecking(t *testing.T) { // The remote differential log order is non-deterministic, so let's use // the same UA for all applicable RVAs. @@ -663,288 +676,439 @@ func TestMultiCAARechecking(t *testing.T) { brokenUA = "broken" hijackedUA = "hijacked" ) - remoteVA := setupRemote(nil, remoteUA, nil) - brokenVA := setupRemote(nil, brokenUA, caaBrokenDNS{}) - // Returns incorrect results - hijackedVA := setupRemote(nil, hijackedUA, caaHijackedDNS{}) testCases := []struct { name string - maxLookupFailures int - domains string - remoteVAs []RemoteVA + ident identifier.ACMEIdentifier + remoteVAs []remoteConf expectedProbSubstring string expectedProbType probs.ProblemType expectedDiffLogSubstring string + expectedSummary *mpicSummary + expectedLabels prometheus.Labels localDNSClient bdns.Client }{ { name: "all VAs functional, no CAA records", - domains: "present-dns-only.com", + ident: identifier.NewDNS("present-dns-only.com"), localDNSClient: caaMockDNS{}, - remoteVAs: []RemoteVA{ - {remoteVA, remoteUA}, - {remoteVA, remoteUA}, - {remoteVA, remoteUA}, + remoteVAs: []remoteConf{ + {ua: remoteUA, rir: arin}, + {ua: remoteUA, rir: ripe}, + {ua: remoteUA, rir: apnic}, + }, + expectedLabels: prometheus.Labels{ + "operation": opCAA, + "perspective": allPerspectives, + "challenge_type": string(core.ChallengeTypeDNS01), + "problem_type": "", + "result": pass, }, }, { name: "broken localVA, RVAs functional, no CAA records", - domains: "present-dns-only.com", + ident: identifier.NewDNS("present-dns-only.com"), localDNSClient: caaBrokenDNS{}, expectedProbSubstring: "While processing CAA for present-dns-only.com: dnsClient is broken", expectedProbType: probs.DNSProblem, - remoteVAs: []RemoteVA{ - {remoteVA, remoteUA}, - {remoteVA, remoteUA}, - {remoteVA, remoteUA}, + remoteVAs: []remoteConf{ + {ua: remoteUA, rir: arin}, + {ua: remoteUA, rir: ripe}, + {ua: remoteUA, rir: apnic}, + }, + expectedLabels: prometheus.Labels{ + "operation": opCAA, + "perspective": allPerspectives, + "challenge_type": string(core.ChallengeTypeDNS01), + "problem_type": string(probs.DNSProblem), + "result": fail, }, }, { name: "functional localVA, 1 broken RVA, no CAA records", - domains: "present-dns-only.com", - expectedProbSubstring: "During secondary CAA checking: While processing CAA", - expectedProbType: probs.DNSProblem, - expectedDiffLogSubstring: `RemoteSuccesses":2,"RemoteFailures":[{"VAHostname":"broken","Problem":{"type":"dns","detail":"While processing CAA for`, + ident: identifier.NewDNS("present-dns-only.com"), localDNSClient: caaMockDNS{}, - remoteVAs: []RemoteVA{ - {brokenVA, brokenUA}, - {remoteVA, remoteUA}, - {remoteVA, remoteUA}, + expectedDiffLogSubstring: `"RemoteSuccesses":2,"RemoteFailures":1`, + expectedSummary: &mpicSummary{ + Passed: []string{"dc-1-RIPE", "dc-2-APNIC"}, + Failed: []string{"dc-0-ARIN"}, + PassedRIRs: []string{ripe, apnic}, + QuorumResult: "2/3", + }, + remoteVAs: []remoteConf{ + {ua: brokenUA, rir: arin, dns: caaBrokenDNS{}}, + {ua: remoteUA, rir: ripe}, + {ua: remoteUA, rir: apnic}, + }, + expectedLabels: prometheus.Labels{ + "operation": opCAA, + "perspective": allPerspectives, + "challenge_type": string(core.ChallengeTypeDNS01), + "problem_type": "", + "result": pass, + }, + }, + { + name: "functional localVA, 2 broken RVA, no CAA records", + ident: identifier.NewDNS("present-dns-only.com"), + expectedProbSubstring: "During secondary validation: While processing CAA", + expectedProbType: probs.DNSProblem, + expectedDiffLogSubstring: `"RemoteSuccesses":1,"RemoteFailures":2`, + expectedSummary: &mpicSummary{ + Passed: []string{"dc-2-APNIC"}, + Failed: []string{"dc-0-ARIN", "dc-1-RIPE"}, + PassedRIRs: []string{apnic}, + QuorumResult: "1/3", + }, + localDNSClient: caaMockDNS{}, + remoteVAs: []remoteConf{ + {ua: brokenUA, rir: arin, dns: caaBrokenDNS{}}, + {ua: brokenUA, rir: ripe, dns: caaBrokenDNS{}}, + {ua: remoteUA, rir: apnic}, + }, + expectedLabels: prometheus.Labels{ + "operation": opCAA, + "perspective": allPerspectives, + "challenge_type": string(core.ChallengeTypeDNS01), + "problem_type": string(probs.DNSProblem), + "result": fail, }, }, { name: "functional localVA, all broken RVAs, no CAA records", - domains: "present-dns-only.com", - expectedProbSubstring: "During secondary CAA checking: While processing CAA", + ident: identifier.NewDNS("present-dns-only.com"), + expectedProbSubstring: "During secondary validation: While processing CAA", expectedProbType: probs.DNSProblem, - expectedDiffLogSubstring: `RemoteSuccesses":0,"RemoteFailures":[{"VAHostname":"broken","Problem":{"type":"dns","detail":"While processing CAA for`, - localDNSClient: caaMockDNS{}, - remoteVAs: []RemoteVA{ - {brokenVA, brokenUA}, - {brokenVA, brokenUA}, - {brokenVA, brokenUA}, + expectedDiffLogSubstring: `"RemoteSuccesses":0,"RemoteFailures":3`, + expectedSummary: &mpicSummary{ + Passed: []string{}, + Failed: []string{"dc-0-ARIN", "dc-1-RIPE", "dc-2-APNIC"}, + PassedRIRs: []string{}, + QuorumResult: "0/3", + }, + localDNSClient: caaMockDNS{}, + remoteVAs: []remoteConf{ + {ua: brokenUA, rir: arin, dns: caaBrokenDNS{}}, + {ua: brokenUA, rir: ripe, dns: caaBrokenDNS{}}, + {ua: brokenUA, rir: apnic, dns: caaBrokenDNS{}}, + }, + expectedLabels: prometheus.Labels{ + "operation": opCAA, + "perspective": allPerspectives, + "challenge_type": string(core.ChallengeTypeDNS01), + "problem_type": string(probs.DNSProblem), + "result": fail, }, }, { name: "all VAs functional, CAA issue type present", - domains: "present.com", + ident: identifier.NewDNS("present.com"), localDNSClient: caaMockDNS{}, - remoteVAs: []RemoteVA{ - {remoteVA, remoteUA}, - {remoteVA, remoteUA}, - {remoteVA, remoteUA}, + remoteVAs: []remoteConf{ + {ua: remoteUA, rir: arin}, + {ua: remoteUA, rir: ripe}, + {ua: remoteUA, rir: apnic}, + }, + expectedLabels: prometheus.Labels{ + "operation": opCAA, + "perspective": allPerspectives, + "challenge_type": string(core.ChallengeTypeDNS01), + "problem_type": "", + "result": pass, }, }, { name: "functional localVA, 1 broken RVA, CAA issue type present", - domains: "present.com", - expectedProbSubstring: "During secondary CAA checking: While processing CAA", + ident: identifier.NewDNS("present.com"), + expectedDiffLogSubstring: `"RemoteSuccesses":2,"RemoteFailures":1`, + expectedSummary: &mpicSummary{ + Passed: []string{"dc-1-RIPE", "dc-2-APNIC"}, + Failed: []string{"dc-0-ARIN"}, + PassedRIRs: []string{ripe, apnic}, + QuorumResult: "2/3", + }, + localDNSClient: caaMockDNS{}, + remoteVAs: []remoteConf{ + {ua: brokenUA, rir: arin, dns: caaBrokenDNS{}}, + {ua: remoteUA, rir: ripe}, + {ua: remoteUA, rir: apnic}, + }, + expectedLabels: prometheus.Labels{ + "operation": opCAA, + "perspective": allPerspectives, + "challenge_type": string(core.ChallengeTypeDNS01), + "problem_type": "", + "result": pass, + }, + }, + { + name: "functional localVA, 2 broken RVA, CAA issue type present", + ident: identifier.NewDNS("present.com"), + expectedProbSubstring: "During secondary validation: While processing CAA", expectedProbType: probs.DNSProblem, - expectedDiffLogSubstring: `RemoteSuccesses":2,"RemoteFailures":[{"VAHostname":"broken","Problem":{"type":"dns","detail":"While processing CAA for`, - localDNSClient: caaMockDNS{}, - remoteVAs: []RemoteVA{ - {brokenVA, brokenUA}, - {remoteVA, remoteUA}, - {remoteVA, remoteUA}, + expectedDiffLogSubstring: `"RemoteSuccesses":1,"RemoteFailures":2`, + expectedSummary: &mpicSummary{ + Passed: []string{"dc-2-APNIC"}, + Failed: []string{"dc-0-ARIN", "dc-1-RIPE"}, + PassedRIRs: []string{apnic}, + QuorumResult: "1/3", + }, + localDNSClient: caaMockDNS{}, + remoteVAs: []remoteConf{ + {ua: brokenUA, rir: arin, dns: caaBrokenDNS{}}, + {ua: brokenUA, rir: ripe, dns: caaBrokenDNS{}}, + {ua: remoteUA, rir: apnic}, + }, + expectedLabels: prometheus.Labels{ + "operation": opCAA, + "perspective": allPerspectives, + "challenge_type": string(core.ChallengeTypeDNS01), + "problem_type": string(probs.DNSProblem), + "result": fail, }, }, { name: "functional localVA, all broken RVAs, CAA issue type present", - domains: "present.com", - expectedProbSubstring: "During secondary CAA checking: While processing CAA", + ident: identifier.NewDNS("present.com"), + expectedProbSubstring: "During secondary validation: While processing CAA", expectedProbType: probs.DNSProblem, - expectedDiffLogSubstring: `RemoteSuccesses":0,"RemoteFailures":[{"VAHostname":"broken","Problem":{"type":"dns","detail":"While processing CAA for`, - localDNSClient: caaMockDNS{}, - remoteVAs: []RemoteVA{ - {brokenVA, brokenUA}, - {brokenVA, brokenUA}, - {brokenVA, brokenUA}, + expectedDiffLogSubstring: `"RemoteSuccesses":0,"RemoteFailures":3`, + expectedSummary: &mpicSummary{ + Passed: []string{}, + Failed: []string{"dc-0-ARIN", "dc-1-RIPE", "dc-2-APNIC"}, + PassedRIRs: []string{}, + QuorumResult: "0/3", + }, + localDNSClient: caaMockDNS{}, + remoteVAs: []remoteConf{ + {ua: brokenUA, rir: arin, dns: caaBrokenDNS{}}, + {ua: brokenUA, rir: ripe, dns: caaBrokenDNS{}}, + {ua: brokenUA, rir: apnic, dns: caaBrokenDNS{}}, + }, + expectedLabels: prometheus.Labels{ + "operation": opCAA, + "perspective": allPerspectives, + "challenge_type": string(core.ChallengeTypeDNS01), + "problem_type": string(probs.DNSProblem), + "result": fail, }, }, { - // The localVA kicks off the background goroutines before doing its - // own check. But if its own check fails, it doesn't wait for their - // results. + // The localVA returns early with a problem before kicking off the + // remote checks. name: "all VAs functional, CAA issue type forbids issuance", - domains: "unsatisfiable.com", + ident: identifier.NewDNS("unsatisfiable.com"), expectedProbSubstring: "CAA record for unsatisfiable.com prevents issuance", expectedProbType: probs.CAAProblem, localDNSClient: caaMockDNS{}, - remoteVAs: []RemoteVA{ - {remoteVA, remoteUA}, - {remoteVA, remoteUA}, - {remoteVA, remoteUA}, + remoteVAs: []remoteConf{ + {ua: remoteUA, rir: arin}, + {ua: remoteUA, rir: ripe}, + {ua: remoteUA, rir: apnic}, }, }, { name: "1 hijacked RVA, CAA issue type present", - domains: "present.com", - expectedProbSubstring: "CAA record for present.com prevents issuance", - expectedProbType: probs.CAAProblem, - expectedDiffLogSubstring: `RemoteSuccesses":2,"RemoteFailures":[{"VAHostname":"hijacked","Problem":{"type":"caa","detail":"While processing CAA for`, - localDNSClient: caaMockDNS{}, - remoteVAs: []RemoteVA{ - {hijackedVA, hijackedUA}, - {remoteVA, remoteUA}, - {remoteVA, remoteUA}, + ident: identifier.NewDNS("present.com"), + expectedDiffLogSubstring: `"RemoteSuccesses":2,"RemoteFailures":1`, + expectedSummary: &mpicSummary{ + Passed: []string{"dc-1-RIPE", "dc-2-APNIC"}, + Failed: []string{"dc-0-ARIN"}, + PassedRIRs: []string{ripe, apnic}, + QuorumResult: "2/3", + }, + localDNSClient: caaMockDNS{}, + remoteVAs: []remoteConf{ + {ua: hijackedUA, rir: arin, dns: caaHijackedDNS{}}, + {ua: remoteUA, rir: ripe}, + {ua: remoteUA, rir: apnic}, }, }, { name: "2 hijacked RVAs, CAA issue type present", - domains: "present.com", - expectedProbSubstring: "During secondary CAA checking: While processing CAA", + ident: identifier.NewDNS("present.com"), + expectedProbSubstring: "During secondary validation: While processing CAA", expectedProbType: probs.CAAProblem, - expectedDiffLogSubstring: `RemoteSuccesses":1,"RemoteFailures":[{"VAHostname":"hijacked","Problem":{"type":"caa","detail":"While processing CAA for`, - localDNSClient: caaMockDNS{}, - remoteVAs: []RemoteVA{ - {hijackedVA, hijackedUA}, - {hijackedVA, hijackedUA}, - {remoteVA, remoteUA}, + expectedDiffLogSubstring: `"RemoteSuccesses":1,"RemoteFailures":2`, + expectedSummary: &mpicSummary{ + Passed: []string{"dc-2-APNIC"}, + Failed: []string{"dc-0-ARIN", "dc-1-RIPE"}, + PassedRIRs: []string{apnic}, + QuorumResult: "1/3", + }, + localDNSClient: caaMockDNS{}, + remoteVAs: []remoteConf{ + {ua: hijackedUA, rir: arin, dns: caaHijackedDNS{}}, + {ua: hijackedUA, rir: ripe, dns: caaHijackedDNS{}}, + {ua: remoteUA, rir: apnic}, }, }, { name: "3 hijacked RVAs, CAA issue type present", - domains: "present.com", - expectedProbSubstring: "During secondary CAA checking: While processing CAA", + ident: identifier.NewDNS("present.com"), + expectedProbSubstring: "During secondary validation: While processing CAA", expectedProbType: probs.CAAProblem, - expectedDiffLogSubstring: `RemoteSuccesses":0,"RemoteFailures":[{"VAHostname":"hijacked","Problem":{"type":"caa","detail":"While processing CAA for`, - localDNSClient: caaMockDNS{}, - remoteVAs: []RemoteVA{ - {hijackedVA, hijackedUA}, - {hijackedVA, hijackedUA}, - {hijackedVA, hijackedUA}, + expectedDiffLogSubstring: `"RemoteSuccesses":0,"RemoteFailures":3`, + expectedSummary: &mpicSummary{ + Passed: []string{}, + Failed: []string{"dc-0-ARIN", "dc-1-RIPE", "dc-2-APNIC"}, + PassedRIRs: []string{}, + QuorumResult: "0/3", + }, + localDNSClient: caaMockDNS{}, + remoteVAs: []remoteConf{ + {ua: hijackedUA, rir: arin, dns: caaHijackedDNS{}}, + {ua: hijackedUA, rir: ripe, dns: caaHijackedDNS{}}, + {ua: hijackedUA, rir: apnic, dns: caaHijackedDNS{}}, }, }, { name: "1 hijacked RVA, CAA issuewild type present", - domains: "satisfiable-wildcard.com", - expectedProbSubstring: "During secondary CAA checking: While processing CAA", - expectedProbType: probs.CAAProblem, - expectedDiffLogSubstring: `RemoteSuccesses":2,"RemoteFailures":[{"VAHostname":"hijacked","Problem":{"type":"caa","detail":"While processing CAA for`, - localDNSClient: caaMockDNS{}, - remoteVAs: []RemoteVA{ - {hijackedVA, hijackedUA}, - {remoteVA, remoteUA}, - {remoteVA, remoteUA}, + ident: identifier.NewDNS("satisfiable-wildcard.com"), + expectedDiffLogSubstring: `"RemoteSuccesses":2,"RemoteFailures":1`, + expectedSummary: &mpicSummary{ + Passed: []string{"dc-1-RIPE", "dc-2-APNIC"}, + Failed: []string{"dc-0-ARIN"}, + PassedRIRs: []string{ripe, apnic}, + QuorumResult: "2/3", + }, + localDNSClient: caaMockDNS{}, + remoteVAs: []remoteConf{ + {ua: hijackedUA, rir: arin, dns: caaHijackedDNS{}}, + {ua: remoteUA, rir: ripe}, + {ua: remoteUA, rir: apnic}, }, }, { name: "2 hijacked RVAs, CAA issuewild type present", - domains: "satisfiable-wildcard.com", - expectedProbSubstring: "During secondary CAA checking: While processing CAA", + ident: identifier.NewDNS("satisfiable-wildcard.com"), + expectedProbSubstring: "During secondary validation: While processing CAA", expectedProbType: probs.CAAProblem, - expectedDiffLogSubstring: `RemoteSuccesses":1,"RemoteFailures":[{"VAHostname":"hijacked","Problem":{"type":"caa","detail":"While processing CAA for`, - localDNSClient: caaMockDNS{}, - remoteVAs: []RemoteVA{ - {hijackedVA, hijackedUA}, - {hijackedVA, hijackedUA}, - {remoteVA, remoteUA}, + expectedDiffLogSubstring: `"RemoteSuccesses":1,"RemoteFailures":2`, + expectedSummary: &mpicSummary{ + Passed: []string{"dc-2-APNIC"}, + Failed: []string{"dc-0-ARIN", "dc-1-RIPE"}, + PassedRIRs: []string{apnic}, + QuorumResult: "1/3", + }, + localDNSClient: caaMockDNS{}, + remoteVAs: []remoteConf{ + {ua: hijackedUA, rir: arin, dns: caaHijackedDNS{}}, + {ua: hijackedUA, rir: ripe, dns: caaHijackedDNS{}}, + {ua: remoteUA, rir: apnic}, }, }, { name: "3 hijacked RVAs, CAA issuewild type present", - domains: "satisfiable-wildcard.com", - expectedProbSubstring: "During secondary CAA checking: While processing CAA", + ident: identifier.NewDNS("satisfiable-wildcard.com"), + expectedProbSubstring: "During secondary validation: While processing CAA", expectedProbType: probs.CAAProblem, - expectedDiffLogSubstring: `RemoteSuccesses":0,"RemoteFailures":[{"VAHostname":"hijacked","Problem":{"type":"caa","detail":"While processing CAA for`, - localDNSClient: caaMockDNS{}, - remoteVAs: []RemoteVA{ - {hijackedVA, hijackedUA}, - {hijackedVA, hijackedUA}, - {hijackedVA, hijackedUA}, + expectedDiffLogSubstring: `"RemoteSuccesses":0,"RemoteFailures":3`, + expectedSummary: &mpicSummary{ + Passed: []string{}, + Failed: []string{"dc-0-ARIN", "dc-1-RIPE", "dc-2-APNIC"}, + PassedRIRs: []string{}, + QuorumResult: "0/3", + }, + localDNSClient: caaMockDNS{}, + remoteVAs: []remoteConf{ + {ua: hijackedUA, rir: arin, dns: caaHijackedDNS{}}, + {ua: hijackedUA, rir: ripe, dns: caaHijackedDNS{}}, + {ua: hijackedUA, rir: apnic, dns: caaHijackedDNS{}}, }, }, { name: "1 hijacked RVA, CAA issuewild type present, 1 failure allowed", - domains: "satisfiable-wildcard.com", - maxLookupFailures: 1, - expectedDiffLogSubstring: `RemoteSuccesses":2,"RemoteFailures":[{"VAHostname":"hijacked","Problem":{"type":"caa","detail":"While processing CAA for`, - localDNSClient: caaMockDNS{}, - remoteVAs: []RemoteVA{ - {hijackedVA, hijackedUA}, - {remoteVA, remoteUA}, - {remoteVA, remoteUA}, + ident: identifier.NewDNS("satisfiable-wildcard.com"), + expectedDiffLogSubstring: `"RemoteSuccesses":2,"RemoteFailures":1`, + expectedSummary: &mpicSummary{ + Passed: []string{"dc-1-RIPE", "dc-2-APNIC"}, + Failed: []string{"dc-0-ARIN"}, + PassedRIRs: []string{ripe, apnic}, + QuorumResult: "2/3", + }, + localDNSClient: caaMockDNS{}, + remoteVAs: []remoteConf{ + {ua: hijackedUA, rir: arin, dns: caaHijackedDNS{}}, + {ua: remoteUA, rir: ripe}, + {ua: remoteUA, rir: apnic}, }, }, { name: "2 hijacked RVAs, CAA issuewild type present, 1 failure allowed", - domains: "satisfiable-wildcard.com", - maxLookupFailures: 1, - expectedProbSubstring: "During secondary CAA checking: While processing CAA", + ident: identifier.NewDNS("satisfiable-wildcard.com"), + expectedProbSubstring: "During secondary validation: While processing CAA", expectedProbType: probs.CAAProblem, - expectedDiffLogSubstring: `RemoteSuccesses":1,"RemoteFailures":[{"VAHostname":"hijacked","Problem":{"type":"caa","detail":"While processing CAA for`, - localDNSClient: caaMockDNS{}, - remoteVAs: []RemoteVA{ - {hijackedVA, hijackedUA}, - {hijackedVA, hijackedUA}, - {remoteVA, remoteUA}, + expectedDiffLogSubstring: `"RemoteSuccesses":1,"RemoteFailures":2`, + expectedSummary: &mpicSummary{ + Passed: []string{"dc-2-APNIC"}, + Failed: []string{"dc-0-ARIN", "dc-1-RIPE"}, + PassedRIRs: []string{apnic}, + QuorumResult: "1/3", + }, + localDNSClient: caaMockDNS{}, + remoteVAs: []remoteConf{ + {ua: hijackedUA, rir: arin, dns: caaHijackedDNS{}}, + {ua: hijackedUA, rir: ripe, dns: caaHijackedDNS{}}, + {ua: remoteUA, rir: apnic}, }, }, { name: "3 hijacked RVAs, CAA issuewild type present, 1 failure allowed", - domains: "satisfiable-wildcard.com", - maxLookupFailures: 1, - expectedProbSubstring: "During secondary CAA checking: While processing CAA", + ident: identifier.NewDNS("satisfiable-wildcard.com"), + expectedProbSubstring: "During secondary validation: While processing CAA", expectedProbType: probs.CAAProblem, - expectedDiffLogSubstring: `RemoteSuccesses":0,"RemoteFailures":[{"VAHostname":"hijacked","Problem":{"type":"caa","detail":"While processing CAA for`, - localDNSClient: caaMockDNS{}, - remoteVAs: []RemoteVA{ - {hijackedVA, hijackedUA}, - {hijackedVA, hijackedUA}, - {hijackedVA, hijackedUA}, + expectedDiffLogSubstring: `"RemoteSuccesses":0,"RemoteFailures":3`, + expectedSummary: &mpicSummary{ + Passed: []string{}, + Failed: []string{"dc-0-ARIN", "dc-1-RIPE", "dc-2-APNIC"}, + PassedRIRs: []string{}, + QuorumResult: "0/3", + }, + localDNSClient: caaMockDNS{}, + remoteVAs: []remoteConf{ + {ua: hijackedUA, rir: arin, dns: caaHijackedDNS{}}, + {ua: hijackedUA, rir: ripe, dns: caaHijackedDNS{}}, + {ua: hijackedUA, rir: apnic, dns: caaHijackedDNS{}}, }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - va, mockLog := setup(nil, tc.maxLookupFailures, localUA, tc.remoteVAs, tc.localDNSClient) + va, mockLog := setupWithRemotes(nil, localUA, tc.remoteVAs, tc.localDNSClient) defer mockLog.Clear() - // MultiCAAFullResults: false is inherently flaky because of the - // non-deterministic nature of concurrent goroutine returns. We, - // boulder dev, made a decision to skip testing that path and - // eventually make MultiCAAFullResults: true the default. features.Set(features.Config{ - EnforceMultiCAA: true, - MultiCAAFullResults: true, + EnforceMultiCAA: true, }) defer features.Reset() - isValidRes, err := va.IsCAAValid(context.TODO(), &vapb.IsCAAValidRequest{ - Domain: tc.domains, + isValidRes, err := va.DoCAA(context.TODO(), &vapb.IsCAAValidRequest{ + Identifier: tc.ident.ToProto(), ValidationMethod: string(core.ChallengeTypeDNS01), AccountURIID: 1, }) test.AssertNotError(t, err, "Should not have errored, but did") if tc.expectedProbSubstring != "" { + test.AssertNotNil(t, isValidRes.Problem, "IsCAAValidRequest returned nil problem, but should not have") test.AssertContains(t, isValidRes.Problem.Detail, tc.expectedProbSubstring) } else if isValidRes.Problem != nil { test.AssertBoxedNil(t, isValidRes.Problem, "IsCAAValidRequest returned a problem, but should not have") } if tc.expectedProbType != "" { + test.AssertNotNil(t, isValidRes.Problem, "IsCAAValidRequest returned nil problem, but should not have") test.AssertEquals(t, string(tc.expectedProbType), isValidRes.Problem.ProblemType) } - var invalidRVACount int - for _, x := range va.remoteVAs { - if x.Address == "broken" || x.Address == "hijacked" { - invalidRVACount++ - } - } - - gotRequestProbs := mockLog.GetAllMatching(".IsCAAValid returned problem: ") - test.AssertEquals(t, len(gotRequestProbs), invalidRVACount) - - gotDifferential := mockLog.GetAllMatching("remoteVADifferentials JSON=.*") - if features.Get().MultiCAAFullResults && tc.expectedDiffLogSubstring != "" { - test.AssertEquals(t, len(gotDifferential), 1) - test.AssertContains(t, gotDifferential[0], tc.expectedDiffLogSubstring) - } else { - test.AssertEquals(t, len(gotDifferential), 0) + if tc.expectedSummary != nil { + gotAuditLog := parseValidationLogEvent(t, mockLog.GetAllMatching("JSON=.*")) + slices.Sort(tc.expectedSummary.Passed) + slices.Sort(tc.expectedSummary.Failed) + slices.Sort(tc.expectedSummary.PassedRIRs) + test.AssertDeepEquals(t, gotAuditLog.Summary, tc.expectedSummary) } gotAnyRemoteFailures := mockLog.GetAllMatching("CAA check failed due to remote failures:") @@ -954,29 +1118,34 @@ func TestMultiCAARechecking(t *testing.T) { } else { test.AssertEquals(t, len(gotAnyRemoteFailures), 0) } + + if tc.expectedLabels != nil { + test.AssertMetricWithLabelsEquals(t, va.metrics.validationLatency, tc.expectedLabels, 1) + } + }) } } func TestCAAFailure(t *testing.T) { - hs := httpSrv(t, expectedToken) + hs := httpSrv(t, expectedToken, false) defer hs.Close() - va, _ := setup(hs, 0, "", nil, caaMockDNS{}) + va, _ := setup(hs, "", nil, caaMockDNS{}) - err := va.checkCAA(ctx, dnsi("reserved.com"), &caaParams{1, core.ChallengeTypeHTTP01}) + err := va.checkCAA(ctx, identifier.NewDNS("reserved.com"), &caaParams{1, core.ChallengeTypeHTTP01}) if err == nil { t.Fatalf("Expected CAA rejection for reserved.com, got success") } test.AssertErrorIs(t, err, berrors.CAA) - err = va.checkCAA(ctx, dnsi("example.gonetld"), &caaParams{1, core.ChallengeTypeHTTP01}) + err = va.checkCAA(ctx, identifier.NewDNS("example.gonetld"), &caaParams{1, core.ChallengeTypeHTTP01}) if err == nil { t.Fatalf("Expected CAA rejection for gonetld, got success") } prob := detailedError(err) test.AssertEquals(t, prob.Type, probs.DNSProblem) - test.AssertContains(t, prob.Error(), "NXDOMAIN") + test.AssertContains(t, prob.String(), "NXDOMAIN") } func TestFilterCAA(t *testing.T) { @@ -1109,16 +1278,17 @@ func TestSelectCAA(t *testing.T) { } func TestAccountURIMatches(t *testing.T) { + t.Parallel() tests := []struct { name string - params map[string]string + params []caaParameter prefixes []string id int64 want bool }{ { name: "empty accounturi", - params: map[string]string{}, + params: nil, prefixes: []string{ "https://acme-v01.api.letsencrypt.org/acme/reg/", }, @@ -1126,10 +1296,17 @@ func TestAccountURIMatches(t *testing.T) { want: true, }, { - name: "non-uri accounturi", - params: map[string]string{ - "accounturi": "\\invalid 😎/123456", + name: "no accounturi in rr, but other parameters exist", + params: []caaParameter{{tag: "validationmethods", val: "tls-alpn-01"}}, + prefixes: []string{ + "https://acme-v02.api.letsencrypt.org/acme/reg/", }, + id: 123456, + want: true, + }, + { + name: "non-uri accounturi", + params: []caaParameter{{tag: "accounturi", val: "\\invalid 😎/123456"}}, prefixes: []string{ "\\invalid 😎", }, @@ -1137,10 +1314,8 @@ func TestAccountURIMatches(t *testing.T) { want: false, }, { - name: "simple match", - params: map[string]string{ - "accounturi": "https://acme-v01.api.letsencrypt.org/acme/reg/123456", - }, + name: "simple match", + params: []caaParameter{{tag: "accounturi", val: "https://acme-v01.api.letsencrypt.org/acme/reg/123456"}}, prefixes: []string{ "https://acme-v01.api.letsencrypt.org/acme/reg/", }, @@ -1148,10 +1323,17 @@ func TestAccountURIMatches(t *testing.T) { want: true, }, { - name: "accountid mismatch", - params: map[string]string{ - "accounturi": "https://acme-v01.api.letsencrypt.org/acme/reg/123456", + name: "simple match, but has a friend", + params: []caaParameter{{tag: "validationmethods", val: "dns-01"}, {tag: "accounturi", val: "https://acme-v01.api.letsencrypt.org/acme/reg/123456"}}, + prefixes: []string{ + "https://acme-v01.api.letsencrypt.org/acme/reg/", }, + id: 123456, + want: true, + }, + { + name: "accountid mismatch", + params: []caaParameter{{tag: "accounturi", val: "https://acme-v01.api.letsencrypt.org/acme/reg/123456"}}, prefixes: []string{ "https://acme-v01.api.letsencrypt.org/acme/reg/", }, @@ -1159,10 +1341,53 @@ func TestAccountURIMatches(t *testing.T) { want: false, }, { - name: "multiple prefixes, match first", - params: map[string]string{ - "accounturi": "https://acme-staging.api.letsencrypt.org/acme/reg/123456", + name: "single parameter, no value", + params: []caaParameter{{tag: "accounturi", val: ""}}, + prefixes: []string{ + "https://acme-v02.api.letsencrypt.org/acme/reg/", }, + id: 123456, + want: false, + }, + { + name: "multiple parameters, each with no value", + params: []caaParameter{{tag: "accounturi", val: ""}, {tag: "accounturi", val: ""}}, + prefixes: []string{ + "https://acme-v02.api.letsencrypt.org/acme/reg/", + }, + id: 123456, + want: false, + }, + { + name: "multiple parameters, one with no value", + params: []caaParameter{{tag: "accounturi", val: ""}, {tag: "accounturi", val: "https://acme-v02.api.letsencrypt.org/acme/reg/123456"}}, + prefixes: []string{ + "https://acme-v02.api.letsencrypt.org/acme/reg/", + }, + id: 123456, + want: false, + }, + { + name: "multiple parameters, each with an identical value", + params: []caaParameter{{tag: "accounturi", val: "https://acme-v02.api.letsencrypt.org/acme/reg/123456"}, {tag: "accounturi", val: "https://acme-v02.api.letsencrypt.org/acme/reg/123456"}}, + prefixes: []string{ + "https://acme-v02.api.letsencrypt.org/acme/reg/", + }, + id: 123456, + want: false, + }, + { + name: "multiple parameters, each with a different value", + params: []caaParameter{{tag: "accounturi", val: "https://acme-v02.api.letsencrypt.org/acme/reg/69"}, {tag: "accounturi", val: "https://acme-v02.api.letsencrypt.org/acme/reg/420"}}, + prefixes: []string{ + "https://acme-v02.api.letsencrypt.org/acme/reg/", + }, + id: 69, + want: false, + }, + { + name: "multiple prefixes, match first", + params: []caaParameter{{tag: "accounturi", val: "https://acme-staging.api.letsencrypt.org/acme/reg/123456"}}, prefixes: []string{ "https://acme-staging.api.letsencrypt.org/acme/reg/", "https://acme-staging-v02.api.letsencrypt.org/acme/acct/", @@ -1171,10 +1396,8 @@ func TestAccountURIMatches(t *testing.T) { want: true, }, { - name: "multiple prefixes, match second", - params: map[string]string{ - "accounturi": "https://acme-v02.api.letsencrypt.org/acme/acct/123456", - }, + name: "multiple prefixes, match second", + params: []caaParameter{{tag: "accounturi", val: "https://acme-v02.api.letsencrypt.org/acme/acct/123456"}}, prefixes: []string{ "https://acme-v01.api.letsencrypt.org/acme/reg/", "https://acme-v02.api.letsencrypt.org/acme/acct/", @@ -1183,10 +1406,8 @@ func TestAccountURIMatches(t *testing.T) { want: true, }, { - name: "multiple prefixes, match none", - params: map[string]string{ - "accounturi": "https://acme-v02.api.letsencrypt.org/acme/acct/123456", - }, + name: "multiple prefixes, match none", + params: []caaParameter{{tag: "accounturi", val: "https://acme-v02.api.letsencrypt.org/acme/acct/123456"}}, prefixes: []string{ "https://acme-v01.api.letsencrypt.org/acme/acct/", "https://acme-v03.api.letsencrypt.org/acme/acct/", @@ -1195,10 +1416,8 @@ func TestAccountURIMatches(t *testing.T) { want: false, }, { - name: "three prefixes", - params: map[string]string{ - "accounturi": "https://acme-v02.api.letsencrypt.org/acme/acct/123456", - }, + name: "three prefixes", + params: []caaParameter{{tag: "accounturi", val: "https://acme-v02.api.letsencrypt.org/acme/acct/123456"}}, prefixes: []string{ "https://acme-v01.api.letsencrypt.org/acme/reg/", "https://acme-v02.api.letsencrypt.org/acme/acct/", @@ -1208,10 +1427,8 @@ func TestAccountURIMatches(t *testing.T) { want: true, }, { - name: "multiple prefixes, wrong accountid", - params: map[string]string{ - "accounturi": "https://acme-v02.api.letsencrypt.org/acme/acct/123456", - }, + name: "multiple prefixes, wrong accountid", + params: []caaParameter{{tag: "accounturi", val: "https://acme-v02.api.letsencrypt.org/acme/acct/123456"}}, prefixes: []string{ "https://acme-v01.api.letsencrypt.org/acme/reg/", "https://acme-v02.api.letsencrypt.org/acme/acct/", @@ -1223,6 +1440,7 @@ func TestAccountURIMatches(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { + t.Parallel() got := caaAccountURIMatches(tc.params, tc.prefixes, tc.id) test.AssertEquals(t, got, tc.want) }) @@ -1230,79 +1448,106 @@ func TestAccountURIMatches(t *testing.T) { } func TestValidationMethodMatches(t *testing.T) { + t.Parallel() tests := []struct { name string - params map[string]string + params []caaParameter method core.AcmeChallenge want bool }{ { name: "empty validationmethods", - params: map[string]string{}, + params: nil, method: core.ChallengeTypeHTTP01, want: true, }, { - name: "only comma", - params: map[string]string{ - "validationmethods": ",", - }, - method: core.ChallengeTypeHTTP01, - want: false, - }, - { - name: "malformed method", - params: map[string]string{ - "validationmethods": "howdy !", - }, - method: core.ChallengeTypeHTTP01, - want: false, - }, - { - name: "invalid method", - params: map[string]string{ - "validationmethods": "tls-sni-01", - }, - method: core.ChallengeTypeHTTP01, - want: false, - }, - { - name: "simple match", - params: map[string]string{ - "validationmethods": "http-01", - }, + name: "no validationmethods in rr, but other parameters exist", // validationmethods is not mandatory + params: []caaParameter{{tag: "accounturi", val: "ph1LwuzHere"}}, method: core.ChallengeTypeHTTP01, want: true, }, { - name: "simple mismatch", - params: map[string]string{ - "validationmethods": "dns-01", - }, + name: "no value", + params: []caaParameter{{tag: "validationmethods", val: ""}}, // equivalent to forbidding issuance method: core.ChallengeTypeHTTP01, want: false, }, { - name: "multiple choices, match first", - params: map[string]string{ - "validationmethods": "http-01,dns-01", - }, + name: "only comma", + params: []caaParameter{{tag: "validationmethods", val: ","}}, + method: core.ChallengeTypeHTTP01, + want: false, + }, + { + name: "malformed method", + params: []caaParameter{{tag: "validationmethods", val: "howdy !"}}, + method: core.ChallengeTypeHTTP01, + want: false, + }, + { + name: "invalid method", + params: []caaParameter{{tag: "validationmethods", val: "tls-sni-01"}}, + method: core.ChallengeTypeHTTP01, + want: false, + }, + { + name: "simple match", + params: []caaParameter{{tag: "validationmethods", val: "http-01"}}, method: core.ChallengeTypeHTTP01, want: true, }, { - name: "multiple choices, match second", - params: map[string]string{ - "validationmethods": "http-01,dns-01", - }, + name: "simple match, but has a friend", + params: []caaParameter{{tag: "accounturi", val: "https://example.org"}, {tag: "validationmethods", val: "http-01"}}, + method: core.ChallengeTypeHTTP01, + want: true, + }, + { + name: "multiple validationmethods, each with no value", + params: []caaParameter{{tag: "validationmethods", val: ""}, {tag: "validationmethods", val: ""}}, + method: core.ChallengeTypeHTTP01, + want: false, + }, + { + name: "multiple validationmethods, one with no value", + params: []caaParameter{{tag: "validationmethods", val: ""}, {tag: "validationmethods", val: "http-01"}}, + method: core.ChallengeTypeHTTP01, + want: false, + }, + { + name: "multiple validationmethods, each with an identical value", + params: []caaParameter{{tag: "validationmethods", val: "http-01"}, {tag: "validationmethods", val: "http-01"}}, + method: core.ChallengeTypeHTTP01, + want: false, + }, + { + name: "multiple validationmethods, each with a different value", + params: []caaParameter{{tag: "validationmethods", val: "http-01"}, {tag: "validationmethods", val: "dns-01"}}, + method: core.ChallengeTypeHTTP01, + want: false, + }, + { + name: "simple mismatch", + params: []caaParameter{{tag: "validationmethods", val: "dns-01"}}, + method: core.ChallengeTypeHTTP01, + want: false, + }, + { + name: "multiple choices, match first", + params: []caaParameter{{tag: "validationmethods", val: "http-01,dns-01"}}, + method: core.ChallengeTypeHTTP01, + want: true, + }, + { + name: "multiple choices, match second", + params: []caaParameter{{tag: "validationmethods", val: "http-01,dns-01"}}, method: core.ChallengeTypeDNS01, want: true, }, { - name: "multiple choices, match none", - params: map[string]string{ - "validationmethods": "http-01,dns-01", - }, + name: "multiple choices, match none", + params: []caaParameter{{tag: "validationmethods", val: "http-01,dns-01"}}, method: core.ChallengeTypeTLSALPN01, want: false, }, @@ -1310,6 +1555,7 @@ func TestValidationMethodMatches(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { + t.Parallel() got := caaValidationMethodMatches(tc.params, tc.method) test.AssertEquals(t, got, tc.want) }) @@ -1317,81 +1563,96 @@ func TestValidationMethodMatches(t *testing.T) { } func TestExtractIssuerDomainAndParameters(t *testing.T) { + t.Parallel() tests := []struct { name string value string wantDomain string - wantParameters map[string]string + wantParameters []caaParameter expectErrSubstr string }{ { name: "empty record is valid", value: "", wantDomain: "", - wantParameters: map[string]string{}, + wantParameters: nil, expectErrSubstr: "", }, { name: "only semicolon is valid", value: ";", wantDomain: "", - wantParameters: map[string]string{}, + wantParameters: nil, expectErrSubstr: "", }, { name: "only semicolon and whitespace is valid", value: " ; ", wantDomain: "", - wantParameters: map[string]string{}, + wantParameters: nil, expectErrSubstr: "", }, { name: "only domain is valid", value: "letsencrypt.org", wantDomain: "letsencrypt.org", - wantParameters: map[string]string{}, + wantParameters: nil, expectErrSubstr: "", }, { name: "only domain with trailing semicolon is valid", value: "letsencrypt.org;", wantDomain: "letsencrypt.org", - wantParameters: map[string]string{}, + wantParameters: nil, + expectErrSubstr: "", + }, + { + name: "only domain with semicolon and trailing whitespace is valid", + value: "letsencrypt.org; ", + wantDomain: "letsencrypt.org", + wantParameters: nil, expectErrSubstr: "", }, { name: "domain with params and whitespace is valid", value: " letsencrypt.org ;foo=bar;baz=bar", wantDomain: "letsencrypt.org", - wantParameters: map[string]string{"foo": "bar", "baz": "bar"}, + wantParameters: []caaParameter{{tag: "foo", val: "bar"}, {tag: "baz", val: "bar"}}, expectErrSubstr: "", }, { name: "domain with params and different whitespace is valid", value: " letsencrypt.org ;foo=bar;baz=bar", wantDomain: "letsencrypt.org", - wantParameters: map[string]string{"foo": "bar", "baz": "bar"}, + wantParameters: []caaParameter{{tag: "foo", val: "bar"}, {tag: "baz", val: "bar"}}, expectErrSubstr: "", }, { name: "empty params are valid", value: "letsencrypt.org; foo=; baz = bar", wantDomain: "letsencrypt.org", - wantParameters: map[string]string{"foo": "", "baz": "bar"}, + wantParameters: []caaParameter{{tag: "foo", val: ""}, {tag: "baz", val: "bar"}}, expectErrSubstr: "", }, { name: "whitespace around params is valid", value: "letsencrypt.org; foo= ; baz = bar", wantDomain: "letsencrypt.org", - wantParameters: map[string]string{"foo": "", "baz": "bar"}, + wantParameters: []caaParameter{{tag: "foo", val: ""}, {tag: "baz", val: "bar"}}, expectErrSubstr: "", }, { name: "comma-separated param values are valid", value: "letsencrypt.org; foo=b1,b2,b3 ; baz = a=b ", wantDomain: "letsencrypt.org", - wantParameters: map[string]string{"foo": "b1,b2,b3", "baz": "a=b"}, + wantParameters: []caaParameter{{tag: "foo", val: "b1,b2,b3"}, {tag: "baz", val: "a=b"}}, + expectErrSubstr: "", + }, + { + name: "duplicate tags are valid", + value: "letsencrypt.org; foo=b1,b2,b3 ; foo= b1,b2,b3 ", + wantDomain: "letsencrypt.org", + wantParameters: []caaParameter{{tag: "foo", val: "b1,b2,b3"}, {tag: "foo", val: "b1,b2,b3"}}, expectErrSubstr: "", }, { @@ -1413,7 +1674,7 @@ func TestExtractIssuerDomainAndParameters(t *testing.T) { name: "hyphens in param values are valid", value: "letsencrypt.org; 1=2; baz=a-b", wantDomain: "letsencrypt.org", - wantParameters: map[string]string{"1": "2", "baz": "a-b"}, + wantParameters: []caaParameter{{tag: "1", val: "2"}, {tag: "baz", val: "a-b"}}, expectErrSubstr: "", }, { @@ -1444,6 +1705,7 @@ func TestExtractIssuerDomainAndParameters(t *testing.T) { } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { + t.Parallel() gotDomain, gotParameters, gotErr := parseCAARecord(&dns.CAA{Value: tc.value}) if tc.expectErrSubstr == "" { diff --git a/third-party/github.com/letsencrypt/boulder/va/config/config.go b/third-party/github.com/letsencrypt/boulder/va/config/config.go index 28a430619..e4faf4ce1 100644 --- a/third-party/github.com/letsencrypt/boulder/va/config/config.go +++ b/third-party/github.com/letsencrypt/boulder/va/config/config.go @@ -10,6 +10,8 @@ import ( // Common contains all of the shared fields for a VA and a Remote VA (RVA). type Common struct { cmd.ServiceConfig + // UserAgent is the "User-Agent" header sent during http-01 challenges and + // DoH queries. UserAgent string IssuerDomain string diff --git a/third-party/github.com/letsencrypt/boulder/va/dns.go b/third-party/github.com/letsencrypt/boulder/va/dns.go index 5ab61b9b1..d1639d2a5 100644 --- a/third-party/github.com/letsencrypt/boulder/va/dns.go +++ b/third-party/github.com/letsencrypt/boulder/va/dns.go @@ -6,7 +6,7 @@ import ( "crypto/subtle" "encoding/base64" "fmt" - "net" + "net/netip" "github.com/letsencrypt/boulder/bdns" "github.com/letsencrypt/boulder/core" @@ -15,12 +15,12 @@ import ( ) // getAddr will query for all A/AAAA records associated with hostname and return -// the preferred address, the first net.IP in the addrs slice, and all addresses -// resolved. This is the same choice made by the Go internal resolution library -// used by net/http. If there is an error resolving the hostname, or if no -// usable IP addresses are available then a berrors.DNSError instance is -// returned with a nil net.IP slice. -func (va ValidationAuthorityImpl) getAddrs(ctx context.Context, hostname string) ([]net.IP, bdns.ResolverAddrs, error) { +// the preferred address, the first netip.Addr in the addrs slice, and all +// addresses resolved. This is the same choice made by the Go internal +// resolution library used by net/http. If there is an error resolving the +// hostname, or if no usable IP addresses are available then a berrors.DNSError +// instance is returned with a nil netip.Addr slice. +func (va ValidationAuthorityImpl) getAddrs(ctx context.Context, hostname string) ([]netip.Addr, bdns.ResolverAddrs, error) { addrs, resolvers, err := va.dnsClient.LookupHost(ctx, hostname) if err != nil { return nil, resolvers, berrors.DNSError("%v", err) @@ -37,9 +37,9 @@ func (va ValidationAuthorityImpl) getAddrs(ctx context.Context, hostname string) // availableAddresses takes a ValidationRecord and splits the AddressesResolved // into a list of IPv4 and IPv6 addresses. -func availableAddresses(allAddrs []net.IP) (v4 []net.IP, v6 []net.IP) { +func availableAddresses(allAddrs []netip.Addr) (v4 []netip.Addr, v6 []netip.Addr) { for _, addr := range allAddrs { - if addr.To4() != nil { + if addr.Is4() { v4 = append(v4, addr) } else { v6 = append(v6, addr) @@ -49,9 +49,9 @@ func availableAddresses(allAddrs []net.IP) (v4 []net.IP, v6 []net.IP) { } func (va *ValidationAuthorityImpl) validateDNS01(ctx context.Context, ident identifier.ACMEIdentifier, keyAuthorization string) ([]core.ValidationRecord, error) { - if ident.Type != identifier.DNS { + if ident.Type != identifier.TypeDNS { va.log.Infof("Identifier type for DNS challenge was not DNS: %s", ident) - return nil, berrors.MalformedError("Identifier type for DNS was not itself DNS") + return nil, berrors.MalformedError("Identifier type for DNS challenge was not DNS") } // Compute the digest of the key authorization file diff --git a/third-party/github.com/letsencrypt/boulder/va/dns_test.go b/third-party/github.com/letsencrypt/boulder/va/dns_test.go index a545228a4..ebaa81071 100644 --- a/third-party/github.com/letsencrypt/boulder/va/dns_test.go +++ b/third-party/github.com/letsencrypt/boulder/va/dns_test.go @@ -3,86 +3,76 @@ package va import ( "context" "fmt" - "net" + "net/netip" "testing" "time" "github.com/jmhodges/clock" - "github.com/prometheus/client_golang/prometheus" "github.com/letsencrypt/boulder/bdns" - "github.com/letsencrypt/boulder/core" "github.com/letsencrypt/boulder/identifier" "github.com/letsencrypt/boulder/metrics" "github.com/letsencrypt/boulder/probs" "github.com/letsencrypt/boulder/test" ) -func TestDNSValidationEmpty(t *testing.T) { - va, _ := setup(nil, 0, "", nil, nil) - - // This test calls PerformValidation directly, because that is where the - // metrics checked below are incremented. - req := createValidationRequest("empty-txts.com", core.ChallengeTypeDNS01) - res, _ := va.PerformValidation(context.Background(), req) - test.AssertEquals(t, res.Problems.ProblemType, "unauthorized") - test.AssertEquals(t, res.Problems.Detail, "No TXT record found at _acme-challenge.empty-txts.com") - - test.AssertMetricWithLabelsEquals(t, va.metrics.validationTime, prometheus.Labels{ - "type": "dns-01", - "result": "invalid", - "problem_type": "unauthorized", - }, 1) -} - func TestDNSValidationWrong(t *testing.T) { - va, _ := setup(nil, 0, "", nil, nil) - _, err := va.validateDNS01(context.Background(), dnsi("wrong-dns01.com"), expectedKeyAuthorization) + va, _ := setup(nil, "", nil, nil) + _, err := va.validateDNS01(context.Background(), identifier.NewDNS("wrong-dns01.com"), expectedKeyAuthorization) if err == nil { t.Fatalf("Successful DNS validation with wrong TXT record") } prob := detailedError(err) - test.AssertEquals(t, prob.Error(), "unauthorized :: Incorrect TXT record \"a\" found at _acme-challenge.wrong-dns01.com") + test.AssertEquals(t, prob.String(), "unauthorized :: Incorrect TXT record \"a\" found at _acme-challenge.wrong-dns01.com") } func TestDNSValidationWrongMany(t *testing.T) { - va, _ := setup(nil, 0, "", nil, nil) + va, _ := setup(nil, "", nil, nil) - _, err := va.validateDNS01(context.Background(), dnsi("wrong-many-dns01.com"), expectedKeyAuthorization) + _, err := va.validateDNS01(context.Background(), identifier.NewDNS("wrong-many-dns01.com"), expectedKeyAuthorization) if err == nil { t.Fatalf("Successful DNS validation with wrong TXT record") } prob := detailedError(err) - test.AssertEquals(t, prob.Error(), "unauthorized :: Incorrect TXT record \"a\" (and 4 more) found at _acme-challenge.wrong-many-dns01.com") + test.AssertEquals(t, prob.String(), "unauthorized :: Incorrect TXT record \"a\" (and 4 more) found at _acme-challenge.wrong-many-dns01.com") } func TestDNSValidationWrongLong(t *testing.T) { - va, _ := setup(nil, 0, "", nil, nil) + va, _ := setup(nil, "", nil, nil) - _, err := va.validateDNS01(context.Background(), dnsi("long-dns01.com"), expectedKeyAuthorization) + _, err := va.validateDNS01(context.Background(), identifier.NewDNS("long-dns01.com"), expectedKeyAuthorization) if err == nil { t.Fatalf("Successful DNS validation with wrong TXT record") } prob := detailedError(err) - test.AssertEquals(t, prob.Error(), "unauthorized :: Incorrect TXT record \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...\" found at _acme-challenge.long-dns01.com") + test.AssertEquals(t, prob.String(), "unauthorized :: Incorrect TXT record \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...\" found at _acme-challenge.long-dns01.com") } func TestDNSValidationFailure(t *testing.T) { - va, _ := setup(nil, 0, "", nil, nil) + va, _ := setup(nil, "", nil, nil) - _, err := va.validateDNS01(ctx, dnsi("localhost"), expectedKeyAuthorization) + _, err := va.validateDNS01(ctx, identifier.NewDNS("localhost"), expectedKeyAuthorization) prob := detailedError(err) test.AssertEquals(t, prob.Type, probs.UnauthorizedProblem) } +func TestDNSValidationIP(t *testing.T) { + va, _ := setup(nil, "", nil, nil) + + _, err := va.validateDNS01(ctx, identifier.NewIP(netip.MustParseAddr("127.0.0.1")), expectedKeyAuthorization) + prob := detailedError(err) + + test.AssertEquals(t, prob.Type, probs.MalformedProblem) +} + func TestDNSValidationInvalid(t *testing.T) { var notDNS = identifier.ACMEIdentifier{ Type: identifier.IdentifierType("iris"), Value: "790DB180-A274-47A4-855F-31C428CB1072", } - va, _ := setup(nil, 0, "", nil, nil) + va, _ := setup(nil, "", nil, nil) _, err := va.validateDNS01(ctx, notDNS, expectedKeyAuthorization) prob := detailedError(err) @@ -91,95 +81,96 @@ func TestDNSValidationInvalid(t *testing.T) { } func TestDNSValidationServFail(t *testing.T) { - va, _ := setup(nil, 0, "", nil, nil) + va, _ := setup(nil, "", nil, nil) - _, err := va.validateDNS01(ctx, dnsi("servfail.com"), expectedKeyAuthorization) + _, err := va.validateDNS01(ctx, identifier.NewDNS("servfail.com"), expectedKeyAuthorization) prob := detailedError(err) test.AssertEquals(t, prob.Type, probs.DNSProblem) } func TestDNSValidationNoServer(t *testing.T) { - va, log := setup(nil, 0, "", nil, nil) + va, log := setup(nil, "", nil, nil) staticProvider, err := bdns.NewStaticProvider([]string{}) test.AssertNotError(t, err, "Couldn't make new static provider") - va.dnsClient = bdns.NewTest( + va.dnsClient = bdns.New( time.Second*5, staticProvider, metrics.NoopRegisterer, clock.New(), 1, + "", log, nil) - _, err = va.validateDNS01(ctx, dnsi("localhost"), expectedKeyAuthorization) + _, err = va.validateDNS01(ctx, identifier.NewDNS("localhost"), expectedKeyAuthorization) prob := detailedError(err) test.AssertEquals(t, prob.Type, probs.DNSProblem) } func TestDNSValidationOK(t *testing.T) { - va, _ := setup(nil, 0, "", nil, nil) + va, _ := setup(nil, "", nil, nil) - _, prob := va.validateDNS01(ctx, dnsi("good-dns01.com"), expectedKeyAuthorization) + _, prob := va.validateDNS01(ctx, identifier.NewDNS("good-dns01.com"), expectedKeyAuthorization) test.Assert(t, prob == nil, "Should be valid.") } func TestDNSValidationNoAuthorityOK(t *testing.T) { - va, _ := setup(nil, 0, "", nil, nil) + va, _ := setup(nil, "", nil, nil) - _, prob := va.validateDNS01(ctx, dnsi("no-authority-dns01.com"), expectedKeyAuthorization) + _, prob := va.validateDNS01(ctx, identifier.NewDNS("no-authority-dns01.com"), expectedKeyAuthorization) test.Assert(t, prob == nil, "Should be valid.") } func TestAvailableAddresses(t *testing.T) { - v6a := net.ParseIP("::1") - v6b := net.ParseIP("2001:db8::2:1") // 2001:DB8 is reserved for docs (RFC 3849) - v4a := net.ParseIP("127.0.0.1") - v4b := net.ParseIP("192.0.2.1") // 192.0.2.0/24 is reserved for docs (RFC 5737) + v6a := netip.MustParseAddr("::1") + v6b := netip.MustParseAddr("2001:db8::2:1") // 2001:DB8 is reserved for docs (RFC 3849) + v4a := netip.MustParseAddr("127.0.0.1") + v4b := netip.MustParseAddr("192.0.2.1") // 192.0.2.0/24 is reserved for docs (RFC 5737) testcases := []struct { - input []net.IP - v4 []net.IP - v6 []net.IP + input []netip.Addr + v4 []netip.Addr + v6 []netip.Addr }{ // An empty validation record { - []net.IP{}, - []net.IP{}, - []net.IP{}, + []netip.Addr{}, + []netip.Addr{}, + []netip.Addr{}, }, // A validation record with one IPv4 address { - []net.IP{v4a}, - []net.IP{v4a}, - []net.IP{}, + []netip.Addr{v4a}, + []netip.Addr{v4a}, + []netip.Addr{}, }, // A dual homed record with an IPv4 and IPv6 address { - []net.IP{v4a, v6a}, - []net.IP{v4a}, - []net.IP{v6a}, + []netip.Addr{v4a, v6a}, + []netip.Addr{v4a}, + []netip.Addr{v6a}, }, // The same as above but with the v4/v6 order flipped { - []net.IP{v6a, v4a}, - []net.IP{v4a}, - []net.IP{v6a}, + []netip.Addr{v6a, v4a}, + []netip.Addr{v4a}, + []netip.Addr{v6a}, }, // A validation record with just IPv6 addresses { - []net.IP{v6a, v6b}, - []net.IP{}, - []net.IP{v6a, v6b}, + []netip.Addr{v6a, v6b}, + []netip.Addr{}, + []netip.Addr{v6a, v6b}, }, // A validation record with interleaved IPv4/IPv6 records { - []net.IP{v6a, v4a, v6b, v4b}, - []net.IP{v4a, v4b}, - []net.IP{v6a, v6b}, + []netip.Addr{v6a, v4a, v6b, v4b}, + []netip.Addr{v4a, v4b}, + []netip.Addr{v6a, v6b}, }, } diff --git a/third-party/github.com/letsencrypt/boulder/va/http.go b/third-party/github.com/letsencrypt/boulder/va/http.go index 5702e66bd..d0623fae0 100644 --- a/third-party/github.com/letsencrypt/boulder/va/http.go +++ b/third-party/github.com/letsencrypt/boulder/va/http.go @@ -8,6 +8,7 @@ import ( "io" "net" "net/http" + "net/netip" "net/url" "strconv" "strings" @@ -41,7 +42,7 @@ const ( // The hostname of the preresolvedDialer is used to ensure the dial only completes // using the pre-resolved IP/port when used for the correct host. type preresolvedDialer struct { - ip net.IP + ip netip.Addr port int hostname string timeout time.Duration @@ -159,7 +160,7 @@ func httpTransport(df dialerFunc) *http.Transport { // httpValidationTarget bundles all of the information needed to make an HTTP-01 // validation request against a target. type httpValidationTarget struct { - // the hostname being validated + // the host being validated host string // the port for the validation request port int @@ -169,14 +170,14 @@ type httpValidationTarget struct { // following redirects) query string // all of the IP addresses available for the host - available []net.IP + available []netip.Addr // the IP addresses that were tried for validation previously that were cycled // out of cur by calls to nextIP() - tried []net.IP + tried []netip.Addr // the IP addresses that will be drawn from by calls to nextIP() to set curIP - next []net.IP + next []netip.Addr // the current IP address being used for validation (if any) - cur net.IP + cur netip.Addr // the DNS resolver(s) that will attempt to fulfill the validation request resolvers bdns.ResolverAddrs } @@ -203,18 +204,32 @@ func (vt *httpValidationTarget) nextIP() error { // lookups fail. func (va *ValidationAuthorityImpl) newHTTPValidationTarget( ctx context.Context, - host string, + ident identifier.ACMEIdentifier, port int, path string, query string) (*httpValidationTarget, error) { - // Resolve IP addresses for the hostname - addrs, resolvers, err := va.getAddrs(ctx, host) - if err != nil { - return nil, err + var addrs []netip.Addr + var resolvers bdns.ResolverAddrs + switch ident.Type { + case identifier.TypeDNS: + // Resolve IP addresses for the identifier + dnsAddrs, dnsResolvers, err := va.getAddrs(ctx, ident.Value) + if err != nil { + return nil, err + } + addrs, resolvers = dnsAddrs, dnsResolvers + case identifier.TypeIP: + netIP, err := netip.ParseAddr(ident.Value) + if err != nil { + return nil, fmt.Errorf("can't parse IP address %q: %s", ident.Value, err) + } + addrs = []netip.Addr{netIP} + default: + return nil, fmt.Errorf("unknown identifier type: %s", ident.Type) } target := &httpValidationTarget{ - host: host, + host: ident.Value, port: port, path: path, query: query, @@ -230,19 +245,19 @@ func (va *ValidationAuthorityImpl) newHTTPValidationTarget( if !hasV6Addrs && !hasV4Addrs { // If there are no v6 addrs and no v4addrs there was a bug with getAddrs or // availableAddresses and we need to return an error. - return nil, fmt.Errorf("host %q has no IPv4 or IPv6 addresses", host) + return nil, fmt.Errorf("host %q has no IPv4 or IPv6 addresses", ident.Value) } else if !hasV6Addrs && hasV4Addrs { // If there are no v6 addrs and there are v4 addrs then use the first v4 // address. There's no fallback address. - target.next = []net.IP{v4Addrs[0]} + target.next = []netip.Addr{v4Addrs[0]} } else if hasV6Addrs && hasV4Addrs { // If there are both v6 addrs and v4 addrs then use the first v6 address and // fallback with the first v4 address. - target.next = []net.IP{v6Addrs[0], v4Addrs[0]} + target.next = []netip.Addr{v6Addrs[0], v4Addrs[0]} } else if hasV6Addrs && !hasV4Addrs { // If there are just v6 addrs then use the first v6 address. There's no // fallback address. - target.next = []net.IP{v6Addrs[0]} + target.next = []netip.Addr{v6Addrs[0]} } // Advance the target using nextIP to populate the cur IP before returning @@ -250,45 +265,47 @@ func (va *ValidationAuthorityImpl) newHTTPValidationTarget( return target, nil } -// extractRequestTarget extracts the hostname and port specified in the provided +// extractRequestTarget extracts the host and port specified in the provided // HTTP redirect request. If the request's URL's protocol schema is not HTTP or // HTTPS an error is returned. If an explicit port is specified in the request's -// URL and it isn't the VA's HTTP or HTTPS port, an error is returned. If the -// request's URL's Host is a bare IPv4 or IPv6 address and not a domain name an -// error is returned. -func (va *ValidationAuthorityImpl) extractRequestTarget(req *http.Request) (string, int, error) { +// URL and it isn't the VA's HTTP or HTTPS port, an error is returned. +func (va *ValidationAuthorityImpl) extractRequestTarget(req *http.Request) (identifier.ACMEIdentifier, int, error) { // A nil request is certainly not a valid redirect and has no port to extract. if req == nil { - return "", 0, fmt.Errorf("redirect HTTP request was nil") + return identifier.ACMEIdentifier{}, 0, fmt.Errorf("redirect HTTP request was nil") } reqScheme := req.URL.Scheme // The redirect request must use HTTP or HTTPs protocol schemes regardless of the port.. if reqScheme != "http" && reqScheme != "https" { - return "", 0, berrors.ConnectionFailureError( + return identifier.ACMEIdentifier{}, 0, berrors.ConnectionFailureError( "Invalid protocol scheme in redirect target. "+ `Only "http" and "https" protocol schemes are supported, not %q`, reqScheme) } - // Try and split an explicit port number from the request URL host. If there is - // one we need to make sure its a valid port. If there isn't one we need to - // pick the port based on the reqScheme default port. - reqHost := req.URL.Host + // Try to parse an explicit port number from the request URL host. If there + // is one, we need to make sure its a valid port. If there isn't one we need + // to pick the port based on the reqScheme default port. + reqHost := req.URL.Hostname() var reqPort int - if h, p, err := net.SplitHostPort(reqHost); err == nil { - reqHost = h - reqPort, err = strconv.Atoi(p) + // URL.Port() will return "" for an invalid port, not just an empty port. To + // reject invalid ports, we rely on the calling function having used + // URL.Parse(), which does enforce validity. + if req.URL.Port() != "" { + parsedPort, err := strconv.Atoi(req.URL.Port()) if err != nil { - return "", 0, err + return identifier.ACMEIdentifier{}, 0, err } // The explicit port must match the VA's configured HTTP or HTTPS port. - if reqPort != va.httpPort && reqPort != va.httpsPort { - return "", 0, berrors.ConnectionFailureError( + if parsedPort != va.httpPort && parsedPort != va.httpsPort { + return identifier.ACMEIdentifier{}, 0, berrors.ConnectionFailureError( "Invalid port in redirect target. Only ports %d and %d are supported, not %d", - va.httpPort, va.httpsPort, reqPort) + va.httpPort, va.httpsPort, parsedPort) } + + reqPort = parsedPort } else if reqScheme == "http" { reqPort = va.httpPort } else if reqScheme == "https" { @@ -296,17 +313,11 @@ func (va *ValidationAuthorityImpl) extractRequestTarget(req *http.Request) (stri } else { // This shouldn't happen but defensively return an internal server error in // case it does. - return "", 0, fmt.Errorf("unable to determine redirect HTTP request port") + return identifier.ACMEIdentifier{}, 0, fmt.Errorf("unable to determine redirect HTTP request port") } if reqHost == "" { - return "", 0, berrors.ConnectionFailureError("Invalid empty hostname in redirect target") - } - - // Check that the request host isn't a bare IP address. We only follow - // redirects to hostnames. - if net.ParseIP(reqHost) != nil { - return "", 0, berrors.ConnectionFailureError("Invalid host in redirect target %q. Only domain names are supported, not IP addresses", reqHost) + return identifier.ACMEIdentifier{}, 0, berrors.ConnectionFailureError("Invalid empty host in redirect target") } // Often folks will misconfigure their webserver to send an HTTP redirect @@ -319,17 +330,26 @@ func (va *ValidationAuthorityImpl) extractRequestTarget(req *http.Request) (stri // This happens frequently enough we want to return a distinct error message // for this case by detecting the reqHost ending in ".well-known". if strings.HasSuffix(reqHost, ".well-known") { - return "", 0, berrors.ConnectionFailureError( + return identifier.ACMEIdentifier{}, 0, berrors.ConnectionFailureError( "Invalid host in redirect target %q. Check webserver config for missing '/' in redirect target.", reqHost, ) } - if _, err := iana.ExtractSuffix(reqHost); err != nil { - return "", 0, berrors.ConnectionFailureError("Invalid hostname in redirect target, must end in IANA registered TLD") + reqIP, err := netip.ParseAddr(reqHost) + if err == nil { + err := va.isReservedIPFunc(reqIP) + if err != nil { + return identifier.ACMEIdentifier{}, 0, berrors.ConnectionFailureError("Invalid host in redirect target: %s", err) + } + return identifier.NewIP(reqIP), reqPort, nil } - return reqHost, reqPort, nil + if _, err := iana.ExtractSuffix(reqHost); err != nil { + return identifier.ACMEIdentifier{}, 0, berrors.ConnectionFailureError("Invalid host in redirect target, must end in IANA registered TLD") + } + + return identifier.NewDNS(reqHost), reqPort, nil } // setupHTTPValidation sets up a preresolvedDialer and a validation record for @@ -364,13 +384,21 @@ func (va *ValidationAuthorityImpl) setupHTTPValidation( // Get the target IP to build a preresolved dialer with targetIP := target.cur - if targetIP == nil { + if (targetIP == netip.Addr{}) { return nil, record, fmt.Errorf( "host %q has no IP addresses remaining to use", target.host) } + + // This is a backstop check to avoid connecting to reserved IP addresses. + // They should have been caught and excluded by `bdns.LookupHost`. + err := va.isReservedIPFunc(targetIP) + if err != nil { + return nil, record, err + } + record.AddressUsed = targetIP dialer := &preresolvedDialer{ @@ -382,20 +410,6 @@ func (va *ValidationAuthorityImpl) setupHTTPValidation( return dialer, record, nil } -// fetchHTTP invokes processHTTPValidation and if an error result is -// returned, converts it to a problem. Otherwise the results from -// processHTTPValidation are returned. -func (va *ValidationAuthorityImpl) fetchHTTP( - ctx context.Context, - host string, - path string) ([]byte, []core.ValidationRecord, error) { - body, records, err := va.processHTTPValidation(ctx, host, path) - if err != nil { - return body, records, err - } - return body, records, nil -} - // fallbackErr returns true only for net.OpError instances where the op is equal // to "dial", or url.Error instances wrapping such an error. fallbackErr returns // false for all other errors. By policy, only dial errors (not read or write @@ -417,14 +431,27 @@ func fallbackErr(err error) bool { // a non-nil error and potentially some ValidationRecords are returned. func (va *ValidationAuthorityImpl) processHTTPValidation( ctx context.Context, - host string, + ident identifier.ACMEIdentifier, path string) ([]byte, []core.ValidationRecord, error) { // Create a target for the host, port and path with no query parameters - target, err := va.newHTTPValidationTarget(ctx, host, va.httpPort, path, "") + target, err := va.newHTTPValidationTarget(ctx, ident, va.httpPort, path, "") if err != nil { return nil, nil, err } + // When constructing a URL, bare IPv6 addresses must be enclosed in square + // brackets. Otherwise, a colon may be interpreted as a port separator. + host := ident.Value + if ident.Type == identifier.TypeIP { + netipHost, err := netip.ParseAddr(host) + if err != nil { + return nil, nil, fmt.Errorf("couldn't parse IP address from identifier") + } + if !netipHost.Is4() { + host = "[" + host + "]" + } + } + // Create an initial GET Request initialURL := url.URL{ Scheme: "http", @@ -494,13 +521,6 @@ func (va *ValidationAuthorityImpl) processHTTPValidation( numRedirects++ va.metrics.http01Redirects.Inc() - // If TLS was used, record the negotiated key exchange mechanism in the most - // recent validationRecord. - // TODO(#7321): Remove this when we have collected enough data. - if req.Response.TLS != nil { - records[len(records)-1].UsedRSAKEX = usedRSAKEX(req.Response.TLS.CipherSuite) - } - if req.Response.TLS != nil && req.Response.TLS.Version < tls.VersionTLS12 { return berrors.ConnectionFailureError( "validation attempt was redirected to an HTTPS server that doesn't " + @@ -613,7 +633,7 @@ func (va *ValidationAuthorityImpl) processHTTPValidation( // If the retry still failed there isn't anything more to do, return the // error immediately. if err != nil { - return nil, records, newIPError(retryRecord.AddressUsed, err) + return nil, records, newIPError(records[len(records)-1].AddressUsed, err) } } else if err != nil { // if the error was not a fallbackErr then return immediately. @@ -643,25 +663,18 @@ func (va *ValidationAuthorityImpl) processHTTPValidation( records[len(records)-1].URL, body)) } - // We were successful, so record the negotiated key exchange mechanism in the - // last validationRecord. - // TODO(#7321): Remove this when we have collected enough data. - if httpResponse.TLS != nil { - records[len(records)-1].UsedRSAKEX = usedRSAKEX(httpResponse.TLS.CipherSuite) - } - return body, records, nil } func (va *ValidationAuthorityImpl) validateHTTP01(ctx context.Context, ident identifier.ACMEIdentifier, token string, keyAuthorization string) ([]core.ValidationRecord, error) { - if ident.Type != identifier.DNS { - va.log.Infof("Got non-DNS identifier for HTTP validation: %s", ident) - return nil, berrors.MalformedError("Identifier type for HTTP validation was not DNS") + if ident.Type != identifier.TypeDNS && ident.Type != identifier.TypeIP { + va.log.Info(fmt.Sprintf("Identifier type for HTTP-01 challenge was not DNS or IP: %s", ident)) + return nil, berrors.MalformedError("Identifier type for HTTP-01 challenge was not DNS or IP") } // Perform the fetch path := fmt.Sprintf(".well-known/acme-challenge/%s", token) - body, validationRecords, err := va.fetchHTTP(ctx, ident.Value, "/"+path) + body, validationRecords, err := va.processHTTPValidation(ctx, ident, "/"+path) if err != nil { return validationRecords, err } diff --git a/third-party/github.com/letsencrypt/boulder/va/http_test.go b/third-party/github.com/letsencrypt/boulder/va/http_test.go index 038803539..70b5fc155 100644 --- a/third-party/github.com/letsencrypt/boulder/va/http_test.go +++ b/third-party/github.com/letsencrypt/boulder/va/http_test.go @@ -6,10 +6,11 @@ import ( "encoding/base64" "errors" "fmt" - mrand "math/rand" + mrand "math/rand/v2" "net" "net/http" "net/http/httptest" + "net/netip" "net/url" "regexp" "strconv" @@ -34,7 +35,7 @@ import ( // a dial to another host produces the expected dialerMismatchError. func TestDialerMismatchError(t *testing.T) { d := preresolvedDialer{ - ip: net.ParseIP("127.0.0.1"), + ip: netip.MustParseAddr("127.0.0.1"), port: 1337, hostname: "letsencrypt.org", } @@ -53,11 +54,24 @@ func TestDialerMismatchError(t *testing.T) { test.AssertEquals(t, err.Error(), expectedErr.Error()) } -// TestPreresolvedDialerTimeout tests that the preresolvedDialer's DialContext +// dnsMockReturnsUnroutable is a DNSClient mock that always returns an +// unroutable address for LookupHost. This is useful in testing connect +// timeouts. +type dnsMockReturnsUnroutable struct { + *bdns.MockClient +} + +func (mock dnsMockReturnsUnroutable) LookupHost(_ context.Context, hostname string) ([]netip.Addr, bdns.ResolverAddrs, error) { + return []netip.Addr{netip.MustParseAddr("64.112.117.254")}, bdns.ResolverAddrs{"dnsMockReturnsUnroutable"}, nil +} + +// TestDialerTimeout tests that the preresolvedDialer's DialContext // will timeout after the expected singleDialTimeout. This ensures timeouts at -// the TCP level are handled correctly. -func TestPreresolvedDialerTimeout(t *testing.T) { - va, _ := setup(nil, 0, "", nil, nil) +// the TCP level are handled correctly. It also ensures that we show the client +// the appropriate "Timeout during connect" error message, which helps clients +// distinguish between firewall problems and server problems. +func TestDialerTimeout(t *testing.T) { + va, _ := setup(nil, "", nil, nil) // Timeouts below 50ms tend to be flaky. va.singleDialTimeout = 50 * time.Millisecond @@ -75,9 +89,9 @@ func TestPreresolvedDialerTimeout(t *testing.T) { var took time.Duration for range 20 { started := time.Now() - _, _, err = va.fetchHTTP(ctx, "unroutable.invalid", "/.well-known/acme-challenge/whatever") + _, _, err = va.processHTTPValidation(ctx, identifier.NewDNS("unroutable.invalid"), "/.well-known/acme-challenge/whatever") took = time.Since(started) - if err != nil && strings.Contains(err.Error(), "Network unreachable") { + if err != nil && strings.Contains(err.Error(), "network is unreachable") { continue } else { break @@ -97,13 +111,7 @@ func TestPreresolvedDialerTimeout(t *testing.T) { } prob := detailedError(err) test.AssertEquals(t, prob.Type, probs.ConnectionProblem) - - expectMatch := regexp.MustCompile( - "Fetching http://unroutable.invalid/.well-known/acme-challenge/.*: Timeout during connect") - if !expectMatch.MatchString(prob.Detail) { - t.Errorf("Problem details incorrect. Got %q, expected to match %q", - prob.Detail, expectMatch) - } + test.AssertContains(t, prob.Detail, "Timeout during connect (likely firewall problem)") } func TestHTTPTransport(t *testing.T) { @@ -126,31 +134,41 @@ func TestHTTPValidationTarget(t *testing.T) { // hostnames used in this test. testCases := []struct { Name string - Host string + Ident identifier.ACMEIdentifier ExpectedError error ExpectedIPs []string }{ { - Name: "No IPs for host", - Host: "always.invalid", + Name: "No IPs for DNS identifier", + Ident: identifier.NewDNS("always.invalid"), ExpectedError: berrors.DNSError("No valid IP addresses found for always.invalid"), }, { - Name: "Only IPv4 addrs for host", - Host: "some.example.com", + Name: "Only IPv4 addrs for DNS identifier", + Ident: identifier.NewDNS("some.example.com"), ExpectedIPs: []string{"127.0.0.1"}, }, { - Name: "Only IPv6 addrs for host", - Host: "ipv6.localhost", + Name: "Only IPv6 addrs for DNS identifier", + Ident: identifier.NewDNS("ipv6.localhost"), ExpectedIPs: []string{"::1"}, }, { - Name: "Both IPv6 and IPv4 addrs for host", - Host: "ipv4.and.ipv6.localhost", + Name: "Both IPv6 and IPv4 addrs for DNS identifier", + Ident: identifier.NewDNS("ipv4.and.ipv6.localhost"), // In this case we expect 1 IPv6 address first, and then 1 IPv4 address ExpectedIPs: []string{"::1", "127.0.0.1"}, }, + { + Name: "IPv4 IP address identifier", + Ident: identifier.NewIP(netip.MustParseAddr("127.0.0.1")), + ExpectedIPs: []string{"127.0.0.1"}, + }, + { + Name: "IPv6 IP address identifier", + Ident: identifier.NewIP(netip.MustParseAddr("::1")), + ExpectedIPs: []string{"::1"}, + }, } const ( @@ -159,12 +177,12 @@ func TestHTTPValidationTarget(t *testing.T) { exampleQuery = "my-path=was&my=own" ) - va, _ := setup(nil, 0, "", nil, nil) + va, _ := setup(nil, "", nil, nil) for _, tc := range testCases { t.Run(tc.Name, func(t *testing.T) { target, err := va.newHTTPValidationTarget( context.Background(), - tc.Host, + tc.Ident, examplePort, examplePath, exampleQuery) @@ -181,7 +199,7 @@ func TestHTTPValidationTarget(t *testing.T) { // order. for i, expectedIP := range tc.ExpectedIPs { gotIP := target.cur - if gotIP == nil { + if (gotIP == netip.Addr{}) { t.Errorf("Expected IP %d to be %s got nil", i, expectedIP) } else { test.AssertEquals(t, gotIP.String(), expectedIP) @@ -203,7 +221,7 @@ func TestExtractRequestTarget(t *testing.T) { Name string Req *http.Request ExpectedError error - ExpectedHost string + ExpectedIdent identifier.ACMEIdentifier ExpectedPort int }{ { @@ -228,11 +246,11 @@ func TestExtractRequestTarget(t *testing.T) { "and 443 are supported, not 9999"), }, { - Name: "invalid empty hostname", + Name: "invalid empty host", Req: &http.Request{ URL: mustURL("https:///who/needs/a/hostname?not=me"), }, - ExpectedError: errors.New("Invalid empty hostname in redirect target"), + ExpectedError: errors.New("Invalid empty host in redirect target"), }, { Name: "invalid .well-known hostname", @@ -246,51 +264,137 @@ func TestExtractRequestTarget(t *testing.T) { Req: &http.Request{ URL: mustURL("https://my.tld.is.cpu/pretty/cool/right?yeah=Ithoughtsotoo"), }, - ExpectedError: errors.New("Invalid hostname in redirect target, must end in IANA registered TLD"), + ExpectedError: errors.New("Invalid host in redirect target, must end in IANA registered TLD"), }, { - Name: "bare IP", + Name: "malformed wildcard-ish IPv4 address", Req: &http.Request{ - URL: mustURL("https://10.10.10.10"), + URL: mustURL("https://10.10.10.*"), }, - ExpectedError: fmt.Errorf(`Invalid host in redirect target "10.10.10.10". ` + - "Only domain names are supported, not IP addresses"), + ExpectedError: errors.New("Invalid host in redirect target, must end in IANA registered TLD"), + }, + { + Name: "malformed too-long IPv6 address", + Req: &http.Request{ + URL: mustURL("https://[a:b:c:d:e:f:b:a:d]"), + }, + ExpectedError: errors.New("Invalid host in redirect target, must end in IANA registered TLD"), + }, + { + Name: "bare IPv4, implicit port", + Req: &http.Request{ + URL: mustURL("http://127.0.0.1"), + }, + ExpectedIdent: identifier.NewIP(netip.MustParseAddr("127.0.0.1")), + ExpectedPort: 80, + }, + { + Name: "bare IPv4, explicit valid port", + Req: &http.Request{ + URL: mustURL("http://127.0.0.1:80"), + }, + ExpectedIdent: identifier.NewIP(netip.MustParseAddr("127.0.0.1")), + ExpectedPort: 80, + }, + { + Name: "bare IPv4, explicit invalid port", + Req: &http.Request{ + URL: mustURL("http://127.0.0.1:9999"), + }, + ExpectedError: fmt.Errorf("Invalid port in redirect target. Only ports 80 " + + "and 443 are supported, not 9999"), + }, + { + Name: "bare IPv4, HTTPS", + Req: &http.Request{ + URL: mustURL("https://127.0.0.1"), + }, + ExpectedIdent: identifier.NewIP(netip.MustParseAddr("127.0.0.1")), + ExpectedPort: 443, + }, + { + Name: "bare IPv4, reserved IP address", + Req: &http.Request{ + URL: mustURL("http://10.10.10.10"), + }, + ExpectedError: fmt.Errorf("Invalid host in redirect target: " + + "IP address is in a reserved address block: [RFC1918]: Private-Use"), + }, + { + Name: "bare IPv6, implicit port", + Req: &http.Request{ + URL: mustURL("http://[::1]"), + }, + ExpectedIdent: identifier.NewIP(netip.MustParseAddr("::1")), + ExpectedPort: 80, + }, + { + Name: "bare IPv6, explicit valid port", + Req: &http.Request{ + URL: mustURL("http://[::1]:80"), + }, + ExpectedIdent: identifier.NewIP(netip.MustParseAddr("::1")), + ExpectedPort: 80, + }, + { + Name: "bare IPv6, explicit invalid port", + Req: &http.Request{ + URL: mustURL("http://[::1]:9999"), + }, + ExpectedError: fmt.Errorf("Invalid port in redirect target. Only ports 80 " + + "and 443 are supported, not 9999"), + }, + { + Name: "bare IPv6, HTTPS", + Req: &http.Request{ + URL: mustURL("https://[::1]"), + }, + ExpectedIdent: identifier.NewIP(netip.MustParseAddr("::1")), + ExpectedPort: 443, + }, + { + Name: "bare IPv6, reserved IP address", + Req: &http.Request{ + URL: mustURL("http://[3fff:aaa:aaaa:aaaa:abad:0ff1:cec0:ffee]"), + }, + ExpectedError: fmt.Errorf("Invalid host in redirect target: " + + "IP address is in a reserved address block: [RFC9637]: Documentation"), }, { Name: "valid HTTP redirect, explicit port", Req: &http.Request{ URL: mustURL("http://cpu.letsencrypt.org:80"), }, - ExpectedHost: "cpu.letsencrypt.org", - ExpectedPort: 80, + ExpectedIdent: identifier.NewDNS("cpu.letsencrypt.org"), + ExpectedPort: 80, }, { Name: "valid HTTP redirect, implicit port", Req: &http.Request{ URL: mustURL("http://cpu.letsencrypt.org"), }, - ExpectedHost: "cpu.letsencrypt.org", - ExpectedPort: 80, + ExpectedIdent: identifier.NewDNS("cpu.letsencrypt.org"), + ExpectedPort: 80, }, { Name: "valid HTTPS redirect, explicit port", Req: &http.Request{ URL: mustURL("https://cpu.letsencrypt.org:443/hello.world"), }, - ExpectedHost: "cpu.letsencrypt.org", - ExpectedPort: 443, + ExpectedIdent: identifier.NewDNS("cpu.letsencrypt.org"), + ExpectedPort: 443, }, { Name: "valid HTTPS redirect, implicit port", Req: &http.Request{ URL: mustURL("https://cpu.letsencrypt.org/hello.world"), }, - ExpectedHost: "cpu.letsencrypt.org", - ExpectedPort: 443, + ExpectedIdent: identifier.NewDNS("cpu.letsencrypt.org"), + ExpectedPort: 443, }, } - va, _ := setup(nil, 0, "", nil, nil) + va, _ := setup(nil, "", nil, nil) for _, tc := range testCases { t.Run(tc.Name, func(t *testing.T) { host, port, err := va.extractRequestTarget(tc.Req) @@ -301,7 +405,7 @@ func TestExtractRequestTarget(t *testing.T) { } else if err == nil && tc.ExpectedError != nil { t.Errorf("Expected err %v, got nil", tc.ExpectedError) } else { - test.AssertEquals(t, host, tc.ExpectedHost) + test.AssertEquals(t, host, tc.ExpectedIdent) test.AssertEquals(t, port, tc.ExpectedPort) } }) @@ -312,9 +416,9 @@ func TestExtractRequestTarget(t *testing.T) { // generates a DNS error, and checks that a log line with the detailed error is // generated. func TestHTTPValidationDNSError(t *testing.T) { - va, mockLog := setup(nil, 0, "", nil, nil) + va, mockLog := setup(nil, "", nil, nil) - _, _, prob := va.fetchHTTP(ctx, "always.error", "/.well-known/acme-challenge/whatever") + _, _, prob := va.processHTTPValidation(ctx, identifier.NewDNS("always.error"), "/.well-known/acme-challenge/whatever") test.AssertError(t, prob, "Expected validation fetch to fail") matchingLines := mockLog.GetAllMatching(`read udp: some net error`) if len(matchingLines) != 1 { @@ -328,9 +432,9 @@ func TestHTTPValidationDNSError(t *testing.T) { // the mock resolver results in valid query/response data being logged in // a format we can decode successfully. func TestHTTPValidationDNSIdMismatchError(t *testing.T) { - va, mockLog := setup(nil, 0, "", nil, nil) + va, mockLog := setup(nil, "", nil, nil) - _, _, prob := va.fetchHTTP(ctx, "id.mismatch", "/.well-known/acme-challenge/whatever") + _, _, prob := va.processHTTPValidation(ctx, identifier.NewDNS("id.mismatch"), "/.well-known/acme-challenge/whatever") test.AssertError(t, prob, "Expected validation fetch to fail") matchingLines := mockLog.GetAllMatching(`logDNSError ID mismatch`) if len(matchingLines) != 1 { @@ -367,12 +471,12 @@ func TestHTTPValidationDNSIdMismatchError(t *testing.T) { } func TestSetupHTTPValidation(t *testing.T) { - va, _ := setup(nil, 0, "", nil, nil) + va, _ := setup(nil, "", nil, nil) mustTarget := func(t *testing.T, host string, port int, path string) *httpValidationTarget { target, err := va.newHTTPValidationTarget( context.Background(), - host, + identifier.NewDNS(host), port, path, "") @@ -427,12 +531,12 @@ func TestSetupHTTPValidation(t *testing.T) { Hostname: "ipv4.and.ipv6.localhost", Port: strconv.Itoa(va.httpPort), URL: "http://ipv4.and.ipv6.localhost/yellow/brick/road", - AddressesResolved: []net.IP{net.ParseIP("::1"), net.ParseIP("127.0.0.1")}, - AddressUsed: net.ParseIP("::1"), + AddressesResolved: []netip.Addr{netip.MustParseAddr("::1"), netip.MustParseAddr("127.0.0.1")}, + AddressUsed: netip.MustParseAddr("::1"), ResolverAddrs: []string{"MockClient"}, }, ExpectedDialer: &preresolvedDialer{ - ip: net.ParseIP("::1"), + ip: netip.MustParseAddr("::1"), port: va.httpPort, timeout: va.singleDialTimeout, }, @@ -445,12 +549,12 @@ func TestSetupHTTPValidation(t *testing.T) { Hostname: "ipv4.and.ipv6.localhost", Port: strconv.Itoa(va.httpsPort), URL: "https://ipv4.and.ipv6.localhost/yellow/brick/road", - AddressesResolved: []net.IP{net.ParseIP("::1"), net.ParseIP("127.0.0.1")}, - AddressUsed: net.ParseIP("::1"), + AddressesResolved: []netip.Addr{netip.MustParseAddr("::1"), netip.MustParseAddr("127.0.0.1")}, + AddressUsed: netip.MustParseAddr("::1"), ResolverAddrs: []string{"MockClient"}, }, ExpectedDialer: &preresolvedDialer{ - ip: net.ParseIP("::1"), + ip: netip.MustParseAddr("::1"), port: va.httpsPort, timeout: va.singleDialTimeout, }, @@ -479,11 +583,19 @@ func TestSetupHTTPValidation(t *testing.T) { } // A more concise version of httpSrv() that supports http.go tests -func httpTestSrv(t *testing.T) *httptest.Server { +func httpTestSrv(t *testing.T, ipv6 bool) *httptest.Server { t.Helper() mux := http.NewServeMux() server := httptest.NewUnstartedServer(mux) + if ipv6 { + l, err := net.Listen("tcp", "[::1]:0") + if err != nil { + panic(fmt.Sprintf("httptest: failed to listen on a port: %v", err)) + } + server.Listener = l + } + server.Start() httpPort := getPort(server) @@ -548,11 +660,20 @@ func httpTestSrv(t *testing.T) *httptest.Server { }) // A path that always redirects to a URL with a bare IP address - mux.HandleFunc("/redir-bad-host", func(resp http.ResponseWriter, req *http.Request) { + mux.HandleFunc("/redir-bare-ipv4", func(resp http.ResponseWriter, req *http.Request) { http.Redirect( resp, req, - "https://127.0.0.1", + "http://127.0.0.1/ok", + http.StatusMovedPermanently, + ) + }) + + mux.HandleFunc("/redir-bare-ipv6", func(resp http.ResponseWriter, req *http.Request) { + http.Redirect( + resp, + req, + "http://[::1]/ok", http.StatusMovedPermanently, ) }) @@ -731,16 +852,20 @@ func TestFallbackErr(t *testing.T) { } func TestFetchHTTP(t *testing.T) { - // Create a test server - testSrv := httpTestSrv(t) - defer testSrv.Close() + // Create test servers + testSrvIPv4 := httpTestSrv(t, false) + defer testSrvIPv4.Close() + testSrvIPv6 := httpTestSrv(t, true) + defer testSrvIPv6.Close() - // Setup a VA. By providing the testSrv to setup the VA will use the testSrv's + // Setup VAs. By providing the testSrv to setup the VA will use the testSrv's // randomly assigned port as its HTTP port. - va, _ := setup(testSrv, 0, "", nil, nil) + vaIPv4, _ := setup(testSrvIPv4, "", nil, nil) + vaIPv6, _ := setup(testSrvIPv6, "", nil, nil) // We need to know the randomly assigned HTTP port for testcases as well - httpPort := getPort(testSrv) + httpPortIPv4 := getPort(testSrvIPv4) + httpPortIPv6 := getPort(testSrvIPv6) // For the looped test case we expect one validation record per redirect // until boulder detects that a url has been used twice indicating a @@ -754,15 +879,15 @@ func TestFetchHTTP(t *testing.T) { // The first request will not have a port # in the URL. url := "http://example.com/loop" if i != 0 { - url = fmt.Sprintf("http://example.com:%d/loop", httpPort) + url = fmt.Sprintf("http://example.com:%d/loop", httpPortIPv4) } expectedLoopRecords = append(expectedLoopRecords, core.ValidationRecord{ Hostname: "example.com", - Port: strconv.Itoa(httpPort), + Port: strconv.Itoa(httpPortIPv4), URL: url, - AddressesResolved: []net.IP{net.ParseIP("127.0.0.1")}, - AddressUsed: net.ParseIP("127.0.0.1"), + AddressesResolved: []netip.Addr{netip.MustParseAddr("127.0.0.1")}, + AddressUsed: netip.MustParseAddr("127.0.0.1"), ResolverAddrs: []string{"MockClient"}, }) } @@ -775,15 +900,15 @@ func TestFetchHTTP(t *testing.T) { // The first request will not have a port # in the URL. url := "http://example.com/max-redirect/0" if i != 0 { - url = fmt.Sprintf("http://example.com:%d/max-redirect/%d", httpPort, i) + url = fmt.Sprintf("http://example.com:%d/max-redirect/%d", httpPortIPv4, i) } expectedTooManyRedirRecords = append(expectedTooManyRedirRecords, core.ValidationRecord{ Hostname: "example.com", - Port: strconv.Itoa(httpPort), + Port: strconv.Itoa(httpPortIPv4), URL: url, - AddressesResolved: []net.IP{net.ParseIP("127.0.0.1")}, - AddressUsed: net.ParseIP("127.0.0.1"), + AddressesResolved: []netip.Addr{netip.MustParseAddr("127.0.0.1")}, + AddressUsed: netip.MustParseAddr("127.0.0.1"), ResolverAddrs: []string{"MockClient"}, }) } @@ -795,16 +920,17 @@ func TestFetchHTTP(t *testing.T) { testCases := []struct { Name string - Host string + IPv6 bool + Ident identifier.ACMEIdentifier Path string ExpectedBody string ExpectedRecords []core.ValidationRecord ExpectedProblem *probs.ProblemDetails }{ { - Name: "No IPs for host", - Host: "always.invalid", - Path: "/.well-known/whatever", + Name: "No IPs for host", + Ident: identifier.NewDNS("always.invalid"), + Path: "/.well-known/whatever", ExpectedProblem: probs.DNS( "No valid IP addresses found for always.invalid"), // There are no validation records in this case because the base record @@ -812,61 +938,43 @@ func TestFetchHTTP(t *testing.T) { ExpectedRecords: nil, }, { - Name: "Timeout for host with standard ACME allowed port", - Host: "example.com", - Path: "/timeout", + Name: "Timeout for host with standard ACME allowed port", + Ident: identifier.NewDNS("example.com"), + Path: "/timeout", ExpectedProblem: probs.Connection( "127.0.0.1: Fetching http://example.com/timeout: " + "Timeout after connect (your server may be slow or overloaded)"), ExpectedRecords: []core.ValidationRecord{ { Hostname: "example.com", - Port: strconv.Itoa(httpPort), + Port: strconv.Itoa(httpPortIPv4), URL: "http://example.com/timeout", - AddressesResolved: []net.IP{net.ParseIP("127.0.0.1")}, - AddressUsed: net.ParseIP("127.0.0.1"), + AddressesResolved: []netip.Addr{netip.MustParseAddr("127.0.0.1")}, + AddressUsed: netip.MustParseAddr("127.0.0.1"), ResolverAddrs: []string{"MockClient"}, }, }, }, { - Name: "Connecting to bad port", - Host: "example.com:" + strconv.Itoa(httpPort), - Path: "/timeout", - ExpectedProblem: probs.Connection( - "127.0.0.1: Fetching http://example.com:" + strconv.Itoa(httpPort) + "/timeout: " + - "Error getting validation data"), - ExpectedRecords: []core.ValidationRecord{ - { - Hostname: "example.com:" + strconv.Itoa(httpPort), - Port: strconv.Itoa(httpPort), - URL: "http://example.com:" + strconv.Itoa(httpPort) + "/timeout", - AddressesResolved: []net.IP{net.ParseIP("127.0.0.1")}, - AddressUsed: net.ParseIP("127.0.0.1"), - ResolverAddrs: []string{"MockClient"}, - }, - }, - }, - { - Name: "Redirect loop", - Host: "example.com", - Path: "/loop", + Name: "Redirect loop", + Ident: identifier.NewDNS("example.com"), + Path: "/loop", ExpectedProblem: probs.Connection(fmt.Sprintf( - "127.0.0.1: Fetching http://example.com:%d/loop: Redirect loop detected", httpPort)), + "127.0.0.1: Fetching http://example.com:%d/loop: Redirect loop detected", httpPortIPv4)), ExpectedRecords: expectedLoopRecords, }, { - Name: "Too many redirects", - Host: "example.com", - Path: "/max-redirect/0", + Name: "Too many redirects", + Ident: identifier.NewDNS("example.com"), + Path: "/max-redirect/0", ExpectedProblem: probs.Connection(fmt.Sprintf( - "127.0.0.1: Fetching http://example.com:%d/max-redirect/12: Too many redirects", httpPort)), + "127.0.0.1: Fetching http://example.com:%d/max-redirect/12: Too many redirects", httpPortIPv4)), ExpectedRecords: expectedTooManyRedirRecords, }, { - Name: "Redirect to bad protocol", - Host: "example.com", - Path: "/redir-bad-proto", + Name: "Redirect to bad protocol", + Ident: identifier.NewDNS("example.com"), + Path: "/redir-bad-proto", ExpectedProblem: probs.Connection( "127.0.0.1: Fetching gopher://example.com: Invalid protocol scheme in " + `redirect target. Only "http" and "https" protocol schemes ` + @@ -874,206 +982,234 @@ func TestFetchHTTP(t *testing.T) { ExpectedRecords: []core.ValidationRecord{ { Hostname: "example.com", - Port: strconv.Itoa(httpPort), + Port: strconv.Itoa(httpPortIPv4), URL: "http://example.com/redir-bad-proto", - AddressesResolved: []net.IP{net.ParseIP("127.0.0.1")}, - AddressUsed: net.ParseIP("127.0.0.1"), + AddressesResolved: []netip.Addr{netip.MustParseAddr("127.0.0.1")}, + AddressUsed: netip.MustParseAddr("127.0.0.1"), ResolverAddrs: []string{"MockClient"}, }, }, }, { - Name: "Redirect to bad port", - Host: "example.com", - Path: "/redir-bad-port", + Name: "Redirect to bad port", + Ident: identifier.NewDNS("example.com"), + Path: "/redir-bad-port", ExpectedProblem: probs.Connection(fmt.Sprintf( "127.0.0.1: Fetching https://example.com:1987: Invalid port in redirect target. "+ - "Only ports %d and 443 are supported, not 1987", httpPort)), + "Only ports %d and 443 are supported, not 1987", httpPortIPv4)), ExpectedRecords: []core.ValidationRecord{ { Hostname: "example.com", - Port: strconv.Itoa(httpPort), + Port: strconv.Itoa(httpPortIPv4), URL: "http://example.com/redir-bad-port", - AddressesResolved: []net.IP{net.ParseIP("127.0.0.1")}, - AddressUsed: net.ParseIP("127.0.0.1"), + AddressesResolved: []netip.Addr{netip.MustParseAddr("127.0.0.1")}, + AddressUsed: netip.MustParseAddr("127.0.0.1"), ResolverAddrs: []string{"MockClient"}, }, }, }, { - Name: "Redirect to bad host (bare IP address)", - Host: "example.com", - Path: "/redir-bad-host", - ExpectedProblem: probs.Connection( - "127.0.0.1: Fetching https://127.0.0.1: Invalid host in redirect target " + - `"127.0.0.1". Only domain names are supported, not IP addresses`), + Name: "Redirect to bare IPv4 address", + Ident: identifier.NewDNS("example.com"), + Path: "/redir-bare-ipv4", + ExpectedBody: "ok", ExpectedRecords: []core.ValidationRecord{ { Hostname: "example.com", - Port: strconv.Itoa(httpPort), - URL: "http://example.com/redir-bad-host", - AddressesResolved: []net.IP{net.ParseIP("127.0.0.1")}, - AddressUsed: net.ParseIP("127.0.0.1"), + Port: strconv.Itoa(httpPortIPv4), + URL: "http://example.com/redir-bare-ipv4", + AddressesResolved: []netip.Addr{netip.MustParseAddr("127.0.0.1")}, + AddressUsed: netip.MustParseAddr("127.0.0.1"), ResolverAddrs: []string{"MockClient"}, }, + { + Hostname: "127.0.0.1", + Port: strconv.Itoa(httpPortIPv4), + URL: "http://127.0.0.1/ok", + AddressesResolved: []netip.Addr{netip.MustParseAddr("127.0.0.1")}, + AddressUsed: netip.MustParseAddr("127.0.0.1"), + }, + }, + }, { + Name: "Redirect to bare IPv6 address", + IPv6: true, + Ident: identifier.NewDNS("ipv6.localhost"), + Path: "/redir-bare-ipv6", + ExpectedBody: "ok", + ExpectedRecords: []core.ValidationRecord{ + { + Hostname: "ipv6.localhost", + Port: strconv.Itoa(httpPortIPv6), + URL: "http://ipv6.localhost/redir-bare-ipv6", + AddressesResolved: []netip.Addr{netip.MustParseAddr("::1")}, + AddressUsed: netip.MustParseAddr("::1"), + ResolverAddrs: []string{"MockClient"}, + }, + { + Hostname: "::1", + Port: strconv.Itoa(httpPortIPv6), + URL: "http://[::1]/ok", + AddressesResolved: []netip.Addr{netip.MustParseAddr("::1")}, + AddressUsed: netip.MustParseAddr("::1"), + }, }, }, { - Name: "Redirect to long path", - Host: "example.com", - Path: "/redir-path-too-long", + Name: "Redirect to long path", + Ident: identifier.NewDNS("example.com"), + Path: "/redir-path-too-long", ExpectedProblem: probs.Connection( "127.0.0.1: Fetching https://example.com/this-is-too-long-01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789: Redirect target too long"), ExpectedRecords: []core.ValidationRecord{ { Hostname: "example.com", - Port: strconv.Itoa(httpPort), + Port: strconv.Itoa(httpPortIPv4), URL: "http://example.com/redir-path-too-long", - AddressesResolved: []net.IP{net.ParseIP("127.0.0.1")}, - AddressUsed: net.ParseIP("127.0.0.1"), + AddressesResolved: []netip.Addr{netip.MustParseAddr("127.0.0.1")}, + AddressUsed: netip.MustParseAddr("127.0.0.1"), ResolverAddrs: []string{"MockClient"}, }, }, }, { - Name: "Wrong HTTP status code", - Host: "example.com", - Path: "/bad-status-code", + Name: "Wrong HTTP status code", + Ident: identifier.NewDNS("example.com"), + Path: "/bad-status-code", ExpectedProblem: probs.Unauthorized( "127.0.0.1: Invalid response from http://example.com/bad-status-code: 410"), ExpectedRecords: []core.ValidationRecord{ { Hostname: "example.com", - Port: strconv.Itoa(httpPort), + Port: strconv.Itoa(httpPortIPv4), URL: "http://example.com/bad-status-code", - AddressesResolved: []net.IP{net.ParseIP("127.0.0.1")}, - AddressUsed: net.ParseIP("127.0.0.1"), + AddressesResolved: []netip.Addr{netip.MustParseAddr("127.0.0.1")}, + AddressUsed: netip.MustParseAddr("127.0.0.1"), ResolverAddrs: []string{"MockClient"}, }, }, }, { - Name: "HTTP status code 303 redirect", - Host: "example.com", - Path: "/303-see-other", + Name: "HTTP status code 303 redirect", + Ident: identifier.NewDNS("example.com"), + Path: "/303-see-other", ExpectedProblem: probs.Connection( "127.0.0.1: Fetching http://example.org/303-see-other: received disallowed redirect status code"), ExpectedRecords: []core.ValidationRecord{ { Hostname: "example.com", - Port: strconv.Itoa(httpPort), + Port: strconv.Itoa(httpPortIPv4), URL: "http://example.com/303-see-other", - AddressesResolved: []net.IP{net.ParseIP("127.0.0.1")}, - AddressUsed: net.ParseIP("127.0.0.1"), + AddressesResolved: []netip.Addr{netip.MustParseAddr("127.0.0.1")}, + AddressUsed: netip.MustParseAddr("127.0.0.1"), ResolverAddrs: []string{"MockClient"}, }, }, }, { - Name: "Response too large", - Host: "example.com", - Path: "/resp-too-big", + Name: "Response too large", + Ident: identifier.NewDNS("example.com"), + Path: "/resp-too-big", ExpectedProblem: probs.Unauthorized(fmt.Sprintf( "127.0.0.1: Invalid response from http://example.com/resp-too-big: %q", expectedTruncatedResp.String(), )), ExpectedRecords: []core.ValidationRecord{ { Hostname: "example.com", - Port: strconv.Itoa(httpPort), + Port: strconv.Itoa(httpPortIPv4), URL: "http://example.com/resp-too-big", - AddressesResolved: []net.IP{net.ParseIP("127.0.0.1")}, - AddressUsed: net.ParseIP("127.0.0.1"), + AddressesResolved: []netip.Addr{netip.MustParseAddr("127.0.0.1")}, + AddressUsed: netip.MustParseAddr("127.0.0.1"), ResolverAddrs: []string{"MockClient"}, }, }, }, { - Name: "Broken IPv6 only", - Host: "ipv6.localhost", - Path: "/ok", + Name: "Broken IPv6 only", + Ident: identifier.NewDNS("ipv6.localhost"), + Path: "/ok", ExpectedProblem: probs.Connection( "::1: Fetching http://ipv6.localhost/ok: Connection refused"), ExpectedRecords: []core.ValidationRecord{ { Hostname: "ipv6.localhost", - Port: strconv.Itoa(httpPort), + Port: strconv.Itoa(httpPortIPv4), URL: "http://ipv6.localhost/ok", - AddressesResolved: []net.IP{net.ParseIP("::1")}, - AddressUsed: net.ParseIP("::1"), + AddressesResolved: []netip.Addr{netip.MustParseAddr("::1")}, + AddressUsed: netip.MustParseAddr("::1"), ResolverAddrs: []string{"MockClient"}, }, }, }, { Name: "Dual homed w/ broken IPv6, working IPv4", - Host: "ipv4.and.ipv6.localhost", + Ident: identifier.NewDNS("ipv4.and.ipv6.localhost"), Path: "/ok", ExpectedBody: "ok", ExpectedRecords: []core.ValidationRecord{ { Hostname: "ipv4.and.ipv6.localhost", - Port: strconv.Itoa(httpPort), + Port: strconv.Itoa(httpPortIPv4), URL: "http://ipv4.and.ipv6.localhost/ok", - AddressesResolved: []net.IP{net.ParseIP("::1"), net.ParseIP("127.0.0.1")}, + AddressesResolved: []netip.Addr{netip.MustParseAddr("::1"), netip.MustParseAddr("127.0.0.1")}, // The first validation record should have used the IPv6 addr - AddressUsed: net.ParseIP("::1"), + AddressUsed: netip.MustParseAddr("::1"), ResolverAddrs: []string{"MockClient"}, }, { Hostname: "ipv4.and.ipv6.localhost", - Port: strconv.Itoa(httpPort), + Port: strconv.Itoa(httpPortIPv4), URL: "http://ipv4.and.ipv6.localhost/ok", - AddressesResolved: []net.IP{net.ParseIP("::1"), net.ParseIP("127.0.0.1")}, + AddressesResolved: []netip.Addr{netip.MustParseAddr("::1"), netip.MustParseAddr("127.0.0.1")}, // The second validation record should have used the IPv4 addr as a fallback - AddressUsed: net.ParseIP("127.0.0.1"), + AddressUsed: netip.MustParseAddr("127.0.0.1"), ResolverAddrs: []string{"MockClient"}, }, }, }, { Name: "Working IPv4 only", - Host: "example.com", + Ident: identifier.NewDNS("example.com"), Path: "/ok", ExpectedBody: "ok", ExpectedRecords: []core.ValidationRecord{ { Hostname: "example.com", - Port: strconv.Itoa(httpPort), + Port: strconv.Itoa(httpPortIPv4), URL: "http://example.com/ok", - AddressesResolved: []net.IP{net.ParseIP("127.0.0.1")}, - AddressUsed: net.ParseIP("127.0.0.1"), + AddressesResolved: []netip.Addr{netip.MustParseAddr("127.0.0.1")}, + AddressUsed: netip.MustParseAddr("127.0.0.1"), ResolverAddrs: []string{"MockClient"}, }, }, }, { Name: "Redirect to uppercase Public Suffix", - Host: "example.com", + Ident: identifier.NewDNS("example.com"), Path: "/redir-uppercase-publicsuffix", ExpectedBody: "ok", ExpectedRecords: []core.ValidationRecord{ { Hostname: "example.com", - Port: strconv.Itoa(httpPort), + Port: strconv.Itoa(httpPortIPv4), URL: "http://example.com/redir-uppercase-publicsuffix", - AddressesResolved: []net.IP{net.ParseIP("127.0.0.1")}, - AddressUsed: net.ParseIP("127.0.0.1"), + AddressesResolved: []netip.Addr{netip.MustParseAddr("127.0.0.1")}, + AddressUsed: netip.MustParseAddr("127.0.0.1"), ResolverAddrs: []string{"MockClient"}, }, { Hostname: "example.com", - Port: strconv.Itoa(httpPort), + Port: strconv.Itoa(httpPortIPv4), URL: "http://example.com/ok", - AddressesResolved: []net.IP{net.ParseIP("127.0.0.1")}, - AddressUsed: net.ParseIP("127.0.0.1"), + AddressesResolved: []netip.Addr{netip.MustParseAddr("127.0.0.1")}, + AddressUsed: netip.MustParseAddr("127.0.0.1"), ResolverAddrs: []string{"MockClient"}, }, }, }, { - Name: "Reflected response body containing printf verbs", - Host: "example.com", - Path: "/printf-verbs", + Name: "Reflected response body containing printf verbs", + Ident: identifier.NewDNS("example.com"), + Path: "/printf-verbs", ExpectedProblem: &probs.ProblemDetails{ Type: probs.UnauthorizedProblem, Detail: fmt.Sprintf("127.0.0.1: Invalid response from http://example.com/printf-verbs: %q", @@ -1083,10 +1219,10 @@ func TestFetchHTTP(t *testing.T) { ExpectedRecords: []core.ValidationRecord{ { Hostname: "example.com", - Port: strconv.Itoa(httpPort), + Port: strconv.Itoa(httpPortIPv4), URL: "http://example.com/printf-verbs", - AddressesResolved: []net.IP{net.ParseIP("127.0.0.1")}, - AddressUsed: net.ParseIP("127.0.0.1"), + AddressesResolved: []netip.Addr{netip.MustParseAddr("127.0.0.1")}, + AddressUsed: netip.MustParseAddr("127.0.0.1"), ResolverAddrs: []string{"MockClient"}, }, }, @@ -1097,7 +1233,14 @@ func TestFetchHTTP(t *testing.T) { t.Run(tc.Name, func(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*500) defer cancel() - body, records, err := va.fetchHTTP(ctx, tc.Host, tc.Path) + var body []byte + var records []core.ValidationRecord + var err error + if tc.IPv6 { + body, records, err = vaIPv6.processHTTPValidation(ctx, tc.Ident, tc.Path) + } else { + body, records, err = vaIPv4.processHTTPValidation(ctx, tc.Ident, tc.Path) + } if tc.ExpectedProblem == nil { test.AssertNotError(t, err, "expected nil prob") } else { @@ -1130,7 +1273,7 @@ const pathLooper = "looper" const pathValid = "valid" const rejectUserAgent = "rejectMe" -func httpSrv(t *testing.T, token string) *httptest.Server { +func httpSrv(t *testing.T, token string, ipv6 bool) *httptest.Server { m := http.NewServeMux() server := httptest.NewUnstartedServer(m) @@ -1171,7 +1314,7 @@ func httpSrv(t *testing.T, token string) *httptest.Server { port := getPort(server) http.Redirect(w, r, fmt.Sprintf("http://other.valid.com:%d/path", port), http.StatusFound) } else if strings.HasSuffix(r.URL.Path, pathReLookupInvalid) { - t.Logf("HTTPSRV: Got a redirect req to an invalid hostname\n") + t.Logf("HTTPSRV: Got a redirect req to an invalid host\n") http.Redirect(w, r, "http://invalid.invalid/path", http.StatusFound) } else if strings.HasSuffix(r.URL.Path, pathRedirectToFailingURL) { t.Logf("HTTPSRV: Redirecting to a URL that will fail\n") @@ -1200,23 +1343,31 @@ func httpSrv(t *testing.T, token string) *httptest.Server { } }) + if ipv6 { + l, err := net.Listen("tcp", "[::1]:0") + if err != nil { + panic(fmt.Sprintf("httptest: failed to listen on a port: %v", err)) + } + server.Listener = l + } + server.Start() return server } func TestHTTPBadPort(t *testing.T) { - hs := httpSrv(t, expectedToken) + hs := httpSrv(t, expectedToken, false) defer hs.Close() - va, _ := setup(hs, 0, "", nil, nil) + va, _ := setup(hs, "", nil, nil) // Pick a random port between 40000 and 65000 - with great certainty we won't // have an HTTP server listening on this port and the test will fail as // intended - badPort := 40000 + mrand.Intn(25000) + badPort := 40000 + mrand.IntN(25000) va.httpPort = badPort - _, err := va.validateHTTP01(ctx, dnsi("localhost"), expectedToken, expectedKeyAuthorization) + _, err := va.validateHTTP01(ctx, identifier.NewDNS("localhost"), expectedToken, expectedKeyAuthorization) if err == nil { t.Fatalf("Server's down; expected refusal. Where did we connect?") } @@ -1227,6 +1378,23 @@ func TestHTTPBadPort(t *testing.T) { } } +func TestHTTPBadIdentifier(t *testing.T) { + hs := httpSrv(t, expectedToken, false) + defer hs.Close() + + va, _ := setup(hs, "", nil, nil) + + _, err := va.validateHTTP01(ctx, identifier.ACMEIdentifier{Type: "smime", Value: "dobber@bad.horse"}, expectedToken, expectedKeyAuthorization) + if err == nil { + t.Fatalf("Server accepted a hypothetical S/MIME identifier") + } + prob := detailedError(err) + test.AssertEquals(t, prob.Type, probs.MalformedProblem) + if !strings.Contains(prob.Detail, "Identifier type for HTTP-01 challenge was not DNS or IP") { + t.Errorf("Expected an identifier type error, got %q", prob.Detail) + } +} + func TestHTTPKeyAuthorizationFileMismatch(t *testing.T) { m := http.NewServeMux() hs := httptest.NewUnstartedServer(m) @@ -1235,39 +1403,39 @@ func TestHTTPKeyAuthorizationFileMismatch(t *testing.T) { }) hs.Start() - va, _ := setup(hs, 0, "", nil, nil) - _, err := va.validateHTTP01(ctx, dnsi("localhost.com"), expectedToken, expectedKeyAuthorization) + va, _ := setup(hs, "", nil, nil) + _, err := va.validateHTTP01(ctx, identifier.NewDNS("localhost.com"), expectedToken, expectedKeyAuthorization) if err == nil { t.Fatalf("Expected validation to fail when file mismatched.") } - expected := `The key authorization file from the server did not match this challenge. Expected "LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0.9jg46WB3rR_AHD-EBXdN7cBkH1WOu0tA3M9fm21mqTI" (got "\xef\xffAABBCC")` + expected := fmt.Sprintf(`The key authorization file from the server did not match this challenge. Expected "%s" (got "\xef\xffAABBCC")`, expectedKeyAuthorization) if err.Error() != expected { t.Errorf("validation failed with %s, expected %s", err, expected) } } func TestHTTP(t *testing.T) { - // NOTE: We do not attempt to shut down the server. The problem is that the - // "wait-long" handler sleeps for ten seconds, but this test finishes in less - // than that. So if we try to call hs.Close() at the end of the test, we'll be - // closing the test server while a request is still pending. Unfortunately, - // there appears to be an issue in httptest that trips Go's race detector when - // that happens, failing the test. So instead, we live with leaving the server - // around till the process exits. - // TODO(#1989): close hs - hs := httpSrv(t, expectedToken) + hs := httpSrv(t, expectedToken, false) + defer hs.Close() - va, log := setup(hs, 0, "", nil, nil) + va, log := setup(hs, "", nil, nil) - _, err := va.validateHTTP01(ctx, dnsi("localhost.com"), expectedToken, expectedKeyAuthorization) + _, err := va.validateHTTP01(ctx, identifier.NewDNS("localhost.com"), expectedToken, expectedKeyAuthorization) if err != nil { - t.Errorf("Unexpected failure in HTTP validation: %s", err) + t.Errorf("Unexpected failure in HTTP validation for DNS: %s", err) } test.AssertEquals(t, len(log.GetAllMatching(`\[AUDIT\] `)), 1) log.Clear() - _, err = va.validateHTTP01(ctx, dnsi("localhost.com"), path404, ka(path404)) + _, err = va.validateHTTP01(ctx, identifier.NewIP(netip.MustParseAddr("127.0.0.1")), expectedToken, expectedKeyAuthorization) + if err != nil { + t.Errorf("Unexpected failure in HTTP validation for IPv4: %s", err) + } + test.AssertEquals(t, len(log.GetAllMatching(`\[AUDIT\] `)), 1) + + log.Clear() + _, err = va.validateHTTP01(ctx, identifier.NewDNS("localhost.com"), path404, ka(path404)) if err == nil { t.Fatalf("Should have found a 404 for the challenge.") } @@ -1277,7 +1445,7 @@ func TestHTTP(t *testing.T) { log.Clear() // The "wrong token" will actually be the expectedToken. It's wrong // because it doesn't match pathWrongToken. - _, err = va.validateHTTP01(ctx, dnsi("localhost.com"), pathWrongToken, ka(pathWrongToken)) + _, err = va.validateHTTP01(ctx, identifier.NewDNS("localhost.com"), pathWrongToken, ka(pathWrongToken)) if err == nil { t.Fatalf("Should have found the wrong token value.") } @@ -1286,7 +1454,7 @@ func TestHTTP(t *testing.T) { test.AssertEquals(t, len(log.GetAllMatching(`\[AUDIT\] `)), 1) log.Clear() - _, err = va.validateHTTP01(ctx, dnsi("localhost.com"), pathMoved, ka(pathMoved)) + _, err = va.validateHTTP01(ctx, identifier.NewDNS("localhost.com"), pathMoved, ka(pathMoved)) if err != nil { t.Fatalf("Failed to follow http.StatusMovedPermanently redirect") } @@ -1295,7 +1463,7 @@ func TestHTTP(t *testing.T) { test.AssertEquals(t, len(matchedValidRedirect), 1) log.Clear() - _, err = va.validateHTTP01(ctx, dnsi("localhost.com"), pathFound, ka(pathFound)) + _, err = va.validateHTTP01(ctx, identifier.NewDNS("localhost.com"), pathFound, ka(pathFound)) if err != nil { t.Fatalf("Failed to follow http.StatusFound redirect") } @@ -1304,14 +1472,7 @@ func TestHTTP(t *testing.T) { test.AssertEquals(t, len(matchedValidRedirect), 1) test.AssertEquals(t, len(matchedMovedRedirect), 1) - ipIdentifier := identifier.ACMEIdentifier{Type: identifier.IdentifierType("ip"), Value: "127.0.0.1"} - _, err = va.validateHTTP01(ctx, ipIdentifier, pathFound, ka(pathFound)) - if err == nil { - t.Fatalf("IdentifierType IP shouldn't have worked.") - } - test.AssertErrorIs(t, err, berrors.Malformed) - - _, err = va.validateHTTP01(ctx, identifier.ACMEIdentifier{Type: identifier.DNS, Value: "always.invalid"}, pathFound, ka(pathFound)) + _, err = va.validateHTTP01(ctx, identifier.NewDNS("always.invalid"), pathFound, ka(pathFound)) if err == nil { t.Fatalf("Domain name is invalid.") } @@ -1319,17 +1480,30 @@ func TestHTTP(t *testing.T) { test.AssertEquals(t, prob.Type, probs.DNSProblem) } -func TestHTTPTimeout(t *testing.T) { - hs := httpSrv(t, expectedToken) - // TODO(#1989): close hs +func TestHTTPIPv6(t *testing.T) { + hs := httpSrv(t, expectedToken, true) + defer hs.Close() - va, _ := setup(hs, 0, "", nil, nil) + va, log := setup(hs, "", nil, nil) + + _, err := va.validateHTTP01(ctx, identifier.NewIP(netip.MustParseAddr("::1")), expectedToken, expectedKeyAuthorization) + if err != nil { + t.Errorf("Unexpected failure in HTTP validation for IPv6: %s", err) + } + test.AssertEquals(t, len(log.GetAllMatching(`\[AUDIT\] `)), 1) +} + +func TestHTTPTimeout(t *testing.T) { + hs := httpSrv(t, expectedToken, false) + defer hs.Close() + + va, _ := setup(hs, "", nil, nil) started := time.Now() timeout := 250 * time.Millisecond ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() - _, err := va.validateHTTP01(ctx, dnsi("localhost"), pathWaitLong, ka(pathWaitLong)) + _, err := va.validateHTTP01(ctx, identifier.NewDNS("localhost"), pathWaitLong, ka(pathWaitLong)) if err == nil { t.Fatalf("Connection should've timed out") } @@ -1348,70 +1522,12 @@ func TestHTTPTimeout(t *testing.T) { test.AssertEquals(t, prob.Detail, "127.0.0.1: Fetching http://localhost/.well-known/acme-challenge/wait-long: Timeout after connect (your server may be slow or overloaded)") } -// dnsMockReturnsUnroutable is a DNSClient mock that always returns an -// unroutable address for LookupHost. This is useful in testing connect -// timeouts. -type dnsMockReturnsUnroutable struct { - *bdns.MockClient -} - -func (mock dnsMockReturnsUnroutable) LookupHost(_ context.Context, hostname string) ([]net.IP, bdns.ResolverAddrs, error) { - return []net.IP{net.ParseIP("198.51.100.1")}, bdns.ResolverAddrs{"dnsMockReturnsUnroutable"}, nil -} - -// TestHTTPDialTimeout tests that we give the proper "Timeout during connect" -// error when dial fails. We do this by using a mock DNS client that resolves -// everything to an unroutable IP address. -func TestHTTPDialTimeout(t *testing.T) { - va, _ := setup(nil, 0, "", nil, nil) - - started := time.Now() - timeout := 250 * time.Millisecond - ctx, cancel := context.WithTimeout(context.Background(), timeout) - defer cancel() - - va.dnsClient = dnsMockReturnsUnroutable{&bdns.MockClient{}} - // The only method I've found so far to trigger a connect timeout is to - // connect to an unrouteable IP address. This usually generates a connection - // timeout, but will rarely return "Network unreachable" instead. If we get - // that, just retry until we get something other than "Network unreachable". - var err error - for range 20 { - _, err = va.validateHTTP01(ctx, dnsi("unroutable.invalid"), expectedToken, expectedKeyAuthorization) - if err != nil && strings.Contains(err.Error(), "network is unreachable") { - continue - } else { - break - } - } - if err == nil { - t.Fatalf("Connection should've timed out") - } - took := time.Since(started) - // Check that the HTTP connection doesn't return too fast, and times - // out after the expected time - if took < (timeout-200*time.Millisecond)/2 { - t.Fatalf("HTTP returned before %s (%s) with %q", timeout, took, err.Error()) - } - if took > 2*timeout { - t.Fatalf("HTTP connection didn't timeout after %s seconds", timeout) - } - prob := detailedError(err) - test.AssertEquals(t, prob.Type, probs.ConnectionProblem) - expectMatch := regexp.MustCompile( - "Fetching http://unroutable.invalid/.well-known/acme-challenge/.*: Timeout during connect") - if !expectMatch.MatchString(prob.Detail) { - t.Errorf("Problem details incorrect. Got %q, expected to match %q", - prob.Detail, expectMatch) - } -} - func TestHTTPRedirectLookup(t *testing.T) { - hs := httpSrv(t, expectedToken) + hs := httpSrv(t, expectedToken, false) defer hs.Close() - va, log := setup(hs, 0, "", nil, nil) + va, log := setup(hs, "", nil, nil) - _, err := va.validateHTTP01(ctx, dnsi("localhost.com"), pathMoved, ka(pathMoved)) + _, err := va.validateHTTP01(ctx, identifier.NewDNS("localhost.com"), pathMoved, ka(pathMoved)) if err != nil { t.Fatalf("Unexpected failure in redirect (%s): %s", pathMoved, err) } @@ -1421,7 +1537,7 @@ func TestHTTPRedirectLookup(t *testing.T) { test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for localhost.com: \[127.0.0.1\]`)), 2) log.Clear() - _, err = va.validateHTTP01(ctx, dnsi("localhost.com"), pathFound, ka(pathFound)) + _, err = va.validateHTTP01(ctx, identifier.NewDNS("localhost.com"), pathFound, ka(pathFound)) if err != nil { t.Fatalf("Unexpected failure in redirect (%s): %s", pathFound, err) } @@ -1431,14 +1547,14 @@ func TestHTTPRedirectLookup(t *testing.T) { test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for localhost.com: \[127.0.0.1\]`)), 3) log.Clear() - _, err = va.validateHTTP01(ctx, dnsi("localhost.com"), pathReLookupInvalid, ka(pathReLookupInvalid)) + _, err = va.validateHTTP01(ctx, identifier.NewDNS("localhost.com"), pathReLookupInvalid, ka(pathReLookupInvalid)) test.AssertError(t, err, "error for pathReLookupInvalid should not be nil") test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for localhost.com: \[127.0.0.1\]`)), 1) prob := detailedError(err) - test.AssertDeepEquals(t, prob, probs.Connection(`127.0.0.1: Fetching http://invalid.invalid/path: Invalid hostname in redirect target, must end in IANA registered TLD`)) + test.AssertDeepEquals(t, prob, probs.Connection(`127.0.0.1: Fetching http://invalid.invalid/path: Invalid host in redirect target, must end in IANA registered TLD`)) log.Clear() - _, err = va.validateHTTP01(ctx, dnsi("localhost.com"), pathReLookup, ka(pathReLookup)) + _, err = va.validateHTTP01(ctx, identifier.NewDNS("localhost.com"), pathReLookup, ka(pathReLookup)) if err != nil { t.Fatalf("Unexpected error in redirect (%s): %s", pathReLookup, err) } @@ -1448,7 +1564,7 @@ func TestHTTPRedirectLookup(t *testing.T) { test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for other.valid.com: \[127.0.0.1\]`)), 1) log.Clear() - _, err = va.validateHTTP01(ctx, dnsi("localhost.com"), pathRedirectInvalidPort, ka(pathRedirectInvalidPort)) + _, err = va.validateHTTP01(ctx, identifier.NewDNS("localhost.com"), pathRedirectInvalidPort, ka(pathRedirectInvalidPort)) test.AssertNotNil(t, err, "error for pathRedirectInvalidPort should not be nil") prob = detailedError(err) test.AssertEquals(t, prob.Detail, fmt.Sprintf( @@ -1459,7 +1575,7 @@ func TestHTTPRedirectLookup(t *testing.T) { // HTTP 500 errors. The test case is ensuring that the connection error // is referencing the redirected to host, instead of the original host. log.Clear() - _, err = va.validateHTTP01(ctx, dnsi("localhost.com"), pathRedirectToFailingURL, ka(pathRedirectToFailingURL)) + _, err = va.validateHTTP01(ctx, identifier.NewDNS("localhost.com"), pathRedirectToFailingURL, ka(pathRedirectToFailingURL)) test.AssertNotNil(t, err, "err should not be nil") prob = detailedError(err) test.AssertDeepEquals(t, prob, @@ -1469,28 +1585,28 @@ func TestHTTPRedirectLookup(t *testing.T) { } func TestHTTPRedirectLoop(t *testing.T) { - hs := httpSrv(t, expectedToken) + hs := httpSrv(t, expectedToken, false) defer hs.Close() - va, _ := setup(hs, 0, "", nil, nil) + va, _ := setup(hs, "", nil, nil) - _, prob := va.validateHTTP01(ctx, dnsi("localhost"), "looper", ka("looper")) + _, prob := va.validateHTTP01(ctx, identifier.NewDNS("localhost"), "looper", ka("looper")) if prob == nil { t.Fatalf("Challenge should have failed for looper") } } func TestHTTPRedirectUserAgent(t *testing.T) { - hs := httpSrv(t, expectedToken) + hs := httpSrv(t, expectedToken, false) defer hs.Close() - va, _ := setup(hs, 0, "", nil, nil) + va, _ := setup(hs, "", nil, nil) va.userAgent = rejectUserAgent - _, prob := va.validateHTTP01(ctx, dnsi("localhost"), pathMoved, ka(pathMoved)) + _, prob := va.validateHTTP01(ctx, identifier.NewDNS("localhost"), pathMoved, ka(pathMoved)) if prob == nil { t.Fatalf("Challenge with rejectUserAgent should have failed (%s).", pathMoved) } - _, prob = va.validateHTTP01(ctx, dnsi("localhost"), pathFound, ka(pathFound)) + _, prob = va.validateHTTP01(ctx, identifier.NewDNS("localhost"), pathFound, ka(pathFound)) if prob == nil { t.Fatalf("Challenge with rejectUserAgent should have failed (%s).", pathFound) } @@ -1515,23 +1631,23 @@ func getPort(hs *httptest.Server) int { func TestValidateHTTP(t *testing.T) { token := core.NewToken() - hs := httpSrv(t, token) + hs := httpSrv(t, token, false) defer hs.Close() - va, _ := setup(hs, 0, "", nil, nil) + va, _ := setup(hs, "", nil, nil) - _, prob := va.validateHTTP01(ctx, dnsi("localhost"), token, ka(token)) + _, prob := va.validateHTTP01(ctx, identifier.NewDNS("localhost"), token, ka(token)) test.Assert(t, prob == nil, "validation failed") } func TestLimitedReader(t *testing.T) { token := core.NewToken() - hs := httpSrv(t, "012345\xff67890123456789012345678901234567890123456789012345678901234567890123456789") - va, _ := setup(hs, 0, "", nil, nil) + hs := httpSrv(t, "012345\xff67890123456789012345678901234567890123456789012345678901234567890123456789", false) + va, _ := setup(hs, "", nil, nil) defer hs.Close() - _, err := va.validateHTTP01(ctx, dnsi("localhost"), token, ka(token)) + _, err := va.validateHTTP01(ctx, identifier.NewDNS("localhost"), token, ka(token)) prob := detailedError(err) test.AssertEquals(t, prob.Type, probs.UnauthorizedProblem) @@ -1542,3 +1658,71 @@ func TestLimitedReader(t *testing.T) { t.Errorf("Problem Detail contained an invalid UTF-8 string") } } + +type hostHeaderHandler struct { + host string +} + +func (handler *hostHeaderHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) { + handler.host = req.Host +} + +// TestHTTPHostHeader tests compliance with RFC 8555, Sec. 8.3 & RFC 8738, Sec. +// 5. +func TestHTTPHostHeader(t *testing.T) { + testCases := []struct { + Name string + Ident identifier.ACMEIdentifier + IPv6 bool + want string + }{ + { + Name: "DNS name", + Ident: identifier.NewDNS("example.com"), + want: "example.com", + }, + { + Name: "IPv4 address", + Ident: identifier.NewIP(netip.MustParseAddr("127.0.0.1")), + want: "127.0.0.1", + }, + { + Name: "IPv6 address", + Ident: identifier.NewIP(netip.MustParseAddr("::1")), + IPv6: true, + want: "[::1]", + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*500) + defer cancel() + + handler := hostHeaderHandler{} + testSrv := httptest.NewUnstartedServer(&handler) + + if tc.IPv6 { + l, err := net.Listen("tcp", "[::1]:0") + if err != nil { + panic(fmt.Sprintf("httptest: failed to listen on a port: %v", err)) + } + testSrv.Listener = l + } + + testSrv.Start() + defer testSrv.Close() + + // Setup VA. By providing the testSrv to setup the VA will use the + // testSrv's randomly assigned port as its HTTP port. + va, _ := setup(testSrv, "", nil, nil) + + var got string + _, _, _ = va.processHTTPValidation(ctx, tc.Ident, "/ok") + got = handler.host + if got != tc.want { + t.Errorf("Got host %#v, but want %#v", got, tc.want) + } + }) + } +} diff --git a/third-party/github.com/letsencrypt/boulder/va/proto/va.pb.go b/third-party/github.com/letsencrypt/boulder/va/proto/va.pb.go index 8e8ee1950..b65fe526a 100644 --- a/third-party/github.com/letsencrypt/boulder/va/proto/va.pb.go +++ b/third-party/github.com/letsencrypt/boulder/va/proto/va.pb.go @@ -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: va.proto @@ -12,6 +12,7 @@ import ( protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" + unsafe "unsafe" ) const ( @@ -22,23 +23,22 @@ const ( ) type IsCAAValidRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - // NOTE: Domain may be a name with a wildcard prefix (e.g. `*.example.com`) - Domain string `protobuf:"bytes,1,opt,name=domain,proto3" json:"domain,omitempty"` - ValidationMethod string `protobuf:"bytes,2,opt,name=validationMethod,proto3" json:"validationMethod,omitempty"` - AccountURIID int64 `protobuf:"varint,3,opt,name=accountURIID,proto3" json:"accountURIID,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + // NOTE: For DNS identifiers, the value may be a wildcard domain name (e.g. + // `*.example.com`). + Identifier *proto.Identifier `protobuf:"bytes,5,opt,name=identifier,proto3" json:"identifier,omitempty"` + ValidationMethod string `protobuf:"bytes,2,opt,name=validationMethod,proto3" json:"validationMethod,omitempty"` + AccountURIID int64 `protobuf:"varint,3,opt,name=accountURIID,proto3" json:"accountURIID,omitempty"` + AuthzID string `protobuf:"bytes,4,opt,name=authzID,proto3" json:"authzID,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *IsCAAValidRequest) Reset() { *x = IsCAAValidRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_va_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_va_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *IsCAAValidRequest) String() string { @@ -49,7 +49,7 @@ func (*IsCAAValidRequest) ProtoMessage() {} func (x *IsCAAValidRequest) ProtoReflect() protoreflect.Message { mi := &file_va_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) @@ -64,11 +64,11 @@ func (*IsCAAValidRequest) Descriptor() ([]byte, []int) { return file_va_proto_rawDescGZIP(), []int{0} } -func (x *IsCAAValidRequest) GetDomain() string { +func (x *IsCAAValidRequest) GetIdentifier() *proto.Identifier { if x != nil { - return x.Domain + return x.Identifier } - return "" + return nil } func (x *IsCAAValidRequest) GetValidationMethod() string { @@ -85,22 +85,28 @@ func (x *IsCAAValidRequest) GetAccountURIID() int64 { return 0 } +func (x *IsCAAValidRequest) GetAuthzID() string { + if x != nil { + return x.AuthzID + } + return "" +} + // If CAA is valid for the requested domain, the problem will be empty type IsCAAValidResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Problem *proto.ProblemDetails `protobuf:"bytes,1,opt,name=problem,proto3" json:"problem,omitempty"` + Perspective string `protobuf:"bytes,3,opt,name=perspective,proto3" json:"perspective,omitempty"` + Rir string `protobuf:"bytes,4,opt,name=rir,proto3" json:"rir,omitempty"` unknownFields protoimpl.UnknownFields - - Problem *proto.ProblemDetails `protobuf:"bytes,1,opt,name=problem,proto3" json:"problem,omitempty"` + sizeCache protoimpl.SizeCache } func (x *IsCAAValidResponse) Reset() { *x = IsCAAValidResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_va_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_va_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *IsCAAValidResponse) String() string { @@ -111,7 +117,7 @@ func (*IsCAAValidResponse) ProtoMessage() {} func (x *IsCAAValidResponse) ProtoReflect() protoreflect.Message { mi := &file_va_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) @@ -133,24 +139,35 @@ func (x *IsCAAValidResponse) GetProblem() *proto.ProblemDetails { return nil } -type PerformValidationRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields +func (x *IsCAAValidResponse) GetPerspective() string { + if x != nil { + return x.Perspective + } + return "" +} - Domain string `protobuf:"bytes,1,opt,name=domain,proto3" json:"domain,omitempty"` - Challenge *proto.Challenge `protobuf:"bytes,2,opt,name=challenge,proto3" json:"challenge,omitempty"` - Authz *AuthzMeta `protobuf:"bytes,3,opt,name=authz,proto3" json:"authz,omitempty"` - ExpectedKeyAuthorization string `protobuf:"bytes,4,opt,name=expectedKeyAuthorization,proto3" json:"expectedKeyAuthorization,omitempty"` +func (x *IsCAAValidResponse) GetRir() string { + if x != nil { + return x.Rir + } + return "" +} + +type PerformValidationRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Identifier *proto.Identifier `protobuf:"bytes,5,opt,name=identifier,proto3" json:"identifier,omitempty"` + Challenge *proto.Challenge `protobuf:"bytes,2,opt,name=challenge,proto3" json:"challenge,omitempty"` + Authz *AuthzMeta `protobuf:"bytes,3,opt,name=authz,proto3" json:"authz,omitempty"` + ExpectedKeyAuthorization string `protobuf:"bytes,4,opt,name=expectedKeyAuthorization,proto3" json:"expectedKeyAuthorization,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *PerformValidationRequest) Reset() { *x = PerformValidationRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_va_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_va_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *PerformValidationRequest) String() string { @@ -161,7 +178,7 @@ func (*PerformValidationRequest) ProtoMessage() {} func (x *PerformValidationRequest) ProtoReflect() protoreflect.Message { mi := &file_va_proto_msgTypes[2] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -176,11 +193,11 @@ func (*PerformValidationRequest) Descriptor() ([]byte, []int) { return file_va_proto_rawDescGZIP(), []int{2} } -func (x *PerformValidationRequest) GetDomain() string { +func (x *PerformValidationRequest) GetIdentifier() *proto.Identifier { if x != nil { - return x.Domain + return x.Identifier } - return "" + return nil } func (x *PerformValidationRequest) GetChallenge() *proto.Challenge { @@ -205,21 +222,18 @@ func (x *PerformValidationRequest) GetExpectedKeyAuthorization() string { } type AuthzMeta struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + RegID int64 `protobuf:"varint,2,opt,name=regID,proto3" json:"regID,omitempty"` unknownFields protoimpl.UnknownFields - - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - RegID int64 `protobuf:"varint,2,opt,name=regID,proto3" json:"regID,omitempty"` + sizeCache protoimpl.SizeCache } func (x *AuthzMeta) Reset() { *x = AuthzMeta{} - if protoimpl.UnsafeEnabled { - mi := &file_va_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_va_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *AuthzMeta) String() string { @@ -230,7 +244,7 @@ func (*AuthzMeta) ProtoMessage() {} func (x *AuthzMeta) ProtoReflect() protoreflect.Message { mi := &file_va_proto_msgTypes[3] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -260,21 +274,20 @@ func (x *AuthzMeta) GetRegID() int64 { } type ValidationResult struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Records []*proto.ValidationRecord `protobuf:"bytes,1,rep,name=records,proto3" json:"records,omitempty"` + Problem *proto.ProblemDetails `protobuf:"bytes,2,opt,name=problem,proto3" json:"problem,omitempty"` + Perspective string `protobuf:"bytes,3,opt,name=perspective,proto3" json:"perspective,omitempty"` + Rir string `protobuf:"bytes,4,opt,name=rir,proto3" json:"rir,omitempty"` unknownFields protoimpl.UnknownFields - - Records []*proto.ValidationRecord `protobuf:"bytes,1,rep,name=records,proto3" json:"records,omitempty"` - Problems *proto.ProblemDetails `protobuf:"bytes,2,opt,name=problems,proto3" json:"problems,omitempty"` + sizeCache protoimpl.SizeCache } func (x *ValidationResult) Reset() { *x = ValidationResult{} - if protoimpl.UnsafeEnabled { - mi := &file_va_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_va_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ValidationResult) String() string { @@ -285,7 +298,7 @@ func (*ValidationResult) ProtoMessage() {} func (x *ValidationResult) ProtoReflect() protoreflect.Message { mi := &file_va_proto_msgTypes[4] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -307,107 +320,135 @@ func (x *ValidationResult) GetRecords() []*proto.ValidationRecord { return nil } -func (x *ValidationResult) GetProblems() *proto.ProblemDetails { +func (x *ValidationResult) GetProblem() *proto.ProblemDetails { if x != nil { - return x.Problems + return x.Problem } return nil } +func (x *ValidationResult) GetPerspective() string { + if x != nil { + return x.Perspective + } + return "" +} + +func (x *ValidationResult) GetRir() string { + if x != nil { + return x.Rir + } + return "" +} + var File_va_proto protoreflect.FileDescriptor -var file_va_proto_rawDesc = []byte{ +var file_va_proto_rawDesc = string([]byte{ 0x0a, 0x08, 0x76, 0x61, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x02, 0x76, 0x61, 0x1a, 0x15, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x63, 0x6f, 0x72, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x7b, 0x0a, 0x11, 0x49, 0x73, 0x43, 0x41, 0x41, 0x56, 0x61, - 0x6c, 0x69, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x6f, - 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, - 0x69, 0x6e, 0x12, 0x2a, 0x0a, 0x10, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x76, 0x61, - 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x22, - 0x0a, 0x0c, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x55, 0x52, 0x49, 0x49, 0x44, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x55, 0x52, 0x49, - 0x49, 0x44, 0x22, 0x44, 0x0a, 0x12, 0x49, 0x73, 0x43, 0x41, 0x41, 0x56, 0x61, 0x6c, 0x69, 0x64, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2e, 0x0a, 0x07, 0x70, 0x72, 0x6f, 0x62, - 0x6c, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x63, 0x6f, 0x72, 0x65, - 0x2e, 0x50, 0x72, 0x6f, 0x62, 0x6c, 0x65, 0x6d, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x52, - 0x07, 0x70, 0x72, 0x6f, 0x62, 0x6c, 0x65, 0x6d, 0x22, 0xc2, 0x01, 0x0a, 0x18, 0x50, 0x65, 0x72, - 0x66, 0x6f, 0x72, 0x6d, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x2d, 0x0a, - 0x09, 0x63, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x0f, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x43, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, - 0x65, 0x52, 0x09, 0x63, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x12, 0x23, 0x0a, 0x05, - 0x61, 0x75, 0x74, 0x68, 0x7a, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x76, 0x61, - 0x2e, 0x41, 0x75, 0x74, 0x68, 0x7a, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x05, 0x61, 0x75, 0x74, 0x68, - 0x7a, 0x12, 0x3a, 0x0a, 0x18, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x4b, 0x65, 0x79, - 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x18, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x4b, 0x65, 0x79, - 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x31, 0x0a, - 0x09, 0x41, 0x75, 0x74, 0x68, 0x7a, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x65, - 0x67, 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x72, 0x65, 0x67, 0x49, 0x44, - 0x22, 0x76, 0x0a, 0x10, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xb5, 0x01, 0x0a, 0x11, 0x49, 0x73, 0x43, 0x41, 0x41, 0x56, + 0x61, 0x6c, 0x69, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x30, 0x0a, 0x0a, 0x69, + 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x10, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, + 0x72, 0x52, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x2a, 0x0a, + 0x10, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x74, 0x68, 0x6f, + 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x22, 0x0a, 0x0c, 0x61, 0x63, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x55, 0x52, 0x49, 0x49, 0x44, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x0c, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x55, 0x52, 0x49, 0x49, 0x44, 0x12, 0x18, 0x0a, + 0x07, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x49, 0x44, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, + 0x61, 0x75, 0x74, 0x68, 0x7a, 0x49, 0x44, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x22, 0x78, 0x0a, + 0x12, 0x49, 0x73, 0x43, 0x41, 0x41, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x2e, 0x0a, 0x07, 0x70, 0x72, 0x6f, 0x62, 0x6c, 0x65, 0x6d, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x50, 0x72, 0x6f, 0x62, + 0x6c, 0x65, 0x6d, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x52, 0x07, 0x70, 0x72, 0x6f, 0x62, + 0x6c, 0x65, 0x6d, 0x12, 0x20, 0x0a, 0x0b, 0x70, 0x65, 0x72, 0x73, 0x70, 0x65, 0x63, 0x74, 0x69, + 0x76, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x70, 0x65, 0x72, 0x73, 0x70, 0x65, + 0x63, 0x74, 0x69, 0x76, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x72, 0x69, 0x72, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x03, 0x72, 0x69, 0x72, 0x22, 0xe2, 0x01, 0x0a, 0x18, 0x50, 0x65, 0x72, 0x66, + 0x6f, 0x72, 0x6d, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x30, 0x0a, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, + 0x65, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, + 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x0a, 0x69, 0x64, 0x65, 0x6e, + 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x2d, 0x0a, 0x09, 0x63, 0x68, 0x61, 0x6c, 0x6c, 0x65, + 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x63, 0x6f, 0x72, 0x65, + 0x2e, 0x43, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x52, 0x09, 0x63, 0x68, 0x61, 0x6c, + 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x12, 0x23, 0x0a, 0x05, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x76, 0x61, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x7a, 0x4d, + 0x65, 0x74, 0x61, 0x52, 0x05, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x12, 0x3a, 0x0a, 0x18, 0x65, 0x78, + 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, + 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x18, 0x65, 0x78, + 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, + 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x22, 0x31, 0x0a, 0x09, + 0x41, 0x75, 0x74, 0x68, 0x7a, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x65, 0x67, + 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x72, 0x65, 0x67, 0x49, 0x44, 0x22, + 0xa8, 0x01, 0x0a, 0x10, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x30, 0x0a, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x72, - 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x12, 0x30, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x62, 0x6c, 0x65, - 0x6d, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, - 0x50, 0x72, 0x6f, 0x62, 0x6c, 0x65, 0x6d, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x52, 0x08, - 0x70, 0x72, 0x6f, 0x62, 0x6c, 0x65, 0x6d, 0x73, 0x32, 0x4f, 0x0a, 0x02, 0x56, 0x41, 0x12, 0x49, - 0x0a, 0x11, 0x50, 0x65, 0x72, 0x66, 0x6f, 0x72, 0x6d, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x2e, 0x76, 0x61, 0x2e, 0x50, 0x65, 0x72, 0x66, 0x6f, 0x72, 0x6d, - 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x14, 0x2e, 0x76, 0x61, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0x00, 0x32, 0x44, 0x0a, 0x03, 0x43, 0x41, 0x41, - 0x12, 0x3d, 0x0a, 0x0a, 0x49, 0x73, 0x43, 0x41, 0x41, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x12, 0x15, - 0x2e, 0x76, 0x61, 0x2e, 0x49, 0x73, 0x43, 0x41, 0x41, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x76, 0x61, 0x2e, 0x49, 0x73, 0x43, 0x41, 0x41, - 0x56, 0x61, 0x6c, 0x69, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 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, 0x76, 0x61, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x33, -} + 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x12, 0x2e, 0x0a, 0x07, 0x70, 0x72, 0x6f, 0x62, 0x6c, 0x65, + 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x50, + 0x72, 0x6f, 0x62, 0x6c, 0x65, 0x6d, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x52, 0x07, 0x70, + 0x72, 0x6f, 0x62, 0x6c, 0x65, 0x6d, 0x12, 0x20, 0x0a, 0x0b, 0x70, 0x65, 0x72, 0x73, 0x70, 0x65, + 0x63, 0x74, 0x69, 0x76, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x70, 0x65, 0x72, + 0x73, 0x70, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x72, 0x69, 0x72, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x72, 0x69, 0x72, 0x32, 0x43, 0x0a, 0x02, 0x56, 0x41, + 0x12, 0x3d, 0x0a, 0x05, 0x44, 0x6f, 0x44, 0x43, 0x56, 0x12, 0x1c, 0x2e, 0x76, 0x61, 0x2e, 0x50, + 0x65, 0x72, 0x66, 0x6f, 0x72, 0x6d, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x76, 0x61, 0x2e, 0x56, 0x61, 0x6c, + 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0x00, 0x32, + 0x3f, 0x0a, 0x03, 0x43, 0x41, 0x41, 0x12, 0x38, 0x0a, 0x05, 0x44, 0x6f, 0x43, 0x41, 0x41, 0x12, + 0x15, 0x2e, 0x76, 0x61, 0x2e, 0x49, 0x73, 0x43, 0x41, 0x41, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x76, 0x61, 0x2e, 0x49, 0x73, 0x43, 0x41, + 0x41, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, + 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, 0x76, 0x61, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, +}) var ( file_va_proto_rawDescOnce sync.Once - file_va_proto_rawDescData = file_va_proto_rawDesc + file_va_proto_rawDescData []byte ) func file_va_proto_rawDescGZIP() []byte { file_va_proto_rawDescOnce.Do(func() { - file_va_proto_rawDescData = protoimpl.X.CompressGZIP(file_va_proto_rawDescData) + file_va_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_va_proto_rawDesc), len(file_va_proto_rawDesc))) }) return file_va_proto_rawDescData } var file_va_proto_msgTypes = make([]protoimpl.MessageInfo, 5) -var file_va_proto_goTypes = []interface{}{ +var file_va_proto_goTypes = []any{ (*IsCAAValidRequest)(nil), // 0: va.IsCAAValidRequest (*IsCAAValidResponse)(nil), // 1: va.IsCAAValidResponse (*PerformValidationRequest)(nil), // 2: va.PerformValidationRequest (*AuthzMeta)(nil), // 3: va.AuthzMeta (*ValidationResult)(nil), // 4: va.ValidationResult - (*proto.ProblemDetails)(nil), // 5: core.ProblemDetails - (*proto.Challenge)(nil), // 6: core.Challenge - (*proto.ValidationRecord)(nil), // 7: core.ValidationRecord + (*proto.Identifier)(nil), // 5: core.Identifier + (*proto.ProblemDetails)(nil), // 6: core.ProblemDetails + (*proto.Challenge)(nil), // 7: core.Challenge + (*proto.ValidationRecord)(nil), // 8: core.ValidationRecord } var file_va_proto_depIdxs = []int32{ - 5, // 0: va.IsCAAValidResponse.problem:type_name -> core.ProblemDetails - 6, // 1: va.PerformValidationRequest.challenge:type_name -> core.Challenge - 3, // 2: va.PerformValidationRequest.authz:type_name -> va.AuthzMeta - 7, // 3: va.ValidationResult.records:type_name -> core.ValidationRecord - 5, // 4: va.ValidationResult.problems:type_name -> core.ProblemDetails - 2, // 5: va.VA.PerformValidation:input_type -> va.PerformValidationRequest - 0, // 6: va.CAA.IsCAAValid:input_type -> va.IsCAAValidRequest - 4, // 7: va.VA.PerformValidation:output_type -> va.ValidationResult - 1, // 8: va.CAA.IsCAAValid:output_type -> va.IsCAAValidResponse - 7, // [7:9] is the sub-list for method output_type - 5, // [5:7] is the sub-list for method input_type - 5, // [5:5] is the sub-list for extension type_name - 5, // [5:5] is the sub-list for extension extendee - 0, // [0:5] is the sub-list for field type_name + 5, // 0: va.IsCAAValidRequest.identifier:type_name -> core.Identifier + 6, // 1: va.IsCAAValidResponse.problem:type_name -> core.ProblemDetails + 5, // 2: va.PerformValidationRequest.identifier:type_name -> core.Identifier + 7, // 3: va.PerformValidationRequest.challenge:type_name -> core.Challenge + 3, // 4: va.PerformValidationRequest.authz:type_name -> va.AuthzMeta + 8, // 5: va.ValidationResult.records:type_name -> core.ValidationRecord + 6, // 6: va.ValidationResult.problem:type_name -> core.ProblemDetails + 2, // 7: va.VA.DoDCV:input_type -> va.PerformValidationRequest + 0, // 8: va.CAA.DoCAA:input_type -> va.IsCAAValidRequest + 4, // 9: va.VA.DoDCV:output_type -> va.ValidationResult + 1, // 10: va.CAA.DoCAA:output_type -> va.IsCAAValidResponse + 9, // [9:11] is the sub-list for method output_type + 7, // [7:9] is the sub-list for method input_type + 7, // [7:7] is the sub-list for extension type_name + 7, // [7:7] is the sub-list for extension extendee + 0, // [0:7] is the sub-list for field type_name } func init() { file_va_proto_init() } @@ -415,73 +456,11 @@ func file_va_proto_init() { if File_va_proto != nil { return } - if !protoimpl.UnsafeEnabled { - file_va_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*IsCAAValidRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_va_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*IsCAAValidResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_va_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PerformValidationRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_va_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*AuthzMeta); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_va_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ValidationResult); 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_va_proto_rawDesc, + RawDescriptor: unsafe.Slice(unsafe.StringData(file_va_proto_rawDesc), len(file_va_proto_rawDesc)), NumEnums: 0, NumMessages: 5, NumExtensions: 0, @@ -492,7 +471,6 @@ func file_va_proto_init() { MessageInfos: file_va_proto_msgTypes, }.Build() File_va_proto = out.File - file_va_proto_rawDesc = nil file_va_proto_goTypes = nil file_va_proto_depIdxs = nil } diff --git a/third-party/github.com/letsencrypt/boulder/va/proto/va.proto b/third-party/github.com/letsencrypt/boulder/va/proto/va.proto index 76a37320a..7fba73f6e 100644 --- a/third-party/github.com/letsencrypt/boulder/va/proto/va.proto +++ b/third-party/github.com/letsencrypt/boulder/va/proto/va.proto @@ -6,27 +6,35 @@ option go_package = "github.com/letsencrypt/boulder/va/proto"; import "core/proto/core.proto"; service VA { - rpc PerformValidation(PerformValidationRequest) returns (ValidationResult) {} + rpc DoDCV(PerformValidationRequest) returns (ValidationResult) {} } service CAA { - rpc IsCAAValid(IsCAAValidRequest) returns (IsCAAValidResponse) {} + rpc DoCAA(IsCAAValidRequest) returns (IsCAAValidResponse) {} } message IsCAAValidRequest { - // NOTE: Domain may be a name with a wildcard prefix (e.g. `*.example.com`) - string domain = 1; + // Next unused field number: 6 + reserved 1; // Previously domain + // NOTE: For DNS identifiers, the value may be a wildcard domain name (e.g. + // `*.example.com`). + core.Identifier identifier = 5; string validationMethod = 2; int64 accountURIID = 3; + string authzID = 4; } // If CAA is valid for the requested domain, the problem will be empty message IsCAAValidResponse { core.ProblemDetails problem = 1; + string perspective = 3; + string rir = 4; } message PerformValidationRequest { - string domain = 1; + // Next unused field number: 6 + reserved 1; // Previously dnsName + core.Identifier identifier = 5; core.Challenge challenge = 2; AuthzMeta authz = 3; string expectedKeyAuthorization = 4; @@ -39,5 +47,7 @@ message AuthzMeta { message ValidationResult { repeated core.ValidationRecord records = 1; - core.ProblemDetails problems = 2; + core.ProblemDetails problem = 2; + string perspective = 3; + string rir = 4; } diff --git a/third-party/github.com/letsencrypt/boulder/va/proto/va_grpc.pb.go b/third-party/github.com/letsencrypt/boulder/va/proto/va_grpc.pb.go index b7c3df4f3..274b7a166 100644 --- a/third-party/github.com/letsencrypt/boulder/va/proto/va_grpc.pb.go +++ b/third-party/github.com/letsencrypt/boulder/va/proto/va_grpc.pb.go @@ -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: va.proto @@ -19,14 +19,14 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - VA_PerformValidation_FullMethodName = "/va.VA/PerformValidation" + VA_DoDCV_FullMethodName = "/va.VA/DoDCV" ) // VAClient is the client API for VA 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. type VAClient interface { - PerformValidation(ctx context.Context, in *PerformValidationRequest, opts ...grpc.CallOption) (*ValidationResult, error) + DoDCV(ctx context.Context, in *PerformValidationRequest, opts ...grpc.CallOption) (*ValidationResult, error) } type vAClient struct { @@ -37,10 +37,10 @@ func NewVAClient(cc grpc.ClientConnInterface) VAClient { return &vAClient{cc} } -func (c *vAClient) PerformValidation(ctx context.Context, in *PerformValidationRequest, opts ...grpc.CallOption) (*ValidationResult, error) { +func (c *vAClient) DoDCV(ctx context.Context, in *PerformValidationRequest, opts ...grpc.CallOption) (*ValidationResult, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(ValidationResult) - err := c.cc.Invoke(ctx, VA_PerformValidation_FullMethodName, in, out, cOpts...) + err := c.cc.Invoke(ctx, VA_DoDCV_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -49,20 +49,24 @@ func (c *vAClient) PerformValidation(ctx context.Context, in *PerformValidationR // VAServer is the server API for VA service. // All implementations must embed UnimplementedVAServer -// for forward compatibility +// for forward compatibility. type VAServer interface { - PerformValidation(context.Context, *PerformValidationRequest) (*ValidationResult, error) + DoDCV(context.Context, *PerformValidationRequest) (*ValidationResult, error) mustEmbedUnimplementedVAServer() } -// UnimplementedVAServer must be embedded to have forward compatible implementations. -type UnimplementedVAServer struct { -} +// UnimplementedVAServer 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 UnimplementedVAServer struct{} -func (UnimplementedVAServer) PerformValidation(context.Context, *PerformValidationRequest) (*ValidationResult, error) { - return nil, status.Errorf(codes.Unimplemented, "method PerformValidation not implemented") +func (UnimplementedVAServer) DoDCV(context.Context, *PerformValidationRequest) (*ValidationResult, error) { + return nil, status.Errorf(codes.Unimplemented, "method DoDCV not implemented") } func (UnimplementedVAServer) mustEmbedUnimplementedVAServer() {} +func (UnimplementedVAServer) testEmbeddedByValue() {} // UnsafeVAServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to VAServer will @@ -72,23 +76,30 @@ type UnsafeVAServer interface { } func RegisterVAServer(s grpc.ServiceRegistrar, srv VAServer) { + // If the following call pancis, it indicates UnimplementedVAServer 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(&VA_ServiceDesc, srv) } -func _VA_PerformValidation_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { +func _VA_DoDCV_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(PerformValidationRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { - return srv.(VAServer).PerformValidation(ctx, in) + return srv.(VAServer).DoDCV(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: VA_PerformValidation_FullMethodName, + FullMethod: VA_DoDCV_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(VAServer).PerformValidation(ctx, req.(*PerformValidationRequest)) + return srv.(VAServer).DoDCV(ctx, req.(*PerformValidationRequest)) } return interceptor(ctx, in, info, handler) } @@ -101,8 +112,8 @@ var VA_ServiceDesc = grpc.ServiceDesc{ HandlerType: (*VAServer)(nil), Methods: []grpc.MethodDesc{ { - MethodName: "PerformValidation", - Handler: _VA_PerformValidation_Handler, + MethodName: "DoDCV", + Handler: _VA_DoDCV_Handler, }, }, Streams: []grpc.StreamDesc{}, @@ -110,14 +121,14 @@ var VA_ServiceDesc = grpc.ServiceDesc{ } const ( - CAA_IsCAAValid_FullMethodName = "/va.CAA/IsCAAValid" + CAA_DoCAA_FullMethodName = "/va.CAA/DoCAA" ) // CAAClient is the client API for CAA 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. type CAAClient interface { - IsCAAValid(ctx context.Context, in *IsCAAValidRequest, opts ...grpc.CallOption) (*IsCAAValidResponse, error) + DoCAA(ctx context.Context, in *IsCAAValidRequest, opts ...grpc.CallOption) (*IsCAAValidResponse, error) } type cAAClient struct { @@ -128,10 +139,10 @@ func NewCAAClient(cc grpc.ClientConnInterface) CAAClient { return &cAAClient{cc} } -func (c *cAAClient) IsCAAValid(ctx context.Context, in *IsCAAValidRequest, opts ...grpc.CallOption) (*IsCAAValidResponse, error) { +func (c *cAAClient) DoCAA(ctx context.Context, in *IsCAAValidRequest, opts ...grpc.CallOption) (*IsCAAValidResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(IsCAAValidResponse) - err := c.cc.Invoke(ctx, CAA_IsCAAValid_FullMethodName, in, out, cOpts...) + err := c.cc.Invoke(ctx, CAA_DoCAA_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -140,20 +151,24 @@ func (c *cAAClient) IsCAAValid(ctx context.Context, in *IsCAAValidRequest, opts // CAAServer is the server API for CAA service. // All implementations must embed UnimplementedCAAServer -// for forward compatibility +// for forward compatibility. type CAAServer interface { - IsCAAValid(context.Context, *IsCAAValidRequest) (*IsCAAValidResponse, error) + DoCAA(context.Context, *IsCAAValidRequest) (*IsCAAValidResponse, error) mustEmbedUnimplementedCAAServer() } -// UnimplementedCAAServer must be embedded to have forward compatible implementations. -type UnimplementedCAAServer struct { -} +// UnimplementedCAAServer 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 UnimplementedCAAServer struct{} -func (UnimplementedCAAServer) IsCAAValid(context.Context, *IsCAAValidRequest) (*IsCAAValidResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method IsCAAValid not implemented") +func (UnimplementedCAAServer) DoCAA(context.Context, *IsCAAValidRequest) (*IsCAAValidResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method DoCAA not implemented") } func (UnimplementedCAAServer) mustEmbedUnimplementedCAAServer() {} +func (UnimplementedCAAServer) testEmbeddedByValue() {} // UnsafeCAAServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to CAAServer will @@ -163,23 +178,30 @@ type UnsafeCAAServer interface { } func RegisterCAAServer(s grpc.ServiceRegistrar, srv CAAServer) { + // If the following call pancis, it indicates UnimplementedCAAServer 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(&CAA_ServiceDesc, srv) } -func _CAA_IsCAAValid_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { +func _CAA_DoCAA_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(IsCAAValidRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { - return srv.(CAAServer).IsCAAValid(ctx, in) + return srv.(CAAServer).DoCAA(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: CAA_IsCAAValid_FullMethodName, + FullMethod: CAA_DoCAA_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(CAAServer).IsCAAValid(ctx, req.(*IsCAAValidRequest)) + return srv.(CAAServer).DoCAA(ctx, req.(*IsCAAValidRequest)) } return interceptor(ctx, in, info, handler) } @@ -192,8 +214,8 @@ var CAA_ServiceDesc = grpc.ServiceDesc{ HandlerType: (*CAAServer)(nil), Methods: []grpc.MethodDesc{ { - MethodName: "IsCAAValid", - Handler: _CAA_IsCAAValid_Handler, + MethodName: "DoCAA", + Handler: _CAA_DoCAA_Handler, }, }, Streams: []grpc.StreamDesc{}, diff --git a/third-party/github.com/letsencrypt/boulder/va/tlsalpn.go b/third-party/github.com/letsencrypt/boulder/va/tlsalpn.go index f4a23e793..d4ac4cc31 100644 --- a/third-party/github.com/letsencrypt/boulder/va/tlsalpn.go +++ b/third-party/github.com/letsencrypt/boulder/va/tlsalpn.go @@ -13,9 +13,12 @@ import ( "errors" "fmt" "net" + "net/netip" "strconv" "strings" + "github.com/miekg/dns" + "github.com/letsencrypt/boulder/core" berrors "github.com/letsencrypt/boulder/errors" "github.com/letsencrypt/boulder/identifier" @@ -58,28 +61,42 @@ func certAltNames(cert *x509.Certificate) []string { func (va *ValidationAuthorityImpl) tryGetChallengeCert( ctx context.Context, - identifier identifier.ACMEIdentifier, - tlsConfig *tls.Config, + ident identifier.ACMEIdentifier, ) (*x509.Certificate, *tls.ConnectionState, core.ValidationRecord, error) { - - allAddrs, resolvers, err := va.getAddrs(ctx, identifier.Value) validationRecord := core.ValidationRecord{ - Hostname: identifier.Value, - AddressesResolved: allAddrs, - Port: strconv.Itoa(va.tlsPort), - ResolverAddrs: resolvers, + Hostname: ident.Value, + Port: strconv.Itoa(va.tlsPort), } - if err != nil { - return nil, nil, validationRecord, err + + var addrs []netip.Addr + switch ident.Type { + case identifier.TypeDNS: + // Resolve IP addresses for the identifier + dnsAddrs, dnsResolvers, err := va.getAddrs(ctx, ident.Value) + if err != nil { + return nil, nil, validationRecord, err + } + addrs, validationRecord.ResolverAddrs = dnsAddrs, dnsResolvers + validationRecord.AddressesResolved = addrs + case identifier.TypeIP: + netIP, err := netip.ParseAddr(ident.Value) + if err != nil { + return nil, nil, validationRecord, fmt.Errorf("can't parse IP address %q: %s", ident.Value, err) + } + addrs = []netip.Addr{netIP} + default: + // This should never happen. The calling function should check the + // identifier type. + return nil, nil, validationRecord, fmt.Errorf("unknown identifier type: %s", ident.Type) } // Split the available addresses into v4 and v6 addresses - v4, v6 := availableAddresses(allAddrs) + v4, v6 := availableAddresses(addrs) addresses := append(v4, v6...) // This shouldn't happen, but be defensive about it anyway if len(addresses) < 1 { - return nil, nil, validationRecord, berrors.MalformedError("no IP addresses found for %q", identifier.Value) + return nil, nil, validationRecord, berrors.MalformedError("no IP addresses found for %q", ident.Value) } // If there is at least one IPv6 address then try it first @@ -87,7 +104,7 @@ func (va *ValidationAuthorityImpl) tryGetChallengeCert( address := net.JoinHostPort(v6[0].String(), validationRecord.Port) validationRecord.AddressUsed = v6[0] - cert, cs, err := va.getChallengeCert(ctx, address, identifier, tlsConfig) + cert, cs, err := va.getChallengeCert(ctx, address, ident) // If there is no problem, return immediately if err == nil { @@ -114,33 +131,68 @@ func (va *ValidationAuthorityImpl) tryGetChallengeCert( // talking to the first IPv6 address, try the first IPv4 address validationRecord.AddressUsed = v4[0] address := net.JoinHostPort(v4[0].String(), validationRecord.Port) - cert, cs, err := va.getChallengeCert(ctx, address, identifier, tlsConfig) + cert, cs, err := va.getChallengeCert(ctx, address, ident) return cert, cs, validationRecord, err } func (va *ValidationAuthorityImpl) getChallengeCert( ctx context.Context, hostPort string, - identifier identifier.ACMEIdentifier, - config *tls.Config, + ident identifier.ACMEIdentifier, ) (*x509.Certificate, *tls.ConnectionState, error) { - va.log.Info(fmt.Sprintf("%s [%s] Attempting to validate for %s %s", core.ChallengeTypeTLSALPN01, identifier, hostPort, config.ServerName)) - // We expect a self-signed challenge certificate, do not verify it here. - config.InsecureSkipVerify = true + var serverName string + switch ident.Type { + case identifier.TypeDNS: + serverName = ident.Value + case identifier.TypeIP: + reverseIP, err := dns.ReverseAddr(ident.Value) + if err != nil { + va.log.Infof("%s Failed to parse IP address %s.", core.ChallengeTypeTLSALPN01, ident.Value) + return nil, nil, fmt.Errorf("failed to parse IP address") + } + serverName = reverseIP + default: + // This should never happen. The calling function should check the + // identifier type. + va.log.Infof("%s Unknown identifier type '%s' for %s.", core.ChallengeTypeTLSALPN01, ident.Type, ident.Value) + return nil, nil, fmt.Errorf("unknown identifier type: %s", ident.Type) + } + + va.log.Info(fmt.Sprintf("%s [%s] Attempting to validate for %s %s", core.ChallengeTypeTLSALPN01, ident, hostPort, serverName)) dialCtx, cancel := context.WithTimeout(ctx, va.singleDialTimeout) defer cancel() - dialer := &tls.Dialer{Config: config} + dialer := &tls.Dialer{Config: &tls.Config{ + MinVersion: tls.VersionTLS12, + NextProtos: []string{ACMETLS1Protocol}, + ServerName: serverName, + // We expect a self-signed challenge certificate, do not verify it here. + InsecureSkipVerify: true, + }} + + // This is a backstop check to avoid connecting to reserved IP addresses. + // They should have been caught and excluded by `bdns.LookupHost`. + host, _, err := net.SplitHostPort(hostPort) + if err != nil { + return nil, nil, err + } + hostIP, _ := netip.ParseAddr(host) + if (hostIP != netip.Addr{}) { + err = va.isReservedIPFunc(hostIP) + if err != nil { + return nil, nil, err + } + } + conn, err := dialer.DialContext(dialCtx, "tcp", hostPort) if err != nil { - va.log.Infof("%s connection failure for %s. err=[%#v] errStr=[%s]", core.ChallengeTypeTLSALPN01, identifier, err, err) - host, _, splitErr := net.SplitHostPort(hostPort) - if splitErr == nil && net.ParseIP(host) != nil { + va.log.Infof("%s connection failure for %s. err=[%#v] errStr=[%s]", core.ChallengeTypeTLSALPN01, ident, err, err) + if (hostIP != netip.Addr{}) { // Wrap the validation error and the IP of the remote host in an // IPError so we can display the IP in the problem details returned // to the client. - return nil, nil, ipError{net.ParseIP(host), err} + return nil, nil, ipError{hostIP, err} } return nil, nil, err } @@ -150,36 +202,69 @@ func (va *ValidationAuthorityImpl) getChallengeCert( cs := conn.(*tls.Conn).ConnectionState() certs := cs.PeerCertificates if len(certs) == 0 { - va.log.Infof("%s challenge for %s resulted in no certificates", core.ChallengeTypeTLSALPN01, identifier.Value) + va.log.Infof("%s challenge for %s resulted in no certificates", core.ChallengeTypeTLSALPN01, ident.Value) return nil, nil, berrors.UnauthorizedError("No certs presented for %s challenge", core.ChallengeTypeTLSALPN01) } for i, cert := range certs { va.log.AuditInfof("%s challenge for %s received certificate (%d of %d): cert=[%s]", - core.ChallengeTypeTLSALPN01, identifier.Value, i+1, len(certs), hex.EncodeToString(cert.Raw)) + core.ChallengeTypeTLSALPN01, ident.Value, i+1, len(certs), hex.EncodeToString(cert.Raw)) } return certs[0], &cs, nil } -func checkExpectedSAN(cert *x509.Certificate, name identifier.ACMEIdentifier) error { - if len(cert.DNSNames) != 1 { - return errors.New("wrong number of dNSNames") +func checkExpectedSAN(cert *x509.Certificate, ident identifier.ACMEIdentifier) error { + var expectedSANBytes []byte + switch ident.Type { + case identifier.TypeDNS: + if len(cert.DNSNames) != 1 || len(cert.IPAddresses) != 0 { + return errors.New("wrong number of identifiers") + } + if !strings.EqualFold(cert.DNSNames[0], ident.Value) { + return errors.New("identifier does not match expected identifier") + } + bytes, err := asn1.Marshal([]asn1.RawValue{ + {Tag: 2, Class: 2, Bytes: []byte(ident.Value)}, + }) + if err != nil { + return fmt.Errorf("composing SAN extension: %w", err) + } + expectedSANBytes = bytes + case identifier.TypeIP: + if len(cert.IPAddresses) != 1 || len(cert.DNSNames) != 0 { + return errors.New("wrong number of identifiers") + } + if !cert.IPAddresses[0].Equal(net.ParseIP(ident.Value)) { + return errors.New("identifier does not match expected identifier") + } + netipAddr, err := netip.ParseAddr(ident.Value) + if err != nil { + return fmt.Errorf("parsing IP address identifier: %w", err) + } + netipBytes, err := netipAddr.MarshalBinary() + if err != nil { + return fmt.Errorf("marshalling IP address identifier: %w", err) + } + bytes, err := asn1.Marshal([]asn1.RawValue{ + {Tag: 7, Class: 2, Bytes: netipBytes}, + }) + if err != nil { + return fmt.Errorf("composing SAN extension: %w", err) + } + expectedSANBytes = bytes + default: + // This should never happen. The calling function should check the + // identifier type. + return fmt.Errorf("unknown identifier type: %s", ident.Type) } for _, ext := range cert.Extensions { if IdCeSubjectAltName.Equal(ext.Id) { - expectedSANs, err := asn1.Marshal([]asn1.RawValue{ - {Tag: 2, Class: 2, Bytes: []byte(cert.DNSNames[0])}, - }) - if err != nil || !bytes.Equal(expectedSANs, ext.Value) { + if !bytes.Equal(ext.Value, expectedSANBytes) { return errors.New("SAN extension does not match expected bytes") } } } - if !strings.EqualFold(cert.DNSNames[0], name.Value) { - return errors.New("dNSName does not match expected identifier") - } - return nil } @@ -205,23 +290,19 @@ func checkAcceptableExtensions(exts []pkix.Extension, requiredOIDs []asn1.Object return nil } -func (va *ValidationAuthorityImpl) validateTLSALPN01(ctx context.Context, identifier identifier.ACMEIdentifier, keyAuthorization string) ([]core.ValidationRecord, error) { - if identifier.Type != "dns" { - va.log.Info(fmt.Sprintf("Identifier type for TLS-ALPN-01 was not DNS: %s", identifier)) - return nil, berrors.MalformedError("Identifier type for TLS-ALPN-01 was not DNS") +func (va *ValidationAuthorityImpl) validateTLSALPN01(ctx context.Context, ident identifier.ACMEIdentifier, keyAuthorization string) ([]core.ValidationRecord, error) { + if ident.Type != identifier.TypeDNS && ident.Type != identifier.TypeIP { + va.log.Info(fmt.Sprintf("Identifier type for TLS-ALPN-01 challenge was not DNS or IP: %s", ident)) + return nil, berrors.MalformedError("Identifier type for TLS-ALPN-01 challenge was not DNS or IP") } - cert, cs, tvr, problem := va.tryGetChallengeCert(ctx, identifier, &tls.Config{ - MinVersion: tls.VersionTLS12, - NextProtos: []string{ACMETLS1Protocol}, - ServerName: identifier.Value, - }) + cert, cs, tvr, err := va.tryGetChallengeCert(ctx, ident) // Copy the single validationRecord into the slice that we have to return, and // get a reference to it so we can modify it if we have to. validationRecords := []core.ValidationRecord{tvr} validationRecord := &validationRecords[0] - if problem != nil { - return validationRecords, problem + if err != nil { + return validationRecords, err } if cs.NegotiatedProtocol != ACMETLS1Protocol { @@ -237,11 +318,11 @@ func (va *ValidationAuthorityImpl) validateTLSALPN01(ctx context.Context, identi return berrors.UnauthorizedError( "Incorrect validation certificate for %s challenge. "+ "Requested %s from %s. %s", - core.ChallengeTypeTLSALPN01, identifier.Value, hostPort, msg) + core.ChallengeTypeTLSALPN01, ident.Value, hostPort, msg) } // The certificate must be self-signed. - err := cert.CheckSignature(cert.SignatureAlgorithm, cert.RawTBSCertificate, cert.Signature) + err = cert.CheckSignature(cert.SignatureAlgorithm, cert.RawTBSCertificate, cert.Signature) if err != nil || !bytes.Equal(cert.RawSubject, cert.RawIssuer) { return validationRecords, badCertErr( "Received certificate which is not self-signed.") @@ -259,8 +340,8 @@ func (va *ValidationAuthorityImpl) validateTLSALPN01(ctx context.Context, identi } // The certificate returned must have a subjectAltName extension containing - // only the dNSName being validated and no other entries. - err = checkExpectedSAN(cert, identifier) + // only the identifier being validated and no other entries. + err = checkExpectedSAN(cert, ident) if err != nil { names := strings.Join(certAltNames(cert), ", ") return validationRecords, badCertErr( @@ -289,10 +370,6 @@ func (va *ValidationAuthorityImpl) validateTLSALPN01(ctx context.Context, identi hex.EncodeToString(h[:]), )) } - // We were successful, so record the negotiated key exchange mechanism in - // the validationRecord. - // TODO(#7321): Remove this when we have collected enough data. - validationRecord.UsedRSAKEX = usedRSAKEX(cs.CipherSuite) return validationRecords, nil } } diff --git a/third-party/github.com/letsencrypt/boulder/va/tlsalpn_test.go b/third-party/github.com/letsencrypt/boulder/va/tlsalpn_test.go index 9e11bd319..d33a086fe 100644 --- a/third-party/github.com/letsencrypt/boulder/va/tlsalpn_test.go +++ b/third-party/github.com/letsencrypt/boulder/va/tlsalpn_test.go @@ -2,6 +2,8 @@ package va import ( "context" + "crypto/ecdsa" + "crypto/elliptic" "crypto/rand" "crypto/sha256" "crypto/tls" @@ -14,8 +16,8 @@ import ( "net" "net/http" "net/http/httptest" + "net/netip" "net/url" - "strconv" "strings" "testing" "time" @@ -29,8 +31,26 @@ import ( "github.com/letsencrypt/boulder/test" ) -func tlsCertTemplate(names []string) *x509.Certificate { - return &x509.Certificate{ +// acmeExtension returns the ACME TLS-ALPN-01 extension for the given key +// authorization. The OID can also be changed for the sake of testing. +func acmeExtension(oid asn1.ObjectIdentifier, keyAuthorization string) pkix.Extension { + shasum := sha256.Sum256([]byte(keyAuthorization)) + encHash, _ := asn1.Marshal(shasum[:]) + return pkix.Extension{ + Id: oid, + Critical: true, + Value: encHash, + } +} + +// testACMEExt is the ACME TLS-ALPN-01 extension with the default OID and +// key authorization used in most tests. +var testACMEExt = acmeExtension(IdPeAcmeIdentifier, expectedKeyAuthorization) + +// testTLSCert returns a ready-to-use self-signed certificate with the given +// SANs and Extensions. It generates a new ECDSA key on each call. +func testTLSCert(names []string, ips []net.IP, extensions []pkix.Extension) *tls.Certificate { + template := &x509.Certificate{ SerialNumber: big.NewInt(1337), Subject: pkix.Name{ Organization: []string{"tests"}, @@ -38,44 +58,33 @@ func tlsCertTemplate(names []string) *x509.Certificate { NotBefore: time.Now(), NotAfter: time.Now().AddDate(0, 0, 1), - KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + KeyUsage: x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, BasicConstraintsValid: true, - DNSNames: names, + DNSNames: names, + IPAddresses: ips, + ExtraExtensions: extensions, } -} + key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + certBytes, _ := x509.CreateCertificate(rand.Reader, template, template, key.Public(), key) -func makeACert(names []string) *tls.Certificate { - template := tlsCertTemplate(names) - certBytes, _ := x509.CreateCertificate(rand.Reader, template, template, &TheKey.PublicKey, &TheKey) return &tls.Certificate{ Certificate: [][]byte{certBytes}, - PrivateKey: &TheKey, + PrivateKey: key, } } -// tlssniSrvWithNames is kept around for the use of TestValidateTLSALPN01UnawareSrv -func tlssniSrvWithNames(t *testing.T, names ...string) *httptest.Server { - t.Helper() - - cert := makeACert(names) - tlsConfig := &tls.Config{ - Certificates: []tls.Certificate{*cert}, - ClientAuth: tls.NoClientCert, - GetCertificate: func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { - return cert, nil - }, - NextProtos: []string{"http/1.1"}, - } - - hs := httptest.NewUnstartedServer(http.DefaultServeMux) - hs.TLS = tlsConfig - hs.StartTLS() - return hs +// testACMECert returns a certificate with the correctly-formed ACME TLS-ALPN-01 +// extension with our default test values. Use acmeExtension and testCert if you +// need to customize the contents of that extension. +func testACMECert(names []string) *tls.Certificate { + return testTLSCert(names, nil, []pkix.Extension{testACMEExt}) } -func tlsalpn01SrvWithCert(t *testing.T, acmeCert *tls.Certificate, tlsVersion uint16) *httptest.Server { +// tlsalpn01SrvWithCert creates a test server which will present the given +// certificate when asked to do a tls-alpn-01 handshake. +func tlsalpn01SrvWithCert(t *testing.T, acmeCert *tls.Certificate, tlsVersion uint16, ipv6 bool) *httptest.Server { t.Helper() tlsConfig := &tls.Config{ @@ -96,68 +105,32 @@ func tlsalpn01SrvWithCert(t *testing.T, acmeCert *tls.Certificate, tlsVersion ui _ = conn.Close() }, } + if ipv6 { + l, err := net.Listen("tcp", "[::1]:0") + if err != nil { + panic(fmt.Sprintf("httptest: failed to listen on a port: %v", err)) + } + hs.Listener = l + } hs.StartTLS() return hs } -func tlsalpn01Srv( - t *testing.T, - keyAuthorization string, - oid asn1.ObjectIdentifier, - tlsVersion uint16, - names ...string) (*httptest.Server, error) { - template := tlsCertTemplate(names) - - shasum := sha256.Sum256([]byte(keyAuthorization)) - encHash, err := asn1.Marshal(shasum[:]) - if err != nil { - return nil, err - } - acmeExtension := pkix.Extension{ - Id: oid, - Critical: true, - Value: encHash, - } - template.ExtraExtensions = []pkix.Extension{acmeExtension} - - certBytes, err := x509.CreateCertificate(rand.Reader, template, template, &TheKey.PublicKey, &TheKey) - if err != nil { - return nil, err - } - - acmeCert := &tls.Certificate{ - Certificate: [][]byte{certBytes}, - PrivateKey: &TheKey, - } - - return tlsalpn01SrvWithCert(t, acmeCert, tlsVersion), nil -} - -func TestTLSALPN01FailIP(t *testing.T) { - hs, err := tlsalpn01Srv(t, expectedKeyAuthorization, IdPeAcmeIdentifier, 0, "expected") - test.AssertNotError(t, err, "Error creating test server") - - va, _ := setup(hs, 0, "", nil, nil) - - port := getPort(hs) - _, err = va.validateTLSALPN01(ctx, identifier.ACMEIdentifier{ - Type: identifier.IdentifierType("ip"), - Value: net.JoinHostPort("127.0.0.1", strconv.Itoa(port)), - }, expectedKeyAuthorization) - if err == nil { - t.Fatalf("IdentifierType IP shouldn't have worked.") - } - prob := detailedError(err) - test.AssertEquals(t, prob.Type, probs.MalformedProblem) +// testTLSALPN01Srv creates a test server with all default values, for tests +// that don't need to customize specific names or extensions in the certificate +// served by the TLS server. +func testTLSALPN01Srv(t *testing.T) *httptest.Server { + return tlsalpn01SrvWithCert(t, testACMECert([]string{"expected"}), 0, false) } func slowTLSSrv() *httptest.Server { + cert := testTLSCert([]string{"nomatter"}, nil, nil) server := httptest.NewUnstartedServer(http.DefaultServeMux) server.TLS = &tls.Config{ NextProtos: []string{"http/1.1", ACMETLS1Protocol}, GetCertificate: func(*tls.ClientHelloInfo) (*tls.Certificate, error) { time.Sleep(100 * time.Millisecond) - return makeACert([]string{"nomatter"}), nil + return cert, nil }, } server.StartTLS() @@ -166,14 +139,14 @@ func slowTLSSrv() *httptest.Server { func TestTLSALPNTimeoutAfterConnect(t *testing.T) { hs := slowTLSSrv() - va, _ := setup(hs, 0, "", nil, nil) + va, _ := setup(hs, "", nil, nil) timeout := 50 * time.Millisecond ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() started := time.Now() - _, err := va.validateTLSALPN01(ctx, dnsi("slow.server"), expectedKeyAuthorization) + _, err := va.validateTLSALPN01(ctx, identifier.NewDNS("slow.server"), expectedKeyAuthorization) if err == nil { t.Fatalf("Validation should've failed") } @@ -203,7 +176,7 @@ func TestTLSALPNTimeoutAfterConnect(t *testing.T) { func TestTLSALPN01DialTimeout(t *testing.T) { hs := slowTLSSrv() - va, _ := setup(hs, 0, "", nil, dnsMockReturnsUnroutable{&bdns.MockClient{}}) + va, _ := setup(hs, "", nil, dnsMockReturnsUnroutable{&bdns.MockClient{}}) started := time.Now() timeout := 50 * time.Millisecond @@ -216,7 +189,7 @@ func TestTLSALPN01DialTimeout(t *testing.T) { // that, just retry until we get something other than "Network unreachable". var err error for range 20 { - _, err = va.validateTLSALPN01(ctx, dnsi("unroutable.invalid"), expectedKeyAuthorization) + _, err = va.validateTLSALPN01(ctx, identifier.NewDNS("unroutable.invalid"), expectedKeyAuthorization) if err != nil && strings.Contains(err.Error(), "Network unreachable") { continue } else { @@ -243,20 +216,20 @@ func TestTLSALPN01DialTimeout(t *testing.T) { } prob := detailedError(err) test.AssertEquals(t, prob.Type, probs.ConnectionProblem) - expected := "198.51.100.1: Timeout during connect (likely firewall problem)" + expected := "64.112.117.254: Timeout during connect (likely firewall problem)" if prob.Detail != expected { t.Errorf("Wrong error detail. Expected %q, got %q", expected, prob.Detail) } } func TestTLSALPN01Refused(t *testing.T) { - hs, err := tlsalpn01Srv(t, expectedKeyAuthorization, IdPeAcmeIdentifier, 0, "expected") - test.AssertNotError(t, err, "Error creating test server") + hs := testTLSALPN01Srv(t) + + va, _ := setup(hs, "", nil, nil) - va, _ := setup(hs, 0, "", nil, nil) // Take down validation server and check that validation fails. hs.Close() - _, err = va.validateTLSALPN01(ctx, dnsi("expected"), expectedKeyAuthorization) + _, err := va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization) if err == nil { t.Fatalf("Server's down; expected refusal. Where did we connect?") } @@ -269,18 +242,19 @@ func TestTLSALPN01Refused(t *testing.T) { } func TestTLSALPN01TalkingToHTTP(t *testing.T) { - hs, err := tlsalpn01Srv(t, expectedKeyAuthorization, IdPeAcmeIdentifier, 0, "expected") - test.AssertNotError(t, err, "Error creating test server") + hs := testTLSALPN01Srv(t) - va, _ := setup(hs, 0, "", nil, nil) - httpOnly := httpSrv(t, "") + va, _ := setup(hs, "", nil, nil) + + // Make the server only speak HTTP. + httpOnly := httpSrv(t, "", false) va.tlsPort = getPort(httpOnly) - _, err = va.validateTLSALPN01(ctx, dnsi("expected"), expectedKeyAuthorization) + _, err := va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization) test.AssertError(t, err, "TLS-SNI-01 validation passed when talking to a HTTP-only server") prob := detailedError(err) expected := "Server only speaks HTTP, not TLS" - if !strings.HasSuffix(prob.Error(), expected) { + if !strings.HasSuffix(prob.String(), expected) { t.Errorf("Got wrong error detail. Expected %q, got %q", expected, prob) } } @@ -299,9 +273,9 @@ func brokenTLSSrv() *httptest.Server { func TestTLSError(t *testing.T) { hs := brokenTLSSrv() - va, _ := setup(hs, 0, "", nil, nil) + va, _ := setup(hs, "", nil, nil) - _, err := va.validateTLSALPN01(ctx, dnsi("expected"), expectedKeyAuthorization) + _, err := va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization) if err == nil { t.Fatalf("TLS validation should have failed: What cert was used?") } @@ -315,9 +289,9 @@ func TestTLSError(t *testing.T) { func TestDNSError(t *testing.T) { hs := brokenTLSSrv() - va, _ := setup(hs, 0, "", nil, nil) + va, _ := setup(hs, "", nil, nil) - _, err := va.validateTLSALPN01(ctx, dnsi("always.invalid"), expectedKeyAuthorization) + _, err := va.validateTLSALPN01(ctx, identifier.NewDNS("always.invalid"), expectedKeyAuthorization) if err == nil { t.Fatalf("TLS validation should have failed: what IP was used?") } @@ -363,6 +337,16 @@ func TestCertNames(t *testing.T) { }, } + // Round-trip the certificate through generation and parsing, to make sure + // certAltNames can handle "real" certificates and not just templates. + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + test.AssertNotError(t, err, "Error creating test key") + certBytes, err := x509.CreateCertificate(rand.Reader, template, template, key.Public(), key) + test.AssertNotError(t, err, "Error creating certificate") + + cert, err := x509.ParseCertificate(certBytes) + test.AssertNotError(t, err, "Error parsing certificate") + // We expect only unique names, in sorted order. expected := []string{ "192.168.0.1", @@ -375,26 +359,50 @@ func TestCertNames(t *testing.T) { "hello@world.gov", } - // Create the certificate, check that certNames provides the expected result - certBytes, err := x509.CreateCertificate(rand.Reader, template, template, &TheKey.PublicKey, &TheKey) - test.AssertNotError(t, err, "Error creating certificate") - - cert, err := x509.ParseCertificate(certBytes) - test.AssertNotError(t, err, "Error parsing certificate") - actual := certAltNames(cert) test.AssertDeepEquals(t, actual, expected) } -func TestTLSALPN01Success(t *testing.T) { - hs, err := tlsalpn01Srv(t, expectedKeyAuthorization, IdPeAcmeIdentifier, 0, "expected") - test.AssertNotError(t, err, "Error creating test server") +func TestTLSALPN01SuccessDNS(t *testing.T) { + hs := testTLSALPN01Srv(t) - va, _ := setup(hs, 0, "", nil, nil) + va, _ := setup(hs, "", nil, nil) - _, prob := va.validateTLSALPN01(ctx, dnsi("expected"), expectedKeyAuthorization) - if prob != nil { - t.Errorf("Validation failed: %v", prob) + _, err := va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization) + if err != nil { + t.Errorf("Validation failed: %v", err) + } + test.AssertMetricWithLabelsEquals( + t, va.metrics.tlsALPNOIDCounter, prometheus.Labels{"oid": IdPeAcmeIdentifier.String()}, 1) + + hs.Close() +} + +func TestTLSALPN01SuccessIPv4(t *testing.T) { + cert := testTLSCert(nil, []net.IP{net.ParseIP("127.0.0.1")}, []pkix.Extension{testACMEExt}) + hs := tlsalpn01SrvWithCert(t, cert, 0, false) + + va, _ := setup(hs, "", nil, nil) + + _, err := va.validateTLSALPN01(ctx, identifier.NewIP(netip.MustParseAddr("127.0.0.1")), expectedKeyAuthorization) + if err != nil { + t.Errorf("Validation failed: %v", err) + } + test.AssertMetricWithLabelsEquals( + t, va.metrics.tlsALPNOIDCounter, prometheus.Labels{"oid": IdPeAcmeIdentifier.String()}, 1) + + hs.Close() +} + +func TestTLSALPN01SuccessIPv6(t *testing.T) { + cert := testTLSCert(nil, []net.IP{net.ParseIP("::1")}, []pkix.Extension{testACMEExt}) + hs := tlsalpn01SrvWithCert(t, cert, 0, true) + + va, _ := setup(hs, "", nil, nil) + + _, err := va.validateTLSALPN01(ctx, identifier.NewIP(netip.MustParseAddr("::1")), expectedKeyAuthorization) + if err != nil { + t.Errorf("Validation failed: %v", err) } test.AssertMetricWithLabelsEquals( t, va.metrics.tlsALPNOIDCounter, prometheus.Labels{"oid": IdPeAcmeIdentifier.String()}, 1) @@ -412,25 +420,25 @@ func TestTLSALPN01ObsoleteFailure(t *testing.T) { // id-pe OID + 30 (acmeIdentifier) + 1 (v1) IdPeAcmeIdentifierV1Obsolete := asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 30, 1} - hs, err := tlsalpn01Srv(t, expectedKeyAuthorization, IdPeAcmeIdentifierV1Obsolete, 0, "expected") - test.AssertNotError(t, err, "Error creating test server") + cert := testTLSCert([]string{"expected"}, nil, []pkix.Extension{acmeExtension(IdPeAcmeIdentifierV1Obsolete, expectedKeyAuthorization)}) + hs := tlsalpn01SrvWithCert(t, cert, 0, false) - va, _ := setup(hs, 0, "", nil, nil) + va, _ := setup(hs, "", nil, nil) - _, prob := va.validateTLSALPN01(ctx, dnsi("expected"), expectedKeyAuthorization) - test.AssertNotNil(t, prob, "expected validation to fail") + _, err := va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization) + test.AssertNotNil(t, err, "expected validation to fail") + test.AssertContains(t, err.Error(), "Required extension OID 1.3.6.1.5.5.7.1.31 is not present") } func TestValidateTLSALPN01BadChallenge(t *testing.T) { badKeyAuthorization := ka("bad token") - hs, err := tlsalpn01Srv(t, badKeyAuthorization, IdPeAcmeIdentifier, 0, "expected") - test.AssertNotError(t, err, "Error creating test server") + cert := testTLSCert([]string{"expected"}, nil, []pkix.Extension{acmeExtension(IdPeAcmeIdentifier, badKeyAuthorization)}) + hs := tlsalpn01SrvWithCert(t, cert, 0, false) - va, _ := setup(hs, 0, "", nil, nil) - - _, err = va.validateTLSALPN01(ctx, dnsi("expected"), expectedKeyAuthorization) + va, _ := setup(hs, "", nil, nil) + _, err := va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization) if err == nil { t.Fatalf("TLS ALPN validation should have failed.") } @@ -449,9 +457,9 @@ func TestValidateTLSALPN01BadChallenge(t *testing.T) { func TestValidateTLSALPN01BrokenSrv(t *testing.T) { hs := brokenTLSSrv() - va, _ := setup(hs, 0, "", nil, nil) + va, _ := setup(hs, "", nil, nil) - _, err := va.validateTLSALPN01(ctx, dnsi("expected"), expectedKeyAuthorization) + _, err := va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization) if err == nil { t.Fatalf("TLS ALPN validation should have failed.") } @@ -460,11 +468,21 @@ func TestValidateTLSALPN01BrokenSrv(t *testing.T) { } func TestValidateTLSALPN01UnawareSrv(t *testing.T) { - hs := tlssniSrvWithNames(t, "expected") + cert := testTLSCert([]string{"expected"}, nil, nil) + hs := httptest.NewUnstartedServer(http.DefaultServeMux) + hs.TLS = &tls.Config{ + Certificates: []tls.Certificate{}, + ClientAuth: tls.NoClientCert, + GetCertificate: func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { + return cert, nil + }, + NextProtos: []string{"http/1.1"}, // Doesn't list ACMETLS1Protocol + } + hs.StartTLS() - va, _ := setup(hs, 0, "", nil, nil) + va, _ := setup(hs, "", nil, nil) - _, err := va.validateTLSALPN01(ctx, dnsi("expected"), expectedKeyAuthorization) + _, err := va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization) if err == nil { t.Fatalf("TLS ALPN validation should have failed.") } @@ -472,22 +490,11 @@ func TestValidateTLSALPN01UnawareSrv(t *testing.T) { test.AssertEquals(t, prob.Type, probs.TLSProblem) } -// TestValidateTLSALPN01BadUTFSrv tests that validating TLS-ALPN-01 against -// a host that returns a certificate with a SAN/CN that contains invalid UTF-8 -// will result in a problem with the invalid UTF-8. -func TestValidateTLSALPN01BadUTFSrv(t *testing.T) { - _, err := tlsalpn01Srv(t, expectedKeyAuthorization, IdPeAcmeIdentifier, 0, "expected", "\xf0\x28\x8c\xbc") - test.AssertContains(t, err.Error(), "cannot be encoded as an IA5String") -} - // TestValidateTLSALPN01MalformedExtnValue tests that validating TLS-ALPN-01 // against a host that returns a certificate that contains an ASN.1 DER // acmeValidation extension value that does not parse or is the wrong length // will result in an Unauthorized problem func TestValidateTLSALPN01MalformedExtnValue(t *testing.T) { - names := []string{"expected"} - template := tlsCertTemplate(names) - wrongTypeDER, _ := asn1.Marshal("a string") wrongLengthDER, _ := asn1.Marshal(make([]byte, 31)) badExtensions := []pkix.Extension{ @@ -504,17 +511,11 @@ func TestValidateTLSALPN01MalformedExtnValue(t *testing.T) { } for _, badExt := range badExtensions { - template.ExtraExtensions = []pkix.Extension{badExt} - certBytes, _ := x509.CreateCertificate(rand.Reader, template, template, &TheKey.PublicKey, &TheKey) - acmeCert := &tls.Certificate{ - Certificate: [][]byte{certBytes}, - PrivateKey: &TheKey, - } + acmeCert := testTLSCert([]string{"expected"}, nil, []pkix.Extension{badExt}) + hs := tlsalpn01SrvWithCert(t, acmeCert, 0, false) + va, _ := setup(hs, "", nil, nil) - hs := tlsalpn01SrvWithCert(t, acmeCert, 0) - va, _ := setup(hs, 0, "", nil, nil) - - _, err := va.validateTLSALPN01(ctx, dnsi("expected"), expectedKeyAuthorization) + _, err := va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization) hs.Close() if err == nil { @@ -530,6 +531,8 @@ func TestValidateTLSALPN01MalformedExtnValue(t *testing.T) { } func TestTLSALPN01TLSVersion(t *testing.T) { + cert := testACMECert([]string{"expected"}) + for _, tc := range []struct { version uint16 expectError bool @@ -548,21 +551,21 @@ func TestTLSALPN01TLSVersion(t *testing.T) { }, } { // Create a server that only negotiates the given TLS version - hs, err := tlsalpn01Srv(t, expectedKeyAuthorization, IdPeAcmeIdentifier, tc.version, "expected") - test.AssertNotError(t, err, "Error creating test server") + hs := tlsalpn01SrvWithCert(t, cert, tc.version, false) - va, _ := setup(hs, 0, "", nil, nil) + va, _ := setup(hs, "", nil, nil) - _, prob := va.validateTLSALPN01(ctx, dnsi("expected"), expectedKeyAuthorization) + _, err := va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization) if !tc.expectError { - if prob != nil { - t.Errorf("expected success, got: %v", prob) + if err != nil { + t.Errorf("expected success, got: %v", err) } // The correct TLS-ALPN-01 OID counter should have been incremented test.AssertMetricWithLabelsEquals( t, va.metrics.tlsALPNOIDCounter, prometheus.Labels{"oid": IdPeAcmeIdentifier.String()}, 1) } else { - test.AssertNotNil(t, prob, "expected validation error") + test.AssertNotNil(t, err, "expected validation error") + test.AssertContains(t, err.Error(), "protocol version not supported") test.AssertMetricWithLabelsEquals( t, va.metrics.tlsALPNOIDCounter, prometheus.Labels{"oid": IdPeAcmeIdentifier.String()}, 0) } @@ -573,29 +576,80 @@ func TestTLSALPN01TLSVersion(t *testing.T) { func TestTLSALPN01WrongName(t *testing.T) { // Create a cert with a different name from what we're validating - hs, err := tlsalpn01Srv(t, expectedKeyAuthorization, IdPeAcmeIdentifier, tls.VersionTLS12, "incorrect") - test.AssertNotError(t, err, "failed to set up tls-alpn-01 server") + hs := tlsalpn01SrvWithCert(t, testACMECert([]string{"incorrect"}), 0, false) - va, _ := setup(hs, 0, "", nil, nil) + va, _ := setup(hs, "", nil, nil) - _, prob := va.validateTLSALPN01(ctx, dnsi("expected"), expectedKeyAuthorization) - test.AssertError(t, prob, "validation should have failed") + _, err := va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization) + test.AssertError(t, err, "validation should have failed") + test.AssertContains(t, err.Error(), "identifier does not match expected identifier") +} + +func TestTLSALPN01WrongIPv4(t *testing.T) { + // Create a cert with a different IP address from what we're validating + cert := testTLSCert(nil, []net.IP{net.ParseIP("10.10.10.10")}, []pkix.Extension{testACMEExt}) + hs := tlsalpn01SrvWithCert(t, cert, 0, false) + + va, _ := setup(hs, "", nil, nil) + + _, err := va.validateTLSALPN01(ctx, identifier.NewIP(netip.MustParseAddr("127.0.0.1")), expectedKeyAuthorization) + test.AssertError(t, err, "validation should have failed") + test.AssertContains(t, err.Error(), "identifier does not match expected identifier") +} + +func TestTLSALPN01WrongIPv6(t *testing.T) { + // Create a cert with a different IP address from what we're validating + cert := testTLSCert(nil, []net.IP{net.ParseIP("::2")}, []pkix.Extension{testACMEExt}) + hs := tlsalpn01SrvWithCert(t, cert, 0, true) + + va, _ := setup(hs, "", nil, nil) + + _, err := va.validateTLSALPN01(ctx, identifier.NewIP(netip.MustParseAddr("::1")), expectedKeyAuthorization) + test.AssertError(t, err, "validation should have failed") + test.AssertContains(t, err.Error(), "identifier does not match expected identifier") } func TestTLSALPN01ExtraNames(t *testing.T) { // Create a cert with two names when we only want to validate one. - hs, err := tlsalpn01Srv(t, expectedKeyAuthorization, IdPeAcmeIdentifier, tls.VersionTLS12, "expected", "extra") - test.AssertNotError(t, err, "failed to set up tls-alpn-01 server") + hs := tlsalpn01SrvWithCert(t, testACMECert([]string{"expected", "extra"}), 0, false) - va, _ := setup(hs, 0, "", nil, nil) + va, _ := setup(hs, "", nil, nil) - _, prob := va.validateTLSALPN01(ctx, dnsi("expected"), expectedKeyAuthorization) - test.AssertError(t, prob, "validation should have failed") + _, err := va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization) + test.AssertError(t, err, "validation should have failed") + test.AssertContains(t, err.Error(), "wrong number of identifiers") +} + +func TestTLSALPN01WrongIdentType(t *testing.T) { + // Create a cert with an IP address encoded as a name. + hs := tlsalpn01SrvWithCert(t, testACMECert([]string{"127.0.0.1"}), 0, false) + + va, _ := setup(hs, "", nil, nil) + + _, err := va.validateTLSALPN01(ctx, identifier.NewIP(netip.MustParseAddr("127.0.0.1")), expectedKeyAuthorization) + test.AssertError(t, err, "validation should have failed") + test.AssertContains(t, err.Error(), "wrong number of identifiers") +} + +func TestTLSALPN01TooManyIdentTypes(t *testing.T) { + // Create a cert with both a name and an IP address when we only want to validate one. + hs := tlsalpn01SrvWithCert(t, testTLSCert([]string{"expected"}, []net.IP{net.ParseIP("127.0.0.1")}, []pkix.Extension{testACMEExt}), 0, false) + + va, _ := setup(hs, "", nil, nil) + + _, err := va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization) + test.AssertError(t, err, "validation should have failed") + test.AssertContains(t, err.Error(), "wrong number of identifiers") + + _, err = va.validateTLSALPN01(ctx, identifier.NewIP(netip.MustParseAddr("127.0.0.1")), expectedKeyAuthorization) + test.AssertError(t, err, "validation should have failed") + test.AssertContains(t, err.Error(), "wrong number of identifiers") } func TestTLSALPN01NotSelfSigned(t *testing.T) { - // Create a cert with an extra non-dnsName identifier. - template := &x509.Certificate{ + // Create a normal-looking cert. We don't use testTLSCert because we need to + // control the issuer. + eeTemplate := &x509.Certificate{ SerialNumber: big.NewInt(1337), Subject: pkix.Name{ Organization: []string{"tests"}, @@ -606,22 +660,15 @@ func TestTLSALPN01NotSelfSigned(t *testing.T) { KeyUsage: x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, - DNSNames: []string{"expected"}, - IPAddresses: []net.IP{net.ParseIP("192.168.0.1")}, + DNSNames: []string{"expected"}, + IPAddresses: []net.IP{net.ParseIP("192.168.0.1")}, + ExtraExtensions: []pkix.Extension{testACMEExt}, } - shasum := sha256.Sum256([]byte(expectedKeyAuthorization)) - encHash, err := asn1.Marshal(shasum[:]) - test.AssertNotError(t, err, "failed to create key authorization") + eeKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + test.AssertNotError(t, err, "creating test key") - acmeExtension := pkix.Extension{ - Id: IdPeAcmeIdentifier, - Critical: true, - Value: encHash, - } - template.ExtraExtensions = []pkix.Extension{acmeExtension} - - parent := &x509.Certificate{ + issuerCert := &x509.Certificate{ SerialNumber: big.NewInt(1234), Subject: pkix.Name{ Organization: []string{"testissuer"}, @@ -631,27 +678,49 @@ func TestTLSALPN01NotSelfSigned(t *testing.T) { BasicConstraintsValid: true, } - // Note that this currently only tests that the subject and issuer are the - // same; it does not test the case where the cert is signed by a different key. - certBytes, err := x509.CreateCertificate(rand.Reader, template, parent, &TheKey.PublicKey, &TheKey) + issuerKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + test.AssertNotError(t, err, "creating test key") + + // Test that a cert with mismatched subject and issuer fields is rejected, + // even though its signature is produced with the right (self-signed) key. + certBytes, err := x509.CreateCertificate(rand.Reader, eeTemplate, issuerCert, eeKey.Public(), eeKey) test.AssertNotError(t, err, "failed to create acme-tls/1 cert") acmeCert := &tls.Certificate{ Certificate: [][]byte{certBytes}, - PrivateKey: &TheKey, + PrivateKey: eeKey, } - hs := tlsalpn01SrvWithCert(t, acmeCert, tls.VersionTLS12) + hs := tlsalpn01SrvWithCert(t, acmeCert, 0, false) - va, _ := setup(hs, 0, "", nil, nil) + va, _ := setup(hs, "", nil, nil) - _, err = va.validateTLSALPN01(ctx, dnsi("expected"), expectedKeyAuthorization) + _, err = va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization) + test.AssertError(t, err, "validation should have failed") + test.AssertContains(t, err.Error(), "not self-signed") + + // Test that a cert whose signature was produced by some other key is rejected, + // even though its subject and issuer fields claim that it is self-signed. + certBytes, err = x509.CreateCertificate(rand.Reader, eeTemplate, eeTemplate, eeKey.Public(), issuerKey) + test.AssertNotError(t, err, "failed to create acme-tls/1 cert") + + acmeCert = &tls.Certificate{ + Certificate: [][]byte{certBytes}, + PrivateKey: eeKey, + } + + hs = tlsalpn01SrvWithCert(t, acmeCert, 0, false) + + va, _ = setup(hs, "", nil, nil) + + _, err = va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization) test.AssertError(t, err, "validation should have failed") test.AssertContains(t, err.Error(), "not self-signed") } func TestTLSALPN01ExtraIdentifiers(t *testing.T) { - // Create a cert with an extra non-dnsName identifier. + // Create a cert with an extra non-dnsName identifier. We don't use testTLSCert + // because we need to set the IPAddresses field. template := &x509.Certificate{ SerialNumber: big.NewInt(1337), Subject: pkix.Name{ @@ -660,154 +729,73 @@ func TestTLSALPN01ExtraIdentifiers(t *testing.T) { NotBefore: time.Now(), NotAfter: time.Now().AddDate(0, 0, 1), - KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + KeyUsage: x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, BasicConstraintsValid: true, - DNSNames: []string{"expected"}, - IPAddresses: []net.IP{net.ParseIP("192.168.0.1")}, + DNSNames: []string{"expected"}, + IPAddresses: []net.IP{net.ParseIP("192.168.0.1")}, + ExtraExtensions: []pkix.Extension{testACMEExt}, } - shasum := sha256.Sum256([]byte(expectedKeyAuthorization)) - encHash, err := asn1.Marshal(shasum[:]) - test.AssertNotError(t, err, "failed to create key authorization") - - acmeExtension := pkix.Extension{ - Id: IdPeAcmeIdentifier, - Critical: true, - Value: encHash, - } - template.ExtraExtensions = []pkix.Extension{acmeExtension} - certBytes, err := x509.CreateCertificate(rand.Reader, template, template, &TheKey.PublicKey, &TheKey) + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + test.AssertNotError(t, err, "creating test key") + certBytes, err := x509.CreateCertificate(rand.Reader, template, template, key.Public(), key) test.AssertNotError(t, err, "failed to create acme-tls/1 cert") acmeCert := &tls.Certificate{ Certificate: [][]byte{certBytes}, - PrivateKey: &TheKey, + PrivateKey: key, } - hs := tlsalpn01SrvWithCert(t, acmeCert, tls.VersionTLS12) + hs := tlsalpn01SrvWithCert(t, acmeCert, tls.VersionTLS12, false) - va, _ := setup(hs, 0, "", nil, nil) + va, _ := setup(hs, "", nil, nil) - _, prob := va.validateTLSALPN01(ctx, dnsi("expected"), expectedKeyAuthorization) - test.AssertError(t, prob, "validation should have failed") + _, err = va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization) + test.AssertError(t, err, "validation should have failed") + test.AssertContains(t, err.Error(), "Received certificate with unexpected identifiers") } func TestTLSALPN01ExtraSANs(t *testing.T) { // Create a cert with multiple SAN extensions - template := &x509.Certificate{ - SerialNumber: big.NewInt(1337), - Subject: pkix.Name{ - Organization: []string{"tests"}, - }, - NotBefore: time.Now(), - NotAfter: time.Now().AddDate(0, 0, 1), - - KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, - BasicConstraintsValid: true, - } - - shasum := sha256.Sum256([]byte(expectedKeyAuthorization)) - encHash, err := asn1.Marshal(shasum[:]) - test.AssertNotError(t, err, "failed to create key authorization") - - acmeExtension := pkix.Extension{ - Id: IdPeAcmeIdentifier, - Critical: true, - Value: encHash, - } - - subjectAltName := pkix.Extension{} - subjectAltName.Id = asn1.ObjectIdentifier{2, 5, 29, 17} - subjectAltName.Critical = false - subjectAltName.Value, err = asn1.Marshal([]asn1.RawValue{ + sanValue, err := asn1.Marshal([]asn1.RawValue{ {Tag: 2, Class: 2, Bytes: []byte(`expected`)}, }) - test.AssertNotError(t, err, "failed to marshal first SAN") + test.AssertNotError(t, err, "failed to marshal test SAN") - extraSubjectAltName := pkix.Extension{} - extraSubjectAltName.Id = asn1.ObjectIdentifier{2, 5, 29, 17} - extraSubjectAltName.Critical = false - extraSubjectAltName.Value, err = asn1.Marshal([]asn1.RawValue{ - {Tag: 2, Class: 2, Bytes: []byte(`expected`)}, - }) - test.AssertNotError(t, err, "failed to marshal extra SAN") - - template.ExtraExtensions = []pkix.Extension{acmeExtension, subjectAltName, extraSubjectAltName} - certBytes, err := x509.CreateCertificate(rand.Reader, template, template, &TheKey.PublicKey, &TheKey) - test.AssertNotError(t, err, "failed to create acme-tls/1 cert") - - acmeCert := &tls.Certificate{ - Certificate: [][]byte{certBytes}, - PrivateKey: &TheKey, + subjectAltName := pkix.Extension{ + Id: asn1.ObjectIdentifier{2, 5, 29, 17}, + Critical: false, + Value: sanValue, } - hs := tlsalpn01SrvWithCert(t, acmeCert, tls.VersionTLS12) + extensions := []pkix.Extension{testACMEExt, subjectAltName, subjectAltName} + hs := tlsalpn01SrvWithCert(t, testTLSCert([]string{"expected"}, nil, extensions), 0, false) - va, _ := setup(hs, 0, "", nil, nil) + va, _ := setup(hs, "", nil, nil) - _, err = va.validateTLSALPN01(ctx, dnsi("expected"), expectedKeyAuthorization) + _, err = va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization) test.AssertError(t, err, "validation should have failed") // In go >= 1.19, the TLS client library detects that the certificate has // a duplicate extension and terminates the connection itself. prob := detailedError(err) - test.AssertContains(t, prob.Error(), "Error getting validation data") + test.AssertContains(t, prob.String(), "Error getting validation data") } func TestTLSALPN01ExtraAcmeExtensions(t *testing.T) { // Create a cert with multiple SAN extensions - template := &x509.Certificate{ - SerialNumber: big.NewInt(1337), - Subject: pkix.Name{ - Organization: []string{"tests"}, - }, - NotBefore: time.Now(), - NotAfter: time.Now().AddDate(0, 0, 1), + extensions := []pkix.Extension{testACMEExt, testACMEExt} + hs := tlsalpn01SrvWithCert(t, testTLSCert([]string{"expected"}, nil, extensions), 0, false) - KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, - BasicConstraintsValid: true, + va, _ := setup(hs, "", nil, nil) - DNSNames: []string{"expected"}, - } - - shasum := sha256.Sum256([]byte(expectedKeyAuthorization)) - encHash, err := asn1.Marshal(shasum[:]) - test.AssertNotError(t, err, "failed to create key authorization") - - acmeExtension := pkix.Extension{ - Id: IdPeAcmeIdentifier, - Critical: true, - Value: encHash, - } - - extraAcmeExtension := pkix.Extension{ - Id: IdPeAcmeIdentifier, - Critical: true, - Value: encHash, - } - - template.ExtraExtensions = []pkix.Extension{acmeExtension, extraAcmeExtension} - certBytes, err := x509.CreateCertificate(rand.Reader, template, template, &TheKey.PublicKey, &TheKey) - test.AssertNotError(t, err, "failed to create acme-tls/1 cert") - - acmeCert := &tls.Certificate{ - Certificate: [][]byte{certBytes}, - PrivateKey: &TheKey, - } - - hs := tlsalpn01SrvWithCert(t, acmeCert, tls.VersionTLS12) - - va, _ := setup(hs, 0, "", nil, nil) - - _, err = va.validateTLSALPN01(ctx, dnsi("expected"), expectedKeyAuthorization) + _, err := va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization) test.AssertError(t, err, "validation should have failed") - prob := detailedError(err) // In go >= 1.19, the TLS client library detects that the certificate has // a duplicate extension and terminates the connection itself. - test.AssertContains(t, prob.Error(), "Error getting validation data") + prob := detailedError(err) + test.AssertContains(t, prob.String(), "Error getting validation data") } func TestAcceptableExtensions(t *testing.T) { @@ -816,14 +804,15 @@ func TestAcceptableExtensions(t *testing.T) { IdCeSubjectAltName, } - var err error - subjectAltName := pkix.Extension{} - subjectAltName.Id = asn1.ObjectIdentifier{2, 5, 29, 17} - subjectAltName.Critical = false - subjectAltName.Value, err = asn1.Marshal([]asn1.RawValue{ + sanValue, err := asn1.Marshal([]asn1.RawValue{ {Tag: 2, Class: 2, Bytes: []byte(`expected`)}, }) - test.AssertNotError(t, err, "failed to marshal SAN") + test.AssertNotError(t, err, "failed to marshal test SAN") + subjectAltName := pkix.Extension{ + Id: asn1.ObjectIdentifier{2, 5, 29, 17}, + Critical: false, + Value: sanValue, + } acmeExtension := pkix.Extension{ Id: IdPeAcmeIdentifier, @@ -858,3 +847,96 @@ func TestAcceptableExtensions(t *testing.T) { err = checkAcceptableExtensions(okayWithUnexpectedExt, requireAcmeAndSAN) test.AssertNotError(t, err, "Correct type and number of extensions") } + +func TestTLSALPN01BadIdentifier(t *testing.T) { + hs := httpSrv(t, expectedToken, false) + defer hs.Close() + + va, _ := setup(hs, "", nil, nil) + + _, err := va.validateTLSALPN01(ctx, identifier.ACMEIdentifier{Type: "smime", Value: "dobber@bad.horse"}, expectedKeyAuthorization) + test.AssertError(t, err, "Server accepted a hypothetical S/MIME identifier") + prob := detailedError(err) + test.AssertContains(t, prob.String(), "Identifier type for TLS-ALPN-01 challenge was not DNS or IP") +} + +// TestTLSALPN01ServerName tests compliance with RFC 8737, Sec. 3 (step 3) & RFC +// 8738, Sec. 6. +func TestTLSALPN01ServerName(t *testing.T) { + testCases := []struct { + Name string + Ident identifier.ACMEIdentifier + CertNames []string + CertIPs []net.IP + IPv6 bool + want string + }{ + { + Name: "DNS name", + Ident: identifier.NewDNS("example.com"), + CertNames: []string{"example.com"}, + want: "example.com", + }, + { + // RFC 8738, Sec. 6. + Name: "IPv4 address", + Ident: identifier.NewIP(netip.MustParseAddr("127.0.0.1")), + CertIPs: []net.IP{net.ParseIP("127.0.0.1")}, + want: "1.0.0.127.in-addr.arpa", + }, + { + // RFC 8738, Sec. 6. + Name: "IPv6 address", + Ident: identifier.NewIP(netip.MustParseAddr("::1")), + CertIPs: []net.IP{net.ParseIP("::1")}, + IPv6: true, + want: "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa", + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*500) + defer cancel() + + tlsConfig := &tls.Config{ + Certificates: []tls.Certificate{}, + ClientAuth: tls.NoClientCert, + NextProtos: []string{"http/1.1", ACMETLS1Protocol}, + GetCertificate: func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { + got := clientHello.ServerName + if got != tc.want { + return nil, fmt.Errorf("Got host %#v, but want %#v", got, tc.want) + } + return testTLSCert(tc.CertNames, tc.CertIPs, []pkix.Extension{testACMEExt}), nil + }, + } + + hs := httptest.NewUnstartedServer(http.DefaultServeMux) + hs.TLS = tlsConfig + hs.Config.TLSNextProto = map[string]func(*http.Server, *tls.Conn, http.Handler){ + ACMETLS1Protocol: func(_ *http.Server, conn *tls.Conn, _ http.Handler) { + _ = conn.Close() + }, + } + if tc.IPv6 { + l, err := net.Listen("tcp", "[::1]:0") + if err != nil { + panic(fmt.Sprintf("httptest: failed to listen on a port: %v", err)) + } + hs.Listener = l + } + hs.StartTLS() + defer hs.Close() + + va, _ := setup(hs, "", nil, nil) + + // The actual test happens in the tlsConfig.GetCertificate function, + // which the validation will call and depend on for its success. + _, err := va.validateTLSALPN01(ctx, tc.Ident, expectedKeyAuthorization) + if err != nil { + t.Errorf("Validation failed: %v", err) + } + }) + } +} diff --git a/third-party/github.com/letsencrypt/boulder/va/va.go b/third-party/github.com/letsencrypt/boulder/va/va.go index d43346bbc..4307e57b4 100644 --- a/third-party/github.com/letsencrypt/boulder/va/va.go +++ b/third-party/github.com/letsencrypt/boulder/va/va.go @@ -4,24 +4,27 @@ import ( "bytes" "context" "crypto/tls" - "encoding/json" "errors" "fmt" - "math/rand" + "maps" + "math/rand/v2" "net" + "net/netip" "net/url" "os" "regexp" + "slices" "strings" "syscall" "time" "github.com/jmhodges/clock" "github.com/prometheus/client_golang/prometheus" + "google.golang.org/protobuf/proto" "github.com/letsencrypt/boulder/bdns" - "github.com/letsencrypt/boulder/canceled" "github.com/letsencrypt/boulder/core" + corepb "github.com/letsencrypt/boulder/core/proto" berrors "github.com/letsencrypt/boulder/errors" bgrpc "github.com/letsencrypt/boulder/grpc" "github.com/letsencrypt/boulder/identifier" @@ -31,6 +34,18 @@ import ( vapb "github.com/letsencrypt/boulder/va/proto" ) +const ( + PrimaryPerspective = "Primary" + allPerspectives = "all" + + opDCVAndCAA = "dcv+caa" + opDCV = "dcv" + opCAA = "caa" + + pass = "pass" + fail = "fail" +) + var ( // badTLSHeader contains the string 'HTTP /' which is returned when // we try to talk TLS to a server that only talks HTTP @@ -77,18 +92,20 @@ type RemoteClients struct { // extract this metadata which is useful for debugging gRPC connection issues. type RemoteVA struct { RemoteClients - Address string + Address string + Perspective string + RIR string } type vaMetrics struct { - validationTime *prometheus.HistogramVec - localValidationTime *prometheus.HistogramVec - remoteValidationTime *prometheus.HistogramVec - remoteValidationFailures prometheus.Counter - caaCheckTime *prometheus.HistogramVec - localCAACheckTime *prometheus.HistogramVec - remoteCAACheckTime *prometheus.HistogramVec - remoteCAACheckFailures prometheus.Counter + // validationLatency is a histogram of the latency to perform validations + // from the primary and remote VA perspectives. It's labelled by: + // - operation: VA.DoDCV or VA.DoCAA as [dcv|caa|dcv+caa] + // - perspective: ValidationAuthorityImpl.perspective + // - challenge_type: core.Challenge.Type + // - problem_type: probs.ProblemType + // - result: the result of the validation as [pass|fail] + validationLatency *prometheus.HistogramVec prospectiveRemoteCAACheckFailures prometheus.Counter tlsALPNOIDCounter *prometheus.CounterVec http01Fallbacks prometheus.Counter @@ -98,66 +115,15 @@ type vaMetrics struct { } func initMetrics(stats prometheus.Registerer) *vaMetrics { - validationTime := prometheus.NewHistogramVec( + validationLatency := prometheus.NewHistogramVec( prometheus.HistogramOpts{ - Name: "validation_time", - Help: "Total time taken to validate a challenge and aggregate results", + Name: "validation_latency", + Help: "Histogram of the latency to perform validations from the primary and remote VA perspectives", Buckets: metrics.InternetFacingBuckets, }, - []string{"type", "result", "problem_type"}) - stats.MustRegister(validationTime) - localValidationTime := prometheus.NewHistogramVec( - prometheus.HistogramOpts{ - Name: "local_validation_time", - Help: "Time taken to locally validate a challenge", - Buckets: metrics.InternetFacingBuckets, - }, - []string{"type", "result"}) - stats.MustRegister(localValidationTime) - remoteValidationTime := prometheus.NewHistogramVec( - prometheus.HistogramOpts{ - Name: "remote_validation_time", - Help: "Time taken to remotely validate a challenge", - Buckets: metrics.InternetFacingBuckets, - }, - []string{"type"}) - stats.MustRegister(remoteValidationTime) - remoteValidationFailures := prometheus.NewCounter( - prometheus.CounterOpts{ - Name: "remote_validation_failures", - Help: "Number of validations failed due to remote VAs returning failure when consensus is enforced", - }) - stats.MustRegister(remoteValidationFailures) - caaCheckTime := prometheus.NewHistogramVec( - prometheus.HistogramOpts{ - Name: "caa_check_time", - Help: "Total time taken to check CAA records and aggregate results", - Buckets: metrics.InternetFacingBuckets, - }, - []string{"result"}) - stats.MustRegister(caaCheckTime) - localCAACheckTime := prometheus.NewHistogramVec( - prometheus.HistogramOpts{ - Name: "caa_check_time_local", - Help: "Time taken to locally check CAA records", - Buckets: metrics.InternetFacingBuckets, - }, - []string{"result"}) - stats.MustRegister(localCAACheckTime) - remoteCAACheckTime := prometheus.NewHistogramVec( - prometheus.HistogramOpts{ - Name: "caa_check_time_remote", - Help: "Time taken to remotely check CAA records", - Buckets: metrics.InternetFacingBuckets, - }, - []string{"result"}) - stats.MustRegister(remoteCAACheckTime) - remoteCAACheckFailures := prometheus.NewCounter( - prometheus.CounterOpts{ - Name: "remote_caa_check_failures", - Help: "Number of CAA checks failed due to remote VAs returning failure when consensus is enforced", - }) - stats.MustRegister(remoteCAACheckFailures) + []string{"operation", "perspective", "challenge_type", "problem_type", "result"}, + ) + stats.MustRegister(validationLatency) prospectiveRemoteCAACheckFailures := prometheus.NewCounter( prometheus.CounterOpts{ Name: "prospective_remote_caa_check_failures", @@ -196,14 +162,7 @@ func initMetrics(stats prometheus.Registerer) *vaMetrics { stats.MustRegister(ipv4FallbackCounter) return &vaMetrics{ - validationTime: validationTime, - remoteValidationTime: remoteValidationTime, - localValidationTime: localValidationTime, - remoteValidationFailures: remoteValidationFailures, - caaCheckTime: caaCheckTime, - localCAACheckTime: localCAACheckTime, - remoteCAACheckTime: remoteCAACheckTime, - remoteCAACheckFailures: remoteCAACheckFailures, + validationLatency: validationLatency, prospectiveRemoteCAACheckFailures: prospectiveRemoteCAACheckFailures, tlsALPNOIDCounter: tlsALPNOIDCounter, http01Fallbacks: http01Fallbacks, @@ -256,6 +215,9 @@ type ValidationAuthorityImpl struct { maxRemoteFailures int accountURIPrefixes []string singleDialTimeout time.Duration + perspective string + rir string + isReservedIPFunc func(netip.Addr) error metrics *vaMetrics } @@ -267,19 +229,29 @@ var _ vapb.CAAServer = (*ValidationAuthorityImpl)(nil) func NewValidationAuthorityImpl( resolver bdns.Client, remoteVAs []RemoteVA, - maxRemoteFailures int, userAgent string, issuerDomain string, stats prometheus.Registerer, clk clock.Clock, logger blog.Logger, accountURIPrefixes []string, + perspective string, + rir string, + reservedIPChecker func(netip.Addr) error, ) (*ValidationAuthorityImpl, error) { if len(accountURIPrefixes) == 0 { return nil, errors.New("no account URI prefixes configured") } + for i, va1 := range remoteVAs { + for j, va2 := range remoteVAs { + if i != j && va1.Perspective == va2.Perspective { + return nil, fmt.Errorf("duplicate remote VA perspective %q", va1.Perspective) + } + } + } + pc := newDefaultPortConfig() va := &ValidationAuthorityImpl{ @@ -293,40 +265,49 @@ func NewValidationAuthorityImpl( clk: clk, metrics: initMetrics(stats), remoteVAs: remoteVAs, - maxRemoteFailures: maxRemoteFailures, + maxRemoteFailures: maxAllowedFailures(len(remoteVAs)), accountURIPrefixes: accountURIPrefixes, // singleDialTimeout specifies how long an individual `DialContext` operation may take // before timing out. This timeout ignores the base RPC timeout and is strictly // used for the DialContext operations that take place during an // HTTP-01 challenge validation. singleDialTimeout: 10 * time.Second, + perspective: perspective, + rir: rir, + isReservedIPFunc: reservedIPChecker, } return va, nil } -// Used for audit logging -type verificationRequestEvent struct { - ID string `json:",omitempty"` - Requester int64 `json:",omitempty"` - Hostname string `json:",omitempty"` - Challenge core.Challenge `json:",omitempty"` - ValidationLatency float64 - UsedRSAKEX bool `json:",omitempty"` - Error string `json:",omitempty"` - InternalError string `json:",omitempty"` +// maxAllowedFailures returns the maximum number of allowed failures +// for a given number of remote perspectives, according to the "Quorum +// Requirements" table in BRs Section 3.2.2.9, as follows: +// +// | # of Distinct Remote Network Perspectives Used | # of Allowed non-Corroborations | +// | --- | --- | +// | 2-5 | 1 | +// | 6+ | 2 | +func maxAllowedFailures(perspectiveCount int) int { + if perspectiveCount < 2 { + return 0 + } + if perspectiveCount < 6 { + return 1 + } + return 2 } // ipError is an error type used to pass though the IP address of the remote // host when an error occurs during HTTP-01 and TLS-ALPN domain validation. type ipError struct { - ip net.IP + ip netip.Addr err error } // newIPError wraps an error and the IP of the remote host in an ipError so we // can display the IP in the problem details returned to the client. -func newIPError(ip net.IP, err error) error { +func newIPError(ip netip.Addr, err error) error { return ipError{ip: ip, err: err} } @@ -349,7 +330,7 @@ func detailedError(err error) *probs.ProblemDetails { var ipErr ipError if errors.As(err, &ipErr) { detailedErr := detailedError(ipErr.err) - if ipErr.ip == nil { + if (ipErr.ip == netip.Addr{}) { // This should never happen. return detailedErr } @@ -419,6 +400,11 @@ func detailedError(err error) *probs.ProblemDetails { return probs.Connection("Error getting validation data") } +// isPrimaryVA returns true if the VA is the primary validation perspective. +func (va *ValidationAuthorityImpl) isPrimaryVA() bool { + return va.perspective == PrimaryPerspective +} + // validateChallenge simply passes through to the appropriate validation method // depending on the challenge type. func (va *ValidationAuthorityImpl) validateChallenge( @@ -428,13 +414,12 @@ func (va *ValidationAuthorityImpl) validateChallenge( token string, keyAuthorization string, ) ([]core.ValidationRecord, error) { - // Strip a (potential) leading wildcard token from the identifier. - ident.Value = strings.TrimPrefix(ident.Value, "*.") - switch kind { case core.ChallengeTypeHTTP01: return va.validateHTTP01(ctx, ident, token, keyAuthorization) case core.ChallengeTypeDNS01: + // Strip a (potential) leading wildcard token from the identifier. + ident.Value = strings.TrimPrefix(ident.Value, "*.") return va.validateDNS01(ctx, ident, keyAuthorization) case core.ChallengeTypeTLSALPN01: return va.validateTLSALPN01(ctx, ident, keyAuthorization) @@ -442,255 +427,282 @@ func (va *ValidationAuthorityImpl) validateChallenge( return nil, berrors.MalformedError("invalid challenge type %s", kind) } -// performRemoteValidation coordinates the whole process of kicking off and -// collecting results from calls to remote VAs' PerformValidation function. It -// returns a problem if too many remote perspectives failed to corroborate -// domain control, or nil if enough succeeded to surpass our corroboration -// threshold. -func (va *ValidationAuthorityImpl) performRemoteValidation( - ctx context.Context, - req *vapb.PerformValidationRequest, -) *probs.ProblemDetails { - if len(va.remoteVAs) == 0 { - return nil +// observeLatency records entries in the validationLatency histogram of the +// latency to perform validations from the primary and remote VA perspectives. +// The labels are: +// - operation: VA.DoDCV or VA.DoCAA as [dcv|caa] +// - perspective: [ValidationAuthorityImpl.perspective|all] +// - challenge_type: core.Challenge.Type +// - problem_type: probs.ProblemType +// - result: the result of the validation as [pass|fail] +func (va *ValidationAuthorityImpl) observeLatency(op, perspective, challType, probType, result string, latency time.Duration) { + labels := prometheus.Labels{ + "operation": op, + "perspective": perspective, + "challenge_type": challType, + "problem_type": probType, + "result": result, + } + va.metrics.validationLatency.With(labels).Observe(latency.Seconds()) +} + +// remoteOperation is a func type that encapsulates the operation and request +// passed to va.performRemoteOperation. The operation must be a method on +// vapb.VAClient or vapb.CAAClient, and the request must be the corresponding +// proto.Message passed to that method. +type remoteOperation = func(context.Context, RemoteVA, proto.Message) (remoteResult, error) + +// remoteResult is an interface that must be implemented by the results of a +// remoteOperation, such as *vapb.ValidationResult and *vapb.IsCAAValidResponse. +// It provides methods to access problem details, the associated perspective, +// and the RIR. +type remoteResult interface { + proto.Message + GetProblem() *corepb.ProblemDetails + GetPerspective() string + GetRir() string +} + +const ( + // requiredRIRs is the minimum number of distinct Regional Internet + // Registries required for MPIC-compliant validation. Per BRs Section + // 3.2.2.9, starting March 15, 2026, the required number is 2. + requiredRIRs = 2 +) + +// mpicSummary is returned by doRemoteOperation and contains a summary of the +// validation results for logging purposes. To ensure that the JSON output does +// not contain nil slices, and to ensure deterministic output use the +// summarizeMPIC function to prepare an mpicSummary. +type mpicSummary struct { + // Passed are the perspectives that passed validation. + Passed []string `json:"passedPerspectives"` + + // Failed are the perspectives that failed validation. + Failed []string `json:"failedPerspectives"` + + // PassedRIRs are the Regional Internet Registries that the passing + // perspectives reside in. + PassedRIRs []string `json:"passedRIRs"` + + // QuorumResult is the Multi-Perspective Issuance Corroboration quorum + // result, per BRs Section 5.4.1, Requirement 2.7 (i.e., "3/4" which should + // be interpreted as "Three (3) out of four (4) attempted Network + // Perspectives corroborated the determinations made by the Primary Network + // Perspective". + QuorumResult string `json:"quorumResult"` +} + +// summarizeMPIC prepares an *mpicSummary for logging, ensuring there are no nil +// slices and output is deterministic. +func summarizeMPIC(passed, failed []string, passedRIRSet map[string]struct{}) *mpicSummary { + if passed == nil { + passed = []string{} + } + slices.Sort(passed) + if failed == nil { + failed = []string{} + } + slices.Sort(failed) + + passedRIRs := []string{} + if passedRIRSet != nil { + for rir := range maps.Keys(passedRIRSet) { + passedRIRs = append(passedRIRs, rir) + } + } + slices.Sort(passedRIRs) + + return &mpicSummary{ + Passed: passed, + Failed: failed, + PassedRIRs: passedRIRs, + QuorumResult: fmt.Sprintf("%d/%d", len(passed), len(passed)+len(failed)), + } +} + +// doRemoteOperation concurrently calls the provided operation with `req` and a +// RemoteVA once for each configured RemoteVA. It cancels remaining operations +// and returns early if either the required number of successful results is +// obtained or the number of failures exceeds va.maxRemoteFailures. +// +// Internal logic errors are logged. If the number of operation failures exceeds +// va.maxRemoteFailures, the first encountered problem is returned as a +// *probs.ProblemDetails. +func (va *ValidationAuthorityImpl) doRemoteOperation(ctx context.Context, op remoteOperation, req proto.Message) (*mpicSummary, *probs.ProblemDetails) { + remoteVACount := len(va.remoteVAs) + // - Mar 15, 2026: MUST implement using at least 3 perspectives + // - Jun 15, 2026: MUST implement using at least 4 perspectives + // - Dec 15, 2026: MUST implement using at least 5 perspectives + // See "Phased Implementation Timeline" in + // https://github.com/cabforum/servercert/blob/main/docs/BR.md#3229-multi-perspective-issuance-corroboration + if remoteVACount < 3 { + return nil, probs.ServerInternal("Insufficient remote perspectives: need at least 3") } - start := va.clk.Now() - defer func() { - va.metrics.remoteValidationTime.With(prometheus.Labels{ - "type": req.Challenge.Type, - }).Observe(va.clk.Since(start).Seconds()) - }() - - type rvaResult struct { - hostname string - response *vapb.ValidationResult - err error + type response struct { + addr string + perspective string + rir string + result remoteResult + err error } - results := make(chan *rvaResult) + subCtx, cancel := context.WithCancel(ctx) + defer cancel() - for _, i := range rand.Perm(len(va.remoteVAs)) { - remoteVA := va.remoteVAs[i] - go func(rva RemoteVA, out chan<- *rvaResult) { - res, err := rva.PerformValidation(ctx, req) - out <- &rvaResult{ - hostname: rva.Address, - response: res, - err: err, + responses := make(chan *response, remoteVACount) + for _, i := range rand.Perm(remoteVACount) { + go func(rva RemoteVA) { + res, err := op(subCtx, rva, req) + if err != nil { + responses <- &response{rva.Address, rva.Perspective, rva.RIR, res, err} + return } - }(remoteVA, results) + if res.GetPerspective() != rva.Perspective || res.GetRir() != rva.RIR { + err = fmt.Errorf( + "Expected perspective %q (%q) but got reply from %q (%q) - misconfiguration likely", rva.Perspective, rva.RIR, res.GetPerspective(), res.GetRir(), + ) + responses <- &response{rva.Address, rva.Perspective, rva.RIR, res, err} + return + } + responses <- &response{rva.Address, rva.Perspective, rva.RIR, res, err} + }(va.remoteVAs[i]) } - required := len(va.remoteVAs) - va.maxRemoteFailures - good := 0 - bad := 0 + required := remoteVACount - va.maxRemoteFailures + var passed []string + var failed []string + var passedRIRs = map[string]struct{}{} var firstProb *probs.ProblemDetails - for res := range results { + for resp := range responses { var currProb *probs.ProblemDetails - if res.err != nil { - bad++ + if resp.err != nil { + // Failed to communicate with the remote VA. + failed = append(failed, resp.perspective) - if canceled.Is(res.err) { - currProb = probs.ServerInternal("Remote PerformValidation RPC canceled") + if core.IsCanceled(resp.err) { + currProb = probs.ServerInternal("Secondary validation RPC canceled") } else { - va.log.Errf("Remote VA %q.PerformValidation failed: %s", res.hostname, res.err) - currProb = probs.ServerInternal("Remote PerformValidation RPC failed") + va.log.Errf("Operation on remote VA (%s) failed: %s", resp.addr, resp.err) + currProb = probs.ServerInternal("Secondary validation RPC failed") } - } else if res.response.Problems != nil { - bad++ + } else if resp.result.GetProblem() != nil { + // The remote VA returned a problem. + failed = append(failed, resp.perspective) var err error - currProb, err = bgrpc.PBToProblemDetails(res.response.Problems) + currProb, err = bgrpc.PBToProblemDetails(resp.result.GetProblem()) if err != nil { - va.log.Errf("Remote VA %q.PerformValidation returned malformed problem: %s", res.hostname, err) - currProb = probs.ServerInternal("Remote PerformValidation RPC returned malformed result") + va.log.Errf("Operation on Remote VA (%s) returned malformed problem: %s", resp.addr, err) + currProb = probs.ServerInternal("Secondary validation RPC returned malformed result") } } else { - good++ + // The remote VA returned a successful result. + passed = append(passed, resp.perspective) + passedRIRs[resp.rir] = struct{}{} } if firstProb == nil && currProb != nil { + // A problem was encountered for the first time. firstProb = currProb } - // Return as soon as we have enough successes or failures for a definitive result. - if good >= required { - return nil - } - if bad > va.maxRemoteFailures { - va.metrics.remoteValidationFailures.Inc() - firstProb.Detail = fmt.Sprintf("During secondary validation: %s", firstProb.Detail) - return firstProb - } - - // If we somehow haven't returned early, we need to break the loop once all - // of the VAs have returned a result. - if good+bad >= len(va.remoteVAs) { + // Once all the VAs have returned a result, break the loop. + if len(passed)+len(failed) >= remoteVACount { break } } - - // This condition should not occur - it indicates the good/bad counts neither - // met the required threshold nor the maxRemoteFailures threshold. - return probs.ServerInternal("Too few remote PerformValidation RPC results") + if len(passed) >= required && len(passedRIRs) >= requiredRIRs { + return summarizeMPIC(passed, failed, passedRIRs), nil + } + if firstProb == nil { + // This should never happen. If we didn't meet the thresholds above we + // should have seen at least one error. + return summarizeMPIC(passed, failed, passedRIRs), probs.ServerInternal( + "During secondary validation: validation failed but the problem is unavailable") + } + firstProb.Detail = fmt.Sprintf("During secondary validation: %s", firstProb.Detail) + return summarizeMPIC(passed, failed, passedRIRs), firstProb } -// logRemoteResults is called by `processRemoteCAAResults` when the -// `MultiCAAFullResults` feature flag is enabled. It produces a JSON log line -// that contains the results each remote VA returned. -func (va *ValidationAuthorityImpl) logRemoteResults( - domain string, - acctID int64, - challengeType string, - remoteResults []*remoteVAResult) { - - var successes, failures []*remoteVAResult - - for _, result := range remoteResults { - if result.Problem != nil { - failures = append(failures, result) - } else { - successes = append(successes, result) - } - } - if len(failures) == 0 { - // There's no point logging a differential line if everything succeeded. - return - } - - logOb := struct { - Domain string - AccountID int64 - ChallengeType string - RemoteSuccesses int - RemoteFailures []*remoteVAResult - }{ - Domain: domain, - AccountID: acctID, - ChallengeType: challengeType, - RemoteSuccesses: len(successes), - RemoteFailures: failures, - } - - logJSON, err := json.Marshal(logOb) - if err != nil { - // log a warning - a marshaling failure isn't expected given the data - // isn't critical enough to break validation by returning an error the - // caller. - va.log.Warningf("Could not marshal log object in "+ - "logRemoteDifferential: %s", err) - return - } - - va.log.Infof("remoteVADifferentials JSON=%s", string(logJSON)) +// validationLogEvent is a struct that contains the information needed to log +// the results of DoCAA and DoDCV. +type validationLogEvent struct { + AuthzID string + Requester int64 + Identifier identifier.ACMEIdentifier + Challenge core.Challenge + Error string `json:",omitempty"` + InternalError string `json:",omitempty"` + Latency float64 + Summary *mpicSummary `json:",omitempty"` } -// remoteVAResult is a struct that combines a problem details instance (that may -// be nil) with the remote VA hostname that produced it. -type remoteVAResult struct { - VAHostname string - Problem *probs.ProblemDetails -} - -// performLocalValidation performs primary domain control validation and then -// checks CAA. If either step fails, it immediately returns a bare error so -// that our audit logging can include the underlying error. -func (va *ValidationAuthorityImpl) performLocalValidation( - ctx context.Context, - ident identifier.ACMEIdentifier, - regid int64, - kind core.AcmeChallenge, - token string, - keyAuthorization string, -) ([]core.ValidationRecord, error) { - // Do primary domain control validation. Any kind of error returned by this - // counts as a validation error, and will be converted into an appropriate - // probs.ProblemDetails by the calling function. - records, err := va.validateChallenge(ctx, ident, kind, token, keyAuthorization) - if err != nil { - return records, err - } - - // Do primary CAA checks. Any kind of error returned by this counts as not - // receiving permission to issue, and will be converted into an appropriate - // probs.ProblemDetails by the calling function. - err = va.checkCAA(ctx, ident, &caaParams{ - accountURIID: regid, - validationMethod: kind, - }) - if err != nil { - return records, err - } - - return records, nil -} - -// PerformValidation validates the challenge for the domain in the request. -// The returned result will always contain a list of validation records, even -// when it also contains a problem. -func (va *ValidationAuthorityImpl) PerformValidation(ctx context.Context, req *vapb.PerformValidationRequest) (*vapb.ValidationResult, error) { - // TODO(#7514): Add req.ExpectedKeyAuthorization to this check - if core.IsAnyNilOrZero(req, req.Domain, req.Challenge, req.Authz) { +// DoDCV conducts a local Domain Control Validation (DCV) for the specified +// challenge. When invoked on the primary Validation Authority (VA) and the +// local validation succeeds, it also performs DCV validations using the +// configured remote VAs. Failed validations are indicated by a non-nil Problems +// in the returned ValidationResult. DoDCV returns error only for internal logic +// errors (and the client may receive errors from gRPC in the event of a +// communication problem). ValidationResult always includes a list of +// ValidationRecords, even when it also contains Problems. This method +// implements the DCV portion of Multi-Perspective Issuance Corroboration as +// defined in BRs Sections 3.2.2.9 and 5.4.1. +func (va *ValidationAuthorityImpl) DoDCV(ctx context.Context, req *vapb.PerformValidationRequest) (*vapb.ValidationResult, error) { + if core.IsAnyNilOrZero(req, req.Identifier, req.Challenge, req.Authz, req.ExpectedKeyAuthorization) { return nil, berrors.InternalServerError("Incomplete validation request") } - challenge, err := bgrpc.PBToChallenge(req.Challenge) + ident := identifier.FromProto(req.Identifier) + + chall, err := bgrpc.PBToChallenge(req.Challenge) if err != nil { return nil, errors.New("challenge failed to deserialize") } - err = challenge.CheckPending() + err = chall.CheckPending() if err != nil { return nil, berrors.MalformedError("challenge failed consistency check: %s", err) } - // TODO(#7514): Remove this fallback and belt-and-suspenders check. - keyAuthorization := req.ExpectedKeyAuthorization - if len(keyAuthorization) == 0 { - keyAuthorization = req.Challenge.KeyAuthorization - } - if len(keyAuthorization) == 0 { - return nil, errors.New("no expected keyAuthorization provided") - } - - // Set up variables and a deferred closure to report validation latency - // metrics and log validation errors. Below here, do not use := to redeclare - // `prob`, or this will fail. + // Initialize variables and a deferred function to handle validation latency + // metrics, log validation errors, and log an MPIC summary. Avoid using := + // to redeclare `prob`, `localLatency`, or `summary` below this point. var prob *probs.ProblemDetails + var summary *mpicSummary var localLatency time.Duration - vStart := va.clk.Now() - logEvent := verificationRequestEvent{ - ID: req.Authz.Id, - Requester: req.Authz.RegID, - Hostname: req.Domain, - Challenge: challenge, + start := va.clk.Now() + logEvent := validationLogEvent{ + AuthzID: req.Authz.Id, + Requester: req.Authz.RegID, + Identifier: ident, + Challenge: chall, } defer func() { - problemType := "" + probType := "" + outcome := fail if prob != nil { - problemType = string(prob.Type) - logEvent.Error = prob.Error() + probType = string(prob.Type) + logEvent.Error = prob.String() logEvent.Challenge.Error = prob logEvent.Challenge.Status = core.StatusInvalid } else { logEvent.Challenge.Status = core.StatusValid + outcome = pass + } + // Observe local validation latency (primary|remote). + va.observeLatency(opDCV, va.perspective, string(chall.Type), probType, outcome, localLatency) + if va.isPrimaryVA() { + // Observe total validation latency (primary+remote). + va.observeLatency(opDCV, allPerspectives, string(chall.Type), probType, outcome, va.clk.Since(start)) + logEvent.Summary = summary } - va.metrics.localValidationTime.With(prometheus.Labels{ - "type": string(logEvent.Challenge.Type), - "result": string(logEvent.Challenge.Status), - }).Observe(localLatency.Seconds()) - - va.metrics.validationTime.With(prometheus.Labels{ - "type": string(logEvent.Challenge.Type), - "result": string(logEvent.Challenge.Status), - "problem_type": problemType, - }).Observe(time.Since(vStart).Seconds()) - - logEvent.ValidationLatency = time.Since(vStart).Round(time.Millisecond).Seconds() + // Log the total validation latency. + logEvent.Latency = va.clk.Since(start).Round(time.Millisecond).Seconds() va.log.AuditObject("Validation result", logEvent) }() @@ -698,14 +710,16 @@ func (va *ValidationAuthorityImpl) PerformValidation(ctx context.Context, req *v // *before* checking whether it returned an error. These few checks are // carefully written to ensure that they work whether the local validation // was successful or not, and cannot themselves fail. - records, err := va.performLocalValidation( + records, err := va.validateChallenge( ctx, - identifier.DNSIdentifier(req.Domain), - req.Authz.RegID, - challenge.Type, - challenge.Token, - keyAuthorization) - localLatency = time.Since(vStart) + ident, + chall.Type, + chall.Token, + req.ExpectedKeyAuthorization, + ) + + // Stop the clock for local validation latency. + localLatency = va.clk.Since(start) // Check for malformed ValidationRecords logEvent.Challenge.ValidationRecord = records @@ -713,33 +727,26 @@ func (va *ValidationAuthorityImpl) PerformValidation(ctx context.Context, req *v err = errors.New("records from local validation failed sanity check") } - // Copy the "UsedRSAKEX" value from the last validationRecord into the log - // event. Only the last record should have this bool set, because we only - // record it if/when validation is finally successful, but we use the loop - // just in case that assumption changes. - // TODO(#7321): Remove this when we have collected enough data. - for _, record := range records { - logEvent.UsedRSAKEX = record.UsedRSAKEX || logEvent.UsedRSAKEX - } - if err != nil { logEvent.InternalError = err.Error() prob = detailedError(err) - return bgrpc.ValidationResultToPB(records, filterProblemDetails(prob)) + return bgrpc.ValidationResultToPB(records, filterProblemDetails(prob), va.perspective, va.rir) } - // Do remote validation. We do this after local validation is complete to - // avoid wasting work when validation will fail anyway. This only returns a - // singular problem, because the remote VAs have already audit-logged their - // own validation records, and it's not helpful to present multiple large - // errors to the end user. - prob = va.performRemoteValidation(ctx, req) - return bgrpc.ValidationResultToPB(records, filterProblemDetails(prob)) -} - -// usedRSAKEX returns true if the given cipher suite involves the use of an -// RSA key exchange mechanism. -// TODO(#7321): Remove this when we have collected enough data. -func usedRSAKEX(cs uint16) bool { - return strings.HasPrefix(tls.CipherSuiteName(cs), "TLS_RSA_") + if va.isPrimaryVA() { + // Do remote validation. We do this after local validation is complete + // to avoid wasting work when validation will fail anyway. This only + // returns a singular problem, because the remote VAs have already + // logged their own validationLogEvent, and it's not helpful to present + // multiple large errors to the end user. + op := func(ctx context.Context, remoteva RemoteVA, req proto.Message) (remoteResult, error) { + validationRequest, ok := req.(*vapb.PerformValidationRequest) + if !ok { + return nil, fmt.Errorf("got type %T, want *vapb.PerformValidationRequest", req) + } + return remoteva.DoDCV(ctx, validationRequest) + } + summary, prob = va.doRemoteOperation(ctx, op, req) + } + return bgrpc.ValidationResultToPB(records, filterProblemDetails(prob), va.perspective, va.rir) } diff --git a/third-party/github.com/letsencrypt/boulder/va/va_test.go b/third-party/github.com/letsencrypt/boulder/va/va_test.go index a7ca0ee06..df0526e50 100644 --- a/third-party/github.com/letsencrypt/boulder/va/va_test.go +++ b/third-party/github.com/letsencrypt/boulder/va/va_test.go @@ -10,6 +10,7 @@ import ( "net" "net/http" "net/http/httptest" + "net/netip" "os" "strings" "sync" @@ -26,6 +27,7 @@ import ( "github.com/letsencrypt/boulder/core" corepb "github.com/letsencrypt/boulder/core/proto" "github.com/letsencrypt/boulder/features" + "github.com/letsencrypt/boulder/iana" "github.com/letsencrypt/boulder/identifier" blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/metrics" @@ -34,10 +36,6 @@ import ( vapb "github.com/letsencrypt/boulder/va/proto" ) -var expectedToken = "LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0" -var expectedThumbprint = "9jg46WB3rR_AHD-EBXdN7cBkH1WOu0tA3M9fm21mqTI" -var expectedKeyAuthorization = ka(expectedToken) - func ka(token string) string { return token + "." + expectedThumbprint } @@ -53,24 +51,25 @@ func intFromB64(b64 string) int { return int(bigIntFromB64(b64).Int64()) } +// Any changes to this key must be reflected in //bdns/mocks.go, where values +// derived from it are hardcoded as the "correct" responses for DNS challenges. +// This key should not be used for anything other than computing Key +// Authorizations, i.e. it should not be used as the key to create a self-signed +// TLS-ALPN-01 certificate. var n = bigIntFromB64("n4EPtAOCc9AlkeQHPzHStgAbgs7bTZLwUBZdR8_KuKPEHLd4rHVTeT-O-XV2jRojdNhxJWTDvNd7nqQ0VEiZQHz_AJmSCpMaJMRBSFKrKb2wqVwGU_NsYOYL-QtiWN2lbzcEe6XC0dApr5ydQLrHqkHHig3RBordaZ6Aj-oBHqFEHYpPe7Tpe-OfVfHd1E6cS6M1FZcD1NNLYD5lFHpPI9bTwJlsde3uhGqC0ZCuEHg8lhzwOHrtIQbS0FVbb9k3-tVTU4fg_3L_vniUFAKwuCLqKnS2BYwdq_mzSnbLY7h_qixoR7jig3__kRhuaxwUkRz5iaiQkqgc5gHdrNP5zw==") var e = intFromB64("AQAB") var d = bigIntFromB64("bWUC9B-EFRIo8kpGfh0ZuyGPvMNKvYWNtB_ikiH9k20eT-O1q_I78eiZkpXxXQ0UTEs2LsNRS-8uJbvQ-A1irkwMSMkK1J3XTGgdrhCku9gRldY7sNA_AKZGh-Q661_42rINLRCe8W-nZ34ui_qOfkLnK9QWDDqpaIsA-bMwWWSDFu2MUBYwkHTMEzLYGqOe04noqeq1hExBTHBOBdkMXiuFhUq1BU6l-DqEiWxqg82sXt2h-LMnT3046AOYJoRioz75tSUQfGCshWTBnP5uDjd18kKhyv07lhfSJdrPdM5Plyl21hsFf4L_mHCuoFau7gdsPfHPxxjVOcOpBrQzwQ==") var p = bigIntFromB64("uKE2dh-cTf6ERF4k4e_jy78GfPYUIaUyoSSJuBzp3Cubk3OCqs6grT8bR_cu0Dm1MZwWmtdqDyI95HrUeq3MP15vMMON8lHTeZu2lmKvwqW7anV5UzhM1iZ7z4yMkuUwFWoBvyY898EXvRD-hdqRxHlSqAZ192zB3pVFJ0s7pFc=") var q = bigIntFromB64("uKE2dh-cTf6ERF4k4e_jy78GfPYUIaUyoSSJuBzp3Cubk3OCqs6grT8bR_cu0Dm1MZwWmtdqDyI95HrUeq3MP15vMMON8lHTeZu2lmKvwqW7anV5UzhM1iZ7z4yMkuUwFWoBvyY898EXvRD-hdqRxHlSqAZ192zB3pVFJ0s7pFc=") - var TheKey = rsa.PrivateKey{ PublicKey: rsa.PublicKey{N: n, E: e}, D: d, Primes: []*big.Int{p, q}, } - var accountKey = &jose.JSONWebKey{Key: TheKey.Public()} - -// Return an ACME DNS identifier for the given hostname -func dnsi(hostname string) identifier.ACMEIdentifier { - return identifier.DNSIdentifier(hostname) -} +var expectedToken = "LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0" +var expectedThumbprint = "9jg46WB3rR_AHD-EBXdN7cBkH1WOu0tA3M9fm21mqTI" +var expectedKeyAuthorization = ka(expectedToken) var ctx context.Context @@ -84,26 +83,40 @@ func TestMain(m *testing.M) { var accountURIPrefixes = []string{"http://boulder.service.consul:4000/acme/reg/"} -func createValidationRequest(domain string, challengeType core.AcmeChallenge) *vapb.PerformValidationRequest { +func createValidationRequest(ident identifier.ACMEIdentifier, challengeType core.AcmeChallenge) *vapb.PerformValidationRequest { return &vapb.PerformValidationRequest{ - Domain: domain, + Identifier: ident.ToProto(), Challenge: &corepb.Challenge{ Type: string(challengeType), Status: string(core.StatusPending), Token: expectedToken, Validationrecords: nil, - KeyAuthorization: expectedKeyAuthorization, }, Authz: &vapb.AuthzMeta{ Id: "", RegID: 1, }, + ExpectedKeyAuthorization: expectedKeyAuthorization, } } +// isNonLoopbackReservedIP is a mock reserved IP checker that permits loopback +// networks. +func isNonLoopbackReservedIP(ip netip.Addr) error { + loopbackV4 := netip.MustParsePrefix("127.0.0.0/8") + loopbackV6 := netip.MustParsePrefix("::1/128") + if loopbackV4.Contains(ip) || loopbackV6.Contains(ip) { + return nil + } + return iana.IsReservedAddr(ip) +} + // setup returns an in-memory VA and a mock logger. The default resolver client // is MockClient{}, but can be overridden. -func setup(srv *httptest.Server, maxRemoteFailures int, userAgent string, remoteVAs []RemoteVA, mockDNSClientOverride bdns.Client) (*ValidationAuthorityImpl, *blog.Mock) { +// +// If remoteVAs is nil, this builds a VA that acts like a remote (and does not +// perform multi-perspective validation). Otherwise it acts like a primary. +func setup(srv *httptest.Server, userAgent string, remoteVAs []RemoteVA, mockDNSClientOverride bdns.Client) (*ValidationAuthorityImpl, *blog.Mock) { features.Reset() fc := clock.NewFake() @@ -113,17 +126,29 @@ func setup(srv *httptest.Server, maxRemoteFailures int, userAgent string, remote userAgent = "user agent 1.0" } + perspective := PrimaryPerspective + if len(remoteVAs) == 0 { + // We're being set up as a remote. Use a distinct perspective from other remotes + // to better simulate what prod will be like. + perspective = "example perspective " + core.RandomString(4) + } + va, err := NewValidationAuthorityImpl( &bdns.MockClient{Log: logger}, - nil, - maxRemoteFailures, + remoteVAs, userAgent, "letsencrypt.org", metrics.NoopRegisterer, fc, logger, accountURIPrefixes, + perspective, + "", + isNonLoopbackReservedIP, ) + if err != nil { + panic(fmt.Sprintf("Failed to create validation authority: %v", err)) + } if mockDNSClientOverride != nil { va.dnsClient = mockDNSClientOverride @@ -137,19 +162,68 @@ func setup(srv *httptest.Server, maxRemoteFailures int, userAgent string, remote va.tlsPort = port } - if err != nil { - panic(fmt.Sprintf("Failed to create validation authority: %v", err)) - } - if remoteVAs != nil { - va.remoteVAs = remoteVAs - } return va, logger } -func setupRemote(srv *httptest.Server, userAgent string, mockDNSClientOverride bdns.Client) RemoteClients { - rva, _ := setup(srv, 0, userAgent, nil, mockDNSClientOverride) +func setupRemote(srv *httptest.Server, userAgent string, mockDNSClientOverride bdns.Client, perspective, rir string) RemoteClients { + rva, _ := setup(srv, userAgent, nil, mockDNSClientOverride) + rva.perspective = perspective + rva.rir = rir - return RemoteClients{VAClient: &inMemVA{*rva}, CAAClient: &inMemVA{*rva}} + return RemoteClients{VAClient: &inMemVA{rva}, CAAClient: &inMemVA{rva}} +} + +// RIRs +const ( + arin = "ARIN" + ripe = "RIPE" + apnic = "APNIC" + lacnic = "LACNIC" + afrinic = "AFRINIC" +) + +// remoteConf is used in conjunction with setupRemotes/withRemotes to configure +// a remote VA. +type remoteConf struct { + // ua is optional, will default to "user agent 1.0". When set to "broken" or + // "hijacked", the Address field of the resulting RemoteVA will be set to + // match. This is a bit hacky, but it's the easiest way to satisfy some of + // our existing TestMultiCAARechecking tests. + ua string + // rir is required. + rir string + // dns is optional. + dns bdns.Client + // impl is optional. + impl RemoteClients +} + +func setupRemotes(confs []remoteConf, srv *httptest.Server) []RemoteVA { + remoteVAs := make([]RemoteVA, 0, len(confs)) + for i, c := range confs { + if c.rir == "" { + panic("rir is required") + } + // perspective MUST be unique for each remote VA, otherwise the VA will + // fail to start. + perspective := fmt.Sprintf("dc-%d-%s", i, c.rir) + clients := setupRemote(srv, c.ua, c.dns, perspective, c.rir) + if c.impl != (RemoteClients{}) { + clients = c.impl + } + remoteVAs = append(remoteVAs, RemoteVA{ + RemoteClients: clients, + Perspective: perspective, + RIR: c.rir, + }) + } + + return remoteVAs +} + +func setupWithRemotes(srv *httptest.Server, userAgent string, remotes []remoteConf, mockDNSClientOverride bdns.Client) (*ValidationAuthorityImpl, *blog.Mock) { + remoteVAs := setupRemotes(remotes, srv) + return setup(srv, userAgent, remoteVAs, mockDNSClientOverride) } type multiSrv struct { @@ -159,14 +233,6 @@ type multiSrv struct { allowedUAs map[string]bool } -func (s *multiSrv) setAllowedUAs(allowedUAs map[string]bool) { - s.mu.Lock() - defer s.mu.Unlock() - s.allowedUAs = allowedUAs -} - -const slowRemoteSleepMillis = 1000 - func httpMultiSrv(t *testing.T, token string, allowedUAs map[string]bool) *multiSrv { t.Helper() m := http.NewServeMux() @@ -175,9 +241,6 @@ func httpMultiSrv(t *testing.T, token string, allowedUAs map[string]bool) *multi ms := &multiSrv{server, sync.Mutex{}, allowedUAs} m.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - if r.UserAgent() == "slow remote" { - time.Sleep(slowRemoteSleepMillis) - } ms.mu.Lock() defer ms.mu.Unlock() if ms.allowedUAs[r.UserAgent()] { @@ -197,11 +260,11 @@ func httpMultiSrv(t *testing.T, token string, allowedUAs map[string]bool) *multi // PerformValidation calls type cancelledVA struct{} -func (v cancelledVA) PerformValidation(_ context.Context, _ *vapb.PerformValidationRequest, _ ...grpc.CallOption) (*vapb.ValidationResult, error) { +func (v cancelledVA) DoDCV(_ context.Context, _ *vapb.PerformValidationRequest, _ ...grpc.CallOption) (*vapb.ValidationResult, error) { return nil, context.Canceled } -func (v cancelledVA) IsCAAValid(_ context.Context, _ *vapb.IsCAAValidRequest, _ ...grpc.CallOption) (*vapb.IsCAAValidResponse, error) { +func (v cancelledVA) DoCAA(_ context.Context, _ *vapb.IsCAAValidRequest, _ ...grpc.CallOption) (*vapb.IsCAAValidResponse, error) { return nil, context.Canceled } @@ -213,12 +276,12 @@ type brokenRemoteVA struct{} // PerformValidation and IsSafeDomain functions. var errBrokenRemoteVA = errors.New("brokenRemoteVA is broken") -// PerformValidation returns errBrokenRemoteVA unconditionally -func (b brokenRemoteVA) PerformValidation(_ context.Context, _ *vapb.PerformValidationRequest, _ ...grpc.CallOption) (*vapb.ValidationResult, error) { +// DoDCV returns errBrokenRemoteVA unconditionally +func (b brokenRemoteVA) DoDCV(_ context.Context, _ *vapb.PerformValidationRequest, _ ...grpc.CallOption) (*vapb.ValidationResult, error) { return nil, errBrokenRemoteVA } -func (b brokenRemoteVA) IsCAAValid(_ context.Context, _ *vapb.IsCAAValidRequest, _ ...grpc.CallOption) (*vapb.IsCAAValidResponse, error) { +func (b brokenRemoteVA) DoCAA(_ context.Context, _ *vapb.IsCAAValidRequest, _ ...grpc.CallOption) (*vapb.IsCAAValidResponse, error) { return nil, errBrokenRemoteVA } @@ -227,47 +290,124 @@ func (b brokenRemoteVA) IsCAAValid(_ context.Context, _ *vapb.IsCAAValidRequest, // ValidationAuthorityImpl rather than over the network. This lets a local // in-memory mock VA act like a remote VA. type inMemVA struct { - rva ValidationAuthorityImpl + rva *ValidationAuthorityImpl } -func (inmem inMemVA) PerformValidation(ctx context.Context, req *vapb.PerformValidationRequest, _ ...grpc.CallOption) (*vapb.ValidationResult, error) { - return inmem.rva.PerformValidation(ctx, req) +func (inmem *inMemVA) DoDCV(ctx context.Context, req *vapb.PerformValidationRequest, _ ...grpc.CallOption) (*vapb.ValidationResult, error) { + return inmem.rva.DoDCV(ctx, req) } -func (inmem inMemVA) IsCAAValid(ctx context.Context, req *vapb.IsCAAValidRequest, _ ...grpc.CallOption) (*vapb.IsCAAValidResponse, error) { - return inmem.rva.IsCAAValid(ctx, req) +func (inmem *inMemVA) DoCAA(ctx context.Context, req *vapb.IsCAAValidRequest, _ ...grpc.CallOption) (*vapb.IsCAAValidResponse, error) { + return inmem.rva.DoCAA(ctx, req) +} + +func TestNewValidationAuthorityImplWithDuplicateRemotes(t *testing.T) { + var remoteVAs []RemoteVA + for i := 0; i < 3; i++ { + remoteVAs = append(remoteVAs, RemoteVA{ + RemoteClients: setupRemote(nil, "", nil, "dadaist", arin), + Perspective: "dadaist", + RIR: arin, + }) + } + + _, err := NewValidationAuthorityImpl( + &bdns.MockClient{Log: blog.NewMock()}, + remoteVAs, + "user agent 1.0", + "letsencrypt.org", + metrics.NoopRegisterer, + clock.NewFake(), + blog.NewMock(), + accountURIPrefixes, + "example perspective", + "", + isNonLoopbackReservedIP, + ) + test.AssertError(t, err, "NewValidationAuthorityImpl allowed duplicate remote perspectives") + test.AssertContains(t, err.Error(), "duplicate remote VA perspective \"dadaist\"") +} + +func TestPerformValidationWithMismatchedRemoteVAPerspectives(t *testing.T) { + t.Parallel() + + mismatched1 := RemoteVA{ + RemoteClients: setupRemote(nil, "", nil, "dadaist", arin), + Perspective: "baroque", + RIR: arin, + } + mismatched2 := RemoteVA{ + RemoteClients: setupRemote(nil, "", nil, "impressionist", ripe), + Perspective: "minimalist", + RIR: ripe, + } + remoteVAs := setupRemotes([]remoteConf{{rir: ripe}}, nil) + remoteVAs = append(remoteVAs, mismatched1, mismatched2) + + va, mockLog := setup(nil, "", remoteVAs, nil) + req := createValidationRequest(identifier.NewDNS("good-dns01.com"), core.ChallengeTypeDNS01) + res, _ := va.DoDCV(context.Background(), req) + test.AssertNotNil(t, res.GetProblem(), "validation succeeded with mismatched remote VA perspectives") + test.AssertEquals(t, len(mockLog.GetAllMatching("Expected perspective")), 2) +} + +func TestPerformValidationWithMismatchedRemoteVARIRs(t *testing.T) { + t.Parallel() + + mismatched1 := RemoteVA{ + RemoteClients: setupRemote(nil, "", nil, "dadaist", arin), + Perspective: "dadaist", + RIR: ripe, + } + mismatched2 := RemoteVA{ + RemoteClients: setupRemote(nil, "", nil, "impressionist", ripe), + Perspective: "impressionist", + RIR: arin, + } + remoteVAs := setupRemotes([]remoteConf{{rir: ripe}}, nil) + remoteVAs = append(remoteVAs, mismatched1, mismatched2) + + va, mockLog := setup(nil, "", remoteVAs, nil) + req := createValidationRequest(identifier.NewDNS("good-dns01.com"), core.ChallengeTypeDNS01) + res, _ := va.DoDCV(context.Background(), req) + test.AssertNotNil(t, res.GetProblem(), "validation succeeded with mismatched remote VA perspectives") + test.AssertEquals(t, len(mockLog.GetAllMatching("Expected perspective")), 2) } func TestValidateMalformedChallenge(t *testing.T) { - va, _ := setup(nil, 0, "", nil, nil) + va, _ := setup(nil, "", nil, nil) - _, err := va.validateChallenge(ctx, dnsi("example.com"), "fake-type-01", expectedToken, expectedKeyAuthorization) + _, err := va.validateChallenge(ctx, identifier.NewDNS("example.com"), "fake-type-01", expectedToken, expectedKeyAuthorization) prob := detailedError(err) test.AssertEquals(t, prob.Type, probs.MalformedProblem) } func TestPerformValidationInvalid(t *testing.T) { - va, _ := setup(nil, 0, "", nil, nil) + t.Parallel() + va, _ := setup(nil, "", nil, nil) - req := createValidationRequest("foo.com", core.ChallengeTypeDNS01) - res, _ := va.PerformValidation(context.Background(), req) - test.Assert(t, res.Problems != nil, "validation succeeded") - - test.AssertMetricWithLabelsEquals(t, va.metrics.validationTime, prometheus.Labels{ - "type": "dns-01", - "result": "invalid", - "problem_type": "unauthorized", + req := createValidationRequest(identifier.NewDNS("foo.com"), core.ChallengeTypeDNS01) + res, _ := va.DoDCV(context.Background(), req) + test.Assert(t, res.Problem != nil, "validation succeeded") + test.AssertMetricWithLabelsEquals(t, va.metrics.validationLatency, prometheus.Labels{ + "operation": opDCV, + "perspective": va.perspective, + "challenge_type": string(core.ChallengeTypeDNS01), + "problem_type": string(probs.UnauthorizedProblem), + "result": fail, }, 1) } func TestInternalErrorLogged(t *testing.T) { - va, mockLog := setup(nil, 0, "", nil, nil) + t.Parallel() + + va, mockLog := setup(nil, "", nil, nil) ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond) defer cancel() - req := createValidationRequest("nonexistent.com", core.ChallengeTypeHTTP01) - _, err := va.PerformValidation(ctx, req) + req := createValidationRequest(identifier.NewDNS("nonexistent.com"), core.ChallengeTypeHTTP01) + _, err := va.DoDCV(ctx, req) test.AssertNotError(t, err, "failed validation should not be an error") matchingLogs := mockLog.GetAllMatching( `Validation result JSON=.*"InternalError":"127.0.0.1: Get.*nonexistent.com/\.well-known.*: context deadline exceeded`) @@ -275,51 +415,58 @@ func TestInternalErrorLogged(t *testing.T) { } func TestPerformValidationValid(t *testing.T) { - va, mockLog := setup(nil, 0, "", nil, nil) + t.Parallel() + + va, mockLog := setup(nil, "", nil, nil) // create a challenge with well known token - req := createValidationRequest("good-dns01.com", core.ChallengeTypeDNS01) - res, _ := va.PerformValidation(context.Background(), req) - test.Assert(t, res.Problems == nil, fmt.Sprintf("validation failed: %#v", res.Problems)) - - test.AssertMetricWithLabelsEquals(t, va.metrics.validationTime, prometheus.Labels{ - "type": "dns-01", - "result": "valid", - "problem_type": "", + req := createValidationRequest(identifier.NewDNS("good-dns01.com"), core.ChallengeTypeDNS01) + res, _ := va.DoDCV(context.Background(), req) + test.Assert(t, res.Problem == nil, fmt.Sprintf("validation failed: %#v", res.Problem)) + test.AssertMetricWithLabelsEquals(t, va.metrics.validationLatency, prometheus.Labels{ + "operation": opDCV, + "perspective": va.perspective, + "challenge_type": string(core.ChallengeTypeDNS01), + "problem_type": "", + "result": pass, }, 1) resultLog := mockLog.GetAllMatching(`Validation result`) if len(resultLog) != 1 { t.Fatalf("Wrong number of matching lines for 'Validation result'") } - if !strings.Contains(resultLog[0], `"Hostname":"good-dns01.com"`) { - t.Error("PerformValidation didn't log validation hostname.") + + if !strings.Contains(resultLog[0], `"Identifier":{"type":"dns","value":"good-dns01.com"}`) { + t.Error("PerformValidation didn't log validation identifier.") } } // TestPerformValidationWildcard tests that the VA properly strips the `*.` // prefix from a wildcard name provided to the PerformValidation function. func TestPerformValidationWildcard(t *testing.T) { - va, mockLog := setup(nil, 0, "", nil, nil) + t.Parallel() + + va, mockLog := setup(nil, "", nil, nil) // create a challenge with well known token - req := createValidationRequest("*.good-dns01.com", core.ChallengeTypeDNS01) + req := createValidationRequest(identifier.NewDNS("*.good-dns01.com"), core.ChallengeTypeDNS01) // perform a validation for a wildcard name - res, _ := va.PerformValidation(context.Background(), req) - test.Assert(t, res.Problems == nil, fmt.Sprintf("validation failed: %#v", res.Problems)) - - test.AssertMetricWithLabelsEquals(t, va.metrics.validationTime, prometheus.Labels{ - "type": "dns-01", - "result": "valid", - "problem_type": "", + res, _ := va.DoDCV(context.Background(), req) + test.Assert(t, res.Problem == nil, fmt.Sprintf("validation failed: %#v", res.Problem)) + test.AssertMetricWithLabelsEquals(t, va.metrics.validationLatency, prometheus.Labels{ + "operation": opDCV, + "perspective": va.perspective, + "challenge_type": string(core.ChallengeTypeDNS01), + "problem_type": "", + "result": pass, }, 1) resultLog := mockLog.GetAllMatching(`Validation result`) if len(resultLog) != 1 { t.Fatalf("Wrong number of matching lines for 'Validation result'") } - // We expect that the top level Hostname reflect the wildcard name - if !strings.Contains(resultLog[0], `"Hostname":"*.good-dns01.com"`) { - t.Errorf("PerformValidation didn't log correct validation hostname.") + // We expect that the top level Identifier reflect the wildcard name + if !strings.Contains(resultLog[0], `"Identifier":{"type":"dns","value":"*.good-dns01.com"}`) { + t.Errorf("PerformValidation didn't log correct validation identifier.") } // We expect that the ValidationRecord contain the correct non-wildcard // hostname that was validated @@ -328,53 +475,12 @@ func TestPerformValidationWildcard(t *testing.T) { } } -func TestDCVAndCAASequencing(t *testing.T) { - va, mockLog := setup(nil, 0, "", nil, nil) - - // When validation succeeds, CAA should be checked. - mockLog.Clear() - req := createValidationRequest("good-dns01.com", core.ChallengeTypeDNS01) - res, err := va.PerformValidation(context.Background(), req) - test.AssertNotError(t, err, "performing validation") - test.Assert(t, res.Problems == nil, fmt.Sprintf("validation failed: %#v", res.Problems)) - caaLog := mockLog.GetAllMatching(`Checked CAA records for`) - test.AssertEquals(t, len(caaLog), 1) - - // When validation fails, CAA should be skipped. - mockLog.Clear() - req = createValidationRequest("bad-dns01.com", core.ChallengeTypeDNS01) - res, err = va.PerformValidation(context.Background(), req) - test.AssertNotError(t, err, "performing validation") - test.Assert(t, res.Problems != nil, "validation succeeded") - caaLog = mockLog.GetAllMatching(`Checked CAA records for`) - test.AssertEquals(t, len(caaLog), 0) -} - func TestMultiVA(t *testing.T) { + t.Parallel() + // Create a new challenge to use for the httpSrv - req := createValidationRequest("localhost", core.ChallengeTypeHTTP01) + req := createValidationRequest(identifier.NewDNS("localhost"), core.ChallengeTypeHTTP01) - const ( - remoteUA1 = "remote 1" - remoteUA2 = "remote 2" - localUA = "local 1" - ) - allowedUAs := map[string]bool{ - localUA: true, - remoteUA1: true, - remoteUA2: true, - } - - // Create an IPv4 test server - ms := httpMultiSrv(t, expectedToken, allowedUAs) - defer ms.Close() - - remoteVA1 := setupRemote(ms.Server, remoteUA1, nil) - remoteVA2 := setupRemote(ms.Server, remoteUA2, nil) - remoteVAs := []RemoteVA{ - {remoteVA1, remoteUA1}, - {remoteVA2, remoteUA2}, - } brokenVA := RemoteClients{ VAClient: brokenRemoteVA{}, CAAClient: brokenRemoteVA{}, @@ -384,208 +490,245 @@ func TestMultiVA(t *testing.T) { CAAClient: cancelledVA{}, } - unauthorized := probs.Unauthorized(fmt.Sprintf( - `The key authorization file from the server did not match this challenge. Expected %q (got "???")`, - expectedKeyAuthorization)) - expectedInternalErrLine := fmt.Sprintf( - `ERR: \[AUDIT\] Remote VA "broken".PerformValidation failed: %s`, - errBrokenRemoteVA.Error()) testCases := []struct { - Name string - RemoteVAs []RemoteVA - AllowedUAs map[string]bool - ExpectedProb *probs.ProblemDetails - ExpectedLog string + Name string + Remotes []remoteConf + PrimaryUA string + ExpectedProbType string + ExpectedLogContains string }{ { - // With local and both remote VAs working there should be no problem. - Name: "Local and remote VAs OK", - RemoteVAs: remoteVAs, - AllowedUAs: allowedUAs, + // With local and all remote VAs working there should be no problem. + Name: "Local and remote VAs OK", + Remotes: []remoteConf{ + {ua: pass, rir: arin}, + {ua: pass, rir: ripe}, + {ua: pass, rir: apnic}, + }, + PrimaryUA: pass, }, { // If the local VA fails everything should fail - Name: "Local VA bad, remote VAs OK", - RemoteVAs: remoteVAs, - AllowedUAs: map[string]bool{remoteUA1: true, remoteUA2: true}, - ExpectedProb: unauthorized, + Name: "Local VA bad, remote VAs OK", + Remotes: []remoteConf{ + {ua: pass, rir: arin}, + {ua: pass, rir: ripe}, + {ua: pass, rir: apnic}, + }, + PrimaryUA: fail, + ExpectedProbType: string(probs.UnauthorizedProblem), }, { - // If a remote VA fails with an internal err it should fail - Name: "Local VA ok, remote VA internal err", - RemoteVAs: []RemoteVA{ - {remoteVA1, remoteUA1}, - {brokenVA, "broken"}, + // If one out of three remote VAs fails with an internal err it should succeed + Name: "Local VA ok, 1/3 remote VA internal err", + Remotes: []remoteConf{ + {ua: pass, rir: arin}, + {ua: pass, rir: ripe}, + {ua: pass, rir: apnic, impl: brokenVA}, }, - AllowedUAs: allowedUAs, - ExpectedProb: probs.ServerInternal("During secondary validation: Remote PerformValidation RPC failed"), + PrimaryUA: pass, + }, + { + // If two out of three remote VAs fail with an internal err it should fail + Name: "Local VA ok, 2/3 remote VAs internal err", + Remotes: []remoteConf{ + {ua: pass, rir: arin}, + {ua: pass, rir: ripe, impl: brokenVA}, + {ua: pass, rir: apnic, impl: brokenVA}, + }, + PrimaryUA: pass, + ExpectedProbType: string(probs.ServerInternalProblem), // The real failure cause should be logged - ExpectedLog: expectedInternalErrLine, + ExpectedLogContains: errBrokenRemoteVA.Error(), + }, + { + // If one out of five remote VAs fail with an internal err it should succeed + Name: "Local VA ok, 1/5 remote VAs internal err", + Remotes: []remoteConf{ + {ua: pass, rir: arin}, + {ua: pass, rir: ripe}, + {ua: pass, rir: apnic}, + {ua: pass, rir: lacnic}, + {ua: pass, rir: afrinic, impl: brokenVA}, + }, + PrimaryUA: pass, + }, + { + // If two out of five remote VAs fail with an internal err it should fail + Name: "Local VA ok, 2/5 remote VAs internal err", + Remotes: []remoteConf{ + {ua: pass, rir: arin}, + {ua: pass, rir: ripe}, + {ua: pass, rir: apnic}, + {ua: pass, rir: arin, impl: brokenVA}, + {ua: pass, rir: ripe, impl: brokenVA}, + }, + PrimaryUA: pass, + ExpectedProbType: string(probs.ServerInternalProblem), + // The real failure cause should be logged + ExpectedLogContains: errBrokenRemoteVA.Error(), + }, + { + // If two out of six remote VAs fail with an internal err it should succeed + Name: "Local VA ok, 2/6 remote VAs internal err", + Remotes: []remoteConf{ + {ua: pass, rir: arin}, + {ua: pass, rir: ripe}, + {ua: pass, rir: apnic}, + {ua: pass, rir: lacnic}, + {ua: pass, rir: afrinic, impl: brokenVA}, + {ua: pass, rir: arin, impl: brokenVA}, + }, + PrimaryUA: pass, + }, + { + // If three out of six remote VAs fail with an internal err it should fail + Name: "Local VA ok, 4/6 remote VAs internal err", + Remotes: []remoteConf{ + {ua: pass, rir: arin}, + {ua: pass, rir: ripe}, + {ua: pass, rir: apnic}, + {ua: pass, rir: lacnic, impl: brokenVA}, + {ua: pass, rir: afrinic, impl: brokenVA}, + {ua: pass, rir: arin, impl: brokenVA}, + }, + PrimaryUA: pass, + ExpectedProbType: string(probs.ServerInternalProblem), + // The real failure cause should be logged + ExpectedLogContains: errBrokenRemoteVA.Error(), }, { // With only one working remote VA there should be a validation failure - Name: "Local VA and one remote VA OK", - RemoteVAs: remoteVAs, - AllowedUAs: map[string]bool{localUA: true, remoteUA2: true}, - ExpectedProb: probs.Unauthorized(fmt.Sprintf( - `During secondary validation: The key authorization file from the server did not match this challenge. Expected %q (got "???")`, - expectedKeyAuthorization)), + Name: "Local VA and one remote VA OK", + Remotes: []remoteConf{ + {ua: pass, rir: arin}, + {ua: fail, rir: ripe}, + {ua: fail, rir: apnic}, + }, + PrimaryUA: pass, + ExpectedProbType: string(probs.UnauthorizedProblem), + ExpectedLogContains: "During secondary validation: The key authorization file from the server", }, { - // Any remote VA cancellations are a problem. + // If one remote VA cancels, it should succeed Name: "Local VA and one remote VA OK, one cancelled VA", - RemoteVAs: []RemoteVA{ - {remoteVA1, remoteUA1}, - {cancelledVA, remoteUA2}, + Remotes: []remoteConf{ + {ua: pass, rir: arin}, + {ua: pass, rir: ripe, impl: cancelledVA}, + {ua: pass, rir: apnic}, }, - AllowedUAs: allowedUAs, - ExpectedProb: probs.ServerInternal("During secondary validation: Remote PerformValidation RPC canceled"), + PrimaryUA: pass, }, { - // Any remote VA cancellations are a problem. - Name: "Local VA OK, two cancelled remote VAs", - RemoteVAs: []RemoteVA{ - {cancelledVA, remoteUA1}, - {cancelledVA, remoteUA2}, + // If all remote VAs cancel, it should fail + Name: "Local VA OK, three cancelled remote VAs", + Remotes: []remoteConf{ + {ua: pass, rir: arin, impl: cancelledVA}, + {ua: pass, rir: ripe, impl: cancelledVA}, + {ua: pass, rir: apnic, impl: cancelledVA}, }, - AllowedUAs: allowedUAs, - ExpectedProb: probs.ServerInternal("During secondary validation: Remote PerformValidation RPC canceled"), + PrimaryUA: pass, + ExpectedProbType: string(probs.ServerInternalProblem), + ExpectedLogContains: "During secondary validation: Secondary validation RPC canceled", }, { // With the local and remote VAs seeing diff problems, we expect a problem. - Name: "Local and remote VA differential, full results, enforce multi VA", - RemoteVAs: remoteVAs, - AllowedUAs: map[string]bool{localUA: true}, - ExpectedProb: probs.Unauthorized(fmt.Sprintf( - `During secondary validation: The key authorization file from the server did not match this challenge. Expected %q (got "???")`, - expectedKeyAuthorization)), + Name: "Local and remote VA differential", + Remotes: []remoteConf{ + {ua: fail, rir: arin}, + {ua: fail, rir: ripe}, + {ua: fail, rir: apnic}, + }, + PrimaryUA: pass, + ExpectedProbType: string(probs.UnauthorizedProblem), + ExpectedLogContains: "During secondary validation: The key authorization file from the server", }, } for _, tc := range testCases { t.Run(tc.Name, func(t *testing.T) { - // Configure the test server with the testcase allowed UAs. - ms.setAllowedUAs(tc.AllowedUAs) + t.Parallel() + + // Configure one test server per test case so that all tests can run in parallel. + ms := httpMultiSrv(t, expectedToken, map[string]bool{pass: true, fail: false}) + defer ms.Close() // Configure a primary VA with testcase remote VAs. - localVA, mockLog := setup(ms.Server, 0, localUA, tc.RemoteVAs, nil) + localVA, mockLog := setupWithRemotes(ms.Server, tc.PrimaryUA, tc.Remotes, nil) // Perform all validations - res, _ := localVA.PerformValidation(ctx, req) - if res.Problems == nil && tc.ExpectedProb != nil { - t.Errorf("expected prob %v, got nil", tc.ExpectedProb) - } else if res.Problems != nil && tc.ExpectedProb == nil { - t.Errorf("expected no prob, got %v", res.Problems) - } else if res.Problems != nil && tc.ExpectedProb != nil { + res, _ := localVA.DoDCV(ctx, req) + if res.Problem == nil && tc.ExpectedProbType != "" { + t.Errorf("expected prob %v, got nil", tc.ExpectedProbType) + } else if res.Problem != nil && tc.ExpectedProbType == "" { + t.Errorf("expected no prob, got %v", res.Problem) + } else if res.Problem != nil && tc.ExpectedProbType != "" { // That result should match expected. - test.AssertEquals(t, res.Problems.ProblemType, string(tc.ExpectedProb.Type)) - test.AssertEquals(t, res.Problems.Detail, tc.ExpectedProb.Detail) + test.AssertEquals(t, res.Problem.ProblemType, tc.ExpectedProbType) } - if tc.ExpectedLog != "" { - lines := mockLog.GetAllMatching(tc.ExpectedLog) - if len(lines) != 1 { - t.Fatalf("Got log %v; expected %q", mockLog.GetAll(), tc.ExpectedLog) + if tc.ExpectedLogContains != "" { + lines := mockLog.GetAllMatching(tc.ExpectedLogContains) + if len(lines) == 0 { + t.Fatalf("Got log %v; expected %q", mockLog.GetAll(), tc.ExpectedLogContains) } } }) } } -func TestMultiVAEarlyReturn(t *testing.T) { - const ( - remoteUA1 = "remote 1" - remoteUA2 = "slow remote" - localUA = "local 1" - ) - allowedUAs := map[string]bool{ - localUA: true, - remoteUA1: false, // forbid UA 1 to provoke early return - remoteUA2: true, +func TestMultiVAPolicy(t *testing.T) { + t.Parallel() + + remoteConfs := []remoteConf{ + {ua: fail, rir: arin}, + {ua: fail, rir: ripe}, + {ua: fail, rir: apnic}, } - ms := httpMultiSrv(t, expectedToken, allowedUAs) + ms := httpMultiSrv(t, expectedToken, map[string]bool{pass: true, fail: false}) defer ms.Close() - remoteVA1 := setupRemote(ms.Server, remoteUA1, nil) - remoteVA2 := setupRemote(ms.Server, remoteUA2, nil) + // Create a local test VA with the remote VAs + localVA, _ := setupWithRemotes(ms.Server, pass, remoteConfs, nil) - remoteVAs := []RemoteVA{ - {remoteVA1, remoteUA1}, - {remoteVA2, remoteUA2}, - } - - // Create a local test VA with the two remote VAs - localVA, _ := setup(ms.Server, 0, localUA, remoteVAs, nil) - - // Perform all validations - start := time.Now() - req := createValidationRequest("localhost", core.ChallengeTypeHTTP01) - res, _ := localVA.PerformValidation(ctx, req) - - // It should always fail - if res.Problems == nil { + // Perform validation for a domain not in the disabledDomains list + req := createValidationRequest(identifier.NewDNS("letsencrypt.org"), core.ChallengeTypeHTTP01) + res, _ := localVA.DoDCV(ctx, req) + // It should fail + if res.Problem == nil { t.Error("expected prob from PerformValidation, got nil") } - - elapsed := time.Since(start).Round(time.Millisecond).Milliseconds() - - // The slow UA should sleep for `slowRemoteSleepMillis`. But the first remote - // VA should fail quickly and the early-return code should cause the overall - // overall validation to return a prob quickly (i.e. in less than half of - // `slowRemoteSleepMillis`). - if elapsed > slowRemoteSleepMillis/2 { - t.Errorf( - "Expected an early return from PerformValidation in < %d ms, took %d ms", - slowRemoteSleepMillis/2, elapsed) - } } -func TestMultiVAPolicy(t *testing.T) { - const ( - remoteUA1 = "remote 1" - remoteUA2 = "remote 2" - localUA = "local 1" - ) - // Forbid both remote UAs to ensure that multi-va fails - allowedUAs := map[string]bool{ - localUA: true, - remoteUA1: false, - remoteUA2: false, +func TestMultiVALogging(t *testing.T) { + t.Parallel() + + remoteConfs := []remoteConf{ + {ua: pass, rir: arin}, + {ua: pass, rir: ripe}, + {ua: pass, rir: apnic}, } - ms := httpMultiSrv(t, expectedToken, allowedUAs) + ms := httpMultiSrv(t, expectedToken, map[string]bool{pass: true, fail: false}) defer ms.Close() - remoteVA1 := setupRemote(ms.Server, remoteUA1, nil) - remoteVA2 := setupRemote(ms.Server, remoteUA2, nil) - - remoteVAs := []RemoteVA{ - {remoteVA1, remoteUA1}, - {remoteVA2, remoteUA2}, - } - - // Create a local test VA with the two remote VAs - localVA, _ := setup(ms.Server, 0, localUA, remoteVAs, nil) - - // Perform validation for a domain not in the disabledDomains list - req := createValidationRequest("letsencrypt.org", core.ChallengeTypeHTTP01) - res, _ := localVA.PerformValidation(ctx, req) - // It should fail - if res.Problems == nil { - t.Error("expected prob from PerformValidation, got nil") - } + va, _ := setupWithRemotes(ms.Server, pass, remoteConfs, nil) + req := createValidationRequest(identifier.NewDNS("letsencrypt.org"), core.ChallengeTypeHTTP01) + res, err := va.DoDCV(ctx, req) + test.Assert(t, res.Problem == nil, fmt.Sprintf("validation failed with: %#v", res.Problem)) + test.AssertNotError(t, err, "performing validation") } func TestDetailedError(t *testing.T) { cases := []struct { err error - ip net.IP + ip netip.Addr expected string }{ { err: ipError{ - ip: net.ParseIP("192.168.1.1"), + ip: netip.MustParseAddr("192.168.1.1"), err: &net.OpError{ Op: "dial", Net: "tcp", @@ -617,7 +760,7 @@ func TestDetailedError(t *testing.T) { Err: syscall.ECONNRESET, }, }, - ip: nil, + ip: netip.Addr{}, expected: "Connection reset by peer", }, } @@ -628,71 +771,3 @@ func TestDetailedError(t *testing.T) { } } } - -func TestLogRemoteDifferentials(t *testing.T) { - // Create some remote VAs - remoteVA1 := setupRemote(nil, "remote 1", nil) - remoteVA2 := setupRemote(nil, "remote 2", nil) - remoteVA3 := setupRemote(nil, "remote 3", nil) - remoteVAs := []RemoteVA{ - {remoteVA1, "remote 1"}, - {remoteVA2, "remote 2"}, - {remoteVA3, "remote 3"}, - } - - // Set up a local VA that allows a max of 2 remote failures. - localVA, mockLog := setup(nil, 2, "local 1", remoteVAs, nil) - - egProbA := probs.DNS("root DNS servers closed at 4:30pm") - egProbB := probs.OrderNotReady("please take a number") - - testCases := []struct { - name string - remoteProbs []*remoteVAResult - expectedLog string - }{ - { - name: "all results equal (nil)", - remoteProbs: []*remoteVAResult{ - {Problem: nil, VAHostname: "remoteA"}, - {Problem: nil, VAHostname: "remoteB"}, - {Problem: nil, VAHostname: "remoteC"}, - }, - }, - { - name: "all results equal (not nil)", - remoteProbs: []*remoteVAResult{ - {Problem: egProbA, VAHostname: "remoteA"}, - {Problem: egProbA, VAHostname: "remoteB"}, - {Problem: egProbA, VAHostname: "remoteC"}, - }, - expectedLog: `INFO: remoteVADifferentials JSON={"Domain":"example.com","AccountID":1999,"ChallengeType":"blorpus-01","RemoteSuccesses":0,"RemoteFailures":[{"VAHostname":"remoteA","Problem":{"type":"dns","detail":"root DNS servers closed at 4:30pm","status":400}},{"VAHostname":"remoteB","Problem":{"type":"dns","detail":"root DNS servers closed at 4:30pm","status":400}},{"VAHostname":"remoteC","Problem":{"type":"dns","detail":"root DNS servers closed at 4:30pm","status":400}}]}`, - }, - { - name: "differing results, some non-nil", - remoteProbs: []*remoteVAResult{ - {Problem: nil, VAHostname: "remoteA"}, - {Problem: egProbB, VAHostname: "remoteB"}, - {Problem: nil, VAHostname: "remoteC"}, - }, - expectedLog: `INFO: remoteVADifferentials JSON={"Domain":"example.com","AccountID":1999,"ChallengeType":"blorpus-01","RemoteSuccesses":2,"RemoteFailures":[{"VAHostname":"remoteB","Problem":{"type":"orderNotReady","detail":"please take a number","status":403}}]}`, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - mockLog.Clear() - - localVA.logRemoteResults( - "example.com", 1999, "blorpus-01", tc.remoteProbs) - - lines := mockLog.GetAllMatching("remoteVADifferentials JSON=.*") - if tc.expectedLog != "" { - test.AssertEquals(t, len(lines), 1) - test.AssertEquals(t, lines[0], tc.expectedLog) - } else { - test.AssertEquals(t, len(lines), 0) - } - }) - } -} diff --git a/third-party/github.com/letsencrypt/boulder/web/context.go b/third-party/github.com/letsencrypt/boulder/web/context.go index 249438589..6c8a4afeb 100644 --- a/third-party/github.com/letsencrypt/boulder/web/context.go +++ b/third-party/github.com/letsencrypt/boulder/web/context.go @@ -7,14 +7,32 @@ import ( "crypto/rsa" "encoding/json" "fmt" - "net" "net/http" + "net/netip" "strings" "time" + "github.com/letsencrypt/boulder/features" + "github.com/letsencrypt/boulder/identifier" blog "github.com/letsencrypt/boulder/log" ) +type userAgentContextKey struct{} + +func UserAgent(ctx context.Context) string { + // The below type assertion is safe because this context key can only be + // set by this package and is only set to a string. + val, ok := ctx.Value(userAgentContextKey{}).(string) + if !ok { + return "" + } + return val +} + +func WithUserAgent(ctx context.Context, ua string) context.Context { + return context.WithValue(ctx, userAgentContextKey{}, ua) +} + // RequestEvent is a structured record of the metadata we care about for a // single web request. It is generated when a request is received, passed to // the request handler which can populate its fields as appropriate, and then @@ -34,7 +52,12 @@ type RequestEvent struct { Slug string `json:",omitempty"` InternalErrors []string `json:",omitempty"` Error string `json:",omitempty"` - UserAgent string `json:"ua,omitempty"` + // If there is an error checking the data store for our rate limits + // we ignore it, but attach the error to the log event for analysis. + // TODO(#7796): Treat errors from the rate limit system as normal + // errors and put them into InternalErrors. + IgnoredRateLimitError string `json:",omitempty"` + UserAgent string `json:"ua,omitempty"` // Origin is sent by the browser from XHR-based clients. Origin string `json:",omitempty"` Extra map[string]interface{} `json:",omitempty"` @@ -45,12 +68,9 @@ type RequestEvent struct { // For challenge and authorization GETs and POSTs: // the status of the authorization at the time the request began. Status string `json:",omitempty"` - // The DNS name, if there is a single relevant name, for instance - // in an authorization or challenge request. - DNSName string `json:",omitempty"` - // The set of DNS names, if there are potentially multiple relevant - // names, for instance in a new-order, finalize, or revoke request. - DNSNames []string `json:",omitempty"` + // The set of identifiers, for instance in an authorization, challenge, + // new-order, finalize, or revoke request. + Identifiers identifier.ACMEIdentifiers `json:",omitempty"` // For challenge POSTs, the challenge type. ChallengeType string `json:",omitempty"` @@ -116,23 +136,32 @@ func (r *responseWriterWithStatus) WriteHeader(code int) { func (th *TopHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Check that this header is well-formed, since we assume it is when logging. realIP := r.Header.Get("X-Real-IP") - if net.ParseIP(realIP) == nil { + _, err := netip.ParseAddr(realIP) + if err != nil { realIP = "0.0.0.0" } + userAgent := r.Header.Get("User-Agent") + logEvent := &RequestEvent{ RealIP: realIP, Method: r.Method, - UserAgent: r.Header.Get("User-Agent"), + UserAgent: userAgent, Origin: r.Header.Get("Origin"), Extra: make(map[string]interface{}), } - // We specifically override the default r.Context() because we would prefer - // for clients to not be able to cancel our operations in arbitrary places. - // Instead we start a new context, and apply timeouts in our various RPCs. - ctx := context.WithoutCancel(r.Context()) + + ctx := WithUserAgent(r.Context(), userAgent) r = r.WithContext(ctx) + if !features.Get().PropagateCancels { + // We specifically override the default r.Context() because we would prefer + // for clients to not be able to cancel our operations in arbitrary places. + // Instead we start a new context, and apply timeouts in our various RPCs. + ctx := context.WithoutCancel(r.Context()) + r = r.WithContext(ctx) + } + // Some clients will send a HTTP Host header that includes the default port // for the scheme that they are using. Previously when we were fronted by // Akamai they would rewrite the header and strip out the unnecessary port, diff --git a/third-party/github.com/letsencrypt/boulder/web/context_test.go b/third-party/github.com/letsencrypt/boulder/web/context_test.go index a5e806c55..ed98597cd 100644 --- a/third-party/github.com/letsencrypt/boulder/web/context_test.go +++ b/third-party/github.com/letsencrypt/boulder/web/context_test.go @@ -2,13 +2,16 @@ package web import ( "bytes" + "context" "crypto/tls" "fmt" "net/http" "net/http/httptest" "strings" "testing" + "time" + "github.com/letsencrypt/boulder/features" blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/test" ) @@ -117,3 +120,36 @@ func TestHostHeaderRewrite(t *testing.T) { req.Host = "localhost:123" th.ServeHTTP(httptest.NewRecorder(), req) } + +type cancelHandler struct { + res chan string +} + +func (ch cancelHandler) ServeHTTP(e *RequestEvent, w http.ResponseWriter, r *http.Request) { + select { + case <-r.Context().Done(): + ch.res <- r.Context().Err().Error() + case <-time.After(300 * time.Millisecond): + ch.res <- "300 ms passed" + } +} + +func TestPropagateCancel(t *testing.T) { + mockLog := blog.UseMock() + res := make(chan string) + features.Set(features.Config{PropagateCancels: true}) + th := NewTopHandler(mockLog, cancelHandler{res}) + ctx, cancel := context.WithCancel(context.Background()) + go func() { + req, err := http.NewRequestWithContext(ctx, "GET", "/thisisignored", &bytes.Reader{}) + if err != nil { + t.Error(err) + } + th.ServeHTTP(httptest.NewRecorder(), req) + }() + cancel() + result := <-res + if result != "context canceled" { + t.Errorf("expected 'context canceled', got %q", result) + } +} diff --git a/third-party/github.com/letsencrypt/boulder/web/probs.go b/third-party/github.com/letsencrypt/boulder/web/probs.go index 31f8596c0..1f1c9c8a9 100644 --- a/third-party/github.com/letsencrypt/boulder/web/probs.go +++ b/third-party/github.com/letsencrypt/boulder/web/probs.go @@ -40,6 +40,8 @@ func problemDetailsForBoulderError(err *berrors.BoulderError, msg string) *probs outProb = probs.BadPublicKey(fmt.Sprintf("%s :: %s", msg, err)) case berrors.BadCSR: outProb = probs.BadCSR(fmt.Sprintf("%s :: %s", msg, err)) + case berrors.AlreadyReplaced: + outProb = probs.AlreadyReplaced(fmt.Sprintf("%s :: %s", msg, err)) case berrors.AlreadyRevoked: outProb = probs.AlreadyRevoked(fmt.Sprintf("%s :: %s", msg, err)) case berrors.BadRevocationReason: @@ -48,6 +50,14 @@ func problemDetailsForBoulderError(err *berrors.BoulderError, msg string) *probs outProb = probs.UnsupportedContact(fmt.Sprintf("%s :: %s", msg, err)) case berrors.Conflict: outProb = probs.Conflict(fmt.Sprintf("%s :: %s", msg, err)) + case berrors.InvalidProfile: + outProb = probs.InvalidProfile(fmt.Sprintf("%s :: %s", msg, err)) + case berrors.BadSignatureAlgorithm: + outProb = probs.BadSignatureAlgorithm(fmt.Sprintf("%s :: %s", msg, err)) + case berrors.AccountDoesNotExist: + outProb = probs.AccountDoesNotExist(fmt.Sprintf("%s :: %s", msg, err)) + case berrors.BadNonce: + outProb = probs.BadNonce(fmt.Sprintf("%s :: %s", msg, err)) default: // Internal server error messages may include sensitive data, so we do // not include it. @@ -65,17 +75,13 @@ func problemDetailsForBoulderError(err *berrors.BoulderError, msg string) *probs return outProb } -// ProblemDetailsForError turns an error into a ProblemDetails with the special -// case of returning the same error back if its already a ProblemDetails. If the -// error is of an type unknown to ProblemDetailsForError, it will return a -// ServerInternal ProblemDetails. +// ProblemDetailsForError turns an error into a ProblemDetails. If the error is +// of an type unknown to ProblemDetailsForError, it will return a ServerInternal +// ProblemDetails. func ProblemDetailsForError(err error, msg string) *probs.ProblemDetails { - var probsProblemDetails *probs.ProblemDetails - var berrorsBoulderError *berrors.BoulderError - if errors.As(err, &probsProblemDetails) { - return probsProblemDetails - } else if errors.As(err, &berrorsBoulderError) { - return problemDetailsForBoulderError(berrorsBoulderError, msg) + var bErr *berrors.BoulderError + if errors.As(err, &bErr) { + return problemDetailsForBoulderError(bErr, msg) } else { // Internal server error messages may include sensitive data, so we do // not include it. diff --git a/third-party/github.com/letsencrypt/boulder/web/probs_test.go b/third-party/github.com/letsencrypt/boulder/web/probs_test.go index 130109cda..cd69093d9 100644 --- a/third-party/github.com/letsencrypt/boulder/web/probs_test.go +++ b/third-party/github.com/letsencrypt/boulder/web/probs_test.go @@ -11,7 +11,7 @@ import ( "github.com/letsencrypt/boulder/test" ) -func TestProblemDetailsFromError(t *testing.T) { +func TestProblemDetailsForError(t *testing.T) { // errMsg is used as the msg argument for `ProblemDetailsForError` and is // always returned in the problem detail. const errMsg = "testError" @@ -50,14 +50,6 @@ func TestProblemDetailsFromError(t *testing.T) { t.Errorf("Expected detailed message %q, got %q", c.detail, p.Detail) } } - - expected := &probs.ProblemDetails{ - Type: probs.MalformedProblem, - HTTPStatus: 200, - Detail: "gotcha", - } - p := ProblemDetailsForError(expected, "k") - test.AssertDeepEquals(t, expected, p) } func TestSubProblems(t *testing.T) { @@ -67,14 +59,14 @@ func TestSubProblems(t *testing.T) { }).WithSubErrors( []berrors.SubBoulderError{ { - Identifier: identifier.DNSIdentifier("threeletter.agency"), + Identifier: identifier.NewDNS("threeletter.agency"), BoulderError: &berrors.BoulderError{ Type: berrors.CAA, Detail: "Forbidden by ■■■■■■■■■■■ and directive ■■■■", }, }, { - Identifier: identifier.DNSIdentifier("area51.threeletter.agency"), + Identifier: identifier.NewDNS("area51.threeletter.agency"), BoulderError: &berrors.BoulderError{ Type: berrors.NotFound, Detail: "No Such Area...", diff --git a/third-party/github.com/letsencrypt/boulder/web/send_error.go b/third-party/github.com/letsencrypt/boulder/web/send_error.go index c0e68d707..8c0e8e0f7 100644 --- a/third-party/github.com/letsencrypt/boulder/web/send_error.go +++ b/third-party/github.com/letsencrypt/boulder/web/send_error.go @@ -37,8 +37,15 @@ func SendError( response.WriteHeader(http.StatusInternalServerError) } + // Suppress logging of the "Your account is temporarily prevented from + // requesting certificates" error. + var primaryDetail = prob.Detail + if prob.Type == probs.PausedProblem { + primaryDetail = "account/ident pair is paused" + } + // Record details to the log event - logEvent.Error = fmt.Sprintf("%d :: %s :: %s", prob.HTTPStatus, prob.Type, prob.Detail) + logEvent.Error = fmt.Sprintf("%d :: %s :: %s", prob.HTTPStatus, prob.Type, primaryDetail) if len(prob.SubProblems) > 0 { subDetails := make([]string, len(prob.SubProblems)) for i, sub := range prob.SubProblems { @@ -47,7 +54,7 @@ func SendError( logEvent.Error += fmt.Sprintf(" [%s]", strings.Join(subDetails, ", ")) } if ierr != nil { - logEvent.AddError(fmt.Sprintf("%s", ierr)) + logEvent.AddError("%s", ierr) } // Set the proper namespace for the problem and any sub-problems. diff --git a/third-party/github.com/letsencrypt/boulder/web/send_error_test.go b/third-party/github.com/letsencrypt/boulder/web/send_error_test.go index 4bdedee53..0360efe2f 100644 --- a/third-party/github.com/letsencrypt/boulder/web/send_error_test.go +++ b/third-party/github.com/letsencrypt/boulder/web/send_error_test.go @@ -8,6 +8,7 @@ import ( berrors "github.com/letsencrypt/boulder/errors" "github.com/letsencrypt/boulder/identifier" "github.com/letsencrypt/boulder/log" + "github.com/letsencrypt/boulder/probs" "github.com/letsencrypt/boulder/test" ) @@ -19,14 +20,14 @@ func TestSendErrorSubProblemNamespace(t *testing.T) { }).WithSubErrors( []berrors.SubBoulderError{ { - Identifier: identifier.DNSIdentifier("example.com"), + Identifier: identifier.NewDNS("example.com"), BoulderError: &berrors.BoulderError{ Type: berrors.Malformed, Detail: "nop", }, }, { - Identifier: identifier.DNSIdentifier("what about example.com"), + Identifier: identifier.NewDNS("what about example.com"), BoulderError: &berrors.BoulderError{ Type: berrors.Malformed, Detail: "nah", @@ -73,14 +74,14 @@ func TestSendErrorSubProbLogging(t *testing.T) { }).WithSubErrors( []berrors.SubBoulderError{ { - Identifier: identifier.DNSIdentifier("example.com"), + Identifier: identifier.NewDNS("example.com"), BoulderError: &berrors.BoulderError{ Type: berrors.Malformed, Detail: "nop", }, }, { - Identifier: identifier.DNSIdentifier("what about example.com"), + Identifier: identifier.NewDNS("what about example.com"), BoulderError: &berrors.BoulderError{ Type: berrors.Malformed, Detail: "nah", @@ -94,3 +95,11 @@ func TestSendErrorSubProbLogging(t *testing.T) { test.AssertEquals(t, logEvent.Error, `400 :: malformed :: dfoop :: bad ["example.com :: malformed :: dfoop :: nop", "what about example.com :: malformed :: dfoop :: nah"]`) } + +func TestSendErrorPausedProblemLoggingSuppression(t *testing.T) { + rw := httptest.NewRecorder() + logEvent := RequestEvent{} + SendError(log.NewMock(), rw, &logEvent, probs.Paused("I better not see any of this"), nil) + + test.AssertEquals(t, logEvent.Error, "429 :: rateLimited :: account/ident pair is paused") +} diff --git a/third-party/github.com/letsencrypt/boulder/web/server.go b/third-party/github.com/letsencrypt/boulder/web/server.go new file mode 100644 index 000000000..99606f075 --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/web/server.go @@ -0,0 +1,40 @@ +package web + +import ( + "bytes" + "fmt" + "log" + "net/http" + "time" + + blog "github.com/letsencrypt/boulder/log" +) + +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 +} + +// NewServer returns an http.Server which will listen on the given address, when +// started, for each path in the handler. Errors are sent to the given logger. +func NewServer(listenAddr string, handler http.Handler, logger blog.Logger) http.Server { + return http.Server{ + ReadTimeout: 30 * time.Second, + WriteTimeout: 120 * time.Second, + IdleTimeout: 120 * time.Second, + Addr: listenAddr, + ErrorLog: log.New(errorWriter{logger}, "", 0), + Handler: handler, + } +} diff --git a/third-party/github.com/letsencrypt/boulder/web/server_test.go b/third-party/github.com/letsencrypt/boulder/web/server_test.go new file mode 100644 index 000000000..c1f7ddbed --- /dev/null +++ b/third-party/github.com/letsencrypt/boulder/web/server_test.go @@ -0,0 +1,36 @@ +package web + +import ( + "context" + "errors" + "net/http" + "sync" + "testing" + + blog "github.com/letsencrypt/boulder/log" + "github.com/letsencrypt/boulder/test" +) + +func TestNewServer(t *testing.T) { + srv := NewServer(":0", nil, blog.NewMock()) + + var wg sync.WaitGroup + wg.Add(1) + go func() { + err := srv.ListenAndServe() + test.Assert(t, errors.Is(err, http.ErrServerClosed), "Could not start server") + wg.Done() + }() + + err := srv.Shutdown(context.TODO()) + test.AssertNotError(t, err, "Could not shut down server") + wg.Wait() +} + +func TestUnorderedShutdownIsFine(t *testing.T) { + srv := NewServer(":0", nil, blog.NewMock()) + err := srv.Shutdown(context.TODO()) + test.AssertNotError(t, err, "Could not shut down server") + err = srv.ListenAndServe() + test.Assert(t, errors.Is(err, http.ErrServerClosed), "Could not start server") +} diff --git a/third-party/github.com/letsencrypt/boulder/wfe2/stale.go b/third-party/github.com/letsencrypt/boulder/wfe2/stale.go deleted file mode 100644 index 0e423a82b..000000000 --- a/third-party/github.com/letsencrypt/boulder/wfe2/stale.go +++ /dev/null @@ -1,74 +0,0 @@ -package wfe2 - -import ( - "fmt" - "net/http" - "strings" - "time" - - "github.com/letsencrypt/boulder/core" - corepb "github.com/letsencrypt/boulder/core/proto" - "github.com/letsencrypt/boulder/probs" - "github.com/letsencrypt/boulder/web" -) - -// requiredStale checks if a request is a GET request with a logEvent indicating -// the endpoint starts with getAPIPrefix. If true then the caller is expected to -// apply staleness requirements via staleEnoughToGETOrder, staleEnoughToGETCert -// and staleEnoughToGETAuthz. -func requiredStale(req *http.Request, logEvent *web.RequestEvent) bool { - return req.Method == http.MethodGet && strings.HasPrefix(logEvent.Endpoint, getAPIPrefix) -} - -// staleEnoughToGETOrder checks if the given order was created long enough ago -// in the past to be acceptably stale for accessing via the Boulder specific GET -// API. -func (wfe *WebFrontEndImpl) staleEnoughToGETOrder(order *corepb.Order) *probs.ProblemDetails { - return wfe.staleEnoughToGET("Order", order.Created.AsTime()) -} - -// staleEnoughToGETCert checks if the given cert was issued long enough in the -// past to be acceptably stale for accessing via the Boulder specific GET API. -func (wfe *WebFrontEndImpl) staleEnoughToGETCert(cert *corepb.Certificate) *probs.ProblemDetails { - return wfe.staleEnoughToGET("Certificate", cert.Issued.AsTime()) -} - -// staleEnoughToGETAuthz checks if the given authorization was created long -// enough ago in the past to be acceptably stale for accessing via the Boulder -// specific GET API. Since authorization creation date is not tracked directly -// the appropriate lifetime for the authz is subtracted from the expiry to find -// the creation date. -func (wfe *WebFrontEndImpl) staleEnoughToGETAuthz(authzPB *corepb.Authorization) *probs.ProblemDetails { - // If the authorization was deactivated we cannot reliably tell what the creation date was - // because we can't easily tell if it was pending or finalized before deactivation. - // As these authorizations can no longer be used for anything, just make them immediately - // available for access. - if core.AcmeStatus(authzPB.Status) == core.StatusDeactivated { - return nil - } - // We don't directly track authorization creation time. Instead subtract the - // pendingAuthorization lifetime from the expiry. This will be inaccurate if - // we change the pendingAuthorizationLifetime but is sufficient for the weak - // staleness requirements of the GET API. - createdTime := authzPB.Expires.AsTime().Add(-wfe.pendingAuthorizationLifetime) - // if the authz is valid then we need to subtract the authorizationLifetime - // instead of the pendingAuthorizationLifetime. - if core.AcmeStatus(authzPB.Status) == core.StatusValid { - createdTime = authzPB.Expires.AsTime().Add(-wfe.authorizationLifetime) - } - return wfe.staleEnoughToGET("Authorization", createdTime) -} - -// staleEnoughToGET checks that the createDate for the given resource is at -// least wfe.staleTimeout in the past. If the resource is newer than the -// wfe.staleTimeout then an unauthorized problem is returned. -func (wfe *WebFrontEndImpl) staleEnoughToGET(resourceType string, createDate time.Time) *probs.ProblemDetails { - if wfe.clk.Since(createDate) < wfe.staleTimeout { - return probs.Unauthorized(fmt.Sprintf( - "%s is too new for GET API. "+ - "You should only use this non-standard API to access resources created more than %s ago", - resourceType, - wfe.staleTimeout)) - } - return nil -} diff --git a/third-party/github.com/letsencrypt/boulder/wfe2/stale_test.go b/third-party/github.com/letsencrypt/boulder/wfe2/stale_test.go deleted file mode 100644 index 662ddbbdd..000000000 --- a/third-party/github.com/letsencrypt/boulder/wfe2/stale_test.go +++ /dev/null @@ -1,78 +0,0 @@ -package wfe2 - -import ( - "net/http" - "testing" - "time" - - "github.com/jmhodges/clock" - "github.com/letsencrypt/boulder/core" - corepb "github.com/letsencrypt/boulder/core/proto" - "github.com/letsencrypt/boulder/test" - "github.com/letsencrypt/boulder/web" - "google.golang.org/protobuf/types/known/timestamppb" -) - -func TestRequiredStale(t *testing.T) { - testCases := []struct { - name string - req *http.Request - logEvent *web.RequestEvent - expectRequired bool - }{ - { - name: "not GET", - req: &http.Request{Method: http.MethodPost}, - logEvent: &web.RequestEvent{}, - expectRequired: false, - }, - { - name: "GET, not getAPIPrefix", - req: &http.Request{Method: http.MethodGet}, - logEvent: &web.RequestEvent{}, - expectRequired: false, - }, - { - name: "GET, getAPIPrefix", - req: &http.Request{Method: http.MethodGet}, - logEvent: &web.RequestEvent{Endpoint: getAPIPrefix + "whatever"}, - expectRequired: true, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - test.AssertEquals(t, requiredStale(tc.req, tc.logEvent), tc.expectRequired) - }) - } -} - -func TestSaleEnoughToGETOrder(t *testing.T) { - fc := clock.NewFake() - wfe := WebFrontEndImpl{clk: fc, staleTimeout: time.Minute * 30} - fc.Add(time.Hour * 24) - created := fc.Now() - fc.Add(time.Hour) - prob := wfe.staleEnoughToGETOrder(&corepb.Order{ - Created: timestamppb.New(created), - }) - test.Assert(t, prob == nil, "wfe.staleEnoughToGETOrder returned a non-nil problem") -} - -func TestStaleEnoughToGETAuthzDeactivated(t *testing.T) { - fc := clock.NewFake() - wfe := WebFrontEndImpl{ - clk: fc, - staleTimeout: time.Minute * 30, - pendingAuthorizationLifetime: 7 * 24 * time.Hour, - authorizationLifetime: 30 * 24 * time.Hour, - } - fc.Add(time.Hour * 24) - expires := fc.Now().Add(wfe.authorizationLifetime) - fc.Add(time.Hour) - prob := wfe.staleEnoughToGETAuthz(&corepb.Authorization{ - Status: string(core.StatusDeactivated), - Expires: timestamppb.New(expires), - }) - test.Assert(t, prob == nil, "wfe.staleEnoughToGETOrder returned a non-nil problem") -} diff --git a/third-party/github.com/letsencrypt/boulder/wfe2/verify.go b/third-party/github.com/letsencrypt/boulder/wfe2/verify.go index 665048f15..6dc261376 100644 --- a/third-party/github.com/letsencrypt/boulder/wfe2/verify.go +++ b/third-party/github.com/letsencrypt/boulder/wfe2/verify.go @@ -26,7 +26,6 @@ import ( nb "github.com/letsencrypt/boulder/grpc/noncebalancer" "github.com/letsencrypt/boulder/nonce" noncepb "github.com/letsencrypt/boulder/nonce/proto" - "github.com/letsencrypt/boulder/probs" sapb "github.com/letsencrypt/boulder/sa/proto" "github.com/letsencrypt/boulder/web" ) @@ -52,7 +51,7 @@ func sigAlgorithmForKey(key *jose.JSONWebKey) (jose.SignatureAlgorithm, error) { return jose.ES512, nil } } - return "", errors.New("JWK contains unsupported key type (expected RSA, or ECDSA P-256, P-384, or P-521)") + return "", berrors.BadPublicKeyError("JWK contains unsupported key type (expected RSA, or ECDSA P-256, P-384, or P-521)") } // getSupportedAlgs returns a sorted slice of joseSignatureAlgorithm's from a @@ -74,7 +73,7 @@ func getSupportedAlgs() []jose.SignatureAlgorithm { func checkAlgorithm(key *jose.JSONWebKey, header jose.Header) error { sigHeaderAlg := jose.SignatureAlgorithm(header.Algorithm) if !slices.Contains(getSupportedAlgs(), sigHeaderAlg) { - return fmt.Errorf( + return berrors.BadSignatureAlgorithmError( "JWS signature header contains unsupported algorithm %q, expected one of %s", header.Algorithm, getSupportedAlgs(), ) @@ -85,10 +84,10 @@ func checkAlgorithm(key *jose.JSONWebKey, header jose.Header) error { return err } if sigHeaderAlg != expectedAlg { - return fmt.Errorf("JWS signature header algorithm %q does not match expected algorithm %q for JWK", sigHeaderAlg, string(expectedAlg)) + return berrors.MalformedError("JWS signature header algorithm %q does not match expected algorithm %q for JWK", sigHeaderAlg, string(expectedAlg)) } if key.Algorithm != "" && key.Algorithm != string(expectedAlg) { - return fmt.Errorf("JWK key header algorithm %q does not match expected algorithm %q for JWK", key.Algorithm, string(expectedAlg)) + return berrors.MalformedError("JWK key header algorithm %q does not match expected algorithm %q for JWK", key.Algorithm, string(expectedAlg)) } return nil } @@ -108,15 +107,14 @@ const ( // determine if the request being authenticated by the JWS is identified using // an embedded JWK or an embedded key ID. If no signatures are present, or // mutually exclusive authentication types are specified at the same time, a -// problem is returned. checkJWSAuthType is separate from enforceJWSAuthType so +// error is returned. checkJWSAuthType is separate from enforceJWSAuthType so // that endpoints that need to handle both embedded JWK and embedded key ID // requests can determine which type of request they have and act accordingly // (e.g. acme v2 cert revocation). -func checkJWSAuthType(header jose.Header) (jwsAuthType, *probs.ProblemDetails) { +func checkJWSAuthType(header jose.Header) (jwsAuthType, error) { // There must not be a Key ID *and* an embedded JWK if header.KeyID != "" && header.JSONWebKey != nil { - return invalidAuthType, probs.Malformed( - "jwk and kid header fields are mutually exclusive") + return invalidAuthType, berrors.MalformedError("jwk and kid header fields are mutually exclusive") } else if header.KeyID != "" { return embeddedKeyID, nil } else if header.JSONWebKey != nil { @@ -129,25 +127,25 @@ func checkJWSAuthType(header jose.Header) (jwsAuthType, *probs.ProblemDetails) { // enforceJWSAuthType enforces that the protected headers from a // bJSONWebSignature have the provided auth type. If there is an error // determining the auth type or if it is not the expected auth type then a -// problem is returned. +// error is returned. func (wfe *WebFrontEndImpl) enforceJWSAuthType( header jose.Header, - expectedAuthType jwsAuthType) *probs.ProblemDetails { + expectedAuthType jwsAuthType) error { // Check the auth type for the provided JWS - authType, prob := checkJWSAuthType(header) - if prob != nil { + authType, err := checkJWSAuthType(header) + if err != nil { wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSAuthTypeInvalid"}).Inc() - return prob + return err } - // If the auth type isn't the one expected return a sensible problem based on + // If the auth type isn't the one expected return a sensible error based on // what was expected if authType != expectedAuthType { wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSAuthTypeWrong"}).Inc() switch expectedAuthType { case embeddedKeyID: - return probs.Malformed("No Key ID in JWS header") + return berrors.MalformedError("No Key ID in JWS header") case embeddedJWK: - return probs.Malformed("No embedded JWK in JWS header") + return berrors.MalformedError("No embedded JWK in JWS header") } } return nil @@ -156,47 +154,45 @@ func (wfe *WebFrontEndImpl) enforceJWSAuthType( // validPOSTRequest checks a *http.Request to ensure it has the headers // a well-formed ACME POST request has, and to ensure there is a body to // process. -func (wfe *WebFrontEndImpl) validPOSTRequest(request *http.Request) *probs.ProblemDetails { +func (wfe *WebFrontEndImpl) validPOSTRequest(request *http.Request) error { // All POSTs should have an accompanying Content-Length header if _, present := request.Header["Content-Length"]; !present { wfe.stats.httpErrorCount.With(prometheus.Labels{"type": "ContentLengthRequired"}).Inc() - return probs.ContentLengthRequired() + return berrors.MalformedError("missing Content-Length header") } // Per 6.2 ALL POSTs should have the correct JWS Content-Type for flattened // JSON serialization. if _, present := request.Header["Content-Type"]; !present { wfe.stats.httpErrorCount.With(prometheus.Labels{"type": "NoContentType"}).Inc() - return probs.InvalidContentType(fmt.Sprintf("No Content-Type header on POST. Content-Type must be %q", - expectedJWSContentType)) + return berrors.MalformedError("No Content-Type header on POST. Content-Type must be %q", expectedJWSContentType) } if contentType := request.Header.Get("Content-Type"); contentType != expectedJWSContentType { wfe.stats.httpErrorCount.With(prometheus.Labels{"type": "WrongContentType"}).Inc() - return probs.InvalidContentType(fmt.Sprintf("Invalid Content-Type header on POST. Content-Type must be %q", - expectedJWSContentType)) + return berrors.MalformedError("Invalid Content-Type header on POST. Content-Type must be %q", expectedJWSContentType) } // Per 6.4.1 "Replay-Nonce" clients should not send a Replay-Nonce header in // the HTTP request, it needs to be part of the signed JWS request body if _, present := request.Header["Replay-Nonce"]; present { wfe.stats.httpErrorCount.With(prometheus.Labels{"type": "ReplayNonceOutsideJWS"}).Inc() - return probs.Malformed("HTTP requests should NOT contain Replay-Nonce header. Use JWS nonce field") + return berrors.MalformedError("HTTP requests should NOT contain Replay-Nonce header. Use JWS nonce field") } // All POSTs should have a non-nil body if request.Body == nil { wfe.stats.httpErrorCount.With(prometheus.Labels{"type": "NoPOSTBody"}).Inc() - return probs.Malformed("No body on POST") + return berrors.MalformedError("No body on POST") } return nil } // nonceWellFormed checks a JWS' Nonce header to ensure it is well-formed, -// otherwise a bad nonce problem is returned. This avoids unnecessary RPCs to +// otherwise a bad nonce error is returned. This avoids unnecessary RPCs to // the nonce redemption service. -func nonceWellFormed(nonceHeader string, prefixLen int) *probs.ProblemDetails { - errBadNonce := probs.BadNonce(fmt.Sprintf("JWS has an invalid anti-replay nonce: %q", nonceHeader)) +func nonceWellFormed(nonceHeader string, prefixLen int) error { + errBadNonce := berrors.BadNonceError("JWS has an invalid anti-replay nonce: %q", nonceHeader) if len(nonceHeader) <= prefixLen { // Nonce header was an unexpected length because there is either: // 1) no nonce, or @@ -216,19 +212,19 @@ func nonceWellFormed(nonceHeader string, prefixLen int) *probs.ProblemDetails { } // validNonce checks a JWS' Nonce header to ensure it is one that the -// nonceService knows about, otherwise a bad nonce problem is returned. +// nonceService knows about, otherwise a bad nonce error is returned. // NOTE: this function assumes the JWS has already been verified with the // correct public key. -func (wfe *WebFrontEndImpl) validNonce(ctx context.Context, header jose.Header) *probs.ProblemDetails { +func (wfe *WebFrontEndImpl) validNonce(ctx context.Context, header jose.Header) error { if len(header.Nonce) == 0 { wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSMissingNonce"}).Inc() - return probs.BadNonce("JWS has no anti-replay nonce") + return berrors.BadNonceError("JWS has no anti-replay nonce") } - prob := nonceWellFormed(header.Nonce, nonce.PrefixLen) - if prob != nil { + err := nonceWellFormed(header.Nonce, nonce.PrefixLen) + if err != nil { wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSMalformedNonce"}).Inc() - return prob + return err } // Populate the context with the nonce prefix and HMAC key. These are @@ -241,7 +237,7 @@ func (wfe *WebFrontEndImpl) validNonce(ctx context.Context, header jose.Header) if err != nil { rpcStatus, ok := status.FromError(err) if !ok || rpcStatus != nb.ErrNoBackendsMatchPrefix { - return web.ProblemDetailsForError(err, "failed to redeem nonce") + return fmt.Errorf("failed to redeem nonce: %w", err) } // ErrNoBackendsMatchPrefix suggests that the nonce backend, which @@ -254,7 +250,7 @@ func (wfe *WebFrontEndImpl) validNonce(ctx context.Context, header jose.Header) if !resp.Valid { wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSInvalidNonce"}).Inc() - return probs.BadNonce(fmt.Sprintf("JWS has an invalid anti-replay nonce: %q", header.Nonce)) + return berrors.BadNonceError("JWS has an invalid anti-replay nonce: %q", header.Nonce) } return nil } @@ -262,21 +258,21 @@ func (wfe *WebFrontEndImpl) validNonce(ctx context.Context, header jose.Header) // validPOSTURL checks the JWS' URL header against the expected URL based on the // HTTP request. This prevents a JWS intended for one endpoint being replayed // against a different endpoint. If the URL isn't present, is invalid, or -// doesn't match the HTTP request a problem is returned. +// doesn't match the HTTP request a error is returned. func (wfe *WebFrontEndImpl) validPOSTURL( request *http.Request, - header jose.Header) *probs.ProblemDetails { + header jose.Header) error { extraHeaders := header.ExtraHeaders // Check that there is at least one Extra Header if len(extraHeaders) == 0 { wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSNoExtraHeaders"}).Inc() - return probs.Malformed("JWS header parameter 'url' required") + return berrors.MalformedError("JWS header parameter 'url' required") } // Try to read a 'url' Extra Header as a string headerURL, ok := extraHeaders[jose.HeaderKey("url")].(string) if !ok || len(headerURL) == 0 { wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSMissingURL"}).Inc() - return probs.Malformed("JWS header parameter 'url' required") + return berrors.MalformedError("JWS header parameter 'url' required") } // Compute the URL we expect to be in the JWS based on the HTTP request expectedURL := url.URL{ @@ -288,17 +284,15 @@ func (wfe *WebFrontEndImpl) validPOSTURL( // header if expectedURL.String() != headerURL { wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSMismatchedURL"}).Inc() - return probs.Malformed(fmt.Sprintf( - "JWS header parameter 'url' incorrect. Expected %q got %q", - expectedURL.String(), headerURL)) + return berrors.MalformedError("JWS header parameter 'url' incorrect. Expected %q got %q", expectedURL.String(), headerURL) } return nil } // matchJWSURLs checks two JWS' URL headers are equal. This is used during key // rollover to check that the inner JWS URL matches the outer JWS URL. If the -// JWS URLs do not match a problem is returned. -func (wfe *WebFrontEndImpl) matchJWSURLs(outer, inner jose.Header) *probs.ProblemDetails { +// JWS URLs do not match a error is returned. +func (wfe *WebFrontEndImpl) matchJWSURLs(outer, inner jose.Header) error { // Verify that the outer JWS has a non-empty URL header. This is strictly // defensive since the expectation is that endpoints using `matchJWSURLs` // have received at least one of their JWS from calling validPOSTForAccount(), @@ -307,22 +301,20 @@ func (wfe *WebFrontEndImpl) matchJWSURLs(outer, inner jose.Header) *probs.Proble outerURL, ok := outer.ExtraHeaders[jose.HeaderKey("url")].(string) if !ok || len(outerURL) == 0 { wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "KeyRolloverOuterJWSNoURL"}).Inc() - return probs.Malformed("Outer JWS header parameter 'url' required") + return berrors.MalformedError("Outer JWS header parameter 'url' required") } // Verify the inner JWS has a non-empty URL header. innerURL, ok := inner.ExtraHeaders[jose.HeaderKey("url")].(string) if !ok || len(innerURL) == 0 { wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "KeyRolloverInnerJWSNoURL"}).Inc() - return probs.Malformed("Inner JWS header parameter 'url' required") + return berrors.MalformedError("Inner JWS header parameter 'url' required") } // Verify that the outer URL matches the inner URL if outerURL != innerURL { wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "KeyRolloverMismatchedURLs"}).Inc() - return probs.Malformed(fmt.Sprintf( - "Outer JWS 'url' value %q does not match inner JWS 'url' value %q", - outerURL, innerURL)) + return berrors.MalformedError("Outer JWS 'url' value %q does not match inner JWS 'url' value %q", outerURL, innerURL) } return nil @@ -337,9 +329,9 @@ type bJSONWebSignature struct { // parseJWS extracts a JSONWebSignature from a byte slice. If there is an error // reading the JWS or it is unacceptable (e.g. too many/too few signatures, -// presence of unprotected headers) a problem is returned, otherwise a +// presence of unprotected headers) a error is returned, otherwise a // *bJSONWebSignature is returned. -func (wfe *WebFrontEndImpl) parseJWS(body []byte) (*bJSONWebSignature, *probs.ProblemDetails) { +func (wfe *WebFrontEndImpl) parseJWS(body []byte) (*bJSONWebSignature, error) { // Parse the raw JWS JSON to check that: // * the unprotected Header field is not being used. // * the "signatures" member isn't present, just "signature". @@ -353,14 +345,14 @@ func (wfe *WebFrontEndImpl) parseJWS(body []byte) (*bJSONWebSignature, *probs.Pr err := json.Unmarshal(body, &unprotected) if err != nil { wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSUnmarshalFailed"}).Inc() - return nil, probs.Malformed("Parse error reading JWS") + return nil, berrors.MalformedError("Parse error reading JWS") } // ACME v2 never uses values from the unprotected JWS header. Reject JWS that // include unprotected headers. if unprotected.Header != nil { wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSUnprotectedHeaders"}).Inc() - return nil, probs.Malformed( + return nil, berrors.MalformedError( "JWS \"header\" field not allowed. All headers must be in \"protected\" field") } @@ -368,7 +360,7 @@ func (wfe *WebFrontEndImpl) parseJWS(body []byte) (*bJSONWebSignature, *probs.Pr // mandatory "signature" field. Reject JWS that include the "signatures" array. if len(unprotected.Signatures) > 0 { wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSMultiSig"}).Inc() - return nil, probs.Malformed( + return nil, berrors.MalformedError( "JWS \"signatures\" field not allowed. Only the \"signature\" field should contain a signature") } @@ -377,30 +369,40 @@ func (wfe *WebFrontEndImpl) parseJWS(body []byte) (*bJSONWebSignature, *probs.Pr bodyStr := string(body) parsedJWS, err := jose.ParseSigned(bodyStr, getSupportedAlgs()) if err != nil { + var unexpectedSignAlgoErr *jose.ErrUnexpectedSignatureAlgorithm + if errors.As(err, &unexpectedSignAlgoErr) { + wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSAlgorithmCheckFailed"}).Inc() + return nil, berrors.BadSignatureAlgorithmError( + "JWS signature header contains unsupported algorithm %q, expected one of %s", + unexpectedSignAlgoErr.Got, + getSupportedAlgs(), + ) + } + wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSParseError"}).Inc() - return nil, probs.Malformed("Parse error reading JWS") + return nil, berrors.MalformedError("Parse error reading JWS") } if len(parsedJWS.Signatures) > 1 { wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSTooManySignatures"}).Inc() - return nil, probs.Malformed("Too many signatures in POST body") + return nil, berrors.MalformedError("Too many signatures in POST body") } if len(parsedJWS.Signatures) == 0 { wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSNoSignatures"}).Inc() - return nil, probs.Malformed("POST JWS not signed") + return nil, berrors.MalformedError("POST JWS not signed") } if len(parsedJWS.Signatures) == 1 && len(parsedJWS.Signatures[0].Signature) == 0 { wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSEmptySignature"}).Inc() - return nil, probs.Malformed("POST JWS not signed") + return nil, berrors.MalformedError("POST JWS not signed") } return &bJSONWebSignature{parsedJWS}, nil } // parseJWSRequest extracts a bJSONWebSignature from an HTTP POST request's body using parseJWS. -func (wfe *WebFrontEndImpl) parseJWSRequest(request *http.Request) (*bJSONWebSignature, *probs.ProblemDetails) { +func (wfe *WebFrontEndImpl) parseJWSRequest(request *http.Request) (*bJSONWebSignature, error) { // Verify that the POST request has the expected headers - if prob := wfe.validPOSTRequest(request); prob != nil { - return nil, prob + if err := wfe.validPOSTRequest(request); err != nil { + return nil, err } // Read the POST request body's bytes. validPOSTRequest has already checked @@ -408,40 +410,40 @@ func (wfe *WebFrontEndImpl) parseJWSRequest(request *http.Request) (*bJSONWebSig bodyBytes, err := io.ReadAll(http.MaxBytesReader(nil, request.Body, maxRequestSize)) if err != nil { if err.Error() == "http: request body too large" { - return nil, probs.Unauthorized("request body too large") + return nil, berrors.UnauthorizedError("request body too large") } wfe.stats.httpErrorCount.With(prometheus.Labels{"type": "UnableToReadReqBody"}).Inc() - return nil, probs.ServerInternal("unable to read request body") + return nil, errors.New("unable to read request body") } - jws, prob := wfe.parseJWS(bodyBytes) - if prob != nil { - return nil, prob + jws, err := wfe.parseJWS(bodyBytes) + if err != nil { + return nil, err } return jws, nil } // extractJWK extracts a JWK from the protected headers of a bJSONWebSignature -// or returns a problem. It expects that the JWS is using the embedded JWK style +// or returns a error. It expects that the JWS is using the embedded JWK style // of authentication and does not contain an embedded Key ID. Callers should // have acquired the headers from a bJSONWebSignature returned by parseJWS to // ensure it has the correct number of signatures present. -func (wfe *WebFrontEndImpl) extractJWK(header jose.Header) (*jose.JSONWebKey, *probs.ProblemDetails) { +func (wfe *WebFrontEndImpl) extractJWK(header jose.Header) (*jose.JSONWebKey, error) { // extractJWK expects the request to be using an embedded JWK auth type and // to not contain the mutually exclusive KeyID. - if prob := wfe.enforceJWSAuthType(header, embeddedJWK); prob != nil { - return nil, prob + if err := wfe.enforceJWSAuthType(header, embeddedJWK); err != nil { + return nil, err } // We can be sure that JSONWebKey is != nil because we have already called // enforceJWSAuthType() key := header.JSONWebKey - // If the key isn't considered valid by go-jose return a problem immediately + // If the key isn't considered valid by go-jose return a error immediately if !key.Valid() { wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWKInvalid"}).Inc() - return nil, probs.Malformed("Invalid JWK in JWS header") + return nil, berrors.MalformedError("Invalid JWK in JWS header") } return key, nil @@ -449,8 +451,8 @@ func (wfe *WebFrontEndImpl) extractJWK(header jose.Header) (*jose.JSONWebKey, *p // acctIDFromURL extracts the numeric int64 account ID from a ACMEv1 or ACMEv2 // account URL. If the acctURL has an invalid URL or the account ID in the -// acctURL is non-numeric a MalformedProblem is returned. -func (wfe *WebFrontEndImpl) acctIDFromURL(acctURL string, request *http.Request) (int64, *probs.ProblemDetails) { +// acctURL is non-numeric a MalformedError is returned. +func (wfe *WebFrontEndImpl) acctIDFromURL(acctURL string, request *http.Request) (int64, error) { // For normal ACME v2 accounts we expect the account URL has a prefix composed // of the Host header and the acctPath. expectedURLPrefix := web.RelativeEndpoint(request, acctPath) @@ -465,65 +467,62 @@ func (wfe *WebFrontEndImpl) acctIDFromURL(acctURL string, request *http.Request) } else if strings.HasPrefix(acctURL, wfe.LegacyKeyIDPrefix) { accountIDStr = strings.TrimPrefix(acctURL, wfe.LegacyKeyIDPrefix) } else { - return 0, probs.Malformed( - fmt.Sprintf("KeyID header contained an invalid account URL: %q", acctURL)) + return 0, berrors.MalformedError("KeyID header contained an invalid account URL: %q", acctURL) } // Convert the raw account ID string to an int64 for use with the SA's // GetRegistration RPC accountID, err := strconv.ParseInt(accountIDStr, 10, 64) if err != nil { - return 0, probs.Malformed("Malformed account ID in KeyID header URL: %q", acctURL) + return 0, berrors.MalformedError("Malformed account ID in KeyID header URL: %q", acctURL) } return accountID, nil } // lookupJWK finds a JWK associated with the Key ID present in the provided // headers, returning the JWK and a pointer to the associated account, or a -// problem. It expects that the JWS header is using the embedded Key ID style of +// error. It expects that the JWS header is using the embedded Key ID style of // authentication and does not contain an embedded JWK. Callers should have // acquired headers from a bJSONWebSignature. func (wfe *WebFrontEndImpl) lookupJWK( header jose.Header, ctx context.Context, request *http.Request, - logEvent *web.RequestEvent) (*jose.JSONWebKey, *core.Registration, *probs.ProblemDetails) { + logEvent *web.RequestEvent) (*jose.JSONWebKey, *core.Registration, error) { // We expect the request to be using an embedded Key ID auth type and to not // contain the mutually exclusive embedded JWK. - if prob := wfe.enforceJWSAuthType(header, embeddedKeyID); prob != nil { - return nil, nil, prob + if err := wfe.enforceJWSAuthType(header, embeddedKeyID); err != nil { + return nil, nil, err } accountURL := header.KeyID - accountID, prob := wfe.acctIDFromURL(accountURL, request) - if prob != nil { + accountID, err := wfe.acctIDFromURL(accountURL, request) + if err != nil { wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSInvalidKeyID"}).Inc() - return nil, nil, prob + return nil, nil, err } // Try to find the account for this account ID account, err := wfe.accountGetter.GetRegistration(ctx, &sapb.RegistrationID{Id: accountID}) if err != nil { - // If the account isn't found, return a suitable problem + // If the account isn't found, return a suitable error if errors.Is(err, berrors.NotFound) { wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSKeyIDNotFound"}).Inc() - return nil, nil, probs.AccountDoesNotExist(fmt.Sprintf( - "Account %q not found", accountURL)) + return nil, nil, berrors.AccountDoesNotExistError("Account %q not found", accountURL) } // If there was an error and it isn't a "Not Found" error, return - // a ServerInternal problem since this is unexpected. + // a ServerInternal error since this is unexpected. wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSKeyIDLookupFailed"}).Inc() // Add an error to the log event with the internal error message logEvent.AddError("calling SA.GetRegistration: %s", err) - return nil, nil, web.ProblemDetailsForError(err, fmt.Sprintf("Error retrieving account %q", accountURL)) + return nil, nil, berrors.InternalServerError("Error retrieving account %q: %s", accountURL, err) } // Verify the account is not deactivated if core.AcmeStatus(account.Status) != core.StatusValid { wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSKeyIDAccountInvalid"}).Inc() - return nil, nil, probs.Unauthorized( - fmt.Sprintf("Account is not valid, has status %q", account.Status)) + return nil, nil, berrors.UnauthorizedError("Account is not valid, has status %q", account.Status) } // Update the logEvent with the account information and return the JWK @@ -531,8 +530,7 @@ func (wfe *WebFrontEndImpl) lookupJWK( acct, err := grpc.PbToRegistration(account) if err != nil { - return nil, nil, probs.ServerInternal(fmt.Sprintf( - "Error unmarshalling account %q", accountURL)) + return nil, nil, fmt.Errorf("error unmarshalling account %q: %w", accountURL, err) } return acct.Key, &acct, nil } @@ -547,11 +545,11 @@ func (wfe *WebFrontEndImpl) validJWSForKey( ctx context.Context, jws *bJSONWebSignature, jwk *jose.JSONWebKey, - request *http.Request) ([]byte, *probs.ProblemDetails) { + request *http.Request) ([]byte, error) { err := checkAlgorithm(jwk, jws.Signatures[0].Header) if err != nil { wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSAlgorithmCheckFailed"}).Inc() - return nil, probs.BadSignatureAlgorithm(err.Error()) + return nil, err } // Verify the JWS signature with the public key. @@ -563,17 +561,17 @@ func (wfe *WebFrontEndImpl) validJWSForKey( payload, err := jws.Verify(jwk) if err != nil { wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSVerifyFailed"}).Inc() - return nil, probs.Malformed("JWS verification error") + return nil, berrors.MalformedError("JWS verification error") } // Check that the JWS contains a correct Nonce header - if prob := wfe.validNonce(ctx, jws.Signatures[0].Header); prob != nil { - return nil, prob + if err := wfe.validNonce(ctx, jws.Signatures[0].Header); err != nil { + return nil, err } // Check that the HTTP request URL matches the URL in the signed JWS - if prob := wfe.validPOSTURL(request, jws.Signatures[0].Header); prob != nil { - return nil, prob + if err := wfe.validPOSTURL(request, jws.Signatures[0].Header); err != nil { + return nil, err } // In the WFE1 package the check for the request URL required unmarshalling @@ -585,7 +583,7 @@ func (wfe *WebFrontEndImpl) validJWSForKey( err = json.Unmarshal(payload, &parsedBody) if string(payload) != "" && err != nil { wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSBodyUnmarshalFailed"}).Inc() - return nil, probs.Malformed("Request payload did not parse as JSON") + return nil, berrors.MalformedError("Request payload did not parse as JSON") } return payload, nil @@ -597,22 +595,22 @@ func (wfe *WebFrontEndImpl) validJWSForKey( // specified key ID, specifies the correct URL, and has a valid nonce) then // `validJWSForAccount` returns the validated JWS body, the parsed // JSONWebSignature, and a pointer to the JWK's associated account. If any of -// these conditions are not met or an error occurs only a problem is returned. +// these conditions are not met or an error occurs only a error is returned. func (wfe *WebFrontEndImpl) validJWSForAccount( jws *bJSONWebSignature, request *http.Request, ctx context.Context, - logEvent *web.RequestEvent) ([]byte, *bJSONWebSignature, *core.Registration, *probs.ProblemDetails) { + logEvent *web.RequestEvent) ([]byte, *bJSONWebSignature, *core.Registration, error) { // Lookup the account and JWK for the key ID that authenticated the JWS - pubKey, account, prob := wfe.lookupJWK(jws.Signatures[0].Header, ctx, request, logEvent) - if prob != nil { - return nil, nil, nil, prob + pubKey, account, err := wfe.lookupJWK(jws.Signatures[0].Header, ctx, request, logEvent) + if err != nil { + return nil, nil, nil, err } // Verify the JWS with the JWK from the SA - payload, prob := wfe.validJWSForKey(ctx, jws, pubKey, request) - if prob != nil { - return nil, nil, nil, prob + payload, err := wfe.validJWSForKey(ctx, jws, pubKey, request) + if err != nil { + return nil, nil, nil, err } return payload, jws, account, nil @@ -620,17 +618,17 @@ func (wfe *WebFrontEndImpl) validJWSForAccount( // validPOSTForAccount checks that a given POST request has a valid JWS // using `validJWSForAccount`. If valid, the authenticated JWS body and the -// registration that authenticated the body are returned. Otherwise a problem is +// registration that authenticated the body are returned. Otherwise a error is // returned. The returned JWS body may be empty if the request is a POST-as-GET // request. func (wfe *WebFrontEndImpl) validPOSTForAccount( request *http.Request, ctx context.Context, - logEvent *web.RequestEvent) ([]byte, *bJSONWebSignature, *core.Registration, *probs.ProblemDetails) { + logEvent *web.RequestEvent) ([]byte, *bJSONWebSignature, *core.Registration, error) { // Parse the JWS from the POST request - jws, prob := wfe.parseJWSRequest(request) - if prob != nil { - return nil, nil, nil, prob + jws, err := wfe.parseJWSRequest(request) + if err != nil { + return nil, nil, nil, err } return wfe.validJWSForAccount(jws, request, ctx, logEvent) } @@ -639,27 +637,27 @@ func (wfe *WebFrontEndImpl) validPOSTForAccount( // `validPOSTForAccount`. It additionally validates that the JWS request payload // is empty, indicating that it is a POST-as-GET request per ACME draft 15+ // section 6.3 "GET and POST-as-GET requests". If a non empty payload is -// provided in the JWS the invalidPOSTAsGETErr problem is returned. This +// provided in the JWS the invalidPOSTAsGETErr error is returned. This // function is useful only for endpoints that do not need to handle both POSTs // with a body and POST-as-GET requests (e.g. Order, Certificate). func (wfe *WebFrontEndImpl) validPOSTAsGETForAccount( request *http.Request, ctx context.Context, - logEvent *web.RequestEvent) (*core.Registration, *probs.ProblemDetails) { + logEvent *web.RequestEvent) (*core.Registration, error) { // Call validPOSTForAccount to verify the JWS and extract the body. - body, _, reg, prob := wfe.validPOSTForAccount(request, ctx, logEvent) - if prob != nil { - return nil, prob + body, _, reg, err := wfe.validPOSTForAccount(request, ctx, logEvent) + if err != nil { + return nil, err } // Verify the POST-as-GET payload is empty if string(body) != "" { - return nil, probs.Malformed("POST-as-GET requests must have an empty payload") + return nil, berrors.MalformedError("POST-as-GET requests must have an empty payload") } // To make log analysis easier we choose to elevate the pseudo ACME HTTP // method "POST-as-GET" to the logEvent's Method, replacing the // http.MethodPost value. logEvent.Method = "POST-as-GET" - return reg, prob + return reg, err } // validSelfAuthenticatedJWS checks that a given JWS verifies with the JWK @@ -672,7 +670,7 @@ func (wfe *WebFrontEndImpl) validPOSTAsGETForAccount( // embedded in it, has the correct URL, and includes a valid nonce) then // `validSelfAuthenticatedJWS` returns the validated JWS body and the JWK that // was embedded in the JWS. Otherwise if the valid JWS conditions are not met or -// an error occurs only a problem is returned. +// an error occurs only a error is returned. // Note that this function does *not* enforce that the JWK abides by our goodkey // policies. This is because this method is used by the RevokeCertificate path, // which must allow JWKs which are signed by blocklisted (i.e. already revoked @@ -681,17 +679,17 @@ func (wfe *WebFrontEndImpl) validPOSTAsGETForAccount( func (wfe *WebFrontEndImpl) validSelfAuthenticatedJWS( ctx context.Context, jws *bJSONWebSignature, - request *http.Request) ([]byte, *jose.JSONWebKey, *probs.ProblemDetails) { + request *http.Request) ([]byte, *jose.JSONWebKey, error) { // Extract the embedded JWK from the parsed protected JWS' headers - pubKey, prob := wfe.extractJWK(jws.Signatures[0].Header) - if prob != nil { - return nil, nil, prob + pubKey, err := wfe.extractJWK(jws.Signatures[0].Header) + if err != nil { + return nil, nil, err } // Verify the JWS with the embedded JWK - payload, prob := wfe.validJWSForKey(ctx, jws, pubKey, request) - if prob != nil { - return nil, nil, prob + payload, err := wfe.validJWSForKey(ctx, jws, pubKey, request) + if err != nil { + return nil, nil, err } return payload, pubKey, nil @@ -702,27 +700,27 @@ func (wfe *WebFrontEndImpl) validSelfAuthenticatedJWS( // goodkey policies (key algorithm, length, blocklist, etc). func (wfe *WebFrontEndImpl) validSelfAuthenticatedPOST( ctx context.Context, - request *http.Request) ([]byte, *jose.JSONWebKey, *probs.ProblemDetails) { + request *http.Request) ([]byte, *jose.JSONWebKey, error) { // Parse the JWS from the POST request - jws, prob := wfe.parseJWSRequest(request) - if prob != nil { - return nil, nil, prob + jws, err := wfe.parseJWSRequest(request) + if err != nil { + return nil, nil, err } // Extract and validate the embedded JWK from the parsed JWS - payload, pubKey, prob := wfe.validSelfAuthenticatedJWS(ctx, jws, request) - if prob != nil { - return nil, nil, prob + payload, pubKey, err := wfe.validSelfAuthenticatedJWS(ctx, jws, request) + if err != nil { + return nil, nil, err } - // If the key doesn't meet the GoodKey policy return a problem - err := wfe.keyPolicy.GoodKey(ctx, pubKey.Key) + // If the key doesn't meet the GoodKey policy return a error + err = wfe.keyPolicy.GoodKey(ctx, pubKey.Key) if err != nil { if errors.Is(err, goodkey.ErrBadKey) { wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWKRejectedByGoodKey"}).Inc() - return nil, nil, probs.BadPublicKey(err.Error()) + return nil, nil, berrors.BadPublicKeyError("invalid request signing key: %s", err.Error()) } - return nil, nil, probs.ServerInternal("error checking key quality") + return nil, nil, berrors.InternalServerError("internal error while checking JWK: %s", err) } return payload, pubKey, nil @@ -757,7 +755,7 @@ type rolloverOperation struct { // field will be set to the JWK from the inner JWS. // // If the request is valid a *rolloverOperation object is returned, -// otherwise a problem is returned. The caller is left to verify +// otherwise a error is returned. The caller is left to verify // whether the new key is appropriate (e.g. isn't being used by another existing // account) and that the account field of the rollover object matches the // account that verified the outer JWS. @@ -765,25 +763,25 @@ func (wfe *WebFrontEndImpl) validKeyRollover( ctx context.Context, outerJWS *bJSONWebSignature, innerJWS *bJSONWebSignature, - oldKey *jose.JSONWebKey) (*rolloverOperation, *probs.ProblemDetails) { + oldKey *jose.JSONWebKey) (*rolloverOperation, error) { // Extract the embedded JWK from the inner JWS' protected headers - innerJWK, prob := wfe.extractJWK(innerJWS.Signatures[0].Header) - if prob != nil { - return nil, prob + innerJWK, err := wfe.extractJWK(innerJWS.Signatures[0].Header) + if err != nil { + return nil, err } - // If the key doesn't meet the GoodKey policy return a problem immediately - err := wfe.keyPolicy.GoodKey(ctx, innerJWK.Key) + // If the key doesn't meet the GoodKey policy return a error immediately + err = wfe.keyPolicy.GoodKey(ctx, innerJWK.Key) if err != nil { wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "KeyRolloverJWKRejectedByGoodKey"}).Inc() - return nil, probs.BadPublicKey(err.Error()) + return nil, berrors.BadPublicKeyError("invalid request signing key: %s", err.Error()) } // Check that the public key and JWS algorithms match expected err = checkAlgorithm(innerJWK, innerJWS.Signatures[0].Header) if err != nil { - return nil, probs.Malformed(err.Error()) + return nil, err } // Verify the inner JWS signature with the public key from the embedded JWK. @@ -793,38 +791,39 @@ func (wfe *WebFrontEndImpl) validKeyRollover( innerPayload, err := innerJWS.Verify(innerJWK) if err != nil { wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "KeyRolloverJWSVerifyFailed"}).Inc() - return nil, probs.Malformed("Inner JWS does not verify with embedded JWK") + return nil, berrors.MalformedError("Inner JWS does not verify with embedded JWK") } // NOTE(@cpu): we do not stomp the web.RequestEvent's payload here since that is set // from the outerJWS in validPOSTForAccount and contains the inner JWS and inner // payload already. // Verify that the outer and inner JWS protected URL headers match - if prob := wfe.matchJWSURLs(outerJWS.Signatures[0].Header, innerJWS.Signatures[0].Header); prob != nil { - return nil, prob + if err := wfe.matchJWSURLs(outerJWS.Signatures[0].Header, innerJWS.Signatures[0].Header); err != nil { + return nil, err } var req rolloverRequest if json.Unmarshal(innerPayload, &req) != nil { wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "KeyRolloverUnmarshalFailed"}).Inc() - return nil, probs.Malformed( - "Inner JWS payload did not parse as JSON key rollover object") + return nil, berrors.MalformedError("Inner JWS payload did not parse as JSON key rollover object") } // If there's no oldkey specified fail before trying to use // core.PublicKeyEqual on a nil argument. if req.OldKey.Key == nil { wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "KeyRolloverWrongOldKey"}).Inc() - return nil, probs.Malformed("Inner JWS does not contain old key field matching current account key") + return nil, berrors.MalformedError("Inner JWS does not contain old key field matching current account key") } // We must validate that the inner JWS' rollover request specifies the correct // oldKey. - if keysEqual, err := core.PublicKeysEqual(req.OldKey.Key, oldKey.Key); err != nil { - return nil, probs.Malformed("Unable to compare new and old keys: %s", err.Error()) - } else if !keysEqual { + keysEqual, err := core.PublicKeysEqual(req.OldKey.Key, oldKey.Key) + if err != nil { + return nil, berrors.MalformedError("Unable to compare new and old keys: %s", err.Error()) + } + if !keysEqual { wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "KeyRolloverWrongOldKey"}).Inc() - return nil, probs.Malformed("Inner JWS does not contain old key field matching current account key") + return nil, berrors.MalformedError("Inner JWS does not contain old key field matching current account key") } // Return a rolloverOperation populated with the validated old JWK, the diff --git a/third-party/github.com/letsencrypt/boulder/wfe2/verify_test.go b/third-party/github.com/letsencrypt/boulder/wfe2/verify_test.go index bc74f8c35..ca96194e9 100644 --- a/third-party/github.com/letsencrypt/boulder/wfe2/verify_test.go +++ b/third-party/github.com/letsencrypt/boulder/wfe2/verify_test.go @@ -7,8 +7,10 @@ import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rsa" + "errors" "fmt" "net/http" + "slices" "strings" "testing" @@ -16,11 +18,11 @@ import ( "github.com/letsencrypt/boulder/core" corepb "github.com/letsencrypt/boulder/core/proto" + berrors "github.com/letsencrypt/boulder/errors" "github.com/letsencrypt/boulder/goodkey" bgrpc "github.com/letsencrypt/boulder/grpc" "github.com/letsencrypt/boulder/grpc/noncebalancer" noncepb "github.com/letsencrypt/boulder/nonce/proto" - "github.com/letsencrypt/boulder/probs" sapb "github.com/letsencrypt/boulder/sa/proto" "github.com/letsencrypt/boulder/test" "github.com/letsencrypt/boulder/web" @@ -496,7 +498,7 @@ func TestValidPOSTRequest(t *testing.T) { Headers map[string][]string Body *string HTTPStatus int - ProblemDetail string + ErrorDetail string ErrorStatType string EnforceContentType bool }{ @@ -505,7 +507,7 @@ func TestValidPOSTRequest(t *testing.T) { Name: "POST without a Content-Length header", Headers: nil, HTTPStatus: http.StatusLengthRequired, - ProblemDetail: "missing Content-Length header", + ErrorDetail: "missing Content-Length header", ErrorStatType: "ContentLengthRequired", }, // POST requests with a Replay-Nonce header should produce a problem @@ -517,7 +519,7 @@ func TestValidPOSTRequest(t *testing.T) { "Content-Type": {expectedJWSContentType}, }, HTTPStatus: http.StatusBadRequest, - ProblemDetail: "HTTP requests should NOT contain Replay-Nonce header. Use JWS nonce field", + ErrorDetail: "HTTP requests should NOT contain Replay-Nonce header. Use JWS nonce field", ErrorStatType: "ReplayNonceOutsideJWS", }, // POST requests without a body should produce a problem @@ -528,7 +530,7 @@ func TestValidPOSTRequest(t *testing.T) { "Content-Type": {expectedJWSContentType}, }, HTTPStatus: http.StatusBadRequest, - ProblemDetail: "No body on POST", + ErrorDetail: "No body on POST", ErrorStatType: "NoPOSTBody", }, { @@ -537,7 +539,7 @@ func TestValidPOSTRequest(t *testing.T) { "Content-Length": dummyContentLength, }, HTTPStatus: http.StatusUnsupportedMediaType, - ProblemDetail: fmt.Sprintf( + ErrorDetail: fmt.Sprintf( "No Content-Type header on POST. Content-Type must be %q", expectedJWSContentType), ErrorStatType: "NoContentType", @@ -550,7 +552,7 @@ func TestValidPOSTRequest(t *testing.T) { "Content-Type": {"fresh.and.rare"}, }, HTTPStatus: http.StatusUnsupportedMediaType, - ProblemDetail: fmt.Sprintf( + ErrorDetail: fmt.Sprintf( "Invalid Content-Type header on POST. Content-Type must be %q", expectedJWSContentType), ErrorStatType: "WrongContentType", @@ -565,11 +567,10 @@ func TestValidPOSTRequest(t *testing.T) { Header: tc.Headers, } t.Run(tc.Name, func(t *testing.T) { - prob := wfe.validPOSTRequest(input) - test.Assert(t, prob != nil, "No error returned for invalid POST") - test.AssertEquals(t, prob.Type, probs.MalformedProblem) - test.AssertEquals(t, prob.HTTPStatus, tc.HTTPStatus) - test.AssertEquals(t, prob.Detail, tc.ProblemDetail) + err := wfe.validPOSTRequest(input) + test.AssertError(t, err, "No error returned for invalid POST") + test.AssertErrorIs(t, err, berrors.Malformed) + test.AssertContains(t, err.Error(), tc.ErrorDetail) test.AssertMetricWithLabelsEquals( t, wfe.stats.httpErrorCount, prometheus.Labels{"type": tc.ErrorStatType}, 1) }) @@ -605,71 +606,72 @@ func TestEnforceJWSAuthType(t *testing.T) { } testCases := []struct { - Name string - JWS *jose.JSONWebSignature - ExpectedAuthType jwsAuthType - ExpectedResult *probs.ProblemDetails - ErrorStatType string + Name string + JWS *jose.JSONWebSignature + AuthType jwsAuthType + WantErrType berrors.ErrorType + WantErrDetail string + WantStatType string }{ { - Name: "Key ID and embedded JWS", - JWS: conflictJWS, - ExpectedAuthType: invalidAuthType, - ExpectedResult: &probs.ProblemDetails{ - Type: probs.MalformedProblem, - Detail: "jwk and kid header fields are mutually exclusive", - HTTPStatus: http.StatusBadRequest, - }, - ErrorStatType: "JWSAuthTypeInvalid", + Name: "Key ID and embedded JWS", + JWS: conflictJWS, + AuthType: invalidAuthType, + WantErrType: berrors.Malformed, + WantErrDetail: "jwk and kid header fields are mutually exclusive", + WantStatType: "JWSAuthTypeInvalid", }, { - Name: "Key ID when expected is embedded JWK", - JWS: testKeyIDJWS, - ExpectedAuthType: embeddedJWK, - ExpectedResult: &probs.ProblemDetails{ - Type: probs.MalformedProblem, - Detail: "No embedded JWK in JWS header", - HTTPStatus: http.StatusBadRequest, - }, - ErrorStatType: "JWSAuthTypeWrong", + Name: "Key ID when expected is embedded JWK", + JWS: testKeyIDJWS, + AuthType: embeddedJWK, + WantErrType: berrors.Malformed, + WantErrDetail: "No embedded JWK in JWS header", + WantStatType: "JWSAuthTypeWrong", }, { - Name: "Embedded JWK when expected is Key ID", - JWS: testEmbeddedJWS, - ExpectedAuthType: embeddedKeyID, - ExpectedResult: &probs.ProblemDetails{ - Type: probs.MalformedProblem, - Detail: "No Key ID in JWS header", - HTTPStatus: http.StatusBadRequest, - }, - ErrorStatType: "JWSAuthTypeWrong", + Name: "Embedded JWK when expected is Key ID", + JWS: testEmbeddedJWS, + AuthType: embeddedKeyID, + WantErrType: berrors.Malformed, + WantErrDetail: "No Key ID in JWS header", + WantStatType: "JWSAuthTypeWrong", }, { - Name: "Key ID when expected is KeyID", - JWS: testKeyIDJWS, - ExpectedAuthType: embeddedKeyID, - ExpectedResult: nil, + Name: "Key ID when expected is KeyID", + JWS: testKeyIDJWS, + AuthType: embeddedKeyID, }, { - Name: "Embedded JWK when expected is embedded JWK", - JWS: testEmbeddedJWS, - ExpectedAuthType: embeddedJWK, - ExpectedResult: nil, + Name: "Embedded JWK when expected is embedded JWK", + JWS: testEmbeddedJWS, + AuthType: embeddedJWK, }, } for _, tc := range testCases { t.Run(tc.Name, func(t *testing.T) { wfe.stats.joseErrorCount.Reset() - prob := wfe.enforceJWSAuthType(tc.JWS.Signatures[0].Header, tc.ExpectedAuthType) - if tc.ExpectedResult == nil && prob != nil { - t.Fatalf("Expected nil result, got %#v", prob) + in := tc.JWS.Signatures[0].Header + + gotErr := wfe.enforceJWSAuthType(in, tc.AuthType) + if tc.WantErrDetail == "" { + if gotErr != nil { + t.Fatalf("enforceJWSAuthType(%#v, %#v) = %#v, want nil", in, tc.AuthType, gotErr) + } } else { - test.AssertMarshaledEquals(t, prob, tc.ExpectedResult) - } - if tc.ErrorStatType != "" { + berr, ok := gotErr.(*berrors.BoulderError) + if !ok { + t.Fatalf("enforceJWSAuthType(%#v, %#v) returned %T, want BoulderError", in, tc.AuthType, gotErr) + } + if berr.Type != tc.WantErrType { + t.Errorf("enforceJWSAuthType(%#v, %#v) = %#v, want %#v", in, tc.AuthType, berr.Type, tc.WantErrType) + } + if !strings.Contains(berr.Detail, tc.WantErrDetail) { + t.Errorf("enforceJWSAuthType(%#v, %#v) = %q, want %q", in, tc.AuthType, berr.Detail, tc.WantErrDetail) + } test.AssertMetricWithLabelsEquals( - t, wfe.stats.joseErrorCount, prometheus.Labels{"type": tc.ErrorStatType}, 1) + t, wfe.stats.joseErrorCount, prometheus.Labels{"type": tc.WantStatType}, 1) } }) } @@ -699,70 +701,69 @@ func TestValidNonce(t *testing.T) { goodJWS, _, _ := signer.embeddedJWK(nil, "", "") testCases := []struct { - Name string - JWS *jose.JSONWebSignature - ExpectedResult *probs.ProblemDetails - ErrorStatType string + Name string + JWS *jose.JSONWebSignature + WantErrType berrors.ErrorType + WantErrDetail string + WantStatType string }{ { - Name: "No nonce in JWS", - JWS: signer.missingNonce(), - ExpectedResult: &probs.ProblemDetails{ - Type: probs.BadNonceProblem, - Detail: "JWS has no anti-replay nonce", - HTTPStatus: http.StatusBadRequest, - }, - ErrorStatType: "JWSMissingNonce", + Name: "No nonce in JWS", + JWS: signer.missingNonce(), + WantErrType: berrors.BadNonce, + WantErrDetail: "JWS has no anti-replay nonce", + WantStatType: "JWSMissingNonce", }, { - Name: "Malformed nonce in JWS", - JWS: signer.malformedNonce(), - ExpectedResult: &probs.ProblemDetails{ - Type: probs.BadNonceProblem, - Detail: "JWS has an invalid anti-replay nonce: \"im-a-nonce\"", - HTTPStatus: http.StatusBadRequest, - }, - ErrorStatType: "JWSMalformedNonce", + Name: "Malformed nonce in JWS", + JWS: signer.malformedNonce(), + WantErrType: berrors.BadNonce, + WantErrDetail: "JWS has an invalid anti-replay nonce: \"im-a-nonce\"", + WantStatType: "JWSMalformedNonce", }, { - Name: "Canned nonce shorter than prefixLength in JWS", - JWS: signer.shortNonce(), - ExpectedResult: &probs.ProblemDetails{ - Type: probs.BadNonceProblem, - Detail: "JWS has an invalid anti-replay nonce: \"woww\"", - HTTPStatus: http.StatusBadRequest, - }, - ErrorStatType: "JWSMalformedNonce", + Name: "Canned nonce shorter than prefixLength in JWS", + JWS: signer.shortNonce(), + WantErrType: berrors.BadNonce, + WantErrDetail: "JWS has an invalid anti-replay nonce: \"woww\"", + WantStatType: "JWSMalformedNonce", }, { - Name: "Invalid nonce in JWS (test/config-next)", - JWS: signer.invalidNonce(), - ExpectedResult: &probs.ProblemDetails{ - Type: probs.BadNonceProblem, - Detail: "JWS has an invalid anti-replay nonce: \"mlolmlol3ov77I5Ui-cdaY_k8IcjK58FvbG0y_BCRrx5rGQ8rjA\"", - HTTPStatus: http.StatusBadRequest, - }, - ErrorStatType: "JWSInvalidNonce", + Name: "Invalid nonce in JWS (test/config-next)", + JWS: signer.invalidNonce(), + WantErrType: berrors.BadNonce, + WantErrDetail: "JWS has an invalid anti-replay nonce: \"mlolmlol3ov77I5Ui-cdaY_k8IcjK58FvbG0y_BCRrx5rGQ8rjA\"", + WantStatType: "JWSInvalidNonce", }, { - Name: "Valid nonce in JWS", - JWS: goodJWS, - ExpectedResult: nil, + Name: "Valid nonce in JWS", + JWS: goodJWS, }, } for _, tc := range testCases { t.Run(tc.Name, func(t *testing.T) { + in := tc.JWS.Signatures[0].Header wfe.stats.joseErrorCount.Reset() - prob := wfe.validNonce(context.Background(), tc.JWS.Signatures[0].Header) - if tc.ExpectedResult == nil && prob != nil { - t.Fatalf("Expected nil result, got %#v", prob) + + gotErr := wfe.validNonce(context.Background(), in) + if tc.WantErrDetail == "" { + if gotErr != nil { + t.Fatalf("validNonce(%#v) = %#v, want nil", in, gotErr) + } } else { - test.AssertMarshaledEquals(t, prob, tc.ExpectedResult) - } - if tc.ErrorStatType != "" { + berr, ok := gotErr.(*berrors.BoulderError) + if !ok { + t.Fatalf("validNonce(%#v) returned %T, want BoulderError", in, gotErr) + } + if berr.Type != tc.WantErrType { + t.Errorf("validNonce(%#v) = %#v, want %#v", in, berr.Type, tc.WantErrType) + } + if !strings.Contains(berr.Detail, tc.WantErrDetail) { + t.Errorf("validNonce(%#v) = %q, want %q", in, berr.Detail, tc.WantErrDetail) + } test.AssertMetricWithLabelsEquals( - t, wfe.stats.joseErrorCount, prometheus.Labels{"type": tc.ErrorStatType}, 1) + t, wfe.stats.joseErrorCount, prometheus.Labels{"type": tc.WantStatType}, 1) } }) } @@ -783,11 +784,10 @@ func TestValidNonce_NoMatchingBackendFound(t *testing.T) { // A valid JWS with a nonce whose prefix matches no known nonce provider should // result in a BadNonceProblem. - prob := wfe.validNonce(context.Background(), goodJWS.Signatures[0].Header) - test.Assert(t, prob != nil, "Expected error for valid nonce with no backend") - test.AssertEquals(t, prob.Type, probs.BadNonceProblem) - test.AssertEquals(t, prob.HTTPStatus, http.StatusBadRequest) - test.AssertContains(t, prob.Detail, "JWS has an invalid anti-replay nonce") + err := wfe.validNonce(context.Background(), goodJWS.Signatures[0].Header) + test.AssertError(t, err, "Expected error for valid nonce with no backend") + test.AssertErrorIs(t, err, berrors.BadNonce) + test.AssertContains(t, err.Error(), "JWS has an invalid anti-replay nonce") test.AssertMetricWithLabelsEquals(t, wfe.stats.nonceNoMatchingBackendCount, prometheus.Labels{}, 1) } @@ -844,66 +844,68 @@ func TestValidPOSTURL(t *testing.T) { correctURLHeaderRequest := makePostRequestWithPath("test-path", correctURLHeaderJWSBody) testCases := []struct { - Name string - JWS *jose.JSONWebSignature - Request *http.Request - ExpectedResult *probs.ProblemDetails - ErrorStatType string + Name string + JWS *jose.JSONWebSignature + Request *http.Request + WantErrType berrors.ErrorType + WantErrDetail string + WantStatType string }{ { - Name: "No extra headers in JWS", - JWS: noHeadersJWS, - Request: noHeadersRequest, - ExpectedResult: &probs.ProblemDetails{ - Type: probs.MalformedProblem, - Detail: "JWS header parameter 'url' required", - HTTPStatus: http.StatusBadRequest, - }, - ErrorStatType: "JWSNoExtraHeaders", + Name: "No extra headers in JWS", + JWS: noHeadersJWS, + Request: noHeadersRequest, + WantErrType: berrors.Malformed, + WantErrDetail: "JWS header parameter 'url' required", + WantStatType: "JWSNoExtraHeaders", }, { - Name: "No URL header in JWS", - JWS: noURLHeaderJWS, - Request: noURLHeaderRequest, - ExpectedResult: &probs.ProblemDetails{ - Type: probs.MalformedProblem, - Detail: "JWS header parameter 'url' required", - HTTPStatus: http.StatusBadRequest, - }, - ErrorStatType: "JWSMissingURL", + Name: "No URL header in JWS", + JWS: noURLHeaderJWS, + Request: noURLHeaderRequest, + WantErrType: berrors.Malformed, + WantErrDetail: "JWS header parameter 'url' required", + WantStatType: "JWSMissingURL", }, { - Name: "Wrong URL header in JWS", - JWS: wrongURLHeaderJWS, - Request: wrongURLHeaderRequest, - ExpectedResult: &probs.ProblemDetails{ - Type: probs.MalformedProblem, - Detail: "JWS header parameter 'url' incorrect. Expected \"http://localhost/test-path\" got \"foobar\"", - HTTPStatus: http.StatusBadRequest, - }, - ErrorStatType: "JWSMismatchedURL", + Name: "Wrong URL header in JWS", + JWS: wrongURLHeaderJWS, + Request: wrongURLHeaderRequest, + WantErrType: berrors.Malformed, + WantErrDetail: "JWS header parameter 'url' incorrect. Expected \"http://localhost/test-path\" got \"foobar\"", + WantStatType: "JWSMismatchedURL", }, { - Name: "Correct URL header in JWS", - JWS: correctURLHeaderJWS, - Request: correctURLHeaderRequest, - ExpectedResult: nil, + Name: "Correct URL header in JWS", + JWS: correctURLHeaderJWS, + Request: correctURLHeaderRequest, }, } for _, tc := range testCases { t.Run(tc.Name, func(t *testing.T) { + in := tc.JWS.Signatures[0].Header tc.Request.Header.Add("Content-Type", expectedJWSContentType) wfe.stats.joseErrorCount.Reset() - prob := wfe.validPOSTURL(tc.Request, tc.JWS.Signatures[0].Header) - if tc.ExpectedResult == nil && prob != nil { - t.Fatalf("Expected nil result, got %#v", prob) + + got := wfe.validPOSTURL(tc.Request, in) + if tc.WantErrDetail == "" { + if got != nil { + t.Fatalf("validPOSTURL(%#v) = %#v, want nil", in, got) + } } else { - test.AssertMarshaledEquals(t, prob, tc.ExpectedResult) - } - if tc.ErrorStatType != "" { + berr, ok := got.(*berrors.BoulderError) + if !ok { + t.Fatalf("validPOSTURL(%#v) returned %T, want BoulderError", in, got) + } + if berr.Type != tc.WantErrType { + t.Errorf("validPOSTURL(%#v) = %#v, want %#v", in, berr.Type, tc.WantErrType) + } + if !strings.Contains(berr.Detail, tc.WantErrDetail) { + t.Errorf("validPOSTURL(%#v) = %q, want %q", in, berr.Detail, tc.WantErrDetail) + } test.AssertMetricWithLabelsEquals( - t, wfe.stats.joseErrorCount, prometheus.Labels{"type": tc.ErrorStatType}, 1) + t, wfe.stats.joseErrorCount, prometheus.Labels{"type": tc.WantStatType}, 1) } }) } @@ -970,13 +972,21 @@ func TestParseJWSRequest(t *testing.T) { "payload": "Zm9v", "signatures": ["PKWWclRsiHF4bm-nmpxDez6Y_3Mdtu263YeYklbGYt1EiMOLiKY_dr_EqhUUKAKEWysFLO-hQLXVU7kVkHeYWQFFOA18oFgcZgkSF2Pr3DNZrVj9e2gl0eZ2i2jk6X5GYPt1lIfok_DrL92wrxEKGcrmxqXXGm0JgP6Al2VGapKZK2HaYbCHoGvtzNmzUX9rC21sKewq5CquJRvTmvQp5bmU7Q9KeafGibFr0jl6IA3W5LBGgf6xftuUtEVEbKmKaKtaG7tXsQH1mIVOPUZZoLWz9sWJSFLmV0QSXm3ZHV0DrOhLfcADbOCoQBMeGdseBQZuUO541A3BEKGv2Aikjw"] } +` + wrongSignatureTypeJWSBody := ` +{ + "protected": "eyJhbGciOiJIUzI1NiJ9", + "payload" : "IiI", + "signature" : "5WiUupHzCWfpJza6EMteSxMDY8_6xIV7HnKaUqmykIQ" +} ` testCases := []struct { - Name string - Request *http.Request - ExpectedProblem *probs.ProblemDetails - ErrorStatType string + Name string + Request *http.Request + WantErrType berrors.ErrorType + WantErrDetail string + WantStatType string }{ { Name: "Invalid POST request", @@ -985,91 +995,87 @@ func TestParseJWSRequest(t *testing.T) { Method: "POST", URL: mustParseURL("/"), }, - ExpectedProblem: &probs.ProblemDetails{ - Type: probs.MalformedProblem, - Detail: "missing Content-Length header", - HTTPStatus: http.StatusLengthRequired, - }, + WantErrType: berrors.Malformed, + WantErrDetail: "missing Content-Length header", }, { - Name: "Invalid JWS in POST body", - Request: makePostRequestWithPath("test-path", `{`), - ExpectedProblem: &probs.ProblemDetails{ - Type: probs.MalformedProblem, - Detail: "Parse error reading JWS", - HTTPStatus: http.StatusBadRequest, - }, - ErrorStatType: "JWSUnmarshalFailed", + Name: "Invalid JWS in POST body", + Request: makePostRequestWithPath("test-path", `{`), + WantErrType: berrors.Malformed, + WantErrDetail: "Parse error reading JWS", + WantStatType: "JWSUnmarshalFailed", }, { - Name: "Too few signatures in JWS", - Request: missingSigsJWSRequest, - ExpectedProblem: &probs.ProblemDetails{ - Type: probs.MalformedProblem, - Detail: "POST JWS not signed", - HTTPStatus: http.StatusBadRequest, - }, - ErrorStatType: "JWSEmptySignature", + Name: "Too few signatures in JWS", + Request: missingSigsJWSRequest, + WantErrType: berrors.Malformed, + WantErrDetail: "POST JWS not signed", + WantStatType: "JWSEmptySignature", }, { - Name: "Too many signatures in JWS", - Request: makePostRequestWithPath("test-path", tooManySigsJWSBody), - ExpectedProblem: &probs.ProblemDetails{ - Type: probs.MalformedProblem, - Detail: "JWS \"signatures\" field not allowed. Only the \"signature\" field should contain a signature", - HTTPStatus: http.StatusBadRequest, - }, - ErrorStatType: "JWSMultiSig", + Name: "Too many signatures in JWS", + Request: makePostRequestWithPath("test-path", tooManySigsJWSBody), + WantErrType: berrors.Malformed, + WantErrDetail: "JWS \"signatures\" field not allowed. Only the \"signature\" field should contain a signature", + WantStatType: "JWSMultiSig", }, { - Name: "Unprotected JWS headers", - Request: makePostRequestWithPath("test-path", unprotectedHeadersJWSBody), - ExpectedProblem: &probs.ProblemDetails{ - Type: probs.MalformedProblem, - Detail: "JWS \"header\" field not allowed. All headers must be in \"protected\" field", - HTTPStatus: http.StatusBadRequest, - }, - ErrorStatType: "JWSUnprotectedHeaders", + Name: "Unprotected JWS headers", + Request: makePostRequestWithPath("test-path", unprotectedHeadersJWSBody), + WantErrType: berrors.Malformed, + WantErrDetail: "JWS \"header\" field not allowed. All headers must be in \"protected\" field", + WantStatType: "JWSUnprotectedHeaders", }, { - Name: "Unsupported signatures field in JWS", - Request: makePostRequestWithPath("test-path", wrongSignaturesFieldJWSBody), - ExpectedProblem: &probs.ProblemDetails{ - Type: probs.MalformedProblem, - Detail: "JWS \"signatures\" field not allowed. Only the \"signature\" field should contain a signature", - HTTPStatus: http.StatusBadRequest, - }, - ErrorStatType: "JWSMultiSig", + Name: "Unsupported signatures field in JWS", + Request: makePostRequestWithPath("test-path", wrongSignaturesFieldJWSBody), + WantErrType: berrors.Malformed, + WantErrDetail: "JWS \"signatures\" field not allowed. Only the \"signature\" field should contain a signature", + WantStatType: "JWSMultiSig", }, { - Name: "Valid JWS in POST request", - Request: validJWSRequest, - ExpectedProblem: nil, + Name: "JWS with an invalid algorithm", + Request: makePostRequestWithPath("test-path", wrongSignatureTypeJWSBody), + WantErrType: berrors.BadSignatureAlgorithm, + WantErrDetail: "JWS signature header contains unsupported algorithm \"HS256\", expected one of [RS256 ES256 ES384 ES512]", + WantStatType: "JWSAlgorithmCheckFailed", }, { - Name: "POST body too large", - Request: makePostRequestWithPath("test-path", - fmt.Sprintf(`{"a":"%s"}`, strings.Repeat("a", 50000))), - ExpectedProblem: &probs.ProblemDetails{ - Type: probs.UnauthorizedProblem, - Detail: "request body too large", - HTTPStatus: http.StatusForbidden, - }, + Name: "Valid JWS in POST request", + Request: validJWSRequest, + }, + { + Name: "POST body too large", + Request: makePostRequestWithPath("test-path", fmt.Sprintf(`{"a":"%s"}`, strings.Repeat("a", 50000))), + WantErrType: berrors.Unauthorized, + WantErrDetail: "request body too large", }, } for _, tc := range testCases { t.Run(tc.Name, func(t *testing.T) { wfe.stats.joseErrorCount.Reset() - _, prob := wfe.parseJWSRequest(tc.Request) - if tc.ExpectedProblem == nil && prob != nil { - t.Fatalf("Expected nil problem, got %#v\n", prob) + + _, gotErr := wfe.parseJWSRequest(tc.Request) + if tc.WantErrDetail == "" { + if gotErr != nil { + t.Fatalf("parseJWSRequest(%#v) = %#v, want nil", tc.Request, gotErr) + } } else { - test.AssertMarshaledEquals(t, prob, tc.ExpectedProblem) - } - if tc.ErrorStatType != "" { - test.AssertMetricWithLabelsEquals( - t, wfe.stats.joseErrorCount, prometheus.Labels{"type": tc.ErrorStatType}, 1) + berr, ok := gotErr.(*berrors.BoulderError) + if !ok { + t.Fatalf("parseJWSRequest(%#v) returned %T, want BoulderError", tc.Request, gotErr) + } + if berr.Type != tc.WantErrType { + t.Errorf("parseJWSRequest(%#v) = %#v, want %#v", tc.Request, berr.Type, tc.WantErrType) + } + if !strings.Contains(berr.Detail, tc.WantErrDetail) { + t.Errorf("parseJWSRequest(%#v) = %q, want %q", tc.Request, berr.Detail, tc.WantErrDetail) + } + if tc.WantStatType != "" { + test.AssertMetricWithLabelsEquals( + t, wfe.stats.joseErrorCount, prometheus.Labels{"type": tc.WantStatType}, 1) + } } }) } @@ -1082,36 +1088,46 @@ func TestExtractJWK(t *testing.T) { goodJWS, goodJWK, _ := signer.embeddedJWK(nil, "", "") testCases := []struct { - Name string - JWS *jose.JSONWebSignature - ExpectedKey *jose.JSONWebKey - ExpectedProblem *probs.ProblemDetails + Name string + JWS *jose.JSONWebSignature + WantKey *jose.JSONWebKey + WantErrType berrors.ErrorType + WantErrDetail string }{ { - Name: "JWS with wrong auth type (Key ID vs embedded JWK)", - JWS: keyIDJWS, - ExpectedProblem: &probs.ProblemDetails{ - Type: probs.MalformedProblem, - Detail: "No embedded JWK in JWS header", - HTTPStatus: http.StatusBadRequest, - }, + Name: "JWS with wrong auth type (Key ID vs embedded JWK)", + JWS: keyIDJWS, + WantErrType: berrors.Malformed, + WantErrDetail: "No embedded JWK in JWS header", }, { - Name: "Valid JWS with embedded JWK", - JWS: goodJWS, - ExpectedKey: goodJWK, + Name: "Valid JWS with embedded JWK", + JWS: goodJWS, + WantKey: goodJWK, }, } for _, tc := range testCases { t.Run(tc.Name, func(t *testing.T) { - jwkHeader, prob := wfe.extractJWK(tc.JWS.Signatures[0].Header) - if tc.ExpectedProblem == nil && prob != nil { - t.Fatalf("Expected nil problem, got %#v\n", prob) - } else if tc.ExpectedProblem == nil { - test.AssertMarshaledEquals(t, jwkHeader, tc.ExpectedKey) + in := tc.JWS.Signatures[0].Header + + gotKey, gotErr := wfe.extractJWK(in) + if tc.WantErrDetail == "" { + if gotErr != nil { + t.Fatalf("extractJWK(%#v) = %#v, want nil", in, gotKey) + } + test.AssertMarshaledEquals(t, gotKey, tc.WantKey) } else { - test.AssertMarshaledEquals(t, prob, tc.ExpectedProblem) + berr, ok := gotErr.(*berrors.BoulderError) + if !ok { + t.Fatalf("extractJWK(%#v) returned %T, want BoulderError", in, gotErr) + } + if berr.Type != tc.WantErrType { + t.Errorf("extractJWK(%#v) = %#v, want %#v", in, berr.Type, tc.WantErrType) + } + if !strings.Contains(berr.Detail, tc.WantErrDetail) { + t.Errorf("extractJWK(%#v) = %q, want %q", in, berr.Detail, tc.WantErrDetail) + } } }) } @@ -1179,114 +1195,110 @@ func TestLookupJWK(t *testing.T) { // good key, log event requester is set testCases := []struct { - Name string - JWS *jose.JSONWebSignature - Request *http.Request - ExpectedProblem *probs.ProblemDetails - ExpectedKey *jose.JSONWebKey - ExpectedAccount *core.Registration - ErrorStatType string + Name string + JWS *jose.JSONWebSignature + Request *http.Request + WantJWK *jose.JSONWebKey + WantAccount *core.Registration + WantErrType berrors.ErrorType + WantErrDetail string + WantStatType string }{ { - Name: "JWS with wrong auth type (embedded JWK vs Key ID)", - JWS: embeddedJWS, - Request: makePostRequestWithPath("test-path", embeddedJWSBody), - ExpectedProblem: &probs.ProblemDetails{ - Type: probs.MalformedProblem, - Detail: "No Key ID in JWS header", - HTTPStatus: http.StatusBadRequest, - }, - ErrorStatType: "JWSAuthTypeWrong", + Name: "JWS with wrong auth type (embedded JWK vs Key ID)", + JWS: embeddedJWS, + Request: makePostRequestWithPath("test-path", embeddedJWSBody), + WantErrType: berrors.Malformed, + WantErrDetail: "No Key ID in JWS header", + WantStatType: "JWSAuthTypeWrong", }, { - Name: "JWS with invalid key ID URL", - JWS: invalidKeyIDJWS, - Request: makePostRequestWithPath("test-path", invalidKeyIDJWSBody), - ExpectedProblem: &probs.ProblemDetails{ - Type: probs.MalformedProblem, - Detail: "KeyID header contained an invalid account URL: \"https://acme-99.lettuceencrypt.org/acme/reg/1\"", - HTTPStatus: http.StatusBadRequest, - }, - ErrorStatType: "JWSInvalidKeyID", + Name: "JWS with invalid key ID URL", + JWS: invalidKeyIDJWS, + Request: makePostRequestWithPath("test-path", invalidKeyIDJWSBody), + WantErrType: berrors.Malformed, + WantErrDetail: "KeyID header contained an invalid account URL: \"https://acme-99.lettuceencrypt.org/acme/reg/1\"", + WantStatType: "JWSInvalidKeyID", }, { - Name: "JWS with non-numeric account ID in key ID URL", - JWS: nonNumericKeyIDJWS, - Request: makePostRequestWithPath("test-path", nonNumericKeyIDJWSBody), - ExpectedProblem: &probs.ProblemDetails{ - Type: probs.MalformedProblem, - Detail: "Malformed account ID in KeyID header URL: \"https://acme-v00.lettuceencrypt.org/acme/reg/abcd\"", - HTTPStatus: http.StatusBadRequest, - }, - ErrorStatType: "JWSInvalidKeyID", + Name: "JWS with non-numeric account ID in key ID URL", + JWS: nonNumericKeyIDJWS, + Request: makePostRequestWithPath("test-path", nonNumericKeyIDJWSBody), + WantErrType: berrors.Malformed, + WantErrDetail: "Malformed account ID in KeyID header URL: \"https://acme-v00.lettuceencrypt.org/acme/reg/abcd\"", + WantStatType: "JWSInvalidKeyID", }, { - Name: "JWS with account ID that causes GetRegistration error", - JWS: errorIDJWS, - Request: makePostRequestWithPath("test-path", errorIDJWSBody), - ExpectedProblem: &probs.ProblemDetails{ - Type: probs.ServerInternalProblem, - Detail: "Error retrieving account \"http://localhost/acme/acct/100\"", - HTTPStatus: http.StatusInternalServerError, - }, - ErrorStatType: "JWSKeyIDLookupFailed", + Name: "JWS with account ID that causes GetRegistration error", + JWS: errorIDJWS, + Request: makePostRequestWithPath("test-path", errorIDJWSBody), + WantErrType: berrors.InternalServer, + WantErrDetail: "Error retrieving account \"http://localhost/acme/acct/100\"", + WantStatType: "JWSKeyIDLookupFailed", }, { - Name: "JWS with account ID that doesn't exist", - JWS: missingIDJWS, - Request: makePostRequestWithPath("test-path", missingIDJWSBody), - ExpectedProblem: &probs.ProblemDetails{ - Type: probs.AccountDoesNotExistProblem, - Detail: "Account \"http://localhost/acme/acct/102\" not found", - HTTPStatus: http.StatusBadRequest, - }, - ErrorStatType: "JWSKeyIDNotFound", + Name: "JWS with account ID that doesn't exist", + JWS: missingIDJWS, + Request: makePostRequestWithPath("test-path", missingIDJWSBody), + WantErrType: berrors.AccountDoesNotExist, + WantErrDetail: "Account \"http://localhost/acme/acct/102\" not found", + WantStatType: "JWSKeyIDNotFound", }, { - Name: "JWS with account ID that is deactivated", - JWS: deactivatedIDJWS, - Request: makePostRequestWithPath("test-path", deactivatedIDJWSBody), - ExpectedProblem: &probs.ProblemDetails{ - Type: probs.UnauthorizedProblem, - Detail: "Account is not valid, has status \"deactivated\"", - HTTPStatus: http.StatusForbidden, - }, - ErrorStatType: "JWSKeyIDAccountInvalid", + Name: "JWS with account ID that is deactivated", + JWS: deactivatedIDJWS, + Request: makePostRequestWithPath("test-path", deactivatedIDJWSBody), + WantErrType: berrors.Unauthorized, + WantErrDetail: "Account is not valid, has status \"deactivated\"", + WantStatType: "JWSKeyIDAccountInvalid", }, { - Name: "Valid JWS with legacy account ID", - JWS: legacyKeyIDJWS, - Request: makePostRequestWithPath("test-path", legacyKeyIDJWSBody), - ExpectedKey: validKey, - ExpectedAccount: &validAccount, + Name: "Valid JWS with legacy account ID", + JWS: legacyKeyIDJWS, + Request: makePostRequestWithPath("test-path", legacyKeyIDJWSBody), + WantJWK: validKey, + WantAccount: &validAccount, }, { - Name: "Valid JWS with valid account ID", - JWS: validJWS, - Request: makePostRequestWithPath("test-path", validJWSBody), - ExpectedKey: validKey, - ExpectedAccount: &validAccount, + Name: "Valid JWS with valid account ID", + JWS: validJWS, + Request: makePostRequestWithPath("test-path", validJWSBody), + WantJWK: validKey, + WantAccount: &validAccount, }, } for _, tc := range testCases { t.Run(tc.Name, func(t *testing.T) { wfe.stats.joseErrorCount.Reset() + in := tc.JWS.Signatures[0].Header inputLogEvent := newRequestEvent() - jwkHeader, acct, prob := wfe.lookupJWK(tc.JWS.Signatures[0].Header, context.Background(), tc.Request, inputLogEvent) - if tc.ExpectedProblem == nil && prob != nil { - t.Fatalf("Expected nil problem, got %#v\n", prob) - } else if tc.ExpectedProblem == nil { - inThumb, _ := tc.ExpectedKey.Thumbprint(crypto.SHA256) - outThumb, _ := jwkHeader.Thumbprint(crypto.SHA256) - test.AssertDeepEquals(t, inThumb, outThumb) - test.AssertMarshaledEquals(t, acct, tc.ExpectedAccount) - test.AssertEquals(t, inputLogEvent.Requester, acct.ID) + + gotJWK, gotAcct, gotErr := wfe.lookupJWK(in, context.Background(), tc.Request, inputLogEvent) + if tc.WantErrDetail == "" { + if gotErr != nil { + t.Fatalf("lookupJWK(%#v) = %#v, want nil", in, gotErr) + } + gotThumb, _ := gotJWK.Thumbprint(crypto.SHA256) + wantThumb, _ := tc.WantJWK.Thumbprint(crypto.SHA256) + if !slices.Equal(gotThumb, wantThumb) { + t.Fatalf("lookupJWK(%#v) = %#v, want %#v", tc.Request, gotThumb, wantThumb) + } + test.AssertMarshaledEquals(t, gotAcct, tc.WantAccount) + test.AssertEquals(t, inputLogEvent.Requester, gotAcct.ID) } else { - test.AssertMarshaledEquals(t, prob, tc.ExpectedProblem) - } - if tc.ErrorStatType != "" { + var berr *berrors.BoulderError + ok := errors.As(gotErr, &berr) + if !ok { + t.Fatalf("lookupJWK(%#v) returned %T, want BoulderError", in, gotErr) + } + if berr.Type != tc.WantErrType { + t.Errorf("lookupJWK(%#v) = %#v, want %#v", in, berr.Type, tc.WantErrType) + } + if !strings.Contains(berr.Detail, tc.WantErrDetail) { + t.Errorf("lookupJWK(%#v) = %q, want %q", in, berr.Detail, tc.WantErrDetail) + } test.AssertMetricWithLabelsEquals( - t, wfe.stats.joseErrorCount, prometheus.Labels{"type": tc.ErrorStatType}, 1) + t, wfe.stats.joseErrorCount, prometheus.Labels{"type": tc.WantStatType}, 1) } }) } @@ -1325,67 +1337,53 @@ func TestValidJWSForKey(t *testing.T) { badJSONJWS, _, _ := signer.embeddedJWK(nil, testURL, `{`) testCases := []struct { - Name string - JWS bJSONWebSignature - JWK *jose.JSONWebKey - Body string - ExpectedProblem *probs.ProblemDetails - ErrorStatType string + Name string + JWS bJSONWebSignature + JWK *jose.JSONWebKey + Body string + WantErrType berrors.ErrorType + WantErrDetail string + WantStatType string }{ { - Name: "JWS with an invalid algorithm", - JWS: bJSONWebSignature{wrongAlgJWS}, - JWK: goodJWK, - ExpectedProblem: &probs.ProblemDetails{ - Type: probs.BadSignatureAlgorithmProblem, - Detail: "JWS signature header contains unsupported algorithm \"HS256\", expected one of [RS256 ES256 ES384 ES512]", - HTTPStatus: http.StatusBadRequest, - }, - ErrorStatType: "JWSAlgorithmCheckFailed", + Name: "JWS with an invalid algorithm", + JWS: bJSONWebSignature{wrongAlgJWS}, + JWK: goodJWK, + WantErrType: berrors.BadSignatureAlgorithm, + WantErrDetail: "JWS signature header contains unsupported algorithm \"HS256\", expected one of [RS256 ES256 ES384 ES512]", + WantStatType: "JWSAlgorithmCheckFailed", }, { - Name: "JWS with an invalid nonce (test/config-next)", - JWS: bJSONWebSignature{signer.invalidNonce()}, - JWK: goodJWK, - ExpectedProblem: &probs.ProblemDetails{ - Type: probs.BadNonceProblem, - Detail: "JWS has an invalid anti-replay nonce: \"mlolmlol3ov77I5Ui-cdaY_k8IcjK58FvbG0y_BCRrx5rGQ8rjA\"", - HTTPStatus: http.StatusBadRequest, - }, - ErrorStatType: "JWSInvalidNonce", + Name: "JWS with an invalid nonce (test/config-next)", + JWS: bJSONWebSignature{signer.invalidNonce()}, + JWK: goodJWK, + WantErrType: berrors.BadNonce, + WantErrDetail: "JWS has an invalid anti-replay nonce: \"mlolmlol3ov77I5Ui-cdaY_k8IcjK58FvbG0y_BCRrx5rGQ8rjA\"", + WantStatType: "JWSInvalidNonce", }, { - Name: "JWS with broken signature", - JWS: bJSONWebSignature{badJWS}, - JWK: badJWS.Signatures[0].Header.JSONWebKey, - ExpectedProblem: &probs.ProblemDetails{ - Type: probs.MalformedProblem, - Detail: "JWS verification error", - HTTPStatus: http.StatusBadRequest, - }, - ErrorStatType: "JWSVerifyFailed", + Name: "JWS with broken signature", + JWS: bJSONWebSignature{badJWS}, + JWK: badJWS.Signatures[0].Header.JSONWebKey, + WantErrType: berrors.Malformed, + WantErrDetail: "JWS verification error", + WantStatType: "JWSVerifyFailed", }, { - Name: "JWS with incorrect URL", - JWS: bJSONWebSignature{wrongURLHeaderJWS}, - JWK: wrongURLHeaderJWS.Signatures[0].Header.JSONWebKey, - ExpectedProblem: &probs.ProblemDetails{ - Type: probs.MalformedProblem, - Detail: "JWS header parameter 'url' incorrect. Expected \"http://localhost/test\" got \"foobar\"", - HTTPStatus: http.StatusBadRequest, - }, - ErrorStatType: "JWSMismatchedURL", + Name: "JWS with incorrect URL", + JWS: bJSONWebSignature{wrongURLHeaderJWS}, + JWK: wrongURLHeaderJWS.Signatures[0].Header.JSONWebKey, + WantErrType: berrors.Malformed, + WantErrDetail: "JWS header parameter 'url' incorrect. Expected \"http://localhost/test\" got \"foobar\"", + WantStatType: "JWSMismatchedURL", }, { - Name: "Valid JWS with invalid JSON in the protected body", - JWS: bJSONWebSignature{badJSONJWS}, - JWK: goodJWK, - ExpectedProblem: &probs.ProblemDetails{ - Type: probs.MalformedProblem, - Detail: "Request payload did not parse as JSON", - HTTPStatus: http.StatusBadRequest, - }, - ErrorStatType: "JWSBodyUnmarshalFailed", + Name: "Valid JWS with invalid JSON in the protected body", + JWS: bJSONWebSignature{badJSONJWS}, + JWK: goodJWK, + WantErrType: berrors.Malformed, + WantErrDetail: "Request payload did not parse as JSON", + WantStatType: "JWSBodyUnmarshalFailed", }, { Name: "Good JWS and JWK", @@ -1398,17 +1396,28 @@ func TestValidJWSForKey(t *testing.T) { t.Run(tc.Name, func(t *testing.T) { wfe.stats.joseErrorCount.Reset() request := makePostRequestWithPath("test", tc.Body) - outPayload, prob := wfe.validJWSForKey(context.Background(), &tc.JWS, tc.JWK, request) - if tc.ExpectedProblem == nil && prob != nil { - t.Fatalf("Expected nil problem, got %#v\n", prob) - } else if tc.ExpectedProblem == nil { - test.AssertEquals(t, string(outPayload), payload) + + gotPayload, gotErr := wfe.validJWSForKey(context.Background(), &tc.JWS, tc.JWK, request) + if tc.WantErrDetail == "" { + if gotErr != nil { + t.Fatalf("validJWSForKey(%#v, %#v, %#v) = %#v, want nil", tc.JWS, tc.JWK, request, gotErr) + } + if string(gotPayload) != payload { + t.Fatalf("validJWSForKey(%#v, %#v, %#v) = %q, want %q", tc.JWS, tc.JWK, request, string(gotPayload), payload) + } } else { - test.AssertMarshaledEquals(t, prob, tc.ExpectedProblem) - } - if tc.ErrorStatType != "" { + berr, ok := gotErr.(*berrors.BoulderError) + if !ok { + t.Fatalf("validJWSForKey(%#v, %#v, %#v) returned %T, want BoulderError", tc.JWS, tc.JWK, request, gotErr) + } + if berr.Type != tc.WantErrType { + t.Errorf("validJWSForKey(%#v, %#v, %#v) = %#v, want %#v", tc.JWS, tc.JWK, request, berr.Type, tc.WantErrType) + } + if !strings.Contains(berr.Detail, tc.WantErrDetail) { + t.Errorf("validJWSForKey(%#v, %#v, %#v) = %q, want %q", tc.JWS, tc.JWK, request, berr.Detail, tc.WantErrDetail) + } test.AssertMetricWithLabelsEquals( - t, wfe.stats.joseErrorCount, prometheus.Labels{"type": tc.ErrorStatType}, 1) + t, wfe.stats.joseErrorCount, prometheus.Labels{"type": tc.WantStatType}, 1) } }) } @@ -1431,60 +1440,49 @@ func TestValidPOSTForAccount(t *testing.T) { _, _, embeddedJWSBody := signer.embeddedJWK(nil, "http://localhost/test", `{"test":"passed"}`) testCases := []struct { - Name string - Request *http.Request - ExpectedProblem *probs.ProblemDetails - ExpectedPayload string - ExpectedAcct *core.Registration - ExpectedJWS *jose.JSONWebSignature - ErrorStatType string + Name string + Request *http.Request + WantPayload string + WantAcct *core.Registration + WantJWS *jose.JSONWebSignature + WantErrType berrors.ErrorType + WantErrDetail string + WantStatType string }{ { - Name: "Invalid JWS", - Request: makePostRequestWithPath("test", "foo"), - ExpectedProblem: &probs.ProblemDetails{ - Type: probs.MalformedProblem, - Detail: "Parse error reading JWS", - HTTPStatus: http.StatusBadRequest, - }, - ErrorStatType: "JWSUnmarshalFailed", + Name: "Invalid JWS", + Request: makePostRequestWithPath("test", "foo"), + WantErrType: berrors.Malformed, + WantErrDetail: "Parse error reading JWS", + WantStatType: "JWSUnmarshalFailed", }, { - Name: "Embedded Key JWS", - Request: makePostRequestWithPath("test", embeddedJWSBody), - ExpectedProblem: &probs.ProblemDetails{ - Type: probs.MalformedProblem, - Detail: "No Key ID in JWS header", - HTTPStatus: http.StatusBadRequest, - }, - ErrorStatType: "JWSAuthTypeWrong", + Name: "Embedded Key JWS", + Request: makePostRequestWithPath("test", embeddedJWSBody), + WantErrType: berrors.Malformed, + WantErrDetail: "No Key ID in JWS header", + WantStatType: "JWSAuthTypeWrong", }, { - Name: "JWS signed by account that doesn't exist", - Request: makePostRequestWithPath("test", missingJWSBody), - ExpectedProblem: &probs.ProblemDetails{ - Type: probs.AccountDoesNotExistProblem, - Detail: "Account \"http://localhost/acme/acct/102\" not found", - HTTPStatus: http.StatusBadRequest, - }, - ErrorStatType: "JWSKeyIDNotFound", + Name: "JWS signed by account that doesn't exist", + Request: makePostRequestWithPath("test", missingJWSBody), + WantErrType: berrors.AccountDoesNotExist, + WantErrDetail: "Account \"http://localhost/acme/acct/102\" not found", + WantStatType: "JWSKeyIDNotFound", }, { - Name: "JWS signed by account that's deactivated", - Request: makePostRequestWithPath("test", deactivatedJWSBody), - ExpectedProblem: &probs.ProblemDetails{ - Type: probs.UnauthorizedProblem, - Detail: "Account is not valid, has status \"deactivated\"", - HTTPStatus: http.StatusForbidden, - }, - ErrorStatType: "JWSKeyIDAccountInvalid", + Name: "JWS signed by account that's deactivated", + Request: makePostRequestWithPath("test", deactivatedJWSBody), + WantErrType: berrors.Unauthorized, + WantErrDetail: "Account is not valid, has status \"deactivated\"", + WantStatType: "JWSKeyIDAccountInvalid", }, { - Name: "Valid JWS for account", - Request: makePostRequestWithPath("test", validJWSBody), - ExpectedPayload: `{"test":"passed"}`, - ExpectedAcct: &validAccount, - ExpectedJWS: validJWS, + Name: "Valid JWS for account", + Request: makePostRequestWithPath("test", validJWSBody), + WantPayload: `{"test":"passed"}`, + WantAcct: &validAccount, + WantJWS: validJWS, }, } @@ -1492,19 +1490,30 @@ func TestValidPOSTForAccount(t *testing.T) { t.Run(tc.Name, func(t *testing.T) { wfe.stats.joseErrorCount.Reset() inputLogEvent := newRequestEvent() - outPayload, jws, acct, prob := wfe.validPOSTForAccount(tc.Request, context.Background(), inputLogEvent) - if tc.ExpectedProblem == nil && prob != nil { - t.Fatalf("Expected nil problem, got %#v\n", prob) - } else if tc.ExpectedProblem == nil { - test.AssertEquals(t, string(outPayload), tc.ExpectedPayload) - test.AssertMarshaledEquals(t, acct, tc.ExpectedAcct) - test.AssertMarshaledEquals(t, jws, tc.ExpectedJWS) + + gotPayload, gotJWS, gotAcct, gotErr := wfe.validPOSTForAccount(tc.Request, context.Background(), inputLogEvent) + if tc.WantErrDetail == "" { + if gotErr != nil { + t.Fatalf("validPOSTForAccount(%#v) = %#v, want nil", tc.Request, gotErr) + } + if string(gotPayload) != tc.WantPayload { + t.Fatalf("validPOSTForAccount(%#v) = %q, want %q", tc.Request, string(gotPayload), tc.WantPayload) + } + test.AssertMarshaledEquals(t, gotJWS, tc.WantJWS) + test.AssertMarshaledEquals(t, gotAcct, tc.WantAcct) } else { - test.AssertMarshaledEquals(t, prob, tc.ExpectedProblem) - } - if tc.ErrorStatType != "" { + berr, ok := gotErr.(*berrors.BoulderError) + if !ok { + t.Fatalf("validPOSTForAccount(%#v) returned %T, want BoulderError", tc.Request, gotErr) + } + if berr.Type != tc.WantErrType { + t.Errorf("validPOSTForAccount(%#v) = %#v, want %#v", tc.Request, berr.Type, tc.WantErrType) + } + if !strings.Contains(berr.Detail, tc.WantErrDetail) { + t.Errorf("validPOSTForAccount(%#v) = %q, want %q", tc.Request, berr.Detail, tc.WantErrDetail) + } test.AssertMetricWithLabelsEquals( - t, wfe.stats.joseErrorCount, prometheus.Labels{"type": tc.ErrorStatType}, 1) + t, wfe.stats.joseErrorCount, prometheus.Labels{"type": tc.WantStatType}, 1) } }) } @@ -1524,38 +1533,50 @@ func TestValidPOSTAsGETForAccount(t *testing.T) { _, _, validRequest := signer.byKeyID(1, nil, "http://localhost/test", "") testCases := []struct { - Name string - Request *http.Request - ExpectedProblem *probs.ProblemDetails - ExpectedLogEvent web.RequestEvent + Name string + Request *http.Request + WantErrType berrors.ErrorType + WantErrDetail string + WantLogEvent web.RequestEvent }{ { - Name: "Non-empty JWS payload", - Request: makePostRequestWithPath("test", invalidPayloadRequest), - ExpectedProblem: probs.Malformed("POST-as-GET requests must have an empty payload"), - ExpectedLogEvent: web.RequestEvent{}, + Name: "Non-empty JWS payload", + Request: makePostRequestWithPath("test", invalidPayloadRequest), + WantErrType: berrors.Malformed, + WantErrDetail: "POST-as-GET requests must have an empty payload", + WantLogEvent: web.RequestEvent{}, }, { Name: "Valid POST-as-GET", Request: makePostRequestWithPath("test", validRequest), - ExpectedLogEvent: web.RequestEvent{ + WantLogEvent: web.RequestEvent{ Method: "POST-as-GET", }, }, } for _, tc := range testCases { - ev := newRequestEvent() - _, prob := wfe.validPOSTAsGETForAccount( - tc.Request, - context.Background(), - ev) - if tc.ExpectedProblem == nil && prob != nil { - t.Fatalf("Expected nil problem, got %#v\n", prob) - } else if tc.ExpectedProblem != nil { - test.AssertMarshaledEquals(t, prob, tc.ExpectedProblem) - } - test.AssertMarshaledEquals(t, *ev, tc.ExpectedLogEvent) + t.Run(tc.Name, func(t *testing.T) { + ev := newRequestEvent() + _, gotErr := wfe.validPOSTAsGETForAccount(tc.Request, context.Background(), ev) + if tc.WantErrDetail == "" { + if gotErr != nil { + t.Fatalf("validPOSTAsGETForAccount(%#v) = %#v, want nil", tc.Request, gotErr) + } + } else { + berr, ok := gotErr.(*berrors.BoulderError) + if !ok { + t.Fatalf("validPOSTAsGETForAccount(%#v) returned %T, want BoulderError", tc.Request, gotErr) + } + if berr.Type != tc.WantErrType { + t.Errorf("validPOSTAsGETForAccount(%#v) = %#v, want %#v", tc.Request, berr.Type, tc.WantErrType) + } + if !strings.Contains(berr.Detail, tc.WantErrDetail) { + t.Errorf("validPOSTAsGETForAccount(%#v) = %q, want %q", tc.Request, berr.Detail, tc.WantErrDetail) + } + } + test.AssertMarshaledEquals(t, *ev, tc.WantLogEvent) + }) } } @@ -1586,10 +1607,10 @@ func TestValidPOSTForAccountSwappedKey(t *testing.T) { // Ensure that ValidPOSTForAccount produces an error since the // mockSADifferentStoredKey will return a different key than the one we used to // sign the request - _, _, _, prob := wfe.validPOSTForAccount(request, ctx, event) - test.Assert(t, prob != nil, "No error returned for request signed by wrong key") - test.AssertEquals(t, prob.Type, probs.MalformedProblem) - test.AssertEquals(t, prob.Detail, "JWS verification error") + _, _, _, err := wfe.validPOSTForAccount(request, ctx, event) + test.AssertError(t, err, "No error returned for request signed by wrong key") + test.AssertErrorIs(t, err, berrors.Malformed) + test.AssertContains(t, err.Error(), "JWS verification error") } func TestValidSelfAuthenticatedPOSTGoodKeyErrors(t *testing.T) { @@ -1607,8 +1628,8 @@ func TestValidSelfAuthenticatedPOSTGoodKeyErrors(t *testing.T) { _, _, validJWSBody := signer.embeddedJWK(nil, "http://localhost/test", `{"test":"passed"}`) request := makePostRequestWithPath("test", validJWSBody) - _, _, prob := wfe.validSelfAuthenticatedPOST(context.Background(), request) - test.AssertEquals(t, prob.Type, probs.ServerInternalProblem) + _, _, err = wfe.validSelfAuthenticatedPOST(context.Background(), request) + test.AssertErrorIs(t, err, berrors.InternalServer) badKeyCheckFunc := func(ctx context.Context, keyHash []byte) (bool, error) { return false, fmt.Errorf("oh no: %w", goodkey.ErrBadKey) @@ -1622,8 +1643,8 @@ func TestValidSelfAuthenticatedPOSTGoodKeyErrors(t *testing.T) { _, _, validJWSBody = signer.embeddedJWK(nil, "http://localhost/test", `{"test":"passed"}`) request = makePostRequestWithPath("test", validJWSBody) - _, _, prob = wfe.validSelfAuthenticatedPOST(context.Background(), request) - test.AssertEquals(t, prob.Type, probs.BadPublicKeyProblem) + _, _, err = wfe.validSelfAuthenticatedPOST(context.Background(), request) + test.AssertErrorIs(t, err, berrors.BadPublicKey) } func TestValidSelfAuthenticatedPOST(t *testing.T) { @@ -1634,58 +1655,65 @@ func TestValidSelfAuthenticatedPOST(t *testing.T) { _, _, keyIDJWSBody := signer.byKeyID(1, nil, "http://localhost/test", `{"test":"passed"}`) testCases := []struct { - Name string - Request *http.Request - ExpectedProblem *probs.ProblemDetails - ExpectedPayload string - ExpectedJWK *jose.JSONWebKey - ErrorStatType string + Name string + Request *http.Request + WantPayload string + WantJWK *jose.JSONWebKey + WantErrType berrors.ErrorType + WantErrDetail string + WantStatType string }{ { - Name: "Invalid JWS", - Request: makePostRequestWithPath("test", "foo"), - ExpectedProblem: &probs.ProblemDetails{ - Type: probs.MalformedProblem, - Detail: "Parse error reading JWS", - HTTPStatus: http.StatusBadRequest, - }, - ErrorStatType: "JWSUnmarshalFailed", + Name: "Invalid JWS", + Request: makePostRequestWithPath("test", "foo"), + WantErrType: berrors.Malformed, + WantErrDetail: "Parse error reading JWS", + WantStatType: "JWSUnmarshalFailed", }, { - Name: "JWS with key ID", - Request: makePostRequestWithPath("test", keyIDJWSBody), - ExpectedProblem: &probs.ProblemDetails{ - Type: probs.MalformedProblem, - Detail: "No embedded JWK in JWS header", - HTTPStatus: http.StatusBadRequest, - }, - ErrorStatType: "JWSAuthTypeWrong", + Name: "JWS with key ID", + Request: makePostRequestWithPath("test", keyIDJWSBody), + WantErrType: berrors.Malformed, + WantErrDetail: "No embedded JWK in JWS header", + WantStatType: "JWSAuthTypeWrong", }, { - Name: "Valid JWS", - Request: makePostRequestWithPath("test", validJWSBody), - ExpectedPayload: `{"test":"passed"}`, - ExpectedJWK: validKey, + Name: "Valid JWS", + Request: makePostRequestWithPath("test", validJWSBody), + WantPayload: `{"test":"passed"}`, + WantJWK: validKey, }, } for _, tc := range testCases { t.Run(tc.Name, func(t *testing.T) { wfe.stats.joseErrorCount.Reset() - outPayload, jwk, prob := wfe.validSelfAuthenticatedPOST(context.Background(), tc.Request) - if tc.ExpectedProblem == nil && prob != nil { - t.Fatalf("Expected nil problem, got %#v\n", prob) - } else if tc.ExpectedProblem == nil { - inThumb, _ := tc.ExpectedJWK.Thumbprint(crypto.SHA256) - outThumb, _ := jwk.Thumbprint(crypto.SHA256) - test.AssertDeepEquals(t, inThumb, outThumb) - test.AssertEquals(t, string(outPayload), tc.ExpectedPayload) + gotPayload, gotJWK, gotErr := wfe.validSelfAuthenticatedPOST(context.Background(), tc.Request) + if tc.WantErrDetail == "" { + if gotErr != nil { + t.Fatalf("validSelfAuthenticatedPOST(%#v) = %#v, want nil", tc.Request, gotErr) + } + if string(gotPayload) != tc.WantPayload { + t.Fatalf("validSelfAuthenticatedPOST(%#v) = %q, want %q", tc.Request, string(gotPayload), tc.WantPayload) + } + gotThumb, _ := gotJWK.Thumbprint(crypto.SHA256) + wantThumb, _ := tc.WantJWK.Thumbprint(crypto.SHA256) + if !slices.Equal(gotThumb, wantThumb) { + t.Fatalf("validSelfAuthenticatedPOST(%#v) = %#v, want %#v", tc.Request, gotThumb, wantThumb) + } } else { - test.AssertMarshaledEquals(t, prob, tc.ExpectedProblem) - } - if tc.ErrorStatType != "" { + berr, ok := gotErr.(*berrors.BoulderError) + if !ok { + t.Fatalf("validSelfAuthenticatedPOST(%#v) returned %T, want BoulderError", tc.Request, gotErr) + } + if berr.Type != tc.WantErrType { + t.Errorf("validSelfAuthenticatedPOST(%#v) = %#v, want %#v", tc.Request, berr.Type, tc.WantErrType) + } + if !strings.Contains(berr.Detail, tc.WantErrDetail) { + t.Errorf("validSelfAuthenticatedPOST(%#v) = %q, want %q", tc.Request, berr.Detail, tc.WantErrDetail) + } test.AssertMetricWithLabelsEquals( - t, wfe.stats.joseErrorCount, prometheus.Labels{"type": tc.ErrorStatType}, 1) + t, wfe.stats.joseErrorCount, prometheus.Labels{"type": tc.WantStatType}, 1) } }) } @@ -1699,56 +1727,44 @@ func TestMatchJWSURLs(t *testing.T) { urlBJWS, _, _ := signer.embeddedJWK(nil, "example.org", "") testCases := []struct { - Name string - Outer *jose.JSONWebSignature - Inner *jose.JSONWebSignature - ExpectedProblem *probs.ProblemDetails - ErrorStatType string + Name string + Outer *jose.JSONWebSignature + Inner *jose.JSONWebSignature + WantErrType berrors.ErrorType + WantErrDetail string + WantStatType string }{ { - Name: "Outer JWS without URL", - Outer: noURLJWS, - Inner: urlAJWS, - ExpectedProblem: &probs.ProblemDetails{ - Type: probs.MalformedProblem, - Detail: "Outer JWS header parameter 'url' required", - HTTPStatus: http.StatusBadRequest, - }, - ErrorStatType: "KeyRolloverOuterJWSNoURL", + Name: "Outer JWS without URL", + Outer: noURLJWS, + Inner: urlAJWS, + WantErrType: berrors.Malformed, + WantErrDetail: "Outer JWS header parameter 'url' required", + WantStatType: "KeyRolloverOuterJWSNoURL", }, { - Name: "Inner JWS without URL", - Outer: urlAJWS, - Inner: noURLJWS, - ExpectedProblem: &probs.ProblemDetails{ - Type: probs.MalformedProblem, - Detail: "Inner JWS header parameter 'url' required", - HTTPStatus: http.StatusBadRequest, - }, - ErrorStatType: "KeyRolloverInnerJWSNoURL", + Name: "Inner JWS without URL", + Outer: urlAJWS, + Inner: noURLJWS, + WantErrType: berrors.Malformed, + WantErrDetail: "Inner JWS header parameter 'url' required", + WantStatType: "KeyRolloverInnerJWSNoURL", }, { - Name: "Inner and outer JWS without URL", - Outer: noURLJWS, - Inner: noURLJWS, - ExpectedProblem: &probs.ProblemDetails{ - Type: probs.MalformedProblem, - // The Outer JWS is validated first - Detail: "Outer JWS header parameter 'url' required", - HTTPStatus: http.StatusBadRequest, - }, - ErrorStatType: "KeyRolloverOuterJWSNoURL", + Name: "Inner and outer JWS without URL", + Outer: noURLJWS, + Inner: noURLJWS, + WantErrType: berrors.Malformed, + WantErrDetail: "Outer JWS header parameter 'url' required", + WantStatType: "KeyRolloverOuterJWSNoURL", }, { - Name: "Mismatched inner and outer JWS URLs", - Outer: urlAJWS, - Inner: urlBJWS, - ExpectedProblem: &probs.ProblemDetails{ - Type: probs.MalformedProblem, - Detail: "Outer JWS 'url' value \"example.com\" does not match inner JWS 'url' value \"example.org\"", - HTTPStatus: http.StatusBadRequest, - }, - ErrorStatType: "KeyRolloverMismatchedURLs", + Name: "Mismatched inner and outer JWS URLs", + Outer: urlAJWS, + Inner: urlBJWS, + WantErrType: berrors.Malformed, + WantErrDetail: "Outer JWS 'url' value \"example.com\" does not match inner JWS 'url' value \"example.org\"", + WantStatType: "KeyRolloverMismatchedURLs", }, { Name: "Matching inner and outer JWS URLs", @@ -1760,15 +1776,27 @@ func TestMatchJWSURLs(t *testing.T) { for _, tc := range testCases { t.Run(tc.Name, func(t *testing.T) { wfe.stats.joseErrorCount.Reset() - prob := wfe.matchJWSURLs(tc.Outer.Signatures[0].Header, tc.Inner.Signatures[0].Header) - if prob != nil && tc.ExpectedProblem == nil { - t.Errorf("matchJWSURLs failed. Expected no problem, got %#v", prob) + outer := tc.Outer.Signatures[0].Header + inner := tc.Inner.Signatures[0].Header + + gotErr := wfe.matchJWSURLs(outer, inner) + if tc.WantErrDetail == "" { + if gotErr != nil { + t.Fatalf("matchJWSURLs(%#v, %#v) = %#v, want nil", outer, inner, gotErr) + } } else { - test.AssertMarshaledEquals(t, prob, tc.ExpectedProblem) - } - if tc.ErrorStatType != "" { + berr, ok := gotErr.(*berrors.BoulderError) + if !ok { + t.Fatalf("matchJWSURLs(%#v, %#v) returned %T, want BoulderError", outer, inner, gotErr) + } + if berr.Type != tc.WantErrType { + t.Errorf("matchJWSURLs(%#v, %#v) = %#v, want %#v", outer, inner, berr.Type, tc.WantErrType) + } + if !strings.Contains(berr.Detail, tc.WantErrDetail) { + t.Errorf("matchJWSURLs(%#v, %#v) = %q, want %q", outer, inner, berr.Detail, tc.WantErrDetail) + } test.AssertMetricWithLabelsEquals( - t, wfe.stats.joseErrorCount, prometheus.Labels{"type": tc.ErrorStatType}, 1) + t, wfe.stats.joseErrorCount, prometheus.Labels{"type": tc.WantStatType}, 1) } }) } diff --git a/third-party/github.com/letsencrypt/boulder/wfe2/wfe.go b/third-party/github.com/letsencrypt/boulder/wfe2/wfe.go index 1b3cc0b15..891d165b6 100644 --- a/third-party/github.com/letsencrypt/boulder/wfe2/wfe.go +++ b/third-party/github.com/letsencrypt/boulder/wfe2/wfe.go @@ -10,9 +10,11 @@ import ( "errors" "fmt" "math/big" + "math/rand/v2" "net" "net/http" - "slices" + "net/netip" + "net/url" "strconv" "strings" "time" @@ -21,28 +23,29 @@ import ( "github.com/prometheus/client_golang/prometheus" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "go.opentelemetry.io/otel/trace" + "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/emptypb" "github.com/letsencrypt/boulder/core" corepb "github.com/letsencrypt/boulder/core/proto" + emailpb "github.com/letsencrypt/boulder/email/proto" berrors "github.com/letsencrypt/boulder/errors" "github.com/letsencrypt/boulder/features" "github.com/letsencrypt/boulder/goodkey" bgrpc "github.com/letsencrypt/boulder/grpc" - "github.com/letsencrypt/boulder/policy" - "github.com/letsencrypt/boulder/ratelimits" - - // 'grpc/noncebalancer' is imported for its init function. - _ "github.com/letsencrypt/boulder/grpc/noncebalancer" + _ "github.com/letsencrypt/boulder/grpc/noncebalancer" // imported for its init function. "github.com/letsencrypt/boulder/identifier" "github.com/letsencrypt/boulder/issuance" blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/metrics/measured_http" "github.com/letsencrypt/boulder/nonce" + "github.com/letsencrypt/boulder/policy" "github.com/letsencrypt/boulder/probs" rapb "github.com/letsencrypt/boulder/ra/proto" + "github.com/letsencrypt/boulder/ratelimits" "github.com/letsencrypt/boulder/revocation" sapb "github.com/letsencrypt/boulder/sa/proto" + "github.com/letsencrypt/boulder/unpause" "github.com/letsencrypt/boulder/web" ) @@ -51,30 +54,23 @@ import ( // lowercase plus hyphens. If you violate that assumption you should update // measured_http. const ( - directoryPath = "/directory" - newAcctPath = "/acme/new-acct" - acctPath = "/acme/acct/" - // When we moved to authzv2, we used a "-v3" suffix to avoid confusion - // regarding ACMEv2. - authzPath = "/acme/authz-v3/" - challengePath = "/acme/chall-v3/" - certPath = "/acme/cert/" - revokeCertPath = "/acme/revoke-cert" - buildIDPath = "/build" - rolloverPath = "/acme/key-change" + directoryPath = "/directory" newNoncePath = "/acme/new-nonce" + newAcctPath = "/acme/new-acct" newOrderPath = "/acme/new-order" + rolloverPath = "/acme/key-change" + revokeCertPath = "/acme/revoke-cert" + acctPath = "/acme/acct/" orderPath = "/acme/order/" + authzPath = "/acme/authz/" + challengePath = "/acme/chall/" finalizeOrderPath = "/acme/finalize/" + certPath = "/acme/cert/" + renewalInfoPath = "/acme/renewal-info/" - getAPIPrefix = "/get/" - getOrderPath = getAPIPrefix + "order/" - getAuthzPath = getAPIPrefix + "authz-v3/" - getChallengePath = getAPIPrefix + "chall-v3/" - getCertPath = getAPIPrefix + "cert/" - - // Draft or likely-to-change paths - renewalInfoPath = "/draft-ietf-acme-ari-03/renewalInfo/" + // Non-ACME paths. + getCertPath = "/get/cert/" + buildIDPath = "/build" ) const ( @@ -94,6 +90,7 @@ var errIncompleteGRPCResponse = errors.New("incomplete gRPC response message") type WebFrontEndImpl struct { ra rapb.RegistrationAuthorityClient sa sapb.StorageAuthorityReadOnlyClient + ee emailpb.ExporterClient // gnc is a nonce-service client used exclusively for the issuance of // nonces. It's configured to route requests to backends colocated with the // WFE. @@ -106,7 +103,7 @@ type WebFrontEndImpl struct { rnc nonce.Redeemer // rncKey is the HMAC key used to derive the prefix of nonce backends used // for nonce redemption. - rncKey string + rncKey []byte accountGetter AccountGetter log blog.Logger clk clock.Clock @@ -146,29 +143,29 @@ type WebFrontEndImpl struct { // CORS settings AllowOrigins []string + // How many contacts to allow in a single NewAccount request. + maxContactsPerReg int + // requestTimeout is the per-request overall timeout. requestTimeout time.Duration - // StaleTimeout determines the required staleness for resources allowed to be - // accessed via Boulder-specific GET-able APIs. Resources newer than + // StaleTimeout determines the required staleness for certificates to be + // accessed via the Boulder-specific GET API. Certificates newer than // staleTimeout must be accessed via POST-as-GET and the RFC 8555 ACME API. We // do this to incentivize client developers to use the standard API. staleTimeout time.Duration - // How long before authorizations and pending authorizations expire. The - // Boulder specific GET-able API uses these values to find the creation date - // of authorizations to determine if they are stale enough. The values should - // match the ones used by the RA. - authorizationLifetime time.Duration - pendingAuthorizationLifetime time.Duration - limiter *ratelimits.Limiter - txnBuilder *ratelimits.TransactionBuilder - maxNames int + limiter *ratelimits.Limiter + txnBuilder *ratelimits.TransactionBuilder - // certificateProfileNames is a list of profile names that are allowed to be - // passed to the newOrder endpoint. If a profile name is not in this list, - // the request will be rejected as malformed. - certificateProfileNames []string + unpauseSigner unpause.JWTSigner + unpauseJWTLifetime time.Duration + unpauseURL string + + // 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. + certProfiles map[string]string } // NewWebFrontEndImpl constructs a web service for Boulder @@ -181,18 +178,20 @@ func NewWebFrontEndImpl( logger blog.Logger, requestTimeout time.Duration, staleTimeout time.Duration, - authorizationLifetime time.Duration, - pendingAuthorizationLifetime time.Duration, + maxContactsPerReg int, rac rapb.RegistrationAuthorityClient, sac sapb.StorageAuthorityReadOnlyClient, + eec emailpb.ExporterClient, gnc nonce.Getter, rnc nonce.Redeemer, - rncKey string, + rncKey []byte, accountGetter AccountGetter, limiter *ratelimits.Limiter, txnBuilder *ratelimits.TransactionBuilder, - maxNames int, - certificateProfileNames []string, + certProfiles map[string]string, + unpauseSigner unpause.JWTSigner, + unpauseJWTLifetime time.Duration, + unpauseURL string, ) (WebFrontEndImpl, error) { if len(issuerCertificates) == 0 { return WebFrontEndImpl{}, errors.New("must provide at least one issuer certificate") @@ -211,26 +210,28 @@ func NewWebFrontEndImpl( } wfe := WebFrontEndImpl{ - log: logger, - clk: clk, - keyPolicy: keyPolicy, - certificateChains: certificateChains, - issuerCertificates: issuerCertificates, - stats: initStats(stats), - requestTimeout: requestTimeout, - staleTimeout: staleTimeout, - authorizationLifetime: authorizationLifetime, - pendingAuthorizationLifetime: pendingAuthorizationLifetime, - ra: rac, - sa: sac, - gnc: gnc, - rnc: rnc, - rncKey: rncKey, - accountGetter: accountGetter, - limiter: limiter, - txnBuilder: txnBuilder, - maxNames: maxNames, - certificateProfileNames: certificateProfileNames, + log: logger, + clk: clk, + keyPolicy: keyPolicy, + certificateChains: certificateChains, + issuerCertificates: issuerCertificates, + stats: initStats(stats), + requestTimeout: requestTimeout, + staleTimeout: staleTimeout, + maxContactsPerReg: maxContactsPerReg, + ra: rac, + sa: sac, + ee: eec, + gnc: gnc, + rnc: rnc, + rncKey: rncKey, + accountGetter: accountGetter, + limiter: limiter, + txnBuilder: txnBuilder, + certProfiles: certProfiles, + unpauseSigner: unpauseSigner, + unpauseJWTLifetime: unpauseJWTLifetime, + unpauseURL: unpauseURL, } return wfe, nil @@ -274,11 +275,6 @@ func (wfe *WebFrontEndImpl) HandleFunc(mux *http.ServeMux, pattern string, h web if request.URL != nil { logEvent.Slug = request.URL.Path } - tls := request.Header.Get("TLS-Version") - if tls == "TLSv1" || tls == "TLSv1.1" { - wfe.sendError(response, logEvent, probs.Malformed("upgrade your ACME client to support TLSv1.2 or better"), nil) - return - } if request.Method != "GET" || pattern == newNoncePath { nonceMsg, err := wfe.gnc.Nonce(ctx, &emptypb.Empty{}) if err != nil { @@ -408,8 +404,6 @@ func (wfe *WebFrontEndImpl) relativeDirectory(request *http.Request, directory m // various ACME-specified paths. func (wfe *WebFrontEndImpl) Handler(stats prometheus.Registerer, oTelHTTPOptions ...otelhttp.Option) http.Handler { m := http.NewServeMux() - // Boulder specific endpoints - wfe.HandleFunc(m, buildIDPath, wfe.BuildID, "GET") // POSTable ACME endpoints wfe.HandleFunc(m, newAcctPath, wfe.NewAccount, "POST") @@ -422,18 +416,14 @@ func (wfe *WebFrontEndImpl) Handler(stats prometheus.Registerer, oTelHTTPOptions // GETable and POST-as-GETable ACME endpoints wfe.HandleFunc(m, directoryPath, wfe.Directory, "GET", "POST") wfe.HandleFunc(m, newNoncePath, wfe.Nonce, "GET", "POST") - // POST-as-GETable ACME endpoints - // TODO(@cpu): After November 1st, 2020 support for "GET" to the following - // endpoints will be removed, leaving only POST-as-GET support. wfe.HandleFunc(m, orderPath, wfe.GetOrder, "GET", "POST") - wfe.HandleFunc(m, authzPath, wfe.Authorization, "GET", "POST") - wfe.HandleFunc(m, challengePath, wfe.Challenge, "GET", "POST") + wfe.HandleFunc(m, authzPath, wfe.AuthorizationHandler, "GET", "POST") + wfe.HandleFunc(m, challengePath, wfe.ChallengeHandler, "GET", "POST") wfe.HandleFunc(m, certPath, wfe.Certificate, "GET", "POST") - // Boulder-specific GET-able resource endpoints - wfe.HandleFunc(m, getOrderPath, wfe.GetOrder, "GET") - wfe.HandleFunc(m, getAuthzPath, wfe.Authorization, "GET") - wfe.HandleFunc(m, getChallengePath, wfe.Challenge, "GET") + + // Boulder specific endpoints wfe.HandleFunc(m, getCertPath, wfe.Certificate, "GET") + wfe.HandleFunc(m, buildIDPath, wfe.BuildID, "GET") // Endpoint for draft-ietf-acme-ari if features.Get().ServeRenewalInfo { @@ -521,9 +511,9 @@ func (wfe *WebFrontEndImpl) Directory( } if request.Method == http.MethodPost { - acct, prob := wfe.validPOSTAsGETForAccount(request, ctx, logEvent) - if prob != nil { - wfe.sendError(response, logEvent, prob, nil) + acct, err := wfe.validPOSTAsGETForAccount(request, ctx, logEvent) + if err != nil { + wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "Unable to validate JWS"), err) return } logEvent.Requester = acct.ID @@ -550,6 +540,9 @@ func (wfe *WebFrontEndImpl) Directory( wfe.DirectoryCAAIdentity, } } + if len(wfe.certProfiles) != 0 { + metaMap["profiles"] = wfe.certProfiles + } // The "meta" directory entry may also include a string with a website URL if wfe.DirectoryWebsite != "" { metaMap["website"] = wfe.DirectoryWebsite @@ -578,9 +571,9 @@ func (wfe *WebFrontEndImpl) Nonce( response http.ResponseWriter, request *http.Request) { if request.Method == http.MethodPost { - acct, prob := wfe.validPOSTAsGETForAccount(request, ctx, logEvent) - if prob != nil { - wfe.sendError(response, logEvent, prob, nil) + acct, err := wfe.validPOSTAsGETForAccount(request, ctx, logEvent) + if err != nil { + wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "Unable to validate JWS"), err) return } logEvent.Requester = acct.ID @@ -598,10 +591,31 @@ func (wfe *WebFrontEndImpl) Nonce( // field with the "no-store" directive in responses for the newNonce resource, // in order to prevent caching of this resource. response.Header().Set("Cache-Control", "no-store") + + // No need to log successful nonce requests, they're boring. + logEvent.Suppress() } // sendError wraps web.SendError -func (wfe *WebFrontEndImpl) sendError(response http.ResponseWriter, logEvent *web.RequestEvent, prob *probs.ProblemDetails, ierr error) { +func (wfe *WebFrontEndImpl) sendError(response http.ResponseWriter, logEvent *web.RequestEvent, eerr any, ierr error) { + // TODO(#4980): Simplify this function to only take a single error argument, + // and use web.ProblemDetailsForError to extract the corresponding prob from + // that. For now, though, the third argument has to be `any` so that it can + // be either an error or a problem, and this function can handle either one. + var prob *probs.ProblemDetails + switch v := eerr.(type) { + case *probs.ProblemDetails: + prob = v + case error: + prob = web.ProblemDetailsForError(v, "") + default: + panic(fmt.Sprintf("wfe.sendError got %#v (type %T), but expected ProblemDetails or error", eerr, eerr)) + } + + if prob.Type == probs.BadSignatureAlgorithmProblem { + prob.Algorithms = getSupportedAlgs() + } + var bErr *berrors.BoulderError if errors.As(ierr, &bErr) { retryAfterSeconds := int(bErr.RetryAfter.Round(time.Second).Seconds()) @@ -612,6 +626,9 @@ func (wfe *WebFrontEndImpl) sendError(response http.ResponseWriter, logEvent *we } } } + if prob.HTTPStatus == http.StatusInternalServerError { + response.Header().Add(headerRetryAfter, "60") + } wfe.stats.httpErrorCount.With(prometheus.Labels{"type": string(prob.Type)}).Inc() web.SendError(wfe.log, response, logEvent, prob, ierr) } @@ -620,72 +637,86 @@ func link(url, relation string) string { return fmt.Sprintf("<%s>;rel=\"%s\"", url, relation) } -func (wfe *WebFrontEndImpl) newNewAccountLimitTransactions(ip net.IP) []ratelimits.Transaction { - if wfe.limiter == nil && wfe.txnBuilder == nil { - // Limiter is disabled. - return nil +// contactsToEmails converts a slice of ACME contacts (e.g. +// "mailto:person@example.com") to a slice of valid email addresses. If any of +// the contacts contain non-mailto schemes, unparsable addresses, or forbidden +// mail domains, it returns an error so that we can provide feedback to +// misconfigured clients. +func (wfe *WebFrontEndImpl) contactsToEmails(contacts []string) ([]string, error) { + if len(contacts) == 0 { + return nil, nil } - warn := func(err error, limit ratelimits.Name) { - // TODO(#5545): Once key-value rate limits are authoritative this log - // line should be removed in favor of returning the error. - wfe.log.Warningf("checking %s rate limit: %s", limit, err) + if wfe.maxContactsPerReg > 0 && len(contacts) > wfe.maxContactsPerReg { + return nil, berrors.MalformedError("too many contacts provided: %d > %d", len(contacts), wfe.maxContactsPerReg) } - var transactions []ratelimits.Transaction - txn, err := wfe.txnBuilder.RegistrationsPerIPAddressTransaction(ip) - if err != nil { - warn(err, ratelimits.NewRegistrationsPerIPAddress) - return nil - } - transactions = append(transactions, txn) + var emails []string + for _, contact := range contacts { + if contact == "" { + return nil, berrors.InvalidEmailError("empty contact") + } - if ip.To4() != nil { - // This request was made from an IPv4 address. - return transactions + parsed, err := url.Parse(contact) + if err != nil { + return nil, berrors.InvalidEmailError("unparsable contact") + } + + if parsed.Scheme != "mailto" { + return nil, berrors.UnsupportedContactError("only contact scheme 'mailto:' is supported") + } + + if parsed.RawQuery != "" || contact[len(contact)-1] == '?' { + return nil, berrors.InvalidEmailError("contact email contains a question mark") + } + + if parsed.Fragment != "" || contact[len(contact)-1] == '#' { + return nil, berrors.InvalidEmailError("contact email contains a '#'") + } + + if !core.IsASCII(contact) { + return nil, berrors.InvalidEmailError("contact email contains non-ASCII characters") + } + + err = policy.ValidEmail(parsed.Opaque) + if err != nil { + return nil, err + } + + emails = append(emails, parsed.Opaque) } - txn, err = wfe.txnBuilder.RegistrationsPerIPv6RangeTransaction(ip) - if err != nil { - warn(err, ratelimits.NewRegistrationsPerIPv6Range) - return nil - } - return append(transactions, txn) + return emails, nil } // checkNewAccountLimits checks whether sufficient limit quota exists for the // creation of a new account. If so, that quota is spent. If an error is -// encountered during the check, it is logged but not returned. -// -// TODO(#5545): For now we're simply exercising the new rate limiter codepath. -// This should eventually return a berrors.RateLimit error containing the retry -// after duration among other information available in the ratelimits.Decision. -func (wfe *WebFrontEndImpl) checkNewAccountLimits(ctx context.Context, transactions []ratelimits.Transaction) { - if wfe.limiter == nil && wfe.txnBuilder == nil { - // Limiter is disabled. - return - } - - _, err := wfe.limiter.BatchSpend(ctx, transactions) +// encountered during the check, it is logged but not returned. A refund +// function is returned that can be called to refund the quota if the account +// creation fails, the func will be nil if any error was encountered during the +// check. +func (wfe *WebFrontEndImpl) checkNewAccountLimits(ctx context.Context, ip netip.Addr) (func(), error) { + txns, err := wfe.txnBuilder.NewAccountLimitTransactions(ip) if err != nil { - wfe.log.Errf("checking newAccount limits: %s", err) - } -} - -// refundNewAccountLimits is typically called when a new account creation fails. -// It refunds the limit quota consumed by the request, allowing the caller to -// retry immediately. If an error is encountered during the refund, it is logged -// but not returned. -func (wfe *WebFrontEndImpl) refundNewAccountLimits(ctx context.Context, transactions []ratelimits.Transaction) { - if wfe.limiter == nil && wfe.txnBuilder == nil { - // Limiter is disabled. - return + return nil, fmt.Errorf("building new account limit transactions: %w", err) } - _, err := wfe.limiter.BatchRefund(ctx, transactions) + d, err := wfe.limiter.BatchSpend(ctx, txns) if err != nil { - wfe.log.Errf("refunding newAccount limits: %s", err) + return nil, fmt.Errorf("spending new account limits: %w", err) } + + err = d.Result(wfe.clk.Now()) + if err != nil { + return nil, err + } + + return func() { + _, err := wfe.limiter.BatchRefund(ctx, txns) + if err != nil { + wfe.log.Warningf("refunding new account limits: %s", err) + } + }, nil } // NewAccount is used by clients to submit a new account @@ -698,20 +729,20 @@ func (wfe *WebFrontEndImpl) NewAccount( // NewAccount uses `validSelfAuthenticatedPOST` instead of // `validPOSTforAccount` because there is no account to authenticate against // until after it is created! - body, key, prob := wfe.validSelfAuthenticatedPOST(ctx, request) - if prob != nil { + body, key, err := wfe.validSelfAuthenticatedPOST(ctx, request) + if err != nil { // validSelfAuthenticatedPOST handles its own setting of logEvent.Errors - wfe.sendError(response, logEvent, prob, nil) + wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "Unable to validate JWS"), err) return } var accountCreateRequest struct { - Contact *[]string `json:"contact"` - TermsOfServiceAgreed bool `json:"termsOfServiceAgreed"` - OnlyReturnExisting bool `json:"onlyReturnExisting"` + Contact []string `json:"contact"` + TermsOfServiceAgreed bool `json:"termsOfServiceAgreed"` + OnlyReturnExisting bool `json:"onlyReturnExisting"` } - err := json.Unmarshal(body, &accountCreateRequest) + err = json.Unmarshal(body, &accountCreateRequest) if err != nil { wfe.sendError(response, logEvent, probs.Malformed("Error unmarshaling JSON"), err) return @@ -776,70 +807,52 @@ func (wfe *WebFrontEndImpl) NewAccount( return } + // Do this extraction now, so that we can reject requests whose contact field + // does not contain valid contacts before we actually create the account. + emails, err := wfe.contactsToEmails(accountCreateRequest.Contact) + if err != nil { + wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "Error validating contact(s)"), nil) + return + } + ip, err := extractRequesterIP(request) if err != nil { wfe.sendError( response, logEvent, probs.ServerInternal("couldn't parse the remote (that is, the client's) address"), - fmt.Errorf("Couldn't parse RemoteAddr: %s", request.RemoteAddr), + fmt.Errorf("couldn't parse RemoteAddr: %s", request.RemoteAddr), ) return } - // Prepare account information to create corepb.Registration - ipBytes, err := ip.MarshalText() + refundLimits, err := wfe.checkNewAccountLimits(ctx, ip) if err != nil { - wfe.sendError(response, logEvent, - web.ProblemDetailsForError(err, "Error creating new account"), err) - return + if errors.Is(err, berrors.RateLimit) { + wfe.sendError(response, logEvent, probs.RateLimited(err.Error()), err) + return + } else { + // Proceed, since we don't want internal rate limit system failures to + // block all account creation. + logEvent.IgnoredRateLimitError = err.Error() + } } - var contacts []string - var contactsPresent bool - if accountCreateRequest.Contact != nil { - contactsPresent = true - contacts = *accountCreateRequest.Contact - } - - // Create corepb.Registration from provided account information - reg := corepb.Registration{ - Contact: contacts, - ContactsPresent: contactsPresent, - Agreement: wfe.SubscriberAgreementURL, - Key: keyBytes, - InitialIP: ipBytes, - } - - // TODO(#5545): Spending and Refunding can be async until these rate limits - // are authoritative. This saves us from adding latency to each request. - // Goroutines spun out below will respect a context deadline set by the - // ratelimits package and cannot be prematurely canceled by the requester. - txns := wfe.newNewAccountLimitTransactions(ip) - go wfe.checkNewAccountLimits(ctx, txns) var newRegistrationSuccessful bool - var errIsRateLimit bool defer func() { - if !newRegistrationSuccessful && !errIsRateLimit { - // This can be a little racy, but we're not going to worry about it - // for now. If the check hasn't completed yet, we can pretty safely - // assume that the refund will be similarly delayed. - go wfe.refundNewAccountLimits(ctx, txns) + if !newRegistrationSuccessful && refundLimits != nil { + go refundLimits() } }() - // Send the registration to the RA via grpc + // Create corepb.Registration from provided account information + reg := corepb.Registration{ + Agreement: wfe.SubscriberAgreementURL, + Key: keyBytes, + } + acctPB, err := wfe.ra.NewRegistration(ctx, ®) if err != nil { - if errors.Is(err, berrors.RateLimit) { - // Request was denied by a legacy rate limit. In this error case we - // do not want to refund the quota consumed by the request because - // repeated requests would result in unearned refunds. - // - // TODO(#5545): Once key-value rate limits are authoritative this - // can be removed. - errIsRateLimit = true - } if errors.Is(err, berrors.Duplicate) { existingAcct, err := wfe.sa.GetRegistrationByKey(ctx, &sapb.JSONWebKey{Jwk: keyBytes}) if err == nil { @@ -857,7 +870,7 @@ func (wfe *WebFrontEndImpl) NewAccount( } registrationValid := func(reg *corepb.Registration) bool { - return !(len(reg.Key) == 0 || len(reg.InitialIP) == 0) && reg.Id != 0 + return !(len(reg.Key) == 0) && reg.Id != 0 } if acctPB == nil || !registrationValid(acctPB) { @@ -891,6 +904,18 @@ func (wfe *WebFrontEndImpl) NewAccount( return } newRegistrationSuccessful = true + + if wfe.ee != nil && len(emails) > 0 { + _, err := wfe.ee.SendContacts(ctx, &emailpb.SendContactsRequest{ + // Note: We are explicitly using the contacts provided by the + // subscriber here. The RA will eventually stop accepting contacts. + Emails: emails, + }) + if err != nil { + wfe.sendError(response, logEvent, probs.ServerInternal("Error sending contacts"), err) + return + } + } } // parseRevocation accepts the payload for a revocation request and parses it @@ -899,7 +924,7 @@ func (wfe *WebFrontEndImpl) NewAccount( // or revocation reason don't pass simple static checks. Also populates some // metadata fields on the given logEvent. func (wfe *WebFrontEndImpl) parseRevocation( - jwsBody []byte, logEvent *web.RequestEvent) (*x509.Certificate, revocation.Reason, *probs.ProblemDetails) { + jwsBody []byte, logEvent *web.RequestEvent) (*x509.Certificate, revocation.Reason, error) { // Read the revoke request from the JWS payload var revokeRequest struct { CertificateDER core.JSONBuffer `json:"certificate"` @@ -907,13 +932,13 @@ func (wfe *WebFrontEndImpl) parseRevocation( } err := json.Unmarshal(jwsBody, &revokeRequest) if err != nil { - return nil, 0, probs.Malformed("Unable to JSON parse revoke request") + return nil, 0, berrors.MalformedError("Unable to JSON parse revoke request") } // Parse the provided certificate parsedCertificate, err := x509.ParseCertificate(revokeRequest.CertificateDER) if err != nil { - return nil, 0, probs.Malformed("Unable to parse certificate DER") + return nil, 0, berrors.MalformedError("Unable to parse certificate DER") } // Compute and record the serial number of the provided certificate @@ -927,31 +952,23 @@ func (wfe *WebFrontEndImpl) parseRevocation( // issuer certificate. issuerCert, ok := wfe.issuerCertificates[issuance.IssuerNameID(parsedCertificate)] if !ok || issuerCert == nil { - return nil, 0, probs.NotFound("Certificate from unrecognized issuer") + return nil, 0, berrors.NotFoundError("Certificate from unrecognized issuer") } err = parsedCertificate.CheckSignatureFrom(issuerCert.Certificate) if err != nil { - return nil, 0, probs.NotFound("No such certificate") + return nil, 0, berrors.NotFoundError("No such certificate") } - logEvent.DNSNames = parsedCertificate.DNSNames + logEvent.Identifiers = identifier.FromCert(parsedCertificate) if parsedCertificate.NotAfter.Before(wfe.clk.Now()) { - return nil, 0, probs.Unauthorized("Certificate is expired") + return nil, 0, berrors.UnauthorizedError("Certificate is expired") } // Verify the revocation reason supplied is allowed reason := revocation.Reason(0) if revokeRequest.Reason != nil { if _, present := revocation.UserAllowedReasons[*revokeRequest.Reason]; !present { - reasonStr, ok := revocation.ReasonToString[*revokeRequest.Reason] - if !ok { - reasonStr = "unknown" - } - return nil, 0, probs.BadRevocationReason( - "unsupported revocation reason code provided: %s (%d). Supported reasons: %s", - reasonStr, - *revokeRequest.Reason, - revocation.UserAllowedReasonsMessage) + return nil, 0, berrors.BadRevocationReasonError(int64(*revokeRequest.Reason)) } reason = *revokeRequest.Reason } @@ -975,14 +992,14 @@ func (wfe *WebFrontEndImpl) revokeCertBySubscriberKey( logEvent *web.RequestEvent) error { // For Key ID revocations we authenticate the outer JWS by using // `validJWSForAccount` similar to other WFE endpoints - jwsBody, _, acct, prob := wfe.validJWSForAccount(outerJWS, request, ctx, logEvent) - if prob != nil { - return prob + jwsBody, _, acct, err := wfe.validJWSForAccount(outerJWS, request, ctx, logEvent) + if err != nil { + return err } - cert, reason, prob := wfe.parseRevocation(jwsBody, logEvent) - if prob != nil { - return prob + cert, reason, err := wfe.parseRevocation(jwsBody, logEvent) + if err != nil { + return err } wfe.log.AuditObject("Authenticated revocation", revocationEvidence{ @@ -995,7 +1012,7 @@ func (wfe *WebFrontEndImpl) revokeCertBySubscriberKey( // The RA will confirm that the authenticated account either originally // issued the certificate, or has demonstrated control over all identifiers // in the certificate. - _, err := wfe.ra.RevokeCertByApplicant(ctx, &rapb.RevokeCertByApplicantRequest{ + _, err = wfe.ra.RevokeCertByApplicant(ctx, &rapb.RevokeCertByApplicantRequest{ Cert: cert.Raw, Code: int64(reason), RegID: acct.ID, @@ -1025,16 +1042,16 @@ func (wfe *WebFrontEndImpl) revokeCertByCertKey( return prob } - cert, reason, prob := wfe.parseRevocation(jwsBody, logEvent) - if prob != nil { - return prob + cert, reason, err := wfe.parseRevocation(jwsBody, logEvent) + if err != nil { + return err } // For embedded JWK revocations we decide if a requester is able to revoke a specific // certificate by checking that to-be-revoked certificate has the same public // key as the JWK that was used to authenticate the request if !core.KeyDigestEquals(jwk, cert.PublicKey) { - return probs.Unauthorized( + return berrors.UnauthorizedError( "JWK embedded in revocation request must be the same public key as the cert to be revoked") } @@ -1047,7 +1064,7 @@ func (wfe *WebFrontEndImpl) revokeCertByCertKey( // The RA assumes here that the WFE2 has validated the JWS as proving // control of the private key corresponding to this certificate. - _, err := wfe.ra.RevokeCertByKey(ctx, &rapb.RevokeCertByKeyRequest{ + _, err = wfe.ra.RevokeCertByKey(ctx, &rapb.RevokeCertByKeyRequest{ Cert: cert.Raw, }) if err != nil { @@ -1074,22 +1091,21 @@ func (wfe *WebFrontEndImpl) RevokeCertificate( // certificates are authorized to be revoked by the requester // Parse the JWS from the HTTP Request - jws, prob := wfe.parseJWSRequest(request) - if prob != nil { - wfe.sendError(response, logEvent, prob, nil) + jws, err := wfe.parseJWSRequest(request) + if err != nil { + wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "Unable to validate JWS"), err) return } // Figure out which type of authentication this JWS uses - authType, prob := checkJWSAuthType(jws.Signatures[0].Header) - if prob != nil { - wfe.sendError(response, logEvent, prob, nil) + authType, err := checkJWSAuthType(jws.Signatures[0].Header) + if err != nil { + wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "Unable to validate JWS"), err) return } // Handle the revocation request according to how it is authenticated, or if // the authentication type is unknown, error immediately - var err error switch authType { case embeddedKeyID: err = wfe.revokeCertBySubscriberKey(ctx, jws, request, logEvent) @@ -1099,38 +1115,45 @@ func (wfe *WebFrontEndImpl) RevokeCertificate( err = berrors.MalformedError("Malformed JWS, no KeyID or embedded JWK") } if err != nil { - wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "unable to revoke"), nil) + wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "Unable to revoke"), err) return } response.WriteHeader(http.StatusOK) } -// Challenge handles POST requests to challenge URLs. -// Such requests are clients' responses to the server's challenges. -func (wfe *WebFrontEndImpl) Challenge( +// ChallengeHandler handles POST requests to challenge URLs of the form /acme/chall/{regID}/{authzID}/{challID}. +func (wfe *WebFrontEndImpl) ChallengeHandler( ctx context.Context, logEvent *web.RequestEvent, response http.ResponseWriter, request *http.Request) { - notFound := func() { - wfe.sendError(response, logEvent, probs.NotFound("No such challenge"), nil) - } slug := strings.Split(request.URL.Path, "/") - if len(slug) != 2 { - notFound() + if len(slug) != 3 { + wfe.sendError(response, logEvent, probs.NotFound("No such challenge"), nil) return } - authorizationID, err := strconv.ParseInt(slug[0], 10, 64) + // TODO(#7683): the regID is currently ignored. + wfe.Challenge(ctx, logEvent, response, request, slug[1], slug[2]) +} + +// Challenge handles POSTS to both formats of challenge URLs. +func (wfe *WebFrontEndImpl) Challenge( + ctx context.Context, + logEvent *web.RequestEvent, + response http.ResponseWriter, + request *http.Request, + authorizationIDStr string, + challengeID string) { + authorizationID, err := strconv.ParseInt(authorizationIDStr, 10, 64) if err != nil { wfe.sendError(response, logEvent, probs.Malformed("Invalid authorization ID"), nil) return } - challengeID := slug[1] - authzPB, err := wfe.sa.GetAuthorization2(ctx, &sapb.AuthorizationID2{Id: authorizationID}) + authzPB, err := wfe.ra.GetAuthorization(ctx, &rapb.GetAuthorizationRequest{Id: authorizationID}) if err != nil { if errors.Is(err, berrors.NotFound) { - notFound() + wfe.sendError(response, logEvent, probs.NotFound("No such challenge"), nil) } else { wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "Problem getting authorization"), err) } @@ -1138,8 +1161,7 @@ func (wfe *WebFrontEndImpl) Challenge( } // Ensure gRPC response is complete. - // TODO(#7153): Check each value via core.IsAnyNilOrZero - if authzPB.Id == "" || authzPB.Identifier == "" || authzPB.Status == "" || core.IsAnyNilOrZero(authzPB.Expires) { + if core.IsAnyNilOrZero(authzPB.Id, authzPB.Identifier, authzPB.Status, authzPB.Expires) { wfe.sendError(response, logEvent, probs.ServerInternal("Problem getting authorization"), errIncompleteGRPCResponse) return } @@ -1151,7 +1173,7 @@ func (wfe *WebFrontEndImpl) Challenge( } challengeIndex := authz.FindChallengeByStringID(challengeID) if challengeIndex == -1 { - notFound() + wfe.sendError(response, logEvent, probs.NotFound("No such challenge"), nil) return } @@ -1160,16 +1182,7 @@ func (wfe *WebFrontEndImpl) Challenge( return } - if requiredStale(request, logEvent) { - if prob := wfe.staleEnoughToGETAuthz(authzPB); prob != nil { - wfe.sendError(response, logEvent, prob, nil) - return - } - } - - if authz.Identifier.Type == identifier.DNS { - logEvent.DNSName = authz.Identifier.Value - } + logEvent.Identifiers = identifier.ACMEIdentifiers{authz.Identifier} logEvent.Status = string(authz.Status) challenge := authz.Challenges[challengeIndex] @@ -1204,12 +1217,13 @@ func prepAccountForDisplay(acct *core.Registration) { // prepChallengeForDisplay takes a core.Challenge and prepares it for display to // the client by filling in its URL field and clearing several unnecessary // fields. -func (wfe *WebFrontEndImpl) prepChallengeForDisplay(request *http.Request, authz core.Authorization, challenge *core.Challenge) { +func (wfe *WebFrontEndImpl) prepChallengeForDisplay( + request *http.Request, + authz core.Authorization, + challenge *core.Challenge, +) { // Update the challenge URL to be relative to the HTTP request Host - challenge.URL = web.RelativeEndpoint(request, fmt.Sprintf("%s%s/%s", challengePath, authz.ID, challenge.StringID())) - - // ACMEv2 never sends the KeyAuthorization back in a challenge object. - challenge.ProvidedKeyAuthorization = "" + challenge.URL = web.RelativeEndpoint(request, fmt.Sprintf("%s%d/%s/%s", challengePath, authz.RegistrationID, authz.ID, challenge.StringID())) // Internally, we store challenge error problems with just the short form // (e.g. "CAA") of the problem type. But for external display, we need to @@ -1231,14 +1245,16 @@ func (wfe *WebFrontEndImpl) prepChallengeForDisplay(request *http.Request, authz } // prepAuthorizationForDisplay takes a core.Authorization and prepares it for -// display to the client by clearing its ID and RegistrationID fields, and -// preparing all its challenges. +// display to the client by preparing all its challenges. func (wfe *WebFrontEndImpl) prepAuthorizationForDisplay(request *http.Request, authz *core.Authorization) { for i := range authz.Challenges { wfe.prepChallengeForDisplay(request, *authz, &authz.Challenges[i]) } - authz.ID = "" - authz.RegistrationID = 0 + + // Shuffle the challenges so no one relies on their order. + rand.Shuffle(len(authz.Challenges), func(i, j int) { + authz.Challenges[i], authz.Challenges[j] = authz.Challenges[j], authz.Challenges[i] + }) // The ACME spec forbids allowing "*" in authorization identifiers. Boulder // allows this internally as a means of tracking when an authorization @@ -1259,7 +1275,6 @@ func (wfe *WebFrontEndImpl) getChallenge( authz core.Authorization, challenge *core.Challenge, logEvent *web.RequestEvent) { - wfe.prepChallengeForDisplay(request, authz, challenge) authzURL := urlForAuthz(authz, request) @@ -1282,11 +1297,11 @@ func (wfe *WebFrontEndImpl) postChallenge( authz core.Authorization, challengeIndex int, logEvent *web.RequestEvent) { - body, _, currAcct, prob := wfe.validPOSTForAccount(request, ctx, logEvent) + body, _, currAcct, err := wfe.validPOSTForAccount(request, ctx, logEvent) addRequesterHeader(response, logEvent.Requester) - if prob != nil { + if err != nil { // validPOSTForAccount handles its own setting of logEvent.Errors - wfe.sendError(response, logEvent, prob, nil) + wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "Unable to validate JWS"), err) return } @@ -1339,8 +1354,7 @@ func (wfe *WebFrontEndImpl) postChallenge( Authz: authzPB, ChallengeIndex: int64(challengeIndex), }) - // TODO(#7153): Check each value via core.IsAnyNilOrZero - if err != nil || authzPB == nil || authzPB.Id == "" || authzPB.Identifier == "" || authzPB.Status == "" || core.IsAnyNilOrZero(authzPB.Expires) { + if err != nil || core.IsAnyNilOrZero(authzPB, authzPB.Id, authzPB.Identifier, authzPB.Status, authzPB.Expires) { wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "Unable to update challenge"), err) return } @@ -1361,7 +1375,7 @@ func (wfe *WebFrontEndImpl) postChallenge( response.Header().Add("Location", challenge.URL) response.Header().Add("Link", link(authzURL, "up")) - err := wfe.writeJsonResponse(response, logEvent, http.StatusOK, challenge) + err = wfe.writeJsonResponse(response, logEvent, http.StatusOK, challenge) if err != nil { // ServerInternal because we made the challenges, they should be OK wfe.sendError(response, logEvent, probs.ServerInternal("Failed to marshal challenge"), err) @@ -1375,11 +1389,11 @@ func (wfe *WebFrontEndImpl) Account( logEvent *web.RequestEvent, response http.ResponseWriter, request *http.Request) { - body, _, currAcct, prob := wfe.validPOSTForAccount(request, ctx, logEvent) + body, _, currAcct, err := wfe.validPOSTForAccount(request, ctx, logEvent) addRequesterHeader(response, logEvent.Requester) - if prob != nil { + if err != nil { // validPOSTForAccount handles its own setting of logEvent.Errors - wfe.sendError(response, logEvent, prob, nil) + wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "Unable to validate JWS"), err) return } @@ -1388,23 +1402,26 @@ func (wfe *WebFrontEndImpl) Account( idStr := request.URL.Path id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { - wfe.sendError(response, logEvent, probs.Malformed("Account ID must be an integer"), err) + wfe.sendError(response, logEvent, probs.Malformed(fmt.Sprintf("Account ID must be an integer, was %q", idStr)), err) return } else if id <= 0 { - msg := fmt.Sprintf("Account ID must be a positive non-zero integer, was %d", id) - wfe.sendError(response, logEvent, probs.Malformed(msg), nil) + wfe.sendError(response, logEvent, probs.Malformed(fmt.Sprintf("Account ID must be a positive non-zero integer, was %d", id)), nil) return } else if id != currAcct.ID { - wfe.sendError(response, logEvent, - probs.Unauthorized("Request signing key did not match account key"), nil) + wfe.sendError(response, logEvent, probs.Unauthorized("Request signing key did not match account key"), nil) return } - // If the body was not empty, then this is an account update request. - if string(body) != "" { - currAcct, prob = wfe.updateAccount(ctx, body, currAcct) - if prob != nil { - wfe.sendError(response, logEvent, prob, nil) + var acct *core.Registration + if string(body) == "" || string(body) == "{}" { + // An empty string means POST-as-GET (i.e. no update). A body of "{}" means + // an update of zero fields, returning the unchanged object. This was the + // recommended way to fetch the account object in ACMEv1. + acct = currAcct + } else { + acct, err = wfe.updateAccount(ctx, body, currAcct) + if err != nil { + wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "Unable to update account"), nil) return } } @@ -1413,99 +1430,55 @@ func (wfe *WebFrontEndImpl) Account( response.Header().Add("Link", link(wfe.SubscriberAgreementURL, "terms-of-service")) } - prepAccountForDisplay(currAcct) + prepAccountForDisplay(acct) - err = wfe.writeJsonResponse(response, logEvent, http.StatusOK, currAcct) + err = wfe.writeJsonResponse(response, logEvent, http.StatusOK, acct) if err != nil { - // ServerInternal because we just generated the account, it should be OK - wfe.sendError(response, logEvent, - probs.ServerInternal("Failed to marshal account"), err) + wfe.sendError(response, logEvent, probs.ServerInternal("Failed to marshal account"), err) return } } // updateAccount unmarshals an account update request from the provided // requestBody to update the given registration. Important: It is assumed the -// request has already been authenticated by the caller. If the request is -// a valid update the resulting updated account is returned, otherwise a problem +// request has already been authenticated by the caller. If the request is a +// valid update the resulting updated account is returned, otherwise a problem // is returned. -func (wfe *WebFrontEndImpl) updateAccount( - ctx context.Context, - requestBody []byte, - currAcct *core.Registration) (*core.Registration, *probs.ProblemDetails) { - // Only the Contact and Status fields of an account may be updated this way. +func (wfe *WebFrontEndImpl) updateAccount(ctx context.Context, requestBody []byte, currAcct *core.Registration) (*core.Registration, error) { + // Only the Status field of an account may be updated this way. // For key updates clients should be using the key change endpoint. var accountUpdateRequest struct { - Contact *[]string `json:"contact"` - Status core.AcmeStatus `json:"status"` + Status core.AcmeStatus `json:"status"` } err := json.Unmarshal(requestBody, &accountUpdateRequest) if err != nil { - return nil, probs.Malformed("Error unmarshaling account") + return nil, berrors.MalformedError("parsing account update request: %s", err) } - // Convert existing account to corepb.Registration - basePb, err := bgrpc.RegistrationToPB(*currAcct) - if err != nil { - return nil, probs.ServerInternal("Error updating account") - } - - var contacts []string - var contactsPresent bool - if accountUpdateRequest.Contact != nil { - contactsPresent = true - contacts = *accountUpdateRequest.Contact - } - - // Copy over the fields from the request to the registration object used for - // the RA updates. - // Create corepb.Registration from provided account information - updatePb := &corepb.Registration{ - Contact: contacts, - ContactsPresent: contactsPresent, - Status: string(accountUpdateRequest.Status), - } - - // People *will* POST their full accounts to this endpoint, including - // the 'valid' status, to avoid always failing out when that happens only - // attempt to deactivate if the provided status is different from their current - // status. - // - // If a user tries to send both a deactivation request and an update to their - // contacts or subscriber agreement URL the deactivation will take place and - // return before an update would be performed. - if updatePb.Status != "" && updatePb.Status != basePb.Status { - if updatePb.Status != string(core.StatusDeactivated) { - return nil, probs.Malformed("Invalid value provided for status field") - } - _, err := wfe.ra.DeactivateRegistration(ctx, basePb) - if err != nil { - return nil, web.ProblemDetailsForError(err, "Unable to deactivate account") - } - currAcct.Status = core.StatusDeactivated + switch accountUpdateRequest.Status { + case core.StatusValid, "": + // They probably intended to update their contact address, but we don't do + // that anymore, so simply return their account as-is. We don't error out + // here because it would break too many clients. return currAcct, nil + + case core.StatusDeactivated: + updatedAcct, err := wfe.ra.DeactivateRegistration( + ctx, &rapb.DeactivateRegistrationRequest{RegistrationID: currAcct.ID}) + if err != nil { + return nil, fmt.Errorf("deactivating account: %w", err) + } + + updatedReg, err := bgrpc.PbToRegistration(updatedAcct) + if err != nil { + return nil, fmt.Errorf("parsing deactivated account: %w", err) + } + return &updatedReg, nil + + default: + return nil, berrors.MalformedError("invalid status %q for account update request, must be %q or %q", accountUpdateRequest.Status, core.StatusValid, core.StatusDeactivated) } - - // Account objects contain a JWK object which are merged in UpdateRegistration - // if it is different from the existing account key. Since this isn't how you - // update the key we just copy the existing one into the update object here. This - // ensures the key isn't changed and that we can cleanly serialize the update as - // JSON to send via RPC to the RA. - updatePb.Key = basePb.Key - - updatedAcct, err := wfe.ra.UpdateRegistration(ctx, &rapb.UpdateRegistrationRequest{Base: basePb, Update: updatePb}) - if err != nil { - return nil, web.ProblemDetailsForError(err, "Unable to update account") - } - - // Convert proto to core.Registration for return - updatedReg, err := bgrpc.PbToRegistration(updatedAcct) - if err != nil { - return nil, probs.ServerInternal("Error updating account") - } - - return &updatedReg, nil } // deactivateAuthorization processes the given JWS POST body as a request to @@ -1543,11 +1516,29 @@ func (wfe *WebFrontEndImpl) deactivateAuthorization( return true } -func (wfe *WebFrontEndImpl) Authorization( +// AuthorizationHandler handles requests to authorization URLs of the form /acme/authz/{regID}/{authzID}. +func (wfe *WebFrontEndImpl) AuthorizationHandler( ctx context.Context, logEvent *web.RequestEvent, response http.ResponseWriter, request *http.Request) { + slug := strings.Split(request.URL.Path, "/") + if len(slug) != 2 { + wfe.sendError(response, logEvent, probs.NotFound("No such authorization"), nil) + return + } + // TODO(#7683): The regID is currently ignored. + wfe.Authorization(ctx, logEvent, response, request, slug[1]) +} + +// Authorization handles both `/acme/authz/{authzID}` and `/acme/authz/{regID}/{authzID}` requests, +// after the calling function has parsed out the authzID. +func (wfe *WebFrontEndImpl) Authorization( + ctx context.Context, + logEvent *web.RequestEvent, + response http.ResponseWriter, + request *http.Request, + authzIDStr string) { var requestAccount *core.Registration var requestBody []byte // If the request is a POST it is either: @@ -1555,23 +1546,23 @@ func (wfe *WebFrontEndImpl) Authorization( // B) a POST-as-GET to query the authorization details if request.Method == "POST" { // Both POST options need to be authenticated by an account - body, _, acct, prob := wfe.validPOSTForAccount(request, ctx, logEvent) + body, _, acct, err := wfe.validPOSTForAccount(request, ctx, logEvent) addRequesterHeader(response, logEvent.Requester) - if prob != nil { - wfe.sendError(response, logEvent, prob, nil) + if err != nil { + wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "Unable to validate JWS"), err) return } requestAccount = acct requestBody = body } - authzID, err := strconv.ParseInt(request.URL.Path, 10, 64) + authzID, err := strconv.ParseInt(authzIDStr, 10, 64) if err != nil { wfe.sendError(response, logEvent, probs.Malformed("Invalid authorization ID"), nil) return } - authzPB, err := wfe.sa.GetAuthorization2(ctx, &sapb.AuthorizationID2{Id: authzID}) + authzPB, err := wfe.ra.GetAuthorization(ctx, &rapb.GetAuthorizationRequest{Id: authzID}) if errors.Is(err, berrors.NotFound) { wfe.sendError(response, logEvent, probs.NotFound("No such authorization"), nil) return @@ -1583,16 +1574,15 @@ func (wfe *WebFrontEndImpl) Authorization( return } + ident := identifier.FromProto(authzPB.Identifier) + // Ensure gRPC response is complete. - // TODO(#7153): Check each value via core.IsAnyNilOrZero - if authzPB.Id == "" || authzPB.Identifier == "" || authzPB.Status == "" || core.IsAnyNilOrZero(authzPB.Expires) { + if core.IsAnyNilOrZero(authzPB.Id, ident, authzPB.Status, authzPB.Expires) { wfe.sendError(response, logEvent, probs.ServerInternal("Problem getting authorization"), errIncompleteGRPCResponse) return } - if identifier.IdentifierType(authzPB.Identifier) == identifier.DNS { - logEvent.DNSName = authzPB.Identifier - } + logEvent.Identifiers = identifier.ACMEIdentifiers{ident} logEvent.Status = authzPB.Status // After expiring, authorizations are inaccessible @@ -1601,13 +1591,6 @@ func (wfe *WebFrontEndImpl) Authorization( return } - if requiredStale(request, logEvent) { - if prob := wfe.staleEnoughToGETAuthz(authzPB); prob != nil { - wfe.sendError(response, logEvent, prob, nil) - return - } - } - // If this was a POST that has an associated requestAccount and that account // doesn't own the authorization, abort before trying to deactivate the authz // or return its details @@ -1651,9 +1634,9 @@ func (wfe *WebFrontEndImpl) Certificate(ctx context.Context, logEvent *web.Reque // Any POSTs to the Certificate endpoint should be POST-as-GET requests. There are // no POSTs with a body allowed for this endpoint. if request.Method == "POST" { - acct, prob := wfe.validPOSTAsGETForAccount(request, ctx, logEvent) - if prob != nil { - wfe.sendError(response, logEvent, prob, nil) + acct, err := wfe.validPOSTAsGETForAccount(request, ctx, logEvent) + if err != nil { + wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "Unable to validate JWS"), err) return } requesterAccount = acct @@ -1700,11 +1683,13 @@ func (wfe *WebFrontEndImpl) Certificate(ctx context.Context, logEvent *web.Reque return } - if requiredStale(request, logEvent) { - if prob := wfe.staleEnoughToGETCert(cert); prob != nil { - wfe.sendError(response, logEvent, prob, nil) - return - } + // Don't serve certificates from the /get/ path until they're a little stale, + // to prevent ACME clients from using that path. + if strings.HasPrefix(logEvent.Endpoint, getCertPath) && wfe.clk.Since(cert.Issued.AsTime()) < wfe.staleTimeout { + wfe.sendError(response, logEvent, probs.Unauthorized(fmt.Sprintf( + "Certificate is too new for GET API. You should only use this non-standard API to access resources created more than %s ago", + wfe.staleTimeout)), nil) + return } // If there was a requesterAccount (e.g. because it was a POST-as-GET request) @@ -1880,25 +1865,25 @@ func (wfe *WebFrontEndImpl) KeyRollover( request *http.Request) { // Validate the outer JWS on the key rollover in standard fashion using // validPOSTForAccount - outerBody, outerJWS, acct, prob := wfe.validPOSTForAccount(request, ctx, logEvent) + outerBody, outerJWS, acct, err := wfe.validPOSTForAccount(request, ctx, logEvent) addRequesterHeader(response, logEvent.Requester) - if prob != nil { - wfe.sendError(response, logEvent, prob, nil) + if err != nil { + wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "Unable to validate JWS"), err) return } oldKey := acct.Key // Parse the inner JWS from the validated outer JWS body - innerJWS, prob := wfe.parseJWS(outerBody) - if prob != nil { - wfe.sendError(response, logEvent, prob, nil) + innerJWS, err := wfe.parseJWS(outerBody) + if err != nil { + wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "Unable to validate JWS"), err) return } // Validate the inner JWS as a key rollover request for the outer JWS - rolloverOperation, prob := wfe.validKeyRollover(ctx, outerJWS, innerJWS, oldKey) - if prob != nil { - wfe.sendError(response, logEvent, prob, nil) + rolloverOperation, err := wfe.validKeyRollover(ctx, outerJWS, innerJWS, oldKey) + if err != nil { + wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "Unable to validate JWS"), err) return } newKey := rolloverOperation.NewKey @@ -1949,18 +1934,9 @@ func (wfe *WebFrontEndImpl) KeyRollover( wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "Failed to lookup existing keys"), err) return } - // Convert account to proto for grpc - regPb, err := bgrpc.RegistrationToPB(*acct) - if err != nil { - wfe.sendError(response, logEvent, probs.ServerInternal("Error marshaling Registration to proto"), err) - return - } - - // Copy new key into an empty registration to provide as the update - updatePb := &corepb.Registration{Key: newKeyBytes} // Update the account key to the new key - updatedAcctPb, err := wfe.ra.UpdateRegistration(ctx, &rapb.UpdateRegistrationRequest{Base: regPb, Update: updatePb}) + updatedAcctPb, err := wfe.ra.UpdateRegistrationKey(ctx, &rapb.UpdateRegistrationKeyRequest{RegistrationID: acct.ID, Jwk: newKeyBytes}) if err != nil { if errors.Is(err, berrors.Duplicate) { // It is possible that between checking for the existing key, and performing the update @@ -1997,32 +1973,31 @@ func (wfe *WebFrontEndImpl) KeyRollover( } type orderJSON struct { - Status core.AcmeStatus `json:"status"` - Expires time.Time `json:"expires"` - Identifiers []identifier.ACMEIdentifier `json:"identifiers"` - Authorizations []string `json:"authorizations"` - Finalize string `json:"finalize"` - Profile string `json:"profile,omitempty"` - Certificate string `json:"certificate,omitempty"` - Error *probs.ProblemDetails `json:"error,omitempty"` + Status core.AcmeStatus `json:"status"` + Expires time.Time `json:"expires"` + Identifiers identifier.ACMEIdentifiers `json:"identifiers"` + Authorizations []string `json:"authorizations"` + Finalize string `json:"finalize"` + Profile string `json:"profile,omitempty"` + Certificate string `json:"certificate,omitempty"` + Error *probs.ProblemDetails `json:"error,omitempty"` + Replaces string `json:"replaces,omitempty"` } // orderToOrderJSON converts a *corepb.Order instance into an orderJSON struct // that is returned in HTTP API responses. It will convert the order names to // DNS type identifiers and additionally create absolute URLs for the finalize -// URL and the ceritificate URL as appropriate. +// URL and the certificate URL as appropriate. func (wfe *WebFrontEndImpl) orderToOrderJSON(request *http.Request, order *corepb.Order) orderJSON { - idents := make([]identifier.ACMEIdentifier, len(order.Names)) - for i, name := range order.Names { - idents[i] = identifier.ACMEIdentifier{Type: identifier.DNS, Value: name} - } finalizeURL := web.RelativeEndpoint(request, fmt.Sprintf("%s%d/%d", finalizeOrderPath, order.RegistrationID, order.Id)) respObj := orderJSON{ Status: core.AcmeStatus(order.Status), Expires: order.Expires.AsTime(), - Identifiers: idents, + Identifiers: identifier.FromProtoSlice(order.Identifiers), Finalize: finalizeURL, + Profile: order.CertificateProfileName, + Replaces: order.Replaces, } // If there is an order error, prefix its type with the V2 namespace if order.Error != nil { @@ -2035,7 +2010,7 @@ func (wfe *WebFrontEndImpl) orderToOrderJSON(request *http.Request, order *corep respObj.Error.Type = probs.ErrorNS + respObj.Error.Type } for _, v2ID := range order.V2Authorizations { - respObj.Authorizations = append(respObj.Authorizations, web.RelativeEndpoint(request, fmt.Sprintf("%s%d", authzPath, v2ID))) + respObj.Authorizations = append(respObj.Authorizations, web.RelativeEndpoint(request, fmt.Sprintf("%s%d/%d", authzPath, order.RegistrationID, v2ID))) } if respObj.Status == core.StatusValid { certURL := web.RelativeEndpoint(request, @@ -2045,93 +2020,45 @@ func (wfe *WebFrontEndImpl) orderToOrderJSON(request *http.Request, order *corep return respObj } -// newNewOrderLimitTransactions constructs a set of rate limit transactions to -// evaluate for a new-order request. -// -// Precondition: names must be a list of DNS names that all pass -// policy.WellFormedDomainNames. -func (wfe *WebFrontEndImpl) newNewOrderLimitTransactions(regId int64, names []string) []ratelimits.Transaction { - if wfe.limiter == nil && wfe.txnBuilder == nil { - // Limiter is disabled. - return nil - } - - logTxnErr := func(err error, limit ratelimits.Name) { - // TODO(#5545): Once key-value rate limits are authoritative this log - // line should be removed in favor of returning the error. - wfe.log.Infof("error constructing rate limit transaction for %s rate limit: %s", limit, err) - } - - var transactions []ratelimits.Transaction - txn, err := wfe.txnBuilder.OrdersPerAccountTransaction(regId) - if err != nil { - logTxnErr(err, ratelimits.NewOrdersPerAccount) - return nil - } - transactions = append(transactions, txn) - - failedAuthzTxns, err := wfe.txnBuilder.FailedAuthorizationsPerDomainPerAccountCheckOnlyTransactions(regId, names, wfe.maxNames) - if err != nil { - logTxnErr(err, ratelimits.FailedAuthorizationsPerDomainPerAccount) - return nil - } - transactions = append(transactions, failedAuthzTxns...) - - certsPerDomainTxns, err := wfe.txnBuilder.CertificatesPerDomainTransactions(regId, names, wfe.maxNames) - if err != nil { - logTxnErr(err, ratelimits.CertificatesPerDomain) - return nil - } - transactions = append(transactions, certsPerDomainTxns...) - - txn, err = wfe.txnBuilder.CertificatesPerFQDNSetTransaction(names) - if err != nil { - logTxnErr(err, ratelimits.CertificatesPerFQDNSet) - return nil - } - return append(transactions, txn) -} - // checkNewOrderLimits checks whether sufficient limit quota exists for the // creation of a new order. If so, that quota is spent. If an error is -// encountered during the check, it is logged but not returned. +// encountered during the check, it is logged but not returned. A refund +// function is returned that can be used to refund the quota if the order is not +// created, the func will be nil if any error was encountered during the check. // -// TODO(#5545): For now we're simply exercising the new rate limiter codepath. -// This should eventually return a berrors.RateLimit error containing the retry -// after duration among other information available in the ratelimits.Decision. -func (wfe *WebFrontEndImpl) checkNewOrderLimits(ctx context.Context, transactions []ratelimits.Transaction) { - if wfe.limiter == nil && wfe.txnBuilder == nil { - // Limiter is disabled. - return +// Precondition: idents must be a list of identifiers that all pass +// policy.WellFormedIdentifiers. +func (wfe *WebFrontEndImpl) checkNewOrderLimits(ctx context.Context, regId int64, idents identifier.ACMEIdentifiers, isRenewal bool) (func(), error) { + txns, err := wfe.txnBuilder.NewOrderLimitTransactions(regId, idents, isRenewal) + if err != nil { + return nil, fmt.Errorf("building new order limit transactions: %w", err) } - _, err := wfe.limiter.BatchSpend(ctx, transactions) + d, err := wfe.limiter.BatchSpend(ctx, txns) if err != nil { - if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { - return + return nil, fmt.Errorf("spending new order limits: %w", err) + } + + err = d.Result(wfe.clk.Now()) + if err != nil { + return nil, err + } + + return func() { + _, err := wfe.limiter.BatchRefund(ctx, txns) + if err != nil { + wfe.log.Warningf("refunding new order limits: %s", err) } - wfe.log.Errf("checking newOrder limits: %s", err) - } -} - -func (wfe *WebFrontEndImpl) refundNewOrderLimits(ctx context.Context, transactions []ratelimits.Transaction) { - if wfe.limiter == nil || wfe.txnBuilder == nil { - return - } - - _, err := wfe.limiter.BatchRefund(ctx, transactions) - if err != nil { - wfe.log.Errf("refunding newOrder limits: %s", err) - } + }, nil } // orderMatchesReplacement checks if the order matches the provided certificate // as identified by the provided ARI CertID. This function ensures that: // - the certificate being replaced exists, // - the requesting account owns that certificate, and -// - a name in this new order matches a name in the certificate being +// - an identifier in this new order matches an identifier in the certificate being // replaced. -func (wfe *WebFrontEndImpl) orderMatchesReplacement(ctx context.Context, acct *core.Registration, names []string, serial string) error { +func (wfe *WebFrontEndImpl) orderMatchesReplacement(ctx context.Context, acct *core.Registration, idents identifier.ACMEIdentifiers, serial string) error { // It's okay to use GetCertificate (vs trying to get a precertificate), // because we don't intend to serve ARI for certs that never made it past // the precert stage. @@ -2151,17 +2078,17 @@ func (wfe *WebFrontEndImpl) orderMatchesReplacement(ctx context.Context, acct *c return fmt.Errorf("error parsing certificate replaced by this order: %w", err) } - var nameMatch bool - for _, name := range names { - if parsedCert.VerifyHostname(name) == nil { - // At least one name in the new order matches a name in the - // predecessor certificate. - nameMatch = true + var identMatch bool + for _, ident := range idents { + if parsedCert.VerifyHostname(ident.Value) == nil { + // At least one identifier in the new order matches an identifier in + // the predecessor certificate. + identMatch = true break } } - if !nameMatch { - return berrors.MalformedError("identifiers in this order do not match any names in the certificate being replaced") + if !identMatch { + return berrors.MalformedError("identifiers in this order do not match any identifiers in the certificate being replaced") } return nil } @@ -2174,8 +2101,15 @@ func (wfe *WebFrontEndImpl) determineARIWindow(ctx context.Context, serial strin } if len(result.Incidents) > 0 { + // Find the earliest incident. + var earliest *sapb.Incident + for _, incident := range result.Incidents { + if earliest == nil || incident.RenewBy.AsTime().Before(earliest.RenewBy.AsTime()) { + earliest = incident + } + } // The existing cert is impacted by an incident, renew immediately. - return core.RenewalInfoImmediate(wfe.clk.Now()), nil + return core.RenewalInfoImmediate(wfe.clk.Now(), earliest.Url), nil } // Check if the serial is revoked. @@ -2186,7 +2120,7 @@ func (wfe *WebFrontEndImpl) determineARIWindow(ctx context.Context, serial strin if status.Status == string(core.OCSPStatusRevoked) { // The existing certificate is revoked, renew immediately. - return core.RenewalInfoImmediate(wfe.clk.Now()), nil + return core.RenewalInfoImmediate(wfe.clk.Now(), ""), nil } // It's okay to use GetCertificate (vs trying to get a precertificate), @@ -2220,7 +2154,7 @@ func (wfe *WebFrontEndImpl) determineARIWindow(ctx context.Context, serial strin // Otherwise, this value is false. // - The last value is an error, this is non-nil unless the order is not a // replacement or there was an error while validating the replacement. -func (wfe *WebFrontEndImpl) validateReplacementOrder(ctx context.Context, acct *core.Registration, names []string, replaces string) (string, bool, error) { +func (wfe *WebFrontEndImpl) validateReplacementOrder(ctx context.Context, acct *core.Registration, idents identifier.ACMEIdentifiers, replaces string) (string, bool, error) { if replaces == "" { // No replacement indicated. return "", false, nil @@ -2236,13 +2170,13 @@ func (wfe *WebFrontEndImpl) validateReplacementOrder(ctx context.Context, acct * return "", false, fmt.Errorf("checking replacement status of existing certificate: %w", err) } if exists.Exists { - return "", false, berrors.ConflictError( + return "", false, berrors.AlreadyReplacedError( "cannot indicate an order replaces certificate with serial %q, which already has a replacement order", decodedSerial, ) } - err = wfe.orderMatchesReplacement(ctx, acct, names, decodedSerial) + err = wfe.orderMatchesReplacement(ctx, acct, idents, decodedSerial) if err != nil { // The provided replacement field value failed to meet the required // criteria. We're going to return the error to the caller instead @@ -2267,14 +2201,45 @@ func (wfe *WebFrontEndImpl) validateCertificateProfileName(profile string) error // No profile name is specified. return nil } - if !slices.Contains(wfe.certificateProfileNames, profile) { + if _, ok := wfe.certProfiles[profile]; !ok { // The profile name is not in the list of configured profiles. - return errors.New("not a recognized profile name") + return fmt.Errorf("profile name %q not recognized", profile) } return nil } +func (wfe *WebFrontEndImpl) checkIdentifiersPaused(ctx context.Context, orderIdents identifier.ACMEIdentifiers, regID int64) ([]string, error) { + uniqueOrderIdents := identifier.Normalize(orderIdents) + var idents []*corepb.Identifier + for _, ident := range uniqueOrderIdents { + idents = append(idents, &corepb.Identifier{ + Type: string(ident.Type), + Value: ident.Value, + }) + } + + paused, err := wfe.sa.CheckIdentifiersPaused(ctx, &sapb.PauseRequest{ + RegistrationID: regID, + Identifiers: idents, + }) + if err != nil { + return nil, err + } + if len(paused.Identifiers) <= 0 { + // No identifiers are paused. + return nil, nil + } + + // At least one of the requested identifiers is paused. + pausedValues := make([]string, 0, len(paused.Identifiers)) + for _, ident := range paused.Identifiers { + pausedValues = append(pausedValues, ident.Value) + } + + return pausedValues, nil +} + // NewOrder is used by clients to create a new order object and a set of // authorizations to fulfill for issuance. func (wfe *WebFrontEndImpl) NewOrder( @@ -2282,11 +2247,11 @@ func (wfe *WebFrontEndImpl) NewOrder( logEvent *web.RequestEvent, response http.ResponseWriter, request *http.Request) { - body, _, acct, prob := wfe.validPOSTForAccount(request, ctx, logEvent) + body, _, acct, err := wfe.validPOSTForAccount(request, ctx, logEvent) addRequesterHeader(response, logEvent.Requester) - if prob != nil { + if err != nil { // validPOSTForAccount handles its own setting of logEvent.Errors - wfe.sendError(response, logEvent, prob, nil) + wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "Unable to validate JWS"), err) return } @@ -2294,13 +2259,13 @@ func (wfe *WebFrontEndImpl) NewOrder( // support the identifiers and replaces fields. If notBefore or notAfter are // sent we return a probs.Malformed as we do not support them. var newOrderRequest struct { - Identifiers []identifier.ACMEIdentifier `json:"identifiers"` + Identifiers identifier.ACMEIdentifiers `json:"identifiers"` NotBefore string NotAfter string Replaces string Profile string } - err := json.Unmarshal(body, &newOrderRequest) + err = json.Unmarshal(body, &newOrderRequest) if err != nil { wfe.sendError(response, logEvent, probs.Malformed("Unable to unmarshal NewOrder request body"), err) @@ -2317,103 +2282,119 @@ func (wfe *WebFrontEndImpl) NewOrder( return } - // Collect up all of the DNS identifier values into a []string for - // subsequent layers to process. We reject anything with a non-DNS - // type identifier here. Check to make sure one of the strings is - // short enough to meet the max CN bytes requirement. - names := make([]string, len(newOrderRequest.Identifiers)) - for i, ident := range newOrderRequest.Identifiers { - if ident.Type != identifier.DNS { + idents := newOrderRequest.Identifiers + for _, ident := range idents { + if !ident.Type.IsValid() { wfe.sendError(response, logEvent, - probs.UnsupportedIdentifier("NewOrder request included invalid non-DNS type identifier: type %q, value %q", + probs.UnsupportedIdentifier("NewOrder request included unsupported identifier: type %q, value %q", ident.Type, ident.Value), nil) return } if ident.Value == "" { - wfe.sendError(response, logEvent, probs.Malformed("NewOrder request included empty domain name"), nil) + wfe.sendError(response, logEvent, probs.Malformed("NewOrder request included empty identifier"), nil) return } - names[i] = ident.Value } + idents = identifier.Normalize(idents) + logEvent.Identifiers = idents - names = core.UniqueLowerNames(names) - err = policy.WellFormedDomainNames(names) + err = policy.WellFormedIdentifiers(idents) if err != nil { wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "Invalid identifiers requested"), nil) return } - if len(names) > wfe.maxNames { - wfe.sendError(response, logEvent, probs.Malformed("Order cannot contain more than %d DNS names", wfe.maxNames), nil) + + if features.Get().CheckIdentifiersPaused { + pausedValues, err := wfe.checkIdentifiersPaused(ctx, idents, acct.ID) + if err != nil { + wfe.sendError(response, logEvent, probs.ServerInternal("Failure while checking pause status of identifiers"), err) + return + } + if len(pausedValues) > 0 { + jwt, err := unpause.GenerateJWT(wfe.unpauseSigner, acct.ID, pausedValues, wfe.unpauseJWTLifetime, wfe.clk) + if err != nil { + wfe.sendError(response, logEvent, probs.ServerInternal("Error generating JWT for unpause portal"), err) + } + msg := fmt.Sprintf( + "Your account is temporarily prevented from requesting certificates for %s and possibly others. Please visit: %s", + strings.Join(pausedValues, ", "), + fmt.Sprintf("%s%s?jwt=%s", wfe.unpauseURL, unpause.GetForm, jwt), + ) + wfe.sendError(response, logEvent, probs.Paused(msg), nil) + return + } + } + + var replacesSerial string + var isARIRenewal bool + replacesSerial, isARIRenewal, err = wfe.validateReplacementOrder(ctx, acct, idents, newOrderRequest.Replaces) + if err != nil { + wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "Could not validate ARI 'replaces' field"), err) return } - logEvent.DNSNames = names - - var replaces string - var limitsExempt bool - if features.Get().TrackReplacementCertificatesARI { - replaces, limitsExempt, err = wfe.validateReplacementOrder(ctx, acct, names, newOrderRequest.Replaces) + var isRenewal bool + if !isARIRenewal { + // The Subscriber does not have an ARI exemption. However, we can check + // if the order is a renewal, and thus exempt from the NewOrdersPerAccount + // and CertificatesPerDomain limits. + timestamps, err := wfe.sa.FQDNSetTimestampsForWindow(ctx, &sapb.CountFQDNSetsRequest{ + Identifiers: idents.ToProtoSlice(), + Window: durationpb.New(120 * 24 * time.Hour), + Limit: 1, + }) if err != nil { - wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "While validating order as a replacement an error occurred"), err) + wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "While checking renewal exemption status"), err) return } + isRenewal = len(timestamps.Timestamps) > 0 } err = wfe.validateCertificateProfileName(newOrderRequest.Profile) if err != nil { // TODO(#7392) Provide link to profile documentation. - wfe.sendError(response, logEvent, probs.Malformed("Invalid certificate profile, %q: %s", newOrderRequest.Profile, err), err) + wfe.sendError(response, logEvent, probs.InvalidProfile(err.Error()), err) return } - // TODO(#5545): Spending and Refunding can be async until these rate limits - // are authoritative. This saves us from adding latency to each request. - // Goroutines spun out below will respect a context deadline set by the - // ratelimits package and cannot be prematurely canceled by the requester. - var txns []ratelimits.Transaction - if !limitsExempt { - txns = wfe.newNewOrderLimitTransactions(acct.ID, names) - go wfe.checkNewOrderLimits(ctx, txns) + var refundLimits func() + if !isARIRenewal { + refundLimits, err = wfe.checkNewOrderLimits(ctx, acct.ID, idents, isRenewal) + if err != nil { + if errors.Is(err, berrors.RateLimit) { + wfe.sendError(response, logEvent, probs.RateLimited(err.Error()), err) + return + } else { + // Proceed, since we don't want internal rate limit system failures to + // block all issuance. + logEvent.IgnoredRateLimitError = err.Error() + } + } } var newOrderSuccessful bool - var errIsRateLimit bool defer func() { - if features.Get().TrackReplacementCertificatesARI { - wfe.stats.ariReplacementOrders.With(prometheus.Labels{ - "isReplacement": fmt.Sprintf("%t", replaces != ""), - "limitsExempt": fmt.Sprintf("%t", limitsExempt), - }).Inc() - } + wfe.stats.ariReplacementOrders.With(prometheus.Labels{ + "isReplacement": fmt.Sprintf("%t", replacesSerial != ""), + "limitsExempt": fmt.Sprintf("%t", isARIRenewal), + }).Inc() - if !newOrderSuccessful && !errIsRateLimit { - // This can be a little racy, but we're not going to worry about it - // for now. If the check hasn't completed yet, we can pretty safely - // assume that the refund will be similarly delayed. - go wfe.refundNewOrderLimits(ctx, txns) + if !newOrderSuccessful && refundLimits != nil { + go refundLimits() } }() order, err := wfe.ra.NewOrder(ctx, &rapb.NewOrderRequest{ RegistrationID: acct.ID, - Names: names, - ReplacesSerial: replaces, - LimitsExempt: limitsExempt, + Identifiers: idents.ToProtoSlice(), CertificateProfileName: newOrderRequest.Profile, + Replaces: newOrderRequest.Replaces, + ReplacesSerial: replacesSerial, }) - // TODO(#7153): Check each value via core.IsAnyNilOrZero - if err != nil || order == nil || order.Id == 0 || order.RegistrationID == 0 || len(order.Names) == 0 || core.IsAnyNilOrZero(order.Created, order.Expires) { + + if err != nil || core.IsAnyNilOrZero(order, order.Id, order.RegistrationID, order.Identifiers, order.Created, order.Expires) { wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "Error creating new order"), err) - if errors.Is(err, berrors.RateLimit) { - // Request was denied by a legacy rate limit. In this error case we - // do not want to refund the quota consumed by the request because - // repeated requests would result in unearned refunds. - // - // TODO(#5545): Once key-value rate limits are authoritative this - // can be removed. - errIsRateLimit = true - } return } logEvent.Created = fmt.Sprintf("%d", order.Id) @@ -2437,9 +2418,9 @@ func (wfe *WebFrontEndImpl) GetOrder(ctx context.Context, logEvent *web.RequestE // Any POSTs to the Order endpoint should be POST-as-GET requests. There are // no POSTs with a body allowed for this endpoint. if request.Method == http.MethodPost { - acct, prob := wfe.validPOSTAsGETForAccount(request, ctx, logEvent) - if prob != nil { - wfe.sendError(response, logEvent, prob, nil) + acct, err := wfe.validPOSTAsGETForAccount(request, ctx, logEvent) + if err != nil { + wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "Unable to validate JWS"), err) return } requesterAccount = acct @@ -2473,19 +2454,11 @@ func (wfe *WebFrontEndImpl) GetOrder(ctx context.Context, logEvent *web.RequestE return } - // TODO(#7153): Check each value via core.IsAnyNilOrZero - if order.Id == 0 || order.Status == "" || order.RegistrationID == 0 || len(order.Names) == 0 || core.IsAnyNilOrZero(order.Created, order.Expires) { + if core.IsAnyNilOrZero(order.Id, order.Status, order.RegistrationID, order.Identifiers, order.Created, order.Expires) { wfe.sendError(response, logEvent, probs.ServerInternal(fmt.Sprintf("Failed to retrieve order for ID %d", orderID)), errIncompleteGRPCResponse) return } - if requiredStale(request, logEvent) { - if prob := wfe.staleEnoughToGETOrder(order); prob != nil { - wfe.sendError(response, logEvent, prob, nil) - return - } - } - if order.RegistrationID != acctID { wfe.sendError(response, logEvent, probs.NotFound(fmt.Sprintf("No order found for account ID %d", acctID)), nil) return @@ -2505,6 +2478,10 @@ func (wfe *WebFrontEndImpl) GetOrder(ctx context.Context, logEvent *web.RequestE response.Header().Set(headerRetryAfter, strconv.Itoa(orderRetryAfter)) } + orderURL := web.RelativeEndpoint(request, + fmt.Sprintf("%s%d/%d", orderPath, acctID, order.Id)) + response.Header().Set("Location", orderURL) + err = wfe.writeJsonResponse(response, logEvent, http.StatusOK, respObj) if err != nil { wfe.sendError(response, logEvent, probs.ServerInternal("Error marshaling order"), err) @@ -2518,10 +2495,10 @@ func (wfe *WebFrontEndImpl) GetOrder(ctx context.Context, logEvent *web.RequestE func (wfe *WebFrontEndImpl) FinalizeOrder(ctx context.Context, logEvent *web.RequestEvent, response http.ResponseWriter, request *http.Request) { // Validate the POST body signature and get the authenticated account for this // finalize order request - body, _, acct, prob := wfe.validPOSTForAccount(request, ctx, logEvent) + body, _, acct, err := wfe.validPOSTForAccount(request, ctx, logEvent) addRequesterHeader(response, logEvent.Requester) - if prob != nil { - wfe.sendError(response, logEvent, prob, nil) + if err != nil { + wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "Unable to validate JWS"), err) return } @@ -2543,6 +2520,11 @@ func (wfe *WebFrontEndImpl) FinalizeOrder(ctx context.Context, logEvent *web.Req return } + if acct.ID != acctID { + wfe.sendError(response, logEvent, probs.Malformed("Mismatched account ID"), nil) + return + } + order, err := wfe.sa.GetOrder(ctx, &sapb.OrderRequest{Id: orderID}) if err != nil { if errors.Is(err, berrors.NotFound) { @@ -2554,17 +2536,12 @@ func (wfe *WebFrontEndImpl) FinalizeOrder(ctx context.Context, logEvent *web.Req return } - // TODO(#7153): Check each value via core.IsAnyNilOrZero - if order.Id == 0 || order.Status == "" || order.RegistrationID == 0 || len(order.Names) == 0 || core.IsAnyNilOrZero(order.Created, order.Expires) { + orderIdents := identifier.FromProtoSlice(order.Identifiers) + if core.IsAnyNilOrZero(order.Id, order.Status, order.RegistrationID, orderIdents, order.Created, order.Expires) { wfe.sendError(response, logEvent, probs.ServerInternal(fmt.Sprintf("Failed to retrieve order for ID %d", orderID)), errIncompleteGRPCResponse) return } - if order.RegistrationID != acctID { - wfe.sendError(response, logEvent, probs.NotFound(fmt.Sprintf("No order found for account ID %d", acctID)), nil) - return - } - // If the authenticated account ID doesn't match the order's registration ID // pretend it doesn't exist and abort. if acct.ID != order.RegistrationID { @@ -2574,11 +2551,7 @@ func (wfe *WebFrontEndImpl) FinalizeOrder(ctx context.Context, logEvent *web.Req // Only ready orders can be finalized. if order.Status != string(core.StatusReady) { - wfe.sendError(response, logEvent, - probs.OrderNotReady( - "Order's status (%q) is not acceptable for finalization", - order.Status), - nil) + wfe.sendError(response, logEvent, probs.OrderNotReady(fmt.Sprintf("Order's status (%q) is not acceptable for finalization", order.Status)), nil) return } @@ -2589,6 +2562,16 @@ func (wfe *WebFrontEndImpl) FinalizeOrder(ctx context.Context, logEvent *web.Req return } + // Don't finalize orders with profiles we no longer recognize. + if order.CertificateProfileName != "" { + err = wfe.validateCertificateProfileName(order.CertificateProfileName) + if err != nil { + // TODO(#7392) Provide link to profile documentation. + wfe.sendError(response, logEvent, probs.InvalidProfile(err.Error()), err) + return + } + } + // The authenticated finalize message body should be an encoded CSR var rawCSR core.RawCertificateRequest err = json.Unmarshal(body, &rawCSR) @@ -2605,7 +2588,7 @@ func (wfe *WebFrontEndImpl) FinalizeOrder(ctx context.Context, logEvent *web.Req return } - logEvent.DNSNames = order.Names + logEvent.Identifiers = orderIdents logEvent.Extra["KeyType"] = web.KeyTypeToString(csr.PublicKey) updatedOrder, err := wfe.ra.FinalizeOrder(ctx, &rapb.FinalizeOrderRequest{ @@ -2616,8 +2599,7 @@ func (wfe *WebFrontEndImpl) FinalizeOrder(ctx context.Context, logEvent *web.Req wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "Error finalizing order"), err) return } - // TODO(#7153): Check each value via core.IsAnyNilOrZero - if updatedOrder == nil || order.Id == 0 || order.RegistrationID == 0 || len(order.Names) == 0 || core.IsAnyNilOrZero(order.Created, order.Expires) { + if core.IsAnyNilOrZero(updatedOrder.Id, updatedOrder.RegistrationID, updatedOrder.Identifiers, updatedOrder.Created, updatedOrder.Expires) { wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "Error validating order"), errIncompleteGRPCResponse) return } @@ -2704,7 +2686,7 @@ func (wfe *WebFrontEndImpl) RenewalInfo(ctx context.Context, logEvent *web.Reque renewalInfo, err := wfe.determineARIWindow(ctx, decodedSerial) if err != nil { if errors.Is(err, berrors.NotFound) { - wfe.sendError(response, logEvent, probs.NotFound("Certificate replaced by this order was not found"), nil) + wfe.sendError(response, logEvent, probs.NotFound("Requested certificate was not found"), nil) return } wfe.sendError(response, logEvent, probs.ServerInternal("Error determining renewal window"), err) @@ -2719,18 +2701,18 @@ func (wfe *WebFrontEndImpl) RenewalInfo(ctx context.Context, logEvent *web.Reque } } -func extractRequesterIP(req *http.Request) (net.IP, error) { - ip := net.ParseIP(req.Header.Get("X-Real-IP")) - if ip != nil { +func extractRequesterIP(req *http.Request) (netip.Addr, error) { + ip, err := netip.ParseAddr(req.Header.Get("X-Real-IP")) + if err == nil { return ip, nil } host, _, err := net.SplitHostPort(req.RemoteAddr) if err != nil { - return nil, err + return netip.Addr{}, err } - return net.ParseIP(host), nil + return netip.ParseAddr(host) } func urlForAuthz(authz core.Authorization, request *http.Request) string { - return web.RelativeEndpoint(request, authzPath+authz.ID) + return web.RelativeEndpoint(request, fmt.Sprintf("%s%d/%s", authzPath, authz.RegistrationID, authz.ID)) } diff --git a/third-party/github.com/letsencrypt/boulder/wfe2/wfe_test.go b/third-party/github.com/letsencrypt/boulder/wfe2/wfe_test.go index 754c7562d..b5f31677a 100644 --- a/third-party/github.com/letsencrypt/boulder/wfe2/wfe_test.go +++ b/third-party/github.com/letsencrypt/boulder/wfe2/wfe_test.go @@ -21,6 +21,8 @@ import ( "net/http/httptest" "net/url" "os" + "reflect" + "slices" "sort" "strconv" "strings" @@ -35,14 +37,13 @@ import ( "google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/timestamppb" - capb "github.com/letsencrypt/boulder/ca/proto" "github.com/letsencrypt/boulder/cmd" + "github.com/letsencrypt/boulder/config" "github.com/letsencrypt/boulder/core" corepb "github.com/letsencrypt/boulder/core/proto" berrors "github.com/letsencrypt/boulder/errors" "github.com/letsencrypt/boulder/features" "github.com/letsencrypt/boulder/goodkey" - bgrpc "github.com/letsencrypt/boulder/grpc" "github.com/letsencrypt/boulder/identifier" "github.com/letsencrypt/boulder/issuance" blog "github.com/letsencrypt/boulder/log" @@ -54,11 +55,11 @@ import ( "github.com/letsencrypt/boulder/probs" rapb "github.com/letsencrypt/boulder/ra/proto" "github.com/letsencrypt/boulder/ratelimits" - bredis "github.com/letsencrypt/boulder/redis" "github.com/letsencrypt/boulder/revocation" sapb "github.com/letsencrypt/boulder/sa/proto" "github.com/letsencrypt/boulder/test" inmemnonce "github.com/letsencrypt/boulder/test/inmem/nonce" + "github.com/letsencrypt/boulder/unpause" "github.com/letsencrypt/boulder/web" ) @@ -184,21 +185,31 @@ EeMZ9nWyIM6bktLrE11HnFOnKhAYsM5fZA== ) type MockRegistrationAuthority struct { + rapb.RegistrationAuthorityClient + clk clock.Clock lastRevocationReason revocation.Reason } func (ra *MockRegistrationAuthority) NewRegistration(ctx context.Context, in *corepb.Registration, _ ...grpc.CallOption) (*corepb.Registration, error) { in.Id = 1 + in.Contact = nil created := time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC) in.CreatedAt = timestamppb.New(created) return in, nil } -func (ra *MockRegistrationAuthority) UpdateRegistration(ctx context.Context, in *rapb.UpdateRegistrationRequest, _ ...grpc.CallOption) (*corepb.Registration, error) { - if !bytes.Equal(in.Base.Key, in.Update.Key) { - in.Base.Key = in.Update.Key - } - return in.Base, nil +func (ra *MockRegistrationAuthority) UpdateRegistrationKey(ctx context.Context, in *rapb.UpdateRegistrationKeyRequest, _ ...grpc.CallOption) (*corepb.Registration, error) { + return &corepb.Registration{ + Status: string(core.StatusValid), + Key: in.Jwk, + }, nil +} + +func (ra *MockRegistrationAuthority) DeactivateRegistration(context.Context, *rapb.DeactivateRegistrationRequest, ...grpc.CallOption) (*corepb.Registration, error) { + return &corepb.Registration{ + Status: string(core.StatusDeactivated), + Key: []byte(test1KeyPublicJSON), + }, nil } func (ra *MockRegistrationAuthority) PerformValidation(context.Context, *rapb.PerformValidationRequest, ...grpc.CallOption) (*corepb.Authorization, error) { @@ -215,30 +226,71 @@ func (ra *MockRegistrationAuthority) RevokeCertByKey(ctx context.Context, in *ra return &emptypb.Empty{}, nil } -func (ra *MockRegistrationAuthority) GenerateOCSP(ctx context.Context, req *rapb.GenerateOCSPRequest, _ ...grpc.CallOption) (*capb.OCSPResponse, error) { - return nil, nil -} +// GetAuthorization returns a different authorization depending on the requested +// ID. All authorizations are associated with RegID 1, except for the one that isn't. +func (ra *MockRegistrationAuthority) GetAuthorization(_ context.Context, in *rapb.GetAuthorizationRequest, _ ...grpc.CallOption) (*corepb.Authorization, error) { + switch in.Id { + case 1: // Return a valid authorization with a single valid challenge. + return &corepb.Authorization{ + Id: "1", + RegistrationID: 1, + Identifier: identifier.NewDNS("not-an-example.com").ToProto(), + Status: string(core.StatusValid), + Expires: timestamppb.New(ra.clk.Now().AddDate(100, 0, 0)), + Challenges: []*corepb.Challenge{ + {Id: 1, Type: "http-01", Status: string(core.StatusValid), Token: "token"}, + }, + }, nil + case 2: // Return a pending authorization with three pending challenges. + return &corepb.Authorization{ + Id: "2", + RegistrationID: 1, + Identifier: identifier.NewDNS("not-an-example.com").ToProto(), + Status: string(core.StatusPending), + Expires: timestamppb.New(ra.clk.Now().AddDate(100, 0, 0)), + Challenges: []*corepb.Challenge{ + {Id: 1, Type: "http-01", Status: string(core.StatusPending), Token: "token"}, + {Id: 2, Type: "dns-01", Status: string(core.StatusPending), Token: "token"}, + {Id: 3, Type: "tls-alpn-01", Status: string(core.StatusPending), Token: "token"}, + }, + }, nil + case 3: // Return an expired authorization with three pending (but expired) challenges. + return &corepb.Authorization{ + Id: "3", + RegistrationID: 1, + Identifier: identifier.NewDNS("not-an-example.com").ToProto(), + Status: string(core.StatusPending), + Expires: timestamppb.New(ra.clk.Now().AddDate(-1, 0, 0)), + Challenges: []*corepb.Challenge{ + {Id: 1, Type: "http-01", Status: string(core.StatusPending), Token: "token"}, + {Id: 2, Type: "dns-01", Status: string(core.StatusPending), Token: "token"}, + {Id: 3, Type: "tls-alpn-01", Status: string(core.StatusPending), Token: "token"}, + }, + }, nil + case 4: // Return an internal server error. + return nil, fmt.Errorf("unspecified error") + case 5: // Return a pending authorization as above, but associated with RegID 2. + return &corepb.Authorization{ + Id: "5", + RegistrationID: 2, + Identifier: identifier.NewDNS("not-an-example.com").ToProto(), + Status: string(core.StatusPending), + Expires: timestamppb.New(ra.clk.Now().AddDate(100, 0, 0)), + Challenges: []*corepb.Challenge{ + {Id: 1, Type: "http-01", Status: string(core.StatusPending), Token: "token"}, + {Id: 2, Type: "dns-01", Status: string(core.StatusPending), Token: "token"}, + {Id: 3, Type: "tls-alpn-01", Status: string(core.StatusPending), Token: "token"}, + }, + }, nil + } -func (ra *MockRegistrationAuthority) AdministrativelyRevokeCertificate(context.Context, *rapb.AdministrativelyRevokeCertificateRequest, ...grpc.CallOption) (*emptypb.Empty, error) { - return &emptypb.Empty{}, nil -} - -func (ra *MockRegistrationAuthority) OnValidationUpdate(context.Context, core.Authorization, ...grpc.CallOption) error { - return nil + return nil, berrors.NotFoundError("no authorization found with id %q", in.Id) } func (ra *MockRegistrationAuthority) DeactivateAuthorization(context.Context, *corepb.Authorization, ...grpc.CallOption) (*emptypb.Empty, error) { return &emptypb.Empty{}, nil } -func (ra *MockRegistrationAuthority) DeactivateRegistration(context.Context, *corepb.Registration, ...grpc.CallOption) (*emptypb.Empty, error) { - return &emptypb.Empty{}, nil -} - -func (ra *MockRegistrationAuthority) UnpauseAccount(context.Context, *rapb.UnpauseAccountRequest, ...grpc.CallOption) (*emptypb.Empty, error) { - return &emptypb.Empty{}, nil -} - func (ra *MockRegistrationAuthority) NewOrder(ctx context.Context, in *rapb.NewOrderRequest, _ ...grpc.CallOption) (*corepb.Order, error) { created := time.Date(2021, 1, 1, 1, 1, 1, 0, time.UTC) expires := time.Date(2021, 2, 1, 1, 1, 1, 0, time.UTC) @@ -248,7 +300,7 @@ func (ra *MockRegistrationAuthority) NewOrder(ctx context.Context, in *rapb.NewO RegistrationID: in.RegistrationID, Created: timestamppb.New(created), Expires: timestamppb.New(expires), - Names: in.Names, + Identifiers: in.Identifiers, Status: string(core.StatusPending), V2Authorizations: []int64{1}, }, nil @@ -348,10 +400,9 @@ func setupWFE(t *testing.T) (WebFrontEndImpl, clock.FakeClock, requestSigner) { mockSA := mocks.NewStorageAuthorityReadOnly(fc) - log := blog.NewMock() - // Use derived nonces. - noncePrefix := nonce.DerivePrefix("192.168.1.1:8080", "b8c758dd85e113ea340ce0b3a99f389d40a308548af94d1730a7692c1874f1f") + rncKey := []byte("b8c758dd85e113ea340ce0b3a99f389d40a308548af94d1730a7692c1874f1f") + noncePrefix := nonce.DerivePrefix("192.168.1.1:8080", rncKey) nonceService, err := nonce.NewNonceService(metrics.NoopRegisterer, 100, noncePrefix) test.AssertNotError(t, err, "making nonceService") @@ -360,33 +411,15 @@ func setupWFE(t *testing.T) (WebFrontEndImpl, clock.FakeClock, requestSigner) { rnc := inmemNonceService // Setup rate limiting. - rc := bredis.Config{ - Username: "unittest-rw", - TLS: cmd.TLSConfig{ - CACertFile: "../test/certs/ipki/minica.pem", - CertFile: "../test/certs/ipki/localhost/cert.pem", - KeyFile: "../test/certs/ipki/localhost/key.pem", - }, - Lookups: []cmd.ServiceDomain{ - { - Service: "redisratelimits", - Domain: "service.consul", - }, - }, - LookupDNSAuthority: "consul.service.consul", - } - rc.PasswordConfig = cmd.PasswordConfig{ - PasswordFile: "../test/secrets/ratelimits_redis_password", - } - ring, err := bredis.NewRingFromConfig(rc, stats, log) - test.AssertNotError(t, err, "making redis ring client") - source := ratelimits.NewRedisSource(ring.Ring, fc, stats) - test.AssertNotNil(t, source, "source should not be nil") - limiter, err := ratelimits.NewLimiter(fc, source, stats) + limiter, err := ratelimits.NewLimiter(fc, ratelimits.NewInmemSource(), stats) test.AssertNotError(t, err, "making limiter") - txnBuilder, err := ratelimits.NewTransactionBuilder("../test/config-next/wfe2-ratelimit-defaults.yml", "") + txnBuilder, err := ratelimits.NewTransactionBuilderFromFiles("../test/config-next/wfe2-ratelimit-defaults.yml", "") test.AssertNotError(t, err, "making transaction composer") + unpauseSigner, err := unpause.NewJWTSigner(cmd.HMACKeyConfig{KeyFile: "../test/secrets/sfe_unpause_key"}) + test.AssertNotError(t, err, "making unpause signer") + unpauseLifetime := time.Hour * 24 * 14 + unpauseURL := "https://boulder.service.consul:4003" wfe, err := NewWebFrontEndImpl( stats, fc, @@ -396,18 +429,20 @@ func setupWFE(t *testing.T) (WebFrontEndImpl, clock.FakeClock, requestSigner) { blog.NewMock(), 10*time.Second, 10*time.Second, - 30*24*time.Hour, - 7*24*time.Hour, - &MockRegistrationAuthority{}, + 2, + &MockRegistrationAuthority{clk: fc}, mockSA, + nil, gnc, rnc, - "rncKey", + rncKey, mockSA, limiter, txnBuilder, - 100, - []string{""}, + map[string]string{"default": "a test profile"}, + unpauseSigner, + unpauseLifetime, + unpauseURL, ) test.AssertNotError(t, err, "Unable to create WFE") @@ -775,7 +810,10 @@ func TestDirectory(t *testing.T) { expectedJSON: `{ "keyChange": "http://localhost:4300/acme/key-change", "meta": { - "termsOfService": "http://example.invalid/terms" + "termsOfService": "http://example.invalid/terms", + "profiles": { + "default": "a test profile" + } }, "newNonce": "http://localhost:4300/acme/new-nonce", "newAccount": "http://localhost:4300/acme/new-acct", @@ -797,7 +835,10 @@ func TestDirectory(t *testing.T) { "Radiant Lock" ], "termsOfService": "http://example.invalid/terms", - "website": "zombo.com" + "website": "zombo.com", + "profiles": { + "default": "a test profile" + } }, "newAccount": "http://localhost:4300/acme/new-acct", "newNonce": "http://localhost:4300/acme/new-nonce", @@ -818,7 +859,10 @@ func TestDirectory(t *testing.T) { "Radiant Lock" ], "termsOfService": "http://example.invalid/terms", - "website": "zombo.com" + "website": "zombo.com", + "profiles": { + "default": "a test profile" + } }, "newAccount": "http://localhost/acme/new-acct", "newNonce": "http://localhost/acme/new-nonce", @@ -866,7 +910,10 @@ func TestRelativeDirectory(t *testing.T) { fmt.Fprintf(expected, `"newOrder":"%s/acme/new-order",`, hostname) fmt.Fprintf(expected, `"revokeCert":"%s/acme/revoke-cert",`, hostname) fmt.Fprintf(expected, `"AAAAAAAAAAA":"https://community.letsencrypt.org/t/adding-random-entries-to-the-directory/33417",`) - fmt.Fprintf(expected, `"meta":{"termsOfService":"http://example.invalid/terms"}`) + fmt.Fprintf(expected, `"meta":{`) + fmt.Fprintf(expected, `"termsOfService":"http://example.invalid/terms",`) + fmt.Fprintf(expected, `"profiles":{"default":"a test profile"}`) + fmt.Fprintf(expected, "}") fmt.Fprintf(expected, "}") return expected.String() } @@ -1105,43 +1152,41 @@ func TestHTTPMethods(t *testing.T) { } } -func TestGetChallenge(t *testing.T) { +func TestGetChallengeHandler(t *testing.T) { wfe, _, _ := setupWFE(t) - challengeURL := "http://localhost/acme/chall-v3/1/-ZfxEw" + // The slug "7TyhFQ" is the StringID of a challenge with type "http-01" and + // token "token". + challSlug := "7TyhFQ" for _, method := range []string{"GET", "HEAD"} { resp := httptest.NewRecorder() + // We set req.URL.Path separately to emulate the path-stripping that + // Boulder's request handler does. + challengeURL := fmt.Sprintf("http://localhost/acme/chall/1/1/%s", challSlug) req, err := http.NewRequest(method, challengeURL, nil) - req.URL.Path = "1/-ZfxEw" test.AssertNotError(t, err, "Could not make NewRequest") + req.URL.Path = fmt.Sprintf("1/1/%s", challSlug) + + wfe.ChallengeHandler(ctx, newRequestEvent(), resp, req) + test.AssertEquals(t, resp.Code, http.StatusOK) + test.AssertEquals(t, resp.Header().Get("Location"), challengeURL) + test.AssertEquals(t, resp.Header().Get("Content-Type"), "application/json") + test.AssertEquals(t, resp.Header().Get("Link"), `;rel="up"`) - wfe.Challenge(ctx, newRequestEvent(), resp, req) - test.AssertEquals(t, - resp.Code, - http.StatusOK) - test.AssertEquals(t, - resp.Header().Get("Location"), - challengeURL) - test.AssertEquals(t, - resp.Header().Get("Content-Type"), - "application/json") - test.AssertEquals(t, - resp.Header().Get("Link"), - `;rel="up"`) // Body is only relevant for GET. For HEAD, body will // be discarded by HandleFunc() anyway, so it doesn't // matter what Challenge() writes to it. if method == "GET" { test.AssertUnmarshaledEquals( t, resp.Body.String(), - `{"status": "pending", "type":"dns","token":"token","url":"http://localhost/acme/chall-v3/1/-ZfxEw"}`) + `{"status": "valid", "type":"http-01","token":"token","url":"http://localhost/acme/chall/1/1/7TyhFQ"}`) } } } -func TestChallenge(t *testing.T) { +func TestChallengeHandler(t *testing.T) { wfe, _, signer := setupWFE(t) post := func(path string) *http.Request { @@ -1163,50 +1208,51 @@ func TestChallenge(t *testing.T) { }{ { Name: "Valid challenge", - Request: post("1/-ZfxEw"), + Request: post("1/1/7TyhFQ"), ExpectedStatus: http.StatusOK, ExpectedHeaders: map[string]string{ - "Location": "http://localhost/acme/chall-v3/1/-ZfxEw", - "Link": `;rel="up"`, + "Content-Type": "application/json", + "Location": "http://localhost/acme/chall/1/1/7TyhFQ", + "Link": `;rel="up"`, }, - ExpectedBody: `{"status": "pending", "type":"dns","token":"token","url":"http://localhost/acme/chall-v3/1/-ZfxEw"}`, + ExpectedBody: `{"status": "valid", "type":"http-01","token":"token","url":"http://localhost/acme/chall/1/1/7TyhFQ"}`, }, { Name: "Expired challenge", - Request: post("3/-ZfxEw"), + Request: post("1/3/7TyhFQ"), ExpectedStatus: http.StatusNotFound, ExpectedBody: `{"type":"` + probs.ErrorNS + `malformed","detail":"Expired authorization","status":404}`, }, { Name: "Missing challenge", - Request: post("1/"), + Request: post("1/1/"), ExpectedStatus: http.StatusNotFound, ExpectedBody: `{"type":"` + probs.ErrorNS + `malformed","detail":"No such challenge","status":404}`, }, { Name: "Unspecified database error", - Request: post("4/-ZfxEw"), + Request: post("1/4/7TyhFQ"), ExpectedStatus: http.StatusInternalServerError, ExpectedBody: `{"type":"` + probs.ErrorNS + `serverInternal","detail":"Problem getting authorization","status":500}`, }, { Name: "POST-as-GET, wrong owner", - Request: postAsGet(1, "5/-ZfxEw", ""), + Request: postAsGet(1, "1/5/7TyhFQ", ""), ExpectedStatus: http.StatusForbidden, ExpectedBody: `{"type":"` + probs.ErrorNS + `unauthorized","detail":"User account ID doesn't match account ID in authorization","status":403}`, }, { Name: "Valid POST-as-GET", - Request: postAsGet(1, "1/-ZfxEw", ""), + Request: postAsGet(1, "1/1/7TyhFQ", ""), ExpectedStatus: http.StatusOK, - ExpectedBody: `{"status": "pending", "type":"dns", "token":"token", "url": "http://localhost/acme/chall-v3/1/-ZfxEw"}`, + ExpectedBody: `{"status": "valid", "type":"http-01", "token":"token", "url": "http://localhost/acme/chall/1/1/7TyhFQ"}`, }, } for _, tc := range testCases { t.Run(tc.Name, func(t *testing.T) { responseWriter := httptest.NewRecorder() - wfe.Challenge(ctx, newRequestEvent(), responseWriter, tc.Request) + wfe.ChallengeHandler(ctx, newRequestEvent(), responseWriter, tc.Request) // Check the response code, headers and body match expected headers := responseWriter.Header() body := responseWriter.Body.String() @@ -1229,43 +1275,43 @@ func (ra *MockRAPerformValidationError) PerformValidation(context.Context, *rapb return nil, errors.New("broken on purpose") } -// TestUpdateChallengeFinalizedAuthz tests that POSTing a challenge associated +// TestUpdateChallengeHandlerFinalizedAuthz tests that POSTing a challenge associated // with an already valid authorization just returns the challenge without calling // the RA. -func TestUpdateChallengeFinalizedAuthz(t *testing.T) { - wfe, _, signer := setupWFE(t) - wfe.ra = &MockRAPerformValidationError{} +func TestUpdateChallengeHandlerFinalizedAuthz(t *testing.T) { + wfe, fc, signer := setupWFE(t) + wfe.ra = &MockRAPerformValidationError{MockRegistrationAuthority{clk: fc}} responseWriter := httptest.NewRecorder() - signedURL := "http://localhost/1/-ZfxEw" + signedURL := "http://localhost/1/1/7TyhFQ" _, _, jwsBody := signer.byKeyID(1, nil, signedURL, `{}`) - request := makePostRequestWithPath("1/-ZfxEw", jwsBody) - wfe.Challenge(ctx, newRequestEvent(), responseWriter, request) + request := makePostRequestWithPath("1/1/7TyhFQ", jwsBody) + wfe.ChallengeHandler(ctx, newRequestEvent(), responseWriter, request) body := responseWriter.Body.String() test.AssertUnmarshaledEquals(t, body, `{ - "status": "pending", - "type": "dns", - "token":"token", - "url": "http://localhost/acme/chall-v3/1/-ZfxEw" + "status": "valid", + "type": "http-01", + "token": "token", + "url": "http://localhost/acme/chall/1/1/7TyhFQ" }`) } -// TestUpdateChallengeRAError tests that when the RA returns an error from +// TestUpdateChallengeHandlerRAError tests that when the RA returns an error from // PerformValidation that the WFE returns an internal server error as expected // and does not panic or otherwise bug out. -func TestUpdateChallengeRAError(t *testing.T) { - wfe, _, signer := setupWFE(t) +func TestUpdateChallengeHandlerRAError(t *testing.T) { + wfe, fc, signer := setupWFE(t) // Mock the RA to always fail PerformValidation - wfe.ra = &MockRAPerformValidationError{} + wfe.ra = &MockRAPerformValidationError{MockRegistrationAuthority{clk: fc}} // Update a pending challenge - signedURL := "http://localhost/2/-ZfxEw" + signedURL := "http://localhost/1/2/7TyhFQ" _, _, jwsBody := signer.byKeyID(1, nil, signedURL, `{}`) responseWriter := httptest.NewRecorder() - request := makePostRequestWithPath("2/-ZfxEw", jwsBody) + request := makePostRequestWithPath("1/2/7TyhFQ", jwsBody) - wfe.Challenge(ctx, newRequestEvent(), responseWriter, request) + wfe.ChallengeHandler(ctx, newRequestEvent(), responseWriter, request) // The result should be an internal server error problem. body := responseWriter.Body.String() @@ -1297,7 +1343,7 @@ func TestBadNonce(t *testing.T) { test.AssertNotError(t, err, "Failed to sign body") wfe.NewAccount(ctx, newRequestEvent(), responseWriter, makePostRequestWithPath("nonce", result.FullSerialize())) - test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), `{"type":"`+probs.ErrorNS+`badNonce","detail":"JWS has no anti-replay nonce","status":400}`) + test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), `{"type":"`+probs.ErrorNS+`badNonce","detail":"Unable to validate JWS :: JWS has no anti-replay nonce","status":400}`) } func TestNewECDSAAccount(t *testing.T) { @@ -1321,10 +1367,7 @@ func TestNewECDSAAccount(t *testing.T) { responseBody := responseWriter.Body.String() err := json.Unmarshal([]byte(responseBody), &acct) test.AssertNotError(t, err, "Couldn't unmarshal returned account object") - test.Assert(t, len(*acct.Contact) >= 1, "No contact field in account") - test.AssertEquals(t, (*acct.Contact)[0], "mailto:person@mail.com") test.AssertEquals(t, acct.Agreement, "") - test.AssertEquals(t, acct.InitialIP.String(), "1.1.1.1") test.AssertEquals(t, responseWriter.Header().Get("Location"), "http://localhost/acme/acct/1") @@ -1347,7 +1390,6 @@ func TestNewECDSAAccount(t *testing.T) { "x": "FwvSZpu06i3frSk_mz9HcD9nETn4wf3mQ-zDtG21Gao", "y": "S8rR-0dWa8nAcw1fbunF_ajS3PQZ-QwLps-2adgLgPk" }, - "initialIp": "", "status": "" }`) test.AssertEquals(t, responseWriter.Header().Get("Location"), "http://localhost/acme/acct/3") @@ -1377,7 +1419,6 @@ func TestNewECDSAAccount(t *testing.T) { // a populated acct object will be returned. func TestEmptyAccount(t *testing.T) { wfe, _, signer := setupWFE(t) - responseWriter := httptest.NewRecorder() // Test Key 1 is mocked in the mock StorageAuthority used in setupWFE to // return a populated account for GetRegistrationByKey when test key 1 is @@ -1386,31 +1427,64 @@ func TestEmptyAccount(t *testing.T) { _, ok := key.(*rsa.PrivateKey) test.Assert(t, ok, "Couldn't load RSA key") - payload := `{}` path := "1" signedURL := "http://localhost/1" - _, _, body := signer.byKeyID(1, key, signedURL, payload) - request := makePostRequestWithPath(path, body) - // Send an account update with the trivial body - wfe.Account( - ctx, - newRequestEvent(), - responseWriter, - request) + testCases := []struct { + Name string + Payload string + ExpectedStatus int + }{ + { + Name: "POST empty string to acct", + Payload: "", + ExpectedStatus: http.StatusOK, + }, + { + Name: "POST empty JSON object to acct", + Payload: "{}", + ExpectedStatus: http.StatusOK, + }, + { + Name: "POST invalid empty JSON string to acct", + Payload: "\"\"", + ExpectedStatus: http.StatusBadRequest, + }, + { + Name: "POST invalid empty JSON array to acct", + Payload: "[]", + ExpectedStatus: http.StatusBadRequest, + }, + } - responseBody := responseWriter.Body.String() - // There should be no error - test.AssertNotContains(t, responseBody, probs.ErrorNS) + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + responseWriter := httptest.NewRecorder() - // We should get back a populated Account - var acct core.Registration - err := json.Unmarshal([]byte(responseBody), &acct) - test.AssertNotError(t, err, "Couldn't unmarshal returned account object") - test.Assert(t, len(*acct.Contact) >= 1, "No contact field in account") - test.AssertEquals(t, (*acct.Contact)[0], "mailto:person@mail.com") - test.AssertEquals(t, acct.Agreement, "") - responseWriter.Body.Reset() + _, _, body := signer.byKeyID(1, key, signedURL, tc.Payload) + request := makePostRequestWithPath(path, body) + + // Send an account update with the trivial body + wfe.Account( + ctx, + newRequestEvent(), + responseWriter, + request) + + responseBody := responseWriter.Body.String() + test.AssertEquals(t, responseWriter.Code, tc.ExpectedStatus) + + // If success is expected, we should get back a populated Account + if tc.ExpectedStatus == http.StatusOK { + var acct core.Registration + err := json.Unmarshal([]byte(responseBody), &acct) + test.AssertNotError(t, err, "Couldn't unmarshal returned account object") + test.AssertEquals(t, acct.Agreement, "") + } + + responseWriter.Body.Reset() + }) + } } func TestNewAccount(t *testing.T) { @@ -1446,19 +1520,19 @@ func TestNewAccount(t *testing.T) { "Content-Type": {expectedJWSContentType}, }, }, - `{"type":"` + probs.ErrorNS + `malformed","detail":"No body on POST","status":400}`, + `{"type":"` + probs.ErrorNS + `malformed","detail":"Unable to validate JWS :: No body on POST","status":400}`, }, // POST, but body that isn't valid JWS { makePostRequestWithPath(newAcctPath, "hi"), - `{"type":"` + probs.ErrorNS + `malformed","detail":"Parse error reading JWS","status":400}`, + `{"type":"` + probs.ErrorNS + `malformed","detail":"Unable to validate JWS :: Parse error reading JWS","status":400}`, }, // POST, Properly JWS-signed, but payload is "foo", not base64-encoded JSON. { makePostRequestWithPath(newAcctPath, fooBody), - `{"type":"` + probs.ErrorNS + `malformed","detail":"Request payload did not parse as JSON","status":400}`, + `{"type":"` + probs.ErrorNS + `malformed","detail":"Unable to validate JWS :: Request payload did not parse as JSON","status":400}`, }, // Same signed body, but payload modified by one byte, breaking signature. @@ -1466,7 +1540,7 @@ func TestNewAccount(t *testing.T) { { makePostRequestWithPath(newAcctPath, `{"payload":"Zm9x","protected":"eyJhbGciOiJSUzI1NiIsImp3ayI6eyJrdHkiOiJSU0EiLCJuIjoicW5BUkxyVDdYejRnUmNLeUxkeWRtQ3ItZXk5T3VQSW1YNFg0MHRoazNvbjI2RmtNem5SM2ZSanM2NmVMSzdtbVBjQlo2dU9Kc2VVUlU2d0FhWk5tZW1vWXgxZE12cXZXV0l5aVFsZUhTRDdROHZCcmhSNnVJb080akF6SlpSLUNoelp1U0R0N2lITi0zeFVWc3B1NVhHd1hVX01WSlpzaFR3cDRUYUZ4NWVsSElUX09iblR2VE9VM1hoaXNoMDdBYmdaS21Xc1ZiWGg1cy1DcklpY1U0T2V4SlBndW5XWl9ZSkp1ZU9LbVR2bkxsVFY0TXpLUjJvWmxCS1oyN1MwLVNmZFZfUUR4X3lkbGU1b01BeUtWdGxBVjM1Y3lQTUlzWU53Z1VHQkNkWV8yVXppNWVYMGxUYzdNUFJ3ejZxUjFraXAtaTU5VmNHY1VRZ3FIVjZGeXF3IiwiZSI6IkFRQUIifSwia2lkIjoiIiwibm9uY2UiOiJyNHpuenZQQUVwMDlDN1JwZUtYVHhvNkx3SGwxZVBVdmpGeXhOSE1hQnVvIiwidXJsIjoiaHR0cDovL2xvY2FsaG9zdC9hY21lL25ldy1yZWcifQ","signature":"jcTdxSygm_cvD7KbXqsxgnoPApCTSkV4jolToSOd2ciRkg5W7Yl0ZKEEKwOc-dYIbQiwGiDzisyPCicwWsOUA1WSqHylKvZ3nxSMc6KtwJCW2DaOqcf0EEjy5VjiZJUrOt2c-r6b07tbn8sfOJKwlF2lsOeGi4s-rtvvkeQpAU-AWauzl9G4bv2nDUeCviAZjHx_PoUC-f9GmZhYrbDzAvXZ859ktM6RmMeD0OqPN7bhAeju2j9Gl0lnryZMtq2m0J2m1ucenQBL1g4ZkP1JiJvzd2cAz5G7Ftl2YeJJyWhqNd3qq0GVOt1P11s8PTGNaSoM0iR9QfUxT9A6jxARtg"}`), - `{"type":"` + probs.ErrorNS + `malformed","detail":"JWS verification error","status":400}`, + `{"type":"` + probs.ErrorNS + `malformed","detail":"Unable to validate JWS :: JWS verification error","status":400}`, }, { makePostRequestWithPath(newAcctPath, wrongAgreementBody), @@ -1491,9 +1565,6 @@ func TestNewAccount(t *testing.T) { responseBody := responseWriter.Body.String() err := json.Unmarshal([]byte(responseBody), &acct) test.AssertNotError(t, err, "Couldn't unmarshal returned account object") - test.Assert(t, len(*acct.Contact) >= 1, "No contact field in account") - test.AssertEquals(t, (*acct.Contact)[0], "mailto:person@mail.com") - test.AssertEquals(t, acct.InitialIP.String(), "1.1.1.1") // Agreement is an ACMEv1 field and should not be present test.AssertEquals(t, acct.Agreement, "") @@ -1528,7 +1599,6 @@ func TestNewAccount(t *testing.T) { "contact": [ "mailto:person@mail.com" ], - "initialIp": "", "status": "valid" }`) } @@ -1573,22 +1643,136 @@ func TestNewAccountNoID(t *testing.T) { "n": "qnARLrT7Xz4gRcKyLdydmCr-ey9OuPImX4X40thk3on26FkMznR3fRjs66eLK7mmPcBZ6uOJseURU6wAaZNmemoYx1dMvqvWWIyiQleHSD7Q8vBrhR6uIoO4jAzJZR-ChzZuSDt7iHN-3xUVspu5XGwXU_MVJZshTwp4TaFx5elHIT_ObnTvTOU3Xhish07AbgZKmWsVbXh5s-CrIicU4OexJPgunWZ_YJJueOKmTvnLlTV4MzKR2oZlBKZ27S0-SfdV_QDx_ydle5oMAyKVtlAV35cyPMIsYNwgUGBCdY_2Uzi5eX0lTc7MPRwz6qR1kip-i59VcGcUQgqHV6Fyqw", "e": "AQAB" }, - "contact": [ - "mailto:person@mail.com" - ], - "initialIp": "1.1.1.1", "createdAt": "2021-01-01T00:00:00Z", "status": "" }`) } -func TestGetAuthorization(t *testing.T) { +func TestContactsToEmails(t *testing.T) { + t.Parallel() + wfe, _, _ := setupWFE(t) + + for _, tc := range []struct { + name string + contacts []string + want []string + wantErr string + }{ + { + name: "no contacts", + contacts: []string{}, + want: []string{}, + }, + { + name: "happy path", + contacts: []string{"mailto:one@mail.com", "mailto:two@mail.com"}, + want: []string{"one@mail.com", "two@mail.com"}, + }, + { + name: "empty url", + contacts: []string{""}, + wantErr: "empty contact", + }, + { + name: "too many contacts", + contacts: []string{"mailto:one@mail.com", "mailto:two@mail.com", "mailto:three@mail.com"}, + wantErr: "too many contacts", + }, + { + name: "unknown scheme", + contacts: []string{"ansible:earth.sol.milkyway.laniakea/letsencrypt"}, + wantErr: "contact scheme", + }, + { + name: "malformed email", + contacts: []string{"mailto:admin.com"}, + wantErr: "unable to parse email address", + }, + { + name: "non-ascii email", + contacts: []string{"mailto:señor@email.com"}, + wantErr: "contains non-ASCII characters", + }, + { + name: "unarseable email", + contacts: []string{"mailto:a@mail.com, b@mail.com"}, + wantErr: "unable to parse email address", + }, + { + name: "forbidden example domain", + contacts: []string{"mailto:a@example.org"}, + wantErr: "forbidden", + }, + { + name: "forbidden non-public domain", + contacts: []string{"mailto:admin@localhost"}, + wantErr: "needs at least one dot", + }, + { + name: "forbidden non-iana domain", + contacts: []string{"mailto:admin@non.iana.suffix"}, + wantErr: "does not end with a valid public suffix", + }, + { + name: "forbidden ip domain", + contacts: []string{"mailto:admin@1.2.3.4"}, + wantErr: "value is an IP address", + }, + { + name: "forbidden bracketed ip domain", + contacts: []string{"mailto:admin@[1.2.3.4]"}, + wantErr: "contains an invalid character", + }, + { + name: "query parameter", + contacts: []string{"mailto:admin@a.com?no-reminder-emails"}, + wantErr: "contains a question mark", + }, + { + name: "empty query parameter", + contacts: []string{"mailto:admin@a.com?"}, + wantErr: "contains a question mark", + }, + { + name: "fragment url", + contacts: []string{"mailto:admin@a.com#optional"}, + wantErr: "contains a '#'", + }, + { + name: "empty fragment url", + contacts: []string{"mailto:admin@a.com#"}, + wantErr: "contains a '#'", + }, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + got, err := wfe.contactsToEmails(tc.contacts) + if tc.wantErr != "" { + if err == nil { + t.Fatalf("contactsToEmails(%#v) = nil, but want %q", tc.contacts, tc.wantErr) + } + if !strings.Contains(err.Error(), tc.wantErr) { + t.Errorf("contactsToEmails(%#v) = %q, but want %q", tc.contacts, err.Error(), tc.wantErr) + } + } else { + if err != nil { + t.Fatalf("contactsToEmails(%#v) = %q, but want %#v", tc.contacts, err.Error(), tc.want) + } + if !slices.Equal(got, tc.want) { + t.Errorf("contactsToEmails(%#v) = %#v, but want %#v", tc.contacts, got, tc.want) + } + } + }) + } +} + +func TestGetAuthorizationHandler(t *testing.T) { wfe, _, signer := setupWFE(t) // Expired authorizations should be inaccessible - authzURL := "3" + authzURL := "1/3" responseWriter := httptest.NewRecorder() - wfe.Authorization(ctx, newRequestEvent(), responseWriter, &http.Request{ + wfe.AuthorizationHandler(ctx, newRequestEvent(), responseWriter, &http.Request{ Method: "GET", URL: mustParseURL(authzURL), }) @@ -1598,19 +1782,19 @@ func TestGetAuthorization(t *testing.T) { responseWriter.Body.Reset() // Ensure that a valid authorization can't be reached with an invalid URL - wfe.Authorization(ctx, newRequestEvent(), responseWriter, &http.Request{ - URL: mustParseURL("1d"), + wfe.AuthorizationHandler(ctx, newRequestEvent(), responseWriter, &http.Request{ + URL: mustParseURL("1/1d"), Method: "GET", }) test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), `{"type":"`+probs.ErrorNS+`malformed","detail":"Invalid authorization ID","status":400}`) - _, _, jwsBody := signer.byKeyID(1, nil, "http://localhost/1", "") - postAsGet := makePostRequestWithPath("1", jwsBody) + _, _, jwsBody := signer.byKeyID(1, nil, "http://localhost/1/1", "") + postAsGet := makePostRequestWithPath("1/1", jwsBody) responseWriter = httptest.NewRecorder() // Ensure that a POST-as-GET to an authorization works - wfe.Authorization(ctx, newRequestEvent(), responseWriter, postAsGet) + wfe.AuthorizationHandler(ctx, newRequestEvent(), responseWriter, postAsGet) test.AssertEquals(t, responseWriter.Code, http.StatusOK) body := responseWriter.Body.String() test.AssertUnmarshaledEquals(t, body, ` @@ -1623,24 +1807,24 @@ func TestGetAuthorization(t *testing.T) { "expires": "2070-01-01T00:00:00Z", "challenges": [ { - "status": "pending", - "type": "dns", + "status": "valid", + "type": "http-01", "token":"token", - "url": "http://localhost/acme/chall-v3/1/-ZfxEw" + "url": "http://localhost/acme/chall/1/1/7TyhFQ" } ] }`) } -// TestAuthorization500 tests that internal errors on GetAuthorization result in +// TestAuthorizationHandler500 tests that internal errors on GetAuthorization result in // a 500. -func TestAuthorization500(t *testing.T) { +func TestAuthorizationHandler500(t *testing.T) { wfe, _, _ := setupWFE(t) responseWriter := httptest.NewRecorder() - wfe.Authorization(ctx, newRequestEvent(), responseWriter, &http.Request{ + wfe.AuthorizationHandler(ctx, newRequestEvent(), responseWriter, &http.Request{ Method: "GET", - URL: mustParseURL("4"), + URL: mustParseURL("1/4"), }) expected := `{ "type": "urn:ietf:params:acme:error:serverInternal", @@ -1650,49 +1834,46 @@ func TestAuthorization500(t *testing.T) { test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), expected) } -// SAWithFailedChallenges is a mocks.StorageAuthority that has -// a `GetAuthorization` implementation that can return authorizations with -// failed challenges. -type SAWithFailedChallenges struct { - sapb.StorageAuthorityReadOnlyClient - Clk clock.FakeClock +// RAWithFailedChallenges is a fake RA whose GetAuthorization method returns +// an authz with a failed challenge. +type RAWithFailedChallenge struct { + rapb.RegistrationAuthorityClient + clk clock.Clock } -func (sa *SAWithFailedChallenges) GetAuthorization2(ctx context.Context, id *sapb.AuthorizationID2, _ ...grpc.CallOption) (*corepb.Authorization, error) { - authz := core.Authorization{ - ID: "55", - Status: core.StatusValid, +func (ra *RAWithFailedChallenge) GetAuthorization(ctx context.Context, id *rapb.GetAuthorizationRequest, _ ...grpc.CallOption) (*corepb.Authorization, error) { + return &corepb.Authorization{ + Id: "6", RegistrationID: 1, - Identifier: identifier.DNSIdentifier("not-an-example.com"), - Challenges: []core.Challenge{ + Identifier: identifier.NewDNS("not-an-example.com").ToProto(), + Status: string(core.StatusInvalid), + Expires: timestamppb.New(ra.clk.Now().AddDate(100, 0, 0)), + Challenges: []*corepb.Challenge{ { - Status: core.StatusInvalid, - Type: "dns", - Token: "exampleToken", - Error: &probs.ProblemDetails{ - Type: "things:are:whack", - Detail: "whack attack", - HTTPStatus: 555, + Id: 1, + Type: "http-01", + Status: string(core.StatusInvalid), + Token: "token", + Error: &corepb.ProblemDetails{ + ProblemType: "things:are:whack", + Detail: "whack attack", + HttpStatus: 555, }, }, }, - } - exp := sa.Clk.Now().AddDate(100, 0, 0) - authz.Expires = &exp - return bgrpc.AuthzToPB(authz) + }, nil } -// TestAuthorizationChallengeNamespace tests that the runtime prefixing of +// TestAuthorizationChallengeHandlerNamespace tests that the runtime prefixing of // Challenge Problem Types works as expected -func TestAuthorizationChallengeNamespace(t *testing.T) { +func TestAuthorizationChallengeHandlerNamespace(t *testing.T) { wfe, clk, _ := setupWFE(t) - - wfe.sa = &SAWithFailedChallenges{Clk: clk} + wfe.ra = &RAWithFailedChallenge{clk: clk} responseWriter := httptest.NewRecorder() - wfe.Authorization(ctx, newRequestEvent(), responseWriter, &http.Request{ + wfe.AuthorizationHandler(ctx, newRequestEvent(), responseWriter, &http.Request{ Method: "GET", - URL: mustParseURL("55"), + URL: mustParseURL("1/6"), }) var authz core.Authorization @@ -1704,15 +1885,6 @@ func TestAuthorizationChallengeNamespace(t *testing.T) { responseWriter.Body.Reset() } -func contains(s []string, e string) bool { - for _, a := range s { - if a == e { - return true - } - } - return false -} - func TestAccount(t *testing.T) { wfe, _, signer := setupWFE(t) mux := wfe.Handler(metrics.NoopRegisterer) @@ -1732,7 +1904,7 @@ func TestAccount(t *testing.T) { wfe.Account(ctx, newRequestEvent(), responseWriter, makePostRequestWithPath("2", "invalid")) test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), - `{"type":"`+probs.ErrorNS+`malformed","detail":"Parse error reading JWS","status":400}`) + `{"type":"`+probs.ErrorNS+`malformed","detail":"Unable to validate JWS :: Parse error reading JWS","status":400}`) responseWriter.Body.Reset() key := loadKey(t, []byte(test2KeyPrivatePEM)) @@ -1750,7 +1922,7 @@ func TestAccount(t *testing.T) { wfe.Account(ctx, newRequestEvent(), responseWriter, request) test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), - `{"type":"`+probs.ErrorNS+`accountDoesNotExist","detail":"Account \"http://localhost/acme/acct/102\" not found","status":400}`) + `{"type":"`+probs.ErrorNS+`accountDoesNotExist","detail":"Unable to validate JWS :: Account \"http://localhost/acme/acct/102\" not found","status":400}`) responseWriter.Body.Reset() key = loadKey(t, []byte(test1KeyPrivatePEM)) @@ -1767,7 +1939,7 @@ func TestAccount(t *testing.T) { wfe.Account(ctx, newRequestEvent(), responseWriter, request) test.AssertNotContains(t, responseWriter.Body.String(), probs.ErrorNS) links := responseWriter.Header()["Link"] - test.AssertEquals(t, contains(links, "<"+agreementURL+">;rel=\"terms-of-service\""), true) + test.AssertEquals(t, slices.Contains(links, "<"+agreementURL+">;rel=\"terms-of-service\""), true) responseWriter.Body.Reset() // Test POST valid JSON with garbage in URL but valid account ID @@ -1808,6 +1980,85 @@ func TestAccount(t *testing.T) { }`) } +func TestUpdateAccount(t *testing.T) { + t.Parallel() + wfe, _, _ := setupWFE(t) + + for _, tc := range []struct { + name string + req string + wantAcct *core.Registration + wantErr string + }{ + { + name: "empty status", + req: `{}`, + wantAcct: &core.Registration{Status: core.StatusValid}, + }, + { + name: "empty status with contact", + req: `{"contact": ["mailto:admin@example.com"]}`, + wantAcct: &core.Registration{Status: core.StatusValid}, + }, + { + name: "valid", + req: `{"status": "valid"}`, + wantAcct: &core.Registration{Status: core.StatusValid}, + }, + { + name: "valid with contact", + req: `{"status": "valid", "contact": ["mailto:admin@example.com"]}`, + wantAcct: &core.Registration{Status: core.StatusValid}, + }, + { + name: "deactivate", + req: `{"status": "deactivated"}`, + wantAcct: &core.Registration{Status: core.StatusDeactivated}, + }, + { + name: "deactivate with contact", + req: `{"status": "deactivated", "contact": ["mailto:admin@example.com"]}`, + wantAcct: &core.Registration{Status: core.StatusDeactivated}, + }, + { + name: "unrecognized status", + req: `{"status": "foo"}`, + wantErr: "invalid status", + }, + { + // We're happy to ignore fields we don't recognize; they might be useful + // for other CAs. + name: "unrecognized request field", + req: `{"foo": "bar"}`, + wantAcct: &core.Registration{Status: core.StatusValid}, + }, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + currAcct := core.Registration{Status: core.StatusValid} + + gotAcct, gotProb := wfe.updateAccount(context.Background(), []byte(tc.req), &currAcct) + if tc.wantAcct != nil { + if gotAcct.Status != tc.wantAcct.Status { + t.Errorf("want status %s, got %s", tc.wantAcct.Status, gotAcct.Status) + } + if !reflect.DeepEqual(gotAcct.Contact, tc.wantAcct.Contact) { + t.Errorf("want contact %v, got %v", tc.wantAcct.Contact, gotAcct.Contact) + } + } + if tc.wantErr != "" { + if gotProb == nil { + t.Fatalf("want error %q, got nil", tc.wantErr) + } + if !strings.Contains(gotProb.Error(), tc.wantErr) { + t.Errorf("want error %q, got %q", tc.wantErr, gotProb.Error()) + } + } + }) + } +} + type mockSAWithCert struct { sapb.StorageAuthorityReadOnlyClient cert *x509.Certificate @@ -1864,7 +2115,7 @@ func newMockSAWithIncident(sa sapb.StorageAuthorityReadOnlyClient, serial []stri { Id: 0, SerialTable: "incident_foo", - Url: agreementURL, + Url: "http://big.bad/incident", RenewBy: nil, Enabled: true, }, @@ -1951,7 +2202,7 @@ func TestGetCertificate(t *testing.T) { ExpectedBody: `{ "type": "urn:ietf:params:acme:error:malformed", "status": 400, - "detail": "POST-as-GET requests must have an empty payload" + "detail": "Unable to validate JWS :: POST-as-GET requests must have an empty payload" }`, }, { @@ -2337,27 +2588,27 @@ func TestHeaderBoulderRequester(t *testing.T) { test.AssertEquals(t, responseWriter.Header().Get("Boulder-Requester"), "1") } -func TestDeactivateAuthorization(t *testing.T) { +func TestDeactivateAuthorizationHandler(t *testing.T) { wfe, _, signer := setupWFE(t) responseWriter := httptest.NewRecorder() responseWriter.Body.Reset() payload := `{"status":""}` - _, _, body := signer.byKeyID(1, nil, "http://localhost/1", payload) - request := makePostRequestWithPath("1", body) + _, _, body := signer.byKeyID(1, nil, "http://localhost/1/1", payload) + request := makePostRequestWithPath("1/1", body) - wfe.Authorization(ctx, newRequestEvent(), responseWriter, request) + wfe.AuthorizationHandler(ctx, newRequestEvent(), responseWriter, request) test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), `{"type": "`+probs.ErrorNS+`malformed","detail": "Invalid status value","status": 400}`) responseWriter.Body.Reset() payload = `{"status":"deactivated"}` - _, _, body = signer.byKeyID(1, nil, "http://localhost/1", payload) - request = makePostRequestWithPath("1", body) + _, _, body = signer.byKeyID(1, nil, "http://localhost/1/1", payload) + request = makePostRequestWithPath("1/1", body) - wfe.Authorization(ctx, newRequestEvent(), responseWriter, request) + wfe.AuthorizationHandler(ctx, newRequestEvent(), responseWriter, request) test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), `{ @@ -2369,10 +2620,10 @@ func TestDeactivateAuthorization(t *testing.T) { "expires": "2070-01-01T00:00:00Z", "challenges": [ { - "status": "pending", - "type": "dns", - "token":"token", - "url": "http://localhost/acme/chall-v3/1/-ZfxEw" + "status": "valid", + "type": "http-01", + "token": "token", + "url": "http://localhost/acme/chall/1/1/7TyhFQ" } ] }`) @@ -2392,7 +2643,7 @@ func TestDeactivateAccount(t *testing.T) { wfe.Account(ctx, newRequestEvent(), responseWriter, request) test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), - `{"type": "`+probs.ErrorNS+`malformed","detail": "Invalid value provided for status field","status": 400}`) + `{"type": "`+probs.ErrorNS+`malformed","detail": "Unable to update account :: invalid status \"asd\" for account update request, must be \"valid\" or \"deactivated\"","status": 400}`) responseWriter.Body.Reset() payload = `{"status":"deactivated"}` @@ -2408,10 +2659,6 @@ func TestDeactivateAccount(t *testing.T) { "n": "yNWVhtYEKJR21y9xsHV-PD_bYwbXSeNuFal46xYxVfRL5mqha7vttvjB_vc7Xg2RvgCxHPCqoxgMPTzHrZT75LjCwIW2K_klBYN8oYvTwwmeSkAz6ut7ZxPv-nZaT5TJhGk0NT2kh_zSpdriEJ_3vW-mqxYbbBmpvHqsa1_zx9fSuHYctAZJWzxzUZXykbWMWQZpEiE0J4ajj51fInEzVn7VxV-mzfMyboQjujPh7aNJxAWSq4oQEJJDgWwSh9leyoJoPpONHxh5nEE5AjE01FkGICSxjpZsF-w8hOTI3XXohUdu29Se26k2B0PolDSuj0GIQU6-W9TdLXSjBb2SpQ", "e": "AQAB" }, - "contact": [ - "mailto:person@mail.com" - ], - "initialIp": "", "status": "deactivated" }`) @@ -2428,10 +2675,6 @@ func TestDeactivateAccount(t *testing.T) { "n": "yNWVhtYEKJR21y9xsHV-PD_bYwbXSeNuFal46xYxVfRL5mqha7vttvjB_vc7Xg2RvgCxHPCqoxgMPTzHrZT75LjCwIW2K_klBYN8oYvTwwmeSkAz6ut7ZxPv-nZaT5TJhGk0NT2kh_zSpdriEJ_3vW-mqxYbbBmpvHqsa1_zx9fSuHYctAZJWzxzUZXykbWMWQZpEiE0J4ajj51fInEzVn7VxV-mzfMyboQjujPh7aNJxAWSq4oQEJJDgWwSh9leyoJoPpONHxh5nEE5AjE01FkGICSxjpZsF-w8hOTI3XXohUdu29Se26k2B0PolDSuj0GIQU6-W9TdLXSjBb2SpQ", "e": "AQAB" }, - "contact": [ - "mailto:person@mail.com" - ], - "initialIp": "", "status": "deactivated" }`) @@ -2452,7 +2695,7 @@ func TestDeactivateAccount(t *testing.T) { responseWriter.Body.String(), `{ "type": "`+probs.ErrorNS+`unauthorized", - "detail": "Account is not valid, has status \"deactivated\"", + "detail": "Unable to validate JWS :: Account is not valid, has status \"deactivated\"", "status": 403 }`) } @@ -2465,7 +2708,7 @@ func TestNewOrder(t *testing.T) { targetPath := "new-order" signedURL := fmt.Sprintf("http://%s/%s", targetHost, targetPath) - nonDNSIdentifierBody := ` + invalidIdentifierBody := ` { "Identifiers": [ {"type": "dns", "value": "not-example.com"}, @@ -2479,7 +2722,8 @@ func TestNewOrder(t *testing.T) { { "Identifiers": [ {"type": "dns", "value": "not-example.com"}, - {"type": "dns", "value": "www.not-example.com"} + {"type": "dns", "value": "www.not-example.com"}, + {"type": "ip", "value": "9.9.9.9"} ] }` @@ -2487,7 +2731,8 @@ func TestNewOrder(t *testing.T) { { "Identifiers": [ {"type": "dns", "value": "Not-Example.com"}, - {"type": "dns", "value": "WWW.Not-example.com"} + {"type": "dns", "value": "WWW.Not-example.com"}, + {"type": "ip", "value": "9.9.9.9"} ] }` @@ -2531,37 +2776,47 @@ func TestNewOrder(t *testing.T) { "Content-Type": {expectedJWSContentType}, }, }, - ExpectedBody: `{"type":"` + probs.ErrorNS + `malformed","detail":"No body on POST","status":400}`, + ExpectedBody: `{"type":"` + probs.ErrorNS + `malformed","detail":"Unable to validate JWS :: No body on POST","status":400}`, }, { Name: "POST, with an invalid JWS body", Request: makePostRequestWithPath("hi", "hi"), - ExpectedBody: `{"type":"` + probs.ErrorNS + `malformed","detail":"Parse error reading JWS","status":400}`, + ExpectedBody: `{"type":"` + probs.ErrorNS + `malformed","detail":"Unable to validate JWS :: Parse error reading JWS","status":400}`, }, { Name: "POST, properly signed JWS, payload isn't valid", Request: signAndPost(signer, targetPath, signedURL, "foo"), - ExpectedBody: `{"type":"` + probs.ErrorNS + `malformed","detail":"Request payload did not parse as JSON","status":400}`, + ExpectedBody: `{"type":"` + probs.ErrorNS + `malformed","detail":"Unable to validate JWS :: Request payload did not parse as JSON","status":400}`, }, { - Name: "POST, empty domain name identifier", + Name: "POST, empty DNS identifier", Request: signAndPost(signer, targetPath, signedURL, `{"identifiers":[{"type":"dns","value":""}]}`), - ExpectedBody: `{"type":"` + probs.ErrorNS + `malformed","detail":"NewOrder request included empty domain name","status":400}`, + ExpectedBody: `{"type":"` + probs.ErrorNS + `malformed","detail":"NewOrder request included empty identifier","status":400}`, }, { - Name: "POST, invalid domain name identifier", + Name: "POST, empty IP identifier", + Request: signAndPost(signer, targetPath, signedURL, `{"identifiers":[{"type":"ip","value":""}]}`), + ExpectedBody: `{"type":"` + probs.ErrorNS + `malformed","detail":"NewOrder request included empty identifier","status":400}`, + }, + { + Name: "POST, invalid DNS identifier", Request: signAndPost(signer, targetPath, signedURL, `{"identifiers":[{"type":"dns","value":"example.invalid"}]}`), ExpectedBody: `{"type":"` + probs.ErrorNS + `rejectedIdentifier","detail":"Invalid identifiers requested :: Cannot issue for \"example.invalid\": Domain name does not end with a valid public suffix (TLD)","status":400}`, }, + { + Name: "POST, invalid IP identifier", + Request: signAndPost(signer, targetPath, signedURL, `{"identifiers":[{"type":"ip","value":"127.0.0.0.0.0.0.1"}]}`), + ExpectedBody: `{"type":"` + probs.ErrorNS + `rejectedIdentifier","detail":"Invalid identifiers requested :: Cannot issue for \"127.0.0.0.0.0.0.1\": IP address is invalid","status":400}`, + }, { Name: "POST, no identifiers in payload", Request: signAndPost(signer, targetPath, signedURL, "{}"), ExpectedBody: `{"type":"` + probs.ErrorNS + `malformed","detail":"NewOrder request did not specify any identifiers","status":400}`, }, { - Name: "POST, non-DNS identifier in payload", - Request: signAndPost(signer, targetPath, signedURL, nonDNSIdentifierBody), - ExpectedBody: `{"type":"` + probs.ErrorNS + `unsupportedIdentifier","detail":"NewOrder request included invalid non-DNS type identifier: type \"fakeID\", value \"www.i-am-21.com\"","status":400}`, + Name: "POST, invalid identifier type in payload", + Request: signAndPost(signer, targetPath, signedURL, invalidIdentifierBody), + ExpectedBody: `{"type":"` + probs.ErrorNS + `unsupportedIdentifier","detail":"NewOrder request included unsupported identifier: type \"fakeID\", value \"www.i-am-21.com\"","status":400}`, }, { Name: "POST, notAfter and notBefore in payload", @@ -2579,7 +2834,7 @@ func TestNewOrder(t *testing.T) { { "type": "dns", "value": "thisreallylongexampledomainisabytelongerthanthemaxcnbytelimit.com"} ], "authorizations": [ - "http://localhost/acme/authz-v3/1" + "http://localhost/acme/authz/1/1" ], "finalize": "http://localhost/acme/finalize/1/1" }`, @@ -2596,7 +2851,7 @@ func TestNewOrder(t *testing.T) { { "type": "dns", "value": "thisreallylongexampledomainisabytelongerthanthemaxcnbytelimit.com"} ], "authorizations": [ - "http://localhost/acme/authz-v3/1" + "http://localhost/acme/authz/1/1" ], "finalize": "http://localhost/acme/finalize/1/1" }`, @@ -2610,10 +2865,11 @@ func TestNewOrder(t *testing.T) { "expires": "2021-02-01T01:01:01Z", "identifiers": [ { "type": "dns", "value": "not-example.com"}, - { "type": "dns", "value": "www.not-example.com"} + { "type": "dns", "value": "www.not-example.com"}, + { "type": "ip", "value": "9.9.9.9"} ], "authorizations": [ - "http://localhost/acme/authz-v3/1" + "http://localhost/acme/authz/1/1" ], "finalize": "http://localhost/acme/finalize/1/1" }`, @@ -2627,10 +2883,11 @@ func TestNewOrder(t *testing.T) { "expires": "2021-02-01T01:01:01Z", "identifiers": [ { "type": "dns", "value": "not-example.com"}, - { "type": "dns", "value": "www.not-example.com"} + { "type": "dns", "value": "www.not-example.com"}, + { "type": "ip", "value": "9.9.9.9"} ], "authorizations": [ - "http://localhost/acme/authz-v3/1" + "http://localhost/acme/authz/1/1" ], "finalize": "http://localhost/acme/finalize/1/1" }`, @@ -2694,17 +2951,17 @@ func TestFinalizeOrder(t *testing.T) { "Content-Type": {expectedJWSContentType}, }, }, - ExpectedBody: `{"type":"` + probs.ErrorNS + `malformed","detail":"No body on POST","status":400}`, + ExpectedBody: `{"type":"` + probs.ErrorNS + `malformed","detail":"Unable to validate JWS :: No body on POST","status":400}`, }, { Name: "POST, with an invalid JWS body", Request: makePostRequestWithPath(targetPath, "hi"), - ExpectedBody: `{"type":"` + probs.ErrorNS + `malformed","detail":"Parse error reading JWS","status":400}`, + ExpectedBody: `{"type":"` + probs.ErrorNS + `malformed","detail":"Unable to validate JWS :: Parse error reading JWS","status":400}`, }, { Name: "POST, properly signed JWS, payload isn't valid", Request: signAndPost(signer, targetPath, signedURL, "foo"), - ExpectedBody: `{"type":"` + probs.ErrorNS + `malformed","detail":"Request payload did not parse as JSON","status":400}`, + ExpectedBody: `{"type":"` + probs.ErrorNS + `malformed","detail":"Unable to validate JWS :: Request payload did not parse as JSON","status":400}`, }, { Name: "Invalid path", @@ -2724,7 +2981,7 @@ func TestFinalizeOrder(t *testing.T) { // stripped by the global WFE2 handler. We need the JWS URL to match the request // URL so we fudge both such that the finalize-order prefix has been removed. Request: signAndPost(signer, "2/1", "http://localhost/2/1", "{}"), - ExpectedBody: `{"type":"` + probs.ErrorNS + `malformed","detail":"No order found for account ID 2","status":404}`, + ExpectedBody: `{"type":"` + probs.ErrorNS + `malformed","detail":"Mismatched account ID","status":400}`, }, { Name: "Order ID is invalid", @@ -2768,8 +3025,9 @@ func TestFinalizeOrder(t *testing.T) { "identifiers": [ {"type":"dns","value":"example.com"} ], + "profile": "default", "authorizations": [ - "http://localhost/acme/authz-v3/1" + "http://localhost/acme/authz/1/1" ], "finalize": "http://localhost/acme/finalize/1/8" }`, @@ -2786,8 +3044,7 @@ func TestFinalizeOrder(t *testing.T) { t.Errorf("Header %q: Expected %q, got %q", k, v, got) } } - test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), - tc.ExpectedBody) + test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), tc.ExpectedBody) }) } @@ -2821,7 +3078,7 @@ func TestKeyRollover(t *testing.T) { responseWriter.Body.String(), `{ "type": "`+probs.ErrorNS+`malformed", - "detail": "Parse error reading JWS", + "detail": "Unable to validate JWS :: Parse error reading JWS", "status": 400 }`) @@ -2848,7 +3105,7 @@ func TestKeyRollover(t *testing.T) { Payload: `{"oldKey":` + string(newJWKJSON) + `,"account":"http://localhost/acme/acct/1"}`, ExpectedResponse: `{ "type": "` + probs.ErrorNS + `malformed", - "detail": "Inner JWS does not contain old key field matching current account key", + "detail": "Unable to validate JWS :: Inner JWS does not contain old key field matching current account key", "status": 400 }`, NewKey: newKeyPriv, @@ -2869,10 +3126,6 @@ func TestKeyRollover(t *testing.T) { Payload: `{"oldKey":` + test1KeyPublicJSON + `,"account":"http://localhost/acme/acct/1"}`, ExpectedResponse: `{ "key": ` + string(newJWKJSON) + `, - "contact": [ - "mailto:person@mail.com" - ], - "initialIp": "", "status": "valid" }`, NewKey: newKeyPriv, @@ -2912,7 +3165,7 @@ func TestKeyRolloverMismatchedJWSURLs(t *testing.T) { test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), ` { "type": "urn:ietf:params:acme:error:malformed", - "detail": "Outer JWS 'url' value \"http://localhost/key-change\" does not match inner JWS 'url' value \"http://localhost/wrong-url\"", + "detail": "Unable to validate JWS :: Outer JWS 'url' value \"http://localhost/key-change\" does not match inner JWS 'url' value \"http://localhost/wrong-url\"", "status": 400 }`) } @@ -2934,12 +3187,11 @@ func TestGetOrder(t *testing.T) { Request *http.Request Response string Headers map[string]string - Endpoint string }{ { Name: "Good request", Request: makeGet("1/1"), - Response: `{"status": "valid","expires": "2000-01-01T00:00:00Z","identifiers":[{"type":"dns", "value":"example.com"}], "authorizations":["http://localhost/acme/authz-v3/1"],"finalize":"http://localhost/acme/finalize/1/1","certificate":"http://localhost/acme/cert/serial"}`, + Response: `{"status": "valid","expires": "2000-01-01T00:00:00Z","identifiers":[{"type":"dns", "value":"example.com"}], "profile": "default", "authorizations":["http://localhost/acme/authz/1/1"],"finalize":"http://localhost/acme/finalize/1/1","certificate":"http://localhost/acme/cert/serial"}`, }, { Name: "404 request", @@ -2974,7 +3226,7 @@ func TestGetOrder(t *testing.T) { { Name: "Invalid POST-as-GET", Request: makePost(1, "1/1", "{}"), - Response: `{"type":"` + probs.ErrorNS + `malformed","detail":"POST-as-GET requests must have an empty payload", "status":400}`, + Response: `{"type":"` + probs.ErrorNS + `malformed","detail":"Unable to validate JWS :: POST-as-GET requests must have an empty payload", "status":400}`, }, { Name: "Valid POST-as-GET, wrong account", @@ -2984,28 +3236,22 @@ func TestGetOrder(t *testing.T) { { Name: "Valid POST-as-GET", Request: makePost(1, "1/1", ""), - Response: `{"status": "valid","expires": "2000-01-01T00:00:00Z","identifiers":[{"type":"dns", "value":"example.com"}], "authorizations":["http://localhost/acme/authz-v3/1"],"finalize":"http://localhost/acme/finalize/1/1","certificate":"http://localhost/acme/cert/serial"}`, - }, - { - Name: "GET new order", - Request: makeGet("1/9"), - Response: `{"type":"` + probs.ErrorNS + `unauthorized","detail":"Order is too new for GET API. You should only use this non-standard API to access resources created more than 10s ago","status":403}`, - Endpoint: "/get/order/", + Response: `{"status": "valid","expires": "2000-01-01T00:00:00Z","identifiers":[{"type":"dns", "value":"example.com"}], "profile": "default", "authorizations":["http://localhost/acme/authz/1/1"],"finalize":"http://localhost/acme/finalize/1/1","certificate":"http://localhost/acme/cert/serial"}`, }, { Name: "GET new order from old endpoint", Request: makeGet("1/9"), - Response: `{"status": "valid","expires": "2000-01-01T00:00:00Z","identifiers":[{"type":"dns", "value":"example.com"}], "authorizations":["http://localhost/acme/authz-v3/1"],"finalize":"http://localhost/acme/finalize/1/9","certificate":"http://localhost/acme/cert/serial"}`, + Response: `{"status": "valid","expires": "2000-01-01T00:00:00Z","identifiers":[{"type":"dns", "value":"example.com"}], "profile": "default", "authorizations":["http://localhost/acme/authz/1/1"],"finalize":"http://localhost/acme/finalize/1/9","certificate":"http://localhost/acme/cert/serial"}`, }, { Name: "POST-as-GET new order", Request: makePost(1, "1/9", ""), - Response: `{"status": "valid","expires": "2000-01-01T00:00:00Z","identifiers":[{"type":"dns", "value":"example.com"}], "authorizations":["http://localhost/acme/authz-v3/1"],"finalize":"http://localhost/acme/finalize/1/9","certificate":"http://localhost/acme/cert/serial"}`, + Response: `{"status": "valid","expires": "2000-01-01T00:00:00Z","identifiers":[{"type":"dns", "value":"example.com"}], "profile": "default", "authorizations":["http://localhost/acme/authz/1/1"],"finalize":"http://localhost/acme/finalize/1/9","certificate":"http://localhost/acme/cert/serial"}`, }, { Name: "POST-as-GET processing order", Request: makePost(1, "1/10", ""), - Response: `{"status": "processing","expires": "2000-01-01T00:00:00Z","identifiers":[{"type":"dns", "value":"example.com"}], "authorizations":["http://localhost/acme/authz-v3/1"],"finalize":"http://localhost/acme/finalize/1/10"}`, + Response: `{"status": "processing","expires": "2000-01-01T00:00:00Z","identifiers":[{"type":"dns", "value":"example.com"}], "profile": "default", "authorizations":["http://localhost/acme/authz/1/1"],"finalize":"http://localhost/acme/finalize/1/10"}`, Headers: map[string]string{"Retry-After": "3"}, }, } @@ -3013,11 +3259,10 @@ func TestGetOrder(t *testing.T) { for _, tc := range testCases { t.Run(tc.Name, func(t *testing.T) { responseWriter := httptest.NewRecorder() - if tc.Endpoint != "" { - wfe.GetOrder(ctx, &web.RequestEvent{Extra: make(map[string]interface{}), Endpoint: tc.Endpoint}, responseWriter, tc.Request) - } else { - wfe.GetOrder(ctx, newRequestEvent(), responseWriter, tc.Request) - } + wfe.GetOrder(ctx, newRequestEvent(), responseWriter, tc.Request) + t.Log(tc.Name) + t.Log("actual:", responseWriter.Body.String()) + t.Log("expect:", tc.Response) test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), tc.Response) for k, v := range tc.Headers { test.AssertEquals(t, responseWriter.Header().Get(k), v) @@ -3137,7 +3382,7 @@ func TestRevokeCertificateNotIssued(t *testing.T) { makePostRequestWithPath("revoke-cert", jwsBody)) // It should result in a 404 response with a problem body test.AssertEquals(t, responseWriter.Code, 404) - test.AssertEquals(t, responseWriter.Body.String(), "{\n \"type\": \"urn:ietf:params:acme:error:malformed\",\n \"detail\": \"Certificate from unrecognized issuer\",\n \"status\": 404\n}") + test.AssertEquals(t, responseWriter.Body.String(), "{\n \"type\": \"urn:ietf:params:acme:error:malformed\",\n \"detail\": \"Unable to revoke :: Certificate from unrecognized issuer\",\n \"status\": 404\n}") } func TestRevokeCertificateExpired(t *testing.T) { @@ -3162,7 +3407,7 @@ func TestRevokeCertificateExpired(t *testing.T) { wfe.RevokeCertificate(ctx, newRequestEvent(), responseWriter, makePostRequestWithPath("revoke-cert", jwsBody)) test.AssertEquals(t, responseWriter.Code, 403) - test.AssertEquals(t, responseWriter.Body.String(), "{\n \"type\": \"urn:ietf:params:acme:error:unauthorized\",\n \"detail\": \"Certificate is expired\",\n \"status\": 403\n}") + test.AssertEquals(t, responseWriter.Body.String(), "{\n \"type\": \"urn:ietf:params:acme:error:unauthorized\",\n \"detail\": \"Unable to revoke :: Certificate is expired\",\n \"status\": 403\n}") } func TestRevokeCertificateReasons(t *testing.T) { @@ -3197,13 +3442,13 @@ func TestRevokeCertificateReasons(t *testing.T) { Name: "Unsupported reason", Reason: &reason2, ExpectedHTTPCode: http.StatusBadRequest, - ExpectedBody: `{"type":"` + probs.ErrorNS + `badRevocationReason","detail":"unsupported revocation reason code provided: cACompromise (2). Supported reasons: unspecified (0), keyCompromise (1), superseded (4), cessationOfOperation (5)","status":400}`, + ExpectedBody: `{"type":"` + probs.ErrorNS + `badRevocationReason","detail":"Unable to revoke :: disallowed revocation reason: 2","status":400}`, }, { Name: "Non-existent reason", Reason: &reason100, ExpectedHTTPCode: http.StatusBadRequest, - ExpectedBody: `{"type":"` + probs.ErrorNS + `badRevocationReason","detail":"unsupported revocation reason code provided: unknown (100). Supported reasons: unspecified (0), keyCompromise (1), superseded (4), cessationOfOperation (5)","status":400}`, + ExpectedBody: `{"type":"` + probs.ErrorNS + `badRevocationReason","detail":"Unable to revoke :: disallowed revocation reason: 100","status":400}`, }, } @@ -3249,7 +3494,7 @@ func TestRevokeCertificateWrongCertificateKey(t *testing.T) { makePostRequestWithPath("revoke-cert", jwsBody)) test.AssertEquals(t, responseWriter.Code, 403) test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), - `{"type":"`+probs.ErrorNS+`unauthorized","detail":"JWK embedded in revocation request must be the same public key as the cert to be revoked","status":403}`) + `{"type":"`+probs.ErrorNS+`unauthorized","detail":"Unable to revoke :: JWK embedded in revocation request must be the same public key as the cert to be revoked","status":403}`) } type mockSAGetRegByKeyFails struct { @@ -3325,36 +3570,119 @@ func TestNewAccountWhenGetRegByKeyNotFound(t *testing.T) { } func TestPrepAuthzForDisplay(t *testing.T) { + t.Parallel() wfe, _, _ := setupWFE(t) - // Make an authz for a wildcard identifier authz := &core.Authorization{ ID: "12345", Status: core.StatusPending, RegistrationID: 1, - Identifier: identifier.DNSIdentifier("*.example.com"), + Identifier: identifier.NewDNS("example.com"), Challenges: []core.Challenge{ - { - Type: "dns", - ProvidedKeyAuthorization: " 🔑", - }, + {Type: core.ChallengeTypeDNS01, Status: core.StatusPending, Token: "token"}, + {Type: core.ChallengeTypeHTTP01, Status: core.StatusPending, Token: "token"}, + {Type: core.ChallengeTypeTLSALPN01, Status: core.StatusPending, Token: "token"}, }, } - // Prep the wildcard authz for display + // This modifies the authz in-place. wfe.prepAuthorizationForDisplay(&http.Request{Host: "localhost"}, authz) - // The authz should not have a wildcard prefix in the identifier value + // Ensure ID and RegID are omitted. + authzJSON, err := json.Marshal(authz) + test.AssertNotError(t, err, "Failed to marshal authz") + test.AssertNotContains(t, string(authzJSON), "\"id\":\"12345\"") + test.AssertNotContains(t, string(authzJSON), "\"registrationID\":\"1\"") +} + +func TestPrepRevokedAuthzForDisplay(t *testing.T) { + t.Parallel() + wfe, _, _ := setupWFE(t) + + authz := &core.Authorization{ + ID: "12345", + Status: core.StatusInvalid, + RegistrationID: 1, + Identifier: identifier.NewDNS("example.com"), + Challenges: []core.Challenge{ + {Type: core.ChallengeTypeDNS01, Status: core.StatusPending, Token: "token"}, + {Type: core.ChallengeTypeHTTP01, Status: core.StatusPending, Token: "token"}, + {Type: core.ChallengeTypeTLSALPN01, Status: core.StatusPending, Token: "token"}, + }, + } + + // This modifies the authz in-place. + wfe.prepAuthorizationForDisplay(&http.Request{Host: "localhost"}, authz) + + // All of the challenges should be revoked as well. + for _, chall := range authz.Challenges { + test.AssertEquals(t, chall.Status, core.StatusInvalid) + } +} + +func TestPrepWildcardAuthzForDisplay(t *testing.T) { + t.Parallel() + wfe, _, _ := setupWFE(t) + + authz := &core.Authorization{ + ID: "12345", + Status: core.StatusPending, + RegistrationID: 1, + Identifier: identifier.NewDNS("*.example.com"), + Challenges: []core.Challenge{ + {Type: core.ChallengeTypeDNS01, Status: core.StatusPending, Token: "token"}, + }, + } + + // This modifies the authz in-place. + wfe.prepAuthorizationForDisplay(&http.Request{Host: "localhost"}, authz) + + // The identifier should not start with a star, but the authz should be marked + // as a wildcard. test.AssertEquals(t, strings.HasPrefix(authz.Identifier.Value, "*."), false) - // The authz should be marked as corresponding to a wildcard name test.AssertEquals(t, authz.Wildcard, true) +} - // We expect the authz challenge has its URL set and the URI emptied. - authz.ID = "12345" - wfe.prepAuthorizationForDisplay(&http.Request{Host: "localhost"}, authz) - chal := authz.Challenges[0] - test.AssertEquals(t, chal.URL, "http://localhost/acme/chall-v3/12345/po1V2w") - test.AssertEquals(t, chal.ProvidedKeyAuthorization, "") +func TestPrepAuthzForDisplayShuffle(t *testing.T) { + t.Parallel() + wfe, _, _ := setupWFE(t) + + authz := &core.Authorization{ + ID: "12345", + Status: core.StatusPending, + RegistrationID: 1, + Identifier: identifier.NewDNS("example.com"), + Challenges: []core.Challenge{ + {Type: core.ChallengeTypeDNS01, Status: core.StatusPending, Token: "token"}, + {Type: core.ChallengeTypeHTTP01, Status: core.StatusPending, Token: "token"}, + {Type: core.ChallengeTypeTLSALPN01, Status: core.StatusPending, Token: "token"}, + }, + } + + // The challenges should be presented in an unpredictable order. + + // Create a structure to count how many times each challenge type ends up in + // each position in the output authz.Challenges list. + counts := make(map[core.AcmeChallenge]map[int]int) + counts[core.ChallengeTypeDNS01] = map[int]int{0: 0, 1: 0, 2: 0} + counts[core.ChallengeTypeHTTP01] = map[int]int{0: 0, 1: 0, 2: 0} + counts[core.ChallengeTypeTLSALPN01] = map[int]int{0: 0, 1: 0, 2: 0} + + // Prep the authz 100 times, and count where each challenge ended up each time. + for range 100 { + // This modifies the authz in place + wfe.prepAuthorizationForDisplay(&http.Request{Host: "localhost"}, authz) + for i, chall := range authz.Challenges { + counts[chall.Type][i] += 1 + } + } + + // Ensure that at least some amount of randomization is happening. + for challType, indices := range counts { + for index, count := range indices { + test.Assert(t, count > 10, fmt.Sprintf("challenge type %s did not appear in position %d as often as expected", challType, index)) + } + } } // noSCTMockRA is a mock RA that always returns a `berrors.MissingSCTsError` from `FinalizeOrder` @@ -3400,36 +3728,17 @@ func TestOrderToOrderJSONV2Authorizations(t *testing.T) { orderJSON := wfe.orderToOrderJSON(&http.Request{}, &corepb.Order{ Id: 1, RegistrationID: 1, - Names: []string{"a"}, + Identifiers: []*corepb.Identifier{identifier.NewDNS("a").ToProto()}, Status: string(core.StatusPending), Expires: timestamppb.New(expires), V2Authorizations: []int64{1, 2}, }) test.AssertDeepEquals(t, orderJSON.Authorizations, []string{ - "http://localhost/acme/authz-v3/1", - "http://localhost/acme/authz-v3/2", + "http://localhost/acme/authz/1/1", + "http://localhost/acme/authz/1/2", }) } -func TestGetChallengeUpRel(t *testing.T) { - wfe, _, _ := setupWFE(t) - - challengeURL := "http://localhost/acme/chall-v3/1/-ZfxEw" - resp := httptest.NewRecorder() - - req, err := http.NewRequest("GET", challengeURL, nil) - test.AssertNotError(t, err, "Could not make NewRequest") - req.URL.Path = "1/-ZfxEw" - - wfe.Challenge(ctx, newRequestEvent(), resp, req) - test.AssertEquals(t, - resp.Code, - http.StatusOK) - test.AssertEquals(t, - resp.Header().Get("Link"), - `;rel="up"`) -} - func TestPrepAccountForDisplay(t *testing.T) { acct := &core.Registration{ ID: 1987, @@ -3445,84 +3754,6 @@ func TestPrepAccountForDisplay(t *testing.T) { test.AssertEquals(t, acct.ID, int64(0)) } -func TestGETAPIAuthz(t *testing.T) { - wfe, _, _ := setupWFE(t) - makeGet := func(path, endpoint string) (*http.Request, *web.RequestEvent) { - return &http.Request{URL: &url.URL{Path: path}, Method: "GET"}, - &web.RequestEvent{Endpoint: endpoint} - } - - testCases := []struct { - name string - path string - expectTooFreshErr bool - }{ - { - name: "fresh authz", - path: "1", - expectTooFreshErr: true, - }, - { - name: "old authz", - path: "2", - expectTooFreshErr: false, - }, - } - - tooFreshErr := `{"type":"` + probs.ErrorNS + `unauthorized","detail":"Authorization is too new for GET API. You should only use this non-standard API to access resources created more than 10s ago","status":403}` - for _, tc := range testCases { - responseWriter := httptest.NewRecorder() - req, logEvent := makeGet(tc.path, getAuthzPath) - wfe.Authorization(context.Background(), logEvent, responseWriter, req) - - if responseWriter.Code == http.StatusOK && tc.expectTooFreshErr { - t.Errorf("expected too fresh error, got http.StatusOK") - } else { - test.AssertEquals(t, responseWriter.Code, http.StatusForbidden) - test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), tooFreshErr) - } - } -} - -func TestGETAPIChallenge(t *testing.T) { - wfe, _, _ := setupWFE(t) - makeGet := func(path, endpoint string) (*http.Request, *web.RequestEvent) { - return &http.Request{URL: &url.URL{Path: path}, Method: "GET"}, - &web.RequestEvent{Endpoint: endpoint} - } - - testCases := []struct { - name string - path string - expectTooFreshErr bool - }{ - { - name: "fresh authz challenge", - path: "1/-ZfxEw", - expectTooFreshErr: true, - }, - { - name: "old authz challenge", - path: "2/-ZfxEw", - expectTooFreshErr: false, - }, - } - - tooFreshErr := `{"type":"` + probs.ErrorNS + `unauthorized","detail":"Authorization is too new for GET API. You should only use this non-standard API to access resources created more than 10s ago","status":403}` - for _, tc := range testCases { - responseWriter := httptest.NewRecorder() - req, logEvent := makeGet(tc.path, getAuthzPath) - wfe.Challenge(context.Background(), logEvent, responseWriter, req) - - if responseWriter.Code == http.StatusOK && tc.expectTooFreshErr { - t.Errorf("expected too fresh error, got http.StatusOK") - } else { - test.AssertEquals(t, responseWriter.Code, http.StatusForbidden) - test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), tooFreshErr) - } - } -} - // TestGet404 tests that a 404 is served and that the expected endpoint of // "/" is logged when an unknown path is requested. This will test the // codepath to the wfe.Index() handler which handles "/" and all non-api @@ -3664,21 +3895,8 @@ func TestIncidentARI(t *testing.T) { test.AssertEquals(t, ri.SuggestedWindow.End.After(ri.SuggestedWindow.Start), true) // The end of the window should also be in the past. test.AssertEquals(t, ri.SuggestedWindow.End.Before(wfe.clk.Now()), true) -} - -func TestOldTLSInbound(t *testing.T) { - wfe, _, _ := setupWFE(t) - req := &http.Request{ - URL: &url.URL{Path: "/directory"}, - Method: "GET", - Header: http.Header(map[string][]string{ - http.CanonicalHeaderKey("TLS-Version"): {"TLSv1"}, - }), - } - - responseWriter := httptest.NewRecorder() - wfe.Handler(metrics.NoopRegisterer).ServeHTTP(responseWriter, req) - test.AssertEquals(t, responseWriter.Code, http.StatusBadRequest) + // The explanationURL should be set. + test.AssertEquals(t, ri.ExplanationURL, "http://big.bad/incident") } func Test_sendError(t *testing.T) { @@ -3712,20 +3930,39 @@ func Test_sendError(t *testing.T) { test.AssertEquals(t, testResponse.Header().Get("Link"), "") } -type mockSA struct { +func Test_sendErrorInternalServerError(t *testing.T) { + features.Reset() + wfe, _, _ := setupWFE(t) + testResponse := httptest.NewRecorder() + + wfe.sendError(testResponse, &web.RequestEvent{}, probs.ServerInternal("oh no"), nil) + test.AssertEquals(t, testResponse.Header().Get("Retry-After"), "60") +} + +// mockSAForARI provides a mock SA with the methods required for an issuance and +// a renewal with the ARI `Replaces` field. +// +// Note that FQDNSetTimestampsForWindow always return an empty list, which allows us to act +// as if a certificate is not getting the renewal exemption, even when we are repeatedly +// issuing for the same names. +type mockSAForARI struct { sapb.StorageAuthorityReadOnlyClient cert *corepb.Certificate } +func (sa *mockSAForARI) FQDNSetTimestampsForWindow(ctx context.Context, in *sapb.CountFQDNSetsRequest, opts ...grpc.CallOption) (*sapb.Timestamps, error) { + return &sapb.Timestamps{Timestamps: nil}, nil +} + // GetCertificate returns the inner certificate if it matches the given serial. -func (sa *mockSA) GetCertificate(ctx context.Context, req *sapb.Serial, _ ...grpc.CallOption) (*corepb.Certificate, error) { +func (sa *mockSAForARI) GetCertificate(ctx context.Context, req *sapb.Serial, _ ...grpc.CallOption) (*corepb.Certificate, error) { if req.Serial == sa.cert.Serial { return sa.cert, nil } return nil, berrors.NotFoundError("certificate with serial %q not found", req.Serial) } -func (sa *mockSA) ReplacementOrderExists(ctx context.Context, in *sapb.Serial, opts ...grpc.CallOption) (*sapb.Exists, error) { +func (sa *mockSAForARI) ReplacementOrderExists(ctx context.Context, in *sapb.Serial, opts ...grpc.CallOption) (*sapb.Exists, error) { if in.Serial == sa.cert.Serial { return &sapb.Exists{Exists: false}, nil @@ -3733,11 +3970,11 @@ func (sa *mockSA) ReplacementOrderExists(ctx context.Context, in *sapb.Serial, o return &sapb.Exists{Exists: true}, nil } -func (sa *mockSA) IncidentsForSerial(ctx context.Context, in *sapb.Serial, opts ...grpc.CallOption) (*sapb.Incidents, error) { +func (sa *mockSAForARI) IncidentsForSerial(ctx context.Context, in *sapb.Serial, opts ...grpc.CallOption) (*sapb.Incidents, error) { return &sapb.Incidents{}, nil } -func (sa *mockSA) GetCertificateStatus(ctx context.Context, in *sapb.Serial, opts ...grpc.CallOption) (*corepb.CertificateStatus, error) { +func (sa *mockSAForARI) GetCertificateStatus(ctx context.Context, in *sapb.Serial, opts ...grpc.CallOption) (*corepb.CertificateStatus, error) { return &corepb.CertificateStatus{Serial: in.Serial, Status: string(core.OCSPStatusGood)}, nil } @@ -3755,7 +3992,7 @@ func TestOrderMatchesReplacement(t *testing.T) { mockDer, err := x509.CreateCertificate(rand.Reader, &rawCert, &rawCert, &testKey.PublicKey, testKey) test.AssertNotError(t, err, "failed to create test certificate") - wfe.sa = &mockSA{ + wfe.sa = &mockSAForARI{ cert: &corepb.Certificate{ RegistrationID: 1, Serial: expectSerial.String(), @@ -3764,23 +4001,23 @@ func TestOrderMatchesReplacement(t *testing.T) { } // Working with a single matching identifier. - err = wfe.orderMatchesReplacement(context.Background(), &core.Registration{ID: 1}, []string{"example.com"}, expectSerial.String()) + err = wfe.orderMatchesReplacement(context.Background(), &core.Registration{ID: 1}, identifier.ACMEIdentifiers{identifier.NewDNS("example.com")}, expectSerial.String()) test.AssertNotError(t, err, "failed to check order is replacement") // Working with a different matching identifier. - err = wfe.orderMatchesReplacement(context.Background(), &core.Registration{ID: 1}, []string{"example-a.com"}, expectSerial.String()) + err = wfe.orderMatchesReplacement(context.Background(), &core.Registration{ID: 1}, identifier.ACMEIdentifiers{identifier.NewDNS("example-a.com")}, expectSerial.String()) test.AssertNotError(t, err, "failed to check order is replacement") // No matching identifiers. - err = wfe.orderMatchesReplacement(context.Background(), &core.Registration{ID: 1}, []string{"example-b.com"}, expectSerial.String()) + err = wfe.orderMatchesReplacement(context.Background(), &core.Registration{ID: 1}, identifier.ACMEIdentifiers{identifier.NewDNS("example-b.com")}, expectSerial.String()) test.AssertErrorIs(t, err, berrors.Malformed) // RegID for predecessor order does not match. - err = wfe.orderMatchesReplacement(context.Background(), &core.Registration{ID: 2}, []string{"example.com"}, expectSerial.String()) + err = wfe.orderMatchesReplacement(context.Background(), &core.Registration{ID: 2}, identifier.ACMEIdentifiers{identifier.NewDNS("example.com")}, expectSerial.String()) test.AssertErrorIs(t, err, berrors.Unauthorized) // Predecessor certificate not found. - err = wfe.orderMatchesReplacement(context.Background(), &core.Registration{ID: 1}, []string{"example.com"}, "1") + err = wfe.orderMatchesReplacement(context.Background(), &core.Registration{ID: 1}, identifier.ACMEIdentifiers{identifier.NewDNS("example.com")}, "1") test.AssertErrorIs(t, err, berrors.NotFound) } @@ -3802,7 +4039,7 @@ func (sa *mockRA) NewOrder(ctx context.Context, in *rapb.NewOrderRequest, opts . RegistrationID: 987654321, Created: timestamppb.New(created), Expires: timestamppb.New(exp), - Names: []string{"example.com"}, + Identifiers: []*corepb.Identifier{identifier.NewDNS("example.com").ToProto()}, Status: string(core.StatusValid), V2Authorizations: []int64{1}, CertificateSerial: "serial", @@ -3816,7 +4053,7 @@ func TestNewOrderWithProfile(t *testing.T) { expectProfileName := "test-profile" wfe.ra = &mockRA{expectProfileName: expectProfileName} mux := wfe.Handler(metrics.NoopRegisterer) - wfe.certificateProfileNames = []string{expectProfileName} + wfe.certProfiles = map[string]string{expectProfileName: "description"} // Test that the newOrder endpoint returns the proper error if an invalid // profile is specified. @@ -3835,8 +4072,8 @@ func TestNewOrderWithProfile(t *testing.T) { var errorResp map[string]interface{} err := json.Unmarshal(responseWriter.Body.Bytes(), &errorResp) test.AssertNotError(t, err, "Failed to unmarshal error response") - test.AssertEquals(t, errorResp["type"], "urn:ietf:params:acme:error:malformed") - test.AssertEquals(t, errorResp["detail"], "Invalid certificate profile, \"bad-profile\": not a recognized profile name") + test.AssertEquals(t, errorResp["type"], "urn:ietf:params:acme:error:invalidProfile") + test.AssertEquals(t, errorResp["detail"], "profile name \"bad-profile\" not recognized") // Test that the newOrder endpoint returns no error if the valid profile is specified. validOrderBody := ` @@ -3855,8 +4092,8 @@ func TestNewOrderWithProfile(t *testing.T) { test.AssertNotError(t, err, "Failed to unmarshal order response") test.AssertEquals(t, errorResp1["status"], "valid") - // Set the acceptable profiles to an empty list, the WFE should no longer accept any profiles. - wfe.certificateProfileNames = []string{} + // Set the acceptable profiles to the empty set, the WFE should no longer accept any profiles. + wfe.certProfiles = map[string]string{} responseWriter = httptest.NewRecorder() r = signAndPost(signer, newOrderPath, "http://localhost"+newOrderPath, validOrderBody) mux.ServeHTTP(responseWriter, r) @@ -3864,8 +4101,8 @@ func TestNewOrderWithProfile(t *testing.T) { var errorResp2 map[string]interface{} err = json.Unmarshal(responseWriter.Body.Bytes(), &errorResp2) test.AssertNotError(t, err, "Failed to unmarshal error response") - test.AssertEquals(t, errorResp2["type"], "urn:ietf:params:acme:error:malformed") - test.AssertEquals(t, errorResp2["detail"], "Invalid certificate profile, \"test-profile\": not a recognized profile name") + test.AssertEquals(t, errorResp2["type"], "urn:ietf:params:acme:error:invalidProfile") + test.AssertEquals(t, errorResp2["detail"], "profile name \"test-profile\" not recognized") } func makeARICertID(leaf *x509.Certificate) (string, error) { @@ -3899,22 +4136,22 @@ func makeARICertID(leaf *x509.Certificate) (string, error) { } func TestCountNewOrderWithReplaces(t *testing.T) { - wfe, _, signer := setupWFE(t) - features.Set(features.Config{TrackReplacementCertificatesARI: true}) + wfe, fc, signer := setupWFE(t) - expectExpiry := time.Now().AddDate(0, 0, 1) - var expectAKID []byte + // Pick a random issuer to "issue" expectCert. + var issuer *issuance.Certificate for _, v := range wfe.issuerCertificates { - expectAKID = v.SubjectKeyId + issuer = v break } - testKey, _ := rsa.GenerateKey(rand.Reader, 1024) + testKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) expectSerial := big.NewInt(1337) expectCert := &x509.Certificate{ - NotAfter: expectExpiry, + NotBefore: fc.Now(), + NotAfter: fc.Now().AddDate(0, 0, 90), DNSNames: []string{"example.com"}, SerialNumber: expectSerial, - AuthorityKeyId: expectAKID, + AuthorityKeyId: issuer.SubjectKeyId, } expectCertId, err := makeARICertID(expectCert) test.AssertNotError(t, err, "failed to create test cert id") @@ -3922,16 +4159,23 @@ func TestCountNewOrderWithReplaces(t *testing.T) { test.AssertNotError(t, err, "failed to create test certificate") // MockSA that returns the certificate with the expected serial. - wfe.sa = &mockSA{ + wfe.sa = &mockSAForARI{ cert: &corepb.Certificate{ RegistrationID: 1, Serial: core.SerialToString(expectSerial), Der: expectDer, + Issued: timestamppb.New(expectCert.NotBefore), + Expires: timestamppb.New(expectCert.NotAfter), }, } mux := wfe.Handler(metrics.NoopRegisterer) responseWriter := httptest.NewRecorder() + // Set the fake clock forward to 1s past the suggested renewal window start + // time. + renewalWindowStart := core.RenewalInfoSimple(expectCert.NotBefore, expectCert.NotAfter).SuggestedWindow.Start + fc.Set(renewalWindowStart.Add(time.Second)) + body := fmt.Sprintf(` { "Identifiers": [ @@ -3945,3 +4189,165 @@ func TestCountNewOrderWithReplaces(t *testing.T) { test.AssertEquals(t, responseWriter.Code, http.StatusCreated) test.AssertMetricWithLabelsEquals(t, wfe.stats.ariReplacementOrders, prometheus.Labels{"isReplacement": "true", "limitsExempt": "true"}, 1) } + +func TestNewOrderRateLimits(t *testing.T) { + wfe, fc, signer := setupWFE(t) + + // Set the default ratelimits to only allow one new order per account per 24 + // hours. + txnBuilder, err := ratelimits.NewTransactionBuilder(ratelimits.LimitConfigs{ + ratelimits.NewOrdersPerAccount.String(): &ratelimits.LimitConfig{ + Burst: 1, + Count: 1, + Period: config.Duration{Duration: time.Hour * 24}}, + }) + test.AssertNotError(t, err, "making transaction composer") + wfe.txnBuilder = txnBuilder + + // Pick a random issuer to "issue" extantCert. + var issuer *issuance.Certificate + for _, v := range wfe.issuerCertificates { + issuer = v + break + } + testKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + test.AssertNotError(t, err, "failed to create test key") + extantCert := &x509.Certificate{ + NotBefore: fc.Now(), + NotAfter: fc.Now().AddDate(0, 0, 90), + DNSNames: []string{"example.com"}, + SerialNumber: big.NewInt(1337), + AuthorityKeyId: issuer.SubjectKeyId, + } + extantCertId, err := makeARICertID(extantCert) + test.AssertNotError(t, err, "failed to create test cert id") + extantDer, err := x509.CreateCertificate(rand.Reader, extantCert, extantCert, &testKey.PublicKey, testKey) + test.AssertNotError(t, err, "failed to create test certificate") + + // Mock SA that returns the certificate with the expected serial. + wfe.sa = &mockSAForARI{ + cert: &corepb.Certificate{ + RegistrationID: 1, + Serial: core.SerialToString(extantCert.SerialNumber), + Der: extantDer, + Issued: timestamppb.New(extantCert.NotBefore), + Expires: timestamppb.New(extantCert.NotAfter), + }, + } + + // Set the fake clock forward to 1s past the suggested renewal window start + // time. + renewalWindowStart := core.RenewalInfoSimple(extantCert.NotBefore, extantCert.NotAfter).SuggestedWindow.Start + fc.Set(renewalWindowStart.Add(time.Second)) + + mux := wfe.Handler(metrics.NoopRegisterer) + + // Request the certificate for the first time. Because we mocked together + // the certificate, it will have been issued 60 days ago. + r := signAndPost(signer, newOrderPath, "http://localhost"+newOrderPath, + `{"Identifiers": [{"type": "dns", "value": "example.com"}]}`) + responseWriter := httptest.NewRecorder() + mux.ServeHTTP(responseWriter, r) + test.AssertEquals(t, responseWriter.Code, http.StatusCreated) + + // Request another, identical certificate. This should fail for violating + // the NewOrdersPerAccount rate limit. + r = signAndPost(signer, newOrderPath, "http://localhost"+newOrderPath, + `{"Identifiers": [{"type": "dns", "value": "example.com"}]}`) + responseWriter = httptest.NewRecorder() + mux.ServeHTTP(responseWriter, r) + features.Set(features.Config{ + UseKvLimitsForNewOrder: true, + }) + test.AssertEquals(t, responseWriter.Code, http.StatusTooManyRequests) + + // Make a request with the "Replaces" field, which should satisfy ARI checks + // and therefore bypass the rate limit. + r = signAndPost(signer, newOrderPath, "http://localhost"+newOrderPath, + fmt.Sprintf(`{"Identifiers": [{"type": "dns", "value": "example.com"}], "Replaces": %q}`, extantCertId)) + responseWriter = httptest.NewRecorder() + mux.ServeHTTP(responseWriter, r) + test.AssertEquals(t, responseWriter.Code, http.StatusCreated) +} + +func TestNewAccountCreatesContacts(t *testing.T) { + t.Parallel() + + key := loadKey(t, []byte(test2KeyPrivatePEM)) + _, ok := key.(*rsa.PrivateKey) + test.Assert(t, ok, "Couldn't load test2 key") + + path := newAcctPath + signedURL := fmt.Sprintf("http://localhost%s", path) + + testCases := []struct { + name string + contacts []string + expected []string + }{ + { + name: "No email", + contacts: []string{}, + expected: []string{}, + }, + { + name: "One email", + contacts: []string{"mailto:person@mail.com"}, + expected: []string{"person@mail.com"}, + }, + { + name: "Two emails", + contacts: []string{"mailto:person1@mail.com", "mailto:person2@mail.com"}, + expected: []string{"person1@mail.com", "person2@mail.com"}, + }, + { + name: "Invalid email", + contacts: []string{"mailto:lol@%mail.com"}, + expected: []string{}, + }, + { + name: "One valid email, one invalid email", + contacts: []string{"mailto:person@mail.com", "mailto:lol@%mail.com"}, + expected: []string{}, + }, + { + name: "Valid email with non-email prefix", + contacts: []string{"heliograph:person@mail.com"}, + expected: []string{}, + }, + { + name: "Non-email prefix with correct field signal instructions", + contacts: []string{`heliograph:STATION OF RECEPTION: High Ridge above Black Hollow, near Lone Pine. +AZIMUTH TO SIGNAL STATION: Due West, bearing Twin Peaks. +WATCH PERIOD: Third hour post-zenith; observation maintained for 30 minutes. +SIGNAL CODE: Standard Morse, three-flash attention signal. +ALTERNATE SITE: If no reply, move to Observation Point B at Broken Cairn.`}, + expected: []string{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + wfe, _, signer := setupWFE(t) + + mockPardotClient, mockImpl := mocks.NewMockPardotClientImpl() + wfe.ee = mocks.NewMockExporterImpl(mockPardotClient) + + contactsJSON, err := json.Marshal(tc.contacts) + test.AssertNotError(t, err, "Failed to marshal contacts") + + payload := fmt.Sprintf(`{"contact":%s,"termsOfServiceAgreed":true}`, contactsJSON) + _, _, body := signer.embeddedJWK(key, signedURL, payload) + request := makePostRequestWithPath(path, body) + + responseWriter := httptest.NewRecorder() + wfe.NewAccount(context.Background(), newRequestEvent(), responseWriter, request) + + for _, email := range tc.expected { + test.AssertSliceContains(t, mockImpl.GetCreatedContacts(), email) + } + }) + } +} diff --git a/third-party/golang.org/x/exp/LICENSE b/third-party/golang.org/x/exp/slices/LICENSE similarity index 92% rename from third-party/golang.org/x/exp/LICENSE rename to third-party/golang.org/x/exp/slices/LICENSE index 6a66aea5e..2a7cf70da 100644 --- a/third-party/golang.org/x/exp/LICENSE +++ b/third-party/golang.org/x/exp/slices/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2009 The Go Authors. All rights reserved. +Copyright 2009 The Go Authors. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are @@ -10,7 +10,7 @@ notice, this list of conditions and the following disclaimer. copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - * Neither the name of Google Inc. nor the names of its + * Neither the name of Google LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. diff --git a/third-party/k8s.io/klog/v2/LICENSE b/third-party/k8s.io/klog/v2/LICENSE deleted file mode 100644 index 37ec93a14..000000000 --- a/third-party/k8s.io/klog/v2/LICENSE +++ /dev/null @@ -1,191 +0,0 @@ -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, and -distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright -owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other entities -that control, are controlled by, or are under common control with that entity. -For the purposes of this definition, "control" means (i) the power, direct or -indirect, to cause the direction or management of such entity, whether by -contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the -outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising -permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, including -but not limited to software source code, documentation source, and configuration -files. - -"Object" form shall mean any form resulting from mechanical transformation or -translation of a Source form, including but not limited to compiled object code, -generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, made -available under the License, as indicated by a copyright notice that is included -in or attached to the work (an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object form, that -is based on (or derived from) the Work and for which the editorial revisions, -annotations, elaborations, or other modifications represent, as a whole, an -original work of authorship. For the purposes of this License, Derivative Works -shall not include works that remain separable from, or merely link (or bind by -name) to the interfaces of, the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including the original version -of the Work and any modifications or additions to that Work or Derivative Works -thereof, that is intentionally submitted to Licensor for inclusion in the Work -by the copyright owner or by an individual or Legal Entity authorized to submit -on behalf of the copyright owner. For the purposes of this definition, -"submitted" means any form of electronic, verbal, or written communication sent -to the Licensor or its representatives, including but not limited to -communication on electronic mailing lists, source code control systems, and -issue tracking systems that are managed by, or on behalf of, the Licensor for -the purpose of discussing and improving the Work, but excluding communication -that is conspicuously marked or otherwise designated in writing by the copyright -owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf -of whom a Contribution has been received by Licensor and subsequently -incorporated within the Work. - -2. Grant of Copyright License. - -Subject to the terms and conditions of this License, each Contributor hereby -grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, -irrevocable copyright license to reproduce, prepare Derivative Works of, -publicly display, publicly perform, sublicense, and distribute the Work and such -Derivative Works in Source or Object form. - -3. Grant of Patent License. - -Subject to the terms and conditions of this License, each Contributor hereby -grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, -irrevocable (except as stated in this section) patent license to make, have -made, use, offer to sell, sell, import, and otherwise transfer the Work, where -such license applies only to those patent claims licensable by such Contributor -that are necessarily infringed by their Contribution(s) alone or by combination -of their Contribution(s) with the Work to which such Contribution(s) was -submitted. If You institute patent litigation against any entity (including a -cross-claim or counterclaim in a lawsuit) alleging that the Work or a -Contribution incorporated within the Work constitutes direct or contributory -patent infringement, then any patent licenses granted to You under this License -for that Work shall terminate as of the date such litigation is filed. - -4. Redistribution. - -You may reproduce and distribute copies of the Work or Derivative Works thereof -in any medium, with or without modifications, and in Source or Object form, -provided that You meet the following conditions: - -You must give any other recipients of the Work or Derivative Works a copy of -this License; and -You must cause any modified files to carry prominent notices stating that You -changed the files; and -You must retain, in the Source form of any Derivative Works that You distribute, -all copyright, patent, trademark, and attribution notices from the Source form -of the Work, excluding those notices that do not pertain to any part of the -Derivative Works; and -If the Work includes a "NOTICE" text file as part of its distribution, then any -Derivative Works that You distribute must include a readable copy of the -attribution notices contained within such NOTICE file, excluding those notices -that do not pertain to any part of the Derivative Works, in at least one of the -following places: within a NOTICE text file distributed as part of the -Derivative Works; within the Source form or documentation, if provided along -with the Derivative Works; or, within a display generated by the Derivative -Works, if and wherever such third-party notices normally appear. The contents of -the NOTICE file are for informational purposes only and do not modify the -License. You may add Your own attribution notices within Derivative Works that -You distribute, alongside or as an addendum to the NOTICE text from the Work, -provided that such additional attribution notices cannot be construed as -modifying the License. -You may add Your own copyright statement to Your modifications and may provide -additional or different license terms and conditions for use, reproduction, or -distribution of Your modifications, or for any such Derivative Works as a whole, -provided Your use, reproduction, and distribution of the Work otherwise complies -with the conditions stated in this License. - -5. Submission of Contributions. - -Unless You explicitly state otherwise, any Contribution intentionally submitted -for inclusion in the Work by You to the Licensor shall be under the terms and -conditions of this License, without any additional terms or conditions. -Notwithstanding the above, nothing herein shall supersede or modify the terms of -any separate license agreement you may have executed with Licensor regarding -such Contributions. - -6. Trademarks. - -This License does not grant permission to use the trade names, trademarks, -service marks, or product names of the Licensor, except as required for -reasonable and customary use in describing the origin of the Work and -reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. - -Unless required by applicable law or agreed to in writing, Licensor provides the -Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, -including, without limitation, any warranties or conditions of TITLE, -NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are -solely responsible for determining the appropriateness of using or -redistributing the Work and assume any risks associated with Your exercise of -permissions under this License. - -8. Limitation of Liability. - -In no event and under no legal theory, whether in tort (including negligence), -contract, or otherwise, unless required by applicable law (such as deliberate -and grossly negligent acts) or agreed to in writing, shall any Contributor be -liable to You for damages, including any direct, indirect, special, incidental, -or consequential damages of any character arising as a result of this License or -out of the use or inability to use the Work (including but not limited to -damages for loss of goodwill, work stoppage, computer failure or malfunction, or -any and all other commercial damages or losses), even if such Contributor has -been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. - -While redistributing the Work or Derivative Works thereof, You may choose to -offer, and charge a fee for, acceptance of support, warranty, indemnity, or -other liability obligations and/or rights consistent with this License. However, -in accepting such obligations, You may act only on Your own behalf and on Your -sole responsibility, not on behalf of any other Contributor, and only if You -agree to indemnify, defend, and hold each Contributor harmless for any liability -incurred by, or claims asserted against, such Contributor by reason of your -accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work - -To apply the Apache License to your work, attach the following boilerplate -notice, with the fields enclosed by brackets "[]" replaced with your own -identifying information. (Don't include the brackets!) The text should be -enclosed in the appropriate comment syntax for the file format. We also -recommend that a file or class name and description of purpose be included on -the same "printed page" as the copyright notice for easier identification within -third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License.