AB#1770 (semi)automatic PCR updates (#7)

Signed-off-by: Daniel Weiße <dw@edgeless.systems>
This commit is contained in:
Daniel Weiße 2022-03-23 14:10:58 +01:00 committed by GitHub
parent 752571bbf8
commit 1f843d4593
5 changed files with 544 additions and 0 deletions

1
.gitignore vendored
View File

@ -18,6 +18,7 @@ build
admin.conf admin.conf
coordinatorConfig.json coordinatorConfig.json
coordinator-* coordinator-*
util/pcr-reader/pcrs/
# VS Code configuration folder # VS Code configuration folder
.vscode .vscode

89
util/pcr-reader/README.md Normal file
View File

@ -0,0 +1,89 @@
# PCR-updater
New images result in different PCR values for the image.
This utility program makes it simple to update the expected PCR values of the CLI.
## Usage
### Script
Run `fetch_pcrs.sh` to create Constellations on all supported cloud providers and read their PCR states.
```shell
./fetch_pcrs.sh
```
The result is printed to screen and written as Go code to files in `./pcrs`.
```bash
+ main
+ command -v constellation
+ command -v go
+ mkdir -p ./pcrs
+ constellation create azure 2 Standard_D4s_v3 --name pcr-fetch -y
Your Constellation was created successfully.
++ jq '.azurecoordinators | to_entries[] | select(.key|startswith("")) | .value.PublicIP' -rcM constellation-state.json
+ coord_ip=192.0.2.1
+ go run ../main.go -coord-ip 192.0.2.1 -o ./pcrs/azure_pcrs.go
connecting to Coordinator at 192.0.2.1:9000
PCRs:
{
"0": "q27iAZeXGAiCPdu1bqRA2gAoyMO2KrXWY4YkTCQowc4=",
...
"9": "dEGJtQe3h+SI0z42yO7TklzwPixtM3iMCUeJPGRozvg="
}
+ constellation terminate
Your Constellation was terminated successfully.
+ constellation create gcp 2 n2d-standard-2 --name pcr-fetch -y
Your Constellation was created successfully.
++ jq '.gcpcoordinators | to_entries[] | select(.key|startswith("")) | .value.PublicIP' -rcM constellation-state.json
+ coord_ip=192.0.2.2
+ go run ../main.go -coord-ip 192.0.2.2 -o ./pcrs/gcp_pcrs.go
connecting to Coordinator at 192.0.2.2:9000
PCRs:
{
"0": "DzXCFGCNk8em5ornNZtKi+Wg6Z7qkQfs5CfE3qTkOc8=",
...
"9": "gse53SjsqREEdOpImJH4KAb0b8PqIgwI+Ps/XSiFnN4="
}
+ constellation terminate
Your Constellation was terminated successfully.
```
### Manual
To read the PCR state of any running Constellation node, run the following:
```shell
go run main.go -coord-ip <NODE_IP> -coord-port <COORDINATOR_PORT>
```
The output is similar to the following:
```shell
$ go run main.go -coord-ip 192.0.2.3 -coord-port 12345
connecting to Coordinator at 192.0.2.3:12345
PCRs:
{
"0": "DzXCFGCNk8em5ornNZtKi+Wg6Z7qkQfs5CfE3qTkOc8=",
"1": "XBoRlWuQx6nIDr5vgUL0DlJHy6H6u1dPU3qK2NyToc8=",
"10": "WLmYFRmDft/ajZJ056CAhpheU6Vbt73aR8eIQpLRGq0=",
"11": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
"12": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
"13": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
"14": "4tPyJd6A5g09KduV3+nWZQCiEzHAiRT5DulmAqlvpZU=",
"15": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
"16": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
"17": "//////////////////////////////////////////8=",
"18": "//////////////////////////////////////////8=",
"19": "//////////////////////////////////////////8=",
"2": "PUWM/lXMA+ofRD8VYr7sjfUcdeFKn8+acjShPxmOeWk=",
"20": "//////////////////////////////////////////8=",
"21": "//////////////////////////////////////////8=",
"22": "//////////////////////////////////////////8=",
"23": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
"3": "PUWM/lXMA+ofRD8VYr7sjfUcdeFKn8+acjShPxmOeWk=",
"4": "MmkueFj1rP2seH+bjeIRsO4dUnLnMdl7QgtGoAtQH7M=",
"5": "ExaiapuIfo0KMBo8wj6kPDORLocgnH1C0G/KY8DcV3A=",
"6": "PUWM/lXMA+ofRD8VYr7sjfUcdeFKn8+acjShPxmOeWk=",
"7": "UZcW+fhFRMpFkgU+EfKG2s3KdmgEA+TD2quLmthQHbo=",
"8": "KLSMootYaHBjysWKq9CAYXkXpeYx9PUBimlSEZGJqUM=",
"9": "gse53SjsqREEdOpImJH4KAb0b8PqIgwI+Ps/XSiFnN4="
}
```

