Skip to content

chore: move app proxying code to workspaceapps pkg #6998

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

Merged
merged 12 commits into from
Apr 5, 2023
Merged
Prev Previous commit
Next Next commit
chore: Rename AppSigningKey to AppSecurityKey
  • Loading branch information
Emyrk committed Apr 5, 2023
commit fe28e42220513eb5007fca5b861d2427a0c5845f
14 changes: 7 additions & 7 deletions cli/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -781,7 +781,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
// Read the app signing key from the DB. We store it hex encoded
// since the config table uses strings for the value and we
// don't want to deal with automatic encoding issues.
appSigningKeyStr, err := tx.GetAppSigningKey(ctx)
appSecurityKeyStr, err := tx.GetAppSecurityKey(ctx)
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
return xerrors.Errorf("get app signing key: %w", err)
}
Expand All @@ -794,26 +794,26 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
// generated automatically on failure. Any workspace app token
// smuggling operations in progress may fail, although with a
// helpful error.
if decoded, err := hex.DecodeString(appSigningKeyStr); err != nil || len(decoded) != len(workspaceapps.SigningKey{}) {
b := make([]byte, len(workspaceapps.SigningKey{}))
if decoded, err := hex.DecodeString(appSecurityKeyStr); err != nil || len(decoded) != len(workspaceapps.SecurityKey{}) {
b := make([]byte, len(workspaceapps.SecurityKey{}))
_, err := rand.Read(b)
if err != nil {
return xerrors.Errorf("generate fresh app signing key: %w", err)
}

appSigningKeyStr = hex.EncodeToString(b)
err = tx.UpsertAppSigningKey(ctx, appSigningKeyStr)
appSecurityKeyStr = hex.EncodeToString(b)
err = tx.UpsertAppSecurityKey(ctx, appSecurityKeyStr)
if err != nil {
return xerrors.Errorf("insert freshly generated app signing key to database: %w", err)
}
}

appSigningKey, err := workspaceapps.KeyFromString(appSigningKeyStr)
appSecurityKey, err := workspaceapps.KeyFromString(appSecurityKeyStr)
if err != nil {
return xerrors.Errorf("decode app signing key from database: %w", err)
}

options.AppSigningKey = appSigningKey
options.AppSecurityKey = appSecurityKey
return nil
}, nil)
if err != nil {
Expand Down
10 changes: 5 additions & 5 deletions coderd/coderd.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,9 @@ type Options struct {
SwaggerEndpoint bool
SetUserGroups func(ctx context.Context, tx database.Store, userID uuid.UUID, groupNames []string) error
TemplateScheduleStore schedule.TemplateScheduleStore
// AppSigningKey denotes the symmetric key to use for signing temporary app
// tokens.
AppSigningKey workspaceapps.SigningKey
// AppSecurityKey is the crypto key used to sign and encrypt tokens related to
// workspace applications. It consists of both a signing and encryption key.
AppSecurityKey workspaceapps.SecurityKey
HealthcheckFunc func(ctx context.Context) (*healthcheck.Report, error)
HealthcheckTimeout time.Duration
HealthcheckRefresh time.Duration
Expand Down Expand Up @@ -302,7 +302,7 @@ func New(options *Options) *API {
options.DeploymentValues,
oauthConfigs,
options.AgentInactiveDisconnectTimeout,
options.AppSigningKey,
options.AppSecurityKey,
),
metricsCache: metricsCache,
Auditor: atomic.Pointer[audit.Auditor]{},
Expand Down Expand Up @@ -340,7 +340,7 @@ func New(options *Options) *API {

SignedTokenProvider: api.WorkspaceAppsProvider,
WorkspaceConnCache: api.workspaceAgentCache,
AppSigningKey: options.AppSigningKey,
AppSecurityKey: options.AppSecurityKey,
}

apiKeyMiddleware := httpmw.ExtractAPIKeyMW(httpmw.ExtractAPIKeyConfig{
Expand Down
6 changes: 3 additions & 3 deletions coderd/coderdtest/coderdtest.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,9 @@ import (
"github.com/coder/coder/testutil"
)

// AppSigningKey is a 64-byte key used to sign JWTs and encrypt JWEs for
// AppSecurityKey is a 96-byte key used to sign JWTs and encrypt JWEs for
// workspace app tokens in tests.
var AppSigningKey = must(workspaceapps.KeyFromString("6465616e207761732068657265206465616e207761732068657265206465616e207761732068657265206465616e207761732068657265206465616e207761732068657265206465616e207761732068657265206465616e2077617320686572"))
var AppSecurityKey = must(workspaceapps.KeyFromString("6465616e207761732068657265206465616e207761732068657265206465616e207761732068657265206465616e207761732068657265206465616e207761732068657265206465616e207761732068657265206465616e2077617320686572"))

type Options struct {
// AccessURL denotes a custom access URL. By default we use the httptest
Expand Down Expand Up @@ -338,7 +338,7 @@ func NewOptions(t *testing.T, options *Options) (func(http.Handler), context.Can
DeploymentValues: options.DeploymentValues,
UpdateCheckOptions: options.UpdateCheckOptions,
SwaggerEndpoint: options.SwaggerEndpoint,
AppSigningKey: AppSigningKey,
AppSecurityKey: AppSecurityKey,
SSHConfig: options.ConfigSSH,
HealthcheckFunc: options.HealthcheckFunc,
HealthcheckTimeout: options.HealthcheckTimeout,
Expand Down
8 changes: 4 additions & 4 deletions coderd/database/dbauthz/querier.go
Original file line number Diff line number Diff line change
Expand Up @@ -379,14 +379,14 @@ func (q *querier) GetLogoURL(ctx context.Context) (string, error) {
return q.db.GetLogoURL(ctx)
}

func (q *querier) GetAppSigningKey(ctx context.Context) (string, error) {
func (q *querier) GetAppSecurityKey(ctx context.Context) (string, error) {
// No authz checks
return q.db.GetAppSigningKey(ctx)
return q.db.GetAppSecurityKey(ctx)
}

func (q *querier) UpsertAppSigningKey(ctx context.Context, data string) error {
func (q *querier) UpsertAppSecurityKey(ctx context.Context, data string) error {
// No authz checks as this is done during startup
return q.db.UpsertAppSigningKey(ctx, data)
return q.db.UpsertAppSecurityKey(ctx, data)
}

func (q *querier) GetServiceBanner(ctx context.Context) (string, error) {
Expand Down
10 changes: 5 additions & 5 deletions coderd/database/dbfake/databasefake.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ type data struct {
lastUpdateCheck []byte
serviceBanner []byte
logoURL string
appSigningKey string
appSecurityKey string
lastLicenseID int32
}

Expand Down Expand Up @@ -4444,18 +4444,18 @@ func (q *fakeQuerier) GetLogoURL(_ context.Context) (string, error) {
return q.logoURL, nil
}

func (q *fakeQuerier) GetAppSigningKey(_ context.Context) (string, error) {
func (q *fakeQuerier) GetAppSecurityKey(_ context.Context) (string, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()

return q.appSigningKey, nil
return q.appSecurityKey, nil
}

func (q *fakeQuerier) UpsertAppSigningKey(_ context.Context, data string) error {
func (q *fakeQuerier) UpsertAppSecurityKey(_ context.Context, data string) error {
q.mutex.Lock()
defer q.mutex.Unlock()

q.appSigningKey = data
q.appSecurityKey = data
return nil
}

Expand Down
4 changes: 2 additions & 2 deletions coderd/database/querier.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 6 additions & 6 deletions coderd/database/queries.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions coderd/database/queries/siteconfig.sql
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'logo_url';
-- name: GetLogoURL :one
SELECT value FROM site_configs WHERE key = 'logo_url';

-- name: GetAppSigningKey :one
-- name: GetAppSecurityKey :one
SELECT value FROM site_configs WHERE key = 'app_signing_key';

-- name: UpsertAppSigningKey :exec
-- name: UpsertAppSecurityKey :exec
INSERT INTO site_configs (key, value) VALUES ('app_signing_key', $1)
ON CONFLICT (key) DO UPDATE set value = $1 WHERE site_configs.key = 'app_signing_key';
2 changes: 1 addition & 1 deletion coderd/workspaceapps.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ func (api *API) workspaceApplicationAuth(rw http.ResponseWriter, r *http.Request
}

// Encrypt the API key.
encryptedAPIKey, err := api.AppSigningKey.EncryptAPIKey(workspaceapps.EncryptedAPIKeyPayload{
encryptedAPIKey, err := api.AppSecurityKey.EncryptAPIKey(workspaceapps.EncryptedAPIKeyPayload{
APIKey: cookie.Value,
})
if err != nil {
Expand Down
4 changes: 2 additions & 2 deletions coderd/workspaceapps/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@ type DBTokenProvider struct {
DeploymentValues *codersdk.DeploymentValues
OAuth2Configs *httpmw.OAuth2Configs
WorkspaceAgentInactiveTimeout time.Duration
SigningKey SigningKey
SigningKey SecurityKey
}

var _ SignedTokenProvider = &DBTokenProvider{}

func NewDBTokenProvider(log slog.Logger, accessURL *url.URL, authz rbac.Authorizer, db database.Store, cfg *codersdk.DeploymentValues, oauth2Cfgs *httpmw.OAuth2Configs, workspaceAgentInactiveTimeout time.Duration, signingKey SigningKey) SignedTokenProvider {
func NewDBTokenProvider(log slog.Logger, accessURL *url.URL, authz rbac.Authorizer, db database.Store, cfg *codersdk.DeploymentValues, oauth2Cfgs *httpmw.OAuth2Configs, workspaceAgentInactiveTimeout time.Duration, signingKey SecurityKey) SignedTokenProvider {
if workspaceAgentInactiveTimeout == 0 {
workspaceAgentInactiveTimeout = 1 * time.Minute
}
Expand Down
6 changes: 3 additions & 3 deletions coderd/workspaceapps/db_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ func Test_ResolveRequest(t *testing.T) {
require.Equal(t, codersdk.DevURLSignedAppTokenCookie, cookie.Name)
require.Equal(t, req.BasePath, cookie.Path)

parsedToken, err := api.AppSigningKey.VerifySignedToken(cookie.Value)
parsedToken, err := api.AppSecurityKey.VerifySignedToken(cookie.Value)
require.NoError(t, err)
// normalize expiry
require.WithinDuration(t, token.Expiry, parsedToken.Expiry, 2*time.Second)
Expand Down Expand Up @@ -482,7 +482,7 @@ func Test_ResolveRequest(t *testing.T) {
AgentID: agentID,
AppURL: appURL,
}
badTokenStr, err := api.AppSigningKey.SignToken(badToken)
badTokenStr, err := api.AppSecurityKey.SignToken(badToken)
require.NoError(t, err)

req := workspaceapps.Request{
Expand Down Expand Up @@ -518,7 +518,7 @@ func Test_ResolveRequest(t *testing.T) {
require.Len(t, cookies, 1)
require.Equal(t, cookies[0].Name, codersdk.DevURLSignedAppTokenCookie)
require.NotEqual(t, cookies[0].Value, badTokenStr)
parsedToken, err := api.AppSigningKey.VerifySignedToken(cookies[0].Value)
parsedToken, err := api.AppSecurityKey.VerifySignedToken(cookies[0].Value)
require.NoError(t, err)
require.Equal(t, appNameOwner, parsedToken.AppSlugOrPort)
})
Expand Down
6 changes: 3 additions & 3 deletions coderd/workspaceapps/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@ type Server struct {
RealIPConfig *httpmw.RealIPConfig

SignedTokenProvider SignedTokenProvider
WorkspaceConnCache *wsconncache.Cache
AppSigningKey SigningKey
WorkspaceConnCache *wsconncache.Cache
AppSecurityKey SecurityKey

websocketWaitMutex sync.Mutex
websocketWaitGroup sync.WaitGroup
Expand Down Expand Up @@ -245,7 +245,7 @@ func (s *Server) SubdomainAppMW(middlewares ...func(http.Handler) http.Handler)
// cookie and strip that query parameter.
if encryptedAPIKey := r.URL.Query().Get(SubdomainProxyAPIKeyParam); encryptedAPIKey != "" {
// Exchange the encoded API key for a real one.
token, err := s.AppSigningKey.DecryptAPIKey(encryptedAPIKey)
token, err := s.AppSecurityKey.DecryptAPIKey(encryptedAPIKey)
if err != nil {
s.Logger.Debug(ctx, "could not decrypt API key", slog.Error(err))
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
Expand Down
34 changes: 22 additions & 12 deletions coderd/workspaceapps/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,24 @@ func (t SignedToken) MatchesRequest(req Request) bool {
t.AppSlugOrPort == req.AppSlugOrPort
}

// AppSigningKey is used for signing and encrypting app tokens and API keys.
// SecurityKey is used for signing and encrypting app tokens and API keys.
//
// The first 64 bytes of the key are used for signing tokens with HMAC-SHA256,
// and the last 32 bytes are used for encrypting API keys with AES-256-GCM.
type SigningKey [96]byte
// We use a single key for both operations to avoid having to store and manage
// two keys.
type SecurityKey [96]byte

func KeyFromString(str string) (SigningKey, error) {
var key SigningKey
func (k SecurityKey) signingKey() []byte {
return k[:64]
}

func (k SecurityKey) encryptionKey() []byte {
return k[64:]
}

func KeyFromString(str string) (SecurityKey, error) {
var key SecurityKey
decoded, err := hex.DecodeString(str)
if err != nil {
return key, xerrors.Errorf("decode key: %w", err)
Expand All @@ -67,7 +77,7 @@ func KeyFromString(str string) (SigningKey, error) {
// SignToken generates a signed workspace app token with the given payload. If
// the payload doesn't have an expiry, it will be set to the current time plus
// the default expiry.
func (k SigningKey) SignToken(payload SignedToken) (string, error) {
func (k SecurityKey) SignToken(payload SignedToken) (string, error) {
if payload.Expiry.IsZero() {
payload.Expiry = time.Now().Add(DefaultTokenExpiry)
}
Expand All @@ -78,7 +88,7 @@ func (k SigningKey) SignToken(payload SignedToken) (string, error) {

signer, err := jose.NewSigner(jose.SigningKey{
Algorithm: tokenSigningAlgorithm,
Key: k[:64],
Key: k.signingKey(),
}, nil)
if err != nil {
return "", xerrors.Errorf("create signer: %w", err)
Expand All @@ -100,7 +110,7 @@ func (k SigningKey) SignToken(payload SignedToken) (string, error) {
// VerifySignedToken parses a signed workspace app token with the given key and
// returns the payload. If the token is invalid or expired, an error is
// returned.
func (k SigningKey) VerifySignedToken(str string) (SignedToken, error) {
func (k SecurityKey) VerifySignedToken(str string) (SignedToken, error) {
object, err := jose.ParseSigned(str)
if err != nil {
return SignedToken{}, xerrors.Errorf("parse JWS: %w", err)
Expand All @@ -112,7 +122,7 @@ func (k SigningKey) VerifySignedToken(str string) (SignedToken, error) {
return SignedToken{}, xerrors.Errorf("expected token signing algorithm to be %q, got %q", tokenSigningAlgorithm, object.Signatures[0].Header.Algorithm)
}

output, err := object.Verify(k[:64])
output, err := object.Verify(k.signingKey())
if err != nil {
return SignedToken{}, xerrors.Errorf("verify JWS: %w", err)
}
Expand All @@ -135,7 +145,7 @@ type EncryptedAPIKeyPayload struct {
}

// EncryptAPIKey encrypts an API key for subdomain token smuggling.
func (k SigningKey) EncryptAPIKey(payload EncryptedAPIKeyPayload) (string, error) {
func (k SecurityKey) EncryptAPIKey(payload EncryptedAPIKeyPayload) (string, error) {
if payload.APIKey == "" {
return "", xerrors.New("API key is empty")
}
Expand All @@ -155,7 +165,7 @@ func (k SigningKey) EncryptAPIKey(payload EncryptedAPIKeyPayload) (string, error
jose.A256GCM,
jose.Recipient{
Algorithm: apiKeyEncryptionAlgorithm,
Key: k[64:],
Key: k.encryptionKey(),
},
&jose.EncrypterOptions{
Compression: jose.DEFLATE,
Expand All @@ -174,7 +184,7 @@ func (k SigningKey) EncryptAPIKey(payload EncryptedAPIKeyPayload) (string, error
}

// DecryptAPIKey undoes EncryptAPIKey and is used in the subdomain app handler.
func (k SigningKey) DecryptAPIKey(encryptedAPIKey string) (string, error) {
func (k SecurityKey) DecryptAPIKey(encryptedAPIKey string) (string, error) {
encrypted, err := base64.RawURLEncoding.DecodeString(encryptedAPIKey)
if err != nil {
return "", xerrors.Errorf("base64 decode encrypted API key: %w", err)
Expand All @@ -189,7 +199,7 @@ func (k SigningKey) DecryptAPIKey(encryptedAPIKey string) (string, error) {
}

// Decrypt using the hashed secret.
decrypted, err := object.Decrypt(k[64:])
decrypted, err := object.Decrypt(k.encryptionKey())
if err != nil {
return "", xerrors.Errorf("decrypt API key: %w", err)
}
Expand Down
Loading