mirror of
https://github.com/JanDeDobbeleer/oh-my-posh.git
synced 2025-02-21 02:55:37 -08:00
feat(install): validate file signature before installation
This commit is contained in:
parent
0829afb478
commit
b6729ff414
|
@ -2,9 +2,11 @@ package cli
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
stdruntime "runtime"
|
||||
"slices"
|
||||
|
||||
"github.com/jandedobbeleer/oh-my-posh/src/build"
|
||||
"github.com/jandedobbeleer/oh-my-posh/src/config"
|
||||
"github.com/jandedobbeleer/oh-my-posh/src/runtime"
|
||||
"github.com/jandedobbeleer/oh-my-posh/src/terminal"
|
||||
|
@ -32,9 +34,7 @@ var upgradeCmd = &cobra.Command{
|
|||
return
|
||||
}
|
||||
|
||||
env := &runtime.Terminal{
|
||||
CmdFlags: &runtime.Flags{},
|
||||
}
|
||||
env := &runtime.Terminal{}
|
||||
env.Init()
|
||||
defer env.Close()
|
||||
|
||||
|
@ -43,24 +43,43 @@ var upgradeCmd = &cobra.Command{
|
|||
|
||||
defer fmt.Print(terminal.StopProgress())
|
||||
|
||||
latest, err := upgrade.Latest(env)
|
||||
if err != nil {
|
||||
fmt.Printf("\n❌ %s\n\n", err)
|
||||
os.Exit(1)
|
||||
return
|
||||
}
|
||||
|
||||
if force {
|
||||
upgrade.Run(env)
|
||||
executeUpgrade(latest)
|
||||
return
|
||||
}
|
||||
|
||||
cfg := config.Load(env)
|
||||
|
||||
if _, hasNotice := upgrade.Notice(env, true); !hasNotice {
|
||||
version := fmt.Sprintf("v%s", build.Version)
|
||||
|
||||
if version == latest {
|
||||
if !cfg.DisableNotice {
|
||||
fmt.Print("\n✅ no new version available\n\n")
|
||||
fmt.Print("\n✅ no new version available\n\n")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
upgrade.Run(env)
|
||||
executeUpgrade(latest)
|
||||
},
|
||||
}
|
||||
|
||||
func executeUpgrade(latest string) {
|
||||
err := upgrade.Run(latest)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("\n❌ %s\n\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func init() {
|
||||
upgradeCmd.Flags().BoolVarP(&force, "force", "f", false, "force the upgrade even if the version is up to date")
|
||||
RootCmd.AddCommand(upgradeCmd)
|
||||
|
|
1
src/test/signing/checksums.txt
Normal file
1
src/test/signing/checksums.txt
Normal file
|
@ -0,0 +1 @@
|
|||
4ad2e70db23f6c0441df61a07370d352081e93fab32867e797d6dfa3f45814ce posh-windows-arm64.exe
|
4
src/test/signing/checksums.txt.invalid.sig
Normal file
4
src/test/signing/checksums.txt.invalid.sig
Normal file
|
@ -0,0 +1,4 @@
|
|||
<EFBFBD>Ίv<EFBFBD>3.*
|
||||
ςΖΐ«¦k”WbJ.ό΄ΚsGueeΏ§5σΰά¨[lΦτ<CEA6>UΌ—{γοΨ…ΑR‡-Ν…υX‘η‡_η‚ΙΫEο7`£ί5Σ}—-<2D>W”.Q³S"ΨΒ} «C]³<>'FΒZΫΈPΧhYΚ©Ι―δ©7n<37>πΐDτ0γlΈ8<CE88>‘O><3E>}•.<2E><, ς<EFBFBD>†½·<C2BD>«ο–+<2B>»%ΒFPΎ<50>
|
||||
¨Ό ±^‹ξ
|
||||
‡KΡ"Z=mx8pΰy―08[―|z&y‚δΡξ<14>Ψlηu<CEB7>Q$„@Τ‚½£¶χ>„τςατ'νΰnΝW8«Ξ‹M½.¦ΐρtΦ¦
|
1
src/test/signing/checksums.txt.sig
Normal file
1
src/test/signing/checksums.txt.sig
Normal file
|
@ -0,0 +1 @@
|
|||
œ3Jà¦ÝSDòÊ»9IbêfC(ñ6c¬œªŒUž®F‘¿–Ý,îoš«g«ïPåHÊ
Ü‹i†ØÕùfèF‡‘_ˆ¥
|
|
@ -2,13 +2,11 @@ package upgrade
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/charmbracelet/bubbles/spinner"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/jandedobbeleer/oh-my-posh/src/build"
|
||||
"github.com/jandedobbeleer/oh-my-posh/src/runtime"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -24,10 +22,15 @@ type state int
|
|||
const (
|
||||
validating state = iota
|
||||
downloading
|
||||
verifying
|
||||
installing
|
||||
)
|
||||
|
||||
func setState(message state) {
|
||||
if program == nil {
|
||||
return
|
||||
}
|
||||
|
||||
program.Send(stateMsg(message))
|
||||
}
|
||||
|
||||
|
@ -37,25 +40,28 @@ type model struct {
|
|||
spinner spinner.Model
|
||||
message string
|
||||
state state
|
||||
error error
|
||||
|
||||
tag string
|
||||
}
|
||||
|
||||
func initialModel() *model {
|
||||
func initialModel(tag string) *model {
|
||||
s := spinner.New()
|
||||
s.Spinner = spinner.Dot
|
||||
s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("170"))
|
||||
return &model{spinner: s}
|
||||
return &model{spinner: s, tag: tag}
|
||||
}
|
||||
|
||||
func (m *model) Init() tea.Cmd {
|
||||
defer func() {
|
||||
go func() {
|
||||
if err := install(); err != nil {
|
||||
message := fmt.Sprintf("⚠️ %s", err)
|
||||
program.Send(resultMsg(message))
|
||||
if err := install(m.tag); err != nil {
|
||||
m.error = err
|
||||
program.Quit()
|
||||
return
|
||||
}
|
||||
|
||||
program.Send(resultMsg("🚀 Upgrade successful, restart your shell to take full advantage of the new functionality."))
|
||||
program.Send(resultMsg("🚀 Upgrade successful, restart your shell to take full advantage of the new functionality."))
|
||||
}()
|
||||
}()
|
||||
|
||||
|
@ -101,6 +107,9 @@ func (m *model) View() string {
|
|||
case downloading:
|
||||
m.spinner.Spinner = spinner.Globe
|
||||
message = "Downloading latest version"
|
||||
case verifying:
|
||||
m.spinner.Spinner = spinner.Moon
|
||||
message = "Verifying download"
|
||||
case installing:
|
||||
message = "Installing"
|
||||
}
|
||||
|
@ -108,20 +117,29 @@ func (m *model) View() string {
|
|||
return title + textStyle.Render(fmt.Sprintf("%s %s", m.spinner.View(), message))
|
||||
}
|
||||
|
||||
func Run(env runtime.Environment) {
|
||||
func Run(latest string) error {
|
||||
titleStyle := lipgloss.NewStyle().Margin(1, 0, 1, 0)
|
||||
title = "📦 Upgrading Oh My Posh"
|
||||
title = "📦 Upgrading Oh My Posh"
|
||||
|
||||
version, err := Latest(env)
|
||||
if err == nil {
|
||||
title = fmt.Sprintf("%s from %s to %s", title, build.Version, version)
|
||||
current := build.Version
|
||||
if len(current) == 0 {
|
||||
current = "dev"
|
||||
}
|
||||
|
||||
title = fmt.Sprintf("%s from %s to %s", title, current, latest)
|
||||
title = titleStyle.Render(title)
|
||||
|
||||
program = tea.NewProgram(initialModel())
|
||||
if _, err := program.Run(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
program = tea.NewProgram(initialModel(latest))
|
||||
resultModel, err := program.Run()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
programModel, OK := resultModel.(*model)
|
||||
if !OK {
|
||||
return nil
|
||||
}
|
||||
|
||||
return programModel.error
|
||||
}
|
||||
|
|
39
src/upgrade/github.go
Normal file
39
src/upgrade/github.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
package upgrade
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
httplib "net/http"
|
||||
|
||||
"github.com/jandedobbeleer/oh-my-posh/src/runtime/http"
|
||||
)
|
||||
|
||||
func downloadReleaseAsset(tag, asset string) ([]byte, error) {
|
||||
url := fmt.Sprintf("https://github.com/JanDeDobbeleer/oh-my-posh/releases/download/%s/%s", tag, asset)
|
||||
|
||||
req, err := httplib.NewRequestWithContext(context.Background(), "GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Add("User-Agent", "oh-my-posh")
|
||||
|
||||
resp, err := http.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != httplib.StatusOK {
|
||||
return nil, fmt.Errorf("failed to download asset: %s", url)
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
|
@ -2,19 +2,13 @@ package upgrade
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
httplib "net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
stdruntime "runtime"
|
||||
|
||||
"github.com/jandedobbeleer/oh-my-posh/src/runtime"
|
||||
"github.com/jandedobbeleer/oh-my-posh/src/runtime/http"
|
||||
)
|
||||
|
||||
func install() error {
|
||||
func install(tag string) error {
|
||||
setState(validating)
|
||||
|
||||
executable, err := os.Executable()
|
||||
|
@ -22,34 +16,9 @@ func install() error {
|
|||
return err
|
||||
}
|
||||
|
||||
extension := ""
|
||||
if stdruntime.GOOS == runtime.WINDOWS {
|
||||
extension = ".exe"
|
||||
}
|
||||
|
||||
asset := fmt.Sprintf("posh-%s-%s%s", stdruntime.GOOS, stdruntime.GOARCH, extension)
|
||||
|
||||
setState(downloading)
|
||||
|
||||
url := fmt.Sprintf("https://github.com/JanDeDobbeleer/oh-my-posh/releases/latest/download/%s", asset)
|
||||
|
||||
req, err := httplib.NewRequestWithContext(context.Background(), "GET", url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := http.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.StatusCode != httplib.StatusOK {
|
||||
return fmt.Errorf("failed to download installer: %s", url)
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
newBytes, err := io.ReadAll(resp.Body)
|
||||
data, err := downloadAndVerify(tag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -65,7 +34,7 @@ func install() error {
|
|||
|
||||
defer fp.Close()
|
||||
|
||||
_, err = io.Copy(fp, bytes.NewReader(newBytes))
|
||||
_, err = io.Copy(fp, bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -49,6 +49,11 @@ func Latest(env runtime.Environment) (string, error) {
|
|||
var release release
|
||||
// this can't fail
|
||||
_ = json.Unmarshal(body, &release)
|
||||
|
||||
if len(release.TagName) == 0 {
|
||||
return "", fmt.Errorf("failed to get latest release")
|
||||
}
|
||||
|
||||
return release.TagName, nil
|
||||
}
|
||||
|
||||
|
|
3
src/upgrade/public_key.pem
Normal file
3
src/upgrade/public_key.pem
Normal file
|
@ -0,0 +1,3 @@
|
|||
-----BEGIN PUBLIC KEY-----
|
||||
MCowBQYDK2VwAyEA98lHhNau5x0JtjSuwiWLuC2yKO6NA6/0bH2gE8tAq4c=
|
||||
-----END PUBLIC KEY-----
|
114
src/upgrade/verify.go
Normal file
114
src/upgrade/verify.go
Normal file
|
@ -0,0 +1,114 @@
|
|||
package upgrade
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
_ "embed"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
stdruntime "runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/jandedobbeleer/oh-my-posh/src/runtime"
|
||||
)
|
||||
|
||||
//go:embed public_key.pem
|
||||
var publicKey []byte
|
||||
|
||||
func downloadAndVerify(tag string) ([]byte, error) {
|
||||
extension := ""
|
||||
if stdruntime.GOOS == runtime.WINDOWS {
|
||||
extension = ".exe"
|
||||
}
|
||||
|
||||
asset := fmt.Sprintf("posh-%s-%s%s", stdruntime.GOOS, stdruntime.GOARCH, extension)
|
||||
|
||||
data, err := downloadReleaseAsset(tag, asset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
setState(verifying)
|
||||
|
||||
err = verify(tag, asset, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func verify(tag, asset string, binary []byte) error {
|
||||
checksums, err := downloadReleaseAsset(tag, "checksums.txt")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
signature, err := downloadReleaseAsset(tag, "checksums.txt.sig")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
OK := validateSignature(checksums, signature)
|
||||
if !OK {
|
||||
return fmt.Errorf("failed to verify checksums signature")
|
||||
}
|
||||
|
||||
return validateChecksum(asset, checksums, binary)
|
||||
}
|
||||
|
||||
func validateSignature(data, signature []byte) bool {
|
||||
ed25519PublicKey, err := loadPublicKey()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return ed25519.Verify(*ed25519PublicKey, data, signature)
|
||||
}
|
||||
|
||||
func loadPublicKey() (*ed25519.PublicKey, error) {
|
||||
block, _ := pem.Decode(publicKey)
|
||||
if block == nil {
|
||||
return nil, fmt.Errorf("error parsing PEM block: key not found")
|
||||
}
|
||||
|
||||
pubKey, err := x509.ParsePKIXPublicKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing public key: %v", err)
|
||||
}
|
||||
|
||||
ed25519PubKey, ok := pubKey.(ed25519.PublicKey)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid public key format: %v", err)
|
||||
}
|
||||
|
||||
return &ed25519PubKey, nil
|
||||
}
|
||||
|
||||
func validateChecksum(asset string, sha256sums, binary []byte) error {
|
||||
var assetChecksum string
|
||||
checksums := strings.Split(string(sha256sums), "\n")
|
||||
|
||||
for _, line := range checksums {
|
||||
if !strings.HasSuffix(line, asset) {
|
||||
continue
|
||||
}
|
||||
|
||||
assetChecksum = strings.Fields(line)[0]
|
||||
break
|
||||
}
|
||||
|
||||
if len(assetChecksum) == 0 {
|
||||
return fmt.Errorf("failed to find checksum for asset")
|
||||
}
|
||||
|
||||
// calculate the checksum of the binary
|
||||
binaryChecksum := fmt.Sprintf("%x", sha256.Sum256(binary))
|
||||
|
||||
if assetChecksum != binaryChecksum {
|
||||
return fmt.Errorf("checksum mismatch")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
30
src/upgrade/verify_test.go
Normal file
30
src/upgrade/verify_test.go
Normal file
|
@ -0,0 +1,30 @@
|
|||
package upgrade
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestVerify(t *testing.T) {
|
||||
checksum, err := os.ReadFile("../test/signing/checksums.txt")
|
||||
assert.NoError(t, err)
|
||||
|
||||
signature, err := os.ReadFile("../test/signing/checksums.txt.sig")
|
||||
assert.NoError(t, err)
|
||||
|
||||
OK := validateSignature(checksum, signature)
|
||||
assert.True(t, OK)
|
||||
}
|
||||
|
||||
func TestVerifyFail(t *testing.T) {
|
||||
checksum, err := os.ReadFile("../test/signing/checksums.txt")
|
||||
assert.NoError(t, err)
|
||||
|
||||
signature, err := os.ReadFile("../test/signing/checksums.txt.invalid.sig")
|
||||
assert.NoError(t, err)
|
||||
|
||||
OK := validateSignature(checksum, signature)
|
||||
assert.False(t, OK)
|
||||
}
|
Loading…
Reference in a new issue