teleport/lib/services/authority.go

601 lines
18 KiB
Go
Raw Normal View History

2016-12-28 00:39:43 +00:00
package services
import (
2016-12-28 01:28:46 +00:00
"encoding/json"
"fmt"
"time"
2016-12-28 01:28:46 +00:00
"github.com/gravitational/teleport"
2016-12-28 23:50:32 +00:00
"github.com/gravitational/teleport/lib/defaults"
2016-12-28 01:28:46 +00:00
"github.com/gravitational/teleport/lib/utils"
2016-12-28 00:39:43 +00:00
"github.com/gravitational/trace"
2017-04-07 23:51:31 +00:00
"github.com/jonboulle/clockwork"
2016-12-28 01:28:46 +00:00
"golang.org/x/crypto/ssh"
2016-12-28 00:39:43 +00:00
)
2017-05-17 17:36:25 +00:00
// HostCertParams defines all parameters needed to generate a host certificate
type HostCertParams struct {
// PrivateCASigningKey is the private key of the CA that will sign the public key of the host
PrivateCASigningKey []byte
// PublicHostKey is the public key of the host
PublicHostKey []byte
// HostID is used by Teleport to uniquely identify a node within a cluster
HostID string
// NodeName is the DNS name of the node
NodeName string
// ClusterName is the name of the cluster within which a node lives
ClusterName string
// Roles identifies the roles of a Teleport instance
Roles teleport.Roles
// TTL defines how long a certificate is valid for
TTL time.Duration
}
2017-05-17 17:36:25 +00:00
func (c *HostCertParams) Check() error {
if c.HostID == "" || c.ClusterName == "" {
return trace.BadParameter("HostID [%q] and ClusterName [%q] are required",
c.HostID, c.ClusterName)
}
if err := c.Roles.Check(); err != nil {
return trace.Wrap(err)
}
return nil
}
2017-05-17 17:36:25 +00:00
// UserCertParams defines OpenSSH user certificate parameters
type UserCertParams struct {
// PrivateCASigningKey is the private key of the CA that will sign the public key of the user
PrivateCASigningKey []byte
// PublicUserKey is the public key of the user
PublicUserKey []byte
// TTL defines how long a certificate is valid for
TTL time.Duration
// Username is teleport username
Username string
// AllowedLogins is a list of SSH principals
AllowedLogins []string
// PermitAgentForwarding permits agent forwarding for this cert
PermitAgentForwarding bool
// Roles is a list of roles assigned to this user
Roles []string
}
// CertRoles defines certificate roles
type CertRoles struct {
// Version is current version of the roles
Version string `json:"version"`
// Roles is a list of roles
Roles []string `json:"roles"`
}
2017-05-18 03:43:21 +00:00
// CertRolesSchema defines cert roles schema
const CertRolesSchema = `{
"type": "object",
"additionalProperties": false,
"properties": {
"version": {"type": "string"},
"roles": {
"type": "array",
"items": {
"type": "string"
}
}
}
}`
2017-05-17 17:36:25 +00:00
// MarshalCertRoles marshal roles list to OpenSSH
func MarshalCertRoles(roles []string) (string, error) {
2017-05-19 17:06:48 +00:00
out, err := json.Marshal(CertRoles{Version: V1, Roles: roles})
2017-05-18 03:43:21 +00:00
if err != nil {
return "", trace.Wrap(err)
}
2017-05-17 17:36:25 +00:00
return string(out), err
}
2017-05-18 03:43:21 +00:00
// UnmarshalCertRoles marshals roles list to OpenSSH
func UnmarshalCertRoles(data string) ([]string, error) {
var certRoles CertRoles
if err := utils.UnmarshalWithSchema(CertRolesSchema, &certRoles, []byte(data)); err != nil {
return nil, trace.BadParameter(err.Error())
}
return certRoles.Roles, nil
2017-05-17 17:36:25 +00:00
}
2016-12-28 00:39:43 +00:00
// CertAuthority is a host or user certificate authority that
// can check and if it has private key stored as well, sign it too
type CertAuthority interface {
2017-04-07 23:51:31 +00:00
// Resource sets common resource properties
Resource
2016-12-29 02:47:33 +00:00
// GetID returns certificate authority ID -
// combined type and name
GetID() CertAuthID
2016-12-28 00:39:43 +00:00
// GetType returns user or host certificate authority
2016-12-28 22:07:03 +00:00
GetType() CertAuthType
2016-12-28 00:39:43 +00:00
// GetClusterName returns cluster name this cert authority
// is associated with
GetClusterName() string
// GetCheckingKeys returns public keys to check signature
GetCheckingKeys() [][]byte
2016-12-30 23:13:45 +00:00
// GetSigning keys returns signing keys
GetSigningKeys() [][]byte
2017-05-18 03:43:21 +00:00
// CombinedMapping is used to specify combined mapping from legacy property Roles
// and new property RoleMap
CombinedMapping() RoleMap
// GetRoleMap returns role map property
GetRoleMap() RoleMap
// SetRoleMap sets role map
SetRoleMap(m RoleMap)
2016-12-28 00:39:43 +00:00
// GetRoles returns a list of roles assumed by users signed by this CA
2016-12-28 22:07:03 +00:00
GetRoles() []string
2017-03-31 00:52:58 +00:00
// SetRoles sets assigned roles for this certificate authority
SetRoles(roles []string)
2016-12-28 00:39:43 +00:00
// FirstSigningKey returns first signing key or returns error if it's not here
// The first key is returned because multiple keys can exist during key rotation.
2016-12-28 00:39:43 +00:00
FirstSigningKey() ([]byte, error)
2016-12-28 01:28:46 +00:00
// GetRawObject returns raw object data, used for migrations
GetRawObject() interface{}
2016-12-29 02:47:33 +00:00
// Check checks object for errors
Check() error
// SetSigningKeys sets signing keys
SetSigningKeys([][]byte) error
2016-12-29 23:56:10 +00:00
// AddRole adds a role to ca role list
AddRole(name string)
2016-12-30 00:17:56 +00:00
// Checkers returns public keys that can be used to check cert authorities
Checkers() ([]ssh.PublicKey, error)
// Signers returns a list of signers that could be used to sign keys
Signers() ([]ssh.Signer, error)
2016-12-30 22:47:52 +00:00
// V1 returns V1 version of the resource
V1() *CertAuthorityV1
// V2 returns V2 version of the resource
V2() *CertAuthorityV2
}
// NewCertAuthority returns new cert authority
func NewCertAuthority(caType CertAuthType, clusterName string, signingKeys, checkingKeys [][]byte, roles []string) CertAuthority {
return &CertAuthorityV2{
Kind: KindCertAuthority,
Version: V2,
Metadata: Metadata{
Name: clusterName,
Namespace: defaults.Namespace,
},
Spec: CertAuthoritySpecV2{
Roles: roles,
Type: caType,
ClusterName: clusterName,
CheckingKeys: checkingKeys,
SigningKeys: signingKeys,
},
}
2016-12-28 00:39:43 +00:00
}
2016-12-30 21:25:35 +00:00
// CertAuthoritiesToV1 converts list of cert authorities to V1 slice
func CertAuthoritiesToV1(in []CertAuthority) ([]CertAuthorityV1, error) {
out := make([]CertAuthorityV1, len(in))
type cav1 interface {
V1() *CertAuthorityV1
}
for i, ca := range in {
v1, ok := ca.(cav1)
if !ok {
return nil, trace.BadParameter("could not transform object to V1")
}
out[i] = *(v1.V1())
}
return out, nil
}
2016-12-29 20:23:58 +00:00
// CertAuthorityV2 is version 1 resource spec for Cert Authority
type CertAuthorityV2 struct {
2016-12-28 00:39:43 +00:00
// Kind is a resource kind
Kind string `json:"kind"`
// Version is version
Version string `json:"version"`
// Metadata is connector metadata
Metadata Metadata `json:"metadata"`
// Spec contains cert authority specification
2016-12-29 20:23:58 +00:00
Spec CertAuthoritySpecV2 `json:"spec"`
2016-12-28 01:28:46 +00:00
// rawObject is object that is raw object stored in DB
2016-12-28 22:07:03 +00:00
// without any conversions applied, used in migrations
2016-12-28 01:28:46 +00:00
rawObject interface{}
2016-12-28 00:39:43 +00:00
}
2017-04-07 23:51:31 +00:00
// GetMetadata returns object metadata
func (c *CertAuthorityV2) GetMetadata() Metadata {
return c.Metadata
}
// SetExpiry sets expiry time for the object
func (c *CertAuthorityV2) SetExpiry(expires time.Time) {
c.Metadata.SetExpiry(expires)
}
// Expires retuns object expiry setting
func (c *CertAuthorityV2) Expiry() time.Time {
return c.Metadata.Expiry()
}
// SetTTL sets Expires header using realtime clock
func (c *CertAuthorityV2) SetTTL(clock clockwork.Clock, ttl time.Duration) {
c.Metadata.SetTTL(clock, ttl)
}
2016-12-29 23:16:42 +00:00
// V2 returns V2 version of the resouirce - itself
func (c *CertAuthorityV2) V2() *CertAuthorityV2 {
return c
}
// V1 returns V1 version of the object
func (c *CertAuthorityV2) V1() *CertAuthorityV1 {
return &CertAuthorityV1{
Type: c.Spec.Type,
DomainName: c.Spec.ClusterName,
CheckingKeys: c.Spec.CheckingKeys,
SigningKeys: c.Spec.SigningKeys,
}
}
2016-12-29 23:56:10 +00:00
// AddRole adds a role to ca role list
func (ca *CertAuthorityV2) AddRole(name string) {
for _, r := range ca.Spec.Roles {
if r == name {
return
}
}
ca.Spec.Roles = append(ca.Spec.Roles, name)
}
2016-12-30 23:13:45 +00:00
// GetSigning keys returns signing keys
func (ca *CertAuthorityV2) GetSigningKeys() [][]byte {
return ca.Spec.SigningKeys
}
2016-12-29 02:47:33 +00:00
// SetSigningKeys sets signing keys
2016-12-29 20:23:58 +00:00
func (ca *CertAuthorityV2) SetSigningKeys(keys [][]byte) error {
2016-12-29 02:47:33 +00:00
ca.Spec.SigningKeys = keys
return nil
}
// GetID returns certificate authority ID -
// combined type and name
2016-12-29 20:23:58 +00:00
func (ca *CertAuthorityV2) GetID() CertAuthID {
2016-12-29 02:47:33 +00:00
return CertAuthID{Type: ca.Spec.Type, DomainName: ca.Metadata.Name}
}
2017-04-07 23:51:31 +00:00
// SetName sets cert authority name
func (ca *CertAuthorityV2) SetName(name string) {
ca.Metadata.SetName(name)
}
2016-12-28 22:07:03 +00:00
// GetName returns cert authority name
2016-12-29 20:23:58 +00:00
func (ca *CertAuthorityV2) GetName() string {
2016-12-28 22:07:03 +00:00
return ca.Metadata.Name
}
// GetType returns user or host certificate authority
2016-12-29 20:23:58 +00:00
func (ca *CertAuthorityV2) GetType() CertAuthType {
2016-12-28 22:07:03 +00:00
return ca.Spec.Type
}
// GetClusterName returns cluster name this cert authority
// is associated with
2016-12-29 20:23:58 +00:00
func (ca *CertAuthorityV2) GetClusterName() string {
2016-12-28 22:07:03 +00:00
return ca.Spec.ClusterName
}
// GetCheckingKeys returns public keys to check signature
2016-12-29 20:23:58 +00:00
func (ca *CertAuthorityV2) GetCheckingKeys() [][]byte {
2016-12-28 22:07:03 +00:00
return ca.Spec.CheckingKeys
}
// GetRoles returns a list of roles assumed by users signed by this CA
2016-12-29 20:23:58 +00:00
func (ca *CertAuthorityV2) GetRoles() []string {
2016-12-28 22:07:03 +00:00
return ca.Spec.Roles
}
2017-03-31 00:52:58 +00:00
// SetRoles sets assigned roles for this certificate authority
func (ca *CertAuthorityV2) SetRoles(roles []string) {
ca.Spec.Roles = roles
}
2017-05-18 03:43:21 +00:00
// CombinedMapping is used to specify combined mapping from legacy property Roles
// and new property RoleMap
func (ca *CertAuthorityV2) CombinedMapping() RoleMap {
if len(ca.Spec.Roles) != 0 {
return []RoleMapping{{Remote: Wildcard, Local: ca.Spec.Roles}}
}
return ca.Spec.RoleMap
}
// GetRoleMap returns role map property
func (ca *CertAuthorityV2) GetRoleMap() RoleMap {
return ca.Spec.RoleMap
}
// SetRoleMap sets role map
func (c *CertAuthorityV2) SetRoleMap(m RoleMap) {
c.Spec.RoleMap = m
}
2016-12-28 22:07:03 +00:00
// GetRawObject returns raw object data, used for migrations
2016-12-29 20:23:58 +00:00
func (ca *CertAuthorityV2) GetRawObject() interface{} {
2016-12-28 22:07:03 +00:00
return ca.rawObject
}
2016-12-28 00:39:43 +00:00
// FirstSigningKey returns first signing key or returns error if it's not here
2016-12-29 20:23:58 +00:00
func (ca *CertAuthorityV2) FirstSigningKey() ([]byte, error) {
2016-12-28 00:39:43 +00:00
if len(ca.Spec.SigningKeys) == 0 {
return nil, trace.NotFound("%v has no signing keys", ca.Metadata.Name)
}
return ca.Spec.SigningKeys[0], nil
}
// ID returns id (consisting of domain name and type) that
// identifies the authority this key belongs to
2016-12-29 20:23:58 +00:00
func (ca *CertAuthorityV2) ID() *CertAuthID {
2016-12-28 00:39:43 +00:00
return &CertAuthID{DomainName: ca.Spec.ClusterName, Type: ca.Spec.Type}
}
// Checkers returns public keys that can be used to check cert authorities
2016-12-29 20:23:58 +00:00
func (ca *CertAuthorityV2) Checkers() ([]ssh.PublicKey, error) {
2016-12-28 00:39:43 +00:00
out := make([]ssh.PublicKey, 0, len(ca.Spec.CheckingKeys))
for _, keyBytes := range ca.Spec.CheckingKeys {
key, _, _, _, err := ssh.ParseAuthorizedKey(keyBytes)
if err != nil {
return nil, trace.BadParameter("invalid authority public key (len=%d): %v", len(keyBytes), err)
}
out = append(out, key)
}
return out, nil
}
// Signers returns a list of signers that could be used to sign keys
2016-12-29 20:23:58 +00:00
func (ca *CertAuthorityV2) Signers() ([]ssh.Signer, error) {
2016-12-28 00:39:43 +00:00
out := make([]ssh.Signer, 0, len(ca.Spec.SigningKeys))
for _, keyBytes := range ca.Spec.SigningKeys {
signer, err := ssh.ParsePrivateKey(keyBytes)
if err != nil {
return nil, trace.Wrap(err)
}
out = append(out, signer)
}
return out, nil
}
// Check checks if all passed parameters are valid
2016-12-29 20:23:58 +00:00
func (ca *CertAuthorityV2) Check() error {
2016-12-28 00:39:43 +00:00
err := ca.ID().Check()
if err != nil {
return trace.Wrap(err)
}
_, err = ca.Checkers()
if err != nil {
return trace.Wrap(err)
}
_, err = ca.Signers()
if err != nil {
return trace.Wrap(err)
}
2017-05-18 03:43:21 +00:00
// This is to force users to migrate
if len(ca.Spec.Roles) != 0 && len(ca.Spec.RoleMap) != 0 {
return trace.BadParameter("should set either 'roles' or 'role_map', not both")
}
if err := ca.Spec.RoleMap.Check(); err != nil {
return trace.Wrap(err)
}
2016-12-28 00:39:43 +00:00
return nil
}
2016-12-29 20:23:58 +00:00
// CertAuthoritySpecV2 is a host or user certificate authority that
2016-12-28 00:39:43 +00:00
// can check and if it has private key stored as well, sign it too
2016-12-29 20:23:58 +00:00
type CertAuthoritySpecV2 struct {
2016-12-28 00:39:43 +00:00
// Type is either user or host certificate authority
Type CertAuthType `json:"type"`
// ClusterName identifies cluster name this authority serves,
// for host authorities that means base hostname of all servers,
// for user authorities that means organization name
ClusterName string `json:"cluster_name"`
// Checkers is a list of SSH public keys that can be used to check
// certificate signatures
CheckingKeys [][]byte `json:"checking_keys"`
// SigningKeys is a list of private keys used for signing
2016-12-30 20:20:48 +00:00
SigningKeys [][]byte `json:"signing_keys,omitempty"`
2016-12-28 00:39:43 +00:00
// Roles is a list of roles assumed by users signed by this CA
2016-12-29 02:47:33 +00:00
Roles []string `json:"roles,omitempty"`
2017-05-18 03:43:21 +00:00
// RoleMap specifies role mappings to remote roles
RoleMap RoleMap `json:"role_map,omitempty"`
2016-12-28 00:39:43 +00:00
}
2016-12-29 20:23:58 +00:00
// CertAuthoritySpecV2Schema is JSON schema for cert authority V2
const CertAuthoritySpecV2Schema = `{
2016-12-28 00:39:43 +00:00
"type": "object",
"additionalProperties": false,
2016-12-30 20:20:48 +00:00
"required": ["type", "cluster_name", "checking_keys"],
2016-12-28 00:39:43 +00:00
"properties": {
"type": {"type": "string"},
"cluster_name": {"type": "string"},
"checking_keys": {
"type": "array",
"items": {
2016-12-29 02:47:33 +00:00
"type": "string"
2016-12-28 00:39:43 +00:00
}
},
"signing_keys": {
"type": "array",
"items": {
2016-12-29 02:47:33 +00:00
"type": "string"
2016-12-28 00:39:43 +00:00
}
},
"roles": {
"type": "array",
"items": {
"type": "string"
}
2017-05-18 03:43:21 +00:00
},
"role_map": %v
2016-12-28 00:39:43 +00:00
}
}`
2016-12-29 23:16:42 +00:00
// CertAuthorityV1 is a host or user certificate authority that
2016-12-28 00:39:43 +00:00
// can check and if it has private key stored as well, sign it too
2016-12-29 23:16:42 +00:00
type CertAuthorityV1 struct {
2016-12-28 00:39:43 +00:00
// Type is either user or host certificate authority
Type CertAuthType `json:"type"`
// DomainName identifies domain name this authority serves,
// for host authorities that means base hostname of all servers,
// for user authorities that means organization name
DomainName string `json:"domain_name"`
// Checkers is a list of SSH public keys that can be used to check
// certificate signatures
CheckingKeys [][]byte `json:"checking_keys"`
// SigningKeys is a list of private keys used for signing
SigningKeys [][]byte `json:"signing_keys"`
// AllowedLogins is a list of allowed logins for users within
// this certificate authority
AllowedLogins []string `json:"allowed_logins"`
}
2016-12-28 01:28:46 +00:00
2017-05-18 03:43:21 +00:00
// CombinedMapping is used to specify combined mapping from legacy property Roles
// and new property RoleMap
func (ca *CertAuthorityV1) CombinedMapping() RoleMap {
return []RoleMapping{}
}
// GetRoleMap returns role map property
func (ca *CertAuthorityV1) GetRoleMap() RoleMap {
return nil
}
// SetRoleMap sets role map
func (c *CertAuthorityV1) SetRoleMap(m RoleMap) {
}
2016-12-29 23:16:42 +00:00
// V1 returns V1 version of the resource
func (c *CertAuthorityV1) V1() *CertAuthorityV1 {
return c
}
// V2 returns V2 version of the resource
func (c *CertAuthorityV1) V2() *CertAuthorityV2 {
2016-12-29 20:23:58 +00:00
return &CertAuthorityV2{
2016-12-28 01:28:46 +00:00
Kind: KindCertAuthority,
2016-12-29 20:23:58 +00:00
Version: V2,
2016-12-28 01:28:46 +00:00
Metadata: Metadata{
2016-12-28 23:50:32 +00:00
Name: c.DomainName,
Namespace: defaults.Namespace,
2016-12-28 01:28:46 +00:00
},
2016-12-29 20:23:58 +00:00
Spec: CertAuthoritySpecV2{
2016-12-28 01:28:46 +00:00
Type: c.Type,
ClusterName: c.DomainName,
CheckingKeys: c.CheckingKeys,
SigningKeys: c.SigningKeys,
},
rawObject: *c,
}
}
var certAuthorityMarshaler CertAuthorityMarshaler = &TeleportCertAuthorityMarshaler{}
// SetCertAuthorityMarshaler sets global user marshaler
func SetCertAuthorityMarshaler(u CertAuthorityMarshaler) {
marshalerMutex.Lock()
defer marshalerMutex.Unlock()
certAuthorityMarshaler = u
}
// GetCertAuthorityMarshaler returns currently set user marshaler
func GetCertAuthorityMarshaler() CertAuthorityMarshaler {
marshalerMutex.RLock()
defer marshalerMutex.RUnlock()
return certAuthorityMarshaler
}
// CertAuthorityMarshaler implements marshal/unmarshal of User implementations
// mostly adds support for extended versions
type CertAuthorityMarshaler interface {
// UnmarshalCertAuthority unmarhsals cert authority from binary representation
2016-12-28 22:07:03 +00:00
UnmarshalCertAuthority(bytes []byte) (CertAuthority, error)
2016-12-28 01:28:46 +00:00
// MarshalCertAuthority to binary representation
2016-12-29 23:16:42 +00:00
MarshalCertAuthority(c CertAuthority, opts ...MarshalOption) ([]byte, error)
2017-02-11 02:55:51 +00:00
// GenerateCertAuthority is used to generate new cert authority
// based on standard teleport one and is used to add custom
// parameters and extend it in extensions of teleport
GenerateCertAuthority(CertAuthority) (CertAuthority, error)
2016-12-28 01:28:46 +00:00
}
// GetCertAuthoritySchema returns JSON Schema for cert authorities
func GetCertAuthoritySchema() string {
2017-05-18 03:43:21 +00:00
return fmt.Sprintf(V2SchemaTemplate, MetadataSchema, fmt.Sprintf(CertAuthoritySpecV2Schema, RoleMapSchema))
2016-12-28 01:28:46 +00:00
}
type TeleportCertAuthorityMarshaler struct{}
2017-02-11 02:55:51 +00:00
// GenerateCertAuthority is used to generate new cert authority
// based on standard teleport one and is used to add custom
// parameters and extend it in extensions of teleport
func (*TeleportCertAuthorityMarshaler) GenerateCertAuthority(ca CertAuthority) (CertAuthority, error) {
return ca, nil
}
2016-12-28 01:28:46 +00:00
// UnmarshalUser unmarshals user from JSON
2016-12-28 22:07:03 +00:00
func (*TeleportCertAuthorityMarshaler) UnmarshalCertAuthority(bytes []byte) (CertAuthority, error) {
2016-12-28 01:28:46 +00:00
var h ResourceHeader
err := json.Unmarshal(bytes, &h)
if err != nil {
return nil, trace.Wrap(err)
}
switch h.Version {
case "":
2016-12-29 23:16:42 +00:00
var ca CertAuthorityV1
2016-12-28 01:28:46 +00:00
err := json.Unmarshal(bytes, &ca)
if err != nil {
return nil, trace.Wrap(err)
}
2016-12-29 20:23:58 +00:00
return ca.V2(), nil
case V2:
var ca CertAuthorityV2
2016-12-28 01:28:46 +00:00
if err := utils.UnmarshalWithSchema(GetCertAuthoritySchema(), &ca, bytes); err != nil {
return nil, trace.BadParameter(err.Error())
}
2017-04-07 23:51:31 +00:00
utils.UTC(&ca.Metadata.Expires)
2016-12-29 02:54:10 +00:00
return &ca, nil
2016-12-28 01:28:46 +00:00
}
2016-12-29 02:54:10 +00:00
return nil, trace.BadParameter("cert authority resource version %v is not supported", h.Version)
2016-12-28 01:28:46 +00:00
}
// MarshalUser marshalls cert authority into JSON
2016-12-29 23:16:42 +00:00
func (*TeleportCertAuthorityMarshaler) MarshalCertAuthority(ca CertAuthority, opts ...MarshalOption) ([]byte, error) {
cfg, err := collectOptions(opts)
if err != nil {
return nil, trace.Wrap(err)
}
type cav1 interface {
V1() *CertAuthorityV1
}
type cav2 interface {
V2() *CertAuthorityV2
}
version := cfg.GetVersion()
switch version {
case V1:
v, ok := ca.(cav1)
if !ok {
return nil, trace.BadParameter("don't know how to marshal %v", V1)
}
return json.Marshal(v.V1())
case V2:
v, ok := ca.(cav2)
if !ok {
return nil, trace.BadParameter("don't know how to marshal %v", V2)
}
return json.Marshal(v.V2())
default:
return nil, trace.BadParameter("version %v is not supported", version)
}
2016-12-28 01:28:46 +00:00
}