8000 Retrieve instance info from AK certificate by jessieqliu · Pull Request #184 · google/go-tpm-tools · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Retrieve instance info from AK certificate #184

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

Merged
merged 8 commits into from
Apr 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 80 additions & 14 deletions server/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package server
import (
"crypto"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"errors"
"fmt"
Expand All @@ -21,6 +22,8 @@ var supportedHashAlgs = []tpm2.Algorithm{
tpm2.AlgSHA512, tpm2.AlgSHA384, tpm2.AlgSHA256, tpm2.AlgSHA1,
}

var cloudComputeInstanceIdentifierOID asn1.ObjectIdentifier = []int{1, 3, 6, 1, 4, 1, 11129, 2, 1, 21}

// VerifyOpts allows for customizing the functionality of VerifyAttestation.
type VerifyOpts struct {
// The nonce used when calling client.Attest
Expand All @@ -44,6 +47,21 @@ type VerifyOpts struct {
IntermediateCerts []*x509.Certificate
}

// TODO: Change int64 fields to uint64 when compatible with ASN1 parsing.
type gceSecurityProperties struct {
SecurityVersion int64 `asn1:"optional"`
IsProduction bool `asn1:"optional"`
}

type gceInstanceInfo struct {
Zone string `asn1:"utf8"`
ProjectNumber int64
ProjectID string `asn1:"utf8"`
InstanceID int64
InstanceName string `asn1:"utf8"`
SecurityProperties gceSecurityProperties `asn1:"optional"`
}

// VerifyAttestation performs the following checks on an Attestation:
// - the AK used to generate the attestation is trusted (based on VerifyOpts)
// - the provided signature is generated by the trusted AK public key
Expand Down Expand Up @@ -73,9 +91,11 @@ func VerifyAttestation(attestation *pb.Attestation, opts VerifyOpts) (*pb.Machin
if err != nil {
return nil, fmt.Errorf("attestation intermediates: %w", err)
}

opts.IntermediateCerts = append(opts.IntermediateCerts, certs...)

if err := checkAKTrusted(akPubKey, attestation.GetAkCert(), opts); err != nil {
machineState, err := validateAK(akPubKey, attestation.GetAkCert(), opts)
if err != nil {
return nil, fmt.Errorf("failed to validate AK: %w", err)
}

Expand Down Expand Up @@ -111,8 +131,6 @@ func VerifyAttestation(attestation *pb.Attestation, opts VerifyOpts) (*pb.Machin
continue
}

proto.Merge(celState, state)

// Verify the PCR hash algorithm. We have this check here (instead of at
// the start of the loop) so that the user gets a "SHA-1 not supported"
// error only if allowing SHA-1 support would actually allow the log
Expand All @@ -123,7 +141,10 @@ func VerifyAttestation(attestation *pb.Attestation, opts VerifyOpts) (*pb.Machin
continue
}

return celState, nil
proto.Merge(machineState, celState)
proto.Merge(machineState, state)

return machineState, nil
}

if lastErr != nil {
Expand All @@ -132,37 +153,76 @@ func VerifyAttestation(attestation *pb.Attestation, opts VerifyOpts) (*pb.Machin
return nil, fmt.Errorf("attestation does not contain a supported quote")
}

func getInstanceInfo(extensions []pkix.Extension) (*pb.GCEInstanceInfo, error) {
var rawInfo []byte
for _, ext := range extensions {
if ext.Id.Equal(cloudComputeInstanceIdentifierOID) {
rawInfo = ext.Value
break
}
}

// If GCE Instance Info extension is not found.
if len(rawInfo) == 0 {
return nil, nil
}

info := gceInstanceInfo{}
if _, err := asn1.Unmarshal(rawInfo, &info); err != nil {
return nil, fmt.Errorf("failed to parse GCE Instance Information Extension: %w", err)
}

// TODO: Remove when fields are changed to uint64.
if info.ProjectNumber < 0 || info.InstanceID < 0 || info.SecurityProperties.SecurityVersion < 0 {
return nil, fmt.Errorf("negative integer fields found in GCE Instance Information Extension")
}

// Check production.
if !info.SecurityProperties.IsProduction {
return nil, nil
}

return &pb.GCEInstanceInfo{
Zone: info.Zone,
ProjectId: info.ProjectID,
ProjectNumber: uint64(info.ProjectNumber),
InstanceName: info.InstanceName,
InstanceId: uint64(info.InstanceID),
}, nil
}

// Checks if the provided AK public key can be trusted
func checkAKTrusted(ak crypto.PublicKey, akCertBytes []byte, opts VerifyOpts) error {
func validateAK(ak crypto.PublicKey, akCertBytes []byte, opts VerifyOpts) (*pb.MachineState, error) {
checkPub := len(opts.TrustedAKs) > 0
checkCert := opts.TrustedRootCerts != nil
if !checkPub && !checkCert {
return fmt.Errorf("no trust mechanism provided, either use TrustedAKs or TrustedRootCerts")
return nil, fmt.Errorf("no trust mechanism provided, either use TrustedAKs or TrustedRootCerts")
}
if checkPub && checkCert {
return fmt.Errorf("multiple trust mechanisms provided, only use one of TrustedAKs or TrustedRootCerts")
return nil, fmt.Errorf("multiple trust mechanisms provided, only use one of TrustedAKs or TrustedRootCerts")
}

// Check against known AKs
if checkPub {
for _, trusted := range opts.TrustedAKs {
if internal.PubKeysEqual(ak, trusted) {
return nil
return &pb.MachineState{}, nil
}
}
return fmt.Errorf("public key is not trusted")
return nil, fmt.Errorf("public key is not trusted")
}

// Check if the AK Cert chains to a trusted root
if len(akCertBytes) == 0 {
return errors.New("no certificate provided in attestation")
return nil, errors.New("no certificate provided in attestation")
}
akCert, err := x509.ParseCertificate(akCertBytes)
if err != nil {
return fmt.Errorf("failed to parse certificate: %w", err)
return nil, fmt.Errorf("failed to parse certificate: %w", err)
}

if !internal.PubKeysEqual(ak, akCert.PublicKey) {
return fmt.Errorf("mismatch between public key and certificate")
return nil, fmt.Errorf("mismatch between public key and certificate")
}

// We manually handle the SAN extension because x509 marks it unhandled if
Expand All @@ -188,9 +248,15 @@ func checkAKTrusted(ak crypto.PublicKey, akCertBytes []byte, opts VerifyOpts) er
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsage(x509.ExtKeyUsageAny)},
}
if _, err := akCert.Verify(x509Opts); err != nil {
return fmt.Errorf("failed to verify certificate against trusted roots: %v", err)
return nil, fmt.Errorf("failed to verify certificate against trusted roots: %v", err)
}

instanceInfo, err := getInstanceInfo(akCert.Extensions)
if err != nil {
return nil, fmt.Errorf("error getting instance info: %v", err)
}
return nil

return &pb.MachineState{Platform: &pb.PlatformState{InstanceInfo: instanceInfo}}, nil
}

func checkHashAlgSupported(hash tpm2.Algorithm, opts VerifyOpts) error {
Expand Down
177 changes: 177 additions & 0 deletions server/verify_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509/pkix"
"encoding/asn1"
"fmt"
"io"
"strings"
Expand Down Expand Up @@ -609,3 +611,178 @@ func TestVerifyFailsWithMalformedIntermediatesInAttestation(t *testing.T) {
t.Error("expected error when calling VerifyAttestation with malformed intermediate")
}
}

func TestGetInstanceInfo(t *testing.T) {
expectedInstanceInfo := &attestpb.GCEInstanceInfo{
Zone: "expected zone",
ProjectId: "expected project id",
ProjectNumber: 0,
InstanceName: "expected instance name",
InstanceId: 1,
}

extStruct := gceInstanceInfo{
Zone: expectedInstanceInfo.Zone,
ProjectID: expectedInstanceInfo.ProjectId,
ProjectNumber: int64(expectedInstanceInfo.ProjectNumber),
InstanceName: expectedInstanceInfo.InstanceName,
InstanceID: int64(expectedInstanceInfo.InstanceId),
SecurityProperties: gceSecurityProperties{
SecurityVersion: 0,
IsProduction: true,
},
}

marshaledExt, err := asn1.Marshal(extStruct)
if err != nil {
t.Fatalf("Error marshaling test extension: %v", err)
}

ext := []pkix.Extension{{
Id: cloudComputeInstanceIdentifierOID,
Value: marshaledExt,
}}

instanceInfo, err := getInstanceInfo(ext)
if err != nil {
t.Fatalf("getInstanceInfo returned with error: %v", err)
}
if instanceInfo == nil {
t.Fatal("getInstanceInfo returned nil instance info.")
}

if !proto.Equal(instanceInfo, expectedInstanceInfo) {
t.Errorf("getInstanceInfo did not return expected instance info: got %v, want %v", instanceInfo, expectedInstanceInfo)
}
}

func TestGetInstanceInfoReturnsNil(t *testing.T) {
extStruct := gceInstanceInfo{
Zone: "zone",
ProjectID: "project id",
ProjectNumber: 0,
InstanceName: "instance name",
InstanceID: 1,
SecurityProperties: gceSecurityProperties{IsProduction: false},
}

marshaledExt, err := asn1.Marshal(extStruct)
if err != nil {
t.Fatalf("Error marshaling test extension: %v", err)
}

testcases := []struct {
name string
ext []pkix.Extension
}{
{
name: "No extension with expected OID",
ext: []pkix.Extension{{
Id: asn1.ObjectIdentifier([]int{1, 2, 3, 4}),
Value: []byte("fake extension"),
}},
},
{
name: "IsProduction is false",
ext: []pkix.Extension{{
Id: cloudComputeInstanceIdentifierOID,
Value: marshaledExt,
}},
},
}

for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
instanceInfo, err := getInstanceInfo(tc.ext)
if err != nil {
t.Fatalf("getInstanceInfo returned with error: %v", err)
}

if instanceInfo != nil {
t.Error("getInstanceInfo returned instance information, expected nil")
}
})
}
}