41
util/pcr-reader/fetch_pcrs.sh Executable file
View File

@ -0,0 +1,41 @@
#!/bin/bash
set -o xtrace
trap 'terminate $?' ERR
terminate() {
echo "error: $1"
constellation terminate
popd || exit 1
exit 1
}
main() {
if ! command -v constellation &> /dev/null
then
echo "constellation is not in path"
exit 1
fi
if ! command -v go &> /dev/null
then
echo "go is not in path"
exit 1
fi
mkdir -p ./pcrs
# Fetch Azure PCRs
# TODO: Switch to confidential VMs
constellation create azure 2 Standard_D4s_v3 --name pcr-fetch -y
coord_ip=$(jq '.azurecoordinators | to_entries[] | select(.key|startswith("")) | .value.PublicIP' -rcM constellation-state.json)
go run main.go -coord-ip "${coord_ip}" -o ./pcrs/azure_pcrs.go
constellation terminate
# Fetch GCP PCRs
constellation create gcp 2 n2d-standard-2 --name pcr-fetch -y
coord_ip=$(jq '.gcpcoordinators | to_entries[] | select(.key|startswith("")) | .value.PublicIP' -rcM constellation-state.json)
go run main.go -coord-ip "${coord_ip}" -o ./pcrs/gcp_pcrs.go
constellation terminate
}
main

167
util/pcr-reader/main.go Normal file
View File

