2018-03-17 04:20:54 +00:00
package structs
2018-03-20 04:00:01 +00:00
import (
2018-07-20 23:04:04 +00:00
"fmt"
2018-09-13 14:43:00 +00:00
"reflect"
2018-03-21 17:55:39 +00:00
"time"
2018-07-20 23:04:04 +00:00
"github.com/mitchellh/mapstructure"
2021-07-05 23:10:23 +00:00
"github.com/hashicorp/consul/lib"
2018-03-20 04:00:01 +00:00
)
2020-07-23 20:05:28 +00:00
const (
DefaultLeafCertTTL = "72h"
2021-11-02 18:02:10 +00:00
DefaultIntermediateCertTTL = "8760h" // ~ 1 year = 365 * 24h
DefaultRootCertTTL = "87600h" // ~ 10 years = 365 * 24h * 10
2020-07-23 20:05:28 +00:00
)
2018-03-17 04:20:54 +00:00
// IndexedCARoots is the list of currently trusted CA Roots.
type IndexedCARoots struct {
// ActiveRootID is the ID of a root in Roots that is the active CA root.
// Other roots are still valid if they're in the Roots list but are in
// the process of being rotated out.
ActiveRootID string
2018-05-08 13:23:44 +00:00
// TrustDomain is the identification root for this Consul cluster. All
// certificates signed by the cluster's CA must have their identifying URI in
// this domain.
//
// This does not include the protocol (currently spiffe://) since we may
// implement other protocols in future with equivalent semantics. It should be
// compared against the "authority" section of a URI (i.e. host:port).
//
2018-11-12 20:20:12 +00:00
// We need to support migrating a cluster between trust domains to support
// Multi-DC migration in Enterprise. In this case the current trust domain is
// here but entries in Roots may also have ExternalTrustDomain set to a
// non-empty value implying they were previous roots that are still trusted
// but under a different trust domain.
//
// Note that we DON'T validate trust domain during AuthZ since it causes
// issues of loss of connectivity during migration between trust domains. The
// only time the additional validation adds value is where the cluster shares
// an external root (e.g. organization-wide root) with another distinct Consul
// cluster or PKI system. In this case, x509 Name Constraints can be added to
// enforce that Consul's CA can only validly sign or trust certs within the
// same trust-domain. Name constraints as enforced by TLS handshake also allow
// seamless rotation between trust domains thanks to cross-signing.
2018-05-08 13:23:44 +00:00
TrustDomain string
2018-03-17 04:20:54 +00:00
// Roots is a list of root CA certs to trust.
Roots [ ] * CARoot
2018-03-19 05:07:52 +00:00
// QueryMeta contains the meta sent via a header. We ignore for JSON
// so this whole structure can be returned.
QueryMeta ` json:"-" `
2018-03-17 04:20:54 +00:00
}
// CARoot represents a root CA certificate that is trusted.
type CARoot struct {
// ID is a globally unique ID (UUID) representing this CA root.
ID string
// Name is a human-friendly name for this CA root. This value is
// opaque to Consul and is not used for anything internally.
Name string
2018-05-04 23:01:54 +00:00
// SerialNumber is the x509 serial number of the certificate.
SerialNumber uint64
2019-01-10 12:46:11 +00:00
// SigningKeyID is the ID of the public key that corresponds to the private
2019-09-26 16:54:14 +00:00
// key used to sign leaf certificates. Is is the HexString format of the
// raw AuthorityKeyID bytes.
2018-05-04 23:01:54 +00:00
SigningKeyID string
2018-11-12 20:20:12 +00:00
// ExternalTrustDomain is the trust domain this root was generated under. It
// is usually empty implying "the current cluster trust-domain". It is set
// only in the case that a cluster changes trust domain and then all old roots
// that are still trusted have the old trust domain set here.
//
// We currently DON'T validate these trust domains explicitly anywhere, see
// IndexedRoots.TrustDomain doc. We retain this information for debugging and
// future flexibility.
2018-10-15 16:17:48 +00:00
ExternalTrustDomain string
2018-05-04 23:01:54 +00:00
// Time validity bounds.
NotBefore time . Time
NotAfter time . Time
2021-11-23 17:49:43 +00:00
// RootCert is the PEM-encoded public certificate for the root CA. The
// certificate is the same for all federated clusters.
2018-03-17 04:20:54 +00:00
RootCert string
2018-04-24 18:50:31 +00:00
// IntermediateCerts is a list of PEM-encoded intermediate certs to
2021-11-23 17:49:43 +00:00
// attach to any leaf certs signed by this CA. The list may include a
// certificate cross-signed by an old root CA, any subordinate CAs below the
// root CA, and the intermediate CA used to sign leaf certificates in the
// local Datacenter.
//
// If the provider which created this root uses an intermediate to sign
// leaf certificates (Vault provider), or this is a secondary Datacenter then
// the intermediate used to sign leaf certificates will be the last in the
// list.
2018-04-24 18:50:31 +00:00
IntermediateCerts [ ] string
2018-04-21 03:39:51 +00:00
2018-03-17 04:20:54 +00:00
// SigningCert is the PEM-encoded signing certificate and SigningKey
2018-03-19 21:36:17 +00:00
// is the PEM-encoded private key for the signing certificate. These
// may actually be empty if the CA plugin in use manages these for us.
2018-03-21 19:55:43 +00:00
SigningCert string ` json:",omitempty" `
SigningKey string ` json:",omitempty" `
2018-03-17 04:20:54 +00:00
2018-03-20 03:29:14 +00:00
// Active is true if this is the current active CA. This must only
// be true for exactly one CA. For any method that modifies roots in the
// state store, tests should be written to verify that multiple roots
// cannot be active.
Active bool
2018-07-06 23:05:25 +00:00
// RotatedOutAt is the time at which this CA was removed from the state.
2018-06-21 22:42:28 +00:00
// This will only be set on roots that have been rotated out from being the
2018-07-06 23:05:25 +00:00
// active root.
RotatedOutAt time . Time ` json:"-" `
2018-06-21 22:42:28 +00:00
2019-11-01 13:20:26 +00:00
// PrivateKeyType is the type of the private key used to sign certificates. It
// may be "rsa" or "ec". This is provided as a convenience to avoid parsing
// the public key to from the certificate to infer the type.
2019-07-30 21:47:39 +00:00
PrivateKeyType string
2019-11-01 13:20:26 +00:00
// PrivateKeyBits is the length of the private key used to sign certificates.
// This is provided as a convenience to avoid parsing the public key from the
// certificate to infer the type.
2019-07-30 21:47:39 +00:00
PrivateKeyBits int
2018-03-17 04:20:54 +00:00
RaftIndex
}
2020-11-17 23:35:33 +00:00
func ( c * CARoot ) Clone ( ) * CARoot {
if c == nil {
return nil
}
newCopy := * c
2020-11-20 04:08:06 +00:00
newCopy . IntermediateCerts = CloneStringSlice ( c . IntermediateCerts )
2020-11-17 23:35:33 +00:00
return & newCopy
}
2018-03-17 04:20:54 +00:00
// CARoots is a list of CARoot structures.
type CARoots [ ] * CARoot
2018-03-19 21:36:17 +00:00
// CASignRequest is the request for signing a service certificate.
type CASignRequest struct {
// Datacenter is the target for this request.
Datacenter string
// CSR is the PEM-encoded CSR.
CSR string
// WriteRequest is a common struct containing ACL tokens and other
// write-related common elements for requests.
WriteRequest
}
// RequestDatacenter returns the datacenter for a given request.
func ( q * CASignRequest ) RequestDatacenter ( ) string {
return q . Datacenter
}
2018-03-20 04:00:01 +00:00
// IssuedCert is a certificate that has been issued by a Connect CA.
type IssuedCert struct {
// SerialNumber is the unique serial number for this certificate.
2018-03-21 22:54:51 +00:00
// This is encoded in standard hex separated by :.
SerialNumber string
2018-03-20 04:00:01 +00:00
2018-03-21 17:55:39 +00:00
// CertPEM and PrivateKeyPEM are the PEM-encoded certificate and private
// key for that cert, respectively. This should not be stored in the
2018-03-20 04:00:01 +00:00
// state store, but is present in the sign API response.
2018-03-21 17:55:39 +00:00
CertPEM string ` json:",omitempty" `
2018-03-21 19:55:43 +00:00
PrivateKeyPEM string ` json:",omitempty" `
2018-03-21 17:55:39 +00:00
// Service is the name of the service for which the cert was issued.
// ServiceURI is the cert URI value.
2019-06-27 20:22:07 +00:00
Service string ` json:",omitempty" `
ServiceURI string ` json:",omitempty" `
// Agent is the name of the node for which the cert was issued.
// AgentURI is the cert URI value.
Agent string ` json:",omitempty" `
AgentURI string ` json:",omitempty" `
2018-03-21 17:55:39 +00:00
// ValidAfter and ValidBefore are the validity periods for the
// certificate.
ValidAfter time . Time
ValidBefore time . Time
2020-02-03 14:26:47 +00:00
// EnterpriseMeta is the Consul Enterprise specific metadata
EnterpriseMeta
2018-03-21 17:55:39 +00:00
RaftIndex
2018-03-20 04:00:01 +00:00
}
2018-03-21 17:10:53 +00:00
// CAOp is the operation for a request related to intentions.
type CAOp string
const (
2020-01-09 15:32:19 +00:00
CAOpSetRoots CAOp = "set-roots"
CAOpSetConfig CAOp = "set-config"
CAOpSetProviderState CAOp = "set-provider-state"
CAOpDeleteProviderState CAOp = "delete-provider-state"
CAOpSetRootsAndConfig CAOp = "set-roots-config"
CAOpIncrementProviderSerialNumber CAOp = "increment-provider-serial"
2018-03-21 17:10:53 +00:00
)
// CARequest is used to modify connect CA data. This is used by the
// FSM (agent/consul/fsm) to apply changes.
type CARequest struct {
// Op is the type of operation being requested. This determines what
// other fields are required.
Op CAOp
2018-04-09 04:58:31 +00:00
// Datacenter is the target for this request.
Datacenter string
2018-04-20 08:30:34 +00:00
// Index is used by CAOpSetRoots and CAOpSetConfig for a CAS operation.
2018-03-21 17:10:53 +00:00
Index uint64
// Roots is a list of roots. This is used for CAOpSet. One root must
// always be active.
Roots [ ] * CARoot
2018-04-09 04:58:31 +00:00
// Config is the configuration for the current CA plugin.
Config * CAConfiguration
2018-04-20 08:30:34 +00:00
// ProviderState is the state for the builtin CA provider.
ProviderState * CAConsulProviderState
2018-04-09 04:58:31 +00:00
// WriteRequest is a common struct containing ACL tokens and other
// write-related common elements for requests.
WriteRequest
2018-03-21 17:10:53 +00:00
}
2018-03-21 19:42:42 +00:00
2018-04-09 04:58:31 +00:00
// RequestDatacenter returns the datacenter for a given request.
func ( q * CARequest ) RequestDatacenter ( ) string {
return q . Datacenter
}
const (
ConsulCAProvider = "consul"
2018-06-13 08:40:03 +00:00
VaultCAProvider = "vault"
2019-11-21 17:40:29 +00:00
AWSCAProvider = "aws-pca"
2018-04-09 04:58:31 +00:00
)
2018-03-21 19:42:42 +00:00
// CAConfiguration is the configuration for the current CA plugin.
type CAConfiguration struct {
2018-04-24 18:50:31 +00:00
// ClusterID is a unique identifier for the cluster
ClusterID string ` json:"-" `
2018-04-20 08:30:34 +00:00
2018-03-21 19:42:42 +00:00
// Provider is the CA provider implementation to use.
Provider string
// Configuration is arbitrary configuration for the provider. This
// should only contain primitive values and containers (such as lists
// and maps).
Config map [ string ] interface { }
2018-04-09 04:58:31 +00:00
2019-11-11 20:57:16 +00:00
// State is optionally used by the provider to persist information it needs
// between reloads like UUIDs of resources it manages. It only supports string
// values to avoid gotchas with interface{} since this is encoded through
// msgpack when it's written through raft. For example if providers used a
// custom struct or even a simple `int` type, msgpack with loose type
// information during encode/decode and providers will end up getting back
// different types have have to remember to test multiple variants of state
// handling to account for cases where it's been through msgpack or not.
// Keeping this as strings only forces compatibility and leaves the input
// Providers have to work with unambiguous - they can parse ints or other
// types as they need. We expect this only to be used to store a handful of
// identifiers anyway so this is simpler.
State map [ string ] string
2019-11-11 21:36:22 +00:00
// ForceWithoutCrossSigning indicates that the CA reconfiguration should go
// ahead even if the current CA is unable to cross sign certificates. This
// risks temporary connection failures during the rollout as new leafs will be
// rejected by proxies that have not yet observed the new root cert but is the
// only option if a CA that doesn't support cross signing needs to be
// reconfigured or mirated away from.
ForceWithoutCrossSigning bool
2018-04-20 08:30:34 +00:00
RaftIndex
}
2019-11-11 21:36:22 +00:00
func ( c * CAConfiguration ) UnmarshalJSON ( data [ ] byte ) ( err error ) {
type Alias CAConfiguration
aux := & struct {
ForceWithoutCrossSigningSnake bool ` json:"force_without_cross_signing" `
* Alias
} {
Alias : ( * Alias ) ( c ) ,
}
2019-12-06 16:14:56 +00:00
if err = lib . UnmarshalJSON ( data , & aux ) ; err != nil {
2019-11-11 21:36:22 +00:00
return err
}
if aux . ForceWithoutCrossSigningSnake {
c . ForceWithoutCrossSigning = aux . ForceWithoutCrossSigningSnake
}
2019-11-11 20:57:16 +00:00
return nil
}
2018-07-20 23:04:04 +00:00
func ( c * CAConfiguration ) GetCommonConfig ( ) ( * CommonCAProviderConfig , error ) {
if c == nil {
return nil , fmt . Errorf ( "config map was nil" )
}
var config CommonCAProviderConfig
2019-01-22 17:19:36 +00:00
// Set Defaults
config . CSRMaxPerSecond = 50 // See doc comment for rationale here.
2018-07-20 23:04:04 +00:00
decodeConf := & mapstructure . DecoderConfig {
2018-09-13 14:43:00 +00:00
DecodeHook : ParseDurationFunc ( ) ,
Result : & config ,
WeaklyTypedInput : true ,
2018-07-20 23:04:04 +00:00
}
decoder , err := mapstructure . NewDecoder ( decodeConf )
if err != nil {
return nil , err
}
if err := decoder . Decode ( c . Config ) ; err != nil {
return nil , fmt . Errorf ( "error decoding config: %s" , err )
}
return & config , nil
}
2018-07-16 09:46:10 +00:00
type CommonCAProviderConfig struct {
2020-09-10 06:04:56 +00:00
LeafCertTTL time . Duration
IntermediateCertTTL time . Duration
2021-11-02 18:02:10 +00:00
RootCertTTL time . Duration
2018-07-20 23:04:04 +00:00
SkipValidate bool
2019-01-22 17:19:36 +00:00
// CSRMaxPerSecond is a rate limit on processing Connect Certificate Signing
// Requests on the servers. It applies to all CA providers so can be used to
// limit rate to an external CA too. 0 disables the rate limit. Defaults to 50
// which is low enough to prevent overload of a reasonably sized production
// server while allowing a cluster with 1000 service instances to complete a
// rotation in 20 seconds. For reference a quad-core 2017 MacBook pro can
// process 100 signing RPCs a second while using less than half of one core.
// For large clusters with powerful servers it's advisable to increase this
// rate or to disable this limit and instead rely on CSRMaxConcurrent to only
// consume a subset of the server's cores.
CSRMaxPerSecond float32
// CSRMaxConcurrent is a limit on how many concurrent CSR signing requests
// will be processed in parallel. New incoming signing requests will try for
// `consul.csrSemaphoreWait` (currently 500ms) for a slot before being
// rejected with a "rate limited" backpressure response. This effectively sets
// how many CPU cores can be occupied by Connect CA signing activity and
// should be a (small) subset of your server's available cores to allow other
// tasks to complete when a barrage of CSRs come in (e.g. after a CA root
// rotation). Setting to 0 disables the limit, attempting to sign certs
// immediately in the RPC goroutine. This is 0 by default and CSRMaxPerSecond
// is used. This is ignored if CSRMaxPerSecond is non-zero.
CSRMaxConcurrent int
2019-07-30 21:47:39 +00:00
2019-11-01 13:20:26 +00:00
// PrivateKeyType specifies which type of key the CA should generate. It only
// applies when the provider is generating its own key and is ignored if the
// provider already has a key or an external key is provided. Supported values
// are "ec" or "rsa". "ec" is the default and will generate a NIST P-256
// Elliptic key.
2019-07-30 21:47:39 +00:00
PrivateKeyType string
2019-11-01 13:20:26 +00:00
// PrivateKeyBits specifies the number of bits the CA's private key should
// use. For RSA, supported values are 2048 and 4096. For EC, supported values
// are 224, 256, 384 and 521 and correspond to the NIST P-* curve of the same
// name. As with PrivateKeyType this is only relevant whan the provier is
// generating new CA keys (root or intermediate).
2019-07-30 21:47:39 +00:00
PrivateKeyBits int
2018-07-20 23:04:04 +00:00
}
2020-02-10 23:05:49 +00:00
var MinLeafCertTTL = time . Hour
var MaxLeafCertTTL = 365 * 24 * time . Hour
2020-09-10 06:04:56 +00:00
// intermediateCertRenewInterval is the interval at which the expiration
// of the intermediate cert is checked and renewed if necessary.
var IntermediateCertRenewInterval = time . Hour
2018-07-20 23:04:04 +00:00
func ( c CommonCAProviderConfig ) Validate ( ) error {
if c . SkipValidate {
return nil
}
2021-11-02 18:02:10 +00:00
// it's sufficient to check that the root cert ttl >= intermediate cert ttl
// since intermediate cert ttl >= 3* leaf cert ttl; so root cert ttl >= 3 * leaf cert ttl > leaf cert ttl
if c . RootCertTTL < c . IntermediateCertTTL {
return fmt . Errorf ( "root cert TTL is set and is not greater than intermediate cert ttl. root cert ttl: %s, intermediate cert ttl: %s" , c . RootCertTTL , c . IntermediateCertTTL )
}
2020-02-10 23:05:49 +00:00
if c . LeafCertTTL < MinLeafCertTTL {
return fmt . Errorf ( "leaf cert TTL must be greater or equal than %s" , MinLeafCertTTL )
2018-07-20 23:04:04 +00:00
}
2020-02-10 23:05:49 +00:00
if c . LeafCertTTL > MaxLeafCertTTL {
return fmt . Errorf ( "leaf cert TTL must be less than %s" , MaxLeafCertTTL )
2018-07-20 23:04:04 +00:00
}
2020-09-10 06:04:56 +00:00
if c . IntermediateCertTTL < ( 3 * IntermediateCertRenewInterval ) {
// Intermediate Certificates are checked every
// hour(intermediateCertRenewInterval) if they are about to
// expire. Recreating an intermediate certs is started once
// more than half its lifetime has passed.
// If it would be 2h, worst case is that the check happens
// right before half time and when the check happens again, the
// certificate is very close to expiring, leaving only a small
// timeframe to renew. 3h leaves more than 30min to recreate.
// Right now the minimum LeafCertTTL is 1h, which means this
// check not strictly needed, because the same thing is covered
// in the next check too. But just in case minimum LeafCertTTL
// changes at some point, this validation must still be
// performed.
return fmt . Errorf ( "Intermediate Cert TTL must be greater or equal than %dh" , 3 * int ( IntermediateCertRenewInterval . Hours ( ) ) )
}
if c . IntermediateCertTTL < ( 3 * c . LeafCertTTL ) {
// Intermediate Certificates are being sent to the proxy when
// the Leaf Certificate changes because they are bundled
// together.
// That means that the Intermediate Certificate TTL must be at
// a minimum of 3 * Leaf Certificate TTL to ensure that the new
// Intermediate is being set together with the Leaf Certificate
// before it expires.
return fmt . Errorf ( "Intermediate Cert TTL must be greater or equal than 3 * LeafCertTTL (>=%s)." , 3 * c . LeafCertTTL )
}
2019-07-30 21:47:39 +00:00
switch c . PrivateKeyType {
case "ec" :
if c . PrivateKeyBits != 224 && c . PrivateKeyBits != 256 && c . PrivateKeyBits != 384 && c . PrivateKeyBits != 521 {
2019-11-01 13:20:26 +00:00
return fmt . Errorf ( "EC key length must be one of (224, 256, 384, 521) bits" )
2019-07-30 21:47:39 +00:00
}
case "rsa" :
if c . PrivateKeyBits != 2048 && c . PrivateKeyBits != 4096 {
return fmt . Errorf ( "RSA key length must be 2048 or 4096 bits" )
}
default :
2019-11-01 13:20:26 +00:00
return fmt . Errorf ( "private key type must be either 'ec' or 'rsa'" )
2019-07-30 21:47:39 +00:00
}
2018-07-20 23:04:04 +00:00
return nil
2018-07-16 09:46:10 +00:00
}
2018-04-25 00:14:30 +00:00
type ConsulCAProviderConfig struct {
2018-07-16 09:46:10 +00:00
CommonCAProviderConfig ` mapstructure:",squash" `
2021-07-05 23:10:23 +00:00
PrivateKey string
RootCert string
2019-11-11 21:36:22 +00:00
// DisableCrossSigning is really only useful in test code to use the built in
// provider while exercising logic that depends on the CA provider ability to
// cross sign. We don't document this config field publicly or make any
// attempt to parse it from snake case unlike other fields here.
DisableCrossSigning bool
2018-04-25 00:14:30 +00:00
}
2020-02-10 23:05:49 +00:00
func ( c * ConsulCAProviderConfig ) Validate ( ) error {
return nil
}
2018-04-20 08:30:34 +00:00
// CAConsulProviderState is used to track the built-in Consul CA provider's state.
type CAConsulProviderState struct {
2018-09-07 02:18:54 +00:00
ID string
PrivateKey string
RootCert string
IntermediateCert string
2018-04-20 08:30:34 +00:00
RaftIndex
2018-03-21 19:42:42 +00:00
}
2018-06-13 08:40:03 +00:00
type VaultCAProviderConfig struct {
2018-07-16 09:46:10 +00:00
CommonCAProviderConfig ` mapstructure:",squash" `
2018-06-13 08:40:03 +00:00
Address string
Token string
RootPKIPath string
IntermediatePKIPath string
2021-11-05 16:42:28 +00:00
Namespace string
2019-01-08 16:09:22 +00:00
CAFile string
CAPath string
CertFile string
KeyFile string
TLSServerName string
TLSSkipVerify bool
2021-11-18 20:15:28 +00:00
AuthMethod * VaultAuthMethod ` alias:"auth_method" `
}
type VaultAuthMethod struct {
Type string
MountPath string ` alias:"mount_path" `
Params map [ string ] interface { }
2018-06-13 08:40:03 +00:00
}
2018-09-13 14:43:00 +00:00
2019-11-21 17:40:29 +00:00
type AWSCAProviderConfig struct {
CommonCAProviderConfig ` mapstructure:",squash" `
ExistingARN string
DeleteOnExit bool
}
2019-01-11 21:04:57 +00:00
// CALeafOp is the operation for a request related to leaf certificates.
type CALeafOp string
const (
CALeafOpIncrementIndex CALeafOp = "increment-index"
)
// CALeafRequest is used to modify connect CA leaf data. This is used by the
// FSM (agent/consul/fsm) to apply changes.
type CALeafRequest struct {
// Op is the type of operation being requested. This determines what
// other fields are required.
Op CALeafOp
// Datacenter is the target for this request.
Datacenter string
// WriteRequest is a common struct containing ACL tokens and other
// write-related common elements for requests.
WriteRequest
}
// RequestDatacenter returns the datacenter for a given request.
func ( q * CALeafRequest ) RequestDatacenter ( ) string {
return q . Datacenter
}
2018-09-13 14:43:00 +00:00
// ParseDurationFunc is a mapstructure hook for decoding a string or
// []uint8 into a time.Duration value.
func ParseDurationFunc ( ) mapstructure . DecodeHookFunc {
return func (
f reflect . Type ,
t reflect . Type ,
data interface { } ) ( interface { } , error ) {
var v time . Duration
if t != reflect . TypeOf ( v ) {
return data , nil
}
switch {
case f . Kind ( ) == reflect . String :
if dur , err := time . ParseDuration ( data . ( string ) ) ; err != nil {
return nil , err
} else {
v = dur
}
return v , nil
case f == reflect . SliceOf ( reflect . TypeOf ( uint8 ( 0 ) ) ) :
s := Uint8ToString ( data . ( [ ] uint8 ) )
if dur , err := time . ParseDuration ( s ) ; err != nil {
return nil , err
} else {
v = dur
}
return v , nil
default :
return data , nil
}
}
}
func Uint8ToString ( bs [ ] uint8 ) string {
b := make ( [ ] byte , len ( bs ) )
for i , v := range bs {
2020-04-16 17:35:28 +00:00
b [ i ] = v
2018-09-13 14:43:00 +00:00
}
return string ( b )
}