diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 2bb9bdc24..fbe1e58f0 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -12,51 +12,51 @@ }, { "ImportPath": "github.com/cloudflare/cfssl/auth", - "Rev": "6f428f654df58d23d1321bcbe3598f6b8a02167a" + "Rev": "fd649feb9eb317e3ed24afee0d92c0ea55bf4a33" }, { "ImportPath": "github.com/cloudflare/cfssl/config", - "Rev": "6f428f654df58d23d1321bcbe3598f6b8a02167a" + "Rev": "fd649feb9eb317e3ed24afee0d92c0ea55bf4a33" }, { "ImportPath": "github.com/cloudflare/cfssl/crypto/pkcs11key", - "Rev": "6f428f654df58d23d1321bcbe3598f6b8a02167a" + "Rev": "fd649feb9eb317e3ed24afee0d92c0ea55bf4a33" }, { "ImportPath": "github.com/cloudflare/cfssl/crypto/pkcs12", - "Rev": "6f428f654df58d23d1321bcbe3598f6b8a02167a" + "Rev": "fd649feb9eb317e3ed24afee0d92c0ea55bf4a33" }, { "ImportPath": "github.com/cloudflare/cfssl/crypto/pkcs7", - "Rev": "6f428f654df58d23d1321bcbe3598f6b8a02167a" + "Rev": "fd649feb9eb317e3ed24afee0d92c0ea55bf4a33" }, { "ImportPath": "github.com/cloudflare/cfssl/csr", - "Rev": "6f428f654df58d23d1321bcbe3598f6b8a02167a" + "Rev": "fd649feb9eb317e3ed24afee0d92c0ea55bf4a33" }, { "ImportPath": "github.com/cloudflare/cfssl/errors", - "Rev": "6f428f654df58d23d1321bcbe3598f6b8a02167a" + "Rev": "fd649feb9eb317e3ed24afee0d92c0ea55bf4a33" }, { "ImportPath": "github.com/cloudflare/cfssl/helpers", - "Rev": "6f428f654df58d23d1321bcbe3598f6b8a02167a" + "Rev": "fd649feb9eb317e3ed24afee0d92c0ea55bf4a33" }, { "ImportPath": "github.com/cloudflare/cfssl/info", - "Rev": "6f428f654df58d23d1321bcbe3598f6b8a02167a" + "Rev": "fd649feb9eb317e3ed24afee0d92c0ea55bf4a33" }, { "ImportPath": "github.com/cloudflare/cfssl/log", - "Rev": "6f428f654df58d23d1321bcbe3598f6b8a02167a" + "Rev": "fd649feb9eb317e3ed24afee0d92c0ea55bf4a33" }, { "ImportPath": "github.com/cloudflare/cfssl/ocsp", - "Rev": "6f428f654df58d23d1321bcbe3598f6b8a02167a" + "Rev": "fd649feb9eb317e3ed24afee0d92c0ea55bf4a33" }, { "ImportPath": "github.com/cloudflare/cfssl/signer", - "Rev": "6f428f654df58d23d1321bcbe3598f6b8a02167a" + "Rev": "fd649feb9eb317e3ed24afee0d92c0ea55bf4a33" }, { "ImportPath": "github.com/codegangsta/cli", diff --git a/Godeps/_workspace/src/github.com/cloudflare/cfssl/config/config.go b/Godeps/_workspace/src/github.com/cloudflare/cfssl/config/config.go index 7c020fb28..e79622c31 100644 --- a/Godeps/_workspace/src/github.com/cloudflare/cfssl/config/config.go +++ b/Godeps/_workspace/src/github.com/cloudflare/cfssl/config/config.go @@ -38,38 +38,46 @@ type CSRWhitelist struct { // JSON marshal / unmarshal. type OID asn1.ObjectIdentifier -// CertificatePolicy is a flattening of the ASN.1 PolicyInformation structure from +// CertificatePolicy represents the ASN.1 PolicyInformation structure from // https://tools.ietf.org/html/rfc3280.html#page-106. // Valid values of Type are "id-qt-unotice" and "id-qt-cps" type CertificatePolicy struct { - ID OID - Type string - Qualifier string + ID OID + Qualifiers []CertificatePolicyQualifier +} + +// CertificatePolicyQualifier represents a single qualifier from an ASN.1 +// PolicyInformation structure. +type CertificatePolicyQualifier struct { + Type string + Value string } // A SigningProfile stores information that the CA needs to store // signature policy. type SigningProfile struct { - Usage []string `json:"usages"` - IssuerURL []string `json:"issuer_urls"` - OCSP string `json:"ocsp_url"` - CRL string `json:"crl_url"` - CA bool `json:"is_ca"` - OCSPNoCheck bool `json:"ocsp_no_check"` - ExpiryString string `json:"expiry"` - BackdateString string `json:"backdate"` - AuthKeyName string `json:"auth_key"` - RemoteName string `json:"remote"` - NotBefore time.Time `json:"not_before"` - NotAfter time.Time `json:"not_after"` + Usage []string `json:"usages"` + IssuerURL []string `json:"issuer_urls"` + OCSP string `json:"ocsp_url"` + CRL string `json:"crl_url"` + CA bool `json:"is_ca"` + OCSPNoCheck bool `json:"ocsp_no_check"` + ExpiryString string `json:"expiry"` + BackdateString string `json:"backdate"` + AuthKeyName string `json:"auth_key"` + RemoteName string `json:"remote"` + NotBefore time.Time `json:"not_before"` + NotAfter time.Time `json:"not_after"` + NameWhitelistString string `json:"name_whitelist"` - Policies []CertificatePolicy - Expiry time.Duration - Backdate time.Duration - Provider auth.Provider - RemoteServer string - UseSerialSeq bool - CSRWhitelist *CSRWhitelist + Policies []CertificatePolicy + Expiry time.Duration + Backdate time.Duration + Provider auth.Provider + RemoteServer string + UseSerialSeq bool + CSRWhitelist *CSRWhitelist + NameWhitelist *regexp.Regexp } // UnmarshalJSON unmarshals a JSON string into an OID. @@ -162,8 +170,11 @@ func (p *SigningProfile) populate(cfg *Config) error { if len(p.Policies) > 0 { for _, policy := range p.Policies { - if policy.Type != "" && policy.Type != "id-qt-unotice" && policy.Type != "id-qt-cps" { - return cferr.Wrap(cferr.PolicyError, cferr.InvalidPolicy, err) + for _, qualifier := range policy.Qualifiers { + if qualifier.Type != "" && qualifier.Type != "id-qt-unotice" && qualifier.Type != "id-qt-cps" { + return cferr.Wrap(cferr.PolicyError, cferr.InvalidPolicy, + errors.New("invalid policy qualifier type")) + } } } } @@ -200,6 +211,16 @@ func (p *SigningProfile) populate(cfg *Config) error { } } + if p.NameWhitelistString != "" { + log.Debug("compiling whitelist regular expression") + rule, err := regexp.Compile(p.NameWhitelistString) + if err != nil { + return cferr.Wrap(cferr.PolicyError, cferr.InvalidPolicy, + errors.New("failed to compile name whitelist section")) + } + p.NameWhitelist = rule + } + return nil } diff --git a/Godeps/_workspace/src/github.com/cloudflare/cfssl/signer/local/local.go b/Godeps/_workspace/src/github.com/cloudflare/cfssl/signer/local/local.go index 9f569b5e9..609d909fd 100644 --- a/Godeps/_workspace/src/github.com/cloudflare/cfssl/signer/local/local.go +++ b/Godeps/_workspace/src/github.com/cloudflare/cfssl/signer/local/local.go @@ -225,6 +225,20 @@ func (s *Signer) Sign(req signer.SignRequest) (cert []byte, err error) { OverrideHosts(&safeTemplate, req.Hosts) safeTemplate.Subject = PopulateSubjectFromCSR(req.Subject, safeTemplate.Subject) + // If there is a whitelist, ensure that both the Common Name and SAN DNSNames match + if profile.NameWhitelist != nil { + if safeTemplate.Subject.CommonName != "" { + if profile.NameWhitelist.Find([]byte(safeTemplate.Subject.CommonName)) == nil { + return nil, cferr.New(cferr.PolicyError, cferr.InvalidPolicy) + } + } + for _, name := range safeTemplate.DNSNames { + if profile.NameWhitelist.Find([]byte(name)) == nil { + return nil, cferr.New(cferr.PolicyError, cferr.InvalidPolicy) + } + } + } + return s.sign(&safeTemplate, profile, serialSeq) } diff --git a/Godeps/_workspace/src/github.com/cloudflare/cfssl/signer/local/local_test.go b/Godeps/_workspace/src/github.com/cloudflare/cfssl/signer/local/local_test.go index e71ae3b43..d7d09ee53 100644 --- a/Godeps/_workspace/src/github.com/cloudflare/cfssl/signer/local/local_test.go +++ b/Godeps/_workspace/src/github.com/cloudflare/cfssl/signer/local/local_test.go @@ -6,6 +6,7 @@ import ( "encoding/pem" "io/ioutil" "reflect" + "regexp" "sort" "strings" "testing" @@ -807,3 +808,75 @@ func TestWhitelistSign(t *testing.T) { 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, + CA: 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) + } + +} diff --git a/Godeps/_workspace/src/github.com/cloudflare/cfssl/signer/remote/remote.go b/Godeps/_workspace/src/github.com/cloudflare/cfssl/signer/remote/remote.go index d4f3e66be..3a749a5c9 100644 --- a/Godeps/_workspace/src/github.com/cloudflare/cfssl/signer/remote/remote.go +++ b/Godeps/_workspace/src/github.com/cloudflare/cfssl/signer/remote/remote.go @@ -83,9 +83,9 @@ func (s *Signer) remoteOp(req interface{}, profile, target string) (resp interfa if target == "info" { resp, err = server.Info(jsonData) } else if p.Provider != nil { - resp, err = server.AuthReq(jsonData, nil, p.Provider, target) + resp, err = server.AuthSign(jsonData, nil, p.Provider) } else { - resp, err = server.Req(jsonData, target) + resp, err = server.Sign(jsonData) } if err != nil { diff --git a/Godeps/_workspace/src/github.com/cloudflare/cfssl/signer/signer.go b/Godeps/_workspace/src/github.com/cloudflare/cfssl/signer/signer.go index 532bcda08..f2de75d56 100644 --- a/Godeps/_workspace/src/github.com/cloudflare/cfssl/signer/signer.go +++ b/Godeps/_workspace/src/github.com/cloudflare/cfssl/signer/signer.go @@ -27,19 +27,11 @@ import ( // MaxPathLen is the default path length for a new CA certificate. var MaxPathLen = 2 -// A Whitelist marks which fields should be set. As a bool's default -// value is false, a whitelist should only keep those fields marked -// true. -type Whitelist struct { - CN, C, ST, L, O, OU bool -} - // Subject contains the information that should be used to override the // subject information when signing a certificate. type Subject struct { - CN string - Names []csr.Name `json:"names"` - Whitelist *Whitelist `json:"whitelist,omitempty"` + CN string + Names []csr.Name `json:"names"` } // SignRequest stores a signature request, which contains the hostname, @@ -351,13 +343,26 @@ func FillTemplate(template *x509.Certificate, defaultProfile, profile *config.Si return nil } -type policyQualifier struct { +type policyInformation struct { + PolicyIdentifier asn1.ObjectIdentifier + Qualifiers []interface{} + CPSPolicyQualifiers []cpsPolicyQualifier `asn1:"omitempty"` + // User Notice policy qualifiers have a slightly different ASN.1 structure + // from that used for CPS policy qualifiers. + UserNoticePolicyQualifiers []userNoticePolicyQualifier `asn1:"omitempty"` +} + +type cpsPolicyQualifier struct { PolicyQualifierID asn1.ObjectIdentifier Qualifier string `asn1:"tag:optional,ia5"` } -type policyInformation struct { - PolicyIdentifier asn1.ObjectIdentifier - PolicyQualifiers []policyQualifier `asn1:"omitempty"` + +type userNotice struct { + ExplicitText string `asn1:"tag:optional,utf8"` +} +type userNoticePolicyQualifier struct { + PolicyQualifierID asn1.ObjectIdentifier + Qualifier userNotice } var ( @@ -382,26 +387,25 @@ func addPolicies(template *x509.Certificate, policies []config.CertificatePolicy // The PolicyIdentifier is an OID assigned to a given issuer. PolicyIdentifier: asn1.ObjectIdentifier(policy.ID), } - switch policy.Type { - case "id-qt-unotice": - pi.PolicyQualifiers = []policyQualifier{ - policyQualifier{ - PolicyQualifierID: iDQTUserNotice, - Qualifier: policy.Qualifier, - }, + for _, qualifier := range policy.Qualifiers { + switch qualifier.Type { + case "id-qt-unotice": + pi.Qualifiers = append(pi.Qualifiers, + userNoticePolicyQualifier{ + PolicyQualifierID: iDQTUserNotice, + Qualifier: userNotice{ + ExplicitText: qualifier.Value, + }, + }) + case "id-qt-cps": + pi.Qualifiers = append(pi.Qualifiers, + cpsPolicyQualifier{ + PolicyQualifierID: iDQTCertificationPracticeStatement, + Qualifier: qualifier.Value, + }) + default: + return errors.New("Invalid qualifier type in Policies " + qualifier.Type) } - case "id-qt-cps": - pi.PolicyQualifiers = []policyQualifier{ - policyQualifier{ - PolicyQualifierID: iDQTCertificationPracticeStatement, - Qualifier: policy.Qualifier, - }, - } - case "": - // Empty qualifier type is fine: Include this Certificate Policy, but - // don't include a Policy Qualifier. - default: - return errors.New("Invalid qualifier type in Policies " + policy.Type) } asn1PolicyList = append(asn1PolicyList, pi) } diff --git a/Godeps/_workspace/src/github.com/cloudflare/cfssl/signer/signer_test.go b/Godeps/_workspace/src/github.com/cloudflare/cfssl/signer/signer_test.go index c34668597..c58dc398b 100644 --- a/Godeps/_workspace/src/github.com/cloudflare/cfssl/signer/signer_test.go +++ b/Godeps/_workspace/src/github.com/cloudflare/cfssl/signer/signer_test.go @@ -1,7 +1,15 @@ package signer import ( + "bytes" + "crypto/x509" + "encoding/asn1" + "encoding/hex" + "fmt" + "reflect" "testing" + + "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cloudflare/cfssl/config" ) func TestSplitHosts(t *testing.T) { @@ -23,3 +31,63 @@ func TestSplitHosts(t *testing.T) { t.Fatal("SplitHost fails to split multiple domains") } } + +func TestAddPolicies(t *testing.T) { + var cert x509.Certificate + addPolicies(&cert, []config.CertificatePolicy{ + config.CertificatePolicy{ + ID: config.OID{1, 2, 3, 4}, + }, + }) + + if len(cert.ExtraExtensions) != 1 { + t.Fatal("No extension added") + } + ext := cert.ExtraExtensions[0] + if !reflect.DeepEqual(ext.Id, asn1.ObjectIdentifier{2, 5, 29, 32}) { + t.Fatal(fmt.Sprintf("Wrong OID for policy qualifier %v", ext.Id)) + } + if ext.Critical { + t.Fatal("Policy qualifier marked critical") + } + expectedBytes, _ := hex.DecodeString("3009300706032a03043000") + if !bytes.Equal(ext.Value, expectedBytes) { + t.Fatal(fmt.Sprintf("Value didn't match expected bytes: %s vs %s", + hex.EncodeToString(ext.Value), hex.EncodeToString(expectedBytes))) + } +} + +func TestAddPoliciesWithQualifiers(t *testing.T) { + var cert x509.Certificate + addPolicies(&cert, []config.CertificatePolicy{ + config.CertificatePolicy{ + ID: config.OID{1, 2, 3, 4}, + Qualifiers: []config.CertificatePolicyQualifier{ + config.CertificatePolicyQualifier{ + Type: "id-qt-cps", + Value: "http://example.com/cps", + }, + config.CertificatePolicyQualifier{ + Type: "id-qt-unotice", + Value: "Do What Thou Wilt", + }, + }, + }, + }) + + if len(cert.ExtraExtensions) != 1 { + t.Fatal("No extension added") + } + ext := cert.ExtraExtensions[0] + if !reflect.DeepEqual(ext.Id, asn1.ObjectIdentifier{2, 5, 29, 32}) { + t.Fatal(fmt.Sprintf("Wrong OID for policy qualifier %v", ext.Id)) + } + if ext.Critical { + t.Fatal("Policy qualifier marked critical") + } + expectedBytes, _ := hex.DecodeString("304e304c06032a03043045302206082b060105050702011616687474703a2f2f6578616d706c652e636f6d2f637073301f06082b0601050507020230130c11446f20576861742054686f752057696c74") + if !bytes.Equal(ext.Value, expectedBytes) { + t.Fatal(fmt.Sprintf("Value didn't match expected bytes: %s vs %s", + hex.EncodeToString(ext.Value), hex.EncodeToString(expectedBytes))) + } +} diff --git a/test/boulder-config.json b/test/boulder-config.json index 65780a8bf..1a37e0853 100644 --- a/test/boulder-config.json +++ b/test/boulder-config.json @@ -74,9 +74,14 @@ "ID": "2.23.140.1.2.1" }, { - "ID": "1.3.6.1.4.1.44947.1.1.1", - "type": "id-qt-cps", - "qualifier": "http://cps.root-x1.letsencrypt.org" + "ID": "1.2.3.4", + "Qualifiers": [ { + "type": "id-qt-cps", + "value": "http://example.com/cps" + }, { + "type": "id-qt-unotice", + "value": "Do What Thou Wilt" + } ] } ], "expiry": "8760h", diff --git a/test/boulder-pkcs11-example-config.json b/test/boulder-pkcs11-example-config.json index aa6968830..57b63594a 100644 --- a/test/boulder-pkcs11-example-config.json +++ b/test/boulder-pkcs11-example-config.json @@ -74,11 +74,6 @@ { "ID": "2.23.140.1.2.1" }, - { - "ID": "1.3.6.1.4.1.44947.1.1.1", - "type": "id-qt-cps", - "qualifier": "http://cps.root-x1.letsencrypt.org" - } ], "expiry": "8760h", "CSRWhitelist": { diff --git a/test/boulder-test-config.json b/test/boulder-test-config.json index 3fe5e0504..0630eed23 100644 --- a/test/boulder-test-config.json +++ b/test/boulder-test-config.json @@ -69,9 +69,14 @@ "ID": "2.23.140.1.2.1" }, { - "ID": "1.3.6.1.4.1.44947.1.1.1", - "type": "id-qt-cps", - "qualifier": "http://cps.root-x1.letsencrypt.org" + "ID": "1.2.3.4", + "Qualifiers": [ { + "type": "id-qt-cps", + "value": "http://example.com/cps" + }, { + "type": "id-qt-unotice", + "value": "Do What Thou Wilt" + } ] } ], "expiry": "8760h",