From 7d1378240e7aa65dde45e67397c62e78259bbe78 Mon Sep 17 00:00:00 2001 From: pardomue Date: Thu, 25 Jan 2024 17:06:33 +0100 Subject: [PATCH] feat: support AWS KMS for the SecureBoot signing Fixes #8197 Signed-off-by: pardomue Signed-off-by: Andrey Smirnov (cherry picked from commit 53721883d50bd9979edeb4f94a0f1cfcf74d4d80) --- go.mod | 7 +- go.sum | 14 +- pkg/imager/profile/input.go | 32 +++- pkg/imager/profile/internal/signer/aws/aws.go | 23 +++ .../profile/internal/signer/aws/aws_test.go | 38 +++++ pkg/imager/profile/internal/signer/aws/pcr.go | 151 ++++++++++++++++++ .../profile/internal/signer/aws/secureboot.go | 63 ++++++++ 7 files changed, 315 insertions(+), 13 deletions(-) create mode 100644 pkg/imager/profile/internal/signer/aws/aws.go create mode 100644 pkg/imager/profile/internal/signer/aws/aws_test.go create mode 100644 pkg/imager/profile/internal/signer/aws/pcr.go create mode 100644 pkg/imager/profile/internal/signer/aws/secureboot.go diff --git a/go.mod b/go.mod index bccda377a3..843654a1ef 100644 --- a/go.mod +++ b/go.mod @@ -34,6 +34,7 @@ require ( github.com/BurntSushi/toml v1.3.2 github.com/aws/aws-sdk-go-v2/config v1.25.6 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.5 + github.com/aws/aws-sdk-go-v2/service/kms v1.26.5 github.com/aws/smithy-go v1.17.0 github.com/beevik/ntp v1.3.0 github.com/benbjohnson/clock v1.3.5 // project archived on 2023-05-18 @@ -173,10 +174,10 @@ require ( github.com/ProtonMail/gopenpgp/v2 v2.7.4 // indirect github.com/adrg/xdg v0.4.0 // indirect github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 // indirect - github.com/aws/aws-sdk-go-v2 v1.23.1 // indirect + github.com/aws/aws-sdk-go-v2 v1.23.2 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.16.5 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.4 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.4 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.5 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.5 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.7.1 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.1 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.4 // indirect diff --git a/go.sum b/go.sum index 56535d5402..c6e8ed3da0 100644 --- a/go.sum +++ b/go.sum @@ -86,24 +86,26 @@ github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2/go.mod h1:3U/XgcO3hC github.com/armon/go-proxyproto v0.0.0-20210323213023-7e956b284f0a/go.mod h1:QmP9hvJ91BbJmGVGSbutW19IC0Q9phDCLGaomwTJbgU= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= -github.com/aws/aws-sdk-go-v2 v1.23.1 h1:qXaFsOOMA+HsZtX8WoCa+gJnbyW7qyFFBlPqvTSzbaI= -github.com/aws/aws-sdk-go-v2 v1.23.1/go.mod h1:i1XDttT4rnf6vxc9AuskLc6s7XBee8rlLilKlc03uAA= +github.com/aws/aws-sdk-go-v2 v1.23.2 h1:UoTll1Y5b88x8h53OlsJGgOHwpggdMr7UVnLjMb3XYg= +github.com/aws/aws-sdk-go-v2 v1.23.2/go.mod h1:i1XDttT4rnf6vxc9AuskLc6s7XBee8rlLilKlc03uAA= github.com/aws/aws-sdk-go-v2/config v1.25.6 h1:p7b0sR6lHVNNOK/dE4xZgq2R+NNFRjtAXy8WNE6jbpo= github.com/aws/aws-sdk-go-v2/config v1.25.6/go.mod h1:E/nt0ERX9ZX2RCcJWBax94jFn738UERvjSn4R3msEeQ= github.com/aws/aws-sdk-go-v2/credentials v1.16.5 h1:oJz7X2VzKl8Y9pX7Fa5sIy4+3OnknF+Ne0KYu7DCoQQ= github.com/aws/aws-sdk-go-v2/credentials v1.16.5/go.mod h1:2HvVzcP9ih6XR66omXIsgWjtolkL0MlQVqPcK3nXK+E= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.5 h1:KehRNiVzIfAcj6gw98zotVbb/K67taJE0fkfgM6vzqU= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.5/go.mod h1:VhnExhw6uXy9QzetvpXDolo1/hjhx4u9qukBGkuUwjs= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.4 h1:LAm3Ycm9HJfbSCd5I+wqC2S9Ej7FPrgr5CQoOljJZcE= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.4/go.mod h1:xEhvbJcyUf/31yfGSQBe01fukXwXJ0gxDp7rLfymWE0= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.4 h1:4GV0kKZzUxiWxSVpn/9gwR0g21NF1Jsyduzo9rHgC/Q= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.4/go.mod h1:dYvTNAggxDZy6y1AF7YDwXsPuHFy/VNEpEI/2dWK9IU= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.5 h1:16Z1XuMUv63fcyW5bIUno6AFcX4drsrE0gof+xue6g4= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.5/go.mod h1:pRvFacV2qbRKy34ZFptHZW4wpauJA445bqFbvA6ikSo= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.5 h1:RxpMuBgzP3Dj1n5CZY6droLFcsn5gc7QsrIcaGQoeCs= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.5/go.mod h1:dO8Js7ym4Jzg/wcjTgCRVln/jFn3nI82XNhsG2lWbDI= github.com/aws/aws-sdk-go-v2/internal/ini v1.7.1 h1:uR9lXYjdPX0xY+NhvaJ4dD8rpSRz5VY81ccIIoNG+lw= github.com/aws/aws-sdk-go-v2/internal/ini v1.7.1/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.1 h1:rpkF4n0CyFcrJUG/rNNohoTmhtWlFTRI4BsZOh9PvLs= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.1/go.mod h1:l9ymW25HOqymeU2m1gbUQ3rUIsTwKs8gYHXkqDQUhiI= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.4 h1:rdovz3rEu0vZKbzoMYPTehp0E8veoE9AyfzqCr5Eeao= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.4/go.mod h1:aYCGNjyUCUelhofxlZyj63srdxWUSsBSGg5l6MCuXuE= +github.com/aws/aws-sdk-go-v2/service/kms v1.26.5 h1:MRNoQVbEtjzhYFeKVMifHae4K5q4FuK9B7tTDskIF/g= +github.com/aws/aws-sdk-go-v2/service/kms v1.26.5/go.mod h1:gfe6e+rOxaiz/gr5Myk83ruBD6F9WvM7TZbLjcTNsDM= github.com/aws/aws-sdk-go-v2/service/sso v1.17.4 h1:WSMiDIMaDGyIiXwruNITU0IJF0d0foXwjxpxRylamqQ= github.com/aws/aws-sdk-go-v2/service/sso v1.17.4/go.mod h1:oA6VjNsLll2eVuUoF2D+CMyORgNzPEW/3PyUdq6WQjI= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.20.2 h1:GsrlsvTPBNxHvE3KBCwUMnR76MTO/6qnnO1ILSUOpTA= diff --git a/pkg/imager/profile/input.go b/pkg/imager/profile/input.go index 84a689fa37..b20f401bf5 100644 --- a/pkg/imager/profile/input.go +++ b/pkg/imager/profile/input.go @@ -20,6 +20,7 @@ import ( "github.com/siderolabs/talos/internal/pkg/secureboot/measure" "github.com/siderolabs/talos/internal/pkg/secureboot/pesign" "github.com/siderolabs/talos/pkg/archiver" + "github.com/siderolabs/talos/pkg/imager/profile/internal/signer/aws" "github.com/siderolabs/talos/pkg/imager/profile/internal/signer/azure" "github.com/siderolabs/talos/pkg/imager/profile/internal/signer/file" "github.com/siderolabs/talos/pkg/images" @@ -91,24 +92,43 @@ type SecureBootAssets struct { // SigningKeyAndCertificate describes a signing key & certificate. type SigningKeyAndCertificate struct { - // File-based: + // File-based. + // + // Static key and certificate paths. KeyPath string `yaml:"keyPath,omitempty"` CertPath string `yaml:"certPath,omitempty"` - // Azure: + // Azure. + // + // Azure Vault URL and certificate ID, key will be found from the certificate. AzureVaultURL string `yaml:"azureVaultURL,omitempty"` AzureCertificateID string `yaml:"azureCertificateID,omitempty"` + // AWS. + // + // AWS KMS Key ID and region. + // AWS doesn't have a good way to store a certificate, so it's expected to be a file. + AwsKMSKeyID string `yaml:"awsKMSKeyID,omitempty"` + AwsRegion string `yaml:"awsRegion,omitempty"` + AwsCertPath string `yaml:"awsCertPath,omitempty"` } // SigningKey describes a signing key. type SigningKey struct { - // File-based: + // File-based. + // + // Static key path. KeyPath string `yaml:"keyPath,omitempty"` - // Azure: + // Azure. // + // Azure Vault URL and key ID. // AzureKeyVersion might be left empty to use the latest key version. AzureVaultURL string `yaml:"azureVaultURL,omitempty"` AzureKeyID string `yaml:"azureKeyID,omitempty"` AzureKeyVersion string `yaml:"azureKeyVersion,omitempty"` + // AWS. + // + // AWS KMS Key ID and region. + AwsKMSKeyID string `yaml:"awsKMSKeyID,omitempty"` + AwsRegion string `yaml:"awsRegion,omitempty"` } // GetSigner returns the signer. @@ -118,6 +138,8 @@ func (key SigningKey) GetSigner(ctx context.Context) (measure.RSAKey, error) { return file.NewPCRSigner(key.KeyPath) case key.AzureVaultURL != "" && key.AzureKeyID != "": return azure.NewPCRSigner(ctx, key.AzureVaultURL, key.AzureKeyID, key.AzureKeyVersion) + case key.AwsKMSKeyID != "": + return aws.NewPCRSigner(ctx, key.AwsKMSKeyID, key.AwsRegion) default: return nil, fmt.Errorf("unsupported PCR signer") } @@ -130,6 +152,8 @@ func (keyAndCert SigningKeyAndCertificate) GetSigner(ctx context.Context) (pesig return file.NewSecureBootSigner(keyAndCert.CertPath, keyAndCert.KeyPath) case keyAndCert.AzureVaultURL != "" && keyAndCert.AzureCertificateID != "": return azure.NewSecureBootSigner(ctx, keyAndCert.AzureVaultURL, keyAndCert.AzureCertificateID, keyAndCert.AzureCertificateID) + case keyAndCert.AwsKMSKeyID != "" && keyAndCert.AwsCertPath != "": + return aws.NewSecureBootSigner(ctx, keyAndCert.AwsKMSKeyID, keyAndCert.AwsRegion, keyAndCert.AwsCertPath) default: return nil, fmt.Errorf("unsupported PCR signer") } diff --git a/pkg/imager/profile/internal/signer/aws/aws.go b/pkg/imager/profile/internal/signer/aws/aws.go new file mode 100644 index 0000000000..1ec7cb851b --- /dev/null +++ b/pkg/imager/profile/internal/signer/aws/aws.go @@ -0,0 +1,23 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +// Package aws implements SecureBoot/PCR signers via AWS Key Management Service. +package aws + +import ( + "context" + "fmt" + + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/kms" +) + +func getKmsClient(ctx context.Context, awsRegion string) (*kms.Client, error) { + awsCfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(awsRegion)) + if err != nil { + return nil, fmt.Errorf("error initializing AWS default config: %w", err) + } + + return kms.NewFromConfig(awsCfg), nil +} diff --git a/pkg/imager/profile/internal/signer/aws/aws_test.go b/pkg/imager/profile/internal/signer/aws/aws_test.go new file mode 100644 index 0000000000..6be343b36a --- /dev/null +++ b/pkg/imager/profile/internal/signer/aws/aws_test.go @@ -0,0 +1,38 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package aws_test + +import ( + "context" + "crypto/sha256" + "os" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/siderolabs/talos/pkg/imager/profile/internal/signer/aws" +) + +func TestIntegration(t *testing.T) { + for _, envVar := range []string{"AWS_KMS_KEY_ID", "AWS_REGION", "AWS_CERT_PATH"} { + if os.Getenv(envVar) == "" { + t.Skipf("%s not set", envVar) + } + } + + signer, err := aws.NewPCRSigner(context.TODO(), os.Getenv("AWS_KMS_KEY_ID"), os.Getenv("AWS_REGION")) + require.NoError(t, err) + + digest := sha256.Sum256(nil) + + _, err = signer.Sign(nil, digest[:], nil) + require.NoError(t, err) + + sbSigner, err := aws.NewSecureBootSigner(context.TODO(), os.Getenv("AWS_KMS_KEY_ID"), os.Getenv("AWS_REGION"), os.Getenv("AWS_CERT_PATH")) + require.NoError(t, err) + + _, err = sbSigner.Signer().Sign(nil, digest[:], nil) + require.NoError(t, err) +} diff --git a/pkg/imager/profile/internal/signer/aws/pcr.go b/pkg/imager/profile/internal/signer/aws/pcr.go new file mode 100644 index 0000000000..117e7b02a3 --- /dev/null +++ b/pkg/imager/profile/internal/signer/aws/pcr.go @@ -0,0 +1,151 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package aws + +import ( + "context" + "crypto" + "crypto/rsa" + "crypto/x509" + "fmt" + "io" + "math/big" + + "github.com/aws/aws-sdk-go-v2/service/kms" + "github.com/aws/aws-sdk-go-v2/service/kms/types" + + "github.com/siderolabs/talos/internal/pkg/secureboot/measure" +) + +// KeySigner implements measure.RSAKey interface. +// +// KeySigner wraps Azure APIs to provide public key and crypto.Signer interface out of Azure Key Vault RSA key. +type KeySigner struct { + keyName string + mode mode + + client *kms.Client + publicKey *rsa.PublicKey +} + +var algMap = map[mode]map[crypto.Hash]types.SigningAlgorithmSpec{ + rsaPKCS1v15: { + crypto.SHA256: types.SigningAlgorithmSpecRsassaPkcs1V15Sha256, + crypto.SHA384: types.SigningAlgorithmSpecRsassaPkcs1V15Sha384, + crypto.SHA512: types.SigningAlgorithmSpecRsassaPkcs1V15Sha512, + }, + rsaPSS: { + crypto.SHA256: types.SigningAlgorithmSpecRsassaPssSha256, + crypto.SHA384: types.SigningAlgorithmSpecRsassaPssSha384, + crypto.SHA512: types.SigningAlgorithmSpecRsassaPssSha512, + }, + ecdsa: { + crypto.SHA256: types.SigningAlgorithmSpecEcdsaSha256, + crypto.SHA384: types.SigningAlgorithmSpecEcdsaSha384, + crypto.SHA512: types.SigningAlgorithmSpecEcdsaSha512, + }, +} + +type mode string + +const ( + rsaPKCS1v15 mode = "pkcs1v15" + rsaPSS mode = "pss" + ecdsa mode = "ecdsa" +) + +// PublicRSAKey returns the public key. +func (s *KeySigner) PublicRSAKey() *rsa.PublicKey { + return s.publicKey +} + +// Public returns the public key. +func (s *KeySigner) Public() crypto.PublicKey { + return s.PublicRSAKey() +} + +// Sign implements the crypto.Signer interface. +func (s *KeySigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { + mode := s.mode + + inner := algMap[mode] + if inner == nil { + return nil, fmt.Errorf("mode not supported") + } + + hf := crypto.SHA256 + + if opts != nil { + hf = opts.HashFunc() + } + + algorithm := inner[hf] + if algorithm == "" { + return nil, fmt.Errorf("algorithm not supported") + } + + resp, err := s.client.Sign(context.Background(), &kms.SignInput{ + KeyId: &s.keyName, + Message: digest, + MessageType: types.MessageTypeDigest, + SigningAlgorithm: algorithm, + }) + if err != nil { + return nil, err + } + + return resp.Signature, nil +} + +// Verify interface. +var _ measure.RSAKey = (*KeySigner)(nil) + +// NewPCRSigner creates a new PCR signer from AWS settings. +func NewPCRSigner(ctx context.Context, kmsKeyID, awsRegion string) (*KeySigner, error) { + client, err := getKmsClient(ctx, awsRegion) + if err != nil { + return nil, fmt.Errorf("failed to build AWS kms client: %w", err) + } + + keyResponse, err := client.GetPublicKey(ctx, &kms.GetPublicKeyInput{ + KeyId: &kmsKeyID, + }) + if err != nil { + return nil, fmt.Errorf("failed to get key: %w", err) + } + + if keyResponse.KeyUsage != "SIGN_VERIFY" { + return nil, fmt.Errorf("key usage is not SIGN_VERIFY") + } + + switch keyResponse.KeySpec { //nolint:exhaustive + case types.KeySpecRsa2048, types.KeySpecRsa3072, types.KeySpecRsa4096: + // expected, continue + default: + return nil, fmt.Errorf("key type is not RSA") + } + + parsedKey, err := x509.ParsePKIXPublicKey(keyResponse.PublicKey) + if err != nil { + return nil, fmt.Errorf("Public key is not valid: %w", err) + } + + rsaKey := parsedKey.(*rsa.PublicKey) //nolint:errcheck + if rsaKey.E == 0 { + return nil, fmt.Errorf("property e is empty") + } + + if rsaKey.N.Cmp(big.NewInt(0)) == 0 { + return nil, fmt.Errorf("property N is empty") + } + + return &KeySigner{ + keyName: kmsKeyID, + mode: rsaPKCS1v15, // TODO: make this configurable + + publicKey: rsaKey, + client: client, + }, nil +} diff --git a/pkg/imager/profile/internal/signer/aws/secureboot.go b/pkg/imager/profile/internal/signer/aws/secureboot.go new file mode 100644 index 0000000000..8c17d48d08 --- /dev/null +++ b/pkg/imager/profile/internal/signer/aws/secureboot.go @@ -0,0 +1,63 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package aws + +import ( + "context" + "crypto" + "crypto/x509" + "encoding/pem" + "fmt" + "os" + + "github.com/siderolabs/talos/internal/pkg/secureboot/pesign" +) + +// SecureBootSigner implements pesign.CertificateSigner interface. +type SecureBootSigner struct { + keySigner *KeySigner + cert *x509.Certificate +} + +// Verify interface. +var _ pesign.CertificateSigner = (*SecureBootSigner)(nil) + +// Signer returns the signer. +func (s *SecureBootSigner) Signer() crypto.Signer { + return s.keySigner +} + +// Certificate returns the certificate. +func (s *SecureBootSigner) Certificate() *x509.Certificate { + return s.cert +} + +// NewSecureBootSigner creates a new SecureBootSigner. +func NewSecureBootSigner(ctx context.Context, kmsKeyID, awsRegion, certPath string) (*SecureBootSigner, error) { + keySigner, err := NewPCRSigner(ctx, kmsKeyID, awsRegion) + if err != nil { + return nil, fmt.Errorf("failed to initialize certificate key signer (kms): %w", err) + } + + certData, err := os.ReadFile(certPath) + if err != nil { + return nil, err + } + + certBlock, _ := pem.Decode(certData) + if certBlock == nil { + return nil, fmt.Errorf("failed to decode certificate") + } + + cert, err := x509.ParseCertificate(certBlock.Bytes) + if err != nil { + return nil, fmt.Errorf("failed to parse certificate: %w", err) + } + + return &SecureBootSigner{ + keySigner: keySigner, + cert: cert, + }, nil +}