Skip to content

Commit cabfb83

Browse files
feat: support digest arguments in verify-artifact command
Allow verify-artifact to accept sha256:xxx or sha512:xxx digest strings as positional arguments instead of file paths. This enables verification without downloading large artifacts when only the digest is known. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: Shunsuke Suzuki <suzuki.shunsuke.1989@gmail.com>
1 parent 748161a commit cabfb83

3 files changed

Lines changed: 212 additions & 2 deletions

File tree

cli/slsa-verifier/verify/utils.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,13 @@
1515
package verify
1616

1717
import (
18+
"crypto/sha256"
1819
"encoding/hex"
20+
"fmt"
1921
"hash"
2022
"io"
2123
"os"
24+
"strings"
2225
)
2326

2427
func computeFileHash(filePath string, h hash.Hash) (string, error) {
@@ -33,3 +36,38 @@ func computeFileHash(filePath string, h hash.Hash) (string, error) {
3336
}
3437
return hex.EncodeToString(h.Sum(nil)), nil
3538
}
39+
40+
// computeArtifactHash returns the hash of the artifact.
41+
// If artifact is a digest (sha256:xxx or sha512:xxx), it parses and validates it.
42+
// Otherwise, it computes the SHA256 hash of the file at the given path.
43+
func computeArtifactHash(artifact string) (string, error) {
44+
if strings.HasPrefix(artifact, "sha256:") || strings.HasPrefix(artifact, "sha512:") {
45+
return parseDigest(artifact)
46+
}
47+
return computeFileHash(artifact, sha256.New())
48+
}
49+
50+
// parseDigest parses and validates a digest string in the format "algorithm:hexvalue"
51+
// and returns the hex value.
52+
func parseDigest(digest string) (string, error) {
53+
parts := strings.SplitN(digest, ":", 2)
54+
if len(parts) != 2 {
55+
return "", fmt.Errorf("invalid digest format: %s", digest)
56+
}
57+
algorithm := parts[0]
58+
hash := parts[1]
59+
60+
expectedLen := map[string]int{"sha256": 64, "sha512": 128}
61+
exp, ok := expectedLen[algorithm]
62+
if !ok {
63+
return "", fmt.Errorf("unsupported digest algorithm: %s (supported: sha256, sha512)", algorithm)
64+
}
65+
if len(hash) != exp {
66+
return "", fmt.Errorf("invalid %s digest length: expected %d characters, got %d", algorithm, exp, len(hash))
67+
}
68+
if _, err := hex.DecodeString(hash); err != nil {
69+
return "", fmt.Errorf("invalid hex in digest: %s", hash)
70+
}
71+
72+
return hash, nil
73+
}
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
// Copyright 2023 SLSA Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package verify
16+
17+
import (
18+
"os"
19+
"path/filepath"
20+
"strings"
21+
"testing"
22+
)
23+
24+
func TestParseDigest(t *testing.T) {
25+
tests := []struct {
26+
name string
27+
input string
28+
want string
29+
wantErr bool
30+
}{
31+
{
32+
name: "valid sha256",
33+
input: "sha256:" + strings.Repeat("a", 64),
34+
want: strings.Repeat("a", 64),
35+
wantErr: false,
36+
},
37+
{
38+
name: "valid sha512",
39+
input: "sha512:" + strings.Repeat("b", 128),
40+
want: strings.Repeat("b", 128),
41+
wantErr: false,
42+
},
43+
{
44+
name: "valid sha256 with mixed case hex",
45+
input: "sha256:" + strings.Repeat("aB", 32),
46+
want: strings.Repeat("aB", 32),
47+
wantErr: false,
48+
},
49+
{
50+
name: "sha256 wrong length - too short",
51+
input: "sha256:abc",
52+
want: "",
53+
wantErr: true,
54+
},
55+
{
56+
name: "sha256 wrong length - too long",
57+
input: "sha256:" + strings.Repeat("a", 65),
58+
want: "",
59+
wantErr: true,
60+
},
61+
{
62+
name: "sha512 wrong length",
63+
input: "sha512:" + strings.Repeat("a", 64),
64+
want: "",
65+
wantErr: true,
66+
},
67+
{
68+
name: "invalid hex characters",
69+
input: "sha256:" + strings.Repeat("g", 64),
70+
want: "",
71+
wantErr: true,
72+
},
73+
{
74+
name: "unsupported algorithm sha384",
75+
input: "sha384:" + strings.Repeat("a", 96),
76+
want: "",
77+
wantErr: true,
78+
},
79+
{
80+
name: "unsupported algorithm md5",
81+
input: "md5:" + strings.Repeat("a", 32),
82+
want: "",
83+
wantErr: true,
84+
},
85+
{
86+
name: "missing colon",
87+
input: "sha256" + strings.Repeat("a", 64),
88+
want: "",
89+
wantErr: true,
90+
},
91+
{
92+
name: "empty hash value",
93+
input: "sha256:",
94+
want: "",
95+
wantErr: true,
96+
},
97+
}
98+
99+
for _, tt := range tests {
100+
t.Run(tt.name, func(t *testing.T) {
101+
got, err := parseDigest(tt.input)
102+
if (err != nil) != tt.wantErr {
103+
t.Errorf("parseDigest() error = %v, wantErr %v", err, tt.wantErr)
104+
return
105+
}
106+
if got != tt.want {
107+
t.Errorf("parseDigest() = %v, want %v", got, tt.want)
108+
}
109+
})
110+
}
111+
}
112+
113+
func TestComputeArtifactHash(t *testing.T) {
114+
// Create a temporary file for testing file hash computation
115+
tmpDir := t.TempDir()
116+
tmpFile := filepath.Join(tmpDir, "test.txt")
117+
content := []byte("hello world")
118+
if err := os.WriteFile(tmpFile, content, 0o600); err != nil {
119+
t.Fatalf("failed to create temp file: %v", err)
120+
}
121+
// SHA256 of "hello world" is b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9
122+
123+
tests := []struct {
124+
name string
125+
input string
126+
want string
127+
wantErr bool
128+
}{
129+
{
130+
name: "sha256 digest",
131+
input: "sha256:" + strings.Repeat("a", 64),
132+
want: strings.Repeat("a", 64),
133+
wantErr: false,
134+
},
135+
{
136+
name: "sha512 digest",
137+
input: "sha512:" + strings.Repeat("b", 128),
138+
want: strings.Repeat("b", 128),
139+
wantErr: false,
140+
},
141+
{
142+
name: "file path",
143+
input: tmpFile,
144+
want: "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
145+
wantErr: false,
146+
},
147+
{
148+
name: "invalid digest format",
149+
input: "sha256:invalid",
150+
want: "",
151+
wantErr: true,
152+
},
153+
{
154+
name: "non-existent file",
155+
input: "/non/existent/file.txt",
156+
want: "",
157+
wantErr: true,
158+
},
159+
}
160+
161+
for _, tt := range tests {
162+
t.Run(tt.name, func(t *testing.T) {
163+
got, err := computeArtifactHash(tt.input)
164+
if (err != nil) != tt.wantErr {
165+
t.Errorf("computeArtifactHash() error = %v, wantErr %v", err, tt.wantErr)
166+
return
167+
}
168+
if got != tt.want {
169+
t.Errorf("computeArtifactHash() = %v, want %v", got, tt.want)
170+
}
171+
})
172+
}
173+
}

cli/slsa-verifier/verify/verify_artifact.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ package verify
1616

1717
import (
1818
"context"
19-
"crypto/sha256"
2019
"fmt"
2120
"os"
2221

@@ -41,7 +40,7 @@ func (c *VerifyArtifactCommand) Exec(ctx context.Context, artifacts []string) (*
4140
var builderID *utils.TrustedBuilderID
4241

4342
for _, artifact := range artifacts {
44-
artifactHash, err := computeFileHash(artifact, sha256.New())
43+
artifactHash, err := computeArtifactHash(artifact)
4544
if err != nil {
4645
fmt.Fprintf(os.Stderr, "Verifying artifact %s: FAILED: %v\n\n", artifact, err)
4746
return nil, err

0 commit comments

Comments
 (0)