| package dkms | |
| import ( | |
| "bytes" | |
| "context" | |
| "crypto" | |
| "crypto/rand" | |
| "crypto/rsa" | |
| "crypto/x509" | |
| "crypto/x509/pkix" | |
| "encoding/asn1" | |
| "encoding/binary" | |
| "encoding/pem" | |
| "fmt" | |
| "hash/crc32" | |
| "math/big" | |
| "os" | |
| "strings" | |
| kms "cloud.google.com/go/kms/apiv1" | |
| "cloud.google.com/go/kms/apiv1/kmspb" | |
| "github.com/golang/glog" | |
| "google.golang.org/protobuf/types/known/wrapperspb" | |
| ) | |
| 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 | |
| } | |
| // ModuleSigner is an interface for signing kernel modules. | |
| type ModuleSigner interface { | |
| // Sign generates a digital signature for the given module bytes. | |
| Sign(moduleBytes []byte, hash crypto.Hash) ([]byte, error) | |
| // retrieveDigestEncryptionAlgorithm returns the algorithm identifier used for | |
| // encrypting the digest (e.g., RSA). | |
| retrieveDigestEncryptionAlgorithm() (*pkix.AlgorithmIdentifier, error) | |
| } | |
| // KmsSigner implements the ModuleSigner interface using Google Cloud KMS for signing operations. | |
| type KmsSigner struct { | |
| ctx context.Context | |
| client *kms.KeyManagementClient | |
| keyName string | |
| } | |
| // LocalSigner implements the ModuleSigner interface using a local private key for signing. | |
| type LocalSigner struct { | |
| key crypto.PrivateKey | |
| } | |
| 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, | |
| }, | |
| } | |
| ) | |
| func newKmsSigner(ctx context.Context, kmsKey string) (*KmsSigner, error) { | |
| client, err := kms.NewKeyManagementClient(ctx) | |
| if err != nil { | |
| return nil, err | |
| } | |
| return &KmsSigner{ctx: ctx, client: client, keyName: kmsKey}, nil | |
| } | |
| func newLocalSigner(keyPath string) (*LocalSigner, error) { | |
| keyBytes, err := os.ReadFile(keyPath) | |
| if err != nil { | |
| return nil, fmt.Errorf("failed to read private key file (%s): %v", keyPath, err) | |
| } | |
| key, err := parsePrivateKey(keyBytes) | |
| if err != nil { | |
| return nil, fmt.Errorf("failed to retrieve private key: %v", err) | |
| } | |
| return &LocalSigner{key: key}, nil | |
| } | |
| func (l *LocalSigner) Sign(moduleBytes []byte, hash crypto.Hash) ([]byte, error) { | |
| // Retrieve the crypto signer if it is implemented for the private key | |
| signer, ok := l.key.(crypto.Signer) | |
| if !ok { | |
| return nil, fmt.Errorf("could not retrieve crypto signer for key type: %T", l.key) | |
| } | |
| 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 (l *LocalSigner) retrieveDigestEncryptionAlgorithm() (*pkix.AlgorithmIdentifier, error) { | |
| switch l.key.(type) { | |
| case *rsa.PrivateKey: | |
| return &pkix.AlgorithmIdentifier{Algorithm: oidRsaEncryption, Parameters: asn1.NullRawValue}, nil | |
| default: | |
| return nil, fmt.Errorf("unsupported digest encryption algorithm: %T", l.key) | |
| } | |
| } | |
| func (k *KmsSigner) Sign(moduleBytes []byte, hash crypto.Hash) ([]byte, error) { | |
| h := hash.New() | |
| h.Write(moduleBytes) | |
| moduleDigest := h.Sum(nil) | |
| kmsDigest, err := k.retrieveDigestAlg(hash, moduleDigest) | |
| if err != nil { | |
| return nil, fmt.Errorf("failed to retrieve KMS digest algorithm: %v", err) | |
| } | |
| // Compute digest's CRC32C | |
| t := crc32.MakeTable(crc32.Castagnoli) | |
| digestCRC32C := crc32.Checksum(moduleDigest, t) | |
| req := &kmspb.AsymmetricSignRequest{ | |
| Name: k.keyName, | |
| Digest: kmsDigest, | |
| DigestCrc32C: wrapperspb.Int64(int64(digestCRC32C)), | |
| } | |
| result, err := k.client.AsymmetricSign(k.ctx, req) | |
| if err != nil { | |
| return nil, fmt.Errorf("failed to generate signature using KMS: %v", err) | |
| } | |
| // Verify integrity of the result | |
| if !result.VerifiedDigestCrc32C { | |
| return nil, fmt.Errorf("AsymmetricSignError: digest corrupted in transit") | |
| } | |
| if result.Name != req.Name { | |
| return nil, fmt.Errorf("AsymmetricSignError: mismatched key name in the response") | |
| } | |
| if int64(crc32.Checksum(result.Signature, t)) != result.SignatureCrc32C.Value { | |
| return nil, fmt.Errorf("AsymmetricSignError: response corrupted in transit") | |
| } | |
| return result.Signature, nil | |
| } | |
| func (k *KmsSigner) retrieveDigestEncryptionAlgorithm() (*pkix.AlgorithmIdentifier, error) { | |
| req := &kmspb.GetCryptoKeyVersionRequest{ | |
| Name: k.keyName, | |
| } | |
| keyVersion, err := k.client.GetCryptoKeyVersion(k.ctx, req) | |
| if err != nil { | |
| return nil, fmt.Errorf("failed to get key version: %v", err) | |
| } | |
| // Verify that key encryption is supported | |
| encryptAlg := keyVersion.GetAlgorithm().String() | |
| if strings.HasPrefix(encryptAlg, "RSA_") { | |
| return &pkix.AlgorithmIdentifier{Algorithm: oidRsaEncryption, Parameters: asn1.NullRawValue}, nil | |
| } | |
| return nil, fmt.Errorf("unsupported digest encryption algorithm: %v", encryptAlg) | |
| } | |
| func (k *KmsSigner) retrieveDigestAlg(hash crypto.Hash, moduleDigest []byte) (*kmspb.Digest, error) { | |
| var digest *kmspb.Digest | |
| switch hash { | |
| case crypto.SHA256: | |
| digest = &kmspb.Digest{ | |
| Digest: &kmspb.Digest_Sha256{ | |
| Sha256: moduleDigest, | |
| }, | |
| } | |
| case crypto.SHA384: | |
| digest = &kmspb.Digest{ | |
| Digest: &kmspb.Digest_Sha384{ | |
| Sha384: moduleDigest, | |
| }, | |
| } | |
| case crypto.SHA512: | |
| digest = &kmspb.Digest{ | |
| Digest: &kmspb.Digest_Sha512{ | |
| Sha512: moduleDigest, | |
| }, | |
| } | |
| default: | |
| return nil, fmt.Errorf("unsupported kms digest algorithm: %s", hash.String()) | |
| } | |
| return digest, nil | |
| } | |
| // SignModules signs a list of compiled kernel modules using the provided ModuleSigner. | |
| // Currently, only rsa private keys are supported. It creates a pkcs7 signature | |
| // and appends it to the end of a compiled kernel module. | |
| // | |
| // The ModuleSigner handles the specific signing method (e.g., local key or Cloud KMS) | |
| // This is an implementation of https://github.com/torvalds/linux/blob/master/scripts/sign-file.c | |
| func SignModules(modules []Module, certificatePath, hash string, signer ModuleSigner) error { | |
| 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) | |
| } | |
| encryptAlg, err := signer.retrieveDigestEncryptionAlgorithm() | |
| if err != nil { | |
| return fmt.Errorf("failed to retrieve digest encryption algorithm: %v", err) | |
| } | |
| var signedModules, skippedModules []string | |
| for _, module := range modules { | |
| moduleName := module.BuiltName | |
| modulePath := module.BuiltPath() | |
| glog.Infof("signing module %s", moduleName) | |
| moduleBytes, err := os.ReadFile(modulePath) | |
| if err != nil { | |
| skippedModules = append(skippedModules, moduleName) | |
| glog.Errorf("failed to read module file (%s) [skipping]: %v", moduleName, err) | |
| continue | |
| } | |
| signedBytes, err := signModule(signer, moduleBytes, digest, cert, encryptAlg) | |
| if err != nil { | |
| skippedModules = append(skippedModules, moduleName) | |
| glog.Errorf("failed to sign module (%s) [skipping]: %v", moduleName, err) | |
| continue | |
| } | |
| if err := os.WriteFile(modulePath, signedBytes, 0644); err != nil { | |
| skippedModules = append(skippedModules, moduleName) | |
| glog.Errorf("failed to write signed module (%s) to file (%s) [skipping]: %v", moduleName, modulePath, err) | |
| continue | |
| } | |
| signedModules = append(signedModules, moduleName) | |
| glog.Infof("successfully signed module '%s'", moduleName) | |
| } | |
| 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) { | |
| // Retrieve pem encoded 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(signer ModuleSigner, moduleBytes []byte, digest digestAlgorithm, cert *x509.Certificate, encryptAlg *pkix.AlgorithmIdentifier) ([]byte, error) { | |
| signature, err := signer.Sign(moduleBytes, digest.hash) | |
| if err != nil { | |
| return nil, fmt.Errorf("failed to generate module signature: %v", err) | |
| } | |
| digestAlg := &pkix.AlgorithmIdentifier{Algorithm: digest.identifier, Parameters: asn1.NullRawValue} | |
| sinfo := signerInfo{ | |
| Version: 1, | |
| IssuerAndSerialNumber: issuerAndSerialNumber{ | |
| Issuer: asn1.RawValue{FullBytes: cert.RawIssuer}, | |
| SerialNumber: cert.SerialNumber, | |
| }, | |
| DigestAlgorithm: *digestAlg, | |
| DigestEncryptionAlgorithm: *encryptAlg, | |
| 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 nil, fmt.Errorf("failed to marshal signed module pkcs7 blob: %v", err) | |
| } | |
| return appendSignature(marshalled, moduleBytes) | |
| } | |
| // 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 | |
| } |