Skip to content

Commit

Permalink
add activate credential service to metadata server
Browse files Browse the repository at this point in the history
Activate credential provides proof that the Endorsement Key (EK) and
a signig key (in this case eve created AIK) are owned by the same TPM.
This is way to extend the trust from EK (which theoretically comes with
OEM certificate) to a arbitrary TPM resident, restricted, signing key.

Signed-off-by: Shahriyar Jalayeri <[email protected]>
  • Loading branch information
shjala committed Aug 6, 2024
1 parent 98acc79 commit 2aef3d8
Show file tree
Hide file tree
Showing 8 changed files with 602 additions and 2 deletions.
134 changes: 134 additions & 0 deletions pkg/pillar/cmd/msrv/activatecred.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package msrv

import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"

"github.com/google/go-tpm/legacy/tpm2"
"github.com/google/go-tpm/tpmutil"
etpm "github.com/lf-edge/eve/pkg/pillar/evetpm"
)

// ActivateCredTpmParam provides the parameters required to activate the
// credential using HWTPM EK and HWTPM AIK.
type ActivateCredTpmParam struct {
Ek string `json:"ek"` // HWTPM EK public key
AikPub string `json:"aikpub"` // HWTPM AIK public key
AikName string `json:"aikname"` // HWTPM AIK name
}

// ActivateCredGenerated contains the generated credential and data to sign.
type ActivateCredGenerated struct {
Data string `json:"data"`
Cred string `json:"cred"`
Secret string `json:"secret"`
}

// ActivateCredActivated contains the activated credential (decrypted secret)
// and signature of the ActivateCredGenerated.Data.
type ActivateCredActivated struct {
Secret string `json:"secret"`
Sig string `json:"sig"`
}

// handles the GET request /tmp/activate-credential/, this is used to get the
// HTPM EK public key, HWTPM AIK public key, HWTPM AIK name.
func getActivateCredntialParams() ([]byte, []byte, []byte, error) {
rw, err := tpm2.OpenTPM(etpm.TpmDevicePath)
if err != nil {
return nil, nil, nil, err
}
defer rw.Close()

ekPub, _, _, err := tpm2.ReadPublic(rw, etpm.TpmEKHdl)
if err != nil {
return nil, nil, nil, err
}

ekPubByte, err := ekPub.Encode()
if err != nil {
return nil, nil, nil, err
}

var aikName tpmutil.U16Bytes
aikPub, aikName, _, err := tpm2.ReadPublic(rw, etpm.TpmAIKHdl)
if err != nil {
return nil, nil, nil, err
}

aikPubByte, err := aikPub.Encode()
if err != nil {
return nil, nil, nil, err
}

aikNameMarshaled := &bytes.Buffer{}
if err := aikName.TPMMarshal(aikNameMarshaled); err != nil {
return nil, nil, nil, err
}

return ekPubByte, aikPubByte, aikNameMarshaled.Bytes(), nil
}

// handles the POST request /tmp/activate-credential/, this is used to activate
// the credential (decrypt the secret) using HWTPM EK and HWTPM AIK.
func activateCredntial(jsonData []byte) ([]byte, []byte, error) {
rw, err := tpm2.OpenTPM(etpm.TpmDevicePath)
if err != nil {
return nil, nil, err
}
defer rw.Close()

var credPayload ActivateCredGenerated
if err := json.Unmarshal(jsonData, &credPayload); err != nil {
return nil, nil, err
}

credBlob, err := base64.StdEncoding.DecodeString(credPayload.Cred)
if err != nil {
return nil, nil, err
}

encryptedSecret, err := base64.StdEncoding.DecodeString(credPayload.Secret)
if err != nil {
return nil, nil, err
}

// we need to skip the first 2 bytes of the credBlob and encryptedSecret
// as it contains the type. so make sure the length is greater than 2.
if len(credBlob) < 2 || len(encryptedSecret) < 2 {
return nil, nil, fmt.Errorf("malformed parameters")
}
credBlob = credBlob[2:]
encryptedSecret = encryptedSecret[2:]

// activate the credential
recoveredCred, err := tpm2.ActivateCredential(rw,
etpm.TpmAIKHdl,
etpm.TpmEKHdl,
etpm.EmptyPassword,
etpm.EmptyPassword,
credBlob,
encryptedSecret)
if err != nil {
return nil, nil, err
}

dataToSign, err := base64.StdEncoding.DecodeString(credPayload.Data)
if err != nil {
return nil, nil, err
}

digest, validation, err := tpm2.Hash(rw, tpm2.AlgSHA256, dataToSign, tpm2.HandleOwner)
if err != nil {
return nil, nil, err
}

sig, err := tpm2.Sign(rw, etpm.TpmAIKHdl, etpm.EmptyPassword, digest, validation, nil)
if err != nil {
return nil, nil, err
}

return recoveredCred, sig.RSA.Signature, nil
}
191 changes: 191 additions & 0 deletions pkg/pillar/cmd/msrv/activatecred_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
package msrv_test

