Add helper for encoding/decoding root tokens and OTP generation in SDK module (#10504) (#10505)

This commit is contained in:
Pavlos Tzianos 2021-12-01 13:05:49 +00:00 committed by GitHub
parent b4d9ee60d8
commit 0abc8f43fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 239 additions and 90 deletions

View File

@ -9,7 +9,7 @@ require (
github.com/frankban/quicktest v1.13.0 // indirect
github.com/go-test/deep v1.0.2
github.com/hashicorp/errwrap v1.1.0
github.com/hashicorp/go-cleanhttp v0.5.1
github.com/hashicorp/go-cleanhttp v0.5.2
github.com/hashicorp/go-hclog v0.16.2
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/go-retryablehttp v0.6.6

View File

@ -89,8 +89,9 @@ github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brv
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-hclog v0.16.2 h1:K4ev2ib4LdQETX5cSZBG0DVLk1jwGqSPXBjdah3veNs=
@ -138,6 +139,7 @@ github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyX
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/keybase/go-crypto v0.0.0-20190403132359-d65b6b94177f/go.mod h1:ghbZscTyKdM07+Fw3KSi0hcJm+AlEUWj8QLlPtijN/M=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=

3
changelog/10505.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
sdk: Add helper for decoding root tokens
```

View File

@ -2,19 +2,15 @@ package command
import (
"bytes"
"crypto/rand"
"encoding/base64"
"fmt"
"io"
"os"
"strings"
"github.com/hashicorp/go-secure-stdlib/base62"
"github.com/hashicorp/go-secure-stdlib/password"
uuid "github.com/hashicorp/go-uuid"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/helper/pgpkeys"
"github.com/hashicorp/vault/helper/xor"
"github.com/hashicorp/vault/sdk/helper/roottoken"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
@ -290,32 +286,15 @@ func (c *OperatorGenerateRootCommand) generateOTP(client *api.Client, kind gener
return "", 2
}
switch status.OTPLength {
case 0:
// This is the fallback case
buf := make([]byte, 16)
readLen, err := rand.Read(buf)
if err != nil {
c.UI.Error(fmt.Sprintf("Error reading random bytes: %s", err))
return "", 2
}
if readLen != 16 {
c.UI.Error(fmt.Sprintf("Read %d bytes when we should have read 16", readLen))
return "", 2
}
return base64.StdEncoding.EncodeToString(buf), 0
default:
otp, err := base62.Random(status.OTPLength)
if err != nil {
c.UI.Error(fmt.Errorf("Error reading random bytes: %w", err).Error())
return "", 2
}
return otp, 0
otp, err := roottoken.GenerateOTP(status.OTPLength)
var retCode int
if err != nil {
retCode = 2
c.UI.Error(err.Error())
} else {
retCode = 0
}
return otp, retCode
}
// decode decodes the given value using the otp.
@ -364,36 +343,10 @@ func (c *OperatorGenerateRootCommand) decode(client *api.Client, encoded, otp st
return 2
}
var token string
switch status.OTPLength {
case 0:
// Backwards compat
tokenBytes, err := xor.XORBase64(encoded, otp)
if err != nil {
c.UI.Error(fmt.Sprintf("Error xoring token: %s", err))
return 1
}
uuidToken, err := uuid.FormatUUID(tokenBytes)
if err != nil {
c.UI.Error(fmt.Sprintf("Error formatting base64 token value: %s", err))
return 1
}
token = strings.TrimSpace(uuidToken)
default:
tokenBytes, err := base64.RawStdEncoding.DecodeString(encoded)
if err != nil {
c.UI.Error(fmt.Errorf("Error decoding base64'd token: %w", err).Error())
return 1
}
tokenBytes, err = xor.XORBytes(tokenBytes, []byte(otp))
if err != nil {
c.UI.Error(fmt.Errorf("Error xoring token: %w", err).Error())
return 1
}
token = string(tokenBytes)
token, err := roottoken.DecodeToken(encoded, otp, status.OTPLength)
if err != nil {
c.UI.Error(fmt.Sprintf("Error decoding root token: %s", err))
return 1
}
switch Format(c.UI) {

View File

@ -11,7 +11,7 @@ import (
"strings"
"testing"
"github.com/hashicorp/vault/helper/xor"
"github.com/hashicorp/vault/sdk/helper/xor"
"github.com/hashicorp/vault/vault"
"github.com/mitchellh/cli"
)

View File

@ -17,8 +17,8 @@ import (
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/helper/metricsutil"
"github.com/hashicorp/vault/helper/namespace"
"github.com/hashicorp/vault/helper/xor"
"github.com/hashicorp/vault/physical/raft"
"github.com/hashicorp/vault/sdk/helper/xor"
"github.com/hashicorp/vault/vault"
"github.com/mitchellh/go-testing-interface"
)

View File

@ -14,7 +14,7 @@ import (
"github.com/go-test/deep"
"github.com/hashicorp/vault/helper/namespace"
"github.com/hashicorp/vault/helper/pgpkeys"
"github.com/hashicorp/vault/helper/xor"
"github.com/hashicorp/vault/sdk/helper/xor"
"github.com/hashicorp/vault/sdk/logical"
"github.com/hashicorp/vault/vault"
)

View File

@ -13,6 +13,7 @@ require (
github.com/golang/protobuf v1.5.2
github.com/golang/snappy v0.0.4
github.com/hashicorp/errwrap v1.1.0
github.com/hashicorp/go-cleanhttp v0.5.2
github.com/hashicorp/go-hclog v0.16.2
github.com/hashicorp/go-immutable-radix v1.3.1
github.com/hashicorp/go-kms-wrapping/entropy v0.1.0
@ -29,6 +30,7 @@ require (
github.com/hashicorp/go-version v1.2.0
github.com/hashicorp/golang-lru v0.5.4
github.com/hashicorp/hcl v1.0.0
github.com/keybase/go-crypto v0.0.0-20190403132359-d65b6b94177f
github.com/kr/text v0.2.0 // indirect
github.com/mattn/go-colorable v0.1.6 // indirect
github.com/mitchellh/copystructure v1.0.0

View File

@ -89,6 +89,8 @@ github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brv
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-hclog v0.16.2 h1:K4ev2ib4LdQETX5cSZBG0DVLk1jwGqSPXBjdah3veNs=
github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
@ -135,6 +137,8 @@ github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyX
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/keybase/go-crypto v0.0.0-20190403132359-d65b6b94177f h1:Gsc9mVHLRqBjMgdQCghN9NObCcRncDqxJvBvEaIIQEo=
github.com/keybase/go-crypto v0.0.0-20190403132359-d65b6b94177f/go.mod h1:ghbZscTyKdM07+Fw3KSi0hcJm+AlEUWj8QLlPtijN/M=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=

View File

@ -0,0 +1,40 @@
package roottoken
import (
"encoding/base64"
"fmt"
"strings"
uuid "github.com/hashicorp/go-uuid"
"github.com/hashicorp/vault/sdk/helper/xor"
)
// DecodeToken will decode the root token returned by the Vault API
// The algorithm was initially used in the generate root command
func DecodeToken(encoded, otp string, otpLength int) (string, error) {
switch otpLength {
case 0:
// Backwards compat
tokenBytes, err := xor.XORBase64(encoded, otp)
if err != nil {
return "", fmt.Errorf("error xoring token: %s", err)
}
uuidToken, err := uuid.FormatUUID(tokenBytes)
if err != nil {
return "", fmt.Errorf("error formatting base64 token value: %s", err)
}
return strings.TrimSpace(uuidToken), nil
default:
tokenBytes, err := base64.RawStdEncoding.DecodeString(encoded)
if err != nil {
return "", fmt.Errorf("error decoding base64'd token: %v", err)
}
tokenBytes, err = xor.XORBytes(tokenBytes, []byte(otp))
if err != nil {
return "", fmt.Errorf("error xoring token: %v", err)
}
return string(tokenBytes), nil
}
}

View File

@ -0,0 +1,26 @@
package roottoken
import (
"encoding/base64"
"fmt"
"github.com/hashicorp/vault/sdk/helper/xor"
)
// EncodeToken gets a token and an OTP and encodes the token.
// The OTP must have the same length as the token.
func EncodeToken(token, otp string) (string, error) {
if len(token) == 0 {
return "", fmt.Errorf("no token provided")
} else if len(otp) == 0 {
return "", fmt.Errorf("no otp provided")
}
// This function performs decoding checks so rather than decode the OTP,
// just encode the value we're passing in.
tokenBytes, err := xor.XORBytes([]byte(otp), []byte(token))
if err != nil {
return "", fmt.Errorf("xor of root token failed: %w", err)
}
return base64.RawStdEncoding.EncodeToString(tokenBytes), nil
}

View File

@ -0,0 +1,72 @@
package roottoken
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestTokenEncodingDecodingWithOTP(t *testing.T) {
otpTestCases := []struct {
token string
name string
otpLength int
expectedEncodingErr string
expectedDecodingErr string
}{
{
token: "someToken",
name: "test token encoding with base64",
otpLength: 0,
expectedEncodingErr: "xor of root token failed: length of byte slices is not equivalent: 24 != 9",
expectedDecodingErr: "",
},
{
token: "someToken",
name: "test token encoding with base62",
otpLength: len("someToken"),
expectedEncodingErr: "",
expectedDecodingErr: "",
},
{
token: "someToken",
name: "test token encoding with base62 - wrong otp length",
otpLength: len("someToken") + 1,
expectedEncodingErr: "xor of root token failed: length of byte slices is not equivalent: 10 != 9",
expectedDecodingErr: "",
},
{
token: "",
name: "test no token to encode",
otpLength: 0,
expectedEncodingErr: "no token provided",
expectedDecodingErr: "",
},
}
for _, otpTestCase := range otpTestCases {
t.Run(otpTestCase.name, func(t *testing.T) {
otp, err := GenerateOTP(otpTestCase.otpLength)
if err != nil {
t.Fatal(err.Error())
}
encodedToken, err := EncodeToken(otpTestCase.token, otp)
if err != nil || otpTestCase.expectedDecodingErr != "" {
assert.EqualError(t, err, otpTestCase.expectedEncodingErr)
return
}
assert.NotEqual(t, otp, encodedToken)
assert.NotEqual(t, encodedToken, otpTestCase.token)
decodedToken, err := DecodeToken(encodedToken, otp, len(otp))
if err != nil || otpTestCase.expectedDecodingErr != "" {
assert.EqualError(t, err, otpTestCase.expectedDecodingErr)
return
}
assert.Equal(t, otpTestCase.token, decodedToken)
})
}
}
func TestTokenEncodingDecodingWithNoOTPorPGPKey(t *testing.T) {
_, err := EncodeToken("", "")
assert.EqualError(t, err, "no token provided")
}

View File

@ -0,0 +1,40 @@
package roottoken
import (
"crypto/rand"
"encoding/base64"
"fmt"
"github.com/hashicorp/go-secure-stdlib/base62"
)
// DefaultBase64EncodedOTPLength is the number of characters that will be randomly generated
// before the Base64 encoding process takes place.
const defaultBase64EncodedOTPLength = 16
// GenerateOTP generates a random token and encodes it as a Base64 or as a Base62 encoded string.
// Returns 0 if the generation completed without any error, 2 otherwise, along with the error.
func GenerateOTP(otpLength int) (string, error) {
switch otpLength {
case 0:
// This is the fallback case
buf := make([]byte, defaultBase64EncodedOTPLength)
readLen, err := rand.Read(buf)
if err != nil {
return "", fmt.Errorf("error reading random bytes: %s", err)
}
if readLen != defaultBase64EncodedOTPLength {
return "", fmt.Errorf("read %d bytes when we should have read 16", readLen)
}
return base64.StdEncoding.EncodeToString(buf), nil
default:
otp, err := base62.Random(otpLength)
if err != nil {
return "", fmt.Errorf("error reading random bytes: %w", err)
}
return otp, nil
}
}

View File

@ -0,0 +1,19 @@
package roottoken
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestBase64OTPGeneration(t *testing.T) {
token, err := GenerateOTP(0)
assert.Len(t, token, 24)
assert.Nil(t, err)
}
func TestBase62OTPGeneration(t *testing.T) {
token, err := GenerateOTP(20)
assert.Len(t, token, 20)
assert.Nil(t, err)
}

View File

@ -9,8 +9,8 @@ import (
"github.com/hashicorp/go-uuid"
"github.com/hashicorp/vault/helper/pgpkeys"
"github.com/hashicorp/vault/helper/xor"
"github.com/hashicorp/vault/sdk/helper/consts"
"github.com/hashicorp/vault/sdk/helper/roottoken"
"github.com/hashicorp/vault/shamir"
)
@ -318,40 +318,28 @@ func (c *Core) GenerateRootUpdate(ctx context.Context, key []byte, nonce string,
return nil, err
}
var tokenBytes []byte
var encodedToken string
// Get the encoded value first so that if there is an error we don't create
// the root token.
switch {
case len(c.generateRootConfig.OTP) > 0:
// This function performs decoding checks so rather than decode the OTP,
// just encode the value we're passing in.
tokenBytes, err = xor.XORBytes([]byte(c.generateRootConfig.OTP), []byte(token))
if err != nil {
cleanupFunc()
c.logger.Error("xor of root token failed", "error", err)
return nil, err
}
token = base64.RawStdEncoding.EncodeToString(tokenBytes)
encodedToken, err = roottoken.EncodeToken(token, c.generateRootConfig.OTP)
case len(c.generateRootConfig.PGPKey) > 0:
_, tokenBytesArr, err := pgpkeys.EncryptShares([][]byte{[]byte(token)}, []string{c.generateRootConfig.PGPKey})
if err != nil {
cleanupFunc()
c.logger.Error("error encrypting new root token", "error", err)
return nil, err
}
token = base64.StdEncoding.EncodeToString(tokenBytesArr[0])
var tokenBytesArr [][]byte
_, tokenBytesArr, err = pgpkeys.EncryptShares([][]byte{[]byte(token)}, []string{c.generateRootConfig.PGPKey})
encodedToken = base64.StdEncoding.EncodeToString(tokenBytesArr[0])
default:
err = fmt.Errorf("unreachable condition")
}
if err != nil {
cleanupFunc()
return nil, fmt.Errorf("unreachable condition")
return nil, err
}
results := &GenerateRootResult{
Progress: progress,
Required: config.SecretThreshold,
EncodedToken: token,
EncodedToken: encodedToken,
PGPFingerprint: c.generateRootConfig.PGPFingerprint,
}

View File

@ -7,7 +7,7 @@ import (
"github.com/hashicorp/go-secure-stdlib/base62"
"github.com/hashicorp/vault/helper/namespace"
"github.com/hashicorp/vault/helper/pgpkeys"
"github.com/hashicorp/vault/helper/xor"
"github.com/hashicorp/vault/sdk/helper/xor"
)
func TestCore_GenerateRoot_Lifecycle(t *testing.T) {