cos-dkms: Enable kernel module signing during build

This change integrates kernel module signing into the build process.
Modules are now automatically signed before they are saved
to the build directory.

Signing is conditional on the presence of a private key and
a corresponding certificate. The build process sources signing keys
from the MODULES_SIGN_KEY and MODULES_SIGN_CERT environment variables,
or falls back to the default file paths.

Added an option to specify the hashing algorithm for module signing.
If the --hash-algorithm flag is passed, that algorithm will be used
for signing. Otherwise, the process will default to using SHA-256.

BUG=b/400448330
TEST= presubmit, `docker run -it -v $PWD:/usr/src/mymodule-1.0 -v
$HOME/test:/usr/src/test -e MODULES_SIGN_KEY="/usr/src/test/secure-boot-key.rsa"
-e MODULES_SIGN_CERT="/usr/src/test/secure-boot-cert.der" cos-dkms:test build
mymodule/1.0 --install-build-dependencies --make-variables=cos-default --build-id=19126.0.0
--kernel-version=cos-default`

Change-Id: I8c3c39ae71a8c9be48c761584ec3368c41cb4811
Reviewed-on: https://cos-review.googlesource.com/c/cos/tools/+/110112
Cloud-Build: GCB Service account <228075978874@cloudbuild.gserviceaccount.com>
Reviewed-by: Kevin Berry <kpberry@google.com>
Tested-by: Kevin Berry <kpberry@google.com>
diff --git a/src/cmd/cos_dkms/actions/args.go b/src/cmd/cos_dkms/actions/args.go
index a3aff80..bb5dcdc 100644
--- a/src/cmd/cos_dkms/actions/args.go
+++ b/src/cmd/cos_dkms/actions/args.go
@@ -165,6 +165,21 @@
 	return nil
 }
 
