Skip to content
This repository was archived by the owner on Feb 16, 2023. It is now read-only.

Commit ffe2fce

Browse files
Merge pull request #157 from secrethub/release/v0.25.0
Release v0.25.0
2 parents a2d07b0 + e48c093 commit ffe2fce

File tree

16 files changed

+696
-65
lines changed

16 files changed

+696
-65
lines changed

internals/api/credential.go

Lines changed: 82 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,28 @@ import (
1111
"time"
1212

1313
"github.com/secrethub/secrethub-go/internals/api/uuid"
14+
"github.com/secrethub/secrethub-go/internals/crypto"
1415
)
1516

1617
// Errors
1718
var (
18-
ErrInvalidFingerprint = errAPI.Code("invalid_fingerprint").StatusError("fingerprint is invalid", http.StatusBadRequest)
19-
ErrInvalidVerifier = errAPI.Code("invalid_verifier").StatusError("verifier is invalid", http.StatusBadRequest)
20-
ErrInvalidCredentialType = errAPI.Code("invalid_credential_type").StatusError("credential type is invalid", http.StatusBadRequest)
21-
ErrInvalidAWSEndpoint = errAPI.Code("invalid_aws_endpoint").StatusError("invalid AWS endpoint provided", http.StatusBadRequest)
22-
ErrInvalidProof = errAPI.Code("invalid_proof").StatusError("invalid proof provided for credential", http.StatusUnauthorized)
23-
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)
24-
ErrAWSAuthFailed = errAPI.Code("aws_auth_failed").StatusError("authentication not accepted by AWS", http.StatusUnauthorized)
25-
ErrAWSKMSKeyNotFound = errAPI.Code("aws_kms_key_not_found").StatusError("could not found the KMS key", http.StatusNotFound)
26-
ErrInvalidRoleARN = errAPI.Code("invalid_role_arn").StatusError("provided role is not a valid ARN", http.StatusBadRequest)
27-
ErrMissingMetadata = errAPI.Code("missing_metadata").StatusErrorPref("expecting %s metadata provided for credentials of type %s", http.StatusBadRequest)
28-
ErrInvalidMetadataKey = errAPI.Code("invalid_metadata_key").StatusErrorPref("invalid metadata key %s for credential type %s", http.StatusBadRequest)
29-
ErrUnknownMetadataKey = errAPI.Code("unknown_metadata_key").StatusErrorPref("unknown metadata key: %s", http.StatusBadRequest)
30-
ErrRoleDoesNotMatch = errAPI.Code("role_does_not_match").StatusError("role in metadata does not match the verifier", http.StatusBadRequest)
19+
ErrInvalidFingerprint = errAPI.Code("invalid_fingerprint").StatusError("fingerprint is invalid", http.StatusBadRequest)
20+
ErrTooShortFingerprint = errAPI.Code("too_short_fingerprint").StatusErrorf("at least %d characters of the fingerprint must be entered", http.StatusBadRequest, ShortCredentialFingerprintMinimumLength)
21+
ErrCredentialFingerprintNotUnique = errAPI.Code("fingerprint_not_unique").StatusErrorf("there are multiple credentials that start with the given fingerprint. Please use the full fingerprint", http.StatusConflict)
22+
ErrInvalidVerifier = errAPI.Code("invalid_verifier").StatusError("verifier is invalid", http.StatusBadRequest)
23+
ErrInvalidCredentialType = errAPI.Code("invalid_credential_type").StatusError("credential type is invalid", http.StatusBadRequest)
24+
ErrInvalidCredentialDescription = errAPI.Code("invalid_credential_description").StatusError("credential description can be at most 32 characters long", http.StatusBadRequest)
25+
ErrInvalidAWSEndpoint = errAPI.Code("invalid_aws_endpoint").StatusError("invalid AWS endpoint provided", http.StatusBadRequest)
26+
ErrInvalidProof = errAPI.Code("invalid_proof").StatusError("invalid proof provided for credential", http.StatusUnauthorized)
27+
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)
28+
ErrAWSAuthFailed = errAPI.Code("aws_auth_failed").StatusError("authentication not accepted by AWS", http.StatusUnauthorized)
29+
ErrAWSKMSKeyNotFound = errAPI.Code("aws_kms_key_not_found").StatusError("could not found the KMS key", http.StatusNotFound)
30+
ErrInvalidRoleARN = errAPI.Code("invalid_role_arn").StatusError("provided role is not a valid ARN", http.StatusBadRequest)
31+
ErrMissingMetadata = errAPI.Code("missing_metadata").StatusErrorPref("expecting %s metadata provided for credentials of type %s", http.StatusBadRequest)
32+
ErrInvalidMetadataKey = errAPI.Code("invalid_metadata_key").StatusErrorPref("invalid metadata key %s for credential type %s", http.StatusBadRequest)
33+
ErrUnknownMetadataKey = errAPI.Code("unknown_metadata_key").StatusErrorPref("unknown metadata key: %s", http.StatusBadRequest)
34+
ErrRoleDoesNotMatch = errAPI.Code("role_does_not_match").StatusError("role in metadata does not match the verifier", http.StatusBadRequest)
35+
ErrCannotDisableCurrentCredential = errAPI.Code("cannot_disable_current_credential").StatusError("cannot disable the credential that is currently used on this device", http.StatusConflict)
3136
)
3237

