AB#2444 Verify Azure trusted launch attestation keys (#203)

Signed-off-by: Daniel Weiße <dw@edgeless.systems>
This commit is contained in:
Daniel Weiße 2022-10-04 16:44:44 +02:00 committed by GitHub
parent a60e76e91f
commit acdcb535c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 457 additions and 182 deletions

View File

@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Loadbalancer for control-plane recovery - Loadbalancer for control-plane recovery
- K8s conformance mode - K8s conformance mode
- Local cluster creation based on QEMU - Local cluster creation based on QEMU
- Verification of Azure trusted launch attestation keys
### Changed ### Changed
<!-- For changes in existing functionality. --> <!-- For changes in existing functionality. -->

5
go.mod
View File

@ -30,7 +30,10 @@ replace (
k8s.io/sample-apiserver v0.0.0 => k8s.io/sample-apiserver v0.24.6 k8s.io/sample-apiserver v0.0.0 => k8s.io/sample-apiserver v0.24.6
) )
replace github.com/google/go-attestation => github.com/malt3/go-attestation v0.0.0-20220816131639-92b6394e4e0e replace (
github.com/google/go-attestation => github.com/malt3/go-attestation v0.0.0-20220816131639-92b6394e4e0e
github.com/google/go-tpm-tools => github.com/daniel-weisse/go-tpm-tools v0.0.0-20220929072523-22862750ed86
)
require ( require (
cloud.google.com/go/compute v1.7.0 cloud.google.com/go/compute v1.7.0

8
go.sum
View File

@ -441,6 +441,8 @@ github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ
github.com/cyphar/filepath-securejoin v0.2.3 h1:YX6ebbZCZP7VkM3scTTokDgBL2TY741X51MTk3ycuNI= github.com/cyphar/filepath-securejoin v0.2.3 h1:YX6ebbZCZP7VkM3scTTokDgBL2TY741X51MTk3ycuNI=
github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
github.com/daixiang0/gci v0.2.9/go.mod h1:+4dZ7TISfSmqfAGv59ePaHfNzgGtIkHAhhdKggP1JAc= github.com/daixiang0/gci v0.2.9/go.mod h1:+4dZ7TISfSmqfAGv59ePaHfNzgGtIkHAhhdKggP1JAc=
github.com/daniel-weisse/go-tpm-tools v0.0.0-20220929072523-22862750ed86 h1:fJzK2MkfHItDnNVEgCUQfaH0SzcNBkpTB31XCBd3YUk=
github.com/daniel-weisse/go-tpm-tools v0.0.0-20220929072523-22862750ed86/go.mod h1:22JvWmHcD5w55cs+nMeqDGDxgNS15/2pDq2cLqnc3rc=
github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg= github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg=
github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -755,14 +757,8 @@ github.com/google/go-licenses v0.0.0-20210329231322-ce1d9163b77d/go.mod h1:+TYOm
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/go-replayers/grpcreplay v0.1.0/go.mod h1:8Ig2Idjpr6gifRd6pNVggX6TC1Zw6Jx74AKp7QNH2QE= github.com/google/go-replayers/grpcreplay v0.1.0/go.mod h1:8Ig2Idjpr6gifRd6pNVggX6TC1Zw6Jx74AKp7QNH2QE=
github.com/google/go-replayers/httpreplay v0.1.0/go.mod h1:YKZViNhiGgqdBlUbI2MwGpq4pXxNmhJLPHQ7cv2b5no= github.com/google/go-replayers/httpreplay v0.1.0/go.mod h1:YKZViNhiGgqdBlUbI2MwGpq4pXxNmhJLPHQ7cv2b5no=
github.com/google/go-tpm v0.1.2-0.20190725015402-ae6dd98980d4/go.mod h1:H9HbmUG2YgV/PHITkO7p6wxEEj/v5nlsVWIwumwH2NI=
github.com/google/go-tpm v0.3.0/go.mod h1:iVLWvrPp/bHeEkxTFi9WG6K9w0iy2yIszHwZGHPbzAw=
github.com/google/go-tpm v0.3.3 h1:P/ZFNBZYXRxc+z7i5uyd8VP7MaDteuLZInzrH2idRGo= github.com/google/go-tpm v0.3.3 h1:P/ZFNBZYXRxc+z7i5uyd8VP7MaDteuLZInzrH2idRGo=
github.com/google/go-tpm v0.3.3/go.mod h1:9Hyn3rgnzWF9XBWVk6ml6A6hNkbWjNFlDQL51BeghL4= github.com/google/go-tpm v0.3.3/go.mod h1:9Hyn3rgnzWF9XBWVk6ml6A6hNkbWjNFlDQL51BeghL4=
github.com/google/go-tpm-tools v0.0.0-20190906225433-1614c142f845/go.mod h1:AVfHadzbdzHo54inR2x1v640jdi1YSi3NauM2DUsxk0=
github.com/google/go-tpm-tools v0.2.0/go.mod h1:npUd03rQ60lxN7tzeBJreG38RvWwme2N1reF/eeiBk4=
github.com/google/go-tpm-tools v0.3.8 h1:ecZgxez5lyKWjnkK8lP3ru4rkgLyIM7pPY36FOFnAx4=
github.com/google/go-tpm-tools v0.3.8/go.mod h1:rp+rDmmDCnWiMmxOTF3ypWxpChEQ4vwA6wtAIq09Qtc=
github.com/google/go-tspi v0.2.1-0.20190423175329-115dea689aad/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5by8CiHI= github.com/google/go-tspi v0.2.1-0.20190423175329-115dea689aad/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5by8CiHI=
github.com/google/go-tspi v0.3.0 h1:ADtq8RKfP+jrTyIWIZDIYcKOMecRqNJFOew2IT0Inus= github.com/google/go-tspi v0.3.0 h1:ADtq8RKfP+jrTyIWIZDIYcKOMecRqNJFOew2IT0Inus=
github.com/google/go-tspi v0.3.0/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5by8CiHI= github.com/google/go-tspi v0.3.0/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5by8CiHI=

View File

@ -25,6 +25,7 @@ const (
lenSnpReport = 0x4a0 lenSnpReport = 0x4a0
lenSnpReportRuntimeDataPadding = 0x14 lenSnpReportRuntimeDataPadding = 0x14
tpmReportIdx = 0x01400001 tpmReportIdx = 0x01400001
tpmAkIdx = 0x81000003
) )
// GetIdKeyDigest reads the idkeydigest from the snp report saved in the TPM's non-volatile memory. // GetIdKeyDigest reads the idkeydigest from the snp report saved in the TPM's non-volatile memory.
@ -37,7 +38,7 @@ func GetIdKeyDigest(open vtpm.TPMOpenFunc) ([]byte, error) {
reportRaw, err := tpm2.NVReadEx(tpm, tpmReportIdx, tpm2.HandleOwner, "", 0) reportRaw, err := tpm2.NVReadEx(tpm, tpmReportIdx, tpm2.HandleOwner, "", 0)
if err != nil { if err != nil {
return nil, fmt.Errorf("reading idx %x from TMP: %w", tpmReportIdx, err) return nil, fmt.Errorf("reading idx %x from TPM: %w", tpmReportIdx, err)
} }
report, err := newSNPReportFromBytes(reportRaw[lenHclHeader:]) report, err := newSNPReportFromBytes(reportRaw[lenHclHeader:])
@ -107,30 +108,9 @@ func getInstanceInfo(reportGetter tpmReportGetter, imdsAPI imdsApi) func(tpm io.
} }
} }
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,
},
}
}
// getAttestationKey reads the attesation key put into the TPM during early boot. // getAttestationKey reads the attesation key put into the TPM during early boot.
func getAttestationKey(tpm io.ReadWriter) (*tpmclient.Key, error) { func getAttestationKey(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. ak, err := tpmclient.LoadCachedKey(tpm, tpmAkIdx)
// 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 { if err != nil {
return nil, fmt.Errorf("reading HCL attestation key from TPM: %w", err) return nil, fmt.Errorf("reading HCL attestation key from TPM: %w", err)
} }

