Files
mfer/mfer/serialize.go
sneak 82b31c7d23
All checks were successful
check / check (push) Successful in 39s
Update golangci-lint to v2.12.2 with canonical config
- Add canonical .golangci.yml (v2 schema, default: all, project
  thresholds for lll/funlen/cyclop/dupl)
- Bump golangci-lint pins from v2.0.2 to v2.12.2 in Makefile
  (go install, new /v2 module path) and Dockerfile (tagged+digest
  Debian image pin)
- Fix all lint findings surfaced by the new linter set across
  cmd/mfer, internal/bork, internal/cli, internal/log, and mfer:
  static sentinel errors (err113), context-aware HTTP and exec
  (noctx), guarded integer conversions and stricter permissions
  (gosec), named constants (mnd, goconst), function decomposition
  (funlen, cyclop, gocognit, nestif), declaration ordering
  (funcorder), t.Parallel/t.TempDir/t.Setenv adoption in tests
  (paralleltest, usetesting), protobuf getters (protogetter), plus
  formatting and style cleanups (wsl_v5, nlreturn, lll, revive,
  testifylint, and others)
- Serialize CLI runs in tests behind a mutex so parallel tests do
  not cross-wire the process-global logger's captured output
2026-08-07 17:07:44 +00:00

162 lines
3.6 KiB
Go

package mfer
import (
"bytes"
"crypto/sha256"
"errors"
"fmt"
"math"
"time"
"github.com/google/uuid"
"github.com/klauspost/compress/zstd"
"google.golang.org/protobuf/proto"
)
// MAGIC is the file format magic bytes prefix (rot13 of "MANIFEST").
const MAGIC string = "ZNAVSRFG"
var errInnerNotSet = errors.New("internal error: pbInner not set")
// nanosecondsInt32 converts t's nanosecond component to int32.
// time.Time.Nanosecond is documented to return a value in [0, 999999999],
// so the conversion cannot overflow; the guard makes that explicit.
func nanosecondsInt32(t time.Time) int32 {
n := t.Nanosecond()
if n < 0 || n > math.MaxInt32 {
return 0
}
return int32(n)
}
func newTimestampFromTime(t time.Time) *Timestamp {
return &Timestamp{
Seconds: t.Unix(),
Nanos: nanosecondsInt32(t),
}
}
func (m *Manifest) generate() error {
if m.pbInner == nil {
return errInnerNotSet
}
if m.pbOuter == nil {
e := m.generateOuter()
if e != nil {
return e
}
}
dat, err := proto.MarshalOptions{Deterministic: true}.Marshal(m.pbOuter)
if err != nil {
return fmt.Errorf("serialize: marshal outer: %w", err)
}
m.output = bytes.NewBufferString(MAGIC)
_, err = m.output.Write(dat)
if err != nil {
return fmt.Errorf("serialize: write output: %w", err)
}
return nil
}
func (m *Manifest) generateOuter() error {
if m.pbInner == nil {
return errInnerNotSet
}
// Use fixed UUID if provided, otherwise generate a new one
var manifestUUID uuid.UUID
if len(m.fixedUUID) == uuidLength {
copy(manifestUUID[:], m.fixedUUID)
} else {
manifestUUID = uuid.New()
}
m.pbInner.Uuid = manifestUUID[:]
innerData, err := proto.MarshalOptions{Deterministic: true}.Marshal(m.pbInner)
if err != nil {
return fmt.Errorf("serialize: marshal inner: %w", err)
}
// Compress the inner data
idc := new(bytes.Buffer)
zw, err := zstd.NewWriter(idc, zstd.WithEncoderLevel(zstd.SpeedBestCompression))
if err != nil {
return fmt.Errorf("serialize: create compressor: %w", err)
}
_, err = zw.Write(innerData)
if err != nil {
return fmt.Errorf("serialize: compress: %w", err)
}
_ = zw.Close()
compressedData := idc.Bytes()
// Hash the compressed data for integrity verification before decompression
h := sha256.New()
_, err = h.Write(compressedData)
if err != nil {
return fmt.Errorf("serialize: hash write: %w", err)
}
sha256Hash := h.Sum(nil)
m.pbOuter = &MFFileOuter{
InnerMessage: compressedData,
Size: int64(len(innerData)),
Sha256: sha256Hash,
Uuid: manifestUUID[:],
Version: MFFileOuter_VERSION_ONE,
CompressionType: MFFileOuter_COMPRESSION_ZSTD,
}
// Sign the manifest if signing options are provided
if m.signingOptions != nil && m.signingOptions.KeyID != "" {
return m.signOuter()
}
return nil
}
// signOuter signs the outer message with the configured GPG key and
// embeds the signature, signer fingerprint, and public key.
func (m *Manifest) signOuter() error {
sigString, err := m.signatureString()
if err != nil {
return fmt.Errorf("failed to generate signature string: %w", err)
}
sig, err := gpgSign([]byte(sigString), m.signingOptions.KeyID)
if err != nil {
return fmt.Errorf("failed to sign manifest: %w", err)
}
m.pbOuter.Signature = sig
fingerprint, err := gpgGetKeyFingerprint(m.signingOptions.KeyID)
if err != nil {
return fmt.Errorf("failed to get key fingerprint: %w", err)
}
m.pbOuter.Signer = fingerprint
pubKey, err := gpgExportPublicKey(m.signingOptions.KeyID)
if err != nil {
return fmt.Errorf("failed to export public key: %w", err)
}
m.pbOuter.SigningPubKey = pubKey
return nil
}