@ -0,0 +1,167 @@
package main
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"log"
"net"
"os"
"time"
"github.com/edgelesssys/constellation/cli/status"
"github.com/edgelesssys/constellation/coordinator/atls"
"github.com/edgelesssys/constellation/coordinator/attestation/vtpm"
"github.com/edgelesssys/constellation/coordinator/oid"
"github.com/edgelesssys/constellation/coordinator/pubapi/pubproto"
coordinatorstate "github.com/edgelesssys/constellation/coordinator/state"
"github.com/spf13/afero"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
var (
coordIP = flag.String("coord-ip", "", "IP of the VM the Coordinator is running on")
coordinatorPort = flag.String("coord-port", "9000", "Port of the Coordinator's pub API")
export = flag.String("o", "", "Write PCRs, formatted as Go code, to file")
quiet = flag.Bool("q", false, "Set to disable output")
)
func main() {
flag.Parse()
fmt.Printf("connecting to Coordinator at %s:%s\n", *coordIP, *coordinatorPort)
addr := net.JoinHostPort(*coordIP, *coordinatorPort)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
// wait for coordinator to come online
waiter := status.NewWaiter(map[uint32][]byte{})
if err := waiter.WaitFor(ctx, coordinatorstate.AcceptingInit, addr); err != nil {
log.Fatal(err)
}
attDocRaw := &[]byte{}
tlsConfig, err := atls.CreateUnverifiedClientTLSConfig()
if err != nil {
log.Fatal(err)
}
tlsConfig.VerifyPeerCertificate = getVerifyPeerCertificateFunc(attDocRaw)
if err := connectToCoordinator(ctx, addr, tlsConfig); err != nil {
log.Fatal(err)
}
pcrs, err := validatePCRAttDoc(*attDocRaw)
if err != nil {
log.Fatal(err)
}
if !*quiet {
if err := printPCRs(os.Stdout, pcrs); err != nil {
log.Fatal(err)
}
}
if *export != "" {
if err := exportToFile(*export, pcrs, &afero.Afero{Fs: afero.NewOsFs()}); err != nil {
log.Fatal(err)
}
}
}
// connectToCoordinator connects to the Constellation Coordinator and returns its attestation document.
func connectToCoordinator(ctx context.Context, addr string, tlsConfig *tls.Config) error {
conn, err := grpc.DialContext(
ctx, addr, grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)),
)
if err != nil {
return err
}
defer conn.Close()
client := pubproto.NewAPIClient(conn)
_, err = client.GetState(ctx, &pubproto.GetStateRequest{})
return err
}
// getVerifyPeerCertificateFunc returns a VerifyPeerCertificate function, which writes the attestation document extension to the given byte slice pointer.
func getVerifyPeerCertificateFunc(attDoc *[]byte) func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
return func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
if len(rawCerts) == 0 {
return errors.New("rawCerts is empty")
}
cert, err := x509.ParseCertificate(rawCerts[0])
if err != nil {
return err
}
for _, ex := range cert.Extensions {
if ex.Id.Equal(oid.Azure{}.OID()) || ex.Id.Equal(oid.GCP{}.OID()) || ex.Id.Equal(oid.GCPNonCVM{}.OID()) {
if err := json.Unmarshal(ex.Value, attDoc); err != nil {
*attDoc = ex.Value
}
}
}
if len(*attDoc) == 0 {
return errors.New("did not receive attestation document in certificate extension")
}
return nil
}
}
// validatePCRAttDoc parses and validates PCRs of an attestation document.
func validatePCRAttDoc(attDocRaw []byte) (map[uint32][]byte, error) {
attDoc := vtpm.AttestationDocument{}
if err := json.Unmarshal(attDocRaw, &attDoc); err != nil {
return nil, err
}
if attDoc.Attestation == nil {
return nil, errors.New("empty attestation")
}
qIdx, err := vtpm.GetSHA256QuoteIndex(attDoc.Attestation.Quotes)
if err != nil {
return nil, err
}
for idx, pcr := range attDoc.Attestation.Quotes[qIdx].Pcrs.Pcrs {
if len(pcr) != 32 {
return nil, fmt.Errorf("incomplete PCR at index: %d", idx)
}
}
return attDoc.Attestation.Quotes[qIdx].Pcrs.Pcrs, nil
}
// printPCRs formates and prints PCRs to the given writer.
func printPCRs(w io.Writer, pcrs map[uint32][]byte) error {
pcrJSON, err := json.MarshalIndent(pcrs, "", " ")
if err != nil {
return err
}
fmt.Fprintf(w, "PCRs:\n%s\n", string(pcrJSON))
return nil
}
// exportToFile writes pcrs to a file, formatted to be valid Go code.
// Validity of the PCR map is not checked, and should be handled by the caller.
func exportToFile(path string, pcrs map[uint32][]byte, fs *afero.Afero) error {
goCode := `package pcrs
var pcrs = map[uint32][]byte{%s
}
`
pcrsFormatted := ""
for i := 0; i < len(pcrs); i++ {
pcrHex := fmt.Sprintf("%#02X", pcrs[uint32(i)][0])
for j := 1; j < len(pcrs[uint32(i)]); j++ {
pcrHex = fmt.Sprintf("%s, %#02X", pcrHex, pcrs[uint32(i)][j])
}
pcrsFormatted = pcrsFormatted + fmt.Sprintf("\n\t%d: {%s},", i, pcrHex)
}
return fs.WriteFile(path, []byte(fmt.Sprintf(goCode, pcrsFormatted)), 0o644)
}

View File

