diff options
-rw-r--r-- | cmd/caddy2/main.go | 1 | ||||
-rw-r--r-- | modules/caddytls/connpolicy.go | 14 | ||||
-rw-r--r-- | modules/caddytls/sessiontickets.go | 214 | ||||
-rw-r--r-- | modules/caddytls/standardstek/stek.go | 112 | ||||
-rw-r--r-- | modules/caddytls/tls.go | 109 | ||||
-rw-r--r-- | modules/caddytls/values.go | 102 |
6 files changed, 453 insertions, 99 deletions
diff --git a/cmd/caddy2/main.go b/cmd/caddy2/main.go index daed20e..7d702f0 100644 --- a/cmd/caddy2/main.go +++ b/cmd/caddy2/main.go @@ -13,6 +13,7 @@ import ( _ "bitbucket.org/lightcodelabs/caddy2/modules/caddyhttp/reverseproxy" _ "bitbucket.org/lightcodelabs/caddy2/modules/caddyhttp/rewrite" _ "bitbucket.org/lightcodelabs/caddy2/modules/caddytls" + _ "bitbucket.org/lightcodelabs/caddy2/modules/caddytls/standardstek" ) func main() { diff --git a/modules/caddytls/connpolicy.go b/modules/caddytls/connpolicy.go index 3241d3c..006afe9 100644 --- a/modules/caddytls/connpolicy.go +++ b/modules/caddytls/connpolicy.go @@ -133,9 +133,21 @@ func (p *ConnectionPolicy) buildStandardTLSConfig(ctx caddy2.Context) error { }, MinVersion: tls.VersionTLS12, MaxVersion: tls.VersionTLS13, - // TODO: Session ticket key rotation (use Storage) } + // session tickets support + cfg.SessionTicketsDisabled = tlsApp.SessionTickets.Disabled + + // session ticket key rotation + tlsApp.SessionTickets.register(cfg) + ctx.OnCancel(func() { + // do cleanup when the context is cancelled because, + // though unlikely, it is possible that a context + // needing a TLS server config could exist for less + // than the lifetime of the whole app + tlsApp.SessionTickets.unregister(cfg) + }) + // add all the cipher suites in order, without duplicates cipherSuitesAdded := make(map[uint16]struct{}) for _, csName := range p.CipherSuites { diff --git a/modules/caddytls/sessiontickets.go b/modules/caddytls/sessiontickets.go new file mode 100644 index 0000000..22c9a2f --- /dev/null +++ b/modules/caddytls/sessiontickets.go @@ -0,0 +1,214 @@ +package caddytls + +import ( + "crypto/rand" + "crypto/tls" + "encoding/json" + "fmt" + "io" + "sync" + "time" + + "bitbucket.org/lightcodelabs/caddy2" +) + +// SessionTicketService configures and manages TLS session tickets. +type SessionTicketService struct { + KeySource json.RawMessage `json:"key_source,omitempty"` + RotationInterval caddy2.Duration `json:"rotation_interval,omitempty"` + MaxKeys int `json:"max_keys,omitempty"` + DisableRotation bool `json:"disable_rotation,omitempty"` + Disabled bool `json:"disabled,omitempty"` + + keySource STEKProvider + configs map[*tls.Config]struct{} + stopChan chan struct{} + currentKeys [][32]byte + mu *sync.Mutex +} + +func (s *SessionTicketService) provision(ctx caddy2.Context) error { + s.configs = make(map[*tls.Config]struct{}) + s.mu = new(sync.Mutex) + + // establish sane defaults + if s.RotationInterval == 0 { + s.RotationInterval = caddy2.Duration(defaultSTEKRotationInterval) + } + if s.MaxKeys <= 0 { + s.MaxKeys = defaultMaxSTEKs + } + if s.KeySource == nil { + s.KeySource = json.RawMessage(`{"provider":"standard"}`) + } + + // load the STEK module, which will provide keys + val, err := ctx.LoadModuleInline("provider", "tls.stek", s.KeySource) + if err != nil { + return fmt.Errorf("loading TLS session ticket ephemeral keys provider module: %s", err) + } + s.keySource = val.(STEKProvider) + s.KeySource = nil // allow GC to deallocate - TODO: Does this help? + + // if session tickets or just rotation are + // disabled, no need to start service + if s.Disabled || s.DisableRotation { + return nil + } + + // start the STEK module; this ensures we have + // a starting key before any config needs one + return s.start() +} + +// start loads the starting STEKs and spawns a goroutine +// which loops to rotate the STEKs, which continues until +// stop() is called. If start() was already called, this +// is a no-op. +func (s *SessionTicketService) start() error { + if s.stopChan != nil { + return nil + } + s.stopChan = make(chan struct{}) + + // initializing the key source gives us our + // initial key(s) to start with; if successful, + // we need to be sure to call Next() so that + // the key source can know when it is done + initialKeys, err := s.keySource.Initialize(s) + if err != nil { + return fmt.Errorf("setting STEK module configuration: %v", err) + } + + s.mu.Lock() + s.currentKeys = initialKeys + s.mu.Unlock() + + // keep the keys rotated + go s.stayUpdated() + + return nil +} + +// stayUpdated is a blocking function which rotates +// the keys whenever new ones are sent. It reads +// from keysChan until s.stop() is called. +func (s *SessionTicketService) stayUpdated() { + // this call is essential when Initialize() + // returns without error, because the stop + // channel is the only way the key source + // will know when to clean up + keysChan := s.keySource.Next(s.stopChan) + + for { + select { + case newKeys := <-keysChan: + s.mu.Lock() + s.currentKeys = newKeys + configs := s.configs + s.mu.Unlock() + for cfg := range configs { + cfg.SetSessionTicketKeys(newKeys) + } + case <-s.stopChan: + return + } + } +} + +// stop terminates the key rotation goroutine. +func (s *SessionTicketService) stop() { + if s.stopChan != nil { + close(s.stopChan) + } +} + +// register sets the session ticket keys on cfg +// and keeps them updated. Any values registered +// must be unregistered, or they will not be +// garbage-collected. s.start() must have been +// called first. If session tickets are disabled +// or if ticket key rotation is disabled, this +// function is a no-op. +func (s *SessionTicketService) register(cfg *tls.Config) { + if s.Disabled || s.DisableRotation { + return + } + s.mu.Lock() + cfg.SetSessionTicketKeys(s.currentKeys) + s.configs[cfg] = struct{}{} + s.mu.Unlock() +} + +// unregister stops session key management on cfg and +// removes the internal stored reference to cfg. If +// session tickets are disabled or if ticket key rotation +// is disabled, this function is a no-op. +func (s *SessionTicketService) unregister(cfg *tls.Config) { + if s.Disabled || s.DisableRotation { + return + } + s.mu.Lock() + delete(s.configs, cfg) + s.mu.Unlock() +} + +// RotateSTEKs rotates the keys in keys by producing a new key and eliding +// the oldest one. The new slice of keys is returned. +func (s SessionTicketService) RotateSTEKs(keys [][32]byte) ([][32]byte, error) { + // produce a new key + newKey, err := s.generateSTEK() + if err != nil { + return nil, fmt.Errorf("generating STEK: %v", err) + } + + // we need to prepend this new key to the list of + // keys so that it is preferred, but we need to be + // careful that we do not grow the slice larger + // than MaxKeys, otherwise we'll be storing one + // more key in memory than we expect; so be sure + // that the slice does not grow beyond the limit + // even for a brief period of time, since there's + // no guarantee when that extra allocation will + // be overwritten; this is why we first trim the + // length to one less the max, THEN prepend the + // new key + if len(keys) >= s.MaxKeys { + keys[len(keys)-1] = [32]byte{} // zero-out memory of oldest key + keys = keys[:s.MaxKeys-1] // trim length of slice + } + keys = append([][32]byte{newKey}, keys...) // prepend new key + + return keys, nil +} + +// generateSTEK generates key material suitable for use as a +// session ticket ephemeral key. +func (s *SessionTicketService) generateSTEK() ([32]byte, error) { + var newTicketKey [32]byte + _, err := io.ReadFull(rand.Reader, newTicketKey[:]) + return newTicketKey, err +} + +// STEKProvider is a type that can provide session ticket ephemeral +// keys (STEKs). +type STEKProvider interface { + // Initialize provides the STEK configuration to the STEK + // module so that it can obtain and manage keys accordingly. + // It returns the initial key(s) to use. Implementations can + // rely on Next() being called if Initialize() returns + // without error, so that it may know when it is done. + Initialize(config *SessionTicketService) ([][32]byte, error) + + // Next returns the channel through which the next session + // ticket keys will be transmitted until doneChan is closed. + // Keys should be sent on keysChan as they are updated. + // When doneChan is closed, any resources allocated in + // Initialize() must be cleaned up. + Next(doneChan <-chan struct{}) (keysChan <-chan [][32]byte) +} + +const ( + defaultSTEKRotationInterval = 12 * time.Hour + defaultMaxSTEKs = 4 +) diff --git a/modules/caddytls/standardstek/stek.go b/modules/caddytls/standardstek/stek.go new file mode 100644 index 0000000..46ac786 --- /dev/null +++ b/modules/caddytls/standardstek/stek.go @@ -0,0 +1,112 @@ +package standardstek + +import ( + "log" + "sync" + "time" + + "bitbucket.org/lightcodelabs/caddy2" + "bitbucket.org/lightcodelabs/caddy2/modules/caddytls" +) + +func init() { + caddy2.RegisterModule(caddy2.Module{ + Name: "tls.stek.standard", + New: func() interface{} { return new(standardSTEKProvider) }, + }) +} + +type standardSTEKProvider struct { + stekConfig *caddytls.SessionTicketService + timer *time.Timer +} + +// Initialize sets the configuration for s and returns the starting keys. +func (s *standardSTEKProvider) Initialize(config *caddytls.SessionTicketService) ([][32]byte, error) { + // keep a reference to the config, we'll need when rotating keys + s.stekConfig = config + + itvl := time.Duration(s.stekConfig.RotationInterval) + + mutex.Lock() + defer mutex.Unlock() + + // if this is our first rotation or we are overdue + // for one, perform a rotation immediately; otherwise, + // we assume that the keys are non-empty and fresh + since := time.Since(lastRotation) + if lastRotation.IsZero() || since > itvl { + var err error + keys, err = s.stekConfig.RotateSTEKs(keys) + if err != nil { + return nil, err + } + since = 0 // since this is overdue or is the first rotation, use full interval + lastRotation = time.Now() + } + + // create timer for the remaining time on the interval; + // this timer is cleaned up only when Next() returns + s.timer = time.NewTimer(itvl - since) + + return keys, nil +} + +// Next returns a channel which transmits the latest session ticket keys. +func (s *standardSTEKProvider) Next(doneChan <-chan struct{}) <-chan [][32]byte { + keysChan := make(chan [][32]byte) + go s.rotate(doneChan, keysChan) + return keysChan +} + +// rotate rotates keys on a regular basis, sending each updated set of +// keys down keysChan, until doneChan is closed. +func (s *standardSTEKProvider) rotate(doneChan <-chan struct{}, keysChan chan<- [][32]byte) { + for { + select { + case now := <-s.timer.C: + // copy the slice header to avoid races + mutex.RLock() + keysCopy := keys + mutex.RUnlock() + + // generate a new key, rotating old ones + var err error + keysCopy, err = s.stekConfig.RotateSTEKs(keysCopy) + if err != nil { + // TODO: improve this handling + log.Printf("[ERROR] Generating STEK: %v", err) + continue + } + + // replace keys slice with updated value and + // record the timestamp of rotation + mutex.Lock() + keys = keysCopy + lastRotation = now + mutex.Unlock() + + // send the updated keys to the service + keysChan <- keysCopy + + // timer channel is already drained, so reset directly (see godoc) + s.timer.Reset(time.Duration(s.stekConfig.RotationInterval)) + + case <-doneChan: + // again, see godocs for why timer is stopped this way + if !s.timer.Stop() { + <-s.timer.C + } + return + } + } +} + +var ( + lastRotation time.Time + keys [][32]byte + mutex sync.RWMutex // protects keys and lastRotation +) + +// Interface guard +var _ caddytls.STEKProvider = (*standardSTEKProvider)(nil) diff --git a/modules/caddytls/tls.go b/modules/caddytls/tls.go index 29ca88e..6a9c97e 100644 --- a/modules/caddytls/tls.go +++ b/modules/caddytls/tls.go @@ -2,14 +2,12 @@ package caddytls import ( "crypto/tls" - "crypto/x509" "encoding/json" "fmt" "net/http" "bitbucket.org/lightcodelabs/caddy2" "github.com/go-acme/lego/challenge" - "github.com/klauspost/cpuid" "github.com/mholt/certmagic" ) @@ -22,8 +20,9 @@ func init() { // TLS represents a process-wide TLS configuration. type TLS struct { - Certificates map[string]json.RawMessage `json:"certificates"` - Automation AutomationConfig `json:"automation"` + Certificates map[string]json.RawMessage `json:"certificates,omitempty"` + Automation AutomationConfig `json:"automation,omitempty"` + SessionTickets SessionTicketService `json:"session_tickets,omitempty"` certificateLoaders []CertificateLoader certCache *certmagic.Cache @@ -44,6 +43,7 @@ func (t *TLS) Provision(ctx caddy2.Context) error { }, }) + // automation/management policies for i, ap := range t.Automation.Policies { val, err := ctx.LoadModuleInline("module", "tls.management", ap.Management) if err != nil { @@ -65,6 +65,12 @@ func (t *TLS) Provision(ctx caddy2.Context) error { t.certificateLoaders = append(t.certificateLoaders, val.(CertificateLoader)) } + // session ticket ephemeral keys (STEK) service and provider + err := t.SessionTickets.provision(ctx) + if err != nil { + return fmt.Errorf("provisioning session tickets configuration: %v", err) + } + return nil } @@ -110,6 +116,7 @@ func (t *TLS) Stop() error { // TODO: ensure locks are cleaned up too... maybe in certmagic though t.certCache.Stop() } + t.SessionTickets.stop() return nil } @@ -230,98 +237,4 @@ type ManagerMaker interface { newManager(interactive bool) (certmagic.Manager, error) } -// supportedCipherSuites is the unordered map of cipher suite -// string names to their definition in crypto/tls. -// TODO: might not be needed much longer, see: -// https://github.com/golang/go/issues/30325 -var supportedCipherSuites = map[string]uint16{ - "ECDHE_ECDSA_AES256_GCM_SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - "ECDHE_RSA_AES256_GCM_SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - "ECDHE_ECDSA_AES128_GCM_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - "ECDHE_RSA_AES128_GCM_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - "ECDHE_ECDSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, - "ECDHE_RSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, - "ECDHE_RSA_AES256_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, - "ECDHE_RSA_AES128_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, - "ECDHE_ECDSA_AES256_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, - "ECDHE_ECDSA_AES128_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, - "RSA_AES256_CBC_SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA, - "RSA_AES128_CBC_SHA": tls.TLS_RSA_WITH_AES_128_CBC_SHA, - "ECDHE_RSA_3DES_EDE_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, - "RSA_3DES_EDE_CBC_SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, -} - -// defaultCipherSuites is the ordered list of all the cipher -// suites we want to support by default, assuming AES-NI -// (hardware acceleration for AES). -var defaultCipherSuitesWithAESNI = []uint16{ - tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, - tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, -} - -// defaultCipherSuites is the ordered list of all the cipher -// suites we want to support by default, assuming lack of -// AES-NI (NO hardware acceleration for AES). -var defaultCipherSuitesWithoutAESNI = []uint16{ - tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, - tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, - tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, -} - -// getOptimalDefaultCipherSuites returns an appropriate cipher -// suite to use depending on the hardware support for AES. -// -// See https://github.com/mholt/caddy/issues/1674 -func getOptimalDefaultCipherSuites() []uint16 { - if cpuid.CPU.AesNi() { - return defaultCipherSuitesWithAESNI - } - return defaultCipherSuitesWithoutAESNI -} - -// supportedCurves is the unordered map of supported curves. -// https://golang.org/pkg/crypto/tls/#CurveID -var supportedCurves = map[string]tls.CurveID{ - "X25519": tls.X25519, - "P256": tls.CurveP256, - "P384": tls.CurveP384, - "P521": tls.CurveP521, -} - -// defaultCurves is the list of only the curves we want to use -// by default, in descending order of preference. -// -// This list should only include curves which are fast by design -// (e.g. X25519) and those for which an optimized assembly -// implementation exists (e.g. P256). The latter ones can be -// found here: -// https://github.com/golang/go/tree/master/src/crypto/elliptic -var defaultCurves = []tls.CurveID{ - tls.X25519, - tls.CurveP256, -} - -// supportedProtocols is a map of supported protocols. -// HTTP/2 only supports TLS 1.2 and higher. -var supportedProtocols = map[string]uint16{ - "tls1.0": tls.VersionTLS10, - "tls1.1": tls.VersionTLS11, - "tls1.2": tls.VersionTLS12, - "tls1.3": tls.VersionTLS13, -} - -// publicKeyAlgorithms is the map of supported public key algorithms. -var publicKeyAlgorithms = map[string]x509.PublicKeyAlgorithm{ - "rsa": x509.RSA, - "dsa": x509.DSA, - "ecdsa": x509.ECDSA, -} - const automateKey = "automate" diff --git a/modules/caddytls/values.go b/modules/caddytls/values.go new file mode 100644 index 0000000..0559dc6 --- /dev/null +++ b/modules/caddytls/values.go @@ -0,0 +1,102 @@ +package caddytls + +import ( + "crypto/tls" + "crypto/x509" + + "github.com/klauspost/cpuid" +) + +// supportedCipherSuites is the unordered map of cipher suite +// string names to their definition in crypto/tls. +// TODO: might not be needed much longer, see: +// https://github.com/golang/go/issues/30325 +var supportedCipherSuites = map[string]uint16{ + "ECDHE_ECDSA_AES256_GCM_SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + "ECDHE_RSA_AES256_GCM_SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + "ECDHE_ECDSA_AES128_GCM_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + "ECDHE_RSA_AES128_GCM_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + "ECDHE_ECDSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, + "ECDHE_RSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, + "ECDHE_RSA_AES256_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + "ECDHE_RSA_AES128_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + "ECDHE_ECDSA_AES256_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, + "ECDHE_ECDSA_AES128_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + "RSA_AES256_CBC_SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA, + "RSA_AES128_CBC_SHA": tls.TLS_RSA_WITH_AES_128_CBC_SHA, + "ECDHE_RSA_3DES_EDE_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, + "RSA_3DES_EDE_CBC_SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, +} + +// defaultCipherSuites is the ordered list of all the cipher +// suites we want to support by default, assuming AES-NI +// (hardware acceleration for AES). +var defaultCipherSuitesWithAESNI = []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, + tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, +} + +// defaultCipherSuites is the ordered list of all the cipher +// suites we want to support by default, assuming lack of +// AES-NI (NO hardware acceleration for AES). +var defaultCipherSuitesWithoutAESNI = []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, + tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, +} + +// getOptimalDefaultCipherSuites returns an appropriate cipher +// suite to use depending on the hardware support for AES. +// +// See https://github.com/mholt/caddy/issues/1674 +func getOptimalDefaultCipherSuites() []uint16 { + if cpuid.CPU.AesNi() { + return defaultCipherSuitesWithAESNI + } + return defaultCipherSuitesWithoutAESNI +} + +// supportedCurves is the unordered map of supported curves. +// https://golang.org/pkg/crypto/tls/#CurveID +var supportedCurves = map[string]tls.CurveID{ + "X25519": tls.X25519, + "P256": tls.CurveP256, + "P384": tls.CurveP384, + "P521": tls.CurveP521, +} + +// defaultCurves is the list of only the curves we want to use +// by default, in descending order of preference. +// +// This list should only include curves which are fast by design +// (e.g. X25519) and those for which an optimized assembly +// implementation exists (e.g. P256). The latter ones can be +// found here: +// https://github.com/golang/go/tree/master/src/crypto/elliptic +var defaultCurves = []tls.CurveID{ + tls.X25519, + tls.CurveP256, +} + +// supportedProtocols is a map of supported protocols. +// HTTP/2 only supports TLS 1.2 and higher. +var supportedProtocols = map[string]uint16{ + "tls1.0": tls.VersionTLS10, + "tls1.1": tls.VersionTLS11, + "tls1.2": tls.VersionTLS12, + "tls1.3": tls.VersionTLS13, +} + +// publicKeyAlgorithms is the map of supported public key algorithms. +var publicKeyAlgorithms = map[string]x509.PublicKeyAlgorithm{ + "rsa": x509.RSA, + "dsa": x509.DSA, + "ecdsa": x509.ECDSA, +} |