Update golangci-lint to v2.12.2 with canonical config (closes #60)
check / check (push) Has been cancelled
check / check (push) Has been cancelled
Adopts golangci-lint v2.12.2 and the canonical .golangci.yml (default: all), and fixes all resulting findings across the tree. Two intended behavior changes: absent MFFilePath.Mtime is handled explicitly in freshen, list and export rather than dereferenced (main panicked); gpg positional key IDs now follow an explicit -- end-of-options marker. All twelve reworded user-visible error messages restored to byte-identical parity with main and pinned by tests.
This commit was merged in pull request #59.
This commit is contained in:
+129
-46
@@ -15,105 +15,174 @@ import (
|
||||
"sneak.berlin/go/mfer/internal/log"
|
||||
)
|
||||
|
||||
var (
|
||||
errInvalidUUIDLength = errors.New("invalid UUID length")
|
||||
errInvalidUUIDFormat = errors.New("invalid UUID format")
|
||||
errUnknownVersion = errors.New("unknown version")
|
||||
errUnknownCompression = errors.New("unknown compression type")
|
||||
errCompressedHashWrong = errors.New("compressed data hash mismatch")
|
||||
errSignatureNoPubKey = errors.New("signature present but no public key")
|
||||
errDecompressedTooLarge = errors.New("decompressed data exceeds maximum allowed size")
|
||||
errUUIDMismatch = errors.New("outer and inner UUID mismatch")
|
||||
errInvalidFileFormat = errors.New("invalid file format")
|
||||
)
|
||||
|
||||
// validateUUID checks that the byte slice is a valid UUID (16 bytes, parseable).
|
||||
func validateUUID(data []byte) error {
|
||||
if len(data) != 16 {
|
||||
return errors.New("invalid UUID length")
|
||||
if len(data) != uuidLength {
|
||||
return errInvalidUUIDLength
|
||||
}
|
||||
// Try to parse as UUID to validate format
|
||||
_, err := uuid.FromBytes(data)
|
||||
if err != nil {
|
||||
return errors.New("invalid UUID format")
|
||||
return errInvalidUUIDFormat
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *manifest) deserializeInner() error {
|
||||
if m.pbOuter.Version != MFFileOuter_VERSION_ONE {
|
||||
return errors.New("unknown version")
|
||||
// validateOuterHeader checks the outer message's version, compression
|
||||
// type, and UUID.
|
||||
func (m *manifest) validateOuterHeader() error {
|
||||
if m.pbOuter.GetVersion() != MFFileOuter_VERSION_ONE {
|
||||
return errUnknownVersion
|
||||
}
|
||||
if m.pbOuter.CompressionType != MFFileOuter_COMPRESSION_ZSTD {
|
||||
return errors.New("unknown compression type")
|
||||
|
||||
if m.pbOuter.GetCompressionType() != MFFileOuter_COMPRESSION_ZSTD {
|
||||
return errUnknownCompression
|
||||
}
|
||||
|
||||
// Validate outer UUID before any decompression
|
||||
if err := validateUUID(m.pbOuter.Uuid); err != nil {
|
||||
return errors.New("outer UUID invalid: " + err.Error())
|
||||
err := validateUUID(m.pbOuter.GetUuid())
|
||||
if err != nil {
|
||||
return fmt.Errorf("outer UUID invalid: %w", err)
|
||||
}
|
||||
|
||||
// Verify hash of compressed data before decompression
|
||||
return nil
|
||||
}
|
||||
|
||||
// verifyOuterIntegrity checks the hash of the compressed payload and,
|
||||
// if a signature is present, verifies it against the embedded public key.
|
||||
func (m *manifest) verifyOuterIntegrity() error {
|
||||
h := sha256.New()
|
||||
if _, err := h.Write(m.pbOuter.InnerMessage); err != nil {
|
||||
|
||||
_, err := h.Write(m.pbOuter.GetInnerMessage())
|
||||
if err != nil {
|
||||
return fmt.Errorf("deserialize: hash write: %w", err)
|
||||
}
|
||||
|
||||
sha256Hash := h.Sum(nil)
|
||||
if !bytes.Equal(sha256Hash, m.pbOuter.Sha256) {
|
||||
return errors.New("compressed data hash mismatch")
|
||||
if !bytes.Equal(sha256Hash, m.pbOuter.GetSha256()) {
|
||||
return errCompressedHashWrong
|
||||
}
|
||||
|
||||
// Verify signature if present
|
||||
if len(m.pbOuter.Signature) > 0 {
|
||||
if len(m.pbOuter.SigningPubKey) == 0 {
|
||||
return errors.New("signature present but no public key")
|
||||
}
|
||||
|
||||
sigString, err := m.signatureString()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate signature string for verification: %w", err)
|
||||
}
|
||||
|
||||
if err := gpgVerify([]byte(sigString), m.pbOuter.Signature, m.pbOuter.SigningPubKey); err != nil {
|
||||
return fmt.Errorf("signature verification failed: %w", err)
|
||||
}
|
||||
log.Infof("signature verified successfully")
|
||||
if len(m.pbOuter.GetSignature()) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
bb := bytes.NewBuffer(m.pbOuter.InnerMessage)
|
||||
if len(m.pbOuter.GetSigningPubKey()) == 0 {
|
||||
return errSignatureNoPubKey
|
||||
}
|
||||
|
||||
sigString, err := m.signatureString()
|
||||
if err != nil {
|
||||
return fmt.Errorf(
|
||||
"failed to generate signature string for verification: %w", err,
|
||||
)
|
||||
}
|
||||
|
||||
err = gpgVerify(
|
||||
[]byte(sigString),
|
||||
m.pbOuter.GetSignature(),
|
||||
m.pbOuter.GetSigningPubKey(),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("signature verification failed: %w", err)
|
||||
}
|
||||
|
||||
log.Infof("signature verified successfully")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// decompressInner decompresses the inner payload, enforcing size limits
|
||||
// to prevent decompression bombs.
|
||||
func (m *manifest) decompressInner() ([]byte, error) {
|
||||
bb := bytes.NewBuffer(m.pbOuter.GetInnerMessage())
|
||||
|
||||
zr, err := zstd.NewReader(bb)
|
||||
if err != nil {
|
||||
return fmt.Errorf("deserialize: zstd reader: %w", err)
|
||||
return nil, fmt.Errorf("deserialize: zstd reader: %w", err)
|
||||
}
|
||||
defer zr.Close()
|
||||
|
||||
// Limit decompressed size to prevent decompression bombs.
|
||||
// Use declared size + 1 byte to detect overflow, capped at MaxDecompressedSize.
|
||||
maxSize := MaxDecompressedSize
|
||||
if m.pbOuter.Size > 0 && m.pbOuter.Size < int64(maxSize) {
|
||||
maxSize = int64(m.pbOuter.Size) + 1
|
||||
if m.pbOuter.GetSize() > 0 && m.pbOuter.GetSize() < maxSize {
|
||||
maxSize = m.pbOuter.GetSize() + 1
|
||||
}
|
||||
|
||||
limitedReader := io.LimitReader(zr, maxSize)
|
||||
|
||||
dat, err := io.ReadAll(limitedReader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("deserialize: decompress: %w", err)
|
||||
return nil, fmt.Errorf("deserialize: decompress: %w", err)
|
||||
}
|
||||
|
||||
if int64(len(dat)) >= MaxDecompressedSize {
|
||||
return fmt.Errorf("decompressed data exceeds maximum allowed size of %d bytes", MaxDecompressedSize)
|
||||
return nil, fmt.Errorf(
|
||||
"%w of %d bytes", errDecompressedTooLarge, MaxDecompressedSize,
|
||||
)
|
||||
}
|
||||
|
||||
return dat, nil
|
||||
}
|
||||
|
||||
func (m *manifest) deserializeInner() error {
|
||||
err := m.validateOuterHeader()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.verifyOuterIntegrity()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dat, err := m.decompressInner()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
isize := len(dat)
|
||||
if int64(isize) != m.pbOuter.Size {
|
||||
log.Debugf("truncated data, got %d expected %d", isize, m.pbOuter.Size)
|
||||
if int64(isize) != m.pbOuter.GetSize() {
|
||||
log.Debugf("truncated data, got %d expected %d", isize, m.pbOuter.GetSize())
|
||||
|
||||
return bork.ErrFileTruncated
|
||||
}
|
||||
|
||||
// Deserialize inner message
|
||||
m.pbInner = new(MFFile)
|
||||
if err := proto.Unmarshal(dat, m.pbInner); err != nil {
|
||||
|
||||
err = proto.Unmarshal(dat, m.pbInner)
|
||||
if err != nil {
|
||||
return fmt.Errorf("deserialize: unmarshal inner: %w", err)
|
||||
}
|
||||
|
||||
// Validate inner UUID
|
||||
if err := validateUUID(m.pbInner.Uuid); err != nil {
|
||||
return errors.New("inner UUID invalid: " + err.Error())
|
||||
err = validateUUID(m.pbInner.GetUuid())
|
||||
if err != nil {
|
||||
return fmt.Errorf("inner UUID invalid: %w", err)
|
||||
}
|
||||
|
||||
// Verify UUIDs match
|
||||
if !bytes.Equal(m.pbOuter.Uuid, m.pbInner.Uuid) {
|
||||
return errors.New("outer and inner UUID mismatch")
|
||||
if !bytes.Equal(m.pbOuter.GetUuid(), m.pbInner.GetUuid()) {
|
||||
return errUUIDMismatch
|
||||
}
|
||||
|
||||
log.Infof("loaded manifest with %d files", len(m.pbInner.Files))
|
||||
log.Infof("loaded manifest with %d files", len(m.pbInner.GetFiles()))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -122,20 +191,26 @@ func validateMagic(dat []byte) bool {
|
||||
if len(dat) < ml {
|
||||
return false
|
||||
}
|
||||
|
||||
got := dat[0:ml]
|
||||
expected := []byte(MAGIC)
|
||||
|
||||
return bytes.Equal(got, expected)
|
||||
}
|
||||
|
||||
// NewManifestFromReader reads a manifest from an io.Reader.
|
||||
//
|
||||
//nolint:revive // unexported-return: exporting manifest is owner question 13
|
||||
func NewManifestFromReader(input io.Reader) (*manifest, error) {
|
||||
m := &manifest{}
|
||||
|
||||
dat, err := io.ReadAll(input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !validateMagic(dat) {
|
||||
return nil, errors.New("invalid file format")
|
||||
return nil, errInvalidFileFormat
|
||||
}
|
||||
|
||||
// remove magic bytes prefix:
|
||||
@@ -145,12 +220,15 @@ func NewManifestFromReader(input io.Reader) (*manifest, error) {
|
||||
|
||||
// deserialize outer:
|
||||
m.pbOuter = new(MFFileOuter)
|
||||
if err := proto.Unmarshal(dat, m.pbOuter); err != nil {
|
||||
|
||||
err = proto.Unmarshal(dat, m.pbOuter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// deserialize inner:
|
||||
if err := m.deserializeInner(); err != nil {
|
||||
err = m.deserializeInner()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -159,14 +237,19 @@ func NewManifestFromReader(input io.Reader) (*manifest, error) {
|
||||
|
||||
// NewManifestFromFile reads a manifest from a file path using the given filesystem.
|
||||
// If fs is nil, the real filesystem (OsFs) is used.
|
||||
//
|
||||
//nolint:revive // unexported-return: exporting manifest is owner question 13
|
||||
func NewManifestFromFile(fs afero.Fs, path string) (*manifest, error) {
|
||||
if fs == nil {
|
||||
fs = afero.NewOsFs()
|
||||
}
|
||||
|
||||
f, err := fs.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
return NewManifestFromReader(f)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user