metropolis: unify utility packages
One last sweeping rename / reshuffle.
We get rid of //metropolis/node/common and //golibs, unifying them into
a single //metropolis/pkg meta-package.
This is to be documented somwhere properly, but here's the new logic
behind selecting where to place a new library package:
- if it's specific to k8s-on-metropolis, put it in
//metropolis/node/kubernetes/*. This is a self-contained tree that
other paths cannot import from.
- if it's a big new subsystem of the metropolis core, put it in
//metropolis/node/core. This can be imported by anything in
//m/n (eg the Kubernetes code at //m/n/kubernetes
- otherwise, treat it as generic library that's part of the metropolis
project, and put it in //metropolis/pkg. This can be imported by
anything within //metropolis.
This will be followed up by a diff that updates visibility rules.
Test Plan: Pure refactor, CI only.
X-Origin-Diff: phab/D683
GitOrigin-RevId: 883e7f09a7d22d64e966d07bbe839454ed081c79
diff --git a/metropolis/pkg/tpm/BUILD.bazel b/metropolis/pkg/tpm/BUILD.bazel
new file mode 100644
index 0000000..d06ff37
--- /dev/null
+++ b/metropolis/pkg/tpm/BUILD.bazel
@@ -0,0 +1,22 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+ name = "go_default_library",
+ srcs = [
+ "credactivation_compat.go",
+ "tpm.go",
+ ],
+ importpath = "git.monogon.dev/source/nexantic.git/metropolis/pkg/tpm",
+ visibility = ["//visibility:public"],
+ deps = [
+ "//metropolis/pkg/logtree:go_default_library",
+ "//metropolis/pkg/sysfs:go_default_library",
+ "@com_github_gogo_protobuf//proto:go_default_library",
+ "@com_github_google_go_tpm//tpm2:go_default_library",
+ "@com_github_google_go_tpm//tpmutil:go_default_library",
+ "@com_github_google_go_tpm_tools//proto:go_default_library",
+ "@com_github_google_go_tpm_tools//tpm2tools:go_default_library",
+ "@com_github_pkg_errors//:go_default_library",
+ "@org_golang_x_sys//unix:go_default_library",
+ ],
+)
diff --git a/metropolis/pkg/tpm/credactivation_compat.go b/metropolis/pkg/tpm/credactivation_compat.go
new file mode 100644
index 0000000..039f8d5
--- /dev/null
+++ b/metropolis/pkg/tpm/credactivation_compat.go
@@ -0,0 +1,123 @@
+// Copyright 2020 The Monogon Project Authors.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+// 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 tpm
+
+// This file is adapted from github.com/google/go-tpm/tpm2/credactivation which outputs broken
+// challenges for unknown reasons. They use u16 length-delimited outputs for the challenge blobs
+// which is incorrect. Rather than rewriting the routine, we only applied minimal fixes to it
+// and skip the ECC part of the issue (because we would rather trust the proprietary RSA implementation).
+//
+// TODO(lorenz): I'll eventually deal with this upstream, but for now just fix it here (it's not that)
+// much code after all (https://github.com/google/go-tpm/issues/121)
+
+import (
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/hmac"
+ "crypto/rsa"
+ "fmt"
+ "io"
+
+ "github.com/google/go-tpm/tpm2"
+ "github.com/google/go-tpm/tpmutil"
+)
+
+const (
+ labelIdentity = "IDENTITY"
+ labelStorage = "STORAGE"
+ labelIntegrity = "INTEGRITY"
+)
+
+func generateRSA(aik *tpm2.HashValue, pub *rsa.PublicKey, symBlockSize int, secret []byte, rnd io.Reader) ([]byte, []byte, error) {
+ newAIKHash, err := aik.Alg.HashConstructor()
+ if err != nil {
+ return nil, nil, err
+ }
+
+ // The seed length should match the keysize used by the EKs symmetric cipher.
+ // For typical RSA EKs, this will be 128 bits (16 bytes).
+ // Spec: TCG 2.0 EK Credential Profile revision 14, section 2.1.5.1.
+ seed := make([]byte, symBlockSize)
+ if _, err := io.ReadFull(rnd, seed); err != nil {
+ return nil, nil, fmt.Errorf("generating seed: %v", err)
+ }
+
+ // Encrypt the seed value using the provided public key.
+ // See annex B, section 10.4 of the TPM specification revision 2 part 1.
+ label := append([]byte(labelIdentity), 0)
+ encSecret, err := rsa.EncryptOAEP(newAIKHash(), rnd, pub, seed, label)
+ if err != nil {
+ return nil, nil, fmt.Errorf("generating encrypted seed: %v", err)
+ }
+
+ // Generate the encrypted credential by convolving the seed with the digest of
+ // the AIK, and using the result as the key to encrypt the secret.
+ // See section 24.4 of TPM 2.0 specification, part 1.
+ aikNameEncoded, err := aik.Encode()
+ if err != nil {
+ return nil, nil, fmt.Errorf("encoding aikName: %v", err)
+ }
+ symmetricKey, err := tpm2.KDFa(aik.Alg, seed, labelStorage, aikNameEncoded, nil, len(seed)*8)
+ if err != nil {
+ return nil, nil, fmt.Errorf("generating symmetric key: %v", err)
+ }
+ c, err := aes.NewCipher(symmetricKey)
+ if err != nil {
+ return nil, nil, fmt.Errorf("symmetric cipher setup: %v", err)
+ }
+ cv, err := tpmutil.Pack(tpmutil.U16Bytes(secret))
+ if err != nil {
+ return nil, nil, fmt.Errorf("generating cv (TPM2B_Digest): %v", err)
+ }
+
+ // IV is all null bytes. encIdentity represents the encrypted credential.
+ encIdentity := make([]byte, len(cv))
+ cipher.NewCFBEncrypter(c, make([]byte, len(symmetricKey))).XORKeyStream(encIdentity, cv)
+
+ // Generate the integrity HMAC, which is used to protect the integrity of the
+ // encrypted structure.
+ // See section 24.5 of the TPM specification revision 2 part 1.
+ macKey, err := tpm2.KDFa(aik.Alg, seed, labelIntegrity, nil, nil, newAIKHash().Size()*8)
+ if err != nil {
+ return nil, nil, fmt.Errorf("generating HMAC key: %v", err)
+ }
+
+ mac := hmac.New(newAIKHash, macKey)
+ mac.Write(encIdentity)
+ mac.Write(aikNameEncoded)
+ integrityHMAC := mac.Sum(nil)
+
+ idObject := &tpm2.IDObject{
+ IntegrityHMAC: integrityHMAC,
+ EncIdentity: encIdentity,
+ }
+ id, err := tpmutil.Pack(idObject)
+ if err != nil {
+ return nil, nil, fmt.Errorf("encoding IDObject: %v", err)
+ }
+
+ packedID, err := tpmutil.Pack(id)
+ if err != nil {
+ return nil, nil, fmt.Errorf("packing id: %v", err)
+ }
+ packedEncSecret, err := tpmutil.Pack(encSecret)
+ if err != nil {
+ return nil, nil, fmt.Errorf("packing encSecret: %v", err)
+ }
+
+ return packedID, packedEncSecret, nil
+}
diff --git a/metropolis/pkg/tpm/eventlog/BUILD.bazel b/metropolis/pkg/tpm/eventlog/BUILD.bazel
new file mode 100644
index 0000000..94a7ee9
--- /dev/null
+++ b/metropolis/pkg/tpm/eventlog/BUILD.bazel
@@ -0,0 +1,17 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+ name = "go_default_library",
+ srcs = [
+ "compat.go",
+ "eventlog.go",
+ "secureboot.go",
+ ],
+ importpath = "git.monogon.dev/source/nexantic.git/metropolis/pkg/tpm/eventlog",
+ visibility = ["//visibility:public"],
+ deps = [
+ "//metropolis/pkg/tpm/eventlog/internal:go_default_library",
+ "@com_github_google_certificate_transparency_go//x509:go_default_library",
+ "@com_github_google_go_tpm//tpm2:go_default_library",
+ ],
+)
diff --git a/metropolis/pkg/tpm/eventlog/LICENSE-3RD-PARTY.txt b/metropolis/pkg/tpm/eventlog/LICENSE-3RD-PARTY.txt
new file mode 100644
index 0000000..2d3298c
--- /dev/null
+++ b/metropolis/pkg/tpm/eventlog/LICENSE-3RD-PARTY.txt
@@ -0,0 +1,12 @@
+Copyright 2020 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.
\ No newline at end of file
diff --git a/metropolis/pkg/tpm/eventlog/compat.go b/metropolis/pkg/tpm/eventlog/compat.go
new file mode 100644
index 0000000..f83972b
--- /dev/null
+++ b/metropolis/pkg/tpm/eventlog/compat.go
@@ -0,0 +1,32 @@
+// Copyright 2020 The Monogon Project Authors.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+// 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 eventlog
+
+// This file contains compatibility functions for our TPM library
+
+import (
+ "crypto"
+)
+
+// ConvertRawPCRs converts from raw PCRs to eventlog PCR structures
+func ConvertRawPCRs(pcrs [][]byte) []PCR {
+ var evPCRs []PCR
+ for i, digest := range pcrs {
+ evPCRs = append(evPCRs, PCR{DigestAlg: crypto.SHA256, Index: i, Digest: digest})
+ }
+ return evPCRs
+}
diff --git a/metropolis/pkg/tpm/eventlog/eventlog.go b/metropolis/pkg/tpm/eventlog/eventlog.go
new file mode 100644
index 0000000..49a8a26
--- /dev/null
+++ b/metropolis/pkg/tpm/eventlog/eventlog.go
@@ -0,0 +1,646 @@
+// Copyright 2020 The Monogon Project Authors.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+// 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.
+
+// Taken and pruned from go-attestation revision 2453c8f39a4ff46009f6a9db6fb7c6cca789d9a1 under Apache 2.0
+
+package eventlog
+
+import (
+ "bytes"
+ "crypto"
+ "crypto/sha1"
+ "crypto/sha256"
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "io"
+ "sort"
+
+ // Ensure hashes are available.
+ _ "crypto/sha256"
+
+ "github.com/google/go-tpm/tpm2"
+)
+
+// HashAlg identifies a hashing Algorithm.
+type HashAlg uint8
+
+// Valid hash algorithms.
+var (
+ HashSHA1 = HashAlg(tpm2.AlgSHA1)
+ HashSHA256 = HashAlg(tpm2.AlgSHA256)
+)
+
+func (a HashAlg) cryptoHash() crypto.Hash {
+ switch a {
+ case HashSHA1:
+ return crypto.SHA1
+ case HashSHA256:
+ return crypto.SHA256
+ }
+ return 0
+}
+
+func (a HashAlg) goTPMAlg() tpm2.Algorithm {
+ switch a {
+ case HashSHA1:
+ return tpm2.AlgSHA1
+ case HashSHA256:
+ return tpm2.AlgSHA256
+ }
+ return 0
+}
+
+// String returns a human-friendly representation of the hash algorithm.
+func (a HashAlg) String() string {
+ switch a {
+ case HashSHA1:
+ return "SHA1"
+ case HashSHA256:
+ return "SHA256"
+ }
+ return fmt.Sprintf("HashAlg<%d>", int(a))
+}
+
+// ReplayError describes the parsed events that failed to verify against
+// a particular PCR.
+type ReplayError struct {
+ Events []Event
+ invalidPCRs []int
+}
+
+func (e ReplayError) affected(pcr int) bool {
+ for _, p := range e.invalidPCRs {
+ if p == pcr {
+ return true
+ }
+ }
+ return false
+}
+
+// Error returns a human-friendly description of replay failures.
+func (e ReplayError) Error() string {
+ return fmt.Sprintf("event log failed to verify: the following registers failed to replay: %v", e.invalidPCRs)
+}
+
+// TPM algorithms. See the TPM 2.0 specification section 6.3.
+//
+// https://trustedcomputinggroup.org/wp-content/uploads/TPM-Rev-2.0-Part-2-Structures-01.38.pdf#page=42
+const (
+ algSHA1 uint16 = 0x0004
+ algSHA256 uint16 = 0x000B
+)
+
+// EventType indicates what kind of data an event is reporting.
+type EventType uint32
+
+// Event is a single event from a TCG event log. This reports descrete items such
+// as BIOs measurements or EFI states.
+type Event struct {
+ // order of the event in the event log.
+ sequence int
+
+ // PCR index of the event.
+ Index int
+ // Type of the event.
+ Type EventType
+
+ // Data of the event. For certain kinds of events, this must match the event
+ // digest to be valid.
+ Data []byte
+ // Digest is the verified digest of the event data. While an event can have
+ // multiple for different hash values, this is the one that was matched to the
+ // PCR value.
+ Digest []byte
+
+ // TODO(ericchiang): Provide examples or links for which event types must
+ // match their data to their digest.
+}
+
+func (e *Event) digestEquals(b []byte) error {
+ if len(e.Digest) == 0 {
+ return errors.New("no digests present")
+ }
+
+ switch len(e.Digest) {
+ case crypto.SHA256.Size():
+ s := sha256.Sum256(b)
+ if bytes.Equal(s[:], e.Digest) {
+ return nil
+ }
+ case crypto.SHA1.Size():
+ s := sha1.Sum(b)
+ if bytes.Equal(s[:], e.Digest) {
+ return nil
+ }
+ default:
+ return fmt.Errorf("cannot compare hash of length %d", len(e.Digest))
+ }
+
+ return fmt.Errorf("digest (len %d) does not match", len(e.Digest))
+}
+
+// EventLog is a parsed measurement log. This contains unverified data representing
+// boot events that must be replayed against PCR values to determine authenticity.
+type EventLog struct {
+ // Algs holds the set of algorithms that the event log uses.
+ Algs []HashAlg
+
+ rawEvents []rawEvent
+}
+
+func (e *EventLog) clone() *EventLog {
+ out := EventLog{
+ Algs: make([]HashAlg, len(e.Algs)),
+ rawEvents: make([]rawEvent, len(e.rawEvents)),
+ }
+ copy(out.Algs, e.Algs)
+ copy(out.rawEvents, e.rawEvents)
+ return &out
+}
+
+type elWorkaround struct {
+ id string
+ affectedPCR int
+ apply func(e *EventLog) error
+}
+
+// inject3 appends two new events into the event log.
+func inject3(e *EventLog, pcr int, data1, data2, data3 string) error {
+ if err := inject(e, pcr, data1); err != nil {
+ return err
+ }
+ if err := inject(e, pcr, data2); err != nil {
+ return err
+ }
+ return inject(e, pcr, data3)
+}
+
+// inject2 appends two new events into the event log.
+func inject2(e *EventLog, pcr int, data1, data2 string) error {
+ if err := inject(e, pcr, data1); err != nil {
+ return err
+ }
+ return inject(e, pcr, data2)
+}
+
+// inject appends a new event into the event log.
+func inject(e *EventLog, pcr int, data string) error {
+ evt := rawEvent{
+ data: []byte(data),
+ index: pcr,
+ sequence: e.rawEvents[len(e.rawEvents)-1].sequence + 1,
+ }
+ for _, alg := range e.Algs {
+ h := alg.cryptoHash().New()
+ h.Write([]byte(data))
+ evt.digests = append(evt.digests, digest{hash: alg.cryptoHash(), data: h.Sum(nil)})
+ }
+ e.rawEvents = append(e.rawEvents, evt)
+ return nil
+}
+
+const (
+ ebsInvocation = "Exit Boot Services Invocation"
+ ebsSuccess = "Exit Boot Services Returned with Success"
+ ebsFailure = "Exit Boot Services Returned with Failure"
+)
+
+var eventlogWorkarounds = []elWorkaround{
+ {
+ id: "EBS Invocation + Success",
+ affectedPCR: 5,
+ apply: func(e *EventLog) error {
+ return inject2(e, 5, ebsInvocation, ebsSuccess)
+ },
+ },
+ {
+ id: "EBS Invocation + Failure",
+ affectedPCR: 5,
+ apply: func(e *EventLog) error {
+ return inject2(e, 5, ebsInvocation, ebsFailure)
+ },
+ },
+ {
+ id: "EBS Invocation + Failure + Success",
+ affectedPCR: 5,
+ apply: func(e *EventLog) error {
+ return inject3(e, 5, ebsInvocation, ebsFailure, ebsSuccess)
+ },
+ },
+}
+
+// Verify replays the event log against a TPM's PCR values, returning the
+// events which could be matched to a provided PCR value.
+// An error is returned if the replayed digest for events with a given PCR
+// index do not match any provided value for that PCR index.
+func (e *EventLog) Verify(pcrs []PCR) ([]Event, error) {
+ events, err := e.verify(pcrs)
+ // If there were any issues replaying the PCRs, try each of the workarounds
+ // in turn.
+ // TODO(jsonp): Allow workarounds to be combined.
+ if rErr, isReplayErr := err.(ReplayError); isReplayErr {
+ for _, wkrd := range eventlogWorkarounds {
+ if !rErr.affected(wkrd.affectedPCR) {
+ continue
+ }
+ el := e.clone()
+ if err := wkrd.apply(el); err != nil {
+ return nil, fmt.Errorf("failed applying workaround %q: %v", wkrd.id, err)
+ }
+ if events, err := el.verify(pcrs); err == nil {
+ return events, nil
+ }
+ }
+ }
+
+ return events, err
+}
+
+// PCR encapsulates the value of a PCR at a point in time.
+type PCR struct {
+ Index int
+ Digest []byte
+ DigestAlg crypto.Hash
+}
+
+func (e *EventLog) verify(pcrs []PCR) ([]Event, error) {
+ events, err := replayEvents(e.rawEvents, pcrs)
+ if err != nil {
+ if _, isReplayErr := err.(ReplayError); isReplayErr {
+ return nil, err
+ }
+ return nil, fmt.Errorf("pcrs failed to replay: %v", err)
+ }
+ return events, nil
+}
+
+func extend(pcr PCR, replay []byte, e rawEvent) (pcrDigest []byte, eventDigest []byte, err error) {
+ h := pcr.DigestAlg
+
+ for _, digest := range e.digests {
+ if digest.hash != pcr.DigestAlg {
+ continue
+ }
+ if len(digest.data) != len(pcr.Digest) {
+ return nil, nil, fmt.Errorf("digest data length (%d) doesn't match PCR digest length (%d)", len(digest.data), len(pcr.Digest))
+ }
+ hash := h.New()
+ if len(replay) != 0 {
+ hash.Write(replay)
+ } else {
+ b := make([]byte, h.Size())
+ hash.Write(b)
+ }
+ hash.Write(digest.data)
+ return hash.Sum(nil), digest.data, nil
+ }
+ return nil, nil, fmt.Errorf("no event digest matches pcr algorithm: %v", pcr.DigestAlg)
+}
+
+// replayPCR replays the event log for a specific PCR, using pcr and
+// event digests with the algorithm in pcr. An error is returned if the
+// replayed values do not match the final PCR digest, or any event tagged
+// with that PCR does not posess an event digest with the specified algorithm.
+func replayPCR(rawEvents []rawEvent, pcr PCR) ([]Event, bool) {
+ var (
+ replay []byte
+ outEvents []Event
+ )
+
+ for _, e := range rawEvents {
+ if e.index != pcr.Index {
+ continue
+ }
+
+ replayValue, digest, err := extend(pcr, replay, e)
+ if err != nil {
+ return nil, false
+ }
+ replay = replayValue
+ outEvents = append(outEvents, Event{sequence: e.sequence, Data: e.data, Digest: digest, Index: pcr.Index, Type: e.typ})
+ }
+
+ if len(outEvents) > 0 && !bytes.Equal(replay, pcr.Digest) {
+ return nil, false
+ }
+ return outEvents, true
+}
+
+type pcrReplayResult struct {
+ events []Event
+ successful bool
+}
+
+func replayEvents(rawEvents []rawEvent, pcrs []PCR) ([]Event, error) {
+ var (
+ invalidReplays []int
+ verifiedEvents []Event
+ allPCRReplays = map[int][]pcrReplayResult{}
+ )
+
+ // Replay the event log for every PCR and digest algorithm combination.
+ for _, pcr := range pcrs {
+ events, ok := replayPCR(rawEvents, pcr)
+ allPCRReplays[pcr.Index] = append(allPCRReplays[pcr.Index], pcrReplayResult{events, ok})
+ }
+
+ // Record PCR indices which do not have any successful replay. Record the
+ // events for a successful replay.
+pcrLoop:
+ for i, replaysForPCR := range allPCRReplays {
+ for _, replay := range replaysForPCR {
+ if replay.successful {
+ // We consider the PCR verified at this stage: The replay of values with
+ // one digest algorithm matched a provided value.
+ // As such, we save the PCR's events, and proceed to the next PCR.
+ verifiedEvents = append(verifiedEvents, replay.events...)
+ continue pcrLoop
+ }
+ }
+ invalidReplays = append(invalidReplays, i)
+ }
+
+ if len(invalidReplays) > 0 {
+ events := make([]Event, 0, len(rawEvents))
+ for _, e := range rawEvents {
+ events = append(events, Event{e.sequence, e.index, e.typ, e.data, nil})
+ }
+ return nil, ReplayError{
+ Events: events,
+ invalidPCRs: invalidReplays,
+ }
+ }
+
+ sort.Slice(verifiedEvents, func(i int, j int) bool {
+ return verifiedEvents[i].sequence < verifiedEvents[j].sequence
+ })
+ return verifiedEvents, nil
+}
+
+// EV_NO_ACTION is a special event type that indicates information to the parser
+// instead of holding a measurement. For TPM 2.0, this event type is used to signal
+// switching from SHA1 format to a variable length digest.
+//
+// https://trustedcomputinggroup.org/wp-content/uploads/TCG_PCClientSpecPlat_TPM_2p0_1p04_pub.pdf#page=110
+const eventTypeNoAction = 0x03
+
+// ParseEventLog parses an unverified measurement log.
+func ParseEventLog(measurementLog []byte) (*EventLog, error) {
+ var specID *specIDEvent
+ r := bytes.NewBuffer(measurementLog)
+ parseFn := parseRawEvent
+ var el EventLog
+ e, err := parseFn(r, specID)
+ if err != nil {
+ return nil, fmt.Errorf("parse first event: %v", err)
+ }
+ if e.typ == eventTypeNoAction {
+ specID, err = parseSpecIDEvent(e.data)
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse spec ID event: %v", err)
+ }
+ for _, alg := range specID.algs {
+ switch tpm2.Algorithm(alg.ID) {
+ case tpm2.AlgSHA1:
+ el.Algs = append(el.Algs, HashSHA1)
+ case tpm2.AlgSHA256:
+ el.Algs = append(el.Algs, HashSHA256)
+ }
+ }
+ if len(el.Algs) == 0 {
+ return nil, fmt.Errorf("measurement log didn't use sha1 or sha256 digests")
+ }
+ // Switch to parsing crypto agile events. Don't include this in the
+ // replayed events since it intentionally doesn't extend the PCRs.
+ //
+ // Note that this doesn't actually guarentee that events have SHA256
+ // digests.
+ parseFn = parseRawEvent2
+ } else {
+ el.Algs = []HashAlg{HashSHA1}
+ el.rawEvents = append(el.rawEvents, e)
+ }
+ sequence := 1
+ for r.Len() != 0 {
+ e, err := parseFn(r, specID)
+ if err != nil {
+ return nil, err
+ }
+ e.sequence = sequence
+ sequence++
+ el.rawEvents = append(el.rawEvents, e)
+ }
+ return &el, nil
+}
+
+type specIDEvent struct {
+ algs []specAlgSize
+}
+
+type specAlgSize struct {
+ ID uint16
+ Size uint16
+}
+
+// Expected values for various Spec ID Event fields.
+// https://trustedcomputinggroup.org/wp-content/uploads/EFI-Protocol-Specification-rev13-160330final.pdf#page=19
+var wantSignature = [16]byte{0x53, 0x70,
+ 0x65, 0x63, 0x20, 0x49,
+ 0x44, 0x20, 0x45, 0x76,
+ 0x65, 0x6e, 0x74, 0x30,
+ 0x33, 0x00} // "Spec ID Event03\0"
+
+const (
+ wantMajor = 2
+ wantMinor = 0
+ wantErrata = 0
+)
+
+// parseSpecIDEvent parses a TCG_EfiSpecIDEventStruct structure from the reader.
+//
+// https://trustedcomputinggroup.org/wp-content/uploads/EFI-Protocol-Specification-rev13-160330final.pdf#page=18
+func parseSpecIDEvent(b []byte) (*specIDEvent, error) {
+ r := bytes.NewReader(b)
+ var header struct {
+ Signature [16]byte
+ PlatformClass uint32
+ VersionMinor uint8
+ VersionMajor uint8
+ Errata uint8
+ UintnSize uint8
+ NumAlgs uint32
+ }
+ if err := binary.Read(r, binary.LittleEndian, &header); err != nil {
+ return nil, fmt.Errorf("reading event header: %v", err)
+ }
+ if header.Signature != wantSignature {
+ return nil, fmt.Errorf("invalid spec id signature: %x", header.Signature)
+ }
+ if header.VersionMajor != wantMajor {
+ return nil, fmt.Errorf("invalid spec major version, got %02x, wanted %02x",
+ header.VersionMajor, wantMajor)
+ }
+ if header.VersionMinor != wantMinor {
+ return nil, fmt.Errorf("invalid spec minor version, got %02x, wanted %02x",
+ header.VersionMajor, wantMinor)
+ }
+
+ // TODO(ericchiang): Check errata? Or do we expect that to change in ways
+ // we're okay with?
+
+ specAlg := specAlgSize{}
+ e := specIDEvent{}
+ for i := 0; i < int(header.NumAlgs); i++ {
+ if err := binary.Read(r, binary.LittleEndian, &specAlg); err != nil {
+ return nil, fmt.Errorf("reading algorithm: %v", err)
+ }
+ e.algs = append(e.algs, specAlg)
+ }
+
+ var vendorInfoSize uint8
+ if err := binary.Read(r, binary.LittleEndian, &vendorInfoSize); err != nil {
+ return nil, fmt.Errorf("reading vender info size: %v", err)
+ }
+ if r.Len() != int(vendorInfoSize) {
+ return nil, fmt.Errorf("reading vendor info, expected %d remaining bytes, got %d", vendorInfoSize, r.Len())
+ }
+ return &e, nil
+}
+
+type digest struct {
+ hash crypto.Hash
+ data []byte
+}
+
+type rawEvent struct {
+ sequence int
+ index int
+ typ EventType
+ data []byte
+ digests []digest
+}
+
+// TPM 1.2 event log format. See "5.1 SHA1 Event Log Entry Format"
+// https://trustedcomputinggroup.org/wp-content/uploads/EFI-Protocol-Specification-rev13-160330final.pdf#page=15
+type rawEventHeader struct {
+ PCRIndex uint32
+ Type uint32
+ Digest [20]byte
+ EventSize uint32
+}
+
+type eventSizeErr struct {
+ eventSize uint32
+ logSize int
+}
+
+func (e *eventSizeErr) Error() string {
+ return fmt.Sprintf("event data size (%d bytes) is greater than remaining measurement log (%d bytes)", e.eventSize, e.logSize)
+}
+
+func parseRawEvent(r *bytes.Buffer, specID *specIDEvent) (event rawEvent, err error) {
+ var h rawEventHeader
+ if err = binary.Read(r, binary.LittleEndian, &h); err != nil {
+ return event, err
+ }
+ if h.EventSize == 0 {
+ return event, errors.New("event data size is 0")
+ }
+ if h.EventSize > uint32(r.Len()) {
+ return event, &eventSizeErr{h.EventSize, r.Len()}
+ }
+
+ data := make([]byte, int(h.EventSize))
+ if _, err := io.ReadFull(r, data); err != nil {
+ return event, err
+ }
+
+ digests := []digest{{hash: crypto.SHA1, data: h.Digest[:]}}
+
+ return rawEvent{
+ typ: EventType(h.Type),
+ data: data,
+ index: int(h.PCRIndex),
+ digests: digests,
+ }, nil
+}
+
+// TPM 2.0 event log format. See "5.2 Crypto Agile Log Entry Format"
+// https://trustedcomputinggroup.org/wp-content/uploads/EFI-Protocol-Specification-rev13-160330final.pdf#page=15
+type rawEvent2Header struct {
+ PCRIndex uint32
+ Type uint32
+}
+
+func parseRawEvent2(r *bytes.Buffer, specID *specIDEvent) (event rawEvent, err error) {
+ var h rawEvent2Header
+
+ if err = binary.Read(r, binary.LittleEndian, &h); err != nil {
+ return event, err
+ }
+ event.typ = EventType(h.Type)
+ event.index = int(h.PCRIndex)
+
+ // parse the event digests
+ var numDigests uint32
+ if err := binary.Read(r, binary.LittleEndian, &numDigests); err != nil {
+ return event, err
+ }
+
+ for i := 0; i < int(numDigests); i++ {
+ var algID uint16
+ if err := binary.Read(r, binary.LittleEndian, &algID); err != nil {
+ return event, err
+ }
+ var digest digest
+
+ for _, alg := range specID.algs {
+ if alg.ID != algID {
+ continue
+ }
+ if uint16(r.Len()) < alg.Size {
+ return event, fmt.Errorf("reading digest: %v", io.ErrUnexpectedEOF)
+ }
+ digest.data = make([]byte, alg.Size)
+ digest.hash = HashAlg(alg.ID).cryptoHash()
+ }
+ if len(digest.data) == 0 {
+ return event, fmt.Errorf("unknown algorithm ID %x", algID)
+ }
+ if _, err := io.ReadFull(r, digest.data); err != nil {
+ return event, err
+ }
+ event.digests = append(event.digests, digest)
+ }
+
+ // parse event data
+ var eventSize uint32
+ if err = binary.Read(r, binary.LittleEndian, &eventSize); err != nil {
+ return event, err
+ }
+ if eventSize == 0 {
+ return event, errors.New("event data size is 0")
+ }
+ if eventSize > uint32(r.Len()) {
+ return event, &eventSizeErr{eventSize, r.Len()}
+ }
+ event.data = make([]byte, int(eventSize))
+ if _, err := io.ReadFull(r, event.data); err != nil {
+ return event, err
+ }
+ return event, err
+}
diff --git a/metropolis/pkg/tpm/eventlog/internal/BUILD.bazel b/metropolis/pkg/tpm/eventlog/internal/BUILD.bazel
new file mode 100644
index 0000000..a73bcba
--- /dev/null
+++ b/metropolis/pkg/tpm/eventlog/internal/BUILD.bazel
@@ -0,0 +1,12 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+ name = "go_default_library",
+ srcs = ["events.go"],
+ importpath = "git.monogon.dev/source/nexantic.git/metropolis/pkg/tpm/eventlog/internal",
+ visibility = ["//metropolis/pkg/tpm/eventlog:__subpackages__"],
+ deps = [
+ "@com_github_google_certificate_transparency_go//asn1:go_default_library",
+ "@com_github_google_certificate_transparency_go//x509:go_default_library",
+ ],
+)
diff --git a/metropolis/pkg/tpm/eventlog/internal/events.go b/metropolis/pkg/tpm/eventlog/internal/events.go
new file mode 100644
index 0000000..d9b933b
--- /dev/null
+++ b/metropolis/pkg/tpm/eventlog/internal/events.go
@@ -0,0 +1,403 @@
+// Copyright 2020 The Monogon Project Authors.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+// 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.
+
+// Taken from go-attestation under Apache 2.0
+package internal
+
+import (
+ "bytes"
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "io"
+ "unicode/utf16"
+
+ "github.com/google/certificate-transparency-go/asn1"
+ "github.com/google/certificate-transparency-go/x509"
+)
+
+const (
+ // maxNameLen is the maximum accepted byte length for a name field.
+ // This value should be larger than any reasonable value.
+ maxNameLen = 2048
+ // maxDataLen is the maximum size in bytes of a variable data field.
+ // This value should be larger than any reasonable value.
+ maxDataLen = 1024 * 1024 // 1 Megabyte.
+)
+
+// GUIDs representing the contents of an UEFI_SIGNATURE_LIST.
+var (
+ hashSHA256SigGUID = efiGUID{0xc1c41626, 0x504c, 0x4092, [8]byte{0xac, 0xa9, 0x41, 0xf9, 0x36, 0x93, 0x43, 0x28}}
+ hashSHA1SigGUID = efiGUID{0x826ca512, 0xcf10, 0x4ac9, [8]byte{0xb1, 0x87, 0xbe, 0x01, 0x49, 0x66, 0x31, 0xbd}}
+ hashSHA224SigGUID = efiGUID{0x0b6e5233, 0xa65c, 0x44c9, [8]byte{0x94, 0x07, 0xd9, 0xab, 0x83, 0xbf, 0xc8, 0xbd}}
+ hashSHA384SigGUID = efiGUID{0xff3e5307, 0x9fd0, 0x48c9, [8]byte{0x85, 0xf1, 0x8a, 0xd5, 0x6c, 0x70, 0x1e, 0x01}}
+ hashSHA512SigGUID = efiGUID{0x093e0fae, 0xa6c4, 0x4f50, [8]byte{0x9f, 0x1b, 0xd4, 0x1e, 0x2b, 0x89, 0xc1, 0x9a}}
+ keyRSA2048SigGUID = efiGUID{0x3c5766e8, 0x269c, 0x4e34, [8]byte{0xaa, 0x14, 0xed, 0x77, 0x6e, 0x85, 0xb3, 0xb6}}
+ certRSA2048SHA256SigGUID = efiGUID{0xe2b36190, 0x879b, 0x4a3d, [8]byte{0xad, 0x8d, 0xf2, 0xe7, 0xbb, 0xa3, 0x27, 0x84}}
+ certRSA2048SHA1SigGUID = efiGUID{0x67f8444f, 0x8743, 0x48f1, [8]byte{0xa3, 0x28, 0x1e, 0xaa, 0xb8, 0x73, 0x60, 0x80}}
+ certX509SigGUID = efiGUID{0xa5c059a1, 0x94e4, 0x4aa7, [8]byte{0x87, 0xb5, 0xab, 0x15, 0x5c, 0x2b, 0xf0, 0x72}}
+ certHashSHA256SigGUID = efiGUID{0x3bd2a492, 0x96c0, 0x4079, [8]byte{0xb4, 0x20, 0xfc, 0xf9, 0x8e, 0xf1, 0x03, 0xed}}
+ certHashSHA384SigGUID = efiGUID{0x7076876e, 0x80c2, 0x4ee6, [8]byte{0xaa, 0xd2, 0x28, 0xb3, 0x49, 0xa6, 0x86, 0x5b}}
+ certHashSHA512SigGUID = efiGUID{0x446dbf63, 0x2502, 0x4cda, [8]byte{0xbc, 0xfa, 0x24, 0x65, 0xd2, 0xb0, 0xfe, 0x9d}}
+)
+
+// EventType describes the type of event signalled in the event log.
+type EventType uint32
+
+// BIOS Events (TCG PC Client Specific Implementation Specification for Conventional BIOS 1.21)
+const (
+ PrebootCert EventType = 0x00000000
+ PostCode EventType = 0x00000001
+ unused EventType = 0x00000002
+ NoAction EventType = 0x00000003
+ Separator EventType = 0x00000004
+ Action EventType = 0x00000005
+ EventTag EventType = 0x00000006
+ SCRTMContents EventType = 0x00000007
+ SCRTMVersion EventType = 0x00000008
+ CpuMicrocode EventType = 0x00000009
+ PlatformConfigFlags EventType = 0x0000000A
+ TableOfDevices EventType = 0x0000000B
+ CompactHash EventType = 0x0000000C
+ Ipl EventType = 0x0000000D
+ IplPartitionData EventType = 0x0000000E
+ NonhostCode EventType = 0x0000000F
+ NonhostConfig EventType = 0x00000010
+ NonhostInfo EventType = 0x00000011
+ OmitBootDeviceEvents EventType = 0x00000012
+)
+
+// EFI Events (TCG EFI Platform Specification Version 1.22)
+const (
+ EFIEventBase EventType = 0x80000000
+ EFIVariableDriverConfig EventType = 0x80000001
+ EFIVariableBoot EventType = 0x80000002
+ EFIBootServicesApplication EventType = 0x80000003
+ EFIBootServicesDriver EventType = 0x80000004
+ EFIRuntimeServicesDriver EventType = 0x80000005
+ EFIGPTEvent EventType = 0x80000006
+ EFIAction EventType = 0x80000007
+ EFIPlatformFirmwareBlob EventType = 0x80000008
+ EFIHandoffTables EventType = 0x80000009
+ EFIHCRTMEvent EventType = 0x80000010
+ EFIVariableAuthority EventType = 0x800000e0
+)
+
+// ErrSigMissingGUID is returned if an EFI_SIGNATURE_DATA structure was parsed
+// successfully, however was missing the SignatureOwner GUID. This case is
+// handled specially as a workaround for a bug relating to authority events.
+var ErrSigMissingGUID = errors.New("signature data was missing owner GUID")
+
+var eventTypeNames = map[EventType]string{
+ PrebootCert: "Preboot Cert",
+ PostCode: "POST Code",
+ unused: "Unused",
+ NoAction: "No Action",
+ Separator: "Separator",
+ Action: "Action",
+ EventTag: "Event Tag",
+ SCRTMContents: "S-CRTM Contents",
+ SCRTMVersion: "S-CRTM Version",
+ CpuMicrocode: "CPU Microcode",
+ PlatformConfigFlags: "Platform Config Flags",
+ TableOfDevices: "Table of Devices",
+ CompactHash: "Compact Hash",
+ Ipl: "IPL",
+ IplPartitionData: "IPL Partition Data",
+ NonhostCode: "Non-Host Code",
+ NonhostConfig: "Non-HostConfig",
+ NonhostInfo: "Non-Host Info",
+ OmitBootDeviceEvents: "Omit Boot Device Events",
+
+ EFIEventBase: "EFI Event Base",
+ EFIVariableDriverConfig: "EFI Variable Driver Config",
+ EFIVariableBoot: "EFI Variable Boot",
+ EFIBootServicesApplication: "EFI Boot Services Application",
+ EFIBootServicesDriver: "EFI Boot Services Driver",
+ EFIRuntimeServicesDriver: "EFI Runtime Services Driver",
+ EFIGPTEvent: "EFI GPT Event",
+ EFIAction: "EFI Action",
+ EFIPlatformFirmwareBlob: "EFI Platform Firmware Blob",
+ EFIVariableAuthority: "EFI Variable Authority",
+ EFIHandoffTables: "EFI Handoff Tables",
+ EFIHCRTMEvent: "EFI H-CRTM Event",
+}
+
+func (e EventType) String() string {
+ if s, ok := eventTypeNames[e]; ok {
+ return s
+ }
+ return fmt.Sprintf("EventType(0x%x)", uint32(e))
+}
+
+// UntrustedParseEventType returns the event type indicated by
+// the provided value.
+func UntrustedParseEventType(et uint32) (EventType, error) {
+ // "The value associated with a UEFI specific platform event type MUST be in
+ // the range between 0x80000000 and 0x800000FF, inclusive."
+ if (et < 0x80000000 && et > 0x800000FF) || (et < 0x0 && et > 0x12) {
+ return EventType(0), fmt.Errorf("event type not between [0x0, 0x12] or [0x80000000, 0x800000FF]: got %#x", et)
+ }
+ if _, ok := eventTypeNames[EventType(et)]; !ok {
+ return EventType(0), fmt.Errorf("unknown event type %#x", et)
+ }
+ return EventType(et), nil
+}
+
+// efiGUID represents the EFI_GUID type.
+// See section "2.3.1 Data Types" in the specification for more information.
+// type efiGUID [16]byte
+type efiGUID struct {
+ Data1 uint32
+ Data2 uint16
+ Data3 uint16
+ Data4 [8]byte
+}
+
+func (d efiGUID) String() string {
+ var u [8]byte
+ binary.BigEndian.PutUint32(u[:4], d.Data1)
+ binary.BigEndian.PutUint16(u[4:6], d.Data2)
+ binary.BigEndian.PutUint16(u[6:8], d.Data3)
+ return fmt.Sprintf("%x-%x-%x-%x-%x", u[:4], u[4:6], u[6:8], d.Data4[:2], d.Data4[2:])
+}
+
+// UEFIVariableDataHeader represents the leading fixed-size fields
+// within UEFI_VARIABLE_DATA.
+type UEFIVariableDataHeader struct {
+ VariableName efiGUID
+ UnicodeNameLength uint64 // uintN
+ VariableDataLength uint64 // uintN
+}
+
+// UEFIVariableData represents the UEFI_VARIABLE_DATA structure.
+type UEFIVariableData struct {
+ Header UEFIVariableDataHeader
+ UnicodeName []uint16
+ VariableData []byte // []int8
+}
+
+// ParseUEFIVariableData parses the data section of an event structured as
+// a UEFI variable.
+//
+// https://trustedcomputinggroup.org/wp-content/uploads/TCG_PCClient_Specific_Platform_Profile_for_TPM_2p0_1p04_PUBLIC.pdf#page=100
+func ParseUEFIVariableData(r io.Reader) (ret UEFIVariableData, err error) {
+ err = binary.Read(r, binary.LittleEndian, &ret.Header)
+ if err != nil {
+ return
+ }
+ if ret.Header.UnicodeNameLength > maxNameLen {
+ return UEFIVariableData{}, fmt.Errorf("unicode name too long: %d > %d", ret.Header.UnicodeNameLength, maxNameLen)
+ }
+ ret.UnicodeName = make([]uint16, ret.Header.UnicodeNameLength)
+ for i := 0; uint64(i) < ret.Header.UnicodeNameLength; i++ {
+ err = binary.Read(r, binary.LittleEndian, &ret.UnicodeName[i])
+ if err != nil {
+ return
+ }
+ }
+ if ret.Header.VariableDataLength > maxDataLen {
+ return UEFIVariableData{}, fmt.Errorf("variable data too long: %d > %d", ret.Header.VariableDataLength, maxDataLen)
+ }
+ ret.VariableData = make([]byte, ret.Header.VariableDataLength)
+ _, err = io.ReadFull(r, ret.VariableData)
+ return
+}
+
+func (v *UEFIVariableData) VarName() string {
+ return string(utf16.Decode(v.UnicodeName))
+}
+
+func (v *UEFIVariableData) SignatureData() (certs []x509.Certificate, hashes [][]byte, err error) {
+ return parseEfiSignatureList(v.VariableData)
+}
+
+// UEFIVariableAuthority describes the contents of a UEFI variable authority
+// event.
+type UEFIVariableAuthority struct {
+ Certs []x509.Certificate
+}
+
+// ParseUEFIVariableAuthority parses the data section of an event structured as
+// a UEFI variable authority.
+//
+// https://uefi.org/sites/default/files/resources/UEFI_Spec_2_8_final.pdf#page=1789
+func ParseUEFIVariableAuthority(r io.Reader) (UEFIVariableAuthority, error) {
+ v, err := ParseUEFIVariableData(r)
+ if err != nil {
+ return UEFIVariableAuthority{}, err
+ }
+ certs, err := parseEfiSignature(v.VariableData)
+ return UEFIVariableAuthority{Certs: certs}, err
+}
+
+// efiSignatureData represents the EFI_SIGNATURE_DATA type.
+// See section "31.4.1 Signature Database" in the specification for more information.
+type efiSignatureData struct {
+ SignatureOwner efiGUID
+ SignatureData []byte // []int8
+}
+
+// efiSignatureList represents the EFI_SIGNATURE_LIST type.
+// See section "31.4.1 Signature Database" in the specification for more information.
+type efiSignatureListHeader struct {
+ SignatureType efiGUID
+ SignatureListSize uint32
+ SignatureHeaderSize uint32
+ SignatureSize uint32
+}
+
+type efiSignatureList struct {
+ Header efiSignatureListHeader
+ SignatureData []byte
+ Signatures []byte
+}
+
+// parseEfiSignatureList parses a EFI_SIGNATURE_LIST structure.
+// The structure and related GUIDs are defined at:
+// https://uefi.org/sites/default/files/resources/UEFI_Spec_2_8_final.pdf#page=1790
+func parseEfiSignatureList(b []byte) ([]x509.Certificate, [][]byte, error) {
+ if len(b) < 28 {
+ // Being passed an empty signature list here appears to be valid
+ return nil, nil, nil
+ }
+ signatures := efiSignatureList{}
+ buf := bytes.NewReader(b)
+ certificates := []x509.Certificate{}
+ hashes := [][]byte{}
+
+ for buf.Len() > 0 {
+ err := binary.Read(buf, binary.LittleEndian, &signatures.Header)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ if signatures.Header.SignatureHeaderSize > maxDataLen {
+ return nil, nil, fmt.Errorf("signature header too large: %d > %d", signatures.Header.SignatureHeaderSize, maxDataLen)
+ }
+ if signatures.Header.SignatureListSize > maxDataLen {
+ return nil, nil, fmt.Errorf("signature list too large: %d > %d", signatures.Header.SignatureListSize, maxDataLen)
+ }
+
+ signatureType := signatures.Header.SignatureType
+ switch signatureType {
+ case certX509SigGUID: // X509 certificate
+ for sigOffset := 0; uint32(sigOffset) < signatures.Header.SignatureListSize-28; {
+ signature := efiSignatureData{}
+ signature.SignatureData = make([]byte, signatures.Header.SignatureSize-16)
+ err := binary.Read(buf, binary.LittleEndian, &signature.SignatureOwner)
+ if err != nil {
+ return nil, nil, err
+ }
+ err = binary.Read(buf, binary.LittleEndian, &signature.SignatureData)
+ if err != nil {
+ return nil, nil, err
+ }
+ cert, err := x509.ParseCertificate(signature.SignatureData)
+ if err != nil {
+ return nil, nil, err
+ }
+ sigOffset += int(signatures.Header.SignatureSize)
+ certificates = append(certificates, *cert)
+ }
+ case hashSHA256SigGUID: // SHA256
+ for sigOffset := 0; uint32(sigOffset) < signatures.Header.SignatureListSize-28; {
+ signature := efiSignatureData{}
+ signature.SignatureData = make([]byte, signatures.Header.SignatureSize-16)
+ err := binary.Read(buf, binary.LittleEndian, &signature.SignatureOwner)
+ if err != nil {
+ return nil, nil, err
+ }
+ err = binary.Read(buf, binary.LittleEndian, &signature.SignatureData)
+ if err != nil {
+ return nil, nil, err
+ }
+ hashes = append(hashes, signature.SignatureData)
+ sigOffset += int(signatures.Header.SignatureSize)
+ }
+ case keyRSA2048SigGUID:
+ err = errors.New("unhandled RSA2048 key")
+ case certRSA2048SHA256SigGUID:
+ err = errors.New("unhandled RSA2048-SHA256 key")
+ case hashSHA1SigGUID:
+ err = errors.New("unhandled SHA1 hash")
+ case certRSA2048SHA1SigGUID:
+ err = errors.New("unhandled RSA2048-SHA1 key")
+ case hashSHA224SigGUID:
+ err = errors.New("unhandled SHA224 hash")
+ case hashSHA384SigGUID:
+ err = errors.New("unhandled SHA384 hash")
+ case hashSHA512SigGUID:
+ err = errors.New("unhandled SHA512 hash")
+ case certHashSHA256SigGUID:
+ err = errors.New("unhandled X509-SHA256 hash metadata")
+ case certHashSHA384SigGUID:
+ err = errors.New("unhandled X509-SHA384 hash metadata")
+ case certHashSHA512SigGUID:
+ err = errors.New("unhandled X509-SHA512 hash metadata")
+ default:
+ err = fmt.Errorf("unhandled signature type %s", signatureType)
+ }
+ if err != nil {
+ return nil, nil, err
+ }
+ }
+ return certificates, hashes, nil
+}
+
+// EFISignatureData represents the EFI_SIGNATURE_DATA type.
+// See section "31.4.1 Signature Database" in the specification
+// for more information.
+type EFISignatureData struct {
+ SignatureOwner efiGUID
+ SignatureData []byte // []int8
+}
+
+func parseEfiSignature(b []byte) ([]x509.Certificate, error) {
+ certificates := []x509.Certificate{}
+
+ if len(b) < 16 {
+ return nil, fmt.Errorf("invalid signature: buffer smaller than header (%d < %d)", len(b), 16)
+ }
+
+ buf := bytes.NewReader(b)
+ signature := EFISignatureData{}
+ signature.SignatureData = make([]byte, len(b)-16)
+
+ if err := binary.Read(buf, binary.LittleEndian, &signature.SignatureOwner); err != nil {
+ return certificates, err
+ }
+ if err := binary.Read(buf, binary.LittleEndian, &signature.SignatureData); err != nil {
+ return certificates, err
+ }
+
+ cert, err := x509.ParseCertificate(signature.SignatureData)
+ if err == nil {
+ certificates = append(certificates, *cert)
+ } else {
+ // A bug in shim may cause an event to be missing the SignatureOwner GUID.
+ // We handle this, but signal back to the caller using ErrSigMissingGUID.
+ if _, isStructuralErr := err.(asn1.StructuralError); isStructuralErr {
+ var err2 error
+ cert, err2 = x509.ParseCertificate(b)
+ if err2 == nil {
+ certificates = append(certificates, *cert)
+ err = ErrSigMissingGUID
+ }
+ }
+ }
+ return certificates, err
+}
diff --git a/metropolis/pkg/tpm/eventlog/secureboot.go b/metropolis/pkg/tpm/eventlog/secureboot.go
new file mode 100644
index 0000000..46e1f95
--- /dev/null
+++ b/metropolis/pkg/tpm/eventlog/secureboot.go
@@ -0,0 +1,210 @@
+// Copyright 2020 The Monogon Project Authors.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+// 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.
+
+// Taken and pruned from go-attestation under Apache 2.0
+package eventlog
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+
+ "github.com/google/certificate-transparency-go/x509"
+
+ "git.monogon.dev/source/nexantic.git/metropolis/pkg/tpm/eventlog/internal"
+)
+
+// SecurebootState describes the secure boot status of a machine, as determined
+// by processing its event log.
+type SecurebootState struct {
+ Enabled bool
+
+ // PlatformKeys enumerates keys which can sign a key exchange key.
+ PlatformKeys []x509.Certificate
+ // PlatformKeys enumerates key hashes which can sign a key exchange key.
+ PlatformKeyHashes [][]byte
+
+ // ExchangeKeys enumerates keys which can sign a database of permitted or
+ // forbidden keys.
+ ExchangeKeys []x509.Certificate
+ // ExchangeKeyHashes enumerates key hashes which can sign a database or
+ // permitted or forbidden keys.
+ ExchangeKeyHashes [][]byte
+
+ // PermittedKeys enumerates keys which may sign binaries to run.
+ PermittedKeys []x509.Certificate
+ // PermittedHashes enumerates hashes which permit binaries to run.
+ PermittedHashes [][]byte
+
+ // ForbiddenKeys enumerates keys which must not permit a binary to run.
+ ForbiddenKeys []x509.Certificate
+ // ForbiddenKeys enumerates hashes which must not permit a binary to run.
+ ForbiddenHashes [][]byte
+
+ // PreSeparatorAuthority describes the use of a secure-boot key to authorize
+ // the execution of a binary before the separator.
+ PreSeparatorAuthority []x509.Certificate
+ // PostSeparatorAuthority describes the use of a secure-boot key to authorize
+ // the execution of a binary after the separator.
+ PostSeparatorAuthority []x509.Certificate
+}
+
+// ParseSecurebootState parses a series of events to determine the
+// configuration of secure boot on a device. An error is returned if
+// the state cannot be determined, or if the event log is structured
+// in such a way that it may have been tampered post-execution of
+// platform firmware.
+func ParseSecurebootState(events []Event) (*SecurebootState, error) {
+ // This algorithm verifies the following:
+ // - All events in PCR 7 have event types which are expected in PCR 7.
+ // - All events are parsable according to their event type.
+ // - All events have digests values corresponding to their data/event type.
+ // - No unverifiable events were present.
+ // - All variables are specified before the separator and never duplicated.
+ // - The SecureBoot variable has a value of 0 or 1.
+ // - If SecureBoot was 1 (enabled), authority events were present indicating
+ // keys were used to perform verification.
+ // - If SecureBoot was 1 (enabled), platform + exchange + database keys
+ // were specified.
+ // - No UEFI debugger was attached.
+
+ var (
+ out SecurebootState
+ seenSeparator bool
+ seenAuthority bool
+ seenVars = map[string]bool{}
+ )
+
+ for _, e := range events {
+ if e.Index != 7 {
+ continue
+ }
+
+ et, err := internal.UntrustedParseEventType(uint32(e.Type))
+ if err != nil {
+ return nil, fmt.Errorf("unrecognised event type: %v", err)
+ }
+
+ digestVerify := e.digestEquals(e.Data)
+ switch et {
+ case internal.Separator:
+ if seenSeparator {
+ return nil, fmt.Errorf("duplicate separator at event %d", e.sequence)
+ }
+ seenSeparator = true
+ if !bytes.Equal(e.Data, []byte{0, 0, 0, 0}) {
+ return nil, fmt.Errorf("invalid separator data at event %d: %v", e.sequence, e.Data)
+ }
+ if digestVerify != nil {
+ return nil, fmt.Errorf("invalid separator digest at event %d: %v", e.sequence, digestVerify)
+ }
+
+ case internal.EFIAction:
+ if string(e.Data) == "UEFI Debug Mode" {
+ return nil, errors.New("a UEFI debugger was present during boot")
+ }
+ return nil, fmt.Errorf("event %d: unexpected EFI action event", e.sequence)
+
+ case internal.EFIVariableDriverConfig:
+ v, err := internal.ParseUEFIVariableData(bytes.NewReader(e.Data))
+ if err != nil {
+ return nil, fmt.Errorf("failed parsing EFI variable at event %d: %v", e.sequence, err)
+ }
+ if _, seenBefore := seenVars[v.VarName()]; seenBefore {
+ return nil, fmt.Errorf("duplicate EFI variable %q at event %d", v.VarName(), e.sequence)
+ }
+ seenVars[v.VarName()] = true
+ if seenSeparator {
+ return nil, fmt.Errorf("event %d: variable %q specified after separator", e.sequence, v.VarName())
+ }
+
+ if digestVerify != nil {
+ return nil, fmt.Errorf("invalid digest for variable %q on event %d: %v", v.VarName(), e.sequence, digestVerify)
+ }
+
+ switch v.VarName() {
+ case "SecureBoot":
+ if len(v.VariableData) != 1 {
+ return nil, fmt.Errorf("event %d: SecureBoot data len is %d, expected 1", e.sequence, len(v.VariableData))
+ }
+ out.Enabled = v.VariableData[0] == 1
+ case "PK":
+ if out.PlatformKeys, out.PlatformKeyHashes, err = v.SignatureData(); err != nil {
+ return nil, fmt.Errorf("event %d: failed parsing platform keys: %v", e.sequence, err)
+ }
+ case "KEK":
+ if out.ExchangeKeys, out.ExchangeKeyHashes, err = v.SignatureData(); err != nil {
+ return nil, fmt.Errorf("event %d: failed parsing key exchange keys: %v", e.sequence, err)
+ }
+ case "db":
+ if out.PermittedKeys, out.PermittedHashes, err = v.SignatureData(); err != nil {
+ return nil, fmt.Errorf("event %d: failed parsing signature database: %v", e.sequence, err)
+ }
+ case "dbx":
+ if out.ForbiddenKeys, out.ForbiddenHashes, err = v.SignatureData(); err != nil {
+ return nil, fmt.Errorf("event %d: failed parsing forbidden signature database: %v", e.sequence, err)
+ }
+ }
+
+ case internal.EFIVariableAuthority:
+ a, err := internal.ParseUEFIVariableAuthority(bytes.NewReader(e.Data))
+ if err != nil {
+ // Workaround for: https://github.com/google/go-attestation/issues/157
+ if err == internal.ErrSigMissingGUID {
+ // Versions of shim which do not carry
+ // https://github.com/rhboot/shim/commit/8a27a4809a6a2b40fb6a4049071bf96d6ad71b50
+ // have an erroneous additional byte in the event, which breaks digest
+ // verification. If verification failed, we try removing the last byte.
+ if digestVerify != nil {
+ digestVerify = e.digestEquals(e.Data[:len(e.Data)-1])
+ }
+ } else {
+ return nil, fmt.Errorf("failed parsing EFI variable authority at event %d: %v", e.sequence, err)
+ }
+ }
+ seenAuthority = true
+ if digestVerify != nil {
+ return nil, fmt.Errorf("invalid digest for authority on event %d: %v", e.sequence, digestVerify)
+ }
+ if !seenSeparator {
+ out.PreSeparatorAuthority = append(out.PreSeparatorAuthority, a.Certs...)
+ } else {
+ out.PostSeparatorAuthority = append(out.PostSeparatorAuthority, a.Certs...)
+ }
+
+ default:
+ return nil, fmt.Errorf("unexpected event type: %v", et)
+ }
+ }
+
+ if !out.Enabled {
+ return &out, nil
+ }
+
+ if !seenAuthority {
+ return nil, errors.New("secure boot was enabled but no key was used")
+ }
+ if len(out.PlatformKeys) == 0 && len(out.PlatformKeyHashes) == 0 {
+ return nil, errors.New("secure boot was enabled but no platform keys were known")
+ }
+ if len(out.ExchangeKeys) == 0 && len(out.ExchangeKeyHashes) == 0 {
+ return nil, errors.New("secure boot was enabled but no key exchange keys were known")
+ }
+ if len(out.PermittedKeys) == 0 && len(out.PermittedHashes) == 0 {
+ return nil, errors.New("secure boot was enabled but no keys or hashes were permitted")
+ }
+ return &out, nil
+}
diff --git a/metropolis/pkg/tpm/tpm.go b/metropolis/pkg/tpm/tpm.go
new file mode 100644
index 0000000..29bd208
--- /dev/null
+++ b/metropolis/pkg/tpm/tpm.go
@@ -0,0 +1,561 @@
+// Copyright 2020 The Monogon Project Authors.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+// 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 tpm
+
+import (
+ "bytes"
+ "crypto"
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/x509"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/gogo/protobuf/proto"
+ tpmpb "github.com/google/go-tpm-tools/proto"
+ "github.com/google/go-tpm-tools/tpm2tools"
+ "github.com/google/go-tpm/tpm2"
+ "github.com/google/go-tpm/tpmutil"
+ "github.com/pkg/errors"
+ "golang.org/x/sys/unix"
+
+ "git.monogon.dev/source/nexantic.git/metropolis/pkg/logtree"
+ "git.monogon.dev/source/nexantic.git/metropolis/pkg/sysfs"
+)
+
+var (
+ // SecureBootPCRs are all PCRs that measure the current Secure Boot configuration.
+ // This is what we want if we rely on secure boot to verify boot integrity. The firmware
+ // hashes the secure boot policy and custom keys into the PCR.
+ //
+ // This requires an extra step that provisions the custom keys.
+ //
+ // Some background: https://mjg59.dreamwidth.org/48897.html?thread=1847297
+ // (the initramfs issue mentioned in the article has been solved by integrating
+ // it into the kernel binary, and we don't have a shim bootloader)
+ //
+ // PCR7 alone is not sufficient - it needs to be combined with firmware measurements.
+ SecureBootPCRs = []int{7}
+
+ // FirmwarePCRs are alle PCRs that contain the firmware measurements
+ // See https://trustedcomputinggroup.org/wp-content/uploads/TCG_EFI_Platform_1_22_Final_-v15.pdf
+ FirmwarePCRs = []int{
+ 0, // platform firmware
+ 2, // option ROM code
+ 3, // option ROM configuration and data
+ }
+
+ // FullSystemPCRs are all PCRs that contain any measurements up to the currently running EFI payload.
+ FullSystemPCRs = []int{
+ 0, // platform firmware
+ 1, // host platform configuration
+ 2, // option ROM code
+ 3, // option ROM configuration and data
+ 4, // EFI payload
+ }
+
+ // Using FullSystemPCRs is the most secure, but also the most brittle option since updating the EFI
+ // binary, updating the platform firmware, changing platform settings or updating the binary
+ // would invalidate the sealed data. It's annoying (but possible) to predict values for PCR4,
+ // and even more annoying for the firmware PCR (comparison to known values on similar hardware
+ // is the only thing that comes to mind).
+ //
+ // See also: https://github.com/mxre/sealkey (generates PCR4 from EFI image, BSD license)
+ //
+ // Using only SecureBootPCRs is the easiest and still reasonably secure, if we assume that the
+ // platform knows how to take care of itself (i.e. Intel Boot Guard), and that secure boot
+ // is implemented properly. It is, however, a much larger amount of code we need to trust.
+ //
+ // We do not care about PCR 5 (GPT partition table) since modifying it is harmless. All of
+ // the boot options and cmdline are hardcoded in the kernel image, and we use no bootloader,
+ // so there's no PCR for bootloader configuration or kernel cmdline.
+)
+
+var (
+ numSRTMPCRs = 16
+ srtmPCRs = tpm2.PCRSelection{Hash: tpm2.AlgSHA256, PCRs: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}}
+ // TCG Trusted Platform Module Library Level 00 Revision 0.99 Table 6
+ tpmGeneratedValue = uint32(0xff544347)
+)
+
+var (
+ // ErrNotExists is returned when no TPMs are available in the system
+ ErrNotExists = errors.New("no TPMs found")
+ // ErrNotInitialized is returned when this package was not initialized successfully
+ ErrNotInitialized = errors.New("no TPM was initialized")
+)
+
+// Singleton since the TPM is too
+var tpm *TPM
+
+// We're serializing all TPM operations since it has a limited number of handles and recovering
+// if it runs out is difficult to implement correctly. Might also be marginally more secure.
+var lock sync.Mutex
+
+// TPM represents a high-level interface to a connected TPM 2.0
+type TPM struct {
+ logger logtree.LeveledLogger
+ device io.ReadWriteCloser
+
+ // We keep the AK loaded since it's used fairly often and deriving it is expensive
+ akHandleCache tpmutil.Handle
+ akPublicKey crypto.PublicKey
+}
+
+// Initialize finds and opens the TPM (if any). If there is no TPM available it returns
+// ErrNotExists
+func Initialize(logger logtree.LeveledLogger) error {
+ lock.Lock()
+ defer lock.Unlock()
+ tpmDir, err := os.Open("/sys/class/tpm")
+ if err != nil {
+ return errors.Wrap(err, "failed to open sysfs TPM class")
+ }
+ defer tpmDir.Close()
+
+ tpms, err := tpmDir.Readdirnames(2)
+ if err != nil {
+ return errors.Wrap(err, "failed to read TPM device class")
+ }
+
+ if len(tpms) == 0 {
+ return ErrNotExists
+ }
+ if len(tpms) > 1 {
+ // If this is changed GetMeasurementLog() needs to be updated too
+ logger.Warningf("Found more than one TPM, using the first one")
+ }
+ tpmName := tpms[0]
+ ueventData, err := sysfs.ReadUevents(filepath.Join("/sys/class/tpm", tpmName, "uevent"))
+ majorDev, err := strconv.Atoi(ueventData["MAJOR"])
+ if err != nil {
+ return fmt.Errorf("failed to convert uevent: %w", err)
+ }
+ minorDev, err := strconv.Atoi(ueventData["MINOR"])
+ if err != nil {
+ return fmt.Errorf("failed to convert uevent: %w", err)
+ }
+ if err := unix.Mknod("/dev/tpm", 0600|unix.S_IFCHR, int(unix.Mkdev(uint32(majorDev), uint32(minorDev)))); err != nil {
+ return errors.Wrap(err, "failed to create TPM device node")
+ }
+ device, err := tpm2.OpenTPM("/dev/tpm")
+ if err != nil {
+ return errors.Wrap(err, "failed to open TPM")
+ }
+ tpm = &TPM{
+ device: device,
+ logger: logger,
+ }
+ return nil
+}
+
+// GenerateSafeKey uses two sources of randomness (Kernel & TPM) to generate the key
+func GenerateSafeKey(size uint16) ([]byte, error) {
+ lock.Lock()
+ defer lock.Unlock()
+ if tpm == nil {
+ return []byte{}, ErrNotInitialized
+ }
+ encryptionKeyHost := make([]byte, size)
+ if _, err := io.ReadFull(rand.Reader, encryptionKeyHost); err != nil {
+ return []byte{}, errors.Wrap(err, "failed to generate host portion of new key")
+ }
+ var encryptionKeyTPM []byte
+ for i := 48; i > 0; i-- {
+ tpmKeyPart, err := tpm2.GetRandom(tpm.device, size-uint16(len(encryptionKeyTPM)))
+ if err != nil {
+ return []byte{}, errors.Wrap(err, "failed to generate TPM portion of new key")
+ }
+ encryptionKeyTPM = append(encryptionKeyTPM, tpmKeyPart...)
+ if len(encryptionKeyTPM) >= int(size) {
+ break
+ }
+ }
+
+ if len(encryptionKeyTPM) != int(size) {
+ return []byte{}, fmt.Errorf("got incorrect amount of TPM randomess: %v, requested %v", len(encryptionKeyTPM), size)
+ }
+
+ encryptionKey := make([]byte, size)
+ for i := uint16(0); i < size; i++ {
+ encryptionKey[i] = encryptionKeyHost[i] ^ encryptionKeyTPM[i]
+ }
+ return encryptionKey, nil
+}
+
+// Seal seals sensitive data and only allows access if the current platform configuration in
+// matches the one the data was sealed on.
+func Seal(data []byte, pcrs []int) ([]byte, error) {
+ lock.Lock()
+ defer lock.Unlock()
+ if tpm == nil {
+ return []byte{}, ErrNotInitialized
+ }
+ srk, err := tpm2tools.StorageRootKeyRSA(tpm.device)
+ if err != nil {
+ return []byte{}, errors.Wrap(err, "failed to load TPM SRK")
+ }
+ defer srk.Close()
+ sealedKey, err := srk.Seal(pcrs, data)
+ sealedKeyRaw, err := proto.Marshal(sealedKey)
+ if err != nil {
+ return []byte{}, errors.Wrapf(err, "failed to marshal sealed data")
+ }
+ return sealedKeyRaw, nil
+}
+
+// Unseal unseals sensitive data if the current platform configuration allows and sealing constraints
+// allow it.
+func Unseal(data []byte) ([]byte, error) {
+ lock.Lock()
+ defer lock.Unlock()
+ if tpm == nil {
+ return []byte{}, ErrNotInitialized
+ }
+ srk, err := tpm2tools.StorageRootKeyRSA(tpm.device)
+ if err != nil {
+ return []byte{}, errors.Wrap(err, "failed to load TPM SRK")
+ }
+ defer srk.Close()
+
+ var sealedKey tpmpb.SealedBytes
+ if err := proto.Unmarshal(data, &sealedKey); err != nil {
+ return []byte{}, errors.Wrap(err, "failed to decode sealed data")
+ }
+ // Logging this for auditing purposes
+ pcrList := []string{}
+ for _, pcr := range sealedKey.Pcrs {
+ pcrList = append(pcrList, string(pcr))
+ }
+ tpm.logger.Infof("Attempting to unseal data protected with PCRs %s", strings.Join(pcrList, ","))
+ unsealedData, err := srk.Unseal(&sealedKey)
+ if err != nil {
+ return []byte{}, errors.Wrap(err, "failed to unseal data")
+ }
+ return unsealedData, nil
+}
+
+// Standard AK template for RSA2048 non-duplicatable restricted signing for attestation
+var akTemplate = tpm2.Public{
+ Type: tpm2.AlgRSA,
+ NameAlg: tpm2.AlgSHA256,
+ Attributes: tpm2.FlagSignerDefault,
+ RSAParameters: &tpm2.RSAParams{
+ Sign: &tpm2.SigScheme{
+ Alg: tpm2.AlgRSASSA,
+ Hash: tpm2.AlgSHA256,
+ },
+ KeyBits: 2048,
+ },
+}
+
+func loadAK() error {
+ var err error
+ // Rationale: The AK is an EK-equivalent key and used only for attestation. Using a non-primary
+ // key here would require us to store the wrapped version somewhere, which is inconvenient.
+ // This being a primary key in the Endorsement hierarchy means that it can always be recreated
+ // and can never be "destroyed". Under our security model this is of no concern since we identify
+ // a node by its IK (Identity Key) which we can destroy.
+ tpm.akHandleCache, tpm.akPublicKey, err = tpm2.CreatePrimary(tpm.device, tpm2.HandleEndorsement,
+ tpm2.PCRSelection{}, "", "", akTemplate)
+ return err
+}
+
+// Process documented in TCG EK Credential Profile 2.2.1
+func loadEK() (tpmutil.Handle, crypto.PublicKey, error) {
+ // The EK is a primary key which is supposed to be certified by the manufacturer of the TPM.
+ // Its public attributes are standardized in TCG EK Credential Profile 2.0 Table 1. These need
+ // to match exactly or we aren't getting the key the manufacturere signed. tpm2tools contains
+ // such a template already, so we're using that instead of redoing it ourselves.
+ // This ignores the more complicated ways EKs can be specified, the additional stuff you can do
+ // is just absolutely crazy (see 2.2.1.2 onward)
+ return tpm2.CreatePrimary(tpm.device, tpm2.HandleEndorsement,
+ tpm2.PCRSelection{}, "", "", tpm2tools.DefaultEKTemplateRSA())
+}
+
+// GetAKPublic gets the TPM2T_PUBLIC of the AK key
+func GetAKPublic() ([]byte, error) {
+ lock.Lock()
+ defer lock.Unlock()
+ if tpm == nil {
+ return []byte{}, ErrNotInitialized
+ }
+ if tpm.akHandleCache == tpmutil.Handle(0) {
+ if err := loadAK(); err != nil {
+ return []byte{}, fmt.Errorf("failed to load AK primary key: %w", err)
+ }
+ }
+ public, _, _, err := tpm2.ReadPublic(tpm.device, tpm.akHandleCache)
+ if err != nil {
+ return []byte{}, err
+ }
+ return public.Encode()
+}
+
+// TCG TPM v2.0 Provisioning Guidance v1.0 7.8 Table 2 and
+// TCG EK Credential Profile v2.1 2.2.1.4 de-facto Standard for Windows
+// These are both non-normative and reference Windows 10 documentation that's no longer available :(
+// But in practice this is what people are using, so if it's normative or not doesn't really matter
+const ekCertHandle = 0x01c00002
+
+// GetEKPublic gets the public key and (if available) Certificate of the EK
+func GetEKPublic() ([]byte, []byte, error) {
+ lock.Lock()
+ defer lock.Unlock()
+ if tpm == nil {
+ return []byte{}, []byte{}, ErrNotInitialized
+ }
+ ekHandle, publicRaw, err := loadEK()
+ if err != nil {
+ return []byte{}, []byte{}, fmt.Errorf("failed to load EK primary key: %w", err)
+ }
+ defer tpm2.FlushContext(tpm.device, ekHandle)
+ // Don't question the use of HandleOwner, that's the Standard™
+ ekCertRaw, err := tpm2.NVReadEx(tpm.device, ekCertHandle, tpm2.HandleOwner, "", 0)
+ if err != nil {
+ return []byte{}, []byte{}, err
+ }
+
+ publicKey, err := x509.MarshalPKIXPublicKey(publicRaw)
+ if err != nil {
+ return []byte{}, []byte{}, err
+ }
+
+ return publicKey, ekCertRaw, nil
+}
+
+// MakeAKChallenge generates a challenge for TPM residency and attributes of the AK
+func MakeAKChallenge(ekPubKey, akPub []byte, nonce []byte) ([]byte, []byte, error) {
+ ekPubKeyData, err := x509.ParsePKIXPublicKey(ekPubKey)
+ if err != nil {
+ return []byte{}, []byte{}, fmt.Errorf("failed to decode EK pubkey: %w", err)
+ }
+ akPubData, err := tpm2.DecodePublic(akPub)
+ if err != nil {
+ return []byte{}, []byte{}, fmt.Errorf("failed to decode AK public part: %w", err)
+ }
+ // Make sure we're attesting the right attributes (in particular Restricted)
+ if !akPubData.MatchesTemplate(akTemplate) {
+ return []byte{}, []byte{}, errors.New("the key being challenged is not a valid AK")
+ }
+ akName, err := akPubData.Name()
+ if err != nil {
+ return []byte{}, []byte{}, fmt.Errorf("failed to derive AK name: %w", err)
+ }
+ return generateRSA(akName.Digest, ekPubKeyData.(*rsa.PublicKey), 16, nonce, rand.Reader)
+}
+
+// SolveAKChallenge solves a challenge for TPM residency of the AK
+func SolveAKChallenge(credBlob, secretChallenge []byte) ([]byte, error) {
+ lock.Lock()
+ defer lock.Unlock()
+ if tpm == nil {
+ return []byte{}, ErrNotInitialized
+ }
+ if tpm.akHandleCache == tpmutil.Handle(0) {
+ if err := loadAK(); err != nil {
+ return []byte{}, fmt.Errorf("failed to load AK primary key: %w", err)
+ }
+ }
+
+ ekHandle, _, err := loadEK()
+ if err != nil {
+ return []byte{}, fmt.Errorf("failed to load EK: %w", err)
+ }
+ defer tpm2.FlushContext(tpm.device, ekHandle)
+
+ // This is necessary since the EK requires an endorsement handle policy in its session
+ // For us this is stupid because we keep all hierarchies open anyways since a) we cannot safely
+ // store secrets on the OS side pre-global unlock and b) it makes no sense in this security model
+ // since an uncompromised host OS will not let an untrusted entity attest as itself and a
+ // compromised OS can either not pass PCR policy checks or the game's already over (you
+ // successfully runtime-exploited a production Metropolis node)
+ endorsementSession, _, err := tpm2.StartAuthSession(
+ tpm.device,
+ tpm2.HandleNull,
+ tpm2.HandleNull,
+ make([]byte, 16),
+ nil,
+ tpm2.SessionPolicy,
+ tpm2.AlgNull,
+ tpm2.AlgSHA256)
+ if err != nil {
+ panic(err)
+ }
+ defer tpm2.FlushContext(tpm.device, endorsementSession)
+
+ _, err = tpm2.PolicySecret(tpm.device, tpm2.HandleEndorsement, tpm2.AuthCommand{Session: tpm2.HandlePasswordSession, Attributes: tpm2.AttrContinueSession}, endorsementSession, nil, nil, nil, 0)
+ if err != nil {
+ return []byte{}, fmt.Errorf("failed to make a policy secret session: %w", err)
+ }
+
+ for {
+ solution, err := tpm2.ActivateCredentialUsingAuth(tpm.device, []tpm2.AuthCommand{
+ {Session: tpm2.HandlePasswordSession, Attributes: tpm2.AttrContinueSession}, // Use standard no-password authentication
+ {Session: endorsementSession, Attributes: tpm2.AttrContinueSession}, // Use a full policy session for the EK
+ }, tpm.akHandleCache, ekHandle, credBlob, secretChallenge)
+ if warn, ok := err.(tpm2.Warning); ok && warn.Code == tpm2.RCRetry {
+ time.Sleep(100 * time.Millisecond)
+ continue
+ }
+ return solution, err
+ }
+}
+
+// FlushTransientHandles flushes all sessions and non-persistent handles
+func FlushTransientHandles() error {
+ lock.Lock()
+ defer lock.Unlock()
+ if tpm == nil {
+ return ErrNotInitialized
+ }
+ flushHandleTypes := []tpm2.HandleType{tpm2.HandleTypeTransient, tpm2.HandleTypeLoadedSession, tpm2.HandleTypeSavedSession}
+ for _, handleType := range flushHandleTypes {
+ handles, err := tpm2tools.Handles(tpm.device, handleType)
+ if err != nil {
+ return err
+ }
+ for _, handle := range handles {
+ if err := tpm2.FlushContext(tpm.device, handle); err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
+
+// AttestPlatform performs a PCR quote using the AK and returns the quote and its signature
+func AttestPlatform(nonce []byte) ([]byte, []byte, error) {
+ lock.Lock()
+ defer lock.Unlock()
+ if tpm == nil {
+ return []byte{}, []byte{}, ErrNotInitialized
+ }
+ if tpm.akHandleCache == tpmutil.Handle(0) {
+ if err := loadAK(); err != nil {
+ return []byte{}, []byte{}, fmt.Errorf("failed to load AK primary key: %w", err)
+ }
+ }
+ // We only care about SHA256 since SHA1 is weak. This is supported on at least GCE and
+ // Intel / AMD fTPM, which is good enough for now. Alg is null because that would just hash the
+ // nonce, which is dumb.
+ quote, signature, err := tpm2.Quote(tpm.device, tpm.akHandleCache, "", "", nonce, srtmPCRs,
+ tpm2.AlgNull)
+ if err != nil {
+ return []byte{}, []byte{}, fmt.Errorf("failed to quote PCRs: %w", err)
+ }
+ return quote, signature.RSA.Signature, err
+}
+
+// VerifyAttestPlatform verifies a given attestation. You can rely on all data coming back as being
+// from the TPM on which the AK is bound to.
+func VerifyAttestPlatform(nonce, akPub, quote, signature []byte) (*tpm2.AttestationData, error) {
+ hash := crypto.SHA256.New()
+ hash.Write(quote)
+
+ akPubData, err := tpm2.DecodePublic(akPub)
+ if err != nil {
+ return nil, fmt.Errorf("invalid AK: %w", err)
+ }
+ akPublicKey, err := akPubData.Key()
+ if err != nil {
+ return nil, fmt.Errorf("invalid AK: %w", err)
+ }
+ akRSAKey, ok := akPublicKey.(*rsa.PublicKey)
+ if !ok {
+ return nil, errors.New("invalid AK: invalid key type")
+ }
+
+ if err := rsa.VerifyPKCS1v15(akRSAKey, crypto.SHA256, hash.Sum(nil), signature); err != nil {
+ return nil, err
+ }
+
+ quoteData, err := tpm2.DecodeAttestationData(quote)
+ if err != nil {
+ return nil, err
+ }
+ // quoteData.Magic works together with the TPM's Restricted key attribute. If this attribute is set
+ // (which it needs to be for the AK to be considered valid) the TPM will not sign external data
+ // having this prefix with such a key. Only data that originates inside the TPM like quotes and
+ // key certifications can have this prefix and sill be signed by a restricted key. This check
+ // is thus vital, otherwise somebody can just feed the TPM an arbitrary attestation to sign with
+ // its AK and this function will happily accept the forged attestation.
+ if quoteData.Magic != tpmGeneratedValue {
+ return nil, errors.New("invalid TPM quote: data marker for internal data not set - forged attestation")
+ }
+ if quoteData.Type != tpm2.TagAttestQuote {
+ return nil, errors.New("invalid TPM qoute: not a TPM quote")
+ }
+ if !bytes.Equal(quoteData.ExtraData, nonce) {
+ return nil, errors.New("invalid TPM quote: wrong nonce")
+ }
+
+ return quoteData, nil
+}
+
+// GetPCRs returns all SRTM PCRs in-order
+func GetPCRs() ([][]byte, error) {
+ lock.Lock()
+ defer lock.Unlock()
+ if tpm == nil {
+ return [][]byte{}, ErrNotInitialized
+ }
+ pcrs := make([][]byte, numSRTMPCRs)
+
+ // The TPM can (and most do) return partial results. Let's just retry as many times as we have
+ // PCRs since each read should return at least one PCR.
+readLoop:
+ for i := 0; i < numSRTMPCRs; i++ {
+ sel := tpm2.PCRSelection{Hash: tpm2.AlgSHA256}
+ for pcrN := 0; pcrN < numSRTMPCRs; pcrN++ {
+ if len(pcrs[pcrN]) == 0 {
+ sel.PCRs = append(sel.PCRs, pcrN)
+ }
+ }
+
+ readPCRs, err := tpm2.ReadPCRs(tpm.device, sel)
+ if err != nil {
+ return nil, fmt.Errorf("failed to read PCRs: %w", err)
+ }
+
+ for pcrN, pcr := range readPCRs {
+ pcrs[pcrN] = pcr
+ }
+ for _, pcr := range pcrs {
+ // If at least one PCR is still not read, continue
+ if len(pcr) == 0 {
+ continue readLoop
+ }
+ }
+ break
+ }
+
+ return pcrs, nil
+}
+
+// GetMeasurmentLog returns the binary log of all data hashed into PCRs. The result can be parsed by eventlog.
+// As this library currently doesn't support extending PCRs it just returns the log as supplied by the EFI interface.
+func GetMeasurementLog() ([]byte, error) {
+ return ioutil.ReadFile("/sys/kernel/security/tpm0/binary_bios_measurements")
+}