Refactor enforced/expected PCRs (#553)

* Merge enforced and expected measurements

* Update measurement generation to new format

* Write expected measurements hex encoded by default

* Allow hex or base64 encoded expected measurements

* Allow hex or base64 encoded clusterID

* Allow security upgrades to warnOnly flag

* Upload signed measurements in JSON format

* Fetch measurements either from JSON or YAML

* Use yaml.v3 instead of yaml.v2

* Error on invalid enforced selection

* Add placeholder measurements to config

* Update e2e test to new measurement format

Signed-off-by: Daniel Weiße <dw@edgeless.systems>
This commit is contained in:
Daniel Weiße 2022-11-24 10:57:58 +01:00 committed by GitHub
parent 8ce954e012
commit f8001efbc0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 1180 additions and 801 deletions

View file

@ -24,24 +24,24 @@ import (
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/crypto"
"github.com/edgelesssys/constellation/v2/verify/verifyproto"
"github.com/spf13/afero"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"gopkg.in/yaml.v3"
)
var (
coordIP = flag.String("constell-ip", "", "Public IP of the Constellation")
port = flag.String("constell-port", strconv.Itoa(constants.VerifyServiceNodePortGRPC), "NodePort of the Constellation's verification service")
export = flag.String("o", "", "Write PCRs, formatted as Go code, to file")
format = flag.String("format", "json", "Output format: json, yaml (default json)")
quiet = flag.Bool("q", false, "Set to disable output")
timeout = flag.Duration("timeout", 2*time.Minute, "Wait this duration for the verification service to become available")
)
func main() {
coordIP := flag.String("constell-ip", "", "Public IP of the Constellation")
port := flag.String("constell-port", strconv.Itoa(constants.VerifyServiceNodePortGRPC), "NodePort of the Constellation's verification service")
format := flag.String("format", "json", "Output format: json, yaml (default json)")
quiet := flag.Bool("q", false, "Set to disable output")
timeout := flag.Duration("timeout", 2*time.Minute, "Wait this duration for the verification service to become available")
flag.Parse()
if *coordIP == "" || *port == "" {
flag.Usage()
os.Exit(1)
}
addr := net.JoinHostPort(*coordIP, *port)
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
defer cancel()
@ -51,18 +51,13 @@ func main() {
log.Fatal(err)
}
pcrs, err := validatePCRAttDoc(attDocRaw)
measurements, err := validatePCRAttDoc(attDocRaw)
if err != nil {
log.Fatal(err)
}
if !*quiet {
if err := printPCRs(os.Stdout, pcrs, *format); err != nil {
log.Fatal(err)
}
}
if *export != "" {
if err := exportToFile(*export, pcrs, &afero.Afero{Fs: afero.NewOsFs()}); err != nil {
if err := printPCRs(os.Stdout, measurements, *format); err != nil {
log.Fatal(err)
}
}
@ -104,16 +99,23 @@ func validatePCRAttDoc(attDocRaw []byte) (measurements.M, error) {
if err != nil {
return nil, err
}
m := measurements.M{}
for idx, pcr := range attDoc.Attestation.Quotes[qIdx].Pcrs.Pcrs {
if len(pcr) != 32 {
return nil, fmt.Errorf("incomplete PCR at index: %d", idx)
}
m[idx] = measurements.Measurement{
Expected: *(*[32]byte)(pcr),
WarnOnly: true,
}
}
return attDoc.Attestation.Quotes[qIdx].Pcrs.Pcrs, nil
return m, nil
}
// printPCRs formates and prints PCRs to the given writer.
// format can be one of 'json' or 'yaml'. If it doesnt match defaults to 'json'.
// printPCRs formats and prints PCRs to the given writer.
// format can be one of 'json' or 'yaml'. If it doesn't match defaults to 'json'.
func printPCRs(w io.Writer, pcrs measurements.M, format string) error {
switch format {
case "json":
@ -142,24 +144,3 @@ func printPCRsJSON(w io.Writer, pcrs measurements.M) error {
fmt.Fprintf(w, "%s", 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 measurements.M, 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

@ -8,7 +8,7 @@ package main
import (
"bytes"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"testing"
@ -17,67 +17,13 @@ import (
"github.com/edgelesssys/constellation/v2/internal/attestation/vtpm"
"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"
"go.uber.org/goleak"
)
func TestMain(m *testing.M) {
goleak.VerifyTestMain(m,
// https://github.com/census-instrumentation/opencensus-go/issues/1262
goleak.IgnoreTopFunction("go.opencensus.io/stats/view.(*worker).start"),
)
}
func TestExportToFile(t *testing.T) {
testCases := map[string]struct {
pcrs measurements.M
fs *afero.Afero
wantErr bool
}{
"file not writeable": {
pcrs: measurements.M{
0: {0x1, 0x2, 0x3},
1: {0x1, 0x2, 0x3},
2: {0x1, 0x2, 0x3},
},
fs: &afero.Afero{Fs: afero.NewReadOnlyFs(afero.NewMemMapFs())},
wantErr: true,
},
"file writeable": {
pcrs: measurements.M{
0: {0x1, 0x2, 0x3},
1: {0x1, 0x2, 0x3},
2: {0x1, 0x2, 0x3},
},
fs: &afero.Afero{Fs: afero.NewMemMapFs()},
wantErr: 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.wantErr {
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))
}
}
}
})
}
goleak.VerifyTestMain(m)
}
func TestValidatePCRAttDoc(t *testing.T) {
@ -106,7 +52,7 @@ func TestValidatePCRAttDoc(t *testing.T) {
{
Pcrs: &tpm.PCRs{
Hash: tpm.HashAlgo_SHA256,
Pcrs: measurements.M{
Pcrs: map[uint32][]byte{
0: {0x1, 0x2, 0x3},
},
},
@ -123,8 +69,8 @@ func TestValidatePCRAttDoc(t *testing.T) {
{
Pcrs: &tpm.PCRs{
Hash: tpm.HashAlgo_SHA256,
Pcrs: measurements.M{
0: measurements.PCRWithAllBytes(0xAA),
Pcrs: map[uint32][]byte{
0: bytes.Repeat([]byte{0xAA}, 32),
},
},
},
@ -150,7 +96,10 @@ func TestValidatePCRAttDoc(t *testing.T) {
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)
for pcrIdx, pcrVal := range pcrs {
assert.Equal(pcrVal.Expected[:], attDoc.Attestation.Quotes[qIdx].Pcrs.Pcrs[pcrIdx])
}
}
})
}
@ -164,31 +113,15 @@ func mustMarshalAttDoc(t *testing.T, attDoc vtpm.AttestationDocument) []byte {
func TestPrintPCRs(t *testing.T) {
testCases := map[string]struct {
pcrs measurements.M
format string
}{
"json": {
pcrs: measurements.M{
0: {0x1, 0x2, 0x3},
1: {0x1, 0x2, 0x3},
2: {0x1, 0x2, 0x3},
},
format: "json",
},
"empty format": {
pcrs: measurements.M{
0: {0x1, 0x2, 0x3},
1: {0x1, 0x2, 0x3},
2: {0x1, 0x2, 0x3},
},
format: "",
},
"yaml": {
pcrs: measurements.M{
0: {0x1, 0x2, 0x3},
1: {0x1, 0x2, 0x3},
2: {0x1, 0x2, 0x3},
},
format: "yaml",
},
}
@ -197,13 +130,19 @@ func TestPrintPCRs(t *testing.T) {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
pcrs := measurements.M{
0: measurements.WithAllBytes(0xAA, true),
1: measurements.WithAllBytes(0xBB, true),
2: measurements.WithAllBytes(0xCC, true),
}
var out bytes.Buffer
err := printPCRs(&out, tc.pcrs, tc.format)
err := printPCRs(&out, pcrs, tc.format)
assert.NoError(err)
for idx, pcr := range tc.pcrs {
for idx, pcr := range pcrs {
assert.Contains(out.String(), fmt.Sprintf("%d", idx))
assert.Contains(out.String(), base64.StdEncoding.EncodeToString(pcr))
assert.Contains(out.String(), hex.EncodeToString(pcr.Expected[:]))
}
})
}