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:
Noel Georgi 2023-07-14 23:39:26 +05:30
parent 130518de71
commit 166d75fe88
No known key found for this signature in database
GPG Key ID: 21A9F444075C9E36
23 changed files with 923 additions and 820 deletions

View File

@ -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)

View File

@ -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"
}

View File

@ -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,

View File

@ -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

View File

@ -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,

View File

@ -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",

View File

@ -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
}

View File

@ -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
}

View File

@ -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,
)
}

View File

@ -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,

View File

@ -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
)

View 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,
}),
}
}

View 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
}

View File

@ -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)

View 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
}

View 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,
)
}

View 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
}

View 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
}

View 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-----

View 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
}

View 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
}

View File

@ -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
}

View File

@ -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