3338
// Credential metadata keys
@@ -36,24 +41,30 @@ const (
3641
CredentialMetadataAWSRole = "aws_role"
3742
)
3843

44+
const (
45+
ShortCredentialFingerprintMinimumLength = 10
46+
)
47+
3948
// Credential is used to authenticate to the API and to encrypt the account key.
4049
type Credential struct {
4150
AccountID uuid.UUID `json:"account_id"`
4251
Type CredentialType `json:"type"`
4352
CreatedAt time.Time `json:"created_at"`
4453
Fingerprint string `json:"fingerprint"`
45-
Name string `json:"name"`
54+
Description string `json:"description"`
4655
Verifier []byte `json:"verifier"`
4756
Metadata map[string]string `json:"metadata,omitempty"`
57+
Enabled bool `json:"enabled"`
4858
}
4959

5060
// CredentialType is used to identify the type of algorithm that is used for a credential.
5161
type CredentialType string
5262

5363
// Credential types
5464
const (
55-
CredentialTypeKey CredentialType = "key"
56-
CredentialTypeAWS CredentialType = "aws"
65+
CredentialTypeKey CredentialType = "key"
66+
CredentialTypeAWS CredentialType = "aws"
67+
CredentialTypeBackupCode CredentialType = "backup-code"
5768
)
5869