import (
"bytes"
"crypto"
"crypto/rand"
"crypto/rsa"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"time"

"github.com/google/go-tpm/legacy/tpm2"
"github.com/google/go-tpm/legacy/tpm2/credactivation"
"github.com/lf-edge/eve/pkg/pillar/base"
"github.com/lf-edge/eve/pkg/pillar/cmd/msrv"
etpm "github.com/lf-edge/eve/pkg/pillar/evetpm"
"github.com/lf-edge/eve/pkg/pillar/pubsub"
"github.com/onsi/gomega"
"github.com/sirupsen/logrus"
)

const TpmDevicePath = "/tmp/eve-tpm/srv.sock"

var log = base.NewSourceLogObject(logrus.StandardLogger(), "test", 1234)

func waitForTpmReadyState() error {
for i := 0; i < 10; i++ {
if err := etpm.SealDiskKey(log, []byte("secret"), etpm.DiskKeySealingPCRs); err != nil {
// this is RCRetry, so retry
if strings.Contains(err.Error(), "code 0x22") {
time.Sleep(100 * time.Millisecond)
continue
} else {
return fmt.Errorf("Something is wrong with the TPM : %w", err)
}
} else {
return nil
}
}

return fmt.Errorf("TPM did't become ready after 10 attempts, failing the test")
}

// TestTpmActivateCred contains TPM kong-fu, not for the faint of heart.
func TestTpmActivateCred(t *testing.T) {
_, err := os.Stat(TpmDevicePath)
if err != nil {
t.Skip("TPM is not available, skipping the test.")
}

// for some reason testing SWTPM TPM might return RCRetry for the first
// few operations, so we need to wait for it to become ready.
if err := waitForTpmReadyState(); err != nil {
log.Fatalf("Failed to wait for TPM ready state: %v", err)
}

t.Parallel()
g := gomega.NewGomegaWithT(t)

logger := logrus.StandardLogger()
log := base.NewSourceLogObject(logger, "pubsub", 1234)
ps := pubsub.New(pubsub.NewMemoryDriver(), logger, log)

srv := &msrv.Msrv{
Log: log,
PubSub: ps,
Logger: logger,
}

dir, err := os.MkdirTemp("/tmp", "msrv_test")
g.Expect(err).ToNot(gomega.HaveOccurred())
defer os.RemoveAll(dir)

err = srv.Init(dir, true)
g.Expect(err).ToNot(gomega.HaveOccurred())

err = srv.Activate()
g.Expect(err).ToNot(gomega.HaveOccurred())

handler := srv.MakeMetadataHandler()

// Get the activate credential parameters
pCred := httptest.NewRequest(http.MethodGet, "/eve/v1/tpm/activatecredential/", nil)
pCred.RemoteAddr = "192.168.1.1:0"
pCredRec := httptest.NewRecorder()

handler.ServeHTTP(pCredRec, pCred)
defer pCredRec.Body.Reset()
g.Expect(pCredRec.Code).To(gomega.Equal(http.StatusOK))

var credParam msrv.ActivateCredTpmParam
err = json.Unmarshal(pCredRec.Body.Bytes(), &credParam)
g.Expect(err).ToNot(gomega.HaveOccurred())

// Decode the EK back to tpm2.Public, in practices you need to find a
// way to trust EK using decive cert or OEM cert or whatever.
eKBytes, err := base64.StdEncoding.DecodeString(credParam.Ek)
g.Expect(err).ToNot(gomega.HaveOccurred())

ekPub, err := tpm2.DecodePublic(eKBytes)
g.Expect(err).ToNot(gomega.HaveOccurred())

// Decode the name back to name tpm2.Name
nameBytes, err := base64.StdEncoding.DecodeString(credParam.AikName)
g.Expect(err).ToNot(gomega.HaveOccurred())

name, err := tpm2.DecodeName(bytes.NewBuffer(nameBytes))
g.Expect(err).ToNot(gomega.HaveOccurred())

// Decode the AIK back to tpm2.Public
aikBytes, err := base64.StdEncoding.DecodeString(credParam.AikPub)
g.Expect(err).ToNot(gomega.HaveOccurred())

aikPub, err := tpm2.DecodePublic(aikBytes)
g.Expect(err).ToNot(gomega.HaveOccurred())

// Verify the name matches the AIK
nameHash, err := name.Digest.Alg.Hash()
g.Expect(err).ToNot(gomega.HaveOccurred())

p, err := aikPub.Encode()
g.Expect(err).ToNot(gomega.HaveOccurred())

aikPubHash := nameHash.New()
aikPubHash.Write(p)
aikPubDigest := aikPubHash.Sum(nil)
g.Expect(bytes.Equal(name.Digest.Value, aikPubDigest)).To(gomega.BeTrue())

// Verify the AIK is a restricted signing key
g.Expect((aikPub.Attributes & tpm2.FlagFixedTPM)).To(gomega.BeEquivalentTo(tpm2.FlagFixedTPM))
g.Expect((aikPub.Attributes & tpm2.FlagRestricted)).To(gomega.BeEquivalentTo(tpm2.FlagRestricted))
g.Expect((aikPub.Attributes & tpm2.FlagFixedParent)).To(gomega.BeEquivalentTo(tpm2.FlagFixedParent))
g.Expect((aikPub.Attributes & tpm2.FlagSensitiveDataOrigin)).To(gomega.BeEquivalentTo(tpm2.FlagSensitiveDataOrigin))

// Generate a credential
encKey, err := ekPub.Key()
g.Expect(err).ToNot(gomega.HaveOccurred())

dataToSign := []byte("Data to sign")
credential := make([]byte, 32)
rand.Read(credential)
symBlockSize := int(ekPub.RSAParameters.Symmetric.KeyBits) / 8
credBlob, encryptedSecret, err := credactivation.Generate(name.Digest, encKey, symBlockSize, credential)
g.Expect(err).ToNot(gomega.HaveOccurred())

var activeCredParam msrv.ActivateCredGenerated
activeCredParam.Cred = base64.StdEncoding.EncodeToString(credBlob)
activeCredParam.Secret = base64.StdEncoding.EncodeToString(encryptedSecret)
activeCredParam.Data = base64.StdEncoding.EncodeToString(dataToSign)
jsonStr, err := json.Marshal(activeCredParam)
g.Expect(err).ToNot(gomega.HaveOccurred())

// Ask TPM to activate (decrypt) the credential
aCred := httptest.NewRequest(http.MethodPost, "/eve/v1/tpm/activatecredential/", bytes.NewBuffer(jsonStr))
aCred.RemoteAddr = "192.168.1.1:0"
aCredRec := httptest.NewRecorder()

handler.ServeHTTP(aCredRec, aCred)
defer aCredRec.Body.Reset()
g.Expect(aCredRec.Code).To(gomega.Equal(http.StatusOK))

var actCred msrv.ActivateCredActivated
err = json.Unmarshal(aCredRec.Body.Bytes(), &actCred)
g.Expect(err).ToNot(gomega.HaveOccurred())

recovered, err := base64.StdEncoding.DecodeString(actCred.Secret)
g.Expect(err).ToNot(gomega.HaveOccurred())

g.Expect(bytes.Equal(recovered, credential)).To(gomega.BeTrue())

// Verify the the signature
sig, err := base64.StdEncoding.DecodeString(actCred.Sig)
g.Expect(err).ToNot(gomega.HaveOccurred())

dataHash := crypto.SHA256.New()
dataHash.Write(dataToSign)
dataDigest := dataHash.Sum(nil)

sinerPubKey, err := aikPub.Key()
g.Expect(err).ToNot(gomega.HaveOccurred())

sinerPub := sinerPubKey.(*rsa.PublicKey)
err = rsa.VerifyPKCS1v15(sinerPub, crypto.SHA256, dataDigest[:], sig)
g.Expect(err).ToNot(gomega.HaveOccurred())
}
64 changes: 64 additions & 0 deletions pkg/pillar/cmd/msrv/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -709,3 +709,67 @@ func (msrv *Msrv) withPatchEnvelopesByIP() func(http.Handler) http.Handler {
})
}
}

