Skip to content

Commit e3e1fe4

Browse files
committed
Refactor plugin verifications
1 parent 960377a commit e3e1fe4

File tree

1 file changed

+70
-62
lines changed

1 file changed

+70
-62
lines changed

plugin/install.go

Lines changed: 70 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -95,23 +95,15 @@ var ErrPluginNotVerified = errors.New("plugin not verified")
9595

9696
// Install fetches the release from GitHub and puts the binary in the plugin directory.
9797
// This installation process will automatically check the checksum of the downloaded zip file.
98-
// Therefore, the release must always contain a checksum file.
99-
// In addition, the release must meet the following conventions:
98+
// The release must always contain a checksum file and meet the following conventions:
10099
//
101100
// - The release must be tagged with a name like v1.1.1
102101
// - The release must contain an asset with a name like tflint-ruleset-{name}_{GOOS}_{GOARCH}.zip
103102
// - The zip file must contain a binary named tflint-ruleset-{name} (tflint-ruleset-{name}.exe in Windows)
104103
// - The release must contain a checksum file for the zip file with the name checksums.txt
105104
// - The checksum file must contain a sha256 hash and filename
106105
//
107-
// If Artifact Attestations are present, TFLint will verify the checksum file
108-
// to ensure that it has not been tampered with.
109-
//
110-
// If the following conditions are met, the checksum file will be verified
111-
// as being signed with the PGP key.
112-
//
113-
// - The release must contain a signature file for the checksum file with the name checksums.txt.sig
114-
// - The signature file must be binary OpenPGP format
106+
// If possible, verify the signature to ensure that the checksum file has not been tampered with.
115107
func (c *InstallConfig) Install() (string, error) {
116108
dir, err := getPluginDir(c.globalConfig)
117109
if err != nil {
@@ -138,62 +130,17 @@ func (c *InstallConfig) Install() (string, error) {
138130
return "", fmt.Errorf("Failed to download checksums.txt: %s", err)
139131
}
140132

141-
var skipVerify bool
133+
var verified bool
142134
sigchecker := NewSignatureChecker(c)
143135
if sigchecker.HasSigningKey() {
144-
// Verify by PGP signing key
145-
log.Printf("[DEBUG] Download checksums.txt.sig")
146-
signatureFile, err := c.downloadToTempFile(assets["checksums.txt.sig"])
147-
if signatureFile != nil {
148-
defer os.Remove(signatureFile.Name())
136+
if err := c.verifyChecksumsSignature(sigchecker, checksumsFile, assets); err != nil {
137+
return "", err
149138
}
150-
if err != nil {
151-
return "", fmt.Errorf("Failed to download checksums.txt.sig: %s", err)
152-
}
153-
154-
if err := sigchecker.Verify(checksumsFile, signatureFile); err != nil {
155-
return "", fmt.Errorf("Failed to check checksums.txt signature: %s", err)
156-
}
157-
if _, err := checksumsFile.Seek(0, 0); err != nil {
158-
return "", fmt.Errorf("Failed to check checksums.txt signature: %s", err)
159-
}
160-
log.Printf("[DEBUG] Verified signature successfully")
161-
139+
verified = true
162140
} else {
163-
// Attempt to verify by artifact attestations.
164-
var attestations []*github.Attestation
165-
repo, err := c.fetchRepository()
141+
verified, err = c.tryKeylessVerifyChecksumsSignature(sigchecker, checksumsFile)
166142
if err != nil {
167-
return "", fmt.Errorf("Failed to get GitHub repository metadata: %s", err)
168-
}
169-
// If the repository is private, artifact attestations is not always available
170-
// because it requires GitHub Enterprise Cloud plan, so we skip verification here.
171-
if repo.Private != nil && *repo.Private {
172-
skipVerify = true
173-
} else {
174-
log.Printf("[DEBUG] Download artifact attestations")
175-
attestations, err = c.fetchArtifactAttestations(checksumsFile)
176-
if err != nil {
177-
var gerr *github.ErrorResponse
178-
// If there are no attestations, it will be ignored without errors.
179-
// However, experimental mode is enabled, enforces that attestations are present.
180-
if errors.As(err, &gerr) && gerr.Response.StatusCode == 404 && !IsExperimentalModeEnabled() {
181-
log.Printf("[DEBUG] Artifact attestations not found and will be ignored: %s", err)
182-
skipVerify = true
183-
} else {
184-
return "", fmt.Errorf("Failed to download artifact attestations: %s", err)
185-
}
186-
}
187-
}
188-
189-
if !skipVerify {
190-
if err := sigchecker.VerifyKeyless(checksumsFile, attestations); err != nil {
191-
return "", fmt.Errorf("Failed to check checksums.txt signature: %s", err)
192-
}
193-
if _, err := checksumsFile.Seek(0, 0); err != nil {
194-
return "", fmt.Errorf("Failed to check checksums.txt signature: %s", err)
195-
}
196-
log.Printf("[DEBUG] Verified signature successfully")
143+
return "", err
197144
}
198145
}
199146

@@ -220,12 +167,73 @@ func (c *InstallConfig) Install() (string, error) {
220167
}
221168

222169
log.Printf("[DEBUG] Installed %s successfully", path)
223-
if skipVerify {
170+
if !verified {
224171
return path, ErrPluginNotVerified
225172
}
226173
return path, nil
227174
}
228175

176+
// Verify checksums.txt.sig by PGP signing key.
177+
// The release must contain a signature file for the checksum file with the name checksums.txt.sig.
178+
// The signature file must be binary OpenPGP format.
179+
func (c *InstallConfig) verifyChecksumsSignature(sigchecker *SignatureChecker, checksum io.ReadSeeker, assets map[string]*github.ReleaseAsset) error {
180+
log.Printf("[DEBUG] Download checksums.txt.sig")
181+
signatureFile, err := c.downloadToTempFile(assets["checksums.txt.sig"])
182+
if signatureFile != nil {
183+
defer os.Remove(signatureFile.Name())
184+
}
185+
if err != nil {
186+
return fmt.Errorf("Failed to download checksums.txt.sig: %s", err)
187+
}
188+
189+
if err := sigchecker.Verify(checksum, signatureFile); err != nil {
190+
return fmt.Errorf("Failed to check checksums.txt signature: %s", err)
191+
}
192+
if _, err := checksum.Seek(0, 0); err != nil {
193+
return fmt.Errorf("Failed to check checksums.txt signature: %s", err)
194+
}
195+
196+
log.Printf("[DEBUG] Verified signature successfully")
197+
return nil
198+
}
199+
200+
// Verify checksums.txt signature by artifact attestations.
201+
func (c *InstallConfig) tryKeylessVerifyChecksumsSignature(sigchecker *SignatureChecker, checksum io.ReadSeeker) (bool, error) {
202+
repo, err := c.fetchRepository()
203+
if err != nil {
204+
return false, fmt.Errorf("Failed to get GitHub repository metadata: %s", err)
205+
}
206+
// If the repository is private, artifact attestations is not always available
207+
// because it requires GitHub Enterprise Cloud plan, so we skip verification here.
208+
if repo.Private != nil && *repo.Private {
209+
return false, nil
210+
}
211+
212+
log.Printf("[DEBUG] Download artifact attestations")
213+
attestations, err := c.fetchArtifactAttestations(checksum)
214+
if err != nil {
215+
var gerr *github.ErrorResponse
216+
// If there are no attestations, it will be ignored without errors.
217+
// However, experimental mode is enabled, enforces that attestations are present.
218+
if errors.As(err, &gerr) && gerr.Response.StatusCode == 404 && !IsExperimentalModeEnabled() {
219+
log.Printf("[DEBUG] Artifact attestations not found and will be ignored: %s", err)
220+
return false, nil
221+
} else {
222+
return false, fmt.Errorf("Failed to download artifact attestations: %s", err)
223+
}
224+
}
225+
226+
if err := sigchecker.VerifyKeyless(checksum, attestations); err != nil {
227+
return false, fmt.Errorf("Failed to check checksums.txt signature: %s", err)
228+
}
229+
if _, err := checksum.Seek(0, 0); err != nil {
230+
return false, fmt.Errorf("Failed to check checksums.txt signature: %s", err)
231+
}
232+
233+
log.Printf("[DEBUG] Verified signature successfully")
234+
return true, nil
235+
}
236+
229237
// fetchReleaseAssets fetches assets from the GitHub release.
230238
// The release is determined by the source path and tag name.
231239
func (c *InstallConfig) fetchReleaseAssets() (map[string]*github.ReleaseAsset, error) {

0 commit comments

Comments
 (0)