View File

@ -14,7 +14,9 @@ import (
"io" "io"
"testing" "testing"
tpmclient "github.com/google/go-tpm-tools/client"
"github.com/google/go-tpm-tools/simulator" "github.com/google/go-tpm-tools/simulator"
"github.com/google/go-tpm/tpm2"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -91,12 +93,34 @@ func TestGetSNPAttestation(t *testing.T) {
// Testing anything else will only verify that the simulator works as expected, since getAkPub // Testing anything else will only verify that the simulator works as expected, since getAkPub
// only retrieves the attestation key from the TPM. // only retrieves the attestation key from the TPM.
func TestGetHCLAttestationKey(t *testing.T) { func TestGetHCLAttestationKey(t *testing.T) {
require := require.New(t)
assert := assert.New(t) assert := assert.New(t)
tpm, err := simulator.Get() tpm, err := simulator.Get()
assert.NoError(err) require.NoError(err)
defer tpm.Close() defer tpm.Close()
// we should receive an error if no key was saved at index `tpmAkIdx`
_, err = getAttestationKey(tpm)
assert.Error(err)
// create a key at the index
tpmAk, err := tpmclient.NewCachedKey(tpm, tpm2.HandleOwner, tpm2.Public{
Type: tpm2.AlgRSA,
NameAlg: tpm2.AlgSHA256,
Attributes: tpm2.FlagFixedTPM | tpm2.FlagFixedParent | tpm2.FlagSensitiveDataOrigin | tpm2.FlagUserWithAuth | tpm2.FlagNoDA | tpm2.FlagRestricted | tpm2.FlagSign,
RSAParameters: &tpm2.RSAParams{
Sign: &tpm2.SigScheme{
Alg: tpm2.AlgRSASSA,
Hash: tpm2.AlgSHA256,
},
KeyBits: 2048,
},
}, tpmAkIdx)
require.NoError(err)
defer tpmAk.Close()
// we should now be able to retrieve the key
_, err = getAttestationKey(tpm) _, err = getAttestationKey(tpm)
assert.NoError(err) assert.NoError(err)
} }

View File

@ -7,31 +7,117 @@ SPDX-License-Identifier: AGPL-3.0-only
package trustedlaunch package trustedlaunch
import ( import (
"context"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"io" "io"
"net/http"
"time"
"github.com/edgelesssys/constellation/v2/internal/attestation/vtpm" "github.com/edgelesssys/constellation/v2/internal/attestation/vtpm"
"github.com/edgelesssys/constellation/v2/internal/oid" "github.com/edgelesssys/constellation/v2/internal/oid"
tpmclient "github.com/google/go-tpm-tools/client" tpmclient "github.com/google/go-tpm-tools/client"
"github.com/google/go-tpm/tpm2"
)
const (
tpmAkIdx = 0x81000003
tpmAkCertIdx = 0x1C101D0
) )
// Issuer for Azure trusted launch TPM attestation. // Issuer for Azure trusted launch TPM attestation.
type Issuer struct { type Issuer struct {
oid.AzureTrustedLaunch oid.AzureTrustedLaunch
*vtpm.Issuer *vtpm.Issuer
hClient httpClient
} }
// NewIssuer initializes a new Azure Issuer. // NewIssuer initializes a new Azure Issuer.
func NewIssuer() *Issuer { func NewIssuer() *Issuer {
return &Issuer{ i := &Issuer{
Issuer: vtpm.NewIssuer( hClient: &http.Client{},
vtpm.OpenVTPM,
tpmclient.AttestationKeyRSA,
getAttestation,
),
} }
i.Issuer = vtpm.NewIssuer(
vtpm.OpenVTPM,
getAttestationKey,
i.getAttestationCert,
)
return i
} }
// getAttestation returns nil. // akSigner holds the attestation key certificate and the corresponding CA certificate.
func getAttestation(tpm io.ReadWriteCloser) ([]byte, error) { type akSigner struct {
return nil, nil AkCert []byte
CA []byte
}
// getAttestationCert returns the DER encoded certificate of the TPM's attestation key and it's CA.
func (i *Issuer) getAttestationCert(tpm io.ReadWriteCloser) ([]byte, error) {
certDER, err := tpm2.NVReadEx(tpm, tpmAkCertIdx, tpm2.HandleOwner, "", 0)
if err != nil {
return nil, fmt.Errorf("reading attestation key certificate from TPM: %w", err)
}
cert, err := x509.ParseCertificate(certDER)
if err != nil {
return nil, fmt.Errorf("parsing attestation key certificate: %w", err)
}
// try to load the CA certificate from the issuing certificate URL
// any error is ignored and the next URL is tried
// if no CA certificate can be loaded, an error is returned
var caCert *x509.Certificate
for _, caCertURL := range cert.IssuingCertificateURL {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*20)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, caCertURL, nil)
if err != nil {
continue
}
resp, err := i.hClient.Do(req)
if err != nil {
continue
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
continue
}
caCertDER, err := io.ReadAll(resp.Body)
if err != nil {
continue
}
caCert, err = x509.ParseCertificate(caCertDER)
if err != nil {
continue
}
break
}
if caCert == nil {
return nil, errors.New("failed to load CA certificate")
}
signerInfo := akSigner{
AkCert: certDER,
CA: caCert.Raw,
}
return json.Marshal(signerInfo)
}
// getAttestationKey reads the Azure trusted launch attesation key.
func getAttestationKey(tpm io.ReadWriter) (*tpmclient.Key, error) {
ak, err := tpmclient.LoadCachedKey(tpm, tpmAkIdx)
if err != nil {
return nil, fmt.Errorf("reading attestation key from TPM: %w", err)
}
return ak, nil
}
type httpClient interface {
Do(req *http.Request) (*http.Response, error)
} }

View File

@ -1,46 +0,0 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package trustedlaunch
import (
"testing"
"github.com/edgelesssys/constellation/v2/internal/attestation/simulator"
"github.com/edgelesssys/constellation/v2/internal/attestation/vtpm"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGetSNPAttestation(t *testing.T) {
testCases := map[string]struct {
tpmFunc vtpm.TPMOpenFunc
wantErr bool
}{
"success": {
tpmFunc: simulator.OpenSimulatedTPM,
wantErr: false,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
tpm, err := tc.tpmFunc()
require.NoError(err)
defer tpm.Close()
_, err = getAttestation(tpm)
if tc.wantErr {
assert.Error(err)
} else {
assert.NoError(err)
}
})
}
}

View File

@ -0,0 +1,250 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package trustedlaunch
import (
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"io"
"net/http"
"testing"
"time"
"github.com/edgelesssys/constellation/v2/internal/attestation/simulator"
"github.com/edgelesssys/constellation/v2/internal/crypto"
tpmclient "github.com/google/go-tpm-tools/client"
"github.com/google/go-tpm/tpm2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGetAttestationCert(t *testing.T) {
require := require.New(t)
tpm, err := simulator.OpenSimulatedTPM()
require.NoError(err)
defer tpm.Close()
// create key in TPM
tpmAk, err := tpmclient.NewCachedKey(tpm, tpm2.HandleOwner, tpm2.Public{
Type: tpm2.AlgRSA,
NameAlg: tpm2.AlgSHA256,
Attributes: tpm2.FlagFixedTPM | tpm2.FlagFixedParent | tpm2.FlagSensitiveDataOrigin | tpm2.FlagUserWithAuth | tpm2.FlagNoDA | tpm2.FlagRestricted | tpm2.FlagSign,
RSAParameters: &tpm2.RSAParams{
Sign: &tpm2.SigScheme{
Alg: tpm2.AlgRSASSA,
Hash: tpm2.AlgSHA256,
},
KeyBits: 2048,
},
}, tpmAkIdx)
require.NoError(err)
defer tpmAk.Close()
akPub, err := tpmAk.PublicArea().Encode()
require.NoError(err)
// root certificate
rootKey, rootTemplate := fillCertTemplate(t, &x509.Certificate{
Subject: pkix.Name{CommonName: "root CA"},
IsCA: true,
BasicConstraintsValid: true,
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
})
rootCert := newTestCert(t, rootTemplate, rootTemplate, rootKey.Public(), rootKey)
// intermediate certificate
intermediateKey, intermediateTemplate := fillCertTemplate(t, &x509.Certificate{
Subject: pkix.Name{CommonName: "intermediate CA"},
Issuer: rootTemplate.Subject,
IsCA: true,
BasicConstraintsValid: true,
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
})
intermediateCert := newTestCert(t, intermediateTemplate, rootTemplate, intermediateKey.Public(), rootKey)
// define NV index once to avoid the need for fancy error handling later
require.NoError(tpm2.NVDefineSpace(
tpm, tpm2.HandleOwner, tpmAkCertIdx, "", "", []byte{},
tpm2.AttrOwnerWrite|tpm2.AttrOwnerRead|tpm2.AttrAuthRead|tpm2.AttrAuthWrite|tpm2.AttrNoDA, 1,
))
defaultAkCertFunc := func(*testing.T) *x509.Certificate {
t.Helper()
_, certTemplate := fillCertTemplate(t, &x509.Certificate{
IssuingCertificateURL: []string{
"192.0.2.1/ca.crt",
},
Subject: pkix.Name{CommonName: "AK Certificate"},
Issuer: intermediateCert.Subject,
})
return newTestCert(t, certTemplate, intermediateCert, tpmAk.PublicKey(), intermediateKey)
}
testCases := map[string]struct {
crlServer roundTripFunc
getAkCert func(*testing.T) *x509.Certificate
wantIssueErr bool
wantValidateErr bool
}{
"success": {
crlServer: func(req *http.Request) *http.Response {
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewReader(intermediateCert.Raw)),
}
},
getAkCert: defaultAkCertFunc,
},
"intermediate cert is fetched from multiple URLs": {
crlServer: func(req *http.Request) *http.Response {
if req.URL.String() == "192.0.2.1/ca.crt" {
return &http.Response{StatusCode: http.StatusNotFound}
}
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewReader(intermediateCert.Raw)),
}
},
getAkCert: func(*testing.T) *x509.Certificate {
t.Helper()
_, certTemplate := fillCertTemplate(t, &x509.Certificate{
IssuingCertificateURL: []string{
"192.0.2.1/ca.crt",
"192.0.2.2/ca.crt",
},
Subject: pkix.Name{CommonName: "AK Certificate"},
Issuer: intermediateCert.Subject,
})
return newTestCert(t, certTemplate, intermediateCert, tpmAk.PublicKey(), intermediateKey)
},
},
"intermediate cert cannot be fetched": {
crlServer: func(req *http.Request) *http.Response {
return &http.Response{StatusCode: http.StatusNotFound}
},
getAkCert: defaultAkCertFunc,
wantIssueErr: true,
},
"intermediate cert is not signed by root cert": {
crlServer: func(req *http.Request) *http.Response {
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewReader(rootCert.Raw)),
}
},
getAkCert: defaultAkCertFunc,
wantValidateErr: true,
},
"ak does not match ak cert public key": {
crlServer: func(req *http.Request) *http.Response {
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewReader(intermediateCert.Raw)),
}
},
getAkCert: func(*testing.T) *x509.Certificate {
t.Helper()
key, certTemplate := fillCertTemplate(t, &x509.Certificate{
IssuingCertificateURL: []string{
"192.0.2.1/ca.crt",
},
Subject: pkix.Name{CommonName: "AK Certificate"},
Issuer: intermediateCert.Subject,
})
return newTestCert(t, certTemplate, intermediateCert, key.Public(), intermediateKey)
},
wantValidateErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
akCert := tc.getAkCert(t).Raw
// write akCert to TPM
require.NoError(tpm2.NVUndefineSpace(tpm, "", tpm2.HandleOwner, tpmAkCertIdx))
require.NoError(tpm2.NVDefineSpace(
tpm, tpm2.HandleOwner, tpmAkCertIdx, "", "", []byte{},
tpm2.AttrOwnerWrite|tpm2.AttrOwnerRead|tpm2.AttrAuthRead|tpm2.AttrAuthWrite|tpm2.AttrNoDA,
uint16(len(akCert)),
))
require.NoError(tpm2.NVWrite(tpm, tpm2.HandleOwner, tpmAkCertIdx, "", akCert, 0))
issuer := NewIssuer()
issuer.hClient = newTestClient(tc.crlServer)
certs, err := issuer.getAttestationCert(tpm)
if tc.wantIssueErr {
assert.Error(err)
return
}
require.NoError(err)
validator := NewValidator(map[uint32][]byte{}, []uint32{}, nil)
cert, err := x509.ParseCertificate(rootCert.Raw)
require.NoError(err)
roots := x509.NewCertPool()
roots.AddCert(cert)
validator.roots = roots
key, err := validator.verifyAttestationKey(akPub, certs)
if tc.wantValidateErr {
assert.Error(err)
return
}
assert.NoError(err)
rsaKey, ok := key.(*rsa.PublicKey)
require.True(ok)
assert.True(rsaKey.Equal(tpmAk.PublicKey()))
})
}
}
type roundTripFunc func(req *http.Request) *http.Response
func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
return f(req), nil
}
// newTestClient returns *http.Client with Transport replaced to avoid making real calls.
func newTestClient(fn roundTripFunc) *http.Client {
return &http.Client{
Transport: fn,
}
}
func newTestCert(t *testing.T, template *x509.Certificate, parent *x509.Certificate, pub, priv any) *x509.Certificate {
t.Helper()
require := require.New(t)
certDER, err := x509.CreateCertificate(rand.Reader, template, parent, pub, priv)
require.NoError(err)
cert, err := x509.ParseCertificate(certDER)
require.NoError(err)
return cert
}
func fillCertTemplate(t *testing.T, template *x509.Certificate) (*rsa.PrivateKey, *x509.Certificate) {
t.Helper()
require := require.New(t)
key, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(err, "generating root key failed")
serialNumber, err := crypto.GenerateCertificateSerialNumber()
require.NoError(err)
now := time.Now()
template.SerialNumber = serialNumber
template.NotBefore = now.Add(-2 * time.Hour)
template.NotAfter = now.Add(24 * 365 * time.Hour)
return key, template
}

