Skip to content

Commit

Permalink
feat: support AWS KMS for the SecureBoot signing
Browse files Browse the repository at this point in the history
Fixes #8197

Signed-off-by: pardomue <edgar_ruben.pardo_munoz@roche.com>
Signed-off-by: Andrey Smirnov <andrey.smirnov@siderolabs.com>
  • Loading branch information
pardomue authored and smira committed Feb 16, 2024
1 parent 7ee999f commit 5372188
Show file tree
Hide file tree
Showing 7 changed files with 306 additions and 4 deletions.
1 change: 1 addition & 0 deletions go.mod
Expand Up @@ -35,6 +35,7 @@ require (
github.com/BurntSushi/toml v1.3.2
github.com/aws/aws-sdk-go-v2/config v1.27.0
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.0
github.com/aws/aws-sdk-go-v2/service/kms v1.28.1
github.com/aws/smithy-go v1.20.0
github.com/beevik/ntp v1.3.1
github.com/benbjohnson/clock v1.3.5 // project archived on 2023-05-18
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Expand Up @@ -107,6 +107,8 @@ github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.0 h1:a33HuFl
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.0/go.mod h1:SxIkWpByiGbhbHYTo9CMTUnx2G4p4ZQMrDPcRRy//1c=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.0 h1:SHN/umDLTmFTmYfI+gkanz6da3vK8Kvj/5wkqnTHbuA=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.0/go.mod h1:l8gPU5RYGOFHJqWEpPMoRTP0VoaWQSkJdKo+hwWnnDA=
github.com/aws/aws-sdk-go-v2/service/kms v1.28.1 h1:+KE6+fDNH9gwg/t6DRddIZW7MJVqf3/IdZqeNTFehuA=
github.com/aws/aws-sdk-go-v2/service/kms v1.28.1/go.mod h1:Y/mkxhbaWCswchbBBLRwet6uYKl/026DZXS87c0DmuU=
github.com/aws/aws-sdk-go-v2/service/sso v1.19.0 h1:u6OkVDxtBPnxPkZ9/63ynEe+8kHbtS5IfaC4PzVxzWM=
github.com/aws/aws-sdk-go-v2/service/sso v1.19.0/go.mod h1:YqbU3RS/pkDVu+v+Nwxvn0i1WB0HkNWEePWbmODEbbs=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.22.0 h1:6DL0qu5+315wbsAEEmzK+P9leRwNbkp+lGjPC+CEvb8=
Expand Down
32 changes: 28 additions & 4 deletions pkg/imager/profile/input.go
Expand Up @@ -21,6 +21,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"
Expand Down Expand Up @@ -92,24 +93,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.
Expand All @@ -119,6 +139,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, errors.New("unsupported PCR signer")
}
Expand All @@ -131,6 +153,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, errors.New("unsupported PCR signer")
}
Expand Down
23 changes: 23 additions & 0 deletions 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
}
38 changes: 38 additions & 0 deletions 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)
}
151 changes: 151 additions & 0 deletions 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
}
63 changes: 63 additions & 0 deletions 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
}

0 comments on commit 5372188

Please sign in to comment.