diff --git a/arc/cmd/verify_kat.go b/arc/cmd/verify_kat.go new file mode 100644 index 0000000..2ea2ff7 --- /dev/null +++ b/arc/cmd/verify_kat.go @@ -0,0 +1,246 @@ +// Copyright 2023 Contributors to the Veraison project. +// SPDX-License-Identifier: Apache-2.0 +package cmd + +import ( + "bytes" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "strings" + "time" + + "github.com/hf/nitrite" + "github.com/spf13/afero" + "github.com/spf13/cobra" +) + +var ( + verifyKatInput string + verifyKatAttesterType string + verifyKatRefValues string + verifyKatEndorsements string + verifyKatClockSkew time.Duration +) + +var verifyKatCmd = NewVerifyKatCmd() + +func NewVerifyKatCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "verify-kat [flags] ", + Short: "verify a key attestation of a EAR signing key", + Long: `Verify a key attestation of a EAR signing key using optional +endorsements and reference values. + + The following example verifies the key (and platform) attestation of a + Veraison deployment that runs in a AWS Nitro enclave. This assumes the key + attestation is verified offline at a later point in time. Therefore, a + clock skew of 10 hours is give to adjust the key attestation key validity. + + arc verify-kat \ + --attester aws-nitro \ + --refval data/nitro-ref-values.json \ + --clock-skew -10h \ + data/nitro-key-attestation.cbor + + `, + RunE: func(cmd *cobra.Command, args []string) error { + var ( + katBytes []byte + err error + ) + + if err = checkVerifyKatArgs(args); err != nil { + return fmt.Errorf("validating arguments: %w", err) + } + + verifyKatInput = args[0] + + if katBytes, err = afero.ReadFile(fs, verifyKatInput); err != nil { + return fmt.Errorf("loading key attestation from %q: %w", verifyKatInput, err) + } + + // at this point the verifyKatAttesterType argument has already been + // sanitized by checkVerifyKatArgs + verify := attesterHandler[verifyKatAttesterType] + + return verify(katBytes, verifyKatRefValues, verifyKatClockSkew) + }, + } + + cmd.Flags().StringVarP( + &verifyKatAttesterType, + "attester", + "a", + "", + fmt.Sprintf("attester type, one of: %s", strings.Join(supportedAttesterTypes(), ",")), + ) + _ = cmd.MarkFlagRequired("attester") + + cmd.Flags().StringVarP( + &verifyKatRefValues, + "refval", + "r", + "", + "file containing reference values", + ) + + cmd.Flags().StringVarP( + &verifyKatEndorsements, + "endorsements", + "e", + "", + "file containing endorsements", + ) + + cmd.Flags().DurationVarP( + &verifyKatClockSkew, + "clock-skew", + "c", + 0, + "clock skew expressed as time duration (e.g., 10h, -2h45m)", + ) + + return cmd +} + +type AttesterHandler func(kat []byte, rv string, clockSkew time.Duration) error + +type NitroRefValues struct { + Measurements NitroMeasurements +} + +type NitroMeasurements struct { + HashAlgorithm string + PCR0 HexString + PCR1 HexString + PCR2 HexString + PCR3 HexString + PCR4 HexString + PCR8 HexString +} + +type HexString []byte + +func (o *HexString) UnmarshalJSON(b []byte) error { + var ( + s string + err error + ) + + if err = json.Unmarshal(b, &s); err != nil { + return fmt.Errorf("unmarshaling hex string: %w", err) + } + + if *o, err = hex.DecodeString(s); err != nil { + return fmt.Errorf("decoding hex string: %w", err) + } + + return nil +} + +func nitroLoadRefValues(rv string) (*NitroMeasurements, error) { + var m NitroRefValues + + b, err := afero.ReadFile(fs, rv) + if err != nil { + return nil, fmt.Errorf("reading file: %w", err) + } + + if err = json.Unmarshal(b, &m); err != nil { + return nil, fmt.Errorf("unmarshaling JSON: %w", err) + } + + return &m.Measurements, nil +} + +func NitroHandler(kat []byte, rvFile string, clockSkew time.Duration) error { + var ( + rvs *NitroMeasurements + err error + ) + + if rvFile != "" { + rvs, err = nitroLoadRefValues(rvFile) + if err != nil { + return fmt.Errorf("loading aws-nitro reference values from %q: %w", rvFile, err) + } + } + + t := time.Now().Add(clockSkew) + opts := nitrite.VerifyOptions{CurrentTime: t} + + res, err := nitrite.Verify(kat, opts) + if err != nil { + return fmt.Errorf("verification of aws-nitro attestation document failed: %w", err) + } + + if rvs != nil { + var expected, actual []byte + + for _, i := range []uint{0, 1, 2, 3, 4, 8} { + switch i { + case 0: + expected = rvs.PCR0 + case 1: + expected = rvs.PCR1 + case 2: + expected = rvs.PCR2 + case 3: + expected = rvs.PCR3 + case 4: + expected = rvs.PCR4 + case 8: + expected = rvs.PCR8 + } + + if len(expected) == 0 { + continue + } + + actual = res.Document.PCRs[i] + + if bytes.Equal(expected, actual) { + fmt.Printf("PCR[%d] ok\n", i) + } else { + return fmt.Errorf("PCR[%d] check failed: want %x, got %x", i, expected, actual) + } + } + } + + fmt.Printf(">> Attested public key: %s\n\n", string(res.Document.PublicKey)) + + return nil +} + +var attesterHandler = map[string]AttesterHandler{ + "aws-nitro": NitroHandler, +} + +func supportedAttesterTypes() []string { + a := make([]string, 0, len(attesterHandler)) + + for k := range attesterHandler { + a = append(a, k) + } + + return a +} + +func checkVerifyKatArgs(args []string) error { + if len(args) != 1 { + return errors.New("no KAT file supplied") + } + + _, ok := attesterHandler[verifyKatAttesterType] + if !ok { + return fmt.Errorf("unsupported attester type: %s", verifyKatAttesterType) + } + + return nil +} + +func init() { + rootCmd.AddCommand(verifyKatCmd) +} diff --git a/arc/cmd/verify_kat_test.go b/arc/cmd/verify_kat_test.go new file mode 100644 index 0000000..61f678d --- /dev/null +++ b/arc/cmd/verify_kat_test.go @@ -0,0 +1,118 @@ +// Copyright 2023 Contributors to the Veraison project. +// SPDX-License-Identifier: Apache-2.0 +package cmd + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_VerifyKatCmd_unknown_argument(t *testing.T) { + cmd := NewVerifyKatCmd() + + args := []string{"--unknown-argument=val"} + cmd.SetArgs(args) + + err := cmd.Execute() + assert.EqualError(t, err, "unknown flag: --unknown-argument") +} + +func Test_VerifyKatCmd_no_kat_file(t *testing.T) { + cmd := NewVerifyKatCmd() + + cmd.SetArgs([]string{"-a aws-nitro"}) + + err := cmd.Execute() + assert.EqualError(t, err, "validating arguments: no KAT file supplied") +} + +func Test_VerifyKatCmd_unknown_attester_type(t *testing.T) { + cmd := NewVerifyKatCmd() + + cmd.SetArgs([]string{ + "--attester=xyz", + "kat-file", + }) + + err := cmd.Execute() + assert.EqualError(t, err, "validating arguments: unsupported attester type: xyz") +} + +func Test_VerifyKatCmd_kat_file_not_found(t *testing.T) { + cmd := NewVerifyKatCmd() + + args := []string{ + "--attester=aws-nitro", + "non-existent", + } + cmd.SetArgs(args) + + expectedErr := `loading key attestation from "non-existent": open non-existent: file does not exist` + + err := cmd.Execute() + assert.EqualError(t, err, expectedErr) +} + +func Test_VerifyKatCmd_kat_file_bad_format(t *testing.T) { + cmd := NewVerifyKatCmd() + + files := []fileEntry{ + {"kat.jwt", []byte("")}, + } + makeFS(t, files) + + args := []string{ + "--attester=aws-nitro", + "kat.jwt", + } + cmd.SetArgs(args) + + expectedErr := `verification of aws-nitro attestation document failed: Data is not a COSESign1 array` + + err := cmd.Execute() + assert.EqualError(t, err, expectedErr) +} + +func Test_VerifyKatCmd_refvalue_bad_format(t *testing.T) { + cmd := NewVerifyKatCmd() + + files := []fileEntry{ + {"bad-refval.json", []byte(`{ "Measurements": { "PCR0": "XYZ"} }`)}, + {"kat.jwt", []byte("")}, + } + makeFS(t, files) + + args := []string{ + "--attester=aws-nitro", + "--refval=bad-refval.json", + "kat.jwt", + } + cmd.SetArgs(args) + + expectedErr := `loading aws-nitro reference values from "bad-refval.json": unmarshaling JSON: decoding hex string: encoding/hex: invalid byte: U+0058 'X'` + + err := cmd.Execute() + assert.EqualError(t, err, expectedErr) +} + +func Test_VerifyKatCmd_refvalue_file_not_found(t *testing.T) { + cmd := NewVerifyKatCmd() + + files := []fileEntry{ + {"kat.jwt", []byte("")}, + } + makeFS(t, files) + + args := []string{ + "--attester=aws-nitro", + "--refval=non-existent", + "kat.jwt", + } + cmd.SetArgs(args) + + expectedErr := `loading aws-nitro reference values from "non-existent": reading file: open non-existent: file does not exist` + + err := cmd.Execute() + assert.EqualError(t, err, expectedErr) +} diff --git a/arc/data/nitro-key-attestation.cbor b/arc/data/nitro-key-attestation.cbor new file mode 100644 index 0000000..3630762 Binary files /dev/null and b/arc/data/nitro-key-attestation.cbor differ diff --git a/arc/data/nitro-ref-values.json b/arc/data/nitro-ref-values.json new file mode 100644 index 0000000..881723f --- /dev/null +++ b/arc/data/nitro-ref-values.json @@ -0,0 +1,8 @@ +{ + "Measurements": { + "HashAlgorithm": "Sha384 { ... }", + "PCR0": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "PCR1": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "PCR2": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + } +} diff --git a/go.mod b/go.mod index 858d40f..c6c5947 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/veraison/ear go 1.18 require ( + github.com/hf/nitrite v0.0.0-20211104000856-f9e0dcc73703 github.com/huandu/xstrings v1.3.3 github.com/lestrrat-go/jwx/v2 v2.0.6 github.com/spf13/afero v1.9.2 @@ -15,6 +16,7 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect github.com/fsnotify/fsnotify v1.5.4 // indirect + github.com/fxamacker/cbor/v2 v2.2.0 // indirect github.com/goccy/go-json v0.9.11 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.0.1 // indirect @@ -32,9 +34,10 @@ require ( github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.4.1 // indirect + github.com/x448/float16 v0.8.4 // indirect golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f // indirect - golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect - golang.org/x/text v0.3.7 // indirect + golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect + golang.org/x/text v0.3.8 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 99254ae..260f967 100644 --- a/go.sum +++ b/go.sum @@ -62,6 +62,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7 github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/fxamacker/cbor/v2 v2.2.0 h1:6eXqdDDe588rSYAi1HfZKbx6YYQO4mxQ9eC6xYpU/JQ= +github.com/fxamacker/cbor/v2 v2.2.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -126,6 +128,8 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hf/nitrite v0.0.0-20211104000856-f9e0dcc73703 h1:oTi0zYvHo1sfk5sevGc4LrfgpLYB6cIhP/HllCUGcZ8= +github.com/hf/nitrite v0.0.0-20211104000856-f9e0dcc73703/go.mod h1:ycRhVmo6wegyEl6WN+zXOHUTJvB0J2tiuH88q/McTK8= github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4= github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= @@ -193,6 +197,8 @@ github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PK github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -332,8 +338,8 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -342,8 +348,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=