Skip to content

Commit 9b9ed86

Browse files
authored
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
1 parent b017c1b commit 9b9ed86

File tree

7 files changed

+122
-42
lines changed

7 files changed

+122
-42
lines changed

cmd/id-exporter/main.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,10 @@ type resultEntry struct {
3838
Hostname string `json:"hostname,omitempty"`
3939
}
4040

41-
// reverseHostname converts (reversed) names sourced from the
42-
// registrations table to standard hostnames.
43-
func (r *resultEntry) reverseHostname() {
44-
r.Hostname = sa.ReverseName(r.Hostname)
41+
// encodeIssuedName converts FQDNs and IP addresses to/from their format in the
42+
// issuedNames table.
43+
func (r *resultEntry) encodeIssuedName() {
44+
r.Hostname = sa.EncodeIssuedName(r.Hostname)
4545
}
4646

4747
// idExporterResults is passed as a selectable 'holder' for the results
@@ -112,7 +112,7 @@ func (c idExporter) findIDsWithExampleHostnames(ctx context.Context) (idExporter
112112
}
113113

114114
for _, result := range holder {
115-
result.reverseHostname()
115+
result.encodeIssuedName()
116116
}
117117
return holder, nil
118118
}
@@ -135,7 +135,7 @@ func (c idExporter) findIDsForHostnames(ctx context.Context, hostnames []string)
135135
AND n.reversedName = :reversedName;`,
136136
map[string]interface{}{
137137
"expireCutoff": c.clk.Now().Add(-c.grace),
138-
"reversedName": sa.ReverseName(hostname),
138+
"reversedName": sa.EncodeIssuedName(hostname),
139139
},
140140
)
141141
if err != nil {

cmd/reversed-hostname-checker/main.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
// Read a list of reversed hostnames, separated by newlines. Print only those
2-
// that are rejected by the current policy.
1+
// Read a list of reversed FQDNs and/or normal IP addresses, separated by
2+
// newlines. Print only those that are rejected by the current policy.
33

44
package notmain
55

@@ -9,6 +9,7 @@ import (
99
"fmt"
1010
"io"
1111
"log"
12+
"net/netip"
1213
"os"
1314

1415
"github.com/letsencrypt/boulder/cmd"
@@ -50,8 +51,15 @@ func main() {
5051
}
5152
var errors bool
5253
for scanner.Scan() {
53-
n := sa.ReverseName(scanner.Text())
54-
err := pa.WillingToIssue(identifier.ACMEIdentifiers{identifier.NewDNS(n)})
54+
n := sa.EncodeIssuedName(scanner.Text())
55+
var ident identifier.ACMEIdentifier
56+
ip, err := netip.ParseAddr(n)
57+
if err == nil {
58+
ident = identifier.NewIP(ip)
59+
} else {
60+
ident = identifier.NewDNS(n)
61+
}
62+
err = pa.WillingToIssue(identifier.ACMEIdentifiers{ident})
5563
if err != nil {
5664
errors = true
5765
fmt.Printf("%s: %s\n", n, err)

sa/model.go

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"errors"
1111
"fmt"
1212
"math"
13+
"net/netip"
1314
"net/url"
1415
"slices"
1516
"strconv"
@@ -1056,8 +1057,6 @@ func deleteOrderFQDNSet(
10561057
}
10571058

10581059
func addIssuedNames(ctx context.Context, queryer db.Execer, cert *x509.Certificate, isRenewal bool) error {
1059-
// TODO(#7311): Determine & explicitly document whether to place IP address
1060-
// identifiers in issuedNames. We currently skip them.
10611060
if len(cert.DNSNames) == 0 && len(cert.IPAddresses) == 0 {
10621061
return berrors.InternalServerError("certificate has no DNSNames or IPAddresses")
10631062
}
@@ -1068,7 +1067,18 @@ func addIssuedNames(ctx context.Context, queryer db.Execer, cert *x509.Certifica
10681067
}
10691068
for _, name := range cert.DNSNames {
10701069
err = multiInserter.Add([]interface{}{
1071-
ReverseName(name),
1070+
reverseFQDN(name),
1071+
core.SerialToString(cert.SerialNumber),
1072+
cert.NotBefore.Truncate(24 * time.Hour),
1073+
isRenewal,
1074+
})
1075+
if err != nil {
1076+
return err
1077+
}
1078+
}
1079+
for _, ip := range cert.IPAddresses {
1080+
err = multiInserter.Add([]interface{}{
1081+
ip.String(),
10721082
core.SerialToString(cert.SerialNumber),
10731083
cert.NotBefore.Truncate(24 * time.Hour),
10741084
isRenewal,
@@ -1080,6 +1090,32 @@ func addIssuedNames(ctx context.Context, queryer db.Execer, cert *x509.Certifica
10801090
return multiInserter.Insert(ctx, queryer)
10811091
}
10821092

1093+
// EncodeIssuedName translates a FQDN to/from the issuedNames table by reversing
1094+
// its dot-separated elements, and translates an IP address by returning its
1095+
// normal string form.
1096+
//
1097+
// This is for strings of ambiguous identifier values. If you know your string
1098+
// is a FQDN, use reverseFQDN(). If you have an IP address, use
1099+
// netip.Addr.String() or net.IP.String().
1100+
func EncodeIssuedName(name string) string {
1101+
netIP, err := netip.ParseAddr(name)
1102+
if err == nil {
1103+
return netIP.String()
1104+
}
1105+
return reverseFQDN(name)
1106+
}
1107+
1108+
// reverseFQDN reverses the elements of a dot-separated FQDN.
1109+
//
1110+
// If your string might be an IP address, use EncodeIssuedName() instead.
1111+
func reverseFQDN(fqdn string) string {
1112+
labels := strings.Split(fqdn, ".")
1113+
for i, j := 0, len(labels)-1; i < j; i, j = i+1, j-1 {
1114+
labels[i], labels[j] = labels[j], labels[i]
1115+
}
1116+
return strings.Join(labels, ".")
1117+
}
1118+
10831119
func addKeyHash(ctx context.Context, db db.Inserter, cert *x509.Certificate) error {
10841120
if cert.RawSubjectPublicKeyInfo == nil {
10851121
return errors.New("certificate has a nil RawSubjectPublicKeyInfo")

sa/sa_test.go

Lines changed: 60 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ func TestReplicationLagRetries(t *testing.T) {
322322

323323
// findIssuedName is a small helper test function to directly query the
324324
// issuedNames table for a given name to find a serial (or return an err).
325-
func findIssuedName(ctx context.Context, dbMap db.OneSelector, name string) (string, error) {
325+
func findIssuedName(ctx context.Context, dbMap db.OneSelector, issuedName string) (string, error) {
326326
var issuedNamesSerial string
327327
err := dbMap.SelectOne(
328328
ctx,
@@ -331,7 +331,7 @@ func findIssuedName(ctx context.Context, dbMap db.OneSelector, name string) (str
331331
WHERE reversedName = ?
332332
ORDER BY notBefore DESC
333333
LIMIT 1`,
334-
ReverseName(name))
334+
issuedName)
335335
return issuedNamesSerial, err
336336
}
337337

