-
-
Notifications
You must be signed in to change notification settings - Fork 614
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
base: main
Are you sure you want to change the base?
Conversation
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
…within structs only
There was a problem hiding this 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.
pbIdents := make([]*corepb.Identifier, len(idents)) | ||
for i, ident := range idents { | ||
pbIdents[i] = ident.AsProto() | ||
} | ||
return pbIdents |
There was a problem hiding this comment.
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.
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 |
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) { |
There was a problem hiding this comment.
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:
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// 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)) |
There was a problem hiding this comment.
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.
if len(names) == 0 { | ||
return nil | ||
} |
There was a problem hiding this comment.
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.
// 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)) |
There was a problem hiding this comment.
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:
// 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, |
names := make([]string, len(idents)) | ||
for i, ident := range idents { | ||
names[i] = ident.Value | ||
} |
There was a problem hiding this comment.
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.
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)) | ||
} |
There was a problem hiding this comment.
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.
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)) | ||
} |
There was a problem hiding this comment.
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:
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.
Add
identifier
fields, which will soon replace thednsName
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 ofdnsName
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
andsa
versions will be incompatible because of a type change in the arguments tosa.SelectAuthzsMatchingIssuance
.Part of #7311