mirror of
https://github.com/tailscale/tailscale.git
synced 2025-10-05 20:41:06 +02:00
feature/tpm: implement key.HardwareAttestationKey (#17256)
Updates #15830 Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
This commit is contained in:
parent
a40f23ad4a
commit
c49ed5dd5a
264
feature/tpm/attestation.go
Normal file
264
feature/tpm/attestation.go
Normal file
@ -0,0 +1,264 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package tpm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/google/go-tpm/tpm2"
|
||||||
|
"github.com/google/go-tpm/tpm2/transport"
|
||||||
|
"golang.org/x/crypto/cryptobyte"
|
||||||
|
"golang.org/x/crypto/cryptobyte/asn1"
|
||||||
|
"tailscale.com/types/key"
|
||||||
|
)
|
||||||
|
|
||||||
|
type attestationKey struct {
|
||||||
|
tpm transport.TPMCloser
|
||||||
|
// private and public parts of the TPM key as returned from tpm2.Create.
|
||||||
|
// These are used for serialization.
|
||||||
|
tpmPrivate tpm2.TPM2BPrivate
|
||||||
|
tpmPublic tpm2.TPM2BPublic
|
||||||
|
// handle of the loaded TPM key.
|
||||||
|
handle *tpm2.NamedHandle
|
||||||
|
// pub is the parsed *ecdsa.PublicKey.
|
||||||
|
pub crypto.PublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAttestationKey() (ak *attestationKey, retErr error) {
|
||||||
|
tpm, err := open()
|
||||||
|
if err != nil {
|
||||||
|
return nil, key.ErrUnsupported
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if retErr != nil {
|
||||||
|
tpm.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
ak = &attestationKey{tpm: tpm}
|
||||||
|
|
||||||
|
// Create a key under the storage hierarchy.
|
||||||
|
if err := withSRK(log.Printf, ak.tpm, func(srk tpm2.AuthHandle) error {
|
||||||
|
resp, err := tpm2.Create{
|
||||||
|
ParentHandle: tpm2.NamedHandle{
|
||||||
|
Handle: srk.Handle,
|
||||||
|
Name: srk.Name,
|
||||||
|
},
|
||||||
|
InPublic: tpm2.New2B(
|
||||||
|
tpm2.TPMTPublic{
|
||||||
|
Type: tpm2.TPMAlgECC,
|
||||||
|
NameAlg: tpm2.TPMAlgSHA256,
|
||||||
|
ObjectAttributes: tpm2.TPMAObject{
|
||||||
|
SensitiveDataOrigin: true,
|
||||||
|
UserWithAuth: true,
|
||||||
|
AdminWithPolicy: true,
|
||||||
|
NoDA: true,
|
||||||
|
FixedTPM: true,
|
||||||
|
FixedParent: true,
|
||||||
|
SignEncrypt: true,
|
||||||
|
},
|
||||||
|
Parameters: tpm2.NewTPMUPublicParms(
|
||||||
|
tpm2.TPMAlgECC,
|
||||||
|
&tpm2.TPMSECCParms{
|
||||||
|
CurveID: tpm2.TPMECCNistP256,
|
||||||
|
Scheme: tpm2.TPMTECCScheme{
|
||||||
|
Scheme: tpm2.TPMAlgECDSA,
|
||||||
|
Details: tpm2.NewTPMUAsymScheme(
|
||||||
|
tpm2.TPMAlgECDSA,
|
||||||
|
&tpm2.TPMSSigSchemeECDSA{
|
||||||
|
// Unfortunately, TPMs don't let us use
|
||||||
|
// TPMAlgNull here to make the hash
|
||||||
|
// algorithm dynamic higher in the
|
||||||
|
// stack. We have to hardcode it here.
|
||||||
|
HashAlg: tpm2.TPMAlgSHA256,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}.Execute(ak.tpm)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("tpm2.Create: %w", err)
|
||||||
|
}
|
||||||
|
ak.tpmPrivate = resp.OutPrivate
|
||||||
|
ak.tpmPublic = resp.OutPublic
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ak, ak.load()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ak *attestationKey) loaded() bool {
|
||||||
|
return ak.tpm != nil && ak.handle != nil && ak.pub != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// load the key into the TPM from its public/private components. Must be called
|
||||||
|
// before Sign or Public.
|
||||||
|
func (ak *attestationKey) load() error {
|
||||||
|
if ak.loaded() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if len(ak.tpmPrivate.Buffer) == 0 || len(ak.tpmPublic.Bytes()) == 0 {
|
||||||
|
return fmt.Errorf("attestationKey.load called without tpmPrivate or tpmPublic")
|
||||||
|
}
|
||||||
|
return withSRK(log.Printf, ak.tpm, func(srk tpm2.AuthHandle) error {
|
||||||
|
resp, err := tpm2.Load{
|
||||||
|
ParentHandle: tpm2.NamedHandle{
|
||||||
|
Handle: srk.Handle,
|
||||||
|
Name: srk.Name,
|
||||||
|
},
|
||||||
|
InPrivate: ak.tpmPrivate,
|
||||||
|
InPublic: ak.tpmPublic,
|
||||||
|
}.Execute(ak.tpm)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("tpm2.Load: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ak.handle = &tpm2.NamedHandle{
|
||||||
|
Handle: resp.ObjectHandle,
|
||||||
|
Name: resp.Name,
|
||||||
|
}
|
||||||
|
pub, err := ak.tpmPublic.Contents()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ak.pub, err = tpm2.Pub(*pub)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// attestationKeySerialized is the JSON-serialized representation of
|
||||||
|
// attestationKey.
|
||||||
|
type attestationKeySerialized struct {
|
||||||
|
TPMPrivate []byte `json:"tpmPrivate"`
|
||||||
|
TPMPublic []byte `json:"tpmPublic"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ak *attestationKey) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(attestationKeySerialized{
|
||||||
|
TPMPublic: ak.tpmPublic.Bytes(),
|
||||||
|
TPMPrivate: ak.tpmPrivate.Buffer,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ak *attestationKey) UnmarshalJSON(data []byte) (retErr error) {
|
||||||
|
var aks attestationKeySerialized
|
||||||
|
if err := json.Unmarshal(data, &aks); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ak.tpmPrivate = tpm2.TPM2BPrivate{Buffer: aks.TPMPrivate}
|
||||||
|
ak.tpmPublic = tpm2.BytesAs2B[tpm2.TPMTPublic, *tpm2.TPMTPublic](aks.TPMPublic)
|
||||||
|
|
||||||
|
tpm, err := open()
|
||||||
|
if err != nil {
|
||||||
|
return key.ErrUnsupported
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if retErr != nil {
|
||||||
|
tpm.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
ak.tpm = tpm
|
||||||
|
|
||||||
|
return ak.load()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ak *attestationKey) Public() crypto.PublicKey {
|
||||||
|
return ak.pub
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ak *attestationKey) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) {
|
||||||
|
if !ak.loaded() {
|
||||||
|
return nil, errors.New("tpm2 attestation key is not loaded during Sign")
|
||||||
|
}
|
||||||
|
// Unfortunately, TPMs don't let us make keys with dynamic hash algorithms.
|
||||||
|
// The hash algorithm is fixed at key creation time (tpm2.Create).
|
||||||
|
if opts != crypto.SHA256 {
|
||||||
|
return nil, fmt.Errorf("tpm2 key is restricted to SHA256, have %q", opts)
|
||||||
|
}
|
||||||
|
resp, err := tpm2.Sign{
|
||||||
|
KeyHandle: ak.handle,
|
||||||
|
Digest: tpm2.TPM2BDigest{
|
||||||
|
Buffer: digest,
|
||||||
|
},
|
||||||
|
InScheme: tpm2.TPMTSigScheme{
|
||||||
|
Scheme: tpm2.TPMAlgECDSA,
|
||||||
|
Details: tpm2.NewTPMUSigScheme(
|
||||||
|
tpm2.TPMAlgECDSA,
|
||||||
|
&tpm2.TPMSSchemeHash{
|
||||||
|
HashAlg: tpm2.TPMAlgSHA256,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
Validation: tpm2.TPMTTKHashCheck{
|
||||||
|
Tag: tpm2.TPMSTHashCheck,
|
||||||
|
},
|
||||||
|
}.Execute(ak.tpm)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("tpm2.Sign: %w", err)
|
||||||
|
}
|
||||||
|
sig, err := resp.Signature.Signature.ECDSA()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return encodeSignature(sig.SignatureR.Buffer, sig.SignatureS.Buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copied from crypto/ecdsa.
|
||||||
|
func encodeSignature(r, s []byte) ([]byte, error) {
|
||||||
|
var b cryptobyte.Builder
|
||||||
|
b.AddASN1(asn1.SEQUENCE, func(b *cryptobyte.Builder) {
|
||||||
|
addASN1IntBytes(b, r)
|
||||||
|
addASN1IntBytes(b, s)
|
||||||
|
})
|
||||||
|
return b.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// addASN1IntBytes encodes in ASN.1 a positive integer represented as
|
||||||
|
// a big-endian byte slice with zero or more leading zeroes.
|
||||||
|
func addASN1IntBytes(b *cryptobyte.Builder, bytes []byte) {
|
||||||
|
for len(bytes) > 0 && bytes[0] == 0 {
|
||||||
|
bytes = bytes[1:]
|
||||||
|
}
|
||||||
|
if len(bytes) == 0 {
|
||||||
|
b.SetError(errors.New("invalid integer"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b.AddASN1(asn1.INTEGER, func(c *cryptobyte.Builder) {
|
||||||
|
if bytes[0]&0x80 != 0 {
|
||||||
|
c.AddUint8(0)
|
||||||
|
}
|
||||||
|
c.AddBytes(bytes)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ak *attestationKey) Close() error {
|
||||||
|
var errs []error
|
||||||
|
if ak.handle != nil && ak.tpm != nil {
|
||||||
|
_, err := tpm2.FlushContext{FlushHandle: ak.handle.Handle}.Execute(ak.tpm)
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
if ak.tpm != nil {
|
||||||
|
errs = append(errs, ak.tpm.Close())
|
||||||
|
}
|
||||||
|
return errors.Join(errs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ak *attestationKey) Clone() key.HardwareAttestationKey {
|
||||||
|
return &attestationKey{
|
||||||
|
tpm: ak.tpm,
|
||||||
|
tpmPrivate: ak.tpmPrivate,
|
||||||
|
tpmPublic: ak.tpmPublic,
|
||||||
|
handle: ak.handle,
|
||||||
|
pub: ak.pub,
|
||||||
|
}
|
||||||
|
}
|
98
feature/tpm/attestation_test.go
Normal file
98
feature/tpm/attestation_test.go
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package tpm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAttestationKeySign(t *testing.T) {
|
||||||
|
skipWithoutTPM(t)
|
||||||
|
ak, err := newAttestationKey()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if err := ak.Close(); err != nil {
|
||||||
|
t.Errorf("ak.Close: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
data := []byte("secrets")
|
||||||
|
digest := sha256.Sum256(data)
|
||||||
|
|
||||||
|
// Check signature/validation round trip.
|
||||||
|
sig, err := ak.Sign(rand.Reader, digest[:], crypto.SHA256)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !ecdsa.VerifyASN1(ak.Public().(*ecdsa.PublicKey), digest[:], sig) {
|
||||||
|
t.Errorf("ecdsa.VerifyASN1 failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a different key.
|
||||||
|
ak2, err := newAttestationKey()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if err := ak2.Close(); err != nil {
|
||||||
|
t.Errorf("ak2.Close: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Make sure that the keys are distinct via their public keys and the
|
||||||
|
// signatures they produce.
|
||||||
|
if ak.Public().(*ecdsa.PublicKey).Equal(ak2.Public()) {
|
||||||
|
t.Errorf("public keys of distinct attestation keys are the same")
|
||||||
|
}
|
||||||
|
sig2, err := ak2.Sign(rand.Reader, digest[:], crypto.SHA256)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if bytes.Equal(sig, sig2) {
|
||||||
|
t.Errorf("signatures from distinct attestation keys are the same")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAttestationKeyUnmarshal(t *testing.T) {
|
||||||
|
skipWithoutTPM(t)
|
||||||
|
ak, err := newAttestationKey()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if err := ak.Close(); err != nil {
|
||||||
|
t.Errorf("ak.Close: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
buf, err := ak.MarshalJSON()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var ak2 attestationKey
|
||||||
|
if err := json.Unmarshal(buf, &ak2); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if err := ak2.Close(); err != nil {
|
||||||
|
t.Errorf("ak2.Close: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if !ak2.loaded() {
|
||||||
|
t.Error("unmarshalled key is not loaded")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ak.Public().(*ecdsa.PublicKey).Equal(ak2.Public()) {
|
||||||
|
t.Error("unmarshalled public key is not the same as the original public key")
|
||||||
|
}
|
||||||
|
}
|
@ -28,6 +28,7 @@ import (
|
|||||||
"tailscale.com/ipn/store"
|
"tailscale.com/ipn/store"
|
||||||
"tailscale.com/paths"
|
"tailscale.com/paths"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
|
"tailscale.com/types/key"
|
||||||
"tailscale.com/types/logger"
|
"tailscale.com/types/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -39,6 +40,10 @@ func init() {
|
|||||||
hi.TPM = infoOnce()
|
hi.TPM = infoOnce()
|
||||||
})
|
})
|
||||||
store.Register(store.TPMPrefix, newStore)
|
store.Register(store.TPMPrefix, newStore)
|
||||||
|
key.RegisterHardwareAttestationKeyFns(
|
||||||
|
func() key.HardwareAttestationKey { return &attestationKey{} },
|
||||||
|
func() (key.HardwareAttestationKey, error) { return newAttestationKey() },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func info() *tailcfg.TPMInfo {
|
func info() *tailcfg.TPMInfo {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user