mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-11 22:49:54 +00:00
[v17] Sign managed updates Linux tarballs with cosign (#69536)
This commit is contained in:
@@ -367,6 +367,12 @@ ifeq ("$(GITHUB_REPOSITORY_OWNER)","gravitational")
|
||||
# This is done here to prevent any changes to the (BUI)LDFLAGS passed to the other binaries
|
||||
TELEPORT_LDFLAGS ?= -ldflags '$(GO_LDFLAGS) -X github.com/gravitational/teleport/lib/modules.teleportBuildType=community'
|
||||
TOOLS_LDFLAGS ?= -ldflags '$(GO_LDFLAGS) $(KUBECTL_SETVERSION) -X github.com/gravitational/teleport/lib/modules.teleportBuildType=community'
|
||||
TELEPORT_UPDATE_ARTIFACT_SIGNATURE_PUBLIC_KEY_B64 ?=
|
||||
TELEPORT_UPDATE_ARTIFACT_SIGNATURE_BACKUP_PUBLIC_KEY_B64 ?=
|
||||
TELEPORT_UPDATE_ARTIFACT_SIGNATURE_ADDITIONAL_PUBLIC_KEY_B64 ?=
|
||||
TELEPORT_UPDATE_ARTIFACT_SIGNATURE_ADDITIONAL_BACKUP_PUBLIC_KEY_B64 ?=
|
||||
TELEPORT_UPDATE_DEV_BUILD ?=
|
||||
TELEPORT_UPDATE_LDFLAGS ?= -ldflags '$(GO_LDFLAGS) $(KUBECTL_SETVERSION) -X github.com/gravitational/teleport/lib/modules.teleportBuildType=community -X main.artifactSignaturePublicKeyB64=$(TELEPORT_UPDATE_ARTIFACT_SIGNATURE_PUBLIC_KEY_B64) -X main.artifactSignatureBackupPublicKeyB64=$(TELEPORT_UPDATE_ARTIFACT_SIGNATURE_BACKUP_PUBLIC_KEY_B64) -X main.artifactSignatureAdditionalPublicKeyB64=$(TELEPORT_UPDATE_ARTIFACT_SIGNATURE_ADDITIONAL_PUBLIC_KEY_B64) -X main.artifactSignatureAdditionalBackupPublicKeyB64=$(TELEPORT_UPDATE_ARTIFACT_SIGNATURE_ADDITIONAL_BACKUP_PUBLIC_KEY_B64)'
|
||||
endif
|
||||
|
||||
# By making these 3 targets below (tsh, tctl and teleport) PHONY we are solving
|
||||
@@ -406,7 +412,11 @@ $(BUILDDIR)/tbot:
|
||||
|
||||
.PHONY: $(BUILDDIR)/teleport-update
|
||||
$(BUILDDIR)/teleport-update:
|
||||
GOOS=$(OS) GOARCH=$(ARCH) CGO_ENABLED=0 go build -o $(BUILDDIR)/teleport-update $(BUILDFLAGS_TELEPORT_UPDATE) ./tool/teleport-update
|
||||
@if [[ (-z "$(TELEPORT_UPDATE_ARTIFACT_SIGNATURE_PUBLIC_KEY_B64)" || -z "$(TELEPORT_UPDATE_ARTIFACT_SIGNATURE_BACKUP_PUBLIC_KEY_B64)") && "$(TELEPORT_UPDATE_DEV_BUILD)" != "1" ]]; then \
|
||||
echo "TELEPORT_UPDATE_ARTIFACT_SIGNATURE_PUBLIC_KEY_B64 and TELEPORT_UPDATE_ARTIFACT_SIGNATURE_BACKUP_PUBLIC_KEY_B64 must be set when building teleport-update (or set TELEPORT_UPDATE_DEV_BUILD=1 for unsigned dev builds)" >&2; \
|
||||
exit 1; \
|
||||
fi
|
||||
GOOS=$(OS) GOARCH=$(ARCH) CGO_ENABLED=0 go build -o $(BUILDDIR)/teleport-update $(BUILDFLAGS_TELEPORT_UPDATE) $(TELEPORT_UPDATE_LDFLAGS) ./tool/teleport-update
|
||||
|
||||
TELEPORT_ARGS ?= start
|
||||
.PHONY: teleport-hot-reload
|
||||
@@ -1446,6 +1456,7 @@ IS_PROD_SEMVER = $(if $(findstring -,$(VERSION)),$(call find-any,$(PROD_VERSIONS
|
||||
.PHONY: tag-build
|
||||
tag-build: CLOUD_ONLY = $(if $(IS_CLOUD_SEMVER),true,false)
|
||||
tag-build: ENVIRONMENT = $(if $(IS_PROD_SEMVER),prod/build,stage/build)
|
||||
tag-build: MANAGED_UPDATES_SIGNING_KEY ?= primary
|
||||
tag-build:
|
||||
@which gh >/dev/null 2>&1 || { echo 'gh command needed. https://github.com/cli/cli'; exit 1; }
|
||||
gh workflow run tag-build.yaml \
|
||||
@@ -1454,7 +1465,8 @@ tag-build:
|
||||
-f "oss-teleport-repo=$(shell gh repo view --json nameWithOwner --jq .nameWithOwner)" \
|
||||
-f "oss-teleport-ref=v$(VERSION)" \
|
||||
-f "cloud-only=$(CLOUD_ONLY)" \
|
||||
-f "environment=$(ENVIRONMENT)"
|
||||
-f "environment=$(ENVIRONMENT)" \
|
||||
-f "managed-updates-signing-key=$(MANAGED_UPDATES_SIGNING_KEY)"
|
||||
@echo See runs at: https://github.com/gravitational/teleport.e/actions/workflows/tag-build.yaml
|
||||
|
||||
# Publishes a tag build.
|
||||
|
||||
@@ -8,7 +8,7 @@ HOSTNAME=buildbox
|
||||
SRCDIR=/go/src/github.com/gravitational/teleport
|
||||
GOMODCACHE ?= /tmp/gomodcache
|
||||
# TODO(hugoShaka) remove HELM_PLUGINS with teleport13 buildbox
|
||||
DOCKERFLAGS := --rm=true -v "$$(pwd)/../":$(SRCDIR) -v /tmp:/tmp -w $(SRCDIR) -h $(HOSTNAME) -e GOMODCACHE=$(GOMODCACHE) -e HELM_PLUGINS=/home/ci/.local/share/helm/plugins-new -e GITHUB_REPOSITORY_OWNER=$(GITHUB_REPOSITORY_OWNER) -e MAKEFLAGS -e CI=true
|
||||
DOCKERFLAGS := --rm=true -v "$$(pwd)/../":$(SRCDIR) -v /tmp:/tmp -w $(SRCDIR) -h $(HOSTNAME) -e GOMODCACHE=$(GOMODCACHE) -e HELM_PLUGINS=/home/ci/.local/share/helm/plugins-new -e GITHUB_REPOSITORY_OWNER=$(GITHUB_REPOSITORY_OWNER) -e MAKEFLAGS -e CI=true -e TELEPORT_UPDATE_ARTIFACT_SIGNATURE_PUBLIC_KEY_B64 -e TELEPORT_UPDATE_ARTIFACT_SIGNATURE_BACKUP_PUBLIC_KEY_B64 -e TELEPORT_UPDATE_ARTIFACT_SIGNATURE_ADDITIONAL_PUBLIC_KEY_B64 -e TELEPORT_UPDATE_ARTIFACT_SIGNATURE_ADDITIONAL_BACKUP_PUBLIC_KEY_B64
|
||||
# Teleport version - some targets require this to be set
|
||||
VERSION ?= $(shell egrep ^VERSION ../Makefile | cut -d= -f2)
|
||||
|
||||
|
||||
@@ -79,6 +79,12 @@ type UpdateSpec struct {
|
||||
Group string `yaml:"group,omitempty"`
|
||||
// BaseURL is CDN base URL used for the Teleport tgz download URL.
|
||||
BaseURL string `yaml:"base_url,omitempty"`
|
||||
// InsecureSkipSignatureVerify disables artifact signature verification and falls back
|
||||
// to checksum-only verification. This is intended for development builds that are not signed.
|
||||
InsecureSkipSignatureVerify bool `yaml:"insecure_skip_signature_verify,omitempty"`
|
||||
// EnableStagingSignatureVerify enforces detached signature verification for staging CDN artifacts
|
||||
// that would otherwise skip signature verification.
|
||||
EnableStagingSignatureVerify bool `yaml:"enable_staging_signature_verify,omitempty"`
|
||||
// Enabled controls whether auto-updates are enabled.
|
||||
Enabled bool `yaml:"enabled"`
|
||||
// Pinned controls whether the active_version is pinned.
|
||||
@@ -233,6 +239,12 @@ func updateConfigSpec(spec *UpdateSpec, override OverrideConfig) error {
|
||||
!strings.HasPrefix(strings.ToLower(spec.BaseURL), "https://") {
|
||||
return trace.Errorf("Teleport download base URL %s must use TLS (https://)", spec.BaseURL)
|
||||
}
|
||||
if override.InsecureSkipSignatureVerifyChanged {
|
||||
spec.InsecureSkipSignatureVerify = override.InsecureSkipSignatureVerify
|
||||
}
|
||||
if override.EnableStagingSignatureVerifyChanged {
|
||||
spec.EnableStagingSignatureVerify = override.EnableStagingSignatureVerify
|
||||
}
|
||||
if override.Enabled {
|
||||
spec.Enabled = true
|
||||
}
|
||||
|
||||
@@ -130,11 +130,15 @@ func TestValidateConfigSpec(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
config UpdateSpec
|
||||
override UpdateSpec
|
||||
result UpdateSpec
|
||||
errMatch string
|
||||
name string
|
||||
config UpdateSpec
|
||||
override UpdateSpec
|
||||
insecureSkipSignatureVerify bool
|
||||
insecureChanged bool
|
||||
enableStagingSignatureVerify bool
|
||||
enableStagingChanged bool
|
||||
result UpdateSpec
|
||||
errMatch string
|
||||
}{
|
||||
{
|
||||
name: "overrides",
|
||||
@@ -221,9 +225,76 @@ func TestValidateConfigSpec(t *testing.T) {
|
||||
},
|
||||
errMatch: "must use TLS",
|
||||
},
|
||||
{
|
||||
name: "insecure checksum-only mode allowed",
|
||||
override: UpdateSpec{
|
||||
BaseURL: "https://example.com",
|
||||
},
|
||||
insecureSkipSignatureVerify: true,
|
||||
insecureChanged: true,
|
||||
result: UpdateSpec{
|
||||
BaseURL: "https://example.com",
|
||||
InsecureSkipSignatureVerify: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "insecure checksum-only mode allowed with default base URL",
|
||||
insecureSkipSignatureVerify: true,
|
||||
insecureChanged: true,
|
||||
result: UpdateSpec{
|
||||
InsecureSkipSignatureVerify: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "insecure checksum-only mode can be cleared",
|
||||
config: UpdateSpec{
|
||||
BaseURL: "https://example.com",
|
||||
InsecureSkipSignatureVerify: true,
|
||||
},
|
||||
insecureChanged: true,
|
||||
result: UpdateSpec{
|
||||
BaseURL: "https://example.com",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "enable staging signature verification can be enabled",
|
||||
override: UpdateSpec{
|
||||
BaseURL: "https://cdn.cloud.gravitational.io",
|
||||
},
|
||||
enableStagingSignatureVerify: true,
|
||||
enableStagingChanged: true,
|
||||
result: UpdateSpec{
|
||||
BaseURL: "https://cdn.cloud.gravitational.io",
|
||||
EnableStagingSignatureVerify: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "enable staging signature verification can be cleared",
|
||||
config: UpdateSpec{
|
||||
BaseURL: "https://cdn.cloud.gravitational.io",
|
||||
EnableStagingSignatureVerify: true,
|
||||
},
|
||||
enableStagingChanged: true,
|
||||
result: UpdateSpec{
|
||||
BaseURL: "https://cdn.cloud.gravitational.io",
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := updateConfigSpec(&tt.config, OverrideConfig{UpdateSpec: tt.override})
|
||||
err := updateConfigSpec(&tt.config, OverrideConfig{
|
||||
UpdateSpec: UpdateSpec{
|
||||
Proxy: tt.override.Proxy,
|
||||
Path: tt.override.Path,
|
||||
Group: tt.override.Group,
|
||||
BaseURL: tt.override.BaseURL,
|
||||
Enabled: tt.override.Enabled,
|
||||
Pinned: tt.override.Pinned,
|
||||
InsecureSkipSignatureVerify: tt.insecureSkipSignatureVerify,
|
||||
EnableStagingSignatureVerify: tt.enableStagingSignatureVerify,
|
||||
},
|
||||
InsecureSkipSignatureVerifyChanged: tt.insecureChanged,
|
||||
EnableStagingSignatureVerifyChanged: tt.enableStagingChanged,
|
||||
})
|
||||
if tt.errMatch != "" {
|
||||
require.ErrorContains(t, err, tt.errMatch)
|
||||
return
|
||||
|
||||
@@ -22,19 +22,27 @@ import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-semver/semver"
|
||||
"github.com/gravitational/trace"
|
||||
"github.com/sigstore/sigstore/pkg/cryptoutils"
|
||||
"github.com/sigstore/sigstore/pkg/signature"
|
||||
sigopts "github.com/sigstore/sigstore/pkg/signature/options"
|
||||
|
||||
"github.com/gravitational/teleport/lib/autoupdate"
|
||||
"github.com/gravitational/teleport/lib/utils"
|
||||
@@ -51,6 +59,14 @@ const (
|
||||
configFileMode = 0644
|
||||
// systemDirMode is the mode used for new directories.
|
||||
systemDirMode = 0755
|
||||
// artifactSignatureType is the detached cosign signature extension for Teleport tgzs.
|
||||
artifactSignatureType = "sig"
|
||||
// maxArtifactSignatureSize is the maximum allowed size of a detached artifact signature.
|
||||
maxArtifactSignatureSize = 4_096 // 4 KB, ECDSA P256 signatures are ~96 bytes
|
||||
// stagingCDNBaseURL is the staging CDN used for tenant dev-build workflows.
|
||||
stagingCDNBaseURL = "https://cdn.cloud.gravitational.io"
|
||||
// stagingSignatureVerificationVersion is the first major version that verifies staging CDN artifacts by default.
|
||||
stagingSignatureVerificationVersion = 19
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -58,6 +74,8 @@ const (
|
||||
serviceDir = "lib/systemd/system"
|
||||
// serviceName contains the upstream name of the Teleport SystemD service file.
|
||||
serviceName = "teleport.service"
|
||||
// artifactSignatureHash is the digest algorithm used for detached cosign signatures.
|
||||
artifactSignatureHash = crypto.SHA256
|
||||
)
|
||||
|
||||
// ServiceFile represents a systemd service file for a Teleport binary.
|
||||
@@ -110,6 +128,8 @@ type LocalInstaller struct {
|
||||
ValidateBinary func(ctx context.Context, path string) (bool, error)
|
||||
// Template is download URI Template of Teleport packages.
|
||||
Template string
|
||||
// ArtifactSignatureVerifiers verifies detached release artifact signatures.
|
||||
ArtifactSignatureVerifiers []signature.Verifier
|
||||
}
|
||||
|
||||
// Remove a Teleport version directory from InstallDir.
|
||||
@@ -140,7 +160,7 @@ func (li *LocalInstaller) Remove(ctx context.Context, rev Revision) error {
|
||||
// Install a Teleport version directory in InstallDir.
|
||||
// This function is idempotent.
|
||||
// See Installer interface for additional specs.
|
||||
func (li *LocalInstaller) Install(ctx context.Context, rev Revision, baseURL string, force bool) (err error) {
|
||||
func (li *LocalInstaller) Install(ctx context.Context, rev Revision, baseURL string, force, insecureSkipSignatureVerify, enableStagingSignatureVerify bool) (err error) {
|
||||
versionDir, err := li.revisionDir(rev)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
@@ -153,8 +173,6 @@ func (li *LocalInstaller) Install(ctx context.Context, rev Revision, baseURL str
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
// Get new and old checksums. If they match, skip download.
|
||||
// Otherwise, clear the old version directory and re-download.
|
||||
checksumURI := uri + "." + checksumType
|
||||
newSum, err := li.getChecksum(ctx, checksumURI)
|
||||
if err != nil {
|
||||
@@ -200,9 +218,8 @@ func (li *LocalInstaller) Install(ctx context.Context, rev Revision, baseURL str
|
||||
if err != nil {
|
||||
return trace.Wrap(err, "failed to download teleport")
|
||||
}
|
||||
// Seek to the start of the tgz file after writing
|
||||
if _, err := f.Seek(0, io.SeekStart); err != nil {
|
||||
return trace.Wrap(err, "failed seek to start of download")
|
||||
return trace.Wrap(err, "failed to reset artifact after download")
|
||||
}
|
||||
|
||||
// If interrupted, close the file immediately to stop extracting.
|
||||
@@ -211,10 +228,19 @@ func (li *LocalInstaller) Install(ctx context.Context, rev Revision, baseURL str
|
||||
context.AfterFunc(ctx, func() {
|
||||
_ = f.Close() // safe to close file multiple times
|
||||
})
|
||||
// Check integrity before decompression
|
||||
if !bytes.Equal(newSum, pathSum) {
|
||||
return trace.Errorf("mismatched checksum, download possibly corrupt")
|
||||
|
||||
if insecureSkipSignatureVerify {
|
||||
li.Log.WarnContext(ctx, "Artifact signature verification is disabled. Falling back to checksum-only verification.", "version", rev)
|
||||
} else if shouldSkipStagingSignatureVerification(baseURL, rev.Version, enableStagingSignatureVerify) {
|
||||
li.Log.WarnContext(ctx, "Artifact signature verification skipped: staging CDN in use.", "version", rev)
|
||||
} else if err := li.verifyArtifactSignature(ctx, uri+"."+artifactSignatureType, pathSum); err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(newSum, pathSum) {
|
||||
return trace.BadParameter("downloaded checksum does not match artifact digest")
|
||||
}
|
||||
|
||||
// Get uncompressed size of the tgz
|
||||
n, err := uncompressedSize(f)
|
||||
if err != nil {
|
||||
@@ -297,6 +323,171 @@ func (li *LocalInstaller) getChecksum(ctx context.Context, url string) ([]byte,
|
||||
return sum, nil
|
||||
}
|
||||
|
||||
func isStagingCDN(baseURL string) bool {
|
||||
ok, err := sameHostname(baseURL, stagingCDNBaseURL)
|
||||
return err == nil && ok
|
||||
}
|
||||
|
||||
func shouldSkipStagingSignatureVerification(baseURL, version string, enableStagingSignatureVerify bool) bool {
|
||||
if !isStagingCDN(baseURL) || enableStagingSignatureVerify {
|
||||
return false
|
||||
}
|
||||
semVersion, err := semver.NewVersion(version)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return semVersion.Major < stagingSignatureVerificationVersion
|
||||
}
|
||||
|
||||
func sameHostname(rawURL, wantURL string) (bool, error) {
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return false, trace.Wrap(err)
|
||||
}
|
||||
w, err := url.Parse(wantURL)
|
||||
if err != nil {
|
||||
return false, trace.Wrap(err)
|
||||
}
|
||||
|
||||
got := strings.TrimSuffix(strings.ToLower(u.Hostname()), ".")
|
||||
want := strings.TrimSuffix(strings.ToLower(w.Hostname()), ".")
|
||||
|
||||
return got == want, nil
|
||||
}
|
||||
|
||||
// NewArtifactSignatureVerifiers constructs the trusted verifier set for
|
||||
// teleport-update. The primary/backup pair is required. An additional
|
||||
// primary/backup pair may be provided to extend trust for specific build lanes,
|
||||
// such as allowing a staging-built updater to verify prod-signed artifacts.
|
||||
func NewArtifactSignatureVerifiers(primaryPublicKeyB64, backupPublicKeyB64, additionalPublicKeyB64, additionalBackupPublicKeyB64 string) ([]signature.Verifier, error) {
|
||||
if primaryPublicKeyB64 == "" {
|
||||
return nil, trace.BadParameter("teleport-update artifact signature public key is not configured")
|
||||
}
|
||||
if backupPublicKeyB64 == "" {
|
||||
return nil, trace.BadParameter("teleport-update backup artifact signature public key is not configured")
|
||||
}
|
||||
|
||||
if additionalPublicKeyB64 != "" && additionalBackupPublicKeyB64 == "" {
|
||||
return nil, trace.BadParameter("teleport-update additional backup artifact signature public key is not configured")
|
||||
}
|
||||
|
||||
if additionalPublicKeyB64 == "" && additionalBackupPublicKeyB64 != "" {
|
||||
return nil, trace.BadParameter("teleport-update additional artifact signature public key is not configured")
|
||||
}
|
||||
|
||||
primaryVerifier, err := loadArtifactSignatureVerifier(primaryPublicKeyB64, "primary")
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
backupVerifier, err := loadArtifactSignatureVerifier(backupPublicKeyB64, "backup")
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
verifiers := []signature.Verifier{primaryVerifier, backupVerifier}
|
||||
if additionalPublicKeyB64 != "" {
|
||||
additionalPrimaryVerifier, err := loadArtifactSignatureVerifier(additionalPublicKeyB64, "additional primary")
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
verifiers = append(verifiers, additionalPrimaryVerifier)
|
||||
|
||||
additionalBackupVerifier, err := loadArtifactSignatureVerifier(additionalBackupPublicKeyB64, "additional backup")
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
verifiers = append(verifiers, additionalBackupVerifier)
|
||||
}
|
||||
|
||||
return verifiers, nil
|
||||
}
|
||||
|
||||
func loadArtifactSignatureVerifier(publicKeyB64 string, label string) (signature.Verifier, error) {
|
||||
pem, err := base64.StdEncoding.DecodeString(publicKeyB64)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err, "failed to decode %s teleport-update artifact signature public key", label)
|
||||
}
|
||||
pubKey, err := cryptoutils.UnmarshalPEMToPublicKey(pem)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err, "failed to parse %s teleport-update artifact signature public key", label)
|
||||
}
|
||||
verifier, err := signature.LoadVerifier(pubKey, artifactSignatureHash)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err, "failed to initialize %s teleport-update artifact signature verifier", label)
|
||||
}
|
||||
return verifier, nil
|
||||
}
|
||||
|
||||
func (li *LocalInstaller) getSignature(ctx context.Context, url string) ([]byte, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
resp, err := li.HTTP.Do(req)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return nil, trace.Errorf("signature not found: %s", url)
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, trace.Errorf("unexpected HTTP status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
raw, err := io.ReadAll(io.LimitReader(resp.Body, maxArtifactSignatureSize+1))
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
if len(raw) > maxArtifactSignatureSize {
|
||||
return nil, trace.BadParameter("signature exceeds maximum size")
|
||||
}
|
||||
sig, err := base64.StdEncoding.DecodeString(string(bytes.TrimSpace(raw)))
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err, "failed to decode signature")
|
||||
}
|
||||
return sig, nil
|
||||
}
|
||||
|
||||
func (li *LocalInstaller) verifyArtifactSignature(ctx context.Context, url string, digest []byte) error {
|
||||
if len(li.ArtifactSignatureVerifiers) == 0 {
|
||||
return trace.BadParameter("teleport-update artifact signature verifier is not configured")
|
||||
}
|
||||
|
||||
sig, err := li.getSignature(ctx, url)
|
||||
if err != nil {
|
||||
return trace.Wrap(err, "failed to download signature from %s", url)
|
||||
}
|
||||
// WithDigest reuses the SHA-256 computed during download, so the verifier does
|
||||
// not need the artifact bytes here for the current ECDSA verifier path. Use
|
||||
// an empty reader as a safe placeholder rather than nil; upstream
|
||||
// sigstore-go uses the same digest-only pattern:
|
||||
// https://github.com/sigstore/sigstore-go/blob/v1.1.4/pkg/verify/signature.go#L398
|
||||
//
|
||||
// Note: this is not a generic sigstore pattern for all key types. In
|
||||
// particular, sigstore's Ed25519 verifier rejects WithDigest-based
|
||||
// verification, so this code would need to change if the signing algorithm
|
||||
// changes in the future.
|
||||
//
|
||||
// Sigstore documents an ECDSA malleability warning for WithDigest:
|
||||
// https://github.com/sigstore/sigstore/blob/v1.10.5/pkg/signature/options/digest.go#L29
|
||||
// Here the digest is computed locally from the downloaded artifact, not
|
||||
// supplied by an untrusted source.
|
||||
var errs []error
|
||||
for _, verifier := range li.ArtifactSignatureVerifiers {
|
||||
err := verifier.VerifySignature(bytes.NewReader(sig), bytes.NewReader(nil), sigopts.WithDigest(digest))
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
errs = append(errs, err)
|
||||
}
|
||||
return trace.Wrap(errors.Join(errs...), "artifact signature verification failed for all trusted keys")
|
||||
}
|
||||
|
||||
func (li *LocalInstaller) download(ctx context.Context, w io.Writer, max int64, url string) (sum []byte, err error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
|
||||
@@ -23,13 +23,21 @@ import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
@@ -38,6 +46,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/sigstore/sigstore/pkg/signature"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
@@ -48,15 +57,23 @@ func TestLocalInstaller_Install(t *testing.T) {
|
||||
t.Parallel()
|
||||
const version = "new-version"
|
||||
|
||||
_, testSum := testTGZ(t, version)
|
||||
tgz, testSum := testTGZ(t, version)
|
||||
verifier, sig := testArtifactSignatureVerifier(t, tgz.Bytes())
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
reservedTmp uint64
|
||||
reservedInstall uint64
|
||||
existingSum string
|
||||
flags autoupdate.InstallFlags
|
||||
force bool
|
||||
name string
|
||||
reservedTmp uint64
|
||||
reservedInstall uint64
|
||||
existingSum string
|
||||
checksumValue string
|
||||
signatureValue string
|
||||
signatureStatus int
|
||||
flags autoupdate.InstallFlags
|
||||
baseURL string
|
||||
force bool
|
||||
insecure bool
|
||||
enableStagingSignatureVerify bool
|
||||
alreadyPresent bool
|
||||
|
||||
errMatch string
|
||||
}{
|
||||
@@ -64,14 +81,52 @@ func TestLocalInstaller_Install(t *testing.T) {
|
||||
name: "not present",
|
||||
},
|
||||
{
|
||||
name: "present",
|
||||
existingSum: testSum,
|
||||
name: "not present insecure checksum only",
|
||||
insecure: true,
|
||||
checksumValue: testSum,
|
||||
},
|
||||
{
|
||||
name: "not present internal development CDN pre-v19 checksum fallback by default",
|
||||
baseURL: stagingCDNBaseURL,
|
||||
checksumValue: testSum,
|
||||
},
|
||||
{
|
||||
name: "not present internal development CDN pre-v19 signature verification explicitly enabled",
|
||||
baseURL: stagingCDNBaseURL,
|
||||
checksumValue: testSum,
|
||||
enableStagingSignatureVerify: true,
|
||||
},
|
||||
{
|
||||
name: "v19 internal development CDN verifies by default",
|
||||
baseURL: stagingCDNBaseURL,
|
||||
checksumValue: testSum,
|
||||
},
|
||||
{
|
||||
name: "present",
|
||||
existingSum: testSum,
|
||||
alreadyPresent: true,
|
||||
},
|
||||
{
|
||||
name: "mismatched checksum",
|
||||
existingSum: hex.EncodeToString(sha256.New().Sum(nil)),
|
||||
force: true,
|
||||
},
|
||||
{
|
||||
name: "insecure checksum mismatch",
|
||||
insecure: true,
|
||||
checksumValue: strings.Repeat("0", checksumHexLen),
|
||||
errMatch: "downloaded checksum does not match artifact digest",
|
||||
},
|
||||
{
|
||||
name: "invalid signature",
|
||||
signatureValue: base64.StdEncoding.EncodeToString([]byte("invalid-signature")),
|
||||
errMatch: "artifact signature verification failed",
|
||||
},
|
||||
{
|
||||
name: "missing signature",
|
||||
signatureStatus: http.StatusNotFound,
|
||||
errMatch: "signature not found",
|
||||
},
|
||||
{
|
||||
name: "unreadable checksum",
|
||||
existingSum: "bad",
|
||||
@@ -98,29 +153,51 @@ func TestLocalInstaller_Install(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
installVersion := version
|
||||
switch tt.name {
|
||||
case "not present internal development CDN pre-v19 checksum fallback by default", "not present internal development CDN pre-v19 signature verification explicitly enabled":
|
||||
installVersion = "18.10.1"
|
||||
case "v19 internal development CDN verifies by default":
|
||||
installVersion = "19.0.0-dev.test.1"
|
||||
}
|
||||
|
||||
dir := t.TempDir()
|
||||
err := os.MkdirAll(filepath.Join(dir, version), os.ModePerm)
|
||||
err := os.MkdirAll(filepath.Join(dir, installVersion), os.ModePerm)
|
||||
require.NoError(t, err)
|
||||
|
||||
if tt.existingSum != "" {
|
||||
err := os.WriteFile(filepath.Join(dir, version, checksumType), []byte(tt.existingSum), os.ModePerm)
|
||||
err := os.WriteFile(filepath.Join(dir, installVersion, checksumType), []byte(tt.existingSum), os.ModePerm)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// test parameters
|
||||
var dlPath, shaPath, shasum string
|
||||
var dlPath, shaPath, sigPath string
|
||||
|
||||
// test server
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
tgz, sum := testTGZ(t, version)
|
||||
shasum = sum
|
||||
var out *bytes.Buffer
|
||||
if strings.HasSuffix(r.URL.Path, "."+checksumType) { // checksum request
|
||||
switch {
|
||||
case strings.HasSuffix(r.URL.Path, "."+checksumType): // checksum request
|
||||
shaPath = r.URL.Path
|
||||
out = bytes.NewBufferString(sum)
|
||||
} else { // tgz request
|
||||
checksum := testSum
|
||||
if tt.checksumValue != "" {
|
||||
checksum = tt.checksumValue
|
||||
}
|
||||
out = bytes.NewBufferString(checksum)
|
||||
case strings.HasSuffix(r.URL.Path, "."+artifactSignatureType):
|
||||
sigPath = r.URL.Path
|
||||
if tt.signatureStatus != 0 {
|
||||
w.WriteHeader(tt.signatureStatus)
|
||||
return
|
||||
}
|
||||
signatureValue := sig
|
||||
if tt.signatureValue != "" {
|
||||
signatureValue = tt.signatureValue
|
||||
}
|
||||
out = bytes.NewBufferString(signatureValue)
|
||||
default: // tgz request
|
||||
dlPath = r.URL.Path
|
||||
out = tgz
|
||||
out = bytes.NewBuffer(tgz.Bytes())
|
||||
}
|
||||
w.Header().Set("Content-Length", strconv.Itoa(out.Len()))
|
||||
_, err := io.Copy(w, out)
|
||||
@@ -130,16 +207,39 @@ func TestLocalInstaller_Install(t *testing.T) {
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
httpClient := server.Client()
|
||||
baseURL := server.URL
|
||||
if tt.baseURL != "" {
|
||||
baseURL = tt.baseURL
|
||||
}
|
||||
if isStagingCDN(baseURL) {
|
||||
serverURL, err := url.Parse(server.URL)
|
||||
require.NoError(t, err)
|
||||
baseTransport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
httpClient = &http.Client{
|
||||
Transport: roundTripperFunc(func(r *http.Request) (*http.Response, error) {
|
||||
clone := r.Clone(r.Context())
|
||||
clone.URL = new(url.URL)
|
||||
*clone.URL = *r.URL
|
||||
clone.URL.Scheme = serverURL.Scheme
|
||||
clone.URL.Host = serverURL.Host
|
||||
clone.Host = serverURL.Host
|
||||
return baseTransport.RoundTrip(clone)
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
installer := &LocalInstaller{
|
||||
InstallDir: dir,
|
||||
HTTP: http.DefaultClient,
|
||||
Log: slog.Default(),
|
||||
ReservedFreeTmpDisk: tt.reservedTmp,
|
||||
ReservedFreeInstallDisk: tt.reservedInstall,
|
||||
Template: "{{.BaseURL}}/{{.Package}}-{{.OS}}/{{.Arch}}/{{.Version}}",
|
||||
InstallDir: dir,
|
||||
HTTP: httpClient,
|
||||
Log: slog.Default(),
|
||||
ReservedFreeTmpDisk: tt.reservedTmp,
|
||||
ReservedFreeInstallDisk: tt.reservedInstall,
|
||||
Template: "{{.BaseURL}}/{{.Package}}-{{.OS}}/{{.Arch}}/{{.Version}}",
|
||||
ArtifactSignatureVerifiers: []signature.Verifier{verifier},
|
||||
}
|
||||
ctx := context.Background()
|
||||
err = installer.Install(ctx, NewRevision(version, tt.flags), server.URL, tt.force)
|
||||
err = installer.Install(ctx, NewRevision(installVersion, tt.flags), baseURL, tt.force, tt.insecure, tt.enableStagingSignatureVerify)
|
||||
if tt.errMatch != "" {
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tt.errMatch)
|
||||
@@ -147,32 +247,217 @@ func TestLocalInstaller_Install(t *testing.T) {
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
const expectedPath = "/teleport-" + runtime.GOOS + "/" + runtime.GOARCH + "/" + version
|
||||
require.Equal(t, expectedPath+"."+checksumType, shaPath)
|
||||
|
||||
if tt.existingSum == testSum {
|
||||
expectedPath := "/teleport-" + runtime.GOOS + "/" + runtime.GOARCH + "/" + installVersion
|
||||
if tt.alreadyPresent {
|
||||
require.Equal(t, expectedPath+"."+checksumType, shaPath)
|
||||
require.Empty(t, dlPath)
|
||||
require.Empty(t, sigPath)
|
||||
return
|
||||
}
|
||||
|
||||
require.Equal(t, expectedPath, dlPath)
|
||||
require.Equal(t, expectedPath+"."+checksumType, shaPath)
|
||||
if tt.insecure || shouldSkipStagingSignatureVerification(baseURL, installVersion, tt.enableStagingSignatureVerify) {
|
||||
require.Empty(t, sigPath)
|
||||
} else {
|
||||
require.Equal(t, expectedPath+"."+artifactSignatureType, sigPath)
|
||||
}
|
||||
|
||||
for _, p := range []string{
|
||||
filepath.Join(dir, version, "lib", "systemd", "system", "teleport.service"),
|
||||
filepath.Join(dir, version, "bin", "teleport"),
|
||||
filepath.Join(dir, version, "bin", "tsh"),
|
||||
filepath.Join(dir, installVersion, "lib", "systemd", "system", "teleport.service"),
|
||||
filepath.Join(dir, installVersion, "bin", "teleport"),
|
||||
filepath.Join(dir, installVersion, "bin", "tsh"),
|
||||
} {
|
||||
v, err := os.ReadFile(p)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, version, string(v))
|
||||
}
|
||||
|
||||
sum, err := os.ReadFile(filepath.Join(dir, version, checksumType))
|
||||
sum, err := os.ReadFile(filepath.Join(dir, installVersion, checksumType))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, string(sum), shasum)
|
||||
require.Equal(t, testSum, string(sum))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalInstaller_Install_MismatchedChecksumMetadata(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const version = "new-version"
|
||||
tgz, _ := testTGZ(t, version)
|
||||
verifier, sig := testArtifactSignatureVerifier(t, tgz.Bytes())
|
||||
var shaPath string
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case strings.HasSuffix(r.URL.Path, "."+checksumType):
|
||||
shaPath = r.URL.Path
|
||||
_, err := fmt.Fprint(w, strings.Repeat("0", checksumHexLen))
|
||||
require.NoError(t, err)
|
||||
case strings.HasSuffix(r.URL.Path, "."+artifactSignatureType):
|
||||
_, err := fmt.Fprint(w, sig)
|
||||
require.NoError(t, err)
|
||||
default:
|
||||
w.Header().Set("Content-Length", strconv.Itoa(tgz.Len()))
|
||||
_, err := io.Copy(w, bytes.NewReader(tgz.Bytes()))
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
installDir := t.TempDir()
|
||||
installer := &LocalInstaller{
|
||||
InstallDir: installDir,
|
||||
HTTP: http.DefaultClient,
|
||||
Log: slog.Default(),
|
||||
Template: "{{.BaseURL}}/{{.Package}}-{{.OS}}/{{.Arch}}/{{.Version}}",
|
||||
ArtifactSignatureVerifiers: []signature.Verifier{verifier},
|
||||
}
|
||||
|
||||
err := installer.Install(context.Background(), NewRevision(version, 0), server.URL, false, false, false)
|
||||
require.ErrorContains(t, err, "downloaded checksum does not match artifact digest")
|
||||
require.Equal(t, "/teleport-"+runtime.GOOS+"/"+runtime.GOARCH+"/"+version+"."+checksumType, shaPath)
|
||||
|
||||
_, err = os.ReadFile(filepath.Join(installDir, version, checksumType))
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestLocalInstaller_VerifyArtifactSignature_FallsBackToBackupKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
primaryVerifier, _ := testArtifactSignatureVerifier(t, []byte("different-payload"))
|
||||
backupVerifier, backupSig := testArtifactSignatureVerifier(t, []byte("payload"))
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, err := fmt.Fprint(w, backupSig)
|
||||
require.NoError(t, err)
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
installer := &LocalInstaller{
|
||||
HTTP: http.DefaultClient,
|
||||
ArtifactSignatureVerifiers: []signature.Verifier{
|
||||
primaryVerifier,
|
||||
backupVerifier,
|
||||
},
|
||||
}
|
||||
|
||||
digest := sha256.Sum256([]byte("payload"))
|
||||
require.NoError(t, installer.verifyArtifactSignature(context.Background(), server.URL, digest[:]))
|
||||
}
|
||||
|
||||
func TestIsStagingCDN(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
baseURL string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "exact match",
|
||||
baseURL: stagingCDNBaseURL,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "trailing slash",
|
||||
baseURL: stagingCDNBaseURL + "/",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "uppercase host",
|
||||
baseURL: "https://CDN.CLOUD.GRAVITATIONAL.IO",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "hostname trailing dot",
|
||||
baseURL: "https://cdn.cloud.gravitational.io.",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "port preserved",
|
||||
baseURL: "https://cdn.cloud.gravitational.io:443",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "path ignored",
|
||||
baseURL: "https://cdn.cloud.gravitational.io/releases",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "default prod cdn",
|
||||
baseURL: autoupdate.DefaultBaseURL,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "different dev host",
|
||||
baseURL: "https://cdn.dev-blue.cloud.teleportinfra.dev",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "invalid url",
|
||||
baseURL: "://bad-url",
|
||||
want: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
require.Equal(t, tt.want, isStagingCDN(tt.baseURL))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewArtifactSignatureVerifiers(t *testing.T) {
|
||||
_, err := NewArtifactSignatureVerifiers("", "", "", "")
|
||||
require.ErrorContains(t, err, "teleport-update artifact signature public key is not configured")
|
||||
|
||||
primaryKey, primaryB64 := testArtifactSignatureKey(t)
|
||||
_, err = NewArtifactSignatureVerifiers(primaryB64, "", "", "")
|
||||
require.ErrorContains(t, err, "teleport-update backup artifact signature public key is not configured")
|
||||
|
||||
backupKey, backupB64 := testArtifactSignatureKey(t)
|
||||
|
||||
verifiers, err := NewArtifactSignatureVerifiers(primaryB64, backupB64, "", "")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, verifiers, 2)
|
||||
|
||||
digest := sha256.Sum256([]byte("payload"))
|
||||
sig, err := ecdsa.SignASN1(rand.Reader, primaryKey, digest[:])
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, verifiers[0].VerifySignature(bytes.NewReader(sig), bytes.NewReader([]byte("payload"))))
|
||||
|
||||
sig, err = ecdsa.SignASN1(rand.Reader, backupKey, digest[:])
|
||||
require.NoError(t, err)
|
||||
require.Error(t, verifiers[0].VerifySignature(bytes.NewReader(sig), bytes.NewReader([]byte("payload"))))
|
||||
require.NoError(t, verifiers[1].VerifySignature(bytes.NewReader(sig), bytes.NewReader([]byte("payload"))))
|
||||
|
||||
extraKey, extraB64 := testArtifactSignatureKey(t)
|
||||
additionalBackupKey, additionalBackupB64 := testArtifactSignatureKey(t)
|
||||
verifiers, err = NewArtifactSignatureVerifiers(primaryB64, backupB64, extraB64, additionalBackupB64)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, verifiers, 4)
|
||||
sig, err = ecdsa.SignASN1(rand.Reader, extraKey, digest[:])
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, verifiers[2].VerifySignature(bytes.NewReader(sig), bytes.NewReader([]byte("payload"))))
|
||||
sig, err = ecdsa.SignASN1(rand.Reader, additionalBackupKey, digest[:])
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, verifiers[3].VerifySignature(bytes.NewReader(sig), bytes.NewReader([]byte("payload"))))
|
||||
|
||||
_, err = NewArtifactSignatureVerifiers(primaryB64, "not-base64", "", "")
|
||||
require.ErrorContains(t, err, "failed to decode backup teleport-update artifact signature public key")
|
||||
|
||||
_, err = NewArtifactSignatureVerifiers(primaryB64, backupB64, extraB64, "")
|
||||
require.ErrorContains(t, err, "teleport-update additional backup artifact signature public key is not configured")
|
||||
|
||||
_, err = NewArtifactSignatureVerifiers(primaryB64, backupB64, "", additionalBackupB64)
|
||||
require.ErrorContains(t, err, "teleport-update additional artifact signature public key is not configured")
|
||||
|
||||
_, err = NewArtifactSignatureVerifiers(primaryB64, backupB64, "not-base64", additionalBackupB64)
|
||||
require.ErrorContains(t, err, "failed to decode additional primary teleport-update artifact signature public key")
|
||||
|
||||
_, err = NewArtifactSignatureVerifiers(primaryB64, backupB64, extraB64, "not-base64")
|
||||
require.ErrorContains(t, err, "failed to decode additional backup teleport-update artifact signature public key")
|
||||
}
|
||||
|
||||
func testTGZ(t *testing.T, version string) (tgz *bytes.Buffer, shasum string) {
|
||||
t.Helper()
|
||||
|
||||
@@ -211,6 +496,37 @@ func testTGZ(t *testing.T, version string) (tgz *bytes.Buffer, shasum string) {
|
||||
return &buf, hex.EncodeToString(sha.Sum(nil))
|
||||
}
|
||||
|
||||
func testArtifactSignatureVerifier(t *testing.T, payload []byte) (signature.Verifier, string) {
|
||||
t.Helper()
|
||||
|
||||
key, _ := testArtifactSignatureKey(t)
|
||||
verifier, err := signature.LoadVerifier(key.Public(), crypto.SHA256)
|
||||
require.NoError(t, err)
|
||||
|
||||
digest := sha256.Sum256(payload)
|
||||
sig, err := ecdsa.SignASN1(rand.Reader, key, digest[:])
|
||||
require.NoError(t, err)
|
||||
|
||||
return verifier, base64.StdEncoding.EncodeToString(sig)
|
||||
}
|
||||
|
||||
func testArtifactSignatureKey(t *testing.T) (*ecdsa.PrivateKey, string) {
|
||||
t.Helper()
|
||||
|
||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
require.NoError(t, err)
|
||||
pubDER, err := x509.MarshalPKIXPublicKey(key.Public())
|
||||
require.NoError(t, err)
|
||||
publicKeyB64 := base64.StdEncoding.EncodeToString(pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: pubDER}))
|
||||
return key, publicKeyB64
|
||||
}
|
||||
|
||||
type roundTripperFunc func(*http.Request) (*http.Response, error)
|
||||
|
||||
func (f roundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||
return f(r)
|
||||
}
|
||||
|
||||
func TestLocalInstaller_Link(t *testing.T) {
|
||||
t.Parallel()
|
||||
const version = "new-version"
|
||||
|
||||
@@ -40,6 +40,7 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/gravitational/trace"
|
||||
"github.com/sigstore/sigstore/pkg/signature"
|
||||
|
||||
"github.com/gravitational/teleport/api/client/webclient"
|
||||
"github.com/gravitational/teleport/api/constants"
|
||||
@@ -124,6 +125,10 @@ func NewLocalUpdater(cfg LocalUpdaterConfig, ns *Namespace) (*Updater, error) {
|
||||
}
|
||||
validator := Validator{Log: cfg.Log}
|
||||
debugClient := debug.NewClient(ns.dataDir)
|
||||
if len(cfg.ArtifactSignatureVerifiers) == 0 {
|
||||
return nil, trace.BadParameter("teleport-update artifact signature verifier is not configured")
|
||||
}
|
||||
|
||||
return &Updater{
|
||||
Log: cfg.Log,
|
||||
Pool: certPool,
|
||||
@@ -146,14 +151,15 @@ func NewLocalUpdater(cfg LocalUpdaterConfig, ns *Namespace) (*Updater, error) {
|
||||
ExampleFunc: ns.ReplaceTeleportService,
|
||||
},
|
||||
},
|
||||
SystemBinDir: filepath.Join(cfg.SystemDir, "bin"),
|
||||
SystemServiceDir: filepath.Join(cfg.SystemDir, serviceDir),
|
||||
HTTP: client,
|
||||
Log: cfg.Log,
|
||||
ReservedFreeTmpDisk: reservedFreeDisk,
|
||||
ReservedFreeInstallDisk: reservedFreeDisk,
|
||||
ValidateBinary: validator.IsBinary,
|
||||
Template: autoupdate.DefaultCDNURITemplate,
|
||||
SystemBinDir: filepath.Join(cfg.SystemDir, "bin"),
|
||||
SystemServiceDir: filepath.Join(cfg.SystemDir, serviceDir),
|
||||
HTTP: client,
|
||||
Log: cfg.Log,
|
||||
ReservedFreeTmpDisk: reservedFreeDisk,
|
||||
ReservedFreeInstallDisk: reservedFreeDisk,
|
||||
ValidateBinary: validator.IsBinary,
|
||||
Template: autoupdate.DefaultCDNURITemplate,
|
||||
ArtifactSignatureVerifiers: cfg.ArtifactSignatureVerifiers,
|
||||
},
|
||||
Process: &SystemdService{
|
||||
ServiceName: filepath.Base(ns.serviceFile),
|
||||
@@ -216,6 +222,8 @@ type LocalUpdaterConfig struct {
|
||||
Debug bool
|
||||
// LogFormat controls the format of logging. Can be either `json` or `text`.
|
||||
LogFormat string
|
||||
// ArtifactSignatureVerifiers contains the trusted verifiers used to validate detached artifact signatures.
|
||||
ArtifactSignatureVerifiers []signature.Verifier
|
||||
}
|
||||
|
||||
// Updater implements the agent-local logic for Teleport agent auto-updates.
|
||||
@@ -267,7 +275,7 @@ type Installer interface {
|
||||
// Install the Teleport agent at revision from the download Template.
|
||||
// If force is true, Install will remove broken revisions.
|
||||
// Install must be idempotent.
|
||||
Install(ctx context.Context, rev Revision, baseURL string, force bool) error
|
||||
Install(ctx context.Context, rev Revision, baseURL string, force bool, insecureSkipSignatureVerify bool, enableStagingSignatureVerify bool) error
|
||||
// Link the Teleport agent at the specified revision of Teleport into path.
|
||||
// The revert function must restore the previous linking, returning false on any failure.
|
||||
// If force is true, Link will overwrite non-symlinks.
|
||||
@@ -362,6 +370,12 @@ type OverrideConfig struct {
|
||||
AllowOverwrite bool
|
||||
// AllowProxyConflict when proxies in teleport.yaml and update.yaml are mismatched.
|
||||
AllowProxyConflict bool
|
||||
// InsecureSkipSignatureVerifyChanged specifies whether the user explicitly toggled
|
||||
// artifact verification fallback behavior.
|
||||
InsecureSkipSignatureVerifyChanged bool
|
||||
// EnableStagingSignatureVerifyChanged specifies whether the user explicitly toggled
|
||||
// detached signature verification for pre-v19 artifacts downloaded from the staging CDN.
|
||||
EnableStagingSignatureVerifyChanged bool
|
||||
}
|
||||
|
||||
func deref[T any](ptr *T) T {
|
||||
@@ -995,7 +1009,7 @@ func (u *Updater) update(ctx context.Context, cfg *UpdateConfig, target Revision
|
||||
if err != nil {
|
||||
return trace.Wrap(err, "failed to determine if linked")
|
||||
}
|
||||
err = u.Installer.Install(ctx, target, baseURL, !linked)
|
||||
err = u.Installer.Install(ctx, target, baseURL, !linked, cfg.Spec.InsecureSkipSignatureVerify, cfg.Spec.EnableStagingSignatureVerify)
|
||||
if err != nil {
|
||||
return trace.Wrap(err, "failed to install")
|
||||
}
|
||||
|
||||
@@ -21,7 +21,13 @@ package agent
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
@@ -35,6 +41,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sigstore/sigstore/pkg/signature"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/yaml.v3"
|
||||
@@ -49,6 +56,29 @@ func TestMain(m *testing.M) {
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func testArtifactSignatureVerifiers(t *testing.T) []signature.Verifier {
|
||||
t.Helper()
|
||||
|
||||
setEmbeddedArtifactSignatureKey := func() string {
|
||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
require.NoError(t, err)
|
||||
pubDER, err := x509.MarshalPKIXPublicKey(key.Public())
|
||||
require.NoError(t, err)
|
||||
return base64.StdEncoding.EncodeToString(
|
||||
pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: pubDER}),
|
||||
)
|
||||
}
|
||||
|
||||
verifiers, err := NewArtifactSignatureVerifiers(
|
||||
setEmbeddedArtifactSignatureKey(),
|
||||
setEmbeddedArtifactSignatureKey(),
|
||||
"",
|
||||
"",
|
||||
)
|
||||
require.NoError(t, err)
|
||||
return verifiers
|
||||
}
|
||||
|
||||
func TestWarnUmask(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -133,7 +163,8 @@ func TestUpdater_Disable(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
cfgPath := filepath.Join(ns.Dir(), updateConfigName)
|
||||
updater, err := NewLocalUpdater(LocalUpdaterConfig{
|
||||
InsecureSkipVerify: true,
|
||||
InsecureSkipVerify: true,
|
||||
ArtifactSignatureVerifiers: testArtifactSignatureVerifiers(t),
|
||||
}, ns)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -221,7 +252,8 @@ func TestUpdater_Unpin(t *testing.T) {
|
||||
cfgPath := filepath.Join(ns.Dir(), updateConfigName)
|
||||
|
||||
updater, err := NewLocalUpdater(LocalUpdaterConfig{
|
||||
InsecureSkipVerify: true,
|
||||
InsecureSkipVerify: true,
|
||||
ArtifactSignatureVerifiers: testArtifactSignatureVerifiers(t),
|
||||
}, ns)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -763,7 +795,8 @@ func TestUpdater_Update(t *testing.T) {
|
||||
cfgPath := filepath.Join(ns.Dir(), updateConfigName)
|
||||
|
||||
updater, err := NewLocalUpdater(LocalUpdaterConfig{
|
||||
InsecureSkipVerify: true,
|
||||
InsecureSkipVerify: true,
|
||||
ArtifactSignatureVerifiers: testArtifactSignatureVerifiers(t),
|
||||
}, ns)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -787,7 +820,7 @@ func TestUpdater_Update(t *testing.T) {
|
||||
reloadCalls int
|
||||
)
|
||||
updater.Installer = &testInstaller{
|
||||
FuncInstall: func(_ context.Context, rev Revision, baseURL string, force bool) error {
|
||||
FuncInstall: func(_ context.Context, rev Revision, baseURL string, force bool, insecureSkipSignatureVerify bool, enableStagingSignatureVerify bool) error {
|
||||
for _, r := range tt.linkedRevisions {
|
||||
if r == rev {
|
||||
require.False(t, force)
|
||||
@@ -1019,7 +1052,8 @@ func TestUpdater_LinkPackage(t *testing.T) {
|
||||
cfgPath := filepath.Join(ns.Dir(), updateConfigName)
|
||||
|
||||
updater, err := NewLocalUpdater(LocalUpdaterConfig{
|
||||
InsecureSkipVerify: true,
|
||||
InsecureSkipVerify: true,
|
||||
ArtifactSignatureVerifiers: testArtifactSignatureVerifiers(t),
|
||||
}, ns)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -1332,7 +1366,8 @@ func TestUpdater_Remove(t *testing.T) {
|
||||
cfgPath := filepath.Join(ns.Dir(), updateConfigName)
|
||||
|
||||
updater, err := NewLocalUpdater(LocalUpdaterConfig{
|
||||
InsecureSkipVerify: true,
|
||||
InsecureSkipVerify: true,
|
||||
ArtifactSignatureVerifiers: testArtifactSignatureVerifiers(t),
|
||||
}, ns)
|
||||
require.NoError(t, err)
|
||||
updater.TeleportServiceName = serviceName
|
||||
@@ -1705,7 +1740,8 @@ func TestUpdater_Install(t *testing.T) {
|
||||
cfgPath := filepath.Join(ns.Dir(), updateConfigName)
|
||||
|
||||
updater, err := NewLocalUpdater(LocalUpdaterConfig{
|
||||
InsecureSkipVerify: true,
|
||||
InsecureSkipVerify: true,
|
||||
ArtifactSignatureVerifiers: testArtifactSignatureVerifiers(t),
|
||||
}, ns)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -1754,7 +1790,7 @@ func TestUpdater_Install(t *testing.T) {
|
||||
revertSetupCalls int
|
||||
)
|
||||
updater.Installer = &testInstaller{
|
||||
FuncInstall: func(_ context.Context, rev Revision, baseURL string, force bool) error {
|
||||
FuncInstall: func(_ context.Context, rev Revision, baseURL string, force bool, insecureSkipSignatureVerify bool, enableStagingSignatureVerify bool) error {
|
||||
installedRevision = rev
|
||||
installedBaseURL = baseURL
|
||||
return tt.installErr
|
||||
@@ -1971,7 +2007,9 @@ func TestUpdater_Setup(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ns := &Namespace{}
|
||||
updater, err := NewLocalUpdater(LocalUpdaterConfig{}, ns)
|
||||
updater, err := NewLocalUpdater(LocalUpdaterConfig{
|
||||
ArtifactSignatureVerifiers: testArtifactSignatureVerifiers(t),
|
||||
}, ns)
|
||||
require.NoError(t, err)
|
||||
|
||||
updater.Process = &testProcess{
|
||||
@@ -2108,7 +2146,7 @@ func blankTestAddr(s []byte) []byte {
|
||||
}
|
||||
|
||||
type testInstaller struct {
|
||||
FuncInstall func(ctx context.Context, rev Revision, baseURL string, force bool) error
|
||||
FuncInstall func(ctx context.Context, rev Revision, baseURL string, force bool, insecureSkipSignatureVerify bool, enableStagingSignatureVerify bool) error
|
||||
FuncRemove func(ctx context.Context, rev Revision) error
|
||||
FuncLink func(ctx context.Context, rev Revision, path string, force bool) (revert func(context.Context) bool, err error)
|
||||
FuncLinkSystem func(ctx context.Context) (revert func(context.Context) bool, err error)
|
||||
@@ -2120,8 +2158,8 @@ type testInstaller struct {
|
||||
FuncIsLinked func(ctx context.Context, rev Revision, path string) (bool, error)
|
||||
}
|
||||
|
||||
func (ti *testInstaller) Install(ctx context.Context, rev Revision, baseURL string, force bool) error {
|
||||
return ti.FuncInstall(ctx, rev, baseURL, force)
|
||||
func (ti *testInstaller) Install(ctx context.Context, rev Revision, baseURL string, force bool, insecureSkipSignatureVerify bool, enableStagingSignatureVerify bool) error {
|
||||
return ti.FuncInstall(ctx, rev, baseURL, force, insecureSkipSignatureVerify, enableStagingSignatureVerify)
|
||||
}
|
||||
|
||||
func (ti *testInstaller) Remove(ctx context.Context, rev Revision) error {
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gravitational/trace"
|
||||
"github.com/sigstore/sigstore/pkg/signature"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/gravitational/teleport"
|
||||
@@ -65,6 +66,26 @@ const (
|
||||
|
||||
var plog = logutils.NewPackageLogger(teleport.ComponentKey, teleport.ComponentUpdater)
|
||||
|
||||
// artifactSignaturePublicKeyB64 is injected into the teleport-update
|
||||
// binary at build time via ldflags.
|
||||
var artifactSignaturePublicKeyB64 string
|
||||
|
||||
// artifactSignatureBackupPublicKeyB64 is injected into the teleport-update
|
||||
// binary at build time via ldflags.
|
||||
var artifactSignatureBackupPublicKeyB64 string
|
||||
|
||||
// artifactSignatureAdditionalPublicKeyB64 is injected into the
|
||||
// teleport-update binary at build time via ldflags. It provides an additional
|
||||
// trusted artifact signing key beyond the primary/backup pair, for example to
|
||||
// allow a staging-built updater to verify prod-signed artifacts.
|
||||
var artifactSignatureAdditionalPublicKeyB64 string
|
||||
|
||||
// artifactSignatureAdditionalBackupPublicKeyB64 is injected into the
|
||||
// teleport-update binary at build time via ldflags. It provides an additional
|
||||
// trusted backup artifact signing key beyond the primary/backup pair, for
|
||||
// example to allow a staging-built updater to verify prod-signed artifacts.
|
||||
var artifactSignatureAdditionalBackupPublicKeyB64 string
|
||||
|
||||
func main() {
|
||||
if code := Run(os.Args[1:]); code != 0 {
|
||||
os.Exit(code)
|
||||
@@ -113,6 +134,10 @@ func Run(args []string) int {
|
||||
Hidden().StringVar(&ccfg.InstallDir)
|
||||
app.Flag("insecure", "Insecure mode disables certificate verification. Do not use in production.").
|
||||
BoolVar(&ccfg.Insecure)
|
||||
app.Flag("insecure-skip-signature-verify", "Disable artifact signature verification and fall back to checksum-only verification. Do not use in production.").
|
||||
Hidden().IsSetByUser(&ccfg.InsecureSkipSignatureVerifyChanged).BoolVar(&ccfg.InsecureSkipSignatureVerify)
|
||||
app.Flag("enable-staging-signature-verify", "Enable detached signature verification for staging CDN artifacts that would otherwise skip verification").
|
||||
IsSetByUser(&ccfg.EnableStagingSignatureVerifyChanged).BoolVar(&ccfg.EnableStagingSignatureVerify)
|
||||
|
||||
app.HelpFlag.Short('h')
|
||||
|
||||
@@ -277,6 +302,15 @@ func setupLogger(debug bool, format string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func newArtifactSignatureVerifiers() ([]signature.Verifier, error) {
|
||||
return autoupdate.NewArtifactSignatureVerifiers(
|
||||
artifactSignaturePublicKeyB64,
|
||||
artifactSignatureBackupPublicKeyB64,
|
||||
artifactSignatureAdditionalPublicKeyB64,
|
||||
artifactSignatureAdditionalBackupPublicKeyB64,
|
||||
)
|
||||
}
|
||||
|
||||
func initConfig(ctx context.Context, ccfg *cliConfig) (updater *autoupdate.Updater, lockFile string, err error) {
|
||||
ns, err := autoupdate.NewNamespace(ctx, plog, ccfg.InstallSuffix, ccfg.InstallDir)
|
||||
if err != nil {
|
||||
@@ -286,12 +320,17 @@ func initConfig(ctx context.Context, ccfg *cliConfig) (updater *autoupdate.Updat
|
||||
if err != nil {
|
||||
return nil, "", trace.Wrap(err)
|
||||
}
|
||||
artifactSignatureVerifiers, err := newArtifactSignatureVerifiers()
|
||||
if err != nil {
|
||||
return nil, "", trace.Wrap(err)
|
||||
}
|
||||
updater, err = autoupdate.NewLocalUpdater(autoupdate.LocalUpdaterConfig{
|
||||
SelfSetup: ccfg.SelfSetup,
|
||||
Log: plog,
|
||||
LogFormat: ccfg.LogFormat,
|
||||
Debug: ccfg.Debug,
|
||||
InsecureSkipVerify: ccfg.Insecure,
|
||||
SelfSetup: ccfg.SelfSetup,
|
||||
Log: plog,
|
||||
LogFormat: ccfg.LogFormat,
|
||||
Debug: ccfg.Debug,
|
||||
InsecureSkipVerify: ccfg.Insecure,
|
||||
ArtifactSignatureVerifiers: artifactSignatureVerifiers,
|
||||
}, ns)
|
||||
return updater, lockFile, trace.Wrap(err)
|
||||
}
|
||||
@@ -301,12 +340,17 @@ func statusConfig(ctx context.Context, ccfg *cliConfig) (*autoupdate.Updater, er
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
artifactSignatureVerifiers, err := newArtifactSignatureVerifiers()
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
updater, err := autoupdate.NewLocalUpdater(autoupdate.LocalUpdaterConfig{
|
||||
SelfSetup: ccfg.SelfSetup,
|
||||
Log: plog,
|
||||
LogFormat: ccfg.LogFormat,
|
||||
Debug: ccfg.Debug,
|
||||
InsecureSkipVerify: ccfg.Insecure,
|
||||
SelfSetup: ccfg.SelfSetup,
|
||||
Log: plog,
|
||||
LogFormat: ccfg.LogFormat,
|
||||
Debug: ccfg.Debug,
|
||||
InsecureSkipVerify: ccfg.Insecure,
|
||||
ArtifactSignatureVerifiers: artifactSignatureVerifiers,
|
||||
}, ns)
|
||||
return updater, trace.Wrap(err)
|
||||
}
|
||||
@@ -466,12 +510,17 @@ func cmdSetup(ctx context.Context, ccfg *cliConfig) error {
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
artifactSignatureVerifiers, err := newArtifactSignatureVerifiers()
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
updater, err := autoupdate.NewLocalUpdater(autoupdate.LocalUpdaterConfig{
|
||||
SelfSetup: ccfg.SelfSetup,
|
||||
Log: plog,
|
||||
LogFormat: ccfg.LogFormat,
|
||||
Debug: ccfg.Debug,
|
||||
InsecureSkipVerify: ccfg.Insecure,
|
||||
SelfSetup: ccfg.SelfSetup,
|
||||
Log: plog,
|
||||
LogFormat: ccfg.LogFormat,
|
||||
Debug: ccfg.Debug,
|
||||
InsecureSkipVerify: ccfg.Insecure,
|
||||
ArtifactSignatureVerifiers: artifactSignatureVerifiers,
|
||||
}, ns)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
|
||||
Reference in New Issue
Block a user