Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adding implementation for client lib #36

Merged
merged 102 commits into from
May 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
102 commits
Select commit Hold shift + click to select a range
8a97b8e
feat: add config class and logic
raserva Apr 19, 2022
5868c92
PR feedback
raserva Apr 20, 2022
7a4831a
change version from float to integer
raserva Apr 20, 2022
7f0d533
changed int8 to uint 8 in config version
raserva Apr 20, 2022
a90f317
feat: Added protobufs and gen_protos script
raserva Apr 21, 2022
7ffe726
added newline
raserva Apr 21, 2022
13a85c6
removed extra newline
raserva Apr 21, 2022
0a50402
Merge branch 'main' into rsrv-protos
raserva Apr 21, 2022
364538a
remove optional
raserva Apr 21, 2022
d914274
s/v1/v0
raserva Apr 25, 2022
ecf1a47
adding implementation for jvs server
raserva Apr 26, 2022
8b1060c
add newly generated protos
raserva Apr 26, 2022
831b63f
Merge branch 'main' into rsrv-protos
raserva Apr 26, 2022
bf77dac
Merge branch 'rsrv-protos' into rsrv-jvs-service
raserva Apr 26, 2022
40f7602
implementing jvs server
raserva Apr 26, 2022
00a9c65
Merge branch 'main' into rsrv-jvs-service
raserva Apr 27, 2022
2944c6d
minor updates
raserva Apr 27, 2022
eaa58e6
adding config tests
raserva Apr 27, 2022
498f271
PR feedback
raserva Apr 27, 2022
b093d8a
Update pkg/config/justification_config.go
raserva Apr 27, 2022
8b69954
feat: added signing code to jvs
raserva Apr 28, 2022
ed16923
Merge branch 'main' into rsrv-jvs-impl
raserva Apr 28, 2022
528060c
minor updates
raserva Apr 28, 2022
b83a150
Update to not use gcp jwt library
raserva May 2, 2022
9283e47
minor cosmetic fixes
raserva May 2, 2022
31976e8
minor fixes
raserva May 2, 2022
cae72df
PR feedback
raserva May 2, 2022
5e2f7fe
updated comment
raserva May 2, 2022
4b184f4
ran go mod tidy and go format
raserva May 2, 2022
27e84a9
PR feedback
raserva May 3, 2022
7d032b5
fix issue
raserva May 3, 2022
7c47940
feat: Implementing cert rotation logic that leverages an external db to
raserva May 5, 2022
51168f5
PR feedback
raserva May 5, 2022
d6b20c2
Merge branch 'rsrv-jvs-impl' into cert-db
raserva May 5, 2022
96c5d11
cont'd
raserva May 5, 2022
eab7bd1
PR feedback
raserva May 5, 2022
cec613f
Merge branch 'rsrv-jvs-impl' into cert-db
raserva May 5, 2022
71cf83d
updates
raserva May 5, 2022
676430d
reduce nesting
raserva May 5, 2022
3e21b74
Merge branch 'rsrv-jvs-impl' into cert-db
raserva May 5, 2022
81f7a3c
added working implementation & tests
raserva May 6, 2022
38972df
small fixes and comments
raserva May 6, 2022
e84ce3e
Merge branch 'main' into cert-db
raserva May 6, 2022
d7faf07
tidy
raserva May 6, 2022
11b3f86
moar tests
raserva May 6, 2022
2776b79
changed to use labels
raserva May 9, 2022
7f876cc
updated labels to match kms guidelines
raserva May 9, 2022
20e4364
remove unnecessary config members
raserva May 9, 2022
d570553
added comment
raserva May 9, 2022
b908e56
fix bug where current time is not updated.
raserva May 9, 2022
cf52e59
refactor to make state store an interface
raserva May 10, 2022
1994472
add required dependency to main
raserva May 10, 2022
564ad0f
added some comments
raserva May 10, 2022
3b0a66a
move method into state store
raserva May 10, 2022
af62787
starting public key api
raserva May 10, 2022
8be1fc7
PR feedback
raserva May 10, 2022
9141549
Merge branch 'cert-db' into pub-key
raserva May 10, 2022
e910332
more public key implementation
raserva May 11, 2022
27f8c36
minor updates
raserva May 11, 2022
6b7ab91
remove dependency, some PR feedback
raserva May 12, 2022
51da6d5
add cache
raserva May 13, 2022
fbbd395
only save primary in labels
raserva May 13, 2022
57da08c
change to en cache
raserva May 13, 2022
b9bcc5a
Merge branch 'cert-db' into pub-key
raserva May 13, 2022
c486edc
fix one merge miss
raserva May 13, 2022
e5515aa
clean up rotation handler
raserva May 17, 2022
21784c2
order similar functions together
raserva May 17, 2022
378c421
Merge branch 'main' into cert-db
raserva May 17, 2022
16450d5
go mod tidy
raserva May 17, 2022
754c418
Merge branch 'cert-db' into pub-key
raserva May 17, 2022
e05eaff
update to use zap
raserva May 17, 2022
4da82ac
PR feedback
raserva May 18, 2022
c978be4
added the cache tests
raserva May 18, 2022
9a30daa
fix comment
raserva May 18, 2022
6351f01
add return after http err
raserva May 18, 2022
834bcb4
Merge branch 'main' into pub-key
raserva May 18, 2022
12bab89
Merge branch 'main' into pub-key
raserva May 19, 2022
f6c4c97
fix some linter issues
raserva May 19, 2022
bc5cfa3
PR feedback
raserva May 19, 2022
f96b662
Update pkg/jvscrypto/key_hosting_test.go
raserva May 19, 2022
b3281ea
adding client libs
raserva May 19, 2022
b668698
added ECDSA key sanity check
raserva May 19, 2022
48acf78
update some other strings to remove escaped quotes
raserva May 19, 2022
6a0a88a
Merge branch 'pub-key' into cli-lib
raserva May 19, 2022
475d6a5
feat: adding client implementation
raserva May 19, 2022
75ab456
Merge branch 'main' into cli-lib
raserva May 23, 2022
84fc4f6
few small improvements
raserva May 23, 2022
10ad476
switched to JWX library
raserva May 24, 2022
5b006e8
formatting
raserva May 24, 2022
012ec50
cache timeout validation
raserva May 24, 2022
3df822d
remove print statements
raserva May 24, 2022
1d9c721
add doc
raserva May 24, 2022
cdbbeaa
Update client-lib/go/client/jvs_client.go
raserva May 25, 2022
07776b7
Update client-lib/go/client/jvs_client_test.go
raserva May 25, 2022
56afc51
Update client-lib/go/client/jvs_client_test.go
raserva May 25, 2022
4e7e06d
Update client-lib/go/client/jvs_client_test.go
raserva May 25, 2022
5cf3790
Update client-lib/go/client/jvs_client_test.go
raserva May 25, 2022
379f44d
Update client-lib/go/client/jvs_client_test.go
raserva May 25, 2022
3d07a08
Update client-lib/go/client/jvs_client_test.go
raserva May 25, 2022
cd39419
Update client-lib/go/client/jvs_client_test.go
raserva May 25, 2022
9ae8f89
Update client-lib/go/client/jvs_client_test.go
raserva May 25, 2022
f7cfcaf
refactored tests
raserva May 25, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apis/v0/jwt_specification.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@ import (
// JVSClaims are the claims that will be held within a JWT minted by the JVS server.
type JVSClaims struct {
*jwt.StandardClaims
Justifications []*Justification
Justifications []*Justification `json:"justs,omitempty"`
KeyID string `json:"kid,omitempty"`
raserva marked this conversation as resolved.
Show resolved Hide resolved
}
65 changes: 65 additions & 0 deletions client-lib/go/client/jvs_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package client provides a client library for JVS
package client

import (
"context"
"fmt"
"sync"

"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/lestrrat-go/jwx/v2/jws"
"github.com/lestrrat-go/jwx/v2/jwt"
)

// JVSClient allows for getting JWK keys from the JVS and validating JWTs with those keys
type JVSClient struct {
raserva marked this conversation as resolved.
Show resolved Hide resolved
raserva marked this conversation as resolved.
Show resolved Hide resolved
config *JVSConfig
keys jwk.Set
mu sync.RWMutex
}

// NewJVSClient returns a JVSClient with the cache initialized
func NewJVSClient(ctx context.Context, config *JVSConfig) (*JVSClient, error) {
c := jwk.NewCache(ctx)
c.Register(config.JVSEndpoint, jwk.WithMinRefreshInterval(config.CacheTimeout))

// check that cache is correctly set up and certs are available
if _, err := c.Refresh(ctx, config.JVSEndpoint); err != nil {
return nil, fmt.Errorf("failed to retrieve JVS public keys: %w", err)
}

cached := jwk.NewCachedSet(c, config.JVSEndpoint)

if err := config.Validate(); err != nil {
return nil, fmt.Errorf("failed to validate configuration: %w", err)
}

return &JVSClient{
config: config,
keys: cached,
}, nil
}

// ValidateJWT takes a jwt string, converts it to a JWT, and validates the signature.
func (j *JVSClient) ValidateJWT(ctx context.Context, jwtStr string) (*jwt.Token, error) {
verifiedToken, err := jwt.Parse([]byte(jwtStr), jwt.WithKeySet(j.keys, jws.WithInferAlgorithmFromKey(true)))
if err != nil {
return nil, fmt.Errorf("failed to verify jwt %s: %w", jwtStr, err)
}

return &verifiedToken, nil
}
191 changes: 191 additions & 0 deletions client-lib/go/client/jvs_client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package client

import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"

v0 "github.com/abcxyz/jvs/apis/v0"
"github.com/abcxyz/jvs/pkg/testutil"
"github.com/google/go-cmp/cmp"
"github.com/lestrrat-go/jwx/v2/jwa"
"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/lestrrat-go/jwx/v2/jws"
"github.com/lestrrat-go/jwx/v2/jwt"
)

