From 7acde9880670f12c1decde6ff4b3ae046b7d63ca Mon Sep 17 00:00:00 2001 From: Brandon Croft Date: Thu, 24 Aug 2023 08:42:54 -0600 Subject: [PATCH] cloudplugin: change manifest format to match releases API Changes the release manifest format to more closely match the releases API V1 (example https://api.releases.hashicorp.com/v1/releases/terraform-cloudplugin/0.1.0-prototype) - The new format doesn't carry the SHASUM for each build, so it made the matching_sums check in releaseauth redundant. - Added tests for checksum parsing - Added ID-based selection of signature file --- internal/cloudplugin/binary.go | 44 +++---- internal/cloudplugin/client.go | 177 ++++++++++++++++++++++---- internal/cloudplugin/client_test.go | 67 +++++++++- internal/cloudplugin/testing.go | 44 +++++-- internal/releaseauth/all_test.go | 5 - internal/releaseauth/hash_test.go | 69 ++++++++++ internal/releaseauth/matching_sums.go | 55 -------- internal/releaseauth/signature.go | 5 +- 8 files changed, 334 insertions(+), 132 deletions(-) create mode 100644 internal/releaseauth/hash_test.go delete mode 100644 internal/releaseauth/matching_sums.go diff --git a/internal/cloudplugin/binary.go b/internal/cloudplugin/binary.go index 6bf5bbb458..7818d91b4b 100644 --- a/internal/cloudplugin/binary.go +++ b/internal/cloudplugin/binary.go @@ -101,10 +101,10 @@ func (v BinaryManager) Resolve() (*Binary, error) { } // Check if there's a cached binary - if cachedBinary := v.cachedVersion(manifest.ProductVersion); cachedBinary != nil { + if cachedBinary := v.cachedVersion(manifest.Version); cachedBinary != nil { return &Binary{ Path: *cachedBinary, - ProductVersion: manifest.ProductVersion, + ProductVersion: manifest.Version, ResolvedFromCache: true, }, nil } @@ -125,7 +125,7 @@ func (v BinaryManager) Resolve() (*Binary, error) { // Authenticate the archive err = v.verifyCloudPlugin(manifest, buildInfo, t.Name()) if err != nil { - return nil, fmt.Errorf("could not resolve cloudplugin version %q: %w", manifest.ProductVersion, err) + return nil, fmt.Errorf("could not resolve cloudplugin version %q: %w", manifest.Version, err) } // Unarchive @@ -141,14 +141,14 @@ func (v BinaryManager) Resolve() (*Binary, error) { return nil, fmt.Errorf("failed to decompress cloud plugin: %w", err) } - err = os.WriteFile(path.Join(targetPath, ".version"), []byte(manifest.ProductVersion), 0644) + err = os.WriteFile(path.Join(targetPath, ".version"), []byte(manifest.Version), 0644) if err != nil { log.Printf("[ERROR] failed to write .version file to %q: %s", targetPath, err) } return &Binary{ Path: path.Join(targetPath, v.binaryName), - ProductVersion: manifest.ProductVersion, + ProductVersion: manifest.Version, ResolvedFromCache: false, }, nil } @@ -165,32 +165,33 @@ func (v BinaryManager) downloadFileBuffer(pathOrURL string) ([]byte, error) { } // verifyCloudPlugin authenticates the downloaded release archive -func (v BinaryManager) verifyCloudPlugin(archiveManifest *Manifest, info *ManifestReleaseBuild, archiveLocation string) error { - signature, err := v.downloadFileBuffer(archiveManifest.SHA256SumsSignatureURL) +func (v BinaryManager) verifyCloudPlugin(archiveManifest *Release, info *BuildArtifact, archiveLocation string) error { + signature, err := v.downloadFileBuffer(archiveManifest.URLSHASumsSignatures[0]) if err != nil { return fmt.Errorf("failed to download cloudplugin SHA256SUMS signature file: %w", err) } - sums, err := v.downloadFileBuffer(archiveManifest.SHA256SumsURL) + sums, err := v.downloadFileBuffer(archiveManifest.URLSHASums) if err != nil { return fmt.Errorf("failed to download cloudplugin SHA256SUMS file: %w", err) } - reportedSHA, err := releaseauth.SHA256FromHex(info.SHA256Sum) - if err != nil { - return fmt.Errorf("the reported checksum %q is not valid: %w", info.SHA256Sum, err) - } checksums, err := releaseauth.ParseChecksums(sums) if err != nil { return fmt.Errorf("failed to parse cloudplugin SHA256SUMS file: %w", err) } + filename := path.Base(info.URL) + reportedSHA, ok := checksums[filename] + if !ok { + return fmt.Errorf("could not find checksum for file %q", filename) + } + sigAuth := releaseauth.NewSignatureAuthentication(signature, sums) if len(v.signingKey) > 0 { sigAuth.PublicKey = v.signingKey } all := releaseauth.AllAuthenticators( - releaseauth.NewMatchingChecksumsAuthentication(reportedSHA, path.Base(info.URL), checksums), releaseauth.NewChecksumAuthentication(reportedSHA, archiveLocation), sigAuth, ) @@ -198,25 +199,22 @@ func (v BinaryManager) verifyCloudPlugin(archiveManifest *Manifest, info *Manife return all.Authenticate() } -func (v BinaryManager) latestManifest(ctx context.Context) (*Manifest, error) { +func (v BinaryManager) latestManifest(ctx context.Context) (*Release, error) { manifestCacheLocation := path.Join(v.cloudPluginDataDir, v.host.String(), "manifest.json") // Find the manifest cache for the hostname. - info, err := os.Stat(manifestCacheLocation) + data, err := os.ReadFile(manifestCacheLocation) modTime := time.Time{} - var localManifest *Manifest + var localManifest *Release if err != nil { log.Printf("[TRACE] no cloudplugin manifest cache found for host %q", v.host) } else { log.Printf("[TRACE] cloudplugin manifest cache found for host %q", v.host) - modTime = info.ModTime() - data, err := os.ReadFile(manifestCacheLocation) - if err == nil { - localManifest, err = decodeManifest(bytes.NewBuffer(data)) - if err != nil { - log.Printf("[WARN] failed to decode cloudplugin manifest cache %q: %s", manifestCacheLocation, err) - } + localManifest, err = decodeManifest(bytes.NewBuffer(data)) + modTime = localManifest.TimestampUpdated + if err != nil { + log.Printf("[WARN] failed to decode cloudplugin manifest cache %q: %s", manifestCacheLocation, err) } } diff --git a/internal/cloudplugin/client.go b/internal/cloudplugin/client.go index 54f7e04809..eaab4b147b 100644 --- a/internal/cloudplugin/client.go +++ b/internal/cloudplugin/client.go @@ -18,26 +18,123 @@ import ( "github.com/hashicorp/go-retryablehttp" "github.com/hashicorp/terraform/internal/httpclient" "github.com/hashicorp/terraform/internal/logging" + "github.com/hashicorp/terraform/internal/releaseauth" ) var ( defaultRequestTimeout = 60 * time.Second ) -// ManifestReleaseBuild is the json-encoded details about a particular -// build of terraform-cloudplygin -type ManifestReleaseBuild struct { - URL string `json:"url"` - SHA256Sum string `json:"sha256sum"` +// SHASumsSignatures holds a list of URLs, each referring a detached signature of the release's build artifacts. +type SHASumsSignatures []string + +// BuildArtifact represents a single build artifact in a release response. +type BuildArtifact struct { + + // The hardware architecture of the build artifact + // Enum: [386 all amd64 amd64-lxc arm arm5 arm6 arm64 arm7 armelv5 armhfv6 i686 mips mips64 mipsle ppc64le s390x ui x86_64] + Arch string `json:"arch"` + + // The Operating System corresponding to the build artifact + // Enum: [archlinux centos darwin debian dragonfly freebsd linux netbsd openbsd plan9 python solaris terraform web windows] + Os string `json:"os"` + + // This build is unsupported and provided for convenience only. + Unsupported bool `json:"unsupported,omitempty"` + + // The URL where this build can be downloaded + URL string `json:"url"` } -// Manifest is the json-encoded manifest details sent by Terraform Cloud -type Manifest struct { - ProductVersion string `json:"plugin_version"` - Archives map[string]ManifestReleaseBuild `json:"archives"` - SHA256SumsURL string `json:"sha256sums_url"` - SHA256SumsSignatureURL string `json:"sha256sums_signature_url"` - lastModified time.Time +// ReleaseStatus Status of the product release +// Example: {"message":"This release is supported","state":"supported"} +type ReleaseStatus struct { + + // Provides information about the most recent change; must be provided when Name="withdrawn" + Message string `json:"message,omitempty"` + + // The state name of the release + // Enum: [supported unsupported withdrawn] + State string `json:"state"` + + // The timestamp for the creation of the product release status + // Example: 2009-11-10T23:00:00Z + // Format: date-time + TimestampUpdated time.Time `json:"timestamp_updated"` +} + +// Release All metadata for a single product release +type Release struct { + // builds + Builds []*BuildArtifact `json:"builds,omitempty"` + + // A docker image name and tag for this release in the format `name`:`tag` + // Example: consul:1.10.0-beta3 + DockerNameTag string `json:"docker_name_tag,omitempty"` + + // True if and only if this product release is a prerelease. + IsPrerelease bool `json:"is_prerelease"` + + // The license class indicates how this product is licensed. + // Enum: [enterprise hcp oss] + LicenseClass string `json:"license_class"` + + // The product name + // Example: consul-enterprise + // Required: true + Name string `json:"name"` + + // Status + Status ReleaseStatus `json:"status"` + + // Timestamp at which this product release was created. + // Example: 2009-11-10T23:00:00Z + // Format: date-time + TimestampCreated time.Time `json:"timestamp_created"` + + // Timestamp when this product release was most recently updated. + // Example: 2009-11-10T23:00:00Z + // Format: date-time + TimestampUpdated time.Time `json:"timestamp_updated"` + + // URL for a blogpost announcing this release + URLBlogpost string `json:"url_blogpost,omitempty"` + + // URL for the changelog covering this release + URLChangelog string `json:"url_changelog,omitempty"` + + // The project's docker repo on Amazon ECR-Public + URLDockerRegistryDockerhub string `json:"url_docker_registry_dockerhub,omitempty"` + + // The project's docker repo on DockerHub + URLDockerRegistryEcr string `json:"url_docker_registry_ecr,omitempty"` + + // URL for the software license applicable to this release + // Required: true + URLLicense string `json:"url_license,omitempty"` + + // The project's website URL + URLProjectWebsite string `json:"url_project_website,omitempty"` + + // URL for this release's change notes + URLReleaseNotes string `json:"url_release_notes,omitempty"` + + // URL for this release's file containing checksums of all the included build artifacts + URLSHASums string `json:"url_shasums"` + + // An array of URLs, each pointing to a signature file. Each signature file is a detached signature + // of the checksums file (see field `url_shasums`). Signature files may or may not embed the signing + // key ID in the filename. + URLSHASumsSignatures SHASumsSignatures `json:"url_shasums_signatures"` + + // URL for the product's source code repository. This field is empty for + // enterprise and hcp products. + URLSourceRepository string `json:"url_source_repository,omitempty"` + + // The version of this release + // Example: 1.10.0-beta3 + // Required: true + Version string `json:"version"` } // CloudPluginClient fetches and verifies release distributions of the cloudplugin @@ -54,8 +151,8 @@ func requestLogHook(logger retryablehttp.Logger, req *http.Request, i int) { } } -func decodeManifest(data io.Reader) (*Manifest, error) { - var man Manifest +func decodeManifest(data io.Reader) (*Release, error) { + var man Release dec := json.NewDecoder(data) if err := dec.Decode(&man); err != nil { return nil, ErrQueryFailed{ @@ -88,8 +185,8 @@ func NewCloudPluginClient(ctx context.Context, serviceURL *url.URL) (*CloudPlugi // FetchManifest retrieves the cloudplugin manifest from Terraform Cloud, // but returns a nil manifest if a 304 response is received, depending // on the lastModified time. -func (c CloudPluginClient) FetchManifest(lastModified time.Time) (*Manifest, error) { - req, _ := retryablehttp.NewRequestWithContext(c.ctx, "GET", c.serviceURL.JoinPath("manifest").String(), nil) +func (c CloudPluginClient) FetchManifest(lastModified time.Time) (*Release, error) { + req, _ := retryablehttp.NewRequestWithContext(c.ctx, "GET", c.serviceURL.JoinPath("manifest.json").String(), nil) req.Header.Set("If-Modified-Since", lastModified.Format(http.TimeFormat)) resp, err := c.httpClient.Do(req) @@ -105,15 +202,10 @@ func (c CloudPluginClient) FetchManifest(lastModified time.Time) (*Manifest, err switch resp.StatusCode { case http.StatusOK: - lastModifiedRaw := resp.Header.Get("Last-Modified") - if len(lastModifiedRaw) > 0 { - lastModified, _ = time.Parse(http.TimeFormat, lastModifiedRaw) - } manifest, err := decodeManifest(resp.Body) if err != nil { return nil, err } - manifest.lastModified = lastModified return manifest, nil case http.StatusNotModified: return nil, nil @@ -180,19 +272,52 @@ func (c CloudPluginClient) resolveManifestURL(pathOrURL string) (*url.URL, error } // Select gets the specific build data from the Manifest for the specified OS/Architecture -func (m Manifest) Select(goos, arch string) (*ManifestReleaseBuild, error) { +func (m Release) Select(goos, arch string) (*BuildArtifact, error) { var supported []string - for key := range m.Archives { + var found *BuildArtifact + for _, build := range m.Builds { + key := fmt.Sprintf("%s_%s", build.Os, build.Arch) supported = append(supported, key) + + if goos == build.Os && arch == build.Arch { + found = build + } } osArchKey := fmt.Sprintf("%s_%s", goos, arch) log.Printf("[TRACE] checking for cloudplugin archive for %s. Supported architectures: %v", osArchKey, supported) - archiveOSArch, ok := m.Archives[osArchKey] - if !ok { + if found == nil { return nil, ErrArchNotSupported } - return &archiveOSArch, nil + return found, nil +} + +// PrimarySHASumsSignatureURL returns the URL among the URLSHASumsSignatures that matches +// the public key known by this version of terraform. It falls back to the first URL with no +// ID in the URL. +func (m Release) PrimarySHASumsSignatureURL() (string, error) { + if len(m.URLSHASumsSignatures) == 0 { + return "", fmt.Errorf("no SHA256SUMS URLs were available") + } + + findBySuffix := func(suffix string) string { + for _, url := range m.URLSHASumsSignatures { + if len(url) > len(suffix) && strings.EqualFold(suffix, url[len(url)-len(suffix):]) { + return url + } + } + return "" + } + + withKeyID := findBySuffix(fmt.Sprintf(".%s.sig", releaseauth.HashiCorpPublicKeyID)) + if withKeyID == "" { + withNoKeyID := findBySuffix("_SHA256SUMS.sig") + if withNoKeyID == "" { + return "", fmt.Errorf("no SHA256SUMS URLs matched the known public key") + } + return withNoKeyID, nil + } + return withKeyID, nil } diff --git a/internal/cloudplugin/client_test.go b/internal/cloudplugin/client_test.go index 8e73afe622..f8bfad5b12 100644 --- a/internal/cloudplugin/client_test.go +++ b/internal/cloudplugin/client_test.go @@ -41,7 +41,7 @@ func TestCloudPluginClient_DownloadFile(t *testing.T) { t.Run("200 response", func(t *testing.T) { buffer := bytes.Buffer{} - err := client.DownloadFile("/archives/terraform-cloudplugin/terraform-cloudplugin_0.1.0_SHA256SUMS", &buffer) + err := client.DownloadFile("/archives/terraform-cloudplugin_0.1.0_SHA256SUMS", &buffer) if err != nil { t.Fatal("expected no error") } @@ -83,12 +83,8 @@ func TestCloudPluginClient_FetchManifest(t *testing.T) { t.Fatal("expected manifest") } - if manifest.lastModified != testManifestLastModified { - t.Errorf("expected lastModified %q, got %q", manifest.lastModified, testManifestLastModified) - } - - if expected := "0.1.0"; manifest.ProductVersion != expected { - t.Errorf("expected ProductVersion %q, got %q", expected, manifest.ProductVersion) + if expected := "0.1.0"; manifest.Version != expected { + t.Errorf("expected ProductVersion %q, got %q", expected, manifest.Version) } }) @@ -124,3 +120,60 @@ func TestCloudPluginClient_NotSupportedByTerraformCloud(t *testing.T) { t.Errorf("Expected ErrCloudPluginNotSupported, got %v", err) } } + +func TestRelease_PrimarySHASumsSignatureURL(t *testing.T) { + example := Release{ + URLSHASumsSignatures: []string{ + "https://releases.hashicorp.com/terraform-cloudplugin/0.1.0-prototype/terraform-cloudplugin_0.1.0-prototype_SHA256SUMS.sig", + "https://releases.hashicorp.com/terraform-cloudplugin/0.1.0-prototype/terraform-cloudplugin_0.1.0-prototype_SHA256SUMS/72D7468F.sig", // Not quite right + "https://releases.hashicorp.com/terraform-cloudplugin/0.1.0-prototype/terraform-cloudplugin_0.1.0-prototype_SHA256SUMS.72D7468F.sig", + }, + } + + url, err := example.PrimarySHASumsSignatureURL() + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + + if url != example.URLSHASumsSignatures[2] { + t.Errorf("Expected URL %q, but got %q", example.URLSHASumsSignatures[2], url) + } +} + +func TestRelease_PrimarySHASumsSignatureURL_lowercase_should_match(t *testing.T) { + example := Release{ + URLSHASumsSignatures: []string{ + "https://releases.hashicorp.com/terraform-cloudplugin/0.1.0-prototype/terraform-cloudplugin_0.1.0-prototype_SHA256SUMS.sig", + "https://releases.hashicorp.com/terraform-cloudplugin/0.1.0-prototype/terraform-cloudplugin_0.1.0-prototype_SHA256SUMS.72d7468f.sig", + }, + } + + url, err := example.PrimarySHASumsSignatureURL() + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + + // Not expected but technically fine since these are hex values + if url != example.URLSHASumsSignatures[1] { + t.Errorf("Expected URL %q, but got %q", example.URLSHASumsSignatures[1], url) + } +} + +func TestRelease_PrimarySHASumsSignatureURL_no_known_keys(t *testing.T) { + example := Release{ + URLSHASumsSignatures: []string{ + "https://releases.hashicorp.com/terraform-cloudplugin/0.1.0-prototype/terraform-cloudplugin_0.1.0-prototype_SHA256SUMS.sig", + "https://releases.hashicorp.com/terraform-cloudplugin/0.1.0-prototype/terraform-cloudplugin_0.1.0-prototype_SHA256SUMS.ABCDEF012.sig", + }, + } + + url, err := example.PrimarySHASumsSignatureURL() + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + + // Returns key with no ID + if url != example.URLSHASumsSignatures[0] { + t.Errorf("Expected URL %q, but got %q", example.URLSHASumsSignatures[0], url) + } +} diff --git a/internal/cloudplugin/testing.go b/internal/cloudplugin/testing.go index cafe688279..84007a4107 100644 --- a/internal/cloudplugin/testing.go +++ b/internal/cloudplugin/testing.go @@ -9,25 +9,42 @@ import ( "net/http" "net/http/httptest" "os" - "path" "testing" "time" ) var testManifest = `{ - "plugin_version": "0.1.0", - "archives": { - "darwin_amd64": { - "url": "/archives/terraform-cloudplugin/terraform-cloudplugin_0.1.0_darwin_amd64.zip", - "sha256sum": "22db2f0c70b50cff42afd4878fea9f6848a63f1b6532bd8b64b899f574acb35d" - } - }, - "sha256sums_url": "/archives/terraform-cloudplugin/terraform-cloudplugin_0.1.0_SHA256SUMS", - "sha256sums_signature_url": "/archives/terraform-cloudplugin/terraform-cloudplugin_0.1.0_SHA256SUMS.sig" + "builds": [ + { + "arch": "amd64", + "os": "darwin", + "url": "/archives/terraform-cloudplugin_0.1.0_darwin_amd64.zip" + } + ], + "is_prerelease": true, + "license_class": "ent", + "name": "terraform-cloudplugin", + "status": { + "state": "supported", + "timestamp_updated": "2023-07-31T15:18:20.243Z" + }, + "timestamp_created": "2023-07-31T15:18:20.243Z", + "timestamp_updated": "2023-07-31T15:18:20.243Z", + "url_changelog": "https://github.com/hashicorp/terraform-cloudplugin/blob/main/CHANGELOG.md", + "url_license": "https://github.com/hashicorp/terraform-cloudplugin/blob/main/LICENSE", + "url_project_website": "https://www.terraform.io/", + "url_shasums": "/archives/terraform-cloudplugin_0.1.0_SHA256SUMS", + "url_shasums_signatures": [ + "/archives/terraform-cloudplugin_0.1.0_SHA256SUMS.sig", + "/archives/terraform-cloudplugin_0.1.0_SHA256SUMS.72D7468F.sig" + ], + "url_source_repository": "https://github.com/hashicorp/terraform-cloudplugin", + "version": "0.1.0" }` var ( - testManifestLastModified = time.Date(2023, time.August, 1, 0, 0, 0, 0, time.UTC) + // This is the same as timestamp_updated above + testManifestLastModified, _ = time.Parse(time.RFC3339, "2023-07-31T15:18:20Z") ) type testHTTPHandler struct { @@ -40,7 +57,7 @@ func (h *testHTTPHandler) Handle(w http.ResponseWriter, r *http.Request) { } switch r.URL.Path { - case "/api/cloudplugin/v1/manifest": + case "/api/cloudplugin/v1/manifest.json": ifModifiedSince, _ := time.Parse(http.TimeFormat, r.Header.Get("If-Modified-Since")) w.Header().Set("Last-Modified", testManifestLastModified.Format(http.TimeFormat)) @@ -50,8 +67,7 @@ func (h *testHTTPHandler) Handle(w http.ResponseWriter, r *http.Request) { w.Write([]byte(testManifest)) } default: - baseName := path.Base(r.URL.Path) - fileToSend, err := os.Open(fmt.Sprintf("testdata/archives/%s", baseName)) + fileToSend, err := os.Open(fmt.Sprintf("testdata/%s", r.URL.Path)) if err == nil { io.Copy(w, fileToSend) return diff --git a/internal/releaseauth/all_test.go b/internal/releaseauth/all_test.go index 4daab7c410..59baa94c64 100644 --- a/internal/releaseauth/all_test.go +++ b/internal/releaseauth/all_test.go @@ -26,16 +26,11 @@ func TestAll(t *testing.T) { if err != nil { t.Fatal(err) } - checksums, err := ParseChecksums(sums) - if err != nil { - t.Fatal(err) - } sigAuth := NewSignatureAuthentication(signature, sums) sigAuth.PublicKey = string(publicKey) all := AllAuthenticators( - NewMatchingChecksumsAuthentication(actualChecksum, "sample_0.1.0_darwin_amd64.zip", checksums), NewChecksumAuthentication(actualChecksum, "testdata/sample_release/sample_0.1.0_darwin_amd64.zip"), sigAuth, ) diff --git a/internal/releaseauth/hash_test.go b/internal/releaseauth/hash_test.go new file mode 100644 index 0000000000..fe53baf418 --- /dev/null +++ b/internal/releaseauth/hash_test.go @@ -0,0 +1,69 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package releaseauth + +import ( + "strings" + "testing" +) + +func Test_ParseChecksums(t *testing.T) { + sample := `bb611bb4c082fec9943d3c315fcd7cacd7dabce43fbf79b8e6b451bb4e54096d terraform-cloudplugin_0.1.0-prototype_darwin_amd64.zip +295bf15c2af01d18ce7832d6d357667119e4b14eb8fd2454d506b23ed7825652 terraform-cloudplugin_0.1.0-prototype_darwin_arm64.zip +b0744b9c8c0eb7ea61824728c302d0fd4fda4bb841fb6b3e701ef9eb10adbc39 terraform-cloudplugin_0.1.0-prototype_freebsd_386.zip +8fc967d1402c5106fb0ca1b084b7edd2b11fd8d7c2225f5cd05584a56e0b2a16 terraform-cloudplugin_0.1.0-prototype_freebsd_amd64.zip +2f35b2fc748b6f279b067a4eefd65264f811a2ae86a969461851dae546aa402d terraform-cloudplugin_0.1.0-prototype_linux_386.zip +c877c8cebf76209c2c7d427d31e328212cd4716fdd8b6677939fd2a01e06a2d0 terraform-cloudplugin_0.1.0-prototype_linux_amd64.zip +97ff8fe4e2e853c9ea54605305732e5b16437045230a2df21f410e36edcfe7bd terraform-cloudplugin_0.1.0-prototype_linux_arm.zip +d415a1c39b9ec79bd00efe72d0bf14e557833b6c1ce9898f223a7dd22abd0241 terraform-cloudplugin_0.1.0-prototype_linux_arm64.zip +0f33a13eca612d1b3cda959d655a1535d69bcc1195dee37407c667c12c4900b5 terraform-cloudplugin_0.1.0-prototype_solaris_amd64.zip +a6d572e5064e1b1cf8b0b4e64bc058dc630313c95e975b44e0540f231655d31c terraform-cloudplugin_0.1.0-prototype_windows_386.zip +2aaceed12ebdf25d21f9953a09c328bd8892f5a5bd5382bd502f054478f56998 terraform-cloudplugin_0.1.0-prototype_windows_amd64.zip +` + + sums, err := ParseChecksums([]byte(sample)) + if err != nil { + t.Fatalf("Expected no error, got: %s", err) + } + + expectedSum, err := SHA256FromHex("2f35b2fc748b6f279b067a4eefd65264f811a2ae86a969461851dae546aa402d") + if err != nil { + t.Fatalf("Expected no error, got: %s", err) + } + + if found := sums["terraform-cloudplugin_0.1.0-prototype_linux_386.zip"]; found != expectedSum { + t.Errorf("Expected %q, got %q", expectedSum, found) + } +} + +func Test_ParseChecksums_Empty(t *testing.T) { + sample := ` +` + + sums, err := ParseChecksums([]byte(sample)) + if err != nil { + t.Fatalf("Expected no error, got: %s", err) + } + + expectedSum, err := SHA256FromHex("2f35b2fc748b6f279b067a4eefd65264f811a2ae86a969461851dae546aa402d") + if err != nil { + t.Fatalf("Expected no error, got: %s", err) + } + + err = sums.Validate("terraform-cloudplugin_0.1.0-prototype_linux_arm.zip", expectedSum) + if err == nil || !strings.Contains(err.Error(), "no checksum found for filename") { + t.Errorf("Expected error %q, got nil", "no checksum found for filename") + } +} + +func Test_ParseChecksums_BadFormat(t *testing.T) { + sample := `xxxxxxxxxxxxxxxxxxxxxx terraform-cloudplugin_0.1.0-prototype_darwin_amd64.zip + zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz terraform-cloudplugin_0.1.0-prototype_darwin_arm64.zip +` + + _, err := ParseChecksums([]byte(sample)) + if err == nil || !strings.Contains(err.Error(), "failed to parse checksums") { + t.Fatalf("Expected error %q, got: %s", "failed to parse checksums", err) + } +} diff --git a/internal/releaseauth/matching_sums.go b/internal/releaseauth/matching_sums.go deleted file mode 100644 index 6201269e34..0000000000 --- a/internal/releaseauth/matching_sums.go +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package releaseauth - -import "fmt" - -// ErrChecksumMismatch is the error returned when a reported checksum does not match -// what is stored in a SHA256SUMS file -type ErrChecksumMismatch struct { - Inner error -} - -func (e ErrChecksumMismatch) Error() string { - return fmt.Sprintf("failed to authenticate that release checksum matches checksum provided by the manifest: %v", e.Inner) -} - -func (e ErrChecksumMismatch) Unwrap() error { - return e.Inner -} - -// MatchingChecksumsAuthentication is an archive Authenticator that checks if a reported checksum -// matches the checksum that was stored in a SHA256SUMS file -type MatchingChecksumsAuthentication struct { - Authenticator - - expected SHA256Hash - sums SHA256Checksums - baseName string -} - -var _ Authenticator = MatchingChecksumsAuthentication{} - -// NewMatchingChecksumsAuthentication creates the Authenticator given an expected hash, -// the parsed SHA256SUMS data, and a filename. -func NewMatchingChecksumsAuthentication(expected SHA256Hash, baseName string, sums SHA256Checksums) *MatchingChecksumsAuthentication { - return &MatchingChecksumsAuthentication{ - expected: expected, - sums: sums, - baseName: baseName, - } -} - -// Authenticate ensures that the given hash matches what is found in the SHA256SUMS file -// for the corresponding filename -func (a MatchingChecksumsAuthentication) Authenticate() error { - err := a.sums.Validate(a.baseName, a.expected) - if err != nil { - return ErrChecksumMismatch{ - Inner: err, - } - } - - return nil -} diff --git a/internal/releaseauth/signature.go b/internal/releaseauth/signature.go index d70071a406..97ffc16d54 100644 --- a/internal/releaseauth/signature.go +++ b/internal/releaseauth/signature.go @@ -36,7 +36,7 @@ func NewSignatureAuthentication(signature []byte, signed []byte) *SignatureAuthe return &SignatureAuthentication{ signature: signature, signed: signed, - PublicKey: HashicorpPublicKey, + PublicKey: HashiCorpPublicKey, } } @@ -59,7 +59,8 @@ func (a SignatureAuthentication) Authenticate() error { // HashicorpPublicKey is the HashiCorp public key, also available at // https://www.hashicorp.com/security -const HashicorpPublicKey = `-----BEGIN PGP PUBLIC KEY BLOCK----- +const HashiCorpPublicKeyID = "72D7468F" +const HashiCorpPublicKey = `-----BEGIN PGP PUBLIC KEY BLOCK----- mQINBGB9+xkBEACabYZOWKmgZsHTdRDiyPJxhbuUiKX65GUWkyRMJKi/1dviVxOX PG6hBPtF48IFnVgxKpIb7G6NjBousAV+CuLlv5yqFKpOZEGC6sBV+Gx8Vu1CICpl