sbctl/keys.go

295 lines
7.4 KiB
Go

package sbctl
import (
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"fmt"
"math/big"
"os"
"path/filepath"
"time"
"github.com/foxboron/go-uefi/efi"
"github.com/foxboron/go-uefi/efi/pecoff"
"github.com/foxboron/go-uefi/efi/pkcs7"
"github.com/foxboron/go-uefi/efi/signature"
"github.com/foxboron/go-uefi/efi/util"
"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(name string) ([]byte, []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, nil, err
}
privateKeyBytes, err := x509.MarshalPKCS8PrivateKey(priv)
if err != nil {
return nil, nil, fmt.Errorf("unable to marshal private key: %v", err)
}
keyOut := new(bytes.Buffer)
if err := pem.Encode(keyOut, &pem.Block{Type: "PRIVATE KEY", Bytes: privateKeyBytes}); err != nil {
return nil, nil, fmt.Errorf("failed to write data to key: %v", err)
}
derBytes, err := x509.CreateCertificate(rand.Reader, &c, &c, &priv.PublicKey, priv)
if err != nil {
return nil, nil, err
}
certOut := new(bytes.Buffer)
if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
return nil, nil, fmt.Errorf("failed to write data to certificate: %v", err)
}
return keyOut.Bytes(), certOut.Bytes(), nil
}
func SaveKey(k []byte, file string) error {
os.MkdirAll(filepath.Dir(file), os.ModePerm)
err := os.WriteFile(file, k, 0400)
if err != nil {
return err
}
return nil
}
func Enroll(uuid util.EFIGUID, cert, signerKey, signerPem []byte, efivar string) error {
c := signature.NewSignatureList(signature.CERT_X509_GUID)
c.AppendBytes(uuid, cert)
buf := new(bytes.Buffer)
signature.WriteSignatureList(buf, *c)
key, err := util.ReadKey(signerKey)
if err != nil {
return nil
}
crt, err := util.ReadCert(signerPem)
if err != nil {
return nil
}
signedBuf, err := efi.SignEFIVariable(key, crt, efivar, buf.Bytes())
if err != nil {
return err
}
return efi.WriteEFIVariable(efivar, signedBuf)
}
func KeySync(guid util.EFIGUID, keydir string) error {
PKKey, _ := os.ReadFile(filepath.Join(keydir, "PK", "PK.key"))
PKPem, _ := os.ReadFile(filepath.Join(keydir, "PK", "PK.pem"))
KEKKey, _ := os.ReadFile(filepath.Join(keydir, "KEK", "KEK.key"))
KEKPem, _ := os.ReadFile(filepath.Join(keydir, "KEK", "KEK.pem"))
dbPem, _ := os.ReadFile(filepath.Join(keydir, "db", "db.pem"))
if err := Enroll(guid, dbPem, KEKKey, KEKPem, "db"); err != nil {
return err
}
if err := Enroll(guid, KEKPem, PKKey, PKPem, "KEK"); err != nil {
return err
}
if err := Enroll(guid, PKPem, PKKey, PKPem, "PK"); err != nil {
return err
}
return nil
}
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)
}
peFile, err := os.ReadFile(file)
if err != nil {
return false, err
}
x509Cert, err := util.ReadCertFromFile(cert)
if err != nil {
return false, err
}
sigs, err := pecoff.GetSignatures(peFile)
if err != nil {
return false, fmt.Errorf("%s: %w", file, err)
}
if len(sigs) == 0 {
return false, nil
}
for _, signature := range sigs {
ok, err := pkcs7.VerifySignature(x509Cert, signature.Certificate)
if err != nil {
return false, err
}
if ok {
return true, nil
}
}
// If we come this far we haven't found a signature that matches the cert
return false, 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 errors.Is(err, os.ErrNotExist) && (file != output) {
// if the file does not exist and file is not the same as output
// then we just catch the error and continue. This is expected
// behaviour
} else 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)
}
// We want to write the file back with correct permissions
si, err := os.Stat(file)
if err != nil {
return fmt.Errorf("failed signing file: %w", err)
}
peFile, err := os.ReadFile(file)
if err != nil {
return err
}
Cert, err := util.ReadCertFromFile(cert)
if err != nil {
return err
}
Key, err := util.ReadKeyFromFile(key)
if err != nil {
return err
}
ctx := pecoff.PECOFFChecksum(peFile)
sig, err := pecoff.CreateSignature(ctx, Cert, Key)
if err != nil {
return err
}
b, err := pecoff.AppendToBinary(ctx, sig)
if err != nil {
return err
}
if err = os.WriteFile(output, b, si.Mode()); err != nil {
return err
}
return nil
}
// Map up our default keys in a struct
var SecureBootKeys = []struct {
Key string
Description string
}{
{
Key: "PK",
Description: "Platform Key",
},
{
Key: "KEK",
Description: "Key Exchange Key",
},
{
Key: "db",
Description: "Database Key",
},
// Haven't used this yet so WIP
// {
// Key: "dbx",
// Description: "Forbidden Database Key",
// SignedWith: "KEK",
// },
}
// Check if we have already intialized keys in the given output directory
func CheckIfKeysInitialized(output string) bool {
for _, key := range SecureBootKeys {
path := filepath.Join(output, key.Key)
if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
return false
}
}
return true
}
// Initialize the secure boot keys needed to setup secure boot.
// It creates the following keys:
// * Platform Key (PK)
// * Key Exchange Key (KEK)
// * db (database)
func InitializeSecureBootKeys(output string) error {
if CheckIfKeysInitialized(output) {
return nil
}
for _, key := range SecureBootKeys {
keyfile, cert, err := CreateKey(key.Description)
if err != nil {
return err
}
path := filepath.Join(output, key.Key)
SaveKey(keyfile, filepath.Join(path, fmt.Sprintf("%s.key", key.Key)))
SaveKey(cert, filepath.Join(path, fmt.Sprintf("%s.pem", key.Key)))
}
return nil
}