+func parseKeyCertPaths(options *dkms.Options) {
+	privateKeyPath := os.Getenv("MODULES_SIGN_KEY")
+	if privateKeyPath == "" {
+		privateKeyPath = "/var/lib/dkms/mok.key"
+	}
+
+	certificatePath := os.Getenv("MODULES_SIGN_CERT")
+	if certificatePath == "" {
+		certificatePath = "/var/lib/dkms/mok.cert"
+	}
+
+	options.PrivateKeyPath = privateKeyPath
+	options.CertificatePath = certificatePath
+}
+
 func FetchLatestCompatiblePackageVersion(pkg *dkms.Package, cache *gcs.GCSBucket) (string, error) {
 	versions, err := dkms.CompatiblePackageVersions(pkg)
 	if err != nil {
@@ -309,6 +324,7 @@
 	if err := parsePositionalArgs(args, RootPackage); err != nil {
 		return err
 	}
+	parseKeyCertPaths(RootOptions)
 
 	InitCOSValues(RootPackage, RootOptions.LSBReleasePath)
 
diff --git a/src/cmd/cos_dkms/actions/args_test.go b/src/cmd/cos_dkms/actions/args_test.go
index 323eac1..a28fad1 100644
--- a/src/cmd/cos_dkms/actions/args_test.go
+++ b/src/cmd/cos_dkms/actions/args_test.go
@@ -1,4 +1,5 @@
 package actions
+
 import (
 	"context"
 	"fmt"
@@ -32,6 +33,20 @@
 	}
 }
 
+func TestParseKeyCertPaths(t *testing.T) {
+	t.Setenv("MODULES_SIGN_KEY", "/var/modules/key")
+	t.Setenv("MODULES_SIGN_CERT", "/var/modules/cert")
+	opts := &dkms.Options{}
+	parseKeyCertPaths(opts)
+	if opts.PrivateKeyPath != "/var/modules/key" {
+		t.Fatalf("expected private key path '/var/modules/key', got: %s", opts.PrivateKeyPath)
+	}
+	if opts.CertificatePath != "/var/modules/cert" {
+		t.Fatalf("expected private key path '/var/modules/cert', got: %s", opts.CertificatePath)
+	}
+
+}
+
 func TestFetchKernelVersionFromHeaders(t *testing.T) {
 	kernelVersion, err := FetchKernelVersionFromHeaders("19126.0.0", "lakitu")
 	if err != nil {
@@ -177,10 +192,10 @@
 	t.Setenv("ARCH", "test-arch")
 	pkg := &dkms.Package{Name: "mymodule/1.0", Trees: &dkms.Trees{}}
 	expectedTrees := &dkms.Trees{
-		Source:  "/usr/src",
-		Dkms:    "",
-		Install: "/lib/modules/test-kernel-version",
-		Kernel:  "/lib/modules/test-kernel-version/build",
+		Source:        "/usr/src",
+		Dkms:          "",
+		Install:       "/lib/modules/test-kernel-version",
+		Kernel:        "/lib/modules/test-kernel-version/build",
 		KernelModules: "/lib/modules/test-kernel-version",
 	}
 	expectedPackage := &dkms.Package{
diff --git a/src/cmd/cos_dkms/actions/root.go b/src/cmd/cos_dkms/actions/root.go
index ecca365..f17f0cc 100644
--- a/src/cmd/cos_dkms/actions/root.go
+++ b/src/cmd/cos_dkms/actions/root.go
@@ -147,6 +147,8 @@
 		"variables to be passed to the make command, such as CC. If set to 'cos-default', the variables for compiling the COS kernel will be used.")
 	pflags.StringVar(&RootOptions.LSBReleasePath, "lsb-release-path", "/etc/lsb-release",
 		"path to the LSB Release file which will be used to populate default values for --build-id and --board if they are not supplied")
+	pflags.StringVar(&RootOptions.Hash, "hash-algorithm", "sha256",
+		"the hash algorithm used during signing of modules. Currently, only SHA-2 and SHA-3 families are supported (e.g. 'sha256')")
 }
 
 func Execute() {
diff --git a/src/pkg/dkms/BUILD.bazel b/src/pkg/dkms/BUILD.bazel
index 203acee..e71d0b3 100644
--- a/src/pkg/dkms/BUILD.bazel
+++ b/src/pkg/dkms/BUILD.bazel
@@ -32,6 +32,7 @@
         "unbuild.go",
         "uninstall.go",
         "versions.go",
+        "sign.go",
     ],
     importpath = "cos.googlesource.com/cos/tools.git/src/pkg/dkms",
     visibility = ["//visibility:public"],
@@ -57,6 +58,7 @@
         "install_test.go",
         "toolchain_test.go",
         "versions_test.go",
+        "sign_test.go",
     ],
     data = glob(
         ["testdata/**"],
diff --git a/src/pkg/dkms/build.go b/src/pkg/dkms/build.go
index 952ad5b..4bb006a 100644
--- a/src/pkg/dkms/build.go
+++ b/src/pkg/dkms/build.go
@@ -18,8 +18,14 @@
 	"google.golang.org/api/option"
 )
 
-// Build compiles a DKMS package using the MAKE command from dkms.conf and saves
-// the results to the package's build directory in the DKMS tree.
+// Build compiles a DKMS package using the MAKE command from dkms.conf,
+// signs the compiled modules, and saves the results to the package's
+// build directory in the DKMS tree.
+//
+// If the path to a private key and a certificate is specified in the environmental
+// variables 'MODULES_SIGN_KEY' and 'MODULES_SIGN_CERT' respectively, or the files
+// exist in '/var/lib/dkms/mok.key' and '/var/lib/dkms/mok.pub', then this would
+// sign the compiled modules with the provided key and certificate.
 //
 // If the package is not already added to the DKMS source tree, this will try to
 // add it.
@@ -97,6 +103,10 @@
 		return fmt.Errorf("failed to compile modules: %v", err)
 	}
 
+	if err := SignModules(config.Modules, options.PrivateKeyPath, options.CertificatePath, options.Hash); err != nil {
+		glog.Warningf("skipping module signing for package (%s): %v", pkg.Name, err)
+	}
+
 	if config.PostBuild != "" {
 		if err := utils.RunCommandString(buildDir, config.PostBuild); err != nil {
 			return fmt.Errorf("failed to run post-build script: %v", err)
diff --git a/src/pkg/dkms/build_test.go b/src/pkg/dkms/build_test.go
index 6da1485..15c5800 100644
--- a/src/pkg/dkms/build_test.go
+++ b/src/pkg/dkms/build_test.go
@@ -48,12 +48,77 @@
 
 	buildMakefilePath := path.Join(trees.Dkms, "mymodule", "1.0", "6.1.100", "x86_64", "18244.151.14", "build", "Makefile")
 	if !fs.IsFile(buildMakefilePath) {
-		t.Fatalf("expected to find file %s after building", makefilePath)
+		t.Fatalf("expected to find file %s after building", buildMakefilePath)
 	}
 
 	builtModulePath := path.Join(trees.Dkms, "mymodule", "1.0", "6.1.100", "x86_64", "18244.151.14", "build", "mymodule.ko")
 	if !fs.IsFile(builtModulePath) {
-		t.Fatalf("expected to find file %s after building", makefilePath)
+		t.Fatalf("expected to find file %s after building", builtModulePath)
+	}
+}
+
+func TestBuildModuleSigning(t *testing.T) {
+	signingDir := t.TempDir()
+
+	options := &Options{
+		Force:                    false,
+		MakeVariables:            "",
+		InstallBuildDependencies: false,
+		Hash:                     "sha256",
+		PrivateKeyPath:           path.Join(signingDir, "rsaKey.pem"),
+		CertificatePath:          path.Join(signingDir, "cert.der"),
+	}
+
+	trees := &Trees{
+		Dkms:   t.TempDir(),
+		Source: "testdata/source-tree",
+		Kernel: "testdata/kernel-source/lib/modules/6.1.100/build",
+	}
+
+	pkg := &Package{
+		Name:          "mymodule",
+		Version:       "1.0",
+		KernelVersion: "6.1.100",
+		Arch:          "x86_64",
+		BuildId:       "18244.151.14",
+		Trees:         trees,
+	}
+
+	key, keyBytes, err := generateRSAPrivateKey()
+	if err != nil {
+		t.Fatalf("failed to generate rsa key: %v", err)
+	}
+	if err = os.WriteFile(options.PrivateKeyPath, keyBytes, 0600); err != nil {
+		t.Fatalf("failed to write rsa key to file (%s): %v", options.PrivateKeyPath, err)
+	}
+
+	certBytes, err := generateCertificate(key)
+	if err != nil {
+		t.Fatalf("failed to generate certificate: %v", err)
+	}
+	if err = os.WriteFile(options.CertificatePath, certBytes, 0600); err != nil {
+		t.Fatalf("failed to write certificate to file (%s): %v", options.CertificatePath, err)
+	}
+
+	err = Build(pkg, options)
+	if err != nil {
+		t.Fatalf("%v", err)
+	}
+
+	builtModulePath := path.Join(trees.Dkms, "mymodule", "1.0", "6.1.100", "x86_64", "18244.151.14", "build", "mymodule.ko")
+	if !fs.IsFile(builtModulePath) {
+		t.Fatalf("expected to find file %s after building", builtModulePath)
+	}
+
+	moduleBytes, err := os.ReadFile(builtModulePath)
+	if err != nil {
+		t.Fatalf("failed to read module: %s", builtModulePath)
+	}
+	expectedMessage := []byte("~Module signature appended~\n")
+
+	diff := cmp.Diff(expectedMessage, moduleBytes[len(moduleBytes)-len(expectedMessage):])
+	if diff != "" {
+		t.Fatalf("expected signature did not match\ndiff: %s", diff)
 	}
 }
 
diff --git a/src/pkg/dkms/options.go b/src/pkg/dkms/options.go
index 91972a4..9227afa 100644
--- a/src/pkg/dkms/options.go
+++ b/src/pkg/dkms/options.go
@@ -72,4 +72,14 @@
 	// to fill in the package Build and Board values if they are not otherwise
 	// specified.
 	LSBReleasePath string
+	// Hash is a string that tells Build what hashing digest algorithm to use
+	// during module signing. The default hashing algorithm is 'sha256'. Cos-dkms
+	// currently supports SHA-2 and SHA-3 families.
+	Hash string
+	// PrivateKeyPath is the path to the private key that would be used to sign
+	// modules during Build.
+	PrivateKeyPath string
+	// CertificatePath is the path to the certificate that would be used to sign
+	// modules during Build.
+	CertificatePath string
 }
