arblant
firefox-vpn-client / core / internal / signing / signing.go
signing.go49 lines · 1.7 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
// Package signing checks the Ed25519 signatures of updates: the releases'
// SHA256SUMS.txt, which the Windows app checks before it installs a new
// version of itself (internal/update), and the TUN service's executable,
// which the installed service checks before it replaces itself
// (internal/tunservice). tools/sign makes the key and the signatures.
package signing

import (
	"crypto/ed25519"
	"encoding/base64"
	"errors"
	"strings"
)

// Key is the base64 Ed25519 public key that updates must be signed with,
// set by the build:
//
//	-ldflags "-X github.com/armanlife/firefox-vpn-client/core/internal/signing.Key=..."
//
// The release build uses the release key, whose private half only CI holds
// (the secret FIREFOX_VPN_SIGNING_KEY); a local build may use a developer
// key. A build without one takes no updates.
var Key string

// ErrNoKey means this build has no key, so it takes no updates.
var ErrNoKey = errors.New("this build has no update key, so it takes no updates")

// Available reports whether this build can check signatures.
func Available() bool { return Key != "" }

// Verify checks sig, a base64 signature as tools/sign writes it (one line),
// over data against Key.
func Verify(data, sig []byte) error {
	if Key == "" {
		return ErrNoKey
	}
	key, err := base64.StdEncoding.DecodeString(Key)
	if err != nil || len(key) != ed25519.PublicKeySize {
		return errors.New("the build's update key is broken")
	}
	raw, err := base64.StdEncoding.DecodeString(strings.TrimSpace(string(sig)))
	if err != nil || len(raw) != ed25519.SignatureSize {
		return errors.New("the signature is unreadable")
	}
	if !ed25519.Verify(ed25519.PublicKey(key), data, raw) {
		return errors.New("the signature does not match")
	}
	return nil
}