...

Source file src/github.com/letsencrypt/boulder/sa/model_test.go

Documentation: github.com/letsencrypt/boulder/sa

     1  package sa
     2  
     3  import (
     4  	"context"
     5  	"crypto/rand"
     6  	"crypto/rsa"
     7  	"crypto/x509"
     8  	"crypto/x509/pkix"
     9  	"encoding/base64"
    10  	"fmt"
    11  	"math/big"
    12  	"net"
    13  	"testing"
    14  	"time"
    15  
    16  	"github.com/jmhodges/clock"
    17  	"github.com/letsencrypt/boulder/db"
    18  	"github.com/letsencrypt/boulder/grpc"
    19  	"github.com/letsencrypt/boulder/probs"
    20  	"github.com/letsencrypt/boulder/test/vars"
    21  	"google.golang.org/protobuf/types/known/timestamppb"
    22  
    23  	"github.com/letsencrypt/boulder/core"
    24  	corepb "github.com/letsencrypt/boulder/core/proto"
    25  	"github.com/letsencrypt/boulder/test"
    26  )
    27  
    28  func TestRegistrationModelToPb(t *testing.T) {
    29  	badCases := []struct {
    30  		name  string
    31  		input regModel
    32  	}{
    33  		{
    34  			name:  "No ID",
    35  			input: regModel{ID: 0, Key: []byte("foo"), InitialIP: []byte("foo")},
    36  		},
    37  		{
    38  			name:  "No Key",
    39  			input: regModel{ID: 1, Key: nil, InitialIP: []byte("foo")},
    40  		},
    41  		{
    42  			name:  "No IP",
    43  			input: regModel{ID: 1, Key: []byte("foo"), InitialIP: nil},
    44  		},
    45  		{
    46  			name:  "Bad IP",
    47  			input: regModel{ID: 1, Key: []byte("foo"), InitialIP: []byte("foo")},
    48  		},
    49  	}
    50  	for _, tc := range badCases {
    51  		t.Run(tc.name, func(t *testing.T) {
    52  			_, err := registrationModelToPb(&tc.input)
    53  			test.AssertError(t, err, "Should fail")
    54  		})
    55  	}
    56  
    57  	_, err := registrationModelToPb(&regModel{
    58  		ID: 1, Key: []byte("foo"), InitialIP: net.ParseIP("1.2.3.4"),
    59  	})
    60  	test.AssertNotError(t, err, "Should pass")
    61  }
    62  
    63  func TestRegistrationPbToModel(t *testing.T) {}
    64  
    65  func TestAuthzModel(t *testing.T) {
    66  	clk := clock.New()
    67  	now := clk.Now()
    68  	expires := now.Add(24 * time.Hour)
    69  	authzPB := &corepb.Authorization{
    70  		Id:             "1",
    71  		Identifier:     "example.com",
    72  		RegistrationID: 1,
    73  		Status:         string(core.StatusValid),
    74  		ExpiresNS:      expires.UnixNano(),
    75  		Expires:        timestamppb.New(expires),
    76  		Challenges: []*corepb.Challenge{
    77  			{
    78  				Type:        string(core.ChallengeTypeHTTP01),
    79  				Status:      string(core.StatusValid),
    80  				Token:       "MTIz",
    81  				ValidatedNS: now.UnixNano(),
    82  				Validated:   timestamppb.New(now),
    83  				Validationrecords: []*corepb.ValidationRecord{
    84  					{
    85  						AddressUsed:       []byte("1.2.3.4"),
    86  						Url:               "https://example.com",
    87  						Hostname:          "example.com",
    88  						Port:              "443",
    89  						AddressesResolved: [][]byte{{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4}},
    90  						AddressesTried:    [][]byte{{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4}},
    91  					},
    92  				},
    93  			},
    94  		},
    95  	}
    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 backinto the validation record should
   109  	// succeed because authzPB validation record will 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  
   115  	now = clk.Now()
   116  	expires = now.Add(24 * time.Hour)
   117  	authzPB = &corepb.Authorization{
   118  		Id:             "1",
   119  		Identifier:     "example.com",
   120  		RegistrationID: 1,
   121  		Status:         string(core.StatusValid),
   122  		ExpiresNS:      expires.UnixNano(),
   123  		Expires:        timestamppb.New(expires),
   124  		Challenges: []*corepb.Challenge{
   125  			{
   126  				Type:        string(core.ChallengeTypeHTTP01),
   127  				Status:      string(core.StatusValid),
   128  				Token:       "MTIz",
   129  				ValidatedNS: now.UnixNano(),
   130  				Validated:   timestamppb.New(now),
   131  				Validationrecords: []*corepb.ValidationRecord{
   132  					{
   133  						AddressUsed:       []byte("1.2.3.4"),
   134  						Url:               "https://example.com",
   135  						Hostname:          "example.com",
   136  						Port:              "443",
   137  						AddressesResolved: [][]byte{{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4}},
   138  						AddressesTried:    [][]byte{{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4}},
   139  					},
   140  				},
   141  			},
   142  		},
   143  	}
   144  
   145  	validationErr := probs.Connection("weewoo")
   146  
   147  	authzPB.Challenges[0].Status = string(core.StatusInvalid)
   148  	authzPB.Challenges[0].Error, err = grpc.ProblemDetailsToPB(validationErr)
   149  	test.AssertNotError(t, err, "grpc.ProblemDetailsToPB failed")
   150  	model, err = authzPBToModel(authzPB)
   151  	test.AssertNotError(t, err, "authzPBToModel failed")
   152  
   153  	authzPBOut, err = modelToAuthzPB(*model)
   154  	test.AssertNotError(t, err, "modelToAuthzPB failed")
   155  	if authzPB.Challenges[0].Validationrecords[0].Hostname != "" {
   156  		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))
   157  	}
   158  	if authzPB.Challenges[0].Validationrecords[0].Port != "" {
   159  		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))
   160  	}
   161  	// Shoving the Hostname and Port back into the validation record should
   162  	// succeed because authzPB validation record will should match the retrieved
   163  	// model from the database with the rehydrated Hostname and Port.
   164  	authzPB.Challenges[0].Validationrecords[0].Hostname = "example.com"
   165  	authzPB.Challenges[0].Validationrecords[0].Port = "443"
   166  	test.AssertDeepEquals(t, authzPB.Challenges, authzPBOut.Challenges)
   167  
   168  	now = clk.Now()
   169  	expires = now.Add(24 * time.Hour)
   170  	authzPB = &corepb.Authorization{
   171  		Id:             "1",
   172  		Identifier:     "example.com",
   173  		RegistrationID: 1,
   174  		Status:         string(core.StatusInvalid),
   175  		ExpiresNS:      expires.UnixNano(),
   176  		Expires:        timestamppb.New(expires),
   177  		Challenges: []*corepb.Challenge{
   178  			{
   179  				Type:   string(core.ChallengeTypeHTTP01),
   180  				Status: string(core.StatusInvalid),
   181  				Token:  "MTIz",
   182  				Validationrecords: []*corepb.ValidationRecord{
   183  					{
   184  						AddressUsed:       []byte("1.2.3.4"),
   185  						Url:               "url",
   186  						AddressesResolved: [][]byte{{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4}},
   187  						AddressesTried:    [][]byte{{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4}},
   188  					},
   189  				},
   190  			},
   191  			{
   192  				Type:   string(core.ChallengeTypeDNS01),
   193  				Status: string(core.StatusInvalid),
   194  				Token:  "MTIz",
   195  				Validationrecords: []*corepb.ValidationRecord{
   196  					{
   197  						AddressUsed:       []byte("1.2.3.4"),
   198  						Url:               "url",
   199  						AddressesResolved: [][]byte{{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4}},
   200  						AddressesTried:    [][]byte{{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4}},
   201  					},
   202  				},
   203  			},
   204  		},
   205  	}
   206  	_, err = authzPBToModel(authzPB)
   207  	test.AssertError(t, err, "authzPBToModel didn't fail with multiple non-pending challenges")
   208  
   209  	// Test that the caller Hostname and Port rehydration returns the expected data in the expected fields.
   210  	now = clk.Now()
   211  	expires = now.Add(24 * time.Hour)
   212  	authzPB = &corepb.Authorization{
   213  		Id:             "1",
   214  		Identifier:     "example.com",
   215  		RegistrationID: 1,
   216  		Status:         string(core.StatusValid),
   217  		ExpiresNS:      expires.UnixNano(),
   218  		Expires:        timestamppb.New(expires),
   219  		Challenges: []*corepb.Challenge{
   220  			{
   221  				Type:        string(core.ChallengeTypeHTTP01),
   222  				Status:      string(core.StatusValid),
   223  				Token:       "MTIz",
   224  				ValidatedNS: now.UnixNano(),
   225  				Validated:   timestamppb.New(now),
   226  				Validationrecords: []*corepb.ValidationRecord{
   227  					{
   228  						AddressUsed:       []byte("1.2.3.4"),
   229  						Url:               "https://example.com",
   230  						AddressesResolved: [][]byte{{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4}},
   231  						AddressesTried:    [][]byte{{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4}},
   232  					},
   233  				},
   234  			},
   235  		},
   236  	}
   237  
   238  	model, err = authzPBToModel(authzPB)
   239  	test.AssertNotError(t, err, "authzPBToModel failed")
   240  
   241  	authzPBOut, err = modelToAuthzPB(*model)
   242  	test.AssertNotError(t, err, "modelToAuthzPB failed")
   243  	if authzPBOut.Challenges[0].Validationrecords[0].Hostname != "example.com" {
   244  		test.Assert(t, false, fmt.Sprintf("rehydrated http-01 validation record expected hostname example.com but found %v", authzPBOut.Challenges[0].Validationrecords[0].Hostname))
   245  	}
   246  	if authzPBOut.Challenges[0].Validationrecords[0].Port != "443" {
   247  		test.Assert(t, false, fmt.Sprintf("rehydrated http-01 validation record expected port 443 but found %v", authzPBOut.Challenges[0].Validationrecords[0].Port))
   248  	}
   249  }
   250  
   251  // TestModelToOrderBADJSON tests that converting an order model with an invalid
   252  // validation error JSON field to an Order produces the expected bad JSON error.
   253  func TestModelToOrderBadJSON(t *testing.T) {
   254  	badJSON := []byte(`{`)
   255  	_, err := modelToOrder(&orderModel{
   256  		Error: badJSON,
   257  	})
   258  	test.AssertError(t, err, "expected error from modelToOrder")
   259  	var badJSONErr errBadJSON
   260  	test.AssertErrorWraps(t, err, &badJSONErr)
   261  	test.AssertEquals(t, string(badJSONErr.json), string(badJSON))
   262  }
   263  
   264  // TestPopulateAttemptedFieldsBadJSON tests that populating a challenge from an
   265  // authz2 model with an invalid validation error or an invalid validation record
   266  // produces the expected bad JSON error.
   267  func TestPopulateAttemptedFieldsBadJSON(t *testing.T) {
   268  	badJSON := []byte(`{`)
   269  
   270  	testCases := []struct {
   271  		Name  string
   272  		Model *authzModel
   273  	}{
   274  		{
   275  			Name: "Bad validation error field",
   276  			Model: &authzModel{
   277  				ValidationError: badJSON,
   278  			},
   279  		},
   280  		{
   281  			Name: "Bad validation record field",
   282  			Model: &authzModel{
   283  				ValidationRecord: badJSON,
   284  			},
   285  		},
   286  	}
   287  	for _, tc := range testCases {
   288  		t.Run(tc.Name, func(t *testing.T) {
   289  			err := populateAttemptedFields(*tc.Model, &corepb.Challenge{})
   290  			test.AssertError(t, err, "expected error from populateAttemptedFields")
   291  			var badJSONErr errBadJSON
   292  			test.AssertErrorWraps(t, err, &badJSONErr)
   293  			test.AssertEquals(t, string(badJSONErr.json), string(badJSON))
   294  		})
   295  	}
   296  }
   297  
   298  func TestCertificatesTableContainsDuplicateSerials(t *testing.T) {
   299  	ctx := context.Background()
   300  
   301  	sa, fc, cleanUp := initSA(t)
   302  	defer cleanUp()
   303  
   304  	serialString := core.SerialToString(big.NewInt(1337))
   305  
   306  	// Insert a certificate with a serial of `1337`.
   307  	err := insertCertificate(ctx, sa.dbMap, fc, "1337.com", "leet", 1337, 1)
   308  	test.AssertNotError(t, err, "couldn't insert valid certificate")
   309  
   310  	// This should return the certificate that we just inserted.
   311  	certA, err := SelectCertificate(ctx, sa.dbMap, serialString)
   312  	test.AssertNotError(t, err, "received an error for a valid query")
   313  
   314  	// Insert a certificate with a serial of `1337` but for a different
   315  	// hostname.
   316  	err = insertCertificate(ctx, sa.dbMap, fc, "1337.net", "leet", 1337, 1)
   317  	test.AssertNotError(t, err, "couldn't insert valid certificate")
   318  
   319  	// Despite a duplicate being present, this shouldn't error.
   320  	certB, err := SelectCertificate(ctx, sa.dbMap, serialString)
   321  	test.AssertNotError(t, err, "received an error for a valid query")
   322  
   323  	// Ensure that `certA` and `certB` are the same.
   324  	test.AssertByteEquals(t, certA.DER, certB.DER)
   325  }
   326  
   327  func insertCertificate(ctx context.Context, dbMap *db.WrappedMap, fc clock.FakeClock, hostname, cn string, serial, regID int64) error {
   328  	serialBigInt := big.NewInt(serial)
   329  	serialString := core.SerialToString(serialBigInt)
   330  
   331  	template := x509.Certificate{
   332  		Subject: pkix.Name{
   333  			CommonName: cn,
   334  		},
   335  		NotAfter:     fc.Now().Add(30 * 24 * time.Hour),
   336  		DNSNames:     []string{hostname},
   337  		SerialNumber: serialBigInt,
   338  	}
   339  
   340  	testKey := makeKey()
   341  	certDer, _ := x509.CreateCertificate(rand.Reader, &template, &template, &testKey.PublicKey, &testKey)
   342  	cert := &core.Certificate{
   343  		RegistrationID: regID,
   344  		Serial:         serialString,
   345  		Expires:        template.NotAfter,
   346  		DER:            certDer,
   347  	}
   348  	err := dbMap.Insert(ctx, cert)
   349  	if err != nil {
   350  		return err
   351  	}
   352  	return nil
   353  }
   354  
   355  func bigIntFromB64(b64 string) *big.Int {
   356  	bytes, _ := base64.URLEncoding.DecodeString(b64)
   357  	x := big.NewInt(0)
   358  	x.SetBytes(bytes)
   359  	return x
   360  }
   361  
   362  func makeKey() rsa.PrivateKey {
   363  	n := bigIntFromB64("n4EPtAOCc9AlkeQHPzHStgAbgs7bTZLwUBZdR8_KuKPEHLd4rHVTeT-O-XV2jRojdNhxJWTDvNd7nqQ0VEiZQHz_AJmSCpMaJMRBSFKrKb2wqVwGU_NsYOYL-QtiWN2lbzcEe6XC0dApr5ydQLrHqkHHig3RBordaZ6Aj-oBHqFEHYpPe7Tpe-OfVfHd1E6cS6M1FZcD1NNLYD5lFHpPI9bTwJlsde3uhGqC0ZCuEHg8lhzwOHrtIQbS0FVbb9k3-tVTU4fg_3L_vniUFAKwuCLqKnS2BYwdq_mzSnbLY7h_qixoR7jig3__kRhuaxwUkRz5iaiQkqgc5gHdrNP5zw==")
   364  	e := int(bigIntFromB64("AQAB").Int64())
   365  	d := bigIntFromB64("bWUC9B-EFRIo8kpGfh0ZuyGPvMNKvYWNtB_ikiH9k20eT-O1q_I78eiZkpXxXQ0UTEs2LsNRS-8uJbvQ-A1irkwMSMkK1J3XTGgdrhCku9gRldY7sNA_AKZGh-Q661_42rINLRCe8W-nZ34ui_qOfkLnK9QWDDqpaIsA-bMwWWSDFu2MUBYwkHTMEzLYGqOe04noqeq1hExBTHBOBdkMXiuFhUq1BU6l-DqEiWxqg82sXt2h-LMnT3046AOYJoRioz75tSUQfGCshWTBnP5uDjd18kKhyv07lhfSJdrPdM5Plyl21hsFf4L_mHCuoFau7gdsPfHPxxjVOcOpBrQzwQ==")
   366  	p := bigIntFromB64("uKE2dh-cTf6ERF4k4e_jy78GfPYUIaUyoSSJuBzp3Cubk3OCqs6grT8bR_cu0Dm1MZwWmtdqDyI95HrUeq3MP15vMMON8lHTeZu2lmKvwqW7anV5UzhM1iZ7z4yMkuUwFWoBvyY898EXvRD-hdqRxHlSqAZ192zB3pVFJ0s7pFc=")
   367  	q := bigIntFromB64("uKE2dh-cTf6ERF4k4e_jy78GfPYUIaUyoSSJuBzp3Cubk3OCqs6grT8bR_cu0Dm1MZwWmtdqDyI95HrUeq3MP15vMMON8lHTeZu2lmKvwqW7anV5UzhM1iZ7z4yMkuUwFWoBvyY898EXvRD-hdqRxHlSqAZ192zB3pVFJ0s7pFc=")
   368  	return rsa.PrivateKey{PublicKey: rsa.PublicKey{N: n, E: e}, D: d, Primes: []*big.Int{p, q}}
   369  }
   370  
   371  func TestIncidentSerialModel(t *testing.T) {
   372  	ctx := context.Background()
   373  
   374  	testIncidentsDbMap, err := DBMapForTest(vars.DBConnIncidentsFullPerms)
   375  	test.AssertNotError(t, err, "Couldn't create test dbMap")
   376  	defer test.ResetIncidentsTestDatabase(t)
   377  
   378  	// Inserting and retrieving a row with only the serial populated should work.
   379  	_, err = testIncidentsDbMap.ExecContext(ctx,
   380  		"INSERT INTO incident_foo (serial) VALUES (?)",
   381  		"1337",
   382  	)
   383  	test.AssertNotError(t, err, "inserting row with only serial")
   384  
   385  	var res1 incidentSerialModel
   386  	err = testIncidentsDbMap.SelectOne(
   387  		ctx,
   388  		&res1,
   389  		"SELECT * FROM incident_foo WHERE serial = ?",
   390  		"1337",
   391  	)
   392  	test.AssertNotError(t, err, "selecting row with only serial")
   393  
   394  	test.AssertEquals(t, res1.Serial, "1337")
   395  	test.AssertBoxedNil(t, res1.RegistrationID, "registrationID should be NULL")
   396  	test.AssertBoxedNil(t, res1.OrderID, "orderID should be NULL")
   397  	test.AssertBoxedNil(t, res1.LastNoticeSent, "lastNoticeSent should be NULL")
   398  
   399  	// Inserting and retrieving a row with all columns populated should work.
   400  	_, err = testIncidentsDbMap.ExecContext(ctx,
   401  		"INSERT INTO incident_foo (serial, registrationID, orderID, lastNoticeSent) VALUES (?, ?, ?, ?)",
   402  		"1338",
   403  		1,
   404  		2,
   405  		time.Date(2023, 06, 29, 16, 9, 00, 00, time.UTC),
   406  	)
   407  	test.AssertNotError(t, err, "inserting row with only serial")
   408  
   409  	var res2 incidentSerialModel
   410  	err = testIncidentsDbMap.SelectOne(
   411  		ctx,
   412  		&res2,
   413  		"SELECT * FROM incident_foo WHERE serial = ?",
   414  		"1338",
   415  	)
   416  	test.AssertNotError(t, err, "selecting row with only serial")
   417  
   418  	test.AssertEquals(t, res2.Serial, "1338")
   419  	test.AssertEquals(t, *res2.RegistrationID, int64(1))
   420  	test.AssertEquals(t, *res2.OrderID, int64(2))
   421  	test.AssertEquals(t, *res2.LastNoticeSent, time.Date(2023, 06, 29, 16, 9, 00, 00, time.UTC))
   422  }
   423  

View as plain text