go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/tokenserver/appengine/impl/certchecker/certchecker_test.go (about)

     1  // Copyright 2016 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package certchecker
    16  
    17  import (
    18  	"context"
    19  	"crypto/rsa"
    20  	"crypto/x509"
    21  	"crypto/x509/pkix"
    22  	"math/big"
    23  	"testing"
    24  	"time"
    25  
    26  	"google.golang.org/protobuf/proto"
    27  
    28  	"go.chromium.org/luci/appengine/gaetesting"
    29  	"go.chromium.org/luci/common/clock"
    30  	"go.chromium.org/luci/common/clock/testclock"
    31  	"go.chromium.org/luci/common/data/rand/cryptorand"
    32  	"go.chromium.org/luci/gae/service/datastore"
    33  	"go.chromium.org/luci/tokenserver/api/admin/v1"
    34  	"go.chromium.org/luci/tokenserver/appengine/impl/certconfig"
    35  
    36  	. "github.com/smartystreets/goconvey/convey"
    37  	. "go.chromium.org/luci/common/testing/assertions"
    38  )
    39  
    40  func TestCertChecker(t *testing.T) {
    41  	Convey("CertChecker works", t, func() {
    42  		ctx := gaetesting.TestingContext()
    43  		ctx = cryptorand.MockForTest(ctx, 0)
    44  		ctx, clk := testclock.UseTime(ctx, testclock.TestTimeUTC)
    45  
    46  		// Generate new CA private key and certificate.
    47  		pkey, caCert, err := generateCA(ctx, "Some CA: ca-name.fake")
    48  		So(err, ShouldBeNil)
    49  
    50  		// Generate phony CA config.
    51  		configBlob, _ := proto.Marshal(&admin.CertificateAuthorityConfig{
    52  			UniqueId: 0,
    53  			Cn:       "Some CA: ca-name.fake",
    54  			CrlUrl:   "http://example.com",
    55  		})
    56  
    57  		// Nothing in the datastore yet.
    58  		checker, err := GetCertChecker(ctx, "Some CA: ca-name.fake")
    59  		So(err, ShouldNotBeNil)
    60  
    61  		// Put it into the datastore.
    62  		caEntity := certconfig.CA{
    63  			CN:     "Some CA: ca-name.fake",
    64  			Config: configBlob,
    65  			Cert:   caCert,
    66  			Ready:  true,
    67  		}
    68  		err = datastore.Put(ctx, &caEntity)
    69  		So(err, ShouldBeNil)
    70  
    71  		// In the datastore now.
    72  		checker, err = GetCertChecker(ctx, "Some CA: ca-name.fake")
    73  		So(err, ShouldBeNil)
    74  
    75  		// Update associated CRL (it's empty).
    76  		err = certconfig.UpdateCRLSet(ctx, "Some CA: ca-name.fake",
    77  			certconfig.CRLShardCount, &pkix.CertificateList{})
    78  		So(err, ShouldBeNil)
    79  
    80  		// Generate some certificate signed by the CA.
    81  		certDer, err := generateCert(ctx, 2, "some-cert-name.fake", caCert, pkey)
    82  		So(err, ShouldBeNil)
    83  
    84  		// Use CertChecker to check its validity. Need to parse DER first.
    85  		parsedCert, err := x509.ParseCertificate(certDer)
    86  		So(err, ShouldBeNil)
    87  		So(parsedCert.Issuer.CommonName, ShouldEqual, "Some CA: ca-name.fake")
    88  
    89  		// Valid!
    90  		ca, err := checker.CheckCertificate(ctx, parsedCert)
    91  		So(err, ShouldBeNil)
    92  		So(ca.CN, ShouldEqual, "Some CA: ca-name.fake")
    93  		So(ca.ParsedConfig, ShouldNotBeNil)
    94  
    95  		// Revoke the certificate by generating new CRL and putting it into the
    96  		// datastore.
    97  		err = certconfig.UpdateCRLSet(ctx, "Some CA: ca-name.fake", certconfig.CRLShardCount,
    98  			&pkix.CertificateList{
    99  				TBSCertList: pkix.TBSCertificateList{
   100  					RevokedCertificates: []pkix.RevokedCertificate{
   101  						{SerialNumber: big.NewInt(2)},
   102  					},
   103  				},
   104  			})
   105  		So(err, ShouldBeNil)
   106  
   107  		// Bump time to invalidate cert checker caches.
   108  		clk.Add(10 * time.Minute)
   109  
   110  		// Check same cert again. Should be rejected now as revoked.
   111  		_, err = checker.CheckCertificate(ctx, parsedCert)
   112  		So(err, ShouldErrLike, "certificate with SN 2 has been revoked")
   113  
   114  		// Fast forward past cert expiration time.
   115  		clk.Add(6 * time.Hour)
   116  
   117  		// Should be rejected as expired now.
   118  		_, err = checker.CheckCertificate(ctx, parsedCert)
   119  		So(err, ShouldErrLike, "certificate has expired")
   120  
   121  		// Generate a cert signed by a CA with the same name, but with different
   122  		// unexpected CA keys.
   123  		phonyCAKey, phonyCACert, err := generateCA(ctx, "Some CA: ca-name.fake")
   124  		So(err, ShouldBeNil)
   125  		certDer, err = generateCert(ctx, 3, "some-name", phonyCACert, phonyCAKey)
   126  		So(err, ShouldBeNil)
   127  
   128  		// CertChecker rejects it.
   129  		parsedCert, _ = x509.ParseCertificate(certDer)
   130  		_, err = checker.CheckCertificate(ctx, parsedCert)
   131  		So(err, ShouldErrLike, "crypto/rsa: verification error")
   132  	})
   133  }
   134  
   135  func generateCA(c context.Context, name string) (*rsa.PrivateKey, []byte, error) {
   136  	// See https://golang.org/src/crypto/tls/generate_cert.go.
   137  	rand := cryptorand.Get(c)
   138  	privKey, err := rsa.GenerateKey(rand, 512) // use short key in tests
   139  	if err != nil {
   140  		return nil, nil, err
   141  	}
   142  	template := x509.Certificate{
   143  		SerialNumber:          big.NewInt(1),
   144  		Subject:               pkix.Name{CommonName: name},
   145  		NotBefore:             clock.Now(c),
   146  		NotAfter:              clock.Now(c).Add(30 * time.Hour),
   147  		IsCA:                  true,
   148  		KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
   149  		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
   150  		BasicConstraintsValid: true,
   151  	}
   152  	derBytes, err := x509.CreateCertificate(rand, &template, &template, privKey.Public(), privKey)
   153  	if err != nil {
   154  		return nil, nil, err
   155  	}
   156  	return privKey, derBytes, nil
   157  }
   158  
   159  func generateCert(c context.Context, sn int64, name string, caCert []byte, caKey *rsa.PrivateKey) ([]byte, error) {
   160  	parent, err := x509.ParseCertificate(caCert)
   161  	if err != nil {
   162  		return nil, err
   163  	}
   164  	rand := cryptorand.Get(c)
   165  	privKey, err := rsa.GenerateKey(rand, 512) // use short key in tests
   166  	if err != nil {
   167  		return nil, err
   168  	}
   169  	template := x509.Certificate{
   170  		SerialNumber:          big.NewInt(sn),
   171  		Subject:               pkix.Name{CommonName: name},
   172  		NotBefore:             clock.Now(c),
   173  		NotAfter:              clock.Now(c).Add(5 * time.Hour),
   174  		KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
   175  		BasicConstraintsValid: true,
   176  	}
   177  	return x509.CreateCertificate(rand, &template, parent, privKey.Public(), caKey)
   178  }