github.com/letsencrypt/boulder@v0.20251208.0/sa/model_test.go (about)

     1  package sa
     2  
     3  import (
     4  	"context"
     5  	"crypto/ecdsa"
     6  	"crypto/elliptic"
     7  	"crypto/rand"
     8  	"crypto/x509"
     9  	"crypto/x509/pkix"
    10  	"database/sql"
    11  	"fmt"
    12  	"math/big"
    13  	"net/netip"
    14  	"slices"
    15  	"testing"
    16  	"time"
    17  
    18  	"github.com/jmhodges/clock"
    19  	"google.golang.org/protobuf/proto"
    20  	"google.golang.org/protobuf/types/known/timestamppb"
    21  
    22  	"github.com/letsencrypt/boulder/db"
    23  	"github.com/letsencrypt/boulder/grpc"
    24  	"github.com/letsencrypt/boulder/identifier"
    25  	"github.com/letsencrypt/boulder/probs"
    26  	sapb "github.com/letsencrypt/boulder/sa/proto"
    27  	"github.com/letsencrypt/boulder/test/vars"
    28  
    29  	"github.com/letsencrypt/boulder/core"
    30  	corepb "github.com/letsencrypt/boulder/core/proto"
    31  	"github.com/letsencrypt/boulder/test"
    32  )
    33  
    34  func TestRegistrationModelToPb(t *testing.T) {
    35  	badCases := []struct {
    36  		name  string
    37  		input regModel
    38  	}{
    39  		{
    40  			name:  "No ID",
    41  			input: regModel{ID: 0, Key: []byte("foo")},
    42  		},
    43  		{
    44  			name:  "No Key",
    45  			input: regModel{ID: 1, Key: nil},
    46  		},
    47  	}
    48  	for _, tc := range badCases {
    49  		t.Run(tc.name, func(t *testing.T) {
    50  			_, err := registrationModelToPb(&tc.input)
    51  			test.AssertError(t, err, "Should fail")
    52  		})
    53  	}
    54  
    55  	_, err := registrationModelToPb(&regModel{ID: 1, Key: []byte("foo")})
    56  	test.AssertNotError(t, err, "Should pass")
    57  }
    58  
    59  func TestAuthzModel(t *testing.T) {
    60  	// newTestAuthzPB returns a new *corepb.Authorization for `example.com` that
    61  	// is valid, and contains a single valid HTTP-01 challenge. These are the
    62  	// most common authorization attributes used in tests. Some tests will
    63  	// customize them after calling this.
    64  	newTestAuthzPB := func(validated time.Time) *corepb.Authorization {
    65  		return &corepb.Authorization{
    66  			Id:             "1",
    67  			Identifier:     identifier.NewDNS("example.com").ToProto(),
    68  			RegistrationID: 1,
    69  			Status:         string(core.StatusValid),
    70  			Expires:        timestamppb.New(validated.Add(24 * time.Hour)),
    71  			Challenges: []*corepb.Challenge{
    72  				{
    73  					Type:      string(core.ChallengeTypeHTTP01),
    74  					Status:    string(core.StatusValid),
    75  					Token:     "MTIz",
    76  					Validated: timestamppb.New(validated),
    77  					Validationrecords: []*corepb.ValidationRecord{
    78  						{
    79  							AddressUsed:       []byte("1.2.3.4"),
    80  							Url:               "https://example.com",
    81  							Hostname:          "example.com",
    82  							Port:              "443",
    83  							AddressesResolved: [][]byte{{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4}},
    84  							AddressesTried:    [][]byte{{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4}},
    85  						},
    86  					},
    87  				},
    88  			},
    89  		}
    90  	}
    91  
    92  	clk := clock.New()
    93  
    94  	authzPB := newTestAuthzPB(clk.Now())
    95  	authzPB.CertificateProfileName = "test"
    96  
    97  	model, err := authzPBToModel(authzPB)
    98  	test.AssertNotError(t, err, "authzPBToModel failed")
    99  
   100  	authzPBOut, err := modelToAuthzPB(*model)
   101  	test.AssertNotError(t, err, "modelToAuthzPB failed")
   102  	if authzPB.Challenges[0].Validationrecords[0].Hostname != "" {
   103  		test.Assert(t, false, fmt.Sprintf("dehydrated http-01 validation record expected hostname field to be missing, but found %v", authzPB.Challenges[0].Validationrecords[0].Hostname))
   104  	}
   105  	if authzPB.Challenges[0].Validationrecords[0].Port != "" {
   106  		test.Assert(t, false, fmt.Sprintf("rehydrated http-01 validation record expected port field to be missing, but found %v", authzPB.Challenges[0].Validationrecords[0].Port))
   107  	}
   108  	// Shoving the Hostname and Port back into the validation record should
   109  	// succeed because authzPB validation record should match the retrieved
   110  	// model from the database with the rehydrated Hostname and Port.
   111  	authzPB.Challenges[0].Validationrecords[0].Hostname = "example.com"
   112  	authzPB.Challenges[0].Validationrecords[0].Port = "443"
   113  	test.AssertDeepEquals(t, authzPB.Challenges, authzPBOut.Challenges)
   114  	test.AssertEquals(t, authzPBOut.CertificateProfileName, authzPB.CertificateProfileName)
   115  
   116  	authzPB = newTestAuthzPB(clk.Now())
   117  
   118  	validationErr := probs.Connection("weewoo")
   119  
   120  	authzPB.Challenges[0].Status = string(core.StatusInvalid)
   121  	authzPB.Challenges[0].Error, err = grpc.ProblemDetailsToPB(validationErr)
   122  	test.AssertNotError(t, err, "grpc.ProblemDetailsToPB failed")
   123  	model, err = authzPBToModel(authzPB)
   124  	test.AssertNotError(t, err, "authzPBToModel failed")
   125  
   126  	authzPBOut, err = modelToAuthzPB(*model)
   127  	test.AssertNotError(t, err, "modelToAuthzPB failed")
   128  	if authzPB.Challenges[0].Validationrecords[0].Hostname != "" {
   129  		test.Assert(t, false, fmt.Sprintf("dehydrated http-01 validation record expected hostname field to be missing, but found %v", authzPB.Challenges[0].Validationrecords[0].Hostname))
   130  	}
   131  	if authzPB.Challenges[0].Validationrecords[0].Port != "" {
   132  		test.Assert(t, false, fmt.Sprintf("rehydrated http-01 validation record expected port field to be missing, but found %v", authzPB.Challenges[0].Validationrecords[0].Port))
   133  	}
   134  	// Shoving the Hostname and Port back into the validation record should
   135  	// succeed because authzPB validation record should match the retrieved
   136  	// model from the database with the rehydrated Hostname and Port.
   137  	authzPB.Challenges[0].Validationrecords[0].Hostname = "example.com"
   138  	authzPB.Challenges[0].Validationrecords[0].Port = "443"
   139  	test.AssertDeepEquals(t, authzPB.Challenges, authzPBOut.Challenges)
   140  
   141  	authzPB = newTestAuthzPB(clk.Now())
   142  	authzPB.Status = string(core.StatusInvalid)
   143  	authzPB.Challenges = []*corepb.Challenge{
   144  		{
   145  			Type:   string(core.ChallengeTypeHTTP01),
   146  			Status: string(core.StatusInvalid),
   147  			Token:  "MTIz",
   148  			Validationrecords: []*corepb.ValidationRecord{
   149  				{
   150  					AddressUsed:       []byte("1.2.3.4"),
   151  					Url:               "url",
   152  					AddressesResolved: [][]byte{{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4}},
   153  					AddressesTried:    [][]byte{{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4}},
   154  				},
   155  			},
   156  		},
   157  		{
   158  			Type:   string(core.ChallengeTypeDNS01),
   159  			Status: string(core.StatusInvalid),
   160  			Token:  "MTIz",
   161  			Validationrecords: []*corepb.ValidationRecord{
   162  				{
   163  					AddressUsed:       []byte("1.2.3.4"),
   164  					Url:               "url",
   165  					AddressesResolved: [][]byte{{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4}},
   166  					AddressesTried:    [][]byte{{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4}},
   167  				},
   168  			},
   169  		},
   170  	}
   171  	_, err = authzPBToModel(authzPB)
   172  	test.AssertError(t, err, "authzPBToModel didn't fail with multiple non-pending challenges")
   173  
   174  	// Test that the caller Hostname and Port rehydration returns the expected
   175  	// data in the expected fields.
   176  	authzPB = newTestAuthzPB(clk.Now())
   177  
   178  	model, err = authzPBToModel(authzPB)
   179  	test.AssertNotError(t, err, "authzPBToModel failed")
   180  
   181  	authzPBOut, err = modelToAuthzPB(*model)
   182  	test.AssertNotError(t, err, "modelToAuthzPB failed")
   183  	if authzPBOut.Challenges[0].Validationrecords[0].Hostname != "example.com" {
   184  		test.Assert(t, false, fmt.Sprintf("rehydrated http-01 validation record expected hostname example.com but found %v", authzPBOut.Challenges[0].Validationrecords[0].Hostname))
   185  	}
   186  	if authzPBOut.Challenges[0].Validationrecords[0].Port != "443" {
   187  		test.Assert(t, false, fmt.Sprintf("rehydrated http-01 validation record expected port 443 but found %v", authzPBOut.Challenges[0].Validationrecords[0].Port))
   188  	}
   189  
   190  	authzPB = newTestAuthzPB(clk.Now())
   191  	authzPB.Identifier = identifier.NewIP(netip.MustParseAddr("1.2.3.4")).ToProto()
   192  	authzPB.Challenges[0].Validationrecords[0].Url = "https://1.2.3.4"
   193  	authzPB.Challenges[0].Validationrecords[0].Hostname = "1.2.3.4"
   194  
   195  	model, err = authzPBToModel(authzPB)
   196  	test.AssertNotError(t, err, "authzPBToModel failed")
   197  	authzPBOut, err = modelToAuthzPB(*model)
   198  	test.AssertNotError(t, err, "modelToAuthzPB failed")
   199  
   200  	identOut := identifier.FromProto(authzPBOut.Identifier)
   201  	if identOut.Type != identifier.TypeIP {
   202  		test.Assert(t, false, fmt.Sprintf("expected identifier type ip but found %s", identOut.Type))
   203  	}
   204  	if identOut.Value != "1.2.3.4" {
   205  		test.Assert(t, false, fmt.Sprintf("expected identifier value 1.2.3.4 but found %s", identOut.Value))
   206  	}
   207  
   208  	if authzPBOut.Challenges[0].Validationrecords[0].Hostname != "1.2.3.4" {
   209  		test.Assert(t, false, fmt.Sprintf("rehydrated http-01 validation record expected hostname 1.2.3.4 but found %v", authzPBOut.Challenges[0].Validationrecords[0].Hostname))
   210  	}
   211  	if authzPBOut.Challenges[0].Validationrecords[0].Port != "443" {
   212  		test.Assert(t, false, fmt.Sprintf("rehydrated http-01 validation record expected port 443 but found %v", authzPBOut.Challenges[0].Validationrecords[0].Port))
   213  	}
   214  }
   215  
   216  // TestModelToOrderBADJSON tests that converting an order model with an invalid
   217  // validation error JSON field to an Order produces the expected bad JSON error.
   218  func TestModelToOrderBadJSON(t *testing.T) {
   219  	badJSON := []byte(`{`)
   220  	_, err := modelToOrder(&orderModel{
   221  		Error: badJSON,
   222  	})
   223  	test.AssertError(t, err, "expected error from modelToOrderv2")
   224  	var badJSONErr errBadJSON
   225  	test.AssertErrorWraps(t, err, &badJSONErr)
   226  	test.AssertEquals(t, string(badJSONErr.json), string(badJSON))
   227  }
   228  
   229  // TestModelToOrderAuthzs tests that the Authzs field is properly decoded and
   230  // assigned to V2Authorizations.
   231  func TestModelToOrderAuthzs(t *testing.T) {
   232  	expectedAuthzIDs := []int64{1, 2, 3, 42}
   233  	encodedAuthzs, err := proto.Marshal(&sapb.Authzs{AuthzIDs: expectedAuthzIDs})
   234  	test.AssertNotError(t, err, "failed to marshal authzs")
   235  
   236  	testCases := []struct {
   237  		name             string
   238  		model            *orderModel
   239  		expectedAuthzIDs []int64
   240  	}{
   241  		{
   242  			name:             "with authzs",
   243  			model:            &orderModel{Authzs: encodedAuthzs},
   244  			expectedAuthzIDs: expectedAuthzIDs,
   245  		},
   246  		{
   247  			name:             "without authzs",
   248  			model:            &orderModel{},
   249  			expectedAuthzIDs: nil,
   250  		},
   251  	}
   252  	for _, tc := range testCases {
   253  		t.Run(tc.name, func(t *testing.T) {
   254  			order, err := modelToOrder(tc.model)
   255  			if err != nil {
   256  				t.Fatalf("modelToOrder(%v) = %s, want success", tc.model, err)
   257  			}
   258  			if !slices.Equal(order.V2Authorizations, tc.expectedAuthzIDs) {
   259  				t.Errorf("modelToOrder(%v) = %v, want %v", tc.model, order.V2Authorizations, tc.expectedAuthzIDs)
   260  			}
   261  		})
   262  	}
   263  }
   264  
   265  // TestPopulateAttemptedFieldsBadJSON tests that populating a challenge from an
   266  // authz2 model with an invalid validation error or an invalid validation record
   267  // produces the expected bad JSON error.
   268  func TestPopulateAttemptedFieldsBadJSON(t *testing.T) {
   269  	badJSON := []byte(`{`)
   270  
   271  	testCases := []struct {
   272  		Name  string
   273  		Model *authzModel
   274  	}{
   275  		{
   276  			Name: "Bad validation error field",
   277  			Model: &authzModel{
   278  				ValidationError: badJSON,
   279  			},
   280  		},
   281  		{
   282  			Name: "Bad validation record field",
   283  			Model: &authzModel{
   284  				ValidationRecord: badJSON,
   285  			},
   286  		},
   287  	}
   288  	for _, tc := range testCases {
   289  		t.Run(tc.Name, func(t *testing.T) {
   290  			err := populateAttemptedFields(*tc.Model, &corepb.Challenge{})
   291  			test.AssertError(t, err, "expected error from populateAttemptedFields")
   292  			var badJSONErr errBadJSON
   293  			test.AssertErrorWraps(t, err, &badJSONErr)
   294  			test.AssertEquals(t, string(badJSONErr.json), string(badJSON))
   295  		})
   296  	}
   297  }
   298  
   299  func TestCertificatesTableContainsDuplicateSerials(t *testing.T) {
   300  	ctx := context.Background()
   301  
   302  	sa, fc, cleanUp := initSA(t)
   303  	defer cleanUp()
   304  
   305  	serialString := core.SerialToString(big.NewInt(1337))
   306  
   307  	// Insert a certificate with a serial of `1337`.
   308  	err := insertCertificate(ctx, sa.dbMap, fc, "1337.com", "leet", 1337, 1)
   309  	test.AssertNotError(t, err, "couldn't insert valid certificate")
   310  
   311  	// This should return the certificate that we just inserted.
   312  	certA, err := SelectCertificate(ctx, sa.dbMap, serialString)
   313  	test.AssertNotError(t, err, "received an error for a valid query")
   314  
   315  	// Insert a certificate with a serial of `1337` but for a different
   316  	// hostname.
   317  	err = insertCertificate(ctx, sa.dbMap, fc, "1337.net", "leet", 1337, 1)
   318  	test.AssertNotError(t, err, "couldn't insert valid certificate")
   319  
   320  	// Despite a duplicate being present, this shouldn't error.
   321  	certB, err := SelectCertificate(ctx, sa.dbMap, serialString)
   322  	test.AssertNotError(t, err, "received an error for a valid query")
   323  
   324  	// Ensure that `certA` and `certB` are the same.
   325  	test.AssertByteEquals(t, certA.Der, certB.Der)
   326  }
   327  
   328  func insertCertificate(ctx context.Context, dbMap *db.WrappedMap, fc clock.FakeClock, hostname, cn string, serial, regID int64) error {
   329  	serialBigInt := big.NewInt(serial)
   330  	serialString := core.SerialToString(serialBigInt)
   331  
   332  	template := x509.Certificate{
   333  		Subject: pkix.Name{
   334  			CommonName: cn,
   335  		},
   336  		NotAfter:     fc.Now().Add(30 * 24 * time.Hour),
   337  		DNSNames:     []string{hostname},
   338  		SerialNumber: serialBigInt,
   339  	}
   340  
   341  	key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
   342  	if err != nil {
   343  		return fmt.Errorf("generating test key: %w", err)
   344  	}
   345  	certDer, err := x509.CreateCertificate(rand.Reader, &template, &template, key.Public(), key)
   346  	if err != nil {
   347  		return fmt.Errorf("generating test cert: %w", err)
   348  	}
   349  	cert := &core.Certificate{
   350  		RegistrationID: regID,
   351  		Issued:         fc.Now(),
   352  		Serial:         serialString,
   353  		Expires:        template.NotAfter,
   354  		DER:            certDer,
   355  	}
   356  	err = dbMap.Insert(ctx, cert)
   357  	if err != nil {
   358  		return err
   359  	}
   360  	return nil
   361  }
   362  
   363  func TestIncidentSerialModel(t *testing.T) {
   364  	ctx := context.Background()
   365  
   366  	testIncidentsDbMap, err := DBMapForTest(vars.DBConnIncidentsFullPerms)
   367  	test.AssertNotError(t, err, "Couldn't create test dbMap")
   368  	defer test.ResetIncidentsTestDatabase(t)
   369  
   370  	// Inserting and retrieving a row with only the serial populated should work.
   371  	_, err = testIncidentsDbMap.ExecContext(ctx,
   372  		"INSERT INTO incident_foo (serial) VALUES (?)",
   373  		"1337",
   374  	)
   375  	test.AssertNotError(t, err, "inserting row with only serial")
   376  
   377  	var res1 incidentSerialModel
   378  	err = testIncidentsDbMap.SelectOne(
   379  		ctx,
   380  		&res1,
   381  		"SELECT * FROM incident_foo WHERE serial = ?",
   382  		"1337",
   383  	)
   384  	test.AssertNotError(t, err, "selecting row with only serial")
   385  
   386  	test.AssertEquals(t, res1.Serial, "1337")
   387  	test.AssertBoxedNil(t, res1.RegistrationID, "registrationID should be NULL")
   388  	test.AssertBoxedNil(t, res1.OrderID, "orderID should be NULL")
   389  	test.AssertBoxedNil(t, res1.LastNoticeSent, "lastNoticeSent should be NULL")
   390  
   391  	// Inserting and retrieving a row with all columns populated should work.
   392  	_, err = testIncidentsDbMap.ExecContext(ctx,
   393  		"INSERT INTO incident_foo (serial, registrationID, orderID, lastNoticeSent) VALUES (?, ?, ?, ?)",
   394  		"1338",
   395  		1,
   396  		2,
   397  		time.Date(2023, 06, 29, 16, 9, 00, 00, time.UTC),
   398  	)
   399  	test.AssertNotError(t, err, "inserting row with only serial")
   400  
   401  	var res2 incidentSerialModel
   402  	err = testIncidentsDbMap.SelectOne(
   403  		ctx,
   404  		&res2,
   405  		"SELECT * FROM incident_foo WHERE serial = ?",
   406  		"1338",
   407  	)
   408  	test.AssertNotError(t, err, "selecting row with only serial")
   409  
   410  	test.AssertEquals(t, res2.Serial, "1338")
   411  	test.AssertEquals(t, *res2.RegistrationID, int64(1))
   412  	test.AssertEquals(t, *res2.OrderID, int64(2))
   413  	test.AssertEquals(t, *res2.LastNoticeSent, time.Date(2023, 06, 29, 16, 9, 00, 00, time.UTC))
   414  }
   415  
   416  func TestAddReplacementOrder(t *testing.T) {
   417  	sa, _, cleanUp := initSA(t)
   418  	defer cleanUp()
   419  
   420  	oldCertSerial := "1234567890"
   421  	orderId := int64(1337)
   422  	orderExpires := time.Now().Add(24 * time.Hour).UTC().Truncate(time.Second)
   423  
   424  	// Add a replacement order which doesn't exist.
   425  	err := addReplacementOrder(ctx, sa.dbMap, oldCertSerial, orderId, orderExpires)
   426  	test.AssertNotError(t, err, "addReplacementOrder failed")
   427  
   428  	// Fetch the replacement order so we can ensure it was added.
   429  	var replacementRow replacementOrderModel
   430  	err = sa.dbReadOnlyMap.SelectOne(
   431  		ctx,
   432  		&replacementRow,
   433  		"SELECT * FROM replacementOrders WHERE serial = ? LIMIT 1",
   434  		oldCertSerial,
   435  	)
   436  	test.AssertNotError(t, err, "SELECT from replacementOrders failed")
   437  	test.AssertEquals(t, oldCertSerial, replacementRow.Serial)
   438  	test.AssertEquals(t, orderId, replacementRow.OrderID)
   439  	test.AssertEquals(t, orderExpires, replacementRow.OrderExpires)
   440  
   441  	nextOrderId := int64(1338)
   442  	nextOrderExpires := time.Now().Add(48 * time.Hour).UTC().Truncate(time.Second)
   443  
   444  	// Add a replacement order which already exists.
   445  	err = addReplacementOrder(ctx, sa.dbMap, oldCertSerial, nextOrderId, nextOrderExpires)
   446  	test.AssertNotError(t, err, "addReplacementOrder failed")
   447  
   448  	// Fetch the replacement order so we can ensure it was updated.
   449  	err = sa.dbReadOnlyMap.SelectOne(
   450  		ctx,
   451  		&replacementRow,
   452  		"SELECT * FROM replacementOrders WHERE serial = ? LIMIT 1",
   453  		oldCertSerial,
   454  	)
   455  	test.AssertNotError(t, err, "SELECT from replacementOrders failed")
   456  	test.AssertEquals(t, oldCertSerial, replacementRow.Serial)
   457  	test.AssertEquals(t, nextOrderId, replacementRow.OrderID)
   458  	test.AssertEquals(t, nextOrderExpires, replacementRow.OrderExpires)
   459  }
   460  
   461  func TestSetReplacementOrderFinalized(t *testing.T) {
   462  	sa, _, cleanUp := initSA(t)
   463  	defer cleanUp()
   464  
   465  	oldCertSerial := "1234567890"
   466  	orderId := int64(1337)
   467  	orderExpires := time.Now().Add(24 * time.Hour).UTC().Truncate(time.Second)
   468  
   469  	// Mark a non-existent certificate as finalized/replaced.
   470  	err := setReplacementOrderFinalized(ctx, sa.dbMap, orderId)
   471  	test.AssertNotError(t, err, "setReplacementOrderFinalized failed")
   472  
   473  	// Ensure no replacement order was added for some reason.
   474  	var replacementRow replacementOrderModel
   475  	err = sa.dbReadOnlyMap.SelectOne(
   476  		ctx,
   477  		&replacementRow,
   478  		"SELECT * FROM replacementOrders WHERE serial = ? LIMIT 1",
   479  		oldCertSerial,
   480  	)
   481  	test.AssertErrorIs(t, err, sql.ErrNoRows)
   482  
   483  	// Add a replacement order.
   484  	err = addReplacementOrder(ctx, sa.dbMap, oldCertSerial, orderId, orderExpires)
   485  	test.AssertNotError(t, err, "addReplacementOrder failed")
   486  
   487  	// Mark the certificate as finalized/replaced.
   488  	err = setReplacementOrderFinalized(ctx, sa.dbMap, orderId)
   489  	test.AssertNotError(t, err, "setReplacementOrderFinalized failed")
   490  
   491  	// Fetch the replacement order so we can ensure it was finalized.
   492  	err = sa.dbReadOnlyMap.SelectOne(
   493  		ctx,
   494  		&replacementRow,
   495  		"SELECT * FROM replacementOrders WHERE serial = ? LIMIT 1",
   496  		oldCertSerial,
   497  	)
   498  	test.AssertNotError(t, err, "SELECT from replacementOrders failed")
   499  	test.Assert(t, replacementRow.Replaced, "replacement order should be marked as finalized")
   500  }