feat: initial commit

Signed-off-by: Harald Hoyer <harald@matterlabs.dev>
This commit is contained in:
Harald Hoyer 2023-10-24 09:38:58 +02:00 committed by Harald Hoyer
commit c2411a45a7
Signed by: harald
GPG key ID: F519A1143B3FBE32
29 changed files with 6542 additions and 0 deletions

61
tee/backend.go Normal file
View 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

File diff suppressed because one or more lines are too long

40
tee/path_info.go Normal file
View 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
View 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, &quote)
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, &quoteStart)
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

File diff suppressed because one or more lines are too long

331
tee/path_tees.go Normal file
View 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.
`

View 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-----

View 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
View 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.
*/