View File

@ -8,42 +8,104 @@ package trustedlaunch
import ( import (
"crypto" "crypto"
"crypto/rsa"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"github.com/edgelesssys/constellation/v2/internal/attestation/vtpm" "github.com/edgelesssys/constellation/v2/internal/attestation/vtpm"
certutil "github.com/edgelesssys/constellation/v2/internal/crypto"
"github.com/edgelesssys/constellation/v2/internal/oid" "github.com/edgelesssys/constellation/v2/internal/oid"
"github.com/google/go-tpm/tpm2" "github.com/google/go-tpm/tpm2"
) )
// ameRoot is the AME root CA certificate used to sign Azure's AME Infra CA certificates.
// The certificate can be found at http://crl.microsoft.com/pkiinfra/certs/AMERoot_ameroot.crt.
var ameRoot = mustParseX509("-----BEGIN CERTIFICATE-----\nMIIFVjCCAz6gAwIBAgIQJdrLVcnGd4FAnlaUgt5N/jANBgkqhkiG9w0BAQsFADA8\nMRMwEQYKCZImiZPyLGQBGRYDR0JMMRMwEQYKCZImiZPyLGQBGRYDQU1FMRAwDgYD\nVQQDEwdhbWVyb290MB4XDTE2MDUyNDIyNTI1NFoXDTI2MDUyNDIyNTcwM1owPDET\nMBEGCgmSJomT8ixkARkWA0dCTDETMBEGCgmSJomT8ixkARkWA0FNRTEQMA4GA1UE\nAxMHYW1lcm9vdDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALv4uChY\noVuO+bxBOcn8v4FajoGkxo0YgVwEqEPDVPI6vzmnEqHVhQ1GMVeDyiRrgQT1vCk1\nHMMzo9LlWowPrzbXOwjOTFbXc36+UU41yNN2GeNa49RXbAkfbzKE/SYLfbqOD0dN\nZLwvOhgIb25oA1eAxW/DI/hvJLLKh2SscvkIyd3o2BUeFm7NtyYG/buCKJh8lOq8\n0iBwRoEoInb0vhorHaswSMmqY1g+AJndY/M7uGUqkhDGBhLu53bU9wbUPHsEI+wa\nq6WypCijZYT+C4BS5GJrEPZ2O92pztd+ULqhzNRoPj5RuElUww7+z5RnbCaupyBY\nOmmJMH30EiRSq8dK/irixXXwJraSywR5kyfmAkv6GYWlRlxFUiK3/co47JLA3TDK\nN0wfutbpqxdZQYyGfO2nZrr5JbKfSU0sMtOZDkK6hlafV++hfkVSvFfNHE5B5uN1\nMK6agl1dzi28HfJT9aO7cmjGxl1SJ5qoCvcwZNQ2SPHFdrslcwXEFOMDaEzVOA3V\n7j3+6lrT8sHXg0sErkcd8lrBImfzhLxM/Wh8CgOUNeUu3flUoxmFv3el+QWalSNy\n2SXs2NgWuYE5Iog7CHD/xCnoEnZwwjqLkrro4hYWE4Xj3VlA2Eq+VxqJOgdyFl3m\nckSZ08OcwLeprY4+2GEvCXNGNdXUmNNgk2PvAgMBAAGjVDBSMAsGA1UdDwQEAwIB\nhjASBgNVHRMBAf8ECDAGAQH/AgEBMB0GA1UdDgQWBBQpXlFeZK40ueusnA2njHUB\n0QkLKDAQBgkrBgEEAYI3FQEEAwIBADANBgkqhkiG9w0BAQsFAAOCAgEAcznFDnJx\nsXaazFY1DuIPvUaiWS7ELxAVXMGZ7ROjLrDq1FNYVewL4emDqyEIEMFncec8rqyk\nVBvLQA5YqMCxQWJpL0SlgRSknzLh9ZVcQw1TshC49/XV2N/CLOuyInEQwS//46so\nT20Cf8UGUiOK472LZlvM4KchyDR3FTNtmMg0B/LKVjevpX9sk5MiyjjLUj3jtPIP\n7jpsfZDd/BNsg/89kpsIF5O64I7iYFj3MHu9o4UJcEX0hRt7OzUxqa9THTssvzE5\nVkWo8Rtou2T5TobKV6Rr5Ob9wchLXqVtCyZF16voEKheBnalhGUvErI/6VtBwLb7\n13C0JkKLBNMen+HClNliicVIaubnpY2g+AqxOgKBHiZnzq2HhE1qqEUf4VfqahNU\niaXtbtyo54f2dCf9UL9uG9dllN3nxBE/Y/aWF6E1M8Bslj1aYAtfUQ/xlhEXCly6\nzohw697i3XFUt76RwvfW8quvqdH9Mx0PBpYo4wJJRwAecSJQNy6wIJhAuDgOemXJ\nYViBi/bDnhPcFEVQxsypQSw91BUw7Mxh+W59H5MC25SAIw9fLMT9LRqSYpPyasNp\n4nACjR+bv/6cI+ICOrGmD2mrk2c4dNnYpDx96FfX/Y158RV0wotqIglACk6m1qyo\nyTra6P0Kvo6xz4KaVm8F7VDzUP+heAAhPAs=\n-----END CERTIFICATE-----\n")
// Validator for Azure trusted launch VM attestation. // Validator for Azure trusted launch VM attestation.
type Validator struct { type Validator struct {
oid.AzureTrustedLaunch oid.AzureTrustedLaunch
*vtpm.Validator *vtpm.Validator
roots *x509.CertPool
} }
// NewValidator initializes a new Azure validator with the provided PCR values. // NewValidator initializes a new Azure validator with the provided PCR values.
func NewValidator(pcrs map[uint32][]byte, enforcedPCRs []uint32, log vtpm.WarnLogger) *Validator { func NewValidator(pcrs map[uint32][]byte, enforcedPCRs []uint32, log vtpm.WarnLogger) *Validator {
return &Validator{ rootPool := x509.NewCertPool()
Validator: vtpm.NewValidator( rootPool.AddCert(ameRoot)
v := &Validator{roots: rootPool}
v.Validator = vtpm.NewValidator(
pcrs, pcrs,
enforcedPCRs, enforcedPCRs,
trustedKey, v.verifyAttestationKey,
validateVM, validateVM,
vtpm.VerifyPKCS1v15, vtpm.VerifyPKCS1v15,
log, log,
), )
} return v
} }
// trustedKey returns the key encoded in the given TPMT_PUBLIC message. // verifyAttestationKey establishes trust in an attestation key.
func trustedKey(akPub, instanceInfo []byte) (crypto.PublicKey, error) { // It does so by verifying the certificate chain of the attestation key certificate.
func (v *Validator) verifyAttestationKey(akPub, instanceInfo []byte) (crypto.PublicKey, error) {
pubArea, err := tpm2.DecodePublic(akPub) pubArea, err := tpm2.DecodePublic(akPub)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("decoding attestation key public area: %w", err)
} }
return pubArea.Key()
var akSigner akSigner
if err := json.Unmarshal(instanceInfo, &akSigner); err != nil {
return nil, fmt.Errorf("unmarshaling attestation key signer info: %w", err)
}
akCert, err := x509.ParseCertificate(akSigner.AkCert)
if err != nil {
return nil, fmt.Errorf("parsing attestation key certificate: %w", err)
}
akCertCA, err := x509.ParseCertificate(akSigner.CA)
if err != nil {
return nil, fmt.Errorf("parsing attestation key CA certificate: %w", err)
}
intermediates := x509.NewCertPool()
intermediates.AddCert(akCertCA)
if _, err := akCert.Verify(x509.VerifyOptions{
Roots: v.roots,
Intermediates: intermediates,
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
}); err != nil {
return nil, fmt.Errorf("verifying attestation key certificate: %w", err)
}
pubKey, err := pubArea.Key()
if err != nil {
return nil, fmt.Errorf("getting public key: %w", err)
}
pubKeyRSA, ok := pubKey.(*rsa.PublicKey)
if !ok {
return nil, errors.New("attestation key is not an RSA key")
}
if !pubKeyRSA.Equal(akCert.PublicKey) {
return nil, errors.New("certificate public key does not match attestation key")
}
return pubKeyRSA, nil
} }
// validateVM returns nil. // validateVM returns nil.
func validateVM(attestation vtpm.AttestationDocument) error { func validateVM(attestation vtpm.AttestationDocument) error {
return nil return nil
} }
func mustParseX509(pem string) *x509.Certificate {
cert, err := certutil.PemToX509Cert([]byte(pem))
if err != nil {
panic(err)
}
return cert
}

