-
Notifications
You must be signed in to change notification settings - Fork 90
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
gemalto: add support for Gemalto KeySecure
This commit adds support for Gemalto KeySecure as KMS. KeySecure is a KMS implementation developed by Gemalto (now Thales) and provides a KMIP as well as a REST API. This commit adds a REST client that uses the KeySecure secrets API (`/v1/vault/secrets`) to fetch and store cryptographic keys. *** KeySecure has multi-user support (username / password). However, KeySecure also has the concept of refresh and authentication tokens for programmatic access. A refresh token is an opaque string, e.g. `CEvk5cdHLG7si05LReIeDbXE3PKD082YdUFAnxX75md3jzV0BnyHyAmPPJiA0Kgn`, that can be generated by a user to give an application programmatic access. With the refresh token an application can request a short-lived (5 min by default) authentication token. The authentication token can then be used to perform operations - like creating a new secret. Therefore, it has to be sent as part of the `Authorization` header. With the refresh token the application can renew the authentication token before it expires. KES supports only refresh tokens, not username/password, since a refresh token is the recommended approach to grant programmatic access. A refresh token can be easily revoked. *** Apart from authentication, KeySecure provides a secret store that is quite similar to e.g. Hashicorp Vault's K/V engine. Therefore, KES uses the secrets API to store cryptographic keys. *** This commit also adds a wrapper for an HTTP client that implements an automatic retry mechanism. This is needed since KeySecure does not provide a Go SDK and KES should be robust and tolerate temp. network errors.
- Loading branch information
1 parent
b0826ba
commit 1b64284
Showing
6 changed files
with
842 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,173 @@ | ||
// Copyright 2020 - MinIO, Inc. All rights reserved. | ||
// Use of this source code is governed by the AGPLv3 | ||
// license that can be found in the LICENSE file. | ||
|
||
package gemalto | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"io" | ||
"log" | ||
"net/http" | ||
"sync" | ||
"time" | ||
|
||
xhttp "github.com/minio/kes/internal/http" | ||
) | ||
|
||
// authToken is a KeySecure authentication token. | ||
// It can be used to authenticate API requests. | ||
type authToken struct { | ||
Type string | ||
Value string | ||
Expiry time.Duration | ||
} | ||
|
||
// String returns the string representation of | ||
// the authentication token. | ||
func (t *authToken) String() string { return fmt.Sprintf("%s %s", t.Type, t.Value) } | ||
|
||
// client is a KeySecure REST API client | ||
// responsible for fetching and renewing | ||
// authentication tokens. | ||
type client struct { | ||
xhttp.Retry | ||
ErrorLog *log.Logger | ||
|
||
lock sync.Mutex | ||
token authToken | ||
} | ||
|
||
// Authenticate tries to obtain a new authentication token | ||
// from the given KeySecure endpoint via the given refresh | ||
// token. | ||
// | ||
// Athenticate should be called to obtain the first authentication | ||
// token. This token can then be renewed via RenewAuthToken. | ||
func (c *client) Authenticate(endpoint string, login Credentials) error { | ||
type Request struct { | ||
Type string `json:"grant_type"` | ||
Token string `json:"refresh_token"` | ||
Domain string `json:"domain"` | ||
} | ||
type Response struct { | ||
Type string `json:"token_type"` | ||
Token string `json:"jwt"` | ||
Expiry uint64 `json:"duration"` // KeySecure returns expiry in seconds | ||
} | ||
|
||
body, err := json.Marshal(Request{ | ||
Type: "refresh_token", | ||
Token: login.Token, | ||
Domain: login.Domain, | ||
}) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
url := fmt.Sprintf("%s/api/v1/auth/tokens", endpoint) | ||
req, err := http.NewRequest(http.MethodPost, url, xhttp.RetryReader(bytes.NewReader(body))) | ||
if err != nil { | ||
return err | ||
} | ||
req.Header.Set("Content-Type", "application/json") | ||
|
||
resp, err := c.Do(req) | ||
if err != nil { | ||
return err | ||
} | ||
defer resp.Body.Close() | ||
|
||
if resp.StatusCode != http.StatusOK { | ||
response, err := parseServerError(resp) | ||
if err != nil { | ||
return fmt.Errorf("%s: %v", resp.Status, err) | ||
} | ||
return fmt.Errorf("%s: %s (%d)", resp.Status, response.Message, response.Code) | ||
} | ||
|
||
const MaxSize = 1 << 20 // An auth. token response should not exceed 1 MiB | ||
var response Response | ||
if err = json.NewDecoder(io.LimitReader(resp.Body, MaxSize)).Decode(&response); err != nil { | ||
return err | ||
} | ||
if response.Token == "" { | ||
return errors.New("server response does not contain an auth token") | ||
} | ||
if response.Type != "Bearer" { | ||
return fmt.Errorf("unexpected auth token type '%s'", response.Type) | ||
} | ||
if response.Expiry <= 0 { | ||
return fmt.Errorf("invalid auth token expiry '%d'", response.Expiry) | ||
} | ||
|
||
c.lock.Lock() | ||
c.token = authToken{ | ||
Type: response.Type, | ||
Value: response.Token, | ||
Expiry: time.Duration(response.Expiry) * time.Second, | ||
} | ||
c.lock.Unlock() | ||
return nil | ||
} | ||
|
||
// RenewAuthToken tries to renew the client's authentication | ||
// token before it expires. It blocks until <-ctx.Done() completes. | ||
// | ||
// Before calling RenewAuthToken the client should already have a | ||
// authentication token. Therefore, RenewAuthToken should be called | ||
// only after a Authenticate. | ||
// | ||
// RenewAuthToken tries get a new authentication token from the given | ||
// KeySecure endpoint by presenting the given refresh token. | ||
// It continuesly tries to renew the authentication before it expires. | ||
// | ||
// If RenewAuthToken fails to request or renew the client's authentication | ||
// token then it keeps retrying and waits for the given login.Retry delay | ||
// between each retry attempt. | ||
// | ||
// If login.Retry is 0 then RenewAuthToken uses a reasonable default retry delay. | ||
func (c *client) RenewAuthToken(ctx context.Context, endpoint string, login Credentials) { | ||
if login.Retry == 0 { | ||
login.Retry = 5 * time.Second | ||
} | ||
var ( | ||
timer *time.Timer | ||
err error | ||
) | ||
for { | ||
if err != nil { | ||
logf(c.ErrorLog, "gemalto: failed to renew auth token: %v", err) | ||
timer = time.NewTimer(login.Retry) | ||
} else { | ||
c.lock.Lock() | ||
timer = time.NewTimer(c.token.Expiry / 2) | ||
c.lock.Unlock() | ||
} | ||
|
||
select { | ||
case <-ctx.Done(): | ||
timer.Stop() | ||
return | ||
case <-timer.C: | ||
err = c.Authenticate(endpoint, login) | ||
timer.Stop() | ||
} | ||
} | ||
} | ||
|
||
// AuthToken returns an authentication token that can be | ||
// used to authenticate API requests to a KeySecure instance. | ||
// | ||
// Typically, it is a JWT token and should be used as HTTP | ||
// Authorization header value. | ||
func (c *client) AuthToken() string { | ||
c.lock.Lock() | ||
defer c.lock.Unlock() | ||
|
||
return c.token.String() | ||
} |
Oops, something went wrong.