diff --git a/src/pkg/dkms/sign.go b/src/pkg/dkms/sign.go
new file mode 100644
index 0000000..45c62ae
--- /dev/null
+++ b/src/pkg/dkms/sign.go
@@ -0,0 +1,328 @@
+package dkms
+
+import (
+	"bytes"
+	"crypto"
+	"crypto/rand"
+	"crypto/rsa"
+	"crypto/x509"
+	"crypto/x509/pkix"
+	"encoding/asn1"
+	"encoding/binary"
+	"encoding/pem"
+	"fmt"
+	"math/big"
+	"os"
+	"strings"
+
+	"github.com/golang/glog"
+)
+
+const (
+	// PKEYIDPKCS7 is the constant PKEY_ID_PKCS7 defined in https://github.com/torvalds/linux/blob/master/scripts/sign-file.c
+	PKEYIDPKCS7 = byte(2)
+	// magicNumber is the constant magic_number defined in https://github.com/torvalds/linux/blob/master/scripts/sign-file.c
+	magicNumber = "~Module signature appended~\n"
+)
+
+// https://www.rfc-editor.org/rfc/rfc2315#section-9.1
+//  SignedData ::= SEQUENCE {
+//  version Version,
+//  digestAlgorithms DigestAlgorithmIdentifiers,
+//  contentInfo ContentInfo,
+//  certificates
+//     [0] IMPLICIT ExtendedCertificatesAndCertificates
+//       OPTIONAL,
+//  crls
+//    [1] IMPLICIT CertificateRevocationLists OPTIONAL,
+//  signerInfos SignerInfos }
+
+type signedData struct {
+	Version                    int
+	DigestAlgorithmIdentifiers []pkix.AlgorithmIdentifier `asn1:"set"`
+	ContentInfo                contentInfo
+	SignerInfos                []signerInfo `asn1:"set"`
+}
+
+// https://www.rfc-editor.org/rfc/rfc2315#section-7
+// ContentInfo ::= SEQUENCE {
+//
+//	 contentType ContentType,
+//	 content  // NOTE: not needed for detached signature
+//	 [0] EXPLICIT ANY DEFINED BY contentType OPTIONAL }
+//
+//	ContentType ::= OBJECT IDENTIFIER
+type contentInfo struct {
+	ContentType asn1.ObjectIdentifier
+}
+
+// https://www.rfc-editor.org/rfc/rfc2315#section-9.2
+// SignerInfo ::= SEQUENCE {
+//
+//	version Version,
+//	issuerAndSerialNumber IssuerAndSerialNumber,
+//	digestAlgorithm DigestAlgorithmIdentifier,
+//	authenticatedAttributes  // NOTE: not used in Linux kernel module signature
+//	  [0] IMPLICIT Attributes OPTIONAL,
+//	digestEncryptionAlgorithm
+//	  DigestEncryptionAlgorithmIdentifier,
+//	encryptedDigest EncryptedDigest,
+//	unauthenticatedAttributes // NOTE: not used in Linux kernel module signature
+//	  [1] IMPLICIT Attributes OPTIONAL }
+//
+// EncryptedDigest ::= OCTET STRING
+type signerInfo struct {
+	Version                   int
+	IssuerAndSerialNumber     issuerAndSerialNumber
+	DigestAlgorithm           pkix.AlgorithmIdentifier
+	DigestEncryptionAlgorithm pkix.AlgorithmIdentifier
+	EncryptedDigest           []byte
+}
+
+// https://www.rfc-editor.org/rfc/rfc2315#section-6.7
+// IssuerAndSerialNumber ::= SEQUENCE {
+//
+//	issuer Name,
+//	serialNumber CertificateSerialNumber }
+type issuerAndSerialNumber struct {
+	Issuer       asn1.RawValue
+	SerialNumber *big.Int
+}
+
+type pkcs7Blob struct {
+	Oid        asn1.ObjectIdentifier
+	SignedData signedData `asn1:"tag:0,explicit"`
+}
+
+type digestAlgorithm struct {
+	identifier asn1.ObjectIdentifier
+	hash       crypto.Hash
+}
+
+var (
+	oidData          = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 1}
+	oidRsaEncryption = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 1}
+	oidSignedData    = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 2}
+
+	digests = map[string]digestAlgorithm{
+		"sha256": {
+			identifier: asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 1},
+			hash:       crypto.SHA256,
+		},
+		"sha384": {
+			identifier: asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 2},
+			hash:       crypto.SHA384,
+		},
+		"sha512": {
+			identifier: asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 3},
+			hash:       crypto.SHA512,
+		},
+		"sha3-256": {
+			identifier: asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 8},
+			hash:       crypto.SHA3_256,
+		},
+		"sha3-384": {
+			identifier: asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 9},
+			hash:       crypto.SHA3_384,
+		},
+		"sha3-512": {
+			identifier: asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 10},
+			hash:       crypto.SHA3_512,
+		},
+	}
+)
+
+// SignModules signs compiled kernel modules with a private key and certificate.
+//
+// Currently, only rsa private keys are supported. It creates a pksc7 signature
+// and appends it to the end of a compiled kernel module.
+// This is an implementation of https://github.com/torvalds/linux/blob/master/scripts/sign-file.c
+func SignModules(modules []Module, keyPath, certificatePath, hash string) error {
+	keyBytes, err := os.ReadFile(keyPath)
+	if err != nil {
+		return fmt.Errorf("failed to read private key file (%s): %v", keyPath, err)
+	}
+	key, err := parsePrivateKey(keyBytes)
+	if err != nil {
+		return fmt.Errorf("failed to retrieve private key: %v", err)
+	}
+
+	certBytes, err := os.ReadFile(certificatePath)
+	if err != nil {
+		return fmt.Errorf("failed to read certificate key file (%s): %v", certificatePath, err)
+	}
+	cert, err := x509.ParseCertificate(certBytes)
+	if err != nil {
+		return fmt.Errorf("failed to retrieve certificate (%s): %v", certificatePath, err)
+	}
+	digest, ok := digests[strings.ToLower(hash)]
+	if !ok {
+		return fmt.Errorf("unsupported digest algorithm: %s", hash)
+	}
+
+	var signedModules, skippedModules []string
+	for _, module := range modules {
+		glog.Infof("signing module %s", module.BuiltName)
+		if err := signModule(module, key, cert, digest); err != nil {
+			skippedModules = append(skippedModules, module.BuiltName)
+			glog.Errorf("failed to sign module (%s) [skipping]: %v", module.BuiltName, err)
+			continue
+		}
+		signedModules = append(signedModules, module.BuiltName)
+		glog.Infof("successfully signed module '%s'", module.BuiltName)
+	}
+
+	glog.Infof("\nSummary:")
+	if len(signedModules) > 0 {
+		glog.Infof("Successfully signed module(s): %v", signedModules)
+	}
+	if len(skippedModules) > 0 {
+		glog.Errorf("Failed to sign module(s): %v", skippedModules)
+	}
+
+	return nil
+}
+
+func parsePrivateKey(contents []byte) (crypto.PrivateKey, error) {
+	// Retrive pem encodded block.
+	block, _ := pem.Decode(contents)
+	if block == nil {
+		return nil, fmt.Errorf("failed to decode pem-formatted key or invalid key type")
+	}
+
+	var privateKey crypto.PrivateKey
+	var err error
+	switch block.Type {
+	case "RSA PRIVATE KEY":
+		privateKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
+		if err != nil {
+			return nil, fmt.Errorf("failed to parse rsa private key: %v", err)
+		}
+	case "PRIVATE KEY":
+		key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
+		if err != nil {
+			return nil, fmt.Errorf("failed to parse private key: %v", err)
+		}
+		switch k := key.(type) {
+		case *rsa.PrivateKey:
+			privateKey = k
+		default:
+			return nil, fmt.Errorf("unsupported key type: %T", key)
+		}
+	default:
+		return nil, fmt.Errorf("unsupported block type: %s", block.Type)
+	}
+	return privateKey, nil
+}
+
+func signModule(module Module, key crypto.PrivateKey, certificate *x509.Certificate, digest digestAlgorithm) error {
+	moduleName := module.BuiltName
+	modulePath := module.BuiltPath()
+
+	moduleBytes, err := os.ReadFile(modulePath)
+	if err != nil {
+		return fmt.Errorf("failed to read module file (%s): %v", modulePath, err)
+	}
+	signature, err := generateModuleSignature(key, moduleBytes, digest.hash)
+	if err != nil {
+		return fmt.Errorf("failed to generate signature for module (%s): %v", moduleName, err)
+	}
+
+	digestAlg := &pkix.AlgorithmIdentifier{Algorithm: digest.identifier, Parameters: asn1.NullRawValue}
+	encrypAlg, err := retrieveDigestEncryptionAlgo(key)
+	if err != nil {
+		return fmt.Errorf("failed to retrieve digest encryption algorithm for module (%s): %v", moduleName, err)
+	}
+	sinfo := signerInfo{
+		Version: 1,
+		IssuerAndSerialNumber: issuerAndSerialNumber{
+			Issuer:       asn1.RawValue{FullBytes: certificate.RawIssuer},
+			SerialNumber: certificate.SerialNumber,
+		},
+		DigestAlgorithm:           *digestAlg,
+		DigestEncryptionAlgorithm: *encrypAlg,
+		EncryptedDigest:           signature,
+	}
+
+	blob := pkcs7Blob{
+		Oid: oidSignedData,
+		SignedData: signedData{
+			Version:                    1,
+			DigestAlgorithmIdentifiers: []pkix.AlgorithmIdentifier{*digestAlg},
+			ContentInfo:                contentInfo{ContentType: oidData},
+			SignerInfos:                []signerInfo{sinfo},
+		},
+	}
+	marshalled, err := asn1.Marshal(blob)
+	if err != nil {
+		return fmt.Errorf("failed to marshal pkcs7 blob for module (%s): %v", moduleName, err)
+	}
+
+	signedBytes, err := appendSignature(marshalled, moduleBytes)
+	if err != nil {
+		return fmt.Errorf("failed to append signature to module (%s): %v", moduleName, err)
+	}
+
+	// Write the buffer content to the output file, overwriting it.
+	if err := os.WriteFile(modulePath, signedBytes, 0644); err != nil {
+		return fmt.Errorf("failed to write signed module to %s: %v", modulePath, err)
+	}
+	return nil
+}
+
+func generateModuleSignature(privateKey crypto.PrivateKey, moduleBytes []byte, hash crypto.Hash) ([]byte, error) {
+	// Retrieve the crypto signer if it is implemented for the private key
+	signer, ok := privateKey.(crypto.Signer)
+	if !ok {
+		return nil, fmt.Errorf("could not retrieve crypto signer for key type: %T", privateKey)
+	}
+	h := hash.New()
+	h.Write(moduleBytes)
+	moduleDigest := h.Sum(nil)
+	signature, err := signer.Sign(rand.Reader, moduleDigest, hash)
+	if err != nil {
+		return nil, fmt.Errorf("failed to sign the module digest: %v", err)
+	}
+	return signature, nil
+}
+
+func retrieveDigestEncryptionAlgo(key crypto.PrivateKey) (*pkix.AlgorithmIdentifier, error) {
+	switch key.(type) {
+	case *rsa.PrivateKey:
+		return &pkix.AlgorithmIdentifier{Algorithm: oidRsaEncryption, Parameters: asn1.NullRawValue}, nil
+	default:
+		return nil, fmt.Errorf("unsupported digest encryption algorithm: %T", key)
+	}
+}
+
+// appendSignature appends a raw PKCS#7 signature to the end of a given kernel module.
+func appendSignature(signature []byte, moduleBytes []byte) ([]byte, error) {
+	var buf bytes.Buffer
+	// Write bytes of kernel module.
+	if _, err := buf.Write(moduleBytes); err != nil {
+		return nil, fmt.Errorf("failed to write module bytes to buffer: %v", err)
+	}
+	// Copy bytes of module signature.
+	sigSize, err := buf.Write(signature)
+	if err != nil {
+		return nil, fmt.Errorf("failed to write signature to buffer: %v", err)
+	}
+	// Append the marker and the PKCS#7 message.
+	// signatureInfo is the struct module_signature defined in
+	// https://github.com/torvalds/linux/blob/master/scripts/sign-file.c
+	signatureInfo := [12]byte{}
+	// signatureInfo[2] is the id_type of struct module_signature
+	signatureInfo[2] = PKEYIDPKCS7
+	// signatureInfo[8:12] is the sig_len of struct module_signature.
+	// Using BigEndian as the sig_len should be in network byte order.
+	binary.BigEndian.PutUint32(signatureInfo[8:12], uint32(sigSize))
+	if _, err := buf.Write(signatureInfo[:]); err != nil {
+		return nil, fmt.Errorf("failed to write module signature struct to buffer: %v", err)
+	}
+
+	if _, err := buf.WriteString(magicNumber); err != nil {
+		return nil, fmt.Errorf("failed to write magic number to buffer: %v", err)
+	}
+
+	return buf.Bytes(), nil
+}
diff --git a/src/pkg/dkms/sign_test.go b/src/pkg/dkms/sign_test.go
new file mode 100644
index 0000000..a37b4a4
--- /dev/null
+++ b/src/pkg/dkms/sign_test.go
@@ -0,0 +1,254 @@
+package dkms
+
+import (
+	"bytes"
+	"crypto"
+	"crypto/ecdsa"
+	"crypto/elliptic"
+	"crypto/rand"
+	"crypto/rsa"
+	"crypto/sha256"
+	"crypto/x509"
+	"crypto/x509/pkix"
+	"encoding/pem"
+	"fmt"
+	"math/big"
+	"os"
+	"path"
+	"reflect"
+	"testing"
+	"time"
+
+	"github.com/google/go-cmp/cmp"
+)
+
+func TestParsePrivateKey(t *testing.T) {
+	ecdsaBytes, err := generateECDSAPrivateKey()
+	if err != nil {
+		t.Fatalf("failed to generate ecdsa private key: %v", err)
+	}
+
+	_, rsaBytes, err := generateRSAPrivateKey()
+	if err != nil {
+		t.Fatalf("failed to generate rsa private key: %v", err)
+	}
+	testCases := []struct {
+		desc        string
+		contents    []byte
+		wantErr     bool
+		expectedKey crypto.PrivateKey
+	}{
+		{
+			"Unsupported key",
+			ecdsaBytes,
+			true,
+			nil,
+		},
+		{
+			"Supported rsa key",
+			rsaBytes,
+			false,
+			&rsa.PrivateKey{},
+		},
+		{
+			"Empty bytes",
+			[]byte{},
+			true,
+			nil,
+		},
+		{
+			"Nonsense bytes",
+			[]byte("this is not a key"),
+			true,
+			nil,
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.desc, func(t *testing.T) {
+			privateKey, err := parsePrivateKey(test.contents)
+			if gotErr := err != nil; gotErr != test.wantErr {
+				t.Fatalf("got error: %v, want error: %v", err, test.wantErr)
+			}
+			if reflect.TypeOf(privateKey) != reflect.TypeOf(test.expectedKey) {
+				t.Fatalf("private key type did not match; expected: %T, got: %T", test.expectedKey, privateKey)
+			}
+		})
+	}
+}
+
+func TestGenerateModuleSignature(t *testing.T) {
+	privateKey, _, err := generateRSAPrivateKey()
+	if err != nil {
+		t.Fatalf("failed to generate rsa key: %v", err)
+	}
+
+	content := []byte("module")
+
+	hash := crypto.SHA256
+	signature, err := generateModuleSignature(privateKey, content, hash)
+	if err != nil {
+		t.Fatalf("%v", err)
+	}
+
+	hashedDigest := sha256.Sum256(content)
+	// Ensure signature is RSA PKCS #1 v1.5 signature
+	publicKey := privateKey.PublicKey
+	if err := rsa.VerifyPKCS1v15(&publicKey, hash, hashedDigest[:], signature); err != nil {
+		t.Fatalf("failed to verify signature: %v", err)
+	}
+}
+
+func TestAppendSignature(t *testing.T) {
+	signature := []byte("signature")
+	contents := []byte("module")
+
+	signedBytes, err := appendSignature(signature, contents)
+	if err != nil {
+		t.Fatalf("%v", err)
+	}
+
+	expectedBytes := [...]byte{
+		// The following line is the bytes of the original module: "module"
+		0x6D, 0x6F, 0x64, 0x75, 0x6c, 0x65,
+		// The following line is the bytes of the signature: "signature"
+		0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65,
+		// The following lines are the bytes of module_signature struct
+		0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00,
+		0x00, 0x00, 0x00, 0x09,
+		// The following lines are the bytes of PKCS7 message: "~Module signature appended~\n"
+		0x7e, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x20, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x20, 0x61,
+		0x70, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x64, 0x7e, 0xa,
+	}
+
+	if diff := cmp.Diff(expectedBytes[:], signedBytes); diff != "" {
+		t.Errorf("signed module content doesn't match,\nwant: %v\ngot: %v\ndiff: %v",
+			expectedBytes, signedBytes, diff)
+	}
+}
+
+func TestSignModules(t *testing.T) {
+	tmpDir := t.TempDir()
+	keyPath := path.Join(tmpDir, "rsaKey.pem")
+
+	key, keyBytes, err := generateRSAPrivateKey()
+	if err != nil {
+		t.Fatalf("failed to generate rsa key: %v", err)
+	}
+	if err = os.WriteFile(keyPath, keyBytes, 0600); err != nil {
+		t.Fatalf("failed to write rsa key to file (%s): %v", keyPath, err)
+	}
+
+	certPath := path.Join(tmpDir, "cert.der")
+	certBytes, err := generateCertificate(key)
+	if err != nil {
+		t.Fatalf("failed to generate certificate: %v", err)
+	}
+	if err = os.WriteFile(certPath, certBytes, 0600); err != nil {
+		t.Fatalf("failed to write certificate content to file (%s): %v", keyPath, err)
+	}
+
+	pkg := &Package{
+		Name:          "module",
+		Version:       "1.0",
+		KernelVersion: "6.1.100",
+		Arch:          "x86_64",
+		BuildId:       "18244.151.14",
+		Trees:         &Trees{Dkms: tmpDir},
+	}
+
+	module := Module{
+		BuiltName: "module",
+		Package:   pkg,
+	}
+
+	modules := []Module{module}
+	modulePath := module.BuiltPath()
+
+	err = os.MkdirAll(path.Dir(modulePath), 0755)
+	if err != nil {
+		t.Fatalf("failed to create module directory (%s): %v", modulePath, err)
+	}
+
+	content := []byte("module")
+	if err := os.WriteFile(modulePath, content, 0600); err != nil {
+		t.Fatalf("failed to write temp module file (%s): %v", modulePath, err)
+	}
+
+	if err := SignModules(modules, keyPath, certPath, "sha256"); err != nil {
+		t.Fatalf("%v", err)
+	}
+
+	signedModule, err := os.ReadFile(modulePath)
+	if err != nil {
+		t.Fatalf("failed to read from module file after signing: %v", err)
+	}
+
+	if !bytes.HasSuffix(signedModule, []byte(magicNumber)) {
+		t.Errorf("signed module does not end with magic number:\nexpected:%s\tgot:%s",
+			[]byte(magicNumber), signedModule[len(signedModule)-len(magicNumber):])
+	}
+	if !bytes.HasPrefix(signedModule, content) {
+		t.Errorf("signed module does not start with original content:\nexpected:%s\tgot:%s",
+			content, signedModule[len(content):])
+	}
+}
+
+// generateRSAPrivateKey creates a new RSA private key
+func generateRSAPrivateKey() (*rsa.PrivateKey, []byte, error) {
+	privateKey, err := rsa.GenerateKey(rand.Reader, 4096)
+	if err != nil {
+		return nil, nil, fmt.Errorf("failed to generate private key: %v", err)
+	}
+
+	// Convert the private key to PEM format.
+	pemBytes := pem.EncodeToMemory(&pem.Block{
+		Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)})
+
+	return privateKey, pemBytes, nil
+}
+
+// generateECDSAPrivateKey creates a new ecdsa private key
+func generateECDSAPrivateKey() ([]byte, error) {
+	privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+	if err != nil {
+		return nil, fmt.Errorf("failed to generate private key: %v", err)
+	}
+
+	derBytes, err := x509.MarshalECPrivateKey(privateKey)
+	if err != nil {
+		return nil, fmt.Errorf("failed to marshal ecdsa private key: %v", err)
+	}
+
+	// Retrieve the PEM format of the private key
+	pemBytes := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: derBytes})
+	return pemBytes, nil
+}
+
+// generateCertificate creates a new certificate with an associated private key
+func generateCertificate(privateKey *rsa.PrivateKey) ([]byte, error) {
+	serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
+	if err != nil {
+		return nil, fmt.Errorf("failed to generate serial number: %v", err)
+	}
+	template := x509.Certificate{
+		SerialNumber: serialNumber,
+		Subject: pkix.Name{
+			Country:    []string{"US"},
+			CommonName: "secure-boot-cert",
+		},
+		NotBefore: time.Now(),
+
+		KeyUsage:    x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
+		ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
+		DNSNames:    []string{"secure-boot.com"},
+	}
+
+	derBytes, err := x509.CreateCertificate(
+		rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create certificate: %v", err)
+	}
+
+	return derBytes, nil
+}