time.UnixNano() panics for dates outside ~1678-2262. The Nanosecond() method returns just the nanosecond component (0-999999999) and is safe for all dates. This matches the pattern already used in ModTime.Timestamp().
119 lines
2.7 KiB
Go
119 lines
2.7 KiB
Go
package mfer
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"errors"
|
|
"fmt"
|
|
"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"
|
|
|
|
func newTimestampFromTime(t time.Time) *Timestamp {
|
|
return &Timestamp{
|
|
Seconds: t.Unix(),
|
|
Nanos: int32(t.Nanosecond()),
|
|
}
|
|
}
|
|
|
|
func (m *manifest) generate() error {
|
|
if m.pbInner == nil {
|
|
return errors.New("internal error: pbInner not set")
|
|
}
|
|
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 err
|
|
}
|
|
m.output = bytes.NewBuffer([]byte(MAGIC))
|
|
_, err = m.output.Write(dat)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *manifest) generateOuter() error {
|
|
if m.pbInner == nil {
|
|
return errors.New("internal error")
|
|
}
|
|
|
|
// Generate UUID and set on inner message
|
|
manifestUUID := uuid.New()
|
|
m.pbInner.Uuid = manifestUUID[:]
|
|
|
|
innerData, err := proto.MarshalOptions{Deterministic: true}.Marshal(m.pbInner)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Compress the inner data
|
|
idc := new(bytes.Buffer)
|
|
zw, err := zstd.NewWriter(idc, zstd.WithEncoderLevel(zstd.SpeedBestCompression))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = zw.Write(innerData)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_ = zw.Close()
|
|
|
|
compressedData := idc.Bytes()
|
|
|
|
// Hash the compressed data for integrity verification before decompression
|
|
h := sha256.New()
|
|
if _, err := h.Write(compressedData); err != nil {
|
|
return 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 != "" {
|
|
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
|
|
}
|