blob: 9b06358e3226d28475c4c5d50f099520b1e35a31 [file] [log] [blame]
// Copyright 2020 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package hwsec
import (
"context"
"crypto"
"crypto/ecdsa"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/asn1"
"encoding/base64"
"encoding/hex"
"fmt"
"io/ioutil"
"math/big"
"net/http"
"net/url"
"strings"
"github.com/golang/protobuf/proto"
apb "chromiumos/system_api/attestation_proto"
"chromiumos/tast/errors"
)
// SendPostRequestTo sends POST request with body to serverURL.
func SendPostRequestTo(ctx context.Context, body, serverURL string) (string, error) {
req, err := http.NewRequest("POST", serverURL, strings.NewReader(body))
req.WithContext(ctx)
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/octet-stream")
return sendHTTPRequest(req)
}
// SendGetRequestTo sends GET request to serverURL
func SendGetRequestTo(ctx context.Context, serverURL string) (string, error) {
req, err := http.NewRequest("GET", serverURL, strings.NewReader(""))
req.WithContext(ctx)
if err != nil {
return "", err
}
return sendHTTPRequest(req)
}
func sendHTTPRequest(req *http.Request) (string, error) {
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return "", errors.Errorf("%v %v", resp.StatusCode, http.StatusText(resp.StatusCode))
}
respBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(respBody), nil
}
// UnmarshalSignedData unmarshal d into apb.SignedData; also returns encountered error if any
func UnmarshalSignedData(d []byte) (*apb.SignedData, error) {
var out apb.SignedData
if err := proto.Unmarshal(d, &out); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal")
}
return &out, nil
}
// HexDecode decode the hex-encoded enc into []byte; also returns encountered error if any
func HexDecode(enc []byte) ([]byte, error) {
dec := make([]byte, hex.DecodedLen(len(enc)))
n, err := hex.Decode(dec, enc)
if err != nil {
return []byte{}, errors.Wrap(err, "failed to call hex.Decode")
}
return dec[:n], nil
}
type attestationClient interface {
// IsEnrolled returns the flag to indicate if the DUT is
// enrolled and any encountered error during the operation.
IsEnrolled(ctx context.Context) (bool, error)
// Creates an enroll request that is sent to the corresponding pca server of pcaType
// later, and any error encountered during the operation.
CreateEnrollRequest(ctx context.Context, pcaType PCAType) (string, error)
// Finishes the enroll with resp from pca server of pcaType. Returns any
// encountered error during the operation.
FinishEnroll(ctx context.Context, pcaType PCAType, resp string) error
// Creates a certificate request that is sent to the corresponding pca server
// of pcaType later, and any error encountered during the operation.
CreateCertRequest(ctx context.Context, pcaType PCAType, profile apb.CertificateProfile, username, origin string) (string, error)
// Finishes the certified key creation with |resp| from PCA server. Returns any encountered error during the operation.
FinishCertRequest(ctx context.Context, response, username, label string) error
// SignEnterpriseVAChallenge performs SPKAC for the challenge.
SignEnterpriseVAChallenge(
ctx context.Context,
vaType VAType,
username,
label,
domain,
deviceID string,
includeSignedPublicKey bool,
challenge []byte) (string, error)
// SignSimpleChallenge signs the challenge with the specified key.
SignSimpleChallenge(ctx context.Context, username, label string, challenge []byte) (string, error)
// GetPublicKey gets the public part of the key.
GetPublicKey(ctx context.Context, username, label string) (string, error)
}
func caServerURL(pcaType PCAType) string {
switch pcaType {
case DefaultPCA:
return "https://chromeos-ca.gstatic.com"
case TestPCA:
return "https://asbestos-qa.corp.google.com"
}
panic(fmt.Sprintf("Unexpected PCA type: %v", pcaType))
}
func enrollURL(pcaType PCAType) string {
return caServerURL(pcaType) + "/enroll"
}
func certURL(pcaType PCAType) string {
return caServerURL(pcaType) + "/sign"
}
// VA declares a pair of functions that get and verify the VA challenge.
type VA interface {
// GetDecodedVAChallenge returns a new VA challenge.
GetDecodedVAChallenge(ctx context.Context) ([]byte, error)
// VerifyEncodedVAChallenge verifies the signed VA challenge response.
VerifyEncodedVAChallenge(ctx context.Context, signedChallenge string) error
}
// RealVA implements the VA functionality by talking to the real VA servers used in production.
type RealVA struct{}
// NewRealVA creates a new instance of RealVA.
func NewRealVA() *RealVA {
return &RealVA{}
}
// GetDecodedVAChallenge get the VA challenge from the default VA server and decoded it. In case of any of any error, retries for a certain small number of times.
func (rc *RealVA) GetDecodedVAChallenge(ctx context.Context) ([]byte, error) {
const retryCount = 5
return getDecodedVAChallenge(ctx, retryCount)
}
// VerifyEncodedVAChallenge sends the signed challenge to the default VA server.
func (rc *RealVA) VerifyEncodedVAChallenge(ctx context.Context, signedChallenge string) error {
urlForVerification := "https://test-dvproxy-server.sandbox.google.com/dvproxy/verifychallengeresponse?signeddata=" + url.QueryEscape(signedChallenge)
_, err := SendGetRequestTo(ctx, urlForVerification)
return err
}
// PCA declares functions that handle PCA requests by attestation.
type PCA interface {
// GetDecodedPCAChallenge returns a new VA challenge.
HandleEnrollRequest(ctx context.Context, request string, pcaType PCAType) (string, error)
// VerifyEncodedPCAChallenge verifies the signed VA challenge response.
HandleCertificateRequest(ctx context.Context, request string, pcaType PCAType) (string, error)
}
// PCAGoLib implements the PCA functionality by talking to the real servers used in production. The underlying implementation sends the HTTP request using Go's built-in packages.
type PCAGoLib struct{}
// NewPCAGoLib creates a new instance of PCAGoLib.
func NewPCAGoLib() *PCAGoLib {
return &PCAGoLib{}
}
// HandleEnrollRequest sends the request to the real PCA server in production directly.
func (rp *PCAGoLib) HandleEnrollRequest(ctx context.Context, request string, pcaType PCAType) (string, error) {
return SendPostRequestTo(ctx, request, enrollURL(pcaType))
}
// HandleCertificateRequest sends the request to the real PCA server in production directly.
func (rp *PCAGoLib) HandleCertificateRequest(ctx context.Context, request string, pcaType PCAType) (string, error) {
return SendPostRequestTo(ctx, request, certURL(pcaType))
}
// AttestationTest provides the complex operations in the attestation flow along with validations
type AttestationTest struct {
ac attestationClient
pcaType PCAType
pca PCA
va VA
}
// NewAttestationTestWith creates a new AttestationTest instance with the default PCA and VA instances that talk to the real servers used in production.
func NewAttestationTestWith(ac attestationClient, pcaType PCAType, pca PCA, va VA) *AttestationTest {
return &AttestationTest{ac, pcaType, pca, va}
}
// NewAttestationTest creates a new AttestationTest instance with the default PCA and VA objects that talk to the real servers used in production.
func NewAttestationTest(ac attestationClient, pcaType PCAType) *AttestationTest {
return NewAttestationTestWith(ac, pcaType, &PCAGoLib{}, &RealVA{})
}
// Enroll creates the enroll request, sends it to the corresponding PCA server, and finishes the request with the received response.
func (at *AttestationTest) Enroll(ctx context.Context) error {
req, err := at.ac.CreateEnrollRequest(ctx, DefaultPCA)
if err != nil {
return errors.Wrap(err, "failed to create enroll request")
}
resp, err := at.pca.HandleEnrollRequest(ctx, req, at.pcaType)
if err != nil {
return errors.Wrap(err, "failed to send request to CA")
}
if err := at.ac.FinishEnroll(ctx, DefaultPCA, resp); err != nil {
return errors.Wrap(err, "failed to finish enrollment")
}
isEnrolled, err := at.ac.IsEnrolled(ctx)
if err != nil {
return errors.Wrap(err, "failed to get enrollment status")
}
if !isEnrolled {
return errors.New("inconsistent reported status: after enrollment, status shows 'not enrolled'")
}
return nil
}
// GetCertificate creates the cert request, sends it to the corresponding PCA server, and finishes the request with the received response.
func (at *AttestationTest) GetCertificate(ctx context.Context, username, label string) error {
req, err := at.ac.CreateCertRequest(ctx, DefaultPCA, DefaultCertProfile, username, DefaultCertOrigin)
if err != nil {
return errors.Wrap(err, "failed to create certificate request")
}
resp, err := at.pca.HandleCertificateRequest(ctx, req, at.pcaType)
if err != nil {
return errors.Wrap(err, "failed to send request to CA")
}
if len(resp) == 0 {
return errors.New("unexpected empty cert")
}
err = at.ac.FinishCertRequest(ctx, resp, username, label)
if err != nil {
return errors.Wrap(err, "failed to finish cert request")
}
return nil
}
func getDecodedVAChallenge(ctx context.Context, retryCount int) ([]byte, error) {
var challenge []byte
var lastErr error
for i := 0; i < retryCount; i++ {
resp, err := SendGetRequestTo(ctx, "https://test-dvproxy-server.sandbox.google.com/dvproxy/getchallenge")
if err != nil {
lastErr = errors.Wrap(err, "failed to send request to VA")
continue
}
challenge, lastErr = base64.StdEncoding.DecodeString(resp)
if lastErr != nil {
lastErr = errors.Wrap(lastErr, "failed to base64-decode challenge")
continue
}
break
}
return challenge, lastErr
}
// SignEnterpriseChallenge gets the challenge from default VA server, perform SPKAC, and sends the signed challenge back to verify it
func (at *AttestationTest) SignEnterpriseChallenge(ctx context.Context, username, label string) error {
// In case the request fails for any reason, retry for 5 times.
challenge, err := at.va.GetDecodedVAChallenge(ctx)
if err != nil {
return errors.Wrap(err, "failed to base64-decode challenge")
}
signedChallenge, err := at.ac.SignEnterpriseVAChallenge(
ctx,
0,
username,
label,
username,
"fake_device_id",
true,
challenge)
if err != nil {
return errors.Wrap(err, "failed to sign VA challenge")
}
b64SignedChallenge := base64.StdEncoding.EncodeToString([]byte(signedChallenge))
if err := at.va.VerifyEncodedVAChallenge(ctx, b64SignedChallenge); err != nil {
return errors.Wrap(err, "failed to verify VA challenge")
}
return nil
}
// SignSimpleChallenge signs a known, short data with the cert, and verify it using its public key
func (at *AttestationTest) SignSimpleChallenge(ctx context.Context, username, label string) error {
signedChallenge, err := at.ac.SignSimpleChallenge(ctx, username, label, []byte{})
if err != nil {
return errors.Wrap(err, "failed to sign simple challenge")
}
signedChallengeProto, err := UnmarshalSignedData([]byte(signedChallenge))
if err != nil {
return errors.Wrap(err, "failed to unmarshal simple challenge reply")
}
publicKeyHex, err := at.ac.GetPublicKey(ctx, username, label)
if err != nil {
return errors.Wrap(err, "failed to get public key")
}
publicKeyDer, err := HexDecode([]byte(publicKeyHex))
if err != nil {
return errors.Wrap(err, "failed to hex-decode public key")
}
publicKey, err := x509.ParsePKIXPublicKey(publicKeyDer)
if err != nil {
return errors.Wrap(err, "failed to construct rsa public key")
}
hashValue := sha256.Sum256(signedChallengeProto.GetData())
switch publicKey := publicKey.(type) {
case *rsa.PublicKey:
if err := rsa.VerifyPKCS1v15(publicKey, crypto.SHA256, hashValue[:], signedChallengeProto.GetSignature()); err != nil {
return errors.Wrap(err, "failed to verify signature")
}
case *ecdsa.PublicKey:
var eSig struct {
R, S *big.Int
}
if _, err := asn1.Unmarshal(signedChallengeProto.GetSignature(), &eSig); err != nil {
return errors.Wrap(err, "failed to unmarshal ecdsa signature")
}
if !ecdsa.Verify(publicKey, hashValue[:], eSig.R, eSig.S) {
return errors.New("failed to verify signature")
}
}
return nil
}