-
Notifications
You must be signed in to change notification settings - Fork 164
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add activate credential service to metadata server
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
Showing
8 changed files
with
636 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 getActivateCredentialParams() ([]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 activateCredential(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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
} |
Oops, something went wrong.