blob: 7893f727a99fc38f072ec4ae33713e713717ad37 [file] [log] [blame]
// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package tlslib
import (
"context"
"encoding/json"
"fmt"
"log"
"net/url"
"path"
"regexp"
"strconv"
"strings"
"golang.org/x/crypto/ssh"
"go.chromium.org/chromiumos/config/go/api/test/tls"
)
const (
imageloaderLacrosRootComponentPath = "/var/lib/imageloader/lacros"
pageSize = 4096
)
var versionRegex = regexp.MustCompile(`^(\d+\.)(\d+\.)(\d+\.)(\d+)$`)
type lacrosMetadata struct {
Content struct {
Version string `json:"version"`
} `json:"content"`
}
// lacrosSourceType describes the source types of Lacros for provisioning (either GS or device file).
type lacrosSourceType int
const (
GsPath lacrosSourceType = iota
DeviceFilePath
)
type provisionLacrosState struct {
s *Server
c *ssh.Client
dutName string
sourceURL string
sourceType lacrosSourceType
metadata lacrosMetadata
lacrosComponentRootPath string // component path, eg. "/home/chronos/cros-components/lacros-dogfood-dev/"
lacrosComponentPath string // versioned path, eg. "/home/chronos/cros-components/lacros-dogfood-dev/9999.0.0.0/"
lacrosImagePath string
lacrosTablePath string
overrideVersion string
}
func newProvisionLacrosState(s *Server, req *tls.ProvisionLacrosRequest) (*provisionLacrosState, error) {
p := &provisionLacrosState{
s: s,
dutName: req.Name,
}
// Verify the incoming request path oneof is valid.
switch t := req.GetImage().GetPathOneof().(type) {
case *tls.ProvisionLacrosRequest_LacrosImage_GsPathPrefix:
p.sourceURL = req.GetImage().GetGsPathPrefix()
p.sourceType = GsPath
case *tls.ProvisionLacrosRequest_LacrosImage_DeviceFilePrefix:
p.sourceURL = req.GetImage().GetDeviceFilePrefix()
p.sourceType = DeviceFilePath
default:
return nil, fmt.Errorf("newProvisionStateLacros: unsupported ImagePathOneof in ProvisionLacrosRequest, %T", t)
}
// Verify the override version is valid if |OverrideVersion| is specified.
if req.OverrideVersion != "" {
if !versionRegex.MatchString(req.OverrideVersion) {
return nil, fmt.Errorf("newProvisionLacrosState: failed to parse version: %v", req.OverrideVersion)
}
p.overrideVersion = req.OverrideVersion
}
// Override the component root path if |OverrideInstallPath| is specified.
if req.OverrideInstallPath != "" {
p.lacrosComponentRootPath = req.OverrideInstallPath
} else {
// Use the ImageLoader component path by default.
p.lacrosComponentRootPath = imageloaderLacrosRootComponentPath
}
return p, nil
}
func (p *provisionLacrosState) connect(ctx context.Context, addr string) (func(), error) {
c, err := p.s.clientPool.GetContext(ctx, addr)
if err != nil {
return nil, fmt.Errorf("connect: DUT unreachable, %s", err)
}
p.c = c
disconnect := func() {
p.s.clientPool.Put(addr, p.c)
}
return disconnect, nil
}
func (p *provisionLacrosState) provisionLacros(ctx context.Context) error {
if err := p.extractLacrosMetadata(ctx); err != nil {
return fmt.Errorf("provisionLacros: failed to extract Lacros metadata.json, %s", err)
}
if err := p.installLacrosAsComponent(ctx); err != nil {
return fmt.Errorf("provisionLacros: failed to install Lacros as component, %s", err)
}
// Get the Lacros image hash. (Must be after installing Lacros image and running verity).
lacrosImageHash, err := getSHA256Sum(ctx, p.c, p.lacrosImagePath)
if err != nil {
return fmt.Errorf("provisionLacros: failed to get Lacros image hash, %s", err)
}
// Get the Lacros table hash.
lacrosTableHash, err := getSHA256Sum(ctx, p.c, p.lacrosTablePath)
if err != nil {
return fmt.Errorf("provisionLacros: failed to get Lacros table hash, %s", err)
}
// Create the Lacros manifest file.
if err := p.writeManifest(ctx, lacrosImageHash, lacrosTableHash); err != nil {
return fmt.Errorf("provisionLacros: failed to write Lacros manifest, %s", err)
}
// Create the component updater Lacros manifest file.
if err := p.writeComponentManifest(ctx); err != nil {
return fmt.Errorf("provisionLacros: failed to write component Lacros manifest, %s", err)
}
// Write the Lacros version to the latest-version file.
lacrosLastestVersionPath := path.Join(p.lacrosComponentRootPath, "latest-version")
if err := runCmd(p.c, fmt.Sprintf("echo -n %s > %s", p.metadata.Content.Version, lacrosLastestVersionPath)); err != nil {
return fmt.Errorf("provisionLacros: failed to write Lacros version to latest-version file, %s", err)
}
// Change file mode and owner of provisioned files if the path is prefixed with the CrOS component path.
const crosComponentRootPath = "/home/chronos/cros-components"
if strings.HasPrefix(p.lacrosComponentRootPath, crosComponentRootPath) {
if err := runCmd(p.c, fmt.Sprintf("chown -R chronos:chronos %s && chmod -R 0755 %s", crosComponentRootPath, crosComponentRootPath)); err != nil {
return fmt.Errorf("provisionLacros: failed to chown/chmod provisioned files under %s, %s", crosComponentRootPath, err)
}
}
return nil
}
// extractLacrosMetadata will unmarshal the metadata.json in the GS path into the state.
func (p *provisionLacrosState) extractLacrosMetadata(ctx context.Context) error {
metadataURL, err := p.getSourceFullURL(ctx, "metadata.json")
if err != nil {
return fmt.Errorf("extractMetadata: failed to get a metadata URL, %s", err)
}
metadataJSONStr, err := runCmdOutput(p.c, fmt.Sprintf("curl --keepalive-time 20 -S -s -v -# -C - --retry 3 --retry-delay 60 %s", metadataURL))
if err != nil {
return fmt.Errorf("extractMetadata: failed to read Lacros metadata.json from %v, %s", metadataURL, err)
}
metadataJSON := lacrosMetadata{}
if err := json.Unmarshal([]byte(metadataJSONStr), &metadataJSON); err != nil {
return fmt.Errorf("extractMetadata: failed to unmarshal Lacros metadata.json, %s", err)
}
p.metadata = metadataJSON
if p.overrideVersion != "" {
log.Printf("extractLacrosMetadata: override version %v to %v\n", p.metadata.Content.Version, p.overrideVersion)
p.metadata.Content.Version = p.overrideVersion
}
return nil
}
// installLacrosAsComponent will download and install the Lacros image into the imageloader
// component directory with the version from the metadata.json file.
// The Lacros image will also be modified with the correct verity output.
func (p *provisionLacrosState) installLacrosAsComponent(ctx context.Context) error {
var imageFileName string
switch p.sourceType {
case GsPath:
imageFileName = "lacros_compressed.squash"
case DeviceFilePath:
imageFileName = "lacros.squash"
default:
return fmt.Errorf("installLacrosAsComponent: unknown source type: %v", p.sourceType)
}
imageURL, err := p.getSourceFullURL(ctx, imageFileName)
if err != nil {
return fmt.Errorf("installLacrosAsComponent: failed to get a image URL, %s", err)
}
p.lacrosComponentPath = path.Join(p.lacrosComponentRootPath, p.metadata.Content.Version)
p.lacrosImagePath = path.Join(p.lacrosComponentPath, "image.squash")
if err := runCmdRetry(ctx, p.c, 5, fmt.Sprintf("mkdir -p %s && curl --keepalive-time 20 -S -s -v -# -C - --retry 3 --retry-delay 60 %s --output %s", p.lacrosComponentPath, imageURL, p.lacrosImagePath)); err != nil {
return fmt.Errorf("installLacrosAsComponent: failed to install Lacros image from %s, %s", imageURL, err)
}
lacrosBlocks, err := alignImageToPage(ctx, p.c, p.lacrosImagePath)
if err != nil {
return fmt.Errorf("installLacrosAsComponent: failed to align Lacros image, %s", err)
}
// Generate the verity (hashtree and table) from Lacros image.
lacrosHashtreePath := path.Join(p.lacrosComponentPath, "hashtree")
p.lacrosTablePath = path.Join(p.lacrosComponentPath, "table")
if err := runCmd(p.c,
fmt.Sprintf("verity mode=create alg=sha256 payload=%s payload_blocks=%d hashtree=%s salt=random > %s",
p.lacrosImagePath, lacrosBlocks, lacrosHashtreePath, p.lacrosTablePath)); err != nil {
return fmt.Errorf("installLacrosAsComponent: failed to generate verity for Lacros image, %s", err)
}
// Append the hashtree (merkle tree) onto the end of the Lacros image.
if err := runCmd(p.c, fmt.Sprintf("cat %s >> %s", lacrosHashtreePath, p.lacrosImagePath)); err != nil {
return fmt.Errorf("installLacrosAsComponent: failed to append hashtree to Lacros image, %s", err)
}
return nil
}
// getSHA256Sum will get the SHA256 sum of a file on the device.
func getSHA256Sum(ctx context.Context, c *ssh.Client, path string) (string, error) {
hash, err := runCmdOutput(c, fmt.Sprintf("sha256sum %s | cut -d' ' -f1", path))
if err != nil {
return "", fmt.Errorf("getSHA256Sum: failed to get hash of %s, %s", path, err)
}
return strings.TrimSpace(hash), nil
}
// alignImageToPage will align the file to 4KB page alignment and return the number of page blocks.
func alignImageToPage(ctx context.Context, c *ssh.Client, path string) (int, error) {
sizeStr, err := runCmdOutput(c, "stat -c%s "+path)
if err != nil {
return 0, fmt.Errorf("alignImageToPage: failed to get image size, %s", err)
}
sizeStr = strings.TrimSpace(sizeStr)
size, err := strconv.Atoi(sizeStr)
if err != nil {
return 0, fmt.Errorf("alignImageToPage: failed to get image size as an integer, %s", err)
}
// Round up to the nearest 4KB block size.
blocks := (size + pageSize - 1) / pageSize
// Check if the Lacros image is 4KB aligned, if not extend it to 4KB alignment.
if size != blocks*pageSize {
log.Printf("alignImageToPage: image %s isn't 4KB aligned, so extending it", path)
if err := runCmd(c, fmt.Sprintf("dd if=/dev/zero bs=1 count=%d seek=%d of=%s",
blocks*pageSize-size, size, path)); err != nil {
return 0, fmt.Errorf("alignImageToPage: failed to align image to 4KB, %s", err)
}
}
return blocks, nil
}
// writeManifest will create and write the Lacros component manifest out.
func (p *provisionLacrosState) writeManifest(ctx context.Context, imageHash, tableHash string) error {
lacrosManifestJSON, err := json.MarshalIndent(struct {
ManifestVersion int `json:"manifest-version"`
FsType string `json:"fs-type"`
Version string `json:"version"`
ImageSha256Hash string `json:"image-sha256-hash"`
TableSha256Hash string `json:"table-sha256-hash"`
}{
ManifestVersion: 1,
FsType: "squashfs",
Version: p.metadata.Content.Version,
ImageSha256Hash: imageHash,
TableSha256Hash: tableHash,
}, "", " ")
if err != nil {
return fmt.Errorf("writeManifest: failed to Marshal Lacros manifest json, %s", err)
}
lacrosManifestPath := path.Join(p.lacrosComponentPath, "imageloader.json")
if err := runCmd(p.c, fmt.Sprintf("echo '%s' > %s", lacrosManifestJSON, lacrosManifestPath)); err != nil {
return fmt.Errorf("writeManifest: failed to write Lacros manifest json to DUT, %s", err)
}
return nil
}
// writeComponentManifest will create and write the Lacros component manifest out usable by component updater.
func (p *provisionLacrosState) writeComponentManifest(ctx context.Context) error {
lacrosComponentManifestJSON, err := json.MarshalIndent(struct {
ManifestVersion int `json:"manifest-version"`
Name string `json:"name"`
Version string `json:"version"`
ImageName string `json:"imageName"`
Squash bool `json:"squash"`
FsType string `json:"fsType"`
IsRemovable bool `json:"isRemovable"`
}{
ManifestVersion: 2,
Name: "lacros",
Version: p.metadata.Content.Version,
ImageName: "image.squash",
Squash: true,
FsType: "squashfs",
IsRemovable: false,
}, "", " ")
if err != nil {
return fmt.Errorf("writeComponentManifest: failed to Marshal Lacros manifest json, %s", err)
}
lacrosComponentManifestPath := path.Join(p.lacrosComponentPath, "manifest.json")
if err := runCmd(p.c, fmt.Sprintf("echo '%s' > %s", lacrosComponentManifestJSON, lacrosComponentManifestPath)); err != nil {
return fmt.Errorf("writeComponentManifest: failed to write Lacros manifest json to DUT, %s", err)
}
return nil
}
// getSourceFullURL returns a full URL of the Lacros binary files in a given base URL and path.
func (p *provisionLacrosState) getSourceFullURL(ctx context.Context, part string) (string, error) {
url, err := func(base string, part string) (*url.URL, error) {
parsed, err := url.Parse(base)
if err != nil {
return nil, err
}
parsed.Path = path.Join(parsed.Path, part)
return parsed, nil
}(p.sourceURL, part)
if err != nil {
return "", fmt.Errorf("getSourceFullURL: failed to join a URL, %s", err)
}
fullURL := ""
switch p.sourceType {
case GsPath:
// TODO(kimjae): Use CacheForDrone TLS API once implemented and then download
// from TLS side instead of from the DUT.
fullURL, err = p.s.cacheForDut(ctx, url.String(), p.dutName)
if err != nil {
return "", fmt.Errorf("getSourceFullURL: failed to CacheForDut, %s", err)
}
case DeviceFilePath:
if url.Scheme != "" && url.Scheme != "file" {
return "", fmt.Errorf("getSourceFullURL: wrong URL scheme. expected: file, actual: %s, url:%s", url.Scheme, url.String())
}
url.Scheme = "file"
fullURL = url.String()
}
return fullURL, nil
}