mirror of
https://github.com/hashicorp/vault.git
synced 2026-07-16 12:26:02 -04:00
Merge remote-tracking branch 'remotes/from/ce/main'
Some checks failed
build / setup (push) Has been cancelled
build / hcp-setup (push) Has been cancelled
CI / setup (push) Has been cancelled
Run linters / Setup (push) Has been cancelled
Run linters / Semgrep (push) Has been cancelled
Check Copywrite Headers / copywrite (push) Has been cancelled
Security Scan / scan (push) Has been cancelled
build / Check ce/* Pull Requests (push) Has been cancelled
build / ui (push) Has been cancelled
build / artifacts-ce (push) Has been cancelled
build / artifacts-ent (push) Has been cancelled
build / hcp-image (push) Has been cancelled
build / test (push) Has been cancelled
build / test-hcp-image (push) Has been cancelled
build / completed-successfully (push) Has been cancelled
CI / Run Autopilot upgrade tool (push) Has been cancelled
CI / Run Go tests (push) Has been cancelled
CI / Run Go tests tagged with testonly (push) Has been cancelled
CI / Run Go tests with data race detection (push) Has been cancelled
CI / Run Go tests with FIPS configuration (push) Has been cancelled
CI / Test UI (push) Has been cancelled
CI / tests-completed (push) Has been cancelled
Run linters / Deprecated functions (push) Has been cancelled
Run linters / Code checks (push) Has been cancelled
Run linters / Protobuf generate delta (push) Has been cancelled
Run linters / Format (push) Has been cancelled
Some checks failed
build / setup (push) Has been cancelled
build / hcp-setup (push) Has been cancelled
CI / setup (push) Has been cancelled
Run linters / Setup (push) Has been cancelled
Run linters / Semgrep (push) Has been cancelled
Check Copywrite Headers / copywrite (push) Has been cancelled
Security Scan / scan (push) Has been cancelled
build / Check ce/* Pull Requests (push) Has been cancelled
build / ui (push) Has been cancelled
build / artifacts-ce (push) Has been cancelled
build / artifacts-ent (push) Has been cancelled
build / hcp-image (push) Has been cancelled
build / test (push) Has been cancelled
build / test-hcp-image (push) Has been cancelled
build / completed-successfully (push) Has been cancelled
CI / Run Autopilot upgrade tool (push) Has been cancelled
CI / Run Go tests (push) Has been cancelled
CI / Run Go tests tagged with testonly (push) Has been cancelled
CI / Run Go tests with data race detection (push) Has been cancelled
CI / Run Go tests with FIPS configuration (push) Has been cancelled
CI / Test UI (push) Has been cancelled
CI / tests-completed (push) Has been cancelled
Run linters / Deprecated functions (push) Has been cancelled
Run linters / Code checks (push) Has been cancelled
Run linters / Protobuf generate delta (push) Has been cancelled
Run linters / Format (push) Has been cancelled
This commit is contained in:
commit
042e17bf44
9 changed files with 155 additions and 83 deletions
|
|
@ -8,6 +8,8 @@ package transit
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/hashicorp/vault/sdk/helper/keysutil"
|
||||
)
|
||||
|
||||
var errEntOnly = errors.New("managed keys are supported within enterprise edition only")
|
||||
|
|
@ -15,3 +17,7 @@ var errEntOnly = errors.New("managed keys are supported within enterprise editio
|
|||
func GetManagedKeyUUID(ctx context.Context, b *backend, keyName string, keyId string) (uuid string, err error) {
|
||||
return "", errEntOnly
|
||||
}
|
||||
|
||||
func getFormattedManagedKeyPublicKey(_ context.Context, _ *backend, _ *keysutil.Policy) (map[string]map[string]interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -133,7 +133,18 @@ func (b *backend) pathCreateCsrWrite(ctx context.Context, req *logical.Request,
|
|||
return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest
|
||||
}
|
||||
|
||||
pemCsr, err := p.CreateCsr(signingKeyVersion, csrTemplate)
|
||||
var getCsrRequest keysutil.CsrRequestGetter
|
||||
if p.Type == keysutil.KeyType_MANAGED_KEY {
|
||||
factory, err := b.GetManagedKeyFactory(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
getCsrRequest = p.GetCsrRequestFromManagedKey(factory.GetManagedKeyParameters())
|
||||
} else {
|
||||
getCsrRequest = p.GetCsrRequestFromKey
|
||||
}
|
||||
|
||||
pemCsr, err := p.CreateCsr(signingKeyVersion, csrTemplate, getCsrRequest)
|
||||
if err != nil {
|
||||
prefixedErr := fmt.Errorf("could not create the csr: %w", err)
|
||||
switch err.(type) {
|
||||
|
|
@ -194,7 +205,18 @@ func (b *backend) pathImportCertChainWrite(ctx context.Context, req *logical.Req
|
|||
return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest
|
||||
}
|
||||
|
||||
err = p.ValidateAndPersistCertificateChain(ctx, keyVersion, certChain, req.Storage)
|
||||
var validateKeyMatch keysutil.LeafCertKeyMatchValidator
|
||||
if p.Type == keysutil.KeyType_MANAGED_KEY {
|
||||
factory, err := b.GetManagedKeyFactory(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
validateKeyMatch = p.GetLeafCertKeyMatchValidatorFromManagedKey(factory.GetManagedKeyParameters())
|
||||
} else {
|
||||
validateKeyMatch = p.GetLeafCertKeyMatchValidator()
|
||||
}
|
||||
|
||||
err = p.ValidateAndPersistCertificateChain(ctx, keyVersion, certChain, validateKeyMatch, req.Storage)
|
||||
if err != nil {
|
||||
prefixedErr := fmt.Errorf("failed to persist certificate chain: %w", err)
|
||||
switch err.(type) {
|
||||
|
|
|
|||
|
|
@ -247,7 +247,7 @@ func (b *backend) pathPolicyWrite(ctx context.Context, req *logical.Request, d *
|
|||
}
|
||||
defer p.Unlock()
|
||||
|
||||
resp, err := b.formatKeyPolicy(p, nil)
|
||||
resp, err := b.formatKeyPolicy(ctx, p, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -297,7 +297,7 @@ func (b *backend) pathPolicyRead(ctx context.Context, req *logical.Request, d *f
|
|||
}
|
||||
|
||||
b.TryRecordObservationWithRequest(ctx, req, ObservationTypeTransitKeyRead, b.keyPolicyObservationMetadata(p))
|
||||
return b.formatKeyPolicy(p, context)
|
||||
return b.formatKeyPolicy(ctx, p, context)
|
||||
}
|
||||
|
||||
func (b *backend) keyPolicyObservationMetadata(p *keysutil.Policy) map[string]interface{} {
|
||||
|
|
@ -341,7 +341,7 @@ func (b *backend) keyPolicyObservationMetadata(p *keysutil.Policy) map[string]in
|
|||
return metadata
|
||||
}
|
||||
|
||||
func (b *backend) formatKeyPolicy(p *keysutil.Policy, context []byte) (*logical.Response, error) {
|
||||
func (b *backend) formatKeyPolicy(ctx context.Context, p *keysutil.Policy, context []byte) (*logical.Response, error) {
|
||||
// Return the response
|
||||
resp := &logical.Response{
|
||||
Data: map[string]interface{}{
|
||||
|
|
@ -415,6 +415,12 @@ func (b *backend) formatKeyPolicy(p *keysutil.Policy, context []byte) (*logical.
|
|||
}
|
||||
resp.Data["keys"] = retKeys
|
||||
|
||||
case keysutil.KeyType_MANAGED_KEY:
|
||||
retKeys, err := getFormattedManagedKeyPublicKey(ctx, b, p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.Data["keys"] = retKeys
|
||||
case keysutil.KeyType_ECDSA_P256, keysutil.KeyType_ECDSA_P384, keysutil.KeyType_ECDSA_P521, keysutil.KeyType_ED25519, keysutil.KeyType_RSA2048, keysutil.KeyType_RSA3072, keysutil.KeyType_RSA4096, keysutil.KeyType_ML_DSA, keysutil.KeyType_HYBRID, keysutil.KeyType_SLH_DSA:
|
||||
retKeys := map[string]map[string]interface{}{}
|
||||
for k, v := range p.Keys {
|
||||
|
|
|
|||
|
|
@ -223,7 +223,7 @@ func (b *backend) pathKeysConfigWrite(ctx context.Context, req *logical.Request,
|
|||
}
|
||||
|
||||
if !persistNeeded {
|
||||
resp, err := b.formatKeyPolicy(p, nil)
|
||||
resp, err := b.formatKeyPolicy(ctx, p, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -244,7 +244,7 @@ func (b *backend) pathKeysConfigWrite(ctx context.Context, req *logical.Request,
|
|||
return nil, err
|
||||
}
|
||||
|
||||
resp, err = b.formatKeyPolicy(p, nil)
|
||||
resp, err = b.formatKeyPolicy(ctx, p, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ func (b *backend) pathRotateWrite(ctx context.Context, req *logical.Request, d *
|
|||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := b.formatKeyPolicy(p, nil)
|
||||
resp, err := b.formatKeyPolicy(ctx, p, nil)
|
||||
if err != nil {
|
||||
b.Logger().Error("failed to rotate key on user request", "name", name, "error", err.Error())
|
||||
b.TryRecordObservationWithRequest(ctx, req, ObservationTypeTransitKeyRotateFail, keyMetadata)
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ func (b *backend) pathTrimUpdate() framework.OperationFunc {
|
|||
|
||||
b.TryRecordObservationWithRequest(ctx, req, ObservationTypeTransitKeyTrim, b.keyPolicyObservationMetadata(p))
|
||||
|
||||
return b.formatKeyPolicy(p, nil)
|
||||
return b.formatKeyPolicy(ctx, p, nil)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
3
changelog/_15908.txt
Normal file
3
changelog/_15908.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:bug
|
||||
secrets/transit (enterprise): Add managed key support to CSR sign and set certificate chain endpoints.
|
||||
```
|
||||
|
|
@ -192,6 +192,12 @@ type SigningResult struct {
|
|||
PublicKey []byte
|
||||
}
|
||||
|
||||
// CsrBytesGetter is a function that extracts CSR bytes from a key
|
||||
type CsrRequestGetter func(keyVersion int, csrTemplate *x509.CertificateRequest) ([]byte, error)
|
||||
|
||||
// LeafCertKeyMatchValidator is a function that validates whether a certificate's public key matches a transit key version.
|
||||
type LeafCertKeyMatchValidator func(keyVersion int, certPublicKeyAlgorithm x509.PublicKeyAlgorithm, certPublicKey any) (bool, error)
|
||||
|
||||
type ecdsaSignature struct {
|
||||
R, S *big.Int
|
||||
}
|
||||
|
|
@ -2692,11 +2698,8 @@ func wrapTargetPKCS8ForImport(wrappingKey *rsa.PublicKey, preppedTargetKey []byt
|
|||
return base64.StdEncoding.EncodeToString(wrappedKeys), nil
|
||||
}
|
||||
|
||||
func (p *Policy) CreateCsr(keyVersion int, csrTemplate *x509.CertificateRequest) ([]byte, error) {
|
||||
if !p.Type.SigningSupported() {
|
||||
return nil, errutil.UserError{Err: fmt.Sprintf("key type '%s' does not support signing", p.Type)}
|
||||
}
|
||||
|
||||
// GetCsrRequestFromKey extracts CSR bytes from regular (non-managed) keys
|
||||
func (p *Policy) GetCsrRequestFromKey(keyVersion int, csrTemplate *x509.CertificateRequest) ([]byte, error) {
|
||||
keyEntry, err := p.safeGetKeyEntry(keyVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -2706,9 +2709,6 @@ func (p *Policy) CreateCsr(keyVersion int, csrTemplate *x509.CertificateRequest)
|
|||
return nil, errutil.UserError{Err: "private key not imported for key version selected"}
|
||||
}
|
||||
|
||||
csrTemplate.Signature = nil
|
||||
csrTemplate.SignatureAlgorithm = x509.UnknownSignatureAlgorithm
|
||||
|
||||
var key crypto.Signer
|
||||
switch p.Type {
|
||||
case KeyType_ECDSA_P256, KeyType_ECDSA_P384, KeyType_ECDSA_P521:
|
||||
|
|
@ -2743,9 +2743,26 @@ func (p *Policy) CreateCsr(keyVersion int, csrTemplate *x509.CertificateRequest)
|
|||
default:
|
||||
return nil, errutil.InternalError{Err: fmt.Sprintf("selected key type '%s' does not support signing", p.Type.String())}
|
||||
}
|
||||
|
||||
csrBytes, err := x509.CreateCertificateRequest(rand.Reader, csrTemplate, key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create the cerfificate request: %w", err)
|
||||
return nil, fmt.Errorf("could not create the certificate request: %w", err)
|
||||
}
|
||||
|
||||
return csrBytes, nil
|
||||
}
|
||||
|
||||
func (p *Policy) CreateCsr(keyVersion int, csrTemplate *x509.CertificateRequest, getCsrRequest CsrRequestGetter) ([]byte, error) {
|
||||
if !p.Type.SigningSupported() {
|
||||
return nil, errutil.UserError{Err: fmt.Sprintf("key type '%s' does not support signing", p.Type)}
|
||||
}
|
||||
|
||||
csrTemplate.Signature = nil
|
||||
csrTemplate.SignatureAlgorithm = x509.UnknownSignatureAlgorithm
|
||||
|
||||
csrBytes, err := getCsrRequest(keyVersion, csrTemplate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pemCsr := pem.EncodeToMemory(&pem.Block{
|
||||
|
|
@ -2756,84 +2773,89 @@ func (p *Policy) CreateCsr(keyVersion int, csrTemplate *x509.CertificateRequest)
|
|||
return pemCsr, nil
|
||||
}
|
||||
|
||||
func (p *Policy) ValidateLeafCertKeyMatch(keyVersion int, certPublicKeyAlgorithm x509.PublicKeyAlgorithm, certPublicKey any) (bool, error) {
|
||||
if !p.Type.SigningSupported() {
|
||||
return false, errutil.UserError{Err: fmt.Sprintf("key type '%s' does not support signing", p.Type)}
|
||||
}
|
||||
|
||||
var keyTypeMatches bool
|
||||
switch p.Type {
|
||||
case KeyType_ECDSA_P256, KeyType_ECDSA_P384, KeyType_ECDSA_P521:
|
||||
if certPublicKeyAlgorithm == x509.ECDSA {
|
||||
keyTypeMatches = true
|
||||
// GetLeafCertKeyMatchValidator returns a LeafCertKeyMatchValidator that checks whether a certificate's
|
||||
// public key matches a transit key version. It must only be used with non-managed key types.
|
||||
func (p *Policy) GetLeafCertKeyMatchValidator() LeafCertKeyMatchValidator {
|
||||
return func(keyVersion int, certPublicKeyAlgorithm x509.PublicKeyAlgorithm, certPublicKey any) (bool, error) {
|
||||
if !p.Type.SigningSupported() {
|
||||
return false, errutil.UserError{Err: fmt.Sprintf("key type '%s' does not support signing", p.Type)}
|
||||
}
|
||||
case KeyType_ED25519:
|
||||
if certPublicKeyAlgorithm == x509.Ed25519 {
|
||||
keyTypeMatches = true
|
||||
}
|
||||
case KeyType_RSA2048, KeyType_RSA3072, KeyType_RSA4096:
|
||||
if certPublicKeyAlgorithm == x509.RSA {
|
||||
keyTypeMatches = true
|
||||
}
|
||||
}
|
||||
if !keyTypeMatches {
|
||||
return false, errutil.UserError{Err: fmt.Sprintf("provided leaf certificate public key algorithm '%s' does not match the transit key type '%s'",
|
||||
certPublicKeyAlgorithm, p.Type)}
|
||||
}
|
||||
|
||||
keyEntry, err := p.safeGetKeyEntry(keyVersion)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
switch certPublicKeyAlgorithm {
|
||||
case x509.ECDSA:
|
||||
certPublicKey := certPublicKey.(*ecdsa.PublicKey)
|
||||
var curve elliptic.Curve
|
||||
var keyTypeMatches bool
|
||||
switch p.Type {
|
||||
case KeyType_ECDSA_P384:
|
||||
curve = elliptic.P384()
|
||||
case KeyType_ECDSA_P521:
|
||||
curve = elliptic.P521()
|
||||
default:
|
||||
curve = elliptic.P256()
|
||||
case KeyType_ECDSA_P256, KeyType_ECDSA_P384, KeyType_ECDSA_P521:
|
||||
if certPublicKeyAlgorithm == x509.ECDSA {
|
||||
keyTypeMatches = true
|
||||
}
|
||||
case KeyType_ED25519:
|
||||
if certPublicKeyAlgorithm == x509.Ed25519 {
|
||||
keyTypeMatches = true
|
||||
}
|
||||
case KeyType_RSA2048, KeyType_RSA3072, KeyType_RSA4096:
|
||||
if certPublicKeyAlgorithm == x509.RSA {
|
||||
keyTypeMatches = true
|
||||
}
|
||||
}
|
||||
if !keyTypeMatches {
|
||||
return false, errutil.UserError{Err: fmt.Sprintf("provided leaf certificate public key algorithm '%s' does not match the transit key type '%s'",
|
||||
certPublicKeyAlgorithm, p.Type)}
|
||||
}
|
||||
|
||||
publicKey := &ecdsa.PublicKey{
|
||||
Curve: curve,
|
||||
X: keyEntry.EC_X,
|
||||
Y: keyEntry.EC_Y,
|
||||
}
|
||||
|
||||
return publicKey.Equal(certPublicKey), nil
|
||||
|
||||
case x509.Ed25519:
|
||||
if p.Derived {
|
||||
return false, errutil.UserError{Err: "operation not supported on keys with derivation enabled"}
|
||||
}
|
||||
certPublicKey := certPublicKey.(ed25519.PublicKey)
|
||||
|
||||
raw, err := base64.StdEncoding.DecodeString(keyEntry.FormattedPublicKey)
|
||||
keyEntry, err := p.safeGetKeyEntry(keyVersion)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
publicKey := ed25519.PublicKey(raw)
|
||||
|
||||
return publicKey.Equal(certPublicKey), nil
|
||||
switch certPublicKeyAlgorithm {
|
||||
case x509.ECDSA:
|
||||
certPublicKey := certPublicKey.(*ecdsa.PublicKey)
|
||||
var curve elliptic.Curve
|
||||
switch p.Type {
|
||||
case KeyType_ECDSA_P384:
|
||||
curve = elliptic.P384()
|
||||
case KeyType_ECDSA_P521:
|
||||
curve = elliptic.P521()
|
||||
default:
|
||||
curve = elliptic.P256()
|
||||
}
|
||||
|
||||
case x509.RSA:
|
||||
certPublicKey := certPublicKey.(*rsa.PublicKey)
|
||||
publicKey := keyEntry.RSAKey.PublicKey
|
||||
return publicKey.Equal(certPublicKey), nil
|
||||
publicKey := &ecdsa.PublicKey{
|
||||
Curve: curve,
|
||||
X: keyEntry.EC_X,
|
||||
Y: keyEntry.EC_Y,
|
||||
}
|
||||
|
||||
case x509.UnknownPublicKeyAlgorithm:
|
||||
return false, errutil.InternalError{Err: fmt.Sprint("certificate signed with an unknown algorithm")}
|
||||
return publicKey.Equal(certPublicKey), nil
|
||||
|
||||
case x509.Ed25519:
|
||||
if p.Derived {
|
||||
return false, errutil.UserError{Err: "operation not supported on keys with derivation enabled"}
|
||||
}
|
||||
certPublicKey := certPublicKey.(ed25519.PublicKey)
|
||||
|
||||
raw, err := base64.StdEncoding.DecodeString(keyEntry.FormattedPublicKey)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
publicKey := ed25519.PublicKey(raw)
|
||||
|
||||
return publicKey.Equal(certPublicKey), nil
|
||||
|
||||
case x509.RSA:
|
||||
certPublicKey := certPublicKey.(*rsa.PublicKey)
|
||||
publicKey := keyEntry.RSAKey.PublicKey
|
||||
return publicKey.Equal(certPublicKey), nil
|
||||
|
||||
case x509.UnknownPublicKeyAlgorithm:
|
||||
return false, errutil.InternalError{Err: fmt.Sprint("certificate signed with an unknown algorithm")}
|
||||
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (p *Policy) ValidateAndPersistCertificateChain(ctx context.Context, keyVersion int, certChain []*x509.Certificate, storage logical.Storage) error {
|
||||
func (p *Policy) ValidateAndPersistCertificateChain(ctx context.Context, keyVersion int, certChain []*x509.Certificate, validateKeyMatch LeafCertKeyMatchValidator, storage logical.Storage) error {
|
||||
if len(certChain) == 0 {
|
||||
return errutil.UserError{Err: "expected at least one certificate in the parsed certificate chain"}
|
||||
}
|
||||
|
|
@ -2848,7 +2870,7 @@ func (p *Policy) ValidateAndPersistCertificateChain(ctx context.Context, keyVers
|
|||
}
|
||||
}
|
||||
|
||||
valid, err := p.ValidateLeafCertKeyMatch(keyVersion, certChain[0].PublicKeyAlgorithm, certChain[0].PublicKey)
|
||||
valid, err := validateKeyMatch(keyVersion, certChain[0].PublicKeyAlgorithm, certChain[0].PublicKey)
|
||||
if err != nil {
|
||||
prefixedErr := fmt.Errorf("could not validate key match between leaf certificate key and key version in transit: %w", err)
|
||||
switch err.(type) {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
package keysutil
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
|
|
@ -37,3 +38,15 @@ func entEncryptWithOptions(p *Policy, opts EncryptionOptions, value []byte) ([]b
|
|||
func entDecryptWithOptions(p *Policy, opts EncryptionOptions, value []byte) ([]byte, error) {
|
||||
return nil, errutil.InternalError{Err: fmt.Sprintf("unsupported key type %v", p.Type)}
|
||||
}
|
||||
|
||||
func (p *Policy) GetCsrRequestFromManagedKey(params ManagedKeyParameters) CsrRequestGetter {
|
||||
return func(_ int, _ *x509.CertificateRequest) ([]byte, error) {
|
||||
return nil, errutil.InternalError{Err: fmt.Sprintf("unsupported key type %v", p.Type)}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Policy) GetLeafCertKeyMatchValidatorFromManagedKey(params ManagedKeyParameters) LeafCertKeyMatchValidator {
|
||||
return func(keyVersion int, certPublicKeyAlgorithm x509.PublicKeyAlgorithm, certPublicKey any) (bool, error) {
|
||||
return false, errutil.InternalError{Err: fmt.Sprintf("unsupported key type %v", p.Type)}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue