mirror of
https://github.com/edgelesssys/constellation.git
synced 2024-10-01 01:36:09 -04:00
AB#2444 Verify Azure trusted launch attestation keys (#203)
Signed-off-by: Daniel Weiße <dw@edgeless.systems>
This commit is contained in:
parent
a60e76e91f
commit
acdcb535c0
@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Loadbalancer for control-plane recovery
|
||||
- K8s conformance mode
|
||||
- Local cluster creation based on QEMU
|
||||
- Verification of Azure trusted launch attestation keys
|
||||
|
||||
### Changed
|
||||
<!-- For changes in existing functionality. -->
|
||||
|
5
go.mod
5
go.mod
@ -30,7 +30,10 @@ replace (
|
||||
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 (
|
||||
cloud.google.com/go/compute v1.7.0
|
||||
|
8
go.sum
8
go.sum
@ -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/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
|
||||
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/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=
|
||||
@ -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-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-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/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.3.0 h1:ADtq8RKfP+jrTyIWIZDIYcKOMecRqNJFOew2IT0Inus=
|
||||
github.com/google/go-tspi v0.3.0/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5by8CiHI=
|
||||
|
@ -25,6 +25,7 @@ const (
|
||||
lenSnpReport = 0x4a0
|
||||
lenSnpReportRuntimeDataPadding = 0x14
|
||||
tpmReportIdx = 0x01400001
|
||||
tpmAkIdx = 0x81000003
|
||||
)
|
||||
|
||||
// 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)
|
||||
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:])
|
||||
@ -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.
|
||||
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.
|
||||
// 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)
|
||||
ak, err := tpmclient.LoadCachedKey(tpm, tpmAkIdx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading HCL attestation key from TPM: %w", err)
|
||||
}
|
||||
|
@ -14,7 +14,9 @@ import (
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
tpmclient "github.com/google/go-tpm-tools/client"
|
||||
"github.com/google/go-tpm-tools/simulator"
|
||||
"github.com/google/go-tpm/tpm2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"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
|
||||
// only retrieves the attestation key from the TPM.
|
||||
func TestGetHCLAttestationKey(t *testing.T) {
|
||||
require := require.New(t)
|
||||
assert := assert.New(t)
|
||||
|
||||
tpm, err := simulator.Get()
|
||||
assert.NoError(err)
|
||||
require.NoError(err)
|
||||
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)
|
||||
assert.NoError(err)
|
||||
}
|
||||
|
@ -7,31 +7,117 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
package trustedlaunch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/internal/attestation/vtpm"
|
||||
"github.com/edgelesssys/constellation/v2/internal/oid"
|
||||
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.
|
||||
type Issuer struct {
|
||||
oid.AzureTrustedLaunch
|
||||
*vtpm.Issuer
|
||||
hClient httpClient
|
||||
}
|
||||
|
||||
// NewIssuer initializes a new Azure Issuer.
|
||||
func NewIssuer() *Issuer {
|
||||
return &Issuer{
|
||||
Issuer: vtpm.NewIssuer(
|
||||
vtpm.OpenVTPM,
|
||||
tpmclient.AttestationKeyRSA,
|
||||
getAttestation,
|
||||
),
|
||||
i := &Issuer{
|
||||
hClient: &http.Client{},
|
||||
}
|
||||
i.Issuer = vtpm.NewIssuer(
|
||||
vtpm.OpenVTPM,
|
||||
getAttestationKey,
|
||||
i.getAttestationCert,
|
||||
)
|
||||
return i
|
||||
}
|
||||
|
||||
// getAttestation returns nil.
|
||||
func getAttestation(tpm io.ReadWriteCloser) ([]byte, error) {
|
||||
return nil, nil
|
||||
// akSigner holds the attestation key certificate and the corresponding CA certificate.
|
||||
type akSigner struct {
|
||||
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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
250
internal/attestation/azure/trustedlaunch/trustedlaunch_test.go
Normal file
250
internal/attestation/azure/trustedlaunch/trustedlaunch_test.go
Normal 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
|
||||
}
|
@ -8,42 +8,104 @@ package trustedlaunch
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"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/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.
|
||||
type Validator struct {
|
||||
oid.AzureTrustedLaunch
|
||||
*vtpm.Validator
|
||||
roots *x509.CertPool
|
||||
}
|
||||
|
||||
// NewValidator initializes a new Azure validator with the provided PCR values.
|
||||
func NewValidator(pcrs map[uint32][]byte, enforcedPCRs []uint32, log vtpm.WarnLogger) *Validator {
|
||||
return &Validator{
|
||||
Validator: vtpm.NewValidator(
|
||||
pcrs,
|
||||
enforcedPCRs,
|
||||
trustedKey,
|
||||
validateVM,
|
||||
vtpm.VerifyPKCS1v15,
|
||||
log,
|
||||
),
|
||||
}
|
||||
rootPool := x509.NewCertPool()
|
||||
rootPool.AddCert(ameRoot)
|
||||
v := &Validator{roots: rootPool}
|
||||
v.Validator = vtpm.NewValidator(
|
||||
pcrs,
|
||||
enforcedPCRs,
|
||||
v.verifyAttestationKey,
|
||||
validateVM,
|
||||
vtpm.VerifyPKCS1v15,
|
||||
log,
|
||||
)
|
||||
return v
|
||||
}
|
||||
|
||||
// trustedKey returns the key encoded in the given TPMT_PUBLIC message.
|
||||
func trustedKey(akPub, instanceInfo []byte) (crypto.PublicKey, error) {
|
||||
// verifyAttestationKey establishes trust in an attestation key.
|
||||
// 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)
|
||||
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.
|
||||
func validateVM(attestation vtpm.AttestationDocument) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func mustParseX509(pem string) *x509.Certificate {
|
||||
cert, err := certutil.PemToX509Cert([]byte(pem))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return cert
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -45,7 +45,7 @@ const (
|
||||
// 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"
|
||||
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"
|
||||
KmsImage = "ghcr.io/edgelesssys/constellation/kmsserver:v2.0.0"
|
||||
VerificationImage = "ghcr.io/edgelesssys/constellation/verification-service:v2.0.0"
|
||||
|
Loading…
Reference in New Issue
Block a user