mirror of
https://github.com/hashicorp/terraform.git
synced 2026-02-03 20:50:59 -05:00
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:
parent
cd99fac466
commit
7acde98806
8 changed files with 334 additions and 132 deletions
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
69
internal/releaseauth/hash_test.go
Normal file
69
internal/releaseauth/hash_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue