Compare commits

...

312 Commits

Author SHA1 Message Date
Samantha Frank 8aafb31347
ratelimits: Small cleanup in transaction.go (#8275) 2025-06-26 17:43:02 -04:00
Aaron Gable 30eac83730
RFC 9773: Update ARI URL (#8274)
https://www.rfc-editor.org/rfc/rfc9773.html is no longer a draft; it
deserves a better-looking path!
2025-06-26 08:50:44 -07:00
Aaron Gable 4e74a25582
Restore TestAccountEmailError (#8273)
This integration test was removed in the early versions of
https://github.com/letsencrypt/boulder/pull/8245, because that PR had
removed all validation of contact addresses. However, later iterations
of that PR restored (most) contact validation, so this PR restores (most
of) the TestAccountEmailError integration test.
2025-06-25 16:35:52 -07:00
James Renken 21d022840b
Really fix GHA for IANA registries (#8271) 2025-06-25 15:58:44 -07:00
Aaron Gable e110ec9a03
Confine contact addresses to the WFE (#8245)
Change the WFE to stop populating the Contact field of the
NewRegistration requests it sends to the RA. Similarly change the WFE to
ignore the Contact field of any update-account requests it receives,
thereby removing all calls to the RA's UpdateRegistrationContact method.

Hoist the RA's contact validation logic into the WFE, so that we can
still return errors to clients which are presenting grossly malformed
contact fields, and have a first layer of protection against trying to
send malformed addresses to email-exporter.

A follow-up change (after a deploy cycle) will remove the deprecated RA
and SA methods.

Part of https://github.com/letsencrypt/boulder/issues/8199
2025-06-25 15:51:44 -07:00
James Renken ea23894910
Fix GHA for IANA registries (#8270)
Add `org: read` to the IANA GHA token's scope, so it can ask
boulder-developers for review.

Add a line break formatting change from IANA.
2025-06-25 13:30:47 -07:00
James Renken 9308392adf
iana: Embed & parse reserved IP registries from primary source (#8249)
Move `policy.IsReservedIP` to `iana.IsReservedAddr`.

Move `policy.IsReservedPrefix` to `iana.IsReservedPrefix`.

Embed & parse IANA's special-purpose address registries for IPv4 and
IPv6 in their original CSV format.

Fixes #8080
2025-06-25 12:05:25 -07:00
dependabot[bot] 901f2dba7c
build(deps): bump the aws group with 4 updates (#8263)
Bumps the aws group with 4 updates:
[github.com/aws/aws-sdk-go-v2](https://github.com/aws/aws-sdk-go-v2),
[github.com/aws/aws-sdk-go-v2/config](https://github.com/aws/aws-sdk-go-v2),
[github.com/aws/aws-sdk-go-v2/service/s3](https://github.com/aws/aws-sdk-go-v2)
and [github.com/aws/smithy-go](https://github.com/aws/smithy-go).

Updates `github.com/aws/aws-sdk-go-v2` from 1.36.4 to 1.36.5
Updates `github.com/aws/aws-sdk-go-v2/config` from 1.29.16 to 1.29.17
Updates `github.com/aws/aws-sdk-go-v2/service/s3` from 1.80.2 to 1.80.3
Updates `github.com/aws/smithy-go` from 1.22.2 to 1.22.4

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-25 14:08:41 -04:00
James Renken a29f2f37d6
va: Check for reserved IP addresses at dialer creation (#8257)
Fixes #8041
2025-06-25 10:09:47 -07:00
Aaron Gable c576a200d0
Remove id-kp-clientAuth from intermediate ceremony (#8265)
Fixes https://github.com/letsencrypt/boulder/issues/8264
2025-06-24 16:19:31 -07:00
Matthew McPherrin 5ddd5acf99
Print key hash as hex in admin tool. (#8266)
The ProtoText printing of this structure prints the binary string as
escaped
utf8 text, which is essentially gibberish for my processes.

---------

Co-authored-by: Aaron Gable <aaron@letsencrypt.org>
2025-06-23 17:36:06 -07:00
Jacob Hoffman-Andrews cd02caea99
Add verify-release-ancestry.sh (#8268)
And run it from the release workflow.
2025-06-23 17:22:47 -07:00
Samantha Frank ddc4c8683b
email-exporter: Don't waste limited attempts on cached entries (#8262)
Currently, we check the cache only immediately before attempting to send
an email address. However, we only reach that point if the rate limiter
(used to respect the daily API quota) permits it. As a result, around
40% of sends are wasted on email addresses that are ultimately skipped
due to cache hits.

Replace the pre-send cache `Seen` check with an atomic `StoreIfAbsent`
executed before the `limiter.Wait()` so that limiter tokens are consumed
only for email addresses that actually need sending. Skip the
`limiter.Wait()` on cache hits, remove cache entries only when a send
fails, and increment metrics only on successful sends.
2025-06-23 14:55:53 -07:00
Jacob Hoffman-Andrews f087d280be
Add a GitHub Action that only runs on main or hotfix (#8267)
It can be used by tag protection rules to ensure that tags may only be
pushed if their corresponding commit was first pushed to main or a
hotfix branch.
2025-06-23 12:16:01 -07:00
Samantha Frank 1bfc3186c8
grpc: Enable client-side health_v1 health checking (#8254)
- Configure all gRPC clients to check the overall serving status of each
endpoint via the `grpc_health_v1` service.
- Configure all gRPC servers to expose the `grpc_health_v1` service to
any client permitted to access one of the server’s services.
- Modify long-running, deep health checks to set and transition the
overall (empty string) health status of the gRPC server in addition to
the specific service they were configured for.

Fixes #8227
2025-06-18 10:37:20 -04:00
Aaron Gable b6c5ee69ed
Make ARI error messages clearer (#8260)
Fixes https://github.com/letsencrypt/boulder/issues/8259
2025-06-17 16:55:36 -07:00
Jacob Hoffman-Andrews 5ad5f85cfb
bdns: deprecate DOH feature flag (#8234)
Since the bdns unittests used a local DNS server via TCP, modify that
server to instead speak DoH.

Fixes #8120
2025-06-17 14:45:52 -07:00
Samantha Frank c97b312e65
integration: Move test_order_finalize_early to the Go tests (#8258)
Hyrum’s Law strikes again: our Python integration tests were implicitly
relying on behavior that was changed upstream in Certbot’s ACME client
(see https://github.com/certbot/certbot/pull/10239). To ensure continued
coverage, replicate this test in our Go integration test suite.
2025-06-17 17:19:34 -04:00
Aaron Gable aa3c9f0eee
Drop contact column from registrations table (#8201)
Drop the contact column from the Registrations table.

Part of https://github.com/letsencrypt/boulder/issues/8199
2025-06-16 14:58:53 -07:00
James Renken 61d2558b29
bad-key-revoker: Fix log message formatting (#8252)
Fixes #8251
2025-06-16 11:30:14 -07:00
Aaron Gable c68e27ea6f
Stop overwriting contact column upon account deactivation (#8248)
This fixes an oversight in
https://github.com/letsencrypt/boulder/pull/8200.

Part of https://github.com/letsencrypt/boulder/issues/8199
2025-06-16 09:29:57 -07:00
Aaron Gable fbf0c06427
Delete admin update-email subcommand (#8246)
Part of https://github.com/letsencrypt/boulder/issues/8199
2025-06-16 09:29:44 -07:00
Aaron Gable 24c385c1cc
Delete contact-auditor (#8244)
The contact-auditor's purpose was to scan the contact emails stored in
our database and identify invalid addresses which could be removed. As
of https://github.com/letsencrypt/boulder/pull/8201 we no longer have
any contacts in the database, so this tool no longer has a purpose.

Part of https://github.com/letsencrypt/boulder/issues/8199
2025-06-16 09:29:33 -07:00
dependabot[bot] 6872dfc63a
build(deps): bump the aws group with 4 updates (#8242)
Bumps the aws group with 4 updates:
[github.com/aws/aws-sdk-go-v2](https://github.com/aws/aws-sdk-go-v2),
[github.com/aws/aws-sdk-go-v2/config](https://github.com/aws/aws-sdk-go-v2),
[github.com/aws/aws-sdk-go-v2/service/s3](https://github.com/aws/aws-sdk-go-v2)
and [github.com/aws/smithy-go](https://github.com/aws/smithy-go).

Updates `github.com/aws/aws-sdk-go-v2` from 1.32.2 to 1.36.4
<details>
<summary>Commits</summary>
<ul>
<li><a
href="983f192608"><code>983f192</code></a>
Release 2025-06-10</li>
<li><a
href="a5c1277d48"><code>a5c1277</code></a>
Regenerated Clients</li>
<li><a
href="a42991177c"><code>a429911</code></a>
Update endpoints model</li>
<li><a
href="4ea1cecfb1"><code>4ea1cec</code></a>
Update API model</li>
<li><a
href="5b11c8d01f"><code>5b11c8d</code></a>
remove changelog directions for now because of <a
href="https://redirect.github.com/aws/aws-sdk-go-v2/issues/3107">#3107</a></li>
<li><a
href="79f492ceb2"><code>79f492c</code></a>
fixup changelog</li>
<li><a
href="4f82369def"><code>4f82369</code></a>
use UTC() in v4 event stream signing (<a
href="https://redirect.github.com/aws/aws-sdk-go-v2/issues/3105">#3105</a>)</li>
<li><a
href="755839b2ee"><code>755839b</code></a>
Release 2025-06-09</li>
<li><a
href="ba3d22d775"><code>ba3d22d</code></a>
Regenerated Clients</li>
<li><a
href="01587c6c41"><code>01587c6</code></a>
Update endpoints model</li>
<li>Additional commits viewable in <a
href="https://github.com/aws/aws-sdk-go-v2/compare/v1.32.2...v1.36.4">compare
view</a></li>
</ul>
</details>
<br />

Updates `github.com/aws/aws-sdk-go-v2/config` from 1.27.43 to 1.29.16
<details>
<summary>Commits</summary>
<ul>
<li><a
href="983f192608"><code>983f192</code></a>
Release 2025-06-10</li>
<li><a
href="a5c1277d48"><code>a5c1277</code></a>
Regenerated Clients</li>
<li><a
href="a42991177c"><code>a429911</code></a>
Update endpoints model</li>
<li><a
href="4ea1cecfb1"><code>4ea1cec</code></a>
Update API model</li>
<li><a
href="5b11c8d01f"><code>5b11c8d</code></a>
remove changelog directions for now because of <a
href="https://redirect.github.com/aws/aws-sdk-go-v2/issues/3107">#3107</a></li>
<li><a
href="79f492ceb2"><code>79f492c</code></a>
fixup changelog</li>
<li><a
href="4f82369def"><code>4f82369</code></a>
use UTC() in v4 event stream signing (<a
href="https://redirect.github.com/aws/aws-sdk-go-v2/issues/3105">#3105</a>)</li>
<li><a
href="755839b2ee"><code>755839b</code></a>
Release 2025-06-09</li>
<li><a
href="ba3d22d775"><code>ba3d22d</code></a>
Regenerated Clients</li>
<li><a
href="01587c6c41"><code>01587c6</code></a>
Update endpoints model</li>
<li>Additional commits viewable in <a
href="https://github.com/aws/aws-sdk-go-v2/compare/config/v1.27.43...config/v1.29.16">compare
view</a></li>
</ul>
</details>
<br />

Updates `github.com/aws/aws-sdk-go-v2/service/s3` from 1.65.3 to 1.80.2
<details>
<summary>Commits</summary>
<ul>
<li><a
href="983f192608"><code>983f192</code></a>
Release 2025-06-10</li>
<li><a
href="a5c1277d48"><code>a5c1277</code></a>
Regenerated Clients</li>
<li><a
href="a42991177c"><code>a429911</code></a>
Update endpoints model</li>
<li><a
href="4ea1cecfb1"><code>4ea1cec</code></a>
Update API model</li>
<li><a
href="5b11c8d01f"><code>5b11c8d</code></a>
remove changelog directions for now because of <a
href="https://redirect.github.com/aws/aws-sdk-go-v2/issues/3107">#3107</a></li>
<li><a
href="79f492ceb2"><code>79f492c</code></a>
fixup changelog</li>
<li><a
href="4f82369def"><code>4f82369</code></a>
use UTC() in v4 event stream signing (<a
href="https://redirect.github.com/aws/aws-sdk-go-v2/issues/3105">#3105</a>)</li>
<li><a
href="755839b2ee"><code>755839b</code></a>
Release 2025-06-09</li>
<li><a
href="ba3d22d775"><code>ba3d22d</code></a>
Regenerated Clients</li>
<li><a
href="01587c6c41"><code>01587c6</code></a>
Update endpoints model</li>
<li>Additional commits viewable in <a
href="https://github.com/aws/aws-sdk-go-v2/compare/service/s3/v1.65.3...service/s3/v1.80.2">compare
view</a></li>
</ul>
</details>
<br />

Updates `github.com/aws/smithy-go` from 1.22.0 to 1.22.2
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/aws/smithy-go/blob/main/CHANGELOG.md">github.com/aws/smithy-go's
changelog</a>.</em></p>
<blockquote>
<h1>Release (2025-02-17)</h1>
<h2>General Highlights</h2>
<ul>
<li><strong>Dependency Update</strong>: Updated to the latest SDK module
versions</li>
</ul>
<h2>Module Highlights</h2>
<ul>
<li><code>github.com/aws/smithy-go</code>: v1.22.3</li>
<li><strong>Dependency Update</strong>: Bump minimum Go version to 1.22
per our language support policy.</li>
</ul>
<h1>Release (2025-01-21)</h1>
<h2>General Highlights</h2>
<ul>
<li><strong>Dependency Update</strong>: Updated to the latest SDK module
versions</li>
</ul>
<h2>Module Highlights</h2>
<ul>
<li><code>github.com/aws/smithy-go</code>: v1.22.2
<ul>
<li><strong>Bug Fix</strong>: Fix HTTP metrics data race.</li>
<li><strong>Bug Fix</strong>: Replace usages of deprecated ioutil
package.</li>
</ul>
</li>
</ul>
<h1>Release (2024-11-15)</h1>
<h2>General Highlights</h2>
<ul>
<li><strong>Dependency Update</strong>: Updated to the latest SDK module
versions</li>
</ul>
<h2>Module Highlights</h2>
<ul>
<li><code>github.com/aws/smithy-go</code>: v1.22.1
<ul>
<li><strong>Bug Fix</strong>: Fix failure to replace URI path segments
when their names overlap.</li>
</ul>
</li>
</ul>
<h1>Release (2024-10-03)</h1>
<h2>General Highlights</h2>
<ul>
<li><strong>Dependency Update</strong>: Updated to the latest SDK module
versions</li>
</ul>
<h2>Module Highlights</h2>
<ul>
<li><code>github.com/aws/smithy-go</code>: v1.22.0
<ul>
<li><strong>Feature</strong>: Add HTTP client metrics.</li>
</ul>
</li>
</ul>
<h1>Release (2024-09-25)</h1>
<h2>Module Highlights</h2>
<ul>
<li><code>github.com/aws/smithy-go/aws-http-auth</code>: <a
href="https://github.com/aws/smithy-go/blob/main/aws-http-auth/CHANGELOG.md#v100-2024-09-25">v1.0.0</a>
<ul>
<li><strong>Release</strong>: Initial release of module aws-http-auth,
which implements generically consumable SigV4 and SigV4a request
signing.</li>
</ul>
</li>
</ul>
<h1>Release (2024-09-19)</h1>
<h2>General Highlights</h2>
<ul>
<li><strong>Dependency Update</strong>: Updated to the latest SDK module
versions</li>
</ul>
<h2>Module Highlights</h2>
<ul>
<li><code>github.com/aws/smithy-go</code>: v1.21.0</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="f2ae388e50"><code>f2ae388</code></a>
Release 2025-01-21</li>
<li><a
href="d9b8ee9d55"><code>d9b8ee9</code></a>
refactor: fix deprecated for ioutil (<a
href="https://redirect.github.com/aws/smithy-go/issues/560">#560</a>)</li>
<li><a
href="ee8334e832"><code>ee8334e</code></a>
transport/http: fix metrics race condition (<a
href="https://redirect.github.com/aws/smithy-go/issues/555">#555</a>)</li>
<li><a
href="7e8149709c"><code>7e81497</code></a>
transport/http: fix go doc typo (<a
href="https://redirect.github.com/aws/smithy-go/issues/554">#554</a>)</li>
<li><a
href="a7d0f1ef5f"><code>a7d0f1e</code></a>
fix potential nil deref in waiter path matcher (<a
href="https://redirect.github.com/aws/smithy-go/issues/563">#563</a>)</li>
<li><a
href="e5c5ac3012"><code>e5c5ac3</code></a>
add changelog instructions and make recipe</li>
<li><a
href="5e16ee7648"><code>5e16ee7</code></a>
add missing waiter retry breakout on non-nil non-matched error (<a
href="https://redirect.github.com/aws/smithy-go/issues/561">#561</a>)</li>
<li><a
href="10fbeed6f8"><code>10fbeed</code></a>
Revert &quot;Change defaults when generating a client via smithy CLI (<a
href="https://redirect.github.com/aws/smithy-go/issues/558">#558</a>)&quot;
(<a
href="https://redirect.github.com/aws/smithy-go/issues/559">#559</a>)</li>
<li><a
href="95ba31879b"><code>95ba318</code></a>
Change defaults when generating a client via smithy CLI (<a
href="https://redirect.github.com/aws/smithy-go/issues/558">#558</a>)</li>
<li><a
href="bed421c3d7"><code>bed421c</code></a>
Release 2024-11-15</li>
<li>Additional commits viewable in <a
href="https://github.com/aws/smithy-go/compare/v1.22.0...v1.22.2">compare
view</a></li>
</ul>
</details>
<br />

<details>
<summary>Most Recent Ignore Conditions Applied to This Pull
Request</summary>

| Dependency Name | Ignore Conditions |
| --- | --- |
| github.com/aws/aws-sdk-go-v2/service/s3 | [< 1.28, > 1.27.1] |
| github.com/aws/aws-sdk-go-v2/config | [< 1.18, > 1.17.1] |
| github.com/aws/aws-sdk-go-v2/service/s3 | [< 1.31, > 1.30.5] |
</details>


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-13 22:40:08 -07:00
Aaron Gable 1ffa95d53d
Stop interacting with registration.contact column (#8200)
Deprecate the IgnoreAccountContacts feature flag. This causes the SA to
never query the contact column when reading registrations from the
database, and to never write a value for the contact column when
creating a new registration.

This requires updating or disabling several tests. These tests could be
deleted now, but I felt it was more appropriate for them to be fully
deleted when their corresponding services (e.g. expiration-mailer) are
also deleted.

Fixes https://github.com/letsencrypt/boulder/issues/8176
2025-06-13 14:40:19 -07:00
James Renken 7214b285e4
identifier: Remove helper funcs from PB identifiers migration (#8236)
Remove `ToDNSSlice`, `FromProtoWithDefault`, and
`FromProtoSliceWithDefault` now that all their callers are gone. All
protobufs but one have migrated from DnsNames to Identifiers.

Remove TODOs for the exception, `ValidationRecord`, where an identifier
type isn't appropriate and it really only needs a string.

Rename `corepb.ValidationRecord.DnsName` to `Hostname` for clarity, to
match the corresponding PB's field name.

Improve various comments and docs re: IP address identifiers.

Depends on #8221 (which removes the last callers)
Fixes #8023
2025-06-13 12:55:32 -07:00
Aaron Gable b9a681dbcc
Delete notify-mailer, expiration-mailer, and id-exporter (#8230)
These services existed solely for the purpose of sending emails, which
we no longer do.

Part of https://github.com/letsencrypt/boulder/issues/8199
2025-06-12 15:45:04 -07:00
James Renken 0a095e2f6b
policy, ra: Remove default allows for DNS identifiers (#8233)
Fixes #8184
2025-06-12 15:25:23 -07:00
James Renken 48d5ad3c19
ratelimits: Add IP address identifier support (#8221)
Change most functions in `ratelimits` to use full ACMEIdentifier(s) as
arguments, instead of using their values as strings. This makes the
plumbing from other packages more consistent, and allows us to:

Rename `FQDNsToETLDsPlusOne` to `coveringIdentifiers` and handle IP
identifiers, parsing IPv6 addresses into their covering /64 prefixes for
CertificatesPerDomain[PerAccount] bucket keys.

Port improved IP/CIDR validation logic to NewRegistrationsPerIPAddress &
PerIPv6Range.

Rename `domain` parts of bucket keys to either `identValue` or
`domainOrCIDR`.

Rename other internal functions to clarify that they now handle
identifier values, not just domains.

Add the new reserved IPv6 address range from RFC 9780.

For deployability, don't (yet) rename rate limits themselves; and
because it remains the name of the database table, preserve the term
`fqdnSets`.

Fixes #8223
Part of #7311
2025-06-12 11:47:32 -07:00
Aaron Gable 1f36d654ba
Update CI to mariadb 10.6.22 (#8239)
Fixes https://github.com/letsencrypt/boulder/issues/8238
2025-06-11 15:19:09 -07:00
Aaron Gable 44f75d6abd
Remove mail functionality from bad-key-revoker (#8229)
Simplify the main logic loop to simply revoke certs as soon as they're
identified, rather than jumping through hoops to identify and
deduplicate the associated accounts and emails. Make the Mailer portion
of the config optional for deployability.

Part of https://github.com/letsencrypt/boulder/issues/8199
2025-06-09 14:36:19 -07:00
Aaron Gable d4e706eeb8
Update CI to go1.24.4 (#8232)
Go 1.24.4 is a security release containing fixes to net/http,
os.OpenFile, and x509.Certificate.Verify, all of which we use. We appear
to be unaffected by the specific vulnerabilities described, however. See
the announcement here:
https://groups.google.com/g/golang-announce/c/ufZ8WpEsA3A
2025-06-09 09:30:33 -07:00
dependabot[bot] 426482781c
build(deps): bump the otel group (#7968)
Update:
- https://github.com/open-telemetry/opentelemetry-go-contrib from 0.55.0 to 0.61.0
- https://github.com/open-telemetry/opentelemetry-go from 1.30.0 to 1.36.0
- several golang.org/x/ packages
- their transitive dependencies
2025-06-06 17:22:48 -07:00
Aaron Gable 1d713ed8eb
Ignore IP CNs in CSRs (#8231)
If a finalize CSR contains a SAN which looks like an IP address, don't
actually include that CN in our IssuanceRequest, and don't promote any
other SAN to be the CN either. This is similar to how we ignore the
CSR's CN when it is too long.
2025-06-06 14:57:12 -07:00
Aaron Gable 83b6b05177
Update golangci-lint to v2 (#8228)
The golangci-lint project has released a v2, which is noticeably faster,
splits linters and formatters into separate categories, has greatly
improved support for staticcheck, and has an incompatible config file
format. Update our boulder-tools version of golangci-lint to v2, remove
our standalone staticcheck, and update our config file to match.
2025-06-06 14:38:15 -07:00
Aaron Gable d951304b54
Ratelimits: don't validate our own constructed bucket keys (#8225)
All of the identifiers being passed into the bucket construction helpers
have already passed through policy.WellFormedIdentifiers in the WFE. We
can trust that function, and our own ability to construct bucket keys,
to reduce the amount of revalidation we do before sending bucket keys to
redis.

The validateIdForName function is still used to validate override bucket
keys loaded from yaml.
2025-06-03 15:43:07 -07:00
Aaron Gable 474fc7f9a7
Partially revert "bdns, va: Remove DNSAllowLoopbackAddresses" (#8226)
This partially reverts https://github.com/letsencrypt/boulder/pull/8203,
which was landed as commit dea81c7381.

It leaves all of the boulder integration test environment changes in
place, while restoring the DNSAllowLoopbackAddresses config key and its
ability to influence the VA's behavior.
2025-06-03 14:52:46 -07:00
Samantha Frank 0d7ea60b2c
email-exporter: Add an LRU cache of seen hashed email addresses (#8219) 2025-05-30 17:04:35 -04:00
Aaron Gable 23608e19c5
Simplify docker-compose network setup (#8214)
Remove static IPs from services that can be reached by their service
name. Remove consulnet and redisnet, and have the services which
connected to those network connect directly to bouldernet instead.
Instruct docker-compose to only dynamically allocate IPs from the upper
half of the bouldernet subset, to avoid clashing with the few static IPs
we still specify.
2025-05-30 13:23:27 -07:00
Samantha Frank 69ba857d5e
ra: Allow rate limit overrides to be added/updated (#8218)
#8217
2025-05-30 14:07:58 -04:00
James Renken dea81c7381
bdns, va: Remove DNSAllowLoopbackAddresses (#8203)
We no longer need a code path to resolve reserved IP addresses during
integration tests.

Move to a public IP for the remaining tests, after #8187 did so for many
of them.

Depends on #8187
2025-05-28 10:08:03 -07:00
James Renken ac68828f43
Replace most uses of net.IP with netip.Addr (#8205)
Retain `net.IP` only where we directly work with `x509.Certificate` and
friends.

Fixes #5925
Depends on #8196
2025-05-27 15:05:35 -07:00
James Renken 9b9ed86c10
sa: Encode IP identifiers for issuedNames (#8210)
Move usage of `sa.ReverseName` to a new `sa.EncodeIssuedName`, which
detects IP addresses and exempts them from being reversed. Retain
`reverseName` as an internal helper function.

Update `id-exporter`, `reversed-hostname-checker`, and tests to use the
new function and handle IP addresses.

Part of #7311
2025-05-27 14:55:19 -07:00
James Renken b017c1b46d
bdns, policy: Move reserved IP checking from bdns to policy & refactor (#8196)
Move `IsReservedIP` and its supporting vars from `bdns` to `policy`.

Rewrite `IsReservedIP` to:
* Use `netip` because `netip.Prefix` can be used as a map key, allowing
us to define prefix lists more elegantly. This will enable future work
to import prefix lists from IANA's primary source data.
* Return an error including the reserved network's name.

Refactor `IsReservedIP` tests to be table-based.

Fixes #8040
2025-05-27 13:24:21 -07:00
James Renken 103ffb03d0
wfe, csr: Add IP address identifier support & integration test (#8187)
Permit all valid identifier types in `wfe.NewOrder` and `csr.VerifyCSR`.

Permit certs with just IP address identifiers to skip
`sa.addIssuedNames`.

Check that URI SANs are empty in `csr.VerifyCSR`, which was previously
missed.

Use a real (Let's Encrypt) IP address range in integration testing, to
let challtestsrv satisfy IP address challenges.

Fixes #8192
Depends on #8154
2025-05-27 13:17:47 -07:00
Aaron Gable 8a7c3193a9
SA: Use IgnoreAccountContacts flag to shortcut UpdateRegistrationContact (#8208)
If the IgnoreAccountContacts flag is set, don't bother writing the new
contacts to the database and instead just return the account object as
it stands. This does not require any test changes because
https://github.com/letsencrypt/boulder/pull/8198 already changed
registrationModelToPb to omit whatever contacts were retrieved from the
database before responding to the RA.

Part of https://github.com/letsencrypt/boulder/issues/8176
2025-05-23 13:01:51 -07:00
Aaron Gable 930e69b8f5
Remove expectation of contacts from id-exporter (#8209)
It appears that, in the past, we wanted id-exporter's "tell me all the
accounts with unexpired certificates" functionality to limit itself to
account that have contact info. The reasons for this limitation are
unclear, and are quickly becoming obsolete as we remove contact info
from the registrations table.

Remove this layer of filtering, so that id-exporter will retrieve all
accounts with active certificates, and not care whether the contact
column exists or not.

Part of https://github.com/letsencrypt/boulder/issues/8199
2025-05-23 13:01:27 -07:00
Aaron Gable d63f65c837
Give registrations.contact column a default value (#8207)
Alter the "registrations" table so that the "contact" column has a
default value of the JSON empty list "[]". This, once deployed to all
production environments, will allow Boulder to stop writing to and
reading from this column, in turn allowing it to be eventually wholly
dropped from the database.

IN-11365 tracks the corresponding production database changes
Part of https://github.com/letsencrypt/boulder/issues/8176
2025-05-22 15:04:37 -07:00
Aaron Gable 2eaa2fea64
SA: Stop storing and retrieving contacts (#8198)
Add a feature flag "IgnoreAccountContacts" which has two effects in the
SA:
- When a new account is created, don't insert any contacts provided; and
- When an account is retrieved, ignore any contacts already present.

This causes boulder to act as though all accounts have no associated
contacts, and is the first step towards being able to drop the contacts
from the database entirely.

Part of https://github.com/letsencrypt/boulder/issues/8176
2025-05-21 16:23:35 -07:00
Aaron Gable d662c0843d
Include Location: header in GET Order responses (#8202)
This causes the GET Order polling response to match the NewOrder and
FinalizeOrder responses.

Fixes https://github.com/letsencrypt/boulder/issues/8197
2025-05-21 16:21:54 -07:00
Phil Porada 7ea51e5f91
boulder-observer: check certificate status via CRL too (#8186)
Let's Encrypt [recently removed OCSP URLs from
certificates](https://community.letsencrypt.org/t/removing-ocsp-urls-from-certificates/236699)
which unfortunately caused the boulder-observer TLS prober to panic.
This change short circuits the OCSP checking logic if no OCSP URL exists
in the to-be-checked certificate.

Fixes https://github.com/letsencrypt/boulder/issues/8185

---------

Co-authored-by: Aaron Gable <aaron@letsencrypt.org>
2025-05-20 09:24:21 -07:00
Aaron Gable ac2dae70f2
cert-checker: add support for ipAddress SANs (#8188)
In cert-checker, inspect both the DNS Names and the IP Addresses
contained within the certificate being examined. Also add a check that
no other kinds of SANs exist in the certificate.

Fixes https://github.com/letsencrypt/boulder/issues/8183
2025-05-16 16:22:56 -07:00
James Renken aaaf623d49
va: Remove deprecated Domain from vapb.IsCAAValidRequest (#8193)
Part of #8023
2025-05-16 15:21:28 -07:00
James Renken 60033836db
ra: Add IdentifierTypes to profiles (#8154)
Add `IdentifierTypes` to validation profiles' config, defaulting to DNS
if not set.

In `NewOrder`, check that the order's profile permits each identifier's
type.

Fixes #8137
Depends on #8173
2025-05-16 13:57:02 -07:00
Aaron Gable c9e2f98b5d
Remove OCSP and MustStaple support from issuance (#8181)
Remove the ability for the issuance package to include the AIA OCSP URI
and the Must Staple (more properly known as the tlsRequest) extension in
certificates. Deprecate the "OmitOCSP" and "AllowMustStaple" profile
config keys, as they no longer have any effect. Similarly deprecate the
"OCSPURL" issuer config key, as it is no longer included in
certificates.

Update the tests to always include to CRLDP extension instead, and
remove some OCSP- or Stapling-specific test cases.

Fixes https://github.com/letsencrypt/boulder/issues/8179
2025-05-16 11:51:02 -07:00
Matthew McPherrin caa29b2937
Update to zlint 3.6.6 (#8194)
v3.6.5 and v3.6.6 include several new lints and bugfixes.
Release notes at https://github.com/zmap/zlint/releases
2025-05-16 11:48:31 -07:00
James Renken bef73f3c8b
va: Fix deployability of CAA change in #8153 (#8190)
In #8153, we started using identifiers in `vapb.IsCAAValidRequest`, and
added logic at the top of `va.DoCAA` to populate the `ident` variable
from the deprecated `Domain` value, in order to accommodate clients that
don't yet populate the `Identifier`.

Unfortunately, we didn't use the `ident` variable throughout the entire
function. Two places refer directly to `req.Identifier` and can't handle
it being nil.

Fixes #8189
2025-05-15 12:21:30 -07:00
Aaron Gable 4d7473e5ea
Remove support for OCSP Must-Staple allowlist (#8180)
Fixes https://github.com/letsencrypt/boulder/issues/8178
2025-05-14 16:20:05 -07:00
James Renken 648ab05b37
policy: Support IP address identifiers (#8173)
Add `pa.validIP` to test IP address validity & absence from IANA
reservations.

Modify `pa.WillingToIssue` and `pa.WellFormedIdentifiers` to support IP
address identifiers.

Add a map of allowed identifier types to the `pa` config.

Part of #8137
2025-05-14 13:49:51 -07:00
James Renken 4d28e010f6
Add more lints: asciicheck, bidichk, spancheck (#8182)
Remove a few trivial instances of trailing whitespace.
2025-05-13 11:56:40 -07:00
Jacob Hoffman-Andrews f0dfbfdb08
deps: update certificate-transparency-go (#8171)
This allows us to drop a transitive dependency on k8s.io/klog.
2025-05-12 14:55:09 -07:00
Jacob Hoffman-Andrews 388c68cb49
sa: use internal certificateStatusModel instead of core.CertificateStatus (#8159)
Part of https://github.com/letsencrypt/boulder/issues/8112
2025-05-12 14:53:08 -07:00
Jacob Hoffman-Andrews 01a299cd0f
Deprecate MPICFullResults feature flag (#8169)
Fixes https://github.com/letsencrypt/boulder/issues/8121
2025-05-12 14:47:32 -07:00
Samantha Frank b6887a945e
email-exporter: Count Pardot API errors encountered (#8175) 2025-05-12 14:43:09 -07:00
Aaron Gable faa07f5e36
Finish cleaning up unused CT config types (#8174)
The last use of these types was removed in
https://github.com/letsencrypt/boulder/pull/8156
2025-05-10 18:37:59 -07:00
Samantha Frank e625ff3534
sa: Store and manage rate limit overrides in the database (#8142)
Add support for managing and querying rate limit overrides in the
database.
- Add `sa.AddRateLimitOverride` to insert or update a rate limit
override. This will be used during Rate Limit Override Portal to commit
approved overrides to the database.
- Add `sa.DisableRateLimitOverride` and `sa.EnableRateLimitOverride` to
toggle override state. These will be used by the `admin` tool.
- Add `sa.GetRateLimitOverride` to retrieve a single override by limit
enum and bucket key. This will be used by the Rate Limit Portal to
prevent duplicate or downgrade requests but allow upgrade requests.
- Add `sa.GetEnabledRateLimitOverrides` to stream all currently enabled
overrides. This will be used by the rate limit consumers (`wfe` and
`ra`) to refresh the overrides in-memory.
- Implement test coverage for all new methods.
2025-05-08 14:50:30 -04:00
James Renken 650c269bf6
ra, va: Bypass CAA for IP identifiers & use Identifier in IsCAAValidRequest (#8153)
In `vapb.IsCAAValidRequest`, even though CAA is only for DNS names,
deprecate `Domain` in favour of `Identifier` for consistency.

In `va.DoCAA`, reject attempts to validate CAA for non-DNS identifiers.

Rename `identifier` to `ident` inside some VA functions, also for
consistency.

In `ra.checkDCVAndCAA` & `ra.checkAuthorizationsCAA`, bypass CAA checks
for IP address identifiers.

Part of #7995
2025-05-08 11:22:06 -07:00
Aaron Gable f86f88d563
Include supported algs in badSignatureAlgorithm problem doc (#8170)
Add an "algorithms" field to all problem documents, but tag it so it
won't be included in the serialized json unless populated. Populate it
only when the problem type is "badSignatureAlgorithm", as specified in
RFC 8555 Section 6.2.

The resulting problem document looks like this:
```json
{
    "type": "urn:ietf:params:acme:error:badSignatureAlgorithm",
    "detail": "Unable to validate JWS :: JWS signature header contains unsupported algorithm
 \"RS512\", expected one of [RS256 ES256 ES384 ES512]",
    "status": 400,
    "algorithms": [
        "RS256",
        "ES256",
        "ES384",
        "ES512"
    ]
}
```

Fixes https://github.com/letsencrypt/boulder/issues/8155
2025-05-07 18:29:14 -07:00
James Renken 52615d9060
ra: Fully support identifiers in NewOrder, PerformValidation & RevokeCertByApplicant (#8139)
In `ra.NewOrder`, improve safety of authz reuse logic by making it
explicit that only DNS identifiers might be wildcards. Also, now that
the conditional statements need to be more complicated, collapse them
for brevity.

In `vapb.PerformValidationRequest`, remove `DnsName`.

In `ra.PerformValidation`, pass an `Identifier` instead of a `DnsName`.

In `ra.RevokeCertByApplicant`, check that the requester controls
identifiers of all types (not just DNS).

Fixes #7995 (the RA now fully supports IP address identifiers, except
for rate limits)
Fixes #7647 
Part of #8023
2025-05-07 15:11:41 -07:00
Matthew McPherrin b26b116861
Update certificate-transparency-go for bugfix (#8160)
This updates to current `master`,
bc7acd89f703743d050f5cd4a3b9746808e0fdae

Notably, it includes a bug-fix to error handling in the HTTP client,
which we found was hiding errors from CT logs, hindering our debugging.

That fix is
https://github.com/google/certificate-transparency-go/pull/1695

No release has been tagged since this PR merged, so using the `master`
commit.

A few mutual dependencies used by both Boulder and ct-go are updated,
including mysql, otel, and grpc.
2025-05-06 12:10:53 -07:00
Matthew McPherrin 36bb6527e5
Remove obsolete informational CT config (#8156)
This field is unused. This has been configured in the CTLogs field for
years.

The field has been a no-op since #6485 and was removed from Let's
Encrypt prod configuration in 2022.
2025-05-05 14:18:35 -04:00
Aaron Gable 9102759f4e
Make CT log selection simpler and more robust (#8152)
Simplify the way we load and handle CT logs: rather than keeping them
grouped by operator, simply keep a flat list and annotate each log with
its operator's name. At submission time, instead of shuffling operator
groups and submitting to one log from each group, shuffle the whole set
of individual logs.

Support tiled logs by similarly annotating each log with whether it is
tiled or not.

Also make the way we know when to stop getting SCTs more robust.
Previously we would stop as soon as we had two, since we knew that they
would be from different operator groups and didn't care about tiled
logs. Instead, introduce an explicit CT policy compliance evaluation
function which tells us if the set of SCTs we have so far forms a
compliant set.

This is not our desired end-state for CT log submission. Ideally we'd
like to: simplify things even further (don't race all the logs, simply
try to submit to two at a time), improve selection (intelligently pick
the next log to submit to, rather than just a random shuffle), and
fine-tune latency (tiled logs should have longer timeouts than classic
ones). Those improvements will come in future PRs.

Part of https://github.com/letsencrypt/boulder/issues/7872
2025-05-01 17:24:19 -07:00
Aaron Gable e01bc22984
Update protoc-gen-go to match updated grpc libraries (#8151)
https://github.com/letsencrypt/boulder/pull/8150 updated our runtime
protobuf dependency from v1.34.1 to v1.36.5. This change does the same
for our build-time dependency, to keep them in sync.
2025-05-01 17:14:57 -07:00
Aaron Gable 1c1c4dcfef
Update certificate-transparency-go to get static/tiled log support (#8150)
Update github.com/google/certificate-transparency-go from v1.1.6 to
v1.3.1. This updates the loglist file schema to recognize logs which are
tagged as being tiled logs / implementing the static CT API.

Transitively update:
- github.com/go-sql-driver/mysql from v1.7.1 to v1.8.1
- github.com/prometheus/client_golang from v1.15.1 to v1.22.0
- github.com/prometheus/client_model from v0.4.0 to v0.6.1
- go.opentelemetry.io/otel from v1.30.0 to v1.31.0
- google.golang.org/grpc from v1.66.1 to v1.69.4
- google.golang.org/protobuf from v1.34.2 to v1.36.5
- and a variety of indirect dependencies

Remove one indirect dependency:
- github.com/matttproud/golang_protobuf_extensions

Add two new indirect dependencies:
- filippo.io/edwards25519@v1.1.0 (used by go-sql-driver to handle
mariadb's custom encryption implementation)
- github.com/munnerz/goautoneg@v0.0.0-20191010083416-a7dc8b61c822
(previously inlined into prometheus/common)

Also fix two unit tests which need minor modifications to work with
updated type signatures and behavior.

Part of https://github.com/letsencrypt/boulder/issues/7872
2025-04-30 15:56:31 -07:00
Samantha Frank 1274878d5e
integration: Fix second MPIC validation flake (#8146)
Break validation of length and content of expected User-Agents out into
two assertion functions. Make it so that DOH and MPICFullResults can be
deprecated in either order.

Fixes #8145
2025-04-28 11:14:38 -04:00
Aaron Gable 0038149c79
Fix profile comparison when looking for authzs to reuse (#8144)
Previously, if the request asked for a profile, we were comparing the
address of that requested profile to the address of the profile field of
the found authz. Obviously these addresses were never the same. Instead,
compare the actual values, with an added nil check for safety.

This fixes a bug reported on the community forum. The updated test fails
without the accompanying code change.
2025-04-25 15:24:50 -07:00
Aaron Gable 42138ff2da
Run .deb build on ubuntu 24.04 (#8143) 2025-04-24 17:33:13 -07:00
Aaron Gable bc899ac3ef
Update go-sql-driver/mysql from v1.5.0 to v1.7.1 (#8138)
Version v1.5.0 was released in January 2020, over five years ago. We
have attempted to update this package several times since then -- first
to v1.6.0, later to v1.7.1 -- but have reverted the change due to
nigh-inexplicable performance regressions each time. Since our last
attempt, we believe we have addressed the underlying issue by truncating
timestamps when we talk to the database (see
https://github.com/letsencrypt/boulder/pull/7556) so that our indices
don't try to track nanosecond precision.

We are now ready to reattempt updating this package to v1.7.1 again. If
that goes well, we will further update it to the newest version.

Fixes https://github.com/letsencrypt/boulder/issues/5437
Part of https://github.com/letsencrypt/boulder/issues/7872
2025-04-24 17:29:41 -07:00
James Renken dc8fa5a95f
ca: Add IP address issuance (#8117)
Refactor `ca.issuePrecertificateInner` away from the old `NamesFromCSR`
logic, and to our `identifier` functions.

Add `identifier.ToValues` to provide slices of identifier values, split
up by type.

Fixes #8135 
Part of #7311
2025-04-22 16:25:22 -07:00
dependabot[bot] 1ce439bc92
build(deps): bump golang.org/x/net from 0.37.0 to 0.38.0 (#8125)
Bumps https://github.com/golang/net from 0.37.0 to 0.38.0. This
resolves a minor vulnerability that does not directly affect Boulder.

Changelog: https://github.com/golang/net/compare/v0.37.0...v0.38.0
2025-04-21 13:56:26 -07:00
Jacob Hoffman-Andrews 726b3c91e8
test: copy some config-next settings to config (#8116)
Methodology:

 - Copy test/config-next/* to test/config/.
 - Review the diff, reverting things that should stay `next`-only.
 - When in doubt, check against prod configs (e.g. for feature flags).

In the process I noticed that config for the TCP prober in `observer`
had been added to test/config but not test/config-next, so I ported it
forward (and my IDE stripped some trailing spaces in both versions).
2025-04-21 13:54:31 -07:00
Jacob Hoffman-Andrews c95ab5c75f
crl-updater: UpdatePeriod safety check (#8131)
The current requirement is that CRLs must be published within 24 hours
after revoking a certificate.

Fixes #8110
2025-04-21 13:54:14 -07:00
Jacob Hoffman-Andrews 967d722cf4
sa: use internal certificateModel (#8130)
This follows the system we've used for other types, where the SA has a
model type that is converted to a proto message for use outside the SA.

Part of #8112.
2025-04-21 13:48:29 -07:00
Jacob Hoffman-Andrews 37147d4dfa
lint: add sqlclosecheck (#8129)
Picking up from #7709
2025-04-21 11:01:37 -07:00
Jacob Hoffman-Andrews e8eddc0d50
ca: remove capb.IssueCertificateForPrecertificateRequest (#8127)
Fixes #8039
2025-04-18 12:18:31 -07:00
Samantha Frank 6021d4b47d
docker: Update image to Ubuntu 24.04 (#8128)
#8109 updated CI to use 24.04 runners, now update the Docker image to
build 24.04 and CI to use it.

Build fixes:
- Unpin mariadb-client-core, 10.3 is no longer provided in 24.04 apt
repositories
- Use new pip flag --break-system-packages to comply with PEP 668, which
is now enforced in Python 3.12+

Runtime fixes:
- Start rsyslogd directly due to missing symlink (see:
https://github.com/rsyslog/rsyslog/issues/5611)
- Fix SyntaxWarning: invalid escape sequence '\w' error.
- Replace OpenSSL.crypto.load_certificate with
x509.load_pem_x509_certificate due to
d73d0ed417
2025-04-17 13:41:20 -04:00
Jacob Hoffman-Andrews 3e8ccdb8ba
Build deb in docker (#8126)
This allows us to build on Ubuntu 20.04 a little longer.
2025-04-17 11:15:52 -04:00
Jacob Hoffman-Andrews 585319f247
issuance: remove profile hashes (#8118)
Part of #8039
2025-04-16 16:57:24 -07:00
James Renken 23e14f1149
Update CI to Ubuntu 24.04 (#8109)
Fixes #7775
2025-04-16 14:32:55 -07:00
Samantha Frank b2eaabb4e1
test: Fix integration tests sensitive to MPICFullResults (#8122) 2025-04-16 10:08:17 -04:00
Jacob Hoffman-Andrews 3ddaa6770f
ca: make orderID mandatory (#8119)
It was allowed to be empty for ACMEv1 requests, but those are long gone.

Also, move the IsAnyNilOrZero checks up to the RPC entry point.
2025-04-15 14:56:28 -07:00
Samantha Frank 7a3feb2ceb
va/rva: Validate user-agent for http-01 and DoH requests (#8114)
Plumb the userAgent field, used to set http-01 User-Agent headers, from
va/rva configuration through to where User-Agent headers can be set for
DoH queries. Use integration tests to validate that the User-Agent is
set for http-01 challenges, dns-01 challenges over DoH, and CAA checks
over DoH.

Fixes #7963.
2025-04-15 16:31:08 -04:00
Jacob Hoffman-Andrews d800055fe6
ca: Remove IssuePrecertificateResponse (#8115)
Instead, simply return DER bytes from `issuePrecertificate`, and accept
regular parameters to `issueCertificateForPrecertificate` (instead of a
proto message).

Also, move the lookup of the certificate profile up to
`IssueCertificate`, and pass the selected `*certProfileWithId` to both
`issuePrecertificate` and `issueCertificateForPrecertificate`.

Also, change `issueCertificateForPrecertificate` to just return DER, not
a `*corepb.Certificate` (of which most fields were already being
ignored).
2025-04-10 17:56:13 -07:00
Jacob Hoffman-Andrews 203c836925
core: remove `db:` tags for Registration and Authorization (#8113)
These objects aren't used for database serialization anymore. Instead
the SA uses an internal model object.
2025-04-10 15:59:16 -07:00
James Renken 722f7c5318
sa: Support new identifier types in authz getting funcs (#8104)
Refactor `GetAuthorizations2`, `GetValidAuthorizations2` and
`GetValidOrderAuthorizations2` to support non-DNS identifier types.

Remove the deprecated `DnsNames` field from the
`GetAuthorizationsRequest` and `GetValidAuthorizationsRequest` structs.
All users of these structs use `Identifier` instead.

Fixes #7922
Part of #7311
2025-04-10 10:57:17 -07:00
Jacob Hoffman-Andrews 97828d82db
ca: Create "OmitOCSP" profile config option (#8103)
Add a new config field for profiles which causes the profile to omit the
AIA OCSP URI. It can only be omitted if the CRLDP extension is
configured to be included instead. Enable this flag in config-next.

When a certificate is revoked, if it does not have an AIA OCSP URI,
don't bother with an Akamai OCSP purge.

Builds on #8089

Most of the changes in this PR relate to tests. Different from #8089, I
chose to keep testing of OCSP in the config-next world. This is because
we intend to keep operating OCSP even after we have stopped including it
in new certificates. So we should test it in as many environments as
possible.

Adds a WithURLFallback option to ocsp_helper. When
`ocsp_helper.ReqDer()` is called for a certificate with no OCSP URI, it
will query the fallback URL instead. As before, if the certificate has
an OCSP URI ocsp_helper will use that. Use that for all places in the
integration tests that call ocsp_helper.
2025-04-09 11:46:58 -07:00
Samantha Frank bc39780908
test: Add integration tests for MPIC validation (#8102)
- Update the chall-test-srv-client to make DNS events and DNS01 methods
more convenient
- Add an integration test that counts DCV and CAA checks for each
validation method

Part of #7963
2025-04-09 12:53:07 -04:00
orangepizza 5cc8a77ce3
wfe: Separately handle badSignature at JWS parse time (#8091)
solve https://github.com/letsencrypt/boulder/issues/8088

RFC8555 6.2 requires badSignatureAlgorithm on unacceptable JWS signing
algorithm, but current boulder return malform:failed to parse jws error
instead

Its because this only checks about JWS protected header's signature
algorithm, current checkAlgorithm is while too late to catch parse time
error but not redundant, as it checks against a key and signed message

---------

Co-authored-by: Samantha Frank <hello@entropy.cat>
2025-04-08 15:45:06 -07:00
James Renken ff9e59d70b
core: Remove DnsNames from Order (#8108)
Remove the deprecated `DnsNames` field from the `corepb.Order` proto
message. All users of this struct use `Identifiers` instead.

This unblocks future changes that will require `Order` users to handle
different identifier types.

Part of #7311
2025-04-08 15:17:18 -07:00
James Renken 9b53c3455b
sa: Remove DnsNames from more request protos (#8105)
Remove the deprecated `DnsNames` field from the `CountFQDNSetsRequest`,
`FQDNSetExistsRequest`, and `GetOrderForNamesRequest` structs. All users
of these structs use `Identifier` instead.

Part of #7311
2025-04-08 13:38:03 -07:00
James Renken c426fc71f6
sa: Remove DnsNames from NewOrderRequest (#8101)
Remove the deprecated `DnsNames` field from the `NewOrderRequest`
struct. All users of this struct use `Identifier` instead.

Part of #7311
2025-04-08 12:27:08 -07:00
James Renken b9f93b386f
admin: Fix race in revokeSerials (#8107)
Redeclare `err` rather than assigning to the parent function's `err`, as
there are multiple goroutines running. Thanks to @jsha for the
diagnosis.
2025-04-08 10:15:25 -07:00
James Renken 38a7197909
sa: Support IP identifiers in CountInvalidAuthorizations2 (#8098)
Remove the deprecated `DnsName` field from the
`CountInvalidAuthorizationsRequest` struct. All users of this struct use
`Identifier` instead.

Part of #7311
2025-04-08 10:15:08 -07:00
James Renken 26ae6f83a3
sa: Support IP identifiers in modelToAuthzPB (#8099)
Partially refactor `TestAuthzModel` for readability.

Part of #7311

Depends on #8097 (because it removes `DnsName` coverage from
`TestAuthzModel`)
2025-04-08 10:14:09 -07:00
James Renken 1e00ee58b3
ra: Remove DnsNames from NewOrderRequest (#8100)
Remove the deprecated `DnsNames` field from the `NewOrderRequest`
struct. All users of this struct use `Identifier` instead.

Part of #7311
2025-04-07 20:48:58 -07:00
James Renken 767abc73a4
core: Remove DnsName from Authorization (#8097)
Remove the deprecated `DnsName` field from the core `Authorization`
struct. All users of this struct use `Identifier` instead.

This unblocks future changes that will require `Authorization` users to
handle different identifier types.

Part of #7311
2025-04-07 15:25:59 -07:00
Samantha Frank 098cf91e99
dependencies: Update v4.0.5 to v4.1.0 (#8106)
Diff: https://github.com/go-jose/go-jose/compare/v4.0.5...v4.1.0
2025-04-07 18:03:53 -04:00
dependabot[bot] 7b75602bbc
build(deps): bump docker/login-action from 3.3.0 to 3.4.0 (#8090)
Bumps [docker/login-action](https://github.com/docker/login-action) from
3.3.0 to 3.4.0.

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-04 17:01:33 -04:00
Samantha Frank c87c917348
test: Add HTTP client for chall-test-srv (#8095) 2025-04-04 09:15:59 -04:00
Jacob Hoffman-Andrews 76de5bf561
ca: unexport IssuePrecertificate and IssueCertificateForPrecertificate (#8092)
These methods are still preserved as-is for now, and still take proto
messages as arguments. But they are not exported as RPCs. Refactoring
the arguments will be a followup PR.

Part of #8039
2025-04-03 16:11:39 -07:00
Jacob Hoffman-Andrews 27e08f4846
Fix re-revocation representations on CRL (#8096)
For explicitly sharded certificates, CRL status is read from the
`revokedCertificates` table. This table gets written at revocation time.
At re-revocation time (for key compromise), it only gets written by the
SA if the caller passes a nonzero ShardIdx to UpdateRevokedCertificate.
The RA was never passing a nonzero ShardIdx to UpdateRevokedCertificate.
2025-04-03 15:03:57 -07:00
Samantha Frank 0fe66b6e8e
test: Copy challtestsrv management API from pebble (#8094)
- Copy
https://pkg.go.dev/github.com/letsencrypt/pebble/v2/cmd/pebble-challtestsrv
to `test/chall-test-srv`
- Rename pebble-challtestsrv to chall-test-srv, consistent with other
test server naming in Boulder
- Replace Dockerfile go install with Makefile compilation of
`chall-test-srv`
- Run chall-test-srv from `./bin/chall-test-srv`
- Bump `github.com/letsencrypt/challtestsrv` from `v1.2.1` to `v1.3.2`
in go.mod
- Update boulder-ci GitHub workflow to use `go1.24.1_2025-04-02`

Part of #7963
2025-04-03 15:10:18 -04:00
Samantha Frank 13f98daabf
docker: Update pkimetal v1.19.0 to v1.20.0 (#8093)
Changes: https://github.com/pkimetal/pkimetal/compare/v1.19.0...v1.20.0
2025-04-03 11:14:55 -07:00
Aaron Gable 3438b057d8
Replace Python test_recheck_caa with Go TestCAARechecking (#8085)
Replace a python integration test which relies on our
"setup_twenty_days_ago" scaffolding with a Go test that uses direct
database statements to avoid any need to do clock manipulation. The
resulting test is much more verbose, but also (in my opinion) much
clearer and significantly faster.
2025-03-31 09:10:22 -07:00
Aaron Gable c0e31f9a4f
Add integration test for when CRL entries are removed (#8084)
We already have an integration test showing that a serial does not show
up on any CRL before its certificate has been revoked, and does show up
afterwards. Extend that test to cover three new times:
- shortly before the certificate expires, when the entry must still
appear;
- shortly after the certificate expires, when the entry must still
appear; and
- significantly after the certificate expires, when the entry may be
removed.

To facilitate this, augment the s3-test-srv with a new reset endpoint,
so that the integration test can query the contents of only the
most-recently-generated set of CRLs.

I have confirmed that the new integration test fails with
https://github.com/letsencrypt/boulder/pull/8072 reverted.

Fixes https://github.com/letsencrypt/boulder/issues/8083
2025-03-31 09:07:41 -07:00
Aaron Gable 037c654d3d
Move some python revocation tests to Go (#8082)
Delete several python revocation integration tests whose functionality
is already replicated by the go revocation integration tests. Add
support for revoking via admin-revoker to TestRevocation, and use that
to replace several more python tests.

The go versions of these tests use CRLs, rather than OCSP, to confirm
the revocation status of the certs in question. This is fine because the
purpose of these tests is to ensure that we handle revocation requests
correctly in general, not specifically via OCSP.

Part of https://github.com/letsencrypt/boulder/issues/8059
2025-03-28 18:57:39 -05:00
Aaron Gable 2c28c4799c
ProblemDetails no longer implements Error (#8078)
Remove the .Error() method from probs.ProblemDetails, so that it can no
longer be returned from functions which return an error. Update various
call sites to use the .String() method to get a textual representation
of the problem instead. Simplify ProblemDetailsForError to not
special-case and pass-through ProblemDetails, since they are no longer a
valid input to that function.

This reduces instances of "boxed nil" bugs, and paves the way for all of
the WFE methods to be refactored to simply return errors instead of
writing them directly into the response object.

Part of https://github.com/letsencrypt/boulder/issues/4980
2025-03-28 13:36:26 -05:00
Samantha Frank 082142867d
sfe/unpause: Test that identifiers appear on confirmation (#8087)
#8075 fixed a rendering issue caused by #8066, now test that
identifier(s) are always rendered as expected.
2025-03-28 13:12:05 -04:00
Aaron Gable 53c35ac669
WFE: Return errors from JWS-verification functions (#8077)
Change all of the helper methods and functions in verify.go to return an
`error` instead of a `probs.ProblemDetails`. Add a few new types to our
errors package, and support for those types in ProblemDetailsForError,
to maintain the same public-facing error types. Update the tests to
check for specific errors instead of specific problems.

This is a building block towards making the probs.ProblemDetails type
not implement the Error interface, and only be used when rendering
errors to the user (i.e. not within Boulder logic itself).

Part of https://github.com/letsencrypt/boulder/issues/4980
2025-03-26 18:06:03 -05:00
Aaron Gable 8b1a87ea8d
Simplify profile config hashing (#8081)
Remove the backwards-compatible profile hashing code. It is no longer
necessary, since all deployed profile configs now set
IncludeCRLDistributionPoints to true and set the UnsplitIssuance flag to
true. Catch up the CA and crl-updater configs to match config-next and
what is actively deployed in prod.

Part of https://github.com/letsencrypt/boulder/issues/8039
Part of https://github.com/letsencrypt/boulder/issues/8059
2025-03-26 17:59:18 -05:00
dependabot[bot] c881ce1f94
build(deps): bump github.com/redis/go-redis/v9 from 9.5.3 to 9.7.3 (#8079)
Bumps [github.com/redis/go-redis/v9](https://github.com/redis/go-redis)
from 9.5.3 to 9.7.3.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/redis/go-redis/releases">github.com/redis/go-redis/v9's
releases</a>.</em></p>
<blockquote>
<h2>v9.7.3</h2>
<h2>What's Changed</h2>
<ul>
<li>fix: handle network error on SETINFO (<a
href="https://redirect.github.com/redis/go-redis/issues/3295">#3295</a>)
(<a
href="https://github.com/redis/go-redis/security/advisories/GHSA-92cp-5422-2mw7">CVE-2025-29923</a>)</li>
<li>Deprecating misspelled <code>DisableIndentity</code> flag in the
client options.</li>
<li>Introducing <code>DisableIdentity</code> flag in the client
options.</li>
<li>Updating the documentation related to the new flag and the one that
was deprecated.</li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/redis/go-redis/compare/v9.7.1...v9.7.3">https://github.com/redis/go-redis/compare/v9.7.1...v9.7.3</a></p>
<h2>v9.7.1</h2>
<h1>Changes</h1>
<ul>
<li>Recognize byte slice for key argument in cluster client hash slot
computation (<a
href="https://redirect.github.com/redis/go-redis/issues/3049">#3049</a>)</li>
<li>fix(search&amp;aggregate):fix error overwrite and typo <a
href="https://redirect.github.com/redis/go-redis/issues/3220">#3220</a>
(<a
href="https://redirect.github.com/redis/go-redis/issues/3224">#3224</a>)</li>
<li>fix: linter configuration (<a
href="https://redirect.github.com/redis/go-redis/issues/3279">#3279</a>)</li>
<li>fix(search): if ft.aggregate use limit when limitoffset is zero (<a
href="https://redirect.github.com/redis/go-redis/issues/3275">#3275</a>)</li>
<li>Reinstate read-only lock on hooks access in dialHook to fix data
race (<a
href="https://redirect.github.com/redis/go-redis/issues/3225">#3225</a>)</li>
<li>fix: flaky ClientKillByFilter test (<a
href="https://redirect.github.com/redis/go-redis/issues/3268">#3268</a>)</li>
<li>chore: fix some comments (<a
href="https://redirect.github.com/redis/go-redis/issues/3226">#3226</a>)</li>
<li>fix(aggregate, search): ft.aggregate bugfixes (<a
href="https://redirect.github.com/redis/go-redis/issues/3263">#3263</a>)</li>
<li>fix: add unstableresp3 to cluster client (<a
href="https://redirect.github.com/redis/go-redis/issues/3266">#3266</a>)</li>
<li>Fix race condition in clusterNodes.Addrs() (<a
href="https://redirect.github.com/redis/go-redis/issues/3219">#3219</a>)</li>
<li>SortByWithCount FTSearchOptions fix (<a
href="https://redirect.github.com/redis/go-redis/issues/3201">#3201</a>)</li>
<li>Eliminate redundant dial mutex causing unbounded connection queue
contention (<a
href="https://redirect.github.com/redis/go-redis/issues/3088">#3088</a>)</li>
<li>Add guidance on unstable RESP3 support for RediSearch commands to
README (<a
href="https://redirect.github.com/redis/go-redis/issues/3177">#3177</a>)</li>
</ul>
<h2>🚀 New Features</h2>
<ul>
<li>Add guidance on unstable RESP3 support for RediSearch commands to
README (<a
href="https://redirect.github.com/redis/go-redis/issues/3177">#3177</a>)</li>
</ul>
<h2>🐛 Bug Fixes</h2>
<ul>
<li>fix(search): if ft.aggregate use limit when limitoffset is zero (<a
href="https://redirect.github.com/redis/go-redis/issues/3275">#3275</a>)</li>
<li>fix: add unstableresp3 to cluster client (<a
href="https://redirect.github.com/redis/go-redis/issues/3266">#3266</a>)</li>
<li>fix(aggregate, search): ft.aggregate bugfixes (<a
href="https://redirect.github.com/redis/go-redis/issues/3263">#3263</a>)</li>
<li>SortByWithCount FTSearchOptions fix (<a
href="https://redirect.github.com/redis/go-redis/issues/3201">#3201</a>)</li>
<li>Recognize byte slice for key argument in cluster client hash slot
computation (<a
href="https://redirect.github.com/redis/go-redis/issues/3049">#3049</a>)</li>
</ul>
<h2>Contributors</h2>
<p>We'd like to thank all the contributors who worked on this
release!</p>
<p><a
href="https://github.com/ofekshenawa"><code>@​ofekshenawa</code></a>, <a
href="https://github.com/Cgol9"><code>@​Cgol9</code></a>, <a
href="https://github.com/LINKIWI"><code>@​LINKIWI</code></a>, <a
href="https://github.com/shawnwgit"><code>@​shawnwgit</code></a>, <a
href="https://github.com/zhuhaicity"><code>@​zhuhaicity</code></a>, <a
href="https://github.com/bitsark"><code>@​bitsark</code></a>, <a
href="https://github.com/vladvildanov"><code>@​vladvildanov</code></a>,
<a href="https://github.com/ndyakov"><code>@​ndyakov</code></a></p>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/redis/go-redis/compare/v9.7.0...v9.7.1">https://github.com/redis/go-redis/compare/v9.7.0...v9.7.1</a></p>
<h2>9.7.0</h2>
<h1>Changes</h1>
<h2>🚀 New Features</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="a29d91d9ca"><code>a29d91d</code></a>
release 9.7.3, retract 9.7.2 (<a
href="https://redirect.github.com/redis/go-redis/issues/3314">#3314</a>)</li>
<li><a
href="ce3034c7b3"><code>ce3034c</code></a>
bump version to 9.7.2</li>
<li><a
href="0af2b32f93"><code>0af2b32</code></a>
fix: handle network error on SETINFO (<a
href="https://redirect.github.com/redis/go-redis/issues/3295">#3295</a>)
(CVE-2025-29923)</li>
<li><a
href="3d041a1dd6"><code>3d041a1</code></a>
release: 9.7.1 patch (<a
href="https://redirect.github.com/redis/go-redis/issues/3278">#3278</a>)</li>
<li><a
href="ed37c33a90"><code>ed37c33</code></a>
Updated package version [9.7] (<a
href="https://redirect.github.com/redis/go-redis/issues/3159">#3159</a>)</li>
<li><a
href="135f8e3b12"><code>135f8e3</code></a>
Fix field name spellings (<a
href="https://redirect.github.com/redis/go-redis/issues/3132">#3132</a>)
(<a
href="https://redirect.github.com/redis/go-redis/issues/3156">#3156</a>)</li>
<li><a
href="ac2e91d9d9"><code>ac2e91d</code></a>
Support Json with Resp 2 (<a
href="https://redirect.github.com/redis/go-redis/issues/3146">#3146</a>)
(<a
href="https://redirect.github.com/redis/go-redis/issues/3155">#3155</a>)</li>
<li><a
href="ec680aec14"><code>ec680ae</code></a>
Remove direct read from TLS underlying conn (<a
href="https://redirect.github.com/redis/go-redis/issues/3138">#3138</a>)
(<a
href="https://redirect.github.com/redis/go-redis/issues/3154">#3154</a>)</li>
<li><a
href="ad131f49b0"><code>ad131f4</code></a>
Updated package version (<a
href="https://redirect.github.com/redis/go-redis/issues/3134">#3134</a>)</li>
<li><a
href="d9eeed131a"><code>d9eeed1</code></a>
Fix Flaky Test: should handle FTAggregate with Unstable RESP3 Search
Module a...</li>
<li>Additional commits viewable in <a
href="https://github.com/redis/go-redis/compare/v9.5.3...v9.7.3">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/redis/go-redis/v9&package-manager=go_modules&previous-version=9.5.3&new-version=9.7.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-26 14:56:48 -07:00
James Renken 3f879ed0b4
Add Identifiers to Authorization & Order structs (#7961)
Add `identifier` fields, which will soon replace the `dnsName` fields,
to:
- `corepb.Authorization`
- `corepb.Order`
- `rapb.NewOrderRequest`
- `sapb.CountFQDNSetsRequest`
- `sapb.CountInvalidAuthorizationsRequest`
- `sapb.FQDNSetExistsRequest`
- `sapb.GetAuthorizationsRequest`
- `sapb.GetOrderForNamesRequest`
- `sapb.GetValidAuthorizationsRequest`
- `sapb.NewOrderRequest`

Populate these `identifier` fields in every function that creates
instances of these structs.

Use these `identifier` fields instead of `dnsName` fields (at least
preferentially) in every function that uses these structs. When crossing
component boundaries, don't assume they'll be present, for
deployability's sake.

Deployability note: Mismatched `cert-checker` and `sa` versions will be
incompatible because of a type change in the arguments to
`sa.SelectAuthzsMatchingIssuance`.

Part of #7311
2025-03-26 10:30:24 -07:00
Aaron Gable f71d2ea04f
WFE: Return err instead of prob from parseRevocation helper (#8076)
Change the wfe.parseRevocation function to return `error` instead of
`probs.ProblemDetails`. This slightly changes some of our user-facing
error messages to be more complete and verbose, thanks to how
ProblemDetailsForError works.

This is a building block towards making the probs.ProblemDetails type
not implement the Error interface, and only be used when rendering
errors to the user (i.e. not within Boulder logic itself).

Part of https://github.com/letsencrypt/boulder/issues/4980
2025-03-25 11:25:32 -07:00
Phil Porada d3669ebde9
sfe: Fix broken unpause form (#8075)
Fix incorrect struct member name cause broken unpause form caused by
https://github.com/letsencrypt/boulder/pull/8066. Add the `text/html` 
Content-Type header to all rendered templates.
2025-03-21 14:47:08 -07:00
James Renken b491abb051
va: Add RFC 8738 test cases (#8073)
Followup to #8020
2025-03-21 11:11:39 -07:00
James Renken b4308df0cc
identifier: Add FromCert & FromCSR; move Normalize from core (#8065)
Part of #7311
2025-03-19 17:03:59 -04:00
James Renken 9f4b18c6ce
identifier: Rename FromDNSNames & AsProto; add ACMEIdentifiers named type (#8070)
Rename `FromDNSNames` to `NewDNSSlice`, since it's exactly `NewDNS`
except for slices.

Rename `AsProto` to use the "To" prefix, since it's the opposite of
"From".

Add a named type `ACMEIdentifiers` so that we can add methods to slices.
We will have a lot of slice handling code coming up, which this will
make more elegant and readable.

Add a comment to explain naming conventions in the `identifier` package.

Part of #7311
Alternative to #8068
2025-03-19 17:03:39 -04:00
Aaron Gable b8eb2f2fe7
WFE: Return err instead of prob from updateAccount helper (#8062)
Fixes https://github.com/letsencrypt/boulder/issues/7936
2025-03-19 09:34:48 -07:00
Jacob Hoffman-Andrews 0a726370b9
crl/updater: fix lookback period (#8072)
We were adding the lookback period to `clk.Now()` but should have been
subtracting it. Includes a unittest, which I've verified fails against
the pre-fix code.
2025-03-18 10:39:29 -07:00
Aaron Gable 75a89f7a4a
Simplify and fix CRL observer IDP check (#8069)
The conditional introduced in
https://github.com/letsencrypt/boulder/pull/8067 contained a bug left
over from an earlier draft of the PR. Remove the zero-length check to
ensure the code matches the documented intent.
2025-03-17 14:34:14 -07:00
Aaron Gable 6071bedb52
Use PKIMetal to lint CRLs in CI (#8061)
Add a new custom lint which sends CRLs to PKIMetal, and configure it to
run in our integration test environment. Factor out most of the code
used to talk to the PKIMetal API so that it can be shared by the two
custom lints which do so. Add the ability to configure lints to the
CRLProfileConfig, so that zlint knows where to load the necessary custom
config from.
2025-03-14 16:28:56 -07:00
Aaron Gable d045b387ef
Observer: detect CRL IDP mismatch (#8067)
Give boulder-observer the ability to detect if the CRL it fetches is the
CRL it expects, by comparing that CRLs issuingDistributionPoint
extension to the prober's configured URL. Only do this if instructed to
(by configuring the CRL prober as "partitioned") because non-partitioned
CRLs do not necessarily contain an IDP.

Fixes https://github.com/letsencrypt/boulder/issues/7527
2025-03-14 14:52:29 -07:00
Aaron Gable ebf232cccb
Return updated account object on DeactivateRegistration path (#8060)
Update the SA to re-query the database for the updated account after
deactivating it, and return this to the RA. Update the RA to pass this
value through to the WFE. Update the WFE to return this value, rather
than locally modifying the pre-deactivation account object, if it gets
one (for deployability).

Also remove the RA's requirement that the request object specify its
current status so that the request can be trimmed down to just an ID.
This proto change is backwards-compatible because the new
DeactivateRegistrationRequest's registrationID field has the same type
(int64) and field number (1) as corepb.Registration's id field.

Part of https://github.com/letsencrypt/boulder/issues/5554
2025-03-14 14:17:42 -07:00
James Renken cb94164b54
policy: Add initial Identifier support (#8064)
Change WillingToIssue and WellFormedDomainNames to use Identifiers, and
(for now) reject non-DNS identifiers.

Part of #7311
2025-03-14 11:34:59 -07:00
James Renken edc3c7fa6d
Shorten "identifier(s)" in variable names & function arguments (#8066)
For consistency, and to prevent confusion with the `identifier` package,
use "ident(s)" instead.

Part of #7311
2025-03-14 10:59:38 -07:00
Aaron Gable 767c5d168b
Improve how cert-checker runs lints (#8063)
Give cert-checker the ability to load zlint configs, so that it can be
configured to talk to PKIMetal in CI and hopefully in staging/production
in the future.

Also update how cert-checker executes lints, so that it uses a real lint
registry instead of using the global registry and passing around a
dictionary of lints to filter out of the results.

Fixes https://github.com/letsencrypt/boulder/issues/7786
2025-03-13 16:35:09 -07:00
Samantha Frank 5889d6a2a6
ceremony/issuance: Remove PolicyIdentifiers extension and default to Policies (#7969) 2025-03-12 21:30:06 -04:00
Aaron Gable 358bdab8f4
Replace pkilint with pkimetal in CI (#8058)
Replace the bpkilint container with a new bpkimetal container. Update
our custom lint which calls out to that API to speak PKIMetal's (very
similar) protocol instead. Update our zlint custom configuration to
configure this updated lint.

Fixes https://github.com/letsencrypt/boulder/issues/8009
2025-03-12 12:21:40 -07:00
Samantha Frank 428fcb30de
ARI: Store and reflect optional "replaces" value for Orders (#8056)
- Plumb the "replaces" value from the WFE through to the SA via the RA
- Store validated "replaces" value for new orders in the orders table
- Reflect the stored "replaces" value to subscribers in the order object
- Reorder CertificateProfileName before Replaces/ReplacesSerial in RA
and SA protos for consistency

Fixes #8034
2025-03-12 15:09:29 -04:00
Samantha Frank 3a33aa9f8b
ARI: Return alreadyReplaced error instead of conflict (#8053)
Return "alreadyReplaced" in addition to HTTP 409 Conflict to signal that
an order indicates that it replaces a certificate which already has a
replacement order.
2025-03-12 15:08:43 -04:00
マルコメ adf1d06d64
add `syntax` parser directive to Dockerfile (#8055)
As recommended by https://docs.docker.com/build/concepts/dockerfile/#dockerfile-syntax
2025-03-11 17:09:11 -07:00
Aaron Gable 077c3c5db1
Remove go1.23 from CI and update go.mod to go1.24 (#8052)
We have upgraded to go1.24.1 in production, and no longer need to test
go1.23.x. Updating the version in our go.mod also allows us to begin
using x509.Certificate.Policies instead of .PolicyIdentifiers.
2025-03-11 12:45:03 -07:00
Samantha Frank c9557c8c27
database: Do not skip replacementOrders tests for config (#8057)
The replacementOrders table was moved from db-next to db back in #7639.
2025-03-11 12:30:23 -04:00
Aaron Gable dc14caf907
Add MPICFullResults feature flag to turn off VA early return (#8046)
Add a new "MPICFullResults" feature flag. When this flag is enabled in
the VA, it will wait for all Remote VAs to return their results for both
Domain Control Validation and CAA checking, rather than short-circuiting
as soon as it has seen enough results to know whether corroboration will
or will not be achieved.

We make this change because waiting for these to return honestly doesn't
take that long, because we do validation (although not CAA rechecking)
asynchronously, and because it improves the quality of our MPIC quorum
summary logs (so we don't always say only 3/4 concurred because the
fourth was cancelled).

Fixes https://github.com/letsencrypt/boulder/issues/7809
2025-03-11 08:49:05 -07:00
Aaron Gable ad651d4a3d
Update PSL (#8050)
Update the Public Suffix List (last updated in August 2024).

Transitively update various golang.org/x/ packages, as used by the
publicsuffix-go repo:
- /x/crypto: v0.32.0 -> v0.36.0
- /x/net: v0.29.0 -> v0.37.0
- /x/sync: v0.10.0 -> v0.12.0
- /x/term: v0.28.0 -> v0.30.0
- /x/text: v0.21.0 -> v0.23.0
- /x/sys: v0.29.0 -> v0.31.0
2025-03-10 12:58:44 -07:00
Eng Zer Jun eac26b8edb
Populate x509.Certificate.Policies field (#7940)
Populate the new x509.Certificate.Policies field everywhere we currently populate the x509.Certificate.PolicyIdentifiers field. This allows Go to use whichever field it prefers (go1.23 prefers PolicyIdentifiers, go1.24 prefers Policies) as the source of truth when serializing a certificate.

Part of https://github.com/letsencrypt/boulder/issues/7148
2025-03-10 11:48:51 -07:00
Aaron Gable df23344dbf
Update CI to go1.23.7 and go1.24.1 (#8051)
These versions contain security fixes to the net/http package, but not
to the parts of it which we use.
2025-03-10 11:28:31 -07:00
Aaron Gable 2ac1ac0f39
WFE: Don't remove contacts on empty update-account request (#8049)
When we receive an update-account request which is not empty, but
doesn't contain the "contact" field, don't assume that they want to
remove their contacts. Only remove contacts if the "contact" field is
present, but empty.

Add a unit test and an integration test which will catch regressions in
this behavior.
2025-03-07 14:54:15 -08:00
Aaron Gable dd566a959c
Fix TestMultiVAEarlyReturn (#8045)
Previously this test was passing not because the VA was returning early,
but because the fake HTTP server was only sleeping for 1000 nanoseconds
instead of 1000 milliseconds. The test cases were not exercising the
VA's early-return codepath, because they do not include sufficiently
high ratios of passing or failing remotes to hit quorum early.

Fix the sleep time so the fake HTTP server works as expected, and reduce
the (desired) sleep time from 1000ms to 100ms because that's more than
sufficient for the behavior we're testing.

Fix and diversify the test cases to actually hit positive or negative
quorum, so that the VA's early-return codepath is actually exercised.

This PR will be followed by a non-test PR which removes this
early-return codepath and modifies this test further, but I thought it
was important to have this test in fully working order before modifying
the code it tests.

Part of https://github.com/letsencrypt/boulder/issues/7809
2025-03-07 14:05:24 -08:00
Samantha Frank f8d1d85349
wfe: Remove SendContacts call from updateAccount (#8048)
PR #8018 integrated the email-exporter service with WFE, updating
wfe.NewAccount and wfe.updateAccount to submit valid email contacts to
the Salesforce Pardot API. However, our new_or_updated_contact metric
shows that (account) contact updates currently exceed the highest
Salesforce tier’s daily submission limit by several times.

This change can be reverted if additional filtering logic reduces
updated (+ new) account contacts below the daily submission limit.
2025-03-07 15:33:31 -05:00
Jacob Hoffman-Andrews 98b6d3f8bf
crl-updater: remove deprecated options (#8021)
Note: the issues listed in the TODOs (#6438 and #7023) are already
closed.
2025-03-07 11:27:49 -08:00
Aaron Gable 12e660874d
Reduce flakiness in crl-updater integration tests (#8044)
Remove crl-updater from the list of services run by startservers.py, so
that it isn't running at the same time as the crl-updater instances run
by specific integration tests. In return, add a new integration test
which starts crl-updater and waits for it to listen on its debug port,
just like startservers does.

Also make the existing crl-updater integration tests more robust and
more parallelizable by having them always reset the leasedUntil column
before executing the updater, instead of requiring each individual test
to perform that reset.

Fixes https://github.com/letsencrypt/boulder/issues/7590
2025-03-07 09:38:02 -08:00
Jacob Hoffman-Andrews 7aebcb1aeb
ra: deprecate UnsplitIssuance flag (#8043)
Remove some RA tests that were checking for errors specific to the split
issuance flow. Make one of the tests test GetSCTs directly, which makes
for a much nicer test!
2025-03-06 13:43:06 -08:00
Samantha Frank b1e4721d1a
cmd/email-exporter: Initial implementation and integration with WFE (#8018)
Add a new boulder service, email-exporter, which uses the Pardot API
client added in #8016 and the email.Exporter gRPC service added in
#8017.

Add pardot-test-srv, a test-only service for mocking communication with
Salesforce OAuth and Pardot APIs in non-production environments. Since
Salesforce does not provide Pardot functionality in developer sandboxes,
pardot-test-srv must run in all non-production environments (e.g.,
sre-development and staging).

Integrate the email-exporter service with the WFE and modify
WFE.NewAccount and WFE.UpdateAccount to submit valid email contacts.
Ensure integration tests verify that contacts eventually reach
pardot-test-srv.

Update configuration where necessary to:
- Build pardot-test-srv as a standalone binary.
- Bring up pardot-test-srv and cmd/email-exporter for integration
testing.
- Integrate WFE with cmd/email-exporter when running test/config-next.

Closes #7966
2025-03-06 15:20:55 -05:00
James Renken 3e6a8e2d25
va: Support IP address identifiers (#8020)
Add an `identifier` field to the `va.PerformValidationRequest` proto, which will soon replace its `dnsName` field.

Accept and prefer the `identifier` field in every VA function that uses this struct. Don't (yet) assume it will be present.

Throughout the VA, accept and handle the IP address identifier type. Handling is similar to DNS names, except that `getAddrs` is not called, and consider that:
- IPs are represented in a different field in the `x509.Certificate` struct.
- IPs must be presented as reverse DNS (`.arpa`) names in SNI for [TLS-ALPN-01 challenge requests](https://datatracker.ietf.org/doc/html/rfc8738#name-tls-with-application-layer-).
- IPv6 addresses are enclosed in square brackets when composing or parsing URLs.

For HTTP-01 challenges, accept redirects to bare IP addresses, which were previously rejected.

Fixes #2706
Part of #7311
2025-03-06 11:39:22 -08:00
Aaron Gable 5822ba3c20
CAA: Handle non-empty RRSets correctly during wildcard checking (#8033)
When checking CAA, issuance is allowed if the relevant RRSet (as defined
in RFC 8659, Section 3) does not contain any records of the right
Property kind (issue or issuewild) for the kind of checking being
attempted. Previously, we correctly detected that a non-wildcard
issuance attempt could short-circuit our validation logic if no issue
records are present. However, we did not do a similar short-circuit for
wildcard issuance attempts when no issue records and no issuewild
records are present.

Add a test which demonstrates that a nearly-empty RRSet accidentally
forbade issuance of wildcard certs. Update our logic to perform the "no
relevant records" check slightly later, so that it catches both the
wildcard and non-wildcard cases, causing the new test to pass.

Fixes https://github.com/letsencrypt/boulder/issues/8032
2025-03-06 09:49:53 -08:00
Aaron Gable 1a3f898e7e
crl: Improve crlNumber and thisUpdate comparison (#8037)
Fixes https://github.com/letsencrypt/boulder/issues/8036
2025-03-06 08:01:03 -08:00
Aaron Gable a00821ada6
Scale ARI suggested window to cert lifetime (#8024)
Compute the width of the ARI suggested renewal window as 2% of the
validity period. This means that 90-day certificates have their
suggested window shrink slightly from 48 hours to 43.2 hours, and gives
six-day (160h) certs a suggested window 3.2 hours wide.

Also move the center of that window to the midpoint of the certificate
validity period for certs which are valid for less than 10 days, so that
operators have (proportionally) a little more time to respond to renewal
issues.

Fixes https://github.com/letsencrypt/boulder/issues/7996
2025-03-05 15:32:25 -08:00
Samantha Frank 6b85b3480b
email/exporter: Add email.Exporter gRPC service (#8017)
Initial implementation of the email.Exporter gRPC service to be used by
the new cmd/email-exporter.

Part of #7966
2025-03-05 12:18:32 -05:00
James Renken 49ebc99e8e
va: Put most recent, not original, IP in error messages (add'l case) (#8028)
Fix a remaining edge case after #7468: one call to `newIPError` did not
account for when we retry *successfully,* but then are served a redirect
which errors. In those cases, our `client.Do` call results in our
redirect handler `processRedirect` appending yet another validation
record to `records`, which was missed.

Fixes #7347
2025-03-04 11:35:16 -08:00
Aaron Gable 28b49a82d4
SA: Improve concurrency robustness of CRL leasing transactions (#8030)
In a few places within the SA, we use explicit transactions to wrap
read-then-update style operations. Because we set the transaction
isolation level on a per-session basis, these transactions do not in
fact change their isolation level, and therefore generally remain at the
default isolation level of REPEATABLE READ.

Unfortunately, we cannot resolve this simply by converting the SELECT
statements into SELECT...FOR UPDATE statements: although this would fix
the issue by making those queries into locking statements, it also
triggers what appears to be an InnoDB bug when many transactions all
attempt to select-then-insert into a table with both a primary key and a
separate unique key, as the crlShards table has. This causes the
integration tests in GitHub Actions, which run with an empty database
and therefore use the needToInsert codepath instead of the update
codepath, to consistently flake.

Instead, resolve the issue by having the UPDATE statements specify that
the value of the leasedUntil column is still the same as was read by the
initial SELECT. Although two crl-updaters may still attempt these
transactions concurrently, the UPDATE statements will still be fully
sequenced, and the latter one will fail.

Part of https://github.com/letsencrypt/boulder/issues/8031
2025-03-03 15:29:57 -08:00
Samantha Frank e6c812a3db
va/ra: Deprecate EnforceMultiCAA and EnforceMPIC (#8025)
Replace DCV and CAA checks (PerformValidation and IsCAAValid) in
va/va.go and va/caa.go with their MPIC compliant counterparts (DoDCV and
DoCAA) in va/vampic.go. Deprecate EnforceMultiCAA and EnforceMPIC and
default code paths as though they are both true. Require that RIR and
Perspective be set for primary and remote VAs.

Fixes #7965
Fixes #7819
2025-03-03 16:33:27 -05:00
Samantha Frank 1f5ee7c645
wfe: Remove an impossible condition (#8035)
We already check !isARIRenewal on the line above.
2025-03-03 16:21:48 -05:00
Aaron Gable 10d9ef9af7
Make crl-updater deadline setting clearer (#8027)
Rather than using context.WithTimeout to magically convert a timeout to
a deadline for us, compute the deadline ourselves and then set it
directly on the context.
2025-02-28 15:24:37 -08:00
Samantha Frank f6702fffe8
email/pardot: Add a Pardot API client (#8016)
Initial implementation of the Pardot API client to be used by the new
email.Exporter gRPC service.

Part of #7966
2025-02-28 16:13:39 -05:00
Aaron Gable a2141cb695
RA: Control MaxNames via profile (#8019)
Add MaxNames to the set of things that can be configured on a
per-profile basis. Remove all references to the RA's global maxNames,
replacing them with reference's to the current profile's maxNames. Add
code to the RA's main() to copy a globally-configured MaxNames into each
profile, for deployability.

Also remove any understanding of MaxNames from the WFE, as it is
redundant with the RA and is not configured in staging or prod. Instead,
hardcode the upper limit of 100 into the ratelimit package itself.

Fixes https://github.com/letsencrypt/boulder/issues/7993
2025-02-27 15:51:00 -06:00
Samantha Frank 1fbaf9f8a9
sfe: Replace text/template with html/template (#8026) 2025-02-26 17:44:03 -05:00
dependabot[bot] d27f0c8a96
build(deps): bump github.com/go-jose/go-jose/v4 from 4.0.1 to 4.0.5 (#8022)
Changelog: https://github.com/go-jose/go-jose/compare/v4.0.1...v4.0.5
2025-02-25 08:40:44 -08:00
Jacob Hoffman-Andrews 692bd53ae5
ca: unsplit issuance flow (#8014)
Add a new RPC to the CA: `IssueCertificate` covers issuance of both the
precertificate and the final certificate. In between, it calls out to
the RA's new method `GetSCTs`.

The RA calls the new `CA.IssueCertificate` if the `UnsplitIssuance`
feature flag is true.

The RA had a metric that counted certificates by profile name and hash.
Since the RA doesn't receive a profile hash in the new flow, simply
record the total number of issuances.

Fixes https://github.com/letsencrypt/boulder/issues/7983
2025-02-24 11:37:17 -08:00
Samantha Frank fb2fd8af75
noncebalancer: Fix race on Picker.prefixToBackend initialization (#8015) 2025-02-20 18:20:38 -05:00
Aaron Gable d9433fe293
Remove 'RETURNING' functionality from MultiInserter (#7740)
Deprecate the "InsertAuthzsIndividually" feature flag, which has been
set to true in both Staging and Production. Delete the code guarded
behind that flag being false, namely the ability of the MultiInserter to
return the newly-created IDs from all of the rows it has inserted. This
behavior is being removed because it is not supported in MySQL / Vitess.

Fixes https://github.com/letsencrypt/boulder/issues/7718

---

> [!WARNING]
> ~~Do not merge until IN-10737 is complete~~
2025-02-19 14:37:22 -08:00
Aaron Gable 212a66ab49
Update go versions in CI and release (#7971)
Update from go1.23.1 to go1.23.6 for our primary CI and release builds.
This brings in a few security fixes that aren't directly relevant to us.

Add go1.24.0 to our matrix of CI and release versions, to prepare for
switching to this next major version in prod.
2025-02-19 14:37:01 -08:00
Aaron Gable eab90ee2f5
Remove unused non-ACME /get/ paths for orders and authzs (#8010)
These paths receive (literally) zero traffic, and they require the WFE
to duplicate the RA's authorization lifetime configuration. Since that
configuration is now per-profile, the WFE can no longer easily replicate
it, and the resulting staleness calculations will be wrong. Remove the
duplicated configuration, remove the unused endpoints that rely on it,
and remove the staleness-checking code which supported those endpoints.

Leave the non-ACME /get/ endpoint for certificates in place, because
checking staleness for those does not require any additional
configuration, and having a non-ACME serial-based API for certificates
is a good thing.

Fixes https://github.com/letsencrypt/boulder/issues/8007
2025-02-14 10:21:00 -08:00
Jacob Hoffman-Andrews e0e5a17899
crl: add cache control headers (#8011)
The crl-storer passes along Cache-Control and Expires from the
crl-updater (because the crl-updater knows the UpdatePeriod).

The crl-updater calculates the Expires header based on when it expects
to update the CRL, plus a margin of error.

Fixes #8004
2025-02-13 14:20:29 -08:00
Jacob Hoffman-Andrews a8b2fd6960
test: increase pkilint timeout (#8008)
Increase pkilint timeout from 200ms to 2s. In #8006 I found that errors
were stemming from timeouts talking to the bpkilint container. These
probably showed up in TestRevocation particularly because that
integration test now issues for many certificates in parallel. Pkilint's
slowness, combined with the relatively small number of cores in CI,
probably resulted in some requests taking too long.
2025-02-12 10:10:02 -08:00
Aaron Gable 63a0e500ed
Create profiles integration test (#8003)
This wasn't previously possible because eggsampler/acme didn't support
profiles until late last week.
2025-02-11 15:47:41 -08:00
Aaron Gable 3e4bc168ae
RA: Clean up deprecated validation configuration (#7992)
Remove the RA's deprecated top-level config keys which used to control
order and authz lifetimes. Make the new profile-based config keys which
replaced them required.

Since configuring a profile and default profile name is now mandatory,
always supply a profile name to the CA when requesting issuance.

Fixes https://github.com/letsencrypt/boulder/issues/7986
2025-02-11 14:35:11 -08:00
Aaron Gable a9e3ad1143
CA: Require RA to always provide profile name (#7991)
Deprecate the CA's DefaultCertificateProfileName config key, now that
default profile selection is being handled by the RA instead.

Part of https://github.com/letsencrypt/boulder/issues/7986
2025-02-11 13:10:29 -08:00
Aaron Gable 0efb2a026d
Make authz reuse expiry cutoff proportional to authz lifetime (#8000)
Continue to use a 24-hour cutoff for authzs with "long" lifetimes, so
that our behavior is unchanged for authzs created with no profile
specified. Use a 1-hour cutoff for authzs with "short" (less than
24-hour) lifetimes, so that we can reuse authzs created with modern
profiles. Use linear interpolation between those values.

Fixes https://github.com/letsencrypt/boulder/issues/7994
2025-02-11 08:41:21 -08:00
James Renken 64f4aabbf3
admin: Remove deprecated debugAddr (#7999)
The parameter was removed in production in IN-10874.

Followup to #7838, #7840
2025-02-10 12:26:57 -08:00
Aaron Gable 27cbd1c94c
Fix RA profile deployability issue (#8002)
If validation profiles haven't been explicitly configured, use the
default profile for all incoming requests regardless of which profile
they specify.

Fixes https://github.com/letsencrypt/boulder/issues/7605
2025-02-10 09:33:58 -08:00
James Renken f6c748c1c3
WFE/nonce: Remove deprecated NoncePrefixKey field (#7825)
Remove the deprecated WFE & nonce config field `NoncePrefixKey`, which
has been replaced by `NonceHMACKey`.

<del>DO NOT MERGE until:</del>
- <del>#7793 (in `release-2024-11-18`) has been deployed, AND:</del>
- <del>`NoncePrefixKey` has been removed from all running configs.</del>

Fixes #7632
2025-02-06 15:32:49 -08:00
Aaron Gable f66d0301c5
VA: Remove unnecessary wrapper function (#7997)
This function lost its purpose when we made it so all VA functions
return errors instead of problems in
https://github.com/letsencrypt/boulder/pull/7313.
2025-02-05 10:28:58 -08:00
Jacob Hoffman-Andrews eda496606d
crl-updater: split temporal/explicit sharding by serial (#7990)
When we turn on explicit sharding, we'll change the CA serial prefix, so
we can know that all issuance from the new prefixes uses explicit
sharding, and all issuance from the old prefixes uses temporal sharding.
This lets us avoid putting a revoked cert in two different CRL shards
(the temporal one and the explicit one).

To achieve this, the crl-updater gets a list of temporally sharded
serial prefixes. When it queries the `certificateStatus` table by date
(`GetRevokedCerts`), it will filter out explicitly sharded certificates:
those that don't have their prefix on the list.

Part of #7094
2025-02-04 11:45:46 -05:00
Aaron Gable 2f8c6bc522
RA: Use Validation Profiles to determine order/authz lifetimes (#7989)
Add three new fields to the ra.ValidationProfile structure, representing
the profile's pending authorization lifetime (used to assign an
expiration when a new authz is created), valid authorization lifetime
(used to assign an expiration when an authz is successfully validated),
and order lifetime (used to assign an expiration when a new order is
created). Remove the prior top-level fields which controlled these
values across all orders.

Add a "defaultProfileName" field to the RA as well, to facilitate
looking up a default set of lifetimes when the order doesn't specify a
profile. If this default name is explicitly configured, always provide
it to the CA when requesting issuance, so we don't have to duplicate the
default between the two services.

Modify the RA's config struct in a corresponding way: add three new
fields to the ValidationProfiles structure, and deprecate the three old
top-level fields. Also upgrade the ra.NewValidationProfile constructor
to handle these new fields, including doing validation on their values.

Fixes https://github.com/letsencrypt/boulder/issues/7605
2025-02-04 11:44:43 -05:00
Aaron Gable 6695895f8b
RA: Don't reuse authzs with mismatched profiles (#7967)
In the RA, inspect the profile of all authorizations returned when
looking for authz reuse, and refuse to reuse any whose profile doesn't
match the requested profile of the current NewOrder request.

Fixes https://github.com/letsencrypt/boulder/issues/7949
2025-02-03 16:47:35 -05:00
Samantha Frank 1d2601515b
RA: Count new registrations with contacts (#7984)
Adding a temporary metric to estimate the rate of new contacts for
accounts.

Part of #7966
2025-02-03 11:50:43 -05:00
Jacob Hoffman-Andrews f11475ccc3
issuance: add CRLDistributionPoints to certs (#7974)
The CRLDP is included only when the profile's
IncludeCRLDistributionPoints field is true.

Introduce a new config field for issuers, CRLShards. If
IncludeCRLDistributionPoints is true and this is zero, issuance will
error.

The CRL shard is assigned at issuance time based on the (random) low
bits of the serial number.

Part of https://github.com/letsencrypt/boulder/issues/7094
2025-01-30 14:39:22 -08:00
Aaron Gable c5a28cd26d
WFE: Refuse to finalize orders with unrecognized profiles (#7988)
The current profiles draft
(https://datatracker.ietf.org/doc/draft-aaron-acme-profiles/00/) says:

> If a server receives a request to finalize an Order whose profile the
> CA is no longer willing to issue under, it MUST respond with a
> problem document of type "invalidProfile".  The server SHOULD attempt
> to avoid this situation, e.g. by ensuring that all Orders for a
> profile have expired before it stops issuing under that profile.

Add types and helper functions representing this new error type to the
berrors, probs, and web packages. Update the WFE code which rejects
new-order requests with unrecognized profiles to use these new types,
and add similar code to the WFE's finalize path. Update the unit and
integration tests to reflect the fact that we now configure at least one
profile in both Staging and Prod (tracked in IN-10574).
2025-01-30 14:10:02 -08:00
Aaron Gable 1ae184713b
Remove duplicate check from wfe.FinalizeOrder (#7987)
This check is duplicated in the next stanza. Instead, replace it with a
check that the acctID encoded in the URL and the acctID corresponding to
the JWS used to sign the request match.
2025-01-30 11:57:05 -08:00
Jacob Hoffman-Andrews d93f0c316a
issuance: add new IncludeCRLDistributionPoints bool (#7985)
To achieve this without breaking hashes of deployed configs, create a
ProfileConfigNew containing the new field (and removing some deprecated
fields).

Move the CA's profile-hashing logic into the `issuance` package, and
gate it on the presence of IncludeCRLDistributionPoints. If that field
is false (the default), create an instance of the old `ProfileConfig`
with the appropriate values and encode/hash that instead.

Note: the IncludeCRLDistributionPoints field does not yet control any
behavior. That will be part of #7974.

Part of #7094
2025-01-30 11:48:54 -08:00
Samantha Frank c7da1201db
RA: Make ProfileSelectionAllowList test clearer (#7981)
Improve the test of the `ra.validationProfiles` field by providing a
constructed `map[string]*ValidationProfile` instead of constructing one
inside the test. Much like how this data is provided, or `nil`, in calls
to `ra.NewRegistrationAuthorityImpl()`.
2025-01-28 15:54:31 -05:00
Jacob Hoffman-Andrews 5cc29be589
doc: add documentation for CRL generation (#7980)
Part of #7094
2025-01-27 15:52:55 -08:00
Aaron Gable 86ab2ed245
SA: Support profiles associated with authorizations (#7956)
Add "certificateProfileName" to the model used to insert new authz2 rows
and to the list of column names read when retrieving rows from the
authz2 table. Add support for this column to the functions which convert
to and from authz2 model types.

Add support for the profile field to core types so that it can be
returned by the SA.

Fixes https://github.com/letsencrypt/boulder/issues/7955
2025-01-27 14:53:30 -08:00
Samantha Frank 811e6073d1
ra: Gate OCSP Must-Staple issuance on account-based allow list (#7976)
Add support in the RA for an allow list of accounts permitted to request
certificates containing the OCSP Must-Staple extension. If no allow list
is configured, all accounts are permitted. When a list is provided,
Finalize requests with Must-Staple are rejected unless the account is on
the list, and metrics are updated to track allowed and denied requests.

Fixes #7914
2025-01-27 14:53:11 -08:00
dependabot[bot] 888581b386
build(deps): bump golang.org/x/sys from 0.25.0 to 0.29.0 (#7927)
Updates /x/sys from v0.25.0 to v0.29.0.
- Changelog: https://go.googlesource.com/sys/+log/v0.25.0..v0.29.0
- Diff: https://go.googlesource.com/sys/+diff/v0.25.0..v0.29.0

Also updates these transitive dependencies, none of which result in any changes to our vendored code:
- /x/crypto from v0.27.0 to v0.32.0
- /x/sync from v0.8.0 to v0.10.0
- /x/term from v0.24.0 to v0.28.0
- /x/text from v0.18.0 to v0.21.0
2025-01-27 14:49:45 -08:00
Jacob Hoffman-Andrews 55b8cbef6c
tests: increase wfe log level (#7982)
We've been seeing some flaky integration tests where issuance fails. The
integration test only has access to the generic user-facing error. The
real error is available as `InternalError` in the WFE logs, but we need
a higher log level to see it.
2025-01-27 11:24:08 -08:00
Jacob Hoffman-Andrews e0221b6bbe
crl-updater: query by explicit shard too (#7973)
Add querying by explicit shard (SA.GetRevokedCertsByShard) in addition
to querying by temporal shard (SA.GetRevokedCerts).

Merge results from both kinds of shard. De-duplicate by serial within a
shard, because the same certificate could wind up in a temporal shard
that matches its explicit shard.

When de-duplicating, validate that revocation reasons are the same or
(very unlikely) represent a re-revocation based on demonstrating key
compromise. This can happen because the two different SA queries occur
at slightly different times.

Add unit testing that CRL entries make it through the whole pipeline
from SA, to CA, to uploader.

Rename some types in the unittest to be more accessible.

Tweak a comment in SA.UpdateRevokedCertificate to make it clear that
status _and_ reason are critical for re-revocation.

Note: This GetRevokedCertsByShard code path will always return zero
certificates right now, because nothing is writing to the
`revokedCertificates` table. Writing to that table is gated on
certificates having CRL URLs in them, which is not yet implemented (and
will be config-gated).

Part of #7094
2025-01-27 10:11:09 -08:00
James Renken 3fcaebe934
core: Remove contactsPresent from Registration (#7952)
Remove the `contactsPresent` field from `corepb.Registration`, and all
places where it is set. #7933 removed all places where it was used.

Fixes #7920
2025-01-25 17:46:52 -08:00
Samantha Frank 8ab022e8c9
test: No longer accumulate orphans on each test run (#7978)
Stop producing orphans and `No such container: boulder_tests` on each
test invocation.
2025-01-24 13:28:58 -08:00
James Renken dbb248eba6
wfe: Fix updatedOrder check (#7977)
Since its introduction, this check has been evaluating `order` - but in
context, it must be meant to evaluate `updatedOrder`.
2025-01-24 12:28:58 -08:00
Samantha Frank a78efb82b5
RA: Allow profile selection to be gated on account-based allow lists (#7959)
Use the new allowlist package added in #7958 to implement an
account-based allow list for profile selection in the RA.

Part of #7604
2025-01-24 12:27:24 -05:00
Shiloh Heurich 2d1f277635
Fix typos in documentation and error messages (#7975)
- Fix 'requesteed' -> 'requested' in errors/errors.go
- Fix 'paylod' -> 'payload' in docs/acme-divergences.md

These changes address typos identified by the linter.
2025-01-24 08:10:06 -08:00
Jacob Hoffman-Andrews a8074d2e9d
test: add more testing for CRL revocation (#7957)
In revocation_test.go, fetch all CRLs, and look for revoked certificates
on both CRLs and OCSP.

Make s3-test-srv listen on all interfaces, so the CRL URLs in the CA
config work.

Add IssuerNameIDs to the CRL URLs in ca.json, to match how those CRLs
are uploaded to S3.

Make TestRevocation parallel. Speedup from ~60s to ~3s.

Increase ocsp-responder's allowed parallelism to account for parallel
test. Also, add "maxInflightSignings" to config/ since it's in prod.
"maxSigningWaiters" is not yet in prod, so don't move that field.

Add a mutex around running crl-updater, and decrease the log level so
errors stand out more when they happen.
2025-01-23 18:49:55 -08:00
Samantha Frank ca73500467
integration: Fix typo in TestReRevocation (#7970) 2025-01-22 13:50:48 -08:00
Jacob Hoffman-Andrews 02af55293e
sa: add GetRevokedCertsByShard (#7946)
The SA had some logic (not yet in use) to return revoked certificates
either by temporal sharding (if `req.ShardIdx` is zero) or by explicit
sharding (if `req.ShardIdx` is nonzero).

This PR splits the function into two. The existing `GetRevokedCerts`
always does temporal sharding. The new `GetRevokedCertsByShard` always
does explicit sharding. Eventually only `GetRevokedCertsByShard` will be
necessary. This change was discussed in
https://github.com/letsencrypt/boulder/issues/7094#issuecomment-2587940962
and is a precursor to having the crl-updater call both methods, so we
can merge the results when generating CRLs.
2025-01-22 09:46:57 -08:00
Jacob Hoffman-Andrews a9080705b4
ra: revoke with explicit CRL shard (#7944)
In RA.RevokedCertificate, if the certificate being revoked has a
crlDistributionPoints extension, parse the URL and pass the appropriate
shard to the SA.

This required some changes to the `admin` tool. When a malformed
certificate is revoked, we don't have a parsed copy of the certificate
to extract a CRL URL from. So, specifically when a malformed certificate
is being revoked, allow specifying a CRL shard. Because different
certificates will have different shards, require one-at-a-time
revocation for malformed certificates.

To support that refactoring, move the serial-cleaning functionality
earlier in the `admin` tool's flow.

Also, split out one of the cases handled by the `revokeCertificate`
helper in the RA. For admin malformed revocations, we need to accept a
human-specified ShardIdx, so call the SA directly in that case (and skip
stat increment since admin revocations aren't useful for metrics). This
allows `revokeCertificate` to be a more helpful helper, by extracting
serial, issuer ID, and CRL shard automatically from an
`*x509.Certificate`.

Note: we don't yet issue certificates with the crlDistributionPoints
extension, so this code will not be active until we start doing so.

Part of #7094.
2025-01-21 21:31:40 -08:00
Samantha Frank c971a053a2
RA: Replace IsCAAValid call with DoCAA (#7962)
Replace the non-MPIC-compliant IsCAAValid VA method with the correct
MPIC-compliant DoCAA VA method when the EnforceMPIC feature is enabled.
This fixes the mistake introduced in #7870.
2025-01-21 11:31:48 -08:00
Samantha Frank 87a52d6fad
RA: Delete legacy rate limit metrics (#7960)
Remove two legacy rate limits metrics which are no longer in use.
2025-01-21 12:55:03 -05:00
Samantha Frank d2d86d9309
allowlist: Provide a generic implementation of an allow list (#7958)
Allow lists are a common pattern in Boulder, provide a generic
implementation in its own package.

Part of #7604
2025-01-21 12:54:42 -05:00
Aaron Gable 1806294460
Add schema for profile column in authz2 table (#7954)
Use MariaDB's "instant add column" feature to add a new
certificateProfileName column to the existing authz2 table. This column
is nullable to reflect the fact that profiles are optional, and to
mirror the similarly-added column on the orders table.

This change is standalone, with no code reading or writing this field,
so that it can be deployed to production and a follow-up change can
begin reading and writing the field all at once with no deployability
concerns.

Part of https://github.com/letsencrypt/boulder/issues/7955
2025-01-21 09:38:47 -08:00
Jacob Hoffman-Andrews 600010305a
grpc: factor out setup func (#7909)
This uses a pattern that is new to our tests. setup accepts a variadic
list of options, and uses a type switch to make use of those options
during setup. This allows us to pass setup only the options that are
relevant to any given test case, leaving the rest to sensible defaults.
2025-01-20 12:31:57 -05:00
Aaron Gable 6b1e7f04e8
SA: Clean up pre-profile order schema and feature flag (#7953)
Deprecate the MultipleCertificateProfiles feature flag, which has been
enabled in both Staging and Prod. Delete all code protected by that flag
being false, namely the orderModelv1 type and its support code. Update
the config schema to match the config-next schema.

Fixes https://github.com/letsencrypt/boulder/issues/7324
Fixes https://github.com/letsencrypt/boulder/issues/7408
2025-01-17 17:15:01 -08:00
Aaron Gable dbe2fe24a4
Remove unused keys from CA config (#7948)
Remove the singular Profile field from the CA config, as it has been
replaced by the plural CertProfiles key. Remove the Expiry, Backdate,
LintConfig, and IgnoredLints keys from the top-level CA config, as they
are now also configured on a per-profile basis. Remove the LifespanCRL
key from the CA config, as it is now configured within the CRLProfile.
For all of the above, remove transitional fallbacks from within
//ca/main.go.

These config changes were deployed to production in IN-10568, IN-10506,
and IN-10045.

Fixes https://github.com/letsencrypt/boulder/issues/7414
Fixes https://github.com/letsencrypt/boulder/issues/7159
2025-01-17 16:30:58 -08:00
Matthew McPherrin ace233cbdc
Update admin-revoker certs to be admin (#7947)
The admin and admin-revoker tools shared certs. admin-revoker is gone,
so update the certs to use the admin name only.
2025-01-17 16:02:20 -05:00
Samantha Frank 10c9d73b82
database: Alter registrations to drop initialIP (#7945)
Part of https://github.com/letsencrypt/boulder/issues/7917
2025-01-17 16:00:27 -05:00
James Renken 6f4eb5a2e1
Stop using LockCol in registrations table (#7935)
Alter the `LockCol` column to have a default value, so we can omit it
from `INSERT`s.

Part of #7934
2025-01-17 12:41:11 -08:00
Samantha Frank dfdf554f76
config: Use hex-encoding for HMACKey (#7950) 2025-01-15 14:28:09 -05:00
James Renken 7da9a83deb
ra, pb: Don't expect or validate contactsPresent (#7933)
Part of #7920

There will be a followup removing the remaining places that set
`contactsPresent`.

---------

Co-authored-by: Jacob Hoffman-Andrews <jsha+github@letsencrypt.org>
2025-01-14 15:58:56 -08:00
James Renken 2e1f733c26
ra/sa: Remove deprecated UpdateRegistration methods (#7911)
This is the final stage of #5554: removing the old, combined
`UpdateRegistration` flow, which has been replaced by
`UpdateRegistrationContact` and `UpdateRegistrationKey`. Those new
functions have their own tests.

The RA's `UpdateRegistration` function no longer has any callers (as of
#7827's deployment), so it is safely deployable to remove it from the SA
too, and its request from gRPC.

Fixes #5554

---------

Co-authored-by: Jacob Hoffman-Andrews <jsha+github@letsencrypt.org>
Co-authored-by: Aaron Gable <aaron@letsencrypt.org>
2025-01-14 13:54:06 -08:00
Jacob Hoffman-Andrews 04dec59c67
ra: log User-Agent (#7908)
In the WFE, store the User-Agent in a `context.Context` object. In our
gRPC interceptors, pass that field in a Metadata header, and re-add it
to `Context` on the server side.

Add a test in the gRPC interceptors that User-Agent is properly
propagated.

Note: this adds a new `setup()` function for the gRPC tests that is
currently only used by the new test. I'll upload another PR shortly that
expands the use of that function to more tests.

Fixes https://github.com/letsencrypt/boulder/issues/7792
2025-01-14 13:39:41 -08:00
Matthew McPherrin bb9d82b85f
Remove the dead admin-revoker tool (#7941)
The admin-revoker tool is dead. Long live the admin tool.

There's a number places that still reference admin-revoker, including
Boulder's ipki and the revocation source in the database which are still
used, even if the tool is gone. But nothing actually using the tool.
2025-01-13 17:05:15 -08:00
Samantha Frank 45a56ae9bd
database: No longer store or retrieve InitialIP (#7942)
The initialIP column has been defaulted to 0.0.0.0 since #7760. Remove
this field from the all structs while leaving the schema itself intact.

Part of #7917
2025-01-13 17:33:59 -05:00
James Renken 274d4463d1
ra: Remove isRenewal & isARIRenewal from NewOrderRequest proto (#7932)
Fixes #7671 
Fixes #5545
2025-01-13 16:14:17 -05:00
Matthew McPherrin 1b44b8acfd
Cert-checker: Don't require clientEKU (#7939)
This is required now that we're going to issue certificates with only
the server EKU.

Fixes #7938

---------

Co-authored-by: James Renken <jprenken@users.noreply.github.com>
2025-01-13 13:08:26 -08:00
Aaron Gable 7209bc2632
RA: Fix special error case when finalizing authz (#7929)
Replace looking for AlreadyRevoked (which is never returned by the
underlying SA method) with the correct NotFound. Also add a comment
documenting why this behavior exists.

Fixes https://github.com/letsencrypt/boulder/issues/3995
2025-01-10 15:05:00 -08:00
Matthew McPherrin 8a01611b70
Switch to loglist3 package for parsing CT log list (#7930)
The schema tool used to parse log_list_schema.json doesn't work well
with the updated schema. This is going to be required to support
static-ct-api logs from current Chrome log lists.

Instead, use the loglist3 package inside the certificate-transparency-go
project, which Boulder already uses for CT submission otherwise.

As well, the Log IDs and keys returned from loglist3 have already been
base64 decoded, so this re-encodes them to minimize the impact on the
rest of the codebase and keep this change small.

The test log_list.json file needed to be made a bit more realistic for
loglist3 to parse without base64 or date parsing errors.
2025-01-10 13:29:40 -08:00
James Renken e4668b4ca7
Deprecate DisableLegacyLimitWrites & UseKvLimitsForNewOrder flags; remove code using certificatesPerName & newOrdersRL tables (#7858)
Remove code using `certificatesPerName` & `newOrdersRL` tables.

Deprecate `DisableLegacyLimitWrites` & `UseKvLimitsForNewOrder` flags.

Remove legacy `ratelimit` package.

Delete these RA test cases:

- `TestAuthzFailedRateLimitingNewOrder` (rl:
`FailedAuthorizationsPerDomainPerAccount`)
- `TestCheckCertificatesPerNameLimit` (rl: `CertificatesPerDomain`)
- `TestCheckExactCertificateLimit` (rl: `CertificatesPerFQDNSet`)
- `TestExactPublicSuffixCertLimit` (rl: `CertificatesPerDomain`)

Rate limits in NewOrder are now enforced by the WFE, starting here:
5a9b4c4b18/wfe2/wfe.go (L781)

We collect a batch of transactions to check limits, check them all at
once, go through and find which one(s) failed, and serve the failure
with the Retry-After that's furthest in the future. All this code
doesn't really need to be tested again; what needs to be tested is that
we're returning the correct failure. That code is
`NewOrderLimitTransactions`, and the `ratelimits` package's tests cover
this.

The public suffix handling behavior is tested by
`TestFQDNsToETLDsPlusOne`:
5a9b4c4b18/ratelimits/utilities_test.go (L9)

Some other RA rate limit tests were deleted earlier, in #7869.

Part of #7671.
2025-01-10 12:50:57 -08:00
Jacob Hoffman-Andrews f37c36205c
tools: use simpler packaging by default (#7928)
Feedback from SRE was to just go straight to the new packaging.

Also, fix the Architecture field of the .deb to be amd64 (Debian
requires this specific value), and check that we are building on x86_64
OR amd64.
2025-01-09 11:03:51 -08:00
Jacob Hoffman-Andrews 635f43266a
use core.IsAnyNilOrZero more places (#7925)
There were a bunch of places that had `TODO(#7153)`; that issue is now
closed, so let's tidy up.
2025-01-07 15:48:47 -08:00
Jacob Hoffman-Andrews f10f462959
sa: streamline use of dates in test (#7924)
Add mustTime and mustTimestamp, each of which parses a time in a simple
format and panics if it cannot be parsed.

Also, make the intent of each check in the GetRevokedCerts tests a
little clearer by starting with a basicRequest, and then defining
variations in terms of that request.

Fix the "different issuer" case in `TestGetRevokedCerts`, which was not
actually setting a different issuer.
2025-01-07 14:28:47 -08:00
Jacob Hoffman-Andrews 673b93c7ae
sa: clean up config gates in tests (#7923)
Remove the gates for the paused and revokedCertificates tables, which
are now live and in `config`. Refine the documentation for the
orderModelv2 migration.
2025-01-07 13:28:47 -08:00
Jacob Hoffman-Andrews 01ed436ef6
doc: add detail on ShutdownStopTimeout (#7921)
Also move the ShutdownStopTimeout stanza next to timeout, and make the
comment the same across the multiple components. In the future we may
want to factor out some of the common config fields into a struct that
can be embedded.
2025-01-07 13:28:36 -08:00
Jacob Hoffman-Andrews cefa709a01
ci: build packages without fpm (#7915)
For now, run alongside the `fpm` build and create `boulder-newpkg-*`
packages. If these packages work, we'll eliminate the `fpm` build.
2025-01-07 13:27:28 -08:00
Aaron Gable 9b3c8829e8
Grant push-release action permission to write packages (#7916) 2025-01-06 15:38:54 -08:00
Aaron Gable 442d152b72
Fix orderModelv2 for nullable profile column (#7907)
Change the type of the orderModelv2 CertificateProfileName field to be a
pointer to a string, reflecting the fact that the underlying database
column is nullable. Add tests to ensure that order rows inserted with
either order model can be read using the other model.

Fixes https://github.com/letsencrypt/boulder/issues/7906
2025-01-06 13:26:11 -08:00
Jacob Hoffman-Andrews d3625f9881
ci: install specific gem versions in make-assets.sh (#7913)
We recently started getting these errors in CI:

```
ERROR:  Error installing fpm:
	The last version of rchardet (~> 1.8) to support your Ruby & RubyGems was 1.8.0. Try installing it with `gem install rchardet -v 1.8.0` and then running the current command again
	rchardet requires Ruby version >= 3.0.0. The current ruby version is 2.7.0.0.
```

Installing specific versions of dependencies fixes it.
2025-01-06 12:05:14 -08:00
Jacob Hoffman-Andrews ef6593d06b
ra, wfe: use TimestampsForWindow to check renewal (#7888)
And in the RA, log the notBefore of the previous issuance.

To make this happen, I had to hoist the "check for previous certificate"
up a level into `issueCertificateOuter`. That meant I also had to hoist
the "split off a WithoutCancel context" logic all the way up to
`FinalizeOrder`.
2025-01-06 10:16:53 -08:00
Jacob Hoffman-Andrews d6e163c15d
Revert "wfe: on rate limit error, serve 500 (#7796)" (#7900)
This reverts commit 242d746040 (#7796)

We want to make this change, but it carries some risk that we'd prefer
not to take over the holiday. We'd also like to keep `main` in a state
where it would be reasonable to deploy (even if, in practice, any
over-the-holiday deploy would be a hotfix, not a direct tag from
`main`).
2024-12-20 11:04:19 -08:00
Samantha Frank 6402a2275f
ratelimits: Remove a metric and some labels that we're not finding useful (#7902) 2024-12-20 08:44:08 -05:00
Matthew McPherrin 1797450389
Remove boulder invocation via symlinks (#7905)
Boulder switched from multiple binaries to one by having symlinks for
the old binaries, but we invoke boulder via subcommands now. This drops
support for running via symlinks in Boulder, and drops them from the
build process.

This does explicitly list out the four binaries in the makefile, which I
think explicitly listing them is fine given that we rarely add them.
This also avoids needing to duplicate mentioning the special ct-test-srv
in the deb/tar rules. We could probably just look at what's in `bin/`
after `go install ./...`, but I didn't want to get too into makefile
changes.

We haven't used the symlinked versions of commands for a while, and can
drop them from builds.

This also drops the .rpm builds, which we also haven't used in a long
time.
2024-12-19 12:11:24 -08:00
Jacob Hoffman-Andrews e8a49c5a02
wfe: remove authz-v3 and chall-v3 paths (#7904)
This removes the `handlerPath` parameter to various calls, which was
used solely to distinguish the `-v3`-style paths from the `WithAccount`
paths.

Also, this removes `WithAccount` from all names that had it. The fact
that these URLS include an account ID is now implicit.
2024-12-19 11:19:49 -08:00
Jacob Hoffman-Andrews d42865c187
sa: add Limit field to CountFQDNSetsRequest (#7887)
This allows us to replace FQDNSetExists with a call to
FQDNSetTimestampsForWindow, with Limit set to 1. That, in turn, will
allow us to log the time since issuance of the most recent certificate
with a given FQDNSet.
2024-12-19 10:11:50 -08:00
James Renken 62299362bd
ra/ratelimits: Update tests, use new TransactionBuilder constructor, fix ARI rate limit exception (#7869)
Add a new `ratelimits.NewTransactionBuilderWithLimits` constructor which
takes pre-populated rate limit data, instead of filenames for reading it
off disk.

Use this new constructor to change rate limits during RA tests, instead
of using extra `testdata` files.

Fix ARI renewals' exception from rate limits: consider `isARIRenewal` as
part of the `isRenewal` arg to `checkNewOrderLimits`.

Remove obsolete RA tests for rate limits that are now only checked in
the WFE.

Update remaining new order rate limit tests from deprecated `ratelimit`s
to new Redis `ratelimits`.
2024-12-18 14:23:13 -08:00
Aaron Gable 0e5e1e98d1
Upgrade zlint v3.6.4 (#7897)
This brings in several new and useful lints. It also brings in one CABF
BR lint which we have to ignore in our default profile which includes
the Subject Key Identifier extension:
"w_ext_subject_key_identifier_not_recommended_subscriber". In our modern
profile which omits several fields, we have to ignore the opposite
RFC5280 lint "w_ext_subject_key_identifier_missing_sub_cert".

Release notes: https://github.com/zmap/zlint/releases/tag/v3.6.4
Changelog: https://github.com/zmap/zlint/compare/v3.6.0...v3.6.4
Note that the majority of the ~400 file changes are merely copyright
date changes.

The corresponding production config changes tracked in IN-10466 are
complete.
2024-12-18 11:41:12 -08:00
Aaron Gable 0c658f202a
Fix error when deactivating an account (#7899)
The RA's DeactivateAccount method expects the account provided to it by
the WFE to still have status Valid. The new WFE deactivation code was
hardcoding the status to Deactivated. Fix the WFE to pass the account's
current status instead.

Add an integration test to confirm both the breakage and the fix. Also
leave behind some TODOs to simplify this codepath further, and not
require the status to be provided at all.

Part of #5554
2024-12-18 10:06:08 -08:00
Aaron Gable 5c34d05d3a
Fix incorrect ARI error message (#7895)
This confusing error message was an accidental carry-over from sharing
some code with the NewOrder "replaces" ARI codepath.

Fixes https://github.com/letsencrypt/boulder/issues/7889
2024-12-18 07:42:21 -08:00
Jacob Hoffman-Andrews 242d746040
wfe: on rate limit error, serve 500 (#7796)
This affects NewAccount and NewOrder.
2024-12-17 17:09:57 -08:00
Matthew McPherrin ba624ac5be
Log the flakinessrate at ct-test-srv startup (#7896)
This is useful for checking configurations via logs.
2024-12-17 16:48:03 -08:00
Matthew McPherrin 5b945107bd
Publish ct-test-srv container on releases (#7891)
This can replace the old ct-test-srv container at https://registry.hub.docker.com/r/letsencrypt/ct-test-srv
2024-12-17 15:25:11 -08:00
Aaron Gable 7744fa3054
Publisher: cache by both URI and pubkey (#7893)
Fixes https://github.com/letsencrypt/boulder/issues/7892
2024-12-17 14:53:08 -08:00
Samantha Frank 11d543bd98
ratelimits: Correctly handle stale and concurrently initialized buckets (#7886)
#7782 fixed an issue where concurrent requests to the same existing
bucket ignored all but one rate limit spend. However, concurrent
requests to the same empty bucket can still cause multiple
initializations that skip all but one spend. Use BatchSetNotExisting
(SETNX in Redis) to detect this scenario and then fall back to
BatchIncrement (INCRBY in Redis).

#7782 sets the TTL (Time-To-Live) of incremented buckets to the maximum
possible burst for the applied limit. Because this TTL doesn’t match the
TAT, these buckets can become "stale," holding a TAT in the past.
Incrementing these stale buckets by cost * emissionInterval leaves the
new TAT behind the current time, allowing clients who are sometimes idle
to gain extra burst capacity. Instead, use BatchSet (SET in Redis) to
overwrite the TAT to now + cost * emissionInterval. Though this
introduces a similar race condition as empty buckets, it’s less harmful
than granting extra burst capacity.
2024-12-17 12:42:51 -05:00
Jacob Hoffman-Andrews 1f9f2bccf5
sa: remove CountFQDNSetTimestamps (#7883)
This was superseded in #6220 by FQDNTimestampsForWindow and is no longer
called.
2024-12-16 12:24:01 -08:00
Jacob Hoffman-Andrews 2678e68806
test: move "make build" for webpki into generate.sh (#7885)
webpki.go was discarding stdout when "make build" failed. We can make it
print stdout in that context, but it's more straightforward to run "make
build" from the shell script that calls webpki.go, where its stdout will
naturally be emitted.

Inspired by a recent CI run where there was a straightforward build
failure in some of Boulder's code, but it was masked by an error running
webpki.go in the `bsetup` container.
2024-12-13 15:19:22 -08:00
James Renken 62f1a26ccf
wfe: Use separate UpdateRegistrationContact & UpdateRegistrationKey methods (#7827)
Fixes #7716
Part of #5554
2024-12-13 11:41:59 -05:00
Jacob Hoffman-Andrews efaa370457
doc: boulder now has Retry-After on all ratelimits (#7876)
Thanks to MikeMcQ from the forum for
[noticing](https://community.letsencrypt.org/t/new-rate-limit-page-in-conflict-with-boulder-variances/229849).
2024-12-12 16:27:58 -05:00
Samantha Frank 1ddd4633f5
DB: Promote pausing schema from config-next to config (#7878) 2024-12-11 14:38:55 -05:00
Jacob Hoffman-Andrews 40e100c297
doc: replace "leaky" with "token" bucket (#7881)
Mostly we refer consistently to token bucket, but these two places (one
of which is soon to be removed) still had the "leaky" terminology, which
is potentially confusing.
2024-12-10 16:39:30 -08:00
James Renken 1b7b9a776b
cmd: Make a debug listen address optional (#7840)
Remove `debugAddr` from the `admin` tool, which doesn't use it - or need
it, now that `newStatsRegistry` via `StatsAndLogging` doesn't require
it.

Remove `debugAddr` from `config-next/sfe.json`, as we usually set it on
the CLI instead.

Fixes #7838
2024-12-10 12:25:12 -08:00
Samantha Frank dda8acc34a
RA/VA: Add MPIC compliant DCV and CAA checks (#7870)
Today, we have VA.PerformValidation, a method called by the RA at
challenge time to perform DCV and check CAA. We also have VA.IsCAAValid,
a method invoked by the RA at finalize time when a CAA re-check is
necessary. Both of these methods can be executed on remote VA
perspectives by calling the generic VA.performRemoteValidation.

This change splits VA.PerformValidation into VA.DoDCV and VA.DoCAA,
which are both called on remote VA perspectives by calling the generic
VA.doRemoteOperation. VA.DoDCV, VA.DoCAA, and VA.doRemoteOperation
fulfill the requirements of SC-067 V3: Require Multi-Perspective
Issuance Corroboration by:

- Requiring at least three distinct perspectives, as outlined in the
"Phased Implementation Timeline" in BRs section 3.2.2.9 ("Effective
March 15, 2025").
- Ensuring that the number of non-corroborating (failing) perspectives
remains below the threshold defined by the "Table: Quorum Requirements"
in BRs section 3.2.2.9.
- Ensuring that corroborating (passing) perspectives reside in at least
2 distinct Regional Internet Registries (RIRs) per the "Phased
Implementation Timeline" in BRs section 3.2.2.9 ("Effective March 15,
2026").
- Including an MPIC summary consisting of: passing perspectives, failing
perspectives, passing RIRs, and a quorum met for issuance (e.g., 2/3 or
3/3) in each validation audit log event, per BRs Section 5.4.1,
Requirement 2.8.

When the new SeparateDCVAndCAAChecks feature flag is enabled on the RA,
calls to VA.IsCAAValid (during finalization) and VA.PerformValidation
(during challenge) are replaced with calls to VA.DoCAA and a sequence of
VA.DoDCV followed by VA.DoCAA, respectively.

Fixes #7612
Fixes #7614
Fixes #7615
Fixes #7616
2024-12-10 11:26:08 -05:00
James Renken 071b8c5b35
wfe: Handle empty JSON to /acme/acct like POST-as-GET (#7844)
Early drafts of the ACME spec said that clients should retrieve their existing account information by POSTing the empty JSON object `{}` to their account URL. This instruction was removed in the final version of ACME, replaced by the concept of POST-as-GET, which uses a wholly empty body to accomplish the same goal. However, Boulder has continued to incidentally support this behavior: when we receive an empty JSON object, our `updateAccount` code in the RA applies their desired diff (none) on top of their current account, writes it back to the database, and returns the updated account object...which hasn't actually changed. This behavior is also half-tested by `TestEmptyAccount`, but that test is actually testing that the MockRA implements the same behavior as the real RA; it's not truly testing the WFE's behavior.

This PR changes the WFE to explicitly treat receiving the empty JSON object as a request to retrieve the account data unchanged, rather than implicitly relying on internal details of the RA's account-update logic, which are expected to change in #7827.

---------

Co-authored-by: Jacob Hoffman-Andrews <jsha+github@letsencrypt.org>
2024-12-06 16:45:43 -08:00
Matthew McPherrin 7e8b3fa10f
Run most workflows on ubuntu-24.04 (#7875)
Github is currently rolling out ubuntu-latest as ubuntu-24.04. Manage
that switch explicitly by running most jobs on 24.04

https://github.com/actions/runner-images/issues/10636

This keeps the release on 20.04 to ensure released binaries can run on
older operating systems (because of CGO/glibc versions)
2024-12-06 13:13:04 -08:00
Eng Zer Jun 13db2a252f
refactor: remove usages of experimental maps package (#7849)
All 4 usages of the `maps.Keys` function from the
`golang.org/x/exp/maps` package can be refactored to a simpler
alternative. If we need it in the future, it is available in the
standard library since Go 1.23.
2024-12-06 11:50:32 -08:00
Samantha Frank 87104b0a3e
va: Check for RIR and Perspective mismatches at runtime when they're provided (#7841)
- Ensure the Perspective and RIR reported by each remoteVA in the
*vapb.ValidationResult returned by VA.PerformValidation, matches the
expected local configuration when that configuration is present.
- Correct "AfriNIC" to "AFRINIC", everywhere.


Part of https://github.com/letsencrypt/boulder/issues/7819
2024-12-06 14:27:28 -05:00
Aaron Gable 749f9afa6b
Fix RA unit test merge conflict (#7874) 2024-12-06 08:59:01 -05:00
Aaron Gable 95e5f87f9e
Add feature flag to disable pending authz reuse (#7836)
Pending authz reuse is a nice-to-have feature because it allows us to
create fewer rows in the authz database table when creating new orders.
However, stats show that less than 2% of authorizations that we attach
to new orders are reused pending authzs. And as we move towards using a
more streamlined database schema to store our orders, authorizations,
and validation attempts, disabling pending authz reuse will greatly
simplify our database schema and code.

CPS Compliance Review: our CPS does not speak to whether or not we reuse
pending authorizations for new orders.
IN-10859 tracks enabling this flag in prod

Part of https://github.com/letsencrypt/boulder/issues/7715
2024-12-05 16:14:57 -08:00
Jacob Hoffman-Andrews 27e65f3e9f
ratelimits: add detail to error messages (#7871)
For batch operations, include the operation and the number of keys in
the error message. This should help diagnose whether we are getting `i/o
timeout` errors disproportionately for larger requests, or for certain
operations.

Also, make the ignored errors part of the overall WFE request logs,
which allows us to get additional context, like whether certain
requesters or domain names are getting disproportionately many errors.

Related to #7846.
2024-12-05 15:58:26 -08:00
Jacob Hoffman-Andrews 8c45a4fb04
docs: update CONTRIBUTING.md (#7857)
Describe what to do if a bot comments on your PR, and update our
description of feature flags and their deprecation.
2024-12-04 18:26:08 -08:00
Aaron Gable aac7c22946
Simplify RA pausing unit tests (#7868)
Greatly simplify the two RA unit tests covering failed validations and
account+identifier pausing. Most importantly, directly manipulate the
ratelimit backing store during test setup, to avoid having to "perform"
extra validations.

Fixes https://github.com/letsencrypt/boulder/issues/7812
2024-12-04 13:51:37 -08:00
Aaron Gable d962c61067
RA and WFE tests: use inmem rate limit source (#7859)
The purpose of these RA and WFE unit tests is to test how they deal with
certain rate limit conditions, not to test talking to an actual redis
instance. Streamline the tests by having them talk to an in-memory rate
limits store, rather than a redis-backed one.
2024-12-03 14:52:16 -08:00
Aaron Gable bac5602c6d
Always use INCRBY for redis rate limits (#7856)
Deprecate the IncrementRateLimits feature flag, and always use the redis
INCRBY instruction to update rate limit TATs.

Fixes https://github.com/letsencrypt/boulder/issues/7855
2024-12-02 15:25:33 -08:00
Jacob Hoffman-Andrews 5cdfa3e26c
ra: wait for validations on clean shutdown (#7854)
This reduces the number of validations that get left indefinitely in
"pending" state.

Rename `DrainFinalize()` to `Drain()` to indicate that it now covers
more cases than just finalize.
2024-12-02 11:00:17 -08:00
Samantha Frank d64132eebc
VA: Use performValidation for IsCAAValid remote checks (#7850)
- Remove undeployed feature flag MultiCAAFullResults
- Perform local CAA checks prior to initiating remote checks, instead of
starting remote checks and proceeding to perform local checks.
- Remove VA.IsCAAValid specific remote validation logic, use
VA.performRemoteOperation instead
- Refactor va.logRemoteResults to be easier to test and omit the RVA
problem
- Drive-by fix: Calculate logEvent.Latency with va.clk.Since() instead
of time.Since() like everything else in VA.performRemoteOperation
2024-11-28 15:24:47 -05:00
Samantha Frank 27a77142ad
VA: Make performRemoteValidation more generic (#7847)
- Make performRemoteValidation a more generic function that returns a
new remoteResult interface
- Modify the return value of IsCAAValid and PerformValidation to satisfy
the remoteResult interface
- Include compile time checks and tests that pass an arbitrary operation
2024-11-27 15:29:33 -05:00
Aaron Gable ded2e5e610
Remove logging of contact email addresses (#7833)
Fixes https://github.com/letsencrypt/boulder/issues/7801
2024-11-25 13:33:56 -08:00
Samantha Frank c3948314ff
va: Make the primary VA aware of the Perspective and RIR of each remote (#7839)
- Make the primary VA aware of the expected Perspective and RIR of each
remote VA.
- All Perspectives should be unique, have the primary VA check for
duplicate Perspectives at startup.
- Update test setup functions to ensure that each remote VA client and
corresponding inmem impl have a matching perspective and RIR.

Part of #7819
2024-11-25 13:02:03 -05:00
Aaron Gable 7791262815
Rate limit deactivating pending authzs (#7835)
When a client deactivates a pending authorization, count that towards
their FailedAuthorizationsPerDomainPerAccount and
FailedAuthorizationsForPausingPerDomainPerAccount rate limits. This
should help curb the few clients which constantly create new orders and
authzs, deactivate those pending authzs preventing reuse of them or
their orders, and then rinse and repeat.

Fixes https://github.com/letsencrypt/boulder/issues/7834
2024-11-21 17:17:16 -08:00
Jacob Hoffman-Andrews 53f3cb91c0
wfe: rename deprecated paths and handlers (#7837)
Now that the paths with an account (and no `-v3`) are the default,
rename the old-style path constants and handlers to reflect that they
are deprecated.

Part of #7683.
2024-11-21 16:51:36 -08:00
Jacob Hoffman-Andrews 01c1488b0f
va: use cancels to early-return. (#7832)
This allows us to collect a consistent number of error results for
logging.

Related to #7616.
2024-11-20 13:37:53 -08:00
Samantha Frank 8bf13a90f4
VA: Make PerformValidation more like DoDCV (#7828)
- Remove Perspective and RIR from ValidationRecords
- Make ValidationResultToPB Perspective and RIR aware
- Update comment for VA.PerformValidation
- Make verificationRequestEvent more like doDCVAuditLog
- Update language used in problems created by performRemoteValidation to
be more like doRemoteDCV.
2024-11-20 14:13:55 -05:00
Samantha Frank c2632585f5
core: Move canceled.Is to core.IsCanceled (#7831)
Small refactor to use errors.Is() rather than the equality operator.
Also moves this utility function into core.
2024-11-20 13:10:02 -05:00
Samantha Frank a8cdaf8989
ratelimit: Remove legacy registrations per IP implementation (#7760)
Part of #7671
2024-11-19 18:39:21 -05:00
Samantha Frank 65de9fb4b8
VA: Fix two IsCAAValid bugs (#7829)
Fix two bugs introduced in #7816:
- Fix localLatency time for CAA rechecking is always 0
- Fix logEvent.InternalError is no longer being written to log
2024-11-19 11:14:34 -08:00
Jacob Hoffman-Andrews 577a1e38eb
va: prepare to require minimum of 3 RVAs (#7815)
To prepare for the MPIC requirement of having a minimum of 3
perspectives, I added code to `NewValidationAuthorityImpl` to error if
there aren't enough remote VAs configured _and_ the current VA is the
primary perspective. Then I fixed all the tests, which involved adding
some backends in the unittests, and spinning up `remoteva-c` in the
integration tests.

As a reminder, the `boulder va` command always considers itself the
primary perspective, while `boulder remoteva` gives itself a perspective
based on its config.

I wound up backing out the code in `NewValidationAuthorityImpl` because
right now our remote VAs are actually running the `boulder va` command,
so they would error out in prod, even though our actual primary
perspective does have enough backends. So this wound up as a test-only
change.
2024-11-19 10:23:32 -05:00
Jacob Hoffman-Andrews a46c388f66
va: compute maxRemoteFailures based on MPIC (#7810)
Previously this was a configuration field.

Ports `maxAllowedFailures()` from `determineMaxAllowedFailures()` in
#7794.

Test updates:
 
Remove the `maxRemoteFailures` param from `setup` in all VA tests.

Some tests were depending on setting this param directly to provoke
failures.

For example, `TestMultiVAEarlyReturn` previously relied on "zero allowed
failures". Since the number of allowed failures is now 1 for the number
of remote VAs we were testing (2), the VA wasn't returning early with an
error; it was succeeding! To fix that, make sure there are two failures.
Since two failures from two RVAs wouldn't exercise the right situation,
add a third RVA, so we get two failures from three RVAs.

Similarly, TestMultiCAARechecking had several test cases that omitted
this field, effectively setting it to zero allowed failures. I updated
the "1 RVA failure" test case to expect overall success and added a "2
RVA failures" test case to expect overall failure (we previously
expected overall failure from a single RVA failing).

In TestMultiVA I had to change a test for `len(lines) != 1` to
`len(lines) == 0`, because with more backends we were now logging more
errors, and finding e.g. `len(lines)` to be 2.
2024-11-18 15:36:09 -08:00
Jacob Hoffman-Andrews 20fdcbcfe0
ratelimits: always use a pointer for limit (#7804)
The zero value for `limit` is invalid, so returning `nil` in error cases
avoids silently returning invalid limits (and means that if code makes a
mistake and references an invalid limit it will be an obvious clear
stack trace).

This is more consistent, since the methods on `limit` use a pointer
receiver. Also, since `limit` is a fairly large object, this saves some
copying.

Related to #7803 and #7797.
2024-11-15 13:45:07 -08:00
Samantha Frank 3506f09285
RA: Make calls to countCertificateIssued and countFailedValidations synchronous (#7824)
Solves CI flakes in TestCertificatesPerDomain and
TestIdentifiersPausedForAccount that are the result of a race on the
Redis database. This has the downside of making failed validations and
successful finalizations take slightly longer.
2024-11-15 16:33:51 -05:00
Samantha Frank 3baac6f6df
VA: Consolidate multiple metrics into one histogram (#7816)
- Add a new histogram, validationLatency
- Add a VA.observeLatency for observing validation latency
- Refactor to ensure this metric can be observed exclusively within
VA.PerformValidation and VA.IsCAAValid.
- Replace validationTime, localValidationTime, remoteValidationTime,
remoteValidationFailures, caaCheckTime, localCAACheckTime,
remoteCAACheckTime, and remoteCAACheckFailures with validationLatency
2024-11-15 15:51:39 -05:00
Samantha Frank f9fb688427
build: Specify go 1.23.0 in go.mod (#7822)
We're using `.Keys()` a method of the maps package added in 1.23
(https://pkg.go.dev/maps@master#Keys) but our go.mod states 1.22.0. This
causes some in-IDE linting errors in VSCode.
2024-11-15 12:49:04 -08:00
Samantha Frank b2b5645e16
VA: Cleanup performRemoteValidation (#7814)
Bring this code more in line with `VA.remoteDoDCV` in #7794. This should
make these two easier to diff in review.
2024-11-15 12:25:06 -08:00
Samantha Frank 2502113ac3
VA: Remove logging of RIR and Perspective (#7818) 2024-11-15 14:54:03 -05:00
Jacob Hoffman-Andrews 56f0ed6419
wfe: orders link to authz IDs with acccount (#7790)
This means that most traffic will go to the authz URLs with account.
After this has been deployed for 30 days (the max lifetime of an order),
we can remove support for the old paths.

Part of #7683
2024-11-15 10:34:14 -08:00
Jacob Hoffman-Andrews 0d70b12a75
ra: fix unittest for resetting pause limit (#7813)
TestPerformValidation_FailedThenSuccessfulValidationResetsPauseIdentifiersRatelimit
checks for a bucket being empty after a reset. However, that bucket is
based on an account ID that is shared across multiple test cases.
Instead, use a unique account and domain for this test.

Fixes #7812
2024-11-14 15:04:38 -08:00
Jacob Hoffman-Andrews c39f33e24f
va: fix race in TestMultiVALogging (#7811) 2024-11-14 14:17:42 -08:00
Jacob Hoffman-Andrews 5e385e440a
ra: clean up countFailedValidations (#7797)
Return an error and do logging in the caller. This adds early returns on
a number of error conditions, which can prevent nil pointer dereference
in those cases.

Also update the description for AutomaticallyPauseZombieClients.

Follows up #7763.
2024-11-14 16:16:36 -05:00
Jacob Hoffman-Andrews 26a9910911
ratelimits: improve disabled limit handling (#7803)
In the FailedAuthorizations limits, there was code that intentionally
ignored errLimitDisabled errors (`errors.Is(err, errLimitDisabled)`).
However, that that resulted in those functions later using a returned
`limit` value that was invalid (i.e. its zero value). That happened to
trigger some later checks in validateTransaction. Specifically this
check failed:

    	if txn.cost > txn.limit.Burst {
        // error

When txt.limit.Burst is zero, this will always fail.

This problem doesn't really show up in prod, where all the limits are
configured. But it showed up in tests, specifically
TestPerformValidation_FailedValidationsTriggerPauseIdentifiersRatelimit,
where the limits are constructed using a simplified config that leaves
most of them disabled.

In this change, I tried to make handling of errLimitDisabled more
consistent, and always return an allow-only transaction as early as
possible instead of falling through the error condition.

Where that wasn't possible, I used a boolean to record whether the
result of `builder.getLimit()` was valid before referencing any of its
fields.

I also added some "shouldn't happen" errors to catch this problem
earlier if it recurs.

I removed some "skip disabled limit" comments because those say "what
the code does" (which the code also says), not "why the code does it".

Fixes the test failures in #7797.
2024-11-13 16:23:50 -05:00
James Renken 0a27cba9f4
WFE/nonce: Add NonceHMACKey field (#7793)
Add a new WFE & nonce config field, `NonceHMACKey`, which uses the new
`cmd.HMACKeyConfig` type. Deprecate the `NoncePrefixKey` config field.

Generalize the error message when validating `HMACKeyConfig` in
`config`.

Remove the deprecated `UseDerivablePrefix` config field, which is no
longer used anywhere.

Part of #7632
2024-11-13 10:31:28 -05:00
Jacob Hoffman-Andrews 5be3e99a4d
features: remove deprecated features (#7805)
Fixes #7802
2024-11-13 10:22:32 -05:00
Jacob Hoffman-Andrews 1d8cf3e212
ra: remove special case for empty DNSNames (#7795)
This case was added to work around a test case that didn't fill it out;
instead, fill DNSNames for that test case.
2024-11-11 11:07:20 -05:00
Kruti Sutaria a79a830f3b
ratelimits: Auto pause zombie clients (#7763)
- Added a new key-value ratelimit
`FailedAuthorizationsForPausingPerDomainPerAccount` which is incremented
each time a client fails a validation.
- As long as capacity exists in the bucket, a successful validation
attempt will reset the bucket back to full capacity.
- Upon exhausting bucket capacity, the RA will send a gRPC to the SA to
pause the `account:identifier`. Further validation attempts will be
rejected by the [WFE](https://github.com/letsencrypt/boulder/pull/7599).
- Added a new feature flag, `AutomaticallyPauseZombieClients`, which
enables automatic pausing of zombie clients in the RA.
- Added a new RA metric `paused_pairs{"paused":[bool],
"repaused":[bool], "grace":[bool]}` to monitor use of this new
functionality.
- Updated `ra_test.go` `initAuthorities` to allow accessing the
`*ratelimits.RedisSource` for checking that the new ratelimit functions
as intended.

Co-authored-by: @pgporada 

Fixes https://github.com/letsencrypt/boulder/issues/7738

---------

Co-authored-by: Phil Porada <pporada@letsencrypt.org>
Co-authored-by: Phil Porada <philporada@gmail.com>
2024-11-08 13:51:41 -08:00
Jacob Hoffman-Andrews 2058d985cc
Allow account IDs in authz and challenge URLs (#7768)
This adds new handlers under `/acme/authz/` and `/acme/chall/` that
expect to be followed by `{regID}/{authzID}` and
`{regID}/{authzID}/{challengeID}`, respectively. For deployability, the
old handlers continue to work, and the URLs returned for newly created
objects will still point to the paths used by the old handlers
(`/acme/authz-v3/` and `/acme/chall-v3/`).

There are some self-referential URLs in authz and challenge responses,
like the Location header, and the URL of challenges embedded in an
authorization object. This PR updates `prepAuthorizationForDisplay` and
`prepChallengeForDisplay` so those URLs can be generated consistently
with the path that was requested.

For the WFE tests, in most cases I duplicated an entire test and then
updated it to test the `WithAccount` handler. The idea is that once
we're fully switched over to the new format we can delete the tests for
the non-`WithAccount` variants.

Part of #7683
2024-11-06 11:52:10 -08:00
Aaron Gable 2603aa45a8
Remove weakKeyFile and blockedKeyFile support (#7783)
Goodkey has two ways to detect a key as weak: it runs a variety of
algorithmic checks (such as Fermat factorization and rocacheck), or the
key can be listed in a "weak key file". Similarly, it has two ways to
detect a key as blocked: it can call a generic function (which we use to
query our database), or the key can be listed in a "blocked key file".

This is two methods too many. Reliance on files of weak or blocked keys
introduces unnecessary complexity to both the implementation and
configuration of the goodkey package. Remove both "key file" options and
delete all code which supported them.

Also remove //test/block-a-key, as it was only used to generate these
test files.

IN-10762 tracked the removal of these files in prod.

Fixes https://github.com/letsencrypt/boulder/issues/7748
2024-11-06 10:48:39 -08:00
James Renken 6a2819a95a
Introduce separate UpdateRegistrationContact & UpdateRegistrationKey methods in RA & SA (#7735)
Introduce separate UpdateRegistrationContact & UpdateRegistrationKey
methods in RA & SA

Clear contact field during DeactivateRegistration

Part of #7716
Part of #5554
2024-11-06 10:07:31 -08:00
Aaron Gable 84b15eb911
Truncate ARI timestamps to 1-second resolution (#7784)
There's no reason for us to be providing nanosecond precision on ARI
timestamps, and apparently it messes up some JSON date-parsing
libraries.

Fixes https://github.com/letsencrypt/boulder/issues/7779
2024-11-05 10:04:27 -08:00
Aaron Gable 46fc4c25ab
Re-enable wastedassign linter (#7788)
Fixes https://github.com/letsencrypt/boulder/issues/6202
2024-11-05 07:45:37 -08:00
Aaron Gable 3b62e81999
Clean up migration to separate remoteva executable (#7787)
Fixes https://github.com/letsencrypt/boulder/issues/7733
2024-11-05 07:44:08 -08:00
Jacob Hoffman-Andrews cb56bf6beb
ca: log cert signing using JSON objects (#7742)
This makes the log events easier to parse, and makes it easier to
consistently use the correct fields from the issuance request.

Also, reduce the number of fields that are logged on error events.
Logging just the serial and the error in most cases should suffice to
cross-reference the error with the item that we attempted to sign.

One downside is that this increases the total log size (10kB above, vs
7kB from a similar production issuance) due in part to more repetition.
For example, both the "signing cert" and "signing cert success" log
lines include the full precert DER.

Note that our long-term plan for more structured logs is to have a
unique event id to join logs on, which can avoid this repetition. But
since we don't currently have convenient ways to do that join, some
duplication (as we currently have in the logs) seems reasonable.
2024-11-04 16:54:07 -08:00
Matthew McPherrin 1fa66781ee
Allow admin command to block key from a CSR file (#7770)
One format we receive key compromise reports is as a CSR file. For
example, from https://pwnedkeys.com/revokinator

This allows the admin command to block a key from a CSR directly,
instead of needing to validate it manually and get the SPKI or key from
it.

I've added a flag (default true) to check the signature on the CSR, in
case we ever decide we want to block a key from a CSR with a bad
signature for whatever reason.
2024-11-04 15:11:43 -08:00
Jacob Hoffman-Andrews 02685602a2
web: add feature flag PropagateCancels (#7778)
This allow client-initiated cancels to propagate through gRPC.

IN-10803 tracks the SRE-side changes to enable this flag.
2024-11-04 14:37:29 -08:00
Aaron Gable 21bc647fa5
Simplify TestTraces to reduce specificity (#7785)
TestTraces is designed to test whether our Open Telemetry tracing system
is working: that spans are being output, that they have the appropriate
parents, etc. It should not be testing whether Boulder took a specific
path through its code -- that's the domain of package-specific unit
tests. Simplify TestTraces to the point that it is asserting (nearly)
the bare minimum about the set of operations Boulder performs.
2024-11-04 12:02:57 -08:00
James Renken 4adc65fb7d
Rate limits: replace redis SET with INCRBY (#7782)
Add a new method, `BatchIncrement`, to issue `IncrBy` (instead of `Set`)
to Redis. This helps prevent the race condition that allows bursts of
near-simultaneous requests to, effectively, spend the same token.

Call this new method when incrementing an existing key. New keys still
need to use `BatchSet` because Redis doesn't have a facility to, within
a single operation, increment _or_ set a default value if none exists.

Add a new feature flag, `IncrementRateLimits`, gating the use of this
new method.

CPS Compliance Review: This feature flag does not change any behaviour
that is described or constrained by our CP/CPS. The closest relation
would just be API availability in general.

Fixes #7780
2024-11-04 11:20:44 -08:00
Jacob Hoffman-Andrews 2d69d7b9df
wfe: set Retry-After header on 500s (#7781) 2024-11-04 10:34:11 -08:00
1830 changed files with 103083 additions and 67667 deletions

View File

@ -27,7 +27,7 @@ jobs:
# tags and 5 tests there would be 10 jobs run.
b:
# The type of runner that the job will run on
runs-on: ubuntu-20.04
runs-on: ubuntu-24.04
strategy:
# When set to true, GitHub cancels all in-progress jobs if any matrix job fails. Default: true
@ -36,7 +36,7 @@ jobs:
matrix:
# Add additional docker image tags here and all tests will be run with the additional image.
BOULDER_TOOLS_TAG:
- go1.23.1_2024-09-05
- 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.3.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.5' ]
go-version: [ '1.24.1' ]
steps:
# Checks out your repository under $GITHUB_WORKSPACE, so your job can access it
@ -153,7 +153,7 @@ jobs:
permissions:
contents: none
if: ${{ always() }}
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
name: Boulder CI Test Matrix
needs:
- b

View File

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

View File

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

View File

@ -15,20 +15,25 @@ jobs:
fail-fast: false
matrix:
GO_VERSION:
- "1.23.1"
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
@ -48,3 +53,14 @@ jobs:
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 boulder*.checksums.txt
- name: Build ct-test-srv Container
run: docker buildx build . --build-arg "GO_VERSION=${{ matrix.GO_VERSION }}" -f test/ct-test-srv/Dockerfile -t "ghcr.io/letsencrypt/ct-test-srv:${{ github.ref_name }}-go${{ matrix.GO_VERSION }}"
- name: Login to ghcr.io
run: printenv GITHUB_TOKEN | docker login ghcr.io -u "${{ github.actor }}" --password-stdin
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Push ct-test-srv Container
run: docker push "ghcr.io/letsencrypt/ct-test-srv:${{ github.ref_name }}-go${{ matrix.GO_VERSION }}"

View File

@ -16,8 +16,8 @@ jobs:
fail-fast: false
matrix:
GO_VERSION:
- "1.23.1"
runs-on: ubuntu-20.04
- "1.24.4"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
@ -27,7 +27,7 @@ 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
@ -42,3 +42,6 @@ jobs:
- name: Show checksums
id: check
run: cat boulder*.checksums.txt
- name: Build ct-test-srv Container
run: docker buildx build . --build-arg "GO_VERSION=${{ matrix.GO_VERSION }}" -f test/ct-test-srv/Dockerfile -t "ghcr.io/letsencrypt/ct-test-srv:${{ github.sha }}-go${{ matrix.GO_VERSION }}"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

43
allowlist/main.go Normal file
View File

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

109
allowlist/main_test.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

@ -6,6 +6,7 @@ import (
"fmt"
"math/rand/v2"
"net"
"net/netip"
"strconv"
"sync"
"time"
@ -61,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

317
ca/ca.go
View File

@ -9,13 +9,11 @@ import (
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/gob"
"encoding/hex"
"errors"
"fmt"
"math/big"
mrand "math/rand/v2"
"strings"
"time"
ct "github.com/google/certificate-transparency-go"
@ -34,13 +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/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"
)
@ -51,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
@ -62,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 {
@ -111,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) {
@ -126,9 +137,10 @@ 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
// The prefix is prepended to the serial number.
prefix byte
@ -168,66 +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.
//
// 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) (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")
}
// 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)
return nil, fmt.Errorf("must pass at least one certificate profile")
}
profilesByName := make(map[string]*certProfileWithID, len(profiles))
profilesByHash := make(map[[32]byte]*certProfileWithID, len(profiles))
for name, profileConfig := range profiles {
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())
withID := certProfileWithID{
profilesByName[name] = &certProfileWithID{
name: name,
hash: hash,
profile: profile,
}
profilesByName[name] = &withID
_, found := profilesByHash[hash]
if found {
return certProfilesMaps{}, fmt.Errorf("duplicate certificate profile hash %d", hash)
}
profilesByHash[hash] = &withID
}
return certProfilesMaps{defaultName, profilesByHash, profilesByName}, nil
return profilesByName, nil
}
// NewCertificateAuthorityImpl creates a CA instance that can sign certificates
@ -235,9 +208,9 @@ func makeCertificateProfilesMap(defaultName string, profiles map[string]*issuanc
// 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,
serialPrefix byte,
maxNames int,
@ -258,7 +231,7 @@ func NewCertificateAuthorityImpl(
return nil, errors.New("must have at least one issuer")
}
certProfiles, err := makeCertificateProfilesMap(defaultCertProfileName, certificateProfiles)
certProfiles, err := makeCertificateProfilesMap(certificateProfiles)
if err != nil {
return nil, err
}
@ -270,6 +243,7 @@ func NewCertificateAuthorityImpl(
ca = &certificateAuthorityImpl{
sa: sa,
sctClient: sctService,
pa: pa,
issuers: issuers,
certProfiles: certProfiles,
@ -291,35 +265,18 @@ 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.
if core.IsAnyNilOrZero(issueReq, issueReq.Csr, issueReq.RegistrationID) {
return nil, berrors.InternalServerError("Incomplete issue certificate request")
}
// 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, fmt.Errorf("the CA is incapable of using a profile named %s", issueReq.CertProfileName)
}
func (ca *certificateAuthorityImpl) issuePrecertificate(ctx context.Context, certProfile *certProfileWithID, issueReq *capb.IssueCertificateRequest) ([]byte, error) {
serialBigInt, err := ca.generateSerialNumber()
if err != nil {
return nil, err
@ -339,7 +296,7 @@ func (ca *certificateAuthorityImpl) IssuePrecertificate(ctx context.Context, iss
return nil, err
}
precertDER, cpwid, err := ca.issuePrecertificateInner(ctx, issueReq, certProfile, serialBigInt, notBefore, notAfter)
precertDER, _, err := ca.issuePrecertificateInner(ctx, issueReq, certProfile, serialBigInt, notBefore, notAfter)
if err != nil {
return nil, err
}
@ -349,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.
@ -366,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
@ -379,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
}
@ -409,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
}
@ -428,28 +403,37 @@ 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)
@ -462,28 +446,21 @@ 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
}
// generateSerialNumber produces a big.Int which has more than 64 bits of
@ -566,26 +543,26 @@ func (ca *certificateAuthorityImpl) issuePrecertificateInner(ctx context.Context
serialHex := core.SerialToString(serialBigInt)
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: notBefore,
NotAfter: notAfter,
dnsNames, ipAddresses, err := identifier.FromCSR(csr).ToValues()
if err != nil {
return nil, nil, err
}
ca.log.AuditInfof("Signing precert: serial=[%s] regID=[%d] names=[%s] csr=[%s]",
serialHex, issueReq.RegistrationID, strings.Join(req.DNSNames, ", "), hex.EncodeToString(csr.Raw))
req := &issuance.IssuanceRequest{
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(req.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()
}
@ -608,17 +585,32 @@ 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(req.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)
@ -631,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] precert=[%s] certProfileName=[%s] certProfileHash=[%x]",
issuer.Name(), serialHex, issueReq.RegistrationID, strings.Join(req.DNSNames, ", "), hex.EncodeToString(certDER), certProfile.name, certProfile.hash)
return certDER, &certProfileWithID{certProfile.name, certProfile.hash, nil}, nil
logEvent.Result.Precertificate = hex.EncodeToString(certDER)
// The CSR is big and not that informative, so don't log it a second time.
logEvent.CSR = ""
ca.log.AuditObject("Signing precert success", logEvent)
return certDER, &certProfileWithID{certProfile.name, nil}, nil
}
// verifyTBSCertIsDeterministic verifies that x509.CreateCertificate signing

View File

@ -11,6 +11,7 @@ import (
"errors"
"fmt"
"math/big"
mrand "math/rand"
"os"
"strings"
"testing"
@ -32,11 +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"
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"
)
@ -91,25 +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
certProfiles map[string]*issuance.ProfileConfig
serialPrefix byte
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 {
@ -148,27 +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["legacy"] = &issuance.ProfileConfig{
AllowMustStaple: true,
MaxValidityPeriod: config.Duration{Duration: time.Hour * 24 * 90},
MaxValidityBackdate: config.Duration{Duration: time.Hour},
IgnoredLints: []string{"w_subject_common_name_included"},
IncludeCRLDistributionPoints: true,
MaxValidityPeriod: config.Duration{Duration: time.Hour * 24 * 90},
MaxValidityBackdate: config.Duration{Duration: time.Hour},
IgnoredLints: []string{"w_subject_common_name_included"},
}
certProfiles["modern"] = &issuance.ProfileConfig{
AllowMustStaple: true,
OmitCommonName: true,
OmitKeyEncipherment: true,
OmitClientAuth: true,
OmitSKID: 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"},
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)
@ -179,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),
@ -205,7 +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}
certificatesCount := prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "certificates",
Help: "Number of certificates issued",
}, []string{"profile"})
cametrics := &caMetrics{signatureCount, signErrorCount, lintErrorCount, certificatesCount}
ocsp, err := NewOCSPImpl(
boulderIssuers,
@ -232,18 +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: "legacy",
certProfiles: certProfiles,
serialPrefix: 0x11,
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(),
}
}
@ -255,7 +260,7 @@ func TestSerialPrefix(t *testing.T) {
nil,
nil,
nil,
"",
nil,
nil,
0x00,
testCtx.maxNames,
@ -269,7 +274,7 @@ func TestSerialPrefix(t *testing.T) {
nil,
nil,
nil,
"",
nil,
nil,
0x80,
testCtx.maxNames,
@ -311,7 +316,6 @@ func TestIssuePrecertificate(t *testing.T) {
{"IssuePrecertificate", CNandSANCSR, issueCertificateSubTestIssuePrecertificate},
{"ProfileSelectionRSA", CNandSANCSR, issueCertificateSubTestProfileSelectionRSA},
{"ProfileSelectionECDSA", ECDSACSR, issueCertificateSubTestProfileSelectionECDSA},
{"MustStaple", MustStapleCSR, issueCertificateSubTestMustStaple},
{"UnknownExtension", UnsupportedExtensionCSR, issueCertificateSubTestUnknownExtension},
{"CTPoisonExtension", CTPoisonExtensionCSR, issueCertificateSubTestCTPoisonExtension},
{"CTPoisonExtensionEmpty", CTPoisonExtensionEmptyCSR, issueCertificateSubTestCTPoisonExtension},
@ -328,13 +332,11 @@ func TestIssuePrecertificate(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")
@ -359,14 +361,20 @@ func TestIssuePrecertificate(t *testing.T) {
}
}
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)
sa := &mockSA{}
ca, err := NewCertificateAuthorityImpl(
sa,
mockSCTService{},
testCtx.pa,
testCtx.boulderIssuers,
testCtx.defaultCertProfileName,
testCtx.certProfiles,
testCtx.serialPrefix,
testCtx.maxNames,
@ -403,9 +411,9 @@ func TestNoIssuers(t *testing.T) {
sa := &mockSA{}
_, err := NewCertificateAuthorityImpl(
sa,
mockSCTService{},
testCtx.pa,
nil, // No issuers
testCtx.defaultCertProfileName,
testCtx.certProfiles,
testCtx.serialPrefix,
testCtx.maxNames,
@ -424,9 +432,9 @@ func TestMultipleIssuers(t *testing.T) {
sa := &mockSA{}
ca, err := NewCertificateAuthorityImpl(
sa,
mockSCTService{},
testCtx.pa,
testCtx.boulderIssuers,
testCtx.defaultCertProfileName,
testCtx.certProfiles,
testCtx.serialPrefix,
testCtx.maxNames,
@ -436,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] {
@ -457,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] {
@ -488,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),
@ -498,9 +504,9 @@ func TestUnpredictableIssuance(t *testing.T) {
ca, err := NewCertificateAuthorityImpl(
sa,
mockSCTService{},
testCtx.pa,
boulderIssuers,
testCtx.defaultCertProfileName,
testCtx.certProfiles,
testCtx.serialPrefix,
testCtx.maxNames,
@ -522,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")
@ -547,23 +554,11 @@ func TestMakeCertificateProfilesMap(t *testing.T) {
testCtx := setup(t)
test.AssertEquals(t, len(testCtx.certProfiles), 2)
testProfile := issuance.ProfileConfig{
AllowMustStaple: false,
MaxValidityPeriod: config.Duration{Duration: time.Hour * 24 * 90},
MaxValidityBackdate: config.Duration{Duration: time.Hour},
}
type nameToHash struct {
name string
hash [32]byte
}
testCases := []struct {
name string
defaultName string
profileConfigs map[string]*issuance.ProfileConfig
expectedErrSubstr string
expectedProfiles []nameToHash
expectedProfiles []string
}{
{
name: "nil profile map",
@ -576,56 +571,30 @@ func TestMakeCertificateProfilesMap(t *testing.T) {
expectedErrSubstr: "at least one certificate profile",
},
{
name: "no profile matching default name",
defaultName: "default",
profileConfigs: map[string]*issuance.ProfileConfig{
"notDefault": &testProfile,
},
expectedErrSubstr: "profile object was not found for that name",
},
{
name: "duplicate hash",
defaultName: "default",
profileConfigs: map[string]*issuance.ProfileConfig{
"default": &testProfile,
"default2": &testProfile,
},
expectedErrSubstr: "duplicate certificate profile hash",
},
{
name: "empty profile config",
defaultName: "empty",
name: "empty profile config",
profileConfigs: map[string]*issuance.ProfileConfig{
"empty": {},
},
expectedProfiles: []nameToHash{
{
name: "empty",
hash: [32]byte{0x25, 0x27, 0x72, 0xa1, 0xaf, 0x95, 0xfe, 0xc7, 0x32, 0x78, 0x38, 0x97, 0xd0, 0xf1, 0x83, 0x92, 0xc3, 0xac, 0x60, 0x91, 0x68, 0x4f, 0x22, 0xb6, 0x57, 0x2f, 0x89, 0x1a, 0x54, 0xe5, 0xd8, 0xa3},
},
},
expectedErrSubstr: "at least one revocation mechanism must be included",
},
{
name: "default profiles from setup func",
defaultName: testCtx.defaultCertProfileName,
profileConfigs: testCtx.certProfiles,
expectedProfiles: []nameToHash{
{
name: "legacy",
hash: [32]byte{0x44, 0xc5, 0xbc, 0x73, 0x8, 0x95, 0xba, 0x4c, 0x13, 0x12, 0xc4, 0xc, 0x5d, 0x77, 0x2f, 0x54, 0xf8, 0x54, 0x1, 0xb8, 0x84, 0xaf, 0x6c, 0x58, 0x74, 0x6, 0xac, 0xda, 0x3e, 0x37, 0xfc, 0x88},
},
{
name: "modern",
hash: [32]byte{0x58, 0x7, 0xea, 0x3a, 0x85, 0xcd, 0xf9, 0xd1, 0x7a, 0x9a, 0x59, 0x76, 0xfc, 0x92, 0xea, 0x1b, 0x69, 0x54, 0xe4, 0xbe, 0xcf, 0xe3, 0x91, 0xfa, 0x85, 0x4, 0xbf, 0x1f, 0x55, 0x97, 0x2c, 0x8b},
},
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 {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
profiles, err := makeCertificateProfilesMap(tc.defaultName, tc.profileConfigs)
profiles, err := makeCertificateProfilesMap(tc.profileConfigs)
if tc.expectedErrSubstr != "" {
test.AssertError(t, err, "profile construction should have failed")
@ -635,17 +604,14 @@ func TestMakeCertificateProfilesMap(t *testing.T) {
}
if tc.expectedProfiles != nil {
test.AssertEquals(t, len(profiles.profileByName), len(tc.expectedProfiles))
test.AssertEquals(t, len(profiles), len(tc.expectedProfiles))
}
for _, expected := range tc.expectedProfiles {
cpwid, ok := profiles.profileByName[expected.name]
test.Assert(t, ok, fmt.Sprintf("expected profile %q not found", expected.name))
test.AssertEquals(t, cpwid.hash, expected.hash)
cpwid, ok := profiles[expected]
test.Assert(t, ok, fmt.Sprintf("expected profile %q not found", expected))
cpwid, ok = profiles.profileByHash[expected.hash]
test.Assert(t, ok, fmt.Sprintf("expected profile %q not found", expected.hash))
test.AssertEquals(t, cpwid.name, expected.name)
test.AssertEquals(t, cpwid.name, expected)
}
})
}
@ -703,9 +669,9 @@ func TestInvalidCSRs(t *testing.T) {
sa := &mockSA{}
ca, err := NewCertificateAuthorityImpl(
sa,
mockSCTService{},
testCtx.pa,
testCtx.boulderIssuers,
testCtx.defaultCertProfileName,
testCtx.certProfiles,
testCtx.serialPrefix,
testCtx.maxNames,
@ -718,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)
@ -742,9 +709,9 @@ func TestRejectValidityTooLong(t *testing.T) {
ca, err := NewCertificateAuthorityImpl(
&mockSA{},
mockSCTService{},
testCtx.pa,
testCtx.boulderIssuers,
testCtx.defaultCertProfileName,
testCtx.certProfiles,
testCtx.serialPrefix,
testCtx.maxNames,
@ -755,7 +722,8 @@ func TestRejectValidityTooLong(t *testing.T) {
test.AssertNotError(t, err, "Failed to create CA")
// 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)
}
@ -774,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)
}
@ -835,9 +785,9 @@ func TestIssueCertificateForPrecertificate(t *testing.T) {
sa := &mockSA{}
ca, err := NewCertificateAuthorityImpl(
sa,
mockSCTService{},
testCtx.pa,
testCtx.boulderIssuers,
testCtx.defaultCertProfileName,
testCtx.certProfiles,
testCtx.serialPrefix,
testCtx.maxNames,
@ -847,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)
@ -870,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)
@ -900,9 +847,9 @@ func TestIssueCertificateForPrecertificateWithSpecificCertificateProfile(t *test
sa := &mockSA{}
ca, err := NewCertificateAuthorityImpl(
sa,
mockSCTService{},
testCtx.pa,
testCtx.boulderIssuers,
testCtx.defaultCertProfileName,
testCtx.certProfiles,
testCtx.serialPrefix,
testCtx.maxNames,
@ -912,19 +859,19 @@ func TestIssueCertificateForPrecertificateWithSpecificCertificateProfile(t *test
testCtx.fc)
test.AssertNotError(t, err, "Failed to create CA")
selectedProfile := "legacy"
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)
@ -941,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)
@ -1016,9 +962,9 @@ func TestIssueCertificateForPrecertificateDuplicateSerial(t *testing.T) {
sa := &dupeSA{}
ca, err := NewCertificateAuthorityImpl(
sa,
mockSCTService{},
testCtx.pa,
testCtx.boulderIssuers,
testCtx.defaultCertProfileName,
testCtx.certProfiles,
testCtx.serialPrefix,
testCtx.maxNames,
@ -1033,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.")
}
@ -1063,9 +1005,9 @@ func TestIssueCertificateForPrecertificateDuplicateSerial(t *testing.T) {
errorsa := &getCertErrorSA{}
errorca, err := NewCertificateAuthorityImpl(
errorsa,
mockSCTService{},
testCtx.pa,
testCtx.boulderIssuers,
testCtx.defaultCertProfileName,
testCtx.certProfiles,
testCtx.serialPrefix,
testCtx.maxNames,
@ -1075,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.")
}

View File

@ -4,6 +4,7 @@ import (
"context"
"crypto/x509"
"encoding/hex"
mrand "math/rand"
"testing"
"time"
@ -31,9 +32,9 @@ func TestOCSP(t *testing.T) {
testCtx := setup(t)
ca, err := NewCertificateAuthorityImpl(
&mockSA{},
mockSCTService{},
testCtx.pa,
testCtx.boulderIssuers,
testCtx.defaultCertProfileName,
testCtx.certProfiles,
testCtx.serialPrefix,
testCtx.maxNames,
@ -44,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{
@ -69,9 +71,9 @@ func TestOCSP(t *testing.T) {
// Issue a certificate from an ECDSA issuer, request OCSP from the same issuer,
// and make sure it works.
ecdsaCertPB, err := ca.IssuePrecertificate(ctx, &capb.IssueCertificateRequest{Csr: ECDSACSR, RegistrationID: arbitraryRegID})
ecdsaCertDER, err := ca.issuePrecertificate(ctx, profile, &capb.IssueCertificateRequest{Csr: ECDSACSR, RegistrationID: mrand.Int63(), OrderID: mrand.Int63(), CertProfileName: "legacy"})
test.AssertNotError(t, err, "Failed to issue certificate")
ecdsaCert, err := x509.ParseCertificate(ecdsaCertPB.DER)
ecdsaCert, err := x509.ParseCertificate(ecdsaCertDER)
test.AssertNotError(t, err, "Failed to parse ecdsaCert")
ecdsaIssuerID := issuance.IssuerNameID(ecdsaCert)
ecdsaOCSPPB, err := ocspi.GenerateOCSP(ctx, &capb.GenerateOCSPRequest{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

59
cmd/admin/admin_test.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -31,8 +31,6 @@ type Config struct {
RAService *cmd.GRPCClientConfig
SAService *cmd.GRPCClientConfig
DebugAddr string
Features features.Config
}
@ -72,7 +70,6 @@ func main() {
subcommands := map[string]subcommand{
"revoke-cert": &subcommandRevokeCert{},
"block-key": &subcommandBlockKey{},
"update-email": &subcommandUpdateEmail{},
"pause-identifier": &subcommandPauseIdentifier{},
"unpause-account": &subcommandUnpauseAccount{},
}

View File

@ -39,12 +39,12 @@ func (p *subcommandPauseIdentifier) Run(ctx context.Context, a *admin) error {
return errors.New("the -batch-file flag is required")
}
identifiers, err := a.readPausedAccountFile(p.batchFile)
idents, err := a.readPausedAccountFile(p.batchFile)
if err != nil {
return err
}
_, err = a.pauseIdentifiers(ctx, identifiers, p.parallelism)
_, err = a.pauseIdentifiers(ctx, idents, p.parallelism)
if err != nil {
return err
}
@ -60,19 +60,19 @@ func (a *admin) pauseIdentifiers(ctx context.Context, entries []pauseCSVData, pa
return nil, errors.New("cannot pause identifiers because no pauseData was sent")
}
accountToIdentifiers := make(map[int64][]*corepb.Identifier)
accountToIdents := make(map[int64][]*corepb.Identifier)
for _, entry := range entries {
accountToIdentifiers[entry.accountID] = append(accountToIdentifiers[entry.accountID], &corepb.Identifier{
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(accountToIdentifiers))
respChan := make(chan *sapb.PauseIdentifiersResponse, len(accountToIdents))
work := make(chan struct {
accountID int64
identifiers []*corepb.Identifier
accountID int64
idents []*corepb.Identifier
}, parallelism)
var wg sync.WaitGroup
@ -83,11 +83,11 @@ func (a *admin) pauseIdentifiers(ctx context.Context, entries []pauseCSVData, pa
for data := range work {
response, err := a.sac.PauseIdentifiers(ctx, &sapb.PauseRequest{
RegistrationID: data.accountID,
Identifiers: data.identifiers,
Identifiers: data.idents,
})
if err != nil {
errCount.Add(1)
a.log.Errf("error pausing identifier(s) %q for account %d: %v", data.identifiers, data.accountID, err)
a.log.Errf("error pausing identifier(s) %q for account %d: %v", data.idents, data.accountID, err)
} else {
respChan <- response
}
@ -95,11 +95,11 @@ func (a *admin) pauseIdentifiers(ctx context.Context, entries []pauseCSVData, pa
}()
}
for accountID, identifiers := range accountToIdentifiers {
for accountID, idents := range accountToIdents {
work <- struct {
accountID int64
identifiers []*corepb.Identifier
}{accountID, identifiers}
accountID int64
idents []*corepb.Identifier
}{accountID, idents}
}
close(work)
wg.Wait()

View File

@ -14,7 +14,6 @@ import (
sapb "github.com/letsencrypt/boulder/sa/proto"
"github.com/letsencrypt/boulder/unpause"
"golang.org/x/exp/maps"
)
// subcommandUnpauseAccount encapsulates the "admin unpause-account" command.
@ -44,16 +43,13 @@ func (u *subcommandUnpauseAccount) Run(ctx context.Context, a *admin) error {
"-account": u.accountID != 0,
"-batch-file": u.batchFile != "",
}
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 regIDs []int64
var err error
switch maps.Keys(setInputs)[0] {
switch activeFlag {
case "-account":
regIDs = []int64{u.accountID}
case "-batch-file":

View File

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

View File

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

View File

@ -3,8 +3,8 @@ package notmain
import (
"context"
"flag"
"fmt"
"os"
"reflect"
"strconv"
"time"
@ -19,6 +19,7 @@ import (
bgrpc "github.com/letsencrypt/boulder/grpc"
"github.com/letsencrypt/boulder/issuance"
"github.com/letsencrypt/boulder/policy"
rapb "github.com/letsencrypt/boulder/ra/proto"
sapb "github.com/letsencrypt/boulder/sa/proto"
)
@ -32,43 +33,26 @@ 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.
// 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 is a path to a zlint config file.
// Deprecated: Use CertProfiles.LintConfig instead.
LintConfig string
// IgnoredLints is a list of lint names for which any errors should be
// ignored.
// Deprecated: Use CertProfiles.IgnoredLints instead.
IgnoredLints []string
}
// How long issued certificates are valid for.
// Deprecated: Use Issuance.CertProfiles.MaxValidityPeriod instead.
Expiry config.Duration
// How far back certificates should be backdated.
// Deprecated: Use Issuance.CertProfiles.MaxValidityBackdate instead.
Backdate config.Duration
// What digits we should prepend to serials after randomly generating them.
// Deprecated: Use SerialPrefixHex instead.
SerialPrefix int `validate:"required_without=SerialPrefixHex,omitempty,min=1,max=127"`
@ -94,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
@ -179,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())
@ -195,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 == "" {
@ -213,48 +183,40 @@ func main() {
}
clk := cmd.Clock()
var crlShards int
issuers := make([]*issuance.Issuer, 0, len(c.CA.Issuance.Issuers))
for _, issuerConfig := range c.CA.Issuance.Issuers {
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")
}
// If no individual cert profiles are configured, pretend that the deprecated
// top-level profile as the only individual profile instead.
// TODO(#7414) Remove this fallback.
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
}
// If any individual cert profile doesn't have its own lint configuration,
// instead copy in the deprecated top-level lint configuration.
// TODO(#7414): Remove this fallback.
for _, prof := range c.CA.Issuance.CertProfiles {
if prof.LintConfig == "" && len(prof.IgnoredLints) == 0 {
prof.LintConfig = c.CA.Issuance.LintConfig
prof.IgnoredLints = c.CA.Issuance.IgnoredLints
}
cmd.Fail("At least one profile must be configured")
}
tlsConfig, err := c.CA.TLS.Load(scope)
cmd.FailOnError(err, "TLS config")
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")
@ -295,9 +257,9 @@ func main() {
if !c.CA.DisableCertService {
cai, err := ca.NewCertificateAuthorityImpl(
sa,
sctService,
pa,
issuers,
c.CA.Issuance.DefaultCertificateProfileName,
c.CA.Issuance.CertProfiles,
serialPrefix,
c.CA.MaxNames,

View File

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

View File

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

View File

@ -12,6 +12,7 @@ import (
"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"
@ -42,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 requests 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
@ -71,14 +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.
//
// TODO(#7632) Update this to use the new HMACKeyConfig.
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
@ -115,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
@ -151,13 +156,6 @@ 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.
@ -243,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{}
@ -287,6 +280,13 @@ func main() {
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.")
}
@ -294,11 +294,8 @@ func main() {
cmd.Fail("'getNonceService' must be configured")
}
var noncePrefixKey string
if c.WFE.NoncePrefixKey.PasswordFile != "" {
noncePrefixKey, err = c.WFE.NoncePrefixKey.Pass()
cmd.FailOnError(err, "Failed to load noncePrefixKey")
}
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")
@ -320,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
@ -349,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")
}
@ -372,17 +355,16 @@ func main() {
logger,
c.WFE.Timeout.Duration,
c.WFE.StaleTimeout.Duration,
authorizationLifetime,
pendingAuthorizationLifetime,
c.WFE.MaxContactsPerRegistration,
rac,
sac,
eec,
gnc,
rnc,
noncePrefixKey,
accountGetter,
limiter,
txnBuilder,
maxNames,
c.WFE.CertProfiles,
unpauseSigner,
c.WFE.Unpause.JWTLifetime.Duration,

View File

@ -5,7 +5,6 @@ import (
"os"
"strings"
_ "github.com/letsencrypt/boulder/cmd/admin-revoker"
_ "github.com/letsencrypt/boulder/cmd/akamai-purger"
_ "github.com/letsencrypt/boulder/cmd/bad-key-revoker"
_ "github.com/letsencrypt/boulder/cmd/boulder-ca"
@ -16,15 +15,12 @@ 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"
@ -85,37 +81,31 @@ var boulderUsage = fmt.Sprintf(`Usage: %s <subcommand> [flags]
func main() {
defer cmd.AuditPanic()
var command string
if core.Command() == "boulder" {
// Operator passed the boulder component as a subcommand.
if len(os.Args) <= 1 {
// No arguments passed.
fmt.Fprint(os.Stderr, boulderUsage)
return
}
if os.Args[1] == "--help" || os.Args[1] == "-help" {
// Help flag passed.
fmt.Fprint(os.Stderr, boulderUsage)
return
}
if os.Args[1] == "--list" || os.Args[1] == "-list" {
// List flag passed.
for _, c := range cmd.AvailableCommands() {
fmt.Println(c)
}
return
}
command = os.Args[1]
// Remove the subcommand from the arguments.
os.Args = os.Args[1:]
} else {
// Operator ran a boulder component using a symlink.
command = core.Command()
if len(os.Args) <= 1 {
// No arguments passed.
fmt.Fprint(os.Stderr, boulderUsage)
return
}
if os.Args[1] == "--help" || os.Args[1] == "-help" {
// Help flag passed.
fmt.Fprint(os.Stderr, boulderUsage)
return
}
if os.Args[1] == "--list" || os.Args[1] == "-list" {
// List flag passed.
for _, c := range cmd.AvailableCommands() {
fmt.Println(c)
}
return
}
// Remove the subcommand from the arguments.
command := os.Args[1]
os.Args = os.Args[1:]
config := getConfigPath()
if config != "" {
// Config flag passed.

View File

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

View File

@ -305,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
@ -318,11 +317,11 @@ func makeTemplate(randReader io.Reader, profile *certProfile, pubKey []byte, tbc
}
for _, policyConfig := range profile.Policies {
oid, err := parseOID(policyConfig.OID)
x509OID, err := x509.ParseOID(policyConfig.OID)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to parse %s as OID: %w", policyConfig.OID, err)
}
cert.PolicyIdentifiers = append(cert.PolicyIdentifiers, oid)
cert.Policies = append(cert.Policies, x509OID)
}
return cert, nil

View File

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

View File

@ -96,7 +96,7 @@ func postIssuanceLinting(fc *x509.Certificate, skipLints []string) error {
type keyGenConfig struct {
Type string `yaml:"type"`
RSAModLength uint `yaml:"rsa-mod-length"`
RSAModLength int `yaml:"rsa-mod-length"`
ECDSACurve string `yaml:"ecdsa-curve"`
}

View File

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

View File

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

View File

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

View File

@ -18,7 +18,6 @@ import (
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)
}
@ -66,8 +71,8 @@ func init() {
}
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.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{
@ -372,7 +377,7 @@ func TestGetAndProcessCerts(t *testing.T) {
test.AssertEquals(t, len(checker.certs), 5)
wg := new(sync.WaitGroup)
wg.Add(1)
checker.processCerts(context.Background(), wg, false, nil)
checker.processCerts(context.Background(), wg, false)
test.AssertEquals(t, checker.issuedReport.BadCerts, int64(5))
test.AssertEquals(t, len(checker.issuedReport.Entries), 5)
}
@ -396,9 +401,6 @@ func (db mismatchedCountDB) SelectNullInt(_ context.Context, _ string, _ ...inte
// `getCerts` then calls `Select` to retrieve the Certificate rows. We pull
// a dastardly switch-a-roo here and return an empty set
func (db mismatchedCountDB) Select(_ context.Context, output interface{}, _ string, _ ...interface{}) ([]interface{}, error) {
// But actually return nothing
outputPtr, _ := output.(*[]sa.CertWithID)
*outputPtr = []sa.CertWithID{}
return nil, nil
}
@ -427,7 +429,7 @@ func (db mismatchedCountDB) SelectOne(_ context.Context, _ interface{}, _ string
func TestGetCertsEmptyResults(t *testing.T) {
saDbMap, err := sa.DBMapForTest(vars.DBConnSA)
test.AssertNotError(t, err, "Couldn't connect to database")
checker := newChecker(saDbMap, clock.NewFake(), pa, kp, time.Hour, testValidityDurations, blog.NewMock())
checker := newChecker(saDbMap, clock.NewFake(), pa, kp, time.Hour, testValidityDurations, nil, blog.NewMock())
checker.dbMap = mismatchedCountDB{}
batchSize = 3
@ -453,7 +455,7 @@ func (db emptyDB) SelectNullInt(_ context.Context, _ string, _ ...interface{}) (
// expected if the DB finds no certificates to match the SELECT query and
// should return an error.
func TestGetCertsNullResults(t *testing.T) {
checker := newChecker(emptyDB{}, clock.NewFake(), pa, kp, time.Hour, testValidityDurations, blog.NewMock())
checker := newChecker(emptyDB{}, clock.NewFake(), pa, kp, time.Hour, testValidityDurations, nil, blog.NewMock())
err := checker.getCerts(context.Background())
test.AssertError(t, err, "Should have gotten error from empty DB")
@ -497,7 +499,7 @@ func TestGetCertsLate(t *testing.T) {
clk := clock.NewFake()
db := &lateDB{issuedTime: clk.Now().Add(-time.Hour)}
checkPeriod := 24 * time.Hour
checker := newChecker(db, clk, pa, kp, checkPeriod, testValidityDurations, blog.NewMock())
checker := newChecker(db, clk, pa, kp, checkPeriod, testValidityDurations, nil, blog.NewMock())
err := checker.getCerts(context.Background())
test.AssertNotError(t, err, "getting certs")
@ -582,21 +584,22 @@ func TestIgnoredLint(t *testing.T) {
err = loglist.InitLintList("../../test/ct-test-srv/log_list.json")
test.AssertNotError(t, err, "failed to load ct log list")
testKey, _ := rsa.GenerateKey(rand.Reader, 2048)
checker := newChecker(saDbMap, clock.NewFake(), pa, kp, time.Hour, testValidityDurations, blog.NewMock())
checker := newChecker(saDbMap, clock.NewFake(), pa, kp, time.Hour, testValidityDurations, nil, blog.NewMock())
serial := big.NewInt(1337)
x509OID, err := x509.OIDFromInts([]uint64{1, 2, 3})
test.AssertNotError(t, err, "failed to create x509.OID")
template := &x509.Certificate{
Subject: pkix.Name{
CommonName: "CPU's Cool CA",
},
SerialNumber: serial,
NotBefore: time.Now(),
NotAfter: time.Now().Add(testValidityDuration - time.Second),
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
PolicyIdentifiers: []asn1.ObjectIdentifier{
{1, 2, 3},
},
SerialNumber: serial,
NotBefore: time.Now(),
NotAfter: time.Now().Add(testValidityDuration - time.Second),
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
Policies: []x509.OID{x509OID},
BasicConstraintsValid: true,
IsCA: true,
IssuingCertificateURL: []string{"http://aia.example.org"},
@ -623,43 +626,46 @@ func TestIgnoredLint(t *testing.T) {
subjectCert, err := x509.ParseCertificate(subjectCertDer)
test.AssertNotError(t, err, "failed to parse EE cert")
cert := core.Certificate{
cert := &corepb.Certificate{
Serial: core.SerialToString(serial),
DER: subjectCertDer,
Der: subjectCertDer,
Digest: core.Fingerprint256(subjectCertDer),
Issued: subjectCert.NotBefore,
Expires: subjectCert.NotAfter,
Issued: timestamppb.New(subjectCert.NotBefore),
Expires: timestamppb.New(subjectCert.NotAfter),
}
// Without any ignored lints we expect one error level result due to the
// missing OCSP url in the template.
// Without any ignored lints we expect several errors and warnings about SCTs,
// the common name, and the subject key identifier extension.
expectedProblems := []string{
"zlint error: e_sub_cert_aia_does_not_contain_ocsp_url",
"zlint warn: w_subject_common_name_included",
"zlint warn: w_ext_subject_key_identifier_not_recommended_subscriber",
"zlint info: w_ct_sct_policy_count_unsatisfied Certificate had 0 embedded SCTs. Browser policy may require 2 for this certificate.",
"zlint error: e_scts_from_same_operator Certificate had too few embedded SCTs; browser policy requires 2.",
}
sort.Strings(expectedProblems)
slices.Sort(expectedProblems)
// Check the certificate with a nil ignore map. This should return the
// expected zlint problems.
_, problems := checker.checkCert(context.Background(), cert, nil)
sort.Strings(problems)
_, problems := checker.checkCert(context.Background(), cert)
slices.Sort(problems)
test.AssertDeepEquals(t, problems, expectedProblems)
// Check the certificate again with an ignore map that excludes the affected
// lints. This should return no problems.
_, problems = checker.checkCert(context.Background(), cert, map[string]bool{
"e_sub_cert_aia_does_not_contain_ocsp_url": true,
"w_subject_common_name_included": true,
"w_ct_sct_policy_count_unsatisfied": true,
"e_scts_from_same_operator": true,
lints, err := linter.NewRegistry([]string{
"w_subject_common_name_included",
"w_ext_subject_key_identifier_not_recommended_subscriber",
"w_ct_sct_policy_count_unsatisfied",
"e_scts_from_same_operator",
})
test.AssertNotError(t, err, "creating test lint registry")
checker.lints = lints
_, problems = checker.checkCert(context.Background(), cert)
test.AssertEquals(t, len(problems), 0)
}
func TestPrecertCorrespond(t *testing.T) {
checker := newChecker(nil, clock.New(), pa, kp, time.Hour, testValidityDurations, blog.NewMock())
checker := newChecker(nil, clock.New(), pa, kp, time.Hour, testValidityDurations, nil, blog.NewMock())
checker.getPrecert = func(_ context.Context, _ string) ([]byte, error) {
return []byte("hello"), nil
}
@ -675,14 +681,14 @@ func TestPrecertCorrespond(t *testing.T) {
SerialNumber: serial,
}
certDer, _ := x509.CreateCertificate(rand.Reader, &rawCert, &rawCert, &testKey.PublicKey, testKey)
cert := core.Certificate{
cert := &corepb.Certificate{
Serial: core.SerialToString(serial),
Digest: core.Fingerprint256(certDer),
DER: certDer,
Issued: time.Now(),
Expires: expiry,
Der: certDer,
Issued: timestamppb.New(time.Now()),
Expires: timestamppb.New(expiry),
}
_, problems := checker.checkCert(context.Background(), cert, nil)
_, problems := checker.checkCert(context.Background(), cert)
if len(problems) == 0 {
t.Errorf("expected precert correspondence problem")
}

View File

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

View File

@ -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,33 +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 an HMAC key. The key must
// consist of 256 bits of random data to be suitable for use as a 256-bit
// hashing key (e.g., the output of `openssl rand -hex 32`).
// 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 loads the HMAC key from the file, ensures it is exactly 32 characters
// in length, and returns it as a byte slice.
// 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
}
trimmed := strings.TrimRight(string(contents), "\n")
if len(trimmed) != 32 {
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 unpauseHMACKey, length must be 32 alphanumeric characters, got %d",
len(trimmed),
"validating HMAC key, must be exactly 256 bits (32 bytes) after decoding, got %d",
len(decoded),
)
}
return []byte(trimmed), nil
return decoded, nil
}

View File

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

View File

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

View File

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

View File

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

View File

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

130
cmd/email-exporter/main.go Normal file
View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@ -5,6 +5,7 @@ import (
"flag"
"fmt"
"net"
"net/netip"
"os"
"github.com/letsencrypt/boulder/cmd"
@ -19,30 +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.
//
// TODO(#7632) Update this to use the new HMACKeyConfig.
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)
@ -51,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() {
@ -84,12 +75,9 @@ func main() {
c.NonceService.DebugAddr = *debugAddr
}
if c.NonceService.NoncePrefixKey.PasswordFile == "" {
cmd.Fail("NoncePrefixKey PasswordFile must be set")
}
key, err := c.NonceService.NonceHMACKey.Load()
cmd.FailOnError(err, "Failed to load nonceHMACKey file.")
key, err := c.NonceService.NoncePrefixKey.Pass()
cmd.FailOnError(err, "Failed to load 'noncePrefixKey' file.")
noncePrefix, err := derivePrefix(key, c.NonceService.GRPC.Address)
cmd.FailOnError(err, "Failed to derive nonce prefix")

View File

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

View File

@ -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)
}
}
}

View File

@ -1,3 +0,0 @@
This is a test message body regarding these domains:
{{ range . }} {{ .Extra.domainName }}
{{ end }}

View File

@ -1,4 +0,0 @@
id,domainName
1,one.example.com
2,two.example.net
3,three.example.org
1 id domainName
2 1 one.example.com
3 2 two.example.net
4 3 three.example.org

View File

@ -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

View File

@ -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"
@ -25,9 +26,7 @@ type Config struct {
// 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.
//
// TODO(#7615): Make mandatory.
Perspective string `validate:"omitempty"`
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
@ -38,10 +37,8 @@ type Config struct {
// - RIPE
// - APNIC
// - LACNIC
// - AfriNIC
//
// TODO(#7615): Make mandatory.
RIR string `validate:"omitempty,oneof=ARIN RIPE APNIC LACNIC AfriNIC"`
// - 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
@ -90,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()
@ -119,6 +112,7 @@ func main() {
scope,
clk,
c.RVA.DNSTries,
c.RVA.UserAgent,
logger,
tlsConfig)
} else {
@ -128,6 +122,7 @@ func main() {
scope,
clk,
c.RVA.DNSTries,
c.RVA.UserAgent,
logger,
tlsConfig)
}
@ -135,7 +130,6 @@ 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,
@ -143,7 +137,8 @@ func main() {
logger,
c.RVA.AccountURIPrefixes,
c.RVA.Perspective,
c.RVA.RIR)
c.RVA.RIR,
iana.IsReservedAddr)
cmd.FailOnError(err, "Unable to create Remote-VA server")
start, err := bgrpc.NewServer(c.RVA.GRPC, logger).Add(

View File

@ -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)

View File

@ -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
}
@ -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}
}
}
}

View File

@ -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)
}

View File

@ -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

View File

@ -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))
}

View File

@ -25,11 +25,12 @@ type Config struct {
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 the SFE.
// lower than the upstream's timeout when making requests to this service.
Timeout config.Duration `validate:"-"`
// ShutdownStopTimeout is the duration that the SFE will wait before
// shutting down any listening servers.
// 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

View File

@ -31,7 +31,7 @@ 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"
@ -261,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{}))
@ -287,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{

View File

@ -23,22 +23,24 @@ 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": {}
}`)
)
@ -47,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) {
@ -127,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) {
@ -273,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()
@ -288,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) {
@ -302,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()

View File

@ -7,7 +7,7 @@ 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
WillingToIssue(identifier.ACMEIdentifiers) error
ChallengeTypesFor(identifier.ACMEIdentifier) ([]AcmeChallenge, error)
ChallengeTypeEnabled(AcmeChallenge) bool
CheckAuthzChallenges(*Authorization) error

View File

@ -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,12 @@ type ValidationRecord struct {
URL string `json:"url,omitempty"`
// Shared
DnsName 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
@ -144,30 +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"`
// 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 either the Primary Perspective
// (VA) or a group of RVAs deployed in the same datacenter.
Perspective string `json:"perspective,omitempty"`
// 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 `json:"rir,omitempty"`
}
// Challenge is an aggregate of all data needed for any challenges.
@ -229,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.DnsName == "" || rec.Port == "" || rec.AddressUsed == nil ||
if rec.URL == "" || rec.Hostname == "" || rec.Port == "" || (rec.AddressUsed == netip.Addr{}) ||
len(rec.AddressesResolved) == 0 {
return false
}
@ -243,8 +224,8 @@ 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].DnsName == "" || ch.ValidationRecord[0].Port == "" ||
ch.ValidationRecord[0].AddressUsed == nil || len(ch.ValidationRecord[0].AddressesResolved) == 0 {
if ch.ValidationRecord[0].Hostname == "" || ch.ValidationRecord[0].Port == "" ||
(ch.ValidationRecord[0].AddressUsed == netip.Addr{}) || len(ch.ValidationRecord[0].AddressesResolved) == 0 {
return false
}
case ChallengeTypeDNS01:
@ -253,7 +234,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].DnsName == "" {
if ch.ValidationRecord[0].Hostname == "" {
return false
}
return true
@ -290,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:"-" 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:"-" 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
@ -323,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
//
@ -337,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
@ -481,16 +467,21 @@ type RenewalInfo struct {
// 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),
},
}
}
@ -505,8 +496,8 @@ 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,
}

View File

@ -4,7 +4,7 @@ import (
"crypto/rsa"
"encoding/json"
"math/big"
"net"
"net/netip"
"testing"
"time"
@ -37,10 +37,10 @@ func TestRecordSanityCheckOnUnsupportedChallengeType(t *testing.T) {
rec := []ValidationRecord{
{
URL: "http://localhost/test",
DnsName: "localhost",
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"},
},
}

View File

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.34.1
// protoc-gen-go v1.36.5
// protoc v3.20.1
// source: core.proto
@ -12,6 +12,7 @@ import (
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
@ -22,21 +23,18 @@ const (
)
type Identifier struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
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
Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"`
Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *Identifier) Reset() {
*x = Identifier{}
if protoimpl.UnsafeEnabled {
mi := &file_core_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
mi := &file_core_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Identifier) String() string {
@ -47,7 +45,7 @@ func (*Identifier) ProtoMessage() {}
func (x *Identifier) ProtoReflect() protoreflect.Message {
mi := &file_core_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)
@ -77,11 +75,8 @@ func (x *Identifier) GetValue() string {
}
type Challenge struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
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"`
@ -92,15 +87,15 @@ type Challenge struct {
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[1]
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 {
@ -111,7 +106,7 @@ func (*Challenge) ProtoMessage() {}
func (x *Challenge) ProtoReflect() protoreflect.Message {
mi := &file_core_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)
@ -183,31 +178,28 @@ func (x *Challenge) GetValidationrecords() []*ValidationRecord {
}
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[2]
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 {
@ -218,7 +210,7 @@ func (*ValidationRecord) ProtoMessage() {}
func (x *ValidationRecord) ProtoReflect() protoreflect.Message {
mi := &file_core_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)
@ -290,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[3]
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 {
@ -316,7 +305,7 @@ func (*ProblemDetails) ProtoMessage() {}
func (x *ProblemDetails) ProtoReflect() protoreflect.Message {
mi := &file_core_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)
@ -353,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"`
@ -364,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[4]
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 {
@ -383,7 +369,7 @@ func (*Certificate) ProtoMessage() {}
func (x *Certificate) ProtoReflect() protoreflect.Message {
mi := &file_core_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)
@ -441,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"`
@ -455,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[5]
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 {
@ -474,7 +457,7 @@ func (*CertificateStatus) ProtoMessage() {}
func (x *CertificateStatus) ProtoReflect() protoreflect.Message {
mi := &file_core_proto_msgTypes[5]
if protoimpl.UnsafeEnabled && x != nil {
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@ -553,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[6]
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 {
@ -585,7 +563,7 @@ func (*Registration) ProtoMessage() {}
func (x *Registration) ProtoReflect() protoreflect.Message {
mi := &file_core_proto_msgTypes[6]
if protoimpl.UnsafeEnabled && x != nil {
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@ -621,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
@ -635,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
@ -657,26 +621,23 @@ func (x *Registration) GetStatus() string {
}
type Authorization struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
RegistrationID int64 `protobuf:"varint,3,opt,name=registrationID,proto3" json:"registrationID,omitempty"`
// Fields specified by RFC 8555, Section 7.1.4
DnsName string `protobuf:"bytes,2,opt,name=dnsName,proto3" json:"dnsName,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[7]
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 {
@ -687,7 +648,7 @@ func (*Authorization) ProtoMessage() {}
func (x *Authorization) ProtoReflect() protoreflect.Message {
mi := &file_core_proto_msgTypes[7]
if protoimpl.UnsafeEnabled && x != nil {
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@ -716,11 +677,11 @@ func (x *Authorization) GetRegistrationID() int64 {
return 0
}
func (x *Authorization) GetDnsName() string {
func (x *Authorization) GetIdentifier() *Identifier {
if x != nil {
return x.DnsName
return x.Identifier
}
return ""
return nil
}
func (x *Authorization) GetStatus() string {
@ -744,35 +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 ""
}
Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
RegistrationID int64 `protobuf:"varint,2,opt,name=registrationID,proto3" json:"registrationID,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"`
DnsNames []string `protobuf:"bytes,8,rep,name=dnsNames,proto3" json:"dnsNames,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"`
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[8]
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 {
@ -783,7 +749,7 @@ func (*Order) ProtoMessage() {}
func (x *Order) ProtoReflect() protoreflect.Message {
mi := &file_core_proto_msgTypes[8]
if protoimpl.UnsafeEnabled && x != nil {
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@ -826,9 +792,9 @@ func (x *Order) GetExpires() *timestamppb.Timestamp {
return nil
}
func (x *Order) GetDnsNames() []string {
func (x *Order) GetIdentifiers() []*Identifier {
if x != nil {
return x.DnsNames
return x.Identifiers
}
return nil
}
@ -868,6 +834,13 @@ func (x *Order) GetCertificateProfileName() string {
return ""
}
func (x *Order) GetReplaces() string {
if x != nil {
return x.Replaces
}
return ""
}
func (x *Order) GetBeganProcessing() bool {
if x != nil {
return x.BeganProcessing
@ -876,23 +849,20 @@ func (x *Order) GetBeganProcessing() bool {
}
type CRLEntry struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
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[9]
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 {
@ -903,7 +873,7 @@ func (*CRLEntry) ProtoMessage() {}
func (x *CRLEntry) ProtoReflect() protoreflect.Message {
mi := &file_core_proto_msgTypes[9]
if protoimpl.UnsafeEnabled && x != nil {
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
@ -941,7 +911,7 @@ 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,
@ -1036,96 +1006,101 @@ var file_core_proto_rawDesc = []byte{
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,
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, 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, 0xf2, 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, 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, 0x64, 0x6e, 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01,
0x28, 0x09, 0x52, 0x07, 0x64, 0x6e, 0x73, 0x4e, 0x61, 0x6d, 0x65, 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, 0xd9, 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, 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, 0x12, 0x1a, 0x0a, 0x08,
0x64, 0x6e, 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08,
0x64, 0x6e, 0x73, 0x4e, 0x61, 0x6d, 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, 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, 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, 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,
}
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, 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, 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,
})
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, 10)
var file_core_proto_goTypes = []interface{}{
var file_core_proto_goTypes = []any{
(*Identifier)(nil), // 0: core.Identifier
(*Challenge)(nil), // 1: core.Challenge
(*ValidationRecord)(nil), // 2: core.ValidationRecord
@ -1149,17 +1124,19 @@ var file_core_proto_depIdxs = []int32{
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
10, // 10: core.Authorization.expires:type_name -> google.protobuf.Timestamp
1, // 11: core.Authorization.challenges:type_name -> core.Challenge
10, // 12: core.Order.expires:type_name -> google.protobuf.Timestamp
3, // 13: core.Order.error:type_name -> core.ProblemDetails
10, // 14: core.Order.created:type_name -> google.protobuf.Timestamp
10, // 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
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() }
@ -1167,133 +1144,11 @@ 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.(*Identifier); 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.(*Challenge); 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.(*ValidationRecord); 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.(*ProblemDetails); 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.(*Certificate); 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.(*CertificateStatus); 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.(*Registration); 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.(*Authorization); 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.(*Order); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_core_proto_msgTypes[9].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: 10,
NumExtensions: 0,
@ -1304,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
}

View File

@ -30,15 +30,15 @@ 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;
}
@ -84,30 +84,32 @@ 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;
int64 registrationID = 3;
// Fields specified by RFC 8555, Section 7.1.4
string dnsName = 2;
reserved 2; // Previously dnsName
Identifier identifier = 11;
string status = 4;
google.protobuf.Timestamp expires = 9;
repeated core.Challenge challenges = 6;
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;
@ -116,13 +118,15 @@ message Order {
// finalize and certificate URLs from the id and certificateSerial fields.
string status = 7;
google.protobuf.Timestamp expires = 12;
repeated string dnsNames = 8;
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;
}

View File

@ -1,6 +1,7 @@
package core
import (
"context"
"crypto"
"crypto/ecdsa"
"crypto/rand"
@ -20,16 +21,18 @@ import (
"path"
"reflect"
"regexp"
"slices"
"sort"
"strings"
"time"
"unicode"
"github.com/go-jose/go-jose/v4"
"github.com/letsencrypt/boulder/identifier"
"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"
@ -318,26 +321,15 @@ func UniqueLowerNames(names []string) (unique []string) {
return
}
// NormalizeIdentifiers 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.
func NormalizeIdentifiers(identifiers []identifier.ACMEIdentifier) []identifier.ACMEIdentifier {
for i := range identifiers {
identifiers[i].Value = strings.ToLower(identifiers[i].Value)
// 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)
}
sort.Slice(identifiers, func(i, j int) bool {
return fmt.Sprintf("%s:%s", identifiers[i].Type, identifiers[i].Value) < fmt.Sprintf("%s:%s", identifiers[j].Type, identifiers[j].Value)
})
return slices.Compact(identifiers)
}
// 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, ",")))
hash := sha256.Sum256([]byte(strings.Join(values, ",")))
return hash[:]
}
@ -395,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])
}

View File

@ -1,18 +1,23 @@
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"
@ -251,26 +256,6 @@ func TestUniqueLowerNames(t *testing.T) {
test.AssertDeepEquals(t, []string{"a.com", "bar.com", "baz.com", "foobar.com"}, u)
}
func TestNormalizeIdentifiers(t *testing.T) {
identifiers := []identifier.ACMEIdentifier{
{Type: "DNS", Value: "foobar.com"},
{Type: "DNS", Value: "fooBAR.com"},
{Type: "DNS", Value: "baz.com"},
{Type: "DNS", Value: "foobar.com"},
{Type: "DNS", Value: "bar.com"},
{Type: "DNS", Value: "bar.com"},
{Type: "DNS", Value: "a.com"},
}
expected := []identifier.ACMEIdentifier{
{Type: "DNS", Value: "a.com"},
{Type: "DNS", Value: "bar.com"},
{Type: "DNS", Value: "baz.com"},
{Type: "DNS", Value: "foobar.com"},
}
u := NormalizeIdentifiers(identifiers)
test.AssertDeepEquals(t, expected, u)
}
func TestValidSerial(t *testing.T) {
notLength32Or36 := "A"
length32 := strings.Repeat("A", 32)
@ -336,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.")
}
}

View File

@ -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")
}

View File

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.34.1
// protoc-gen-go v1.36.5
// protoc v3.20.1
// source: 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
}

View File

@ -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;
}

View File

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.3.0
// - protoc-gen-go-grpc v1.5.1
// - protoc v3.20.1
// source: 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)
}

View File

@ -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)

View File

@ -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")

View File

@ -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)),
},
},
})

View File

@ -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)
}
}

View File

@ -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. It uses
// NamesFromCSR to normalize the DNS names before checking whether we'll issue
// for them.
// 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,67 +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
}
// NamesFromCSR also performs normalization, returning values that may not
// match the literal CSR contents.
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 a CN was provided, it will be used if it
// is short enough, otherwise there will be no CN. If no CN 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. The resulting SANs will
// always include the original CN, if any.
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 names{SANs: core.UniqueLowerNames(sans)}
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 ""
}

View File

@ -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) ChallengeTypesFor(identifier identifier.ACMEIdentifier) ([]core.AcmeChallenge, error) {
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")
}
}
@ -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,7 +109,7 @@ func TestVerifyCSR(t *testing.T) {
signedReq,
100,
&mockPA{},
invalidNoDNS,
invalidNoIdent,
},
{
signedReqWithLongCN,
@ -115,7 +121,7 @@ func TestVerifyCSR(t *testing.T) {
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,44 +161,38 @@ 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, all SANs too long to be the CN",
@ -195,7 +201,6 @@ func TestNamesFromCSR(t *testing.T) {
tooLongString + ".b.com",
}},
"",
[]string{tooLongString + ".a.com", tooLongString + ".b.com"},
},
{
"no explicit CN, leading SANs too long to be the CN",
@ -206,7 +211,6 @@ func TestNamesFromCSR(t *testing.T) {
"b.com",
}},
"a.com",
[]string{"a.com", tooLongString + ".a.com", tooLongString + ".b.com", "b.com"},
},
{
"explicit CN, leading SANs too long to be the CN",
@ -219,7 +223,6 @@ 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",
@ -227,7 +230,6 @@ func TestNamesFromCSR(t *testing.T) {
Subject: pkix.Name{CommonName: tooLongString + ".a.com"},
},
"",
[]string{tooLongString + ".a.com"},
},
{
"explicit CN that's too long to be the CN, with a SAN",
@ -237,14 +239,27 @@ func TestNamesFromCSR(t *testing.T) {
"b.com",
}},
"",
[]string{tooLongString + ".a.com", "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)
})
}
}

View File

@ -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
}

View File

@ -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")
}

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