Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Identifiers to Authorization & Order structs #7961

Draft
wants to merge 82 commits into
base: main
Choose a base branch
from
Draft

Conversation

jprenken
Copy link
Contributor

@jprenken jprenken commented Jan 21, 2025

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

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.

Preferentially use these `identifier` fields in every function that uses these structs - but when crossing component boundaries, don't assume they'll be present, for deployability's sake.

Part of #7311
@jprenken jprenken marked this pull request as ready for review January 26, 2025 01:39
@jprenken jprenken requested a review from a team as a code owner January 26, 2025 01:39
Copy link
Contributor

@jsha jsha left a comment

Choose a reason for hiding this comment

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

SliceFromProto is neatly designed in that it does double duty as both a conversion function and a defaulting function, but the number of places it's called with the first or second arg nil suggests that mixing up those roles is actually having a negative effect.

I think we should split up the defaulting function from the type conversion functions, so it's clear at the call site which behavior we're using a function for.

Also: you and I had talked on video the other day about the neat property that proto objects all have named accessor functions. At the time I wasn't sure if it made sense to take advantage of that property, but looking at this PR I think it would be quite useful. We have a bunch of different objects that all have Identifiers and DnsNames (and thus .GetIdentifiers() and .GetDnsNames()). And we want the same logic for all of them: if you got Identifiers, use it verbatim and ignore DnsNames; otherwise convert DnsNames and use that.

It would look something like this (untested):

type HasIdentifiers interface {
  GetIdentifiers() []corepb.Identifier
  GetDnsNames() []string
}

func ProtoToProtoDefaulted(input HasIdentifiers) []*corepb.Identifier {
  if len(input.GetIdentifiers()) > 0 {
    return input.GetIdentifiers()
  }
  return ToProto(DNSNames(input.GetDnsNames()))
}

// DNSNames returns a list of ACMEIdentifier of type "dns".
func DNSNames(input []string) []ACMEIdentifier {
  var out []ACMEIdentifier
  for _, in := range input {
    out = append(out, NewDNS(in))
  }
  return out
}

// ToProto turns a list of ACMEIdentifier into a list of *corepb.Identifier.
func ToProto(input []ACMEIdentifier) []*corepb.Identifier {
  var out []*corepb.Identifier
  for _, in := range input {
    out = append(out, in.AsProto())
  }
  return out
}

I think there's also a place for another function:

// AllDNS returns a list of DNS names from the input, if the input contains only DNS identifiers. Otherwise it returns an error.
func AllDNS(input []ACMEIdentifier) ([]string, error)

This would facilitate updating some of the places where we're still assuming DNS, and ensuring that we return error if that assumption fails.

In terms of looking for split points where smaller PRs could land on their own, it looks like the changes to policy/pa.go and their call sites (WillingToIssue, WellFormedDomainNames) are nicely independent. Also, the new FromCert() (and updating all the call sites that need it) is a nicely independent piece of code.

Comment on lines +57 to +61
pbIdents := make([]*corepb.Identifier, len(idents))
for i, ident := range idents {
pbIdents[i] = ident.AsProto()
}
return pbIdents
Copy link
Contributor

Choose a reason for hiding this comment

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

Remember, we avoid preallocating except in very performance-sensitive functions. And when we do preallocate we particularly avoid this form of make that exposes zero-initialized values.

Suggested change
pbIdents := make([]*corepb.Identifier, len(idents))
for i, ident := range idents {
pbIdents[i] = ident.AsProto()
}
return pbIdents
var out []*corepb.Identifier
for _, ident := range idents {
out = append(out, ident.AsProto())
}
return out

