Files
Packer-Cn/command/verify_attestation_test.go
fb8fa2b0fe [COMPLIANCE] Add/Update Copyright Headers (#13699)
* [COMPLIANCE] Add/Update Copyright Headers

* make generate update

* make generate

---------

Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
Co-authored-by: sanya <[email protected]>
Co-authored-by: Sanya <[email protected]>
2026-09-15 17:10:54 +05:30

245 lines
7.6 KiB
Go

// Copyright IBM Corp. 2024, 2026
// SPDX-License-Identifier: BUSL-1.1
package command
import (
"bytes"
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/json"
"encoding/pem"
"os"
"path/filepath"
"strings"
"testing"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
internalattestation "github.com/hashicorp/packer/internal/attestation"
internalprovenance "github.com/hashicorp/packer/internal/provenance"
)
func TestVerifyAttestationCommandRun(t *testing.T) {
artifactPath := filepath.Join(t.TempDir(), "artifact.txt")
if err := os.WriteFile(artifactPath, []byte("hello verify"), 0600); err != nil {
t.Fatalf("write artifact: %v", err)
}
privateKeyPath, publicKeyPath := writeVerifierKeypair(t)
attestationPath := writeSignedAttestation(t, privateKeyPath, artifactPath, "https://example.com/builder", "git+https://github.com/hashicorp/packer@refs/heads/main")
meta := testMeta(t)
command := &VerifyAttestationCommand{Meta: meta}
ret := command.Run([]string{
"-verifier=" + publicKeyPath,
"-predicate-type=" + internalprovenance.SLSAProvenanceV1PredicateType,
"-builder-id=https://example.com/builder",
"-source-uri=git+https://github.com/hashicorp/packer@refs/heads/main",
"-artifact=" + artifactPath,
attestationPath,
})
if ret != 0 {
fatalCommand(t, meta)
}
ui := meta.Ui.(*packersdk.BasicUi)
if got := ui.Writer.(*bytes.Buffer).String(); !strings.Contains(got, "Attestation verified.") {
t.Fatalf("expected success output, got %q", got)
}
}
func TestVerifyAttestationCommandRejectsMismatchedBuilderID(t *testing.T) {
artifactPath := filepath.Join(t.TempDir(), "artifact.txt")
if err := os.WriteFile(artifactPath, []byte("hello verify"), 0600); err != nil {
t.Fatalf("write artifact: %v", err)
}
privateKeyPath, publicKeyPath := writeVerifierKeypair(t)
attestationPath := writeSignedAttestation(t, privateKeyPath, artifactPath, "https://example.com/builder", "git+https://github.com/hashicorp/packer@refs/heads/main")
meta := testMeta(t)
command := &VerifyAttestationCommand{Meta: meta}
ret := command.Run([]string{
"-verifier=" + publicKeyPath,
"-builder-id=https://example.com/other-builder",
attestationPath,
})
if ret == 0 {
t.Fatalf("expected builder mismatch to fail")
}
}
func TestVerifyAttestationCommandRequiresBundleForRekorChecks(t *testing.T) {
artifactPath := filepath.Join(t.TempDir(), "artifact.txt")
if err := os.WriteFile(artifactPath, []byte("hello verify"), 0600); err != nil {
t.Fatalf("write artifact: %v", err)
}
privateKeyPath, publicKeyPath := writeVerifierKeypair(t)
attestationPath := writeSignedAttestation(t, privateKeyPath, artifactPath, "https://example.com/builder", "git+https://github.com/hashicorp/packer@refs/heads/main")
meta := testMeta(t)
command := &VerifyAttestationCommand{Meta: meta}
ret := command.Run([]string{
"-verifier=" + publicKeyPath,
"-require-rekor",
attestationPath,
})
if ret == 0 {
t.Fatalf("expected missing bundle to fail")
}
ui := meta.Ui.(*packersdk.BasicUi)
if got := ui.ErrorWriter.(*bytes.Buffer).String(); !strings.Contains(got, "requires -bundle") {
t.Fatalf("expected missing bundle error, got %q", got)
}
}
func TestVerifyAttestationCommandRejectsTamperedPayload(t *testing.T) {
artifactPath := filepath.Join(t.TempDir(), "artifact.txt")
if err := os.WriteFile(artifactPath, []byte("hello verify"), 0600); err != nil {
t.Fatalf("write artifact: %v", err)
}
privateKeyPath, publicKeyPath := writeVerifierKeypair(t)
attestationPath := writeSignedAttestation(t, privateKeyPath, artifactPath, "https://example.com/builder", "git+https://github.com/hashicorp/packer@refs/heads/main")
tamperAttestationPayload(t, attestationPath)
meta := testMeta(t)
command := &VerifyAttestationCommand{Meta: meta}
ret := command.Run([]string{
"-verifier=" + publicKeyPath,
attestationPath,
})
if ret == 0 {
t.Fatalf("expected tampered payload to fail verification")
}
}
func tamperAttestationPayload(t *testing.T, attestationPath string) {
t.Helper()
contents, err := os.ReadFile(attestationPath)
if err != nil {
t.Fatalf("read attestation: %v", err)
}
var envelope internalattestation.Envelope
if err := json.Unmarshal(contents, &envelope); err != nil {
t.Fatalf("unmarshal envelope: %v", err)
}
payload, err := internalattestation.DecodeEnvelopePayload(envelope)
if err != nil {
t.Fatalf("decode payload: %v", err)
}
tampered := bytes.Replace(payload, []byte("https://example.com/builder"), []byte("https://example.com/evilbuilder"), 1)
if bytes.Equal(tampered, payload) {
t.Fatalf("expected payload to be modified")
}
envelope.Payload = base64.StdEncoding.EncodeToString(tampered)
tamperedEnvelope, err := json.Marshal(envelope)
if err != nil {
t.Fatalf("marshal tampered envelope: %v", err)
}
if err := os.WriteFile(attestationPath, tamperedEnvelope, 0600); err != nil {
t.Fatalf("write tampered attestation: %v", err)
}
}
func writeSignedAttestation(t *testing.T, privateKeyPath, artifactPath, builderID, sourceURI string) string {
t.Helper()
artifactDigest := sha256.Sum256([]byte(readFileString(t, artifactPath)))
statement := internalprovenance.WrapInToto(
[]internalprovenance.Subject{{
Name: filepath.Base(artifactPath),
Digest: internalprovenance.DigestSet{
"sha256": hex.EncodeToString(artifactDigest[:]),
},
}},
internalprovenance.SLSAProvenanceV1PredicateType,
internalprovenance.BuildSLSAPredicate(internalprovenance.PredicateInput{
BuilderID: builderID,
ResolvedDependencies: []internalprovenance.ResolvedDependency{{URI: sourceURI}},
}),
)
payload, err := internalattestation.MarshalPayload(statement)
if err != nil {
t.Fatalf("marshal statement: %v", err)
}
signer, err := internalattestation.NewSigner(context.Background(), internalattestation.BackendConfig{Mode: internalattestation.SigningModeKey, SignerRef: privateKeyPath})
if err != nil {
t.Fatalf("create signer: %v", err)
}
signature, err := signer.Sign(context.Background(), internalattestation.InTotoPayloadType, payload)
if err != nil {
t.Fatalf("sign statement: %v", err)
}
attestationPath := filepath.Join(t.TempDir(), "attestation.json")
envelope, err := json.Marshal(internalattestation.NewEnvelope(internalattestation.InTotoPayloadType, payload, signature))
if err != nil {
t.Fatalf("marshal envelope: %v", err)
}
if err := os.WriteFile(attestationPath, envelope, 0600); err != nil {
t.Fatalf("write attestation: %v", err)
}
return attestationPath
}
func writeVerifierKeypair(t *testing.T) (string, string) {
t.Helper()
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatalf("generate private key: %v", err)
}
privateKeyDER, err := x509.MarshalECPrivateKey(privateKey)
if err != nil {
t.Fatalf("marshal private key: %v", err)
}
publicKeyDER, err := x509.MarshalPKIXPublicKey(privateKey.Public())
if err != nil {
t.Fatalf("marshal public key: %v", err)
}
dir := t.TempDir()
privateKeyPath := filepath.Join(dir, "signer.pem")
publicKeyPath := filepath.Join(dir, "verifier.pem")
if err := os.WriteFile(privateKeyPath, pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: privateKeyDER}), 0600); err != nil {
t.Fatalf("write private key: %v", err)
}
if err := os.WriteFile(publicKeyPath, pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: publicKeyDER}), 0600); err != nil {
t.Fatalf("write public key: %v", err)
}
return privateKeyPath, publicKeyPath
}
func readFileString(t *testing.T, path string) string {
t.Helper()
contents, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read file %q: %v", path, err)
}
return string(contents)
}