@ -0,0 +1,246 @@
package main
import (
"bytes"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
"encoding/json"
"fmt"
"math/big"
"testing"
"github.com/edgelesssys/constellation/coordinator/attestation/vtpm"
"github.com/edgelesssys/constellation/coordinator/oid"
"github.com/google/go-tpm-tools/proto/attest"
"github.com/google/go-tpm-tools/proto/tpm"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGetVerifyPeerCertificateFunc(t *testing.T) {
testCases := map[string]struct {
rawCerts [][]byte
errExpected bool
}{
"no certificates": {
rawCerts: nil,
errExpected: true,
},
"invalid certificate": {
rawCerts: [][]byte{
{0x1, 0x2, 0x3},
},
errExpected: true,
},
"no extension": {
rawCerts: [][]byte{
mustGenerateTestCert(t, &x509.Certificate{
SerialNumber: big.NewInt(123),
}),
},
errExpected: true,
},
"certificate with attestation": {
rawCerts: [][]byte{
mustGenerateTestCert(t, &x509.Certificate{
SerialNumber: big.NewInt(123),
ExtraExtensions: []pkix.Extension{
{
Id: oid.GCP{}.OID(),
Value: []byte{0x1, 0x2, 0x3},
Critical: true,
},
},
}),
},
errExpected: false,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
attDoc := &[]byte{}
verify := getVerifyPeerCertificateFunc(attDoc)
err := verify(tc.rawCerts, nil)
if tc.errExpected {
assert.Error(err)
} else {
require.NoError(err)
assert.NotNil(attDoc)
cert, err := x509.ParseCertificate(tc.rawCerts[0])
require.NoError(err)
assert.Equal(cert.Extensions[0].Value, *attDoc)
}
})
}
}
func mustGenerateTestCert(t *testing.T, template *x509.Certificate) []byte {
require := require.New(t)
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(err)
cert, err := x509.CreateCertificate(rand.Reader, template, template, priv.Public(), priv)
require.NoError(err)
return cert
}
func TestExportToFile(t *testing.T) {
testCases := map[string]struct {
pcrs map[uint32][]byte
fs *afero.Afero
errExpected bool
}{
"file not writeable": {
pcrs: map[uint32][]byte{
0: {0x1, 0x2, 0x3},
1: {0x1, 0x2, 0x3},
2: {0x1, 0x2, 0x3},
},
fs: &afero.Afero{Fs: afero.NewReadOnlyFs(afero.NewMemMapFs())},
errExpected: true,
},
"file writeable": {
pcrs: map[uint32][]byte{
0: {0x1, 0x2, 0x3},
1: {0x1, 0x2, 0x3},
2: {0x1, 0x2, 0x3},
},
fs: &afero.Afero{Fs: afero.NewMemMapFs()},
errExpected: false,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
path := "test-file"
err := exportToFile(path, tc.pcrs, tc.fs)
if tc.errExpected {
assert.Error(err)
} else {
assert.NoError(err)
content, err := tc.fs.ReadFile(path)
require.NoError(err)
for _, pcr := range tc.pcrs {
for _, register := range pcr {
assert.Contains(string(content), fmt.Sprintf("%#02X", register))
}
}
}
})
}
}
func TestValidatePCRAttDoc(t *testing.T) {
testCases := map[string]struct {
attDocRaw []byte
errExpected bool
}{
"invalid attestation document": {
attDocRaw: []byte{0x1, 0x2, 0x3},
errExpected: true,
},
"nil attestation": {
attDocRaw: mustMarshalAttDoc(t, vtpm.AttestationDocument{}),
errExpected: true,
},
"nil quotes": {
attDocRaw: mustMarshalAttDoc(t, vtpm.AttestationDocument{
Attestation: &attest.Attestation{},
}),
errExpected: true,
},
"invalid PCRs": {
attDocRaw: mustMarshalAttDoc(t, vtpm.AttestationDocument{
Attestation: &attest.Attestation{
Quotes: []*tpm.Quote{
{
Pcrs: &tpm.PCRs{
Hash: tpm.HashAlgo_SHA256,
Pcrs: map[uint32][]byte{
0: {0x1, 0x2, 0x3},
},
},
},
},
},
}),
errExpected: true,
},
"valid PCRs": {
attDocRaw: mustMarshalAttDoc(t, vtpm.AttestationDocument{
Attestation: &attest.Attestation{
Quotes: []*tpm.Quote{
{
Pcrs: &tpm.PCRs{
Hash: tpm.HashAlgo_SHA256,
Pcrs: map[uint32][]byte{
0: []byte("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"),
},
},
},
},
},
}),
errExpected: false,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
pcrs, err := validatePCRAttDoc(tc.attDocRaw)
if tc.errExpected {
assert.Error(err)
} else {
require.NoError(err)
attDoc := vtpm.AttestationDocument{}
require.NoError(json.Unmarshal(tc.attDocRaw, &attDoc))
qIdx, err := vtpm.GetSHA256QuoteIndex(attDoc.Attestation.Quotes)
require.NoError(err)
assert.EqualValues(attDoc.Attestation.Quotes[qIdx].Pcrs.Pcrs, pcrs)
}
})
}
}
func mustMarshalAttDoc(t *testing.T, attDoc vtpm.AttestationDocument) []byte {
attDocRaw, err := json.Marshal(attDoc)
require.NoError(t, err)
return attDocRaw
}
func TestPrintPCRs(t *testing.T) {
assert := assert.New(t)
pcrs := map[uint32][]byte{
0: {0x1, 0x2, 0x3},
1: {0x1, 0x2, 0x3},
2: {0x1, 0x2, 0x3},
}
var out bytes.Buffer
err := printPCRs(&out, pcrs)
assert.NoError(err)
for idx, pcr := range pcrs {
assert.Contains(out.String(), fmt.Sprintf("\"%d\": ", idx))
assert.Contains(out.String(), fmt.Sprintf(": \"%s\"", base64.StdEncoding.EncodeToString(pcr)))
}
}