Skip to content

Commit 69b7eed

Browse files
authored
feat: Check decompressed coder-slim binaries via SHA1 (coder#2556)
1 parent a0c8e70 commit 69b7eed

File tree

3 files changed

+261
-42
lines changed

3 files changed

+261
-42
lines changed

scripts/build_go_slim.sh

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ done
5858
# Check dependencies
5959
dependencies go
6060
if [[ $compress != 0 ]]; then
61-
dependencies tar zstd
61+
dependencies shasum tar zstd
6262

6363
if [[ $compress != [0-9]* ]] || [[ $compress -gt 22 ]] || [[ $compress -lt 1 ]]; then
6464
error "Invalid value for compress, must in in the range of [1, 22]"
@@ -110,10 +110,24 @@ for f in ./coder-slim_*; do
110110
done
111111

112112
if [[ $compress != 0 ]]; then
113-
log "--- Compressing coder-slim binaries using zstd level $compress ($dest_dir/coder.tar.zst)"
114113
pushd "$dest_dir"
115-
tar cf coder.tar coder-*
114+
sha_file=coder.sha1
115+
sha_dest="$dest_dir/$sha_file"
116+
log "--- Generating SHA1 for coder-slim binaries ($sha_dest)"
117+
shasum -b -a 1 coder-* | tee $sha_file
118+
echo "$sha_dest"
119+
log
120+
log
121+
122+
tar_name=coder.tar.zst
123+
tar_dest="$dest_dir/$tar_name"
124+
log "--- Compressing coder-slim binaries using zstd level $compress ($tar_dest)"
125+
tar cf coder.tar $sha_file coder-*
116126
rm coder-*
117-
zstd --force --ultra --long -"${compress}" --rm --no-progress coder.tar -o coder.tar.zst
127+
zstd --force --ultra --long -"${compress}" --rm --no-progress coder.tar -o $tar_name
128+
echo "$tar_dest"
129+
log
130+
log
131+
118132
popd
119133
fi

site/site.go

Lines changed: 105 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"archive/tar"
55
"bytes"
66
"context"
7+
"crypto/sha1" //#nosec // Not used for cryptography.
8+
"encoding/hex"
79
"errors"
810
"fmt"
911
"io"
@@ -20,6 +22,7 @@ import (
2022
"github.com/klauspost/compress/zstd"
2123
"github.com/unrolled/secure"
2224
"golang.org/x/exp/slices"
25+
"golang.org/x/sync/errgroup"
2326
"golang.org/x/xerrors"
2427
)
2528

@@ -439,12 +442,18 @@ func ExtractOrReadBinFS(dest string, siteFS fs.FS) (http.FileSystem, error) {
439442
return nil, err
440443
}
441444

442-
n, err := extractBin(dest, archive)
445+
ok, err := verifyBinSha1IsCurrent(dest, siteFS)
443446
if err != nil {
444-
return nil, xerrors.Errorf("extract coder binaries failed: %w", err)
447+
return nil, xerrors.Errorf("verify coder binaries sha1 failed: %w", err)
445448
}
446-
if n == 0 {
447-
return nil, xerrors.New("no files were extracted from coder binaries archive")
449+
if !ok {
450+
n, err := extractBin(dest, archive)
451+
if err != nil {
452+
return nil, xerrors.Errorf("extract coder binaries failed: %w", err)
453+
}
454+
if n == 0 {
455+
return nil, xerrors.New("no files were extracted from coder binaries archive")
456+
}
448457
}
449458

450459
return dir, nil
@@ -461,6 +470,98 @@ func filterFiles(files []fs.DirEntry, names ...string) []fs.DirEntry {
461470
return filtered
462471
}
463472

473+
// errHashMismatch is a sentinel error used in verifyBinSha1IsCurrent.
474+
var errHashMismatch = xerrors.New("hash mismatch")
475+
476+
func verifyBinSha1IsCurrent(dest string, siteFS fs.FS) (ok bool, err error) {
477+
b1, err := fs.ReadFile(siteFS, "bin/coder.sha1")
478+
if err != nil {
479+
return false, xerrors.Errorf("read coder sha1 from embedded fs failed: %w", err)
480+
}
481+
// Parse sha1 file.
482+
shaFiles := make(map[string][]byte)
483+
for _, line := range bytes.Split(bytes.TrimSpace(b1), []byte{'\n'}) {
484+
parts := bytes.Split(line, []byte{' ', '*'})
485+
if len(parts) != 2 {
486+
return false, xerrors.Errorf("malformed sha1 file: %w", err)
487+
}
488+
shaFiles[string(parts[1])] = parts[0]
489+
}
490+
if len(shaFiles) == 0 {
491+
return false, xerrors.Errorf("empty sha1 file: %w", err)
492+
}
493+
494+
b2, err := os.ReadFile(filepath.Join(dest, "coder.sha1"))
495+
if err != nil {
496+
if xerrors.Is(err, fs.ErrNotExist) {
497+
return false, nil
498+
}
499+
return false, xerrors.Errorf("read coder sha1 failed: %w", err)
500+
}
501+
502+
// Check shasum files for equality for early-exit.
503+
if !bytes.Equal(b1, b2) {
504+
return false, nil
505+
}
506+
507+
var eg errgroup.Group
508+
// Speed up startup by verifying files concurrently. Concurrency
509+
// is limited to save resources / early-exit. Early-exit speed
510+
// could be improved by using a context aware io.Reader and
511+
// passing the context from errgroup.WithContext.
512+
eg.SetLimit(3)
513+
514+
// Verify the hash of each on-disk binary.
515+
for file, hash1 := range shaFiles {
516+
file := file
517+
hash1 := hash1
518+
eg.Go(func() error {
519+
hash2, err := sha1HashFile(filepath.Join(dest, file))
520+
if err != nil {
521+
if xerrors.Is(err, fs.ErrNotExist) {
522+
return errHashMismatch
523+
}
524+
return xerrors.Errorf("hash file failed: %w", err)
525+
}
526+
if !bytes.Equal(hash1, hash2) {
527+
return errHashMismatch
528+
}
529+
return nil
530+
})
531+
}
532+
err = eg.Wait()
533+
if err != nil {
534+
if xerrors.Is(err, errHashMismatch) {
535+
return false, nil
536+
}
537+
return false, err
538+
}
539+
540+
return true, nil
541+
}
542+
543+
// sha1HashFile computes a SHA1 hash of the file, returning the hex
544+
// representation.
545+
func sha1HashFile(name string) ([]byte, error) {
546+
//#nosec // Not used for cryptography.
547+
hash := sha1.New()
548+
f, err := os.Open(name)
549+
if err != nil {
550+
return nil, err
551+
}
552+
defer f.Close()
553+
554+
_, err = io.Copy(hash, f)
555+
if err != nil {
556+
return nil, err
557+
}
558+
559+
b := make([]byte, hash.Size())
560+
hash.Sum(b[:0])
561+
562+
return []byte(hex.EncodeToString(b)), nil
563+
}
564+
464565
func extractBin(dest string, r io.Reader) (numExtraced int, err error) {
465566
opts := []zstd.DOption{
466567
// Concurrency doesn't help us when decoding the tar and

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy