github.com/stulluk/snapd@v0.0.0-20210611110309-f6d5d5bd24b0/boot/bootchain.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2020 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package boot 21 22 import ( 23 "bytes" 24 "encoding/json" 25 "fmt" 26 "os" 27 "path/filepath" 28 "sort" 29 30 "github.com/snapcore/snapd/asserts" 31 "github.com/snapcore/snapd/bootloader" 32 "github.com/snapcore/snapd/dirs" 33 "github.com/snapcore/snapd/osutil" 34 "github.com/snapcore/snapd/secboot" 35 ) 36 37 // TODO:UC20 add a doc comment when this is stabilized 38 type bootChain struct { 39 BrandID string `json:"brand-id"` 40 Model string `json:"model"` 41 Grade asserts.ModelGrade `json:"grade"` 42 ModelSignKeyID string `json:"model-sign-key-id"` 43 AssetChain []bootAsset `json:"asset-chain"` 44 Kernel string `json:"kernel"` 45 // KernelRevision is the revision of the kernel snap. It is empty if 46 // kernel is unasserted, in which case always reseal. 47 KernelRevision string `json:"kernel-revision"` 48 KernelCmdlines []string `json:"kernel-cmdlines"` 49 50 model *asserts.Model 51 kernelBootFile bootloader.BootFile 52 } 53 54 // TODO:UC20 add a doc comment when this is stabilized 55 type bootAsset struct { 56 Role bootloader.Role `json:"role"` 57 Name string `json:"name"` 58 Hashes []string `json:"hashes"` 59 } 60 61 func bootAssetLess(b, other *bootAsset) bool { 62 byRole := b.Role < other.Role 63 byName := b.Name < other.Name 64 // sort order: role -> name -> hash list (len -> lexical) 65 if b.Role != other.Role { 66 return byRole 67 } 68 if b.Name != other.Name { 69 return byName 70 } 71 return stringListsLess(b.Hashes, other.Hashes) 72 } 73 74 func stringListsEqual(sl1, sl2 []string) bool { 75 if len(sl1) != len(sl2) { 76 return false 77 } 78 for i := range sl1 { 79 if sl1[i] != sl2[i] { 80 return false 81 } 82 } 83 return true 84 } 85 86 func stringListsLess(sl1, sl2 []string) bool { 87 if len(sl1) != len(sl2) { 88 return len(sl1) < len(sl2) 89 } 90 for idx := range sl1 { 91 if sl1[idx] < sl2[idx] { 92 return true 93 } 94 } 95 return false 96 } 97 98 func toPredictableBootAsset(b *bootAsset) *bootAsset { 99 if b == nil { 100 return nil 101 } 102 newB := *b 103 if b.Hashes != nil { 104 newB.Hashes = make([]string, len(b.Hashes)) 105 copy(newB.Hashes, b.Hashes) 106 sort.Strings(newB.Hashes) 107 } 108 return &newB 109 } 110 111 func toPredictableBootChain(b *bootChain) *bootChain { 112 if b == nil { 113 return nil 114 } 115 newB := *b 116 if b.AssetChain != nil { 117 newB.AssetChain = make([]bootAsset, len(b.AssetChain)) 118 for i := range b.AssetChain { 119 newB.AssetChain[i] = *toPredictableBootAsset(&b.AssetChain[i]) 120 } 121 } 122 if b.KernelCmdlines != nil { 123 newB.KernelCmdlines = make([]string, len(b.KernelCmdlines)) 124 copy(newB.KernelCmdlines, b.KernelCmdlines) 125 sort.Strings(newB.KernelCmdlines) 126 } 127 return &newB 128 } 129 130 func predictableBootAssetsEqual(b1, b2 []bootAsset) bool { 131 b1JSON, err := json.Marshal(b1) 132 if err != nil { 133 return false 134 } 135 b2JSON, err := json.Marshal(b2) 136 if err != nil { 137 return false 138 } 139 return bytes.Equal(b1JSON, b2JSON) 140 } 141 142 func predictableBootAssetsLess(b1, b2 []bootAsset) bool { 143 if len(b1) != len(b2) { 144 return len(b1) < len(b2) 145 } 146 for i := range b1 { 147 if bootAssetLess(&b1[i], &b2[i]) { 148 return true 149 } 150 } 151 return false 152 } 153 154 type byBootChainOrder []bootChain 155 156 func (b byBootChainOrder) Len() int { return len(b) } 157 func (b byBootChainOrder) Swap(i, j int) { b[i], b[j] = b[j], b[i] } 158 func (b byBootChainOrder) Less(i, j int) bool { 159 // sort by model info 160 if b[i].BrandID != b[j].BrandID { 161 return b[i].BrandID < b[j].BrandID 162 } 163 if b[i].Model != b[j].Model { 164 return b[i].Model < b[j].Model 165 } 166 if b[i].Grade != b[j].Grade { 167 return b[i].Grade < b[j].Grade 168 } 169 if b[i].ModelSignKeyID != b[j].ModelSignKeyID { 170 return b[i].ModelSignKeyID < b[j].ModelSignKeyID 171 } 172 // then boot assets 173 if !predictableBootAssetsEqual(b[i].AssetChain, b[j].AssetChain) { 174 return predictableBootAssetsLess(b[i].AssetChain, b[j].AssetChain) 175 } 176 // then kernel 177 if b[i].Kernel != b[j].Kernel { 178 return b[i].Kernel < b[j].Kernel 179 } 180 if b[i].KernelRevision != b[j].KernelRevision { 181 return b[i].KernelRevision < b[j].KernelRevision 182 } 183 // and last kernel command lines 184 if !stringListsEqual(b[i].KernelCmdlines, b[j].KernelCmdlines) { 185 return stringListsLess(b[i].KernelCmdlines, b[j].KernelCmdlines) 186 } 187 return false 188 } 189 190 type predictableBootChains []bootChain 191 192 // hasUnrevisionedKernels returns true if any of the chains have an 193 // unrevisioned kernel. Revisions will not be set for unasserted 194 // kernels. 195 func (pbc predictableBootChains) hasUnrevisionedKernels() bool { 196 for i := range pbc { 197 if pbc[i].KernelRevision == "" { 198 return true 199 } 200 } 201 return false 202 } 203 204 func toPredictableBootChains(chains []bootChain) predictableBootChains { 205 if chains == nil { 206 return nil 207 } 208 predictableChains := make([]bootChain, len(chains)) 209 for i := range chains { 210 predictableChains[i] = *toPredictableBootChain(&chains[i]) 211 } 212 sort.Sort(byBootChainOrder(predictableChains)) 213 return predictableChains 214 } 215 216 type bootChainEquivalence int 217 218 const ( 219 bootChainEquivalent bootChainEquivalence = 0 220 bootChainDifferent bootChainEquivalence = 1 221 bootChainUnrevisioned bootChainEquivalence = -1 222 ) 223 224 // predictableBootChainsEqualForReseal returns bootChainEquivalent 225 // when boot chains are equivalent for reseal. If the boot chains 226 // are clearly different it returns bootChainDifferent. 227 // If it would return bootChainEquivalent but the chains contain 228 // unrevisioned kernels it will return bootChainUnrevisioned. 229 func predictableBootChainsEqualForReseal(pb1, pb2 predictableBootChains) bootChainEquivalence { 230 pb1JSON, err := json.Marshal(pb1) 231 if err != nil { 232 return bootChainDifferent 233 } 234 pb2JSON, err := json.Marshal(pb2) 235 if err != nil { 236 return bootChainDifferent 237 } 238 if bytes.Equal(pb1JSON, pb2JSON) { 239 if pb1.hasUnrevisionedKernels() { 240 return bootChainUnrevisioned 241 } 242 return bootChainEquivalent 243 } 244 return bootChainDifferent 245 } 246 247 // bootAssetsToLoadChains generates a list of load chains covering given boot 248 // assets sequence. At the end of each chain, adds an entry for the kernel boot 249 // file. 250 func bootAssetsToLoadChains(assets []bootAsset, kernelBootFile bootloader.BootFile, roleToBlName map[bootloader.Role]string) ([]*secboot.LoadChain, error) { 251 // kernel is added after all the assets 252 addKernelBootFile := len(assets) == 0 253 if addKernelBootFile { 254 return []*secboot.LoadChain{secboot.NewLoadChain(kernelBootFile)}, nil 255 } 256 257 thisAsset := assets[0] 258 blName := roleToBlName[thisAsset.Role] 259 if blName == "" { 260 return nil, fmt.Errorf("internal error: no bootloader name for boot asset role %q", thisAsset.Role) 261 } 262 var chains []*secboot.LoadChain 263 for _, hash := range thisAsset.Hashes { 264 var bf bootloader.BootFile 265 var next []*secboot.LoadChain 266 var err error 267 268 p := filepath.Join( 269 dirs.SnapBootAssetsDir, 270 trustedAssetCacheRelPath(blName, thisAsset.Name, hash)) 271 if !osutil.FileExists(p) { 272 return nil, fmt.Errorf("file %s not found in boot assets cache", p) 273 } 274 bf = bootloader.NewBootFile( 275 "", // asset comes from the filesystem, not a snap 276 p, 277 thisAsset.Role, 278 ) 279 next, err = bootAssetsToLoadChains(assets[1:], kernelBootFile, roleToBlName) 280 if err != nil { 281 return nil, err 282 } 283 chains = append(chains, secboot.NewLoadChain(bf, next...)) 284 } 285 return chains, nil 286 } 287 288 // predictableBootChainsWrapperForStorage wraps the boot chains so 289 // that we do not store the arrays directly as JSON and we can add 290 // other information 291 type predictableBootChainsWrapperForStorage struct { 292 ResealCount int `json:"reseal-count"` 293 BootChains predictableBootChains `json:"boot-chains"` 294 } 295 296 func readBootChains(path string) (pbc predictableBootChains, resealCount int, err error) { 297 inf, err := os.Open(path) 298 if err != nil { 299 if os.IsNotExist(err) { 300 return nil, 0, nil 301 } 302 return nil, 0, fmt.Errorf("cannot open existing boot chains data file: %v", err) 303 } 304 defer inf.Close() 305 var wrapped predictableBootChainsWrapperForStorage 306 if err := json.NewDecoder(inf).Decode(&wrapped); err != nil { 307 return nil, 0, fmt.Errorf("cannot read boot chains data: %v", err) 308 } 309 return wrapped.BootChains, wrapped.ResealCount, nil 310 } 311 312 func writeBootChains(pbc predictableBootChains, path string, resealCount int) error { 313 if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { 314 return fmt.Errorf("cannot create device fde state directory: %v", err) 315 } 316 outf, err := osutil.NewAtomicFile(path, 0600, 0, osutil.NoChown, osutil.NoChown) 317 if err != nil { 318 return fmt.Errorf("cannot create a temporary boot chains file: %v", err) 319 } 320 // becomes noop when the file is committed 321 defer outf.Cancel() 322 323 wrapped := predictableBootChainsWrapperForStorage{ 324 ResealCount: resealCount, 325 BootChains: pbc, 326 } 327 if err := json.NewEncoder(outf).Encode(wrapped); err != nil { 328 return fmt.Errorf("cannot write boot chains data: %v", err) 329 } 330 return outf.Commit() 331 }