diff --git a/cli/slsa-verifier/verify/utils.go b/cli/slsa-verifier/verify/utils.go index 065e924d8..3e2639507 100644 --- a/cli/slsa-verifier/verify/utils.go +++ b/cli/slsa-verifier/verify/utils.go @@ -15,10 +15,13 @@ package verify import ( + "crypto/sha256" "encoding/hex" + "fmt" "hash" "io" "os" + "strings" ) func computeFileHash(filePath string, h hash.Hash) (string, error) { @@ -33,3 +36,38 @@ func computeFileHash(filePath string, h hash.Hash) (string, error) { } return hex.EncodeToString(h.Sum(nil)), nil } + +// computeArtifactHash returns the hash of the artifact. +// If artifact is a digest (sha256:xxx or sha512:xxx), it parses and validates it. +// Otherwise, it computes the SHA256 hash of the file at the given path. +func computeArtifactHash(artifact string) (string, error) { + if strings.HasPrefix(artifact, "sha256:") || strings.HasPrefix(artifact, "sha512:") { + return parseDigest(artifact) + } + return computeFileHash(artifact, sha256.New()) +} + +// parseDigest parses and validates a digest string in the format "algorithm:hexvalue" +// and returns the hex value. +func parseDigest(digest string) (string, error) { + parts := strings.SplitN(digest, ":", 2) + if len(parts) != 2 { + return "", fmt.Errorf("invalid digest format: %s", digest) + } + algorithm := parts[0] + h := parts[1] + + expectedLen := map[string]int{"sha256": 64, "sha512": 128} + exp, ok := expectedLen[algorithm] + if !ok { + return "", fmt.Errorf("unsupported digest algorithm: %s (supported: sha256, sha512)", algorithm) + } + if len(h) != exp { + return "", fmt.Errorf("invalid %s digest length: expected %d characters, got %d", algorithm, exp, len(h)) + } + if _, err := hex.DecodeString(h); err != nil { + return "", fmt.Errorf("invalid hex in digest: %s", h) + } + + return h, nil +} diff --git a/cli/slsa-verifier/verify/utils_test.go b/cli/slsa-verifier/verify/utils_test.go new file mode 100644 index 000000000..4509f2261 --- /dev/null +++ b/cli/slsa-verifier/verify/utils_test.go @@ -0,0 +1,173 @@ +// Copyright 2023 SLSA Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package verify + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestParseDigest(t *testing.T) { + tests := []struct { + name string + input string + want string + wantErr bool + }{ + { + name: "valid sha256", + input: "sha256:" + strings.Repeat("a", 64), + want: strings.Repeat("a", 64), + wantErr: false, + }, + { + name: "valid sha512", + input: "sha512:" + strings.Repeat("b", 128), + want: strings.Repeat("b", 128), + wantErr: false, + }, + { + name: "valid sha256 with mixed case hex", + input: "sha256:" + strings.Repeat("aB", 32), + want: strings.Repeat("aB", 32), + wantErr: false, + }, + { + name: "sha256 wrong length - too short", + input: "sha256:abc", + want: "", + wantErr: true, + }, + { + name: "sha256 wrong length - too long", + input: "sha256:" + strings.Repeat("a", 65), + want: "", + wantErr: true, + }, + { + name: "sha512 wrong length", + input: "sha512:" + strings.Repeat("a", 64), + want: "", + wantErr: true, + }, + { + name: "invalid hex characters", + input: "sha256:" + strings.Repeat("g", 64), + want: "", + wantErr: true, + }, + { + name: "unsupported algorithm sha384", + input: "sha384:" + strings.Repeat("a", 96), + want: "", + wantErr: true, + }, + { + name: "unsupported algorithm md5", + input: "md5:" + strings.Repeat("a", 32), + want: "", + wantErr: true, + }, + { + name: "missing colon", + input: "sha256" + strings.Repeat("a", 64), + want: "", + wantErr: true, + }, + { + name: "empty hash value", + input: "sha256:", + want: "", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseDigest(tt.input) + if (err != nil) != tt.wantErr { + t.Errorf("parseDigest() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("parseDigest() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestComputeArtifactHash(t *testing.T) { + // Create a temporary file for testing file hash computation + tmpDir := t.TempDir() + tmpFile := filepath.Join(tmpDir, "test.txt") + content := []byte("hello world") + if err := os.WriteFile(tmpFile, content, 0o600); err != nil { + t.Fatalf("failed to create temp file: %v", err) + } + // SHA256 of "hello world" is b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9 + + tests := []struct { + name string + input string + want string + wantErr bool + }{ + { + name: "sha256 digest", + input: "sha256:" + strings.Repeat("a", 64), + want: strings.Repeat("a", 64), + wantErr: false, + }, + { + name: "sha512 digest", + input: "sha512:" + strings.Repeat("b", 128), + want: strings.Repeat("b", 128), + wantErr: false, + }, + { + name: "file path", + input: tmpFile, + want: "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", + wantErr: false, + }, + { + name: "invalid digest format", + input: "sha256:invalid", + want: "", + wantErr: true, + }, + { + name: "non-existent file", + input: "/non/existent/file.txt", + want: "", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := computeArtifactHash(tt.input) + if (err != nil) != tt.wantErr { + t.Errorf("computeArtifactHash() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("computeArtifactHash() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/cli/slsa-verifier/verify/verify_artifact.go b/cli/slsa-verifier/verify/verify_artifact.go index d6fb4ec0f..d5cf32ea7 100644 --- a/cli/slsa-verifier/verify/verify_artifact.go +++ b/cli/slsa-verifier/verify/verify_artifact.go @@ -16,7 +16,6 @@ package verify import ( "context" - "crypto/sha256" "fmt" "os" @@ -41,7 +40,7 @@ func (c *VerifyArtifactCommand) Exec(ctx context.Context, artifacts []string) (* var builderID *utils.TrustedBuilderID for _, artifact := range artifacts { - artifactHash, err := computeFileHash(artifact, sha256.New()) + artifactHash, err := computeArtifactHash(artifact) if err != nil { fmt.Fprintf(os.Stderr, "Verifying artifact %s: FAILED: %v\n\n", artifact, err) return nil, err