From 7321153e78f97f1db124feb25fccdccb1f326f50 Mon Sep 17 00:00:00 2001 From: Joris Coenen Date: Mon, 18 Nov 2019 17:48:59 +0100 Subject: [PATCH 01/35] Remove error return from api.GetFingerprint --- internals/api/credential.go | 16 ++++------------ internals/aws/service_creator.go | 6 +----- internals/crypto/hash.go | 9 +++++++++ pkg/secrethub/credentials/rsa.go | 5 +---- 4 files changed, 15 insertions(+), 21 deletions(-) create mode 100644 internals/crypto/hash.go diff --git a/internals/api/credential.go b/internals/api/credential.go index de4ddce6..d654de06 100644 --- a/internals/api/credential.go +++ b/internals/api/credential.go @@ -2,7 +2,6 @@ package api import ( "bytes" - "crypto/sha256" "encoding/base64" "encoding/hex" "encoding/json" @@ -11,6 +10,7 @@ import ( "time" "github.com/secrethub/secrethub-go/internals/api/uuid" + "github.com/secrethub/secrethub-go/internals/crypto" ) // Errors @@ -138,10 +138,7 @@ func (req *CreateCredentialRequest) Validate() error { return ErrMissingField("proof") } - fingerprint, err := GetFingerprint(req.Type, req.Verifier) - if err != nil { - return err - } + fingerprint := GetFingerprint(req.Type, req.Verifier) if req.Fingerprint != fingerprint { return ErrInvalidFingerprint } @@ -193,7 +190,7 @@ func (p CredentialProofAWS) Validate() error { type CredentialProofKey struct{} // GetFingerprint returns the fingerprint of a credential. -func GetFingerprint(t CredentialType, verifier []byte) (string, error) { +func GetFingerprint(t CredentialType, verifier []byte) string { var toHash []byte if t == CredentialTypeKey { // Provide compatibility with traditional RSA credentials. @@ -203,10 +200,5 @@ func GetFingerprint(t CredentialType, verifier []byte) (string, error) { toHash = []byte(fmt.Sprintf("credential_type=%s;verifier=%s", t, encodedVerifier)) } - h := sha256.New() - _, err := h.Write(toHash) - if err != nil { - return "", err - } - return hex.EncodeToString(h.Sum(nil)), nil + return hex.EncodeToString(crypto.SHA256(toHash)) } diff --git a/internals/aws/service_creator.go b/internals/aws/service_creator.go index 70b7aac1..43921046 100644 --- a/internals/aws/service_creator.go +++ b/internals/aws/service_creator.go @@ -71,11 +71,7 @@ func (c CredentialCreator) Type() api.CredentialType { // Verifier returns the verifier of an AWS service. func (c CredentialCreator) Export() ([]byte, string, error) { verifier := []byte(c.role) - - fingerprint, err := api.GetFingerprint(c.Type(), verifier) - if err != nil { - return nil, "", err - } + fingerprint := api.GetFingerprint(c.Type(), verifier) return verifier, fingerprint, nil } diff --git a/internals/crypto/hash.go b/internals/crypto/hash.go new file mode 100644 index 00000000..6f669635 --- /dev/null +++ b/internals/crypto/hash.go @@ -0,0 +1,9 @@ +package crypto + +import "crypto/sha256" + +// SHA256 creates a SHA256 hash of the given bytes. +func SHA256(in []byte) []byte { + hash := sha256.Sum256(in) + return hash[:] +} diff --git a/pkg/secrethub/credentials/rsa.go b/pkg/secrethub/credentials/rsa.go index 044f4c67..90e94c0e 100644 --- a/pkg/secrethub/credentials/rsa.go +++ b/pkg/secrethub/credentials/rsa.go @@ -34,10 +34,7 @@ func (c RSACredential) Export() ([]byte, string, error) { if err != nil { return nil, "", err } - fingerprint, err := api.GetFingerprint(c.Type(), verifier) - if err != nil { - return nil, "", err - } + fingerprint := api.GetFingerprint(c.Type(), verifier) return verifier, fingerprint, nil } From 808a92d540c14f481b9a3d11bedbd2b91aeb8a28 Mon Sep 17 00:00:00 2001 From: Joris Coenen Date: Mon, 18 Nov 2019 16:49:06 +0100 Subject: [PATCH 02/35] Add BackupCode credential and corresponding BootstrapCode EncryptionKey --- internals/api/credential.go | 32 ++++++++++++++++---- internals/api/credential_test.go | 42 ++++++++++++++++++++++++++ internals/api/encrypted_data.go | 2 ++ internals/api/encrypted_data_test.go | 3 ++ internals/api/encryption_key.go | 45 ++++++++++++++++++++++++---- 5 files changed, 113 insertions(+), 11 deletions(-) diff --git a/internals/api/credential.go b/internals/api/credential.go index d654de06..bd7639df 100644 --- a/internals/api/credential.go +++ b/internals/api/credential.go @@ -2,6 +2,7 @@ package api import ( "bytes" + "crypto/sha256" "encoding/base64" "encoding/hex" "encoding/json" @@ -52,8 +53,9 @@ type CredentialType string // Credential types const ( - CredentialTypeKey CredentialType = "key" - CredentialTypeAWS CredentialType = "aws" + CredentialTypeKey CredentialType = "key" + CredentialTypeAWS CredentialType = "aws" + CredentialTypeBackupCode CredentialType = "backup-code" ) const ( @@ -63,10 +65,15 @@ const ( // Validate validates whether the algorithm type is valid. func (a CredentialType) Validate() error { - if a == CredentialTypeKey || a == CredentialTypeAWS { - return nil + var credentialTypeList = map[CredentialType]struct{}{ + CredentialTypeKey: {}, + CredentialTypeAWS: {}, + CredentialTypeBackupCode: {}, } - return ErrInvalidCredentialType + if _, ok := credentialTypeList[a]; !ok { + return ErrInvalidCredentialType + } + return nil } // CreateCredentialRequest contains the fields to add a credential to an account. @@ -102,6 +109,8 @@ func (req *CreateCredentialRequest) UnmarshalJSON(b []byte) error { dec.Proof = &CredentialProofAWS{} case CredentialTypeKey: dec.Proof = &CredentialProofKey{} + case CredentialTypeBackupCode: + dec.Proof = &CredentialProofBackupCode{} default: return ErrInvalidCredentialType } @@ -134,6 +143,16 @@ func (req *CreateCredentialRequest) Validate() error { return err } + if req.Type == CredentialTypeBackupCode { + decoded, err := base64.StdEncoding.DecodeString(string(req.Verifier)) + if err != nil { + return ErrInvalidVerifier + } + if len(decoded) != sha256.Size { + return ErrInvalidVerifier + } + } + if req.Type == CredentialTypeAWS && req.Proof == nil { return ErrMissingField("proof") } @@ -189,6 +208,9 @@ func (p CredentialProofAWS) Validate() error { // CredentialProofKey is proof for when the credential type is RSA. type CredentialProofKey struct{} +// CredentialProofBackupCode is proof for when the credential type is backup key. +type CredentialProofBackupCode struct{} + // GetFingerprint returns the fingerprint of a credential. func GetFingerprint(t CredentialType, verifier []byte) string { var toHash []byte diff --git a/internals/api/credential_test.go b/internals/api/credential_test.go index 1739b0f3..c96f615b 100644 --- a/internals/api/credential_test.go +++ b/internals/api/credential_test.go @@ -134,6 +134,48 @@ func TestCreateCredentialRequest_Validate(t *testing.T) { }, err: ErrUnknownMetadataKey("foo"), }, + "backup code success": { + req: CreateCredentialRequest{ + Type: CredentialTypeBackupCode, + Fingerprint: "69cf01c1e969b4430ca1b08ede7dab5f91a64a306e321f0348667446e1b3597e", + Verifier: []byte("DdAaVTKxoYgxzWY2UWrdl1xHOOv4ZUozra4Vm8WGxmU="), + Proof: &CredentialProofBackupCode{}, + Metadata: map[string]string{}, + }, + err: nil, + }, + "backup code too short verifier": { + req: CreateCredentialRequest{ + Type: CredentialTypeBackupCode, + Fingerprint: "69cf01c1e969b4430ca1b08ede7dab5f91a64a306e321f0348667446e1b3597e", + Verifier: []byte("DdAaVTKxoYgxzWY2UWrdl1OOv4ZUozra4Vm8WGxmU="), + Proof: &CredentialProofBackupCode{}, + Metadata: map[string]string{}, + }, + err: ErrInvalidVerifier, + }, + "backup code non base64 verifier": { + req: CreateCredentialRequest{ + Type: CredentialTypeBackupCode, + Fingerprint: "69cf01c1e969b4430ca1b08ede7dab5f91a64a306e321f0348667446e1b3597e", + Verifier: []byte("DdAaVTKxoYgxzWY2UWrdl1OOv4ZUozra4Vm8WGxm&="), + Proof: &CredentialProofBackupCode{}, + Metadata: map[string]string{}, + }, + err: ErrInvalidVerifier, + }, + "backup code with metadata": { + req: CreateCredentialRequest{ + Type: CredentialTypeBackupCode, + Fingerprint: "69cf01c1e969b4430ca1b08ede7dab5f91a64a306e321f0348667446e1b3597e", + Verifier: []byte("DdAaVTKxoYgxzWY2UWrdl1xHOOv4ZUozra4Vm8WGxmU="), + Proof: &CredentialProofBackupCode{}, + Metadata: map[string]string{ + CredentialMetadataAWSKMSKey: "test", + }, + }, + err: ErrInvalidMetadataKey(CredentialMetadataAWSKMSKey, CredentialTypeBackupCode), + }, } for name, tc := range cases { diff --git a/internals/api/encrypted_data.go b/internals/api/encrypted_data.go index 072f6d60..752cfdb7 100644 --- a/internals/api/encrypted_data.go +++ b/internals/api/encrypted_data.go @@ -133,6 +133,8 @@ func (ed *EncryptedData) UnmarshalJSON(b []byte) error { dec.Key = &EncryptionKeyEncrypted{} case KeyTypeLocal: dec.Key = &EncryptionKeyLocal{} + case KeyTypeBootstrapCode: + dec.Key = &EncryptionKeyBootstrapCode{} case KeyTypeAccountKey: dec.Key = &EncryptionKeyAccountKey{} case KeyTypeSecretKey: diff --git a/internals/api/encrypted_data_test.go b/internals/api/encrypted_data_test.go index 87d2a3fb..457fa1a5 100644 --- a/internals/api/encrypted_data_test.go +++ b/internals/api/encrypted_data_test.go @@ -35,6 +35,9 @@ func TestEncryptedData_MarshalUnmarshalValidate(t *testing.T) { "aes with scrypt": { in: NewEncryptedDataAESGCM([]byte("ciphertext"), []byte("nonce"), 96, NewEncryptionKeyDerivedScrypt(256, 1, 2, 3, []byte("just-a-salt"))), }, + "aes with bootstrap code": { + in: NewEncryptedDataAESGCM([]byte("ciphertext"), []byte("nonce"), 96, NewEncryptionKeyBootstrapCode(256)), + }, "rsa with missing key": { in: NewEncryptedDataAESGCM([]byte("ciphertext"), []byte("nonce"), 96, nil), expectedErr: ErrInvalidKeyType, diff --git a/internals/api/encryption_key.go b/internals/api/encryption_key.go index 152eed40..340b1abb 100644 --- a/internals/api/encryption_key.go +++ b/internals/api/encryption_key.go @@ -37,12 +37,13 @@ func (ed *KeyDerivationAlgorithm) UnmarshalJSON(b []byte) error { // Options for KeyType const ( - KeyTypeDerived KeyType = "derived" - KeyTypeEncrypted KeyType = "encrypted" - KeyTypeLocal KeyType = "local" - KeyTypeAccountKey KeyType = "account-key" - KeyTypeSecretKey KeyType = "secret-key" - KeyTypeAWS KeyType = "aws" + KeyTypeDerived KeyType = "derived" + KeyTypeEncrypted KeyType = "encrypted" + KeyTypeLocal KeyType = "local" + KeyTypeAccountKey KeyType = "account-key" + KeyTypeSecretKey KeyType = "secret-key" + KeyTypeAWS KeyType = "aws" + KeyTypeBootstrapCode KeyType = "bootstrap-code" ) // Options for KeyDerivationAlgorithm @@ -243,6 +244,38 @@ func (k EncryptionKeyLocal) Validate() error { return nil } +// NewEncryptionKeyLocal creates a EncryptionKeyBootstrapCode. +func NewEncryptionKeyBootstrapCode(length int) *EncryptionKeyBootstrapCode { + return &EncryptionKeyBootstrapCode{ + EncryptionKey: EncryptionKey{ + Type: KeyTypeBootstrapCode, + }, + Length: length, + } +} + +// EncryptionKeyBootstrapCode is an encryption key that is stored as a code memorized by the user. +type EncryptionKeyBootstrapCode struct { + EncryptionKey + Length int `json:"length"` +} + +// SupportsAlgorithm returns true when the encryption key supports the given algorithm. +func (EncryptionKeyBootstrapCode) SupportsAlgorithm(a EncryptionAlgorithm) bool { + return a == EncryptionAlgorithmAESGCM +} + +// Validate whether the EncryptionKeyBootstrapCode is valid. +func (k EncryptionKeyBootstrapCode) Validate() error { + if k.Length == 0 { + return ErrMissingField("length") + } + if k.Length <= 0 { + return ErrInvalidKeyLength + } + return nil +} + // NewEncryptionKeyAccountKey creates a EncryptionKeyAccountKey. func NewEncryptionKeyAccountKey(length int, id uuid.UUID) *EncryptionKeyAccountKey { return &EncryptionKeyAccountKey{ From 78dd270be95323c4357be3e35748d58c49108b41 Mon Sep 17 00:00:00 2001 From: Joris Coenen Date: Wed, 20 Nov 2019 16:04:11 +0100 Subject: [PATCH 03/35] Add optional AccountKey to CreateCredentialRequest This can be used to create a new credential for an already keyed account. --- internals/api/credential.go | 13 +++++++------ internals/api/credential_test.go | 23 +++++++++++++++++++++++ 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/internals/api/credential.go b/internals/api/credential.go index bd7639df..5f0b77a2 100644 --- a/internals/api/credential.go +++ b/internals/api/credential.go @@ -78,12 +78,13 @@ func (a CredentialType) Validate() error { // CreateCredentialRequest contains the fields to add a credential to an account. type CreateCredentialRequest struct { - Type CredentialType `json:"type"` - Fingerprint string `json:"fingerprint"` - Name string `json:"name,omitempty"` - Verifier []byte `json:"verifier"` - Proof interface{} `json:"proof"` - Metadata map[string]string `json:"metadata"` + Type CredentialType `json:"type"` + Fingerprint string `json:"fingerprint"` + Name string `json:"name,omitempty"` + Verifier []byte `json:"verifier"` + Proof interface{} `json:"proof"` + Metadata map[string]string `json:"metadata"` + AccountKey *CreateAccountKeyRequest `json:"account_key,omitempty"` } // UnmarshalJSON converts a JSON representation into a CreateCredentialRequest with the correct Proof. diff --git a/internals/api/credential_test.go b/internals/api/credential_test.go index c96f615b..a63f9ef8 100644 --- a/internals/api/credential_test.go +++ b/internals/api/credential_test.go @@ -20,6 +20,29 @@ func TestCreateCredentialRequest_Validate(t *testing.T) { }, err: nil, }, + "success including account key": { + req: CreateCredentialRequest{ + Name: "Personal laptop credential", + Type: CredentialTypeKey, + Fingerprint: "88c9eae68eb300b2971a2bec9e5a26ff4179fd661d6b7d861e4c6557b9aaee14", + Verifier: []byte("verifier"), + AccountKey: &CreateAccountKeyRequest{ + EncryptedPrivateKey: NewEncryptedDataAESGCM([]byte("encrypted"), []byte("nonce"), 96, NewEncryptionKeyLocal(256)), + PublicKey: []byte("public-key"), + }, + }, + err: nil, + }, + "including invalid account key": { + req: CreateCredentialRequest{ + Name: "Personal laptop credential", + Type: CredentialTypeKey, + Fingerprint: "88c9eae68eb300b2971a2bec9e5a26ff4179fd661d6b7d861e4c6557b9aaee14", + Verifier: []byte("verifier"), + AccountKey: &CreateAccountKeyRequest{}, + }, + err: ErrInvalidPublicKey, + }, "success without name": { req: CreateCredentialRequest{ Type: CredentialTypeKey, From 16cc764ae91ee00891e0babf350d8889a6dc8115 Mon Sep 17 00:00:00 2001 From: Joris Coenen Date: Wed, 20 Nov 2019 16:20:04 +0100 Subject: [PATCH 04/35] Add more specific api.EncryptedDataASESGCM type This allows the caller to call EncryptedData.AESGCM() once to the whole struct to the correct type. --- internals/api/encrypted_data.go | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/internals/api/encrypted_data.go b/internals/api/encrypted_data.go index 752cfdb7..6cfe887c 100644 --- a/internals/api/encrypted_data.go +++ b/internals/api/encrypted_data.go @@ -222,3 +222,33 @@ func (ed *EncryptedData) Validate() error { } return nil } + +// EncryptedDataAESGCM is a typed EncryptedData for the AESGCM algorithm. +type EncryptedDataAESGCM struct { + Key interface{} + Parameters EncryptionParametersAESGCM + Metadata EncryptionMetadataAESGCM + Ciphertext []byte +} + +// AESGCM casts the EncryptedData to EncryptedDataAESGCM. +// Returns an error if the EncryptedData does not have AESGCM as its algorithm. +func (ed *EncryptedData) AESGCM() (*EncryptedDataAESGCM, error) { + if ed.Algorithm != EncryptionAlgorithmAESGCM { + return nil, ErrInvalidEncryptionAlgorithm + } + parameters, ok := ed.Parameters.(*EncryptionParametersAESGCM) + if !ok { + return nil, ErrInvalidEncryptionAlgorithm + } + metadata, ok := ed.Metadata.(*EncryptionMetadataAESGCM) + if !ok { + return nil, ErrInvalidEncryptionAlgorithm + } + return &EncryptedDataAESGCM{ + Key: ed.Key, + Parameters: *parameters, + Metadata: *metadata, + Ciphertext: ed.Ciphertext, + }, nil +} From 3ff5e7c0e3ea51b54105a4d7ca549c79c861b93e Mon Sep 17 00:00:00 2001 From: Joris Coenen Date: Wed, 20 Nov 2019 16:51:39 +0100 Subject: [PATCH 05/35] Add Provider and Creator for BackupCode credential The bootstrapCode type can later also be used for enroll codes, which are very similar to backup codes. --- pkg/secrethub/credentials/bootstrap_code.go | 155 ++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 pkg/secrethub/credentials/bootstrap_code.go diff --git a/pkg/secrethub/credentials/bootstrap_code.go b/pkg/secrethub/credentials/bootstrap_code.go new file mode 100644 index 00000000..32c92b52 --- /dev/null +++ b/pkg/secrethub/credentials/bootstrap_code.go @@ -0,0 +1,155 @@ +package credentials + +import ( + "encoding/base64" + "encoding/hex" + "errors" + + "github.com/secrethub/secrethub-go/internals/api" + "github.com/secrethub/secrethub-go/internals/auth" + "github.com/secrethub/secrethub-go/internals/crypto" + "github.com/secrethub/secrethub-go/pkg/secrethub/internals/http" +) + +var _ Creator = (*BackupCodeCreator)(nil) +var _ Provider = (*bootstrapCodeProvider)(nil) +var _ auth.Signer = (*bootstrapCode)(nil) + +// BackupCodeCreator creates a new credential based on a backup code. +type BackupCodeCreator struct { + bootstrapCode *bootstrapCode +} + +// CreateBackupCode returns a Creator that creates a backup code credential. +func CreateBackupCode() *BackupCodeCreator { + return &BackupCodeCreator{} +} + +// Create generates a new code and stores it in the BackupCodeGenerator. +func (b *BackupCodeCreator) Create() error { + key, err := crypto.GenerateSymmetricKey() + if err != nil { + return err + } + b.bootstrapCode = newBootstrapCode(key.Export(), api.CredentialTypeBackupCode) + return nil +} + +// Code returns the string representation of the backup code. +// Can only be called after the credential has been created. +func (b *BackupCodeCreator) Code() (string, error) { + if b.bootstrapCode == nil { + return "", errors.New("backup code has not yet been generated") + } + return hex.EncodeToString(b.bootstrapCode.encryptionKey.Export()), nil +} + +// Verifier returns a Verifier that can be used for creating a new credential from this backup code. +func (b *BackupCodeCreator) Verifier() Verifier { + return b.bootstrapCode +} + +// Encrypter returns a Encrypter that can be used to encrypt data with this backup code. +func (b *BackupCodeCreator) Encrypter() Encrypter { + return b.bootstrapCode +} + +// Metadata returns the metadata for a backup code. +func (b *BackupCodeCreator) Metadata() map[string]string { + return nil +} + +// bootstrapCodeProvider is a Provider that can be used to authenticate and decrypt with a bootstrap code. +type bootstrapCodeProvider struct { + code string + t api.CredentialType +} + +// UseBackupCode returns a Provider for authentication and decryption with the given backup code. +func UseBackupCode(code string) Provider { + return &bootstrapCodeProvider{ + code: code, + t: api.CredentialTypeBackupCode, + } +} + +// Provide returns the auth.Authenticator and Decrypter corresponding to a bootstrap code. +func (b *bootstrapCodeProvider) Provide(_ *http.Client) (auth.Authenticator, Decrypter, error) { + bytes, err := hex.DecodeString(b.code) + if err != nil || len(bytes) != 32 { + return nil, nil, errors.New("malformed code") + } + bootstrapCode := newBootstrapCode(bytes, b.t) + return auth.NewHTTPSigner(bootstrapCode), bootstrapCode, nil +} + +// bootstrapCode is a type that represents both backup and enroll codes. +type bootstrapCode struct { + t api.CredentialType + encryptionKey *crypto.SymmetricKey + signKey *crypto.SymmetricKey +} + +// newBootstrapCode returns a new bootstrapCode for the given AES key and credential type. +func newBootstrapCode(key []byte, t api.CredentialType) *bootstrapCode { + encryptionKey := crypto.NewSymmetricKey(key) + signKey := crypto.NewSymmetricKey(crypto.SHA256(key)) + return &bootstrapCode{ + t: t, + encryptionKey: encryptionKey, + signKey: signKey, + } +} + +func (b *bootstrapCode) Export() ([]byte, string, error) { + verifierBytes := []byte(base64.StdEncoding.EncodeToString(b.signKey.Export())) + fingerprint := api.GetFingerprint(b.t, verifierBytes) + return verifierBytes, fingerprint, nil +} + +func (b *bootstrapCode) Type() api.CredentialType { + return b.t +} + +func (b *bootstrapCode) AddProof(req *api.CreateCredentialRequest) error { + return nil +} + +func (b *bootstrapCode) ID() (string, error) { + _, fingerprint, err := b.Export() + if err != nil { + return "", err + } + return fingerprint, nil +} + +func (b *bootstrapCode) Sign(in []byte) ([]byte, error) { + return b.signKey.HMAC(in) +} + +func (b *bootstrapCode) SignMethod() string { + return "BootstrapCode-HMAC" +} + +func (b *bootstrapCode) Wrap(plaintext []byte) (*api.EncryptedData, error) { + enc, err := b.encryptionKey.Encrypt(plaintext) + if err != nil { + return nil, err + } + return api.NewEncryptedDataAESGCM(enc.Data, enc.Nonce, len(enc.Nonce)*8, api.NewEncryptionKeyBootstrapCode(256)), nil +} + +func (b *bootstrapCode) Unwrap(ciphertext *api.EncryptedData) ([]byte, error) { + ciphertextAESGCM, err := ciphertext.AESGCM() + if err != nil { + return nil, err + } + decrypted, err := b.encryptionKey.Decrypt(crypto.CiphertextAES{ + Data: ciphertextAESGCM.Ciphertext, + Nonce: ciphertextAESGCM.Metadata.Nonce, + }) + if err != nil { + return nil, err + } + return decrypted, nil +} From d1e9b173319d4c87c84eb29121a4c07066e02b58 Mon Sep 17 00:00:00 2001 From: Joris Coenen Date: Wed, 20 Nov 2019 16:52:49 +0100 Subject: [PATCH 06/35] Add CredentialService with CreateCredential method --- pkg/secrethub/client.go | 7 +++ pkg/secrethub/credentials.go | 70 ++++++++++++++++++++++++++ pkg/secrethub/internals/http/client.go | 9 ++++ 3 files changed, 86 insertions(+) create mode 100644 pkg/secrethub/credentials.go diff --git a/pkg/secrethub/client.go b/pkg/secrethub/client.go index 6bcaccb5..34b6d551 100644 --- a/pkg/secrethub/client.go +++ b/pkg/secrethub/client.go @@ -32,6 +32,8 @@ type ClientInterface interface { AccessRules() AccessRuleService // Accounts returns a service used to manage SecretHub accounts. Accounts() AccountService + // Credentials returns a service used to manage credentials. + Credentials() CredentialService // Dirs returns a service used to manage directories. Dirs() DirService // Me returns a service used to manage the current authenticated account. @@ -168,6 +170,11 @@ func (c *Client) Accounts() AccountService { return newAccountService(c) } +// Credentials returns a service used to manage credentials. +func (c *Client) Credentials() CredentialService { + return newCredentialService(c) +} + // Dirs returns a service used to manage directories. func (c *Client) Dirs() DirService { return newDirService(c) diff --git a/pkg/secrethub/credentials.go b/pkg/secrethub/credentials.go new file mode 100644 index 00000000..312db2c4 --- /dev/null +++ b/pkg/secrethub/credentials.go @@ -0,0 +1,70 @@ +package secrethub + +import ( + "github.com/secrethub/secrethub-go/internals/api" + "github.com/secrethub/secrethub-go/pkg/secrethub/credentials" +) + +// CredentialService handles operations on credentials on SecretHub. +type CredentialService interface { + // Create a new credential from the credentials.Creator for an existing account. + Create(credentials.Creator) error +} + +func newCredentialService(client *Client) CredentialService { + return credentialService{ + client: client, + } +} + +type credentialService struct { + client *Client +} + +// Create a new credential from the credentials.Creator for an existing account. +// This includes a re-encrypted copy the the account key. +func (s credentialService) Create(creator credentials.Creator) error { + accountKey, err := s.client.getAccountKey() + if err != nil { + return err + } + + err = creator.Create() + if err != nil { + return err + } + + verifier := creator.Verifier() + bytes, fingerprint, err := verifier.Export() + if err != nil { + return err + } + + accountKeyRequest, err := s.client.createAccountKeyRequest(creator.Encrypter(), *accountKey) + if err != nil { + return err + } + + req := api.CreateCredentialRequest{ + Fingerprint: fingerprint, + Verifier: bytes, + Type: verifier.Type(), + Metadata: creator.Metadata(), + AccountKey: accountKeyRequest, + } + err = verifier.AddProof(&req) + if err != nil { + return err + } + + err = req.Validate() + if err != nil { + return err + } + + _, err = s.client.httpClient.CreateCredential(&req) + if err != nil { + return err + } + return nil +} diff --git a/pkg/secrethub/internals/http/client.go b/pkg/secrethub/internals/http/client.go index 66d17d9d..7b7b9a65 100644 --- a/pkg/secrethub/internals/http/client.go +++ b/pkg/secrethub/internals/http/client.go @@ -43,6 +43,7 @@ const ( // Account pathAccount = "%s/account/%s" + pathCredentials = "%s/me/credentials" pathCreateAccountKey = "%s/me/credentials/%s/key" // Users @@ -168,6 +169,14 @@ func (c *Client) GetMyUser() (*api.User, error) { return out, errio.Error(err) } +// CreateCredential creates a new credential for the account. +func (c *Client) CreateCredential(in *api.CreateCredentialRequest) (*api.Credential, error) { + out := &api.Credential{} + rawURL := fmt.Sprintf(pathCredentials, c.base) + err := c.post(rawURL, true, http.StatusCreated, in, out) + return out, errio.Error(err) +} + // SendVerificationEmail sends an email to the users registered email address for them to prove they // own that email address. func (c *Client) SendVerificationEmail() error { From 05ebe8d2f16f1c73b4230e4be704cee4b6ed551f Mon Sep 17 00:00:00 2001 From: Joris Coenen Date: Wed, 20 Nov 2019 18:06:01 +0100 Subject: [PATCH 07/35] Validate AccountKey if set --- internals/api/credential.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/internals/api/credential.go b/internals/api/credential.go index 5f0b77a2..6040f3ed 100644 --- a/internals/api/credential.go +++ b/internals/api/credential.go @@ -144,6 +144,12 @@ func (req *CreateCredentialRequest) Validate() error { return err } + if req.AccountKey != nil { + if err := req.AccountKey.Validate(); err != nil { + return err + } + } + if req.Type == CredentialTypeBackupCode { decoded, err := base64.StdEncoding.DecodeString(string(req.Verifier)) if err != nil { From 9fe1c31ca964e7ddbe553d759104c8ae7a31d7ad Mon Sep 17 00:00:00 2001 From: Joris Coenen Date: Thu, 21 Nov 2019 11:48:13 +0100 Subject: [PATCH 08/35] Add credential listing --- pkg/secrethub/credentials.go | 7 +++++++ pkg/secrethub/internals/http/client.go | 8 ++++++++ 2 files changed, 15 insertions(+) diff --git a/pkg/secrethub/credentials.go b/pkg/secrethub/credentials.go index 312db2c4..f9f0524f 100644 --- a/pkg/secrethub/credentials.go +++ b/pkg/secrethub/credentials.go @@ -9,6 +9,8 @@ import ( type CredentialService interface { // Create a new credential from the credentials.Creator for an existing account. Create(credentials.Creator) error + // ListMine lists all credentials of the currently authenticated account. + ListMine() ([]*api.Credential, error) } func newCredentialService(client *Client) CredentialService { @@ -68,3 +70,8 @@ func (s credentialService) Create(creator credentials.Creator) error { } return nil } + +// ListMine lists all credentials of the currently authenticated account. +func (s credentialService) ListMine() ([]*api.Credential, error) { + return s.client.httpClient.ListMyCredentials() +} diff --git a/pkg/secrethub/internals/http/client.go b/pkg/secrethub/internals/http/client.go index 7b7b9a65..96424223 100644 --- a/pkg/secrethub/internals/http/client.go +++ b/pkg/secrethub/internals/http/client.go @@ -177,6 +177,14 @@ func (c *Client) CreateCredential(in *api.CreateCredentialRequest) (*api.Credent return out, errio.Error(err) } +// ListMyCredentials list all the currently authenticated account's credentials. +func (c *Client) ListMyCredentials() ([]*api.Credential, error) { + var out []*api.Credential + rawURL := fmt.Sprintf(pathCredentials, c.base) + err := c.get(rawURL, true, &out) + return out, errio.Error(err) +} + // SendVerificationEmail sends an email to the users registered email address for them to prove they // own that email address. func (c *Client) SendVerificationEmail() error { From 15d66482722ea66ddbab8e163d7de85addbe1de2 Mon Sep 17 00:00:00 2001 From: Joris Coenen Date: Thu, 21 Nov 2019 12:04:38 +0100 Subject: [PATCH 09/35] Embed ClientInterface instead of Client This makes sure the fake client always implements the ClientInterface. --- pkg/secrethub/fakeclient/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/secrethub/fakeclient/client.go b/pkg/secrethub/fakeclient/client.go index c9416ce2..aab68550 100644 --- a/pkg/secrethub/fakeclient/client.go +++ b/pkg/secrethub/fakeclient/client.go @@ -16,7 +16,7 @@ type Client struct { SecretService *SecretService ServiceService *ServiceService UserService *UserService - secrethub.Client + secrethub.ClientInterface } // AccessRules implements the secrethub.Client interface. From cfa8e5e98b73d24f76456d8cae5dced96a63811a Mon Sep 17 00:00:00 2001 From: Joris Coenen Date: Thu, 21 Nov 2019 12:04:38 +0100 Subject: [PATCH 10/35] Embed ClientInterface instead of Client This makes sure the fake client always implements the ClientInterface. --- pkg/secrethub/fakeclient/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/secrethub/fakeclient/client.go b/pkg/secrethub/fakeclient/client.go index c9416ce2..aab68550 100644 --- a/pkg/secrethub/fakeclient/client.go +++ b/pkg/secrethub/fakeclient/client.go @@ -16,7 +16,7 @@ type Client struct { SecretService *SecretService ServiceService *ServiceService UserService *UserService - secrethub.Client + secrethub.ClientInterface } // AccessRules implements the secrethub.Client interface. From 3d54d42b5ac42c10c005d68fd29266e6cfc484ea Mon Sep 17 00:00:00 2001 From: Joris Coenen Date: Thu, 21 Nov 2019 14:48:38 +0100 Subject: [PATCH 11/35] Add credential name validation --- internals/api/credential.go | 7 +++++++ internals/api/patterns.go | 11 +++++++++++ 2 files changed, 18 insertions(+) diff --git a/internals/api/credential.go b/internals/api/credential.go index 6040f3ed..d0f7f045 100644 --- a/internals/api/credential.go +++ b/internals/api/credential.go @@ -19,6 +19,7 @@ var ( ErrInvalidFingerprint = errAPI.Code("invalid_fingerprint").StatusError("fingerprint is invalid", http.StatusBadRequest) ErrInvalidVerifier = errAPI.Code("invalid_verifier").StatusError("verifier is invalid", http.StatusBadRequest) ErrInvalidCredentialType = errAPI.Code("invalid_credential_type").StatusError("credential type is invalid", http.StatusBadRequest) + ErrInvalidCredentialName = errAPI.Code("invalid_credential_name").StatusError("credential name must be between 1 and 20 characters long", http.StatusBadRequest) ErrInvalidAWSEndpoint = errAPI.Code("invalid_aws_endpoint").StatusError("invalid AWS endpoint provided", http.StatusBadRequest) ErrInvalidProof = errAPI.Code("invalid_proof").StatusError("invalid proof provided for credential", http.StatusUnauthorized) ErrAWSAccountMismatch = errAPI.Code("aws_account_mismatch").StatusError("the AWS Account ID in the role ARN does not match the AWS Account ID of the AWS credentials used for authentication. Make sure you are using AWS credentials that correspond to the role you are trying to add.", http.StatusUnauthorized) @@ -139,6 +140,12 @@ func (req *CreateCredentialRequest) Validate() error { return ErrMissingField("type") } + if req.Name != "" { + if err := ValidateCredentialName(req.Name); err != nil { + return err + } + } + err := req.Type.Validate() if err != nil { return err diff --git a/internals/api/patterns.go b/internals/api/patterns.go index aa754c5d..df9143a2 100644 --- a/internals/api/patterns.go +++ b/internals/api/patterns.go @@ -251,3 +251,14 @@ func ValidateDirPath(path string) error { return nil } + +// ValidateCredentialName validates the name for a credential. +func ValidateCredentialName(name string) error { + if len(name) > 20 { + return ErrInvalidCredentialName + } + if !whitelistDescription.MatchString(name) { + return ErrInvalidCredentialName + } + return nil +} From 3361c12596618443e886497f071b0194a8dbce6f Mon Sep 17 00:00:00 2001 From: Joris Coenen Date: Fri, 22 Nov 2019 14:49:54 +0100 Subject: [PATCH 12/35] Fix typo and add comment --- pkg/secrethub/credentials/bootstrap_code.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/secrethub/credentials/bootstrap_code.go b/pkg/secrethub/credentials/bootstrap_code.go index 32c92b52..3742847b 100644 --- a/pkg/secrethub/credentials/bootstrap_code.go +++ b/pkg/secrethub/credentials/bootstrap_code.go @@ -11,6 +11,7 @@ import ( "github.com/secrethub/secrethub-go/pkg/secrethub/internals/http" ) +// Enforce implementation of interfaces by structs. var _ Creator = (*BackupCodeCreator)(nil) var _ Provider = (*bootstrapCodeProvider)(nil) var _ auth.Signer = (*bootstrapCode)(nil) @@ -25,7 +26,7 @@ func CreateBackupCode() *BackupCodeCreator { return &BackupCodeCreator{} } -// Create generates a new code and stores it in the BackupCodeGenerator. +// Create generates a new code and stores it in the BackupCodeCreator. func (b *BackupCodeCreator) Create() error { key, err := crypto.GenerateSymmetricKey() if err != nil { From 9aeb872b8dc96a8e91cbdea8da47e4b8b811d8fe Mon Sep 17 00:00:00 2001 From: Joris Coenen Date: Mon, 25 Nov 2019 12:38:59 +0100 Subject: [PATCH 13/35] First try in stub credential iterator --- pkg/secrethub/credentials.go | 36 +++++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/pkg/secrethub/credentials.go b/pkg/secrethub/credentials.go index f9f0524f..c073a205 100644 --- a/pkg/secrethub/credentials.go +++ b/pkg/secrethub/credentials.go @@ -1,6 +1,8 @@ package secrethub import ( + "github.com/secrethub/secrethub-go/pkg/secrethub/iterator" + "github.com/secrethub/secrethub-go/internals/api" "github.com/secrethub/secrethub-go/pkg/secrethub/credentials" ) @@ -9,8 +11,8 @@ import ( type CredentialService interface { // Create a new credential from the credentials.Creator for an existing account. Create(credentials.Creator) error - // ListMine lists all credentials of the currently authenticated account. - ListMine() ([]*api.Credential, error) + // List lists all credentials of the currently authenticated account. + List() (CredentialIterator, error) } func newCredentialService(client *Client) CredentialService { @@ -71,7 +73,31 @@ func (s credentialService) Create(creator credentials.Creator) error { return nil } -// ListMine lists all credentials of the currently authenticated account. -func (s credentialService) ListMine() ([]*api.Credential, error) { - return s.client.httpClient.ListMyCredentials() +type CredentialIterator interface { + Next() (api.Credential, error) +} + +type credentialIterator struct { + credentials []*api.Credential + currentIndex int +} + +func (c *credentialIterator) Next() (api.Credential, error) { + currentIndex := c.currentIndex + if currentIndex >= len(c.credentials) { + return api.Credential{}, iterator.Done + } + c.currentIndex++ + return *c.credentials[currentIndex], nil +} + +// List lists all credentials of the currently authenticated account. +func (s credentialService) List() (CredentialIterator, error) { + creds, err := s.client.httpClient.ListMyCredentials() + if err != nil { + return nil, err + } + return &credentialIterator{ + credentials: creds, + }, nil } From bc89579eddd5a6b704cafca83b53dafed38ad04b Mon Sep 17 00:00:00 2001 From: Joris Coenen Date: Tue, 26 Nov 2019 10:00:16 +0100 Subject: [PATCH 14/35] Add Enabled field to credential --- internals/api/credential.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internals/api/credential.go b/internals/api/credential.go index d0f7f045..9ee47213 100644 --- a/internals/api/credential.go +++ b/internals/api/credential.go @@ -47,6 +47,7 @@ type Credential struct { Name string `json:"name"` Verifier []byte `json:"verifier"` Metadata map[string]string `json:"metadata,omitempty"` + Enabled bool `json:"enabled"` } // CredentialType is used to identify the type of algorithm that is used for a credential. From c77c8d8560c6e0aacc4497eb94b529ffddd85dcc Mon Sep 17 00:00:00 2001 From: Joris Coenen Date: Tue, 26 Nov 2019 10:21:36 +0100 Subject: [PATCH 15/35] Add UpdateCredentialRequest --- internals/api/credential.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/internals/api/credential.go b/internals/api/credential.go index 9ee47213..ee01f366 100644 --- a/internals/api/credential.go +++ b/internals/api/credential.go @@ -226,6 +226,16 @@ type CredentialProofKey struct{} // CredentialProofBackupCode is proof for when the credential type is backup key. type CredentialProofBackupCode struct{} +// UpdateCredentialRequest contains the fields of a credential that can be updated. +type UpdateCredentialRequest struct { + Enabled *bool `json:"enabled,omitempty"` +} + +// Validate whether the UpdateCredentialRequest is a valid request. +func (req *UpdateCredentialRequest) Validate() error { + return nil +} + // GetFingerprint returns the fingerprint of a credential. func GetFingerprint(t CredentialType, verifier []byte) string { var toHash []byte From 5376bd58d5ea2d59bd0007723e9b922ab1fa0ea5 Mon Sep 17 00:00:00 2001 From: Joris Coenen Date: Tue, 26 Nov 2019 11:27:25 +0100 Subject: [PATCH 16/35] Add function to validate credential fingerprints --- internals/api/patterns.go | 14 +++++++++++++ internals/api/patterns_test.go | 37 ++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/internals/api/patterns.go b/internals/api/patterns.go index df9143a2..d5a58161 100644 --- a/internals/api/patterns.go +++ b/internals/api/patterns.go @@ -47,6 +47,8 @@ var ( whitelistSecretPathInDirPath = regexp.MustCompile(fmt.Sprintf(`((?i)^(%s\/%s\/%s(\/%s)*(?:\:.+)?)$)`, patternUniformName, patternUniformName, patternUniformName, patternUniformName)) whitelistSecretVersionIdentifierInSecretPath = regexp.MustCompile(fmt.Sprintf(`(?i)^(%s)\/(%s)\/(%s\/)*(%s)(:(.+)?)$`, patternUniformName, patternUniformName, patternUniformName, patternUniformName)) whitelistSecretVersionInSecretPath = regexp.MustCompile(fmt.Sprintf(`(?i)^(%s)\/(%s)\/(%s\/)*(%s)(:([0-9]{1,9}|latest))$`, patternUniformName, patternUniformName, patternUniformName, patternUniformName)) + + whitelistCredentialFingerprint = regexp.MustCompile("^[0-9a-fA-F]{64}$") ) // Errors @@ -74,6 +76,10 @@ var ( "directory roles must be either read, write, or admin", http.StatusBadRequest, ) + ErrInvalidCredentialFingerprint = errAPI.Code("invalid_credential_fingerprint").StatusError( + "credential fingerprint must consist of 64 hexadecimal characters", + http.StatusBadRequest, + ) ) // ValidateNamespace validates a username. @@ -262,3 +268,11 @@ func ValidateCredentialName(name string) error { } return nil } + +// ValidateCredentialFingerprint validates whether the given string is a valid credential fingerprint. +func ValidateCredentialFingerprint(fingerprint string) error { + if !whitelistCredentialFingerprint.MatchString(fingerprint) { + return ErrInvalidFingerprint + } + return nil +} diff --git a/internals/api/patterns_test.go b/internals/api/patterns_test.go index 422764f8..73e870c2 100644 --- a/internals/api/patterns_test.go +++ b/internals/api/patterns_test.go @@ -427,3 +427,40 @@ func TestValidateSecretPath(t *testing.T) { } } } + +func TestValidateCredentialFingerprint(t *testing.T) { + cases := map[string]struct { + in string + expected error + }{ + "valid lowercase": { + in: "d9db31d1bfd9a8a55a4dd715501017fd8d2c33025cb05049664eaf195dafb801", + }, + "valid uppercase": { + in: "D9DB31D1BFD9A8A55A4DD715501017FD8D2C33025CB05049664EAF195DAFB801", + }, + "valid mixed case": { + in: "d9db31d1bfd9a8a55a4dd715501017FD8D2C33025CB05049664EAF195DAFB801", + }, + "too short": { + in: "d9db31d1bfd9a8a55a4dd715501017fd8d2c33025cb05049664eaf195dafb80", + expected: api.ErrInvalidFingerprint, + }, + "too long": { + in: "d9db31d1bfd9a8a55a4dd715501017fd8d2c33025cb05049664eaf195dafb801b", + expected: api.ErrInvalidFingerprint, + }, + "illegal character": { + in: "Q9db31d1bfd9a8a55a4dd715501017fd8d2c33025cb05049664eaf195dafb801", + expected: api.ErrInvalidFingerprint, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + err := api.ValidateCredentialFingerprint(tc.in) + + assert.Equal(t, err, tc.expected) + }) + } +} From a2fec52e83ff3cac4e1fbba679eaef7baf1694b3 Mon Sep 17 00:00:00 2001 From: Joris Coenen Date: Tue, 26 Nov 2019 13:51:52 +0100 Subject: [PATCH 17/35] Add support for short credential fingerprint This is the first part of a credential fingerprint. Updating credentials also allows only the first part of the fingerprint, as long as it uniquely refers to a credential. --- internals/api/credential.go | 34 ++++++++++++++++++++-------------- internals/api/patterns.go | 17 ++++++++++++++++- 2 files changed, 36 insertions(+), 15 deletions(-) diff --git a/internals/api/credential.go b/internals/api/credential.go index ee01f366..4dba4ab3 100644 --- a/internals/api/credential.go +++ b/internals/api/credential.go @@ -16,20 +16,22 @@ import ( // Errors var ( - ErrInvalidFingerprint = errAPI.Code("invalid_fingerprint").StatusError("fingerprint is invalid", http.StatusBadRequest) - ErrInvalidVerifier = errAPI.Code("invalid_verifier").StatusError("verifier is invalid", http.StatusBadRequest) - ErrInvalidCredentialType = errAPI.Code("invalid_credential_type").StatusError("credential type is invalid", http.StatusBadRequest) - ErrInvalidCredentialName = errAPI.Code("invalid_credential_name").StatusError("credential name must be between 1 and 20 characters long", http.StatusBadRequest) - ErrInvalidAWSEndpoint = errAPI.Code("invalid_aws_endpoint").StatusError("invalid AWS endpoint provided", http.StatusBadRequest) - ErrInvalidProof = errAPI.Code("invalid_proof").StatusError("invalid proof provided for credential", http.StatusUnauthorized) - ErrAWSAccountMismatch = errAPI.Code("aws_account_mismatch").StatusError("the AWS Account ID in the role ARN does not match the AWS Account ID of the AWS credentials used for authentication. Make sure you are using AWS credentials that correspond to the role you are trying to add.", http.StatusUnauthorized) - ErrAWSAuthFailed = errAPI.Code("aws_auth_failed").StatusError("authentication not accepted by AWS", http.StatusUnauthorized) - ErrAWSKMSKeyNotFound = errAPI.Code("aws_kms_key_not_found").StatusError("could not found the KMS key", http.StatusNotFound) - ErrInvalidRoleARN = errAPI.Code("invalid_role_arn").StatusError("provided role is not a valid ARN", http.StatusBadRequest) - ErrMissingMetadata = errAPI.Code("missing_metadata").StatusErrorPref("expecting %s metadata provided for credentials of type %s", http.StatusBadRequest) - ErrInvalidMetadataKey = errAPI.Code("invalid_metadata_key").StatusErrorPref("invalid metadata key %s for credential type %s", http.StatusBadRequest) - ErrUnknownMetadataKey = errAPI.Code("unknown_metadata_key").StatusErrorPref("unknown metadata key: %s", http.StatusBadRequest) - ErrRoleDoesNotMatch = errAPI.Code("role_does_not_match").StatusError("role in metadata does not match the verifier", http.StatusBadRequest) + ErrInvalidFingerprint = errAPI.Code("invalid_fingerprint").StatusError("fingerprint is invalid", http.StatusBadRequest) + ErrTooShortFingerprint = errAPI.Code("too_short_fingerprint").StatusErrorf("at least %d characters of the fingerprint must be entered", http.StatusBadRequest, ShortCredentialFingerprintMinimumLength) + ErrCredentialFingerprintNotUnique = errAPI.Code("fingerprint_not_unique").StatusErrorf("there are multiple credentials that start with the given fingerprint. Please use the full fingerprint", http.StatusConflict) + ErrInvalidVerifier = errAPI.Code("invalid_verifier").StatusError("verifier is invalid", http.StatusBadRequest) + ErrInvalidCredentialType = errAPI.Code("invalid_credential_type").StatusError("credential type is invalid", http.StatusBadRequest) + ErrInvalidCredentialName = errAPI.Code("invalid_credential_name").StatusError("credential name must be between 1 and 20 characters long", http.StatusBadRequest) + ErrInvalidAWSEndpoint = errAPI.Code("invalid_aws_endpoint").StatusError("invalid AWS endpoint provided", http.StatusBadRequest) + ErrInvalidProof = errAPI.Code("invalid_proof").StatusError("invalid proof provided for credential", http.StatusUnauthorized) + ErrAWSAccountMismatch = errAPI.Code("aws_account_mismatch").StatusError("the AWS Account ID in the role ARN does not match the AWS Account ID of the AWS credentials used for authentication. Make sure you are using AWS credentials that correspond to the role you are trying to add.", http.StatusUnauthorized) + ErrAWSAuthFailed = errAPI.Code("aws_auth_failed").StatusError("authentication not accepted by AWS", http.StatusUnauthorized) + ErrAWSKMSKeyNotFound = errAPI.Code("aws_kms_key_not_found").StatusError("could not found the KMS key", http.StatusNotFound) + ErrInvalidRoleARN = errAPI.Code("invalid_role_arn").StatusError("provided role is not a valid ARN", http.StatusBadRequest) + ErrMissingMetadata = errAPI.Code("missing_metadata").StatusErrorPref("expecting %s metadata provided for credentials of type %s", http.StatusBadRequest) + ErrInvalidMetadataKey = errAPI.Code("invalid_metadata_key").StatusErrorPref("invalid metadata key %s for credential type %s", http.StatusBadRequest) + ErrUnknownMetadataKey = errAPI.Code("unknown_metadata_key").StatusErrorPref("unknown metadata key: %s", http.StatusBadRequest) + ErrRoleDoesNotMatch = errAPI.Code("role_does_not_match").StatusError("role in metadata does not match the verifier", http.StatusBadRequest) ) // Credential metadata keys @@ -38,6 +40,10 @@ const ( CredentialMetadataAWSRole = "aws_role" ) +const ( + ShortCredentialFingerprintMinimumLength = 10 +) + // Credential is used to authenticate to the API and to encrypt the account key. type Credential struct { AccountID uuid.UUID `json:"account_id"` diff --git a/internals/api/patterns.go b/internals/api/patterns.go index d5a58161..68239a59 100644 --- a/internals/api/patterns.go +++ b/internals/api/patterns.go @@ -48,7 +48,7 @@ var ( whitelistSecretVersionIdentifierInSecretPath = regexp.MustCompile(fmt.Sprintf(`(?i)^(%s)\/(%s)\/(%s\/)*(%s)(:(.+)?)$`, patternUniformName, patternUniformName, patternUniformName, patternUniformName)) whitelistSecretVersionInSecretPath = regexp.MustCompile(fmt.Sprintf(`(?i)^(%s)\/(%s)\/(%s\/)*(%s)(:([0-9]{1,9}|latest))$`, patternUniformName, patternUniformName, patternUniformName, patternUniformName)) - whitelistCredentialFingerprint = regexp.MustCompile("^[0-9a-fA-F]{64}$") + whitelistCredentialFingerprint = regexp.MustCompile("^[0-9a-fA-F]{1,64}$") ) // Errors @@ -274,5 +274,20 @@ func ValidateCredentialFingerprint(fingerprint string) error { if !whitelistCredentialFingerprint.MatchString(fingerprint) { return ErrInvalidFingerprint } + if len(fingerprint) != 64 { + return ErrInvalidFingerprint + } + return nil +} + +// ValidateShortCredentialFingerprint validates whether the given string can be used as a short version of a credential +// fingerprint. +func ValidateShortCredentialFingerprint(fingerprint string) error { + if !whitelistCredentialFingerprint.MatchString(fingerprint) { + return ErrInvalidFingerprint + } + if len(fingerprint) < ShortCredentialFingerprintMinimumLength { + return ErrTooShortFingerprint + } return nil } From 4d795aa1757abdac5a9ccea44677fd6eea11c39e Mon Sep 17 00:00:00 2001 From: Joris Coenen Date: Tue, 26 Nov 2019 13:52:40 +0100 Subject: [PATCH 18/35] Implement disabling of credentials --- pkg/secrethub/credentials.go | 21 +++++++++++++++++++++ pkg/secrethub/internals/http/client.go | 8 ++++++++ 2 files changed, 29 insertions(+) diff --git a/pkg/secrethub/credentials.go b/pkg/secrethub/credentials.go index 312db2c4..d44c0142 100644 --- a/pkg/secrethub/credentials.go +++ b/pkg/secrethub/credentials.go @@ -9,6 +9,8 @@ import ( type CredentialService interface { // Create a new credential from the credentials.Creator for an existing account. Create(credentials.Creator) error + // Disable an existing credential. + Disable(fingerprint string) error } func newCredentialService(client *Client) CredentialService { @@ -68,3 +70,22 @@ func (s credentialService) Create(creator credentials.Creator) error { } return nil } + +// Disable an existing credential. +func (s credentialService) Disable(fingerprint string) error { + err := api.ValidateShortCredentialFingerprint(fingerprint) + if err != nil { + return err + } + + f := false + req := &api.UpdateCredentialRequest{ + Enabled: &f, + } + err = req.Validate() + if err != nil { + return err + } + + return s.client.httpClient.UpdateCredential(fingerprint, req) +} diff --git a/pkg/secrethub/internals/http/client.go b/pkg/secrethub/internals/http/client.go index 7b7b9a65..53c6bd3b 100644 --- a/pkg/secrethub/internals/http/client.go +++ b/pkg/secrethub/internals/http/client.go @@ -44,6 +44,7 @@ const ( // Account pathAccount = "%s/account/%s" pathCredentials = "%s/me/credentials" + pathCredential = "%s/me/credentials/%s" pathCreateAccountKey = "%s/me/credentials/%s/key" // Users @@ -177,6 +178,13 @@ func (c *Client) CreateCredential(in *api.CreateCredentialRequest) (*api.Credent return out, errio.Error(err) } +// UpdateCredential updates an existing credential. +func (c *Client) UpdateCredential(fingerprint string, in *api.UpdateCredentialRequest) error { + rawURL := fmt.Sprintf(pathCredential, c.base, fingerprint) + err := c.patch(rawURL, true, http.StatusNoContent, in, nil) + return err +} + // SendVerificationEmail sends an email to the users registered email address for them to prove they // own that email address. func (c *Client) SendVerificationEmail() error { From f1b5ff22737fd33b71d1623d27db5b8fc46ada50 Mon Sep 17 00:00:00 2001 From: Joris Coenen Date: Tue, 26 Nov 2019 14:26:06 +0100 Subject: [PATCH 19/35] Add error for disabling currently used credential --- internals/api/credential.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internals/api/credential.go b/internals/api/credential.go index 4dba4ab3..51e820e8 100644 --- a/internals/api/credential.go +++ b/internals/api/credential.go @@ -32,6 +32,7 @@ var ( ErrInvalidMetadataKey = errAPI.Code("invalid_metadata_key").StatusErrorPref("invalid metadata key %s for credential type %s", http.StatusBadRequest) ErrUnknownMetadataKey = errAPI.Code("unknown_metadata_key").StatusErrorPref("unknown metadata key: %s", http.StatusBadRequest) ErrRoleDoesNotMatch = errAPI.Code("role_does_not_match").StatusError("role in metadata does not match the verifier", http.StatusBadRequest) + ErrCannotDisableCurrentCredential = errAPI.Code("cannot_disable_current_credential").StatusError("cannot disable the credential that is currently used on this device", http.StatusConflict) ) // Credential metadata keys From 08bd9cb362b7b7ec082d6fec2ae69cbbebadc65d Mon Sep 17 00:00:00 2001 From: Joris Coenen Date: Tue, 26 Nov 2019 14:30:21 +0100 Subject: [PATCH 20/35] Increase allowed credential name length --- internals/api/patterns.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internals/api/patterns.go b/internals/api/patterns.go index 68239a59..3a56fbd8 100644 --- a/internals/api/patterns.go +++ b/internals/api/patterns.go @@ -260,7 +260,7 @@ func ValidateDirPath(path string) error { // ValidateCredentialName validates the name for a credential. func ValidateCredentialName(name string) error { - if len(name) > 20 { + if len(name) > 32 { return ErrInvalidCredentialName } if !whitelistDescription.MatchString(name) { From 8b470370f13e085460405f614ba22273650a7b03 Mon Sep 17 00:00:00 2001 From: Joris Coenen Date: Mon, 25 Nov 2019 13:18:02 +0100 Subject: [PATCH 21/35] Allow setting the credential name for new credentials For now we're not setting the credential name on a signup, as this is a backwards-incompatible change. We can later add the functionality to rename any credentials without a name. --- pkg/secrethub/credentials.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/secrethub/credentials.go b/pkg/secrethub/credentials.go index d44c0142..30b61e98 100644 --- a/pkg/secrethub/credentials.go +++ b/pkg/secrethub/credentials.go @@ -8,7 +8,7 @@ import ( // CredentialService handles operations on credentials on SecretHub. type CredentialService interface { // Create a new credential from the credentials.Creator for an existing account. - Create(credentials.Creator) error + Create(string, credentials.Creator) error // Disable an existing credential. Disable(fingerprint string) error } @@ -25,7 +25,7 @@ type credentialService struct { // Create a new credential from the credentials.Creator for an existing account. // This includes a re-encrypted copy the the account key. -func (s credentialService) Create(creator credentials.Creator) error { +func (s credentialService) Create(name string, creator credentials.Creator) error { accountKey, err := s.client.getAccountKey() if err != nil { return err @@ -48,6 +48,7 @@ func (s credentialService) Create(creator credentials.Creator) error { } req := api.CreateCredentialRequest{ + Name: name, Fingerprint: fingerprint, Verifier: bytes, Type: verifier.Type(), From 564fa33f2e5c22db076be8bf24d2efeedddbabc8 Mon Sep 17 00:00:00 2001 From: Joris Coenen Date: Wed, 27 Nov 2019 11:44:26 +0100 Subject: [PATCH 22/35] Deprecate credential.Name in favour of credential.Description The new name better reflect that the field is optional. --- internals/api/credential.go | 10 ++++----- internals/api/credential_test.go | 38 ++++++++++++++++++++------------ internals/api/patterns.go | 12 +++++----- internals/api/user_test.go | 1 - pkg/secrethub/credentials.go | 12 +++++++--- 5 files changed, 44 insertions(+), 29 deletions(-) diff --git a/internals/api/credential.go b/internals/api/credential.go index 51e820e8..dbd07554 100644 --- a/internals/api/credential.go +++ b/internals/api/credential.go @@ -21,7 +21,7 @@ var ( ErrCredentialFingerprintNotUnique = errAPI.Code("fingerprint_not_unique").StatusErrorf("there are multiple credentials that start with the given fingerprint. Please use the full fingerprint", http.StatusConflict) ErrInvalidVerifier = errAPI.Code("invalid_verifier").StatusError("verifier is invalid", http.StatusBadRequest) ErrInvalidCredentialType = errAPI.Code("invalid_credential_type").StatusError("credential type is invalid", http.StatusBadRequest) - ErrInvalidCredentialName = errAPI.Code("invalid_credential_name").StatusError("credential name must be between 1 and 20 characters long", http.StatusBadRequest) + ErrInvalidCredentialDescription = errAPI.Code("invalid_credential_description").StatusError("credential description must be between 1 and 20 characters long", http.StatusBadRequest) ErrInvalidAWSEndpoint = errAPI.Code("invalid_aws_endpoint").StatusError("invalid AWS endpoint provided", http.StatusBadRequest) ErrInvalidProof = errAPI.Code("invalid_proof").StatusError("invalid proof provided for credential", http.StatusUnauthorized) ErrAWSAccountMismatch = errAPI.Code("aws_account_mismatch").StatusError("the AWS Account ID in the role ARN does not match the AWS Account ID of the AWS credentials used for authentication. Make sure you are using AWS credentials that correspond to the role you are trying to add.", http.StatusUnauthorized) @@ -51,7 +51,7 @@ type Credential struct { Type CredentialType `json:"type"` CreatedAt time.Time `json:"created_at"` Fingerprint string `json:"fingerprint"` - Name string `json:"name"` + Description string `json:"description"` Verifier []byte `json:"verifier"` Metadata map[string]string `json:"metadata,omitempty"` Enabled bool `json:"enabled"` @@ -89,7 +89,7 @@ func (a CredentialType) Validate() error { type CreateCredentialRequest struct { Type CredentialType `json:"type"` Fingerprint string `json:"fingerprint"` - Name string `json:"name,omitempty"` + Description *string `json:"name,omitempty"` Verifier []byte `json:"verifier"` Proof interface{} `json:"proof"` Metadata map[string]string `json:"metadata"` @@ -148,8 +148,8 @@ func (req *CreateCredentialRequest) Validate() error { return ErrMissingField("type") } - if req.Name != "" { - if err := ValidateCredentialName(req.Name); err != nil { + if req.Description != nil { + if err := ValidateCredentialDescription(*req.Description); err != nil { return err } } diff --git a/internals/api/credential_test.go b/internals/api/credential_test.go index a63f9ef8..e25f5504 100644 --- a/internals/api/credential_test.go +++ b/internals/api/credential_test.go @@ -7,13 +7,23 @@ import ( ) func TestCreateCredentialRequest_Validate(t *testing.T) { + description := "Personal laptop credential" + cases := map[string]struct { req CreateCredentialRequest err error }{ "success": { req: CreateCredentialRequest{ - Name: "Personal laptop credential", + Description: &description, + Type: CredentialTypeKey, + Fingerprint: "88c9eae68eb300b2971a2bec9e5a26ff4179fd661d6b7d861e4c6557b9aaee14", + Verifier: []byte("verifier"), + }, + err: nil, + }, + "success without description": { + req: CreateCredentialRequest{ Type: CredentialTypeKey, Fingerprint: "88c9eae68eb300b2971a2bec9e5a26ff4179fd661d6b7d861e4c6557b9aaee14", Verifier: []byte("verifier"), @@ -22,7 +32,7 @@ func TestCreateCredentialRequest_Validate(t *testing.T) { }, "success including account key": { req: CreateCredentialRequest{ - Name: "Personal laptop credential", + Description: &description, Type: CredentialTypeKey, Fingerprint: "88c9eae68eb300b2971a2bec9e5a26ff4179fd661d6b7d861e4c6557b9aaee14", Verifier: []byte("verifier"), @@ -35,7 +45,7 @@ func TestCreateCredentialRequest_Validate(t *testing.T) { }, "including invalid account key": { req: CreateCredentialRequest{ - Name: "Personal laptop credential", + Description: &description, Type: CredentialTypeKey, Fingerprint: "88c9eae68eb300b2971a2bec9e5a26ff4179fd661d6b7d861e4c6557b9aaee14", Verifier: []byte("verifier"), @@ -43,7 +53,7 @@ func TestCreateCredentialRequest_Validate(t *testing.T) { }, err: ErrInvalidPublicKey, }, - "success without name": { + "success without Description": { req: CreateCredentialRequest{ Type: CredentialTypeKey, Fingerprint: "88c9eae68eb300b2971a2bec9e5a26ff4179fd661d6b7d861e4c6557b9aaee14", @@ -53,16 +63,16 @@ func TestCreateCredentialRequest_Validate(t *testing.T) { }, "no fingerprint": { req: CreateCredentialRequest{ - Type: CredentialTypeKey, - Name: "Personal laptop credential", - Verifier: []byte("verifier"), + Type: CredentialTypeKey, + Description: &description, + Verifier: []byte("verifier"), }, err: ErrMissingField("fingerprint"), }, "invalid fingerprint": { req: CreateCredentialRequest{ Type: CredentialTypeKey, - Name: "Personal laptop credential", + Description: &description, Fingerprint: "not-valid", Verifier: []byte("verifier"), }, @@ -71,7 +81,7 @@ func TestCreateCredentialRequest_Validate(t *testing.T) { "empty verifier": { req: CreateCredentialRequest{ Type: CredentialTypeKey, - Name: "Personal laptop credential", + Description: &description, Fingerprint: "fingerprint", Verifier: nil, }, @@ -79,7 +89,7 @@ func TestCreateCredentialRequest_Validate(t *testing.T) { }, "empty type": { req: CreateCredentialRequest{ - Name: "Personal laptop credential", + Description: &description, Fingerprint: "88c9eae68eb300b2971a2bec9e5a26ff4179fd661d6b7d861e4c6557b9aaee14", Verifier: []byte("verifier"), }, @@ -87,7 +97,7 @@ func TestCreateCredentialRequest_Validate(t *testing.T) { }, "invalid type": { req: CreateCredentialRequest{ - Name: "Personal laptop credential", + Description: &description, Fingerprint: "88c9eae68eb300b2971a2bec9e5a26ff4179fd661d6b7d861e4c6557b9aaee14", Verifier: []byte("verifier"), Type: CredentialType("invalid"), @@ -133,7 +143,7 @@ func TestCreateCredentialRequest_Validate(t *testing.T) { }, "extra metadata": { req: CreateCredentialRequest{ - Name: "Personal laptop credential", + Description: &description, Type: CredentialTypeKey, Fingerprint: "88c9eae68eb300b2971a2bec9e5a26ff4179fd661d6b7d861e4c6557b9aaee14", Verifier: []byte("verifier"), @@ -201,8 +211,8 @@ func TestCreateCredentialRequest_Validate(t *testing.T) { }, } - for name, tc := range cases { - t.Run(name, func(t *testing.T) { + for Description, tc := range cases { + t.Run(Description, func(t *testing.T) { // Do err := tc.req.Validate() diff --git a/internals/api/patterns.go b/internals/api/patterns.go index 3a56fbd8..b240a3b1 100644 --- a/internals/api/patterns.go +++ b/internals/api/patterns.go @@ -258,13 +258,13 @@ func ValidateDirPath(path string) error { return nil } -// ValidateCredentialName validates the name for a credential. -func ValidateCredentialName(name string) error { - if len(name) > 32 { - return ErrInvalidCredentialName +// ValidateCredentialDescription validates the description for a credential. +func ValidateCredentialDescription(description string) error { + if len(description) < 1 || len(description) > 32 { + return ErrInvalidCredentialDescription } - if !whitelistDescription.MatchString(name) { - return ErrInvalidCredentialName + if !whitelistDescription.MatchString(description) { + return ErrInvalidCredentialDescription } return nil } diff --git a/internals/api/user_test.go b/internals/api/user_test.go index 41ca9e28..feef638a 100644 --- a/internals/api/user_test.go +++ b/internals/api/user_test.go @@ -189,7 +189,6 @@ func TestCreateUserRequest_Validate(t *testing.T) { Email: "test-account.dev1@secrethub.io", FullName: "Test Tester", Credential: &CreateCredentialRequest{ - Name: "Personal laptop credential", Type: CredentialTypeKey, Fingerprint: "88c9eae68eb300b2971a2bec9e5a26ff4179fd661d6b7d861e4c6557b9aaee14", Verifier: []byte("verifier"), diff --git a/pkg/secrethub/credentials.go b/pkg/secrethub/credentials.go index 30b61e98..45d30dea 100644 --- a/pkg/secrethub/credentials.go +++ b/pkg/secrethub/credentials.go @@ -8,7 +8,7 @@ import ( // CredentialService handles operations on credentials on SecretHub. type CredentialService interface { // Create a new credential from the credentials.Creator for an existing account. - Create(string, credentials.Creator) error + Create(credentials.Creator, string) error // Disable an existing credential. Disable(fingerprint string) error } @@ -25,7 +25,8 @@ type credentialService struct { // Create a new credential from the credentials.Creator for an existing account. // This includes a re-encrypted copy the the account key. -func (s credentialService) Create(name string, creator credentials.Creator) error { +// Description is optional and can be left empty. +func (s credentialService) Create(creator credentials.Creator, description string) error { accountKey, err := s.client.getAccountKey() if err != nil { return err @@ -47,10 +48,15 @@ func (s credentialService) Create(name string, creator credentials.Creator) erro return err } + var reqDescription *string + if description != "" { + reqDescription = &description + } + req := api.CreateCredentialRequest{ - Name: name, Fingerprint: fingerprint, Verifier: bytes, + Description: reqDescription, Type: verifier.Type(), Metadata: creator.Metadata(), AccountKey: accountKeyRequest, From 8bcb89f3c0846bbf96ccdaa3c282ba137e7640ff Mon Sep 17 00:00:00 2001 From: Joris Coenen Date: Thu, 28 Nov 2019 11:06:06 +0100 Subject: [PATCH 23/35] Accept new server response for updating credential --- pkg/secrethub/credentials.go | 3 ++- pkg/secrethub/internals/http/client.go | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/pkg/secrethub/credentials.go b/pkg/secrethub/credentials.go index d44c0142..60a42278 100644 --- a/pkg/secrethub/credentials.go +++ b/pkg/secrethub/credentials.go @@ -87,5 +87,6 @@ func (s credentialService) Disable(fingerprint string) error { return err } - return s.client.httpClient.UpdateCredential(fingerprint, req) + _, err = s.client.httpClient.UpdateCredential(fingerprint, req) + return err } diff --git a/pkg/secrethub/internals/http/client.go b/pkg/secrethub/internals/http/client.go index 53c6bd3b..86ac8a5c 100644 --- a/pkg/secrethub/internals/http/client.go +++ b/pkg/secrethub/internals/http/client.go @@ -179,10 +179,11 @@ func (c *Client) CreateCredential(in *api.CreateCredentialRequest) (*api.Credent } // UpdateCredential updates an existing credential. -func (c *Client) UpdateCredential(fingerprint string, in *api.UpdateCredentialRequest) error { +func (c *Client) UpdateCredential(fingerprint string, in *api.UpdateCredentialRequest) (*api.Credential, error) { + var out api.Credential rawURL := fmt.Sprintf(pathCredential, c.base, fingerprint) - err := c.patch(rawURL, true, http.StatusNoContent, in, nil) - return err + err := c.patch(rawURL, true, http.StatusOK, in, &out) + return &out, err } // SendVerificationEmail sends an email to the users registered email address for them to prove they From 4d6426e361882da092338c7fb0476b085de6b39b Mon Sep 17 00:00:00 2001 From: Joris Coenen Date: Thu, 28 Nov 2019 14:51:55 +0100 Subject: [PATCH 24/35] Return an iterator for Credentials.List() --- pkg/secrethub/credentials.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pkg/secrethub/credentials.go b/pkg/secrethub/credentials.go index c073a205..a1a81d9b 100644 --- a/pkg/secrethub/credentials.go +++ b/pkg/secrethub/credentials.go @@ -12,7 +12,7 @@ type CredentialService interface { // Create a new credential from the credentials.Creator for an existing account. Create(credentials.Creator) error // List lists all credentials of the currently authenticated account. - List() (CredentialIterator, error) + List(_ *CredentialListParams) (CredentialIterator, error) } func newCredentialService(client *Client) CredentialService { @@ -73,6 +73,10 @@ func (s credentialService) Create(creator credentials.Creator) error { return nil } +// CredentialListParams are the parameters than configure credential listing. +type CredentialListParams struct{} + +// CredentialIterator can be used to iterate over a list of credentials. type CredentialIterator interface { Next() (api.Credential, error) } @@ -91,8 +95,8 @@ func (c *credentialIterator) Next() (api.Credential, error) { return *c.credentials[currentIndex], nil } -// List lists all credentials of the currently authenticated account. -func (s credentialService) List() (CredentialIterator, error) { +// List returns an iterator that lists all credentials of the currently authenticated account. +func (s credentialService) List(_ *CredentialListParams) (CredentialIterator, error) { creds, err := s.client.httpClient.ListMyCredentials() if err != nil { return nil, err From cc6d83ddeed4676572a5244ddfba4975fdd4b97d Mon Sep 17 00:00:00 2001 From: Joris Coenen Date: Thu, 28 Nov 2019 14:52:24 +0100 Subject: [PATCH 25/35] Fix missing cast to String() --- pkg/secrethub/internals/http/client.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/secrethub/internals/http/client.go b/pkg/secrethub/internals/http/client.go index 369b97c6..74df03ed 100644 --- a/pkg/secrethub/internals/http/client.go +++ b/pkg/secrethub/internals/http/client.go @@ -171,7 +171,7 @@ func (c *Client) GetMyUser() (*api.User, error) { // CreateCredential creates a new credential for the account. func (c *Client) CreateCredential(in *api.CreateCredentialRequest) (*api.Credential, error) { out := &api.Credential{} - rawURL := fmt.Sprintf(pathCredentials, c.base) + rawURL := fmt.Sprintf(pathCredentials, c.base.String()) err := c.post(rawURL, true, http.StatusCreated, in, out) return out, errio.Error(err) } @@ -179,7 +179,7 @@ func (c *Client) CreateCredential(in *api.CreateCredentialRequest) (*api.Credent // ListMyCredentials list all the currently authenticated account's credentials. func (c *Client) ListMyCredentials() ([]*api.Credential, error) { var out []*api.Credential - rawURL := fmt.Sprintf(pathCredentials, c.base) + rawURL := fmt.Sprintf(pathCredentials, c.base.String()) err := c.get(rawURL, true, &out) return out, errio.Error(err) } From 190f30af7a37fc6a50053f268e8ea8c3bcf4d282 Mon Sep 17 00:00:00 2001 From: Joris Coenen Date: Thu, 28 Nov 2019 15:03:46 +0100 Subject: [PATCH 26/35] Do not return error with List() as is conventional for iterators --- pkg/secrethub/credentials.go | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/pkg/secrethub/credentials.go b/pkg/secrethub/credentials.go index 37b18243..12a46cdb 100644 --- a/pkg/secrethub/credentials.go +++ b/pkg/secrethub/credentials.go @@ -12,7 +12,7 @@ type CredentialService interface { // Create a new credential from the credentials.Creator for an existing account. Create(credentials.Creator) error // List lists all credentials of the currently authenticated account. - List(_ *CredentialListParams) (CredentialIterator, error) + List(_ *CredentialListParams) CredentialIterator // Disable an existing credential. Disable(fingerprint string) error } @@ -86,9 +86,14 @@ type CredentialIterator interface { type credentialIterator struct { credentials []*api.Credential currentIndex int + err error } func (c *credentialIterator) Next() (api.Credential, error) { + if c.err != nil { + return api.Credential{}, c.err + } + currentIndex := c.currentIndex if currentIndex >= len(c.credentials) { return api.Credential{}, iterator.Done @@ -98,16 +103,14 @@ func (c *credentialIterator) Next() (api.Credential, error) { } // List returns an iterator that lists all credentials of the currently authenticated account. -func (s credentialService) List(_ *CredentialListParams) (CredentialIterator, error) { +func (s credentialService) List(_ *CredentialListParams) CredentialIterator { creds, err := s.client.httpClient.ListMyCredentials() - if err != nil { - return nil, err - } return &credentialIterator{ credentials: creds, - }, nil + err: err, + } } - + // Disable an existing credential. func (s credentialService) Disable(fingerprint string) error { err := api.ValidateShortCredentialFingerprint(fingerprint) From 58284dd579bac5b789e9290217194433a68d8cdf Mon Sep 17 00:00:00 2001 From: Joris Coenen Date: Thu, 28 Nov 2019 15:07:04 +0100 Subject: [PATCH 27/35] gofmt --- pkg/secrethub/internals/http/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/secrethub/internals/http/client.go b/pkg/secrethub/internals/http/client.go index ece8ed81..1cb62458 100644 --- a/pkg/secrethub/internals/http/client.go +++ b/pkg/secrethub/internals/http/client.go @@ -184,7 +184,7 @@ func (c *Client) ListMyCredentials() ([]*api.Credential, error) { err := c.get(rawURL, true, &out) return out, errio.Error(err) } - + // UpdateCredential updates an existing credential. func (c *Client) UpdateCredential(fingerprint string, in *api.UpdateCredentialRequest) error { rawURL := fmt.Sprintf(pathCredential, c.base, fingerprint) From 7a6a1e3b744eb1d38dc970ea7cff041feadefad1 Mon Sep 17 00:00:00 2001 From: Joris Coenen Date: Thu, 28 Nov 2019 15:39:47 +0100 Subject: [PATCH 28/35] Add missing call to String() --- pkg/secrethub/internals/http/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/secrethub/internals/http/client.go b/pkg/secrethub/internals/http/client.go index 1cb62458..35d76b04 100644 --- a/pkg/secrethub/internals/http/client.go +++ b/pkg/secrethub/internals/http/client.go @@ -187,7 +187,7 @@ func (c *Client) ListMyCredentials() ([]*api.Credential, error) { // UpdateCredential updates an existing credential. func (c *Client) UpdateCredential(fingerprint string, in *api.UpdateCredentialRequest) error { - rawURL := fmt.Sprintf(pathCredential, c.base, fingerprint) + rawURL := fmt.Sprintf(pathCredential, c.base.String(), fingerprint) err := c.patch(rawURL, true, http.StatusNoContent, in, nil) return err } From 4077fc7f62a60a8c581d6834e6deeba6d9ead2ce Mon Sep 17 00:00:00 2001 From: Joris Coenen Date: Thu, 28 Nov 2019 16:54:30 +0100 Subject: [PATCH 29/35] Fix error message text --- internals/api/credential.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internals/api/credential.go b/internals/api/credential.go index dbd07554..5b79671b 100644 --- a/internals/api/credential.go +++ b/internals/api/credential.go @@ -21,7 +21,7 @@ var ( ErrCredentialFingerprintNotUnique = errAPI.Code("fingerprint_not_unique").StatusErrorf("there are multiple credentials that start with the given fingerprint. Please use the full fingerprint", http.StatusConflict) ErrInvalidVerifier = errAPI.Code("invalid_verifier").StatusError("verifier is invalid", http.StatusBadRequest) ErrInvalidCredentialType = errAPI.Code("invalid_credential_type").StatusError("credential type is invalid", http.StatusBadRequest) - ErrInvalidCredentialDescription = errAPI.Code("invalid_credential_description").StatusError("credential description must be between 1 and 20 characters long", http.StatusBadRequest) + ErrInvalidCredentialDescription = errAPI.Code("invalid_credential_description").StatusError("credential description can be at most 32 characters long", http.StatusBadRequest) ErrInvalidAWSEndpoint = errAPI.Code("invalid_aws_endpoint").StatusError("invalid AWS endpoint provided", http.StatusBadRequest) ErrInvalidProof = errAPI.Code("invalid_proof").StatusError("invalid proof provided for credential", http.StatusUnauthorized) ErrAWSAccountMismatch = errAPI.Code("aws_account_mismatch").StatusError("the AWS Account ID in the role ARN does not match the AWS Account ID of the AWS credentials used for authentication. Make sure you are using AWS credentials that correspond to the role you are trying to add.", http.StatusUnauthorized) From 627cc13398909d01cf906a9655c1b3a91e0d7127 Mon Sep 17 00:00:00 2001 From: Joris Coenen Date: Thu, 28 Nov 2019 16:55:59 +0100 Subject: [PATCH 30/35] Fix typo --- pkg/secrethub/credentials.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/secrethub/credentials.go b/pkg/secrethub/credentials.go index 12a46cdb..41084281 100644 --- a/pkg/secrethub/credentials.go +++ b/pkg/secrethub/credentials.go @@ -75,7 +75,7 @@ func (s credentialService) Create(creator credentials.Creator) error { return nil } -// CredentialListParams are the parameters than configure credential listing. +// CredentialListParams are the parameters that configure credential listing. type CredentialListParams struct{} // CredentialIterator can be used to iterate over a list of credentials. From 8c2092a46d67db7bfb021cd13d470753b416bc62 Mon Sep 17 00:00:00 2001 From: Joris Coenen Date: Thu, 28 Nov 2019 18:06:23 +0100 Subject: [PATCH 31/35] Return created credential for Credentials.Create() --- pkg/secrethub/credentials.go | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/pkg/secrethub/credentials.go b/pkg/secrethub/credentials.go index 4a557189..311462d9 100644 --- a/pkg/secrethub/credentials.go +++ b/pkg/secrethub/credentials.go @@ -10,7 +10,7 @@ import ( // CredentialService handles operations on credentials on SecretHub. type CredentialService interface { // Create a new credential from the credentials.Creator for an existing account. - Create(credentials.Creator, string) error + Create(credentials.Creator, string) (*api.Credential, error) // List lists all credentials of the currently authenticated account. List(_ *CredentialListParams) CredentialIterator // Disable an existing credential. @@ -30,26 +30,26 @@ type credentialService struct { // Create a new credential from the credentials.Creator for an existing account. // This includes a re-encrypted copy the the account key. // Description is optional and can be left empty. -func (s credentialService) Create(creator credentials.Creator, description string) error { +func (s credentialService) Create(creator credentials.Creator, description string) (*api.Credential, error) { accountKey, err := s.client.getAccountKey() if err != nil { - return err + return nil, err } err = creator.Create() if err != nil { - return err + return nil, err } verifier := creator.Verifier() bytes, fingerprint, err := verifier.Export() if err != nil { - return err + return nil, err } accountKeyRequest, err := s.client.createAccountKeyRequest(creator.Encrypter(), *accountKey) if err != nil { - return err + return nil, err } var reqDescription *string @@ -67,19 +67,15 @@ func (s credentialService) Create(creator credentials.Creator, description strin } err = verifier.AddProof(&req) if err != nil { - return err + return nil, err } err = req.Validate() if err != nil { - return err + return nil, err } - _, err = s.client.httpClient.CreateCredential(&req) - if err != nil { - return err - } - return nil + return s.client.httpClient.CreateCredential(&req) } // CredentialListParams are the parameters that configure credential listing. From 81d26a8cad331a6a32b82e61ab79cd9399576631 Mon Sep 17 00:00:00 2001 From: Joris Coenen Date: Mon, 2 Dec 2019 10:48:40 +0100 Subject: [PATCH 32/35] Display backup code as groups of 8 uppercase characters --- pkg/secrethub/credentials/bootstrap_code.go | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/pkg/secrethub/credentials/bootstrap_code.go b/pkg/secrethub/credentials/bootstrap_code.go index 3742847b..621e225d 100644 --- a/pkg/secrethub/credentials/bootstrap_code.go +++ b/pkg/secrethub/credentials/bootstrap_code.go @@ -4,6 +4,7 @@ import ( "encoding/base64" "encoding/hex" "errors" + "strings" "github.com/secrethub/secrethub-go/internals/api" "github.com/secrethub/secrethub-go/internals/auth" @@ -42,7 +43,9 @@ func (b *BackupCodeCreator) Code() (string, error) { if b.bootstrapCode == nil { return "", errors.New("backup code has not yet been generated") } - return hex.EncodeToString(b.bootstrapCode.encryptionKey.Export()), nil + code := strings.ToUpper(hex.EncodeToString(b.bootstrapCode.encryptionKey.Export())) + delimitedCode := strings.Join(splitStringByWidth(code, 8), "-") + return delimitedCode, nil } // Verifier returns a Verifier that can be used for creating a new credential from this backup code. @@ -154,3 +157,17 @@ func (b *bootstrapCode) Unwrap(ciphertext *api.EncryptedData) ([]byte, error) { } return decrypted, nil } + +func splitStringByWidth(in string, width int) []string { + var out []string + tmp := "" + for i, r := range in { + tmp += string(r) + + if (i+1)%width == 0 { + out = append(out, tmp) + tmp = "" + } + } + return out +} From cd2c5d48976cc02aec67e411a0aaed16b68aef94 Mon Sep 17 00:00:00 2001 From: Joris Coenen Date: Mon, 2 Dec 2019 12:18:07 +0100 Subject: [PATCH 33/35] Add parsing of bootstrap code --- pkg/secrethub/credentials/bootstrap_code.go | 30 ++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/pkg/secrethub/credentials/bootstrap_code.go b/pkg/secrethub/credentials/bootstrap_code.go index 621e225d..96d244d7 100644 --- a/pkg/secrethub/credentials/bootstrap_code.go +++ b/pkg/secrethub/credentials/bootstrap_code.go @@ -4,6 +4,8 @@ import ( "encoding/base64" "encoding/hex" "errors" + "fmt" + "regexp" "strings" "github.com/secrethub/secrethub-go/internals/api" @@ -12,6 +14,10 @@ import ( "github.com/secrethub/secrethub-go/pkg/secrethub/internals/http" ) +var ( + bootstrapCodeRegexp = regexp.MustCompile("[^a-zA-Z0-9]+") +) + // Enforce implementation of interfaces by structs. var _ Creator = (*BackupCodeCreator)(nil) var _ Provider = (*bootstrapCodeProvider)(nil) @@ -27,6 +33,20 @@ func CreateBackupCode() *BackupCodeCreator { return &BackupCodeCreator{} } +// ParseBootstrapCode parses a string and checks whether it is a valid bootstrap code. +// If it is valid, the bytes of the code are returned. +func ParseBootstrapCode(code string) ([]byte, error) { + code = filterBootstrapCode(code) + decoded, err := hex.DecodeString(code) + if err != nil { + return nil, errors.New("illegal characters in code") + } + if len(decoded) != crypto.SymmetricKeyLength { + return nil, errors.New("wrong length") + } + return decoded, nil +} + // Create generates a new code and stores it in the BackupCodeCreator. func (b *BackupCodeCreator) Create() error { key, err := crypto.GenerateSymmetricKey() @@ -79,9 +99,9 @@ func UseBackupCode(code string) Provider { // Provide returns the auth.Authenticator and Decrypter corresponding to a bootstrap code. func (b *bootstrapCodeProvider) Provide(_ *http.Client) (auth.Authenticator, Decrypter, error) { - bytes, err := hex.DecodeString(b.code) - if err != nil || len(bytes) != 32 { - return nil, nil, errors.New("malformed code") + bytes, err := ParseBootstrapCode(b.code) + if err != nil { + return nil, nil, fmt.Errorf("malformed code: %w", err) } bootstrapCode := newBootstrapCode(bytes, b.t) return auth.NewHTTPSigner(bootstrapCode), bootstrapCode, nil @@ -158,6 +178,10 @@ func (b *bootstrapCode) Unwrap(ciphertext *api.EncryptedData) ([]byte, error) { return decrypted, nil } +func filterBootstrapCode(code string) string { + return bootstrapCodeRegexp.ReplaceAllString(code, "") +} + func splitStringByWidth(in string, width int) []string { var out []string tmp := "" From 8e4536fffb7376eecd652b2154612b126929aa3f Mon Sep 17 00:00:00 2001 From: Joris Coenen Date: Mon, 2 Dec 2019 12:28:15 +0100 Subject: [PATCH 34/35] Rewrite Bootstrap Code validation to only validate This is only exposes the validation, not the parsed code itself. --- pkg/secrethub/credentials/bootstrap_code.go | 23 ++++++++++----------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/pkg/secrethub/credentials/bootstrap_code.go b/pkg/secrethub/credentials/bootstrap_code.go index 96d244d7..243a9365 100644 --- a/pkg/secrethub/credentials/bootstrap_code.go +++ b/pkg/secrethub/credentials/bootstrap_code.go @@ -33,18 +33,13 @@ func CreateBackupCode() *BackupCodeCreator { return &BackupCodeCreator{} } -// ParseBootstrapCode parses a string and checks whether it is a valid bootstrap code. -// If it is valid, the bytes of the code are returned. -func ParseBootstrapCode(code string) ([]byte, error) { - code = filterBootstrapCode(code) - decoded, err := hex.DecodeString(code) - if err != nil { - return nil, errors.New("illegal characters in code") - } - if len(decoded) != crypto.SymmetricKeyLength { - return nil, errors.New("wrong length") +// ValidateBootstrapCode validates a string and checks whether it is a valid bootstrap code. +func ValidateBootstrapCode(code string) error { + filtered := filterBootstrapCode(code) + if len(filtered) != crypto.SymmetricKeyLength*2 { + return errors.New("code does not consist of 64 hexadecimal characters") } - return decoded, nil + return nil } // Create generates a new code and stores it in the BackupCodeCreator. @@ -99,7 +94,11 @@ func UseBackupCode(code string) Provider { // Provide returns the auth.Authenticator and Decrypter corresponding to a bootstrap code. func (b *bootstrapCodeProvider) Provide(_ *http.Client) (auth.Authenticator, Decrypter, error) { - bytes, err := ParseBootstrapCode(b.code) + err := ValidateBootstrapCode(b.code) + if err != nil { + return nil, nil, fmt.Errorf("malformed code: %w", err) + } + bytes, err := hex.DecodeString(filterBootstrapCode(b.code)) if err != nil { return nil, nil, fmt.Errorf("malformed code: %w", err) } From e48c0931d512657709a7f8e87fee19e3a11bf9c7 Mon Sep 17 00:00:00 2001 From: Joris Coenen Date: Mon, 2 Dec 2019 13:11:29 +0100 Subject: [PATCH 35/35] Use %v instead of %w for error wrapping We're not at go 1.13 yet. We can update later. --- pkg/secrethub/credentials/bootstrap_code.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/secrethub/credentials/bootstrap_code.go b/pkg/secrethub/credentials/bootstrap_code.go index 243a9365..be321fc9 100644 --- a/pkg/secrethub/credentials/bootstrap_code.go +++ b/pkg/secrethub/credentials/bootstrap_code.go @@ -96,11 +96,11 @@ func UseBackupCode(code string) Provider { func (b *bootstrapCodeProvider) Provide(_ *http.Client) (auth.Authenticator, Decrypter, error) { err := ValidateBootstrapCode(b.code) if err != nil { - return nil, nil, fmt.Errorf("malformed code: %w", err) + return nil, nil, fmt.Errorf("malformed code: %v", err) } bytes, err := hex.DecodeString(filterBootstrapCode(b.code)) if err != nil { - return nil, nil, fmt.Errorf("malformed code: %w", err) + return nil, nil, fmt.Errorf("malformed code: %v", err) } bootstrapCode := newBootstrapCode(bytes, b.t) return auth.NewHTTPSigner(bootstrapCode), bootstrapCode, nil