From a11460e10de07a262408914f85a1603d887736ea Mon Sep 17 00:00:00 2001 From: daeMOn Date: Tue, 6 Oct 2020 15:03:44 +0200 Subject: [PATCH 01/32] idp: extract access token generation --- cmd/hubauth-ext/main.go | 7 ++-- pkg/idp/oauth.go | 62 +++++++++++++---------------------- pkg/idp/oauth_test.go | 42 +++++++----------------- pkg/idp/steps.go | 37 +++++++-------------- pkg/idp/steps_test.go | 53 ++++++++++++++++-------------- pkg/idp/token/builder.go | 57 ++++++++++++++++++++++++++++++++ pkg/idp/token/builder_test.go | 57 ++++++++++++++++++++++++++++++++ pkg/kmssign/kms.go | 14 ++++++++ 8 files changed, 208 insertions(+), 121 deletions(-) create mode 100644 pkg/idp/token/builder.go create mode 100644 pkg/idp/token/builder_test.go diff --git a/cmd/hubauth-ext/main.go b/cmd/hubauth-ext/main.go index d78b7c7..ef75f1f 100644 --- a/cmd/hubauth-ext/main.go +++ b/cmd/hubauth-ext/main.go @@ -15,6 +15,7 @@ import ( "github.com/flynn/hubauth/pkg/datastore" "github.com/flynn/hubauth/pkg/httpapi" "github.com/flynn/hubauth/pkg/idp" + "github.com/flynn/hubauth/pkg/idp/token" "github.com/flynn/hubauth/pkg/kmssign" "github.com/flynn/hubauth/pkg/rp/google" "go.opencensus.io/plugin/ochttp" @@ -77,10 +78,12 @@ func main() { os.Getenv("RP_GOOGLE_CLIENT_SECRET"), os.Getenv("BASE_URL")+"/rp/google", ), - kmsClient, []byte(secret("CODE_KEY_SECRET")), refreshKey, - idp.AudienceKeyNameFunc(os.Getenv("PROJECT_ID"), os.Getenv("KMS_LOCATION"), os.Getenv("KMS_KEYRING")), + token.NewSignedPBBuilder( + kmsClient, + kmssign.AudienceKeyNameFunc(os.Getenv("PROJECT_ID"), os.Getenv("KMS_LOCATION"), os.Getenv("KMS_KEYRING")), + ), ), CookieKey: []byte(secret("COOKIE_KEY_SECRET")), ProjectID: os.Getenv("PROJECT_ID"), diff --git a/pkg/idp/oauth.go b/pkg/idp/oauth.go index 3815fad..1dccd8f 100644 --- a/pkg/idp/oauth.go +++ b/pkg/idp/oauth.go @@ -2,16 +2,14 @@ package idp import ( "context" - "crypto" "encoding/base64" - "net/url" "strings" "time" "github.com/flynn/hubauth/pkg/clog" "github.com/flynn/hubauth/pkg/hmacpb" "github.com/flynn/hubauth/pkg/hubauth" - "github.com/flynn/hubauth/pkg/kmssign" + "github.com/flynn/hubauth/pkg/idp/token" "github.com/flynn/hubauth/pkg/pb" "github.com/flynn/hubauth/pkg/rp" "github.com/flynn/hubauth/pkg/signpb" @@ -22,22 +20,10 @@ import ( "golang.org/x/sync/errgroup" ) -type AudienceKeyNamer func(audience string) string - const oobRedirectURI = "urn:ietf:wg:oauth:2.0:oob" const codeExpiry = 30 * time.Second const accessTokenDuration = 5 * time.Minute -func AudienceKeyNameFunc(projectID, location, keyRing string) func(string) string { - return func(aud string) string { - u, err := url.Parse(aud) - if err != nil { - return "" - } - return fmt.Sprintf("projects/%s/locations/%s/keyRings/%s/cryptoKeys/%s/cryptoKeyVersions/1", projectID, location, keyRing, strings.Replace(u.Host, ".", "_", -1)) - } -} - type clock interface { Now() time.Time } @@ -61,17 +47,15 @@ type idpSteps interface { SignRefreshToken(ctx context.Context, signKey signpb.PrivateKey, t *signedRefreshTokenData) (string, error) RenewRefreshToken(ctx context.Context, clientID, oldTokenID string, oldTokenIssueTime, now time.Time) (*hubauth.RefreshToken, error) VerifyRefreshToken(ctx context.Context, rt *hubauth.RefreshToken, now time.Time) error - SignAccessToken(ctx context.Context, signKey signpb.PrivateKey, t *accessTokenData, now time.Time) (string, error) + SignAccessToken(ctx context.Context, audience string, t *token.AccessTokenData, now time.Time) (string, error) } type idpService struct { - db hubauth.DataStore - rp rp.AuthService - kms kmssign.KMSClient + db hubauth.DataStore + rp rp.AuthService - codeKey hmacpb.Key - refreshKey signpb.Key - audienceKey AudienceKeyNamer + codeKey hmacpb.Key + refreshKey signpb.Key steps idpSteps clock clock @@ -79,16 +63,15 @@ type idpService struct { var _ hubauth.IdPService = (*idpService)(nil) -func New(db hubauth.DataStore, rp rp.AuthService, kms kmssign.KMSClient, codeKey hmacpb.Key, refreshKey signpb.Key, audienceKey AudienceKeyNamer) hubauth.IdPService { +func New(db hubauth.DataStore, rp rp.AuthService, codeKey hmacpb.Key, refreshKey signpb.Key, tokenBuilder token.AccessTokenBuilder) hubauth.IdPService { return &idpService{ - db: db, - rp: rp, - kms: kms, - codeKey: codeKey, - refreshKey: refreshKey, - audienceKey: audienceKey, + db: db, + rp: rp, + codeKey: codeKey, + refreshKey: refreshKey, steps: &steps{ - db: db, + db: db, + builder: tokenBuilder, }, clock: clockImpl{}, } @@ -325,11 +308,11 @@ func (s *idpService) ExchangeCode(parentCtx context.Context, req *hubauth.Exchan if req.Audience == "" { return nil } - signKey := kmssign.NewPrivateKey(s.kms, s.audienceKey(req.Audience), crypto.SHA256) - accessToken, err = s.steps.SignAccessToken(ctx, signKey, &accessTokenData{ - clientID: req.ClientID, - userID: codeInfo.UserId, - userEmail: codeInfo.UserEmail, + + accessToken, err = s.steps.SignAccessToken(ctx, req.Audience, &token.AccessTokenData{ + ClientID: req.ClientID, + UserID: codeInfo.UserId, + UserEmail: codeInfo.UserEmail, }, now) return err }) @@ -399,11 +382,10 @@ func (s *idpService) RefreshToken(ctx context.Context, req *hubauth.RefreshToken if req.Audience == "" { return nil } - signKey := kmssign.NewPrivateKey(s.kms, s.audienceKey(req.Audience), crypto.SHA256) - accessToken, err = s.steps.SignAccessToken(ctx, signKey, &accessTokenData{ - clientID: req.ClientID, - userID: oldToken.UserID, - userEmail: oldToken.UserEmail, + accessToken, err = s.steps.SignAccessToken(ctx, req.Audience, &token.AccessTokenData{ + ClientID: req.ClientID, + UserID: oldToken.UserID, + UserEmail: oldToken.UserEmail, }, now) return err }) diff --git a/pkg/idp/oauth_test.go b/pkg/idp/oauth_test.go index d513374..3f2e62c 100644 --- a/pkg/idp/oauth_test.go +++ b/pkg/idp/oauth_test.go @@ -2,7 +2,6 @@ package idp import ( "context" - "crypto" "crypto/rand" "errors" "fmt" @@ -14,6 +13,7 @@ import ( "github.com/flynn/hubauth/pkg/datastore" "github.com/flynn/hubauth/pkg/hmacpb" "github.com/flynn/hubauth/pkg/hubauth" + "github.com/flynn/hubauth/pkg/idp/token" "github.com/flynn/hubauth/pkg/kmssign" "github.com/flynn/hubauth/pkg/kmssign/kmssim" "github.com/flynn/hubauth/pkg/pb" @@ -40,10 +40,6 @@ func (m *mockAuthService) Exchange(ctx context.Context, rr *rp.RedirectResult) ( return args.Get(0).(*rp.Token), args.Error(1) } -func audienceKeyNamer(s string) string { - return fmt.Sprintf("%s_named", s) -} - type mockSteps struct { mock.Mock } @@ -82,8 +78,8 @@ func (m *mockSteps) SignRefreshToken(ctx context.Context, signKey signpb.Private args := m.Called(ctx, signKey, t) return args.String(0), args.Error(1) } -func (m *mockSteps) SignAccessToken(ctx context.Context, signKey signpb.PrivateKey, t *accessTokenData, now time.Time) (string, error) { - args := m.Called(ctx, signKey, t, now) +func (m *mockSteps) SignAccessToken(ctx context.Context, audience string, t *token.AccessTokenData, now time.Time) (string, error) { + args := m.Called(ctx, audience, t, now) return args.String(0), args.Error(1) } func (m *mockSteps) RenewRefreshToken(ctx context.Context, clientID, oldTokenID string, oldTokenIssueTime, now time.Time) (*hubauth.RefreshToken, error) { @@ -124,7 +120,7 @@ func newTestIdPService(t *testing.T, kmsKeys ...string) *idpService { refreshKey, err := kmssign.NewKey(context.Background(), kms, refreshKeyName) require.NoError(t, err) - s := New(db, authService, kms, codeKey, refreshKey, audienceKeyNamer).(*idpService) + s := New(db, authService, codeKey, refreshKey, nil).(*idpService) s.steps = &mockSteps{} s.clock = &mockClock{} @@ -669,10 +665,10 @@ func TestExchangeCode(t *testing.T) { }).Return(verifiedCode, nil) idpService.steps.(*mockSteps).On("SaveRefreshToken", mock.Anything, b64CodeID, redirectURI, rtData).Return(client, nil) idpService.steps.(*mockSteps).On("SignRefreshToken", mock.Anything, idpService.refreshKey, signedRTData).Return(refreshToken, nil) - idpService.steps.(*mockSteps).On("SignAccessToken", mock.Anything, kmssign.NewPrivateKey(idpService.kms, audienceKeyNamer(audienceURL), crypto.SHA256), &accessTokenData{ - clientID: clientID, - userID: userID, - userEmail: userEmail, + idpService.steps.(*mockSteps).On("SignAccessToken", mock.Anything, audienceURL, &token.AccessTokenData{ + ClientID: clientID, + UserID: userID, + UserEmail: userEmail, }, now).Return(accessToken, nil) req := &hubauth.ExchangeCodeRequest{ @@ -898,11 +894,10 @@ func TestRefreshToken(t *testing.T) { }, ExpiryTime: expireTimeProto.AsTime(), }).Return(newRefreshTokenStr, nil) - signKey := kmssign.NewPrivateKey(idpService.kms, audienceKeyNamer(testCase.AudienceURL), crypto.SHA256) - idpService.steps.(*mockSteps).On("SignAccessToken", mock.Anything, signKey, &accessTokenData{ - clientID: b64ClientID, - userID: userID, - userEmail: userEmail, + idpService.steps.(*mockSteps).On("SignAccessToken", mock.Anything, testCase.AudienceURL, &token.AccessTokenData{ + ClientID: b64ClientID, + UserID: userID, + UserEmail: userEmail, }, now).Return(newAccessTokenStr, nil) oldTokenSigned, err := signpb.SignMarshal(context.Background(), idpService.refreshKey, &pb.RefreshToken{ @@ -1015,12 +1010,6 @@ func TestRefreshTokenStepErrors(t *testing.T) { } func prepareInvalidRefreshTokenTestCases(t *testing.T, idpService *idpService, wrongKeyName string) []*invalidRefreshTokenTestCase { - wrongKey, err := kmssign.NewKey(context.Background(), idpService.kms, wrongKeyName) - require.NoError(t, err) - - wrongKeyRefreshToken, err := signpb.SignMarshal(context.Background(), wrongKey, &pb.RefreshToken{}) - require.NoError(t, err) - now := time.Now() expiredTime, _ := ptypes.TimestampProto(now.Add(-1 * time.Second)) expiredRefreshToken, err := signpb.SignMarshal(context.Background(), idpService.refreshKey, &pb.RefreshToken{ @@ -1045,13 +1034,6 @@ func prepareInvalidRefreshTokenTestCases(t *testing.T, idpService *idpService, w Description: "invalid refresh_token", }, }, - { - RefreshToken: base64Encode(wrongKeyRefreshToken), - Err: &hubauth.OAuthError{ - Code: "invalid_grant", - Description: "invalid refresh_token", - }, - }, { RefreshToken: base64Encode(expiredRefreshToken), Err: &hubauth.OAuthError{ diff --git a/pkg/idp/steps.go b/pkg/idp/steps.go index 5a2c6be..96769c9 100644 --- a/pkg/idp/steps.go +++ b/pkg/idp/steps.go @@ -9,6 +9,7 @@ import ( "github.com/flynn/hubauth/pkg/clog" "github.com/flynn/hubauth/pkg/hmacpb" "github.com/flynn/hubauth/pkg/hubauth" + "github.com/flynn/hubauth/pkg/idp/token" "github.com/flynn/hubauth/pkg/pb" "github.com/flynn/hubauth/pkg/signpb" "github.com/golang/protobuf/ptypes" @@ -19,7 +20,8 @@ import ( ) type steps struct { - db hubauth.DataStore + db hubauth.DataStore + builder token.AccessTokenBuilder } var _ idpSteps = (*steps)(nil) @@ -349,37 +351,21 @@ func (s *steps) VerifyRefreshToken(ctx context.Context, rt *hubauth.RefreshToken return nil } -type accessTokenData struct { - clientID string - userID string - userEmail string -} - -func (s *steps) SignAccessToken(ctx context.Context, signKey signpb.PrivateKey, t *accessTokenData, now time.Time) (token string, err error) { +func (s *steps) SignAccessToken(ctx context.Context, audience string, t *token.AccessTokenData, now time.Time) (token string, err error) { ctx, span := trace.StartSpan(ctx, "idp.SignAccessToken") span.AddAttributes( - trace.StringAttribute("client_id", t.clientID), - trace.StringAttribute("user_id", t.userID), - trace.StringAttribute("user_email", t.userEmail), + trace.StringAttribute("client_id", t.ClientID), + trace.StringAttribute("user_id", t.UserID), + trace.StringAttribute("user_email", t.UserEmail), ) defer span.End() - exp, _ := ptypes.TimestampProto(now.Add(accessTokenDuration)) - iss, _ := ptypes.TimestampProto(now) - msg := &pb.AccessToken{ - ClientId: t.clientID, - UserId: t.userID, - UserEmail: t.userEmail, - IssueTime: iss, - ExpireTime: exp, - } - tokenBytes, err := signpb.SignMarshal(ctx, signKey, msg) + tokenBytes, err := s.builder.Build(ctx, audience, t, now, accessTokenDuration) if err != nil { - return "", fmt.Errorf("idp: error signing access token: %w", err) + return "", fmt.Errorf("idp: error building access token: %w", err) } - idBytes := sha256.Sum256(tokenBytes) - token = base64.URLEncoding.EncodeToString(tokenBytes) + idBytes := sha256.Sum256(tokenBytes) accessTokenID := base64Encode(idBytes[:]) span.AddAttributes(trace.StringAttribute("access_token_id", accessTokenID)) @@ -387,5 +373,6 @@ func (s *steps) SignAccessToken(ctx context.Context, signKey signpb.PrivateKey, zap.String("issued_access_token_id", accessTokenID), zap.Duration("issued_access_token_expires_in", accessTokenDuration), ) - return token, nil + + return base64.URLEncoding.EncodeToString(tokenBytes), nil } diff --git a/pkg/idp/steps_test.go b/pkg/idp/steps_test.go index 46865c6..971b0a9 100644 --- a/pkg/idp/steps_test.go +++ b/pkg/idp/steps_test.go @@ -12,21 +12,39 @@ import ( "github.com/flynn/hubauth/pkg/datastore" "github.com/flynn/hubauth/pkg/hmacpb" "github.com/flynn/hubauth/pkg/hubauth" + "github.com/flynn/hubauth/pkg/idp/token" "github.com/flynn/hubauth/pkg/kmssign" "github.com/flynn/hubauth/pkg/kmssign/kmssim" "github.com/flynn/hubauth/pkg/pb" "github.com/flynn/hubauth/pkg/signpb" "github.com/golang/protobuf/ptypes" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "google.golang.org/protobuf/proto" ) +const ( + testAudienceName = "audienceXYZ" +) + +type mockAccessTokenBuilder struct { + mock.Mock +} + +var _ token.AccessTokenBuilder = (*mockAccessTokenBuilder)(nil) + +func (m *mockAccessTokenBuilder) Build(ctx context.Context, audience string, t *token.AccessTokenData, now time.Time, duration time.Duration) ([]byte, error) { + args := m.Called(ctx, audience, t, now, duration) + return args.Get(0).([]byte), args.Error(1) +} + func newTestSteps(t *testing.T) *steps { dsc, err := gdatastore.NewClient(context.Background(), "test") require.NoError(t, err) return &steps{ - db: datastore.New(dsc), + db: datastore.New(dsc), + builder: &mockAccessTokenBuilder{}, } } @@ -754,36 +772,23 @@ func TestVerifyRefreshTokenErrors(t *testing.T) { func TestSignAccessToken(t *testing.T) { s := newTestSteps(t) - signKeyName := "refreshKey" - kms := kmssim.NewClient([]string{signKeyName}) - signKey, err := kmssign.NewKey(context.Background(), kms, signKeyName) - require.NoError(t, err) - now := time.Now() - data := &accessTokenData{ - clientID: "clientID", - userID: "userID", - userEmail: "userEmail", + data := &token.AccessTokenData{ + ClientID: "clientID", + UserID: "userID", + UserEmail: "userEmail", } - accessToken, err := s.SignAccessToken(context.Background(), signKey, data, now) + expectedAccessToken := []byte("expected-access-token") + + s.builder.(*mockAccessTokenBuilder).On("Build", mock.Anything, testAudienceName, data, now, accessTokenDuration).Return(expectedAccessToken, nil) + + accessToken, err := s.SignAccessToken(context.Background(), testAudienceName, data, now) require.NoError(t, err) require.NotEmpty(t, accessToken) - got := new(pb.AccessToken) - accessTokenBytes, err := base64Decode(accessToken) require.NoError(t, err) - - require.NoError(t, signpb.VerifyUnmarshal(signKey, accessTokenBytes, got)) - require.Equal(t, data.clientID, got.ClientId) - require.Equal(t, data.userID, got.UserId) - require.Equal(t, data.userEmail, got.UserEmail) - - nowPb, _ := ptypes.TimestampProto(now) - require.Equal(t, nowPb, got.IssueTime) - - expirePb, _ := ptypes.TimestampProto(now.Add(accessTokenDuration)) - require.Equal(t, expirePb, got.ExpireTime) + require.Equal(t, expectedAccessToken, accessTokenBytes) } diff --git a/pkg/idp/token/builder.go b/pkg/idp/token/builder.go new file mode 100644 index 0000000..5a60575 --- /dev/null +++ b/pkg/idp/token/builder.go @@ -0,0 +1,57 @@ +package token + +import ( + "context" + "crypto" + "fmt" + "time" + + "github.com/flynn/hubauth/pkg/kmssign" + "github.com/flynn/hubauth/pkg/pb" + "github.com/flynn/hubauth/pkg/signpb" + "github.com/golang/protobuf/ptypes" +) + +type AccessTokenData struct { + ClientID string + UserID string + UserEmail string +} + +type AccessTokenBuilder interface { + Build(ctx context.Context, audience string, t *AccessTokenData, now time.Time, duration time.Duration) ([]byte, error) +} + +type signedPbBuilder struct { + kms kmssign.KMSClient + audienceKey kmssign.AudienceKeyNamer +} + +var _ AccessTokenBuilder = (*signedPbBuilder)(nil) + +func NewSignedPBBuilder(kms kmssign.KMSClient, audienceKey kmssign.AudienceKeyNamer) AccessTokenBuilder { + return &signedPbBuilder{ + kms: kms, + audienceKey: audienceKey, + } +} + +func (b *signedPbBuilder) Build(ctx context.Context, audience string, t *AccessTokenData, now time.Time, duration time.Duration) ([]byte, error) { + signKey := kmssign.NewPrivateKey(b.kms, b.audienceKey(audience), crypto.SHA256) + + exp, _ := ptypes.TimestampProto(now.Add(duration)) + iss, _ := ptypes.TimestampProto(now) + msg := &pb.AccessToken{ + ClientId: t.ClientID, + UserId: t.UserID, + UserEmail: t.UserEmail, + IssueTime: iss, + ExpireTime: exp, + } + tokenBytes, err := signpb.SignMarshal(ctx, signKey, msg) + if err != nil { + return nil, fmt.Errorf("token: error signing access token: %w", err) + } + + return tokenBytes, nil +} diff --git a/pkg/idp/token/builder_test.go b/pkg/idp/token/builder_test.go new file mode 100644 index 0000000..9785c77 --- /dev/null +++ b/pkg/idp/token/builder_test.go @@ -0,0 +1,57 @@ +package token + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/flynn/hubauth/pkg/kmssign" + "github.com/flynn/hubauth/pkg/kmssign/kmssim" + "github.com/flynn/hubauth/pkg/pb" + "github.com/flynn/hubauth/pkg/signpb" + "github.com/golang/protobuf/ptypes" + "github.com/stretchr/testify/require" +) + +func audienceKeyNamer(s string) string { + return fmt.Sprintf("%s_named", s) +} + +func TestSignedPBBuilder(t *testing.T) { + audienceName := "audience_url" + audienceKeyName := audienceKeyNamer(audienceName) + kms := kmssim.NewClient([]string{audienceKeyName}) + + builder := NewSignedPBBuilder(kms, audienceKeyNamer) + + signKey, err := kmssign.NewKey(context.Background(), kms, audienceKeyName) + require.NoError(t, err) + + now := time.Now() + ctx := context.Background() + + data := &AccessTokenData{ + ClientID: "clientID", + UserEmail: "userEmail", + UserID: "userID", + } + + accessTokenDuration := 5 * time.Minute + + accessTokenBytes, err := builder.Build(ctx, audienceName, data, now, accessTokenDuration) + require.NoError(t, err) + + got := new(pb.AccessToken) + require.NoError(t, signpb.VerifyUnmarshal(signKey, accessTokenBytes, got)) + + require.Equal(t, data.ClientID, got.ClientId) + require.Equal(t, data.UserID, got.UserId) + require.Equal(t, data.UserEmail, got.UserEmail) + + nowPb, _ := ptypes.TimestampProto(now) + require.Equal(t, nowPb, got.IssueTime) + + expirePb, _ := ptypes.TimestampProto(now.Add(accessTokenDuration)) + require.Equal(t, expirePb, got.ExpireTime) +} diff --git a/pkg/kmssign/kms.go b/pkg/kmssign/kms.go index 8671e3e..766ffe3 100644 --- a/pkg/kmssign/kms.go +++ b/pkg/kmssign/kms.go @@ -8,6 +8,8 @@ import ( "encoding/pem" "io" "math/big" + "net/url" + "strings" gax "github.com/googleapis/gax-go/v2" "golang.org/x/crypto/cryptobyte" @@ -16,6 +18,18 @@ import ( kmspb "google.golang.org/genproto/googleapis/cloud/kms/v1" ) +type AudienceKeyNamer func(audience string) string + +func AudienceKeyNameFunc(projectID, location, keyRing string) func(string) string { + return func(aud string) string { + u, err := url.Parse(aud) + if err != nil { + return "" + } + return fmt.Sprintf("projects/%s/locations/%s/keyRings/%s/cryptoKeys/%s/cryptoKeyVersions/1", projectID, location, keyRing, strings.Replace(u.Host, ".", "_", -1)) + } +} + type KMSClient interface { AsymmetricSign(ctx context.Context, req *kmspb.AsymmetricSignRequest, opts ...gax.CallOption) (*kmspb.AsymmetricSignResponse, error) GetPublicKey(ctx context.Context, req *kmspb.GetPublicKeyRequest, opts ...gax.CallOption) (*kmspb.PublicKey, error) From 3b83165af6f72ab22d24d0b6101d1ccfc0f19956 Mon Sep 17 00:00:00 2001 From: daeMOn Date: Thu, 8 Oct 2020 16:53:04 +0200 Subject: [PATCH 02/32] add biscuit wrappers and helpers to generate, sign and verify hubauth biscuits --- go.mod | 1 + go.sum | 97 +++------- pkg/biscuit/biscuit.go | 137 ++++++++++++++ pkg/biscuit/biscuit_test.go | 57 ++++++ pkg/biscuit/signature.go | 162 ++++++++++++++++ pkg/biscuit/signature_test.go | 258 ++++++++++++++++++++++++++ pkg/biscuit/wrapper.go | 336 ++++++++++++++++++++++++++++++++++ 7 files changed, 972 insertions(+), 76 deletions(-) create mode 100644 pkg/biscuit/biscuit.go create mode 100644 pkg/biscuit/biscuit_test.go create mode 100644 pkg/biscuit/signature.go create mode 100644 pkg/biscuit/signature_test.go create mode 100644 pkg/biscuit/wrapper.go diff --git a/go.mod b/go.mod index 20f3edd..de15033 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/alecthomas/kong v0.2.12 github.com/aws/aws-sdk-go v1.34.6 // indirect github.com/census-instrumentation/opencensus-proto v0.3.0 // indirect + github.com/flynn/biscuit-go v0.0.0-20200907174027-193b7bdbbdca github.com/golang/protobuf v1.4.3 github.com/googleapis/gax-go/v2 v2.0.5 github.com/jedib0t/go-pretty/v6 v6.0.5 diff --git a/go.sum b/go.sum index 45c0169..39fe673 100644 --- a/go.sum +++ b/go.sum @@ -19,9 +19,8 @@ cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZ cloud.google.com/go v0.62.0 h1:RmDygqvj27Zf3fCQjQRtLyC7KwFcHkeJitcO0OoGOcA= cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.68.0 h1:AnVkaPGAuWaIY/8a75HlNzZNrHDee6YL4rWkwS+CeyE= -cloud.google.com/go v0.68.0/go.mod h1:91NO4SCDjUfe1zeC0f4/dpckkUNpuNEyqm4X2KLrzNQ= cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0 h1:kpgPA77kSSbjSs+fWHkPTxQ6J5Z2Qkruo5jfXEkHxNQ= cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= @@ -53,14 +52,14 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7 github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/alecthomas/kong v0.2.11 h1:RKeJXXWfg9N47RYfMm0+igkxBCTF4bzbneAxaqid0c4= -github.com/alecthomas/kong v0.2.11/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE= +github.com/alecthomas/kong v0.2.12 h1:X3kkCOXGUNzLmiu+nQtoxWqj4U2a39MpSJR3QdQXOwI= github.com/alecthomas/kong v0.2.12/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE= +github.com/alecthomas/participle v0.6.0/go.mod h1:HfdmEuwvr12HXQN44HPWXR0lHmVolVYe4dyL6lQ3duY= +github.com/alecthomas/repr v0.0.0-20181024024818-d37bc2a10ba1/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= github.com/aws/aws-sdk-go v1.23.20 h1:2CBuL21P0yKdZN5urf2NxKa1ha8fhnY+A3pBCHFeZoA= github.com/aws/aws-sdk-go v1.23.20/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.34.6 h1:2aPXQGkR6xeheN5dns13mSoDWeUlj4wDmfZ+8ZDHauw= github.com/aws/aws-sdk-go v1.34.6/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= -github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.3.0 h1:t/LhUZLVitR1Ow2YOnduCsavhwFUklBMoGVYUCqmCqk= github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -79,11 +78,12 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/flynn/biscuit-go v0.0.0-20200907174027-193b7bdbbdca h1:LUZQQzaCT+gltxii4icyPH5oMdAP38JmbvO9aI0E4qM= +github.com/flynn/biscuit-go v0.0.0-20200907174027-193b7bdbbdca/go.mod h1:EMJZ3stAYtwaP763F5HcGjPjCnYu21V2TEsg/iw88I8= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -96,20 +96,16 @@ github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0 h1:oOuy+ugB+P/kBdUnG5QaMXSIyJ1q38wWSojYCb3z5VQ= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= @@ -121,16 +117,13 @@ github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1 h1:/exdXoGamhu5ONeUJH0deniYLWYvQwW66yvlfiiKTu0= github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= @@ -143,7 +136,6 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200905233945-acf8798be1f7/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= @@ -151,13 +143,14 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gtank/ristretto255 v0.1.2 h1:JEqUCPA1NvLq5DwYtuzigd7ss8fwbYay9fi4/5uMzcc= +github.com/gtank/ristretto255 v0.1.2/go.mod h1:Ph5OpO6c7xKUGROZfWVLiJf9icMDwUeIvY4OmlYW69o= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/jedib0t/go-pretty/v6 v6.0.5 h1:oOo0/jSb3NEYKT6l1hhFXoX2UZnkanMuCE2DVT1mqnE= github.com/jedib0t/go-pretty/v6 v6.0.5/go.mod h1:MTr6FgcfNdnN5wPVBzJ6mhJeDyiF0yBvS2TMXEV/XSU= -github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc= github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= @@ -172,7 +165,6 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -186,7 +178,6 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -197,9 +188,7 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3 h1:8sGtKOrtQqkN1bp2AtX+misvLIlOmsEsNd+9NIcPEm8= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4 h1:LYy1Hy3MJdrCdMwwzxA/dRok4ejH+RwNGbuoD9fCjto= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= @@ -214,7 +203,6 @@ go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de h1:ikNHVSjEfnvz6sxdSPCaPt572qowuyMDMJLLm3Db3ig= @@ -227,7 +215,6 @@ golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd h1:zkO/Lhoka23X63N9OSzpSeROEUQ5ODw47tM3YWjygbs= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6 h1:QE6XYQK6naiK1EPAe1g/ILLxN5RBoH5xkJk3CqlMI/Y= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= @@ -243,10 +230,10 @@ golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367 h1:0IiAsCRByjO2QjX7ZPkw5oU9x+n1YqRL802rjC0c3Aw= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 h1:2M3HP5CCK1Si9FQhwnzYhXdG6DXeebvUHFpre8QvbyI= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= @@ -254,14 +241,13 @@ golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKG golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0 h1:8pl+sMODzuvGJkmj2W4kZihvVb5mKm8pB/X44PIQHv8= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -275,49 +261,42 @@ golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0 h1:MsuvTghUPjX762sGLnGsxC3HM0B5r83wEtYcYR8/vRs= golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200927032502-5d4f70055728/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201010224723-4f7140c49acb h1:mUVeFHoDKis5nxCAzoAi7E8Ghb86EXh/RK6wtvJIqRY= -golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11 h1:lwlPPsmjDKK0J6eG6xDWd5XPehI0R024zxjDnw3esPA= golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 h1:ld7aEMNHoBnnDAX15v1T6z31v8HwR2A9FYOuAhWqkwc= golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5 h1:Lm4OryKCca1vehdsWogr9N4t7NfZxLbJoc/H0w4K4S4= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180816055513-1c9583448a9c/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -335,9 +314,7 @@ golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4 h1:sfkvUWPNGwSV+8/fNqctR5lS2AqCSqYwXdrjCxp/dXo= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -346,26 +323,25 @@ golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642 h1:B6caxRw+hozq68X2MY7jEpZh/cr4/aHLv9xU8Kkadrw= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f h1:Fqb3ao1hUmOR3GkUOg/Y+BadLwykBIzs5q8Ez2SbHyc= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3 h1:kzM6+9dur93BcC2kVlYl34cHU+TYZLanmpSJHVMmL64= golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -397,7 +373,6 @@ golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56 h1:DFtSed2q3HtNuVazwVDZ4nSRS/JrZEig0gz2BY4VNrg= golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= @@ -408,21 +383,17 @@ golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7 h1:LHW24ah7B+uV/OePwNP0p/t889F3QSyLvY8Sg/bK0SY= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d h1:szSOL78iTCl0LF1AMjhSWJj8tIM0KixlUUnBtYXsmd8= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= golang.org/x/tools v0.0.0-20200916150407-587cf2330ce8/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= -golang.org/x/tools v0.0.0-20201002184944-ecd9fd270d5d h1:vWQvJ/Z0Lu+9/8oQ/pAYXNzbc7CMnBl+tULGVHOy3oE= -golang.org/x/tools v0.0.0-20201002184944-ecd9fd270d5d/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2 h1:vEtypaVub6UvKkiXZ2xx9QIvp9TL7sI7xp7vdi2kezA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -434,36 +405,29 @@ google.golang.org/api v0.10.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhE google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0 h1:0q95w+VuFtv4PAx4PZVQdBMmYbaCHbnfKaEiDIcVyag= google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0 h1:TgDr+1inK2XVUKZx3BYAqQg/GwucGdBkzZjWaTg/I+A= google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0 h1:jMF5hhVfMkTZwHW1SDpKq5CkgWLXOb31Foaca9Zr3oM= google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0 h1:BaiDisFir8O4IJxvAabCGGkQ6yCJegNQqSVoYUNAnbk= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0 h1:yfrXXP61wVuLb0vBcG6qaOoIoqYEzOQS8jum51jkv2w= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= google.golang.org/api v0.32.0 h1:Le77IccnTqEa8ryp9wIpX5W3zYm7Gf9LhOp9PHcwFts= google.golang.org/api v0.32.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= -google.golang.org/api v0.33.0 h1:+gL0XvACeMIvpwLZ5rQZzLn5cwOsgg8dIcfJ2SYfBVw= -google.golang.org/api v0.33.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0 h1:l2Nfbl2GPXdWorv+dT2XfinX2jOOw4zv1VhLstx+6rE= google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.2/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -480,9 +444,7 @@ google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvx google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce h1:1mbrb1tUU+Zmt5C94IGKADBTJZjZXAd+BubWi7r9EiI= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63 h1:YzfoEYWbODU5Fbt37+h7X16BWQbad7Q4S6gclTKFXM8= google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= @@ -491,22 +453,16 @@ google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790 h1:FGjyjrQGURdc98leD1P65IdQD9Zlr4McvRcqIlV6OSs= google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f h1:ohwtWcCwB/fZUxh/vjazHorYmBnua3NmY3CAjwC7mEA= google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c h1:Lq4llNryJoaVFRmvrIwC/ZHH7tNt4tUYIu8+se2aayY= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200916143405-f6a2fa72f0c4/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201002142447-3860012362da/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201014134559-03b6142f0dc9 h1:fG84H9C3EXfuDlzkG+VEPDYHHExklP6scH1QZ5gQTqU= -google.golang.org/genproto v0.0.0-20201014134559-03b6142f0dc9/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc h1:BgQmMjmd7K1zov8j8lYULHW0WnmBGUIMp6+VDwlGErc= google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= @@ -516,34 +472,25 @@ google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1 h1:zvIju4sqAGvwKspUQOhwnpcqSbzi7/H6QomNNjTL4sk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0 h1:bO/TA4OxCOummhSf10siHuG7vJOiwh7SpRpFZDkOgl4= google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1 h1:EC2SB8S04d2r73uptxphDSUG+kTKVgjRPF+N3xpxRB4= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0 h1:M5a8xTlYTxwMn5ZFkwhRabsygDY5G8TYLyQDBxJNAxE= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0 h1:T7P4R73V3SSDPhH7WW7ATbfViLtmamH0DKrP3f9AuDI= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.1 h1:SfXqXS5hkufcdZ/mHtYCh53P2b+92WQq/DZcKLgsFRs= google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.32.0 h1:zWTV+LMdc3kaiJMSTOFz2UgSBgx8RNQoTGiZu3fR9S0= google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0 h1:raiipEjMOIC/TO2AvyTxP25XFdLxNIBwzDh3FM3XztI= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0 h1:qdOKuR/EIArgaWNjetjgTzgVTAZ+S/WXVrq9HW9zimw= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0 h1:UhZDfRO8JRQru4/+LlLE0BRKGF8L+PICnvYZmx/fEGA= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= @@ -559,9 +506,7 @@ honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3 h1:sXmLre5bzIR6ypkjXCDI3jHPssRhc8KD/Ome589sc3U= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= diff --git a/pkg/biscuit/biscuit.go b/pkg/biscuit/biscuit.go new file mode 100644 index 0000000..ef1a452 --- /dev/null +++ b/pkg/biscuit/biscuit.go @@ -0,0 +1,137 @@ +package biscuit + +import ( + "crypto/rand" + "fmt" + + "github.com/flynn/biscuit-go" + "github.com/flynn/biscuit-go/sig" + "github.com/flynn/hubauth/pkg/kmssign" +) + +type UserKeyPair struct { + Public []byte + Private []byte +} + +// GenerateSignable returns a biscuit which will only verify after being +// signed with the private key matching the given userPubkey. +func GenerateSignable(rootKey sig.Keypair, audience string, audienceKey *kmssign.Key, userPubkey []byte) ([]byte, error) { + builder := &hubauthBuilder{ + Builder: biscuit.NewBuilder(rand.Reader, rootKey), + } + + if err := builder.withAudienceSignature(audience, audienceKey); err != nil { + return nil, err + } + + if err := builder.withUserToSignFact(userPubkey); err != nil { + return nil, err + } + + b, err := builder.Build() + if err != nil { + return nil, err + } + + return b.Serialize() +} + +// Sign append a user signature on the given token and return it. +// The UserKeyPair key format to provide depends on the signature algorithm: +// - for ECDSA_P256_SHA256, the private key must be encoded in SEC 1, ASN.1 DER form, +// and the public key in PKIX, ASN.1 DER form. +func Sign(token []byte, rootPubKey sig.PublicKey, userKey *UserKeyPair) ([]byte, error) { + b, err := biscuit.Unmarshal(token) + if err != nil { + return nil, fmt.Errorf("biscuit: failed to unmarshal: %w", err) + } + + v, err := b.Verify(rootPubKey) + if err != nil { + return nil, fmt.Errorf("biscuit: failed to verify: %w", err) + } + verifier := &hubauthVerifier{ + Verifier: v, + } + + toSignData, err := verifier.getUserToSignData(userKey.Public, b.BlockCount()) + if err != nil { + return nil, fmt.Errorf("biscuit: failed to get to_sign data: %w", err) + } + + if err := verifier.ensureNotAlreadyUserSigned(toSignData.DataID, userKey.Public); err != nil { + return nil, fmt.Errorf("biscuit: previous signature check failed: %w", err) + } + + tokenHash, err := b.SHA256Sum(b.BlockCount()) + if err != nil { + return nil, err + } + + signData, err := userSign(tokenHash, userKey, toSignData) + if err != nil { + return nil, fmt.Errorf("biscuit: signature failed: %w", err) + } + + builder := &hubauthBlockBuilder{ + BlockBuilder: b.CreateBlock(), + } + if err := builder.withUserSignature(signData); err != nil { + return nil, fmt.Errorf("biscuit: failed to create signature block: %w", err) + } + + clientKey := sig.GenerateKeypair(rand.Reader) + b, err = b.Append(rand.Reader, clientKey, builder.Build()) + if err != nil { + return nil, fmt.Errorf("biscuit: failed to append signature block: %w", err) + } + + return b.Serialize() +} + +// Verify will verify the biscuit, the included audience and user signature, and return an error +// when anything is invalid. +func Verify(token []byte, rootPubKey sig.PublicKey, audience string, audienceKey *kmssign.Key) error { + b, err := biscuit.Unmarshal(token) + if err != nil { + return fmt.Errorf("biscuit: failed to unmarshal: %w", err) + } + + v, err := b.Verify(rootPubKey) + if err != nil { + return fmt.Errorf("biscuit: failed to verify: %w", err) + } + verifier := &hubauthVerifier{v} + + audienceVerificationData, err := verifier.getAudienceVerificationData(audience) + if err != nil { + return fmt.Errorf("biscuit: failed to retrieve audience signature data: %w", err) + } + + if err := verifyAudienceSignature(audienceKey, audienceVerificationData); err != nil { + return fmt.Errorf("biscuit: failed to verify audience signature: %w", err) + } + if err := verifier.withValidatedAudienceSignature(audienceVerificationData); err != nil { + return fmt.Errorf("biscuit: failed to add validated signature: %w", err) + } + + userVerificationData, err := verifier.getUserVerificationData() + if err != nil { + return fmt.Errorf("biscuit: failed to retrieve user signature data: %w", err) + } + + signedTokenHash, err := b.SHA256Sum(int(userVerificationData.SignedBlockCount)) + if err != nil { + return fmt.Errorf("biscuit: failed to generate token hash: %w", err) + } + + if err := verifyUserSignature(signedTokenHash, userVerificationData); err != nil { + return fmt.Errorf("biscuit: failed to verify user signature: %w", err) + } + if err := verifier.withValidatedUserSignature(userVerificationData); err != nil { + return fmt.Errorf("biscuit: failed to add validated signature: %w", err) + } + + return verifier.Verify() +} diff --git a/pkg/biscuit/biscuit_test.go b/pkg/biscuit/biscuit_test.go new file mode 100644 index 0000000..ed7b273 --- /dev/null +++ b/pkg/biscuit/biscuit_test.go @@ -0,0 +1,57 @@ +package biscuit + +import ( + "context" + "crypto/rand" + "testing" + + "github.com/flynn/biscuit-go/sig" + "github.com/flynn/hubauth/pkg/kmssign" + "github.com/flynn/hubauth/pkg/kmssign/kmssim" + "github.com/stretchr/testify/require" +) + +func TestBiscuit(t *testing.T) { + rootKey := sig.GenerateKeypair(rand.Reader) + audience := "http://random.audience.url" + + kms := kmssim.NewClient([]string{audience}) + audienceKey, err := kmssign.NewKey(context.Background(), kms, audience) + require.NoError(t, err) + + userKey := generateUserKeyPair(t) + + signableBiscuit, err := GenerateSignable(rootKey, audience, audienceKey, userKey.Public) + require.NoError(t, err) + t.Logf("signable biscuit size: %d", len(signableBiscuit)) + + t.Run("happy path", func(t *testing.T) { + signedBiscuit, err := Sign(signableBiscuit, rootKey.Public(), userKey) + require.NoError(t, err) + t.Logf("signed biscuit size: %d", len(signedBiscuit)) + + err = Verify(signedBiscuit, rootKey.Public(), audience, audienceKey) + require.NoError(t, err) + }) + + t.Run("user sign with wrong key", func(t *testing.T) { + _, err := Sign(signableBiscuit, rootKey.Public(), generateUserKeyPair(t)) + require.Error(t, err) + }) + + t.Run("verify wrong audience", func(t *testing.T) { + signedBiscuit, err := Sign(signableBiscuit, rootKey.Public(), userKey) + require.NoError(t, err) + + err = Verify(signedBiscuit, rootKey.Public(), "http://another.audience.url", audienceKey) + require.Error(t, err) + + wrongAudience := "http://another.audience.url" + kms := kmssim.NewClient([]string{wrongAudience}) + wrongAudienceKey, err := kmssign.NewKey(context.Background(), kms, wrongAudience) + require.NoError(t, err) + + err = Verify(signedBiscuit, rootKey.Public(), audience, wrongAudienceKey) + require.Error(t, err) + }) +} diff --git a/pkg/biscuit/signature.go b/pkg/biscuit/signature.go new file mode 100644 index 0000000..385e4bf --- /dev/null +++ b/pkg/biscuit/signature.go @@ -0,0 +1,162 @@ +package biscuit + +import ( + "crypto" + "crypto/ecdsa" + "crypto/rand" + "crypto/sha256" + "crypto/x509" + "errors" + "time" + + "github.com/flynn/biscuit-go" + "github.com/flynn/hubauth/pkg/kmssign" +) + +var ( + ErrUnsupportedSignatureAlg = errors.New("unsupported signature algorithm") + ErrInvalidSignature = errors.New("invalid signature") +) + +type SignatureAlg biscuit.Symbol + +const ( + ECDSA_P256_SHA256 SignatureAlg = "ECDSA_P256_SHA256" +) + +type userToSignData struct { + DataID biscuit.Integer + Alg biscuit.Symbol + Data biscuit.Bytes + SignedBlockCount biscuit.Integer +} + +type userSignatureData struct { + DataID biscuit.Integer + UserPubKey biscuit.Bytes + Signature biscuit.Bytes + SignedBlockCount biscuit.Integer + Nonce biscuit.Bytes + Timestamp biscuit.Date +} + +type userVerificationData struct { + DataID biscuit.Integer + Alg biscuit.Symbol + Data biscuit.Bytes + UserPubKey biscuit.Bytes + Signature biscuit.Bytes + SignedBlockCount biscuit.Integer + Nonce biscuit.Bytes + Timestamp biscuit.Date +} + +func userSign(tokenHash []byte, userKey *UserKeyPair, toSignData *userToSignData) (*userSignatureData, error) { + if len(tokenHash) == 0 { + return nil, errors.New("invalid tokenHash") + } + + signerTimestamp := time.Now() + signerNonce := make([]byte, nonceSize) + if _, err := rand.Read(signerNonce); err != nil { + return nil, err + } + + var dataToSign []byte + dataToSign = append(dataToSign, toSignData.Data...) + dataToSign = append(dataToSign, tokenHash...) + dataToSign = append(dataToSign, signerNonce...) + dataToSign = append(dataToSign, []byte(signerTimestamp.Format(time.RFC3339))...) + dataToSign = append(dataToSign, []byte(toSignData.SignedBlockCount.String())...) + + var signedData biscuit.Bytes + switch SignatureAlg(toSignData.Alg) { + case ECDSA_P256_SHA256: + privKey, err := x509.ParseECPrivateKey(userKey.Private) + if err != nil { + return nil, err + } + hash := sha256.Sum256(dataToSign) + signedData, err = ecdsa.SignASN1(rand.Reader, privKey, hash[:]) + if err != nil { + return nil, err + } + default: + return nil, ErrUnsupportedSignatureAlg + } + + return &userSignatureData{ + DataID: toSignData.DataID, + Nonce: signerNonce, + Signature: signedData, + SignedBlockCount: toSignData.SignedBlockCount, + Timestamp: biscuit.Date(signerTimestamp), + UserPubKey: userKey.Public, + }, nil +} + +func verifyUserSignature(signedTokenHash []byte, data *userVerificationData) error { + var signedData []byte + signedData = append(signedData, data.Data...) + signedData = append(signedData, signedTokenHash...) + signedData = append(signedData, data.Nonce...) + signedData = append(signedData, []byte(time.Time(data.Timestamp).Format(time.RFC3339))...) + signedData = append(signedData, []byte(data.SignedBlockCount.String())...) + + switch SignatureAlg(data.Alg) { + case ECDSA_P256_SHA256: + pk, err := x509.ParsePKIXPublicKey(data.UserPubKey) + if err != nil { + return err + } + pubkey, ok := pk.(*ecdsa.PublicKey) + if !ok { + return errors.New("invalid pubkey, not an *ecdsa.PublicKey") + } + + hash := sha256.Sum256(signedData) + if !ecdsa.VerifyASN1(pubkey, hash[:], data.Signature) { + return ErrInvalidSignature + } + return nil + default: + return ErrUnsupportedSignatureAlg + } +} + +type audienceVerificationData struct { + Audience biscuit.Symbol + Challenge biscuit.Bytes + Signature biscuit.Bytes +} + +func audienceSign(audience string, audienceKey *kmssign.Key) (*audienceVerificationData, error) { + challenge := make([]byte, challengeSize) + if _, err := rand.Reader.Read(challenge); err != nil { + return nil, err + } + + signedData := append(signStaticCtx, challenge...) + signedData = append(signedData, []byte(audience)...) + signedHash := sha256.Sum256(signedData) + signature, err := audienceKey.Sign(rand.Reader, signedHash[:], crypto.SHA256) + if err != nil { + return nil, err + } + + return &audienceVerificationData{ + Audience: biscuit.Symbol(audience), + Challenge: challenge, + Signature: signature, + }, nil +} + +func verifyAudienceSignature(audiencePubkey *kmssign.Key, data *audienceVerificationData) error { + signedData := append(signStaticCtx, data.Challenge...) + signedData = append(signedData, []byte(data.Audience)...) + hash := sha256.Sum256(signedData) + if !audiencePubkey.Verify(hash[:], data.Signature) { + return errors.New("invalid signature") + } + return nil +} diff --git a/pkg/biscuit/signature_test.go b/pkg/biscuit/signature_test.go new file mode 100644 index 0000000..5cb5d8b --- /dev/null +++ b/pkg/biscuit/signature_test.go @@ -0,0 +1,258 @@ +package biscuit + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "testing" + "time" + + "github.com/flynn/biscuit-go" + "github.com/stretchr/testify/require" +) + +func TestUserSignVerify(t *testing.T) { + tokenHash := make([]byte, 32) + _, err := rand.Read(tokenHash) + require.NoError(t, err) + + challenge := make([]byte, challengeSize) + _, err = rand.Read(challenge) + require.NoError(t, err) + + userKey := generateUserKeyPair(t) + + toSignData := &userToSignData{ + DataID: 1, + Alg: biscuit.Symbol(ECDSA_P256_SHA256), + Data: []byte("challenge"), + SignedBlockCount: 2, + } + + signedData, err := userSign(tokenHash, userKey, toSignData) + require.NoError(t, err) + require.NotEmpty(t, signedData.Signature) + require.Equal(t, biscuit.Integer(2), signedData.SignedBlockCount) + require.Equal(t, biscuit.Integer(1), signedData.DataID) + require.Equal(t, biscuit.Bytes(userKey.Public), signedData.UserPubKey) + + require.Len(t, signedData.Nonce, nonceSize) + zeroNonce := make([]byte, nonceSize) + require.NotEqual(t, biscuit.Bytes(zeroNonce), signedData.Nonce) + + require.WithinDuration(t, time.Now(), time.Time(signedData.Timestamp), 1*time.Second) + + require.NoError(t, verifyUserSignature(tokenHash, &userVerificationData{ + DataID: toSignData.DataID, + Alg: toSignData.Alg, + Data: toSignData.Data, + Nonce: signedData.Nonce, + Signature: signedData.Signature, + SignedBlockCount: signedData.SignedBlockCount, + Timestamp: signedData.Timestamp, + UserPubKey: signedData.UserPubKey, + })) +} + +func TestUserSignFail(t *testing.T) { + validTokenHash := make([]byte, 32) + _, err := rand.Read(validTokenHash) + require.NoError(t, err) + + validChallenge := make([]byte, challengeSize) + _, err = rand.Read(validChallenge) + require.NoError(t, err) + + invalidPrivateKey := &UserKeyPair{ + Private: make([]byte, 32), + } + + testCases := []struct { + desc string + tokenHash []byte + userKey *UserKeyPair + data *userToSignData + expectedErr error + }{ + { + desc: "empty tokenHash", + tokenHash: []byte{}, + }, + { + desc: "unsupported alg", + tokenHash: validTokenHash, + data: &userToSignData{ + Alg: "unsupported", + }, + expectedErr: ErrUnsupportedSignatureAlg, + }, + { + desc: "wrong private key encoding", + tokenHash: validTokenHash, + data: &userToSignData{ + Alg: biscuit.Symbol(ECDSA_P256_SHA256), + }, + userKey: invalidPrivateKey, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.desc, func(t *testing.T) { + _, err := userSign(testCase.tokenHash, testCase.userKey, testCase.data) + require.Error(t, err) + if testCase.expectedErr != nil { + require.Equal(t, testCase.expectedErr, err) + } + }) + } +} + +func TestVerifyUserSignatureFail(t *testing.T) { + tokenHash := []byte("token hash") + toSignData := &userToSignData{ + DataID: 1, + Alg: biscuit.Symbol(ECDSA_P256_SHA256), + Data: []byte("challenge"), + SignedBlockCount: 2, + } + + userKey := generateUserKeyPair(t) + invalidKey := generateUserKeyPair(t) + + signedData, err := userSign(tokenHash, userKey, toSignData) + require.NoError(t, err) + + rsaKey, err := rsa.GenerateKey(rand.Reader, 1024) + require.NoError(t, err) + wrongKeyKind, err := x509.MarshalPKIXPublicKey(&rsaKey.PublicKey) + require.NoError(t, err) + + testCases := []struct { + desc string + tokenHash []byte + data *userVerificationData + expectedErr error + }{ + { + desc: "unsupported alg", + expectedErr: ErrUnsupportedSignatureAlg, + data: &userVerificationData{ + Alg: "unknown", + }, + }, + { + desc: "invalid pubkey encoding", + data: &userVerificationData{ + Alg: biscuit.Symbol(ECDSA_P256_SHA256), + UserPubKey: make([]byte, 32), + }, + }, + { + desc: "invalid pubkey kind", + data: &userVerificationData{ + Alg: biscuit.Symbol(ECDSA_P256_SHA256), + UserPubKey: wrongKeyKind, + }, + }, + { + desc: "wrong pubkey", + tokenHash: tokenHash, + data: &userVerificationData{ + Alg: biscuit.Symbol(ECDSA_P256_SHA256), + UserPubKey: invalidKey.Public, + Data: toSignData.Data, + DataID: toSignData.DataID, + Nonce: signedData.Nonce, + Signature: signedData.Signature, + Timestamp: signedData.Timestamp, + SignedBlockCount: signedData.SignedBlockCount, + }, + }, + { + desc: "tampered token hash", + expectedErr: ErrInvalidSignature, + tokenHash: []byte("wrong"), + data: &userVerificationData{ + Alg: biscuit.Symbol(ECDSA_P256_SHA256), + UserPubKey: userKey.Public, + Data: toSignData.Data, + DataID: toSignData.DataID, + Nonce: signedData.Nonce, + Signature: signedData.Signature, + Timestamp: signedData.Timestamp, + SignedBlockCount: signedData.SignedBlockCount, + }, + }, + { + desc: "tampered nonce", + expectedErr: ErrInvalidSignature, + tokenHash: tokenHash, + data: &userVerificationData{ + Alg: biscuit.Symbol(ECDSA_P256_SHA256), + UserPubKey: userKey.Public, + Data: toSignData.Data, + DataID: toSignData.DataID, + Nonce: []byte("another nonce"), + Signature: signedData.Signature, + SignedBlockCount: signedData.SignedBlockCount, + Timestamp: signedData.Timestamp, + }, + }, + { + desc: "tampered timestamp", + expectedErr: ErrInvalidSignature, + tokenHash: tokenHash, + data: &userVerificationData{ + Alg: biscuit.Symbol(ECDSA_P256_SHA256), + UserPubKey: userKey.Public, + Data: toSignData.Data, + DataID: toSignData.DataID, + Nonce: signedData.Nonce, + Signature: signedData.Signature, + Timestamp: biscuit.Date(time.Now().Add(1 * time.Second)), + SignedBlockCount: signedData.SignedBlockCount, + }, + }, + { + desc: "tampered signedBlockCount", + expectedErr: ErrInvalidSignature, + tokenHash: tokenHash, + data: &userVerificationData{ + Alg: biscuit.Symbol(ECDSA_P256_SHA256), + UserPubKey: userKey.Public, + Data: toSignData.Data, + DataID: toSignData.DataID, + Nonce: signedData.Nonce, + Signature: signedData.Signature, + Timestamp: signedData.Timestamp, + SignedBlockCount: signedData.SignedBlockCount + 1, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.desc, func(t *testing.T) { + + err := verifyUserSignature(testCase.tokenHash, testCase.data) + require.Error(t, err) + if testCase.expectedErr != nil { + require.Equal(t, testCase.expectedErr, err) + } + }) + } +} + +func generateUserKeyPair(t *testing.T) *UserKeyPair { + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + privBytes, err := x509.MarshalECPrivateKey(priv) + require.NoError(t, err) + pubBytes, err := x509.MarshalPKIXPublicKey(&priv.PublicKey) + require.NoError(t, err) + return &UserKeyPair{ + Private: privBytes, + Public: pubBytes, + } +} diff --git a/pkg/biscuit/wrapper.go b/pkg/biscuit/wrapper.go new file mode 100644 index 0000000..ec16ebd --- /dev/null +++ b/pkg/biscuit/wrapper.go @@ -0,0 +1,336 @@ +package biscuit + +import ( + "bytes" + "crypto/rand" + "errors" + "fmt" + + "github.com/flynn/biscuit-go" + "github.com/flynn/hubauth/pkg/kmssign" +) + +var ( + ErrAlreadySigned = errors.New("already signed") + ErrInvalidToSignDataPrefix = errors.New("invalid to_sign data prefix") +) + +var ( + signStaticCtx = []byte("biscuit-pop-v0") + challengeSize = 16 + nonceSize = 16 +) + +type hubauthBuilder struct { + biscuit.Builder +} + +// withUserToSignFact add an authority should_sign fact and associated data to the biscuit +// with an authority caveat requiring the verifier to provide a valid_signature fact. +// the verifier is responsible of ensuring that a valid signature exists over the data. +func (b *hubauthBuilder) withUserToSignFact(userPubkey []byte) error { + dataID := biscuit.Integer(0) + + if err := b.AddAuthorityFact(biscuit.Fact{Predicate: biscuit.Predicate{ + Name: "should_sign", + IDs: []biscuit.Atom{ + dataID, + biscuit.Symbol(ECDSA_P256_SHA256), + biscuit.Bytes(userPubkey), + }, + }}); err != nil { + return err + } + + challenge := make([]byte, challengeSize) + if _, err := rand.Reader.Read(challenge); err != nil { + return err + } + + if err := b.AddAuthorityFact(biscuit.Fact{Predicate: biscuit.Predicate{ + Name: "data", + IDs: []biscuit.Atom{ + dataID, + biscuit.Bytes(append(signStaticCtx, challenge...)), + }, + }}); err != nil { + return err + } + + if err := b.AddAuthorityCaveat(biscuit.Rule{ + Head: biscuit.Predicate{Name: "valid", IDs: []biscuit.Atom{biscuit.Variable(0)}}, + Body: []biscuit.Predicate{ + {Name: "should_sign", IDs: []biscuit.Atom{biscuit.SymbolAuthority, biscuit.Variable(0), biscuit.Variable(1), biscuit.Variable(2)}}, + {Name: "valid_signature", IDs: []biscuit.Atom{biscuit.Symbol("ambient"), biscuit.Variable(0), biscuit.Variable(1), biscuit.Variable(2)}}, + }, + }); err != nil { + return err + } + + return nil +} + +// withAudienceSignature add an authority audience_signature fact, containing a challenge and +// a matching signature using the audience key. +// the verifier is responsible of providing a valid_audience_signature fact, after +// verifying the signature using the audience pubkey. +func (b *hubauthBuilder) withAudienceSignature(audience string, audienceKey *kmssign.Key) error { + if len(audience) == 0 { + return errors.New("audience is required") + } + + data, err := audienceSign(audience, audienceKey) + if err != nil { + return err + } + + if err := b.AddAuthorityFact(biscuit.Fact{Predicate: biscuit.Predicate{ + Name: "audience_signature", + IDs: []biscuit.Atom{ + data.Audience, + data.Challenge, + data.Signature, + }, + }}); err != nil { + return err + } + + if err := b.AddAuthorityCaveat(biscuit.Rule{ + Head: biscuit.Predicate{Name: "valid_audience", IDs: []biscuit.Atom{biscuit.Variable(0)}}, + Body: []biscuit.Predicate{ + {Name: "audience_signature", IDs: []biscuit.Atom{biscuit.SymbolAuthority, biscuit.Variable(0), biscuit.Variable(1), biscuit.Variable(2)}}, + {Name: "valid_audience_signature", IDs: []biscuit.Atom{biscuit.Symbol("ambient"), biscuit.Variable(0), biscuit.Variable(2)}}, + }, + }); err != nil { + return err + } + + return nil +} + +type hubauthBlockBuilder struct { + biscuit.BlockBuilder +} + +func (b *hubauthBlockBuilder) withUserSignature(sigData *userSignatureData) error { + return b.AddFact(biscuit.Fact{Predicate: biscuit.Predicate{ + Name: "signature", + IDs: []biscuit.Atom{ + sigData.DataID, + sigData.UserPubKey, + sigData.Signature, + sigData.Nonce, + sigData.Timestamp, + sigData.SignedBlockCount, + }, + }}) +} + +type hubauthVerifier struct { + biscuit.Verifier +} + +func (v *hubauthVerifier) getUserToSignData(userPubKey biscuit.Bytes, signedBlockCount int) (*userToSignData, error) { + toSign, err := v.Query(biscuit.Rule{ + Head: biscuit.Predicate{ + Name: "to_sign", + IDs: []biscuit.Atom{biscuit.Variable(0), biscuit.Variable(1), biscuit.Variable(2)}, + }, + Body: []biscuit.Predicate{ + { + Name: "should_sign", IDs: []biscuit.Atom{ + biscuit.SymbolAuthority, + biscuit.Variable(0), + biscuit.Variable(1), + biscuit.Bytes(userPubKey), + }, + }, { + Name: "data", IDs: []biscuit.Atom{ + biscuit.SymbolAuthority, + biscuit.Variable(0), + biscuit.Variable(2), + }, + }, + }, + }) + if err != nil { + return nil, err + } + + if g, w := len(toSign), 1; g != w { + return nil, fmt.Errorf("invalid to_sign fact count, got %d, want %d", g, w) + } + + toSignFact := toSign[0] + if g, w := len(toSignFact.IDs), 3; g != w { + return nil, fmt.Errorf("invalid to_sign fact, got %d atoms, want %d", g, w) + } + + sigData := &userToSignData{} + var ok bool + sigData.DataID, ok = toSign[0].IDs[0].(biscuit.Integer) + if !ok { + return nil, errors.New("invalid to_sign atom: dataID") + } + sigData.Alg, ok = toSign[0].IDs[1].(biscuit.Symbol) + if !ok { + return nil, errors.New("invalid to_sign atom: alg") + } + sigData.Data, ok = toSign[0].IDs[2].(biscuit.Bytes) + if !ok { + return nil, errors.New("invalid to_sign atom: data") + } + + if !bytes.HasPrefix(sigData.Data, signStaticCtx) { + return nil, ErrInvalidToSignDataPrefix + } + + sigData.SignedBlockCount = biscuit.Integer(signedBlockCount) + + return sigData, nil +} + +func (v *hubauthVerifier) ensureNotAlreadyUserSigned(dataID biscuit.Integer, userPubKey biscuit.Bytes) error { + alreadySigned, err := v.Query(biscuit.Rule{ + Head: biscuit.Predicate{Name: "already_signed", IDs: []biscuit.Atom{biscuit.Variable(0)}}, + Body: []biscuit.Predicate{ + {Name: "signature", IDs: []biscuit.Atom{dataID, userPubKey, biscuit.Variable(0)}}, + }, + }) + if err != nil { + return err + } + if len(alreadySigned) != 0 { + return ErrAlreadySigned + } + + return nil +} + +func (v *hubauthVerifier) getUserVerificationData() (*userVerificationData, error) { + toValidate, err := v.Query(biscuit.Rule{ + Head: biscuit.Predicate{ + Name: "to_validate", + IDs: []biscuit.Atom{ + biscuit.Variable(0), // dataID + biscuit.Variable(1), // alg + biscuit.Variable(2), // pubkey + biscuit.Variable(3), // data + biscuit.Variable(4), // signature + biscuit.Variable(5), // signerNonce + biscuit.Variable(6), // signerTimestamp + biscuit.Variable(7), // signedBlockCount + }}, + Body: []biscuit.Predicate{ + {Name: "should_sign", IDs: []biscuit.Atom{biscuit.SymbolAuthority, biscuit.Variable(0), biscuit.Variable(1), biscuit.Variable(2)}}, + {Name: "data", IDs: []biscuit.Atom{biscuit.SymbolAuthority, biscuit.Variable(0), biscuit.Variable(3)}}, + {Name: "signature", IDs: []biscuit.Atom{biscuit.Variable(0), biscuit.Variable(2), biscuit.Variable(4), biscuit.Variable(5), biscuit.Variable(6), biscuit.Variable(7)}}, + }, + }) + if err != nil { + return nil, err + } + + if g, w := len(toValidate), 1; g != w { + return nil, fmt.Errorf("invalid to_validate fact count, got %d, want %d", g, w) + } + + toValidateFact := toValidate[0] + if g, w := len(toValidateFact.IDs), 8; g != w { + return nil, fmt.Errorf("invalid to_valid fact atom count, got %d, want %d", g, w) + } + + toVerify := &userVerificationData{} + var ok bool + toVerify.DataID, ok = toValidateFact.IDs[0].(biscuit.Integer) + if !ok { + return nil, errors.New("invalid to_validate atom: dataID") + } + toVerify.Alg, ok = toValidateFact.IDs[1].(biscuit.Symbol) + if !ok { + return nil, errors.New("invalid to_validate atom: alg") + } + toVerify.UserPubKey, ok = toValidateFact.IDs[2].(biscuit.Bytes) + if !ok { + return nil, errors.New("invalid to_validate atom: userPubKey") + } + toVerify.Data, ok = toValidateFact.IDs[3].(biscuit.Bytes) + if !ok { + return nil, errors.New("invalid to_validate atom: data") + } + toVerify.Signature, ok = toValidateFact.IDs[4].(biscuit.Bytes) + if !ok { + return nil, errors.New("invalid to_validate atom: signature") + } + toVerify.Nonce, ok = toValidateFact.IDs[5].(biscuit.Bytes) + if !ok { + return nil, errors.New("invalid to_validate atom: nonce") + } + toVerify.Timestamp, ok = toValidateFact.IDs[6].(biscuit.Date) + if !ok { + return nil, errors.New("invalid to_validate atom: timestamp") + } + toVerify.SignedBlockCount, ok = toValidateFact.IDs[7].(biscuit.Integer) + if !ok { + return nil, errors.New("invalid to_validate atom: signedBlockCount") + } + + return toVerify, nil +} + +func (v *hubauthVerifier) withValidatedUserSignature(data *userVerificationData) error { + v.AddFact(biscuit.Fact{Predicate: biscuit.Predicate{ + Name: "valid_signature", + IDs: []biscuit.Atom{biscuit.Symbol("ambient"), data.DataID, data.Alg, data.UserPubKey}, + }}) + + return nil +} + +func (v *hubauthVerifier) getAudienceVerificationData(audience string) (*audienceVerificationData, error) { + toValidate, err := v.Query(biscuit.Rule{ + Head: biscuit.Predicate{ + Name: "audience_to_validate", + IDs: []biscuit.Atom{ + biscuit.Variable(0), // challenge + biscuit.Variable(1), // signature + }}, + Body: []biscuit.Predicate{ + {Name: "audience_signature", IDs: []biscuit.Atom{biscuit.SymbolAuthority, biscuit.Symbol(audience), biscuit.Variable(0), biscuit.Variable(1)}}, + }, + }) + if err != nil { + return nil, err + } + + if g, w := len(toValidate), 1; g != w { + return nil, fmt.Errorf("invalid audience_to_validate fact count, got %d, want %d", g, w) + } + + toValidateFact := toValidate[0] + if g, w := len(toValidateFact.IDs), 2; g != w { + return nil, fmt.Errorf("invalid audience_to_validate fact atom count, got %d, want %d", g, w) + } + + toVerify := &audienceVerificationData{Audience: biscuit.Symbol(audience)} + var ok bool + toVerify.Challenge, ok = toValidateFact.IDs[0].(biscuit.Bytes) + if !ok { + return nil, errors.New("invalid audience_to_validate atom: challenge") + } + toVerify.Signature, ok = toValidateFact.IDs[1].(biscuit.Bytes) + if !ok { + return nil, errors.New("invalid audience_to_validate atom: signature") + } + + return toVerify, nil +} + +func (v *hubauthVerifier) withValidatedAudienceSignature(data *audienceVerificationData) error { + v.AddFact(biscuit.Fact{Predicate: biscuit.Predicate{ + Name: "valid_audience_signature", + IDs: []biscuit.Atom{biscuit.Symbol("ambient"), data.Audience, data.Signature}, + }}) + + return nil +} From 0f47b7a88853481f96cf9e7633ed6cec8c0e54f4 Mon Sep 17 00:00:00 2001 From: daeMOn Date: Thu, 8 Oct 2020 16:56:01 +0200 Subject: [PATCH 03/32] idp/token: split builder in multiple files --- pkg/idp/token/builder.go | 41 ---------------- pkg/idp/token/oauth.go | 47 +++++++++++++++++++ .../token/{builder_test.go => oauth_test.go} | 0 3 files changed, 47 insertions(+), 41 deletions(-) create mode 100644 pkg/idp/token/oauth.go rename pkg/idp/token/{builder_test.go => oauth_test.go} (100%) diff --git a/pkg/idp/token/builder.go b/pkg/idp/token/builder.go index 5a60575..15cd05e 100644 --- a/pkg/idp/token/builder.go +++ b/pkg/idp/token/builder.go @@ -2,14 +2,7 @@ package token import ( "context" - "crypto" - "fmt" "time" - - "github.com/flynn/hubauth/pkg/kmssign" - "github.com/flynn/hubauth/pkg/pb" - "github.com/flynn/hubauth/pkg/signpb" - "github.com/golang/protobuf/ptypes" ) type AccessTokenData struct { @@ -21,37 +14,3 @@ type AccessTokenData struct { type AccessTokenBuilder interface { Build(ctx context.Context, audience string, t *AccessTokenData, now time.Time, duration time.Duration) ([]byte, error) } - -type signedPbBuilder struct { - kms kmssign.KMSClient - audienceKey kmssign.AudienceKeyNamer -} - -var _ AccessTokenBuilder = (*signedPbBuilder)(nil) - -func NewSignedPBBuilder(kms kmssign.KMSClient, audienceKey kmssign.AudienceKeyNamer) AccessTokenBuilder { - return &signedPbBuilder{ - kms: kms, - audienceKey: audienceKey, - } -} - -func (b *signedPbBuilder) Build(ctx context.Context, audience string, t *AccessTokenData, now time.Time, duration time.Duration) ([]byte, error) { - signKey := kmssign.NewPrivateKey(b.kms, b.audienceKey(audience), crypto.SHA256) - - exp, _ := ptypes.TimestampProto(now.Add(duration)) - iss, _ := ptypes.TimestampProto(now) - msg := &pb.AccessToken{ - ClientId: t.ClientID, - UserId: t.UserID, - UserEmail: t.UserEmail, - IssueTime: iss, - ExpireTime: exp, - } - tokenBytes, err := signpb.SignMarshal(ctx, signKey, msg) - if err != nil { - return nil, fmt.Errorf("token: error signing access token: %w", err) - } - - return tokenBytes, nil -} diff --git a/pkg/idp/token/oauth.go b/pkg/idp/token/oauth.go new file mode 100644 index 0000000..3b96b0c --- /dev/null +++ b/pkg/idp/token/oauth.go @@ -0,0 +1,47 @@ +package token + +import ( + "context" + "crypto" + "fmt" + "time" + + "github.com/flynn/hubauth/pkg/kmssign" + "github.com/flynn/hubauth/pkg/pb" + "github.com/flynn/hubauth/pkg/signpb" + "github.com/golang/protobuf/ptypes" +) + +type signedPbBuilder struct { + kms kmssign.KMSClient + audienceKey kmssign.AudienceKeyNamer +} + +var _ AccessTokenBuilder = (*signedPbBuilder)(nil) + +func NewSignedPBBuilder(kms kmssign.KMSClient, audienceKey kmssign.AudienceKeyNamer) AccessTokenBuilder { + return &signedPbBuilder{ + kms: kms, + audienceKey: audienceKey, + } +} + +func (b *signedPbBuilder) Build(ctx context.Context, audience string, t *AccessTokenData, now time.Time, duration time.Duration) ([]byte, error) { + signKey := kmssign.NewPrivateKey(b.kms, b.audienceKey(audience), crypto.SHA256) + + exp, _ := ptypes.TimestampProto(now.Add(duration)) + iss, _ := ptypes.TimestampProto(now) + msg := &pb.AccessToken{ + ClientId: t.ClientID, + UserId: t.UserID, + UserEmail: t.UserEmail, + IssueTime: iss, + ExpireTime: exp, + } + tokenBytes, err := signpb.SignMarshal(ctx, signKey, msg) + if err != nil { + return nil, fmt.Errorf("token: error signing access token: %w", err) + } + + return tokenBytes, nil +} diff --git a/pkg/idp/token/builder_test.go b/pkg/idp/token/oauth_test.go similarity index 100% rename from pkg/idp/token/builder_test.go rename to pkg/idp/token/oauth_test.go From 9bbf14b8af19ec39a0c9cd878a18024d8ebb24e2 Mon Sep 17 00:00:00 2001 From: daeMOn Date: Mon, 12 Oct 2020 09:39:15 +0200 Subject: [PATCH 04/32] add biscuit metadata and expiration date Verifiers must now provide the current time for verifying the biscuit, and can extract user informations. User's pubkeys are now provided in http param when exchanging code. Removed block count from biscuit weakening the signature. --- go.mod | 6 +- go.sum | 4 ++ pkg/biscuit/biscuit.go | 56 ++++++++++++----- pkg/biscuit/biscuit_test.go | 20 ++++-- pkg/biscuit/signature.go | 66 ++++++++++++-------- pkg/biscuit/signature_test.go | 105 +++++++++++++------------------ pkg/biscuit/wrapper.go | 112 ++++++++++++++++++++++++++++++---- pkg/httpapi/http.go | 11 ++-- pkg/hubauth/idp.go | 18 +++--- pkg/idp/oauth.go | 47 ++++++++++---- pkg/idp/oauth_test.go | 36 ++++++----- pkg/idp/steps.go | 6 +- pkg/idp/steps_test.go | 18 +++--- pkg/idp/token/biscuit.go | 41 +++++++++++++ pkg/idp/token/builder.go | 11 ++-- pkg/idp/token/oauth.go | 7 +-- pkg/idp/token/oauth_test.go | 14 +++-- 17 files changed, 389 insertions(+), 189 deletions(-) create mode 100644 pkg/idp/token/biscuit.go diff --git a/go.mod b/go.mod index de15033..61e8549 100644 --- a/go.mod +++ b/go.mod @@ -9,14 +9,14 @@ require ( github.com/alecthomas/kong v0.2.12 github.com/aws/aws-sdk-go v1.34.6 // indirect github.com/census-instrumentation/opencensus-proto v0.3.0 // indirect - github.com/flynn/biscuit-go v0.0.0-20200907174027-193b7bdbbdca - github.com/golang/protobuf v1.4.3 + github.com/flynn/biscuit-go v0.0.0-20201009174859-e7eb59a90195 + github.com/golang/protobuf v1.4.2 github.com/googleapis/gax-go/v2 v2.0.5 github.com/jedib0t/go-pretty/v6 v6.0.5 github.com/stretchr/testify v1.6.1 go.opencensus.io v0.22.5 go.uber.org/zap v1.16.0 - golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de + golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 golang.org/x/exp/errors v0.0.0-20200513190911-00229845015e golang.org/x/net v0.0.0-20201209123823-ac852fbbde11 golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5 diff --git a/go.sum b/go.sum index 39fe673..f0f0ad5 100644 --- a/go.sum +++ b/go.sum @@ -80,6 +80,8 @@ github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5y github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/flynn/biscuit-go v0.0.0-20200907174027-193b7bdbbdca h1:LUZQQzaCT+gltxii4icyPH5oMdAP38JmbvO9aI0E4qM= github.com/flynn/biscuit-go v0.0.0-20200907174027-193b7bdbbdca/go.mod h1:EMJZ3stAYtwaP763F5HcGjPjCnYu21V2TEsg/iw88I8= +github.com/flynn/biscuit-go v0.0.0-20201009174859-e7eb59a90195 h1:TP3jMHmhjz8XxqqigEd5OQffNAO/6KPvGUYII6TFdmI= +github.com/flynn/biscuit-go v0.0.0-20201009174859-e7eb59a90195/go.mod h1:EMJZ3stAYtwaP763F5HcGjPjCnYu21V2TEsg/iw88I8= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -207,6 +209,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de h1:ikNHVSjEfnvz6sxdSPCaPt572qowuyMDMJLLm3Db3ig= golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 h1:hb9wdF1z5waM+dSIICn1l0DkLVDT3hqhhQsDNUmHPRE= +golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= diff --git a/pkg/biscuit/biscuit.go b/pkg/biscuit/biscuit.go index ef1a452..e335e60 100644 --- a/pkg/biscuit/biscuit.go +++ b/pkg/biscuit/biscuit.go @@ -3,12 +3,20 @@ package biscuit import ( "crypto/rand" "fmt" + "time" "github.com/flynn/biscuit-go" "github.com/flynn/biscuit-go/sig" "github.com/flynn/hubauth/pkg/kmssign" ) +type Metadata struct { + ClientID string + UserID string + UserEmail string + IssueTime time.Time +} + type UserKeyPair struct { Public []byte Private []byte @@ -16,7 +24,7 @@ type UserKeyPair struct { // GenerateSignable returns a biscuit which will only verify after being // signed with the private key matching the given userPubkey. -func GenerateSignable(rootKey sig.Keypair, audience string, audienceKey *kmssign.Key, userPubkey []byte) ([]byte, error) { +func GenerateSignable(rootKey sig.Keypair, audience string, audienceKey *kmssign.Key, userPublicKey []byte, expireTime time.Time, m *Metadata) ([]byte, error) { builder := &hubauthBuilder{ Builder: biscuit.NewBuilder(rand.Reader, rootKey), } @@ -25,7 +33,15 @@ func GenerateSignable(rootKey sig.Keypair, audience string, audienceKey *kmssign return nil, err } - if err := builder.withUserToSignFact(userPubkey); err != nil { + if err := builder.withUserToSignFact(userPublicKey); err != nil { + return nil, err + } + + if err := builder.withExpire(expireTime); err != nil { + return nil, err + } + + if err := builder.withMetadata(m); err != nil { return nil, err } @@ -55,7 +71,7 @@ func Sign(token []byte, rootPubKey sig.PublicKey, userKey *UserKeyPair) ([]byte, Verifier: v, } - toSignData, err := verifier.getUserToSignData(userKey.Public, b.BlockCount()) + toSignData, err := verifier.getUserToSignData(userKey.Public) if err != nil { return nil, fmt.Errorf("biscuit: failed to get to_sign data: %w", err) } @@ -92,46 +108,56 @@ func Sign(token []byte, rootPubKey sig.PublicKey, userKey *UserKeyPair) ([]byte, // Verify will verify the biscuit, the included audience and user signature, and return an error // when anything is invalid. -func Verify(token []byte, rootPubKey sig.PublicKey, audience string, audienceKey *kmssign.Key) error { +func Verify(token []byte, rootPubKey sig.PublicKey, audience string, audienceKey *kmssign.Key) (*Metadata, error) { b, err := biscuit.Unmarshal(token) if err != nil { - return fmt.Errorf("biscuit: failed to unmarshal: %w", err) + return nil, fmt.Errorf("biscuit: failed to unmarshal: %w", err) } v, err := b.Verify(rootPubKey) if err != nil { - return fmt.Errorf("biscuit: failed to verify: %w", err) + return nil, fmt.Errorf("biscuit: failed to verify: %w", err) } verifier := &hubauthVerifier{v} audienceVerificationData, err := verifier.getAudienceVerificationData(audience) if err != nil { - return fmt.Errorf("biscuit: failed to retrieve audience signature data: %w", err) + return nil, fmt.Errorf("biscuit: failed to retrieve audience signature data: %w", err) } if err := verifyAudienceSignature(audienceKey, audienceVerificationData); err != nil { - return fmt.Errorf("biscuit: failed to verify audience signature: %w", err) + return nil, fmt.Errorf("biscuit: failed to verify audience signature: %w", err) } if err := verifier.withValidatedAudienceSignature(audienceVerificationData); err != nil { - return fmt.Errorf("biscuit: failed to add validated signature: %w", err) + return nil, fmt.Errorf("biscuit: failed to add validated signature: %w", err) } userVerificationData, err := verifier.getUserVerificationData() if err != nil { - return fmt.Errorf("biscuit: failed to retrieve user signature data: %w", err) + return nil, fmt.Errorf("biscuit: failed to retrieve user signature data: %w", err) } - signedTokenHash, err := b.SHA256Sum(int(userVerificationData.SignedBlockCount)) + // TODO: improve biscuit API to allow retrieve the block index the signature is at + // so that we can still append other blocks if needed. Right now the signature MUST BE the last block. + signedTokenHash, err := b.SHA256Sum(b.BlockCount() - 1) if err != nil { - return fmt.Errorf("biscuit: failed to generate token hash: %w", err) + return nil, fmt.Errorf("biscuit: failed to generate token hash: %w", err) } if err := verifyUserSignature(signedTokenHash, userVerificationData); err != nil { - return fmt.Errorf("biscuit: failed to verify user signature: %w", err) + return nil, fmt.Errorf("biscuit: failed to verify user signature: %w", err) } if err := verifier.withValidatedUserSignature(userVerificationData); err != nil { - return fmt.Errorf("biscuit: failed to add validated signature: %w", err) + return nil, fmt.Errorf("biscuit: failed to add validated signature: %w", err) + } + + if err := verifier.withCurrentTime(time.Now()); err != nil { + return nil, fmt.Errorf("biscuit: failed to add current time: %w", err) + } + + if err := verifier.Verify(); err != nil { + return nil, fmt.Errorf("biscuit: failed to verify: %w", err) } - return verifier.Verify() + return verifier.getMetadata() } diff --git a/pkg/biscuit/biscuit_test.go b/pkg/biscuit/biscuit_test.go index ed7b273..bf6199b 100644 --- a/pkg/biscuit/biscuit_test.go +++ b/pkg/biscuit/biscuit_test.go @@ -4,6 +4,7 @@ import ( "context" "crypto/rand" "testing" + "time" "github.com/flynn/biscuit-go/sig" "github.com/flynn/hubauth/pkg/kmssign" @@ -20,8 +21,13 @@ func TestBiscuit(t *testing.T) { require.NoError(t, err) userKey := generateUserKeyPair(t) - - signableBiscuit, err := GenerateSignable(rootKey, audience, audienceKey, userKey.Public) + metas := &Metadata{ + ClientID: "abcd", + UserEmail: "1234@example.com", + UserID: "1234", + IssueTime: time.Now(), + } + signableBiscuit, err := GenerateSignable(rootKey, audience, audienceKey, userKey.Public, time.Now().Add(5*time.Minute), metas) require.NoError(t, err) t.Logf("signable biscuit size: %d", len(signableBiscuit)) @@ -30,8 +36,12 @@ func TestBiscuit(t *testing.T) { require.NoError(t, err) t.Logf("signed biscuit size: %d", len(signedBiscuit)) - err = Verify(signedBiscuit, rootKey.Public(), audience, audienceKey) + res, err := Verify(signedBiscuit, rootKey.Public(), audience, audienceKey) require.NoError(t, err) + require.Equal(t, metas.ClientID, res.ClientID) + require.Equal(t, metas.UserID, res.UserID) + require.Equal(t, metas.UserEmail, res.UserEmail) + require.WithinDuration(t, metas.IssueTime, res.IssueTime, 1*time.Second) }) t.Run("user sign with wrong key", func(t *testing.T) { @@ -43,7 +53,7 @@ func TestBiscuit(t *testing.T) { signedBiscuit, err := Sign(signableBiscuit, rootKey.Public(), userKey) require.NoError(t, err) - err = Verify(signedBiscuit, rootKey.Public(), "http://another.audience.url", audienceKey) + _, err = Verify(signedBiscuit, rootKey.Public(), "http://another.audience.url", audienceKey) require.Error(t, err) wrongAudience := "http://another.audience.url" @@ -51,7 +61,7 @@ func TestBiscuit(t *testing.T) { wrongAudienceKey, err := kmssign.NewKey(context.Background(), kms, wrongAudience) require.NoError(t, err) - err = Verify(signedBiscuit, rootKey.Public(), audience, wrongAudienceKey) + _, err = Verify(signedBiscuit, rootKey.Public(), audience, wrongAudienceKey) require.Error(t, err) }) } diff --git a/pkg/biscuit/signature.go b/pkg/biscuit/signature.go index 385e4bf..078eaa6 100644 --- a/pkg/biscuit/signature.go +++ b/pkg/biscuit/signature.go @@ -3,10 +3,12 @@ package biscuit import ( "crypto" "crypto/ecdsa" + "crypto/elliptic" "crypto/rand" "crypto/sha256" "crypto/x509" "errors" + "fmt" "time" "github.com/flynn/biscuit-go" @@ -25,30 +27,27 @@ const ( ) type userToSignData struct { - DataID biscuit.Integer - Alg biscuit.Symbol - Data biscuit.Bytes - SignedBlockCount biscuit.Integer + DataID biscuit.Integer + Alg biscuit.Symbol + Data biscuit.Bytes } type userSignatureData struct { - DataID biscuit.Integer - UserPubKey biscuit.Bytes - Signature biscuit.Bytes - SignedBlockCount biscuit.Integer - Nonce biscuit.Bytes - Timestamp biscuit.Date + DataID biscuit.Integer + UserPubKey biscuit.Bytes + Signature biscuit.Bytes + Nonce biscuit.Bytes + Timestamp biscuit.Date } type userVerificationData struct { - DataID biscuit.Integer - Alg biscuit.Symbol - Data biscuit.Bytes - UserPubKey biscuit.Bytes - Signature biscuit.Bytes - SignedBlockCount biscuit.Integer - Nonce biscuit.Bytes - Timestamp biscuit.Date + DataID biscuit.Integer + Alg biscuit.Symbol + Data biscuit.Bytes + UserPubKey biscuit.Bytes + Signature biscuit.Bytes + Nonce biscuit.Bytes + Timestamp biscuit.Date } func userSign(tokenHash []byte, userKey *UserKeyPair, toSignData *userToSignData) (*userSignatureData, error) { @@ -67,7 +66,6 @@ func userSign(tokenHash []byte, userKey *UserKeyPair, toSignData *userToSignData dataToSign = append(dataToSign, tokenHash...) dataToSign = append(dataToSign, signerNonce...) dataToSign = append(dataToSign, []byte(signerTimestamp.Format(time.RFC3339))...) - dataToSign = append(dataToSign, []byte(toSignData.SignedBlockCount.String())...) var signedData biscuit.Bytes switch SignatureAlg(toSignData.Alg) { @@ -86,12 +84,11 @@ func userSign(tokenHash []byte, userKey *UserKeyPair, toSignData *userToSignData } return &userSignatureData{ - DataID: toSignData.DataID, - Nonce: signerNonce, - Signature: signedData, - SignedBlockCount: toSignData.SignedBlockCount, - Timestamp: biscuit.Date(signerTimestamp), - UserPubKey: userKey.Public, + DataID: toSignData.DataID, + Nonce: signerNonce, + Signature: signedData, + Timestamp: biscuit.Date(signerTimestamp), + UserPubKey: userKey.Public, }, nil } @@ -101,7 +98,6 @@ func verifyUserSignature(signedTokenHash []byte, data *userVerificationData) err signedData = append(signedData, signedTokenHash...) signedData = append(signedData, data.Nonce...) signedData = append(signedData, []byte(time.Time(data.Timestamp).Format(time.RFC3339))...) - signedData = append(signedData, []byte(data.SignedBlockCount.String())...) switch SignatureAlg(data.Alg) { case ECDSA_P256_SHA256: @@ -160,3 +156,21 @@ func verifyAudienceSignature(audiencePubkey *kmssign.Key, data *audienceVerifica } return nil } + +func validatePKIXP256PublicKey(pubkey []byte) error { + key, err := x509.ParsePKIXPublicKey(pubkey) + if err != nil { + return fmt.Errorf("failed to parse PKIX, ASN.1 DER public key: %v", err) + } + + ecKey, ok := key.(*ecdsa.PublicKey) + if !ok { + return errors.New("public key is not an *ecdsa.PublicKey") + } + + if ecKey.Curve != elliptic.P256() { + return fmt.Errorf("publickey is on wrong curve, expected P256") + } + + return nil +} diff --git a/pkg/biscuit/signature_test.go b/pkg/biscuit/signature_test.go index 5cb5d8b..d4d4cc2 100644 --- a/pkg/biscuit/signature_test.go +++ b/pkg/biscuit/signature_test.go @@ -25,16 +25,14 @@ func TestUserSignVerify(t *testing.T) { userKey := generateUserKeyPair(t) toSignData := &userToSignData{ - DataID: 1, - Alg: biscuit.Symbol(ECDSA_P256_SHA256), - Data: []byte("challenge"), - SignedBlockCount: 2, + DataID: 1, + Alg: biscuit.Symbol(ECDSA_P256_SHA256), + Data: []byte("challenge"), } signedData, err := userSign(tokenHash, userKey, toSignData) require.NoError(t, err) require.NotEmpty(t, signedData.Signature) - require.Equal(t, biscuit.Integer(2), signedData.SignedBlockCount) require.Equal(t, biscuit.Integer(1), signedData.DataID) require.Equal(t, biscuit.Bytes(userKey.Public), signedData.UserPubKey) @@ -45,14 +43,13 @@ func TestUserSignVerify(t *testing.T) { require.WithinDuration(t, time.Now(), time.Time(signedData.Timestamp), 1*time.Second) require.NoError(t, verifyUserSignature(tokenHash, &userVerificationData{ - DataID: toSignData.DataID, - Alg: toSignData.Alg, - Data: toSignData.Data, - Nonce: signedData.Nonce, - Signature: signedData.Signature, - SignedBlockCount: signedData.SignedBlockCount, - Timestamp: signedData.Timestamp, - UserPubKey: signedData.UserPubKey, + DataID: toSignData.DataID, + Alg: toSignData.Alg, + Data: toSignData.Data, + Nonce: signedData.Nonce, + Signature: signedData.Signature, + Timestamp: signedData.Timestamp, + UserPubKey: signedData.UserPubKey, })) } @@ -112,10 +109,9 @@ func TestUserSignFail(t *testing.T) { func TestVerifyUserSignatureFail(t *testing.T) { tokenHash := []byte("token hash") toSignData := &userToSignData{ - DataID: 1, - Alg: biscuit.Symbol(ECDSA_P256_SHA256), - Data: []byte("challenge"), - SignedBlockCount: 2, + DataID: 1, + Alg: biscuit.Symbol(ECDSA_P256_SHA256), + Data: []byte("challenge"), } userKey := generateUserKeyPair(t) @@ -160,14 +156,13 @@ func TestVerifyUserSignatureFail(t *testing.T) { desc: "wrong pubkey", tokenHash: tokenHash, data: &userVerificationData{ - Alg: biscuit.Symbol(ECDSA_P256_SHA256), - UserPubKey: invalidKey.Public, - Data: toSignData.Data, - DataID: toSignData.DataID, - Nonce: signedData.Nonce, - Signature: signedData.Signature, - Timestamp: signedData.Timestamp, - SignedBlockCount: signedData.SignedBlockCount, + Alg: biscuit.Symbol(ECDSA_P256_SHA256), + UserPubKey: invalidKey.Public, + Data: toSignData.Data, + DataID: toSignData.DataID, + Nonce: signedData.Nonce, + Signature: signedData.Signature, + Timestamp: signedData.Timestamp, }, }, { @@ -175,14 +170,13 @@ func TestVerifyUserSignatureFail(t *testing.T) { expectedErr: ErrInvalidSignature, tokenHash: []byte("wrong"), data: &userVerificationData{ - Alg: biscuit.Symbol(ECDSA_P256_SHA256), - UserPubKey: userKey.Public, - Data: toSignData.Data, - DataID: toSignData.DataID, - Nonce: signedData.Nonce, - Signature: signedData.Signature, - Timestamp: signedData.Timestamp, - SignedBlockCount: signedData.SignedBlockCount, + Alg: biscuit.Symbol(ECDSA_P256_SHA256), + UserPubKey: userKey.Public, + Data: toSignData.Data, + DataID: toSignData.DataID, + Nonce: signedData.Nonce, + Signature: signedData.Signature, + Timestamp: signedData.Timestamp, }, }, { @@ -190,14 +184,13 @@ func TestVerifyUserSignatureFail(t *testing.T) { expectedErr: ErrInvalidSignature, tokenHash: tokenHash, data: &userVerificationData{ - Alg: biscuit.Symbol(ECDSA_P256_SHA256), - UserPubKey: userKey.Public, - Data: toSignData.Data, - DataID: toSignData.DataID, - Nonce: []byte("another nonce"), - Signature: signedData.Signature, - SignedBlockCount: signedData.SignedBlockCount, - Timestamp: signedData.Timestamp, + Alg: biscuit.Symbol(ECDSA_P256_SHA256), + UserPubKey: userKey.Public, + Data: toSignData.Data, + DataID: toSignData.DataID, + Nonce: []byte("another nonce"), + Signature: signedData.Signature, + Timestamp: signedData.Timestamp, }, }, { @@ -205,29 +198,13 @@ func TestVerifyUserSignatureFail(t *testing.T) { expectedErr: ErrInvalidSignature, tokenHash: tokenHash, data: &userVerificationData{ - Alg: biscuit.Symbol(ECDSA_P256_SHA256), - UserPubKey: userKey.Public, - Data: toSignData.Data, - DataID: toSignData.DataID, - Nonce: signedData.Nonce, - Signature: signedData.Signature, - Timestamp: biscuit.Date(time.Now().Add(1 * time.Second)), - SignedBlockCount: signedData.SignedBlockCount, - }, - }, - { - desc: "tampered signedBlockCount", - expectedErr: ErrInvalidSignature, - tokenHash: tokenHash, - data: &userVerificationData{ - Alg: biscuit.Symbol(ECDSA_P256_SHA256), - UserPubKey: userKey.Public, - Data: toSignData.Data, - DataID: toSignData.DataID, - Nonce: signedData.Nonce, - Signature: signedData.Signature, - Timestamp: signedData.Timestamp, - SignedBlockCount: signedData.SignedBlockCount + 1, + Alg: biscuit.Symbol(ECDSA_P256_SHA256), + UserPubKey: userKey.Public, + Data: toSignData.Data, + DataID: toSignData.DataID, + Nonce: signedData.Nonce, + Signature: signedData.Signature, + Timestamp: biscuit.Date(time.Now().Add(1 * time.Second)), }, }, } diff --git a/pkg/biscuit/wrapper.go b/pkg/biscuit/wrapper.go index ec16ebd..f39681f 100644 --- a/pkg/biscuit/wrapper.go +++ b/pkg/biscuit/wrapper.go @@ -5,8 +5,10 @@ import ( "crypto/rand" "errors" "fmt" + "time" "github.com/flynn/biscuit-go" + "github.com/flynn/biscuit-go/datalog" "github.com/flynn/hubauth/pkg/kmssign" ) @@ -31,6 +33,10 @@ type hubauthBuilder struct { func (b *hubauthBuilder) withUserToSignFact(userPubkey []byte) error { dataID := biscuit.Integer(0) + if err := validatePKIXP256PublicKey(userPubkey); err != nil { + return err + } + if err := b.AddAuthorityFact(biscuit.Fact{Predicate: biscuit.Predicate{ Name: "should_sign", IDs: []biscuit.Atom{ @@ -108,6 +114,38 @@ func (b *hubauthBuilder) withAudienceSignature(audience string, audienceKey *kms return nil } +func (b *hubauthBuilder) withMetadata(m *Metadata) error { + return b.AddAuthorityFact(biscuit.Fact{Predicate: biscuit.Predicate{ + Name: "metadata", + IDs: []biscuit.Atom{ + biscuit.String(m.ClientID), + biscuit.String(m.UserID), + biscuit.String(m.UserEmail), + biscuit.Date(m.IssueTime), + }, + }}) +} + +func (b *hubauthBuilder) withExpire(exp time.Time) error { + if err := b.AddAuthorityCaveat(biscuit.Rule{ + Head: biscuit.Predicate{Name: "not_expired", IDs: []biscuit.Atom{biscuit.Variable(0)}}, + Body: []biscuit.Predicate{ + {Name: "current_time", IDs: []biscuit.Atom{biscuit.Symbol("ambient"), biscuit.Variable(0)}}, + }, + Constraints: []biscuit.Constraint{{ + Name: biscuit.Variable(0), + Checker: biscuit.DateComparisonChecker{ + Comparison: datalog.DateComparisonBefore, + Date: biscuit.Date(exp), + }, + }}, + }); err != nil { + return err + } + + return nil +} + type hubauthBlockBuilder struct { biscuit.BlockBuilder } @@ -121,7 +159,6 @@ func (b *hubauthBlockBuilder) withUserSignature(sigData *userSignatureData) erro sigData.Signature, sigData.Nonce, sigData.Timestamp, - sigData.SignedBlockCount, }, }}) } @@ -130,7 +167,7 @@ type hubauthVerifier struct { biscuit.Verifier } -func (v *hubauthVerifier) getUserToSignData(userPubKey biscuit.Bytes, signedBlockCount int) (*userToSignData, error) { +func (v *hubauthVerifier) getUserToSignData(userPubKey biscuit.Bytes) (*userToSignData, error) { toSign, err := v.Query(biscuit.Rule{ Head: biscuit.Predicate{ Name: "to_sign", @@ -185,8 +222,6 @@ func (v *hubauthVerifier) getUserToSignData(userPubKey biscuit.Bytes, signedBloc return nil, ErrInvalidToSignDataPrefix } - sigData.SignedBlockCount = biscuit.Integer(signedBlockCount) - return sigData, nil } @@ -219,12 +254,11 @@ func (v *hubauthVerifier) getUserVerificationData() (*userVerificationData, erro biscuit.Variable(4), // signature biscuit.Variable(5), // signerNonce biscuit.Variable(6), // signerTimestamp - biscuit.Variable(7), // signedBlockCount }}, Body: []biscuit.Predicate{ {Name: "should_sign", IDs: []biscuit.Atom{biscuit.SymbolAuthority, biscuit.Variable(0), biscuit.Variable(1), biscuit.Variable(2)}}, {Name: "data", IDs: []biscuit.Atom{biscuit.SymbolAuthority, biscuit.Variable(0), biscuit.Variable(3)}}, - {Name: "signature", IDs: []biscuit.Atom{biscuit.Variable(0), biscuit.Variable(2), biscuit.Variable(4), biscuit.Variable(5), biscuit.Variable(6), biscuit.Variable(7)}}, + {Name: "signature", IDs: []biscuit.Atom{biscuit.Variable(0), biscuit.Variable(2), biscuit.Variable(4), biscuit.Variable(5), biscuit.Variable(6)}}, }, }) if err != nil { @@ -236,7 +270,7 @@ func (v *hubauthVerifier) getUserVerificationData() (*userVerificationData, erro } toValidateFact := toValidate[0] - if g, w := len(toValidateFact.IDs), 8; g != w { + if g, w := len(toValidateFact.IDs), 7; g != w { return nil, fmt.Errorf("invalid to_valid fact atom count, got %d, want %d", g, w) } @@ -270,10 +304,6 @@ func (v *hubauthVerifier) getUserVerificationData() (*userVerificationData, erro if !ok { return nil, errors.New("invalid to_validate atom: timestamp") } - toVerify.SignedBlockCount, ok = toValidateFact.IDs[7].(biscuit.Integer) - if !ok { - return nil, errors.New("invalid to_validate atom: signedBlockCount") - } return toVerify, nil } @@ -326,6 +356,54 @@ func (v *hubauthVerifier) getAudienceVerificationData(audience string) (*audienc return toVerify, nil } +func (v *hubauthVerifier) getMetadata() (*Metadata, error) { + metaFacts, err := v.Query(biscuit.Rule{ + Head: biscuit.Predicate{ + Name: "metadata", + IDs: []biscuit.Atom{ + biscuit.Variable(0), // clientID + biscuit.Variable(1), // userID + biscuit.Variable(2), // userEmail + biscuit.Variable(3), // issueTime + }}, + Body: []biscuit.Predicate{ + {Name: "metadata", IDs: []biscuit.Atom{biscuit.SymbolAuthority, biscuit.Variable(0), biscuit.Variable(1), biscuit.Variable(2), biscuit.Variable(3)}}, + }, + }) + if err != nil { + return nil, err + } + + if g, w := len(metaFacts), 1; g != w { + return nil, fmt.Errorf("invalid metadata fact count, got %d, want %d", g, w) + } + + metaFact := metaFacts[0] + + clientID, ok := metaFact.IDs[0].(biscuit.String) + if !ok { + return nil, errors.New("invalid metadata atom: clientID") + } + userID, ok := metaFact.IDs[1].(biscuit.String) + if !ok { + return nil, errors.New("invalid metadata atom: userID") + } + userEmail, ok := metaFact.IDs[2].(biscuit.String) + if !ok { + return nil, errors.New("invalid metadata atom: userEmail") + } + issueTime, ok := metaFact.IDs[3].(biscuit.Date) + if !ok { + return nil, errors.New("invalid metadata atom: issueTime") + } + return &Metadata{ + ClientID: string(clientID), + UserID: string(userID), + UserEmail: string(userEmail), + IssueTime: time.Time(issueTime), + }, nil +} + func (v *hubauthVerifier) withValidatedAudienceSignature(data *audienceVerificationData) error { v.AddFact(biscuit.Fact{Predicate: biscuit.Predicate{ Name: "valid_audience_signature", @@ -334,3 +412,15 @@ func (v *hubauthVerifier) withValidatedAudienceSignature(data *audienceVerificat return nil } + +func (v *hubauthVerifier) withCurrentTime(t time.Time) error { + v.AddFact(biscuit.Fact{Predicate: biscuit.Predicate{ + Name: "current_time", + IDs: []biscuit.Atom{ + biscuit.Symbol("ambient"), + biscuit.Date(t), + }, + }}) + + return nil +} diff --git a/pkg/httpapi/http.go b/pkg/httpapi/http.go index 58da911..641212d 100644 --- a/pkg/httpapi/http.go +++ b/pkg/httpapi/http.go @@ -329,11 +329,12 @@ func (a *api) Token(w http.ResponseWriter, req *http.Request) { switch req.Form.Get("grant_type") { case "authorization_code": res, err = a.IdP.ExchangeCode(req.Context(), &hubauth.ExchangeCodeRequest{ - ClientID: req.PostForm.Get("client_id"), - Audience: aud, - RedirectURI: req.PostForm.Get("redirect_uri"), - Code: req.PostForm.Get("code"), - CodeVerifier: req.PostForm.Get("code_verifier"), + ClientID: req.PostForm.Get("client_id"), + Audience: aud, + RedirectURI: req.PostForm.Get("redirect_uri"), + Code: req.PostForm.Get("code"), + CodeVerifier: req.PostForm.Get("code_verifier"), + UserPublicKey: req.PostForm.Get("user_public_key"), }) case "refresh_token": res, err = a.IdP.RefreshToken(req.Context(), &hubauth.RefreshTokenRequest{ diff --git a/pkg/hubauth/idp.go b/pkg/hubauth/idp.go index 66d4722..7b4d132 100644 --- a/pkg/hubauth/idp.go +++ b/pkg/hubauth/idp.go @@ -38,11 +38,12 @@ type AuthorizeResponse struct { } type ExchangeCodeRequest struct { - ClientID string - RedirectURI string - Audience string - Code string - CodeVerifier string + ClientID string + RedirectURI string + Audience string + Code string + CodeVerifier string + UserPublicKey string } type AccessToken struct { @@ -61,9 +62,10 @@ type AccessToken struct { } type RefreshTokenRequest struct { - ClientID string - Audience string - RefreshToken string + ClientID string + Audience string + RefreshToken string + UserPublicKey string } type ListAudiencesRequest struct { diff --git a/pkg/idp/oauth.go b/pkg/idp/oauth.go index 1dccd8f..1e21b9a 100644 --- a/pkg/idp/oauth.go +++ b/pkg/idp/oauth.go @@ -47,7 +47,7 @@ type idpSteps interface { SignRefreshToken(ctx context.Context, signKey signpb.PrivateKey, t *signedRefreshTokenData) (string, error) RenewRefreshToken(ctx context.Context, clientID, oldTokenID string, oldTokenIssueTime, now time.Time) (*hubauth.RefreshToken, error) VerifyRefreshToken(ctx context.Context, rt *hubauth.RefreshToken, now time.Time) error - SignAccessToken(ctx context.Context, audience string, t *token.AccessTokenData, now time.Time) (string, error) + BuildAccessToken(ctx context.Context, audience string, t *token.AccessTokenData) (string, error) } type idpService struct { @@ -309,11 +309,23 @@ func (s *idpService) ExchangeCode(parentCtx context.Context, req *hubauth.Exchan return nil } - accessToken, err = s.steps.SignAccessToken(ctx, req.Audience, &token.AccessTokenData{ - ClientID: req.ClientID, - UserID: codeInfo.UserId, - UserEmail: codeInfo.UserEmail, - }, now) + var userPublicKey []byte + if len(req.UserPublicKey) > 0 { + var err error + userPublicKey, err = base64Decode(req.UserPublicKey) + if err != nil { + return fmt.Errorf("idp: invalid public key: %v", err) + } + } + + accessToken, err = s.steps.BuildAccessToken(ctx, req.Audience, &token.AccessTokenData{ + ClientID: req.ClientID, + UserID: codeInfo.UserId, + UserEmail: codeInfo.UserEmail, + UserPublicKey: userPublicKey, + IssueTime: now, + ExpireTime: now.Add(accessTokenDuration), + }) return err }) @@ -382,11 +394,24 @@ func (s *idpService) RefreshToken(ctx context.Context, req *hubauth.RefreshToken if req.Audience == "" { return nil } - accessToken, err = s.steps.SignAccessToken(ctx, req.Audience, &token.AccessTokenData{ - ClientID: req.ClientID, - UserID: oldToken.UserID, - UserEmail: oldToken.UserEmail, - }, now) + + var userPublicKey []byte + if len(req.UserPublicKey) > 0 { + var err error + userPublicKey, err = base64Decode(req.UserPublicKey) + if err != nil { + return fmt.Errorf("idp: invalid public key: %v", err) + } + } + + accessToken, err = s.steps.BuildAccessToken(ctx, req.Audience, &token.AccessTokenData{ + ClientID: req.ClientID, + UserID: oldToken.UserID, + UserEmail: oldToken.UserEmail, + UserPublicKey: userPublicKey, + IssueTime: now, + ExpireTime: now.Add(accessTokenDuration), + }) return err }) diff --git a/pkg/idp/oauth_test.go b/pkg/idp/oauth_test.go index 3f2e62c..870951d 100644 --- a/pkg/idp/oauth_test.go +++ b/pkg/idp/oauth_test.go @@ -78,8 +78,8 @@ func (m *mockSteps) SignRefreshToken(ctx context.Context, signKey signpb.Private args := m.Called(ctx, signKey, t) return args.String(0), args.Error(1) } -func (m *mockSteps) SignAccessToken(ctx context.Context, audience string, t *token.AccessTokenData, now time.Time) (string, error) { - args := m.Called(ctx, audience, t, now) +func (m *mockSteps) BuildAccessToken(ctx context.Context, audience string, t *token.AccessTokenData) (string, error) { + args := m.Called(ctx, audience, t) return args.String(0), args.Error(1) } func (m *mockSteps) RenewRefreshToken(ctx context.Context, clientID, oldTokenID string, oldTokenIssueTime, now time.Time) (*hubauth.RefreshToken, error) { @@ -665,11 +665,13 @@ func TestExchangeCode(t *testing.T) { }).Return(verifiedCode, nil) idpService.steps.(*mockSteps).On("SaveRefreshToken", mock.Anything, b64CodeID, redirectURI, rtData).Return(client, nil) idpService.steps.(*mockSteps).On("SignRefreshToken", mock.Anything, idpService.refreshKey, signedRTData).Return(refreshToken, nil) - idpService.steps.(*mockSteps).On("SignAccessToken", mock.Anything, audienceURL, &token.AccessTokenData{ - ClientID: clientID, - UserID: userID, - UserEmail: userEmail, - }, now).Return(accessToken, nil) + idpService.steps.(*mockSteps).On("BuildAccessToken", mock.Anything, audienceURL, &token.AccessTokenData{ + ClientID: clientID, + UserID: userID, + UserEmail: userEmail, + IssueTime: now, + ExpireTime: now.Add(accessTokenDuration), + }).Return(accessToken, nil) req := &hubauth.ExchangeCodeRequest{ ClientID: clientID, @@ -783,7 +785,7 @@ func TestExchangeCodeErrors(t *testing.T) { ExpectedErr: expectedErr, }, { - Desc: "SignAccessToken error", + Desc: "BuildAccessToken error", Code: base64Encode(validCode), SignATErr: expectedErr, ExpectedErr: expectedErr, @@ -800,7 +802,7 @@ func TestExchangeCodeErrors(t *testing.T) { idpService.steps.(*mockSteps).On("VerifyAudience", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(testCase.VerifyAudienceErr) idpService.steps.(*mockSteps).On("SaveRefreshToken", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&hubauth.Client{}, testCase.SaveErr) idpService.steps.(*mockSteps).On("SignRefreshToken", mock.Anything, mock.Anything, mock.Anything).Return("", testCase.SignRTErr) - idpService.steps.(*mockSteps).On("SignAccessToken", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("", testCase.SignATErr) + idpService.steps.(*mockSteps).On("BuildAccessToken", mock.Anything, mock.Anything, mock.Anything).Return("", testCase.SignATErr) req := &hubauth.ExchangeCodeRequest{ Code: testCase.Code, @@ -894,11 +896,13 @@ func TestRefreshToken(t *testing.T) { }, ExpiryTime: expireTimeProto.AsTime(), }).Return(newRefreshTokenStr, nil) - idpService.steps.(*mockSteps).On("SignAccessToken", mock.Anything, testCase.AudienceURL, &token.AccessTokenData{ - ClientID: b64ClientID, - UserID: userID, - UserEmail: userEmail, - }, now).Return(newAccessTokenStr, nil) + idpService.steps.(*mockSteps).On("BuildAccessToken", mock.Anything, testCase.AudienceURL, &token.AccessTokenData{ + ClientID: b64ClientID, + UserID: userID, + UserEmail: userEmail, + IssueTime: now, + ExpireTime: now.Add(accessTokenDuration), + }).Return(newAccessTokenStr, nil) oldTokenSigned, err := signpb.SignMarshal(context.Background(), idpService.refreshKey, &pb.RefreshToken{ Key: oldTokenID, @@ -974,7 +978,7 @@ func TestRefreshTokenStepErrors(t *testing.T) { ExpectedErr: expectedErr, }, { - Desc: "SignAccessToken error", + Desc: "BuildAccessToken error", SignATErr: expectedErr, ExpectedErr: expectedErr, }, @@ -1001,7 +1005,7 @@ func TestRefreshTokenStepErrors(t *testing.T) { idpService.steps.(*mockSteps).On("VerifyAudience", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(testCase.VerifyAudienceErr) idpService.steps.(*mockSteps).On("RenewRefreshToken", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&hubauth.RefreshToken{}, testCase.RenewRTErr) idpService.steps.(*mockSteps).On("SignRefreshToken", mock.Anything, mock.Anything, mock.Anything).Return("", testCase.SignRTErr) - idpService.steps.(*mockSteps).On("SignAccessToken", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("", testCase.SignATErr) + idpService.steps.(*mockSteps).On("BuildAccessToken", mock.Anything, mock.Anything, mock.Anything).Return("", testCase.SignATErr) _, err = idpService.RefreshToken(context.Background(), req) require.Equal(t, testCase.ExpectedErr, err) diff --git a/pkg/idp/steps.go b/pkg/idp/steps.go index 96769c9..14eb144 100644 --- a/pkg/idp/steps.go +++ b/pkg/idp/steps.go @@ -351,8 +351,8 @@ func (s *steps) VerifyRefreshToken(ctx context.Context, rt *hubauth.RefreshToken return nil } -func (s *steps) SignAccessToken(ctx context.Context, audience string, t *token.AccessTokenData, now time.Time) (token string, err error) { - ctx, span := trace.StartSpan(ctx, "idp.SignAccessToken") +func (s *steps) BuildAccessToken(ctx context.Context, audience string, t *token.AccessTokenData) (token string, err error) { + ctx, span := trace.StartSpan(ctx, "idp.BuildAccessToken") span.AddAttributes( trace.StringAttribute("client_id", t.ClientID), trace.StringAttribute("user_id", t.UserID), @@ -360,7 +360,7 @@ func (s *steps) SignAccessToken(ctx context.Context, audience string, t *token.A ) defer span.End() - tokenBytes, err := s.builder.Build(ctx, audience, t, now, accessTokenDuration) + tokenBytes, err := s.builder.Build(ctx, audience, t) if err != nil { return "", fmt.Errorf("idp: error building access token: %w", err) } diff --git a/pkg/idp/steps_test.go b/pkg/idp/steps_test.go index 971b0a9..903d7b0 100644 --- a/pkg/idp/steps_test.go +++ b/pkg/idp/steps_test.go @@ -33,8 +33,8 @@ type mockAccessTokenBuilder struct { var _ token.AccessTokenBuilder = (*mockAccessTokenBuilder)(nil) -func (m *mockAccessTokenBuilder) Build(ctx context.Context, audience string, t *token.AccessTokenData, now time.Time, duration time.Duration) ([]byte, error) { - args := m.Called(ctx, audience, t, now, duration) +func (m *mockAccessTokenBuilder) Build(ctx context.Context, audience string, t *token.AccessTokenData) ([]byte, error) { + args := m.Called(ctx, audience, t) return args.Get(0).([]byte), args.Error(1) } @@ -769,21 +769,23 @@ func TestVerifyRefreshTokenErrors(t *testing.T) { } } -func TestSignAccessToken(t *testing.T) { +func TestBuildAccessToken(t *testing.T) { s := newTestSteps(t) now := time.Now() data := &token.AccessTokenData{ - ClientID: "clientID", - UserID: "userID", - UserEmail: "userEmail", + ClientID: "clientID", + UserID: "userID", + UserEmail: "userEmail", + IssueTime: now, + ExpireTime: now.Add(accessTokenDuration), } expectedAccessToken := []byte("expected-access-token") - s.builder.(*mockAccessTokenBuilder).On("Build", mock.Anything, testAudienceName, data, now, accessTokenDuration).Return(expectedAccessToken, nil) + s.builder.(*mockAccessTokenBuilder).On("Build", mock.Anything, testAudienceName, data).Return(expectedAccessToken, nil) - accessToken, err := s.SignAccessToken(context.Background(), testAudienceName, data, now) + accessToken, err := s.BuildAccessToken(context.Background(), testAudienceName, data) require.NoError(t, err) require.NotEmpty(t, accessToken) diff --git a/pkg/idp/token/biscuit.go b/pkg/idp/token/biscuit.go new file mode 100644 index 0000000..6e9f333 --- /dev/null +++ b/pkg/idp/token/biscuit.go @@ -0,0 +1,41 @@ +package token + +import ( + "context" + "crypto" + "errors" + + "github.com/flynn/biscuit-go/sig" + "github.com/flynn/hubauth/pkg/biscuit" + "github.com/flynn/hubauth/pkg/kmssign" +) + +var ( + ErrPublicKeyRequired = errors.New("token: a public key is required") +) + +type biscuitBuilder struct { + kms kmssign.KMSClient + audienceKey kmssign.AudienceKeyNamer + rootKeyPair sig.Keypair +} + +func NewBiscuitBuilder() AccessTokenBuilder { + return &biscuitBuilder{} +} + +func (b *biscuitBuilder) Build(ctx context.Context, audience string, t *AccessTokenData) ([]byte, error) { + if len(t.UserPublicKey) == 0 { + return nil, ErrPublicKeyRequired + } + audienceKey := kmssign.NewPrivateKey(b.kms, b.audienceKey(audience), crypto.SHA256) + + meta := &biscuit.Metadata{ + ClientID: t.ClientID, + UserID: t.UserID, + UserEmail: t.UserEmail, + IssueTime: t.IssueTime, + } + + return biscuit.GenerateSignable(b.rootKeyPair, audience, audienceKey, t.UserPublicKey, t.ExpireTime, meta) +} diff --git a/pkg/idp/token/builder.go b/pkg/idp/token/builder.go index 15cd05e..5afdd98 100644 --- a/pkg/idp/token/builder.go +++ b/pkg/idp/token/builder.go @@ -6,11 +6,14 @@ import ( ) type AccessTokenData struct { - ClientID string - UserID string - UserEmail string + ClientID string + UserID string + UserEmail string + UserPublicKey []byte + IssueTime time.Time + ExpireTime time.Time } type AccessTokenBuilder interface { - Build(ctx context.Context, audience string, t *AccessTokenData, now time.Time, duration time.Duration) ([]byte, error) + Build(ctx context.Context, audience string, t *AccessTokenData) ([]byte, error) } diff --git a/pkg/idp/token/oauth.go b/pkg/idp/token/oauth.go index 3b96b0c..2ec12f6 100644 --- a/pkg/idp/token/oauth.go +++ b/pkg/idp/token/oauth.go @@ -4,7 +4,6 @@ import ( "context" "crypto" "fmt" - "time" "github.com/flynn/hubauth/pkg/kmssign" "github.com/flynn/hubauth/pkg/pb" @@ -26,11 +25,11 @@ func NewSignedPBBuilder(kms kmssign.KMSClient, audienceKey kmssign.AudienceKeyNa } } -func (b *signedPbBuilder) Build(ctx context.Context, audience string, t *AccessTokenData, now time.Time, duration time.Duration) ([]byte, error) { +func (b *signedPbBuilder) Build(ctx context.Context, audience string, t *AccessTokenData) ([]byte, error) { signKey := kmssign.NewPrivateKey(b.kms, b.audienceKey(audience), crypto.SHA256) - exp, _ := ptypes.TimestampProto(now.Add(duration)) - iss, _ := ptypes.TimestampProto(now) + exp, _ := ptypes.TimestampProto(t.ExpireTime) + iss, _ := ptypes.TimestampProto(t.IssueTime) msg := &pb.AccessToken{ ClientId: t.ClientID, UserId: t.UserID, diff --git a/pkg/idp/token/oauth_test.go b/pkg/idp/token/oauth_test.go index 9785c77..52350e1 100644 --- a/pkg/idp/token/oauth_test.go +++ b/pkg/idp/token/oauth_test.go @@ -31,15 +31,17 @@ func TestSignedPBBuilder(t *testing.T) { now := time.Now() ctx := context.Background() + accessTokenDuration := 5 * time.Minute + data := &AccessTokenData{ - ClientID: "clientID", - UserEmail: "userEmail", - UserID: "userID", + ClientID: "clientID", + UserEmail: "userEmail", + UserID: "userID", + IssueTime: now, + ExpireTime: now.Add(accessTokenDuration), } - accessTokenDuration := 5 * time.Minute - - accessTokenBytes, err := builder.Build(ctx, audienceName, data, now, accessTokenDuration) + accessTokenBytes, err := builder.Build(ctx, audienceName, data) require.NoError(t, err) got := new(pb.AccessToken) From a571a386a6fdd4e57925eb03b4ed9d57aeba6be6 Mon Sep 17 00:00:00 2001 From: daeMOn Date: Mon, 12 Oct 2020 10:17:47 +0200 Subject: [PATCH 05/32] update returned token type to depend on configured builder --- pkg/idp/oauth.go | 12 +++++++----- pkg/idp/oauth_test.go | 12 ++++++------ pkg/idp/steps.go | 6 +++--- pkg/idp/steps_test.go | 10 +++++++++- pkg/idp/token/biscuit.go | 4 ++++ pkg/idp/token/builder.go | 1 + pkg/idp/token/oauth.go | 4 ++++ 7 files changed, 34 insertions(+), 15 deletions(-) diff --git a/pkg/idp/oauth.go b/pkg/idp/oauth.go index 1e21b9a..18fdf4a 100644 --- a/pkg/idp/oauth.go +++ b/pkg/idp/oauth.go @@ -47,7 +47,7 @@ type idpSteps interface { SignRefreshToken(ctx context.Context, signKey signpb.PrivateKey, t *signedRefreshTokenData) (string, error) RenewRefreshToken(ctx context.Context, clientID, oldTokenID string, oldTokenIssueTime, now time.Time) (*hubauth.RefreshToken, error) VerifyRefreshToken(ctx context.Context, rt *hubauth.RefreshToken, now time.Time) error - BuildAccessToken(ctx context.Context, audience string, t *token.AccessTokenData) (string, error) + BuildAccessToken(ctx context.Context, audience string, t *token.AccessTokenData) (string, string, error) } type idpService struct { @@ -304,6 +304,7 @@ func (s *idpService) ExchangeCode(parentCtx context.Context, req *hubauth.Exchan }) var accessToken string + var tokenType string g.Go(func() (err error) { if req.Audience == "" { return nil @@ -318,7 +319,7 @@ func (s *idpService) ExchangeCode(parentCtx context.Context, req *hubauth.Exchan } } - accessToken, err = s.steps.BuildAccessToken(ctx, req.Audience, &token.AccessTokenData{ + accessToken, tokenType, err = s.steps.BuildAccessToken(ctx, req.Audience, &token.AccessTokenData{ ClientID: req.ClientID, UserID: codeInfo.UserId, UserEmail: codeInfo.UserEmail, @@ -348,7 +349,7 @@ func (s *idpService) ExchangeCode(parentCtx context.Context, req *hubauth.Exchan res.AccessToken = res.RefreshToken res.ExpiresIn = res.RefreshTokenExpiresIn } else { - res.TokenType = "Bearer" + res.TokenType = tokenType res.ExpiresIn = int(accessTokenDuration / time.Second) } return res, nil @@ -390,6 +391,7 @@ func (s *idpService) RefreshToken(ctx context.Context, req *hubauth.RefreshToken }) var accessToken string + var tokenType string g.Go(func() (err error) { if req.Audience == "" { return nil @@ -404,7 +406,7 @@ func (s *idpService) RefreshToken(ctx context.Context, req *hubauth.RefreshToken } } - accessToken, err = s.steps.BuildAccessToken(ctx, req.Audience, &token.AccessTokenData{ + accessToken, tokenType, err = s.steps.BuildAccessToken(ctx, req.Audience, &token.AccessTokenData{ ClientID: req.ClientID, UserID: oldToken.UserID, UserEmail: oldToken.UserEmail, @@ -433,7 +435,7 @@ func (s *idpService) RefreshToken(ctx context.Context, req *hubauth.RefreshToken res.AccessToken = res.RefreshToken res.ExpiresIn = res.RefreshTokenExpiresIn } else { - res.TokenType = "Bearer" + res.TokenType = tokenType res.ExpiresIn = int(accessTokenDuration / time.Second) } return res, nil diff --git a/pkg/idp/oauth_test.go b/pkg/idp/oauth_test.go index 870951d..c4b09c4 100644 --- a/pkg/idp/oauth_test.go +++ b/pkg/idp/oauth_test.go @@ -78,9 +78,9 @@ func (m *mockSteps) SignRefreshToken(ctx context.Context, signKey signpb.Private args := m.Called(ctx, signKey, t) return args.String(0), args.Error(1) } -func (m *mockSteps) BuildAccessToken(ctx context.Context, audience string, t *token.AccessTokenData) (string, error) { +func (m *mockSteps) BuildAccessToken(ctx context.Context, audience string, t *token.AccessTokenData) (string, string, error) { args := m.Called(ctx, audience, t) - return args.String(0), args.Error(1) + return args.String(0), args.String(1), args.Error(2) } func (m *mockSteps) RenewRefreshToken(ctx context.Context, clientID, oldTokenID string, oldTokenIssueTime, now time.Time) (*hubauth.RefreshToken, error) { args := m.Called(ctx, clientID, oldTokenID, oldTokenIssueTime, now) @@ -671,7 +671,7 @@ func TestExchangeCode(t *testing.T) { UserEmail: userEmail, IssueTime: now, ExpireTime: now.Add(accessTokenDuration), - }).Return(accessToken, nil) + }).Return(accessToken, testCase.Want.TokenType, nil) req := &hubauth.ExchangeCodeRequest{ ClientID: clientID, @@ -802,7 +802,7 @@ func TestExchangeCodeErrors(t *testing.T) { idpService.steps.(*mockSteps).On("VerifyAudience", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(testCase.VerifyAudienceErr) idpService.steps.(*mockSteps).On("SaveRefreshToken", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&hubauth.Client{}, testCase.SaveErr) idpService.steps.(*mockSteps).On("SignRefreshToken", mock.Anything, mock.Anything, mock.Anything).Return("", testCase.SignRTErr) - idpService.steps.(*mockSteps).On("BuildAccessToken", mock.Anything, mock.Anything, mock.Anything).Return("", testCase.SignATErr) + idpService.steps.(*mockSteps).On("BuildAccessToken", mock.Anything, mock.Anything, mock.Anything).Return("", "", testCase.SignATErr) req := &hubauth.ExchangeCodeRequest{ Code: testCase.Code, @@ -902,7 +902,7 @@ func TestRefreshToken(t *testing.T) { UserEmail: userEmail, IssueTime: now, ExpireTime: now.Add(accessTokenDuration), - }).Return(newAccessTokenStr, nil) + }).Return(newAccessTokenStr, testCase.Want.TokenType, nil) oldTokenSigned, err := signpb.SignMarshal(context.Background(), idpService.refreshKey, &pb.RefreshToken{ Key: oldTokenID, @@ -1005,7 +1005,7 @@ func TestRefreshTokenStepErrors(t *testing.T) { idpService.steps.(*mockSteps).On("VerifyAudience", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(testCase.VerifyAudienceErr) idpService.steps.(*mockSteps).On("RenewRefreshToken", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&hubauth.RefreshToken{}, testCase.RenewRTErr) idpService.steps.(*mockSteps).On("SignRefreshToken", mock.Anything, mock.Anything, mock.Anything).Return("", testCase.SignRTErr) - idpService.steps.(*mockSteps).On("BuildAccessToken", mock.Anything, mock.Anything, mock.Anything).Return("", testCase.SignATErr) + idpService.steps.(*mockSteps).On("BuildAccessToken", mock.Anything, mock.Anything, mock.Anything).Return("", "", testCase.SignATErr) _, err = idpService.RefreshToken(context.Background(), req) require.Equal(t, testCase.ExpectedErr, err) diff --git a/pkg/idp/steps.go b/pkg/idp/steps.go index 14eb144..8e19815 100644 --- a/pkg/idp/steps.go +++ b/pkg/idp/steps.go @@ -351,7 +351,7 @@ func (s *steps) VerifyRefreshToken(ctx context.Context, rt *hubauth.RefreshToken return nil } -func (s *steps) BuildAccessToken(ctx context.Context, audience string, t *token.AccessTokenData) (token string, err error) { +func (s *steps) BuildAccessToken(ctx context.Context, audience string, t *token.AccessTokenData) (token string, tokenType string, err error) { ctx, span := trace.StartSpan(ctx, "idp.BuildAccessToken") span.AddAttributes( trace.StringAttribute("client_id", t.ClientID), @@ -362,7 +362,7 @@ func (s *steps) BuildAccessToken(ctx context.Context, audience string, t *token. tokenBytes, err := s.builder.Build(ctx, audience, t) if err != nil { - return "", fmt.Errorf("idp: error building access token: %w", err) + return "", "", fmt.Errorf("idp: error building access token: %w", err) } idBytes := sha256.Sum256(tokenBytes) @@ -374,5 +374,5 @@ func (s *steps) BuildAccessToken(ctx context.Context, audience string, t *token. zap.Duration("issued_access_token_expires_in", accessTokenDuration), ) - return base64.URLEncoding.EncodeToString(tokenBytes), nil + return base64.URLEncoding.EncodeToString(tokenBytes), s.builder.TokenType(), nil } diff --git a/pkg/idp/steps_test.go b/pkg/idp/steps_test.go index 903d7b0..1c3ba4e 100644 --- a/pkg/idp/steps_test.go +++ b/pkg/idp/steps_test.go @@ -38,6 +38,11 @@ func (m *mockAccessTokenBuilder) Build(ctx context.Context, audience string, t * return args.Get(0).([]byte), args.Error(1) } +func (m *mockAccessTokenBuilder) TokenType() string { + args := m.Called() + return args.String(0) +} + func newTestSteps(t *testing.T) *steps { dsc, err := gdatastore.NewClient(context.Background(), "test") require.NoError(t, err) @@ -782,13 +787,16 @@ func TestBuildAccessToken(t *testing.T) { } expectedAccessToken := []byte("expected-access-token") + expectedTokenType := "MockBearer" s.builder.(*mockAccessTokenBuilder).On("Build", mock.Anything, testAudienceName, data).Return(expectedAccessToken, nil) + s.builder.(*mockAccessTokenBuilder).On("TokenType").Return(expectedTokenType) - accessToken, err := s.BuildAccessToken(context.Background(), testAudienceName, data) + accessToken, tokenType, err := s.BuildAccessToken(context.Background(), testAudienceName, data) require.NoError(t, err) require.NotEmpty(t, accessToken) + require.Equal(t, expectedTokenType, tokenType) accessTokenBytes, err := base64Decode(accessToken) require.NoError(t, err) diff --git a/pkg/idp/token/biscuit.go b/pkg/idp/token/biscuit.go index 6e9f333..8be1dd8 100644 --- a/pkg/idp/token/biscuit.go +++ b/pkg/idp/token/biscuit.go @@ -39,3 +39,7 @@ func (b *biscuitBuilder) Build(ctx context.Context, audience string, t *AccessTo return biscuit.GenerateSignable(b.rootKeyPair, audience, audienceKey, t.UserPublicKey, t.ExpireTime, meta) } + +func (b *biscuitBuilder) TokenType() string { + return "Biscuit" +} diff --git a/pkg/idp/token/builder.go b/pkg/idp/token/builder.go index 5afdd98..0da8c9a 100644 --- a/pkg/idp/token/builder.go +++ b/pkg/idp/token/builder.go @@ -16,4 +16,5 @@ type AccessTokenData struct { type AccessTokenBuilder interface { Build(ctx context.Context, audience string, t *AccessTokenData) ([]byte, error) + TokenType() string } diff --git a/pkg/idp/token/oauth.go b/pkg/idp/token/oauth.go index 2ec12f6..5039157 100644 --- a/pkg/idp/token/oauth.go +++ b/pkg/idp/token/oauth.go @@ -44,3 +44,7 @@ func (b *signedPbBuilder) Build(ctx context.Context, audience string, t *AccessT return tokenBytes, nil } + +func (b *signedPbBuilder) TokenType() string { + return "Bearer" +} From 9ea5bf39b7370c3a1fd2c55659b3a831bfc6312d Mon Sep 17 00:00:00 2001 From: daeMOn Date: Mon, 12 Oct 2020 11:03:01 +0200 Subject: [PATCH 06/32] add access token builder creation switch from env --- cmd/hubauth-ext/main.go | 26 ++++++++++++++++++++++---- pkg/idp/token/biscuit.go | 24 ++++++++++++++++++++++-- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/cmd/hubauth-ext/main.go b/cmd/hubauth-ext/main.go index ef75f1f..88c7071 100644 --- a/cmd/hubauth-ext/main.go +++ b/cmd/hubauth-ext/main.go @@ -69,6 +69,27 @@ func main() { return result.Payload.String() } + audienceKeyNamer := kmssign.AudienceKeyNameFunc(os.Getenv("PROJECT_ID"), os.Getenv("KMS_LOCATION"), os.Getenv("KMS_KEYRING")) + + var accessTokenBuilder token.AccessTokenBuilder + tokenType, exists := os.LookupEnv("TOKEN_TYPE") + if !exists { + tokenType = "Bearer" + } + switch tokenType { + case "Bearer": + accessTokenBuilder = token.NewSignedPBBuilder(kmsClient, audienceKeyNamer) + case "Biscuit": + biscuitKey, err := token.DecodeB64PrivateKey(secret("BISCUIT_ROOT_PRIVKEY")) + if err != nil { + log.Fatalf("failed to initialize biscuit keypair: %v", err) + } + + accessTokenBuilder = token.NewBiscuitBuilder(kmsClient, audienceKeyNamer, biscuitKey) + default: + log.Fatalf("invalid TOKEN_TYPE, must be one of: Bearer, Biscuit") + } + log.Fatal(http.ListenAndServe(":"+httpPort, &ochttp.Handler{ Propagation: &propagation.HTTPFormat{}, Handler: httpapi.New(httpapi.Config{ @@ -80,10 +101,7 @@ func main() { ), []byte(secret("CODE_KEY_SECRET")), refreshKey, - token.NewSignedPBBuilder( - kmsClient, - kmssign.AudienceKeyNameFunc(os.Getenv("PROJECT_ID"), os.Getenv("KMS_LOCATION"), os.Getenv("KMS_KEYRING")), - ), + accessTokenBuilder, ), CookieKey: []byte(secret("COOKIE_KEY_SECRET")), ProjectID: os.Getenv("PROJECT_ID"), diff --git a/pkg/idp/token/biscuit.go b/pkg/idp/token/biscuit.go index 8be1dd8..7736391 100644 --- a/pkg/idp/token/biscuit.go +++ b/pkg/idp/token/biscuit.go @@ -3,7 +3,9 @@ package token import ( "context" "crypto" + "encoding/base64" "errors" + "fmt" "github.com/flynn/biscuit-go/sig" "github.com/flynn/hubauth/pkg/biscuit" @@ -20,8 +22,12 @@ type biscuitBuilder struct { rootKeyPair sig.Keypair } -func NewBiscuitBuilder() AccessTokenBuilder { - return &biscuitBuilder{} +func NewBiscuitBuilder(kms kmssign.KMSClient, audienceKey kmssign.AudienceKeyNamer, rootKeyPair sig.Keypair) AccessTokenBuilder { + return &biscuitBuilder{ + kms: kms, + audienceKey: audienceKey, + rootKeyPair: rootKeyPair, + } } func (b *biscuitBuilder) Build(ctx context.Context, audience string, t *AccessTokenData) ([]byte, error) { @@ -43,3 +49,17 @@ func (b *biscuitBuilder) Build(ctx context.Context, audience string, t *AccessTo func (b *biscuitBuilder) TokenType() string { return "Biscuit" } + +func DecodeB64PrivateKey(b64key string) (sig.Keypair, error) { + var kp sig.Keypair + privKey, err := base64.StdEncoding.DecodeString(b64key) + if err != nil { + return kp, fmt.Errorf("failed to decode b64 key: %w", err) + } + rootPrivateKey, err := sig.NewPrivateKey(privKey) + if err != nil { + return kp, fmt.Errorf("failed to create biscuit private key: %w", err) + } + kp = sig.NewKeypair(rootPrivateKey) + return kp, nil +} From 70e61c8bdd4a3b2bfe302f02d0d7e1beff0d7b71 Mon Sep 17 00:00:00 2001 From: daeMOn Date: Mon, 12 Oct 2020 11:32:03 +0200 Subject: [PATCH 07/32] add /public-key HTTP endpoint --- cmd/hubauth-ext/main.go | 8 +++++++- pkg/httpapi/http.go | 25 +++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/cmd/hubauth-ext/main.go b/cmd/hubauth-ext/main.go index 88c7071..33b3383 100644 --- a/cmd/hubauth-ext/main.go +++ b/cmd/hubauth-ext/main.go @@ -66,12 +66,16 @@ func main() { if err != nil { log.Fatalf("failed to access secret version for %s: %s", name, err) } - return result.Payload.String() + + // Payload.String() would return a json encoded version of the secret: {"data": "..."} + // the actual secret is in Data. + return string(result.Payload.Data) } audienceKeyNamer := kmssign.AudienceKeyNameFunc(os.Getenv("PROJECT_ID"), os.Getenv("KMS_LOCATION"), os.Getenv("KMS_KEYRING")) var accessTokenBuilder token.AccessTokenBuilder + var rootPubKey []byte tokenType, exists := os.LookupEnv("TOKEN_TYPE") if !exists { tokenType = "Bearer" @@ -85,6 +89,7 @@ func main() { log.Fatalf("failed to initialize biscuit keypair: %v", err) } + rootPubKey = biscuitKey.Public().Bytes() accessTokenBuilder = token.NewBiscuitBuilder(kmsClient, audienceKeyNamer, biscuitKey) default: log.Fatalf("invalid TOKEN_TYPE, must be one of: Bearer, Biscuit") @@ -107,6 +112,7 @@ func main() { ProjectID: os.Getenv("PROJECT_ID"), Repository: fmt.Sprintf("https://source.developers.google.com/p/%s/r/%s", os.Getenv("PROJECT_ID"), os.Getenv("BUILD_REPO")), Revision: os.Getenv("BUILD_REV"), + PublicKey: rootPubKey, }), }, )) diff --git a/pkg/httpapi/http.go b/pkg/httpapi/http.go index 641212d..cd0d284 100644 --- a/pkg/httpapi/http.go +++ b/pkg/httpapi/http.go @@ -34,6 +34,7 @@ func (clockImpl) Now() time.Time { type Config struct { IdP hubauth.IdPService CookieKey hmacpb.Key + PublicKey []byte ProjectID string Repository string Revision string @@ -87,6 +88,8 @@ func (a *api) ServeHTTP(rw http.ResponseWriter, req *http.Request) { w.Header().Set("Access-Control-Allow-Methods", "GET") w.Header().Set("Access-Control-Max-Age", "86400") w.WriteHeader(http.StatusOK) + case req.Method == "GET" && req.URL.Path == "/public-key": + a.PublicKey(w, req) case req.Method == "GET" && req.URL.Path == "/": http.Redirect(w, req, "https://flynn.io/", http.StatusFound) case req.Method == "GET" && req.URL.Path == "/privacy": @@ -394,6 +397,28 @@ func (a *api) Audiences(w http.ResponseWriter, req *http.Request) { json.NewEncoder(w).Encode(res) } +func (a *api) PublicKey(w http.ResponseWriter, req *http.Request) { + if len(a.Config.PublicKey) == 0 { + a.handleErr(w, req, &hubauth.OAuthError{ + Code: "unsupported_request", + Description: "no public key configured", + }) + return + } + + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Content-Type", "application/json") + + w.WriteHeader(http.StatusOK) + + type key struct { + PublicKey string `json:"public-key"` + } + json.NewEncoder(w).Encode(&key{ + PublicKey: base64.StdEncoding.EncodeToString(a.Config.PublicKey), + }) +} + func (a *api) handleErr(w http.ResponseWriter, req *http.Request, err error) { oe, ok := err.(*hubauth.OAuthError) if !ok { From 8846d2b12f31111cc31071c0226739cee0490526 Mon Sep 17 00:00:00 2001 From: daeMOn Date: Mon, 12 Oct 2020 16:53:54 +0200 Subject: [PATCH 08/32] update biscuit verify audience key type to use *ecdsa.PublicKey instead of *kmssign.Key --- pkg/biscuit/biscuit.go | 3 ++- pkg/biscuit/biscuit_test.go | 7 ++++--- pkg/biscuit/signature.go | 5 +++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/pkg/biscuit/biscuit.go b/pkg/biscuit/biscuit.go index e335e60..488d302 100644 --- a/pkg/biscuit/biscuit.go +++ b/pkg/biscuit/biscuit.go @@ -1,6 +1,7 @@ package biscuit import ( + "crypto/ecdsa" "crypto/rand" "fmt" "time" @@ -108,7 +109,7 @@ func Sign(token []byte, rootPubKey sig.PublicKey, userKey *UserKeyPair) ([]byte, // Verify will verify the biscuit, the included audience and user signature, and return an error // when anything is invalid. -func Verify(token []byte, rootPubKey sig.PublicKey, audience string, audienceKey *kmssign.Key) (*Metadata, error) { +func Verify(token []byte, rootPubKey sig.PublicKey, audience string, audienceKey *ecdsa.PublicKey) (*Metadata, error) { b, err := biscuit.Unmarshal(token) if err != nil { return nil, fmt.Errorf("biscuit: failed to unmarshal: %w", err) diff --git a/pkg/biscuit/biscuit_test.go b/pkg/biscuit/biscuit_test.go index bf6199b..5882f32 100644 --- a/pkg/biscuit/biscuit_test.go +++ b/pkg/biscuit/biscuit_test.go @@ -2,6 +2,7 @@ package biscuit import ( "context" + "crypto/ecdsa" "crypto/rand" "testing" "time" @@ -36,7 +37,7 @@ func TestBiscuit(t *testing.T) { require.NoError(t, err) t.Logf("signed biscuit size: %d", len(signedBiscuit)) - res, err := Verify(signedBiscuit, rootKey.Public(), audience, audienceKey) + res, err := Verify(signedBiscuit, rootKey.Public(), audience, audienceKey.Public().(*ecdsa.PublicKey)) require.NoError(t, err) require.Equal(t, metas.ClientID, res.ClientID) require.Equal(t, metas.UserID, res.UserID) @@ -53,7 +54,7 @@ func TestBiscuit(t *testing.T) { signedBiscuit, err := Sign(signableBiscuit, rootKey.Public(), userKey) require.NoError(t, err) - _, err = Verify(signedBiscuit, rootKey.Public(), "http://another.audience.url", audienceKey) + _, err = Verify(signedBiscuit, rootKey.Public(), "http://another.audience.url", audienceKey.Public().(*ecdsa.PublicKey)) require.Error(t, err) wrongAudience := "http://another.audience.url" @@ -61,7 +62,7 @@ func TestBiscuit(t *testing.T) { wrongAudienceKey, err := kmssign.NewKey(context.Background(), kms, wrongAudience) require.NoError(t, err) - _, err = Verify(signedBiscuit, rootKey.Public(), audience, wrongAudienceKey) + _, err = Verify(signedBiscuit, rootKey.Public(), audience, wrongAudienceKey.Public().(*ecdsa.PublicKey)) require.Error(t, err) }) } diff --git a/pkg/biscuit/signature.go b/pkg/biscuit/signature.go index 078eaa6..86b5545 100644 --- a/pkg/biscuit/signature.go +++ b/pkg/biscuit/signature.go @@ -147,11 +147,12 @@ func audienceSign(audience string, audienceKey *kmssign.Key) (*audienceVerificat }, nil } -func verifyAudienceSignature(audiencePubkey *kmssign.Key, data *audienceVerificationData) error { +func verifyAudienceSignature(audiencePubkey *ecdsa.PublicKey, data *audienceVerificationData) error { signedData := append(signStaticCtx, data.Challenge...) signedData = append(signedData, []byte(data.Audience)...) hash := sha256.Sum256(signedData) - if !audiencePubkey.Verify(hash[:], data.Signature) { + + if !ecdsa.VerifyASN1(audiencePubkey, hash[:], data.Signature) { return errors.New("invalid signature") } return nil From 6cce9d2700504a6b35832d0f5c004dd214a67861 Mon Sep 17 00:00:00 2001 From: daeMOn Date: Mon, 12 Oct 2020 16:54:13 +0200 Subject: [PATCH 09/32] simplify http public key encoding --- pkg/httpapi/http.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/httpapi/http.go b/pkg/httpapi/http.go index cd0d284..74d7d98 100644 --- a/pkg/httpapi/http.go +++ b/pkg/httpapi/http.go @@ -412,10 +412,10 @@ func (a *api) PublicKey(w http.ResponseWriter, req *http.Request) { w.WriteHeader(http.StatusOK) type key struct { - PublicKey string `json:"public-key"` + PublicKey []byte `json:"public-key"` } json.NewEncoder(w).Encode(&key{ - PublicKey: base64.StdEncoding.EncodeToString(a.Config.PublicKey), + PublicKey: a.Config.PublicKey, }) } From 5c2751a5d80eda4a342557d43eb7463e50e2522c Mon Sep 17 00:00:00 2001 From: daeMOn Date: Mon, 12 Oct 2020 17:38:00 +0200 Subject: [PATCH 10/32] add helper to create UserKeyPair from an ecdsa.PrivateKey --- pkg/biscuit/biscuit.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pkg/biscuit/biscuit.go b/pkg/biscuit/biscuit.go index 488d302..900de10 100644 --- a/pkg/biscuit/biscuit.go +++ b/pkg/biscuit/biscuit.go @@ -3,6 +3,7 @@ package biscuit import ( "crypto/ecdsa" "crypto/rand" + "crypto/x509" "fmt" "time" @@ -23,6 +24,21 @@ type UserKeyPair struct { Private []byte } +func NewECDSAKeyPair(priv *ecdsa.PrivateKey) (*UserKeyPair, error) { + privKeyBytes, err := x509.MarshalECPrivateKey(priv) + if err != nil { + return nil, fmt.Errorf("failed to marshal ecdsa privkey: %v", err) + } + pubKeyBytes, err := x509.MarshalPKIXPublicKey(&priv.PublicKey) + if err != nil { + return nil, fmt.Errorf("failed to marshal ecdsa pubkey: %v", err) + } + return &UserKeyPair{ + Private: privKeyBytes, + Public: pubKeyBytes, + }, nil +} + // GenerateSignable returns a biscuit which will only verify after being // signed with the private key matching the given userPubkey. func GenerateSignable(rootKey sig.Keypair, audience string, audienceKey *kmssign.Key, userPublicKey []byte, expireTime time.Time, m *Metadata) ([]byte, error) { From 4e32e6a139bb558388bf5becf0ed460ec99b5e7a Mon Sep 17 00:00:00 2001 From: daeMOn Date: Tue, 13 Oct 2020 11:50:18 +0200 Subject: [PATCH 11/32] cleanup --- go.mod | 2 +- go.sum | 8 ++------ pkg/biscuit/biscuit.go | 2 +- pkg/biscuit/signature_test.go | 11 +++-------- 4 files changed, 7 insertions(+), 16 deletions(-) diff --git a/go.mod b/go.mod index 61e8549..367d530 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/alecthomas/kong v0.2.12 github.com/aws/aws-sdk-go v1.34.6 // indirect github.com/census-instrumentation/opencensus-proto v0.3.0 // indirect - github.com/flynn/biscuit-go v0.0.0-20201009174859-e7eb59a90195 + github.com/flynn/biscuit-go v0.0.0-20201013094706-57a856cd63ea github.com/golang/protobuf v1.4.2 github.com/googleapis/gax-go/v2 v2.0.5 github.com/jedib0t/go-pretty/v6 v6.0.5 diff --git a/go.sum b/go.sum index f0f0ad5..279d1ce 100644 --- a/go.sum +++ b/go.sum @@ -78,10 +78,8 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/flynn/biscuit-go v0.0.0-20200907174027-193b7bdbbdca h1:LUZQQzaCT+gltxii4icyPH5oMdAP38JmbvO9aI0E4qM= -github.com/flynn/biscuit-go v0.0.0-20200907174027-193b7bdbbdca/go.mod h1:EMJZ3stAYtwaP763F5HcGjPjCnYu21V2TEsg/iw88I8= -github.com/flynn/biscuit-go v0.0.0-20201009174859-e7eb59a90195 h1:TP3jMHmhjz8XxqqigEd5OQffNAO/6KPvGUYII6TFdmI= -github.com/flynn/biscuit-go v0.0.0-20201009174859-e7eb59a90195/go.mod h1:EMJZ3stAYtwaP763F5HcGjPjCnYu21V2TEsg/iw88I8= +github.com/flynn/biscuit-go v0.0.0-20201013094706-57a856cd63ea h1:QKWFyrvV0SKwUq4wYxdQZxVJASGsrmoGOolGpo/mjAU= +github.com/flynn/biscuit-go v0.0.0-20201013094706-57a856cd63ea/go.mod h1:EMJZ3stAYtwaP763F5HcGjPjCnYu21V2TEsg/iw88I8= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -207,8 +205,6 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de h1:ikNHVSjEfnvz6sxdSPCaPt572qowuyMDMJLLm3Db3ig= -golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 h1:hb9wdF1z5waM+dSIICn1l0DkLVDT3hqhhQsDNUmHPRE= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= diff --git a/pkg/biscuit/biscuit.go b/pkg/biscuit/biscuit.go index 900de10..bf7e15b 100644 --- a/pkg/biscuit/biscuit.go +++ b/pkg/biscuit/biscuit.go @@ -43,7 +43,7 @@ func NewECDSAKeyPair(priv *ecdsa.PrivateKey) (*UserKeyPair, error) { // signed with the private key matching the given userPubkey. func GenerateSignable(rootKey sig.Keypair, audience string, audienceKey *kmssign.Key, userPublicKey []byte, expireTime time.Time, m *Metadata) ([]byte, error) { builder := &hubauthBuilder{ - Builder: biscuit.NewBuilder(rand.Reader, rootKey), + biscuit.NewBuilder(rand.Reader, rootKey), } if err := builder.withAudienceSignature(audience, audienceKey); err != nil { diff --git a/pkg/biscuit/signature_test.go b/pkg/biscuit/signature_test.go index d4d4cc2..2351e26 100644 --- a/pkg/biscuit/signature_test.go +++ b/pkg/biscuit/signature_test.go @@ -211,7 +211,6 @@ func TestVerifyUserSignatureFail(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.desc, func(t *testing.T) { - err := verifyUserSignature(testCase.tokenHash, testCase.data) require.Error(t, err) if testCase.expectedErr != nil { @@ -224,12 +223,8 @@ func TestVerifyUserSignatureFail(t *testing.T) { func generateUserKeyPair(t *testing.T) *UserKeyPair { priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) require.NoError(t, err) - privBytes, err := x509.MarshalECPrivateKey(priv) - require.NoError(t, err) - pubBytes, err := x509.MarshalPKIXPublicKey(&priv.PublicKey) + + kp, err := NewECDSAKeyPair(priv) require.NoError(t, err) - return &UserKeyPair{ - Private: privBytes, - Public: pubBytes, - } + return kp } From 23ffbf879b3ae3b0b9955105b73ea299ab315924 Mon Sep 17 00:00:00 2001 From: daeMOn Date: Tue, 13 Oct 2020 11:50:44 +0200 Subject: [PATCH 12/32] add biscuit setup instructions in readme --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index 13252a1..9f415fc 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,22 @@ Create a new job: - URL: use the hubauth-int URL: `/cron` +## Enabling Biscuit + +To use biscuit tokens instead of bearers, configure the following: + +### In Security > Secret manager + +Create new secret +- HUBAUTH_BISCUIT_ROOT_PRIVKEY: a base64 encoded p256 EC private key + +### In variables + +Add a new variable +- TOKEN_TYPE: `Biscuit` +- BISCUIT_ROOT_PRIVKEY: set to the resource ID from `HUBAUTH_BISCUIT_ROOT_PRIVKEY` + + ## Hubauth CLI Configure gcloud auth application-default with the following command, and follow the browser instructions: From 4ee2432d0a59974a548444aba6c9a10854dcb6dc Mon Sep 17 00:00:00 2001 From: daeMOn Date: Tue, 13 Oct 2020 13:56:27 +0200 Subject: [PATCH 13/32] renamed signedPbBuilder to bearerBuilder --- cmd/hubauth-ext/main.go | 2 +- pkg/idp/token/{oauth.go => bearer.go} | 12 ++++++------ pkg/idp/token/{oauth_test.go => bearer_test.go} | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) rename pkg/idp/token/{oauth.go => bearer.go} (69%) rename pkg/idp/token/{oauth_test.go => bearer_test.go} (96%) diff --git a/cmd/hubauth-ext/main.go b/cmd/hubauth-ext/main.go index 33b3383..fce743d 100644 --- a/cmd/hubauth-ext/main.go +++ b/cmd/hubauth-ext/main.go @@ -82,7 +82,7 @@ func main() { } switch tokenType { case "Bearer": - accessTokenBuilder = token.NewSignedPBBuilder(kmsClient, audienceKeyNamer) + accessTokenBuilder = token.NewBearerBuilder(kmsClient, audienceKeyNamer) case "Biscuit": biscuitKey, err := token.DecodeB64PrivateKey(secret("BISCUIT_ROOT_PRIVKEY")) if err != nil { diff --git a/pkg/idp/token/oauth.go b/pkg/idp/token/bearer.go similarity index 69% rename from pkg/idp/token/oauth.go rename to pkg/idp/token/bearer.go index 5039157..8ddb234 100644 --- a/pkg/idp/token/oauth.go +++ b/pkg/idp/token/bearer.go @@ -11,21 +11,21 @@ import ( "github.com/golang/protobuf/ptypes" ) -type signedPbBuilder struct { +type bearerBuilder struct { kms kmssign.KMSClient audienceKey kmssign.AudienceKeyNamer } -var _ AccessTokenBuilder = (*signedPbBuilder)(nil) +var _ AccessTokenBuilder = (*bearerBuilder)(nil) -func NewSignedPBBuilder(kms kmssign.KMSClient, audienceKey kmssign.AudienceKeyNamer) AccessTokenBuilder { - return &signedPbBuilder{ +func NewBearerBuilder(kms kmssign.KMSClient, audienceKey kmssign.AudienceKeyNamer) AccessTokenBuilder { + return &bearerBuilder{ kms: kms, audienceKey: audienceKey, } } -func (b *signedPbBuilder) Build(ctx context.Context, audience string, t *AccessTokenData) ([]byte, error) { +func (b *bearerBuilder) Build(ctx context.Context, audience string, t *AccessTokenData) ([]byte, error) { signKey := kmssign.NewPrivateKey(b.kms, b.audienceKey(audience), crypto.SHA256) exp, _ := ptypes.TimestampProto(t.ExpireTime) @@ -45,6 +45,6 @@ func (b *signedPbBuilder) Build(ctx context.Context, audience string, t *AccessT return tokenBytes, nil } -func (b *signedPbBuilder) TokenType() string { +func (b *bearerBuilder) TokenType() string { return "Bearer" } diff --git a/pkg/idp/token/oauth_test.go b/pkg/idp/token/bearer_test.go similarity index 96% rename from pkg/idp/token/oauth_test.go rename to pkg/idp/token/bearer_test.go index 52350e1..0789be4 100644 --- a/pkg/idp/token/oauth_test.go +++ b/pkg/idp/token/bearer_test.go @@ -23,7 +23,7 @@ func TestSignedPBBuilder(t *testing.T) { audienceKeyName := audienceKeyNamer(audienceName) kms := kmssim.NewClient([]string{audienceKeyName}) - builder := NewSignedPBBuilder(kms, audienceKeyNamer) + builder := NewBearerBuilder(kms, audienceKeyNamer) signKey, err := kmssign.NewKey(context.Background(), kms, audienceKeyName) require.NoError(t, err) From d84af7227a9566f0c8ddca40bc3fc58330e118c0 Mon Sep 17 00:00:00 2001 From: daeMOn Date: Tue, 13 Oct 2020 14:11:42 +0200 Subject: [PATCH 14/32] extracted user signature nonce and timestamp --- pkg/biscuit/biscuit.go | 18 ++++++++++++++++-- pkg/biscuit/biscuit_test.go | 2 ++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/pkg/biscuit/biscuit.go b/pkg/biscuit/biscuit.go index bf7e15b..ebc8958 100644 --- a/pkg/biscuit/biscuit.go +++ b/pkg/biscuit/biscuit.go @@ -123,9 +123,15 @@ func Sign(token []byte, rootPubKey sig.PublicKey, userKey *UserKeyPair) ([]byte, return b.Serialize() } +type VerifiedMetadata struct { + *Metadata + UserSignatureNonce []byte + UserSignatureTimestamp time.Time +} + // Verify will verify the biscuit, the included audience and user signature, and return an error // when anything is invalid. -func Verify(token []byte, rootPubKey sig.PublicKey, audience string, audienceKey *ecdsa.PublicKey) (*Metadata, error) { +func Verify(token []byte, rootPubKey sig.PublicKey, audience string, audienceKey *ecdsa.PublicKey) (*VerifiedMetadata, error) { b, err := biscuit.Unmarshal(token) if err != nil { return nil, fmt.Errorf("biscuit: failed to unmarshal: %w", err) @@ -176,5 +182,13 @@ func Verify(token []byte, rootPubKey sig.PublicKey, audience string, audienceKey return nil, fmt.Errorf("biscuit: failed to verify: %w", err) } - return verifier.getMetadata() + metas, err := verifier.getMetadata() + if err != nil { + return nil, fmt.Errorf("biscuit: failed to get metadata: %v", err) + } + return &VerifiedMetadata{ + Metadata: metas, + UserSignatureNonce: userVerificationData.Nonce, + UserSignatureTimestamp: time.Time(userVerificationData.Timestamp), + }, nil } diff --git a/pkg/biscuit/biscuit_test.go b/pkg/biscuit/biscuit_test.go index 5882f32..560a063 100644 --- a/pkg/biscuit/biscuit_test.go +++ b/pkg/biscuit/biscuit_test.go @@ -43,6 +43,8 @@ func TestBiscuit(t *testing.T) { require.Equal(t, metas.UserID, res.UserID) require.Equal(t, metas.UserEmail, res.UserEmail) require.WithinDuration(t, metas.IssueTime, res.IssueTime, 1*time.Second) + require.NotEmpty(t, res.UserSignatureNonce) + require.NotEmpty(t, res.UserSignatureTimestamp) }) t.Run("user sign with wrong key", func(t *testing.T) { From 31f2745089cfcf3bc6079a40ec1bb32fcba2a17e Mon Sep 17 00:00:00 2001 From: daeMOn Date: Tue, 13 Oct 2020 14:35:51 +0200 Subject: [PATCH 15/32] idp/token: add biscuit tests --- pkg/idp/token/biscuit_test.go | 63 +++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 pkg/idp/token/biscuit_test.go diff --git a/pkg/idp/token/biscuit_test.go b/pkg/idp/token/biscuit_test.go new file mode 100644 index 0000000..41b705f --- /dev/null +++ b/pkg/idp/token/biscuit_test.go @@ -0,0 +1,63 @@ +package token + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "encoding/base64" + "testing" + "time" + + "github.com/flynn/biscuit-go" + "github.com/flynn/biscuit-go/sig" + "github.com/flynn/hubauth/pkg/kmssign/kmssim" + "github.com/stretchr/testify/require" +) + +func TestBiscuitBuilder(t *testing.T) { + audience := "https://audience.url" + audienceKeyName := audienceKeyNamer(audience) + kmsClient := kmssim.NewClient([]string{audienceKeyName}) + rootKeyPair := sig.GenerateKeypair(rand.Reader) + + builder := NewBiscuitBuilder(kmsClient, audienceKeyNamer, rootKeyPair) + + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + userPublicKey, err := x509.MarshalPKIXPublicKey(&priv.PublicKey) + require.NoError(t, err) + + now := time.Now() + accessTokenData := &AccessTokenData{ + ClientID: "clientID", + ExpireTime: now.Add(1 * time.Minute), + IssueTime: now, + UserEmail: "user@email", + UserID: "userID", + } + _, err = builder.Build(context.Background(), audience, accessTokenData) + require.Equal(t, ErrPublicKeyRequired, err) + + accessTokenData.UserPublicKey = userPublicKey + token, err := builder.Build(context.Background(), audience, accessTokenData) + require.NoError(t, err) + require.NotEmpty(t, token) + + b, err := biscuit.Unmarshal(token) + require.NoError(t, err) + + _, err = b.Verify(rootKeyPair.Public()) + require.NoError(t, err) +} + +func TestDecodeB64PrivateKey(t *testing.T) { + expectedKP := sig.GenerateKeypair(rand.Reader) + b64key := base64.StdEncoding.EncodeToString(expectedKP.Private().Bytes()) + + kp, err := DecodeB64PrivateKey(b64key) + require.NoError(t, err) + require.Equal(t, expectedKP.Private().Bytes(), kp.Private().Bytes()) + require.Equal(t, expectedKP.Public().Bytes(), kp.Public().Bytes()) +} From 88c2864c5f4f78eb466ffa87db6658d588009c4d Mon Sep 17 00:00:00 2001 From: Flavien Binet Date: Thu, 15 Oct 2020 10:38:31 +0200 Subject: [PATCH 16/32] moved biscuit pkg to biscuit-go repo --- go.mod | 4 +- go.sum | 5 +- pkg/biscuit/biscuit.go | 194 ---------------- pkg/biscuit/biscuit_test.go | 70 ------ pkg/biscuit/signature.go | 177 -------------- pkg/biscuit/signature_test.go | 230 ------------------ pkg/biscuit/wrapper.go | 426 ---------------------------------- pkg/idp/token/biscuit.go | 6 +- 8 files changed, 8 insertions(+), 1104 deletions(-) delete mode 100644 pkg/biscuit/biscuit.go delete mode 100644 pkg/biscuit/biscuit_test.go delete mode 100644 pkg/biscuit/signature.go delete mode 100644 pkg/biscuit/signature_test.go delete mode 100644 pkg/biscuit/wrapper.go diff --git a/go.mod b/go.mod index 367d530..09be740 100644 --- a/go.mod +++ b/go.mod @@ -9,8 +9,8 @@ require ( github.com/alecthomas/kong v0.2.12 github.com/aws/aws-sdk-go v1.34.6 // indirect github.com/census-instrumentation/opencensus-proto v0.3.0 // indirect - github.com/flynn/biscuit-go v0.0.0-20201013094706-57a856cd63ea - github.com/golang/protobuf v1.4.2 + github.com/flynn/biscuit-go v0.0.0-20201015081742-15d7d351f345 + github.com/golang/protobuf v1.4.3 github.com/googleapis/gax-go/v2 v2.0.5 github.com/jedib0t/go-pretty/v6 v6.0.5 github.com/stretchr/testify v1.6.1 diff --git a/go.sum b/go.sum index 279d1ce..ce60913 100644 --- a/go.sum +++ b/go.sum @@ -78,8 +78,9 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/flynn/biscuit-go v0.0.0-20201013094706-57a856cd63ea h1:QKWFyrvV0SKwUq4wYxdQZxVJASGsrmoGOolGpo/mjAU= -github.com/flynn/biscuit-go v0.0.0-20201013094706-57a856cd63ea/go.mod h1:EMJZ3stAYtwaP763F5HcGjPjCnYu21V2TEsg/iw88I8= +github.com/flynn/biscuit-go v0.0.0-20201015071554-9ea2c863efe8 h1:jz3x+pevBqZiO9m7M58fN/W8K0OyDUfchIsHE8XgZT0= +github.com/flynn/biscuit-go v0.0.0-20201015081742-15d7d351f345 h1:ME6bm5dwn9V2DUlfXJqeN121B5nM7rDFqLFOATALqYE= +github.com/flynn/biscuit-go v0.0.0-20201015081742-15d7d351f345/go.mod h1:Sj4oR2hNkrZH1cf3Cj5DPHc3Xq0o61GWeau6UkZR+3c= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= diff --git a/pkg/biscuit/biscuit.go b/pkg/biscuit/biscuit.go deleted file mode 100644 index ebc8958..0000000 --- a/pkg/biscuit/biscuit.go +++ /dev/null @@ -1,194 +0,0 @@ -package biscuit - -import ( - "crypto/ecdsa" - "crypto/rand" - "crypto/x509" - "fmt" - "time" - - "github.com/flynn/biscuit-go" - "github.com/flynn/biscuit-go/sig" - "github.com/flynn/hubauth/pkg/kmssign" -) - -type Metadata struct { - ClientID string - UserID string - UserEmail string - IssueTime time.Time -} - -type UserKeyPair struct { - Public []byte - Private []byte -} - -func NewECDSAKeyPair(priv *ecdsa.PrivateKey) (*UserKeyPair, error) { - privKeyBytes, err := x509.MarshalECPrivateKey(priv) - if err != nil { - return nil, fmt.Errorf("failed to marshal ecdsa privkey: %v", err) - } - pubKeyBytes, err := x509.MarshalPKIXPublicKey(&priv.PublicKey) - if err != nil { - return nil, fmt.Errorf("failed to marshal ecdsa pubkey: %v", err) - } - return &UserKeyPair{ - Private: privKeyBytes, - Public: pubKeyBytes, - }, nil -} - -// GenerateSignable returns a biscuit which will only verify after being -// signed with the private key matching the given userPubkey. -func GenerateSignable(rootKey sig.Keypair, audience string, audienceKey *kmssign.Key, userPublicKey []byte, expireTime time.Time, m *Metadata) ([]byte, error) { - builder := &hubauthBuilder{ - biscuit.NewBuilder(rand.Reader, rootKey), - } - - if err := builder.withAudienceSignature(audience, audienceKey); err != nil { - return nil, err - } - - if err := builder.withUserToSignFact(userPublicKey); err != nil { - return nil, err - } - - if err := builder.withExpire(expireTime); err != nil { - return nil, err - } - - if err := builder.withMetadata(m); err != nil { - return nil, err - } - - b, err := builder.Build() - if err != nil { - return nil, err - } - - return b.Serialize() -} - -// Sign append a user signature on the given token and return it. -// The UserKeyPair key format to provide depends on the signature algorithm: -// - for ECDSA_P256_SHA256, the private key must be encoded in SEC 1, ASN.1 DER form, -// and the public key in PKIX, ASN.1 DER form. -func Sign(token []byte, rootPubKey sig.PublicKey, userKey *UserKeyPair) ([]byte, error) { - b, err := biscuit.Unmarshal(token) - if err != nil { - return nil, fmt.Errorf("biscuit: failed to unmarshal: %w", err) - } - - v, err := b.Verify(rootPubKey) - if err != nil { - return nil, fmt.Errorf("biscuit: failed to verify: %w", err) - } - verifier := &hubauthVerifier{ - Verifier: v, - } - - toSignData, err := verifier.getUserToSignData(userKey.Public) - if err != nil { - return nil, fmt.Errorf("biscuit: failed to get to_sign data: %w", err) - } - - if err := verifier.ensureNotAlreadyUserSigned(toSignData.DataID, userKey.Public); err != nil { - return nil, fmt.Errorf("biscuit: previous signature check failed: %w", err) - } - - tokenHash, err := b.SHA256Sum(b.BlockCount()) - if err != nil { - return nil, err - } - - signData, err := userSign(tokenHash, userKey, toSignData) - if err != nil { - return nil, fmt.Errorf("biscuit: signature failed: %w", err) - } - - builder := &hubauthBlockBuilder{ - BlockBuilder: b.CreateBlock(), - } - if err := builder.withUserSignature(signData); err != nil { - return nil, fmt.Errorf("biscuit: failed to create signature block: %w", err) - } - - clientKey := sig.GenerateKeypair(rand.Reader) - b, err = b.Append(rand.Reader, clientKey, builder.Build()) - if err != nil { - return nil, fmt.Errorf("biscuit: failed to append signature block: %w", err) - } - - return b.Serialize() -} - -type VerifiedMetadata struct { - *Metadata - UserSignatureNonce []byte - UserSignatureTimestamp time.Time -} - -// Verify will verify the biscuit, the included audience and user signature, and return an error -// when anything is invalid. -func Verify(token []byte, rootPubKey sig.PublicKey, audience string, audienceKey *ecdsa.PublicKey) (*VerifiedMetadata, error) { - b, err := biscuit.Unmarshal(token) - if err != nil { - return nil, fmt.Errorf("biscuit: failed to unmarshal: %w", err) - } - - v, err := b.Verify(rootPubKey) - if err != nil { - return nil, fmt.Errorf("biscuit: failed to verify: %w", err) - } - verifier := &hubauthVerifier{v} - - audienceVerificationData, err := verifier.getAudienceVerificationData(audience) - if err != nil { - return nil, fmt.Errorf("biscuit: failed to retrieve audience signature data: %w", err) - } - - if err := verifyAudienceSignature(audienceKey, audienceVerificationData); err != nil { - return nil, fmt.Errorf("biscuit: failed to verify audience signature: %w", err) - } - if err := verifier.withValidatedAudienceSignature(audienceVerificationData); err != nil { - return nil, fmt.Errorf("biscuit: failed to add validated signature: %w", err) - } - - userVerificationData, err := verifier.getUserVerificationData() - if err != nil { - return nil, fmt.Errorf("biscuit: failed to retrieve user signature data: %w", err) - } - - // TODO: improve biscuit API to allow retrieve the block index the signature is at - // so that we can still append other blocks if needed. Right now the signature MUST BE the last block. - signedTokenHash, err := b.SHA256Sum(b.BlockCount() - 1) - if err != nil { - return nil, fmt.Errorf("biscuit: failed to generate token hash: %w", err) - } - - if err := verifyUserSignature(signedTokenHash, userVerificationData); err != nil { - return nil, fmt.Errorf("biscuit: failed to verify user signature: %w", err) - } - if err := verifier.withValidatedUserSignature(userVerificationData); err != nil { - return nil, fmt.Errorf("biscuit: failed to add validated signature: %w", err) - } - - if err := verifier.withCurrentTime(time.Now()); err != nil { - return nil, fmt.Errorf("biscuit: failed to add current time: %w", err) - } - - if err := verifier.Verify(); err != nil { - return nil, fmt.Errorf("biscuit: failed to verify: %w", err) - } - - metas, err := verifier.getMetadata() - if err != nil { - return nil, fmt.Errorf("biscuit: failed to get metadata: %v", err) - } - return &VerifiedMetadata{ - Metadata: metas, - UserSignatureNonce: userVerificationData.Nonce, - UserSignatureTimestamp: time.Time(userVerificationData.Timestamp), - }, nil -} diff --git a/pkg/biscuit/biscuit_test.go b/pkg/biscuit/biscuit_test.go deleted file mode 100644 index 560a063..0000000 --- a/pkg/biscuit/biscuit_test.go +++ /dev/null @@ -1,70 +0,0 @@ -package biscuit - -import ( - "context" - "crypto/ecdsa" - "crypto/rand" - "testing" - "time" - - "github.com/flynn/biscuit-go/sig" - "github.com/flynn/hubauth/pkg/kmssign" - "github.com/flynn/hubauth/pkg/kmssign/kmssim" - "github.com/stretchr/testify/require" -) - -func TestBiscuit(t *testing.T) { - rootKey := sig.GenerateKeypair(rand.Reader) - audience := "http://random.audience.url" - - kms := kmssim.NewClient([]string{audience}) - audienceKey, err := kmssign.NewKey(context.Background(), kms, audience) - require.NoError(t, err) - - userKey := generateUserKeyPair(t) - metas := &Metadata{ - ClientID: "abcd", - UserEmail: "1234@example.com", - UserID: "1234", - IssueTime: time.Now(), - } - signableBiscuit, err := GenerateSignable(rootKey, audience, audienceKey, userKey.Public, time.Now().Add(5*time.Minute), metas) - require.NoError(t, err) - t.Logf("signable biscuit size: %d", len(signableBiscuit)) - - t.Run("happy path", func(t *testing.T) { - signedBiscuit, err := Sign(signableBiscuit, rootKey.Public(), userKey) - require.NoError(t, err) - t.Logf("signed biscuit size: %d", len(signedBiscuit)) - - res, err := Verify(signedBiscuit, rootKey.Public(), audience, audienceKey.Public().(*ecdsa.PublicKey)) - require.NoError(t, err) - require.Equal(t, metas.ClientID, res.ClientID) - require.Equal(t, metas.UserID, res.UserID) - require.Equal(t, metas.UserEmail, res.UserEmail) - require.WithinDuration(t, metas.IssueTime, res.IssueTime, 1*time.Second) - require.NotEmpty(t, res.UserSignatureNonce) - require.NotEmpty(t, res.UserSignatureTimestamp) - }) - - t.Run("user sign with wrong key", func(t *testing.T) { - _, err := Sign(signableBiscuit, rootKey.Public(), generateUserKeyPair(t)) - require.Error(t, err) - }) - - t.Run("verify wrong audience", func(t *testing.T) { - signedBiscuit, err := Sign(signableBiscuit, rootKey.Public(), userKey) - require.NoError(t, err) - - _, err = Verify(signedBiscuit, rootKey.Public(), "http://another.audience.url", audienceKey.Public().(*ecdsa.PublicKey)) - require.Error(t, err) - - wrongAudience := "http://another.audience.url" - kms := kmssim.NewClient([]string{wrongAudience}) - wrongAudienceKey, err := kmssign.NewKey(context.Background(), kms, wrongAudience) - require.NoError(t, err) - - _, err = Verify(signedBiscuit, rootKey.Public(), audience, wrongAudienceKey.Public().(*ecdsa.PublicKey)) - require.Error(t, err) - }) -} diff --git a/pkg/biscuit/signature.go b/pkg/biscuit/signature.go deleted file mode 100644 index 86b5545..0000000 --- a/pkg/biscuit/signature.go +++ /dev/null @@ -1,177 +0,0 @@ -package biscuit - -import ( - "crypto" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "crypto/sha256" - "crypto/x509" - "errors" - "fmt" - "time" - - "github.com/flynn/biscuit-go" - "github.com/flynn/hubauth/pkg/kmssign" -) - -var ( - ErrUnsupportedSignatureAlg = errors.New("unsupported signature algorithm") - ErrInvalidSignature = errors.New("invalid signature") -) - -type SignatureAlg biscuit.Symbol - -const ( - ECDSA_P256_SHA256 SignatureAlg = "ECDSA_P256_SHA256" -) - -type userToSignData struct { - DataID biscuit.Integer - Alg biscuit.Symbol - Data biscuit.Bytes -} - -type userSignatureData struct { - DataID biscuit.Integer - UserPubKey biscuit.Bytes - Signature biscuit.Bytes - Nonce biscuit.Bytes - Timestamp biscuit.Date -} - -type userVerificationData struct { - DataID biscuit.Integer - Alg biscuit.Symbol - Data biscuit.Bytes - UserPubKey biscuit.Bytes - Signature biscuit.Bytes - Nonce biscuit.Bytes - Timestamp biscuit.Date -} - -func userSign(tokenHash []byte, userKey *UserKeyPair, toSignData *userToSignData) (*userSignatureData, error) { - if len(tokenHash) == 0 { - return nil, errors.New("invalid tokenHash") - } - - signerTimestamp := time.Now() - signerNonce := make([]byte, nonceSize) - if _, err := rand.Read(signerNonce); err != nil { - return nil, err - } - - var dataToSign []byte - dataToSign = append(dataToSign, toSignData.Data...) - dataToSign = append(dataToSign, tokenHash...) - dataToSign = append(dataToSign, signerNonce...) - dataToSign = append(dataToSign, []byte(signerTimestamp.Format(time.RFC3339))...) - - var signedData biscuit.Bytes - switch SignatureAlg(toSignData.Alg) { - case ECDSA_P256_SHA256: - privKey, err := x509.ParseECPrivateKey(userKey.Private) - if err != nil { - return nil, err - } - hash := sha256.Sum256(dataToSign) - signedData, err = ecdsa.SignASN1(rand.Reader, privKey, hash[:]) - if err != nil { - return nil, err - } - default: - return nil, ErrUnsupportedSignatureAlg - } - - return &userSignatureData{ - DataID: toSignData.DataID, - Nonce: signerNonce, - Signature: signedData, - Timestamp: biscuit.Date(signerTimestamp), - UserPubKey: userKey.Public, - }, nil -} - -func verifyUserSignature(signedTokenHash []byte, data *userVerificationData) error { - var signedData []byte - signedData = append(signedData, data.Data...) - signedData = append(signedData, signedTokenHash...) - signedData = append(signedData, data.Nonce...) - signedData = append(signedData, []byte(time.Time(data.Timestamp).Format(time.RFC3339))...) - - switch SignatureAlg(data.Alg) { - case ECDSA_P256_SHA256: - pk, err := x509.ParsePKIXPublicKey(data.UserPubKey) - if err != nil { - return err - } - pubkey, ok := pk.(*ecdsa.PublicKey) - if !ok { - return errors.New("invalid pubkey, not an *ecdsa.PublicKey") - } - - hash := sha256.Sum256(signedData) - if !ecdsa.VerifyASN1(pubkey, hash[:], data.Signature) { - return ErrInvalidSignature - } - return nil - default: - return ErrUnsupportedSignatureAlg - } -} - -type audienceVerificationData struct { - Audience biscuit.Symbol - Challenge biscuit.Bytes - Signature biscuit.Bytes -} - -func audienceSign(audience string, audienceKey *kmssign.Key) (*audienceVerificationData, error) { - challenge := make([]byte, challengeSize) - if _, err := rand.Reader.Read(challenge); err != nil { - return nil, err - } - - signedData := append(signStaticCtx, challenge...) - signedData = append(signedData, []byte(audience)...) - signedHash := sha256.Sum256(signedData) - signature, err := audienceKey.Sign(rand.Reader, signedHash[:], crypto.SHA256) - if err != nil { - return nil, err - } - - return &audienceVerificationData{ - Audience: biscuit.Symbol(audience), - Challenge: challenge, - Signature: signature, - }, nil -} - -func verifyAudienceSignature(audiencePubkey *ecdsa.PublicKey, data *audienceVerificationData) error { - signedData := append(signStaticCtx, data.Challenge...) - signedData = append(signedData, []byte(data.Audience)...) - hash := sha256.Sum256(signedData) - - if !ecdsa.VerifyASN1(audiencePubkey, hash[:], data.Signature) { - return errors.New("invalid signature") - } - return nil -} - -func validatePKIXP256PublicKey(pubkey []byte) error { - key, err := x509.ParsePKIXPublicKey(pubkey) - if err != nil { - return fmt.Errorf("failed to parse PKIX, ASN.1 DER public key: %v", err) - } - - ecKey, ok := key.(*ecdsa.PublicKey) - if !ok { - return errors.New("public key is not an *ecdsa.PublicKey") - } - - if ecKey.Curve != elliptic.P256() { - return fmt.Errorf("publickey is on wrong curve, expected P256") - } - - return nil -} diff --git a/pkg/biscuit/signature_test.go b/pkg/biscuit/signature_test.go deleted file mode 100644 index 2351e26..0000000 --- a/pkg/biscuit/signature_test.go +++ /dev/null @@ -1,230 +0,0 @@ -package biscuit - -import ( - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "testing" - "time" - - "github.com/flynn/biscuit-go" - "github.com/stretchr/testify/require" -) - -func TestUserSignVerify(t *testing.T) { - tokenHash := make([]byte, 32) - _, err := rand.Read(tokenHash) - require.NoError(t, err) - - challenge := make([]byte, challengeSize) - _, err = rand.Read(challenge) - require.NoError(t, err) - - userKey := generateUserKeyPair(t) - - toSignData := &userToSignData{ - DataID: 1, - Alg: biscuit.Symbol(ECDSA_P256_SHA256), - Data: []byte("challenge"), - } - - signedData, err := userSign(tokenHash, userKey, toSignData) - require.NoError(t, err) - require.NotEmpty(t, signedData.Signature) - require.Equal(t, biscuit.Integer(1), signedData.DataID) - require.Equal(t, biscuit.Bytes(userKey.Public), signedData.UserPubKey) - - require.Len(t, signedData.Nonce, nonceSize) - zeroNonce := make([]byte, nonceSize) - require.NotEqual(t, biscuit.Bytes(zeroNonce), signedData.Nonce) - - require.WithinDuration(t, time.Now(), time.Time(signedData.Timestamp), 1*time.Second) - - require.NoError(t, verifyUserSignature(tokenHash, &userVerificationData{ - DataID: toSignData.DataID, - Alg: toSignData.Alg, - Data: toSignData.Data, - Nonce: signedData.Nonce, - Signature: signedData.Signature, - Timestamp: signedData.Timestamp, - UserPubKey: signedData.UserPubKey, - })) -} - -func TestUserSignFail(t *testing.T) { - validTokenHash := make([]byte, 32) - _, err := rand.Read(validTokenHash) - require.NoError(t, err) - - validChallenge := make([]byte, challengeSize) - _, err = rand.Read(validChallenge) - require.NoError(t, err) - - invalidPrivateKey := &UserKeyPair{ - Private: make([]byte, 32), - } - - testCases := []struct { - desc string - tokenHash []byte - userKey *UserKeyPair - data *userToSignData - expectedErr error - }{ - { - desc: "empty tokenHash", - tokenHash: []byte{}, - }, - { - desc: "unsupported alg", - tokenHash: validTokenHash, - data: &userToSignData{ - Alg: "unsupported", - }, - expectedErr: ErrUnsupportedSignatureAlg, - }, - { - desc: "wrong private key encoding", - tokenHash: validTokenHash, - data: &userToSignData{ - Alg: biscuit.Symbol(ECDSA_P256_SHA256), - }, - userKey: invalidPrivateKey, - }, - } - - for _, testCase := range testCases { - t.Run(testCase.desc, func(t *testing.T) { - _, err := userSign(testCase.tokenHash, testCase.userKey, testCase.data) - require.Error(t, err) - if testCase.expectedErr != nil { - require.Equal(t, testCase.expectedErr, err) - } - }) - } -} - -func TestVerifyUserSignatureFail(t *testing.T) { - tokenHash := []byte("token hash") - toSignData := &userToSignData{ - DataID: 1, - Alg: biscuit.Symbol(ECDSA_P256_SHA256), - Data: []byte("challenge"), - } - - userKey := generateUserKeyPair(t) - invalidKey := generateUserKeyPair(t) - - signedData, err := userSign(tokenHash, userKey, toSignData) - require.NoError(t, err) - - rsaKey, err := rsa.GenerateKey(rand.Reader, 1024) - require.NoError(t, err) - wrongKeyKind, err := x509.MarshalPKIXPublicKey(&rsaKey.PublicKey) - require.NoError(t, err) - - testCases := []struct { - desc string - tokenHash []byte - data *userVerificationData - expectedErr error - }{ - { - desc: "unsupported alg", - expectedErr: ErrUnsupportedSignatureAlg, - data: &userVerificationData{ - Alg: "unknown", - }, - }, - { - desc: "invalid pubkey encoding", - data: &userVerificationData{ - Alg: biscuit.Symbol(ECDSA_P256_SHA256), - UserPubKey: make([]byte, 32), - }, - }, - { - desc: "invalid pubkey kind", - data: &userVerificationData{ - Alg: biscuit.Symbol(ECDSA_P256_SHA256), - UserPubKey: wrongKeyKind, - }, - }, - { - desc: "wrong pubkey", - tokenHash: tokenHash, - data: &userVerificationData{ - Alg: biscuit.Symbol(ECDSA_P256_SHA256), - UserPubKey: invalidKey.Public, - Data: toSignData.Data, - DataID: toSignData.DataID, - Nonce: signedData.Nonce, - Signature: signedData.Signature, - Timestamp: signedData.Timestamp, - }, - }, - { - desc: "tampered token hash", - expectedErr: ErrInvalidSignature, - tokenHash: []byte("wrong"), - data: &userVerificationData{ - Alg: biscuit.Symbol(ECDSA_P256_SHA256), - UserPubKey: userKey.Public, - Data: toSignData.Data, - DataID: toSignData.DataID, - Nonce: signedData.Nonce, - Signature: signedData.Signature, - Timestamp: signedData.Timestamp, - }, - }, - { - desc: "tampered nonce", - expectedErr: ErrInvalidSignature, - tokenHash: tokenHash, - data: &userVerificationData{ - Alg: biscuit.Symbol(ECDSA_P256_SHA256), - UserPubKey: userKey.Public, - Data: toSignData.Data, - DataID: toSignData.DataID, - Nonce: []byte("another nonce"), - Signature: signedData.Signature, - Timestamp: signedData.Timestamp, - }, - }, - { - desc: "tampered timestamp", - expectedErr: ErrInvalidSignature, - tokenHash: tokenHash, - data: &userVerificationData{ - Alg: biscuit.Symbol(ECDSA_P256_SHA256), - UserPubKey: userKey.Public, - Data: toSignData.Data, - DataID: toSignData.DataID, - Nonce: signedData.Nonce, - Signature: signedData.Signature, - Timestamp: biscuit.Date(time.Now().Add(1 * time.Second)), - }, - }, - } - - for _, testCase := range testCases { - t.Run(testCase.desc, func(t *testing.T) { - err := verifyUserSignature(testCase.tokenHash, testCase.data) - require.Error(t, err) - if testCase.expectedErr != nil { - require.Equal(t, testCase.expectedErr, err) - } - }) - } -} - -func generateUserKeyPair(t *testing.T) *UserKeyPair { - priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - require.NoError(t, err) - - kp, err := NewECDSAKeyPair(priv) - require.NoError(t, err) - return kp -} diff --git a/pkg/biscuit/wrapper.go b/pkg/biscuit/wrapper.go deleted file mode 100644 index f39681f..0000000 --- a/pkg/biscuit/wrapper.go +++ /dev/null @@ -1,426 +0,0 @@ -package biscuit - -import ( - "bytes" - "crypto/rand" - "errors" - "fmt" - "time" - - "github.com/flynn/biscuit-go" - "github.com/flynn/biscuit-go/datalog" - "github.com/flynn/hubauth/pkg/kmssign" -) - -var ( - ErrAlreadySigned = errors.New("already signed") - ErrInvalidToSignDataPrefix = errors.New("invalid to_sign data prefix") -) - -var ( - signStaticCtx = []byte("biscuit-pop-v0") - challengeSize = 16 - nonceSize = 16 -) - -type hubauthBuilder struct { - biscuit.Builder -} - -// withUserToSignFact add an authority should_sign fact and associated data to the biscuit -// with an authority caveat requiring the verifier to provide a valid_signature fact. -// the verifier is responsible of ensuring that a valid signature exists over the data. -func (b *hubauthBuilder) withUserToSignFact(userPubkey []byte) error { - dataID := biscuit.Integer(0) - - if err := validatePKIXP256PublicKey(userPubkey); err != nil { - return err - } - - if err := b.AddAuthorityFact(biscuit.Fact{Predicate: biscuit.Predicate{ - Name: "should_sign", - IDs: []biscuit.Atom{ - dataID, - biscuit.Symbol(ECDSA_P256_SHA256), - biscuit.Bytes(userPubkey), - }, - }}); err != nil { - return err - } - - challenge := make([]byte, challengeSize) - if _, err := rand.Reader.Read(challenge); err != nil { - return err - } - - if err := b.AddAuthorityFact(biscuit.Fact{Predicate: biscuit.Predicate{ - Name: "data", - IDs: []biscuit.Atom{ - dataID, - biscuit.Bytes(append(signStaticCtx, challenge...)), - }, - }}); err != nil { - return err - } - - if err := b.AddAuthorityCaveat(biscuit.Rule{ - Head: biscuit.Predicate{Name: "valid", IDs: []biscuit.Atom{biscuit.Variable(0)}}, - Body: []biscuit.Predicate{ - {Name: "should_sign", IDs: []biscuit.Atom{biscuit.SymbolAuthority, biscuit.Variable(0), biscuit.Variable(1), biscuit.Variable(2)}}, - {Name: "valid_signature", IDs: []biscuit.Atom{biscuit.Symbol("ambient"), biscuit.Variable(0), biscuit.Variable(1), biscuit.Variable(2)}}, - }, - }); err != nil { - return err - } - - return nil -} - -// withAudienceSignature add an authority audience_signature fact, containing a challenge and -// a matching signature using the audience key. -// the verifier is responsible of providing a valid_audience_signature fact, after -// verifying the signature using the audience pubkey. -func (b *hubauthBuilder) withAudienceSignature(audience string, audienceKey *kmssign.Key) error { - if len(audience) == 0 { - return errors.New("audience is required") - } - - data, err := audienceSign(audience, audienceKey) - if err != nil { - return err - } - - if err := b.AddAuthorityFact(biscuit.Fact{Predicate: biscuit.Predicate{ - Name: "audience_signature", - IDs: []biscuit.Atom{ - data.Audience, - data.Challenge, - data.Signature, - }, - }}); err != nil { - return err - } - - if err := b.AddAuthorityCaveat(biscuit.Rule{ - Head: biscuit.Predicate{Name: "valid_audience", IDs: []biscuit.Atom{biscuit.Variable(0)}}, - Body: []biscuit.Predicate{ - {Name: "audience_signature", IDs: []biscuit.Atom{biscuit.SymbolAuthority, biscuit.Variable(0), biscuit.Variable(1), biscuit.Variable(2)}}, - {Name: "valid_audience_signature", IDs: []biscuit.Atom{biscuit.Symbol("ambient"), biscuit.Variable(0), biscuit.Variable(2)}}, - }, - }); err != nil { - return err - } - - return nil -} - -func (b *hubauthBuilder) withMetadata(m *Metadata) error { - return b.AddAuthorityFact(biscuit.Fact{Predicate: biscuit.Predicate{ - Name: "metadata", - IDs: []biscuit.Atom{ - biscuit.String(m.ClientID), - biscuit.String(m.UserID), - biscuit.String(m.UserEmail), - biscuit.Date(m.IssueTime), - }, - }}) -} - -func (b *hubauthBuilder) withExpire(exp time.Time) error { - if err := b.AddAuthorityCaveat(biscuit.Rule{ - Head: biscuit.Predicate{Name: "not_expired", IDs: []biscuit.Atom{biscuit.Variable(0)}}, - Body: []biscuit.Predicate{ - {Name: "current_time", IDs: []biscuit.Atom{biscuit.Symbol("ambient"), biscuit.Variable(0)}}, - }, - Constraints: []biscuit.Constraint{{ - Name: biscuit.Variable(0), - Checker: biscuit.DateComparisonChecker{ - Comparison: datalog.DateComparisonBefore, - Date: biscuit.Date(exp), - }, - }}, - }); err != nil { - return err - } - - return nil -} - -type hubauthBlockBuilder struct { - biscuit.BlockBuilder -} - -func (b *hubauthBlockBuilder) withUserSignature(sigData *userSignatureData) error { - return b.AddFact(biscuit.Fact{Predicate: biscuit.Predicate{ - Name: "signature", - IDs: []biscuit.Atom{ - sigData.DataID, - sigData.UserPubKey, - sigData.Signature, - sigData.Nonce, - sigData.Timestamp, - }, - }}) -} - -type hubauthVerifier struct { - biscuit.Verifier -} - -func (v *hubauthVerifier) getUserToSignData(userPubKey biscuit.Bytes) (*userToSignData, error) { - toSign, err := v.Query(biscuit.Rule{ - Head: biscuit.Predicate{ - Name: "to_sign", - IDs: []biscuit.Atom{biscuit.Variable(0), biscuit.Variable(1), biscuit.Variable(2)}, - }, - Body: []biscuit.Predicate{ - { - Name: "should_sign", IDs: []biscuit.Atom{ - biscuit.SymbolAuthority, - biscuit.Variable(0), - biscuit.Variable(1), - biscuit.Bytes(userPubKey), - }, - }, { - Name: "data", IDs: []biscuit.Atom{ - biscuit.SymbolAuthority, - biscuit.Variable(0), - biscuit.Variable(2), - }, - }, - }, - }) - if err != nil { - return nil, err - } - - if g, w := len(toSign), 1; g != w { - return nil, fmt.Errorf("invalid to_sign fact count, got %d, want %d", g, w) - } - - toSignFact := toSign[0] - if g, w := len(toSignFact.IDs), 3; g != w { - return nil, fmt.Errorf("invalid to_sign fact, got %d atoms, want %d", g, w) - } - - sigData := &userToSignData{} - var ok bool - sigData.DataID, ok = toSign[0].IDs[0].(biscuit.Integer) - if !ok { - return nil, errors.New("invalid to_sign atom: dataID") - } - sigData.Alg, ok = toSign[0].IDs[1].(biscuit.Symbol) - if !ok { - return nil, errors.New("invalid to_sign atom: alg") - } - sigData.Data, ok = toSign[0].IDs[2].(biscuit.Bytes) - if !ok { - return nil, errors.New("invalid to_sign atom: data") - } - - if !bytes.HasPrefix(sigData.Data, signStaticCtx) { - return nil, ErrInvalidToSignDataPrefix - } - - return sigData, nil -} - -func (v *hubauthVerifier) ensureNotAlreadyUserSigned(dataID biscuit.Integer, userPubKey biscuit.Bytes) error { - alreadySigned, err := v.Query(biscuit.Rule{ - Head: biscuit.Predicate{Name: "already_signed", IDs: []biscuit.Atom{biscuit.Variable(0)}}, - Body: []biscuit.Predicate{ - {Name: "signature", IDs: []biscuit.Atom{dataID, userPubKey, biscuit.Variable(0)}}, - }, - }) - if err != nil { - return err - } - if len(alreadySigned) != 0 { - return ErrAlreadySigned - } - - return nil -} - -func (v *hubauthVerifier) getUserVerificationData() (*userVerificationData, error) { - toValidate, err := v.Query(biscuit.Rule{ - Head: biscuit.Predicate{ - Name: "to_validate", - IDs: []biscuit.Atom{ - biscuit.Variable(0), // dataID - biscuit.Variable(1), // alg - biscuit.Variable(2), // pubkey - biscuit.Variable(3), // data - biscuit.Variable(4), // signature - biscuit.Variable(5), // signerNonce - biscuit.Variable(6), // signerTimestamp - }}, - Body: []biscuit.Predicate{ - {Name: "should_sign", IDs: []biscuit.Atom{biscuit.SymbolAuthority, biscuit.Variable(0), biscuit.Variable(1), biscuit.Variable(2)}}, - {Name: "data", IDs: []biscuit.Atom{biscuit.SymbolAuthority, biscuit.Variable(0), biscuit.Variable(3)}}, - {Name: "signature", IDs: []biscuit.Atom{biscuit.Variable(0), biscuit.Variable(2), biscuit.Variable(4), biscuit.Variable(5), biscuit.Variable(6)}}, - }, - }) - if err != nil { - return nil, err - } - - if g, w := len(toValidate), 1; g != w { - return nil, fmt.Errorf("invalid to_validate fact count, got %d, want %d", g, w) - } - - toValidateFact := toValidate[0] - if g, w := len(toValidateFact.IDs), 7; g != w { - return nil, fmt.Errorf("invalid to_valid fact atom count, got %d, want %d", g, w) - } - - toVerify := &userVerificationData{} - var ok bool - toVerify.DataID, ok = toValidateFact.IDs[0].(biscuit.Integer) - if !ok { - return nil, errors.New("invalid to_validate atom: dataID") - } - toVerify.Alg, ok = toValidateFact.IDs[1].(biscuit.Symbol) - if !ok { - return nil, errors.New("invalid to_validate atom: alg") - } - toVerify.UserPubKey, ok = toValidateFact.IDs[2].(biscuit.Bytes) - if !ok { - return nil, errors.New("invalid to_validate atom: userPubKey") - } - toVerify.Data, ok = toValidateFact.IDs[3].(biscuit.Bytes) - if !ok { - return nil, errors.New("invalid to_validate atom: data") - } - toVerify.Signature, ok = toValidateFact.IDs[4].(biscuit.Bytes) - if !ok { - return nil, errors.New("invalid to_validate atom: signature") - } - toVerify.Nonce, ok = toValidateFact.IDs[5].(biscuit.Bytes) - if !ok { - return nil, errors.New("invalid to_validate atom: nonce") - } - toVerify.Timestamp, ok = toValidateFact.IDs[6].(biscuit.Date) - if !ok { - return nil, errors.New("invalid to_validate atom: timestamp") - } - - return toVerify, nil -} - -func (v *hubauthVerifier) withValidatedUserSignature(data *userVerificationData) error { - v.AddFact(biscuit.Fact{Predicate: biscuit.Predicate{ - Name: "valid_signature", - IDs: []biscuit.Atom{biscuit.Symbol("ambient"), data.DataID, data.Alg, data.UserPubKey}, - }}) - - return nil -} - -func (v *hubauthVerifier) getAudienceVerificationData(audience string) (*audienceVerificationData, error) { - toValidate, err := v.Query(biscuit.Rule{ - Head: biscuit.Predicate{ - Name: "audience_to_validate", - IDs: []biscuit.Atom{ - biscuit.Variable(0), // challenge - biscuit.Variable(1), // signature - }}, - Body: []biscuit.Predicate{ - {Name: "audience_signature", IDs: []biscuit.Atom{biscuit.SymbolAuthority, biscuit.Symbol(audience), biscuit.Variable(0), biscuit.Variable(1)}}, - }, - }) - if err != nil { - return nil, err - } - - if g, w := len(toValidate), 1; g != w { - return nil, fmt.Errorf("invalid audience_to_validate fact count, got %d, want %d", g, w) - } - - toValidateFact := toValidate[0] - if g, w := len(toValidateFact.IDs), 2; g != w { - return nil, fmt.Errorf("invalid audience_to_validate fact atom count, got %d, want %d", g, w) - } - - toVerify := &audienceVerificationData{Audience: biscuit.Symbol(audience)} - var ok bool - toVerify.Challenge, ok = toValidateFact.IDs[0].(biscuit.Bytes) - if !ok { - return nil, errors.New("invalid audience_to_validate atom: challenge") - } - toVerify.Signature, ok = toValidateFact.IDs[1].(biscuit.Bytes) - if !ok { - return nil, errors.New("invalid audience_to_validate atom: signature") - } - - return toVerify, nil -} - -func (v *hubauthVerifier) getMetadata() (*Metadata, error) { - metaFacts, err := v.Query(biscuit.Rule{ - Head: biscuit.Predicate{ - Name: "metadata", - IDs: []biscuit.Atom{ - biscuit.Variable(0), // clientID - biscuit.Variable(1), // userID - biscuit.Variable(2), // userEmail - biscuit.Variable(3), // issueTime - }}, - Body: []biscuit.Predicate{ - {Name: "metadata", IDs: []biscuit.Atom{biscuit.SymbolAuthority, biscuit.Variable(0), biscuit.Variable(1), biscuit.Variable(2), biscuit.Variable(3)}}, - }, - }) - if err != nil { - return nil, err - } - - if g, w := len(metaFacts), 1; g != w { - return nil, fmt.Errorf("invalid metadata fact count, got %d, want %d", g, w) - } - - metaFact := metaFacts[0] - - clientID, ok := metaFact.IDs[0].(biscuit.String) - if !ok { - return nil, errors.New("invalid metadata atom: clientID") - } - userID, ok := metaFact.IDs[1].(biscuit.String) - if !ok { - return nil, errors.New("invalid metadata atom: userID") - } - userEmail, ok := metaFact.IDs[2].(biscuit.String) - if !ok { - return nil, errors.New("invalid metadata atom: userEmail") - } - issueTime, ok := metaFact.IDs[3].(biscuit.Date) - if !ok { - return nil, errors.New("invalid metadata atom: issueTime") - } - return &Metadata{ - ClientID: string(clientID), - UserID: string(userID), - UserEmail: string(userEmail), - IssueTime: time.Time(issueTime), - }, nil -} - -func (v *hubauthVerifier) withValidatedAudienceSignature(data *audienceVerificationData) error { - v.AddFact(biscuit.Fact{Predicate: biscuit.Predicate{ - Name: "valid_audience_signature", - IDs: []biscuit.Atom{biscuit.Symbol("ambient"), data.Audience, data.Signature}, - }}) - - return nil -} - -func (v *hubauthVerifier) withCurrentTime(t time.Time) error { - v.AddFact(biscuit.Fact{Predicate: biscuit.Predicate{ - Name: "current_time", - IDs: []biscuit.Atom{ - biscuit.Symbol("ambient"), - biscuit.Date(t), - }, - }}) - - return nil -} diff --git a/pkg/idp/token/biscuit.go b/pkg/idp/token/biscuit.go index 7736391..9c997be 100644 --- a/pkg/idp/token/biscuit.go +++ b/pkg/idp/token/biscuit.go @@ -7,8 +7,8 @@ import ( "errors" "fmt" + "github.com/flynn/biscuit-go/cookbook/signedbiscuit" "github.com/flynn/biscuit-go/sig" - "github.com/flynn/hubauth/pkg/biscuit" "github.com/flynn/hubauth/pkg/kmssign" ) @@ -36,14 +36,14 @@ func (b *biscuitBuilder) Build(ctx context.Context, audience string, t *AccessTo } audienceKey := kmssign.NewPrivateKey(b.kms, b.audienceKey(audience), crypto.SHA256) - meta := &biscuit.Metadata{ + meta := &signedbiscuit.Metadata{ ClientID: t.ClientID, UserID: t.UserID, UserEmail: t.UserEmail, IssueTime: t.IssueTime, } - return biscuit.GenerateSignable(b.rootKeyPair, audience, audienceKey, t.UserPublicKey, t.ExpireTime, meta) + return signedbiscuit.GenerateSignable(b.rootKeyPair, audience, audienceKey, t.UserPublicKey, t.ExpireTime, meta) } func (b *biscuitBuilder) TokenType() string { From 249a3e98109eb96f977216c77a0bbde1ee879a2b Mon Sep 17 00:00:00 2001 From: Flavien Binet Date: Tue, 20 Oct 2020 13:59:21 +0200 Subject: [PATCH 17/32] go mod tidy --- go.sum | 1 - 1 file changed, 1 deletion(-) diff --git a/go.sum b/go.sum index ce60913..f37b6fd 100644 --- a/go.sum +++ b/go.sum @@ -78,7 +78,6 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/flynn/biscuit-go v0.0.0-20201015071554-9ea2c863efe8 h1:jz3x+pevBqZiO9m7M58fN/W8K0OyDUfchIsHE8XgZT0= github.com/flynn/biscuit-go v0.0.0-20201015081742-15d7d351f345 h1:ME6bm5dwn9V2DUlfXJqeN121B5nM7rDFqLFOATALqYE= github.com/flynn/biscuit-go v0.0.0-20201015081742-15d7d351f345/go.mod h1:Sj4oR2hNkrZH1cf3Cj5DPHc3Xq0o61GWeau6UkZR+3c= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= From 3c32c13ede9092320af6a041658f49ce59e70731 Mon Sep 17 00:00:00 2001 From: daeMOn Date: Thu, 8 Oct 2020 16:53:04 +0200 Subject: [PATCH 18/32] add biscuit wrappers and helpers to generate, sign and verify hubauth biscuits --- go.sum | 16 +- pkg/biscuit/biscuit.go | 137 ++++++++++++++ pkg/biscuit/biscuit_test.go | 57 ++++++ pkg/biscuit/signature.go | 162 ++++++++++++++++ pkg/biscuit/signature_test.go | 258 ++++++++++++++++++++++++++ pkg/biscuit/wrapper.go | 336 ++++++++++++++++++++++++++++++++++ 6 files changed, 952 insertions(+), 14 deletions(-) create mode 100644 pkg/biscuit/biscuit.go create mode 100644 pkg/biscuit/biscuit_test.go create mode 100644 pkg/biscuit/signature.go create mode 100644 pkg/biscuit/signature_test.go create mode 100644 pkg/biscuit/wrapper.go diff --git a/go.sum b/go.sum index f37b6fd..45ab81b 100644 --- a/go.sum +++ b/go.sum @@ -55,6 +55,8 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym github.com/alecthomas/kong v0.2.12 h1:X3kkCOXGUNzLmiu+nQtoxWqj4U2a39MpSJR3QdQXOwI= github.com/alecthomas/kong v0.2.12/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE= github.com/alecthomas/participle v0.6.0/go.mod h1:HfdmEuwvr12HXQN44HPWXR0lHmVolVYe4dyL6lQ3duY= +github.com/alecthomas/participle v0.6.0/go.mod h1:HfdmEuwvr12HXQN44HPWXR0lHmVolVYe4dyL6lQ3duY= +github.com/alecthomas/repr v0.0.0-20181024024818-d37bc2a10ba1/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= github.com/alecthomas/repr v0.0.0-20181024024818-d37bc2a10ba1/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= github.com/aws/aws-sdk-go v1.23.20 h1:2CBuL21P0yKdZN5urf2NxKa1ha8fhnY+A3pBCHFeZoA= github.com/aws/aws-sdk-go v1.23.20/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= @@ -69,7 +71,6 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -108,7 +109,6 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= @@ -121,11 +121,9 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -231,7 +229,6 @@ golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 h1:2M3HP5CCK1Si9FQhwnzYhXdG6DXeebvUHFpre8QvbyI= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= @@ -242,7 +239,6 @@ golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0 h1:8pl+sMODzuvGJkmj2W4kZihvVb5mKm8pB/X44PIQHv8= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -282,7 +278,6 @@ golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4Iltr golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 h1:ld7aEMNHoBnnDAX15v1T6z31v8HwR2A9FYOuAhWqkwc= golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5 h1:Lm4OryKCca1vehdsWogr9N4t7NfZxLbJoc/H0w4K4S4= @@ -294,7 +289,6 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -324,9 +318,7 @@ golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f h1:Fqb3ao1hUmOR3GkUOg/Y+BadLwykBIzs5q8Ez2SbHyc= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3 h1:kzM6+9dur93BcC2kVlYl34cHU+TYZLanmpSJHVMmL64= @@ -336,7 +328,6 @@ golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fq golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -414,7 +405,6 @@ google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.32.0 h1:Le77IccnTqEa8ryp9wIpX5W3zYm7Gf9LhOp9PHcwFts= google.golang.org/api v0.32.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= google.golang.org/api v0.36.0 h1:l2Nfbl2GPXdWorv+dT2XfinX2jOOw4zv1VhLstx+6rE= @@ -425,7 +415,6 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.2/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= @@ -478,7 +467,6 @@ google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3Iji google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.32.0 h1:zWTV+LMdc3kaiJMSTOFz2UgSBgx8RNQoTGiZu3fR9S0= google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0 h1:raiipEjMOIC/TO2AvyTxP25XFdLxNIBwzDh3FM3XztI= diff --git a/pkg/biscuit/biscuit.go b/pkg/biscuit/biscuit.go new file mode 100644 index 0000000..ef1a452 --- /dev/null +++ b/pkg/biscuit/biscuit.go @@ -0,0 +1,137 @@ +package biscuit + +import ( + "crypto/rand" + "fmt" + + "github.com/flynn/biscuit-go" + "github.com/flynn/biscuit-go/sig" + "github.com/flynn/hubauth/pkg/kmssign" +) + +type UserKeyPair struct { + Public []byte + Private []byte +} + +// GenerateSignable returns a biscuit which will only verify after being +// signed with the private key matching the given userPubkey. +func GenerateSignable(rootKey sig.Keypair, audience string, audienceKey *kmssign.Key, userPubkey []byte) ([]byte, error) { + builder := &hubauthBuilder{ + Builder: biscuit.NewBuilder(rand.Reader, rootKey), + } + + if err := builder.withAudienceSignature(audience, audienceKey); err != nil { + return nil, err + } + + if err := builder.withUserToSignFact(userPubkey); err != nil { + return nil, err + } + + b, err := builder.Build() + if err != nil { + return nil, err + } + + return b.Serialize() +} + +// Sign append a user signature on the given token and return it. +// The UserKeyPair key format to provide depends on the signature algorithm: +// - for ECDSA_P256_SHA256, the private key must be encoded in SEC 1, ASN.1 DER form, +// and the public key in PKIX, ASN.1 DER form. +func Sign(token []byte, rootPubKey sig.PublicKey, userKey *UserKeyPair) ([]byte, error) { + b, err := biscuit.Unmarshal(token) + if err != nil { + return nil, fmt.Errorf("biscuit: failed to unmarshal: %w", err) + } + + v, err := b.Verify(rootPubKey) + if err != nil { + return nil, fmt.Errorf("biscuit: failed to verify: %w", err) + } + verifier := &hubauthVerifier{ + Verifier: v, + } + + toSignData, err := verifier.getUserToSignData(userKey.Public, b.BlockCount()) + if err != nil { + return nil, fmt.Errorf("biscuit: failed to get to_sign data: %w", err) + } + + if err := verifier.ensureNotAlreadyUserSigned(toSignData.DataID, userKey.Public); err != nil { + return nil, fmt.Errorf("biscuit: previous signature check failed: %w", err) + } + + tokenHash, err := b.SHA256Sum(b.BlockCount()) + if err != nil { + return nil, err + } + + signData, err := userSign(tokenHash, userKey, toSignData) + if err != nil { + return nil, fmt.Errorf("biscuit: signature failed: %w", err) + } + + builder := &hubauthBlockBuilder{ + BlockBuilder: b.CreateBlock(), + } + if err := builder.withUserSignature(signData); err != nil { + return nil, fmt.Errorf("biscuit: failed to create signature block: %w", err) + } + + clientKey := sig.GenerateKeypair(rand.Reader) + b, err = b.Append(rand.Reader, clientKey, builder.Build()) + if err != nil { + return nil, fmt.Errorf("biscuit: failed to append signature block: %w", err) + } + + return b.Serialize() +} + +// Verify will verify the biscuit, the included audience and user signature, and return an error +// when anything is invalid. +func Verify(token []byte, rootPubKey sig.PublicKey, audience string, audienceKey *kmssign.Key) error { + b, err := biscuit.Unmarshal(token) + if err != nil { + return fmt.Errorf("biscuit: failed to unmarshal: %w", err) + } + + v, err := b.Verify(rootPubKey) + if err != nil { + return fmt.Errorf("biscuit: failed to verify: %w", err) + } + verifier := &hubauthVerifier{v} + + audienceVerificationData, err := verifier.getAudienceVerificationData(audience) + if err != nil { + return fmt.Errorf("biscuit: failed to retrieve audience signature data: %w", err) + } + + if err := verifyAudienceSignature(audienceKey, audienceVerificationData); err != nil { + return fmt.Errorf("biscuit: failed to verify audience signature: %w", err) + } + if err := verifier.withValidatedAudienceSignature(audienceVerificationData); err != nil { + return fmt.Errorf("biscuit: failed to add validated signature: %w", err) + } + + userVerificationData, err := verifier.getUserVerificationData() + if err != nil { + return fmt.Errorf("biscuit: failed to retrieve user signature data: %w", err) + } + + signedTokenHash, err := b.SHA256Sum(int(userVerificationData.SignedBlockCount)) + if err != nil { + return fmt.Errorf("biscuit: failed to generate token hash: %w", err) + } + + if err := verifyUserSignature(signedTokenHash, userVerificationData); err != nil { + return fmt.Errorf("biscuit: failed to verify user signature: %w", err) + } + if err := verifier.withValidatedUserSignature(userVerificationData); err != nil { + return fmt.Errorf("biscuit: failed to add validated signature: %w", err) + } + + return verifier.Verify() +} diff --git a/pkg/biscuit/biscuit_test.go b/pkg/biscuit/biscuit_test.go new file mode 100644 index 0000000..ed7b273 --- /dev/null +++ b/pkg/biscuit/biscuit_test.go @@ -0,0 +1,57 @@ +package biscuit + +import ( + "context" + "crypto/rand" + "testing" + + "github.com/flynn/biscuit-go/sig" + "github.com/flynn/hubauth/pkg/kmssign" + "github.com/flynn/hubauth/pkg/kmssign/kmssim" + "github.com/stretchr/testify/require" +) + +func TestBiscuit(t *testing.T) { + rootKey := sig.GenerateKeypair(rand.Reader) + audience := "http://random.audience.url" + + kms := kmssim.NewClient([]string{audience}) + audienceKey, err := kmssign.NewKey(context.Background(), kms, audience) + require.NoError(t, err) + + userKey := generateUserKeyPair(t) + + signableBiscuit, err := GenerateSignable(rootKey, audience, audienceKey, userKey.Public) + require.NoError(t, err) + t.Logf("signable biscuit size: %d", len(signableBiscuit)) + + t.Run("happy path", func(t *testing.T) { + signedBiscuit, err := Sign(signableBiscuit, rootKey.Public(), userKey) + require.NoError(t, err) + t.Logf("signed biscuit size: %d", len(signedBiscuit)) + + err = Verify(signedBiscuit, rootKey.Public(), audience, audienceKey) + require.NoError(t, err) + }) + + t.Run("user sign with wrong key", func(t *testing.T) { + _, err := Sign(signableBiscuit, rootKey.Public(), generateUserKeyPair(t)) + require.Error(t, err) + }) + + t.Run("verify wrong audience", func(t *testing.T) { + signedBiscuit, err := Sign(signableBiscuit, rootKey.Public(), userKey) + require.NoError(t, err) + + err = Verify(signedBiscuit, rootKey.Public(), "http://another.audience.url", audienceKey) + require.Error(t, err) + + wrongAudience := "http://another.audience.url" + kms := kmssim.NewClient([]string{wrongAudience}) + wrongAudienceKey, err := kmssign.NewKey(context.Background(), kms, wrongAudience) + require.NoError(t, err) + + err = Verify(signedBiscuit, rootKey.Public(), audience, wrongAudienceKey) + require.Error(t, err) + }) +} diff --git a/pkg/biscuit/signature.go b/pkg/biscuit/signature.go new file mode 100644 index 0000000..385e4bf --- /dev/null +++ b/pkg/biscuit/signature.go @@ -0,0 +1,162 @@ +package biscuit + +import ( + "crypto" + "crypto/ecdsa" + "crypto/rand" + "crypto/sha256" + "crypto/x509" + "errors" + "time" + + "github.com/flynn/biscuit-go" + "github.com/flynn/hubauth/pkg/kmssign" +) + +var ( + ErrUnsupportedSignatureAlg = errors.New("unsupported signature algorithm") + ErrInvalidSignature = errors.New("invalid signature") +) + +type SignatureAlg biscuit.Symbol + +const ( + ECDSA_P256_SHA256 SignatureAlg = "ECDSA_P256_SHA256" +) + +type userToSignData struct { + DataID biscuit.Integer + Alg biscuit.Symbol + Data biscuit.Bytes + SignedBlockCount biscuit.Integer +} + +type userSignatureData struct { + DataID biscuit.Integer + UserPubKey biscuit.Bytes + Signature biscuit.Bytes + SignedBlockCount biscuit.Integer + Nonce biscuit.Bytes + Timestamp biscuit.Date +} + +type userVerificationData struct { + DataID biscuit.Integer + Alg biscuit.Symbol + Data biscuit.Bytes + UserPubKey biscuit.Bytes + Signature biscuit.Bytes + SignedBlockCount biscuit.Integer + Nonce biscuit.Bytes + Timestamp biscuit.Date +} + +func userSign(tokenHash []byte, userKey *UserKeyPair, toSignData *userToSignData) (*userSignatureData, error) { + if len(tokenHash) == 0 { + return nil, errors.New("invalid tokenHash") + } + + signerTimestamp := time.Now() + signerNonce := make([]byte, nonceSize) + if _, err := rand.Read(signerNonce); err != nil { + return nil, err + } + + var dataToSign []byte + dataToSign = append(dataToSign, toSignData.Data...) + dataToSign = append(dataToSign, tokenHash...) + dataToSign = append(dataToSign, signerNonce...) + dataToSign = append(dataToSign, []byte(signerTimestamp.Format(time.RFC3339))...) + dataToSign = append(dataToSign, []byte(toSignData.SignedBlockCount.String())...) + + var signedData biscuit.Bytes + switch SignatureAlg(toSignData.Alg) { + case ECDSA_P256_SHA256: + privKey, err := x509.ParseECPrivateKey(userKey.Private) + if err != nil { + return nil, err + } + hash := sha256.Sum256(dataToSign) + signedData, err = ecdsa.SignASN1(rand.Reader, privKey, hash[:]) + if err != nil { + return nil, err + } + default: + return nil, ErrUnsupportedSignatureAlg + } + + return &userSignatureData{ + DataID: toSignData.DataID, + Nonce: signerNonce, + Signature: signedData, + SignedBlockCount: toSignData.SignedBlockCount, + Timestamp: biscuit.Date(signerTimestamp), + UserPubKey: userKey.Public, + }, nil +} + +func verifyUserSignature(signedTokenHash []byte, data *userVerificationData) error { + var signedData []byte + signedData = append(signedData, data.Data...) + signedData = append(signedData, signedTokenHash...) + signedData = append(signedData, data.Nonce...) + signedData = append(signedData, []byte(time.Time(data.Timestamp).Format(time.RFC3339))...) + signedData = append(signedData, []byte(data.SignedBlockCount.String())...) + + switch SignatureAlg(data.Alg) { + case ECDSA_P256_SHA256: + pk, err := x509.ParsePKIXPublicKey(data.UserPubKey) + if err != nil { + return err + } + pubkey, ok := pk.(*ecdsa.PublicKey) + if !ok { + return errors.New("invalid pubkey, not an *ecdsa.PublicKey") + } + + hash := sha256.Sum256(signedData) + if !ecdsa.VerifyASN1(pubkey, hash[:], data.Signature) { + return ErrInvalidSignature + } + return nil + default: + return ErrUnsupportedSignatureAlg + } +} + +type audienceVerificationData struct { + Audience biscuit.Symbol + Challenge biscuit.Bytes + Signature biscuit.Bytes +} + +func audienceSign(audience string, audienceKey *kmssign.Key) (*audienceVerificationData, error) { + challenge := make([]byte, challengeSize) + if _, err := rand.Reader.Read(challenge); err != nil { + return nil, err + } + + signedData := append(signStaticCtx, challenge...) + signedData = append(signedData, []byte(audience)...) + signedHash := sha256.Sum256(signedData) + signature, err := audienceKey.Sign(rand.Reader, signedHash[:], crypto.SHA256) + if err != nil { + return nil, err + } + + return &audienceVerificationData{ + Audience: biscuit.Symbol(audience), + Challenge: challenge, + Signature: signature, + }, nil +} + +func verifyAudienceSignature(audiencePubkey *kmssign.Key, data *audienceVerificationData) error { + signedData := append(signStaticCtx, data.Challenge...) + signedData = append(signedData, []byte(data.Audience)...) + hash := sha256.Sum256(signedData) + if !audiencePubkey.Verify(hash[:], data.Signature) { + return errors.New("invalid signature") + } + return nil +} diff --git a/pkg/biscuit/signature_test.go b/pkg/biscuit/signature_test.go new file mode 100644 index 0000000..5cb5d8b --- /dev/null +++ b/pkg/biscuit/signature_test.go @@ -0,0 +1,258 @@ +package biscuit + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "testing" + "time" + + "github.com/flynn/biscuit-go" + "github.com/stretchr/testify/require" +) + +func TestUserSignVerify(t *testing.T) { + tokenHash := make([]byte, 32) + _, err := rand.Read(tokenHash) + require.NoError(t, err) + + challenge := make([]byte, challengeSize) + _, err = rand.Read(challenge) + require.NoError(t, err) + + userKey := generateUserKeyPair(t) + + toSignData := &userToSignData{ + DataID: 1, + Alg: biscuit.Symbol(ECDSA_P256_SHA256), + Data: []byte("challenge"), + SignedBlockCount: 2, + } + + signedData, err := userSign(tokenHash, userKey, toSignData) + require.NoError(t, err) + require.NotEmpty(t, signedData.Signature) + require.Equal(t, biscuit.Integer(2), signedData.SignedBlockCount) + require.Equal(t, biscuit.Integer(1), signedData.DataID) + require.Equal(t, biscuit.Bytes(userKey.Public), signedData.UserPubKey) + + require.Len(t, signedData.Nonce, nonceSize) + zeroNonce := make([]byte, nonceSize) + require.NotEqual(t, biscuit.Bytes(zeroNonce), signedData.Nonce) + + require.WithinDuration(t, time.Now(), time.Time(signedData.Timestamp), 1*time.Second) + + require.NoError(t, verifyUserSignature(tokenHash, &userVerificationData{ + DataID: toSignData.DataID, + Alg: toSignData.Alg, + Data: toSignData.Data, + Nonce: signedData.Nonce, + Signature: signedData.Signature, + SignedBlockCount: signedData.SignedBlockCount, + Timestamp: signedData.Timestamp, + UserPubKey: signedData.UserPubKey, + })) +} + +func TestUserSignFail(t *testing.T) { + validTokenHash := make([]byte, 32) + _, err := rand.Read(validTokenHash) + require.NoError(t, err) + + validChallenge := make([]byte, challengeSize) + _, err = rand.Read(validChallenge) + require.NoError(t, err) + + invalidPrivateKey := &UserKeyPair{ + Private: make([]byte, 32), + } + + testCases := []struct { + desc string + tokenHash []byte + userKey *UserKeyPair + data *userToSignData + expectedErr error + }{ + { + desc: "empty tokenHash", + tokenHash: []byte{}, + }, + { + desc: "unsupported alg", + tokenHash: validTokenHash, + data: &userToSignData{ + Alg: "unsupported", + }, + expectedErr: ErrUnsupportedSignatureAlg, + }, + { + desc: "wrong private key encoding", + tokenHash: validTokenHash, + data: &userToSignData{ + Alg: biscuit.Symbol(ECDSA_P256_SHA256), + }, + userKey: invalidPrivateKey, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.desc, func(t *testing.T) { + _, err := userSign(testCase.tokenHash, testCase.userKey, testCase.data) + require.Error(t, err) + if testCase.expectedErr != nil { + require.Equal(t, testCase.expectedErr, err) + } + }) + } +} + +func TestVerifyUserSignatureFail(t *testing.T) { + tokenHash := []byte("token hash") + toSignData := &userToSignData{ + DataID: 1, + Alg: biscuit.Symbol(ECDSA_P256_SHA256), + Data: []byte("challenge"), + SignedBlockCount: 2, + } + + userKey := generateUserKeyPair(t) + invalidKey := generateUserKeyPair(t) + + signedData, err := userSign(tokenHash, userKey, toSignData) + require.NoError(t, err) + + rsaKey, err := rsa.GenerateKey(rand.Reader, 1024) + require.NoError(t, err) + wrongKeyKind, err := x509.MarshalPKIXPublicKey(&rsaKey.PublicKey) + require.NoError(t, err) + + testCases := []struct { + desc string + tokenHash []byte + data *userVerificationData + expectedErr error + }{ + { + desc: "unsupported alg", + expectedErr: ErrUnsupportedSignatureAlg, + data: &userVerificationData{ + Alg: "unknown", + }, + }, + { + desc: "invalid pubkey encoding", + data: &userVerificationData{ + Alg: biscuit.Symbol(ECDSA_P256_SHA256), + UserPubKey: make([]byte, 32), + }, + }, + { + desc: "invalid pubkey kind", + data: &userVerificationData{ + Alg: biscuit.Symbol(ECDSA_P256_SHA256), + UserPubKey: wrongKeyKind, + }, + }, + { + desc: "wrong pubkey", + tokenHash: tokenHash, + data: &userVerificationData{ + Alg: biscuit.Symbol(ECDSA_P256_SHA256), + UserPubKey: invalidKey.Public, + Data: toSignData.Data, + DataID: toSignData.DataID, + Nonce: signedData.Nonce, + Signature: signedData.Signature, + Timestamp: signedData.Timestamp, + SignedBlockCount: signedData.SignedBlockCount, + }, + }, + { + desc: "tampered token hash", + expectedErr: ErrInvalidSignature, + tokenHash: []byte("wrong"), + data: &userVerificationData{ + Alg: biscuit.Symbol(ECDSA_P256_SHA256), + UserPubKey: userKey.Public, + Data: toSignData.Data, + DataID: toSignData.DataID, + Nonce: signedData.Nonce, + Signature: signedData.Signature, + Timestamp: signedData.Timestamp, + SignedBlockCount: signedData.SignedBlockCount, + }, + }, + { + desc: "tampered nonce", + expectedErr: ErrInvalidSignature, + tokenHash: tokenHash, + data: &userVerificationData{ + Alg: biscuit.Symbol(ECDSA_P256_SHA256), + UserPubKey: userKey.Public, + Data: toSignData.Data, + DataID: toSignData.DataID, + Nonce: []byte("another nonce"), + Signature: signedData.Signature, + SignedBlockCount: signedData.SignedBlockCount, + Timestamp: signedData.Timestamp, + }, + }, + { + desc: "tampered timestamp", + expectedErr: ErrInvalidSignature, + tokenHash: tokenHash, + data: &userVerificationData{ + Alg: biscuit.Symbol(ECDSA_P256_SHA256), + UserPubKey: userKey.Public, + Data: toSignData.Data, + DataID: toSignData.DataID, + Nonce: signedData.Nonce, + Signature: signedData.Signature, + Timestamp: biscuit.Date(time.Now().Add(1 * time.Second)), + SignedBlockCount: signedData.SignedBlockCount, + }, + }, + { + desc: "tampered signedBlockCount", + expectedErr: ErrInvalidSignature, + tokenHash: tokenHash, + data: &userVerificationData{ + Alg: biscuit.Symbol(ECDSA_P256_SHA256), + UserPubKey: userKey.Public, + Data: toSignData.Data, + DataID: toSignData.DataID, + Nonce: signedData.Nonce, + Signature: signedData.Signature, + Timestamp: signedData.Timestamp, + SignedBlockCount: signedData.SignedBlockCount + 1, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.desc, func(t *testing.T) { + + err := verifyUserSignature(testCase.tokenHash, testCase.data) + require.Error(t, err) + if testCase.expectedErr != nil { + require.Equal(t, testCase.expectedErr, err) + } + }) + } +} + +func generateUserKeyPair(t *testing.T) *UserKeyPair { + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + privBytes, err := x509.MarshalECPrivateKey(priv) + require.NoError(t, err) + pubBytes, err := x509.MarshalPKIXPublicKey(&priv.PublicKey) + require.NoError(t, err) + return &UserKeyPair{ + Private: privBytes, + Public: pubBytes, + } +} diff --git a/pkg/biscuit/wrapper.go b/pkg/biscuit/wrapper.go new file mode 100644 index 0000000..ec16ebd --- /dev/null +++ b/pkg/biscuit/wrapper.go @@ -0,0 +1,336 @@ +package biscuit + +import ( + "bytes" + "crypto/rand" + "errors" + "fmt" + + "github.com/flynn/biscuit-go" + "github.com/flynn/hubauth/pkg/kmssign" +) + +var ( + ErrAlreadySigned = errors.New("already signed") + ErrInvalidToSignDataPrefix = errors.New("invalid to_sign data prefix") +) + +var ( + signStaticCtx = []byte("biscuit-pop-v0") + challengeSize = 16 + nonceSize = 16 +) + +type hubauthBuilder struct { + biscuit.Builder +} + +// withUserToSignFact add an authority should_sign fact and associated data to the biscuit +// with an authority caveat requiring the verifier to provide a valid_signature fact. +// the verifier is responsible of ensuring that a valid signature exists over the data. +func (b *hubauthBuilder) withUserToSignFact(userPubkey []byte) error { + dataID := biscuit.Integer(0) + + if err := b.AddAuthorityFact(biscuit.Fact{Predicate: biscuit.Predicate{ + Name: "should_sign", + IDs: []biscuit.Atom{ + dataID, + biscuit.Symbol(ECDSA_P256_SHA256), + biscuit.Bytes(userPubkey), + }, + }}); err != nil { + return err + } + + challenge := make([]byte, challengeSize) + if _, err := rand.Reader.Read(challenge); err != nil { + return err + } + + if err := b.AddAuthorityFact(biscuit.Fact{Predicate: biscuit.Predicate{ + Name: "data", + IDs: []biscuit.Atom{ + dataID, + biscuit.Bytes(append(signStaticCtx, challenge...)), + }, + }}); err != nil { + return err + } + + if err := b.AddAuthorityCaveat(biscuit.Rule{ + Head: biscuit.Predicate{Name: "valid", IDs: []biscuit.Atom{biscuit.Variable(0)}}, + Body: []biscuit.Predicate{ + {Name: "should_sign", IDs: []biscuit.Atom{biscuit.SymbolAuthority, biscuit.Variable(0), biscuit.Variable(1), biscuit.Variable(2)}}, + {Name: "valid_signature", IDs: []biscuit.Atom{biscuit.Symbol("ambient"), biscuit.Variable(0), biscuit.Variable(1), biscuit.Variable(2)}}, + }, + }); err != nil { + return err + } + + return nil +} + +// withAudienceSignature add an authority audience_signature fact, containing a challenge and +// a matching signature using the audience key. +// the verifier is responsible of providing a valid_audience_signature fact, after +// verifying the signature using the audience pubkey. +func (b *hubauthBuilder) withAudienceSignature(audience string, audienceKey *kmssign.Key) error { + if len(audience) == 0 { + return errors.New("audience is required") + } + + data, err := audienceSign(audience, audienceKey) + if err != nil { + return err + } + + if err := b.AddAuthorityFact(biscuit.Fact{Predicate: biscuit.Predicate{ + Name: "audience_signature", + IDs: []biscuit.Atom{ + data.Audience, + data.Challenge, + data.Signature, + }, + }}); err != nil { + return err + } + + if err := b.AddAuthorityCaveat(biscuit.Rule{ + Head: biscuit.Predicate{Name: "valid_audience", IDs: []biscuit.Atom{biscuit.Variable(0)}}, + Body: []biscuit.Predicate{ + {Name: "audience_signature", IDs: []biscuit.Atom{biscuit.SymbolAuthority, biscuit.Variable(0), biscuit.Variable(1), biscuit.Variable(2)}}, + {Name: "valid_audience_signature", IDs: []biscuit.Atom{biscuit.Symbol("ambient"), biscuit.Variable(0), biscuit.Variable(2)}}, + }, + }); err != nil { + return err + } + + return nil +} + +type hubauthBlockBuilder struct { + biscuit.BlockBuilder +} + +func (b *hubauthBlockBuilder) withUserSignature(sigData *userSignatureData) error { + return b.AddFact(biscuit.Fact{Predicate: biscuit.Predicate{ + Name: "signature", + IDs: []biscuit.Atom{ + sigData.DataID, + sigData.UserPubKey, + sigData.Signature, + sigData.Nonce, + sigData.Timestamp, + sigData.SignedBlockCount, + }, + }}) +} + +type hubauthVerifier struct { + biscuit.Verifier +} + +func (v *hubauthVerifier) getUserToSignData(userPubKey biscuit.Bytes, signedBlockCount int) (*userToSignData, error) { + toSign, err := v.Query(biscuit.Rule{ + Head: biscuit.Predicate{ + Name: "to_sign", + IDs: []biscuit.Atom{biscuit.Variable(0), biscuit.Variable(1), biscuit.Variable(2)}, + }, + Body: []biscuit.Predicate{ + { + Name: "should_sign", IDs: []biscuit.Atom{ + biscuit.SymbolAuthority, + biscuit.Variable(0), + biscuit.Variable(1), + biscuit.Bytes(userPubKey), + }, + }, { + Name: "data", IDs: []biscuit.Atom{ + biscuit.SymbolAuthority, + biscuit.Variable(0), + biscuit.Variable(2), + }, + }, + }, + }) + if err != nil { + return nil, err + } + + if g, w := len(toSign), 1; g != w { + return nil, fmt.Errorf("invalid to_sign fact count, got %d, want %d", g, w) + } + + toSignFact := toSign[0] + if g, w := len(toSignFact.IDs), 3; g != w { + return nil, fmt.Errorf("invalid to_sign fact, got %d atoms, want %d", g, w) + } + + sigData := &userToSignData{} + var ok bool + sigData.DataID, ok = toSign[0].IDs[0].(biscuit.Integer) + if !ok { + return nil, errors.New("invalid to_sign atom: dataID") + } + sigData.Alg, ok = toSign[0].IDs[1].(biscuit.Symbol) + if !ok { + return nil, errors.New("invalid to_sign atom: alg") + } + sigData.Data, ok = toSign[0].IDs[2].(biscuit.Bytes) + if !ok { + return nil, errors.New("invalid to_sign atom: data") + } + + if !bytes.HasPrefix(sigData.Data, signStaticCtx) { + return nil, ErrInvalidToSignDataPrefix + } + + sigData.SignedBlockCount = biscuit.Integer(signedBlockCount) + + return sigData, nil +} + +func (v *hubauthVerifier) ensureNotAlreadyUserSigned(dataID biscuit.Integer, userPubKey biscuit.Bytes) error { + alreadySigned, err := v.Query(biscuit.Rule{ + Head: biscuit.Predicate{Name: "already_signed", IDs: []biscuit.Atom{biscuit.Variable(0)}}, + Body: []biscuit.Predicate{ + {Name: "signature", IDs: []biscuit.Atom{dataID, userPubKey, biscuit.Variable(0)}}, + }, + }) + if err != nil { + return err + } + if len(alreadySigned) != 0 { + return ErrAlreadySigned + } + + return nil +} + +func (v *hubauthVerifier) getUserVerificationData() (*userVerificationData, error) { + toValidate, err := v.Query(biscuit.Rule{ + Head: biscuit.Predicate{ + Name: "to_validate", + IDs: []biscuit.Atom{ + biscuit.Variable(0), // dataID + biscuit.Variable(1), // alg + biscuit.Variable(2), // pubkey + biscuit.Variable(3), // data + biscuit.Variable(4), // signature + biscuit.Variable(5), // signerNonce + biscuit.Variable(6), // signerTimestamp + biscuit.Variable(7), // signedBlockCount + }}, + Body: []biscuit.Predicate{ + {Name: "should_sign", IDs: []biscuit.Atom{biscuit.SymbolAuthority, biscuit.Variable(0), biscuit.Variable(1), biscuit.Variable(2)}}, + {Name: "data", IDs: []biscuit.Atom{biscuit.SymbolAuthority, biscuit.Variable(0), biscuit.Variable(3)}}, + {Name: "signature", IDs: []biscuit.Atom{biscuit.Variable(0), biscuit.Variable(2), biscuit.Variable(4), biscuit.Variable(5), biscuit.Variable(6), biscuit.Variable(7)}}, + }, + }) + if err != nil { + return nil, err + } + + if g, w := len(toValidate), 1; g != w { + return nil, fmt.Errorf("invalid to_validate fact count, got %d, want %d", g, w) + } + + toValidateFact := toValidate[0] + if g, w := len(toValidateFact.IDs), 8; g != w { + return nil, fmt.Errorf("invalid to_valid fact atom count, got %d, want %d", g, w) + } + + toVerify := &userVerificationData{} + var ok bool + toVerify.DataID, ok = toValidateFact.IDs[0].(biscuit.Integer) + if !ok { + return nil, errors.New("invalid to_validate atom: dataID") + } + toVerify.Alg, ok = toValidateFact.IDs[1].(biscuit.Symbol) + if !ok { + return nil, errors.New("invalid to_validate atom: alg") + } + toVerify.UserPubKey, ok = toValidateFact.IDs[2].(biscuit.Bytes) + if !ok { + return nil, errors.New("invalid to_validate atom: userPubKey") + } + toVerify.Data, ok = toValidateFact.IDs[3].(biscuit.Bytes) + if !ok { + return nil, errors.New("invalid to_validate atom: data") + } + toVerify.Signature, ok = toValidateFact.IDs[4].(biscuit.Bytes) + if !ok { + return nil, errors.New("invalid to_validate atom: signature") + } + toVerify.Nonce, ok = toValidateFact.IDs[5].(biscuit.Bytes) + if !ok { + return nil, errors.New("invalid to_validate atom: nonce") + } + toVerify.Timestamp, ok = toValidateFact.IDs[6].(biscuit.Date) + if !ok { + return nil, errors.New("invalid to_validate atom: timestamp") + } + toVerify.SignedBlockCount, ok = toValidateFact.IDs[7].(biscuit.Integer) + if !ok { + return nil, errors.New("invalid to_validate atom: signedBlockCount") + } + + return toVerify, nil +} + +func (v *hubauthVerifier) withValidatedUserSignature(data *userVerificationData) error { + v.AddFact(biscuit.Fact{Predicate: biscuit.Predicate{ + Name: "valid_signature", + IDs: []biscuit.Atom{biscuit.Symbol("ambient"), data.DataID, data.Alg, data.UserPubKey}, + }}) + + return nil +} + +func (v *hubauthVerifier) getAudienceVerificationData(audience string) (*audienceVerificationData, error) { + toValidate, err := v.Query(biscuit.Rule{ + Head: biscuit.Predicate{ + Name: "audience_to_validate", + IDs: []biscuit.Atom{ + biscuit.Variable(0), // challenge + biscuit.Variable(1), // signature + }}, + Body: []biscuit.Predicate{ + {Name: "audience_signature", IDs: []biscuit.Atom{biscuit.SymbolAuthority, biscuit.Symbol(audience), biscuit.Variable(0), biscuit.Variable(1)}}, + }, + }) + if err != nil { + return nil, err + } + + if g, w := len(toValidate), 1; g != w { + return nil, fmt.Errorf("invalid audience_to_validate fact count, got %d, want %d", g, w) + } + + toValidateFact := toValidate[0] + if g, w := len(toValidateFact.IDs), 2; g != w { + return nil, fmt.Errorf("invalid audience_to_validate fact atom count, got %d, want %d", g, w) + } + + toVerify := &audienceVerificationData{Audience: biscuit.Symbol(audience)} + var ok bool + toVerify.Challenge, ok = toValidateFact.IDs[0].(biscuit.Bytes) + if !ok { + return nil, errors.New("invalid audience_to_validate atom: challenge") + } + toVerify.Signature, ok = toValidateFact.IDs[1].(biscuit.Bytes) + if !ok { + return nil, errors.New("invalid audience_to_validate atom: signature") + } + + return toVerify, nil +} + +func (v *hubauthVerifier) withValidatedAudienceSignature(data *audienceVerificationData) error { + v.AddFact(biscuit.Fact{Predicate: biscuit.Predicate{ + Name: "valid_audience_signature", + IDs: []biscuit.Atom{biscuit.Symbol("ambient"), data.Audience, data.Signature}, + }}) + + return nil +} From 5f99e4da15d9e46fbd58917248ee572f53cf9b58 Mon Sep 17 00:00:00 2001 From: daeMOn Date: Mon, 12 Oct 2020 09:39:15 +0200 Subject: [PATCH 19/32] add biscuit metadata and expiration date Verifiers must now provide the current time for verifying the biscuit, and can extract user informations. User's pubkeys are now provided in http param when exchanging code. Removed block count from biscuit weakening the signature. --- pkg/biscuit/biscuit.go | 56 ++++++++++++----- pkg/biscuit/biscuit_test.go | 20 ++++-- pkg/biscuit/signature.go | 66 ++++++++++++-------- pkg/biscuit/signature_test.go | 105 +++++++++++++------------------ pkg/biscuit/wrapper.go | 112 ++++++++++++++++++++++++++++++---- 5 files changed, 238 insertions(+), 121 deletions(-) diff --git a/pkg/biscuit/biscuit.go b/pkg/biscuit/biscuit.go index ef1a452..e335e60 100644 --- a/pkg/biscuit/biscuit.go +++ b/pkg/biscuit/biscuit.go @@ -3,12 +3,20 @@ package biscuit import ( "crypto/rand" "fmt" + "time" "github.com/flynn/biscuit-go" "github.com/flynn/biscuit-go/sig" "github.com/flynn/hubauth/pkg/kmssign" ) +type Metadata struct { + ClientID string + UserID string + UserEmail string + IssueTime time.Time +} + type UserKeyPair struct { Public []byte Private []byte @@ -16,7 +24,7 @@ type UserKeyPair struct { // GenerateSignable returns a biscuit which will only verify after being // signed with the private key matching the given userPubkey. -func GenerateSignable(rootKey sig.Keypair, audience string, audienceKey *kmssign.Key, userPubkey []byte) ([]byte, error) { +func GenerateSignable(rootKey sig.Keypair, audience string, audienceKey *kmssign.Key, userPublicKey []byte, expireTime time.Time, m *Metadata) ([]byte, error) { builder := &hubauthBuilder{ Builder: biscuit.NewBuilder(rand.Reader, rootKey), } @@ -25,7 +33,15 @@ func GenerateSignable(rootKey sig.Keypair, audience string, audienceKey *kmssign return nil, err } - if err := builder.withUserToSignFact(userPubkey); err != nil { + if err := builder.withUserToSignFact(userPublicKey); err != nil { + return nil, err + } + + if err := builder.withExpire(expireTime); err != nil { + return nil, err + } + + if err := builder.withMetadata(m); err != nil { return nil, err } @@ -55,7 +71,7 @@ func Sign(token []byte, rootPubKey sig.PublicKey, userKey *UserKeyPair) ([]byte, Verifier: v, } - toSignData, err := verifier.getUserToSignData(userKey.Public, b.BlockCount()) + toSignData, err := verifier.getUserToSignData(userKey.Public) if err != nil { return nil, fmt.Errorf("biscuit: failed to get to_sign data: %w", err) } @@ -92,46 +108,56 @@ func Sign(token []byte, rootPubKey sig.PublicKey, userKey *UserKeyPair) ([]byte, // Verify will verify the biscuit, the included audience and user signature, and return an error // when anything is invalid. -func Verify(token []byte, rootPubKey sig.PublicKey, audience string, audienceKey *kmssign.Key) error { +func Verify(token []byte, rootPubKey sig.PublicKey, audience string, audienceKey *kmssign.Key) (*Metadata, error) { b, err := biscuit.Unmarshal(token) if err != nil { - return fmt.Errorf("biscuit: failed to unmarshal: %w", err) + return nil, fmt.Errorf("biscuit: failed to unmarshal: %w", err) } v, err := b.Verify(rootPubKey) if err != nil { - return fmt.Errorf("biscuit: failed to verify: %w", err) + return nil, fmt.Errorf("biscuit: failed to verify: %w", err) } verifier := &hubauthVerifier{v} audienceVerificationData, err := verifier.getAudienceVerificationData(audience) if err != nil { - return fmt.Errorf("biscuit: failed to retrieve audience signature data: %w", err) + return nil, fmt.Errorf("biscuit: failed to retrieve audience signature data: %w", err) } if err := verifyAudienceSignature(audienceKey, audienceVerificationData); err != nil { - return fmt.Errorf("biscuit: failed to verify audience signature: %w", err) + return nil, fmt.Errorf("biscuit: failed to verify audience signature: %w", err) } if err := verifier.withValidatedAudienceSignature(audienceVerificationData); err != nil { - return fmt.Errorf("biscuit: failed to add validated signature: %w", err) + return nil, fmt.Errorf("biscuit: failed to add validated signature: %w", err) } userVerificationData, err := verifier.getUserVerificationData() if err != nil { - return fmt.Errorf("biscuit: failed to retrieve user signature data: %w", err) + return nil, fmt.Errorf("biscuit: failed to retrieve user signature data: %w", err) } - signedTokenHash, err := b.SHA256Sum(int(userVerificationData.SignedBlockCount)) + // TODO: improve biscuit API to allow retrieve the block index the signature is at + // so that we can still append other blocks if needed. Right now the signature MUST BE the last block. + signedTokenHash, err := b.SHA256Sum(b.BlockCount() - 1) if err != nil { - return fmt.Errorf("biscuit: failed to generate token hash: %w", err) + return nil, fmt.Errorf("biscuit: failed to generate token hash: %w", err) } if err := verifyUserSignature(signedTokenHash, userVerificationData); err != nil { - return fmt.Errorf("biscuit: failed to verify user signature: %w", err) + return nil, fmt.Errorf("biscuit: failed to verify user signature: %w", err) } if err := verifier.withValidatedUserSignature(userVerificationData); err != nil { - return fmt.Errorf("biscuit: failed to add validated signature: %w", err) + return nil, fmt.Errorf("biscuit: failed to add validated signature: %w", err) + } + + if err := verifier.withCurrentTime(time.Now()); err != nil { + return nil, fmt.Errorf("biscuit: failed to add current time: %w", err) + } + + if err := verifier.Verify(); err != nil { + return nil, fmt.Errorf("biscuit: failed to verify: %w", err) } - return verifier.Verify() + return verifier.getMetadata() } diff --git a/pkg/biscuit/biscuit_test.go b/pkg/biscuit/biscuit_test.go index ed7b273..bf6199b 100644 --- a/pkg/biscuit/biscuit_test.go +++ b/pkg/biscuit/biscuit_test.go @@ -4,6 +4,7 @@ import ( "context" "crypto/rand" "testing" + "time" "github.com/flynn/biscuit-go/sig" "github.com/flynn/hubauth/pkg/kmssign" @@ -20,8 +21,13 @@ func TestBiscuit(t *testing.T) { require.NoError(t, err) userKey := generateUserKeyPair(t) - - signableBiscuit, err := GenerateSignable(rootKey, audience, audienceKey, userKey.Public) + metas := &Metadata{ + ClientID: "abcd", + UserEmail: "1234@example.com", + UserID: "1234", + IssueTime: time.Now(), + } + signableBiscuit, err := GenerateSignable(rootKey, audience, audienceKey, userKey.Public, time.Now().Add(5*time.Minute), metas) require.NoError(t, err) t.Logf("signable biscuit size: %d", len(signableBiscuit)) @@ -30,8 +36,12 @@ func TestBiscuit(t *testing.T) { require.NoError(t, err) t.Logf("signed biscuit size: %d", len(signedBiscuit)) - err = Verify(signedBiscuit, rootKey.Public(), audience, audienceKey) + res, err := Verify(signedBiscuit, rootKey.Public(), audience, audienceKey) require.NoError(t, err) + require.Equal(t, metas.ClientID, res.ClientID) + require.Equal(t, metas.UserID, res.UserID) + require.Equal(t, metas.UserEmail, res.UserEmail) + require.WithinDuration(t, metas.IssueTime, res.IssueTime, 1*time.Second) }) t.Run("user sign with wrong key", func(t *testing.T) { @@ -43,7 +53,7 @@ func TestBiscuit(t *testing.T) { signedBiscuit, err := Sign(signableBiscuit, rootKey.Public(), userKey) require.NoError(t, err) - err = Verify(signedBiscuit, rootKey.Public(), "http://another.audience.url", audienceKey) + _, err = Verify(signedBiscuit, rootKey.Public(), "http://another.audience.url", audienceKey) require.Error(t, err) wrongAudience := "http://another.audience.url" @@ -51,7 +61,7 @@ func TestBiscuit(t *testing.T) { wrongAudienceKey, err := kmssign.NewKey(context.Background(), kms, wrongAudience) require.NoError(t, err) - err = Verify(signedBiscuit, rootKey.Public(), audience, wrongAudienceKey) + _, err = Verify(signedBiscuit, rootKey.Public(), audience, wrongAudienceKey) require.Error(t, err) }) } diff --git a/pkg/biscuit/signature.go b/pkg/biscuit/signature.go index 385e4bf..078eaa6 100644 --- a/pkg/biscuit/signature.go +++ b/pkg/biscuit/signature.go @@ -3,10 +3,12 @@ package biscuit import ( "crypto" "crypto/ecdsa" + "crypto/elliptic" "crypto/rand" "crypto/sha256" "crypto/x509" "errors" + "fmt" "time" "github.com/flynn/biscuit-go" @@ -25,30 +27,27 @@ const ( ) type userToSignData struct { - DataID biscuit.Integer - Alg biscuit.Symbol - Data biscuit.Bytes - SignedBlockCount biscuit.Integer + DataID biscuit.Integer + Alg biscuit.Symbol + Data biscuit.Bytes } type userSignatureData struct { - DataID biscuit.Integer - UserPubKey biscuit.Bytes - Signature biscuit.Bytes - SignedBlockCount biscuit.Integer - Nonce biscuit.Bytes - Timestamp biscuit.Date + DataID biscuit.Integer + UserPubKey biscuit.Bytes + Signature biscuit.Bytes + Nonce biscuit.Bytes + Timestamp biscuit.Date } type userVerificationData struct { - DataID biscuit.Integer - Alg biscuit.Symbol - Data biscuit.Bytes - UserPubKey biscuit.Bytes - Signature biscuit.Bytes - SignedBlockCount biscuit.Integer - Nonce biscuit.Bytes - Timestamp biscuit.Date + DataID biscuit.Integer + Alg biscuit.Symbol + Data biscuit.Bytes + UserPubKey biscuit.Bytes + Signature biscuit.Bytes + Nonce biscuit.Bytes + Timestamp biscuit.Date } func userSign(tokenHash []byte, userKey *UserKeyPair, toSignData *userToSignData) (*userSignatureData, error) { @@ -67,7 +66,6 @@ func userSign(tokenHash []byte, userKey *UserKeyPair, toSignData *userToSignData dataToSign = append(dataToSign, tokenHash...) dataToSign = append(dataToSign, signerNonce...) dataToSign = append(dataToSign, []byte(signerTimestamp.Format(time.RFC3339))...) - dataToSign = append(dataToSign, []byte(toSignData.SignedBlockCount.String())...) var signedData biscuit.Bytes switch SignatureAlg(toSignData.Alg) { @@ -86,12 +84,11 @@ func userSign(tokenHash []byte, userKey *UserKeyPair, toSignData *userToSignData } return &userSignatureData{ - DataID: toSignData.DataID, - Nonce: signerNonce, - Signature: signedData, - SignedBlockCount: toSignData.SignedBlockCount, - Timestamp: biscuit.Date(signerTimestamp), - UserPubKey: userKey.Public, + DataID: toSignData.DataID, + Nonce: signerNonce, + Signature: signedData, + Timestamp: biscuit.Date(signerTimestamp), + UserPubKey: userKey.Public, }, nil } @@ -101,7 +98,6 @@ func verifyUserSignature(signedTokenHash []byte, data *userVerificationData) err signedData = append(signedData, signedTokenHash...) signedData = append(signedData, data.Nonce...) signedData = append(signedData, []byte(time.Time(data.Timestamp).Format(time.RFC3339))...) - signedData = append(signedData, []byte(data.SignedBlockCount.String())...) switch SignatureAlg(data.Alg) { case ECDSA_P256_SHA256: @@ -160,3 +156,21 @@ func verifyAudienceSignature(audiencePubkey *kmssign.Key, data *audienceVerifica } return nil } + +func validatePKIXP256PublicKey(pubkey []byte) error { + key, err := x509.ParsePKIXPublicKey(pubkey) + if err != nil { + return fmt.Errorf("failed to parse PKIX, ASN.1 DER public key: %v", err) + } + + ecKey, ok := key.(*ecdsa.PublicKey) + if !ok { + return errors.New("public key is not an *ecdsa.PublicKey") + } + + if ecKey.Curve != elliptic.P256() { + return fmt.Errorf("publickey is on wrong curve, expected P256") + } + + return nil +} diff --git a/pkg/biscuit/signature_test.go b/pkg/biscuit/signature_test.go index 5cb5d8b..d4d4cc2 100644 --- a/pkg/biscuit/signature_test.go +++ b/pkg/biscuit/signature_test.go @@ -25,16 +25,14 @@ func TestUserSignVerify(t *testing.T) { userKey := generateUserKeyPair(t) toSignData := &userToSignData{ - DataID: 1, - Alg: biscuit.Symbol(ECDSA_P256_SHA256), - Data: []byte("challenge"), - SignedBlockCount: 2, + DataID: 1, + Alg: biscuit.Symbol(ECDSA_P256_SHA256), + Data: []byte("challenge"), } signedData, err := userSign(tokenHash, userKey, toSignData) require.NoError(t, err) require.NotEmpty(t, signedData.Signature) - require.Equal(t, biscuit.Integer(2), signedData.SignedBlockCount) require.Equal(t, biscuit.Integer(1), signedData.DataID) require.Equal(t, biscuit.Bytes(userKey.Public), signedData.UserPubKey) @@ -45,14 +43,13 @@ func TestUserSignVerify(t *testing.T) { require.WithinDuration(t, time.Now(), time.Time(signedData.Timestamp), 1*time.Second) require.NoError(t, verifyUserSignature(tokenHash, &userVerificationData{ - DataID: toSignData.DataID, - Alg: toSignData.Alg, - Data: toSignData.Data, - Nonce: signedData.Nonce, - Signature: signedData.Signature, - SignedBlockCount: signedData.SignedBlockCount, - Timestamp: signedData.Timestamp, - UserPubKey: signedData.UserPubKey, + DataID: toSignData.DataID, + Alg: toSignData.Alg, + Data: toSignData.Data, + Nonce: signedData.Nonce, + Signature: signedData.Signature, + Timestamp: signedData.Timestamp, + UserPubKey: signedData.UserPubKey, })) } @@ -112,10 +109,9 @@ func TestUserSignFail(t *testing.T) { func TestVerifyUserSignatureFail(t *testing.T) { tokenHash := []byte("token hash") toSignData := &userToSignData{ - DataID: 1, - Alg: biscuit.Symbol(ECDSA_P256_SHA256), - Data: []byte("challenge"), - SignedBlockCount: 2, + DataID: 1, + Alg: biscuit.Symbol(ECDSA_P256_SHA256), + Data: []byte("challenge"), } userKey := generateUserKeyPair(t) @@ -160,14 +156,13 @@ func TestVerifyUserSignatureFail(t *testing.T) { desc: "wrong pubkey", tokenHash: tokenHash, data: &userVerificationData{ - Alg: biscuit.Symbol(ECDSA_P256_SHA256), - UserPubKey: invalidKey.Public, - Data: toSignData.Data, - DataID: toSignData.DataID, - Nonce: signedData.Nonce, - Signature: signedData.Signature, - Timestamp: signedData.Timestamp, - SignedBlockCount: signedData.SignedBlockCount, + Alg: biscuit.Symbol(ECDSA_P256_SHA256), + UserPubKey: invalidKey.Public, + Data: toSignData.Data, + DataID: toSignData.DataID, + Nonce: signedData.Nonce, + Signature: signedData.Signature, + Timestamp: signedData.Timestamp, }, }, { @@ -175,14 +170,13 @@ func TestVerifyUserSignatureFail(t *testing.T) { expectedErr: ErrInvalidSignature, tokenHash: []byte("wrong"), data: &userVerificationData{ - Alg: biscuit.Symbol(ECDSA_P256_SHA256), - UserPubKey: userKey.Public, - Data: toSignData.Data, - DataID: toSignData.DataID, - Nonce: signedData.Nonce, - Signature: signedData.Signature, - Timestamp: signedData.Timestamp, - SignedBlockCount: signedData.SignedBlockCount, + Alg: biscuit.Symbol(ECDSA_P256_SHA256), + UserPubKey: userKey.Public, + Data: toSignData.Data, + DataID: toSignData.DataID, + Nonce: signedData.Nonce, + Signature: signedData.Signature, + Timestamp: signedData.Timestamp, }, }, { @@ -190,14 +184,13 @@ func TestVerifyUserSignatureFail(t *testing.T) { expectedErr: ErrInvalidSignature, tokenHash: tokenHash, data: &userVerificationData{ - Alg: biscuit.Symbol(ECDSA_P256_SHA256), - UserPubKey: userKey.Public, - Data: toSignData.Data, - DataID: toSignData.DataID, - Nonce: []byte("another nonce"), - Signature: signedData.Signature, - SignedBlockCount: signedData.SignedBlockCount, - Timestamp: signedData.Timestamp, + Alg: biscuit.Symbol(ECDSA_P256_SHA256), + UserPubKey: userKey.Public, + Data: toSignData.Data, + DataID: toSignData.DataID, + Nonce: []byte("another nonce"), + Signature: signedData.Signature, + Timestamp: signedData.Timestamp, }, }, { @@ -205,29 +198,13 @@ func TestVerifyUserSignatureFail(t *testing.T) { expectedErr: ErrInvalidSignature, tokenHash: tokenHash, data: &userVerificationData{ - Alg: biscuit.Symbol(ECDSA_P256_SHA256), - UserPubKey: userKey.Public, - Data: toSignData.Data, - DataID: toSignData.DataID, - Nonce: signedData.Nonce, - Signature: signedData.Signature, - Timestamp: biscuit.Date(time.Now().Add(1 * time.Second)), - SignedBlockCount: signedData.SignedBlockCount, - }, - }, - { - desc: "tampered signedBlockCount", - expectedErr: ErrInvalidSignature, - tokenHash: tokenHash, - data: &userVerificationData{ - Alg: biscuit.Symbol(ECDSA_P256_SHA256), - UserPubKey: userKey.Public, - Data: toSignData.Data, - DataID: toSignData.DataID, - Nonce: signedData.Nonce, - Signature: signedData.Signature, - Timestamp: signedData.Timestamp, - SignedBlockCount: signedData.SignedBlockCount + 1, + Alg: biscuit.Symbol(ECDSA_P256_SHA256), + UserPubKey: userKey.Public, + Data: toSignData.Data, + DataID: toSignData.DataID, + Nonce: signedData.Nonce, + Signature: signedData.Signature, + Timestamp: biscuit.Date(time.Now().Add(1 * time.Second)), }, }, } diff --git a/pkg/biscuit/wrapper.go b/pkg/biscuit/wrapper.go index ec16ebd..f39681f 100644 --- a/pkg/biscuit/wrapper.go +++ b/pkg/biscuit/wrapper.go @@ -5,8 +5,10 @@ import ( "crypto/rand" "errors" "fmt" + "time" "github.com/flynn/biscuit-go" + "github.com/flynn/biscuit-go/datalog" "github.com/flynn/hubauth/pkg/kmssign" ) @@ -31,6 +33,10 @@ type hubauthBuilder struct { func (b *hubauthBuilder) withUserToSignFact(userPubkey []byte) error { dataID := biscuit.Integer(0) + if err := validatePKIXP256PublicKey(userPubkey); err != nil { + return err + } + if err := b.AddAuthorityFact(biscuit.Fact{Predicate: biscuit.Predicate{ Name: "should_sign", IDs: []biscuit.Atom{ @@ -108,6 +114,38 @@ func (b *hubauthBuilder) withAudienceSignature(audience string, audienceKey *kms return nil } +func (b *hubauthBuilder) withMetadata(m *Metadata) error { + return b.AddAuthorityFact(biscuit.Fact{Predicate: biscuit.Predicate{ + Name: "metadata", + IDs: []biscuit.Atom{ + biscuit.String(m.ClientID), + biscuit.String(m.UserID), + biscuit.String(m.UserEmail), + biscuit.Date(m.IssueTime), + }, + }}) +} + +func (b *hubauthBuilder) withExpire(exp time.Time) error { + if err := b.AddAuthorityCaveat(biscuit.Rule{ + Head: biscuit.Predicate{Name: "not_expired", IDs: []biscuit.Atom{biscuit.Variable(0)}}, + Body: []biscuit.Predicate{ + {Name: "current_time", IDs: []biscuit.Atom{biscuit.Symbol("ambient"), biscuit.Variable(0)}}, + }, + Constraints: []biscuit.Constraint{{ + Name: biscuit.Variable(0), + Checker: biscuit.DateComparisonChecker{ + Comparison: datalog.DateComparisonBefore, + Date: biscuit.Date(exp), + }, + }}, + }); err != nil { + return err + } + + return nil +} + type hubauthBlockBuilder struct { biscuit.BlockBuilder } @@ -121,7 +159,6 @@ func (b *hubauthBlockBuilder) withUserSignature(sigData *userSignatureData) erro sigData.Signature, sigData.Nonce, sigData.Timestamp, - sigData.SignedBlockCount, }, }}) } @@ -130,7 +167,7 @@ type hubauthVerifier struct { biscuit.Verifier } -func (v *hubauthVerifier) getUserToSignData(userPubKey biscuit.Bytes, signedBlockCount int) (*userToSignData, error) { +func (v *hubauthVerifier) getUserToSignData(userPubKey biscuit.Bytes) (*userToSignData, error) { toSign, err := v.Query(biscuit.Rule{ Head: biscuit.Predicate{ Name: "to_sign", @@ -185,8 +222,6 @@ func (v *hubauthVerifier) getUserToSignData(userPubKey biscuit.Bytes, signedBloc return nil, ErrInvalidToSignDataPrefix } - sigData.SignedBlockCount = biscuit.Integer(signedBlockCount) - return sigData, nil } @@ -219,12 +254,11 @@ func (v *hubauthVerifier) getUserVerificationData() (*userVerificationData, erro biscuit.Variable(4), // signature biscuit.Variable(5), // signerNonce biscuit.Variable(6), // signerTimestamp - biscuit.Variable(7), // signedBlockCount }}, Body: []biscuit.Predicate{ {Name: "should_sign", IDs: []biscuit.Atom{biscuit.SymbolAuthority, biscuit.Variable(0), biscuit.Variable(1), biscuit.Variable(2)}}, {Name: "data", IDs: []biscuit.Atom{biscuit.SymbolAuthority, biscuit.Variable(0), biscuit.Variable(3)}}, - {Name: "signature", IDs: []biscuit.Atom{biscuit.Variable(0), biscuit.Variable(2), biscuit.Variable(4), biscuit.Variable(5), biscuit.Variable(6), biscuit.Variable(7)}}, + {Name: "signature", IDs: []biscuit.Atom{biscuit.Variable(0), biscuit.Variable(2), biscuit.Variable(4), biscuit.Variable(5), biscuit.Variable(6)}}, }, }) if err != nil { @@ -236,7 +270,7 @@ func (v *hubauthVerifier) getUserVerificationData() (*userVerificationData, erro } toValidateFact := toValidate[0] - if g, w := len(toValidateFact.IDs), 8; g != w { + if g, w := len(toValidateFact.IDs), 7; g != w { return nil, fmt.Errorf("invalid to_valid fact atom count, got %d, want %d", g, w) } @@ -270,10 +304,6 @@ func (v *hubauthVerifier) getUserVerificationData() (*userVerificationData, erro if !ok { return nil, errors.New("invalid to_validate atom: timestamp") } - toVerify.SignedBlockCount, ok = toValidateFact.IDs[7].(biscuit.Integer) - if !ok { - return nil, errors.New("invalid to_validate atom: signedBlockCount") - } return toVerify, nil } @@ -326,6 +356,54 @@ func (v *hubauthVerifier) getAudienceVerificationData(audience string) (*audienc return toVerify, nil } +func (v *hubauthVerifier) getMetadata() (*Metadata, error) { + metaFacts, err := v.Query(biscuit.Rule{ + Head: biscuit.Predicate{ + Name: "metadata", + IDs: []biscuit.Atom{ + biscuit.Variable(0), // clientID + biscuit.Variable(1), // userID + biscuit.Variable(2), // userEmail + biscuit.Variable(3), // issueTime + }}, + Body: []biscuit.Predicate{ + {Name: "metadata", IDs: []biscuit.Atom{biscuit.SymbolAuthority, biscuit.Variable(0), biscuit.Variable(1), biscuit.Variable(2), biscuit.Variable(3)}}, + }, + }) + if err != nil { + return nil, err + } + + if g, w := len(metaFacts), 1; g != w { + return nil, fmt.Errorf("invalid metadata fact count, got %d, want %d", g, w) + } + + metaFact := metaFacts[0] + + clientID, ok := metaFact.IDs[0].(biscuit.String) + if !ok { + return nil, errors.New("invalid metadata atom: clientID") + } + userID, ok := metaFact.IDs[1].(biscuit.String) + if !ok { + return nil, errors.New("invalid metadata atom: userID") + } + userEmail, ok := metaFact.IDs[2].(biscuit.String) + if !ok { + return nil, errors.New("invalid metadata atom: userEmail") + } + issueTime, ok := metaFact.IDs[3].(biscuit.Date) + if !ok { + return nil, errors.New("invalid metadata atom: issueTime") + } + return &Metadata{ + ClientID: string(clientID), + UserID: string(userID), + UserEmail: string(userEmail), + IssueTime: time.Time(issueTime), + }, nil +} + func (v *hubauthVerifier) withValidatedAudienceSignature(data *audienceVerificationData) error { v.AddFact(biscuit.Fact{Predicate: biscuit.Predicate{ Name: "valid_audience_signature", @@ -334,3 +412,15 @@ func (v *hubauthVerifier) withValidatedAudienceSignature(data *audienceVerificat return nil } + +func (v *hubauthVerifier) withCurrentTime(t time.Time) error { + v.AddFact(biscuit.Fact{Predicate: biscuit.Predicate{ + Name: "current_time", + IDs: []biscuit.Atom{ + biscuit.Symbol("ambient"), + biscuit.Date(t), + }, + }}) + + return nil +} From f60e260b3b825101e5e18d79a3af10cf83959198 Mon Sep 17 00:00:00 2001 From: daeMOn Date: Tue, 13 Oct 2020 11:50:18 +0200 Subject: [PATCH 20/32] cleanup --- pkg/biscuit/biscuit.go | 2 +- pkg/biscuit/signature_test.go | 11 +++-------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/pkg/biscuit/biscuit.go b/pkg/biscuit/biscuit.go index e335e60..0c7a3e4 100644 --- a/pkg/biscuit/biscuit.go +++ b/pkg/biscuit/biscuit.go @@ -26,7 +26,7 @@ type UserKeyPair struct { // signed with the private key matching the given userPubkey. func GenerateSignable(rootKey sig.Keypair, audience string, audienceKey *kmssign.Key, userPublicKey []byte, expireTime time.Time, m *Metadata) ([]byte, error) { builder := &hubauthBuilder{ - Builder: biscuit.NewBuilder(rand.Reader, rootKey), + biscuit.NewBuilder(rand.Reader, rootKey), } if err := builder.withAudienceSignature(audience, audienceKey); err != nil { diff --git a/pkg/biscuit/signature_test.go b/pkg/biscuit/signature_test.go index d4d4cc2..2351e26 100644 --- a/pkg/biscuit/signature_test.go +++ b/pkg/biscuit/signature_test.go @@ -211,7 +211,6 @@ func TestVerifyUserSignatureFail(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.desc, func(t *testing.T) { - err := verifyUserSignature(testCase.tokenHash, testCase.data) require.Error(t, err) if testCase.expectedErr != nil { @@ -224,12 +223,8 @@ func TestVerifyUserSignatureFail(t *testing.T) { func generateUserKeyPair(t *testing.T) *UserKeyPair { priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) require.NoError(t, err) - privBytes, err := x509.MarshalECPrivateKey(priv) - require.NoError(t, err) - pubBytes, err := x509.MarshalPKIXPublicKey(&priv.PublicKey) + + kp, err := NewECDSAKeyPair(priv) require.NoError(t, err) - return &UserKeyPair{ - Private: privBytes, - Public: pubBytes, - } + return kp } From 49c96b2cfdc1d5221f93f7e0068f3bf30c527b58 Mon Sep 17 00:00:00 2001 From: Flavien Binet Date: Thu, 15 Oct 2020 10:38:31 +0200 Subject: [PATCH 21/32] moved biscuit pkg to biscuit-go repo --- pkg/biscuit/biscuit.go | 163 ------------- pkg/biscuit/biscuit_test.go | 67 ------ pkg/biscuit/signature.go | 176 -------------- pkg/biscuit/signature_test.go | 230 ------------------ pkg/biscuit/wrapper.go | 426 ---------------------------------- 5 files changed, 1062 deletions(-) delete mode 100644 pkg/biscuit/biscuit.go delete mode 100644 pkg/biscuit/biscuit_test.go delete mode 100644 pkg/biscuit/signature.go delete mode 100644 pkg/biscuit/signature_test.go delete mode 100644 pkg/biscuit/wrapper.go diff --git a/pkg/biscuit/biscuit.go b/pkg/biscuit/biscuit.go deleted file mode 100644 index 0c7a3e4..0000000 --- a/pkg/biscuit/biscuit.go +++ /dev/null @@ -1,163 +0,0 @@ -package biscuit - -import ( - "crypto/rand" - "fmt" - "time" - - "github.com/flynn/biscuit-go" - "github.com/flynn/biscuit-go/sig" - "github.com/flynn/hubauth/pkg/kmssign" -) - -type Metadata struct { - ClientID string - UserID string - UserEmail string - IssueTime time.Time -} - -type UserKeyPair struct { - Public []byte - Private []byte -} - -// GenerateSignable returns a biscuit which will only verify after being -// signed with the private key matching the given userPubkey. -func GenerateSignable(rootKey sig.Keypair, audience string, audienceKey *kmssign.Key, userPublicKey []byte, expireTime time.Time, m *Metadata) ([]byte, error) { - builder := &hubauthBuilder{ - biscuit.NewBuilder(rand.Reader, rootKey), - } - - if err := builder.withAudienceSignature(audience, audienceKey); err != nil { - return nil, err - } - - if err := builder.withUserToSignFact(userPublicKey); err != nil { - return nil, err - } - - if err := builder.withExpire(expireTime); err != nil { - return nil, err - } - - if err := builder.withMetadata(m); err != nil { - return nil, err - } - - b, err := builder.Build() - if err != nil { - return nil, err - } - - return b.Serialize() -} - -// Sign append a user signature on the given token and return it. -// The UserKeyPair key format to provide depends on the signature algorithm: -// - for ECDSA_P256_SHA256, the private key must be encoded in SEC 1, ASN.1 DER form, -// and the public key in PKIX, ASN.1 DER form. -func Sign(token []byte, rootPubKey sig.PublicKey, userKey *UserKeyPair) ([]byte, error) { - b, err := biscuit.Unmarshal(token) - if err != nil { - return nil, fmt.Errorf("biscuit: failed to unmarshal: %w", err) - } - - v, err := b.Verify(rootPubKey) - if err != nil { - return nil, fmt.Errorf("biscuit: failed to verify: %w", err) - } - verifier := &hubauthVerifier{ - Verifier: v, - } - - toSignData, err := verifier.getUserToSignData(userKey.Public) - if err != nil { - return nil, fmt.Errorf("biscuit: failed to get to_sign data: %w", err) - } - - if err := verifier.ensureNotAlreadyUserSigned(toSignData.DataID, userKey.Public); err != nil { - return nil, fmt.Errorf("biscuit: previous signature check failed: %w", err) - } - - tokenHash, err := b.SHA256Sum(b.BlockCount()) - if err != nil { - return nil, err - } - - signData, err := userSign(tokenHash, userKey, toSignData) - if err != nil { - return nil, fmt.Errorf("biscuit: signature failed: %w", err) - } - - builder := &hubauthBlockBuilder{ - BlockBuilder: b.CreateBlock(), - } - if err := builder.withUserSignature(signData); err != nil { - return nil, fmt.Errorf("biscuit: failed to create signature block: %w", err) - } - - clientKey := sig.GenerateKeypair(rand.Reader) - b, err = b.Append(rand.Reader, clientKey, builder.Build()) - if err != nil { - return nil, fmt.Errorf("biscuit: failed to append signature block: %w", err) - } - - return b.Serialize() -} - -// Verify will verify the biscuit, the included audience and user signature, and return an error -// when anything is invalid. -func Verify(token []byte, rootPubKey sig.PublicKey, audience string, audienceKey *kmssign.Key) (*Metadata, error) { - b, err := biscuit.Unmarshal(token) - if err != nil { - return nil, fmt.Errorf("biscuit: failed to unmarshal: %w", err) - } - - v, err := b.Verify(rootPubKey) - if err != nil { - return nil, fmt.Errorf("biscuit: failed to verify: %w", err) - } - verifier := &hubauthVerifier{v} - - audienceVerificationData, err := verifier.getAudienceVerificationData(audience) - if err != nil { - return nil, fmt.Errorf("biscuit: failed to retrieve audience signature data: %w", err) - } - - if err := verifyAudienceSignature(audienceKey, audienceVerificationData); err != nil { - return nil, fmt.Errorf("biscuit: failed to verify audience signature: %w", err) - } - if err := verifier.withValidatedAudienceSignature(audienceVerificationData); err != nil { - return nil, fmt.Errorf("biscuit: failed to add validated signature: %w", err) - } - - userVerificationData, err := verifier.getUserVerificationData() - if err != nil { - return nil, fmt.Errorf("biscuit: failed to retrieve user signature data: %w", err) - } - - // TODO: improve biscuit API to allow retrieve the block index the signature is at - // so that we can still append other blocks if needed. Right now the signature MUST BE the last block. - signedTokenHash, err := b.SHA256Sum(b.BlockCount() - 1) - if err != nil { - return nil, fmt.Errorf("biscuit: failed to generate token hash: %w", err) - } - - if err := verifyUserSignature(signedTokenHash, userVerificationData); err != nil { - return nil, fmt.Errorf("biscuit: failed to verify user signature: %w", err) - } - if err := verifier.withValidatedUserSignature(userVerificationData); err != nil { - return nil, fmt.Errorf("biscuit: failed to add validated signature: %w", err) - } - - if err := verifier.withCurrentTime(time.Now()); err != nil { - return nil, fmt.Errorf("biscuit: failed to add current time: %w", err) - } - - if err := verifier.Verify(); err != nil { - return nil, fmt.Errorf("biscuit: failed to verify: %w", err) - } - - return verifier.getMetadata() -} diff --git a/pkg/biscuit/biscuit_test.go b/pkg/biscuit/biscuit_test.go deleted file mode 100644 index bf6199b..0000000 --- a/pkg/biscuit/biscuit_test.go +++ /dev/null @@ -1,67 +0,0 @@ -package biscuit - -import ( - "context" - "crypto/rand" - "testing" - "time" - - "github.com/flynn/biscuit-go/sig" - "github.com/flynn/hubauth/pkg/kmssign" - "github.com/flynn/hubauth/pkg/kmssign/kmssim" - "github.com/stretchr/testify/require" -) - -func TestBiscuit(t *testing.T) { - rootKey := sig.GenerateKeypair(rand.Reader) - audience := "http://random.audience.url" - - kms := kmssim.NewClient([]string{audience}) - audienceKey, err := kmssign.NewKey(context.Background(), kms, audience) - require.NoError(t, err) - - userKey := generateUserKeyPair(t) - metas := &Metadata{ - ClientID: "abcd", - UserEmail: "1234@example.com", - UserID: "1234", - IssueTime: time.Now(), - } - signableBiscuit, err := GenerateSignable(rootKey, audience, audienceKey, userKey.Public, time.Now().Add(5*time.Minute), metas) - require.NoError(t, err) - t.Logf("signable biscuit size: %d", len(signableBiscuit)) - - t.Run("happy path", func(t *testing.T) { - signedBiscuit, err := Sign(signableBiscuit, rootKey.Public(), userKey) - require.NoError(t, err) - t.Logf("signed biscuit size: %d", len(signedBiscuit)) - - res, err := Verify(signedBiscuit, rootKey.Public(), audience, audienceKey) - require.NoError(t, err) - require.Equal(t, metas.ClientID, res.ClientID) - require.Equal(t, metas.UserID, res.UserID) - require.Equal(t, metas.UserEmail, res.UserEmail) - require.WithinDuration(t, metas.IssueTime, res.IssueTime, 1*time.Second) - }) - - t.Run("user sign with wrong key", func(t *testing.T) { - _, err := Sign(signableBiscuit, rootKey.Public(), generateUserKeyPair(t)) - require.Error(t, err) - }) - - t.Run("verify wrong audience", func(t *testing.T) { - signedBiscuit, err := Sign(signableBiscuit, rootKey.Public(), userKey) - require.NoError(t, err) - - _, err = Verify(signedBiscuit, rootKey.Public(), "http://another.audience.url", audienceKey) - require.Error(t, err) - - wrongAudience := "http://another.audience.url" - kms := kmssim.NewClient([]string{wrongAudience}) - wrongAudienceKey, err := kmssign.NewKey(context.Background(), kms, wrongAudience) - require.NoError(t, err) - - _, err = Verify(signedBiscuit, rootKey.Public(), audience, wrongAudienceKey) - require.Error(t, err) - }) -} diff --git a/pkg/biscuit/signature.go b/pkg/biscuit/signature.go deleted file mode 100644 index 078eaa6..0000000 --- a/pkg/biscuit/signature.go +++ /dev/null @@ -1,176 +0,0 @@ -package biscuit - -import ( - "crypto" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "crypto/sha256" - "crypto/x509" - "errors" - "fmt" - "time" - - "github.com/flynn/biscuit-go" - "github.com/flynn/hubauth/pkg/kmssign" -) - -var ( - ErrUnsupportedSignatureAlg = errors.New("unsupported signature algorithm") - ErrInvalidSignature = errors.New("invalid signature") -) - -type SignatureAlg biscuit.Symbol - -const ( - ECDSA_P256_SHA256 SignatureAlg = "ECDSA_P256_SHA256" -) - -type userToSignData struct { - DataID biscuit.Integer - Alg biscuit.Symbol - Data biscuit.Bytes -} - -type userSignatureData struct { - DataID biscuit.Integer - UserPubKey biscuit.Bytes - Signature biscuit.Bytes - Nonce biscuit.Bytes - Timestamp biscuit.Date -} - -type userVerificationData struct { - DataID biscuit.Integer - Alg biscuit.Symbol - Data biscuit.Bytes - UserPubKey biscuit.Bytes - Signature biscuit.Bytes - Nonce biscuit.Bytes - Timestamp biscuit.Date -} - -func userSign(tokenHash []byte, userKey *UserKeyPair, toSignData *userToSignData) (*userSignatureData, error) { - if len(tokenHash) == 0 { - return nil, errors.New("invalid tokenHash") - } - - signerTimestamp := time.Now() - signerNonce := make([]byte, nonceSize) - if _, err := rand.Read(signerNonce); err != nil { - return nil, err - } - - var dataToSign []byte - dataToSign = append(dataToSign, toSignData.Data...) - dataToSign = append(dataToSign, tokenHash...) - dataToSign = append(dataToSign, signerNonce...) - dataToSign = append(dataToSign, []byte(signerTimestamp.Format(time.RFC3339))...) - - var signedData biscuit.Bytes - switch SignatureAlg(toSignData.Alg) { - case ECDSA_P256_SHA256: - privKey, err := x509.ParseECPrivateKey(userKey.Private) - if err != nil { - return nil, err - } - hash := sha256.Sum256(dataToSign) - signedData, err = ecdsa.SignASN1(rand.Reader, privKey, hash[:]) - if err != nil { - return nil, err - } - default: - return nil, ErrUnsupportedSignatureAlg - } - - return &userSignatureData{ - DataID: toSignData.DataID, - Nonce: signerNonce, - Signature: signedData, - Timestamp: biscuit.Date(signerTimestamp), - UserPubKey: userKey.Public, - }, nil -} - -func verifyUserSignature(signedTokenHash []byte, data *userVerificationData) error { - var signedData []byte - signedData = append(signedData, data.Data...) - signedData = append(signedData, signedTokenHash...) - signedData = append(signedData, data.Nonce...) - signedData = append(signedData, []byte(time.Time(data.Timestamp).Format(time.RFC3339))...) - - switch SignatureAlg(data.Alg) { - case ECDSA_P256_SHA256: - pk, err := x509.ParsePKIXPublicKey(data.UserPubKey) - if err != nil { - return err - } - pubkey, ok := pk.(*ecdsa.PublicKey) - if !ok { - return errors.New("invalid pubkey, not an *ecdsa.PublicKey") - } - - hash := sha256.Sum256(signedData) - if !ecdsa.VerifyASN1(pubkey, hash[:], data.Signature) { - return ErrInvalidSignature - } - return nil - default: - return ErrUnsupportedSignatureAlg - } -} - -type audienceVerificationData struct { - Audience biscuit.Symbol - Challenge biscuit.Bytes - Signature biscuit.Bytes -} - -func audienceSign(audience string, audienceKey *kmssign.Key) (*audienceVerificationData, error) { - challenge := make([]byte, challengeSize) - if _, err := rand.Reader.Read(challenge); err != nil { - return nil, err - } - - signedData := append(signStaticCtx, challenge...) - signedData = append(signedData, []byte(audience)...) - signedHash := sha256.Sum256(signedData) - signature, err := audienceKey.Sign(rand.Reader, signedHash[:], crypto.SHA256) - if err != nil { - return nil, err - } - - return &audienceVerificationData{ - Audience: biscuit.Symbol(audience), - Challenge: challenge, - Signature: signature, - }, nil -} - -func verifyAudienceSignature(audiencePubkey *kmssign.Key, data *audienceVerificationData) error { - signedData := append(signStaticCtx, data.Challenge...) - signedData = append(signedData, []byte(data.Audience)...) - hash := sha256.Sum256(signedData) - if !audiencePubkey.Verify(hash[:], data.Signature) { - return errors.New("invalid signature") - } - return nil -} - -func validatePKIXP256PublicKey(pubkey []byte) error { - key, err := x509.ParsePKIXPublicKey(pubkey) - if err != nil { - return fmt.Errorf("failed to parse PKIX, ASN.1 DER public key: %v", err) - } - - ecKey, ok := key.(*ecdsa.PublicKey) - if !ok { - return errors.New("public key is not an *ecdsa.PublicKey") - } - - if ecKey.Curve != elliptic.P256() { - return fmt.Errorf("publickey is on wrong curve, expected P256") - } - - return nil -} diff --git a/pkg/biscuit/signature_test.go b/pkg/biscuit/signature_test.go deleted file mode 100644 index 2351e26..0000000 --- a/pkg/biscuit/signature_test.go +++ /dev/null @@ -1,230 +0,0 @@ -package biscuit - -import ( - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "testing" - "time" - - "github.com/flynn/biscuit-go" - "github.com/stretchr/testify/require" -) - -func TestUserSignVerify(t *testing.T) { - tokenHash := make([]byte, 32) - _, err := rand.Read(tokenHash) - require.NoError(t, err) - - challenge := make([]byte, challengeSize) - _, err = rand.Read(challenge) - require.NoError(t, err) - - userKey := generateUserKeyPair(t) - - toSignData := &userToSignData{ - DataID: 1, - Alg: biscuit.Symbol(ECDSA_P256_SHA256), - Data: []byte("challenge"), - } - - signedData, err := userSign(tokenHash, userKey, toSignData) - require.NoError(t, err) - require.NotEmpty(t, signedData.Signature) - require.Equal(t, biscuit.Integer(1), signedData.DataID) - require.Equal(t, biscuit.Bytes(userKey.Public), signedData.UserPubKey) - - require.Len(t, signedData.Nonce, nonceSize) - zeroNonce := make([]byte, nonceSize) - require.NotEqual(t, biscuit.Bytes(zeroNonce), signedData.Nonce) - - require.WithinDuration(t, time.Now(), time.Time(signedData.Timestamp), 1*time.Second) - - require.NoError(t, verifyUserSignature(tokenHash, &userVerificationData{ - DataID: toSignData.DataID, - Alg: toSignData.Alg, - Data: toSignData.Data, - Nonce: signedData.Nonce, - Signature: signedData.Signature, - Timestamp: signedData.Timestamp, - UserPubKey: signedData.UserPubKey, - })) -} - -func TestUserSignFail(t *testing.T) { - validTokenHash := make([]byte, 32) - _, err := rand.Read(validTokenHash) - require.NoError(t, err) - - validChallenge := make([]byte, challengeSize) - _, err = rand.Read(validChallenge) - require.NoError(t, err) - - invalidPrivateKey := &UserKeyPair{ - Private: make([]byte, 32), - } - - testCases := []struct { - desc string - tokenHash []byte - userKey *UserKeyPair - data *userToSignData - expectedErr error - }{ - { - desc: "empty tokenHash", - tokenHash: []byte{}, - }, - { - desc: "unsupported alg", - tokenHash: validTokenHash, - data: &userToSignData{ - Alg: "unsupported", - }, - expectedErr: ErrUnsupportedSignatureAlg, - }, - { - desc: "wrong private key encoding", - tokenHash: validTokenHash, - data: &userToSignData{ - Alg: biscuit.Symbol(ECDSA_P256_SHA256), - }, - userKey: invalidPrivateKey, - }, - } - - for _, testCase := range testCases { - t.Run(testCase.desc, func(t *testing.T) { - _, err := userSign(testCase.tokenHash, testCase.userKey, testCase.data) - require.Error(t, err) - if testCase.expectedErr != nil { - require.Equal(t, testCase.expectedErr, err) - } - }) - } -} - -func TestVerifyUserSignatureFail(t *testing.T) { - tokenHash := []byte("token hash") - toSignData := &userToSignData{ - DataID: 1, - Alg: biscuit.Symbol(ECDSA_P256_SHA256), - Data: []byte("challenge"), - } - - userKey := generateUserKeyPair(t) - invalidKey := generateUserKeyPair(t) - - signedData, err := userSign(tokenHash, userKey, toSignData) - require.NoError(t, err) - - rsaKey, err := rsa.GenerateKey(rand.Reader, 1024) - require.NoError(t, err) - wrongKeyKind, err := x509.MarshalPKIXPublicKey(&rsaKey.PublicKey) - require.NoError(t, err) - - testCases := []struct { - desc string - tokenHash []byte - data *userVerificationData - expectedErr error - }{ - { - desc: "unsupported alg", - expectedErr: ErrUnsupportedSignatureAlg, - data: &userVerificationData{ - Alg: "unknown", - }, - }, - { - desc: "invalid pubkey encoding", - data: &userVerificationData{ - Alg: biscuit.Symbol(ECDSA_P256_SHA256), - UserPubKey: make([]byte, 32), - }, - }, - { - desc: "invalid pubkey kind", - data: &userVerificationData{ - Alg: biscuit.Symbol(ECDSA_P256_SHA256), - UserPubKey: wrongKeyKind, - }, - }, - { - desc: "wrong pubkey", - tokenHash: tokenHash, - data: &userVerificationData{ - Alg: biscuit.Symbol(ECDSA_P256_SHA256), - UserPubKey: invalidKey.Public, - Data: toSignData.Data, - DataID: toSignData.DataID, - Nonce: signedData.Nonce, - Signature: signedData.Signature, - Timestamp: signedData.Timestamp, - }, - }, - { - desc: "tampered token hash", - expectedErr: ErrInvalidSignature, - tokenHash: []byte("wrong"), - data: &userVerificationData{ - Alg: biscuit.Symbol(ECDSA_P256_SHA256), - UserPubKey: userKey.Public, - Data: toSignData.Data, - DataID: toSignData.DataID, - Nonce: signedData.Nonce, - Signature: signedData.Signature, - Timestamp: signedData.Timestamp, - }, - }, - { - desc: "tampered nonce", - expectedErr: ErrInvalidSignature, - tokenHash: tokenHash, - data: &userVerificationData{ - Alg: biscuit.Symbol(ECDSA_P256_SHA256), - UserPubKey: userKey.Public, - Data: toSignData.Data, - DataID: toSignData.DataID, - Nonce: []byte("another nonce"), - Signature: signedData.Signature, - Timestamp: signedData.Timestamp, - }, - }, - { - desc: "tampered timestamp", - expectedErr: ErrInvalidSignature, - tokenHash: tokenHash, - data: &userVerificationData{ - Alg: biscuit.Symbol(ECDSA_P256_SHA256), - UserPubKey: userKey.Public, - Data: toSignData.Data, - DataID: toSignData.DataID, - Nonce: signedData.Nonce, - Signature: signedData.Signature, - Timestamp: biscuit.Date(time.Now().Add(1 * time.Second)), - }, - }, - } - - for _, testCase := range testCases { - t.Run(testCase.desc, func(t *testing.T) { - err := verifyUserSignature(testCase.tokenHash, testCase.data) - require.Error(t, err) - if testCase.expectedErr != nil { - require.Equal(t, testCase.expectedErr, err) - } - }) - } -} - -func generateUserKeyPair(t *testing.T) *UserKeyPair { - priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - require.NoError(t, err) - - kp, err := NewECDSAKeyPair(priv) - require.NoError(t, err) - return kp -} diff --git a/pkg/biscuit/wrapper.go b/pkg/biscuit/wrapper.go deleted file mode 100644 index f39681f..0000000 --- a/pkg/biscuit/wrapper.go +++ /dev/null @@ -1,426 +0,0 @@ -package biscuit - -import ( - "bytes" - "crypto/rand" - "errors" - "fmt" - "time" - - "github.com/flynn/biscuit-go" - "github.com/flynn/biscuit-go/datalog" - "github.com/flynn/hubauth/pkg/kmssign" -) - -var ( - ErrAlreadySigned = errors.New("already signed") - ErrInvalidToSignDataPrefix = errors.New("invalid to_sign data prefix") -) - -var ( - signStaticCtx = []byte("biscuit-pop-v0") - challengeSize = 16 - nonceSize = 16 -) - -type hubauthBuilder struct { - biscuit.Builder -} - -// withUserToSignFact add an authority should_sign fact and associated data to the biscuit -// with an authority caveat requiring the verifier to provide a valid_signature fact. -// the verifier is responsible of ensuring that a valid signature exists over the data. -func (b *hubauthBuilder) withUserToSignFact(userPubkey []byte) error { - dataID := biscuit.Integer(0) - - if err := validatePKIXP256PublicKey(userPubkey); err != nil { - return err - } - - if err := b.AddAuthorityFact(biscuit.Fact{Predicate: biscuit.Predicate{ - Name: "should_sign", - IDs: []biscuit.Atom{ - dataID, - biscuit.Symbol(ECDSA_P256_SHA256), - biscuit.Bytes(userPubkey), - }, - }}); err != nil { - return err - } - - challenge := make([]byte, challengeSize) - if _, err := rand.Reader.Read(challenge); err != nil { - return err - } - - if err := b.AddAuthorityFact(biscuit.Fact{Predicate: biscuit.Predicate{ - Name: "data", - IDs: []biscuit.Atom{ - dataID, - biscuit.Bytes(append(signStaticCtx, challenge...)), - }, - }}); err != nil { - return err - } - - if err := b.AddAuthorityCaveat(biscuit.Rule{ - Head: biscuit.Predicate{Name: "valid", IDs: []biscuit.Atom{biscuit.Variable(0)}}, - Body: []biscuit.Predicate{ - {Name: "should_sign", IDs: []biscuit.Atom{biscuit.SymbolAuthority, biscuit.Variable(0), biscuit.Variable(1), biscuit.Variable(2)}}, - {Name: "valid_signature", IDs: []biscuit.Atom{biscuit.Symbol("ambient"), biscuit.Variable(0), biscuit.Variable(1), biscuit.Variable(2)}}, - }, - }); err != nil { - return err - } - - return nil -} - -// withAudienceSignature add an authority audience_signature fact, containing a challenge and -// a matching signature using the audience key. -// the verifier is responsible of providing a valid_audience_signature fact, after -// verifying the signature using the audience pubkey. -func (b *hubauthBuilder) withAudienceSignature(audience string, audienceKey *kmssign.Key) error { - if len(audience) == 0 { - return errors.New("audience is required") - } - - data, err := audienceSign(audience, audienceKey) - if err != nil { - return err - } - - if err := b.AddAuthorityFact(biscuit.Fact{Predicate: biscuit.Predicate{ - Name: "audience_signature", - IDs: []biscuit.Atom{ - data.Audience, - data.Challenge, - data.Signature, - }, - }}); err != nil { - return err - } - - if err := b.AddAuthorityCaveat(biscuit.Rule{ - Head: biscuit.Predicate{Name: "valid_audience", IDs: []biscuit.Atom{biscuit.Variable(0)}}, - Body: []biscuit.Predicate{ - {Name: "audience_signature", IDs: []biscuit.Atom{biscuit.SymbolAuthority, biscuit.Variable(0), biscuit.Variable(1), biscuit.Variable(2)}}, - {Name: "valid_audience_signature", IDs: []biscuit.Atom{biscuit.Symbol("ambient"), biscuit.Variable(0), biscuit.Variable(2)}}, - }, - }); err != nil { - return err - } - - return nil -} - -func (b *hubauthBuilder) withMetadata(m *Metadata) error { - return b.AddAuthorityFact(biscuit.Fact{Predicate: biscuit.Predicate{ - Name: "metadata", - IDs: []biscuit.Atom{ - biscuit.String(m.ClientID), - biscuit.String(m.UserID), - biscuit.String(m.UserEmail), - biscuit.Date(m.IssueTime), - }, - }}) -} - -func (b *hubauthBuilder) withExpire(exp time.Time) error { - if err := b.AddAuthorityCaveat(biscuit.Rule{ - Head: biscuit.Predicate{Name: "not_expired", IDs: []biscuit.Atom{biscuit.Variable(0)}}, - Body: []biscuit.Predicate{ - {Name: "current_time", IDs: []biscuit.Atom{biscuit.Symbol("ambient"), biscuit.Variable(0)}}, - }, - Constraints: []biscuit.Constraint{{ - Name: biscuit.Variable(0), - Checker: biscuit.DateComparisonChecker{ - Comparison: datalog.DateComparisonBefore, - Date: biscuit.Date(exp), - }, - }}, - }); err != nil { - return err - } - - return nil -} - -type hubauthBlockBuilder struct { - biscuit.BlockBuilder -} - -func (b *hubauthBlockBuilder) withUserSignature(sigData *userSignatureData) error { - return b.AddFact(biscuit.Fact{Predicate: biscuit.Predicate{ - Name: "signature", - IDs: []biscuit.Atom{ - sigData.DataID, - sigData.UserPubKey, - sigData.Signature, - sigData.Nonce, - sigData.Timestamp, - }, - }}) -} - -type hubauthVerifier struct { - biscuit.Verifier -} - -func (v *hubauthVerifier) getUserToSignData(userPubKey biscuit.Bytes) (*userToSignData, error) { - toSign, err := v.Query(biscuit.Rule{ - Head: biscuit.Predicate{ - Name: "to_sign", - IDs: []biscuit.Atom{biscuit.Variable(0), biscuit.Variable(1), biscuit.Variable(2)}, - }, - Body: []biscuit.Predicate{ - { - Name: "should_sign", IDs: []biscuit.Atom{ - biscuit.SymbolAuthority, - biscuit.Variable(0), - biscuit.Variable(1), - biscuit.Bytes(userPubKey), - }, - }, { - Name: "data", IDs: []biscuit.Atom{ - biscuit.SymbolAuthority, - biscuit.Variable(0), - biscuit.Variable(2), - }, - }, - }, - }) - if err != nil { - return nil, err - } - - if g, w := len(toSign), 1; g != w { - return nil, fmt.Errorf("invalid to_sign fact count, got %d, want %d", g, w) - } - - toSignFact := toSign[0] - if g, w := len(toSignFact.IDs), 3; g != w { - return nil, fmt.Errorf("invalid to_sign fact, got %d atoms, want %d", g, w) - } - - sigData := &userToSignData{} - var ok bool - sigData.DataID, ok = toSign[0].IDs[0].(biscuit.Integer) - if !ok { - return nil, errors.New("invalid to_sign atom: dataID") - } - sigData.Alg, ok = toSign[0].IDs[1].(biscuit.Symbol) - if !ok { - return nil, errors.New("invalid to_sign atom: alg") - } - sigData.Data, ok = toSign[0].IDs[2].(biscuit.Bytes) - if !ok { - return nil, errors.New("invalid to_sign atom: data") - } - - if !bytes.HasPrefix(sigData.Data, signStaticCtx) { - return nil, ErrInvalidToSignDataPrefix - } - - return sigData, nil -} - -func (v *hubauthVerifier) ensureNotAlreadyUserSigned(dataID biscuit.Integer, userPubKey biscuit.Bytes) error { - alreadySigned, err := v.Query(biscuit.Rule{ - Head: biscuit.Predicate{Name: "already_signed", IDs: []biscuit.Atom{biscuit.Variable(0)}}, - Body: []biscuit.Predicate{ - {Name: "signature", IDs: []biscuit.Atom{dataID, userPubKey, biscuit.Variable(0)}}, - }, - }) - if err != nil { - return err - } - if len(alreadySigned) != 0 { - return ErrAlreadySigned - } - - return nil -} - -func (v *hubauthVerifier) getUserVerificationData() (*userVerificationData, error) { - toValidate, err := v.Query(biscuit.Rule{ - Head: biscuit.Predicate{ - Name: "to_validate", - IDs: []biscuit.Atom{ - biscuit.Variable(0), // dataID - biscuit.Variable(1), // alg - biscuit.Variable(2), // pubkey - biscuit.Variable(3), // data - biscuit.Variable(4), // signature - biscuit.Variable(5), // signerNonce - biscuit.Variable(6), // signerTimestamp - }}, - Body: []biscuit.Predicate{ - {Name: "should_sign", IDs: []biscuit.Atom{biscuit.SymbolAuthority, biscuit.Variable(0), biscuit.Variable(1), biscuit.Variable(2)}}, - {Name: "data", IDs: []biscuit.Atom{biscuit.SymbolAuthority, biscuit.Variable(0), biscuit.Variable(3)}}, - {Name: "signature", IDs: []biscuit.Atom{biscuit.Variable(0), biscuit.Variable(2), biscuit.Variable(4), biscuit.Variable(5), biscuit.Variable(6)}}, - }, - }) - if err != nil { - return nil, err - } - - if g, w := len(toValidate), 1; g != w { - return nil, fmt.Errorf("invalid to_validate fact count, got %d, want %d", g, w) - } - - toValidateFact := toValidate[0] - if g, w := len(toValidateFact.IDs), 7; g != w { - return nil, fmt.Errorf("invalid to_valid fact atom count, got %d, want %d", g, w) - } - - toVerify := &userVerificationData{} - var ok bool - toVerify.DataID, ok = toValidateFact.IDs[0].(biscuit.Integer) - if !ok { - return nil, errors.New("invalid to_validate atom: dataID") - } - toVerify.Alg, ok = toValidateFact.IDs[1].(biscuit.Symbol) - if !ok { - return nil, errors.New("invalid to_validate atom: alg") - } - toVerify.UserPubKey, ok = toValidateFact.IDs[2].(biscuit.Bytes) - if !ok { - return nil, errors.New("invalid to_validate atom: userPubKey") - } - toVerify.Data, ok = toValidateFact.IDs[3].(biscuit.Bytes) - if !ok { - return nil, errors.New("invalid to_validate atom: data") - } - toVerify.Signature, ok = toValidateFact.IDs[4].(biscuit.Bytes) - if !ok { - return nil, errors.New("invalid to_validate atom: signature") - } - toVerify.Nonce, ok = toValidateFact.IDs[5].(biscuit.Bytes) - if !ok { - return nil, errors.New("invalid to_validate atom: nonce") - } - toVerify.Timestamp, ok = toValidateFact.IDs[6].(biscuit.Date) - if !ok { - return nil, errors.New("invalid to_validate atom: timestamp") - } - - return toVerify, nil -} - -func (v *hubauthVerifier) withValidatedUserSignature(data *userVerificationData) error { - v.AddFact(biscuit.Fact{Predicate: biscuit.Predicate{ - Name: "valid_signature", - IDs: []biscuit.Atom{biscuit.Symbol("ambient"), data.DataID, data.Alg, data.UserPubKey}, - }}) - - return nil -} - -func (v *hubauthVerifier) getAudienceVerificationData(audience string) (*audienceVerificationData, error) { - toValidate, err := v.Query(biscuit.Rule{ - Head: biscuit.Predicate{ - Name: "audience_to_validate", - IDs: []biscuit.Atom{ - biscuit.Variable(0), // challenge - biscuit.Variable(1), // signature - }}, - Body: []biscuit.Predicate{ - {Name: "audience_signature", IDs: []biscuit.Atom{biscuit.SymbolAuthority, biscuit.Symbol(audience), biscuit.Variable(0), biscuit.Variable(1)}}, - }, - }) - if err != nil { - return nil, err - } - - if g, w := len(toValidate), 1; g != w { - return nil, fmt.Errorf("invalid audience_to_validate fact count, got %d, want %d", g, w) - } - - toValidateFact := toValidate[0] - if g, w := len(toValidateFact.IDs), 2; g != w { - return nil, fmt.Errorf("invalid audience_to_validate fact atom count, got %d, want %d", g, w) - } - - toVerify := &audienceVerificationData{Audience: biscuit.Symbol(audience)} - var ok bool - toVerify.Challenge, ok = toValidateFact.IDs[0].(biscuit.Bytes) - if !ok { - return nil, errors.New("invalid audience_to_validate atom: challenge") - } - toVerify.Signature, ok = toValidateFact.IDs[1].(biscuit.Bytes) - if !ok { - return nil, errors.New("invalid audience_to_validate atom: signature") - } - - return toVerify, nil -} - -func (v *hubauthVerifier) getMetadata() (*Metadata, error) { - metaFacts, err := v.Query(biscuit.Rule{ - Head: biscuit.Predicate{ - Name: "metadata", - IDs: []biscuit.Atom{ - biscuit.Variable(0), // clientID - biscuit.Variable(1), // userID - biscuit.Variable(2), // userEmail - biscuit.Variable(3), // issueTime - }}, - Body: []biscuit.Predicate{ - {Name: "metadata", IDs: []biscuit.Atom{biscuit.SymbolAuthority, biscuit.Variable(0), biscuit.Variable(1), biscuit.Variable(2), biscuit.Variable(3)}}, - }, - }) - if err != nil { - return nil, err - } - - if g, w := len(metaFacts), 1; g != w { - return nil, fmt.Errorf("invalid metadata fact count, got %d, want %d", g, w) - } - - metaFact := metaFacts[0] - - clientID, ok := metaFact.IDs[0].(biscuit.String) - if !ok { - return nil, errors.New("invalid metadata atom: clientID") - } - userID, ok := metaFact.IDs[1].(biscuit.String) - if !ok { - return nil, errors.New("invalid metadata atom: userID") - } - userEmail, ok := metaFact.IDs[2].(biscuit.String) - if !ok { - return nil, errors.New("invalid metadata atom: userEmail") - } - issueTime, ok := metaFact.IDs[3].(biscuit.Date) - if !ok { - return nil, errors.New("invalid metadata atom: issueTime") - } - return &Metadata{ - ClientID: string(clientID), - UserID: string(userID), - UserEmail: string(userEmail), - IssueTime: time.Time(issueTime), - }, nil -} - -func (v *hubauthVerifier) withValidatedAudienceSignature(data *audienceVerificationData) error { - v.AddFact(biscuit.Fact{Predicate: biscuit.Predicate{ - Name: "valid_audience_signature", - IDs: []biscuit.Atom{biscuit.Symbol("ambient"), data.Audience, data.Signature}, - }}) - - return nil -} - -func (v *hubauthVerifier) withCurrentTime(t time.Time) error { - v.AddFact(biscuit.Fact{Predicate: biscuit.Predicate{ - Name: "current_time", - IDs: []biscuit.Atom{ - biscuit.Symbol("ambient"), - biscuit.Date(t), - }, - }}) - - return nil -} From 068da6e8db94aa265a45336a2595d1e88ad5e346 Mon Sep 17 00:00:00 2001 From: Flavien Binet Date: Tue, 20 Oct 2020 16:50:27 +0200 Subject: [PATCH 22/32] datastore: add keyVersion field to audience --- pkg/datastore/audience.go | 3 +++ pkg/hubauth/data.go | 1 + 2 files changed, 4 insertions(+) diff --git a/pkg/datastore/audience.go b/pkg/datastore/audience.go index 9690799..50ab47a 100644 --- a/pkg/datastore/audience.go +++ b/pkg/datastore/audience.go @@ -22,6 +22,7 @@ func buildAudience(c *hubauth.Audience) *audience { Name: c.Name, Type: c.Type, ClientIDs: c.ClientIDs, + KeyVersion: c.KeyVersion, Policies: policies, CreateTime: now, UpdateTime: now, @@ -34,6 +35,7 @@ type audience struct { Type string ClientIDs []string Policies []googleUserPolicy `datastore:",flatten"` + KeyVersion int CreateTime time.Time UpdateTime time.Time } @@ -71,6 +73,7 @@ func (c *audience) Export() *hubauth.Audience { Name: c.Name, Type: c.Type, ClientIDs: c.ClientIDs, + KeyVersion: c.KeyVersion, Policies: policies, CreateTime: c.CreateTime, UpdateTime: c.UpdateTime, diff --git a/pkg/hubauth/data.go b/pkg/hubauth/data.go index a603b31..9a1b1e4 100644 --- a/pkg/hubauth/data.go +++ b/pkg/hubauth/data.go @@ -71,6 +71,7 @@ type Audience struct { URL string `json:"url"` Name string `json:"name"` Type string `json:"type"` + KeyVersion int `json:"-"` ClientIDs []string `json:"-"` Policies []*GoogleUserPolicy `json:"-"` CreateTime time.Time `json:"-"` From 1dcc2c27332ca597910f11c3be683444e46e4e9d Mon Sep 17 00:00:00 2001 From: Flavien Binet Date: Tue, 20 Oct 2020 16:51:32 +0200 Subject: [PATCH 23/32] idp: replace AudienceKeyNamer by VersionnedAudienceKeyNamer --- cmd/hubauth-ext/main.go | 6 ++++-- pkg/idp/token/bearer.go | 6 +++++- pkg/idp/token/bearer_test.go | 6 +++--- pkg/idp/token/biscuit.go | 6 +++++- pkg/idp/token/biscuit_test.go | 2 +- pkg/kmssign/kms.go | 33 ++++++++++++++++++++++++++++----- 6 files changed, 46 insertions(+), 13 deletions(-) diff --git a/cmd/hubauth-ext/main.go b/cmd/hubauth-ext/main.go index fce743d..af1ecc4 100644 --- a/cmd/hubauth-ext/main.go +++ b/cmd/hubauth-ext/main.go @@ -72,7 +72,9 @@ func main() { return string(result.Payload.Data) } - audienceKeyNamer := kmssign.AudienceKeyNameFunc(os.Getenv("PROJECT_ID"), os.Getenv("KMS_LOCATION"), os.Getenv("KMS_KEYRING")) + db := datastore.New(dsClient) + + audienceKeyNamer := kmssign.VersionnedAudienceKeyNameFunc(db, os.Getenv("PROJECT_ID"), os.Getenv("KMS_LOCATION"), os.Getenv("KMS_KEYRING")) var accessTokenBuilder token.AccessTokenBuilder var rootPubKey []byte @@ -98,7 +100,7 @@ func main() { log.Fatal(http.ListenAndServe(":"+httpPort, &ochttp.Handler{ Propagation: &propagation.HTTPFormat{}, Handler: httpapi.New(httpapi.Config{ - IdP: idp.New(datastore.New(dsClient), + IdP: idp.New(db, google.New( os.Getenv("RP_GOOGLE_CLIENT_ID"), os.Getenv("RP_GOOGLE_CLIENT_SECRET"), diff --git a/pkg/idp/token/bearer.go b/pkg/idp/token/bearer.go index 8ddb234..e56ee50 100644 --- a/pkg/idp/token/bearer.go +++ b/pkg/idp/token/bearer.go @@ -26,7 +26,11 @@ func NewBearerBuilder(kms kmssign.KMSClient, audienceKey kmssign.AudienceKeyName } func (b *bearerBuilder) Build(ctx context.Context, audience string, t *AccessTokenData) ([]byte, error) { - signKey := kmssign.NewPrivateKey(b.kms, b.audienceKey(audience), crypto.SHA256) + keyName, err := b.audienceKey(audience) + if err != nil { + return nil, fmt.Errorf("token: failed to get audience key name: %w", err) + } + signKey := kmssign.NewPrivateKey(b.kms, keyName, crypto.SHA256) exp, _ := ptypes.TimestampProto(t.ExpireTime) iss, _ := ptypes.TimestampProto(t.IssueTime) diff --git a/pkg/idp/token/bearer_test.go b/pkg/idp/token/bearer_test.go index 0789be4..5f68483 100644 --- a/pkg/idp/token/bearer_test.go +++ b/pkg/idp/token/bearer_test.go @@ -14,13 +14,13 @@ import ( "github.com/stretchr/testify/require" ) -func audienceKeyNamer(s string) string { - return fmt.Sprintf("%s_named", s) +func audienceKeyNamer(s string) (string, error) { + return fmt.Sprintf("%s_named", s), nil } func TestSignedPBBuilder(t *testing.T) { audienceName := "audience_url" - audienceKeyName := audienceKeyNamer(audienceName) + audienceKeyName, _ := audienceKeyNamer(audienceName) kms := kmssim.NewClient([]string{audienceKeyName}) builder := NewBearerBuilder(kms, audienceKeyNamer) diff --git a/pkg/idp/token/biscuit.go b/pkg/idp/token/biscuit.go index 9c997be..819a5f4 100644 --- a/pkg/idp/token/biscuit.go +++ b/pkg/idp/token/biscuit.go @@ -34,7 +34,11 @@ func (b *biscuitBuilder) Build(ctx context.Context, audience string, t *AccessTo if len(t.UserPublicKey) == 0 { return nil, ErrPublicKeyRequired } - audienceKey := kmssign.NewPrivateKey(b.kms, b.audienceKey(audience), crypto.SHA256) + keyName, err := b.audienceKey(audience) + if err != nil { + return nil, fmt.Errorf("token: failed to get audience key name: %w", err) + } + audienceKey := kmssign.NewPrivateKey(b.kms, keyName, crypto.SHA256) meta := &signedbiscuit.Metadata{ ClientID: t.ClientID, diff --git a/pkg/idp/token/biscuit_test.go b/pkg/idp/token/biscuit_test.go index 41b705f..e2c0ec4 100644 --- a/pkg/idp/token/biscuit_test.go +++ b/pkg/idp/token/biscuit_test.go @@ -18,7 +18,7 @@ import ( func TestBiscuitBuilder(t *testing.T) { audience := "https://audience.url" - audienceKeyName := audienceKeyNamer(audience) + audienceKeyName, _ := audienceKeyNamer(audience) kmsClient := kmssim.NewClient([]string{audienceKeyName}) rootKeyPair := sig.GenerateKeypair(rand.Reader) diff --git a/pkg/kmssign/kms.go b/pkg/kmssign/kms.go index 766ffe3..0fa129c 100644 --- a/pkg/kmssign/kms.go +++ b/pkg/kmssign/kms.go @@ -11,6 +11,7 @@ import ( "net/url" "strings" + "github.com/flynn/hubauth/pkg/hubauth" gax "github.com/googleapis/gax-go/v2" "golang.org/x/crypto/cryptobyte" "golang.org/x/crypto/cryptobyte/asn1" @@ -18,15 +19,37 @@ import ( kmspb "google.golang.org/genproto/googleapis/cloud/kms/v1" ) -type AudienceKeyNamer func(audience string) string +type AudienceKeyNamer func(audience string) (string, error) -func AudienceKeyNameFunc(projectID, location, keyRing string) func(string) string { - return func(aud string) string { +// AudienceKeyNameFunc returns the GCP KMS resource name of the audience key, fixed at version 1. +func AudienceKeyNameFunc(projectID, location, keyRing string) func(string) (string, error) { + return func(aud string) (string, error) { u, err := url.Parse(aud) if err != nil { - return "" + return "", err } - return fmt.Sprintf("projects/%s/locations/%s/keyRings/%s/cryptoKeys/%s/cryptoKeyVersions/1", projectID, location, keyRing, strings.Replace(u.Host, ".", "_", -1)) + return fmt.Sprintf("projects/%s/locations/%s/keyRings/%s/cryptoKeys/%s/cryptoKeyVersions/1", projectID, location, keyRing, strings.Replace(u.Host, ".", "_", -1)), nil + } +} + +// VersionnedAudienceKeyNameFunc returns the GCP KMS resource name of the audience key, fetching the version to use from the db. +func VersionnedAudienceKeyNameFunc(db hubauth.AudienceStore, projectID, location, keyRing string) func(string) (string, error) { + return func(aud string) (string, error) { + audience, err := db.GetAudience(context.Background(), aud) + if err != nil { + return "", err + } + + keyVersion := audience.KeyVersion + if keyVersion <= 0 { + keyVersion = 1 + } + + u, err := url.Parse(aud) + if err != nil { + return "", err + } + return fmt.Sprintf("projects/%s/locations/%s/keyRings/%s/cryptoKeys/%s/cryptoKeyVersions/%d", projectID, location, keyRing, strings.Replace(u.Host, ".", "_", -1), keyVersion), nil } } From 5f5f625b2cb548ed11a77de7f304d085d2757642 Mon Sep 17 00:00:00 2001 From: Flavien Binet Date: Wed, 21 Oct 2020 15:49:10 +0200 Subject: [PATCH 24/32] WIP: cli: add commands for audience key management --- pkg/cli/audiences.go | 357 +++++++++++++++++++++++++++++---- pkg/cli/audiences_test.go | 72 ++++--- pkg/cli/kms/kms.go | 15 ++ pkg/datastore/audience.go | 10 +- pkg/datastore/audience_test.go | 2 +- pkg/hubauth/data.go | 12 +- pkg/kmssign/kms.go | 17 +- 7 files changed, 411 insertions(+), 74 deletions(-) diff --git a/pkg/cli/audiences.go b/pkg/cli/audiences.go index 172b62d..4613ef0 100644 --- a/pkg/cli/audiences.go +++ b/pkg/cli/audiences.go @@ -12,19 +12,27 @@ import ( "github.com/flynn/hubauth/pkg/hubauth" "github.com/jedib0t/go-pretty/v6/table" "google.golang.org/genproto/googleapis/cloud/kms/v1" + kmspb "google.golang.org/genproto/googleapis/cloud/kms/v1" + fieldmask "google.golang.org/genproto/protobuf/field_mask" ) type audiencesCmd struct { - List audiencesListCmd `kong:"cmd,help='list audiences',default:'1'"` - Create audiencesCreateCmd `kong:"cmd,help='create audience'"` - UpdateType audienceUpdateTypeCmd `kong:"cmd,name='update-type',help='change audience type'"` - UpdateClientIDs audiencesUpdateClientsIDsCmd `kong:"cmd,name='update-client-ids',help='add or remove audience client IDs'"` - Delete audiencesDeleteCmd `kong:"cmd,help='delete audience and all its keys'"` - ListPolicies audiencesListPoliciesCmd `kong:"cmd,name='list-policies',help='list audience policies'"` - SetPolicy audiencesSetPolicyCmd `kong:"cmd,name='set-policy',help='set audience auth policy'"` - UpdatePolicy audiencesUpdatePolicyCmd `kong:"cmd,name='update-policy',help='modify audience policy api user or groups'"` - DeletePolicy audiencesDeletePolicyCmd `kong:"cmd,name='delete-policy',help='delete audience auth policy'"` - Key audiencesKeyCmd `kong:"cmd,help='get audience public key'"` + List audiencesListCmd `kong:"cmd,help='list audiences',default:'1'"` + Create audiencesCreateCmd `kong:"cmd,help='create audience'"` + UpdateType audienceUpdateTypeCmd `kong:"cmd,name='update-type',help='change audience type'"` + UpdateClientIDs audiencesUpdateClientsIDsCmd `kong:"cmd,name='update-client-ids',help='add or remove audience client IDs'"` + Delete audiencesDeleteCmd `kong:"cmd,help='delete audience and all its keys'"` + ListPolicies audiencesListPoliciesCmd `kong:"cmd,name='list-policies',help='list audience policies'"` + SetPolicy audiencesSetPolicyCmd `kong:"cmd,name='set-policy',help='set audience auth policy'"` + UpdatePolicy audiencesUpdatePolicyCmd `kong:"cmd,name='update-policy',help='modify audience policy api user or groups'"` + DeletePolicy audiencesDeletePolicyCmd `kong:"cmd,name='delete-policy',help='delete audience auth policy'"` + Key audiencesKeyCmd `kong:"cmd,help='get audience public key'"` + ListKeyVersions audiencesListKeyVersionsCmd `kong:"cmd,help='list audience key versions'"` + CreateKeyVersion audienceCreateKeyVersion `kong:"cmd,help='create a new audience key version'"` + SetKeyVersion audienceSetKeyVersion `kong:"cmd,help='set an audience key version'"` + DeleteKeyVersion audiencesDeleteKeyVersion `kong:"cmd,help='schedule an audience key version for deletion'"` + RestoreKeyVersion audiencesRestoreKeyVersion `kong:"cmd,help='restore an audience key version scheduled for deletion'"` + PruneKeyVersions audiencesPruneKeyVersions `kong:"cmd,help='schedule for deletion all unused key versions'"` } type audiencesListCmd struct{} @@ -36,9 +44,15 @@ func (c *audiencesListCmd) Run(cfg *Config) error { } t := table.NewWriter() t.SetOutputMirror(os.Stdout) - t.AppendHeader(table.Row{"URL", "Type", "ClientIDs", "CreateTime", "UpdateTime"}) + t.AppendHeader(table.Row{"URL", "Type", "KeyVersion", "ClientIDs", "CreateTime", "UpdateTime"}) for _, audience := range audiences { - t.AppendRow(table.Row{audience.URL, audience.Type, audience.ClientIDs, audience.CreateTime, audience.UpdateTime}) + versionID := "1" + if audience.KeyVersion != "" { + split := strings.Split(audience.KeyVersion, "/") + versionID = split[len(split)-1] + } + + t.AppendRow(table.Row{audience.URL, audience.Type, versionID, audience.ClientIDs, audience.CreateTime, audience.UpdateTime}) } t.Render() return nil @@ -88,10 +102,17 @@ func (c *audiencesCreateCmd) Run(cfg *Config) error { return fmt.Errorf("error creating audience key: %w", err) } + // TODO: versions may not be "1" when recreating a previously deleted audience, better fetch the proper version from KMS. + keyVersion, err := cryptoKeyVersion(cfg.ProjectID, c.KMSLocation, c.KMSKeyring, c.URL, 1) + if err != nil { + return fmt.Errorf("invalid key version: %w", err) + } + return cfg.DB.CreateAudience(ctx, &hubauth.Audience{ - URL: "https://" + u.Host, - Type: c.Type, - ClientIDs: c.ClientIDs, + URL: "https://" + u.Host, + Type: c.Type, + ClientIDs: c.ClientIDs, + KeyVersion: keyVersion, }) } @@ -102,7 +123,7 @@ type audienceUpdateTypeCmd struct { func (c *audienceUpdateTypeCmd) Run(cfg *Config) error { return cfg.DB.MutateAudience(context.Background(), c.AudienceURL, []*hubauth.AudienceMutation{{ - Op: hubauth.AudienceMutationSetType, + Op: hubauth.AudienceMutationOpSetType, Type: c.AudienceType, }}) @@ -139,18 +160,13 @@ type audiencesDeleteCmd struct { } func (c *audiencesDeleteCmd) Run(cfg *Config) error { - u, err := url.Parse(c.AudienceURL) + keyName, err := cryptoKeyName(cfg.ProjectID, c.KMSLocation, c.KMSKeyring, c.AudienceURL) if err != nil { - return fmt.Errorf("error parsing audience URL: %w", err) + return fmt.Errorf("invalid key name: %w", err) } versions, err := cfg.KMS.ListCryptoKeyVersions(context.Background(), &kms.ListCryptoKeyVersionsRequest{ - Parent: fmt.Sprintf("projects/%s/locations/%s/keyRings/%s/cryptoKeys/%s", - cfg.ProjectID, - c.KMSLocation, - c.KMSKeyring, - strings.Replace(u.Host, ".", "_", -1), - ), + Parent: keyName, }) if err != nil { @@ -262,26 +278,299 @@ type audiencesKeyCmd struct { func (c *audiencesKeyCmd) Run(cfg *Config) error { ctx := context.Background() - - u, err := url.Parse(c.URL) + audience, err := cfg.DB.GetAudience(ctx, c.URL) if err != nil { - return fmt.Errorf("error parsing audience URL: %w", err) - } - if u.Scheme != "https" { - return fmt.Errorf("audience URL must be https://") + return fmt.Errorf("failed to retrieve audience: %w", err) } - if u.Path != "" { - return fmt.Errorf("unexpected path in audience URL") + if audience.KeyVersion == "" { + v, err := cryptoKeyVersion(cfg.ProjectID, c.KMSLocation, c.KMSKeyring, audience.URL, 1) + if err != nil { + return fmt.Errorf("invalid key version: %w", err) + } + audience.KeyVersion = v } res, err := cfg.KMS.GetPublicKey(ctx, &kms.GetPublicKeyRequest{ - Name: fmt.Sprintf("projects/%s/locations/%s/keyRings/%s/cryptoKeys/%s/cryptoKeyVersions/1", cfg.ProjectID, c.KMSLocation, c.KMSKeyring, strings.Replace(u.Host, ".", "_", -1)), + Name: audience.KeyVersion, }) if err != nil { - return err + return fmt.Errorf("failed to fetch public key: %w", err) } b, _ := pem.Decode([]byte(res.Pem)) fmt.Println(base64.URLEncoding.EncodeToString(b.Bytes)) return nil } + +type audiencesListKeyVersionsCmd struct { + URL string `kong:"required,name='audience-url',help='audience URL'"` + KMSLocation string `kong:"name='kms-location',default='us',help='KMS keyring location'"` + KMSKeyring string `kong:"name='kms-keyring',default='hubauth-audiences-us',help='KMS keyring name'"` +} + +func (c *audiencesListKeyVersionsCmd) Run(cfg *Config) error { + audience, err := cfg.DB.GetAudience(context.Background(), c.URL) + if err != nil { + return fmt.Errorf("failed to retrieve audience: %w", err) + } + if audience.KeyVersion == "" { + v, err := cryptoKeyVersion(cfg.ProjectID, c.KMSLocation, c.KMSKeyring, audience.URL, 1) + if err != nil { + return fmt.Errorf("invalid key version: %w", err) + } + audience.KeyVersion = v + } + + keyName, err := cryptoKeyName(cfg.ProjectID, c.KMSLocation, c.KMSKeyring, audience.URL) + if err != nil { + return fmt.Errorf("invalid key name: %w", err) + } + versions, err := cfg.KMS.ListCryptoKeyVersions(context.Background(), &kms.ListCryptoKeyVersionsRequest{ + Parent: keyName, + }) + if err != nil { + return fmt.Errorf("failed to list versions: %w", err) + } + + t := table.NewWriter() + t.SetOutputMirror(os.Stdout) + t.AppendHeader(table.Row{"Version", "Active", "State", "Alg", "CreateTime", "DestroyTime"}) + for _, v := range versions { + split := strings.Split(v.Name, "/") + versionID := split[len(split)-1] + + active := "" + if v.Name == audience.KeyVersion { + active = "yes" + } + + destroyedAt := "" + if v.DestroyTime != nil { + destroyedAt = v.DestroyTime.AsTime().String() + } + + t.AppendRow(table.Row{versionID, active, v.State, v.Algorithm, v.CreateTime.AsTime(), destroyedAt}) + } + t.Render() + + return nil +} + +type audienceCreateKeyVersion struct { + URL string `kong:"required,name='audience-url',help='audience URL'"` + KMSLocation string `kong:"name='kms-location',default='us',help='KMS keyring location'"` + KMSKeyring string `kong:"name='kms-keyring',default='hubauth-audiences-us',help='KMS keyring name'"` +} + +func (c *audienceCreateKeyVersion) Run(cfg *Config) error { + audience, err := cfg.DB.GetAudience(context.Background(), c.URL) + if err != nil { + return fmt.Errorf("failed to retrieve audience: %w", err) + } + + keyName, err := cryptoKeyName(cfg.ProjectID, c.KMSLocation, c.KMSKeyring, audience.URL) + if err != nil { + return fmt.Errorf("invalid key name: %w", err) + } + + v, err := cfg.KMS.CreateCryptoKeyVersion(context.Background(), &kms.CreateCryptoKeyVersionRequest{ + Parent: keyName, + }) + if err != nil { + return fmt.Errorf("error creating audience key: %w", err) + } + + fmt.Println(v.Name) + + return nil +} + +type audienceSetKeyVersion struct { + URL string `kong:"required,name='audience-url',help='audience URL'"` + KeyVersion int `kong:"required,name='key-version',help='key version'"` + KMSLocation string `kong:"name='kms-location',default='us',help='KMS keyring location'"` + KMSKeyring string `kong:"name='kms-keyring',default='hubauth-audiences-us',help='KMS keyring name'"` +} + +func (c *audienceSetKeyVersion) Run(cfg *Config) error { + keyVersion, err := cryptoKeyVersion(cfg.ProjectID, c.KMSLocation, c.KMSKeyring, c.URL, c.KeyVersion) + if err != nil { + return fmt.Errorf("invalid key version: %w", err) + } + + // ensure key exists and is in enabled state + _, err = cfg.KMS.GetPublicKey(context.Background(), &kms.GetPublicKeyRequest{ + Name: keyVersion, + }) + if err != nil { + return fmt.Errorf("failed to retrieve key version: %w", err) + } + + return cfg.DB.MutateAudience(context.Background(), c.URL, []*hubauth.AudienceMutation{{ + Op: hubauth.AudienceMutationOpSetKeyVersion, + KeyVersion: keyVersion, + }}) +} + +type audiencesDeleteKeyVersion struct { + URL string `kong:"required,name='audience-url',help='audience URL'"` + KeyVersion int `kong:"required,name='key-version',help='key version'"` + KMSLocation string `kong:"name='kms-location',default='us',help='KMS keyring location'"` + KMSKeyring string `kong:"name='kms-keyring',default='hubauth-audiences-us',help='KMS keyring name'"` +} + +func (c *audiencesDeleteKeyVersion) Run(cfg *Config) error { + audience, err := cfg.DB.GetAudience(context.Background(), c.URL) + if err != nil { + return fmt.Errorf("failed to retrieve audience: %w", err) + } + if audience.KeyVersion == "" { + v, err := cryptoKeyVersion(cfg.ProjectID, c.KMSLocation, c.KMSKeyring, audience.URL, 1) + if err != nil { + return fmt.Errorf("invalid key version: %w", err) + } + audience.KeyVersion = v + } + + keyVersion, err := cryptoKeyVersion(cfg.ProjectID, c.KMSLocation, c.KMSKeyring, audience.URL, c.KeyVersion) + if err != nil { + return fmt.Errorf("invalid key version: %w", err) + } + + if audience.KeyVersion == keyVersion { + return fmt.Errorf("cannot delete current active key version %d, set another version first", c.KeyVersion) + } + + if _, err = cfg.KMS.DestroyCryptoKeyVersion(context.Background(), &kms.DestroyCryptoKeyVersionRequest{ + Name: keyVersion, + }); err != nil { + return fmt.Errorf("failed to delete crypto key version: %w", err) + } + + return nil +} + +type audiencesRestoreKeyVersion struct { + URL string `kong:"required,name='audience-url',help='audience URL'"` + KeyVersion int `kong:"required,name='key-version',help='key version'"` + KMSLocation string `kong:"name='kms-location',default='us',help='KMS keyring location'"` + KMSKeyring string `kong:"name='kms-keyring',default='hubauth-audiences-us',help='KMS keyring name'"` +} + +func (c *audiencesRestoreKeyVersion) Run(cfg *Config) error { + keyVersion, err := cryptoKeyVersion(cfg.ProjectID, c.KMSLocation, c.KMSKeyring, c.URL, c.KeyVersion) + if err != nil { + return fmt.Errorf("invalid key version: %w", err) + } + + key, err := cfg.KMS.RestoreCryptoKeyVersion(context.Background(), &kms.RestoreCryptoKeyVersionRequest{ + Name: keyVersion, + }) + if err != nil { + return err + } + + // restored keys are in disabled state, so this enable it + _, err = cfg.KMS.UpdateCryptoKeyVersion(context.Background(), &kms.UpdateCryptoKeyVersionRequest{ + CryptoKeyVersion: &kmspb.CryptoKeyVersion{ + Name: key.Name, + State: kmspb.CryptoKeyVersion_ENABLED, + }, + UpdateMask: &fieldmask.FieldMask{ + Paths: []string{"state"}, + }, + }) + if err != nil { + return err + } + + return nil +} + +type audiencesPruneKeyVersions struct { + URL string `kong:"name='audience-url',help='audience URL'"` + KMSLocation string `kong:"name='kms-location',default='us',help='KMS keyring location'"` + KMSKeyring string `kong:"name='kms-keyring',default='hubauth-audiences-us',help='KMS keyring name'"` +} + +func (c *audiencesPruneKeyVersions) Run(cfg *Config) error { + var audiences []*hubauth.Audience + if c.URL != "" { + aud, err := cfg.DB.GetAudience(context.Background(), c.URL) + if err != nil { + return fmt.Errorf("failed to retrieve audience: %w", err) + } + audiences = []*hubauth.Audience{aud} + } else { + var err error + audiences, err = cfg.DB.ListAudiences(context.Background()) + if err != nil { + return fmt.Errorf("failed to fetch audiences: %w", err) + } + } + + for _, audience := range audiences { + if audience.KeyVersion == "" { + v, err := cryptoKeyVersion(cfg.ProjectID, c.KMSLocation, c.KMSKeyring, audience.URL, 1) + if err != nil { + return fmt.Errorf("invalid key version: %w", err) + } + audience.KeyVersion = v + } + + keyName, err := cryptoKeyName(cfg.ProjectID, c.KMSLocation, c.KMSKeyring, audience.URL) + if err != nil { + return fmt.Errorf("invalid key name: %w", err) + } + versions, err := cfg.KMS.ListCryptoKeyVersions(context.Background(), &kmspb.ListCryptoKeyVersionsRequest{ + Parent: keyName, + }) + if err != nil { + return fmt.Errorf("failed to list key versions: %w", err) + } + + for _, v := range versions { + if v.Name == audience.KeyVersion { + fmt.Printf("skipped currently used key version %q\n", v.Name) + continue + } + + if v.State == kmspb.CryptoKeyVersion_DESTROY_SCHEDULED || v.State == kms.CryptoKeyVersion_DESTROYED { + fmt.Printf("skipped already destroyed key version %q\n", v.Name) + continue + } + + _, err = cfg.KMS.DestroyCryptoKeyVersion(context.Background(), &kmspb.DestroyCryptoKeyVersionRequest{ + Name: v.Name, + }) + if err != nil { + return fmt.Errorf("failed to schedule key version for deletion: %w", err) + } + fmt.Printf("key version %q scheduled for deletion\n", v.Name) + } + } + + return nil +} + +func cryptoKeyName(projectID, kmsLocation, kmsKeyring string, audienceURL string) (string, error) { + u, err := url.Parse(audienceURL) + if err != nil { + return "", err + } + + return fmt.Sprintf("projects/%s/locations/%s/keyRings/%s/cryptoKeys/%s", + projectID, + kmsLocation, + kmsKeyring, + strings.Replace(u.Host, ".", "_", -1), + ), nil +} + +func cryptoKeyVersion(projectID, kmsLocation, kmsKeyring string, audienceURL string, version int) (string, error) { + name, err := cryptoKeyName(projectID, kmsLocation, kmsKeyring, audienceURL) + if err != nil { + return "", err + } + return fmt.Sprintf("%s/cryptoKeyVersions/%d", name, version), nil +} diff --git a/pkg/cli/audiences_test.go b/pkg/cli/audiences_test.go index 5da7475..26826ae 100644 --- a/pkg/cli/audiences_test.go +++ b/pkg/cli/audiences_test.go @@ -12,6 +12,7 @@ import ( "errors" "fmt" "os" + "strings" "testing" "time" @@ -46,6 +47,18 @@ func (m *mockKMS) DestroyCryptoKeyVersion(ctx context.Context, req *kmspb.Destro args := m.Called(ctx, req) return args.Get(0).(*kmspb.CryptoKeyVersion), args.Error(1) } +func (m *mockKMS) CreateCryptoKeyVersion(ctx context.Context, req *kmspb.CreateCryptoKeyVersionRequest, opts ...gax.CallOption) (*kmspb.CryptoKeyVersion, error) { + args := m.Called(ctx, req) + return args.Get(0).(*kmspb.CryptoKeyVersion), args.Error(1) +} +func (m *mockKMS) RestoreCryptoKeyVersion(ctx context.Context, req *kmspb.RestoreCryptoKeyVersionRequest, opts ...gax.CallOption) (*kmspb.CryptoKeyVersion, error) { + args := m.Called(ctx, req) + return args.Get(0).(*kmspb.CryptoKeyVersion), args.Error(1) +} +func (m *mockKMS) UpdateCryptoKeyVersion(ctx context.Context, req *kmspb.UpdateCryptoKeyVersionRequest, opts ...gax.CallOption) (*kmspb.CryptoKeyVersion, error) { + args := m.Called(ctx, req) + return args.Get(0).(*kmspb.CryptoKeyVersion), args.Error(1) +} type mockAudienceDatastore struct { mock.Mock @@ -105,6 +118,7 @@ func TestAudiencesListCmd(t *testing.T) { { URL: "audience2URL", Type: "type2", + KeyVersion: "key/resource/name/3", ClientIDs: []string{"client3"}, CreateTime: createTime, UpdateTime: updateTime, @@ -135,9 +149,15 @@ func TestAudiencesListCmd(t *testing.T) { expectedBuf := new(bytes.Buffer) tw := table.NewWriter() tw.SetOutputMirror(expectedBuf) - tw.AppendHeader(table.Row{"URL", "Type", "ClientIDs", "CreateTime", "UpdateTime"}) + tw.AppendHeader(table.Row{"URL", "Type", "KeyVersion", "ClientIDs", "CreateTime", "UpdateTime"}) for _, a := range audiences { - tw.AppendRow(table.Row{a.URL, a.Type, a.ClientIDs, a.CreateTime, a.UpdateTime}) + expectedKeyVersion := "1" + if a.KeyVersion != "" { + split := strings.Split(a.KeyVersion, "/") + expectedKeyVersion = split[len(split)-1] + } + + tw.AppendRow(table.Row{a.URL, a.Type, expectedKeyVersion, a.ClientIDs, a.CreateTime, a.UpdateTime}) } tw.Render() @@ -187,10 +207,14 @@ func TestAudienceCreateCmd(t *testing.T) { }, }).Return(&kmspb.CryptoKey{}, nil) + expectedKeyVersion, err := cryptoKeyVersion(cfg.ProjectID, cmd.KMSLocation, cmd.KMSKeyring, cmd.URL, 1) + require.NoError(t, err) + cfg.DB.(*mockAudienceDatastore).On("CreateAudience", mock.Anything, &hubauth.Audience{ - URL: "https://audience.url.com", - Type: "flynn_controller", - ClientIDs: cmd.ClientIDs, + URL: "https://audience.url.com", + Type: "flynn_controller", + ClientIDs: cmd.ClientIDs, + KeyVersion: expectedKeyVersion, }).Return(nil) require.NoError(t, cmd.Run(cfg)) @@ -334,7 +358,13 @@ func TestAudienceKeyCmd(t *testing.T) { ProjectID: "projectID", } - expectedKeyName := fmt.Sprintf("projects/%s/locations/%s/keyRings/%s/cryptoKeys/%s/cryptoKeyVersions/1", cfg.ProjectID, cmd.KMSLocation, cmd.KMSKeyring, "audience_url") + expectedKeyVersion, err := cryptoKeyVersion(cfg.ProjectID, cmd.KMSLocation, cmd.KMSKeyring, cmd.URL, 5) + require.NoError(t, err) + + cfg.DB.(*mockAudienceDatastore).On("GetAudience", mock.Anything, cmd.URL).Return(&hubauth.Audience{ + URL: cmd.URL, + KeyVersion: expectedKeyVersion, + }, nil) privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) require.NoError(t, err) @@ -343,7 +373,7 @@ func TestAudienceKeyCmd(t *testing.T) { pubKeyPEM := string(pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: pubKeyDER})) expectedPublicKey := &kmspb.PublicKey{Pem: pubKeyPEM} - cfg.KMS.(*mockKMS).On("GetPublicKey", mock.Anything, &kmspb.GetPublicKeyRequest{Name: expectedKeyName}).Return(expectedPublicKey, nil) + cfg.KMS.(*mockKMS).On("GetPublicKey", mock.Anything, &kmspb.GetPublicKeyRequest{Name: expectedKeyVersion}).Return(expectedPublicKey, nil) r, w, err := os.Pipe() require.NoError(t, err) @@ -365,21 +395,15 @@ func TestAudienceKeyCmd(t *testing.T) { func TestAudienceKeyErrors(t *testing.T) { testCases := []struct { Desc string + GetAudienceErr error GetPublicKeyErr error ExpectedErr error AudienceURL string }{ { - Desc: "audience url fail to parse", - AudienceURL: "://audience.url", - }, - { - Desc: "audience url no https", - AudienceURL: "http://audience.url", - }, - { - Desc: "audience url path not empty", - AudienceURL: "https://audience.url/path", + Desc: "invalid audience", + GetAudienceErr: errors.New("audience not found"), + ExpectedErr: errors.New("audience not found"), }, { Desc: "fail to get public key", @@ -402,11 +426,12 @@ func TestAudienceKeyErrors(t *testing.T) { ProjectID: "projectID", } + cfg.DB.(*mockAudienceDatastore).On("GetAudience", mock.Anything, cmd.URL).Return(&hubauth.Audience{}, testCase.GetAudienceErr) cfg.KMS.(*mockKMS).On("GetPublicKey", mock.Anything, mock.Anything).Return(&kmspb.PublicKey{}, testCase.GetPublicKeyErr) err := cmd.Run(cfg) if testCase.ExpectedErr != nil { - require.Equal(t, testCase.ExpectedErr, err) + require.Equal(t, testCase.ExpectedErr, errors.Unwrap(err)) } else { require.Error(t, err) } @@ -484,12 +509,11 @@ func TestAudienceDeleteCmd(t *testing.T) { versions := []*kmspb.CryptoKeyVersion{{Name: "v1"}, {Name: "v2"}} + expectedKeyName, err := cryptoKeyName(cfg.ProjectID, cmd.KMSLocation, cmd.KMSKeyring, cmd.AudienceURL) + require.NoError(t, err) + cfg.KMS.(*mockKMS).On("ListCryptoKeyVersions", mock.Anything, &kmspb.ListCryptoKeyVersionsRequest{ - Parent: fmt.Sprintf("projects/%s/locations/%s/keyRings/%s/cryptoKeys/removed_audience_url", - cfg.ProjectID, - cmd.KMSLocation, - cmd.KMSKeyring, - ), + Parent: expectedKeyName, }).Return(versions, nil) cfg.KMS.(*mockKMS).On("DestroyCryptoKeyVersion", mock.Anything, &kms.DestroyCryptoKeyVersionRequest{Name: "v1"}).Once().Return(&kmspb.CryptoKeyVersion{}, nil) @@ -615,7 +639,7 @@ func TestAudienceUpdateTypeCmd(t *testing.T) { DB: &mockAudienceDatastore{}, } muts := []*hubauth.AudienceMutation{{ - Op: hubauth.AudienceMutationSetType, + Op: hubauth.AudienceMutationOpSetType, Type: cmd.AudienceType, }} diff --git a/pkg/cli/kms/kms.go b/pkg/cli/kms/kms.go index aba10a0..f171568 100644 --- a/pkg/cli/kms/kms.go +++ b/pkg/cli/kms/kms.go @@ -15,7 +15,10 @@ type Client interface { GetPublicKey(context.Context, *kmspb.GetPublicKeyRequest, ...gax.CallOption) (*kmspb.PublicKey, error) // ListCryptoKeyVersions differs from google KMS client interface to get rid of their *kms.CryptoKeyVersionIterator ListCryptoKeyVersions(context.Context, *kmspb.ListCryptoKeyVersionsRequest, ...gax.CallOption) ([]*kmspb.CryptoKeyVersion, error) + CreateCryptoKeyVersion(ctx context.Context, req *kmspb.CreateCryptoKeyVersionRequest, opts ...gax.CallOption) (*kmspb.CryptoKeyVersion, error) DestroyCryptoKeyVersion(context.Context, *kmspb.DestroyCryptoKeyVersionRequest, ...gax.CallOption) (*kmspb.CryptoKeyVersion, error) + RestoreCryptoKeyVersion(ctx context.Context, req *kmspb.RestoreCryptoKeyVersionRequest, opts ...gax.CallOption) (*kmspb.CryptoKeyVersion, error) + UpdateCryptoKeyVersion(ctx context.Context, req *kmspb.UpdateCryptoKeyVersionRequest, opts ...gax.CallOption) (*kmspb.CryptoKeyVersion, error) } type kms struct { @@ -58,3 +61,15 @@ func (k *kms) ListCryptoKeyVersions(ctx context.Context, req *kmspb.ListCryptoKe func (k *kms) DestroyCryptoKeyVersion(ctx context.Context, req *kmspb.DestroyCryptoKeyVersionRequest, opts ...gax.CallOption) (*kmspb.CryptoKeyVersion, error) { return k.c.DestroyCryptoKeyVersion(ctx, req, opts...) } + +func (k *kms) CreateCryptoKeyVersion(ctx context.Context, req *kmspb.CreateCryptoKeyVersionRequest, opts ...gax.CallOption) (*kmspb.CryptoKeyVersion, error) { + return k.c.CreateCryptoKeyVersion(ctx, req, opts...) +} + +func (k *kms) RestoreCryptoKeyVersion(ctx context.Context, req *kmspb.RestoreCryptoKeyVersionRequest, opts ...gax.CallOption) (*kmspb.CryptoKeyVersion, error) { + return k.c.RestoreCryptoKeyVersion(ctx, req, opts...) +} + +func (k *kms) UpdateCryptoKeyVersion(ctx context.Context, req *kmspb.UpdateCryptoKeyVersionRequest, opts ...gax.CallOption) (*kmspb.CryptoKeyVersion, error) { + return k.c.UpdateCryptoKeyVersion(ctx, req, opts...) +} diff --git a/pkg/datastore/audience.go b/pkg/datastore/audience.go index 50ab47a..7787b2b 100644 --- a/pkg/datastore/audience.go +++ b/pkg/datastore/audience.go @@ -35,7 +35,7 @@ type audience struct { Type string ClientIDs []string Policies []googleUserPolicy `datastore:",flatten"` - KeyVersion int + KeyVersion string CreateTime time.Time UpdateTime time.Time } @@ -168,12 +168,18 @@ func (s *service) MutateAudience(ctx context.Context, url string, mut []*hubauth aud.Policies = aud.Policies[:len(aud.Policies)-1] modified = true } - case hubauth.AudienceMutationSetType: + case hubauth.AudienceMutationOpSetType: if aud.Type == m.Type { continue } aud.Type = m.Type modified = true + case hubauth.AudienceMutationOpSetKeyVersion: + if aud.KeyVersion == m.KeyVersion { + continue + } + aud.KeyVersion = m.KeyVersion + modified = true default: return fmt.Errorf("datastore: unknown audience mutation op %s", m.Op) } diff --git a/pkg/datastore/audience_test.go b/pkg/datastore/audience_test.go index 61d14a2..385f04d 100644 --- a/pkg/datastore/audience_test.go +++ b/pkg/datastore/audience_test.go @@ -347,7 +347,7 @@ func TestAudienceMutate(t *testing.T) { desc: "update type", mut: []*hubauth.AudienceMutation{ { - Op: hubauth.AudienceMutationSetType, + Op: hubauth.AudienceMutationOpSetType, Type: "new-type", }, }, diff --git a/pkg/hubauth/data.go b/pkg/hubauth/data.go index 9a1b1e4..34a3fb1 100644 --- a/pkg/hubauth/data.go +++ b/pkg/hubauth/data.go @@ -71,7 +71,7 @@ type Audience struct { URL string `json:"url"` Name string `json:"name"` Type string `json:"type"` - KeyVersion int `json:"-"` + KeyVersion string `json:"-"` ClientIDs []string `json:"-"` Policies []*GoogleUserPolicy `json:"-"` CreateTime time.Time `json:"-"` @@ -91,15 +91,17 @@ const ( AudienceMutationOpDeleteClientID AudienceMutationOpSetPolicy AudienceMutationOpDeletePolicy - AudienceMutationSetType + AudienceMutationOpSetType + AudienceMutationOpSetKeyVersion ) type AudienceMutation struct { Op AudienceMutationOp - ClientID string - Type string - Policy GoogleUserPolicy + ClientID string + Type string + KeyVersion string + Policy GoogleUserPolicy } type AudiencePolicyMutationOp byte diff --git a/pkg/kmssign/kms.go b/pkg/kmssign/kms.go index 0fa129c..b52210a 100644 --- a/pkg/kmssign/kms.go +++ b/pkg/kmssign/kms.go @@ -35,21 +35,22 @@ func AudienceKeyNameFunc(projectID, location, keyRing string) func(string) (stri // VersionnedAudienceKeyNameFunc returns the GCP KMS resource name of the audience key, fetching the version to use from the db. func VersionnedAudienceKeyNameFunc(db hubauth.AudienceStore, projectID, location, keyRing string) func(string) (string, error) { return func(aud string) (string, error) { - audience, err := db.GetAudience(context.Background(), aud) + u, err := url.Parse(aud) if err != nil { return "", err } - keyVersion := audience.KeyVersion - if keyVersion <= 0 { - keyVersion = 1 - } - - u, err := url.Parse(aud) + audience, err := db.GetAudience(context.Background(), aud) if err != nil { return "", err } - return fmt.Sprintf("projects/%s/locations/%s/keyRings/%s/cryptoKeys/%s/cryptoKeyVersions/%d", projectID, location, keyRing, strings.Replace(u.Host, ".", "_", -1), keyVersion), nil + + name := audience.KeyVersion + if name == "" { + name = fmt.Sprintf("projects/%s/locations/%s/keyRings/%s/cryptoKeys/%s/cryptoKeyVersions/1", projectID, location, keyRing, strings.Replace(u.Host, ".", "_", -1)) + } + + return name, nil } } From d56215306d745931a3a27013472fc9cd75eeaf34 Mon Sep 17 00:00:00 2001 From: Flavien Binet Date: Wed, 21 Oct 2020 17:30:42 +0200 Subject: [PATCH 25/32] remove keyVersion stored in db --- cmd/hubauth-ext/main.go | 20 +++- pkg/cli/audiences.go | 186 ++++---------------------------------- pkg/cli/audiences_test.go | 42 ++++----- pkg/datastore/audience.go | 9 -- pkg/hubauth/data.go | 2 - pkg/kmssign/kms.go | 33 +++---- 6 files changed, 66 insertions(+), 226 deletions(-) diff --git a/cmd/hubauth-ext/main.go b/cmd/hubauth-ext/main.go index af1ecc4..f46890b 100644 --- a/cmd/hubauth-ext/main.go +++ b/cmd/hubauth-ext/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "encoding/json" "fmt" "log" "net/http" @@ -72,9 +73,21 @@ func main() { return string(result.Payload.Data) } - db := datastore.New(dsClient) + forcedAudienceKeyVersions := new(kmssign.ForcedAudiencesKeyVersion) + // AUDIENCE_KEYS is a env variable containing a serialized json object, holding tuples of audienceURL: keyVersion + // it allows to specify a different key to use for some audience. + // example: + // { + // "https://audience.url": "projects/PROJECT/locations/KMS_LOCATION/keyRings/KMS_KEYRING/cryptoKeys/AUDIENCE_NAME/cryptoKeyVersions/VERSION", + // "https://another.audience.url": "projects/PROJECT/locations/KMS_LOCATION/keyRings/KMS_KEYRING/cryptoKeys/AUDIENCE_NAME/cryptoKeyVersions/VERSION" + // } + if keys := os.Getenv("AUDIENCE_KEYS"); keys != "" { + if err := json.Unmarshal([]byte(keys), forcedAudienceKeyVersions); err != nil { + log.Fatalf("invalid audience keys: %v", err) + } + } - audienceKeyNamer := kmssign.VersionnedAudienceKeyNameFunc(db, os.Getenv("PROJECT_ID"), os.Getenv("KMS_LOCATION"), os.Getenv("KMS_KEYRING")) + audienceKeyNamer := kmssign.AudienceKeyNameFunc(*forcedAudienceKeyVersions, os.Getenv("PROJECT_ID"), os.Getenv("KMS_LOCATION"), os.Getenv("KMS_KEYRING")) var accessTokenBuilder token.AccessTokenBuilder var rootPubKey []byte @@ -100,7 +113,8 @@ func main() { log.Fatal(http.ListenAndServe(":"+httpPort, &ochttp.Handler{ Propagation: &propagation.HTTPFormat{}, Handler: httpapi.New(httpapi.Config{ - IdP: idp.New(db, + IdP: idp.New( + datastore.New(dsClient), google.New( os.Getenv("RP_GOOGLE_CLIENT_ID"), os.Getenv("RP_GOOGLE_CLIENT_SECRET"), diff --git a/pkg/cli/audiences.go b/pkg/cli/audiences.go index 4613ef0..80f979b 100644 --- a/pkg/cli/audiences.go +++ b/pkg/cli/audiences.go @@ -29,10 +29,8 @@ type audiencesCmd struct { Key audiencesKeyCmd `kong:"cmd,help='get audience public key'"` ListKeyVersions audiencesListKeyVersionsCmd `kong:"cmd,help='list audience key versions'"` CreateKeyVersion audienceCreateKeyVersion `kong:"cmd,help='create a new audience key version'"` - SetKeyVersion audienceSetKeyVersion `kong:"cmd,help='set an audience key version'"` DeleteKeyVersion audiencesDeleteKeyVersion `kong:"cmd,help='schedule an audience key version for deletion'"` RestoreKeyVersion audiencesRestoreKeyVersion `kong:"cmd,help='restore an audience key version scheduled for deletion'"` - PruneKeyVersions audiencesPruneKeyVersions `kong:"cmd,help='schedule for deletion all unused key versions'"` } type audiencesListCmd struct{} @@ -44,15 +42,9 @@ func (c *audiencesListCmd) Run(cfg *Config) error { } t := table.NewWriter() t.SetOutputMirror(os.Stdout) - t.AppendHeader(table.Row{"URL", "Type", "KeyVersion", "ClientIDs", "CreateTime", "UpdateTime"}) + t.AppendHeader(table.Row{"URL", "Type", "ClientIDs", "CreateTime", "UpdateTime"}) for _, audience := range audiences { - versionID := "1" - if audience.KeyVersion != "" { - split := strings.Split(audience.KeyVersion, "/") - versionID = split[len(split)-1] - } - - t.AppendRow(table.Row{audience.URL, audience.Type, versionID, audience.ClientIDs, audience.CreateTime, audience.UpdateTime}) + t.AppendRow(table.Row{audience.URL, audience.Type, audience.ClientIDs, audience.CreateTime, audience.UpdateTime}) } t.Render() return nil @@ -102,17 +94,10 @@ func (c *audiencesCreateCmd) Run(cfg *Config) error { return fmt.Errorf("error creating audience key: %w", err) } - // TODO: versions may not be "1" when recreating a previously deleted audience, better fetch the proper version from KMS. - keyVersion, err := cryptoKeyVersion(cfg.ProjectID, c.KMSLocation, c.KMSKeyring, c.URL, 1) - if err != nil { - return fmt.Errorf("invalid key version: %w", err) - } - return cfg.DB.CreateAudience(ctx, &hubauth.Audience{ - URL: "https://" + u.Host, - Type: c.Type, - ClientIDs: c.ClientIDs, - KeyVersion: keyVersion, + URL: "https://" + u.Host, + Type: c.Type, + ClientIDs: c.ClientIDs, }) } @@ -272,26 +257,20 @@ func (c *audiencesDeletePolicyCmd) Run(cfg *Config) error { type audiencesKeyCmd struct { URL string `kong:"required,name='audience-url',help='audience URL'"` + KeyVersion int `kong:"name='key-version',help='key version',default=1"` KMSLocation string `kong:"name='kms-location',default='us',help='KMS keyring location'"` KMSKeyring string `kong:"name='kms-keyring',default='hubauth-audiences-us',help='KMS keyring name'"` } func (c *audiencesKeyCmd) Run(cfg *Config) error { ctx := context.Background() - audience, err := cfg.DB.GetAudience(ctx, c.URL) + keyVersion, err := cryptoKeyVersion(cfg.ProjectID, c.KMSLocation, c.KMSKeyring, c.URL, c.KeyVersion) if err != nil { - return fmt.Errorf("failed to retrieve audience: %w", err) - } - if audience.KeyVersion == "" { - v, err := cryptoKeyVersion(cfg.ProjectID, c.KMSLocation, c.KMSKeyring, audience.URL, 1) - if err != nil { - return fmt.Errorf("invalid key version: %w", err) - } - audience.KeyVersion = v + return fmt.Errorf("invalid key version: %w", err) } res, err := cfg.KMS.GetPublicKey(ctx, &kms.GetPublicKeyRequest{ - Name: audience.KeyVersion, + Name: keyVersion, }) if err != nil { return fmt.Errorf("failed to fetch public key: %w", err) @@ -309,19 +288,7 @@ type audiencesListKeyVersionsCmd struct { } func (c *audiencesListKeyVersionsCmd) Run(cfg *Config) error { - audience, err := cfg.DB.GetAudience(context.Background(), c.URL) - if err != nil { - return fmt.Errorf("failed to retrieve audience: %w", err) - } - if audience.KeyVersion == "" { - v, err := cryptoKeyVersion(cfg.ProjectID, c.KMSLocation, c.KMSKeyring, audience.URL, 1) - if err != nil { - return fmt.Errorf("invalid key version: %w", err) - } - audience.KeyVersion = v - } - - keyName, err := cryptoKeyName(cfg.ProjectID, c.KMSLocation, c.KMSKeyring, audience.URL) + keyName, err := cryptoKeyName(cfg.ProjectID, c.KMSLocation, c.KMSKeyring, c.URL) if err != nil { return fmt.Errorf("invalid key name: %w", err) } @@ -334,22 +301,17 @@ func (c *audiencesListKeyVersionsCmd) Run(cfg *Config) error { t := table.NewWriter() t.SetOutputMirror(os.Stdout) - t.AppendHeader(table.Row{"Version", "Active", "State", "Alg", "CreateTime", "DestroyTime"}) + t.AppendHeader(table.Row{"Version", "State", "Alg", "CreateTime", "DestroyTime"}) for _, v := range versions { split := strings.Split(v.Name, "/") versionID := split[len(split)-1] - active := "" - if v.Name == audience.KeyVersion { - active = "yes" - } - destroyedAt := "" if v.DestroyTime != nil { destroyedAt = v.DestroyTime.AsTime().String() } - t.AppendRow(table.Row{versionID, active, v.State, v.Algorithm, v.CreateTime.AsTime(), destroyedAt}) + t.AppendRow(table.Row{versionID, v.State, v.Algorithm, v.CreateTime.AsTime(), destroyedAt}) } t.Render() @@ -363,12 +325,7 @@ type audienceCreateKeyVersion struct { } func (c *audienceCreateKeyVersion) Run(cfg *Config) error { - audience, err := cfg.DB.GetAudience(context.Background(), c.URL) - if err != nil { - return fmt.Errorf("failed to retrieve audience: %w", err) - } - - keyName, err := cryptoKeyName(cfg.ProjectID, c.KMSLocation, c.KMSKeyring, audience.URL) + keyName, err := cryptoKeyName(cfg.ProjectID, c.KMSLocation, c.KMSKeyring, c.URL) if err != nil { return fmt.Errorf("invalid key name: %w", err) } @@ -385,33 +342,6 @@ func (c *audienceCreateKeyVersion) Run(cfg *Config) error { return nil } -type audienceSetKeyVersion struct { - URL string `kong:"required,name='audience-url',help='audience URL'"` - KeyVersion int `kong:"required,name='key-version',help='key version'"` - KMSLocation string `kong:"name='kms-location',default='us',help='KMS keyring location'"` - KMSKeyring string `kong:"name='kms-keyring',default='hubauth-audiences-us',help='KMS keyring name'"` -} - -func (c *audienceSetKeyVersion) Run(cfg *Config) error { - keyVersion, err := cryptoKeyVersion(cfg.ProjectID, c.KMSLocation, c.KMSKeyring, c.URL, c.KeyVersion) - if err != nil { - return fmt.Errorf("invalid key version: %w", err) - } - - // ensure key exists and is in enabled state - _, err = cfg.KMS.GetPublicKey(context.Background(), &kms.GetPublicKeyRequest{ - Name: keyVersion, - }) - if err != nil { - return fmt.Errorf("failed to retrieve key version: %w", err) - } - - return cfg.DB.MutateAudience(context.Background(), c.URL, []*hubauth.AudienceMutation{{ - Op: hubauth.AudienceMutationOpSetKeyVersion, - KeyVersion: keyVersion, - }}) -} - type audiencesDeleteKeyVersion struct { URL string `kong:"required,name='audience-url',help='audience URL'"` KeyVersion int `kong:"required,name='key-version',help='key version'"` @@ -420,27 +350,11 @@ type audiencesDeleteKeyVersion struct { } func (c *audiencesDeleteKeyVersion) Run(cfg *Config) error { - audience, err := cfg.DB.GetAudience(context.Background(), c.URL) - if err != nil { - return fmt.Errorf("failed to retrieve audience: %w", err) - } - if audience.KeyVersion == "" { - v, err := cryptoKeyVersion(cfg.ProjectID, c.KMSLocation, c.KMSKeyring, audience.URL, 1) - if err != nil { - return fmt.Errorf("invalid key version: %w", err) - } - audience.KeyVersion = v - } - - keyVersion, err := cryptoKeyVersion(cfg.ProjectID, c.KMSLocation, c.KMSKeyring, audience.URL, c.KeyVersion) + keyVersion, err := cryptoKeyVersion(cfg.ProjectID, c.KMSLocation, c.KMSKeyring, c.URL, c.KeyVersion) if err != nil { return fmt.Errorf("invalid key version: %w", err) } - if audience.KeyVersion == keyVersion { - return fmt.Errorf("cannot delete current active key version %d, set another version first", c.KeyVersion) - } - if _, err = cfg.KMS.DestroyCryptoKeyVersion(context.Background(), &kms.DestroyCryptoKeyVersionRequest{ Name: keyVersion, }); err != nil { @@ -487,77 +401,17 @@ func (c *audiencesRestoreKeyVersion) Run(cfg *Config) error { return nil } -type audiencesPruneKeyVersions struct { - URL string `kong:"name='audience-url',help='audience URL'"` - KMSLocation string `kong:"name='kms-location',default='us',help='KMS keyring location'"` - KMSKeyring string `kong:"name='kms-keyring',default='hubauth-audiences-us',help='KMS keyring name'"` -} - -func (c *audiencesPruneKeyVersions) Run(cfg *Config) error { - var audiences []*hubauth.Audience - if c.URL != "" { - aud, err := cfg.DB.GetAudience(context.Background(), c.URL) - if err != nil { - return fmt.Errorf("failed to retrieve audience: %w", err) - } - audiences = []*hubauth.Audience{aud} - } else { - var err error - audiences, err = cfg.DB.ListAudiences(context.Background()) - if err != nil { - return fmt.Errorf("failed to fetch audiences: %w", err) - } - } - - for _, audience := range audiences { - if audience.KeyVersion == "" { - v, err := cryptoKeyVersion(cfg.ProjectID, c.KMSLocation, c.KMSKeyring, audience.URL, 1) - if err != nil { - return fmt.Errorf("invalid key version: %w", err) - } - audience.KeyVersion = v - } - - keyName, err := cryptoKeyName(cfg.ProjectID, c.KMSLocation, c.KMSKeyring, audience.URL) - if err != nil { - return fmt.Errorf("invalid key name: %w", err) - } - versions, err := cfg.KMS.ListCryptoKeyVersions(context.Background(), &kmspb.ListCryptoKeyVersionsRequest{ - Parent: keyName, - }) - if err != nil { - return fmt.Errorf("failed to list key versions: %w", err) - } - - for _, v := range versions { - if v.Name == audience.KeyVersion { - fmt.Printf("skipped currently used key version %q\n", v.Name) - continue - } - - if v.State == kmspb.CryptoKeyVersion_DESTROY_SCHEDULED || v.State == kms.CryptoKeyVersion_DESTROYED { - fmt.Printf("skipped already destroyed key version %q\n", v.Name) - continue - } - - _, err = cfg.KMS.DestroyCryptoKeyVersion(context.Background(), &kmspb.DestroyCryptoKeyVersionRequest{ - Name: v.Name, - }) - if err != nil { - return fmt.Errorf("failed to schedule key version for deletion: %w", err) - } - fmt.Printf("key version %q scheduled for deletion\n", v.Name) - } - } - - return nil -} - func cryptoKeyName(projectID, kmsLocation, kmsKeyring string, audienceURL string) (string, error) { u, err := url.Parse(audienceURL) if err != nil { return "", err } + if u.Scheme != "https" { + return "", fmt.Errorf("audience URL must be https://") + } + if u.Path != "" { + return "", fmt.Errorf("unexpected path in audience URL") + } return fmt.Sprintf("projects/%s/locations/%s/keyRings/%s/cryptoKeys/%s", projectID, diff --git a/pkg/cli/audiences_test.go b/pkg/cli/audiences_test.go index 26826ae..0e82d30 100644 --- a/pkg/cli/audiences_test.go +++ b/pkg/cli/audiences_test.go @@ -12,7 +12,6 @@ import ( "errors" "fmt" "os" - "strings" "testing" "time" @@ -118,7 +117,6 @@ func TestAudiencesListCmd(t *testing.T) { { URL: "audience2URL", Type: "type2", - KeyVersion: "key/resource/name/3", ClientIDs: []string{"client3"}, CreateTime: createTime, UpdateTime: updateTime, @@ -149,15 +147,9 @@ func TestAudiencesListCmd(t *testing.T) { expectedBuf := new(bytes.Buffer) tw := table.NewWriter() tw.SetOutputMirror(expectedBuf) - tw.AppendHeader(table.Row{"URL", "Type", "KeyVersion", "ClientIDs", "CreateTime", "UpdateTime"}) + tw.AppendHeader(table.Row{"URL", "Type", "ClientIDs", "CreateTime", "UpdateTime"}) for _, a := range audiences { - expectedKeyVersion := "1" - if a.KeyVersion != "" { - split := strings.Split(a.KeyVersion, "/") - expectedKeyVersion = split[len(split)-1] - } - - tw.AppendRow(table.Row{a.URL, a.Type, expectedKeyVersion, a.ClientIDs, a.CreateTime, a.UpdateTime}) + tw.AppendRow(table.Row{a.URL, a.Type, a.ClientIDs, a.CreateTime, a.UpdateTime}) } tw.Render() @@ -207,14 +199,10 @@ func TestAudienceCreateCmd(t *testing.T) { }, }).Return(&kmspb.CryptoKey{}, nil) - expectedKeyVersion, err := cryptoKeyVersion(cfg.ProjectID, cmd.KMSLocation, cmd.KMSKeyring, cmd.URL, 1) - require.NoError(t, err) - cfg.DB.(*mockAudienceDatastore).On("CreateAudience", mock.Anything, &hubauth.Audience{ - URL: "https://audience.url.com", - Type: "flynn_controller", - ClientIDs: cmd.ClientIDs, - KeyVersion: expectedKeyVersion, + URL: "https://audience.url.com", + Type: "flynn_controller", + ClientIDs: cmd.ClientIDs, }).Return(nil) require.NoError(t, cmd.Run(cfg)) @@ -350,6 +338,7 @@ func TestAudienceKeyCmd(t *testing.T) { KMSKeyring: "kmsKeyring", KMSLocation: "kmsLocation", URL: "https://audience.url", + KeyVersion: 2, } cfg := &Config{ @@ -358,12 +347,11 @@ func TestAudienceKeyCmd(t *testing.T) { ProjectID: "projectID", } - expectedKeyVersion, err := cryptoKeyVersion(cfg.ProjectID, cmd.KMSLocation, cmd.KMSKeyring, cmd.URL, 5) + expectedKeyVersion, err := cryptoKeyVersion(cfg.ProjectID, cmd.KMSLocation, cmd.KMSKeyring, cmd.URL, 2) require.NoError(t, err) cfg.DB.(*mockAudienceDatastore).On("GetAudience", mock.Anything, cmd.URL).Return(&hubauth.Audience{ - URL: cmd.URL, - KeyVersion: expectedKeyVersion, + URL: cmd.URL, }, nil) privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) @@ -401,9 +389,16 @@ func TestAudienceKeyErrors(t *testing.T) { AudienceURL string }{ { - Desc: "invalid audience", - GetAudienceErr: errors.New("audience not found"), - ExpectedErr: errors.New("audience not found"), + Desc: "audience url fail to parse", + AudienceURL: "://audience.url", + }, + { + Desc: "audience url no https", + AudienceURL: "http://audience.url", + }, + { + Desc: "audience url path not empty", + AudienceURL: "https://audience.url/path", }, { Desc: "fail to get public key", @@ -426,7 +421,6 @@ func TestAudienceKeyErrors(t *testing.T) { ProjectID: "projectID", } - cfg.DB.(*mockAudienceDatastore).On("GetAudience", mock.Anything, cmd.URL).Return(&hubauth.Audience{}, testCase.GetAudienceErr) cfg.KMS.(*mockKMS).On("GetPublicKey", mock.Anything, mock.Anything).Return(&kmspb.PublicKey{}, testCase.GetPublicKeyErr) err := cmd.Run(cfg) diff --git a/pkg/datastore/audience.go b/pkg/datastore/audience.go index 7787b2b..30b043c 100644 --- a/pkg/datastore/audience.go +++ b/pkg/datastore/audience.go @@ -22,7 +22,6 @@ func buildAudience(c *hubauth.Audience) *audience { Name: c.Name, Type: c.Type, ClientIDs: c.ClientIDs, - KeyVersion: c.KeyVersion, Policies: policies, CreateTime: now, UpdateTime: now, @@ -35,7 +34,6 @@ type audience struct { Type string ClientIDs []string Policies []googleUserPolicy `datastore:",flatten"` - KeyVersion string CreateTime time.Time UpdateTime time.Time } @@ -73,7 +71,6 @@ func (c *audience) Export() *hubauth.Audience { Name: c.Name, Type: c.Type, ClientIDs: c.ClientIDs, - KeyVersion: c.KeyVersion, Policies: policies, CreateTime: c.CreateTime, UpdateTime: c.UpdateTime, @@ -174,12 +171,6 @@ func (s *service) MutateAudience(ctx context.Context, url string, mut []*hubauth } aud.Type = m.Type modified = true - case hubauth.AudienceMutationOpSetKeyVersion: - if aud.KeyVersion == m.KeyVersion { - continue - } - aud.KeyVersion = m.KeyVersion - modified = true default: return fmt.Errorf("datastore: unknown audience mutation op %s", m.Op) } diff --git a/pkg/hubauth/data.go b/pkg/hubauth/data.go index 34a3fb1..73d30dd 100644 --- a/pkg/hubauth/data.go +++ b/pkg/hubauth/data.go @@ -71,7 +71,6 @@ type Audience struct { URL string `json:"url"` Name string `json:"name"` Type string `json:"type"` - KeyVersion string `json:"-"` ClientIDs []string `json:"-"` Policies []*GoogleUserPolicy `json:"-"` CreateTime time.Time `json:"-"` @@ -92,7 +91,6 @@ const ( AudienceMutationOpSetPolicy AudienceMutationOpDeletePolicy AudienceMutationOpSetType - AudienceMutationOpSetKeyVersion ) type AudienceMutation struct { diff --git a/pkg/kmssign/kms.go b/pkg/kmssign/kms.go index b52210a..40a7948 100644 --- a/pkg/kmssign/kms.go +++ b/pkg/kmssign/kms.go @@ -11,7 +11,6 @@ import ( "net/url" "strings" - "github.com/flynn/hubauth/pkg/hubauth" gax "github.com/googleapis/gax-go/v2" "golang.org/x/crypto/cryptobyte" "golang.org/x/crypto/cryptobyte/asn1" @@ -21,36 +20,26 @@ import ( type AudienceKeyNamer func(audience string) (string, error) -// AudienceKeyNameFunc returns the GCP KMS resource name of the audience key, fixed at version 1. -func AudienceKeyNameFunc(projectID, location, keyRing string) func(string) (string, error) { - return func(aud string) (string, error) { - u, err := url.Parse(aud) - if err != nil { - return "", err - } - return fmt.Sprintf("projects/%s/locations/%s/keyRings/%s/cryptoKeys/%s/cryptoKeyVersions/1", projectID, location, keyRing, strings.Replace(u.Host, ".", "_", -1)), nil - } -} +// ForcedAudiencesKeyVersion list audienceURL with a key version resource name +// when provided to an AudienceKeyNameFunc, it allows to override a default audience +// key version with the one from the map. +type ForcedAudiencesKeyVersion map[string]string -// VersionnedAudienceKeyNameFunc returns the GCP KMS resource name of the audience key, fetching the version to use from the db. -func VersionnedAudienceKeyNameFunc(db hubauth.AudienceStore, projectID, location, keyRing string) func(string) (string, error) { +// AudienceKeyNameFunc returns the GCP KMS resource name of the audience key. +// The default version returned is 1, and it can be overridden by adding the audience URL +// and the new key to the ForcedAudiencesKeyVersion map. +func AudienceKeyNameFunc(forcedAudiencesKeyVersion ForcedAudiencesKeyVersion, projectID, location, keyRing string) func(string) (string, error) { return func(aud string) (string, error) { u, err := url.Parse(aud) if err != nil { return "", err } - audience, err := db.GetAudience(context.Background(), aud) - if err != nil { - return "", err - } - - name := audience.KeyVersion - if name == "" { - name = fmt.Sprintf("projects/%s/locations/%s/keyRings/%s/cryptoKeys/%s/cryptoKeyVersions/1", projectID, location, keyRing, strings.Replace(u.Host, ".", "_", -1)) + if keyName, ok := forcedAudiencesKeyVersion[aud]; ok { + return keyName, nil } - return name, nil + return fmt.Sprintf("projects/%s/locations/%s/keyRings/%s/cryptoKeys/%s/cryptoKeyVersions/1", projectID, location, keyRing, strings.Replace(u.Host, ".", "_", -1)), nil } } From 0d4a8e7ffe11fe66cc36960bae5e2482d0e6dd27 Mon Sep 17 00:00:00 2001 From: Flavien Binet Date: Wed, 21 Oct 2020 17:50:26 +0200 Subject: [PATCH 26/32] cli: add tests for audience delete command --- pkg/cli/audiences.go | 8 +++-- pkg/cli/audiences_test.go | 62 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/pkg/cli/audiences.go b/pkg/cli/audiences.go index 80f979b..9bdbf9d 100644 --- a/pkg/cli/audiences.go +++ b/pkg/cli/audiences.go @@ -162,11 +162,15 @@ func (c *audiencesDeleteCmd) Run(cfg *Config) error { if _, err = cfg.KMS.DestroyCryptoKeyVersion(context.Background(), &kms.DestroyCryptoKeyVersionRequest{ Name: version.Name, }); err != nil { - return fmt.Errorf("failed to delete crypto key version %s: %v", version.Name, err) + return fmt.Errorf("failed to delete crypto key version %s: %w", version.Name, err) } } - return cfg.DB.DeleteAudience(context.Background(), c.AudienceURL) + if err := cfg.DB.DeleteAudience(context.Background(), c.AudienceURL); err != nil { + return fmt.Errorf("failed to delete audience: %w", err) + } + + return nil } type audiencesListPoliciesCmd struct { diff --git a/pkg/cli/audiences_test.go b/pkg/cli/audiences_test.go index 0e82d30..2fd06f9 100644 --- a/pkg/cli/audiences_test.go +++ b/pkg/cli/audiences_test.go @@ -383,7 +383,6 @@ func TestAudienceKeyCmd(t *testing.T) { func TestAudienceKeyErrors(t *testing.T) { testCases := []struct { Desc string - GetAudienceErr error GetPublicKeyErr error ExpectedErr error AudienceURL string @@ -517,6 +516,67 @@ func TestAudienceDeleteCmd(t *testing.T) { require.NoError(t, cmd.Run(cfg)) } +func TestAudienceDeleteErrors(t *testing.T) { + testCases := []struct { + Desc string + AudienceURL string + ListCryptoKeyVersionsErr error + DestroyCryptoKeyVersionErr error + DeleteAudienceErr error + ExpectedErr error + }{ + { + Desc: "audience url fail to parse", + AudienceURL: "://audience.url", + }, + { + Desc: "fail to list keys", + AudienceURL: "https://audience.url", + ListCryptoKeyVersionsErr: errors.New("list key versions error"), + ExpectedErr: errors.New("list key versions error"), + }, + { + Desc: "fail to destroy key", + AudienceURL: "https://audience.url", + DestroyCryptoKeyVersionErr: errors.New("destroy key error"), + ExpectedErr: errors.New("destroy key error"), + }, + { + Desc: "fail to delete audience", + AudienceURL: "https://audience.url", + DeleteAudienceErr: errors.New("delete audience error"), + ExpectedErr: errors.New("delete audience error"), + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Desc, func(t *testing.T) { + cmd := &audiencesDeleteCmd{ + AudienceURL: testCase.AudienceURL, + KMSLocation: "global", + KMSKeyring: "keyring", + } + + cfg := &Config{ + DB: &mockAudienceDatastore{}, + KMS: &mockKMS{}, + ProjectID: "projectID", + } + + cfg.KMS.(*mockKMS).On("ListCryptoKeyVersions", mock.Anything, mock.Anything).Return([]*kmspb.CryptoKeyVersion{{Name: "v1"}}, testCase.ListCryptoKeyVersionsErr) + cfg.KMS.(*mockKMS).On("DestroyCryptoKeyVersion", mock.Anything, mock.Anything).Return(&kmspb.CryptoKeyVersion{}, testCase.DestroyCryptoKeyVersionErr) + + cfg.DB.(*mockAudienceDatastore).On("DeleteAudience", mock.Anything, cmd.AudienceURL).Return(testCase.DeleteAudienceErr) + err := cmd.Run(cfg) + if testCase.ExpectedErr != nil { + require.Equal(t, testCase.ExpectedErr, errors.Unwrap(err)) + } else { + require.Error(t, err) + } + }) + } +} + func TestAudienceListPolicies(t *testing.T) { cmd := &audiencesListPoliciesCmd{ AudienceURL: "https://audience.url", From d703be811cabf7bf04f4055867073bb625b31efd Mon Sep 17 00:00:00 2001 From: Flavien Binet Date: Wed, 21 Oct 2020 17:51:00 +0200 Subject: [PATCH 27/32] cli: fix linter warns on clients_tests --- pkg/cli/clients_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/cli/clients_test.go b/pkg/cli/clients_test.go index 687a7a8..3a14ef5 100644 --- a/pkg/cli/clients_test.go +++ b/pkg/cli/clients_test.go @@ -215,7 +215,7 @@ func TestClientUpdateCmd(t *testing.T) { } cfg := &Config{DB: &mockClientDatastore{}} expectedMutations := []*hubauth.ClientMutation{ - &hubauth.ClientMutation{ + { Op: hubauth.ClientMutationOpSetRefreshTokenExpiry, RefreshTokenExpiry: 5 * time.Minute, }, @@ -232,19 +232,19 @@ func TestClientUpdateCmd(t *testing.T) { } cfg := &Config{DB: &mockClientDatastore{}} expectedMutations := []*hubauth.ClientMutation{ - &hubauth.ClientMutation{ + { Op: hubauth.ClientMutationOpSetRefreshTokenExpiry, RefreshTokenExpiry: 5 * time.Minute, }, - &hubauth.ClientMutation{ + { Op: hubauth.ClientMutationOpAddRedirectURI, RedirectURI: "http://localhost:1234", }, - &hubauth.ClientMutation{ + { Op: hubauth.ClientMutationOpAddRedirectURI, RedirectURI: "http://localhost:5678", }, - &hubauth.ClientMutation{ + { Op: hubauth.ClientMutationOpDeleteRedirectURI, RedirectURI: "http://removed-domain:1234", }, From fd20bd3798f6e7790153ef612386a3084fad6d7a Mon Sep 17 00:00:00 2001 From: Flavien Binet Date: Thu, 22 Oct 2020 10:32:44 +0200 Subject: [PATCH 28/32] cli: add tests for audiences list-key-versions --- pkg/cli/audiences_test.go | 103 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/pkg/cli/audiences_test.go b/pkg/cli/audiences_test.go index 2fd06f9..578c69f 100644 --- a/pkg/cli/audiences_test.go +++ b/pkg/cli/audiences_test.go @@ -22,6 +22,7 @@ import ( "github.com/stretchr/testify/require" "google.golang.org/genproto/googleapis/cloud/kms/v1" kmspb "google.golang.org/genproto/googleapis/cloud/kms/v1" + "google.golang.org/protobuf/types/known/timestamppb" ) type mockKMS struct { @@ -701,3 +702,105 @@ func TestAudienceUpdateTypeCmd(t *testing.T) { require.NoError(t, cmd.Run(cfg)) } + +func TestAudiencesListKeyVersionsCmd(t *testing.T) { + cmd := &audiencesListKeyVersionsCmd{ + URL: "https://audience.url", + KMSKeyring: "keyring", + KMSLocation: "location", + } + + cfg := &Config{ + KMS: &mockKMS{}, + ProjectID: "project-id", + } + + keyName, err := cryptoKeyName(cfg.ProjectID, cmd.KMSLocation, cmd.KMSKeyring, cmd.URL) + require.NoError(t, err) + + now := time.Now() + versions := []*kmspb.CryptoKeyVersion{ + { + Name: "resource/name/1", + State: kms.CryptoKeyVersion_ENABLED, + Algorithm: kms.CryptoKeyVersion_EC_SIGN_P256_SHA256, + CreateTime: timestamppb.New(now.Add(-5 * time.Minute)), + }, + { + Name: "resource/name/2", + State: kms.CryptoKeyVersion_DESTROY_SCHEDULED, + Algorithm: kms.CryptoKeyVersion_EC_SIGN_P384_SHA384, + CreateTime: timestamppb.New(now.Add(-10 * time.Second)), + DestroyTime: timestamppb.New(now), + }, + } + + cfg.KMS.(*mockKMS).On("ListCryptoKeyVersions", mock.Anything, &kms.ListCryptoKeyVersionsRequest{ + Parent: keyName, + }).Return(versions, nil) + + r, w, err := os.Pipe() + require.NoError(t, err) + origStdout := os.Stdout + os.Stdout = w + + require.NoError(t, cmd.Run(cfg)) + + os.Stdout = origStdout + buf := make([]byte, 2048) + n, err := r.Read(buf) + require.NoError(t, err) + + expectedBuf := new(bytes.Buffer) + tw := table.NewWriter() + tw.SetOutputMirror(expectedBuf) + tw.AppendHeader(table.Row{"Version", "State", "Alg", "CreateTime", "DestroyTime"}) + tw.AppendRow(table.Row{"1", kmspb.CryptoKeyVersion_ENABLED, kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256, timestamppb.New(now.Add(-5 * time.Minute)).AsTime(), ""}) + tw.AppendRow(table.Row{"2", kmspb.CryptoKeyVersion_DESTROY_SCHEDULED, kmspb.CryptoKeyVersion_EC_SIGN_P384_SHA384, timestamppb.New(now.Add(-10 * time.Second)).AsTime(), timestamppb.New(now).AsTime()}) + tw.Render() + + require.Equal(t, expectedBuf.String(), string(buf[:n])) +} + +func TestAudiencesListKeyVersionsErrors(t *testing.T) { + testCases := []struct { + Desc string + AudienceURL string + ListCryptoKeyVersionsErr error + ExpectedErr error + }{ + { + Desc: "audience url fail to parse", + AudienceURL: "://audience.url", + }, + { + Desc: "fail to list keys", + AudienceURL: "https://audience.url", + ListCryptoKeyVersionsErr: errors.New("list key versions error"), + ExpectedErr: errors.New("list key versions error"), + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Desc, func(t *testing.T) { + cmd := &audiencesListKeyVersionsCmd{ + URL: testCase.AudienceURL, + KMSLocation: "global", + KMSKeyring: "keyring", + } + + cfg := &Config{ + KMS: &mockKMS{}, + ProjectID: "projectID", + } + + cfg.KMS.(*mockKMS).On("ListCryptoKeyVersions", mock.Anything, mock.Anything).Return([]*kmspb.CryptoKeyVersion{{Name: "v1"}}, testCase.ListCryptoKeyVersionsErr) + err := cmd.Run(cfg) + if testCase.ExpectedErr != nil { + require.Equal(t, testCase.ExpectedErr, errors.Unwrap(err)) + } else { + require.Error(t, err) + } + }) + } +} From 73c1440381f639b3df64a9a8a884deca8814a1de Mon Sep 17 00:00:00 2001 From: Flavien Binet Date: Thu, 22 Oct 2020 11:15:48 +0200 Subject: [PATCH 29/32] cli: add tests for audiences create / delete / restore key versions --- pkg/cli/audiences.go | 44 +++---- pkg/cli/audiences_test.go | 244 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 266 insertions(+), 22 deletions(-) diff --git a/pkg/cli/audiences.go b/pkg/cli/audiences.go index 9bdbf9d..fbe2256 100644 --- a/pkg/cli/audiences.go +++ b/pkg/cli/audiences.go @@ -17,20 +17,20 @@ import ( ) type audiencesCmd struct { - List audiencesListCmd `kong:"cmd,help='list audiences',default:'1'"` - Create audiencesCreateCmd `kong:"cmd,help='create audience'"` - UpdateType audienceUpdateTypeCmd `kong:"cmd,name='update-type',help='change audience type'"` - UpdateClientIDs audiencesUpdateClientsIDsCmd `kong:"cmd,name='update-client-ids',help='add or remove audience client IDs'"` - Delete audiencesDeleteCmd `kong:"cmd,help='delete audience and all its keys'"` - ListPolicies audiencesListPoliciesCmd `kong:"cmd,name='list-policies',help='list audience policies'"` - SetPolicy audiencesSetPolicyCmd `kong:"cmd,name='set-policy',help='set audience auth policy'"` - UpdatePolicy audiencesUpdatePolicyCmd `kong:"cmd,name='update-policy',help='modify audience policy api user or groups'"` - DeletePolicy audiencesDeletePolicyCmd `kong:"cmd,name='delete-policy',help='delete audience auth policy'"` - Key audiencesKeyCmd `kong:"cmd,help='get audience public key'"` - ListKeyVersions audiencesListKeyVersionsCmd `kong:"cmd,help='list audience key versions'"` - CreateKeyVersion audienceCreateKeyVersion `kong:"cmd,help='create a new audience key version'"` - DeleteKeyVersion audiencesDeleteKeyVersion `kong:"cmd,help='schedule an audience key version for deletion'"` - RestoreKeyVersion audiencesRestoreKeyVersion `kong:"cmd,help='restore an audience key version scheduled for deletion'"` + List audiencesListCmd `kong:"cmd,help='list audiences',default:'1'"` + Create audiencesCreateCmd `kong:"cmd,help='create audience'"` + UpdateType audienceUpdateTypeCmd `kong:"cmd,name='update-type',help='change audience type'"` + UpdateClientIDs audiencesUpdateClientsIDsCmd `kong:"cmd,name='update-client-ids',help='add or remove audience client IDs'"` + Delete audiencesDeleteCmd `kong:"cmd,help='delete audience and all its keys'"` + ListPolicies audiencesListPoliciesCmd `kong:"cmd,name='list-policies',help='list audience policies'"` + SetPolicy audiencesSetPolicyCmd `kong:"cmd,name='set-policy',help='set audience auth policy'"` + UpdatePolicy audiencesUpdatePolicyCmd `kong:"cmd,name='update-policy',help='modify audience policy api user or groups'"` + DeletePolicy audiencesDeletePolicyCmd `kong:"cmd,name='delete-policy',help='delete audience auth policy'"` + Key audiencesKeyCmd `kong:"cmd,help='get audience public key'"` + ListKeyVersions audiencesListKeyVersionsCmd `kong:"cmd,help='list audience key versions'"` + CreateKeyVersion audiencesCreateKeyVersionCmd `kong:"cmd,help='create a new audience key version'"` + DeleteKeyVersion audiencesDeleteKeyVersionCmd `kong:"cmd,help='schedule an audience key version for deletion'"` + RestoreKeyVersion audiencesRestoreKeyVersionCmd `kong:"cmd,help='restore an audience key version scheduled for deletion'"` } type audiencesListCmd struct{} @@ -322,13 +322,13 @@ func (c *audiencesListKeyVersionsCmd) Run(cfg *Config) error { return nil } -type audienceCreateKeyVersion struct { +type audiencesCreateKeyVersionCmd struct { URL string `kong:"required,name='audience-url',help='audience URL'"` KMSLocation string `kong:"name='kms-location',default='us',help='KMS keyring location'"` KMSKeyring string `kong:"name='kms-keyring',default='hubauth-audiences-us',help='KMS keyring name'"` } -func (c *audienceCreateKeyVersion) Run(cfg *Config) error { +func (c *audiencesCreateKeyVersionCmd) Run(cfg *Config) error { keyName, err := cryptoKeyName(cfg.ProjectID, c.KMSLocation, c.KMSKeyring, c.URL) if err != nil { return fmt.Errorf("invalid key name: %w", err) @@ -346,14 +346,14 @@ func (c *audienceCreateKeyVersion) Run(cfg *Config) error { return nil } -type audiencesDeleteKeyVersion struct { +type audiencesDeleteKeyVersionCmd struct { URL string `kong:"required,name='audience-url',help='audience URL'"` KeyVersion int `kong:"required,name='key-version',help='key version'"` KMSLocation string `kong:"name='kms-location',default='us',help='KMS keyring location'"` KMSKeyring string `kong:"name='kms-keyring',default='hubauth-audiences-us',help='KMS keyring name'"` } -func (c *audiencesDeleteKeyVersion) Run(cfg *Config) error { +func (c *audiencesDeleteKeyVersionCmd) Run(cfg *Config) error { keyVersion, err := cryptoKeyVersion(cfg.ProjectID, c.KMSLocation, c.KMSKeyring, c.URL, c.KeyVersion) if err != nil { return fmt.Errorf("invalid key version: %w", err) @@ -368,14 +368,14 @@ func (c *audiencesDeleteKeyVersion) Run(cfg *Config) error { return nil } -type audiencesRestoreKeyVersion struct { +type audiencesRestoreKeyVersionCmd struct { URL string `kong:"required,name='audience-url',help='audience URL'"` KeyVersion int `kong:"required,name='key-version',help='key version'"` KMSLocation string `kong:"name='kms-location',default='us',help='KMS keyring location'"` KMSKeyring string `kong:"name='kms-keyring',default='hubauth-audiences-us',help='KMS keyring name'"` } -func (c *audiencesRestoreKeyVersion) Run(cfg *Config) error { +func (c *audiencesRestoreKeyVersionCmd) Run(cfg *Config) error { keyVersion, err := cryptoKeyVersion(cfg.ProjectID, c.KMSLocation, c.KMSKeyring, c.URL, c.KeyVersion) if err != nil { return fmt.Errorf("invalid key version: %w", err) @@ -385,7 +385,7 @@ func (c *audiencesRestoreKeyVersion) Run(cfg *Config) error { Name: keyVersion, }) if err != nil { - return err + return fmt.Errorf("failed to restore key version: %w", err) } // restored keys are in disabled state, so this enable it @@ -399,7 +399,7 @@ func (c *audiencesRestoreKeyVersion) Run(cfg *Config) error { }, }) if err != nil { - return err + return fmt.Errorf("failed to update key version state: %w", err) } return nil diff --git a/pkg/cli/audiences_test.go b/pkg/cli/audiences_test.go index 578c69f..0cdd813 100644 --- a/pkg/cli/audiences_test.go +++ b/pkg/cli/audiences_test.go @@ -22,6 +22,7 @@ import ( "github.com/stretchr/testify/require" "google.golang.org/genproto/googleapis/cloud/kms/v1" kmspb "google.golang.org/genproto/googleapis/cloud/kms/v1" + fieldmask "google.golang.org/genproto/protobuf/field_mask" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -804,3 +805,246 @@ func TestAudiencesListKeyVersionsErrors(t *testing.T) { }) } } + +func TestAudiencesCreateKeyVersionCmd(t *testing.T) { + cmd := &audiencesCreateKeyVersionCmd{ + URL: "https://audience.url", + KMSKeyring: "keyring", + KMSLocation: "location", + } + + cfg := &Config{ + KMS: &mockKMS{}, + ProjectID: "project-id", + } + + keyName, err := cryptoKeyName(cfg.ProjectID, cmd.KMSLocation, cmd.KMSKeyring, cmd.URL) + require.NoError(t, err) + + now := time.Now() + version := &kmspb.CryptoKeyVersion{ + Name: "resource/name/5", + State: kms.CryptoKeyVersion_ENABLED, + Algorithm: kms.CryptoKeyVersion_EC_SIGN_P256_SHA256, + CreateTime: timestamppb.New(now.Add(-5 * time.Minute)), + } + + cfg.KMS.(*mockKMS).On("CreateCryptoKeyVersion", mock.Anything, &kms.CreateCryptoKeyVersionRequest{ + Parent: keyName, + }).Return(version, nil) + + r, w, err := os.Pipe() + require.NoError(t, err) + origStdout := os.Stdout + os.Stdout = w + + require.NoError(t, cmd.Run(cfg)) + + os.Stdout = origStdout + buf := make([]byte, 2048) + n, err := r.Read(buf) + require.NoError(t, err) + require.Equal(t, version.Name+"\n", string(buf[:n])) +} + +func TestAudiencesCreateKeyVersionErrors(t *testing.T) { + testCases := []struct { + Desc string + AudienceURL string + CreateCryptoKeyVersionErr error + ExpectedErr error + }{ + { + Desc: "audience url fail to parse", + AudienceURL: "://audience.url", + }, + { + Desc: "fail to create key", + AudienceURL: "https://audience.url", + CreateCryptoKeyVersionErr: errors.New("create key versions error"), + ExpectedErr: errors.New("create key versions error"), + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Desc, func(t *testing.T) { + cmd := &audiencesCreateKeyVersionCmd{ + URL: testCase.AudienceURL, + KMSLocation: "global", + KMSKeyring: "keyring", + } + + cfg := &Config{ + KMS: &mockKMS{}, + ProjectID: "projectID", + } + + cfg.KMS.(*mockKMS).On("CreateCryptoKeyVersion", mock.Anything, mock.Anything).Return(&kmspb.CryptoKeyVersion{}, testCase.CreateCryptoKeyVersionErr) + + err := cmd.Run(cfg) + if testCase.ExpectedErr != nil { + require.Equal(t, testCase.ExpectedErr, errors.Unwrap(err)) + } else { + require.Error(t, err) + } + }) + } +} + +func TestAudiencesDeleteKeyVersionCmd(t *testing.T) { + cmd := &audiencesDeleteKeyVersionCmd{ + URL: "https://audience.url", + KeyVersion: 1, + KMSKeyring: "keyring", + KMSLocation: "location", + } + + cfg := &Config{ + KMS: &mockKMS{}, + ProjectID: "project-id", + } + + keyVersion, err := cryptoKeyVersion(cfg.ProjectID, cmd.KMSLocation, cmd.KMSKeyring, cmd.URL, cmd.KeyVersion) + require.NoError(t, err) + + cfg.KMS.(*mockKMS).On("DestroyCryptoKeyVersion", mock.Anything, &kms.DestroyCryptoKeyVersionRequest{ + Name: keyVersion, + }).Return(&kmspb.CryptoKeyVersion{}, nil) + + require.NoError(t, cmd.Run(cfg)) +} + +func TestAudiencesDeleteKeyVersionErrors(t *testing.T) { + testCases := []struct { + Desc string + AudienceURL string + DeleteCryptoKeyVersionErr error + ExpectedErr error + }{ + { + Desc: "audience url fail to parse", + AudienceURL: "://audience.url", + }, + { + Desc: "fail to delete key", + AudienceURL: "https://audience.url", + DeleteCryptoKeyVersionErr: errors.New("delete key versions error"), + ExpectedErr: errors.New("delete key versions error"), + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Desc, func(t *testing.T) { + cmd := &audiencesDeleteKeyVersionCmd{ + URL: testCase.AudienceURL, + KeyVersion: 12, + KMSLocation: "global", + KMSKeyring: "keyring", + } + + cfg := &Config{ + KMS: &mockKMS{}, + ProjectID: "projectID", + } + + cfg.KMS.(*mockKMS).On("DestroyCryptoKeyVersion", mock.Anything, mock.Anything).Return(&kmspb.CryptoKeyVersion{}, testCase.DeleteCryptoKeyVersionErr) + + err := cmd.Run(cfg) + if testCase.ExpectedErr != nil { + require.Equal(t, testCase.ExpectedErr, errors.Unwrap(err)) + } else { + require.Error(t, err) + } + }) + } +} + +func TestAudiencesRestoreKeyVersionCmd(t *testing.T) { + cmd := &audiencesRestoreKeyVersionCmd{ + URL: "https://audience.url", + KeyVersion: 1, + KMSKeyring: "keyring", + KMSLocation: "location", + } + + cfg := &Config{ + KMS: &mockKMS{}, + ProjectID: "project-id", + } + + keyVersion, err := cryptoKeyVersion(cfg.ProjectID, cmd.KMSLocation, cmd.KMSKeyring, cmd.URL, cmd.KeyVersion) + require.NoError(t, err) + + key := &kmspb.CryptoKeyVersion{ + Name: "key123", + } + + cfg.KMS.(*mockKMS).On("RestoreCryptoKeyVersion", mock.Anything, &kms.RestoreCryptoKeyVersionRequest{ + Name: keyVersion, + }).Return(key, nil) + + cfg.KMS.(*mockKMS).On("UpdateCryptoKeyVersion", mock.Anything, &kms.UpdateCryptoKeyVersionRequest{ + CryptoKeyVersion: &kmspb.CryptoKeyVersion{ + Name: key.Name, + State: kmspb.CryptoKeyVersion_ENABLED, + }, + UpdateMask: &fieldmask.FieldMask{ + Paths: []string{"state"}, + }, + }).Return(&kmspb.CryptoKeyVersion{}, nil) + + require.NoError(t, cmd.Run(cfg)) +} + +func TestAudiencesRestoreKeyVersionErrors(t *testing.T) { + testCases := []struct { + Desc string + AudienceURL string + RestoreCryptoKeyVersionErr error + UpdateCryptoKeyVersionErr error + ExpectedErr error + }{ + { + Desc: "audience url fail to parse", + AudienceURL: "://audience.url", + }, + { + Desc: "fail to restore key", + AudienceURL: "https://audience.url", + RestoreCryptoKeyVersionErr: errors.New("restore key version error"), + ExpectedErr: errors.New("restore key version error"), + }, + { + Desc: "fail to update key", + AudienceURL: "https://audience.url", + UpdateCryptoKeyVersionErr: errors.New("update key version error"), + ExpectedErr: errors.New("update key version error"), + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Desc, func(t *testing.T) { + cmd := &audiencesRestoreKeyVersionCmd{ + URL: testCase.AudienceURL, + KeyVersion: 12, + KMSLocation: "global", + KMSKeyring: "keyring", + } + + cfg := &Config{ + KMS: &mockKMS{}, + ProjectID: "projectID", + } + + cfg.KMS.(*mockKMS).On("RestoreCryptoKeyVersion", mock.Anything, mock.Anything).Return(&kmspb.CryptoKeyVersion{}, testCase.RestoreCryptoKeyVersionErr) + cfg.KMS.(*mockKMS).On("UpdateCryptoKeyVersion", mock.Anything, mock.Anything).Return(&kmspb.CryptoKeyVersion{}, testCase.UpdateCryptoKeyVersionErr) + + err := cmd.Run(cfg) + if testCase.ExpectedErr != nil { + require.Equal(t, testCase.ExpectedErr, errors.Unwrap(err)) + } else { + require.Error(t, err) + } + }) + } +} From 42fcd9f78f72fcd6158ca23f57e8c9e5ca545f06 Mon Sep 17 00:00:00 2001 From: Flavien Binet Date: Thu, 22 Oct 2020 11:41:20 +0200 Subject: [PATCH 30/32] kms: add AudienceKeyNameFunc test --- pkg/kmssign/kms_test.go | 64 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/pkg/kmssign/kms_test.go b/pkg/kmssign/kms_test.go index fd910f1..fb4273e 100644 --- a/pkg/kmssign/kms_test.go +++ b/pkg/kmssign/kms_test.go @@ -304,3 +304,67 @@ func TestVerifyPanicWithEmptyPubkey(t *testing.T) { t.Errorf("did not panic") } + +func TestAudienceKeyNameFunc(t *testing.T) { + testCases := []struct { + Desc string + ForcedVersions ForcedAudiencesKeyVersion + ProjectID string + Location string + KeyRing string + Audience string + ExpectedKeyName string + ExpectErr bool + }{ + { + Desc: "default key is version 1", + ForcedVersions: map[string]string{}, + ProjectID: "projectID", + Location: "location", + KeyRing: "keyRing", + Audience: "https://audience.url", + ExpectedKeyName: "projects/projectID/locations/location/keyRings/keyRing/cryptoKeys/audience_url/cryptoKeyVersions/1", + ExpectErr: false, + }, + { + Desc: "invalid audience url", + ForcedVersions: map[string]string{}, + Audience: "://audience.url", + ExpectedKeyName: "", + ExpectErr: true, + }, + { + Desc: "overriden version", + ForcedVersions: map[string]string{"https://audience.url": "new key version"}, + ProjectID: "projectID", + Location: "location", + KeyRing: "keyRing", + Audience: "https://audience.url", + ExpectedKeyName: "new key version", + ExpectErr: false, + }, + { + Desc: "overriden version different audience", + ForcedVersions: map[string]string{"https://another.url": "new key version"}, + ProjectID: "projectID", + Location: "location", + KeyRing: "keyRing", + Audience: "https://audience.url", + ExpectedKeyName: "projects/projectID/locations/location/keyRings/keyRing/cryptoKeys/audience_url/cryptoKeyVersions/1", + ExpectErr: false, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Desc, func(t *testing.T) { + f := AudienceKeyNameFunc(testCase.ForcedVersions, testCase.ProjectID, testCase.Location, testCase.KeyRing) + keyName, err := f(testCase.Audience) + if testCase.ExpectErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + require.Equal(t, testCase.ExpectedKeyName, keyName) + }) + } +} From 05a5aa1f838aed3e91747686609cf8f1c6a2b527 Mon Sep 17 00:00:00 2001 From: daeMOn Date: Wed, 28 Oct 2020 09:34:25 +0100 Subject: [PATCH 31/32] cloudbuild: add script to setup hubauth (#88) --- cloudbuild.yaml | 5 ++ script/setup-hubauth.sh | 193 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 198 insertions(+) create mode 100755 script/setup-hubauth.sh diff --git a/cloudbuild.yaml b/cloudbuild.yaml index fadd8a9..b779d5b 100644 --- a/cloudbuild.yaml +++ b/cloudbuild.yaml @@ -17,6 +17,8 @@ steps: '--image', 'gcr.io/$PROJECT_ID/$_APP:$BUILD_ID', '--region', '$_DEPLOY_REGION_PRIMARY', '--update-env-vars', 'BUILD_REPO=$REPO_NAME,BUILD_REV=$COMMIT_SHA', + '--command', '/app/hubauth-ext', + '--allow-unauthenticated' ] - name: 'gcr.io/google.com/cloudsdktool/cloud-sdk:slim' waitFor: ['build'] @@ -27,6 +29,8 @@ steps: '--image', 'gcr.io/$PROJECT_ID/$_APP:$BUILD_ID', '--region', '$_DEPLOY_REGION_FALLBACK', '--update-env-vars', 'BUILD_REPO=$REPO_NAME,BUILD_REV=$COMMIT_SHA', + '--command', '/app/hubauth-ext', + '--allow-unauthenticated' ] - name: 'gcr.io/google.com/cloudsdktool/cloud-sdk:slim' waitFor: ['build'] @@ -37,6 +41,7 @@ steps: '--image', 'gcr.io/$PROJECT_ID/$_APP:$BUILD_ID', '--region', '$_DEPLOY_REGION_PRIMARY', '--update-env-vars', 'BUILD_REPO=$REPO_NAME,BUILD_REV=$COMMIT_SHA', + '--command', '/app/hubauth-int', ] substitutions: _APP: hubauth diff --git a/script/setup-hubauth.sh b/script/setup-hubauth.sh new file mode 100755 index 0000000..831f697 --- /dev/null +++ b/script/setup-hubauth.sh @@ -0,0 +1,193 @@ +#!/usr/bin/env bash + +set -ueo pipefail + +KMS_LOCATION=${KMS_LOCATION-"global"} +KMS_KEYRING=${KMS_KEYRING-"hubauth"} +TOKEN_TYPE=${TOKEN_TYPE-"Bearer"} # || Biscuit +PROJECT_ID=${PROJECT_ID-""} + +EXPECTED_HUBAUTH_EXT_ENV=( + # env provided or script default + "KMS_LOCATION" + "KMS_KEYRING" + "TOKEN_TYPE" + # env provided or auto populated using gcloud + "PROJECT_ID" + # auto populated using gcloud + "BASE_URL" + # auto generated kms key if not existing + "REFRESH_KEY" + # auto generated secrets if not existing + "COOKIE_KEY_SECRET" + "CODE_KEY_SECRET" + "BISCUIT_ROOT_PRIVKEY" # only when TOKEN_TYPE == "Biscuit" + # need user input + "RP_GOOGLE_CLIENT_ID" + "RP_GOOGLE_CLIENT_SECRET" +) + +EXPECTED_HUBAUTH_INT_ENV=( + # env provided or auto populated using gcloud + "PROJECT_ID" +) + + +case "${TOKEN_TYPE}" in +"Bearer" | "Biscuit") + ;; +*) + echo "invalid TOKEN_TYPE \"${TOKEN_TYPE}\", must be either \"Bearer\" or \"Biscuit\"" + exit 1 + ;; +esac + +if [ $# -lt 2 ] || [[ " $@ " =~ "-h" ]] || [[ " $@ " =~ "--help" ]]; then + echo "Wizard to check and configure hubauth services and their GCP dependencies" + echo -e "\nUsage:\n$0 [-h|--help]\n" + echo -e "\nARGUMENTS:" + echo -e "\tAPP: the application to configure (\"hubauth-int\" or \"hubauth-ext\")" + echo -e "\tREGION: a GCP region where the application exists (ie: \"us-central1\")" + echo -e "\t-h | --help: print this help" + echo -e "\nENV:" + echo -e "\tPROJECT_ID (defaults to current gcloud active config project)" + echo -e "\t Prompts for confirmation when not specified" + echo -e "\tKMS_LOCATION (defaults to \"${KMS_LOCATION}\")" + echo -e "\tKMS_KEYRING (defaults to \"${KMS_KEYRING}\")" + echo -e "\tTOKEN_TYPE (defaults to \"${TOKEN_TYPE}\", accepts \"Bearer\" or \"Biscuit\")\n" + exit 1 +fi + +APP=$1 +REGION=$2 + +if [ -z "${PROJECT_ID}" ]; then + PROJECT_ID=$(gcloud config get-value project) + read -p "Current project: ${PROJECT_ID}, confirm ? [Yn]: " + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo "operation cancelled" + exit 0 + fi +fi + +GCLOUD="gcloud --project ${PROJECT_ID}" + +case "${APP}" in +"hubauth-ext") + EXPECTED_APP_ENV=${EXPECTED_HUBAUTH_EXT_ENV[@]} + ;; +"hubauth-int") + EXPECTED_APP_ENV=${EXPECTED_HUBAUTH_INT_ENV[@]} + ;; +*) + echo "invalid app, it must be either hubauth-ext or hubauth-int" + exit 1 + ;; +esac + + +SPECS=$(${GCLOUD} run services describe "${APP}-${REGION}" --platform managed --region "${REGION}" --format json) + +CONTAINER_SPECS=$(echo "${SPECS}" | jq '.spec.template.spec.containers[0]') +IMAGE=$(echo "${CONTAINER_SPECS}" | jq -r '.image') + +NEW_ENVS=() +for env in ${EXPECTED_APP_ENV[@]}; do + EXISTS=$(echo "${CONTAINER_SPECS}" | jq '.env[] | . | select(.name | contains('\"${env}\"'))') + if [ ! -z "${EXISTS}" ]; then + continue + fi + + case "${env}" in + "KMS_LOCATION") + value="${KMS_LOCATION}" + ;; + "KMS_KEYRING") + value="${KMS_KEYRING}" + ;; + "TOKEN_TYPE") + value="${TOKEN_TYPE}" + ;; + "PROJECT_ID") + value=${PROJECT_ID} + ;; + "BASE_URL") + value=$(echo "${SPECS}" | jq -r '.status.url // empty') + ;; + "COOKIE_KEY_SECRET" | "CODE_KEY_SECRET") + value=$(${GCLOUD} secrets describe "${env}" --format 'value("name")' || true) + if [ -z "${value}" ]; then + head -c 32 /dev/random | base64 -w0 | ${GCLOUD} secrets create ${env} --data-file - + value=$(${GCLOUD} secrets describe "${env}" --format 'value("name")') + fi + value+="/versions/latest" + ;; + "BISCUIT_ROOT_PRIVKEY") + if [ "${TOKEN_TYPE}" != "Biscuit" ]; then + continue + fi + value=$(${GCLOUD} secrets describe "${env}" --format 'value("name")' || true) + if [ -z "${value}" ]; then + head -c 32 /dev/random | base64 -w0 | ${GCLOUD} secrets create ${env} --data-file - + value=$(${GCLOUD} secrets describe "${env}" --format 'value("name")') + fi + value+="/versions/latest" + ;; + "REFRESH_KEY") + value=$(${GCLOUD} kms keys versions list --key "${env}" --keyring "${KMS_KEYRING}" --location "${KMS_LOCATION}" --format 'value("name")' 2>/dev/null | sort -r | head -n1 || true) + if [ -z "${value}" ]; then + ${GCLOUD} kms keys create "${env}" \ + --keyring "${KMS_KEYRING}" \ + --location "${KMS_LOCATION}" \ + --purpose "asymmetric-signing" \ + --default-algorithm "ec-sign-p256-sha256" \ + --protection-level "software" + value=$(${GCLOUD} kms keys versions list --key "${env}" --keyring "${KMS_KEYRING}" --location "${KMS_LOCATION}" --format 'value("name")') + fi + ;; + *) # default ask for user input + echo -n "enter value for ${env}: " + read value + ;; + esac + + if [ ! -z "${value}" ]; then + NEW_ENVS+=("${env}=${value}") + fi +done + +if [ ${#NEW_ENVS[@]} -gt 0 ]; then + ENV_STR=$(IFS=,; printf '%s' "${NEW_ENVS[*]}") + ${GCLOUD} run deploy "${APP}-${REGION}" --platform managed --region "${REGION}" --image "${IMAGE}" --update-env-vars "${ENV_STR}" +fi + +BASE_URL=$(${GCLOUD} run services describe "${APP}-${REGION}" --platform managed --region "${REGION}" --format json | jq -r '.status.url') + +# we need a first successful deployment in order to obtain the service URL. +# so when it was empty, and the above deploy succeeded, we can immediately redeploy setting the BASE_URL env +if [ -z "$(echo ${SPECS} | jq -r '.status.url // empty')" ] && [[ "${EXPECTED_APP_ENV[@]}" =~ "BASE_URL" ]]; then + echo "just obtained a service url for the first time, setting BASE_URL env variable..." + ${GCLOUD} run deploy "${APP}-${REGION}" --platform managed --region "${REGION}" --image "${IMAGE}" --update-env-vars "BASE_URL=${BASE_URL}" +fi + +# Create a scheduler invoking hubauth-int /cron endpoint +# Using below service account for authentication (or create it if needed) +SA_NAME="hubauth-scheduler" +SA_EMAIL="${SA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com" + +if [ ${APP} = "hubauth-int" ] && [ ! -z ${BASE_URL} ]; then + if [ -z "$(${GCLOUD} scheduler jobs describe "${APP}-${REGION}-CRON" 2>/dev/null || true)" ]; then + if [ -z "$(${GCLOUD} iam service-accounts describe "${SA_EMAIL}" 2>/dev/null || true)" ]; then + ${GCLOUD} iam service-accounts create "${SA_NAME}" --display-name="GCloud Scheduler SA" + ${GCLOUD} projects add-iam-policy-binding --quiet "${PROJECT_ID}" --member "serviceAccount:${SA_EMAIL}" --role "roles/run.invoker" + fi + + ${GCLOUD} scheduler jobs create http "${APP}-${REGION}-CRON" \ + --description "sync & cleanup task for hubauth" \ + --schedule "0 */1 * * *" \ + --uri "${BASE_URL}/cron" \ + --http-method "get" \ + --oidc-service-account-email "${SA_EMAIL}" \ + --oidc-token-audience "${BASE_URL}" + fi +fi From efa7ad60ea70c219cbee405c2b2579cee193e01c Mon Sep 17 00:00:00 2001 From: Flavien Binet Date: Tue, 10 Nov 2020 09:30:01 +0100 Subject: [PATCH 32/32] fix typos --- pkg/kmssign/kms.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/kmssign/kms.go b/pkg/kmssign/kms.go index 40a7948..e643b58 100644 --- a/pkg/kmssign/kms.go +++ b/pkg/kmssign/kms.go @@ -20,8 +20,8 @@ import ( type AudienceKeyNamer func(audience string) (string, error) -// ForcedAudiencesKeyVersion list audienceURL with a key version resource name -// when provided to an AudienceKeyNameFunc, it allows to override a default audience +// ForcedAudiencesKeyVersion lists audienceURLs with a key version resource name +// when provided to an AudienceKeyNameFunc, it allows overriding a default audience // key version with the one from the map. type ForcedAudiencesKeyVersion map[string]string