| // Package config contains the configuration logic for CFSSL. |
| package config |
| |
| import ( |
| "crypto/tls" |
| "crypto/x509" |
| "encoding/asn1" |
| "encoding/json" |
| "errors" |
| "fmt" |
| "io/ioutil" |
| "regexp" |
| "strconv" |
| "strings" |
| "time" |
| |
| "github.com/cloudflare/cfssl/auth" |
| cferr "github.com/cloudflare/cfssl/errors" |
| "github.com/cloudflare/cfssl/helpers" |
| "github.com/cloudflare/cfssl/log" |
| ocspConfig "github.com/cloudflare/cfssl/ocsp/config" |
| ) |
| |
| // A CSRWhitelist stores booleans for fields in the CSR. If a CSRWhitelist is |
| // not present in a SigningProfile, all of these fields may be copied from the |
| // CSR into the signed certificate. If a CSRWhitelist *is* present in a |
| // SigningProfile, only those fields with a `true` value in the CSRWhitelist may |
| // be copied from the CSR to the signed certificate. Note that some of these |
| // fields, like Subject, can be provided or partially provided through the API. |
| // Since API clients are expected to be trusted, but CSRs are not, fields |
| // provided through the API are not subject to whitelisting through this |
| // mechanism. |
| type CSRWhitelist struct { |
| Subject, PublicKeyAlgorithm, PublicKey, SignatureAlgorithm bool |
| DNSNames, IPAddresses, EmailAddresses bool |
| } |
| |
| // OID is our own version of asn1's ObjectIdentifier, so we can define a custom |
| // JSON marshal / unmarshal. |
| type OID asn1.ObjectIdentifier |
| |
| // 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 |
| Qualifiers []CertificatePolicyQualifier |
| } |
| |
| // CertificatePolicyQualifier represents a single qualifier from an ASN.1 |
| // PolicyInformation structure. |
| type CertificatePolicyQualifier struct { |
| Type string |
| Value string |
| } |
| |
| // AuthRemote is an authenticated remote signer. |
| type AuthRemote struct { |
| RemoteName string `json:"remote"` |
| AuthKeyName string `json:"auth_key"` |
| } |
| |
| // CAConstraint specifies various CA constraints on the signed certificate. |
| // CAConstraint would verify against (and override) the CA |
| // extensions in the given CSR. |
| type CAConstraint struct { |
| IsCA bool `json:"is_ca"` |
| MaxPathLen int `json:"max_path_len"` |
| MaxPathLenZero bool `json:"max_path_len_zero"` |
| } |
| |
| // 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"` |
| CAConstraint CAConstraint `json:"ca_constraint"` |
| 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"` |
| AuthRemote AuthRemote `json:"auth_remote"` |
| CTLogServers []string `json:"ct_log_servers"` |
| AllowedExtensions []OID `json:"allowed_extensions"` |
| CertStore string `json:"cert_store"` |
| |
| Policies []CertificatePolicy |
| Expiry time.Duration |
| Backdate time.Duration |
| Provider auth.Provider |
| RemoteProvider auth.Provider |
| RemoteServer string |
| RemoteCAs *x509.CertPool |
| ClientCert *tls.Certificate |
| CSRWhitelist *CSRWhitelist |
| NameWhitelist *regexp.Regexp |
| ExtensionWhitelist map[string]bool |
| ClientProvidesSerialNumbers bool |
| } |
| |
| // UnmarshalJSON unmarshals a JSON string into an OID. |
| func (oid *OID) UnmarshalJSON(data []byte) (err error) { |
| if data[0] != '"' || data[len(data)-1] != '"' { |
| return errors.New("OID JSON string not wrapped in quotes." + string(data)) |
| } |
| data = data[1 : len(data)-1] |
| parsedOid, err := parseObjectIdentifier(string(data)) |
| if err != nil { |
| return err |
| } |
| *oid = OID(parsedOid) |
| return |
| } |
| |
| // MarshalJSON marshals an oid into a JSON string. |
| func (oid OID) MarshalJSON() ([]byte, error) { |
| return []byte(fmt.Sprintf(`"%v"`, asn1.ObjectIdentifier(oid))), nil |
| } |
| |
| func parseObjectIdentifier(oidString string) (oid asn1.ObjectIdentifier, err error) { |
| validOID, err := regexp.MatchString("\\d(\\.\\d+)*", oidString) |
| if err != nil { |
| return |
| } |
| if !validOID { |
| err = errors.New("Invalid OID") |
| return |
| } |
| |
| segments := strings.Split(oidString, ".") |
| oid = make(asn1.ObjectIdentifier, len(segments)) |
| for i, intString := range segments { |
| oid[i], err = strconv.Atoi(intString) |
| if err != nil { |
| return |
| } |
| } |
| return |
| } |
| |
| const timeFormat = "2006-01-02T15:04:05" |
| |
| // populate is used to fill in the fields that are not in JSON |
| // |
| // First, the ExpiryString parameter is needed to parse |
| // expiration timestamps from JSON. The JSON decoder is not able to |
| // decode a string time duration to a time.Duration, so this is called |
| // when loading the configuration to properly parse and fill out the |
| // Expiry parameter. |
| // This function is also used to create references to the auth key |
| // and default remote for the profile. |
| // It returns true if ExpiryString is a valid representation of a |
| // time.Duration, and the AuthKeyString and RemoteName point to |
| // valid objects. It returns false otherwise. |
| func (p *SigningProfile) populate(cfg *Config) error { |
| if p == nil { |
| return cferr.Wrap(cferr.PolicyError, cferr.InvalidPolicy, errors.New("can't parse nil profile")) |
| } |
| |
| var err error |
| if p.RemoteName == "" && p.AuthRemote.RemoteName == "" { |
| log.Debugf("parse expiry in profile") |
| if p.ExpiryString == "" { |
| return cferr.Wrap(cferr.PolicyError, cferr.InvalidPolicy, errors.New("empty expiry string")) |
| } |
| |
| dur, err := time.ParseDuration(p.ExpiryString) |
| if err != nil { |
| return cferr.Wrap(cferr.PolicyError, cferr.InvalidPolicy, err) |
| } |
| |
| log.Debugf("expiry is valid") |
| p.Expiry = dur |
| |
| if p.BackdateString != "" { |
| dur, err = time.ParseDuration(p.BackdateString) |
| if err != nil { |
| return cferr.Wrap(cferr.PolicyError, cferr.InvalidPolicy, err) |
| } |
| |
| p.Backdate = dur |
| } |
| |
| if !p.NotBefore.IsZero() && !p.NotAfter.IsZero() && p.NotAfter.Before(p.NotBefore) { |
| return cferr.Wrap(cferr.PolicyError, cferr.InvalidPolicy, err) |
| } |
| |
| if len(p.Policies) > 0 { |
| for _, policy := range p.Policies { |
| 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")) |
| } |
| } |
| } |
| } |
| } else if p.RemoteName != "" { |
| log.Debug("match remote in profile to remotes section") |
| if p.AuthRemote.RemoteName != "" { |
| log.Error("profile has both a remote and an auth remote specified") |
| return cferr.New(cferr.PolicyError, cferr.InvalidPolicy) |
| } |
| if remote := cfg.Remotes[p.RemoteName]; remote != "" { |
| if err := p.updateRemote(remote); err != nil { |
| return err |
| } |
| } else { |
| return cferr.Wrap(cferr.PolicyError, cferr.InvalidPolicy, |
| errors.New("failed to find remote in remotes section")) |
| } |
| } else { |
| log.Debug("match auth remote in profile to remotes section") |
| if remote := cfg.Remotes[p.AuthRemote.RemoteName]; remote != "" { |
| if err := p.updateRemote(remote); err != nil { |
| return err |
| } |
| } else { |
| return cferr.Wrap(cferr.PolicyError, cferr.InvalidPolicy, |
| errors.New("failed to find remote in remotes section")) |
| } |
| } |
| |
| if p.AuthKeyName != "" { |
| log.Debug("match auth key in profile to auth_keys section") |
| if key, ok := cfg.AuthKeys[p.AuthKeyName]; ok == true { |
| if key.Type == "standard" { |
| p.Provider, err = auth.New(key.Key, nil) |
| if err != nil { |
| log.Debugf("failed to create new standard auth provider: %v", err) |
| return cferr.Wrap(cferr.PolicyError, cferr.InvalidPolicy, |
| errors.New("failed to create new standard auth provider")) |
| } |
| } else { |
| log.Debugf("unknown authentication type %v", key.Type) |
| return cferr.Wrap(cferr.PolicyError, cferr.InvalidPolicy, |
| errors.New("unknown authentication type")) |
| } |
| } else { |
| return cferr.Wrap(cferr.PolicyError, cferr.InvalidPolicy, |
| errors.New("failed to find auth_key in auth_keys section")) |
| } |
| } |
| |
| if p.AuthRemote.AuthKeyName != "" { |
| log.Debug("match auth remote key in profile to auth_keys section") |
| if key, ok := cfg.AuthKeys[p.AuthRemote.AuthKeyName]; ok == true { |
| if key.Type == "standard" { |
| p.RemoteProvider, err = auth.New(key.Key, nil) |
| if err != nil { |
| log.Debugf("failed to create new standard auth provider: %v", err) |
| return cferr.Wrap(cferr.PolicyError, cferr.InvalidPolicy, |
| errors.New("failed to create new standard auth provider")) |
| } |
| } else { |
| log.Debugf("unknown authentication type %v", key.Type) |
| return cferr.Wrap(cferr.PolicyError, cferr.InvalidPolicy, |
| errors.New("unknown authentication type")) |
| } |
| } else { |
| return cferr.Wrap(cferr.PolicyError, cferr.InvalidPolicy, |
| errors.New("failed to find auth_remote's auth_key in auth_keys section")) |
| } |
| } |
| |
| 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 |
| } |
| |
| p.ExtensionWhitelist = map[string]bool{} |
| for _, oid := range p.AllowedExtensions { |
| p.ExtensionWhitelist[asn1.ObjectIdentifier(oid).String()] = true |
| } |
| |
| return nil |
| } |
| |
| // updateRemote takes a signing profile and initializes the remote server object |
| // to the hostname:port combination sent by remote. |
| func (p *SigningProfile) updateRemote(remote string) error { |
| if remote != "" { |
| p.RemoteServer = remote |
| } |
| return nil |
| } |
| |
| // OverrideRemotes takes a signing configuration and updates the remote server object |
| // to the hostname:port combination sent by remote |
| func (p *Signing) OverrideRemotes(remote string) error { |
| if remote != "" { |
| var err error |
| for _, profile := range p.Profiles { |
| err = profile.updateRemote(remote) |
| if err != nil { |
| return err |
| } |
| } |
| err = p.Default.updateRemote(remote) |
| if err != nil { |
| return err |
| } |
| } |
| return nil |
| } |
| |
| // SetClientCertKeyPairFromFile updates the properties to set client certificates for mutual |
| // authenticated TLS remote requests |
| func (p *Signing) SetClientCertKeyPairFromFile(certFile string, keyFile string) error { |
| if certFile != "" && keyFile != "" { |
| cert, err := helpers.LoadClientCertificate(certFile, keyFile) |
| if err != nil { |
| return err |
| } |
| for _, profile := range p.Profiles { |
| profile.ClientCert = cert |
| } |
| p.Default.ClientCert = cert |
| } |
| return nil |
| } |
| |
| // SetRemoteCAsFromFile reads root CAs from file and updates the properties to set remote CAs for TLS |
| // remote requests |
| func (p *Signing) SetRemoteCAsFromFile(caFile string) error { |
| if caFile != "" { |
| remoteCAs, err := helpers.LoadPEMCertPool(caFile) |
| if err != nil { |
| return err |
| } |
| p.SetRemoteCAs(remoteCAs) |
| } |
| return nil |
| } |
| |
| // SetRemoteCAs updates the properties to set remote CAs for TLS |
| // remote requests |
| func (p *Signing) SetRemoteCAs(remoteCAs *x509.CertPool) { |
| for _, profile := range p.Profiles { |
| profile.RemoteCAs = remoteCAs |
| } |
| p.Default.RemoteCAs = remoteCAs |
| } |
| |
| // NeedsRemoteSigner returns true if one of the profiles has a remote set |
| func (p *Signing) NeedsRemoteSigner() bool { |
| for _, profile := range p.Profiles { |
| if profile.RemoteServer != "" { |
| return true |
| } |
| } |
| |
| if p.Default.RemoteServer != "" { |
| return true |
| } |
| |
| return false |
| } |
| |
| // NeedsLocalSigner returns true if one of the profiles doe not have a remote set |
| func (p *Signing) NeedsLocalSigner() bool { |
| for _, profile := range p.Profiles { |
| if profile.RemoteServer == "" { |
| return true |
| } |
| } |
| |
| if p.Default.RemoteServer == "" { |
| return true |
| } |
| |
| return false |
| } |
| |
| // Usages parses the list of key uses in the profile, translating them |
| // to a list of X.509 key usages and extended key usages. The unknown |
| // uses are collected into a slice that is also returned. |
| func (p *SigningProfile) Usages() (ku x509.KeyUsage, eku []x509.ExtKeyUsage, unk []string) { |
| for _, keyUse := range p.Usage { |
| if kuse, ok := KeyUsage[keyUse]; ok { |
| ku |= kuse |
| } else if ekuse, ok := ExtKeyUsage[keyUse]; ok { |
| eku = append(eku, ekuse) |
| } else { |
| unk = append(unk, keyUse) |
| } |
| } |
| return |
| } |
| |
| // A valid profile must be a valid local profile or a valid remote profile. |
| // A valid local profile has defined at least key usages to be used, and a |
| // valid local default profile has defined at least a default expiration. |
| // A valid remote profile (default or not) has remote signer initialized. |
| // In addition, a remote profile must has a valid auth provider if auth |
| // key defined. |
| func (p *SigningProfile) validProfile(isDefault bool) bool { |
| if p == nil { |
| return false |
| } |
| |
| if p.AuthRemote.RemoteName == "" && p.AuthRemote.AuthKeyName != "" { |
| log.Debugf("invalid auth remote profile: no remote signer specified") |
| return false |
| } |
| |
| if p.RemoteName != "" { |
| log.Debugf("validate remote profile") |
| |
| if p.RemoteServer == "" { |
| log.Debugf("invalid remote profile: no remote signer specified") |
| return false |
| } |
| |
| if p.AuthKeyName != "" && p.Provider == nil { |
| log.Debugf("invalid remote profile: auth key name is defined but no auth provider is set") |
| return false |
| } |
| |
| if p.AuthRemote.RemoteName != "" { |
| log.Debugf("invalid remote profile: auth remote is also specified") |
| return false |
| } |
| } else if p.AuthRemote.RemoteName != "" { |
| log.Debugf("validate auth remote profile") |
| if p.RemoteServer == "" { |
| log.Debugf("invalid auth remote profile: no remote signer specified") |
| return false |
| } |
| |
| if p.AuthRemote.AuthKeyName == "" || p.RemoteProvider == nil { |
| log.Debugf("invalid auth remote profile: no auth key is defined") |
| return false |
| } |
| } else { |
| log.Debugf("validate local profile") |
| if !isDefault { |
| if len(p.Usage) == 0 { |
| log.Debugf("invalid local profile: no usages specified") |
| return false |
| } else if _, _, unk := p.Usages(); len(unk) == len(p.Usage) { |
| log.Debugf("invalid local profile: no valid usages") |
| return false |
| } |
| } else { |
| if p.Expiry == 0 { |
| log.Debugf("invalid local profile: no expiry set") |
| return false |
| } |
| } |
| } |
| |
| log.Debugf("profile is valid") |
| return true |
| } |
| |
| // This checks if the SigningProfile object contains configurations that are only effective with a local signer |
| // which has access to CA private key. |
| func (p *SigningProfile) hasLocalConfig() bool { |
| if p.Usage != nil || |
| p.IssuerURL != nil || |
| p.OCSP != "" || |
| p.ExpiryString != "" || |
| p.BackdateString != "" || |
| p.CAConstraint.IsCA != false || |
| !p.NotBefore.IsZero() || |
| !p.NotAfter.IsZero() || |
| p.NameWhitelistString != "" || |
| len(p.CTLogServers) != 0 { |
| return true |
| } |
| return false |
| } |
| |
| // warnSkippedSettings prints a log warning message about skipped settings |
| // in a SigningProfile, usually due to remote signer. |
| func (p *Signing) warnSkippedSettings() { |
| const warningMessage = `The configuration value by "usages", "issuer_urls", "ocsp_url", "crl_url", "ca_constraint", "expiry", "backdate", "not_before", "not_after", "cert_store" and "ct_log_servers" are skipped` |
| if p == nil { |
| return |
| } |
| |
| if (p.Default.RemoteName != "" || p.Default.AuthRemote.RemoteName != "") && p.Default.hasLocalConfig() { |
| log.Warning("default profile points to a remote signer: ", warningMessage) |
| } |
| |
| for name, profile := range p.Profiles { |
| if (profile.RemoteName != "" || profile.AuthRemote.RemoteName != "") && profile.hasLocalConfig() { |
| log.Warningf("Profiles[%s] points to a remote signer: %s", name, warningMessage) |
| } |
| } |
| } |
| |
| // Signing codifies the signature configuration policy for a CA. |
| type Signing struct { |
| Profiles map[string]*SigningProfile `json:"profiles"` |
| Default *SigningProfile `json:"default"` |
| } |
| |
| // Config stores configuration information for the CA. |
| type Config struct { |
| Signing *Signing `json:"signing"` |
| OCSP *ocspConfig.Config `json:"ocsp"` |
| AuthKeys map[string]AuthKey `json:"auth_keys,omitempty"` |
| Remotes map[string]string `json:"remotes,omitempty"` |
| } |
| |
| // Valid ensures that Config is a valid configuration. It should be |
| // called immediately after parsing a configuration file. |
| func (c *Config) Valid() bool { |
| return c.Signing.Valid() |
| } |
| |
| // Valid checks the signature policies, ensuring they are valid |
| // policies. A policy is valid if it has defined at least key usages |
| // to be used, and a valid default profile has defined at least a |
| // default expiration. |
| func (p *Signing) Valid() bool { |
| if p == nil { |
| return false |
| } |
| |
| log.Debugf("validating configuration") |
| if !p.Default.validProfile(true) { |
| log.Debugf("default profile is invalid") |
| return false |
| } |
| |
| for _, sp := range p.Profiles { |
| if !sp.validProfile(false) { |
| log.Debugf("invalid profile") |
| return false |
| } |
| } |
| |
| p.warnSkippedSettings() |
| |
| return true |
| } |
| |
| // KeyUsage contains a mapping of string names to key usages. |
| var KeyUsage = map[string]x509.KeyUsage{ |
| "signing": x509.KeyUsageDigitalSignature, |
| "digital signature": x509.KeyUsageDigitalSignature, |
| "content committment": x509.KeyUsageContentCommitment, |
| "key encipherment": x509.KeyUsageKeyEncipherment, |
| "key agreement": x509.KeyUsageKeyAgreement, |
| "data encipherment": x509.KeyUsageDataEncipherment, |
| "cert sign": x509.KeyUsageCertSign, |
| "crl sign": x509.KeyUsageCRLSign, |
| "encipher only": x509.KeyUsageEncipherOnly, |
| "decipher only": x509.KeyUsageDecipherOnly, |
| } |
| |
| // ExtKeyUsage contains a mapping of string names to extended key |
| // usages. |
| var ExtKeyUsage = map[string]x509.ExtKeyUsage{ |
| "any": x509.ExtKeyUsageAny, |
| "server auth": x509.ExtKeyUsageServerAuth, |
| "client auth": x509.ExtKeyUsageClientAuth, |
| "code signing": x509.ExtKeyUsageCodeSigning, |
| "email protection": x509.ExtKeyUsageEmailProtection, |
| "s/mime": x509.ExtKeyUsageEmailProtection, |
| "ipsec end system": x509.ExtKeyUsageIPSECEndSystem, |
| "ipsec tunnel": x509.ExtKeyUsageIPSECTunnel, |
| "ipsec user": x509.ExtKeyUsageIPSECUser, |
| "timestamping": x509.ExtKeyUsageTimeStamping, |
| "ocsp signing": x509.ExtKeyUsageOCSPSigning, |
| "microsoft sgc": x509.ExtKeyUsageMicrosoftServerGatedCrypto, |
| "netscape sgc": x509.ExtKeyUsageNetscapeServerGatedCrypto, |
| } |
| |
| // An AuthKey contains an entry for a key used for authentication. |
| type AuthKey struct { |
| // Type contains information needed to select the appropriate |
| // constructor. For example, "standard" for HMAC-SHA-256, |
| // "standard-ip" for HMAC-SHA-256 incorporating the client's |
| // IP. |
| Type string `json:"type"` |
| // Key contains the key information, such as a hex-encoded |
| // HMAC key. |
| Key string `json:"key"` |
| } |
| |
| // DefaultConfig returns a default configuration specifying basic key |
| // usage and a 1 year expiration time. The key usages chosen are |
| // signing, key encipherment, client auth and server auth. |
| func DefaultConfig() *SigningProfile { |
| d := helpers.OneYear |
| return &SigningProfile{ |
| Usage: []string{"signing", "key encipherment", "server auth", "client auth"}, |
| Expiry: d, |
| ExpiryString: "8760h", |
| } |
| } |
| |
| // LoadFile attempts to load the configuration file stored at the path |
| // and returns the configuration. On error, it returns nil. |
| func LoadFile(path string) (*Config, error) { |
| log.Debugf("loading configuration file from %s", path) |
| if path == "" { |
| return nil, cferr.Wrap(cferr.PolicyError, cferr.InvalidPolicy, errors.New("invalid path")) |
| } |
| |
| body, err := ioutil.ReadFile(path) |
| if err != nil { |
| return nil, cferr.Wrap(cferr.PolicyError, cferr.InvalidPolicy, errors.New("could not read configuration file")) |
| } |
| |
| return LoadConfig(body) |
| } |
| |
| // LoadConfig attempts to load the configuration from a byte slice. |
| // On error, it returns nil. |
| func LoadConfig(config []byte) (*Config, error) { |
| var cfg = &Config{} |
| err := json.Unmarshal(config, &cfg) |
| if err != nil { |
| return nil, cferr.Wrap(cferr.PolicyError, cferr.InvalidPolicy, |
| errors.New("failed to unmarshal configuration: "+err.Error())) |
| } |
| |
| if cfg.Signing == nil { |
| return nil, errors.New("No \"signing\" field present") |
| } |
| |
| if cfg.Signing.Default == nil { |
| log.Debugf("no default given: using default config") |
| cfg.Signing.Default = DefaultConfig() |
| } else { |
| if err := cfg.Signing.Default.populate(cfg); err != nil { |
| return nil, err |
| } |
| } |
| |
| for k := range cfg.Signing.Profiles { |
| if err := cfg.Signing.Profiles[k].populate(cfg); err != nil { |
| return nil, err |
| } |
| } |
| |
| if !cfg.Valid() { |
| return nil, cferr.Wrap(cferr.PolicyError, cferr.InvalidPolicy, errors.New("invalid configuration")) |
| } |
| |
| log.Debugf("configuration ok") |
| return cfg, nil |
| } |