From 71bf68f77795f65dd1e92c63e02107a22a9afea4 Mon Sep 17 00:00:00 2001 From: Jan Dubois Date: Sat, 14 Dec 2024 12:44:54 -0800 Subject: [PATCH] Add optional digest suffix to template locator The suffix is "@digest" which may include an optional algorithm (defaults to "sha256"). The encoded digest must be at least 7 characters long. Examples: - template://my@sha256:60a87371451eabcd211c929759db61746a7c6a1c068f59d868db6aa8dca637bd - template://my@sha256:60a87371451 - template://my@60a8737 Signed-off-by: Jan Dubois --- pkg/limatmpl/locator.go | 74 ++++++++++++++++++++++++++++++++--------- 1 file changed, 59 insertions(+), 15 deletions(-) diff --git a/pkg/limatmpl/locator.go b/pkg/limatmpl/locator.go index ddb9b61bab9..cf3bff71e3b 100644 --- a/pkg/limatmpl/locator.go +++ b/pkg/limatmpl/locator.go @@ -9,11 +9,13 @@ import ( "os" "path" "path/filepath" + "regexp" "strings" "github.com/containerd/containerd/identifiers" "github.com/lima-vm/lima/pkg/ioutilx" "github.com/lima-vm/lima/pkg/templatestore" + "github.com/opencontainers/go-digest" "github.com/sirupsen/logrus" ) @@ -21,10 +23,39 @@ type Template struct { Name string Locator string Bytes []byte + + algorithm digest.Algorithm + digest string } const yBytesLimit = 4 * 1024 * 1024 // 4MiB +// Only sha256, sha384, and sha512 are actually available but we reserve all lowercase and digit strings. +// Note that only lowercase hex digits are accepted. +var digestSuffixRegex = regexp.MustCompile(`^(.+)@(?:([a-z0-9]+):)?([a-f0-9]+)$`) + +// splitOffDigest splits off an optional @algorithm:digest suffix from the locator. +func (tmpl *Template) splitOffDigest() error { + matches := digestSuffixRegex.FindStringSubmatch(tmpl.Locator) + if matches != nil { + tmpl.algorithm = digest.Algorithm(matches[2]) + if tmpl.algorithm == "" { + tmpl.algorithm = digest.SHA256 + } + if !tmpl.algorithm.Available() { + return fmt.Errorf("locator %q uses unavailable digest algorithm", tmpl.Locator) + } + tmpl.digest = matches[3] + if len(tmpl.digest) < 7 { + return fmt.Errorf("locator %q digest has fewer than 7 hex digits", tmpl.Locator) + } + tmpl.Locator = matches[1] + } + return nil +} + +// Read fetches the content pointed at by a template locator. If the locator has an optional +// digest suffix, then the digest must match, or Read will return an error. func Read(ctx context.Context, name, locator string) (*Template, error) { var err error @@ -32,13 +63,16 @@ func Read(ctx context.Context, name, locator string) (*Template, error) { Name: name, Locator: locator, } + if err = tmpl.splitOffDigest(); err != nil { + return nil, err + } - isTemplateURL, templateURL := SeemsTemplateURL(locator) + isTemplateURL, templateURL := SeemsTemplateURL(tmpl.Locator) switch { case isTemplateURL: // No need to use SecureJoin here. https://github.com/lima-vm/lima/pull/805#discussion_r853411702 templateName := filepath.Join(templateURL.Host, templateURL.Path) - logrus.Debugf("interpreting argument %q as a template name %q", locator, templateName) + logrus.Debugf("interpreting argument %q as a template name %q", tmpl.Locator, templateName) if tmpl.Name == "" { // e.g., templateName = "deprecated/centos-7" , tmpl.Name = "centos-7" tmpl.Name = filepath.Base(templateName) @@ -47,15 +81,15 @@ func Read(ctx context.Context, name, locator string) (*Template, error) { if err != nil { return nil, err } - case SeemsHTTPURL(locator): + case SeemsHTTPURL(tmpl.Locator): if tmpl.Name == "" { - tmpl.Name, err = InstNameFromURL(locator) + tmpl.Name, err = InstNameFromURL(tmpl.Locator) if err != nil { return nil, err } } - logrus.Debugf("interpreting argument %q as a http url for instance %q", locator, tmpl.Name) - req, err := http.NewRequestWithContext(ctx, http.MethodGet, locator, http.NoBody) + logrus.Debugf("interpreting argument %q as a http url for instance %q", tmpl.Locator, tmpl.Name) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, tmpl.Locator, http.NoBody) if err != nil { return nil, err } @@ -68,15 +102,15 @@ func Read(ctx context.Context, name, locator string) (*Template, error) { if err != nil { return nil, err } - case SeemsFileURL(locator): + case SeemsFileURL(tmpl.Locator): if tmpl.Name == "" { - tmpl.Name, err = InstNameFromURL(locator) + tmpl.Name, err = InstNameFromURL(tmpl.Locator) if err != nil { return nil, err } } - logrus.Debugf("interpreting argument %q as a file url for instance %q", locator, tmpl.Name) - r, err := os.Open(strings.TrimPrefix(locator, "file://")) + logrus.Debugf("interpreting argument %q as a file url for instance %q", tmpl.Locator, tmpl.Name) + r, err := os.Open(strings.TrimPrefix(tmpl.Locator, "file://")) if err != nil { return nil, err } @@ -85,15 +119,15 @@ func Read(ctx context.Context, name, locator string) (*Template, error) { if err != nil { return nil, err } - case SeemsYAMLPath(locator): + case SeemsYAMLPath(tmpl.Locator): if tmpl.Name == "" { - tmpl.Name, err = InstNameFromYAMLPath(locator) + tmpl.Name, err = InstNameFromYAMLPath(tmpl.Locator) if err != nil { return nil, err } } - logrus.Debugf("interpreting argument %q as a file path for instance %q", locator, tmpl.Name) - r, err := os.Open(locator) + logrus.Debugf("interpreting argument %q as a file path for instance %q", tmpl.Locator, tmpl.Name) + r, err := os.Open(tmpl.Locator) if err != nil { return nil, err } @@ -102,12 +136,22 @@ func Read(ctx context.Context, name, locator string) (*Template, error) { if err != nil { return nil, err } - case locator == "-": + case tmpl.Locator == "-": tmpl.Bytes, err = io.ReadAll(os.Stdin) if err != nil { return nil, fmt.Errorf("unexpected error reading stdin: %w", err) } } + + if tmpl.digest != "" { + actualDigest := digest.Algorithm(tmpl.algorithm).FromBytes(tmpl.Bytes).Encoded() + if len(tmpl.digest) < len(actualDigest) { + actualDigest = actualDigest[:len(tmpl.digest)] + } + if actualDigest != tmpl.digest { + return nil, fmt.Errorf("locator %q digest doesn't match content digest %q", locator, actualDigest) + } + } return tmpl, nil }