View File

@ -1,81 +0,0 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package trustedlaunch
import (
"testing"
"github.com/edgelesssys/constellation/v2/internal/attestation/simulator"
"github.com/edgelesssys/constellation/v2/internal/attestation/vtpm"
"github.com/google/go-tpm-tools/client"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestTrustedKeyFromSNP(t *testing.T) {
require := require.New(t)
tpm, err := simulator.OpenSimulatedTPM()
require.NoError(err)
defer tpm.Close()
key, err := client.AttestationKeyRSA(tpm)
require.NoError(err)
defer key.Close()
akPub, err := key.PublicArea().Encode()
require.NoError(err)
testCases := map[string]struct {
key []byte
instanceInfo []byte
wantErr bool
}{
"success": {
key: akPub,
instanceInfo: []byte{},
wantErr: false,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
key, err := trustedKey(tc.key, tc.instanceInfo)
if tc.wantErr {
assert.Error(err)
} else {
assert.NoError(err)
assert.NotNil(key)
}
})
}
}
func TestValidateAzureCVM(t *testing.T) {
testCases := map[string]struct {
attDoc vtpm.AttestationDocument
wantErr bool
}{
"success": {
attDoc: vtpm.AttestationDocument{},
wantErr: false,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
err := validateVM(tc.attDoc)
if tc.wantErr {
assert.Error(err)
} else {
assert.NoError(err)
}
})
}
}

