mirror of
https://github.com/siderolabs/talos.git
synced 2026-05-04 20:06:18 +02:00
fix: tpm2 encrypt/decrypt flow
The previous flow was using TPM PCR 11 values to bound the policy which means TPM cannot unseal when UKI changes. Now it's fixed to use PCR 7 which is bound to the SecureBoot state (SecureBoot status and Certificates). This provides a full chain of trust bound to SecureBoot state and signed PCR signature. Also the code has been refactored to use PolicyCalculator from the TPM library. Signed-off-by: Noel Georgi <git@frezbo.dev>
This commit is contained in:
parent
130518de71
commit
166d75fe88
@ -26,7 +26,7 @@ import (
|
||||
"github.com/siderolabs/talos/internal/pkg/mount/switchroot"
|
||||
"github.com/siderolabs/talos/internal/pkg/rng"
|
||||
"github.com/siderolabs/talos/internal/pkg/secureboot"
|
||||
"github.com/siderolabs/talos/internal/pkg/tpm2"
|
||||
"github.com/siderolabs/talos/internal/pkg/secureboot/tpm2"
|
||||
"github.com/siderolabs/talos/pkg/machinery/constants"
|
||||
"github.com/siderolabs/talos/pkg/machinery/extensions"
|
||||
"github.com/siderolabs/talos/pkg/version"
|
||||
@ -61,8 +61,8 @@ func run() (err error) {
|
||||
}
|
||||
|
||||
// extend PCR 11 with enter-initrd
|
||||
if err = tpm2.PCRExtent(constants.UKIMeasuredPCR, []byte(secureboot.EnterInitrd)); err != nil {
|
||||
return fmt.Errorf("failed to extend PCR %d with enter-initrd: %v", constants.UKIMeasuredPCR, err)
|
||||
if err = tpm2.PCRExtent(secureboot.UKIPCR, []byte(secureboot.EnterInitrd)); err != nil {
|
||||
return fmt.Errorf("failed to extend PCR %d with enter-initrd: %v", secureboot.UKIPCR, err)
|
||||
}
|
||||
|
||||
log.Printf("booting Talos %s", version.Tag)
|
||||
|
||||
@ -62,7 +62,7 @@ import (
|
||||
"github.com/siderolabs/talos/internal/pkg/mount"
|
||||
"github.com/siderolabs/talos/internal/pkg/partition"
|
||||
"github.com/siderolabs/talos/internal/pkg/secureboot"
|
||||
"github.com/siderolabs/talos/internal/pkg/tpm2"
|
||||
"github.com/siderolabs/talos/internal/pkg/secureboot/tpm2"
|
||||
"github.com/siderolabs/talos/pkg/conditions"
|
||||
"github.com/siderolabs/talos/pkg/images"
|
||||
krnl "github.com/siderolabs/talos/pkg/kernel"
|
||||
@ -832,7 +832,7 @@ func WriteUdevRules(runtime.Sequence, any) (runtime.TaskExecutionFunc, string) {
|
||||
// StartMachined represents the task to start machined.
|
||||
func StartMachined(_ runtime.Sequence, _ any) (runtime.TaskExecutionFunc, string) {
|
||||
return func(ctx context.Context, logger *log.Logger, r runtime.Runtime) error {
|
||||
if err := tpm2.PCRExtent(constants.UKIMeasuredPCR, []byte(secureboot.EnterMachined)); err != nil {
|
||||
if err := tpm2.PCRExtent(secureboot.UKIPCR, []byte(secureboot.EnterMachined)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -889,7 +889,7 @@ func StartUdevd(runtime.Sequence, any) (runtime.TaskExecutionFunc, string) {
|
||||
// ExtendPCRStartAll represents the task to extend the PCR with the StartTheWorld PCR phase.
|
||||
func ExtendPCRStartAll(runtime.Sequence, any) (runtime.TaskExecutionFunc, string) {
|
||||
return func(ctx context.Context, logger *log.Logger, r runtime.Runtime) (err error) {
|
||||
return tpm2.PCRExtent(constants.UKIMeasuredPCR, []byte(secureboot.StartTheWorld))
|
||||
return tpm2.PCRExtent(secureboot.UKIPCR, []byte(secureboot.StartTheWorld))
|
||||
}, "extendPCRStartAll"
|
||||
}
|
||||
|
||||
|
||||
@ -14,8 +14,8 @@ import (
|
||||
"github.com/siderolabs/go-blockdevice/blockdevice/encryption/luks"
|
||||
"github.com/siderolabs/go-blockdevice/blockdevice/encryption/token"
|
||||
|
||||
"github.com/siderolabs/talos/internal/pkg/tpm2"
|
||||
"github.com/siderolabs/talos/pkg/machinery/constants"
|
||||
"github.com/siderolabs/talos/internal/pkg/secureboot"
|
||||
"github.com/siderolabs/talos/internal/pkg/secureboot/tpm2"
|
||||
)
|
||||
|
||||
// TPMToken is the userdata stored in the partition token metadata.
|
||||
@ -59,7 +59,7 @@ func (h *TPMKeyHandler) NewKey(ctx context.Context) (*encryption.Key, token.Toke
|
||||
KeySlots: []int{h.slot},
|
||||
SealedBlobPrivate: resp.SealedBlobPrivate,
|
||||
SealedBlobPublic: resp.SealedBlobPublic,
|
||||
PCRs: []int{constants.UKIMeasuredPCR},
|
||||
PCRs: []int{secureboot.UKIPCR},
|
||||
Alg: "sha256",
|
||||
PolicyHash: resp.PolicyDigest,
|
||||
KeyName: resp.KeyName,
|
||||
|
||||
@ -15,7 +15,7 @@ import (
|
||||
|
||||
"github.com/siderolabs/talos/internal/pkg/mount"
|
||||
"github.com/siderolabs/talos/internal/pkg/secureboot"
|
||||
"github.com/siderolabs/talos/internal/pkg/tpm2"
|
||||
"github.com/siderolabs/talos/internal/pkg/secureboot/tpm2"
|
||||
"github.com/siderolabs/talos/pkg/machinery/constants"
|
||||
)
|
||||
|
||||
@ -69,8 +69,8 @@ func Switch(prefix string, mountpoints *mount.Points) (err error) {
|
||||
}
|
||||
|
||||
// extend PCR 11 with leave-initrd
|
||||
if err = tpm2.PCRExtent(constants.UKIMeasuredPCR, []byte(secureboot.LeaveInitrd)); err != nil {
|
||||
return fmt.Errorf("failed to extend PCR %d with leave-initrd: %v", constants.UKIMeasuredPCR, err)
|
||||
if err = tpm2.PCRExtent(secureboot.UKIPCR, []byte(secureboot.LeaveInitrd)); err != nil {
|
||||
return fmt.Errorf("failed to extend PCR %d with leave-initrd: %v", secureboot.UKIPCR, err)
|
||||
}
|
||||
|
||||
// Note that /sbin/init is machined. We call it init since this is the
|
||||
|
||||
@ -15,13 +15,13 @@ import (
|
||||
"github.com/google/go-tpm/tpm2"
|
||||
|
||||
"github.com/siderolabs/talos/internal/pkg/secureboot"
|
||||
talostpm2 "github.com/siderolabs/talos/internal/pkg/tpm2"
|
||||
tpm2internal "github.com/siderolabs/talos/internal/pkg/secureboot/tpm2"
|
||||
)
|
||||
|
||||
// CalculateBankData calculates the PCR bank data for a given set of UKI file sections.
|
||||
//
|
||||
// This mimics the process happening happening in the TPM when the UKI is being loaded.
|
||||
func CalculateBankData(pcrNumber int, alg tpm2.TPMAlgID, sectionData map[secureboot.Section]string, rsaKey *rsa.PrivateKey) ([]talostpm2.BankData, error) {
|
||||
func CalculateBankData(pcrNumber int, alg tpm2.TPMAlgID, sectionData map[secureboot.Section]string, rsaKey *rsa.PrivateKey) ([]tpm2internal.BankData, error) {
|
||||
// get fingerprint of public key
|
||||
pubKeyFingerprint := sha256.Sum256(x509.MarshalPKCS1PublicKey(&rsaKey.PublicKey))
|
||||
|
||||
@ -30,7 +30,7 @@ func CalculateBankData(pcrNumber int, alg tpm2.TPMAlgID, sectionData map[secureb
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pcrSelector, err := CreateSelector([]int{secureboot.UKIPCR})
|
||||
pcrSelector, err := tpm2internal.CreateSelector([]int{secureboot.UKIPCR})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create PCR selection: %v", err)
|
||||
}
|
||||
@ -59,7 +59,7 @@ func CalculateBankData(pcrNumber int, alg tpm2.TPMAlgID, sectionData map[secureb
|
||||
}
|
||||
}
|
||||
|
||||
banks := make([]talostpm2.BankData, 0)
|
||||
banks := make([]tpm2internal.BankData, 0)
|
||||
|
||||
for _, phaseInfo := range secureboot.OrderedPhases() {
|
||||
// extend always, but only calculate signature if requested
|
||||
@ -71,7 +71,7 @@ func CalculateBankData(pcrNumber int, alg tpm2.TPMAlgID, sectionData map[secureb
|
||||
|
||||
hash := hashData.Hash()
|
||||
|
||||
policyPCR, err := CalculatePolicy(hash, pcrSelection)
|
||||
policyPCR, err := tpm2internal.CalculatePolicy(hash, pcrSelection)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -81,7 +81,7 @@ func CalculateBankData(pcrNumber int, alg tpm2.TPMAlgID, sectionData map[secureb
|
||||
return nil, err
|
||||
}
|
||||
|
||||
banks = append(banks, talostpm2.BankData{
|
||||
banks = append(banks, tpm2internal.BankData{
|
||||
PCRs: []int{pcrNumber},
|
||||
PKFP: hex.EncodeToString(pubKeyFingerprint[:]),
|
||||
Sig: sigData.SignatureBase64,
|
||||
|
||||
@ -15,7 +15,7 @@ import (
|
||||
|
||||
"github.com/siderolabs/talos/internal/pkg/secureboot"
|
||||
"github.com/siderolabs/talos/internal/pkg/secureboot/measure/internal/pcr"
|
||||
talostpm2 "github.com/siderolabs/talos/internal/pkg/tpm2"
|
||||
tpm2internal "github.com/siderolabs/talos/internal/pkg/secureboot/tpm2"
|
||||
)
|
||||
|
||||
func TestCalculateBankData(t *testing.T) {
|
||||
@ -40,7 +40,7 @@ func TestCalculateBankData(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t,
|
||||
[]talostpm2.BankData{
|
||||
[]tpm2internal.BankData{
|
||||
{
|
||||
PCRs: []int{15},
|
||||
PKFP: "58f58f625bd8a8b6681e4b40688cf99b26419b6b2c5f6e14a2c7c67a3b0b1620",
|
||||
|
||||
@ -1,25 +0,0 @@
|
||||
// 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 pcr contains code that handles PCR operations.
|
||||
package pcr
|
||||
|
||||
import "fmt"
|
||||
|
||||
// CreateSelector converts PCR numbers into a bitmask.
|
||||
func CreateSelector(pcrs []int) ([]byte, error) {
|
||||
const sizeOfPCRSelect = 3
|
||||
|
||||
mask := make([]byte, sizeOfPCRSelect)
|
||||
|
||||
for _, n := range pcrs {
|
||||
if n >= 8*sizeOfPCRSelect {
|
||||
return nil, fmt.Errorf("PCR index %d is out of range (exceeds maximum value %d)", n, 8*sizeOfPCRSelect-1)
|
||||
}
|
||||
|
||||
mask[n>>3] |= 1 << (n & 0x7)
|
||||
}
|
||||
|
||||
return mask, nil
|
||||
}
|
||||
@ -1,34 +0,0 @@
|
||||
// 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 pcr
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
|
||||
"github.com/google/go-tpm/tpm2"
|
||||
)
|
||||
|
||||
// CalculatePolicy calculates the policy hash for a given PCR value and PCR selection.
|
||||
func CalculatePolicy(pcrValue []byte, pcrSelection tpm2.TPMLPCRSelection) ([]byte, error) {
|
||||
calculator, err := tpm2.NewPolicyCalculator(tpm2.TPMAlgSHA256)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pcrHash := sha256.Sum256(pcrValue)
|
||||
|
||||
policy := tpm2.PolicyPCR{
|
||||
PcrDigest: tpm2.TPM2BDigest{
|
||||
Buffer: pcrHash[:],
|
||||
},
|
||||
Pcrs: pcrSelection,
|
||||
}
|
||||
|
||||
if err := policy.Update(calculator); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return calculator.Hash().Digest, nil
|
||||
}
|
||||
@ -1,32 +0,0 @@
|
||||
// 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 pcr_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-tpm/tpm2"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/siderolabs/talos/internal/pkg/secureboot/measure/internal/pcr"
|
||||
)
|
||||
|
||||
func TestCalculatePolicy(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
policy, err := pcr.CalculatePolicy([]byte{1, 3, 5}, tpm2.TPMLPCRSelection{
|
||||
PCRSelections: []tpm2.TPMSPCRSelection{
|
||||
{
|
||||
Hash: tpm2.TPMAlgSHA256,
|
||||
PCRSelect: []byte{10, 11, 12},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t,
|
||||
[]byte{0x84, 0xd6, 0x51, 0x47, 0xb0, 0x53, 0x94, 0xd0, 0xfa, 0xc4, 0x5e, 0x36, 0x0, 0x20, 0x3e, 0x3a, 0x11, 0x1, 0x27, 0xfb, 0xe2, 0x6f, 0xc1, 0xe3, 0x3, 0x3, 0x10, 0x21, 0x33, 0xf9, 0x15, 0xe3},
|
||||
policy,
|
||||
)
|
||||
}
|
||||
@ -18,7 +18,7 @@ import (
|
||||
|
||||
"github.com/siderolabs/talos/internal/pkg/secureboot"
|
||||
"github.com/siderolabs/talos/internal/pkg/secureboot/measure/internal/pcr"
|
||||
talostpm2 "github.com/siderolabs/talos/internal/pkg/tpm2"
|
||||
tpm2internal "github.com/siderolabs/talos/internal/pkg/secureboot/tpm2"
|
||||
)
|
||||
|
||||
// SectionsData holds a map of Section to file path to the corresponding section.
|
||||
@ -45,17 +45,17 @@ func loadRSAKey(path string) (*rsa.PrivateKey, error) {
|
||||
}
|
||||
|
||||
// GenerateSignedPCR generates the PCR signed data for a given set of UKI file sections.
|
||||
func GenerateSignedPCR(sectionsData SectionsData, rsaKeyPath string) (*talostpm2.PCRData, error) {
|
||||
func GenerateSignedPCR(sectionsData SectionsData, rsaKeyPath string) (*tpm2internal.PCRData, error) {
|
||||
rsaKey, err := loadRSAKey(rsaKeyPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data := &talostpm2.PCRData{}
|
||||
data := &tpm2internal.PCRData{}
|
||||
|
||||
for _, algo := range []struct {
|
||||
alg tpm2.TPMAlgID
|
||||
bankDataSetter *[]talostpm2.BankData
|
||||
bankDataSetter *[]tpm2internal.BankData
|
||||
}{
|
||||
{
|
||||
alg: tpm2.TPMAlgSHA1,
|
||||
|
||||
@ -78,5 +78,10 @@ func OrderedPhases() []PhaseInfo {
|
||||
}
|
||||
}
|
||||
|
||||
// UKIPCR is the PCR number where sections except `.pcrsig` are measured.
|
||||
const UKIPCR = 11
|
||||
const (
|
||||
// UKIPCR is the PCR number where sections except `.pcrsig` are measured.
|
||||
UKIPCR = 11
|
||||
// SecureBootStatePCR is the PCR number where the secure boot state and the signature are measured.
|
||||
// PCR 7 changes when UEFI SecureBoot mode is enabled/disabled, or firmware certificates (PK, KEK, db, dbx, …) are updated.
|
||||
SecureBootStatePCR = 7
|
||||
)
|
||||
|
||||
72
internal/pkg/secureboot/tpm2/keys.go
Normal file
72
internal/pkg/secureboot/tpm2/keys.go
Normal file
@ -0,0 +1,72 @@
|
||||
// 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 tpm2 provides TPM2.0 related functionality helpers.
|
||||
package tpm2
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/google/go-tpm/tpm2"
|
||||
)
|
||||
|
||||
// ParsePCRSigningPubKey parses a PEM encoded RSA public key.
|
||||
func ParsePCRSigningPubKey(file string) (*rsa.PublicKey, error) {
|
||||
pcrSigningPubKey, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read pcr signing public key: %v", err)
|
||||
}
|
||||
|
||||
block, _ := pem.Decode(pcrSigningPubKey)
|
||||
if block == nil {
|
||||
return nil, fmt.Errorf("failed to decode pcr signing public key")
|
||||
}
|
||||
|
||||
// parse rsa public key
|
||||
tpm2PubKeyAny, err := x509.ParsePKIXPublicKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tpm2PubKey, ok := tpm2PubKeyAny.(*rsa.PublicKey)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to cast pcr signing public key to rsa")
|
||||
}
|
||||
|
||||
return tpm2PubKey, nil
|
||||
}
|
||||
|
||||
// RSAPubKeyTemplate returns a TPM2.0 public key template for RSA keys.
|
||||
func RSAPubKeyTemplate(bitlen, exponent int, modulus []byte) tpm2.TPMTPublic {
|
||||
return tpm2.TPMTPublic{
|
||||
Type: tpm2.TPMAlgRSA,
|
||||
NameAlg: tpm2.TPMAlgSHA256,
|
||||
ObjectAttributes: tpm2.TPMAObject{
|
||||
Decrypt: true,
|
||||
SignEncrypt: true,
|
||||
UserWithAuth: true,
|
||||
},
|
||||
Parameters: tpm2.NewTPMUPublicParms(tpm2.TPMAlgRSA, &tpm2.TPMSRSAParms{
|
||||
Symmetric: tpm2.TPMTSymDefObject{
|
||||
Algorithm: tpm2.TPMAlgNull,
|
||||
Mode: tpm2.NewTPMUSymMode(tpm2.TPMAlgRSA, tpm2.TPMAlgNull),
|
||||
},
|
||||
Scheme: tpm2.TPMTRSAScheme{
|
||||
Scheme: tpm2.TPMAlgNull,
|
||||
Details: tpm2.NewTPMUAsymScheme(tpm2.TPMAlgRSA, &tpm2.TPMSSigSchemeRSASSA{
|
||||
HashAlg: tpm2.TPMAlgNull,
|
||||
}),
|
||||
},
|
||||
KeyBits: tpm2.TPMKeyBits(bitlen),
|
||||
Exponent: uint32(exponent),
|
||||
}),
|
||||
Unique: tpm2.NewTPMUPublicID(tpm2.TPMAlgRSA, &tpm2.TPM2BPublicKeyRSA{
|
||||
Buffer: modulus,
|
||||
}),
|
||||
}
|
||||
}
|
||||
205
internal/pkg/secureboot/tpm2/pcr.go
Normal file
205
internal/pkg/secureboot/tpm2/pcr.go
Normal file
@ -0,0 +1,205 @@
|
||||
// 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 tpm2 provides TPM2.0 related functionality helpers.
|
||||
package tpm2
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/google/go-tpm/tpm2"
|
||||
"github.com/google/go-tpm/tpm2/transport"
|
||||
|
||||
"github.com/siderolabs/talos/internal/pkg/secureboot"
|
||||
)
|
||||
|
||||
// CreateSelector converts PCR numbers into a bitmask.
|
||||
func CreateSelector(pcrs []int) ([]byte, error) {
|
||||
const sizeOfPCRSelect = 3
|
||||
|
||||
mask := make([]byte, sizeOfPCRSelect)
|
||||
|
||||
for _, n := range pcrs {
|
||||
if n >= 8*sizeOfPCRSelect {
|
||||
return nil, fmt.Errorf("PCR index %d is out of range (exceeds maximum value %d)", n, 8*sizeOfPCRSelect-1)
|
||||
}
|
||||
|
||||
mask[n>>3] |= 1 << (n & 0x7)
|
||||
}
|
||||
|
||||
return mask, nil
|
||||
}
|
||||
|
||||
// ReadPCR reads the value of a single PCR.
|
||||
func ReadPCR(t transport.TPM, pcr int) ([]byte, error) {
|
||||
pcrSelector, err := CreateSelector([]int{pcr})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create PCR selection: %v", err)
|
||||
}
|
||||
|
||||
pcrRead := tpm2.PCRRead{
|
||||
PCRSelectionIn: tpm2.TPMLPCRSelection{
|
||||
PCRSelections: []tpm2.TPMSPCRSelection{
|
||||
{
|
||||
Hash: tpm2.TPMAlgSHA256,
|
||||
PCRSelect: pcrSelector,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
pcrValue, err := pcrRead.Execute(t)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read PCR: %v", err)
|
||||
}
|
||||
|
||||
return pcrValue.PCRValues.Digests[0].Buffer, nil
|
||||
}
|
||||
|
||||
// PCRExtent hashes the input and extends the PCR with the hash.
|
||||
func PCRExtent(pcr int, data []byte) error {
|
||||
t, err := transport.OpenTPM()
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
log.Printf("TPM device is not available, skipping PCR extension")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
defer t.Close() // nolint: errcheck
|
||||
|
||||
// since we are using SHA256, we can assume that the PCR bank is SHA256
|
||||
digest := sha256.Sum256(data)
|
||||
|
||||
pcrHandle := tpm2.PCRExtend{
|
||||
PCRHandle: tpm2.AuthHandle{
|
||||
Handle: tpm2.TPMHandle(pcr),
|
||||
Auth: tpm2.PasswordAuth(nil),
|
||||
},
|
||||
Digests: tpm2.TPMLDigestValues{
|
||||
Digests: []tpm2.TPMTHA{
|
||||
{
|
||||
HashAlg: tpm2.TPMAlgSHA256,
|
||||
Digest: digest[:],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if _, err = pcrHandle.Execute(t); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// PolicyPCRDigest executes policyPCR and returns the digest.
|
||||
func PolicyPCRDigest(t transport.TPM, policyHandle tpm2.TPMHandle, pcrSelection tpm2.TPMLPCRSelection) (*tpm2.TPM2BDigest, error) {
|
||||
policyPCR := tpm2.PolicyPCR{
|
||||
PolicySession: policyHandle,
|
||||
Pcrs: pcrSelection,
|
||||
}
|
||||
|
||||
if _, err := policyPCR.Execute(t); err != nil {
|
||||
return nil, fmt.Errorf("failed to execute policyPCR: %v", err)
|
||||
}
|
||||
|
||||
policyGetDigest := tpm2.PolicyGetDigest{
|
||||
PolicySession: policyHandle,
|
||||
}
|
||||
|
||||
policyGetDigestResponse, err := policyGetDigest.Execute(t)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get policy digest: %v", err)
|
||||
}
|
||||
|
||||
return &policyGetDigestResponse.PolicyDigest, nil
|
||||
}
|
||||
|
||||
// nolint:gocyclo
|
||||
func validatePCRBanks(t transport.TPM) error {
|
||||
pcrValue, err := ReadPCR(t, secureboot.UKIPCR)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read PCR: %v", err)
|
||||
}
|
||||
|
||||
if err = validatePCRNotZeroAndNotFilled(pcrValue, secureboot.UKIPCR); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pcrValue, err = ReadPCR(t, secureboot.SecureBootStatePCR)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read PCR: %v", err)
|
||||
}
|
||||
|
||||
if err = validatePCRNotZeroAndNotFilled(pcrValue, secureboot.SecureBootStatePCR); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
caps := tpm2.GetCapability{
|
||||
Capability: tpm2.TPMCapPCRs,
|
||||
Property: 0,
|
||||
PropertyCount: 1,
|
||||
}
|
||||
|
||||
capsResp, err := caps.Execute(t)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get PCR capabilities: %v", err)
|
||||
}
|
||||
|
||||
assignedPCRs, err := capsResp.CapabilityData.Data.AssignedPCR()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse assigned PCRs: %v", err)
|
||||
}
|
||||
|
||||
for _, s := range assignedPCRs.PCRSelections {
|
||||
h, err := s.Hash.Hash()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse hash algorithm: %v", err)
|
||||
}
|
||||
|
||||
switch h { //nolint:exhaustive
|
||||
case crypto.SHA1:
|
||||
continue
|
||||
case crypto.SHA256:
|
||||
// check if 24 banks are available
|
||||
if len(s.PCRSelect) != 24/8 {
|
||||
return fmt.Errorf("unexpected number of PCR banks: %d", len(s.PCRSelect))
|
||||
}
|
||||
|
||||
// check if all banks are available
|
||||
if s.PCRSelect[0] != 0xff || s.PCRSelect[1] != 0xff || s.PCRSelect[2] != 0xff {
|
||||
return fmt.Errorf("unexpected PCR banks: %v", s.PCRSelect)
|
||||
}
|
||||
case crypto.SHA384:
|
||||
continue
|
||||
case crypto.SHA512:
|
||||
continue
|
||||
default:
|
||||
return fmt.Errorf("unsupported hash algorithm: %s", h.String())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validatePCRNotZeroAndNotFilled(pcrValue []byte, pcr int) error {
|
||||
if bytes.Equal(pcrValue, bytes.Repeat([]byte{0x00}, sha256.Size)) {
|
||||
return fmt.Errorf("PCR bank %d is populated with all zeroes", pcr)
|
||||
}
|
||||
|
||||
if bytes.Equal(pcrValue, bytes.Repeat([]byte{0xFF}, sha256.Size)) {
|
||||
return fmt.Errorf("PCR bank %d is populated with all 0xFF", pcr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -2,14 +2,15 @@
|
||||
// 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 pcr_test
|
||||
// Package tpm2 provides TPM2.0 related functionality helpers.
|
||||
package tpm2_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/siderolabs/talos/internal/pkg/secureboot/measure/internal/pcr"
|
||||
tpm2internal "github.com/siderolabs/talos/internal/pkg/secureboot/tpm2"
|
||||
)
|
||||
|
||||
func TestGetSelection(t *testing.T) {
|
||||
@ -40,7 +41,7 @@ func TestGetSelection(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
actual, err := pcr.CreateSelector(tt.pcrs)
|
||||
actual, err := tpm2internal.CreateSelector(tt.pcrs)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, tt.expected, actual)
|
||||
79
internal/pkg/secureboot/tpm2/policy.go
Normal file
79
internal/pkg/secureboot/tpm2/policy.go
Normal file
@ -0,0 +1,79 @@
|
||||
// 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 tpm2 provides TPM2.0 related functionality helpers.
|
||||
package tpm2
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/go-tpm/tpm2"
|
||||
)
|
||||
|
||||
// CalculatePolicy calculates the policy hash for a given PCR value and PCR selection.
|
||||
func CalculatePolicy(pcrValue []byte, pcrSelection tpm2.TPMLPCRSelection) ([]byte, error) {
|
||||
calculator, err := tpm2.NewPolicyCalculator(tpm2.TPMAlgSHA256)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pcrHash := sha256.Sum256(pcrValue)
|
||||
|
||||
policy := tpm2.PolicyPCR{
|
||||
PcrDigest: tpm2.TPM2BDigest{
|
||||
Buffer: pcrHash[:],
|
||||
},
|
||||
Pcrs: pcrSelection,
|
||||
}
|
||||
|
||||
if err := policy.Update(calculator); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return calculator.Hash().Digest, nil
|
||||
}
|
||||
|
||||
// CalculateSealingPolicyDigest calculates the sealing policy digest for a given PCR value, PCR selection and public key.
|
||||
func CalculateSealingPolicyDigest(pcrValue []byte, pcrSelection tpm2.TPMLPCRSelection, pubKey string) ([]byte, error) {
|
||||
calculator, err := tpm2.NewPolicyCalculator(tpm2.TPMAlgSHA256)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pubKeyData, err := ParsePCRSigningPubKey(pubKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
publicKeyTemplate := RSAPubKeyTemplate(pubKeyData.N.BitLen(), pubKeyData.E, pubKeyData.N.Bytes())
|
||||
|
||||
name, err := tpm2.ObjectName(&publicKeyTemplate)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to calculate name: %v", err)
|
||||
}
|
||||
|
||||
policyAuthorize := tpm2.PolicyAuthorize{
|
||||
KeySign: *name,
|
||||
}
|
||||
|
||||
if err := policyAuthorize.Update(calculator); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pcrHash := sha256.Sum256(pcrValue)
|
||||
|
||||
policy := tpm2.PolicyPCR{
|
||||
PcrDigest: tpm2.TPM2BDigest{
|
||||
Buffer: pcrHash[:],
|
||||
},
|
||||
Pcrs: pcrSelection,
|
||||
}
|
||||
|
||||
if err := policy.Update(calculator); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return calculator.Hash().Digest, nil
|
||||
}
|
||||
51
internal/pkg/secureboot/tpm2/policy_test.go
Normal file
51
internal/pkg/secureboot/tpm2/policy_test.go
Normal file
@ -0,0 +1,51 @@
|
||||
// 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 tpm2 provides TPM2.0 related functionality helpers.
|
||||
package tpm2_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-tpm/tpm2"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
tpm2internal "github.com/siderolabs/talos/internal/pkg/secureboot/tpm2"
|
||||
)
|
||||
|
||||
func TestCalculatePolicy(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
policy, err := tpm2internal.CalculatePolicy([]byte{1, 3, 5}, tpm2.TPMLPCRSelection{
|
||||
PCRSelections: []tpm2.TPMSPCRSelection{
|
||||
{
|
||||
Hash: tpm2.TPMAlgSHA256,
|
||||
PCRSelect: []byte{10, 11, 12},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t,
|
||||
[]byte{0x84, 0xd6, 0x51, 0x47, 0xb0, 0x53, 0x94, 0xd0, 0xfa, 0xc4, 0x5e, 0x36, 0x0, 0x20, 0x3e, 0x3a, 0x11, 0x1, 0x27, 0xfb, 0xe2, 0x6f, 0xc1, 0xe3, 0x3, 0x3, 0x10, 0x21, 0x33, 0xf9, 0x15, 0xe3},
|
||||
policy,
|
||||
)
|
||||
}
|
||||
|
||||
func TestCalculateSealingPolicyDigest(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
calculated, err := tpm2internal.CalculateSealingPolicyDigest([]byte{1, 3, 5}, tpm2.TPMLPCRSelection{
|
||||
PCRSelections: []tpm2.TPMSPCRSelection{
|
||||
{
|
||||
Hash: tpm2.TPMAlgSHA256,
|
||||
PCRSelect: []byte{10, 11, 12},
|
||||
},
|
||||
},
|
||||
}, "testdata/pcr-signing-crt.pem")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t,
|
||||
[]byte{0xa0, 0xf4, 0x29, 0x7a, 0x6c, 0x1a, 0xc8, 0xcf, 0x88, 0x48, 0x8b, 0xcf, 0x63, 0x20, 0xdc, 0x2e, 0x75, 0xc8, 0x2, 0xa1, 0xb4, 0x62, 0x5a, 0xdc, 0x9a, 0xfb, 0x2a, 0x1a, 0x8a, 0xd2, 0xf0, 0x53},
|
||||
calculated,
|
||||
)
|
||||
}
|
||||
139
internal/pkg/secureboot/tpm2/seal.go
Normal file
139
internal/pkg/secureboot/tpm2/seal.go
Normal file
@ -0,0 +1,139 @@
|
||||
// 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 tpm2 provides TPM2.0 related functionality helpers.
|
||||
package tpm2
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/google/go-tpm/tpm2"
|
||||
"github.com/google/go-tpm/tpm2/transport"
|
||||
|
||||
"github.com/siderolabs/talos/internal/pkg/secureboot"
|
||||
"github.com/siderolabs/talos/pkg/machinery/constants"
|
||||
)
|
||||
|
||||
// Seal seals the key using TPM2.0.
|
||||
func Seal(key []byte) (*SealedResponse, error) {
|
||||
t, err := transport.OpenTPM()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer t.Close() // nolint: errcheck
|
||||
|
||||
// fail early if PCR banks are not present or filled with all zeroes or 0xff
|
||||
if err = validatePCRBanks(t); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sealingPolicyDigest, err := calculateSealingPolicyDigest(t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
primary := tpm2.CreatePrimary{
|
||||
PrimaryHandle: tpm2.TPMRHOwner,
|
||||
InPublic: tpm2.New2B(tpm2.ECCSRKTemplate),
|
||||
}
|
||||
|
||||
createPrimaryResponse, err := primary.Execute(t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
flush := tpm2.FlushContext{
|
||||
FlushHandle: createPrimaryResponse.ObjectHandle,
|
||||
}
|
||||
|
||||
_, flushErr := flush.Execute(t)
|
||||
if flushErr != nil {
|
||||
err = flushErr
|
||||
}
|
||||
}()
|
||||
|
||||
outPub, err := createPrimaryResponse.OutPublic.Contents()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
create := tpm2.Create{
|
||||
ParentHandle: tpm2.AuthHandle{
|
||||
Handle: createPrimaryResponse.ObjectHandle,
|
||||
Name: createPrimaryResponse.Name,
|
||||
Auth: tpm2.HMAC(
|
||||
tpm2.TPMAlgSHA256,
|
||||
20,
|
||||
tpm2.Salted(createPrimaryResponse.ObjectHandle, *outPub),
|
||||
tpm2.AESEncryption(128, tpm2.EncryptInOut),
|
||||
),
|
||||
},
|
||||
InSensitive: tpm2.TPM2BSensitiveCreate{
|
||||
Sensitive: &tpm2.TPMSSensitiveCreate{
|
||||
Data: tpm2.NewTPMUSensitiveCreate(&tpm2.TPM2BSensitiveData{
|
||||
Buffer: key,
|
||||
}),
|
||||
},
|
||||
},
|
||||
InPublic: tpm2.New2B(tpm2.TPMTPublic{
|
||||
Type: tpm2.TPMAlgKeyedHash,
|
||||
NameAlg: tpm2.TPMAlgSHA256,
|
||||
ObjectAttributes: tpm2.TPMAObject{
|
||||
FixedTPM: true,
|
||||
FixedParent: true,
|
||||
},
|
||||
Parameters: tpm2.NewTPMUPublicParms(tpm2.TPMAlgKeyedHash, &tpm2.TPMSKeyedHashParms{
|
||||
Scheme: tpm2.TPMTKeyedHashScheme{
|
||||
Scheme: tpm2.TPMAlgNull,
|
||||
},
|
||||
}),
|
||||
AuthPolicy: tpm2.TPM2BDigest{
|
||||
Buffer: sealingPolicyDigest,
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
createResp, err := create.Execute(t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp := SealedResponse{
|
||||
SealedBlobPrivate: tpm2.Marshal(createResp.OutPrivate),
|
||||
SealedBlobPublic: tpm2.Marshal(createResp.OutPublic),
|
||||
KeyName: tpm2.Marshal(createPrimaryResponse.Name),
|
||||
PolicyDigest: sealingPolicyDigest,
|
||||
}
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
func calculateSealingPolicyDigest(t transport.TPM) ([]byte, error) {
|
||||
pcrSelector, err := CreateSelector([]int{secureboot.SecureBootStatePCR})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create PCR selection: %v", err)
|
||||
}
|
||||
|
||||
pcrSelection := tpm2.TPMLPCRSelection{
|
||||
PCRSelections: []tpm2.TPMSPCRSelection{
|
||||
{
|
||||
Hash: tpm2.TPMAlgSHA256,
|
||||
PCRSelect: pcrSelector,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
pcrValue, err := ReadPCR(t, secureboot.SecureBootStatePCR)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sealingDigest, err := CalculateSealingPolicyDigest(pcrValue, pcrSelection, constants.PCRPublicKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to calculate sealing policy digest: %v", err)
|
||||
}
|
||||
|
||||
return sealingDigest, nil
|
||||
}
|
||||
50
internal/pkg/secureboot/tpm2/signature.go
Normal file
50
internal/pkg/secureboot/tpm2/signature.go
Normal file
@ -0,0 +1,50 @@
|
||||
// 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 tpm2 provides TPM2.0 related functionality helpers.
|
||||
package tpm2
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/siderolabs/talos/pkg/machinery/constants"
|
||||
)
|
||||
|
||||
// PCRData is the data structure for PCR signature json.
|
||||
type PCRData struct {
|
||||
SHA1 []BankData `json:"sha1,omitempty"`
|
||||
SHA256 []BankData `json:"sha256,omitempty"`
|
||||
SHA384 []BankData `json:"sha384,omitempty"`
|
||||
SHA512 []BankData `json:"sha512,omitempty"`
|
||||
}
|
||||
|
||||
// BankData constains data for a specific PCR bank.
|
||||
type BankData struct {
|
||||
// list of PCR banks
|
||||
PCRs []int `json:"pcrs"`
|
||||
// Public key of the TPM
|
||||
PKFP string `json:"pkfp"`
|
||||
// Policy digest
|
||||
Pol string `json:"pol"`
|
||||
// Signature of the policy digest in base64
|
||||
Sig string `json:"sig"`
|
||||
}
|
||||
|
||||
// ParsePCRSignature parses the PCR signature json file.
|
||||
func ParsePCRSignature() (*PCRData, error) {
|
||||
pcrSignature, err := os.ReadFile(constants.PCRSignatureJSON)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read pcr signature: %v", err)
|
||||
}
|
||||
|
||||
pcrData := &PCRData{}
|
||||
|
||||
if err = json.Unmarshal(pcrSignature, pcrData); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal pcr signature: %v", err)
|
||||
}
|
||||
|
||||
return pcrData, nil
|
||||
}
|
||||
14
internal/pkg/secureboot/tpm2/testdata/pcr-signing-crt.pem
vendored
Normal file
14
internal/pkg/secureboot/tpm2/testdata/pcr-signing-crt.pem
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA7qhAkdtZxqkIP79DDGin
|
||||
9eaJBeNlJsClJTcbaXbNfk2QJGT3lqo9ErXQQftwYWLGo+kVd8puhnHGPkLW9apT
|
||||
1/ZmUJEFwxV5xws0RllGVPhUga+1oubHUqhEiy707S4RrUEMk/o9wqmtnl2hY5Fx
|
||||
MeQn2o7xrpcNhm8FtHpvQrT0MsbC1cS1ytZH/hwPy/QIB9bx+ugOha6wtQBnpgix
|
||||
1BhHC/NwDIYPg+ONpQSCu9gkXVtLGlKfmjscUANQtBuVKa5NflrjkHw7NAdKYdKp
|
||||
Mnmzr0yu6Tn/2oNmUiJAwHz0BXpfb4Yn8n/IoKJQ5Tv1g6d30wxxpBd0lbwSe9ML
|
||||
RchIDJ5aFRybyRxaPGT17U3yEVzbV78kIFtocaqkc1ise8remZ0wxHzuolbTZD6o
|
||||
swt7C9jMLvfMAQ7JtENXrpDM//XzdRLzyTWKOjhG0YmKKRY6cIrPkugM0PHGCE3R
|
||||
MSH1FmPMrWWBNAMwS0Zba0Wm1b7vdw5fKeE8txH+IpA3IaE9AytYk0ig98ZgmXmB
|
||||
V0sgxmJ/94scEF+sDg65LIkSEJMzf6q30UghbJJoP7eKOoDX9KBrR+POEsWm/EcU
|
||||
5jTEQTHMU+qKtj5KD6TUn8R8yi4wCnyZ7uJLUqm8Ou8MzEZWbrsrbMvrewPDAHn0
|
||||
QQvb2tDtBgn6oH192jpkzckCAwEAAQ==
|
||||
-----END PUBLIC KEY-----
|
||||
14
internal/pkg/secureboot/tpm2/tpm2.go
Normal file
14
internal/pkg/secureboot/tpm2/tpm2.go
Normal file
@ -0,0 +1,14 @@
|
||||
// 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 tpm2 provides TPM2.0 related functionality helpers.
|
||||
package tpm2
|
||||
|
||||
// SealedResponse is the response from the TPM2.0 Seal operation.
|
||||
type SealedResponse struct {
|
||||
SealedBlobPrivate []byte
|
||||
SealedBlobPublic []byte
|
||||
KeyName []byte
|
||||
PolicyDigest []byte
|
||||
}
|
||||
264
internal/pkg/secureboot/tpm2/unseal.go
Normal file
264
internal/pkg/secureboot/tpm2/unseal.go
Normal file
@ -0,0 +1,264 @@
|
||||
// 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 tpm2 provides TPM2.0 related functionality helpers.
|
||||
package tpm2
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/go-tpm/tpm2"
|
||||
"github.com/google/go-tpm/tpm2/transport"
|
||||
|
||||
"github.com/siderolabs/talos/internal/pkg/secureboot"
|
||||
"github.com/siderolabs/talos/pkg/machinery/constants"
|
||||
)
|
||||
|
||||
// Unseal unseals a sealed blob using the TPM
|
||||
// nolint:gocyclo,cyclop
|
||||
func Unseal(sealed SealedResponse) ([]byte, error) {
|
||||
t, err := transport.OpenTPM()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer t.Close() // nolint: errcheck
|
||||
|
||||
// fail early if PCR banks are not present or filled with all zeroes or 0xff
|
||||
if err = validatePCRBanks(t); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tpmPub, err := tpm2.Unmarshal[tpm2.TPM2BPublic](sealed.SealedBlobPublic)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tpmPriv, err := tpm2.Unmarshal[tpm2.TPM2BPrivate](sealed.SealedBlobPrivate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
srk, err := tpm2.Unmarshal[tpm2.TPM2BName](sealed.KeyName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// we need to create a primary since we don't persist the SRK
|
||||
primary := tpm2.CreatePrimary{
|
||||
PrimaryHandle: tpm2.TPMRHOwner,
|
||||
InPublic: tpm2.New2B(tpm2.ECCSRKTemplate),
|
||||
}
|
||||
|
||||
createPrimaryResponse, err := primary.Execute(t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
flush := tpm2.FlushContext{
|
||||
FlushHandle: createPrimaryResponse.ObjectHandle,
|
||||
}
|
||||
|
||||
_, flushErr := flush.Execute(t)
|
||||
if flushErr != nil {
|
||||
err = flushErr
|
||||
}
|
||||
}()
|
||||
|
||||
outPub, err := createPrimaryResponse.OutPublic.Contents()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !bytes.Equal(createPrimaryResponse.Name.Buffer, srk.Buffer) {
|
||||
// this means the srk name does not match, possibly due to a different TPM or tpm was reset
|
||||
// could also mean the disk was used on a different machine
|
||||
return nil, fmt.Errorf("srk name does not match")
|
||||
}
|
||||
|
||||
load := tpm2.Load{
|
||||
ParentHandle: tpm2.NamedHandle{
|
||||
Handle: createPrimaryResponse.ObjectHandle,
|
||||
Name: createPrimaryResponse.Name,
|
||||
},
|
||||
InPrivate: *tpmPriv,
|
||||
InPublic: *tpmPub,
|
||||
}
|
||||
|
||||
loadResponse, err := load.Execute(t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
policySess, policyCloseFunc, err := tpm2.PolicySession(
|
||||
t,
|
||||
tpm2.TPMAlgSHA256,
|
||||
20,
|
||||
tpm2.Salted(createPrimaryResponse.ObjectHandle, *outPub),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create policy session: %v", err)
|
||||
}
|
||||
|
||||
defer policyCloseFunc() // nolint: errcheck
|
||||
|
||||
pubKey, err := ParsePCRSigningPubKey(constants.PCRPublicKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
loadExternal := tpm2.LoadExternal{
|
||||
Hierarchy: tpm2.TPMRHOwner,
|
||||
InPublic: tpm2.New2B(RSAPubKeyTemplate(pubKey.N.BitLen(), pubKey.E, pubKey.N.Bytes())),
|
||||
}
|
||||
|
||||
loadExternalResponse, err := loadExternal.Execute(t)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load external key: %v", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
flush := tpm2.FlushContext{
|
||||
FlushHandle: loadExternalResponse.ObjectHandle,
|
||||
}
|
||||
|
||||
_, flushErr := flush.Execute(t)
|
||||
if flushErr != nil {
|
||||
err = flushErr
|
||||
}
|
||||
}()
|
||||
|
||||
pcrSelector, err := CreateSelector([]int{secureboot.UKIPCR})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
policyDigest, err := PolicyPCRDigest(t, policySess.Handle(), tpm2.TPMLPCRSelection{
|
||||
PCRSelections: []tpm2.TPMSPCRSelection{
|
||||
{
|
||||
Hash: tpm2.TPMAlgSHA256,
|
||||
PCRSelect: pcrSelector,
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to retrieve policy digest: %v", err)
|
||||
}
|
||||
|
||||
sigJSON, err := ParsePCRSignature()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pubKeyFingerprint := sha256.Sum256(x509.MarshalPKCS1PublicKey(pubKey))
|
||||
|
||||
var signature string
|
||||
// TODO: maybe we should use the highest supported algorithm of the TPM
|
||||
// fallback to the next one if the signature is not found
|
||||
for _, bank := range sigJSON.SHA256 {
|
||||
digest, decodeErr := hex.DecodeString(bank.Pol)
|
||||
if decodeErr != nil {
|
||||
return nil, decodeErr
|
||||
}
|
||||
|
||||
if bytes.Equal(digest, policyDigest.Buffer) {
|
||||
signature = bank.Sig
|
||||
|
||||
if hex.EncodeToString(pubKeyFingerprint[:]) != bank.PKFP {
|
||||
return nil, fmt.Errorf("certificate fingerprint does not match")
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if signature == "" {
|
||||
return nil, fmt.Errorf("signature not found")
|
||||
}
|
||||
|
||||
signatureDecoded, err := base64.StdEncoding.DecodeString(signature)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Verify will only verify the RSA part of the RSA+SHA256 signature,
|
||||
// hence we need to do the SHA256 part ourselves
|
||||
policyDigestHash := sha256.Sum256(policyDigest.Buffer)
|
||||
|
||||
verifySignature := tpm2.VerifySignature{
|
||||
KeyHandle: loadExternalResponse.ObjectHandle,
|
||||
Digest: tpm2.TPM2BDigest{
|
||||
Buffer: policyDigestHash[:],
|
||||
},
|
||||
Signature: tpm2.TPMTSignature{
|
||||
SigAlg: tpm2.TPMAlgRSASSA,
|
||||
Signature: tpm2.NewTPMUSignature(tpm2.TPMAlgRSASSA, &tpm2.TPMSSignatureRSA{
|
||||
Hash: tpm2.TPMAlgSHA256,
|
||||
Sig: tpm2.TPM2BPublicKeyRSA{
|
||||
Buffer: signatureDecoded,
|
||||
},
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
verifySignatureResponse, err := verifySignature.Execute(t)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to verify signature: %v", err)
|
||||
}
|
||||
|
||||
policyAuthorize := tpm2.PolicyAuthorize{
|
||||
PolicySession: policySess.Handle(),
|
||||
ApprovedPolicy: *policyDigest,
|
||||
KeySign: loadExternalResponse.Name,
|
||||
CheckTicket: verifySignatureResponse.Validation,
|
||||
}
|
||||
|
||||
if _, err = policyAuthorize.Execute(t); err != nil {
|
||||
return nil, fmt.Errorf("failed to execute policy authorize: %v", err)
|
||||
}
|
||||
|
||||
secureBootStatePCRSelector, err := CreateSelector([]int{secureboot.SecureBootStatePCR})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
secureBootStatePolicyDigest, err := PolicyPCRDigest(t, policySess.Handle(), tpm2.TPMLPCRSelection{
|
||||
PCRSelections: []tpm2.TPMSPCRSelection{
|
||||
{
|
||||
Hash: tpm2.TPMAlgSHA256,
|
||||
PCRSelect: secureBootStatePCRSelector,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if !bytes.Equal(secureBootStatePolicyDigest.Buffer, sealed.PolicyDigest) {
|
||||
return nil, fmt.Errorf("sealing policy digest does not match")
|
||||
}
|
||||
|
||||
unsealOp := tpm2.Unseal{
|
||||
ItemHandle: tpm2.AuthHandle{
|
||||
Handle: loadResponse.ObjectHandle,
|
||||
Name: loadResponse.Name,
|
||||
Auth: policySess,
|
||||
},
|
||||
}
|
||||
|
||||
unsealResponse, err := unsealOp.Execute(t, tpm2.HMAC(
|
||||
tpm2.TPMAlgSHA256,
|
||||
20,
|
||||
tpm2.Salted(createPrimaryResponse.ObjectHandle, *outPub),
|
||||
tpm2.AESEncryption(128, tpm2.EncryptOut),
|
||||
tpm2.Bound(loadResponse.ObjectHandle, loadResponse.Name, nil),
|
||||
))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unseal op: %v", err)
|
||||
}
|
||||
|
||||
return unsealResponse.OutData.Buffer, nil
|
||||
}
|
||||
@ -1,697 +0,0 @@
|
||||
// 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 tpm2 provides TPM2.0 related functionality helpers.
|
||||
package tpm2
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/google/go-tpm/tpm2"
|
||||
"github.com/google/go-tpm/tpm2/transport"
|
||||
"github.com/google/go-tpm/tpmutil"
|
||||
|
||||
"github.com/siderolabs/talos/pkg/machinery/constants"
|
||||
)
|
||||
|
||||
// PCRData is the data structure for PCR signature json.
|
||||
type PCRData struct {
|
||||
SHA1 []BankData `json:"sha1,omitempty"`
|
||||
SHA256 []BankData `json:"sha256,omitempty"`
|
||||
SHA384 []BankData `json:"sha384,omitempty"`
|
||||
SHA512 []BankData `json:"sha512,omitempty"`
|
||||
}
|
||||
|
||||
// BankData constains data for a specific PCR bank.
|
||||
type BankData struct {
|
||||
// list of PCR banks
|
||||
PCRs []int `json:"pcrs"`
|
||||
// Public key of the TPM
|
||||
PKFP string `json:"pkfp"`
|
||||
// Policy digest
|
||||
Pol string `json:"pol"`
|
||||
// Signature of the policy digest in base64
|
||||
Sig string `json:"sig"`
|
||||
}
|
||||
|
||||
// SealedResponse is the response from the TPM2.0 Seal operation.
|
||||
type SealedResponse struct {
|
||||
SealedBlobPrivate []byte
|
||||
SealedBlobPublic []byte
|
||||
KeyName []byte
|
||||
PolicyDigest []byte
|
||||
}
|
||||
|
||||
// Seal seals the key using TPM2.0.
|
||||
func Seal(key []byte) (*SealedResponse, error) {
|
||||
t, err := transport.OpenTPM()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer t.Close() // nolint: errcheck
|
||||
|
||||
// fail early if PCR banks are not present or filled with all zeroes or 0xff
|
||||
if err = validatePCRBanks(t); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sealingPolicyDigest, err := calculateSealingPolicyDigest(t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
primary := tpm2.CreatePrimary{
|
||||
PrimaryHandle: tpm2.TPMRHOwner,
|
||||
InPublic: tpm2.New2B(tpm2.ECCSRKTemplate),
|
||||
}
|
||||
|
||||
createPrimaryResponse, err := primary.Execute(t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
flush := tpm2.FlushContext{
|
||||
FlushHandle: createPrimaryResponse.ObjectHandle,
|
||||
}
|
||||
|
||||
_, flushErr := flush.Execute(t)
|
||||
if flushErr != nil {
|
||||
err = flushErr
|
||||
}
|
||||
}()
|
||||
|
||||
outPub, err := createPrimaryResponse.OutPublic.Contents()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
create := tpm2.Create{
|
||||
ParentHandle: tpm2.AuthHandle{
|
||||
Handle: createPrimaryResponse.ObjectHandle,
|
||||
Name: createPrimaryResponse.Name,
|
||||
Auth: tpm2.HMAC(
|
||||
tpm2.TPMAlgSHA256,
|
||||
20,
|
||||
tpm2.Salted(createPrimaryResponse.ObjectHandle, *outPub),
|
||||
tpm2.AESEncryption(128, tpm2.EncryptInOut),
|
||||
),
|
||||
},
|
||||
InSensitive: tpm2.TPM2BSensitiveCreate{
|
||||
Sensitive: &tpm2.TPMSSensitiveCreate{
|
||||
Data: tpm2.NewTPMUSensitiveCreate(&tpm2.TPM2BSensitiveData{
|
||||
Buffer: key,
|
||||
}),
|
||||
},
|
||||
},
|
||||
InPublic: tpm2.New2B(tpm2.TPMTPublic{
|
||||
Type: tpm2.TPMAlgKeyedHash,
|
||||
NameAlg: tpm2.TPMAlgSHA256,
|
||||
ObjectAttributes: tpm2.TPMAObject{
|
||||
FixedTPM: true,
|
||||
FixedParent: true,
|
||||
},
|
||||
Parameters: tpm2.NewTPMUPublicParms(tpm2.TPMAlgKeyedHash, &tpm2.TPMSKeyedHashParms{
|
||||
Scheme: tpm2.TPMTKeyedHashScheme{
|
||||
Scheme: tpm2.TPMAlgNull,
|
||||
},
|
||||
}),
|
||||
AuthPolicy: tpm2.TPM2BDigest{
|
||||
Buffer: sealingPolicyDigest,
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
createResp, err := create.Execute(t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp := SealedResponse{
|
||||
SealedBlobPrivate: tpm2.Marshal(createResp.OutPrivate),
|
||||
SealedBlobPublic: tpm2.Marshal(createResp.OutPublic),
|
||||
KeyName: tpm2.Marshal(createPrimaryResponse.Name),
|
||||
PolicyDigest: sealingPolicyDigest,
|
||||
}
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// Unseal unseals a sealed blob using the TPM
|
||||
// nolint:gocyclo,cyclop
|
||||
func Unseal(sealed SealedResponse) ([]byte, error) {
|
||||
t, err := transport.OpenTPM()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer t.Close() // nolint: errcheck
|
||||
|
||||
// fail early if PCR banks are not present or filled with all zeroes or 0xff
|
||||
if err = validatePCRBanks(t); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tpmPub, err := tpm2.Unmarshal[tpm2.TPM2BPublic](sealed.SealedBlobPublic)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tpmPriv, err := tpm2.Unmarshal[tpm2.TPM2BPrivate](sealed.SealedBlobPrivate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
srk, err := tpm2.Unmarshal[tpm2.TPM2BName](sealed.KeyName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// we need to create a primary since we don't persist the SRK
|
||||
primary := tpm2.CreatePrimary{
|
||||
PrimaryHandle: tpm2.TPMRHOwner,
|
||||
InPublic: tpm2.New2B(tpm2.ECCSRKTemplate),
|
||||
}
|
||||
|
||||
createPrimaryResponse, err := primary.Execute(t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
flush := tpm2.FlushContext{
|
||||
FlushHandle: createPrimaryResponse.ObjectHandle,
|
||||
}
|
||||
|
||||
_, flushErr := flush.Execute(t)
|
||||
if flushErr != nil {
|
||||
err = flushErr
|
||||
}
|
||||
}()
|
||||
|
||||
outPub, err := createPrimaryResponse.OutPublic.Contents()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !bytes.Equal(createPrimaryResponse.Name.Buffer, srk.Buffer) {
|
||||
// this means the srk name does not match, possibly due to a different TPM or tpm was reset
|
||||
// could also mean the disk was used on a different machine
|
||||
return nil, fmt.Errorf("srk name does not match")
|
||||
}
|
||||
|
||||
load := tpm2.Load{
|
||||
ParentHandle: tpm2.NamedHandle{
|
||||
Handle: createPrimaryResponse.ObjectHandle,
|
||||
Name: createPrimaryResponse.Name,
|
||||
},
|
||||
InPrivate: *tpmPriv,
|
||||
InPublic: *tpmPub,
|
||||
}
|
||||
|
||||
loadResponse, err := load.Execute(t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
policySess, policyCloseFunc, err := tpm2.PolicySession(
|
||||
t,
|
||||
tpm2.TPMAlgSHA256,
|
||||
20,
|
||||
tpm2.Salted(createPrimaryResponse.ObjectHandle, *outPub),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create policy session: %v", err)
|
||||
}
|
||||
|
||||
defer policyCloseFunc() // nolint: errcheck
|
||||
|
||||
pubKey, err := parsePCRSigningPubKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
loadExternal := tpm2.LoadExternal{
|
||||
Hierarchy: tpm2.TPMRHOwner,
|
||||
InPublic: tpm2.New2B(pubKeyTemplate(pubKey.N.BitLen(), pubKey.E, pubKey.N.Bytes())),
|
||||
}
|
||||
|
||||
loadExternalResponse, err := loadExternal.Execute(t)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load external key: %v", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
flush := tpm2.FlushContext{
|
||||
FlushHandle: loadExternalResponse.ObjectHandle,
|
||||
}
|
||||
|
||||
_, flushErr := flush.Execute(t)
|
||||
if flushErr != nil {
|
||||
err = flushErr
|
||||
}
|
||||
}()
|
||||
|
||||
pcrSelector, err := createPCRSelection([]int{constants.UKIMeasuredPCR})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
policyPCR := tpm2.PolicyPCR{
|
||||
PolicySession: policySess.Handle(),
|
||||
Pcrs: tpm2.TPMLPCRSelection{
|
||||
PCRSelections: []tpm2.TPMSPCRSelection{
|
||||
{
|
||||
Hash: tpm2.TPMAlgSHA256,
|
||||
PCRSelect: pcrSelector,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err = policyPCR.Execute(t)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute policy pcr: %v", err)
|
||||
}
|
||||
|
||||
policyGetDigest := tpm2.PolicyGetDigest{
|
||||
PolicySession: policySess.Handle(),
|
||||
}
|
||||
|
||||
policyGetDigestResp, err := policyGetDigest.Execute(t)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute policy get digest: %v", err)
|
||||
}
|
||||
|
||||
sigJSON, err := parsePCRSignature()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pubKeyFingerprint := sha256.Sum256(x509.MarshalPKCS1PublicKey(pubKey))
|
||||
|
||||
var signature string
|
||||
// TODO: maybe we should use the highest supported algorithm of the TPM
|
||||
// fallback to the next one if the signature is not found
|
||||
for _, bank := range sigJSON.SHA256 {
|
||||
digest, decodeErr := hex.DecodeString(bank.Pol)
|
||||
if decodeErr != nil {
|
||||
return nil, decodeErr
|
||||
}
|
||||
|
||||
if bytes.Equal(digest, policyGetDigestResp.PolicyDigest.Buffer) {
|
||||
signature = bank.Sig
|
||||
|
||||
if hex.EncodeToString(pubKeyFingerprint[:]) != bank.PKFP {
|
||||
return nil, fmt.Errorf("certificate fingerprint does not match")
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if signature == "" {
|
||||
return nil, fmt.Errorf("signature not found")
|
||||
}
|
||||
|
||||
signatureDecoded, err := base64.StdEncoding.DecodeString(signature)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Verify will only verify the RSA part of the RSA+SHA256 signature,
|
||||
// hence we need to do the SHA256 part ourselves
|
||||
policyDigestHash := sha256.Sum256(policyGetDigestResp.PolicyDigest.Buffer)
|
||||
|
||||
verifySignature := tpm2.VerifySignature{
|
||||
KeyHandle: loadExternalResponse.ObjectHandle,
|
||||
Digest: tpm2.TPM2BDigest{
|
||||
Buffer: policyDigestHash[:],
|
||||
},
|
||||
Signature: tpm2.TPMTSignature{
|
||||
SigAlg: tpm2.TPMAlgRSASSA,
|
||||
Signature: tpm2.NewTPMUSignature(tpm2.TPMAlgRSASSA, &tpm2.TPMSSignatureRSA{
|
||||
Hash: tpm2.TPMAlgSHA256,
|
||||
Sig: tpm2.TPM2BPublicKeyRSA{
|
||||
Buffer: signatureDecoded,
|
||||
},
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
verifySignatureResponse, err := verifySignature.Execute(t)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to verify signature: %v", err)
|
||||
}
|
||||
|
||||
policyAuthorize := tpm2.PolicyAuthorize{
|
||||
PolicySession: policySess.Handle(),
|
||||
ApprovedPolicy: policyGetDigestResp.PolicyDigest,
|
||||
KeySign: loadExternalResponse.Name,
|
||||
CheckTicket: verifySignatureResponse.Validation,
|
||||
}
|
||||
|
||||
if _, err = policyAuthorize.Execute(t); err != nil {
|
||||
return nil, fmt.Errorf("failed to execute policy authorize: %v", err)
|
||||
}
|
||||
|
||||
// we need to call policyPCR again to update the policy digest
|
||||
if _, err = policyPCR.Execute(t); err != nil {
|
||||
return nil, fmt.Errorf("failed to execute policy pcr: %v", err)
|
||||
}
|
||||
|
||||
policyGetDigestResp, err = policyGetDigest.Execute(t)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute policy get digest: %v", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(policyGetDigestResp.PolicyDigest.Buffer, sealed.PolicyDigest) {
|
||||
return nil, fmt.Errorf("sealing policy digest does not match")
|
||||
}
|
||||
|
||||
unsealOp := tpm2.Unseal{
|
||||
ItemHandle: tpm2.AuthHandle{
|
||||
Handle: loadResponse.ObjectHandle,
|
||||
Name: loadResponse.Name,
|
||||
Auth: policySess,
|
||||
},
|
||||
}
|
||||
|
||||
unsealResponse, err := unsealOp.Execute(t, tpm2.HMAC(
|
||||
tpm2.TPMAlgSHA256,
|
||||
20,
|
||||
tpm2.Salted(createPrimaryResponse.ObjectHandle, *outPub),
|
||||
tpm2.AESEncryption(128, tpm2.EncryptOut),
|
||||
tpm2.Bound(loadResponse.ObjectHandle, loadResponse.Name, nil),
|
||||
))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unseal op: %v", err)
|
||||
}
|
||||
|
||||
return unsealResponse.OutData.Buffer, nil
|
||||
}
|
||||
|
||||
// PCRExtent hashes the input and extends the PCR with the hash.
|
||||
func PCRExtent(pcr int, data []byte) error {
|
||||
t, err := transport.OpenTPM()
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
log.Printf("TPM device is not available, skipping PCR extension")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
defer t.Close() // nolint: errcheck
|
||||
|
||||
// since we are using SHA256, we can assume that the PCR bank is SHA256
|
||||
digest := sha256.Sum256(data)
|
||||
|
||||
pcrHandle := tpm2.PCRExtend{
|
||||
PCRHandle: tpm2.AuthHandle{
|
||||
Handle: tpm2.TPMHandle(pcr),
|
||||
Auth: tpm2.PasswordAuth(nil),
|
||||
},
|
||||
Digests: tpm2.TPMLDigestValues{
|
||||
Digests: []tpm2.TPMTHA{
|
||||
{
|
||||
HashAlg: tpm2.TPMAlgSHA256,
|
||||
Digest: digest[:],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if _, err = pcrHandle.Execute(t); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func pubKeyTemplate(bitlen, exponent int, modulus []byte) tpm2.TPMTPublic {
|
||||
return tpm2.TPMTPublic{
|
||||
Type: tpm2.TPMAlgRSA,
|
||||
NameAlg: tpm2.TPMAlgSHA256,
|
||||
ObjectAttributes: tpm2.TPMAObject{
|
||||
Decrypt: true,
|
||||
SignEncrypt: true,
|
||||
UserWithAuth: true,
|
||||
},
|
||||
Parameters: tpm2.NewTPMUPublicParms(tpm2.TPMAlgRSA, &tpm2.TPMSRSAParms{
|
||||
Symmetric: tpm2.TPMTSymDefObject{
|
||||
Algorithm: tpm2.TPMAlgNull,
|
||||
Mode: tpm2.NewTPMUSymMode(tpm2.TPMAlgRSA, tpm2.TPMAlgNull),
|
||||
},
|
||||
Scheme: tpm2.TPMTRSAScheme{
|
||||
Scheme: tpm2.TPMAlgNull,
|
||||
Details: tpm2.NewTPMUAsymScheme(tpm2.TPMAlgRSA, &tpm2.TPMSSigSchemeRSASSA{
|
||||
HashAlg: tpm2.TPMAlgNull,
|
||||
}),
|
||||
},
|
||||
KeyBits: tpm2.TPMKeyBits(bitlen),
|
||||
Exponent: uint32(exponent),
|
||||
}),
|
||||
Unique: tpm2.NewTPMUPublicID(tpm2.TPMAlgRSA, &tpm2.TPM2BPublicKeyRSA{
|
||||
Buffer: modulus,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
func calculateSealingPolicyDigest(t transport.TPM) ([]byte, error) {
|
||||
policyAuthorizationDigest, err := calculatePolicyAuthorizationDigest()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pcrSelector, err := createPCRSelection([]int{constants.UKIMeasuredPCR})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create PCR selection: %v", err)
|
||||
}
|
||||
|
||||
pcrValue, err := readPCR(t, constants.UKIMeasuredPCR)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sealingDigest := calculatePolicyPCR(policyAuthorizationDigest, pcrValue, tpm2.TPMLPCRSelection{
|
||||
PCRSelections: []tpm2.TPMSPCRSelection{
|
||||
{
|
||||
Hash: tpm2.TPMAlgSHA256,
|
||||
PCRSelect: pcrSelector,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return sealingDigest, nil
|
||||
}
|
||||
|
||||
func calculatePolicyAuthorizationDigest() ([]byte, error) {
|
||||
tpm2PubKey, err := parsePCRSigningPubKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
publicKeyTemplate := pubKeyTemplate(tpm2PubKey.N.BitLen(), tpm2PubKey.E, tpm2PubKey.N.Bytes())
|
||||
|
||||
name, err := tpm2.ObjectName(&publicKeyTemplate)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to calculate name: %v", err)
|
||||
}
|
||||
|
||||
policyAuthorizeCommand := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(policyAuthorizeCommand, uint32(tpm2.TPMCCPolicyAuthorize))
|
||||
|
||||
// PolicyAuthorize does not use the previous hash value
|
||||
// start with all zeros
|
||||
initial := bytes.Repeat([]byte{0x00}, sha256.Size)
|
||||
|
||||
initial = append(initial, policyAuthorizeCommand...)
|
||||
initial = append(initial, name.Buffer...)
|
||||
|
||||
policyAuthorizeInitialDigest := sha256.Sum256(initial)
|
||||
|
||||
// PolicyAuthorize requires hashing twice
|
||||
policyAuthorizeDigest := sha256.Sum256(policyAuthorizeInitialDigest[:])
|
||||
|
||||
return policyAuthorizeDigest[:], nil
|
||||
}
|
||||
|
||||
func parsePCRSigningPubKey() (*rsa.PublicKey, error) {
|
||||
pcrSigningPubKey, err := os.ReadFile(constants.PCRPublicKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read pcr signing public key: %v", err)
|
||||
}
|
||||
|
||||
block, _ := pem.Decode(pcrSigningPubKey)
|
||||
if block == nil {
|
||||
return nil, fmt.Errorf("failed to decode pcr signing public key")
|
||||
}
|
||||
|
||||
// parse rsa public key
|
||||
tpm2PubKeyAny, err := x509.ParsePKIXPublicKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tpm2PubKey, ok := tpm2PubKeyAny.(*rsa.PublicKey)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to cast pcr signing public key to rsa")
|
||||
}
|
||||
|
||||
return tpm2PubKey, nil
|
||||
}
|
||||
|
||||
func parsePCRSignature() (*PCRData, error) {
|
||||
pcrSignature, err := os.ReadFile(constants.PCRSignatureJSON)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read pcr signature: %v", err)
|
||||
}
|
||||
|
||||
pcrData := &PCRData{}
|
||||
|
||||
if err = json.Unmarshal(pcrSignature, pcrData); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal pcr signature: %v", err)
|
||||
}
|
||||
|
||||
return pcrData, nil
|
||||
}
|
||||
|
||||
func calculatePolicyPCR(policyAuthorizeDigest, pcrValue []byte, pcrSelection tpm2.TPMLPCRSelection) []byte {
|
||||
pcrHash := sha256.Sum256(pcrValue)
|
||||
|
||||
policyPCRCommandValue := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(policyPCRCommandValue, uint32(tpm2.TPMCCPolicyPCR))
|
||||
|
||||
pcrSelectionMarshalled := tpm2.Marshal(pcrSelection)
|
||||
|
||||
pcrToHash := make([]byte, 0)
|
||||
|
||||
pcrToHash = append(pcrToHash, policyAuthorizeDigest...)
|
||||
pcrToHash = append(pcrToHash, policyPCRCommandValue...)
|
||||
pcrToHash = append(pcrToHash, pcrSelectionMarshalled...)
|
||||
pcrToHash = append(pcrToHash, pcrHash[:]...)
|
||||
|
||||
pcrDigest := sha256.Sum256(pcrToHash)
|
||||
|
||||
return pcrDigest[:]
|
||||
}
|
||||
|
||||
// nolint:gocyclo
|
||||
func validatePCRBanks(t transport.TPM) error {
|
||||
pcrValue, err := readPCR(t, constants.UKIMeasuredPCR)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read PCR: %v", err)
|
||||
}
|
||||
|
||||
if bytes.Equal(pcrValue, bytes.Repeat([]byte{0x00}, sha256.Size)) {
|
||||
return fmt.Errorf("PCR bank %d is populated with zeroes", constants.UKIMeasuredPCR)
|
||||
}
|
||||
|
||||
if bytes.Equal(pcrValue, bytes.Repeat([]byte{0xFF}, sha256.Size)) {
|
||||
return fmt.Errorf("PCR bank %d is populated with 0xFF", constants.UKIMeasuredPCR)
|
||||
}
|
||||
|
||||
caps := tpm2.GetCapability{
|
||||
Capability: tpm2.TPMCapPCRs,
|
||||
Property: 0,
|
||||
PropertyCount: 1,
|
||||
}
|
||||
|
||||
capsResp, err := caps.Execute(t)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get PCR capabilities: %v", err)
|
||||
}
|
||||
|
||||
assignedPCRs, err := capsResp.CapabilityData.Data.AssignedPCR()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse assigned PCRs: %v", err)
|
||||
}
|
||||
|
||||
for _, s := range assignedPCRs.PCRSelections {
|
||||
h, err := s.Hash.Hash()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse hash algorithm: %v", err)
|
||||
}
|
||||
|
||||
switch h { //nolint:exhaustive
|
||||
case crypto.SHA1:
|
||||
continue
|
||||
case crypto.SHA256:
|
||||
// check if 24 banks are available
|
||||
if len(s.PCRSelect) != 24/8 {
|
||||
return fmt.Errorf("unexpected number of PCR banks: %d", len(s.PCRSelect))
|
||||
}
|
||||
|
||||
// check if all banks are available
|
||||
if s.PCRSelect[0] != 0xff || s.PCRSelect[1] != 0xff || s.PCRSelect[2] != 0xff {
|
||||
return fmt.Errorf("unexpected PCR banks: %v", s.PCRSelect)
|
||||
}
|
||||
case crypto.SHA384:
|
||||
continue
|
||||
case crypto.SHA512:
|
||||
continue
|
||||
default:
|
||||
return fmt.Errorf("unsupported hash algorithm: %s", h.String())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func readPCR(t transport.TPM, pcr int) ([]byte, error) {
|
||||
pcrSelector, err := createPCRSelection([]int{pcr})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create PCR selection: %v", err)
|
||||
}
|
||||
|
||||
pcrRead := tpm2.PCRRead{
|
||||
PCRSelectionIn: tpm2.TPMLPCRSelection{
|
||||
PCRSelections: []tpm2.TPMSPCRSelection{
|
||||
{
|
||||
Hash: tpm2.TPMAlgSHA256,
|
||||
PCRSelect: pcrSelector,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
pcrValue, err := pcrRead.Execute(t)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read PCR: %v", err)
|
||||
}
|
||||
|
||||
return pcrValue.PCRValues.Digests[0].Buffer, nil
|
||||
}
|
||||
|
||||
func createPCRSelection(s []int) ([]byte, error) {
|
||||
const sizeOfPCRSelect = 3
|
||||
|
||||
PCRs := make(tpmutil.RawBytes, sizeOfPCRSelect)
|
||||
|
||||
for _, n := range s {
|
||||
if n >= 8*sizeOfPCRSelect {
|
||||
return nil, fmt.Errorf("PCR index %d is out of range (exceeds maximum value %d)", n, 8*sizeOfPCRSelect-1)
|
||||
}
|
||||
|
||||
byteNum := n / 8
|
||||
bytePos := byte(1 << (n % 8))
|
||||
PCRs[byteNum] |= bytePos
|
||||
}
|
||||
|
||||
return PCRs, nil
|
||||
}
|
||||
@ -553,9 +553,6 @@ const (
|
||||
// https://www.mankier.com/7/systemd-stub#Initrd_Resources
|
||||
PCRPublicKey = SDStubDynamicInitrdPath + "/" + "tpm2-pcr-public-key.pem"
|
||||
|
||||
// UKIMeasuredPCR is the PCR index that systemd-boot measures the UKI values into.
|
||||
UKIMeasuredPCR = 11
|
||||
|
||||
// DefaultCertificateValidityDuration is the default duration for a certificate.
|
||||
DefaultCertificateValidityDuration = x509.DefaultCertificateValidityDuration
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user