Lorenz Brun | ae0d90d | 2019-09-05 17:53:56 +0200 | [diff] [blame] | 1 | // Copyright 2020 The Monogon Project Authors. |
| 2 | // |
| 3 | // SPDX-License-Identifier: Apache-2.0 |
| 4 | // |
| 5 | // Licensed under the Apache License, Version 2.0 (the "License"); |
| 6 | // you may not use this file except in compliance with the License. |
| 7 | // You may obtain a copy of the License at |
| 8 | // |
| 9 | // http://www.apache.org/licenses/LICENSE-2.0 |
| 10 | // |
| 11 | // Unless required by applicable law or agreed to in writing, software |
| 12 | // distributed under the License is distributed on an "AS IS" BASIS, |
| 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 14 | // See the License for the specific language governing permissions and |
| 15 | // limitations under the License. |
| 16 | |
| 17 | package tpm |
| 18 | |
| 19 | import ( |
| 20 | "crypto/rand" |
| 21 | "fmt" |
Hendrik Hofstadt | 0d7c91e | 2019-10-23 21:44:47 +0200 | [diff] [blame] | 22 | "git.monogon.dev/source/nexantic.git/core/pkg/sysfs" |
Lorenz Brun | ae0d90d | 2019-09-05 17:53:56 +0200 | [diff] [blame] | 23 | "io" |
| 24 | "os" |
| 25 | "path/filepath" |
Lorenz Brun | ae0d90d | 2019-09-05 17:53:56 +0200 | [diff] [blame] | 26 | "strconv" |
| 27 | "sync" |
| 28 | |
| 29 | "github.com/gogo/protobuf/proto" |
| 30 | tpmpb "github.com/google/go-tpm-tools/proto" |
| 31 | "github.com/google/go-tpm-tools/tpm2tools" |
| 32 | "github.com/google/go-tpm/tpm2" |
| 33 | "github.com/pkg/errors" |
| 34 | "go.uber.org/zap" |
| 35 | "golang.org/x/sys/unix" |
| 36 | ) |
| 37 | |
| 38 | var ( |
Leopold Schabel | 68c5875 | 2019-11-14 21:00:59 +0100 | [diff] [blame^] | 39 | // SecureBootPCRs are all PCRs that measure the current Secure Boot configuration. |
| 40 | // This is what we want if we rely on secure boot to verify boot integrity. The firmware |
| 41 | // hashes the secure boot policy and custom keys into the PCR. |
| 42 | // |
| 43 | // This requires an extra step that provisions the custom keys. |
| 44 | // |
| 45 | // Some background: https://mjg59.dreamwidth.org/48897.html?thread=1847297 |
| 46 | // (the initramfs issue mentioned in the article has been solved by integrating |
| 47 | // it into the kernel binary, and we don't have a shim bootloader) |
| 48 | // |
| 49 | // PCR7 alone is not sufficient - it needs to be combined with firmware measurements. |
Lorenz Brun | ae0d90d | 2019-09-05 17:53:56 +0200 | [diff] [blame] | 50 | SecureBootPCRs = []int{7} |
| 51 | |
| 52 | // FirmwarePCRs are alle PCRs that contain the firmware measurements |
| 53 | // See https://trustedcomputinggroup.org/wp-content/uploads/TCG_EFI_Platform_1_22_Final_-v15.pdf |
Leopold Schabel | 68c5875 | 2019-11-14 21:00:59 +0100 | [diff] [blame^] | 54 | FirmwarePCRs = []int{ |
| 55 | 0, // platform firmware |
| 56 | 2, // option ROM code |
| 57 | 3, // option ROM configuration and data |
| 58 | } |
Lorenz Brun | ae0d90d | 2019-09-05 17:53:56 +0200 | [diff] [blame] | 59 | |
Leopold Schabel | 68c5875 | 2019-11-14 21:00:59 +0100 | [diff] [blame^] | 60 | // FullSystemPCRs are all PCRs that contain any measurements up to the currently running EFI payload. |
| 61 | FullSystemPCRs = []int{ |
| 62 | 0, // platform firmware |
| 63 | 1, // host platform configuration |
| 64 | 2, // option ROM code |
| 65 | 3, // option ROM configuration and data |
| 66 | 4, // EFI payload |
| 67 | } |
| 68 | |
| 69 | // Using FullSystemPCRs is the most secure, but also the most brittle option since updating the EFI |
| 70 | // binary, updating the platform firmware, changing platform settings or updating the binary |
| 71 | // would invalidate the sealed data. It's annoying (but possible) to predict values for PCR4, |
| 72 | // and even more annoying for the firmware PCR (comparison to known values on similar hardware |
| 73 | // is the only thing that comes to mind). |
| 74 | // |
| 75 | // See also: https://github.com/mxre/sealkey (generates PCR4 from EFI image, BSD license) |
| 76 | // |
| 77 | // Using only SecureBootPCRs is the easiest and still reasonably secure, if we assume that the |
| 78 | // platform knows how to take care of itself (i.e. Intel Boot Guard), and that secure boot |
| 79 | // is implemented properly. It is, however, a much larger amount of code we need to trust. |
| 80 | // |
| 81 | // We do not care about PCR 5 (GPT partition table) since modifying it is harmless. All of |
| 82 | // the boot options and cmdline are hardcoded in the kernel image, and we use no bootloader, |
| 83 | // so there's no PCR for bootloader configuration or kernel cmdline. |
Lorenz Brun | ae0d90d | 2019-09-05 17:53:56 +0200 | [diff] [blame] | 84 | ) |
| 85 | |
| 86 | var ( |
| 87 | // ErrNotExists is returned when no TPMs are available in the system |
| 88 | ErrNotExists = errors.New("no TPMs found") |
| 89 | // ErrNotInitialized is returned when this package was not initialized successfully |
| 90 | ErrNotInitialized = errors.New("no TPM was initialized") |
| 91 | ) |
| 92 | |
| 93 | // Singleton since the TPM is too |
| 94 | var tpm *TPM |
| 95 | |
| 96 | // We're serializing all TPM operations since it has a limited number of handles and recovering |
| 97 | // if it runs out is difficult to implement correctly. Might also be marginally more secure. |
| 98 | var lock sync.Mutex |
| 99 | |
| 100 | // TPM represents a high-level interface to a connected TPM 2.0 |
| 101 | type TPM struct { |
| 102 | logger *zap.Logger |
| 103 | device io.ReadWriteCloser |
| 104 | } |
| 105 | |
| 106 | // Initialize finds and opens the TPM (if any). If there is no TPM available it returns |
| 107 | // ErrNotExists |
| 108 | func Initialize(logger *zap.Logger) error { |
| 109 | lock.Lock() |
| 110 | defer lock.Unlock() |
| 111 | tpmDir, err := os.Open("/sys/class/tpm") |
| 112 | if err != nil { |
| 113 | return errors.Wrap(err, "failed to open sysfs TPM class") |
| 114 | } |
| 115 | defer tpmDir.Close() |
| 116 | |
| 117 | tpms, err := tpmDir.Readdirnames(2) |
| 118 | if err != nil { |
| 119 | return errors.Wrap(err, "failed to read TPM device class") |
| 120 | } |
| 121 | |
| 122 | if len(tpms) == 0 { |
| 123 | return ErrNotExists |
| 124 | } |
| 125 | if len(tpms) > 1 { |
| 126 | logger.Warn("Found more than one TPM, using the first one") |
| 127 | } |
| 128 | tpmName := tpms[0] |
| 129 | ueventData, err := sysfs.ReadUevents(filepath.Join("/sys/class/tpm", tpmName, "uevent")) |
| 130 | majorDev, err := strconv.Atoi(ueventData["MAJOR"]) |
| 131 | if err != nil { |
| 132 | return fmt.Errorf("failed to convert uevent: %w", err) |
| 133 | } |
| 134 | minorDev, err := strconv.Atoi(ueventData["MINOR"]) |
| 135 | if err != nil { |
| 136 | return fmt.Errorf("failed to convert uevent: %w", err) |
| 137 | } |
| 138 | if err := unix.Mknod("/dev/tpm", 0600|unix.S_IFCHR, int(unix.Mkdev(uint32(majorDev), uint32(minorDev)))); err != nil { |
| 139 | return errors.Wrap(err, "failed to create TPM device node") |
| 140 | } |
| 141 | device, err := tpm2.OpenTPM("/dev/tpm") |
| 142 | if err != nil { |
| 143 | return errors.Wrap(err, "failed to open TPM") |
| 144 | } |
| 145 | tpm = &TPM{ |
| 146 | device: device, |
| 147 | logger: logger, |
| 148 | } |
| 149 | return nil |
| 150 | } |
| 151 | |
| 152 | // GenerateSafeKey uses two sources of randomness (Kernel & TPM) to generate the key |
| 153 | func GenerateSafeKey(size uint16) ([]byte, error) { |
| 154 | lock.Lock() |
| 155 | defer lock.Unlock() |
| 156 | if tpm == nil { |
| 157 | return []byte{}, ErrNotInitialized |
| 158 | } |
| 159 | encryptionKeyHost := make([]byte, size) |
| 160 | if _, err := io.ReadFull(rand.Reader, encryptionKeyHost); err != nil { |
| 161 | return []byte{}, errors.Wrap(err, "failed to generate host portion of new key") |
| 162 | } |
| 163 | var encryptionKeyTPM []byte |
| 164 | for i := 48; i > 0; i-- { |
| 165 | tpmKeyPart, err := tpm2.GetRandom(tpm.device, size-uint16(len(encryptionKeyTPM))) |
| 166 | if err != nil { |
| 167 | return []byte{}, errors.Wrap(err, "failed to generate TPM portion of new key") |
| 168 | } |
| 169 | encryptionKeyTPM = append(encryptionKeyTPM, tpmKeyPart...) |
| 170 | if len(encryptionKeyTPM) >= int(size) { |
| 171 | break |
| 172 | } |
| 173 | } |
| 174 | |
| 175 | if len(encryptionKeyTPM) != int(size) { |
| 176 | return []byte{}, fmt.Errorf("got incorrect amount of TPM randomess: %v, requested %v", len(encryptionKeyTPM), size) |
| 177 | } |
| 178 | |
| 179 | encryptionKey := make([]byte, size) |
| 180 | for i := uint16(0); i < size; i++ { |
| 181 | encryptionKey[i] = encryptionKeyHost[i] ^ encryptionKeyTPM[i] |
| 182 | } |
| 183 | return encryptionKey, nil |
| 184 | } |
| 185 | |
| 186 | // Seal seals sensitive data and only allows access if the current platform configuration in |
| 187 | // matches the one the data was sealed on. |
| 188 | func Seal(data []byte, pcrs []int) ([]byte, error) { |
| 189 | lock.Lock() |
| 190 | defer lock.Unlock() |
| 191 | if tpm == nil { |
| 192 | return []byte{}, ErrNotInitialized |
| 193 | } |
| 194 | srk, err := tpm2tools.StorageRootKeyRSA(tpm.device) |
| 195 | if err != nil { |
| 196 | return []byte{}, errors.Wrap(err, "failed to load TPM SRK") |
| 197 | } |
| 198 | defer srk.Close() |
| 199 | sealedKey, err := srk.Seal(pcrs, data) |
| 200 | sealedKeyRaw, err := proto.Marshal(sealedKey) |
| 201 | if err != nil { |
| 202 | return []byte{}, errors.Wrapf(err, "failed to marshal sealed data") |
| 203 | } |
| 204 | return sealedKeyRaw, nil |
| 205 | } |
| 206 | |
| 207 | // Unseal unseals sensitive data if the current platform configuration allows and sealing constraints |
| 208 | // allow it. |
| 209 | func Unseal(data []byte) ([]byte, error) { |
| 210 | lock.Lock() |
| 211 | defer lock.Unlock() |
| 212 | if tpm == nil { |
| 213 | return []byte{}, ErrNotInitialized |
| 214 | } |
| 215 | srk, err := tpm2tools.StorageRootKeyRSA(tpm.device) |
| 216 | if err != nil { |
| 217 | return []byte{}, errors.Wrap(err, "failed to load TPM SRK") |
| 218 | } |
| 219 | defer srk.Close() |
| 220 | |
| 221 | var sealedKey tpmpb.SealedBytes |
| 222 | if err := proto.Unmarshal(data, &sealedKey); err != nil { |
| 223 | return []byte{}, errors.Wrap(err, "failed to decode sealed data") |
| 224 | } |
| 225 | // Logging this for auditing purposes |
| 226 | tpm.logger.Info("Attempting to unseal data protected with PCRs", zap.Int32s("pcrs", sealedKey.Pcrs)) |
| 227 | unsealedData, err := srk.Unseal(&sealedKey) |
| 228 | if err != nil { |
| 229 | return []byte{}, errors.Wrap(err, "failed to unseal data") |
| 230 | } |
| 231 | return unsealedData, nil |
| 232 | } |