func TestValidateJWT(t *testing.T) {
t.Parallel()
ctx := context.Background()

privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}

// create another key, to show the correct key is retrieved from cache and used for validation.
privateKey2, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}

key := "projects/[PROJECT]/locations/[LOCATION]/keyRings/[KEY_RING]/cryptoKeys/[CRYPTO_KEY]"
keyID := key + "/cryptoKeyVersions/[VERSION]-0"
keyID2 := key + "/cryptoKeyVersions/[VERSION]-1"

ecdsaKey, err := jwk.FromRaw(privateKey.PublicKey)
ecdsaKey.Set(jwk.KeyIDKey, keyID)
ecdsaKey2, err := jwk.FromRaw(privateKey2.PublicKey)
ecdsaKey2.Set(jwk.KeyIDKey, keyID2)
jwks := make(map[string][]jwk.Key)
jwks["keys"] = []jwk.Key{ecdsaKey, ecdsaKey2}

j, err := json.MarshalIndent(jwks, "", " ")
if err != nil {
t.Fatal("couldn't create jwks json")
}

path := "/.well-known/jwks"
mux := http.NewServeMux()
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, string(j))
})

svr := httptest.NewServer(mux)

t.Cleanup(func() {
svr.Close()
})

client, err := NewJVSClient(ctx, &JVSConfig{
Version: 1,
JVSEndpoint: svr.URL + path,
CacheTimeout: 5 * time.Minute,
})
if err != nil {
t.Fatalf("failed to create JVS client: %v", err)
}

