mirror of
https://github.com/matter-labs/vault-auth-tee.git
synced 2025-07-23 00:14:47 +02:00
feat: initial commit
Signed-off-by: Harald Hoyer <harald@matterlabs.dev>
This commit is contained in:
commit
d60b17e20f
29 changed files with 6542 additions and 0 deletions
61
tee/backend.go
Normal file
61
tee/backend.go
Normal file
|
@ -0,0 +1,61 @@
|
|||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright (c) Matter Labs
|
||||
|
||||
package tee
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/matter-labs/vault-auth-tee/version"
|
||||
|
||||
"github.com/hashicorp/vault/sdk/framework"
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
)
|
||||
|
||||
const operationPrefixTee = "tee"
|
||||
|
||||
func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) {
|
||||
b := Backend()
|
||||
if err := b.Setup(ctx, conf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func Backend() *backend {
|
||||
var b backend
|
||||
b.Backend = &framework.Backend{
|
||||
Help: backendHelp,
|
||||
PathsSpecial: &logical.Paths{
|
||||
Unauthenticated: []string{
|
||||
"login",
|
||||
},
|
||||
},
|
||||
Paths: []*framework.Path{
|
||||
pathInfo(&b),
|
||||
pathLogin(&b),
|
||||
pathListTees(&b),
|
||||
pathTees(&b),
|
||||
},
|
||||
AuthRenew: b.loginPathWrapper(b.pathLoginRenew),
|
||||
BackendType: logical.TypeCredential,
|
||||
RunningVersion: "v" + version.Version,
|
||||
}
|
||||
|
||||
return &b
|
||||
}
|
||||
|
||||
type backend struct {
|
||||
*framework.Backend
|
||||
}
|
||||
|
||||
const backendHelp = `
|
||||
The "tee" credential provider allows authentication using
|
||||
remote attestation verification together with TLS client certificates.
|
||||
A client connects to Vault and uses the "login" endpoint to generate a client token.
|
||||
|
||||
Trusted execution environments are configured using the "tees/" endpoint
|
||||
by a user with root access. Authentication is then done
|
||||
by supplying the attestation report, the attestation collateral
|
||||
and the client certificate for "login".
|
||||
`
|
284
tee/backend_test.go
Normal file
284
tee/backend_test.go
Normal file
File diff suppressed because one or more lines are too long
40
tee/path_info.go
Normal file
40
tee/path_info.go
Normal file
|
@ -0,0 +1,40 @@
|
|||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright (c) Matter Labs
|
||||
|
||||
package tee
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/hashicorp/vault/sdk/framework"
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
|
||||
"github.com/matter-labs/vault-auth-tee/version"
|
||||
)
|
||||
|
||||
func pathInfo(b *backend) *framework.Path {
|
||||
return &framework.Path{
|
||||
Pattern: "info",
|
||||
HelpSynopsis: "Display information about the plugin",
|
||||
HelpDescription: `
|
||||
|
||||
Displays information about the plugin, such as the plugin version and where to
|
||||
get help.
|
||||
|
||||
`,
|
||||
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||
logical.ReadOperation: b.pathInfoRead,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// pathInfoRead corresponds to READ auth/tee/info.
|
||||
func (b *backend) pathInfoRead(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) {
|
||||
return &logical.Response{
|
||||
Data: map[string]interface{}{
|
||||
"name": version.Name,
|
||||
"version": version.Version,
|
||||
},
|
||||
}, nil
|
||||
}
|
417
tee/path_login.go
Normal file
417
tee/path_login.go
Normal file
|
@ -0,0 +1,417 @@
|
|||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright (c) Matter Labs
|
||||
|
||||
package tee
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/vault/sdk/framework"
|
||||
"github.com/hashicorp/vault/sdk/helper/cidrutil"
|
||||
"github.com/hashicorp/vault/sdk/helper/policyutil"
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
|
||||
"github.com/matter-labs/vault-auth-tee/ratee"
|
||||
)
|
||||
|
||||
var timeNowFunc = time.Now
|
||||
|
||||
func pathLogin(b *backend) *framework.Path {
|
||||
return &framework.Path{
|
||||
Pattern: "login",
|
||||
DisplayAttrs: &framework.DisplayAttributes{
|
||||
OperationPrefix: operationPrefixTee,
|
||||
OperationVerb: "login",
|
||||
},
|
||||
Fields: map[string]*framework.FieldSchema{
|
||||
"name": {
|
||||
Type: framework.TypeString,
|
||||
Description: "The name of the tee role to authenticate against.",
|
||||
},
|
||||
"type": {
|
||||
Type: framework.TypeString,
|
||||
Description: "The type of the TEE.",
|
||||
},
|
||||
"quote": {
|
||||
Type: framework.TypeString,
|
||||
Description: "The quote Base64 encoded.",
|
||||
},
|
||||
"collateral": {
|
||||
Type: framework.TypeString,
|
||||
Description: "The collateral Json encoded.",
|
||||
},
|
||||
"challenge": {
|
||||
Type: framework.TypeString,
|
||||
Description: "Hex encoded bytes to include in the attestation report of the vault quote",
|
||||
},
|
||||
},
|
||||
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||
logical.UpdateOperation: b.loginPathWrapper(b.pathLogin),
|
||||
logical.AliasLookaheadOperation: b.pathLoginAliasLookahead,
|
||||
logical.ResolveRoleOperation: b.loginPathWrapper(b.pathLoginResolveRole),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (b *backend) loginPathWrapper(wrappedOp func(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error)) framework.OperationFunc {
|
||||
return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||
return wrappedOp(ctx, req, data)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *backend) pathLoginResolveRole(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||
quoteBase64 := data.Get("quote").(string)
|
||||
if quoteBase64 == "" {
|
||||
return nil, fmt.Errorf("missing quote")
|
||||
}
|
||||
|
||||
quoteBytes, err := base64.StdEncoding.DecodeString(quoteBase64)
|
||||
if err != nil {
|
||||
return logical.ErrorResponse("quote decode error"), nil
|
||||
}
|
||||
|
||||
var quote = ratee.Quote{}
|
||||
var byteReader = bytes.NewReader(quoteBytes)
|
||||
err = binary.Read(byteReader, binary.BigEndian, "e)
|
||||
if err != nil {
|
||||
return logical.ErrorResponse("quote decode error"), nil
|
||||
}
|
||||
|
||||
names, err := req.Storage.List(ctx, "tee/")
|
||||
if err != nil {
|
||||
return logical.ErrorResponse("no TEE was matched by this request"), nil
|
||||
}
|
||||
|
||||
rb := quote.ReportBody
|
||||
|
||||
mrSignerHex := hex.EncodeToString(rb.MrSigner[:])
|
||||
mrEnclaveHex := hex.EncodeToString(rb.MrEnclave[:])
|
||||
|
||||
for _, name := range names {
|
||||
entry, err := b.Tee(ctx, req.Storage, strings.TrimPrefix(name, "tee/"))
|
||||
if err != nil {
|
||||
b.Logger().Error("failed to load trusted tee", "name", name, "error", err)
|
||||
continue
|
||||
}
|
||||
if entry == nil {
|
||||
// This could happen when the name was provided and the tee doesn't exist,
|
||||
// or just if between the LIST and the GET the tee was deleted.
|
||||
continue
|
||||
}
|
||||
|
||||
if entry.SgxMrsigner != "" && entry.SgxMrsigner != mrSignerHex {
|
||||
continue
|
||||
}
|
||||
if entry.SgxMrenclave != "" && entry.SgxMrenclave != mrEnclaveHex {
|
||||
continue
|
||||
}
|
||||
if entry.SgxIsvProdid != int(binary.LittleEndian.Uint16(rb.IsvProdid[:])) {
|
||||
continue
|
||||
}
|
||||
return logical.ResolveRoleResponse(name)
|
||||
}
|
||||
return logical.ErrorResponse("no TEE was matched by this request"), nil
|
||||
}
|
||||
|
||||
func (b *backend) pathLoginAliasLookahead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
name := d.Get("name").(string)
|
||||
if name == "" {
|
||||
return nil, fmt.Errorf("missing name")
|
||||
}
|
||||
|
||||
return &logical.Response{
|
||||
Auth: &logical.Auth{
|
||||
Alias: &logical.Alias{
|
||||
Name: name,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
func hashPublicKey256(pub interface{}) ([]byte, error) {
|
||||
pubBytes, err := x509.MarshalPKIXPublicKey(pub)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := sha256.Sum256(pubBytes)
|
||||
return result[:], nil
|
||||
}
|
||||
|
||||
func Contains[T comparable](s []T, e T) bool {
|
||||
for _, v := range s {
|
||||
if v == e {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (b *backend) pathLogin(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||
name := data.Get("name").(string)
|
||||
if name == "" {
|
||||
return nil, fmt.Errorf("missing name")
|
||||
}
|
||||
|
||||
// Allow constraining the login request to a single TeeEntry
|
||||
entry, err := b.Tee(ctx, req.Storage, strings.TrimPrefix(name, "tee/"))
|
||||
|
||||
if err != nil {
|
||||
return logical.ErrorResponse(fmt.Sprintf("no TEE matching for this login name; additionally got errors during verification: %v", err)), nil
|
||||
}
|
||||
|
||||
if entry == nil {
|
||||
return logical.ErrorResponse(fmt.Sprintf("no TEE matching for this login name")), nil
|
||||
}
|
||||
|
||||
// Get the connection state
|
||||
if req.Connection == nil || req.Connection.ConnState == nil {
|
||||
return logical.ErrorResponse("tls connection required"), nil
|
||||
}
|
||||
connState := req.Connection.ConnState
|
||||
|
||||
if connState.PeerCertificates == nil || len(connState.PeerCertificates) == 0 {
|
||||
return logical.ErrorResponse("client certificate must be supplied"), nil
|
||||
}
|
||||
|
||||
clientCert := connState.PeerCertificates[0]
|
||||
|
||||
// verify self-signed certificate
|
||||
roots := x509.NewCertPool()
|
||||
roots.AddCert(clientCert)
|
||||
_, err = clientCert.Verify(x509.VerifyOptions{Roots: roots})
|
||||
if err != nil {
|
||||
return logical.ErrorResponse("client certificate must be self-signed"), nil
|
||||
}
|
||||
|
||||
if len(entry.TokenBoundCIDRs) > 0 {
|
||||
if req.Connection == nil {
|
||||
b.Logger().Warn("token bound CIDRs found but no connection information available for validation")
|
||||
return nil, logical.ErrPermissionDenied
|
||||
}
|
||||
if !cidrutil.RemoteAddrIsOk(req.Connection.RemoteAddr, entry.TokenBoundCIDRs) {
|
||||
return nil, logical.ErrPermissionDenied
|
||||
}
|
||||
}
|
||||
|
||||
if clientCert.PublicKey == nil {
|
||||
return logical.ErrorResponse("no public key found in client certificate"), nil
|
||||
}
|
||||
|
||||
hashClientPk, err := hashPublicKey256(clientCert.PublicKey)
|
||||
|
||||
if err != nil {
|
||||
return logical.ErrorResponse("error hashing public key"), nil
|
||||
}
|
||||
|
||||
teeType := data.Get("type").(string)
|
||||
|
||||
if _, ok := entry.Types[teeType]; !ok {
|
||||
return logical.ErrorResponse(fmt.Sprintf("type `%s` not supported for `%s`", teeType, name)), nil
|
||||
}
|
||||
|
||||
quote := data.Get("quote").(string)
|
||||
quoteBytes, err := base64.StdEncoding.DecodeString(quote)
|
||||
if err != nil {
|
||||
return logical.ErrorResponse("quote decode error"), nil
|
||||
}
|
||||
|
||||
// Do a quick check of the quote before doing the expensive verification
|
||||
var quoteStart = ratee.Quote{}
|
||||
var byteReader = bytes.NewReader(quoteBytes)
|
||||
err = binary.Read(byteReader, binary.BigEndian, "eStart)
|
||||
if err != nil {
|
||||
return logical.ErrorResponse("quote decode error"), nil
|
||||
}
|
||||
reportBody := quoteStart.ReportBody
|
||||
|
||||
if !bytes.Equal(reportBody.ReportData[:32], hashClientPk) {
|
||||
return logical.ErrorResponse("client certificate's hashed public key not in report data of attestation quote report"), nil
|
||||
}
|
||||
|
||||
mrSignerHex := hex.EncodeToString(reportBody.MrSigner[:])
|
||||
mrEnclaveHex := hex.EncodeToString(reportBody.MrEnclave[:])
|
||||
|
||||
if entry.SgxMrsigner != "" && entry.SgxMrsigner != mrSignerHex {
|
||||
return logical.ErrorResponse("`sgx_mrsigner` does not match"), nil
|
||||
}
|
||||
if entry.SgxMrenclave != "" && entry.SgxMrenclave != mrEnclaveHex {
|
||||
return logical.ErrorResponse("`sgx_mrenclave` does not match"), nil
|
||||
}
|
||||
if entry.SgxIsvProdid != int(binary.LittleEndian.Uint16(reportBody.IsvProdid[:])) {
|
||||
return logical.ErrorResponse("`sgx_isv_prodid` does not match"), nil
|
||||
}
|
||||
if entry.SgxMinIsvSvn > int(binary.LittleEndian.Uint16(reportBody.IsvSvn[:])) {
|
||||
return logical.ErrorResponse("`sgx_isv_svn` too low"), nil
|
||||
}
|
||||
|
||||
// Decode the collateral
|
||||
jsonCollateralBlob := data.Get("collateral").(string)
|
||||
var collateral ratee.TeeQvCollateral
|
||||
err = json.Unmarshal([]byte(jsonCollateralBlob), &collateral)
|
||||
if err != nil {
|
||||
return logical.ErrorResponse("collateral unmarshal error"), nil
|
||||
}
|
||||
|
||||
// Do the actual remote attestation verification
|
||||
result, err := ratee.SgxVerifyRemoteReportCollateral(quoteBytes, collateral, timeNowFunc().Unix())
|
||||
if err != nil {
|
||||
return logical.ErrorResponse("sgx verify error"), nil
|
||||
}
|
||||
|
||||
if result.CollateralExpired {
|
||||
return logical.ErrorResponse("collateral expired"), nil
|
||||
}
|
||||
|
||||
if result.VerificationResult != ratee.SgxQlQvResultOk {
|
||||
if entry.SgxAllowedTcbLevels[result.VerificationResult] != true {
|
||||
return logical.ErrorResponse("invalid TCB state %v", result.VerificationResult), nil
|
||||
}
|
||||
}
|
||||
|
||||
skid := base64.StdEncoding.EncodeToString(clientCert.SubjectKeyId)
|
||||
akid := base64.StdEncoding.EncodeToString(clientCert.AuthorityKeyId)
|
||||
pkid := base64.StdEncoding.EncodeToString(hashClientPk)
|
||||
|
||||
expirationDate := time.Unix(result.EarliestExpirationDate, 0)
|
||||
metadata := map[string]string{
|
||||
"tee_name": entry.Name,
|
||||
"collateral_expiration_date": expirationDate.Format(time.RFC3339),
|
||||
}
|
||||
|
||||
auth := &logical.Auth{
|
||||
InternalData: map[string]interface{}{
|
||||
"subject_key_id": skid,
|
||||
"authority_key_id": akid,
|
||||
"hash_public_key": pkid,
|
||||
},
|
||||
Alias: &logical.Alias{
|
||||
Name: entry.Name,
|
||||
},
|
||||
DisplayName: entry.DisplayName,
|
||||
Metadata: metadata,
|
||||
}
|
||||
|
||||
entry.PopulateTokenAuth(auth)
|
||||
|
||||
now := timeNowFunc()
|
||||
|
||||
if !now.Add(auth.TTL).After(expirationDate) {
|
||||
auth.TTL = expirationDate.Sub(now)
|
||||
}
|
||||
|
||||
if !now.Add(auth.MaxTTL).After(expirationDate) {
|
||||
auth.MaxTTL = expirationDate.Sub(now)
|
||||
}
|
||||
|
||||
respData := make(map[string]interface{})
|
||||
|
||||
challenge := data.Get("challenge").(string)
|
||||
if challenge != "" {
|
||||
challengeBytes, err := hex.DecodeString(challenge)
|
||||
if err != nil {
|
||||
return logical.ErrorResponse("challenge decode error"), nil
|
||||
}
|
||||
|
||||
ourQuote, err := ratee.SgxGetQuote(challengeBytes)
|
||||
if err != nil {
|
||||
return logical.ErrorResponse("vault quote error"), nil
|
||||
}
|
||||
|
||||
quoteBase64 := base64.StdEncoding.EncodeToString(ourQuote)
|
||||
|
||||
respData["quote"] = quoteBase64
|
||||
|
||||
collateral, err := ratee.SgxGetCollateral(ourQuote)
|
||||
if err != nil {
|
||||
return logical.ErrorResponse("vault collateral error"), nil
|
||||
}
|
||||
|
||||
collateralJson, err := json.Marshal(collateral)
|
||||
if err != nil {
|
||||
return logical.ErrorResponse("vault collateral json error"), nil
|
||||
}
|
||||
|
||||
respData["collateral"] = string(collateralJson)
|
||||
}
|
||||
|
||||
return &logical.Response{
|
||||
Auth: auth,
|
||||
Data: respData,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *backend) pathLoginRenew(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
clientCerts := req.Connection.ConnState.PeerCertificates
|
||||
if len(clientCerts) == 0 {
|
||||
return logical.ErrorResponse("no client certificate found"), nil
|
||||
}
|
||||
hashClientPk, err := hashPublicKey256(clientCerts[0].PublicKey)
|
||||
if err != nil {
|
||||
return logical.ErrorResponse("error hashing public key"), nil
|
||||
}
|
||||
skid := base64.StdEncoding.EncodeToString(clientCerts[0].SubjectKeyId)
|
||||
akid := base64.StdEncoding.EncodeToString(clientCerts[0].AuthorityKeyId)
|
||||
pkid := base64.StdEncoding.EncodeToString(hashClientPk)
|
||||
|
||||
// Certificate should not only match a registered tee policy.
|
||||
// Also, the identity of the certificate presented should match the identity of the certificate used during login
|
||||
if req.Auth.InternalData["subject_key_id"] != skid && req.Auth.InternalData["authority_key_id"] != akid && req.Auth.InternalData["hash_public_key"] != pkid {
|
||||
return nil, fmt.Errorf("client identity during renewal not matching client identity used during login")
|
||||
}
|
||||
|
||||
// Get the tee and use its TTL
|
||||
tee, err := b.Tee(ctx, req.Storage, req.Auth.Metadata["tee_name"])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tee == nil {
|
||||
// User no longer exists, do not renew
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if !policyutil.EquivalentPolicies(tee.TokenPolicies, req.Auth.TokenPolicies) {
|
||||
return nil, fmt.Errorf("policies have changed, not renewing")
|
||||
}
|
||||
|
||||
expirationDate, err := time.Parse(time.RFC3339, req.Auth.Metadata["collateral_expiration_date"])
|
||||
if err != nil {
|
||||
return logical.ErrorResponse("error parsing `collateral_expiration_date` metadata"), nil
|
||||
}
|
||||
|
||||
now := timeNowFunc()
|
||||
|
||||
if expirationDate.Before(now) {
|
||||
return logical.ErrorResponse("Collateral expired"), nil
|
||||
}
|
||||
|
||||
resp := &logical.Response{Auth: req.Auth}
|
||||
|
||||
fmt.Errorf("XXXXXXXX: tee.TokenTTL: %v\n", tee.TokenTTL)
|
||||
|
||||
if now.Add(tee.TokenTTL).After(expirationDate) {
|
||||
resp.Auth.TTL = tee.TokenTTL
|
||||
} else {
|
||||
resp.Auth.TTL = expirationDate.Sub(now)
|
||||
}
|
||||
|
||||
if now.Add(tee.TokenMaxTTL).After(expirationDate) {
|
||||
resp.Auth.MaxTTL = tee.TokenMaxTTL
|
||||
} else {
|
||||
resp.Auth.MaxTTL = expirationDate.Sub(now)
|
||||
}
|
||||
|
||||
resp.Auth.Period = tee.TokenPeriod
|
||||
return resp, nil
|
||||
}
|
145
tee/path_login_test.go
Normal file
145
tee/path_login_test.go
Normal file
File diff suppressed because one or more lines are too long
331
tee/path_tees.go
Normal file
331
tee/path_tees.go
Normal file
|
@ -0,0 +1,331 @@
|
|||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright (c) Matter Labs
|
||||
|
||||
package tee
|
||||
|
||||
import "C"
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/vault/sdk/framework"
|
||||
"github.com/hashicorp/vault/sdk/helper/tokenutil"
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
|
||||
"github.com/matter-labs/vault-auth-tee/ratee"
|
||||
)
|
||||
|
||||
func pathListTees(b *backend) *framework.Path {
|
||||
return &framework.Path{
|
||||
Pattern: "tees/?",
|
||||
|
||||
DisplayAttrs: &framework.DisplayAttributes{
|
||||
OperationPrefix: operationPrefixTee,
|
||||
OperationSuffix: "tees",
|
||||
Navigation: true,
|
||||
ItemType: "Tee",
|
||||
},
|
||||
|
||||
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||
logical.ListOperation: b.pathTeeList,
|
||||
},
|
||||
|
||||
HelpSynopsis: pathTeeHelpSyn,
|
||||
HelpDescription: pathTeeHelpDesc,
|
||||
}
|
||||
}
|
||||
|
||||
func pathTees(b *backend) *framework.Path {
|
||||
p := &framework.Path{
|
||||
Pattern: "tees/" + framework.GenericNameRegex("name"),
|
||||
|
||||
DisplayAttrs: &framework.DisplayAttributes{
|
||||
OperationPrefix: operationPrefixTee,
|
||||
OperationSuffix: "tee",
|
||||
Action: "Create",
|
||||
ItemType: "Tee",
|
||||
},
|
||||
|
||||
Fields: map[string]*framework.FieldSchema{
|
||||
"name": {
|
||||
Type: framework.TypeString,
|
||||
Description: "The name of the Tee, which passes remote attestation verification",
|
||||
},
|
||||
|
||||
"types": {
|
||||
Type: framework.TypeCommaStringSlice,
|
||||
Description: "The types of the TEE.",
|
||||
},
|
||||
|
||||
"sgx_mrsigner": {
|
||||
Type: framework.TypeString,
|
||||
Description: `The SGX mrsigner hex value to check the attestation report against`,
|
||||
},
|
||||
|
||||
"sgx_mrenclave": {
|
||||
Type: framework.TypeString,
|
||||
Description: `The SGX mrenclave hex value to check the attestation report against`,
|
||||
},
|
||||
|
||||
"sgx_isv_prodid": {
|
||||
Type: framework.TypeInt,
|
||||
Description: `The SGX isv_prodid value to check the attestation report against`,
|
||||
},
|
||||
|
||||
"sgx_min_isv_svn": {
|
||||
Type: framework.TypeInt,
|
||||
Description: `The SGX minimum isv_svn value to check the attestation report against`,
|
||||
},
|
||||
|
||||
"sgx_allowed_tcb_levels": {
|
||||
Type: framework.TypeCommaStringSlice,
|
||||
Description: `A comma seperated list of allowed SGX TCB states.
|
||||
Allowed values are: ConfigNeeded, OutOfDate, OutOfDateConfigNeeded, SwHardeningNeeded, ConfigAndSwHardeningNeeded`,
|
||||
},
|
||||
|
||||
"display_name": {
|
||||
Type: framework.TypeString,
|
||||
Description: `The display name to use for clients using this certificate.`,
|
||||
},
|
||||
},
|
||||
|
||||
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||
logical.DeleteOperation: b.pathTeeDelete,
|
||||
logical.ReadOperation: b.pathTeeRead,
|
||||
logical.UpdateOperation: b.pathTeeWrite,
|
||||
},
|
||||
|
||||
HelpSynopsis: pathTeeHelpSyn,
|
||||
HelpDescription: pathTeeHelpDesc,
|
||||
}
|
||||
|
||||
tokenutil.AddTokenFields(p.Fields)
|
||||
return p
|
||||
}
|
||||
|
||||
func (b *backend) Tee(ctx context.Context, s logical.Storage, n string) (*TeeEntry, error) {
|
||||
entry, err := s.Get(ctx, "tee/"+strings.ToLower(n))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if entry == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var result TeeEntry
|
||||
if err := entry.DecodeJSON(&result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (b *backend) pathTeeDelete(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
err := req.Storage.Delete(ctx, "tee/"+strings.ToLower(d.Get("name").(string)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (b *backend) pathTeeList(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
tees, err := req.Storage.List(ctx, "tee/")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return logical.ListResponse(tees), nil
|
||||
}
|
||||
|
||||
func (b *backend) pathTeeRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
tee, err := b.Tee(ctx, req.Storage, strings.ToLower(d.Get("name").(string)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tee == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"display_name": tee.DisplayName,
|
||||
"types": tee.Types,
|
||||
"sgx_mrsigner": tee.SgxMrsigner,
|
||||
"sgx_mrenclave": tee.SgxMrenclave,
|
||||
"sgx_isv_prodid": tee.SgxIsvProdid,
|
||||
"sgx_min_isv_svn": tee.SgxMinIsvSvn,
|
||||
"sgx_allowed_tcb_levels": tee.SgxAllowedTcbLevels,
|
||||
}
|
||||
tee.PopulateTokenData(data)
|
||||
|
||||
return &logical.Response{
|
||||
Data: data,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *backend) pathTeeWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
name := strings.ToLower(d.Get("name").(string))
|
||||
|
||||
tee, err := b.Tee(ctx, req.Storage, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if tee == nil {
|
||||
tee = &TeeEntry{
|
||||
Name: name,
|
||||
}
|
||||
}
|
||||
|
||||
// Get non tokenutil fields
|
||||
if displayNameRaw, ok := d.GetOk("display_name"); ok {
|
||||
tee.DisplayName = displayNameRaw.(string)
|
||||
}
|
||||
|
||||
if teeTypes, ok := d.GetOk("types"); ok {
|
||||
tee.Types = make(map[string]bool)
|
||||
handled := make(map[string]bool)
|
||||
for _, t := range teeTypes.([]string) {
|
||||
// only SGX supported for now
|
||||
if _, ok = handled[t]; ok {
|
||||
return logical.ErrorResponse(fmt.Sprintf("duplicate TEE type `%s`", t)), nil
|
||||
}
|
||||
if t == "sgx" {
|
||||
tee.Types[t] = true
|
||||
handled[t] = true
|
||||
response, err := handleSGXConfig(d, tee)
|
||||
if response != nil || err != nil {
|
||||
return response, err
|
||||
}
|
||||
} else {
|
||||
return logical.ErrorResponse(fmt.Sprintf("invalid TEE type `%s`", t)), nil
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return logical.ErrorResponse("missing TEE types"), nil
|
||||
}
|
||||
|
||||
// Get tokenutil fields
|
||||
if err := tee.ParseTokenFields(req, d); err != nil {
|
||||
return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest
|
||||
}
|
||||
|
||||
var resp logical.Response
|
||||
|
||||
systemDefaultTTL := b.System().DefaultLeaseTTL()
|
||||
if tee.TokenTTL > systemDefaultTTL {
|
||||
resp.AddWarning(fmt.Sprintf("Given ttl of %d seconds is greater than current mount/system default of %d seconds", tee.TokenTTL/time.Second, systemDefaultTTL/time.Second))
|
||||
}
|
||||
systemMaxTTL := b.System().MaxLeaseTTL()
|
||||
if tee.TokenMaxTTL > systemMaxTTL {
|
||||
resp.AddWarning(fmt.Sprintf("Given max_ttl of %d seconds is greater than current mount/system default of %d seconds", tee.TokenMaxTTL/time.Second, systemMaxTTL/time.Second))
|
||||
}
|
||||
if tee.TokenMaxTTL != 0 && tee.TokenTTL > tee.TokenMaxTTL {
|
||||
return logical.ErrorResponse("ttl should be shorter than max_ttl"), nil
|
||||
}
|
||||
if tee.TokenPeriod > systemMaxTTL {
|
||||
resp.AddWarning(fmt.Sprintf("Given period of %d seconds is greater than the backend's maximum TTL of %d seconds", tee.TokenPeriod/time.Second, systemMaxTTL/time.Second))
|
||||
}
|
||||
|
||||
// Default the display name to the certificate name if not given
|
||||
if tee.DisplayName == "" {
|
||||
tee.DisplayName = name
|
||||
}
|
||||
|
||||
// Store it
|
||||
entry, err := logical.StorageEntryJSON("tee/"+name, tee)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := req.Storage.Put(ctx, entry); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(resp.Warnings) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
func handleSGXConfig(d *framework.FieldData, tee *TeeEntry) (*logical.Response, error) {
|
||||
if sgxMrsignerRaw, ok := d.GetOk("sgx_mrsigner"); ok && sgxMrsignerRaw.(string) != "" {
|
||||
tee.SgxMrsigner = strings.ToLower(sgxMrsignerRaw.(string))
|
||||
b, err := hex.DecodeString(tee.SgxMrsigner)
|
||||
if err != nil || len(b) != 32 {
|
||||
return logical.ErrorResponse("`sgx_mrsigner` must be 32 byte hex encoded"), nil
|
||||
}
|
||||
}
|
||||
|
||||
if sgxMrenclaveRaw, ok := d.GetOk("sgx_mrenclave"); ok && sgxMrenclaveRaw.(string) != "" {
|
||||
tee.SgxMrenclave = strings.ToLower(sgxMrenclaveRaw.(string))
|
||||
b, err := hex.DecodeString(tee.SgxMrenclave)
|
||||
if err != nil || len(b) != 32 {
|
||||
return logical.ErrorResponse("`sgx_mrenclave` must be 32 byte hex encoded"), nil
|
||||
}
|
||||
}
|
||||
|
||||
if tee.SgxMrsigner == "" && tee.SgxMrenclave == "" {
|
||||
return logical.ErrorResponse("either `sgx_mrsigner` or `sgx_mrenclave` must be set"), nil
|
||||
}
|
||||
|
||||
if sgxIsvProdidRaw, ok := d.GetOk("sgx_isv_prodid"); ok {
|
||||
tee.SgxIsvProdid = sgxIsvProdidRaw.(int)
|
||||
}
|
||||
|
||||
if sgxMinIsvSvnRaw, ok := d.GetOk("sgx_min_isv_svn"); ok {
|
||||
tee.SgxMinIsvSvn = sgxMinIsvSvnRaw.(int)
|
||||
}
|
||||
|
||||
if sgxAllowedTcbLevelsRaw, ok := d.GetOk("sgx_allowed_tcb_levels"); ok {
|
||||
tee.SgxAllowedTcbLevels = make(map[ratee.SgxQlQvResult]bool)
|
||||
for _, v := range sgxAllowedTcbLevelsRaw.([]string) {
|
||||
var state ratee.SgxQlQvResult
|
||||
switch v {
|
||||
case "Ok":
|
||||
state = ratee.SgxQlQvResultOk
|
||||
case "ConfigNeeded":
|
||||
state = ratee.SgxQlQvResultConfigNeeded
|
||||
case "OutOfDate":
|
||||
state = ratee.SgxQlQvResultOutOfDate
|
||||
case "OutOfDateConfigNeeded":
|
||||
state = ratee.SgxQlQvResultOutOfDateConfigNeeded
|
||||
case "SwHardeningNeeded":
|
||||
state = ratee.SgxQlQvResultSwHardeningNeeded
|
||||
case "ConfigAndSwHardeningNeeded":
|
||||
state = ratee.SgxQlQvResultConfigAndSwHardeningNeeded
|
||||
default:
|
||||
return logical.ErrorResponse("invalid sgx_allowed_tcb_levels value"), logical.ErrInvalidRequest
|
||||
}
|
||||
tee.SgxAllowedTcbLevels[state] = true
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type TeeEntry struct {
|
||||
tokenutil.TokenParams
|
||||
|
||||
Name string
|
||||
DisplayName string
|
||||
Types map[string]bool
|
||||
SgxMrsigner string
|
||||
SgxMrenclave string
|
||||
SgxIsvProdid int
|
||||
SgxMinIsvSvn int
|
||||
SgxAllowedTcbLevels map[ratee.SgxQlQvResult]bool
|
||||
}
|
||||
|
||||
const pathTeeHelpSyn = `
|
||||
Manage TEE remote attestation parameters used for authentication.`
|
||||
|
||||
const pathTeeHelpDesc = `
|
||||
This endpoint allows you to create, read, update, and delete TEEs
|
||||
that are allowed to authenticate.
|
||||
|
||||
Deleting a TEE will not revoke auth for prior authenticated connections.
|
||||
To do this, do a revoke on "login". If you don't need to revoke login immediately,
|
||||
then the next renew will cause the lease to expire.
|
||||
`
|
22
tee/test-fixtures/keys/cert.pem
Normal file
22
tee/test-fixtures/keys/cert.pem
Normal file
|
@ -0,0 +1,22 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIDtTCCAp2gAwIBAgIUf+jhKTFBnqSs34II0WS1L4QsbbAwDQYJKoZIhvcNAQEL
|
||||
BQAwFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wHhcNMTYwMjI5MDIyNzQxWhcNMjUw
|
||||
MTA1MTAyODExWjAbMRkwFwYDVQQDExBjZXJ0LmV4YW1wbGUuY29tMIIBIjANBgkq
|
||||
hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsZx0Svr82YJpFpIy4fJNW5fKA6B8mhxS
|
||||
TRAVnygAftetT8puHflY0ss7Y6X2OXjsU0PRn+1PswtivhKi+eLtgWkUF9cFYFGn
|
||||
SgMld6ZWRhNheZhA6ZfQmeM/BF2pa5HK2SDF36ljgjL9T+nWrru2Uv0BCoHzLAmi
|
||||
YYMiIWplidMmMO5NTRG3k+3AN0TkfakB6JVzjLGhTcXdOcVEMXkeQVqJMAuGouU5
|
||||
donyqtnaHuIJGuUdy54YDnX86txhOQhAv6r7dHXzZxS4pmLvw8UI1rsSf/GLcUVG
|
||||
B+5+AAGF5iuHC3N2DTl4xz3FcN4Cb4w9pbaQ7+mCzz+anqiJfyr2nwIDAQABo4H1
|
||||
MIHyMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAdBgNVHQ4EFgQUm++e
|
||||
HpyM3p708bgZJuRYEdX1o+UwHwYDVR0jBBgwFoAUncSzT/6HMexyuiU9/7EgHu+o
|
||||
k5swOwYIKwYBBQUHAQEELzAtMCsGCCsGAQUFBzAChh9odHRwOi8vMTI3LjAuMC4x
|
||||
OjgyMDAvdjEvcGtpL2NhMCEGA1UdEQQaMBiCEGNlcnQuZXhhbXBsZS5jb22HBH8A
|
||||
AAEwMQYDVR0fBCowKDAmoCSgIoYgaHR0cDovLzEyNy4wLjAuMTo4MjAwL3YxL3Br
|
||||
aS9jcmwwDQYJKoZIhvcNAQELBQADggEBABsuvmPSNjjKTVN6itWzdQy+SgMIrwfs
|
||||
X1Yb9Lefkkwmp9ovKFNQxa4DucuCuzXcQrbKwWTfHGgR8ct4rf30xCRoA7dbQWq4
|
||||
aYqNKFWrRaBRAaaYZ/O1ApRTOrXqRx9Eqr0H1BXLsoAq+mWassL8sf6siae+CpwA
|
||||
KqBko5G0dNXq5T4i2LQbmoQSVetIrCJEeMrU+idkuqfV2h1BQKgSEhFDABjFdTCN
|
||||
QDAHsEHsi2M4/jRW9fqEuhHSDfl2n7tkFUI8wTHUUCl7gXwweJ4qtaSXIwKXYzNj
|
||||
xqKHA8Purc1Yfybz4iE1JCROi9fInKlzr5xABq8nb9Qc/J9DIQM+Xmk=
|
||||
-----END CERTIFICATE-----
|
27
tee/test-fixtures/keys/key.pem
Normal file
27
tee/test-fixtures/keys/key.pem
Normal file
|
@ -0,0 +1,27 @@
|
|||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEogIBAAKCAQEAsZx0Svr82YJpFpIy4fJNW5fKA6B8mhxSTRAVnygAftetT8pu
|
||||
HflY0ss7Y6X2OXjsU0PRn+1PswtivhKi+eLtgWkUF9cFYFGnSgMld6ZWRhNheZhA
|
||||
6ZfQmeM/BF2pa5HK2SDF36ljgjL9T+nWrru2Uv0BCoHzLAmiYYMiIWplidMmMO5N
|
||||
TRG3k+3AN0TkfakB6JVzjLGhTcXdOcVEMXkeQVqJMAuGouU5donyqtnaHuIJGuUd
|
||||
y54YDnX86txhOQhAv6r7dHXzZxS4pmLvw8UI1rsSf/GLcUVGB+5+AAGF5iuHC3N2
|
||||
DTl4xz3FcN4Cb4w9pbaQ7+mCzz+anqiJfyr2nwIDAQABAoIBAHR7fFV0eAGaopsX
|
||||
9OD0TUGlsephBXb43g0GYHfJ/1Ew18w9oaxszJEqkl+PB4W3xZ3yG3e8ZomxDOhF
|
||||
RreF2WgG5xOfhDogMwu6NodbArfgnAvoC6JnW3qha8HMP4F500RFVyCRcd6A3Frd
|
||||
rFtaZn/UyCsBAN8/zkwPeYHayo7xX6d9kzgRl9HluEX5PXI5+3uiBDUiM085gkLI
|
||||
5Cmadh9fMdjfhDXI4x2JYmILpp/9Nlc/krB15s5n1MPNtn3yL0TI0tWp0WlwDCV7
|
||||
oUm1SfIM0F1fXGFyFDcqwoIr6JCQgXk6XtTg31YhH1xgUIclUVdtHqmAwAbLdIhQ
|
||||
GAiHn2kCgYEAwD4pZ8HfpiOG/EHNoWsMATc/5yC7O8F9WbvcHZQIymLY4v/7HKZb
|
||||
VyOR6UQ5/O2cztSGIuKSF6+OK1C34lOyCuTSOTFrjlgEYtLIXjdGLfFdtOO8GRQR
|
||||
akVXdwuzNAjTBaH5eXbG+NKcjmCvZL48dQVlfDTVulzFGbcsVTHIMQUCgYEA7IQI
|
||||
FVsKnY3KqpyGqXq92LMcsT3XgW6X1BIIV+YhJ5AFUFkFrjrbXs94/8XyLfi0xBQy
|
||||
efK+8g5sMs7koF8LyZEcAXWZJQduaKB71hoLlRaU4VQkL/dl2B6VFmAII/CsRCYh
|
||||
r9RmDN2PF/mp98Ih9dpC1VqcCDRGoTYsd7jLalMCgYAMgH5k1wDaZxkSMp1S0AlZ
|
||||
0uP+/evvOOgT+9mWutfPgZolOQx1koQCKLgGeX9j6Xf3I28NubpSfAI84uTyfQrp
|
||||
FnRtb79U5Hh0jMynA+U2e6niZ6UF5H41cQj9Hu+qhKBkj2IP+h96cwfnYnZFkPGR
|
||||
kqZE65KyqfHPeFATwkcImQKBgCdrfhlpGiTWXCABhKQ8s+WpPLAB2ahV8XJEKyXT
|
||||
UlVQuMIChGLcpnFv7P/cUxf8asx/fUY8Aj0/0CLLvulHziQjTmKj4gl86pb/oIQ3
|
||||
xRRtNhU0O+/OsSfLORgIm3K6C0w0esregL/GMbJSR1TnA1gBr7/1oSnw5JC8Ab9W
|
||||
injHAoGAJT1MGAiQrhlt9GCGe6Ajw4omdbY0wS9NXefnFhf7EwL0es52ezZ28zpU
|
||||
2LXqSFbtann5CHgpSLxiMYPDIf+er4xgg9Bz34tz1if1rDfP2Qrxdrpr4jDnrGT3
|
||||
gYC2qCpvVD9RRUMKFfnJTfl5gMQdBW/LINkHtJ82snAeLl3gjQ4=
|
||||
-----END RSA PRIVATE KEY-----
|
302
tee/test_responder.go
Normal file
302
tee/test_responder.go
Normal file
|
@ -0,0 +1,302 @@
|
|||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
|
||||
// Package ocsp implements an OCSP responder based on a generic storage backend.
|
||||
// It provides a couple of sample implementations.
|
||||
// Because OCSP responders handle high query volumes, we have to be careful
|
||||
// about how much logging we do. Error-level logs are reserved for problems
|
||||
// internal to the server, that can be fixed by an administrator. Any type of
|
||||
// incorrect input from a user should be logged and Info or below. For things
|
||||
// that are logged on every request, Debug is the appropriate level.
|
||||
//
|
||||
// From https://github.com/cloudflare/cfssl/blob/master/ocsp/responder.go
|
||||
|
||||
package tee
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ocsp"
|
||||
)
|
||||
|
||||
var (
|
||||
malformedRequestErrorResponse = []byte{0x30, 0x03, 0x0A, 0x01, 0x01}
|
||||
internalErrorErrorResponse = []byte{0x30, 0x03, 0x0A, 0x01, 0x02}
|
||||
unauthorizedErrorResponse = []byte{0x30, 0x03, 0x0A, 0x01, 0x06}
|
||||
|
||||
// ErrNotFound indicates the request OCSP response was not found. It is used to
|
||||
// indicate that the responder should reply with unauthorizedErrorResponse.
|
||||
ErrNotFound = errors.New("Request OCSP Response not found")
|
||||
)
|
||||
|
||||
// Source represents the logical source of OCSP responses, i.e.,
|
||||
// the logic that actually chooses a response based on a request. In
|
||||
// order to create an actual responder, wrap one of these in a Responder
|
||||
// object and pass it to http.Handle. By default the Responder will set
|
||||
// the headers Cache-Control to "max-age=(response.NextUpdate-now), public, no-transform, must-revalidate",
|
||||
// Last-Modified to response.ThisUpdate, Expires to response.NextUpdate,
|
||||
// ETag to the SHA256 hash of the response, and Content-Type to
|
||||
// application/ocsp-response. If you want to override these headers,
|
||||
// or set extra headers, your source should return a http.Header
|
||||
// with the headers you wish to set. If you don'log want to set any
|
||||
// extra headers you may return nil instead.
|
||||
type Source interface {
|
||||
Response(*ocsp.Request) ([]byte, http.Header, error)
|
||||
}
|
||||
|
||||
// An InMemorySource is a map from serialNumber -> der(response)
|
||||
type InMemorySource map[string][]byte
|
||||
|
||||
// Response looks up an OCSP response to provide for a given request.
|
||||
// InMemorySource looks up a response purely based on serial number,
|
||||
// without regard to what issuer the request is asking for.
|
||||
func (src InMemorySource) Response(request *ocsp.Request) ([]byte, http.Header, error) {
|
||||
response, present := src[request.SerialNumber.String()]
|
||||
if !present {
|
||||
return nil, nil, ErrNotFound
|
||||
}
|
||||
return response, nil, nil
|
||||
}
|
||||
|
||||
// Stats is a basic interface that allows users to record information
|
||||
// about returned responses
|
||||
type Stats interface {
|
||||
ResponseStatus(ocsp.ResponseStatus)
|
||||
}
|
||||
|
||||
type logger interface {
|
||||
Log(args ...any)
|
||||
}
|
||||
|
||||
// A Responder object provides the HTTP logic to expose a
|
||||
// Source of OCSP responses.
|
||||
type Responder struct {
|
||||
log logger
|
||||
Source Source
|
||||
stats Stats
|
||||
}
|
||||
|
||||
// NewResponder instantiates a Responder with the give Source.
|
||||
func NewResponder(t logger, source Source, stats Stats) *Responder {
|
||||
return &Responder{
|
||||
Source: source,
|
||||
stats: stats,
|
||||
log: t,
|
||||
}
|
||||
}
|
||||
|
||||
func overrideHeaders(response http.ResponseWriter, headers http.Header) {
|
||||
for k, v := range headers {
|
||||
if len(v) == 1 {
|
||||
response.Header().Set(k, v[0])
|
||||
} else if len(v) > 1 {
|
||||
response.Header().Del(k)
|
||||
for _, e := range v {
|
||||
response.Header().Add(k, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// hashToString contains mappings for the only hash functions
|
||||
// x/crypto/ocsp supports
|
||||
var hashToString = map[crypto.Hash]string{
|
||||
crypto.SHA1: "SHA1",
|
||||
crypto.SHA256: "SHA256",
|
||||
crypto.SHA384: "SHA384",
|
||||
crypto.SHA512: "SHA512",
|
||||
}
|
||||
|
||||
// A Responder can process both GET and POST requests. The mapping
|
||||
// from an OCSP request to an OCSP response is done by the Source;
|
||||
// the Responder simply decodes the request, and passes back whatever
|
||||
// response is provided by the source.
|
||||
// Note: The caller must use http.StripPrefix to strip any path components
|
||||
// (including '/') on GET requests.
|
||||
// Do not use this responder in conjunction with http.NewServeMux, because the
|
||||
// default handler will try to canonicalize path components by changing any
|
||||
// strings of repeated '/' into a single '/', which will break the base64
|
||||
// encoding.
|
||||
func (rs *Responder) ServeHTTP(response http.ResponseWriter, request *http.Request) {
|
||||
// By default we set a 'max-age=0, no-cache' Cache-Control header, this
|
||||
// is only returned to the client if a valid authorized OCSP response
|
||||
// is not found or an error is returned. If a response if found the header
|
||||
// will be altered to contain the proper max-age and modifiers.
|
||||
response.Header().Add("Cache-Control", "max-age=0, no-cache")
|
||||
// Read response from request
|
||||
var requestBody []byte
|
||||
var err error
|
||||
switch request.Method {
|
||||
case "GET":
|
||||
base64Request, err := url.QueryUnescape(request.URL.Path)
|
||||
if err != nil {
|
||||
rs.log.Log("Error decoding URL:", request.URL.Path)
|
||||
response.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// url.QueryUnescape not only unescapes %2B escaping, but it additionally
|
||||
// turns the resulting '+' into a space, which makes base64 decoding fail.
|
||||
// So we go back afterwards and turn ' ' back into '+'. This means we
|
||||
// accept some malformed input that includes ' ' or %20, but that's fine.
|
||||
base64RequestBytes := []byte(base64Request)
|
||||
for i := range base64RequestBytes {
|
||||
if base64RequestBytes[i] == ' ' {
|
||||
base64RequestBytes[i] = '+'
|
||||
}
|
||||
}
|
||||
// In certain situations a UA may construct a request that has a double
|
||||
// slash between the host name and the base64 request body due to naively
|
||||
// constructing the request URL. In that case strip the leading slash
|
||||
// so that we can still decode the request.
|
||||
if len(base64RequestBytes) > 0 && base64RequestBytes[0] == '/' {
|
||||
base64RequestBytes = base64RequestBytes[1:]
|
||||
}
|
||||
requestBody, err = base64.StdEncoding.DecodeString(string(base64RequestBytes))
|
||||
if err != nil {
|
||||
rs.log.Log("Error decoding base64 from URL", string(base64RequestBytes))
|
||||
response.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
case "POST":
|
||||
requestBody, err = ioutil.ReadAll(request.Body)
|
||||
if err != nil {
|
||||
rs.log.Log("Problem reading body of POST", err)
|
||||
response.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
default:
|
||||
response.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
b64Body := base64.StdEncoding.EncodeToString(requestBody)
|
||||
rs.log.Log("Received OCSP request", b64Body)
|
||||
|
||||
// All responses after this point will be OCSP.
|
||||
// We could check for the content type of the request, but that
|
||||
// seems unnecessariliy restrictive.
|
||||
response.Header().Add("Content-Type", "application/ocsp-response")
|
||||
|
||||
// Parse response as an OCSP request
|
||||
// XXX: This fails if the request contains the nonce extension.
|
||||
// We don'log intend to support nonces anyway, but maybe we
|
||||
// should return unauthorizedRequest instead of malformed.
|
||||
ocspRequest, err := ocsp.ParseRequest(requestBody)
|
||||
if err != nil {
|
||||
rs.log.Log("Error decoding request body", b64Body)
|
||||
response.WriteHeader(http.StatusBadRequest)
|
||||
response.Write(malformedRequestErrorResponse)
|
||||
if rs.stats != nil {
|
||||
rs.stats.ResponseStatus(ocsp.Malformed)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Look up OCSP response from source
|
||||
ocspResponse, headers, err := rs.Source.Response(ocspRequest)
|
||||
if err != nil {
|
||||
if err == ErrNotFound {
|
||||
rs.log.Log("No response found for request: serial %x, request body %s",
|
||||
ocspRequest.SerialNumber, b64Body)
|
||||
response.Write(unauthorizedErrorResponse)
|
||||
if rs.stats != nil {
|
||||
rs.stats.ResponseStatus(ocsp.Unauthorized)
|
||||
}
|
||||
return
|
||||
}
|
||||
rs.log.Log("Error retrieving response for request: serial %x, request body %s, error",
|
||||
ocspRequest.SerialNumber, b64Body, err)
|
||||
response.WriteHeader(http.StatusInternalServerError)
|
||||
response.Write(internalErrorErrorResponse)
|
||||
if rs.stats != nil {
|
||||
rs.stats.ResponseStatus(ocsp.InternalError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
parsedResponse, err := ocsp.ParseResponse(ocspResponse, nil)
|
||||
if err != nil {
|
||||
rs.log.Log("Error parsing response for serial %x",
|
||||
ocspRequest.SerialNumber, err)
|
||||
response.Write(internalErrorErrorResponse)
|
||||
if rs.stats != nil {
|
||||
rs.stats.ResponseStatus(ocsp.InternalError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Write OCSP response to response
|
||||
response.Header().Add("Last-Modified", parsedResponse.ThisUpdate.Format(time.RFC1123))
|
||||
response.Header().Add("Expires", parsedResponse.NextUpdate.Format(time.RFC1123))
|
||||
now := time.Now()
|
||||
maxAge := 0
|
||||
if now.Before(parsedResponse.NextUpdate) {
|
||||
maxAge = int(parsedResponse.NextUpdate.Sub(now) / time.Second)
|
||||
} else {
|
||||
// TODO(#530): we want max-age=0 but this is technically an authorized OCSP response
|
||||
// (despite being stale) and 5019 forbids attaching no-cache
|
||||
maxAge = 0
|
||||
}
|
||||
response.Header().Set(
|
||||
"Cache-Control",
|
||||
fmt.Sprintf(
|
||||
"max-age=%d, public, no-transform, must-revalidate",
|
||||
maxAge,
|
||||
),
|
||||
)
|
||||
responseHash := sha256.Sum256(ocspResponse)
|
||||
response.Header().Add("ETag", fmt.Sprintf("\"%X\"", responseHash))
|
||||
|
||||
if headers != nil {
|
||||
overrideHeaders(response, headers)
|
||||
}
|
||||
|
||||
// RFC 7232 says that a 304 response must contain the above
|
||||
// headers if they would also be sent for a 200 for the same
|
||||
// request, so we have to wait until here to do this
|
||||
if etag := request.Header.Get("If-None-Match"); etag != "" {
|
||||
if etag == fmt.Sprintf("\"%X\"", responseHash) {
|
||||
response.WriteHeader(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
}
|
||||
response.WriteHeader(http.StatusOK)
|
||||
response.Write(ocspResponse)
|
||||
if rs.stats != nil {
|
||||
rs.stats.ResponseStatus(ocsp.Success)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Copyright (c) 2014 CloudFlare Inc.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
|
||||
Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
|
||||
Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
||||
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
Loading…
Add table
Add a link
Reference in a new issue