-
Notifications
You must be signed in to change notification settings - Fork 72
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
Add ITA Verifier Client #530
base: tdx_rtmr
Are you sure you want to change the base?
Changes from all commits
6f64f4d
838091c
ba1f10e
8f20661
b13ff34
9cd00af
8ae815f
de4ecf6
8c31fd2
01c5cd7
7b9f778
b9795b1
7f0d2f2
b819896
b621dd1
3a140e1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,255 @@ | ||
package ita | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"crypto/sha512" | ||
"crypto/tls" | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"strings" | ||
|
||
"github.com/google/go-tpm-tools/verifier" | ||
) | ||
|
||
const ( | ||
nonceEndpoint = "/appraisal/v2/nonce" | ||
tokenEndpoint = "/appraisal/v2/attest/gcp/confidentialspace" | ||
|
||
apiKeyHeader = "x-api-key" | ||
acceptHeader = "Accept" | ||
contentTypeHeader = "Content-Type" | ||
applicationJSON = "application/json" | ||
|
||
challengeNamePrefix = "ita://" | ||
) | ||
|
||
var regionalURLs map[string]string = map[string]string{ | ||
"US": "https://api.trustauthority.intel.com", | ||
"EU": "https://api.eu.trustauthority.intel.com", | ||
} | ||
|
||
type client struct { | ||
inner *http.Client | ||
apiURL string | ||
apiKey string | ||
} | ||
|
||
func urlFromRegion(region string) (string, error) { | ||
if region == "" { | ||
return "", errors.New("API region required to initialize ITA client") | ||
} | ||
|
||
url, ok := regionalURLs[strings.ToUpper(region)] | ||
if !ok { | ||
// Create list of allowed regions. | ||
keys := []string{} | ||
for k := range regionalURLs { | ||
keys = append(keys, k) | ||
} | ||
return "", fmt.Errorf("unsupported region %v, expect one of %v", region, keys) | ||
} | ||
|
||
return url, nil | ||
} | ||
|
||
func NewClient(region string, key string) (verifier.Client, error) { | ||
url, err := urlFromRegion(region) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return &client{ | ||
inner: &http.Client{ | ||
Transport: &http.Transport{ | ||
// https://github.com/intel/trustauthority-client-for-go/blob/main/go-connector/token.go#L130. | ||
TLSClientConfig: &tls.Config{ | ||
CipherSuites: []uint16{ | ||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, | ||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, | ||
}, | ||
InsecureSkipVerify: false, | ||
MinVersion: tls.VersionTLS12, | ||
}, | ||
Proxy: http.ProxyFromEnvironment, | ||
}, | ||
}, | ||
apiURL: url, | ||
apiKey: key, | ||
}, nil | ||
} | ||
|
||
// Confirm that client implements verifier.Client interface. | ||
var _ verifier.Client = (*client)(nil) | ||
|
||
type itaNonce struct { | ||
Val []byte `json:"val"` | ||
Iat []byte `json:"iat"` | ||
Signature []byte `json:"signature"` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is verifying the Signature needed? Maybe in createHashedNonce There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't need to verify the signature - this should be done by ITA. We should - however - pass the nonce object back to ITA when requesting a token. |
||
} | ||
|
||
// The ITA evidence nonce is a concatenation+hash of Val and Iat. See references below: | ||
// https://github.com/intel/trustauthority-client-for-go/blob/main/go-connector/attest.go#L22 | ||
// https://github.com/intel/trustauthority-client-for-go/blob/main/go-tdx/tdx_adapter.go#L37 | ||
func createHashedNonce(nonce *itaNonce) ([]byte, error) { | ||
hash := sha512.New() | ||
_, err := hash.Write(append(nonce.Val, nonce.Iat...)) | ||
if err != nil { | ||
return nil, fmt.Errorf("error hashing ITA nonce: %v", err) | ||
} | ||
|
||
return hash.Sum(nil), err | ||
} | ||
|
||
func (c *client) CreateChallenge(_ context.Context) (*verifier.Challenge, error) { | ||
url := c.apiURL + nonceEndpoint | ||
|
||
headers := map[string]string{ | ||
apiKeyHeader: c.apiKey, | ||
acceptHeader: applicationJSON, | ||
} | ||
|
||
resp := &itaNonce{} | ||
if err := c.doHTTPRequest(http.MethodGet, url, nil, headers, &resp); err != nil { | ||
return nil, err | ||
} | ||
|
||
nonce, err := createHashedNonce(resp) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return &verifier.Challenge{ | ||
Name: challengeNamePrefix + string(resp.Val), | ||
Nonce: nonce, | ||
Val: resp.Val, | ||
Iat: resp.Iat, | ||
Signature: resp.Signature, | ||
}, nil | ||
} | ||
|
||
func convertRequestToTokenRequest(request verifier.VerifyAttestationRequest) tokenRequest { | ||
// Trim trailing 0xFF bytes from CCEL Data. | ||
data := request.TDCCELAttestation.CcelData | ||
trimIndex := len(data) | ||
|
||
for ; trimIndex >= 0; trimIndex-- { | ||
c := data[trimIndex-1] | ||
// Proceed until 0xFF padding ends. | ||
if c != byte(255) { | ||
break | ||
} | ||
} | ||
|
||
tokenReq := tokenRequest{ | ||
PolicyMatch: true, | ||
TDX: tdxEvidence{ | ||
EventLog: data[:trimIndex], | ||
CanonicalEventLog: request.TDCCELAttestation.CanonicalEventLog, | ||
Quote: request.TDCCELAttestation.TdQuote, | ||
VerifierNonce: nonce{ | ||
Val: request.Challenge.Val, | ||
Iat: request.Challenge.Iat, | ||
Signature: request.Challenge.Signature, | ||
}, | ||
}, | ||
SigAlg: "RS256", // Figure out what this should be. | ||
GCP: gcpData{ | ||
AKCert: request.TDCCELAttestation.AkCert, | ||
IntermediateCerts: request.TDCCELAttestation.IntermediateCerts, | ||
CSInfo: confidentialSpaceInfo{ | ||
TokenOpts: tokenOptions{ | ||
Audience: request.TokenOptions.CustomAudience, | ||
Nonces: request.TokenOptions.CustomNonce, | ||
TokenType: request.TokenOptions.TokenType, | ||
TokenTypeOpts: tokenTypeOptions{}, | ||
}, | ||
}, | ||
}, | ||
} | ||
|
||
for _, token := range request.GcpCredentials { | ||
tokenReq.GCP.GcpCredentials = append(tokenReq.GCP.GcpCredentials, string(token)) | ||
} | ||
|
||
for _, sig := range request.ContainerImageSignatures { | ||
itaSig := containerSignature{ | ||
Payload: sig.Payload, | ||
Signature: sig.Signature, | ||
} | ||
tokenReq.GCP.CSInfo.SignedEntities = append(tokenReq.GCP.CSInfo.SignedEntities, itaSig) | ||
} | ||
|
||
return tokenReq | ||
} | ||
|
||
func (c *client) VerifyAttestation(_ context.Context, request verifier.VerifyAttestationRequest) (*verifier.VerifyAttestationResponse, error) { | ||
if request.TDCCELAttestation == nil { | ||
return nil, errors.New("TDX required for ITA attestation") | ||
} | ||
|
||
tokenReq := convertRequestToTokenRequest(request) | ||
|
||
url := c.apiURL + tokenEndpoint | ||
headers := map[string]string{ | ||
apiKeyHeader: c.apiKey, | ||
acceptHeader: applicationJSON, | ||
contentTypeHeader: applicationJSON, | ||
} | ||
|
||
resp := &tokenResponse{} | ||
if err := c.doHTTPRequest(http.MethodPost, url, tokenReq, headers, &resp); err != nil { | ||
return nil, err | ||
} | ||
|
||
return &verifier.VerifyAttestationResponse{ | ||
ClaimsToken: []byte(resp.Token), | ||
}, nil | ||
} | ||
|
||
func (c *client) doHTTPRequest(method string, url string, reqStruct any, headers map[string]string, respStruct any) error { | ||
// Create HTTP request. | ||
var req *http.Request | ||
var err error | ||
if reqStruct != nil { | ||
body, err := json.Marshal(reqStruct) | ||
if err != nil { | ||
return fmt.Errorf("error marshaling request: %v", err) | ||
} | ||
|
||
req, err = http.NewRequest(method, url, bytes.NewReader(body)) | ||
if err != nil { | ||
return fmt.Errorf("error creating HTTP request: %v", err) | ||
} | ||
} else { | ||
req, err = http.NewRequest(method, url, nil) | ||
if err != nil { | ||
return fmt.Errorf("error creating HTTP request: %v", err) | ||
} | ||
} | ||
|
||
// Add headers to request. | ||
for key, val := range headers { | ||
req.Header.Add(key, val) | ||
} | ||
|
||
resp, err := c.inner.Do(req) | ||
if err != nil { | ||
return fmt.Errorf("HTTP request error: %v", err) | ||
} | ||
|
||
// Read and unmarshal response body. | ||
respBody, err := io.ReadAll(resp.Body) | ||
if err != nil { | ||
return fmt.Errorf("error reading response body: %v", err) | ||
} | ||
|
||
if err := json.Unmarshal(respBody, respStruct); err != nil { | ||
return fmt.Errorf("error unmarshaling response: %v", err) | ||
} | ||
|
||
return nil | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we really need the proxy setting?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added this since it was in the example ITA sent me. I can test it out without the setting and remove it if needed.