// handleActivateCredntial handles the request sign arbitrary data with HWTPM AIK,
// and proves that AIK resides in the HWTPM with the given HWTPM EK.
func (msrv *Msrv) handleActivateCredntial() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
ekPubByte, aikPubByte, aiKnameMarshaled, err := getActivateCredntialParams()
if err != nil {
msrv.Log.Errorf("handleActivateCredntial: %v", err)
sendError(w, http.StatusInternalServerError, "Operation failed")
return
}

activateCred := ActivateCredTpmParam{
Ek: base64.StdEncoding.EncodeToString(ekPubByte),
AikPub: base64.StdEncoding.EncodeToString(aikPubByte),
AikName: base64.StdEncoding.EncodeToString(aiKnameMarshaled),
}
out, err := json.Marshal(activateCred)
if err != nil {
msrv.Log.Errorf("handleActivateCredntial: error marshaling JSON payload %v", err)
sendError(w, http.StatusInternalServerError, "Operation failed")
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(out)
return

} else if r.Method == http.MethodPost {
in, err := io.ReadAll(io.LimitReader(r.Body, SignerMaxSize))
if err != nil {
msrv.Log.Errorf("handleActivateCredntial, ReadAll : %v", err)
sendError(w, http.StatusInternalServerError, "Operation failed")
return
}

cred, sig, err := activateCredntial(in)
if err != nil {
msrv.Log.Errorf("handleActivateCredntial, activateCredntial: %v", err)
sendError(w, http.StatusInternalServerError, "Operation failed")
return
}

activateCred := ActivateCredActivated{
Secret: base64.StdEncoding.EncodeToString(cred),
Sig: base64.StdEncoding.EncodeToString(sig),
}
out, err := json.Marshal(activateCred)
if err != nil {
msrv.Log.Errorf("handleActivateCredntial, error marshaling JSON payload : %v", err)
sendError(w, http.StatusInternalServerError, "Operation failed")
return
}

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(out)
return
}

http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
}
}
Loading

0 comments on commit 2aef3d8

Please sign in to comment.