Comment on lines +1104 to +1114
if core.IsAnyNilOrZero(authzPB.Id, authzPB.Status, authzPB.Expires) {
wfe.sendError(response, logEvent, probs.ServerInternal("Problem getting authorization"), errIncompleteGRPCResponse)
return
}
// TODO(#7311): Remove this conditional, and merge the IsAnyNilOrZero check
// upwards, once all RPC users are populating Identifiers.
pbIdent := authzPB.Identifier
if pbIdent == nil {
pbIdent = identifier.NewDNS(authzPB.DnsName).AsProto()
}
if core.IsAnyNilOrZero(pbIdent) && core.IsAnyNilOrZero(authzPB.DnsName) {
Copy link
Contributor

Choose a reason for hiding this comment

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

The repeat of this block makes me think we need the single-value equivalent of SliceFromProto, with its defaulting semantics "choose A; if A is empty, convert B and choose it".

Also, we can simplify these blocks by putting the defaulting code first:

Suggested change
if core.IsAnyNilOrZero(authzPB.Id, authzPB.Status, authzPB.Expires) {
wfe.sendError(response, logEvent, probs.ServerInternal("Problem getting authorization"), errIncompleteGRPCResponse)
return
}
// TODO(#7311): Remove this conditional, and merge the IsAnyNilOrZero check
// upwards, once all RPC users are populating Identifiers.
pbIdent := authzPB.Identifier
if pbIdent == nil {
pbIdent = identifier.NewDNS(authzPB.DnsName).AsProto()
}
if core.IsAnyNilOrZero(pbIdent) && core.IsAnyNilOrZero(authzPB.DnsName) {
ident := identifier.FromProtoWithDefault(authzPB.Identifier, authzPB.DnsName)
if core.IsAnyNilOrZero(ident, authzPB.Id, authzPB.Status, authzPB.Expires) {

Then we can place the TODO on FromProtoWithDefault, noting that it can be removed after the transition.

@@ -99,6 +100,8 @@ type names struct {
// 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.
//
// Deprecated: TODO(#7311): Use identifier.FromCert instead.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// Deprecated: TODO(#7311): Use identifier.FromCert instead.
// Deprecated: TODO(#7311): Use identifier.FromCSR instead.

if len(names) == 0 {
return nil
}
idents := make([]ACMEIdentifier, len(names))
Copy link
Contributor

Choose a reason for hiding this comment

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

Same comment as above: don't preallocate slices without performance justification, and in particular avoid exposing zero values unnecessarily.

Comment on lines +74 to +76
if len(names) == 0 {
return nil
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This clause is unnecessary. If names is empty, the returned idents will also be empty.

Comment on lines +1992 to +1996
// TODO(#7311): Remove this conditional once all RPC users are populating
// Identifiers.
idents := order.Identifiers
if idents == nil {
idents = identifier.SliceAsProto(identifier.SliceFromProto(nil, order.DnsNames))
Copy link
Contributor

Choose a reason for hiding this comment

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

This seems like a lot of back-and-forth conversion, and there's no reason for the first arg of SliceFromProto to be nil. I'd expect:

Suggested change
// TODO(#7311): Remove this conditional once all RPC users are populating
// Identifiers.
idents := order.Identifiers
if idents == nil {
idents = identifier.SliceAsProto(identifier.SliceFromProto(nil, order.DnsNames))
// TODO(#7311): Remove this conditional once all RPC users are populating
// Identifiers.
idents := identifier.SliceFromProto(order.Identifiers, order.DnsNames)
...
Identifiers: idents,

Comment on lines +2037 to +2040
names := make([]string, len(idents))
for i, ident := range idents {
names[i] = ident.Value
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Another instance of slice preallocation.

Also: here we make the assumption that identifier type is DNS. Instead of making that assumption we should check it, and error out if not. Otherwise it's too easy to miss updating this later.

Comment on lines +119 to +127
if cert.Subject.CommonName != "" {
// Boulder won't generate certificates with a CN that's not also present
// in the SANs, but such a certificate is possible. If appended, this is
// deduplicated later with Normalize(). We assume the CN is a DNSName,
// because CNs are untyped strings without metadata, and we will never
// configure a Boulder profile to issue a certificate that contains both
// an IP address identifier and a CN.
sans = append(sans, NewDNS(cert.Subject.CommonName))
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Since this function asserts that we're only handling trusted certificates, we can disregard CommonName entirely. We have guarantees elsewhere that anything from the CN is also in the SANs.

Comment on lines +129 to +135
for _, netIP := range cert.IPAddresses {
netipAddr, ok := netip.AddrFromSlice(netIP)
if !ok {
return nil, fmt.Errorf("converting IP from bytes: %s", netIP)
}
sans = append(sans, NewIP(netipAddr))
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Here we're parsing the IP address, then NewIP is un-parsing it. So the effect is just that we're validating it. We don't validate DNSNames in this function; that's the role of other functions. I recommend:

Suggested change
for _, netIP := range cert.IPAddresses {
netipAddr, ok := netip.AddrFromSlice(netIP)
if !ok {
return nil, fmt.Errorf("converting IP from bytes: %s", netIP)
}
sans = append(sans, NewIP(netipAddr))
}
for _, ip := range cert.IPAddresses {
sans = append(sans, ACMEIdentifier{
Type: TypeIP,
Value: ip.String(),
})
}

That also allows us to remove the error return of FromCert(), which is nice.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants