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
This commit is contained in:
Brandon Croft 2023-08-24 08:42:54 -06:00
parent cd99fac466
commit 7acde98806
No known key found for this signature in database
GPG key ID: B01E32423322EB9D
8 changed files with 334 additions and 132 deletions

View file

@ -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)
}
}

View file

@ -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
}

View file

@ -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)
}
}

View file

@ -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

View file

@ -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,
)

View file

@ -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)
}
}

View file

@ -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
}

View file

@ -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