tok := createToken(t, "test_id")
validJWT := signToken(t, tok, privateKey, keyID)

tok2 := createToken(t, "test_id_2")
validJWT2 := signToken(t, tok2, privateKey2, keyID2)

unsig, err := jwt.NewSerializer().Serialize(tok)
if err != nil {
t.Fatal("Couldn't get signing string.")
}
unsignedJWT := string(unsig)

split := strings.Split(validJWT2, ".")
sig := split[len(split)-1]

invalidSignatureJWT := unsignedJWT + sig // signature from a different JWT

tests := []struct {
name string
jwt string
wantErr string
wantToken jwt.Token
}{
{
name: "happy-path",
jwt: validJWT,
wantToken: tok,
}, {
name: "other-key",
jwt: validJWT2,
wantToken: tok2,
}, {
name: "unsigned",
jwt: unsignedJWT,
wantErr: "required field \"signatures\" not present",
}, {
name: "invalid",
jwt: invalidSignatureJWT,
wantErr: "failed to verify jwt",
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
res, err := client.ValidateJWT(ctx, tc.jwt)
testutil.ErrCmp(t, tc.wantErr, err)
if err != nil {
return
}
got, err := json.MarshalIndent(res, "", " ")
if err != nil {
t.Errorf("couldn't marshal returned token %v", err)
}
want, err := json.MarshalIndent(tc.wantToken, "", " ")
if err != nil {
t.Errorf("couldn't marshal expected token %v", err)
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("Token diff (-want, +got): %v", diff)
}
})
}
}

