Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24496880c5 |
@@ -17,15 +17,6 @@ The current version is pre-1.0 and while the repo was published in 2022, there
|
||||
has not yet been any versioned release. [SemVer](https://semver.org) will be
|
||||
used for releases.
|
||||
|
||||
A version is a git tag, and no file in the tree records a version number.
|
||||
`mfer version` prints the revision the build stamped into the binary, which for
|
||||
`make build` and the Docker image is the output of `git describe` on the
|
||||
checkout they build. A binary built without that stamp, as
|
||||
`go install sneak.berlin/go/mfer/cmd/mfer@<tag>` builds it, prints the module
|
||||
version Go records in it instead. A program that imports the `mfer` package
|
||||
finds the version it was built against in its own build information
|
||||
(`runtime/debug.ReadBuildInfo`).
|
||||
|
||||
This project was started by [@sneak](https://sneak.berlin) to scratch an itch in
|
||||
2022 and is currently a one-person effort, though the goal is for this to emerge
|
||||
as a de-facto standard and be incorporated into other software. A compatible
|
||||
|
||||
+6
-4
@@ -10,12 +10,14 @@ import (
|
||||
// Appname is the name of this program.
|
||||
const Appname = "mfer"
|
||||
|
||||
// Gitrev is the revision `mfer version` prints, injected at build time via
|
||||
// -ldflags.
|
||||
// Version and Gitrev are injected at build time via -ldflags.
|
||||
//
|
||||
//nolint:gochecknoglobals // set via ldflags at build time
|
||||
var Gitrev string
|
||||
var (
|
||||
Version string
|
||||
Gitrev string
|
||||
)
|
||||
|
||||
func main() {
|
||||
os.Exit(cli.Run(Appname, Gitrev))
|
||||
os.Exit(cli.Run(Appname, Version, Gitrev))
|
||||
}
|
||||
|
||||
@@ -15,10 +15,11 @@ func TestAppname(t *testing.T) {
|
||||
assert.Equal(t, "mfer", Appname)
|
||||
}
|
||||
|
||||
// TestGitrevDefault documents that Gitrev is empty unless injected at build
|
||||
// time via -ldflags.
|
||||
func TestGitrevDefault(t *testing.T) {
|
||||
// TestVersionDefaults documents that Version and Gitrev are empty unless
|
||||
// injected at build time via -ldflags.
|
||||
func TestVersionDefaults(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert.Empty(t, Version)
|
||||
assert.Empty(t, Gitrev)
|
||||
}
|
||||
|
||||
@@ -174,6 +174,10 @@ also refuses a manifest whose `signingPubKey` holds a DSA key or subkey, or any
|
||||
secret key or subkey, since checking the self-signatures of a DSA key or the
|
||||
numbers of a secret key can take hours when those numbers are very large.
|
||||
|
||||
A signing key that has expired does not make the signature bad, because a
|
||||
manifest outlives its key. A signature past its own expiry time is not good,
|
||||
whether or not the key has expired too.
|
||||
|
||||
## Deterministic Serialization
|
||||
|
||||
By default, manifests are generated deterministically:
|
||||
|
||||
@@ -26,7 +26,8 @@ func noColorEnvSet() bool {
|
||||
// Use DefaultRunOptions for standard CLI execution, or construct manually for testing.
|
||||
type RunOptions struct {
|
||||
Appname string // Application name displayed in help and version output
|
||||
Gitrev string // Revision stamped in at build time, or empty
|
||||
Version string // Version string (typically set at build time)
|
||||
Gitrev string // Git revision hash (typically set at build time)
|
||||
Args []string // Command-line arguments (typically os.Args)
|
||||
Stdin io.Reader // Standard input stream
|
||||
Stdout io.Writer // Standard output stream
|
||||
@@ -35,9 +36,10 @@ type RunOptions struct {
|
||||
}
|
||||
|
||||
// DefaultRunOptions returns RunOptions configured for normal CLI execution.
|
||||
func DefaultRunOptions(appname, gitrev string) *RunOptions {
|
||||
func DefaultRunOptions(appname, version, gitrev string) *RunOptions {
|
||||
return &RunOptions{
|
||||
Appname: appname,
|
||||
Version: version,
|
||||
Gitrev: gitrev,
|
||||
Args: os.Args,
|
||||
Stdin: os.Stdin,
|
||||
@@ -48,14 +50,15 @@ func DefaultRunOptions(appname, gitrev string) *RunOptions {
|
||||
}
|
||||
|
||||
// Run creates and runs the CLI application with default options.
|
||||
func Run(appname, gitrev string) int {
|
||||
return RunWithOptions(DefaultRunOptions(appname, gitrev))
|
||||
func Run(appname, version, gitrev string) int {
|
||||
return RunWithOptions(DefaultRunOptions(appname, version, gitrev))
|
||||
}
|
||||
|
||||
// RunWithOptions creates and runs the CLI application with the given options.
|
||||
func RunWithOptions(opts *RunOptions) int {
|
||||
m := &CLIApp{
|
||||
appname: opts.Appname,
|
||||
version: opts.Version,
|
||||
gitrev: opts.Gitrev,
|
||||
exitCode: 0,
|
||||
maxManifestSize: mfer.MaxManifestSize,
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime/debug"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -28,7 +27,6 @@ import (
|
||||
|
||||
const (
|
||||
testApp = "mfer"
|
||||
testGitrev = "abc123"
|
||||
testDir = "/testdir"
|
||||
testFile1 = "/testdir/file1.txt"
|
||||
testMF = "/testdir/test.mf"
|
||||
@@ -95,7 +93,8 @@ func TestBuild(t *testing.T) {
|
||||
func testOpts(args []string, fs afero.Fs) *RunOptions {
|
||||
return &RunOptions{
|
||||
Appname: testApp,
|
||||
Gitrev: testGitrev,
|
||||
Version: "1.0.0",
|
||||
Gitrev: "abc123",
|
||||
Args: args,
|
||||
Stdin: &bytes.Buffer{},
|
||||
Stdout: &bytes.Buffer{},
|
||||
@@ -137,24 +136,10 @@ func TestVersionCommand(t *testing.T) {
|
||||
exitCode := runCLI(opts)
|
||||
|
||||
assert.Equal(t, 0, exitCode)
|
||||
assert.Equal(t, "mfer version "+testGitrev+"\n", testStdout(t, opts))
|
||||
}
|
||||
|
||||
// TestVersionFromBuildInfo covers a binary whose build stamped no Gitrev,
|
||||
// as `go install` builds it: mfer version prints the main module's version
|
||||
// from the build information instead.
|
||||
func TestVersionFromBuildInfo(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
info, ok := debug.ReadBuildInfo()
|
||||
require.True(t, ok)
|
||||
require.NotEmpty(t, info.Main.Version)
|
||||
|
||||
opts := testOpts([]string{testApp, cmdVersion}, afero.NewMemMapFs())
|
||||
opts.Gitrev = ""
|
||||
|
||||
require.Equal(t, 0, runCLI(opts))
|
||||
assert.Equal(t, "mfer version "+info.Main.Version+"\n", testStdout(t, opts))
|
||||
stdout := testStdout(t, opts)
|
||||
assert.Contains(t, stdout, mfer.Version)
|
||||
assert.Contains(t, stdout, "abc123")
|
||||
}
|
||||
|
||||
// TestVFlagCollision covers the -v/--verbose vs --version flag interaction
|
||||
@@ -180,7 +165,7 @@ func TestVFlagCollision(t *testing.T) {
|
||||
exitCode := runCLI(opts)
|
||||
|
||||
assert.Equal(t, 0, exitCode, "stderr: %s", testStderr(t, opts))
|
||||
assert.Contains(t, testStdout(t, opts), testGitrev)
|
||||
assert.Contains(t, testStdout(t, opts), mfer.Version)
|
||||
assert.NotContains(t, testStderr(t, opts), "two forms of the same flag")
|
||||
})
|
||||
}
|
||||
@@ -754,9 +739,6 @@ func TestBannerOutput(t *testing.T) {
|
||||
stdout := testStdout(t, opts)
|
||||
assert.Contains(t, stdout, "___")
|
||||
assert.Contains(t, stdout, "\\")
|
||||
|
||||
// The banner shows the version mfer version prints, and nothing after it.
|
||||
assert.Contains(t, stdout, "mfer by @sneak: "+testGitrev+"\n")
|
||||
}
|
||||
|
||||
func TestUnknownCommand(t *testing.T) {
|
||||
|
||||
+7
-12
@@ -6,12 +6,12 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
"github.com/urfave/cli/v3"
|
||||
"sneak.berlin/go/mfer/internal/log"
|
||||
"sneak.berlin/go/mfer/mfer"
|
||||
)
|
||||
|
||||
// Command and flag names shared across command definitions and tests.
|
||||
@@ -58,6 +58,7 @@ var errUnexpectedArgument = errors.New("unexpected argument")
|
||||
//nolint:revive // established name used throughout the codebase and tests
|
||||
type CLIApp struct {
|
||||
appname string
|
||||
version string
|
||||
gitrev string
|
||||
startupTime time.Time
|
||||
exitCode int
|
||||
@@ -86,20 +87,13 @@ const banner = `
|
||||
\ \:\ \ \:\ \ \::/ \ \:\
|
||||
\__\/ \__\/ \__\/ \__\/`
|
||||
|
||||
// VersionString returns the version mfer reports: the revision the build
|
||||
// stamped into main.Gitrev or, when it stamped none, as with `go install`,
|
||||
// the main module's version from the binary's build information.
|
||||
// VersionString returns the version and git revision formatted for display.
|
||||
func (mfa *CLIApp) VersionString() string {
|
||||
if mfa.gitrev != "" {
|
||||
return mfa.gitrev
|
||||
return fmt.Sprintf("%s (%s)", mfer.Version, mfa.gitrev)
|
||||
}
|
||||
|
||||
info, ok := debug.ReadBuildInfo()
|
||||
if !ok {
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
return info.Main.Version
|
||||
return mfer.Version
|
||||
}
|
||||
|
||||
// printVersion writes the version line shared by the --version flag and the
|
||||
@@ -112,7 +106,8 @@ func (mfa *CLIApp) printBanner() {
|
||||
if log.GetLevel() <= log.InfoLevel {
|
||||
_, _ = fmt.Fprintln(mfa.Stdout, banner)
|
||||
_, _ = fmt.Fprintf(mfa.Stdout,
|
||||
" mfer by @sneak: %s\n", mfa.VersionString())
|
||||
" mfer by @sneak: v%s released %s\n",
|
||||
mfer.Version, mfer.ReleaseDate)
|
||||
_, _ = fmt.Fprintln(mfa.Stdout, " https://sneak.berlin/go/mfer")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
package mfer
|
||||
|
||||
const (
|
||||
// Version is the current mfer release version.
|
||||
Version = "0.1.0"
|
||||
|
||||
// ReleaseDate is the date on which Version was released.
|
||||
ReleaseDate = "2025-12-17"
|
||||
|
||||
// MaxDecompressedSize is the maximum allowed size of decompressed manifest
|
||||
// data (256 MiB). This prevents decompression bombs from consuming excessive
|
||||
// memory.
|
||||
|
||||
+13
-6
@@ -8,6 +8,7 @@ import (
|
||||
"io"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/go-crypto/openpgp"
|
||||
"github.com/ProtonMail/go-crypto/openpgp/armor"
|
||||
@@ -242,10 +243,10 @@ func fingerprint(key *openpgp.Entity) string {
|
||||
return strings.ToUpper(hex.EncodeToString(key.PrimaryKey.Fingerprint))
|
||||
}
|
||||
|
||||
// verifySignature checks that signature is one good OpenPGP signature
|
||||
// over data, made by the one primary key in pubKey or one of its subkeys,
|
||||
// and returns that primary key's fingerprint. signature and pubKey may each
|
||||
// be armored or binary.
|
||||
// verifySignature checks that signature is one good, unexpired OpenPGP
|
||||
// signature over data, made by the one primary key in pubKey or one of its
|
||||
// subkeys, and returns that primary key's fingerprint. signature and pubKey
|
||||
// may each be armored or binary.
|
||||
func verifySignature(data, signature, pubKey []byte) (string, error) {
|
||||
key, err := readOneKey(pubKey, "embedded public key block", true)
|
||||
if err != nil {
|
||||
@@ -266,10 +267,16 @@ func verifySignature(data, signature, pubKey []byte) (string, error) {
|
||||
return "", fmt.Errorf("%w, found %d", errNotOneSignature, sigs)
|
||||
}
|
||||
|
||||
_, err = openpgp.CheckDetachedSignature(openpgp.EntityList{key},
|
||||
sig, _, err := openpgp.VerifyDetachedSignature(openpgp.EntityList{key},
|
||||
bytes.NewReader(data), bytes.NewReader(sigData), nil)
|
||||
// A manifest outlives its signing key, so a signature by a key that
|
||||
// has expired since is still good.
|
||||
// has expired since is still good, unless the signature has expired
|
||||
// itself. The library does not check that once it finds the key
|
||||
// expired.
|
||||
if errors.Is(err, pgperrors.ErrKeyExpired) && sig.SigExpired(time.Now()) {
|
||||
err = pgperrors.ErrSignatureExpired
|
||||
}
|
||||
|
||||
if err != nil && !errors.Is(err, pgperrors.ErrKeyExpired) {
|
||||
return "", fmt.Errorf("verify signature: %w", err)
|
||||
}
|
||||
|
||||
+14
-3
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/ProtonMail/go-crypto/openpgp"
|
||||
"github.com/ProtonMail/go-crypto/openpgp/armor"
|
||||
"github.com/ProtonMail/go-crypto/openpgp/elgamal"
|
||||
pgperrors "github.com/ProtonMail/go-crypto/openpgp/errors"
|
||||
"github.com/ProtonMail/go-crypto/openpgp/packet"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -416,8 +417,10 @@ func TestVerifySignature(t *testing.T) {
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
// TestVerifySignatureKeyExpiredSince verifies a signature made in 2020 by
|
||||
// a key that expired a day after it was made. The signature is still good.
|
||||
// TestVerifySignatureKeyExpiredSince verifies two signatures made in 2020
|
||||
// by a key that expired a day after it was made. A signature with no
|
||||
// expiry time is still good. One that expired an hour after it was made is
|
||||
// refused, although the key has expired too.
|
||||
func TestVerifySignatureKeyExpiredSince(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -429,9 +432,14 @@ func TestVerifySignatureKeyExpiredSince(t *testing.T) {
|
||||
key := newTestKey(t, config)
|
||||
data := []byte("signed in 2020")
|
||||
|
||||
var sig bytes.Buffer
|
||||
var sig, expiringSig bytes.Buffer
|
||||
|
||||
require.NoError(t, openpgp.ArmoredDetachSign(&sig, key, bytes.NewReader(data), config))
|
||||
require.NoError(t, openpgp.ArmoredDetachSign(&expiringSig, key,
|
||||
bytes.NewReader(data), &packet.Config{
|
||||
Time: config.Time,
|
||||
SigLifetimeSecs: uint32(time.Hour.Seconds()),
|
||||
}))
|
||||
|
||||
pubKey, err := armoredPublicKey(key)
|
||||
require.NoError(t, err)
|
||||
@@ -439,6 +447,9 @@ func TestVerifySignatureKeyExpiredSince(t *testing.T) {
|
||||
signer, err := verifySignature(data, sig.Bytes(), pubKey)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fingerprint(key), signer)
|
||||
|
||||
_, err = verifySignature(data, expiringSig.Bytes(), pubKey)
|
||||
require.ErrorIs(t, err, pgperrors.ErrSignatureExpired)
|
||||
}
|
||||
|
||||
func TestManifestSignatureVerification(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user