mirror of
https://github.com/edgelesssys/constellation.git
synced 2024-10-01 01:36:09 -04:00
AB#2333: Add AMD SNP-based attestation
Currently only available on Azure CVMs. * Get the public attestation key from the TPM. * Get the snp report from the TPM. * Get the VCEK and ASK certificate from the metadata api. * Verify VCEK using hardcoded root key (ARK) * Verify SNP report using VCEK * Verify HCLAkPub using SNP report by comparing AK with runtimeData * Extend unittest Co-authored-by: Thomas Tendyck <51411342+thomasten@users.noreply.github.com> Co-authored-by: Daniel Weiße <dw@edgeless.systems>
This commit is contained in:
parent
0a3a4e9c7f
commit
7c5556864b
@ -34,6 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
<!-- For changes in existing functionality. -->
|
||||
- Use IP from Constellation ID file in init and verify instead of IPs from state file.
|
||||
- Change cdbg to use load balancer for deploy.
|
||||
- Azure CVMs are attested using SNP attestation
|
||||
|
||||
### Deprecated
|
||||
<!-- For soon-to-be removed features. -->
|
||||
|
@ -489,8 +489,7 @@ func (k *KubernetesUtil) createSignedKubeletCert(nodeName string, ips []net.IP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
parentCertPEM, _ := pem.Decode(parentCertRaw)
|
||||
parentCert, err := x509.ParseCertificate(parentCertPEM.Bytes)
|
||||
parentCert, err := crypto.PemToX509Cert(parentCertRaw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -513,7 +512,7 @@ func (k *KubernetesUtil) createSignedKubeletCert(nodeName string, ips []net.IP)
|
||||
case "PRIVATE KEY":
|
||||
parentKey, err = x509.ParsePKCS8PrivateKey(parentKeyPEM.Bytes)
|
||||
default:
|
||||
err = fmt.Errorf("unsupported key type %q", parentCertPEM.Type)
|
||||
err = fmt.Errorf("unsupported key type %q", parentKeyPEM.Type)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
|
43
internal/attestation/azure/imds.go
Normal file
43
internal/attestation/azure/imds.go
Normal file
@ -0,0 +1,43 @@
|
||||
package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Modified version of bootstrapper/cloudprovider/azure/imds.go
|
||||
|
||||
const (
|
||||
imdsVcekURL = "http://169.254.169.254/metadata/THIM/amd/certification"
|
||||
)
|
||||
|
||||
type imdsClient struct {
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// Retrieve retrieves instance metadata from the azure imds API.
|
||||
func (c imdsClient) getVcek(ctx context.Context) (vcekResponse, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, imdsVcekURL, http.NoBody)
|
||||
if err != nil {
|
||||
return vcekResponse{}, err
|
||||
}
|
||||
req.Header.Add("Metadata", "True")
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return vcekResponse{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var res vcekResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
|
||||
return vcekResponse{}, err
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
type vcekResponse struct {
|
||||
VcekCert string
|
||||
CertificateChain string
|
||||
}
|
@ -1,11 +1,23 @@
|
||||
package azure
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/edgelesssys/constellation/internal/attestation/vtpm"
|
||||
"github.com/edgelesssys/constellation/internal/oid"
|
||||
tpmclient "github.com/google/go-tpm-tools/client"
|
||||
"github.com/google/go-tpm/tpm2"
|
||||
)
|
||||
|
||||
const (
|
||||
lenHclHeader = 0x20
|
||||
lenSnpReport = 0x4a0
|
||||
lenSnpReportRuntimeDataPadding = 0x14
|
||||
)
|
||||
|
||||
// Issuer for Azure TPM attestation.
|
||||
@ -16,19 +28,98 @@ type Issuer struct {
|
||||
|
||||
// NewIssuer initializes a new Azure Issuer.
|
||||
func NewIssuer() *Issuer {
|
||||
imdsAPI := imdsClient{
|
||||
client: &http.Client{Transport: &http.Transport{Proxy: nil}},
|
||||
}
|
||||
|
||||
return &Issuer{
|
||||
Issuer: vtpm.NewIssuer(
|
||||
vtpm.OpenVTPM,
|
||||
tpmclient.AttestationKeyRSA,
|
||||
getSNPAttestation,
|
||||
getHCLAttestationKey,
|
||||
getSNPAttestation(&tpmReport{}, imdsAPI),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// getSNPAttestation loads and returns the SEV-SNP attestation statement.
|
||||
//
|
||||
// As long as we are using regular VMs on Azure this is a stub, returning nil.
|
||||
func getSNPAttestation(tpm io.ReadWriteCloser) ([]byte, error) {
|
||||
// TODO: implement this for CVMs
|
||||
return nil, nil
|
||||
func hclAkTemplate() tpm2.Public {
|
||||
akFlags := tpm2.FlagFixedTPM | tpm2.FlagFixedParent | tpm2.FlagSensitiveDataOrigin | tpm2.FlagUserWithAuth | tpm2.FlagNoDA | tpm2.FlagRestricted | tpm2.FlagSign
|
||||
return tpm2.Public{
|
||||
Type: tpm2.AlgRSA,
|
||||
NameAlg: tpm2.AlgSHA256,
|
||||
Attributes: akFlags,
|
||||
RSAParameters: &tpm2.RSAParams{
|
||||
Sign: &tpm2.SigScheme{
|
||||
Alg: tpm2.AlgRSASSA,
|
||||
Hash: tpm2.AlgSHA256,
|
||||
},
|
||||
KeyBits: 2048,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// getHCLAttestationKey reads the attesation key put into the TPM during early boot.
|
||||
func getHCLAttestationKey(tpm io.ReadWriter) (*tpmclient.Key, error) {
|
||||
// A minor drawback of `NewCachedKey` is that it will transparently create/overwrite a key if it does not find one matching the template at the given index.
|
||||
// We actually wouldn't want to continue at this point if we realize that the key at the index is not present, due to
|
||||
// easier debuggability. If `NewCachedKey` creates a new key, attestation will fail at the validator.
|
||||
// The function in tpmclient that doesn't create a new key, ReadPublic, can't be used as we would have to create
|
||||
// a tpmclient.Key object manually, which we can't since there is no constructor exported.
|
||||
ak, err := tpmclient.NewCachedKey(tpm, tpm2.HandleOwner, hclAkTemplate(), 0x81000003)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading HCL attestation key from TPM: %w", err)
|
||||
}
|
||||
|
||||
return ak, nil
|
||||
}
|
||||
|
||||
// getSNPAttestation loads and returns the SEV-SNP attestation report [1] and the
|
||||
// AMD VCEK certificate chain.
|
||||
// The attestation report is loaded from the TPM, the certificate chain is queried
|
||||
// from the cloud metadata API.
|
||||
// [1] https://github.com/AMDESE/sev-guest/blob/main/include/attestation.h
|
||||
func getSNPAttestation(reportGetter tpmReportGetter, imdsAPI imdsApi) func(tpm io.ReadWriteCloser) ([]byte, error) {
|
||||
return func(tpm io.ReadWriteCloser) ([]byte, error) {
|
||||
hclReport, err := reportGetter.get(tpm)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading report from TPM: %w", err)
|
||||
}
|
||||
if len(hclReport) < lenHclHeader+lenSnpReport+lenSnpReportRuntimeDataPadding {
|
||||
return nil, fmt.Errorf("report read from TPM is shorter then expected: %x", hclReport)
|
||||
}
|
||||
hclReport = hclReport[lenHclHeader:]
|
||||
|
||||
runtimeData, _, _ := bytes.Cut(hclReport[lenSnpReport+lenSnpReportRuntimeDataPadding:], []byte{0})
|
||||
|
||||
vcekResponse, err := imdsAPI.getVcek(context.TODO())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getVcekFromIMDS: %w", err)
|
||||
}
|
||||
|
||||
instanceInfo := azureInstanceInfo{
|
||||
Vcek: []byte(vcekResponse.VcekCert),
|
||||
CertChain: []byte(vcekResponse.CertificateChain),
|
||||
AttestationReport: hclReport[:0x4a0],
|
||||
RuntimeData: runtimeData,
|
||||
}
|
||||
statement, err := json.Marshal(instanceInfo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshalling AzureInstanceInfo: %w", err)
|
||||
}
|
||||
|
||||
return statement, nil
|
||||
}
|
||||
}
|
||||
|
||||
type tpmReport struct{}
|
||||
|
||||
func (s *tpmReport) get(tpm io.ReadWriteCloser) ([]byte, error) {
|
||||
return tpm2.NVReadEx(tpm, 0x01400001, tpm2.HandleOwner, "", 0)
|
||||
}
|
||||
|
||||
type tpmReportGetter interface {
|
||||
get(tpm io.ReadWriteCloser) ([]byte, error)
|
||||
}
|
||||
|
||||
type imdsApi interface {
|
||||
getVcek(ctx context.Context) (vcekResponse, error)
|
||||
}
|
||||
|
@ -1,22 +1,40 @@
|
||||
package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/edgelesssys/constellation/internal/attestation/simulator"
|
||||
"github.com/edgelesssys/constellation/internal/attestation/vtpm"
|
||||
"github.com/google/go-tpm-tools/simulator"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetSNPAttestation(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
tpmFunc vtpm.TPMOpenFunc
|
||||
wantErr bool
|
||||
report string
|
||||
vcek string
|
||||
certChain string
|
||||
apiError error
|
||||
wantErr bool
|
||||
}{
|
||||
"success": {
|
||||
tpmFunc: simulator.OpenSimulatedTPM,
|
||||
wantErr: false,
|
||||
report: "48434c41010000001c070000020000000000000000000000000000000000000002000000020000001f0003000000000001000000000000000000000000000000020000000000000000000000000000000000000001000000020000000000065d010000000000000000000000000000000ccc0895ef2f2c3b8c8568f5a2bb65ff5bf9387a09359742ad41e686cacfd38b00000000000000000000000000000000000000000000000000000000000000005677f1de87289e7ad2c7e99c805d0468b1a9ccd83f0d245afa5242d405da4d5725852f8c6550564870e5f3206dfb1841000000000000000000000000000000000000000000000000000000000000000057e229e0ffe5fa92d0faddff6cae0e61c926fc9ef9afd20a8b8cfcf7129db9338cbe5bf3f6987733a2bf65d06dc38fc100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005f7240b24a1babe2ece844c4f792bcd9844bf6907d14aeea00156310b9538daffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff020000000000065d0000000000000000000000000000000000000000000000009e44aaef02cfca6fddbaca669c6cfd29e1ab8d97ebc939857128acbb13b8740df31436d34e86e5f8ae0cdfeb3a0e185db46decac176cc77d761c22a1b9dcf25b020000000000065d0133010001330100020000000000065d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000bcb7dc15abff884802e774b39adba8e6ff7efcf05e115c91588e657065151056a320f70c788d0e3619391052922e422b000000000000000000000000000000000000000000000000e8dbf581140443bbc681c50eca8639a76ef6cab34e0780cbca977e2e2a03f8b864fd4e9774b0f8055511567e031e59bf00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005c020000010000000200000001000000480200007b226b657973223a5b7b226b6964223a2248434c416b507562222c226b65795f6f7073223a5b22656e6372797074225d2c226b7479223a22525341222c2265223a2241514142222c226e223a22747946717641414166324746656c6b5737566352684a6e4132597659364c6a427a65554e3276614d5a6e5a74685f74466e574d6b4b35415874757379434e656c337569703356475a7a54617a3558327447566a4772732d4d56486361703951647771555856573367394f515f74456269786378372d78626c554a516b474551666e626253646e5049326c764c7a4f73315a5f30766a65444178765351726d616773366e592d634a4157482d706744564a79487470735553735f5142576b6c617a44736f3557486d6e4d743973394d75696c57586f7830525379586e55656151796859316a753752545363526e5658754e7936377a5f454a6e774d393264727746623841556430534a5f396f687645596c34615a52444543476f3056726a635348552d4a474a6575574335566844425235454f6f4356424267716539653833765f6c4a784933574c65326f7653495a49497a416d625351227d5d2c22766d2d636f6e66696775726174696f6e223a7b22636f6e736f6c652d656e61626c6564223a747275652c2263757272656e742d74696d65223a313636313435353339312c227365637572652d626f6f74223a66616c73652c2274706d2d656e61626c6564223a747275652c22766d556e697175654964223a2242364339384333422d344543372d344441362d424432462d374439384432304437423735227d7d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
|
||||
vcek: "someVCEKCert",
|
||||
certChain: "ASKARK",
|
||||
wantErr: false,
|
||||
},
|
||||
"report too short": {
|
||||
report: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||
wantErr: true,
|
||||
},
|
||||
"vcek api fails": {
|
||||
report: "48434c41010000001c070000020000000000000000000000000000000000000002000000020000001f0003000000000001000000000000000000000000000000020000000000000000000000000000000000000001000000020000000000065d010000000000000000000000000000000ccc0895ef2f2c3b8c8568f5a2bb65ff5bf9387a09359742ad41e686cacfd38b00000000000000000000000000000000000000000000000000000000000000005677f1de87289e7ad2c7e99c805d0468b1a9ccd83f0d245afa5242d405da4d5725852f8c6550564870e5f3206dfb1841000000000000000000000000000000000000000000000000000000000000000057e229e0ffe5fa92d0faddff6cae0e61c926fc9ef9afd20a8b8cfcf7129db9338cbe5bf3f6987733a2bf65d06dc38fc100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005f7240b24a1babe2ece844c4f792bcd9844bf6907d14aeea00156310b9538daffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff020000000000065d0000000000000000000000000000000000000000000000009e44aaef02cfca6fddbaca669c6cfd29e1ab8d97ebc939857128acbb13b8740df31436d34e86e5f8ae0cdfeb3a0e185db46decac176cc77d761c22a1b9dcf25b020000000000065d0133010001330100020000000000065d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000bcb7dc15abff884802e774b39adba8e6ff7efcf05e115c91588e657065151056a320f70c788d0e3619391052922e422b000000000000000000000000000000000000000000000000e8dbf581140443bbc681c50eca8639a76ef6cab34e0780cbca977e2e2a03f8b864fd4e9774b0f8055511567e031e59bf00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005c020000010000000200000001000000480200007b226b657973223a5b7b226b6964223a2248434c416b507562222c226b65795f6f7073223a5b22656e6372797074225d2c226b7479223a22525341222c2265223a2241514142222c226e223a22747946717641414166324746656c6b5737566352684a6e4132597659364c6a427a65554e3276614d5a6e5a74685f74466e574d6b4b35415874757379434e656c337569703356475a7a54617a3558327447566a4772732d4d56486361703951647771555856573367394f515f74456269786378372d78626c554a516b474551666e626253646e5049326c764c7a4f73315a5f30766a65444178765351726d616773366e592d634a4157482d706744564a79487470735553735f5142576b6c617a44736f3557486d6e4d743973394d75696c57586f7830525379586e55656151796859316a753752545363526e5658754e7936377a5f454a6e774d393264727746623841556430534a5f396f687645596c34615a52444543476f3056726a635348552d4a474a6575574335566844425235454f6f4356424267716539653833765f6c4a784933574c65326f7653495a49497a416d625351227d5d2c22766d2d636f6e66696775726174696f6e223a7b22636f6e736f6c652d656e61626c6564223a747275652c2263757272656e742d74696d65223a313636313435353339312c227365637572652d626f6f74223a66616c73652c2274706d2d656e61626c6564223a747275652c22766d556e697175654964223a2242364339384333422d344543372d344441362d424432462d374439384432304437423735227d7d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
|
||||
apiError: errors.New(""),
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
@ -25,16 +43,82 @@ func TestGetSNPAttestation(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
tpm, err := tc.tpmFunc()
|
||||
tpm, err := simulator.Get()
|
||||
require.NoError(err)
|
||||
defer tpm.Close()
|
||||
|
||||
_, err = getSNPAttestation(tpm)
|
||||
imdsClient := stubImdsClient{
|
||||
vcek: tc.vcek,
|
||||
certChain: tc.certChain,
|
||||
apiError: tc.apiError,
|
||||
}
|
||||
reportDecoded, err := hex.DecodeString(tc.report)
|
||||
assert.NoError(err)
|
||||
snpAttestationReport := stubTpmReport{
|
||||
tpmContent: reportDecoded,
|
||||
err: nil,
|
||||
}
|
||||
attestationJson, err := getSNPAttestation(&snpAttestationReport, imdsClient)(tpm)
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
assert.NoError(err)
|
||||
|
||||
var instanceInfo azureInstanceInfo
|
||||
err = json.Unmarshal(attestationJson, &instanceInfo)
|
||||
assert.NoError(err)
|
||||
|
||||
if tc.wantErr {
|
||||
assert.NotEqualValues(tc.report[0x20:], instanceInfo.AttestationReport)
|
||||
} else {
|
||||
assert.NoError(err)
|
||||
assert.EqualValues(reportDecoded[0x20:0x4a0+0x20], instanceInfo.AttestationReport)
|
||||
assert.Equal(tc.vcek, string(instanceInfo.Vcek))
|
||||
assert.Equal(tc.certChain, string(instanceInfo.CertChain))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetHCLAttestationKey is a basic smoke test that only checks if getAkPub can be run error free.
|
||||
// Testing anything else will only verify that the simulator works as expected, since getAkPub
|
||||
// only retrieves the attestation key from the TPM.
|
||||
func TestGetHCLAttestationKey(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
tpm, err := simulator.Get()
|
||||
assert.NoError(err)
|
||||
defer tpm.Close()
|
||||
|
||||
_, err = getHCLAttestationKey(tpm)
|
||||
assert.NoError(err)
|
||||
}
|
||||
|
||||
type stubImdsClient struct {
|
||||
vcek string
|
||||
certChain string
|
||||
apiError error
|
||||
}
|
||||
|
||||
func (c stubImdsClient) getVcek(ctx context.Context) (vcekResponse, error) {
|
||||
if c.apiError != nil {
|
||||
return vcekResponse{}, c.apiError
|
||||
}
|
||||
|
||||
return vcekResponse{
|
||||
VcekCert: c.vcek,
|
||||
CertificateChain: c.certChain,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type stubTpmReport struct {
|
||||
tpmContent []byte
|
||||
err error
|
||||
}
|
||||
|
||||
func (s *stubTpmReport) get(tpm io.ReadWriteCloser) ([]byte, error) {
|
||||
if s.err != nil {
|
||||
return nil, s.err
|
||||
}
|
||||
return s.tpmContent, nil
|
||||
}
|
||||
|
@ -1,13 +1,27 @@
|
||||
package azure
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/asn1"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
"github.com/edgelesssys/constellation/internal/attestation/vtpm"
|
||||
internalCrypto "github.com/edgelesssys/constellation/internal/crypto"
|
||||
"github.com/edgelesssys/constellation/internal/oid"
|
||||
"github.com/google/go-tpm/tpm2"
|
||||
)
|
||||
|
||||
// AMD root key. Received from the AMD Key Distribution System API (KDS).
|
||||
const arkPEM = "-----BEGIN CERTIFICATE-----\nMIIGYzCCBBKgAwIBAgIDAQAAMEYGCSqGSIb3DQEBCjA5oA8wDQYJYIZIAWUDBAIC\nBQChHDAaBgkqhkiG9w0BAQgwDQYJYIZIAWUDBAICBQCiAwIBMKMDAgEBMHsxFDAS\nBgNVBAsMC0VuZ2luZWVyaW5nMQswCQYDVQQGEwJVUzEUMBIGA1UEBwwLU2FudGEg\nQ2xhcmExCzAJBgNVBAgMAkNBMR8wHQYDVQQKDBZBZHZhbmNlZCBNaWNybyBEZXZp\nY2VzMRIwEAYDVQQDDAlBUkstTWlsYW4wHhcNMjAxMDIyMTcyMzA1WhcNNDUxMDIy\nMTcyMzA1WjB7MRQwEgYDVQQLDAtFbmdpbmVlcmluZzELMAkGA1UEBhMCVVMxFDAS\nBgNVBAcMC1NhbnRhIENsYXJhMQswCQYDVQQIDAJDQTEfMB0GA1UECgwWQWR2YW5j\nZWQgTWljcm8gRGV2aWNlczESMBAGA1UEAwwJQVJLLU1pbGFuMIICIjANBgkqhkiG\n9w0BAQEFAAOCAg8AMIICCgKCAgEA0Ld52RJOdeiJlqK2JdsVmD7FktuotWwX1fNg\nW41XY9Xz1HEhSUmhLz9Cu9DHRlvgJSNxbeYYsnJfvyjx1MfU0V5tkKiU1EesNFta\n1kTA0szNisdYc9isqk7mXT5+KfGRbfc4V/9zRIcE8jlHN61S1ju8X93+6dxDUrG2\nSzxqJ4BhqyYmUDruPXJSX4vUc01P7j98MpqOS95rORdGHeI52Naz5m2B+O+vjsC0\n60d37jY9LFeuOP4Meri8qgfi2S5kKqg/aF6aPtuAZQVR7u3KFYXP59XmJgtcog05\ngmI0T/OitLhuzVvpZcLph0odh/1IPXqx3+MnjD97A7fXpqGd/y8KxX7jksTEzAOg\nbKAeam3lm+3yKIcTYMlsRMXPcjNbIvmsBykD//xSniusuHBkgnlENEWx1UcbQQrs\n+gVDkuVPhsnzIRNgYvM48Y+7LGiJYnrmE8xcrexekBxrva2V9TJQqnN3Q53kt5vi\nQi3+gCfmkwC0F0tirIZbLkXPrPwzZ0M9eNxhIySb2npJfgnqz55I0u33wh4r0ZNQ\neTGfw03MBUtyuzGesGkcw+loqMaq1qR4tjGbPYxCvpCq7+OgpCCoMNit2uLo9M18\nfHz10lOMT8nWAUvRZFzteXCm+7PHdYPlmQwUw3LvenJ/ILXoQPHfbkH0CyPfhl1j\nWhJFZasCAwEAAaN+MHwwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSFrBrRQ/fI\nrFXUxR1BSKvVeErUUzAPBgNVHRMBAf8EBTADAQH/MDoGA1UdHwQzMDEwL6AtoCuG\nKWh0dHBzOi8va2RzaW50Zi5hbWQuY29tL3ZjZWsvdjEvTWlsYW4vY3JsMEYGCSqG\nSIb3DQEBCjA5oA8wDQYJYIZIAWUDBAICBQChHDAaBgkqhkiG9w0BAQgwDQYJYIZI\nAWUDBAICBQCiAwIBMKMDAgEBA4ICAQC6m0kDp6zv4Ojfgy+zleehsx6ol0ocgVel\nETobpx+EuCsqVFRPK1jZ1sp/lyd9+0fQ0r66n7kagRk4Ca39g66WGTJMeJdqYriw\nSTjjDCKVPSesWXYPVAyDhmP5n2v+BYipZWhpvqpaiO+EGK5IBP+578QeW/sSokrK\ndHaLAxG2LhZxj9aF73fqC7OAJZ5aPonw4RE299FVarh1Tx2eT3wSgkDgutCTB1Yq\nzT5DuwvAe+co2CIVIzMDamYuSFjPN0BCgojl7V+bTou7dMsqIu/TW/rPCX9/EUcp\nKGKqPQ3P+N9r1hjEFY1plBg93t53OOo49GNI+V1zvXPLI6xIFVsh+mto2RtgEX/e\npmMKTNN6psW88qg7c1hTWtN6MbRuQ0vm+O+/2tKBF2h8THb94OvvHHoFDpbCELlq\nHnIYhxy0YKXGyaW1NjfULxrrmxVW4wcn5E8GddmvNa6yYm8scJagEi13mhGu4Jqh\n3QU3sf8iUSUr09xQDwHtOQUVIqx4maBZPBtSMf+qUDtjXSSq8lfWcd8bLr9mdsUn\nJZJ0+tuPMKmBnSH860llKk+VpVQsgqbzDIvOLvD6W1Umq25boxCYJ+TuBoa4s+HH\nCViAvgT9kf/rBq1d+ivj6skkHxuzcxbk1xv6ZGxrteJxVH7KlX7YRdZ6eARKwLe4\nAFZEAwoKCQ==\n-----END CERTIFICATE-----\n"
|
||||
|
||||
// Validator for Azure confidential VM attestation.
|
||||
type Validator struct {
|
||||
oid.Azure
|
||||
@ -20,30 +34,262 @@ func NewValidator(pcrs map[uint32][]byte, enforcedPCRs []uint32) *Validator {
|
||||
Validator: vtpm.NewValidator(
|
||||
pcrs,
|
||||
enforcedPCRs,
|
||||
trustedKeyFromSNP,
|
||||
trustedKeyFromSNP(&azureInstanceInfo{}),
|
||||
validateAzureCVM,
|
||||
vtpm.VerifyPKCS1v15,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// trustedKeyFromSNP establishes trust in the given public key.
|
||||
// It does so by verifying the SNP attestation statement in instanceInfo.
|
||||
//
|
||||
// As long as we are using regular VMs on Azure this is a stub, only returning the given key.
|
||||
func trustedKeyFromSNP(akPub, instanceInfo []byte) (crypto.PublicKey, error) {
|
||||
// TODO: convert this to SEV-SNP attestation verification
|
||||
pubArea, err := tpm2.DecodePublic(akPub)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pubArea.Key()
|
||||
type signatureError struct {
|
||||
innerError error
|
||||
}
|
||||
|
||||
// validateAzureCVM validates Azure CVM capabilities.
|
||||
//
|
||||
// This might stay a stub, since SEV-SNP attestation is already verified in trustedKeyFromSNP().
|
||||
func (e *signatureError) Unwrap() error {
|
||||
return e.innerError
|
||||
}
|
||||
|
||||
func (e *signatureError) Error() string {
|
||||
return fmt.Sprintf("signature validation failed: %v", e.innerError)
|
||||
}
|
||||
|
||||
type askError struct {
|
||||
innerError error
|
||||
}
|
||||
|
||||
func (e *askError) Unwrap() error {
|
||||
return e.innerError
|
||||
}
|
||||
|
||||
func (e *askError) Error() string {
|
||||
return fmt.Sprintf("validating ASK: %v", e.innerError)
|
||||
}
|
||||
|
||||
type vcekError struct {
|
||||
innerError error
|
||||
}
|
||||
|
||||
func (e *vcekError) Unwrap() error {
|
||||
return e.innerError
|
||||
}
|
||||
|
||||
func (e *vcekError) Error() string {
|
||||
return fmt.Sprintf("validating VCEK: %v", e.innerError)
|
||||
}
|
||||
|
||||
// trustedKeyFromSNP establishes trust in the given public key.
|
||||
// It does so by verifying the SNP attestation statement in instanceInfo.
|
||||
func trustedKeyFromSNP(hclAk HCLAkValidator) func(akPub, instanceInfoRaw []byte) (crypto.PublicKey, error) {
|
||||
return func(akPub, instanceInfoRaw []byte) (crypto.PublicKey, error) {
|
||||
var instanceInfo azureInstanceInfo
|
||||
if err := json.Unmarshal(instanceInfoRaw, &instanceInfo); err != nil {
|
||||
return nil, fmt.Errorf("unmarshalling instanceInfoRaw: %w", err)
|
||||
}
|
||||
|
||||
report, err := newSNPReportFromBytes(instanceInfo.AttestationReport)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing attestation report: %w", err)
|
||||
}
|
||||
|
||||
vcek, err := validateVCEK(instanceInfo.Vcek, instanceInfo.CertChain)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("validating VCEK: %w", err)
|
||||
}
|
||||
|
||||
if err = validateSNPReport(vcek, report); err != nil {
|
||||
return nil, fmt.Errorf("validating SNP report: %w", err)
|
||||
}
|
||||
|
||||
pubArea, err := tpm2.DecodePublic(akPub)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = hclAk.validateAk(instanceInfo.RuntimeData, report.ReportData[:], pubArea.RSAParameters); err != nil {
|
||||
return nil, fmt.Errorf("validating HCLAkPub: %w", err)
|
||||
}
|
||||
|
||||
return pubArea.Key()
|
||||
}
|
||||
}
|
||||
|
||||
func reverseEndian(b []byte) {
|
||||
for i := 0; i < len(b)/2; i++ {
|
||||
b[i], b[len(b)-i-1] = b[len(b)-i-1], b[i]
|
||||
}
|
||||
}
|
||||
|
||||
// validateAzureCVM is a stub, since SEV-SNP attestation is already verified in trustedKeyFromSNP().
|
||||
func validateAzureCVM(attestation vtpm.AttestationDocument) error {
|
||||
// TODO: implement this for CVMs
|
||||
return nil
|
||||
}
|
||||
|
||||
func newSNPReportFromBytes(reportRaw []byte) (snpAttestationReport, error) {
|
||||
var report snpAttestationReport
|
||||
if err := binary.Read(bytes.NewReader(reportRaw), binary.LittleEndian, &report); err != nil {
|
||||
return snpAttestationReport{}, fmt.Errorf("reading attestation report: %w", err)
|
||||
}
|
||||
|
||||
return report, nil
|
||||
}
|
||||
|
||||
// validateVCEK takes the PEM-encoded X509 certificate VCEK, ASK and ARK and verifies the integrity of the chain.
|
||||
// ARK (hardcoded) validates ASK (cloud metadata API) validates VCEK (cloud metadata API).
|
||||
func validateVCEK(vcekRaw []byte, certChain []byte) (*x509.Certificate, error) {
|
||||
vcek, err := internalCrypto.PemToX509Cert(vcekRaw)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("loading vcek: %w", err)
|
||||
}
|
||||
|
||||
ark, err := internalCrypto.PemToX509Cert([]byte(arkPEM))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("loading arkPEM: %w", err)
|
||||
}
|
||||
|
||||
// certChain includes two PEM encoded certs. The ASK and the ARK, in that order.
|
||||
ask, err := internalCrypto.PemToX509Cert(certChain)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("loading askPEM: %w", err)
|
||||
}
|
||||
|
||||
if err = ask.CheckSignatureFrom(ark); err != nil {
|
||||
return nil, &askError{err}
|
||||
}
|
||||
|
||||
if err = vcek.CheckSignatureFrom(ask); err != nil {
|
||||
return nil, &vcekError{err}
|
||||
}
|
||||
|
||||
return vcek, nil
|
||||
}
|
||||
|
||||
func validateSNPReport(cert *x509.Certificate, report snpAttestationReport) error {
|
||||
sig_r := report.Signature.R[:]
|
||||
sig_s := report.Signature.S[:]
|
||||
|
||||
// Table 107 in https://www.amd.com/system/files/TechDocs/56860.pdf mentions little endian signature components.
|
||||
// They come out of the certificate as big endian.
|
||||
reverseEndian(sig_r)
|
||||
reverseEndian(sig_s)
|
||||
|
||||
r := new(big.Int).SetBytes(sig_r)
|
||||
s := new(big.Int).SetBytes(sig_s)
|
||||
sequence := ecdsaSig{r, s}
|
||||
sigEncoded, err := asn1.Marshal(sequence)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshalling ecdsa signature: %w", err)
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
if err = binary.Write(buf, binary.LittleEndian, report); err != nil {
|
||||
return fmt.Errorf("writing report to buf: %w", err)
|
||||
}
|
||||
// signature is only calculated from 0x0 to 0x2a0
|
||||
if err := cert.CheckSignature(x509.ECDSAWithSHA384, buf.Bytes()[:0x2a0], sigEncoded); err != nil {
|
||||
return &signatureError{err}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type azureInstanceInfo struct {
|
||||
Vcek []byte
|
||||
CertChain []byte
|
||||
AttestationReport []byte
|
||||
RuntimeData []byte
|
||||
}
|
||||
|
||||
// validateAk validates that the attestation key from the TPM is trustworthy. The steps are:
|
||||
// 1. runtime data read from the TPM has the same sha256 digest as reported in `report_data` of the SNP report.
|
||||
// 2. modulus reported in runtime data matches modulus from key at idx 0x81000003.
|
||||
// 3. exponent reported in runtime data matches exponent from key at idx 0x81000003.
|
||||
// The function is currently tested manually on a Azure Ubuntu CVM.
|
||||
func (a *azureInstanceInfo) validateAk(runtimeDataRaw []byte, reportData []byte, rsaParameters *tpm2.RSAParams) error {
|
||||
var runtimeData runtimeData
|
||||
if err := json.Unmarshal(runtimeDataRaw, &runtimeData); err != nil {
|
||||
return fmt.Errorf("unmarshalling json: %w", err)
|
||||
}
|
||||
|
||||
sum := sha256.Sum256(runtimeDataRaw)
|
||||
if !bytes.Equal(sum[:], reportData[:len(sum)]) {
|
||||
return errors.New("unexpected runtimeData digest in TPM")
|
||||
}
|
||||
|
||||
if len(runtimeData.Keys) < 1 {
|
||||
return errors.New("did not receive any keys in runtime data")
|
||||
}
|
||||
rawN, err := base64.RawURLEncoding.DecodeString(runtimeData.Keys[0].N)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !bytes.Equal(rawN, rsaParameters.ModulusRaw) {
|
||||
return fmt.Errorf("unexpected modulus value in TPM")
|
||||
}
|
||||
|
||||
rawE, err := base64.RawURLEncoding.DecodeString(runtimeData.Keys[0].E)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
paddedRawE := make([]byte, 4)
|
||||
copy(paddedRawE, rawE)
|
||||
exponent := binary.LittleEndian.Uint32(paddedRawE)
|
||||
|
||||
// According to this comment [1] the TPM uses "0" to represent the default exponent "65537".
|
||||
// The go tpm library also reports the exponent as 0. Thus we have to handle it specially.
|
||||
// [1] https://github.com/tpm2-software/tpm2-tools/pull/1973#issue-596685005
|
||||
if !((exponent == 65537 && rsaParameters.ExponentRaw == 0) || exponent == rsaParameters.ExponentRaw) {
|
||||
return fmt.Errorf("unexpected N value in TPM")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type HCLAkValidator interface {
|
||||
validateAk(runtimeDataRaw []byte, reportData []byte, rsaParameters *tpm2.RSAParams) error
|
||||
}
|
||||
|
||||
type snpSignature struct {
|
||||
R [72]byte
|
||||
S [72]byte
|
||||
Reserved [512 - 144]byte
|
||||
}
|
||||
|
||||
// Reference: https://github.com/AMDESE/sev-guest/blob/main/include/attestation.h
|
||||
type snpAttestationReport struct {
|
||||
Version uint32 /* 0x000 */
|
||||
GuestSvn uint32 /* 0x004 */
|
||||
Policy uint64 /* 0x008 */
|
||||
FamilyId [16]byte /* 0x010 */
|
||||
ImageId [16]byte /* 0x020 */
|
||||
Vmpl uint32 /* 0x030 */
|
||||
SignatureAlgo uint32 /* 0x034 */
|
||||
PlatformVersion uint64 /* 0x038 */
|
||||
PlatformInfo uint64 /* 0x040 */
|
||||
Flags uint32 /* 0x048 */
|
||||
Reserved0 uint32 /* 0x04C */
|
||||
ReportData [64]byte /* 0x050 */
|
||||
Measurement [48]byte /* 0x090 */
|
||||
HostData [32]byte /* 0x0C0 */
|
||||
IdKeyDigest [48]byte /* 0x0E0 */
|
||||
AuthorKeyDigest [48]byte /* 0x110 */
|
||||
ReportId [32]byte /* 0x140 */
|
||||
ReportIdMa [32]byte /* 0x160 */
|
||||
ReportedTcb uint64 /* 0x180 */
|
||||
Reserved1 [24]byte /* 0x188 */
|
||||
ChipId [64]byte /* 0x1A0 */
|
||||
Reserved2 [192]byte /* 0x1E0 */
|
||||
Signature snpSignature /* 0x2A0 */
|
||||
}
|
||||
|
||||
type ecdsaSig struct {
|
||||
R, S *big.Int
|
||||
}
|
||||
|
||||
type akPub struct {
|
||||
E string
|
||||
N string
|
||||
}
|
||||
|
||||
type runtimeData struct {
|
||||
Keys []akPub
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
@ -4,6 +4,9 @@ package crypto
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
|
||||
@ -48,3 +51,16 @@ func GenerateRandomBytes(length int) ([]byte, error) {
|
||||
}
|
||||
return nonce, nil
|
||||
}
|
||||
|
||||
// PemToX509Cert takes a list of PEM encoded certificates, parses the first one and returns it.
|
||||
func PemToX509Cert(raw []byte) (*x509.Certificate, error) {
|
||||
decoded, _ := pem.Decode(raw)
|
||||
if decoded == nil {
|
||||
return nil, fmt.Errorf("decoding pem: no PEM data found")
|
||||
}
|
||||
cert, err := x509.ParseCertificate(decoded.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing certificate: %w", err)
|
||||
}
|
||||
return cert, nil
|
||||
}
|
||||
|
@ -41,8 +41,7 @@ func (c KubernetesCA) GetCertificate(csr []byte) (cert []byte, err error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parentCertPEM, _ := pem.Decode(parentCertRaw)
|
||||
parentCert, err := x509.ParseCertificate(parentCertPEM.Bytes)
|
||||
parentCert, err := crypto.PemToX509Cert(parentCertRaw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -62,7 +61,7 @@ func (c KubernetesCA) GetCertificate(csr []byte) (cert []byte, err error) {
|
||||
case "PRIVATE KEY":
|
||||
parentKey, err = x509.ParsePKCS8PrivateKey(parentKeyPEM.Bytes)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported key type %q", parentCertPEM.Type)
|
||||
return nil, fmt.Errorf("unsupported key type %q", parentKeyPEM.Type)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
Loading…
Reference in New Issue
Block a user