func createToken(tb testing.TB, id string) jwt.Token {
tb.Helper()

tok, err := jwt.NewBuilder().
Audience([]string{"test_aud"}).
Expiration(time.Now().Add(5 * time.Minute)).
JwtID(id).
IssuedAt(time.Now()).
Issuer(`test_iss`).
NotBefore(time.Now()).
Subject("test_sub").
Build()
if err != nil {
tb.Fatalf("failed to build token: %s\n", err)
}
tok.Set("justs", []*v0.Justification{
{
Category: "explanation",
Value: "this is a test explanation",
},
})
return tok
}

func signToken(tb testing.TB, tok jwt.Token, privateKey *ecdsa.PrivateKey, keyID string) string {
tb.Helper()
hdrs := jws.NewHeaders()
hdrs.Set(jws.KeyIDKey, keyID)

valid, err := jwt.Sign(tok, jwt.WithKey(jwa.ES256, privateKey, jws.WithProtectedHeaders(hdrs)))
if err != nil {
tb.Fatalf("failed to sign token: %s\n", err)
}
return string(valid)
}
94 changes: 94 additions & 0 deletions client-lib/go/client/jvs_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package client

import (
"context"
"fmt"
"time"

"github.com/hashicorp/go-multierror"
"github.com/sethvargo/go-envconfig"
"gopkg.in/yaml.v2"
)

const (
// Version default for config.
Version = 1
CacheTimeoutDefault = 5 * time.Minute
)

// JVSConfig is the jvs client configuration.
type JVSConfig struct {
// Version is the version of the config.
Version uint8 `yaml:"version,omitempty" env:"VERSION,overwrite"`

// JVS Endpoint. Expected to be fully qualified, including port. ex. http://127.0.0.1:8080
JVSEndpoint string `yaml:"endpoint,omitempty" env:"ENDPOINT,overwrite"`

// CacheTimeout is the duration that keys stay in cache before being revoked.
CacheTimeout time.Duration `yaml:"cache_timeout" env:"CACHE_TIMEOUT,overwrite"`
}

// Validate checks if the config is valid.
func (cfg *JVSConfig) Validate() error {
cfg.SetDefault()
var err *multierror.Error
if cfg.Version != Version {
err = multierror.Append(err, fmt.Errorf("unexpected Version %d want %d", cfg.Version, Version))
}
if cfg.JVSEndpoint == "" {
err = multierror.Append(err, fmt.Errorf("endpoint must be set"))
}
raserva marked this conversation as resolved.
Show resolved Hide resolved
if cfg.CacheTimeout <= 0 {
err = multierror.Append(err, fmt.Errorf("cache timeout invalid: %d", cfg.CacheTimeout))
}
return err.ErrorOrNil()
}

// SetDefault sets defaults for the config.
func (cfg *JVSConfig) SetDefault() {
raserva marked this conversation as resolved.
Show resolved Hide resolved
if cfg.Version == 0 {
cfg.Version = Version
}
if cfg.CacheTimeout == 0 {
// env config lib doesn't gracefully handle env overrides with defaults, have to set manually.
cfg.CacheTimeout = CacheTimeoutDefault
}
}

// LoadJVSConfig calls the necessary methods to load in config using the OsLookuper which finds env variables specified on the host.
func LoadJVSConfig(ctx context.Context, b []byte) (*JVSConfig, error) {
return loadJVSConfigFromLookuper(ctx, b, envconfig.OsLookuper())
}

// loadConfigFromLooker reads in a yaml file, applies ENV config overrides from the lookuper, and finally validates the config.
func loadJVSConfigFromLookuper(ctx context.Context, b []byte, lookuper envconfig.Lookuper) (*JVSConfig, error) {
cfg := &JVSConfig{}
if err := yaml.Unmarshal(b, cfg); err != nil {
return nil, err
}

// Process overrides from env vars.
l := envconfig.PrefixLookuper("JVS_", lookuper)
if err := envconfig.ProcessWith(ctx, cfg, l); err != nil {
return nil, err
}

if err := cfg.Validate(); err != nil {
return nil, fmt.Errorf("failed validating config: %w", err)
}
return cfg, nil
}
Loading