mirror of https://github.com/ericonr/sbctl.git
269 lines
7.0 KiB
Go
269 lines
7.0 KiB
Go
package sbctl
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"math/big"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/foxboron/sbctl/logging"
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
var RSAKeySize = 4096
|
|
|
|
var (
|
|
DatabasePath = "/usr/share/secureboot/"
|
|
KeysPath = filepath.Join(DatabasePath, "keys")
|
|
PKKey = filepath.Join(KeysPath, "PK", "PK.key")
|
|
PKCert = filepath.Join(KeysPath, "PK", "PK.pem")
|
|
KEKKey = filepath.Join(KeysPath, "KEK", "KEK.key")
|
|
KEKCert = filepath.Join(KeysPath, "KEK", "KEK.pem")
|
|
DBKey = filepath.Join(KeysPath, "db", "db.key")
|
|
DBCert = filepath.Join(KeysPath, "db", "db.pem")
|
|
|
|
DBPath = filepath.Join(DatabasePath, "files.db")
|
|
|
|
GUIDPath = filepath.Join(DatabasePath, "GUID")
|
|
)
|
|
|
|
// Check if we can access the db certificate to verify files
|
|
func CanVerifyFiles() error {
|
|
if err := unix.Access(DBCert, unix.R_OK); err != nil {
|
|
return fmt.Errorf("couldn't access %s: %w", DBCert, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func CreateKey(path, name string) ([]byte, error) {
|
|
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
|
serialNumber, _ := rand.Int(rand.Reader, serialNumberLimit)
|
|
c := x509.Certificate{
|
|
SerialNumber: serialNumber,
|
|
PublicKeyAlgorithm: x509.RSA,
|
|
SignatureAlgorithm: x509.SHA256WithRSA,
|
|
NotBefore: time.Now(),
|
|
NotAfter: time.Now().AddDate(5, 0, 0),
|
|
KeyUsage: x509.KeyUsageDigitalSignature,
|
|
Subject: pkix.Name{
|
|
Country: []string{name},
|
|
CommonName: name,
|
|
},
|
|
}
|
|
priv, err := rsa.GenerateKey(rand.Reader, RSAKeySize)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
derBytes, err := x509.CreateCertificate(rand.Reader, &c, &c, &priv.PublicKey, priv)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
keyOut, err := os.OpenFile(fmt.Sprintf("%s.key", path), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Failed to open key.pem for writing: %v", err)
|
|
}
|
|
privBytes, err := x509.MarshalPKCS8PrivateKey(priv)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Unable to marshal private key: %v", err)
|
|
}
|
|
if err := pem.Encode(keyOut, &pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}); err != nil {
|
|
return nil, fmt.Errorf("Failed to write data to key.pem: %v", err)
|
|
}
|
|
if err := keyOut.Close(); err != nil {
|
|
return nil, fmt.Errorf("Error closing key.pem: %v", err)
|
|
}
|
|
return derBytes, nil
|
|
}
|
|
|
|
func SaveKey(k []byte, path string) error {
|
|
err := os.WriteFile(fmt.Sprintf("%s.der", path), k, 0644)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
certOut, err := os.Create(fmt.Sprintf("%s.pem", path))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: k}); err != nil {
|
|
return err
|
|
}
|
|
if err := certOut.Close(); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func KeyToSiglist(UUID []byte, input string) error {
|
|
_, err := exec.Command(
|
|
"sbsiglist",
|
|
"--owner", string(UUID),
|
|
"--type", "x509",
|
|
"--output", fmt.Sprintf("%s.esl", input), input,
|
|
).Output()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func SignEFIVariable(key, cert, varname, vardatafile, output string) ([]byte, error) {
|
|
out, err := exec.Command("sbvarsign", "--key", key, "--cert", cert, "--output", output, varname, vardatafile).Output()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed signing EFI variable: %v", err)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func SBKeySync(dir string) bool {
|
|
cmd := exec.Command("sbkeysync", "--pk", "--verbose", "--keystore", dir)
|
|
var out bytes.Buffer
|
|
cmd.Stdout = &out
|
|
cmd.Stderr = &out
|
|
if err := cmd.Run(); err != nil {
|
|
if exitError, ok := err.(*exec.ExitError); ok {
|
|
return exitError.ExitCode() == 0
|
|
}
|
|
}
|
|
stdout := out.String()
|
|
for _, line := range strings.Split(stdout, "\n") {
|
|
if strings.Contains(line, "Operation not permitted") {
|
|
fmt.Println(stdout)
|
|
return false
|
|
}
|
|
if strings.Contains(line, "Permission denied") {
|
|
fmt.Println(stdout)
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func VerifyFile(cert, file string) (bool, error) {
|
|
if err := unix.Access(cert, unix.R_OK); err != nil {
|
|
return false, fmt.Errorf("couldn't access %s: %w", cert, err)
|
|
}
|
|
|
|
cmd := exec.Command("sbverify", "--cert", cert, file)
|
|
if err := cmd.Run(); err != nil {
|
|
if exitError, ok := err.(*exec.ExitError); ok {
|
|
return exitError.ExitCode() == 0, nil
|
|
}
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
var ErrAlreadySigned = errors.New("already signed file")
|
|
|
|
func SignFile(key, cert, file, output, checksum string) error {
|
|
|
|
// Check file exists before we do anything
|
|
if _, err := os.Stat(file); errors.Is(err, os.ErrNotExist) {
|
|
return fmt.Errorf("%s does not exist", file)
|
|
}
|
|
|
|
// Let's check if we have signed it already AND the original file hasn't changed
|
|
ok, err := VerifyFile(cert, output)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
chk, err := ChecksumFile(file)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if ok && chk == checksum {
|
|
return ErrAlreadySigned
|
|
}
|
|
|
|
// Let's also check if we can access the key
|
|
if err := unix.Access(key, unix.R_OK); err != nil {
|
|
return fmt.Errorf("couldn't access %s: %w", key, err)
|
|
}
|
|
|
|
_, err = exec.Command("sbsign", "--key", key, "--cert", cert, "--output", output, file).Output()
|
|
if err != nil {
|
|
return fmt.Errorf("failed signing file: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var SecureBootKeys = []struct {
|
|
Key string
|
|
Description string
|
|
// Path to the key we sign it with
|
|
SignedWith string
|
|
}{
|
|
{
|
|
Key: "PK",
|
|
Description: "Platform Key",
|
|
SignedWith: "PK",
|
|
},
|
|
{
|
|
Key: "KEK",
|
|
Description: "Key Exchange Key",
|
|
SignedWith: "PK",
|
|
},
|
|
{
|
|
Key: "db",
|
|
Description: "Database Key",
|
|
SignedWith: "KEK",
|
|
},
|
|
// Haven't used this yet so WIP
|
|
// {
|
|
// Key: "dbx",
|
|
// Description: "Forbidden Database Key",
|
|
// SignedWith: "KEK",
|
|
// },
|
|
}
|
|
|
|
func CheckIfKeysInitialized(output string) bool {
|
|
for _, key := range SecureBootKeys {
|
|
path := filepath.Join(output, key.Key)
|
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func InitializeSecureBootKeys(output string) error {
|
|
os.MkdirAll(output, os.ModePerm)
|
|
uuid, err := CreateGUID(output)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
logging.Print("Using Owner UUID %s\n", uuid)
|
|
// Create the directories we need and keys
|
|
for _, key := range SecureBootKeys {
|
|
path := filepath.Join(output, "keys", key.Key)
|
|
os.MkdirAll(path, os.ModePerm)
|
|
keyPath := filepath.Join(path, key.Key)
|
|
pk, err := CreateKey(keyPath, key.Description)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
SaveKey(pk, keyPath)
|
|
derSiglist := fmt.Sprintf("%s.der", keyPath)
|
|
if err := KeyToSiglist(uuid, derSiglist); err != nil {
|
|
return err
|
|
}
|
|
logging.Print("Created EFI signature list %s.esl...", derSiglist)
|
|
signingkeyPath := filepath.Join(output, "keys", key.SignedWith, key.SignedWith)
|
|
signingKey := fmt.Sprintf("%s.key", signingkeyPath)
|
|
signingCertificate := fmt.Sprintf("%s.pem", signingkeyPath)
|
|
vardatafile := fmt.Sprintf("%s.der.esl", keyPath)
|
|
logging.Print("Signing %s with %s...", vardatafile, key.Key)
|
|
SignEFIVariable(signingKey, signingCertificate, key.Key, vardatafile, fmt.Sprintf("%s.auth", keyPath))
|
|
}
|
|
return nil
|
|
}
|