5970
const (
@@ -63,20 +74,26 @@ const (
6374

6475
// Validate validates whether the algorithm type is valid.
6576
func (a CredentialType) Validate() error {
66-
if a == CredentialTypeKey || a == CredentialTypeAWS {
67-
return nil
77+
var credentialTypeList = map[CredentialType]struct{}{
78+
CredentialTypeKey: {},
79+
CredentialTypeAWS: {},
80+
CredentialTypeBackupCode: {},
81+
}
82+
if _, ok := credentialTypeList[a]; !ok {
83+
return ErrInvalidCredentialType
6884
}
69-
return ErrInvalidCredentialType
85+
return nil
7086
}
7187

7288
// CreateCredentialRequest contains the fields to add a credential to an account.
7389
type CreateCredentialRequest struct {
74-
Type CredentialType `json:"type"`
75-
Fingerprint string `json:"fingerprint"`
76-
Name string `json:"name,omitempty"`
77-
Verifier []byte `json:"verifier"`
78-
Proof interface{} `json:"proof"`
79-
Metadata map[string]string `json:"metadata"`
90+
Type CredentialType `json:"type"`
91+
Fingerprint string `json:"fingerprint"`
92+
Description *string `json:"name,omitempty"`
93+
Verifier []byte `json:"verifier"`
94+
Proof interface{} `json:"proof"`
95+
Metadata map[string]string `json:"metadata"`
96+
AccountKey *CreateAccountKeyRequest `json:"account_key,omitempty"`
8097
}
8198

8299
// UnmarshalJSON converts a JSON representation into a CreateCredentialRequest with the correct Proof.
@@ -102,6 +119,8 @@ func (req *CreateCredentialRequest) UnmarshalJSON(b []byte) error {
102119
dec.Proof = &CredentialProofAWS{}
103120
case CredentialTypeKey:
104121
dec.Proof = &CredentialProofKey{}
122+
case CredentialTypeBackupCode:
123+
dec.Proof = &CredentialProofBackupCode{}
105124
default:
106125
return ErrInvalidCredentialType
107126
}
@@ -129,19 +148,38 @@ func (req *CreateCredentialRequest) Validate() error {
129148
return ErrMissingField("type")
130149
}
131150

151+
if req.Description != nil {
152+
if err := ValidateCredentialDescription(*req.Description); err != nil {
153+
return err
154+
}
155+
}
156+
132157
err := req.Type.Validate()
133158
if err != nil {
134159
return err
135160
}
136161

162+
if req.AccountKey != nil {
163+
if err := req.AccountKey.Validate(); err != nil {
164+
return err
165+
}
166+
}
167+
168+
if req.Type == CredentialTypeBackupCode {
169+
decoded, err := base64.StdEncoding.DecodeString(string(req.Verifier))
170+
if err != nil {
171+
return ErrInvalidVerifier
172+
}
173+
if len(decoded) != sha256.Size {
174+
return ErrInvalidVerifier
175+
}
176+
}
177+
137178
if req.Type == CredentialTypeAWS && req.Proof == nil {
138179
return ErrMissingField("proof")
139180
}
140181

141-
fingerprint, err := GetFingerprint(req.Type, req.Verifier)
142-
if err != nil {
143-
return err
144-
}
182+
fingerprint := GetFingerprint(req.Type, req.Verifier)
145183
if req.Fingerprint != fingerprint {
146184
return ErrInvalidFingerprint
147185
}
@@ -192,8 +230,21 @@ func (p CredentialProofAWS) Validate() error {
192230
// CredentialProofKey is proof for when the credential type is RSA.
193231
type CredentialProofKey struct{}
194232

233+
// CredentialProofBackupCode is proof for when the credential type is backup key.
234+
type CredentialProofBackupCode struct{}
235+
236+
// UpdateCredentialRequest contains the fields of a credential that can be updated.
237+
type UpdateCredentialRequest struct {
238+
Enabled *bool `json:"enabled,omitempty"`
239+
}
240+
241+
// Validate whether the UpdateCredentialRequest is a valid request.
242+
func (req *UpdateCredentialRequest) Validate() error {
243+
return nil
244+
}
245+
195246
// GetFingerprint returns the fingerprint of a credential.
196-
func GetFingerprint(t CredentialType, verifier []byte) (string, error) {
247+
func GetFingerprint(t CredentialType, verifier []byte) string {
197248
var toHash []byte
198249
if t == CredentialTypeKey {
199250
// Provide compatibility with traditional RSA credentials.
@@ -203,10 +254,5 @@ func GetFingerprint(t CredentialType, verifier []byte) (string, error) {
203254
toHash = []byte(fmt.Sprintf("credential_type=%s;verifier=%s", t, encodedVerifier))
204255

205256
}
206-
h := sha256.New()
207-
_, err := h.Write(toHash)
208-
if err != nil {
209-
return "", err
210-
}
211-
return hex.EncodeToString(h.Sum(nil)), nil
257+
return hex.EncodeToString(crypto.SHA256(toHash))
212258
}

internals/api/credential_test.go

Lines changed: 87 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,53 @@ import (
77
)
88

99
func TestCreateCredentialRequest_Validate(t *testing.T) {
10+
description := "Personal laptop credential"
11+
1012
cases := map[string]struct {
1113
req CreateCredentialRequest
1214
err error
1315
}{
1416
"success": {
1517
req: CreateCredentialRequest{
16-
Name: "Personal laptop credential",
18+
Description: &description,
19+
Type: CredentialTypeKey,
20+
Fingerprint: "88c9eae68eb300b2971a2bec9e5a26ff4179fd661d6b7d861e4c6557b9aaee14",
21+
Verifier: []byte("verifier"),
22+
},
23+
err: nil,
24+
},
25+
"success without description": {
26+
req: CreateCredentialRequest{
1727
Type: CredentialTypeKey,
1828
Fingerprint: "88c9eae68eb300b2971a2bec9e5a26ff4179fd661d6b7d861e4c6557b9aaee14",
1929
Verifier: []byte("verifier"),
2030
},
2131
err: nil,
2232
},
23-
"success without name": {
33+
"success including account key": {
34+
req: CreateCredentialRequest{
35+
Description: &description,
36+
Type: CredentialTypeKey,
37+
Fingerprint: "88c9eae68eb300b2971a2bec9e5a26ff4179fd661d6b7d861e4c6557b9aaee14",
38+
Verifier: []byte("verifier"),
39+
AccountKey: &CreateAccountKeyRequest{
40+
EncryptedPrivateKey: NewEncryptedDataAESGCM([]byte("encrypted"), []byte("nonce"), 96, NewEncryptionKeyLocal(256)),
41+
PublicKey: []byte("public-key"),
42+
},
43+
},
44+
err: nil,
45+
},
46+
"including invalid account key": {
47+
req: CreateCredentialRequest{
48+
Description: &description,
49+
Type: CredentialTypeKey,
50+
Fingerprint: "88c9eae68eb300b2971a2bec9e5a26ff4179fd661d6b7d861e4c6557b9aaee14",
51+
Verifier: []byte("verifier"),
52+
AccountKey: &CreateAccountKeyRequest{},
53+
},
54+
err: ErrInvalidPublicKey,
55+
},
56+
"success without Description": {
2457
req: CreateCredentialRequest{
2558
Type: CredentialTypeKey,
2659
Fingerprint: "88c9eae68eb300b2971a2bec9e5a26ff4179fd661d6b7d861e4c6557b9aaee14",
@@ -30,16 +63,16 @@ func TestCreateCredentialRequest_Validate(t *testing.T) {
3063
},
3164
"no fingerprint": {
3265
req: CreateCredentialRequest{
33-
Type: CredentialTypeKey,
34-
Name: "Personal laptop credential",
35-
Verifier: []byte("verifier"),
66+
Type: CredentialTypeKey,
67+
Description: &description,
68+
Verifier: []byte("verifier"),
3669
},
3770
err: ErrMissingField("fingerprint"),
3871
},
3972
"invalid fingerprint": {
4073
req: CreateCredentialRequest{
4174
Type: CredentialTypeKey,
42-
Name: "Personal laptop credential",
75+
Description: &description,
4376
Fingerprint: "not-valid",
4477
Verifier: []byte("verifier"),
4578
},
@@ -48,23 +81,23 @@ func TestCreateCredentialRequest_Validate(t *testing.T) {
4881
"empty verifier": {
4982
req: CreateCredentialRequest{
5083
Type: CredentialTypeKey,
51-
Name: "Personal laptop credential",
84+
Description: &description,
5285
Fingerprint: "fingerprint",
5386
Verifier: nil,
5487
},
5588
err: ErrMissingField("verifier"),
5689
},
5790
"empty type": {
5891
req: CreateCredentialRequest{
59-
Name: "Personal laptop credential",
92+
Description: &description,
6093
Fingerprint: "88c9eae68eb300b2971a2bec9e5a26ff4179fd661d6b7d861e4c6557b9aaee14",
6194
Verifier: []byte("verifier"),
6295
},
6396
err: ErrMissingField("type"),
6497
},
6598
"invalid type": {
6699
req: CreateCredentialRequest{
67-
Name: "Personal laptop credential",
100+
Description: &description,
68101
Fingerprint: "88c9eae68eb300b2971a2bec9e5a26ff4179fd661d6b7d861e4c6557b9aaee14",
69102
Verifier: []byte("verifier"),
70103
Type: CredentialType("invalid"),
@@ -110,7 +143,7 @@ func TestCreateCredentialRequest_Validate(t *testing.T) {
110143
},
111144
"extra metadata": {
112145
req: CreateCredentialRequest{
113-
Name: "Personal laptop credential",
146+
Description: &description,
114147
Type: CredentialTypeKey,
115148
Fingerprint: "88c9eae68eb300b2971a2bec9e5a26ff4179fd661d6b7d861e4c6557b9aaee14",
116149
Verifier: []byte("verifier"),
@@ -134,10 +167,52 @@ func TestCreateCredentialRequest_Validate(t *testing.T) {
134167
},
135168
err: ErrUnknownMetadataKey("foo"),
136169
},
170+
"backup code success": {
171+
req: CreateCredentialRequest{
172+
Type: CredentialTypeBackupCode,
173+
Fingerprint: "69cf01c1e969b4430ca1b08ede7dab5f91a64a306e321f0348667446e1b3597e",
174+
Verifier: []byte("DdAaVTKxoYgxzWY2UWrdl1xHOOv4ZUozra4Vm8WGxmU="),
175+
Proof: &CredentialProofBackupCode{},
176+
Metadata: map[string]string{},
177+
},
178+
err: nil,
179+
},
180+
"backup code too short verifier": {
181+
req: CreateCredentialRequest{
182+
Type: CredentialTypeBackupCode,
183+
Fingerprint: "69cf01c1e969b4430ca1b08ede7dab5f91a64a306e321f0348667446e1b3597e",
184+
Verifier: []byte("DdAaVTKxoYgxzWY2UWrdl1OOv4ZUozra4Vm8WGxmU="),
185+
Proof: &CredentialProofBackupCode{},
186+
Metadata: map[string]string{},
187+
},
188+
err: ErrInvalidVerifier,
189+
},
190+
"backup code non base64 verifier": {
191+
req: CreateCredentialRequest{
192+
Type: CredentialTypeBackupCode,
193+
Fingerprint: "69cf01c1e969b4430ca1b08ede7dab5f91a64a306e321f0348667446e1b3597e",
194+
Verifier: []byte("DdAaVTKxoYgxzWY2UWrdl1OOv4ZUozra4Vm8WGxm&="),
195+
Proof: &CredentialProofBackupCode{},
196+
Metadata: map[string]string{},
197+
},
198+
err: ErrInvalidVerifier,
199+
},
200+
"backup code with metadata": {
201+
req: CreateCredentialRequest{
202+
Type: CredentialTypeBackupCode,
203+
Fingerprint: "69cf01c1e969b4430ca1b08ede7dab5f91a64a306e321f0348667446e1b3597e",
204+
Verifier: []byte("DdAaVTKxoYgxzWY2UWrdl1xHOOv4ZUozra4Vm8WGxmU="),
205+
Proof: &CredentialProofBackupCode{},
206+
Metadata: map[string]string{
207+
CredentialMetadataAWSKMSKey: "test",
208+
},
209+
},
210+
err: ErrInvalidMetadataKey(CredentialMetadataAWSKMSKey, CredentialTypeBackupCode),
211+
},
137212
}
138213

139-
for name, tc := range cases {
140-
t.Run(name, func(t *testing.T) {
214+
for Description, tc := range cases {
215+
t.Run(Description, func(t *testing.T) {
141216
// Do
142217
err := tc.req.Validate()
143218

0 commit comments

Comments
 (0)