@@ -438,7 +438,7 @@ func TestAddPrecertificate(t *testing.T) {
438438
test.AssertEquals(t, now, certStatus.OcspLastUpdated.AsTime())
439439

440440
// It should show up in the issued names table
441-
issuedNamesSerial, err := findIssuedName(ctx, sa.dbMap, testCert.DNSNames[0])
441+
issuedNamesSerial, err := findIssuedName(ctx, sa.dbMap, reverseFQDN(testCert.DNSNames[0]))
442442
test.AssertNotError(t, err, "expected no err querying issuedNames for precert")
443443
test.AssertEquals(t, issuedNamesSerial, serial)
444444

@@ -912,10 +912,10 @@ func TestDeactivateAccount(t *testing.T) {
912912
test.AssertError(t, err, "Deactivating an already-deactivated account should fail")
913913
}
914914

915-
func TestReverseName(t *testing.T) {
915+
func TestReverseFQDN(t *testing.T) {
916916
testCases := []struct {
917-
inputDomain string
918-
inputReversed string
917+
fqdn string
918+
reversed string
919919
}{
920920
{"", ""},
921921
{"...", "..."},
@@ -926,8 +926,46 @@ func TestReverseName(t *testing.T) {
926926
}
927927

928928
for _, tc := range testCases {
929-
output := ReverseName(tc.inputDomain)
930-
test.AssertEquals(t, output, tc.inputReversed)
929+
output := reverseFQDN(tc.fqdn)
930+
test.AssertEquals(t, output, tc.reversed)
931+
932+
output = reverseFQDN(tc.reversed)
933+
test.AssertEquals(t, output, tc.fqdn)
934+
}
935+
}
936+
937+
func TestEncodeIssuedName(t *testing.T) {
938+
testCases := []struct {
939+
issuedName string
940+
reversed string
941+
oneWay bool
942+
}{
943+
// Empty strings and bare separators/TLDs should be unchanged.
944+
{"", "", false},
945+
{"...", "...", false},
946+
{"com", "com", false},
947+
// FQDNs should be reversed.
948+
{"example.com", "com.example", false},
949+
{"www.example.com", "com.example.www", false},
950+
{"world.wide.web.example.com", "com.example.web.wide.world", false},
951+
// IP addresses should stay the same.
952+
{"1.2.3.4", "1.2.3.4", false},
953+
{"2602:ff3a:1:abad:c0f:fee:abad:cafe", "2602:ff3a:1:abad:c0f:fee:abad:cafe", false},
954+
// Tricksy FQDNs that look like IPv6 addresses should be parsed as FQDNs.
955+
{"2602.ff3a.1.abad.c0f.fee.abad.cafe", "cafe.abad.fee.c0f.abad.1.ff3a.2602", false},
956+
{"2602.ff3a.0001.abad.0c0f.0fee.abad.cafe", "cafe.abad.0fee.0c0f.abad.0001.ff3a.2602", false},
957+
// IPv6 addresses should be returned in RFC 5952 format.
958+
{"2602:ff3a:0001:abad:0c0f:0fee:abad:cafe", "2602:ff3a:1:abad:c0f:fee:abad:cafe", true},
959+
}
960+
961+
for _, tc := range testCases {
962+
output := EncodeIssuedName(tc.issuedName)
963+
test.AssertEquals(t, output, tc.reversed)
964+
965+
if !tc.oneWay {
966+
output = EncodeIssuedName(tc.reversed)
967+
test.AssertEquals(t, output, tc.issuedName)
968+
}
931969
}
932970
}
933971

@@ -2106,7 +2144,7 @@ func TestAddCertificateRenewalBit(t *testing.T) {
21062144

21072145
reg := createWorkingRegistration(t, sa)
21082146

2109-
assertIsRenewal := func(t *testing.T, name string, expected bool) {
2147+
assertIsRenewal := func(t *testing.T, issuedName string, expected bool) {
21102148
t.Helper()
21112149
var count int
21122150
err := sa.dbMap.SelectOne(
@@ -2115,14 +2153,14 @@ func TestAddCertificateRenewalBit(t *testing.T) {
21152153
`SELECT COUNT(*) FROM issuedNames
21162154
WHERE reversedName = ?
21172155
AND renewal = ?`,
2118-
ReverseName(name),
2156+
issuedName,
21192157
expected,
21202158
)
21212159
test.AssertNotError(t, err, "Unexpected error from SelectOne on issuedNames")
21222160
test.AssertEquals(t, count, 1)
21232161
}
21242162

2125-
// Add a certificate with a never-before-seen name.
2163+
// Add a certificate with never-before-seen identifiers.
21262164
_, testCert := test.ThrowAwayCert(t, fc)
21272165
_, err := sa.AddPrecertificate(ctx, &sapb.AddCertificateRequest{
21282166
Der: testCert.Raw,
@@ -2138,16 +2176,19 @@ func TestAddCertificateRenewalBit(t *testing.T) {
21382176
})
21392177
test.AssertNotError(t, err, "Failed to add certificate")
21402178

2141-
// None of the names should have a issuedNames row marking it as a renewal.
2179+
// No identifier should have an issuedNames row marking it as a renewal.
21422180
for _, name := range testCert.DNSNames {
2143-
assertIsRenewal(t, name, false)
2181+
assertIsRenewal(t, reverseFQDN(name), false)
2182+
}
2183+
for _, ip := range testCert.IPAddresses {
2184+
assertIsRenewal(t, ip.String(), false)
21442185
}
21452186

21462187
// Make a new cert and add its FQDN set to the db so it will be considered a
21472188
// renewal
21482189
serial, testCert := test.ThrowAwayCert(t, fc)
21492190
err = addFQDNSet(ctx, sa.dbMap, identifier.FromCert(testCert), serial, testCert.NotBefore, testCert.NotAfter)
2150-
test.AssertNotError(t, err, "Failed to add name set")
2191+
test.AssertNotError(t, err, "Failed to add identifier set")
21512192
_, err = sa.AddPrecertificate(ctx, &sapb.AddCertificateRequest{
21522193
Der: testCert.Raw,
21532194
Issued: timestamppb.New(testCert.NotBefore),
@@ -2162,9 +2203,12 @@ func TestAddCertificateRenewalBit(t *testing.T) {
21622203
})
21632204
test.AssertNotError(t, err, "Failed to add certificate")
21642205

2165-
// All of the names should have a issuedNames row marking it as a renewal.
2206+
// Each identifier should have an issuedNames row marking it as a renewal.
21662207
for _, name := range testCert.DNSNames {
2167-
assertIsRenewal(t, name, true)
2208+
assertIsRenewal(t, reverseFQDN(name), true)
2209+
}
2210+
for _, ip := range testCert.IPAddresses {
2211+
assertIsRenewal(t, ip.String(), true)
21682212
}
21692213
}
21702214

sa/saro.go

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -153,14 +153,6 @@ func (ssa *SQLStorageAuthorityRO) GetRegistrationByKey(ctx context.Context, req
153153
return registrationModelToPb(model)
154154
}
155155

156-
func ReverseName(domain string) string {
157-
labels := strings.Split(domain, ".")
158-
for i, j := 0, len(labels)-1; i < j; i, j = i+1, j-1 {
159-
labels[i], labels[j] = labels[j], labels[i]
160-
}
161-
return strings.Join(labels, ".")
162-
}
163-
164156
// GetSerialMetadata returns metadata stored alongside the serial number,
165157
// such as the RegID whose certificate request created that serial, and when
166158
// the certificate with that serial will expire.

test/hostname-policy.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ ExactBlockedNames:
1414
# all subdomains/wildcards.
1515
HighRiskBlockedNames:
1616
# See RFC 3152
17-
- "ipv6.arpa"
17+
- "ip6.arpa"
1818
# See RFC 2317
1919
- "in-addr.arpa"
2020
# Etc etc etc

test/integration/cert_storage_failed_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ import (
3131

3232
// getPrecertByName finds and parses a precertificate using the given hostname.
3333
// It returns the most recent one.
34-
func getPrecertByName(db *sql.DB, name string) (*x509.Certificate, error) {
35-
name = sa.ReverseName(name)
34+
func getPrecertByName(db *sql.DB, reversedName string) (*x509.Certificate, error) {
35+
reversedName = sa.EncodeIssuedName(reversedName)
3636
// Find the certificate from the precertificates table. We don't know the serial so
3737
// we have to look it up by name.
3838
var der []byte
@@ -43,15 +43,15 @@ func getPrecertByName(db *sql.DB, name string) (*x509.Certificate, error) {
4343
WHERE reversedName = ?
4444
ORDER BY issuedNames.id DESC
4545
LIMIT 1
46-
`, name)
46+
`, reversedName)
4747
for rows.Next() {
4848
err = rows.Scan(&der)
4949
if err != nil {
5050
return nil, err
5151
}
5252
}
5353
if der == nil {
54-
return nil, fmt.Errorf("no precertificate found for %q", name)
54+
return nil, fmt.Errorf("no precertificate found for %q", reversedName)
5555
}
5656

5757
cert, err := x509.ParseCertificate(der)

0 commit comments

Comments
 (0)