func TestGetInstanceInfoError(t *testing.T) {
testcases := []struct {
name string
instanceInfo *gceInstanceInfo
}{
{
name: "Extension value is not valid ASN1",
instanceInfo: nil,
},
{
name: "Negative ProjectNumber",
instanceInfo: &gceInstanceInfo{
Zone: "zone",
ProjectID: "project id",
ProjectNumber: -1,
InstanceName: "instance name",
InstanceID: 1,
SecurityProperties: gceSecurityProperties{IsProduction: false},
},
},
{
name: "Negative InstanceID",
instanceInfo: &gceInstanceInfo{
Zone: "zone",
ProjectID: "project id",
ProjectNumber: 0,
InstanceName: "instance name",
InstanceID: -1,
SecurityProperties: gceSecurityProperties{IsProduction: false},
},
},
{
name: "Negative SecurityVersion",
instanceInfo: &gceInstanceInfo{
Zone: "zone",
ProjectID: "project id",
ProjectNumber: 0,
InstanceName: "instance name",
InstanceID: 1,
SecurityProperties: gceSecurityProperties{
SecurityVersion: -1,
IsProduction: false,
},
},
},
}

for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
var extensionVal []byte
var err error
if tc.instanceInfo != nil {
extensionVal, err = asn1.Marshal(*tc.instanceInfo)
if err != nil {
t.Fatalf("Error marshaling test extension: %v", err)
}
} else {
extensionVal = []byte("Not a valid ASN1 extension.")
}

_, err = getInstanceInfo([]pkix.Extension{{
Id: cloudComputeInstanceIdentifierOID,
Value: extensionVal,
}})

if err == nil {
t.Error("getInstanceInfo returned successfully, expected error")
}
})
}

ext := []pkix.Extension{{
Id: cloudComputeInstanceIdentifierOID,
Value: []byte("not valid ASN1"),
}}

_, err := getInstanceInfo(ext)
if err == nil {
t.Error("getInstanceInfo returned successfully, expected error")
}
}
0