├── .github └── workflows │ ├── build.yml │ └── ci.yaml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── backend ├── backend.go ├── backend_test.go ├── file.go └── tpm.go ├── bundles.go ├── certs ├── builtin.go ├── certs.go ├── certs_test.go └── microsoft │ ├── KEK │ ├── MicCorKEKCA2011_2011-06-24.der │ └── microsoft corporation kek 2k ca 2023.der │ └── db │ ├── MicCorUEFCA2011_2011-06-27.der │ ├── MicWinProPCA2011_2011-10-19.der │ ├── microsoft option rom uefi ca 2023.der │ ├── microsoft uefi ca 2023.der │ └── windows uefi ca 2023.der ├── chattr.go ├── cmd └── sbctl │ ├── bundle.go │ ├── completions.go │ ├── create-keys.go │ ├── debug.go │ ├── enroll-keys.go │ ├── export-enrolled-keys.go │ ├── generate-bundles.go │ ├── import-keys.go │ ├── list-bundles.go │ ├── list-enrolled-keys.go │ ├── list-files.go │ ├── main.go │ ├── remove-bundle.go │ ├── remove-file.go │ ├── reset.go │ ├── rotate-keys.go │ ├── setup.go │ ├── setup_test.go │ ├── sign-all.go │ ├── sign.go │ ├── status.go │ ├── status_test.go │ ├── utils_test.go │ ├── verify.go │ └── version.go ├── config ├── config.go └── config_test.go ├── contrib ├── aur │ └── sbctl-git │ │ ├── .SRCINFO │ │ └── PKGBUILD ├── kernel-install │ └── 91-sbctl.install ├── mkinitcpio │ └── sbctl └── pacman │ └── ZZ-sbctl.hook ├── database.go ├── dmi ├── dmi.go └── dmi_test.go ├── docs ├── asciidoc.conf ├── sbctl.8.txt ├── sbctl.conf.5.txt ├── workflow-example-images │ ├── 01 - Boot Menu.png │ ├── 02 - Secure Boot Menu.png │ ├── 03 - Key Management Menu.png │ ├── 04 - Delete PK.png │ ├── 05 - Delete PK Confirmation.png │ ├── 06 - Keys Cleared.png │ ├── 07 - Secure Boot Disabled, PK Loaded.png │ ├── 08 - Secure Boot Disabled, PK Unloaded.png │ ├── 09 - Secure Boot Custom Keys.png │ └── 10 - Custom Keys.png └── workflow-example.md ├── fs └── fs.go ├── go.mod ├── go.sum ├── guid.go ├── hierarchy └── hierarchy.go ├── keys.go ├── logging └── logging.go ├── lsm └── lsm.go ├── quirks ├── fq0001.go └── quirks.go ├── sbctl.go ├── sbctl_test.go ├── siglist.go ├── stringset ├── stringset.go └── stringset_test.go ├── tests ├── README.md ├── binaries │ └── test.pecoff ├── bzImage ├── integration_test.go ├── integrations │ ├── enroll_keys │ │ └── enroll_keys_test.go │ ├── export_enrolled_keys │ │ └── export_enrolled_keys_test.go │ ├── list_enrolled_keys │ │ └── list_enrolled_keys_test.go │ └── secure_boot_enabled │ │ └── secure_boot_enabled_test.go ├── ovmf │ ├── OVMF_VARS.fd │ └── keys │ │ ├── KEK │ │ ├── KEK.auth │ │ ├── KEK.der │ │ ├── KEK.der.esl │ │ ├── KEK.key │ │ └── KEK.pem │ │ ├── PK │ │ ├── PK.auth │ │ ├── PK.der │ │ ├── PK.der.esl │ │ ├── PK.key │ │ └── PK.pem │ │ ├── db │ │ ├── db.auth │ │ ├── db.der │ │ ├── db.der.esl │ │ ├── db.key │ │ └── db.pem │ │ └── initramfs.cpio ├── shared │ └── test ├── tpm_eventlogs │ ├── t14_eventlog │ ├── t14s_eventlog │ └── t480s_eventlog └── utils │ └── utils.go ├── tpm.go ├── tpm_test.go └── util.go /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and upload binaries 2 | on: 3 | release: 4 | types: [published] 5 | push: 6 | pull_request: 7 | permissions: 8 | contents: read 9 | jobs: 10 | build: 11 | name: Build binaries 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | include: 16 | - {GOOS: linux, GOARCH: amd64} 17 | - {GOOS: linux, GOARCH: arm, GOARM: 6} 18 | - {GOOS: linux, GOARCH: arm64} 19 | steps: 20 | - name: Install Go 21 | uses: actions/setup-go@v2 22 | with: 23 | go-version: 1.x 24 | - name: Checkout repository 25 | uses: actions/checkout@v2 26 | with: 27 | fetch-depth: 0 28 | - name: Build binary 29 | run: | 30 | cp LICENSE "$RUNNER_TEMP/LICENSE" 31 | echo -e "\n---\n" >> "$RUNNER_TEMP/LICENSE" 32 | curl -L "https://go.dev/LICENSE?m=text" >> "$RUNNER_TEMP/LICENSE" 33 | VERSION="$(git describe --tags 2> /dev/null || echo "WIP")" 34 | DIR="$(mktemp -d)" 35 | mkdir "$DIR/sbctl" 36 | cp "$RUNNER_TEMP/LICENSE" "$DIR/sbctl" 37 | go build -o "$DIR/sbctl" -trimpath ./cmd/... 38 | tar -cvzf "sbctl-$VERSION-$GOOS-$GOARCH.tar.gz" -C "$DIR" sbctl 39 | env: 40 | CGO_ENABLED: 0 41 | GOOS: ${{ matrix.GOOS }} 42 | GOARCH: ${{ matrix.GOARCH }} 43 | GOARM: ${{ matrix.GOARM }} 44 | - name: Upload workflow artifacts 45 | uses: actions/upload-artifact@v4 46 | with: 47 | name: sbctl-binaries-${{ matrix.GOOS }}-${{ matrix.GOARCH }} 48 | path: sbctl-* 49 | upload: 50 | name: Upload release binaries 51 | if: github.event_name == 'release' 52 | needs: build 53 | permissions: 54 | contents: write 55 | runs-on: ubuntu-latest 56 | steps: 57 | - name: Download workflow artifacts 58 | uses: actions/download-artifact@v4 59 | with: 60 | name: '**' 61 | merge-multiple: true 62 | - name: Upload release artifacts 63 | run: gh release upload "$GITHUB_REF_NAME" sbctl-* 64 | env: 65 | GH_REPO: ${{ github.repository }} 66 | GH_TOKEN: ${{ github.token }} 67 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | jobs: 4 | arch: 5 | runs-on: ubuntu-latest 6 | container: 7 | image: archlinux:latest 8 | options: --device /dev/kvm 9 | steps: 10 | - run: pacman --noconfirm --noprogressbar -Syu 11 | - run: pacman --noconfirm --noprogressbar -S make go asciidoc gcc git edk2-ovmf qemu-system-x86 12 | - uses: actions/checkout@v1 13 | - run: git config --global --add safe.directory $(pwd) 14 | - run: make 15 | - run: make test 16 | - run: make integration 17 | - run: GOBIN=/usr/bin make lint 18 | void: 19 | runs-on: ubuntu-latest 20 | container: ghcr.io/void-linux/void-musl 21 | steps: 22 | # update xbps, if necessary 23 | - run: xbps-install -Syu || ( xbps-install -yu xbps && xbps-install -Syu) 24 | - run: xbps-install -y make go asciidoc gcc git openssl-devel 25 | - uses: actions/checkout@v1 26 | - run: git config --global --add safe.directory $(pwd) 27 | - run: make 28 | - run: make test 29 | - run: GOBIN=/usr/bin make lint 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tags 2 | releases/* 3 | /sbctl 4 | docs/*.5 5 | docs/*.8 6 | rootfs* 7 | VERSION 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Morten Linderud 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROGNM := sbctl 2 | PREFIX := /usr/local 3 | BINDIR := $(PREFIX)/bin 4 | LIBDIR := $(PREFIX)/lib 5 | SHRDIR := $(PREFIX)/share 6 | DOCDIR := $(PREFIX)/share/doc 7 | MANDIR := $(PREFIX)/share/man 8 | MANS = $(basename $(wildcard docs/*.txt)) 9 | 10 | GOFLAGS ?= -buildmode=pie -trimpath 11 | 12 | TAG = $(shell git describe --abbrev=0 --tags) 13 | 14 | GIT_DESCRIBE = $(shell git describe | sed 's/-/./g;s/^v//;') 15 | 16 | VERSION = $(shell if test -f VERSION; then cat VERSION; else echo -n $(GIT_DESCRIBE) ; fi) 17 | 18 | all: man build 19 | build: sbctl 20 | man: $(MANS) 21 | $(MANS): 22 | 23 | docs/sbctl.%: docs/sbctl.%.txt docs/asciidoc.conf 24 | a2x --no-xmllint --asciidoc-opts="-f docs/asciidoc.conf" -d manpage -f manpage -D docs $< 25 | 26 | .PHONY: sbctl 27 | sbctl: 28 | go build -ldflags="-X github.com/foxboron/sbctl.Version=$(VERSION)" -o $@ ./cmd/$@ 29 | 30 | .PHONY: completions 31 | completions: sbctl 32 | ./sbctl completion bash | install -D /dev/stdin contrib/completions/bash-completion/completions/sbctl 33 | ./sbctl completion zsh | install -D /dev/stdin contrib/completions/zsh/site-functions/_sbctl 34 | ./sbctl completion fish | install -D /dev/stdin contrib/completions/fish/vendor_completions.d/sbctl.fish 35 | 36 | install: sbctl completions man 37 | install -Dm755 sbctl -t '$(DESTDIR)$(BINDIR)' 38 | for manfile in $(MANS); do \ 39 | install -Dm644 "$$manfile" -t '$(DESTDIR)$(MANDIR)/man'"$${manfile##*.}"; \ 40 | done; 41 | install -Dm644 contrib/completions/bash-completion/completions/sbctl '$(DESTDIR)$(SHRDIR)/bash-completion/completions/sbctl' 42 | install -Dm644 contrib/completions/zsh/site-functions/_sbctl '$(DESTDIR)$(SHRDIR)/zsh/site-functions/_sbctl' 43 | install -Dm644 contrib/completions/fish/vendor_completions.d/sbctl.fish '$(DESTDIR)$(SHRDIR)/fish/vendor_completions.d/sbctl.fish' 44 | install -Dm755 contrib/kernel-install/91-sbctl.install '$(DESTDIR)$(LIBDIR)/kernel/install.d/91-sbctl.install' 45 | install -Dm644 LICENSE -t '$(DESTDIR)$(SHRDIR)/licenses/$(PROGNM)' 46 | 47 | .PHONY: release 48 | release: 49 | echo -n "$(GIT_DESCRIBE)" > VERSION 50 | mkdir -p releases 51 | git archive --prefix=${PROGNM}-${TAG}/ --add-file=VERSION -o releases/${PROGNM}-${TAG}.tar.gz ${TAG}; 52 | gpg --detach-sign -o releases/${PROGNM}-${TAG}.tar.gz.sig releases/${PROGNM}-${TAG}.tar.gz 53 | gh release upload ${TAG} releases/${PROGNM}-${TAG}.tar.gz.sig releases/${PROGNM}-${TAG}.tar.gz 54 | 55 | .PHONY: push-aur 56 | push-aur: 57 | git subtree push -P "contrib/aur/sbctl-git" aur:sbctl-git.git master 58 | 59 | clean: 60 | rm -f $(MANS) 61 | rm -f sbctl 62 | 63 | .PHONY: lint 64 | lint: 65 | go vet ./... 66 | go run honnef.co/go/tools/cmd/staticcheck@v0.5.1 ./... 67 | 68 | .PHONY: test 69 | test: 70 | go test -v ./... 71 | 72 | .PHONY: integration 73 | integration: 74 | # vmtest doesn't allow provide a way to pass --tags to the command that compiles 75 | # the test (see: vmtest.RunGoTestsInVM) so we pass it as an env variable. 76 | GOFLAGS=--tags=integration go test -v tests/integration_test.go 77 | 78 | .PHONY: local-aur 79 | .ONESHELL: 80 | local-aur: 81 | cd ./contrib/aur/sbctl-git 82 | mkdir -p ./src 83 | ln -srfT $(CURDIR) ./src/sbctl 84 | makepkg --holdver --syncdeps --noextract --force 85 | -------------------------------------------------------------------------------- /backend/backend_test.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "testing" 7 | 8 | "github.com/foxboron/sbctl/config" 9 | "github.com/foxboron/sbctl/hierarchy" 10 | "github.com/spf13/afero" 11 | ) 12 | 13 | func TestCreateKeys(t *testing.T) { 14 | c := &config.Config{ 15 | Keydir: t.TempDir(), 16 | Keys: &config.Keys{ 17 | PK: &config.KeyConfig{ 18 | Type: "file", 19 | }, 20 | KEK: &config.KeyConfig{}, 21 | Db: &config.KeyConfig{}, 22 | }, 23 | } 24 | state := &config.State{ 25 | Fs: afero.NewOsFs(), 26 | Config: c, 27 | } 28 | hier, err := CreateKeys(state) 29 | if err != nil { 30 | t.Fatalf("%v", err) 31 | } 32 | 33 | err = hier.SaveKeys(afero.NewOsFs(), c.Keydir) 34 | if err != nil { 35 | t.Fatalf("%v", err) 36 | } 37 | 38 | key, err := GetKeyBackend(state, hierarchy.PK) 39 | if err != nil { 40 | log.Fatal(err) 41 | } 42 | fmt.Println(key.Certificate().Subject.CommonName) 43 | } 44 | -------------------------------------------------------------------------------- /backend/file.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "bytes" 5 | "crypto" 6 | "crypto/rand" 7 | "crypto/rsa" 8 | "crypto/x509" 9 | "crypto/x509/pkix" 10 | "encoding/pem" 11 | "fmt" 12 | "math/big" 13 | "path/filepath" 14 | "time" 15 | 16 | "github.com/foxboron/sbctl/fs" 17 | 18 | "github.com/foxboron/sbctl/hierarchy" 19 | "github.com/spf13/afero" 20 | ) 21 | 22 | var RSAKeySize = 4096 23 | 24 | type FileKey struct { 25 | keytype BackendType 26 | cert *x509.Certificate 27 | privkey *rsa.PrivateKey 28 | } 29 | 30 | func NewFileKey(_ hierarchy.Hierarchy, desc string) (*FileKey, error) { 31 | serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) 32 | serialNumber, _ := rand.Int(rand.Reader, serialNumberLimit) 33 | c := x509.Certificate{ 34 | SerialNumber: serialNumber, 35 | PublicKeyAlgorithm: x509.RSA, 36 | SignatureAlgorithm: x509.SHA256WithRSA, 37 | NotBefore: time.Now(), 38 | NotAfter: time.Now().AddDate(5, 0, 0), 39 | Subject: pkix.Name{ 40 | Country: []string{desc}, 41 | CommonName: desc, 42 | }, 43 | } 44 | priv, err := rsa.GenerateKey(rand.Reader, RSAKeySize) 45 | if err != nil { 46 | return nil, err 47 | } 48 | derBytes, err := x509.CreateCertificate(rand.Reader, &c, &c, &priv.PublicKey, priv) 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | cert, err := x509.ParseCertificate(derBytes) 54 | if err != nil { 55 | return nil, err 56 | } 57 | return &FileKey{ 58 | keytype: FileBackend, 59 | cert: cert, 60 | privkey: priv, 61 | }, nil 62 | } 63 | 64 | func ReadFileKey(vfs afero.Fs, dir string, hier hierarchy.Hierarchy) (*FileKey, error) { 65 | path := filepath.Join(dir, hier.String()) 66 | keyname := filepath.Join(path, fmt.Sprintf("%s.key", hier.String())) 67 | certname := filepath.Join(path, fmt.Sprintf("%s.pem", hier.String())) 68 | 69 | // Read privatekey 70 | keyb, err := fs.ReadFile(vfs, keyname) 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | // Read certificate 76 | pemb, err := fs.ReadFile(vfs, certname) 77 | if err != nil { 78 | return nil, err 79 | } 80 | return FileKeyFromBytes(keyb, pemb) 81 | } 82 | 83 | func FileKeyFromBytes(keyb, pemb []byte) (*FileKey, error) { 84 | block, _ := pem.Decode(keyb) 85 | if block == nil { 86 | return nil, fmt.Errorf("failed to parse pem block") 87 | } 88 | priv, err := x509.ParsePKCS8PrivateKey(block.Bytes) 89 | if err != nil { 90 | return nil, fmt.Errorf("failed to parse key: %w", err) 91 | } 92 | 93 | var key *rsa.PrivateKey 94 | switch priv := priv.(type) { 95 | case *rsa.PrivateKey: 96 | key = priv 97 | default: 98 | return nil, fmt.Errorf("unknown type of public key") 99 | } 100 | 101 | block, _ = pem.Decode(pemb) 102 | if block == nil { 103 | return nil, fmt.Errorf("no pem block") 104 | } 105 | 106 | cert, err := x509.ParseCertificate(block.Bytes) 107 | if err != nil { 108 | return nil, fmt.Errorf("failed to parse cert: %w", err) 109 | } 110 | return &FileKey{ 111 | keytype: FileBackend, 112 | cert: cert, 113 | privkey: key, 114 | }, nil 115 | } 116 | 117 | func (f *FileKey) Type() BackendType { return f.keytype } 118 | func (f *FileKey) Certificate() *x509.Certificate { return f.cert } 119 | func (f *FileKey) Signer() crypto.Signer { return f.privkey } 120 | func (f *FileKey) Description() string { return f.Certificate().Subject.SerialNumber } 121 | 122 | func (f *FileKey) PrivateKeyBytes() []byte { 123 | privateKeyBytes, err := x509.MarshalPKCS8PrivateKey(f.privkey) 124 | if err != nil { 125 | panic("not a valid private key") 126 | } 127 | b := new(bytes.Buffer) 128 | if err := pem.Encode(b, &pem.Block{Type: "PRIVATE KEY", Bytes: privateKeyBytes}); err != nil { 129 | panic("failed producing PEM encoded certificate") 130 | } 131 | return b.Bytes() 132 | } 133 | 134 | func (f *FileKey) CertificateBytes() []byte { 135 | b := new(bytes.Buffer) 136 | if err := pem.Encode(b, &pem.Block{Type: "CERTIFICATE", Bytes: f.cert.Raw}); err != nil { 137 | panic("failed producing PEM encoded certificate") 138 | } 139 | return b.Bytes() 140 | } 141 | -------------------------------------------------------------------------------- /backend/tpm.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "bytes" 5 | "crypto" 6 | "crypto/rand" 7 | "crypto/x509" 8 | "crypto/x509/pkix" 9 | "encoding/pem" 10 | "fmt" 11 | "math/big" 12 | "path/filepath" 13 | "time" 14 | 15 | keyfile "github.com/foxboron/go-tpm-keyfiles" 16 | "github.com/foxboron/sbctl/fs" 17 | "github.com/foxboron/sbctl/hierarchy" 18 | "github.com/google/go-tpm/tpm2" 19 | "github.com/google/go-tpm/tpm2/transport" 20 | "github.com/spf13/afero" 21 | ) 22 | 23 | type TPMKey struct { 24 | *keyfile.TPMKey 25 | keytype BackendType 26 | cert *x509.Certificate 27 | tpm func() transport.TPMCloser 28 | } 29 | 30 | func NewTPMKey(tpmcb func() transport.TPMCloser, desc string) (*TPMKey, error) { 31 | rwc := tpmcb() 32 | key, err := keyfile.NewLoadableKey(rwc, tpm2.TPMAlgRSA, 2048, []byte(nil), 33 | keyfile.WithDescription(desc), 34 | ) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) 40 | serialNumber, _ := rand.Int(rand.Reader, serialNumberLimit) 41 | c := x509.Certificate{ 42 | SerialNumber: serialNumber, 43 | PublicKeyAlgorithm: x509.RSA, 44 | SignatureAlgorithm: x509.SHA256WithRSA, 45 | NotBefore: time.Now(), 46 | NotAfter: time.Now().AddDate(5, 0, 0), 47 | Subject: pkix.Name{ 48 | Country: []string{desc}, 49 | CommonName: desc, 50 | }, 51 | } 52 | 53 | pubkey, err := key.PublicKey() 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | signer, err := key.Signer(rwc, []byte(nil), []byte(nil)) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | derBytes, err := x509.CreateCertificate(rand.Reader, &c, &c, pubkey, signer) 64 | if err != nil { 65 | return nil, err 66 | } 67 | 68 | cert, err := x509.ParseCertificate(derBytes) 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | return &TPMKey{ 74 | TPMKey: key, 75 | cert: cert, 76 | tpm: tpmcb, 77 | }, nil 78 | } 79 | 80 | func (t *TPMKey) Type() BackendType { return t.keytype } 81 | func (t *TPMKey) Certificate() *x509.Certificate { return t.cert } 82 | func (t *TPMKey) Description() string { return t.TPMKey.Description } 83 | 84 | func (t *TPMKey) Signer() crypto.Signer { 85 | s, err := t.TPMKey.Signer(t.tpm(), []byte(nil), []byte(nil)) 86 | if err != nil { 87 | panic(err) 88 | } 89 | return s 90 | } 91 | 92 | func (t *TPMKey) PrivateKeyBytes() []byte { 93 | return t.TPMKey.Bytes() 94 | } 95 | 96 | func (t *TPMKey) CertificateBytes() []byte { 97 | b := new(bytes.Buffer) 98 | if err := pem.Encode(b, &pem.Block{Type: "CERTIFICATE", Bytes: t.cert.Raw}); err != nil { 99 | panic("failed producing PEM encoded certificate") 100 | } 101 | return b.Bytes() 102 | } 103 | 104 | func ReadTPMKey(vfs afero.Fs, tpmcb func() transport.TPMCloser, dir string, hier hierarchy.Hierarchy) (*TPMKey, error) { 105 | path := filepath.Join(dir, hier.String()) 106 | keyname := filepath.Join(path, fmt.Sprintf("%s.key", hier.String())) 107 | certname := filepath.Join(path, fmt.Sprintf("%s.pem", hier.String())) 108 | 109 | // Read privatekey 110 | keyb, err := fs.ReadFile(vfs, keyname) 111 | if err != nil { 112 | return nil, err 113 | } 114 | 115 | // Read certificate 116 | pemb, err := fs.ReadFile(vfs, certname) 117 | if err != nil { 118 | return nil, err 119 | } 120 | return TPMKeyFromBytes(tpmcb, keyb, pemb) 121 | } 122 | 123 | func TPMKeyFromBytes(tpmcb func() transport.TPMCloser, keyb, pemb []byte) (*TPMKey, error) { 124 | tpmkey, err := keyfile.Decode(keyb) 125 | if err != nil { 126 | return nil, fmt.Errorf("failed parking tpm keyfile: %v", err) 127 | } 128 | 129 | block, _ := pem.Decode(pemb) 130 | if block == nil { 131 | return nil, fmt.Errorf("no pem block") 132 | } 133 | 134 | cert, err := x509.ParseCertificate(block.Bytes) 135 | if err != nil { 136 | return nil, fmt.Errorf("failed to parse cert: %w", err) 137 | } 138 | return &TPMKey{ 139 | TPMKey: tpmkey, 140 | keytype: TPMBackend, 141 | cert: cert, 142 | tpm: tpmcb, 143 | }, nil 144 | } 145 | -------------------------------------------------------------------------------- /bundles.go: -------------------------------------------------------------------------------- 1 | package sbctl 2 | 3 | import ( 4 | "debug/pe" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "os" 9 | "os/exec" 10 | "runtime" 11 | 12 | "github.com/foxboron/sbctl/config" 13 | "github.com/foxboron/sbctl/fs" 14 | "github.com/spf13/afero" 15 | ) 16 | 17 | type Bundle struct { 18 | Output string `json:"output"` 19 | IntelMicrocode string `json:"intel_microcode"` 20 | AMDMicrocode string `json:"amd_microcode"` 21 | KernelImage string `json:"kernel_image"` 22 | Initramfs string `json:"initramfs"` 23 | Cmdline string `json:"cmdline"` 24 | Splash string `json:"splash"` 25 | OSRelease string `json:"os_release"` 26 | EFIStub string `json:"efi_stub"` 27 | ESP string `json:"esp"` 28 | } 29 | 30 | type Bundles map[string]*Bundle 31 | 32 | func ReadBundleDatabase(vfs afero.Fs, dbpath string) (Bundles, error) { 33 | f, err := ReadOrCreateFile(vfs, dbpath) 34 | if err != nil { 35 | return nil, err 36 | } 37 | bundles := make(Bundles) 38 | if len(f) == 0 { 39 | return bundles, nil 40 | } 41 | if err = json.Unmarshal(f, &bundles); err != nil { 42 | return nil, fmt.Errorf("failed to parse json: %v", err) 43 | } 44 | return bundles, nil 45 | } 46 | 47 | func WriteBundleDatabase(vfs afero.Fs, dbpath string, bundles Bundles) error { 48 | data, err := json.MarshalIndent(bundles, "", " ") 49 | if err != nil { 50 | return err 51 | } 52 | err = fs.WriteFile(vfs, dbpath, data, 0644) 53 | if err != nil { 54 | return err 55 | } 56 | return nil 57 | } 58 | 59 | func BundleIter(state *config.State, fn func(s *Bundle) error) error { 60 | files, err := ReadBundleDatabase(state.Fs, state.Config.BundlesDb) 61 | if err != nil { 62 | return err 63 | } 64 | for _, s := range files { 65 | if err := fn(s); err != nil { 66 | return err 67 | } 68 | } 69 | return nil 70 | } 71 | 72 | func efiStubArch() (string, error) { 73 | switch runtime.GOARCH { 74 | case "amd64": 75 | return "linuxx64.efi.stub", nil 76 | case "arm64": 77 | return "linuxaa64.efi.stub", nil 78 | case "386": 79 | return "linuxia32.efi.stub", nil 80 | } 81 | 82 | return "", fmt.Errorf("unsupported architecture") 83 | } 84 | 85 | func GetEfistub(vfs afero.Fs) (string, error) { 86 | candidatePaths := []string{ 87 | "/lib/systemd/boot/efi/", 88 | "/lib/gummiboot/", 89 | } 90 | stubName, err := efiStubArch() 91 | if err != nil { 92 | return "", fmt.Errorf("cannot search for EFI stub: %v", err) 93 | } 94 | 95 | for _, f := range candidatePaths { 96 | if _, err := vfs.Stat(f + stubName); err == nil { 97 | return f + stubName, nil 98 | } 99 | } 100 | return "", nil 101 | } 102 | 103 | func NewBundle(vfs afero.Fs) (bundle *Bundle, err error) { 104 | esp, err := GetESP(vfs) 105 | if err != nil { 106 | // This is not critical, just use an empty default. 107 | esp = "" 108 | } 109 | 110 | stub, err := GetEfistub(vfs) 111 | if err != nil { 112 | return nil, fmt.Errorf("failed to get default EFI stub location: %v", err) 113 | } 114 | 115 | bundle = &Bundle{ 116 | Output: "", 117 | IntelMicrocode: "", 118 | AMDMicrocode: "", 119 | KernelImage: "/boot/vmlinuz-linux", 120 | Initramfs: "/boot/initramfs-linux.img", 121 | Cmdline: "/etc/kernel/cmdline", 122 | Splash: "", 123 | OSRelease: "/usr/lib/os-release", 124 | EFIStub: stub, 125 | ESP: esp, 126 | } 127 | 128 | return 129 | } 130 | 131 | // Reference ukify from systemd: 132 | // https://github.com/systemd/systemd/blob/d09df6b94e0c4924ea7064c79ab0441f5aff469b/src/ukify/ukify.py 133 | 134 | func GenerateBundle(vfs afero.Fs, bundle *Bundle) (bool, error) { 135 | type section struct { 136 | section string 137 | file string 138 | } 139 | sections := []section{ 140 | {".osrel", bundle.OSRelease}, 141 | {".cmdline", bundle.Cmdline}, 142 | {".splash", bundle.Splash}, 143 | {".initrd", bundle.Initramfs}, 144 | {".linux", bundle.KernelImage}, 145 | } 146 | 147 | if bundle.EFIStub == "" { 148 | return false, fmt.Errorf("could not find EFI stub binary, please install systemd-boot or provide --efi-stub on the command line") 149 | } 150 | 151 | e, err := pe.Open(bundle.EFIStub) 152 | if err != nil { 153 | return false, err 154 | } 155 | e.Close() 156 | s := e.Sections[len(e.Sections)-1] 157 | 158 | vma := uint64(s.VirtualAddress) + uint64(s.VirtualSize) 159 | switch e := e.OptionalHeader.(type) { 160 | case *pe.OptionalHeader32: 161 | vma += uint64(e.ImageBase) 162 | case *pe.OptionalHeader64: 163 | vma += e.ImageBase 164 | } 165 | vma = roundUpToBlockSize(vma) 166 | 167 | var args []string 168 | for _, s := range sections { 169 | if s.file == "" { 170 | // optional sections 171 | switch s.section { 172 | case ".splash": 173 | continue 174 | } 175 | } 176 | fi, err := vfs.Stat(s.file) 177 | if err != nil || fi.IsDir() { 178 | return false, err 179 | } 180 | var flags string 181 | switch s.section { 182 | case ".linux": 183 | flags = "code,readonly" 184 | default: 185 | flags = "data,readonly" 186 | } 187 | args = append(args, 188 | "--add-section", fmt.Sprintf("%s=%s", s.section, s.file), 189 | "--set-section-flags", fmt.Sprintf("%s=%s", s.section, flags), 190 | "--change-section-vma", fmt.Sprintf("%s=%#x", s.section, vma), 191 | ) 192 | vma += roundUpToBlockSize(uint64(fi.Size())) 193 | } 194 | 195 | args = append(args, bundle.EFIStub, bundle.Output) 196 | cmd := exec.Command("objcopy", args...) 197 | cmd.Stdout = os.Stdout 198 | cmd.Stderr = os.Stderr 199 | if err := cmd.Run(); err != nil { 200 | if errors.Is(err, exec.ErrNotFound) { 201 | return false, err 202 | } 203 | if exitError, ok := err.(*exec.ExitError); ok { 204 | return exitError.ExitCode() == 0, nil 205 | } 206 | } 207 | return true, nil 208 | } 209 | 210 | func roundUpToBlockSize(size uint64) uint64 { 211 | const blockSize = 4096 212 | return ((size + blockSize - 1) / blockSize) * blockSize 213 | } 214 | -------------------------------------------------------------------------------- /certs/builtin.go: -------------------------------------------------------------------------------- 1 | package certs 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/foxboron/go-uefi/efi" 8 | "github.com/foxboron/go-uefi/efi/attributes" 9 | "github.com/foxboron/go-uefi/efi/signature" 10 | "github.com/foxboron/go-uefi/efi/util" 11 | ) 12 | 13 | var ( 14 | efiGlobalGuid = util.EFIGUID{ 15 | Data1: 0x8be4df61, 16 | Data2: 0x93ca, 17 | Data3: 0x11d2, 18 | Data4: [8]uint8{0xaa, 0x0d, 0x00, 0xe0, 0x98, 0x03, 0x2b, 0x8c}, 19 | } 20 | defaultSignatureDatabaseNames = map[string]string{ 21 | "db": "dbDefault", 22 | "KEK": "KEKDefault", 23 | "PK": "PKDefault", 24 | } 25 | ) 26 | 27 | type builtinSignatureDataEntry struct { 28 | SignatureType util.EFIGUID 29 | Data *signature.SignatureData 30 | } 31 | 32 | func GetBuiltinCertificates(db string) (*signature.SignatureDatabase, error) { 33 | defaultName, ok := defaultSignatureDatabaseNames[db] 34 | if !ok { 35 | return nil, fmt.Errorf("%s is an unrecognized firmware database", db) 36 | } 37 | attr, buf, err := attributes.ReadEfivarsWithGuid(defaultName, efiGlobalGuid) 38 | if err != nil { 39 | if err == os.ErrNotExist { 40 | // not finding a default db is not a failure! 41 | return signature.NewSignatureDatabase(), nil 42 | } 43 | return nil, err 44 | } 45 | 46 | if attr&attributes.EFI_VARIABLE_NON_VOLATILE != 0 { 47 | // If this variable has non-volatile storage, a malicious user could have created it. 48 | // The EDK2 implementation of default Secure Boot stores marks them volatile. 49 | return nil, fmt.Errorf("vendor default database is non-volatile (and is vulnerable to being tampered with)") 50 | } 51 | 52 | database, err := signature.ReadSignatureDatabase(buf) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | // Remove vendor certificates that are already covered by the built-in vendor database. 58 | // TODO: Do we actually want this? The machine might be enrolled with more Microsoft certs then sbctl actually covers 59 | 60 | detect := map[util.EFIGUID]string{} 61 | for k, v := range oemGUID { 62 | detect[v] = k 63 | } 64 | 65 | removals := make([]*builtinSignatureDataEntry, 0, 8) 66 | 67 | for _, l := range database { 68 | if l.SignatureType == signature.CERT_X509_GUID || l.SignatureType == signature.CERT_X509_SHA256_GUID { 69 | for _, s := range l.Signatures { 70 | if _, ok := detect[s.Owner]; ok { 71 | removals = append(removals, &builtinSignatureDataEntry{ 72 | SignatureType: l.SignatureType, 73 | Data: &s, 74 | }) 75 | } 76 | } 77 | } 78 | } 79 | 80 | // Depending on the implementation of .RemoveSignature, this could be 81 | // expensive; however, we don't expect dbDebault to be particularly huge. 82 | for _, s := range removals { 83 | database.RemoveSignature(s.SignatureType, s.Data) 84 | } 85 | 86 | return &database, nil 87 | } 88 | 89 | func GetSignatureDatabase(s string) (*signature.SignatureDatabase, error) { 90 | switch s { 91 | case "db": 92 | return efi.Getdb() 93 | case "KEK": 94 | return efi.GetKEK() 95 | case "PK": 96 | return efi.GetPK() 97 | } 98 | return nil, nil 99 | } 100 | 101 | func BuiltinSignatureOwners() ([]string, error) { 102 | ret := []string{} 103 | for _, sbDatabase := range []string{"db", "KEK", "PK"} { 104 | db, err := GetSignatureDatabase(sbDatabase) 105 | if err != nil { 106 | return nil, err 107 | } 108 | dbDefault, err := GetBuiltinCertificates(sbDatabase) 109 | if err != nil { 110 | return nil, err 111 | } 112 | for _, siglist := range *dbDefault { 113 | if db.Exists(siglist.SignatureType, siglist) { 114 | ret = append(ret, fmt.Sprintf("builtin-%s", sbDatabase)) 115 | } 116 | } 117 | } 118 | return ret, nil 119 | } 120 | -------------------------------------------------------------------------------- /certs/certs.go: -------------------------------------------------------------------------------- 1 | package certs 2 | 3 | import ( 4 | "embed" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/foxboron/go-uefi/efi/signature" 10 | "github.com/foxboron/go-uefi/efi/util" 11 | ) 12 | 13 | //go:embed microsoft/* 14 | var content embed.FS 15 | 16 | var ( 17 | defaultCerts = []string{"microsoft"} 18 | oemGUID = map[string]util.EFIGUID{ 19 | "microsoft": *util.StringToGUID("77fa9abd-0359-4d32-bd60-28f4e78f784b"), 20 | "tpm-eventlog": *util.StringToGUID("4f52704f-494d-41736e-6e6f79696e6721"), 21 | "custom": *util.StringToGUID("88a69775-5ad7-45d9-9f34-cec43e1f1989"), 22 | } 23 | ) 24 | 25 | func GetVendors() []string { 26 | var oems []string 27 | files, _ := content.ReadDir(".") 28 | for _, file := range files { 29 | oems = append(oems, file.Name()) 30 | } 31 | return oems 32 | } 33 | 34 | func GetOEMCerts(oem string, variable string) (*signature.SignatureDatabase, error) { 35 | GUID, ok := oemGUID[oem] 36 | if !ok { 37 | return nil, fmt.Errorf("invalid OEM") 38 | } 39 | sigdb := signature.NewSignatureDatabase() 40 | files, _ := content.ReadDir(filepath.Join(oem, variable)) 41 | for _, file := range files { 42 | path := filepath.Join(oem, variable, file.Name()) 43 | if !file.Type().IsRegular() { 44 | continue 45 | } 46 | buf, _ := content.ReadFile(path) 47 | if err := sigdb.Append(signature.CERT_X509_GUID, GUID, buf); err != nil { 48 | return nil, err 49 | } 50 | } 51 | return sigdb, nil 52 | } 53 | 54 | func GetCustomCerts(keydir string, variable string) (*signature.SignatureDatabase, error) { 55 | GUID, ok := oemGUID["custom"] 56 | if !ok { 57 | return nil, fmt.Errorf("GUID for custom certs not found") 58 | } 59 | sigdb := signature.NewSignatureDatabase() 60 | files, _ := os.ReadDir(filepath.Join(keydir, "custom", variable)) 61 | for _, file := range files { 62 | path := filepath.Join(keydir, "custom", variable, file.Name()) 63 | if !file.Type().IsRegular() { 64 | continue 65 | } 66 | buf, _ := os.ReadFile(path) 67 | if err := sigdb.Append(signature.CERT_X509_GUID, GUID, buf); err != nil { 68 | return nil, err 69 | } 70 | } 71 | return sigdb, nil 72 | } 73 | 74 | func GetDefaultCerts(variable string) (*signature.SignatureDatabase, error) { 75 | sigdb := signature.NewSignatureDatabase() 76 | for _, oem := range defaultCerts { 77 | db, err := GetOEMCerts(oem, variable) 78 | if err != nil { 79 | return nil, err 80 | } 81 | sigdb.AppendDatabase(db) 82 | } 83 | return sigdb, nil 84 | } 85 | 86 | func DetectVendorCerts(sb *signature.SignatureDatabase) []string { 87 | oems := []string{} 88 | detect := map[util.EFIGUID]string{} 89 | for k, v := range oemGUID { 90 | detect[v] = k 91 | } 92 | for _, l := range *sb { 93 | for _, sig := range l.Signatures { 94 | if o, ok := detect[sig.Owner]; ok { 95 | oems = append(oems, o) 96 | delete(detect, sig.Owner) 97 | } 98 | } 99 | } 100 | return oems 101 | } 102 | -------------------------------------------------------------------------------- /certs/certs_test.go: -------------------------------------------------------------------------------- 1 | package certs 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestGetVendors(t *testing.T) { 9 | oems := GetVendors() 10 | if !reflect.DeepEqual(oems, []string{"microsoft"}) { 11 | t.Fatalf("GetVendors: not the same") 12 | } 13 | } 14 | 15 | func TestGetOEMCertsDb(t *testing.T) { 16 | db, _ := GetOEMCerts("microsoft", "db") 17 | if len(*db) != 5 { 18 | t.Fatalf("GetOEMCerts: not correct size, got %d, expected %d", len(*db), 5) 19 | } 20 | } 21 | 22 | func TestGetOEMCertsKek(t *testing.T) { 23 | kek, _ := GetOEMCerts("microsoft", "KEK") 24 | if len(*kek) != 2 { 25 | t.Fatalf("GetOEMCerts: not correct size, got %d, expected %d", len(*kek), 2) 26 | } 27 | } 28 | 29 | func TestDefaultCertsDb(t *testing.T) { 30 | db, _ := GetDefaultCerts("db") 31 | if len(*db) != 5 { 32 | t.Fatalf("GetDefaultCerts: not correct size, got %d, expected %d", len(*db), 5) 33 | } 34 | } 35 | 36 | func TestDefaultCertsKek(t *testing.T) { 37 | kek, _ := GetDefaultCerts("KEK") 38 | if len(*kek) != 2 { 39 | t.Fatalf("GetDefaultCerts: not correct size, got %d, expected %d", len(*kek), 2) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /certs/microsoft/KEK/MicCorKEKCA2011_2011-06-24.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Foxboron/sbctl/30cc0e7b22a0f7185e9962f009ead32c2e482877/certs/microsoft/KEK/MicCorKEKCA2011_2011-06-24.der -------------------------------------------------------------------------------- /certs/microsoft/KEK/microsoft corporation kek 2k ca 2023.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Foxboron/sbctl/30cc0e7b22a0f7185e9962f009ead32c2e482877/certs/microsoft/KEK/microsoft corporation kek 2k ca 2023.der -------------------------------------------------------------------------------- /certs/microsoft/db/MicCorUEFCA2011_2011-06-27.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Foxboron/sbctl/30cc0e7b22a0f7185e9962f009ead32c2e482877/certs/microsoft/db/MicCorUEFCA2011_2011-06-27.der -------------------------------------------------------------------------------- /certs/microsoft/db/MicWinProPCA2011_2011-10-19.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Foxboron/sbctl/30cc0e7b22a0f7185e9962f009ead32c2e482877/certs/microsoft/db/MicWinProPCA2011_2011-10-19.der -------------------------------------------------------------------------------- /certs/microsoft/db/microsoft option rom uefi ca 2023.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Foxboron/sbctl/30cc0e7b22a0f7185e9962f009ead32c2e482877/certs/microsoft/db/microsoft option rom uefi ca 2023.der -------------------------------------------------------------------------------- /certs/microsoft/db/microsoft uefi ca 2023.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Foxboron/sbctl/30cc0e7b22a0f7185e9962f009ead32c2e482877/certs/microsoft/db/microsoft uefi ca 2023.der -------------------------------------------------------------------------------- /certs/microsoft/db/windows uefi ca 2023.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Foxboron/sbctl/30cc0e7b22a0f7185e9962f009ead32c2e482877/certs/microsoft/db/windows uefi ca 2023.der -------------------------------------------------------------------------------- /chattr.go: -------------------------------------------------------------------------------- 1 | package sbctl 2 | 3 | import ( 4 | "os" 5 | 6 | "golang.org/x/sys/unix" 7 | ) 8 | 9 | const ( 10 | // from /usr/include/linux/fs.h 11 | FS_SECRM_FL = 0x00000001 /* Secure deletion */ 12 | FS_UNRM_FL = 0x00000002 /* Undelete */ 13 | FS_COMPR_FL = 0x00000004 /* Compress file */ 14 | FS_SYNC_FL = 0x00000008 /* Synchronous updates */ 15 | FS_IMMUTABLE_FL = 0x00000010 /* Immutable file */ 16 | FS_APPEND_FL = 0x00000020 /* writes to file may only append */ 17 | FS_NODUMP_FL = 0x00000040 /* do not dump file */ 18 | FS_NOATIME_FL = 0x00000080 /* do not update atime */ 19 | FS_DIRTY_FL = 0x00000100 20 | FS_COMPRBLK_FL = 0x00000200 /* One or more compressed clusters */ 21 | FS_NOCOMP_FL = 0x00000400 /* Don't compress */ 22 | FS_ECOMPR_FL = 0x00000800 /* Compression error */ 23 | FS_BTREE_FL = 0x00001000 /* btree format dir */ 24 | FS_INDEX_FL = 0x00001000 /* hash-indexed directory */ 25 | FS_IMAGIC_FL = 0x00002000 /* AFS directory */ 26 | FS_JOURNAL_DATA_FL = 0x00004000 /* Reserved for ext3 */ 27 | FS_NOTAIL_FL = 0x00008000 /* file tail should not be merged */ 28 | FS_DIRSYNC_FL = 0x00010000 /* dirsync behaviour (directories only) */ 29 | FS_TOPDIR_FL = 0x00020000 /* Top of directory hierarchies*/ 30 | FS_EXTENT_FL = 0x00080000 /* Extents */ 31 | FS_DIRECTIO_FL = 0x00100000 /* Use direct i/o */ 32 | FS_NOCOW_FL = 0x00800000 /* Do not cow file */ 33 | FS_PROJINHERIT_FL = 0x20000000 /* Create with parents projid */ 34 | FS_RESERVED_FL = 0x80000000 /* reserved for ext2 lib */ 35 | ) 36 | 37 | /* The code below won't work correctly if this tool is built 38 | * for a 64-bit big endian platform. 39 | * See https://github.com/golang/go/issues/45585 for context. */ 40 | 41 | // GetAttr retrieves the attributes of a file on a linux filesystem 42 | func GetAttr(f *os.File) (int32, error) { 43 | attr_int, err := unix.IoctlGetInt(int(f.Fd()), unix.FS_IOC_GETFLAGS) 44 | return int32(attr_int), err 45 | } 46 | 47 | // SetAttr sets the attributes of a file on a linux filesystem to the given value 48 | func SetAttr(f *os.File, attr int32) error { 49 | return unix.IoctlSetPointerInt(int(f.Fd()), unix.FS_IOC_SETFLAGS, int(attr)) 50 | } 51 | -------------------------------------------------------------------------------- /cmd/sbctl/bundle.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/foxboron/sbctl" 8 | "github.com/foxboron/sbctl/config" 9 | "github.com/foxboron/sbctl/logging" 10 | "github.com/spf13/afero" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | var ( 15 | amducode string 16 | intelucode string 17 | splashImg string 18 | osRelease string 19 | efiStub string 20 | kernelImg string 21 | cmdline string 22 | initramfs string 23 | espPath string 24 | saveBundle bool 25 | ) 26 | 27 | var bundleCmd = &cobra.Command{ 28 | Use: "bundle", 29 | Short: "Bundle the needed files for an EFI stub image", 30 | RunE: func(cmd *cobra.Command, args []string) error { 31 | state := cmd.Context().Value(stateDataKey{}).(*config.State) 32 | 33 | logging.Errorf("The bundle/uki support in sbctl is deprecated. Please move to dracut/mkinitcpio/ukify.") 34 | 35 | if len(args) < 1 { 36 | logging.Print("Requires a file to sign...\n") 37 | os.Exit(1) 38 | } 39 | checkFiles := []string{amducode, intelucode, splashImg, osRelease, efiStub, kernelImg, cmdline, initramfs} 40 | for _, path := range checkFiles { 41 | if path == "" { 42 | continue 43 | } 44 | if _, err := state.Fs.Stat(path); os.IsNotExist(err) { 45 | logging.Print("%s does not exist!\n", path) 46 | os.Exit(1) 47 | } 48 | } 49 | bundle, err := sbctl.NewBundle(state.Fs) 50 | if err != nil { 51 | return err 52 | } 53 | output, err := filepath.Abs(args[0]) 54 | if err != nil { 55 | return err 56 | } 57 | // Fail early if user wants to save bundle but doesn't have permissions 58 | var bundles sbctl.Bundles 59 | if saveBundle { 60 | // "err" needs to have been declared before this, otherwise it's necessary 61 | // to use ":=", which shadows the "bundles" variable 62 | bundles, err = sbctl.ReadBundleDatabase(state.Fs, state.Config.BundlesDb) 63 | if err != nil { 64 | return err 65 | } 66 | } 67 | bundle.Output = output 68 | bundle.IntelMicrocode = intelucode 69 | bundle.AMDMicrocode = amducode 70 | bundle.KernelImage = kernelImg 71 | bundle.Initramfs = initramfs 72 | bundle.Cmdline = cmdline 73 | bundle.Splash = splashImg 74 | bundle.OSRelease = osRelease 75 | bundle.EFIStub = efiStub 76 | bundle.ESP = espPath 77 | if err = sbctl.CreateBundle(state, *bundle); err != nil { 78 | return err 79 | } 80 | logging.Print("Wrote EFI bundle %s\n", bundle.Output) 81 | if saveBundle { 82 | bundles[bundle.Output] = bundle 83 | err := sbctl.WriteBundleDatabase(state.Fs, state.Config.BundlesDb, bundles) 84 | if err != nil { 85 | return err 86 | } 87 | } 88 | return nil 89 | }, 90 | } 91 | 92 | func bundleCmdFlags(cmd *cobra.Command) { 93 | esp, _ := sbctl.GetESP(afero.NewOsFs()) 94 | f := cmd.Flags() 95 | f.StringVarP(&amducode, "amducode", "a", "", "AMD microcode location") 96 | f.StringVarP(&intelucode, "intelucode", "i", "", "Intel microcode location") 97 | f.StringVarP(&splashImg, "splash-img", "l", "", "Boot splash image location") 98 | f.StringVarP(&osRelease, "os-release", "o", "/usr/lib/os-release", "OS Release file location") 99 | f.StringVarP(&efiStub, "efi-stub", "e", "/usr/lib/systemd/boot/efi/linuxx64.efi.stub", "EFI Stub location") 100 | f.StringVarP(&kernelImg, "kernel-img", "k", "/boot/vmlinuz-linux", "Kernel image location") 101 | f.StringVarP(&cmdline, "cmdline", "c", "/etc/kernel/cmdline", "Cmdline location") 102 | f.StringVarP(&initramfs, "initramfs", "f", "/boot/initramfs-linux.img", "Initramfs location") 103 | f.StringVarP(&espPath, "esp", "p", esp, "ESP location") 104 | f.BoolVarP(&saveBundle, "save", "s", false, "save bundle to the database") 105 | } 106 | 107 | func init() { 108 | bundleCmdFlags(bundleCmd) 109 | CliCommands = append(CliCommands, cliCommand{ 110 | Cmd: bundleCmd, 111 | }) 112 | } 113 | -------------------------------------------------------------------------------- /cmd/sbctl/completions.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | var completionCmd = &cobra.Command{Use: "completion"} 10 | 11 | func completionBashCmd() *cobra.Command { 12 | var completionCmd = &cobra.Command{ 13 | Use: "bash", 14 | Hidden: true, 15 | RunE: func(cmd *cobra.Command, args []string) error { 16 | return rootCmd.GenBashCompletion(os.Stdout) 17 | }, 18 | } 19 | return completionCmd 20 | } 21 | 22 | func completionZshCmd() *cobra.Command { 23 | var completionCmd = &cobra.Command{ 24 | Use: "zsh", 25 | Hidden: true, 26 | RunE: func(cmd *cobra.Command, args []string) error { 27 | return rootCmd.GenZshCompletion(os.Stdout) 28 | }, 29 | } 30 | return completionCmd 31 | } 32 | 33 | func completionFishCmd() *cobra.Command { 34 | var completionCmd = &cobra.Command{ 35 | Use: "fish", 36 | Hidden: true, 37 | RunE: func(cmd *cobra.Command, args []string) error { 38 | return rootCmd.GenFishCompletion(os.Stdout, true) 39 | }, 40 | } 41 | return completionCmd 42 | } 43 | 44 | func init() { 45 | completionCmd.AddCommand(completionBashCmd()) 46 | completionCmd.AddCommand(completionZshCmd()) 47 | completionCmd.AddCommand(completionFishCmd()) 48 | CliCommands = append(CliCommands, cliCommand{ 49 | Cmd: completionCmd, 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /cmd/sbctl/create-keys.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "path" 6 | "path/filepath" 7 | 8 | "github.com/foxboron/sbctl" 9 | "github.com/foxboron/sbctl/backend" 10 | "github.com/foxboron/sbctl/config" 11 | "github.com/foxboron/sbctl/logging" 12 | "github.com/foxboron/sbctl/lsm" 13 | "github.com/landlock-lsm/go-landlock/landlock" 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | var ( 18 | exportPath string 19 | databasePath string 20 | Keytype string 21 | PKKeytype, KEKKeytype, DbKeytype string 22 | ) 23 | 24 | var createKeysCmd = &cobra.Command{ 25 | Use: "create-keys", 26 | Short: "Create a set of secure boot signing keys", 27 | RunE: func(cmd *cobra.Command, args []string) error { 28 | state := cmd.Context().Value(stateDataKey{}).(*config.State) 29 | return RunCreateKeys(state) 30 | }, 31 | } 32 | 33 | func RunCreateKeys(state *config.State) error { 34 | if state.Config.Landlock { 35 | lsm.RestrictAdditionalPaths( 36 | landlock.RWDirs(filepath.Dir(filepath.Dir(filepath.Clean(state.Config.Keydir)))), 37 | ) 38 | if err := lsm.Restrict(); err != nil { 39 | return err 40 | } 41 | } 42 | // Overrides keydir or GUID location 43 | if exportPath != "" { 44 | state.Config.Keydir = exportPath 45 | } 46 | 47 | if databasePath != "" { 48 | state.Config.GUID = databasePath 49 | } 50 | 51 | if err := sbctl.CreateDirectory(state.Fs, state.Config.Keydir); err != nil { 52 | return err 53 | } 54 | if err := sbctl.CreateDirectory(state.Fs, path.Dir(state.Config.GUID)); err != nil { 55 | return err 56 | } 57 | 58 | // Should be own flag type 59 | if Keytype != "" && (Keytype == "file" || Keytype == "tpm") { 60 | state.Config.Keys.PK.Type = Keytype 61 | state.Config.Keys.KEK.Type = Keytype 62 | state.Config.Keys.Db.Type = Keytype 63 | } else { 64 | if PKKeytype != "" && (PKKeytype == "file" || PKKeytype == "tpm") { 65 | state.Config.Keys.PK.Type = PKKeytype 66 | } 67 | if KEKKeytype != "" && (KEKKeytype == "file" || KEKKeytype == "tpm") { 68 | state.Config.Keys.KEK.Type = KEKKeytype 69 | } 70 | if DbKeytype != "" && (DbKeytype == "file" || DbKeytype == "tpm") { 71 | state.Config.Keys.Db.Type = DbKeytype 72 | } 73 | } 74 | 75 | uuid, err := sbctl.CreateGUID(state.Fs, state.Config.GUID) 76 | if err != nil { 77 | return err 78 | } 79 | logging.Print("Created Owner UUID %s\n", uuid) 80 | if !sbctl.CheckIfKeysInitialized(state.Fs, state.Config.Keydir) { 81 | logging.Print("Creating secure boot keys...") 82 | 83 | hier, err := backend.CreateKeys(state) 84 | if err != nil { 85 | logging.NotOk("") 86 | return fmt.Errorf("couldn't initialize secure boot: %w", err) 87 | } 88 | err = hier.SaveKeys(state.Fs, state.Config.Keydir) 89 | if err != nil { 90 | logging.NotOk("") 91 | return fmt.Errorf("couldn't initialize secure boot: %w", err) 92 | } 93 | logging.Ok("") 94 | logging.Println("Secure boot keys created!") 95 | } else { 96 | logging.Ok("Secure boot keys have already been created!") 97 | } 98 | return nil 99 | } 100 | 101 | func createKeysCmdFlags(cmd *cobra.Command) { 102 | f := cmd.Flags() 103 | f.StringVarP(&exportPath, "export", "e", "", "export file path") 104 | f.StringVarP(&databasePath, "database-path", "d", "", "location to create GUID file") 105 | f.StringVarP(&Keytype, "keytype", "", "", "key type for all keys") 106 | f.StringVarP(&PKKeytype, "pk-keytype", "", "", "PK key type (default: file)") 107 | f.StringVarP(&KEKKeytype, "kek-keytype", "", "", "KEK key type (default: file)") 108 | f.StringVarP(&DbKeytype, "db-keytype", "", "", "db key type (default: file)") 109 | } 110 | 111 | func init() { 112 | createKeysCmdFlags(createKeysCmd) 113 | 114 | CliCommands = append(CliCommands, cliCommand{ 115 | Cmd: createKeysCmd, 116 | }) 117 | } 118 | -------------------------------------------------------------------------------- /cmd/sbctl/debug.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "archive/tar" 5 | "compress/gzip" 6 | "encoding/json" 7 | "fmt" 8 | "log" 9 | "os" 10 | "path/filepath" 11 | 12 | "github.com/foxboron/sbctl" 13 | "github.com/foxboron/sbctl/config" 14 | "github.com/foxboron/sbctl/logging" 15 | "github.com/goccy/go-yaml" 16 | "github.com/spf13/cobra" 17 | ) 18 | 19 | type DebugCmdOptions struct { 20 | Output string 21 | } 22 | 23 | var ( 24 | debugCmdOptions = DebugCmdOptions{} 25 | debugCmd = &cobra.Command{ 26 | Use: "debug", 27 | Short: "Produce debug information for sbctl", 28 | Hidden: true, 29 | RunE: func(cmd *cobra.Command, args []string) error { 30 | state := cmd.Context().Value(stateDataKey{}).(*config.State) 31 | return ProduceDebugInformation(state) 32 | }, 33 | } 34 | ) 35 | 36 | func ProduceDebugInformation(state *config.State) error { 37 | output, err := filepath.Abs(debugCmdOptions.Output) 38 | if err != nil { 39 | return err 40 | } 41 | logging.Print("Creating a debug dump to %s...\n", output) 42 | file, err := os.Create(output) 43 | if err != nil { 44 | return fmt.Errorf("could not produce debug tarball: %v", err) 45 | } 46 | 47 | defer file.Close() 48 | gzipWriter := gzip.NewWriter(file) 49 | defer gzipWriter.Close() 50 | 51 | tw := tar.NewWriter(gzipWriter) 52 | defer tw.Close() 53 | 54 | writeTw := func(n string, b []byte) error { 55 | hdr := &tar.Header{ 56 | Name: filepath.Base(n), 57 | Mode: 0600, 58 | Size: int64(len(b)), 59 | } 60 | if err := tw.WriteHeader(hdr); err != nil { 61 | return err 62 | } 63 | if _, err := tw.Write(b); err != nil { 64 | return err 65 | } 66 | return nil 67 | } 68 | 69 | efifiles, err := filepath.Glob("/sys/firmware/efi/efivars/*-8be4df61-93ca-11d2-aa0d-00e098032b8c") 70 | if err != nil { 71 | return err 72 | } 73 | efifiles = append(efifiles, 74 | "/sys/firmware/efi/efivars/dbx-d719b2cb-3d3a-4596-a3bc-dad00e67656f", 75 | "/sys/firmware/efi/efivars/db-d719b2cb-3d3a-4596-a3bc-dad00e67656f", 76 | ) 77 | 78 | for _, f := range efifiles { 79 | b, err := os.ReadFile(f) 80 | if err != nil { 81 | log.Print(err) 82 | continue 83 | } 84 | 85 | if err := writeTw(f, b); err != nil { 86 | return err 87 | } 88 | } 89 | 90 | stateb, err := json.Marshal(state) 91 | if err != nil { 92 | return err 93 | } 94 | if err := writeTw("state.json", stateb); err != nil { 95 | return err 96 | } 97 | 98 | configby, err := yaml.Marshal(state.Config) 99 | if err != nil { 100 | return err 101 | } 102 | if err := writeTw("config.yaml", configby); err != nil { 103 | return err 104 | } 105 | 106 | configbj, err := json.Marshal(state.Config) 107 | if err != nil { 108 | return err 109 | } 110 | if err := writeTw("config.json", configbj); err != nil { 111 | return err 112 | } 113 | 114 | if err := writeTw("VERSION", []byte(sbctl.Version)); err != nil { 115 | return err 116 | } 117 | return nil 118 | } 119 | 120 | func debugCmdFlags(cmd *cobra.Command) { 121 | f := cmd.Flags() 122 | f.StringVarP(&debugCmdOptions.Output, "output", "o", "sbctl_debug.tar.gz", "debug output") 123 | } 124 | 125 | func init() { 126 | debugCmdFlags(debugCmd) 127 | CliCommands = append(CliCommands, cliCommand{ 128 | Cmd: debugCmd, 129 | }) 130 | } 131 | -------------------------------------------------------------------------------- /cmd/sbctl/export-enrolled-keys.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/x509" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/foxboron/go-uefi/efi" 11 | "github.com/foxboron/go-uefi/efi/signature" 12 | "github.com/foxboron/sbctl/config" 13 | "github.com/foxboron/sbctl/lsm" 14 | "github.com/landlock-lsm/go-landlock/landlock" 15 | "github.com/spf13/cobra" 16 | ) 17 | 18 | type DerList map[string][]uint8 19 | 20 | var exportDir, format string 21 | 22 | var exportEnrolledKeysCmd = &cobra.Command{ 23 | Use: "export-enrolled-keys", 24 | Short: "Export already enrolled keys from the system", 25 | RunE: func(cmd *cobra.Command, args []string) error { 26 | state := cmd.Context().Value(stateDataKey{}).(*config.State) 27 | 28 | if exportDir == "" { 29 | fmt.Println("--dir should be set") 30 | os.Exit(1) 31 | } 32 | if state.Config.Landlock { 33 | lsm.RestrictAdditionalPaths( 34 | landlock.RWFiles(exportDir), 35 | ) 36 | if err := lsm.Restrict(); err != nil { 37 | return err 38 | } 39 | } 40 | 41 | var err error 42 | allCerts := map[string]DerList{} 43 | 44 | pk, err := efi.GetPK() 45 | if err != nil { 46 | return err 47 | } 48 | kek, err := efi.GetKEK() 49 | if err != nil { 50 | return err 51 | } 52 | db, err := efi.Getdb() 53 | if err != nil { 54 | return err 55 | } 56 | 57 | exportDir, err = ensureDir(exportDir) 58 | if err != nil { 59 | return fmt.Errorf("creating the output directory: %w", err) 60 | } 61 | 62 | switch format { 63 | case "der": 64 | allCerts["PK"] = ExtractDerFromSignatureDatabase(pk) 65 | allCerts["KEK"] = ExtractDerFromSignatureDatabase(kek) 66 | allCerts["DB"] = ExtractDerFromSignatureDatabase(db) 67 | 68 | if err := writeDerFiles(allCerts, exportDir); err != nil { 69 | return fmt.Errorf("writing the certificates: %w", err) 70 | } 71 | case "esl": 72 | if err := os.WriteFile(filepath.Join(exportDir, "db.esl"), db.Bytes(), 0o644); err != nil { 73 | return err 74 | } 75 | if err := os.WriteFile(filepath.Join(exportDir, "KEK.esl"), kek.Bytes(), 0o644); err != nil { 76 | return err 77 | } 78 | if err := os.WriteFile(filepath.Join(exportDir, "PK.esl"), pk.Bytes(), 0o644); err != nil { 79 | return err 80 | } 81 | default: 82 | return fmt.Errorf("unknown format %s", format) 83 | } 84 | 85 | return nil 86 | }, 87 | } 88 | 89 | func init() { 90 | exportEnrolledKeysCmd.Flags().StringVar(&exportDir, "dir", "", "directory to write the exported certificates") 91 | exportEnrolledKeysCmd.Flags().StringVar(&format, "format", "der", "the export format. One of \"der\", \"esl\"") 92 | 93 | CliCommands = append(CliCommands, cliCommand{ 94 | Cmd: exportEnrolledKeysCmd, 95 | }) 96 | } 97 | 98 | func ExtractDerFromSignatureDatabase(db *signature.SignatureDatabase) DerList { 99 | result := DerList{} 100 | 101 | for _, c := range *db { 102 | for _, s := range c.Signatures { 103 | switch c.SignatureType { 104 | case signature.CERT_X509_GUID, signature.CERT_SHA256_GUID: 105 | certificates, err := x509.ParseCertificates(s.Data) 106 | if err != nil { 107 | fmt.Println("warning: " + err.Error()) 108 | continue 109 | } 110 | for _, c := range certificates { 111 | result[fileNameForCertificate(c)] = s.Data 112 | } 113 | default: 114 | fmt.Printf("warning: format not implemented - %s\n", c.SignatureType.Format()) 115 | continue 116 | } 117 | } 118 | } 119 | 120 | return result 121 | } 122 | 123 | func fileNameForCertificate(c *x509.Certificate) string { 124 | return fmt.Sprintf("%s_%s", 125 | strings.ReplaceAll(c.Issuer.CommonName, " ", "_"), 126 | c.SerialNumber.String(), 127 | ) 128 | } 129 | 130 | func writeDerFiles(allCerts map[string]DerList, outDir string) error { 131 | for t, certList := range allCerts { 132 | certDir := filepath.Join(outDir, t) 133 | if err := os.MkdirAll(certDir, os.ModePerm); err != nil { 134 | return fmt.Errorf("creating directory %s: %w", certDir, err) 135 | } 136 | for fileName, c := range certList { 137 | certPath := filepath.Join(certDir, fileName) + ".der" 138 | if err := os.WriteFile(certPath, []byte(c), os.ModePerm); err != nil { 139 | return fmt.Errorf("writing file %s: %w", certPath, err) 140 | } 141 | } 142 | } 143 | return nil 144 | } 145 | 146 | // ensureDir resolved any relative path and creates the directory if it does not 147 | // exist. It returns the aboslute path to the created directory or an error if one 148 | // occurs. 149 | func ensureDir(dir string) (string, error) { 150 | if !filepath.IsAbs(dir) { 151 | wd, err := os.Getwd() 152 | if err != nil { 153 | return "", err 154 | } 155 | // Resolve the relative path 156 | dir = filepath.Join(wd, dir) 157 | } 158 | 159 | if _, err := os.Stat(dir); err == nil { 160 | return "", fmt.Errorf("directory already exists") 161 | } 162 | 163 | return dir, os.MkdirAll(dir, 0755) 164 | } 165 | -------------------------------------------------------------------------------- /cmd/sbctl/generate-bundles.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/foxboron/sbctl" 8 | "github.com/foxboron/sbctl/backend" 9 | "github.com/foxboron/sbctl/config" 10 | "github.com/foxboron/sbctl/hierarchy" 11 | "github.com/foxboron/sbctl/logging" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | var ( 16 | sign bool 17 | ) 18 | 19 | var generateBundlesCmd = &cobra.Command{ 20 | Use: "generate-bundles", 21 | Short: "Generate all EFI stub bundles", 22 | RunE: func(cmd *cobra.Command, args []string) error { 23 | state := cmd.Context().Value(stateDataKey{}).(*config.State) 24 | 25 | logging.Errorf("The bundle/uki support in sbctl is deprecated. Please move to dracut/mkinitcpio/ukify.") 26 | 27 | logging.Println("Generating EFI bundles....") 28 | out_create := true 29 | out_sign := true 30 | var out_err error 31 | err := sbctl.BundleIter(state, func(bundle *sbctl.Bundle) error { 32 | err := sbctl.CreateBundle(state, *bundle) 33 | if err != nil { 34 | out_create = false 35 | out_err = fmt.Errorf("failed creating bundle %s: %w", bundle.Output, err) 36 | return nil 37 | } 38 | logging.Print("Wrote EFI bundle %s\n", bundle.Output) 39 | if sign { 40 | file := bundle.Output 41 | kh, err := backend.GetKeyHierarchy(state.Fs, state) 42 | if err != nil { 43 | return err 44 | } 45 | err = sbctl.SignFile(state, kh, hierarchy.Db, file, file) 46 | if errors.Is(err, sbctl.ErrAlreadySigned) { 47 | logging.Unknown("Bundle has already been signed") 48 | } else if err != nil { 49 | out_sign = false 50 | out_err = fmt.Errorf("failed signing bundle %s: %w", bundle.Output, err) 51 | } else { 52 | logging.Ok("Signed %s", file) 53 | } 54 | } 55 | return nil 56 | }) 57 | if !out_create || !out_sign { 58 | return out_err 59 | } 60 | if err != nil { 61 | return err 62 | } 63 | return nil 64 | }, 65 | } 66 | 67 | func generateBundlesCmdFlags(cmd *cobra.Command) { 68 | f := cmd.Flags() 69 | f.BoolVarP(&sign, "sign", "s", false, "Sign all the generated bundles") 70 | } 71 | 72 | func init() { 73 | generateBundlesCmdFlags(generateBundlesCmd) 74 | CliCommands = append(CliCommands, cliCommand{ 75 | Cmd: generateBundlesCmd, 76 | }) 77 | } 78 | -------------------------------------------------------------------------------- /cmd/sbctl/import-keys.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "path" 8 | "path/filepath" 9 | 10 | "github.com/foxboron/go-uefi/efi/util" 11 | "github.com/foxboron/sbctl" 12 | "github.com/foxboron/sbctl/config" 13 | "github.com/foxboron/sbctl/logging" 14 | "github.com/foxboron/sbctl/lsm" 15 | "github.com/landlock-lsm/go-landlock/landlock" 16 | "github.com/spf13/afero" 17 | "github.com/spf13/cobra" 18 | ) 19 | 20 | type ImportKeysCmdOptions struct { 21 | Force bool 22 | DbCert string 23 | DbKey string 24 | KEKCert string 25 | KEKKey string 26 | PKCert string 27 | PKKey string 28 | Directory string 29 | } 30 | 31 | var ( 32 | importKeysCmdOptions = ImportKeysCmdOptions{} 33 | importKeysCmd = &cobra.Command{ 34 | Use: "import-keys", 35 | Short: "Import keys into sbctl", 36 | RunE: RunImportKeys, 37 | } 38 | ) 39 | 40 | func Import(vfs afero.Fs, src, dst string) error { 41 | logging.Print("Importing %s...", src) 42 | if err := os.MkdirAll(filepath.Dir(dst), 0777); err != nil { 43 | logging.NotOk("") 44 | return fmt.Errorf("could not create directory for %q: %w", dst, err) 45 | } 46 | if err := sbctl.CopyFile(vfs, src, dst); err != nil { 47 | logging.NotOk("") 48 | return fmt.Errorf("could not move %s: %w", src, err) 49 | } 50 | logging.Ok("") 51 | return nil 52 | } 53 | 54 | func ImportKeysFromDirectory(state *config.State, dir string) error { 55 | keys := []string{ 56 | "PK/PK.key", 57 | "PK/PK.pem", 58 | "KEK/KEK.key", 59 | "KEK/KEK.pem", 60 | "db/db.key", 61 | "db/db.pem", 62 | } 63 | dir, err := filepath.Abs(dir) 64 | if err != nil { 65 | return err 66 | } 67 | for _, f := range keys { 68 | keyFile := path.Join(dir, f) 69 | if _, err := state.Fs.Stat(keyFile); errors.Is(err, os.ErrNotExist) { 70 | return fmt.Errorf("file does not exist: %s", keyFile) 71 | } 72 | } 73 | 74 | for _, f := range keys { 75 | keyFile := path.Join(dir, f) 76 | dstFile := path.Join(state.Config.Keydir, f) 77 | if err = Import(state.Fs, keyFile, dstFile); err != nil { 78 | return err 79 | } 80 | } 81 | return nil 82 | } 83 | 84 | func RunImportKeys(cmd *cobra.Command, args []string) error { 85 | var err error 86 | keypairs := []struct { 87 | Type string 88 | Key string 89 | Cert string 90 | }{ 91 | {"db", importKeysCmdOptions.DbKey, importKeysCmdOptions.DbCert}, 92 | {"KEK", importKeysCmdOptions.KEKKey, importKeysCmdOptions.KEKCert}, 93 | {"PK", importKeysCmdOptions.PKKey, importKeysCmdOptions.PKCert}, 94 | } 95 | 96 | state := cmd.Context().Value(stateDataKey{}).(*config.State) 97 | 98 | if state.Config.Landlock { 99 | for _, key := range keypairs { 100 | if key.Key != "" { 101 | lsm.RestrictAdditionalPaths( 102 | landlock.RWFiles(key.Key), 103 | ) 104 | } 105 | if key.Cert != "" { 106 | lsm.RestrictAdditionalPaths( 107 | landlock.RWFiles(key.Cert), 108 | ) 109 | } 110 | } 111 | 112 | if importKeysCmdOptions.Directory != "" { 113 | lsm.RestrictAdditionalPaths( 114 | landlock.ROFiles(importKeysCmdOptions.Directory), 115 | ) 116 | } 117 | 118 | if err := lsm.Restrict(); err != nil { 119 | return err 120 | } 121 | } 122 | 123 | if importKeysCmdOptions.Directory != "" { 124 | _, err := state.Fs.Stat(state.Config.Keydir) 125 | if err == nil && !importKeysCmdOptions.Force { 126 | return fmt.Errorf("key directory exists. Use --force to overwrite the current directory") 127 | } 128 | return ImportKeysFromDirectory(state, importKeysCmdOptions.Directory) 129 | } 130 | for _, key := range keypairs { 131 | if key.Key == "" && key.Cert == "" { 132 | continue 133 | } 134 | 135 | for _, s := range []string{key.Key, key.Cert} { 136 | if _, err = state.Fs.Stat(s); errors.Is(err, os.ErrNotExist) { 137 | return fmt.Errorf("keyfile %s does not exist", s) 138 | } 139 | } 140 | 141 | if !importKeysCmdOptions.Force { 142 | if _, err := util.ReadCertFromFile(key.Cert); err != nil { 143 | return fmt.Errorf("invalid certificate file") 144 | } 145 | if _, err := util.ReadKeyFromFile(key.Key); err != nil { 146 | return fmt.Errorf("invalid private key file") 147 | } 148 | } 149 | 150 | for src, dst := range map[string]string{ 151 | key.Cert: path.Join(state.Config.Keydir, key.Type, key.Type+".pem"), 152 | key.Key: path.Join(state.Config.Keydir, key.Type, key.Type+".key"), 153 | } { 154 | srcFile, err := filepath.Abs(src) 155 | if err != nil { 156 | return err 157 | } 158 | if err = Import(state.Fs, srcFile, dst); err != nil { 159 | return err 160 | } 161 | } 162 | } 163 | return nil 164 | } 165 | 166 | func importKeysCmdFlags(cmd *cobra.Command) { 167 | f := cmd.Flags() 168 | f.StringVarP(&importKeysCmdOptions.DbCert, "db-cert", "", "", "Database (db) certificate") 169 | f.StringVarP(&importKeysCmdOptions.DbKey, "db-key", "", "", "Database (db) key") 170 | f.StringVarP(&importKeysCmdOptions.KEKCert, "kek-cert", "", "", "Key Exchange Key (KEK) certificate") 171 | f.StringVarP(&importKeysCmdOptions.KEKKey, "kek-key", "", "", "Key Exchange Key (KEK) key") 172 | f.StringVarP(&importKeysCmdOptions.PKCert, "pk-cert", "", "", "Platform Key (PK) certificate") 173 | f.StringVarP(&importKeysCmdOptions.PKKey, "pk-key", "", "", "Platform Key (PK) key") 174 | f.StringVarP(&importKeysCmdOptions.Directory, "directory", "d", "", "Import keys from a directory") 175 | f.BoolVarP(&importKeysCmdOptions.Force, "force", "", false, "Force import") 176 | } 177 | 178 | func init() { 179 | importKeysCmdFlags(importKeysCmd) 180 | CliCommands = append(CliCommands, cliCommand{ 181 | Cmd: importKeysCmd, 182 | }) 183 | } 184 | -------------------------------------------------------------------------------- /cmd/sbctl/list-bundles.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/foxboron/sbctl" 8 | "github.com/foxboron/sbctl/backend" 9 | "github.com/foxboron/sbctl/config" 10 | "github.com/foxboron/sbctl/hierarchy" 11 | "github.com/foxboron/sbctl/logging" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | type JsonBundle struct { 16 | sbctl.Bundle 17 | IsSigned bool `json:"is_signed"` 18 | } 19 | 20 | var listBundlesCmd = &cobra.Command{ 21 | Use: "list-bundles", 22 | Aliases: []string{ 23 | "ls-bundles", 24 | }, 25 | Short: "List stored bundles", 26 | RunE: func(cmd *cobra.Command, args []string) error { 27 | state := cmd.Context().Value(stateDataKey{}).(*config.State) 28 | 29 | logging.Errorf("The bundle/uki support in sbctl is deprecated. Please move to dracut/mkinitcpio/ukify.") 30 | 31 | // if state.Config.Landlock { 32 | // if err := lsm.Restrict(); err != nil { 33 | // return err 34 | // } 35 | // } 36 | 37 | bundles := []JsonBundle{} 38 | var isSigned bool 39 | err := sbctl.BundleIter(state, 40 | func(s *sbctl.Bundle) error { 41 | kh, err := backend.GetKeyHierarchy(state.Fs, state) 42 | if err != nil { 43 | return err 44 | } 45 | ok, err := sbctl.VerifyFile(state, kh, hierarchy.Db, s.Output) 46 | if err != nil { 47 | logging.Error(fmt.Errorf("%s: %w", s.Output, err)) 48 | logging.Error(fmt.Errorf("")) 49 | return nil 50 | } 51 | logging.Println("Enrolled bundles:\n") 52 | logging.Println(s.Output) 53 | logging.Print("\tSigned:\t\t") 54 | if ok { 55 | isSigned = true 56 | logging.Ok("Signed") 57 | } else { 58 | isSigned = false 59 | logging.NotOk("Not Signed") 60 | } 61 | esp, err := sbctl.GetESP(state.Fs) 62 | if err != nil { 63 | return err 64 | } 65 | logging.Print("\tESP Location:\t%s\n", esp) 66 | logging.Print("\tOutput:\t\t└─%s\n", strings.TrimPrefix(s.Output, esp)) 67 | logging.Print("\tEFI Stub Image:\t └─%s\n", s.EFIStub) 68 | if s.Splash != "" { 69 | logging.Print("\tSplash Image:\t ├─%s\n", s.Splash) 70 | } 71 | logging.Print("\tCmdline:\t ├─%s\n", s.Cmdline) 72 | logging.Print("\tOS Release:\t ├─%s\n", s.OSRelease) 73 | logging.Print("\tKernel Image:\t ├─%s\n", s.KernelImage) 74 | logging.Print("\tInitramfs Image: └─%s\n", s.Initramfs) 75 | if s.AMDMicrocode != "" { 76 | logging.Print("\tAMD Microcode: └─%s\n", s.AMDMicrocode) 77 | } 78 | if s.IntelMicrocode != "" { 79 | logging.Print("\tIntel Microcode: └─%s\n", s.IntelMicrocode) 80 | } 81 | bundles = append(bundles, JsonBundle{*s, isSigned}) 82 | logging.Println("") 83 | return nil 84 | }) 85 | if err != nil { 86 | return err 87 | } 88 | if cmdOptions.JsonOutput { 89 | return JsonOut(bundles) 90 | } 91 | return nil 92 | }, 93 | } 94 | 95 | func init() { 96 | CliCommands = append(CliCommands, cliCommand{ 97 | Cmd: listBundlesCmd, 98 | }) 99 | } 100 | -------------------------------------------------------------------------------- /cmd/sbctl/list-enrolled-keys.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/x509" 5 | "fmt" 6 | 7 | "github.com/foxboron/go-uefi/efi" 8 | "github.com/foxboron/go-uefi/efi/signature" 9 | "github.com/foxboron/go-uefi/efi/util" 10 | "github.com/foxboron/sbctl/config" 11 | "github.com/foxboron/sbctl/lsm" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | var listKeysCmd = &cobra.Command{ 16 | Use: "list-enrolled-keys", 17 | Aliases: []string{ 18 | "ls-enrolled-keys", 19 | }, 20 | Short: "List enrolled keys on the system", 21 | RunE: func(cmd *cobra.Command, args []string) error { 22 | state := cmd.Context().Value(stateDataKey{}).(*config.State) 23 | if state.Config.Landlock { 24 | if err := lsm.Restrict(); err != nil { 25 | return err 26 | } 27 | } 28 | 29 | var err error 30 | certList := map[string]([]*x509.Certificate){} 31 | 32 | pk, err := efi.GetPK() 33 | if err != nil { 34 | return err 35 | } 36 | kek, err := efi.GetKEK() 37 | if err != nil { 38 | return err 39 | } 40 | db, err := efi.Getdb() 41 | if err != nil { 42 | return err 43 | } 44 | 45 | certList["PK"] = ExtractCertsFromSignatureDatabase(pk) 46 | certList["KEK"] = ExtractCertsFromSignatureDatabase(kek) 47 | certList["DB"] = ExtractCertsFromSignatureDatabase(db) 48 | 49 | if cmdOptions.JsonOutput { 50 | return JsonOut(certList) 51 | } 52 | 53 | printCertsPlainText(certList) 54 | 55 | return nil 56 | }, 57 | } 58 | 59 | func init() { 60 | CliCommands = append(CliCommands, cliCommand{ 61 | Cmd: listKeysCmd, 62 | }) 63 | } 64 | 65 | // ExtractCertsFromSignatureDatabase returns a []*x509.Certificate from a *signature.SignatureDatabase 66 | func ExtractCertsFromSignatureDatabase(database *signature.SignatureDatabase) []*x509.Certificate { 67 | var result []*x509.Certificate 68 | for _, k := range *database { 69 | if isValidSignature(k.SignatureType) { 70 | for _, k1 := range k.Signatures { 71 | // Note the S at the end of the function, we are parsing multiple certs, not just one 72 | certificates, err := x509.ParseCertificates(k1.Data) 73 | if err != nil { 74 | continue 75 | } 76 | result = append(result, certificates...) 77 | } 78 | } 79 | } 80 | return result 81 | } 82 | 83 | // isValidSignature identifies a signature based as a DER-encoded X.509 certificate 84 | func isValidSignature(sign util.EFIGUID) bool { 85 | return sign == signature.CERT_X509_GUID 86 | } 87 | 88 | func printCertsPlainText(certList map[string][]*x509.Certificate) { 89 | for db, certs := range certList { 90 | fmt.Printf("%s:\n", db) 91 | for _, c := range certs { 92 | fmt.Printf(" %s\n", c.Issuer.CommonName) 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /cmd/sbctl/list-files.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/foxboron/sbctl" 7 | "github.com/foxboron/sbctl/backend" 8 | "github.com/foxboron/sbctl/config" 9 | "github.com/foxboron/sbctl/hierarchy" 10 | "github.com/foxboron/sbctl/logging" 11 | "github.com/foxboron/sbctl/lsm" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | var listFilesCmd = &cobra.Command{ 16 | Use: "list-files", 17 | Aliases: []string{ 18 | "ls-files", 19 | "ls", 20 | }, 21 | Short: "List enrolled files", 22 | RunE: RunList, 23 | } 24 | 25 | type JsonFile struct { 26 | sbctl.SigningEntry 27 | IsSigned bool `json:"is_signed"` 28 | } 29 | 30 | func RunList(cmd *cobra.Command, args []string) error { 31 | state := cmd.Context().Value(stateDataKey{}).(*config.State) 32 | 33 | if state.Config.Landlock { 34 | if err := sbctl.LandlockFromFileDatabase(state); err != nil { 35 | return err 36 | } 37 | if err := lsm.Restrict(); err != nil { 38 | return err 39 | } 40 | } 41 | 42 | files := []JsonFile{} 43 | var isSigned bool 44 | err := sbctl.SigningEntryIter(state, 45 | func(s *sbctl.SigningEntry) error { 46 | kh, err := backend.GetKeyHierarchy(state.Fs, state) 47 | if err != nil { 48 | return err 49 | } 50 | ok, err := sbctl.VerifyFile(state, kh, hierarchy.Db, s.OutputFile) 51 | if err != nil { 52 | logging.Error(fmt.Errorf("%s: %w", s.OutputFile, err)) 53 | logging.Error(fmt.Errorf("")) 54 | return nil 55 | } 56 | logging.Println(s.File) 57 | logging.Print("Signed:\t\t") 58 | if ok { 59 | isSigned = true 60 | logging.Ok("Signed") 61 | } else if !ok { 62 | isSigned = false 63 | logging.NotOk("Not Signed") 64 | } 65 | if s.File != s.OutputFile { 66 | logging.Print("Output File:\t%s\n", s.OutputFile) 67 | } 68 | logging.Println("") 69 | files = append(files, JsonFile{*s, isSigned}) 70 | return nil 71 | }, 72 | ) 73 | if err != nil { 74 | return err 75 | } 76 | if cmdOptions.JsonOutput { 77 | return JsonOut(files) 78 | } 79 | return nil 80 | } 81 | 82 | func init() { 83 | CliCommands = append(CliCommands, cliCommand{ 84 | Cmd: listFilesCmd, 85 | }) 86 | } 87 | -------------------------------------------------------------------------------- /cmd/sbctl/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "log" 9 | "log/slog" 10 | "os" 11 | "strings" 12 | 13 | "github.com/foxboron/go-uefi/efivarfs" 14 | "github.com/foxboron/sbctl" 15 | "github.com/foxboron/sbctl/config" 16 | "github.com/foxboron/sbctl/logging" 17 | "github.com/foxboron/sbctl/lsm" 18 | "github.com/google/go-tpm/tpm2/transport" 19 | "github.com/spf13/afero" 20 | "github.com/spf13/cobra" 21 | ) 22 | 23 | type CmdOptions struct { 24 | JsonOutput bool 25 | QuietOutput bool 26 | Config string 27 | DisableLandlock bool 28 | Debug bool 29 | } 30 | 31 | type cliCommand struct { 32 | Cmd *cobra.Command 33 | } 34 | 35 | type stateDataKey struct{} 36 | 37 | var ( 38 | cmdOptions = CmdOptions{} 39 | CliCommands = []cliCommand{} 40 | ErrSilent = errors.New("SilentErr") 41 | rootCmd = &cobra.Command{ 42 | Use: "sbctl", 43 | Short: "Secure Boot Key Manager", 44 | SilenceUsage: true, 45 | SilenceErrors: true, 46 | } 47 | baseErrorMsg = ` 48 | 49 | There are three flags that can be used: 50 | --microsoft: Enroll the Microsoft OEM certificates into the signature database. 51 | --tpm-eventlog: Enroll OpRom checksums into the signature database (experimental!). 52 | --yes-this-might-brick-my-machine: Ignore this warning and continue regardless. 53 | 54 | Please read the FAQ for more information: https://github.com/Foxboron/sbctl/wiki/FAQ#option-rom` 55 | opromErrorMsg = `Found OptionROM in the bootchain. This means we should not enroll keys into UEFI without some precautions.` + baseErrorMsg 56 | noEventlogErrorMsg = `Could not find any TPM Eventlog in the system. This means we do not know if there is any OptionROM present on the system.` + baseErrorMsg 57 | setupModeDisabled = `Your system is not in Setup Mode! Please reboot your machine and reset secure boot keys before attempting to enroll the keys.` 58 | ) 59 | 60 | func baseFlags(cmd *cobra.Command) { 61 | flags := cmd.PersistentFlags() 62 | flags.BoolVar(&cmdOptions.JsonOutput, "json", false, "Output as json") 63 | flags.BoolVar(&cmdOptions.QuietOutput, "quiet", false, "Mute info from logging") 64 | flags.BoolVar(&cmdOptions.DisableLandlock, "disable-landlock", false, "Disable landlock sandboxing") 65 | flags.BoolVar(&cmdOptions.Debug, "debug", false, "Enable verbose debug logging") 66 | flags.StringVarP(&cmdOptions.Config, "config", "", "", "Path to configuration file") 67 | } 68 | 69 | func JsonOut(v interface{}) error { 70 | b, err := json.MarshalIndent(v, "", " ") 71 | if err != nil { 72 | return fmt.Errorf("could not marshal json: %w", err) 73 | } 74 | logging.PrintOn() 75 | logging.Println(string(b)) 76 | // Json should always be the last print call, but lets safe it :) 77 | logging.PrintOff() 78 | return nil 79 | } 80 | 81 | func main() { 82 | for _, cmd := range CliCommands { 83 | rootCmd.AddCommand(cmd.Cmd) 84 | } 85 | 86 | fs := afero.NewOsFs() 87 | 88 | baseFlags(rootCmd) 89 | 90 | // We save tpmerr and print it when we can print debug messages 91 | rwc, tpmerr := transport.OpenTPM() 92 | if tpmerr == nil { 93 | defer rwc.Close() 94 | } 95 | 96 | // We need to set this after we have parsed stuff 97 | rootCmd.PersistentPreRunE = func(cmd *cobra.Command, _ []string) error { 98 | state := &config.State{ 99 | Fs: fs, 100 | TPM: func() transport.TPMCloser { 101 | return rwc 102 | }, 103 | Efivarfs: efivarfs.NewFS(). 104 | CheckImmutable(). 105 | UnsetImmutable(). 106 | Open(), 107 | } 108 | 109 | var conf *config.Config 110 | 111 | if cmdOptions.Config != "" { 112 | b, err := os.ReadFile(cmdOptions.Config) 113 | if err != nil { 114 | return err 115 | } 116 | conf, err = config.NewConfig(b) 117 | if err != nil { 118 | return err 119 | } 120 | 121 | state.Config = conf 122 | 123 | // TODO: Do we want to overwrite the provided configuration with out existing keys? 124 | // something to figure out 125 | // kh, err := backend.GetKeyHierarchy(fs, state) 126 | // if err != nil { 127 | // return err 128 | // } 129 | // state.Config.Keys = kh.GetConfig(state.Config.Keydir) 130 | // state.Config.DbAdditions = sbctl.GetEnrolledVendorCerts() 131 | } else { 132 | if config.HasOldConfig(fs, sbctl.DatabasePath) && !config.HasConfigurationFile(fs, "/etc/sbctl/sbctl.conf") { 133 | logging.Error(fmt.Errorf("old configuration detected. Please use `sbctl setup --migrate`")) 134 | conf = config.OldConfig(sbctl.DatabasePath) 135 | state.Config = conf 136 | } else if ok, _ := afero.Exists(fs, "/etc/sbctl/sbctl.conf"); ok { 137 | b, err := os.ReadFile("/etc/sbctl/sbctl.conf") 138 | if err != nil { 139 | log.Fatal(err) 140 | } 141 | conf, err = config.NewConfig(b) 142 | if err != nil { 143 | log.Fatal(err) 144 | } 145 | state.Config = conf 146 | } else { 147 | conf = config.DefaultConfig() 148 | state.Config = conf 149 | } 150 | } 151 | 152 | if cmdOptions.JsonOutput { 153 | logging.PrintOff() 154 | } 155 | if cmdOptions.QuietOutput { 156 | logging.DisableInfo = true 157 | } 158 | if cmdOptions.DisableLandlock { 159 | state.Config.Landlock = false 160 | } 161 | 162 | // Setup debug logging 163 | opts := &slog.HandlerOptions{ 164 | Level: slog.LevelInfo, 165 | } 166 | if cmdOptions.Debug { 167 | opts.Level = slog.LevelDebug 168 | } 169 | logger := slog.New(slog.NewTextHandler(os.Stdout, opts)) 170 | slog.SetDefault(logger) 171 | 172 | if !state.HasTPM() { 173 | slog.Debug("can't open tpm", slog.Any("err", tpmerr)) 174 | } 175 | 176 | if state.Config.Landlock { 177 | lsm.LandlockRulesFromConfig(state.Config) 178 | } 179 | ctx := context.WithValue(cmd.Context(), stateDataKey{}, state) 180 | cmd.SetContext(ctx) 181 | return nil 182 | } 183 | 184 | // This returns i the flag is not found with a specific error 185 | rootCmd.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error { 186 | cmd.Println(err) 187 | cmd.Println(cmd.UsageString()) 188 | return ErrSilent 189 | }) 190 | 191 | if err := rootCmd.Execute(); err != nil { 192 | if strings.HasPrefix(err.Error(), "unknown command") { 193 | logging.Println(err.Error()) 194 | } else if errors.Is(err, os.ErrPermission) { 195 | logging.Error(fmt.Errorf("sbctl requires root to run: %w", err)) 196 | } else if errors.Is(err, sbctl.ErrImmutable) { 197 | logging.Println("You need to chattr -i files in efivarfs") 198 | } else if errors.Is(err, sbctl.ErrOprom) { 199 | logging.Error(errors.New(opromErrorMsg)) 200 | } else if errors.Is(err, sbctl.ErrNoEventlog) { 201 | logging.Error(errors.New(noEventlogErrorMsg)) 202 | } else if errors.Is(err, ErrSetupModeDisabled) { 203 | logging.Error(errors.New(setupModeDisabled)) 204 | } else if !errors.Is(err, ErrSilent) { 205 | logging.Error(err) 206 | } 207 | os.Exit(1) 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /cmd/sbctl/remove-bundle.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/foxboron/sbctl" 7 | "github.com/foxboron/sbctl/config" 8 | "github.com/foxboron/sbctl/logging" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | var removeBundleCmd = &cobra.Command{ 13 | Use: "remove-bundle", 14 | Aliases: []string{ 15 | "rm-bundle", 16 | }, 17 | Short: "Remove bundle from database", 18 | RunE: func(cmd *cobra.Command, args []string) error { 19 | state := cmd.Context().Value(stateDataKey{}).(*config.State) 20 | 21 | logging.Errorf("The bundle/uki support in sbctl is deprecated. Please move to dracut/mkinitcpio/ukify.") 22 | 23 | if len(args) < 1 { 24 | logging.Print("Need to specify file\n") 25 | os.Exit(1) 26 | } 27 | bundles, err := sbctl.ReadBundleDatabase(state.Fs, state.Config.BundlesDb) 28 | if err != nil { 29 | return err 30 | } 31 | 32 | if _, ok := bundles[args[0]]; !ok { 33 | logging.Print("Bundle %s doesn't exist in database!\n", args[0]) 34 | os.Exit(1) 35 | } 36 | delete(bundles, args[0]) 37 | err = sbctl.WriteBundleDatabase(state.Fs, state.Config.BundlesDb, bundles) 38 | if err != nil { 39 | return err 40 | } 41 | logging.Print("Removed %s from the database.\n", args[0]) 42 | return nil 43 | }, 44 | } 45 | 46 | func init() { 47 | CliCommands = append(CliCommands, cliCommand{ 48 | Cmd: removeBundleCmd, 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /cmd/sbctl/remove-file.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/foxboron/sbctl" 7 | "github.com/foxboron/sbctl/config" 8 | "github.com/foxboron/sbctl/logging" 9 | "github.com/foxboron/sbctl/lsm" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | var removeFileCmd = &cobra.Command{ 14 | Use: "remove-file", 15 | Aliases: []string{ 16 | "rm-file", 17 | "rm", 18 | }, 19 | Short: "Remove file from database", 20 | RunE: func(cmd *cobra.Command, args []string) error { 21 | state := cmd.Context().Value(stateDataKey{}).(*config.State) 22 | 23 | if state.Config.Landlock { 24 | if err := lsm.Restrict(); err != nil { 25 | return err 26 | } 27 | } 28 | 29 | if len(args) < 1 { 30 | logging.Println("Need to specify file") 31 | os.Exit(1) 32 | } 33 | files, err := sbctl.ReadFileDatabase(state.Fs, state.Config.FilesDb) 34 | if err != nil { 35 | return err 36 | } 37 | if _, ok := files[args[0]]; !ok { 38 | logging.Print("File %s doesn't exist in database!\n", args[0]) 39 | os.Exit(1) 40 | } 41 | delete(files, args[0]) 42 | if err := sbctl.WriteFileDatabase(state.Fs, state.Config.FilesDb, files); err != nil { 43 | return err 44 | } 45 | logging.Print("Removed %s from the database.\n", args[0]) 46 | return nil 47 | }, 48 | } 49 | 50 | func init() { 51 | CliCommands = append(CliCommands, cliCommand{ 52 | Cmd: removeFileCmd, 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /cmd/sbctl/reset.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/foxboron/go-uefi/efi/signature" 8 | "github.com/foxboron/go-uefi/efi/util" 9 | "github.com/foxboron/go-uefi/efivar" 10 | "github.com/foxboron/sbctl" 11 | "github.com/foxboron/sbctl/backend" 12 | "github.com/foxboron/sbctl/config" 13 | "github.com/foxboron/sbctl/fs" 14 | "github.com/foxboron/sbctl/logging" 15 | "github.com/foxboron/sbctl/lsm" 16 | "github.com/foxboron/sbctl/stringset" 17 | "github.com/spf13/cobra" 18 | ) 19 | 20 | type resetCmdOptions struct { 21 | Partial stringset.StringSet 22 | CertFiles string 23 | } 24 | 25 | var ( 26 | resetCmdOpts = resetCmdOptions{ 27 | Partial: stringset.StringSet{Allowed: []string{"PK", "KEK", "db"}}, 28 | } 29 | resetCmd = &cobra.Command{ 30 | Use: "reset", 31 | Short: "Reset Secure Boot Keys", 32 | RunE: RunReset, 33 | } 34 | ) 35 | 36 | func resetKeys(state *config.State) error { 37 | if resetCmdOpts.Partial.Value == "" { 38 | if err := resetPK(state); err != nil { 39 | return fmt.Errorf("could not reset PK: %v", err) 40 | } 41 | 42 | return nil 43 | } 44 | 45 | var paths []string 46 | 47 | if resetCmdOpts.CertFiles != "" { 48 | paths = strings.Split(resetCmdOpts.CertFiles, ";") 49 | } 50 | 51 | switch partial := resetCmdOpts.Partial.Value; partial { 52 | case "db": 53 | if err := resetDB(state, paths...); err != nil { 54 | return err 55 | } 56 | case "KEK": 57 | if err := resetKEK(state, paths...); err != nil { 58 | return err 59 | } 60 | case "PK": 61 | if err := resetPK(state, paths...); err != nil { 62 | return err 63 | } 64 | default: 65 | return fmt.Errorf("unsupported type to reset: %s, allowed values are: %s", partial, enrollKeysCmdOptions.Partial.Type()) 66 | } 67 | 68 | return nil 69 | } 70 | 71 | func resetDB(state *config.State, certPaths ...string) error { 72 | if err := resetDatabase(state, efivar.Db, certPaths...); err != nil { 73 | return err 74 | } 75 | 76 | logging.Ok("Removed Signature Database!") 77 | logging.Println("Use `sbctl enroll-keys` to enroll the Signature Database again.") 78 | return nil 79 | } 80 | 81 | func resetKEK(state *config.State, certPaths ...string) error { 82 | if err := resetDatabase(state, efivar.KEK, certPaths...); err != nil { 83 | return err 84 | } 85 | 86 | logging.Ok("Removed Key Exchange Keys!") 87 | logging.Println("Use `sbctl enroll-keys` to enroll a Key Exchange Key again.") 88 | return nil 89 | } 90 | 91 | func resetPK(state *config.State, certPaths ...string) error { 92 | if err := resetDatabase(state, efivar.PK, certPaths...); err != nil { 93 | return err 94 | } 95 | 96 | logging.Ok("Removed Platform Key!") 97 | logging.Println("Use `sbctl enroll-keys` to enroll the Platform Key again.") 98 | return nil 99 | } 100 | 101 | func resetDatabase(state *config.State, ev efivar.Efivar, certPaths ...string) error { 102 | efistate, err := sbctl.SystemEFIVariables(state.Efivarfs) 103 | if err != nil { 104 | return err 105 | } 106 | 107 | db := signature.NewSignatureDatabase() 108 | 109 | if len(certPaths) != 0 { 110 | var ( 111 | err error 112 | ) 113 | 114 | db = efistate.GetSiglist(ev) 115 | 116 | guid, err := state.Config.GetGUID(state.Fs) 117 | if err != nil { 118 | return err 119 | } 120 | 121 | for _, certPath := range certPaths { 122 | buf, err := fs.ReadFile(state.Fs, certPath) 123 | if err != nil { 124 | return fmt.Errorf("can't read new certificate from path %s: %v", certPath, err) 125 | } 126 | 127 | cert, err := util.ReadCert(buf) 128 | if err != nil { 129 | return err 130 | } 131 | if err := db.Remove(signature.CERT_X509_GUID, *guid, cert.Raw); err != nil { 132 | return err 133 | } 134 | 135 | } 136 | } 137 | 138 | kh, err := backend.GetKeyHierarchy(state.Fs, state) 139 | if err != nil { 140 | return err 141 | } 142 | 143 | switch ev { 144 | case efivar.PK: 145 | efistate.PK = db 146 | case efivar.KEK: 147 | efistate.KEK = db 148 | case efivar.Db: 149 | efistate.Db = db 150 | } 151 | 152 | if err := efistate.EnrollKey(ev, kh); err != nil { 153 | return err 154 | } 155 | 156 | return nil 157 | } 158 | 159 | func RunReset(cmd *cobra.Command, args []string) error { 160 | state := cmd.Context().Value(stateDataKey{}).(*config.State) 161 | if state.Config.Landlock { 162 | if err := lsm.Restrict(); err != nil { 163 | return err 164 | } 165 | } 166 | if err := resetKeys(state); err != nil { 167 | return err 168 | } 169 | return nil 170 | } 171 | 172 | func resetKeysCmdFlags(cmd *cobra.Command) { 173 | f := cmd.Flags() 174 | f.VarPF(&resetCmdOpts.Partial, "partial", "p", "reset a partial set of keys") 175 | f.StringVarP(&resetCmdOpts.CertFiles, "cert-files", "c", "", "optional paths to certificate file to remove from the hierachy (seperate individual paths by ';')") 176 | } 177 | 178 | func init() { 179 | resetKeysCmdFlags(resetCmd) 180 | CliCommands = append(CliCommands, cliCommand{ 181 | Cmd: resetCmd, 182 | }) 183 | } 184 | -------------------------------------------------------------------------------- /cmd/sbctl/setup.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path" 7 | "path/filepath" 8 | 9 | "github.com/foxboron/sbctl" 10 | "github.com/foxboron/sbctl/backend" 11 | "github.com/foxboron/sbctl/config" 12 | "github.com/foxboron/sbctl/logging" 13 | "github.com/foxboron/sbctl/lsm" 14 | "github.com/goccy/go-yaml" 15 | "github.com/landlock-lsm/go-landlock/landlock" 16 | "github.com/spf13/afero" 17 | "github.com/spf13/cobra" 18 | ) 19 | 20 | type SetupCmdOptions struct { 21 | PrintConfig bool 22 | PrintState bool 23 | Migrate bool 24 | Setup bool 25 | } 26 | 27 | var ( 28 | setupCmdOptions = SetupCmdOptions{} 29 | setupCmd = &cobra.Command{ 30 | Use: "setup", 31 | Short: "Setup sbctl", 32 | RunE: RunSetup, 33 | } 34 | ) 35 | 36 | func PrintConfig(state *config.State) error { 37 | if state.Config.Landlock { 38 | if err := lsm.Restrict(); err != nil { 39 | return err 40 | } 41 | } 42 | var ser any 43 | if cmdOptions.Config != "" { 44 | b, err := os.ReadFile(cmdOptions.Config) 45 | if err != nil { 46 | return err 47 | } 48 | state.Config, err = config.NewConfig(b) 49 | if err != nil { 50 | return err 51 | } 52 | } else { 53 | kh, err := backend.GetKeyHierarchy(state.Fs, state) 54 | if err != nil { 55 | return err 56 | } 57 | state.Config.Keys = kh.GetConfig(state.Config.Keydir) 58 | state.Config.DbAdditions = sbctl.GetEnrolledVendorCerts() 59 | } 60 | 61 | // Setup the files 62 | if ok, _ := afero.Exists(state.Fs, state.Config.FilesDb); ok { 63 | var files []*config.FileConfig 64 | if err := sbctl.SigningEntryIter(state, 65 | func(s *sbctl.SigningEntry) error { 66 | files = append(files, &config.FileConfig{ 67 | Path: s.File, 68 | Output: s.OutputFile, 69 | }) 70 | return nil 71 | }); err != nil { 72 | return err 73 | } 74 | state.Config.Files = files 75 | } 76 | 77 | ser = state.Config 78 | if setupCmdOptions.PrintState && !cmdOptions.JsonOutput { 79 | return fmt.Errorf("can only use --print-state with --json") 80 | } 81 | 82 | if setupCmdOptions.PrintState { 83 | ser = state 84 | } else { 85 | ser = state.Config 86 | } 87 | 88 | if cmdOptions.JsonOutput { 89 | if err := JsonOut(ser); err != nil { 90 | return err 91 | } 92 | return nil 93 | } 94 | b, err := yaml.Marshal(ser) 95 | if err != nil { 96 | return err 97 | } 98 | 99 | fmt.Print(string(b)) 100 | return nil 101 | } 102 | 103 | func SetupInstallation(state *config.State) error { 104 | if state.Config.Landlock { 105 | if err := sbctl.LandlockFromFileDatabase(state); err != nil { 106 | return err 107 | } 108 | if err := lsm.Restrict(); err != nil { 109 | return err 110 | } 111 | } 112 | 113 | if ok, _ := state.Efivarfs.GetSetupMode(); !ok { 114 | return ErrSetupModeDisabled 115 | } 116 | if state.IsInstalled() { 117 | return fmt.Errorf("sbctl is already installed") 118 | } 119 | 120 | if err := RunCreateKeys(state); err != nil { 121 | return err 122 | } 123 | 124 | if err := RunEnrollKeys(state); err != nil { 125 | return err 126 | } 127 | 128 | if len(state.Config.Files) == 0 { 129 | return nil 130 | } 131 | 132 | files, err := sbctl.ReadFileDatabase(state.Fs, state.Config.FilesDb) 133 | if err != nil { 134 | return err 135 | } 136 | 137 | for _, f := range state.Config.Files { 138 | if f.Output == "" { 139 | f.Output = f.Path 140 | } 141 | files[f.Path] = &sbctl.SigningEntry{File: f.Path, OutputFile: f.Output} 142 | } 143 | 144 | if err := sbctl.WriteFileDatabase(state.Fs, state.Config.FilesDb, files); err != nil { 145 | return err 146 | } 147 | 148 | if err := SignAll(state); err != nil { 149 | return err 150 | } 151 | 152 | return nil 153 | } 154 | 155 | func MigrateSetup(state *config.State) error { 156 | if !state.IsInstalled() { 157 | return fmt.Errorf("sbctl is not installed") 158 | } 159 | 160 | newConf := config.DefaultConfig() 161 | p := path.Dir(newConf.Keydir) 162 | 163 | // abort early if it exists 164 | if ok, _ := afero.DirExists(state.Fs, newConf.Keydir); ok { 165 | logging.Println("sbctl has already been migrated!") 166 | return nil 167 | } 168 | 169 | if err := state.Fs.MkdirAll(p, os.ModePerm); err != nil { 170 | return err 171 | } 172 | 173 | if state.Config.Landlock { 174 | lsm.RestrictAdditionalPaths( 175 | landlock.RWDirs(filepath.Dir(filepath.Clean(sbctl.DatabasePath))), 176 | landlock.RWDirs(p), 177 | ) 178 | if err := lsm.Restrict(); err != nil { 179 | return err 180 | } 181 | } 182 | 183 | // If state.Config.Keydir is the same as sbctl.DatabasePath 184 | // we dont need to do anything 185 | if sbctl.DatabasePath == p { 186 | logging.Println("Nothing to be done!") 187 | return nil 188 | } 189 | 190 | logging.Print("Moving files...") 191 | if err := sbctl.CopyDirectory(state.Fs, sbctl.DatabasePath, p); err != nil { 192 | logging.NotOk("") 193 | return err 194 | } 195 | 196 | if err := state.Fs.Rename(path.Join(p, "files.db"), newConf.FilesDb); err != nil { 197 | return err 198 | } 199 | if ok, _ := afero.Exists(state.Fs, path.Join(p, "bundles.db")); ok { 200 | if err := state.Fs.Rename(path.Join(p, "bundles.db"), newConf.BundlesDb); err != nil { 201 | return err 202 | } 203 | } 204 | logging.Ok("") 205 | 206 | if err := state.Fs.RemoveAll(sbctl.DatabasePath); err != nil { 207 | return err 208 | } 209 | return nil 210 | } 211 | 212 | func RunSetup(cmd *cobra.Command, args []string) error { 213 | state := cmd.Context().Value(stateDataKey{}).(*config.State) 214 | 215 | if setupCmdOptions.Setup { 216 | if err := SetupInstallation(state); err != nil { 217 | return err 218 | } 219 | } 220 | 221 | if setupCmdOptions.Migrate { 222 | if err := MigrateSetup(state); err != nil { 223 | return err 224 | } 225 | } 226 | 227 | if setupCmdOptions.PrintConfig || setupCmdOptions.PrintState { 228 | return PrintConfig(state) 229 | } 230 | 231 | return nil 232 | } 233 | 234 | func setupCmdFlags(cmd *cobra.Command) { 235 | f := cmd.Flags() 236 | f.BoolVarP(&setupCmdOptions.PrintConfig, "print-config", "", false, "print config file") 237 | f.BoolVarP(&setupCmdOptions.PrintState, "print-state", "", false, "print the state of sbctl") 238 | f.BoolVarP(&setupCmdOptions.Migrate, "migrate", "", false, "migrate the sbctl installation") 239 | f.BoolVarP(&setupCmdOptions.Setup, "setup", "", false, "setup the sbctl installation") 240 | cmd.MarkFlagsOneRequired("print-config", "print-state", "migrate", "setup") 241 | cmd.MarkFlagsMutuallyExclusive("print-config", "print-state", "migrate", "setup") 242 | } 243 | 244 | func init() { 245 | setupCmdFlags(setupCmd) 246 | CliCommands = append(CliCommands, cliCommand{ 247 | Cmd: setupCmd, 248 | }) 249 | } 250 | -------------------------------------------------------------------------------- /cmd/sbctl/setup_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | "testing/fstest" 7 | 8 | "github.com/foxboron/go-uefi/efi/efitest" 9 | "github.com/foxboron/go-uefi/efi/signature" 10 | "github.com/foxboron/go-uefi/efivarfs/testfs" 11 | "github.com/foxboron/sbctl" 12 | "github.com/foxboron/sbctl/backend" 13 | "github.com/foxboron/sbctl/config" 14 | "github.com/foxboron/sbctl/hierarchy" 15 | "github.com/google/go-tpm/tpm2/transport" 16 | "github.com/google/go-tpm/tpm2/transport/simulator" 17 | ) 18 | 19 | func mustBytes(f string) []byte { 20 | b, err := os.ReadFile(f) 21 | if err != nil { 22 | panic(err) 23 | } 24 | return b 25 | } 26 | 27 | func TestSetup(t *testing.T) { 28 | // Embed a TPM eventlog from out test suite for enroll-keys 29 | mapfs := fstest.MapFS{ 30 | systemEventlog: {Data: mustBytes("../../tests/tpm_eventlogs/t480s_eventlog")}, 31 | "/boot/test.efi": {Data: mustBytes("../../tests/binaries/test.pecoff")}, 32 | "/boot/something.efi": {Data: mustBytes("../../tests/binaries/test.pecoff")}, 33 | } 34 | 35 | conf := config.DefaultConfig() 36 | 37 | // Disable landlock in tests 38 | conf.Landlock = false 39 | 40 | // Include test file into our file config 41 | conf.Files = []*config.FileConfig{ 42 | { 43 | Path: "/boot/test.efi", 44 | Output: "/boot/new.efi", 45 | }, 46 | { 47 | Path: "/boot/something.efi", 48 | Output: "", 49 | }, 50 | } 51 | 52 | state := &config.State{ 53 | Fs: efitest.FromMapFS(mapfs), 54 | Efivarfs: testfs.NewTestFS(). 55 | With(efitest.SetUpModeOn(), 56 | mapfs, 57 | ). 58 | Open(), 59 | Config: conf, 60 | } 61 | 62 | // Ignore immutable in enroll-keys 63 | enrollKeysCmdOptions.IgnoreImmutable = true 64 | 65 | err := SetupInstallation(state) 66 | if err != nil { 67 | t.Fatalf("failed running SetupInstallation: %v", err) 68 | } 69 | 70 | // Check that we can sign and verify a file 71 | kh, err := backend.GetKeyHierarchy(state.Fs, state) 72 | if err != nil { 73 | t.Fatalf("can't get key hierarchy: %v", err) 74 | } 75 | ok, err := sbctl.VerifyFile(state, kh, hierarchy.Db, "/boot/new.efi") 76 | if err != nil { 77 | t.Fatalf("can't verify file: %v", err) 78 | } 79 | if !ok { 80 | t.Fatalf("file is not properly signed") 81 | } 82 | 83 | guid, err := conf.GetGUID(state.Fs) 84 | if err != nil { 85 | t.Fatalf("can't get owner guid") 86 | } 87 | 88 | sb, err := state.Efivarfs.Getdb() 89 | if err != nil { 90 | t.Fatalf("can't get db from efivarfs") 91 | } 92 | data := &signature.SignatureData{ 93 | Owner: *guid, 94 | Data: kh.Db.Certificate().Raw, 95 | } 96 | if !sb.SigDataExists(signature.CERT_X509_GUID, data) { 97 | t.Fatalf("can't find db cert in efivarfs") 98 | } 99 | 100 | sb, err = state.Efivarfs.GetKEK() 101 | if err != nil { 102 | t.Fatalf("can't get kek from efivarfs") 103 | } 104 | data = &signature.SignatureData{ 105 | Owner: *guid, 106 | Data: kh.KEK.Certificate().Raw, 107 | } 108 | if !sb.SigDataExists(signature.CERT_X509_GUID, data) { 109 | t.Fatalf("can't find kek cert in efivarfs") 110 | } 111 | 112 | sb, err = state.Efivarfs.GetPK() 113 | if err != nil { 114 | t.Fatalf("can't get pk from efivarfs") 115 | } 116 | data = &signature.SignatureData{ 117 | Owner: *guid, 118 | Data: kh.PK.Certificate().Raw, 119 | } 120 | if !sb.SigDataExists(signature.CERT_X509_GUID, data) { 121 | t.Fatalf("can't find pk cert in efivarfs") 122 | } 123 | } 124 | 125 | func TestSetupTPMKeys(t *testing.T) { 126 | // Embed a TPM eventlog from out test suite for enroll-keys 127 | mapfs := fstest.MapFS{ 128 | systemEventlog: {Data: mustBytes("../../tests/tpm_eventlogs/t480s_eventlog")}, 129 | "/boot/test.efi": {Data: mustBytes("../../tests/binaries/test.pecoff")}, 130 | "/boot/something.efi": {Data: mustBytes("../../tests/binaries/test.pecoff")}, 131 | } 132 | 133 | conf := config.DefaultConfig() 134 | 135 | // Disable landlock 136 | conf.Landlock = false 137 | 138 | // Set PK to be a TPM key 139 | conf.Keys.PK.Type = "tpm" 140 | conf.Keys.KEK.Type = "tpm" 141 | conf.Keys.Db.Type = "tpm" 142 | 143 | // Include test file into our file config 144 | conf.Files = []*config.FileConfig{ 145 | { 146 | Path: "/boot/test.efi", 147 | Output: "/boot/new.efi", 148 | }, 149 | { 150 | Path: "/boot/something.efi", 151 | Output: "", 152 | }, 153 | } 154 | 155 | rwc, err := simulator.OpenSimulator() 156 | if err != nil { 157 | t.Fatal(err) 158 | } 159 | defer rwc.Close() 160 | 161 | state := &config.State{ 162 | TPM: func() transport.TPMCloser { 163 | return rwc 164 | }, 165 | Fs: efitest.FromMapFS(mapfs), 166 | Efivarfs: testfs.NewTestFS(). 167 | With(efitest.SetUpModeOn(), 168 | mapfs, 169 | ). 170 | Open(), 171 | Config: conf, 172 | } 173 | 174 | // Ignore immutable in enroll-keys 175 | enrollKeysCmdOptions.IgnoreImmutable = true 176 | 177 | err = SetupInstallation(state) 178 | if err != nil { 179 | t.Fatalf("failed running SetupInstallation: %v", err) 180 | } 181 | 182 | // Check that we can sign and verify a file 183 | kh, err := backend.GetKeyHierarchy(state.Fs, state) 184 | if err != nil { 185 | t.Fatalf("can't get key hierarchy: %v", err) 186 | } 187 | ok, err := sbctl.VerifyFile(state, kh, hierarchy.Db, "/boot/new.efi") 188 | if err != nil { 189 | t.Fatalf("can't verify file: %v", err) 190 | } 191 | if !ok { 192 | t.Fatalf("file is not properly signed") 193 | } 194 | 195 | guid, err := conf.GetGUID(state.Fs) 196 | if err != nil { 197 | t.Fatalf("can't get owner guid") 198 | } 199 | 200 | sb, err := state.Efivarfs.Getdb() 201 | if err != nil { 202 | t.Fatalf("can't get db from efivarfs") 203 | } 204 | data := &signature.SignatureData{ 205 | Owner: *guid, 206 | Data: kh.Db.Certificate().Raw, 207 | } 208 | if !sb.SigDataExists(signature.CERT_X509_GUID, data) { 209 | t.Fatalf("can't find db cert in efivarfs") 210 | } 211 | 212 | sb, err = state.Efivarfs.GetKEK() 213 | if err != nil { 214 | t.Fatalf("can't get kek from efivarfs") 215 | } 216 | data = &signature.SignatureData{ 217 | Owner: *guid, 218 | Data: kh.KEK.Certificate().Raw, 219 | } 220 | if !sb.SigDataExists(signature.CERT_X509_GUID, data) { 221 | t.Fatalf("can't find kek cert in efivarfs") 222 | } 223 | 224 | sb, err = state.Efivarfs.GetPK() 225 | if err != nil { 226 | t.Fatalf("can't get pk from efivarfs") 227 | } 228 | data = &signature.SignatureData{ 229 | Owner: *guid, 230 | Data: kh.PK.Certificate().Raw, 231 | } 232 | if !sb.SigDataExists(signature.CERT_X509_GUID, data) { 233 | t.Fatalf("can't find pk cert in efivarfs") 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /cmd/sbctl/sign-all.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/foxboron/sbctl" 8 | "github.com/foxboron/sbctl/backend" 9 | "github.com/foxboron/sbctl/config" 10 | "github.com/foxboron/sbctl/hierarchy" 11 | "github.com/foxboron/sbctl/logging" 12 | "github.com/foxboron/sbctl/lsm" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | var ( 17 | generate bool 18 | ) 19 | 20 | var signAllCmd = &cobra.Command{ 21 | Use: "sign-all", 22 | Short: "Sign all enrolled files with secure boot keys", 23 | RunE: func(cmd *cobra.Command, args []string) error { 24 | var gerr error 25 | state := cmd.Context().Value(stateDataKey{}).(*config.State) 26 | // Don't run landlock if we are making UKIs 27 | if state.Config.Landlock && !generate { 28 | if err := sbctl.LandlockFromFileDatabase(state); err != nil { 29 | return err 30 | } 31 | if err := lsm.Restrict(); err != nil { 32 | return err 33 | } 34 | } 35 | 36 | if generate { 37 | sign = true 38 | if err := generateBundlesCmd.RunE(cmd, args); err != nil { 39 | gerr = ErrSilent 40 | logging.Error(err) 41 | } 42 | } 43 | serr := SignAll(state) 44 | if serr != nil || gerr != nil { 45 | return ErrSilent 46 | } 47 | return nil 48 | }, 49 | } 50 | 51 | func SignAll(state *config.State) error { 52 | var signerr error 53 | files, err := sbctl.ReadFileDatabase(state.Fs, state.Config.FilesDb) 54 | if err != nil { 55 | return err 56 | } 57 | for _, entry := range files { 58 | 59 | kh, err := backend.GetKeyHierarchy(state.Fs, state) 60 | if err != nil { 61 | return err 62 | } 63 | 64 | err = sbctl.SignFile(state, kh, hierarchy.Db, entry.File, entry.OutputFile) 65 | if errors.Is(err, sbctl.ErrAlreadySigned) { 66 | logging.Print("File has already been signed %s\n", entry.OutputFile) 67 | } else if err != nil { 68 | logging.Error(fmt.Errorf("failed signing %s: %w", entry.File, err)) 69 | // Ensure we are getting os.Exit(1) 70 | signerr = ErrSilent 71 | continue 72 | } else { 73 | logging.Ok("Signed %s", entry.OutputFile) 74 | } 75 | 76 | // Update checksum after we signed it 77 | files[entry.File] = entry 78 | if err := sbctl.WriteFileDatabase(state.Fs, state.Config.FilesDb, files); err != nil { 79 | return err 80 | } 81 | } 82 | return signerr 83 | } 84 | 85 | func signAllCmdFlags(cmd *cobra.Command) { 86 | f := cmd.Flags() 87 | f.BoolVarP(&generate, "generate", "g", false, "run all generate-* sub-commands before signing") 88 | } 89 | 90 | func init() { 91 | signAllCmdFlags(signAllCmd) 92 | CliCommands = append(CliCommands, cliCommand{ 93 | Cmd: signAllCmd, 94 | }) 95 | } 96 | -------------------------------------------------------------------------------- /cmd/sbctl/sign.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/foxboron/sbctl" 9 | "github.com/foxboron/sbctl/backend" 10 | "github.com/foxboron/sbctl/config" 11 | "github.com/foxboron/sbctl/logging" 12 | "github.com/foxboron/sbctl/lsm" 13 | "github.com/landlock-lsm/go-landlock/landlock" 14 | "github.com/spf13/afero" 15 | "github.com/spf13/cobra" 16 | ) 17 | 18 | var ( 19 | save bool 20 | output string 21 | ) 22 | 23 | var signCmd = &cobra.Command{ 24 | Use: "sign", 25 | Short: "Sign a file with secure boot keys", 26 | RunE: func(cmd *cobra.Command, args []string) error { 27 | state := cmd.Context().Value(stateDataKey{}).(*config.State) 28 | 29 | if len(args) < 1 { 30 | logging.Print("Requires a file to sign\n") 31 | os.Exit(1) 32 | } 33 | 34 | var rules []landlock.Rule 35 | 36 | // Ensure we have absolute paths 37 | file, err := filepath.Abs(args[0]) 38 | if err != nil { 39 | return err 40 | } 41 | // Get output path from database for file if output not specified 42 | if output == "" { 43 | files, err := sbctl.ReadFileDatabase(state.Fs, state.Config.FilesDb) 44 | if err != nil { 45 | return err 46 | } 47 | for _, entry := range files { 48 | if entry.File == file { 49 | output = entry.OutputFile 50 | break 51 | } 52 | } 53 | } 54 | 55 | if output == "" { 56 | output = file 57 | rules = append(rules, lsm.TruncFile(file).IgnoreIfMissing()) 58 | } else { 59 | output, err = filepath.Abs(output) 60 | if err != nil { 61 | return err 62 | } 63 | // Set input file to RO and output dir/file to RW 64 | rules = append(rules, landlock.ROFiles(file).IgnoreIfMissing()) 65 | if ok, _ := afero.Exists(state.Fs, output); ok { 66 | rules = append(rules, lsm.TruncFile(output)) 67 | } else { 68 | rules = append(rules, landlock.RWDirs(filepath.Dir(output))) 69 | } 70 | } 71 | 72 | if state.Config.Landlock { 73 | lsm.RestrictAdditionalPaths(rules...) 74 | if err := lsm.Restrict(); err != nil { 75 | return err 76 | } 77 | } 78 | 79 | kh, err := backend.GetKeyHierarchy(state.Fs, state) 80 | if err != nil { 81 | return err 82 | } 83 | 84 | err = sbctl.Sign(state, kh, file, output, save) 85 | if errors.Is(err, sbctl.ErrAlreadySigned) { 86 | logging.Print("File has already been signed %s\n", output) 87 | } else if err != nil { 88 | return err 89 | } else { 90 | logging.Ok("Signed %s", output) 91 | } 92 | return nil 93 | }, 94 | } 95 | 96 | func signCmdFlags(cmd *cobra.Command) { 97 | f := cmd.Flags() 98 | f.BoolVarP(&save, "save", "s", false, "save file to the database") 99 | f.StringVarP(&output, "output", "o", "", "output filename. Default replaces the file") 100 | } 101 | 102 | func init() { 103 | signCmdFlags(signCmd) 104 | CliCommands = append(CliCommands, cliCommand{ 105 | Cmd: signCmd, 106 | }) 107 | } 108 | -------------------------------------------------------------------------------- /cmd/sbctl/status.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log/slog" 6 | "os" 7 | "strings" 8 | 9 | "github.com/foxboron/go-uefi/efi/signature" 10 | "github.com/foxboron/sbctl" 11 | "github.com/foxboron/sbctl/backend" 12 | "github.com/foxboron/sbctl/certs" 13 | "github.com/foxboron/sbctl/config" 14 | "github.com/foxboron/sbctl/logging" 15 | "github.com/foxboron/sbctl/lsm" 16 | "github.com/foxboron/sbctl/quirks" 17 | "github.com/spf13/cobra" 18 | ) 19 | 20 | var statusCmd = &cobra.Command{ 21 | Use: "status", 22 | Short: "Show current boot status", 23 | RunE: RunStatus, 24 | } 25 | 26 | type Status struct { 27 | Installed bool `json:"installed"` 28 | GUID string `json:"guid"` 29 | SetupMode bool `json:"setup_mode"` 30 | SecureBoot bool `json:"secure_boot"` 31 | Vendors []string `json:"vendors"` 32 | FirmwareQuirks []quirks.Quirk `json:"firmware_quirks"` 33 | } 34 | 35 | func NewStatus() *Status { 36 | return &Status{ 37 | Installed: false, 38 | GUID: "", 39 | SetupMode: false, 40 | SecureBoot: false, 41 | Vendors: []string{}, 42 | FirmwareQuirks: []quirks.Quirk{}, 43 | } 44 | } 45 | 46 | func PrintStatus(s *Status) { 47 | logging.Print("Installed:\t") 48 | if s.Installed { 49 | logging.Ok("sbctl is installed") 50 | if s.GUID != "" { 51 | logging.Print("Owner GUID:\t") 52 | logging.Println(s.GUID) 53 | } 54 | } else { 55 | logging.NotOk("sbctl is not installed") 56 | } 57 | logging.Print("Setup Mode:\t") 58 | if s.SetupMode { 59 | logging.NotOk("Enabled") 60 | } else { 61 | logging.Ok("Disabled") 62 | } 63 | logging.Print("Secure Boot:\t") 64 | if s.SecureBoot { 65 | logging.Ok("Enabled") 66 | } else { 67 | logging.NotOk("Disabled") 68 | } 69 | // TODO: We only have microsoft keys 70 | // this needs to be extended for more keys in the future 71 | logging.Print("Vendor Keys:\t") 72 | if len(s.Vendors) > 0 { 73 | logging.Println(strings.Join(s.Vendors, " ")) 74 | } else { 75 | logging.Println("none") 76 | } 77 | if len(s.FirmwareQuirks) > 0 { 78 | logging.Print("Firmware:\t") 79 | logging.Print(logging.Warnf("Your firmware has known quirks")) 80 | for _, quirk := range s.FirmwareQuirks { 81 | logging.Println("\t\t- " + quirk.ID + ": " + quirk.Name + " (" + quirk.Severity + ")\n\t\t " + quirk.Link) 82 | } 83 | } 84 | } 85 | 86 | func RunDebug(state *config.State) error { 87 | kh, err := backend.GetKeyHierarchy(state.Fs, state) 88 | if err != nil { 89 | return err 90 | } 91 | 92 | efistate, err := sbctl.SystemEFIVariables(state.Efivarfs) 93 | if err != nil { 94 | return err 95 | } 96 | 97 | guid, err := state.Config.GetGUID(state.Fs) 98 | if err != nil { 99 | return err 100 | } 101 | 102 | if efistate.PK.SigDataExists(signature.CERT_X509_GUID, &signature.SignatureData{Owner: *guid, Data: kh.PK.Certificate().Raw}) { 103 | slog.Debug("PK is fine") 104 | } 105 | 106 | if efistate.KEK.SigDataExists(signature.CERT_X509_GUID, &signature.SignatureData{Owner: *guid, Data: kh.KEK.Certificate().Raw}) { 107 | slog.Debug("KEK is fine") 108 | } 109 | 110 | if efistate.Db.SigDataExists(signature.CERT_X509_GUID, &signature.SignatureData{Owner: *guid, Data: kh.Db.Certificate().Raw}) { 111 | slog.Debug("db is fine") 112 | } 113 | 114 | return nil 115 | } 116 | 117 | func RunStatus(cmd *cobra.Command, args []string) error { 118 | state := cmd.Context().Value(stateDataKey{}).(*config.State) 119 | 120 | if state.Config.Landlock { 121 | if err := lsm.Restrict(); err != nil { 122 | return err 123 | } 124 | } 125 | 126 | if cmdOptions.Debug { 127 | RunDebug(state) 128 | } 129 | 130 | stat := NewStatus() 131 | if _, err := state.Fs.Stat("/sys/firmware/efi/efivars/SetupMode-8be4df61-93ca-11d2-aa0d-00e098032b8c"); os.IsNotExist(err) { 132 | return fmt.Errorf("system is not booted with UEFI") 133 | } 134 | 135 | if state.IsInstalled() { 136 | stat.Installed = true 137 | u, err := state.Config.GetGUID(state.Fs) 138 | if err == nil { 139 | stat.GUID = u.Format() 140 | } 141 | } 142 | if ok, _ := state.Efivarfs.GetSetupMode(); ok { 143 | stat.SetupMode = true 144 | } 145 | if ok, _ := state.Efivarfs.GetSecureBoot(); ok { 146 | stat.SecureBoot = true 147 | } 148 | if keys := sbctl.GetEnrolledVendorCerts(); len(keys) > 0 { 149 | stat.Vendors = keys 150 | } 151 | if keys, err := certs.BuiltinSignatureOwners(); err == nil { 152 | stat.Vendors = append(stat.Vendors, keys...) 153 | } 154 | stat.FirmwareQuirks = quirks.CheckFirmwareQuirks(state) 155 | if cmdOptions.JsonOutput { 156 | if err := JsonOut(stat); err != nil { 157 | return err 158 | } 159 | } else { 160 | PrintStatus(stat) 161 | } 162 | return nil 163 | } 164 | 165 | func init() { 166 | CliCommands = append(CliCommands, cliCommand{ 167 | Cmd: statusCmd, 168 | }) 169 | } 170 | -------------------------------------------------------------------------------- /cmd/sbctl/status_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | "testing/fstest" 7 | 8 | "github.com/foxboron/go-uefi/efi/efitest" 9 | "github.com/foxboron/sbctl/quirks" 10 | ) 11 | 12 | var ( 13 | out Status 14 | ) 15 | 16 | func TestStatusOff(t *testing.T) { 17 | cmd := SetFS( 18 | efitest.SecureBootOff(), 19 | efitest.SetUpModeOn(), 20 | ) 21 | 22 | if err := captureJsonOutput(&out, func() error { 23 | return RunStatus(cmd, []string{}) 24 | }); err != nil { 25 | t.Fatal(err) 26 | } 27 | 28 | if out.SecureBoot != false { 29 | t.Fatal("secure boot is not disabled") 30 | } 31 | } 32 | 33 | func TestStatusOn(t *testing.T) { 34 | cmd := SetFS(efitest.SecureBootOn(), 35 | efitest.SetUpModeOff()) 36 | 37 | if err := captureJsonOutput(&out, func() error { 38 | return RunStatus(cmd, []string{}) 39 | }); err != nil { 40 | t.Fatal(err) 41 | } 42 | 43 | if out.SecureBoot != true { 44 | t.Fatal("secure boot is not enabled") 45 | } 46 | } 47 | 48 | func TestFQ0001DateMethod(t *testing.T) { 49 | cmd := SetFS( 50 | fstest.MapFS{"/sys/devices/virtual/dmi/id/bios_date": {Data: []byte("01/06/2023\n")}}, 51 | fstest.MapFS{"/sys/devices/virtual/dmi/id/bios_version": {Data: []byte("A.30\n")}}, 52 | fstest.MapFS{"/sys/devices/virtual/dmi/id/board_name": {Data: []byte("PRO Z790-A WIFI (MS-7E07)\n")}}, 53 | fstest.MapFS{"/sys/devices/virtual/dmi/id/board_vendor": {Data: []byte("Micro-Star International Co., Ltd.\n")}}, 54 | fstest.MapFS{"/sys/devices/virtual/dmi/id/chassis_type": {Data: []byte("3\n")}}, 55 | fstest.MapFS{"/sys/devices/virtual/dmi/id/product_name": {Data: []byte("MS-7E07\n")}}, 56 | efitest.SecureBootOn(), 57 | efitest.SetUpModeOff(), 58 | ) 59 | 60 | if err := captureJsonOutput(&out, func() error { 61 | return RunStatus(cmd, []string{}) 62 | }); err != nil { 63 | t.Fatal(err) 64 | } 65 | 66 | fq0001 := quirks.Quirk{} 67 | for _, quirk := range out.FirmwareQuirks { 68 | if quirk.ID == "FQ0001" { 69 | fq0001 = quirk 70 | } 71 | } 72 | 73 | if reflect.ValueOf(fq0001).IsZero() { 74 | t.Fatal("quirk not detected") 75 | } else if fq0001.Method != "date" { 76 | t.Fatal("expected 'date' method, got '" + fq0001.Method + "'") 77 | } 78 | } 79 | 80 | func TestFQ0001DeviceMethod(t *testing.T) { 81 | cmd := SetFS( 82 | fstest.MapFS{"/sys/devices/virtual/dmi/id/bios_date": {Data: []byte("12/29/2021\n")}}, 83 | fstest.MapFS{"/sys/devices/virtual/dmi/id/bios_version": {Data: []byte("1.80\n")}}, 84 | fstest.MapFS{"/sys/devices/virtual/dmi/id/board_name": {Data: []byte("MAG X570 TOMAHAWK WIFI (MS-7C84)\n")}}, 85 | fstest.MapFS{"/sys/devices/virtual/dmi/id/board_vendor": {Data: []byte("Micro-Star International Co., Ltd.\n")}}, 86 | fstest.MapFS{"/sys/devices/virtual/dmi/id/chassis_type": {Data: []byte("3\n")}}, 87 | fstest.MapFS{"/sys/devices/virtual/dmi/id/product_name": {Data: []byte("MS-7C84\n")}}, 88 | efitest.SecureBootOn(), 89 | efitest.SetUpModeOff(), 90 | ) 91 | 92 | if err := captureJsonOutput(&out, func() error { 93 | return RunStatus(cmd, []string{}) 94 | }); err != nil { 95 | t.Fatal(err) 96 | } 97 | 98 | fq0001 := quirks.Quirk{} 99 | for _, quirk := range out.FirmwareQuirks { 100 | if quirk.ID == "FQ0001" { 101 | fq0001 = quirk 102 | } 103 | } 104 | 105 | if reflect.ValueOf(fq0001).IsZero() { 106 | t.Fatal("quirk not detected") 107 | } else if fq0001.Method != "device_name" { 108 | t.Fatal("expected 'device_name' method, got '" + fq0001.Method + "'") 109 | } 110 | } 111 | 112 | func TestFQ0001ExplicitlyUnaffected(t *testing.T) { 113 | cmd := SetFS( 114 | fstest.MapFS{"/sys/devices/virtual/dmi/id/bios_date": {Data: []byte("03/31/2022\n")}}, 115 | fstest.MapFS{"/sys/devices/virtual/dmi/id/bios_version": {Data: []byte("1.B0\n")}}, 116 | fstest.MapFS{"/sys/devices/virtual/dmi/id/board_name": {Data: []byte("MAG Z490 TOMAHAWK (MS-7C80)\n")}}, 117 | fstest.MapFS{"/sys/devices/virtual/dmi/id/board_vendor": {Data: []byte("Micro-Star International Co., Ltd.\n")}}, 118 | fstest.MapFS{"/sys/devices/virtual/dmi/id/chassis_type": {Data: []byte("3\n")}}, 119 | fstest.MapFS{"/sys/devices/virtual/dmi/id/product_name": {Data: []byte("MS-7C80\n")}}, 120 | efitest.SecureBootOn(), 121 | efitest.SetUpModeOff(), 122 | ) 123 | 124 | if err := captureJsonOutput(&out, func() error { 125 | return RunStatus(cmd, []string{}) 126 | }); err != nil { 127 | t.Fatal(err) 128 | } 129 | 130 | fq0001 := quirks.Quirk{} 131 | for _, quirk := range out.FirmwareQuirks { 132 | if quirk.ID == "FQ0001" { 133 | fq0001 = quirk 134 | } 135 | } 136 | 137 | if !reflect.ValueOf(fq0001).IsZero() { 138 | t.Fatal("quirk got detected, with method '" + fq0001.Method + "'") 139 | } 140 | } 141 | 142 | func TestFQ0001WrongChassis(t *testing.T) { 143 | cmd := SetFS( 144 | fstest.MapFS{"/sys/devices/virtual/dmi/id/bios_date": {Data: []byte("01/06/2023\n")}}, 145 | fstest.MapFS{"/sys/devices/virtual/dmi/id/bios_version": {Data: []byte("A.30\n")}}, 146 | fstest.MapFS{"/sys/devices/virtual/dmi/id/board_name": {Data: []byte("PRO Z790-A WIFI (MS-7E07)\n")}}, 147 | fstest.MapFS{"/sys/devices/virtual/dmi/id/board_vendor": {Data: []byte("Micro-Star International Co., Ltd.\n")}}, 148 | fstest.MapFS{"/sys/devices/virtual/dmi/id/chassis_type": {Data: []byte("5\n")}}, 149 | fstest.MapFS{"/sys/devices/virtual/dmi/id/product_name": {Data: []byte("MS-7E07\n")}}, 150 | efitest.SecureBootOn(), 151 | efitest.SetUpModeOff(), 152 | ) 153 | 154 | if err := captureJsonOutput(&out, func() error { 155 | return RunStatus(cmd, []string{}) 156 | }); err != nil { 157 | t.Fatal(err) 158 | } 159 | 160 | fq0001 := quirks.Quirk{} 161 | for _, quirk := range out.FirmwareQuirks { 162 | if quirk.ID == "FQ0001" { 163 | fq0001 = quirk 164 | } 165 | } 166 | 167 | if !reflect.ValueOf(fq0001).IsZero() { 168 | t.Fatal("quirk got detected using '" + fq0001.Method + "' method") 169 | } 170 | } 171 | 172 | func TestFQ0001WrongVendor(t *testing.T) { 173 | cmd := SetFS( 174 | fstest.MapFS{"/sys/devices/virtual/dmi/id/bios_date": {Data: []byte("01/06/2023\n")}}, 175 | fstest.MapFS{"/sys/devices/virtual/dmi/id/bios_version": {Data: []byte("A.30\n")}}, 176 | fstest.MapFS{"/sys/devices/virtual/dmi/id/board_name": {Data: []byte("PRO Z790-A WIFI (MS-7E07)\n")}}, 177 | fstest.MapFS{"/sys/devices/virtual/dmi/id/board_vendor": {Data: []byte("More-Security Issues Co., Ltd.\n")}}, 178 | fstest.MapFS{"/sys/devices/virtual/dmi/id/chassis_type": {Data: []byte("3\n")}}, 179 | fstest.MapFS{"/sys/devices/virtual/dmi/id/product_name": {Data: []byte("MS-7E07\n")}}, 180 | efitest.SecureBootOn(), 181 | efitest.SetUpModeOff(), 182 | ) 183 | 184 | if err := captureJsonOutput(&out, func() error { 185 | return RunStatus(cmd, []string{}) 186 | }); err != nil { 187 | t.Fatal(err) 188 | } 189 | 190 | fq0001 := quirks.Quirk{} 191 | for _, quirk := range out.FirmwareQuirks { 192 | if quirk.ID == "FQ0001" { 193 | fq0001 = quirk 194 | } 195 | } 196 | 197 | if !reflect.ValueOf(fq0001).IsZero() { 198 | t.Fatal("quirk got detected using '" + fq0001.Method + "' method") 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /cmd/sbctl/utils_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "os" 8 | 9 | "testing/fstest" 10 | 11 | "github.com/foxboron/go-uefi/efi/efitest" 12 | efs "github.com/foxboron/go-uefi/efi/fs" 13 | "github.com/foxboron/go-uefi/efivarfs/testfs" 14 | "github.com/foxboron/sbctl/config" 15 | "github.com/foxboron/sbctl/logging" 16 | "github.com/spf13/cobra" 17 | ) 18 | 19 | func captureOutput(f func() error) ([]byte, error) { 20 | var buf bytes.Buffer 21 | logging.SetOutput(&buf) 22 | err := f() 23 | logging.SetOutput(os.Stderr) 24 | return buf.Bytes(), err 25 | } 26 | 27 | func captureJsonOutput(out any, f func() error) error { 28 | cmdOptions.JsonOutput = true 29 | output, err := captureOutput(f) 30 | if err != nil { 31 | return err 32 | } 33 | return json.Unmarshal(output, &out) 34 | } 35 | 36 | func SetFS(files ...fstest.MapFS) *cobra.Command { 37 | fs := efitest.NewFS(). 38 | With(files...). 39 | ToAfero() 40 | 41 | // TODO: Remove and move to proper efifs implementation 42 | efs.SetFS(fs) 43 | 44 | state := &config.State{ 45 | Fs: fs, 46 | Efivarfs: testfs.NewTestFS(). 47 | With(files...). 48 | Open(), 49 | Config: config.DefaultConfig(), 50 | } 51 | cmd := &cobra.Command{} 52 | ctx := context.WithValue(context.Background(), stateDataKey{}, state) 53 | cmd.SetContext(ctx) 54 | return cmd 55 | } 56 | -------------------------------------------------------------------------------- /cmd/sbctl/verify.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/foxboron/sbctl" 9 | "github.com/foxboron/sbctl/backend" 10 | "github.com/foxboron/sbctl/config" 11 | "github.com/foxboron/sbctl/hierarchy" 12 | "github.com/foxboron/sbctl/logging" 13 | "github.com/foxboron/sbctl/lsm" 14 | "github.com/landlock-lsm/go-landlock/landlock" 15 | "github.com/spf13/afero" 16 | "github.com/spf13/cobra" 17 | ) 18 | 19 | type VerifiedFile struct { 20 | FileName string `json:"file_name"` 21 | // IsSigned should be set to one of these values: 22 | // - 0: "unsigned" 23 | // - 1: "signed" 24 | // - -1: "file does not exist" 25 | IsSigned int8 `json:"is_signed"` 26 | } 27 | 28 | var ( 29 | ErrInvalidHeader = errors.New("invalid pe header") 30 | verifyCmd = &cobra.Command{ 31 | Use: "verify", 32 | Short: "Find and check if files in the ESP are signed or not", 33 | RunE: RunVerify, 34 | } 35 | verifiedFiles []VerifiedFile 36 | ) 37 | 38 | func VerifyOneFile(state *config.State, f string) error { 39 | o, err := state.Fs.Open(f) 40 | fileentry := VerifiedFile{FileName: f, IsSigned: 0} 41 | if errors.Is(err, os.ErrNotExist) { 42 | logging.Warn("%s does not exist", f) 43 | fileentry.IsSigned = -1 44 | verifiedFiles = append(verifiedFiles, fileentry) 45 | return nil 46 | } else if errors.Is(err, os.ErrPermission) { 47 | logging.Warn("%s permission denied. Can't read file\n", f) 48 | return nil 49 | } 50 | defer o.Close() 51 | ok, err := sbctl.CheckMSDos(o) 52 | if err != nil { 53 | logging.Error(fmt.Errorf("failed to read file %s: %s", f, err)) 54 | } 55 | if !ok { 56 | return fmt.Errorf("%s: %w", f, ErrInvalidHeader) 57 | } 58 | 59 | kh, err := backend.GetKeyHierarchy(state.Fs, state) 60 | if err != nil { 61 | return err 62 | } 63 | 64 | ok, err = sbctl.VerifyFile(state, kh, hierarchy.Db, f) 65 | if err != nil { 66 | return err 67 | } 68 | 69 | if ok { 70 | logging.Ok("%s is signed", f) 71 | fileentry.IsSigned = 1 72 | } else { 73 | logging.NotOk("%s is not signed", f) 74 | } 75 | verifiedFiles = append(verifiedFiles, fileentry) 76 | 77 | return nil 78 | } 79 | 80 | func RunVerify(cmd *cobra.Command, args []string) error { 81 | state := cmd.Context().Value(stateDataKey{}).(*config.State) 82 | 83 | // Exit early if we can't verify files 84 | espPath, err := sbctl.GetESP(state.Fs) 85 | if err != nil { 86 | return err 87 | } 88 | 89 | if state.Config.Landlock { 90 | lsm.RestrictAdditionalPaths( 91 | landlock.RWDirs(espPath), 92 | ) 93 | if err := sbctl.LandlockFromFileDatabase(state); err != nil { 94 | return err 95 | } 96 | if err := lsm.Restrict(); err != nil { 97 | return err 98 | } 99 | } 100 | 101 | if len(args) > 0 { 102 | for _, file := range args { 103 | if err := VerifyOneFile(state, file); err != nil { 104 | if errors.Is(err, ErrInvalidHeader) { 105 | logging.Error(fmt.Errorf("%s is not a valid EFI binary", file)) 106 | return nil 107 | } 108 | return err 109 | } 110 | } 111 | if cmdOptions.JsonOutput { 112 | return JsonOut(verifiedFiles) 113 | } 114 | return nil 115 | } 116 | logging.Print("Verifying file database and EFI images in %s...\n", espPath) 117 | if err := sbctl.SigningEntryIter(state, func(file *sbctl.SigningEntry) error { 118 | sbctl.AddChecked(file.OutputFile) 119 | if err := VerifyOneFile(state, file.OutputFile); err != nil { 120 | return err 121 | } 122 | return nil 123 | }); err != nil { 124 | return err 125 | } 126 | 127 | if err := afero.Walk(state.Fs, espPath, func(path string, info os.FileInfo, err error) error { 128 | if err != nil { 129 | logging.Error(fmt.Errorf("failed to read path %s: %s", path, err)) 130 | } 131 | if fi, _ := state.Fs.Stat(path); fi.IsDir() { 132 | return nil 133 | } 134 | if sbctl.InChecked(path) { 135 | return nil 136 | } 137 | if err = VerifyOneFile(state, path); err != nil { 138 | // We are scanning the ESP, so ignore invalid files 139 | if errors.Is(err, ErrInvalidHeader) { 140 | return nil 141 | } 142 | logging.Error(fmt.Errorf("failed to verify file %s: %s", path, err)) 143 | } 144 | return nil 145 | }); err != nil { 146 | return err 147 | } 148 | if cmdOptions.JsonOutput { 149 | return JsonOut(verifiedFiles) 150 | } 151 | return nil 152 | } 153 | 154 | func init() { 155 | CliCommands = append(CliCommands, cliCommand{ 156 | Cmd: verifyCmd, 157 | }) 158 | } 159 | -------------------------------------------------------------------------------- /cmd/sbctl/version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/foxboron/sbctl" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | var ( 11 | versionCmd = &cobra.Command{ 12 | Use: "version", 13 | Short: "Print sbctl version", 14 | Hidden: true, 15 | Run: func(cmd *cobra.Command, args []string) { 16 | fmt.Println(sbctl.Version) 17 | }, 18 | } 19 | ) 20 | 21 | func init() { 22 | CliCommands = append(CliCommands, cliCommand{ 23 | Cmd: versionCmd, 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "os" 7 | "path" 8 | "strings" 9 | 10 | "github.com/foxboron/sbctl/fs" 11 | "github.com/landlock-lsm/go-landlock/landlock" 12 | 13 | "github.com/foxboron/go-uefi/efi/util" 14 | "github.com/foxboron/go-uefi/efivarfs" 15 | "github.com/google/go-tpm/tpm2/transport" 16 | "github.com/google/uuid" 17 | "github.com/spf13/afero" 18 | 19 | yaml "github.com/goccy/go-yaml" 20 | ) 21 | 22 | var ( 23 | DatabasePath string 24 | ) 25 | 26 | type FileConfig struct { 27 | Path string `json:"path"` 28 | Output string `json:"output,omitempty"` 29 | } 30 | 31 | type KeyConfig struct { 32 | Privkey string `json:"privkey"` 33 | Pubkey string `json:"pubkey"` 34 | Type string `json:"type"` 35 | Description string `json:"description,omitempty"` 36 | } 37 | 38 | type Keys struct { 39 | PK *KeyConfig `json:"pk"` 40 | KEK *KeyConfig `json:"kek"` 41 | Db *KeyConfig `json:"db"` 42 | } 43 | 44 | func (k *Keys) GetKeysConfigs() []*KeyConfig { 45 | return []*KeyConfig{ 46 | k.PK, 47 | k.KEK, 48 | k.Db, 49 | } 50 | } 51 | 52 | // Note: Anything serialized as part of this struct will end up in a public 53 | // debug dump at some point, probably. 54 | type Config struct { 55 | Landlock bool `json:"landlock"` 56 | Keydir string `json:"keydir"` 57 | GUID string `json:"guid"` 58 | FilesDb string `json:"files_db"` 59 | BundlesDb string `json:"bundles_db"` 60 | DbAdditions []string `json:"db_additions,omitempty"` 61 | Files []*FileConfig `json:"files,omitempty"` 62 | Keys *Keys `json:"keys"` 63 | } 64 | 65 | func (c *Config) GetGUID(vfs afero.Fs) (*util.EFIGUID, error) { 66 | b, err := fs.ReadFile(vfs, c.GUID) 67 | if err != nil { 68 | return nil, err 69 | } 70 | u, err := uuid.ParseBytes(b) 71 | if err != nil { 72 | return nil, err 73 | } 74 | guid := util.StringToGUID(u.String()) 75 | return guid, err 76 | } 77 | 78 | func MkConfig(dir string) *Config { 79 | conf := &Config{ 80 | Landlock: true, 81 | GUID: path.Join(dir, "GUID"), 82 | Keydir: path.Join(dir, "keys"), 83 | FilesDb: path.Join(dir, "files.json"), 84 | BundlesDb: path.Join(dir, "bundles.json"), 85 | } 86 | conf.Keys = &Keys{ 87 | PK: &KeyConfig{ 88 | Privkey: path.Join(conf.Keydir, "PK", "PK.key"), 89 | Pubkey: path.Join(conf.Keydir, "PK", "PK.pem"), 90 | Type: "file", 91 | }, 92 | KEK: &KeyConfig{ 93 | Privkey: path.Join(conf.Keydir, "KEK", "KEK.key"), 94 | Pubkey: path.Join(conf.Keydir, "KEK", "KEK.pem"), 95 | Type: "file", 96 | }, 97 | Db: &KeyConfig{ 98 | Privkey: path.Join(conf.Keydir, "db", "db.key"), 99 | Pubkey: path.Join(conf.Keydir, "db", "db.pem"), 100 | Type: "file", 101 | }, 102 | } 103 | return conf 104 | } 105 | 106 | func DefaultConfig() *Config { 107 | return MkConfig("/var/lib/sbctl") 108 | } 109 | 110 | func OldConfig(dir string) *Config { 111 | c := MkConfig(dir) 112 | // Rename databases to the old names 113 | c.FilesDb = strings.Replace(c.FilesDb, ".json", ".db", 1) 114 | c.BundlesDb = strings.Replace(c.BundlesDb, ".json", ".db", 1) 115 | return c 116 | } 117 | 118 | func HasOldConfig(fs afero.Fs, dir string) bool { 119 | _, err := fs.Stat(dir) 120 | return !os.IsNotExist(err) 121 | } 122 | 123 | func HasConfigurationFile(fs afero.Fs, file string) bool { 124 | _, err := fs.Stat(file) 125 | return !os.IsNotExist(err) 126 | } 127 | 128 | func NewConfig(b []byte) (*Config, error) { 129 | conf := DefaultConfig() 130 | if err := yaml.Unmarshal(b, conf); err != nil { 131 | return nil, err 132 | } 133 | return conf, nil 134 | } 135 | 136 | // Key creation is going to require differen callbacks to we abstract them away 137 | type State struct { 138 | Fs afero.Fs 139 | TPM func() transport.TPMCloser 140 | Config *Config 141 | Efivarfs *efivarfs.Efivarfs 142 | } 143 | 144 | func (s *State) IsInstalled() bool { 145 | if _, err := s.Fs.Stat(s.Config.Keydir); errors.Is(err, os.ErrNotExist) { 146 | return false 147 | } 148 | return true 149 | } 150 | 151 | func (s *State) HasTPM() bool { 152 | return s.TPM != nil 153 | } 154 | 155 | func (s *State) HasLandlock() bool { 156 | if !s.Config.Landlock { 157 | return false 158 | } 159 | // Minimal test to check if we have some form of landlock available 160 | // TODO: This is probably not good check. 161 | err := landlock.V1.Restrict( 162 | landlock.RWDirs("/"), 163 | ) 164 | return err == nil 165 | } 166 | 167 | func (s *State) MarshalJSON() ([]byte, error) { 168 | return json.Marshal( 169 | map[string]any{ 170 | "installed": s.IsInstalled(), 171 | // We don't want the config embedded probably 172 | "landlock": s.HasLandlock(), 173 | // "config": s.Config, 174 | }, 175 | ) 176 | } 177 | -------------------------------------------------------------------------------- /config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | var conf = ` 9 | --- 10 | keydir: /etc/sbctl/keys 11 | guid: /var/lib/sbctl/GUID 12 | files_db: /var/lib/sbctl/files.db 13 | bundles_db: /var/lib/sbctl/bundles.db 14 | db_additions: 15 | - microsoft 16 | files: 17 | - path: /boot/vmlinuz-linux-lts 18 | - path: /usr/lib/fwupd/efi/fwupdx64.efi 19 | output: /usr/lib/fwupd/efi/fwupdx64.efi.signed 20 | keys: 21 | pk: 22 | privkey: /etc/sbctl/keys/PK/PK.key 23 | pubkey: /etc/sbctl/keys/PK/PK.pem 24 | type: file 25 | kek: 26 | privkey: /etc/sbctl/keys/KEK/KEK.key 27 | pubkey: /etc/sbctl/keys/KEK/KEK.pem 28 | type: file 29 | db: 30 | privkey: /etc/sbctl/keys/db/db.key 31 | pubkey: /etc/sbctl/keys/db/db.pem 32 | type: file 33 | ` 34 | 35 | func TestParseConfig(t *testing.T) { 36 | conf, err := NewConfig([]byte(conf)) 37 | if err != nil { 38 | t.Fatalf("%v", err) 39 | } 40 | fmt.Println(conf.Keys.PK) 41 | } 42 | -------------------------------------------------------------------------------- /contrib/aur/sbctl-git/.SRCINFO: -------------------------------------------------------------------------------- 1 | pkgbase = sbctl-git 2 | pkgdesc = Secure Boot key manager 3 | pkgver = r201.ga43373c 4 | pkgrel = 1 5 | url = https://github.com/Foxboron/sbctl 6 | arch = x86_64 7 | license = MIT 8 | makedepends = go 9 | makedepends = git 10 | makedepends = asciidoc 11 | source = git+https://github.com/Foxboron/sbctl.git?signed 12 | validpgpkeys = C100346676634E80C940FB9E9C02FF419FECBE16 13 | sha256sums = SKIP 14 | 15 | pkgname = sbctl-git 16 | -------------------------------------------------------------------------------- /contrib/aur/sbctl-git/PKGBUILD: -------------------------------------------------------------------------------- 1 | # Maintainer: Morten Linderud 2 | 3 | pkgname=sbctl-git 4 | pkgver=r201.ga43373c 5 | pkgrel=1 6 | pkgdesc="Secure Boot key manager" 7 | arch=("x86_64") 8 | url="https://github.com/Foxboron/sbctl" 9 | license=("MIT") 10 | makedepends=("go" "git" "asciidoc") 11 | source=("git+https://github.com/Foxboron/sbctl.git?signed") 12 | validpgpkeys=("C100346676634E80C940FB9E9C02FF419FECBE16") 13 | sha256sums=('SKIP') 14 | 15 | pkgver() { 16 | cd "${pkgname%-git}" 17 | printf 'r%s.g%s' "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)" 18 | } 19 | 20 | build(){ 21 | cd "${pkgname%-git}" 22 | export CGO_LDFLAGS="${LDFLAGS}" 23 | export CGO_CFLAGS="${CFLAGS}" 24 | export CGO_CPPFLAGS="${CPPFLAGS}" 25 | export CGO_CXXFLAGS="${CXXFLAGS}" 26 | export GOFLAGS="-buildmode=pie -ldflags=-linkmode=external -trimpath -modcacherw" 27 | make 28 | } 29 | 30 | package(){ 31 | cd "${pkgname%-git}" 32 | make PROGNM="sbctl-git" PREFIX="$pkgdir/usr" install 33 | install -Dm644 ./contrib/pacman/ZZ-sbctl.hook "${pkgdir}/usr/share/libalpm/hooks/99-sbctl.hook" 34 | } 35 | -------------------------------------------------------------------------------- /contrib/kernel-install/91-sbctl.install: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # This file is part of sbctl. 3 | 4 | COMMAND="$1" 5 | KERNEL_VERSION="$2" 6 | ENTRY_DIR_ABS="$3" 7 | # shellcheck disable=SC2034 # Unused variables left for readability 8 | KERNEL_IMAGE="$4" 9 | 10 | IMAGE_FILE="$ENTRY_DIR_ABS/linux" 11 | 12 | if [ "$KERNEL_INSTALL_LAYOUT" = "uki" ]; then 13 | UKI_DIR="$KERNEL_INSTALL_BOOT_ROOT/EFI/Linux" 14 | TRIES_FILE="${KERNEL_INSTALL_CONF_ROOT:-/etc/kernel}/tries" 15 | 16 | if [ -f "$TRIES_FILE" ]; then 17 | read -r TRIES <"$TRIES_FILE" 18 | if ! echo "$TRIES" | grep -q '^[0-9][0-9]*$'; then 19 | echo "$TRIES_FILE does not contain an integer." >&2 20 | exit 1 21 | fi 22 | IMAGE_FILE="$UKI_DIR/$KERNEL_INSTALL_ENTRY_TOKEN-$KERNEL_VERSION+$TRIES.efi" 23 | else 24 | IMAGE_FILE="$UKI_DIR/$KERNEL_INSTALL_ENTRY_TOKEN-$KERNEL_VERSION.efi" 25 | fi 26 | fi 27 | 28 | case "$COMMAND" in 29 | add) 30 | printf 'sbctl: Signing kernel %s\n' "$IMAGE_FILE" 31 | 32 | # exit without error if keys don't exist 33 | # https://github.com/Foxboron/sbctl/issues/187 34 | if ! [ "$(sbctl setup --print-state --json | awk '/installed/ { gsub(/,$/,"",$2); print $2 }')" = "true" ]; then 35 | echo "Secureboot key directory doesn't exist, not signing!" 36 | exit 0 37 | fi 38 | 39 | sbctl sign "$IMAGE_FILE" 1>/dev/null 40 | ;; 41 | remove) 42 | [ "$KERNEL_INSTALL_VERBOSE" -gt 0 ] && 43 | printf 'sbctl: Removing kernel %s from signing database\n' "$IMAGE_FILE" 44 | sbctl remove-file "$IMAGE_FILE" 1>/dev/null || : 45 | ;; 46 | esac 47 | -------------------------------------------------------------------------------- /contrib/mkinitcpio/sbctl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | 3 | KERNEL_FILE="$1" 4 | UKI_FILE="$3" 5 | 6 | if ! [ "$(sbctl setup --print-state --json | awk '/installed/ { gsub(/,$/,"",$2); print $2 }')" = "true" ]; then 7 | echo "Secureboot key directory doesn't exist, not signing!" 8 | exit 0 9 | fi 10 | 11 | IMAGE_FILE="$KERNEL_FILE" 12 | if [ -n "$KERNELDESTINATION" ] && [ -f "$KERNELDESTINATION" ]; then 13 | IMAGE_FILE="$KERNELDESTINATION" 14 | fi 15 | if [ -n "$UKI_FILE" ]; then 16 | IMAGE_FILE="$UKI_FILE" 17 | fi 18 | 19 | if [ -z "$IMAGE_FILE" ]; then 20 | echo "No kernel or UKI found for signing" 21 | exit 0 22 | fi 23 | 24 | echo "Signing $IMAGE_FILE" 25 | sbctl sign "$IMAGE_FILE" 26 | -------------------------------------------------------------------------------- /contrib/pacman/ZZ-sbctl.hook: -------------------------------------------------------------------------------- 1 | [Trigger] 2 | Type = Path 3 | Operation = Install 4 | Operation = Upgrade 5 | Operation = Remove 6 | Target = boot/* 7 | Target = efi/* 8 | Target = usr/lib/modules/*/vmlinuz 9 | Target = usr/lib/modules/*/extramodules/* 10 | Target = usr/lib/**/efi/*.efi* 11 | Target = usr/share/**/*.efi* 12 | 13 | [Action] 14 | Description = Signing EFI binaries... 15 | When = PostTransaction 16 | Exec = /usr/bin/sbctl sign-all -g 17 | -------------------------------------------------------------------------------- /database.go: -------------------------------------------------------------------------------- 1 | package sbctl 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "path/filepath" 7 | 8 | "github.com/foxboron/sbctl/config" 9 | "github.com/foxboron/sbctl/fs" 10 | "github.com/foxboron/sbctl/lsm" 11 | "github.com/landlock-lsm/go-landlock/landlock" 12 | 13 | "github.com/spf13/afero" 14 | ) 15 | 16 | type SigningEntry struct { 17 | File string `json:"file"` 18 | OutputFile string `json:"output_file"` 19 | } 20 | 21 | type SigningEntries map[string]*SigningEntry 22 | 23 | func ReadFileDatabase(vfs afero.Fs, dbpath string) (SigningEntries, error) { 24 | f, err := ReadOrCreateFile(vfs, dbpath) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | files := make(SigningEntries) 30 | if len(f) == 0 { 31 | return files, nil 32 | } 33 | if err = json.Unmarshal(f, &files); err != nil { 34 | return nil, fmt.Errorf("failed to parse json: %v", err) 35 | } 36 | 37 | return files, nil 38 | } 39 | 40 | func WriteFileDatabase(vfs afero.Fs, dbpath string, files SigningEntries) error { 41 | data, err := json.MarshalIndent(files, "", " ") 42 | if err != nil { 43 | return err 44 | } 45 | err = fs.WriteFile(vfs, dbpath, data, 0644) 46 | if err != nil { 47 | return err 48 | } 49 | return nil 50 | } 51 | 52 | func SigningEntryIter(state *config.State, fn func(s *SigningEntry) error) error { 53 | files, err := ReadFileDatabase(state.Fs, state.Config.FilesDb) 54 | if err != nil { 55 | return fmt.Errorf("couldn't open database %v: %w", state.Config.FilesDb, err) 56 | } 57 | for _, s := range files { 58 | if err := fn(s); err != nil { 59 | return err 60 | } 61 | } 62 | return nil 63 | } 64 | 65 | func LandlockFromFileDatabase(state *config.State) error { 66 | var llrules []landlock.Rule 67 | files, err := ReadFileDatabase(state.Fs, state.Config.FilesDb) 68 | if err != nil { 69 | return err 70 | } 71 | for _, entry := range files { 72 | if entry.File == entry.OutputFile { 73 | // If file is the same as output, set RW+Trunc on file 74 | llrules = append(llrules, 75 | lsm.TruncFile(entry.File).IgnoreIfMissing(), 76 | ) 77 | } 78 | if entry.File != entry.OutputFile { 79 | // Set input file to RO, ignore if missing so we can bubble a useable 80 | // error to the user 81 | llrules = append(llrules, landlock.ROFiles(entry.File).IgnoreIfMissing()) 82 | 83 | // Check if output file exists 84 | // if it does we set RW on the file directly 85 | // if it doesnt, we set RW on the directory 86 | if ok, _ := afero.Exists(state.Fs, entry.OutputFile); ok { 87 | llrules = append(llrules, lsm.TruncFile(entry.OutputFile)) 88 | } else { 89 | llrules = append(llrules, landlock.RWDirs(filepath.Dir(entry.OutputFile))) 90 | } 91 | } 92 | } 93 | lsm.RestrictAdditionalPaths(llrules...) 94 | return nil 95 | } 96 | -------------------------------------------------------------------------------- /dmi/dmi.go: -------------------------------------------------------------------------------- 1 | package dmi 2 | 3 | import ( 4 | "strings" 5 | "time" 6 | 7 | "github.com/foxboron/sbctl/config" 8 | "github.com/foxboron/sbctl/fs" 9 | ) 10 | 11 | var Table = DMI{} 12 | 13 | type DMI struct { 14 | BoardName string `json:"board_name"` 15 | BoardVendor string `json:"board_vendor"` 16 | BoardVersion string `json:"board_version"` 17 | ChassisType string `json:"chassis_type"` 18 | FirmwareDate time.Time `json:"firmware_date"` 19 | FirmwareRelease string `json:"firmware_release"` 20 | FirmwareVendor string `json:"firmware_vendor"` 21 | FirmwareVersion string `json:"firmware_version"` 22 | ProductFamily string `json:"product_family"` 23 | ProductName string `json:"product_name"` 24 | ProductSKU string `json:"product_sku"` 25 | ProductVersion string `json:"product_version"` 26 | SystemVendor string `json:"system_vendor"` 27 | } 28 | 29 | func ParseDMI(state *config.State) DMI { 30 | dmi := DMI{} 31 | 32 | readValue := func(filename string) string { 33 | f, _ := fs.ReadFile(state.Fs, "/sys/devices/virtual/dmi/id/"+filename) 34 | return strings.TrimSpace(string(f)) 35 | } 36 | 37 | dmi.BoardName = readValue("board_name") 38 | dmi.BoardVendor = readValue("board_vendor") 39 | dmi.BoardVersion = readValue("board_version") 40 | dmi.ChassisType = readValue("chassis_type") 41 | dmi.FirmwareDate, _ = time.Parse("01/02/2006", readValue("bios_date")) 42 | dmi.FirmwareRelease = readValue("bios_release") 43 | dmi.FirmwareVendor = readValue("bios_vendor") 44 | dmi.FirmwareVersion = readValue("bios_version") 45 | dmi.ProductFamily = readValue("product_family") 46 | dmi.ProductName = readValue("product_name") 47 | dmi.ProductSKU = readValue("product_sku") 48 | dmi.ProductVersion = readValue("product_version") 49 | dmi.SystemVendor = readValue("sys_vendor") 50 | 51 | return dmi 52 | } 53 | -------------------------------------------------------------------------------- /dmi/dmi_test.go: -------------------------------------------------------------------------------- 1 | package dmi 2 | 3 | import ( 4 | "testing" 5 | "testing/fstest" 6 | "time" 7 | 8 | "github.com/foxboron/go-uefi/efi/efitest" 9 | "github.com/foxboron/sbctl/config" 10 | ) 11 | 12 | func TestDMIParse(t *testing.T) { 13 | f := efitest.NewFS().With( 14 | fstest.MapFS{"/sys/devices/virtual/dmi/id/bios_date": {Data: []byte("01/13/2023\n")}}, 15 | fstest.MapFS{"/sys/devices/virtual/dmi/id/bios_release": {Data: []byte("HorribleFirmwareRelease\n")}}, 16 | fstest.MapFS{"/sys/devices/virtual/dmi/id/bios_vendor": {Data: []byte("EmbarrassedFirmwareVendor\n")}}, 17 | fstest.MapFS{"/sys/devices/virtual/dmi/id/bios_version": {Data: []byte("InsecureFirmwareVersion\n")}}, 18 | fstest.MapFS{"/sys/devices/virtual/dmi/id/board_name": {Data: []byte("BadBoardName\n")}}, 19 | fstest.MapFS{"/sys/devices/virtual/dmi/id/board_vendor": {Data: []byte("IncompetentBoardVendor\n")}}, 20 | fstest.MapFS{"/sys/devices/virtual/dmi/id/board_version": {Data: []byte("WeirdBoardVersion\n")}}, 21 | fstest.MapFS{"/sys/devices/virtual/dmi/id/chassis_type": {Data: []byte("3\n")}}, 22 | fstest.MapFS{"/sys/devices/virtual/dmi/id/product_family": {Data: []byte("MediocreProductFamily\n")}}, 23 | fstest.MapFS{"/sys/devices/virtual/dmi/id/product_name": {Data: []byte("AwfulProductName\n")}}, 24 | fstest.MapFS{"/sys/devices/virtual/dmi/id/product_sku": {Data: []byte("RandomProductSKU\n")}}, 25 | fstest.MapFS{"/sys/devices/virtual/dmi/id/product_version": {Data: []byte("CrazyProductVersion\n")}}, 26 | fstest.MapFS{"/sys/devices/virtual/dmi/id/sys_vendor": {Data: []byte("EvilSystemVendor\n")}}, 27 | efitest.SecureBootOn(), 28 | ).SetFS() 29 | 30 | dmiTable := ParseDMI(&config.State{Fs: f.ToAfero()}) 31 | 32 | if dmiTable.BoardName != "BadBoardName" { 33 | t.Fatal("BoardName: expected 'BadBoardName', got '" + dmiTable.BoardName + "'") 34 | } 35 | if dmiTable.BoardVendor != "IncompetentBoardVendor" { 36 | t.Fatal("BoardVendor: expected 'IncompetentBoardVendor', got '" + dmiTable.BoardVendor + "'") 37 | } 38 | if dmiTable.BoardVersion != "WeirdBoardVersion" { 39 | t.Fatal("BoardVersion: expected 'WeirdBoardVersion', got '" + dmiTable.BoardVersion + "'") 40 | } 41 | if dmiTable.ChassisType != "3" { 42 | t.Fatal("ChassisType: expected '3', got '" + dmiTable.ChassisType + "'") 43 | } 44 | if dmiTable.FirmwareDate != time.Date(2023, 1, 13, 0, 0, 0, 0, time.UTC) { 45 | t.Fatal("FirmwareDate: expected '2023-01-13', got '" + dmiTable.FirmwareDate.Format("2006-01-02") + "'") 46 | } 47 | if dmiTable.FirmwareRelease != "HorribleFirmwareRelease" { 48 | t.Fatal("FirmwareRelease: expected 'HorribleFirmwareRelease', got '" + dmiTable.FirmwareRelease + "'") 49 | } 50 | if dmiTable.FirmwareVendor != "EmbarrassedFirmwareVendor" { 51 | t.Fatal("FirmwareVendor: expected 'EmbarrassedFirmwareVendor', got '" + dmiTable.FirmwareVendor + "'") 52 | } 53 | if dmiTable.FirmwareVersion != "InsecureFirmwareVersion" { 54 | t.Fatal("FirmwareVersion: expected 'InsecureFirmwareVersion', got '" + dmiTable.FirmwareVersion + "'") 55 | } 56 | if dmiTable.ProductFamily != "MediocreProductFamily" { 57 | t.Fatal("ProductFamily: expected 'MediocreProductFamily', got '" + dmiTable.ProductFamily + "'") 58 | } 59 | if dmiTable.ProductName != "AwfulProductName" { 60 | t.Fatal("ProductName: expected 'AwfulProductName', got '" + dmiTable.ProductName + "'") 61 | } 62 | if dmiTable.ProductSKU != "RandomProductSKU" { 63 | t.Fatal("ProductSKU: expected 'RandomProductSKU', got '" + dmiTable.ProductSKU + "'") 64 | } 65 | if dmiTable.ProductVersion != "CrazyProductVersion" { 66 | t.Fatal("ProductVersion: expected 'CrazyProductVersion' , got '" + dmiTable.ProductVersion + "'") 67 | } 68 | if dmiTable.SystemVendor != "EvilSystemVendor" { 69 | t.Fatal("SystemVendor: expected 'EvilSystemVendor', got '" + dmiTable.SystemVendor + "'") 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /docs/asciidoc.conf: -------------------------------------------------------------------------------- 1 | ## linkman: macro 2 | # Inspired by/borrowed from the GIT source tree at Documentation/asciidoc.conf 3 | # 4 | # Usage: linkman:command[manpage-section] 5 | # 6 | # Note, {0} is the manpage section, while {target} is the command. 7 | # 8 | # Show man link as: (
); if section is defined, else just show 9 | # the command. 10 | 11 | [macros] 12 | (?su)[\\]?(?Plinkman):(?P\S*?)\[(?P.*?)\]= 13 | 14 | [attributes] 15 | asterisk=* 16 | plus=+ 17 | caret=^ 18 | startsb=[ 19 | endsb=] 20 | backslash=\ 21 | tilde=~ 22 | apostrophe=' 23 | backtick=` 24 | litdd=-- 25 | 26 | ifdef::backend-docbook[] 27 | [linkman-inlinemacro] 28 | {0%{target}} 29 | {0#} 30 | {0#{target}{0}} 31 | {0#} 32 | endif::backend-docbook[] 33 | 34 | ifdef::backend-xhtml11[] 35 | [linkman-inlinemacro] 36 | {target}{0?({0})} 37 | endif::backend-xhtml11[] 38 | -------------------------------------------------------------------------------- /docs/sbctl.conf.5.txt: -------------------------------------------------------------------------------- 1 | sbctl.conf(5) 2 | ============= 3 | 4 | Name 5 | ---- 6 | sbctl.conf - the sbctl configuration file 7 | 8 | Synopsis 9 | -------- 10 | 11 | /etc/sbctl/sbctl.conf 12 | 13 | Description 14 | ----------- 15 | 16 | The sbctl configuration file is a YAML file. It is read on startup if present. 17 | 18 | The file can be used for initial setup of a sbctl installation. 19 | 20 | 21 | Configuration directories and precedence 22 | ---------------------------------------- 23 | 24 | The configuration file is currently only read from /etc/sbctl. This might change 25 | in the future. 26 | 27 | 28 | Options 29 | ------- 30 | 31 | *keydir:* /path/to/key/dir :: 32 | Defines the directory where sbctl will look for keys. 33 | + 34 | Default: /var/lib/sbctl/keys 35 | 36 | *guid:* /path/to/guid/file :: 37 | The location of the file that defines the user created GUID. 38 | + 39 | The GUID is used to unique identify the list of certificates stored in the 40 | EFI variables. 41 | + 42 | Default: /var/lib/sbctl/GUID 43 | 44 | *files_db:* /path/to/files/json :: 45 | The location of the json file storing the files sbctl will sign. 46 | + 47 | Default: /var/lib/sbctl/files.json 48 | 49 | *bundles_db:* /path/to/bundles/json :: 50 | The location of the json file storing the bundles sbctl will sign. 51 | + 52 | Default: /var/lib/sbctl/bundles.json 53 | 54 | *landlock:* bool :: 55 | Enable or disable the landlock sandboxing of sbctl. 56 | + 57 | Default: true 58 | 59 | *db_additions:* [ options... ] 60 | Include additional keys or checksums into the authorization database for 61 | Secure Boot. These values are synonymous with the flags passed to *sbctl enroll-keys*. 62 | + 63 | Valid values: microsoft, tpm-eventlog, firmware-builtin, custom 64 | 65 | *files:* [ [*path:* /path/to/file *output:* /path/to/output ], ... ]:: 66 | A list of files sbctl will sign upon setup. It will be used to seed the 67 | files_db during initial setup. 68 | + 69 | *path*;; 70 | Absolute path to a file that sbctl should sign. 71 | + 72 | *output*;; 73 | An optional absolute output path for the signed file. 74 | 75 | *keys:* {*pk:* {...}, *kek:* {...}, *db:* {...}} :: 76 | A key-value pair for all the keys in the key hierarchy used for Secure Boot. 77 | It is used for the initial bootstrap during setup. 78 | 79 | * pk 80 | * kek 81 | * db 82 | 83 | :: Each of the hierarchies can specify key type and location for the private 84 | key and certificate file independent of each other. This allows users to 85 | keep some keys on different storage mediums depending on needs. 86 | An example would be to keep the db key as an unencrypted file easily 87 | accessible for signing and the PK on a hardware backed enclave to be better 88 | secure the key material. 89 | + 90 | 91 | *privkey:* /path/to/privatekey/file ;; 92 | Path to the private key. 93 | + 94 | Defaults: 95 | * *pk:* /var/lib/sbctl/keys/PK/PK.key 96 | * *kek:* /var/lib/sbctl/keys/KEK/KEK.key 97 | * *db*: /var/lib/sbctl/keys/db/db.key 98 | 99 | *pubkey:* /path/to/certificate/file ;; 100 | Path to the public key. 101 | + 102 | Default: 103 | * *pk:* /var/lib/sbctl/keys/PK/PK.pem 104 | * *kek:* /var/lib/sbctl/keys/KEK/KEK.pem 105 | * *db*: /var/lib/sbctl/keys/db/db.pem 106 | 107 | *type:* file ;; 108 | The type of key used for this signing key. 109 | + 110 | Only the key type of *file* is currently supported by sbctl. 111 | + 112 | Default: file 113 | 114 | 115 | Example 116 | ------- 117 | 118 | An example of a /etc/sbctl/sbctl.conf file with the default values. 119 | 120 | --- 121 | keydir: /var/lib/sbctl/keys 122 | guid: /var/lib/sbctl/GUID 123 | files_db: /var/lib/sbctl/files.json 124 | bundles_db: /var/lib/sbctl/bundles.json 125 | landlock: true 126 | db_additions: 127 | - microsoft 128 | files: 129 | - path: /boot/vmlinuz-linux 130 | output: /boot/vmlinuz-linux 131 | - path: /efi/EFI/Linux/arch-linux.efi 132 | output: /efi/EFI/Linux/arch-linux.efi 133 | keys: 134 | pk: 135 | privkey: /var/lib/sbctl/keys/PK/PK.key 136 | pubkey: /var/lib/sbctl/keys/PK/PK.pem 137 | type: file 138 | kek: 139 | privkey: /var/lib/sbctl/keys/KEK/KEK.key 140 | pubkey: /var/lib/sbctl/keys/KEK/KEK.pem 141 | type: file 142 | db: 143 | privkey: /var/lib/sbctl/keys/db/db.key 144 | pubkey: /var/lib/sbctl/keys/db/db.pem 145 | type: file 146 | 147 | See Also 148 | -------- 149 | linkman:sbctl[8] 150 | 151 | Authors 152 | ------- 153 | 154 | * Morten Linderud 155 | -------------------------------------------------------------------------------- /docs/workflow-example-images/01 - Boot Menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Foxboron/sbctl/30cc0e7b22a0f7185e9962f009ead32c2e482877/docs/workflow-example-images/01 - Boot Menu.png -------------------------------------------------------------------------------- /docs/workflow-example-images/02 - Secure Boot Menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Foxboron/sbctl/30cc0e7b22a0f7185e9962f009ead32c2e482877/docs/workflow-example-images/02 - Secure Boot Menu.png -------------------------------------------------------------------------------- /docs/workflow-example-images/03 - Key Management Menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Foxboron/sbctl/30cc0e7b22a0f7185e9962f009ead32c2e482877/docs/workflow-example-images/03 - Key Management Menu.png -------------------------------------------------------------------------------- /docs/workflow-example-images/04 - Delete PK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Foxboron/sbctl/30cc0e7b22a0f7185e9962f009ead32c2e482877/docs/workflow-example-images/04 - Delete PK.png -------------------------------------------------------------------------------- /docs/workflow-example-images/05 - Delete PK Confirmation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Foxboron/sbctl/30cc0e7b22a0f7185e9962f009ead32c2e482877/docs/workflow-example-images/05 - Delete PK Confirmation.png -------------------------------------------------------------------------------- /docs/workflow-example-images/06 - Keys Cleared.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Foxboron/sbctl/30cc0e7b22a0f7185e9962f009ead32c2e482877/docs/workflow-example-images/06 - Keys Cleared.png -------------------------------------------------------------------------------- /docs/workflow-example-images/07 - Secure Boot Disabled, PK Loaded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Foxboron/sbctl/30cc0e7b22a0f7185e9962f009ead32c2e482877/docs/workflow-example-images/07 - Secure Boot Disabled, PK Loaded.png -------------------------------------------------------------------------------- /docs/workflow-example-images/08 - Secure Boot Disabled, PK Unloaded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Foxboron/sbctl/30cc0e7b22a0f7185e9962f009ead32c2e482877/docs/workflow-example-images/08 - Secure Boot Disabled, PK Unloaded.png -------------------------------------------------------------------------------- /docs/workflow-example-images/09 - Secure Boot Custom Keys.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Foxboron/sbctl/30cc0e7b22a0f7185e9962f009ead32c2e482877/docs/workflow-example-images/09 - Secure Boot Custom Keys.png -------------------------------------------------------------------------------- /docs/workflow-example-images/10 - Custom Keys.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Foxboron/sbctl/30cc0e7b22a0f7185e9962f009ead32c2e482877/docs/workflow-example-images/10 - Custom Keys.png -------------------------------------------------------------------------------- /docs/workflow-example.md: -------------------------------------------------------------------------------- 1 | # Example Workflow 2 | 3 | This is an example workflow for enrolling custom secure boot keys on a ASUS 4 | Z170-A motherboard. These instructions can be applied to any other firmware, 5 | but the exact steps to be taken in the firmware setup menus may differ. 6 | 7 | 1. Enter UEFI setup menu by press either of F2/Del/Esc/F10/F11/F12 depending 8 | on your firmware or by using `systemctl --firmware-setup reboot` 9 | 10 | 2. Open the `Boot/Secure Boot` menu: 11 | ![Boot Menu](workflow-example-images/01%20-%20Boot%20Menu.png) 12 | 13 | 3. Do not change `OS Type` to `Custom` as this will not enable `Setup Mode`! 14 | Instead open the sub-menu `Key Management`: 15 | ![Secure Boot Menu](workflow-example-images/02%20-%20Secure%20Boot%20Menu.png) 16 | 17 | 4. Use `Clear Secure Boot Keys` to enter `Setup Mode`: 18 | ![Clear Secure Boot Keys](workflow-example-images/03%20-%20Key%20Management%20Menu.png) 19 | 20 | 5. If your firmware does not provide this, you will have to manually delete the 21 | keys. Open `PK Management` to do so and repeat this step for KEK, DB and DBX: 22 | ![Delete PK](workflow-example-images/04%20-%20Delete%20PK.png) 23 | ![Delete PK Confirmation](workflow-example-images/05%20-%20Delete%20PK%20Confirmation.png) 24 | 25 | 6. The secure boot keys should now be cleared… 26 | ![Secure Boot Keys Cleared](workflow-example-images/06%20-%20Keys%20Cleared.png) 27 | 28 | 7. And secure boot should now be disabled. The platform key will remain loaded 29 | until the system is rebooted. 30 | ![Secure Boot Disabled, Platform Key Loaded](workflow-example-images/07%20-%20Secure%20Boot%20Disabled,%20PK%20Loaded.png) 31 | 32 | 8. Exit the firmware with the save and reset option (even if it says no changes 33 | have been performed). You may optionally enter the firmware setup again to 34 | confirm: 35 | ![Secure Boot Disabled, Platform Key Unloaded](workflow-example-images/08%20-%20Secure%20Boot%20Disabled,%20PK%20Unloaded.png) 36 | 37 | 9. Confirm that setup mode is enabled: 38 | ``` 39 | # sbctl status 40 | Installed: ✘ Sbctl is not installed 41 | Setup Mode: ✘ Enabled 42 | Secure Boot: ✘ Disabled 43 | ``` 44 | 45 | 10. Create custom secure boot keys: 46 | ``` 47 | # sbctl create-keys 48 | Created Owner UUID a9fbbdb7-a05f-48d5-b63a-08c5df45ee70 49 | Creating secure boot keys...✔ 50 | Secure boot keys created! 51 | ``` 52 | 53 | 11. Enroll custom secure boot keys: 54 | ``` 55 | # sbctl enroll-keys 56 | Enrolling keys to EFI variables...✔ 57 | Enrolled keys to the EFI variables! 58 | ``` 59 | 60 | 12. Confirm that setup mode is disabled now. At this point, the device is in 61 | secure boot mode (this may only be reflected after a reboot): 62 | ``` 63 | # sbctl status 64 | Installed: ✔ Sbctl is installed 65 | Owner GUID: a9fbbdb7-a05f-48d5-b63a-08c5df45ee70 66 | Setup Mode: ✔ Disabled 67 | Secure Boot: ✘ Disabled 68 | ``` 69 | 70 | 13. **Sign your bootloader and kernels with `sbctl` before rebooting!** 71 | 72 | 13. Optionally, observe the secure boot state in the firmware menu after 73 | rebooting: 74 | ![Secure Boot With Custom Keys](workflow-example-images/09%20-%20Secure%20Boot%20Custom%20Keys.png) 75 | ![Secure Boot Custom Keys](workflow-example-images/10%20-%20Custom%20Keys.png) 76 | 77 | 15. Confirm secure boot state after reboot: 78 | ``` 79 | # sbctl status 80 | Installed: ✔ Sbctl is installed 81 | Owner GUID: a9fbbdb7-a05f-48d5-b63a-08c5df45ee70 82 | Setup Mode: ✔ Disabled 83 | Secure Boot: ✔ Enabled 84 | ``` 85 | -------------------------------------------------------------------------------- /fs/fs.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "io" 5 | "os" 6 | 7 | "github.com/spf13/afero" 8 | ) 9 | 10 | // Afero misses a few functions. So copy-pasted os/file.go functions here 11 | func WriteFile(fs afero.Fs, name string, data []byte, perm os.FileMode) error { 12 | f, err := fs.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm) 13 | if err != nil { 14 | return err 15 | } 16 | _, err = f.Write(data) 17 | if err1 := f.Close(); err1 != nil && err == nil { 18 | err = err1 19 | } 20 | return err 21 | } 22 | 23 | func ReadFile(fs afero.Fs, name string) ([]byte, error) { 24 | f, err := fs.Open(name) 25 | if err != nil { 26 | return nil, err 27 | } 28 | defer f.Close() 29 | 30 | var size int 31 | if info, err := f.Stat(); err == nil { 32 | size64 := info.Size() 33 | if int64(int(size64)) == size64 { 34 | size = int(size64) 35 | } 36 | } 37 | size++ // one byte for final read at EOF 38 | if size < 512 { 39 | size = 512 40 | } 41 | 42 | data := make([]byte, 0, size) 43 | for { 44 | if len(data) >= cap(data) { 45 | d := append(data[:cap(data)], 0) 46 | data = d[:len(data)] 47 | } 48 | n, err := f.Read(data[len(data):cap(data)]) 49 | data = data[:len(data)+n] 50 | if err != nil { 51 | if err == io.EOF { 52 | err = nil 53 | } 54 | return data, err 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/foxboron/sbctl 2 | 3 | go 1.22.0 4 | 5 | toolchain go1.22.5 6 | 7 | require ( 8 | github.com/fatih/color v1.17.0 9 | github.com/foxboron/go-tpm-keyfiles v0.0.0-20240725205618-b7c5a84edf9d 10 | github.com/foxboron/go-uefi v0.0.0-20250207204325-69fb7dba244f 11 | github.com/goccy/go-yaml v1.11.3 12 | github.com/google/go-attestation v0.5.1 13 | github.com/google/go-tpm v0.9.1 14 | github.com/google/uuid v1.4.0 15 | github.com/hugelgupf/vmtest v0.0.0-20240110072021-f6f07acb7aa1 16 | github.com/landlock-lsm/go-landlock v0.0.0-20240715193425-db0c8d6f1dff 17 | github.com/onsi/gomega v1.7.1 18 | github.com/spf13/afero v1.11.0 19 | github.com/spf13/cobra v1.8.1 20 | golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 21 | golang.org/x/sys v0.28.0 22 | ) 23 | 24 | require ( 25 | github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 // indirect 26 | github.com/creack/pty v1.1.21 // indirect 27 | github.com/dustin/go-humanize v1.0.1 // indirect 28 | github.com/google/certificate-transparency-go v1.1.2 // indirect 29 | github.com/google/go-tpm-tools v0.4.4 // indirect 30 | github.com/google/go-tspi v0.3.0 // indirect 31 | github.com/google/goterm v0.0.0-20200907032337-555d40f16ae2 // indirect 32 | github.com/hashicorp/errwrap v1.1.0 // indirect 33 | github.com/hashicorp/go-multierror v1.1.1 // indirect 34 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 35 | github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 // indirect 36 | github.com/josharian/native v1.1.0 // indirect 37 | github.com/klauspost/compress v1.17.4 // indirect 38 | github.com/klauspost/pgzip v1.2.6 // indirect 39 | github.com/mattn/go-colorable v0.1.13 // indirect 40 | github.com/mattn/go-isatty v0.0.20 // indirect 41 | github.com/mdlayher/packet v1.1.2 // indirect 42 | github.com/mdlayher/socket v0.5.0 // indirect 43 | github.com/pierrec/lz4/v4 v4.1.14 // indirect 44 | github.com/pkg/errors v0.9.1 // indirect 45 | github.com/spf13/pflag v1.0.5 // indirect 46 | github.com/stretchr/objx v0.5.0 // indirect 47 | github.com/u-root/gobusybox/src v0.0.0-20231224233253-2944a440b6b6 // indirect 48 | github.com/u-root/u-root v0.11.1-0.20230807200058-f87ad7ccb594 // indirect 49 | github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 // indirect 50 | github.com/ulikunitz/xz v0.5.11 // indirect 51 | github.com/vishvananda/netlink v1.2.1-beta.2 // indirect 52 | github.com/vishvananda/netns v0.0.4 // indirect 53 | golang.org/x/crypto v0.31.0 // indirect 54 | golang.org/x/mod v0.20.0 // indirect 55 | golang.org/x/net v0.33.0 // indirect 56 | golang.org/x/sync v0.10.0 // indirect 57 | golang.org/x/text v0.21.0 // indirect 58 | golang.org/x/tools v0.24.0 // indirect 59 | golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect 60 | gopkg.in/yaml.v2 v2.4.0 // indirect 61 | kernel.org/pub/linux/libs/security/libcap/psx v1.2.70 // indirect 62 | src.elv.sh v0.16.0-rc1.0.20220116211855-fda62502ad7f // indirect 63 | ) 64 | -------------------------------------------------------------------------------- /guid.go: -------------------------------------------------------------------------------- 1 | package sbctl 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/foxboron/sbctl/fs" 7 | "github.com/google/uuid" 8 | "github.com/spf13/afero" 9 | ) 10 | 11 | func CreateUUID() []byte { 12 | id, _ := uuid.NewRandom() 13 | return []byte(id.String()) 14 | } 15 | 16 | func CreateGUID(vfs afero.Fs, guidPath string) ([]byte, error) { 17 | var uuid []byte 18 | if _, err := vfs.Stat(guidPath); os.IsNotExist(err) { 19 | uuid = CreateUUID() 20 | err := fs.WriteFile(vfs, guidPath, uuid, 0644) 21 | if err != nil { 22 | return nil, err 23 | } 24 | } else { 25 | uuid, err = fs.ReadFile(vfs, guidPath) 26 | if err != nil { 27 | return nil, err 28 | } 29 | } 30 | return uuid, nil 31 | } 32 | -------------------------------------------------------------------------------- /hierarchy/hierarchy.go: -------------------------------------------------------------------------------- 1 | package hierarchy 2 | 3 | import "github.com/foxboron/go-uefi/efivar" 4 | 5 | type Hierarchy uint8 6 | 7 | const ( 8 | PK Hierarchy = iota + 1 9 | KEK 10 | Db 11 | Dbx 12 | ) 13 | 14 | func (h Hierarchy) String() string { 15 | switch h { 16 | case PK: 17 | return "PK" 18 | case KEK: 19 | return "KEK" 20 | case Db: 21 | return "db" 22 | case Dbx: 23 | return "dbx" 24 | default: 25 | return "unknown" 26 | } 27 | } 28 | 29 | func (h Hierarchy) Description() string { 30 | switch h { 31 | case PK: 32 | return "Platform Key" 33 | case KEK: 34 | return "Key Exchange Key" 35 | case Db: 36 | return "Database Key" 37 | case Dbx: 38 | return "Forbidden Database" 39 | default: 40 | return "unknown" 41 | } 42 | } 43 | 44 | func (h Hierarchy) Efivar() efivar.Efivar { 45 | switch h { 46 | case PK: 47 | return efivar.PK 48 | case KEK: 49 | return efivar.KEK 50 | case Db: 51 | return efivar.Db 52 | case Dbx: 53 | return efivar.Dbx 54 | default: 55 | return efivar.Efivar{} 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /keys.go: -------------------------------------------------------------------------------- 1 | package sbctl 2 | 3 | import ( 4 | "bytes" 5 | "crypto" 6 | "errors" 7 | "fmt" 8 | "os" 9 | "path/filepath" 10 | 11 | "github.com/foxboron/go-uefi/authenticode" 12 | "github.com/foxboron/go-uefi/efi" 13 | "github.com/foxboron/sbctl/backend" 14 | "github.com/foxboron/sbctl/certs" 15 | "github.com/foxboron/sbctl/config" 16 | "github.com/foxboron/sbctl/fs" 17 | "github.com/foxboron/sbctl/hierarchy" 18 | "github.com/spf13/afero" 19 | ) 20 | 21 | func EnrollCustom(customBytes []byte, efivar string) error { 22 | return efi.WriteEFIVariable(efivar, customBytes) 23 | } 24 | 25 | func VerifyFile(state *config.State, kh *backend.KeyHierarchy, ev hierarchy.Hierarchy, file string) (bool, error) { 26 | peFile, err := state.Fs.Open(file) 27 | if err != nil { 28 | return false, err 29 | } 30 | defer peFile.Close() 31 | return kh.VerifyFile(ev, peFile) 32 | } 33 | 34 | var ErrAlreadySigned = errors.New("already signed file") 35 | 36 | func SignFile(state *config.State, kh *backend.KeyHierarchy, ev hierarchy.Hierarchy, file, output string) error { 37 | // Check to see if input and output binary is the same 38 | var same bool 39 | 40 | // Make sure that output is always populated by atleast the file path 41 | if output == "" { 42 | output = file 43 | } 44 | 45 | // Check file exists before we do anything 46 | if _, err := state.Fs.Stat(file); errors.Is(err, os.ErrNotExist) { 47 | return fmt.Errorf("%s does not exist", file) 48 | } 49 | 50 | // We want to write the file back with correct permissions 51 | si, err := state.Fs.Stat(file) 52 | if err != nil { 53 | return fmt.Errorf("failed stat of file: %w", err) 54 | } 55 | 56 | peFile, err := state.Fs.Open(file) 57 | if err != nil { 58 | return err 59 | } 60 | defer peFile.Close() 61 | 62 | inputBinary, err := authenticode.Parse(peFile) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | // Check if the files are identical 68 | if file != output { 69 | if outputFile, err := state.Fs.Open(output); err == nil { 70 | defer outputFile.Close() 71 | outputBinary, err := authenticode.Parse(outputFile) 72 | if err != nil { 73 | return err 74 | } 75 | b := outputBinary.Hash(crypto.SHA256) 76 | bb := inputBinary.Hash(crypto.SHA256) 77 | if bytes.Equal(b, bb) { 78 | same = true 79 | } 80 | } 81 | } 82 | 83 | if file == output { 84 | same = true 85 | } 86 | 87 | // Let's check if we have signed it already AND the original file hasn't changed 88 | // TODO: This will run authenticode.Parse again, *and* open the file 89 | // this should be refactored to be nicer 90 | ok, err := VerifyFile(state, kh, ev, output) 91 | if errors.Is(err, authenticode.ErrNoValidSignatures) { 92 | // If we tried to verify the file, but it has signatures but nothing signed 93 | // by our key, we catch the error and continue. 94 | } else if errors.Is(err, os.ErrNotExist) { 95 | // Ignore the error if the file doesn't exist 96 | } else if ok && same { 97 | // If already signed, and the input/output binaries are identical, 98 | // we can just assume everything is fine. 99 | return ErrAlreadySigned 100 | } else if err != nil { 101 | return err 102 | } 103 | 104 | b, err := kh.SignFile(ev, inputBinary) 105 | if err != nil { 106 | return err 107 | } 108 | 109 | if err = fs.WriteFile(state.Fs, output, b, si.Mode()); err != nil { 110 | return err 111 | } 112 | 113 | return nil 114 | } 115 | 116 | // Map up our default keys in a struct 117 | var SecureBootKeys = []struct { 118 | Key string 119 | Description string 120 | }{ 121 | { 122 | Key: "PK", 123 | Description: "Platform Key", 124 | }, 125 | { 126 | Key: "KEK", 127 | Description: "Key Exchange Key", 128 | }, 129 | { 130 | Key: "db", 131 | Description: "Database Key", 132 | }, 133 | // { 134 | // Key: "dbx", 135 | // Description: "Forbidden Database Key", 136 | // }, 137 | } 138 | 139 | // Check if we have already intialized keys in the given output directory 140 | func CheckIfKeysInitialized(vfs afero.Fs, output string) bool { 141 | for _, key := range SecureBootKeys { 142 | path := filepath.Join(output, key.Key) 143 | if _, err := vfs.Stat(path); errors.Is(err, os.ErrNotExist) { 144 | return false 145 | } 146 | } 147 | return true 148 | } 149 | 150 | func GetEnrolledVendorCerts() []string { 151 | db, err := efi.Getdb() 152 | if err != nil { 153 | return []string{} 154 | } 155 | return certs.DetectVendorCerts(db) 156 | } 157 | -------------------------------------------------------------------------------- /logging/logging.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | 8 | "github.com/fatih/color" 9 | ) 10 | 11 | var ( 12 | OkSym = "✓" 13 | NotOkSym = "✗" 14 | WarnSym = "‼" 15 | UnkwnSym = "⁇" 16 | ) 17 | var ( 18 | OkSymText = "[+]" 19 | NotOkSymText = "[-]" 20 | WarnSymText = "[!]" 21 | UnkwnSymText = "[?]" 22 | ) 23 | 24 | var ( 25 | ok string 26 | notok string 27 | warn string 28 | unkwn string 29 | ) 30 | 31 | var ( 32 | on bool 33 | DisableInfo bool = false 34 | output io.Writer = os.Stdout 35 | ) 36 | 37 | func PrintOn() { 38 | on = true 39 | } 40 | 41 | func PrintOff() { 42 | on = false 43 | } 44 | 45 | func SetOutput(w io.Writer) { 46 | output = w 47 | } 48 | 49 | func PrintWithFile(f io.Writer, msg string, a ...interface{}) { 50 | if on { 51 | fmt.Fprintf(f, msg, a...) 52 | } 53 | } 54 | 55 | func Print(msg string, a ...interface{}) { 56 | if DisableInfo && output == os.Stdout { 57 | return 58 | } 59 | PrintWithFile(output, msg, a...) 60 | } 61 | 62 | func Println(msg string) { 63 | if DisableInfo && output == os.Stdout { 64 | return 65 | } 66 | PrintWithFile(output, msg+"\n") 67 | } 68 | 69 | func Okf(m string, a ...interface{}) string { 70 | return fmt.Sprintf("%s %s\n", ok, fmt.Sprintf(m, a...)) 71 | } 72 | 73 | // Print ok string to stdout 74 | func Ok(m string, a ...interface{}) { 75 | Print(Okf(m, a...)) 76 | } 77 | 78 | func NotOkf(m string, a ...interface{}) string { 79 | return fmt.Sprintf("%s %s\n", notok, fmt.Sprintf(m, a...)) 80 | } 81 | 82 | // Print ok string to stdout 83 | func NotOk(m string, a ...interface{}) { 84 | Print(NotOkf(m, a...)) 85 | } 86 | 87 | func Unknownf(m string, a ...interface{}) string { 88 | return fmt.Sprintf("%s %s\n", unkwn, fmt.Sprintf(m, a...)) 89 | } 90 | 91 | func Unknown(m string, a ...interface{}) { 92 | Print(Unknownf(m, a...)) 93 | } 94 | 95 | func Warnf(m string, a ...interface{}) string { 96 | return fmt.Sprintf("%s %s\n", warn, fmt.Sprintf(m, a...)) 97 | } 98 | func Warn(m string, a ...interface{}) { 99 | PrintWithFile(os.Stderr, Warnf(m, a...)) 100 | } 101 | 102 | func Fatalf(m string, a ...interface{}) string { 103 | return color.New(color.FgRed, color.Bold).Sprintf("%s %s\n", UnkwnSym, fmt.Sprintf(m, a...)) 104 | } 105 | 106 | func Fatal(err error) { 107 | PrintWithFile(os.Stderr, Fatalf(err.Error())) 108 | } 109 | 110 | func Errorf(m string, a ...interface{}) string { 111 | return color.New(color.FgRed, color.Bold).Sprintf("%s\n", fmt.Sprintf(m, a...)) 112 | } 113 | 114 | func Error(err error) { 115 | PrintWithFile(os.Stderr, Errorf(err.Error())) 116 | } 117 | 118 | func init() { 119 | if ok := os.Getenv("SBCTL_UNICODE"); ok == "0" { 120 | OkSym = OkSymText 121 | NotOkSym = NotOkSymText 122 | WarnSym = WarnSymText 123 | UnkwnSym = UnkwnSymText 124 | } 125 | 126 | ok = color.New(color.FgGreen, color.Bold).Sprintf(OkSym) 127 | notok = color.New(color.FgRed, color.Bold).Sprintf(NotOkSym) 128 | warn = color.New(color.FgYellow, color.Bold).Sprintf(WarnSym) 129 | unkwn = color.New(color.FgRed, color.Bold).Sprintf(UnkwnSym) 130 | PrintOn() 131 | } 132 | -------------------------------------------------------------------------------- /lsm/lsm.go: -------------------------------------------------------------------------------- 1 | package lsm 2 | 3 | import ( 4 | "log/slog" 5 | "path/filepath" 6 | 7 | "github.com/foxboron/sbctl/config" 8 | "github.com/landlock-lsm/go-landlock/landlock" 9 | 10 | ll "github.com/landlock-lsm/go-landlock/landlock/syscall" 11 | ) 12 | 13 | var ( 14 | rules []landlock.Rule 15 | 16 | // Include file truncation 17 | truncFile landlock.AccessFSSet = ll.AccessFSExecute | ll.AccessFSWriteFile | ll.AccessFSReadFile | ll.AccessFSTruncate 18 | ) 19 | 20 | func TruncFile(p string) landlock.FSRule { 21 | return landlock.PathAccess(truncFile, p) 22 | } 23 | 24 | func LandlockRulesFromConfig(conf *config.Config) { 25 | rules = append(rules, 26 | landlock.RODirs( 27 | "/sys/devices/virtual/dmi/id/", 28 | ).IgnoreIfMissing(), 29 | landlock.RWDirs( 30 | filepath.Dir(conf.Keydir), 31 | // It seems to me that RWFiles should work on efivars, but it doesn't. 32 | // TODO: Lock this down to induvidual files? 33 | "/sys/firmware/efi/efivars/", 34 | ).IgnoreIfMissing(), 35 | landlock.ROFiles( 36 | "/sys/kernel/security/tpm0/binary_bios_measurements", 37 | // Go timezone reads /etc/localtime 38 | "/etc/localtime", 39 | ).IgnoreIfMissing(), 40 | landlock.RWFiles( 41 | conf.GUID, 42 | conf.FilesDb, 43 | conf.BundlesDb, 44 | // Enable the TPM devices by default if they exist 45 | "/dev/tpm0", "/dev/tpmrm0", 46 | ).IgnoreIfMissing(), 47 | ) 48 | } 49 | 50 | func RestrictAdditionalPaths(r ...landlock.Rule) { 51 | rules = append(rules, r...) 52 | } 53 | 54 | func Restrict() error { 55 | for _, r := range rules { 56 | slog.Debug("landlock", slog.Any("rule", r)) 57 | } 58 | landlock.V5.BestEffort().RestrictNet() 59 | return landlock.V5.BestEffort().RestrictPaths(rules...) 60 | } 61 | -------------------------------------------------------------------------------- /quirks/fq0001.go: -------------------------------------------------------------------------------- 1 | package quirks 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/foxboron/sbctl/dmi" 7 | ) 8 | 9 | var FQ0001 = Quirk { 10 | ID: "FQ0001", 11 | Name: "Defaults to executing on Secure Boot policy violation", 12 | Severity: "CRITICAL", 13 | } 14 | 15 | func HasFQ0001() bool { 16 | unaffectedVersions := []unaffectedVersion{ 17 | // MSI MAG Z490 TOMAHAWK 18 | {Name: "MS-7C80", NameSrc: &dmi.Table.ProductName, NameStrict: true, Version: "1.B0", VersionSrc: &dmi.Table.FirmwareVersion}, 19 | // MSI H310M PRO-C 20 | {Name: "MS-7D02", NameSrc: &dmi.Table.ProductName, NameStrict: true, Version: "1.20", VersionSrc: &dmi.Table.FirmwareVersion}, 21 | // MSI MPG X670E CARBON WIFI 22 | {Name: "MS-7D70", NameSrc: &dmi.Table.ProductName, NameStrict: true, Version: "1.K0", VersionSrc: &dmi.Table.FirmwareVersion}, 23 | } 24 | 25 | affectedDateRanges := []affectedDateRange{ 26 | {From: time.Date(2022, 5, 10, 0, 0, 0, 0, time.UTC)}, 27 | } 28 | 29 | affectedDevices := []affectedDevice{ 30 | // MSI AMD boards 31 | {Name: "X570", NameSrc: &dmi.Table.BoardName, NameStrict: false, DateFrom: time.Date(2021, 12, 16, 0, 0, 0, 0, time.UTC)}, 32 | {Name: "X470", NameSrc: &dmi.Table.BoardName, NameStrict: false, DateFrom: time.Date(2021, 9, 28, 0, 0, 0, 0, time.UTC)}, 33 | {Name: "B550", NameSrc: &dmi.Table.BoardName, NameStrict: false, DateFrom: time.Date(2021, 12, 13, 0, 0, 0, 0, time.UTC)}, 34 | {Name: "B450", NameSrc: &dmi.Table.BoardName, NameStrict: false, DateFrom: time.Date(2021, 12, 13, 0, 0, 0, 0, time.UTC)}, 35 | {Name: "B350", NameSrc: &dmi.Table.BoardName, NameStrict: false, DateFrom: time.Date(2021, 11, 1, 0, 0, 0, 0, time.UTC)}, 36 | {Name: "A520", NameSrc: &dmi.Table.BoardName, NameStrict: false, DateFrom: time.Date(2021, 9, 11, 0, 0, 0, 0, time.UTC)}, 37 | // MSI Intel boards 38 | {Name: "Z590", NameSrc: &dmi.Table.BoardName, NameStrict: false, DateFrom: time.Date(2021, 9, 6, 0, 0, 0, 0, time.UTC)}, 39 | {Name: "Z490", NameSrc: &dmi.Table.BoardName, NameStrict: false, DateFrom: time.Date(2021, 9, 30, 0, 0, 0, 0, time.UTC)}, 40 | {Name: "B560", NameSrc: &dmi.Table.BoardName, NameStrict: false, DateFrom: time.Date(2021, 9, 9, 0, 0, 0, 0, time.UTC)}, 41 | {Name: "B460", NameSrc: &dmi.Table.BoardName, NameStrict: false, DateFrom: time.Date(2021, 10, 22, 0, 0, 0, 0, time.UTC)}, 42 | {Name: "H510", NameSrc: &dmi.Table.BoardName, NameStrict: false, DateFrom: time.Date(2021, 9, 10, 0, 0, 0, 0, time.UTC)}, 43 | {Name: "H410", NameSrc: &dmi.Table.BoardName, NameStrict: false, DateFrom: time.Date(2021, 10, 22, 0, 0, 0, 0, time.UTC)}, 44 | } 45 | 46 | if dmi.Table.BoardVendor == "Micro-Star International Co., Ltd." && dmi.Table.ChassisType == "3" { 47 | if isUnaffectedVersion(unaffectedVersions) { 48 | return false 49 | } else if isAffectedDate(affectedDateRanges) { 50 | FQ0001.Method = "date" 51 | return true 52 | } else if isAffectedDevice(affectedDevices) { 53 | FQ0001.Method = "device_name" 54 | return true 55 | } 56 | } 57 | 58 | return false 59 | } 60 | -------------------------------------------------------------------------------- /quirks/quirks.go: -------------------------------------------------------------------------------- 1 | package quirks 2 | 3 | import ( 4 | "strings" 5 | "time" 6 | 7 | "github.com/foxboron/sbctl/config" 8 | "github.com/foxboron/sbctl/dmi" 9 | ) 10 | 11 | type Quirk struct { 12 | ID string `json:"id"` 13 | Name string `json:"name"` 14 | Link string `json:"link"` 15 | Severity string `json:"severity"` 16 | Method string `json:"method"` 17 | } 18 | 19 | type affectedDevice struct { 20 | Name string 21 | NameSrc *string 22 | NameStrict bool 23 | DateFrom time.Time 24 | DateTo time.Time 25 | } 26 | 27 | type unaffectedVersion struct { 28 | Name string 29 | NameSrc *string 30 | NameStrict bool 31 | Version string 32 | VersionSrc *string 33 | } 34 | 35 | type affectedDateRange struct { 36 | From time.Time 37 | To time.Time 38 | } 39 | 40 | func isUnaffectedVersion(list []unaffectedVersion) bool { 41 | for _, item := range list { 42 | if (item.NameStrict && item.Name == *item.NameSrc || !item.NameStrict && strings.Contains(*item.NameSrc, item.Name)) && 43 | item.Version == *item.VersionSrc { 44 | return true 45 | } 46 | } 47 | return false 48 | } 49 | 50 | func isAffectedDate(list []affectedDateRange) bool { 51 | for _, item := range list { 52 | if (item.From.IsZero() || !dmi.Table.FirmwareDate.Before(item.From)) && (item.To.IsZero() || !dmi.Table.FirmwareDate.After(item.To)) { 53 | return true 54 | } 55 | } 56 | return false 57 | } 58 | 59 | func isAffectedDevice(list []affectedDevice) bool { 60 | for _, item := range list { 61 | if (item.NameStrict && item.Name == *item.NameSrc || !item.NameStrict && strings.Contains(*item.NameSrc, item.Name)) && 62 | (item.DateFrom.IsZero() || !dmi.Table.FirmwareDate.Before(item.DateFrom)) && (item.DateTo.IsZero() || !dmi.Table.FirmwareDate.After(item.DateTo)) { 63 | return true 64 | } 65 | } 66 | return false 67 | } 68 | 69 | func CheckFirmwareQuirks(state *config.State) []Quirk { 70 | dmi.Table = dmi.ParseDMI(state) 71 | quirks := []Quirk{} 72 | 73 | if HasFQ0001() { 74 | quirks = append(quirks, FQ0001) 75 | } 76 | 77 | for i := range quirks { 78 | quirks[i].Link = "https://github.com/Foxboron/sbctl/wiki/" + quirks[i].ID 79 | } 80 | 81 | return quirks 82 | } 83 | -------------------------------------------------------------------------------- /sbctl.go: -------------------------------------------------------------------------------- 1 | package sbctl 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | "slices" 12 | 13 | "github.com/foxboron/sbctl/backend" 14 | "github.com/foxboron/sbctl/config" 15 | "github.com/foxboron/sbctl/hierarchy" 16 | "github.com/spf13/afero" 17 | ) 18 | 19 | var ( 20 | // TODO: Remove this at some point 21 | // Only here for legacy reasons to denote the old path 22 | DatabasePath = "/usr/share/secureboot/" 23 | Version = "unknown" 24 | ) 25 | 26 | // Functions that doesn't fit anywhere else 27 | 28 | type LsblkEntry struct { 29 | Parttype string `json:"parttype"` 30 | Mountpoint string `json:"mountpoint"` 31 | Mountpoints []string `json:"mountpoints"` 32 | Pttype string `json:"pttype"` 33 | Fstype string `json:"fstype"` 34 | Children []*LsblkEntry `json:"children"` 35 | } 36 | 37 | type LsblkRoot struct { 38 | Blockdevices []*LsblkEntry `json:"blockdevices"` 39 | } 40 | 41 | var espLocations = []string{ 42 | "/efi", 43 | "/boot", 44 | "/boot/efi", 45 | } 46 | var ErrNoESP = errors.New("failed to find EFI system partition") 47 | 48 | func findESP(b []byte) (string, error) { 49 | var lsblkRoot LsblkRoot 50 | 51 | if err := json.Unmarshal(b, &lsblkRoot); err != nil { 52 | return "", fmt.Errorf("failed to parse json: %v", err) 53 | } 54 | 55 | for _, lsblkEntry := range lsblkRoot.Blockdevices { 56 | // This is our check function, that also checks mountpoints 57 | checkDev := func(e *LsblkEntry, pttype string) *LsblkEntry { 58 | if e.Pttype != "gpt" && (e.Pttype != "" && pttype != "gpt") { 59 | return nil 60 | } 61 | 62 | if e.Fstype != "vfat" { 63 | return nil 64 | } 65 | 66 | if e.Parttype != "c12a7328-f81f-11d2-ba4b-00a0c93ec93b" { 67 | return nil 68 | } 69 | 70 | if slices.Contains(espLocations, e.Mountpoint) { 71 | return e 72 | } 73 | 74 | for _, esp := range espLocations { 75 | n := slices.Index(e.Mountpoints, esp) 76 | if n == -1 { 77 | continue 78 | } 79 | // Replace the top-level Mountpoint with a valid one from mountpoints 80 | e.Mountpoint = e.Mountpoints[n] 81 | return e 82 | } 83 | return nil 84 | } 85 | 86 | // First check top-level devices 87 | p := checkDev(lsblkEntry, "") 88 | if p != nil { 89 | return p.Mountpoint, nil 90 | } 91 | 92 | // Check children, this is not recursive. 93 | for _, ce := range lsblkEntry.Children { 94 | p := checkDev(ce, lsblkEntry.Pttype) 95 | if p != nil { 96 | return p.Mountpoint, nil 97 | } 98 | } 99 | } 100 | return "", ErrNoESP 101 | } 102 | 103 | // Slightly more advanced check 104 | func GetESP(vfs afero.Fs) (string, error) { 105 | for _, env := range []string{"SYSTEMD_ESP_PATH", "ESP_PATH"} { 106 | envEspPath, found := os.LookupEnv(env) 107 | if found { 108 | return envEspPath, nil 109 | } 110 | } 111 | 112 | for _, location := range espLocations { 113 | // "Read" a file inside all candiadate locations to trigger an 114 | // automount if there's an automount partition. 115 | _, _ = vfs.Stat(fmt.Sprintf("%s/does-not-exist", location)) 116 | } 117 | 118 | out, err := exec.Command( 119 | "lsblk", 120 | "--json", 121 | "--tree", 122 | "--output", "PARTTYPE,MOUNTPOINT,PTTYPE,FSTYPE").Output() 123 | if err != nil { 124 | return "", err 125 | } 126 | return findESP(out) 127 | } 128 | 129 | func Sign(state *config.State, keys *backend.KeyHierarchy, file, output string, enroll bool) error { 130 | file, err := filepath.Abs(file) 131 | if err != nil { 132 | return err 133 | } 134 | 135 | if output == "" { 136 | output = file 137 | } else { 138 | output, err = filepath.Abs(output) 139 | if err != nil { 140 | return err 141 | } 142 | } 143 | 144 | kh, err := backend.GetKeyHierarchy(state.Fs, state) 145 | if err != nil { 146 | return err 147 | } 148 | 149 | files, err := ReadFileDatabase(state.Fs, state.Config.FilesDb) 150 | if err != nil { 151 | return fmt.Errorf("couldn't open database: %s", state.Config.FilesDb) 152 | } 153 | 154 | if enroll { 155 | files[file] = &SigningEntry{File: file, OutputFile: output} 156 | if err := WriteFileDatabase(state.Fs, state.Config.FilesDb, files); err != nil { 157 | return err 158 | } 159 | } 160 | 161 | if entry, ok := files[file]; ok && output == entry.OutputFile { 162 | err = SignFile(state, kh, hierarchy.Db, entry.File, entry.OutputFile) 163 | // return early if signing fails 164 | if err != nil { 165 | return err 166 | } 167 | files[file] = entry 168 | if err := WriteFileDatabase(state.Fs, state.Config.FilesDb, files); err != nil { 169 | return err 170 | } 171 | } else { 172 | err = SignFile(state, kh, hierarchy.Db, file, output) 173 | // return early if signing fails 174 | if err != nil { 175 | return err 176 | } 177 | } 178 | 179 | return err 180 | } 181 | 182 | func CombineFiles(vfs afero.Fs, microcode, initramfs string) (afero.File, error) { 183 | for _, file := range []string{microcode, initramfs} { 184 | if _, err := vfs.Stat(file); err != nil { 185 | return nil, fmt.Errorf("%s: %w", file, errors.Unwrap(err)) 186 | } 187 | } 188 | 189 | tmpFile, err := afero.TempFile(vfs, "/var/tmp", "initramfs-") 190 | if err != nil { 191 | return nil, err 192 | } 193 | 194 | one, err := vfs.Open(microcode) 195 | if err != nil { 196 | return nil, err 197 | } 198 | defer one.Close() 199 | 200 | two, err := vfs.Open(initramfs) 201 | if err != nil { 202 | return nil, err 203 | } 204 | defer two.Close() 205 | 206 | _, err = io.Copy(tmpFile, one) 207 | if err != nil { 208 | return nil, fmt.Errorf("failed to append microcode file to output: %w", err) 209 | } 210 | 211 | _, err = io.Copy(tmpFile, two) 212 | if err != nil { 213 | return nil, fmt.Errorf("failed to append initramfs file to output: %w", err) 214 | } 215 | return tmpFile, nil 216 | } 217 | 218 | func CreateBundle(state *config.State, bundle Bundle) error { 219 | var microcode string 220 | make_bundle := false 221 | 222 | if bundle.IntelMicrocode != "" { 223 | microcode = bundle.IntelMicrocode 224 | make_bundle = true 225 | } else if bundle.AMDMicrocode != "" { 226 | microcode = bundle.AMDMicrocode 227 | make_bundle = true 228 | } 229 | 230 | if make_bundle { 231 | tmpFile, err := CombineFiles(state.Fs, microcode, bundle.Initramfs) 232 | if err != nil { 233 | return err 234 | } 235 | defer state.Fs.Remove(tmpFile.Name()) 236 | bundle.Initramfs = tmpFile.Name() 237 | } 238 | 239 | out, err := GenerateBundle(state.Fs, &bundle) 240 | if err != nil { 241 | return err 242 | } 243 | if !out { 244 | return fmt.Errorf("failed to generate bundle %s", bundle.Output) 245 | } 246 | 247 | return nil 248 | } 249 | -------------------------------------------------------------------------------- /sbctl_test.go: -------------------------------------------------------------------------------- 1 | package sbctl 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestGetESP(t *testing.T) { 8 | for _, c := range []struct { 9 | lsblk []byte 10 | esp string 11 | wantErr error 12 | }{ 13 | { 14 | lsblk: []byte(`{"blockdevices":[{"parttype":null,"mountpoint":null,"pttype":"gpt","fstype":"crypto_LUKS","mountpoints":[null],"children":[{"parttype":"c12a7328-f81f-11d2-ba4b-00a0c93ec93b","mountpoint":"/efi","pttype":"gpt","fstype":"vfat","mountpoints":["/efi"]},{"parttype":"4f68bce3-e8cd-4db1-96e7-fbcaf984b709","mountpoint":null,"pttype":"gpt","fstype":"crypto_LUKS","mountpoints":[null],"children":[{"parttype":null,"mountpoint":"/home/.snapshots","pttype":null,"fstype":"btrfs","mountpoints":["/home/.snapshots","/home","/var","/srv","/"]}]}]}]}`), 15 | esp: "/efi", 16 | wantErr: nil, 17 | }, 18 | { 19 | lsblk: []byte(`{"blockdevices":[{"parttype":null,"mountpoint":null,"pttype":"gpt","fstype":"crypto_LUKS","mountpoints":[null],"children":[{"parttype":"c12a7328-f81f-11d2-ba4b-00a0c93ec93b","mountpoint":"/efi","pttype":null,"fstype":"vfat","mountpoints":["/efi"]},{"parttype":"4f68bce3-e8cd-4db1-96e7-fbcaf984b709","mountpoint":null,"pttype":null,"fstype":"crypto_LUKS","mountpoints":[null],"children":[{"parttype":null,"mountpoint":"/home/.snapshots","pttype":null,"fstype":"btrfs","mountpoints":["/home/.snapshots","/home","/var","/srv","/"]}]}]}]}`), 20 | esp: "/efi", 21 | wantErr: nil, 22 | }, 23 | } { 24 | esp, err := findESP(c.lsblk) 25 | if err != nil { 26 | t.Fatalf("%v", err) 27 | } 28 | if esp != c.esp { 29 | t.Fatalf("wrong esp") 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /siglist.go: -------------------------------------------------------------------------------- 1 | package sbctl 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | 7 | "github.com/foxboron/go-uefi/efi/signature" 8 | "github.com/foxboron/go-uefi/efivar" 9 | "github.com/foxboron/go-uefi/efivarfs" 10 | "github.com/foxboron/sbctl/backend" 11 | ) 12 | 13 | type EFIVariables struct { 14 | fs *efivarfs.Efivarfs 15 | PK *signature.SignatureDatabase 16 | KEK *signature.SignatureDatabase 17 | Db *signature.SignatureDatabase 18 | Dbx *signature.SignatureDatabase 19 | } 20 | 21 | func (e *EFIVariables) GetSiglist(ev efivar.Efivar) *signature.SignatureDatabase { 22 | switch ev { 23 | case efivar.PK: 24 | return e.PK 25 | case efivar.KEK: 26 | return e.KEK 27 | case efivar.Db: 28 | return e.Db 29 | case efivar.Dbx: 30 | return e.Dbx 31 | } 32 | return nil 33 | } 34 | 35 | func (e *EFIVariables) EnrollKey(ev efivar.Efivar, hier *backend.KeyHierarchy) error { 36 | // Ensure we are using the correct signer for the backend 37 | var signer backend.KeyBackend 38 | switch ev { 39 | case efivar.PK: 40 | signer = hier.GetKeyBackend(efivar.PK) 41 | case efivar.KEK: 42 | signer = hier.GetKeyBackend(efivar.PK) 43 | case efivar.Db: 44 | signer = hier.GetKeyBackend(efivar.KEK) 45 | } 46 | // fmt.Printf("%s is signed by %s\n", ev.Name, signer.Certificate().SerialNumber.String()) 47 | return e.fs.WriteSignedUpdate(ev, e.GetSiglist(ev), signer.Signer(), signer.Certificate()) 48 | } 49 | 50 | func (e *EFIVariables) EnrollAllKeys(hier *backend.KeyHierarchy) error { 51 | if err := e.EnrollKey(efivar.Db, hier); err != nil { 52 | return err 53 | } 54 | if err := e.EnrollKey(efivar.KEK, hier); err != nil { 55 | return err 56 | } 57 | if err := e.EnrollKey(efivar.PK, hier); err != nil { 58 | return err 59 | } 60 | return nil 61 | } 62 | 63 | func NewEFIVariables(fs *efivarfs.Efivarfs) *EFIVariables { 64 | return &EFIVariables{ 65 | fs: fs, 66 | PK: signature.NewSignatureDatabase(), 67 | KEK: signature.NewSignatureDatabase(), 68 | Db: signature.NewSignatureDatabase(), 69 | Dbx: signature.NewSignatureDatabase(), 70 | } 71 | } 72 | 73 | func SystemEFIVariables(fs *efivarfs.Efivarfs) (*EFIVariables, error) { 74 | var sigpk *signature.SignatureDatabase 75 | var sigkek *signature.SignatureDatabase 76 | var sigdb *signature.SignatureDatabase 77 | var err error 78 | 79 | sigdb, err = fs.Getdb() 80 | if errors.Is(err, os.ErrNotExist) { 81 | sigdb = signature.NewSignatureDatabase() 82 | } else if err != nil { 83 | return nil, err 84 | } 85 | 86 | sigkek, err = fs.GetKEK() 87 | if errors.Is(err, os.ErrNotExist) { 88 | sigkek = signature.NewSignatureDatabase() 89 | } else if err != nil { 90 | return nil, err 91 | } 92 | 93 | sigpk, err = fs.GetPK() 94 | if errors.Is(err, os.ErrNotExist) { 95 | sigpk = signature.NewSignatureDatabase() 96 | } else if err != nil { 97 | return nil, err 98 | } 99 | 100 | return &EFIVariables{ 101 | fs: fs, 102 | PK: sigpk, 103 | KEK: sigkek, 104 | Db: sigdb, 105 | Dbx: signature.NewSignatureDatabase(), 106 | }, nil 107 | } 108 | -------------------------------------------------------------------------------- /stringset/stringset.go: -------------------------------------------------------------------------------- 1 | package stringset 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "golang.org/x/exp/slices" 8 | ) 9 | 10 | type StringSet struct { 11 | Allowed []string 12 | Value string 13 | } 14 | 15 | func NewStringSet(allowed []string, d string) *StringSet { 16 | return &StringSet{ 17 | Allowed: allowed, 18 | Value: d, 19 | } 20 | } 21 | 22 | func (s StringSet) String() string { 23 | return s.Value 24 | } 25 | 26 | func (s *StringSet) Set(p string) error { 27 | if !slices.Contains(s.Allowed, p) { 28 | return fmt.Errorf("%s is not included in %s", p, strings.Join(s.Allowed, ",")) 29 | } 30 | s.Value = p 31 | return nil 32 | } 33 | 34 | func (s *StringSet) Type() string { 35 | var allowedValues string 36 | 37 | for _, allowedValue := range s.Allowed { 38 | allowedValues += fmt.Sprintf("%v,", allowedValue) 39 | } 40 | 41 | allowedValues = strings.TrimRight(allowedValues, ",") 42 | 43 | return fmt.Sprintf("[%v]", allowedValues) 44 | } 45 | -------------------------------------------------------------------------------- /stringset/stringset_test.go: -------------------------------------------------------------------------------- 1 | package stringset 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestStringSet(t *testing.T) { 8 | for _, tt := range []struct { 9 | name string 10 | allowed []string 11 | value string 12 | wantType string 13 | wantError bool 14 | wantString string 15 | }{ 16 | { 17 | name: "no string allowed", 18 | value: "abc", 19 | wantType: "[]", 20 | wantError: true, 21 | }, 22 | { 23 | name: "set value", 24 | allowed: []string{"pk"}, 25 | value: "pk", 26 | wantType: "[pk]", 27 | wantError: false, 28 | wantString: "pk", 29 | }, 30 | { 31 | name: "set wrong value", 32 | allowed: []string{"pk"}, 33 | value: "pj", 34 | wantType: "[pk]", 35 | wantError: true, 36 | }, 37 | { 38 | name: "multiple allowed", 39 | allowed: []string{"pk", "kek", "db"}, 40 | value: "db", 41 | wantType: "[pk,kek,db]", 42 | wantString: "db", 43 | }, 44 | { 45 | name: "fail on multiple allowed", 46 | allowed: []string{"pk", "kek", "db"}, 47 | value: "da", 48 | wantType: "[pk,kek,db]", 49 | wantError: true, 50 | }, 51 | } { 52 | t.Run(tt.name, func(t *testing.T) { 53 | stringSet := NewStringSet(tt.allowed, "") 54 | 55 | if stringSet.Type() != tt.wantType { 56 | t.Errorf("got type of stringSet: %v, but want: %v", stringSet.Type(), tt.wantType) 57 | } 58 | 59 | err := stringSet.Set(tt.value) 60 | if (err != nil && !tt.wantError) || (err == nil && tt.wantError) { 61 | t.Fatalf("expected error: %v, but got %v", tt.wantError, err) 62 | } 63 | 64 | if stringSet.String() != tt.wantString { 65 | t.Errorf("expected stringSet value %v, but got %v", tt.wantString, stringSet.String()) 66 | } 67 | }) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | Integration tests 2 | ================= 3 | 4 | Follow https://github.com/anatol/vmtest/blob/master/docs/prepare_image.md 5 | Expects `/usr/share/edk2-ovmf/x64/OVMF_CODE.secboot.fd` 6 | -------------------------------------------------------------------------------- /tests/binaries/test.pecoff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Foxboron/sbctl/30cc0e7b22a0f7185e9962f009ead32c2e482877/tests/binaries/test.pecoff -------------------------------------------------------------------------------- /tests/bzImage: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Foxboron/sbctl/30cc0e7b22a0f7185e9962f009ead32c2e482877/tests/bzImage -------------------------------------------------------------------------------- /tests/integration_test.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | // +build integration 3 | 4 | package tests 5 | 6 | import ( 7 | "fmt" 8 | "io" 9 | "log" 10 | "os" 11 | "os/exec" 12 | "path" 13 | "testing" 14 | "time" 15 | 16 | "github.com/hugelgupf/vmtest" 17 | "github.com/hugelgupf/vmtest/qemu" 18 | ) 19 | 20 | type VMTest struct { 21 | ovmf string 22 | secboot string 23 | } 24 | 25 | func (vm *VMTest) RunTests(packages ...string) func(t *testing.T) { 26 | return func(t *testing.T) { 27 | vmtest.RunGoTestsInVM(t, packages, 28 | vmtest.WithVMOpt( 29 | vmtest.WithSharedDir("ovmf/keys"), 30 | vmtest.WithInitramfsFiles("sbctl:bin/sbctl"), 31 | vmtest.WithQEMUFn( 32 | qemu.WithVMTimeout(time.Minute), 33 | qemu.WithQEMUCommand("qemu-system-x86_64 -enable-kvm"), 34 | qemu.WithKernel("bzImage"), 35 | qemu.ArbitraryArgs( 36 | "-m", "1G", "-machine", "type=q35,smm=on", 37 | "-drive", fmt.Sprintf("if=pflash,format=raw,unit=0,file=%s,readonly=on", vm.secboot), 38 | "-drive", fmt.Sprintf("if=pflash,format=raw,unit=1,file=%s", vm.ovmf), 39 | ), 40 | )), 41 | ) 42 | } 43 | } 44 | 45 | func TestMain(m *testing.M) { 46 | cmd := exec.Command("go", "build", "../cmd/sbctl") 47 | cmd.Stdout = os.Stdout 48 | cmd.Stderr = os.Stderr 49 | if err := cmd.Run(); err != nil { 50 | log.Fatal(err) 51 | } 52 | os.Exit(m.Run()) 53 | } 54 | 55 | func TestEnrollement(t *testing.T) { 56 | os.Setenv("VMTEST_QEMU", "qemu-system-x86_64") 57 | if err := buildSbctl(); err != nil { 58 | t.Fatal(err) 59 | } 60 | 61 | WithVM(t, func(vm *VMTest) { 62 | t.Run("Enroll keys", vm.RunTests("github.com/foxboron/sbctl/tests/integrations/enroll_keys")) 63 | t.Run("Secure boot enabled", vm.RunTests("github.com/foxboron/sbctl/tests/integrations/secure_boot_enabled")) 64 | t.Run("List enrolled keys", vm.RunTests("github.com/foxboron/sbctl/tests/integrations/list_enrolled_keys")) 65 | t.Run("Export enrolled keys", vm.RunTests("github.com/foxboron/sbctl/tests/integrations/export_enrolled_keys")) 66 | }) 67 | } 68 | 69 | // Sets up the test by making a copy of the OVMF files from the system 70 | func WithVM(t *testing.T, fn func(*VMTest)) { 71 | t.Helper() 72 | dir := t.TempDir() 73 | vm := VMTest{ 74 | ovmf: path.Join(dir, "OVMF_VARS.fd"), 75 | secboot: path.Join(dir, "OVMF_CODE.secboot.fd"), 76 | } 77 | CopyFile("/usr/share/edk2-ovmf/x64/OVMF_VARS.4m.fd", vm.ovmf) 78 | CopyFile("/usr/share/edk2-ovmf/x64/OVMF_CODE.secboot.4m.fd", vm.secboot) 79 | fn(&vm) 80 | } 81 | 82 | func CopyFile(src, dst string) bool { 83 | source, err := os.Open(src) 84 | if err != nil { 85 | log.Fatal(err) 86 | } 87 | defer source.Close() 88 | 89 | f, err := os.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) 90 | if err != nil { 91 | log.Fatal(err) 92 | } 93 | defer f.Close() 94 | io.Copy(f, source) 95 | si, err := os.Stat(src) 96 | if err != nil { 97 | log.Fatal(err) 98 | } 99 | err = os.Chmod(dst, si.Mode()) 100 | if err != nil { 101 | log.Fatal(err) 102 | } 103 | return true 104 | } 105 | 106 | func buildSbctl() error { 107 | cmd := exec.Command("go", "build", "../cmd/sbctl") 108 | cmd.Stdout = os.Stdout 109 | cmd.Stderr = os.Stderr 110 | 111 | return cmd.Run() 112 | } 113 | -------------------------------------------------------------------------------- /tests/integrations/enroll_keys/enroll_keys_test.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | // +build integration 3 | 4 | package main 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/foxboron/go-uefi/efi" 10 | "github.com/foxboron/sbctl/tests/utils" 11 | "github.com/hugelgupf/vmtest/guest" 12 | . "github.com/onsi/gomega" 13 | ) 14 | 15 | func TestEnrollKeys(t *testing.T) { 16 | g := NewWithT(t) 17 | 18 | guest.SkipIfNotInVM(t) 19 | 20 | g.Expect(efi.GetSecureBoot()).To(BeFalse(), "should not be in secure boot mode") 21 | g.Expect(efi.GetSetupMode()).To(BeTrue(), "should be in setup mode") 22 | 23 | utils.Exec("rm -rf /usr/share/secureboot") 24 | utils.Exec("sbctl status") 25 | utils.Exec("sbctl create-keys") 26 | out, err := utils.ExecWithOutput("sbctl enroll-keys") 27 | g.Expect(err).To(HaveOccurred()) 28 | g.Expect(out).To(MatchRegexp("Could not find any TPM Eventlog in the system")) 29 | 30 | out, err = utils.ExecWithOutput("sbctl enroll-keys --yes-this-might-brick-my-machine") 31 | g.Expect(err).ToNot(HaveOccurred(), out) 32 | 33 | g.Expect(efi.GetSetupMode()).To(BeFalse(), "should no longer be in setup mode") 34 | } 35 | -------------------------------------------------------------------------------- /tests/integrations/export_enrolled_keys/export_enrolled_keys_test.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | // +build integration 3 | 4 | package main 5 | 6 | import ( 7 | "crypto/x509" 8 | "fmt" 9 | "os" 10 | "path/filepath" 11 | "regexp" 12 | "testing" 13 | 14 | "github.com/foxboron/go-uefi/efi/signature" 15 | "github.com/foxboron/sbctl/tests/utils" 16 | "github.com/hugelgupf/vmtest/guest" 17 | 18 | . "github.com/onsi/gomega" 19 | ) 20 | 21 | func TestExportEnrolledKeysDer(t *testing.T) { 22 | g := NewWithT(t) 23 | 24 | guest.SkipIfNotInVM(t) 25 | 26 | out, err := utils.ExecWithOutput("sbctl export-enrolled-keys --dir /tmp/exported-der --format der") 27 | g.Expect(err).ToNot(HaveOccurred(), out) 28 | 29 | platformKey, err := findFileByPattern("/tmp/exported-der/PK", ".*Platform.*.der") 30 | g.Expect(err).ToNot(HaveOccurred()) 31 | 32 | derBytes, err := os.ReadFile(platformKey) 33 | g.Expect(err).ToNot(HaveOccurred()) 34 | 35 | cert, err := x509.ParseCertificate(derBytes) 36 | g.Expect(err).ToNot(HaveOccurred()) 37 | 38 | g.Expect(cert.Issuer.String()).To(MatchRegexp("CN=Platform Key,C=Platform Key")) 39 | } 40 | 41 | func TestExportEnrolledKeysEsl(t *testing.T) { 42 | g := NewWithT(t) 43 | 44 | guest.SkipIfNotInVM(t) 45 | 46 | out, err := utils.ExecWithOutput("sbctl export-enrolled-keys --dir /tmp/exported-esl --format esl") 47 | g.Expect(err).ToNot(HaveOccurred(), out) 48 | 49 | eslReader, err := os.Open("/tmp/exported-esl/db.esl") 50 | g.Expect(err).ToNot(HaveOccurred()) 51 | defer eslReader.Close() 52 | 53 | sl, err := signature.ReadSignatureList(eslReader) 54 | g.Expect(err).ToNot(HaveOccurred()) 55 | 56 | s := sl.Signatures[0] 57 | certificates, err := x509.ParseCertificates(s.Data) 58 | g.Expect(err).ToNot(HaveOccurred()) 59 | g.Expect(certificates[0].Issuer.CommonName).To(Equal("Database Key")) 60 | } 61 | 62 | func findFileByPattern(dirPath string, pattern string) (string, error) { 63 | files, err := os.ReadDir(dirPath) 64 | if err != nil { 65 | return "", err 66 | } 67 | 68 | re, err := regexp.Compile(pattern) 69 | if err != nil { 70 | return "", err 71 | } 72 | 73 | for _, file := range files { 74 | fmt.Printf("file.Name() = %+v\n", file.Name()) 75 | if !file.IsDir() && re.MatchString(file.Name()) { 76 | return filepath.Join(dirPath, file.Name()), nil 77 | } 78 | } 79 | 80 | return "", fmt.Errorf("no file matching pattern '%s' found in directory '%s'", pattern, dirPath) 81 | } 82 | -------------------------------------------------------------------------------- /tests/integrations/list_enrolled_keys/list_enrolled_keys_test.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | // +build integration 3 | 4 | package main 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/foxboron/sbctl/tests/utils" 10 | "github.com/hugelgupf/vmtest/guest" 11 | . "github.com/onsi/gomega" 12 | ) 13 | 14 | func TestListEnrolledKeys(t *testing.T) { 15 | g := NewWithT(t) 16 | 17 | guest.SkipIfNotInVM(t) 18 | 19 | out, err := utils.ExecWithOutput("sbctl list-enrolled-keys") 20 | g.Expect(err).ToNot(HaveOccurred(), out) 21 | g.Expect(out).To(SatisfyAll( 22 | MatchRegexp("Platform Key"), 23 | MatchRegexp("Key Exchange Key"), 24 | MatchRegexp("Database Key"))) 25 | } 26 | -------------------------------------------------------------------------------- /tests/integrations/secure_boot_enabled/secure_boot_enabled_test.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | // +build integration 3 | 4 | package main 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/foxboron/go-uefi/efi" 10 | "github.com/foxboron/sbctl/tests/utils" 11 | "github.com/hugelgupf/vmtest/guest" 12 | . "github.com/onsi/gomega" 13 | ) 14 | 15 | func TestSecureBootEnabled(t *testing.T) { 16 | g := NewWithT(t) 17 | 18 | guest.SkipIfNotInVM(t) 19 | 20 | g.Expect(efi.GetSecureBoot()).To(BeTrue(), "should be in secure boot mode") 21 | g.Expect(efi.GetSetupMode()).To(BeFalse(), "should not be in setup mode") 22 | 23 | out, err := utils.ExecWithOutput("sbctl status") 24 | g.Expect(err).ToNot(HaveOccurred(), out) 25 | g.Expect(out).To(MatchRegexp("Secure Boot:.*Enabled")) 26 | } 27 | -------------------------------------------------------------------------------- /tests/ovmf/OVMF_VARS.fd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Foxboron/sbctl/30cc0e7b22a0f7185e9962f009ead32c2e482877/tests/ovmf/OVMF_VARS.fd -------------------------------------------------------------------------------- /tests/ovmf/keys/KEK/KEK.auth: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Foxboron/sbctl/30cc0e7b22a0f7185e9962f009ead32c2e482877/tests/ovmf/keys/KEK/KEK.auth -------------------------------------------------------------------------------- /tests/ovmf/keys/KEK/KEK.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Foxboron/sbctl/30cc0e7b22a0f7185e9962f009ead32c2e482877/tests/ovmf/keys/KEK/KEK.der -------------------------------------------------------------------------------- /tests/ovmf/keys/KEK/KEK.der.esl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Foxboron/sbctl/30cc0e7b22a0f7185e9962f009ead32c2e482877/tests/ovmf/keys/KEK/KEK.der.esl -------------------------------------------------------------------------------- /tests/ovmf/keys/KEK/KEK.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | 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 51 | toIeDSm6JDMFzj0kbzrGsSgFWDW/ 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /tests/ovmf/keys/KEK/KEK.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIExzCCAq+gAwIBAgIRAN0+r2apwvdmm97wInecsaQwDQYJKoZIhvcNAQELBQAw 3 | GzEZMBcGA1UEBhMQS2V5IEV4Y2hhbmdlIEtleTAiGA8wMDAxMDEwMTAwMDAwMFoY 4 | DzAwMDEwMTAxMDAwMDAwWjAbMRkwFwYDVQQGExBLZXkgRXhjaGFuZ2UgS2V5MIIC 5 | IjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAlekYj5cj0tvJEqBY62oGSoVo 6 | yrY1wShrsRCOM2tm5Ksrjszo47Yr4k8AprtYy6ri0MBgMBhXFDkwLDZjQtGw6AP7 7 | tK0JimRTOXreL/Pma1d5zBLJkQoXxDZWn0E+oyl2+1BJcxkUW48Kdz5Ds5k3bdP1 8 | XtyaAzwqcd3lT2BnLPy5wYA+lzhBJg71d1W9VsxJiMWl/Melq0dfdHt4wxO0im0T 9 | 2SfGaS4wohXnRtdlzOR7fjjpW8fFG2usb2dSNTtgvz16KSdRZDrECU7Yu8RQWI6W 10 | Gs4kJBhEZm8WBINxw7/9QWYAwycZQt/b3ClUT54s9oDX96Q64hKlTu10FIAkqP5I 11 | 5Q8AuDMv0gdElTBIvAs6GGX2cXJXnH7XTL+wYMAKuNG0Y+nZ8PWU6fGt4Gl9DAXr 12 | cQkHpJFQYpWXhNpv7Cx5MKe0QZrMONg1seOkFw/uMQGuotGmHyvtJoyx44Uixcjy 13 | akm9oMfcqco2HQR9lRldGrWrwjSwm0W7bGJDqHOW/EmGjRfD2kuEwAR5kXKlOOiy 14 | O8I7MJyRc78LNdyJE3Tpi+SNodU5IQ00x/5CTRrajnfeI5/2cZTgAjF4SOsXcZBn 15 | AAdkMaY6gwxtOoOCK5C3JZwfU6f/URY4nSAE3IZZMSd07SedtSxLxyNCrI4GU3Ss 16 | l5lmRNZcnUngbUnmYc8CAwEAAaMCMAAwDQYJKoZIhvcNAQELBQADggIBABX2H/n/ 17 | S8AdPC+S4uo7CUNvYdussOD7Hmf7xgjo3WGok3IU7bsvLcFB6eCMXmsrswukosRS 18 | SxGPUN0V8MIwG097VWt0JwEsMOv8uY65JAkxpL+Wr+3nhZyKYRP1lF77xVZ2f1uS 19 | 9H9Av39PkySQ6+OKi4NHsXJs4ei87X+WvGW9FspgdPjq9HajrKLyp4MXhjgU+xGN 20 | jUOipTJH/A+bnDp9Idssz2N1rV4mh0QT17lEwKFN42uXsUffd6/WR2CtjFNN6aj/ 21 | T95NxK0Lp611o5IfaD7uOoFVTzucYeniIwsXfu89YWJp9dPJtGH4i9gSL7kS4lTw 22 | SvA4PF5smrN5uadufPbr2hIWxkkibue0C+3/0lFoa/q987Pb6OrVAFnI5mTaRSUZ 23 | S0j00+NiqeSYjrJdqriGhSdYZl1/rtKyWAFq4IVjMICEGNydxZg30JgsyHlxLSRi 24 | WD2dO4PQG0DPmEO6Sjd3Ub2wXn0CyuZZC4fUDGRxgf6tAYiS1rpeXjJ+PGXHNrHb 25 | clULPBwu1oE1fypFCrHPfVHyHKCqMTNcAOHH3SC/xzajPQdEcpQS0ipi52saW/ec 26 | HCj19yeo4Clv9jnv3jk9k32WS6y2fx7p9MOb5ZeHPAXDM0UhlHKVAkzDsgRf9ir6 27 | H8pu90xR+iJJxJYf7KfimvNzuv0EdWfqAQ8F 28 | -----END CERTIFICATE----- 29 | -------------------------------------------------------------------------------- /tests/ovmf/keys/PK/PK.auth: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Foxboron/sbctl/30cc0e7b22a0f7185e9962f009ead32c2e482877/tests/ovmf/keys/PK/PK.auth -------------------------------------------------------------------------------- /tests/ovmf/keys/PK/PK.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Foxboron/sbctl/30cc0e7b22a0f7185e9962f009ead32c2e482877/tests/ovmf/keys/PK/PK.der -------------------------------------------------------------------------------- /tests/ovmf/keys/PK/PK.der.esl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Foxboron/sbctl/30cc0e7b22a0f7185e9962f009ead32c2e482877/tests/ovmf/keys/PK/PK.der.esl -------------------------------------------------------------------------------- /tests/ovmf/keys/PK/PK.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | 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 51 | JPbQnfB/5rpgyxUXUhlBWpQb/R+CuUM= 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /tests/ovmf/keys/PK/PK.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEvzCCAqegAwIBAgIRAINISunhbfmr4cz52wQ/0YAwDQYJKoZIhvcNAQELBQAw 3 | FzEVMBMGA1UEBhMMUGxhdGZvcm0gS2V5MCIYDzAwMDEwMTAxMDAwMDAwWhgPMDAw 4 | MTAxMDEwMDAwMDBaMBcxFTATBgNVBAYTDFBsYXRmb3JtIEtleTCCAiIwDQYJKoZI 5 | hvcNAQEBBQADggIPADCCAgoCggIBAL1IcxU3TvnafusV+xFwPVrSzrkvOGIcJ//B 6 | XFjqi66lOk+M7SMOxfK+//DGu7GlmyeKb031x2TdHdYUVvU6h+BhF3Pa4JZrJYVf 7 | IvHXeQveQv7fIFlB4GI25TXOZ40f/u4VddsAaF+ww/7UkviDpE/iS61H1ntqePlr 8 | 8wofn70nh4mU2L0cmmg4TDvFcxtjCNZXDa0FTuKjNsLa7sf/pjNihV0M+4lMmmhx 9 | v/b/Bk52Kc9fWdQIu10vsgIL0lhXv2QgEI2BaDZeSaQfNM/ozUGvO1bK1zn8jKpt 10 | F+cNA6um/9oAz0Dek/7DVqzhYwXk+Ou9J9sAOwRiTGa5WJW2inc3onBEpnycHTuB 11 | VkLUJXznzfqwJaPdWI2K6P16nzntdmVPRMBbDPpe+RoyP4tfOUIYBcfBPifb+LGV 12 | SEGAA4UqGWZB/Daix0yRMtSV8jizL7Hvh3dAMD7cEIX7iu0sU7x2HJwoaziW/UKF 13 | 5acBHnomRq/pD820PmBiqlZCqP71edeZv5M4q5QIGvVhUhy2vWNiD0XJed5Z4dWO 14 | cQC5L0PBJDcwDeU7yTkmTxyDlBGcMm5sE/4jUmUzRNGcPcxlkurQ6dIyy6v+3UhT 15 | ivIJatrazh8rn7usqsk1iUiFCZjb1wh2SiTFT8YY68BXZOkTnxI6sU9Y8JW7J7IN 16 | qtrOHD73AgMBAAGjAjAAMA0GCSqGSIb3DQEBCwUAA4ICAQARhdgVM8ZRb8rBudZG 17 | UkEELKFtcfel5/O3Ayt5udNOOqvvidh8lAzvXiLbDevK73XY+piLyYTzNUEqT5Kx 18 | 6S4fSRgKjvf/diTq3pSsMcWouy2Nktvx03/4j1FO0nbOCPp6QwQ0HxrbqFaZtGz3 19 | fSupOKYKD992GSEvkEAd7rav4Gq/0NMoyVgp+qToQpLRNHU1GyV7rdNbJp9xHvG9 20 | znskQ3KWTjK43zseWKF+DCPo5/hmLttClAOqCKlAr0FA39N9awxNjlMyKhadQnMW 21 | fNM759zxvi29ZjgYb3fMFubND8hFIPlGLn+EmmzBm/jMrEjvRp+DFkHj5kPzy1/S 22 | UjRmfRwWpjkZf4k6nqkbLAX0JoQpiH/HecJzirkuF8akVVvDQF8DdCfg3gEArEqA 23 | EWkf+5iHpuMRWwFgarRbj5az7vZQaRn6+pzY62FEdIaqdAtovE0WoeoykBbZDuFD 24 | t6y4pE6MhZ3VaGPy9Xlk7RKptzdAVbZuFVFVfsetd2rN32WtjRDmxjk0L21sMl/N 25 | 3zsG4cT1V2nGh4+yzIh7MOWHHNCeBSEJWg9YBJibG2qJJ3R9k5nUj/eYlKhj1MGX 26 | oEJ2L2gHTNMxTUTfLziBltMoRu2UtjcKFsfxygUrXAUNNmDDNhZ6vUYM8tmvlIG6 27 | n4BzfjhCZ2nUKQyVQTf4AuUuhQ== 28 | -----END CERTIFICATE----- 29 | -------------------------------------------------------------------------------- /tests/ovmf/keys/db/db.auth: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Foxboron/sbctl/30cc0e7b22a0f7185e9962f009ead32c2e482877/tests/ovmf/keys/db/db.auth -------------------------------------------------------------------------------- /tests/ovmf/keys/db/db.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Foxboron/sbctl/30cc0e7b22a0f7185e9962f009ead32c2e482877/tests/ovmf/keys/db/db.der -------------------------------------------------------------------------------- /tests/ovmf/keys/db/db.der.esl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Foxboron/sbctl/30cc0e7b22a0f7185e9962f009ead32c2e482877/tests/ovmf/keys/db/db.der.esl -------------------------------------------------------------------------------- /tests/ovmf/keys/db/db.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | 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 51 | c85pERZGQdikEFI/CJ5pb2nfTh865w== 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /tests/ovmf/keys/db/db.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEvzCCAqegAwIBAgIRAI7B/h5WtdYgp1FIPdMe8qUwDQYJKoZIhvcNAQELBQAw 3 | FzEVMBMGA1UEBhMMRGF0YWJhc2UgS2V5MCIYDzAwMDEwMTAxMDAwMDAwWhgPMDAw 4 | MTAxMDEwMDAwMDBaMBcxFTATBgNVBAYTDERhdGFiYXNlIEtleTCCAiIwDQYJKoZI 5 | hvcNAQEBBQADggIPADCCAgoCggIBALTcUnGboQLpg/3oaiideU27MZ1HfptShtzI 6 | QVxP/Mp+xcbmWQ1+sEv3v5NNT0Qs9bukdO2n14F22Rn0PO9D154A9O7jS8cShJBV 7 | qzNHbRbqm9oreKzDiHRzTHdIhKk9BzkGq2W2yUwDYGzCpV0MM8C7GCRS8CejwW2s 8 | JmUZn9PANYHuxvfs+gpUy9AsWHKYD3QAnn8IlsFXptrdaP2TErVIyZavC8GPIbJh 9 | kw6CIZtCTSNhXwWKWhBOhkOpg+Vd0p00RtGB1D/0Kr4IFEkzVLuzlsTzc9SpsGuz 10 | NZ3J4H2U/sR11F4xJuRvhODixhLKLFHFbD79CIdAKooX4lGiXy/vCKXz8gr6O1vO 11 | m4H6iOgs7AGRiAioa/bO+jAE5OZa/fQ1SGNqUiQJZ+ZWKjG6wQPE1F997mmi43vl 12 | wharWq6JBYXr4jPJKsorqS1K8oElAiOSCF27nhjJy81mfF+AF6bazU3fvMO0iS4Q 13 | 2kLF+PVPwx+NqkJaYEumA6dUYa8YYqHSqysghIv1KhdHWSjahCIVWOuCDgn91fd0 14 | m9zItWtfblCFQLYTkP4JOWqHNjuqIk6mAWuDRtKX0nYUnBZsPK07iIVZucTvSOnv 15 | 39GA6l62X8G1DIPY0uViYZvbMUB51H0Oi//qNonUu7xR7EheTvPxtuxtOBskHw9I 16 | y4egdwXdAgMBAAGjAjAAMA0GCSqGSIb3DQEBCwUAA4ICAQCgsBi/ToKlVxu4cliv 17 | KHsQrzOpVCUWINw8GAnUrGspnm9x8AXqhtX3ZqE19DSESA/rDIizTt6ZqvT6+E8t 18 | 7+YOPCbM3RdrHJ/cKoVssxCONaz5OLhFHwdxikAnLiCey6KqO2yjMDwbP32+Vyzu 19 | rrdJhRx3bADwmt+dWZS5OfhksN3GB1JePvccIikkBrkKjuyy5OWcyFhRSBhZJelc 20 | pMNa6FauaQ/X6QM/xWumnBS9D1s0ynqz37qCMpAryGgdbbqs/8tvJVGZ5ReUFCy9 21 | 9US2PS9E/eWGprl6xbZqVVVWpPdnzw4l+Lp12Ih26OxT6rG1cScl8H//qc5wkHMU 22 | rpg2z/5xxdx8UpS2PljpeyhJZMOo0iuY1l1shsB5f1w4MtBxhPfFugYs9HQFObFJ 23 | gmVXpR1qkz8fEy5gU75RPGan9rukvlp41EnW7U8EqCEwF/g9PgDOKypBtS2OmSfO 24 | 9lDWYH5WXezfWO+mUzpxiaTvLUFXJZ8y3T2PisSEM3cKTwYYRuC/Vm4jEs/qFUBt 25 | 5OPyaA6TmB0k1yHcg1GeIp9QhWt/WbHAzJkPLAl06x2ypNfyjcExFsvrwX0YvR74 26 | HnXX8X20YxyLxrq02/ZeYT8m1godiTCG6/PDr8DB1TXkQee5KGhUekS1k0T/bsS9 27 | jdTUvLFzwHTNOEJ/etlCJAPogg== 28 | -----END CERTIFICATE----- 29 | -------------------------------------------------------------------------------- /tests/ovmf/keys/initramfs.cpio: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Foxboron/sbctl/30cc0e7b22a0f7185e9962f009ead32c2e482877/tests/ovmf/keys/initramfs.cpio -------------------------------------------------------------------------------- /tests/shared/test: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Foxboron/sbctl/30cc0e7b22a0f7185e9962f009ead32c2e482877/tests/shared/test -------------------------------------------------------------------------------- /tests/tpm_eventlogs/t14_eventlog: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Foxboron/sbctl/30cc0e7b22a0f7185e9962f009ead32c2e482877/tests/tpm_eventlogs/t14_eventlog -------------------------------------------------------------------------------- /tests/tpm_eventlogs/t14s_eventlog: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Foxboron/sbctl/30cc0e7b22a0f7185e9962f009ead32c2e482877/tests/tpm_eventlogs/t14s_eventlog -------------------------------------------------------------------------------- /tests/tpm_eventlogs/t480s_eventlog: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Foxboron/sbctl/30cc0e7b22a0f7185e9962f009ead32c2e482877/tests/tpm_eventlogs/t480s_eventlog -------------------------------------------------------------------------------- /tests/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | "strings" 7 | ) 8 | 9 | func Exec(c string) error { 10 | args := strings.Split(c, " ") 11 | cmd := exec.Command(args[0], args[1:]...) 12 | cmd.Stdout = os.Stdout 13 | cmd.Stderr = os.Stderr 14 | if err := cmd.Run(); err != nil { 15 | return err 16 | } 17 | return nil 18 | } 19 | 20 | func ExecWithOutput(c string) (string, error) { 21 | args := strings.Split(c, " ") 22 | cmd := exec.Command(args[0], args[1:]...) 23 | b, err := cmd.CombinedOutput() 24 | 25 | return string(b), err 26 | } 27 | -------------------------------------------------------------------------------- /tpm.go: -------------------------------------------------------------------------------- 1 | package sbctl 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | 7 | "github.com/foxboron/go-uefi/efi/signature" 8 | "github.com/foxboron/go-uefi/efi/util" 9 | "github.com/foxboron/sbctl/fs" 10 | "github.com/google/go-attestation/attest" 11 | "github.com/spf13/afero" 12 | ) 13 | 14 | var ( 15 | ErrOprom = errors.New("uefi has oprom") 16 | ErrNoEventlog = errors.New("no eventlog found") 17 | 18 | // For the sake of clarity we reserve this GUID for our SignatureList. 19 | // It says: OpROMIsAnnoying! 20 | eventlogGUID = *util.StringToGUID("4f52704f-494d-41736e-6e6f79696e6721") 21 | ) 22 | 23 | func GetEventlogEvents(vfs afero.Fs, eventlog string) ([]attest.Event, error) { 24 | if _, err := vfs.Stat(eventlog); err != nil { 25 | if errors.Is(err, os.ErrNotExist) { 26 | return nil, ErrNoEventlog 27 | } 28 | return nil, err 29 | } 30 | b, err := fs.ReadFile(vfs, eventlog) 31 | if err != nil { 32 | return nil, err 33 | } 34 | log, err := attest.ParseEventLog(b) 35 | if err != nil { 36 | return nil, err 37 | } 38 | // TODO: Hardcoded. Should probably make this dynamic 39 | return log.Events(attest.HashSHA256), nil 40 | } 41 | 42 | func CheckEventlogOprom(vfs afero.Fs, eventlog string) error { 43 | events, err := GetEventlogEvents(vfs, eventlog) 44 | if err != nil { 45 | return err 46 | } 47 | for _, event := range events { 48 | switch event.Type.String() { 49 | case "EV_EFI_BOOT_SERVICES_DRIVER": 50 | return ErrOprom 51 | } 52 | } 53 | return nil 54 | } 55 | 56 | func GetEventlogChecksums(vfs afero.Fs, eventlog string) (*signature.SignatureDatabase, error) { 57 | events, err := GetEventlogEvents(vfs, eventlog) 58 | if err != nil { 59 | return nil, err 60 | } 61 | sigdb := signature.NewSignatureDatabase() 62 | for _, event := range events { 63 | switch event.Type.String() { 64 | case "EV_EFI_BOOT_SERVICES_DRIVER": 65 | if sigdb.BytesExists(signature.CERT_SHA256_GUID, eventlogGUID, event.Digest) { 66 | continue 67 | } 68 | if err = sigdb.Append(signature.CERT_SHA256_GUID, eventlogGUID, event.Digest); err != nil { 69 | return nil, err 70 | } 71 | } 72 | } 73 | return sigdb, nil 74 | } 75 | 76 | func DetectTPMEventlog(sb *signature.SignatureDatabase) bool { 77 | for _, l := range *sb { 78 | for _, sig := range l.Signatures { 79 | if util.CmpEFIGUID(sig.Owner, eventlogGUID) { 80 | return true 81 | } 82 | } 83 | } 84 | return false 85 | } 86 | -------------------------------------------------------------------------------- /tpm_test.go: -------------------------------------------------------------------------------- 1 | package sbctl 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/spf13/afero" 8 | ) 9 | 10 | var ( 11 | tests = []struct { 12 | Result error 13 | File string 14 | Checksums int 15 | }{ 16 | { 17 | Result: nil, 18 | File: "tests/tpm_eventlogs/t480s_eventlog", 19 | Checksums: 0, 20 | }, 21 | { 22 | Result: ErrOprom, 23 | File: "tests/tpm_eventlogs/t14s_eventlog", 24 | Checksums: 11, 25 | }, 26 | { 27 | Result: ErrOprom, 28 | File: "tests/tpm_eventlogs/t14_eventlog", 29 | Checksums: 7, 30 | }, 31 | { 32 | Result: ErrNoEventlog, 33 | File: "tests/tpm_eventlogs/this_file_does_not_exist", 34 | Checksums: 0, 35 | }, 36 | } 37 | ) 38 | 39 | func TestParseEventlog(t *testing.T) { 40 | for _, test := range tests { 41 | err := CheckEventlogOprom(afero.NewOsFs(), test.File) 42 | if !errors.Is(err, test.Result) { 43 | t.Fatalf("Test case file '%s' not correct. Expected '%s', got '%s'", test.File, test.Result, err.Error()) 44 | } 45 | } 46 | } 47 | 48 | func TestEventlogChecksums(t *testing.T) { 49 | for _, test := range tests { 50 | digests, err := GetEventlogChecksums(afero.NewOsFs(), test.File) 51 | if err != nil { 52 | continue 53 | } 54 | if len((*digests)) == 0 { 55 | continue 56 | } 57 | if len((*digests)[0].Signatures) != test.Checksums { 58 | t.Fatalf("Test case file '%s' not correct. Expected '%d', got '%d'", test.File, test.Checksums, len((*digests))) 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package sbctl 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | 12 | "github.com/foxboron/sbctl/fs" 13 | "github.com/foxboron/sbctl/logging" 14 | "github.com/spf13/afero" 15 | ) 16 | 17 | func CreateDirectory(vfs afero.Fs, path string) error { 18 | _, err := vfs.Stat(path) 19 | switch { 20 | case errors.Is(err, os.ErrNotExist): 21 | // Ignore this error 22 | case errors.Is(err, os.ErrExist): 23 | return nil 24 | case err != nil: 25 | return err 26 | } 27 | if err := vfs.MkdirAll(path, os.ModePerm); err != nil { 28 | return err 29 | } 30 | return nil 31 | } 32 | 33 | func ReadOrCreateFile(vfs afero.Fs, filePath string) ([]byte, error) { 34 | // Try to access or create the file itself 35 | f, err := fs.ReadFile(vfs, filePath) 36 | if err != nil { 37 | // Errors will mainly happen due to permissions or non-existing file 38 | if os.IsNotExist(err) { 39 | // First, guarantee the directory's existence 40 | // os.MkdirAll simply returns nil if the directory already exists 41 | fileDir := filepath.Dir(filePath) 42 | if err = vfs.MkdirAll(fileDir, os.ModePerm); err != nil { 43 | return nil, err 44 | } 45 | 46 | file, err := vfs.Create(filePath) 47 | if err != nil { 48 | return nil, err 49 | } 50 | file.Close() 51 | 52 | // Create zero-length f, which is equivalent to what would be read from empty file 53 | f = make([]byte, 0) 54 | } else { 55 | if os.IsPermission(err) { 56 | return nil, err 57 | } 58 | return nil, err 59 | } 60 | } 61 | 62 | return f, nil 63 | } 64 | 65 | var EfivarFSFiles = []string{ 66 | "/sys/firmware/efi/efivars/PK-8be4df61-93ca-11d2-aa0d-00e098032b8c", 67 | "/sys/firmware/efi/efivars/KEK-8be4df61-93ca-11d2-aa0d-00e098032b8c", 68 | "/sys/firmware/efi/efivars/db-d719b2cb-3d3a-4596-a3bc-dad00e67656f", 69 | } 70 | 71 | var ErrImmutable = errors.New("file is immutable") 72 | var ErrNotImmutable = errors.New("file is not immutable") 73 | 74 | var Immutable = false 75 | 76 | // Check if a given file has the immutable bit set 77 | func IsImmutable(vfs afero.Fs, file string) error { 78 | // We can't actually do the syscall check. Implemented a workaround to test stuff instead 79 | if _, ok := vfs.(afero.OsFs); ok { 80 | if Immutable { 81 | return ErrImmutable 82 | } 83 | return ErrNotImmutable 84 | } 85 | f, err := os.Open(file) 86 | // Files in efivarfs might not exist. Ignore them 87 | if errors.Is(err, os.ErrNotExist) { 88 | return nil 89 | } else if err != nil { 90 | return err 91 | } 92 | defer f.Close() 93 | attr, err := GetAttr(f) 94 | if err != nil { 95 | return err 96 | } 97 | if (attr & FS_IMMUTABLE_FL) != 0 { 98 | return ErrImmutable 99 | } 100 | return ErrNotImmutable 101 | } 102 | 103 | // Check if any files in efivarfs has the immutable bit set 104 | func CheckImmutable(vfs afero.Fs) error { 105 | var isImmutable bool 106 | for _, file := range EfivarFSFiles { 107 | err := IsImmutable(vfs, file) 108 | if errors.Is(err, ErrImmutable) { 109 | isImmutable = true 110 | logging.Warn("File is immutable: %s", file) 111 | } else if errors.Is(err, ErrNotImmutable) { 112 | continue 113 | } else if err != nil { 114 | return fmt.Errorf("couldn't read file: %s", file) 115 | } 116 | } 117 | if isImmutable { 118 | return ErrImmutable 119 | } 120 | return nil 121 | } 122 | 123 | func CheckMSDos(r io.Reader) (bool, error) { 124 | // We are looking for MS-DOS executables. 125 | // They contain "MZ" as the two first bytes 126 | var header [2]byte 127 | if _, err := io.ReadFull(r, header[:]); err != nil { 128 | // File is smaller than 2 bytes 129 | if errors.Is(err, io.EOF) { 130 | return false, nil 131 | } else if errors.Is(err, io.ErrUnexpectedEOF) { 132 | return false, nil 133 | } 134 | return false, err 135 | } 136 | if !bytes.Equal(header[:], []byte{0x4d, 0x5a}) { 137 | return false, nil 138 | } 139 | return true, nil 140 | } 141 | 142 | var ( 143 | checked = make(map[string]bool) 144 | ) 145 | 146 | func AddChecked(path string) { 147 | normalized := strings.Join(strings.Split(path, "/")[2:], "/") 148 | checked[normalized] = true 149 | } 150 | 151 | func InChecked(path string) bool { 152 | normalized := strings.Join(strings.Split(path, "/")[2:], "/") 153 | return checked[normalized] 154 | } 155 | 156 | func CopyFile(vfs afero.Fs, src, dst string) error { 157 | source, err := vfs.Open(src) 158 | if err != nil { 159 | return err 160 | } 161 | defer source.Close() 162 | 163 | f, err := vfs.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) 164 | if err != nil { 165 | return err 166 | } 167 | defer f.Close() 168 | if _, err = io.Copy(f, source); err != nil { 169 | return err 170 | } 171 | si, err := vfs.Stat(src) 172 | if err != nil { 173 | return err 174 | } 175 | return vfs.Chmod(dst, si.Mode()) 176 | } 177 | 178 | // CopyDirectory moves files and creates directories 179 | func CopyDirectory(vfs afero.Fs, src, dst string) error { 180 | return afero.Walk(vfs, src, func(path string, info os.FileInfo, err error) error { 181 | if err != nil { 182 | return err 183 | } 184 | relPath := strings.TrimPrefix(path, src) 185 | newPath := filepath.Join(dst, relPath) 186 | if info.IsDir() { 187 | if err := vfs.MkdirAll(newPath, info.Mode()); err != nil { 188 | return err 189 | } 190 | return nil 191 | } 192 | if err := CopyFile(vfs, path, newPath); err != nil { 193 | return err 194 | } 195 | return nil 196 | }) 197 | } 198 | --------------------------------------------------------------------------------