From 21c2bfd1dc4f6eb4a5e8d6d8dbac4d3a2898285c Mon Sep 17 00:00:00 2001 From: Tom Date: Thu, 28 Mar 2019 13:21:16 -0700 Subject: [PATCH] Initial commit. --- CONTRIBUTING.md | 28 ++ LICENSE | 202 ++++++++++ README.md | 12 + attest/attest.go | 198 ++++++++++ attest/attest_simulated_tpm20_test.go | 231 ++++++++++++ attest/attest_test.go | 224 +++++++++++ attest/attest_tpm12_test.go | 199 ++++++++++ attest/pcp_windows.go | 409 ++++++++++++++++++++ attest/tpm.go | 207 ++++++++++ attest/tpm_linux.go | 520 ++++++++++++++++++++++++++ attest/tpm_windows.go | 303 +++++++++++++++ attest/vendors.go | 44 +++ 12 files changed, 2577 insertions(+) create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 attest/attest.go create mode 100644 attest/attest_simulated_tpm20_test.go create mode 100644 attest/attest_test.go create mode 100644 attest/attest_tpm12_test.go create mode 100644 attest/pcp_windows.go create mode 100644 attest/tpm.go create mode 100644 attest/tpm_linux.go create mode 100644 attest/tpm_windows.go create mode 100644 attest/vendors.go diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..db177d4 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,28 @@ +# How to Contribute + +We'd love to accept your patches and contributions to this project. There are +just a few small guidelines you need to follow. + +## Contributor License Agreement + +Contributions to this project must be accompanied by a Contributor License +Agreement. You (or your employer) retain the copyright to your contribution; +this simply gives us permission to use and redistribute your contributions as +part of the project. Head over to to see +your current agreements on file or to sign a new one. + +You generally only need to submit a CLA once, so if you've already submitted one +(even if it was for a different project), you probably don't need to do it +again. + +## Code reviews + +All submissions, including submissions by project members, require review. We +use GitHub pull requests for this purpose. Consult +[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more +information on using pull requests. + +## Community Guidelines + +This project follows +[Google's Open Source Community Guidelines](https://opensource.google.com/conduct/). diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..96d2344 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +Go-Attestation +============== + +Go-Attestation abstracts remote attestation operations across a variety of platforms +and TPMs. + +## Status + +Go-Attestation is under active development and **is not** ready for production use. Expect +API changes at any time. + +Please note that this is not an official Google product. diff --git a/attest/attest.go b/attest/attest.go new file mode 100644 index 0000000..507b8d7 --- /dev/null +++ b/attest/attest.go @@ -0,0 +1,198 @@ +// Copyright 2019 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. +// Package attest abstracts TPM attestation operations. +package attest + +import ( + "crypto" + "errors" + + "github.com/google/certificate-transparency-go/x509" +) + +// TPMVersion is used to configure a preference in +// which TPM to use, if multiple are available. +type TPMVersion uint8 + +// TPM versions +const ( + TPMVersionAgnostic TPMVersion = iota + TPMVersion12 + TPMVersion20 +) + +// TPMInterface indicates how the client communicates +// with the TPM. +type TPMInterface uint8 + +// TPM interfaces +const ( + TPMInterfaceDirect TPMInterface = iota + TPMInterfaceKernelManaged + TPMInterfaceDaemonManaged +) + +// OpenConfig encapsulates settings passed to OpenTPM(). +type OpenConfig struct { + // TPMVersion indicates which TPM version the library should + // attempt to use. If the specified version is not available, + // ErrTPMNotAvailable is returned. Defaults to TPMVersionAgnostic. + TPMVersion TPMVersion +} + +// KeyEncoding indicates how an exported TPM key is represented. +type KeyEncoding uint8 + +// Key encodings +const ( + KeyEncodingInvalid KeyEncoding = iota + // Managed by the OS but loadable by name. + KeyEncodingOSManaged + // Key fully represented but in encrypted form. + KeyEncodingEncrypted + // Parameters stored, but key must be regenerated before use. + KeyEncodingParameterized +) + +// KeyPurpose indicates the intended use of the key. It is implied that +// the key was created with usage restrictions to constrain its use +// to the given purpose. +type KeyPurpose uint8 + +// Key purposes. +const ( + AttestationKey KeyPurpose = iota + StorageKey +) + +// MintOptions encapsulates parameters for minting keys. This type is defined +// now (despite being empty) for future interface compatibility. +type MintOptions struct { +} + +// EncryptedCredential represents encrypted parameters which must be activated +// against a key. +type EncryptedCredential struct { + Credential []byte + Secret []byte +} + +// Quote encapsulates the results of a Quote operation against the TPM, +// using an attestation key. +type Quote struct { + Version TPMVersion + Quote []byte + Signature []byte +} + +// PCR encapsulates the value of a PCR at a point in time. +type PCR struct { + Index int + Digest []byte + DigestAlg crypto.Hash +} + +// PlatformEK represents a burned-in Endorsement Key, and its +// corrresponding EKCert (where present). +type PlatformEK struct { + Cert *x509.Certificate + Public crypto.PublicKey +} + +var ( + defaultOpenConfig = &OpenConfig{} + + // ErrTPMNotAvailable is returned in response to OpenTPM() when + // either no TPM is available, or a TPM of the requested version + // is not available (if TPMVersion was set in the provided config). + ErrTPMNotAvailable = errors.New("TPM device not available") + // ErrTPM12NotImplemented is returned in response to methods which + // need to interact with the TPM1.2 device in ways that have not + // yet been implemented. + ErrTPM12NotImplemented = errors.New("TPM 1.2 support not yet implemented") +) + +// TPMInfo contains information about the version & interface +// of an open TPM. +type TPMInfo struct { + Version TPMVersion + Interface TPMInterface + VendorInfo string + Manufacturer TCGVendorID +} + +// probedTPM identifies a TPM device on the system, which +// is a candidate for being used. +type probedTPM struct { + Version TPMVersion + Path string +} + +// MatchesConfig returns true if the TPM satisfies the constraints +// specified by the given config. +func (t *probedTPM) MatchesConfig(config OpenConfig) bool { + return config.TPMVersion == TPMVersionAgnostic || t.Version == config.TPMVersion +} + +// OpenTPM initializes access to the TPM based on the +// config provided. +func OpenTPM(config *OpenConfig) (*TPM, error) { + if config == nil { + config = defaultOpenConfig + } + candidateTPMs, err := probeSystemTPMs() + if err != nil { + return nil, err + } + + for _, tpm := range candidateTPMs { + if tpm.MatchesConfig(*config) { + return openTPM(tpm) + } + } + + return nil, ErrTPMNotAvailable +} + +// AvailableTPMs returns information about available TPMs matching +// the given config, without opening the devices. +func AvailableTPMs(config *OpenConfig) ([]TPMInfo, error) { + if config == nil { + config = defaultOpenConfig + } + + candidateTPMs, err := probeSystemTPMs() + if err != nil { + return nil, err + } + + var out []TPMInfo + + for _, tpm := range candidateTPMs { + if tpm.MatchesConfig(*config) { + t, err := openTPM(tpm) + if err != nil { + return nil, err + } + defer t.Close() + i, err := t.Info() + if err != nil { + return nil, err + } + out = append(out, *i) + } + } + + return out, nil +} diff --git a/attest/attest_simulated_tpm20_test.go b/attest/attest_simulated_tpm20_test.go new file mode 100644 index 0000000..d2145b7 --- /dev/null +++ b/attest/attest_simulated_tpm20_test.go @@ -0,0 +1,231 @@ +// Copyright 2019 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. +package attest + +import ( + "bytes" + "crypto" + "crypto/rsa" + "testing" + + "github.com/google/certificate-transparency-go/x509" + + "github.com/google/go-tpm/tpm2/credactivation" + "github.com/google/go-tpm/tpm2" + "github.com/google/go-tpm-tools/simulator" +) + +func setupSimulatedTPM(t *testing.T) (*simulator.Simulator, *TPM) { + t.Helper() + tpm, err := simulator.Get() + if err != nil { + t.Fatal(err) + } + return tpm, &TPM{ + version: TPMVersion20, + interf: TPMInterfaceKernelManaged, + sysPath: "/dev/tpmrm0", + rwc: tpm, + } +} + +func TestEK(t *testing.T) { + sim, tpm := setupSimulatedTPM(t) + defer sim.Close() + + eks, err := tpm.EKs() + if err != nil { + t.Errorf("EKs() failed: %v", err) + } + if len(eks) == 0 || (eks[0].Cert == nil && eks[0].Public == nil) { + t.Errorf("EKs() = %v, want at least 1 EK with populated fields", eks) + } +} + +func TestInfo(t *testing.T) { + sim, tpm := setupSimulatedTPM(t) + defer sim.Close() + + info, err := tpm.Info() + if err != nil { + t.Errorf("tpm.Info() failed: %v", err) + } + // We dont expect anything to be meaningfully populated as this is a simulator. + t.Logf("TPM Info = %+v", info) +} + +func TestAIKCreateAndLoad(t *testing.T) { + sim, tpm := setupSimulatedTPM(t) + defer sim.Close() + + aik, err := tpm.MintAIK(nil) + if err != nil { + t.Fatalf("MintAIK() failed: %v", err) + } + + enc, err := aik.Marshal() + if err != nil { + aik.Close(tpm) + t.Fatalf("aik.Marshal() failed: %v", err) + } + if err := aik.Close(tpm); err != nil { + t.Fatalf("aik.Close() failed: %v", err) + } + + loaded, err := tpm.LoadKey(enc) + if err != nil { + t.Fatalf("LoadKey() failed: %v", err) + } + defer loaded.Close(tpm) + + if !bytes.Equal(loaded.Public, aik.Public) { + t.Error("Original & loaded AIK public blobs did not match.") + t.Logf("Original = %v", aik.Public) + t.Logf("Loaded = %v", loaded.Public) + } +} + +// chooseEK selects the EK public which will be activated against. +func chooseEK(t *testing.T, eks []PlatformEK) crypto.PublicKey { + t.Helper() + + for _, ek := range eks { + if ek.Cert != nil && ek.Cert.PublicKeyAlgorithm == x509.RSA { + return ek.Cert.PublicKey.(*rsa.PublicKey) + } else if ek.Public != nil { + return ek.Public + } + } + + t.Skip("No suitable RSA EK found") + return nil +} + +func TestActivateCredentialTPM20(t *testing.T) { + sim, tpm := setupSimulatedTPM(t) + defer sim.Close() + + aik, err := tpm.MintAIK(nil) + if err != nil { + t.Fatalf("MintAIK() failed: %v", err) + } + defer aik.Close(tpm) + + EKs, err := tpm.EKs() + if err != nil { + t.Fatalf("EKs() failed: %v", err) + } + ek := chooseEK(t, EKs) + + att, err := tpm2.DecodeAttestationData(aik.CreateAttestation) + if err != nil { + t.Fatalf("tpm2.DecodeAttestationData() failed: %v", err) + } + secret := []byte{1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8} + + id, encSecret, err := credactivation.Generate(att.AttestedCreationInfo.Name.Digest, ek, 16, secret) + if err != nil { + t.Fatalf("credactivation.Generate() failed: %v", err) + } + + decryptedSecret, err := aik.ActivateCredential(tpm, EncryptedCredential{ + Credential: id, + Secret: encSecret, + }) + if err != nil { + t.Errorf("aik.ActivateCredential() failed: %v", err) + } + if !bytes.Equal(secret, decryptedSecret) { + t.Error("secret does not match decrypted secret") + t.Logf("Secret = %v", secret) + t.Logf("Decrypted secret = %v", decryptedSecret) + } +} + +func TestQuoteTPM20(t *testing.T) { + sim, tpm := setupSimulatedTPM(t) + defer sim.Close() + + aik, err := tpm.MintAIK(nil) + if err != nil { + t.Fatalf("MintAIK() failed: %v", err) + } + defer aik.Close(tpm) + + nonce := []byte{1, 2, 3, 4, 5, 6, 7, 8} + quote, err := aik.Quote(tpm, nonce, tpm2.AlgSHA256) + if err != nil { + t.Fatalf("aik.Quote() failed: %v", err) + } + // TODO(jsonp): Parse quote structure once gotpm/tpm2 supports it. + if quote == nil { + t.Error("quote was nil, want *Quote") + } +} + +func TestPCRsTPM20(t *testing.T) { + sim, tpm := setupSimulatedTPM(t) + defer sim.Close() + + PCRs, alg, err := tpm.PCRs() + if err != nil { + t.Fatalf("PCRs() failed: %v", err) + } + if len(PCRs) != 24 { + t.Errorf("len(PCRs) = %d, want %d", len(PCRs), 24) + } + if got, want := tpm2.AlgSHA256, alg; got != want { + t.Errorf("alg = %v, want %v", got, want) + } + for i, pcr := range PCRs { + if len(pcr.Digest) != pcr.DigestAlg.Size() { + t.Errorf("PCR %d len(digest) = %d, expected match with digest algorithm size (%d)", pcr.Index, len(pcr.Digest), pcr.DigestAlg.Size()) + } + if pcr.Index != i { + t.Errorf("PCR index %d does not match map index %d", pcr.Index, i) + } + if pcr.DigestAlg != crypto.SHA256 { + t.Errorf("pcr.DigestAlg = %v, expected crypto.SHA256", pcr.DigestAlg) + } + } +} + +func TestPersistence(t *testing.T) { + sim, tpm := setupSimulatedTPM(t) + defer sim.Close() + + ekHnd, p, err := tpm.getPrimaryKeyHandle(commonEkEquivalentHandle) + if err != nil { + t.Fatalf("getPrimaryKeyHandle() failed: %v", err) + } + if ekHnd != commonEkEquivalentHandle { + t.Fatalf("bad EK-equivalent handle: got 0x%x, wanted 0x%x", ekHnd, commonEkEquivalentHandle) + } + if p { + t.Logf("generated a new key") + } else { + t.Logf("used existing key") + } + + ekHnd, p, err = tpm.getPrimaryKeyHandle(commonEkEquivalentHandle) + if err != nil { + t.Fatalf("second getPrimaryKeyHandle() failed: %v", err) + } + if ekHnd != commonEkEquivalentHandle { + t.Fatalf("bad EK-equivalent handle: got 0x%x, wanted 0x%x", ekHnd, commonEkEquivalentHandle) + } + if p { + t.Fatalf("generated a new key the second time; that shouldn't happen") + } +} diff --git a/attest/attest_test.go b/attest/attest_test.go new file mode 100644 index 0000000..02e9a7c --- /dev/null +++ b/attest/attest_test.go @@ -0,0 +1,224 @@ +// Copyright 2019 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. +package attest + +import ( + "bytes" + "crypto" + "crypto/rsa" + "testing" + + "github.com/google/certificate-transparency-go/x509" + + "github.com/google/go-tpm/tpm2/credactivation" + "github.com/google/go-tpm/tpm2" +) + +func TestOpen(t *testing.T) { + tpm, err := OpenTPM(nil) + if err != nil { + t.Fatalf("OpenTPM() failed: %v", err) + } + if tpm == nil { + t.Fatalf("Expected non-nil tpm struct") + } + defer tpm.Close() +} + +func TestInfo(t *testing.T) { + tpm, err := OpenTPM(nil) + if err != nil { + t.Fatalf("OpenTPM() failed: %v", err) + } + defer tpm.Close() + + info, err := tpm.Info() + if err != nil { + t.Errorf("tpm.Info() failed: %v", err) + } + if info.Manufacturer.String() == "" { + t.Error("Expected info.Manufacturer.String() != ''") + } + t.Logf("TPM Info = %+v", info) +} + +func TestEKs(t *testing.T) { + tpm, err := OpenTPM(nil) + if err != nil { + t.Fatalf("OpenTPM() failed: %v", err) + } + defer tpm.Close() + + eks, err := tpm.EKs() + if err != nil { + t.Errorf("EKs() failed: %v", err) + } + if len(eks) == 0 { + t.Log("EKs() did not return anything. This could be an issue if an EK is present.") + } +} + +func TestAIKCreateAndLoad(t *testing.T) { + tpm, err := OpenTPM(nil) + if err != nil { + t.Fatalf("OpenTPM() failed: %v", err) + } + defer tpm.Close() + + aik, err := tpm.MintAIK(nil) + if err != nil { + t.Fatalf("MintAIK() failed: %v", err) + } + + enc, err := aik.Marshal() + if err != nil { + aik.Close(tpm) + t.Fatalf("aik.Marshal() failed: %v", err) + } + if err := aik.Close(tpm); err != nil { + t.Fatalf("aik.Close() failed: %v", err) + } + + loaded, err := tpm.LoadKey(enc) + if err != nil { + t.Fatalf("LoadKey() failed: %v", err) + } + defer loaded.Close(tpm) + + if !bytes.Equal(loaded.Public, aik.Public) { + t.Error("Original & loaded AIK public blobs did not match.") + t.Logf("Original = %v", aik.Public) + t.Logf("Loaded = %v", loaded.Public) + } +} + +// chooseEK selects the EK public which will be activated against. +func chooseEK(t *testing.T, eks []PlatformEK) crypto.PublicKey { + t.Helper() + + for _, ek := range eks { + if ek.Cert != nil && ek.Cert.PublicKeyAlgorithm == x509.RSA { + return ek.Cert.PublicKey.(*rsa.PublicKey) + } else if ek.Public != nil { + return ek.Public + } + } + + t.Skip("No suitable RSA EK found") + return nil +} + +func TestActivateCredentialTPM20(t *testing.T) { + tpm, err := OpenTPM(nil) + if err != nil { + t.Fatalf("OpenTPM() failed: %v", err) + } + defer tpm.Close() + if tpm.version != TPMVersion20 { + t.Skip("N/A for non-TPM2.0 TPMs") + } + + aik, err := tpm.MintAIK(nil) + if err != nil { + t.Fatalf("MintAIK() failed: %v", err) + } + defer aik.Close(tpm) + + EKs, err := tpm.EKs() + if err != nil { + t.Fatalf("EKs() failed: %v", err) + } + ek := chooseEK(t, EKs) + + att, err := tpm2.DecodeAttestationData(aik.CreateAttestation) + if err != nil { + t.Fatalf("tpm2.DecodeAttestationData() failed: %v", err) + } + secret := []byte{1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8} + + id, encSecret, err := credactivation.Generate(att.AttestedCreationInfo.Name.Digest, ek, 16, secret) + if err != nil { + t.Fatalf("credactivation.Generate() failed: %v", err) + } + + decryptedSecret, err := aik.ActivateCredential(tpm, EncryptedCredential{ + Credential: id, + Secret: encSecret, + }) + if err != nil { + t.Errorf("aik.ActivateCredential() failed: %v", err) + } + if !bytes.Equal(secret, decryptedSecret) { + t.Error("secret does not match decrypted secret") + t.Logf("Secret = %v", secret) + t.Logf("Decrypted secret = %v", decryptedSecret) + } +} + +func TestQuoteTPM20(t *testing.T) { + tpm, err := OpenTPM(nil) + if err != nil { + t.Fatalf("OpenTPM() failed: %v", err) + } + defer tpm.Close() + if tpm.version != TPMVersion20 { + t.Skip("N/A for non-TPM2.0 TPMs") + } + + aik, err := tpm.MintAIK(nil) + if err != nil { + t.Fatalf("MintAIK() failed: %v", err) + } + defer aik.Close(tpm) + + nonce := []byte{1, 2, 3, 4, 5, 6, 7, 8} + quote, err := aik.Quote(tpm, nonce, tpm2.AlgSHA1) + if err != nil { + t.Fatalf("aik.Quote() failed: %v", err) + } + // TODO(jsonp): Parse quote structure once gotpm/tpm2 supports it. + if quote == nil { + t.Error("quote was nil, want *Quote") + } +} + +func TestPCRsTPM20(t *testing.T) { + tpm, err := OpenTPM(nil) + if err != nil { + t.Fatalf("OpenTPM() failed: %v", err) + } + defer tpm.Close() + if tpm.version != TPMVersion20 { + t.Skip("N/A for non-TPM2.0 TPMs") + } + + PCRs, _, err := tpm.PCRs() + if err != nil { + t.Fatalf("PCRs() failed: %v", err) + } + if len(PCRs) != 24 { + t.Errorf("len(PCRs) = %d, want %d", len(PCRs), 24) + } + for i, pcr := range PCRs { + if len(pcr.Digest) != pcr.DigestAlg.Size() { + t.Errorf("PCR %d len(digest) = %d, expected match with digest algorithm size (%d)", pcr.Index, len(pcr.Digest), pcr.DigestAlg.Size()) + } + if pcr.Index != i { + t.Errorf("PCR index %d does not match map index %d", pcr.Index, i) + } + if pcr.DigestAlg != crypto.SHA1 { + t.Errorf("pcr.DigestAlg = %v, expected crypto.SHA1", pcr.DigestAlg) + } + } +} diff --git a/attest/attest_tpm12_test.go b/attest/attest_tpm12_test.go new file mode 100644 index 0000000..10fc055 --- /dev/null +++ b/attest/attest_tpm12_test.go @@ -0,0 +1,199 @@ +// Copyright 2019 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. +package attest + +import ( + "bytes" + "crypto/rand" + "flag" + "sort" + "testing" + + "github.com/google/certificate-transparency-go/x509" + "github.com/google/go-tpm/tpm2" + "github.com/google/go-tspi/verification" +) + +var ( + testTPM12 = flag.Bool("testTPM12", false, "run tests for TPM1.2") + tpm12config = &OpenConfig{TPMVersion12} +) + +func TestTPM12Info(t *testing.T) { + if !*testTPM12 { + t.SkipNow() + } + tpm, err := OpenTPM(tpm12config) + if err != nil { + t.Fatalf("Failed to open tpm 1.2: %v", err) + } + defer tpm.Close() + + Info, err := tpm.Info() + if err != nil { + t.Fatalf("Failed to get Vendor info: %v", err) + } + + t.Logf("Vendor info: %s\n", Info.VendorInfo) +} + +func TestTPM12PCRs(t *testing.T) { + if !*testTPM12 { + t.SkipNow() + } + tpm, err := OpenTPM(tpm12config) + if err != nil { + t.Fatalf("Failed to open tpm 1.2: %v", err) + } + defer tpm.Close() + + PCRs, _, err := tpm.PCRs() + if err != nil { + t.Fatalf("Failed to get PCR values: %v", err) + } + + var indices []int + for i, PCR := range PCRs { + if i != PCR.Index { + t.Errorf("Index %d does not match the PCRindex %d\n", i, PCR.Index) + } + indices = append(indices, i) + } + sort.Ints(indices) + for i := range indices { + PCR := PCRs[i] + t.Logf("PCR %v contains value 0x%x, which was caculated using alg %v\n", PCR.Index, bytes.NewBuffer(PCR.Digest), PCR.DigestAlg) + } +} + +func TestTPM12EKs(t *testing.T) { + if !*testTPM12 { + t.SkipNow() + } + tpm, err := OpenTPM(tpm12config) + if err != nil { + t.Fatalf("Failed to open tpm 1.2: %v", err) + } + defer tpm.Close() + + EKs, err := tpm.EKs() + if err != nil { + t.Fatalf("Failed to get EKs: %v", err) + } + + if len(EKs) == 0 { + t.Fatalf("EKs returned nothing") + } + + t.Logf("EKCert Raw: %x\n", EKs[0].Cert.Raw) +} + +func TestMintAIK(t *testing.T) { + if !*testTPM12 { + t.SkipNow() + } + tpm, err := OpenTPM(tpm12config) + if err != nil { + t.Fatalf("failed to open tpm 1.2: %v", err) + } + defer tpm.Close() + + aik, err := tpm.MintAIK(nil) + if err != nil { + t.Fatalf("MintAIK failed: %v", err) + } + + if (aik.TPMVersion != TPMVersion12) || + (aik.Purpose != AttestationKey) { + t.Error("aik does not match expected format") + } + t.Logf("aik blob: %x\naik pubkey: %x\n", aik.KeyBlob, aik.Public) +} + +func TestTPMQuote(t *testing.T) { + if !*testTPM12 { + t.SkipNow() + } + nonce := make([]byte, 20) + rand.Read(nonce) + + tpm, err := OpenTPM(tpm12config) + if err != nil { + t.Fatalf("Failed to open tpm 1.2: %v", err) + } + defer tpm.Close() + + aik, err := tpm.MintAIK(nil) + if err != nil { + t.Fatalf("MintAIK failed: %v", err) + } + + quote, err := aik.Quote(tpm, nonce, tpm2.AlgSHA1) + if err != nil { + t.Fatalf("Quote failed: %v", err) + } + + t.Logf("Quote{version: %v, quote: %x, signature: %x}\n", quote.Version, quote.Quote, quote.Signature) +} + +func chooseEK(t *testing.T, eks []PlatformEK) []byte { + t.Helper() + + for _, ek := range eks { + if ek.Cert != nil && ek.Cert.PublicKeyAlgorithm == x509.RSA || ek.Cert.PublicKeyAlgorithm == x509.RSAESOAEP { + return ek.Cert.Raw + } + } + + t.Skip("No suitable RSA EK found") + return nil +} + +func TestTPMActivateCredential(t *testing.T) { + if !*testTPM12 { + t.SkipNow() + } + var challenge EncryptedCredential + nonce := make([]byte, 20) + rand.Read(nonce) + + tpm, err := OpenTPM(tpm12config) + if err != nil { + t.Fatalf("failed to open tpm 1.2: %v", err) + } + defer tpm.Close() + + aik, err := tpm.MintAIK(nil) + if err != nil { + t.Fatalf("MintAIK failed: %v", err) + } + + EKs, err := tpm.EKs() + if err != nil { + t.Fatalf("failed to read EKs: %v", err) + } + ekcert := chooseEK(t, EKs) + + challenge.Credential, challenge.Secret, err = verification.GenerateChallenge(ekcert, aik.Public, nonce) + if err != nil { + t.Fatalf("GenerateChallenge failed: %v", err) + } + + validation, err := aik.ActivateCredential(tpm, challenge) + if err != nil { + t.Fatalf("ActivateCredential failed: %v", err) + } + + t.Logf("validation: %x", validation) +} diff --git a/attest/pcp_windows.go b/attest/pcp_windows.go new file mode 100644 index 0000000..7cb670f --- /dev/null +++ b/attest/pcp_windows.go @@ -0,0 +1,409 @@ +// Copyright 2019 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. +// +build windows + +package attest + +import ( + "bytes" + "encoding/binary" + "fmt" + "io" + "syscall" + "unsafe" + + "github.com/google/certificate-transparency-go/x509" + + "golang.org/x/sys/windows" + tpmtbs "github.com/google/go-tpm/tpmutil/tbs" + "github.com/google/go-tpm/tpmutil" +) + +const ( + pcpProviderName = "Microsoft Platform Crypto Provider" + cryptENotFound = 0x80092004 // From winerror.h. + + // The below is documented in this Microsoft whitepaper: + // https://github.com/Microsoft/TSS.MSR/blob/master/PCPTool.v11/Using%20the%20Windows%208%20Platform%20Crypto%20Provider%20and%20Associated%20TPM%20Functionality.pdf + ncryptOverwriteKeyFlag = 0x80 + // Key usage value for AIKs. + nCryptPropertyPCPKeyUsagePolicyIdentity = 0x8 +) + +// DLL references. +var ( + nCrypt = windows.MustLoadDLL("ncrypt.dll") + nCryptOpenStorageProvider = nCrypt.MustFindProc("NCryptOpenStorageProvider") + nCryptFreeObject = nCrypt.MustFindProc("NCryptFreeObject") + nCryptGetProperty = nCrypt.MustFindProc("NCryptGetProperty") + nCryptSetProperty = nCrypt.MustFindProc("NCryptSetProperty") + nCryptOpenKey = nCrypt.MustFindProc("NCryptOpenKey") + nCryptCreatePersistedKey = nCrypt.MustFindProc("NCryptCreatePersistedKey") + nCryptFinalizeKey = nCrypt.MustFindProc("NCryptFinalizeKey") + + crypt32 = windows.MustLoadDLL("crypt32.dll") + crypt32CertEnumCertificatesInStore = crypt32.MustFindProc("CertEnumCertificatesInStore") + crypt32CertCloseStore = crypt32.MustFindProc("CertCloseStore") + + tbs = windows.MustLoadDLL("Tbs.dll") + tbsGetDeviceInfo = tbs.MustFindProc("Tbsi_GetDeviceInfo") +) + +func utf16ToString(buf []byte) (string, error) { + b := make([]uint16, len(buf)/2) + // LPCSTR (Windows' representation of utf16) is always little endian. + if err := binary.Read(bytes.NewReader(buf), binary.LittleEndian, &b); err != nil { + return "", err + } + return windows.UTF16ToString(b), nil +} + +// closeNCryptoObject is a helper to call NCryptFreeObject on a given handle. +func closeNCryptObject(hnd uintptr) error { + r, _, msg := nCryptFreeObject.Call(hnd) + if r != 0 { + return fmt.Errorf("NCryptFreeObject returned %X: %v", r, msg) + } + return nil +} + +// getNCryptBufferProperty is a helper to read a byte slice from a NCrypt handle property +// using NCryptGetProperty. +func getNCryptBufferProperty(hnd uintptr, field string) ([]byte, error) { + var size uint32 + wideField, err := windows.UTF16FromString(field) + if err != nil { + return nil, err + } + + r, _, msg := nCryptGetProperty.Call(hnd, uintptr(unsafe.Pointer(&wideField[0])), 0, 0, uintptr(unsafe.Pointer(&size)), 0) + if r != 0 { + return nil, fmt.Errorf("NCryptGetProperty returned %d,%X (%v) for key %q on size read", size, r, msg, field) + } + buff := make([]byte, size) + r, _, msg = nCryptGetProperty.Call(hnd, uintptr(unsafe.Pointer(&wideField[0])), uintptr(unsafe.Pointer(&buff[0])), uintptr(size), uintptr(unsafe.Pointer(&size)), 0) + if r != 0 { + return nil, fmt.Errorf("NCryptGetProperty returned %X (%v) for key %q on data read", r, msg, field) + } + return buff, nil +} + +// winPCP represents a reference to the Platform Crypto Provider. +type winPCP struct { + hProv uintptr +} + +// tbsDeviceInfo represents TPM device information from the TBS +// API. This structure is identical to _TBS_DEVICE_INFO in tbs.h. +type tbsDeviceInfo struct { + TBSVersion uint32 + TPMVersion uint32 + TPMInterfaceType uint32 + TPMImplementationRevision uint32 +} + +// windowsTPMInfo describes the versions of the TPM and OS interface code. +type windowsTPMInfo struct { + Manufacturer string + PCPVersion string + TBSInfo tbsDeviceInfo +} + +// TPMInfo returns version information about the TPM & OS interface code. +func (h *winPCP) TPMInfo() (*windowsTPMInfo, error) { + var err error + out := &windowsTPMInfo{} + + buf, err := getNCryptBufferProperty(h.hProv, "PCP_PLATFORM_TYPE") + if err != nil { + return nil, fmt.Errorf("Failed to read PCP_PLATFORM_TYPE: %v", err) + } + out.Manufacturer, err = utf16ToString(buf) + if err != nil { + return nil, err + } + + buf, err = getNCryptBufferProperty(h.hProv, "PCP_PROVIDER_VERSION") + if err != nil { + return nil, fmt.Errorf("Failed to read PCP_PROVIDER_VERSION: %v", err) + } + out.PCPVersion, err = utf16ToString(buf) + if err != nil { + return nil, err + } + + r, _, msg := tbsGetDeviceInfo.Call(unsafe.Sizeof(out.TBSInfo), uintptr(unsafe.Pointer(&out.TBSInfo))) + if r != 0 { + return nil, fmt.Errorf("Failed to call Tbsi_GetDeviceInfo: %v", msg) + } + + return out, nil +} + +// TPMCommandInterface returns an interface where TPM commands can issued directly. +func (h *winPCP) TPMCommandInterface() (io.ReadWriteCloser, error) { + var provTBS tpmtbs.Context + var sz uint32 + platformHndField, err := windows.UTF16FromString("PCP_PLATFORMHANDLE") + if err != nil { + return nil, err + } + + r, _, err := nCryptGetProperty.Call(h.hProv, uintptr(unsafe.Pointer(&platformHndField[0])), uintptr(unsafe.Pointer(&provTBS)), unsafe.Sizeof(provTBS), uintptr(unsafe.Pointer(&sz)), 0) + if r != 0 { + return nil, fmt.Errorf("NCryptGetProperty for platform handle returned %X (%v)", r, err) + } + + return tpmutil.FromContext(provTBS), nil +} + +// TPMKeyHandle returns a transient handle to the given key on the TPM. +func (h *winPCP) TPMKeyHandle(hnd uintptr) (tpmutil.Handle, error) { + var keyHndTBS tpmutil.Handle + var sz uint32 + platformHndField, err := windows.UTF16FromString("PCP_PLATFORMHANDLE") + if err != nil { + return 0, err + } + + if r, _, err := nCryptGetProperty.Call(hnd, uintptr(unsafe.Pointer(&platformHndField[0])), uintptr(unsafe.Pointer(&keyHndTBS)), unsafe.Sizeof(keyHndTBS), uintptr(unsafe.Pointer(&sz)), 0); r != 0 { + return 0, fmt.Errorf("NCryptGetProperty for hKey platform handle returned %X (%v)", r, err) + } + + return keyHndTBS, nil +} + +// Close releases all resources managed by the Handle. +func (h *winPCP) Close() error { + return closeNCryptObject(h.hProv) +} + +// EKCerts returns the Endorsement Certificates. +// Failure to fetch an ECC certificate is not considered +// an error as they do not exist on all platforms. +func (h *winPCP) EKCerts() ([]*x509.Certificate, error) { + c, err := getPCPCerts(h.hProv, "PCP_RSA_EKNVCERT") + if err != nil { + return nil, err + } + eccCerts, err := getPCPCerts(h.hProv, "PCP_ECC_EKNVCERT") + if err == nil { // ECC certs are not present on all platforms + c = append(c, eccCerts...) + } + + var out []*x509.Certificate + for _, der := range c { + cert, err := x509.ParseCertificate(der) + if err != nil { + return nil, err + } + out = append(out, cert) + } + + return out, nil +} + +// getPCPCerts is a helper to iterate over a certificates in a cert store, +// whose handle was obtained by reading a specific property on a PCP handle. +func getPCPCerts(hProv uintptr, propertyName string) ([][]byte, error) { + var size, cryptCertHnd uintptr + utf16PropName, err := windows.UTF16FromString(propertyName) + if err != nil { + return nil, err + } + + r, _, msg := nCryptGetProperty.Call(hProv, uintptr(unsafe.Pointer(&utf16PropName[0])), uintptr(unsafe.Pointer(&cryptCertHnd)), 8, uintptr(unsafe.Pointer(&size)), 0) + if r != 0 { + return nil, fmt.Errorf("NCryptGetProperty returned %X, %v", r, msg) + } + defer crypt32CertCloseStore.Call(uintptr(unsafe.Pointer(cryptCertHnd)), 0) + + var out [][]byte + var certContext uintptr + for { + certContext, _, msg = crypt32CertEnumCertificatesInStore.Call(uintptr(unsafe.Pointer(cryptCertHnd)), certContext) + if certContext == 0 && msg != nil { + if errno, ok := msg.(syscall.Errno); ok { + // cryptENotFound is returned when there are no more certificates to iterate through. + if errno == cryptENotFound { + break + } + } + return nil, msg + } + cert := (*syscall.CertContext)(unsafe.Pointer(certContext)) + // Copy the buffer. This was taken straight from the Go source: src/crypto/x509/root_windows.go#L70 + buf := (*[1 << 20]byte)(unsafe.Pointer(cert.EncodedCert))[:] + buf2 := make([]byte, cert.Length) + copy(buf2, buf) + out = append(out, buf2) + } + return out, nil +} + +// MintAIK creates a persistent attestation key of the specified name. +func (h *winPCP) MintAIK(name string) (uintptr, error) { + var kh uintptr + utf16Name, err := windows.UTF16FromString(name) + if err != nil { + return 0, err + } + utf16RSA, err := windows.UTF16FromString("RSA") + if err != nil { + return 0, err + } + + // Create a persistent RSA key of the specified name. + r, _, msg := nCryptCreatePersistedKey.Call(h.hProv, uintptr(unsafe.Pointer(&kh)), uintptr(unsafe.Pointer(&utf16RSA[0])), uintptr(unsafe.Pointer(&utf16Name[0])), 0, 0) + if r != 0 { + return 0, fmt.Errorf("NCryptCreatePersistedKey returned %X: %v", r, msg) + } + // Specify generated key length to be 2048 bits. + utf16Length, err := windows.UTF16FromString("Length") + if err != nil { + return 0, err + } + var length uint32 = 2048 + r, _, msg = nCryptSetProperty.Call(kh, uintptr(unsafe.Pointer(&utf16Length[0])), uintptr(unsafe.Pointer(&length)), unsafe.Sizeof(length), 0) + if r != 0 { + return 0, fmt.Errorf("NCryptSetProperty (Length) returned %X: %v", r, msg) + } + // Specify the generated key can only be used for identity attestation. + utf16KeyPolicy, err := windows.UTF16FromString("PCP_KEY_USAGE_POLICY") + if err != nil { + return 0, err + } + var policy uint32 = nCryptPropertyPCPKeyUsagePolicyIdentity + r, _, msg = nCryptSetProperty.Call(kh, uintptr(unsafe.Pointer(&utf16KeyPolicy[0])), uintptr(unsafe.Pointer(&policy)), unsafe.Sizeof(policy), 0) + if r != 0 { + return 0, fmt.Errorf("NCryptSetProperty (PCP KeyUsage Policy) returned %X: %v", r, msg) + } + + // Finalize (create) the key. + r, _, msg = nCryptFinalizeKey.Call(kh, 0) + if r != 0 { + return 0, fmt.Errorf("NCryptFinalizeKey returned %X: %v", r, msg) + } + + return kh, nil +} + +type aikProps struct { + RawPublic []byte + RawCreationData []byte + RawAttest []byte + RawSignature []byte +} + +// AIKProperties returns the binding properties of the given attestation +// key. Note that it is only valid to call this function with the same +// winPCP handle within which the AIK was created. +func (h *winPCP) AIKProperties(kh uintptr) (*aikProps, error) { + idBlob, err := getNCryptBufferProperty(kh, "PCP_TPM12_IDBINDING") + if err != nil { + return nil, err + } + r := bytes.NewReader(idBlob) + var out aikProps + + var publicSize uint16 + if err := binary.Read(r, binary.BigEndian, &publicSize); err != nil { + return nil, fmt.Errorf("failed to decode TPM2B_PUBLIC.size: %v", err) + } + out.RawPublic = make([]byte, publicSize) + if err := binary.Read(r, binary.BigEndian, &out.RawPublic); err != nil { + return nil, fmt.Errorf("failed to decode TPM2B_PUBLIC.data: %v", err) + } + + var creationDataSize uint16 + if err := binary.Read(r, binary.BigEndian, &creationDataSize); err != nil { + return nil, fmt.Errorf("failed to decode TPM2B_CREATION_DATA.size: %v", err) + } + out.RawCreationData = make([]byte, creationDataSize) + if err := binary.Read(r, binary.BigEndian, &out.RawCreationData); err != nil { + return nil, fmt.Errorf("failed to decode TPM2B_CREATION_DATA.data: %v", err) + } + + var attestSize uint16 + if err := binary.Read(r, binary.BigEndian, &attestSize); err != nil { + return nil, fmt.Errorf("failed to decode TPM2B_ATTEST.size: %v", err) + } + out.RawAttest = make([]byte, attestSize) + if err := binary.Read(r, binary.BigEndian, &out.RawAttest); err != nil { + return nil, fmt.Errorf("failed to decode TPM2B_ATTEST.data: %v", err) + } + + // The encoded TPMT_SIGNATURE structure represents the remaining bytes in + // the ID binding blob. + out.RawSignature = make([]byte, r.Len()) + if err := binary.Read(r, binary.BigEndian, &out.RawSignature); err != nil { + return nil, fmt.Errorf("failed to decode TPMT_SIGNATURE.data: %v", err) + } + + return &out, nil +} + +// LoadKeyByName returns a handle to the persistent PCP key with the specified +// name. +func (h *winPCP) LoadKeyByName(name string) (uintptr, error) { + utf16Name, err := windows.UTF16FromString(name) + if err != nil { + return 0, err + } + + var hKey uintptr + r, _, msg := nCryptOpenKey.Call(h.hProv, uintptr(unsafe.Pointer(&hKey)), uintptr(unsafe.Pointer(&utf16Name[0])), 0, 0) + if r != 0 { + return 0, msg + } + return hKey, nil +} + +// ActivateCredential performs TPM2_ActivateCredential or TPM_ActivateIdentity. +func (h *winPCP) ActivateCredential(hKey uintptr, activationBlob []byte) ([]byte, error) { + utf16ActivationStr, err := windows.UTF16FromString("PCP_TPM12_IDACTIVATION") + if err != nil { + return nil, err + } + + r, _, msg := nCryptSetProperty.Call(hKey, uintptr(unsafe.Pointer(&utf16ActivationStr[0])), uintptr(unsafe.Pointer(&activationBlob[0])), uintptr(len(activationBlob)), 0) + if r != 0 { + return nil, fmt.Errorf("NCryptSetProperty returned %X (%v) for key activation", r, msg) + } + + secretBuff := make([]byte, 256) + var size uint32 + r, _, msg = nCryptGetProperty.Call(hKey, uintptr(unsafe.Pointer(&utf16ActivationStr[0])), uintptr(unsafe.Pointer(&secretBuff[0])), uintptr(len(secretBuff)), uintptr(unsafe.Pointer(&size)), 0) + if r != 0 { + return nil, fmt.Errorf("NCryptGetProperty returned %X (%v) for key activation", r, msg) + } + return secretBuff[:size], nil +} + +// openPCP initializes a reference to the Microsoft PCP provider. +// The Caller is expected to call Close() when they are done. +func openPCP() (*winPCP, error) { + var err error + var h winPCP + pname, err := windows.UTF16FromString(pcpProviderName) + if err != nil { + return nil, err + } + + r, _, err := nCryptOpenStorageProvider.Call(uintptr(unsafe.Pointer(&h.hProv)), uintptr(unsafe.Pointer(&pname[0])), 0) + if r != 0 { // r is non-zero on error, err is always populated in this case. + return nil, err + } + return &h, nil +} diff --git a/attest/tpm.go b/attest/tpm.go new file mode 100644 index 0000000..4a23be6 --- /dev/null +++ b/attest/tpm.go @@ -0,0 +1,207 @@ +// Copyright 2019 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. +package attest + +import ( + "crypto" + "encoding/asn1" + "fmt" + "io" + "math/big" + "strings" + + "github.com/google/certificate-transparency-go/x509" + + "github.com/google/go-tpm/tpm2" + "github.com/google/go-tpm/tpmutil" +) + +const ( + tpmPtManufacturer = 0x00000100 + 5 // PT_FIXED + offset of 5 + tpmPtVendorString = 0x00000100 + 6 // PT_FIXED + offset of 6 + + // Defined in "Registry of reserved TPM 2.0 handles and localities". + nvramCertIndex = 0x1c00002 + + // Defined in "Registry of reserved TPM 2.0 handles and localities", and checked on a glinux machine. + commonSrkEquivalentHandle = 0x81000001 + commonEkEquivalentHandle = 0x81010001 +) + +var ( + aikTemplate = tpm2.Public{ + Type: tpm2.AlgRSA, + NameAlg: tpm2.AlgSHA256, + Attributes: tpm2.FlagSignerDefault | tpm2.FlagNoDA, + RSAParameters: &tpm2.RSAParams{ + Sign: &tpm2.SigScheme{ + Alg: tpm2.AlgRSASSA, + Hash: tpm2.AlgSHA256, + }, + KeyBits: 2048, + Modulus: big.NewInt(0), + }, + } + // Default EK template defined in: + // https://trustedcomputinggroup.org/wp-content/uploads/Credential_Profile_EK_V2.0_R14_published.pdf + defaultEKTemplate = tpm2.Public{ + Type: tpm2.AlgRSA, + NameAlg: tpm2.AlgSHA256, + Attributes: tpm2.FlagFixedTPM | tpm2.FlagFixedParent | tpm2.FlagSensitiveDataOrigin | + tpm2.FlagAdminWithPolicy | tpm2.FlagRestricted | tpm2.FlagDecrypt, + AuthPolicy: []byte{ + 0x83, 0x71, 0x97, 0x67, 0x44, 0x84, + 0xB3, 0xF8, 0x1A, 0x90, 0xCC, 0x8D, + 0x46, 0xA5, 0xD7, 0x24, 0xFD, 0x52, + 0xD7, 0x6E, 0x06, 0x52, 0x0B, 0x64, + 0xF2, 0xA1, 0xDA, 0x1B, 0x33, 0x14, + 0x69, 0xAA, + }, + RSAParameters: &tpm2.RSAParams{ + Symmetric: &tpm2.SymScheme{ + Alg: tpm2.AlgAES, + KeyBits: 128, + Mode: tpm2.AlgCFB, + }, + KeyBits: 2048, + Exponent: 0, + ModulusRaw: make([]byte, 256), + }, + } +) + +func readTPM2VendorAttributes(tpm io.ReadWriter) (TCGVendorID, string, error) { + var vendorInfo string + // The Vendor String is split up into 4 sections of 4 bytes, + // for a maximum length of 16 octets of ASCII text. We iterate + // through the 4 indexes to get all 16 bytes & construct vendorInfo. + // See: TPM_PT_VENDOR_STRING_1 in TPM 2.0 Structures reference. + for i := 0; i < 4; i++ { + caps, _, err := tpm2.GetCapability(tpm, tpm2.CapabilityTPMProperties, 1, tpmPtVendorString+uint32(i)) + if err != nil { + return TCGVendorID(0), "", fmt.Errorf("tpm2.GetCapability(PT_VENDOR_STRING_%d) failed: %v", i+1, err) + } + subset, ok := caps[0].(tpm2.TaggedProperty) + if !ok { + return TCGVendorID(0), "", fmt.Errorf("got capability of type %T, want tpm2.TaggedProperty", caps[0]) + } + // Reconstruct the 4 ASCII octets from the uint32 value. + vendorInfo += string(subset.Value&0xFF000000) + string(subset.Value&0xFF0000) + string(subset.Value&0xFF00) + string(subset.Value&0xFF) + } + + caps, _, err := tpm2.GetCapability(tpm, tpm2.CapabilityTPMProperties, 1, tpmPtManufacturer) + if err != nil { + return TCGVendorID(0), "", fmt.Errorf("tpm2.GetCapability(PT_MANUFACTURER) failed: %v", err) + } + manu, ok := caps[0].(tpm2.TaggedProperty) + if !ok { + return TCGVendorID(0), "", fmt.Errorf("got capability of type %T, want tpm2.TaggedProperty", caps[0]) + } + + return TCGVendorID(manu.Value), strings.Trim(vendorInfo, "\x00"), nil +} + +func parseCert(ekCert []byte) (*x509.Certificate, error) { + // If the cert parses fine without any changes, we are G2G. + if c, err := x509.ParseCertificate(ekCert); err == nil { + return c, nil + } + // There might be trailing nonsense in the cert, which Go + // does not parse correctly. As ASN1 data is TLV encoded, we should + // be able to just get the certificate, and then send that to Go's + // certificate parser. + var cert struct { + Raw asn1.RawContent + } + if _, err := asn1.Unmarshal(ekCert, &cert); err != nil { + return nil, err + } + return x509.ParseCertificate(cert.Raw) +} + +func readEKCertFromNVRAM20(tpm io.ReadWriter) (*x509.Certificate, error) { + ekCert, err := tpm2.NVReadEx(tpm, nvramCertIndex, tpm2.HandleOwner, "", 0) + if err != nil { + return nil, fmt.Errorf("reading EK cert: %v", err) + } + return parseCert(ekCert) +} + +func quote20(tpm io.ReadWriter, aikHandle tpmutil.Handle, hashAlg tpm2.Algorithm, nonce []byte) (*Quote, error) { + sel := tpm2.PCRSelection{Hash: hashAlg} + numPCRs := 24 + for pcr := 0; pcr < numPCRs; pcr++ { + sel.PCRs = append(sel.PCRs, pcr) + } + + quote, sig, err := tpm2.Quote(tpm, aikHandle, "", "", nonce, sel, tpm2.AlgNull) + if err != nil { + return nil, err + } + + rawSig, err := tpmutil.Pack(sig.Alg, sig.RSA.HashAlg, sig.RSA.Signature) + return &Quote{ + Version: TPMVersion20, + Quote: quote, + Signature: rawSig, + }, err +} + +func readAllPCRs20(tpm io.ReadWriter, alg tpm2.Algorithm) (map[uint32][]byte, error) { + numPCRs := 24 + out := map[uint32][]byte{} + + // The TPM 2.0 spec says that the TPM can partially fulfill the + // request. As such, we repeat the command up to 8 times to get all + // 24 PCRs. + for i := 0; i < numPCRs; i++ { + // Build a selection structure, specifying all PCRs we do + // not have the value for. + sel := tpm2.PCRSelection{Hash: alg} + for pcr := 0; pcr < numPCRs; pcr++ { + if _, present := out[uint32(pcr)]; !present { + sel.PCRs = append(sel.PCRs, pcr) + } + } + + // Ask the TPM for those PCR values. + ret, err := tpm2.ReadPCRs(tpm, sel) + if err != nil { + return nil, fmt.Errorf("tpm2.ReadPCRs(%+v) failed with err: %v", sel, err) + } + // Keep track of the PCRs we were actually given. + for pcr, digest := range ret { + out[uint32(pcr)] = digest + } + if len(out) == numPCRs { + break + } + } + + if len(out) != numPCRs { + return nil, fmt.Errorf("failed to read all PCRs, only read %d", len(out)) + } + + return out, nil +} + +func allPCRs20(tpm io.ReadWriter) (map[uint32][]byte, crypto.Hash, error) { + out256, err256 := readAllPCRs20(tpm, tpm2.AlgSHA256) + if err256 != nil { + // TPM may not implement active banks with SHA256 - try SHA1. + out1, err1 := readAllPCRs20(tpm, tpm2.AlgSHA1) + return out1, crypto.SHA1, err1 + } + return out256, crypto.SHA256, nil +} diff --git a/attest/tpm_linux.go b/attest/tpm_linux.go new file mode 100644 index 0000000..12d60a9 --- /dev/null +++ b/attest/tpm_linux.go @@ -0,0 +1,520 @@ +// Copyright 2019 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. +// +build linux + +package attest + +import ( + "crypto" + "crypto/rsa" + "encoding/binary" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "os" + "path" + "strings" + + "github.com/google/certificate-transparency-go/x509" + + "github.com/google/go-tspi/tspi" //for tpm12 support + "github.com/google/go-tspi/tspiconst" + + "github.com/google/go-tpm/tpm2" + "github.com/google/go-tpm/tpmutil" + "github.com/google/go-tspi/attestation" +) + +const ( + tpmRoot = "/sys/class/tpm" +) + +// TPM interfaces with a TPM device on the system. +type TPM struct { + version TPMVersion + interf TPMInterface + + sysPath string + rwc io.ReadWriteCloser + ctx *tspi.Context +} + +func probeSystemTPMs() ([]probedTPM, error) { + var tpms []probedTPM + + tpmDevs, err := ioutil.ReadDir(tpmRoot) + if err != nil && !os.IsNotExist(err) { + return nil, err + } + if err == nil { + for _, tpmDev := range tpmDevs { + if strings.HasPrefix(tpmDev.Name(), "tpm") { + tpm := probedTPM{ + Path: path.Join(tpmRoot, tpmDev.Name()), + } + + if _, err := os.Stat(path.Join(tpm.Path, "caps")); err != nil { + if !os.IsNotExist(err) { + return nil, err + } + tpm.Version = TPMVersion20 + } else { + tpm.Version = TPMVersion12 + } + tpms = append(tpms, tpm) + } + } + } + + return tpms, nil +} + +func openTPM(tpm probedTPM) (*TPM, error) { + interf := TPMInterfaceDirect + var rwc io.ReadWriteCloser + var ctx *tspi.Context + var err error + + switch tpm.Version { + case TPMVersion12: + // TPM1.2 must be using Daemon (Connect will fail if not the case) + interf = TPMInterfaceDaemonManaged + ctx, err = tspi.NewContext() + if err != nil { + return nil, err + } + + err = ctx.Connect() + if err != nil { + return nil, err + } + + case TPMVersion20: + // If the TPM has a kernel-provided resource manager, we should + // use that instead of communicating directly. + devPath := path.Join("/dev", path.Base(tpm.Path)) + f, err := ioutil.ReadDir(path.Join(tpm.Path, "device", "tpmrm")) + if err != nil { + if !os.IsNotExist(err) { + return nil, err + } + } else if len(f) > 0 { + devPath = path.Join("/dev", f[0].Name()) + interf = TPMInterfaceKernelManaged + } + + rwc, err = tpm2.OpenTPM(devPath) + if err != nil { + return nil, err + } + } + + return &TPM{ + version: tpm.Version, + interf: interf, + sysPath: tpm.Path, + rwc: rwc, + ctx: ctx, + }, nil +} + +// Close shuts down the connection to the TPM. +func (t *TPM) Close() error { + switch t.version { + case TPMVersion12: + return t.ctx.Close() + case TPMVersion20: + return t.rwc.Close() + default: + return fmt.Errorf("unsupported TPM version: %x", t.version) + } +} + +func readTPM12VendorAttributes(context *tspi.Context) (TCGVendorID, string, error) { + // TPM 1.2 doesn't seem to store vendor data (other than unique ID) + vendor, err := context.GetCapability(tspiconst.TSS_TPMCAP_PROPERTY, 4, tspiconst.TSS_TPMCAP_PROP_MANUFACTURER) + if err != nil { + return TCGVendorID(0), "", fmt.Errorf("tspi::Context::GetCapability failed: %v", err) + } + if len(vendor) > 4 { + return TCGVendorID(0), "", fmt.Errorf("expecting at most 32-bit VendorID, got %d-bit ID instead", len(vendor)*8) + } + vendorID := TCGVendorID(binary.BigEndian.Uint32(vendor)) + return vendorID, vendorID.String(), nil +} + +// Info returns information about the TPM. +func (t *TPM) Info() (*TPMInfo, error) { + var manufacturer TCGVendorID + var vendorInfo string + var err error + switch t.version { + case TPMVersion12: + manufacturer, vendorInfo, err = readTPM12VendorAttributes(t.ctx) + case TPMVersion20: + manufacturer, vendorInfo, err = readTPM2VendorAttributes(t.rwc) + default: + return nil, fmt.Errorf("unsupported TPM version: %x", t.version) + } + if err != nil { + return nil, err + } + + return &TPMInfo{ + Version: t.version, + Interface: t.interf, + VendorInfo: vendorInfo, + Manufacturer: manufacturer, + }, nil +} + +// Return value: handle, whether we generated a new one, error +func (t *TPM) getPrimaryKeyHandle(pHnd tpmutil.Handle) (tpmutil.Handle, bool, error) { + _, _, _, err := tpm2.ReadPublic(t.rwc, pHnd) + if err == nil { + // Found the persistent handle, assume it's the key we want. + return pHnd, false, nil + } + + ekHnd, _, err := tpm2.CreatePrimary(t.rwc, tpm2.HandleEndorsement, tpm2.PCRSelection{}, "", "", defaultEKTemplate) + if err != nil { + return 0, false, fmt.Errorf("EK CreatePrimary failed: %v", err) + } + defer tpm2.FlushContext(t.rwc, ekHnd) + + err = tpm2.EvictControl(t.rwc, "", tpm2.HandleOwner, ekHnd, pHnd) + if err != nil { + return 0, false, fmt.Errorf("EK EvictControl failed: %v", err) + } + + return pHnd, true, nil +} + +func readEKCertFromNVRAM12(ctx *tspi.Context) (*x509.Certificate, error) { + ekCert, err := attestation.GetEKCert(ctx) + if err != nil { + return nil, fmt.Errorf("reading EK cert: %v", err) + } + return parseCert(ekCert) +} + +// EKs returns the endorsement keys burned-in to the platform. +func (t *TPM) EKs() ([]PlatformEK, error) { + var cert *x509.Certificate + var err error + switch t.version { + case TPMVersion12: + cert, err = readEKCertFromNVRAM12(t.ctx) + + if err != nil { + return nil, fmt.Errorf("readEKCertFromNVRAM failed: %v", err) + } + + case TPMVersion20: + cert, err = readEKCertFromNVRAM20(t.rwc) + + if err != nil { + ekHnd, _, err := tpm2.CreatePrimary(t.rwc, tpm2.HandleEndorsement, tpm2.PCRSelection{}, "", "", defaultEKTemplate) + if err != nil { + return nil, fmt.Errorf("EK CreatePrimary failed: %v", err) + } + defer tpm2.FlushContext(t.rwc, ekHnd) + + pub, _, _, err := tpm2.ReadPublic(t.rwc, ekHnd) + if err != nil { + return nil, fmt.Errorf("EK ReadPublic failed: %v", err) + } + if pub.RSAParameters == nil { + return nil, errors.New("ECC EK not yet supported") + } + + return []PlatformEK{ + {nil, &rsa.PublicKey{E: int(pub.RSAParameters.Exponent), N: pub.RSAParameters.Modulus}}, + }, nil + } + + default: + return nil, fmt.Errorf("unsupported TPM version: %x", t.version) + } + + return []PlatformEK{ + {cert, cert.PublicKey}, + }, nil +} + +// Key represents a key bound to the TPM. +type Key struct { + hnd tpmutil.Handle + KeyEncoding KeyEncoding + TPMVersion TPMVersion + Purpose KeyPurpose + + KeyBlob []byte // exclusive to TPM1.2 + Public []byte // used by both TPM1.2 and 2.0 + CreateData []byte + CreateAttestation []byte + CreateSignature []byte +} + +// Marshal represents the key in a persistent format which may be +// loaded at a later time using tpm.LoadKey(). +func (k *Key) Marshal() ([]byte, error) { + return json.Marshal(k) +} + +// Close frees any resources associated with the key. +func (k *Key) Close(tpm *TPM) error { + switch tpm.version { + case TPMVersion12: + return nil + case TPMVersion20: + return tpm2.FlushContext(tpm.rwc, k.hnd) + default: + return fmt.Errorf("unsupported TPM version: %x", tpm.version) + } +} + +// ActivateCredential decrypts the specified credential using key. +// This operation is synonymous with TPM2_ActivateCredential. +func (k *Key) ActivateCredential(t *TPM, in EncryptedCredential) ([]byte, error) { + switch t.version { + case TPMVersion12: + cred, err := attestation.AIKChallengeResponse(t.ctx, k.KeyBlob, in.Credential, in.Secret) + if err != nil { + return nil, fmt.Errorf("failed to refresh aik: %v", err) + } + return cred, nil + + case TPMVersion20: + ekHnd, _, err := t.getPrimaryKeyHandle(commonEkEquivalentHandle) + if err != nil { + return nil, err + } + + sessHandle, _, err := tpm2.StartAuthSession( + t.rwc, + tpm2.HandleNull, /*tpmKey*/ + tpm2.HandleNull, /*bindKey*/ + make([]byte, 16), /*nonceCaller*/ + nil, /*secret*/ + tpm2.SessionPolicy, + tpm2.AlgNull, + tpm2.AlgSHA256) + if err != nil { + return nil, fmt.Errorf("creating session: %v", err) + } + defer tpm2.FlushContext(t.rwc, sessHandle) + + if _, err := tpm2.PolicySecret(t.rwc, tpm2.HandleEndorsement, tpm2.AuthCommand{Session: tpm2.HandlePasswordSession, Attributes: tpm2.AttrContinueSession}, sessHandle, nil, nil, nil, 0); err != nil { + return nil, fmt.Errorf("tpm2.PolicySecret() failed: %v", err) + } + + return tpm2.ActivateCredentialUsingAuth(t.rwc, []tpm2.AuthCommand{ + {Session: tpm2.HandlePasswordSession, Attributes: tpm2.AttrContinueSession}, + {Session: sessHandle, Attributes: tpm2.AttrContinueSession}, + }, k.hnd, ekHnd, in.Credential[2:], in.Secret[2:]) + + default: + return nil, fmt.Errorf("unsupported TPM version: %x", t.version) + + } +} + +func (k *Key) quote12(ctx *tspi.Context, nonce []byte) (*Quote, error) { + quote, rawSig, err := attestation.GetQuote(ctx, k.KeyBlob, nonce) + if err != nil { + return nil, fmt.Errorf("GetQuote() failed: %v", err) + } + + return &Quote{ + Version: TPMVersion12, + Quote: quote, + Signature: rawSig, + }, nil +} + +// Quote returns a quote over the platform state, signed by the key. +func (k *Key) Quote(t *TPM, nonce []byte, alg tpm2.Algorithm) (*Quote, error) { + switch t.version { + case TPMVersion12: + return k.quote12(t.ctx, nonce) + + case TPMVersion20: + return quote20(t.rwc, k.hnd, alg, nonce) + + default: + return nil, fmt.Errorf("unsupported TPM version: %x", t.version) + } +} + +// MintAIK creates an attestation key. +func (t *TPM) MintAIK(opts *MintOptions) (*Key, error) { + switch t.version { + case TPMVersion12: + pub, blob, err := attestation.CreateAIK(t.ctx) + if err != nil { + return nil, fmt.Errorf("CreateAIK failed: %v", err) + } + + return &Key{ + KeyEncoding: KeyEncodingEncrypted, + TPMVersion: t.version, + Purpose: AttestationKey, + KeyBlob: blob, + Public: pub, + }, nil + + case TPMVersion20: + // TODO(jsonp): Abstract choice of hierarchy & parent. + keyHandle, pub, creationData, creationHash, tix, _, err := tpm2.CreatePrimaryEx(t.rwc, tpm2.HandleEndorsement, tpm2.PCRSelection{}, "", "", aikTemplate) + if err != nil { + return nil, fmt.Errorf("CreatePrimaryEx failed: %v", err) + } + + // If any errors occur, free the AIK's handle. + defer func() { + if err != nil { + tpm2.FlushContext(t.rwc, keyHandle) + } + }() + + // We can only certify the creation immediately afterwards, so we cache the result. + attestation, sig, err := tpm2.CertifyCreation(t.rwc, "", keyHandle, keyHandle, nil, creationHash, tpm2.SigScheme{tpm2.AlgRSASSA, tpm2.AlgSHA256, 0}, tix) + if err != nil { + return nil, fmt.Errorf("CertifyCreation failed: %v", err) + } + // Pack the raw structure into a TPMU_SIGNATURE. + signature, err := tpmutil.Pack(tpm2.AlgRSASSA, tpm2.AlgSHA256, sig) + if err != nil { + return nil, fmt.Errorf("failed to pack TPMT_SIGNATURE: %v", err) + } + + return &Key{ + hnd: keyHandle, + KeyEncoding: KeyEncodingParameterized, + TPMVersion: t.version, + Purpose: AttestationKey, + Public: pub, + CreateData: creationData, + CreateAttestation: attestation, + CreateSignature: signature, + }, nil + + default: + return nil, fmt.Errorf("unsupported TPM version: %x", t.version) + } +} + +// LoadKey loads a previously-created key into the TPM for use. +// A key loaded via this function needs to be closed with .Close(). +func (t *TPM) LoadKey(opaqueBlob []byte) (*Key, error) { + // TODO(b/124266168): Load under the key handle loaded by t.getPrimaryKeyHandle() + + var k Key + var err error + if err = json.Unmarshal(opaqueBlob, &k); err != nil { + return nil, fmt.Errorf("Unmarshal failed: %v", err) + } + + if k.TPMVersion != t.version { + return nil, errors.New("key TPM version does not match opened TPM") + } + if k.Purpose != AttestationKey { + return nil, fmt.Errorf("unsupported key kind: %x", k.Purpose) + } + + switch t.version { + case TPMVersion12: + if k.KeyEncoding != KeyEncodingEncrypted { + return nil, fmt.Errorf("unsupported key encoding: %x", k.KeyEncoding) + } + + case TPMVersion20: + if k.KeyEncoding != KeyEncodingParameterized { + return nil, fmt.Errorf("unsupported key encoding: %x", k.KeyEncoding) + } + if k.hnd, _, _, _, _, _, err = tpm2.CreatePrimaryEx(t.rwc, tpm2.HandleEndorsement, tpm2.PCRSelection{}, "", "", aikTemplate); err != nil { + return nil, fmt.Errorf("CreatePrimaryEx failed: %v", err) + } + } + + return &k, nil +} + +// allPCRs12 returns a map of all the PCR values on the TPM +func allPCRs12(ctx *tspi.Context) (map[uint32][]byte, error) { + tpm := ctx.GetTPM() + PCRlist, err := tpm.GetPCRValues() + if err != nil { + return nil, fmt.Errorf("failed to read PCRs: %v", err) + } + + PCRs := make(map[uint32][]byte) + for i := 0; i < len(PCRlist); i++ { + PCRs[(uint32)(i)] = PCRlist[i] + } + return PCRs, nil +} + +// PCRs returns the present value of all Platform Configuration Registers. +func (t *TPM) PCRs() (map[int]PCR, tpm2.Algorithm, error) { + var PCRs map[uint32][]byte + var alg crypto.Hash + var err error + + switch t.version { + case TPMVersion12: + PCRs, err = allPCRs12(t.ctx) + if err != nil { + return nil, 0, fmt.Errorf("failed to read PCRs: %v", err) + } + alg = crypto.SHA1 + + case TPMVersion20: + PCRs, alg, err = allPCRs20(t.rwc) + if err != nil { + return nil, 0, fmt.Errorf("failed to read PCRs: %v", err) + } + + default: + return nil, 0, fmt.Errorf("unsupported TPM version: %x", t.version) + } + + out := map[int]PCR{} + var lastAlg crypto.Hash + for index, digest := range PCRs { + out[int(index)] = PCR{ + Index: int(index), + Digest: digest, + DigestAlg: alg, + } + lastAlg = alg + } + + switch lastAlg { + case crypto.SHA1: + return out, tpm2.AlgSHA1, nil + case crypto.SHA256: + return out, tpm2.AlgSHA256, nil + default: + return nil, 0, fmt.Errorf("unexpected algorithm: %v", lastAlg) + } +} + +// MeasurementLog returns the present value of the System Measurement Log. +func (t *TPM) MeasurementLog() ([]byte, error) { + return ioutil.ReadFile("/sys/kernel/security/tpm0/binary_bios_measurements") +} diff --git a/attest/tpm_windows.go b/attest/tpm_windows.go new file mode 100644 index 0000000..f1a1671 --- /dev/null +++ b/attest/tpm_windows.go @@ -0,0 +1,303 @@ +// Copyright 2019 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. +// +build windows + +package attest + +import ( + "crypto" + "crypto/rand" + "encoding/json" + "errors" + "fmt" + + "github.com/google/go-tpm/tpm2" + tpmtbs "github.com/google/go-tpm/tpmutil/tbs" +) + +// TPM interfaces with a TPM device on the system. +type TPM struct { + version TPMVersion + pcp *winPCP +} + +func probeSystemTPMs() ([]probedTPM, error) { + // Windows systems appear to only support a single abstracted TPM. + // If we fail to initialize the Platform Crypto Provider, we assume + // a TPM is not present. + pcp, err := openPCP() + if err != nil { + return nil, nil + } + defer pcp.Close() + + info, err := pcp.TPMInfo() + if err != nil { + return nil, err + } + + var out probedTPM + out.Version, err = tbsConvertVersion(info.TBSInfo) + if err != nil { + return nil, err + } + + return []probedTPM{out}, nil +} + +func tbsConvertVersion(info tbsDeviceInfo) (TPMVersion, error) { + switch info.TPMVersion { + case 1: + return TPMVersion12, nil + case 2: + return TPMVersion20, nil + default: + return TPMVersionAgnostic, fmt.Errorf("TBSInfo.TPMVersion %d unsupported", info.TPMVersion) + } +} + +func openTPM(tpm probedTPM) (*TPM, error) { + pcp, err := openPCP() + if err != nil { + return nil, err + } + + info, err := pcp.TPMInfo() + if err != nil { + return nil, err + } + vers, err := tbsConvertVersion(info.TBSInfo) + if err != nil { + return nil, err + } + + return &TPM{ + pcp: pcp, + version: vers, + }, nil +} + +// Close shuts down the connection to the TPM. +func (t *TPM) Close() error { + return t.pcp.Close() +} + +// Info returns information about the TPM. +func (t *TPM) Info() (*TPMInfo, error) { + if t.version != TPMVersion20 { + return nil, ErrTPM12NotImplemented + } + + tpm, err := t.pcp.TPMCommandInterface() + if err != nil { + return nil, err + } + manufacturer, vendorInfo, err := readTPM2VendorAttributes(tpm) + if err != nil { + return nil, err + } + + return &TPMInfo{ + Version: t.version, + Interface: TPMInterfaceKernelManaged, + VendorInfo: vendorInfo, + Manufacturer: manufacturer, + }, nil +} + +// EKs returns the Endorsement Keys burned-in to the platform. +func (t *TPM) EKs() ([]PlatformEK, error) { + ekCerts, err := t.pcp.EKCerts() + if err != nil { + return nil, fmt.Errorf("could not read EKCerts from PCP: %v", err) + } + + var out []PlatformEK + for _, cert := range ekCerts { + out = append(out, PlatformEK{cert, cert.PublicKey}) + } + + // TODO(jsonp): Fallback to reading PCP_RSA_EKPUB/PCP_ECC_EKPUB, and maybe direct. + // if len(out) == 0 { + // ... + // } + + return out, nil +} + +// Key represents a key bound to the TPM. +type Key struct { + hnd uintptr + KeyEncoding KeyEncoding + TPMVersion TPMVersion + Purpose KeyPurpose + + PCPKeyName string + Public []byte + CreateData []byte + CreateAttestation []byte + CreateSignature []byte +} + +// Marshal represents the key in a persistent format which may be +// loaded at a later time using tpm.LoadKey(). +func (k *Key) Marshal() ([]byte, error) { + return json.Marshal(k) +} + +// ActivateCredential decrypts the specified credential using key. +// This operation is synonymous with TPM2_ActivateCredential. +func (k *Key) ActivateCredential(tpm *TPM, in EncryptedCredential) ([]byte, error) { + if tpm.version != TPMVersion20 { + return nil, ErrTPM12NotImplemented + } + return tpm.pcp.ActivateCredential(k.hnd, append(in.Credential, in.Secret...)) +} + +// Quote returns a quote over the platform state, signed by the key. +func (k *Key) Quote(t *TPM, nonce []byte, alg tpm2.Algorithm) (*Quote, error) { + if t.version != TPMVersion20 { + return nil, ErrTPM12NotImplemented + } + tpm, err := t.pcp.TPMCommandInterface() + if err != nil { + return nil, fmt.Errorf("TPMCommandInterface() failed: %v", err) + } + tpmKeyHnd, err := t.pcp.TPMKeyHandle(k.hnd) + if err != nil { + return nil, fmt.Errorf("TPMKeyHandle() failed: %v", err) + } + return quote20(tpm, tpmKeyHnd, alg, nonce) +} + +// Close frees any resources associated with the key. +func (k *Key) Close(tpm *TPM) error { + return closeNCryptObject(k.hnd) +} + +// MintAIK creates a persistent attestation key. The returned key must be +// closed with a call to key.Close() when the caller has finished using it. +func (t *TPM) MintAIK(opts *MintOptions) (*Key, error) { + if t.version != TPMVersion20 { + return nil, ErrTPM12NotImplemented + } + + nameHex := make([]byte, 5) + if n, err := rand.Read(nameHex); err != nil || n != len(nameHex) { + return nil, fmt.Errorf("rand.Read() failed with %d/%d bytes read and error: %v", n, len(nameHex), err) + } + name := fmt.Sprintf("aik-%x", nameHex) + + kh, err := t.pcp.MintAIK(name) + if err != nil { + return nil, fmt.Errorf("pcp failed to mint attestation key: %v", err) + } + props, err := t.pcp.AIKProperties(kh) + if err != nil { + closeNCryptObject(kh) + return nil, fmt.Errorf("pcp failed to read attestation key properties: %v", err) + } + + return &Key{ + hnd: kh, + KeyEncoding: KeyEncodingOSManaged, + TPMVersion: t.version, + Purpose: AttestationKey, + PCPKeyName: name, + Public: props.RawPublic, + CreateData: props.RawCreationData, + CreateAttestation: props.RawAttest, + CreateSignature: props.RawSignature, + }, nil +} + +// LoadKey loads a previously-created key into the TPM for use. +// A key loaded via this function needs to be closed with .Close(). +func (t *TPM) LoadKey(opaqueBlob []byte) (*Key, error) { + var k Key + var err error + if err = json.Unmarshal(opaqueBlob, &k); err != nil { + return nil, fmt.Errorf("Unmarshal failed: %v", err) + } + + if k.TPMVersion != t.version { + return nil, errors.New("key TPM version does not match opened TPM") + } + if k.KeyEncoding != KeyEncodingOSManaged { + return nil, fmt.Errorf("unsupported key encoding: %x", k.KeyEncoding) + } + if k.Purpose != AttestationKey { + return nil, fmt.Errorf("unsupported key kind: %x", k.Purpose) + } + + if k.hnd, err = t.pcp.LoadKeyByName(k.PCPKeyName); err != nil { + return nil, fmt.Errorf("pcp failed to load key: %v", err) + } + return &k, nil +} + +// PCRs returns the present value of all Platform Configuration Registers. +func (t *TPM) PCRs() (map[int]PCR, tpm2.Algorithm, error) { + if t.version != TPMVersion20 { + return nil, 0, ErrTPM12NotImplemented + } + tpm, err := t.pcp.TPMCommandInterface() + if err != nil { + return nil, 0, fmt.Errorf("TPMCommandInterface() failed: %v", err) + } + PCRs, alg, err := allPCRs20(tpm) + if err != nil { + return nil, 0, fmt.Errorf("failed to read PCRs: %v", err) + } + + out := map[int]PCR{} + var lastAlg crypto.Hash + for index, digest := range PCRs { + out[int(index)] = PCR{ + Index: int(index), + Digest: digest, + DigestAlg: alg, + } + lastAlg = alg + } + + switch lastAlg { + case crypto.SHA1: + return out, tpm2.AlgSHA1, nil + case crypto.SHA256: + return out, tpm2.AlgSHA256, nil + default: + return nil, 0, fmt.Errorf("unexpected algorithm: %v", lastAlg) + } +} + +// MeasurementLog returns the present value of the System Measurement Log. +func (t *TPM) MeasurementLog() ([]byte, error) { + context, err := tpmtbs.CreateContext(tpmtbs.TPMVersion20, tpmtbs.IncludeTPM20) + if err != nil { + return nil, err + } + defer context.Close() + + // Run command first with nil buffer to get required buffer size. + logLen, err := context.GetTCGLog(nil) + if err != nil { + return nil, err + } + logBuffer := make([]byte, logLen) + if _, err = context.GetTCGLog(logBuffer); err != nil { + return nil, err + } + return logBuffer, nil +} diff --git a/attest/vendors.go b/attest/vendors.go new file mode 100644 index 0000000..7fdb99d --- /dev/null +++ b/attest/vendors.go @@ -0,0 +1,44 @@ +// Copyright 2019 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. +package attest + +// TCGVendorID represents a unique TCG manufacturer code. +// The canonical reference used is located at: +// https://trustedcomputinggroup.org/wp-content/uploads/Vendor_ID_Registry_0-8_clean.pdf +type TCGVendorID uint32 + +var vendors = map[TCGVendorID]string{ + 1095582720: "AMD", + 1096043852: "Atmel", + 1112687437: "Broadcom", + 1229081856: "IBM", + 1229346816: "Infineon", + 1229870147: "Intel", + 1279610368: "Lenovo", + 1314082080: "National Semiconductor", + 1314150912: "Nationz", + 1314145024: "Nuvoton Technology", + 1363365709: "Qualcomm", + 1397576515: "SMSC", + 1398033696: "ST Microelectronics", + 1397576526: "Samsung", + 1397641984: "Sinosun", + 1415073280: "Texas Instruments", + 1464156928: "Winbond", + 1380926275: "Fuzhou Rockchip", +} + +func (id TCGVendorID) String() string { + return vendors[id] +}