package local import ( "bytes" "crypto/rand" "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "encoding/asn1" "encoding/hex" "encoding/pem" "io/ioutil" "math/big" "net" "net/http" "net/http/httptest" "reflect" "regexp" "sort" "strings" "testing" "time" "github.com/cloudflare/cfssl/config" "github.com/cloudflare/cfssl/csr" cferr "github.com/cloudflare/cfssl/errors" "github.com/cloudflare/cfssl/helpers" "github.com/cloudflare/cfssl/log" "github.com/cloudflare/cfssl/signer" "github.com/google/certificate-transparency-go" ) const ( fullSubjectCSR = "testdata/test.csr" testCSR = "testdata/ecdsa256.csr" testSANCSR = "testdata/san_domain.csr" testCaFile = "testdata/ca.pem" testCaKeyFile = "testdata/ca_key.pem" testECDSACaFile = "testdata/ecdsa256_ca.pem" testECDSACaKeyFile = "testdata/ecdsa256_ca_key.pem" ) var expiry = 1 * time.Minute // Start a signer with the testing RSA CA cert and key. func newTestSigner(t *testing.T) (s *Signer) { s, err := NewSignerFromFile(testCaFile, testCaKeyFile, nil) if err != nil { t.Fatal(err) } return } func TestNewSignerFromFilePolicy(t *testing.T) { var CAConfig = &config.Config{ Signing: &config.Signing{ Profiles: map[string]*config.SigningProfile{ "signature": { Usage: []string{"digital signature"}, Expiry: expiry, }, }, Default: &config.SigningProfile{ Usage: []string{"cert sign", "crl sign"}, ExpiryString: "43800h", Expiry: expiry, CAConstraint: config.CAConstraint{IsCA: true}, }, }, } _, err := NewSignerFromFile(testCaFile, testCaKeyFile, CAConfig.Signing) if err != nil { t.Fatal(err) } } func TestNewSignerFromFileInvalidPolicy(t *testing.T) { var invalidConfig = &config.Config{ Signing: &config.Signing{ Profiles: map[string]*config.SigningProfile{ "invalid": { Usage: []string{"wiretapping"}, Expiry: expiry, }, "empty": {}, }, Default: &config.SigningProfile{ Usage: []string{"digital signature"}, Expiry: expiry, }, }, } _, err := NewSignerFromFile(testCaFile, testCaKeyFile, invalidConfig.Signing) if err == nil { t.Fatal(err) } if !strings.Contains(err.Error(), `"code":5200`) { t.Fatal(err) } } func TestNewSignerFromFileNoUsageInPolicy(t *testing.T) { var invalidConfig = &config.Config{ Signing: &config.Signing{ Profiles: map[string]*config.SigningProfile{ "invalid": { Usage: []string{}, Expiry: expiry, }, "empty": {}, }, Default: &config.SigningProfile{ Usage: []string{"digital signature"}, Expiry: expiry, }, }, } _, err := NewSignerFromFile(testCaFile, testCaKeyFile, invalidConfig.Signing) if err == nil { t.Fatal("expect InvalidPolicy error") } if !strings.Contains(err.Error(), `"code":5200`) { t.Fatal(err) } } func TestNewSignerFromFileEdgeCases(t *testing.T) { res, err := NewSignerFromFile("nil", "nil", nil) if res != nil && err == nil { t.Fatal("Incorrect inputs failed to produce correct results") } res, err = NewSignerFromFile(testCaFile, "nil", nil) if res != nil && err == nil { t.Fatal("Incorrect inputs failed to produce correct results") } res, err = NewSignerFromFile("../../helpers/testdata/messedupcert.pem", "local.go", nil) if res != nil && err == nil { t.Fatal("Incorrect inputs failed to produce correct results") } res, err = NewSignerFromFile("../../helpers/testdata/cert.pem", "../../helpers/testdata/messed_up_priv_key.pem", nil) if res != nil && err == nil { t.Fatal("Incorrect inputs failed to produce correct results") } } func TestSign(t *testing.T) { s, err := NewSignerFromFile("testdata/ca.pem", "testdata/ca_key.pem", nil) if err != nil { t.Fatal("Failed to produce signer") } // test the empty request _, err = s.Sign(signer.SignRequest{}) if err == nil { t.Fatalf("Empty request failed to produce an error") } // not a csr certPem, err := ioutil.ReadFile("../../helpers/testdata/cert.pem") if err != nil { t.Fatal(err) } // csr with ip as hostname pem, err := ioutil.ReadFile("testdata/ip.csr") if err != nil { t.Fatal(err) } // improper request validReq := signer.SignRequest{Hosts: signer.SplitHosts(testHostName), Request: string(certPem)} _, err = s.Sign(validReq) if err == nil { t.Fatal("A bad case failed to raise an error") } validReq = signer.SignRequest{Hosts: signer.SplitHosts("128.84.126.213"), Request: string(pem)} _, err = s.Sign(validReq) if err != nil { t.Fatal("A bad case failed to raise an error") } pem, err = ioutil.ReadFile("testdata/ex.csr") validReq = signer.SignRequest{ Request: string(pem), Hosts: []string{"example.com"}, } s.Sign(validReq) if err != nil { t.Fatal("Failed to sign") } } func TestCertificate(t *testing.T) { s, err := NewSignerFromFile("testdata/ca.pem", "testdata/ca_key.pem", nil) if err != nil { t.Fatal(err) } c, err := s.Certificate("", "") if !reflect.DeepEqual(*c, *s.ca) || err != nil { t.Fatal("Certificate() producing incorrect results") } } func TestPolicy(t *testing.T) { s, err := NewSignerFromFile("testdata/ca.pem", "testdata/ca_key.pem", nil) if err != nil { t.Fatal(err) } sgn := config.Signing{} s.SetPolicy(&sgn) if s.Policy() != &sgn { t.Fatal("Policy is malfunctioning") } } func newCustomSigner(t *testing.T, testCaFile, testCaKeyFile string) (s *Signer) { s, err := NewSignerFromFile(testCaFile, testCaKeyFile, nil) if err != nil { t.Fatal(err) } return } func TestNewSignerFromFile(t *testing.T) { newTestSigner(t) } const ( testHostName = "localhost" ) func testSignFile(t *testing.T, certFile string) ([]byte, error) { s := newTestSigner(t) pem, err := ioutil.ReadFile(certFile) if err != nil { t.Fatal(err) } return s.Sign(signer.SignRequest{Hosts: signer.SplitHosts(testHostName), Request: string(pem)}) } type csrTest struct { file string keyAlgo string keyLen int // Error checking function errorCallback func(*testing.T, error) } // A helper function that returns a errorCallback function which expects an error. func ExpectError() func(*testing.T, error) { return func(t *testing.T, err error) { if err == nil { t.Fatal("Expected error. Got nothing.") } } } var csrTests = []csrTest{ { file: "testdata/rsa2048.csr", keyAlgo: "rsa", keyLen: 2048, errorCallback: nil, }, { file: "testdata/rsa3072.csr", keyAlgo: "rsa", keyLen: 3072, errorCallback: nil, }, { file: "testdata/rsa4096.csr", keyAlgo: "rsa", keyLen: 4096, errorCallback: nil, }, { file: "testdata/ecdsa256.csr", keyAlgo: "ecdsa", keyLen: 256, errorCallback: nil, }, { file: "testdata/ecdsa384.csr", keyAlgo: "ecdsa", keyLen: 384, errorCallback: nil, }, { file: "testdata/ecdsa521.csr", keyAlgo: "ecdsa", keyLen: 521, errorCallback: nil, }, { file: "testdata/rsa-old.csr", keyAlgo: "rsa", keyLen: 2048, errorCallback: nil, }, } func TestSignCSRs(t *testing.T) { s := newTestSigner(t) hostname := "cloudflare.com" for _, test := range csrTests { csr, err := ioutil.ReadFile(test.file) if err != nil { t.Fatal("CSR loading error:", err) } // It is possible to use different SHA2 algorithm with RSA CA key. rsaSigAlgos := []x509.SignatureAlgorithm{x509.SHA1WithRSA, x509.SHA256WithRSA, x509.SHA384WithRSA, x509.SHA512WithRSA} for _, sigAlgo := range rsaSigAlgos { s.sigAlgo = sigAlgo certBytes, err := s.Sign(signer.SignRequest{Hosts: signer.SplitHosts(hostname), Request: string(csr)}) if test.errorCallback != nil { test.errorCallback(t, err) } else { if err != nil { t.Fatalf("Expected no error. Got %s. Param %s %d", err.Error(), test.keyAlgo, test.keyLen) } cert, _ := helpers.ParseCertificatePEM(certBytes) if cert.SignatureAlgorithm != s.SigAlgo() { t.Fatal("Cert Signature Algorithm does not match the issuer.") } } } } } func TestECDSASigner(t *testing.T) { s := newCustomSigner(t, testECDSACaFile, testECDSACaKeyFile) hostname := "cloudflare.com" for _, test := range csrTests { csr, err := ioutil.ReadFile(test.file) if err != nil { t.Fatal("CSR loading error:", err) } // Try all ECDSA SignatureAlgorithm SigAlgos := []x509.SignatureAlgorithm{x509.ECDSAWithSHA1, x509.ECDSAWithSHA256, x509.ECDSAWithSHA384, x509.ECDSAWithSHA512} for _, sigAlgo := range SigAlgos { s.sigAlgo = sigAlgo certBytes, err := s.Sign(signer.SignRequest{Hosts: signer.SplitHosts(hostname), Request: string(csr)}) if test.errorCallback != nil { test.errorCallback(t, err) } else { if err != nil { t.Fatalf("Expected no error. Got %s. Param %s %d", err.Error(), test.keyAlgo, test.keyLen) } cert, _ := helpers.ParseCertificatePEM(certBytes) if cert.SignatureAlgorithm != s.SigAlgo() { t.Fatal("Cert Signature Algorithm does not match the issuer.") } } } } } const ( ecdsaInterCSR = "testdata/ecdsa256-inter.csr" ecdsaInterKey = "testdata/ecdsa256-inter.key" rsaInterCSR = "testdata/rsa2048-inter.csr" rsaInterKey = "testdata/rsa2048-inter.key" ) func TestCAIssuing(t *testing.T) { var caCerts = []string{testCaFile, testECDSACaFile} var caKeys = []string{testCaKeyFile, testECDSACaKeyFile} var interCSRs = []string{ecdsaInterCSR, rsaInterCSR} var interKeys = []string{ecdsaInterKey, rsaInterKey} var CAPolicy = &config.Signing{ Default: &config.SigningProfile{ Usage: []string{"cert sign", "crl sign"}, ExpiryString: "1h", Expiry: 1 * time.Hour, CAConstraint: config.CAConstraint{IsCA: true, MaxPathLenZero: true}, }, } var hostname = "cloudflare-inter.com" // Each RSA or ECDSA root CA issues two intermediate CAs (one ECDSA and one RSA). // For each intermediate CA, use it to issue additional RSA and ECDSA intermediate CSRs. for i, caFile := range caCerts { caKeyFile := caKeys[i] s := newCustomSigner(t, caFile, caKeyFile) s.policy = CAPolicy for j, csr := range interCSRs { csrBytes, _ := ioutil.ReadFile(csr) certBytes, err := s.Sign(signer.SignRequest{Hosts: signer.SplitHosts(hostname), Request: string(csrBytes)}) if err != nil { t.Fatal(err) } interCert, err := helpers.ParseCertificatePEM(certBytes) if err != nil { t.Fatal(err) } keyBytes, _ := ioutil.ReadFile(interKeys[j]) interKey, _ := helpers.ParsePrivateKeyPEM(keyBytes) interSigner := &Signer{ ca: interCert, priv: interKey, policy: CAPolicy, sigAlgo: signer.DefaultSigAlgo(interKey), } for _, anotherCSR := range interCSRs { anotherCSRBytes, _ := ioutil.ReadFile(anotherCSR) bytes, err := interSigner.Sign( signer.SignRequest{ Hosts: signer.SplitHosts(hostname), Request: string(anotherCSRBytes), }) if err != nil { t.Fatal(err) } cert, err := helpers.ParseCertificatePEM(bytes) if err != nil { t.Fatal(err) } if cert.SignatureAlgorithm != interSigner.SigAlgo() { t.Fatal("Cert Signature Algorithm does not match the issuer.") } if cert.MaxPathLen != 0 { t.Fatal("CA Cert Max Path is not zero.") } if cert.MaxPathLenZero != true { t.Fatal("CA Cert Max Path is not zero.") } } } } } func TestPopulateSubjectFromCSR(t *testing.T) { // a subject with all its fields full. fullSubject := &signer.Subject{ CN: "CN", Names: []csr.Name{ { C: "C", ST: "ST", L: "L", O: "O", OU: "OU", }, }, SerialNumber: "deadbeef", } fullName := pkix.Name{ CommonName: "CommonName", Country: []string{"Country"}, Province: []string{"Province"}, Organization: []string{"Organization"}, OrganizationalUnit: []string{"OrganizationalUnit"}, SerialNumber: "SerialNumber", } noCN := *fullSubject noCN.CN = "" name := PopulateSubjectFromCSR(&noCN, fullName) if name.CommonName != "CommonName" { t.Fatal("Failed to replace empty common name") } noC := *fullSubject noC.Names[0].C = "" name = PopulateSubjectFromCSR(&noC, fullName) if !reflect.DeepEqual(name.Country, fullName.Country) { t.Fatal("Failed to replace empty country") } noL := *fullSubject noL.Names[0].L = "" name = PopulateSubjectFromCSR(&noL, fullName) if !reflect.DeepEqual(name.Locality, fullName.Locality) { t.Fatal("Failed to replace empty locality") } noO := *fullSubject noO.Names[0].O = "" name = PopulateSubjectFromCSR(&noO, fullName) if !reflect.DeepEqual(name.Organization, fullName.Organization) { t.Fatal("Failed to replace empty organization") } noOU := *fullSubject noOU.Names[0].OU = "" name = PopulateSubjectFromCSR(&noOU, fullName) if !reflect.DeepEqual(name.OrganizationalUnit, fullName.OrganizationalUnit) { t.Fatal("Failed to replace empty organizational unit") } noSerial := *fullSubject noSerial.SerialNumber = "" name = PopulateSubjectFromCSR(&noSerial, fullName) if name.SerialNumber != fullName.SerialNumber { t.Fatalf("Failed to replace empty serial number: want %#v, got %#v", fullName.SerialNumber, name.SerialNumber) } } func TestOverrideSubject(t *testing.T) { csrPEM, err := ioutil.ReadFile(fullSubjectCSR) if err != nil { t.Fatalf("%v", err) } req := &signer.Subject{ Names: []csr.Name{ {O: "example.net"}, }, } s := newCustomSigner(t, testECDSACaFile, testECDSACaKeyFile) request := signer.SignRequest{ Hosts: []string{"127.0.0.1", "localhost", "xyz@example.com"}, Request: string(csrPEM), Subject: req, } certPEM, err := s.Sign(request) if err != nil { t.Fatalf("%v", err) } cert, err := helpers.ParseCertificatePEM(certPEM) if err != nil { t.Fatalf("%v", err) } block, _ := pem.Decode(csrPEM) template, err := x509.ParseCertificateRequest(block.Bytes) if err != nil { t.Fatal(err.Error()) } if cert.Subject.Organization[0] != "example.net" { t.Fatalf("Failed to override subject: want example.net but have %s", cert.Subject.Organization[0]) } if cert.Subject.Country[0] != template.Subject.Country[0] { t.Fatal("Failed to override Country") } if cert.Subject.Locality[0] != template.Subject.Locality[0] { t.Fatal("Failed to override Locality") } if cert.Subject.Organization[0] == template.Subject.Organization[0] { t.Fatal("Shouldn't have overrode Organization") } if cert.Subject.OrganizationalUnit[0] != template.Subject.OrganizationalUnit[0] { t.Fatal("Failed to override OrganizationalUnit") } log.Info("Overrode subject info") } func TestOverwriteHosts(t *testing.T) { for _, csrFile := range []string{testCSR, testSANCSR} { csrPEM, err := ioutil.ReadFile(csrFile) if err != nil { t.Fatal(err) } csrDER, _ := pem.Decode([]byte(csrPEM)) if err != nil { t.Fatal(err) } csr, err := x509.ParseCertificateRequest(csrDER.Bytes) if err != nil { t.Fatal(err) } csrHosts := csr.DNSNames for _, ip := range csr.IPAddresses { csrHosts = append(csrHosts, ip.String()) } sort.Strings(csrHosts) s := newCustomSigner(t, testECDSACaFile, testECDSACaKeyFile) for _, hosts := range [][]string{ nil, {}, {"127.0.0.1", "localhost", "xyz@example.com"}, } { request := signer.SignRequest{ Hosts: hosts, Request: string(csrPEM), Subject: nil, } certPEM, err := s.Sign(request) if err != nil { t.Fatalf("%v", err) } cert, err := helpers.ParseCertificatePEM(certPEM) if err != nil { t.Fatalf("%v", err) } // get the hosts, and add the ips and email addresses certHosts := cert.DNSNames for _, ip := range cert.IPAddresses { certHosts = append(certHosts, ip.String()) } for _, email := range cert.EmailAddresses { certHosts = append(certHosts, email) } // compare the sorted host lists sort.Strings(certHosts) sort.Strings(request.Hosts) if len(request.Hosts) > 0 && !reflect.DeepEqual(certHosts, request.Hosts) { t.Fatalf("Hosts not the same. cert hosts: %v, expected: %v", certHosts, request.Hosts) } if request.Hosts == nil && !reflect.DeepEqual(certHosts, csrHosts) { t.Fatalf("Hosts not the same. cert hosts: %v, expected csr hosts: %v", certHosts, csrHosts) } if request.Hosts != nil && len(request.Hosts) == 0 && len(certHosts) != 0 { t.Fatalf("Hosts not the same. cert hosts: %v, expected: %v", certHosts, request.Hosts) } } } } func TestOverrideValidity(t *testing.T) { csrPEM, err := ioutil.ReadFile(fullSubjectCSR) if err != nil { t.Fatalf("%v", err) } s := newCustomSigner(t, testECDSACaFile, testECDSACaKeyFile) req := signer.SignRequest{ Request: string(csrPEM), } // The default expiry value. expiry := 8760 * time.Hour // default case now := time.Now().UTC() certPEM, err := s.Sign(req) if err != nil { t.Fatalf("Error signing default request: %s", err) } cert, err := helpers.ParseCertificatePEM(certPEM) if err != nil { t.Fatalf("%v", err) } if !cert.NotBefore.After(now.Add(-10*time.Minute)) || !cert.NotBefore.Before(now.Add(10*time.Minute)) { t.Fatalf("Unexpected NotBefore: wanted %s +/-10 minutes, got %s", now, cert.NotBefore) } expectedNotAfter := now.Round(time.Minute).Add(expiry) if !cert.NotAfter.After(expectedNotAfter.Add(-10*time.Minute)) || !cert.NotAfter.Before(expectedNotAfter.Add(10*time.Minute)) { t.Fatalf("Unexpected NotAfter: wanted %s +/-10 minutes, got %s", now, cert.NotAfter) } // custom case, NotBefore only now = time.Now().UTC() req.NotBefore = now.Add(-time.Hour * 5).Truncate(time.Hour) req.NotAfter = time.Time{} certPEM, err = s.Sign(req) if err != nil { t.Fatalf("Error signing default request: %s", err) } cert, err = helpers.ParseCertificatePEM(certPEM) if err != nil { t.Fatalf("%v", err) } if !cert.NotBefore.Equal(req.NotBefore) { t.Fatalf("Unexpected NotBefore: wanted %s, got %s", req.NotBefore, cert.NotBefore) } expectedNotAfter = req.NotBefore.Add(expiry) if !cert.NotAfter.After(expectedNotAfter.Add(-10*time.Minute)) || !cert.NotAfter.Before(expectedNotAfter.Add(10*time.Minute)) { t.Fatalf("Unexpected NotAfter: wanted %s +/-10 minutes, got %s", expectedNotAfter, cert.NotAfter) } // custom case, NotAfter only now = time.Now().UTC() req.NotBefore = time.Time{} req.NotAfter = now.Add(-time.Hour * 5).Truncate(time.Hour) certPEM, err = s.Sign(req) if err != nil { t.Fatalf("Error signing default request: %s", err) } cert, err = helpers.ParseCertificatePEM(certPEM) if err != nil { t.Fatalf("%v", err) } if !cert.NotBefore.After(now.Add(-10*time.Minute)) || !cert.NotBefore.Before(now.Add(10*time.Minute)) { t.Fatalf("Unexpected NotBefore: wanted %s +/-10 minutes, got %s", now, cert.NotBefore) } if !cert.NotAfter.Equal(req.NotAfter) { t.Fatalf("Unexpected NotAfter: wanted %s, got %s", req.NotAfter, cert.NotAfter) } // custom case, NotBefore and NotAfter now = time.Now().UTC() req.NotBefore = now.Add(-time.Hour * 5).Truncate(time.Hour) req.NotAfter = now.Add(time.Hour * 5).Truncate(time.Hour) certPEM, err = s.Sign(req) if err != nil { t.Fatalf("Error signing default request: %s", err) } cert, err = helpers.ParseCertificatePEM(certPEM) if err != nil { t.Fatalf("%v", err) } if !cert.NotBefore.Equal(req.NotBefore) { t.Fatalf("Unexpected NotBefore: wanted %s, got %s", req.NotBefore, cert.NotBefore) } if !cert.NotAfter.Equal(req.NotAfter) { t.Fatalf("Unexpected NotAfter: wanted %s, got %s", req.NotAfter, cert.NotAfter) } } func expectOneValueOf(t *testing.T, s []string, e, n string) { if len(s) != 1 { t.Fatalf("Expected %s to have a single value, but it has %d values", n, len(s)) } if s[0] != e { t.Fatalf("Expected %s to be '%s', but it is '%s'", n, e, s[0]) } } func expectEmpty(t *testing.T, s []string, n string) { if len(s) != 0 { t.Fatalf("Expected no values in %s, but have %d values: %v", n, len(s), s) } } func TestCASignPathlen(t *testing.T) { var csrPathlenTests = []struct { name string caCertFile string caKeyFile string caProfile bool csrFile string err error pathlen int isZero bool isCA bool }{ { name: "pathlen 1 signing pathlen 0", caCertFile: testECDSACaFile, caKeyFile: testECDSACaKeyFile, caProfile: true, csrFile: "testdata/inter_pathlen_0.csr", err: nil, pathlen: 0, isZero: true, isCA: true, }, { name: "pathlen 1 signing pathlen 1", caCertFile: testECDSACaFile, caKeyFile: testECDSACaKeyFile, caProfile: true, csrFile: "testdata/inter_pathlen_1.csr", err: cferr.New(cferr.PolicyError, cferr.InvalidRequest), }, { name: "pathlen 0 signing pathlen 0", caCertFile: testCaFile, caKeyFile: testCaKeyFile, caProfile: true, csrFile: "testdata/inter_pathlen_0.csr", err: cferr.New(cferr.PolicyError, cferr.InvalidRequest), }, { name: "pathlen 0 signing pathlen 1", caCertFile: testCaFile, caKeyFile: testCaKeyFile, caProfile: true, csrFile: "testdata/inter_pathlen_1.csr", err: cferr.New(cferr.PolicyError, cferr.InvalidRequest), }, { name: "pathlen 0 signing pathlen unspecified", caCertFile: testCaFile, caKeyFile: testCaKeyFile, caProfile: true, csrFile: "testdata/inter_pathlen_unspecified.csr", err: cferr.New(cferr.PolicyError, cferr.InvalidRequest), }, { name: "pathlen 1 signing unspecified pathlen", caCertFile: testECDSACaFile, caKeyFile: testECDSACaKeyFile, caProfile: true, csrFile: "testdata/inter_pathlen_unspecified.csr", err: nil, // golang x509 parses unspecified pathlen as MaxPathLen == -1 and // MaxPathLenZero == false pathlen: -1, isZero: false, isCA: true, }, { name: "non-ca singing profile signing pathlen 0", caCertFile: testECDSACaFile, caKeyFile: testECDSACaKeyFile, caProfile: false, csrFile: "testdata/inter_pathlen_0.csr", err: cferr.New(cferr.PolicyError, cferr.InvalidRequest), }, { name: "non-ca singing profile signing pathlen 1", caCertFile: testECDSACaFile, caKeyFile: testECDSACaKeyFile, caProfile: false, csrFile: "testdata/inter_pathlen_1.csr", err: cferr.New(cferr.PolicyError, cferr.InvalidRequest), }, { name: "non-ca singing profile signing pathlen 0", caCertFile: testECDSACaFile, caKeyFile: testECDSACaKeyFile, caProfile: false, csrFile: "testdata/inter_pathlen_unspecified.csr", err: cferr.New(cferr.PolicyError, cferr.InvalidRequest), }, } for _, testCase := range csrPathlenTests { csrPEM, err := ioutil.ReadFile(testCase.csrFile) if err != nil { t.Fatalf("%v", err) } req := &signer.Subject{ Names: []csr.Name{ {O: "sam certificate authority"}, }, CN: "localhost", } s := newCustomSigner(t, testCase.caCertFile, testCase.caKeyFile) // No policy CSR whitelist: the normal set of CSR fields get passed through to // certificate. s.policy = &config.Signing{ Default: &config.SigningProfile{ Usage: []string{"cert sign", "crl sign"}, ExpiryString: "1h", Expiry: 1 * time.Hour, CAConstraint: config.CAConstraint{IsCA: testCase.caProfile, MaxPathLen: testCase.pathlen, MaxPathLenZero: testCase.isZero, }, }, } request := signer.SignRequest{ Hosts: []string{"127.0.0.1", "localhost"}, Request: string(csrPEM), Subject: req, } certPEM, err := s.Sign(request) if !reflect.DeepEqual(err, testCase.err) { t.Fatalf("%s: expected: %v, actual: %v", testCase.name, testCase.err, err) } if err == nil { cert, err := helpers.ParseCertificatePEM(certPEM) if err != nil { t.Fatalf("%s: %v", testCase.name, err) } if cert.IsCA != testCase.isCA { t.Fatalf("%s: unexpected IsCA value: %v", testCase.name, cert.IsCA) } if cert.MaxPathLen != testCase.pathlen { t.Fatalf("%s: unexpected pathlen value: %v", testCase.name, cert.MaxPathLen) } if cert.MaxPathLenZero != testCase.isZero { t.Fatalf("%s: unexpected pathlen value: %v", testCase.name, cert.MaxPathLenZero) } } } } func TestNoWhitelistSign(t *testing.T) { csrPEM, err := ioutil.ReadFile(fullSubjectCSR) if err != nil { t.Fatalf("%v", err) } req := &signer.Subject{ Names: []csr.Name{ {O: "sam certificate authority"}, }, CN: "localhost", } s := newCustomSigner(t, testECDSACaFile, testECDSACaKeyFile) // No policy CSR whitelist: the normal set of CSR fields get passed through to // certificate. s.policy = &config.Signing{ Default: &config.SigningProfile{ Usage: []string{"cert sign", "crl sign"}, ExpiryString: "1h", Expiry: 1 * time.Hour, CAConstraint: config.CAConstraint{IsCA: true}, }, } request := signer.SignRequest{ Hosts: []string{"127.0.0.1", "localhost"}, Request: string(csrPEM), Subject: req, } certPEM, err := s.Sign(request) if err != nil { t.Fatalf("%v", err) } cert, err := helpers.ParseCertificatePEM(certPEM) if err != nil { t.Fatalf("%v", err) } name := cert.Subject if name.CommonName != "localhost" { t.Fatalf("Expected certificate common name to be 'localhost' but have '%v'", name.CommonName) } // CSR has: Subject: C=US, O=CloudFlare, OU=WWW, L=Ithaca, ST=New York // Expect all to be passed through. expectOneValueOf(t, name.Organization, "sam certificate authority", "O") expectOneValueOf(t, name.OrganizationalUnit, "WWW", "OU") expectOneValueOf(t, name.Province, "New York", "ST") expectOneValueOf(t, name.Locality, "Ithaca", "L") expectOneValueOf(t, name.Country, "US", "C") } func TestWhitelistSign(t *testing.T) { csrPEM, err := ioutil.ReadFile(fullSubjectCSR) if err != nil { t.Fatalf("%v", err) } req := &signer.Subject{ Names: []csr.Name{ {O: "sam certificate authority"}, }, } s := newCustomSigner(t, testECDSACaFile, testECDSACaKeyFile) // Whitelist only key-related fields. Subject, DNSNames, etc shouldn't get // passed through from CSR. s.policy = &config.Signing{ Default: &config.SigningProfile{ Usage: []string{"cert sign", "crl sign"}, ExpiryString: "1h", Expiry: 1 * time.Hour, CAConstraint: config.CAConstraint{IsCA: true}, CSRWhitelist: &config.CSRWhitelist{ PublicKey: true, PublicKeyAlgorithm: true, SignatureAlgorithm: true, }, }, } request := signer.SignRequest{ Hosts: []string{"127.0.0.1", "localhost"}, Request: string(csrPEM), Subject: req, } certPEM, err := s.Sign(request) if err != nil { t.Fatalf("%v", err) } cert, err := helpers.ParseCertificatePEM(certPEM) if err != nil { t.Fatalf("%v", err) } name := cert.Subject if name.CommonName != "" { t.Fatalf("Expected empty certificate common name under policy without "+ "Subject whitelist, got %v", name.CommonName) } // O is provided by the signing API request, not the CSR, so it's allowed to // be copied into the certificate. expectOneValueOf(t, name.Organization, "sam certificate authority", "O") expectEmpty(t, name.OrganizationalUnit, "OU") expectEmpty(t, name.Province, "ST") expectEmpty(t, name.Locality, "L") expectEmpty(t, name.Country, "C") if cert.PublicKeyAlgorithm != x509.RSA { t.Fatalf("Expected public key algorithm to be RSA") } // Signature algorithm is allowed to be copied from CSR, but is overridden by // DefaultSigAlgo. if cert.SignatureAlgorithm != x509.ECDSAWithSHA256 { t.Fatalf("Expected public key algorithm to be ECDSAWithSHA256, got %v", cert.SignatureAlgorithm) } } func TestNameWhitelistSign(t *testing.T) { csrPEM, err := ioutil.ReadFile(fullSubjectCSR) if err != nil { t.Fatalf("%v", err) } subInvalid := &signer.Subject{ CN: "localhost.com", } subValid := &signer.Subject{ CN: "1lab41.cf", } wl := regexp.MustCompile("^1[a-z]*[0-9]*\\.cf$") s := newCustomSigner(t, testECDSACaFile, testECDSACaKeyFile) // Whitelist only key-related fields. Subject, DNSNames, etc shouldn't get // passed through from CSR. s.policy = &config.Signing{ Default: &config.SigningProfile{ Usage: []string{"cert sign", "crl sign"}, ExpiryString: "1h", Expiry: 1 * time.Hour, CAConstraint: config.CAConstraint{IsCA: true}, NameWhitelist: wl, }, } request := signer.SignRequest{ Hosts: []string{"127.0.0.1", "1machine23.cf"}, Request: string(csrPEM), } _, err = s.Sign(request) if err != nil { t.Fatalf("%v", err) } request = signer.SignRequest{ Hosts: []string{"invalid.cf", "1machine23.cf"}, Request: string(csrPEM), } _, err = s.Sign(request) if err == nil { t.Fatalf("expected a policy error") } request = signer.SignRequest{ Hosts: []string{"1machine23.cf"}, Request: string(csrPEM), Subject: subInvalid, } _, err = s.Sign(request) if err == nil { t.Fatalf("expected a policy error") } request = signer.SignRequest{ Hosts: []string{"1machine23.cf"}, Request: string(csrPEM), Subject: subValid, } _, err = s.Sign(request) if err != nil { t.Fatalf("%v", err) } } func TestExtensionSign(t *testing.T) { csrPEM, err := ioutil.ReadFile(testCSR) if err != nil { t.Fatalf("%v", err) } s := newCustomSigner(t, testECDSACaFile, testECDSACaKeyFile) // By default, no extensions should be allowed request := signer.SignRequest{ Request: string(csrPEM), Extensions: []signer.Extension{ {ID: config.OID(asn1.ObjectIdentifier{1, 2, 3, 4})}, }, } _, err = s.Sign(request) if err == nil { t.Fatalf("expected a policy error") } // Whitelist a specific extension. The extension with OID 1.2.3.4 should be // allowed through, but the one with OID 1.2.3.5 should not. s.policy = &config.Signing{ Default: &config.SigningProfile{ Usage: []string{"cert sign", "crl sign"}, ExpiryString: "1h", Expiry: 1 * time.Hour, CAConstraint: config.CAConstraint{IsCA: true}, ExtensionWhitelist: map[string]bool{"1.2.3.4": true}, }, } // Test that a forbidden extension triggers a sign error request = signer.SignRequest{ Request: string(csrPEM), Extensions: []signer.Extension{ {ID: config.OID(asn1.ObjectIdentifier{1, 2, 3, 5})}, }, } _, err = s.Sign(request) if err == nil { t.Fatalf("expected a policy error") } extValue := []byte{0x05, 0x00} extValueHex := hex.EncodeToString(extValue) // Test that an allowed extension makes it through request = signer.SignRequest{ Request: string(csrPEM), Extensions: []signer.Extension{ { ID: config.OID(asn1.ObjectIdentifier{1, 2, 3, 4}), Critical: false, Value: extValueHex, }, }, } certPEM, err := s.Sign(request) if err != nil { t.Fatalf("%v", err) } cert, err := helpers.ParseCertificatePEM(certPEM) if err != nil { t.Fatalf("%v", err) } foundAllowed := false for _, ext := range cert.Extensions { if ext.Id.String() == "1.2.3.4" { foundAllowed = true if ext.Critical { t.Fatalf("Extensions should not be marked critical") } if !bytes.Equal(extValue, ext.Value) { t.Fatalf("Extension has wrong value: %s != %s", hex.EncodeToString(ext.Value), extValueHex) } } } if !foundAllowed { t.Fatalf("Custom extension not included in the certificate") } } func TestCTFailure(t *testing.T) { // start a fake CT server that returns bad request ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(400) })) defer ts.Close() var config = &config.Signing{ Default: &config.SigningProfile{ Expiry: helpers.OneYear, CAConstraint: config.CAConstraint{IsCA: true}, Usage: []string{"signing", "key encipherment", "server auth", "client auth"}, ExpiryString: "8760h", CTLogServers: []string{ts.URL}, }, } testSigner, err := NewSignerFromFile(testCaFile, testCaKeyFile, config) if err != nil { t.Fatalf("%v", err) } var pem []byte pem, err = ioutil.ReadFile("testdata/ex.csr") if err != nil { t.Fatalf("%v", err) } validReq := signer.SignRequest{ Request: string(pem), Hosts: []string{"example.com"}, } _, err = testSigner.Sign(validReq) if err == nil { t.Fatal("Expected CT log submission failure") } } func TestCTSuccess(t *testing.T) { // start a fake CT server that will accept the submission ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(`{"sct_version":0,"id":"KHYaGJAn++880NYaAY12sFBXKcenQRvMvfYE9F1CYVM=","timestamp":1337,"extensions":"","signature":"BAMARjBEAiAIc21J5ZbdKZHw5wLxCP+MhBEsV5+nfvGyakOIv6FOvAIgWYMZb6Pw///uiNM7QTg2Of1OqmK1GbeGuEl9VJN8v8c="}`)) w.WriteHeader(200) })) defer ts.Close() var config = &config.Signing{ Default: &config.SigningProfile{ Expiry: helpers.OneYear, CAConstraint: config.CAConstraint{IsCA: true}, Usage: []string{"signing", "key encipherment", "server auth", "client auth"}, ExpiryString: "8760h", CTLogServers: []string{ts.URL}, }, } testSigner, err := NewSignerFromFile(testCaFile, testCaKeyFile, config) if err != nil { t.Fatalf("%v", err) } var pem []byte pem, err = ioutil.ReadFile("testdata/ex.csr") if err != nil { t.Fatalf("%v", err) } validReq := signer.SignRequest{ Request: string(pem), Hosts: []string{"example.com"}, } _, err = testSigner.Sign(validReq) if err != nil { t.Fatal("Expected CT log submission success") } } func TestReturnPrecert(t *testing.T) { var config = &config.Signing{ Default: &config.SigningProfile{ Expiry: helpers.OneYear, CAConstraint: config.CAConstraint{IsCA: true}, Usage: []string{"signing", "key encipherment", "server auth", "client auth"}, ExpiryString: "8760h", }, } testSigner, err := NewSignerFromFile(testCaFile, testCaKeyFile, config) if err != nil { t.Fatalf("%v", err) } csr, err := ioutil.ReadFile("testdata/ex.csr") if err != nil { t.Fatalf("%v", err) } validReq := signer.SignRequest{ Request: string(csr), Hosts: []string{"example.com"}, ReturnPrecert: true, } certBytes, err := testSigner.Sign(validReq) if err != nil { t.Fatal("Failed to sign request") } block, _ := pem.Decode(certBytes) cert, err := x509.ParseCertificate(block.Bytes) if err != nil { t.Fatalf("Failed to parse signed cert: %s", err) } // check cert with poison extension was returned poisoned := false for _, ext := range cert.Extensions { if ext.Id.Equal(signer.CTPoisonOID) { poisoned = true break } } if !poisoned { t.Fatal("Certificate without poison CT extension was returned") } } func TestSignFromPrecert(t *testing.T) { var config = &config.Signing{ Default: &config.SigningProfile{ Expiry: helpers.OneYear, CAConstraint: config.CAConstraint{IsCA: true}, Usage: []string{"signing", "key encipherment", "server auth", "client auth"}, ExpiryString: "8760h", }, } testSigner, err := NewSignerFromFile(testCaFile, testCaKeyFile, config) if err != nil { t.Fatalf("%v", err) } // Generate a precert k, err := rsa.GenerateKey(rand.Reader, 1024) if err != nil { t.Fatalf("Failed to generate test key: %s", err) } precertBytes, err := testSigner.sign(&x509.Certificate{ SignatureAlgorithm: x509.SHA512WithRSA, PublicKey: k.Public(), SerialNumber: big.NewInt(10), Subject: pkix.Name{CommonName: "CN"}, NotBefore: time.Now(), NotAfter: time.Now().Add(time.Hour), ExtraExtensions: []pkix.Extension{ {Id: signer.CTPoisonOID, Critical: true, Value: []byte{0x05, 0x00}}, }, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, SubjectKeyId: []byte{0, 1}, AuthorityKeyId: []byte{1, 0}, OCSPServer: []string{"ocsp?"}, IssuingCertificateURL: []string{"url"}, DNSNames: []string{"example.com"}, EmailAddresses: []string{"email@example.com"}, IPAddresses: []net.IP{net.ParseIP("1.1.1.1")}, CRLDistributionPoints: []string{"crl"}, PolicyIdentifiers: []asn1.ObjectIdentifier{{1, 2, 3}}, }) if err != nil { t.Fatalf("Failed to sign request: %s", err) } block, _ := pem.Decode(precertBytes) precert, err := x509.ParseCertificate(block.Bytes) if err != nil { t.Fatalf("Failed to parse signed cert: %s", err) } // Create a cert from the precert scts := []ct.SignedCertificateTimestamp{{}} certBytes, err := testSigner.SignFromPrecert(precert, scts) if err != nil { t.Fatalf("Failed to sign cert from precert: %s", err) } block, _ = pem.Decode(certBytes) cert, err := x509.ParseCertificate(block.Bytes) if err != nil { t.Fatalf("Failed to parse signed cert: %s", err) } // check cert doesn't contains poison extension poisoned := false for _, ext := range cert.Extensions { if ext.Id.Equal(signer.CTPoisonOID) { poisoned = true break } } if poisoned { t.Fatal("Certificate with poison CT extension was returned") } // check cert contains SCT list extension list := false for _, ext := range cert.Extensions { if ext.Id.Equal(signer.SCTListOID) { list = true break } } if !list { t.Fatal("Certificate without SCT list extension was returned") } // Break poison extension precert.Extensions[7].Value = []byte{1, 3, 3, 7} _, err = testSigner.SignFromPrecert(precert, scts) if err == nil { t.Fatal("SignFromPrecert didn't fail with invalid poison extension") } precert.Extensions[7].Critical = false _, err = testSigner.SignFromPrecert(precert, scts) if err == nil { t.Fatal("SignFromPrecert didn't fail with non-critical poison extension") } precert.Extensions = append(precert.Extensions[:7], precert.Extensions[8:]...) _, err = testSigner.SignFromPrecert(precert, scts) if err == nil { t.Fatal("SignFromPrecert didn't fail with missing poison extension") } precert.Signature = []byte("nop") _, err = testSigner.SignFromPrecert(precert, scts) if err == nil { t.Fatal("SignFromPrecert didn't fail with signature not from CA") } }