View File

@ -45,7 +45,7 @@ const (
// These images are built in a way that they support all versions currently listed in VersionConfigs. // These images are built in a way that they support all versions currently listed in VersionConfigs.
KonnectivityAgentImage = "us.gcr.io/k8s-artifacts-prod/kas-network-proxy/proxy-agent:v0.0.32" KonnectivityAgentImage = "us.gcr.io/k8s-artifacts-prod/kas-network-proxy/proxy-agent:v0.0.32"
KonnectivityServerImage = "registry.k8s.io/kas-network-proxy/proxy-server:v0.0.32" KonnectivityServerImage = "registry.k8s.io/kas-network-proxy/proxy-server:v0.0.32"
JoinImage = "ghcr.io/edgelesssys/constellation/join-service:v2.0.0" JoinImage = "ghcr.io/edgelesssys/constellation/join-service:v2.1.0-pre.0.20220928143744-c46d6e390f2d"
AccessManagerImage = "ghcr.io/edgelesssys/constellation/access-manager:v2.0.0" AccessManagerImage = "ghcr.io/edgelesssys/constellation/access-manager:v2.0.0"
KmsImage = "ghcr.io/edgelesssys/constellation/kmsserver:v2.0.0" KmsImage = "ghcr.io/edgelesssys/constellation/kmsserver:v2.0.0"
VerificationImage = "ghcr.io/edgelesssys/constellation/verification-service:v2.0.0" VerificationImage = "ghcr.io/edgelesssys/constellation/verification-service:v2.0.0"